Skip to main content

sqlite_graphrag/commands/
vacuum.rs

1//! Handler for the `vacuum` CLI subcommand.
2
3use crate::errors::AppError;
4use crate::i18n::errors_msg;
5use crate::output;
6use crate::output::JsonOutputFormat;
7use crate::paths::AppPaths;
8use crate::storage::connection::open_rw;
9use serde::Serialize;
10
11#[derive(clap::Args)]
12pub struct VacuumArgs {
13    #[arg(long, help = "No-op; JSON is always emitted on stdout")]
14    pub json: bool,
15    /// Run a WAL checkpoint before and after `VACUUM`.
16    #[arg(long, default_value_t = true)]
17    pub checkpoint: bool,
18    /// Output format.
19    #[arg(long, value_enum, default_value_t = JsonOutputFormat::Json)]
20    pub format: JsonOutputFormat,
21    #[arg(long, env = "SQLITE_GRAPHRAG_DB_PATH")]
22    pub db: Option<String>,
23}
24
25#[derive(Serialize)]
26struct VacuumResponse {
27    db_path: String,
28    size_before_bytes: u64,
29    size_after_bytes: u64,
30    status: String,
31    /// Total execution time in milliseconds from handler start to serialisation.
32    elapsed_ms: u64,
33}
34
35pub fn run(args: VacuumArgs) -> Result<(), AppError> {
36    let inicio = std::time::Instant::now();
37    let _ = args.format;
38    let paths = AppPaths::resolve(args.db.as_deref())?;
39
40    if !paths.db.exists() {
41        return Err(AppError::NotFound(errors_msg::database_not_found(
42            &paths.db.display().to_string(),
43        )));
44    }
45
46    let size_before_bytes = std::fs::metadata(&paths.db)
47        .map(|meta| meta.len())
48        .unwrap_or(0);
49    let conn = open_rw(&paths.db)?;
50    if args.checkpoint {
51        conn.execute_batch("PRAGMA wal_checkpoint(TRUNCATE);")?;
52    }
53    conn.execute_batch("VACUUM;")?;
54    if args.checkpoint {
55        conn.execute_batch("PRAGMA wal_checkpoint(TRUNCATE);")?;
56    }
57    drop(conn);
58    let size_after_bytes = std::fs::metadata(&paths.db)
59        .map(|meta| meta.len())
60        .unwrap_or(0);
61
62    output::emit_json(&VacuumResponse {
63        db_path: paths.db.display().to_string(),
64        size_before_bytes,
65        size_after_bytes,
66        status: "ok".to_string(),
67        elapsed_ms: inicio.elapsed().as_millis() as u64,
68    })?;
69
70    Ok(())
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76
77    #[test]
78    fn vacuum_response_serializa_todos_campos() {
79        let resp = VacuumResponse {
80            db_path: "/home/user/.local/share/sqlite-graphrag/db.sqlite".to_string(),
81            size_before_bytes: 32768,
82            size_after_bytes: 16384,
83            status: "ok".to_string(),
84            elapsed_ms: 55,
85        };
86        let json = serde_json::to_value(&resp).expect("serialização falhou");
87        assert_eq!(
88            json["db_path"],
89            "/home/user/.local/share/sqlite-graphrag/db.sqlite"
90        );
91        assert_eq!(json["size_before_bytes"], 32768u64);
92        assert_eq!(json["size_after_bytes"], 16384u64);
93        assert_eq!(json["status"], "ok");
94        assert_eq!(json["elapsed_ms"], 55u64);
95    }
96
97    #[test]
98    fn vacuum_response_size_after_menor_ou_igual_before() {
99        let resp = VacuumResponse {
100            db_path: "/data/db.sqlite".to_string(),
101            size_before_bytes: 65536,
102            size_after_bytes: 32768,
103            status: "ok".to_string(),
104            elapsed_ms: 100,
105        };
106        let json = serde_json::to_value(&resp).expect("serialização falhou");
107        let before = json["size_before_bytes"].as_u64().unwrap();
108        let after = json["size_after_bytes"].as_u64().unwrap();
109        assert!(
110            after <= before,
111            "size_after_bytes deve ser <= size_before_bytes após VACUUM"
112        );
113    }
114
115    #[test]
116    fn vacuum_response_status_ok() {
117        let resp = VacuumResponse {
118            db_path: "/data/db.sqlite".to_string(),
119            size_before_bytes: 0,
120            size_after_bytes: 0,
121            status: "ok".to_string(),
122            elapsed_ms: 0,
123        };
124        let json = serde_json::to_value(&resp).expect("serialização falhou");
125        assert_eq!(json["status"], "ok");
126    }
127
128    #[test]
129    fn vacuum_response_elapsed_ms_presente_e_nao_negativo() {
130        let resp = VacuumResponse {
131            db_path: "/data/db.sqlite".to_string(),
132            size_before_bytes: 1024,
133            size_after_bytes: 1024,
134            status: "ok".to_string(),
135            elapsed_ms: 0,
136        };
137        let json = serde_json::to_value(&resp).expect("serialização falhou");
138        assert!(
139            json.get("elapsed_ms").is_some(),
140            "campo elapsed_ms deve estar presente"
141        );
142        assert!(
143            json["elapsed_ms"].as_u64().is_some(),
144            "elapsed_ms deve ser inteiro não negativo"
145        );
146    }
147}