# How the Migration System in Clash Nyanpasu Handles Data Upgrades Between Application Versions

> Learn how Clash Nyanpasu's migration system manages data upgrades between app versions. Discover its JSON state file and trait-based runner for automatic data transformations.

- Repository: [Nyanpasu/clash-nyanpasu](https://github.com/libnyanpasu/clash-nyanpasu)
- Tags: internals
- Published: 2026-03-06

---

**The migration system in Clash Nyanpasu uses a persisted JSON state file and a trait-based runner to automatically execute version-specific data transformations when the application starts after an update.**

Clash Nyanpasu ships with a robust migration framework that ensures user data—profiles, settings, and configuration files—remains compatible across application updates. When a user launches a newer version, the migration system in Clash Nyanpasu detects pending upgrades, executes them in sequence, and persists the results to prevent duplicate runs.

## Core Architecture of the Migration System

The framework is built around three core abstractions: the `Migration` trait that defines individual upgrade steps, a persistent JSON store that tracks execution state, and a `Runner` that orchestrates the process.

### The Migration Trait

Each data upgrade implements the `Migration` trait defined in [`backend/tauri/src/core/migration/mod.rs`](https://github.com/libnyanpasu/clash-nyanpasu/blob/main/backend/tauri/src/core/migration/mod.rs). This interface requires the target version, a unique name, the transformation logic, and an optional rollback mechanism.

```rust
pub trait Migration<'a>: DynClone {
    fn version(&self) -> &'a Version;                 // target version, e.g. 2.0.0
    fn name(&self) -> Cow<'a, str>;                  // unique identifier
    fn migrate(&self) -> std::io::Result<()> { … }   // actual transformation
    fn discard(&self) -> std::io::Result<()> { Ok(()) } // rollback on failure
}

```

### Migration State Persistence

The system records progress in a JSON file named [`migration.json`](https://github.com/libnyanpasu/clash-nyanpasu/blob/main/migration.json) located in the application data directory (e.g., `~/.local/share/clash-nyanpasu/migration.json` on Linux). The schema, defined in [`backend/tauri/src/core/migration/db.rs`](https://github.com/libnyanpasu/clash-nyanpasu/blob/main/backend/tauri/src/core/migration/db.rs), stores the last successfully migrated version and a map of migration names to their execution states.

```json
{
  "version": "2.0.0",
  "states": {
    "profile_script_newtype": "Completed",
    "some_other_migration": "Failed"
  }
}

```

### The Runner Orchestrator

The `Runner` struct, also in [`backend/tauri/src/core/migration/mod.rs`](https://github.com/libnyanpasu/clash-nyanpasu/blob/main/backend/tauri/src/core/migration/mod.rs), serves as the high-level orchestrator. It loads the persisted state, determines which migrations are pending, and executes them in order. The runner provides methods like `run_upcoming_units()` to process all pending migrations for the current application version.

## How Migrations Are Defined and Batched

Migrations are organized into logical groups to simplify version management and ensure atomicity where required.

### Migration Units and Version Grouping

The `Unit` enum wraps either a single migration or a batch of migrations that belong to the same semantic version. This allows the system to treat all migrations for version `2.0.0` as a single logical unit, ensuring they run together or not at all.

### The Static UNITS Registry

All available migration units are registered in a compile-time static array located in [`backend/tauri/src/core/migration/units/mod.rs`](https://github.com/libnyanpasu/clash-nyanpasu/blob/main/backend/tauri/src/core/migration/units/mod.rs). This `UNITS` list maps semantic versions to their corresponding `Unit` implementations, ordered chronologically.

```rust
pub static UNITS: &[(Version, Unit<'static, dyn Migration<'static>>)] = &[
    // ... earlier units ...
    (
        Version::new(2, 0, 0),
        Unit::Single(&ProfileScriptNewtype {}),
    ),
    // ... later units ...
];

```

## Execution Flow and Error Handling

The migration system employs a deterministic execution flow with clear decision logic and robust error handling.

### Decision Logic for Pending Migrations

The `Runner` uses the `advice_migration` method to determine whether a specific migration should run. This logic compares the migration's target version against the stored version and checks the previous execution state.

```rust
pub fn advice_migration<'a, T>(&self, migration: &T) -> MigrationAdvice
where
    T: Clone + Migration<'a> + Send + Sync,
{
    let migration_ver = migration.version();
    let store = self.store.borrow();

    // Run only if migration version ≥ stored version
    if migration_ver >= &store.version {
        // Look up previous state for this named migration
        match store.states.get(&migration.name()) {
            Some(MigrationState::Completed) => MigrationAdvice::Done,
            Some(MigrationState::Failed)    => MigrationAdvice::Pending,
            _                               => MigrationAdvice::Ignored,
        }
    } else {
        MigrationAdvice::Ignored
    }
}

```

### Running Migrations and Rollback Mechanism

When `run_upcoming_units` is called, the runner iterates through the `UNITS` array starting from the stored version. For each pending migration, it executes the `migrate` method. If successful, the state is updated to `Completed`. If an error occurs, the state is marked as `Failed` and the optional `discard` method is invoked to roll back any partial changes.

A `DropGuard` ensures that the state file is written back to disk even if the application crashes immediately after a migration completes, preventing data inconsistency.

## Real-World Example: Profile Script Migration (v2.0.0)

The migration that rewrites the old "profile script" format into a new strongly-typed representation demonstrates the practical implementation. Located in [`backend/tauri/src/core/migration/units/unit_200/profile_script_newtype.rs`](https://github.com/libnyanpasu/clash-nyanpasu/blob/main/backend/tauri/src/core/migration/units/unit_200/profile_script_newtype.rs), this migration implements the `Migration` trait for version 2.0.0.

```rust
// backend/tauri/src/core/migration/units/unit_200/profile_script_newtype.rs
impl Migration<'_> for ProfileScriptNewtype {
    fn version(&self) -> &Version { &Version::new(2, 0, 0) }
    fn name(&self) -> Cow<'_, str> { Cow::Borrowed("profile_script_newtype") }

    fn migrate(&self) -> std::io::Result<()> {
        eprintln!("Trying to migrate profiles files...");
        let profiles = migrate_profile_data(profiles);
        // … write back the transformed YAML …
        Ok(())
    }

    fn discard(&self) -> std::io::Result<()> {
        // If migration fails, we can revert the file to its original backup.
        Ok(())
    }
}

```

When a user upgrades from any version prior to 2.0.0, the `Runner` detects this pending migration, executes the data transformation, and updates the stored version to prevent re-execution on subsequent launches.

## Summary

- **Persistence Layer** – The `MigrationFile` struct in [`backend/tauri/src/core/migration/db.rs`](https://github.com/libnyanpasu/clash-nyanpasu/blob/main/backend/tauri/src/core/migration/db.rs) maintains a JSON record of the last successful version and individual migration states.
- **Trait-Based Abstraction** – Each upgrade step implements the `Migration` trait, defining version targets, transformation logic via `migrate()`, and rollback capability via `discard()`.
- **Version Grouping** – The `Unit` enum batches migrations by semantic version, registered in the static `UNITS` array in [`backend/tauri/src/core/migration/units/mod.rs`](https://github.com/libnyanpasu/clash-nyanpasu/blob/main/backend/tauri/src/core/migration/units/mod.rs).
- **Orchestration Logic** – The `Runner` struct provides `advice_migration()` to determine pending work and `run_upcoming_units()` to execute migrations sequentially.
- **Error Handling** – Failed migrations trigger `discard()` for rollback and are marked as `Failed` in the state file for retry on next startup.
- **Atomic Persistence** – A `DropGuard` ensures the migration state is written to disk even if the application crashes post-migration.

## Frequently Asked Questions

### How does Clash Nyanpasu know which migrations to run after an update?

The migration system in Clash Nyanpasu compares the version stored in [`migration.json`](https://github.com/libnyanpasu/clash-nyanpasu/blob/main/migration.json) against the target versions defined in the static `UNITS` registry. The `Runner::advice_migration()` method checks if a migration's version is greater than or equal to the stored version and whether its state is not already `Completed`, returning `Pending` only for migrations that need execution.

### What happens if a migration fails during the upgrade process?

If a migration fails, the `Runner` catches the error and marks the migration state as `Failed` in the persistent store. It then invokes the optional `discard()` method on the migration implementation to roll back any partial changes. On the next application startup, the migration will be retried since its state remains `Failed` rather than `Completed`.

### Where is the migration state stored on disk?

The migration state is persisted in a JSON file named [`migration.json`](https://github.com/libnyanpasu/clash-nyanpasu/blob/main/migration.json) located in the application data directory, typically `~/.local/share/clash-nyanpasu/migration.json` on Linux or equivalent paths on other platforms. This file stores the last successfully migrated version and a map of individual migration names to their execution states (`NotStarted`, `InProgress`, `Completed`, or `Failed`).

### Can developers test migrations before releasing a new version?

Yes, developers can test migrations by invoking the `Runner` with `skip_advice` set to `true`, which forces the execution of migrations regardless of the stored version state. This is particularly useful in nightly builds or development environments to verify that new migrations in `backend/tauri/src/core/migration/units/` correctly transform data without waiting for an actual version upgrade scenario.