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