# How OpenLogi Handles Configuration Schema Migrations: Inside the Version-Gated Migration System

> Discover how OpenLogi automatically handles configuration schema migrations with its version-gated system. Ensure backward compatibility seamlessly during Config::load_from_path.

- Repository: [Xuan Zhang/OpenLogi](https://github.com/AprilNEA/OpenLogi)
- Tags: internals
- Published: 2026-09-08

---

**OpenLogi automatically migrates outdated TOML configuration files to the current schema version by running a series of private migration helpers during `Config::load_from_path`, ensuring backward compatibility without user intervention.**

OpenLogi stores user preferences in a TOML configuration file whose shape evolves as new features are added. To keep old configuration files usable across updates, the core crate (`openlogi-core`) embeds a **configuration schema migration** system that runs automatically on load, transforming legacy data structures into the current canonical format.

## How OpenLogi Tracks Configuration Schema Versions

Each configuration file contains a `schema_version` field that marks its structural generation. The current build defines `SCHEMA_VERSION = 7` in [`crates/openlogi-core/src/config.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-core/src/config.rs) at line 94. When `Config::load_from_path` reads a file, it compares the stored version against this constant. If the file’s version is older, the loader invokes a pipeline of migration functions to bring the data up to date before the application uses it.

## The Migration Pipeline: Three Private Helpers

The load routine calls three private migration helpers in sequence inside [`crates/openlogi-core/src/config.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-core/src/config.rs). Each function handles a specific historical transition:

### `migrate_owner_locked_gestures` (v3 to v4)

Located at lines 387-436, this function upgrades legacy "owner-locked" gesture layouts from schema versions 3 and 4. The old model stored a single "gesture owner" and dormant gesture maps. The migration converts those maps into explicit per-button `Binding::Gesture` objects, demotes non-owner gestures to simple clicks, and injects a canonical gesture button if missing. This eliminates the owner-lock concept entirely, producing a self-contained set of bindings.

### `migrate_transport_scoped_keys` (v5)

Defined at lines 458-522, this migration rewrites transport-scoped device keys introduced in v5. Keys following the pattern `direct:<vid>:<pid>:<identity>` (for example, `direct:046d:c08d:unit:6be9d300`) are parsed to extract the stable `<identity>` fragment. The device is then re-indexed under this new key, while a `DeviceConfig::links` entry records the discarded route. The function also consolidates duplicate entries—such as the same mouse reachable via USB and Bluetooth—and updates all references (`selected_device`, `host_switch_targets`) to point to the new stable key.

### `migrate_thumbwheel_native_direction` (v7)

Found at lines 534-564, this function fixes pre-v7 thumb-wheel scroll defaults. Before v7, "up" mapped to `HorizontalScrollRight` and "down" to `HorizontalScrollLeft`. Version 7 swapped these defaults to match physical wheel polarity. The migration detects bindings still using the old defaults (or absent bindings implying the old defaults) and rewrites them to the new actions, leaving any custom user bindings untouched.

## Automatic Migration Execution and Persistence

The migration functions are **private** and invoked only during the load sequence in [`crates/openlogi-core/src/config/file.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-core/src/config/file.rs). After the transformations complete, the configuration is saved back to disk with the updated `schema_version`, unless the config is marked as `ephemeral`. This ensures subsequent runs operate on the canonical format without re-processing legacy data.

```rust
use openlogi_core::config::{Config, ConfigFile};
use std::path::Path;

fn load_user_config(path: &Path) -> Result<Config, ConfigError> {
    // Loads the file, automatically runs migrations if needed
    Config::load_from_path(path)
}

```

While rarely necessary, you can manually force a migration loop if you need to re-normalize data:

```rust
// Manually forcing a migration (rarely needed – the load path does it):
let mut cfg = Config::load_from_path(&path)?;
cfg.migrate_transport_scoped_keys(); // rewrites historic direct keys
cfg.save(&path)?; // persists the migrated file

```

## Unit Testing Configuration Migrations

The migration logic is exercised by dedicated unit tests within the inline test module in [`crates/openlogi-core/src/config.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-core/src/config.rs) (around line 1000). Specific test cases include `migrates_v1_button_and_gesture_bindings` and `migrates_pre_v7_thumbwheel_defaults_to_normalised_native_direction`. These tests instantiate legacy TOML snippets, invoke `Config::load_from_path`, and assert that the resulting in-memory structures match the expected post-migration state, ensuring that upgrades remain correct across refactors.

## Summary

- **OpenLogi** uses a `schema_version` field and the `SCHEMA_VERSION` constant to detect outdated TOML configuration files.
- Three private migration functions—`migrate_owner_locked_gestures`, `migrate_transport_scoped_keys`, and `migrate_thumbwheel_native_direction`—handle upgrades from v3, v5, and pre-v7 schemas respectively.
- Migrations run automatically inside `Config::load_from_path` and persist the updated file unless the configuration is ephemeral.
- Unit tests in [`config.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/config.rs) validate each migration path using historical configuration snippets.

## Frequently Asked Questions

### What happens if my OpenLogi config file is from an older schema version?

OpenLogi detects the version mismatch during `Config::load_from_path` and automatically executes the required migration functions to upgrade your data to the current `SCHEMA_VERSION = 7`. Your old gestures, device keys, and wheel bindings are rewritten to the new format without data loss.

### Does OpenLogi migration modify my original configuration file?

Yes, unless the configuration is marked as `ephemeral`. After migrating the in-memory structure, OpenLogi saves the updated TOML back to disk with the new `schema_version`, ensuring future loads skip the migration steps.

### How does OpenLogi handle duplicate device entries during migration?

During the `migrate_transport_scoped_keys` step, OpenLogi detects when the same physical device appears under different transport routes (for example, USB and Bluetooth). It folds these duplicates into a single entry indexed by the stable identity fragment while preserving alternative routes in the `DeviceConfig::links` field.

### Can I manually trigger a configuration migration in OpenLogi?

The migration methods are private and normally run automatically, but you can force the process by loading the configuration, calling the private methods through internal APIs if exposed, or simply loading and saving the file. The standard `Config::load_from_path` routine handles this transparently for most users.