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