Skip to main content

cc_switch_lib/database/
mod.rs

1//! 数据库模块 - SQLite 数据持久化
2//!
3//! 此模块提供应用的核心数据存储功能,包括:
4//! - 供应商配置管理
5//! - MCP 服务器配置
6//! - 提示词管理
7//! - Skills 管理
8//! - 通用设置存储
9//!
10//! ## 架构设计
11//!
12//! ```text
13//! database/
14//! ├── mod.rs        - Database 结构体 + 初始化
15//! ├── schema.rs     - 表结构定义 + Schema 迁移
16//! ├── backup.rs     - SQL 导入导出 + 快照备份
17//! ├── migration.rs  - JSON → SQLite 数据迁移
18//! └── dao/          - 数据访问对象
19//!     ├── providers.rs
20//!     ├── mcp.rs
21//!     ├── prompts.rs
22//!     ├── skills.rs
23//!     └── settings.rs
24//! ```
25
26mod backup;
27mod dao;
28mod migration;
29mod schema;
30
31#[cfg(test)]
32mod tests;
33
34// DAO 类型导出供外部使用
35pub use dao::FailoverQueueItem;
36
37use crate::config::get_app_config_dir;
38use crate::error::AppError;
39use rusqlite::Connection;
40use serde::Serialize;
41use std::sync::atomic::{AtomicU64, Ordering};
42use std::sync::Mutex;
43
44// DAO 方法通过 impl Database 提供,无需额外导出
45
46/// 数据库备份保留数量
47const DB_BACKUP_RETAIN: usize = 10;
48
49/// 当前 Schema 版本号
50/// 每次修改表结构时递增,并在 schema.rs 中添加相应的迁移逻辑
51pub(crate) const SCHEMA_VERSION: i32 = 12;
52
53/// 安全地序列化 JSON,避免 unwrap panic
54pub(crate) fn to_json_string<T: Serialize>(value: &T) -> Result<String, AppError> {
55    serde_json::to_string(value)
56        .map_err(|e| AppError::Config(format!("JSON serialization failed: {e}")))
57}
58
59/// 安全地获取 Mutex 锁,避免 unwrap panic
60macro_rules! lock_conn {
61    ($mutex:expr) => {
62        $mutex
63            .lock()
64            .map_err(|e| AppError::Database(format!("Mutex lock failed: {}", e)))?
65    };
66}
67
68// 导出宏供子模块使用
69pub(crate) use lock_conn;
70
71/// 数据库连接封装
72///
73/// 使用 Mutex 包装 Connection 以支持在多线程环境(如 Tauri State)中共享。
74/// rusqlite::Connection 本身不是 Sync 的,因此需要这层包装。
75pub struct Database {
76    pub(crate) conn: Mutex<Connection>,
77    runtime_key: String,
78}
79
80impl Database {
81    /// 初始化数据库连接并创建表
82    ///
83    /// 数据库文件位于 `~/.cc-switch-tui/cc-switch.db`
84    pub fn init() -> Result<Self, AppError> {
85        let db_path = get_app_config_dir().join("cc-switch.db");
86
87        // 确保父目录存在
88        if let Some(parent) = db_path.parent() {
89            std::fs::create_dir_all(parent).map_err(|e| AppError::io(parent, e))?;
90        }
91
92        let conn = Connection::open(&db_path).map_err(|e| AppError::Database(e.to_string()))?;
93
94        // 启用外键约束
95        conn.execute("PRAGMA foreign_keys = ON;", [])
96            .map_err(|e| AppError::Database(e.to_string()))?;
97
98        let db = Self {
99            conn: Mutex::new(conn),
100            runtime_key: format!("file:{}", db_path.display()),
101        };
102
103        {
104            let conn = lock_conn!(db.conn);
105            let version = Self::get_user_version(&conn)?;
106            drop(conn);
107
108            if version > SCHEMA_VERSION {
109                return Err(Self::future_schema_error(version));
110            }
111
112            if version > 0 && version < SCHEMA_VERSION {
113                log::info!(
114                    "Creating pre-migration database backup (v{version} -> v{SCHEMA_VERSION})"
115                );
116                if let Err(err) = db.backup_database_file() {
117                    log::warn!("Pre-migration backup failed, continuing migration: {err}");
118                }
119            }
120        }
121
122        db.create_tables()?;
123        db.apply_schema_migrations()?;
124        db.ensure_model_pricing_seeded()?;
125
126        Ok(db)
127    }
128
129    /// 创建内存数据库(用于测试)
130    pub fn memory() -> Result<Self, AppError> {
131        static NEXT_MEMORY_DB_ID: AtomicU64 = AtomicU64::new(1);
132
133        let conn = Connection::open_in_memory().map_err(|e| AppError::Database(e.to_string()))?;
134
135        // 启用外键约束
136        conn.execute("PRAGMA foreign_keys = ON;", [])
137            .map_err(|e| AppError::Database(e.to_string()))?;
138
139        let db = Self {
140            conn: Mutex::new(conn),
141            runtime_key: format!(
142                "memory:{}",
143                NEXT_MEMORY_DB_ID.fetch_add(1, Ordering::Relaxed)
144            ),
145        };
146        db.create_tables()?;
147        db.ensure_model_pricing_seeded()?;
148
149        Ok(db)
150    }
151
152    /// 检查 MCP 服务器表是否为空
153    pub fn is_mcp_table_empty(&self) -> Result<bool, AppError> {
154        let conn = lock_conn!(self.conn);
155        let count: i64 = conn
156            .query_row("SELECT COUNT(*) FROM mcp_servers", [], |row| row.get(0))
157            .map_err(|e| AppError::Database(e.to_string()))?;
158        Ok(count == 0)
159    }
160
161    /// 检查提示词表是否为空
162    pub fn is_prompts_table_empty(&self) -> Result<bool, AppError> {
163        let conn = lock_conn!(self.conn);
164        let count: i64 = conn
165            .query_row("SELECT COUNT(*) FROM prompts", [], |row| row.get(0))
166            .map_err(|e| AppError::Database(e.to_string()))?;
167        Ok(count == 0)
168    }
169
170    pub(crate) fn runtime_key(&self) -> &str {
171        &self.runtime_key
172    }
173}