Skip to content

Add a public format-version marker writer for rollback (#830) - #863

Merged
sehkone merged 3 commits into
mainfrom
sehkone/issue-830
Aug 22, 2026
Merged

Add a public format-version marker writer for rollback (#830)#863
sehkone merged 3 commits into
mainfrom
sehkone/issue-830

Conversation

@sehkone

@sehkone sehkone commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds write_version_markers, the public entry point a rollback uses to put the two VERSION markers back in step with a restored database snapshot.

A migration body rewrites data_dir/states.db, but the format version lives beside it in data_dir/VERSION and backup_dir/VERSION. Restoring the pre-update snapshot therefore puts the older database back under markers that still name the newer format, and the earlier binary finds no migration for a marker it does not know. The new function takes the version the caller recorded before the update — the operation attempt's pre_update_version — and writes it to both markers.

  • pub fn write_version_markers<P: AsRef<Path>, Q: AsRef<Path>>(data_dir: P, backup_dir: Q, version: &str) -> Result<()>, re-exported from lib.rs. It parses the whole input as semver::Version before touching the filesystem and writes that parsed value's canonical to_string() form, with no trailing bytes. Validation is syntactic only: it does not read states.db, does not read the markers it replaces, and does not judge whether any binary can migrate from the version. No public marker reader or per-marker writer is exposed.
  • A missing directory, and any missing component of its path, is created with create_dir_all — but only after validation succeeds, so invalid input creates neither directory nor marker and leaves an existing marker untouched.
  • create_version_file now takes the version to write instead of hard-coding CARGO_PKG_VERSION, so both the migration path and the rollback path go through the same write-to-a-temporary, file-sync, rename, directory-sync replacement. Its callers in migrate_data_dir and retrieve_or_create_version pass CARGO_PKG_VERSION explicitly, leaving normal migration behaviour unchanged.
  • The two files can live on different filesystems, so there is no cross-directory atomic commit. A failure reports which of the two directories it belonged to, and the operation is idempotent: correct the failure, call it again with the same paths and version, and both markers end up canonical and equal.
  • The VERSION_TMP_FILE_NAME rustdoc keeps the fixed VERSION.tmp name and now explains that it makes every marker writer in a directory collide, so both startup migration and rollback marker writing require exclusive access to each affected directory.
  • The #[cfg(test)] write_version helper and its raw-marker call sites are retained — its non-canonical and otherwise invalid writes are exactly the setup the public API must reject.

Closes #830

Test plan

  • write_version_markers_writes_canonical_markers — a valid version in two temporary directories puts Version::parse(input)?.to_string() in both VERSION files, byte for byte.
  • write_version_markers_keeps_prerelease_versions — a prerelease such as 0.47.0-alpha.1 survives the round trip, so the alpha format versions this crate migrates between can be recorded.
  • write_version_markers_creates_missing_directories — absent directories, including intermediate components, are created and receive canonical markers.
  • write_version_markers_rejects_invalid_versions"", 0.46, not a version, 0.46.0 , and 0.46.0extra are each rejected; no directory or marker appears, a pre-existing marker keeps its original contents, and no VERSION.tmp is left behind.
  • write_version_markers_replaces_existing_markers — a marker naming a newer format, and one stored non-canonically, are replaced rather than appended to.
  • write_version_markers_reports_the_failing_directory_and_retries — a regular file at the backup path makes its marker operation fail with an error naming the backup directory; replacing it with a directory and retrying the same version leaves both canonical markers equal.
  • write_version_markers_reports_a_failing_data_directory — a failure on the data marker names the data directory and stops before the backup one, so the pair is never left describing two different formats.
  • markers_written_for_rollback_let_migration_run_again — a populated 0.46-format store built in a tempdir as assert_migration_creates_new_column_families does, with both markers set to 0.46.0 through the public API, lets migrate_data_dir succeed instead of reporting migration from 0.46.0 is not supported. It adds the current column families, converts the populated agent and external-service records, and reaches CARGO_PKG_VERSION in both markers.
  • The private-helper tests proving a normal migration writes CARGO_PKG_VERSION to both markers, and that an individual replacement stays atomic, still pass with create_version_file's new argument.
  • cargo fmt -- --check --config group_imports=StdExternalCrate
  • cargo clippy --bins --tests --all-features -- -D warnings
  • cargo test --all-features — 623 lib tests, 12 doctests, 2 compile-fail doctests, 0 failures.
  • markdownlint-cli2 CHANGELOG.md

A rollback restores the pre-update states.db snapshot, but the two
VERSION files are separate metadata the snapshot does not cover. Both
markers keep naming the newer format, and the earlier binary can select
no migration for a marker it does not know, so the restored database
will not open.

The existing marker helper always writes CARGO_PKG_VERSION, which is
exactly the version a rollback must not record. Give it the version to
write and expose write_version_markers on top of it, so the caller can
stamp the format version it recorded before the update onto both
directories through the same atomic per-file path a migration uses.

Part of #831
The marker tests all started from a directory with no VERSION, so
nothing proved that a rollback actually overwrites what the update
left behind rather than appending to it. Write the newer markers
first, in one case non-canonically, and assert the older version
lands byte for byte.

Part of #831
create_version_file used to run only over the data and backup
directories of a migration, but it now also serves the rollback marker
writer, so a failure to flush the directory entry reported "the data
dir" for three of its four call sites. Say which file's directory it is
instead, and let the caller's context name which of the two it was.

Part of #831
@codecov

codecov Bot commented Aug 22, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.46237% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 84.58%. Comparing base (34bb709) to head (e4a9d33).

Files with missing lines Patch % Lines
src/migration.rs 99.46% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #863      +/-   ##
==========================================
+ Coverage   84.50%   84.58%   +0.07%     
==========================================
  Files          92       92              
  Lines       36927    37105     +178     
==========================================
+ Hits        31207    31385     +178     
  Misses       5720     5720              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@sehkone

sehkone commented Aug 22, 2026

Copy link
Copy Markdown
Contributor Author

[Reviewer Round 1]\n\nNo findings. The public re-export and independent path generics match the requested API; write_version_markers validates before any directory creation, canonicalizes the parsed version, and preserves the existing per-marker sync/rename path with data/backup-specific error context. The focused filesystem tests cover invalid input without mutation, missing directories, replacement, partial failure and retry, while the populated 0.46 store test demonstrates that markers written through the public API let the migration chain run and finish with current markers. The PR body also links the issue and includes a complete test plan.

@sehkone

sehkone commented Aug 22, 2026

Copy link
Copy Markdown
Contributor Author

[Review Verdict Round 1: APPROVED]

@sehkone

sehkone commented Aug 22, 2026

Copy link
Copy Markdown
Contributor Author

Suggested squash commit

Title

Add a public format-version marker writer

Body

A migration body rewrites `data_dir/states.db`, but the format version
lives beside it in `data_dir/VERSION` and `backup_dir/VERSION`. A
rollback snapshot therefore covers the database alone: restoring it puts
the older contents back under markers that still name the newer format,
and the earlier binary finds no migration for a marker it does not know.

`write_version_markers` is the metadata companion to that restore. The
caller passes the version it recorded before the update — the operation
attempt's `pre_update_version` — and both markers again describe the
restored contents. The input is parsed as a semantic version before
anything is written, and it is the parsed value's canonical form that
reaches each file, so validation is syntactic only: nothing reads
`states.db`, nothing reads the markers being replaced, and choosing a
version the rollback binary supports stays the caller's responsibility.

`create_version_file` now takes the version to write instead of
hard-coding `CARGO_PKG_VERSION`, so the rollback path and the migration
path share one write-to-a-temporary, sync, rename, and directory-sync
replacement. The two markers can live on different filesystems, so there
is no cross-directory atomic commit; a failure names the directory it
belonged to, and the operation is idempotent, so correcting the failure
and repeating the call leaves both markers canonical and equal.

Closes #830

@sehkone
sehkone merged commit 1727d97 into main Aug 22, 2026
10 checks passed
@sehkone
sehkone deleted the sehkone/issue-830 branch August 22, 2026 09:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add a public format-version marker writer for rollback

1 participant