sqlite_graphrag/commands/
read.rs1use crate::errors::AppError;
2use crate::i18n::erros;
3use crate::output;
4use crate::paths::AppPaths;
5use crate::storage::connection::open_ro;
6use crate::storage::memories;
7use serde::Serialize;
8
9#[derive(clap::Args)]
10pub struct ReadArgs {
11 #[arg(long)]
13 pub name: String,
14 #[arg(long, default_value = "global")]
15 pub namespace: Option<String>,
16 #[arg(long, help = "No-op; JSON is always emitted on stdout")]
17 pub json: bool,
18 #[arg(long, env = "SQLITE_GRAPHRAG_DB_PATH")]
19 pub db: Option<String>,
20}
21
22#[derive(Serialize)]
23struct ReadResponse {
24 id: i64,
26 memory_id: i64,
28 namespace: String,
29 name: String,
30 #[serde(rename = "type")]
32 type_alias: String,
33 memory_type: String,
34 description: String,
35 body: String,
36 body_hash: String,
37 session_id: Option<String>,
38 source: String,
39 metadata: String,
40 version: i64,
42 created_at: i64,
43 created_at_iso: String,
45 updated_at: i64,
46 updated_at_iso: String,
48 elapsed_ms: u64,
50}
51
52fn epoch_to_iso(epoch: i64) -> String {
53 crate::tz::epoch_para_iso(epoch)
54}
55
56pub fn run(args: ReadArgs) -> Result<(), AppError> {
57 let inicio = std::time::Instant::now();
58 let namespace = crate::namespace::resolve_namespace(args.namespace.as_deref())?;
59 let paths = AppPaths::resolve(args.db.as_deref())?;
60 if !paths.db.exists() {
61 return Err(AppError::NotFound(
62 crate::i18n::erros::banco_nao_encontrado(&paths.db.display().to_string()),
63 ));
64 }
65 let conn = open_ro(&paths.db)?;
66
67 match memories::read_by_name(&conn, &namespace, &args.name)? {
68 Some(row) => {
69 let version: i64 = conn
71 .query_row(
72 "SELECT COALESCE(MAX(version), 1) FROM memory_versions WHERE memory_id=?1",
73 rusqlite::params![row.id],
74 |r| r.get(0),
75 )
76 .unwrap_or(1);
77
78 let response = ReadResponse {
79 id: row.id,
80 memory_id: row.id,
81 namespace: row.namespace,
82 name: row.name,
83 type_alias: row.memory_type.clone(),
84 memory_type: row.memory_type,
85 description: row.description,
86 body: row.body,
87 body_hash: row.body_hash,
88 session_id: row.session_id,
89 source: row.source,
90 metadata: row.metadata,
91 version,
92 created_at: row.created_at,
93 created_at_iso: epoch_to_iso(row.created_at),
94 updated_at: row.updated_at,
95 updated_at_iso: epoch_to_iso(row.updated_at),
96 elapsed_ms: inicio.elapsed().as_millis() as u64,
97 };
98 output::emit_json(&response)?;
99 }
100 None => {
101 return Err(AppError::NotFound(erros::memoria_nao_encontrada(
102 &args.name, &namespace,
103 )))
104 }
105 }
106
107 Ok(())
108}
109
110#[cfg(test)]
111mod testes {
112 use super::*;
113
114 #[test]
115 fn epoch_to_iso_converte_zero_para_epoch_unix() {
116 let resultado = epoch_to_iso(0);
117 assert!(
118 resultado.starts_with("1970-01-01T00:00:00"),
119 "epoch 0 deve mapear para 1970-01-01T00:00:00, obtido: {resultado}"
120 );
121 }
122
123 #[test]
124 fn epoch_to_iso_converte_timestamp_conhecido() {
125 let resultado = epoch_to_iso(1_705_320_000);
126 assert!(
127 resultado.starts_with("2024-01-15"),
128 "timestamp 1705320000 deve mapear para 2024-01-15, obtido: {resultado}"
129 );
130 }
131
132 #[test]
133 fn epoch_to_iso_retorna_fallback_para_epoch_negativo_invalido() {
134 let resultado = epoch_to_iso(i64::MIN);
135 assert!(
136 !resultado.is_empty(),
137 "deve retornar string não vazia mesmo para epoch inválido"
138 );
139 }
140
141 #[test]
142 fn read_response_serializa_aliases_id_e_memory_id() {
143 let resp = ReadResponse {
144 id: 42,
145 memory_id: 42,
146 namespace: "global".to_string(),
147 name: "minha-mem".to_string(),
148 type_alias: "fact".to_string(),
149 memory_type: "fact".to_string(),
150 description: "desc".to_string(),
151 body: "corpo".to_string(),
152 body_hash: "abc123".to_string(),
153 session_id: None,
154 source: "agent".to_string(),
155 metadata: "{}".to_string(),
156 version: 1,
157 created_at: 1_705_320_000,
158 created_at_iso: "2024-01-15T12:00:00Z".to_string(),
159 updated_at: 1_705_320_000,
160 updated_at_iso: "2024-01-15T12:00:00Z".to_string(),
161 elapsed_ms: 5,
162 };
163
164 let json = serde_json::to_value(&resp).expect("serialização falhou");
165 assert_eq!(json["id"], 42);
166 assert_eq!(json["memory_id"], 42);
167 assert_eq!(json["type"], "fact");
168 assert_eq!(json["memory_type"], "fact");
169 assert_eq!(json["elapsed_ms"], 5u64);
170 assert!(
171 json["session_id"].is_null(),
172 "session_id None deve serializar como null"
173 );
174 }
175
176 #[test]
177 fn read_response_session_id_some_serializa_string() {
178 let resp = ReadResponse {
179 id: 1,
180 memory_id: 1,
181 namespace: "global".to_string(),
182 name: "mem".to_string(),
183 type_alias: "skill".to_string(),
184 memory_type: "skill".to_string(),
185 description: "d".to_string(),
186 body: "b".to_string(),
187 body_hash: "h".to_string(),
188 session_id: Some("sess-123".to_string()),
189 source: "agent".to_string(),
190 metadata: "{}".to_string(),
191 version: 2,
192 created_at: 0,
193 created_at_iso: "1970-01-01T00:00:00Z".to_string(),
194 updated_at: 0,
195 updated_at_iso: "1970-01-01T00:00:00Z".to_string(),
196 elapsed_ms: 0,
197 };
198
199 let json = serde_json::to_value(&resp).expect("serialização falhou");
200 assert_eq!(json["session_id"], "sess-123");
201 }
202
203 #[test]
204 fn read_response_elapsed_ms_esta_presente() {
205 let resp = ReadResponse {
206 id: 7,
207 memory_id: 7,
208 namespace: "ns".to_string(),
209 name: "n".to_string(),
210 type_alias: "procedure".to_string(),
211 memory_type: "procedure".to_string(),
212 description: "d".to_string(),
213 body: "b".to_string(),
214 body_hash: "h".to_string(),
215 session_id: None,
216 source: "agent".to_string(),
217 metadata: "{}".to_string(),
218 version: 3,
219 created_at: 1000,
220 created_at_iso: "1970-01-01T00:16:40Z".to_string(),
221 updated_at: 2000,
222 updated_at_iso: "1970-01-01T00:33:20Z".to_string(),
223 elapsed_ms: 123,
224 };
225
226 let json = serde_json::to_value(&resp).expect("serialização falhou");
227 assert_eq!(json["elapsed_ms"], 123u64);
228 assert!(json["created_at_iso"].is_string());
229 assert!(json["updated_at_iso"].is_string());
230 }
231}