aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md7
-rw-r--r--benchmark/benchmark_test.go44
-rw-r--r--benchmark/postgres_test.go26
-rw-r--r--handle.go130
-rw-r--r--oblast.go12
-rw-r--r--query_test.go22
-rw-r--r--runtimeindex_test.go2
-rw-r--r--select_test.go16
8 files changed, 168 insertions, 91 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 315d337..9d2a141 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,13 @@ SPDX-FileCopyrightText: 2026 Stefan Majewsky <majewsky@gmx.net>
SPDX-License-Identifier: Apache-2.0
-->
+# v0.9.0 (TBD)
+
+API changes:
+
+- The generic function `Wrap` is replaced with explicit functions `NewDB`, `NewConn` and `NewTx` that yield separate types.
+ This allows our wrapped types to carry the original types from database/sql as embedded types, thus making their use more ergonomic.
+
# v0.8.0 (2026-05-13)
API changes:
diff --git a/benchmark/benchmark_test.go b/benchmark/benchmark_test.go
index 4bc7950..f779b08 100644
--- a/benchmark/benchmark_test.go
+++ b/benchmark/benchmark_test.go
@@ -43,14 +43,14 @@ var (
batchSizesForUpdate = []int{1, 2, 4, 8, 16, 100}
)
-func makeSqliteTestDB(t testing.TB, recordCount int) (db oblast.SqlHandle[*sql.DB], dsn string) {
+func makeSqliteTestDB(t testing.TB, recordCount int) (db *oblast.DB, dsn string) {
dsn = fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())
- db = oblast.Wrap(must.Return(sql.Open("sqlite3", dsn))(t))
- _ = must.Return(db.Base.Exec(`CREATE TABLE entries (id INTEGER, message TEXT, PRIMARY KEY (id AUTOINCREMENT))`))(t)
+ db = oblast.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 {
// fill in some random-looking, but deterministic data
- stmt := must.Return(db.Base.Prepare(`INSERT INTO entries (id, message) VALUES (?, ?)`))(t)
+ stmt := must.Return(db.Prepare(`INSERT INTO entries (id, message) VALUES (?, ?)`))(t)
for idx := range recordCount {
buf := sha256.Sum256([]byte(strconv.Itoa(idx)))
_ = must.Return(stmt.Exec(idx, fmt.Sprintf("sha256:%x", buf[:])))(t)
@@ -91,7 +91,7 @@ func BenchmarkORMSelectMany(b *testing.B) {
oblast.TableNameIs("entries"),
oblast.PrimaryKeyIs("id"),
)
- gorpDB := gorp.DbMap{Db: db.Base, Dialect: gorp.SqliteDialect{}}
+ gorpDB := gorp.DbMap{Db: db.DB, Dialect: gorp.SqliteDialect{}}
gormDB := must.Return(gorm.Open(sqlite.Open(dsn), &gorm.Config{}))(b)
partialQuery := `id < ` + strconv.Itoa(batchSize)
query := `SELECT * FROM entries WHERE ` + partialQuery
@@ -120,7 +120,7 @@ func BenchmarkORMSelectMany(b *testing.B) {
selectWithSqlite := func(b *testing.B) {
var count int
- rows := must.Return(db.Base.Query(query))(b) //nolint:rowserrcheck // false positive
+ rows := must.Return(db.Query(query))(b) //nolint:rowserrcheck // false positive
var (
id int64
message string
@@ -185,7 +185,7 @@ func BenchmarkORMSelectOne(b *testing.B) {
oblast.TableNameIs("entries"),
oblast.PrimaryKeyIs("id"),
)
- gorpDB := gorp.DbMap{Db: db.Base, Dialect: gorp.SqliteDialect{}}
+ gorpDB := gorp.DbMap{Db: db.DB, Dialect: gorp.SqliteDialect{}}
gormDB := must.Return(gorm.Open(sqlite.Open(dsn), &gorm.Config{}))(b)
partialQuery := `id = ` + strconv.Itoa(recordID)
query := `SELECT * FROM entries WHERE ` + partialQuery
@@ -217,7 +217,7 @@ func BenchmarkORMSelectOne(b *testing.B) {
id int64
message string
)
- must.Succeed(b, db.Base.QueryRow(query).Scan(&id, &message))
+ must.Succeed(b, db.QueryRow(query).Scan(&id, &message))
assert.Equal(b, id, int64(recordID))
}
@@ -265,7 +265,7 @@ func BenchmarkORMInsertAndDelete(b *testing.B) {
oblast.TableNameIs("entries"),
oblast.PrimaryKeyIs("id"),
)
- gorpDB := gorp.DbMap{Db: db.Base, Dialect: gorp.SqliteDialect{}}
+ gorpDB := gorp.DbMap{Db: db.DB, Dialect: gorp.SqliteDialect{}}
gorpDB.AddTableWithName(GorpEntry{}, "entries").SetKeys(true, "id")
gormDB := must.Return(gorm.Open(sqlite.Open(dsn), &gorm.Config{}))(b)
@@ -351,23 +351,23 @@ func BenchmarkORMInsertAndDelete(b *testing.B) {
insertAndDeleteWithStraightExec := func(b *testing.B) {
ids := make([]int64, batchSize)
for idx := range ids {
- result := must.Return(db.Base.Exec(`INSERT INTO entries (message) VALUES (?)`, "hello"))(b)
+ result := must.Return(db.Exec(`INSERT INTO entries (message) VALUES (?)`, "hello"))(b)
ids[idx] = must.Return(result.LastInsertId())(b)
}
for _, id := range ids {
- _ = must.Return(db.Base.Exec(`DELETE FROM entries WHERE id = ?`, id))(b)
+ _ = must.Return(db.Exec(`DELETE FROM entries WHERE id = ?`, id))(b)
}
}
insertAndDeleteWithPreparedExec := func(b *testing.B) {
ids := make([]int64, batchSize)
- stmtInsert := must.Return(db.Base.Prepare(`INSERT INTO entries (message) VALUES (?)`))(b)
+ stmtInsert := must.Return(db.Prepare(`INSERT INTO entries (message) VALUES (?)`))(b)
defer stmtInsert.Close()
for idx := range ids {
result := must.Return(stmtInsert.Exec("hello"))(b)
ids[idx] = must.Return(result.LastInsertId())(b)
}
- stmtDelete := must.Return(db.Base.Prepare(`DELETE FROM entries WHERE id = ?`))(b)
+ stmtDelete := must.Return(db.Prepare(`DELETE FROM entries WHERE id = ?`))(b)
defer stmtDelete.Close()
for _, id := range ids {
_ = must.Return(stmtDelete.Exec(id))(b)
@@ -377,21 +377,21 @@ func BenchmarkORMInsertAndDelete(b *testing.B) {
insertAndDeleteWithStraightQueryRow := func(b *testing.B) {
ids := make([]int64, batchSize)
for idx := range ids {
- must.Succeed(b, db.Base.QueryRow(`INSERT INTO entries (message) VALUES (?) RETURNING id`, "hello").Scan(&ids[idx]))
+ must.Succeed(b, db.QueryRow(`INSERT INTO entries (message) VALUES (?) RETURNING id`, "hello").Scan(&ids[idx]))
}
for _, id := range ids {
- _ = must.Return(db.Base.Exec(`DELETE FROM entries WHERE id = ?`, id))(b)
+ _ = must.Return(db.Exec(`DELETE FROM entries WHERE id = ?`, id))(b)
}
}
insertAndDeleteWithPreparedQueryRow := func(b *testing.B) {
ids := make([]int64, batchSize)
- stmtInsert := must.Return(db.Base.Prepare(`INSERT INTO entries (message) VALUES (?) RETURNING id`))(b)
+ stmtInsert := must.Return(db.Prepare(`INSERT INTO entries (message) VALUES (?) RETURNING id`))(b)
defer stmtInsert.Close()
for idx := range ids {
must.Succeed(b, stmtInsert.QueryRow("hello").Scan(&ids[idx]))
}
- stmtDelete := must.Return(db.Base.Prepare(`DELETE FROM entries WHERE id = ?`))(b)
+ stmtDelete := must.Return(db.Prepare(`DELETE FROM entries WHERE id = ?`))(b)
defer stmtDelete.Close()
for _, id := range ids {
_ = must.Return(stmtDelete.Exec(id))(b)
@@ -450,7 +450,7 @@ func BenchmarkORMUpdate(b *testing.B) {
oblast.TableNameIs("entries"),
oblast.PrimaryKeyIs("id"),
)
- gorpDB := gorp.DbMap{Db: db.Base, Dialect: gorp.SqliteDialect{}}
+ gorpDB := gorp.DbMap{Db: db.DB, Dialect: gorp.SqliteDialect{}}
gorpDB.AddTableWithName(GorpEntry{}, "entries").SetKeys(true, "id")
gormDB := must.Return(gorm.Open(sqlite.Open(dsn), &gorm.Config{}))(b)
@@ -458,7 +458,7 @@ func BenchmarkORMUpdate(b *testing.B) {
for _, batchSize := range batchSizesForUpdate {
b.Run("N="+strconv.Itoa(batchSize), func(b *testing.B) {
// prepare a bunch of records that we can update, in a reproducible way
- _ = must.Return(db.Base.Exec(`DELETE FROM entries`))
+ _ = must.Return(db.Exec(`DELETE FROM entries`))
recordsForOblast := make([]OblastEntry, batchSize)
recordsForOblastForInsert := make([]*OblastEntry, batchSize)
for idx := range recordsForOblast {
@@ -498,11 +498,11 @@ func BenchmarkORMUpdate(b *testing.B) {
}
updateWithStraightSqlite := func(b *testing.B, message string) {
for _, r := range recordsForOblast {
- _ = must.Return(db.Base.Exec(`UPDATE entries SET message = ? WHERE id = ?`, message, r.ID))(b)
+ _ = must.Return(db.Exec(`UPDATE entries SET message = ? WHERE id = ?`, message, r.ID))(b)
}
}
updateWithPreparedSqlite := func(b *testing.B, message string) {
- stmt := must.Return(db.Base.Prepare(`UPDATE entries SET message = ? WHERE id = ?`))(b)
+ stmt := must.Return(db.Prepare(`UPDATE entries SET message = ? WHERE id = ?`))(b)
for _, r := range recordsForOblast {
_ = must.Return(stmt.Exec(message, r.ID))(b)
}
@@ -510,7 +510,7 @@ func BenchmarkORMUpdate(b *testing.B) {
}
checkRecordsUpdated := func(b *testing.B, message string) {
var count int64
- must.Succeed(b, db.Base.QueryRow(`SELECT COUNT(*) FROM entries WHERE message = ?`, message).Scan(&count))
+ must.Succeed(b, db.QueryRow(`SELECT COUNT(*) FROM entries WHERE message = ?`, message).Scan(&count))
assert.Equal(b, count, int64(batchSize))
}
diff --git a/benchmark/postgres_test.go b/benchmark/postgres_test.go
index 02c2c43..8f1b7e8 100644
--- a/benchmark/postgres_test.go
+++ b/benchmark/postgres_test.go
@@ -36,14 +36,14 @@ func BenchmarkPostgresHeadingHeadingHeadingHeadingHeadingHeadingHeadingHeading(b
const defaultPostgresDSN = "host=localhost user=postgres dbname=oblast_benchmark sslmode=disable"
-func connectToPostgresTestDB(t testing.TB, recordCount int) oblast.SqlHandle[*sql.DB] {
+func connectToPostgresTestDB(t testing.TB, recordCount int) *oblast.DB {
dsn := cmp.Or(os.Getenv("BENCHMARK_POSTGRES_DSN"), defaultPostgresDSN)
- db := oblast.Wrap(must.Return(sql.Open("postgres", dsn))(t))
- _ = must.Return(db.Base.Exec(`CREATE TEMPORARY TABLE entries (id BIGSERIAL, message TEXT)`))(t)
+ db := oblast.NewDB(must.Return(sql.Open("postgres", dsn))(t))
+ _ = must.Return(db.Exec(`CREATE TEMPORARY TABLE entries (id BIGSERIAL, message TEXT)`))(t)
if recordCount > 0 {
// fill in some random-looking, but deterministic data
- stmt := must.Return(db.Base.Prepare(`INSERT INTO entries (id, message) VALUES ($1, $2)`))(t)
+ stmt := must.Return(db.Prepare(`INSERT INTO entries (id, message) VALUES ($1, $2)`))(t)
for idx := range recordCount {
buf := sha256.Sum256([]byte(strconv.Itoa(idx)))
_ = must.Return(stmt.Exec(idx, fmt.Sprintf("sha256:%x", buf[:])))(t)
@@ -107,7 +107,7 @@ func BenchmarkPostgresSelect(b *testing.B) {
b.Run("driver=pq/strategy=straight", func(b *testing.B) {
for b.Loop() {
var records []OblastEntry
- rows := must.Return(pqDB.Base.Query(query))(b) //nolint:rowserrcheck // false positive
+ rows := must.Return(pqDB.Query(query))(b) //nolint:rowserrcheck // false positive
for rows.Next() {
var e OblastEntry
must.Succeed(b, rows.Scan(&e.ID, &e.Message))
@@ -173,7 +173,7 @@ func BenchmarkPostgresSelectOne(b *testing.B) {
id int64
message string
)
- must.Succeed(b, pqDB.Base.QueryRow(query).Scan(&id, &message))
+ must.Succeed(b, pqDB.QueryRow(query).Scan(&id, &message))
assert.Equal(b, id, int64(recordID))
}
})
@@ -239,10 +239,10 @@ func BenchmarkPostgresInsertAndDelete(b *testing.B) {
for b.Loop() {
ids := make([]int64, batchSize)
for idx := range ids {
- must.Succeed(b, pqDB.Base.QueryRow(insertQuery, "hello").Scan(&ids[idx]))
+ must.Succeed(b, pqDB.QueryRow(insertQuery, "hello").Scan(&ids[idx]))
}
for _, id := range ids {
- _ = must.Return(pqDB.Base.Exec(deleteQuery, id))(b)
+ _ = must.Return(pqDB.Exec(deleteQuery, id))(b)
}
}
})
@@ -262,12 +262,12 @@ func BenchmarkPostgresInsertAndDelete(b *testing.B) {
b.Run("driver=pq/strategy=prepared", func(b *testing.B) {
for b.Loop() {
ids := make([]int64, batchSize)
- stmtInsert := must.Return(pqDB.Base.Prepare(insertQuery))(b)
+ stmtInsert := must.Return(pqDB.Prepare(insertQuery))(b)
defer stmtInsert.Close()
for idx := range ids {
must.Succeed(b, stmtInsert.QueryRow("hello").Scan(&ids[idx]))
}
- stmtDelete := must.Return(pqDB.Base.Prepare(deleteQuery))(b)
+ stmtDelete := must.Return(pqDB.Prepare(deleteQuery))(b)
defer stmtDelete.Close()
for _, id := range ids {
_ = must.Return(stmtDelete.Exec(id))(b)
@@ -309,7 +309,7 @@ func BenchmarkPostgresUpdate(b *testing.B) {
for _, batchSize := range batchSizesForInsertDelete {
b.Run("N="+strconv.Itoa(batchSize), func(b *testing.B) {
// prepare a bunch of records that we can update, in a reproducible way
- _ = must.Return(pqDB.Base.Exec(`DELETE FROM entries`))
+ _ = must.Return(pqDB.Exec(`DELETE FROM entries`))
_ = must.Return(pgxConn.Exec(noctx, `DELETE FROM entries`))
pqRecords := make([]OblastEntry, batchSize)
pqRecordsForInsert := make([]*OblastEntry, batchSize)
@@ -356,7 +356,7 @@ func BenchmarkPostgresUpdate(b *testing.B) {
b.Run("driver=pq/strategy=straight", func(b *testing.B) {
loop(b, func(message string) {
for _, r := range pqRecords {
- _ = must.Return(pqDB.Base.Exec(updateQuery, message, r.ID))(b)
+ _ = must.Return(pqDB.Exec(updateQuery, message, r.ID))(b)
}
})
})
@@ -371,7 +371,7 @@ func BenchmarkPostgresUpdate(b *testing.B) {
b.Run("driver=pq/strategy=prepared", func(b *testing.B) {
loop(b, func(message string) {
- stmt := must.Return(pqDB.Base.Prepare(updateQuery))(b)
+ stmt := must.Return(pqDB.Prepare(updateQuery))(b)
for _, r := range pqRecords {
_ = must.Return(stmt.Exec(message, r.ID))(b)
}
diff --git a/handle.go b/handle.go
index 2d79ea4..f35ae35 100644
--- a/handle.go
+++ b/handle.go
@@ -12,47 +12,120 @@ import (
)
// Handle contains behavior that database handles must offer to Oblast.
-// The standard-library types [*sql.DB] and [*sql.Tx] can satisfy this interface through the [Wrap] function.
// Custom implementations of this interface can be used to connect non-std database drivers to Oblast.
type Handle = handle.Handle
-// SqlHandle wraps types like [*sql.DB] or [*sql.Tx] into a [Handle] that can be used with Oblast.
-// TODO: separate types for wrapped *sql.DB and wrapped *sql.Tx, so we can have those types as embedded fields and forward method implementations
-type SqlHandle[T SqlExecutor] struct {
- // The original database or transaction handle.
- // It is safe to read this field to execute operations that Oblast does not handle (e.g. transactions, savepoints or OLAP queries).
- Base T
+////////////////////////////////////////////////////////////////////////////////
+// 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}}
+}
- // If this is not true, then any methods on this type will panic.
- // This is just to enforce that the handle is constructed with Wrap(), thus guaranteeing future compatibility if actual important private struct fields are added later.
- ok bool
+// 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
}
-// Wrap converts an [*sql.DB] or [*sql.Tx] into a [Handle] that can be used with Oblast functions.
-func Wrap[T SqlExecutor](dbOrTx T) SqlHandle[T] {
- return SqlHandle[T]{Base: dbOrTx, ok: true}
+// 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
}
-// SqlExecutor is an interface covered by both [*sql.DB] and [*sql.Tx].
-// It appears in the signature of function [Wrap].
-type SqlExecutor interface {
+// 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
}
-// static assertion that the respective types implement the interface
-var (
- _ SqlExecutor = &sql.DB{}
- _ SqlExecutor = &sql.Tx{}
-)
+// 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 !h.ok {
- panic("SqlHandle was not constructed through oblast.Wrap()!")
- }
+func (h sqlHandle[T]) OblastPrepare(ctx context.Context, query string, repeated bool) (handle.Statement, error) {
if !repeated {
return wrappedStatement{h.Base, query, nil}, nil
}
@@ -64,15 +137,12 @@ func (h SqlHandle[T]) OblastPrepare(ctx context.Context, query string, repeated
}
// OblastQuery implements the [Handle] interface.
-func (h SqlHandle[T]) OblastQuery(ctx context.Context, query string, args []any) (handle.Rows, error) {
- if !h.ok {
- panic("SqlHandle was not constructed through oblast.Wrap()!")
- }
+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
+ db sqlExecutor
query string
stmt *sql.Stmt // nil if repeated = false
}
diff --git a/oblast.go b/oblast.go
index 5adb33e..78b498f 100644
--- a/oblast.go
+++ b/oblast.go
@@ -24,9 +24,7 @@
//
// Then use it many times to perform load and store operations:
//
-// func doStuff(db *sql.DB) error {
-// dbh := oblast.Wrap(db)
-//
+// func doStuff(db *oblast.DB) error {
// newEntry := LogEntry{
// CreatedAt: time.Now(),
// Message: "Hello World.",
@@ -44,6 +42,10 @@
// fmt.Printf("there are %d log entries so far", len(allEntries))
// }
//
+// In this example, "oblast.DB" is a thin wrapper around [*sql.DB], which can be obtained with the [NewDB] function.
+// A [*DB] can be used in the same way as an [*sql.DB], but if Oblast is only to be used for specific functions,
+// then individual [*sql.Conn] or [*sql.Tx] instances can also be wrapped with the [NewConn] and [NewTx] functions.
+//
// # Mapping rules for record types
//
// If the database column has a different name (or casing, e.g. "id" vs. "ID") than the field name, provide it in the field tag "db".
@@ -133,9 +135,7 @@ func StructTagKeyIs(key string) PlanOption {
return func(opts *planOpts) { opts.StructTagKey = key }
}
-// Store is the main interface of this library.
-//
-// It holds information on how to read and write data into record type R,
+// Store holds information on how to read and write data into record type R,
// and can also be used to execute autogenerated queries if the respective [PlanOption] values were provided during [NewStore].
type Store[R any] struct {
dialect Dialect
diff --git a/query_test.go b/query_test.go
index 382a463..a67dade 100644
--- a/query_test.go
+++ b/query_test.go
@@ -18,7 +18,7 @@ import (
func TestInsertBasic(t *testing.T) {
ctx := t.Context()
md := mock.NewDriver()
- db := oblast.Wrap(sql.OpenDB(md))
+ db := oblast.NewDB(sql.OpenDB(md))
type basicRecord struct {
ID int64 `oblast:"id,auto"`
@@ -52,7 +52,7 @@ func TestInsertBasic(t *testing.T) {
func TestUpdateBasic(t *testing.T) {
ctx := t.Context()
md := mock.NewDriver()
- db := oblast.Wrap(sql.OpenDB(md))
+ db := oblast.NewDB(sql.OpenDB(md))
type basicRecord struct {
ID int64 `db:"id,auto"`
@@ -82,7 +82,7 @@ func TestUpdateBasic(t *testing.T) {
func TestDeleteBasic(t *testing.T) {
ctx := t.Context()
md := mock.NewDriver()
- db := oblast.Wrap(sql.OpenDB(md))
+ db := oblast.NewDB(sql.OpenDB(md))
type basicRecord struct {
ID int64 `db:"id,auto"`
@@ -112,7 +112,7 @@ func TestDeleteBasic(t *testing.T) {
func TestUpsertBasicWithAutoColumn(t *testing.T) {
ctx := t.Context()
md := mock.NewDriver()
- db := oblast.Wrap(sql.OpenDB(md))
+ db := oblast.NewDB(sql.OpenDB(md))
type basicRecord struct {
ID int64 `db:"id,auto"`
@@ -158,7 +158,7 @@ func TestUpsertBasicWithAutoColumn(t *testing.T) {
func TestWriteQueriesNotPossible(t *testing.T) {
ctx := t.Context()
md := mock.NewDriver()
- db := oblast.Wrap(sql.OpenDB(md))
+ db := oblast.NewDB(sql.OpenDB(md))
type basicRecord struct {
ID int64 `db:"id,auto"`
@@ -187,7 +187,7 @@ func TestWriteQueriesNotPossible(t *testing.T) {
func TestWriteQueriesFailDuringPrepare(t *testing.T) {
ctx := t.Context()
md := mock.NewDriver()
- db := oblast.Wrap(sql.OpenDB(md))
+ db := oblast.NewDB(sql.OpenDB(md))
type basicRecord struct {
ID int64 `db:"id,auto"`
@@ -236,7 +236,7 @@ func TestWriteQueriesFailDuringPrepare(t *testing.T) {
func TestUpdateOrUpsertFailsOnMissingRecord(t *testing.T) {
ctx := t.Context()
md := mock.NewDriver()
- db := oblast.Wrap(sql.OpenDB(md))
+ db := oblast.NewDB(sql.OpenDB(md))
type basicRecord struct {
ID int64 `db:"id,auto"`
@@ -271,7 +271,7 @@ func TestUpdateOrUpsertFailsOnMissingRecord(t *testing.T) {
func TestInsertFailsOnFilledAutoField(t *testing.T) {
ctx := t.Context()
md := mock.NewDriver()
- db := oblast.Wrap(sql.OpenDB(md))
+ db := oblast.NewDB(sql.OpenDB(md))
type basicRecord struct {
ID int64 `db:"id,auto"`
@@ -294,7 +294,7 @@ func TestInsertFailsOnFilledAutoField(t *testing.T) {
func TestInsertAndUpsertWithNoAutoColumns(t *testing.T) {
ctx := t.Context()
md := mock.NewDriver()
- db := oblast.Wrap(sql.OpenDB(md))
+ db := oblast.NewDB(sql.OpenDB(md))
type relation struct {
FooID int64 `db:"foo_id"`
@@ -325,7 +325,7 @@ func TestInsertAndUpsertWithNoAutoColumns(t *testing.T) {
func TestUpsertFailsOnMixedAutoFieldState(t *testing.T) {
ctx := t.Context()
md := mock.NewDriver()
- db := oblast.Wrap(sql.OpenDB(md))
+ db := oblast.NewDB(sql.OpenDB(md))
type complexRecord struct {
ID int64 `db:"id,auto"`
@@ -350,7 +350,7 @@ func TestUpsertFailsOnMixedAutoFieldState(t *testing.T) {
func TestUninitializedTransparentPointerStructs(t *testing.T) {
ctx := t.Context()
md := mock.NewDriver()
- db := oblast.Wrap(sql.OpenDB(md))
+ db := oblast.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 8e0b68f..ea77560 100644
--- a/runtimeindex_test.go
+++ b/runtimeindex_test.go
@@ -16,7 +16,7 @@ import (
func TestRuntimeIndex(t *testing.T) {
ctx := t.Context()
md := mock.NewDriver()
- db := oblast.Wrap(sql.OpenDB(md))
+ db := oblast.NewDB(sql.OpenDB(md))
type basicRecord struct {
ID int64 `db:"id"`
diff --git a/select_test.go b/select_test.go
index 7b4191a..c55fe42 100644
--- a/select_test.go
+++ b/select_test.go
@@ -20,7 +20,7 @@ import (
func TestSelectReturningSomeRecords(t *testing.T) {
ctx := t.Context()
md := mock.NewDriver()
- db := oblast.Wrap(sql.OpenDB(md))
+ db := oblast.NewDB(sql.OpenDB(md))
type basicRecord struct {
ID int64 `db:"id"`
@@ -138,7 +138,7 @@ func TestSelectReturningSomeRecords(t *testing.T) {
func TestSelectReturningNoRecords(t *testing.T) {
ctx := t.Context()
md := mock.NewDriver()
- db := oblast.Wrap(sql.OpenDB(md))
+ db := oblast.NewDB(sql.OpenDB(md))
type basicRecord struct {
ID int64 `db:"id"`
@@ -229,7 +229,7 @@ func TestSelectReturningNoRecords(t *testing.T) {
func TestSelectIntoUnexpectedField(t *testing.T) {
ctx := t.Context()
md := mock.NewDriver()
- db := oblast.Wrap(sql.OpenDB(md))
+ db := oblast.NewDB(sql.OpenDB(md))
type basicRecord struct {
ID int64 `db:"id"`
@@ -268,7 +268,7 @@ func TestSelectIntoUnexpectedField(t *testing.T) {
func TestSelectWithScanError(t *testing.T) {
ctx := t.Context()
md := mock.NewDriver()
- db := oblast.Wrap(sql.OpenDB(md))
+ db := oblast.NewDB(sql.OpenDB(md))
type basicRecord struct {
ID int64 `db:"id"`
@@ -331,7 +331,7 @@ func TestSelectWithScanError(t *testing.T) {
func TestSelectIntoEmbeddedTypes(t *testing.T) {
ctx := t.Context()
md := mock.NewDriver()
- db := oblast.Wrap(sql.OpenDB(md))
+ db := oblast.NewDB(sql.OpenDB(md))
type HasCreatedAt struct {
CreatedAt time.Time `db:"created_at"`
@@ -442,7 +442,7 @@ func TestSelectIntoEmbeddedTypes(t *testing.T) {
func TestSelectCapturingQueryError(t *testing.T) {
ctx := t.Context()
md := mock.NewDriver()
- db := oblast.Wrap(sql.OpenDB(md))
+ db := oblast.NewDB(sql.OpenDB(md))
type basicRecord struct {
ID int64 `db:"id"`
@@ -490,7 +490,7 @@ func TestSelectCapturingQueryError(t *testing.T) {
func TestSelectCapturingCloseError(t *testing.T) {
ctx := t.Context()
md := mock.NewDriver()
- db := oblast.Wrap(sql.OpenDB(md))
+ db := oblast.NewDB(sql.OpenDB(md))
type basicRecord struct {
ID int64 `db:"id"`
@@ -553,7 +553,7 @@ func TestSelectCapturingCloseError(t *testing.T) {
func TestSelectNotPossibleWithoutTableName(t *testing.T) {
ctx := t.Context()
md := mock.NewDriver()
- db := oblast.Wrap(sql.OpenDB(md))
+ db := oblast.NewDB(sql.OpenDB(md))
type basicRecord struct {
ID int64 `db:"id"`