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, hide = true, 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 let conn = open_ro(&paths.db)?;
55
56 let memory_type_str = args.r#type.map(|t| t.as_str());
57 let rows = memories::list(&conn, &namespace, memory_type_str, args.limit, args.offset)?;
58
59 let items: Vec<ListItem> = rows
60 .into_iter()
61 .map(|r| {
62 let snippet: String = r.body.chars().take(200).collect();
63 let updated_at_iso = crate::tz::epoch_para_iso(r.updated_at);
64 ListItem {
65 id: r.id,
66 memory_id: r.id,
67 name: r.name,
68 namespace: r.namespace,
69 memory_type: r.memory_type,
70 description: r.description,
71 snippet,
72 updated_at: r.updated_at,
73 updated_at_iso,
74 }
75 })
76 .collect();
77
78 match args.format {
79 OutputFormat::Json => output::emit_json(&ListResponse {
80 items,
81 elapsed_ms: inicio.elapsed().as_millis() as u64,
82 })?,
83 OutputFormat::Text | OutputFormat::Markdown => {
84 for item in &items {
85 output::emit_text(&format!("{}: {}", item.name, item.snippet));
86 }
87 }
88 }
89 Ok(())
90}
91
92#[cfg(test)]
93mod testes {
94 use super::*;
95
96 #[test]
97 fn list_response_serializa_items_e_elapsed_ms() {
98 let resp = ListResponse {
99 items: vec![ListItem {
100 id: 1,
101 memory_id: 1,
102 name: "teste-memoria".to_string(),
103 namespace: "global".to_string(),
104 memory_type: "note".to_string(),
105 description: "descricao de teste".to_string(),
106 snippet: "corpo resumido".to_string(),
107 updated_at: 1_745_000_000,
108 updated_at_iso: "2025-04-19T00:00:00Z".to_string(),
109 }],
110 elapsed_ms: 7,
111 };
112 let json = serde_json::to_value(&resp).unwrap();
113 assert!(json["items"].is_array());
114 assert_eq!(json["items"].as_array().unwrap().len(), 1);
115 assert_eq!(json["items"][0]["name"], "teste-memoria");
116 assert_eq!(json["items"][0]["memory_id"], 1);
117 assert_eq!(json["elapsed_ms"], 7);
118 }
119
120 #[test]
121 fn list_response_items_vazio_serializa_array_vazio() {
122 let resp = ListResponse {
123 items: vec![],
124 elapsed_ms: 0,
125 };
126 let json = serde_json::to_value(&resp).unwrap();
127 assert!(json["items"].is_array());
128 assert_eq!(json["items"].as_array().unwrap().len(), 0);
129 assert_eq!(json["elapsed_ms"], 0);
130 }
131
132 #[test]
133 fn list_item_memory_id_igual_a_id() {
134 let item = ListItem {
135 id: 42,
136 memory_id: 42,
137 name: "memoria-alias".to_string(),
138 namespace: "projeto".to_string(),
139 memory_type: "fact".to_string(),
140 description: "desc".to_string(),
141 snippet: "snip".to_string(),
142 updated_at: 0,
143 updated_at_iso: "1970-01-01T00:00:00Z".to_string(),
144 };
145 let json = serde_json::to_value(&item).unwrap();
146 assert_eq!(
147 json["id"], json["memory_id"],
148 "id e memory_id devem ser iguais"
149 );
150 }
151
152 #[test]
153 fn snippet_truncado_em_200_chars() {
154 let body_longo: String = "a".repeat(300);
155 let snippet: String = body_longo.chars().take(200).collect();
156 assert_eq!(snippet.len(), 200, "snippet deve ter exatamente 200 chars");
157 }
158
159 #[test]
160 fn updated_at_iso_epoch_zero_gera_utc_valido() {
161 let iso = crate::tz::epoch_para_iso(0);
162 assert!(
163 iso.starts_with("1970-01-01T00:00:00"),
164 "epoch 0 deve mapear para 1970-01-01, obtido: {iso}"
165 );
166 assert!(
167 iso.contains('+') || iso.contains('-'),
168 "deve conter sinal de offset, obtido: {iso}"
169 );
170 }
171}