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.
117 lines
4.7 KiB
Markdown
117 lines
4.7 KiB
Markdown
---
|
|
# nbd-q2d1
|
|
title: nbd migrate command
|
|
status: completed
|
|
type: feature
|
|
priority: critical
|
|
created_at: 2026-03-10T23:30:29Z
|
|
updated_at: 2026-03-10T23:30:31Z
|
|
blocked_by:
|
|
- nbd-o3k8
|
|
---
|
|
|
|
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`
|