Back to Cherry Studio

Data Migration System

src/main/data/migration/v2/README.md

2.0.06.5 KB
Original Source

Data Migration System

This directory contains the v2 data migration implementation.

Documentation

Directory Structure

src/main/data/migration/v2/
├── core/              # MigrationEngine, MigrationContext, MigrationPaths
├── migrators/         # Domain-specific migrators
│   └── mappings/      # Mapping definitions
├── migrationDiagnosticBundle.ts # Migration diagnostic ZIP builder
├── utils/             # ReduxStateReader, DexieFileReader, JSONStreamReader, LegacyHomeConfigReader
├── window/            # IPC handlers, window manager
└── index.ts           # Public exports

Path Safety — Use MigrationPaths (Strict Requirement)

⚠️ WARNING: Not using predefined paths may cause user data loss.

v1 users may have configured a custom userData directory via ~/.cherrystudio/config/config.json. If migration code calls app.getPath('userData') or new Store() directly, on the first v2 launch it will read from the Electron default path instead of the user's actual data directory — causing migration to be silently skipped or to migrate empty data, making user data appear lost.

All migration code MUST use the pre-computed path constants from MigrationPaths. NEVER call app.getPath() directly or construct paths with path.join() from scratch inside migration code.

Correct ✅Wrong ❌
ctx.paths.userDataapp.getPath('userData')
ctx.paths.databaseFilepath.join(app.getPath('userData'), 'Data', 'cherrystudio.sqlite')
ctx.paths.legacyClaudeConfigDirpath.join(ctx.paths.userData, '.claude')
ctx.paths.legacyClaudeProjectsDirpath.join(ctx.paths.userData, '.claude', 'projects')
ctx.paths.claudeConfigDirpath.join(ctx.paths.userData, 'Data', 'Agents', '.claude')
ctx.paths.claudeProjectsDirpath.join(ctx.paths.userData, 'Data', 'Agents', '.claude', 'projects')
ctx.paths.knowledgeBaseDirpath.join(app.getPath('userData'), 'Data', 'KnowledgeBase')
ctx.paths.legacyConfigFilepath.join(os.homedir(), '.cherrystudio', 'config', 'config.json')
new Store({ cwd: ctx.paths.userData })new Store()

MigrationPaths is resolved once at the migration gate entry by resolveMigrationPaths() (including v1 legacy userData detection), then passed through MigrationContext.paths to all migrators. If you need a new path, add it to the MigrationPaths interface — do not construct it inline.

Narrow exception: application logger output

Migration diagnostics read the application's own logger output, not v1/v2 migration source data. This change records one narrow exception to the rule above: logs MUST resolve through application.getPath('app.logs') and MUST NOT be added to MigrationPaths. No broader migration filesystem access rule is relaxed.

Migration Diagnostic Bundle

Only Renderer error/version-incompatible pages can request a bundle; there is no native preboot entry. A bundle contains minimal system information plus a stable snapshot for one log date: prefer the failure panel's mount-time local date, otherwise choose the latest eligible date, and never mix dates. If a complete snapshot cannot be formed, publish metadata only and disclose that result in the UI only when the destination can still be proven safe. If destination or source identity cannot be established, saving fails without replacing the existing file. Metadata excludes failure stacks, paths, and run/process fields. Logs may be sensitive and must not be shared publicly or outside Cherry Studio support.

Version Compatibility Gate

Before the migration window is created, the gate validates the upgrade path using core/versionPolicy.ts. This catches manual installs that bypass the auto-updater's version filtering.

Required upgrade path: v1.old → v1.last (≥1.9.12) → v2.0.0 → v2.x

Blocking rules

RuleConditionReason
no_version_logLegacy data exists but version.log is missingUser never ran a v1 version with VersionService (embedded since v1.7)
v1_too_oldpreviousVersion < V1_REQUIRED_VERSIONData not in final v1 form
v2_gateway_skippedpreviousVersion < 2.0.0 && coerce(currentVersion) > 2.0.0Skipped the v2.0.0 migration gateway

Pre-release versions

v2.0.0 pre-releases (alpha/beta/rc) are treated as before v2.0.0 in semver ordering. This means:

  • v1.last → v2.0.0-alpha is allowed (the gateway check uses coerced currentVersion, so gt('2.0.0', '2.0.0') is false)
  • Pre-release → pre-release upgrades work because migration status is already completed after the first successful run
  • v2.0.0 is strictly required as the gateway — v2.0.x patches are blocked until the policy is updated in a future release

Path safety for version.log

The version check reads paths.versionLogFile (resolved by MigrationPaths), NOT VersionService's cached path. This is critical for v1 users with custom userData directories — see the Path Safety section above.

Quick Reference

Creating a New Migrator

  1. Extend BaseMigrator in migrators/
  2. Implement prepare, execute, and validate
  3. Add it to the getAllMigrators() list in migrators/migratorRegistry.ts
  4. Use ctx.paths for all filesystem paths — NEVER call app.getPath() directly

Key Contracts

  • prepare(ctx): Dry-run checks, return counts
  • execute(ctx): Perform inserts, report progress
  • validate(ctx): Verify counts and integrity

AssistantMigrator also owns v1 assistant tag-group migration: it inserts group(entityType='assistant') rows and assigns their IDs to assistant.groupId in the same transaction.

Foreign Keys Caveat

The engine keeps foreign_keys = OFF for the entire migration: MigrationDbService sets the pragma once after migrations run. better-sqlite3 keeps a single connection, so that pragma persists for the whole migration with no per-transaction replay. Migrators must NOT toggle FK themselves. Verify integrity with this.assertOwnedForeignKeys(ctx.db, [...]) at the end of execute() (own, fully-resolved tables only — exclude cross-domain-deferred and shared polymorphic tables); the engine runs a final whole-database foreign_key_check as backstop. See the migration guide for details.