本文档详细描述项目中 SQLite 的使用方式,可作为其他项目的参考模板。
# Cargo.toml
[dependencies]
rusqlite = { version = "0.31", features = ["bundled", "backup"] }bundled: 内置 SQLite,无需系统安装backup: 支持 Backup API(用于快照/导入导出)
use rusqlite::Connection;
use std::sync::Mutex;
pub struct Database {
conn: Mutex<Connection>, // Mutex 包装实现线程安全
}
impl Database {
/// 文件数据库(生产环境)
pub fn init() -> Result<Self, AppError> {
let db_path = get_app_config_dir().join("cc-switch.db");
// 确保目录存在
if let Some(parent) = db_path.parent() {
std::fs::create_dir_all(parent)?;
}
let conn = Connection::open(&db_path)?;
conn.execute("PRAGMA foreign_keys = ON;", [])?; // 启用外键
let db = Self { conn: Mutex::new(conn) };
db.create_tables()?;
db.apply_schema_migrations()?;
Ok(db)
}
/// 内存数据库(测试用)
pub fn memory() -> Result<Self, AppError> {
let conn = Connection::open_in_memory()?;
conn.execute("PRAGMA foreign_keys = ON;", [])?;
let db = Self { conn: Mutex::new(conn) };
db.create_tables()?;
Ok(db)
}
}/// 安全获取 Mutex 锁
macro_rules! lock_conn {
($mutex:expr) => {
$mutex
.lock()
.map_err(|e| AppError::Database(format!("Mutex lock failed: {}", e)))?
};
}
// 使用示例
pub fn query_count(&self) -> Result<i64, AppError> {
let conn = lock_conn!(self.conn);
let count: i64 = conn.query_row("SELECT COUNT(*) FROM table", [], |row| row.get(0))?;
Ok(count)
}pub(crate) const SCHEMA_VERSION: i32 = 1;
impl Database {
fn get_user_version(conn: &Connection) -> Result<i32, AppError> {
conn.query_row("PRAGMA user_version;", [], |row| row.get(0))
}
fn set_user_version(conn: &Connection, version: i32) -> Result<(), AppError> {
conn.execute(&format!("PRAGMA user_version = {version};"), [])
}
/// 迁移入口
fn apply_schema_migrations(&self) -> Result<(), AppError> {
let conn = lock_conn!(self.conn);
conn.execute("SAVEPOINT schema_migration;", [])?;
let mut version = Self::get_user_version(&conn)?;
// 版本过新则拒绝
if version > SCHEMA_VERSION {
conn.execute("ROLLBACK TO schema_migration;", []).ok();
return Err(AppError::Database("数据库版本过新".into()));
}
// 逐版本迁移
while version < SCHEMA_VERSION {
match version {
0 => {
Self::migrate_v0_to_v1(&conn)?;
Self::set_user_version(&conn, 1)?;
}
_ => return Err(AppError::Database("未知版本".into())),
}
version = Self::get_user_version(&conn)?;
}
conn.execute("RELEASE schema_migration;", [])?;
Ok(())
}
}fn add_column_if_missing(
conn: &Connection,
table: &str,
column: &str,
definition: &str,
) -> Result<bool, AppError> {
if Self::has_column(conn, table, column)? {
return Ok(false);
}
let sql = format!("ALTER TABLE \"{table}\" ADD COLUMN \"{column}\" {definition};");
conn.execute(&sql, [])?;
Ok(true)
}
fn has_column(conn: &Connection, table: &str, column: &str) -> Result<bool, AppError> {
let sql = format!("PRAGMA table_info(\"{table}\");");
let mut stmt = conn.prepare(&sql)?;
let mut rows = stmt.query([])?;
while let Some(row) = rows.next()? {
let name: String = row.get(1)?;
if name.eq_ignore_ascii_case(column) {
return Ok(true);
}
}
Ok(false)
}use rusqlite::params;
impl Database {
/// 查询列表
pub fn get_all(&self, app_type: &str) -> Result<Vec<Item>, AppError> {
let conn = lock_conn!(self.conn);
let mut stmt = conn.prepare(
"SELECT id, name, config FROM items WHERE app_type = ?1 ORDER BY id"
)?;
let iter = stmt.query_map(params![app_type], |row| {
Ok(Item {
id: row.get(0)?,
name: row.get(1)?,
config: serde_json::from_str(&row.get::<_, String>(2)?).unwrap_or_default(),
})
})?;
iter.collect::<Result<Vec<_>, _>>()
.map_err(|e| AppError::Database(e.to_string()))
}
/// 事务写入
pub fn save(&self, item: &Item) -> Result<(), AppError> {
let mut conn = lock_conn!(self.conn);
let tx = conn.transaction()?;
tx.execute(
"INSERT OR REPLACE INTO items (id, name, config) VALUES (?1, ?2, ?3)",
params![item.id, item.name, serde_json::to_string(&item.config)?],
)?;
tx.commit()?;
Ok(())
}
/// 删除
pub fn delete(&self, id: &str) -> Result<(), AppError> {
let conn = lock_conn!(self.conn);
conn.execute("DELETE FROM items WHERE id = ?1", params![id])?;
Ok(())
}
}use rusqlite::backup::Backup;
impl Database {
/// 创建内存快照
fn snapshot_to_memory(&self) -> Result<Connection, AppError> {
let conn = lock_conn!(self.conn);
let mut snapshot = Connection::open_in_memory()?;
let backup = Backup::new(&conn, &mut snapshot)?;
backup.step(-1)?; // -1 表示一次性复制全部
Ok(snapshot)
}
/// 从快照恢复
fn restore_from(&self, source: &Connection) -> Result<(), AppError> {
let mut main_conn = lock_conn!(self.conn);
let backup = Backup::new(source, &mut main_conn)?;
backup.step(-1)?;
Ok(())
}
}-- 1. providers (AI 提供商)
CREATE TABLE providers (
id TEXT NOT NULL,
app_type TEXT NOT NULL, -- 'claude' | 'codex' | 'gemini'
name TEXT NOT NULL,
settings_config TEXT NOT NULL, -- JSON: API Key 等配置
website_url TEXT,
category TEXT,
created_at INTEGER, -- Unix timestamp (ms)
sort_index INTEGER,
notes TEXT,
icon TEXT,
icon_color TEXT,
meta TEXT NOT NULL DEFAULT '{}', -- JSON: 扩展元数据
is_current BOOLEAN NOT NULL DEFAULT 0,
PRIMARY KEY (id, app_type)
);
-- 2. provider_endpoints (提供商端点)
CREATE TABLE provider_endpoints (
id INTEGER PRIMARY KEY AUTOINCREMENT,
provider_id TEXT NOT NULL,
app_type TEXT NOT NULL,
url TEXT NOT NULL,
added_at INTEGER,
FOREIGN KEY (provider_id, app_type)
REFERENCES providers(id, app_type) ON DELETE CASCADE
);
-- 3. mcp_servers (MCP 服务器)
CREATE TABLE mcp_servers (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
server_config TEXT NOT NULL, -- JSON: 服务器配置
description TEXT,
homepage TEXT,
docs TEXT,
tags TEXT NOT NULL DEFAULT '[]', -- JSON Array
enabled_claude BOOLEAN NOT NULL DEFAULT 0,
enabled_codex BOOLEAN NOT NULL DEFAULT 0,
enabled_gemini BOOLEAN NOT NULL DEFAULT 0
);
-- 4. prompts (提示词)
CREATE TABLE prompts (
id TEXT NOT NULL,
app_type TEXT NOT NULL,
name TEXT NOT NULL,
content TEXT NOT NULL,
description TEXT,
enabled BOOLEAN NOT NULL DEFAULT 1,
created_at INTEGER,
updated_at INTEGER,
PRIMARY KEY (id, app_type)
);
-- 5. skills (技能)
CREATE TABLE skills (
key TEXT PRIMARY KEY,
installed BOOLEAN NOT NULL DEFAULT 0,
installed_at INTEGER NOT NULL DEFAULT 0
);
-- 6. skill_repos (技能仓库)
CREATE TABLE skill_repos (
owner TEXT NOT NULL,
name TEXT NOT NULL,
branch TEXT NOT NULL DEFAULT 'main',
enabled BOOLEAN NOT NULL DEFAULT 1,
PRIMARY KEY (owner, name)
);
-- 7. settings (通用设置)
CREATE TABLE settings (
key TEXT PRIMARY KEY,
value TEXT -- JSON 或纯文本
);数据库位置: ~/.cc-switch/cc-switch.db
# 命令行访问
sqlite3 ~/.cc-switch/cc-switch.db
# 常用命令
.tables # 列出所有表
.schema providers # 查看表结构
.mode column # 列模式显示
.headers on # 显示列头
# 查询示例
SELECT id, name FROM providers WHERE app_type = 'claude';
SELECT * FROM mcp_servers WHERE enabled_claude = 1;
SELECT key, value FROM settings;
# 导出
.output backup.sql
.dump
.output stdout| 要点 | 说明 |
|---|---|
| 线程安全 | Mutex<Connection> 包装,配合 lock_conn! 宏 |
| 外键约束 | PRAGMA foreign_keys = ON 启用级联删除 |
| 版本管理 | PRAGMA user_version 追踪 Schema 版本 |
| 迁移策略 | SAVEPOINT 事务 + 逐版本迁移 + 回滚保护 |
| JSON 存储 | 复杂配置用 TEXT 存 JSON,读取时 serde_json::from_str |
| 复合主键 | (id, app_type) 支持多应用类型隔离 |
| 备份机制 | rusqlite::backup::Backup API 实现原子快照 |
src-tauri/src/database/
├── mod.rs - Database 结构体 + 初始化
├── schema.rs - 表结构定义 + Schema 迁移
├── backup.rs - SQL 导入导出 + 快照备份
├── migration.rs - JSON → SQLite 数据迁移
└── dao/ - 数据访问对象
├── providers.rs
├── mcp.rs
├── prompts.rs
├── skills.rs
└── settings.rs
ccstart 作为 cc-switch 的 CLI 客户端,采用 只读访问 + Read-Through Cache 模式集成 SQLite。
┌──────────────────┐
│ SQLite DB │
│ (cc-switch.db) │
└────────┬─────────┘
│
[只读查询 providers]
│
v
┌──────────────────┐
│ 内容哈希比较 │
│ (SHA256) │
└────────┬─────────┘
│
┌──────────────┴──────────────┐
│ │
[哈希相同] [哈希不同]
│ │
v v
┌──────────────────┐ ┌──────────────────┐
│ 使用缓存文件 │ │ 重新生成缓存 │
└──────────────────┘ └──────────────────┘
│ │
└──────────────┬──────────────┘
│
v
┌──────────────────┐
│ claude --settings│
│ <path> │
└──────────────────┘
src/
├── db/
│ ├── mod.rs - Database 只读连接
│ └── provider.rs - Provider DAO (查询)
├── config/
│ └── cache.rs - 缓存管理 (哈希比较 + 原子写入)
└── commands/
├── run.rs - Read-Through Cache 执行
├── list.rs - 直接查询 SQLite
└── update.rs - 强制刷新所有缓存
use rusqlite::{Connection, OpenFlags};
impl Database {
pub fn connect(&self) -> AppResult<Connection> {
// 只读模式打开,不会修改上游数据库
Connection::open_with_flags(&self.path, OpenFlags::SQLITE_OPEN_READ_ONLY)
}
}ccstart 使用 SHA256 哈希比较决定是否重新生成缓存:
pub fn ensure_cached(&self, provider: &Provider) -> AppResult<PathBuf> {
let path = self.get_cache_path(&provider.name);
let content = serde_json::to_vec_pretty(&provider.settings_config)?;
let new_hash = sha256(&content);
let should_write = if path.exists() {
let existing = fs::read(&path)?;
sha256(&existing) != new_hash
} else {
true
};
if should_write {
self.write_atomic(&path, &content)?;
}
Ok(path)
}| 命令 | 数据源 | 说明 |
|---|---|---|
ccstart list |
SQLite | 直接查询 providers 表 |
ccstart <name> |
SQLite + 缓存 | 查询 → 哈希比较 → 按需写缓存 → 执行 |
ccstart update |
SQLite | 强制刷新所有缓存文件 |
ccstart completions |
SQLite | 动态查询补全候选 |
| 决策 | 选择 | 原因 |
|---|---|---|
| 访问模式 | 只读 | 不修改上游数据,安全 |
| 缓存策略 | 内容哈希 | 最小化 I/O,只在变化时写入 |
| 缓存路径 | ~/.cc-switch/separated/ |
与旧版兼容 |
init 命令 |
已移除 | SQLite 模式无需初始化 |
update 命令 |
保留 | 用于强制刷新/调试 |