Help us shape the future of Go! Take the 2026 Go Developer Survey

sqlate

package module
v0.6.1 Latest Latest
Warning

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

Go to latest
Published: Oct 15, 2026 License: Apache-2.0 Imports: 6 Imported by: 9

README

sqlate

SQL templating for Go: authored .sql files made dynamic and composable, with a PostgreSQL dialect and a conventions linter.

github.com/standards-lab/sqlate is the base module, and it imports only the standard library. The PostgreSQL dialect is the postgres sub-module and the linter is the sqlint sub-module, each released on its own tags.

The problem

Web services usually avoid real SQL. Queries are embedded as string literals in the host language or generated by an ORM, and either way the SQL is written in a language that is not SQL. sqlate keeps queries in .sql files, so SQL is expressed in its own language. It layers a small syntax onto the files, so a file can express reusable fragments and compose dynamic queries. The name combines SQL and template, for the composition the library adds to .sql files.

Documentation

  1. Concepts: patterns and statements by example, and the grammar the files are written in.
  2. Quick start: a working program from go get to a linted, unit-tested store, each feature shown once.
  3. Features: every feature, package by package, in the detail needed to use it without reading the source.
  4. Glossary: the vocabulary the library introduces, by layer.

The package documentation, go doc github.com/standards-lab/sqlate/..., is the reference for each exported name.

How it works

A statement is a .sql file with a header of declarations and a body. The library reads the header; the body is the SQL the engine receives. The engine is the database server the driver sends SQL to, PostgreSQL through the postgres sub-module. This statement edits a row under an optimistic-concurrency guard:

--| tier: standard
UPDATE team
SET name = {{name}}, {{> sql.guard_set}}
WHERE {{> sql.guard_where}}

{{name}} is a parameter, bound by name. {{> sql.guard_set}} and {{> sql.guard_where}} are includes of two patterns the library publishes under its own namespace, sql:

--| tier: standard
-- The protocol columns a guarded command advances; the command's own SET
-- list precedes it.
updated_at = CURRENT_TIMESTAMP, version = version + 1
--| tier: standard
-- The optimistic-concurrency guard's predicate, over a guarded table's id
-- and version columns; a command includes it as {{> sql.guard_where}}.
id = {{id}} AND version = {{version}}

At compile time the library splices the patterns' bodies into the statement, and the pattern's parameters become the statement's. Compiled for PostgreSQL, the engine receives:

UPDATE team
SET name = $1, updated_at = CURRENT_TIMESTAMP, version = version + 1
WHERE id = $2 AND version = $3

In Go, the statement directory is compiled once against the catalog of patterns, the statement is fetched by its file name and bound to the value that runs it, and the value runs with its arguments by name. db is the session over the program's own *sql.DB.

//go:embed statements/*.sql
var files embed.FS

catalog := query.MustCatalog(query.Patterns())
stmts := catalog.MustCompile(files, "statements", db.Dialect())
edit := stmts.Statement("edit").Guarded(stmts.Statement("version"), "version")

// id is the row's key and current the version the caller read.
version, err := edit.Run(ctx, db, current, query.Args{"id": id, "name": "Core Platform"})

Run returns the new version, sql.ErrNoRows when no row has that id, and query.ErrVersionMismatch when the row exists at another version than current.

Conventions

Three conventions the library keeps are stricter than a reader might expect:

  • The base module depends on the standard library alone. A sourced dependency enters only through a sub-module's go.mod: pgx through postgres, the TOML parser through sqlint.
  • sqltest is a public package: the scripted driver the library's own tests run over is the one a consumer's tests run over too, so a unit test needs no database.
  • Every .sql file declares its tier, standard or native, and a native file names the engine feature it uses and the port to other engines. The port list of a code base is one search for --| tier: native.

Packages

  • sqlate (the root) is the session layer: DB over a plain *sql.DB, transactions, the Dialect interface, and the error taxonomy every engine returns.
  • header reads the --| declaration header of a .sql file.
  • query compiles authored statements against the pattern catalog and binds them to typed values: rows, projections with request directives, and guards.
  • migrate versions a schema over authored SQL under the engine's lock, and replaces a third-party migration dependency.
  • sqltest is the scripted database/sql driver for hermetic tests.
  • postgres (sub-module) is the PostgreSQL dialect: placeholders, error classification, the advisory lock, and the engine's native forms.
  • sqlint (sub-module) is the conventions linter as a package, with the command at sqlint/cmd/sqlint.

Development

The repository is a Go workspace of three modules. Tasks run through mise:

mise run check         # build, vet, format, fix, tidy, unit tier, lint, sqlint; writes nothing
mise run currency      # report requirements, Go, tools, actions, and images behind their latest
mise run upgrade       # upgrade every module's go directive and requirements, and the tools, to their latest
mise run db:up         # build and start the compose PostgreSQL and wait until it is healthy
mise run db:down       # stop PostgreSQL, keeping its data
mise run db:reset      # stop PostgreSQL and drop its data
mise run integration   # run the integration tier against the running PostgreSQL
mise run acceptance    # run db:up and integration, then db:reset whatever the result

compose.yml runs PostgreSQL on port 5433; POSTGRES_PORT moves it. The service builds from compose/postgres/Dockerfile, whose FROM line is its one image pin and which carries its configuration and health check; mise run currency reports a pin behind its latest. mise.toml sets SQLATE_DSN, whose port follows the same variable. The integration tier is not part of CI.

License

Apache License 2.0.

Documentation

Overview

Package sqlate is the session layer of the sqlate library, which every authored statement runs through. It wraps a plain *sql.DB behind the stdlib method set (ExecContext, QueryContext, PrepareContext), maps every driver error through the engine's dialect at that boundary, and owns the transaction runner. The package depends on the standard library alone; each engine lives in a sub-module (postgres) that supplies the dialect, so a consumer imports its engine once, where it opens its pool. The packages above it, query and migrate, take a Session and never a driver.

Wrapper

Wrap builds the DB session over a pool with the engine's Dialect. Lifecycle (opening, readiness, closing) belongs to whoever owns the pool; DB adds error mapping, prepare, options on Begin, and pinned connections. It performs no I/O: a nil pool or dialect is a defect in the caller and panics. DB.Conn pins one connection for a protocol that needs session scope, such as a session-level lock or a run of non-transactional DDL; DB.Dialect returns the dialect statements are compiled against.

Sessions and transactions

Session is the method set the query and migrate packages take, implemented by both DB and Tx, so the same statement runs against the pool or inside a transaction. The consumer owns the transaction boundary: DB.Begin opens a Tx with TxOption values applied, and DB.Transact runs one unit of work with a result: commit on success, rollback on the unit's error or panic; Transact is the same runner over any Beginner. Tx.Commit routes its error through the dialect's MapError, the one place a violation deferred to COMMIT can be classified. ErrorMapper is the capability both sessions expose for errors that arise after a call returns, rows.Err and Scan, which the session cannot see.

Dialect

Dialect is what the library needs from an engine: its name, how it renders the nth bind parameter, and how its driver's errors classify. An engine sub-module implements it. Capabilities beyond it are separate interfaces a protocol asserts: Locker, the named session-scoped lock a concurrent-starter protocol such as migration takes; TryLocker, the same lock tried without waiting, for a protocol that skips its work when another session holds it or, as migration does, polls for it; and query.Returner, which renders a command that returns its changed row as one statement. Beginner is the session capability a protocol asserts to open its own transaction.

Errors

The package owns the error taxonomy consumers match on. ErrConnectionFailed classifies a connectivity failure, ErrInvalidValue a data exception, ErrDependentObjects a refusal to drop an object another still depends on, and ErrSerializationFailure a serialization failure under SERIALIZABLE isolation, each wrapped in the dual form fmt.Errorf("%w: %w", sentinel, err) so errors.Is classifies while the driver's error stays recoverable. The session classifies connectivity itself, before the dialect: a network error or driver.ErrBadConn from any call means the engine never saw the statement, so the dialect's vocabulary does not apply. A dialect's MapError returns the four constraint classes (ErrUniqueViolation, ErrForeignKeyViolation, ErrCheckViolation, ErrNotNullViolation) inside a ConstraintError, with the constraint name when the driver exposes it. sql.ErrNoRows is never mapped; MapError returns it unchanged.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrConnectionFailed classifies a failure to reach the engine: a
	// failure to obtain a connection or begin a transaction, and a network
	// error or driver.ErrBadConn raised by any session call. It is wrapped
	// as fmt.Errorf("%w: %w", ErrConnectionFailed, err) so errors.Is
	// matches it and the driver's error stays reachable.
	ErrConnectionFailed = errors.New("database connection failed")

	// ErrInvalidValue classifies a data exception: a bound value the engine
	// could not read as the type it was cast to (SQLSTATE class 22). It is
	// the engine-side half of request validation (a filter value that is
	// not a uuid, a date that is not a date), and a dialect maps its
	// engine's form to it, wrapped in the same dual form.
	ErrInvalidValue = errors.New("invalid value")

	// ErrUniqueViolation classifies a unique-constraint violation. A
	// dialect's MapError wraps it in a [ConstraintError]; errors.Is
	// matches the class.
	ErrUniqueViolation = errors.New("unique constraint violation")

	// ErrForeignKeyViolation classifies a foreign-key-constraint violation,
	// wrapped the same way as [ErrUniqueViolation].
	ErrForeignKeyViolation = errors.New("foreign key constraint violation")

	// ErrCheckViolation classifies a check-constraint violation, wrapped
	// the same way as [ErrUniqueViolation].
	ErrCheckViolation = errors.New("check constraint violation")

	// ErrNotNullViolation classifies a not-null-constraint violation,
	// wrapped the same way as [ErrUniqueViolation].
	ErrNotNullViolation = errors.New("not-null constraint violation")

	// ErrDependentObjects classifies a refusal to drop an object another
	// object still depends on (SQLSTATE class 2BP01). A multi-set
	// migration's Down or Reset raises it when it reverts sets out of
	// order and a foreign key from a set above still references the set
	// being reverted. A dialect's MapError wraps it as
	// fmt.Errorf("%w: %w", ErrDependentObjects, err) so errors.Is matches
	// the class and the driver's error stays reachable.
	ErrDependentObjects = errors.New("dependent objects still exist")

	// ErrSerializationFailure classifies a serialization failure under
	// SERIALIZABLE isolation (SQLSTATE 40001). Two migrator starters
	// raise it when they race under the migration lock and the engine
	// offers no native advisory lock to serialize them. A dialect maps
	// its engine's form to it, wrapped in the same dual form as
	// [ErrDependentObjects].
	ErrSerializationFailure = errors.New("serialization failure")
)

Functions

func Transact added in v0.3.0

func Transact[T any](ctx context.Context, b Beginner, fn func(*Tx) (T, error), opts ...TxOption) (T, error)

Transact runs fn as one unit of work with a result in a transaction b opens: begin with opts, fn, commit on success. On fn's error it rolls back and returns that error, with a rollback failure joined onto it. A panic in fn rolls back and re-panics, so no transaction leaks to the pool's reaper. b is *DB or any type that embeds it, so a protocol handed a session it asserts to Beginner runs its unit the way DB.Transact does.

Types

type Beginner added in v0.3.0

type Beginner interface {
	Begin(ctx context.Context, opts ...TxOption) (*Tx, error)
}

Beginner is a session that can open a transaction: *DB, and any type that embeds it. A protocol that must run several statements as one unit, and is handed a session rather than a transaction, asserts it to open its own.

type ConstraintError

type ConstraintError struct {
	Constraint string
	Table      string
	Column     string
	Class      error
	Err        error
}

ConstraintError is a classified constraint violation: the class sentinel, the driver's own error, and the violated constraint's name, table, and column when the driver exposes them. A driver exposes them unevenly: a not-null violation names a column and no constraint, so Column is the only handle that case offers. Unwrap yields both wrapped errors, so errors.Is matches the class and errors.As finds the driver error, and a consumer maps the constraint name, or the column name when there is no constraint name, to its field.

func (*ConstraintError) Error

func (e *ConstraintError) Error() string

func (*ConstraintError) Unwrap

func (e *ConstraintError) Unwrap() []error

type DB

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

DB is the pool session over a plain *sql.DB. Lifecycle (opening, readiness, closing) belongs to whoever owns the pool; DB adds mapping, prepare, options on Begin, and pinned connections.

func Wrap

func Wrap(pool *sql.DB, dialect Dialect) *DB

Wrap builds the session over pool with dialect, the engine's or a capability-adding wrapper of it. Nil arguments are a defect in the caller and panic.

func (*DB) Begin

func (d *DB) Begin(ctx context.Context, opts ...TxOption) (*Tx, error)

Begin opens a transaction with the options applied; a failure to begin wraps ErrConnectionFailed.

func (*DB) Conn

func (d *DB) Conn(ctx context.Context) (*sql.Conn, error)

Conn pins one connection for a protocol that needs session scope: a session-level lock, a run of non-transactional DDL. The caller closes it.

func (*DB) Dialect

func (d *DB) Dialect() Dialect

Dialect returns the dialect statements are compiled against.

func (*DB) ExecContext

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

func (*DB) MapError

func (d *DB) MapError(err error) error

MapError classifies a connectivity failure as ErrConnectionFailed and routes every other error through the dialect; nil stays nil.

func (*DB) PrepareContext

func (d *DB) PrepareContext(ctx context.Context, query string) (*sql.Stmt, error)

func (*DB) QueryContext

func (d *DB) QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error)

func (*DB) Transact

func (d *DB) Transact[T any](ctx context.Context, fn func(*Tx) (T, error), opts ...TxOption) (T, error)

Transact runs fn as one unit of work with a result on the pool: it is the root Transact over d.

type Dialect

type Dialect interface {
	// Name identifies the engine.
	Name() string
	// Placeholder renders the 1-based nth bind parameter ("$1" for
	// postgres, "@p1" for a future mssql).
	Placeholder(n int) string
	// MapError translates a driver error into the library's sentinels;
	// it returns sql.ErrNoRows unchanged.
	MapError(err error) error
}

Dialect is what the library needs from an engine: its name, how it renders the nth bind parameter, and how its driver's errors classify. An engine sub-module implements it; a provider library's dialect satisfies it structurally. Capabilities beyond it, Locker, TryLocker, and ErrorMapper, are separate interfaces a protocol asserts.

type ErrorMapper

type ErrorMapper interface {
	MapError(err error) error
}

ErrorMapper is the capability both sessions expose for errors that arise after a call returns, from rows.Err and Scan, which the session's methods cannot see. A runner type-asserts it on its Session.

type Locker

type Locker interface {
	Lock(ctx context.Context, conn *sql.Conn, name string) error
	Unlock(ctx context.Context, conn *sql.Conn, name string) error
}

Locker is the dialect capability a concurrent-starter protocol takes: a session-scoped exclusive lock on a dedicated connection, identified by name. Locks are named, never numbered (<owner>.<structure>, such as migrate.schema_version or organization.tree), and the dialect maps the name to its engine's lock space. A provider without the capability cannot serialize migrations across processes.

type Session

type Session interface {
	ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error)
	QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error)
	PrepareContext(ctx context.Context, query string) (*sql.Stmt, error)
}

Session is the stdlib method set a runner needs, implemented by *DB and *Tx, so the same statement handle runs against the pool or inside a transaction. Every method classifies a connectivity failure and maps every other driver error through the dialect; the dialect itself is not on the interface, because runners never need it at request time.

type TryLocker added in v0.6.0

type TryLocker interface {
	Locker
	TryLock(ctx context.Context, conn *sql.Conn, name string) (bool, error)
}

TryLocker is the dialect capability a protocol takes when it must not wait for a lock another session holds, or must wait without holding a statement open, as migrate does by polling: a Locker that can also try the same named lock without blocking. TryLock reports whether it took the lock; a lock it takes is the same session-scoped lock Lock takes, in the same name space, and the Locker's Unlock on the same conn releases it. A name held by another session, through Lock or TryLock, reports false and no error.

type Tx

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

Tx is one transaction, a Session whose errors are classified and mapped on every call and on commit.

func (*Tx) Commit

func (t *Tx) Commit() error

Commit commits, mapping the error: the one place a violation deferred to COMMIT can be classified.

func (*Tx) ExecContext

func (t *Tx) ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error)

func (*Tx) MapError

func (t *Tx) MapError(err error) error

MapError classifies a connectivity failure as ErrConnectionFailed and routes every other error through the dialect; nil stays nil.

func (*Tx) PrepareContext

func (t *Tx) PrepareContext(ctx context.Context, query string) (*sql.Stmt, error)

func (*Tx) QueryContext

func (t *Tx) QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error)

func (*Tx) Rollback

func (t *Tx) Rollback() error

Rollback rolls back. sql.ErrTxDone passes through unmapped.

type TxOption

type TxOption func(*sql.TxOptions)

TxOption sets a field of the sql.TxOptions Begin opens with.

func Isolation

func Isolation(level sql.IsolationLevel) TxOption

Isolation requests an isolation level; the guard's serializable claim has a code path through it.

func ReadOnly

func ReadOnly() TxOption

ReadOnly opens the transaction read-only.

Directories

Path Synopsis
Package header reads the declaration header of an authored SQL file: the "--|" declarations that precede the body the engine receives.
Package header reads the declaration header of an authored SQL file: the "--|" declarations that precede the body the engine receives.
Package migrate is schema versioning over authored SQL: ordered sets of migrations, each migration SQL text plus a version, applied against a history table on one pinned connection under the dialect's lock capability.
Package migrate is schema versioning over authored SQL: ordered sets of migrations, each migration SQL text plus a version, applied against a history table on one pinned connection under the dialect's lock capability.
postgres module
Package query runs authored SQL files.
Package query runs authored SQL files.
sqlint module
Package sqltest is the scripted database/sql driver a consumer's hermetic tests run over: its connections consume scripted responses in call order and record every statement, prepare, and transaction call they receive.
Package sqltest is the scripted database/sql driver a consumer's hermetic tests run over: its connections consume scripted responses in call order and record every statement, prepare, and transaction call they receive.

Jump to

Keyboard shortcuts

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