lode/migration/ddl
The DDL layer: column types, column definitions, schema operations, and
their rendering. to_sql takes a Dialect (postgres() / sqlite()),
as do lode/migration’s up_sql/down_sql.
Per-column options are pipeline modifiers (Gleam has no default arguments):
ddl.column(“id”, ddl.Serial) |> ddl.primary_key ddl.column(“name”, ddl.Text) |> ddl.not_null ddl.column(“role”, ddl.Text) |> ddl.default(“‘user’”)
Types
pub type Column {
Column(
name: String,
type_: ColumnType,
primary_key: Bool,
null: Bool,
default: option.Option(Default),
references: option.Option(Reference),
)
}
Constructors
-
Column( name: String, type_: ColumnType, primary_key: Bool, null: Bool, default: option.Option(Default), references: option.Option(Reference), )
Supported column types. The names are PostgreSQL-flavoured, but each
Dialect renders them to its own SQL type (e.g. Jsonb is JSONB on
Postgres, TEXT on SQLite).
pub type ColumnType {
Serial
BigSerial
Integer
BigInt
Text
VarChar(Int)
Boolean
Float
Numeric
Date
Time
Timestamp
TimestampTz
Uuid
Jsonb
Array(ColumnType)
}
Constructors
-
Serial -
BigSerial -
Integer -
BigInt -
Text -
VarChar(Int) -
Boolean -
Float -
Numeric -
Date -
TimeA time of day with no date (
TIME/:time). -
TimestampA zone-less instant (
timestamp without time zone/:naive_datetime). -
TimestampTzA timezone-aware instant (
timestamptz/:utc_datetime). The value layer already bindsVTimestampas timestamptz and reconstructs it on load whichever wire shape the driver returns, so this only changes the declared column type — which is what lets aspec.UtcDatetimefield’s generated migration satisfy its own spec (see gen/schema and drift). -
Uuid -
Jsonb -
Array(ColumnType)A native array of the element type (
Array(Text)->TEXT[]on Postgres; nesting renders multidimensional syntax,INTEGER[][]). SQLite has no native arrays and rendersTEXT: its adapter stores aVListparam as JSON text, whichcomposite.arrayparses back on load. The element must be a plain data type —Serial/BigSerialmake no sense inside an array and Postgres rejects them.
A column default: a raw SQL expression, or the engine-portable “current
timestamp” that each Dialect renders to its own expression (see
default_now).
pub type Default {
Raw(String)
Now
}
Constructors
-
Raw(String)A raw SQL expression, rendered verbatim after
DEFAULT(e.g."0","'user'"). -
NowThe current timestamp, rendered per dialect (
Dialect.now).
A DDL dialect: the handful of points where Postgres and SQLite diverge in rendering. Everything else — identifier quoting, NOT NULL / DEFAULT / REFERENCES clauses, the operation structure — is shared.
pub type Dialect {
Dialect(
type_name: fn(ColumnType) -> String,
serial_primary_key: String,
big_serial_primary_key: String,
now: String,
table_suffix: String,
)
}
Constructors
-
Dialect( type_name: fn(ColumnType) -> String, serial_primary_key: String, big_serial_primary_key: String, now: String, table_suffix: String, )Arguments
- type_name
-
Render a non-serial column type to its SQL type name.
- serial_primary_key
-
The full column-body form for an auto-incrementing primary key (Postgres
SERIAL PRIMARY KEY; SQLiteINTEGER PRIMARY KEY AUTOINCREMENT).BigSerialusesbig_serial_primary_key. - big_serial_primary_key
-
As
serial_primary_key, forBigSerial(PostgresBIGSERIAL PRIMARY KEY; SQLite shares the singleINTEGER PRIMARY KEY AUTOINCREMENT). - now
-
The SQL expression a
Nowdefault renders to (afterDEFAULT):now()on Postgres; on SQLite astrftimeexpression producing the same RFC-3339 TEXT shape the SQLite adapter writes for aTimestamp. - table_suffix
-
Appended verbatim after
CREATE TABLE (...)’s closing paren: empty on Postgres,STRICTon SQLite (reject wrong-typed values at insert instead of silently coercing them).
A schema-change operation.
pub type Operation {
CreateTable(name: String, columns: List(Column))
DropTable(name: String)
AddColumn(table: String, column: Column)
DropColumn(table: String, name: String)
CreateIndex(
name: String,
table: String,
columns: List(String),
unique: Bool,
)
DropIndex(name: String)
CreateSchema(name: String)
DropSchema(name: String)
Execute(sql: String)
}
Constructors
-
CreateTable(name: String, columns: List(Column)) -
DropTable(name: String) -
AddColumn(table: String, column: Column) -
DropColumn(table: String, name: String) -
CreateIndex( name: String, table: String, columns: List(String), unique: Bool, ) -
DropIndex(name: String) -
CreateSchema(name: String)CREATE SCHEMA— a PostgreSQL schema/namespace (for prefix-based multi-tenancy), not a table schema. -
DropSchema(name: String)DROP SCHEMA ... CASCADE. -
Execute(sql: String)A raw SQL statement (escape hatch).
A foreign-key target: the referenced table, its key column (id by
default), and the actions taken when that row is deleted or its key updated.
Build with reference and pipe on_delete/on_update.
pub type Reference {
Reference(
table: String,
column: String,
on_delete: ReferentialAction,
on_update: ReferentialAction,
)
}
Constructors
-
Reference( table: String, column: String, on_delete: ReferentialAction, on_update: ReferentialAction, )
A foreign-key referential action, for a reference’s on_delete/on_update
(Ecto’s references(.., on_delete:)). Names are SQL-semantic and shared by
both events; the mapping to Ecto’s atoms is: NoAction = :nothing,
Restrict = :restrict, Cascade = :delete_all (on delete) /
:update_all (on update), SetNull = :nilify_all.
pub type ReferentialAction {
NoAction
Restrict
Cascade
SetNull
}
Constructors
-
NoActionNO ACTION— Postgres’s default; the clause is omitted entirely. -
Restrict -
Cascade -
SetNull
Values
pub fn column(name name: String, of type_: ColumnType) -> Column
A nullable, non-key column with the given type.
pub fn default(column col: Column, to sql: String) -> Column
Give a column a default (raw SQL expression, e.g. "0", "'user'"). For
the current timestamp use default_now, which renders per dialect.
pub fn default_now(column col: Column) -> Column
Default a column to the current timestamp, portably: DEFAULT now() on
Postgres, DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now')) on SQLite — the
same RFC-3339 TEXT shape the SQLite adapter writes for a Timestamp, so
default-filled and adapter-written values load, compare, and sort
uniformly. Note: SQLite cannot ADD COLUMN with a non-constant default
(“Cannot add a column with non-constant default”), so on SQLite this works
in create_table but not add_column.
pub fn index_name(
table table: String,
columns columns: List(String),
) -> String
The conventional index name for a table + columns (table_c1_c2_index).
pub fn on_delete(
reference ref: Reference,
action a: ReferentialAction,
) -> Reference
The action taken when the referenced row is deleted (Ecto’s
references(.., on_delete:)).
pub fn on_update(
reference ref: Reference,
action a: ReferentialAction,
) -> Reference
The action taken when the referenced key is updated (Ecto’s
references(.., on_update:)).
pub fn reference(table table: String) -> Reference
A foreign-key target referencing table’s id column, with no referential
actions (NO ACTION on delete and update). Refine with references_column,
on_delete, and on_update, then attach to a column with references:
ddl.column(“movie_id”, ddl.BigInt) |> ddl.not_null |> ddl.references(ddl.reference(“movies”) |> ddl.on_delete(ddl.Cascade))
pub fn references(
column col: Column,
reference ref: Reference,
) -> Column
Make a column a foreign key by attaching a reference. The column keeps the
type you gave it (lode doesn’t infer it from the referenced key); the
REFERENCES clause renders inline in the column definition.
pub fn references_column(
reference ref: Reference,
column col: String,
) -> Reference
Point a reference at a non-id key column.
pub fn sqlite() -> Dialect
The SQLite dialect: Serial/BigSerial collapse to
INTEGER PRIMARY KEY AUTOINCREMENT; Boolean is INTEGER; the temporal
types (Date/Time/Timestamp/TimestampTz) and Jsonb/Uuid/
VarChar(_)/Numeric are TEXT; Float is REAL.
Tables are created STRICT (SQLite >= 3.37): a wrong-typed insert errors
instead of being silently coerced. For a legacy loosely-typed table use
Dialect(..sqlite(), table_suffix: "") — or Execute raw SQL.
pub fn timestamps() -> List(Column)
The conventional inserted_at / updated_at pair (Ecto’s timestamps()):
two NOT NULL TimestampTz columns defaulting to the current timestamp
(rendered per dialect — see default_now). Splice into a table’s column
list:
migration.create_table(m, “posts”, [ ddl.column(“id”, ddl.Serial) |> ddl.primary_key, ddl.column(“title”, ddl.Text) |> ddl.not_null, ..ddl.timestamps() ])
These are TimestampTz, not Timestamp — the schema-first side reads a
timestamptz column back as spec.UtcDatetime and the codegen emits
temporal.utc_datetime(), so a zone-less pair here drifts against the spec
it generates. Elixir’s Ecto defaults these to :naive_datetime; this
library’s conventions are timezone-aware, so the pair follows suit. For a
zone-less pair, write the two columns by hand with ddl.Timestamp.