sqlite_graphrag/commands/
list.rs1use crate::cli::MemoryType;
2use crate::errors::AppError;
3use crate::output::{self, OutputFormat};
4use crate::paths::AppPaths;
5use crate::storage::connection::open_ro;
6use crate::storage::memories;
7use serde::Serialize;
8
9#[derive(clap::Args)]
10pub struct ListArgs {
11 #[arg(long, default_value = "global")]
12 pub namespace: Option<String>,
13 #[arg(long, value_enum)]
17 pub r#type: Option<MemoryType>,
18 #[arg(long, default_value = "50")]
19 pub limit: usize,
20 #[arg(long, default_value = "0")]
21 pub offset: usize,
22 #[arg(long, value_enum, default_value = "json")]
23 pub format: OutputFormat,
24 #[arg(long, help = "No-op; JSON is always emitted on stdout")]
25 pub json: bool,
26 #[arg(long, env = "SQLITE_GRAPHRAG_DB_PATH")]
27 pub db: Option<String>,
28}
29
30#[derive(Serialize)]
31struct ListItem {
32 id: i64,
33 memory_id: i64,
35 name: String,
36 namespace: String,
37 #[serde(rename = "type")]
38 memory_type: String,
39 description: String,
40 snippet: String,
41 updated_at: i64,
42 updated_at_iso: String,
44}
45
46#[derive(Serialize)]
47struct ListResponse {
48 items: Vec<ListItem>,
49 elapsed_ms: u64,
51}
52
53pub fn run(args: ListArgs) -> Result<(), AppError> {
54 let inicio = std::time::Instant::now();
55 let namespace = crate::namespace::resolve_namespace(args.namespace.as_deref())?;
56 let paths = AppPaths::resolve(args.db.as_deref())?;
57 if !paths.db.exists() {
59 return Err(AppError::NotFound(
60 crate::i18n::erros::banco_nao_encontrado(&paths.db.display().to_string()),
61 ));
62 }
63 let conn = open_ro(&paths.db)?;
64
65 let memory_type_str = args.r#type.map(|t| t.as_str());
66 let rows = memories::list(&conn, &namespace, memory_type_str, args.limit, args.offset)?;
67
68 let items: Vec<ListItem> = rows
69 .into_iter()
70 .map(|r| {
71 let snippet: String = r.body.chars().take(200).collect();
72 let updated_at_iso = crate::tz::epoch_para_iso(r.updated_at);
73 ListItem {
74 id: r.id,
75 memory_id: r.id,
76 name: r.name,
77 namespace: r.namespace,
78 memory_type: r.memory_type,
79 description: r.description,
80 snippet,
81 updated_at: r.updated_at,
82 updated_at_iso,
83 }
84 })
85 .collect();
86
87 match args.format {
88 OutputFormat::Json => output::emit_json(&ListResponse {
89 items,
90 elapsed_ms: inicio.elapsed().as_millis() as u64,
91 })?,
92 OutputFormat::Text | OutputFormat::Markdown => {
93 for item in &items {
94 output::emit_text(&format!("{}: {}", item.name, item.snippet));
95 }
96 }
97 }
98 Ok(())
99}
100
101#[cfg(test)]
102mod testes {
103 use super::*;
104
105 #[test]
106 fn list_response_serializa_items_e_elapsed_ms() {
107 let resp = ListResponse {
108 items: vec![ListItem {
109 id: 1,
110 memory_id: 1,
111 name: "teste-memoria".to_string(),
112 namespace: "global".to_string(),
113 memory_type: "note".to_string(),
114 description: "descricao de teste".to_string(),
115 snippet: "corpo resumido".to_string(),
116 updated_at: 1_745_000_000,
117 updated_at_iso: "2025-04-19T00:00:00Z".to_string(),
118 }],
119 elapsed_ms: 7,
120 };
121 let json = serde_json::to_value(&resp).unwrap();
122 assert!(json["items"].is_array());
123 assert_eq!(json["items"].as_array().unwrap().len(), 1);
124 assert_eq!(json["items"][0]["name"], "teste-memoria");
125 assert_eq!(json["items"][0]["memory_id"], 1);
126 assert_eq!(json["elapsed_ms"], 7);
127 }
128
129 #[test]
130 fn list_response_items_vazio_serializa_array_vazio() {
131 let resp = ListResponse {
132 items: vec![],
133 elapsed_ms: 0,
134 };
135 let json = serde_json::to_value(&resp).unwrap();
136 assert!(json["items"].is_array());
137 assert_eq!(json["items"].as_array().unwrap().len(), 0);
138 assert_eq!(json["elapsed_ms"], 0);
139 }
140
141 #[test]
142 fn list_item_memory_id_igual_a_id() {
143 let item = ListItem {
144 id: 42,
145 memory_id: 42,
146 name: "memoria-alias".to_string(),
147 namespace: "projeto".to_string(),
148 memory_type: "fact".to_string(),
149 description: "desc".to_string(),
150 snippet: "snip".to_string(),
151 updated_at: 0,
152 updated_at_iso: "1970-01-01T00:00:00Z".to_string(),
153 };
154 let json = serde_json::to_value(&item).unwrap();
155 assert_eq!(
156 json["id"], json["memory_id"],
157 "id e memory_id devem ser iguais"
158 );
159 }
160
161 #[test]
162 fn snippet_truncado_em_200_chars() {
163 let body_longo: String = "a".repeat(300);
164 let snippet: String = body_longo.chars().take(200).collect();
165 assert_eq!(snippet.len(), 200, "snippet deve ter exatamente 200 chars");
166 }
167
168 #[test]
169 fn updated_at_iso_epoch_zero_gera_utc_valido() {
170 let iso = crate::tz::epoch_para_iso(0);
171 assert!(
172 iso.starts_with("1970-01-01T00:00:00"),
173 "epoch 0 deve mapear para 1970-01-01, obtido: {iso}"
174 );
175 assert!(
176 iso.contains('+') || iso.contains('-'),
177 "deve conter sinal de offset, obtido: {iso}"
178 );
179 }
180}