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