Soft-drop recovery
VOLTRO_SOFT_DROP=1 renames dropped columns instead of deleting them. voltro db restore-snapshot brings them back. (Distinct from Rollback — this is the one path that recovers DROP-COLUMN data.)
This page is the ONLY mechanism that recovers data from a
DROP COLUMN. The sibling Rollback page covers reversing structural changes via forward-apply and file-baseddownbodies — neither of which brings back dropped data. If you dropped a column and want its data back, you neededVOLTRO_SOFT_DROP=1set at apply time; that's what this page is about.
By default Voltro's applier issues ALTER TABLE ... DROP COLUMN
for any lossy migration. Once that runs the data is gone — your
only options are a full DB restore or replaying from a backup.
VOLTRO_SOFT_DROP=1 rewrites every drop-column op into a
RENAME. The data stays in a sidecar column named
<original>__dropped_<timestamp>. voltro db restore-snapshot <plan-id> walks the migration's operations + RENAMEs them back.
When to use
Set VOLTRO_SOFT_DROP=1 in your apply pipeline as a default
safety net. The cost is one extra column per drop (data still
takes disk space until GC); the benefit is a one-command recovery
window.
Specifically helpful for:
- Reversible production migrations — drop a column, realize 30 minutes later it broke a downstream report, restore it.
- Pre-release schema churn — drop columns liberally during pre-release, recover when you change your mind.
- High-stakes drops — set
VOLTRO_SOFT_DROP=1per-migration via shell env var for the specific apply.
Apply with soft-drop
VOLTRO_SOFT_DROP=1 voltro db applyThe applier looks at every op in the plan. For drop-column ops
it emits a RENAME instead of a DROP:
-- Without VOLTRO_SOFT_DROP
ALTER TABLE "users" DROP COLUMN IF EXISTS "legacy_email";
-- With VOLTRO_SOFT_DROP=1
ALTER TABLE "users" RENAME COLUMN "legacy_email" TO "legacy_email__dropped_20260603145522";New soft-drop operations record their exact renamed column or table name as softDropSnapshot in the executed operation stored in _voltro_migration_plans.operations. The same name is assigned before DDL and retained in the operation resume payload. Transactional DDL commits its completed operation records in the same transaction; a rollback removes both. Operations outside a transaction persist progress around each DDL step. Older history may lack this metadata. The operation remains classified as a drop because the original object no longer belongs to the declared schema; boot fingerprints still describe that declared schema.
When a partially applied migration resumes, its completed history retains both previously completed operations and the operations completed during recovery, in their original order. Already applied soft-drop renames are not repeated; their recorded snapshot identities remain available for restoration.
If an operation-ledger run finished its DDL but was interrupted before history or ledger cleanup, development boot and db apply still enter recovery even when no schema changes remain. Recovery keeps the original run ID, records the completed operations, and reuses a matching history entry if it already exists before clearing the operation ledger. An unreadable ledger fails the recovery check instead of being treated as an empty ledger.
Restore
voltro db restore-snapshot plan_01j5xkqyz...↩ restored users.legacy_email (from legacy_email__dropped_20260603145522)
✓ restored 1 column(s) from plan 'plan_01j5xkqyz...'
The command reads the exact softDropSnapshot identity recorded on each drop operation. It never selects a newer object merely because its name matches. It acquires the migration lock and checks the entire restoration set against the live schema before changing anything. Missing recorded snapshots, existing original names, duplicate targets or missing identity metadata reject the request. A retained table is restored before retained columns inside it.
Plans whose history has no exact snapshot identity cannot be restored automatically. Review the retained objects and their data against the intended migration and backups, then perform an explicitly reviewed recovery with your database's SQL tools. Do not attach the newest snapshot to an old plan by guessing. No source-code codemod can reconstruct this missing historical ownership.
On dialects with transactional plan DDL, restoration runs in one transaction. On dialects with implicit DDL commits, complete preflight prevents known missing/colliding targets from causing partial work, but an unexpected database failure during execution can still leave some renames committed. Inspect the live schema before any further recovery. Successful restoration still requires reintroducing the restored objects in the declared schema before the next apply.
Inspect restoration
SQL-backed API hosts expose the same exact-plan restoration through POST /_voltro/inspect/migrations/restore-snapshot with a JSON body { "planId": "<recorded-plan-id>" }. Inspect must be enabled, and the request requires both the read credential (Authorization: Bearer …) and write credential (x-voltro-inspect-write). Development, source serve and compiled serve share this action. Memory stores do not expose it. A successful response reports restoredColumns and restoredTables; malformed bodies are rejected before execution. The action uses the same ownership checks and migration lock as the CLI, including the SQLite migration coordinator. Restore does not update your declared schema.
Local dashboard
The local Migrations page lists complete restoration sets by their recorded plan, including plans outside the 20 most recent history entries. Objects without exact ownership, consumed snapshots, occupied targets and snapshots claimed by more than one plan are not offered for automatic restoration. They can still appear in the unassigned retained-object inventory. Restore rechecks ownership and the live schema under the migration lock before changing anything.
With Inspect write access, select one replica, choose Restore, review the listed tables and columns, then choose Restore these objects. Cancel performs no write. A changed object set requires a new confirmation. The result is refreshed after the action; the declared schema must still be corrected before the next apply. Cloud displays the same recorded sets read-only and does not expose schema writes.
Custom SoftDropSnapshot / softDropSnapshots consumers use kind: 'table' | 'column', originalName and snapshotName; these replace the column-only column and sidecarColumn fields. planId, table and droppedAt remain. Custom useRestoreSnapshot providers return both restoredColumns and restoredTables. voltro update identifies affected custom consumers for manual review. SQL integrations can use readMigrationSnapshotCandidates(sql, live) from @voltro/database/sql for a read-only preview; it is not permission to execute without revalidation.
Workflow
- Apply a risky migration with soft-drop enabled.
- The dropped column survives as
<col>__dropped_<stamp>. - Test the post-apply state.
- Either:
- It's fine. Wait for GC (see below) to actually drop the column.
- It broke something. Run
voltro db restore-snapshot <plan-id>to bring the column back.
- Re-fix your schema in code (re-declare the column in the
.entity.tsfile) + run a normalvoltro db applyto put the schema back in shape.
Step 5 is important — the framework's declared schema is the source of truth. Just restoring the snapshot brings the data back but the declared schema still says the column shouldn't exist; the next apply would re-drop it.
The differ leaves snapshots alone
A soft drop leaves an object in the database that no schema declares —
that is the whole point of it. The planner treats every
<name>__dropped_<ts> as framework-managed and never plans a drop for
it, the same way it skips _voltro_* and the cluster engine's tables.
This holds for columns and for tables.
You do not need VOLTRO_DB_IGNORE_TABLES for a snapshot, and you should
not add one: that list is for YOUR unmanaged infra tables, and an entry
there would still be in your config long after gc-snapshots reclaimed
the snapshot.
GC
Snapshot columns aren't automatically dropped. They survive until you:
- Run a manual
ALTER TABLE ... DROP COLUMN <col>__dropped_<stamp>(or a wrapping migration that handles it). - Drop the table entirely.
Reclaim the space once your "is the migration confirmed safe?" review
window has passed with voltro db gc-snapshots --before <date> — it
permanently drops every <name>__dropped_<ts> column AND table older
than the date (the <ts> stamp drives the comparison). Add --dry-run
to preview:
voltro db gc-snapshots --before 2026-01-01 --dry-run # preview
voltro db gc-snapshots --before 2026-01-01 # drop them (NOT reversible)The snapshots are dropped as a set. Two tables one plan dropped keep the foreign key between them, and a soft-dropped reference column keeps its own — so the keys the set holds go first, then the columns, then the tables, a child before its parent. If a table outside the set still references a snapshot, nothing is dropped: the command names each constraint (<name> (<table>.<column> → <snapshot>)) and exits 1, because removing that key is a change to a table this command does not own.
You are reminded. A snapshot is a full copy of the dropped data — PII
included — and gc-snapshots is a step an operator has to remember. So a
voltro dev boot and voltro db plan warn, from the stamp in the name and
without an extra query, about every snapshot older than the review window,
naming it and printing the exact gc-snapshots --before to run. The window is
database.softDropRetentionDays in app.config.ts (default 7, the "~7d" the
soft-drop advice promises).
Limitations
- Restore needs
VOLTRO_SOFT_DROP=1at apply time. Without it the drop is a hardDROP COLUMN/DROP TABLEand the data is gone —restore-snapshotcan only bring back what was soft-dropped (renamed to<name>__dropped_<ts>). It restores both columns AND tables. - Lossy at the row level. This recovers DROP COLUMN (everything from before the drop is in the snapshot column). It does NOT recover UPDATE/DELETE row-data — those are forever. For that, use database backups.
- No effect on planner classification. Lossy ops are still
classified as lossy by
voltro db plan.VOLTRO_SOFT_DROPis about HOW the drop happens, not WHETHER it's classified safe. The lossy refusal still requires explicitdropped()annotation orVOLTRO_DESTRUCTIVE_OK=1.
See also
- Migration overview — the full lifecycle
- Operation classes —
the 7-class taxonomy including
lossy - Drift detection — for out-of-band changes