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