diff options
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/plan.go | 6 | ||||
| -rw-r--r-- | internal/plan_test.go | 191 |
2 files changed, 176 insertions, 21 deletions
diff --git a/internal/plan.go b/internal/plan.go index 5b138d0..2ed4136 100644 --- a/internal/plan.go +++ b/internal/plan.go @@ -180,12 +180,14 @@ func (p Plan) buildInsertQueryIfPossible(dialect Dialect) PlannedQuery { } query := fmt.Sprintf( - `INSERT INTO %s (%s) VALUES (%s)%s`, + `INSERT INTO %s (%s) VALUES (%s)`, dialect.QuoteIdentifier(p.TableName), strings.Join(quotedColumnNames, ", "), strings.Join(quotedPlaceholders, ", "), - dialect.InsertSuffixForAutoColumns(p.AutoColumnNames), ) + if len(p.AutoColumnNames) > 0 { + query += dialect.InsertSuffixForAutoColumns(p.AutoColumnNames) + } return PlannedQuery{query, argumentIndexes} } diff --git a/internal/plan_test.go b/internal/plan_test.go index 570833c..c504ace 100644 --- a/internal/plan_test.go +++ b/internal/plan_test.go @@ -50,45 +50,198 @@ func TestPlanFieldTraversal(t *testing.T) { "Message": {4}, }) - assert.Equal(t, plan.Insert.Query, - `INSERT INTO "log_entries" ("created_at", "Message") VALUES ($1, $2) RETURNING "id"`, - ) - assert.DeepEqual(t, plan.Insert.ArgumentIndexes, [][]int{{3}, {4}}) - assert.Equal(t, plan.Update.Query, - `UPDATE "log_entries" SET "created_at" = $1, "Message" = $2 WHERE "id" = $3`, - ) - assert.DeepEqual(t, plan.Update.ArgumentIndexes, [][]int{{3}, {4}, {2}}) - assert.Equal(t, plan.Delete.Query, - `DELETE FROM "log_entries" WHERE "id" = $1`, - ) - assert.DeepEqual(t, plan.Delete.ArgumentIndexes, [][]int{{2}}) + type extraTimestampFields struct { + UpdatedAt *time.Time `db:"updated_at"` + DeletedAt *time.Time `db:"deleted_at"` + } type record struct { Log - Foo bool `db:"foo"` - private2 bool `db:"private2"` //nolint:unused + *extraTimestampFields + Foo bool `db:"foo"` } // check that the plan for record: // 1. works at all, even though it as a whole is an unexported type // 2. traverses into Log and includes all of its fields as well + // 3. traverses into *extraTimestampFields (despite the extra pointer and the type being private), too // 3. completely ignores the marker types in type Log plan, err = internal.BuildPlan(reflect.TypeFor[record](), internal.PostgresDialect{}) if err != nil { t.Error(err) } assert.Equal(t, plan.TableName, "") - assert.DeepEqual(t, plan.AllColumnNames, []string{"id", "created_at", "Message", "foo"}) + assert.DeepEqual(t, plan.AllColumnNames, []string{"id", "created_at", "Message", "updated_at", "deleted_at", "foo"}) assert.DeepEqual(t, plan.PrimaryKeyColumnNames, nil) assert.DeepEqual(t, plan.AutoColumnNames, []string{"id"}) // this is okay, it does not bear significance in practice since no queries are generated assert.DeepEqual(t, plan.IndexByColumnName, map[string][]int{ "id": {0, 2}, "created_at": {0, 3}, "Message": {0, 4}, - "foo": {1}, + "updated_at": {1, 0}, + "deleted_at": {1, 1}, + "foo": {2}, + }) +} + +// 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 { + info.TableNameIs `db:"basic_records"` + info.PrimaryKeyIs `db:"ID"` + ID int64 `db:",auto"` + Description string + CreatedAt time.Time + } + + t.Run("PostgresDialect", func(t *testing.T) { + plan, err := internal.BuildPlan(reflect.TypeFor[record](), internal.PostgresDialect{}) + if err != nil { + t.Error(err) + } + assert.Equal(t, plan.Insert.Query, `INSERT INTO "basic_records" ("Description", "CreatedAt") VALUES ($1, $2) RETURNING "ID"`) + assert.DeepEqual(t, plan.Insert.ArgumentIndexes, [][]int{{3}, {4}}) + assert.Equal(t, plan.Update.Query, `UPDATE "basic_records" SET "Description" = $1, "CreatedAt" = $2 WHERE "ID" = $3`) + assert.DeepEqual(t, plan.Update.ArgumentIndexes, [][]int{{3}, {4}, {2}}) + assert.Equal(t, plan.Delete.Query, `DELETE FROM "basic_records" WHERE "ID" = $1`) + assert.DeepEqual(t, plan.Delete.ArgumentIndexes, [][]int{{2}}) }) - assert.Equal(t, plan.Insert.Query, "") - assert.Equal(t, plan.Update.Query, "") - assert.Equal(t, plan.Delete.Query, "") + t.Run("SqliteDialect", func(t *testing.T) { + plan, err := internal.BuildPlan(reflect.TypeFor[record](), internal.SqliteDialect{}) + if err != nil { + t.Error(err) + } + assert.Equal(t, plan.Insert.Query, `INSERT INTO "basic_records" ("Description", "CreatedAt") VALUES (?, ?)`) + assert.DeepEqual(t, plan.Insert.ArgumentIndexes, [][]int{{3}, {4}}) + assert.Equal(t, plan.Update.Query, `UPDATE "basic_records" SET "Description" = ?, "CreatedAt" = ? WHERE "ID" = ?`) + assert.DeepEqual(t, plan.Update.ArgumentIndexes, [][]int{{3}, {4}, {2}}) + assert.Equal(t, plan.Delete.Query, `DELETE FROM "basic_records" WHERE "ID" = ?`) + assert.DeepEqual(t, plan.Delete.ArgumentIndexes, [][]int{{2}}) + }) +} + +func TestQueryConstructionWithoutPrimaryKey(t *testing.T) { + type relation struct { + info.TableNameIs `db:"foo_bar_relations"` + FooID int64 `db:"foo_id"` + BarID int64 `db:"bar_id"` + } + + t.Run("PostgresDialect", func(t *testing.T) { + plan, err := internal.BuildPlan(reflect.TypeFor[relation](), internal.PostgresDialect{}) + if err != nil { + t.Error(err) + } + 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{{1}, {2}}) + assert.Equal(t, plan.Update.Query, "") + assert.DeepEqual(t, plan.Update.ArgumentIndexes, nil) + assert.Equal(t, plan.Delete.Query, "") + assert.DeepEqual(t, plan.Delete.ArgumentIndexes, nil) + }) + + t.Run("SqliteDialect", func(t *testing.T) { + plan, err := internal.BuildPlan(reflect.TypeFor[relation](), internal.SqliteDialect{}) + if err != nil { + t.Error(err) + } + assert.Equal(t, plan.Insert.Query, `INSERT INTO "foo_bar_relations" ("foo_id", "bar_id") VALUES (?, ?)`) + assert.DeepEqual(t, plan.Insert.ArgumentIndexes, [][]int{{1}, {2}}) + assert.Equal(t, plan.Update.Query, "") + assert.DeepEqual(t, plan.Update.ArgumentIndexes, nil) + assert.Equal(t, plan.Delete.Query, "") + assert.DeepEqual(t, plan.Delete.ArgumentIndexes, nil) + }) +} + +func TestQueryConstructionImpossble(t *testing.T) { + type unstructuredData struct { + Foo int + Bar string + } + + testWith := func(dialect internal.Dialect) func(*testing.T) { + return func(t *testing.T) { + plan, err := internal.BuildPlan(reflect.TypeFor[unstructuredData](), dialect) + if err != nil { + t.Error(err) + } + + assert.Equal(t, plan.Insert.Query, "") + assert.DeepEqual(t, plan.Insert.ArgumentIndexes, nil) + assert.Equal(t, plan.Update.Query, "") + assert.DeepEqual(t, plan.Update.ArgumentIndexes, nil) + assert.Equal(t, plan.Delete.Query, "") + assert.DeepEqual(t, plan.Delete.ArgumentIndexes, nil) + } + } + + t.Run("PostgresDialect", testWith(internal.PostgresDialect{})) + t.Run("SqliteDialect", testWith(internal.SqliteDialect{})) +} + +func TestQueryConstructionWithMultiplePrimaryKeyColumns(t *testing.T) { + type record struct { + info.TableNameIs `db:"complex_records"` + info.PrimaryKeyIs `db:"group_id,name"` + GroupID int64 `db:"group_id"` + Name string `db:"name"` + CreatedAt time.Time `db:"created_at"` + } + + t.Run("PostgresDialect", func(t *testing.T) { + plan, err := internal.BuildPlan(reflect.TypeFor[record](), internal.PostgresDialect{}) + if err != nil { + t.Error(err) + } + 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{{2}, {3}, {4}}) + 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{{4}, {2}, {3}}) + assert.Equal(t, plan.Delete.Query, `DELETE FROM "complex_records" WHERE "group_id" = $1 AND "name" = $2`) + assert.DeepEqual(t, plan.Delete.ArgumentIndexes, [][]int{{2}, {3}}) + }) + + t.Run("SqliteDialect", func(t *testing.T) { + plan, err := internal.BuildPlan(reflect.TypeFor[record](), internal.SqliteDialect{}) + if err != nil { + t.Error(err) + } + assert.Equal(t, plan.Insert.Query, `INSERT INTO "complex_records" ("group_id", "name", "created_at") VALUES (?, ?, ?)`) + assert.DeepEqual(t, plan.Insert.ArgumentIndexes, [][]int{{2}, {3}, {4}}) + assert.Equal(t, plan.Update.Query, `UPDATE "complex_records" SET "created_at" = ? WHERE "group_id" = ? AND "name" = ?`) + assert.DeepEqual(t, plan.Update.ArgumentIndexes, [][]int{{4}, {2}, {3}}) + assert.Equal(t, plan.Delete.Query, `DELETE FROM "complex_records" WHERE "group_id" = ? AND "name" = ?`) + assert.DeepEqual(t, plan.Delete.ArgumentIndexes, [][]int{{2}, {3}}) + }) +} + +func TestQueryConstructionWithMultipleAutoColumns(t *testing.T) { + type record struct { + info.TableNameIs `db:"autogenerated_records"` + info.PrimaryKeyIs `db:"id"` + ID int64 `db:"id,auto"` + Name string `db:"name"` + CreatedAt time.Time `db:"created_at,auto"` + } + + t.Run("PostgresDialect", func(t *testing.T) { + plan, err := internal.BuildPlan(reflect.TypeFor[record](), internal.PostgresDialect{}) + if err != nil { + t.Error(err) + } + assert.Equal(t, plan.Insert.Query, `INSERT INTO "autogenerated_records" ("name") VALUES ($1) RETURNING "id", "created_at"`) + assert.DeepEqual(t, plan.Insert.ArgumentIndexes, [][]int{{3}}) + 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{{3}, {4}, {2}}) + assert.Equal(t, plan.Delete.Query, `DELETE FROM "autogenerated_records" WHERE "id" = $1`) + assert.DeepEqual(t, plan.Delete.ArgumentIndexes, [][]int{{2}}) + }) + + t.Run("SqliteDialect", func(t *testing.T) { + _, err := internal.BuildPlan(reflect.TypeFor[record](), internal.SqliteDialect{}) + 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`) + }) } |
