sql

package
v0.16.0 Latest Latest
Warning

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

Go to latest
Published: Sep 30, 2026 License: MIT Imports: 5 Imported by: 0

Documentation

Overview

Package sql — range 0.2.24.* (ADR 0055 core/sql block).

Package sql — declares the sentinel *errs.Error port outcomes. 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 (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, and the two refusals that keep it 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 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 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 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 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 CodeUnknownDialect errs.Code = 0x00_02_18_01 // 0.2.24.1

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

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/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/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/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/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/sql: Down is the Irreversible step; reversing it requires a new forward migration",
		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 added in v0.11.0

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 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) 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/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/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 added in v0.11.0

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/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/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/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