zanith

Recovery

Every destructive op (drop column, drop table) leaves an artifact — either a soft-drop tombstone or a real archive of the rows. zanith recover lists, inspects, restores, exports, and purges them.

Five artifact kinds

KindWhenStored
**softDropColumn**An op declared soft-drop for a column.Column stays in the table, marked dropped in the artifacts table.
**softDropTable**An op declared soft-drop for a table.Table renamed to _softdrop_, kept in the schema.
**archiveColumn**Column dropped with archive policy.Rows moved to a __archive_ table, then column dropped.
**archiveTable**Table dropped with archive policy.Table renamed to _archive_, schema kept.
**backfillCheckpoint**A long-running backfill checkpointed.Row in the checkpoints table — restart picks up where it left off.

The CLI surface

SHELL
zanith recover list # show every recoverable artifact
zanith recover inspect <name> # rows, size, recorded-at, source migration
zanith recover restore-column <name> # bring a soft-dropped or archived column back
zanith recover restore-table <name> # bring a soft-dropped or archived table back
zanith recover export <name> --format csv # stream rows out to stdout (csv / jsonl)
zanith recover purge <name> # permanently delete the artifact
zanith recover purge --older-than 30d # bulk purge by age

Restoring a column

SHELL
# A column was dropped two days ago — bring it back.
zanith recover list
# > orders_total_softdrop · 14 days ago · 412 MB · from 20260420_120000_drop_orders_total
 
zanith recover restore-column orders_total_softdrop
# > Re-attaches the column, restores the dropped index, marks the artifact restored.

Restore is idempotent — running it twice is a no-op the second time. The artifact stays in the recovery table marked restored until you purge it.

Exporting before purging

SHELL
# Take a CSV snapshot of an archive before purging.
zanith recover export old_orders_archive_2026_03 --format csv > old_orders_2026_03.csv
 
# Or JSONL for downstream pipelines.
zanith recover export old_orders_archive_2026_03 --format jsonl > old_orders.jsonl
 
# Then purge.
zanith recover purge old_orders_archive_2026_03

Retention policies

Each migration that creates an artifact can set retentionDays. purge --older-than sweeps anything past its retention window — a sensible cron entry on most production databases.

SHELL
# Daily housekeeping cron — purge anything past its retention window.
zanith recover purge --older-than 30d --json > /var/log/zanith/purges.jsonl

Reseeding into a new shape

The expand/contract reality: you archived a table, a NEW table took the old name with a different shape, and restore-table correctly refuses rather than clobber.reseed is the paved road — it pours the parked rows into the live table, with renames handled declaratively and unmapped columns falling back to their database defaults.

SHELL
# Old archive had "customer"; the new table calls it "customer_name".
zanith recover reseed orders --map customer_name=customer --skip-conflicts # ON CONFLICT DO NOTHING — re-runs are no-ops
 
# → ✓ Reseeded 1,204 row(s) into orders · columns: id, customer_name, total
# Archive kept — purge with: zanith recover purge <artifact-id>

Computed values (derive a column from the old row) live on the programmatic API, where functions fit:

TS
await reseedArtifact(adapter, artifact, {
map: {
customer_name: 'customer', // rename
status: (row) => row.total > 100 ? 'big' : 'std', // computed
},
skipConflicts: true,
});
// → { table, columns, read, inserted, skipped, batches }

Loud by design: unknown map targets and missing archive sources throw with the available columns named — a reseed never silently writes a partial shape.

Programmatic API

The same operations are exposed as functions — see programmatic API for listArtifacts, findArtifact, restoreSoftDropColumn, restoreArchiveTable, readArtifactRows, reseedArtifact, purgeArtifact, purgeOlderThan.