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

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
  • Time

    A time of day with no date (TIME / :time).

  • Timestamp

    A zone-less instant (timestamp without time zone / :naive_datetime).

  • TimestampTz

    A timezone-aware instant (timestamptz / :utc_datetime). The value layer already binds VTimestamp as timestamptz and reconstructs it on load whichever wire shape the driver returns, so this only changes the declared column type — which is what lets a spec.UtcDatetime field’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 renders TEXT: its adapter stores a VList param as JSON text, which composite.array parses back on load. The element must be a plain data type — Serial/BigSerial make 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'").

  • Now

    The 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; SQLite INTEGER PRIMARY KEY AUTOINCREMENT). BigSerial uses big_serial_primary_key.

    big_serial_primary_key

    As serial_primary_key, for BigSerial (Postgres BIGSERIAL PRIMARY KEY; SQLite shares the single INTEGER PRIMARY KEY AUTOINCREMENT).

    now

    The SQL expression a Now default renders to (after DEFAULT ): now() on Postgres; on SQLite a strftime expression producing the same RFC-3339 TEXT shape the SQLite adapter writes for a Timestamp.

    table_suffix

    Appended verbatim after CREATE TABLE (...)’s closing paren: empty on Postgres, STRICT on 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

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

  • NoAction

    NO 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 not_null(col: Column) -> Column

Mark a column NOT NULL.

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 postgres() -> Dialect

The PostgreSQL dialect: SERIAL/JSONB/BOOLEAN/etc.

pub fn primary_key(col: Column) -> Column

Mark a column as the primary key (implies NOT NULL).

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.

pub fn to_sql(op: Operation, dialect dialect: Dialect) -> String

Render an operation to a single SQL statement in the given dialect.

Search Document