> ## Documentation Index
> Fetch the complete documentation index at: https://cindel.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Schema Migrations: Evolving Your Cindel Data Model

> Use CindelMigrationPlan and CindelMigrationStep to safely evolve your data schema across app versions with typed export and import steps.

When you change a field name, type, or model structure, the data already on a user's device still reflects the old shape. Migrations let you open that existing data with the old schemas, convert it to the current shape, and continue with the new generated API — all transparently at app startup.

## How Cindel Runs Migrations

Cindel stores a database-level migration version inside every database file. When you open the database, `CindelMigrationPlan` compares the stored version with `targetVersion`, runs any missing steps in order, persists each successful `toVersion` after the step completes, and returns a database opened with the target schemas. Completed steps are skipped on future opens; only the outstanding steps run.

```dart theme={null}
final db = await Cindel.open(
  directory: appDataDirectory.path,
  schemas: [UserSchema],
  migrationPlan: migrations,
);
```

## Core Types

### `CindelMigrationPlan`

Declare a plan with a `targetVersion`, a `baselineVersion` (for databases that have no stored version yet), and an ordered list of steps:

```dart theme={null}
final migrations = CindelMigrationPlan(
  targetVersion: 2,
  baselineVersion: 1,
  steps: [
    migrateUsersFrom1To2,
  ],
);
```

Pass the same plan every time you open the database. Steps that have already been applied are skipped automatically.

### `CindelMigrationStep`

Each step describes exactly one version move and carries its own schemas, migration logic, and optional verification hooks:

| Field           | Role                                                      |
| --------------- | --------------------------------------------------------- |
| `fromVersion`   | The version this step starts from                         |
| `toVersion`     | The version this step completes                           |
| `openSchemas`   | Schemas that can read the existing stored shape           |
| `targetSchemas` | Schemas that should exist after the step                  |
| `migrate`       | Required migration callback                               |
| `verifyBefore`  | Optional — throw to abort before data is rewritten        |
| `verifyAfter`   | Optional — throw to abort if the migrated result is wrong |

## Step Lifecycle

Every migration step follows the same ordered lifecycle:

<Steps>
  <Step title="Export old data">
    Use `context.exportObjects(schema)` to read old typed objects, or `context.exportDocuments(schema)` to read raw map-shaped documents.

    ```dart theme={null}
    // Typed export — easy when the old model still maps well to stored data.
    final oldUsers = await context.exportObjects(OldUserSchema);

    // Document export — useful when fields are being heavily renamed.
    final oldDocuments = await context.exportDocuments(OldUserSchema);
    ```

    Both APIs read data in id order with bounded batches, so the process is predictable regardless of collection size.
  </Step>

  <Step title="Register target schemas">
    Call `context.registerTargetSchemas()` to prepare the target collections before you write any converted data into them.

    ```dart theme={null}
    await context.registerTargetSchemas();
    ```

    Do not import target objects before this call.
  </Step>

  <Step title="Import converted data">
    Use `context.importObjects(schema, iterable)` for new typed objects, or `context.importDocuments(schema, iterable)` for target documents.

    ```dart theme={null}
    await context.importObjects(
      UserSchema,
      oldUsers.map(User.fromLegacy),
    );
    ```

    Make sure required fields are filled, removed fields are no longer referenced, and ids are preserved when records should remain the same logical objects.
  </Step>

  <Step title="Verify before and after (optional)">
    The `verifyBefore` hook runs before data is rewritten. The `verifyAfter` hook runs after. Throw from either hook to abort the step and prevent it from being marked complete.

    ```dart theme={null}
    verifyAfter: (context) async {
      final version = await context.database.schemaVersion('users');
      if (version == null) {
        throw StateError('Users schema was not registered.');
      }
    },
    ```

    Useful checks: required collections exist, document counts match expectations, ids were preserved, and converted values are valid for the new model.
  </Step>
</Steps>

## Complete Example: Renaming a Field

This example migrates a `User` model from version 1 to version 2, where the `fullName` field is renamed to `name` and the `email_address` persisted field is normalised to `email`.

```dart theme={null}
// Conversion helper — preserves dbId so records remain the same logical objects.
extension UserFromLegacy on User {
  static User fromLegacy(OldUser old) {
    return User()
      ..dbId = old.dbId
      ..email = old.email
      ..name = old.fullName
      ..active = old.deletedAt == null;
  }
}

// The migration step.
final migrateUsersFrom1To2 = CindelMigrationStep(
  fromVersion: 1,
  toVersion: 2,
  openSchemas: [OldUserSchema],
  targetSchemas: [UserSchema],
  verifyBefore: (context) async {
    // Check that the old collection is readable — throw to abort if not.
    await context.database.documentIds('users');
  },
  migrate: (context) async {
    final oldUsers = await context.exportObjects(OldUserSchema);

    await context.registerTargetSchemas();

    await context.importObjects(
      UserSchema,
      oldUsers.map(User.fromLegacy),
    );
  },
  verifyAfter: (context) async {
    final version = await context.database.schemaVersion('users');
    if (version == null) {
      throw StateError('Users schema was not registered.');
    }
  },
);

// The plan — passed to Cindel.open() on every app start.
final migrations = CindelMigrationPlan(
  targetVersion: 2,
  baselineVersion: 1,
  steps: [migrateUsersFrom1To2],
);

final db = await Cindel.open(
  directory: appDataDirectory.path,
  schemas: [UserSchema],
  migrationPlan: migrations,
);

// After migration, use the current generated API normally.
final users = await db.users.all().findAll();
```

## Automatic Compaction

Set `compactOnSuccess: true` on your `CindelMigrationPlan` to have Cindel call `compact()` automatically after a migration finishes. This reclaims storage space freed by replacing old records and is the preferred way to compact after a migration.

```dart theme={null}
final migrations = CindelMigrationPlan(
  targetVersion: 2,
  baselineVersion: 1,
  compactOnSuccess: true,
  steps: [migrateUsersFrom1To2],
);
```

## Best Practices

* **Keep steps small and single-purpose.** One step should handle exactly one version move. For two releases of schema changes, write two steps — not one step that tries to handle every old shape.
* **Preserve `dbId`s.** When existing records should remain the same logical objects after migration, copy `old.dbId` into the new object during conversion.
* **Always pass the migration plan at startup.** Leave the plan in place until you are certain that no supported device can have a database below `targetVersion`. Removing the plan prematurely leaves some users unable to open their database.
* **Choose your export format deliberately.** Use `exportObjects` when the old schema still produces useful Dart objects. Use `exportDocuments` when fields are being renamed heavily and you need direct access to persisted field names.

<Warning>
  Do not remove the `CindelMigrationPlan` from `Cindel.open()` until every supported database is already at `targetVersion`. Removing it early means users still running an older database version will open without migration, potentially causing a crash or data corruption.
</Warning>

## Low-Level Primitives

`migrationVersion()`, `setMigrationVersion`, `registerMigratedSchemas`, and `compact()` are low-level primitives exposed for controlled tooling and diagnostics. Do not call them directly in application migration code — use `CindelMigrationPlan` and `CindelMigrationStep` instead.
