sqldb

package module
v0.4.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 23, 2026 License: MIT Imports: 25 Imported by: 0

Documentation

Overview

Package sqldb wraps *bun.DB with lifecycle management, query builder proxies, error mapping, and transaction support.

This is a separate Go submodule (github.com/credo-go/credo/store/sqldb) so that applications not using SQL databases do not pull in the Bun dependency.

Creating a Connection

db, err := sqldb.Open(&sqldb.Config{
    Driver: "postgres",
    Host:   "localhost",
    Port:   5432,
    Name:   "myapp",
    User:   "postgres",
    Password: "secret",
    MaxOpen: 25,
    MaxIdle: new(10),
    MaxIdleTime: 5 * time.Minute,
    MaxLifetime: 30 * time.Minute,
})

Driver-family detection uses an exact, case-insensitive allowlist: postgres/pgx, mysql, and sqlite/sqlite3/sqliteshim. A differently named registered driver uses its native Config.DSN plus WithDialect; a connector uses WithConnector plus WithDialect. Credo does not infer a family from a substring or a connector's concrete type, and rejects known driver/dialect family mismatches. When Credo builds a PostgreSQL or MySQL DSN, Port must be between 1 and 65535 and IPv6 hosts are bracketed correctly. Use Config.DSN or WithConnector for driver-native default-port or socket behavior.

Positive PostgreSQL ConnectTimeout values are rounded up to whole seconds so a sub-second value cannot silently disable the timeout. Config.Options cannot override reserved endpoint/credential fields or a simultaneously configured SSLMode/ConnectTimeout; conflicting sources fail without including option values in the error. Explicit nil WithDialect/WithConnector values also fail. SSLMode is driver-specific (PostgreSQL sslmode, MySQL tls), and Credo imposes no universal TLS default; production configuration must choose the driver's verified mode explicitly.

Connection Pool

Credo does not choose a workload-independent finite pool size. MaxOpen=0 preserves database/sql's unlimited-open behavior. If the pool is still unlimited when a successful canonical store.Register inspects it, the app logger emits one structured warning with code sqldb.pool.max_open_unlimited. Standalone users can inspect DB.StoreRegistrationWarningCodes during bootstrap and route the same secret-free codes through their own logger.

MaxIdle uses a pointer to distinguish absence from an explicit zero: nil makes Credo leave the idle limit unset (the effective database/sql default remains subject to MaxOpen), new(0) retains no idle connections, and a positive value is applied exactly. With MaxOpen > 0, MaxIdle must not exceed MaxOpen; Open rejects the configuration instead of relying on database/sql's silent clamp. MaxIdleTime=0 disables idle-age expiry and MaxLifetime=0 disables lifetime expiry. Positive values are applied unchanged.

DB.Stats returns the complete sql.DBStats snapshot. Health details include current open/in-use/idle counts and cumulative wait/closure counters, but readiness does not serialize adapter details. Credo does not derive StatusDegraded from pool saturation: that policy remains gated on production metrics, an explicit SLO, windowed deltas, hysteresis, and opt-in thresholds. Because all stores are currently critical, DEGRADED removes readiness and a noisy universal threshold could cascade across replicas.

Lifecycle Identity

DB implements store.LifecycleIdentityProvider. ResourceIdentity returns the *DB pointer, giving store.Register a stable physical-resource token. A semantic wrapper that embeds *DB inherits this method through ordinary Go method promotion. A named-field wrapper that implements Lifecycle itself must explicitly forward ResourceIdentity to the underlying DB; Credo does not inspect wrapper fields.

The duplicate-resource guarantee is scoped to one store.Registry and its store.Register calls. Do not publish the same *DB again under another DI type with raw Provide/ProvideFactory/ProvideValue/ProvideProtectedValue/Replace; use App.Alias for an interface view.

Query Builder Proxies

The DB type exposes Select, Insert, Update, and Delete methods that return proxy query builders. These proxies inject transactions from context, map errors to store.Err* sentinels, and provide escape hatches (Apply, ApplyQueryBuilder, Unwrap) for advanced usage. Each builder accepts at most one optional model. Supplying more causes the builder to record an error that its terminal returns without executing. SelectQuery's curated Limit and Offset methods also guard Bun v1.2.18's signed-int32 storage: an out-of-range int records ErrInvalidLimitOffset and the terminal returns before database execution. Values inside the int32 range, including zero and negatives, retain Bun's native semantics. Apply and Unwrap expose raw Bun builders and therefore retain Bun's own narrowing contract instead of this curated-method guard.

var user User
err := db.Select(&user).Where("id = ?", id).Scan(ctx)
// err is already mapped: sql.ErrNoRows → store.ErrNotFound

ApplyQueryBuilder surfaces Bun's shared bun.QueryBuilder so a single WHERE predicate (tenant scoping, soft-delete filters, ownership checks) can be reused across Select, Update, and Delete instead of being duplicated per query type. Interceptors are preserved, like Apply.

Bun Visibility Policy

Credo does not hide Bun — it integrates it. The proxy layer exists to attach two guarantees to query execution, not to abstract Bun away:

  • Transaction injection: terminal methods (Scan, Count, Exists, Exec) resolve the connection from the context at execution time, so code inside InTx/RunInTx transparently runs on the transaction. Select terminals use an internal execution snapshot that preserves the explicit connection, builder error, WherePK, soft-delete flags, and model/relation state, so the builder may be reused across executions and TX boundaries.
  • Error mapping: the same terminals pass driver errors through the store.Err* mapping before returning.

Bun types therefore appear in proxy signatures (bun.IConn, bun.QueryBuilder, Apply callbacks) by design. When the curated proxy surface lacks something:

  • Missing builder method: use Apply (per query type) or ApplyQueryBuilder (shared WHERE predicates). Both keep the terminal guarantees intact.
  • Missing terminal method: request an addition to the curated set — the guarantees live in the terminals, so they must be on the proxy.
  • Conn is the transaction-aware native Bun escape hatch. It returns the active transaction or the base DB, but native executions still bypass Credo error mapping.
  • Unwrap and Client are deliberate opt-outs: executions through the raw Bun objects they return get neither automatic TX injection nor error mapping.

SelectQuery.Clone is the public, top-level builder-fork API. It preserves the execution fields patched by Credo but is not a recursive object-graph copy: a bound destination and nested CTE/relation query values may remain shared. Do not mutate or scan shared values concurrently through source and clone.

Transactions

Use InTx (or the package-level RunInTx) to execute a function within a transaction. The adapter stores the TX in a private per-DB scope so repositories using sqldb proxies pick it up automatically without cross-DB collisions:

err := db.InTx(ctx, func(ctx context.Context) error {
    // repos pick up the scoped TX automatically
    return nil
})

InTxWith / RunInTxWith accept sql.TxOptions for configuring isolation level and read-only mode on the outer transaction. Nested calls use a Bun savepoint; because a savepoint cannot apply new transaction options, a nested call with non-default options returns ErrNestedTxOptions instead of silently ignoring them. From a handler, pass the request context: db.InTx(ctx.Context(), fn). Nested savepoint creation and cleanup are cancellation-safe and bounded: callback queries retain their original context, while savepoint operations use an internal context controlled by Credo. An uncertain begin/release/ rollback marks the shared transaction rollback-only before a fail-safe ambient abort, so an outer callback cannot swallow the error and commit; that outer InTx returns ErrTxRollbackOnly. Savepoint operations and ambient abort each use a five-second default budget; WithTxCleanupTimeout overrides it without limiting callback execution time.

A callback error is returned unchanged after rollback; only begin, rollback, and commit driver errors are mapped. Panic rollback preserves the original panic value. A nil callback returns ErrNilTxCallback before BEGIN. A commit error can leave the database outcome unknown, so it must not be treated as proof that no changes were applied or as an unconditional retry signal.

For a native Bun operation that must participate in the transaction, use Conn with the callback context. The returned bun.IDB is borrowed and must not escape the callback:

err := db.InTx(ctx, func(txCtx context.Context) error {
    return db.Conn(txCtx).NewSelect().Model(&users).Scan(txCtx)
})

Pagination

SelectQuery.Page is the typed pagination terminal: it runs COUNT + a LIMIT/OFFSET SELECT and returns a ready *pagination.Page[T]. Like One and All it is a Form-A terminal, so T drives the table and destination and the query is built model-less:

req := &pagination.PageRequest{Page: 2, PerPage: 20} // normalized by BindQuery
page, err := db.Select().
    Where("active = ?", true).
    OrderExpr("created_at DESC, id DESC").
    Page[User](ctx, req)

One, All, and Page require T to be the actual table model and reject a model bound through Select, Model, or Apply with ErrTypedTerminalModel before executing. TableExpr does not turn a typed terminal into a projection API; use TableExpr(...).Scan(ctx, &dest) for projections. For relations, bind the destination and use Scan instead:

var users []User
err := db.Select(&users).Relation("Orders").Scan(ctx)

BindQuery applies PageRequest.Validate, whose input policy defaults/clamps the request. Page does not repeat that policy. Instead it copies req and strictly validates the snapshot before touching the database: nil, non-positive Page or PerPage, native-int offset overflow, and values outside Bun v1.2.18's signed-int32 LIMIT/OFFSET range return pagination.ErrInvalidPageRequest before COUNT. The caller's request is never mutated; a valid PerPage above the package default cap (for example a custom normalized value of 100) remains valid and is not clamped by the terminal. When COUNT reports zero rows, SELECT is skipped and the page keeps the snapshot's page/per-page with a non-nil empty slice.

Total is complete logical projection-row cardinality before ordering and the Page-owned window. Credo removes root ORDER/LIMIT/OFFSET/FOR and counts a universal outer _credo_count_source: an ungrouped aggregate normally contributes one row, Distinct counts selected projection tuples, Group counts groups, and Group with Having counts groups left after Having. Count and Page reject Having without Group and direct UNION/INTERSECT/EXCEPT roots with ErrUnsupportedCountQuery before I/O. Advanced callers restructure those shapes behind an outer derived table or CTE, compose an explicit count query and data query, and build NewPage. MySQL validates derived-table output names when the logical COUNT executes. If it returns ER_DUP_FIELDNAME (1060), Count and Page wrap ErrUnsupportedCountQuery after I/O while preserving the driver cause; unique aliases resolve collisions. Non-count MySQL 1060 errors remain unchanged. The server error cannot identify which derived-table level failed, so an indistinguishable 1060 from a caller-supplied nested source is wrapped too. Driver causes are diagnostic and must not be rendered directly to HTTP. Because the logical projection is evaluated by the count source, expensive or volatile projections should use that explicit custom composition. There is no custom-count strategy until two real consumers require one, and Page never carries an unknown total. CursorPage is the accepted working shape for forward keyset pagination; Slice is only a working name for a future total-free offset result. Neither API is exported yet. Use a stable ORDER BY with a unique tie-breaker for deterministic offset pages. Relation callbacks are applied exactly once while rendering this private source. Predicates/projections are allowed; replacing the model or adding root ORDER/LIMIT/OFFSET/FOR or another unsupported shape fails before I/O.

Logical Count runs BeforeSelect, BeforeAppendModel, and successful-query AfterSelect on the private source. Page runs the hooks again when its data SELECT executes, so hook-added predicates/projections affect Total and Records. The outer count preserves QueryEvent.Model for observability while soft-delete policy is applied only inside the source. Count never scans or mutates a bound model, so AfterSelect observes its pre-count value. Hooks must be deterministic: transaction isolation cannot stabilize a volatile projection or application-side decision across the two statements.

COUNT and SELECT use internal execution snapshots and join the ambient transaction, but remain separate database statements. Page never starts an implicit transaction. PostgreSQL Read Committed can observe statement-level drift; PostgreSQL/InnoDB callers that require one snapshot establish a read-only Repeatable Read outer transaction and pass its txCtx to Page. SQLite keeps the first-read snapshot of a plain explicit InTx; WAL permits a concurrent writer while rollback-journal mode may serialize it. The pinned modernc SQLite driver does not reliably enforce TxOptions Isolation/ReadOnly, so those options are not a SQLite guarantee.

Page responds with the queried type directly. For a model→DTO response, run Page[Model] and reshape it with pagination's Page.Map, which carries the metadata over unchanged:

modelPage, err := db.Select().Where("active = ?", true).Page[Model](ctx, req)
if err != nil {
    return nil, err
}
dtoPage := modelPage.Map(func(m Model) DTO { return toDTO(m) })

When the conversion itself can fail, fetch the model page, map its records with ordinary error handling, and build the DTO page with NewPage:

modelPage, err := q.Page[Model](ctx, req)
// ...map modelPage.Records to dtos, returning any conversion error...
page := pagination.NewPage(
    dtos, modelPage.Total, modelPage.Page, modelPage.PerPage,
)

NewPage computes TotalPages with overflow-safe quotient-and-remainder ceiling division, including totals near math.MaxInt64.

Migrations

The DB wraps Bun's migration engine (bun/migrate — part of the already pinned Bun module) behind two methods: RegisterMigrations stores a *migrate.Migrations set at wiring time, and Migrate runs the pending ones. Multi-replica production should run the same Migrate method in one deadline-bounded pre-deploy job. Migrate also matches credo's App.OnStart hook; this is an opt-in convenience for development and deliberate single-replica deployments:

//go:embed migrations/*.sql
var sqlMigrations embed.FS

migrations := migrate.NewMigrations()
if err := migrations.Discover(sqlMigrations); err != nil {
    return err
}
db.RegisterMigrations(migrations)
app.OnStart(db.Migrate) // dev/single-replica convenience

Seeding is a plain migration file (for example 2_seed_plans.up.sql) — there is no separate seed mechanism. Mark-on-success gives at-least-once retry, not atomic rollback: migrations must be transactional where supported or idempotent/reconcilable. Bun's table lock is fail-fast. Unlock is detached from parent cancellation and caller-bounded to five seconds; timeout leaves its outcome uncertain and is not automatically retried. Direct Bun migrators used for rollback/status/generation do not inherit Credo's options. Status/generation repeat only relevant options; DB-mutating apply/rollback callers additionally own Init, Lock, and bounded Unlock.

Error Mapping

Terminal methods on the proxy types (Scan, Count, Exists, Exec) pass driver errors through mapError before returning. Common mappings:

  • sql.ErrNoRows → store.ErrNotFound
  • unique violation → store.ErrAlreadyExists (ErrDuplicate compatible)
  • foreign-key violation → store.ErrConstraint (ErrConflict compatible)
  • serialization failure → store.ErrSerialization
  • deadlock → store.ErrDeadlock
  • lock/busy contention → store.ErrContention
  • bad connection → store.ErrUnavailable
  • read-only / replica → store.ErrReadOnly
  • context deadline → store.ErrTimeout

The classifier is context- and driver-family-aware. Structured SQLSTATE, MySQL number envelopes, and SQLite numeric codes produce a *store.Error that preserves the original cause and driver code. Loose message matching is not used. Callers can branch with errors.Is or inspect store.KindOf; store.IsTransient describes a condition, not permission to retry an operation. Update.Exec and Delete.Exec do not convert "no rows affected" into ErrNotFound — inspect sql.Result for that.

Escape Hatch

Client() returns the underlying *bun.DB for model registration, advanced migration operations, and calls intentionally tied to the base DB. Queries executed via Client() bypass the proxy interceptors: there is no automatic TX injection from context and no error mapping to store.Err* sentinels. For native Bun work that must join an ambient transaction use Conn(ctx); for normal repository code use the proxy layer. Conn selects the right connection but still does not add Credo error mapping.

Stability

Beta, versioned independently from the root module (see the project README's "Maturity by Area" table). Breaking changes are possible before v1.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrNilTxCallback indicates that a transaction was requested without a
	// callback. The transaction is not started when this error is returned.
	ErrNilTxCallback = errors.New("sqldb: transaction callback must not be nil")

	// ErrNestedTxOptions indicates that non-default sql.TxOptions were supplied
	// to a nested transaction. Bun implements nested transactions with a
	// savepoint, where isolation and read-only options cannot be applied.
	ErrNestedTxOptions = errors.New("sqldb: nested transaction options are not supported")

	// ErrTxRollbackOnly indicates that a nested savepoint operation had an
	// uncertain outcome and poisoned the ambient transaction. The outer
	// transaction is aborted instead of being allowed to commit.
	ErrTxRollbackOnly = errors.New("sqldb: transaction is rollback-only")
)
View Source
var ErrInvalidLimitOffset = errors.New("sqldb: limit/offset is outside Bun v1.2.18 int32 range")

ErrInvalidLimitOffset reports a Limit or Offset value that Bun v1.2.18 cannot represent without narrowing. The builder records this error and the next terminal returns it without executing a query.

View Source
var ErrTypedTerminalModel = errors.New("sqldb: typed terminal requires a model-less query")

ErrTypedTerminalModel reports that One, All, or Page was called on a SelectQuery that already has a model bound to it. Typed terminals own their destination model, so overriding a pre-bound model would discard relation and model state silently.

View Source
var ErrUnsupportedCountQuery = errors.New("sqldb: unsupported Count/Page query shape")

ErrUnsupportedCountQuery reports a query shape that Count and Page cannot execute safely. Direct compound queries and HAVING without GROUP BY fail with this sentinel before I/O and must be restructured behind an outer derived table or CTE. Relation callbacks are rendered once and may not replace the model or add root ORDER/LIMIT/OFFSET/FOR or another unsupported count shape.

MySQL validates derived-table output names at execution. When the generated logical COUNT returns ER_DUP_FIELDNAME (1060), Count and Page wrap this sentinel while preserving the original driver cause. That mapping is local to logical COUNT; the same MySQL error from any other operation passes through unchanged.

Functions

func RunInTx

func RunInTx(ctx context.Context, db *DB, fn func(ctx context.Context) error) error

RunInTx starts a transaction, stores it in this DB's typed context scope, executes fn, and commits on nil / rolls back on error. Nested calls use a Bun savepoint. If fn panics, rollback is attempted and the original panic value is re-raised. A nil fn returns ErrNilTxCallback without starting a transaction.

A callback error is an application value: after a successful rollback it is returned unchanged and is never passed through the SQL driver error mapper. Begin, rollback, and commit errors are mapped separately. A commit error can leave the database outcome unknown; callers must not blindly retry solely because Commit returned an error.

Nested savepoint creation and cleanup are bounded and cancellation-aware. If their outcome becomes uncertain, the shared transaction state is marked rollback-only before the ambient SQL abort starts; an outer callback cannot swallow the nested error and commit. If a callback returns nil after its context was canceled, the savepoint is rolled back and the context error is returned.

The callback is the transaction lifetime boundary. It must not retain txCtx or start transaction work (including nested InTx calls) that outlives its return.

DB.InTx is the method form of this function.

func RunInTxWith

func RunInTxWith(ctx context.Context, db *DB, opts *sql.TxOptions, fn func(ctx context.Context) error) error

RunInTxWith is like RunInTx but accepts sql.TxOptions for configuring the outer transaction's isolation level and read-only mode. Bun implements a nested transaction as a savepoint, which cannot apply new transaction options; a nested call with non-default options returns ErrNestedTxOptions before creating the savepoint or invoking fn. Outer option support remains driver-specific; unsupported levels must not be assumed to have taken effect merely because Begin succeeded.

DB.InTxWith is the method form of this function.

Types

type Config

type Config struct {
	// Driver is the SQL driver name (e.g., "postgres", "pgx", "mysql", "sqlite3").
	// This must match the driver registered via a blank import in the application.
	Driver string

	// Host is the database server hostname or IP address.
	Host string

	// Port is the database server port number. It must be between 1 and 65535
	// when Credo builds a PostgreSQL or MySQL DSN. Use DSN or WithConnector for
	// driver-specific default-port behavior.
	Port int

	// Name is the database name.
	Name string

	// User is the database user.
	User string

	// Password is the database password.
	Password string

	// DSN is an optional raw DSN string. When set, it is used as-is and Credo
	// does not merge Host, credentials, SSLMode, ConnectTimeout, or Options into
	// it.
	DSN string

	// ConnectTimeout is the maximum time to wait for a connection to be
	// established. Zero means no timeout. PostgreSQL DSNs represent this value
	// in whole seconds, so positive fractional seconds are rounded up.
	ConnectTimeout time.Duration

	// MaxOpen is the maximum number of open connections. Zero keeps the
	// database/sql default, which is unlimited.
	MaxOpen int

	// MaxIdle is the maximum number of idle connections. Nil makes Credo leave
	// the idle limit unset; the effective database/sql default remains subject
	// to MaxOpen. A non-nil zero disables idle connections.
	MaxIdle *int

	// MaxLifetime is the maximum lifetime of a connection. Zero disables the
	// lifetime limit.
	MaxLifetime time.Duration

	// MaxIdleTime is the maximum amount of time a connection may remain idle.
	// Zero disables the idle-time limit.
	MaxIdleTime time.Duration

	// SSLMode sets the driver-specific SSL/TLS mode. PostgreSQL receives it as
	// sslmode (for example, "require" or "verify-full"); MySQL receives it as
	// tls (for example, "true" or a registered TLS config name). Credo does not
	// impose a cross-driver TLS default.
	SSLMode string

	// Options holds additional driver-specific connection parameters. Core
	// PostgreSQL endpoint and credential keys are reserved, and an option may not
	// duplicate SSLMode or ConnectTimeout when the corresponding field is set.
	// MySQL parseTime is always true and cannot be overridden. Ambiguous sources
	// fail when Credo builds the DSN; use DSN for full driver-native control.
	Options map[string]string
}

Config holds connection parameters for SQL databases.

type DB

type DB struct {
	// contains filtered or unexported fields
}

DB wraps *bun.DB with lifecycle management, query builder proxies, error mapping, and transaction support.

func Open

func Open(cfg *Config, opts ...Option) (*DB, error)

Open creates a DB from Config.

Steps:

  1. Build or use the provided DSN
  2. Open sql.DB (or use the provided Connector)
  3. Detect or use the provided dialect
  4. Create bun.DB
  5. Apply connection pool settings

func (*DB) Client

func (db *DB) Client() *bun.DB

Client returns the underlying *bun.DB for raw SQL, model registration, advanced migration operations, and any Bun feature not covered by the proxy layer.

Warning: queries executed via the returned *bun.DB bypass the proxy interceptors. There is no automatic TX injection from context (see DB.InTx / DB.Conn) and no error mapping to store.Err* sentinels. Use the proxy layer (DB.Select, DB.Insert, DB.Update, DB.Delete) for normal repository code. For an advanced Bun operation that must join an ambient transaction, build it through db.Conn(ctx). Reserve Client() for model registration and operations intentionally tied to the base DB, such as migration operations beyond DB.Migrate. A directly constructed Bun migrator does not inherit RegisterMigrations options. Status/generation callers repeat the options they need; DB-mutating apply/rollback callers must also own Init, Lock, and bounded cancellation-detached Unlock.

func (*DB) Conn

func (db *DB) Conn(ctx context.Context) bun.IDB

Conn returns the active transaction for this DB from ctx, or the underlying Bun DB when no transaction is active. It is the transaction-aware escape hatch for advanced Bun operations that are not covered by Credo's proxies:

err := db.InTx(ctx, func(txCtx context.Context) error {
	return db.Conn(txCtx).NewSelect().Model(&users).Scan(txCtx)
})

Conn does not lease a dedicated sql.Conn. The returned value is borrowed and must not escape the InTx callback when it represents a transaction. Queries executed through it use Bun directly and therefore bypass Credo error mapping. A nil context selects the underlying DB.

func (*DB) Delete

func (db *DB) Delete(model ...any) *DeleteQuery

Delete creates a new DeleteQuery proxy. It accepts zero or one model; more than one records a builder error that Exec returns.

func (*DB) Exec

func (db *DB) Exec(ctx context.Context, query string, args ...any) (sql.Result, error)

Exec executes a raw SQL query with TX injection and error mapping.

func (*DB) Health

func (db *DB) Health(ctx context.Context) store.Health

Health returns structured health information including status, round-trip latency, and connection pool statistics.

func (*DB) InTx

func (db *DB) InTx(ctx context.Context, fn func(ctx context.Context) error) error

InTx is the method form of RunInTx: it starts a transaction, stores it in the context passed to fn, and commits on nil / rolls back on error. Nested calls use Bun's SAVEPOINT automatically. If fn panics, the transaction is rolled back and the original panic value is re-raised.

In a handler, call it with the request context:

err := db.InTx(ctx.Context(), func(ctx context.Context) error {
    // repos called with this ctx pick up the TX automatically
    return svc.Transfer(ctx, from, to, amount)
})

func (*DB) InTxWith

func (db *DB) InTxWith(ctx context.Context, opts *sql.TxOptions, fn func(ctx context.Context) error) error

InTxWith is like DB.InTx but accepts sql.TxOptions for configuring the outer transaction's isolation level and read-only mode. A nested call uses a savepoint; non-default options there return ErrNestedTxOptions instead of being silently ignored. Option support and enforcement remain driver- specific; Credo does not emulate an unsupported isolation level. The pinned modernc SQLite driver does not reliably enforce Isolation or ReadOnly, so use ordinary InTx for SQLite snapshot reads. It is the method form of RunInTxWith.

func (*DB) Insert

func (db *DB) Insert(model ...any) *InsertQuery

Insert creates a new InsertQuery proxy. It accepts zero or one model; more than one records a builder error that Exec returns.

func (*DB) Migrate

func (db *DB) Migrate(ctx context.Context) (err error)

Migrate runs all pending registered migrations. In multi-replica production, call it from one deadline-bounded pre-deploy job before rolling out the application. Its signature also matches credo's App.OnStart hook; this opt-in form is convenient for development, tests, and deliberate single-replica deployments:

db.RegisterMigrations(migrations)
app.OnStart(db.Migrate)

Migrate creates the Bun migration bookkeeping tables if needed, takes Bun's table-based advisory lock, applies unapplied migrations in order, and releases the lock. If another instance holds the lock, Migrate fails immediately rather than waiting; it does not retry. Running with no pending migrations executes no Up body, although Init, Lock, and Unlock still run.

Unlock ignores parent cancellation but gets a fresh five-second budget. Caller wait remains bounded even if a driver ignores context. A timeout means the unlock outcome is uncertain: the driver goroutine or connection may remain active and the lock row may remain or be deleted later. Do not retry or delete a stale row until the old runner is known to have stopped.

Returns an error if no migration set was registered, or if an operation surfaced by Bun fails; errors are mapped to store.Err* sentinels where applicable. Bun v1.2.18 does not surface SQL-migration transaction-finalizer errors reliably, so use a Go migration with an explicit transaction when the commit result must gate the applied marker. Direct Bun migrators do not inherit RegisterMigrations options. Callers must repeat the options relevant to status or generation; DB-mutating apply/rollback paths must additionally own Init, Lock, and bounded cancellation-detached Unlock themselves.

func (*DB) Ping

func (db *DB) Ping(ctx context.Context) error

Ping verifies the database connection is alive.

func (*DB) Query

func (db *DB) Query(ctx context.Context, dest any, query string, args ...any) error

Query executes a raw SQL query that returns multiple rows, with TX injection and error mapping. dest should be a pointer to a slice.

func (*DB) QueryRow

func (db *DB) QueryRow(ctx context.Context, dest any, query string, args ...any) error

QueryRow executes a raw SQL query that returns a single row, with TX injection and error mapping. dest is scanned into.

func (*DB) RegisterMigrations

func (db *DB) RegisterMigrations(m *migrate.Migrations, opts ...migrate.MigratorOption)

RegisterMigrations registers the migration set that DB.Migrate runs. The set is a plain migrate.Migrations from Bun: populate it with (*migrate.Migrations).Discover for SQL files (works with embed.FS) or MustRegister for Go migrations, then hand it to the DB once at wiring time. Optional opts are passed through to the underlying migrate.NewMigrator (table names, hooks, template data, ...).

By default the wrapper applies migrate.WithMarkAppliedOnSuccess(true), so a migration is recorded only after its Up function returns nil. This is an at-least-once bookkeeping policy, not an atomicity guarantee: partial non-transactional effects or a marker-write failure can make a later DB.Migrate repeat work. Migrations must therefore be transactional where the database supports it or explicitly idempotent/reconcilable. Pass WithMarkAppliedOnSuccess(false) to restore Bun's record-before-running policy when its different recovery tradeoff is intentional.

Panics if m is nil or if migrations were already registered — both are wiring-time programming errors, never runtime conditions.

func (*DB) RequireTx

func (db *DB) RequireTx(ctx context.Context) (bun.IDB, error)

RequireTx returns the active transaction for this DB or store.ErrTxMissing when ctx is outside its transaction scope. Unlike DB.Conn, it never falls back to the underlying DB. The returned transaction is borrowed and must not escape the InTx callback.

func (*DB) ResourceIdentity

func (db *DB) ResourceIdentity() any

ResourceIdentity returns the physical DB wrapper used for lifecycle ownership. Semantic wrapper types that embed *DB inherit this method, so store.Register recognizes multiple wrappers around the same DB as one resource.

func (*DB) Select

func (db *DB) Select(model ...any) *SelectQuery

Select creates a new SelectQuery proxy. It accepts zero or one model; more than one records a builder error that the terminal method returns.

func (*DB) Shutdown

func (db *DB) Shutdown(_ context.Context) error

Shutdown closes the database connection pool. The pool is always closed — even when ctx is already canceled or past its deadline — because leaving it open leaks connections; sql.DB.Close is a fast local operation that takes no context. ctx is accepted only to satisfy the store Shutdowner interface.

func (*DB) Stats

func (db *DB) Stats() sql.DBStats

Stats returns a point-in-time snapshot of the underlying database/sql connection pool. OpenConnections, InUse, and Idle are instantaneous gauges; WaitCount, WaitDuration, and the closed-connection fields are cumulative for the lifetime of the pool. MaxOpenConnections equal to zero means the pool is unlimited. Cumulative values reset when a new DB and pool are opened. Stats does not ping the database.

func (*DB) StoreRegistrationWarningCodes

func (db *DB) StoreRegistrationWarningCodes() []string

StoreRegistrationWarningCodes returns secret-safe warning codes that the store integration may emit when registering this database. It returns sqldb.pool.max_open_unlimited when the pool's effective maximum is unlimited at inspection time. The returned slice is independent of the DB and may be modified by the caller.

func (*DB) Update

func (db *DB) Update(model ...any) *UpdateQuery

Update creates a new UpdateQuery proxy. It accepts zero or one model; more than one records a builder error that Exec returns.

type DeleteQuery

type DeleteQuery struct {
	// contains filtered or unexported fields
}

DeleteQuery proxies bun.DeleteQuery with TX injection and error mapping.

func (*DeleteQuery) Apply

func (q *DeleteQuery) Apply(fns ...func(*bun.DeleteQuery) *bun.DeleteQuery) *DeleteQuery

Apply delegates to Bun's native Apply for advanced builder methods. Nil functions are filtered out.

func (*DeleteQuery) ApplyQueryBuilder

func (q *DeleteQuery) ApplyQueryBuilder(fn func(bun.QueryBuilder) bun.QueryBuilder) *DeleteQuery

ApplyQueryBuilder applies fn to Bun's shared bun.QueryBuilder — the builder-only interface (Where, WhereOr, WhereGroup, WherePK, WhereDeleted, WhereAllWithDeleted) common to select, update, and delete queries. It lets a single WHERE predicate be reused across all three query types instead of being duplicated per type via Apply.

Conditions added through the builder land on this query, so the proxy's terminal methods still apply TX injection and error mapping; interceptors are preserved. A nil fn is a no-op. The builder's Unwrap() any remains a terminal-bypass escape, the same caveat as Unwrap.

func (*DeleteQuery) Conn

func (q *DeleteQuery) Conn(db bun.IConn) *DeleteQuery

Conn sets an explicit connection, bypassing context TX injection.

func (*DeleteQuery) Exec

func (q *DeleteQuery) Exec(ctx context.Context, dest ...any) (sql.Result, error)

Exec executes the delete query.

Before TX injection the builder is shallow-copied so injecting the connection does not mutate the caller's builder. bun exposes Clone only on SelectQuery, so a deep copy is unavailable here; the shallow copy suffices because bun reads — never mutates — the builder while generating SQL.

Driver errors are mapped to store.Err* sentinels. Callers should inspect the returned sql.Result to detect zero-row deletes; the proxy does not convert "no rows affected" into store.ErrNotFound.

func (*DeleteQuery) Model

func (q *DeleteQuery) Model(model any) *DeleteQuery

Model sets the model for the delete.

func (*DeleteQuery) Returning

func (q *DeleteQuery) Returning(query string, args ...any) *DeleteQuery

Returning adds a RETURNING clause.

func (*DeleteQuery) Unwrap

func (q *DeleteQuery) Unwrap() *bun.DeleteQuery

Unwrap returns the underlying *bun.DeleteQuery for builder-only use.

func (*DeleteQuery) Where

func (q *DeleteQuery) Where(query string, args ...any) *DeleteQuery

Where adds a WHERE condition.

func (*DeleteQuery) WherePK

func (q *DeleteQuery) WherePK(cols ...string) *DeleteQuery

WherePK adds a WHERE clause for the primary key columns.

type InsertQuery

type InsertQuery struct {
	// contains filtered or unexported fields
}

InsertQuery proxies bun.InsertQuery with TX injection and error mapping.

func (*InsertQuery) Apply

func (q *InsertQuery) Apply(fns ...func(*bun.InsertQuery) *bun.InsertQuery) *InsertQuery

Apply delegates to Bun's native Apply for advanced builder methods. Nil functions are filtered out.

func (*InsertQuery) Column

func (q *InsertQuery) Column(columns ...string) *InsertQuery

Column specifies columns to insert.

func (*InsertQuery) Conn

func (q *InsertQuery) Conn(db bun.IConn) *InsertQuery

Conn sets an explicit connection, bypassing context TX injection.

func (*InsertQuery) Exec

func (q *InsertQuery) Exec(ctx context.Context, dest ...any) (sql.Result, error)

Exec executes the insert query.

Before TX injection the builder is shallow-copied so injecting the connection does not mutate the caller's builder. bun exposes Clone only on SelectQuery, so a deep copy is unavailable here; the shallow copy suffices because bun reads — never mutates — the builder while generating SQL.

Driver errors are mapped to semantic store errors. Unique-constraint violations become store.ErrAlreadyExists (and retain the deprecated store.ErrDuplicate match); foreign-key violations become store.ErrConstraint (and retain the deprecated store.ErrConflict umbrella match).

func (*InsertQuery) Model

func (q *InsertQuery) Model(model any) *InsertQuery

Model sets the model for the insert.

func (*InsertQuery) On

func (q *InsertQuery) On(query string, args ...any) *InsertQuery

On adds an ON CONFLICT clause.

func (*InsertQuery) Returning

func (q *InsertQuery) Returning(query string, args ...any) *InsertQuery

Returning adds a RETURNING clause.

func (*InsertQuery) Set

func (q *InsertQuery) Set(query string, args ...any) *InsertQuery

Set adds a SET clause for ON CONFLICT DO UPDATE.

func (*InsertQuery) Unwrap

func (q *InsertQuery) Unwrap() *bun.InsertQuery

Unwrap returns the underlying *bun.InsertQuery for builder-only use.

func (*InsertQuery) Value

func (q *InsertQuery) Value(column string, expr string, args ...any) *InsertQuery

Value sets a custom value expression for a column.

type Option

type Option func(*options)

Option configures an Open call.

func WithConnector

func WithConnector(connector driver.Connector) Option

WithConnector provides a custom driver.Connector, bypassing DSN-based connection creation. When set, Config.DSN and the DSN built from Config fields are ignored for sql.Open. Open rejects nil and typed-nil connectors.

func WithDialect

func WithDialect(dialect schema.Dialect) Option

WithDialect overrides the auto-detected dialect. Use this when the driver name does not match an exact known driver alias. Open rejects an explicitly nil dialect and a known dialect that conflicts with the configured known driver family.

func WithTxCleanupTimeout

func WithTxCleanupTimeout(d time.Duration) Option

WithTxCleanupTimeout sets how long Credo waits for each nested savepoint creation, release, or rollback and for the fail-safe ambient transaction abort. The default is 5 seconds. A driver operation that ignores context may continue in its goroutine, but the transaction is marked rollback-only and the caller stops waiting. d must be greater than zero; Open returns an error for invalid values.

type SelectQuery

type SelectQuery struct {
	// contains filtered or unexported fields
}

SelectQuery proxies bun.SelectQuery with TX injection and error mapping.

func (*SelectQuery) All

func (q *SelectQuery) All[T any](ctx context.Context) ([]T, error)

All executes the query and returns every matching row as a []T. T drives both the table and the scan destination, so the query is built model-less and All owns the destination:

users, err := db.Select().Where("active = ?", true).OrderExpr("id").All[User](ctx)

No matching rows yield an empty, non-nil slice and a nil error — unlike One, an empty result is not store.ErrNotFound. Driver errors map to the store.Err* sentinels. The query must be model-less; a model bound through Select, Model, or Apply returns ErrTypedTerminalModel before execution. The receiver is not mutated: an internal execution snapshot receives the destination and ambient transaction from ctx, exactly as for SelectQuery.Scan.

func (*SelectQuery) Apply

func (q *SelectQuery) Apply(fns ...func(*bun.SelectQuery) *bun.SelectQuery) *SelectQuery

Apply delegates to Bun's native Apply for advanced builder methods not in the curated proxy set. Nil functions are filtered out. Interceptors (TX injection, error mapping) are preserved on terminal methods.

func (*SelectQuery) ApplyQueryBuilder

func (q *SelectQuery) ApplyQueryBuilder(fn func(bun.QueryBuilder) bun.QueryBuilder) *SelectQuery

ApplyQueryBuilder applies fn to Bun's shared bun.QueryBuilder — the builder-only interface (Where, WhereOr, WhereGroup, WherePK, WhereDeleted, WhereAllWithDeleted) common to select, update, and delete queries. Unlike Apply, which is typed per query, this lets a single predicate — tenant scoping, soft-delete filters, ownership checks — be reused across all three query types instead of being duplicated per type.

Conditions added through the builder land on this query, so the proxy's terminal methods still apply TX injection and error mapping; interceptors are preserved, exactly like Apply. A nil fn is a no-op.

The bun.QueryBuilder passed to fn also exposes Unwrap() any as a terminal escape; calling terminal methods on that unwrapped query bypasses Credo interceptors — the same caveat as Unwrap.

func (*SelectQuery) Clone

func (q *SelectQuery) Clone() *SelectQuery

Clone returns a top-level query-builder fork while preserving execution state that Bun v1.2.18 omits: an explicit connection, builder errors, WherePK fields, soft-delete flags, and CTE materialization flags. It follows Bun's sharing semantics for nested values, including a bound destination and CTE or relation subqueries; do not mutate or scan source and clone concurrently when they share such values.

func (*SelectQuery) Column

func (q *SelectQuery) Column(columns ...string) *SelectQuery

Column adds columns to select.

func (*SelectQuery) ColumnExpr

func (q *SelectQuery) ColumnExpr(query string, args ...any) *SelectQuery

ColumnExpr adds a raw expression to the SELECT clause. Use for computed columns and aggregates that the model layer cannot express.

func (*SelectQuery) Conn

func (q *SelectQuery) Conn(db bun.IConn) *SelectQuery

Conn sets an explicit connection, bypassing context TX injection.

func (*SelectQuery) Count

func (q *SelectQuery) Count(ctx context.Context) (int, error)

Count executes the query and returns the count of matching logical result rows. Credo counts an outer derived-table source, so ungrouped aggregate projections, Group, Distinct, and Group+Having share the same cardinality contract. Direct compound queries and HAVING without GROUP BY return ErrUnsupportedCountQuery before execution. Driver errors are mapped to store.Err* sentinels. Bun model SELECT hooks run around the logical count source; a Page that reaches its data SELECT invokes them once for COUNT and once for SELECT. Count does not scan or mutate a bound model: successful AfterSelect hooks see its pre-count value. MySQL validates the generated derived-table output names during execution. ER_DUP_FIELDNAME (1060) from that logical COUNT is wrapped with ErrUnsupportedCountQuery while retaining the driver cause; use unique aliases to resolve colliding output names. Relation callbacks may shape predicates/projections, but cannot replace the model or add root ORDER/LIMIT/OFFSET/FOR or an unsupported count shape.

func (*SelectQuery) Distinct

func (q *SelectQuery) Distinct() *SelectQuery

Distinct adds a DISTINCT clause.

func (*SelectQuery) ExcludeColumn

func (q *SelectQuery) ExcludeColumn(columns ...string) *SelectQuery

ExcludeColumn removes columns that the model would otherwise select. Use "*" to start from an empty set and add columns explicitly.

func (*SelectQuery) Exists

func (q *SelectQuery) Exists(ctx context.Context) (bool, error)

Exists executes the query and returns true if at least one row matches. Driver errors are mapped to store.Err* sentinels.

func (*SelectQuery) GroupExpr

func (q *SelectQuery) GroupExpr(query string, args ...any) *SelectQuery

GroupExpr adds a GROUP BY expression.

func (*SelectQuery) Having

func (q *SelectQuery) Having(query string, args ...any) *SelectQuery

Having adds a HAVING clause.

func (*SelectQuery) Join

func (q *SelectQuery) Join(join string, args ...any) *SelectQuery

Join adds a JOIN clause. The join string is the full join expression including the join type and ON condition, e.g. "LEFT JOIN orders AS o ON o.user_id = u.id". For composing the ON clause separately, follow with JoinOn.

func (*SelectQuery) JoinOn

func (q *SelectQuery) JoinOn(cond string, args ...any) *SelectQuery

JoinOn appends an additional ON condition to the most recent Join, joined with AND.

func (*SelectQuery) JoinOnOr

func (q *SelectQuery) JoinOnOr(cond string, args ...any) *SelectQuery

JoinOnOr appends an additional ON condition to the most recent Join, joined with OR.

func (*SelectQuery) Limit

func (q *SelectQuery) Limit(n int) *SelectQuery

Limit sets the LIMIT clause. Values outside Bun v1.2.18's signed-int32 storage range record ErrInvalidLimitOffset; the terminal then fails before executing. Values inside that range retain Bun's zero/negative semantics.

func (*SelectQuery) Model

func (q *SelectQuery) Model(model any) *SelectQuery

Model sets the model for the query.

func (*SelectQuery) Offset

func (q *SelectQuery) Offset(n int) *SelectQuery

Offset sets the OFFSET clause. Values outside Bun v1.2.18's signed-int32 storage range record ErrInvalidLimitOffset; the terminal then fails before executing. Values inside that range retain Bun's zero/negative semantics.

func (*SelectQuery) One

func (q *SelectQuery) One[T any](ctx context.Context) (T, error)

One executes the query and returns its first matching row as a value of T. T drives both the table and the scan destination, so the query is built model-less and One owns the destination. T may be a struct or a pointer to one (User or *User); both forms work:

user, err := db.Select().Where("id = ?", id).One[User](ctx)

One applies LIMIT 1, so multiple matches are not an error — it returns the first row; add an OrderExpr for a deterministic choice. A missing row returns store.ErrNotFound (wrapping sql.ErrNoRows), so callers branch with errors.Is(err, store.ErrNotFound); other driver errors map to the store.Err* sentinels. The query must be model-less; a model bound through Select, Model, or Apply returns ErrTypedTerminalModel before execution. The receiver is not mutated: an internal execution snapshot receives the destination and the ambient transaction from ctx, exactly as for SelectQuery.Scan.

func (*SelectQuery) OrderExpr

func (q *SelectQuery) OrderExpr(query string, args ...any) *SelectQuery

OrderExpr adds an ORDER BY expression.

func (*SelectQuery) Page

func (q *SelectQuery) Page[T any](ctx context.Context, req *pagination.PageRequest) (*pagination.Page[T], error)

Page runs COUNT + SELECT with LIMIT/OFFSET and assembles the result as a *pagination.Page[T]. Like SelectQuery.One and SelectQuery.All it is a typed terminal: T drives both the table and the scan destination, so the query is built model-less and Page owns the result:

page, err := db.Select().
	Where("tenant_id = ?", tenantID).
	OrderExpr("created_at DESC").
	Page[User](ctx, req)            // (*pagination.Page[User], error)

req is copied once and never modified. BindQuery applies defaults through pagination.PageRequest.Validate; manually constructed requests may call pagination.PageRequest.Normalize (or NormalizeWithMax) first. Page itself never defaults or clamps: it strictly requires positive Page and PerPage values and an offset/limit representable by Bun v1.2.18. Nil, invalid, or overflowing requests return an error matching pagination.ErrInvalidPageRequest before COUNT or SELECT executes. Valid custom PerPage values above pagination.MaxPerPage remain unchanged.

The query must be model-less; a model bound through Select, Model, or Apply returns ErrTypedTerminalModel before COUNT or SELECT executes. Use a bound model with SelectQuery.Scan when relations or a custom destination are required.

Total is the number of logical result rows before ordering and pagination. An ungrouped aggregate projection counts as one row when it produces a row; Distinct counts selected projection tuples; Group counts groups; Group with Having counts the groups left after Having. Bun v1.2.18 cannot safely window standalone Having or a direct UNION/INTERSECT/EXCEPT query, so those shapes return ErrUnsupportedCountQuery before execution. Restructure compound input behind an outer derived-table/CTE source and compose explicit Count/Scan terminals when a custom source is required. On MySQL, ER_DUP_FIELDNAME (1060) returned by the COUNT statement is wrapped with ErrUnsupportedCountQuery after I/O while retaining the driver cause. Bun model SELECT hooks run around each statement's private snapshot, so a Page that reaches its data SELECT invokes the lifecycle once for COUNT and once for SELECT; hook-added predicates and projections affect both Total and Records. Keep hooks deterministic: transaction isolation cannot make a volatile projection or application-side hook choose the same result twice.

COUNT runs first; when it reports zero rows the SELECT is skipped and the returned Page carries a non-nil empty Records slice with the requested page and per-page preserved. COUNT and SELECT are separate statements, so under concurrent writes the total and the page can drift. A normal Read Committed transaction does not make the two statements share a snapshot; use an isolation level with that guarantee when consistency is required. Both statements use internal execution snapshots and join the ambient transaction from ctx exactly like SelectQuery.Scan, so the receiver is never mutated. Credo does not start an implicit transaction: PostgreSQL and MySQL/InnoDB callers normally select Repeatable Read on an outer DB.InTxWith call, while SQLite callers use an explicit DB.InTx read transaction. Always pass the callback's txCtx to Page; isolation support remains driver-specific.

Page is the all-in-one terminal for flows that respond with the queried type directly. When records need a model→DTO mapping, run Page[Model] and map it with pagination's Page.Map, which carries the metadata over:

modelPage, err := q.Page[Model](ctx, req)
if err != nil {
	return nil, err
}
dtoPage := modelPage.Map(func(m Model) DTO { return toDTO(m) })

For a conversion that can fail, iterate modelPage.Records with ordinary error handling and build the DTO page with pagination.NewPage, carrying modelPage's Total, Page, and PerPage over.

func (*SelectQuery) Relation

func (q *SelectQuery) Relation(name string, apply ...func(*bun.SelectQuery) *bun.SelectQuery) *SelectQuery

Relation loads a named relation for a bound model. Use the model-bound Scan form for eager loading, for example:

var users []User
err := db.Select(&users).Relation("Orders").Scan(ctx)

A relation cannot be deferred to One, All, or Page: typed terminals require a model-less query and return ErrTypedTerminalModel when a model is already bound.

func (*SelectQuery) Scan

func (q *SelectQuery) Scan(ctx context.Context, dest ...any) error

Scan executes the query and scans results into dest.

Driver errors are mapped to store.Err* sentinels. In particular, sql.ErrNoRows is returned as store.ErrNotFound, so callers can use errors.Is(err, store.ErrNotFound) without importing database/sql.

func (*SelectQuery) TableExpr

func (q *SelectQuery) TableExpr(query string, args ...any) *SelectQuery

TableExpr sets the FROM clause from a raw expression. Use for model-less queries (reporting, ad-hoc projections) where Model is not appropriate.

func (*SelectQuery) Unwrap

func (q *SelectQuery) Unwrap() *bun.SelectQuery

Unwrap returns the underlying *bun.SelectQuery for builder-only use. Terminal methods on the unwrapped query bypass Credo interceptors (TX injection, error mapping). Use Apply for the recommended escape hatch that preserves interceptors.

func (*SelectQuery) Where

func (q *SelectQuery) Where(query string, args ...any) *SelectQuery

Where adds a WHERE condition.

func (*SelectQuery) WhereOr

func (q *SelectQuery) WhereOr(query string, args ...any) *SelectQuery

WhereOr adds an OR WHERE condition.

func (*SelectQuery) WherePK

func (q *SelectQuery) WherePK(cols ...string) *SelectQuery

WherePK adds a WHERE clause for the primary key columns.

type UpdateQuery

type UpdateQuery struct {
	// contains filtered or unexported fields
}

UpdateQuery proxies bun.UpdateQuery with TX injection and error mapping.

func (*UpdateQuery) Apply

func (q *UpdateQuery) Apply(fns ...func(*bun.UpdateQuery) *bun.UpdateQuery) *UpdateQuery

Apply delegates to Bun's native Apply for advanced builder methods. Nil functions are filtered out.

func (*UpdateQuery) ApplyQueryBuilder

func (q *UpdateQuery) ApplyQueryBuilder(fn func(bun.QueryBuilder) bun.QueryBuilder) *UpdateQuery

ApplyQueryBuilder applies fn to Bun's shared bun.QueryBuilder — the builder-only interface (Where, WhereOr, WhereGroup, WherePK, WhereDeleted, WhereAllWithDeleted) common to select, update, and delete queries. It lets a single WHERE predicate be reused across all three query types instead of being duplicated per type via Apply.

Conditions added through the builder land on this query, so the proxy's terminal methods still apply TX injection and error mapping; interceptors are preserved. A nil fn is a no-op. The builder's Unwrap() any remains a terminal-bypass escape, the same caveat as Unwrap.

func (*UpdateQuery) Column

func (q *UpdateQuery) Column(columns ...string) *UpdateQuery

Column specifies columns to update.

func (*UpdateQuery) Conn

func (q *UpdateQuery) Conn(db bun.IConn) *UpdateQuery

Conn sets an explicit connection, bypassing context TX injection.

func (*UpdateQuery) Exec

func (q *UpdateQuery) Exec(ctx context.Context, dest ...any) (sql.Result, error)

Exec executes the update query.

Before TX injection the builder is shallow-copied so injecting the connection does not mutate the caller's builder. bun exposes Clone only on SelectQuery, so a deep copy is unavailable here; the shallow copy suffices because bun reads — never mutates — the builder while generating SQL.

Driver errors are mapped to store.Err* sentinels. Callers should inspect the returned sql.Result to detect zero-row updates; the proxy does not convert "no rows affected" into store.ErrNotFound.

func (*UpdateQuery) Model

func (q *UpdateQuery) Model(model any) *UpdateQuery

Model sets the model for the update.

func (*UpdateQuery) OmitZero

func (q *UpdateQuery) OmitZero() *UpdateQuery

OmitZero omits zero values from the update.

func (*UpdateQuery) Returning

func (q *UpdateQuery) Returning(query string, args ...any) *UpdateQuery

Returning adds a RETURNING clause.

func (*UpdateQuery) Set

func (q *UpdateQuery) Set(query string, args ...any) *UpdateQuery

Set adds a SET clause.

func (*UpdateQuery) Unwrap

func (q *UpdateQuery) Unwrap() *bun.UpdateQuery

Unwrap returns the underlying *bun.UpdateQuery for builder-only use.

func (*UpdateQuery) Where

func (q *UpdateQuery) Where(query string, args ...any) *UpdateQuery

Where adds a WHERE condition.

func (*UpdateQuery) WherePK

func (q *UpdateQuery) WherePK(cols ...string) *UpdateQuery

WherePK adds a WHERE clause for the primary key columns.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL