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.

111 lines
4.6 KiB
Markdown

+++
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)> }`.
```rust
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.rs``Migrate` command, `cmd_migrate`
- `src/store.rs``migrate_tickets`, `MigrateReport`
- `src/display.rs``print_migrate_report`
- `src/tests.rs` — unit tests
- `tests/integration.rs` — integration tests
- `README.md` — document `nbd migrate`