use std::io::Write;
use rushdb_lmpg::{reader::try_access, reconstruct};
use rushdb_storage::{ReadView, ScanBounds, ScanProjection};
use rushdb_types::{
ids::TenantId,
keys::{key, P_REL},
occurrence::{RecordHeader, RelationshipHeader},
};
pub fn export_tenant_jsonl_to<W: Write + ?Sized>(
view: &ReadView,
tenant: TenantId,
writer: &mut W,
) -> Result<(u64, u64), String> {
let mut counts = [0u64; 2];
let mut session = reconstruct::ReconstructSession::new(view, tenant);
for (kind, count) in counts.iter_mut().enumerate() {
let prefix = if kind == 0 {
rushdb_types::keys::record_hdr_prefix(tenant)
} else {
key(tenant, &[P_REL, b""])
};
let mut cursor = view
.scan(ScanBounds::prefix(&prefix), ScanProjection::KeyValue)
.map_err(|e| e.to_string())?;
loop {
let batch = cursor
.next_batch(128, 1024 * 1024)
.map_err(|e| e.to_string())?;
for row in batch.rows {
let raw = row.value.ok_or("export: missing header value")?;
let line = if kind == 0 {
record_line(&mut session, view, tenant, &row.key, &raw)?
} else {
relationship_line(&mut session, view, tenant, &row.key, &raw)?
};
view.check_cancelled().map_err(|e| e.to_string())?;
serde_json::to_writer(&mut *writer, &line).map_err(|e| e.to_string())?;
writer.write_all(b"\n").map_err(|e| e.to_string())?;
view.check_cancelled().map_err(|e| e.to_string())?;
*count = count.checked_add(1).ok_or("export count overflow")?;
}
if batch.done {
break;
}
}
}
let identities = count_keys(view, &rushdb_types::keys::record_id_prefix(tenant))?;
if identities != counts[0] {
return Err("export: record identity index does not match headers".into());
}
Ok((counts[0], counts[1]))
}
fn count_keys(view: &ReadView, prefix: &[u8]) -> Result<u64, String> {
let mut cursor = view
.scan(ScanBounds::prefix(prefix), ScanProjection::Keys)
.map_err(|e| e.to_string())?;
let mut total = 0u64;
loop {
let batch = cursor
.next_batch(1024, 1024 * 1024)
.map_err(|e| e.to_string())?;
total = total
.checked_add(batch.rows.len() as u64)
.ok_or("export count overflow")?;
if batch.done {
return Ok(total);
}
}
}
fn record_line(
session: &mut reconstruct::ReconstructSession<'_, ReadView>,
view: &ReadView,
tenant: TenantId,
header_key: &[u8],
raw: &[u8],
) -> Result<serde_json::Value, String> {
let ordinal = rushdb_types::keys::parse_record_hdr_key(header_key)
.ok_or("export: invalid record header key")?;
let (record_id, label) =
rushdb_types::keys::decode_record_hdr(raw).ok_or("export: invalid record header")?;
let hdr = RecordHeader {
record_id,
ordinal,
label: label.to_owned(),
};
let indexed = try_access::record_by_id(view, tenant, &rushdb_types::ids::RecordId(record_id))
.map_err(|e| e.to_string())?
.ok_or("export: missing record id mapping")?;
if indexed.ordinal != hdr.ordinal {
return Err("export: record id/ordinal identity mismatch".into());
}
let rec = session
.reconstruct_record(hdr.ordinal)
.map_err(|e| e.to_string())?
.ok_or("export: missing record during reconstruction")?;
let serde_json::Value::Object(mut properties) = rec else {
return Err("export: invalid record object".into());
};
properties.retain(|name, _| !rushdb_types::value::is_reserved_name(name));
Ok(serde_json::json!({
"op": "put", "kind": "record",
"id": hex(&hdr.record_id), "label": hdr.label,
"properties": properties,
}))
}
fn relationship_line(
session: &mut reconstruct::ReconstructSession<'_, ReadView>,
view: &ReadView,
tenant: TenantId,
row_key: &[u8],
raw: &[u8],
) -> Result<serde_json::Value, String> {
let hdr: RelationshipHeader = serde_json::from_slice(raw)
.map_err(|e| format!("export: invalid relationship header: {e}"))?;
if row_key != key(tenant, &[P_REL, &hdr.relationship_id]) {
return Err("export: relationship key/header identity mismatch".into());
}
let indexed = try_access::relationship_by_ord(view, tenant, hdr.ordinal)
.map_err(|e| e.to_string())?
.ok_or("export: missing relationship ordinal mapping")?;
if indexed.relationship_id != hdr.relationship_id {
return Err("export: relationship ordinal identity mismatch".into());
}
for (id, ordinal) in [
(hdr.source_id, hdr.source_ord),
(hdr.target_id, hdr.target_ord),
] {
let endpoint = try_access::record_by_id(view, tenant, &rushdb_types::ids::RecordId(id))
.map_err(|e| e.to_string())?
.ok_or("export: missing relationship endpoint")?;
if endpoint.ordinal != ordinal {
return Err("export: relationship endpoint identity mismatch".into());
}
}
let rel = session
.reconstruct_relationship(hdr.ordinal)
.map_err(|e| e.to_string())?
.ok_or("export: missing relationship during reconstruction")?;
let serde_json::Value::Object(mut properties) = rel else {
return Err("export: invalid relationship object".into());
};
properties.retain(|name, _| !rushdb_types::value::is_reserved_name(name));
Ok(serde_json::json!({
"op": "put", "kind": "relationship",
"id": hex(&hdr.relationship_id), "type": hdr.rel_type,
"source": hex(&hdr.source_id), "target": hex(&hdr.target_id),
"properties": properties,
}))
}
fn hex(id: &[u8; 16]) -> String {
uuid::Uuid::from_bytes(*id).simple().to_string()
}