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

schema

package
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Nov 1, 2026 License: MIT Imports: 9 Imported by: 0

README

SQL schema declarations

Use arkoren.dev/foundation/query/schema to declare SQL table changes without executing them during application construction. Each immutable Operation publishes argument-free statements that compile for the selected query dialect. The package uses only the standard library and its parent query package.

Declare and compile a table

Import arkoren.dev/foundation/query and arkoren.dev/foundation/query/schema. Declare columns in callbacks, then compile the statements for PostgreSQL:

op := schema.Create("users", func(table *schema.Table) {
	table.ID()
	table.String("email", 255).Unique()
	table.Integer("visits").Default(0)
})
err := op.Err()
var statements []query.Statement
for _, definition := range op.Statements() {
	statement, err := definition.BuildFor(query.Postgres)
	statements = append(statements, statement)
}

On success, statements contains a CREATE TABLE and a separate CREATE UNIQUE INDEX. No database connection is opened and no DDL is executed. Pass the published statements to your database or migration execution boundary; SQL text is available through Statement.SQL().

Construction and ownership

Create, CreateIfNotExists, and Alter validate the table name before calling any declaration callback. Supplied callbacks run directly once, synchronously, in order on one draft. Nil callback entries and application panics propagate. An expanded nil or empty callback slice supplies no callbacks; missing columns or actions remain ordinary construction errors.

Publication detaches the root, actions, children, and owned name slices before lazy rendering. Retained drafts cannot modify published operations. Drafts are not concurrency-safe; published operations support concurrent compilation.

The first invalid setting remains an error even after replacement. Literal defaults accept strings, integers, finite floats, booleans, and nil (SQL NULL), including named scalar types. Unsupported types, nonfinite floats, and strings containing NUL fail construction. Column and index identifiers, including generated names for DropForeign, must fit the 63-byte limit.

Dialect and execution limits

Construction failures match query.ErrInvalidQuery. Dialect restrictions are checked by BuildFor and also match query.ErrUnsupported. On a failed construction, Statements() returns one failing statement, not partial DDL. Compile statements before execution when you need to detect dialect failures before any changes are applied. Transactional DDL support remains database-specific; Dialect.TransactionalDDL() reports the package's guarantee.

SQLite cannot change existing columns or add/drop foreign-key constraints. An added NOT NULL column needs a non-NULL literal default; Default(nil) does not satisfy that requirement. SQLite REFERENCES targets must be unqualified, even when the referencing table has a schema. ALTER cannot mark multiple added columns as separate primary keys; CREATE still supports composite primary keys.

Empty ReferentialAction clears an explicit delete/update action and leaves the database default. Raw column types, defaults, and USING expressions remain trusted SQL: never interpolate user input. Literal escaping assumes PostgreSQL standard_conforming_strings=on and MySQL without NO_BACKSLASH_ESCAPES.

See the schema API documentation for type mappings, callback examples, and individual operation contracts.

Documentation

Overview

Package schema builds DDL: creating, altering, renaming, and dropping tables, with their columns, keys, indexes, and foreign keys, for the dialects of arkoren.dev/foundation/query.

Operations

Create, CreateIfNotExists, Alter, Rename, Drop, and DropIfExists return an Operation, an immutable value that compiles to one or more argument-free statements. Create, CreateIfNotExists, and Alter accept zero or more callbacks that define the table on the same *Table:

op := schema.Create("users", func(table *schema.Table) {
	table.ID()
	table.ForeignID("tenant_id", "tenants").OnDelete(schema.Cascade)
	table.String("email", 255)
	table.Text("bio").Nullable()
	table.Timestamps()
	table.Unique("tenant_id", "email")
	table.Index("bio")
})
if err := op.Err(); err != nil {
	return err
}
for _, statement := range op.Statements() {
	compiled, err := statement.BuildFor(query.Postgres)
	if err != nil {
		return err
	}
	fmt.Println(compiled.SQL())
}

Supplied callbacks run directly, once, synchronously in argument order, after the table name is validated. Invalid names return an error without invoking callbacks. A nil callback or application panic propagates and stops the sequence; omit callbacks instead of supplying nil to request no declarations. An expanded nil or empty callback slice supplies no callbacks.

The combined declaration is copied and validated after all callbacks. Omitting them or leaving a CREATE without columns or an ALTER without actions is an ordinary construction error. Retaining and changing the Table or its Column, Change, or Foreign children afterwards cannot change the operation. Builders are not safe for concurrent mutation. Arbitrary Default values are not recursively copied. Literal defaults accept scalar strings, integers, finite floats, booleans, and nil. Unsupported types, nonfinite floats, and NUL-bearing strings retain construction errors immediately, even if a later callback replaces the default.

Column methods return a *Column for modifiers; the first problem (an unknown column in an index, a modifier used where it does not apply, an alter-only method in Create, an empty declaration, an invalid name) is kept and reported by Operation.Err. Every statement of an operation with an error fails with that error, so it never runs partially. Dialect limits, such as SQLite changing a column, surface when a statement is compiled and match query.ErrUnsupported; every error also matches query.ErrInvalidQuery.

Statements

Operation.Statements returns one query.Statementer per statement, and the count never depends on the dialect. Create emits a CREATE TABLE with the columns in written order and the primary key and foreign keys inline, then one CREATE [UNIQUE] INDEX per index. Alter emits one statement per call, in written order. Unique keys are always unique indexes, never inline constraints, so Table.DropUnique is a DROP INDEX on every dialect and a unique column can be added on SQLite.

Columns are NOT NULL unless Column.Nullable is called. Identifiers must be unquoted SQL names of at most 63 bytes. Generated names are <table>_<columns>_key (unique index), _index (plain index), <table>_<column>_fkey, and <table>_pkey; a schema-qualified table contributes its schema, as in s_t_col_key, and a generated name longer than 63 bytes is an error. Table.DropIndex, Table.DropUnique, and Table.DropForeign take the same columns as their create and derive the name.

Changing columns

Table.Change returns a *Change that changes only what is called: a type, nullability, and a default each become their own ALTER COLUMN clause on Postgres and ANSI. MySQL changes a default alone with ALTER COLUMN, but any type or nullability change is MODIFY COLUMN, which restates the whole column: it needs a type and Nullable or NotNull together, and drops a default that is not restated. SQLite cannot change columns.

Safety

DDL cannot bind arguments, so defaults are rendered into the SQL: strings have single quotes doubled and NUL rejected, and MySQL backslashes are doubled. That is correct only under standard server modes (standard_conforming_strings=on on PostgreSQL, no NO_BACKSLASH_ESCAPES on MySQL). Table.Column, Column.DefaultRaw, Change.Type, and Change.Using are verbatim: never pass user input. Statements go through query.Raw, so a "?" outside quotes in a raw expression is a placeholder and fails; a "?" inside a quoted default is fine.

MySQL parenthesizes defaults on text, blob, and json columns (MySQL 8.0.13 or later) and has no time zone aware timestamp: TimestampTZ renders datetime(6), so store UTC. SQLite stores SmallInteger as INTEGER and Char as TEXT, and compares timestamps as text. IF NOT EXISTS is unavailable on ANSI for tables and on ANSI and MySQL for indexes. Use query.Dialect.TransactionalDDL to learn whether DDL rolls back. SQLite REFERENCES targets must be unqualified and resolve in the referencing table's schema; qualified targets fail compilation with query.ErrUnsupported.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Change

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

Change redefines an existing column with Table.Change. It changes only what is called: a type method, Change.Nullable or Change.NotNull, and a default method. Calling nothing is an error. When an aspect is called twice, the last call wins.

Postgres and ANSI emit one ALTER COLUMN clause per aspect set. MySQL emits ALTER COLUMN SET/DROP DEFAULT for a default-only change; a type or nullability change emits MODIFY COLUMN, which restates the whole column, so it needs both a type and Nullable or NotNull, and drops a default that is not restated. SQLite cannot change a column. Changes remain mutable only as declaration drafts; later edits cannot modify a published Operation. Individual invalid settings remain errors after edits.

Example:

op := schema.Alter("users", func(table *schema.Table) {
	change := table.Change("name")
	change.String(160).NotNull().Default("guest")
})
if err := op.Err(); err != nil {
	return err
}

func (*Change) BigInteger

func (change *Change) BigInteger() *Change

BigInteger changes the type to a 64-bit integer. It returns the same draft, without selecting nullability or a default. MySQL needs explicit nullability; SQLite cannot compile a type change.

Example:

op := schema.Alter("users", func(table *schema.Table) {
	table.Change("visits").BigInteger().NotNull()
})
if err := op.Err(); err != nil {
	return err
}

func (*Change) Binary

func (change *Change) Binary() *Change

Binary changes the type to a binary large object. It returns this draft, leaving nullability and defaults to separate methods. The database determines whether existing values can convert to its binary type.

Example:

op := schema.Alter("files", func(table *schema.Table) {
	table.Change("contents").Binary().NotNull()
})
if err := op.Err(); err != nil {
	return err
}

func (*Change) Boolean

func (change *Change) Boolean() *Change

Boolean changes the type to boolean. It returns the same draft; conversion of existing data is database-owned. Other aspects remain unchanged except for MySQL's whole-column restatement.

Example:

op := schema.Alter("users", func(table *schema.Table) {
	table.Change("active").Boolean().NotNull().Default(true)
})
if err := op.Err(); err != nil {
	return err
}

func (*Change) Char

func (change *Change) Char(length int) *Change

Char changes the type to a fixed-length character type of length characters. Length must be positive. It returns the same draft and does not change the requested nullability or default; SQLite cannot compile column changes.

Example:

op := schema.Alter("users", func(table *schema.Table) {
	table.Change("country").Char(2).NotNull()
})
if err := op.Err(); err != nil {
	return err
}

func (*Change) Date

func (change *Change) Date() *Change

Date changes the type to a calendar date. It returns the same draft and does not supply a default or conversion policy. SQLite cannot compile the change; MySQL also needs explicit nullability.

Example:

op := schema.Alter("users", func(table *schema.Table) {
	table.Change("birthday").Date().Nullable()
})
if err := op.Err(); err != nil {
	return err
}

func (*Change) Decimal

func (change *Change) Decimal(precision, scale int) *Change

Decimal changes the type to an exact numeric with precision digits, scale of them after the decimal point. Precision must be positive and 0 <= scale <= precision. Invalid parameters stay construction errors; valid calls return the same draft.

Example:

op := schema.Alter("orders", func(table *schema.Table) {
	table.Change("amount").Decimal(12, 2).NotNull()
})
if err := op.Err(); err != nil {
	return err
}

func (*Change) Default

func (change *Change) Default(value any) *Change

Default sets a literal default, like Column.Default. It replaces a previous default or DropDefault and returns this draft. Invalid values remain construction errors even after a later replacement or removal.

Example:

op := schema.Alter("users", func(table *schema.Table) {
	table.Change("visits").Default(0)
})
if err := op.Err(); err != nil {
	return err
}

func (*Change) DefaultNow

func (change *Change) DefaultNow() *Change

DefaultNow defaults the column to the current timestamp. With a type set it must be a timestamp type. It replaces a previous default or DropDefault and returns the same draft. Without a type, checking the existing column's suitability belongs to the database.

Example:

op := schema.Alter("events", func(table *schema.Table) {
	table.Change("recorded_at").DefaultNow()
})
if err := op.Err(); err != nil {
	return err
}

func (*Change) DefaultRaw

func (change *Change) DefaultRaw(expression string) *Change

DefaultRaw defaults the column to expression, like Column.DefaultRaw. It replaces a previous default or DropDefault and returns this draft. Empty expressions retain a construction error; never pass untrusted SQL text.

Example:

op := schema.Alter("jobs", func(table *schema.Table) {
	table.Change("attempts").DefaultRaw("(40 + 2)")
})
if err := op.Err(); err != nil {
	return err
}

func (*Change) DropDefault

func (change *Change) DropDefault() *Change

DropDefault removes the column's default. It replaces any requested default and returns this draft, without clearing errors from invalid earlier values. SQLite cannot compile column changes.

Example:

op := schema.Alter("users", func(table *schema.Table) {
	table.Change("nickname").DropDefault()
})
if err := op.Err(); err != nil {
	return err
}

func (*Change) Float

func (change *Change) Float() *Change

Float changes the type to double-precision floating point. It returns this draft. Use Decimal instead when approximate representation is unsuitable; existing-value conversion remains the database's responsibility.

Example:

op := schema.Alter("readings", func(table *schema.Table) {
	table.Change("temperature").Float().NotNull()
})
if err := op.Err(); err != nil {
	return err
}

func (*Change) Integer

func (change *Change) Integer() *Change

Integer changes the type to a 32-bit integer. It returns the same draft and leaves other aspects untouched. MySQL needs explicit nullability too; SQLite rejects type changes at compilation.

Example:

op := schema.Alter("users", func(table *schema.Table) {
	table.Change("visits").Integer().NotNull()
})
if err := op.Err(); err != nil {
	return err
}

func (*Change) JSON

func (change *Change) JSON() *Change

JSON changes the type to a JSON document. It returns this draft. PostgreSQL uses jsonb and MySQL/ANSI use json; the database validates existing content when the compiled DDL is executed.

Example:

op := schema.Alter("users", func(table *schema.Table) {
	table.Change("preferences").JSON().NotNull()
})
if err := op.Err(); err != nil {
	return err
}

func (*Change) NotNull

func (change *Change) NotNull() *Change

NotNull forbids NULL values in the column. Existing NULLs make the statement fail at run time. It replaces Nullable and returns this draft. MySQL also requires a type.

Example:

op := schema.Alter("users", func(table *schema.Table) {
	table.Change("name").String(160).NotNull()
})
if err := op.Err(); err != nil {
	return err
}

func (*Change) Nullable

func (change *Change) Nullable() *Change

Nullable allows NULL values in the column and returns this draft. It replaces NotNull; MySQL also requires a type for its MODIFY COLUMN form.

Example:

op := schema.Alter("users", func(table *schema.Table) {
	table.Change("name").String(160).Nullable()
})
if err := op.Err(); err != nil {
	return err
}

func (*Change) SmallInteger

func (change *Change) SmallInteger() *Change

SmallInteger changes the type to a 16-bit integer. It returns the same draft. Nullability and defaults are separate aspects; SQLite does not support changing an existing column's type.

Example:

op := schema.Alter("users", func(table *schema.Table) {
	table.Change("priority").SmallInteger().NotNull()
})
if err := op.Err(); err != nil {
	return err
}

func (*Change) String

func (change *Change) String(length int) *Change

String changes the type to a varchar holding up to length characters. Length must be positive; invalid lengths stay construction errors. The same draft is returned, and nullability is unchanged unless explicitly configured.

Example:

op := schema.Alter("users", func(table *schema.Table) {
	table.Change("name").String(160).NotNull()
})
if err := op.Err(); err != nil {
	return err
}

func (*Change) Text

func (change *Change) Text() *Change

Text changes the type to unbounded text. It returns this draft, leaving other aspects unchanged. MySQL also requires explicit nullability when changing a type; SQLite cannot compile the change.

Example:

op := schema.Alter("users", func(table *schema.Table) {
	table.Change("bio").Text().Nullable()
})
if err := op.Err(); err != nil {
	return err
}

func (*Change) Timestamp

func (change *Change) Timestamp() *Change

Timestamp changes the type to a timestamp without time zone. It returns the same draft. Defaults and nullability are separate settings; TimestampTZ selects the timezone-aware type where the dialect supports it.

Example:

op := schema.Alter("events", func(table *schema.Table) {
	table.Change("recorded_at").Timestamp().NotNull()
})
if err := op.Err(); err != nil {
	return err
}

func (*Change) TimestampTZ

func (change *Change) TimestampTZ() *Change

TimestampTZ changes the type to a timestamp with time zone. MySQL renders datetime(6), which stores no zone: store UTC values. It returns the same draft; SQLite cannot change an existing column.

Example:

op := schema.Alter("events", func(table *schema.Table) {
	table.Change("recorded_at").TimestampTZ().NotNull()
})
if err := op.Err(); err != nil {
	return err
}

func (*Change) Type

func (change *Change) Type(sqlType string) *Change

Type changes the type to sqlType, rendered verbatim for every dialect. Never pass user input: it is not escaped. Empty types retain a construction error. It returns the same draft and does not check whether a nonempty native type exists on a particular database.

Example:

op := schema.Alter("users", func(table *schema.Table) {
	table.Change("email").Type("varchar(320)").NotNull()
})
if err := op.Err(); err != nil {
	return err
}

func (*Change) UUID

func (change *Change) UUID() *Change

UUID changes the type to UUID. It returns this draft. PostgreSQL uses uuid; other supported non-SQLite dialects use char(36). SQLite rejects all existing-column changes.

Example:

op := schema.Alter("users", func(table *schema.Table) {
	table.Change("external_id").UUID().NotNull()
})
if err := op.Err(); err != nil {
	return err
}

func (*Change) Using

func (change *Change) Using(expression string) *Change

Using sets the expression that converts existing values when the type changes, such as "id::integer". It needs a type method and only Postgres supports it: other dialects fail with arkoren.dev/foundation/query.ErrUnsupported. The expression is rendered verbatim: never pass user input. Empty expressions retain a construction error; successful calls return this draft and do not evaluate the conversion during declaration.

Example:

op := schema.Alter("users", func(table *schema.Table) {
	table.Change("id").Integer().Using("id::integer")
})
statement, err := op.Statements()[0].BuildFor(query.Postgres)
if err != nil {
	return err
}
_ = statement.SQL()

type Column

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

Column describes one column of a Table. Table methods such as Table.String return it so modifiers can chain; it is mutable only inside the callback that created it, and later changes are ignored. Columns are NOT NULL unless Column.Nullable is called. Combinations no dialect accepts, such as Column.DefaultNow on a text column, are reported by Operation.Err. All modifiers return the same draft; retain the published Operation, not a Column, for concurrent compilation. Invalid individual settings stay errors.

Example:

op := schema.Create("users", func(table *schema.Table) {
	column := table.String("nickname", 80)
	column.Nullable().Default("guest")
})
if err := op.Err(); err != nil {
	return err
}

func (*Column) AutoIncrement

func (column *Column) AutoIncrement() *Column

AutoIncrement makes an integer column an auto-incrementing primary key. It cannot be combined with Nullable or any default. It returns the same draft; Operation.Err reports incompatible types and conflicting primary declarations.

Example:

op := schema.Create("users", func(table *schema.Table) {
	table.BigInteger("id").AutoIncrement()
})
if err := op.Err(); err != nil {
	return err
}

func (*Column) Default

func (column *Column) Default(value any) *Column

Default sets a literal default. It accepts strings, integers, finite floats, booleans, and nil, which means NULL, including named forms of these types. Unsupported types, nonfinite floats, and NUL-bearing strings record a sticky construction error: replacing the default does not clear it. Valid strings are escaped for the selected dialect when compiled.

Example:

op := schema.Create("users", func(table *schema.Table) {
	table.Integer("visits").Default(0)
})
if err := op.Err(); err != nil {
	return err
}

func (*Column) DefaultNow

func (column *Column) DefaultNow() *Column

DefaultNow defaults the column to the current timestamp. It is valid only on Table.Timestamp and Table.TimestampTZ columns. It replaces any earlier default and returns the same draft; Operation.Err reports incompatible types.

Example:

op := schema.Create("users", func(table *schema.Table) {
	table.TimestampTZ("created_at").DefaultNow()
})
if err := op.Err(); err != nil {
	return err
}

func (*Column) DefaultRaw

func (column *Column) DefaultRaw(expression string) *Column

DefaultRaw defaults the column to expression, rendered verbatim. Never pass user input: it is not escaped, and a "?" outside quotes fails. Empty expressions record a sticky construction error. The same draft is returned; dialect syntax and raw placeholder failures surface at compilation.

Example:

op := schema.Create("jobs", func(table *schema.Table) {
	table.Integer("attempts").DefaultRaw("(40 + 2)")
})
if err := op.Err(); err != nil {
	return err
}

func (*Column) Nullable

func (column *Column) Nullable() *Column

Nullable allows NULL values in the column and returns the same draft. It does not change a default; auto-increment columns cannot be nullable.

Example:

op := schema.Create("users", func(table *schema.Table) {
	table.Text("bio").Nullable()
})
if err := op.Err(); err != nil {
	return err
}

func (*Column) OnDelete

func (column *Column) OnDelete(action ReferentialAction) *Column

OnDelete sets the ON DELETE action of the column's foreign key. The column must also call Column.References. Empty clears the action; unknown values record a sticky construction error. It returns the same draft.

Example:

op := schema.Create("posts", func(table *schema.Table) {
	table.ForeignID("user_id", "users").OnDelete(schema.Cascade)
})
if err := op.Err(); err != nil {
	return err
}

func (*Column) OnUpdate

func (column *Column) OnUpdate(action ReferentialAction) *Column

OnUpdate sets the ON UPDATE action of the column's foreign key. The column must also call Column.References. Empty clears the action; unknown values record a sticky construction error. It returns the same draft.

Example:

op := schema.Create("posts", func(table *schema.Table) {
	table.ForeignID("user_id", "users").OnUpdate(schema.Cascade)
})
if err := op.Err(); err != nil {
	return err
}

func (*Column) Primary

func (column *Column) Primary() *Column

Primary marks the column as part of the primary key. Marking several columns forms one composite primary key, in declaration order. It cannot be combined with Table.Primary. In Alter it adds a primary key constraint on this one column; marking multiple added columns fails construction rather than emitting multiple primary-key constraints. Use raw DDL for a composite primary key on an existing table.

Example:

operation := schema.Create("memberships", func(table *schema.Table) {
	table.Integer("tenant_id").Primary()
	table.Integer("user_id").Primary()
})
if err := operation.Err(); err != nil {
	return err
}

func (*Column) References

func (column *Column) References(foreignTable, referenced string) *Column

References adds a foreign key from the column to referenced in foreignTable, which may be schema-qualified. The constraint is named <table>_<column>_fkey. SQLite requires an unqualified foreignTable in the referencing table's schema; a qualified target fails compilation with arkoren.dev/foundation/query.ErrUnsupported, even when it names that schema.

Example:

operation := schema.Create("posts", func(table *schema.Table) {
	table.BigInteger("user_id").References("users", "id")
})
statement, err := operation.Statements()[0].BuildFor(query.SQLite)
if err != nil {
	return err
}
_ = statement.SQL()

func (*Column) Unique

func (column *Column) Unique() *Column

Unique makes the column unique with a unique index named <table>_<column>_key, created after the table (or the added column). It returns the same draft; repeating it does not add another index.

Example:

op := schema.Create("users", func(table *schema.Table) {
	table.String("email", 255).Unique()
})
if err := op.Err(); err != nil {
	return err
}

type Foreign

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

Foreign is a foreign key added to an existing table with Table.Foreign. Its modifiers edit only the declaration draft; publication detaches them and Operation.Err retains invalid actions. SQLite cannot add this constraint.

Example:

op := schema.Alter("posts", func(table *schema.Table) {
	foreign := table.Foreign("user_id", "users", "id")
	foreign.OnDelete(schema.Cascade)
})
if err := op.Err(); err != nil {
	return err
}

func (*Foreign) OnDelete

func (foreign *Foreign) OnDelete(action ReferentialAction) *Foreign

OnDelete sets the ON DELETE action of the foreign key and returns this draft. Empty clears the action; unknown actions retain a construction error.

Example:

op := schema.Alter("posts", func(table *schema.Table) {
	table.Foreign("user_id", "users", "id").OnDelete(schema.Restrict)
})
if err := op.Err(); err != nil {
	return err
}

func (*Foreign) OnUpdate

func (foreign *Foreign) OnUpdate(action ReferentialAction) *Foreign

OnUpdate sets the ON UPDATE action of the foreign key and returns this draft. Empty clears the action; unknown actions retain a construction error.

Example:

op := schema.Alter("posts", func(table *schema.Table) {
	table.Foreign("user_id", "users", "id").OnUpdate(schema.NoAction)
})
if err := op.Err(); err != nil {
	return err
}

type Operation

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

Operation is one schema change: a table created, altered, renamed, or dropped. It compiles to one or more argument-free SQL statements whose number is fixed when the operation is constructed and does not depend on the dialect.

An Operation is an immutable value. Problems found while constructing it (invalid names, unknown index columns, modifier misuse, an empty callback) are kept and reported by Operation.Err; problems that depend on the dialect (for example SQLite changing a column) surface when a statement is compiled and match query.ErrUnsupported. Every error matches query.ErrInvalidQuery. The zero value has no statements and no construction error; constructors reject missing declarations instead of publishing such an empty operation.

Example:

op := schema.Create("users", func(table *schema.Table) {
	table.ID()
})
for _, definition := range op.Statements() {
	statement, err := definition.BuildFor(query.Postgres)
	if err != nil {
		return err
	}
	fmt.Println(statement.SQL())
}

func Alter

func Alter(table string, define ...func(*Table)) Operation

Alter returns the operation that changes table as define declares, one statement per call in written order. A column type method adds the column (ADD COLUMN, followed by CREATE UNIQUE INDEX for a unique column). The name may be schema-qualified. Callbacks follow Create's ordering, direct invocation, panic propagation, and detached publication rules. An omitted or empty declaration reports its missing actions through Operation.Err; it is not a successful no-op.

SQLite cannot change columns or add and drop foreign keys, and its ADD COLUMN rejects primary keys, auto-increment, current-time and raw defaults, a non-NULL default on a referencing column, and NOT NULL without a non-NULL literal default (including an explicit Default(nil)).

Example:

op := schema.Alter("users", func(table *schema.Table) {
	table.String("avatar", 255).Nullable()
	table.RenameColumn("name", "full_name")
	table.DropColumn("legacy")
})
if err := op.Err(); err != nil {
	return err
}

func Create

func Create(table string, define ...func(*Table)) Operation

Create returns the operation that creates table with the columns, keys, and indexes define declares. The name may be schema-qualified.

It compiles to one CREATE TABLE with the columns in written order and the primary key and foreign keys inline, then one CREATE [UNIQUE] INDEX per Table.Index, Table.Unique, and Column.Unique, in written order. Unique keys are always unique indexes, never inline constraints. After validating the table name, Create invokes each supplied callback directly, once, synchronously in argument order on the same Table. A nil callback or an application panic propagates; later callbacks do not run. Invalid names skip the callbacks.

The combined declaration is copied and validated after all callbacks. Omitting callbacks, including an expanded nil or empty callback slice, or supplying only empty callbacks leaves an invalid declaration reported by Operation.Err. Retained tables and child declarations cannot change the published operation.

Example:

op := schema.Create("users", func(table *schema.Table) {
	table.ID()
	table.ForeignID("tenant_id", "tenants").OnDelete(schema.Cascade)
	table.String("email", 255)
	table.Text("bio").Nullable()
	table.Timestamps()
	table.Unique("tenant_id", "email")
	table.Index("bio")
})
if err := op.Err(); err != nil {
	return err
}

func CreateIfNotExists

func CreateIfNotExists(table string, define ...func(*Table)) Operation

CreateIfNotExists is Create with IF NOT EXISTS on the table and on its indexes. ANSI has no table IF NOT EXISTS, and MySQL and ANSI have none for indexes, so those statements fail with query.ErrUnsupported on them. Callback ordering, omission, panic propagation, and ownership follow Create.

Example:

op := schema.CreateIfNotExists("users", func(table *schema.Table) {
	table.Text("name")
})
if err := op.Err(); err != nil {
	return err
}

func Drop

func Drop(table string) Operation

Drop drops the table. The name may be schema-qualified. Invalid identifiers fail construction; declaration and compilation never contact a database. Execute the result only at your migration boundary.

Example:

op := schema.Drop("users")
statement, err := op.Statements()[0].BuildFor(query.Postgres)
if err != nil {
	return err
}
_ = statement.SQL()

func DropIfExists

func DropIfExists(table string) Operation

DropIfExists drops the table when it exists. ANSI has no such syntax, so compiling for query.ANSI fails with query.ErrUnsupported. Invalid names fail construction; existence itself is checked by the database only when the compiled statement is executed.

Example:

op := schema.DropIfExists("users")
statement, err := op.Statements()[0].BuildFor(query.Postgres)
if err != nil {
	return err
}
_ = statement.SQL()

func Rename

func Rename(from, to string) Operation

Rename renames the table from to to. The new name must be unqualified: a table keeps its schema when renamed. Invalid identifiers are retained by Operation.Err. It does not check table existence or execute DDL; compilation remains lazy for the chosen dialect.

Example:

op := schema.Rename("users", "members")
if err := op.Err(); err != nil {
	return err
}
_ = op.Statements()

func (Operation) Err

func (operation Operation) Err() error

Err returns the first problem found while constructing the operation, or nil. The error matches query.ErrInvalidQuery. Dialect-specific problems are not reported here: they appear when a statement is compiled. This check performs no SQL rendering or I/O and does not change the value.

Example:

op := schema.Create("users")
if err := op.Err(); err != nil {
	return err // Missing columns are reported before execution.
}

func (Operation) Statements

func (operation Operation) Statements() []query.Statementer

Statements returns the operation's statements in execution order. Each one compiles lazily with BuildFor and yields a query.Statement without arguments. When Operation.Err is set, a single statement is returned and its BuildFor reports that error, so an unchecked operation never runs partially. The returned slice is a copy.

Example:

op := schema.Create("tags", func(table *schema.Table) {
	table.ID()
	table.String("name", 50).Unique()
})
for _, statement := range op.Statements() { // CREATE TABLE, CREATE UNIQUE INDEX
	compiled, err := statement.BuildFor(query.Postgres)
	if err != nil {
		return err
	}
	fmt.Println(compiled.SQL())
}
Example

ExampleOperation_Statements demonstrates compiling every published statement with checked errors, without opening a database or executing the DDL.

package main

import (
	"fmt"

	"arkoren.dev/foundation/query"
	"arkoren.dev/foundation/query/schema"
)

func main() {
	op := schema.Create("tags", func(table *schema.Table) {
		table.ID()
		table.String("name", 50).Unique()
	})

	for _, definition := range op.Statements() {
		statement, err := definition.BuildFor(query.Postgres)

		if err != nil {
			fmt.Println(err)

			return
		}

		fmt.Println(statement.SQL())
	}
}
Output:
CREATE TABLE "tags" ("id" bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, "name" varchar(50) NOT NULL)
CREATE UNIQUE INDEX "tags_name_key" ON "tags" ("name")

type ReferentialAction

type ReferentialAction string

ReferentialAction is the action a foreign key takes when the referenced row is deleted or updated. Empty ("") omits the explicit action; passing empty to OnDelete or OnUpdate clears a previous choice and leaves database defaults. Unknown nonempty values retain a construction error rather than rendering SQL.

Example:

op := schema.Create("posts", func(table *schema.Table) {
	table.ForeignID("user_id", "users").OnDelete(schema.Cascade).OnDelete("")
})
if err := op.Err(); err != nil {
	return err
}
const (
	// Cascade propagates the delete or update to referencing rows.
	// The database enforces the action; declarations do not execute it.
	//
	// Example:
	//
	//	op := schema.Create("posts", func(table *schema.Table) {
	//		table.ForeignID("user_id", "users").OnDelete(schema.Cascade)
	//	})
	//	if err := op.Err(); err != nil {
	//		return err
	//	}
	Cascade ReferentialAction = "CASCADE"

	// SetNull sets the referencing column to NULL.
	// Make that column Nullable as well so the database can apply the action.
	//
	// Example:
	//
	//	op := schema.Create("posts", func(table *schema.Table) {
	//		table.ForeignID("user_id", "users").Nullable().OnDelete(schema.SetNull)
	//	})
	//	if err := op.Err(); err != nil {
	//		return err
	//	}
	SetNull ReferentialAction = "SET NULL"

	// Restrict rejects the delete or update immediately.
	// Enforcement belongs to the database, not declaration construction.
	//
	// Example:
	//
	//	op := schema.Create("posts", func(table *schema.Table) {
	//		table.ForeignID("user_id", "users").OnDelete(schema.Restrict)
	//	})
	//	if err := op.Err(); err != nil {
	//		return err
	//	}
	Restrict ReferentialAction = "RESTRICT"

	// NoAction rejects the delete or update at the end of the statement.
	// It emits an explicit NO ACTION; empty ReferentialAction instead omits SQL.
	//
	// Example:
	//
	//	op := schema.Create("posts", func(table *schema.Table) {
	//		table.ForeignID("user_id", "users").OnUpdate(schema.NoAction)
	//	})
	//	if err := op.Err(); err != nil {
	//		return err
	//	}
	NoAction ReferentialAction = "NO ACTION"
)

type Table

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

Table collects the definition of one table inside the callbacks of Create, CreateIfNotExists, or Alter. Column methods such as Table.String add a column and return it for modifiers; the other methods add keys, indexes, and, in Alter, changes to existing columns. Calls are recorded in order, and the first problem is reported by Operation.Err.

The callbacks run synchronously, in order, on the same Table. They must not mutate it concurrently. The completed declaration is copied before validation and publication; later changes to retained tables, columns, changes, or foreign keys have no effect on the operation. Arbitrary values passed to Column.Default or Change.Default are not recursively copied.

Example:

op := schema.Create("users", func(table *schema.Table) {
	table.Text("name")
	table.Index("name")
})
if err := op.Err(); err != nil {
	return err
}

func (*Table) BigInteger

func (table *Table) BigInteger(name string) *Column

BigInteger adds a 64-bit integer column. It returns a NOT NULL Column for modifiers. SQLite renders INTEGER; other dialects use bigint. Use AutoIncrement explicitly for an identity column.

Example:

op := schema.Create("users", func(table *schema.Table) {
	table.BigInteger("visits").Default(0)
})
if err := op.Err(); err != nil {
	return err
}

func (*Table) Binary

func (table *Table) Binary(name string) *Column

Binary adds a binary large object column. It returns a NOT NULL Column. PostgreSQL uses bytea; ANSI/MySQL use blob and SQLite uses BLOB. The declaration does not retain any application payload.

Example:

op := schema.Create("files", func(table *schema.Table) {
	table.Binary("contents")
})
if err := op.Err(); err != nil {
	return err
}

func (*Table) Boolean

func (table *Table) Boolean(name string) *Column

Boolean adds a boolean column. It returns a NOT NULL Column. SQLite stores INTEGER; boolean defaults use dialect-appropriate truth literals without adding a CHECK constraint.

Example:

op := schema.Create("users", func(table *schema.Table) {
	table.Boolean("active").Default(true)
})
if err := op.Err(); err != nil {
	return err
}

func (*Table) Change

func (table *Table) Change(name string) *Change

Change redefines the existing column name; see Change for what it changes and the MySQL and SQLite rules. It is valid only in Alter.

Example:

op := schema.Alter("users", func(table *schema.Table) {
	table.Change("email").String(320).NotNull()
	table.Change("bio").DropDefault()
})
if err := op.Err(); err != nil {
	return err
}

func (*Table) Char

func (table *Table) Char(name string, length int) *Column

Char adds a fixed-length character column of length characters, which must be positive. SQLite stores it as TEXT and does not pad. It returns the added NOT NULL column for optional modifiers.

Example:

op := schema.Create("users", func(table *schema.Table) {
	table.Char("country", 2)
})
if err := op.Err(); err != nil {
	return err
}

func (*Table) Column

func (table *Table) Column(name, sqlType string) *Column

Column adds a column whose type, sqlType, is rendered verbatim for every dialect. Never pass user input: it is not escaped. An empty type records a construction error. It returns the NOT NULL Column; nonempty native syntax remains the database's responsibility to accept.

Example:

op := schema.Create("users", func(table *schema.Table) {
	table.Column("email", "varchar(320)")
})
if err := op.Err(); err != nil {
	return err
}

func (*Table) Date

func (table *Table) Date(name string) *Column

Date adds a calendar date column. It returns a NOT NULL Column. The declaration selects a SQL type, not an application codec or date-validation policy; SQLite's type is DATE.

Example:

op := schema.Create("users", func(table *schema.Table) {
	table.Date("birthday").Nullable()
})
if err := op.Err(); err != nil {
	return err
}

func (*Table) Decimal

func (table *Table) Decimal(name string, precision, scale int) *Column

Decimal adds an exact numeric column with precision digits, scale of them after the decimal point. Precision must be positive and 0 <= scale <= precision; invalid values stay construction errors. SQLite renders NUMERIC without enforcing those limits.

Example:

op := schema.Create("orders", func(table *schema.Table) {
	table.Decimal("amount", 12, 2)
})
if err := op.Err(); err != nil {
	return err
}

func (*Table) DropColumn

func (table *Table) DropColumn(names ...string)

DropColumn drops each named column, one statement per column. It is valid only in Alter. Names are copied and validated without inspecting the database. An empty list adds no actions; an otherwise empty ALTER remains invalid.

Example:

op := schema.Alter("users", func(table *schema.Table) {
	table.DropColumn("legacy")
})
if err := op.Err(); err != nil {
	return err
}

func (*Table) DropForeign

func (table *Table) DropForeign(column string)

DropForeign drops the foreign key that Table.Foreign or Column.References created on column. It is valid only in Alter. Construction validates the column and generated constraint-name length. SQLite rejects compilation; no database existence check happens here.

Example:

op := schema.Alter("posts", func(table *schema.Table) {
	table.DropForeign("user_id")
})
if err := op.Err(); err != nil {
	return err
}

func (*Table) DropIndex

func (table *Table) DropIndex(columns ...string)

DropIndex drops the index that Table.Index created over the same columns. It is valid only in Alter. Names are copied and must form a nonempty valid key list; database index existence is not checked at construction.

Example:

op := schema.Alter("users", func(table *schema.Table) {
	table.DropIndex("name")
})
if err := op.Err(); err != nil {
	return err
}

func (*Table) DropUnique

func (table *Table) DropUnique(columns ...string)

DropUnique drops the index that Table.Unique created over the same columns. It is valid only in Alter. Names are copied and validated, including the generated identifier's length. The operation does not check whether that index exists in the database.

Example:

op := schema.Alter("users", func(table *schema.Table) {
	table.DropUnique("email")
})
if err := op.Err(); err != nil {
	return err
}

func (*Table) Float

func (table *Table) Float(name string) *Column

Float adds a double-precision floating-point column. It returns a NOT NULL Column. Use Decimal for exact decimal storage; nonfinite default values are rejected during declaration capture.

Example:

op := schema.Create("readings", func(table *schema.Table) {
	table.Float("temperature")
})
if err := op.Err(); err != nil {
	return err
}

func (*Table) Foreign

func (table *Table) Foreign(column, foreignTable, refColumn string) *Foreign

Foreign adds a foreign key from column to refColumn in foreignTable to an existing table, named <table>_<column>_fkey. SQLite cannot add one. It is valid only in Alter; in Create use Column.References. It returns a mutable child for referential actions. Invalid source/target identifiers are retained as construction errors without contacting the database.

Example:

op := schema.Alter("posts", func(table *schema.Table) {
	table.Foreign("user_id", "users", "id").OnDelete(schema.Cascade)
})
if err := op.Err(); err != nil {
	return err
}

func (*Table) ForeignID

func (table *Table) ForeignID(name, foreignTable string) *Column

ForeignID adds a bigint column that references the id column of foreignTable. Chain OnDelete or OnUpdate on the result, and Nullable for an optional relation. The returned column is NOT NULL by default. Reference names are validated at construction; dialect-specific target restrictions surface at compilation.

Example:

op := schema.Create("posts", func(table *schema.Table) {
	table.ForeignID("user_id", "users").OnDelete(schema.Cascade)
})
if err := op.Err(); err != nil {
	return err
}

func (*Table) ID

func (table *Table) ID() *Column

ID adds an auto-incrementing bigint primary key column named id. It returns that column for modifiers; conflicting primary declarations are construction errors. SQLite renders INTEGER PRIMARY KEY AUTOINCREMENT.

Example:

op := schema.Create("users", func(table *schema.Table) {
	table.ID()
})
if err := op.Err(); err != nil {
	return err
}

func (*Table) Index

func (table *Table) Index(columns ...string)

Index adds a plain index over columns, named <table>_<columns>_index. Names are copied. CREATE validates membership after all callbacks, so an index may precede its columns; empty or repeated column lists are invalid.

Example:

op := schema.Create("users", func(table *schema.Table) {
	table.Text("name")
	table.Index("name")
})
if err := op.Err(); err != nil {
	return err
}

func (*Table) Integer

func (table *Table) Integer(name string) *Column

Integer adds a 32-bit integer column. It returns a NOT NULL Column. SQLite uses INTEGER without enforcing a 32-bit range; application validation must enforce portable value limits.

Example:

op := schema.Create("users", func(table *schema.Table) {
	table.Integer("visits").Default(0)
})
if err := op.Err(); err != nil {
	return err
}

func (*Table) JSON

func (table *Table) JSON(name string) *Column

JSON adds a JSON document column. It returns a NOT NULL Column. PostgreSQL uses jsonb, ANSI/MySQL use json, and SQLite uses TEXT without an added JSON-validity constraint.

Example:

op := schema.Create("users", func(table *schema.Table) {
	table.JSON("preferences").Default("{}")
})
if err := op.Err(); err != nil {
	return err
}

func (*Table) Primary

func (table *Table) Primary(columns ...string)

Primary sets a table-level primary key over columns. It is valid only in Create and cannot be combined with a column's Primary modifier. Input names are copied. Empty, repeated, or undefined CREATE columns record a construction error; a table-level key can be declared only once.

Example:

op := schema.Create("memberships", func(table *schema.Table) {
	table.Integer("tenant_id")
	table.Integer("user_id")
	table.Primary("tenant_id", "user_id")
})
if err := op.Err(); err != nil {
	return err
}

func (*Table) RenameColumn

func (table *Table) RenameColumn(from, to string)

RenameColumn renames column from to to. It is valid only in Alter. Both names must be unqualified identifiers. Construction checks spelling, not whether the source exists or the target is available in the database.

Example:

op := schema.Alter("users", func(table *schema.Table) {
	table.RenameColumn("name", "full_name")
})
if err := op.Err(); err != nil {
	return err
}

func (*Table) SmallInteger

func (table *Table) SmallInteger(name string) *Column

SmallInteger adds a 16-bit integer column. SQLite stores it as INTEGER. It returns the NOT NULL column for modifiers; SQLite does not enforce the narrower range, so applications own range validation for portable data.

Example:

op := schema.Create("jobs", func(table *schema.Table) {
	table.SmallInteger("priority").Default(0)
})
if err := op.Err(); err != nil {
	return err
}

func (*Table) String

func (table *Table) String(name string, length int) *Column

String adds a varchar column holding up to length characters. Length must be positive; invalid lengths stay construction errors. The returned Column is NOT NULL until Nullable is called; SQLite renders TEXT.

Example:

op := schema.Create("users", func(table *schema.Table) {
	table.String("name", 160)
})
if err := op.Err(); err != nil {
	return err
}

func (*Table) Text

func (table *Table) Text(name string) *Column

Text adds an unbounded text column. It returns the added NOT NULL column for modifiers. SQLite renders TEXT; other dialects render text, without enforcing an application length limit.

Example:

op := schema.Create("users", func(table *schema.Table) {
	table.Text("bio").Nullable()
})
if err := op.Err(); err != nil {
	return err
}

func (*Table) Timestamp

func (table *Table) Timestamp(name string) *Column

Timestamp adds a timestamp column without time zone. It returns a NOT NULL Column. ANSI/PostgreSQL use timestamp(6), MySQL uses datetime(6), and SQLite uses DATETIME; no current-time default is implicit.

Example:

op := schema.Create("events", func(table *schema.Table) {
	table.Timestamp("recorded_at").DefaultNow()
})
if err := op.Err(); err != nil {
	return err
}

func (*Table) TimestampTZ

func (table *Table) TimestampTZ(name string) *Column

TimestampTZ adds a timestamp column with time zone. MySQL has no such type, so it renders datetime(6), which stores no time zone: store UTC values. It returns a NOT NULL Column; a current-time default requires DefaultNow.

Example:

op := schema.Create("events", func(table *schema.Table) {
	table.TimestampTZ("recorded_at").DefaultNow()
})
if err := op.Err(); err != nil {
	return err
}

func (*Table) Timestamps

func (table *Table) Timestamps()

Timestamps adds NOT NULL created_at and updated_at timestamptz columns that default to the current time. It does not install an update trigger: applications must update updated_at explicitly. Duplicate column names remain construction errors.

Example:

op := schema.Create("users", func(table *schema.Table) {
	table.ID()
	table.Timestamps()
})
if err := op.Err(); err != nil {
	return err
}

func (*Table) UUID

func (table *Table) UUID(name string) *Column

UUID adds a UUID column. It returns a NOT NULL Column. PostgreSQL uses uuid, ANSI/MySQL use char(36), and SQLite uses TEXT; text-backed dialects do not gain UUID validation.

Example:

op := schema.Create("users", func(table *schema.Table) {
	table.UUID("external_id").Unique()
})
if err := op.Err(); err != nil {
	return err
}

func (*Table) Unique

func (table *Table) Unique(columns ...string)

Unique adds a unique index over columns, named <table>_<columns>_key. Unique keys are always indexes, so Table.DropUnique is a DROP INDEX on every dialect. Names are copied; empty lists, repeated columns, or unknown CREATE columns fail construction. No database schema is inspected during declaration.

Example:

op := schema.Create("users", func(table *schema.Table) {
	table.String("email", 255)
	table.Unique("email")
})
if err := op.Err(); err != nil {
	return err
}

Jump to

Keyboard shortcuts

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