You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

4.6 KiB

+++ title = "nbd migrate command" priority = 9 status = "done" ticket_type = "feature" dependencies = ["d1634a"] +++ 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 migrate on 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-run prints 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<()>:

  1. find_nbd_root()
  2. Call store::migrate_tickets(&root, dry_run).await
  3. Print a summary: Migrated N tickets (M errors).

store.rs

Add migrate_tickets(root: &Path, dry_run: bool) -> Result<MigrateReport>:

  1. fs::read_dir(tickets_dir(root))
  2. For each *.json, *.md, *.toml, *.jsonb file (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 current Ticket, injecting id from 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 and dry_run: print would update {filename}, do not write. f. If changed and not dry_run: write the new bytes to the same path. g. If deserialise fails: record the error, leave the file untouched.
  3. 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 → id is absent (since #[serde(skip)]).
  • File bytes differ → migrate rewrites.

For field addition (e.g. adding tags: Vec<String> later):

  • New field in Ticket gets #[serde(default)].
  • Old files lack tags → deserialise gives vec![].
  • Re-serialise → "tags": [] is written.
  • File bytes differ → migrate rewrites.

Tests

Unit tests (src/tests.rs):

  • migrate_tickets on a store with old-format files (containing "id") rewrites them without id.
  • migrate_tickets on an already-current store returns updated: 0, already_current: N.
  • migrate_tickets --dry-run does not modify files on disk.
  • A file with invalid JSON is counted in errors and left unchanged.

Integration tests (tests/integration.rs):

  • Create tickets with old code (inject id manually into JSON), run nbd migrate, verify id is gone from files.
  • nbd migrate --dry-run reports changes but does not modify files.
  • nbd migrate exits zero even when some tickets error (but prints error summary).
  • nbd migrate --json outputs a valid JSON object with updated, already_current, errors fields.

Files touched

  • src/main.rsMigrate command, cmd_migrate
  • src/store.rsmigrate_tickets, MigrateReport
  • src/display.rsprint_migrate_report
  • src/tests.rs — unit tests
  • tests/integration.rs — integration tests
  • README.md — document nbd migrate