From 01d2d52fd7dfb64c41f7c94808fe01665ffcb881 Mon Sep 17 00:00:00 2001 From: Stefan Majewsky Date: Sat, 18 Apr 2026 15:44:46 +0200 Subject: more test coverage, forbid non-zero auto columns during Insert() --- query_test.go | 201 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 196 insertions(+), 5 deletions(-) (limited to 'query_test.go') diff --git a/query_test.go b/query_test.go index 388183e..000c385 100644 --- a/query_test.go +++ b/query_test.go @@ -87,7 +87,7 @@ func TestUpdateBasic(t *testing.T) { Name string `db:"name"` } store := oblast.MustNewStore[basicRecord]( - oblast.PostgresDialect(), + oblast.SqliteDialect(), oblast.TableNameIs("basic_records"), oblast.PrimaryKeyIs("id"), ) @@ -98,7 +98,7 @@ func TestUpdateBasic(t *testing.T) { for idx := range batchSize { r := basicRecord{ID: int64(42 + idx), Name: "updated"} records[idx] = r - md.ForQuery(`UPDATE "basic_records" SET "name" = $1 WHERE "id" = $2`). + md.ForQuery(`UPDATE "basic_records" SET "name" = ? WHERE "id" = ?`). ExpectExecWithArgs(r.Name, r.ID). AndReturnRowsAffected(1) } @@ -116,7 +116,7 @@ func TestDeleteBasic(t *testing.T) { Name string `db:"name"` } store := oblast.MustNewStore[basicRecord]( - oblast.PostgresDialect(), + oblast.SqliteDialect(), oblast.TableNameIs("basic_records"), oblast.PrimaryKeyIs("id"), ) @@ -127,7 +127,7 @@ func TestDeleteBasic(t *testing.T) { for idx := range batchSize { r := basicRecord{ID: int64(42 + idx), Name: "removed"} records[idx] = r - md.ForQuery(`DELETE FROM "basic_records" WHERE "id" = $1`). + md.ForQuery(`DELETE FROM "basic_records" WHERE "id" = ?`). ExpectExecWithArgs(r.ID). AndReturnRowsAffected(1) } @@ -136,4 +136,195 @@ func TestDeleteBasic(t *testing.T) { } } -// TODO: more test coverage for query.go +func TestWriteQueriesNotPossible(t *testing.T) { + md := mock.NewDriver() + db := sql.OpenDB(md) + + type basicRecord struct { + ID int64 `db:"id,auto"` + Name string `db:"name"` + } + store := oblast.MustNewStore[basicRecord]( + oblast.SqliteDialect(), + // no TableNameIs() or PrimaryKeyIs() given + ) + + r := basicRecord{Name: "foo"} + _, err := store.Insert(db, r) + assert.ErrEqual(t, err, "cannot execute Insert() because query could not be autogenerated") + + r.ID = 42 + err = store.Update(db, r) + assert.ErrEqual(t, err, "cannot execute Update() because query could not be autogenerated") + + err = store.Delete(db, r) + assert.ErrEqual(t, err, "cannot execute Delete() because query could not be autogenerated") +} + +func TestWriteQueriesFailDuringPrepare(t *testing.T) { + md := mock.NewDriver() + db := sql.OpenDB(md) + + type basicRecord struct { + ID int64 `db:"id,auto"` + Name string `db:"name"` + } + store := oblast.MustNewStore[basicRecord]( + oblast.SqliteDialect(), + oblast.TableNameIs("basic_records"), + oblast.PrimaryKeyIs("id"), + ) + + for _, batchSize := range []int{1, oblast.PrepareThreshold - 1, oblast.PrepareThreshold + 1} { + records := make([]basicRecord, batchSize) + for idx := range batchSize { + records[idx] = basicRecord{Name: "foo"} + } + + _, err := store.Insert(db, records...) + baseError := `unexpected query: INSERT INTO "basic_records" ("name") VALUES (?)` + if batchSize < oblast.PrepareThreshold { + assert.ErrEqual(t, err, "during Exec() for record with idx = 0: "+baseError) + } else { + assert.ErrEqual(t, err, "during Prepare(): "+baseError) + } + + for idx := range batchSize { + records[idx].ID = int64(42 + idx) + } + + err = store.Update(db, records...) + baseError = `unexpected query: UPDATE "basic_records" SET "name" = ? WHERE "id" = ?` + if batchSize < oblast.PrepareThreshold { + assert.ErrEqual(t, err, "during Exec() for record with idx = 0: "+baseError) + } else { + assert.ErrEqual(t, err, "during Prepare(): "+baseError) + } + + err = store.Delete(db, records...) + baseError = `unexpected query: DELETE FROM "basic_records" WHERE "id" = ?` + if batchSize < oblast.PrepareThreshold { + assert.ErrEqual(t, err, "during Exec() for record with idx = 0: "+baseError) + } else { + assert.ErrEqual(t, err, "during Prepare(): "+baseError) + } + } + + store = oblast.MustNewStore[basicRecord]( + oblast.PostgresDialect(), // for test coverage of insertUsingReturningClause() + oblast.TableNameIs("basic_records"), + oblast.PrimaryKeyIs("id"), + ) + + for _, batchSize := range []int{1, oblast.PrepareThreshold - 1, oblast.PrepareThreshold + 1} { + records := make([]basicRecord, batchSize) + for idx := range batchSize { + records[idx] = basicRecord{Name: "foo"} + } + + _, err := store.Insert(db, records...) + baseError := `unexpected query: INSERT INTO "basic_records" ("name") VALUES ($1) RETURNING "id"` + if batchSize < oblast.PrepareThreshold { + assert.ErrEqual(t, err, "during QueryRow() for record with idx = 0: "+baseError) + } else { + assert.ErrEqual(t, err, "during Prepare(): "+baseError) + } + } +} + +func TestUpdateFailsOnMissingRecord(t *testing.T) { + md := mock.NewDriver() + db := sql.OpenDB(md) + + type basicRecord struct { + ID int64 `db:"id,auto"` + Name string `db:"name"` + } + store := oblast.MustNewStore[basicRecord]( + oblast.SqliteDialect(), + oblast.TableNameIs("basic_records"), + oblast.PrimaryKeyIs("id"), + ) + + md.ForQuery(`UPDATE "basic_records" SET "name" = ? WHERE "id" = ?`). + ExpectExecWithArgs("changed", 42). + AndReturnRowsAffected(0) + err := store.Update(db, basicRecord{ID: 42, Name: "changed"}) + assert.ErrEqual(t, err, "could not UPDATE record that does not exist in the database: id = 42") + _, hasCorrectType := err.(oblast.MissingRecordError[basicRecord]) //nolint:errorlint // we explicitly do not want a wrapped error + assert.Equal(t, hasCorrectType, true) +} + +func TestInsertWithUnsignedIdField(t *testing.T) { + md := mock.NewDriver() + db := sql.OpenDB(md) + + type basicRecord struct { + ID uint64 `db:"id,auto"` // not int64! + Name string `db:"name"` + } + + t.Run("using LastInsertID", func(t *testing.T) { + store := oblast.MustNewStore[basicRecord]( + oblast.SqliteDialect(), + oblast.TableNameIs("basic_records"), + oblast.PrimaryKeyIs("id"), + ) + + // success case + md.ForQuery(`INSERT INTO "basic_records" ("name") VALUES (?)`). + ExpectExecWithArgs("first"). + AndReturnLastInsertId(42). + AndReturnRowsAffected(1) + records := must.Return(store.Insert(db, basicRecord{Name: "first"}))(t) + assert.SliceEqual(t, records, basicRecord{ID: 42, Name: "first"}) + + // error case: negative ID cannot be cast to uint64 + md.ForQuery(`INSERT INTO "basic_records" ("name") VALUES (?)`). + ExpectExecWithArgs("second"). + AndReturnLastInsertId(-42). + AndReturnRowsAffected(1) + _, err := store.Insert(db, basicRecord{Name: "second"}) + assert.ErrEqual(t, err, "LastInsertId() = -42 for record with idx = 0 cannot be converted to uint") + + // error case: cannot Insert() a record that already has its ID field filled + md.ForQuery(`INSERT INTO "basic_records" ("name") VALUES (?)`). + ExpectExecWithArgs("third"). + AndReturnLastInsertId(42). + AndReturnRowsAffected(1) + _, err = store.Insert(db, basicRecord{ID: 23, Name: "third"}) + assert.ErrEqual(t, err, `refusing to INSERT record with idx = 0 that already has non-zero values in its "auto" columns`) + }) + + t.Run("using RETURNING clause", func(t *testing.T) { + store := oblast.MustNewStore[basicRecord]( + oblast.PostgresDialect(), + oblast.TableNameIs("basic_records"), + oblast.PrimaryKeyIs("id"), + ) + + // success case + md.ForQuery(`INSERT INTO "basic_records" ("name") VALUES ($1) RETURNING "id"`). + ExpectQueryWithArgs("first"). + AndReturnColumns("id"). + WithRow(42) + records := must.Return(store.Insert(db, basicRecord{Name: "first"}))(t) + assert.SliceEqual(t, records, basicRecord{ID: 42, Name: "first"}) + + // error case: negative ID cannot be cast to uint64 + md.ForQuery(`INSERT INTO "basic_records" ("name") VALUES ($1) RETURNING "id"`). + ExpectQueryWithArgs("second"). + AndReturnColumns("id"). + WithRow(-42) + _, err := store.Insert(db, basicRecord{Name: "second"}) + assert.ErrEqual(t, err, `during QueryRow() for record with idx = 0: sql: Scan error on column index 0, name "id": converting driver.Value type int ("-42") to a uint64: invalid syntax`) + + // error case: cannot Insert() a record that already has its ID field filled + md.ForQuery(`INSERT INTO "basic_records" ("name") VALUES ($1) RETURNING "id"`). + ExpectQueryWithArgs("third"). + AndReturnColumns("id"). + WithRow(42) + _, err = store.Insert(db, basicRecord{ID: 23, Name: "third"}) + assert.ErrEqual(t, err, `refusing to INSERT record with idx = 0 that already has non-zero values in its "auto" columns`) + }) +} -- cgit v1.2.3