QAIL.rs v1.2.1 is a small version number with a very practical meaning: the Rust line now handles richer real PostgreSQL schema pulls, keeps corrupted protocol state out of normal connection reuse, and documents migrations around expand, backfill, and contract instead of treating up/down as the primary operating model.
What Changed Across 1.1.1, 1.2.0, and 1.2.1
- v1.1.1 hardened live PostgreSQL migration introspection and shadow verification around generated columns, identity defaults, expression indexes, enum extensions, and composite foreign-key drift.
- v1.2.0 hardened PostgreSQL protocol behavior so COPY, LISTEN/NOTIFY, replication streaming, pooled fetch, driver fetch, query, and pipeline paths fail closed on malformed backend state.
- v1.2.0 also tightened NUL and UTF-8 handling across savepoints, SQL rendering, AST SQL buffers, gateway explain SQL, COPY text decoding, backend wire decoding, and URL percent-decoding.
- v1.2.1 fixed schema parser compatibility for pulled PostgreSQL details: table-level enable_rls and force_rls directives, multi-word types, and comments containing quoted examples.
The release is not about adding another convenience API. It is about making the current API survive real database shape, real protocol failures, and real deployment sequencing.
The Schema Parser Now Accepts What Pull Produces
The parser changes are intentionally narrow. Pulled schemas can now include table-level RLS state without those directives being misread as columns. Multi-word PostgreSQL types are parsed as one type prefix. Comment text can include quoted examples without being rejected as trailing content.
table car_fullday_reseller_pricing {
id UUID
tenant_id UUID
percentage_markup DOUBLE PRECISION
starts_at TIMESTAMP WITH TIME ZONE
enable_rls
force_rls
}
comment on pickup_zones.ribbon_color "Hex color (e.g., \"#f97316\" for orange)"
In the implementation, this is handled by parsing the longest valid column-type prefix before processing column options. That matters because DOUBLE PRECISION and TIMESTAMP WITH TIME ZONE must be treated as types, not as a type plus stray modifiers.
Fail-Closed Means Do Not Reuse a Suspect Connection
The PostgreSQL driver hardening in v1.2.0 follows a simple rule: if protocol ordering, stream framing, unexpected EOF, invalid UTF-8, NUL bytes, or COPY/replication state makes a connection suspect, mark it desynchronized instead of quietly putting it back into normal reuse.
- COPY errors and callback recovery paths mark bad connections as desynchronized.
- Query, fetch, pool fetch, and pipeline paths mark protocol-order failures as desynchronized.
- Replication stream regressions mark the connection as desynchronized.
- LISTEN/NOTIFY cleanup failure is treated as session-state risk, not a harmless warning.
- AST SQL encoding rejects NUL bytes before sending SQL to PostgreSQL.
- Gateway database URL parsing rejects invalid UTF-8 in userinfo instead of lossy-decoding credentials.
How We Handle Migrations Now
The current operational model is not old-style up/down as the normal story. The normal story is phased apply from deltas/: expand first, backfill second, contract last. Rollback still exists, but it is explicit history repair or explicit down-file execution, not the default framing for safe forward deployment.
deltas/
20260527090000_add_user_name/
expand.qail
backfill.qail
contract.qail
# 1. Add compatible schema. Do not break old code yet.
qail migrate apply --phase expand
# 2. Move data in resumable chunks.
qail migrate apply --phase backfill --backfill-chunk-size 10000
# 3. Remove the old shape only after code references are gone.
qail migrate apply --phase contract --codebase ./src
What Each Phase Is For
- Expand is for additive, compatible changes: new nullable columns, new tables, new indexes, or compatibility structures that old and new code can both tolerate.
- Backfill is for data movement. The implementation supports backfill directives, chunk size selection, checkpointing, and resumable progress tracking through _qail_backfill_checkpoints.
- Contract is for destructive cleanup: dropping old columns, dropping old tables, or removing compatibility structures. This phase should run only after code references have been removed.
- The migration discovery code sorts each migration group by phase order: expand, then backfill, then contract.
- The contract safety guard scans the codebase and blocks dropped tables or columns when live references remain, unless an explicit override is used.
How Backfill Directives Work
Backfill files are intentionally directive-driven. The file tells QAIL which table and primary key to walk, which column to set, which source column to read from, and how to transform values. The runner checkpoints progress so a long migration can resume instead of starting over.
-- @backfill.table: users
-- @backfill.pk: id
-- @backfill.set_column: name_ci
-- @backfill.set_source: name
-- @backfill.set_transform: lower|trim
-- @backfill.where_null: name_ci
-- @backfill.chunk_size: 10000
How Contract Safety Fails
The contract phase is allowed to be destructive, but it is not allowed to be casual. If a contract migration drops a table or column, QAIL parses the migration SQL, scans code references, and blocks when QAIL or raw SQL references still point at the dropped shape.
qail migrate apply --phase contract --codebase ./src
# If old references still exist:
# Contract migration '20260527090000_add_user_name/contract.qail' blocked:
# detected live references to dropped fields/tables.
Operational Guidance for This Release
- Regenerate docs and website references from qail.rs after bumping workspace crates to 1.2.1.
- Stop presenting qail migrate up/down as the current migration model in public docs and examples.
- Use deltas/ with expand.qail, backfill.qail, and contract.qail for deployable schema changes.
- Run contract with --codebase so dropped field and table references are caught before cleanup.
- Treat malformed PostgreSQL protocol state as a connection-lifecycle event. A desynchronized connection should be discarded, not reused.
- When a pulled schema includes enable_rls, force_rls, DOUBLE PRECISION, TIMESTAMP WITH TIME ZONE, or quoted comment examples, keep those details in the schema file. They are now part of the accepted input surface.
The Bottom Line
QAIL.rs v1.2.1 makes the stable Rust line more honest about production reality. Real PostgreSQL schemas are messy, driver protocols can desynchronize, and safe migrations are staged, not magical. The current model handles that by accepting the schema details pull actually emits, failing closed when wire state is suspect, and requiring expand/backfill/contract discipline for forward migration.
← Back to Blog