cc_switch_lib/database/
mod.rs1mod backup;
27mod dao;
28mod migration;
29mod schema;
30
31#[cfg(test)]
32mod tests;
33
34pub 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
44const DB_BACKUP_RETAIN: usize = 10;
48
49pub(crate) const SCHEMA_VERSION: i32 = 10;
52
53pub(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
59macro_rules! lock_conn {
61 ($mutex:expr) => {
62 $mutex
63 .lock()
64 .map_err(|e| AppError::Database(format!("Mutex lock failed: {}", e)))?
65 };
66}
67
68pub(crate) use lock_conn;
70
71pub struct Database {
76 pub(crate) conn: Mutex<Connection>,
77 runtime_key: String,
78}
79
80impl Database {
81 pub fn init() -> Result<Self, AppError> {
85 let db_path = get_app_config_dir().join("cc-switch.db");
86
87 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 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 db.create_tables()?;
103
104 {
105 let conn = lock_conn!(db.conn);
106 let version = Self::get_user_version(&conn)?;
107 drop(conn);
108
109 if version > 0 && version < SCHEMA_VERSION {
110 log::info!(
111 "Creating pre-migration database backup (v{version} -> v{SCHEMA_VERSION})"
112 );
113 if let Err(err) = db.backup_database_file() {
114 log::warn!("Pre-migration backup failed, continuing migration: {err}");
115 }
116 }
117 }
118
119 db.apply_schema_migrations()?;
120 db.ensure_model_pricing_seeded()?;
121
122 Ok(db)
123 }
124
125 pub fn memory() -> Result<Self, AppError> {
127 static NEXT_MEMORY_DB_ID: AtomicU64 = AtomicU64::new(1);
128
129 let conn = Connection::open_in_memory().map_err(|e| AppError::Database(e.to_string()))?;
130
131 conn.execute("PRAGMA foreign_keys = ON;", [])
133 .map_err(|e| AppError::Database(e.to_string()))?;
134
135 let db = Self {
136 conn: Mutex::new(conn),
137 runtime_key: format!(
138 "memory:{}",
139 NEXT_MEMORY_DB_ID.fetch_add(1, Ordering::Relaxed)
140 ),
141 };
142 db.create_tables()?;
143 db.ensure_model_pricing_seeded()?;
144
145 Ok(db)
146 }
147
148 pub fn is_mcp_table_empty(&self) -> Result<bool, AppError> {
150 let conn = lock_conn!(self.conn);
151 let count: i64 = conn
152 .query_row("SELECT COUNT(*) FROM mcp_servers", [], |row| row.get(0))
153 .map_err(|e| AppError::Database(e.to_string()))?;
154 Ok(count == 0)
155 }
156
157 pub fn is_prompts_table_empty(&self) -> Result<bool, AppError> {
159 let conn = lock_conn!(self.conn);
160 let count: i64 = conn
161 .query_row("SELECT COUNT(*) FROM prompts", [], |row| row.get(0))
162 .map_err(|e| AppError::Database(e.to_string()))?;
163 Ok(count == 0)
164 }
165
166 pub(crate) fn runtime_key(&self) -> &str {
167 &self.runtime_key
168 }
169}