sqlite_graphrag/commands/
export.rs1use crate::cli::MemoryType;
4use crate::errors::AppError;
5use crate::output;
6use crate::paths::AppPaths;
7use crate::storage::connection::open_ro;
8use serde::Serialize;
9
10#[derive(clap::Args)]
11#[command(after_long_help = "EXAMPLES:\n \
12 # Export all memories as NDJSON\n \
13 sqlite-graphrag export\n\n \
14 # Export only decision memories from a namespace\n \
15 sqlite-graphrag export --type decision --namespace my-project\n\n \
16 # Export including soft-deleted memories\n \
17 sqlite-graphrag export --include-deleted\n\n \
18 # Pipe to file for backup\n \
19 sqlite-graphrag export > backup.ndjson")]
20pub struct ExportArgs {
22 #[arg(
24 long,
25 help = "Namespace (flag / XDG namespace.default / global)"
26 )]
27 pub namespace: Option<String>,
28 #[arg(long, value_enum)]
30 pub r#type: Option<MemoryType>,
31 #[arg(long, default_value_t = false)]
33 pub include_deleted: bool,
34 #[arg(long, default_value_t = 100_000)]
36 pub limit: usize,
37 #[arg(long, default_value_t = 0)]
39 pub offset: usize,
40 #[arg(long, hide = true, help = "No-op; JSON is always emitted on stdout")]
42 pub json: bool,
43 #[arg(long)]
45 pub db: Option<String>,
46}
47
48#[derive(Serialize)]
49struct ExportMemoryLine {
50 name: String,
51 r#type: String,
52 memory_type: String,
53 description: String,
54 body: String,
55 namespace: String,
56 created_at_iso: String,
57 updated_at_iso: String,
58 #[serde(skip_serializing_if = "Option::is_none")]
59 deleted_at_iso: Option<String>,
60}
61
62#[derive(Serialize)]
63struct ExportSummary {
64 summary: bool,
65 exported: usize,
66 namespace: String,
67 elapsed_ms: u64,
68}
69
70pub fn run(args: ExportArgs) -> Result<(), AppError> {
72 let start = std::time::Instant::now();
73 let namespace = crate::namespace::resolve_namespace(args.namespace.as_deref())?;
74 let paths = AppPaths::resolve(args.db.as_deref())?;
75 crate::storage::connection::ensure_db_ready(&paths)?;
76 let conn = open_ro(&paths.db)?;
77
78 let deleted_filter = if args.include_deleted {
79 ""
80 } else {
81 "AND m.deleted_at IS NULL"
82 };
83
84 let limit_i64 = args.limit as i64;
85 let offset_i64 = args.offset as i64;
86 let type_str: Option<String> = args.r#type.map(|t| t.as_str().to_string());
87
88 let rows = fetch_rows(
89 &conn,
90 &namespace,
91 &type_str,
92 deleted_filter,
93 limit_i64,
94 offset_i64,
95 )?;
96
97 let exported = rows.len();
98 for line in &rows {
99 output::emit_json_compact(line)?;
100 }
101
102 output::emit_json_compact(&ExportSummary {
103 summary: true,
104 exported,
105 namespace: namespace.clone(),
106 elapsed_ms: start.elapsed().as_millis() as u64,
107 })?;
108
109 Ok(())
110}
111
112fn fetch_rows(
113 conn: &rusqlite::Connection,
114 namespace: &str,
115 type_str: &Option<String>,
116 deleted_filter: &str,
117 limit: i64,
118 offset: i64,
119) -> Result<Vec<ExportMemoryLine>, AppError> {
120 let rows = if let Some(t) = type_str {
121 let sql = format!(
122 "SELECT m.name, m.type, m.description, m.body, m.namespace, \
123 m.created_at, m.updated_at, m.deleted_at \
124 FROM memories m \
125 WHERE m.namespace = ?1 {deleted_filter} AND m.type = ?2 \
126 ORDER BY m.name \
127 LIMIT ?3 OFFSET ?4"
128 );
129 let mut stmt = conn.prepare(&sql)?;
130 let result = stmt
131 .query_map(rusqlite::params![namespace, t, limit, offset], map_row)?
132 .collect::<Result<Vec<_>, _>>()?;
133 result
134 } else {
135 let sql = format!(
136 "SELECT m.name, m.type, m.description, m.body, m.namespace, \
137 m.created_at, m.updated_at, m.deleted_at \
138 FROM memories m \
139 WHERE m.namespace = ?1 {deleted_filter} \
140 ORDER BY m.name \
141 LIMIT ?2 OFFSET ?3"
142 );
143 let mut stmt = conn.prepare(&sql)?;
144 let result = stmt
145 .query_map(rusqlite::params![namespace, limit, offset], map_row)?
146 .collect::<Result<Vec<_>, _>>()?;
147 result
148 };
149 Ok(rows)
150}
151
152fn map_row(row: &rusqlite::Row<'_>) -> rusqlite::Result<ExportMemoryLine> {
153 let memory_type_val: String = row.get(1)?;
154 Ok(ExportMemoryLine {
155 name: row.get(0)?,
156 r#type: memory_type_val.clone(),
157 memory_type: memory_type_val,
158 description: row.get(2)?,
159 body: row.get(3)?,
160 namespace: row.get(4)?,
161 created_at_iso: crate::tz::epoch_to_iso(row.get::<_, i64>(5)?),
162 updated_at_iso: crate::tz::epoch_to_iso(row.get::<_, i64>(6)?),
163 deleted_at_iso: row.get::<_, Option<i64>>(7)?.map(crate::tz::epoch_to_iso),
164 })
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170
171 #[test]
172 fn export_line_emits_both_type_and_memory_type() {
173 let line = ExportMemoryLine {
174 name: "test".to_string(),
175 r#type: "document".to_string(),
176 memory_type: "document".to_string(),
177 description: "desc".to_string(),
178 body: "body".to_string(),
179 namespace: "global".to_string(),
180 created_at_iso: "2025-01-01T00:00:00Z".to_string(),
181 updated_at_iso: "2025-01-01T00:00:00Z".to_string(),
182 deleted_at_iso: None,
183 };
184 let json = serde_json::to_value(&line).unwrap();
185 assert_eq!(json["type"], "document");
186 assert_eq!(json["memory_type"], "document");
187 }
188}