Migration squashing

voltro db squash — Rails-style consolidation. Mark every applied-before-T migration as squashed, leave a single snapshot row that new envs boot against.

After 2 years of incremental migrations the history table is hundreds of entries. Fresh environments take minutes to bootstrap. voltro db squash consolidates everything applied before a cut-off date into one synthetic snapshot row.

When to squash

  • A new environment (laptop, staging) takes >30s to boot because of the migration replay.
  • The migration history has hundreds of entries and is becoming unreadable in voltro db plans.
  • You've shipped a major schema overhaul and the pre-overhaul history is no longer useful for debugging.

Don't squash:

  • If you're still iterating on the schema in dev — the history is your audit trail.
  • Right before a major release — wait until the release lands everywhere first.
  • File-based migrations — squashing only affects the declarative- diff (auto-diff) entries. File-based migrations keep their separate rollback path.

Quick start

voltro db squash --before 2026-06-01 --note 'consolidate v1 migrations'
✓ squashed 14 migration plan(s)
  snapshot id: plan_squash_l4f2m1
  fingerprint: sha256:a8f2…

What the command does

  1. SELECT every auto-diff row in _voltro_migration_plans applied before the --before cut-off (where squashedAt IS NULL).
  2. Validates the latest pre-squash row's fingerprint matches the current declared-schema fingerprint. If they diverge, the squash refuses — your working tree carries un-applied changes that would silently be locked in.
  3. Marks all eligible rows squashed by setting squashedAt to the current timestamp. The rows STAY (audit trail preserved); they just no longer participate in boot-time replay.
  4. Inserts a synthetic snapshot row with:
    • source: 'squash-snapshot'
    • fingerprint: <current declared fingerprint>
    • notes: <user's --note>
    • operations: { kind: 'squash-snapshot', rowsSquashed: <N> }

New environments booting against the squashed history skip past the squashed rows + use the snapshot's fingerprint as their starting point.

The fingerprint check

The most common gotcha:

$ voltro db squash --before 2026-06-01

squash refused: latest pre-squash fingerprint (a8f2…) differs from
current declared-schema fingerprint (b3c1…). This means the working
tree carries un-applied changes — squashing now would lock the drift in.

Fix: run `voltro db apply` first to align the live schema with the
working tree, then re-squash.

The squash captures the fingerprint of the most recent applied migration. If your declared schema has uncommitted changes, the snapshot would say "this is the post-squash schema" but the actual schema (live DB) doesn't match. New envs would boot against the snapshot fingerprint, see a different declared fingerprint, and refuse to start.

Resolution: apply pending migrations FIRST, then squash.

CLI flags

Flag Required Description
--before yes ISO-8601 date — rows applied before this point are squashed
--note no Human-readable note recorded on the snapshot row

What happens on next boot

A new env booting against a squashed history:

  1. Reads the latest _voltro_migration_plans row.
  2. If source = 'squash-snapshot', treats its fingerprint as the baseline + skips replaying the squashed entries.
  3. Diffs current declared schema against the baseline.
  4. Applies only post-squash migrations (anything with squashedAt IS NULL AND appliedAt > snapshot.createdAt).

What stays available

  • The squashed rows — still in _voltro_migration_plans. Query them via voltro db plans or read the table directly.
  • File-based migrations — untouched by squash. Their up/down pairs remain available for rollback.
  • Drift detectionvoltro db drift compares the live DB fingerprint against the most recent _voltro_migration_plans row (squash snapshot OR a post-squash entry).

What's GONE

  • Rolling back a squashed migration — the rollback path is one-way once a row is marked squashed. To recover, manually UPDATE the row's squashedAt back to NULL + DELETE the snapshot row. The framework doesn't auto-emit a reverse squash.
  • Replay against a fresh DB — new environments use the snapshot fingerprint, not the original migration sequence. The squashed rows are audit, not replayable.

See also

  • Migration overview — the full plan/apply lifecycle the squash slots into
  • Drift detectionvoltro db drift compares against whichever row is most recent (squash or apply)
  • Soft-drop recovery — the orthogonal "I dropped something and want it back" path