diff --git a/pkg/__test__/builder_test.go b/pkg/__test__/builder_test.go new file mode 100644 index 0000000..5108013 --- /dev/null +++ b/pkg/__test__/builder_test.go @@ -0,0 +1,53 @@ +package tests + +import ( + "fmt" + "testing" + + _ "github.com/mattn/go-sqlite3" + "l12.xyz/dal/adapter" + "l12.xyz/dal/builder" +) + +func TestBuilderBasic(t *testing.T) { + a := adapter.DBAdapter{Type: "sqlite3"} + db, err := a.Open("file::memory:?cache=shared") + if err != nil { + t.Fatalf("failed to open db: %v", err) + } + defer db.Close() + _, err = db.Exec("CREATE TABLE test (id INTEGER PRIMARY KEY, name BLOB)") + if err != nil { + t.Fatalf("failed to create table: %v", err) + } + + insert, values := builder.New(adapter.SQLite{}).In("test t").Insert([]builder.Map{ + {"name": "a"}, + {"name": 'b'}, + }).Sql() + fmt.Println(insert, values) + _, err = db.Exec(insert, values...) + if err != nil { + t.Fatalf("failed to insert data: %v", err) + } + + expr, _ := builder.New(adapter.SQLite{}).In("test t").Find(builder.Find{"name": builder.Is{"$in": []interface{}{"a", 'b'}}}).Sql() + fmt.Println(expr) + rows, err := a.Query(adapter.Query{ + Db: "file::memory:?cache=shared", + Expression: expr, + }) + if err != nil { + t.Fatalf("failed to query data: %v", err) + } + defer rows.Close() + for rows.Next() { + var id int + var name string + err = rows.Scan(&id, &name) + if err != nil { + t.Fatalf("failed to scan row: %v", err) + } + fmt.Printf("id: %d, name: %s\n", id, name) + } +} diff --git a/pkg/__test__/go.mod b/pkg/__test__/go.mod index 4921031..31a8e1e 100644 --- a/pkg/__test__/go.mod +++ b/pkg/__test__/go.mod @@ -2,6 +2,7 @@ module l12.xyz/dal/tests go 1.22.6 +require l12.xyz/dal/builder v0.0.0 replace l12.xyz/dal/builder v0.0.0 => ../builder replace l12.xyz/dal/utils v0.0.0 => ../utils @@ -9,9 +10,11 @@ replace l12.xyz/dal/utils v0.0.0 => ../utils require l12.xyz/dal/adapter v0.0.0 replace l12.xyz/dal/adapter v0.0.0 => ../adapter +replace l12.xyz/dal/filters v0.0.0 => ../filters require ( github.com/mattn/go-sqlite3 v1.14.22 github.com/pkg/errors v0.9.1 // indirect l12.xyz/dal/utils v0.0.0 // indirect + l12.xyz/dal/filters v0.0.0 // indirect ) diff --git a/pkg/adapter/SQLiteContext.go b/pkg/adapter/SQLite.go similarity index 76% rename from pkg/adapter/SQLiteContext.go rename to pkg/adapter/SQLite.go index a8a1914..150dece 100644 --- a/pkg/adapter/SQLiteContext.go +++ b/pkg/adapter/SQLite.go @@ -7,13 +7,13 @@ import ( utils "l12.xyz/dal/utils" ) -type SQLiteContext struct { +type SQLite struct { TableName string TableAlias string FieldName string } -func (c SQLiteContext) New(opts CtxOpts) Context { +func (c SQLite) New(opts DialectOpts) Dialect { tn := opts["TableName"] if tn == "" { tn = c.TableName @@ -26,18 +26,18 @@ func (c SQLiteContext) New(opts CtxOpts) Context { if fn == "" { fn = c.FieldName } - return SQLiteContext{ + return SQLite{ TableName: tn, TableAlias: ta, FieldName: fn, } } -func (c SQLiteContext) GetTableName() string { +func (c SQLite) GetTableName() string { return c.TableName } -func (c SQLiteContext) GetFieldName() string { +func (c SQLite) GetFieldName() string { if strings.Contains(c.FieldName, ".") { return c.FieldName } @@ -47,7 +47,7 @@ func (c SQLiteContext) GetFieldName() string { return c.FieldName } -func (c SQLiteContext) GetColumnName(key string) string { +func (c SQLite) GetColumnName(key string) string { if strings.Contains(key, ".") { return key } @@ -57,7 +57,7 @@ func (c SQLiteContext) GetColumnName(key string) string { return key } -func (c SQLiteContext) NormalizeValue(value interface{}) interface{} { +func (c SQLite) NormalizeValue(value interface{}) interface{} { str, ok := value.(string) if utils.IsSQLFunction(str) { return str diff --git a/pkg/adapter/types.go b/pkg/adapter/types.go index ecbd57c..d9869de 100644 --- a/pkg/adapter/types.go +++ b/pkg/adapter/types.go @@ -6,10 +6,10 @@ type Query struct { Data []interface{} `json:"data"` } -type CtxOpts map[string]string +type DialectOpts map[string]string -type Context interface { - New(opts CtxOpts) Context +type Dialect interface { + New(opts DialectOpts) Dialect GetTableName() string GetFieldName() string GetColumnName(key string) string diff --git a/pkg/builder/Builder.go b/pkg/builder/Builder.go index a8d0b64..4fc3da6 100644 --- a/pkg/builder/Builder.go +++ b/pkg/builder/Builder.go @@ -9,7 +9,7 @@ type Builder struct { TableName string TableAlias string Parts SQLParts - Ctx Context + Dialect Dialect } type SQLParts struct { @@ -28,20 +28,20 @@ type SQLParts struct { Update UpdateData } -func New(ctx Context) *Builder { +func New(dialect Dialect) *Builder { return &Builder{ Parts: SQLParts{ Operation: "SELECT", From: "FROM", }, - Ctx: ctx, + Dialect: dialect, } } func (b *Builder) In(table string) *Builder { b.TableName, b.TableAlias = getTableAlias(table) b.Parts.FromExp = table - b.Ctx = b.Ctx.New(CtxOpts{ + b.Dialect = b.Dialect.New(DialectOpts{ "TableName": b.TableName, "TableAlias": b.TableAlias, }) @@ -50,7 +50,7 @@ func (b *Builder) In(table string) *Builder { func (b *Builder) Find(query Find) *Builder { b.Parts.FiterExp = covertFind( - b.Ctx, + b.Dialect, query, ) if b.Parts.Operation == "" { @@ -79,18 +79,18 @@ func (b *Builder) Fields(fields ...Map) *Builder { } func (b *Builder) Join(joins ...interface{}) *Builder { - b.Parts.JoinExps = convertJoin(b.Ctx, joins...) + b.Parts.JoinExps = convertJoin(b.Dialect, joins...) return b } func (b *Builder) Group(keys ...string) *Builder { b.Parts.HavingExp = "HAVING" - b.Parts.GroupExp = convertGroup(b.Ctx, keys) + b.Parts.GroupExp = convertGroup(b.Dialect, keys) return b } func (b *Builder) Sort(sort Map) *Builder { - b.Parts.OrderExp, _ = convertSort(b.Ctx, sort) + b.Parts.OrderExp, _ = convertSort(b.Dialect, sort) return b } @@ -110,7 +110,7 @@ func (b *Builder) Delete() *Builder { } func (b *Builder) Insert(inserts []Map) *Builder { - insertData, _ := convertInsert(b.Ctx, inserts) + insertData, _ := convertInsert(b.Dialect, inserts) b.Parts = SQLParts{ Operation: "INSERT INTO", Insert: insertData, @@ -119,7 +119,7 @@ func (b *Builder) Insert(inserts []Map) *Builder { } func (b *Builder) Set(updates Map) *Builder { - updateData := convertUpdate(b.Ctx, updates) + updateData := convertUpdate(b.Dialect, updates) b.Parts = SQLParts{ Operation: "UPDATE", Update: updateData, @@ -133,7 +133,7 @@ func (b *Builder) Update(updates Map) *Builder { func (b *Builder) OnConflict(fields ...string) *Builder { if b.Parts.Operation == "UPDATE" { - b.Parts.Update.Upsert = convertConflict(b.Ctx, fields...) + b.Parts.Update.Upsert = convertConflict(b.Dialect, fields...) b.Parts.Update.UpsertExp = "DO NOTHING" } else { panic("OnConflict is only available for UPDATE operation") diff --git a/pkg/builder/convert_conflict.go b/pkg/builder/convert_conflict.go index d97f439..e23a58b 100644 --- a/pkg/builder/convert_conflict.go +++ b/pkg/builder/convert_conflict.go @@ -7,7 +7,7 @@ import ( utils "l12.xyz/dal/utils" ) -func convertConflict(ctx Context, fields ...string) string { +func convertConflict(ctx Dialect, fields ...string) string { keys := utils.Map(fields, ctx.GetColumnName) return fmt.Sprintf("ON CONFLICT (%s)", strings.Join(keys, ",")) } diff --git a/pkg/builder/convert_find.go b/pkg/builder/convert_find.go index cd3b6e0..9e43ffd 100644 --- a/pkg/builder/convert_find.go +++ b/pkg/builder/convert_find.go @@ -7,11 +7,11 @@ import ( filters "l12.xyz/dal/filters" ) -func covertFind(ctx Context, find Find) string { +func covertFind(ctx Dialect, find Find) string { return covert_find(ctx, find, "") } -func covert_find(ctx Context, find Find, join string) string { +func covert_find(ctx Dialect, find Find, join string) string { if join == "" { join = " AND " } @@ -29,7 +29,7 @@ func covert_find(ctx Context, find Find, join string) string { expressions = append(expressions, fmt.Sprintf("(%s)", v)) continue } - context := ctx.New(CtxOpts{ + context := ctx.New(DialectOpts{ "FieldName": key, }) values, _ := filters.Convert(context, value) diff --git a/pkg/builder/convert_group.go b/pkg/builder/convert_group.go index 933df50..af6c42b 100644 --- a/pkg/builder/convert_group.go +++ b/pkg/builder/convert_group.go @@ -7,7 +7,7 @@ import ( "l12.xyz/dal/utils" ) -func convertGroup(ctx Context, keys []string) string { +func convertGroup(ctx Dialect, keys []string) string { set := utils.Map(keys, ctx.GetColumnName) return fmt.Sprintf("GROUP BY %s", strings.Join(set, ", ")) } diff --git a/pkg/builder/convert_insert.go b/pkg/builder/convert_insert.go index 2ec7902..9d80284 100644 --- a/pkg/builder/convert_insert.go +++ b/pkg/builder/convert_insert.go @@ -10,27 +10,33 @@ type InsertData struct { Values []interface{} } -func convertInsert(ctx Context, inserts []Map) (InsertData, error) { +func convertInsert(ctx Dialect, inserts []Map) (InsertData, error) { keys := aggregateSortedKeys(inserts) - placeholder := make([]string, 0) + posEnum := make([]string, 0) for range keys { - placeholder = append(placeholder, "?") + posEnum = append(posEnum, "?") } + placeholder := strings.Join(posEnum, ",") + positional := []string{} values := make([]interface{}, 0) for _, insert := range inserts { vals := make([]interface{}, 0) for _, key := range keys { vals = append(vals, insert[key]) } - values = append(values, vals) + values = append(values, vals...) + positional = append( + positional, + fmt.Sprintf("(%s)", placeholder), + ) } sfmt := fmt.Sprintf( - "INSERT INTO %s (%s) VALUES (%s)", + "INSERT INTO %s (%s) VALUES %s", ctx.GetTableName(), strings.Join(keys, ","), - strings.Join(placeholder, ","), + strings.Join(positional, ","), ) return InsertData{ Statement: sfmt, diff --git a/pkg/builder/convert_insert_test.go b/pkg/builder/convert_insert_test.go index c33b261..5dedbc8 100644 --- a/pkg/builder/convert_insert_test.go +++ b/pkg/builder/convert_insert_test.go @@ -1,6 +1,7 @@ package builder import ( + "fmt" "testing" ) @@ -15,12 +16,12 @@ func TestConvertInsert(t *testing.T) { } result, _ := convertInsert(ctx, insert) - if result.Statement != `INSERT INTO test (a,b,c) VALUES (?,?,?)` { - t.Errorf(`Expected "INSERT INTO test (a,b,c) VALUES (?,?,?)", got %s`, result.Statement) + if result.Statement != `INSERT INTO test (a,b,c) VALUES (?,?,?),(?,?,?)` { + t.Errorf(`Expected "INSERT INTO test (a,b,c) VALUES (?,?,?),(?,?,?)", got %s`, result.Statement) } - // for _, r := range result.Values { - // fmt.Println(r) - // } + for _, r := range result.Values { + fmt.Println(r) + } } diff --git a/pkg/builder/convert_join.go b/pkg/builder/convert_join.go index ea17ff1..7bb4955 100644 --- a/pkg/builder/convert_join.go +++ b/pkg/builder/convert_join.go @@ -11,7 +11,7 @@ type Join struct { As string `json:"$as"` } -func (j Join) Convert(ctx Context) string { +func (j Join) Convert(ctx Dialect) string { if j.For == "" { return "" } @@ -23,7 +23,7 @@ func (j Join) Convert(ctx Context) string { return as + fmt.Sprintf("JOIN %s ON %s", j.For, filter) } -func convertJoin(ctx Context, joins ...interface{}) []string { +func convertJoin(ctx Dialect, joins ...interface{}) []string { var result []string for _, join := range joins { jstr, ok := join.(string) diff --git a/pkg/builder/convert_join_test.go b/pkg/builder/convert_join_test.go index 6b375ec..c32601c 100644 --- a/pkg/builder/convert_join_test.go +++ b/pkg/builder/convert_join_test.go @@ -6,7 +6,7 @@ import ( adapter "l12.xyz/dal/adapter" ) -type SQLiteContext = adapter.SQLiteContext +type SQLiteContext = adapter.SQLite func TestJoin(t *testing.T) { j := Join{ diff --git a/pkg/builder/convert_sort.go b/pkg/builder/convert_sort.go index 3a7c9b1..5a2555e 100644 --- a/pkg/builder/convert_sort.go +++ b/pkg/builder/convert_sort.go @@ -5,7 +5,7 @@ import ( "strings" ) -func convertSort(ctx Context, sort Map) (string, error) { +func convertSort(ctx Dialect, sort Map) (string, error) { if sort == nil { return "", nil } diff --git a/pkg/builder/convert_update.go b/pkg/builder/convert_update.go index 1a8f706..c20e809 100644 --- a/pkg/builder/convert_update.go +++ b/pkg/builder/convert_update.go @@ -12,7 +12,7 @@ type UpdateData struct { Values []interface{} } -func convertUpdate(ctx Context, updates Map) UpdateData { +func convertUpdate(ctx Dialect, updates Map) UpdateData { keys := aggregateSortedKeys([]Map{updates}) set := make([]string, 0) values := make([]interface{}, 0) diff --git a/pkg/builder/types.go b/pkg/builder/types.go index 67955a5..fde0228 100644 --- a/pkg/builder/types.go +++ b/pkg/builder/types.go @@ -11,5 +11,5 @@ type Find = filters.Find type Query = filters.Find type Filter = filters.Filter type Is = filters.Filter -type Context = adapter.Context -type CtxOpts = adapter.CtxOpts +type Dialect = adapter.Dialect +type DialectOpts = adapter.DialectOpts diff --git a/pkg/filters/And.go b/pkg/filters/And.go index e5dfdb2..95ef9c3 100644 --- a/pkg/filters/And.go +++ b/pkg/filters/And.go @@ -9,7 +9,7 @@ type And struct { And []string `json:"$and"` } -func (f And) ToSQLPart(ctx Context) string { +func (f And) ToSQLPart(ctx Dialect) string { if f.And == nil { return "" } diff --git a/pkg/filters/Between.go b/pkg/filters/Between.go index d1c8d11..cc0a6bb 100644 --- a/pkg/filters/Between.go +++ b/pkg/filters/Between.go @@ -14,7 +14,7 @@ func (f Between) FromJSON(data interface{}) IFilter { return FromJson[Between](data) } -func (f Between) ToSQLPart(ctx Context) string { +func (f Between) ToSQLPart(ctx Dialect) string { if f.Between == nil { return "" } diff --git a/pkg/filters/Eq.go b/pkg/filters/Eq.go index 3a2ae90..ff18f99 100644 --- a/pkg/filters/Eq.go +++ b/pkg/filters/Eq.go @@ -12,7 +12,7 @@ func (f Eq) FromJSON(data interface{}) IFilter { return FromJson[Eq](data) } -func (f Eq) ToSQLPart(ctx Context) string { +func (f Eq) ToSQLPart(ctx Dialect) string { if f.Eq == nil { return "" } diff --git a/pkg/filters/Glob.go b/pkg/filters/Glob.go index c3a61c7..e542d2a 100644 --- a/pkg/filters/Glob.go +++ b/pkg/filters/Glob.go @@ -10,7 +10,7 @@ func (f Glob) FromJSON(data interface{}) IFilter { return FromJson[Glob](data) } -func (f Glob) ToSQLPart(ctx Context) string { +func (f Glob) ToSQLPart(ctx Dialect) string { if f.Glob == nil { return "" } diff --git a/pkg/filters/Gt.go b/pkg/filters/Gt.go index 8042404..6b5eec8 100644 --- a/pkg/filters/Gt.go +++ b/pkg/filters/Gt.go @@ -12,7 +12,7 @@ func (f Gt) FromJSON(data interface{}) IFilter { return FromJson[Gt](data) } -func (f Gt) ToSQLPart(ctx Context) string { +func (f Gt) ToSQLPart(ctx Dialect) string { if f.Gt == nil { return "" } diff --git a/pkg/filters/Gte.go b/pkg/filters/Gte.go index 07c7cc1..a6fceb5 100644 --- a/pkg/filters/Gte.go +++ b/pkg/filters/Gte.go @@ -12,7 +12,7 @@ func (f Gte) FromJSON(data interface{}) IFilter { return FromJson[Gte](data) } -func (f Gte) ToSQLPart(ctx Context) string { +func (f Gte) ToSQLPart(ctx Dialect) string { if f.Gte == nil { return "" } diff --git a/pkg/filters/In.go b/pkg/filters/In.go index 36c5df1..7acbef4 100644 --- a/pkg/filters/In.go +++ b/pkg/filters/In.go @@ -15,7 +15,7 @@ func (f In) FromJSON(data interface{}) IFilter { return FromJson[In](data) } -func (f In) ToSQLPart(ctx Context) string { +func (f In) ToSQLPart(ctx Dialect) string { if f.In == nil { return "" } diff --git a/pkg/filters/Like.go b/pkg/filters/Like.go index 08fd5b9..13bf787 100644 --- a/pkg/filters/Like.go +++ b/pkg/filters/Like.go @@ -10,7 +10,7 @@ func (f Like) FromJSON(data interface{}) IFilter { return FromJson[Like](data) } -func (f Like) ToSQLPart(ctx Context) string { +func (f Like) ToSQLPart(ctx Dialect) string { if f.Like == nil { return "" } diff --git a/pkg/filters/Lt.go b/pkg/filters/Lt.go index c38aa9a..cc79a8d 100644 --- a/pkg/filters/Lt.go +++ b/pkg/filters/Lt.go @@ -12,7 +12,7 @@ func (f Lt) FromJSON(data interface{}) IFilter { return FromJson[Lt](data) } -func (f Lt) ToSQLPart(ctx Context) string { +func (f Lt) ToSQLPart(ctx Dialect) string { if f.Lt == nil { return "" } diff --git a/pkg/filters/Lte.go b/pkg/filters/Lte.go index cb9879d..dd04a7a 100644 --- a/pkg/filters/Lte.go +++ b/pkg/filters/Lte.go @@ -12,7 +12,7 @@ func (f Lte) FromJSON(data interface{}) IFilter { return FromJson[Lte](data) } -func (f Lte) ToSQLPart(ctx Context) string { +func (f Lte) ToSQLPart(ctx Dialect) string { if f.Lte == nil { return "" } diff --git a/pkg/filters/Ne.go b/pkg/filters/Ne.go index fabc4bc..067d612 100644 --- a/pkg/filters/Ne.go +++ b/pkg/filters/Ne.go @@ -10,7 +10,7 @@ func (f Ne) FromJSON(data interface{}) IFilter { return FromJson[Ne](data) } -func (f Ne) ToSQLPart(ctx Context) string { +func (f Ne) ToSQLPart(ctx Dialect) string { if f.Ne == nil { return "" } diff --git a/pkg/filters/NotBetween.go b/pkg/filters/NotBetween.go index 9f9e7b8..d2568df 100644 --- a/pkg/filters/NotBetween.go +++ b/pkg/filters/NotBetween.go @@ -14,7 +14,7 @@ func (f NotBetween) FromJSON(data interface{}) IFilter { return FromJson[NotBetween](data) } -func (f NotBetween) ToSQLPart(ctx Context) string { +func (f NotBetween) ToSQLPart(ctx Dialect) string { if f.NotBetween == nil { return "" } diff --git a/pkg/filters/NotIn.go b/pkg/filters/NotIn.go index 6c219d0..358bbe7 100644 --- a/pkg/filters/NotIn.go +++ b/pkg/filters/NotIn.go @@ -15,7 +15,7 @@ func (f NotIn) FromJSON(data interface{}) IFilter { return FromJson[NotIn](data) } -func (f NotIn) ToSQLPart(ctx Context) string { +func (f NotIn) ToSQLPart(ctx Dialect) string { if f.NotIn == nil { return "" } diff --git a/pkg/filters/NotLike.go b/pkg/filters/NotLike.go index 3a5792d..6cc2c59 100644 --- a/pkg/filters/NotLike.go +++ b/pkg/filters/NotLike.go @@ -10,7 +10,7 @@ func (f NotLike) FromJSON(data interface{}) IFilter { return FromJson[NotLike](data) } -func (f NotLike) ToSQLPart(ctx Context) string { +func (f NotLike) ToSQLPart(ctx Dialect) string { if f.NotLike == nil { return "" } diff --git a/pkg/filters/Or.go b/pkg/filters/Or.go index d2704bf..fed000a 100644 --- a/pkg/filters/Or.go +++ b/pkg/filters/Or.go @@ -9,7 +9,7 @@ type Or struct { Or []string `json:"$or"` } -func (f Or) ToSQLPart(ctx Context) string { +func (f Or) ToSQLPart(ctx Dialect) string { if f.Or == nil { return "" } diff --git a/pkg/filters/registry.go b/pkg/filters/registry.go index ba606e2..6e9d167 100644 --- a/pkg/filters/registry.go +++ b/pkg/filters/registry.go @@ -22,7 +22,7 @@ var FilterRegistry = map[string]IFilter{ "NotLike": &NotLike{}, } -func Convert(ctx Context, data interface{}) (string, error) { +func Convert(ctx Dialect, data interface{}) (string, error) { for _, impl := range FilterRegistry { filter := impl.FromJSON(data) if reflect.DeepEqual(impl, filter) { diff --git a/pkg/filters/types.go b/pkg/filters/types.go index 4eb818b..13f244d 100644 --- a/pkg/filters/types.go +++ b/pkg/filters/types.go @@ -2,11 +2,11 @@ package filters import "l12.xyz/dal/adapter" -type CtxOpts = adapter.CtxOpts -type Context = adapter.Context +type DialectOpts = adapter.DialectOpts +type Dialect = adapter.Dialect type IFilter interface { - ToSQLPart(ctx Context) string + ToSQLPart(ctx Dialect) string FromJSON(interface{}) IFilter } diff --git a/pkg/filters/unit_test.go b/pkg/filters/unit_test.go index 2077ee6..b377921 100644 --- a/pkg/filters/unit_test.go +++ b/pkg/filters/unit_test.go @@ -6,7 +6,7 @@ import ( adapter "l12.xyz/dal/adapter" ) -type SQLiteContext = adapter.SQLiteContext +type SQLiteContext = adapter.SQLite func TestEq(t *testing.T) { ctx := SQLiteContext{