Documentation
¶
Overview ¶
Package dbx provides opinionated database utilities built on top of sqlx. It covers three areas: opening databases with migrations, type-safe prepared statement wrappers using generics, and cursor-based pagination.
Opening a database ¶
Open and OpenSQLX open a database connection and optionally apply SQLite pragmas and schema migrations. Migrations are read from an fs.FS (typically an embedded filesystem) and applied via golang-migrate. Configuration is done through functional options:
db, err := dbx.OpenSQLX(
dbx.WithDSN(":memory:"),
dbx.WithDriver("sqlite"),
dbx.WithPragmas(
"PRAGMA foreign_keys = ON",
"PRAGMA synchronous = NORMAL",
),
dbx.WithMigrations(migrationsFS, "migrations"),
)
Prepared statements ¶
The package provides generic function types that wrap prepared statements. Constructors prepare immediately and panic on invalid SQL so that wiring errors are caught at init time rather than at runtime (with modernc.org/sqlite this needs v1.53.0 or later, which compiles statements in Prepare).
The underlying prepared statements are captured in closures and cannot be closed individually — they are released when the database connection is closed. This is by design for the recommended init-time wiring pattern:
- NewExecFunc — simple operations (DELETE, etc.) with positional args
- NewNamedExecFunc — INSERT/UPDATE with a struct, returns Result
- NewQueryRowxFunc — single-row query with positional args
- NewEntityQueryRowxFunc — struct in, struct out (e.g. RETURNING *)
- NewSelectFunc — bounded result sets
- NewQueryxIteratorFunc — streaming large result sets via range iterator
- NewPaginatedSelectFunc — cursor-based paginated SELECT (see Pagination)
Transactions ¶
The Tx* function types mirror the prepared-statement types above but execute against a *sqlx.Tx instead of *sqlx.DB. They are prepared on the *sqlx.DB at construction time and bound to the transaction on each call, so the SQL is parsed once per pooled connection. Construct them outside any transaction (typically in a newXStmts constructor): on a database limited to one connection, preparing while a transaction holds it would wait forever. Open limits in-memory SQLite databases to one connection, since each connection to one is a separate database.
Each call binds the statement to the transaction, and database/sql keeps that binding until the transaction ends, so keep transactions that make very many calls short.
RunTx handles the begin/commit/rollback boilerplate:
addTx := dbx.NewTxNamedExecFunc[Device](db, "INSERT INTO devices ... VALUES ...")
pskTx := dbx.NewTxNamedExecFunc[PSK](db, "INSERT INTO psk ... VALUES ...")
err := dbx.RunTx(ctx, db, func(tx *sqlx.Tx) error {
if _, err := addTx(ctx, tx, device); err != nil {
return err
}
_, err := pskTx(ctx, tx, psk)
return err
})
Available Tx function types:
- NewTxExecFunc — positional-arg exec inside a transaction
- NewTxNamedExecFunc — named exec (struct) inside a transaction
- NewTxQueryRowxFunc — single-row query inside a transaction
- NewTxEntityQueryRowxFunc — struct in, struct out inside a transaction
- NewTxBatchExecFunc — multi-row statement for a slice of structs
Batch inserts ¶
NewTxBatchExecFunc inserts many rows with few statements. It takes the statement head, a named template for one row and a tail, prepares statements for 1 to maxRows rows, and binds struct fields by their db tags:
insert := dbx.NewTxBatchExecFunc[Event](db,
"INSERT INTO events (time, path) VALUES ", "(:time, :path)",
" ON CONFLICT DO NOTHING", 8)
Only the row template is parsed for names; head and tail are used verbatim.
err := dbx.RunTx(ctx, db, func(tx *sqlx.Tx) error {
_, err := insert(ctx, tx, events)
return err
})
Each statement costs the driver a fixed amount of work (with a cancellable context, modernc.org/sqlite starts a goroutine per statement), so batching is much faster than one statement per row. Keep maxRows small: some drivers bind parameters in time quadratic in their number.
Pagination ¶
NewPaginatedSelectFunc builds cursor-based paginated SELECT queries, ordered by a column plus a unique tie-breaker column so that equal values are neither skipped nor repeated across pages. Table and column names are validated as safe SQL identifiers, and the statements are prepared (and rebound via sqlx.Rebind) at construction time. Cursors are opaque.
Breaking changes in v1.0.0 ¶
WithPragmas now accepts variadic string arguments instead of a string slice. Change WithPragmas([]string{...}) to WithPragmas(...).
ValidSQLIdentifier now rejects identifiers that start with a digit, aligning with standard SQL identifier rules.
Index ¶
- Constants
- Variables
- func Open(opts ...Option) (*sql.DB, error)
- func OpenSQLX(opts ...Option) (*sqlx.DB, error)
- func RunTx(ctx context.Context, db *sqlx.DB, fn func(tx *sqlx.Tx) error) error
- func ValidSQLIdentifier(s string) bool
- type DriverForFunc
- type EntityQueryRowxFunc
- type ExecFunc
- type NamedExecFunc
- type Option
- type PageRequest
- type PageResponse
- type PaginatedSelectFunc
- type QueryRowxFunc
- type QueryxIteratorFunc
- type Result
- type SelectFunc
- type TxBatchExecFunc
- type TxEntityQueryRowxFunc
- type TxExecFunc
- type TxNamedExecFunc
- type TxQueryRowxFunc
Constants ¶
const MaxPageSize = 1000
MaxPageSize is the upper bound on PageRequest.PageSize. Requests exceeding this value are silently clamped.
Variables ¶
var ( ErrDatabaseTooNew = errors.New("database may be newer than migrations available in this binary") ErrDirtyDatabase = errors.New("database is in a dirty migration state; fix or force version before continuing") ErrNoDSN = errors.New("no data source name given") )
errors
var ( // ErrMutuallyExclusive is returned when both After and Before are set in a PageRequest. ErrMutuallyExclusive = errors.New("after and before are mutually exclusive") // ErrInvalidPageSize is returned for a negative PageRequest.PageSize. ErrInvalidPageSize = errors.New("page size must not be negative") // ErrInvalidCursor is returned for an After or Before cursor that was not // produced by a PaginatedSelectFunc. ErrInvalidCursor = errors.New("invalid page cursor") )
errors
Functions ¶
func RunTx ¶
RunTx executes fn inside a transaction. If fn returns an error or panics the transaction is rolled back; otherwise it is committed.
func ValidSQLIdentifier ¶
ValidSQLIdentifier reports whether s is safe to interpolate as a SQL identifier. Only ASCII letters, digits, and underscores are allowed, and the identifier must not start with a digit.
Types ¶
type DriverForFunc ¶
DriverForFunc returns (driver, migrateDBName, error).
type EntityQueryRowxFunc ¶
EntityQueryRowxFunc is useful for queries that take some entity and return an entity of the same type. For instance when using RETURNING in SQL statement.
func NewEntityQueryRowxFunc ¶
func NewEntityQueryRowxFunc[T any](db *sqlx.DB, stmt string) EntityQueryRowxFunc[T]
NewEntityQueryRowxFunc creates a new EntityQueryRowxFunc
type ExecFunc ¶
ExecFunc is useful for very simple operations like DELETE with positional arguments. It returns an error wrapping sql.ErrNoRows when the driver reports zero rows affected, so it suits statements expected to change at least one row. That includes DDL and updates that leave a row unchanged (on MySQL without clientFoundRows); use NamedExecFunc, which returns the Result, for those.
type NamedExecFunc ¶
NamedExecFunc is useful for create, update etc where you send an entity in and just want a Result and error back.
func NewNamedExecFunc ¶
func NewNamedExecFunc[T any](db *sqlx.DB, stmt string) NamedExecFunc[T]
NewNamedExecFunc creates a new NamedExecFunc
type Option ¶
type Option func(*config) error
Option is a configuration option callback type
func WithLogger ¶ added in v2.1.0
WithLogger sets the logger used while opening the database. It defaults to slog.Default.
func WithMigrationDriver ¶
func WithMigrationDriver(sqlDriverName string, migrateName string, create func(*sql.DB) (database.Driver, error)) Option
WithMigrationDriver is provided in case you want to use SQL databases beyond those provided in the default config (sqlite, postgres).
func WithMigrations ¶
WithMigrations sets the migrations filesystem and path within that filesystem. You can either pass an embed.FS or a fs.FS for a OS filesystem path using os.DirFS(path).
func WithPragmas ¶
WithPragmas appends pragmas to the config. Pragmas are executed as raw SQL statements on every new connection in the pool, since many settings (such as SQLite's foreign_keys) apply per connection. They must be trusted inputs — never pass user-controlled strings.
type PageRequest ¶
type PageRequest struct {
// PageSize is the maximum number of rows to return. Zero means unlimited.
PageSize int
// After is the cursor for forward pagination (exclusive lower bound).
// Mutually exclusive with Before. Only applied when PageSize > 0.
After string
// Before is the cursor for backward pagination (exclusive upper bound).
// Mutually exclusive with After. Only applied when PageSize > 0.
Before string
}
PageRequest carries cursor-based pagination parameters for list operations. A zero PageSize means "return all results" (backward compatible); a negative one is an error.
After and Before are mutually exclusive: setting both is an error. After requests results after the given cursor (forward pagination), while Before requests results before the given cursor (backward pagination).
Note: After and Before are only applied when PageSize > 0 (i.e. when IsPaginated returns true). Setting a cursor without a page size will return all rows and silently ignore the cursor value.
func (PageRequest) IsPaginated ¶
func (p PageRequest) IsPaginated() bool
IsPaginated returns true when the caller has requested a bounded page.
type PageResponse ¶
type PageResponse struct {
// HasMore is true when more rows exist in the direction of travel:
// after LastCursor for a first or After page, before FirstCursor for a
// Before page. It says nothing about the opposite direction.
HasMore bool
// FirstCursor is the cursor of the first item in the returned page.
FirstCursor string
// LastCursor is the cursor of the last item in the returned page.
LastCursor string
}
PageResponse carries pagination metadata returned from list operations. Cursors are opaque: pass them back as PageRequest.After or Before.
type PaginatedSelectFunc ¶
type PaginatedSelectFunc[T any] func(ctx context.Context, page PageRequest, args ...any) ([]T, PageResponse, error)
PaginatedSelectFunc performs a cursor-based paginated SELECT query. The args are positional parameters for the base WHERE clause provided at construction time.
func NewPaginatedSelectFunc ¶
func NewPaginatedSelectFunc[T any](db *sqlx.DB, table, orderCol, keyCol, baseWhere string, cursorFn func(T) (order, key string)) PaginatedSelectFunc[T]
NewPaginatedSelectFunc creates a PaginatedSelectFunc that returns rows ordered by orderCol, then keyCol.
keyCol must be unique (usually the primary key): it breaks ties between rows with the same orderCol value, so no row is skipped or repeated across pages. If orderCol is itself unique, pass it as keyCol too.
table, orderCol and keyCol must be valid SQL identifiers (the constructor panics otherwise, consistent with other dbx constructors that panic on invalid input detected at init time).
baseWhere is an optional filter condition, with or without a leading WHERE keyword (e.g. "tenant_id = ?"). Pass "" to select all rows. It is wrapped in parentheses, so an OR in it cannot capture the cursor condition. Use ? placeholders only; they are rebound for the driver. baseWhere is interpolated directly into the query; it must be a trusted string — never pass user-controlled input.
All statements are prepared at construction, so invalid SQL, including an identifier that is a reserved word, panics there.
cursorFn returns a row's orderCol and keyCol values as strings. They are encoded into the opaque cursors of PageResponse and bound as string parameters when a cursor is used, so the database must compare those columns correctly against text (SQLite does for INTEGER columns; a timestamp must be formatted exactly as stored).
type QueryRowxFunc ¶
QueryRowxFunc is useful for queries that return one row and takes positional arguments. For instance get operations on a single row.
func NewQueryRowxFunc ¶
func NewQueryRowxFunc[T any](db *sqlx.DB, stmt string) QueryRowxFunc[T]
NewQueryRowxFunc creates a new QueryRowxFunc
type QueryxIteratorFunc ¶
QueryxIteratorFunc is useful for queries that return (potentially) large result sets and you want to be able to stream the result. The query runs each time the returned iterator is ranged over.
func NewQueryxIteratorFunc ¶
func NewQueryxIteratorFunc[T any](db *sqlx.DB, stmt string) QueryxIteratorFunc[T]
NewQueryxIteratorFunc creates a new QueryxIteratorFunc
type Result ¶
type Result struct {
// contains filtered or unexported fields
}
Result holds the outcome of a mutating operation
func NewResult ¶
NewResult wraps a sql.Result into a dbx.Result, extracting LastInsertId and RowsAffected if available.
func (Result) LastInsertID ¶
LastInsertID returns the last insert ID if available
func (Result) RowsAffected ¶
RowsAffected returns the number of rows affected if available
type SelectFunc ¶
SelectFunc is useful for when you perform selects and you know the result set will be small or at least bounded to acceptable size.
func NewSelectFunc ¶
func NewSelectFunc[T any](db *sqlx.DB, stmt string) SelectFunc[T]
NewSelectFunc creates a new SelectFunc
type TxBatchExecFunc ¶
TxBatchExecFunc executes a multi-row statement for a slice of entities inside a transaction, one statement per chunk of up to maxRows entities. It returns one Result per statement, in order.
func NewTxBatchExecFunc ¶
NewTxBatchExecFunc creates a TxBatchExecFunc. The statement is head, then row once per entity separated by commas, then tail:
head: "INSERT INTO foo (name, ts) VALUES " row: "(:name, :ts)" tail: " ON CONFLICT DO NOTHING" (or "")
Names in row map to T's db tags as in NewNamedExecFunc and are replaced by the driver's bindvar; a literal colon in row is written "::". head and tail are used verbatim: they are not parsed for names, "::" in them stays "::", and they must not contain placeholders.
Statements for 1 to maxRows rows are prepared on db at construction time. Keep maxRows small: some drivers bind parameters in time quadratic in their number, and SQLite allows at most 32766 per statement.
A nil pointer on the path to a named field, such as a nil element when T is a pointer type, is returned as an error.
type TxEntityQueryRowxFunc ¶
TxEntityQueryRowxFunc executes a named statement inside a transaction that takes an entity and returns an entity of the same type. It mirrors EntityQueryRowxFunc but operates on a *sqlx.Tx.
func NewTxEntityQueryRowxFunc ¶
func NewTxEntityQueryRowxFunc[T any](db *sqlx.DB, stmt string) TxEntityQueryRowxFunc[T]
NewTxEntityQueryRowxFunc creates a TxEntityQueryRowxFunc. The statement is prepared on db at construction time, see NewTxExecFunc. A query that yields no rows returns an error wrapping sql.ErrNoRows.
type TxExecFunc ¶
TxExecFunc executes a statement with positional arguments inside a transaction. It mirrors ExecFunc, including its sql.ErrNoRows on zero rows affected, but operates on a *sqlx.Tx.
func NewTxExecFunc ¶
func NewTxExecFunc(db *sqlx.DB, stmt string) TxExecFunc
NewTxExecFunc creates a TxExecFunc. The statement is prepared on db at construction time and bound to the transaction on each call, so it is parsed once. Construct it outside any transaction: on a database with a single connection, preparing inside one would wait forever.
type TxNamedExecFunc ¶
TxNamedExecFunc executes a named statement inside a transaction. It mirrors NamedExecFunc but operates on a *sqlx.Tx.
func NewTxNamedExecFunc ¶
func NewTxNamedExecFunc[T any](db *sqlx.DB, stmt string) TxNamedExecFunc[T]
NewTxNamedExecFunc creates a TxNamedExecFunc. The statement is prepared on db at construction time, see NewTxExecFunc.
type TxQueryRowxFunc ¶
TxQueryRowxFunc queries a single row inside a transaction with positional arguments. It mirrors QueryRowxFunc but operates on a *sqlx.Tx.
func NewTxQueryRowxFunc ¶
func NewTxQueryRowxFunc[T any](db *sqlx.DB, stmt string) TxQueryRowxFunc[T]
NewTxQueryRowxFunc creates a TxQueryRowxFunc. The statement is prepared on db at construction time, see NewTxExecFunc.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
examples
|
|
|
open
command
Example: opening a database with migrations and pragmas.
|
Example: opening a database with migrations and pragmas. |
|
paginate
command
Example: cursor-based pagination.
|
Example: cursor-based pagination. |
|
prepared
command
Example: prepared statement functions for CRUD operations.
|
Example: prepared statement functions for CRUD operations. |