A Python ORM with a Rust core. Models are Pydantic subclasses; schema compiles to SchemaIR and fans out to runtime DDL and the Alembic bridge.
Materialized View:
A PostgreSQL database object that stores the result of a query and is refreshed on demand. In Ferro it is a read-only MaterializedView subclass — queryable through the normal ORM, not writable.
Avoid: Snapshot table, cache table, denormalized table
Refresh:
The explicit operation that repopulates a materialized view from its defining SELECT. Ferro never refreshes at connect time; the user calls refresh() when they want updated data.
Avoid: Auto-refresh, sync, rebuild
Postgres-only schema object: A model artifact that Ferro emits only on PostgreSQL. On SQLite the class still registers for imports and typing, but DDL is skipped and querying raises a clear error. Avoid: Dialect-specific model, PG-only table
Redefine:
Replacing an existing materialized view by dropping and recreating it when its defining SELECT changes. Authorized by migrate_materialized_redefine=True; otherwise connect fails loudly on drift.
Avoid: Alter, migrate, update
Materialized view column:
A flat, typed field on a MaterializedView — same declarations as Model fields, but no ForeignKey, BackRef, or ManyToMany. Reference related entities by scalar columns (order_id: int), not relations.
Avoid: Relation column, FK field
Materialized query:
The ClassVar SQL string (__materialized_query__) that defines what rows a materialized view stores. Declared alongside typed fields; Ferro validates that SELECT output matches the field contract.
Avoid: Select SQL, view definition, query body
Read-only view:
A MaterializedView that can be queried but never mutated. save(), delete(), and create() raise a clear error.
Avoid: Immutable model, snapshot model
Storage token:
A word in the canonical db_type vocabulary (text, bigint, timestamptz, jsonb, …) naming how a column is stored. One shared vocabulary feeds every emitter; a token never means different things to different emitters.
Avoid: SQL type string, dialect type, column type name
Storage lowering:
The dialect-side degrade of a storage token to the nearest type a backend supports (e.g. jsonb stores as plain JSON on SQLite). Lowering is silent and documented; it never changes value semantics — only the on-disk representation.
Avoid: Fallback type, emulation, downgrade
Json-family field:
A field whose values Ferro stores as JSON documents: dict, list (any element type, including nested models), or a nested Pydantic model. Only json-family fields may opt into JSON storage tokens such as jsonb.
Avoid: Object field, blob field, document column
Column spec: The single authoritative record of one column's facts — identity, type, and constraints — derived exactly once from the field declaration. Provisional at class-body time; authoritative once relationship resolution completes. Avoid: Column metadata, enriched schema property, field dict
Relation traversal:
Attribute access on a declared forward-FK field inside a query lambda (lambda t: t.account.ledger_id), reaching a related model's columns from the root. Traversal narrows the result to rows where the relation exists; keeping rows without the relation requires an explicit left join.
Avoid: Join inference, nested filter, path lookup, string path
Relation path:
The ordered sequence of forward-FK hops a traversal walks (account, or account → owner). A path is the identity of a join: the same path referenced anywhere in a query is one join, and distinct paths to the same model are distinct joins. Left-join requests apply to a whole path.
Avoid: Join alias, lookup chain, dotted path string
Existence test:
The only predicate form on a reverse or many-to-many relation — t.lines.exists(...) — asking whether at least one related row exists, optionally scoped by a full inner predicate over the related model. Renders as a correlated EXISTS; the result stays root-shaped (shape-preserving query), so it composes with any other predicate, ordering, and paging. Reverse relations are tested, never traversed: traversal remains a forward-FK concept.
Avoid: membership filter, semi-join filter, reverse traversal, subquery filter, .any()
Shape-preserving query: The invariant that filtering and ordering never change what a query returns — a query over Transaction yields Transaction instances regardless of which relations its predicates traverse. Only an explicit projection operation may change the result shape. Avoid: Implicit projection, row narrowing
Complete-instance invariant: A model instance always carries a complete row — there is no such thing as a partial or deferred-field model instance, anywhere. Anything narrower than a full row (a column subset, an aggregate) comes back as a projected record, never as the model type. Avoid: Partial instance, deferred field, lightweight model, .only()
Projected record:
The result of an explicit projection — a typed record of named values that is not a model instance and cannot be saved, refreshed, or identity-mapped. Column subsets and aggregation results are projected records; complete model rows are not. Realized as Row, delivered in the list-like Rows container.
Avoid: Partial model, row dict, value tuple
Include:
The explicit request (.include(lambda t: t.account.owner)) that a query populate a forward-FK relation path — the data axis of a query, distinct from joins (membership) and projection (shape). Including a path populates every hop along it and never changes which rows come back.
Avoid: Eager load, select_related, prefetch, join-fetch
Populated relation: A forward-FK field carrying its complete related instance, attached by an explicit include on the query. Attribute access returns the instance directly — no await, no query — matching the field's declared type. An unpopulated relation keeps the awaitable contract; population changes cost and attached data, never the result type (there is no separate "loaded" model type). Avoid: Eager-loaded field, select_related, prefetched attribute, joined attribute
Materialization plan: A query's declaration of what its result columns become: complete root instances (every query today), a projected record of named fields, or — in the future — a populated instance graph. Every query carries exactly one plan; the plan travels with the query rather than being inferred from its column list. Avoid: Select list, projection spec, hydration mode flag
QueryIR payload:
The single typed wire artifact a query ships to the Rust runtime — model identity, predicates, ordering, paging, joins, and exactly one materialization plan, inside a versioned envelope. Compiled only by compile_query; no other code assembles query wire shape.
Avoid: Query dict, query def, payload dict
Paging:
The QueryIR window over matching rows: a size (limit) and a start. A start is either an offset or one position bound (after or before, never both). Both bounds are exclusive of the position. A limited before is the adjacent previous page; an unbounded before is every earlier row in declared order. Paging is not a predicate — it does not change which rows match — and count() drops it.
Avoid: pagination, cursor, page filter
Position:
The ordered tuple of a query's order-key values that marks one row's place in that order. Two rows never share a position: the order keys include the model's primary key. A non-PK slot may be empty (None); the PK slot may not. after/before start the page from a position; position_of reads one off a model instance, or off a projected record that carries every order key. Traversed order keys require those relations populated. Not a cursor — encoding is the caller's.
Avoid: cursor, bookmark, page token, keyset
Order key:
One term in a query's order_by: the column (root or traversed), its direction, and its null placement. A position holds one value per order key, in declaration order.
Avoid: sort field, sort column, order term
Null placement:
Where NULL sort keys land for one order key: last, first, or native (that backend's own default — Postgres and SQLite are opposites). Omitted means last, so the same order on every backend. native is never implied.
Avoid: dialect default, omitted nulls
Compiled query:
The single artifact compile_query returns: the QueryIR payload, its wire JSON, and the plan-scoped hop-class map, all views of one compile. The map is collected from the hop facts the payload itself carries, so wire and hop classes can never disagree; it is None unless the materialization plan decodes or hydrates through a hop model's class (mirroring the Rust needs_hop_classes guard — a both-sides double-check). No other code assembles hop classes for the FFI.
Avoid: payload + kwargs, hop-class side-channel, wire tuple
Golden vector: A hand-authored JSON fixture pinning one wire shape — the independent authority both the Python emitter and the Rust decoder assert against, never regenerated from either side's output. Updating one by hand is the contract-review moment for a wire change. Avoid: Snapshot, fixture dump, example payload
Aggregate projection: A projection containing at least one aggregate field. Each group collapses to exactly one projected record: every non-aggregate field is a group key, so grouping is derived from the projection and never declared separately. With no non-aggregate fields, the whole result collapses to a single record. Grouping collapses rows — bucketing complete instances by a key ("partitioning") is a different, client-side operation and is not grouping. Avoid: Group-by query, summary query, rollup, partition
Traversed projection:
A projected record field whose source column lives across a forward-FK relation path (select(lambda t: t.account.name)). Projection traversal narrows exactly like predicate traversal (ADR-0006). Unaliased, the field takes the bare leaf column name; two selected fields sharing an output name is a build-time error, resolved with an output alias.
Avoid: Nested select, join column, related-field pull
Output alias:
A user-chosen name for one field of a projected record, given as the key in a dict-returning selector (select(lambda t: {"account_name": t.account.name})). Aliases name output fields only — never joins or tables; the relation path remains the sole join identity.
Avoid: Column alias, AS label, join alias
Primary key fact:
The single cached answer to "which column is this model's primary key" — derived once at the compile choke point alongside column specs, stored as __ferro_pk__ (None for a PK-less model). At most one primary_key=True column may be declared; a violation raises at class definition time. Operations that need a PK read the cached fact and raise clearly when it is None — never guess.
Avoid: PK lookup, PK scan, first primary-key column
Registry:
The single owner of Python-side registration state (ferro.registry.REGISTRY): model classes, per-model SchemaIR envelopes + fingerprints, join-table bundles, pending relations, the modelset artifact, and the generation counters. Its stores are private; the agreement invariants (fingerprint pairs its envelope, join-table eviction clears envelopes, one-call test reset) live inside the interface, not in caller convention. Routing/session state is not the Registry — that is ferro.state.
Avoid: state module, global dicts, model cache
Provisional registration: The per-model state installed when a class body finishes executing — enough for runtime codec and PK metadata, but relationships may still be pending and the modelset is not yet authoritative for DDL. Avoid: Import-time registration, partial registry
Resolved registration: The registry epoch after relationship resolution completes — join tables exist, shadow FK columns are wired, and the SchemaIR modelset is authoritative for DDL and auto-migrate. Avoid: Final registration, committed registry
Create pass: The auto-migrate step that brings missing tables into existence — the table, its columns, its indexes and constraints, together. A table that already exists is left completely untouched by this pass, whatever its shape. A table a live rename hint says already exists under its old name is not missing, so the pass holds it back, and with it every table that depends on it. Avoid: Bootstrap, ensure-tables, table sync
Reconciliation pass:
The migrate_updates step that alters existing schema objects — tables and ferro-owned enum types — to match the registered models; the only authority for DDL against an object that already exists. It honours a live rename hint by renaming. It changes schema and never rows, so on SQLite, where an enum label lives in rows, a label hint is a warning. Within one table, column changes land before the indexes and constraints that reference them; label additions land before any table's changes.
Avoid: Update pass, schema sync, drift repair
Migration (in-house): The numbered unit of schema-and-data change in ferro's own migration system: what a developer reviews, applies, and reverts as one thing. It is a directory holding one or more ordered steps and its schema snapshot. Numbered sequentially, so numbers count migrations, never steps. Distinct from a generated revision, which is Alembic's unit. Avoid: Change, change set, revision, version
Step: One file inside a migration, applied and recorded on its own so a failure resumes where it stopped. A DDL step is SQL rendered by the same functions the reconciliation pass runs, once per target dialect; a data step is Python and dialect-neutral. Avoid: Operation, phase, sub-migration
Down (of a step): The reverse of a step, kept beside it: what a run executes to revert that step. Every step has one. A DDL step's down returns the schema to what the previous migration's schema snapshot declares, never the rows a drop removed. It is the planner run from the step's after-state to its before-state, limited to what the step changed, so a change the step only reported is never undone. The generated revision's downgrade is made the same way, toward the live database. A down either does work, declares with a reason that there is nothing to reverse, or declares the step an irreversible step. Avoid: Rollback (a transaction rolls back), downgrade (Alembic's word), undo
Irreversible step: A step whose down is a declaration, with a stated reason, that it cannot be reversed. A run that would have to revert it refuses before reverting anything. Only a person declares one; a generated step is never irreversible. Avoid: One-way step, forward-only step, missing down
Data step: A Python step that moves or transforms rows and never changes schema. It sees historical models, never the models in the codebase today, so it keeps working after the codebase moves on. Every data step is either an atomic data step or a chunked data step; there is no third shape, and the step says which it is: neither is assumed. Avoid: Data migration script, RunPython, backfill file
Historical model: A throwaway class a data step queries and saves through, built from a schema snapshot rather than from code: the columns present in either the previous migration's snapshot or the migration's own, which is the table as it stands between that migration's expand and contract steps: a column only the migration adds is nullable there, and a renamed column appears once, under its new name. It carries columns only — no relations, methods or validators. Every table in that union has one, a many-to-many join table included, though no class was ever written for it. Avoid: Frozen model, snapshot model, old model
Atomic data step: A data step that runs as one transaction with its step record committed inside it, so the whole step lands or none of it does. The shape for work small enough to hold in one transaction. Avoid: Transactional step, small data step
Chunked data step: A data step the runner drives in batches over one declared query, one transaction per batch with the cursor committed in it, so an interrupted step resumes at its last completed batch and never replays a row. Avoid: Batched migration, non-transactional step, manual loop
Cursor: The position of the last row of a chunked data step's last committed batch — its order-key values, primary key included — from which the next batch, or the next run, continues. Avoid: Offset, progress marker, checkpoint
Step context: What a data step is handed to do its work: the historical models, raw SQL on the step's own transaction, the target dialect, and a log. It offers no way to open, commit or leave that transaction, and no way to the models in the codebase today. Avoid: Migration context, environment, connection
Unwritten step: A scaffolded step that still lacks what only a person can supply: a data step holding the marker where a value belongs, or a hand-requested SQL step with no statement. A migration holding one is refused when it is loaded, so it never runs and never writes a placeholder. Avoid: Stub, placeholder step, empty step
Guard step: A generated, complete data step that stands where a scaffolded backfill would, written when the developer states that no row needs a value. It changes nothing and fails the migration if such a row exists. Avoid: Assertion step, skipped backfill, no-op step
Backfill:
A scaffolded data step that supplies the values a schema change demands of rows that already exist: a new required column, a column that stops accepting NULL, an enum label rows still carry. It works only on the rows that still need a value, so running it again is exact. It is an unwritten step until a person supplies what the generator cannot know.
Avoid: Data migration, populate step, seed step
Expand step: The generated DDL step that runs before a migration's data steps. It holds only what existing rows already satisfy: a new column created nullable, with its foreign key and check; on a table that already exists the column's index and unique follow the data steps as index steps. A migration with no data step has no expand step; its DDL is one schema step. Avoid: Pre-step, additive step, phase one
Contract step:
The generated DDL step that runs after a migration's data steps. It holds what the rows had to be prepared for (NOT NULL, the removal of an enum label) and every destructive step statement, so a data step can still read a column or table the same migration drops. A migration has one when it has a generated backfill or guard step, or when it drops a column or table behind a data step a person added; a hand-added data step with nothing to contract has none.
Avoid: Post-step, cleanup step, tighten step
Staged NOT NULL:
How a generated Postgres migration makes an existing column required without scanning its table under a lock that blocks writers: an add-constraint step installs a temporary check that refuses NULL on every new write, and the contract step validates it, sets NOT NULL and removes it. Always generated on Postgres for a table that already exists; a table the same migration creates needs none, and SQLite has no equivalent (a table rebuild).
Avoid: Write-side guard, NOT VALID trick, bouncer
Add-constraint step:
The generated Postgres DDL step between a migration's data steps and its contract step that installs the temporary check of a staged NOT NULL and commits, so the contract's validation never runs under the lock the installation takes. Its down removes the check. Not a guard step, which is a data step.
Avoid: Stage step, guard, pre-contract
Staged constraint: How a generated Postgres migration adds a foreign key or a check to a table that already exists without scanning it under a lock that blocks writers: the constraint is installed under its own name as not yet validated, refusing every new write that violates it, and a later step validates it. Always generated on Postgres for a table that already exists, whether or not the column is new; a table the same migration creates needs none; a unique cannot be staged and is an index step instead; SQLite has no equivalent (a table rebuild). A declared constraint found live but not validated is drift. Avoid: NOT VALID trick, deferred constraint, lazy constraint
Validate step: The generated Postgres DDL step that validates a migration's staged constraints when the migration has no contract step to do it in. It is the data-dependent step of that migration; its down returns each constraint to not yet validated. Never holds a table rebuild. Avoid: Verify step, check step, post-schema step
Index step: The generated DDL step that builds or drops one index on a table that already exists, after the migration's data steps. On Postgres it is a no-transaction step that builds without blocking writers, and its first statement removes whatever an earlier failed build left behind under that name, so it is exact from the top; on SQLite it holds the plain index inside a transaction. The step for a unique is the migration's data-dependent step: a duplicate fails it, and nothing is scaffolded to resolve one. An index on a table the same migration creates needs none; a unique is never a constraint, so this is the whole of its online form. Avoid: Concurrent index step, CONCURRENTLY step, index migration
DDL lock timeout: How long a DDL statement that ferro executes on Postgres, in a run or in the reconciliation pass, waits for a table lock before giving up, so a statement queued behind one long query never queues every other query behind itself. A statement that gives up is retried, its step from the first statement, a fixed number of times before the step fails. Set per database in project configuration; distinct from the wait for the run lock, and not applied to a data step. Avoid: Lock timeout (the run-lock wait), statement timeout, lock retry
Restructure scaffold: The generated expand, backfills and contract for a change that replaces a key and everything that references it, spanning the parent table and each child. A primary-key change is the one case today. Avoid: PK migration, key swap, multi-table split
No-transaction step:
A DDL step that declares, in its file, that the runner opens no transaction around it, so statements that refuse to run inside one (CREATE INDEX CONCURRENTLY) can. Because its record can no longer commit with its DDL, it must be safe to re-run from its first statement. A table rebuild is not one.
Avoid: No-tx step, autocommit step, unsafe step
Table rebuild: The SQLite rendering of a schema change SQLite cannot make in place: the table is created again in its new shape under a temporary name, its rows are copied across, and it replaces the old table. It sits inside the DDL step whose change needs it (the expand step, a schema step, or the contract step; never a validate step, add-constraint step or index step), folds every change that step makes to the table, and recreates the table and its indexes as they stand after the step, so one table may be rebuilt once per such step. A step that rebuilds several tables is still one atomic step. It needs foreign-key enforcement off while it runs, which its file declares and the runner arranges. It refuses a live table holding anything the schema snapshot does not declare, since the copy would discard it. Distinct from a constraint rebuild, which touches no rows. Avoid: Batch operation, table recreate, copy-and-move, 12-step
Run:
One invocation of the in-house migration runner against one database, from taking the run lock to releasing it. It goes one way: it applies zero or more pending steps in order, or reverts applied steps in reverse order through their downs, and stops at the first that fails.
In _core a run is held by a TrackedDatabase (read) that becomes a LockedDatabase (write) for the run's length (ADR-0048).
Avoid: Deploy, session, migration (a run applies migrations; it is not one)
Run lock: The lock that admits one run per database at a time. It is held by a run, a baseline, a rerecord and an auto-migrate pass, so none of them changes a database while another does. It is released by the database or the operating system when the running process dies, never by a timeout or an operator, so a crashed run can always be told from a live one. Avoid: Migration lock, lock row, mutex
Tracking table: The table inside a database where the in-house migration system keeps one step record per step it has started there. It says where that database stands now, not what was ever done to it: reverting a step removes its record. Avoid: Ledger, history table, version table, migration log
Step record:
One row of the tracking table: a step that was started on this database, the checksum of the file that was run and of its migration's schema snapshot, and whether it finished. A record that is started and not finished marks where the next run resumes; a chunked data step's record also carries its cursor. A chunked step being reverted keeps its record, marked as reverting and carrying the down's own cursor, until its last batch.
A finished record is applied: status shows applied, applied (different checksum), applied (baseline) or applied (inherited) (the record an inheritance swapped in for the replaced migrations' own). A squashed migration whose replaced migrations the database finished whole shows pending (inheritable): the next up inherits it.
Avoid: Migration row, version row, installed (for a finished step)
Baseline: Recording migrations as applied on a database that already has their schema, built by auto-migrate or Alembic before the project had migrations. Nothing is executed: the database is checked against the schema snapshot of the last migration being recorded, and the step records are written only when it shows no drift. A baselined migration is never reverted by running its down steps, since it created nothing there; undoing a baseline removes the records. Avoid: Fake, stamp, mark-applied
Schema snapshot: The declared modelset as it was when a migration was generated, stored inside the migration and linked to the snapshot before it. It is the previous state the generator diffs against and one of the two states historical models are built from — what the models said, never what the migration's SQL would produce. Avoid: IR dump, state file, history, replayed state
Database (configured): A named set of models whose tables live together, declared in project configuration. It has one migration lineage. Many connections may reach one database, and one database's migrations may be run against many servers: a tenant per server, SQLite locally and Postgres in production. A model belongs to a database by the module that defines it, never by a declaration on the class. Avoid: Alias, connection name, app
Target dialect: A database dialect a project's migrations are generated for, declared in its configuration per database. A migration carries one rendering of each DDL step per target dialect and nothing for any other. Avoid: Backend, supported database, flavor
Rendering (of a DDL step): One target dialect's SQL for a DDL step. Every DDL step has a rendering for every target dialect under the same step number; where a dialect has no work the rendering says so explicitly. Avoid: Variant, dialect file, translation
Schema change: A difference between two schema snapshots that renders DDL. A difference that renders none (a Python default, a back-reference) is not a schema change and generates no migration. Avoid: Model change, diff, IR change
Rename hint: A declaration on a model or enum saying what a column, a table or an enum label was called before. Without one, a rename is indistinguishable from a drop and an add, and generates exactly that. A hint is live only while the previous schema snapshot still holds the old name and lacks the new one; after its migration is generated it is inert and may be deleted. The reconciliation pass and the generated revision have no snapshot and read the same rule off the live database. An enum type's rename needs no hint: it is read off the columns that moved to it. Avoid: Rename marker, rename directive, migration hint
Destructive step: A generated DDL step that discards data when it runs: it drops a column, a table or an enum type. It is always generated and says so in its file; review is the gate, never a flag or a refusal. Avoid: Dangerous step, unsafe step, data-loss migration
Data-dependent step:
A generated DDL step that discards nothing but fails on a database whose rows do not satisfy it: a type change whose cast fails, a NOT NULL, a unique over existing rows, the validation of a staged constraint, the removal of an enum label rows still carry. It says so in its file.
Avoid: Risky step, may-fail step, conditional step
Op verdict: What the planner says about one change on one dialect: whether it runs natively, needs a table rebuild, is refused, or is irreversible (and why), and whether it asks existing rows for values, drops data, or can fail on the rows it meets. Every door reads the same verdict: a migration turns it into steps and headers, a generated revision into markers and refusals. Avoid: Assignment, needs, classification
Plan report:
Something the planner tells its reader that is not a change it makes: a refused rename hint, a leftover ferro check, an extra enum label, a foreign or unverifiable policy. Each has a kind and a subject, and some recur on every run until someone acts. A migration refuses a report that none of its changes answers.
In Python these are the pass's PassReport.warnings (ADR-0049 records the name); the avoid line below is about prose, not that field.
Avoid: Warning (alone), notice, message
Drift: A live database whose ferro-owned artifacts disagree with the schema snapshot of the last migration applied to it. A database behind the newest migration is pending, not drifted. A live table the snapshot does not declare is not drift: it was never ferro's. Drift is reported, never repaired by the migration system and never a generator input. Avoid: Schema mismatch, out-of-sync, dirty database
Pending: A migration or step the project's migrations directory holds and a database's tracking table has no finished step record for. A database with anything pending is behind, which is not drift; an application can refuse to start on it or apply it. Avoid: Unapplied, outstanding, out of date
Squash: Replacing a range of migrations at the head of the directory with the one migration the generator would have written across them: planned once from the schema snapshot below the first to the snapshot of the last, never by stitching their steps together. The replaced migrations' data steps and hand-written SQL steps are carried into it in order, except a generated backfill still byte for byte as the generator scaffolded it that the squashed plan does not scaffold again, which is dropped with a note; their DDL is regenerated. A squash reads snapshots, never the models in the codebase, and never opens a database. Avoid: Merge, collapse, combine, flatten
Range (of migrations):
The migrations a squash replaces: a tail of the directory from the migration --from names through the head, following each snapshot's link to its parent. Always a tail: a migration the head does not descend from is not in it and is refused.
Avoid: run (that is one invocation of the runner), span, window, set
Witness (of a squash): What a squashed migration records about each migration it replaces, so a database that ran the originals can be recognised: the migration's name, its schema snapshot's checksum and the ordinals of its steps. A witness for a migration that was itself a squash carries that squash's witnesses, so the record nests. It is kept with the snapshot and never expires. Avoid: Replaces list, lineage record, provenance
Inheritance (of a squash):
What a run does with a squashed migration on a database whose tracking table shows every replaced migration finished whole under the snapshot its witness names: it swaps those step records for the squashed migration's own and runs nothing. Decided from the tracking table alone, with no flag and no prompt. Not a baseline (the database really ran the steps, and an inherited step's down runs) and not a rerecord (no file was edited). A database that ran only part of the range does not inherit it; it is ahead, and the refusal names how to finish or revert the range. A witness a baseline recorded whole is inherited as a baseline, so down still stops above it; a witness that is part baseline and part run is refused, and the fix is to squash from above the baseline instead (ferro migrate squash <name> --from <first run-applied migration>, on the commit before the squash).
Avoid: Adoption (that word is the baseline cutover of a Tracked database), adopt, fast-forward, stamp, fake-apply, replaces resolution
Ahead: A database whose tracking table holds step records for migrations the running code's migrations directory does not have: an older build of the application meeting a newer schema. Refused by default, and allowed only when the application says so. A database whose records are, whole, the witness of a squash in the directory is not ahead but pending its inheritance; one that holds a witness in part is ahead. Avoid: Newer database, future migration, unknown migration
Migration test harness: What a project's test suite uses to stand a throwaway database at any migration by name, seed it through that migration's historical models, apply or revert one named migration, and walk the whole lineage. It carries the targets and the promptless revert the application's own calls refuse, because a test is about one migration and is deleted with it. It never makes a database fresh, never skips a target dialect, and names no state between two steps of one migration. Avoid: Test runner, fixtures, migration sandbox
Round trip: The test that applies every migration in order, reverts every one, and applies them again, checking after each stop that the database shows no drift against the schema snapshot it should stand at. It proves every down reaches its parent. An irreversible step ends the downward walk at its migration, reported rather than failed: everything above it round-trips, everything below is applied only. Avoid: Up-down test, reversibility check, migration smoke test
Project configuration:
The committed file that declares a project's databases to ferro's tooling: which modules hold the models, the target dialects, where the migrations live. It is one file, never two merged, and never overridden from the environment. Distinct from session settings, which are Postgres values on a Session.
Avoid: Settings (alone), env config, config layers
Generated revision:
Alembic's unit of schema change, written by alembic revision --autogenerate through ferro's bridge. It holds what the reconciliation pass would do to the database it was generated against, with destructive changes included, since it is reviewed before it runs, plus the drop of each live table no model declares that the context's filters admit (the table Alembic itself would drop), so that table's enum type goes with it. Nothing more: an empty one means no drift and no such table. Its downgrade puts a dropped table back as far as ferro can read it. It carries schema changes only; a change that demands values of existing rows is written plain and marked, and its backfill belongs to a migration.
Avoid: Migration (ferro's own unit), autogenerated migration, Alembic migration
Ferro-owned artifact:
A schema object ferro may reconcile to match the declared model. Indexes and constraints are ferro-owned by naming (idx_, uq_, fk_, ck_); native enum types are ferro-owned by derivation — the type's name matches the name ferro derives from the model. Artifacts owned neither way belong to the user and are never altered or dropped.
Avoid: Managed index, system constraint, internal index
Enum label:
One storable value of a native Postgres enum type, mirrored from a Python StrEnum member's value. Members are the Python-side declaration; labels are what the database accepts and stores.
Avoid: Enum value, variant, choice
Label addition: The reconciliation-pass operation appending model-declared labels missing from a live ferro-owned enum type. Append-only and metadata-only: rows are never touched, and labels the database has but the model lacks are warned about loudly and never removed — removal is reviewed-migration territory. A rename is not a label addition: on Postgres the pass renames a label only on a live rename hint. Avoid: Enum sync, label reconciliation, enum evolution
Type drop:
The DROP TYPE that follows the removal of the last column or table declaring a ferro-owned native enum type, after the table changes, on every door and in either direction: a migration's step or down, the reconciliation pass under migrate_destructive, and a generated revision's upgrade() or downgrade(). One planner decides it.
Avoid: Enum cleanup, type teardown, cascade drop
Type creation:
The guarded CREATE TYPE that brings a native enum type into being before any table change that uses it, on every door and rendered by one function. A table operation never creates a type, so a generated revision renders every enum column create_type=False.
Avoid: Explicit enum create, inline type creation, pre-create
Constraint rebuild:
Drop-and-recreate of a ferro-owned constraint whose live definition no longer matches the declared model — a foreign key's on_delete, its target, its columns, or a table check's predicate. Metadata-only: rows are never touched. On a backend that cannot alter constraints, ferro warns loudly and skips; it never diverges silently.
Avoid: Constraint alter, FK patch, in-place constraint update
Column check:
A single-column CHECK that restricts a closed-domain field to its declared labels (Field(db_check=True) → col IN (...)). Named ck_<table>_<col>; not a table check.
Avoid: enum check, db_check constraint, value check
Table check:
A named boolean invariant over one row of one table, declared on the model as a lambda and enforced by the database as a CHECK constraint. The live name is ck_<table>_<suffix>; the suffix is what the model declares.
Avoid: table-level check, multi-column check, composite check, row check
Check predicate:
The lambda body of a table check — a ferro predicate over that model's own columns (or a forward-FK null test on the relation or its shadow *_id), with literal values only. Relation traversal, existence tests, and aggregates are not check predicates.
Avoid: check SQL, constraint expression, check body
Session settings:
Key/value Postgres settings (GUCs) belonging to a ferro Session, applied by ferro to whichever database connection runs each of the session's statements. The unit of tenancy scope for Row-Level Security.
Avoid: Session variables, Postgres session state, connection settings
Settings delivery:
How session settings reach the database: transaction (default — SET LOCAL inside every transaction, implicit ones included; safe behind any pooler) or connection (opt-in — SET once on a pinned connection, reset on close; direct-Postgres only). Delivery is the mechanism; session settings are the values.
Avoid: Pooler mode, GUC mode, SET mode
Operation atomicity:
Every ferro operation is atomic: it issues one statement, or — when it must issue several — runs them inside the ambient transaction when one exists and self-wraps in its own transaction when none does (the bulk_create chunking contract, #298). Settings delivery rides this invariant; it never creates a new atomicity boundary.
Avoid: Implicit transaction, auto-commit batching, per-statement autocommit
Row policy:
One named row-visibility rule on a model, enforced by Postgres as a rls_<table>_<name> policy. Declared as a column/setting shorthand (rendered with NULLIF and the column spec's cast) or a raw expression; scoped to a command; permissive (OR) or restrictive (AND).
Avoid: RLS rule, tenant filter, row filter
Row security declaration:
The table-level RowSecurity(*policies, force=True) ClassVar (__ferro_rls__) — the single owner of a model's row-security facts: its policies plus the table flags (ENABLE, FORCE). Ferro reconciles it one-way: flags and ferro-owned policies are created and rebuilt to match the model, never disabled or dropped outside migrate_destructive.
Avoid: Policy tuple, RLS config, security metadata
Policy rebuild:
Drop-and-recreate of a ferro-owned row policy whose live catalog entry no longer matches the declaration — its command, its permissive/restrictive composition, which clauses it carries, or a body ferro itself rendered. Metadata-only: no row is read, validated, or rewritten. A body the author wrote (the raw using=/with_check= form) is never rebuilt on a textual difference — Postgres stores its own rewriting of raw SQL, so ferro reports the difference with both texts instead (ADR-0019).
Avoid: Policy alter, policy sync, RLS drift repair
Retired column:
A column the model still declares but that this release's code neither reads nor writes: it is absent from the model's attributes and from every statement the model issues, and it stays in the schema snapshot, nullable, so a database built fresh still has it and historical models still see it. Retiring a required column is the change that makes it nullable; nothing else about the column changes until the declaration is deleted, which is the drop: its index, unique, check, foreign key, and every table check, row policy, composite index or unique that names it stay as declared. Only a scalar field or a ForeignKey field can be retired; a back-reference over a retired foreign key cannot exist, a join table is a table.
Avoid: Deprecated column, hidden column, ignored field, soft-dropped column
Retired label:
An enum label whose member stays on the StrEnum so rows that hold it still hydrate, and stays in the native type and in any column check, but that the model refuses to write. How a label leaves an enum old code may still write: the previous release keeps writing it, the next removes it.
Avoid: Deprecated label, removed value, hidden member
Step class:
Whether the previous release's writes keep succeeding beside a step (expand) or not (contract); a write notion, so an added label is expand though the previous release's reads do not know it yet. Decided once by the planner's op verdict for a generated step, declared in the file by the author of a hand-written SQL step (contract unless the file says expand), always expand for a data step. Recorded with the step record when the step is applied, so an ahead database can answer "ahead by what?" to a checkout that lacks the files; a record without one (written by an older ferro) is read as contract.
Avoid: Safety level, compatibility class, risk class
Expand-only (of an ahead database):
The bounded permission an application gives to run beside an ahead database: every step record the checkout lacks must be of step class expand. Narrower than allowing any ahead migration; refused naming the first contract step.
Avoid: Soft ahead, partial ahead, ahead-by-one
Replaced migration:
An unapplied migration regenerated in place against the current newest schema snapshot below it: new snapshot, regenerated DDL steps, every hand-written step carried across by name with its body intact, renumbered when its number is taken. Named in full (NNNN_<name>), since the number alone is what two branches share. The command never reads a database; a database that applied the old files meets the migration as a changed one and must revert it with the files that were applied.
Avoid: Rebased migration, regenerated migration (alone), merged migration
Foreign default:
A server default on a live column that ferro did not leave there. Ferro persists no server default of its own; the ones it leaves are a serial key's sequence and, on SQLite, a literal on a required column (ADR-0034's kept default, told by shape alone: a literal default on a NOT NULL SQLite column is never drift, an expression default or a default on a nullable column is). Every other live default is drift, reported by drift and baseline, removed only under migrate_destructive.
Avoid: Stray default, hand default, unmanaged default
Tracked database:
A database whose tracking table holds at least one step record. It is adopted (not Inheritance, which is a squash's): baseline refuses it, and so does every auto-migrate flag.
Avoid: Migrated database, adopted database (alone), stamped database
Database line:
The first line every ferro migrate verb prints, naming what it touches: the connection URL with its secrets replaced by ***, or, for new and check, which never connect, the migrations directory. Absent from --json output, which is the document alone.
Avoid: Banner, header line, connection line
Change summary:
What new and check say a migration adds, changes, drops or renames, as counted lists of names by kind (models, enum types, and so on). It is data first and prose second: one plain rendering serves check's ungenerated message and the library's error text, and new (in both forms) and check on a terminal lay their output out from the data.
Avoid: Diff, summary string, change list
Statement log:
Every statement ferro sends to a database, each as one record on the ferro.sql logger as it finishes: the SQL with its placeholders, the parameters apart, secrets replaced by ***, the duration, and the operation it belongs to. Queries, mutations, raw SQL, transaction control, session settings, the catalog probe and DDL alike. Turning the logger on is the only switch.
Avoid: Query log, SQL echo, debug SQL, verbose mode
Rendered statement:
What one Query terminal would send, returned by its .sql() without sending it: the same SQL and parameters the statement log records for the call, produced by the same function, for the database the call would run on. Secrets are *** here too.
Avoid: Compiled query (that is compile_query's artifact), SQL preview, dry run, pretty-printed query