sqlite_graphrag/commands/
vacuum.rs1use 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 #[arg(long, default_value_t = true)]
17 pub checkpoint: bool,
18 #[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 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}