sql

package
v0.17.0 Latest Latest
Warning

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

Go to latest
Published: Oct 3, 2026 License: MIT Imports: 6 Imported by: 0

Documentation

Overview

Package sql — range 0.2.24.* (ADR 0055 core/data/sql block), and the transaction manager's, health probe's and migration runner's 0.3.54.* (ADR 0055 service/data/sql block, declared here since ADR 0160).

Package sql — declares the sentinel *errs.Error port outcomes, and the run outcomes of the transaction manager, the health probe and the migration runner in internal/service/data/sql (ADR 0160: every code is declared in the core, at the service's path). Each var's name equals its errs.Define Reason in SCREAMING_SNAKE form.

Public never carries infrastructure

Driver errors are the SDK's worst leak risk: a failed connection reports the host, the port, the user and sometimes the password; a failed statement reports the statement. Every Public below is a fixed literal that names the CLASS of failure and nothing else, and no call site in this domain ever puts a DSN or a SQL fragment into one. Where the detail matters it goes in a Private or a field, both of which are log-only; and the run outcomes the service emits travel beside the driver's own error under errors.Join — reachable from a log, never from errs.PublicOf (ADR 0055 §D9).

Package sql declares the SDK's relational-database domain: a small set of PORTS above database/sql. It is deliberately NOT an ORM, and that refusal is the design, not an omission — see ADR 0055.

What this domain is

Four things, and nothing else:

  • Executor — the read/write surface a query runs against. *sql.DB, *sql.Tx and *sql.Conn all satisfy it AS THEY ARE, with no adapter.
  • Transactor — the transaction manager: it owns Begin, Commit and Rollback, and it never hands any of the three to the code running inside the transaction.
  • Checker — a bounded liveness probe.
  • Migrator — an ordered, versioned, mutually-exclusive schema runner.

What this domain refuses to be

No entity mapping, no query builder, no lazy loading, no identity map, no change tracking, no repository generation, no schema reflection. A framework that needs those integrates a library that provides them; it does not grow them (ADR 0055 §D1). The SDK's job here is the part every application needs and nobody enjoys writing correctly: transaction ownership, savepoints, pool policy, and a migration runner that two processes can start at once.

It also ships NO DRIVER. pgx, go-sql-driver/mysql and the sqlite bindings are connectors to a third-party system, so they belong under third-party/ by the same rule that put the AWS writers there (ADR 0012). The consumer imports the driver it wants and hands this domain a *sql.DB.

The ports do not hide database/sql

Executor speaks *sql.Rows, *sql.Row and sql.Result — the stdlib's own types, not re-declared equivalents. Re-declaring them would be the first step of the ORM this domain refuses to become: once Rows is ours, scanning is ours, and once scanning is ours, mapping is a small step. Organising the stdlib is the whole ambition.

There is no registry

Like proc (ADR 0016), resilience (ADR 0026), net (ADR 0029), scheduler (ADR 0041), token (ADR 0042), session (ADR 0045) and lifecycle (ADR 0050), this domain has no name->implementation registry. There is one Transactor and one Migrator; a registry would have one entry and would add a way to select a database engine from a configuration string — which is precisely the mistake Dialect exists to prevent, since a dialect the SDK cannot spell must be refused at construction and not resolved at runtime.

Package sql — hosts Dialect, the closed set of SQL engines this domain can spell, the vocabulary each one spells statements with, and the two refusals that keep the set closed.

Package sql — hosts the four ports themselves. Kept apart from sql.go so the package's contract is one file: what a caller may implement, and what the SDK promises to accept.

Package sql — hosts MigrationValue and the Step both of its halves are.

Package sql — hosts TxOptionsValue, the per-transaction options a Transactor honours.

Index

Constants

View Source
const CodeBeginFailed errs.Code = 0x00_03_36_03 // 0.3.54.3

CodeBeginFailed identifies a transaction the driver would not open.

View Source
const CodeCommitFailed errs.Code = 0x00_03_36_04 // 0.3.54.4

CodeCommitFailed identifies a transaction whose COMMIT was rejected. The unit of work did NOT take effect.

View Source
const CodeConfigInvalid errs.Code = 0x00_03_36_01 // 0.3.54.1

CodeConfigInvalid identifies a constructor refused because the Config could never produce a working port: a nil *sql.DB or an unusable Dialect.

View Source
const CodeDialectRefused errs.Code = 0x00_02_18_02 // 0.2.24.2

CodeDialectRefused identifies a dialect this SDK RECOGNISES and declines, because its savepoint grammar is a different algorithm rather than a different string. The reason travels in a field.

View Source
const CodeDuplicateMigration errs.Code = 0x00_03_36_11 // 0.3.54.17

CodeDuplicateMigration identifies two migrations declaring the same version.

View Source
const CodeHealthCheckFailed errs.Code = 0x00_03_36_09 // 0.3.54.9

CodeHealthCheckFailed identifies a liveness probe the database answered with an error.

View Source
const CodeHealthCheckTimeout errs.Code = 0x00_03_36_0A // 0.3.54.10

CodeHealthCheckTimeout identifies a liveness probe that did not answer within its budget.

View Source
const CodeInvalidMigration errs.Code = 0x00_02_18_04 // 0.2.24.4

CodeInvalidMigration identifies a migration that could never run: version zero, an empty name, a nil Up, or a nil Down.

View Source
const CodeMigrationFailed errs.Code = 0x00_03_36_0B // 0.3.54.11

CodeMigrationFailed identifies a migration step, or its version-table bookkeeping, that failed. The transaction was rolled back.

View Source
const CodeMigrationIrreversible errs.Code = 0x00_02_18_05 // 0.2.24.5

CodeMigrationIrreversible identifies a Down refused because the caller declared the migration irreversible with the Irreversible step.

View Source
const CodeMigrationLockTimeout errs.Code = 0x00_03_36_0E // 0.3.54.14

CodeMigrationLockTimeout identifies a migration run abandoned because another holder kept the lock for the whole budget.

View Source
const CodeMigrationLockUnsupported errs.Code = 0x00_03_36_0D // 0.3.54.13

CodeMigrationLockUnsupported identifies a Migrator refused at construction because the dialect offers no lock that dies with its holder. No dialect the SDK speaks answers it since ADR 0140.

View Source
const CodeMigrationOutOfOrder errs.Code = 0x00_03_36_0C // 0.3.54.12

CodeMigrationOutOfOrder identifies a pending migration whose version is lower than one already applied — the two-branches-merged hazard.

View Source
const CodeMigrationUnknownVersion errs.Code = 0x00_03_36_0F // 0.3.54.15

CodeMigrationUnknownVersion identifies a version recorded in the database that this binary carries no migration for — so it cannot be reversed.

View Source
const CodeNestedIsolation errs.Code = 0x00_02_18_03 // 0.2.24.3

CodeNestedIsolation identifies a nested Transact that asked for an isolation level or read-only-ness a savepoint cannot provide.

View Source
const CodePoolMisconfigured errs.Code = 0x00_03_36_02 // 0.3.54.2

CodePoolMisconfigured identifies a pool policy the SDK refuses to apply because the value database/sql would read is not the one the caller meant.

View Source
const CodeRollbackFailed errs.Code = 0x00_03_36_05 // 0.3.54.5

CodeRollbackFailed identifies a ROLLBACK the driver refused. It travels alongside the error that triggered the rollback, never instead of it.

View Source
const CodeSavepointFailed errs.Code = 0x00_03_36_06 // 0.3.54.6

CodeSavepointFailed identifies a SAVEPOINT, RELEASE SAVEPOINT or ROLLBACK TO SAVEPOINT statement the engine rejected.

View Source
const CodeTxClosed errs.Code = 0x00_03_36_08 // 0.3.54.8

CodeTxClosed identifies an Executor used after the scope that lent it returned.

View Source
const CodeTxPoisoned errs.Code = 0x00_03_36_07 // 0.3.54.7

CodeTxPoisoned identifies an operation refused because the transaction can no longer be trusted: its savepoint rollback failed, so the engine's state is unknown and nothing more may be committed on it.

View Source
const CodeUnknownDialect errs.Code = 0x00_02_18_01 // 0.2.24.1

CodeUnknownDialect identifies a dialect name this SDK has never heard of.

View Source
const CodeVersionTableInvalid errs.Code = 0x00_03_36_10 // 0.3.54.16

CodeVersionTableInvalid identifies a version-table name that cannot be interpolated into SQL safely. Identifiers cannot be bound as parameters.

Variables

View Source
var (
	// UnknownDialect is returned by ParseDialect for a name that is in
	// neither the accepted nor the refused set.
	UnknownDialect = errs.Define(CodeUnknownDialect, "UNKNOWN_DIALECT",
		"The SQL dialect is not one this SDK supports",
		"core/data/sql: dialect name is not in the accepted set (postgres, mysql, sqlite); the field carries the name",
		errs.WithExitCode(exitConfig))

	// DialectRefused is returned by ParseDialect for an engine this SDK
	// recognises and declines. It is a DIFFERENT answer from UnknownDialect:
	// a caller debugging a refusal needs to know whether their engine is
	// unheard-of or known-and-declined, because only the second one comes
	// with a reason they can evaluate.
	DialectRefused = errs.Define(CodeDialectRefused, "DIALECT_REFUSED",
		"That SQL engine is recognised and deliberately not supported",
		"core/data/sql: the engine's savepoint grammar is a different algorithm, not a different string; the reason field says which",
		errs.WithExitCode(exitConfig))

	// NestedIsolation is returned when a nested Transact asks for non-zero
	// options. A nested scope is a SAVEPOINT inside an already-open
	// transaction, and a savepoint changes neither the isolation level nor
	// the read-only-ness of that transaction. Honouring the request is
	// impossible; ignoring it would hand the caller a weaker transaction than
	// the one they asked for, under a nil error.
	NestedIsolation = errs.Define(CodeNestedIsolation, "NESTED_ISOLATION",
		"A nested transaction cannot change isolation or read-only mode",
		"core/data/sql: a nested scope is a savepoint; open the outer transaction with the options instead",
		errs.WithExitCode(exitConfig))

	// InvalidMigration is returned for a migration that could never run. The
	// "missing" field names which half is absent.
	InvalidMigration = errs.Define(CodeInvalidMigration, "INVALID_MIGRATION",
		"The migration is not runnable and was refused",
		"core/data/sql: migration has version 0, an empty name, a nil Up or a nil Down; the missing field names which",
		errs.WithExitCode(exitConfig))

	// MigrationIrreversible is what the Irreversible step returns. It is the
	// caller's own declaration coming back at them, not a defect the SDK
	// found — which is why it reads as a statement about the migration rather
	// than about the runner.
	MigrationIrreversible = errs.Define(CodeMigrationIrreversible, "MIGRATION_IRREVERSIBLE",
		"That migration was declared irreversible and cannot be rolled back",
		"core/data/sql: Down is the Irreversible step; reversing it requires a new forward migration",
		errs.WithExitCode(exitConfig))

	// ConfigInvalid is returned by every constructor in
	// internal/service/data/sql for a Config that could never produce a
	// working port.
	ConfigInvalid = errs.Define(CodeConfigInvalid, "CONFIG_INVALID",
		"The SQL configuration is incomplete and was refused",
		"service/data/sql: Config has a nil DB or an unusable Dialect; the missing field names which",
		errs.WithExitCode(exitConfig))

	// PoolMisconfigured is returned when a pool field's zero value would be
	// read by database/sql as a policy nobody chose. MaxOpen is the case that
	// matters: database/sql reads 0 as UNLIMITED, and "as many connections as
	// the application happens to want" is a decision made by forgetting a
	// line, not by taking one (ADR 0031's refusal side).
	PoolMisconfigured = errs.Define(CodePoolMisconfigured, "POOL_MISCONFIGURED",
		"The connection-pool policy is not one this SDK will apply",
		"service/data/sql: a non-positive MaxOpen means unlimited in database/sql; state the ceiling explicitly",
		errs.WithExitCode(exitConfig))

	// BeginFailed is joined with the driver's error when a transaction could
	// not be opened. Nothing ran.
	BeginFailed = errs.Define(CodeBeginFailed, "BEGIN_FAILED",
		"The database refused to open a transaction",
		"service/data/sql: BeginTx failed; the driver error travels beside this one under errors.Join",
		errs.WithExitCode(exitTempFail), errs.WithHTTPStatus(httpUnavailable))

	// CommitFailed is joined with the driver's error when COMMIT was
	// rejected. It says the unit of work did NOT take effect, which is the
	// one fact a caller must not have to infer.
	CommitFailed = errs.Define(CodeCommitFailed, "COMMIT_FAILED",
		"The transaction could not be committed and had no effect",
		"service/data/sql: COMMIT was rejected; the driver error travels beside this one under errors.Join",
		errs.WithExitCode(exitTempFail), errs.WithHTTPStatus(httpUnavailable))

	// RollbackFailed travels ALONGSIDE the error that triggered the rollback,
	// never instead of it. A teardown that also breaks is a second defect,
	// and reporting only it would hide the first — the rule ADR 0050 wrote
	// for UnwindFailed, applied to the same shape one layer down.
	RollbackFailed = errs.Define(CodeRollbackFailed, "ROLLBACK_FAILED",
		"Rolling the transaction back did not complete cleanly",
		"service/data/sql: ROLLBACK was rejected; the connection is discarded by database/sql",
		errs.WithExitCode(exitTempFail))

	// SavepointFailed is returned when SAVEPOINT, RELEASE SAVEPOINT or
	// ROLLBACK TO SAVEPOINT is rejected. The "statement" field names which of
	// the three, because the consequences differ: a failed SAVEPOINT means
	// the nested scope never started, while a failed ROLLBACK TO means the
	// engine's state is unknown and the transaction is poisoned.
	SavepointFailed = errs.Define(CodeSavepointFailed, "SAVEPOINT_FAILED",
		"A savepoint statement was rejected by the database",
		"service/data/sql: the statement field names which of SAVEPOINT / RELEASE / ROLLBACK TO failed",
		errs.WithExitCode(exitTempFail))

	// TxPoisoned refuses every further operation on a transaction whose
	// savepoint rollback failed.
	//
	// This is the answer to "does a failed sub-transaction condemn the outer
	// one?". Normally NO: ROLLBACK TO SAVEPOINT is precisely the statement
	// that clears PostgreSQL's aborted state, so a caught inner failure
	// leaves a usable transaction. But when that statement ITSELF fails, the
	// engine's state is no longer known, and continuing would commit a
	// transaction containing work the SDK believes it undid.
	TxPoisoned = errs.Define(CodeTxPoisoned, "TX_POISONED",
		"The transaction is no longer usable and will not be committed",
		"service/data/sql: a savepoint rollback failed, so the engine state is unknown; the whole transaction is abandoned",
		errs.WithExitCode(exitTempFail))

	// TxClosed is returned when an Executor is used after the scope that lent
	// it returned. database/sql would answer ErrTxDone for a finished
	// transaction and would EXECUTE for a nested scope whose outer
	// transaction is still open — the second case is the one worth catching.
	TxClosed = errs.Define(CodeTxClosed, "TX_CLOSED",
		"That transaction handle is no longer valid",
		"service/data/sql: the Executor outlived its Transact scope; it is valid only for the duration of the call",
		errs.WithExitCode(exitConfig))

	// HealthCheckFailed is joined with the driver's error when the probe was
	// answered with a failure.
	HealthCheckFailed = errs.Define(CodeHealthCheckFailed, "HEALTH_CHECK_FAILED",
		"The database is not reachable",
		"service/data/sql: PingContext returned an error; the driver error travels beside this one under errors.Join",
		errs.WithExitCode(exitTempFail), errs.WithHTTPStatus(httpUnavailable))

	// HealthCheckTimeout is returned when the probe did not answer inside its
	// budget. It is a separate code from HealthCheckFailed because a database
	// that refuses is a different operational fact from one that hangs, and a
	// readiness endpoint routes on the difference.
	HealthCheckTimeout = errs.Define(CodeHealthCheckTimeout, "HEALTH_CHECK_TIMEOUT",
		"The database did not answer the health check in time",
		"service/data/sql: the probe budget expired; the ping goroutine is abandoned, never killed",
		errs.WithExitCode(exitTempFail), errs.WithHTTPStatus(httpUnavailable))

	// MigrationFailed is joined with the step's own error when a migration or
	// its bookkeeping row fails. The transaction was rolled back, so neither
	// the schema change nor the version row took effect — except on engines
	// with non-transactional DDL, which is stated in the package doc rather
	// than papered over.
	MigrationFailed = errs.Define(CodeMigrationFailed, "MIGRATION_FAILED",
		"A migration did not apply and was rolled back",
		"service/data/sql: version+direction fields; step error joined; on MySQL DDL commits implicitly, "+
			"so a failed step can leave the schema change applied WITHOUT its version row (ADR 0055 D8)",
		errs.WithExitCode(exitTempFail))

	// MigrationOutOfOrder refuses a pending migration whose version is lower
	// than one already applied. It is the two-branches-merged hazard: both
	// branches numbered from the same base, one shipped first, and the other
	// would now apply UNDER it — producing a schema that matches neither
	// branch's expectation and a version table that cannot express what
	// happened.
	MigrationOutOfOrder = errs.Define(CodeMigrationOutOfOrder, "MIGRATION_OUT_OF_ORDER",
		"A pending migration is older than one already applied",
		"service/data/sql: renumber the migration above the highest applied version; the fields carry both",
		errs.WithExitCode(exitConfig))

	// MigrationLockUnsupported is the refusal of a Migrator over a dialect
	// that offers no lock dying with its holder — never a run without one: a
	// runner that silently drops mutual exclusion is at its most dangerous
	// exactly when two instances start together, which is the case it exists
	// for.
	//
	// No dialect the SDK speaks answers it since ADR 0140: PostgreSQL and
	// MySQL serialise on the session's advisory lock, SQLite on the database
	// file's write lock. It stays defined because it is published, and a
	// caller matching it would otherwise stop compiling.
	MigrationLockUnsupported = errs.Define(CodeMigrationLockUnsupported, "MIGRATION_LOCK_UNSUPPORTED",
		"That dialect has no advisory lock, so migrations cannot be serialised",
		"service/data/sql: the dialect offers neither a session-scoped advisory lock nor a database-file lock; wire your own exclusion",
		errs.WithExitCode(exitConfig))

	// MigrationLockTimeout is returned when another holder kept the migration
	// lock for the whole budget. Nothing was applied.
	MigrationLockTimeout = errs.Define(CodeMigrationLockTimeout, "MIGRATION_LOCK_TIMEOUT",
		"Another process is running migrations and did not finish in time",
		"service/data/sql: the advisory lock was held throughout the budget; no migration was applied",
		errs.WithExitCode(exitTempFail))

	// MigrationUnknownVersion refuses a Down over a version the running
	// binary has no migration for. The database is ahead of the code, and
	// reversing a change whose Down is not present would be guessing.
	MigrationUnknownVersion = errs.Define(CodeMigrationUnknownVersion, "MIGRATION_UNKNOWN_VERSION",
		"The database records a migration this build does not carry",
		"service/data/sql: Down cannot reverse a version with no Step; deploy the build that owns it",
		errs.WithExitCode(exitConfig))

	// VersionTableInvalid refuses a version-table name that is not a bare SQL
	// identifier. A table name cannot be a bound parameter, so it is
	// interpolated — which makes validating it the only thing between a
	// configuration string and an injection.
	VersionTableInvalid = errs.Define(CodeVersionTableInvalid, "VERSION_TABLE_INVALID",
		"The version-table name is not a plain SQL identifier",
		"service/data/sql: an identifier cannot be bound as a parameter, so only [A-Za-z_][A-Za-z0-9_]* is accepted",
		errs.WithExitCode(exitConfig))

	// DuplicateMigration refuses two migrations declaring the same version.
	// The version IS the identity, so a duplicate makes "has this been
	// applied?" unanswerable.
	DuplicateMigration = errs.Define(CodeDuplicateMigration, "DUPLICATE_MIGRATION",
		"Two migrations declare the same version",
		"service/data/sql: a version identifies a migration; the field carries the duplicated value",
		errs.WithExitCode(exitConfig))
)

Functions

func Irreversible

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

Irreversible is the Step a caller assigns to MigrationValue.Down to declare that the migration cannot be reversed. It always fails, with a typed error naming the version.

It is a value rather than a nil convention so the claim appears in the diff and in the version-table review, and so `Down` reports a refusal the caller wrote rather than a nil dereference the SDK discovered.

Types

type Checker

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
}

Checker reports whether the database is reachable. Implementations MUST be safe for concurrent use.

type Deferrer

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)
}

Deferrer is an ADR 0039 sibling of Transactor: a transactor that holds a function until the transaction a context carries has committed. Discover it by type assertion; the SDK's own transactor implements it.

It is how an effect that must not happen for work that is rolled back — a store's write hooks, a message, a mail — waits for the commit (ADR 0139).

type Dialect

type Dialect uint8

Dialect names a SQL engine whose savepoint grammar, placeholder syntax, identifier quoting, row-lock clauses and advisory-lock mechanism this SDK knows exactly.

The set is CLOSED and small on purpose. Every feature this domain adds — a savepoint, a `CREATE TABLE IF NOT EXISTS`, an advisory lock — is spelled differently by each engine, and there is no portable subset that covers all three. So the domain refuses a dialect it cannot spell AT CONSTRUCTION, by name, rather than generating SQL that will fail at the first savepoint of the first nested transaction on a production database (ADR 0055 §D4).

The zero value is DialectUnknown and is not usable: it is what an unset configuration field looks like, and reading it as "probably Postgres" is how a MySQL deployment discovers the difference in production (ADR 0031).

const (
	// DialectUnknown is the unusable zero value.
	DialectUnknown Dialect = iota
	// DialectPostgres is PostgreSQL 9.1+ (and wire-compatible engines that
	// implement pg_advisory_lock, e.g. CockroachDB advertises the function
	// but the caller must verify it is a real lock before claiming this).
	DialectPostgres
	// DialectMySQL is MySQL 5.7+ / MariaDB 10.x with InnoDB.
	DialectMySQL
	// DialectSQLite is SQLite 3.6.8+.
	DialectSQLite
)

func ParseDialect

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

ParseDialect resolves a dialect name. It never guesses.

A name in the accepted set resolves. A name in the refused set returns DialectRefused carrying WHY, so the caller learns their engine is known and declined rather than unrecognised. Anything else returns UnknownDialect.

func (Dialect) ForUpdate

func (d 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 (d 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 (d 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 (d 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 (d Dialect) String() string

String returns the dialect's canonical lowercase name.

func (Dialect) SupportsAdvisoryLock

func (d 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 (d Dialect) Valid() bool

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

type Executor

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
}

Executor is the surface a statement runs against: a pool, a connection, or a transaction. Implementations MUST be safe for concurrent use, with the documented exception of a transaction-scoped executor, which is confined to its scope (see internal/service/data/sql).

The method set is a strict SUBSET of what *sql.DB, *sql.Tx and *sql.Conn already expose, so all three satisfy this port with no adapter. That is the point: the SDK is not wrapping database/sql, it is naming the part of it a caller should be allowed to reach.

It is FROZEN at three methods. pkg/v1/data/sql aliases it, so under ADR 0039 a fourth method would break every downstream double at compile time with no deprecation window. New capabilities arrive as SIBLING interfaces the implementation is type-asserted for — Preparer is the first.

What is deliberately absent

There is no Commit, no Rollback and no Begin. A function that receives an Executor therefore CANNOT end the transaction it was handed: the method does not exist, so the mistake does not compile. Ownership of a transaction stays with the Transactor that opened it (ADR 0055 §D3).

type Joiner

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)
}

Joiner is an ADR 0039 sibling of Transactor: a transactor that says where a statement issued under a context runs. Discover it by type assertion; the SDK's own transactor implements it.

It is what lets a callee JOIN the transaction its caller opened instead of opening a second one. A second transaction takes a second connection, does not see the caller's uncommitted writes, and under a pool of one waits forever for the connection its own caller holds (ADR 0139).

type MigrationValue

type MigrationValue 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
}

MigrationValue is one versioned schema change and its reversal.

The SDK does NOT own a migration FILE FORMAT, a directory layout, or a naming convention, and it ships no example directory. A migration is a value the consumer constructs, because the moment the SDK reads a directory it has invented a convention every consumer must adopt and no consumer asked for (ADR 0055 §D8). Where the statements come from — a Go literal, an embed.FS, a generator — is the caller's decision and none of the SDK's business.

The zero value is not runnable and is refused at construction.

func (MigrationValue) Validate

func (m MigrationValue) 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

type Migrator interface {
	// 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
	// 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
}

Migrator applies an ordered, versioned set of MigrationValue to a schema, under mutual exclusion. Implementations MUST be safe for concurrent use, and MUST be safe against a SECOND PROCESS running the same migrations at the same instant.

IFACE-PLUGIN: the concrete runner stays unexported behind its constructor in internal/service/data/sql.

type Preparer

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

Preparer is the first ADR 0039 sibling of Executor: an executor that can also prepare a statement for repeated execution. Discover it by type assertion rather than by widening Executor.

*sql.DB, *sql.Tx and *sql.Conn all satisfy it; a transaction-scoped executor does too, and the prepared statement it returns is bound to that transaction and dies with it.

type Step

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

Step is one direction of a migration: the statements that move the schema.

A FUNCTION port, for the same reason TxFunc is: one behaviour, so a named func IS the contract, and a func type cannot grow a method (ADR 0039).

It receives the SAME Executor the surrounding transaction runs on, which is what makes a migration and its version-table row commit together.

type Transactor

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
}

Transactor owns transactions. Implementations MUST be safe for concurrent use.

It is FROZEN at one method, and the options travel in a struct rather than as a second method, precisely so the port never needs a sibling for "Transact, but read-only" (ADR 0039).

IFACE-PLUGIN: the concrete manager stays unexported behind its constructor in internal/service/data/sql.

type TxFunc

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

TxFunc is the unit of work a Transactor runs inside a transaction.

It is a FUNCTION port, the shape internal/core/CLAUDE.md already admits for resilience.Operation, scheduler.Job and lifecycle.Start: one behaviour, so a named func IS the contract. ADR 0039 is satisfied structurally — a func type cannot grow a method at all.

ctx is the transaction-scoped context. Passing it on is what lets a callee join THIS transaction rather than open a second one, and what lets a nested Transactor.Transact become a savepoint instead of a deadlock against the transaction its own caller is holding.

Returning nil commits (or releases the savepoint). Returning an error rolls back (or rolls back to the savepoint) and the error is returned to the caller VERBATIM, so errors.Is keeps working across the boundary.

ex is valid ONLY for the duration of the call. Using it afterwards is refused with a typed error rather than left to database/sql's ErrTxDone.

type TxOptionsValue

type TxOptionsValue 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
}

TxOptionsValue parameterises ONE transaction. It is a value, not a manager setting, because isolation is a property of the unit of work and not of the pool it runs on.

The zero value is a working configuration: the driver's default isolation level, read-write. That is the only non-arbitrary default available — every engine defines its own default level and picking one here would silently change the semantics of an existing application on migration (ADR 0031's clamp side, where the default needs no explanation because it is the database's own).

It is deliberately a struct rather than two extra method parameters so Transactor stays frozen at one method: a new option is a new FIELD, which breaks nobody, where a new method breaks every downstream double (ADR 0039).

func (TxOptionsValue) IsZero

func (o TxOptionsValue) 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 (TxOptionsValue) StdOptions

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

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

Jump to

Keyboard shortcuts

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