ddl

package
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Oct 11, 2026 License: Apache-2.0 Imports: 12 Imported by: 0

README

ddl — CREATE, ALTER and DROP TABLE

Builds the statements that create, change and remove a library's tables, over the same Col[T] values its queries already use, so a column cannot be created under one name or type and read under another. It is authored DDL, never derived: a builder renders what its author wrote, and when and in what order the statements run stays the application's decision.

Every value in DDL is a literal, because no engine accepts a placeholder there, so a value the dialect cannot write faithfully fails Build rather than rendering something that looks right. And a statement cannot be built until it is complete: a CREATE TABLE with no column and an ALTER TABLE with no action have no Build method, so they do not compile.

Full documentation

The page for this package is docs/ddl/README.md, with runnable snippets — every one of them copied from a test whose expected output go test checks. The documentation index is ../docs/README.md and the repository README is ../README.md.

Documentation

Overview

Package ddl builds the statements that create, change and remove what a library's tables are: CREATE TABLE, ALTER TABLE, DROP TABLE and the rest.

It is authored DDL, never derived. A builder renders the statement its author wrote, over the same sqlb.Col[T] values the library's queries use, so a column cannot be created under one name or type and queried under another; nothing here inspects Go declarations and decides what to emit. Which statements run, in what order and when, stays the application's decision. The scope and its limits are ADR-0003.

The package sits above stmt rather than beside it. A view's body is a stmt.Select and a SQLite trigger's body is stmt statements, and engine clauses reuse stmt's Clause machinery, so ddl imports stmt; it does not live in stmt because stmt imports only render and expr, and DDL needs the per-engine type spelling table in schema. The only thing in this package that is universal to Postgres, MySQL 8, SQLite and ClickHouse is what ADR-0002 admits to a core; every engine construct lives in pg or sqlite.

Every value inside a DDL statement is a literal, because no engine accepts a placeholder in DDL. The builders render through render.NewInlineArgs, which writes each value with the dialect's LiteralQuoting and fails closed on one it cannot write faithfully.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrAddNotNull: a NOT NULL column needs a literal default, because the
	// existing rows get it. Give one with Default, or make it Nullable.
	ErrAddNotNull = errors.New("sqlb/ddl: this dialect cannot add a NOT NULL column without a literal default")
	// ErrAddNonConstantDefault: the existing rows would need the expression
	// evaluated for each, which SQLite refuses. Add it nullable and backfill.
	ErrAddNonConstantDefault = errors.New("sqlb/ddl: this dialect cannot add a column with an expression default")
	// ErrAddConstraint: a key on an added column needs an index built over
	// the existing rows. Add the column, then create a unique index.
	ErrAddConstraint = errors.New("sqlb/ddl: this dialect cannot add a column with a key constraint")
)

The ADD COLUMN refusals for a dialect without render.UnrestrictedAddColumn. Each names the rule SQLite would otherwise report as an error about syntax, measured on SQLite 3.50.3, and says what to write instead.

SQLite refuses a key constraint always, but the other three only when the table has rows: on an empty table it accepts them. Build refuses them unconditionally on purpose, because the empty table is the test database and the table with rows is production, so accepting them is how a migration passes CI and fails on deploy. contrib/driver-sqlite measures both halves.

ColumnPartValues is every part a column definition renders, so a test can check each is written.

View Source
var ErrDefaultOnGenerated = errors.New("sqlb/ddl: a column with a generated value cannot also have a default")

ErrDefaultOnGenerated is returned for a column that has a default and a clause that supplies its value, an identity or a generated expression. Every engine here rejects the pair, so it is refused before the server sees it.

View Source
var ErrDuplicateColumn = errors.New("sqlb/ddl: column defined twice")

ErrDuplicateColumn is returned when a CREATE TABLE defines one column twice.

View Source
var ErrEmptyOneOf = errors.New("sqlb/ddl: OneOf needs at least one value")

ErrEmptyOneOf is returned for OneOf with no values. It would render CHECK (col IN ()), which the empty-IN rule writes as FALSE, and a table that refuses every row is never what was meant.

View Source
var ErrForeignColumn = errors.New("sqlb/ddl: column belongs to another table")

ErrForeignColumn is returned when a statement is given a column declared on another table. CREATE TABLE users with Column(sessions.UserID) would create a column the queries read from a different table, which is the drift this package exists to prevent.

View Source
var ErrInvalidJSON = errors.New("sqlb/ddl: a JSON column's default is not valid JSON")

ErrInvalidJSON is returned for a JSON column's default that is not valid JSON, or a value that cannot be turned into JSON.

View Source
var ErrNoColumnTarget = errors.New("sqlb/ddl: a column clause must name its column")

ErrNoColumnTarget is returned for a clause at PositionColumn that does not say which column it belongs to; it would otherwise vanish.

View Source
var ErrNoDefault = errors.New("sqlb/ddl: clause needs the column to have a default")

ErrNoDefault reports a column clause that needs the column to have a default, on a column that declares none. The case it exists for is SQLite's NOT NULL ON CONFLICT REPLACE, which writes the column's default in place of a NULL: with no default there is nothing to write, and SQLite falls back to ABORT, so the row is refused while the schema says it would be kept.

View Source
var ErrNoDefaultType = errors.New("sqlb/ddl: the column's Go type has no default SQL type; give one with As")

ErrNoDefaultType is returned for a column whose Go type has no unambiguous SQL type, such as json.RawMessage, which is a []byte underneath and would be stored as bytea if its kind were guessed. Give the type with As.

View Source
var ErrNoNotNull = errors.New("sqlb/ddl: clause needs a NOT NULL to attach to")

ErrNoNotNull reports a clause placed at PartAfterNotNull on a column whose definition writes no NOT NULL, because it is nullable or generated. The clause would otherwise render where the engine reads it as something else.

View Source
var ErrNoTypeTable = errors.New("sqlb/ddl: the dialect has no spelling for this portable type; use RawType")

ErrNoTypeTable is returned when a portable type is asked of a dialect that has no type table, which an outside dialect does not until it adds one.

View Source
var ErrNotAColumn = errors.New("sqlb/ddl: a column definition needs a column made by Col")

ErrNotAColumn is returned for a ColumnDef built from an expression that is not a column made by Col, which has no name to define.

View Source
var ErrUnknownColumn = errors.New("sqlb/ddl: clause names a column the table does not define")

ErrUnknownColumn is returned for a clause naming a column the CREATE TABLE does not define.

View Source
var ErrUnknownPosition = errors.New("sqlb/ddl: clause asks for a position this statement does not render")

ErrUnknownPosition is returned for an engine clause aimed at a position the statement does not render; the clause would otherwise vanish.

View Source
var ErrZeroValue = errors.New("sqlb/ddl: built from a zero value rather than its constructor")

ErrZeroValue is returned by Build for a clause or statement written as a struct literal rather than made by its constructor, with a field the constructor always sets left empty: a key with no columns, a foreign key with no column pairs, a statement wrapping nothing. Every exported clause and statement type is a struct, so T{} compiles; before this, several such literals panicked inside Build or rendered PRIMARY KEY () for the server to reject. sqlbtest.ZeroValues builds every exported type's zero value to hold the rule.

PositionValues is every position a CREATE TABLE renders, in order, so a test can check each is written somewhere.

Functions

func RefTableName

func RefTableName(schemaName, name string) string

RefTableName is how a schema.ForeignKey names the table it references: the key schema.Set stores a table under, schema-qualified when it has a schema. An engine clause describing a foreign key uses it so the Set can resolve it.

func WriteAddColumn

func WriteAddColumn[D render.Dialect](b *strings.Builder, a *render.Args, spec ColumnSpec, clauses []ColumnClause[D])

WriteAddColumn renders an added column with its column clauses and the restricted-dialect checks, for an engine's own ADD COLUMN action, so the engine's action and AddColumn refuse the same things.

func WriteColumnWith

func WriteColumnWith[D render.Dialect](b *strings.Builder, a *render.Args, spec ColumnSpec, clauses []ColumnClause[D])

WriteColumnWith renders spec with column clauses that must all name it, for an engine ALTER TABLE action that adds a column with a collation or an identity. A clause naming another column fails Build with ErrUnknownColumn, as it would in CREATE TABLE.

Types

type Action

type Action interface {
	// ActionTable is the table the action's column belongs to, which the
	// statement checks against its own.
	ActionTable() expr.Table
	// Standalone reports whether the action must be a statement of its own.
	// In Postgres's grammar RENAME is a top-level form of ALTER TABLE, not one
	// of the actions a comma may join, so combining it is a syntax error.
	Standalone() bool
	WriteAction(b *strings.Builder, a *render.Args)
}

Action is a universal ALTER TABLE action: one every engine writes the same way. There are three, adding, dropping and renaming a column; everything else an engine can alter is an AlterClause in its own package.

func AddColumn

func AddColumn(c ColumnSpec) Action

AddColumn adds a column. On SQLite the definition is subject to that engine's limits on ADD COLUMN.

func DropColumn

func DropColumn[T any](c expr.Expr[T]) Action

DropColumn drops a column.

func RenameColumn

func RenameColumn[T any](from, to expr.Expr[T]) Action

RenameColumn renames a column. It takes the old column and the new one rather than two strings: the code that used the old name is the code that must change, and the new declaration is the one the queries will use. Both are of one Go type, because a rename does not change what the column stores.

type AlterClause

type AlterClause[D render.Dialect] interface {
	WriteSQL(b *strings.Builder, a *render.Args)
	// Standalone reports whether the action must be a statement of its own,
	// as RENAME TO, SET SCHEMA and ATTACH PARTITION must on Postgres.
	Standalone() bool
	AlterClauseOf(D)
}

AlterClause is an engine's ALTER TABLE action, typed to its dialect: a Postgres SET NOT NULL on a SQLite ALTER TABLE does not compile.

func NewAlterClause

func NewAlterClause[D render.Dialect](render func(a *render.Args) string) AlterClause[D]

NewAlterClause makes an AlterClause for an engine package, for an action that may share a statement with others.

func NewStandaloneAlterClause

func NewStandaloneAlterClause[D render.Dialect](render func(a *render.Args) string) AlterClause[D]

NewStandaloneAlterClause makes an AlterClause for an action that must be a statement of its own: in Postgres's grammar RENAME TO, SET SCHEMA and ATTACH or DETACH PARTITION are top-level forms, not joinable actions.

type AlterTable

type AlterTable[D render.Dialect] struct {
	// contains filtered or unexported fields
}

AlterTable is an ALTER TABLE with no action yet. It has no Build, so an ALTER TABLE that does nothing does not compile.

func NewAlterTable

func NewAlterTable[D render.Dialect](d D, t expr.Table) *AlterTable[D]

NewAlterTable starts an ALTER TABLE of t for d.

func (*AlterTable[D]) Action

func (s *AlterTable[D]) Action(first Action, more ...Action) *AlterTableReady[D]

Action adds universal actions, at least one, and so completes the statement.

func (*AlterTable[D]) Clause

func (s *AlterTable[D]) Clause(first AlterClause[D], more ...AlterClause[D]) *AlterTableReady[D]

Clause adds engine actions typed to this statement's dialect, at least one, and so completes the statement.

type AlterTableReady

type AlterTableReady[D render.Dialect] struct {
	// contains filtered or unexported fields
}

AlterTableReady is an ALTER TABLE with at least one action, the only form with Build.

func (*AlterTableReady[D]) Action

func (s *AlterTableReady[D]) Action(as ...Action) *AlterTableReady[D]

Action adds universal actions.

func (*AlterTableReady[D]) Build

func (s *AlterTableReady[D]) Build() (Script, error)

Build renders the actions in the order added, universal and engine alike. On a dialect with render.CombinedAlter, consecutive actions that may be joined share one statement, applied under one lock; a standalone action, RENAME among them, is always a statement of its own. On any other dialect, SQLite among them, every action is a statement, because a list is a syntax error there. The author's code is the same either way.

func (*AlterTableReady[D]) Clause

func (s *AlterTableReady[D]) Clause(cs ...AlterClause[D]) *AlterTableReady[D]

Clause adds engine actions typed to this statement's dialect.

type ColumnClause

type ColumnClause[D render.Dialect] interface {
	TableClause[D]
	Target() ColumnRef
	Part() ColumnPart
}

ColumnClause is an engine clause that belongs inside one column's definition, an identity or a collation. It goes on the statement with the other clauses, at PositionColumn, and the statement routes it into the column it names; a clause naming a column the statement does not define fails at Build.

func NewColumnClause

func NewColumnClause[D render.Dialect](target ColumnRef, part ColumnPart, render func(a *render.Args) string) ColumnClause[D]

NewColumnClause makes a ColumnClause for an engine package. The render function writes no leading space and renders in bare-column mode, since a generated expression names its table's columns without an alias.

func NewRankedColumnClause

func NewRankedColumnClause[D render.Dialect](rank OptionRank, target ColumnRef, part ColumnPart, render func(a *render.Args) string) ColumnClause[D]

NewRankedColumnClause is NewColumnClause for a clause whose engine fixes its order among the clauses sharing its part. Postgres is the case that needs it: STORAGE, COMPRESSION and COLLATE all render straight after a column's type, and its grammar takes them only in that order — measured on 17, every other order is a syntax error. The statement sorts ranked column clauses exactly as it sorts table options; see OptionRank for why it sorts rather than refusing an order it could fix.

type ColumnDef

type ColumnDef[T any] struct {
	// contains filtered or unexported fields
}

ColumnDef defines a column from the query column it will be read through, so the name and the Go type are written once, in the Col declaration.

Nullability follows the Go type: a Col[*time.Time] is NULL, a Col[time.Time] is NOT NULL. Nullable and NotNull override it for the one legitimate disagreement, adding a column nullable, backfilling it, then tightening it. The SQL type is inferred where the Go type makes it unambiguous and given with As everywhere else.

Methods take and return a value, so a definition shared between two statements cannot be changed through either.

func Column

func Column[T any](c expr.Expr[T]) ColumnDef[T]

Column starts a definition of c.

func (ColumnDef[T]) As

func (c ColumnDef[T]) As(t SQLType[T]) ColumnDef[T]

As gives the column's SQL type. It is typed to the column's Go type, so a type for another Go type does not compile.

func (ColumnDef[T]) ColumnFacts

func (c ColumnDef[T]) ColumnFacts() ColumnFacts

ColumnFacts reports the definition's nullability and default.

func (ColumnDef[T]) ColumnName

func (c ColumnDef[T]) ColumnName() string

ColumnName is the column's bare name.

func (ColumnDef[T]) ColumnTable

func (c ColumnDef[T]) ColumnTable() expr.Table

ColumnTable is the table the column was declared on.

func (ColumnDef[T]) Default

func (c ColumnDef[T]) Default(v T) ColumnDef[T]

Default gives the column a default of its own Go type, written as a literal by the dialect.

On a column whose type stores JSON, Portable(schema.TypeJSON) or RawJSONType, the value is JSON text: a json.RawMessage or []byte as is, a string as is, a driver.Valuer such as pg.JSON[T] through Value, anything else through json.Marshal. It is checked to be valid JSON, so invalid JSON fails Build with ErrInvalidJSON on every engine rather than only where the server happens to check, and written as ('<text>'): MySQL refuses a JSON column's plain literal default and accepts only that expression form, and Postgres and SQLite accept it too, measured on all three. Until 2026-09-22 a json.RawMessage default was written as a byte-string literal, because it is a []byte, which a jsonb column refuses.

func (ColumnDef[T]) DefaultExpr

func (c ColumnDef[T]) DefaultExpr(e expr.Expr[T]) ColumnDef[T]

DefaultExpr gives the column a default computed by an expression of its own type, now() or gen_random_uuid() or nextval(...). It is written in parentheses, which SQLite requires of an expression default and Postgres accepts.

func (ColumnDef[T]) DescribeColumn

func (c ColumnDef[T]) DescribeColumn(d render.Dialect) (schema.Column, error)

DescribeColumn describes the definition: its name, its type as a portable schema.Type when it has one and as the dialect's spelling in Raw when it does not, its nullability and its default as the dialect writes it.

func (ColumnDef[T]) NotNull

func (c ColumnDef[T]) NotNull() ColumnDef[T]

NotNull refuses NULL whatever the Go type says.

func (ColumnDef[T]) Nullable

func (c ColumnDef[T]) Nullable() ColumnDef[T]

Nullable allows NULL whatever the Go type says.

func (ColumnDef[T]) WriteColumn

func (c ColumnDef[T]) WriteColumn(b *strings.Builder, a *render.Args, x ColumnExtras)

WriteColumn renders the definition. A failure, a type that cannot be spelled or a default that cannot be written, is recorded on a and fails Build.

type ColumnDescriber

type ColumnDescriber interface {
	DescribeColumn(d render.Dialect) (schema.Column, error)
}

ColumnDescriber is implemented by a ColumnSpec that can describe itself as a schema.Column for a dialect. ColumnDef does; a ColumnSpec written outside this package that does not is described by name alone.

type ColumnExtras

type ColumnExtras struct {
	// AfterType is written straight after the type.
	AfterType []string
	// Constraints is written after the default.
	Constraints []string
	// ValueSource is set when a constraint supplies the value, so a default
	// is refused.
	ValueSource bool
	// Computed is set for a generated column: no default and no NOT NULL.
	Computed bool
	// AfterNotNull is written immediately after NOT NULL, and is refused when
	// the definition writes none; see PartAfterNotNull.
	AfterNotNull []string
	// RequiresDefault is set when a clause needs the column to have a
	// default, so a definition without one is refused with ErrNoDefault; see
	// DefaultRequirer. Exported, like the rest of ColumnExtras, so a
	// ColumnSpec written outside this package can honour it.
	RequiresDefault bool
}

ColumnExtras is what the statement's column clauses add to one column's definition, already rendered, which a statement passes to WriteColumn. It is exported so a ColumnSpec written outside this package can honour it.

type ColumnFacts

type ColumnFacts struct {
	NotNull bool
	Default DefaultKind
}

ColumnFacts is what a definition says about NULL and its default, resolved against its Go type. A ColumnSpec that implements FactsReporter gets its ADD COLUMN checked on a restricted dialect; one that does not is added as it renders.

type ColumnNamer

type ColumnNamer interface {
	NamedColumns() []ColumnRef
}

ColumnNamer is implemented by a table clause that names columns of the statement's own table, a primary key or a unique constraint. The statement checks each one at Build: a clause naming a column the table does not define fails with ErrUnknownColumn instead of reaching the server as an error about SQL the caller never wrote, and one naming another table's column fails with ErrForeignColumn.

type ColumnPart

type ColumnPart int

ColumnPart is where inside one column's definition a column clause renders. The grammar differs by position rather than by engine, so the positions are universal and the clauses that fill them are the engine's.

const (
	// PartAfterType follows the type immediately, where COLLATE must go.
	PartAfterType ColumnPart = iota + 1
	// PartConstraint follows the default: a column-level key, a reference.
	PartConstraint
	// PartValueSource is a constraint that supplies the column's value, an
	// identity or AUTO_INCREMENT: the column cannot also have a default.
	PartValueSource
	// PartComputed is a generated expression: the column cannot have a
	// default, and NOT NULL is not written, because the application never
	// writes the column and the expression decides whether it is NULL.
	PartComputed
	// PartAfterNotNull is written immediately after NOT NULL, which is the
	// only position SQLite accepts a conflict clause on it in: NOT NULL ON
	// CONFLICT REPLACE. It is a part of its own rather than PartAfterType
	// because the clause modifies the NOT NULL rather than the type, and a
	// definition that does not write NOT NULL has nothing for it to attach
	// to, which fails Build with ErrNoNotNull rather than rendering a clause
	// that reads as a constraint of its own.
	PartAfterNotNull
)

The parts of a column definition a column clause can fill.

type ColumnRef

type ColumnRef interface {
	Name() string
	Table() expr.Table
}

ColumnRef is a column a clause names, whatever its Go type. Every expr.Expr made by Col satisfies it, which is what lets PRIMARY KEY (a, b) take columns of two Go types in one call; an expression that is not a column has no name and is refused at Build with ErrNotAColumn.

type ColumnSpec

type ColumnSpec interface {
	// ColumnName is the column's bare name.
	ColumnName() string
	// ColumnTable is the table the column was declared on, which a statement
	// checks against its own.
	ColumnTable() expr.Table
	// WriteColumn renders the definition: name, type, nullability, default,
	// with what the statement's column clauses add to it.
	WriteColumn(b *strings.Builder, a *render.Args, x ColumnExtras)
}

ColumnSpec is a column definition whatever its Go type, which is what a statement's Column and AddColumn take. ColumnDef[T] is the one implementation; the interface exists because a statement holds columns of many types in one list.

type Constraint

type Constraint interface {
	WriteConstraint(b *strings.Builder, a *render.Args)
}

Constraint is a table constraint every engine writes the same way. Only a named CHECK qualifies: primary keys, unique constraints and foreign keys differ between engines, ClickHouse has none of the three, and so by ADR-0002 they are engine clauses in pg and sqlite rather than core constructs.

func Check

Check is a table CHECK constraint. The predicate is an ordinary expression: its columns are written by their bare names and its values as literals, because inside CREATE TABLE there is no alias and no parameter.

func OneOf

func OneOf[T any](c TableConstraint, col expr.Expr[T], values ...T) Constraint

OneOf constrains a column to a closed set of values of its own Go type, as CHECK (col IN (...)). It is the portable enum: the same constants that make a Postgres enum type make this on SQLite, and on Postgres too when a type is more than the schema wants.

type CreateTable

type CreateTable[D render.Dialect] struct {
	// contains filtered or unexported fields
}

CreateTable is a CREATE TABLE with no column yet. It has no Build: a table with no column is rejected by SQLite and useless on Postgres, so the statement is not complete until Column has been called, and the incomplete form cannot be built. That is a type, not a check.

func NewCreateTable

func NewCreateTable[D render.Dialect](d D, t expr.Table) *CreateTable[D]

NewCreateTable starts a CREATE TABLE of t for d. The dialect is in the type, so an engine's table clause for another engine does not compile; an engine package spells this pg.CreateTable(t) or sqlite.CreateTable(t).

func (*CreateTable[D]) Clause

func (c *CreateTable[D]) Clause(cs ...TableClause[D]) *CreateTable[D]

Clause adds engine clauses typed to this statement's dialect.

func (*CreateTable[D]) Column

func (c *CreateTable[D]) Column(first ColumnSpec, more ...ColumnSpec) *CreateTableReady[D]

Column adds the first columns, and so completes the statement. It takes at least one, so an empty call does not compile.

func (*CreateTable[D]) Constraint

func (c *CreateTable[D]) Constraint(cs ...Constraint) *CreateTable[D]

Constraint adds universal table constraints.

func (*CreateTable[D]) IfNotExists

func (c *CreateTable[D]) IfNotExists() *CreateTable[D]

IfNotExists makes the statement do nothing when the table exists. It checks the name only, not the shape: a table that exists with other columns is left as it is.

type CreateTableReady

type CreateTableReady[D render.Dialect] struct {
	// contains filtered or unexported fields
}

CreateTableReady is a CREATE TABLE with at least one column. It is the only form with Build.

func (*CreateTableReady[D]) Build

func (c *CreateTableReady[D]) Build() (Script, error)

Build renders the statement. Every value in it is a literal, so the Script carries no arguments.

func (*CreateTableReady[D]) Clause

func (c *CreateTableReady[D]) Clause(cs ...TableClause[D]) *CreateTableReady[D]

Clause adds engine clauses typed to this statement's dialect.

func (*CreateTableReady[D]) Column

func (c *CreateTableReady[D]) Column(cols ...ColumnSpec) *CreateTableReady[D]

Column adds columns.

func (*CreateTableReady[D]) Constraint

func (c *CreateTableReady[D]) Constraint(cs ...Constraint) *CreateTableReady[D]

Constraint adds universal table constraints.

func (*CreateTableReady[D]) Describe

func (c *CreateTableReady[D]) Describe() (schema.Table, error)

Describe is the schema.Table this statement creates, derived from the statement rather than declared beside it, so the description a migration planner orders and a drift check compares cannot disagree with the DDL that made the table. Columns come from their definitions, and the keys and references from each engine clause that implements Describer; a CHECK and an engine clause that describes nothing are not in the description, because schema.Table has no place for them.

A statement Build refuses is not described: the error is Build's. Without that, a key on a column the table does not define, or a column declared on another table, described cleanly and was ordered by migrate.Install and compared by the vet as if the DDL existed.

func (*CreateTableReady[D]) IfNotExists

func (c *CreateTableReady[D]) IfNotExists() *CreateTableReady[D]

IfNotExists makes the statement do nothing when the table exists.

func (*CreateTableReady[D]) Table

func (c *CreateTableReady[D]) Table() expr.Table

Table is the table the statement creates, for a builder that composes a CREATE TABLE into a larger script, as SQLite's table rebuild does.

type DefaultKind

type DefaultKind int

DefaultKind is what a column definition's default is, which a dialect with a restricted ADD COLUMN must know to refuse the right ones.

const (
	DefaultNone DefaultKind = iota
	DefaultLiteral
	DefaultExpression
)

The kinds of default.

type DefaultRequirer

type DefaultRequirer interface {
	RequiresDefault() bool
}

DefaultRequirer is implemented by a column clause that only means what it says when the column has a default.

Why this is refused at Build rather than documented

SQLite's NOT NULL ON CONFLICT REPLACE on a column with no default is accepted by CREATE TABLE and behaves as ABORT, measured on 3.50.3. The failure therefore does not happen when the schema is installed, nor in any test that never writes a NULL to that column; it happens the first time production does, as "NOT NULL constraint failed", from a table whose declaration promised the opposite. A comment warning about that is read by the person who already knows. Build is the one place the mistake can be seen before it ships.

Why the check lives in ddl and the decision in the engine

Only the column definition knows whether a default was declared, and the clause cannot see it — the same reason ErrNoNotNull lives here. So the engine clause says whether it needs a default, which is a fact about its own semantics, and ddl, which holds the definition, refuses the mismatch. ddl learns nothing about SQLite, and a clause from another engine with the same need reuses the check by implementing this interface.

type Describer

type Describer interface {
	DescribeInto(t *schema.Table)
}

Describer is implemented by an engine clause that adds to a table's description: a primary key sets schema.Table.PrimaryKey, a unique key adds a unique schema.Index, a foreign key a schema.ForeignKey, an auto-increment key its column as the key. A clause that describes nothing, UNLOGGED or STRICT, does not implement it, and Describe leaves it out.

type DropClause

type DropClause[D render.Dialect] interface {
	WriteSQL(b *strings.Builder, a *render.Args)
	DropClauseOf(D)
}

DropClause is an engine's option after DROP TABLE's table name, typed to its dialect: CASCADE is Postgres's, and SQLite has none.

func NewDropClause

func NewDropClause[D render.Dialect](render func(a *render.Args) string) DropClause[D]

NewDropClause makes a DropClause for an engine package.

type DropTable

type DropTable[D render.Dialect] struct {
	// contains filtered or unexported fields
}

DropTable is a DROP TABLE of one or more tables. It is complete from its constructor, which takes at least one table, so there is no incomplete form.

It renders one statement per table on every engine. SQLite accepts one table per DROP TABLE and rejects a list with a syntax error, measured; one per table is valid everywhere, so the tables drop in the order given and the author orders them.

func NewDropTable

func NewDropTable[D render.Dialect](d D, first expr.Table, more ...expr.Table) *DropTable[D]

NewDropTable starts a DROP TABLE for d.

func (*DropTable[D]) Build

func (s *DropTable[D]) Build() (Script, error)

Build renders one statement per table.

func (*DropTable[D]) Clause

func (s *DropTable[D]) Clause(cs ...DropClause[D]) *DropTable[D]

Clause adds engine options typed to this statement's dialect, written after every table's name.

func (*DropTable[D]) IfExists

func (s *DropTable[D]) IfExists() *DropTable[D]

IfExists makes each statement do nothing when its table does not exist.

type FactsReporter

type FactsReporter interface {
	ColumnFacts() ColumnFacts
}

FactsReporter is implemented by a ColumnSpec that can say its ColumnFacts.

type JSONStorer

type JSONStorer interface {
	StoresJSON() bool
}

JSONStorer is an SQLType that stores JSON, which changes how a default is written; see ColumnDef.Default. Portable(schema.TypeJSON) and RawJSONType are the two; a type of an engine package implements it to join them.

type ListedOption

type ListedOption interface {
	ListedOption()
}

ListedOption is implemented by a CREATE TABLE option clause that belongs to a comma-separated list. SQLite writes its table options that way, ") STRICT, WITHOUT ROWID", where Postgres separates its options with spaces; the statement joins consecutive listed options with commas.

type OptionRank

type OptionRank int

OptionRank is where an engine's grammar places one clause among the clauses sharing its position: an option after the closing parenthesis of CREATE TABLE, or a column clause within one part of a column's definition. Lower renders first. Zero means unranked.

It covers column clauses as well because the same bug existed one level down, found by the test written for the table options: passing Postgres's STORAGE, COMPRESSION and COLLATE in reverse rendered them in reverse, and Postgres answers every order but that one with a syntax error. One mechanism for both, rather than a second, because the rule is the same — the engine knows the order, so the statement writes it.

Why this exists

Every clause at PositionOption used to render in the order the caller passed it, and two engines fix that order in their grammar. Measured on 2026-09-24: Postgres 17 takes INHERITS, PARTITION BY, USING, WITH, ON COMMIT, TABLESPACE in exactly that order and answers `syntax error at or near "USING"` to `WITH (fillfactor=70) USING heap`; MySQL 8.4 takes its table options in any order but PARTITION BY only last, answering ER_PARSE_ERROR to `PARTITION BY HASH(id) PARTITIONS 2 ENGINE=InnoDB`. So a caller who passed two correct clauses in the wrong order got a clean Build and a server error about SQL they never wrote — the failure this module exists to prevent. SQLite's STRICT and WITHOUT ROWID are accepted either way round, so SQLite ranks nothing.

Why sort, rather than refuse or leave it

Three answers were considered. Leaving it — documented, and pinned by a test that passes the options in the right order — is honest but keeps the wrong order writable, and a test in the right order proves nothing about a caller in the wrong one. Refusing at Build would name the mistake, but it makes the caller reproduce an order the library already knows: there is exactly one legal order, so there is no choice to get wrong, only busywork to demand. Refusing is right when the caller holds information the library does not; here the library holds all of it. So the statement sorts.

Why the rank is the engine's and the sort is ddl's

Which option comes first is a fact about one engine's grammar, so the ranks live in pg and mysql, each in one table beside its option constructors. ddl knows only that options may carry a rank and that ranked options are written in rank order. That keeps ddl free of any engine's grammar, which is the rule every other layer here follows.

What happens to an unranked option

It stays exactly where the caller put it, and only the ranked options move, each into a position a ranked option already occupied. The statement reorders only what it understands: an option from outside this module, made with NewTableClause, may be anything, and no default place for it is right in every case — first breaks a raw TABLESPACE on Postgres, last breaks a raw table option before MySQL's partitioning. Keeping the caller's placement is the one choice that never makes a correct statement wrong. Every option the engine packages ship is ranked, and a test in each fails on one that is not, so the unranked case is only ever a caller's own clause.

type OptionRanker

type OptionRanker interface {
	OptionRank() OptionRank
}

OptionRanker is implemented by an option that knows where its engine's grammar places it. A clause made by NewOptionClause or NewOptionClauseOn implements it; a clause returning zero is treated as unranked.

type Position

type Position int

Position is where in a CREATE TABLE an engine clause renders.

DDL has its own positions rather than stmt's slots. A slot is an int from stmt's own iota, and every one of them is walked by stmt's completeness tests; a DDL position minted from the same space could collide with one silently. A DDL statement renders each of these, and refuses a clause for any other, so a clause cannot vanish from the statement.

const (
	// PositionModifier is between CREATE and TABLE: UNLOGGED, TEMP.
	PositionModifier Position = iota + 1
	// PositionElement is inside the parentheses, after the columns and the
	// universal constraints: a primary key, a unique or foreign key, EXCLUDE.
	PositionElement
	// PositionOption is after the closing parenthesis: PARTITION BY, STRICT,
	// WITHOUT ROWID.
	PositionOption
	// PositionColumn is inside one column's definition. Only a ColumnClause,
	// which names its column, may use it.
	PositionColumn
)

The positions a CREATE TABLE renders.

type SQLType

type SQLType[T any] interface {
	// Spelling is the type as d writes it.
	Spelling(d render.Dialect) (string, error)
	// TypeOf is never called; it types the SQLType to T.
	TypeOf(T)
}

SQLType is a column's SQL type, typed to the Go type the column stores.

The type parameter is the point. ColumnDef[T].As takes an SQLType[T], so a Postgres varchar can be given to a string column and not to an int64 one, and an enum declared over a Go named type can be given only to a column of that type. TypeOf is the marker that makes the parameter bite: an interface whose parameter appears in no method would be satisfied by every type for every T.

func Portable

func Portable[T any](t schema.Type) SQLType[T]

Portable is one of schema's portable types, spelled by each dialect from the table contrib/migrate and contrib/vet already read, so a column the vet accepts is one this would create. It is how a string column becomes a uuid or a timestamp rather than text, and how a json.RawMessage becomes jsonb.

The Go type is checked against the portable type when the statement is built: Portable[int64](schema.TypeText) fails with schema.ErrTypeMismatch.

func RawJSONType

func RawJSONType[T any](spelling string) SQLType[T]

RawJSONType is RawType for a column that stores JSON under a spelling of its own, jsonb or json, for a Go type the portable JSON type does not take, such as pg.JSON[T]. Its default is written as JSON, as Portable(TypeJSON)'s is.

func RawType

func RawType[T any](spelling string) SQLType[T]

RawType is a column type written exactly as given, for a type no portable type covers: citext, an extension's type, a sized varchar. It is the escape hatch, and it is still typed to the column's Go type, so it cannot be handed to a column of another type. The spelling is written into the statement as is; it is the author's text, never a value from a request.

type Script

type Script []Statement

Script is the statements one builder produced, in the order they run.

Most builders produce one. SQLite accepts one action per ALTER TABLE, so an ALTER TABLE with three actions is three statements there and one on Postgres, and the author writes the same code either way; a SQLite table rebuild is eleven. Every builder returns a Script so a caller never has to know which ones split.

func (Script) SQL

func (s Script) SQL() []string

SQL returns each statement's text in order, for a caller that executes them itself or writes them to a migration file.

func (Script) Transactional

func (s Script) Transactional() bool

Transactional reports whether every statement may run inside one transaction, which is the question a runner asks before wrapping a script in one.

type Statement

type Statement struct {
	SQL string

	// Transactional is false for a statement Postgres refuses inside a
	// transaction block: CREATE INDEX CONCURRENTLY, DROP INDEX CONCURRENTLY and
	// DETACH PARTITION ... CONCURRENTLY, each measured against Postgres 17. A
	// runner must execute such a statement on its own, never in a batch.
	Transactional bool

	// OutsideTransaction is true for a statement that is a silent no-op inside
	// a transaction. SQLite's PRAGMA foreign_keys is the one known case: set to
	// OFF after BEGIN it still reads 1, with no error, which is how a table
	// rebuild wrapped in one transaction quietly loses its guard. A runner must
	// execute it before opening any transaction.
	OutsideTransaction bool

	// MustReturnNoRows is true for a check rather than a change: a statement
	// that reports a problem by returning rows instead of failing. SQLite's
	// PRAGMA foreign_key_check is the case, at the end of a table rebuild; a
	// runner must query it and fail on any row, because executing it and
	// ignoring the result reports success whatever it found.
	MustReturnNoRows bool

	// SkipIf is a query, and a runner that finds it returns a row skips the
	// statement. It is how an engine without IF NOT EXISTS for a statement
	// still writes a repeatable one: MySQL's CREATE INDEX has none, so its
	// IfNotExists sets SkipIf to a query for the index in the catalogue. A
	// runner that ignores the field runs the statement anyway, and a second
	// run fails loudly with a duplicate, rather than doing something else;
	// contrib/migrate's Execute honours it. Empty means always run.
	SkipIf string
}

Statement is one DDL statement. It carries no arguments: every value inside it was written as a literal, so it is passed to a driver as text alone.

type TableChecker

type TableChecker interface {
	CheckTable(t schema.Table) error
}

TableChecker is a table clause whose validity depends on the rest of the table: MySQL's PARTITION BY needs every primary and unique key to include the partition columns, and no foreign key at all. CREATE TABLE describes itself, as Describe does, and hands the description to each such clause; an error fails Build. A clause that checks only itself does not need it.

type TableClause

type TableClause[D render.Dialect] interface {
	Position() Position
	WriteSQL(b *strings.Builder, a *render.Args)
	TableClauseOf(D)
}

TableClause is an engine's clause for CREATE TABLE, typed to its dialect: a Postgres clause on a SQLite CREATE TABLE does not compile, exactly as a Postgres clause on a SQLite SELECT does not. TableClauseOf is the marker that makes the dialect parameter bite.

func NewOptionClause

func NewOptionClause[D render.Dialect](rank OptionRank, render func(a *render.Args) string) TableClause[D]

NewOptionClause makes a TableClause at PositionOption that renders at rank in the order its engine's grammar requires, whatever order the caller passed it in. See OptionRank for why the statement sorts rather than refusing an order it could fix.

func NewOptionClauseOn

func NewOptionClauseOn[D render.Dialect](rank OptionRank, cols []ColumnRef, render func(a *render.Args) string) TableClause[D]

NewOptionClauseOn is NewOptionClause for an option that names columns of the statement's own table, as PARTITION BY does, so the statement both checks each column exists and renders the option at its rank.

func NewTableClause

func NewTableClause[D render.Dialect](pos Position, render func(a *render.Args) string) TableClause[D]

NewTableClause makes a TableClause for an engine package. The render function writes no leading space; the statement supplies separators.

A clause made here at PositionOption carries no OptionRank, so it renders exactly where the caller placed it. An engine package makes its options with NewOptionClause instead, which is what lets the statement put them in the order the engine's grammar requires.

func NewTableClauseOn

func NewTableClauseOn[D render.Dialect](pos Position, cols []ColumnRef, render func(a *render.Args) string) TableClause[D]

NewTableClauseOn makes a TableClause that names columns of the statement's own table, so the statement checks each exists.

type TableConstraint

type TableConstraint struct {
	Table expr.Table
	Name  string
}

TableConstraint is a named constraint of one table, declared once and used everywhere the constraint is named: to create it, validate it, rename it, comment on it, drop it, and target it in an upsert. A constraint's name was a string at each of those sites until 2026-09-22, so a typo in the one that drops it was an error from the server at deploy time, and naming it on the wrong table was the same. Declared once, the name cannot drift, and every statement that takes it checks it is on the statement's own table:

var UsersEmailKey = ddl.TableConstraint{Table: Users, Name: "users_email_key"}

Both fields are required; a zero Name or Table fails Build with ErrZeroValue. A constraint that should stay unnamed, a column's inline REFERENCES say, is written with that construct instead.

func (TableConstraint) ClauseTable

func (c TableConstraint) ClauseTable() expr.Table

ClauseTable is the table the constraint belongs to, which CREATE TABLE and ALTER TABLE check against their own: a constraint of one table added to another fails with ErrForeignColumn.

func (TableConstraint) Valid

func (c TableConstraint) Valid() bool

Valid reports whether both fields are set.

type TableNamer

type TableNamer interface {
	ClauseTable() expr.Table
}

TableNamer is implemented by an engine ALTER TABLE action that belongs to one table, one that adds a column declared on it, so the statement can refuse it on any other with ErrForeignColumn. An action that also names columns implements ColumnNamer, and each is checked the same way.

Jump to

Keyboard shortcuts

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