1use std::io::Write;
5
6use rushdb_lmpg::{reader::try_access, reconstruct};
7use rushdb_storage::{ReadView, ScanBounds, ScanProjection};
8use rushdb_types::{
9 ids::TenantId,
10 keys::{key, P_REL},
11 occurrence::{RecordHeader, RelationshipHeader},
12};
13
14pub fn export_tenant_jsonl_to<W: Write + ?Sized>(
31 view: &ReadView,
32 tenant: TenantId,
33 writer: &mut W,
34) -> Result<(u64, u64), String> {
35 let mut counts = [0u64; 2];
36 let mut session = reconstruct::ReconstructSession::new(view, tenant);
39 for (kind, count) in counts.iter_mut().enumerate() {
40 let prefix = if kind == 0 {
43 rushdb_types::keys::record_hdr_prefix(tenant)
44 } else {
45 key(tenant, &[P_REL, b""])
46 };
47 let mut cursor = view
48 .scan(ScanBounds::prefix(&prefix), ScanProjection::KeyValue)
49 .map_err(|e| e.to_string())?;
50 loop {
51 let batch = cursor
52 .next_batch(128, 1024 * 1024)
53 .map_err(|e| e.to_string())?;
54 for row in batch.rows {
55 let raw = row.value.ok_or("export: missing header value")?;
56 let line = if kind == 0 {
57 record_line(&mut session, view, tenant, &row.key, &raw)?
58 } else {
59 relationship_line(&mut session, view, tenant, &row.key, &raw)?
60 };
61 view.check_cancelled().map_err(|e| e.to_string())?;
62 serde_json::to_writer(&mut *writer, &line).map_err(|e| e.to_string())?;
63 writer.write_all(b"\n").map_err(|e| e.to_string())?;
64 view.check_cancelled().map_err(|e| e.to_string())?;
65 *count = count.checked_add(1).ok_or("export count overflow")?;
66 }
67 if batch.done {
68 break;
69 }
70 }
71 }
72 let identities = count_keys(view, &rushdb_types::keys::record_id_prefix(tenant))?;
76 if identities != counts[0] {
77 return Err("export: record identity index does not match headers".into());
78 }
79 Ok((counts[0], counts[1]))
80}
81
82fn count_keys(view: &ReadView, prefix: &[u8]) -> Result<u64, String> {
84 let mut cursor = view
85 .scan(ScanBounds::prefix(prefix), ScanProjection::Keys)
86 .map_err(|e| e.to_string())?;
87 let mut total = 0u64;
88 loop {
89 let batch = cursor
90 .next_batch(1024, 1024 * 1024)
91 .map_err(|e| e.to_string())?;
92 total = total
93 .checked_add(batch.rows.len() as u64)
94 .ok_or("export count overflow")?;
95 if batch.done {
96 return Ok(total);
97 }
98 }
99}
100
101fn record_line(
103 session: &mut reconstruct::ReconstructSession<'_, ReadView>,
104 view: &ReadView,
105 tenant: TenantId,
106 header_key: &[u8],
107 raw: &[u8],
108) -> Result<serde_json::Value, String> {
109 let ordinal = rushdb_types::keys::parse_record_hdr_key(header_key)
110 .ok_or("export: invalid record header key")?;
111 let (record_id, label) =
112 rushdb_types::keys::decode_record_hdr(raw).ok_or("export: invalid record header")?;
113 let hdr = RecordHeader {
114 record_id,
115 ordinal,
116 label: label.to_owned(),
117 };
118 let indexed = try_access::record_by_id(view, tenant, &rushdb_types::ids::RecordId(record_id))
119 .map_err(|e| e.to_string())?
120 .ok_or("export: missing record id mapping")?;
121 if indexed.ordinal != hdr.ordinal {
122 return Err("export: record id/ordinal identity mismatch".into());
123 }
124 let rec = session
125 .reconstruct_record(hdr.ordinal)
126 .map_err(|e| e.to_string())?
127 .ok_or("export: missing record during reconstruction")?;
128 let serde_json::Value::Object(mut properties) = rec else {
129 return Err("export: invalid record object".into());
130 };
131 properties.retain(|name, _| !rushdb_types::value::is_reserved_name(name));
132 Ok(serde_json::json!({
133 "op": "put", "kind": "record",
134 "id": hex(&hdr.record_id), "label": hdr.label,
135 "properties": properties,
136 }))
137}
138
139fn relationship_line(
141 session: &mut reconstruct::ReconstructSession<'_, ReadView>,
142 view: &ReadView,
143 tenant: TenantId,
144 row_key: &[u8],
145 raw: &[u8],
146) -> Result<serde_json::Value, String> {
147 let hdr: RelationshipHeader = serde_json::from_slice(raw)
148 .map_err(|e| format!("export: invalid relationship header: {e}"))?;
149 if row_key != key(tenant, &[P_REL, &hdr.relationship_id]) {
150 return Err("export: relationship key/header identity mismatch".into());
151 }
152 let indexed = try_access::relationship_by_ord(view, tenant, hdr.ordinal)
153 .map_err(|e| e.to_string())?
154 .ok_or("export: missing relationship ordinal mapping")?;
155 if indexed.relationship_id != hdr.relationship_id {
156 return Err("export: relationship ordinal identity mismatch".into());
157 }
158 for (id, ordinal) in [
159 (hdr.source_id, hdr.source_ord),
160 (hdr.target_id, hdr.target_ord),
161 ] {
162 let endpoint = try_access::record_by_id(view, tenant, &rushdb_types::ids::RecordId(id))
165 .map_err(|e| e.to_string())?
166 .ok_or("export: missing relationship endpoint")?;
167 if endpoint.ordinal != ordinal {
168 return Err("export: relationship endpoint identity mismatch".into());
169 }
170 }
171 let rel = session
172 .reconstruct_relationship(hdr.ordinal)
173 .map_err(|e| e.to_string())?
174 .ok_or("export: missing relationship during reconstruction")?;
175 let serde_json::Value::Object(mut properties) = rel else {
176 return Err("export: invalid relationship object".into());
177 };
178 properties.retain(|name, _| !rushdb_types::value::is_reserved_name(name));
179 Ok(serde_json::json!({
180 "op": "put", "kind": "relationship",
181 "id": hex(&hdr.relationship_id), "type": hdr.rel_type,
182 "source": hex(&hdr.source_id), "target": hex(&hdr.target_id),
183 "properties": properties,
184 }))
185}
186
187fn hex(id: &[u8; 16]) -> String {
188 uuid::Uuid::from_bytes(*id).simple().to_string()
189}