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, env = "SQLITE_GRAPHRAG_DB_PATH")]
41 pub db: Option<String>,
42}
43
44#[derive(Serialize)]
45struct ExportMemoryLine {
46 name: String,
47 r#type: String,
48 description: String,
49 body: String,
50 namespace: String,
51 created_at_iso: String,
52 updated_at_iso: String,
53 #[serde(skip_serializing_if = "Option::is_none")]
54 deleted_at_iso: Option<String>,
55}
56
57#[derive(Serialize)]
58struct ExportSummary {
59 summary: bool,
60 exported: usize,
61 namespace: String,
62 elapsed_ms: u64,
63}
64
65pub fn run(args: ExportArgs) -> Result<(), AppError> {
67 let start = std::time::Instant::now();
68 let namespace = crate::namespace::resolve_namespace(args.namespace.as_deref())?;
69 let paths = AppPaths::resolve(args.db.as_deref())?;
70 crate::storage::connection::ensure_db_ready(&paths)?;
71 let conn = open_ro(&paths.db)?;
72
73 let deleted_filter = if args.include_deleted {
74 ""
75 } else {
76 "AND m.deleted_at IS NULL"
77 };
78
79 let limit_i64 = args.limit as i64;
80 let offset_i64 = args.offset as i64;
81 let type_str: Option<String> = args.r#type.map(|t| t.as_str().to_string());
82
83 let rows = fetch_rows(
84 &conn,
85 &namespace,
86 &type_str,
87 deleted_filter,
88 limit_i64,
89 offset_i64,
90 )?;
91
92 let exported = rows.len();
93 for line in &rows {
94 output::emit_json_compact(line)?;
95 }
96
97 output::emit_json_compact(&ExportSummary {
98 summary: true,
99 exported,
100 namespace: namespace.clone(),
101 elapsed_ms: start.elapsed().as_millis() as u64,
102 })?;
103
104 Ok(())
105}
106
107fn fetch_rows(
108 conn: &rusqlite::Connection,
109 namespace: &str,
110 type_str: &Option<String>,
111 deleted_filter: &str,
112 limit: i64,
113 offset: i64,
114) -> Result<Vec<ExportMemoryLine>, AppError> {
115 let rows = if let Some(t) = type_str {
116 let sql = format!(
117 "SELECT m.name, m.type, m.description, m.body, m.namespace, \
118 m.created_at, m.updated_at, m.deleted_at \
119 FROM memories m \
120 WHERE m.namespace = ?1 {deleted_filter} AND m.type = ?2 \
121 ORDER BY m.name \
122 LIMIT ?3 OFFSET ?4"
123 );
124 let mut stmt = conn.prepare(&sql)?;
125 let result = stmt
126 .query_map(rusqlite::params![namespace, t, limit, offset], map_row)?
127 .collect::<Result<Vec<_>, _>>()?;
128 result
129 } else {
130 let sql = format!(
131 "SELECT m.name, m.type, m.description, m.body, m.namespace, \
132 m.created_at, m.updated_at, m.deleted_at \
133 FROM memories m \
134 WHERE m.namespace = ?1 {deleted_filter} \
135 ORDER BY m.name \
136 LIMIT ?2 OFFSET ?3"
137 );
138 let mut stmt = conn.prepare(&sql)?;
139 let result = stmt
140 .query_map(rusqlite::params![namespace, limit, offset], map_row)?
141 .collect::<Result<Vec<_>, _>>()?;
142 result
143 };
144 Ok(rows)
145}
146
147fn map_row(row: &rusqlite::Row<'_>) -> rusqlite::Result<ExportMemoryLine> {
148 Ok(ExportMemoryLine {
149 name: row.get(0)?,
150 r#type: row.get(1)?,
151 description: row.get(2)?,
152 body: row.get(3)?,
153 namespace: row.get(4)?,
154 created_at_iso: crate::tz::epoch_to_iso(row.get::<_, i64>(5)?),
155 updated_at_iso: crate::tz::epoch_to_iso(row.get::<_, i64>(6)?),
156 deleted_at_iso: row.get::<_, Option<i64>>(7)?.map(crate::tz::epoch_to_iso),
157 })
158}