lode/schema/spec

The declarative schema spec — the single source of truth for codegen and drift checking (DESIGN §14).

A spec is pure data: no closures, no type erasure. Unlike the runtime Schema(row) (whose codecs are opaque functions), a spec is rich enough to be lossless over the type system — enum variants, embeds, decimal, temporal kinds, uuid-vs-string, jsonb — so the record type, the Schema(row) value, and the typed accessors can all be generated from it with no live database, and a drift checker can compare it against one.

pub fn tables() -> List(spec.TableSpec) { [ spec.table(name: “products”, record: “Product”) |> spec.fields([ spec.primary_key(name: “id”, of: spec.Integer), spec.field(name: “name”, of: spec.Text), spec.field(name: “price”, of: spec.Decimal), spec.field(name: “description”, of: spec.Text) |> spec.nullable, spec.virtual(name: “tagline”, of: spec.Text, default: “""”), ]) |> spec.assocs([ spec.has_many(name: “reviews”, table: “reviews”, fk_column: “product_id”), ]), ] }

Two projections come out of one spec (DESIGN §14.6): the DDL/drift projection (stored_fields — stored columns only, what the database must look like) and the schema projection (schema_fields — all fields, stored + virtual, what the generated record and codecs carry). Virtual fields are invisible to DDL and drift by construction.

Types

Per-association options, mirroring association.Options (DESIGN §14.5). The policy options are plain data; where/preload_order/defaults carry Gleam source expressions (the same code-as-data convention as as_custom and virtual defaults), with imports naming any modules those snippets need. Unset options fall back to association.options() defaults.

pub type AssocOpts {
  AssocOpts(
    where: option.Option(String),
    preload_order: option.Option(String),
    on_replace: option.Option(OnReplaceOpt),
    on_delete: option.Option(OnDeleteOpt),
    defaults: option.Option(String),
    imports: List(String),
  )
}

Constructors

  • AssocOpts(
      where: option.Option(String),
      preload_order: option.Option(String),
      on_replace: option.Option(OnReplaceOpt),
      on_delete: option.Option(OnDeleteOpt),
      defaults: option.Option(String),
      imports: List(String),
    )

    Arguments

    where

    Source of an expr.Expr filtering preloads (child at binding 0).

    preload_order

    Source of a List(query.OrderItem) ordering preloaded children.

    defaults

    Source of a List(#(String, value.Value)) applied to new children.

    imports

    Import lines the source snippets above need.

A named association, declared on the owning table. table references another TableSpec by table name; references (where present) overrides the key column on the one side, defaulting to that table’s primary key.

pub type AssocSpec {
  BelongsTo(
    name: String,
    table: String,
    fk_column: String,
    references: option.Option(String),
    opts: AssocOpts,
  )
  HasMany(
    name: String,
    table: String,
    fk_column: String,
    references: option.Option(String),
    opts: AssocOpts,
  )
  HasOne(
    name: String,
    table: String,
    fk_column: String,
    references: option.Option(String),
    opts: AssocOpts,
  )
  ManyToMany(
    name: String,
    table: String,
    join_through: String,
    join_owner_key: String,
    join_related_key: String,
    opts: AssocOpts,
  )
  HasManyThrough(
    name: String,
    via: String,
    to: String,
    opts: AssocOpts,
  )
}

Constructors

  • BelongsTo(
      name: String,
      table: String,
      fk_column: String,
      references: option.Option(String),
      opts: AssocOpts,
    )

    This table holds fk_column referencing table.

  • HasMany(
      name: String,
      table: String,
      fk_column: String,
      references: option.Option(String),
      opts: AssocOpts,
    )

    Rows of table hold fk_column referencing this table.

  • HasOne(
      name: String,
      table: String,
      fk_column: String,
      references: option.Option(String),
      opts: AssocOpts,
    )

    Like HasMany, but at most one child.

  • ManyToMany(
      name: String,
      table: String,
      join_through: String,
      join_owner_key: String,
      join_related_key: String,
      opts: AssocOpts,
    )

    Linked through a join table. The join table itself is usually declared as a manual_schema table so drift checking covers it.

  • HasManyThrough(
      name: String,
      via: String,
      to: String,
      opts: AssocOpts,
    )

    Two-hop, read-only: reach rows via the via association on this table, then the to association on the intermediate table (Ecto’s has_many :x, through: [:via, :to]). Options apply to the leaf load.

The type intent of a column (or virtual field). Carries everything the database representation erases: enum variants, embedded specs, decimal versus float, temporal kind, uuid versus plain text.

pub type ColumnType {
  Integer
  BigInt
  Float
  Boolean
  Text
  VarChar(Int)
  Binary
  Decimal
  Date
  Time
  NaiveDatetime
  UtcDatetime
  Uuid
  Jsonb
  StringEnum(record: String, variants: List(#(String, String)))
  IntEnum(record: String, variants: List(#(String, Int)))
  EmbedsOne(embed: EmbedSpec)
  EmbedsMany(embed: EmbedSpec)
}

Constructors

  • Integer

    INTEGER (Gleam Int).

  • BigInt

    BIGINT (Gleam Int).

  • Float

    DOUBLE PRECISION (Gleam Float).

  • Boolean

    BOOLEAN (Gleam Bool).

  • Text

    TEXT (Gleam String).

  • VarChar(Int)

    VARCHAR(n) (Gleam String).

  • Binary

    BYTEA (Gleam BitArray).

  • Decimal

    NUMERIC (dee-backed Decimal).

  • Date

    DATE (calendar.Date).

  • Time

    TIME (calendar.TimeOfDay).

  • NaiveDatetime

    TIMESTAMP (#(calendar.Date, calendar.TimeOfDay)).

  • UtcDatetime

    TIMESTAMPTZ (timestamp.Timestamp).

  • Uuid

    UUID (canonical lowercase string).

  • Jsonb

    JSONB (Gleam-side value.Value tree).

  • StringEnum(record: String, variants: List(#(String, String)))

    A string-backed enum stored as TEXT. record is the Gleam type to generate; variants pairs each constructor name with its DB string.

  • IntEnum(record: String, variants: List(#(String, Int)))

    An integer-backed enum stored as INTEGER.

  • EmbedsOne(embed: EmbedSpec)

    A single embedded record stored inline as JSONB.

  • EmbedsMany(embed: EmbedSpec)

    A list of embedded records stored inline as JSONB.

A representation override: keep the column’s DB type for DDL and drift, but map it to a different Gleam type via a custom LodeType. Because generation happens at the source level, the override is expressed as source text: the Gleam type name, the expression building the LodeType, and the import lines both need.

pub type CustomType {
  CustomType(
    gleam_type: String,
    lode_expr: String,
    imports: List(String),
  )
}

Constructors

  • CustomType(
      gleam_type: String,
      lode_expr: String,
      imports: List(String),
    )

A nested schema stored inline (no table of its own). Only name, type_, nullable, and as_type are meaningful on embedded fields — embeds have no keys, no autogeneration, and no virtual fields.

pub type EmbedSpec {
  EmbedSpec(record: String, fields: List(FieldSpec))
}

Constructors

  • EmbedSpec(record: String, fields: List(FieldSpec))

One field of a table (or of an embed). Build with field / primary_key / natural_key / virtual and refine with the modifier functions; the constructor is public so the spec stays inspectable pure data.

pub type FieldSpec {
  FieldSpec(
    name: String,
    type_: ColumnType,
    source: option.Option(String),
    nullable: Bool,
    primary_key: Bool,
    autogenerate: Bool,
    virtual: Bool,
    virtual_default: option.Option(String),
    as_type: option.Option(CustomType),
    redact: Bool,
  )
}

Constructors

  • FieldSpec(
      name: String,
      type_: ColumnType,
      source: option.Option(String),
      nullable: Bool,
      primary_key: Bool,
      autogenerate: Bool,
      virtual: Bool,
      virtual_default: option.Option(String),
      as_type: option.Option(CustomType),
      redact: Bool,
    )

    Arguments

    name

    Field name on the record and in changesets.

    type_

    Type intent. For stored fields this drives DDL/drift and the codecs; for virtual fields only the codecs.

    source

    Database column name when it differs from name.

    nullable

    Whether the column may be NULL. Nullable non-key fields are generated as primitive.NullableValue(_).

    autogenerate

    The database generates the value on insert (serial / identity / DEFAULT); omitted from insert payloads.

    virtual

    App-only field (DESIGN §14.6): on the record and cast in changesets, never in DDL or drift; dump omits it and load seeds it from virtual_default.

    virtual_default

    Gleam source expression seeding a virtual field on load. Required for virtual fields — Gleam records are closed and have no nil.

    as_type

    Optional representation override (see CustomType).

    redact

    Mark sensitive fields; generated code carries a (redacted) marker so readers know not to log them.

Mirror of association.OnDelete as pure spec data.

pub type OnDeleteOpt {
  DeleteNothing
  DeleteAll
  NilifyAll
}

Constructors

  • DeleteNothing
  • DeleteAll
  • NilifyAll

Mirror of association.OnReplace as pure spec data.

pub type OnReplaceOpt {
  ReplaceRaise
  ReplaceDelete
  ReplaceNilify
}

Constructors

  • ReplaceRaise
  • ReplaceDelete
  • ReplaceNilify

One table: its name, the Gleam record type to generate, fields, and associations. Authored explicitly — no name derivation, no pluralization guessing.

pub type TableSpec {
  TableSpec(
    name: String,
    record: String,
    fields: List(FieldSpec),
    assocs: List(AssocSpec),
    manual_schema: Bool,
  )
}

Constructors

  • TableSpec(
      name: String,
      record: String,
      fields: List(FieldSpec),
      assocs: List(AssocSpec),
      manual_schema: Bool,
    )

    Arguments

    name

    Table name in the database.

    record

    Gleam record type name to generate (or, under manual_schema, the type the user hand-writes).

    manual_schema

    Whole-table opt-out (DESIGN §14.6 rung 3): no record/schema/accessors are generated — only the DDL/drift descriptor remains in force, and the user hand-writes the record + Schema(row).

Values

pub fn as_custom(
  field f: FieldSpec,
  gleam_type gleam_type: String,
  lode_expr lode_expr: String,
  imports imports: List(String),
) -> FieldSpec

Map the column to a different Gleam type via a custom LodeType while the column type still drives DDL and drift.

pub fn assoc_defaults(
  assoc a: AssocSpec,
  values source: String,
  imports imports: List(String),
) -> AssocSpec

Default column values for newly-inserted children: values is the Gleam source of a List(#(String, value.Value)).

pub fn assoc_name(assoc a: AssocSpec) -> String

The association’s name (its record field).

pub fn assoc_on_delete(
  assoc a: AssocSpec,
  policy policy: OnDeleteOpt,
) -> AssocSpec

Set the on_delete cascade policy.

pub fn assoc_on_replace(
  assoc a: AssocSpec,
  policy policy: OnReplaceOpt,
) -> AssocSpec

Set the on_replace policy (write-side).

pub fn assoc_opts() -> AssocOpts

No options: every behavior at its association.options() default.

pub fn assoc_preload_order(
  assoc a: AssocSpec,
  order source: String,
  imports imports: List(String),
) -> AssocSpec

Order preloaded children: order is the Gleam source of a List(query.OrderItem); imports lists the modules it needs.

pub fn assoc_where(
  assoc a: AssocSpec,
  expr source: String,
  imports imports: List(String),
) -> AssocSpec

Filter preloads of this association: expr is the Gleam source of an expr.Expr (child at binding 0); imports lists the modules it needs.

pub fn assocs(
  table t: TableSpec,
  assocs as_: List(AssocSpec),
) -> TableSpec

Set the table’s associations.

pub fn belongs_to(
  name name: String,
  table table: String,
  fk_column fk_column: String,
) -> AssocSpec

Declare a belongs_to (this table holds the foreign key).

pub fn column_name(field f: FieldSpec) -> String

The database column name backing a field (source override or name).

pub fn embed(
  record record: String,
  fields fields: List(FieldSpec),
) -> EmbedSpec

Build an embedded-schema spec.

pub fn field(
  name name: String,
  of type_: ColumnType,
) -> FieldSpec

A plain stored field (NOT NULL, not a key).

pub fn fields(
  table t: TableSpec,
  fields fs: List(FieldSpec),
) -> TableSpec

Set the table’s fields.

pub fn find_field(
  table t: TableSpec,
  name name: String,
) -> Result(FieldSpec, Nil)

Find a field by name on a table.

pub fn find_table(
  tables tables: List(TableSpec),
  name name: String,
) -> Result(TableSpec, Nil)

Find a table spec by table name.

pub fn has_many(
  name name: String,
  table table: String,
  fk_column fk_column: String,
) -> AssocSpec

Declare a has_many (the related table holds the foreign key).

pub fn has_many_through(
  name name: String,
  via via: String,
  to to: String,
) -> AssocSpec

Declare a has_many ... through two existing associations.

pub fn has_one(
  name name: String,
  table table: String,
  fk_column fk_column: String,
) -> AssocSpec

Declare a has_one (the related table holds the foreign key).

pub fn manual_schema(table t: TableSpec) -> TableSpec

Opt the whole table out of generation: keep it in the spec for DDL/drift, hand-write the record + Schema(row).

pub fn many_to_many(
  name name: String,
  table table: String,
  join_through join_through: String,
  join_owner_key join_owner_key: String,
  join_related_key join_related_key: String,
) -> AssocSpec

Declare a many_to_many through join_through.

pub fn natural_key(
  name name: String,
  of type_: ColumnType,
) -> FieldSpec

An application-supplied (natural) primary key.

pub fn nullable(field f: FieldSpec) -> FieldSpec

Allow NULL. Non-key nullable fields generate as NullableValue(_).

pub fn opts_of(assoc a: AssocSpec) -> AssocOpts

This association’s options.

pub fn primary_key(
  name name: String,
  of type_: ColumnType,
) -> FieldSpec

A database-generated primary key (serial / identity).

pub fn primary_key_columns(table t: TableSpec) -> List(String)

Column names of the primary key (stored fields only), in declared order.

pub fn redacted(field f: FieldSpec) -> FieldSpec

Mark the field as sensitive.

pub fn references(
  assoc a: AssocSpec,
  column column: String,
) -> AssocSpec

Override the referenced column on the one side (defaults to that table’s primary key). No-op for many_to_many/through.

pub fn resolve_through(
  tables tables: List(TableSpec),
  table t: TableSpec,
  via via: String,
  to to: String,
  context here: String,
) -> Result(#(AssocSpec, TableSpec, AssocSpec, TableSpec), String)

Resolve a through chain: the via association on t names the intermediate table; the to association on that table names the leaf. Returns #(via_assoc, mid_table, to_assoc, leaf_table).

pub fn schema_fields(table t: TableSpec) -> List(FieldSpec)

The schema projection: every field (stored + virtual) with its app representation. What the generated record and codecs carry.

pub fn snake(record record: String) -> String

Snake-case a PascalCase record name (for generated function prefixes: "ProductReview" -> "product_review").

pub fn stored_fields(table t: TableSpec) -> List(FieldSpec)

The DDL/drift projection: stored columns only. What the live database is expected to look like; virtual fields are excluded by construction.

pub fn table(
  name name: String,
  record record: String,
) -> TableSpec

Start a table spec.

pub fn validate(
  tables tables: List(TableSpec),
) -> Result(Nil, String)

Check a spec set for the structural mistakes the type system can’t catch: duplicate names, dangling association references, missing virtual defaults, virtual fields posing as keys or columns, unresolvable through chains. Generators and the drift checker call this first.

pub fn virtual(
  name name: String,
  of type_: ColumnType,
  default default: String,
) -> FieldSpec

An app-only field: on the record and cast in changesets, never stored. default is the Gleam source expression that seeds the field when a row is loaded (e.g. "\"\"" for an empty string, "0", "primitive.Absent").

pub fn with_autogenerate(
  field f: FieldSpec,
  value value: Bool,
) -> FieldSpec

Override whether the database generates the value on insert (for non-key generated columns: a DEFAULT now() timestamp, a non-key identity).

pub fn with_source(
  field f: FieldSpec,
  column column: String,
) -> FieldSpec

Override the database column name (the record keeps name).

Search Document