sqlite_graphrag/commands/
stats.rs1use crate::errors::AppError;
4use crate::output;
5use crate::paths::AppPaths;
6use crate::storage::connection::open_ro;
7use serde::Serialize;
8
9#[derive(clap::Args)]
10#[command(after_long_help = "EXAMPLES:\n \
11 # Show database statistics (memory counts, sizes, namespace breakdown)\n \
12 sqlite-graphrag stats\n\n \
13 # Stats for a database at a custom path\n \
14 sqlite-graphrag stats --db /path/to/graphrag.sqlite\n\n \
15 # Explicit database path\n \
16 sqlite-graphrag stats --db /data/graphrag.sqlite")]
17pub struct StatsArgs {
19 #[arg(long)]
21 pub db: Option<String>,
22 #[arg(long, default_value_t = false)]
24 pub json: bool,
25 #[arg(long, value_parser = ["json", "text"], hide = true)]
27 pub format: Option<String>,
28}
29
30#[derive(Serialize)]
31struct StatsResponse {
32 memories: i64,
33 memories_total: i64,
35 total_memories: i64,
39 entities: i64,
40 entities_total: i64,
42 relationships: i64,
43 relationships_total: i64,
45 edges: i64,
47 chunks_total: i64,
49 avg_body_len: f64,
51 namespaces: Vec<String>,
52 db_size_bytes: u64,
53 db_bytes: u64,
55 schema_version: u32,
59 elapsed_ms: u64,
61}
62
63pub fn run(args: StatsArgs) -> Result<(), AppError> {
65 let start = std::time::Instant::now();
66 let _ = args.json; let _ = args.format; let paths = AppPaths::resolve(args.db.as_deref())?;
69
70 crate::storage::connection::ensure_db_ready(&paths)?;
71
72 let conn = open_ro(&paths.db)?;
73
74 let memories: i64 = conn.query_row(
75 "SELECT COUNT(*) FROM memories WHERE deleted_at IS NULL",
76 [],
77 |r| r.get(0),
78 )?;
79 let entities: i64 = conn.query_row("SELECT COUNT(*) FROM entities", [], |r| r.get(0))?;
80 let relationships: i64 =
81 conn.query_row("SELECT COUNT(*) FROM relationships", [], |r| r.get(0))?;
82
83 let mut stmt = conn.prepare_cached(
84 "SELECT DISTINCT namespace FROM memories WHERE deleted_at IS NULL ORDER BY namespace",
85 )?;
86 let namespaces: Vec<String> = stmt
87 .query_map([], |r| r.get(0))?
88 .collect::<Result<Vec<_>, _>>()?;
89
90 let schema_version: u32 = conn
91 .query_row(
92 "SELECT MAX(version) FROM refinery_schema_history",
93 [],
94 |row| row.get::<_, Option<i64>>(0),
95 )
96 .ok()
97 .flatten()
98 .map(|v| v.max(0) as u32)
99 .unwrap_or(0);
100
101 let db_size_bytes = std::fs::metadata(&paths.db).map(|m| m.len()).unwrap_or(0);
102
103 let chunks_total: i64 = match conn.query_row("SELECT COUNT(*) FROM memory_chunks", [], |r| {
107 r.get::<_, i64>(0)
108 }) {
109 Ok(n) => n,
110 Err(rusqlite::Error::SqliteFailure(_, Some(msg))) if msg.contains("no such table") => 0,
111 Err(e) => {
112 tracing::warn!(target: "stats", error = %e, "memory_chunks count failed");
113 0
114 }
115 };
116
117 let avg_body_len: f64 = conn
118 .query_row(
119 "SELECT COALESCE(AVG(LENGTH(body)), 0.0) FROM memories WHERE deleted_at IS NULL",
120 [],
121 |r| r.get(0),
122 )
123 .unwrap_or(0.0);
124
125 output::emit_json(&StatsResponse {
126 memories,
127 memories_total: memories,
128 total_memories: memories,
129 entities,
130 entities_total: entities,
131 relationships,
132 relationships_total: relationships,
133 edges: relationships,
134 chunks_total,
135 avg_body_len,
136 namespaces,
137 db_size_bytes,
138 db_bytes: db_size_bytes,
139 schema_version,
140 elapsed_ms: start.elapsed().as_millis() as u64,
141 })?;
142
143 Ok(())
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149
150 #[test]
151 fn stats_response_serializes_all_fields() {
152 let resp = StatsResponse {
153 memories: 10,
154 memories_total: 10,
155 total_memories: 10,
156 entities: 5,
157 entities_total: 5,
158 relationships: 3,
159 relationships_total: 3,
160 edges: 3,
161 chunks_total: 20,
162 avg_body_len: 42.5,
163 namespaces: vec!["global".to_string(), "project".to_string()],
164 db_size_bytes: 8192,
165 db_bytes: 8192,
166 schema_version: 6,
167 elapsed_ms: 7,
168 };
169 let json = serde_json::to_value(&resp).expect("serialization failed");
170 assert_eq!(json["memories"], 10);
171 assert_eq!(json["memories_total"], 10);
172 assert_eq!(json["total_memories"], 10);
174 assert!(
175 json["total_memories"].is_number(),
176 "total_memories must be a number, not null"
177 );
178 assert_eq!(json["entities"], 5);
179 assert_eq!(json["entities_total"], 5);
180 assert_eq!(json["relationships"], 3);
181 assert_eq!(json["relationships_total"], 3);
182 assert_eq!(json["edges"], 3);
183 assert_eq!(json["chunks_total"], 20);
184 assert_eq!(json["db_size_bytes"], 8192u64);
185 assert_eq!(json["db_bytes"], 8192u64);
186 assert_eq!(json["schema_version"], 6);
187 assert_eq!(json["elapsed_ms"], 7u64);
188 }
189
190 #[test]
191 fn stats_response_namespaces_is_string_array() {
192 let resp = StatsResponse {
193 memories: 0,
194 memories_total: 0,
195 total_memories: 0,
196 entities: 0,
197 entities_total: 0,
198 relationships: 0,
199 relationships_total: 0,
200 edges: 0,
201 chunks_total: 0,
202 avg_body_len: 0.0,
203 namespaces: vec!["ns1".to_string(), "ns2".to_string(), "ns3".to_string()],
204 db_size_bytes: 0,
205 db_bytes: 0,
206 schema_version: 0,
207 elapsed_ms: 0,
208 };
209 let json = serde_json::to_value(&resp).expect("serialization failed");
210 let arr = json["namespaces"]
211 .as_array()
212 .expect("namespaces must be array");
213 assert_eq!(arr.len(), 3);
214 assert_eq!(arr[0], "ns1");
215 assert_eq!(arr[1], "ns2");
216 assert_eq!(arr[2], "ns3");
217 }
218
219 #[test]
220 fn stats_response_namespaces_empty_serializes_empty_array() {
221 let resp = StatsResponse {
222 memories: 0,
223 memories_total: 0,
224 total_memories: 0,
225 entities: 0,
226 entities_total: 0,
227 relationships: 0,
228 relationships_total: 0,
229 edges: 0,
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: 0,
236 elapsed_ms: 0,
237 };
238 let json = serde_json::to_value(&resp).expect("serialization failed");
239 let arr = json["namespaces"]
240 .as_array()
241 .expect("namespaces must be array");
242 assert!(arr.is_empty(), "empty namespaces must serialize as []");
243 }
244
245 #[test]
246 fn stats_response_aliases_memories_total_and_memories_equal() {
247 let resp = StatsResponse {
248 memories: 42,
249 memories_total: 42,
250 total_memories: 42,
251 entities: 7,
252 entities_total: 7,
253 relationships: 2,
254 relationships_total: 2,
255 edges: 2,
256 chunks_total: 0,
257 avg_body_len: 0.0,
258 namespaces: vec![],
259 db_size_bytes: 0,
260 db_bytes: 0,
261 schema_version: 6,
262 elapsed_ms: 0,
263 };
264 let json = serde_json::to_value(&resp).expect("serialization failed");
265 assert_eq!(json["memories"], json["memories_total"]);
266 assert_eq!(json["entities"], json["entities_total"]);
267 assert_eq!(json["relationships"], json["relationships_total"]);
268 assert_eq!(json["relationships"], json["edges"]);
269 assert_eq!(json["db_size_bytes"], json["db_bytes"]);
270 }
271}