mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-08-02 01:31:53 +08:00
feat: add usage daily rollups, incremental auto-vacuum, and sync-aware backup
- Add usage_daily_rollups table (schema v6) to aggregate proxy request logs into daily summaries, reducing query overhead for statistics - Add rollup_and_prune DAO that aggregates old detail logs (>N days) into rollup rows and deletes the originals - Update all usage stats queries to UNION detail logs with rollup data - Introduce incremental auto-vacuum for SQLite, with startup and periodic cleanup of old stream_check_logs and request log rollups - Split backup export/import into full vs sync variants: WebDAV sync now skips local-only table data (proxy_request_logs, stream_check_logs, provider_health, proxy_live_backup, usage_daily_rollups) while preserving them on import - Add enable_logging guard to skip request log writes when disabled - Apply cargo fmt formatting fixes across multiple modules
This commit is contained in:
@@ -44,7 +44,7 @@ use std::sync::Mutex;
|
||||
|
||||
/// 当前 Schema 版本号
|
||||
/// 每次修改表结构时递增,并在 schema.rs 中添加相应的迁移逻辑
|
||||
pub(crate) const SCHEMA_VERSION: i32 = 5;
|
||||
pub(crate) const SCHEMA_VERSION: i32 = 6;
|
||||
|
||||
/// 安全地序列化 JSON,避免 unwrap panic
|
||||
pub(crate) fn to_json_string<T: Serialize>(value: &T) -> Result<String, AppError> {
|
||||
@@ -89,6 +89,7 @@ impl Database {
|
||||
/// 数据库文件位于 `~/.cc-switch/cc-switch.db`
|
||||
pub fn init() -> Result<Self, AppError> {
|
||||
let db_path = get_app_config_dir().join("cc-switch.db");
|
||||
let db_exists = db_path.exists();
|
||||
|
||||
// 确保父目录存在
|
||||
if let Some(parent) = db_path.parent() {
|
||||
@@ -100,6 +101,12 @@ impl Database {
|
||||
// 启用外键约束
|
||||
conn.execute("PRAGMA foreign_keys = ON;", [])
|
||||
.map_err(|e| AppError::Database(e.to_string()))?;
|
||||
if !db_exists {
|
||||
// For a brand-new database, configure incremental auto-vacuum
|
||||
// before creating any tables so no rebuild is needed later.
|
||||
conn.execute("PRAGMA auto_vacuum = INCREMENTAL;", [])
|
||||
.map_err(|e| AppError::Database(e.to_string()))?;
|
||||
}
|
||||
register_db_change_hook(&conn);
|
||||
|
||||
let db = Self {
|
||||
@@ -123,8 +130,26 @@ impl Database {
|
||||
}
|
||||
|
||||
db.apply_schema_migrations()?;
|
||||
if let Err(e) = db.ensure_incremental_auto_vacuum() {
|
||||
log::warn!("Failed to ensure incremental auto-vacuum: {e}");
|
||||
}
|
||||
db.ensure_model_pricing_seeded()?;
|
||||
|
||||
// Startup cleanup: prune old logs and reclaim space
|
||||
if let Err(e) = db.cleanup_old_stream_check_logs(7) {
|
||||
log::warn!("Startup stream_check_logs cleanup failed: {e}");
|
||||
}
|
||||
if let Err(e) = db.rollup_and_prune(30) {
|
||||
log::warn!("Startup rollup_and_prune failed: {e}");
|
||||
}
|
||||
// Reclaim disk space after cleanup
|
||||
{
|
||||
let conn = lock_conn!(db.conn);
|
||||
if let Err(e) = conn.execute_batch("PRAGMA incremental_vacuum;") {
|
||||
log::warn!("Startup incremental vacuum failed: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(db)
|
||||
}
|
||||
|
||||
@@ -135,6 +160,8 @@ impl Database {
|
||||
// 启用外键约束
|
||||
conn.execute("PRAGMA foreign_keys = ON;", [])
|
||||
.map_err(|e| AppError::Database(e.to_string()))?;
|
||||
conn.execute("PRAGMA auto_vacuum = INCREMENTAL;", [])
|
||||
.map_err(|e| AppError::Database(e.to_string()))?;
|
||||
register_db_change_hook(&conn);
|
||||
|
||||
let db = Self {
|
||||
@@ -146,6 +173,79 @@ impl Database {
|
||||
Ok(db)
|
||||
}
|
||||
|
||||
pub(crate) fn get_auto_vacuum_mode(conn: &Connection) -> Result<i32, AppError> {
|
||||
conn.query_row("PRAGMA auto_vacuum;", [], |row| row.get(0))
|
||||
.map_err(|e| AppError::Database(format!("读取 auto_vacuum 失败: {e}")))
|
||||
}
|
||||
|
||||
fn has_user_tables(conn: &Connection) -> Result<bool, AppError> {
|
||||
let count: i64 = conn
|
||||
.query_row(
|
||||
"SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'",
|
||||
[],
|
||||
|row| row.get(0),
|
||||
)
|
||||
.map_err(|e| AppError::Database(format!("读取表数量失败: {e}")))?;
|
||||
Ok(count > 0)
|
||||
}
|
||||
|
||||
pub(crate) fn ensure_incremental_auto_vacuum_on_conn(
|
||||
conn: &Connection,
|
||||
) -> Result<bool, AppError> {
|
||||
let mode = Self::get_auto_vacuum_mode(conn)?;
|
||||
if mode == 2 {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let has_tables = Self::has_user_tables(conn)?;
|
||||
conn.execute("PRAGMA auto_vacuum = INCREMENTAL;", [])
|
||||
.map_err(|e| AppError::Database(format!("设置 auto_vacuum 失败: {e}")))?;
|
||||
|
||||
if !has_tables {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
conn.execute("VACUUM;", [])
|
||||
.map_err(|e| AppError::Database(format!("执行 VACUUM 失败: {e}")))?;
|
||||
conn.execute("PRAGMA foreign_keys = ON;", [])
|
||||
.map_err(|e| AppError::Database(format!("恢复 foreign_keys 失败: {e}")))?;
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub(crate) fn ensure_incremental_auto_vacuum(&self) -> Result<bool, AppError> {
|
||||
let mode = {
|
||||
let conn = lock_conn!(self.conn);
|
||||
Self::get_auto_vacuum_mode(&conn)?
|
||||
};
|
||||
if mode == 2 {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let has_tables = {
|
||||
let conn = lock_conn!(self.conn);
|
||||
Self::has_user_tables(&conn)?
|
||||
};
|
||||
if has_tables {
|
||||
log::info!(
|
||||
"Detected auto_vacuum={mode}, rebuilding database to enable incremental vacuum"
|
||||
);
|
||||
self.backup_database_file()?;
|
||||
}
|
||||
|
||||
let rebuilt = {
|
||||
let conn = lock_conn!(self.conn);
|
||||
Self::ensure_incremental_auto_vacuum_on_conn(&conn)?
|
||||
};
|
||||
|
||||
if rebuilt {
|
||||
log::info!("Incremental auto-vacuum enabled after database rebuild");
|
||||
} else {
|
||||
log::info!("Incremental auto-vacuum configured for new database");
|
||||
}
|
||||
|
||||
Ok(rebuilt)
|
||||
}
|
||||
|
||||
/// 检查 MCP 服务器表是否为空
|
||||
pub fn is_mcp_table_empty(&self) -> Result<bool, AppError> {
|
||||
let conn = lock_conn!(self.conn);
|
||||
|
||||
Reference in New Issue
Block a user