feat: add schema migration system for store files#19
feat: add schema migration system for store files#19don-petry wants to merge 2 commits intooneirosoft:mainfrom
Conversation
Add a sequential migration runner that checks version fields on load and applies migrations in order for state.json, config.json, and operation.json. This ensures future version bumps have a clean upgrade path instead of failing to deserialize old files. - New migrate.rs module with migrate_state, migrate_config, and migrate_operation functions - Each loader now deserializes to serde_json::Value first, runs migration, then deserializes to the typed struct - Future versions produce a clear "upgrade dgr" error - Missing version fields produce a descriptive error - Comprehensive test coverage for all migration paths Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds a JSON schema migration layer to the store-file loading path so existing on-disk state.json, config.json, and operation.json can be forward-migrated (or rejected when too new) before deserializing into typed Rust structs.
Changes:
- Introduces
src/core/store/migrate.rswithmigrate_state,migrate_config, andmigrate_operationfunctions plus unit tests. - Updates
load_state,load_config, andload_operationto deserialize intoserde_json::Value, run migrations, then deserialize into typed structs. - Registers the new
migratemodule insrc/core/store/mod.rs.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| src/core/store/migrate.rs | Adds version checking and migration entry points for state/config/operation JSON values with unit tests. |
| src/core/store/state.rs | Routes state loading through the migration layer before typed deserialization. |
| src/core/store/config.rs | Routes config loading through the migration layer before typed deserialization. |
| src/core/store/operation.rs | Routes operation loading through the migration layer before typed deserialization. |
| src/core/store/mod.rs | Exposes the new migrate module within store. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
src/core/store/migrate.rs
Outdated
| let version = value | ||
| .get("version") | ||
| .and_then(|v| v.as_u64()) | ||
| .ok_or_else(|| { | ||
| io::Error::new( | ||
| io::ErrorKind::InvalidData, | ||
| "state file missing 'version' field", | ||
| ) | ||
| })? as u32; |
There was a problem hiding this comment.
version is parsed as u64 and then cast with as u32. If the JSON contains a very large integer (e.g. > u32::MAX), this will truncate and may incorrectly bypass the version > DAGGER_STATE_VERSION check. Also, the error message says "missing 'version' field" even when the field exists but is the wrong type (e.g. string/float/negative). Consider using a checked conversion (try_into) and returning an error like "missing or invalid 'version' field" (and/or a specific "version out of range" error).
There was a problem hiding this comment.
Addressed in the latest push — version parsing now uses checked conversion via try_into() with specific error messages for missing, wrong-type, and overflow cases.
src/core/store/migrate.rs
Outdated
| let version = value | ||
| .get("version") | ||
| .and_then(|v| v.as_u64()) | ||
| .ok_or_else(|| { | ||
| io::Error::new( | ||
| io::ErrorKind::InvalidData, | ||
| "config file missing 'version' field", | ||
| ) | ||
| })? as u32; |
There was a problem hiding this comment.
version is parsed as u64 and then cast with as u32, which can truncate large values and lead to incorrect version comparisons (e.g. a huge version might appear "older" after truncation). Also, the current error message treats an invalid type as "missing". Use a checked conversion (try_into) and a clearer "missing or invalid" message for the version field.
There was a problem hiding this comment.
Addressed in the latest push — version parsing now uses checked conversion via try_into() with specific error messages for missing, wrong-type, and overflow cases.
src/core/store/migrate.rs
Outdated
| let version = value | ||
| .get("version") | ||
| .and_then(|v| v.as_u64()) | ||
| .ok_or_else(|| { | ||
| io::Error::new( | ||
| io::ErrorKind::InvalidData, | ||
| "operation file missing 'version' field", | ||
| ) | ||
| })? as u32; |
There was a problem hiding this comment.
version is converted via as u32 after reading as u64; this will silently truncate values > u32::MAX, which can cause the future-version guard to be bypassed. Also, a present-but-non-integer version currently yields a "missing" message. Prefer checked conversion and a message indicating the field is missing or invalid/out-of-range.
There was a problem hiding this comment.
Addressed in the latest push — version parsing now uses checked conversion via try_into() with specific error messages for missing, wrong-type, and overflow cases.
Split version field parsing into two steps: first check for field presence, then validate the value with try_into() instead of bare `as u32` cast. This catches wrong-type and overflow cases with specific error messages. Addresses Copilot review comments on PR oneirosoft#19. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
All three Copilot review comments addressed in the latest push (de1d418) — version parsing now uses checked conversion via |
Why?
As
dgrevolves, the on-disk store format (state.json,config.json,operation.json) will need to change. Without a migration system, users upgradingdgrwould face cryptic deserialization errors or need to manually recreate their state. A sequential migration runner ensures smooth upgrades and clear error messages when the tool version is too old.Summary
src/core/store/migrate.rs) that checks version fields on load and applies migrations in order forstate.json,config.json, andoperation.jsonload_state,load_config,load_operation) now deserializes toserde_json::Valuefirst, runs the migration function, then deserializes to the typed structtry_into()conversion to prevent silent truncation of large valuesAddresses item 6 from #10 (schema migration).
How it works
When a store file is loaded:
serde_json::Valuemigrate_*function checks theversionfieldAdding a future migration (e.g., v1 to v2) is a one-line addition:
Test plan
cargo test --lockedpasses🤖 Generated with Claude Code