docs/references/data/v2-migration-guide.md
Architecture for the new one-shot migration from the legacy Dexie + Redux Persist stores into the SQLite schema. This module owns orchestration, data access helpers, migrator plugins, and IPC entry points used by the renderer migration window.
The v2 migration system enforces a linear upgrade path to ensure data integrity:
v1.old → v1.last (≥1.9.0) → v2.0.0 → v2.x
v2.0.0 contains the one-shot data migration from Redux/Dexie to SQLite. Supporting migration from every v1 version would create an O(n²) test matrix. By requiring all users to be on the final v1 release first, the migration code only needs to handle a single source data format.
version.log file to {userData}/ on every launch where the
version changes.v2MigrationGate.ts reads version.log via
MigrationPaths.versionLogFile (using the resolved userData path
that accounts for v1 custom directories).| Scenario | Block reason | User action |
|---|---|---|
No version.log (v1 < 1.7 user) | no_version_log | Install v1.last, run once, then install v2.0.0 |
| Previous version < 1.9.0 | v1_too_old | Upgrade to v1.last first |
| Previous version is v1.x but current > v2.0.0 | v2_gateway_skipped | Install v2.0.0 first |
v2.0.0 pre-releases (alpha/beta/rc) are treated as before v2.0.0
per semver ordering. They are allowed as migration targets from v1.last
(the gateway check coerces currentVersion, so 2.0.0-alpha → 2.0.0
passes). Pre-release to pre-release upgrades work because migration
status is completed after the first successful run.
The gateway is strictly v2.0.0 — v2.0.x patches are blocked from being a first migration target. This may be relaxed in a future release.
The auto-updater (AppUpdaterService) sends the installed version and
other client metadata to the managed release service,
which selects the OTA target and enforces upgrade gateways. The migration
gate is a separate safety net for users who manually download and
install a version. Both systems enforce compatible upgrade paths but
operate independently.
src/main/data/migration/v2/
├── core/ # Engine + shared context
├── migrators/ # Domain-specific migrators and mappings
├── utils/ # Data source readers (Redux, Dexie, streaming JSON)
├── window/ # IPC handlers + migration window manager
└── index.ts # Public exports for main process
core/MigrationEngine.ts coordinates all migrators in order, surfaces progress to the UI, and uses app_state.key = 'migration_v2_status' as the only durable migration marker. It clears new-schema tables before running and aborts on validation or global foreign-key failure.core/MigrationPaths.ts defines MigrationPaths (a frozen object of pre-computed paths) and resolveMigrationPaths() which detects v1 legacy userData directories from ~/.cherrystudio/config/config.json. Called once at the migration gate entry, before engine initialization. All migration code uses these paths instead of app.getPath() — see the Path safety convention below.core/MigrationContext.ts builds the shared context passed to every migrator:
sources: ConfigManager (ElectronStore), ReduxStateReader (parsed Redux Persist data), DexieFileReader (JSON exports), LegacyHomeConfigReader (v1 ~/.cherrystudio/config/config.json for the config-file migration path used by BootConfigMigrator)db: current SQLite connectionpaths: MigrationPaths — pre-computed filesystem paths; migrators that need file paths use ctx.paths instead of app.getPath()sharedData: Map for passing cross-cutting info between migratorslogger: loggerService scoped to migration@shared/data/migration/v2/types defines stages, results, and validation stats used across main and renderer.migrators/BaseMigrator.ts and implement:
id, name, description, order (lower runs first)prepare(ctx): dry-run checks, counts, and staging data; return PrepareResultexecute(ctx): perform inserts/updates; manage your own transactions; report progress via reportProgress; self-check FK integrity of owned tables via assertOwnedForeignKeys (see Conventions → Foreign keys)validate(ctx): verify counts and integrity; return ValidateResult with stats (sourceCount, targetCount, skippedCount) and any errorsmigrators/migratorRegistry.ts so the engine can sort and run them.migrators/README-<name>.md for detailed documentation):
PreferencesMigrator (implemented): maps ElectronStore + Redux settings to the preference table using mappings/PreferencesMappings.ts.ChatMigrator (implemented): migrates topics and messages from Dexie to SQLite. See README-ChatMigrator.md.AiUsageRecordMigrator (implemented): projects usage-bearing migrated chat and agent-session messages into the best-effort AI usage read model. See README-AiUsageRecordMigrator.md.BootConfigMigrator (implemented, file-target): migrates early-boot settings into the file-based bootConfigService (~/.cherrystudio/boot-config.json) rather than a SQLite table. Reads from Redux (disableHardwareAcceleration) and from the v1 home config file (~/.cherrystudio/config/config.json's appDataPath → app.user_data_path) via a 'configfile' source kind. See README-BootConfigMigrator.md.AssistantMigrator, KnowledgeMigrator (placeholders): scaffolding and TODO notes for future tables.loggerService with a migrator-specific context.MigrationContext.sources instead of accessing raw files/stores directly.sharedData to pass IDs or lookup tables between migrators (e.g., assistant -> chat references) instead of re-reading sources.JsonStreamReader) and batch inserts to avoid memory spikes.PRAGMA foreign_keys = OFF once on that connection (in MigrationDbService) and it stays in effect for the entire migration — there is no per-transaction reconnection that could reset it. This lets bulk inserts carry not-yet-resolved references (self-referencing message.parentId, or cross-domain refs a later migrator resolves). Integrity is verified in two layers: (1) each migrator calls this.assertOwnedForeignKeys(ctx.db, [...]) at the end of execute() for the tables it owns, giving early, well-attributed failures; (2) the engine runs a whole-database PRAGMA foreign_key_check after all migrators complete (MigrationEngine.verifyForeignKeys) as the final backstop.
assistant_knowledge_base.knowledgeBaseId is written by AssistantMigrator but only becomes valid after KnowledgeMigrator remaps/prunes it, so KnowledgeMigrator self-checks that table, not AssistantMigrator. Dedicated file association tables (for example chat_message_file_ref) may be self-checked by the migrator that owns both the source rows and ref rows.targetCount < sourceCount - skippedCount or if ValidateResult.errors is non-empty.AgentsMigrator copies legacy Agent files into the v2 layout but never deletes agents.db or legacy short-ID workspaces.ctx.paths (the MigrationPaths object). NEVER call app.getPath('userData') or construct paths with path.join from scratch. Doing so bypasses the v1 legacy userData detection and may cause data loss for users with custom appDataPath configurations. If you need a path not yet in MigrationPaths, add it to the interface — do not inline it.utils/ReduxStateReader.ts: safe accessor for categorized Redux Persist data with dot-path lookup.utils/DexieFileReader.ts: reads exported Dexie JSON tables; can stream large tables.utils/JsonStreamReader.ts: streaming reader with batching, counting, and sampling helpers for very large arrays.utils/LegacyHomeConfigReader.ts: synchronously reads the v1 ~/.cherrystudio/config/config.json file and normalizes its appDataPath field (both the legacy string shape and the current { executablePath, dataPath }[] shape) into a Record<executablePath, dataPath> | null. Used exclusively by BootConfigMigrator's 'configfile' source.window/MigrationIpcHandler.ts exposes IPC channels for the migration UI:
window/MigrationWindowManager.ts creates the frameless migration window, handles lifecycle, and relaunch instructions after completion in production.migrators/mappings/.prepare/execute/validate with explicit counts, batch inserts, and integrity checks.reportProgress so UI shows per-migrator progress.migrators/migratorRegistry.ts with the correct order.MigrationEngine.verifyAndClearNewTables once those tables exist.execute() via this.assertOwnedForeignKeys(ctx.db, [...ownedTables]), excluding cross-domain-deferred refs and shared polymorphic tables (see Conventions → Foreign keys). Do NOT toggle PRAGMA foreign_keys yourself — the engine keeps it OFF for the whole migration.migrators/README-<MigratorName>.md with detailed documentation including:
Legacy Redux/Dexie → SQLite migrators for sortable resources must produce order_key values for every row they insert. The v2 migrator layer owns a pair of pure functions under src/main/data/migration/v2/utils/orderKey.ts that handle this without touching the DB — they take a pre-flattened array and return the same rows with orderKey attached.
| Helper | Shape | Use for |
|---|---|---|
assignOrderKeysInSequence(rows) | Returns rows with one monotonically increasing orderKey per row. | Whole-table ordering (e.g. mcp_server, user_provider, miniapp). |
assignOrderKeysByScope(rows, getScope) | Groups rows by the scope key, stamps each bucket independently (independent key spaces per bucket). | Partitioned tables (e.g. user_model.providerId, group.entityType). |
Pattern — flatten first, stamp last: keep transform* functions pure (no index parameter, no sortOrder argument); flatten the legacy source into an array, then stamp keys onto the whole array:
import { assignOrderKeysByScope, assignOrderKeysInSequence } from '@data/migration/v2/utils/orderKey'
// Before — each transform took an index and emitted a sortOrder
const rows = legacyServers.map((src, i) => transformMcpServer(src, i).row)
// After — transforms are pure; keys are assigned after the flatten
const rows = legacyServers.map((src) => transformMcpServerV2(src).row)
const stamped = assignOrderKeysInSequence(rows)
await tx.insert(mcpServerTable).values(stamped)
// Partitioned example — each providerId becomes its own independent key space
const stamped = assignOrderKeysByScope(userModels, (m) => m.providerId)
Import rule — never reach for fractional-indexing directly: the migrator helpers delegate to generateOrderKeySequence exported from src/main/data/services/utils/orderKey.ts, which is the single sanctioned integration point for the library. Migrator code, migration scripts, and drizzle custom-migration callbacks all re-import from that service-layer wrapper. This keeps the library boundary auditable and leaves a single place to change the character set or swap implementations.
For the runtime counterparts (insertWithOrderKey / insertManyWithOrderKey / applyMoves / resetOrder) used outside the migration window, see Reorder Guide — Server-Side Service Helpers.