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