first commit
This commit is contained in:
+84
@@ -0,0 +1,84 @@
|
|||||||
|
package orm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func buildInsertSql(tablename string, fields []dbfield, dialect Dialect) string {
|
||||||
|
cols := make([]string, len(fields))
|
||||||
|
placeholders := make([]string, len(fields))
|
||||||
|
for i, f := range fields {
|
||||||
|
cols[i] = f.dbname
|
||||||
|
placeholders[i] = fmt.Sprintf("$%d", i+1)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"insert into %s (%s) values (%s)",
|
||||||
|
tablename,
|
||||||
|
strings.Join(cols, ","),
|
||||||
|
strings.Join(placeholders, ","),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildUpdateSql(tablename string, fields []dbfield, dialect Dialect) string {
|
||||||
|
predicates := make([]string, len(fields)-1)
|
||||||
|
var where string
|
||||||
|
i := 0
|
||||||
|
for _, f := range fields {
|
||||||
|
if f.dbname == "id" {
|
||||||
|
where = fmt.Sprintf("id=$%d and _version=$%d", len(fields), len(fields)+1)
|
||||||
|
} else {
|
||||||
|
predicates[i] = fmt.Sprintf("%s=$%d", f.dbname, i+1)
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"update %s set %s where %s",
|
||||||
|
tablename,
|
||||||
|
strings.Join(predicates, ","),
|
||||||
|
where,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildSelectbyidSql(tablename string, fields []dbfield, dialect Dialect) string {
|
||||||
|
columns := make([]string, len(fields))
|
||||||
|
var where string
|
||||||
|
for i, f := range fields {
|
||||||
|
columns[i] = fmt.Sprintf("%s", f.dbname)
|
||||||
|
}
|
||||||
|
where = "id=$1"
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"select %s from %s where %s",
|
||||||
|
strings.Join(columns, ","),
|
||||||
|
tablename,
|
||||||
|
where,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildDeletebyidSql(tablename string, fields []dbfield, dialect Dialect) string {
|
||||||
|
where := "id=$1"
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"delete from %s where %s",
|
||||||
|
tablename,
|
||||||
|
where,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildSelectWhere(tablename string, fields []dbfield, dialect Dialect) string {
|
||||||
|
columns := make([]string, len(fields))
|
||||||
|
for i, f := range fields {
|
||||||
|
columns[i] = fmt.Sprintf("%s", f.dbname)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"select %s from %s",
|
||||||
|
strings.Join(columns, ","),
|
||||||
|
tablename,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildDeleteWhere(tablename string, fields []dbfield, dialect Dialect) string {
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"delete from %s",
|
||||||
|
tablename,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package orm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SqliteDialect struct{}
|
||||||
|
|
||||||
|
func NewSqliteDialect() *SqliteDialect {
|
||||||
|
return &SqliteDialect{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertType(f dbfield) string {
|
||||||
|
switch f.stype {
|
||||||
|
case "string", "*string":
|
||||||
|
return "text"
|
||||||
|
case "bool", "int", "*int", "time.Time", "*time.Time":
|
||||||
|
return "numeric"
|
||||||
|
case "[]byte":
|
||||||
|
return "blob"
|
||||||
|
}
|
||||||
|
return "text"
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultValue(f dbfield) string {
|
||||||
|
if f.dbname == "_date_created" {
|
||||||
|
return "current_timestamp"
|
||||||
|
}
|
||||||
|
switch f.stype {
|
||||||
|
case "string":
|
||||||
|
return "''"
|
||||||
|
case "bool", "int", "time.Time":
|
||||||
|
return "0"
|
||||||
|
}
|
||||||
|
return "''"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *SqliteDialect) TableExists(db *sql.DB, tableName string) (bool, error) {
|
||||||
|
sql := "select name from sqlite_schema where type='table' and name=$1"
|
||||||
|
rows, err := db.Query(sql, tableName)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if rows.Next() {
|
||||||
|
rows.Close()
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *SqliteDialect) ColumnSpec(f dbfield) (string, error) {
|
||||||
|
if f.dbname == "id" {
|
||||||
|
return "id text not null primary key", nil
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(f.stype, "*") || strings.HasPrefix(f.stype, "[]") {
|
||||||
|
return fmt.Sprintf("%s %s", f.dbname, convertType(f)), nil
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s %s not null default %s", f.dbname, convertType(f), defaultValue(f)), nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
module trankilou.fr/lasebuche
|
||||||
|
|
||||||
|
go 1.26.5
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/sixafter/nanoid v1.64.5
|
||||||
|
modernc.org/sqlite v1.56.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.24 // indirect
|
||||||
|
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
|
github.com/sixafter/aes-ctr-drbg v1.19.2 // indirect
|
||||||
|
github.com/sixafter/prng-chacha v1.16.5 // indirect
|
||||||
|
golang.org/x/crypto v0.54.0 // indirect
|
||||||
|
golang.org/x/sys v0.47.0 // indirect
|
||||||
|
modernc.org/libc v1.74.4 // indirect
|
||||||
|
modernc.org/mathutil v1.7.1 // indirect
|
||||||
|
modernc.org/memory v1.11.0 // indirect
|
||||||
|
)
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
|
github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3 h1:LMLX+LgTNWpfvCBdFebv6EsYotImrt/Ppc5cXIriCSo=
|
||||||
|
github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3/go.mod h1:jl5iWTm0/hd5PjEYEOuwAJ57L/CibdZfrqZ5XA5GrCk=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||||
|
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||||
|
github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI=
|
||||||
|
github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A=
|
||||||
|
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||||
|
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
|
github.com/sixafter/aes-ctr-drbg v1.19.2 h1:+Bd1tS9/y/GapqgyyRjmrce6a5wUAQPusJXlAOh44yM=
|
||||||
|
github.com/sixafter/aes-ctr-drbg v1.19.2/go.mod h1:iOBiPPkiy5Z5cEWm2yCoqEtnkfafxNTokJLL0zOPZeQ=
|
||||||
|
github.com/sixafter/nanoid v1.64.5 h1:Aei4SU9i9I35EyJQawo9XYpzYoARGobQRU8EnBf/Pjo=
|
||||||
|
github.com/sixafter/nanoid v1.64.5/go.mod h1:+9tqjRHutec4D9mCKGvTaGSAxj1Q+eEHOvfp2WQgWa4=
|
||||||
|
github.com/sixafter/prng-chacha v1.16.5 h1:RNPMMVDq6rzBS+wDc9BAkpcKoVRhFGfBIwGRUCo5+Cc=
|
||||||
|
github.com/sixafter/prng-chacha v1.16.5/go.mod h1:fY7WtbBwx94oa5qN417ZzCLCozjQ7JDag7dr1f/v2Ss=
|
||||||
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
|
golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
|
||||||
|
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
|
||||||
|
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU=
|
||||||
|
golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU=
|
||||||
|
golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
|
||||||
|
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
|
||||||
|
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
|
||||||
|
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||||
|
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||||
|
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
|
golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
|
||||||
|
golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
modernc.org/cc/v4 v4.29.1 h1:MKgdCV3WykTSPqpVrnxdEDS0HEd2FHpKZDzxzU5LyeI=
|
||||||
|
modernc.org/cc/v4 v4.29.1/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
|
||||||
|
modernc.org/ccgo/v4 v4.34.6 h1:sBgfIwyN0TQ9C5hwIeuqyeAKyMWnbvj2fvpF4L11uzU=
|
||||||
|
modernc.org/ccgo/v4 v4.34.6/go.mod h1:SZ8YcN9NG7XVsQYdm6jYBvi8PQP1qi+kqB6OhjqI3Fk=
|
||||||
|
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
|
||||||
|
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
|
||||||
|
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||||
|
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||||
|
modernc.org/gc/v3 v3.1.4 h1:2g65LGVSmFQrXeITAw97x7hCRvZFcyE1uDP+7Vng7JI=
|
||||||
|
modernc.org/gc/v3 v3.1.4/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
||||||
|
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||||
|
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||||
|
modernc.org/libc v1.74.4 h1:fX1Omw4o2/1C2iRkkIsrQTasJQldLhRmuPreXLoWs9k=
|
||||||
|
modernc.org/libc v1.74.4/go.mod h1:eeQAS9W3sZeKYMFubydxJpII9ybHWshk+7or7bLG9co=
|
||||||
|
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||||
|
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||||
|
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||||
|
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||||
|
modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
|
||||||
|
modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||||
|
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||||
|
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||||
|
modernc.org/sqlite v1.56.0 h1:/D8e2RfFqoy/Zc6PuC76U28zFwmI/sYx1Kjm4yEn9e0=
|
||||||
|
modernc.org/sqlite v1.56.0/go.mod h1:yCJ2cmAaIkHQ25oXWrF8H4O1lIfPYPR26yCEDj2P3pQ=
|
||||||
|
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||||
|
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||||
|
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||||
|
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||||
@@ -0,0 +1,551 @@
|
|||||||
|
package orm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type dbfield struct {
|
||||||
|
name string
|
||||||
|
stype string
|
||||||
|
dbname string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Dialect interface {
|
||||||
|
TableExists(db *sql.DB, tableName string) (bool, error)
|
||||||
|
ColumnSpec(f dbfield) (string, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Table[T any] struct {
|
||||||
|
db *sql.DB
|
||||||
|
dialect Dialect
|
||||||
|
tablename string
|
||||||
|
fields []dbfield
|
||||||
|
insertsql string
|
||||||
|
updatesql string
|
||||||
|
selectbyid string
|
||||||
|
deletebyid string
|
||||||
|
selectwhere string
|
||||||
|
deletewhere string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTable[T any](db *sql.DB, dialect Dialect, sample T, tablename string) (Table[T], error) {
|
||||||
|
t := reflect.TypeOf(sample)
|
||||||
|
fields := make([]dbfield, 0)
|
||||||
|
for field := range t.Fields() {
|
||||||
|
f := dbfield{
|
||||||
|
name: field.Name,
|
||||||
|
stype: field.Type.String(),
|
||||||
|
dbname: field.Tag.Get("db"),
|
||||||
|
}
|
||||||
|
fmt.Println(f.name, f.stype, f.dbname)
|
||||||
|
fields = append(fields, f)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Table[T]{
|
||||||
|
db: db,
|
||||||
|
dialect: dialect,
|
||||||
|
tablename: tablename,
|
||||||
|
fields: fields,
|
||||||
|
insertsql: buildInsertSql(tablename, fields, dialect),
|
||||||
|
updatesql: buildUpdateSql(tablename, fields, dialect),
|
||||||
|
selectbyid: buildSelectbyidSql(tablename, fields, dialect),
|
||||||
|
deletebyid: buildDeletebyidSql(tablename, fields, dialect),
|
||||||
|
selectwhere: buildSelectWhere(tablename, fields, dialect),
|
||||||
|
deletewhere: buildDeleteWhere(tablename, fields, dialect),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Table[T]) Sync() error {
|
||||||
|
if exists, err := t.dialect.TableExists(t.db, t.tablename); exists {
|
||||||
|
return nil
|
||||||
|
} else if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
columns := make([]string, len(t.fields))
|
||||||
|
for i, f := range t.fields {
|
||||||
|
c, err := t.dialect.ColumnSpec(f)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
columns[i] = c
|
||||||
|
}
|
||||||
|
sql := fmt.Sprintf("create table %s (%s)", t.tablename, strings.Join(columns, ","))
|
||||||
|
_, err := t.db.Exec(sql)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Table[T]) Get(id string) (*T, error) {
|
||||||
|
row := t.db.QueryRow(t.selectbyid, id)
|
||||||
|
if row == nil {
|
||||||
|
return nil, sql.ErrNoRows
|
||||||
|
}
|
||||||
|
var obj T
|
||||||
|
|
||||||
|
// Use a slice of pointers to struct fields for scanning
|
||||||
|
// We need to pass pointers to each field in the order of the SQL columns
|
||||||
|
ptrVal := reflect.ValueOf(&obj).Elem()
|
||||||
|
fields := make([]any, len(t.fields))
|
||||||
|
|
||||||
|
for i, f := range t.fields {
|
||||||
|
fieldValue := ptrVal.FieldByName(f.name)
|
||||||
|
if !fieldValue.IsValid() {
|
||||||
|
return nil, fmt.Errorf("field %s not found in struct", f.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// For all fields, pass the address so Scan can set the value
|
||||||
|
// For pointer fields (e.g., *string), this gives **T
|
||||||
|
// For value fields (e.g., string), this gives *T
|
||||||
|
fields[i] = fieldValue.Addr().Interface()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := row.Scan(fields...); err != nil {
|
||||||
|
// Handle time scanning manually for SQLite which returns dates as strings
|
||||||
|
if strings.Contains(err.Error(), "unsupported Scan, storing driver.Value type string into type *time.Time") {
|
||||||
|
return t.getWithTimeHandling(id)
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &obj, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Table[T]) getWithTimeHandling(id string) (*T, error) {
|
||||||
|
row := t.db.QueryRow(t.selectbyid, id)
|
||||||
|
if row == nil {
|
||||||
|
return nil, sql.ErrNoRows
|
||||||
|
}
|
||||||
|
var obj T
|
||||||
|
|
||||||
|
ptrVal := reflect.ValueOf(&obj).Elem()
|
||||||
|
|
||||||
|
// Create a slice to hold all values, using string for time fields
|
||||||
|
tempFields := make([]any, len(t.fields))
|
||||||
|
fieldInfos := make([]struct {
|
||||||
|
index int
|
||||||
|
fieldValue reflect.Value
|
||||||
|
isTime bool
|
||||||
|
isTimePtr bool
|
||||||
|
}, len(t.fields))
|
||||||
|
|
||||||
|
for i, f := range t.fields {
|
||||||
|
fieldValue := ptrVal.FieldByName(f.name)
|
||||||
|
if !fieldValue.IsValid() {
|
||||||
|
return nil, fmt.Errorf("field %s not found in struct", f.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this is a time.Time or *time.Time field
|
||||||
|
isTime := fieldValue.Type() == reflect.TypeOf(time.Time{})
|
||||||
|
isTimePtr := fieldValue.Kind() == reflect.Ptr && fieldValue.Type().Elem() == reflect.TypeOf(time.Time{})
|
||||||
|
|
||||||
|
fieldInfos[i] = struct {
|
||||||
|
index int
|
||||||
|
fieldValue reflect.Value
|
||||||
|
isTime bool
|
||||||
|
isTimePtr bool
|
||||||
|
}{
|
||||||
|
index: i,
|
||||||
|
fieldValue: fieldValue,
|
||||||
|
isTime: isTime,
|
||||||
|
isTimePtr: isTimePtr,
|
||||||
|
}
|
||||||
|
|
||||||
|
// For time fields, use a string to capture the raw value
|
||||||
|
if isTime || isTimePtr {
|
||||||
|
var s sql.NullString
|
||||||
|
tempFields[i] = &s
|
||||||
|
} else {
|
||||||
|
tempFields[i] = fieldValue.Addr().Interface()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := row.Scan(tempFields...); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy values back, parsing time fields
|
||||||
|
for _, info := range fieldInfos {
|
||||||
|
if info.isTime || info.isTimePtr {
|
||||||
|
s := tempFields[info.index].(*sql.NullString)
|
||||||
|
if s.Valid {
|
||||||
|
t, err := time.Parse(time.RFC3339, s.String)
|
||||||
|
if err != nil {
|
||||||
|
// Try other common formats
|
||||||
|
t, err = time.Parse("2006-01-02 15:04:05", s.String)
|
||||||
|
if err != nil {
|
||||||
|
t, err = time.Parse("2006-01-02T15:04:05Z", s.String)
|
||||||
|
if err != nil {
|
||||||
|
continue // Skip if we can't parse
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if info.isTime {
|
||||||
|
info.fieldValue.Set(reflect.ValueOf(t))
|
||||||
|
} else if info.isTimePtr {
|
||||||
|
ptr := reflect.New(info.fieldValue.Type().Elem())
|
||||||
|
ptr.Elem().Set(reflect.ValueOf(t))
|
||||||
|
info.fieldValue.Set(ptr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &obj, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Table[T]) SelectOne(where string, args ...any) (*T, error) {
|
||||||
|
query := t.selectwhere + " where " + where + " limit 1"
|
||||||
|
row := t.db.QueryRow(query, args...)
|
||||||
|
if row == nil {
|
||||||
|
return nil, sql.ErrNoRows
|
||||||
|
}
|
||||||
|
|
||||||
|
var obj T
|
||||||
|
ptrVal := reflect.ValueOf(&obj).Elem()
|
||||||
|
fields := make([]any, len(t.fields))
|
||||||
|
|
||||||
|
for i, f := range t.fields {
|
||||||
|
fieldValue := ptrVal.FieldByName(f.name)
|
||||||
|
if !fieldValue.IsValid() {
|
||||||
|
return nil, fmt.Errorf("field %s not found in struct", f.name)
|
||||||
|
}
|
||||||
|
fields[i] = fieldValue.Addr().Interface()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := row.Scan(fields...); err != nil {
|
||||||
|
if strings.Contains(err.Error(), "unsupported Scan, storing driver.Value type string into type *time.Time") {
|
||||||
|
return t.selectOneWithTimeHandling(where, args...)
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &obj, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Table[T]) selectOneWithTimeHandling(where string, args ...any) (*T, error) {
|
||||||
|
query := t.selectwhere + " where " + where + " limit 1"
|
||||||
|
row := t.db.QueryRow(query, args...)
|
||||||
|
if row == nil {
|
||||||
|
return nil, sql.ErrNoRows
|
||||||
|
}
|
||||||
|
|
||||||
|
return t.scanWithTimeHandling(row)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Table[T]) SelectWhere(where string, args ...any) ([]*T, error) {
|
||||||
|
query := t.selectwhere
|
||||||
|
if where != "" {
|
||||||
|
query += " where " + where
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := t.db.Query(query, args...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var results []*T
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
var obj T
|
||||||
|
ptrVal := reflect.ValueOf(&obj).Elem()
|
||||||
|
fields := make([]any, len(t.fields))
|
||||||
|
for i, f := range t.fields {
|
||||||
|
fieldValue := ptrVal.FieldByName(f.name)
|
||||||
|
if !fieldValue.IsValid() {
|
||||||
|
return nil, fmt.Errorf("field %s not found in struct", f.name)
|
||||||
|
}
|
||||||
|
fields[i] = fieldValue.Addr().Interface()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := rows.Scan(fields...); err != nil {
|
||||||
|
if strings.Contains(err.Error(), "unsupported Scan, storing driver.Value type string into type *time.Time") {
|
||||||
|
// Need to re-execute with time handling
|
||||||
|
rows.Close()
|
||||||
|
return t.selectWhereWithTimeHandling(where, args...)
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
results = append(results, &obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Table[T]) selectWhereWithTimeHandling(where string, args ...any) ([]*T, error) {
|
||||||
|
query := t.selectwhere
|
||||||
|
if where != "" {
|
||||||
|
query += " where " + where
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := t.db.Query(query, args...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var results []*T
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
obj, err := t.scanWithTimeHandling(rows)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
results = append(results, obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Table[T]) scanWithTimeHandling(scanner any) (*T, error) {
|
||||||
|
var obj T
|
||||||
|
|
||||||
|
ptrVal := reflect.ValueOf(&obj).Elem()
|
||||||
|
|
||||||
|
// Create a slice to hold all values, using string for time fields
|
||||||
|
tempFields := make([]any, len(t.fields))
|
||||||
|
fieldInfos := make([]struct {
|
||||||
|
index int
|
||||||
|
fieldValue reflect.Value
|
||||||
|
isTime bool
|
||||||
|
isTimePtr bool
|
||||||
|
}, len(t.fields))
|
||||||
|
|
||||||
|
for i, f := range t.fields {
|
||||||
|
fieldValue := ptrVal.FieldByName(f.name)
|
||||||
|
if !fieldValue.IsValid() {
|
||||||
|
return nil, fmt.Errorf("field %s not found in struct", f.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this is a time.Time or *time.Time field
|
||||||
|
isTime := fieldValue.Type() == reflect.TypeOf(time.Time{})
|
||||||
|
var isTimePtr bool
|
||||||
|
if fieldValue.Kind() == reflect.Ptr {
|
||||||
|
isTimePtr = fieldValue.Type().Elem() == reflect.TypeOf(time.Time{})
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldInfos[i] = struct {
|
||||||
|
index int
|
||||||
|
fieldValue reflect.Value
|
||||||
|
isTime bool
|
||||||
|
isTimePtr bool
|
||||||
|
}{
|
||||||
|
index: i,
|
||||||
|
fieldValue: fieldValue,
|
||||||
|
isTime: isTime,
|
||||||
|
isTimePtr: isTimePtr,
|
||||||
|
}
|
||||||
|
|
||||||
|
// For time fields, use a string to capture the raw value
|
||||||
|
if isTime || isTimePtr {
|
||||||
|
var s sql.NullString
|
||||||
|
tempFields[i] = &s
|
||||||
|
} else {
|
||||||
|
tempFields[i] = fieldValue.Addr().Interface()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
switch s := scanner.(type) {
|
||||||
|
case *sql.Row:
|
||||||
|
err = s.Scan(tempFields...)
|
||||||
|
case *sql.Rows:
|
||||||
|
err = s.Scan(tempFields...)
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupported scanner type")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy values back, parsing time fields
|
||||||
|
for _, info := range fieldInfos {
|
||||||
|
if info.isTime || info.isTimePtr {
|
||||||
|
s := tempFields[info.index].(*sql.NullString)
|
||||||
|
if s.Valid {
|
||||||
|
t, err := time.Parse(time.RFC3339, s.String)
|
||||||
|
if err != nil {
|
||||||
|
// Try other common formats
|
||||||
|
t, err = time.Parse("2006-01-02 15:04:05", s.String)
|
||||||
|
if err != nil {
|
||||||
|
t, err = time.Parse("2006-01-02T15:04:05Z", s.String)
|
||||||
|
if err != nil {
|
||||||
|
// Try SQLite format: YYYY-MM-DD HH:MM:SS
|
||||||
|
t, err = time.Parse("2006-01-02 15:04:05.999999999-07:00", s.String)
|
||||||
|
if err != nil {
|
||||||
|
// Try simpler format
|
||||||
|
t, err = time.Parse("2006-01-02 15:04:05", s.String)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if info.isTime {
|
||||||
|
info.fieldValue.Set(reflect.ValueOf(t))
|
||||||
|
} else if info.isTimePtr {
|
||||||
|
ptr := reflect.New(info.fieldValue.Type().Elem())
|
||||||
|
ptr.Elem().Set(reflect.ValueOf(t))
|
||||||
|
info.fieldValue.Set(ptr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &obj, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Table[T]) Delete(id string) error {
|
||||||
|
_, err := t.db.Exec(t.deletebyid, id)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Table[T]) DeleteWhere(where string, args ...any) error {
|
||||||
|
query := t.deletewhere
|
||||||
|
if where != "" {
|
||||||
|
query += " where " + where
|
||||||
|
}
|
||||||
|
_, err := t.db.Exec(query, args...)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Table[T]) Insert(value *T) (*T, error) {
|
||||||
|
// Get a settable reflect value of the struct
|
||||||
|
// value is *T, &value is **T, .Elem() gives *T (settables), .Elem() gives T (settables)
|
||||||
|
val := reflect.ValueOf(&value).Elem().Elem()
|
||||||
|
|
||||||
|
// Auto-fill ID field if it exists and is empty
|
||||||
|
if fieldID := val.FieldByName("ID"); fieldID.IsValid() && fieldID.CanSet() && fieldID.Kind() == reflect.String {
|
||||||
|
if fieldID.String() == "" {
|
||||||
|
fieldID.SetString(GenID())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-fill VersionId field if it exists and is empty
|
||||||
|
if fieldVersion := val.FieldByName("VersionId"); fieldVersion.IsValid() && fieldVersion.CanSet() && fieldVersion.Kind() == reflect.String {
|
||||||
|
if fieldVersion.String() == "" {
|
||||||
|
fieldVersion.SetString(GenID())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-fill DateCreated or DateCreate field if it exists
|
||||||
|
now := time.Now()
|
||||||
|
if fieldCreate := val.FieldByName("DateCreated"); fieldCreate.IsValid() && fieldCreate.CanSet() && fieldCreate.Type() == reflect.TypeOf(now) {
|
||||||
|
fieldCreate.Set(reflect.ValueOf(now))
|
||||||
|
} else if fieldCreate := val.FieldByName("DateCreate"); fieldCreate.IsValid() && fieldCreate.CanSet() && fieldCreate.Type() == reflect.TypeOf(now) {
|
||||||
|
fieldCreate.Set(reflect.ValueOf(now))
|
||||||
|
}
|
||||||
|
|
||||||
|
vals := make([]any, len(t.fields))
|
||||||
|
for i, f := range t.fields {
|
||||||
|
fieldValue := val.FieldByName(f.name)
|
||||||
|
if !fieldValue.IsValid() {
|
||||||
|
return nil, fmt.Errorf("field %s not found in struct", f.name)
|
||||||
|
}
|
||||||
|
vals[i] = fieldValue.Interface()
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := t.db.Exec(t.insertsql, vals...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return value, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Table[T]) Update(value *T) (*T, error) {
|
||||||
|
// Get a settable reflect value of the struct
|
||||||
|
val := reflect.ValueOf(&value).Elem().Elem()
|
||||||
|
|
||||||
|
// Save old VersionId for WHERE clause
|
||||||
|
versionField := val.FieldByName("VersionId")
|
||||||
|
if !versionField.IsValid() {
|
||||||
|
return nil, fmt.Errorf("VersionId field not found")
|
||||||
|
}
|
||||||
|
oldVersionId := versionField.String()
|
||||||
|
|
||||||
|
// Generate new VersionId for optimistic locking
|
||||||
|
newVersionId := GenID()
|
||||||
|
versionField.SetString(newVersionId)
|
||||||
|
|
||||||
|
// Update DateUpdated field if it exists
|
||||||
|
if fieldUpdated := val.FieldByName("DateUpdated"); fieldUpdated.IsValid() && fieldUpdated.CanSet() {
|
||||||
|
if fieldUpdated.Kind() == reflect.Ptr {
|
||||||
|
now := time.Now()
|
||||||
|
if fieldUpdated.IsNil() {
|
||||||
|
fieldUpdated.Set(reflect.New(fieldUpdated.Type().Elem()))
|
||||||
|
}
|
||||||
|
fieldUpdated.Elem().Set(reflect.ValueOf(now))
|
||||||
|
} else if fieldUpdated.Type() == reflect.TypeOf(time.Time{}) {
|
||||||
|
fieldUpdated.Set(reflect.ValueOf(time.Now()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the values slice for the prepared statement
|
||||||
|
// UPDATE SQL expects: SET fields (excluding ID), then WHERE: ID and VersionId (old version)
|
||||||
|
vals := make([]any, 0, len(t.fields))
|
||||||
|
|
||||||
|
// Add all fields except ID for SET clause (includes new VersionId)
|
||||||
|
for _, f := range t.fields {
|
||||||
|
if f.dbname == "id" {
|
||||||
|
continue // Skip ID field for SET clause
|
||||||
|
}
|
||||||
|
fieldValue := val.FieldByName(f.name)
|
||||||
|
if !fieldValue.IsValid() {
|
||||||
|
return nil, fmt.Errorf("field %s not found in struct", f.name)
|
||||||
|
}
|
||||||
|
vals = append(vals, fieldValue.Interface())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add ID and OLD VersionId at the end for WHERE clause
|
||||||
|
idField := val.FieldByName("ID")
|
||||||
|
|
||||||
|
if !idField.IsValid() {
|
||||||
|
return nil, fmt.Errorf("ID field not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
vals = append(vals, idField.Interface(), oldVersionId)
|
||||||
|
|
||||||
|
result, err := t.db.Exec(t.updatesql, vals...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if any row was affected
|
||||||
|
rowsAffected, err := result.RowsAffected()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if rowsAffected == 0 {
|
||||||
|
return nil, fmt.Errorf("no rows affected: record not found or version mismatch")
|
||||||
|
}
|
||||||
|
|
||||||
|
return value, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Table[T]) Debug() {
|
||||||
|
fmt.Println("insert: ", t.insertsql)
|
||||||
|
fmt.Println("update: ", t.updatesql)
|
||||||
|
fmt.Println("selectbyid: ", t.selectbyid)
|
||||||
|
fmt.Println("deletebyid: ", t.deletebyid)
|
||||||
|
fmt.Println("selectwhere: ", t.selectwhere)
|
||||||
|
fmt.Println("deletewhere: ", t.deletewhere)
|
||||||
|
}
|
||||||
+460
@@ -0,0 +1,460 @@
|
|||||||
|
package orm_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
_ "modernc.org/sqlite"
|
||||||
|
orm "trankilou.fr/lasebuche"
|
||||||
|
)
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
ID string `db:"id" json:"id"`
|
||||||
|
Firstname string `db:"firstname" json:"firstname,omitempty"`
|
||||||
|
Lastname string `db:"lastname" json:"lastname,omitempty"`
|
||||||
|
Password string `db:"password" json:"-"`
|
||||||
|
Email string `db:"email" json:"email,omitempty"`
|
||||||
|
PictureID *string `db:"picture" json:"picture,omitempty"`
|
||||||
|
Enabled bool `db:"enabled" json:"enabled"`
|
||||||
|
Theme string `db:"theme" json:"theme"`
|
||||||
|
Lang string `db:"lang" json:"lang"`
|
||||||
|
Content []byte `db:"content" json:"content"`
|
||||||
|
DateCreated time.Time `db:"_date_created" json:"_date_created"`
|
||||||
|
DateUpdated *time.Time `db:"_date_updated" json:"_date_updated"`
|
||||||
|
VersionId string `db:"_version" json:"_version"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHelloName(t *testing.T) {
|
||||||
|
db, err := sql.Open("sqlite", "testdb")
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err.Error())
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
dialect := orm.NewSqliteDialect()
|
||||||
|
tableUser, err := orm.NewTable[User](db, dialect, User{}, "user")
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
tableUser.Debug()
|
||||||
|
|
||||||
|
err = tableUser.Sync()
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
user := &User{
|
||||||
|
Firstname: "fabien",
|
||||||
|
Lastname: "masson",
|
||||||
|
Email: "masson.fabien@gmail.com",
|
||||||
|
}
|
||||||
|
user, err = tableUser.Insert(user)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
id := user.ID
|
||||||
|
user2, err := tableUser.Get(id)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err.Error())
|
||||||
|
}
|
||||||
|
if user2.Firstname != "fabien" {
|
||||||
|
t.Errorf("GetByID : expected %s, got %s", "fabien", user2.Firstname)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test SelectOne
|
||||||
|
user3, err := tableUser.SelectOne("firstname = $1", "fabien")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("SelectOne failed: %v", err)
|
||||||
|
}
|
||||||
|
if user3 == nil {
|
||||||
|
t.Error("SelectOne returned nil")
|
||||||
|
}
|
||||||
|
if user3.Firstname != "fabien" {
|
||||||
|
t.Errorf("SelectOne: expected firstname %s, got %s", "fabien", user3.Firstname)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test SelectWhere
|
||||||
|
users, err := tableUser.SelectWhere("firstname = $1", "fabien")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("SelectWhere failed: %v", err)
|
||||||
|
}
|
||||||
|
if len(users) == 0 {
|
||||||
|
t.Error("SelectWhere returned no results")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test Update
|
||||||
|
user.Firstname = "Fabien"
|
||||||
|
updatedUser, err := tableUser.Update(user)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Update failed: %v", err)
|
||||||
|
}
|
||||||
|
if updatedUser.Firstname != "Fabien" {
|
||||||
|
t.Errorf("Update: expected firstname %s, got %s", "Fabien", updatedUser.Firstname)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify update with Get
|
||||||
|
user4, err := tableUser.Get(id)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Get after update failed: %v", err)
|
||||||
|
}
|
||||||
|
if user4.Firstname != "Fabien" {
|
||||||
|
t.Errorf("After update, Get: expected firstname %s, got %s", "Fabien", user4.Firstname)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test Delete
|
||||||
|
err = tableUser.Delete(id)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Delete failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify delete
|
||||||
|
_, err = tableUser.Get(id)
|
||||||
|
if err != sql.ErrNoRows {
|
||||||
|
t.Errorf("After delete, expected ErrNoRows, got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test DeleteWhere - insert another user first
|
||||||
|
user5 := &User{
|
||||||
|
Firstname: "Test",
|
||||||
|
Lastname: "User",
|
||||||
|
Email: "test@example.com",
|
||||||
|
}
|
||||||
|
user5, err = tableUser.Insert(user5)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Insert for DeleteWhere test failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the test user
|
||||||
|
err = tableUser.DeleteWhere("firstname = $1", "Test")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("DeleteWhere failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify delete
|
||||||
|
_, err = tableUser.Get(user5.ID)
|
||||||
|
if err != sql.ErrNoRows {
|
||||||
|
t.Errorf("After DeleteWhere, expected ErrNoRows, got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCRUDOperations(t *testing.T) {
|
||||||
|
db, err := sql.Open("sqlite", "testdb")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err.Error())
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
dialect := orm.NewSqliteDialect()
|
||||||
|
tableUser, err := orm.NewTable[User](db, dialect, User{}, "user")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up before test
|
||||||
|
_, err = db.Exec("DELETE FROM user")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test Insert
|
||||||
|
user1 := &User{
|
||||||
|
Firstname: "John",
|
||||||
|
Lastname: "Doe",
|
||||||
|
Email: "john.doe@example.com",
|
||||||
|
Enabled: true,
|
||||||
|
Theme: "dark",
|
||||||
|
Lang: "en",
|
||||||
|
}
|
||||||
|
|
||||||
|
insertedUser, err := tableUser.Insert(user1)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Insert failed: %v", err)
|
||||||
|
}
|
||||||
|
if insertedUser.ID == "" {
|
||||||
|
t.Error("Inserted user should have an ID")
|
||||||
|
}
|
||||||
|
if insertedUser.VersionId == "" {
|
||||||
|
t.Error("Inserted user should have a VersionId")
|
||||||
|
}
|
||||||
|
if insertedUser.DateCreated.IsZero() {
|
||||||
|
t.Error("Inserted user should have DateCreated set")
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("Inserted user with ID: %s, VersionId: %s", insertedUser.ID, insertedUser.VersionId)
|
||||||
|
|
||||||
|
// Test Get
|
||||||
|
fetchedUser, err := tableUser.Get(insertedUser.ID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Get failed: %v", err)
|
||||||
|
}
|
||||||
|
if fetchedUser.ID != insertedUser.ID {
|
||||||
|
t.Errorf("Get: expected ID %s, got %s", insertedUser.ID, fetchedUser.ID)
|
||||||
|
}
|
||||||
|
if fetchedUser.Firstname != "John" {
|
||||||
|
t.Errorf("Get: expected Firstname John, got %s", fetchedUser.Firstname)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test Update
|
||||||
|
fetchedUser.Firstname = "Jane"
|
||||||
|
updatedUser, err := tableUser.Update(fetchedUser)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Update failed: %v", err)
|
||||||
|
}
|
||||||
|
if updatedUser.Firstname != "Jane" {
|
||||||
|
t.Errorf("Update: expected Firstname Jane, got %s", updatedUser.Firstname)
|
||||||
|
}
|
||||||
|
if updatedUser.DateUpdated == nil {
|
||||||
|
t.Error("Update should have set DateUpdated")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify update
|
||||||
|
verifiedUser, err := tableUser.Get(insertedUser.ID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Get after update failed: %v", err)
|
||||||
|
}
|
||||||
|
if verifiedUser.Firstname != "Jane" {
|
||||||
|
t.Errorf("After update, Get: expected Firstname Jane, got %s", verifiedUser.Firstname)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test SelectOne
|
||||||
|
selectedUser, err := tableUser.SelectOne("email = $1", "john.doe@example.com")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("SelectOne failed: %v", err)
|
||||||
|
}
|
||||||
|
if selectedUser == nil {
|
||||||
|
t.Error("SelectOne returned nil")
|
||||||
|
}
|
||||||
|
if selectedUser.Email != "john.doe@example.com" {
|
||||||
|
t.Errorf("SelectOne: expected email john.doe@example.com, got %s", selectedUser.Email)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert another user for SelectWhere test
|
||||||
|
user2 := &User{
|
||||||
|
Firstname: "Alice",
|
||||||
|
Lastname: "Smith",
|
||||||
|
Email: "alice.smith@example.com",
|
||||||
|
}
|
||||||
|
_, err = tableUser.Insert(user2)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Insert user2 failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test SelectWhere
|
||||||
|
users, err := tableUser.SelectWhere("")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("SelectWhere failed: %v", err)
|
||||||
|
}
|
||||||
|
if len(users) < 2 {
|
||||||
|
t.Errorf("SelectWhere: expected at least 2 users, got %d", len(users))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test SelectWhere with condition
|
||||||
|
filteredUsers, err := tableUser.SelectWhere("firstname = $1", "Jane")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("SelectWhere with condition failed: %v", err)
|
||||||
|
}
|
||||||
|
if len(filteredUsers) != 1 {
|
||||||
|
t.Errorf("SelectWhere with condition: expected 1 user, got %d", len(filteredUsers))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test Delete
|
||||||
|
err = tableUser.Delete(insertedUser.ID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Delete failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify deletion
|
||||||
|
_, err = tableUser.Get(insertedUser.ID)
|
||||||
|
if err != sql.ErrNoRows {
|
||||||
|
t.Errorf("After Delete, expected ErrNoRows, got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test DeleteWhere
|
||||||
|
err = tableUser.DeleteWhere("firstname = $1", "Alice")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("DeleteWhere failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify deletion
|
||||||
|
_, err = tableUser.Get(user2.ID)
|
||||||
|
if err != sql.ErrNoRows {
|
||||||
|
t.Errorf("After DeleteWhere, expected ErrNoRows, got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
_, err = db.Exec("DELETE FROM user")
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("Cleanup failed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEdgeCases(t *testing.T) {
|
||||||
|
db, err := sql.Open("sqlite", "testdb")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err.Error())
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
dialect := orm.NewSqliteDialect()
|
||||||
|
tableUser, err := orm.NewTable[User](db, dialect, User{}, "user")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up before test
|
||||||
|
_, err = db.Exec("DELETE FROM user")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test SelectOne with no results
|
||||||
|
user, err := tableUser.SelectOne("firstname = $1", "NonExistent")
|
||||||
|
if err != nil && err != sql.ErrNoRows {
|
||||||
|
t.Errorf("SelectOne with no results: expected error or nil, got: %v", err)
|
||||||
|
}
|
||||||
|
if user != nil {
|
||||||
|
t.Error("SelectOne with no results should return nil user")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test SelectWhere with no results
|
||||||
|
users, err := tableUser.SelectWhere("firstname = $1", "NonExistent")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("SelectWhere with no results: unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if len(users) != 0 {
|
||||||
|
t.Errorf("SelectWhere with no results: expected 0 users, got %d", len(users))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test Get with non-existent ID
|
||||||
|
_, err = tableUser.Get("non-existent-id")
|
||||||
|
if err != sql.ErrNoRows {
|
||||||
|
t.Errorf("Get with non-existent ID: expected ErrNoRows, got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test Delete with non-existent ID (should not error, just affect 0 rows)
|
||||||
|
err = tableUser.Delete("non-existent-id")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Delete with non-existent ID: unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test DeleteWhere with no matching rows
|
||||||
|
err = tableUser.DeleteWhere("firstname = $1", "NonExistent")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("DeleteWhere with no matching rows: unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test Update with non-existent ID (will fail due to WHERE clause)
|
||||||
|
nonExistentUser := &User{
|
||||||
|
ID: "non-existent-id",
|
||||||
|
Firstname: "Test",
|
||||||
|
VersionId: "test-version",
|
||||||
|
}
|
||||||
|
_, err = tableUser.Update(nonExistentUser)
|
||||||
|
// This should fail because the row doesn't exist
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Update with non-existent ID should have failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
_, err = db.Exec("DELETE FROM user")
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("Cleanup failed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVersionLocking(t *testing.T) {
|
||||||
|
db, err := sql.Open("sqlite", "testdb")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err.Error())
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
dialect := orm.NewSqliteDialect()
|
||||||
|
tableUser, err := orm.NewTable[User](db, dialect, User{}, "user")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up before test
|
||||||
|
_, err = db.Exec("DELETE FROM user")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert a user
|
||||||
|
user := &User{
|
||||||
|
Firstname: "Version",
|
||||||
|
Lastname: "Test",
|
||||||
|
Email: "version@test.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
insertedUser, err := tableUser.Insert(user)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Insert failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
originalVersionId := insertedUser.VersionId
|
||||||
|
t.Logf("Original VersionId: %s", originalVersionId)
|
||||||
|
|
||||||
|
// Fetch the user to get fresh data
|
||||||
|
fetchedUser, err := tableUser.Get(insertedUser.ID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Get failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// First update should succeed with correct version
|
||||||
|
fetchedUser.Firstname = "Version2"
|
||||||
|
updatedUser, err := tableUser.Update(fetchedUser)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("First update failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// VersionId should have changed
|
||||||
|
if updatedUser.VersionId == originalVersionId {
|
||||||
|
t.Error("VersionId should have changed after update")
|
||||||
|
}
|
||||||
|
|
||||||
|
newVersionId := updatedUser.VersionId
|
||||||
|
t.Logf("New VersionId after first update: %s", newVersionId)
|
||||||
|
|
||||||
|
// Second update with correct version should succeed
|
||||||
|
updatedUser.Firstname = "Version3"
|
||||||
|
updatedUser2, err := tableUser.Update(updatedUser)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Second update failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// VersionId should have changed again
|
||||||
|
if updatedUser2.VersionId == newVersionId {
|
||||||
|
t.Error("VersionId should have changed after second update")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now try to update with old version - should fail
|
||||||
|
// Create a new user with the old version
|
||||||
|
oldVersionUser := &User{
|
||||||
|
ID: insertedUser.ID,
|
||||||
|
Firstname: "ShouldFail",
|
||||||
|
Lastname: "Test",
|
||||||
|
Email: "version@test.com",
|
||||||
|
VersionId: originalVersionId, // Old version
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = tableUser.Update(oldVersionUser)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Update with old VersionId should have failed")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "version mismatch") {
|
||||||
|
t.Errorf("Expected version mismatch error, got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
_, err = db.Exec("DELETE FROM user")
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("Cleanup failed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
package orm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
|
"github.com/sixafter/nanoid"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GenID() string {
|
||||||
|
id, err := nanoid.New()
|
||||||
|
if err != nil {
|
||||||
|
// Fallback to random hex
|
||||||
|
b := make([]byte, 16)
|
||||||
|
if _, err := rand.Read(b); err != nil {
|
||||||
|
return "fallback-id"
|
||||||
|
}
|
||||||
|
return hex.EncodeToString(b)
|
||||||
|
}
|
||||||
|
return id.String()
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user