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 = 12;
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
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 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 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 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 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}