4.7 KiB
| title | status | type | priority | created_at | updated_at | blocked_by | |
|---|---|---|---|---|---|---|---|
| nbd migrate command | completed | feature | critical | 2026-03-10T23:30:29Z | 2026-03-10T23:30:31Z |
|
Add nbd migrate to bring all ticket files on disk into conformance with the current schema. This is the standard mechanism for handling any schema change — field removals, field additions, renames, or type changes.
Motivation
Schema changes (like removing id from the JSON body, adding new fields with defaults, or renaming a field) leave existing ticket files in an old format. nbd migrate re-serialises every ticket through the current serde schema, which:
- Removes fields that no longer exist in
Ticket(they are ignored on deserialise, then absent on re-serialise). - Adds new fields with their
#[serde(default)]values. - Normalises any formatting differences (e.g. key order, whitespace).
The current immediate use case is scrubbing the "id" key from all existing .json files after the id-from-filename schema change.
Design principles
- Idempotent. Running
nbd migrateon an already-current store is a no-op (files are re-written identically). - Non-destructive. A failure on one ticket does not abort the rest; errors are collected and reported at the end.
- Source of truth unchanged. If a ticket cannot be parsed, it is left on disk as-is and reported as an error.
- Dry-run available.
--dry-runprints what would change without writing.
Approach
main.rs
Add Migrate variant to Commands:
Migrate {
/// Print changes without writing them.
#[arg(long)]
dry_run: bool,
}
Implement cmd_migrate(dry_run: bool) -> store::Result<()>:
find_nbd_root()- Call
store::migrate_tickets(&root, dry_run).await - Print a summary:
Migrated N tickets (M errors).
store.rs
Add migrate_tickets(root: &Path, dry_run: bool) -> Result<MigrateReport>:
fs::read_dir(tickets_dir(root))- For each
*.json,*.md,*.toml,*.jsonbfile (all supported formats — the ones that exist now and future ones added by the multi-format feature): a. Read the raw bytes. b. Attempt to deserialise into currentTicket, injectingidfrom filename. c. Re-serialise to the current schema (same format as the original file's extension). d. Compare raw bytes. If unchanged, skip (count as already-current). e. If changed anddry_run: printwould update {filename}, do not write. f. If changed and notdry_run: write the new bytes to the same path. g. If deserialise fails: record the error, leave the file untouched. - Return
MigrateReport { updated: usize, already_current: usize, errors: Vec<(String, String)> }.
pub struct MigrateReport {
pub updated: usize,
pub already_current: usize,
pub errors: Vec<(String, String)>, // (filename, error message)
}
display.rs
Add print_migrate_report(report: &MigrateReport):
Migrated 3 tickets.
Current 5 tickets (already up to date).
Errors 1 ticket could not be migrated:
bad_ticket.json: trailing comma at line 4
When --json, serialise MigrateReport directly (derive Serialize).
How schema changes use this
For field removal (e.g. removing id from JSON):
- Old files have
"id": "..."→ on deserialise, serde ignores it (unknown field). - Re-serialise →
idis absent (since#[serde(skip)]). - File bytes differ →
migraterewrites.
For field addition (e.g. adding tags: Vec<String> later):
- New field in
Ticketgets#[serde(default)]. - Old files lack
tags→ deserialise givesvec![]. - Re-serialise →
"tags": []is written. - File bytes differ →
migraterewrites.
Tests
Unit tests (src/tests.rs):
migrate_ticketson a store with old-format files (containing"id") rewrites them withoutid.migrate_ticketson an already-current store returnsupdated: 0,already_current: N.migrate_tickets --dry-rundoes not modify files on disk.- A file with invalid JSON is counted in
errorsand left unchanged.
Integration tests (tests/integration.rs):
- Create tickets with old code (inject
idmanually into JSON), runnbd migrate, verifyidis gone from files. nbd migrate --dry-runreports changes but does not modify files.nbd migrateexits zero even when some tickets error (but prints error summary).nbd migrate --jsonoutputs a valid JSON object withupdated,already_current,errorsfields.
Files touched
src/main.rs—Migratecommand,cmd_migratesrc/store.rs—migrate_tickets,MigrateReportsrc/display.rs—print_migrate_reportsrc/tests.rs— unit teststests/integration.rs— integration testsREADME.md— documentnbd migrate