Generators
lode re-expresses Ecto’s and Phoenix’s code generators — mix ecto.gen.migration, mix phx.gen.schema, mix phx.gen.context — without
macros and without a global mix. There is no archive to install: a generator is
a runnable module in your project that you invoke with gleam run -m <task>,
built on the pure functions in lode/gen/schema and lode/migration/gen.
The ready-made runnables live in examples/codegen_demo/src/ — gen_schema.gleam
and gen_context.gleam. Copy them into your own project’s src/ (they’re thin
wrappers over lode/gen/schema) and the commands below work as shown.
The
--separator.gleam runconsumes flags like--standaloneitself, so pass them after a--:gleam run -m gen_schema -- --standalone …. Positional args (records, tables,field:type) need no separator.
gen.schema — a schema + migration
The macro-less mix phx.gen.schema. It parses a <Record> <table> <field:type>…
command line into either a spec entry or a finished module, plus a create_table
migration.
Spec mode (default)
gleam run -m gen_schema Product products name:string price:decimal in_stock:boolean
Writes src/create_products.gleam (the migration) and prints a
spec.table(...) snippet to paste into your db_spec.gleam. Then regenerate the
typed schema modules:
gleam run -m regenerate
This is the §14-pure path: the spec stays the single source of truth, so the
record, Schema(row), accessors, and *Fields are all generated and can’t
drift.
Standalone mode
gleam run -m gen_schema -- --standalone Post posts title:string author_id:references:users
Writes a finished, hand-owned schema module src/post.gleam and the migration
src/create_posts.gleam — Phoenix-style. Use this when a table needs hand-tuning
the codegen can’t express; you then own that file (no spec entry, no regenerate).
Both modes refuse to overwrite an existing file, so re-running is safe.
In standalone mode a
references:becomes a plain foreign-key column only — not abelongs_to— because codegen validates that an association’s table is in the batch, and a one-table generation has none. Add thebelongs_toby hand, or use spec mode where all tables coexist.
gen.context — schema + context + migration
The macro-less mix phx.gen.context. Takes a leading context name:
gleam run -m gen_context Accounts User users name:string email:string:unique
Generates three files:
src/user.gleam— the standalone schema modulesrc/accounts.gleam— the context: CRUD wrappers over the schema —list_users,get_user,create_user/update_user(taking aChangeset),delete_user, andchange_user(thecastbuilder with the schema’s fields)src/create_users.gleam— the migration
It’s standalone-only: the context imports the schema module by name, which is only
predictable for a standalone <snake-record> module.
gen.migration — a migration skeleton
lode/migration/gen gives you the pieces (module_source, file_name); wire
them into a tiny runnable that supplies a timestamp version and writes the file:
// src/gen_migration.gleam
import lode/migration/gen
import simplifile
pub fn main() {
let version = current_utc_stamp() // YYYYMMDDHHMMSS as an Int (e.g. via gleam/time)
let src = gen.module_source(name: "add_users", version: version)
let assert Ok(_) =
simplifile.write(to: "src/" <> gen.file_name("add_users"), contents: src)
}
Note gen.file_name("add_users") is add_users.gleam — no timestamp prefix,
because a Gleam module name can’t start with a digit. Ordering is by the version
inside migration.new(version) and the order of your migrations() list, not
the file name.
The field:type[:modifier] reference
Each field argument is name:type, optionally with a modifier.
type | maps to | Postgres | SQLite |
|---|---|---|---|
string, text | Text | TEXT | TEXT |
integer, int | Integer | INTEGER | INTEGER |
bigint | BigInt | BIGINT | INTEGER |
float | Float | DOUBLE PRECISION | REAL |
boolean, bool | Boolean | BOOLEAN | INTEGER (0/1) |
decimal | Decimal | NUMERIC | TEXT |
date | Date | DATE | TEXT |
time | Time | TIME | TEXT |
naive_datetime | NaiveDatetime | TIMESTAMP | TEXT |
utc_datetime, datetime | UtcDatetime | TIMESTAMPTZ | TEXT |
uuid | Uuid | UUID | TEXT |
map, json, jsonb | Jsonb | JSONB | TEXT |
Modifiers:
name:references:table— a foreign-key column totable(in spec mode, also abelongs_to; the association name is the column minus_id).name:type:unique— adds aUNIQUEindex in the migration.
An auto-incrementing id primary key is always added; you don’t list it.
Where files land, and wiring them up
Generated files are written to src/. Unlike Ecto, lode migrations are
module values, not files in a priv/ directory — so after generating a
migration, add its migration() to your migrations() list (the one you pass to
migrator.run). Run migrations with the migrator entrypoint (see
Getting Started for the migrate.gleam pattern); on
SQLite the migrator picks the dialect automatically (see
Testing with Lode).
Spec mode vs. standalone — which?
- Spec mode when you want the schema-first workflow: one source of truth, no
drift, everything generated. Pair with
regenerateanddrift. - Standalone when a table needs hand-maintenance beyond what the spec expresses — the deliberate manual opt-out (DESIGN §14.6).
See the Divergences from Ecto
generators entry for how these map to mix ecto.gen.* / mix phx.gen.*.