# OpenLogi schema_version Migration: How Configuration Files Upgrade Automatically

> OpenLogi automatically migrates user config files between schema versions. Detects schema_version in TOML and applies sequential migration functions for seamless upgrades.

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

---

**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`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-core/src/config/file.rs). When `ConfigFile::load_from_path` is invoked, it performs the following steps:

1. **Parse** the TOML file into a temporary structure.
2. **Validate** the `schema_version` field against the current `SCHEMA_VERSION` constant (currently **7**).
3. **Iterate** through each intermediate version and invoke the corresponding migration method on the `Config` struct.
4. **Rewrite** the in-memory configuration with updated data structures and bump the version number.
5. **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`](https://github.com/AprilNEA/OpenLogi/blob/main/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`](https://github.com/AprilNEA/OpenLogi/blob/main/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`](https://github.com/AprilNEA/OpenLogi/blob/main/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`](https://github.com/AprilNEA/OpenLogi/blob/main/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:

```rust
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:

```rust
// 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`](https://github.com/AprilNEA/OpenLogi/blob/main/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_VERSION` constant defined in [`crates/openlogi-core/src/config.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-core/src/config.rs).
- The `ConfigFile::load_from_path` method in [`file.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/file.rs) automatically detects outdated `schema_version` values and triggers sequential migrations.
- Three primary migration functions handle specific format changes: `migrate_owner_locked_gestures`, `migrate_transport_scoped_keys`, and `migrate_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`](https://github.com/AprilNEA/OpenLogi/blob/main/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`](https://github.com/AprilNEA/OpenLogi/blob/main/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.