diff options
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/dialect.go | 41 | ||||
| -rw-r--r-- | internal/mock/mock.go (renamed from internal/mock/driver.go) | 0 | ||||
| -rw-r--r-- | internal/plan.go | 302 | ||||
| -rw-r--r-- | internal/plan_test.go | 277 |
4 files changed, 0 insertions, 620 deletions
diff --git a/internal/dialect.go b/internal/dialect.go deleted file mode 100644 index e6db5b8..0000000 --- a/internal/dialect.go +++ /dev/null @@ -1,41 +0,0 @@ -// SPDX-FileCopyrightText: 2026 Stefan Majewsky <majewsky@gmx.net> -// SPDX-License-Identifier: Apache-2.0 - -package internal - -import ( - "strconv" - "strings" -) - -// Dialect is a copy of the interface of the same name in package oblast. -// We cannot refer to that interface within this package because that would constitute a cyclic dependency. -type Dialect interface { - Placeholder(i int) string - QuoteIdentifier(name string) string - UsesLastInsertID() bool - InsertSuffixForAutoColumns(columns []string) string -} - -// PostgresDialect is the dialect of PostgreSQL databases. -type PostgresDialect struct{} - -func (PostgresDialect) Placeholder(i int) string { return "$" + strconv.Itoa(i+1) } -func (PostgresDialect) QuoteIdentifier(name string) string { return `"` + name + `"` } -func (PostgresDialect) UsesLastInsertID() bool { return false } - -func (p PostgresDialect) InsertSuffixForAutoColumns(columns []string) string { - quotedColumns := make([]string, len(columns)) - for idx, name := range columns { - quotedColumns[idx] = p.QuoteIdentifier(name) - } - return ` RETURNING ` + strings.Join(quotedColumns, ", ") -} - -// SqliteDialect is the dialect of SQLite databases. -type SqliteDialect struct{} - -func (SqliteDialect) Placeholder(_ int) string { return "?" } -func (SqliteDialect) QuoteIdentifier(name string) string { return `"` + name + `"` } -func (SqliteDialect) UsesLastInsertID() bool { return true } -func (SqliteDialect) InsertSuffixForAutoColumns(columns []string) string { return "" } diff --git a/internal/mock/driver.go b/internal/mock/mock.go index d3358c4..d3358c4 100644 --- a/internal/mock/driver.go +++ b/internal/mock/mock.go diff --git a/internal/plan.go b/internal/plan.go deleted file mode 100644 index b57b8dd..0000000 --- a/internal/plan.go +++ /dev/null @@ -1,302 +0,0 @@ -// SPDX-FileCopyrightText: 2026 Stefan Majewsky <majewsky@gmx.net> -// SPDX-License-Identifier: Apache-2.0 - -package internal - -import ( - "errors" - "fmt" - "reflect" - "slices" - "strings" -) - -// Plan holds all information that we can derive from reflecting on a given type. -// The queries held within are only valid within the context of a given SQL dialect. -type Plan struct { - TypeName string // for use in error messages - TableName string // from info.TableNameIs marker (if any) - AllColumnNames []string // in order of struct fields - PrimaryKeyColumnNames []string // from info.PrimaryKeyIs marker (if any) - AutoColumnNames []string // subset of AllColumnNames where field has `,auto` marker - - // Argument for reflect.Value.FieldByIndex() for each column name. - IndexByColumnName map[string][]int - - // In dialects with UsesLastInsertID() == true, whether the ID column must be written with reflect.Value.SetInt() or reflect.Value.SetUint(). - FillIDWithSetUint bool - FillIDWithSetInt bool - - // Planned queries. - Select PlannedQuery // only `SELECT ... FROM ... WHERE `; user supplies the rest during Select{,One}Where() - Insert PlannedQuery - Update PlannedQuery - Delete PlannedQuery -} - -// PlannedQuery appears in type Plan. -type PlannedQuery struct { - // Empty if the respective query type is not supported by this Plan for lack of the required marker types. - Query string - // Arguments for reflect.Value.FieldByIndex() in the correct order for the query arguments of the above query. - ArgumentIndexes [][]int - // Arguments for reflect.Value.FieldByIndex() in the correct order for the Scan() arguments of the above query. - ScanIndexes [][]int -} - -// PlanOpts holds additional arguments to BuildPlan(). -type PlanOpts struct { - TableName string - PrimaryKeyColumnNames []string -} - -// BuildPlan creates a new plan for the given struct type. -func BuildPlan(t reflect.Type, dialect Dialect, opts PlanOpts) (Plan, error) { - p, err := buildPlan(t, dialect, opts) - if err != nil { - return Plan{}, fmt.Errorf("cannot use type %s.%s for queries: %w", t.PkgPath(), t.Name(), err) - } - return p, nil -} - -func buildPlan(t reflect.Type, dialect Dialect, opts PlanOpts) (Plan, error) { - if t.Kind() != reflect.Struct { - return Plan{}, fmt.Errorf("expected struct type, but got kind %s", t.Kind().String()) - } - - var p = Plan{ - TypeName: t.Name(), - TableName: opts.TableName, - PrimaryKeyColumnNames: opts.PrimaryKeyColumnNames, - IndexByColumnName: make(map[string][]int), - } - - // discover addressable fields in this type, - // collect information from markers and tags - for _, field := range reflect.VisibleFields(t) { - tags := strings.Split(strings.TrimSpace(field.Tag.Get("db")), ",") - - switch { - case field.PkgPath != "": - // ignore unexported fields (otherwise reflect.Value.Interface() on the field would panic) - continue - case field.Anonymous && field.Type.Kind() == reflect.Struct: - // for embedded struct fields, only consider their members, not the type itself, as a potential column - continue - default: - columnName, extraTags := tags[0], tags[1:] - if columnName == "-" { - continue - } - if columnName == "" { - columnName = field.Name - } - if otherIndex := p.IndexByColumnName[columnName]; otherIndex != nil { - return Plan{}, fmt.Errorf( - "duplicate tag `db:%q` on field index %v, but also on field index %v", - columnName, otherIndex, field.Index, - ) - } - p.IndexByColumnName[columnName] = field.Index - p.AllColumnNames = append(p.AllColumnNames, columnName) - - for _, tag := range extraTags { - switch tag { - case "auto": - p.AutoColumnNames = append(p.AutoColumnNames, columnName) - default: - return Plan{}, fmt.Errorf("unknown tag `db:%q` on field index %v", ","+tag, field.Index) - } - } - } - } - - // validation: defining a primary key only makes sense for records that map onto a single table - if len(p.PrimaryKeyColumnNames) > 0 && p.TableName == "" { - return Plan{}, errors.New("cannot declare a primary key without also providing the TableNameIs option") - } - - // validation: oblast.PrimaryKeyInfo must refer to columns that exist - for _, columnName := range p.PrimaryKeyColumnNames { - _, ok := p.IndexByColumnName[columnName] - if !ok { - return Plan{}, fmt.Errorf("no field has tag `db:%q`, but a field of this name was declared in the primary key", columnName) - } - } - - // validation: LastInsertID() only works if at most one column is auto-filled, and if that column holds an integer type - if dialect.UsesLastInsertID() { - switch len(p.AutoColumnNames) { - case 0: - // nothing to check - case 1: - columnName := p.AutoColumnNames[0] - field := t.FieldByIndex(p.IndexByColumnName[columnName]) - switch field.Type.Kind() { //nolint:exhaustive // false positive - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - p.FillIDWithSetInt = true - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - p.FillIDWithSetUint = true - default: - return Plan{}, fmt.Errorf( - "column is marked as auto-filled (%s), but this SQL dialect only supports auto-filling struct fields with integer types", - strings.Join(p.AutoColumnNames, ", "), - ) - } - default: - return Plan{}, fmt.Errorf( - "multiple columns are marked as auto-filled (%s), but this SQL dialect only supports at most one per table", - strings.Join(p.AutoColumnNames, ", "), - ) - } - } - - // prepare query strings - p.Select = p.buildSelectQueryIfPossible(dialect) - p.Insert = p.buildInsertQueryIfPossible(dialect) - p.Update = p.buildUpdateQueryIfPossible(dialect) - p.Delete = p.buildDeleteQueryIfPossible(dialect) - - return p, nil -} - -func (p Plan) getNonAutoColumnNames() []string { - result := make([]string, 0, len(p.AllColumnNames)-len(p.AutoColumnNames)) - for _, columnName := range p.AllColumnNames { - if !slices.Contains(p.AutoColumnNames, columnName) { - result = append(result, columnName) - } - } - return result -} - -func (p Plan) getNonPrimaryKeyColumnNames() []string { - result := make([]string, 0, len(p.AllColumnNames)-len(p.PrimaryKeyColumnNames)) - for _, columnName := range p.AllColumnNames { - if !slices.Contains(p.PrimaryKeyColumnNames, columnName) { - result = append(result, columnName) - } - } - return result -} - -func (p Plan) buildSelectQueryIfPossible(dialect Dialect) PlannedQuery { - if p.TableName == "" { - return PlannedQuery{Query: ""} - } - - var ( - scanIndexes = make([][]int, len(p.AllColumnNames)) - quotedColumnNames = make([]string, len(p.AllColumnNames)) - ) - for idx, columnName := range p.AllColumnNames { - scanIndexes[idx] = p.IndexByColumnName[columnName] - quotedColumnNames[idx] = dialect.QuoteIdentifier(columnName) - } - - query := fmt.Sprintf( - `SELECT %s FROM %s WHERE `, - strings.Join(quotedColumnNames, ", "), - dialect.QuoteIdentifier(p.TableName), - ) - return PlannedQuery{query, nil, scanIndexes} -} - -func (p Plan) buildInsertQueryIfPossible(dialect Dialect) PlannedQuery { - if p.TableName == "" || len(p.AllColumnNames) == 0 { - return PlannedQuery{Query: ""} - } - nonAutoColumnNames := p.getNonAutoColumnNames() - if len(nonAutoColumnNames) == 0 { - return PlannedQuery{Query: ""} - } - - var ( - argumentIndexes = make([][]int, len(nonAutoColumnNames)) - scanIndexes [][]int - quotedColumnNames = make([]string, len(nonAutoColumnNames)) - quotedPlaceholders = make([]string, len(nonAutoColumnNames)) - ) - for idx, columnName := range nonAutoColumnNames { - argumentIndexes[idx] = p.IndexByColumnName[columnName] - quotedColumnNames[idx] = dialect.QuoteIdentifier(columnName) - quotedPlaceholders[idx] = dialect.Placeholder(idx) - } - if len(p.AutoColumnNames) > 0 { - // NOTE: This is filled even if dialect.UsesLastInsertID() is false. - // We need this index to find the right value on which to run SetInt() or SetUint(). - scanIndexes = make([][]int, len(p.AutoColumnNames)) - for idx, columnName := range p.AutoColumnNames { - scanIndexes[idx] = p.IndexByColumnName[columnName] - } - } - - query := fmt.Sprintf( - `INSERT INTO %s (%s) VALUES (%s)`, - dialect.QuoteIdentifier(p.TableName), - strings.Join(quotedColumnNames, ", "), - strings.Join(quotedPlaceholders, ", "), - ) - if len(p.AutoColumnNames) > 0 { - query += dialect.InsertSuffixForAutoColumns(p.AutoColumnNames) - } - return PlannedQuery{query, argumentIndexes, scanIndexes} -} - -func (p Plan) buildUpdateQueryIfPossible(dialect Dialect) PlannedQuery { - if p.TableName == "" || len(p.PrimaryKeyColumnNames) == 0 { - return PlannedQuery{Query: ""} - } - nonPrimaryKeyColumnNames := p.getNonPrimaryKeyColumnNames() - if len(nonPrimaryKeyColumnNames) == 0 { - return PlannedQuery{Query: ""} - } - - var ( - setArgumentIndexes = make([][]int, len(nonPrimaryKeyColumnNames)) - setClauses = make([]string, len(nonPrimaryKeyColumnNames)) - ) - for idx, columnName := range nonPrimaryKeyColumnNames { - setArgumentIndexes[idx] = p.IndexByColumnName[columnName] - setClauses[idx] = fmt.Sprintf("%s = %s", dialect.QuoteIdentifier(columnName), dialect.Placeholder(idx)) - } - - var ( - whereArgumentIndexes = make([][]int, len(p.PrimaryKeyColumnNames)) - whereClauses = make([]string, len(p.PrimaryKeyColumnNames)) - ) - for idx, columnName := range p.PrimaryKeyColumnNames { - whereArgumentIndexes[idx] = p.IndexByColumnName[columnName] - whereClauses[idx] = fmt.Sprintf("%s = %s", dialect.QuoteIdentifier(columnName), dialect.Placeholder(idx+len(setClauses))) - } - - query := fmt.Sprintf( - `UPDATE %s SET %s WHERE %s`, - dialect.QuoteIdentifier(p.TableName), - strings.Join(setClauses, ", "), - strings.Join(whereClauses, " AND "), - ) - return PlannedQuery{query, slices.Concat(setArgumentIndexes, whereArgumentIndexes), nil} -} - -func (p Plan) buildDeleteQueryIfPossible(dialect Dialect) PlannedQuery { - if p.TableName == "" || len(p.PrimaryKeyColumnNames) == 0 { - return PlannedQuery{Query: ""} - } - - var ( - argumentIndexes = make([][]int, len(p.PrimaryKeyColumnNames)) - clauses = make([]string, len(p.PrimaryKeyColumnNames)) - ) - for idx, columnName := range p.PrimaryKeyColumnNames { - argumentIndexes[idx] = p.IndexByColumnName[columnName] - clauses[idx] = fmt.Sprintf("%s = %s", dialect.QuoteIdentifier(columnName), dialect.Placeholder(idx)) - } - - query := fmt.Sprintf( - `DELETE FROM %s WHERE %s`, - dialect.QuoteIdentifier(p.TableName), - strings.Join(clauses, " AND "), - ) - return PlannedQuery{query, argumentIndexes, nil} -} diff --git a/internal/plan_test.go b/internal/plan_test.go deleted file mode 100644 index e692556..0000000 --- a/internal/plan_test.go +++ /dev/null @@ -1,277 +0,0 @@ -// SPDX-FileCopyrightText: 2026 Stefan Majewsky <majewsky@gmx.net> -// SPDX-License-Identifier: Apache-2.0 - -package internal_test - -import ( - "reflect" - "testing" - "time" - - "go.xyrillian.de/oblast/internal" - "go.xyrillian.de/oblast/internal/assert" -) - -func TestPlanFieldTraversal(t *testing.T) { - type Timestamps struct { - CreatedAt time.Time `db:"created_at"` - UpdatedAt *time.Time `db:"updated_at"` - } - type yetMoreTimestamps struct { - DeletedAt *time.Time `db:"deleted_at"` - } - type Log struct { - ID int64 `db:"id,auto"` - Message string - private1 bool `db:"private1"` //nolint:unused - Ignored any `db:"-"` - Timestamps - yetMoreTimestamps - } - - // check that the plan for Log: - // 1. has no IndexByColumnName entries for marker types - // 2. uses the field name as a column name for "Message" - // 3. ignores "private1" because it cannot be written through reflection - // 4. ignores "Ignored" because its column name is "-" - // 5. traverses into "Timestamps" and includes its fields as well - // 6. traverses into "yetMoreTimestamps" as well (despite the extra pointer and the type being private) - // 7. recognizes "id" as an autofilled column - plan, err := internal.BuildPlan(reflect.TypeFor[Log](), internal.PostgresDialect{}, internal.PlanOpts{ - TableName: "log_entries", - PrimaryKeyColumnNames: []string{"id"}, - }) - if err != nil { - t.Error(err) - } - assert.Equal(t, plan.TableName, "log_entries") - assert.DeepEqual(t, plan.AllColumnNames, []string{"id", "Message", "created_at", "updated_at", "deleted_at"}) - assert.DeepEqual(t, plan.PrimaryKeyColumnNames, []string{"id"}) - assert.DeepEqual(t, plan.AutoColumnNames, []string{"id"}) - assert.DeepEqual(t, plan.IndexByColumnName, map[string][]int{ - "id": {0}, - "Message": {1}, - "created_at": {4, 0}, - "updated_at": {4, 1}, - "deleted_at": {5, 0}, - }) -} - -// TODO: test that, during Select(), assignment into embedded fields with pointer-to-struct type works (docs say that this might panic if we do not allocate into the pointer first) - -func TestQueryConstructionBasic(t *testing.T) { - type record struct { - ID int64 `db:",auto"` - Description string - CreatedAt time.Time - } - opts := internal.PlanOpts{ - TableName: "basic_records", - PrimaryKeyColumnNames: []string{"ID"}, - } - - t.Run("PostgresDialect", func(t *testing.T) { - plan, err := internal.BuildPlan(reflect.TypeFor[record](), internal.PostgresDialect{}, opts) - if err != nil { - t.Error(err) - } - assert.Equal(t, plan.Select.Query, `SELECT "ID", "Description", "CreatedAt" FROM "basic_records" WHERE `) - assert.DeepEqual(t, plan.Select.ArgumentIndexes, nil) - assert.DeepEqual(t, plan.Select.ScanIndexes, [][]int{{0}, {1}, {2}}) - assert.Equal(t, plan.Insert.Query, `INSERT INTO "basic_records" ("Description", "CreatedAt") VALUES ($1, $2) RETURNING "ID"`) - assert.DeepEqual(t, plan.Insert.ArgumentIndexes, [][]int{{1}, {2}}) - assert.DeepEqual(t, plan.Insert.ScanIndexes, [][]int{{0}}) - assert.Equal(t, plan.Update.Query, `UPDATE "basic_records" SET "Description" = $1, "CreatedAt" = $2 WHERE "ID" = $3`) - assert.DeepEqual(t, plan.Update.ArgumentIndexes, [][]int{{1}, {2}, {0}}) - assert.DeepEqual(t, plan.Update.ScanIndexes, nil) - assert.Equal(t, plan.Delete.Query, `DELETE FROM "basic_records" WHERE "ID" = $1`) - assert.DeepEqual(t, plan.Delete.ArgumentIndexes, [][]int{{0}}) - assert.DeepEqual(t, plan.Delete.ScanIndexes, nil) - }) - - t.Run("SqliteDialect", func(t *testing.T) { - plan, err := internal.BuildPlan(reflect.TypeFor[record](), internal.SqliteDialect{}, opts) - if err != nil { - t.Error(err) - } - assert.Equal(t, plan.Select.Query, `SELECT "ID", "Description", "CreatedAt" FROM "basic_records" WHERE `) - assert.DeepEqual(t, plan.Select.ArgumentIndexes, nil) - assert.DeepEqual(t, plan.Select.ScanIndexes, [][]int{{0}, {1}, {2}}) - assert.Equal(t, plan.Insert.Query, `INSERT INTO "basic_records" ("Description", "CreatedAt") VALUES (?, ?)`) - assert.DeepEqual(t, plan.Insert.ArgumentIndexes, [][]int{{1}, {2}}) - assert.DeepEqual(t, plan.Insert.ScanIndexes, [][]int{{0}}) - assert.Equal(t, plan.Update.Query, `UPDATE "basic_records" SET "Description" = ?, "CreatedAt" = ? WHERE "ID" = ?`) - assert.DeepEqual(t, plan.Update.ArgumentIndexes, [][]int{{1}, {2}, {0}}) - assert.DeepEqual(t, plan.Update.ScanIndexes, nil) - assert.Equal(t, plan.Delete.Query, `DELETE FROM "basic_records" WHERE "ID" = ?`) - assert.DeepEqual(t, plan.Delete.ArgumentIndexes, [][]int{{0}}) - assert.DeepEqual(t, plan.Delete.ScanIndexes, nil) - }) -} - -func TestQueryConstructionWithoutPrimaryKey(t *testing.T) { - type relation struct { - FooID int64 `db:"foo_id"` - BarID int64 `db:"bar_id"` - } - opts := internal.PlanOpts{ - TableName: "foo_bar_relations", - } - - t.Run("PostgresDialect", func(t *testing.T) { - plan, err := internal.BuildPlan(reflect.TypeFor[relation](), internal.PostgresDialect{}, opts) - if err != nil { - t.Error(err) - } - assert.Equal(t, plan.Select.Query, `SELECT "foo_id", "bar_id" FROM "foo_bar_relations" WHERE `) - assert.DeepEqual(t, plan.Select.ArgumentIndexes, nil) - assert.DeepEqual(t, plan.Select.ScanIndexes, [][]int{{0}, {1}}) - assert.Equal(t, plan.Insert.Query, `INSERT INTO "foo_bar_relations" ("foo_id", "bar_id") VALUES ($1, $2)`) - assert.DeepEqual(t, plan.Insert.ArgumentIndexes, [][]int{{0}, {1}}) - assert.DeepEqual(t, plan.Insert.ScanIndexes, nil) - assert.Equal(t, plan.Update.Query, "") - assert.DeepEqual(t, plan.Update.ArgumentIndexes, nil) - assert.DeepEqual(t, plan.Update.ScanIndexes, nil) - assert.Equal(t, plan.Delete.Query, "") - assert.DeepEqual(t, plan.Delete.ArgumentIndexes, nil) - assert.DeepEqual(t, plan.Delete.ScanIndexes, nil) - }) - - t.Run("SqliteDialect", func(t *testing.T) { - plan, err := internal.BuildPlan(reflect.TypeFor[relation](), internal.SqliteDialect{}, opts) - if err != nil { - t.Error(err) - } - assert.Equal(t, plan.Select.Query, `SELECT "foo_id", "bar_id" FROM "foo_bar_relations" WHERE `) - assert.DeepEqual(t, plan.Select.ArgumentIndexes, nil) - assert.DeepEqual(t, plan.Select.ScanIndexes, [][]int{{0}, {1}}) - assert.Equal(t, plan.Insert.Query, `INSERT INTO "foo_bar_relations" ("foo_id", "bar_id") VALUES (?, ?)`) - assert.DeepEqual(t, plan.Insert.ArgumentIndexes, [][]int{{0}, {1}}) - assert.DeepEqual(t, plan.Insert.ScanIndexes, nil) - assert.Equal(t, plan.Update.Query, "") - assert.DeepEqual(t, plan.Update.ArgumentIndexes, nil) - assert.DeepEqual(t, plan.Update.ScanIndexes, nil) - assert.Equal(t, plan.Delete.Query, "") - assert.DeepEqual(t, plan.Delete.ArgumentIndexes, nil) - assert.DeepEqual(t, plan.Delete.ScanIndexes, nil) - }) -} - -func TestQueryConstructionImpossble(t *testing.T) { - type unstructuredData struct { - Foo int - Bar string - } - opts := internal.PlanOpts{} - - testWith := func(dialect internal.Dialect) func(*testing.T) { - return func(t *testing.T) { - plan, err := internal.BuildPlan(reflect.TypeFor[unstructuredData](), dialect, opts) - if err != nil { - t.Error(err) - } - - assert.Equal(t, plan.Select.Query, "") - assert.DeepEqual(t, plan.Select.ArgumentIndexes, nil) - assert.DeepEqual(t, plan.Select.ScanIndexes, nil) - assert.Equal(t, plan.Insert.Query, "") - assert.DeepEqual(t, plan.Insert.ArgumentIndexes, nil) - assert.DeepEqual(t, plan.Insert.ScanIndexes, nil) - assert.Equal(t, plan.Update.Query, "") - assert.DeepEqual(t, plan.Update.ArgumentIndexes, nil) - assert.DeepEqual(t, plan.Update.ScanIndexes, nil) - assert.Equal(t, plan.Delete.Query, "") - assert.DeepEqual(t, plan.Delete.ArgumentIndexes, nil) - assert.DeepEqual(t, plan.Delete.ScanIndexes, nil) - } - } - - t.Run("PostgresDialect", testWith(internal.PostgresDialect{})) - t.Run("SqliteDialect", testWith(internal.SqliteDialect{})) -} - -func TestQueryConstructionWithMultiplePrimaryKeyColumns(t *testing.T) { - type record struct { - GroupID int64 `db:"group_id"` - Name string `db:"name"` - CreatedAt time.Time `db:"created_at"` - } - opts := internal.PlanOpts{ - TableName: "complex_records", - PrimaryKeyColumnNames: []string{"group_id", "name"}, - } - - t.Run("PostgresDialect", func(t *testing.T) { - plan, err := internal.BuildPlan(reflect.TypeFor[record](), internal.PostgresDialect{}, opts) - if err != nil { - t.Error(err) - } - assert.Equal(t, plan.Select.Query, `SELECT "group_id", "name", "created_at" FROM "complex_records" WHERE `) - assert.DeepEqual(t, plan.Select.ArgumentIndexes, nil) - assert.DeepEqual(t, plan.Select.ScanIndexes, [][]int{{0}, {1}, {2}}) - assert.Equal(t, plan.Insert.Query, `INSERT INTO "complex_records" ("group_id", "name", "created_at") VALUES ($1, $2, $3)`) - assert.DeepEqual(t, plan.Insert.ArgumentIndexes, [][]int{{0}, {1}, {2}}) - assert.DeepEqual(t, plan.Insert.ScanIndexes, nil) - assert.Equal(t, plan.Update.Query, `UPDATE "complex_records" SET "created_at" = $1 WHERE "group_id" = $2 AND "name" = $3`) - assert.DeepEqual(t, plan.Update.ArgumentIndexes, [][]int{{2}, {0}, {1}}) - assert.DeepEqual(t, plan.Update.ScanIndexes, nil) - assert.Equal(t, plan.Delete.Query, `DELETE FROM "complex_records" WHERE "group_id" = $1 AND "name" = $2`) - assert.DeepEqual(t, plan.Delete.ArgumentIndexes, [][]int{{0}, {1}}) - assert.DeepEqual(t, plan.Delete.ScanIndexes, nil) - }) - - t.Run("SqliteDialect", func(t *testing.T) { - plan, err := internal.BuildPlan(reflect.TypeFor[record](), internal.SqliteDialect{}, opts) - if err != nil { - t.Error(err) - } - assert.Equal(t, plan.Select.Query, `SELECT "group_id", "name", "created_at" FROM "complex_records" WHERE `) - assert.DeepEqual(t, plan.Select.ArgumentIndexes, nil) - assert.DeepEqual(t, plan.Select.ScanIndexes, [][]int{{0}, {1}, {2}}) - assert.Equal(t, plan.Insert.Query, `INSERT INTO "complex_records" ("group_id", "name", "created_at") VALUES (?, ?, ?)`) - assert.DeepEqual(t, plan.Insert.ArgumentIndexes, [][]int{{0}, {1}, {2}}) - assert.DeepEqual(t, plan.Insert.ScanIndexes, nil) - assert.Equal(t, plan.Update.Query, `UPDATE "complex_records" SET "created_at" = ? WHERE "group_id" = ? AND "name" = ?`) - assert.DeepEqual(t, plan.Update.ArgumentIndexes, [][]int{{2}, {0}, {1}}) - assert.DeepEqual(t, plan.Update.ScanIndexes, nil) - assert.Equal(t, plan.Delete.Query, `DELETE FROM "complex_records" WHERE "group_id" = ? AND "name" = ?`) - assert.DeepEqual(t, plan.Delete.ArgumentIndexes, [][]int{{0}, {1}}) - assert.DeepEqual(t, plan.Delete.ScanIndexes, nil) - }) -} - -func TestQueryConstructionWithMultipleAutoColumns(t *testing.T) { - type record struct { - ID int64 `db:"id,auto"` - Name string `db:"name"` - CreatedAt time.Time `db:"created_at,auto"` - } - opts := internal.PlanOpts{ - TableName: "autogenerated_records", - PrimaryKeyColumnNames: []string{"id"}, - } - - t.Run("PostgresDialect", func(t *testing.T) { - plan, err := internal.BuildPlan(reflect.TypeFor[record](), internal.PostgresDialect{}, opts) - if err != nil { - t.Error(err) - } - assert.Equal(t, plan.Select.Query, `SELECT "id", "name", "created_at" FROM "autogenerated_records" WHERE `) - assert.DeepEqual(t, plan.Select.ArgumentIndexes, nil) - assert.DeepEqual(t, plan.Select.ScanIndexes, [][]int{{0}, {1}, {2}}) - assert.Equal(t, plan.Insert.Query, `INSERT INTO "autogenerated_records" ("name") VALUES ($1) RETURNING "id", "created_at"`) - assert.DeepEqual(t, plan.Insert.ArgumentIndexes, [][]int{{1}}) - assert.DeepEqual(t, plan.Insert.ScanIndexes, [][]int{{0}, {2}}) - assert.Equal(t, plan.Update.Query, `UPDATE "autogenerated_records" SET "name" = $1, "created_at" = $2 WHERE "id" = $3`) - assert.DeepEqual(t, plan.Update.ArgumentIndexes, [][]int{{1}, {2}, {0}}) - assert.DeepEqual(t, plan.Update.ScanIndexes, nil) - assert.Equal(t, plan.Delete.Query, `DELETE FROM "autogenerated_records" WHERE "id" = $1`) - assert.DeepEqual(t, plan.Delete.ArgumentIndexes, [][]int{{0}}) - assert.DeepEqual(t, plan.Delete.ScanIndexes, nil) - }) - - t.Run("SqliteDialect", func(t *testing.T) { - _, err := internal.BuildPlan(reflect.TypeFor[record](), internal.SqliteDialect{}, opts) - assert.Equal(t, err.Error(), `cannot use type go.xyrillian.de/oblast/internal_test.record for queries: multiple columns are marked as auto-filled (id, created_at), but this SQL dialect only supports at most one per table`) - }) -} |
