You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
225 lines
5.0 KiB
Go
225 lines
5.0 KiB
Go
2 years ago
|
// This package provides a small framework to work with SQL databases using
|
||
|
// generics. The most important part for now is the "Ref[T]" type that lets
|
||
|
// us pass around typed IDs to table rows in a type safe manner.
|
||
|
//
|
||
|
// For now this library uses both generics for keeping the interface type safe
|
||
|
// and reflection to extract automatically some metedata from structs that
|
||
|
// represent tables.
|
||
|
package db
|
||
|
|
||
|
import (
|
||
|
"database/sql"
|
||
|
"fmt"
|
||
|
"reflect"
|
||
|
"strings"
|
||
|
)
|
||
|
|
||
|
// Option è un valore che nel database può potenzialmente essere "NULL"
|
||
|
//
|
||
|
// NOTE/TODO: Magari si può fare una cosa fatta bene e nascondere
|
||
|
// l'implementazione al Go, ad esempio Option[string] può essere NULL o testo
|
||
|
// nel db mentre Option[time.Time] può essere codificato in json come
|
||
|
//
|
||
|
// "{ present: false }"
|
||
|
//
|
||
|
// oppure
|
||
|
//
|
||
|
// "{ present: true, value: '2023/04/...' }"
|
||
|
//
|
||
|
// in modo da semplificare leggermente la processazione in Go. Altrimenti si
|
||
|
// può separare in Option[T] e Nullable[T].
|
||
|
type Option[T any] struct {
|
||
|
present bool
|
||
|
value T
|
||
|
}
|
||
|
|
||
|
func NewOption[T any](value T) Option[T] {
|
||
|
return Option[T]{true, value}
|
||
|
}
|
||
|
|
||
|
func NewEmptyOption[T any]() Option[T] {
|
||
|
return Option[T]{present: false}
|
||
|
}
|
||
|
|
||
|
func (Option[T]) SqlType() string {
|
||
|
return "BLOB"
|
||
|
}
|
||
|
|
||
|
// func (v *Option[T]) Scan(a any) error {
|
||
|
// data, ok := a.([]byte)
|
||
|
// if !ok {
|
||
|
// return fmt.Errorf(`scan expected []byte`)
|
||
|
// }
|
||
|
|
||
|
// m := map[string]any{}
|
||
|
// if err := json.Unmarshal(data, m); err != nil {
|
||
|
// return err
|
||
|
// }
|
||
|
|
||
|
// if present, _ := m["present"].(bool); present {
|
||
|
|
||
|
// m["value"]
|
||
|
// }
|
||
|
|
||
|
// return nil
|
||
|
// }
|
||
|
|
||
|
// Ref è un id tipato da un'entità presente nel database. È il tipo fondamentale
|
||
|
// esportato da questo package. Non dovrebbe essere troppo importante ma
|
||
|
// internamente è una stringa, inoltre è consigliato che una struct
|
||
|
// con _primary key_ abbia un campo di tipo Ref a se stessa.
|
||
|
//
|
||
|
// type User struct {
|
||
|
// Id db.Ref[User] // meglio se unico e immutabile
|
||
|
// ...
|
||
|
// }
|
||
|
type Ref[T any] string
|
||
|
|
||
|
func (Ref[T]) SqlType() string {
|
||
|
return "TEXT"
|
||
|
}
|
||
|
|
||
|
func (v *Ref[T]) Scan(a any) error {
|
||
|
s, ok := a.(string)
|
||
|
if !ok {
|
||
|
return fmt.Errorf(`scan expected string`)
|
||
|
}
|
||
|
|
||
|
*v = Ref[T](s)
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
type sqlValue interface {
|
||
|
SqlType() string
|
||
|
sql.Scanner
|
||
|
}
|
||
|
|
||
|
type Table[T any] struct {
|
||
|
Name string
|
||
|
PrimaryKey func(value *T, pk ...string) Ref[T]
|
||
|
Columns [][2]string
|
||
|
}
|
||
|
|
||
|
var commonTypes = map[string]string{
|
||
|
"string": "TEXT",
|
||
|
"int": "INTEGER",
|
||
|
"int32": "INTEGER",
|
||
|
"int64": "INTEGER",
|
||
|
"float32": "REAL",
|
||
|
"float64": "REAL",
|
||
|
}
|
||
|
|
||
|
func toSqlType(typ reflect.Type) string {
|
||
|
st, ok := reflect.New(typ).Interface().(sqlValue)
|
||
|
if ok {
|
||
|
return st.SqlType()
|
||
|
}
|
||
|
|
||
|
return commonTypes[typ.Name()]
|
||
|
}
|
||
|
|
||
|
func AutoTable[T any](name string) Table[T] {
|
||
|
columns := [][2]string{}
|
||
|
pkIndex := -1
|
||
|
|
||
|
var zero T
|
||
|
typ := reflect.TypeOf(zero)
|
||
|
for i := 0; i < typ.NumField(); i++ {
|
||
|
fieldTyp := typ.Field(i)
|
||
|
isPK, _, typ, col := fieldInfo(fieldTyp)
|
||
|
if isPK {
|
||
|
pkIndex = i
|
||
|
}
|
||
|
columns = append(columns, [2]string{col, toSqlType(typ)})
|
||
|
}
|
||
|
|
||
|
if pkIndex == -1 {
|
||
|
panic(fmt.Sprintf("struct %T has no primary key field", zero))
|
||
|
}
|
||
|
|
||
|
// debug logging
|
||
|
// log.Printf("[auto table] struct %T has primary key %q", zero, typ.Field(pkIndex).Name)
|
||
|
|
||
|
return Table[T]{
|
||
|
Name: name,
|
||
|
PrimaryKey: func(value *T, pk ...string) Ref[T] {
|
||
|
// SAFETY: this is required to cast *Ref[T] to *string, internally
|
||
|
// they are the same type so this should be safe
|
||
|
ptr := reflect.ValueOf(value).Elem().Field(pkIndex).Addr().UnsafePointer()
|
||
|
pkPtr := (*string)(ptr)
|
||
|
|
||
|
if len(pk) > 0 {
|
||
|
*pkPtr = pk[0]
|
||
|
}
|
||
|
|
||
|
return Ref[T](*pkPtr)
|
||
|
},
|
||
|
Columns: columns,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (table Table[T]) CreateIfNotExists(dbConn *sql.DB) error {
|
||
|
columns := make([]string, len(table.Columns))
|
||
|
|
||
|
for i, col := range table.Columns {
|
||
|
columns[i] = fmt.Sprintf("%s %s", col[0], col[1])
|
||
|
}
|
||
|
|
||
|
stmt := fmt.Sprintf(`CREATE TABLE IF NOT EXISTS %s(%s)`,
|
||
|
table.Name,
|
||
|
strings.Join(columns, ", "),
|
||
|
)
|
||
|
|
||
|
_, err := dbConn.Exec(stmt)
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
func Create[T any](dbConn *sql.DB, table Table[T], value T) (Ref[T], error) {
|
||
|
id := randomHex(15)
|
||
|
|
||
|
table.PrimaryKey(&value, id)
|
||
|
|
||
|
columns, values := structDatabaseColumnValues(&value)
|
||
|
|
||
|
stmt := fmt.Sprintf(`INSERT INTO %s(%s) VALUES (%s)`,
|
||
|
table.Name,
|
||
|
strings.Join(columns, ", "),
|
||
|
strings.Join(repeatSlice("?", len(columns)), ", "),
|
||
|
)
|
||
|
|
||
|
if _, err := dbConn.Exec(stmt, values...); err != nil {
|
||
|
return "", err
|
||
|
}
|
||
|
|
||
|
return Ref[T](id), nil
|
||
|
}
|
||
|
|
||
|
func Insert[T any](dbConn *sql.DB, table Table[T], value T) error {
|
||
|
columns, values := structDatabaseColumnValues(&value)
|
||
|
|
||
|
stmt := fmt.Sprintf(`INSERT INTO %s(%s) VALUES (%s)`,
|
||
|
table.Name,
|
||
|
strings.Join(columns, ", "),
|
||
|
strings.Join(repeatSlice("?", len(columns)), ", "),
|
||
|
)
|
||
|
|
||
|
_, err := dbConn.Exec(stmt, values...)
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
func Read[T any](dbConn *sql.DB, table Table[T], id Ref[T]) (T, error) {
|
||
|
panic("todo")
|
||
|
}
|
||
|
|
||
|
func ReadAll[T any](dbConn *sql.DB, table Table[T]) ([]T, error) {
|
||
|
panic("todo")
|
||
|
}
|
||
|
|
||
|
func Update[T any](dbConn *sql.DB, table Table[T], id Ref[T], value T) error {
|
||
|
panic("todo")
|
||
|
}
|
||
|
|
||
|
func Delete[T any](dbConn *sql.DB, table Table[T], id Ref[T]) error {
|
||
|
panic("todo")
|
||
|
}
|