Skip to content

Schema from an ORM

llms.txtlists every page for an agent
Optional: hand this page to your coding agentThe steps work by hand too.
Show the whole prompt
Bring the tables our ORM defines into SQL Yodeler, following https://intentius.io/sql-yodeler/orm/: add a source under `sources` in yodel.config.ts with the command that prints the ORM's DDL, and remove any declaration in src/ of a table the ORM now owns.
Run `npx yodel new <name>` and `npx yodel lint`, and open a pull request with the config, src/ and the new migration.
Never run `yodel apply` against a shared environment, never run `chant approve`, `yodel approve` or `yodel override`, never edit the `chant/lifecycle` branch or `.chant/allowed_signers`, never merge; approvals and applies belong to people.

When an ORM defines some of the tables, yodel can read them from the DDL the ORM prints instead of having them rewritten as declarations. The ORM’s tables then go through the same plan, approval and apply as the ones in src/.

The simplest way to keep schema as DDL is a .sql file in src/. chant’s build reads every .sql file in the source directory beside the TypeScript declarations, so yodel needs no configuration for it:

-- src/billing.sql
CREATE TABLE app.invoices (
id bigint PRIMARY KEY,
customer_id bigint NOT NULL REFERENCES app.customers (id),
amount numeric(12, 2) NOT NULL
);
CREATE INDEX invoices_customer ON app.invoices (customer_id);

A file reads the same statements a DDL source does (below). A name in it that another object declares, in a .sql file or in TypeScript, is a dependency, so the plan creates app.customers first. An object declared in a file and in a template is a build error naming both. A file whose first line is -- chant-discovery-skip is not read, for seed data or queries kept beside the schema.

Use a file when you write or paste the DDL yourself. Use a DDL source when a command prints it, such as an ORM’s, so the declarations follow the model each time yodel builds.

Name each source in yodel.config.ts under sources, with the command that prints its DDL or a file that holds it:

import { defineConfig } from "@intentius/sql-yodeler";
export default defineConfig({
sources: {
prisma: {
command: "npx prisma migrate diff --from-empty --to-schema-datamodel prisma/schema.prisma --script",
schema: "app",
},
legacy: { file: "db/legacy.sql" },
},
});
Setting What it is
command a shell command, run in the project directory, that prints the DDL on stdout
file a file of DDL, relative to the project directory
schema the Postgres schema (ClickHouse database) for the names the DDL leaves unqualified, as an ORM does when its connection picks the schema

Give one of command and file. A source’s name is lowercase letters, digits and -.

Commands that print an ORM’s DDL:

ORM Command
Prisma npx prisma migrate diff --from-empty --to-schema-datamodel prisma/schema.prisma --script
Drizzle npx drizzle-kit export (or drizzle-kit generate, then a file source on the SQL it wrote)
Django python manage.py sqlmigrate <app> <migration>, one source per app
SQLAlchemy a script that prints CreateTable(table).compile(engine) for each table of the metadata

Before each build (yodel plan, yodel new, yodel apply, yodel drift), yodel runs each source’s command or reads its file, parses the DDL with the sql lexicon, and writes it out as declarations in src/<name>.generated.ts: the same file chant import writes, with a header saying where it came from. The build reads that file like any other declaration, so a model change shows up in yodel plan as the change it makes to the table, and the ApplyOp’s own build finds it too.

Commit the generated file. A build that yodel does not start, such as chant build or an Op run outside yodel, reads the committed copy.

Keep a file source’s DDL out of the schema build. chant’s build reads every .sql file in the directory it builds, and an ApplyOp’s diff builds the project root unless chant.config.ts sets sourceDir. A DDL file there would declare each object a second time, beside the generated file. Set sourceDir: "src" in chant.config.ts, or start the file with a -- chant-discovery-skip line.

yodel reads the DDL with the sql lexicon’s sqlFileDeclarations, the reader chant’s build uses for a .sql file in src/, so a source and a file read the same statements. On Postgres that is each CREATE, GRANT, REVOKE and ALTER DEFAULT PRIVILEGES, with COMMENT ON and ALTER TABLE ... ROW LEVEL SECURITY folded into the object they finish. ORMs print foreign keys as ALTER TABLE <t> ADD CONSTRAINT ... FOREIGN KEY ...; each ALTER TABLE ... ADD of a table constraint is folded into <t>’s CREATE TABLE, where a declaration holds it. BEGIN and COMMIT are skipped. On ClickHouse, it reads CREATE DATABASE, TABLE, VIEW, MATERIALIZED VIEW, DICTIONARY and FUNCTION.

Any other statement is an error that names it, and nothing is built. Leaving it out would leave part of the ORM’s schema unmanaged without saying so.

With schema set, the unqualified names are qualified with it: on Postgres, each object’s own name, an index’s table, a foreign key’s table, and a column whose type is an enum or domain the same DDL creates; on ClickHouse, each object’s own name except a function’s, which belongs to no database.

An object is declared in one place. If a source creates a table that src/ (or another source) also declares, the build stops and names both:

$ yodel plan dev
yodel plan: each object has one owner, and app.customers is declared twice: by src/ (export customers) and by the source "prisma" (src/prisma.generated.ts). Leave it to one of them.

The same holds when src/ exports the object under another name: yodel reads each built object’s name and refuses the build when a source creates it too.

The declarative claim runs a project whose yodel.config.ts has a source whose command prints a Prisma-style DDL file (the claim installs no ORM): the first apply creates the ORM’s tables behind the gate, a column added to the model is the one change yodel plan shows, the approved apply adds it, and the plan is then empty. Under BREAK=1 the DDL changes again after the approval, and the column is not applied.

The scenario claims below run what this page describes against a real server, once plain (it passes) and once with the behaviour broken (the claim catches it). Claims status lists every claim.

Claim What it says Plain, broken Last run
declarative the declarative path: yodel plan shows the change against the live server and yodel apply makes it behind the plan-bound gate, for src/ declarations (on Postgres, functions, procedures and triggers, and access control: a role, a policy and grants, with a grant made by hand revoked; on ClickHouse, a dictionary, a function, and access control: a role, a user, a row policy and grants, with a grant made by hand revoked) and for an ORM’s exported DDL ClickHouse: pass, caught; Postgres: pass, caught c6f58a4, 2026-10-10

SQL Yodeler