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    /// Explicit JSON flag. Accepted as a no-op because output is already JSON by default.
13    #[arg(long, default_value_t = false)]
14    pub json: bool,
15    /// Output format: `json` or `text`. JSON is always emitted on stdout regardless of the value.
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    // v1.0.21 P1-C: query usa tabela `memory_chunks` (correta).
87    // Se a tabela não existir (DB legado pré-chunking), o erro é "no such table"
88    // e o fallback retorna 0. Outros erros são logados via tracing para auditoria.
89    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}