mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-08-03 11:01:22 +08:00
feat: add dry-run mode for JSON→SQLite migration
Core changes: - Extract transaction logic into reusable migrate_from_json_tx() helper - Add migrate_from_json_dry_run() for in-memory validation without disk writes - Implement three-state migration mode enum (Disabled/DryRun/Enabled) - Support CC_SWITCH_ENABLE_JSON_DB_MIGRATION=dryrun for safe testing Code quality improvements: - Remove redundant migration_needed variable - Unify all log messages to English - Use info level for user-initiated operations instead of warn - Add explicit drop(tx) in dry-run to clarify intent Testing: - Add dry_run_does_not_write_to_disk test - Add dry_run_validates_schema_compatibility test with real data - All 6 database tests passing, zero clippy warnings This enables users to safely validate migration compatibility before committing to the database, catching schema mismatches early.
This commit is contained in:
+112
-2
@@ -721,6 +721,34 @@ impl Database {
|
|||||||
.transaction()
|
.transaction()
|
||||||
.map_err(|e| AppError::Database(e.to_string()))?;
|
.map_err(|e| AppError::Database(e.to_string()))?;
|
||||||
|
|
||||||
|
Self::migrate_from_json_tx(&tx, config)?;
|
||||||
|
|
||||||
|
tx.commit()
|
||||||
|
.map_err(|e| AppError::Database(format!("Commit migration failed: {e}")))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run migration dry-run in memory for pre-deployment validation (no disk writes)
|
||||||
|
pub fn migrate_from_json_dry_run(config: &MultiAppConfig) -> Result<(), AppError> {
|
||||||
|
let mut conn =
|
||||||
|
Connection::open_in_memory().map_err(|e| AppError::Database(e.to_string()))?;
|
||||||
|
Self::create_tables_on_conn(&conn)?;
|
||||||
|
Self::apply_schema_migrations_on_conn(&conn)?;
|
||||||
|
|
||||||
|
let tx = conn
|
||||||
|
.transaction()
|
||||||
|
.map_err(|e| AppError::Database(e.to_string()))?;
|
||||||
|
Self::migrate_from_json_tx(&tx, config)?;
|
||||||
|
|
||||||
|
// Explicitly drop transaction without committing (in-memory DB discarded anyway)
|
||||||
|
drop(tx);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn migrate_from_json_tx(
|
||||||
|
tx: &rusqlite::Transaction<'_>,
|
||||||
|
config: &MultiAppConfig,
|
||||||
|
) -> Result<(), AppError> {
|
||||||
// 1. 迁移 Providers
|
// 1. 迁移 Providers
|
||||||
for (app_key, manager) in &config.apps {
|
for (app_key, manager) in &config.apps {
|
||||||
let app_type = app_key; // "claude", "codex", "gemini"
|
let app_type = app_key; // "claude", "codex", "gemini"
|
||||||
@@ -862,8 +890,6 @@ impl Database {
|
|||||||
.map_err(|e| AppError::Database(format!("Migrate settings failed: {e}")))?;
|
.map_err(|e| AppError::Database(format!("Migrate settings failed: {e}")))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
tx.commit()
|
|
||||||
.map_err(|e| AppError::Database(format!("Commit migration failed: {e}")))?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1659,4 +1685,88 @@ mod tests {
|
|||||||
Some("1")
|
Some("1")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dry_run_does_not_write_to_disk() {
|
||||||
|
use crate::app_config::MultiAppConfig;
|
||||||
|
use crate::provider::ProviderManager;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
// Create minimal valid config for migration
|
||||||
|
let mut apps = HashMap::new();
|
||||||
|
apps.insert("claude".to_string(), ProviderManager::default());
|
||||||
|
|
||||||
|
let config = MultiAppConfig {
|
||||||
|
version: 2,
|
||||||
|
apps,
|
||||||
|
mcp: Default::default(),
|
||||||
|
prompts: Default::default(),
|
||||||
|
skills: Default::default(),
|
||||||
|
common_config_snippets: Default::default(),
|
||||||
|
claude_common_config_snippet: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Dry-run should succeed without any file I/O errors
|
||||||
|
let result = Database::migrate_from_json_dry_run(&config);
|
||||||
|
assert!(
|
||||||
|
result.is_ok(),
|
||||||
|
"Dry-run should succeed with valid config: {result:?}"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify dry-run can detect schema errors early
|
||||||
|
// (This would fail if migrate_from_json_tx had incompatible SQL)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dry_run_validates_schema_compatibility() {
|
||||||
|
use crate::app_config::MultiAppConfig;
|
||||||
|
use crate::provider::{Provider, ProviderManager};
|
||||||
|
use indexmap::IndexMap;
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
// Create config with actual provider data
|
||||||
|
let mut providers = IndexMap::new();
|
||||||
|
providers.insert(
|
||||||
|
"test-provider".to_string(),
|
||||||
|
Provider {
|
||||||
|
id: "test-provider".to_string(),
|
||||||
|
name: "Test Provider".to_string(),
|
||||||
|
settings_config: json!({
|
||||||
|
"anthropicApiKey": "sk-test-123",
|
||||||
|
}),
|
||||||
|
website_url: None,
|
||||||
|
category: None,
|
||||||
|
created_at: Some(1234567890),
|
||||||
|
sort_index: None,
|
||||||
|
notes: None,
|
||||||
|
meta: None,
|
||||||
|
icon: None,
|
||||||
|
icon_color: None,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut manager = ProviderManager::default();
|
||||||
|
manager.providers = providers;
|
||||||
|
manager.current = "test-provider".to_string();
|
||||||
|
|
||||||
|
let mut apps = HashMap::new();
|
||||||
|
apps.insert("claude".to_string(), manager);
|
||||||
|
|
||||||
|
let config = MultiAppConfig {
|
||||||
|
version: 2,
|
||||||
|
apps,
|
||||||
|
mcp: Default::default(),
|
||||||
|
prompts: Default::default(),
|
||||||
|
skills: Default::default(),
|
||||||
|
common_config_snippets: Default::default(),
|
||||||
|
claude_common_config_snippet: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Dry-run should validate the full migration path
|
||||||
|
let result = Database::migrate_from_json_dry_run(&config);
|
||||||
|
assert!(
|
||||||
|
result.is_ok(),
|
||||||
|
"Dry-run should succeed with provider data: {result:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+54
-26
@@ -113,14 +113,22 @@ const TRAY_SECTIONS: [TrayAppSection; 3] = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
/// 读取类似布尔的环境变量(1/true/yes/on 视为开启)
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
fn env_flag_enabled(key: &str) -> bool {
|
enum JsonMigrationMode {
|
||||||
match std::env::var(key) {
|
Disabled,
|
||||||
Ok(val) => matches!(
|
DryRun,
|
||||||
val.trim().to_ascii_lowercase().as_str(),
|
Enabled,
|
||||||
"1" | "true" | "yes" | "on"
|
}
|
||||||
),
|
|
||||||
Err(_) => false,
|
/// 解析 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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -553,9 +561,10 @@ pub fn run() {
|
|||||||
let db_path = app_config_dir.join("cc-switch.db");
|
let db_path = app_config_dir.join("cc-switch.db");
|
||||||
let json_path = app_config_dir.join("config.json");
|
let json_path = app_config_dir.join("config.json");
|
||||||
|
|
||||||
// 检查是否需要从 config.json 迁移到 SQLite,功能尚未正式发布,默认关闭
|
// Check if config.json→SQLite migration needed (feature gated, disabled by default)
|
||||||
let migration_opt_in = env_flag_enabled("CC_SWITCH_ENABLE_JSON_DB_MIGRATION");
|
let migration_mode = json_migration_mode();
|
||||||
let migration_needed = !db_path.exists() && json_path.exists() && migration_opt_in;
|
let has_json = json_path.exists();
|
||||||
|
let has_db = db_path.exists();
|
||||||
|
|
||||||
let db = match crate::database::Database::init() {
|
let db = match crate::database::Database::init() {
|
||||||
Ok(db) => Arc::new(db),
|
Ok(db) => Arc::new(db),
|
||||||
@@ -567,23 +576,42 @@ pub fn run() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if !migration_opt_in && !db_path.exists() && json_path.exists() {
|
if !has_db && has_json {
|
||||||
log::warn!(
|
match migration_mode {
|
||||||
"检测到 config.json,但 JSON→数据库迁移功能尚未正式发布,默认跳过。设置 CC_SWITCH_ENABLE_JSON_DB_MIGRATION=1 后再尝试。"
|
JsonMigrationMode::Disabled => {
|
||||||
);
|
log::warn!(
|
||||||
} else if migration_needed {
|
"Detected config.json but migration is disabled by default. \
|
||||||
log::info!("Starting migration from config.json to SQLite (opt-in)...");
|
Set CC_SWITCH_ENABLE_JSON_DB_MIGRATION=1 to migrate, or =dryrun to validate first."
|
||||||
match crate::app_config::MultiAppConfig::load() {
|
);
|
||||||
Ok(config) => {
|
}
|
||||||
if let Err(e) = db.migrate_from_json(&config) {
|
JsonMigrationMode::DryRun => {
|
||||||
log::error!("Migration failed: {e}");
|
log::info!("Running migration dry-run (validation only, no disk writes)");
|
||||||
} else {
|
match crate::app_config::MultiAppConfig::load() {
|
||||||
log::info!("Migration successful");
|
Ok(config) => {
|
||||||
// Optional: Rename config.json
|
if let Err(e) = crate::database::Database::migrate_from_json_dry_run(&config) {
|
||||||
// let _ = std::fs::rename(&json_path, json_path.with_extension("json.bak"));
|
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}"),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user