Skip to main content

sqlite_graphrag/commands/
stats.rs

1use 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    /// Flag explícita de saída JSON. Aceita como no-op pois o output já é JSON por default.
13    #[arg(long, default_value_t = false)]
14    pub json: bool,
15    /// Formato de saída: "json" ou "text". JSON é sempre emitido no stdout independente do valor.
16    #[arg(long, value_parser = ["json", "text"], hide = true)]
17    pub format: Option<String>,
18}
19
20#[derive(Serialize)]
21struct StatsResponse {
22    memories: i64,
23    /// Alias de `memories` para contrato documentado em SKILL.md e AGENT_PROTOCOL.md.
24    memories_total: i64,
25    entities: i64,
26    /// Alias de `entities` para contrato documentado.
27    entities_total: i64,
28    relationships: i64,
29    /// Alias de `relationships` para contrato documentado.
30    relationships_total: i64,
31    /// Alias semântico de `relationships` conforme contrato em AGENT_PROTOCOL.md.
32    edges: i64,
33    /// Total de chunks indexados (linha por chunk em `memory_chunks`).
34    chunks_total: i64,
35    /// Comprimento médio do campo body nas memórias ativas (não deletadas).
36    avg_body_len: f64,
37    namespaces: Vec<String>,
38    db_size_bytes: u64,
39    /// Alias semântico de `db_size_bytes` para contrato documentado.
40    db_bytes: u64,
41    schema_version: String,
42    /// Tempo total de execução em milissegundos desde início do handler até serialização.
43    elapsed_ms: u64,
44}
45
46pub fn run(args: StatsArgs) -> Result<(), AppError> {
47    let inicio = std::time::Instant::now();
48    let _ = args.json; // --json é no-op pois output já é JSON por default
49    let _ = args.format; // --format é no-op; JSON sempre emitido no stdout
50    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 = conn
87        .query_row("SELECT COUNT(*) FROM memory_chunks", [], |r| r.get(0))
88        .unwrap_or(0);
89
90    let avg_body_len: f64 = conn
91        .query_row(
92            "SELECT COALESCE(AVG(LENGTH(body)), 0.0) FROM memories WHERE deleted_at IS NULL",
93            [],
94            |r| r.get(0),
95        )
96        .unwrap_or(0.0);
97
98    output::emit_json(&StatsResponse {
99        memories,
100        memories_total: memories,
101        entities,
102        entities_total: entities,
103        relationships,
104        relationships_total: relationships,
105        edges: relationships,
106        chunks_total,
107        avg_body_len,
108        namespaces,
109        db_size_bytes,
110        db_bytes: db_size_bytes,
111        schema_version,
112        elapsed_ms: inicio.elapsed().as_millis() as u64,
113    })?;
114
115    Ok(())
116}
117
118#[cfg(test)]
119mod testes {
120    use super::*;
121
122    #[test]
123    fn stats_response_serializa_todos_campos() {
124        let resp = StatsResponse {
125            memories: 10,
126            memories_total: 10,
127            entities: 5,
128            entities_total: 5,
129            relationships: 3,
130            relationships_total: 3,
131            edges: 3,
132            chunks_total: 20,
133            avg_body_len: 42.5,
134            namespaces: vec!["global".to_string(), "projeto".to_string()],
135            db_size_bytes: 8192,
136            db_bytes: 8192,
137            schema_version: "5".to_string(),
138            elapsed_ms: 7,
139        };
140        let json = serde_json::to_value(&resp).expect("serialização falhou");
141        assert_eq!(json["memories"], 10);
142        assert_eq!(json["memories_total"], 10);
143        assert_eq!(json["entities"], 5);
144        assert_eq!(json["entities_total"], 5);
145        assert_eq!(json["relationships"], 3);
146        assert_eq!(json["relationships_total"], 3);
147        assert_eq!(json["edges"], 3);
148        assert_eq!(json["chunks_total"], 20);
149        assert_eq!(json["db_size_bytes"], 8192u64);
150        assert_eq!(json["db_bytes"], 8192u64);
151        assert_eq!(json["schema_version"], "5");
152        assert_eq!(json["elapsed_ms"], 7u64);
153    }
154
155    #[test]
156    fn stats_response_namespaces_eh_array_de_strings() {
157        let resp = StatsResponse {
158            memories: 0,
159            memories_total: 0,
160            entities: 0,
161            entities_total: 0,
162            relationships: 0,
163            relationships_total: 0,
164            edges: 0,
165            chunks_total: 0,
166            avg_body_len: 0.0,
167            namespaces: vec!["ns1".to_string(), "ns2".to_string(), "ns3".to_string()],
168            db_size_bytes: 0,
169            db_bytes: 0,
170            schema_version: "unknown".to_string(),
171            elapsed_ms: 0,
172        };
173        let json = serde_json::to_value(&resp).expect("serialização falhou");
174        let arr = json["namespaces"]
175            .as_array()
176            .expect("namespaces deve ser array");
177        assert_eq!(arr.len(), 3);
178        assert_eq!(arr[0], "ns1");
179        assert_eq!(arr[1], "ns2");
180        assert_eq!(arr[2], "ns3");
181    }
182
183    #[test]
184    fn stats_response_namespaces_vazio_serializa_array_vazio() {
185        let resp = StatsResponse {
186            memories: 0,
187            memories_total: 0,
188            entities: 0,
189            entities_total: 0,
190            relationships: 0,
191            relationships_total: 0,
192            edges: 0,
193            chunks_total: 0,
194            avg_body_len: 0.0,
195            namespaces: vec![],
196            db_size_bytes: 0,
197            db_bytes: 0,
198            schema_version: "unknown".to_string(),
199            elapsed_ms: 0,
200        };
201        let json = serde_json::to_value(&resp).expect("serialização falhou");
202        let arr = json["namespaces"]
203            .as_array()
204            .expect("namespaces deve ser array");
205        assert!(arr.is_empty(), "namespaces vazio deve serializar como []");
206    }
207
208    #[test]
209    fn stats_response_aliases_memories_total_e_memories_iguais() {
210        let resp = StatsResponse {
211            memories: 42,
212            memories_total: 42,
213            entities: 7,
214            entities_total: 7,
215            relationships: 2,
216            relationships_total: 2,
217            edges: 2,
218            chunks_total: 0,
219            avg_body_len: 0.0,
220            namespaces: vec![],
221            db_size_bytes: 0,
222            db_bytes: 0,
223            schema_version: "5".to_string(),
224            elapsed_ms: 0,
225        };
226        let json = serde_json::to_value(&resp).expect("serialização falhou");
227        assert_eq!(json["memories"], json["memories_total"]);
228        assert_eq!(json["entities"], json["entities_total"]);
229        assert_eq!(json["relationships"], json["relationships_total"]);
230        assert_eq!(json["relationships"], json["edges"]);
231        assert_eq!(json["db_size_bytes"], json["db_bytes"]);
232    }
233}