AnnouncementsIntroducing Arkoren

    Menu

    Schema Builder

    Define and change tables, columns, indexes, and constraints across database dialects.

    What's on this page
    1. Describe tables, columns, indexes, and foreign keys as schema operations.
    2. Add or change columns with explicit dialect and data-safety constraints.
    3. Inspect generated statements without confusing compilation with execution.
    On this page

    The schema builder describes DDL: statements that create or change database objects. An operation is an immutable definition that compiles into one or more statements. It does not inspect your existing schema or execute SQL. Use migration steps to apply operations with history and coordination.

    Import arkoren.dev/foundation/query/schema and arkoren.dev/foundation/query.

    Creating Tables

    For a user table with optional biography text:

    users := schema.Create("users", func(table *schema.Table) {
    	table.ID()
    	table.String("email", 255).Unique()
    	table.Text("bio").Nullable()
    })

    ID adds an auto-incrementing primary key named id. Columns are NOT NULL unless Nullable is called. String requires a positive length; SQLite renders it as TEXT rather than enforcing that length. Validate portable application length and numeric range rules yourself.

    Common column choices include integers, booleans, decimals, UUIDs, JSON, binary, dates, and timestamps. Timestamps() adds created_at and updated_at with current-time defaults; it does not install an automatic update trigger. TimestampTZ retains provider differences: MySQL uses datetime(6), not a timezone-aware type. Store UTC consistently, especially on SQLite where timestamp comparisons use text.

    Use Default(value) for scalar literal defaults and DefaultRaw(expression) only for trusted database expressions. DDL defaults are rendered into SQL, not bound parameters. Never pass request text to raw types or expressions.

    Keys and Indexes

    If email must be unique per tenant rather than globally, use this alternative table definition. Assume tenants already exists with a compatible id:

    users := schema.Create("users", func(table *schema.Table) {
    	table.ID()
    	table.ForeignID("tenant_id", "tenants").OnDelete(schema.Cascade)
    	table.String("email", 255)
    	table.Unique("tenant_id", "email")
    })

    ForeignID adds a bigint column referencing the parent’s id; cascade deletes remove dependent users when their tenant is deleted. Choose that destructive behavior deliberately. Without a cascade policy, do not assume parent deletion will clean up child rows. SQLite foreign-key enforcement is a connection policy you must enable in the database configuration.

    Index adds a nonunique index. Unique and a column’s Unique() create unique indexes rather than inline constraints. Generated names use the table and columns: users_tenant_id_email_key for this unique index and users_tenant_id_fkey for the foreign key. Schema-qualified names contribute their schema. Names, including generated ones, must fit 63 bytes.

    To remove generated objects during an Alter, supply the original columns to DropIndex, DropUnique, or DropForeign, not the generated index name. Primary supports a table-level primary key; choose key design before data depends on it.

    Adding Columns

    For a rolling deployment, begin with an additive change old code can tolerate:

    addPhone := schema.Alter("users", func(table *schema.Table) {
    	table.String("phone", 32).Nullable()
    })

    The operation adds a nullable column and leaves existing rows readable. Backfill and application changes are separate steps; do not add a required column to populated data without deciding how existing rows receive valid values. DDL can lock or rebuild a table depending on the engine and operation, so assess it on production-sized data before deployment.

    RenameColumn and DropColumn belong inside Alter; schema.Rename renames a table and schema.Drop removes one. Renames and drops can break old instances still serving traffic. Follow expand and contract and back up data before destructive changes.

    Changing Columns

    PostgreSQL can change only the aspects you specify. If an existing phone column should become nullable:

    changePhone := schema.Alter("users", func(table *schema.Table) {
    	table.Change("phone").Nullable()
    })

    This PostgreSQL-oriented operation does not restate its type or default. SQLite cannot compile column changes and returns query.ErrUnsupported. Plan a tested table-rebuild migration when SQLite needs that transformation.

    MySQL type or nullability changes use MODIFY COLUMN, which restates the whole column. Supply a type and either Nullable or NotNull together, and restate any default you need to retain. A default-only change uses ALTER COLUMN instead. There is no universally safe ALTER recipe: inspect the existing attributes and dependent objects before deciding what to restate or transform. Change.Type and Change.Using contain trusted verbatim SQL, not validated user input.

    Inspecting Statements

    Check an operation’s construction error, then inspect each statement for your target dialect. Start with the users operation above:

    err := users.Err()

    After handling that error, compile its statements individually for PostgreSQL:

    for _, statement := range users.Statements() {
    	compiled, err := statement.BuildFor(query.Postgres)
    	// Inspect compiled.SQL() after handling err.
    }

    Create emits the table first, then one statement per index. Alter emits statements in written order. Operation construction errors make every statement fail, while dialect limits appear at compilation under query.ErrUnsupported; schema errors also match query.ErrInvalidQuery. Compilation does not verify existing columns, permissions, data conversion, or acceptable lock time.

    CreateIfNotExists and DropIfExists help make some steps repeatable, but they do not prove an existing object has the intended definition. Table IF NOT EXISTS is unavailable on ANSI; index IF NOT EXISTS is unavailable on ANSI and MySQL. A multi-statement operation can partially apply without transactional DDL.

    Literal rendering assumes PostgreSQL standard_conforming_strings=on and MySQL without NO_BACKSLASH_ESCAPES. MySQL text/blob/JSON defaults require MySQL 8.0.13 or newer. SQLite REFERENCES targets must be unqualified and resolve in the referencing table’s schema. Test generated SQL on each supported deployment profile, not only against ANSI output.

    For all operations and column modifiers, see the schema API reference.

    Open a bulk documentation archive?

    This file contains every guide in its scope, not API declarations. Loading it all into an agent can exhaust its context window and increase processing time and cost. For coding tasks, start with the index and read only the relevant pages.

    Use the full archive for offline indexing, bulk processing, or deliberate large-context use.

    Browse the index instead
    Open archive