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.
website/libs/db/db.go

225 lines
5.0 KiB
Go

// 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")
}