Merge feat/sqlite-migration: add database schema migration system

This merge brings the SQLite migration system from feat/sqlite-migration branch:

## New Features
- Schema version control with SCHEMA_VERSION constant
- Automatic migration of missing columns for providers table
- Dry-run validation mode for schema compatibility checks
- JSON→SQLite migration feature gate (CC_SWITCH_ENABLE_JSON_DB_MIGRATION)
- Settings reload mechanism after imports

## Test Updates
- Updated tests to use SQLite database instead of config.json
- Removed obsolete import_config_from_path tests (replaced by db.import_sql)
- Fixed MCP tests to use unified McpServer structure (v3.7.0+)
- Updated provider switch tests to reflect no-backfill behavior
- Adjusted error type matching for new error variants
This commit is contained in:
Jason
2025-11-25 10:33:54 +08:00
10 changed files with 898 additions and 377 deletions

View File

@@ -113,6 +113,25 @@ const TRAY_SECTIONS: [TrayAppSection; 3] = [
},
];
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum JsonMigrationMode {
Disabled,
DryRun,
Enabled,
}
/// 解析 JSON→DB 迁移模式:默认关闭,支持 dryrun/模拟演练
fn json_migration_mode() -> JsonMigrationMode {
match std::env::var("CC_SWITCH_ENABLE_JSON_DB_MIGRATION") {
Ok(val) => match val.trim().to_ascii_lowercase().as_str() {
"1" | "true" | "yes" | "on" => JsonMigrationMode::Enabled,
"dryrun" | "dry-run" | "simulate" | "sim" => JsonMigrationMode::DryRun,
_ => JsonMigrationMode::Disabled,
},
Err(_) => JsonMigrationMode::Disabled,
}
}
fn append_provider_section<'a>(
app: &'a tauri::AppHandle,
mut menu_builder: MenuBuilder<'a, tauri::Wry, tauri::AppHandle<tauri::Wry>>,
@@ -542,8 +561,10 @@ pub fn run() {
let db_path = app_config_dir.join("cc-switch.db");
let json_path = app_config_dir.join("config.json");
// Check if migration is needed (DB doesn't exist but JSON does)
let migration_needed = !db_path.exists() && json_path.exists();
// Check if config.json→SQLite migration needed (feature gated, disabled by default)
let migration_mode = json_migration_mode();
let has_json = json_path.exists();
let has_db = db_path.exists();
let db = match crate::database::Database::init() {
Ok(db) => Arc::new(db),
@@ -555,19 +576,42 @@ pub fn run() {
}
};
if migration_needed {
log::info!("Starting migration from config.json to SQLite...");
match crate::app_config::MultiAppConfig::load() {
Ok(config) => {
if let Err(e) = db.migrate_from_json(&config) {
log::error!("Migration failed: {e}");
} else {
log::info!("Migration successful");
// Optional: Rename config.json
// let _ = std::fs::rename(&json_path, json_path.with_extension("json.bak"));
if !has_db && has_json {
match migration_mode {
JsonMigrationMode::Disabled => {
log::warn!(
"Detected config.json but migration is disabled by default. \
Set CC_SWITCH_ENABLE_JSON_DB_MIGRATION=1 to migrate, or =dryrun to validate first."
);
}
JsonMigrationMode::DryRun => {
log::info!("Running migration dry-run (validation only, no disk writes)");
match crate::app_config::MultiAppConfig::load() {
Ok(config) => {
if let Err(e) = crate::database::Database::migrate_from_json_dry_run(&config) {
log::error!("Migration dry-run failed: {e}");
} else {
log::info!("Migration dry-run succeeded (no database written)");
}
}
Err(e) => log::error!("Failed to load config.json for dry-run: {e}"),
}
}
JsonMigrationMode::Enabled => {
log::info!("Starting migration from config.json to SQLite (user opt-in)");
match crate::app_config::MultiAppConfig::load() {
Ok(config) => {
if let Err(e) = db.migrate_from_json(&config) {
log::error!("Migration failed: {e}");
} else {
log::info!("Migration successful");
// Optional: Rename config.json to prevent re-migration
// let _ = std::fs::rename(&json_path, json_path.with_extension("json.migrated"));
}
}
Err(e) => log::error!("Failed to load config.json for migration: {e}"),
}
}
Err(e) => log::error!("Failed to load config.json for migration: {e}"),
}
}