Migrating to v0.12.0¶
v0.12.0 is the first public release built on Ferro's IR-first architecture.
Query execution, schema/migration planning, codecs, hydration, and connection
routing now flow through one shared intermediate representation (IR) instead of
several independent code paths.
Why this matters¶
A single source of truth removes whole classes of drift bugs:
- Predictable schema diffs. Runtime DDL and the Alembic autogenerate bridge derive from the same IR, so migrations stop proposing phantom changes.
- Typed query execution. Queries compile through typed IR rather than ad-hoc JSON, so bind and null semantics behave the same on SQLite and PostgreSQL.
- Explicit runtime state. Connection and transaction routing are scoped to a session you control, instead of hidden process-global state.
Your model definitions do not change. The migration below is about how you call a few APIs, not how you declare your data.
What you need to do¶
v0.12.x ships a compatibility window: the older call styles still work,
but each one now emits a DeprecationWarning whose message ends with
Planned removal in v0.14.0. Treat v0.12.x and v0.13.x as your window to migrate before the
old surfaces are removed in v0.14.0.
Turn deprecation warnings into failures on a migration branch so nothing slips through:
Then work through the three changes below.
1. Use lambda predicates in where()¶
Operator-style predicates (Model.field == value) are deprecated. They never
type-checked cleanly — static checkers read User.age >= 18 as a bool, while
where() expects a QueryNode — and they now warn at runtime. Lambda
predicates are the recommended style; col() is a type-safe bridge when you
want to keep the operator shape on a single comparison.
2. Run operations inside a session¶
Calling unqualified operations (User.all(), ferro.execute(...)) without an
active session relied on implicit default-connection routing. That fallback is
deprecated. Wrap request- or task-scoped work in a session so routing, the
identity map, and transactions are explicit and isolated under concurrency.
After a single unnamed connect(dsn), async with ferro.engines.session() (no
argument) binds to that default connection — you do not need to pass
"default" explicitly.
Need to target a specific connection from inside another session? Pass
session= explicitly — it overrides the ambient session.
3. Build Alembic metadata from get_metadata()¶
The private JSON-derivation helpers ferro.migrations.alembic._build_sa_table
and ferro.migrations.alembic._map_to_sa_type are deprecated. Schema metadata
now derives from the IR through the public get_metadata() entry point — use it
directly in your Alembic env.py.
Deprecated surfaces at a glance¶
| Deprecated surface | Replacement | Removed in |
|---|---|---|
Model.where(Model.field OP value) |
where(lambda t: ...) or col(Model.field) |
v0.14.0 |
| Unqualified ORM/raw operations outside an active session | async with ferro.engines.session("name") or explicit session= |
v0.14.0 |
ferro.migrations.alembic._build_sa_table |
ferro.migrations.get_metadata() |
v0.14.0 |
ferro.migrations.alembic._map_to_sa_type |
ferro.migrations.get_metadata() |
v0.14.0 |
Verifying your migration¶
Once your call sites are updated, confirm no deprecation warnings remain on the paths you exercise:
A clean run means your codebase is ready for v0.14.0, where these
compatibility shims are removed.