use crate::cli::MemoryType;
use crate::errors::AppError;
use crate::output;
use crate::paths::AppPaths;
use crate::storage::connection::open_ro;
use serde::Serialize;
#[derive(clap::Args)]
#[command(after_long_help = "EXAMPLES:\n \
# Export all memories as NDJSON\n \
sqlite-graphrag export\n\n \
# Export only decision memories from a namespace\n \
sqlite-graphrag export --type decision --namespace my-project\n\n \
# Export including soft-deleted memories\n \
sqlite-graphrag export --include-deleted\n\n \
# Pipe to file for backup\n \
sqlite-graphrag export > backup.ndjson")]
pub struct ExportArgs {
#[arg(
long,
help = "Namespace (env: SQLITE_GRAPHRAG_NAMESPACE, default: global)"
)]
pub namespace: Option<String>,
#[arg(long, value_enum)]
pub r#type: Option<MemoryType>,
#[arg(long, default_value_t = false)]
pub include_deleted: bool,
#[arg(long, default_value_t = 100_000)]
pub limit: usize,
#[arg(long, default_value_t = 0)]
pub offset: usize,
#[arg(long, hide = true, help = "No-op; JSON is always emitted on stdout")]
pub json: bool,
#[arg(long, env = "SQLITE_GRAPHRAG_DB_PATH")]
pub db: Option<String>,
}
#[derive(Serialize)]
struct ExportMemoryLine {
name: String,
r#type: String,
description: String,
body: String,
namespace: String,
created_at_iso: String,
updated_at_iso: String,
#[serde(skip_serializing_if = "Option::is_none")]
deleted_at_iso: Option<String>,
}
#[derive(Serialize)]
struct ExportSummary {
summary: bool,
exported: usize,
namespace: String,
elapsed_ms: u64,
}
pub fn run(args: ExportArgs) -> Result<(), AppError> {
let start = std::time::Instant::now();
let namespace = crate::namespace::resolve_namespace(args.namespace.as_deref())?;
let paths = AppPaths::resolve(args.db.as_deref())?;
crate::storage::connection::ensure_db_ready(&paths)?;
let conn = open_ro(&paths.db)?;
let deleted_filter = if args.include_deleted {
""
} else {
"AND m.deleted_at IS NULL"
};
let limit_i64 = args.limit as i64;
let offset_i64 = args.offset as i64;
let type_str: Option<String> = args.r#type.map(|t| t.as_str().to_string());
let rows = fetch_rows(
&conn,
&namespace,
&type_str,
deleted_filter,
limit_i64,
offset_i64,
)?;
let exported = rows.len();
for line in &rows {
output::emit_json_compact(line)?;
}
output::emit_json_compact(&ExportSummary {
summary: true,
exported,
namespace: namespace.clone(),
elapsed_ms: start.elapsed().as_millis() as u64,
})?;
Ok(())
}
fn fetch_rows(
conn: &rusqlite::Connection,
namespace: &str,
type_str: &Option<String>,
deleted_filter: &str,
limit: i64,
offset: i64,
) -> Result<Vec<ExportMemoryLine>, AppError> {
let rows = if let Some(t) = type_str {
let sql = format!(
"SELECT m.name, m.type, m.description, m.body, m.namespace, \
m.created_at, m.updated_at, m.deleted_at \
FROM memories m \
WHERE m.namespace = ?1 {deleted_filter} AND m.type = ?2 \
ORDER BY m.name \
LIMIT ?3 OFFSET ?4"
);
let mut stmt = conn.prepare(&sql)?;
let result = stmt
.query_map(rusqlite::params![namespace, t, limit, offset], map_row)?
.collect::<Result<Vec<_>, _>>()?;
result
} else {
let sql = format!(
"SELECT m.name, m.type, m.description, m.body, m.namespace, \
m.created_at, m.updated_at, m.deleted_at \
FROM memories m \
WHERE m.namespace = ?1 {deleted_filter} \
ORDER BY m.name \
LIMIT ?2 OFFSET ?3"
);
let mut stmt = conn.prepare(&sql)?;
let result = stmt
.query_map(rusqlite::params![namespace, limit, offset], map_row)?
.collect::<Result<Vec<_>, _>>()?;
result
};
Ok(rows)
}
fn map_row(row: &rusqlite::Row<'_>) -> rusqlite::Result<ExportMemoryLine> {
Ok(ExportMemoryLine {
name: row.get(0)?,
r#type: row.get(1)?,
description: row.get(2)?,
body: row.get(3)?,
namespace: row.get(4)?,
created_at_iso: crate::tz::epoch_to_iso(row.get::<_, i64>(5)?),
updated_at_iso: crate::tz::epoch_to_iso(row.get::<_, i64>(6)?),
deleted_at_iso: row.get::<_, Option<i64>>(7)?.map(crate::tz::epoch_to_iso),
})
}