fix(scripts): add recover-dev-db.sql for fork-branch migration mismatch#16
Conversation
Local dev DBs that were built up on a fork branch can have migration
IDs 14-18 recorded that map to different migrations than the ones on
main. The Effect-SQL migrator only tracks numeric IDs, so main's
migrations 14-18 never run on those DBs, producing errors like:
SQLiteError: no such column: default_model_selection_json
ProviderSessionDirectoryPersistenceError: Unknown persisted provider
'claudeCode'.
This script applies the missing schema deltas (migrations 14-18) and
renames the legacy 'claudeCode' provider to 'claudeAgent', without
touching effect_sql_migrations.
Usage:
cp ~/.t3/dev/state.sqlite ~/.t3/dev/state.sqlite.bak
sqlite3 ~/.t3/dev/state.sqlite < scripts/recover-dev-db.sql
42tg
left a comment
There was a problem hiding this comment.
Review: scripts/recover-dev-db.sql
Verified SQL against actual migrations 014–018 in apps/server/src/persistence/Migrations/. Line-by-line match is faithful. Migration 16 (CanonicalizeModelSelections) event payload transforms are complex but correctly transcribed from the Effect SQL template literals.
Approve-worthy
- Correctness: Steps B–F match their corresponding migrations exactly.
- Provider rename (Step A): Covers the three right tables plus the REPLACE in
payload_json. - Transaction wrapping: Good — all-or-nothing.
- Clear docs: Backup instructions, verification queries, both recovery paths documented.
Issues to address
-
Missing
019_ProjectionSnapshotLookupIndexescoverage. The migration registry (Migrations.ts:67) assigns ID 19 toProjectionSnapshotLookupIndexes, ID 20 toReviewComments, ID 21 toReviewRequests. The script only patches 14–18. If the fork DB recorded IDs beyond 18 (PR description says "14–21 already applied"), then 19–21 ran on the real main definitions — fine, because ReviewComments/ReviewRequests useCREATE TABLE IF NOT EXISTSand the indexes useCREATE INDEX IF NOT EXISTS. But: if someone's fork had 19+ as different migrations,ProjectionSnapshotLookupIndexeswould be silently skipped. Worth documenting this assumption explicitly. -
No idempotency guards on
ALTER TABLE ADD COLUMN. The actual migration 017 checksPRAGMA table_infobefore addingarchived_at. The script doesn't. If someone runs it on a partially-recovered DB (maybe they manually added some columns), ALTER fails and the transaction rolls back. Suggest adding-- NOTE: not idempotentto the header or, better, guarding with a PRAGMA check like the real migration does:-- Only add if missing (mirrors migration 017 guard) SELECT CASE WHEN COUNT(*) = 0 THEN 1 ELSE 0 END FROM pragma_table_info('projection_threads') WHERE name = 'archived_at';
(SQLite doesn't support
IF NOT EXISTSforALTER TABLE ADD COLUMN, so this is the only option.) -
Orphaned fork schema stays. The fork's migrations 14–18 created columns/tables that don't exist in main's schema (e.g., whatever
ProjectionThreadActivityParentAndItemId,ProjectionThreadsJiraTicket,ReviewRequestsPrMetaadded). These remain as dead weight after recovery. Not a blocker — worth a one-liner in the header noting they're harmless leftovers.
Nit (pre-existing, not from this PR)
019_ReviewComments.ts filename has prefix 019 but is registered as ID 20 in Migrations.ts:69. Confusing — the file numbering and registry IDs diverge starting here.
Verdict
Solid recovery tool. Issues #1 and #3 are documentation-only fixes. Issue #2 is a nice-to-have. Happy to approve once the assumptions are documented.
… schema notes Document assumptions about migrations 19-21, add prominent non-idempotency warning, include ProjectionSnapshotLookupIndexes (Step G), and note that fork-specific orphaned columns are harmless.
Problem
Local dev DBs that were grown on a fork branch can fail to start
bun run devafter switching back tomain:The Effect-SQL migrator tracks migrations by numeric ID only. If a DB has IDs 14–18 recorded from a fork timeline that used different migrations under those IDs, main's 14–18 are silently skipped — leaving the schema half-converted.
Concrete divergence I hit (Tobias' fork → main):
Plus the
claudeCode→claudeAgentprovider rename was code-only, so legacy rows still referenceclaudeCode.What this PR does
Adds
scripts/recover-dev-db.sql— a single-transaction recovery script that:claudeCode→claudeAgentinprovider_session_runtime(provider_name + adapter_key),projection_thread_sessions.provider_name, andorchestration_events.payload_json.projection_thread_proposed_plans:implemented_at,implementation_thread_idprojection_turns:source_proposed_plan_thread_id,source_proposed_plan_idCanonicalizeModelSelections— addsdefault_model_selection_json(projects),model_selection_json(threads), dropsdefault_model/model, and portsorchestration_eventspayloads (project.created,project.meta-updated,thread.created,thread.meta-updated,thread.turn-start-requested) to the newmodelSelectionshape.projection_threads.archived_atidx_projection_threads_project_archived_ateffect_sql_migrations— IDs 14–21 are already recorded as applied, so no migration will be re-run.Recommended path: keep your data (Option B)
This is the path teammates should default to — wiping the DB loses all local sessions.
Fallback only: nuke and re-create (Option A)
Use this only if Option B somehow doesn't work for you. You will lose all local dev sessions.
Verification
After running the script:
First three should be
1, last should be0. Tested locally on a real fork-grown DB (~3.7 MB, ~200 events withclaudeCodereferences) — all checks green, dev server starts cleanly, sessions intact.Out of scope (but worth a follow-up)
The root cause — migrator keying only on ID — is what makes this class of break possible. A future change could verify recorded migration names match expected ones at startup and warn or fail fast.