From 656e5dcce132716b39005814407224f989aa8c2d Mon Sep 17 00:00:00 2001 From: Stefan Majewsky Date: Fri, 31 Jul 2026 21:39:56 +0200 Subject: replace types Handle, DB, Conn, Tx with their gg/gsql equivalents --- CHANGELOG.md | 6 + benchmark/benchmark_test.go | 5 +- benchmark/internal/oblast_pgx/handle.go | 14 +-- benchmark/internal/oblast_pgx/results.go | 14 +-- benchmark/internal/oblast_pgx/statement.go | 18 +-- benchmark/postgres_test.go | 9 +- go.sum | 4 +- handle.go | 174 ----------------------------- handle/handle.go | 54 --------- query.go | 25 ++--- query_test.go | 25 +++-- runtimeindex_test.go | 3 +- select.go | 36 +++--- select_test.go | 17 +-- 14 files changed, 93 insertions(+), 311 deletions(-) delete mode 100644 handle.go delete mode 100644 handle/handle.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 87d7841..98537ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,12 @@ SPDX-FileCopyrightText: 2026 Stefan Majewsky SPDX-License-Identifier: Apache-2.0 --> +# v0.13.0 (TBD) + +API changes: + +- The types `Handle`, `DB`, `Conn` and `Tx` have moved to `go.xyrillian.de/gg/gsql`. + # v0.12.0 (2026-07-17) Changes: diff --git a/benchmark/benchmark_test.go b/benchmark/benchmark_test.go index a3e32f7..4549962 100644 --- a/benchmark/benchmark_test.go +++ b/benchmark/benchmark_test.go @@ -15,6 +15,7 @@ import ( "github.com/go-gorp/gorp/v3" _ "github.com/mattn/go-sqlite3" "go.xyrillian.de/gg/assert" + "go.xyrillian.de/gg/gsql" "go.xyrillian.de/oblast" "go.xyrillian.de/oblast/internal/testhelpers/must" "gorm.io/driver/sqlite" @@ -43,9 +44,9 @@ var ( batchSizesForUpdate = []int{1, 2, 4, 8, 16, 100} ) -func makeSqliteTestDB(t testing.TB, recordCount int) (db *oblast.DB, dsn string) { +func makeSqliteTestDB(t testing.TB, recordCount int) (db *gsql.DB, dsn string) { dsn = fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name()) - db = oblast.NewDB(must.Return(sql.Open("sqlite3", dsn))(t)) + db = gsql.NewDB(must.Return(sql.Open("sqlite3", dsn))(t)) _ = must.Return(db.Exec(`CREATE TABLE entries (id INTEGER, message TEXT, PRIMARY KEY (id AUTOINCREMENT))`))(t) if recordCount > 0 { diff --git a/benchmark/internal/oblast_pgx/handle.go b/benchmark/internal/oblast_pgx/handle.go index 845fbe4..4bd72bd 100644 --- a/benchmark/internal/oblast_pgx/handle.go +++ b/benchmark/internal/oblast_pgx/handle.go @@ -12,7 +12,7 @@ import ( "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgconn" "github.com/jackc/pgx/v5/pgxpool" - "go.xyrillian.de/oblast/handle" + "go.xyrillian.de/gg/gsql" ) type Handle interface { @@ -27,7 +27,7 @@ var ( _ Handle = pgx.Tx(&pgxpool.Tx{}) ) -func Wrap(h Handle) handle.Handle { +func Wrap(h Handle) gsql.Handle { switch h := h.(type) { case *pgx.Conn: return wrappedHandle{h} @@ -46,8 +46,8 @@ type wrappedHandle struct { inner Handle } -// OblastPrepare implements the [handle.Handle] interface. -func (h wrappedHandle) OblastPrepare(ctx context.Context, query string, repeated bool) (handle.Statement, error) { +// GSQLPrepare implements the [gsql.Handle] interface. +func (h wrappedHandle) GSQLPrepare(ctx context.Context, query string, repeated bool) (gsql.Statement, error) { if !repeated { return wrappedUnpreparedStatement{query, h.inner}, nil } @@ -74,7 +74,7 @@ func deallocate(ctx context.Context, h Handle, stmt *pgconn.StatementDescription case *pgx.Conn: return h.Deallocate(ctx, stmt.Name) case *pgxpool.Conn: - panic("unreachable") // because func OblastPrepare() does not return a wrappedPreparedStatement for this underlying type + panic("unreachable") // because func GSQLPrepare() does not return a wrappedPreparedStatement for this underlying type case pgx.Tx: return h.Conn().Deallocate(ctx, stmt.Name) default: @@ -82,8 +82,8 @@ func deallocate(ctx context.Context, h Handle, stmt *pgconn.StatementDescription } } -// OblastQuery implements the [handle.Handle] interface. -func (h wrappedHandle) OblastQuery(ctx context.Context, query string, args []any) (handle.Rows, error) { +// GSQLQuery implements the [gsql.Handle] interface. +func (h wrappedHandle) GSQLQuery(ctx context.Context, query string, args []any) (gsql.Rows, error) { rows, err := h.inner.Query(ctx, query, args...) return wrappedRows{rows}, err } diff --git a/benchmark/internal/oblast_pgx/results.go b/benchmark/internal/oblast_pgx/results.go index 3ccb5ce..f842d36 100644 --- a/benchmark/internal/oblast_pgx/results.go +++ b/benchmark/internal/oblast_pgx/results.go @@ -9,16 +9,16 @@ import ( "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgconn" - "go.xyrillian.de/oblast/handle" + "go.xyrillian.de/gg/gsql" ) type wrappedRows struct { inner pgx.Rows } -var _ handle.Rows = wrappedRows{} +var _ gsql.Rows = wrappedRows{} -// Columns implements the [handle.Rows] interface. +// Columns implements the [gsql.Rows] interface. func (r wrappedRows) Columns() ([]string, error) { descriptions := r.inner.FieldDescriptions() result := make([]string, len(descriptions)) @@ -28,23 +28,23 @@ func (r wrappedRows) Columns() ([]string, error) { return result, nil } -// Close implements the [handle.Rows] interface. +// Close implements the [gsql.Rows] interface. func (r wrappedRows) Close() error { r.inner.Close() return nil } -// Err implements the [handle.Rows] interface. +// Err implements the [gsql.Rows] interface. func (r wrappedRows) Err() error { return r.inner.Err() } -// Next implements the [handle.Rows] interface. +// Next implements the [gsql.Rows] interface. func (r wrappedRows) Next() bool { return r.inner.Next() } -// Scan implements the [handle.Rows] interface. +// Scan implements the [gsql.Rows] interface. func (r wrappedRows) Scan(args ...any) error { return r.inner.Scan(args...) } diff --git a/benchmark/internal/oblast_pgx/statement.go b/benchmark/internal/oblast_pgx/statement.go index d81c579..0a33c73 100644 --- a/benchmark/internal/oblast_pgx/statement.go +++ b/benchmark/internal/oblast_pgx/statement.go @@ -8,7 +8,7 @@ import ( "database/sql" "github.com/jackc/pgx/v5/pgconn" - "go.xyrillian.de/oblast/handle" + "go.xyrillian.de/gg/gsql" ) type wrappedPreparedStatement struct { @@ -23,38 +23,38 @@ type wrappedUnpreparedStatement struct { } var ( - _ handle.Statement = wrappedPreparedStatement{} - _ handle.Statement = wrappedUnpreparedStatement{} + _ gsql.Statement = wrappedPreparedStatement{} + _ gsql.Statement = wrappedUnpreparedStatement{} ) -// Close implements the [handle.Statement] interface. +// Close implements the [gsql.Statement] interface. func (s wrappedPreparedStatement) Close() error { return deallocate(s.ctx, s.handle, s.statement) } -// Close implements the [handle.Statement] interface. +// Close implements the [gsql.Statement] interface. func (s wrappedUnpreparedStatement) Close() error { return nil } -// Exec implements the [handle.Statement] interface. +// Exec implements the [gsql.Statement] interface. func (s wrappedPreparedStatement) Exec(ctx context.Context, args []any) (sql.Result, error) { result, err := s.handle.Exec(ctx, s.statement.Name, args...) return wrappedResult{result}, err } -// Exec implements the [handle.Statement] interface. +// Exec implements the [gsql.Statement] interface. func (s wrappedUnpreparedStatement) Exec(ctx context.Context, args []any) (sql.Result, error) { result, err := s.handle.Exec(ctx, s.query, args...) return wrappedResult{result}, err } -// QueryRow implements the [handle.Statement] interface. +// QueryRow implements the [gsql.Statement] interface. func (s wrappedPreparedStatement) QueryRow(ctx context.Context, args, slots []any) error { return s.handle.QueryRow(ctx, s.statement.Name, args...).Scan(slots...) } -// QueryRow implements the [handle.Statement] interface. +// QueryRow implements the [gsql.Statement] interface. func (s wrappedUnpreparedStatement) QueryRow(ctx context.Context, args, slots []any) error { return s.handle.QueryRow(ctx, s.query, args...).Scan(slots...) } diff --git a/benchmark/postgres_test.go b/benchmark/postgres_test.go index a5957f7..dba7ca8 100644 --- a/benchmark/postgres_test.go +++ b/benchmark/postgres_test.go @@ -16,6 +16,7 @@ import ( "github.com/jackc/pgx/v5" _ "github.com/lib/pq" "go.xyrillian.de/gg/assert" + "go.xyrillian.de/gg/gsql" "go.xyrillian.de/oblast" "go.xyrillian.de/oblast/benchmark/internal/oblast_pgx" "go.xyrillian.de/oblast/internal/testhelpers/must" @@ -36,9 +37,9 @@ func BenchmarkPostgresHeadingHeadingHeadingHeadingHeadingHeadingHeadingHeading(b const defaultPostgresDSN = "host=localhost user=postgres dbname=oblast_benchmark sslmode=disable" -func connectToPostgresTestDB(t testing.TB, recordCount int) *oblast.DB { +func connectToPostgresTestDB(t testing.TB, recordCount int) *gsql.DB { dsn := cmp.Or(os.Getenv("BENCHMARK_POSTGRES_DSN"), defaultPostgresDSN) - db := oblast.NewDB(must.Return(sql.Open("postgres", dsn))(t)) + db := gsql.NewDB(must.Return(sql.Open("postgres", dsn))(t)) _ = must.Return(db.Exec(`CREATE TEMPORARY TABLE entries (id BIGSERIAL, message TEXT)`))(t) if recordCount > 0 { @@ -204,7 +205,7 @@ func BenchmarkPostgresInsertAndDelete(b *testing.B) { // test with different amounts of records for _, batchSize := range batchSizesForInsertDelete { b.Run("N="+strconv.Itoa(batchSize), func(b *testing.B) { - insertAndDeleteWithOblast := func(b *testing.B, dbh oblast.Handle) { + insertAndDeleteWithOblast := func(b *testing.B, dbh gsql.Handle) { records := make([]OblastEntry, batchSize) recordsForInsert := make([]*OblastEntry, batchSize) for idx := range records { @@ -334,7 +335,7 @@ func BenchmarkPostgresUpdate(b *testing.B) { } } - updateWithOblast := func(b *testing.B, dbh oblast.Handle, records []OblastEntry) func(string) { + updateWithOblast := func(b *testing.B, dbh gsql.Handle, records []OblastEntry) func(string) { return func(message string) { for idx := range records { records[idx].Message = message diff --git a/go.sum b/go.sum index 2ea0055..9ddc0a6 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,2 @@ -go.xyrillian.de/gg v1.11.1 h1:7P3kDFkTmR7jx2riYi0GwX5uhgrsL37QSrN16xH/n8E= -go.xyrillian.de/gg v1.11.1/go.mod h1:DoO4fQSWIrBRlNlCjVyrYM0kAEBt/Jg2GkMH+cGRZ0k= +go.xyrillian.de/gg v1.12.0 h1:oW9S91y36lS72D3p9a4axqjkD9zkkjkNZ12+PaZHpos= +go.xyrillian.de/gg v1.12.0/go.mod h1:DoO4fQSWIrBRlNlCjVyrYM0kAEBt/Jg2GkMH+cGRZ0k= diff --git a/handle.go b/handle.go deleted file mode 100644 index f35ae35..0000000 --- a/handle.go +++ /dev/null @@ -1,174 +0,0 @@ -// SPDX-FileCopyrightText: 2026 Stefan Majewsky -// SPDX-License-Identifier: Apache-2.0 - -package oblast - -import ( - "context" - "database/sql" - "fmt" - - "go.xyrillian.de/oblast/handle" -) - -// Handle contains behavior that database handles must offer to Oblast. -// Custom implementations of this interface can be used to connect non-std database drivers to Oblast. -type Handle = handle.Handle - -//////////////////////////////////////////////////////////////////////////////// -// public API for database/sql compatibility -// -// NOTE: The internal structure of these types looks weird at first glance, with -// the pointer to the underlying instance duplicated, but of course that's deliberate. -// -// If our types implemented [Handle] directly, every function call taking them as an argument -// of type [Handle] (e.g. any of the methods on [Store]) would allocate a new fat pointer -// when converting from e.g. [*DB] at the callsite to [Handle] in the argument value. -// -// To circumvent this, our types only _have_ [Handle] instances within them within them -// as an embedded field, thus implementing [Handle] indirectly instead of directly. - -// DB wraps [*sql.DB] into a [Handle] that can be used with Oblast. -// -// Because this type has [*sql.DB] as an embedded field, -// all methods from that type work on this type as well. -type DB struct { - *sql.DB - Handle -} - -// NewDB wraps an instance of [*sql.DB] into Oblast's own [DB] type. -func NewDB(db *sql.DB) *DB { - return &DB{db, sqlHandle[*sql.DB]{db}} -} - -// Begin is like [sql.DB.Begin], but wraps the resulting transaction for use with Oblast. -func (db *DB) Begin() (*Tx, error) { - tx, err := db.DB.Begin() - return maybe(NewTx, tx), err -} - -// BeginTx is like [sql.DB.BeginTx], but wraps the resulting transaction for use with Oblast. -func (db *DB) BeginTx(ctx context.Context, opts *sql.TxOptions) (*Tx, error) { - tx, err := db.DB.BeginTx(ctx, opts) - return maybe(NewTx, tx), err -} - -// Conn is like [sql.DB.Conn], but wraps the resulting connection for use with Oblast. -func (db *DB) Conn(ctx context.Context) (*Conn, error) { - conn, err := db.DB.Conn(ctx) - return maybe(NewConn, conn), err -} - -// Conn wraps [*sql.Conn] into a [Handle] that can be used with Oblast. -// -// Because this type has [*sql.Conn] as an embedded field, -// all methods from that type work on this type as well. -type Conn struct { - *sql.Conn - Handle -} - -// NewConn wraps an instance of [*sql.Conn] into Oblast's own [Conn] type. -func NewConn(db *sql.Conn) *Conn { - return &Conn{db, sqlHandle[*sql.Conn]{db}} -} - -// BeginTx is like [sql.DB.BeginTx], but wraps the resulting transaction for use with Oblast. -func (conn *Conn) BeginTx(ctx context.Context, opts *sql.TxOptions) (*Tx, error) { - tx, err := conn.Conn.BeginTx(ctx, opts) - return maybe(NewTx, tx), err -} - -// Tx wraps [*sql.Tx] into a [Handle] that can be used with Oblast. -// -// Because this type has [*sql.Tx] as an embedded field, -// all methods from that type work on this type as well. -type Tx struct { - *sql.Tx - Handle -} - -// NewTx wraps an instance of [*sql.Tx] into Oblast's own [Tx] type. -func NewTx(db *sql.Tx) *Tx { - return &Tx{db, sqlHandle[*sql.Tx]{db}} -} - -func maybe[T, U any](wrap func(*T) *U, value *T) *U { - if value == nil { - return nil - } - return wrap(value) -} - -// prove that we implement the interfaces that we claim -var ( - _ Handle = &DB{} - _ Handle = &Conn{} - _ Handle = &Tx{} -) - -//////////////////////////////////////////////////////////////////////////////// -// Handle implementation for database/sql types - -// sqlExecutor is an interface covered by both [*sql.DB], [*sql.Conn] and [*sql.Tx]. -type sqlExecutor interface { - ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) - PrepareContext(ctx context.Context, query string) (*sql.Stmt, error) - QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error) - QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row -} - -// sqlHandle provides the [Handle] implementation for any type that implements [sqlExecutor]. -type sqlHandle[T sqlExecutor] struct { - Base T -} - -// OblastPrepare implements the [Handle] interface. -func (h sqlHandle[T]) OblastPrepare(ctx context.Context, query string, repeated bool) (handle.Statement, error) { - if !repeated { - return wrappedStatement{h.Base, query, nil}, nil - } - stmt, err := h.Base.PrepareContext(ctx, query) - if err != nil { - return nil, fmt.Errorf("during Prepare(): %w", err) - } - return wrappedStatement{h.Base, query, stmt}, nil -} - -// OblastQuery implements the [Handle] interface. -func (h sqlHandle[T]) OblastQuery(ctx context.Context, query string, args []any) (handle.Rows, error) { - return h.Base.QueryContext(ctx, query, args...) //nolint:rowserrcheck // the caller does the check -} - -type wrappedStatement struct { - db sqlExecutor - query string - stmt *sql.Stmt // nil if repeated = false -} - -// Close implements the [Statement] interface. -func (s wrappedStatement) Close() error { - if s.stmt == nil { - return nil - } - return s.stmt.Close() -} - -// Exec implements the [Statement] interface. -func (s wrappedStatement) Exec(ctx context.Context, args []any) (sql.Result, error) { - if s.stmt == nil { - return s.db.ExecContext(ctx, s.query, args...) - } else { - return s.stmt.ExecContext(ctx, args...) - } -} - -// QueryRow implements the [Statement] interface. -func (s wrappedStatement) QueryRow(ctx context.Context, args, slots []any) error { - if s.stmt == nil { - return s.db.QueryRowContext(ctx, s.query, args...).Scan(slots...) - } else { - return s.stmt.QueryRowContext(ctx, args...).Scan(slots...) - } -} diff --git a/handle/handle.go b/handle/handle.go deleted file mode 100644 index f6e1694..0000000 --- a/handle/handle.go +++ /dev/null @@ -1,54 +0,0 @@ -// SPDX-FileCopyrightText: 2026 Stefan Majewsky -// SPDX-License-Identifier: Apache-2.0 - -// Package handle contains type definitions for connecting non-std database drivers to Oblast. -// Since most database drivers use the standard interface from database/sql, the Wrap() function from the main package covers the needs of most users. -package handle - -import ( - "context" - "database/sql" -) - -// Handle contains behavior that database handles must offer to Oblast. -// The standard-library types [*sql.DB], [*sql.Conn] and [*sql.Tx] can satisfy this interface through the respective types of the same name from the main Oblast package. -// Custom implementations of this interface can be used to connect non-std database drivers to Oblast. -// -// The method names are deliberately clunky to avoid name clashes with well-known methods like [sql.DB.Prepare] or [sql.DB.Query]. -type Handle interface { - // OblastPrepare prepares to execute a certain SQL query one or multiple times. - // - // The "repeated" flag is a hint to the implementation whether the same statement is going to be run many times. - // If false, the implementation shall choose to forego the additional effort of a full statement preparation if possible, - // and execute one-off queries instead. - OblastPrepare(ctx context.Context, query string, repeated bool) (Statement, error) - - // OblastQuery works like db.QueryContext(ctx, query, args...). - OblastQuery(ctx context.Context, query string, args []any) (Rows, error) -} - -// Statement represents a prepared statement returned from the OblastPrepare() method of [Handle]. -// The Exec and QueryRow methods shall work similarly to the respective functions on [*sql.Tx], as indicated in the comments. -// -// You will not need to interact with this type except when implementing your own [Handle]. -type Statement interface { - Close() error - - // Exec works like stmt.ExecContext(ctx, args...). - Exec(ctx context.Context, args []any) (sql.Result, error) - - // QueryRow works like stmt.QueryRow(ctx, args...).Scan(slots...). - QueryRow(ctx context.Context, args []any, slots []any) error -} - -// Rows represents a set of rows returned from the OblastQuery() method of [Handle]. -// All methods shall behave like on the [*sql.Rows] type from std. -// -// You will not need to interact with this type except when implementing your own [Handle]. -type Rows interface { - Columns() ([]string, error) - Close() error - Err() error - Next() bool - Scan(slots ...any) error -} diff --git a/query.go b/query.go index b92106a..e9d6862 100644 --- a/query.go +++ b/query.go @@ -10,8 +10,7 @@ import ( "reflect" "go.xyrillian.de/gg/errext" - - "go.xyrillian.de/oblast/handle" + "go.xyrillian.de/gg/gsql" ) // PrepareThreshold is a tuning parameter for the strategy used by all methods of [Store] operating on batches of records provided by the caller @@ -29,12 +28,12 @@ import ( var PrepareThreshold int = 8 // prepare behaves like [Handle.Prepare]. -func prepare(ctx context.Context, db Handle, query, operation string, inputSize int) (handle.Statement, error) { +func prepare(ctx context.Context, db gsql.Handle, query, operation string, inputSize int) (gsql.Statement, error) { if query == "" { return nil, fmt.Errorf("cannot execute %s() because query could not be autogenerated", operation) } - return db.OblastPrepare(ctx, query, inputSize >= PrepareThreshold) + return db.GSQLPrepare(ctx, query, inputSize >= PrepareThreshold) } // Insert executes an SQL INSERT statement for each of the provided records. @@ -48,7 +47,7 @@ func prepare(ctx context.Context, db Handle, query, operation string, inputSize // Returns an error if any of the `records` has a non-zero value in any column marked as `db:",auto"`. // Records that already exist in the database should be handled with [Store.Update] instead. // To automatically decide between INSERT and UPDATE on a per-record basis, use [Store.Upsert] instead. -func (s Store[R]) Insert(ctx context.Context, db Handle, records ...*R) error { +func (s Store[R]) Insert(ctx context.Context, db gsql.Handle, records ...*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. @@ -59,7 +58,7 @@ func (s Store[R]) Insert(ctx context.Context, db Handle, records ...*R) error { return s.insertUsing(ctx, stmt, db, records) } -func (s Store[R]) insertUsing(ctx context.Context, stmt handle.Statement, db Handle, records []*R) error { +func (s Store[R]) insertUsing(ctx context.Context, stmt gsql.Statement, db gsql.Handle, records []*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. @@ -85,7 +84,7 @@ func (s Store[R]) insertUsing(ctx context.Context, stmt handle.Statement, db Han return errext.WithCleanup(nil, "Stmt.Close", stmt.Close()) } -func insertRecord(ctx context.Context, plan plan, v reflect.Value, recordIndex int, stmt handle.Statement, argumentIndexes [][]int, argumentSlots []any, scanIndexes [][]int, scanSlots []any) error { +func insertRecord(ctx context.Context, plan plan, v reflect.Value, recordIndex int, stmt gsql.Statement, argumentIndexes [][]int, argumentSlots []any, scanIndexes [][]int, scanSlots []any) error { for idx, index := range argumentIndexes { argumentSlots[idx] = v.FieldByIndex(index).Interface() } @@ -156,7 +155,7 @@ func checkTransparentPointerStructFieldsInitialized(operation string, recordInde // Returns [MissingRecordError] if any of the records does not exist in the database, that is, if for any of the records, the database contains no row with the same primary key values. // // Returns an error if [NewStore] was called without the [TableNameIs] or [PrimaryKeyIs] options, which are both required to generate a query for this method. -func (s Store[R]) Update(ctx context.Context, db Handle, records ...R) error { +func (s Store[R]) Update(ctx context.Context, db gsql.Handle, records ...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. @@ -187,7 +186,7 @@ func (s Store[R]) Update(ctx context.Context, db Handle, records ...R) error { return errext.WithCleanup(nil, "Stmt.Close", stmt.Close()) } -func updateRecord(ctx context.Context, v reflect.Value, recordIndex int, stmt handle.Statement, argumentIndexes [][]int, argumentSlots []any) (int64, error) { +func updateRecord(ctx context.Context, v reflect.Value, recordIndex int, stmt gsql.Statement, argumentIndexes [][]int, argumentSlots []any) (int64, error) { for idx, index := range argumentIndexes { argumentSlots[idx] = v.FieldByIndex(index).Interface() } @@ -205,7 +204,7 @@ func updateRecord(ctx context.Context, v reflect.Value, recordIndex int, stmt ha // Delete executes an SQL DELETE statement for each of the provided records, using their primary keys to locate the respective table rows. // // Returns an error if [NewStore] was called without the [TableNameIs] or [PrimaryKeyIs] options, which are both required to generate a query for this method. -func (s Store[R]) Delete(ctx context.Context, db Handle, records ...R) error { +func (s Store[R]) Delete(ctx context.Context, db gsql.Handle, records ...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. @@ -230,7 +229,7 @@ func (s Store[R]) Delete(ctx context.Context, db Handle, records ...R) error { return errext.WithCleanup(nil, "Stmt.Close", stmt.Close()) } -func deleteRecord(ctx context.Context, plan plan, v reflect.Value, recordIndex int, stmt handle.Statement, argumentIndexes [][]int, argumentSlots []any) error { +func deleteRecord(ctx context.Context, plan plan, v reflect.Value, recordIndex int, stmt gsql.Statement, argumentIndexes [][]int, argumentSlots []any) error { err := checkTransparentPointerStructFieldsInitialized("DELETE", recordIndex, v, plan, true) if err != nil { return errext.WithCleanup(err, "Stmt.Close", stmt.Close()) @@ -253,7 +252,7 @@ func deleteRecord(ctx context.Context, plan plan, v reflect.Value, recordIndex i // Returns an error if [NewStore] was called without the [TableNameIs] or [PrimaryKeyIs] options, which are both required to generate the respective queries for this method. // - For record types that do not have fields declared with the "auto" tag, an INSERT ... ON CONFLICT statement is used. // 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]) Upsert(ctx context.Context, db Handle, records ...*R) error { +func (s Store[R]) Upsert(ctx context.Context, db gsql.Handle, records ...*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. @@ -281,7 +280,7 @@ func (s Store[R]) Upsert(ctx context.Context, db Handle, records ...*R) error { return err } -func (s Store[R]) doUpsert(ctx context.Context, db Handle, insertStmt, updateStmt handle.Statement, records []*R) error { +func (s Store[R]) doUpsert(ctx context.Context, db gsql.Handle, insertStmt, updateStmt gsql.Statement, records []*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. diff --git a/query_test.go b/query_test.go index 7016c61..4dd3caf 100644 --- a/query_test.go +++ b/query_test.go @@ -10,6 +10,7 @@ import ( "time" "go.xyrillian.de/gg/assert" + "go.xyrillian.de/gg/gsql" "go.xyrillian.de/oblast" "go.xyrillian.de/oblast/internal/testhelpers/mock" "go.xyrillian.de/oblast/internal/testhelpers/must" @@ -18,7 +19,7 @@ import ( func TestInsertBasic(t *testing.T) { ctx := t.Context() md := mock.NewDriver() - db := oblast.NewDB(sql.OpenDB(md)) + db := gsql.NewDB(sql.OpenDB(md)) type basicRecord struct { ID int64 `db:"id,auto"` @@ -80,7 +81,7 @@ func TestInsertBasic(t *testing.T) { func TestInsertWithUintPrimaryKey(t *testing.T) { ctx := t.Context() md := mock.NewDriver() - db := oblast.NewDB(sql.OpenDB(md)) + db := gsql.NewDB(sql.OpenDB(md)) type exoticRecord struct { ID uint64 `oblast:"id,auto"` @@ -113,7 +114,7 @@ func TestInsertWithUintPrimaryKey(t *testing.T) { func TestUpdateBasic(t *testing.T) { ctx := t.Context() md := mock.NewDriver() - db := oblast.NewDB(sql.OpenDB(md)) + db := gsql.NewDB(sql.OpenDB(md)) type basicRecord struct { ID int64 `db:"id,auto"` @@ -143,7 +144,7 @@ func TestUpdateBasic(t *testing.T) { func TestDeleteBasic(t *testing.T) { ctx := t.Context() md := mock.NewDriver() - db := oblast.NewDB(sql.OpenDB(md)) + db := gsql.NewDB(sql.OpenDB(md)) type basicRecord struct { ID int64 `db:"id,auto"` @@ -173,7 +174,7 @@ func TestDeleteBasic(t *testing.T) { func TestUpsertBasicWithAutoColumn(t *testing.T) { ctx := t.Context() md := mock.NewDriver() - db := oblast.NewDB(sql.OpenDB(md)) + db := gsql.NewDB(sql.OpenDB(md)) type basicRecord struct { ID int64 `db:"id,auto"` @@ -217,7 +218,7 @@ func TestUpsertBasicWithAutoColumn(t *testing.T) { func TestWriteQueriesNotPossible(t *testing.T) { ctx := t.Context() md := mock.NewDriver() - db := oblast.NewDB(sql.OpenDB(md)) + db := gsql.NewDB(sql.OpenDB(md)) type basicRecord struct { ID int64 `db:"id,auto"` @@ -246,7 +247,7 @@ func TestWriteQueriesNotPossible(t *testing.T) { func TestWriteQueriesFailDuringPrepare(t *testing.T) { ctx := t.Context() md := mock.NewDriver() - db := oblast.NewDB(sql.OpenDB(md)) + db := gsql.NewDB(sql.OpenDB(md)) type basicRecord struct { ID int64 `db:"id,auto"` @@ -295,7 +296,7 @@ func TestWriteQueriesFailDuringPrepare(t *testing.T) { func TestUpdateOrUpsertFailsOnMissingRecord(t *testing.T) { ctx := t.Context() md := mock.NewDriver() - db := oblast.NewDB(sql.OpenDB(md)) + db := gsql.NewDB(sql.OpenDB(md)) type basicRecord struct { ID int64 `db:"id,auto"` @@ -330,7 +331,7 @@ func TestUpdateOrUpsertFailsOnMissingRecord(t *testing.T) { func TestInsertFailsOnFilledAutoField(t *testing.T) { ctx := t.Context() md := mock.NewDriver() - db := oblast.NewDB(sql.OpenDB(md)) + db := gsql.NewDB(sql.OpenDB(md)) type basicRecord struct { ID int64 `db:"id,auto"` @@ -349,7 +350,7 @@ func TestInsertFailsOnFilledAutoField(t *testing.T) { func TestInsertAndUpsertWithNoAutoColumns(t *testing.T) { ctx := t.Context() md := mock.NewDriver() - db := oblast.NewDB(sql.OpenDB(md)) + db := gsql.NewDB(sql.OpenDB(md)) type relation struct { FooID int64 `db:"foo_id"` @@ -380,7 +381,7 @@ func TestInsertAndUpsertWithNoAutoColumns(t *testing.T) { func TestUpsertFailsOnMixedAutoFieldState(t *testing.T) { ctx := t.Context() md := mock.NewDriver() - db := oblast.NewDB(sql.OpenDB(md)) + db := gsql.NewDB(sql.OpenDB(md)) type complexRecord struct { ID int64 `db:"id,auto"` @@ -405,7 +406,7 @@ func TestUpsertFailsOnMixedAutoFieldState(t *testing.T) { func TestUninitializedTransparentPointerStructs(t *testing.T) { ctx := t.Context() md := mock.NewDriver() - db := oblast.NewDB(sql.OpenDB(md)) + db := gsql.NewDB(sql.OpenDB(md)) // declare a record type that has a transparent pointer struct containing non-primary-key fields type timestamps struct { diff --git a/runtimeindex_test.go b/runtimeindex_test.go index 59044c4..dd65b07 100644 --- a/runtimeindex_test.go +++ b/runtimeindex_test.go @@ -8,6 +8,7 @@ import ( "testing" "go.xyrillian.de/gg/assert" + "go.xyrillian.de/gg/gsql" "go.xyrillian.de/oblast" "go.xyrillian.de/oblast/internal/testhelpers/mock" "go.xyrillian.de/oblast/internal/testhelpers/must" @@ -16,7 +17,7 @@ import ( func TestRuntimeIndex(t *testing.T) { ctx := t.Context() md := mock.NewDriver() - db := oblast.NewDB(sql.OpenDB(md)) + db := gsql.NewDB(sql.OpenDB(md)) type basicRecord struct { ID int64 `db:"id"` diff --git a/select.go b/select.go index 37edb92..c171655 100644 --- a/select.go +++ b/select.go @@ -11,8 +11,8 @@ import ( "reflect" "go.xyrillian.de/gg/errext" + "go.xyrillian.de/gg/gsql" . "go.xyrillian.de/gg/option" - "go.xyrillian.de/oblast/handle" ) // Select executes the provided SQL query and fills an instance of the record type R for each row in the result set, @@ -20,7 +20,7 @@ import ( // // An error is returned if any column name in the result set does not correspond to an addressable field in R. // Errors can be retrieved through the methods on type [Selection]. -func (s Store[R]) Select(ctx context.Context, db Handle, query string, args ...any) Selection[R] { +func (s Store[R]) Select(ctx context.Context, db gsql.Handle, query string, args ...any) Selection[R] { // 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. @@ -41,15 +41,15 @@ func (s Store[R]) Select(ctx context.Context, db Handle, query string, args ...a // // Returns an error if [NewStore] was called without the [TableNameIs] option, which is required to generate a query for this method. // Errors can be retrieved through the methods on type [Selection]. -func (s Store[R]) SelectWhere(ctx context.Context, db Handle, partialQuery string, args ...any) Selection[R] { +func (s Store[R]) SelectWhere(ctx context.Context, db gsql.Handle, partialQuery string, args ...any) Selection[R] { // 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. return Selection[R]{startSelectWhereQuery(ctx, db, s.plan, partialQuery, args...)} } -func startSelectQuery(ctx context.Context, db Handle, plan plan, query string, args ...any) selection { - rows, err := db.OblastQuery(ctx, query, args) +func startSelectQuery(ctx context.Context, db gsql.Handle, plan plan, query string, args ...any) selection { + rows, err := db.GSQLQuery(ctx, query, args) if err != nil { return selection{Err: fmt.Errorf("during Query(): %w", err)} } @@ -81,12 +81,12 @@ func startSelectQuery(ctx context.Context, db Handle, plan plan, query string, a } } -func startSelectWhereQuery(ctx context.Context, db Handle, plan plan, partialQuery string, args ...any) selection { +func startSelectWhereQuery(ctx context.Context, db gsql.Handle, plan plan, partialQuery string, args ...any) selection { if plan.Select.Query == "" { return selection{Err: errors.New("cannot execute SelectWhere() because query could not be autogenerated")} } query := plan.Select.Query + partialQuery - rows, err := db.OblastQuery(ctx, query, args) + rows, err := db.GSQLQuery(ctx, query, args) if err != nil { return selection{Err: fmt.Errorf("during Query(): %w", err)} } @@ -106,7 +106,7 @@ func startSelectWhereQuery(ctx context.Context, db Handle, plan plan, partialQue // // Warning: Because of limitations in the interface of database/sql, this function is built on [Store.Select] and cannot be any faster than it. // For maximum performance, use [Store.SelectOneWhere] which avoids the overhead of potentially having to read multiple rows. -func (s Store[R]) SelectOne(ctx context.Context, db Handle, query string, args ...any) (R, error) { +func (s Store[R]) SelectOne(ctx context.Context, db gsql.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. // @@ -117,7 +117,7 @@ func (s Store[R]) SelectOne(ctx context.Context, db Handle, query string, args . } // SelectOneOrNone is like SelectOne, but returns [None] instead of [sql.ErrNoRows]. -func (s Store[R]) SelectOneOrNone(ctx context.Context, db Handle, query string, args ...any) (Option[R], error) { +func (s Store[R]) SelectOneOrNone(ctx context.Context, db gsql.Handle, query string, args ...any) (Option[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. @@ -129,7 +129,7 @@ func (s Store[R]) SelectOneOrNone(ctx context.Context, db Handle, query string, // // This method is more efficient than [Store.SelectOne] on CPU runtime, but has a slight memory allocation overhead per call from query preparation. // This can be avoided by using [Store.PrepareSelectQueryWhere] instead. -func (s Store[R]) SelectOneWhere(ctx context.Context, db Handle, partialQuery string, args ...any) (R, error) { +func (s Store[R]) SelectOneWhere(ctx context.Context, db gsql.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. @@ -139,14 +139,14 @@ func (s Store[R]) SelectOneWhere(ctx context.Context, db Handle, partialQuery st } // SelectOneOrNoneWhere is like SelectOneWhere, but returns [None] instead of [sql.ErrNoRows]. -func (s Store[R]) SelectOneOrNoneWhere(ctx context.Context, db Handle, partialQuery string, args ...any) (Option[R], error) { +func (s Store[R]) SelectOneOrNoneWhere(ctx context.Context, db gsql.Handle, partialQuery string, args ...any) (Option[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. return noRowsToNone(s.SelectOneWhere(ctx, db, partialQuery, args...)) } -func selectOneWhere(ctx context.Context, db Handle, plan plan, v reflect.Value, partialQuery string, args []any) error { +func selectOneWhere(ctx context.Context, db gsql.Handle, plan plan, v reflect.Value, partialQuery string, args []any) error { if plan.Select.Query == "" { return errors.New("cannot execute SelectOneWhere() because query could not be autogenerated") } @@ -154,7 +154,7 @@ func selectOneWhere(ctx context.Context, db Handle, plan plan, v reflect.Value, return selectOne(ctx, db, plan, v, query, args) } -func selectOne(ctx context.Context, db Handle, plan plan, v reflect.Value, query string, args []any) error { +func selectOne(ctx context.Context, db gsql.Handle, plan plan, v reflect.Value, query string, args []any) error { for _, field := range plan.TransparentPointerStructFields { f := v.FieldByIndex(field.Index) f.Set(reflect.New(f.Type().Elem())) @@ -163,7 +163,7 @@ func selectOne(ctx context.Context, db Handle, plan plan, v reflect.Value, query for idx, index := range plan.Select.ScanIndexes { slots[idx] = v.FieldByIndex(index).Addr().Interface() } - stmt, err := db.OblastPrepare(ctx, query, false) + stmt, err := db.GSQLPrepare(ctx, query, false) if err != nil { return err } @@ -218,14 +218,14 @@ type PreparedSelectQuery[R any] struct { } // Select behaves the same as [Store.SelectWhere], but uses the query that was precomputed when q was constructed. -func (q PreparedSelectQuery[R]) Select(ctx context.Context, db Handle, args ...any) Selection[R] { +func (q PreparedSelectQuery[R]) Select(ctx context.Context, db gsql.Handle, args ...any) Selection[R] { // 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. return Selection[R]{startSelectQuery(ctx, db, q.store.plan, q.query, args...)} } // SelectOne behaves the same as [Store.SelectOneWhere], but uses the query that was precomputed when q was constructed. -func (q PreparedSelectQuery[R]) SelectOne(ctx context.Context, db Handle, args ...any) (R, error) { +func (q PreparedSelectQuery[R]) SelectOne(ctx context.Context, db gsql.Handle, 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. @@ -235,7 +235,7 @@ func (q PreparedSelectQuery[R]) SelectOne(ctx context.Context, db Handle, args . } // SelectOneOrNone is like SelectOne, but returns [None] instead of [sql.ErrNoRows]. -func (q PreparedSelectQuery[R]) SelectOneOrNone(ctx context.Context, db Handle, args ...any) (Option[R], error) { +func (q PreparedSelectQuery[R]) SelectOneOrNone(ctx context.Context, db gsql.Handle, args ...any) (Option[R], error) { return noRowsToNone(q.SelectOne(ctx, db, args...)) } @@ -254,7 +254,7 @@ type Selection[R any] struct { // This separate type does not have type arguments and thus is not duplicated by monomorphization. type selection struct { // from startSelectQuery() - Rows handle.Rows + Rows gsql.Rows Slots []any // NOTE: len(s.Slots) == len(s.Indexes) Err error // NOTE: if this field is set, all other fields will be unset // from plan diff --git a/select_test.go b/select_test.go index 3d6ed55..c2b319e 100644 --- a/select_test.go +++ b/select_test.go @@ -9,6 +9,7 @@ import ( "testing" "time" + "go.xyrillian.de/gg/gsql" . "go.xyrillian.de/gg/option" "go.xyrillian.de/gg/assert" @@ -20,7 +21,7 @@ import ( func TestSelectReturningSomeRecords(t *testing.T) { ctx := t.Context() md := mock.NewDriver() - db := oblast.NewDB(sql.OpenDB(md)) + db := gsql.NewDB(sql.OpenDB(md)) type basicRecord struct { ID int64 `db:"id"` @@ -138,7 +139,7 @@ func TestSelectReturningSomeRecords(t *testing.T) { func TestSelectReturningNoRecords(t *testing.T) { ctx := t.Context() md := mock.NewDriver() - db := oblast.NewDB(sql.OpenDB(md)) + db := gsql.NewDB(sql.OpenDB(md)) type basicRecord struct { ID int64 `db:"id"` @@ -229,7 +230,7 @@ func TestSelectReturningNoRecords(t *testing.T) { func TestSelectIntoUnexpectedField(t *testing.T) { ctx := t.Context() md := mock.NewDriver() - db := oblast.NewDB(sql.OpenDB(md)) + db := gsql.NewDB(sql.OpenDB(md)) type basicRecord struct { ID int64 `db:"id"` @@ -268,7 +269,7 @@ func TestSelectIntoUnexpectedField(t *testing.T) { func TestSelectWithScanError(t *testing.T) { ctx := t.Context() md := mock.NewDriver() - db := oblast.NewDB(sql.OpenDB(md)) + db := gsql.NewDB(sql.OpenDB(md)) type basicRecord struct { ID int64 `db:"id"` @@ -331,7 +332,7 @@ func TestSelectWithScanError(t *testing.T) { func TestSelectIntoEmbeddedTypes(t *testing.T) { ctx := t.Context() md := mock.NewDriver() - db := oblast.NewDB(sql.OpenDB(md)) + db := gsql.NewDB(sql.OpenDB(md)) type HasCreatedAt struct { CreatedAt time.Time `db:"created_at"` @@ -442,7 +443,7 @@ func TestSelectIntoEmbeddedTypes(t *testing.T) { func TestSelectCapturingQueryError(t *testing.T) { ctx := t.Context() md := mock.NewDriver() - db := oblast.NewDB(sql.OpenDB(md)) + db := gsql.NewDB(sql.OpenDB(md)) type basicRecord struct { ID int64 `db:"id"` @@ -490,7 +491,7 @@ func TestSelectCapturingQueryError(t *testing.T) { func TestSelectCapturingCloseError(t *testing.T) { ctx := t.Context() md := mock.NewDriver() - db := oblast.NewDB(sql.OpenDB(md)) + db := gsql.NewDB(sql.OpenDB(md)) type basicRecord struct { ID int64 `db:"id"` @@ -553,7 +554,7 @@ func TestSelectCapturingCloseError(t *testing.T) { func TestSelectNotPossibleWithoutTableName(t *testing.T) { ctx := t.Context() md := mock.NewDriver() - db := oblast.NewDB(sql.OpenDB(md)) + db := gsql.NewDB(sql.OpenDB(md)) type basicRecord struct { ID int64 `db:"id"` -- cgit v1.3.1