16. Migration lifecycle
The migration engine compares a live catalog with the desired catalog:
let current = CatalogInspector::new(&pool).inspect().await?;
let plan = MigrationPlan::diff_for(
Dialect::Sqlite,
¤t,
&desired,
DiffOptions::default(),
);
let report = apply_plan(
&pool,
Dialect::Sqlite,
&plan,
ApplyOptions::default(),
)
.await?;The chapter applies a table creation, inspects again, and requires the second diff to be empty. It then renames both the table and one column while preserving an existing row. In an authored RSL schema, carry that identity explicitly during the rename:
model Member {
id BigInt @id
fullName String @map("full_name") @rent.previousName("name")
@@map("members")
@@rent.previousName("users")
}Without @rent.previousName, removing one name and adding another is ambiguous and may be classified as destructive. Keep the annotation for the migration that performs the rename; remove it in a later migration after every environment has applied the change.
Preflight detects an unambiguous drop/add pair and prints the exact annotation to add:
Possible rename: public.users.display_name → name
Preserve its data with @rent.previousName("display_name") on `name`.
Review required: Destructive changes need explicit approval.This is guidance rather than an automatic guess: Rent does not risk data by silently deciding that two unrelated columns are the same object.
The executable chapter then performs a complete expand, backfill, and contract migration on its populated table. It edits an authored SQL draft, validates it before sealing, executes the checksummed script, explicitly approves the reviewed contract operation, makes the column required, and proves the resulting schema reaches another fixed point.
It also proves a destructive reversal is rejected without approval and renders a schema-qualified PostgreSQL table. The CLI wraps the same engine with checksummed migration directories:
rent migrate preflight
rent migrate dev --name rename_users
rent migrate diff
rent migrate plan --name add_posts
rent migrate validate
rent migrate apply
rent migrate reset --yesBackfill existing rows safely
Adding a required column to a populated table normally takes three releases:
- Expand: add the column as nullable so old and new application versions both work.
- Backfill: populate every existing row in a separately authored data migration.
- Contract: make the column required only after the backfill has been verified everywhere.
Create and seal the middle migration explicitly:
rent migrate create --name backfill_member_slugs
# Edit the new SQL file, then freeze its reviewed checksum.
rent migrate seal
rent migrate validate
rent migrate applyThe chapter and social-network system application execute the complete expand, backfill, and contract sequence.
The social application repeats it against SQLite, PostgreSQL, MySQL, and MariaDB. The CLI's nextest contract also
edits a draft, proves checksum drift, validates before modifying rent.sum, seals the authored SQL, and rejects
destructive content unless it is explicitly approved.
The desired application model is captured independently in
crates/rent/examples/tutorial_15_migrations/schema.rsl.
Run cargo run -p rent --example tutorial_15_migrations. The same program runs under nextest, so table and column
identity, row preservation, fixed-point inspection, safety classification, and SQL rendering remain executable
documentation.