Files
codewhale/crates
Hunter Bown 1986a15dd5 feat(persistence): schema migration framework — forward upgrade path for ~/.deepseek/ records (#350)
Every persistence layer in crates/tui/src/ already gates `schema_version
> CURRENT_*` to reject newer-than-supported records (good — prevents
silent truncation when an older binary tries to load a v3 file with v4
fields). What was missing: the **forward upgrade path** for older
records. When we bump CURRENT_SESSION_SCHEMA_VERSION from 3 to 4 to
add a field, every v3 session on disk would silently load with the
new field's serde default — which is OK for additions but breaks
catastrophically for renames or shape changes.

This commit lays down the framework:

**`crates/tui/src/schema_migration.rs`** — new module:

- `SchemaMigration` trait. Each persistence domain implements it once
  with `CURRENT_VERSION`, `DOMAIN`, and an ordered `MIGRATIONS` list
  of `fn(&mut serde_json::Value) -> Result<(), MigrationError>` steps.
  Index `i` migrates from version `i+1` to `i+2`.

- `SchemaMigration::migrate(value, from_version)` — runs every required
  step, stamping `value["schema_version"]` after each step so a partial
  failure leaves a known-state record rather than mixed.

- `MigrationError` — typed error with from/to versions + reason.

- `backup_before_migrate(path, domain)` — creates a `.bak` copy of the
  source file before mutation. Errors are warn-logged and ignored
  (continues because `write_atomic` is itself crash-safe). The `.bak`
  is left on disk as a manual recovery artifact — no automatic GC.

**`schema_migration::registry`** — submodule that registers every
existing persistence domain (session, offline_queue, runtime, task,
automation, automation_run) at its current version with an empty
MIGRATIONS list. No domain has shipped a schema bump yet, so today's
behavior is a no-op. The next bump is now a 4-step recipe:

1. Write the `migrate_<domain>_v<N>_to_v<N+1>` step in this module.
2. Append it to `MIGRATIONS` and bump `CURRENT_VERSION`.
3. Wire `<Domain>Migration::migrate(...)` into the load function in
   the owning module.
4. Add a fixture-based integration test.

Tests: 6 unit tests covering no-op, all-steps, partial migration,
newer-than-current rejection, backup creation, and backup-failure
robustness.

Wiring into individual load sites (session_manager, runtime_threads,
task_manager, automation_manager) is intentionally deferred until the
first actual schema bump needs it — wiring without migrations would
add code paths nothing exercises, and the framework is the part that
needs to land before the next bump can ship safely.

Closes #350.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 10:23:33 -05:00
..