Documentation
¶
Overview ¶
Package sql — hosts Config, the parameters every port in this package is built from, and the pool policy it applies.
Package sql — hosts the only place in this domain that renders dialect-specific SQL. Every statement it sends is built here, spelled with the vocabulary core/data/sql's Dialect owns: the bind markers, the quoting and the row-lock clauses.
Package sql — hosts the scope-bound Executor handed to a TxFunc, and the two guards that stand between a stale handle and a live transaction.
Package sql — hosts the liveness probe and the budget that bounds it.
Package sql — hosts the transactor's two ADR 0039 siblings: where a statement issued under a context runs, and what waits for a commit.
Package sql — hosts the migration runner: the order, the version table, the dry run, and the two directions.
Package sql — hosts MigrateConfig, the migration runner's parameters, and the validation that turns a caller's slice into an ordered plan.
Package sql — hosts the migration lock of an engine with no advisory lock: SQLite's own write lock on the database file, held for the whole run.
Package sql — hosts the migration lock: why it is the engine's own advisory lock, and why it lives on a connection of its own.
Package sql — hosts the validated, sorted, clamped form of a MigrateConfig.
Package sql — hosts the version table: its portable shape, the reads and the two writes that keep it in step with the schema.
Package sql — hosts the validated, clamped form of a Config.
Package sql — hosts the three rendered statements of one savepoint.
Package sql — hosts the one context key this package stores under.
Package sql implements the relational-database ports declared in internal/core/data/sql (ADR 0055): the transaction manager, the pool policy, the health check and the migration runner.
It ships NO DRIVER and imports none. A driver is a connector to a third-party system, so pgx / go-sql-driver / the sqlite bindings belong under third-party/ by the rule that put the AWS writers there (ADR 0012). The caller opens its own *sql.DB and hands it over.
It is also NOT an ORM, has no query builder, no entity mapping and no lazy loading, and never will (ADR 0055 §D1).
Transaction ownership is structural ¶
NewTransactor hands the unit of work a core/data/sql.Executor — three methods, none of which is Commit or Rollback. A function that receives a transaction therefore cannot end it: the mistake does not compile. The manager commits when the function returns nil and rolls back otherwise, and it is the only thing that ever does either.
Nesting is a savepoint, and the traps are handled rather than avoided ¶
database/sql has no nested transactions. A core/data/sql.Transactor.Transact whose context already carries a transaction from THIS manager opens a SAVEPOINT instead:
- The savepoint NAME is generated by the SDK from a counter that never resets inside one transaction. It is never caller-supplied, because an identifier cannot be a bound parameter; and it is never reused, because a repeated name shadows rather than replaces, so RELEASE would release the wrong one.
- The three statements are IDENTICAL on postgres, mysql and sqlite, which is why those three are the supported set. Engines that spell nesting differently are refused BY NAME at ParseDialect (ADR 0055 §D4).
- Non-zero core/data/sql.TxOptionsValue on a nested call is REFUSED: a savepoint cannot change isolation or read-only mode, and silently downgrading a caller's Serializable is worse than saying no.
What a failed sub-transaction does ¶
It rolls back to its savepoint and returns the error to the caller of the inner Transact — verbatim, so errors.Is still answers. The OUTER transaction survives, and that is not optimism: ROLLBACK TO SAVEPOINT is exactly the statement that clears PostgreSQL's aborted state (SQLSTATE 25P02), so a caller who CATCHES the inner error has a usable transaction to continue in. A caller who does not catch it propagates it, and the outer scope rolls back in turn — the common case, and the safe default.
If the ROLLBACK TO SAVEPOINT statement itself fails, the transaction is POISONED: every later operation is refused with coresql.TxPoisoned, the commit never happens, and the whole transaction is rolled back. The engine's state is unknown at that point, and committing work the SDK believes it undid is the one outcome worse than failing.
Joining a transaction, and holding what waits for its commit ¶
The manager is also a core/data/sql Joiner, Deferrer and Aborter (ADR 0139). Join answers the executor of the innermost scope of the transaction a context carries for THIS manager — a retired one, which refuses, when that scope has returned, and never the pool in its place — or the pool when the context carries none. Defer holds a function until that transaction commits; each held function carries the id of the scope that held it, a failed savepoint drops what was held at or above its own id, and the root runs what is left after its COMMIT. OnAbort is the complement: a failed savepoint runs, newest first, what was held for an abort at or above its own id once its ROLLBACK TO SAVEPOINT ran, a root that does not commit runs what is left after its ROLLBACK — a panic included —, and a root that commits drops it.
SQLite's migrations ¶
SQLite has no advisory lock, and its runner holds the database file's write lock instead (ADR 0140): the run is ONE transaction that takes the lock with a write that writes nothing, and each migration is a savepoint of it.
A partially applied migration, and what MySQL does to the guarantee ¶
Each migration runs in ITS OWN transaction, and the version-table row is written by that SAME transaction on that SAME executor. On PostgreSQL and SQLite — where DDL is transactional — that is a real guarantee: a migration that fails rolls back both halves, the database sits exactly at version n-1, and re-running is safe.
On MySQL and MariaDB it is NOT, and the difference is not one this package can close. DDL causes an IMPLICIT COMMIT: the server has already committed the schema change before the runner's transaction reaches COMMIT. A migration whose second statement fails therefore leaves the first statement's ALTER TABLE applied and committed, while the version row — in the same transaction — is rolled back. The database is then in a state no version number describes.
No client-side code can make MySQL's DDL transactional, so this package does three things instead of pretending: it says so (here, in the package CLAUDE.md, in pkg/v1/data/sql's doc and in MigrationFailed's Private), it stops at the first failure rather than continuing, and it reports the version and the direction so the migration to inspect by hand is named.
The operational rule that follows is the CONSUMER's: one DDL statement per migration on MySQL, which makes the failure atomic by construction. This package cannot enforce it — it does not parse SQL — and does not pretend to. See ADR 0055 D8.
Nothing here waits on the wall clock ¶
Every budget — the health-check probe, the migration-lock retry — is read from an injected kernel/clock.Timed, so the suite advances a ManualClock instead of sleeping.
Package sql — hosts Statements, the one helper that turns SQL text into a migration step.
Package sql — hosts the transaction manager: the root transaction, the nested savepoint, and the single rule about who is allowed to commit.
Package sql — hosts the per-context transaction chain: which transactor opened a transaction, and how a nested call finds its own.
Package sql — hosts the per-transaction state every scope of one transaction shares, and the context chain that finds it.
Package sql — hosts Withheld, a driver's error carried beside a verdict and kept out of every rendering.
Index ¶
- Constants
- func ChecksumOf(stmts ...string) string
- func Classify(err error) bool
- func Each[T any](rows *stdsql.Rows, scan func(*stdsql.Rows) (T, error)) iter.Seq2[T, error]
- func FileLockSQL(table string) string
- func InsertRows(ctx context.Context, ex coresql.Executor, d coresql.Dialect, into InsertSpec, ...) (inserted int64, err error)
- func NewChecker(cfg Config) (probe coresql.Checker, err error)
- func NewMigrator(cfg Config, mig MigrateConfig) (runner coresql.Migrator, err error)
- func NewRetrying(tm coresql.Transactor, cfg RetryConfig) (manager coresql.Transactor, err error)
- func NewTransactor(cfg Config) (manager coresql.Transactor, err error)
- func Observe(c driver.Connector, obs coresql.StatementObserver) driver.Connector
- func SlowQueries(threshold time.Duration, log corelogger.Logger, clk clock.Clock) coresql.StatementObserver
- func Statements(stmts ...string) coresql.Step
- func Traced(tracer coretrace.Tracer) coresql.StatementObserver
- type Config
- type InsertSpec
- type MigrateConfig
- type PoolConfig
- type RetryConfig
- type Withheld
Constants ¶
const DefaultCheckTimeout time.Duration = 5 * time.Second
DefaultCheckTimeout is the liveness-probe budget a non-positive Config.CheckTimeout clamps to. A health check that can hang forever is not a health check.
const DefaultLockRetryInterval time.Duration = 250 * time.Millisecond
DefaultLockRetryInterval is how often a blocked runner retries the non-blocking lock acquisition. A non-positive MigrateConfig.LockRetryInterval clamps to it.
const DefaultLockTimeout time.Duration = 2 * time.Minute
DefaultLockTimeout is the migration-lock budget a non-positive MigrateConfig.LockTimeout clamps to.
A migration run that waits forever for another holder is a deployment that hangs with no output, which is strictly worse than one that fails and says why. Two minutes is longer than almost every migration and shorter than every deployment timeout that would otherwise kill the pod first.
const DefaultMaxIdle int = 2
DefaultMaxIdle is the idle-connection ceiling a non-positive PoolConfig.MaxIdle clamps to — the same value database/sql itself uses, so the clamp changes nothing a caller could have observed.
const DefaultMaxLifetime time.Duration = 30 * time.Minute
DefaultMaxLifetime is the connection lifetime a non-positive PoolConfig.MaxLifetime clamps to.
database/sql reads 0 as "reuse a connection forever", and a connection that lives forever survives a failover, a DNS change and a credential rotation — it keeps talking to a replica that has been demoted, and nothing in the application ever notices. The exact number matters far less than its being FINITE, so this is a clamp rather than a refusal (ADR 0031): thirty minutes is long enough to be invisible on the hot path and short enough that a topology change is picked up without a restart.
const DefaultRetryAttempts int = 3
DefaultRetryAttempts is how many times a retrying transactor runs a transaction when RetryConfig.Attempts is not positive: the first attempt and two more. A conflict that three attempts did not clear is contention the caller should hear about, not one a fourth will.
const DefaultRetryDelay time.Duration = 25 * time.Millisecond
DefaultRetryDelay is the base of the wait between two attempts when RetryConfig.Backoff is the zero value, which then grows by 2 per attempt with full jitter: two transactions that deadlocked each other and retry on the same curve without jitter meet again at the same instant.
const DefaultVersionTable string = "schema_migrations"
DefaultVersionTable is the table an empty MigrateConfig.VersionTable clamps to. The name matches the de-facto convention of every migration tool in the ecosystem, so an operator inspecting a database recognises it without being told.
const MaxObservedQuery int = 1024
MaxObservedQuery is how many bytes of a statement's SQL text a coresql.StatementObserver receives: a longer text is cut at the last whole UTF-8 character within it. A generated IN list or a bulk VALUES clause is no slower to log for being a megabyte long, and no more useful.
Variables ¶
This section is empty.
Functions ¶
func ChecksumOf ¶ added in v0.27.0
ChecksumOf is the fingerprint coresql.MigrationValue.Checksum records for a migration built from stmts: SHA-256 over the statements in order, each preceded by its length in bytes as an 8-byte big-endian unsigned integer, rendered as 64 lowercase hexadecimal digits.
The length prefix is what makes the fingerprint the statements' and not their concatenation's: "CREATE TABLE a" then "(x INT)" and "CREATE TABLE a(x INT)" alone are different migrations and get different checksums. Write it beside the steps it describes, from the same statements, so the two cannot drift:
ddl := []string{"CREATE TABLE accounts (id BIGINT PRIMARY KEY)"}
m := sql.Migration{Version: 1, Name: "accounts",
Up: sql.Statements(ddl...), Down: sql.Statements("DROP TABLE accounts"),
Checksum: sql.ChecksumOf(ddl...)}
func Classify ¶ added in v0.27.0
Classify is the SDK's coresql.Transient: true for a failure the database documents as "run the transaction again", read from the methods drivers expose rather than from a driver this package would have to import.
A SQLSTATE of 40001 (serialization failure) or 40P01 (deadlock detected), read through a SQLState() string method — pgx's and lib/pq's errors offer one —, and a SQLite result code whose primary code is 5 (BUSY) or 6 (LOCKED), extended forms such as 517 (BUSY_SNAPSHOT) included, read through a Code() int method — modernc.org/sqlite's errors offer one. Every error the chain joins or wraps is read, so a verdict joined beside the driver's error, or a Withheld, still answers.
A context's end is never transient, whatever else the chain carries. MySQL's deadlock (1213) and lock-wait timeout (1205) are not read here: go-sql-driver exposes them as fields, not methods, so a MySQL connector composes its own reading with this one.
func Each ¶ added in v0.27.0
Each iterates rows, scanning each with scan, and closes rows when the iteration ends — at the last row, at the first failure, or when the loop stops early. A failure — scan's, rows.Err's or the close's — is yielded once, as the last element, beside T's zero value:
rows, err := ex.QueryContext(ctx, "SELECT id, name FROM accounts")
if err != nil {
return err
}
for account, err := range sql.Each(rows, scanAccount) {
if err != nil {
return err
}
use(account)
}
It is the Next/Scan/Err/Close loop every caller writes, written once; it scans nothing itself, since mapping a row is the caller's (ADR 0055 §D1). A nil rows yields nothing.
func FileLockSQL ¶
FileLockSQL renders the statement that makes a SQLite transaction take the database file's write lock at once, where BEGIN — which database/sql sends as SQLite's deferred form — takes none.
It is a DELETE that matches no row. SQLite starts the write transaction a write statement needs when the statement STARTS, before its WHERE clause is weighed, so the lock is taken and nothing is changed (ADR 0140). Two transactions take it: the migration runner's, naming its version table — the one table it knows exists at that point, since it creates it first in the same transaction — and the queue's lease, naming its queue's table before its first read, so the read cannot answer from a snapshot the update after it would be refused for.
table is interpolated exactly as given — bare, as the runner's validated version table is, or already quoted, as the queue's is — and must have been validated at construction, which is the whole defence of an interpolated identifier.
func InsertRows ¶ added in v0.27.0
func InsertRows(ctx context.Context, ex coresql.Executor, d coresql.Dialect, into InsertSpec, rows [][]any) (inserted int64, err error)
InsertRows inserts rows into into.Table, into.PerStatement rows per statement, on ex, and returns how many rows the statements report inserted.
Each statement reads INSERT INTO table (cols) VALUES (…), (…), its bind markers spelled by d and numbered across the statement. into.Table and into.Columns are interpolated exactly as given — bare, or already quoted with coresql.Dialect.QuoteIdent — and must have been validated by the caller, which is the whole defence of an interpolated identifier; the values are always bound.
into.PerStatement is clamped to what d's engine binds in one statement — 999 parameters on SQLite, the oldest ceiling it ships with, and 65535 on PostgreSQL and MySQL — divided by the number of columns; a non-positive one takes that ceiling. A row whose width is not the number of columns, no columns at all or a dialect that is not Valid is refused with coresql.ConfigInvalid before any statement runs. A statement's failure is returned verbatim, with the count of rows the statements before it inserted; run InsertRows in a transaction for all or none.
func NewChecker ¶
NewChecker returns a bounded liveness probe over cfg.DB.
Like NewTransactor it applies cfg.Pool, so a process whose only SQL port is a health check still gets its pool policy installed.
func NewMigrator ¶
func NewMigrator(cfg Config, mig MigrateConfig) (runner coresql.Migrator, err error)
NewMigrator returns the migration runner for cfg.DB.
Every run is mutually exclusive across processes, and the lock is one its holder's death releases: the session's advisory lock on PostgreSQL and MySQL (ADR 0055 §D7), the database file's write lock on SQLite (ADR 0140). Running unlocked is never offered as a fallback: a runner that silently drops mutual exclusion is at its most dangerous in exactly the situation it exists for — two instances of a deployment starting together.
func NewRetrying ¶ added in v0.27.0
func NewRetrying(tm coresql.Transactor, cfg RetryConfig) (manager coresql.Transactor, err error)
NewRetrying returns a transactor that runs a ROOT transaction of tm again when it fails on an error cfg.Transient calls transient, up to cfg.Attempts runs, waiting cfg.Backoff's curve on cfg.Clock between two.
Each attempt is a whole transaction of tm: a new BEGIN, the unit of work from its start, and the commit or the rollback tm decides — so what the unit of work held with Defer is dropped with each attempt that rolled back and runs once, after the commit that stood, and what it held with OnAbort runs after each rollback. The unit of work MUST therefore be safe to run more than once: its effects outside the transaction belong in Defer, as ADR 0055 asks of a retried transaction.
A NESTED call — one whose context already carries a transaction of tm, opened through this transactor or through tm itself — joins it as tm does, and is never run again on its own: a savepoint cannot be retried without the transaction around it. A failure that is not transient, and a context that ended, return at once. When the budget is spent the last attempt's error is returned joined with coresql.TxRetriesExhausted, whose "attempts" field says how many ran.
The transactor offers coresql.Joiner, coresql.Deferrer and coresql.Aborter exactly when tm offers all three, forwarding each to tm. A nil tm is refused with coresql.ConfigInvalid.
func NewTransactor ¶
func NewTransactor(cfg Config) (manager coresql.Transactor, err error)
NewTransactor returns the transaction manager for cfg.DB.
It applies cfg.Pool to the caller's *sql.DB. That is a side effect on a value the caller owns, and it is deliberate: a pool policy that has to be installed by a second call is a pool policy someone will forget, and the consequence of forgetting it (unlimited connections, connections that live forever) lands on the database rather than on this process.
func Observe ¶ added in v0.27.0
Observe returns c with obs told about every statement the connections it opens send: each Exec, Query and Prepare on a connection, each Exec and Query of a statement it prepared, and each transaction's BEGIN, COMMIT and ROLLBACK — savepoints included, which travel as statements. A caller opens its pool over it and hands that pool to this package as it always did:
pool := stdsql.OpenDB(sql.Observe(connector, sql.SlowQueries(time.Second, log, clock.System)))
The observer receives the SQL text as written, cut to MaxObservedQuery bytes, and never an argument or a row. A query's end is told when its rows close, with how many were read. A call the driver skips with driver.ErrSkip — database/sql then prepares the statement, and the prepared calls are told in turn — ends with that error; the two observers this package ships ignore it.
The connections keep every optional interface database/sql reads — a ping, a session reset, a validity check, a driver's own argument checker — answering as the driver's own do, and declining as database/sql's defaults would where the driver has none. A nil obs is no wrapper: c is returned as it is (ADR 0031).
func SlowQueries ¶ added in v0.27.0
func SlowQueries(threshold time.Duration, log corelogger.Logger, clk clock.Clock) coresql.StatementObserver
SlowQueries returns an observer that logs, at Warn, each statement that took threshold or longer, timed on clk: its kind, its SQL text, its duration, the rows it read or affected, and — when it failed — the driver error's class as Withheld renders it, never its text. A statement under the threshold logs nothing, and costs one reading of the clock at each end. A non-positive threshold logs every statement; a nil clk is clock.System.
func Statements ¶
Statements returns a coresql.Step that runs stmts in order on the transaction's executor, stopping at the first failure.
This is the smallest helper that is genuinely useful and deliberately not a file loader. Reading a directory would mean inventing a filename grammar (`0001_name.up.sql`? `V1__name.sql`?), a statement splitter, and a rule for what a `;` inside a string literal means — three conventions the SDK would impose on every consumer and none of them asked for (ADR 0055 §D8).
Each statement is sent as its own Exec rather than as one semicolon-joined string, because multi-statement execution is a per-driver capability that several disable by default, and because a failure then names the statement that failed by its position in the slice.
The text is the caller's own and is never bound: a migration's DDL cannot be parameterised on any engine. That is exactly why migrations must not be built from user input, and why this helper takes a variadic of literals rather than a template.
func Traced ¶ added in v0.27.0
func Traced(tracer coretrace.Tracer) coresql.StatementObserver
Traced returns an observer that opens one client span per statement under the span ctx carries, named "sql " and the kind, with the SQL text as db.query.text, and ends it when the statement ends, with the rows as db.response.returned_rows and an error status — the driver error's class as Withheld renders it — when it failed.
Types ¶
type Config ¶
type Config struct {
// DB is the caller's pool. REQUIRED — this package never opens one,
// because opening one means importing a driver (ADR 0055 §D2).
DB *stdsql.DB
// Dialect selects the SQL grammar. REQUIRED and must be a dialect this
// SDK can spell; resolve a configuration string through
// core/data/sql.ParseDialect, which refuses the engines it cannot support BY
// NAME instead of guessing.
Dialect coresql.Dialect
// Pool is the connection-pool policy, applied to DB at construction.
Pool PoolConfig
// Clock is the time source every budget in this package is read from. A
// nil Clock falls back to clock.System.
//
// clock.Timed rather than clock.Clock because this package both stamps
// (the version table's applied_at) and WAITS (the probe budget, the lock
// retry). Reading package time instead would make every budget assertion
// a sleep, and a sleep a tolerance, and a tolerance a flake.
Clock clock.Timed
// CheckTimeout is the budget one [core/data/sql.Checker.Check] gets. A
// non-positive value clamps to [DefaultCheckTimeout].
CheckTimeout time.Duration
// StatementTimeout bounds each statement the transactor's executors run —
// inside a transaction, and through Join or Reader on a pool — measured
// on Clock. Zero, or negative, is none: a statement then runs until the
// caller's context ends, as database/sql runs it.
//
// An Exec or a Prepare is bounded until it returns; a Query until its
// rows are read and closed, or the budget elapses, whichever comes first
// — the cut context lives until then, one timer per statement in
// flight. A cut answered by the call itself is joined with
// [core/data/sql.StatementTimedOut], its "operation" field naming exec,
// query or prepare; a cut met while reading rows surfaces from Next,
// Err or Scan as the context's own end. The migration runner never
// applies it: a migration's DDL runs as long as it takes, under the
// caller's context.
StatementTimeout time.Duration
// ReadReplica is a second pool, on a replica of DB, that a read-only
// transaction and [core/data/sql.Router]'s Reader run on. Nil is none:
// every transaction opens on DB. Pool is applied to it as to DB, and the
// health probe pings both. It is the caller's, opened with its own
// driver, and this package never closes it.
ReadReplica *stdsql.DB
}
Config parameterises every constructor in this package: the caller's pool, the dialect whose grammar the SDK will speak to it, the pool policy applied at construction, the time source every budget is read from, and the health probe's budget.
It is passed BY VALUE on purpose. A constructor must not be able to observe a later mutation of the caller's struct, and the copy happens once per port rather than once per statement.
type InsertSpec ¶ added in v0.27.0
type InsertSpec struct {
// Table is the table the rows go into, interpolated as given — bare,
// or already quoted with [coresql.Dialect].QuoteIdent. Validate it
// first.
Table string
// Columns are the table's columns each row gives a value for, in that
// order, interpolated as given like Table.
Columns []string
// PerStatement is how many rows one statement carries at most,
// clamped to the engine's bound-parameter ceiling divided by the
// number of columns; a non-positive one takes that ceiling.
PerStatement int
}
InsertSpec is the statement InsertRows writes: the table, its columns in the order each row gives its values, and how many rows one statement carries at most.
type MigrateConfig ¶
type MigrateConfig struct {
// Migrations is the complete set this binary carries. Order does not
// matter — the runner sorts by Version — but every entry must be valid
// and no two may share a version.
//
// It is COPIED at construction, so a caller appending to their slice
// afterwards cannot change what the runner will apply.
Migrations []coresql.MigrationValue
// VersionTable names the bookkeeping table. Empty clamps to
// [DefaultVersionTable]; anything that is not a bare SQL identifier is
// REFUSED, because it is interpolated rather than bound.
VersionTable string
// LockTimeout bounds the wait for the advisory lock. Non-positive clamps
// to [DefaultLockTimeout].
LockTimeout time.Duration
// LockRetryInterval is how often the non-blocking acquisition is retried
// while another holder has the lock. Non-positive clamps to
// [DefaultLockRetryInterval].
LockRetryInterval time.Duration
}
MigrateConfig parameterises NewMigrator.
It carries no directory, no file pattern and no naming convention, and the SDK ships no example migration tree. Migrations are values the consumer builds, because the moment the SDK reads a directory it has invented a convention every consumer must adopt (ADR 0055 §D8).
type PoolConfig ¶
type PoolConfig struct {
// MaxOpen is the hard ceiling on open connections. It is REQUIRED and
// must be positive.
//
// database/sql reads 0 as UNLIMITED. "As many connections as the
// application happens to want" is a decision made by forgetting a line,
// and the consequence lands on the database rather than on the process
// that forgot — every replica racing to exhaust one server's
// max_connections. No value the SDK could invent is defensible either,
// because the right one depends on the server's ceiling divided by the
// number of replicas, which the SDK cannot see. So this is the REFUSAL
// side of ADR 0031.
MaxOpen int
// MaxIdle is the number of connections kept warm. A non-positive value
// clamps to [DefaultMaxIdle]; a value above MaxOpen clamps DOWN to
// MaxOpen, because database/sql does that reduction silently and a caller
// reading their own configuration back would otherwise be misled.
MaxIdle int
// MaxLifetime is how long a connection may be reused. A non-positive
// value clamps to [DefaultMaxLifetime]; see that constant for why an
// unbounded lifetime is not an option.
MaxLifetime time.Duration
// MaxIdleTime is how long an unused connection is kept. A non-positive
// value is passed through as "no idle deadline", and that is deliberate
// rather than an oversight: MaxLifetime is now always finite, so an idle
// connection is already retired on a bounded schedule, and a second
// timer would only decide how aggressively to shrink a warm pool — a
// tuning choice with no dangerous zero.
MaxIdleTime time.Duration
}
PoolConfig is the connection-pool policy. It is applied to the caller's *sql.DB at construction, and it is a POLICY rather than a set of hints: a field whose zero value database/sql would read as a decision nobody made is either clamped or refused, never passed through (ADR 0031).
type RetryConfig ¶ added in v0.27.0
type RetryConfig struct {
// Attempts is how many times a root transaction may run in all, the
// first included: 1 runs it once and never again. A non-positive value
// clamps to [DefaultRetryAttempts].
Attempts int
// Backoff is the curve of the waits between two attempts: the wait
// after the n-th failed attempt is Backoff.Delay(n). The zero value
// clamps to [DefaultRetryDelay] grown by 2 per attempt with full jitter;
// any other value is the caller's curve, as given — a BaseDelay of zero
// with a ceiling or a jitter is a caller's choice to retry at once.
Backoff kbackoff.Value
// Transient decides which failures earn another attempt. A nil value
// clamps to [Classify]. A context's end never does, whatever Transient
// answers.
Transient coresql.Transient
// Clock is the time source the waits are read from. A nil Clock falls
// back to clock.System; a test hands a ManualClock and advances it
// rather than sleeping.
Clock clock.Timed
}
RetryConfig parameterises NewRetrying: how many times a root transaction may run, how long to wait between two runs, which failures earn another run, and the clock the waits are read from.
Every field has a documented fallback (ADR 0031's clamp side): retrying a transaction the database asked to be run again has no dangerous zero.
type Withheld ¶
type Withheld struct {
// contains filtered or unexported fields
}
Withheld carries a driver's error for errors.Is and errors.As while keeping its text out of Error().
A driver describes the row a statement touched. PostgreSQL's unique violation says "Key (doc_key)=(…) already exists" and MySQL's says "Duplicate entry '…'": a document store's key is routinely an e-mail address, and a queue's row holds the caller's payload. So a package whose rows hold a caller's data joins the driver's error through Withheld, beside a verdict of its own — errors.Join, never errs.Wrap, so the verdict's code stays the origin (CLAUDE.md rule 6) — and no rendering of the joined error quotes a row (ADR 0139 §D7, ADR 0151 §D6). The cause stays one errors.As away for a caller who knows its driver, and what it may log.
Error() says only what names no row: a context's end, in the context's own words; otherwise the driver error's Go type, with its SQLSTATE or its result code when a method offers one — a class of failure, never its subject.
The verdict stays each package's own — docstore's STATEMENT_FAILED, queue's QUEUE_BACKEND_FAILED — since each package's errors are its own; what they share is only this rendering of somebody else's.
func NewWithheld ¶
NewWithheld returns cause withheld: reachable by errors.Is and errors.As, absent from Error().
Source Files
¶
- budget.go
- budget_compliance.go
- config.go
- dialect_sql.go
- doc.go
- executor.go
- health.go
- join.go
- migrate.go
- migrate_config.go
- migrate_filelock.go
- migrate_lock.go
- migrate_plan.go
- migrate_table.go
- observe.go
- resolved.go
- retry.go
- rows.go
- savepoint_stmts.go
- scope_key_type.go
- sql.go
- statements.go
- tx.go
- txscope.go
- txstate.go
- withheld.go