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>
feat(state): back up the ledger before migrating it, and migrate atomically
Found reviewing #42. sqlite3.connect creates the destination file before connection.backup() runs, and backup_ledger had no cleanup on the failure path — so a backup interrupted by a full disk, an I/O error, or a killed process left a 0-byte file carrying the real ...-pre-migration.sqlite name. That file is the newest, so latest_pre_migration_backup selected it and curator doctor printed it as the ledger to restore. Following that advice copies an empty database over a live ledger and destroys it — on exactly the disaster path this code exists to protect. Reproduced before fixing: with a good 290KB backup already on disk, one failed attempt left a 0-byte sibling and latest_pre_migration_backup returned the 0-byte one. Backups are now written to a .partial name the restore glob does not match and renamed into place only after the copy completes, so an incomplete backup can never carry the real name — this covers the killed-process case too, which cleanup in an except block would not. The partial is removed on failure, and an unusable file is skipped when choosing what to advertise. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fix(state): never offer a half-written backup as a restore point
Moves both version declarations to 0.1.2, dates the changelog entry, and points the README install pins and version table at the new tag. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
chore(release): v0.1.2 — migration safety
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.
Promotes
devtomainfor the v0.1.2 cut.What ships
The ledger is backed up before a schema migration (#42). Opening
.curator/with a newer Curator applies pending migrations automatically —initialize_databasehas ~114 call sites, so effectively any command migrates. It now copies the ledger to.curator/archive/…-pre-migration.sqlitefirst, using SQLite's online backup API rather than a file copy: the ledger runs in WAL mode, and a copy of the main file alone can miss committed pages still in the-wal. Verified rather than assumed — a file copy of a freshly written ledger cannot even see the table. A ledger with nothing to migrate, the usual case, is not copied.Migrations are all-or-nothing (#42). They used to commit 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 — and because the version was never recorded, the next open would retry and fail on
duplicate column name, permanently. They now share one transaction and roll back together, DDL included.curator doctorshows what is pending, and how to get back (#42). It reports migrations without applying them —doctoronly ever usesconnect_database, neverinitialize_database, so it is a dry run by construction rather than by a flag. It also prints the newest backup with a ready-to-paste restore command, found by reading the directory rather than the ledger, so it still answers when the ledger will not open.A half-written backup is never offered as a restore point (#43). Found reviewing #42.
sqlite3.connectcreates the destination before the copy runs, so a backup interrupted by a full disk or a killed process left a 0-byte file carrying the real name — which, being newest, became whatdoctoradvertised. Copying that over a live ledger destroys it. Backups are now written to a.partialname the restore path ignores and renamed into place only once complete.Version
pyproject.tomlandcurator.__version__both move to0.1.2; the drift test added in v0.1.1 fails if only one moves. README pins and the version table point atv0.1.2.Verification
schema_version [1, 2].After merge
git tag -a v0.1.2 -m "Curator v0.1.2" && git push origin v0.1.2— firesrelease-smoke, which builds, clean-installs the wheel and sdist, and publishes them withSHA256SUMS.gh release create v0.1.2from the CHANGELOG entry.target-branch: devconfig from v0.1.1 finally takes effect, since Dependabot reads it from the default branch. chore(deps-dev): bump ruff from 0.15.19 to 0.16.0 #33 also unblocks once the Ruff pin reachesmain.🤖 Generated with Claude Code