sqlite_graphrag/commands/
stats.rs1use crate::errors::AppError;
2use crate::i18n::erros;
3use crate::output;
4use crate::paths::AppPaths;
5use crate::storage::connection::open_ro;
6use serde::Serialize;
7
8#[derive(clap::Args)]
9pub struct StatsArgs {
10 #[arg(long, env = "SQLITE_GRAPHRAG_DB_PATH")]
11 pub db: Option<String>,
12 #[arg(long, default_value_t = false)]
14 pub json: bool,
15 #[arg(long, value_parser = ["json", "text"], hide = true)]
17 pub format: Option<String>,
18}
19
20#[derive(Serialize)]
21struct StatsResponse {
22 memories: i64,
23 memories_total: i64,
25 entities: i64,
26 entities_total: i64,
28 relationships: i64,
29 relationships_total: i64,
31 edges: i64,
33 chunks_total: i64,
35 avg_body_len: f64,
37 namespaces: Vec<String>,
38 db_size_bytes: u64,
39 db_bytes: u64,
41 schema_version: String,
42 elapsed_ms: u64,
44}
45
46pub fn run(args: StatsArgs) -> Result<(), AppError> {
47 let inicio = std::time::Instant::now();
48 let _ = args.json; let _ = args.format; let paths = AppPaths::resolve(args.db.as_deref())?;
51
52 if !paths.db.exists() {
53 return Err(AppError::NotFound(erros::banco_nao_encontrado(
54 &paths.db.display().to_string(),
55 )));
56 }
57
58 let conn = open_ro(&paths.db)?;
59
60 let memories: i64 = conn.query_row(
61 "SELECT COUNT(*) FROM memories WHERE deleted_at IS NULL",
62 [],
63 |r| r.get(0),
64 )?;
65 let entities: i64 = conn.query_row("SELECT COUNT(*) FROM entities", [], |r| r.get(0))?;
66 let relationships: i64 =
67 conn.query_row("SELECT COUNT(*) FROM relationships", [], |r| r.get(0))?;
68
69 let mut stmt = conn.prepare(
70 "SELECT DISTINCT namespace FROM memories WHERE deleted_at IS NULL ORDER BY namespace",
71 )?;
72 let namespaces: Vec<String> = stmt
73 .query_map([], |r| r.get(0))?
74 .collect::<Result<Vec<_>, _>>()?;
75
76 let schema_version: String = conn
77 .query_row(
78 "SELECT value FROM schema_meta WHERE key='schema_version'",
79 [],
80 |r| r.get(0),
81 )
82 .unwrap_or_else(|_| "unknown".to_string());
83
84 let db_size_bytes = std::fs::metadata(&paths.db).map(|m| m.len()).unwrap_or(0);
85
86 let chunks_total: i64 = match conn.query_row("SELECT COUNT(*) FROM memory_chunks", [], |r| {
90 r.get::<_, i64>(0)
91 }) {
92 Ok(n) => n,
93 Err(rusqlite::Error::SqliteFailure(_, Some(msg))) if msg.contains("no such table") => 0,
94 Err(e) => {
95 tracing::warn!("falha ao contar memory_chunks: {e}");
96 0
97 }
98 };
99
100 let avg_body_len: f64 = conn
101 .query_row(
102 "SELECT COALESCE(AVG(LENGTH(body)), 0.0) FROM memories WHERE deleted_at IS NULL",
103 [],
104 |r| r.get(0),
105 )
106 .unwrap_or(0.0);
107
108 output::emit_json(&StatsResponse {
109 memories,
110 memories_total: memories,
111 entities,
112 entities_total: entities,
113 relationships,
114 relationships_total: relationships,
115 edges: relationships,
116 chunks_total,
117 avg_body_len,
118 namespaces,
119 db_size_bytes,
120 db_bytes: db_size_bytes,
121 schema_version,
122 elapsed_ms: inicio.elapsed().as_millis() as u64,
123 })?;
124
125 Ok(())
126}
127
128#[cfg(test)]
129mod testes {
130 use super::*;
131
132 #[test]
133 fn stats_response_serializa_todos_campos() {
134 let resp = StatsResponse {
135 memories: 10,
136 memories_total: 10,
137 entities: 5,
138 entities_total: 5,
139 relationships: 3,
140 relationships_total: 3,
141 edges: 3,
142 chunks_total: 20,
143 avg_body_len: 42.5,
144 namespaces: vec!["global".to_string(), "projeto".to_string()],
145 db_size_bytes: 8192,
146 db_bytes: 8192,
147 schema_version: "6".to_string(),
148 elapsed_ms: 7,
149 };
150 let json = serde_json::to_value(&resp).expect("serialização falhou");
151 assert_eq!(json["memories"], 10);
152 assert_eq!(json["memories_total"], 10);
153 assert_eq!(json["entities"], 5);
154 assert_eq!(json["entities_total"], 5);
155 assert_eq!(json["relationships"], 3);
156 assert_eq!(json["relationships_total"], 3);
157 assert_eq!(json["edges"], 3);
158 assert_eq!(json["chunks_total"], 20);
159 assert_eq!(json["db_size_bytes"], 8192u64);
160 assert_eq!(json["db_bytes"], 8192u64);
161 assert_eq!(json["schema_version"], "6");
162 assert_eq!(json["elapsed_ms"], 7u64);
163 }
164
165 #[test]
166 fn stats_response_namespaces_eh_array_de_strings() {
167 let resp = StatsResponse {
168 memories: 0,
169 memories_total: 0,
170 entities: 0,
171 entities_total: 0,
172 relationships: 0,
173 relationships_total: 0,
174 edges: 0,
175 chunks_total: 0,
176 avg_body_len: 0.0,
177 namespaces: vec!["ns1".to_string(), "ns2".to_string(), "ns3".to_string()],
178 db_size_bytes: 0,
179 db_bytes: 0,
180 schema_version: "unknown".to_string(),
181 elapsed_ms: 0,
182 };
183 let json = serde_json::to_value(&resp).expect("serialização falhou");
184 let arr = json["namespaces"]
185 .as_array()
186 .expect("namespaces deve ser array");
187 assert_eq!(arr.len(), 3);
188 assert_eq!(arr[0], "ns1");
189 assert_eq!(arr[1], "ns2");
190 assert_eq!(arr[2], "ns3");
191 }
192
193 #[test]
194 fn stats_response_namespaces_vazio_serializa_array_vazio() {
195 let resp = StatsResponse {
196 memories: 0,
197 memories_total: 0,
198 entities: 0,
199 entities_total: 0,
200 relationships: 0,
201 relationships_total: 0,
202 edges: 0,
203 chunks_total: 0,
204 avg_body_len: 0.0,
205 namespaces: vec![],
206 db_size_bytes: 0,
207 db_bytes: 0,
208 schema_version: "unknown".to_string(),
209 elapsed_ms: 0,
210 };
211 let json = serde_json::to_value(&resp).expect("serialização falhou");
212 let arr = json["namespaces"]
213 .as_array()
214 .expect("namespaces deve ser array");
215 assert!(arr.is_empty(), "namespaces vazio deve serializar como []");
216 }
217
218 #[test]
219 fn stats_response_aliases_memories_total_e_memories_iguais() {
220 let resp = StatsResponse {
221 memories: 42,
222 memories_total: 42,
223 entities: 7,
224 entities_total: 7,
225 relationships: 2,
226 relationships_total: 2,
227 edges: 2,
228 chunks_total: 0,
229 avg_body_len: 0.0,
230 namespaces: vec![],
231 db_size_bytes: 0,
232 db_bytes: 0,
233 schema_version: "6".to_string(),
234 elapsed_ms: 0,
235 };
236 let json = serde_json::to_value(&resp).expect("serialização falhou");
237 assert_eq!(json["memories"], json["memories_total"]);
238 assert_eq!(json["entities"], json["entities_total"]);
239 assert_eq!(json["relationships"], json["relationships_total"]);
240 assert_eq!(json["relationships"], json["edges"]);
241 assert_eq!(json["db_size_bytes"], json["db_bytes"]);
242 }
243}