Skip to main content

sqlite_graphrag/commands/
vacuum.rs

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