OpenLogi schema_version Migration: How Configuration Files Upgrade Automatically
OpenLogi automatically migrates user configuration files between schema versions by detecting the stored schema_version in the TOML file and applying sequential migration functions until the data model matches the current version 7.
The AprilNEA/OpenLogi repository handles user preferences through a TOML-based configuration system where the top-level key schema_version tracks the on-disk format version. When the application starts, the loader checks this version and executes one-time migration functions to preserve user settings while upgrading the data structure. This ensures backward compatibility without manual intervention when users upgrade between OpenLogi releases.
How the Migration Pipeline Works
The migration system follows a strict sequential pipeline defined in crates/openlogi-core/src/config/file.rs. When ConfigFile::load_from_path is invoked, it performs the following steps:
- Parse the TOML file into a temporary structure.
- Validate the
schema_versionfield against the currentSCHEMA_VERSIONconstant (currently 7). - Iterate through each intermediate version and invoke the corresponding migration method on the
Configstruct. - Rewrite the in-memory configuration with updated data structures and bump the version number.
- Save the upgraded file atomically on the next write operation via
ConfigFile::save_atomic.
If the stored schema_version exceeds the current constant (indicating a config from a future release), loading fails immediately with a clear error to prevent silent data loss.
Schema Version History and Migration Functions
The current build defines SCHEMA_VERSION = 7 in crates/openlogi-core/src/config.rs. When OpenLogi encounters older versions, it applies the following transformations in sequence:
v3 to v4: Owner-Locked Gestures
The function migrate_owner_locked_gestures (located at line 59 of config.rs) removes the legacy "gesture-owner" model where only one button could own gestures. This migration rewrites stored GestureOwner entries into per-button Binding::Gesture values and consolidates disabled gestures into DeviceConfig::disabled_gestures.
v4 to v5: Transport-Scoped Keys
The migrate_transport_scoped_keys function (line 39 of config.rs) handles the transition from direct-route keys (formatted as direct:<vid>:<pid>:unit:<hex>) to identity-only keys (unit:<hex>). The migration builds a rename map to update device maps, selected_device, and host_switch_targets while preserving link information in DeviceConfig::links.
v6 to v7: Thumb-Wheel Direction Defaults
Prior to version 7, thumb-wheel defaults scrolled right on "up" and left on "down". The migrate_thumbwheel_native_direction function (line 24 of config.rs) detects these legacy default pairs in both global settings and per-app overrides, rewriting them to match the normalized physical direction introduced in v7.
Implementing Config Loading with Automatic Migration
According to the OpenLogi source code, the ConfigFile::load_from_path method orchestrates the entire migration chain. The following example demonstrates loading a potentially outdated configuration:
use openlogi_core::config::{Config, ConfigFile};
use std::path::Path;
fn main() {
// Load a user config that may be from an older OpenLogi release.
let (mut cfg, _) = ConfigFile::load_from_path(Path::new("config.toml"))
.expect("Failed to load config");
// The config is now at the latest schema (7). You can safely modify it.
cfg.set_device_custom_name("unit:1234abcd", Some("My Mouse".into()));
// When you later call `save_atomic`, the file will be written with the
// up-to-date schema version.
}
The loader ensures that by the time the Config object returns, all migrations have been applied and schema_version equals SCHEMA_VERSION.
Testing Specific Migration Paths
For unit testing or debugging specific upgrade scenarios, you can manually invoke migration functions after setting an older schema version:
// Example of invoking a specific migration manually (useful in tests):
let mut cfg = Config::default();
cfg.schema_version = 4; // Simulate an old file
cfg.migrate_transport_scoped_keys(); // Upgrade to v5 format
assert_eq!(cfg.schema_version, 7); // After the full load path it will be 7
The crates/openlogi-core/src/config/tests.rs file contains comprehensive unit tests verifying each migration path and ensuring that future versions are properly rejected.
Summary
- OpenLogi stores the current schema version (7) in the
SCHEMA_VERSIONconstant defined incrates/openlogi-core/src/config.rs. - The
ConfigFile::load_from_pathmethod infile.rsautomatically detects outdatedschema_versionvalues and triggers sequential migrations. - Three primary migration functions handle specific format changes:
migrate_owner_locked_gestures,migrate_transport_scoped_keys, andmigrate_thumbwheel_native_direction. - Configurations from future versions (where stored version > current constant) are rejected to prevent data corruption.
- All migrations preserve user settings by rewriting data structures while maintaining the semantic meaning of the configuration.
Frequently Asked Questions
What happens if I open a configuration file from a newer OpenLogi version?
OpenLogi validates the stored schema_version against the current SCHEMA_VERSION constant immediately after parsing. If the file indicates a version higher than what the current binary supports, ConfigFile::load_from_path returns an error and refuses to load the configuration. This prevents silent data loss that could occur if the application attempted to downgrade or ignore unknown fields.
Can I manually trigger a specific migration for testing purposes?
Yes. While the standard loading path handles automatic migration, individual migration methods are available as public methods on the Config struct. You can instantiate a Config object, manually set schema_version to simulate an old file state, and call methods like migrate_transport_scoped_keys() directly. The test suite in crates/openlogi-core/src/config/tests.rs demonstrates this pattern for verifying migration correctness.
Where is the schema_version constant defined and maintained?
The current schema version is defined as the constant SCHEMA_VERSION in crates/openlogi-core/src/config.rs. When developers introduce breaking changes to the configuration format, they increment this constant and implement a corresponding migration function that upgrades data from the previous version. This constant serves as the source of truth for the entire migration pipeline.
How does OpenLogi prevent data loss during the migration process?
Migrations operate on an in-memory representation of the configuration before any file write occurs. The original TOML file remains untouched until ConfigFile::save_atomic is explicitly called, ensuring that a failed migration does not corrupt the user's existing configuration. Additionally, each migration function performs targeted rewrites that preserve user values (such as custom names and bindings) while only changing the structural representation.
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 →