lode/codegen
Schema-first code generation (DESIGN §14).
The hand-authored lode/schema/spec is the single source of truth:
generate_from_spec turns it into ready-to-compile modules — record
types, Schema(row) values with load/dump codecs, typed FieldRef
accessors, association registrations, and preload constructors — with
no live database. Enums and embedded schemas declared in the spec are
emitted alongside; virtual fields land on the record and in changeset
casting but never in dump or the accessors.
Database introspection is retained for two purposes only:
- bootstrap:
bootstrap_specemits a spec module from an existing database (best-effort on type intent — the database can’t express enums, embeds, or virtual fields), andgeneratestill produces modules straight from a database by routing introspection through the same spec emitters; - verification:
lode/driftcompares the spec’s DDL projection against a live database usingintrospect/introspect_foreign_keys.
Introspection is engine-aware: information_schema on Postgres,
sqlite_master + PRAGMA table_info/foreign_key_list on SQLite (where
the schema argument is ignored; pass "main").
let assert Ok(files) = codegen.generate_from_spec(db_spec.tables()) // files : List(#(filename, source)); write them where you like, or: let assert Ok(_) = codegen.generate_from_spec_to_dir(db_spec.tables(), “src/myapp/db”)
Types
pub type ColumnInfo {
ColumnInfo(
name: String,
data_type: String,
nullable: Bool,
primary_key: Bool,
autogenerate: Bool,
)
}
Constructors
-
ColumnInfo( name: String, data_type: String, nullable: Bool, primary_key: Bool, autogenerate: Bool, )Arguments
- data_type
-
The database’s reported type, lowercased: the
information_schemadata_typeon Postgres; the declared column type on SQLite. - autogenerate
-
Database generates the value on insert (serial / identity / has default).
A foreign key: child_table.fk_column references parent_table.parent_column.
pub type ForeignKey {
ForeignKey(
child_table: String,
fk_column: String,
parent_table: String,
parent_column: String,
)
}
Constructors
-
ForeignKey( child_table: String, fk_column: String, parent_table: String, parent_column: String, )
pub type TableInfo {
TableInfo(name: String, columns: List(ColumnInfo))
}
Constructors
-
TableInfo(name: String, columns: List(ColumnInfo))
Values
pub fn bootstrap_spec(
repo repo: repo.Repo,
schema schema_name: String,
) -> Result(String, error.LodeError)
One-shot importer for adopting the schema-first workflow on an existing
database: introspects schema_name and renders a spec module source
(pub fn tables() -> List(spec.TableSpec)). Best-effort on type intent —
the database can’t express enums, embeds, decimal precision, or virtual
fields, so refine the emitted spec by hand. From then on the spec is the
source of truth: generate_from_spec emits modules and lode/drift
verifies the database against it.
Works on Postgres and SQLite (pass schema: "main" on SQLite — the
argument is ignored there). On SQLite, type intent is limited to what the
storage classes preserve — temporal/uuid/json columns introspect as Text.
pub fn generate(
repo repo: repo.Repo,
schema schema_name: String,
) -> Result(List(#(String, String)), error.LodeError)
Generate modules straight from a live database (introspection routed
through the spec emitters). This is the bootstrap convenience; the
schema-first workflow is bootstrap_spec once, then generate_from_spec.
pub fn generate_from_spec(
tables tables: List(spec.TableSpec),
) -> Result(List(#(String, String)), error.LodeError)
Generate #(filename, source) modules from a hand-authored spec — no live
database. Tables are grouped by the connected components of the association
graph: isolated tables get their own module (clean, unprefixed names); any
cluster of associated tables shares one module (with record-prefixed names)
so the mutually-recursive records and registrations stay intra-module —
Gleam forbids circular imports across modules.
manual_schema tables generate nothing (the user hand-writes their record
and Schema(row)); they stay in the spec for the drift checker.
pub fn generate_from_spec_to_dir(
tables tables: List(spec.TableSpec),
dir dir: String,
) -> Result(List(String), error.LodeError)
generate_from_spec, then write one .gleam file per module into dir.
pub fn generate_module(
table: TableInfo,
foreign_keys: List(ForeignKey),
) -> String
Generate the Gleam source for a single table. foreign_keys is the full FK
list for the schema; the table’s association names become preload-name
constructors, but no association fields or registrations are emitted —
register them by piping onto the generated schema() (use generate /
generate_from_spec for fully-wired modules).
pub fn generate_to_dir(
repo repo: repo.Repo,
schema schema_name: String,
dir dir: String,
) -> Result(List(String), error.LodeError)
Generate from a live database and write one .gleam file per module into
dir. Returns the paths.
pub fn introspect(
repo repo: repo.Repo,
schema schema_name: String,
) -> Result(List(TableInfo), error.LodeError)
Introspect all base tables, returning their columns.
Engine-aware: on Postgres this reads information_schema for the base
tables in schema_name; on SQLite it reads sqlite_master plus
PRAGMA table_info (SQLite has no schemas, so schema_name is ignored —
pass "main"). Engine bookkeeping stays invisible on both: pg_catalog
never appears in information_schema, and sqlite_* internals (e.g.
sqlite_sequence) are excluded here to match.
pub fn introspect_foreign_keys(
repo repo: repo.Repo,
schema schema_name: String,
) -> Result(List(ForeignKey), error.LodeError)
Introspect foreign keys. Used to derive associations. Engine-aware like
introspect (schema_name is ignored on SQLite).
pub fn render_spec_module(
tables tables: List(spec.TableSpec),
schema schema_name: String,
) -> String
Render a spec set back to Gleam source (the inverse of authoring by hand).
pub fn table_specs(
tables tables: List(TableInfo),
foreign_keys fks: List(ForeignKey),
) -> List(spec.TableSpec)
Best-effort spec for introspected tables: column types from the database’s
reported type (Postgres data_type / SQLite declared type; intent the
database can’t express — enums, embeds, virtuals — must be added by hand),
belongs_to/has_many associations from the foreign-key graph.