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