How OpenLogi's Configuration Migration System Manages Schema Upgrades from v2/v3 to v7
OpenLogi uses an incremental migration pipeline that automatically upgrades configuration files from older schema versions (v2/v3) to the current v7 format, preserving user data through atomic backups and sequential transformation functions defined in crates/openlogi-core/src/config.rs.
OpenLogi is an open-source input device customization tool that stores user preferences in a TOML configuration file. As the project evolves, its configuration schema has undergone multiple revisions. The migration system ensures users can upgrade seamlessly without manual intervention. This article examines how OpenLogi handles schema migrations from legacy versions to the current v7 format, based on the source code in the AprilNEA/OpenLogi repository.
Configuration Schema Version Tracking
The schema version serves as the foundation of OpenLogi's migration system. Each configuration file declares its version via the schema_version field, which the loader compares against the current target.
The constant SCHEMA_VERSION = 7 is defined in crates/openlogi-core/src/config.rs at line 94. When ConfigFile::load_from_path executes, it first parses the file header into a ConfigHeader struct to extract the declared version. If the loaded version is lower than SCHEMA_VERSION, the loader preserves the original source text in migrated_from to enable disaster recovery.
This version-aware loading happens in crates/openlogi-core/src/config/file.rs (lines 26-31), where the infrastructure for backup creation is established before any transformations occur.
The Migration Chain: Incremental Upgrades to v7
OpenLogi applies migrations sequentially using three specialized methods. Each function targets a specific schema transition, ensuring idempotent and reversible transformations.
migrate_owner_locked_gestures: v3 → v4 Transition
The migrate_owner_locked_gestures function (lines 59-86 in config.rs) removes the obsolete gesture owner lock mechanism.
Older configurations stored a single gesture_owner field that determined which gesture bindings were active. The migration:
- Infers the effective owner from the legacy configuration
- Demotes non-owner gestures to
Singlebinding mode - Folds dormant gesture maps into
DeviceConfig::disabled_gestures
This transformation creates a per-button gesture mode system, eliminating the global owner lock while preserving all user-defined bindings.
migrate_transport_scoped_keys: v4 → v5 Transition
The migrate_transport_scoped_keys function (lines 39-58 in config.rs) makes configurations transport-agnostic.
Prior to v4, device keys included transport prefixes like direct:. The migration:
- Rewrites keys to bare identity format (
unit:) - Preserves the original routing information as
LinkConfigentries - Updates all references including
selected_deviceandhost_switch_targets
This decouples the configuration from specific transport implementations, enabling seamless switching between connection methods.
migrate_thumbwheel_native_direction: v6 → v7 Transition
The migrate_thumbwheel_native_direction function (lines 24-33 in config.rs) normalizes thumb-wheel scroll direction defaults.
Pre-v7 configurations used inverted defaults (up → right, down → left). The migration:
- Rewrites bindings to new defaults (
up → left,down → right) - Only affects bindings matching the old implicit defaults
- Leaves custom user-defined pairs untouched
This preserves intentional user customizations while aligning native behavior with the corrected polarity.
Sequential Migration Execution
The migration pipeline executes conditionally based on the detected schema version. The control flow in parse_config follows this pattern:
let (mut config, loaded_version) = parse_config(path, &source)?;
if loaded_version <= 3 {
config.migrate_owner_locked_gestures();
}
if loaded_version <= 4 {
config.migrate_transport_scoped_keys();
}
if loaded_version <= 6 {
config.migrate_thumbwheel_native_direction();
}
Each condition remains active to support skipping intermediate versions. A v2 file migrates through all three functions, while a v5 file only executes the final thumb-wheel migration.
Complete v2/v3 to v7 Migration Path
The full upgrade trajectory involves five conceptual steps:
-
v2 → v3: Device map transitions from model-scoped to physical-device keys. Handled transparently by
RawDeviceConfigdeserialization without explicit migration code. -
v3 → v4:
migrate_owner_locked_gesturesremoves owner-locked gesture mode. -
v4 → v5:
migrate_transport_scoped_keysstrips transport prefixes from device keys. -
v5 → v6: Short/long button binding thresholds added. Handled by the deserializer; no migration shim required.
-
v6 → v7:
migrate_thumbwheel_native_directionnormalizes thumb-wheel direction defaults.
Upon completion, the configuration contains schema_version = 7 and a layout fully compatible with the current codebase.
Backup and Safety Guarantees
OpenLogi's migration system implements multiple safeguards to prevent data loss.
Atomic Backup Creation
When migration rewrites a file, the original content is backed up on the first successful save. The backup path is constructed in crates/openlogi-core/src/config/file.rs (lines 398-404) using the pattern:
{original_filename}.{timestamp}.v{source_version}.backup
Atomic Write Operations
The backup and main file write use write_atomic (lines 84-90 in file.rs), ensuring that:
- Partial writes due to crashes leave the original intact
- Disk-full conditions fail cleanly without corruption
- The pre-migration version remains recoverable
Subsequent saves operate without additional backup creation, as the migrated content becomes canonical.
Practical Code Examples
Automatic Migration on Load
use openlogi_core::config::ConfigFile;
// Loads config.toml from the platform-standard path.
// Migrations run automatically if schema_version < 7.
// First save creates backup of pre-migration state.
let (config, mut cfg_file) = ConfigFile::load_or_default()?;
assert_eq!(config.schema_version, openlogi_core::config::SCHEMA_VERSION);
Persisting After Migration
// Modify configuration
config.selected_device = Some("unit:abcd1234".to_string());
// Atomic save creates backup on first post-migration write
// Example: config.toml.2024-09-05.v4.backup
cfg_file.save(&config)?;
Testing Migration Results
#[test]
fn test_migration_v2_to_v7() {
let old_toml = r#"
schema_version = 2
[devices."G502"]
button_bindings = { /* ... */ }
"#;
let path = std::env::temp_dir().join("config_test.toml");
std::fs::write(&path, old_toml).unwrap();
let (cfg, _) = ConfigFile::load_from_path(&path).unwrap();
assert_eq!(cfg.schema_version, openlogi_core::config::SCHEMA_VERSION);
}
Key Implementation Files
| File | Responsibility |
|---|---|
crates/openlogi-core/src/config.rs |
Config struct, SCHEMA_VERSION, migration methods |
crates/openlogi-core/src/config/file.rs |
Version-aware loading, backup creation, atomic writes |
crates/openlogi-core/src/config/tests.rs |
Unit tests for each migration and end-to-end v2→v7 validation |
docs/CONFIGURATION.md |
User-facing schema documentation |
Summary
- Version detection through
schema_versionfield andConfigHeaderparsing enables targeted migrations - Three migration functions handle specific transitions: gesture ownership, transport scoping, and thumb-wheel direction
- Sequential execution with inclusive version checks supports arbitrary upgrade paths
- Atomic backups preserve original configurations on first post-migration save
- Idempotent transformations ensure safe re-application and crash recovery
Frequently Asked Questions
What happens if a migration fails halfway through?
The atomic write infrastructure in file.rs ensures that the original file remains untouched until a complete, validated configuration is ready. Failed migrations propagate errors without partial writes, leaving the pre-migration file intact for recovery.
Can I downgrade from v7 to an older schema version?
OpenLogi does not implement reverse migrations. The backup created on first save provides the only recovery path to pre-migration state. Users should archive this backup if downgrade capability is required.
Does OpenLogi support migrating configurations from forks or modified versions?
The migration system relies on standard schema_version values. Forks using non-standard version numbers or additional fields may require manual adjustment, as the built-in migrations assume specific legacy layouts defined in the upstream repository.
How does the system handle missing or corrupted schema_version fields?
The ConfigHeader parser treats missing versions as legacy (pre-v2) and applies the full migration chain. Corrupted headers trigger deserialization errors before any migration runs, preventing damage to potentially recoverable data.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →