diff options
| author | Stefan Majewsky <majewsky@gmx.net> | 2026-04-17 14:53:52 +0200 |
|---|---|---|
| committer | Stefan Majewsky <majewsky@gmx.net> | 2026-04-17 14:53:55 +0200 |
| commit | 52f44287216b47149da9eb3f038408447f0e5981 (patch) | |
| tree | 8b81794b83d11b26e6753f65d65641b34336995c | |
| parent | e73aee05956d7917e7d76ab793d5e2291ace6416 (diff) | |
| download | go-oblast-52f44287216b47149da9eb3f038408447f0e5981.tar.gz | |
improve test coverage, error reporting for Select()
| -rw-r--r-- | errors.go | 68 | ||||
| -rw-r--r-- | internal/assert/assert.go | 8 | ||||
| -rw-r--r-- | internal/mock/mock.go | 12 | ||||
| -rw-r--r-- | oblast.go | 19 | ||||
| -rw-r--r-- | query.go | 6 | ||||
| -rw-r--r-- | select.go | 43 | ||||
| -rw-r--r-- | select_test.go | 192 |
7 files changed, 242 insertions, 106 deletions
diff --git a/errors.go b/errors.go new file mode 100644 index 0000000..1e81060 --- /dev/null +++ b/errors.go @@ -0,0 +1,68 @@ +// SPDX-FileCopyrightText: 2026 Stefan Majewsky <majewsky@gmx.net> +// SPDX-License-Identifier: Apache-2.0 + +package oblast + +import ( + "fmt" + "reflect" + "strings" +) + +// MissingRecordError is returned by [Store.Update] if one of the rows to be updated does not exist in the DB. +type MissingRecordError[R any] struct { + // The record that was provided to [Store.Update], + // but for which no row with the same primary key values could be located. + Record R + plan plan +} + +// Error implements the builtin/error interface. +func (e MissingRecordError[R]) Error() string { + keyDescs := make([]string, len(e.plan.PrimaryKeyColumnNames)) + v := reflect.ValueOf(e.Record) + for idx, columnName := range e.plan.PrimaryKeyColumnNames { + keyDescs[idx] = fmt.Sprintf("%s = %#v", columnName, v.FieldByIndex(e.plan.IndexByColumnName[columnName])) + } + return "could not UPDATE record that does not exist in the database: " + strings.Join(keyDescs, ", ") +} + +// An error type that optionally contains either one of the following or both: +// - a core error from an IO operation (e.g. a database read) +// - an auxiliary error from closing or otherwise cleaning up the respective IO handle +type ioError struct { + MainError error + CleanupError error + CleanupOperation string +} + +func newIOError(err error, cleanupOperation string, cleanupErr error) error { + if err == nil && cleanupErr == nil { + return nil + } + return ioError{err, cleanupErr, cleanupOperation} +} + +// Error implements the builtin/error interface. +func (e ioError) Error() string { + switch { + case e.CleanupError == nil: + return e.MainError.Error() + case e.MainError == nil: + return fmt.Sprintf("during %s(): %s", e.CleanupOperation, e.CleanupError.Error()) + default: + return fmt.Sprintf("%s (additional error during %s(): %s)", e.MainError.Error(), e.CleanupOperation, e.CleanupError.Error()) + } +} + +// Unwrap implements the interface implied by the documentation of package errors. +func (e ioError) Unwrap() []error { + result := make([]error, 0, 2) + if e.MainError != nil { + result = append(result, e.MainError) + } + if e.CleanupError != nil { + result = append(result, e.CleanupError) + } + return result +} diff --git a/internal/assert/assert.go b/internal/assert/assert.go index 26f91ff..6e641ca 100644 --- a/internal/assert/assert.go +++ b/internal/assert/assert.go @@ -4,6 +4,8 @@ package assert import ( + "cmp" + "errors" "reflect" "testing" ) @@ -26,6 +28,12 @@ func DeepEqual[V any](t testing.TB, actual, expected V) { } } +// ErrEqual is a test assertion. +func ErrEqual(t testing.TB, actual error, expected string) { + t.Helper() + Equal(t, cmp.Or(actual, errors.New("<success>")).Error(), expected) +} + // SliceEqual is a test assertion. func SliceEqual[V comparable](t testing.TB, actual []V, expected ...V) { t.Helper() diff --git a/internal/mock/mock.go b/internal/mock/mock.go index d3358c4..ecbb03e 100644 --- a/internal/mock/mock.go +++ b/internal/mock/mock.go @@ -245,8 +245,9 @@ func (r result) RowsAffected() (int64, error) { // Rows is a mock response for a Query() or QueryRow() call. // It is constructed by [ResponseSet.ExpectQuery]. type Rows struct { - columns []string - results [][]any + columns []string + results [][]any + closeError error } // AndReturnColumns configures the set of column names that will be returend by this query. @@ -272,6 +273,11 @@ func (r *Rows) WithRow(values ...any) *Rows { return r } +// AndCloseFailsWith sets up Close() for this Rows to fail with the provided error message. +func (r *Rows) AndCloseFailsWith(err error) { + r.closeError = err +} + type rows struct { r Rows closed bool @@ -285,7 +291,7 @@ func (r *rows) Columns() []string { // Close implements the [driver.Rows] interface. func (r *rows) Close() error { r.closed = true - return nil + return r.r.closeError } // Next implements the [driver.Rows] interface. @@ -100,7 +100,6 @@ import ( "database/sql/driver" "fmt" "reflect" - "strings" ) var ( @@ -171,21 +170,3 @@ func MustNewStore[R any](dialect Dialect, opts ...PlanOption) Store[R] { } return store } - -// MissingRecordError is returned by [Store.Update] if one of the rows to be updated does not exist in the DB. -type MissingRecordError[R any] struct { - // The record that was provided to [Store.Update], - // but for which no row with the same primary key values could be located. - Record R - plan plan -} - -// Error implements the builtin/error interface. -func (e MissingRecordError[R]) Error() string { - keyDescs := make([]string, len(e.plan.PrimaryKeyColumnNames)) - v := reflect.ValueOf(e.Record) - for idx, columnName := range e.plan.PrimaryKeyColumnNames { - keyDescs[idx] = fmt.Sprintf("%s = %#v", columnName, v.FieldByIndex(e.plan.IndexByColumnName[columnName])) - } - return "could not UPDATE record that does not exist in the database: " + strings.Join(keyDescs, ", ") -} @@ -56,7 +56,7 @@ func (s Store[R]) Insert(db Handle, records ...R) (returnedRecords []R, returned return nil, fmt.Errorf("during Prepare(): %w", err) } defer func() { - returnedError = mergeCloseError("Stmt", returnedError, stmt.Close()) + returnedError = newIOError(returnedError, "Stmt.Close", stmt.Close()) }() } @@ -127,7 +127,7 @@ func (s Store[R]) Update(db Handle, records ...R) (returnedError error) { return fmt.Errorf("during Prepare(): %w", err) } defer func() { - returnedError = mergeCloseError("Stmt", returnedError, stmt.Close()) + returnedError = newIOError(returnedError, "Stmt.Close", stmt.Close()) }() } @@ -184,7 +184,7 @@ func (s Store[R]) Delete(db Handle, records ...R) (returnedError error) { return fmt.Errorf("during Prepare(): %w", err) } defer func() { - returnedError = mergeCloseError("Stmt", returnedError, stmt.Close()) + returnedError = newIOError(returnedError, "Stmt.Close", stmt.Close()) }() } @@ -14,7 +14,7 @@ import ( // according to the column names reported by the database as part of the result set. // // An error is returned if any column name in the result set does not correspond to an addressable field in R. -func (s Store[R]) Select(db Handle, query string, args ...any) (result []R, returnedError error) { +func (s Store[R]) Select(db Handle, query string, args ...any) ([]R, error) { // NOTE: This function body should be as short as possible to reduce the binary size after monomorphization. // Any expression that does not depend on type R should be factored out into a reusable function. @@ -22,10 +22,8 @@ func (s Store[R]) Select(db Handle, query string, args ...any) (result []R, retu if err != nil { return nil, err } - defer func() { - returnedError = mergeCloseError("Rows", returnedError, rows.Close()) - }() + var result []R slots := make([]any, len(indexes)) for rows.Next() { var target R @@ -36,7 +34,7 @@ func (s Store[R]) Select(db Handle, query string, args ...any) (result []R, retu result = append(result, target) } - return result, nil + return result, newIOError(err, "Rows.Err", rows.Err()) } // SelectWhere is like [Store.Select], but you only provide the part of the SELECT query that comes after the WHERE. @@ -52,7 +50,7 @@ func (s Store[R]) Select(db Handle, query string, args ...any) (result []R, retu // Besides a condition for the WHERE clause, it may contain additional clauses, such as ORDER BY or LIMIT. // // Returns an error if [NewStore] was called without the [TableNameIs] option, which is required to generate a query for this method. -func (s Store[R]) SelectWhere(db Handle, partialQuery string, args ...any) (result []R, returnedError error) { +func (s Store[R]) SelectWhere(db Handle, partialQuery string, args ...any) ([]R, error) { // NOTE: This function body should be as short as possible to reduce the binary size after monomorphization. // Any expression that does not depend on type R should be factored out into a reusable function. @@ -60,10 +58,8 @@ func (s Store[R]) SelectWhere(db Handle, partialQuery string, args ...any) (resu if err != nil { return nil, err } - defer func() { - returnedError = mergeCloseError("Rows", returnedError, rows.Close()) - }() + var result []R slots := make([]any, len(indexes)) for rows.Next() { var target R @@ -74,7 +70,7 @@ func (s Store[R]) SelectWhere(db Handle, partialQuery string, args ...any) (resu result = append(result, target) } - return result, nil + return result, newIOError(err, "Rows.Err", rows.Err()) } func startSelectQuery(db Handle, plan plan, query string, args ...any) (returnedRows *sql.Rows, indexes [][]int, returnedError error) { @@ -84,10 +80,8 @@ func startSelectQuery(db Handle, plan plan, query string, args ...any) (returned } defer func() { if returnedError != nil { - closeErr := rows.Close() // NOTE: Not `returnedRows.Close()`! We may have `rows != nil && returnedRows == nil`. - if closeErr != nil { - returnedError = fmt.Errorf("%w (additional error during rows.Close(): %s)", returnedError, closeErr.Error()) - } + // NOTE: Not `returnedRows.Close()`! We may have `rows != nil && returnedRows == nil`. + returnedError = newIOError(returnedError, "Rows.Close", rows.Close()) } }() @@ -116,6 +110,9 @@ func startSelectWhereQuery(db Handle, plan plan, partialQuery string, args ...an } query := plan.Select.Query + partialQuery rows, err = db.Query(query, args...) + if err != nil { + err = fmt.Errorf("during Query(): %w", err) + } return rows, plan.Select.ScanIndexes, err } @@ -127,18 +124,11 @@ func collectRow(rows *sql.Rows, plan plan, v reflect.Value, slots []any, indexes for idx, index := range indexes { slots[idx] = v.FieldByIndex(index).Addr().Interface() } - return rows.Scan(slots...) -} - -func mergeCloseError(typeName string, err, closeErr error) error { - switch { - case closeErr == nil: - return err - case err == nil: - return fmt.Errorf("during %s.Close(): %w", typeName, closeErr) - default: - return fmt.Errorf("%w (additional error during %s.Close(): %s)", err, typeName, closeErr.Error()) + err := rows.Scan(slots...) + if err != nil { + return newIOError(err, "Rows.Close", rows.Close()) } + return nil } // SelectOne executes the provided SQL query and fills an instance of the record type R if there is exactly one row in the result set, @@ -167,8 +157,7 @@ func (s Store[R]) SelectOne(db Handle, query string, args ...any) (result R, err // SelectOneWhere is like [Store.SelectOne], but you only provide the part of the SELECT query that comes after the WHERE. // See [Store.SelectWhere] for an explanation of how the full query is constructed from this partial query. // -// This method is significantly more efficient than [Store.SelectOne]. -// Prefer using it instaed of [Store.SelectOne] whenever possible. +// This method is more efficient than [Store.SelectOne] on CPU runtime, but has a slight memory allocation overhead. func (s Store[R]) SelectOneWhere(db Handle, partialQuery string, args ...any) (result R, err error) { // NOTE: This function body should be as short as possible to reduce the binary size after monomorphization. // Any expression that does not depend on type R should be factored out into a reusable function. diff --git a/select_test.go b/select_test.go index f364e1c..e985548 100644 --- a/select_test.go +++ b/select_test.go @@ -5,6 +5,7 @@ package oblast_test import ( "database/sql" + "errors" "testing" "time" @@ -110,7 +111,7 @@ func TestSelectReturningNoRecords(t *testing.T) { ExpectQueryWithArgs(3). AndReturnColumns("name", "id") _, err := store.SelectOne(db, `SELECT * FROM basic_records WHERE id < ?`, 3) - assert.Equal(t, err.Error(), sql.ErrNoRows.Error()) + assert.ErrEqual(t, err, sql.ErrNoRows.Error()) }) t.Run("using Store.SelectOneWhere", func(t *testing.T) { @@ -118,7 +119,7 @@ func TestSelectReturningNoRecords(t *testing.T) { ExpectQueryWithArgs(3). AndReturnColumns("id", "name") _, err := store.SelectOneWhere(db, `id < ?`, 3) - assert.Equal(t, err.Error(), sql.ErrNoRows.Error()) + assert.ErrEqual(t, err, sql.ErrNoRows.Error()) }) } @@ -137,27 +138,26 @@ func TestSelectIntoUnexpectedField(t *testing.T) { ) expectedError := "result has column \"name\" in position 0, but no field in type basicRecord has `db:\"name\"`" - - // NOTE: This problem cannot occur with SelectWhere() and SelectOneWhere() because of their use of query generation. - - t.Run("using Store.Select", func(t *testing.T) { + commonSetup := func() { md.ForQuery(`SELECT * FROM basic_records WHERE id < ?`). ExpectQueryWithArgs(3). AndReturnColumns("name", "id"). WithRow("foo", 1). WithRow("bar", 2) + } + + // NOTE: This problem cannot occur with SelectWhere() and SelectOneWhere() because of their use of query generation. + + t.Run("using Store.Select", func(t *testing.T) { + commonSetup() _, err := store.Select(db, `SELECT * FROM basic_records WHERE id < ?`, 3) - assert.Equal(t, err.Error(), expectedError) + assert.ErrEqual(t, err, expectedError) }) t.Run("using Store.SelectOne", func(t *testing.T) { - md.ForQuery(`SELECT * FROM basic_records WHERE id < ?`). - ExpectQueryWithArgs(3). - AndReturnColumns("name", "id"). - WithRow("ffoo", 1). - WithRow("bbar", 2) + commonSetup() _, err := store.SelectOne(db, `SELECT * FROM basic_records WHERE id < ?`, 3) - assert.Equal(t, err.Error(), expectedError) + assert.ErrEqual(t, err, expectedError) }) } @@ -176,45 +176,36 @@ func TestSelectWithScanError(t *testing.T) { ) expectedError := `sql: Scan error on column index 1, name "created_at": unsupported Scan, storing driver.Value type string into type *time.Time` - - t.Run("using Store.Select", func(t *testing.T) { - md.ForQuery(`SELECT * FROM basic_records WHERE id < ?`). + commonSetup := func(query string) { + md.ForQuery(query). ExpectQueryWithArgs(3). AndReturnColumns("id", "created_at"). WithRow(1, "foo"). WithRow(2, "bar") + } + + t.Run("using Store.Select", func(t *testing.T) { + commonSetup(`SELECT * FROM basic_records WHERE id < ?`) _, err := store.Select(db, `SELECT * FROM basic_records WHERE id < ?`, 3) - assert.Equal(t, err.Error(), expectedError) + assert.ErrEqual(t, err, expectedError) }) t.Run("using Store.SelectWhere", func(t *testing.T) { - md.ForQuery(`SELECT "id", "created_at" FROM "basic_records" WHERE id < ?`). - ExpectQueryWithArgs(3). - AndReturnColumns("id", "created_at"). - WithRow(1, "ffoo"). - WithRow(2, "bbar") + commonSetup(`SELECT "id", "created_at" FROM "basic_records" WHERE id < ?`) _, err := store.SelectWhere(db, `id < ?`, 3) - assert.Equal(t, err.Error(), expectedError) + assert.ErrEqual(t, err, expectedError) }) t.Run("using Store.SelectOne", func(t *testing.T) { - md.ForQuery(`SELECT * FROM basic_records WHERE id < ?`). - ExpectQueryWithArgs(3). - AndReturnColumns("id", "created_at"). - WithRow(1, "fffoo"). - WithRow(2, "bbbar") + commonSetup(`SELECT * FROM basic_records WHERE id < ?`) _, err := store.SelectOne(db, `SELECT * FROM basic_records WHERE id < ?`, 3) - assert.Equal(t, err.Error(), expectedError) + assert.ErrEqual(t, err, expectedError) }) t.Run("using Store.SelectOneWhere", func(t *testing.T) { - md.ForQuery(`SELECT "id", "created_at" FROM "basic_records" WHERE id < ?`). - ExpectQueryWithArgs(3). - AndReturnColumns("id", "created_at"). - WithRow(1, "ffffoo"). - WithRow(2, "bbbbar") + commonSetup(`SELECT "id", "created_at" FROM "basic_records" WHERE id < ?`) _, err := store.SelectOneWhere(db, `id < ?`, 3) - assert.Equal(t, err.Error(), expectedError) + assert.ErrEqual(t, err, expectedError) }) } @@ -241,12 +232,16 @@ func TestSelectIntoEmbeddedTypes(t *testing.T) { oblast.PrimaryKeyIs("id"), ) - t.Run("using Store.Select", func(t *testing.T) { - md.ForQuery(`SELECT * FROM composite_records`). + commonSetup := func(query string) { + md.ForQuery(query). ExpectQueryWithArgs(nil...). AndReturnColumns("id", "created_at", "updated_at"). WithRow(1, time.Unix(1, 0), time.Unix(3, 0)). WithRow(2, time.Unix(2, 0), nil) + } + + t.Run("using Store.Select", func(t *testing.T) { + commonSetup(`SELECT * FROM composite_records`) records := must.Return(store.Select(db, `SELECT * FROM composite_records`))(t) assert.SliceDeepEqual(t, records, compositeRecord{1, HasCreatedAt{time.Unix(1, 0)}, &HasUpdatedAt{new(time.Unix(3, 0))}}, @@ -255,11 +250,7 @@ func TestSelectIntoEmbeddedTypes(t *testing.T) { }) t.Run("using Store.SelectWhere", func(t *testing.T) { - md.ForQuery(`SELECT "id", "created_at", "updated_at" FROM "composite_records" WHERE TRUE`). - ExpectQueryWithArgs(nil...). - AndReturnColumns("id", "created_at", "updated_at"). - WithRow(1, time.Unix(1, 0), time.Unix(3, 0)). - WithRow(2, time.Unix(2, 0), nil) + commonSetup(`SELECT "id", "created_at", "updated_at" FROM "composite_records" WHERE TRUE`) records := must.Return(store.SelectWhere(db, `TRUE`))(t) assert.SliceDeepEqual(t, records, compositeRecord{1, HasCreatedAt{time.Unix(1, 0)}, &HasUpdatedAt{new(time.Unix(3, 0))}}, @@ -268,11 +259,7 @@ func TestSelectIntoEmbeddedTypes(t *testing.T) { }) t.Run("using Store.SelectOne", func(t *testing.T) { - md.ForQuery(`SELECT * FROM composite_records`). - ExpectQueryWithArgs(nil...). - AndReturnColumns("id", "created_at", "updated_at"). - WithRow(1, time.Unix(1, 0), time.Unix(3, 0)). - WithRow(2, time.Unix(2, 0), nil) + commonSetup(`SELECT * FROM composite_records`) record := must.Return(store.SelectOne(db, `SELECT * FROM composite_records`))(t) assert.DeepEqual(t, record, compositeRecord{1, HasCreatedAt{time.Unix(1, 0)}, &HasUpdatedAt{new(time.Unix(3, 0))}}, @@ -280,11 +267,7 @@ func TestSelectIntoEmbeddedTypes(t *testing.T) { }) t.Run("using Store.SelectOneWhere", func(t *testing.T) { - md.ForQuery(`SELECT "id", "created_at", "updated_at" FROM "composite_records" WHERE TRUE`). - ExpectQueryWithArgs(nil...). - AndReturnColumns("id", "created_at", "updated_at"). - WithRow(1, time.Unix(1, 0), time.Unix(3, 0)). - WithRow(2, time.Unix(2, 0), nil) + commonSetup(`SELECT "id", "created_at", "updated_at" FROM "composite_records" WHERE TRUE`) record := must.Return(store.SelectOneWhere(db, `TRUE`))(t) assert.DeepEqual(t, record, compositeRecord{1, HasCreatedAt{time.Unix(1, 0)}, &HasUpdatedAt{new(time.Unix(3, 0))}}, @@ -292,5 +275,106 @@ func TestSelectIntoEmbeddedTypes(t *testing.T) { }) } -// TODO: test error capture during Rows.Close() -// TODO: check for maximum test coverage in select.go +func TestSelectCapturingQueryError(t *testing.T) { + md := mock.NewDriver() + db := sql.OpenDB(md) + + type basicRecord struct { + ID int64 `db:"id"` + Name string `db:"name"` + } + store := oblast.MustNewStore[basicRecord]( + oblast.SqliteDialect(), + oblast.TableNameIs("basic_records"), + oblast.PrimaryKeyIs("id"), + ) + + t.Run("using Store.Select", func(t *testing.T) { + _, err := store.Select(db, `SELECT * FROM basic_records WHERE id < ?`, 3) + assert.ErrEqual(t, err, "during Query(): unexpected query: SELECT * FROM basic_records WHERE id < ?") + }) + + t.Run("using Store.SelectOne", func(t *testing.T) { + _, err := store.SelectOne(db, `SELECT * FROM basic_records WHERE id < ?`, 3) + assert.ErrEqual(t, err, "during Query(): unexpected query: SELECT * FROM basic_records WHERE id < ?") + }) + + t.Run("using Store.SelectWhere", func(t *testing.T) { + _, err := store.SelectWhere(db, `id < ?`, 3) + assert.ErrEqual(t, err, `during Query(): unexpected query: SELECT "id", "name" FROM "basic_records" WHERE id < ?`) + }) + + t.Run("using Store.SelectOneWhere", func(t *testing.T) { + _, err := store.SelectOneWhere(db, `id < ?`, 3) + assert.ErrEqual(t, err, `unexpected query: SELECT "id", "name" FROM "basic_records" WHERE id < ?`) + }) +} + +func TestSelectCapturingCloseError(t *testing.T) { + md := mock.NewDriver() + db := sql.OpenDB(md) + + type basicRecord struct { + ID int64 `db:"id"` + Name string `db:"name"` + } + store := oblast.MustNewStore[basicRecord]( + oblast.SqliteDialect(), + oblast.TableNameIs("basic_records"), + oblast.PrimaryKeyIs("id"), + ) + + commonSetup := func(query string) { + md.ForQuery(query). + ExpectQueryWithArgs(3). + AndReturnColumns("id", "name"). + WithRow(1, "foo"). + WithRow(2, "bar"). + AndCloseFailsWith(errors.New("datacenter on fire")) + } + + t.Run("using Store.Select", func(t *testing.T) { + commonSetup(`SELECT * FROM basic_records WHERE id < ?`) + _, err := store.Select(db, `SELECT * FROM basic_records WHERE id < ?`, 3) + assert.ErrEqual(t, err, "during Rows.Err(): datacenter on fire") + }) + + t.Run("using Store.SelectOne", func(t *testing.T) { + commonSetup(`SELECT * FROM basic_records WHERE id < ?`) + _, err := store.SelectOne(db, `SELECT * FROM basic_records WHERE id < ?`, 3) + assert.ErrEqual(t, err, "during Rows.Err(): datacenter on fire") + }) + + t.Run("using Store.SelectWhere", func(t *testing.T) { + commonSetup(`SELECT "id", "name" FROM "basic_records" WHERE id < ?`) + _, err := store.SelectWhere(db, `id < ?`, 3) + assert.ErrEqual(t, err, "during Rows.Err(): datacenter on fire") + }) + + t.Run("using Store.SelectOneWhere", func(t *testing.T) { + commonSetup(`SELECT "id", "name" FROM "basic_records" WHERE id < ?`) + _, err := store.SelectOneWhere(db, `id < ?`, 3) + assert.ErrEqual(t, err, "datacenter on fire") + }) +} + +func TestSelectNotPossibleWithoutTableName(t *testing.T) { + md := mock.NewDriver() + db := sql.OpenDB(md) + + type basicRecord struct { + ID int64 `db:"id"` + Name string `db:"name"` + } + store := oblast.MustNewStore[basicRecord](oblast.SqliteDialect()) + + t.Run("using Store.SelectWhere", func(t *testing.T) { + _, err := store.SelectWhere(db, `id < ?`, 3) + assert.ErrEqual(t, err, "cannot execute SelectWhere() because query could not be autogenerated") + }) + + t.Run("using Store.SelectOneWhere", func(t *testing.T) { + _, err := store.SelectOneWhere(db, `id < ?`, 3) + assert.ErrEqual(t, err, "cannot execute SelectOneWhere() because query could not be autogenerated") + }) +} |
