# How OpenLogi's Configuration Migration System Manages Schema Upgrades from v2/v3 to v7

> Learn how OpenLogi's configuration migration system handles schema upgrades from v2/v3 to v7 using an incremental pipeline and atomic backups. Preserve your data seamlessly.

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

---

**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`](https://github.com/AprilNEA/OpenLogi/blob/main/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`](https://github.com/AprilNEA/OpenLogi/blob/main/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`](https://github.com/AprilNEA/OpenLogi/blob/main/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`](https://github.com/AprilNEA/OpenLogi/blob/main/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 `Single` binding 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`](https://github.com/AprilNEA/OpenLogi/blob/main/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 `LinkConfig` entries
- Updates all references including `selected_device` and `host_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`](https://github.com/AprilNEA/OpenLogi/blob/main/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:

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

1. **v2 → v3**: Device map transitions from model-scoped to physical-device keys. Handled transparently by `RawDeviceConfig` deserialization without explicit migration code.

2. **v3 → v4**: `migrate_owner_locked_gestures` removes owner-locked gesture mode.

3. **v4 → v5**: `migrate_transport_scoped_keys` strips transport prefixes from device keys.

4. **v5 → v6**: Short/long button binding thresholds added. Handled by the deserializer; no migration shim required.

5. **v6 → v7**: `migrate_thumbwheel_native_direction` normalizes 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`](https://github.com/AprilNEA/OpenLogi/blob/main/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`](https://github.com/AprilNEA/OpenLogi/blob/main/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

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

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

```rust
#[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`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-core/src/config.rs) | `Config` struct, `SCHEMA_VERSION`, migration methods |
| [`crates/openlogi-core/src/config/file.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-core/src/config/file.rs) | Version-aware loading, backup creation, atomic writes |
| [`crates/openlogi-core/src/config/tests.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-core/src/config/tests.rs) | Unit tests for each migration and end-to-end v2→v7 validation |
| [`docs/CONFIGURATION.md`](https://github.com/AprilNEA/OpenLogi/blob/main/docs/CONFIGURATION.md) | User-facing schema documentation |

## Summary

- **Version detection** through `schema_version` field and `ConfigHeader` parsing 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`](https://github.com/AprilNEA/OpenLogi/blob/main/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.