feat(state): back up the ledger before migrating it, and migrate atomically - #42
Merged
Conversation
…ically Every command that opens .curator/ runs pending migrations, and until now it did so on the live ledger with no copy and no transaction. v0.1.4 adds the Execution tables and dual-writes; that is the first migration with real scope to damage user data, and it should not land before there is a way back. Three gaps, all in initialize_database: No backup. The ledger is now copied to .curator/archive/ before anything writes, but only when there is actually something to migrate — with 114 call sites, copying on every open would be absurd. It uses sqlite3's online backup API, not shutil.copy: the ledger runs in WAL mode, and a copy of the main file alone can miss committed pages still in the -wal. Verified — a file copy of a freshly written ledger cannot even see the table. No transaction. Migrations committed one at a time, so a failure on the third left the first two applied and the ledger in a state no version is written to read. They now share one transaction and roll back together, DDL included. No visibility. Nothing could answer "what will opening this ledger do". curator doctor now reports pending migrations without applying them — it only ever used connect_database, never initialize_database, so it is a dry run by construction rather than by a flag. Backups are timestamped rather than named for the schema version they precede: a migration number is an internal counter, and "v3" reads like a SQLite version to the person who has to find the file. Since a timestamp is no easier to recall, doctor prints the newest backup with the full restore command, and finds it by reading the directory so it still answers when the ledger will not open. Naming is shared with reset's archive so the two cannot drift. Rollback stays documented rather than a command: restoring is one cp, and a command that overwrites a live ledger needs a confirmation gate and a test surface the roadmap did not ask for. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Owner
Author
Code reviewFound 1 issue:
Reproduced against this commit: with a good 290KB backup already on disk, one failed attempt left a 0-byte sibling and Fixed in #43 — backups are written to a The other four dimensions (CLAUDE.md adherence, git history, prior PR comments, code-comment invariants) returned no findings. 🤖 Generated with Claude Code |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
v0.1.2 — migration safety, the roadmap's next rung.
Every command that opens
.curator/runs pending migrations (initialize_databasehas 114 call sites), and until now it did so on the live ledger with no copy and no transaction. This needs to land before v0.1.4, which adds theExecutiontables and dual-writes — the first migration with real scope to damage user data.Three gaps
No backup. The ledger is now copied to
.curator/archive/…-pre-migration.sqlitebefore anything writes — but only when there is actually something to migrate, since copying on every open would be absurd at 114 call sites.It uses
sqlite3.Connection.backup(), notshutil.copy. The ledger runs in WAL mode, and a copy of the main file alone can miss committed pages still in the-wal. That isn't theoretical — a file copy of a freshly written ledger cannot even see the table:No transaction. Migrations committed one at a time, so a failure on the third left the first two applied and the ledger in a state no version of Curator is written to read. They now share a single transaction and roll back together — DDL included, reusing the existing
CuratorConnection.begin_transaction().No visibility. Nothing could answer "what will opening this ledger do to it".
curator doctornow reports pending migrations without applying them. It only ever usedconnect_database, neverinitialize_database, so it is a dry run by construction rather than by a flag — which is also why this adds nocurator migratecommand, whose non-dry-run branch would just duplicate what running any command already does.On the backup filename
Backups are timestamped, not named for the schema version they precede. A migration number is an internal counter, and
pre-v3reads like a SQLite version to the person who has to find the file mid-recovery. Since a timestamp is no easier to recall,doctorprints the newest backup with the full restore command:It finds that by reading the directory rather than the ledger, so it still answers when the ledger will not open — which is when someone actually needs it. Naming is shared with
reset's archive so the two cannot drift.Rollback stays documented rather than a command: restoring is one
cp, and a command that overwrites a live ledger needs a confirmation gate and a test surface the roadmap did not ask for.Verification
381 passing (372 before, 9 new), ruff clean. Beyond the unit tests:
RuntimeErrorescapes and the half-appliedALTER TABLEsurvives.git worktreeat that tag, opens under this branch with no pending migrations and no backup (correct — v0.1.0 already carried migrations 1 and 2). Injecting a v0.1.4-style migration then produces a backup that opens cleanly and still readsschema_version [1, 2].Version is deliberately not bumped;
CHANGELOGlands under[Unreleased]. The v0.1.1 lesson was that pinning docs to a tag that does not exist yet is a 404.🤖 Generated with Claude Code