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:
Jason
2026-03-08 17:18:21 +08:00
parent 3f711d6504
commit bf40b0138c
16 changed files with 910 additions and 97 deletions
+101 -1
View File
@@ -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);