sql

package
v0.12.0 Latest Latest
Warning

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

Go to latest
Published: Nov 3, 2026 License: MIT Imports: 11 Imported by: 4

README

sql

import "github.com/kitsunium/sdk/pkg/v1/data/sql"

Package sql is the public facade for the SDK's relational-database domain: transaction ownership, a pool policy, a health check and a migration runner, over the standard library's database/sql.

It is NOT an ORM, and it never will be. There is no entity mapping, no query builder, no lazy loading and no repository generation — you write SQL, the SDK owns the transaction around it (ADR 0055).

It also ships no driver. Import the one you want, open the pool yourself, and hand it over:

import _ "github.com/jackc/pgx/v5/stdlib"

pool, err := stdsql.Open("pgx", dsn)
dialect, err := sql.ParseDialect("postgres")

tm, err := sql.NewTransactor(sql.Config{
	DB:      pool,
	Dialect: dialect,
	Pool:    sql.PoolConfig{MaxOpen: 20},
})
Who is allowed to commit

Nobody but the manager. Transact hands your unit of work an Executor — Exec, Query, QueryRow, and deliberately nothing else. A helper that receives one CANNOT commit or roll back the transaction it was lent, because those methods do not exist on the type it was given:

err := sql.Transact(ctx, tm, func(ctx context.Context, ex sql.Executor) error {
	if _, err := ex.ExecContext(ctx, "UPDATE accounts SET cents = cents - $1 WHERE id = $2", n, from); err != nil {
		return err
	}
	return credit(ctx, ex, to, n) // credit cannot commit; it has no way to
})

Returning nil commits. Returning an error rolls back and hands the error back to you unchanged, so errors.Is keeps working.

Nesting is a savepoint

database/sql has no nested transactions. Call Transact with a context that already carries one and you get a SAVEPOINT of that transaction rather than a second connection deadlocking against the first:

_ = sql.Transact(ctx, tm, func(ctx context.Context, ex sql.Executor) error {
	if err := mustHappen(ctx, ex); err != nil {
		return err
	}
	// best-effort: if this fails, only ITS work is undone
	if err := sql.Transact(ctx, tm, optional); err != nil {
		log.Warn("optional step skipped", "err", err)
	}
	return nil
})

Three consequences worth knowing before you rely on it:

  • Only postgres, mysql and sqlite are supported, because those three spell savepoints identically. SQL Server, Oracle and Db2 are refused BY NAME at ParseDialect — their nesting is a different algorithm, not a different string, and generating SQL that fails at the first nested transaction in production is not an option.
  • A failed nested scope does NOT condemn the outer transaction. ROLLBACK TO SAVEPOINT is precisely the statement that clears PostgreSQL's aborted state, so catching the inner error leaves you a usable transaction. If that statement itself fails, the transaction is poisoned: every later operation is refused and the commit never happens.
  • A nested call must pass the ZERO TxOptions. A savepoint cannot change isolation or read-only mode, and being handed a weaker transaction than you asked for under a nil error is worse than being told no.
Joining the caller's transaction, and waiting for its commit

The transactor NewTransactor returns has three more capabilities, each an interface of its own beside Transactor, found by type assertion (ADR 0139):

ex, inTx := tm.(sql.Joiner).Join(ctx)          // where a statement under ctx runs
held := tm.(sql.Deferrer).Defer(ctx, notify)    // notify runs once ctx's transaction commits
held = tm.(sql.Aborter).OnAbort(ctx, takeBack) // takeBack runs once ctx's work is undone

Joiner answers the executor of the transaction ctx carries — so a repository called inside its caller's transaction reads what that transaction wrote — or the pool when it carries none. A context that outlived its transaction still names it, and its executor refuses rather than fall back to the pool. Deferrer holds a function until that transaction commits: a rollback drops it, and so does the rollback of the savepoint it was held in. It is how a message, a mail or a store's write hook waits for the work it announces to be committed. The SDK's document store over SQL is built on both. Aborter is the complement of Deferrer: it holds a function until that work is undone — the savepoint it was held in rolls back, or the transaction does not commit — and runs it then, on a context the rollback's cancellation does not reach; a commit drops it. It is how something announced before the commit is taken back when the commit never comes.

Migrations

NewMigrator applies an ordered, versioned set under a lock that excludes every other runner and dies with its holder, so two instances starting together cannot apply the same migration twice and a runner killed mid-run leaves nothing held. On PostgreSQL and MySQL it is the session's advisory lock, which the server drops with the connection. On SQLite it is the database file's write lock (ADR 0140): the run is ONE transaction that takes it with its first statement, each migration a savepoint of it, and the operating system drops a dead process's file locks. A SQLite run needs a single connection, and a migration there cannot run what SQLite refuses inside a transaction — VACUUM, or PRAGMA journal_mode.

Migrations are VALUES you build. The SDK ships no directory, no file format and no naming convention, because the moment it reads a directory it has invented one you did not choose:

m := sql.Migration{
	Version: 20260910143000,
	Name:    "create accounts",
	Up:      sql.Statements("CREATE TABLE accounts (id BIGINT PRIMARY KEY, cents BIGINT NOT NULL)"),
	Down:    sql.Statements("DROP TABLE accounts"),
}

Each migration runs in its own transaction together with its version-table row, so on PostgreSQL and SQLite a failed migration rolls back BOTH halves and the database sits exactly at the previous version.

On MySQL and MariaDB that guarantee does not hold, and it is not something this SDK can fix: DDL causes an IMPLICIT COMMIT, so a migration whose second statement fails leaves the first one applied while its version row is rolled back. Write ONE DDL statement per migration on MySQL and the failure is atomic by construction. See ADR 0055 D8.

Migrator.Plan is the dry run: it reports what Migrator.Up would apply, applies nothing and takes no lock. Irreversible is how a migration says out loud that it cannot be rolled back — a nil Down is refused, because "I forgot the reversal" and "there is no reversal" are the same nil.

Errors

Every failure is a typed SDK error joined with the driver's own, side by side, so neither question loses its answer:

errors.Is(err, sql.CommitFailed)              // the SDK's verdict
errs.HasCode(err, sql.CommitFailed.Code())    // the same, by code
errors.Is(err, driver.ErrBadConn)             // the driver's own

The sentinels above are the matchable values; their Code() is the ADR 0005 dotted quad. No Public message in this domain ever carries a connection string, a query or a bound argument.

Index

Constants

const DialectMySQL Dialect = 2

DialectMySQL is MySQL 5.7+ / MariaDB 10.x with InnoDB.

Initialised with github.com/kitsunium/sdk/internal/core/data/sql.DialectMySQL.

const DialectPostgres Dialect = 1

DialectPostgres is PostgreSQL 9.1+.

Initialised with github.com/kitsunium/sdk/internal/core/data/sql.DialectPostgres.

const DialectSQLite Dialect = 3

DialectSQLite is SQLite 3.6.8+. It has no advisory lock, so NewMigrator serialises a run on the database file's write lock instead (ADR 0140).

Initialised with github.com/kitsunium/sdk/internal/core/data/sql.DialectSQLite.

const StatementBegin StatementKind = 4

StatementBegin is a transaction opened.

Initialised with github.com/kitsunium/sdk/internal/core/data/sql.StatementBegin.

const StatementCommit StatementKind = 5

StatementCommit is a transaction committed.

Initialised with github.com/kitsunium/sdk/internal/core/data/sql.StatementCommit.

const StatementExec StatementKind = 1

StatementExec is a statement that returns no rows.

Initialised with github.com/kitsunium/sdk/internal/core/data/sql.StatementExec.

const StatementPrepare StatementKind = 3

StatementPrepare is a statement compiled for repeated execution.

Initialised with github.com/kitsunium/sdk/internal/core/data/sql.StatementPrepare.

const StatementQuery StatementKind = 2

StatementQuery is a statement that returns rows.

Initialised with github.com/kitsunium/sdk/internal/core/data/sql.StatementQuery.

const StatementRollback StatementKind = 6

StatementRollback is a transaction rolled back.

Initialised with github.com/kitsunium/sdk/internal/core/data/sql.StatementRollback.

Variables

var BeginFailed *errs.Error

BeginFailed reports a transaction the driver would not open.

Initialised with github.com/kitsunium/sdk/internal/core/data/sql.BeginFailed.

Code 0.3.54.3 · reason BEGIN_FAILED · public "The database refused to open a transaction"

var CommitFailed *errs.Error

CommitFailed reports a transaction that had NO effect.

Initialised with github.com/kitsunium/sdk/internal/core/data/sql.CommitFailed.

Code 0.3.54.4 · reason COMMIT_FAILED · public "The transaction could not be committed and had no effect"

var ConfigInvalid *errs.Error

ConfigInvalid is returned by every constructor for a Config that could never produce a working port.

Initialised with github.com/kitsunium/sdk/internal/core/data/sql.ConfigInvalid.

Code 0.3.54.1 · reason CONFIG_INVALID · public "The SQL configuration is incomplete and was refused"

var DialectRefused *errs.Error

DialectRefused is returned by ParseDialect for an engine this SDK recognises and declines. The reason travels in a field.

Initialised with github.com/kitsunium/sdk/internal/core/data/sql.DialectRefused.

Code 0.2.24.2 · reason DIALECT_REFUSED · public "That SQL engine is recognised and deliberately not supported"

var DuplicateMigration *errs.Error

DuplicateMigration refuses two migrations declaring the same version.

Initialised with github.com/kitsunium/sdk/internal/core/data/sql.DuplicateMigration.

Code 0.3.54.17 · reason DUPLICATE_MIGRATION · public "Two migrations declare the same version"

var HealthCheckFailed *errs.Error

HealthCheckFailed reports a database that answered the probe with an error.

Initialised with github.com/kitsunium/sdk/internal/core/data/sql.HealthCheckFailed.

Code 0.3.54.9 · reason HEALTH_CHECK_FAILED · public "The database is not reachable"

var HealthCheckTimeout *errs.Error

HealthCheckTimeout reports a database that did not answer in time.

Initialised with github.com/kitsunium/sdk/internal/core/data/sql.HealthCheckTimeout.

Code 0.3.54.10 · reason HEALTH_CHECK_TIMEOUT · public "The database did not answer the health check in time"

var InvalidMigration *errs.Error

InvalidMigration is returned for a migration with version 0, an empty name, a nil Up or a nil Down.

Initialised with github.com/kitsunium/sdk/internal/core/data/sql.InvalidMigration.

Code 0.2.24.4 · reason INVALID_MIGRATION · public "The migration is not runnable and was refused"

var MigrationChanged *errs.Error

MigrationChanged refuses a run over an applied migration whose statements were edited after they ran. Nothing was applied.

Initialised with github.com/kitsunium/sdk/internal/core/data/sql.MigrationChanged.

Code 0.3.54.18 · reason MIGRATION_CHANGED · public "A migration already applied was edited after it ran"

var MigrationFailed *errs.Error

MigrationFailed reports a migration that did not apply; it was rolled back.

Initialised with github.com/kitsunium/sdk/internal/core/data/sql.MigrationFailed.

Code 0.3.54.11 · reason MIGRATION_FAILED · public "A migration did not apply and was rolled back"

var MigrationIrreversible *errs.Error

MigrationIrreversible is what the Irreversible step returns.

Initialised with github.com/kitsunium/sdk/internal/core/data/sql.MigrationIrreversible.

Code 0.2.24.5 · reason MIGRATION_IRREVERSIBLE · public "That migration was declared irreversible and cannot be rolled back"

var MigrationLockTimeout *errs.Error

MigrationLockTimeout reports another process holding the migration lock for the whole budget. Nothing was applied.

Initialised with github.com/kitsunium/sdk/internal/core/data/sql.MigrationLockTimeout.

Code 0.3.54.14 · reason MIGRATION_LOCK_TIMEOUT · public "Another process is running migrations and did not finish in time"

var MigrationLockUnsupported *errs.Error

MigrationLockUnsupported refuses a Migrator on a dialect with no lock that dies with its holder. No dialect this SDK speaks answers it since SQLite's runner serialises on its file's write lock (ADR 0140).

Initialised with github.com/kitsunium/sdk/internal/core/data/sql.MigrationLockUnsupported.

Code 0.3.54.13 · reason MIGRATION_LOCK_UNSUPPORTED · public "That dialect has no advisory lock, so migrations cannot be serialised"

var MigrationOutOfOrder *errs.Error

MigrationOutOfOrder refuses a pending migration older than one already applied.

Initialised with github.com/kitsunium/sdk/internal/core/data/sql.MigrationOutOfOrder.

Code 0.3.54.12 · reason MIGRATION_OUT_OF_ORDER · public "A pending migration is older than one already applied"

var MigrationUnknownVersion *errs.Error

MigrationUnknownVersion refuses a Down over a version this build does not carry.

Initialised with github.com/kitsunium/sdk/internal/core/data/sql.MigrationUnknownVersion.

Code 0.3.54.15 · reason MIGRATION_UNKNOWN_VERSION · public "The database records a migration this build does not carry"

var NestedIsolation *errs.Error

NestedIsolation is returned when a nested Transact asks for isolation or read-only mode a savepoint cannot provide.

Initialised with github.com/kitsunium/sdk/internal/core/data/sql.NestedIsolation.

Code 0.2.24.3 · reason NESTED_ISOLATION · public "A nested transaction cannot change isolation or read-only mode"

var PoolMisconfigured *errs.Error

PoolMisconfigured refuses a pool policy whose zero value database/sql would read as a decision nobody made — a non-positive MaxOpen.

Initialised with github.com/kitsunium/sdk/internal/core/data/sql.PoolMisconfigured.

Code 0.3.54.2 · reason POOL_MISCONFIGURED · public "The connection-pool policy is not one this SDK will apply"

var RollbackFailed *errs.Error

RollbackFailed travels alongside the error that triggered the rollback.

Initialised with github.com/kitsunium/sdk/internal/core/data/sql.RollbackFailed.

Code 0.3.54.5 · reason ROLLBACK_FAILED · public "Rolling the transaction back did not complete cleanly"

var SavepointFailed *errs.Error

SavepointFailed reports a rejected savepoint statement; the "statement" field names which of the three.

Initialised with github.com/kitsunium/sdk/internal/core/data/sql.SavepointFailed.

Code 0.3.54.6 · reason SAVEPOINT_FAILED · public "A savepoint statement was rejected by the database"

var StatementTimedOut *errs.Error

StatementTimedOut is joined with the driver's error when a statement outlived the transactor's statement budget.

Initialised with github.com/kitsunium/sdk/internal/core/data/sql.StatementTimedOut.

Code 0.3.54.20 · reason STATEMENT_TIMED_OUT · public "A database statement did not complete within its time budget"

var TxClosed *errs.Error

TxClosed reports an Executor used after the scope that lent it returned.

Initialised with github.com/kitsunium/sdk/internal/core/data/sql.TxClosed.

Code 0.3.54.8 · reason TX_CLOSED · public "That transaction handle is no longer valid"

var TxPoisoned *errs.Error

TxPoisoned refuses every operation on a transaction whose savepoint rollback failed. It is never committed.

Initialised with github.com/kitsunium/sdk/internal/core/data/sql.TxPoisoned.

Code 0.3.54.7 · reason TX_POISONED · public "The transaction is no longer usable and will not be committed"

var TxRetriesExhausted *errs.Error

TxRetriesExhausted is joined with the last attempt's error when every attempt of a retrying transactor failed on a transient error.

Initialised with github.com/kitsunium/sdk/internal/core/data/sql.TxRetriesExhausted.

Code 0.3.54.19 · reason TX_RETRIES_EXHAUSTED · public "The transaction kept conflicting with others and was given up"

var UnknownDialect *errs.Error

UnknownDialect is returned by ParseDialect for a name this SDK has never heard of.

Initialised with github.com/kitsunium/sdk/internal/core/data/sql.UnknownDialect.

Code 0.2.24.1 · reason UNKNOWN_DIALECT · public "The SQL dialect is not one this SDK supports"

var VersionTableInvalid *errs.Error

VersionTableInvalid refuses a version-table name that is not a plain SQL identifier.

Initialised with github.com/kitsunium/sdk/internal/core/data/sql.VersionTableInvalid.

Code 0.3.54.16 · reason VERSION_TABLE_INVALID · public "The version-table name is not a plain SQL identifier"

func ChecksumOf

func ChecksumOf(stmts ...string) string

ChecksumOf is the fingerprint a Migration built from stmts records in its Checksum: SHA-256 over the statements, each length-prefixed, in lowercase hex. Set it from the same statements Up runs, and an applied migration edited afterwards is refused with MigrationChanged instead of being called applied by its number alone.

func Classify

func Classify(err error) bool

Classify reports whether err is a failure the database documents as "run the transaction again": SQLSTATE 40001 or 40P01, or SQLite's BUSY or LOCKED. A context's end never is.

func Each

func Each[T any](rows *stdsql.Rows, scan func(*stdsql.Rows) (T, error)) iter.Seq2[T, error]

Each iterates rows with scan and closes them when the iteration ends; a failure is yielded once, last. It is the Next/Scan/Err/Close loop, written once.

func InsertRows

func InsertRows(ctx context.Context, ex Executor, d Dialect, into InsertSpec, rows [][]any) (inserted int64, err error)

InsertRows inserts rows into into.Table in multi-row statements of at most into.PerStatement rows, clamped to the engine's bound-parameter ceiling, and returns how many rows were inserted. into.Table and into.Columns are interpolated as given: validate them first.

func Irreversible

func Irreversible(ctx context.Context, ex Executor) error

Irreversible is the Step a migration assigns to Down to declare, out loud, that it cannot be reversed.

func NewChecker

func NewChecker(cfg Config) (probe Checker, err error)

NewChecker returns a liveness probe bounded by cfg.CheckTimeout.

func NewMigrator

func NewMigrator(cfg Config, mig MigrateConfig) (runner Migrator, err error)

NewMigrator returns the migration runner. Every migration execution is serialised: on the session's advisory lock where the engine has one (PostgreSQL, MySQL), and on the database file's write lock on SQLite, which has none (ADR 0140). No dialect the SDK speaks is refused for the lock.

func NewRetrying

func NewRetrying(tm Transactor, cfg RetryConfig) (manager Transactor, err error)

NewRetrying returns a transactor that runs a root transaction of tm again on a failure cfg.Transient calls transient, up to cfg.Attempts runs, on cfg.Backoff's curve. A nested call is never run again on its own. The unit of work must be safe to run more than once.

func NewTransactor

func NewTransactor(cfg Config) (manager Transactor, err error)

NewTransactor returns the transaction manager for cfg.DB, applying cfg.Pool to it.

func Observe

func Observe(c driver.Connector, obs StatementObserver) driver.Connector

Observe returns c with obs told about every statement its connections send — exec, query, prepare, BEGIN, COMMIT, ROLLBACK —, the SQL text bounded and never an argument. Open the pool over it: stdsql.OpenDB(sql.Observe(connector, obs)). A nil obs returns c.

func ParseDialect

func ParseDialect(name string) (dialect Dialect, err error)

ParseDialect resolves a dialect name and never guesses. A recognised but unsupported engine returns DialectRefused carrying why; an unrecognised one returns UnknownDialect.

func SlowQueries

func SlowQueries(threshold time.Duration, log corelogger.Logger, clk clock.Clock) StatementObserver

SlowQueries returns an observer that logs at Warn each statement that took threshold or longer on clk — its SQL text, duration and rows, never an argument.

func Statements

func Statements(stmts ...string) Step

Statements returns a Step that runs the given statements in order on the transaction's executor.

It is the smallest useful helper and deliberately not a file loader: it invents no directory layout, no naming convention and no parser. Where the text comes from — a literal, an embed.FS, a generator — stays yours.

func Traced

func Traced(tracer coretrace.Tracer) StatementObserver

Traced returns an observer that opens one client span per statement, its status following the statement's error.

func Transact

func Transact(ctx context.Context, tm Transactor, fn TxFunc) error

Transact runs fn inside a transaction with the driver's default isolation.

It is the ergonomic form of Transactor.Transact for the common case. The port itself keeps the options parameter so it never needs a second method (ADR 0039); this helper keeps the call site short.

type Aborter

Aborter is the public alias for the ADR 0039 sibling of Transactor that holds a function until the work a context carries is undone — its savepoint or its transaction rolled back — and drops it with a commit: the complement of Deferrer. Discover it by type assertion.

type Aborter = coresql.Aborter

Aborter is an alias of github.com/kitsunium/sdk/internal/core/data/sql.Aborter, declared as:

type Aborter interface {
	// OnAbort holds fn until the work of the scope ctx carries for this
	// transactor is undone, and reports true.
	//
	// The work is undone when the scope's savepoint rolls back, or when any
	// scope around it does, the root included: the root's unit of work
	// returned an error or panicked, its commit was refused — a poisoned
	// transaction — or the COMMIT itself failed, which [CommitFailed] reports
	// as a unit of work that had no effect. A savepoint that is released
	// hands what it held to the scope around it, which decides in turn; a
	// commit that stands drops every function still held, unrun.
	//
	// The held functions run once, newest first, on the goroutine that ends
	// the scope, after its ROLLBACK or ROLLBACK TO SAVEPOINT and before that
	// scope's Transact returns — or before its panic continues. Each is
	// handed ctx without its cancellation or deadline: a function that
	// cleans up after a rollback must not die of the cancellation that
	// caused it. It must not panic, and it must not open a transaction on
	// ctx, which is over.
	//
	// For a function held with OnAbort and another with Defer under the same
	// scope, exactly one of the two runs, unless a function held with Defer
	// before it panics.
	//
	// OnAbort reports false and holds nothing when ctx carries no open
	// transaction of this transactor. There is nothing to undo, so fn is the
	// caller's again. A nil fn holds nothing and gets the same answer.
	OnAbort(ctx context.Context, fn func(ctx context.Context)) (held bool)
}

func (Aborter) OnAbort
func (Aborter) OnAbort(ctx context.Context, fn func(ctx context.Context)) (held bool)

OnAbort holds fn until the work of the scope ctx carries for this transactor is undone, and reports true.

The work is undone when the scope's savepoint rolls back, or when any scope around it does, the root included: the root's unit of work returned an error or panicked, its commit was refused — a poisoned transaction — or the COMMIT itself failed, which CommitFailed reports as a unit of work that had no effect. A savepoint that is released hands what it held to the scope around it, which decides in turn; a commit that stands drops every function still held, unrun.

The held functions run once, newest first, on the goroutine that ends the scope, after its ROLLBACK or ROLLBACK TO SAVEPOINT and before that scope's Transact returns — or before its panic continues. Each is handed ctx without its cancellation or deadline: a function that cleans up after a rollback must not die of the cancellation that caused it. It must not panic, and it must not open a transaction on ctx, which is over.

For a function held with OnAbort and another with Defer under the same scope, exactly one of the two runs, unless a function held with Defer before it panics.

OnAbort reports false and holds nothing when ctx carries no open transaction of this transactor. There is nothing to undo, so fn is the caller's again. A nil fn holds nothing and gets the same answer.

type Backoff

Backoff is the public alias for the exponential curve a retrying transactor waits on between two attempts (the SDK's one backoff curve, ADR 0103).

type Backoff = kbackoff.Value

Backoff is an alias of github.com/kitsunium/sdk/internal/kernel/backoff.Value, declared as:

type Backoff struct {
	// BaseDelay is the wait after the first failure. Zero or negative waits
	// nothing.
	BaseDelay  time.Duration
	// MaxDelay is the ceiling on the grown delay. Zero means no ceiling.
	MaxDelay   time.Duration
	// Multiplier is the growth factor between consecutive waits. 1 or below,
	// and NaN, mean 2.
	Multiplier float64
	// Jitter widens each wait by a uniform random fraction of itself, drawn
	// from [0, Jitter) and ADDED, after the MaxDelay ceiling — so when a
	// ceiling is set a wait may reach MaxDelay × (1 + Jitter). Zero is the
	// deterministic backoff.
	//
	// It exists because a deterministic backoff SYNCHRONISES the very callers
	// it is meant to spread: N clients that fail against one dependency at the
	// same instant retry at the same instant, N times over. Jitter turns those
	// bursts back into a distribution.
	//
	// It is read only under JitterAdditive.
	Jitter     float64
	// JitterMode is how the randomisation is laid on the curve. The zero,
	// JitterAdditive, is the Jitter above — what Value has always done;
	// JitterFull and JitterDecorrelated are the Builders' Library's two
	// other variants, which randomise the whole wait and ignore Jitter. A
	// value outside the three is read as JitterAdditive.
	JitterMode JitterMode
}

func (Backoff) Delay
func (Backoff) Delay(attempt int) time.Duration

Delay returns the wait after the attempt-th consecutive failure: [Grow] for the curve, then the randomisation JitterMode names — [Widen] by Jitter for JitterAdditive, [Full] for JitterFull, and for JitterDecorrelated [Decorrelate], reading the curve's wait for the attempt before as the previous wait (a loop that keeps its own calls Decorrelate). attempt counts from 1; a smaller value is read as 1, so a loop that has not failed yet and asks anyway waits BaseDelay rather than nothing.

It never returns a negative duration, and it costs at most as many multiplications as it takes the delay to reach its ceiling, however large attempt is.

type Checker

Checker is the public alias for the bounded liveness probe.

type Checker = coresql.Checker

Checker is an alias of github.com/kitsunium/sdk/internal/core/data/sql.Checker, declared as:

type Checker interface {
	// Check probes the database under a bounded budget and returns nil when
	// it answered. A non-nil error is always typed and never carries the
	// connection string.
	Check(ctx context.Context) error
}

func (Checker) Check
func (Checker) Check(ctx context.Context) error

Check probes the database under a bounded budget and returns nil when it answered. A non-nil error is always typed and never carries the connection string.

type Config

Config is the public alias for the parameters every port is built from.

type Config = svcsql.Config

Config is an alias of github.com/kitsunium/sdk/internal/service/data/sql.Config, declared as:

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
}

type Deferrer

Deferrer is the public alias for the ADR 0039 sibling of Transactor that holds a function until the transaction a context carries has committed, and drops it with a rollback. Discover it by type assertion.

type Deferrer = coresql.Deferrer

Deferrer is an alias of github.com/kitsunium/sdk/internal/core/data/sql.Deferrer, declared as:

type Deferrer interface {
	// Defer holds fn until the transaction ctx carries for this transactor
	// commits, and reports true.
	//
	// The held functions run once, in the order they were held, on the
	// goroutine that commits, after the commit and before the outermost
	// Transact returns. A rollback drops them, and so does the rollback of
	// the savepoint they were held in; a savepoint that is released hands
	// them to the scope around it. A panic in one reaches the caller of the
	// outermost Transact after the commit stood, and the functions held after
	// it do not run.
	//
	// Defer reports false and holds nothing when ctx carries no open
	// transaction of this transactor. There is no commit to wait for, so fn
	// is the caller's again, usually to run at once. A nil fn holds nothing
	// and gets the same answer.
	Defer(ctx context.Context, fn func()) (held bool)
}

func (Deferrer) Defer
func (Deferrer) Defer(ctx context.Context, fn func()) (held bool)

Defer holds fn until the transaction ctx carries for this transactor commits, and reports true.

The held functions run once, in the order they were held, on the goroutine that commits, after the commit and before the outermost Transact returns. A rollback drops them, and so does the rollback of the savepoint they were held in; a savepoint that is released hands them to the scope around it. A panic in one reaches the caller of the outermost Transact after the commit stood, and the functions held after it do not run.

Defer reports false and holds nothing when ctx carries no open transaction of this transactor. There is no commit to wait for, so fn is the caller's again, usually to run at once. A nil fn holds nothing and gets the same answer.

type Dialect

Dialect is the public alias for the closed set of SQL engines this SDK can spell. For SQL you write yourself, it spells the engine's vocabulary — the tokens the SDK's own statements are spelled with, never a statement: Dialect.Placeholder renders a bind marker ($1 on PostgreSQL, ? on MySQL and SQLite), Dialect.QuoteIdent a delimited name, its delimiter doubled inside, and Dialect.ForUpdate and Dialect.ForUpdateSkipLocked a row-lock clause, which SQLite — whose exclusion is its one write lock — renders as nothing. QuoteIdent validates nothing: check a name before you interpolate it. An unset Dialect renders no marker and no name, so a statement built from it fails rather than passing for another engine's.

type Dialect = coresql.Dialect

Dialect is an alias of github.com/kitsunium/sdk/internal/core/data/sql.Dialect, declared as:

type Dialect uint8

func (Dialect) ForUpdate
func (Dialect) ForUpdate() string

ForUpdate renders the clause that ends a read by locking the rows it returns until the transaction ends, leading space included: " FOR UPDATE" on PostgreSQL and MySQL. On MySQL it is also a locking read, which reads the latest committed rows rather than a REPEATABLE READ transaction's snapshot.

SQLite has no row lock and no such clause, and renders the empty string: a SQLite transaction excludes every other writer with the database's one write lock, which it takes at its first WRITE, so a transaction that must read under exclusion writes first (ADR 0140). A dialect that is not Dialect.Valid renders the clause: an engine without one refuses the statement, which is louder than a lock dropped in silence.

func (Dialect) ForUpdateSkipLocked
func (Dialect) ForUpdateSkipLocked() string

ForUpdateSkipLocked renders the clause that ends a read by locking the rows it returns and SKIPPING those another transaction holds, leading space included: " FOR UPDATE SKIP LOCKED" on PostgreSQL and MySQL. Two readers then never wait on each other's rows and never take the same one.

It needs PostgreSQL 9.5, MySQL 8.0.1 or MariaDB 10.6 — later than the floors DialectPostgres and DialectMySQL name — and an older server refuses the statement. SQLite renders the empty string and a dialect that is not Dialect.Valid the clause, for the reasons Dialect.ForUpdate gives: a SQLite transaction that has written holds the database's only write lock, so no row it reads is held by anybody else.

func (Dialect) Placeholder
func (Dialect) Placeholder(n int) string

Placeholder renders the bind marker of a statement's n-th argument, counted from 1.

This is the divergence that makes a "portable" hand-written query a fiction: PostgreSQL numbers its markers — $1, $2 — while MySQL and SQLite share the positional ?, whose position is the argument's order. Every statement the SDK binds an argument to takes its markers from here, so a fourth engine is spelled in one place or not at all.

A dialect that is not Dialect.Valid — an unset field — has no marker and renders the empty string, so a statement built from it parses on no engine rather than on whichever one a guess favoured (ADR 0031).

func (Dialect) QuoteIdent
func (Dialect) QuoteIdent(name string) string

QuoteIdent renders name as an identifier of the dialect: delimited, so a table called "order" or "user" is a name rather than a keyword, and with the delimiter doubled wherever the name holds it, which is how every engine spells a delimiter inside a delimited identifier. MySQL delimits with backticks, whatever its ANSI_QUOTES mode says; PostgreSQL and SQLite with the standard's double quotes.

It validates nothing. An identifier cannot be a bound argument, so the SDK interpolates only names it validated at construction, and that check is the whole defence: what a name may hold, and how long it may be — PostgreSQL TRUNCATES a longer one, into what may be another table's name — is the caller's to refuse before quoting it. A dialect that is not Dialect.Valid renders the empty string, for the reason Dialect.Placeholder gives.

func (Dialect) String
func (Dialect) String() string

String returns the dialect's canonical lowercase name.

func (Dialect) SupportsAdvisoryLock
func (Dialect) SupportsAdvisoryLock() bool

SupportsAdvisoryLock reports whether the engine offers a SESSION-SCOPED advisory lock — one the server releases when the connection holding it drops.

SQLite does not: it has no advisory-lock function, only the file lock that serialises writers. That distinction decides HOW Migrator promises mutual exclusion across processes, so it is a property of the dialect and not a runtime discovery (ADR 0055 §D7). On SQLite the runner holds the database file's write lock for the whole run instead, which the operating system releases when its holder dies, as a server releases an advisory lock (ADR 0140).

func (Dialect) Valid
func (Dialect) Valid() bool

Valid reports whether d is a dialect this domain can actually spell.

type Executor

Executor is the public alias for the read/write surface a statement runs against. *sql.DB, *sql.Tx and *sql.Conn all satisfy it with no adapter, and it deliberately carries no Commit, Rollback or Begin.

type Executor = coresql.Executor

Executor is an alias of github.com/kitsunium/sdk/internal/core/data/sql.Executor, declared as:

type Executor interface {
	// ExecContext runs a statement that returns no rows.
	ExecContext(ctx context.Context, query string, args ...any) (stdsql.Result, error)
	// QueryContext runs a statement that returns rows. The caller closes them.
	QueryContext(ctx context.Context, query string, args ...any) (*stdsql.Rows, error)
	// QueryRowContext runs a statement expected to return at most one row.
	QueryRowContext(ctx context.Context, query string, args ...any) *stdsql.Row
}

func (Executor) ExecContext
func (Executor) ExecContext(ctx context.Context, query string, args ...any) (stdsql.Result, error)

ExecContext runs a statement that returns no rows.

func (Executor) QueryContext
func (Executor) QueryContext(ctx context.Context, query string, args ...any) (*stdsql.Rows, error)

QueryContext runs a statement that returns rows. The caller closes them.

func (Executor) QueryRowContext
func (Executor) QueryRowContext(ctx context.Context, query string, args ...any) *stdsql.Row

QueryRowContext runs a statement expected to return at most one row.

type InsertSpec

InsertSpec is the public alias for the statement InsertRows writes: the table, its columns, and how many rows one statement carries at most.

type InsertSpec = svcsql.InsertSpec

InsertSpec is an alias of github.com/kitsunium/sdk/internal/service/data/sql.InsertSpec, declared as:

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
}

type Joiner

Joiner is the public alias for the ADR 0039 sibling of Transactor that says where a statement issued under a context runs: in the transaction the context carries, or on the pool. Discover it by type assertion.

type Joiner = coresql.Joiner

Joiner is an alias of github.com/kitsunium/sdk/internal/core/data/sql.Joiner, declared as:

type Joiner interface {
	// Join returns the executor a statement issued under ctx runs on.
	//
	// When ctx carries a transaction THIS transactor opened, that is the
	// executor of the innermost scope ctx names, and inTx is true. It is
	// valid only while that scope runs and refuses afterwards: a context
	// that outlived its transaction must not fall back to the pool, where
	// its statements would run outside the transaction they were written
	// for.
	//
	// When ctx carries none — no transaction, or only another transactor's —
	// it is the pool, outside any transaction, and inTx is false.
	Join(ctx context.Context) (ex Executor, inTx bool)
}

func (Joiner) Join
func (Joiner) Join(ctx context.Context) (ex Executor, inTx bool)

Join returns the executor a statement issued under ctx runs on.

When ctx carries a transaction THIS transactor opened, that is the executor of the innermost scope ctx names, and inTx is true. It is valid only while that scope runs and refuses afterwards: a context that outlived its transaction must not fall back to the pool, where its statements would run outside the transaction they were written for.

When ctx carries none — no transaction, or only another transactor's — it is the pool, outside any transaction, and inTx is false.

type MigrateConfig

MigrateConfig is the public alias for the migration runner's parameters.

type MigrateConfig = svcsql.MigrateConfig

MigrateConfig is an alias of github.com/kitsunium/sdk/internal/service/data/sql.MigrateConfig, declared as:

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
}

type Migration

Migration is the public alias for one versioned schema change and its reversal.

type Migration = coresql.MigrationValue

Migration is an alias of github.com/kitsunium/sdk/internal/core/data/sql.MigrationValue, declared as:

type Migration struct {
	// Version orders the migration. It must be non-zero and unique within one
	// set: zero is reserved as "before every migration", the value Down takes
	// to reverse everything.
	//
	// Any strictly-increasing scheme works; a UTC timestamp (20260910143000)
	// is the conventional one because two branches developed in parallel then
	// merge without colliding.
	Version  uint64
	// Name is the human label recorded in the version table. It must be
	// non-empty: a schema history whose rows say only "42" is a schema
	// history nobody can read during an incident.
	Name     string
	// Up applies the change. Required.
	Up       Step
	// Down reverses it. Required — and [Irreversible] is how a caller says
	// "this one cannot be reversed" OUT LOUD.
	//
	// A nil Down is refused rather than accepted as "irreversible", because
	// "I forgot the reversal" and "there is no reversal" are the same nil and
	// only one of them is a defect. The same rule core/app/lifecycle applies to a
	// nil Stop, for the same reason.
	Down     Step
	// Checksum is the fingerprint of the statements Up runs, recorded in the
	// version table beside the version when the migration applies, so a
	// later run notices that an applied migration's text was edited — the
	// merge that rewrote a column type, the branch that renumbered — and
	// refuses with [MigrationChanged] before applying anything, instead of
	// calling it applied by its number alone.
	//
	// Empty means unverified: the migration is never compared, which is what
	// a [Step] written as code rather than as statements must be, since a
	// func is opaque (ADR 0055 §D12). A migration built with
	// internal/service/data/sql's Statements sets it from the same
	// statements with ChecksumOf. A version recorded before checksums
	// existed is given this one by the next run that declares it.
	Checksum string
}

func (Migration) Validate
func (Migration) Validate() error

Validate reports why the migration could never run, or nil.

It is a method on the value rather than a check inside the runner so a caller can validate a set at start-up — before any connection exists — which is where a wiring fault should be found.

type Migrator

Migrator is the public alias for the schema-migration runner.

type Migrator = coresql.Migrator

Migrator is an alias of github.com/kitsunium/sdk/internal/core/data/sql.Migrator, declared as:

type Migrator interface {
	// Down reverses every applied migration whose version is STRICTLY GREATER
	// than target, in descending order, each in its own transaction, under the
	// migration lock. Down(ctx, 0) reverses everything.
	Down(ctx context.Context, target uint64) error
	// Plan reports what Up would apply, in the order it would apply it,
	// WITHOUT applying anything and without taking the migration lock. It is
	// the dry run: a read of the version table and a set difference.
	Plan(ctx context.Context) ([]MigrationValue, error)
	// Up applies every pending migration in ascending version order, each in
	// its own transaction, under the migration lock.
	Up(ctx context.Context) error
}

func (Migrator) Down
func (Migrator) Down(ctx context.Context, target uint64) error

Down reverses every applied migration whose version is STRICTLY GREATER than target, in descending order, each in its own transaction, under the migration lock. Down(ctx, 0) reverses everything.

func (Migrator) Plan
func (Migrator) Plan(ctx context.Context) ([]MigrationValue, error)

Plan reports what Up would apply, in the order it would apply it, WITHOUT applying anything and without taking the migration lock. It is the dry run: a read of the version table and a set difference.

func (Migrator) Up
func (Migrator) Up(ctx context.Context) error

Up applies every pending migration in ascending version order, each in its own transaction, under the migration lock.

type PoolConfig

PoolConfig is the public alias for the connection-pool policy.

type PoolConfig = svcsql.PoolConfig

PoolConfig is an alias of github.com/kitsunium/sdk/internal/service/data/sql.PoolConfig, declared as:

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
}

type Preparer

Preparer is the public alias for the ADR 0039 sibling of Executor: an executor that can also prepare a statement. Discover it by type assertion.

type Preparer = coresql.Preparer

Preparer is an alias of github.com/kitsunium/sdk/internal/core/data/sql.Preparer, declared as:

type Preparer interface {
	// PrepareContext compiles a statement. The caller closes it.
	PrepareContext(ctx context.Context, query string) (*stdsql.Stmt, error)
}

func (Preparer) PrepareContext
func (Preparer) PrepareContext(ctx context.Context, query string) (*stdsql.Stmt, error)

PrepareContext compiles a statement. The caller closes it.

type RetryConfig

RetryConfig is the public alias for a retrying transactor's parameters: its attempts, its backoff curve, its classifier and its clock.

type RetryConfig = svcsql.RetryConfig

RetryConfig is an alias of github.com/kitsunium/sdk/internal/service/data/sql.RetryConfig, declared as:

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
}

type Router

Router is the public alias for the transactor's ADR 0039 sibling that routes a read — a read-only transaction, or a read outside one — to the read replica Config.ReadReplica names.

type Router = coresql.Router

Router is an alias of github.com/kitsunium/sdk/internal/core/data/sql.Router, declared as:

type Router interface {
	// Reader returns the executor a read issued under ctx runs on: the
	// innermost scope of the transaction ctx carries for THIS transactor, as
	// [Joiner].Join answers it — on the replica when that transaction is
	// read-only —, else the replica's pool, else the primary's when no replica
	// is configured.
	Reader(ctx context.Context) Executor
}

func (Router) Reader
func (Router) Reader(ctx context.Context) Executor

Reader returns the executor a read issued under ctx runs on: the innermost scope of the transaction ctx carries for THIS transactor, as Joiner.Join answers it — on the replica when that transaction is read-only —, else the replica's pool, else the primary's when no replica is configured.

type StatementKind

StatementKind is the public alias for what a StatementObserver is told about.

type StatementKind = coresql.StatementKind

StatementKind is an alias of github.com/kitsunium/sdk/internal/core/data/sql.StatementKind, declared as:

type StatementKind uint8

func (StatementKind) String
func (StatementKind) String() string

String returns the kind's lowercase name, "unknown" for none.

type StatementObserver

StatementObserver is the public alias for what watches the statements a pool sends: told when each begins, it answers the function told when it ends. It sees the SQL text, bounded, and never an argument or a row.

type StatementObserver = coresql.StatementObserver

StatementObserver is an alias of github.com/kitsunium/sdk/internal/core/data/sql.StatementObserver, declared as:

type StatementObserver interface {
	// Begin is told that a statement of kind is about to run under ctx, and
	// returns end, which is told exactly once when it ended: rows is the
	// count a query's rows read before they closed, or an exec's rows
	// affected when the driver reports it, else 0; err is the driver's own
	// error, nil on success. end is never nil.
	Begin(ctx context.Context, kind StatementKind, query string) (end func(rows int64, err error))
}

func (StatementObserver) Begin
func (StatementObserver) Begin(ctx context.Context, kind StatementKind, query string) (end func(rows int64, err error))

Begin is told that a statement of kind is about to run under ctx, and returns end, which is told exactly once when it ended: rows is the count a query's rows read before they closed, or an exec's rows affected when the driver reports it, else 0; err is the driver's own error, nil on success. end is never nil.

type Step

Step is the public alias for one direction of a migration.

type Step = coresql.Step

Step is an alias of github.com/kitsunium/sdk/internal/core/data/sql.Step, declared as:

type Step func(ctx context.Context, ex Executor) error

type Transactor

Transactor is the public alias for the transaction manager.

type Transactor = coresql.Transactor

Transactor is an alias of github.com/kitsunium/sdk/internal/core/data/sql.Transactor, declared as:

type Transactor interface {
	// Transact runs fn inside a transaction and commits when fn returns nil.
	//
	// If ctx already carries a transaction opened by THIS Transactor, fn runs
	// inside a SAVEPOINT of that transaction instead of a new one, and opts
	// must be the zero value — a savepoint cannot change isolation or
	// read-only-ness, so asking for either is refused with [NestedIsolation]
	// rather than silently ignored (ADR 0055 §D5).
	//
	// A panic inside fn is NOT recovered: the deferred rollback still runs,
	// and the panic reaches the caller with its original stack.
	Transact(ctx context.Context, opts TxOptionsValue, fn TxFunc) error
}

func (Transactor) Transact
func (Transactor) Transact(ctx context.Context, opts TxOptionsValue, fn TxFunc) error

Transact runs fn inside a transaction and commits when fn returns nil.

If ctx already carries a transaction opened by THIS Transactor, fn runs inside a SAVEPOINT of that transaction instead of a new one, and opts must be the zero value — a savepoint cannot change isolation or read-only-ness, so asking for either is refused with NestedIsolation rather than silently ignored (ADR 0055 §D5).

A panic inside fn is NOT recovered: the deferred rollback still runs, and the panic reaches the caller with its original stack.

type Transient

Transient is the public alias for the verdict on a failed transaction: true when the database asks for the whole transaction to run again.

type Transient = coresql.Transient

Transient is an alias of github.com/kitsunium/sdk/internal/core/data/sql.Transient, declared as:

type Transient func(err error) bool

type TxFunc

TxFunc is the public alias for a unit of work run inside a transaction.

type TxFunc = coresql.TxFunc

TxFunc is an alias of github.com/kitsunium/sdk/internal/core/data/sql.TxFunc, declared as:

type TxFunc func(ctx context.Context, ex Executor) error

type TxOptions

TxOptions is the public alias for one transaction's isolation and read-only-ness. The zero value defers to the driver.

type TxOptions = coresql.TxOptionsValue

TxOptions is an alias of github.com/kitsunium/sdk/internal/core/data/sql.TxOptionsValue, declared as:

type TxOptions struct {
	// Isolation is the transaction isolation level. The zero value
	// (sql.LevelDefault) defers to the driver.
	Isolation stdsql.IsolationLevel
	// ReadOnly asks the engine to refuse writes. Not every driver enforces
	// it; those that do not report an error at Begin rather than accepting
	// writes silently, which is why this is passed through rather than
	// emulated.
	ReadOnly  bool
}

func (TxOptions) IsZero
func (TxOptions) IsZero() bool

IsZero reports whether the options ask for nothing beyond the driver's defaults.

It exists because a NESTED scope is a savepoint, and a savepoint can change neither the isolation level nor the read-only-ness of the transaction it sits inside. Non-zero options on a nested call are therefore refused with NestedIsolation rather than ignored — silently downgrading a caller's explicit `Serializable` to whatever the outer transaction happened to use is the kind of "helpful" behaviour that produces a data race nobody can find (ADR 0055 §D5).

func (TxOptions) StdOptions
func (TxOptions) StdOptions() *stdsql.TxOptions

StdOptions renders the value as the stdlib's own option struct, ready for (*sql.DB).BeginTx.

Generated from docs/api by tools/genindex.

Documentation

Overview

Package sql is the public facade for the SDK's relational-database domain: transaction ownership, a pool policy, a health check and a migration runner, over the standard library's database/sql.

It is NOT an ORM, and it never will be. There is no entity mapping, no query builder, no lazy loading and no repository generation — you write SQL, the SDK owns the transaction around it (ADR 0055).

It also ships no driver. Import the one you want, open the pool yourself, and hand it over:

import _ "github.com/jackc/pgx/v5/stdlib"

pool, err := stdsql.Open("pgx", dsn)
dialect, err := sql.ParseDialect("postgres")

tm, err := sql.NewTransactor(sql.Config{
	DB:      pool,
	Dialect: dialect,
	Pool:    sql.PoolConfig{MaxOpen: 20},
})

Who is allowed to commit

Nobody but the manager. Transact hands your unit of work an Executor — Exec, Query, QueryRow, and deliberately nothing else. A helper that receives one CANNOT commit or roll back the transaction it was lent, because those methods do not exist on the type it was given:

err := sql.Transact(ctx, tm, func(ctx context.Context, ex sql.Executor) error {
	if _, err := ex.ExecContext(ctx, "UPDATE accounts SET cents = cents - $1 WHERE id = $2", n, from); err != nil {
		return err
	}
	return credit(ctx, ex, to, n) // credit cannot commit; it has no way to
})

Returning nil commits. Returning an error rolls back and hands the error back to you unchanged, so errors.Is keeps working.

Nesting is a savepoint

database/sql has no nested transactions. Call Transact with a context that already carries one and you get a SAVEPOINT of that transaction rather than a second connection deadlocking against the first:

_ = sql.Transact(ctx, tm, func(ctx context.Context, ex sql.Executor) error {
	if err := mustHappen(ctx, ex); err != nil {
		return err
	}
	// best-effort: if this fails, only ITS work is undone
	if err := sql.Transact(ctx, tm, optional); err != nil {
		log.Warn("optional step skipped", "err", err)
	}
	return nil
})

Three consequences worth knowing before you rely on it:

  • Only postgres, mysql and sqlite are supported, because those three spell savepoints identically. SQL Server, Oracle and Db2 are refused BY NAME at ParseDialect — their nesting is a different algorithm, not a different string, and generating SQL that fails at the first nested transaction in production is not an option.
  • A failed nested scope does NOT condemn the outer transaction. ROLLBACK TO SAVEPOINT is precisely the statement that clears PostgreSQL's aborted state, so catching the inner error leaves you a usable transaction. If that statement itself fails, the transaction is poisoned: every later operation is refused and the commit never happens.
  • A nested call must pass the ZERO TxOptions. A savepoint cannot change isolation or read-only mode, and being handed a weaker transaction than you asked for under a nil error is worse than being told no.

Joining the caller's transaction, and waiting for its commit

The transactor NewTransactor returns has three more capabilities, each an interface of its own beside Transactor, found by type assertion (ADR 0139):

ex, inTx := tm.(sql.Joiner).Join(ctx)          // where a statement under ctx runs
held := tm.(sql.Deferrer).Defer(ctx, notify)    // notify runs once ctx's transaction commits
held = tm.(sql.Aborter).OnAbort(ctx, takeBack) // takeBack runs once ctx's work is undone

Joiner answers the executor of the transaction ctx carries — so a repository called inside its caller's transaction reads what that transaction wrote — or the pool when it carries none. A context that outlived its transaction still names it, and its executor refuses rather than fall back to the pool. Deferrer holds a function until that transaction commits: a rollback drops it, and so does the rollback of the savepoint it was held in. It is how a message, a mail or a store's write hook waits for the work it announces to be committed. The SDK's document store over SQL is built on both. Aborter is the complement of Deferrer: it holds a function until that work is undone — the savepoint it was held in rolls back, or the transaction does not commit — and runs it then, on a context the rollback's cancellation does not reach; a commit drops it. It is how something announced before the commit is taken back when the commit never comes.

Migrations

NewMigrator applies an ordered, versioned set under a lock that excludes every other runner and dies with its holder, so two instances starting together cannot apply the same migration twice and a runner killed mid-run leaves nothing held. On PostgreSQL and MySQL it is the session's advisory lock, which the server drops with the connection. On SQLite it is the database file's write lock (ADR 0140): the run is ONE transaction that takes it with its first statement, each migration a savepoint of it, and the operating system drops a dead process's file locks. A SQLite run needs a single connection, and a migration there cannot run what SQLite refuses inside a transaction — VACUUM, or PRAGMA journal_mode.

Migrations are VALUES you build. The SDK ships no directory, no file format and no naming convention, because the moment it reads a directory it has invented one you did not choose:

m := sql.Migration{
	Version: 20260910143000,
	Name:    "create accounts",
	Up:      sql.Statements("CREATE TABLE accounts (id BIGINT PRIMARY KEY, cents BIGINT NOT NULL)"),
	Down:    sql.Statements("DROP TABLE accounts"),
}

Each migration runs in its own transaction together with its version-table row, so on PostgreSQL and SQLite a failed migration rolls back BOTH halves and the database sits exactly at the previous version.

On MySQL and MariaDB that guarantee does not hold, and it is not something this SDK can fix: DDL causes an IMPLICIT COMMIT, so a migration whose second statement fails leaves the first one applied while its version row is rolled back. Write ONE DDL statement per migration on MySQL and the failure is atomic by construction. See ADR 0055 D8.

Migrator.Plan is the dry run: it reports what Migrator.Up would apply, applies nothing and takes no lock. Irreversible is how a migration says out loud that it cannot be rolled back — a nil Down is refused, because "I forgot the reversal" and "there is no reversal" are the same nil.

Errors

Every failure is a typed SDK error joined with the driver's own, side by side, so neither question loses its answer:

errors.Is(err, sql.CommitFailed)              // the SDK's verdict
errs.HasCode(err, sql.CommitFailed.Code())    // the same, by code
errors.Is(err, driver.ErrBadConn)             // the driver's own

The sentinels above are the matchable values; their Code() is the ADR 0005 dotted quad. No Public message in this domain ever carries a connection string, a query or a bound argument.

Index

Constants

This section is empty.

Variables

View Source
var (
	// UnknownDialect is returned by ParseDialect for a name this SDK has
	// never heard of.
	UnknownDialect = coresql.UnknownDialect
	// DialectRefused is returned by ParseDialect for an engine this SDK
	// recognises and declines. The reason travels in a field.
	DialectRefused = coresql.DialectRefused
	// NestedIsolation is returned when a nested Transact asks for isolation
	// or read-only mode a savepoint cannot provide.
	NestedIsolation = coresql.NestedIsolation
	// InvalidMigration is returned for a migration with version 0, an empty
	// name, a nil Up or a nil Down.
	InvalidMigration = coresql.InvalidMigration
	// MigrationIrreversible is what the Irreversible step returns.
	MigrationIrreversible = coresql.MigrationIrreversible
	// ConfigInvalid is returned by every constructor for a Config that could
	// never produce a working port.
	ConfigInvalid = coresql.ConfigInvalid
	// PoolMisconfigured refuses a pool policy whose zero value database/sql
	// would read as a decision nobody made — a non-positive MaxOpen.
	PoolMisconfigured = coresql.PoolMisconfigured
	// BeginFailed reports a transaction the driver would not open.
	BeginFailed = coresql.BeginFailed
	// CommitFailed reports a transaction that had NO effect.
	CommitFailed = coresql.CommitFailed
	// RollbackFailed travels alongside the error that triggered the rollback.
	RollbackFailed = coresql.RollbackFailed
	// SavepointFailed reports a rejected savepoint statement; the "statement"
	// field names which of the three.
	SavepointFailed = coresql.SavepointFailed
	// TxPoisoned refuses every operation on a transaction whose savepoint
	// rollback failed. It is never committed.
	TxPoisoned = coresql.TxPoisoned
	// TxClosed reports an Executor used after the scope that lent it
	// returned.
	TxClosed = coresql.TxClosed
	// HealthCheckFailed reports a database that answered the probe with an
	// error.
	HealthCheckFailed = coresql.HealthCheckFailed
	// HealthCheckTimeout reports a database that did not answer in time.
	HealthCheckTimeout = coresql.HealthCheckTimeout
	// MigrationFailed reports a migration that did not apply; it was rolled
	// back.
	MigrationFailed = coresql.MigrationFailed
	// MigrationOutOfOrder refuses a pending migration older than one already
	// applied.
	MigrationOutOfOrder = coresql.MigrationOutOfOrder
	// MigrationLockUnsupported refuses a Migrator on a dialect with no lock
	// that dies with its holder. No dialect this SDK speaks answers it since
	// SQLite's runner serialises on its file's write lock (ADR 0140).
	MigrationLockUnsupported = coresql.MigrationLockUnsupported
	// MigrationLockTimeout reports another process holding the migration lock
	// for the whole budget. Nothing was applied.
	MigrationLockTimeout = coresql.MigrationLockTimeout
	// MigrationUnknownVersion refuses a Down over a version this build does
	// not carry.
	MigrationUnknownVersion = coresql.MigrationUnknownVersion
	// VersionTableInvalid refuses a version-table name that is not a plain
	// SQL identifier.
	VersionTableInvalid = coresql.VersionTableInvalid
	// DuplicateMigration refuses two migrations declaring the same version.
	DuplicateMigration = coresql.DuplicateMigration
	// MigrationChanged refuses a run over an applied migration whose
	// statements were edited after they ran. Nothing was applied.
	MigrationChanged = coresql.MigrationChanged
	// TxRetriesExhausted is joined with the last attempt's error when every
	// attempt of a retrying transactor failed on a transient error.
	TxRetriesExhausted = coresql.TxRetriesExhausted
	// StatementTimedOut is joined with the driver's error when a statement
	// outlived the transactor's statement budget.
	StatementTimedOut = coresql.StatementTimedOut
)

Functions

func ChecksumOf added in v0.12.0

func ChecksumOf(stmts ...string) string

ChecksumOf is the fingerprint a Migration built from stmts records in its Checksum: SHA-256 over the statements, each length-prefixed, in lowercase hex. Set it from the same statements Up runs, and an applied migration edited afterwards is refused with MigrationChanged instead of being called applied by its number alone.

func Classify added in v0.12.0

func Classify(err error) bool

Classify reports whether err is a failure the database documents as "run the transaction again": SQLSTATE 40001 or 40P01, or SQLite's BUSY or LOCKED. A context's end never is.

func Each added in v0.12.0

func Each[T any](rows *stdsql.Rows, scan func(*stdsql.Rows) (T, error)) iter.Seq2[T, error]

Each iterates rows with scan and closes them when the iteration ends; a failure is yielded once, last. It is the Next/Scan/Err/Close loop, written once.

func InsertRows added in v0.12.0

func InsertRows(ctx context.Context, ex Executor, d Dialect, into InsertSpec, rows [][]any) (inserted int64, err error)

InsertRows inserts rows into into.Table in multi-row statements of at most into.PerStatement rows, clamped to the engine's bound-parameter ceiling, and returns how many rows were inserted. into.Table and into.Columns are interpolated as given: validate them first.

func Irreversible

func Irreversible(ctx context.Context, ex Executor) error

Irreversible is the Step a migration assigns to Down to declare, out loud, that it cannot be reversed.

func Observe added in v0.12.0

Observe returns c with obs told about every statement its connections send — exec, query, prepare, BEGIN, COMMIT, ROLLBACK —, the SQL text bounded and never an argument. Open the pool over it: stdsql.OpenDB(sql.Observe(connector, obs)). A nil obs returns c.

func Transact

func Transact(ctx context.Context, tm Transactor, fn TxFunc) error

Transact runs fn inside a transaction with the driver's default isolation.

It is the ergonomic form of Transactor.Transact for the common case. The port itself keeps the options parameter so it never needs a second method (ADR 0039); this helper keeps the call site short.

Types

type Aborter added in v0.2.0

type Aborter = coresql.Aborter

Aborter is the public alias for the ADR 0039 sibling of Transactor that holds a function until the work a context carries is undone — its savepoint or its transaction rolled back — and drops it with a commit: the complement of Deferrer. Discover it by type assertion.

type Backoff added in v0.12.0

type Backoff = kbackoff.Value

Backoff is the public alias for the exponential curve a retrying transactor waits on between two attempts (the SDK's one backoff curve, ADR 0103).

type Checker

type Checker = coresql.Checker

Checker is the public alias for the bounded liveness probe.

func NewChecker

func NewChecker(cfg Config) (probe Checker, err error)

NewChecker returns a liveness probe bounded by cfg.CheckTimeout.

type Config

type Config = svcsql.Config

Config is the public alias for the parameters every port is built from.

type Deferrer

type Deferrer = coresql.Deferrer

Deferrer is the public alias for the ADR 0039 sibling of Transactor that holds a function until the transaction a context carries has committed, and drops it with a rollback. Discover it by type assertion.

type Dialect

type Dialect = coresql.Dialect

Dialect is the public alias for the closed set of SQL engines this SDK can spell. For SQL you write yourself, it spells the engine's vocabulary — the tokens the SDK's own statements are spelled with, never a statement: Dialect.Placeholder renders a bind marker ($1 on PostgreSQL, ? on MySQL and SQLite), Dialect.QuoteIdent a delimited name, its delimiter doubled inside, and Dialect.ForUpdate and Dialect.ForUpdateSkipLocked a row-lock clause, which SQLite — whose exclusion is its one write lock — renders as nothing. QuoteIdent validates nothing: check a name before you interpolate it. An unset Dialect renders no marker and no name, so a statement built from it fails rather than passing for another engine's.

const DialectMySQL Dialect = coresql.DialectMySQL

DialectMySQL is MySQL 5.7+ / MariaDB 10.x with InnoDB.

const DialectPostgres Dialect = coresql.DialectPostgres

DialectPostgres is PostgreSQL 9.1+.

const DialectSQLite Dialect = coresql.DialectSQLite

DialectSQLite is SQLite 3.6.8+. It has no advisory lock, so NewMigrator serialises a run on the database file's write lock instead (ADR 0140).

func ParseDialect

func ParseDialect(name string) (dialect Dialect, err error)

ParseDialect resolves a dialect name and never guesses. A recognised but unsupported engine returns DialectRefused carrying why; an unrecognised one returns UnknownDialect.

type Executor

type Executor = coresql.Executor

Executor is the public alias for the read/write surface a statement runs against. *sql.DB, *sql.Tx and *sql.Conn all satisfy it with no adapter, and it deliberately carries no Commit, Rollback or Begin.

type InsertSpec added in v0.12.0

type InsertSpec = svcsql.InsertSpec

InsertSpec is the public alias for the statement InsertRows writes: the table, its columns, and how many rows one statement carries at most.

type Joiner

type Joiner = coresql.Joiner

Joiner is the public alias for the ADR 0039 sibling of Transactor that says where a statement issued under a context runs: in the transaction the context carries, or on the pool. Discover it by type assertion.

type MigrateConfig

type MigrateConfig = svcsql.MigrateConfig

MigrateConfig is the public alias for the migration runner's parameters.

type Migration

type Migration = coresql.MigrationValue

Migration is the public alias for one versioned schema change and its reversal.

type Migrator

type Migrator = coresql.Migrator

Migrator is the public alias for the schema-migration runner.

func NewMigrator

func NewMigrator(cfg Config, mig MigrateConfig) (runner Migrator, err error)

NewMigrator returns the migration runner. Every migration execution is serialised: on the session's advisory lock where the engine has one (PostgreSQL, MySQL), and on the database file's write lock on SQLite, which has none (ADR 0140). No dialect the SDK speaks is refused for the lock.

type PoolConfig

type PoolConfig = svcsql.PoolConfig

PoolConfig is the public alias for the connection-pool policy.

type Preparer

type Preparer = coresql.Preparer

Preparer is the public alias for the ADR 0039 sibling of Executor: an executor that can also prepare a statement. Discover it by type assertion.

type RetryConfig added in v0.12.0

type RetryConfig = svcsql.RetryConfig

RetryConfig is the public alias for a retrying transactor's parameters: its attempts, its backoff curve, its classifier and its clock.

type Router added in v0.12.0

type Router = coresql.Router

Router is the public alias for the transactor's ADR 0039 sibling that routes a read — a read-only transaction, or a read outside one — to the read replica Config.ReadReplica names.

type StatementKind added in v0.12.0

type StatementKind = coresql.StatementKind

StatementKind is the public alias for what a StatementObserver is told about.

const (
	// StatementExec is a statement that returns no rows.
	StatementExec StatementKind = coresql.StatementExec
	// StatementQuery is a statement that returns rows.
	StatementQuery StatementKind = coresql.StatementQuery
	// StatementPrepare is a statement compiled for repeated execution.
	StatementPrepare StatementKind = coresql.StatementPrepare
	// StatementBegin is a transaction opened.
	StatementBegin StatementKind = coresql.StatementBegin
	// StatementCommit is a transaction committed.
	StatementCommit StatementKind = coresql.StatementCommit
	// StatementRollback is a transaction rolled back.
	StatementRollback StatementKind = coresql.StatementRollback
)

type StatementObserver added in v0.12.0

type StatementObserver = coresql.StatementObserver

StatementObserver is the public alias for what watches the statements a pool sends: told when each begins, it answers the function told when it ends. It sees the SQL text, bounded, and never an argument or a row.

func SlowQueries added in v0.12.0

func SlowQueries(threshold time.Duration, log corelogger.Logger, clk clock.Clock) StatementObserver

SlowQueries returns an observer that logs at Warn each statement that took threshold or longer on clk — its SQL text, duration and rows, never an argument.

func Traced added in v0.12.0

func Traced(tracer coretrace.Tracer) StatementObserver

Traced returns an observer that opens one client span per statement, its status following the statement's error.

type Step

type Step = coresql.Step

Step is the public alias for one direction of a migration.

func Statements

func Statements(stmts ...string) Step

Statements returns a Step that runs the given statements in order on the transaction's executor.

It is the smallest useful helper and deliberately not a file loader: it invents no directory layout, no naming convention and no parser. Where the text comes from — a literal, an embed.FS, a generator — stays yours.

type Transactor

type Transactor = coresql.Transactor

Transactor is the public alias for the transaction manager.

func NewRetrying added in v0.12.0

func NewRetrying(tm Transactor, cfg RetryConfig) (manager Transactor, err error)

NewRetrying returns a transactor that runs a root transaction of tm again on a failure cfg.Transient calls transient, up to cfg.Attempts runs, on cfg.Backoff's curve. A nested call is never run again on its own. The unit of work must be safe to run more than once.

func NewTransactor

func NewTransactor(cfg Config) (manager Transactor, err error)

NewTransactor returns the transaction manager for cfg.DB, applying cfg.Pool to it.

type Transient added in v0.12.0

type Transient = coresql.Transient

Transient is the public alias for the verdict on a failed transaction: true when the database asks for the whole transaction to run again.

type TxFunc

type TxFunc = coresql.TxFunc

TxFunc is the public alias for a unit of work run inside a transaction.

type TxOptions

type TxOptions = coresql.TxOptionsValue

TxOptions is the public alias for one transaction's isolation and read-only-ness. The zero value defers to the driver.

Jump to

Keyboard shortcuts

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