schema

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: 5 Imported by: 0

README

schema — what tables exist

Table descriptions, the per-engine type spelling table, and a deterministic topological sort, so two packages that own tables can be migrated together.

It describes and never does. No DDL, no connection, no migration; those live above it, and contrib/migrate is the consumer that proves it carries its weight by executing its generated plan against a real database. contrib/vet reads the same type table, so a column the vet accepts is one CreateTable would have produced — two copies of what a bigint is would have drifted.

Full documentation

The page for this package is docs/schema/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 schema describes what tables exist, so that two packages which do not know about each other can still be migrated together.

The query layers already solve half of this. A library exports typed columns and an application joins across the boundary, because both sides speak one vocabulary: expr.Table and expr.Col. Without a shared vocabulary for the schema itself, every generator and migration tool built on sqlb invents its own description, and the moment a library declares its tables in one format and the application uses another, a migration runner holds two incompatible accounts of the same database and can order neither.

So this package is not a migration engine and never renders DDL. It is the noun those tools agree on. A migration engine, a code generator and an introspector all live above it, in contrib or in somebody else's module, and none of them has to agree with the others about anything but this.

It imports expr, so a declared table can hand back the expr.Table and typed columns the query builder needs, and a caller never maintains the same table in two places.

Index

Constants

This section is empty.

Variables

View Source
var ErrCycle = errors.New("schema: foreign keys form a cycle, so no creation order exists")

ErrCycle reports a circular foreign key dependency.

View Source
var ErrDuplicateTable = errors.New("schema: two packages declare the same table")

ErrDuplicateTable reports that two contributors declared the same table name.

It is an error rather than a last-write-wins merge because the two declarations are usually different, and silently picking one produces a database that matches neither package's expectations. Naming both owners is the whole value of the error: the fix is a prefix or a schema, and you cannot choose one without knowing who collided.

View Source
var ErrMissingTable = errors.New("schema: foreign key references a table no package contributed")

ErrMissingTable reports a foreign key pointing at a table nobody contributed.

This is the error that makes assembling a schema worth doing at all. A library's foreign key names its target by string precisely so the library need not import whoever owns it, and the cost of that freedom is that the reference is unchecked until the pieces are put together. Here is where it gets checked.

View Source
var ErrTypeMismatch = errors.New("schema: Go type does not match the declared column type")

ErrTypeMismatch reports that a Go type does not match a column's declared type.

TypeValues is the canonical iteration order over the portable types, so a DDL renderer walks the same list this package defines rather than drifting into its own copy and silently omitting one.

Functions

func Col

func Col[T any](t Table, name string) (expr.Expr[T], error)

Col returns the typed expression for one of this table's columns.

It is a function rather than a method because Go methods cannot take type parameters. The type argument is the caller's claim about the column's Go type, and it is checked against the declaration: asking for Col[int] on a text column is the mistake this package can actually catch, and catching it at declaration time beats a scan error in production.

func EngineNames

func EngineNames() []string

EngineNames reports which engines have a type table, in a stable order, so a caller can check before it starts rather than after.

func EngineType

func EngineType(engine string, t Type) (string, bool)

EngineType is an engine's spelling of a portable type. The second result is false when the engine has no table or the type is not portable; a caller that can express the type another way, such as Column.Raw, decides what that means.

func GoTypes

func GoTypes(t Type) []string

GoTypes is the Go types a portable type accepts, spelled as reflect prints them, so []byte is "[]uint8". More than one is allowed where the choice is genuinely the caller's, such as whether a timestamp arrives as time.Time or as a string. It is the same table Column.Get checks against, exported so a tool reading declarations from source applies the one rule.

func MustCol

func MustCol[T any](t Table, name string) expr.Expr[T]

MustCol is Col for a package-level declaration, panicking instead of returning an error.

A library declares its columns in a var block, where there is no error to return: package initialisation has no error path, so every consumer would otherwise write the same three-line must helper around Col. Panicking is right here and nowhere else in this module, for the reason regexp.MustCompile panics — the argument is a literal the author wrote, so a failure is a program that is wrong rather than an input that is bad, it happens at package load, and it therefore cannot survive the first run of any test.

Use Col wherever the table or the column name comes from outside the source, because there the failure is data and an error is the honest answer.

Types

type Action

type Action string

Action is what a foreign key does when the referenced row goes away.

const (
	NoAction   Action = "NO ACTION"
	Restrict   Action = "RESTRICT"
	Cascade    Action = "CASCADE"
	SetNull    Action = "SET NULL"
	SetDefault Action = "SET DEFAULT"
)

The referential actions every engine here implements with the same meaning.

type Column

type Column struct {
	Name     string
	Type     Type
	Nullable bool
	// Default is rendered verbatim by a DDL renderer. It is a string rather
	// than a value because a default is frequently an expression, and quoting
	// rules belong to the engine, not here.
	Default string
	// Raw overrides Type with an engine's own spelling, for the case this
	// package's portable set cannot express.
	Raw string
	// Comment is carried through to DDL by renderers that support it.
	Comment string
}

Column is one column's declaration.

Raw is the single documented escape hatch, and it is per column rather than per table so that reaching for an engine-specific type costs one field and is visible in a diff. When Raw is set, Type is ignored by anything rendering DDL; the pair is checked by Validate so the contradiction cannot ship.

type ForeignKey

type ForeignKey struct {
	Name       string
	Columns    []string
	RefTable   string
	RefColumns []string
	OnDelete   Action
	OnUpdate   Action
}

ForeignKey is a reference from this table to another.

Table is named by string rather than by a Table value on purpose: the referenced table is frequently owned by a different package, and taking a value here would force that package to be imported, which is exactly the coupling this package exists to avoid. A Set resolves the names.

type Index

type Index struct {
	Name    string
	Columns []string
	Unique  bool
}

Index is a secondary index declaration. The primary key is on Table, because every engine treats it specially.

type Set

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

Set is the schema of a whole database, assembled from packages that do not know about each other.

This is the type the package exists for. A library contributes its tables, an application contributes its own, and the result can be validated and ordered as one thing. Neither side imports the other; they meet here, in a value both can construct.

A Set is ordinary and copyable, and holds no hidden state: the same tables always produce the same ordering, so a migration planned on a developer's machine is the one that runs in CI.

func New

func New(owner string, tables ...Table) (*Set, error)

New builds a Set from one contribution. Most callers use Add afterwards.

func (*Set) Add

func (s *Set) Add(owner string, tables ...Table) error

Add contributes tables under an owner name, which is used only in errors.

The owner is worth the parameter. When a migration fails because two modules both defined "users", the question is always which two, and a Set that did not record it can only report the name.

func (Set) Owner

func (s Set) Owner(qualifiedName string) (string, bool)

Owner reports which contributor declared a table.

func (Set) Sorted

func (s Set) Sorted() ([]Table, error)

Sorted returns the tables in an order where every table comes after the ones it references, which is the order they can be created in.

This is the question a migration runner actually has, and the reason two packages can be migrated together without either importing the other: the dependency is expressed as a name, and the ordering is computed here from all the names at once. Neither package could have worked it out alone.

The sort is deterministic. Kahn's algorithm leaves genuine freedom in the order of independent tables, and resolving that freedom by name rather than by map iteration is what keeps a generated migration from changing shape between runs for no reason.

func (Set) Tables

func (s Set) Tables() []Table

Tables returns every table, ordered by name. Use Sorted when the order has to respect foreign keys.

func (Set) Validate

func (s Set) Validate() error

Validate checks every table and every cross-table reference.

type Table

type Table struct {
	Name        string
	Schema      string
	Alias       string
	Columns     []Column
	PrimaryKey  []string
	Indexes     []Index
	ForeignKeys []ForeignKey
	Comment     string
}

Table is one table's declaration.

Alias is here so that the schema and the query builder cannot disagree. A caller declares the table once and calls Ref to get the expr.Table the builder wants, rather than writing the name and alias out a second time where a typo is a runtime surprise rather than a compile error.

func (Table) Column

func (t Table) Column(name string) (Column, bool)

Column finds a column by name.

func (Table) ColumnNames

func (t Table) ColumnNames() []string

ColumnNames returns the declared column names in order.

func (Table) Ref

func (t Table) Ref() expr.Table

Ref returns the expr.Table for this declaration, so a schema and the queries against it are never written out twice.

func (Table) Validate

func (t Table) Validate() error

Validate reports every problem with a declaration, rather than the first.

Returning all of them matters because a schema is usually written in one sitting: fixing one typo only to be told about the next is the slowest possible way to find four mistakes.

type Type

type Type string

Type is a logical column type, not a SQL one.

Deliberately logical: "the smallest integer that holds a user id" is a portable idea, whereas INTEGER, INT4 and Int32 are three engines disagreeing about how to spell it. Mapping a Type to an engine's spelling is the job of whatever renders DDL, which is a layer above this one and knows which engine it is targeting. Keeping the mapping out of here is what lets one declaration serve Postgres and SQLite without a conditional in the declaration.

const (
	TypeText      Type = "text"
	TypeInt       Type = "int"
	TypeBigInt    Type = "bigint"
	TypeBool      Type = "bool"
	TypeFloat     Type = "float"
	TypeBytes     Type = "bytes"
	TypeTimestamp Type = "timestamp"
	TypeDate      Type = "date"
	TypeUUID      Type = "uuid"
	TypeJSON      Type = "json"
	TypeDecimal   Type = "decimal"
)

The portable column types. This is a closed set on purpose: every entry has to mean the same thing on Postgres, MySQL, SQLite and ClickHouse, which is the same universality rule the core builder follows. An engine-specific type belongs in a Raw column, where it is visibly engine-specific.

Jump to

Keyboard shortcuts

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