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