1pub const GRAPH_MAX_ID_SIZE: usize = 256;
7pub const GRAPH_MAX_LABEL_SIZE: usize = 512;
8
9pub const GRAPH_NODE_HEADER_SIZE_V1: usize = 10;
10pub const GRAPH_NODE_HEADER_SIZE: usize = 13;
11pub const GRAPH_TABLE_REF_SIZE: usize = 10;
12pub const GRAPH_NODE_FLAG_HAS_TABLE_REF: u8 = 0x01;
13pub const GRAPH_NODE_FLAG_HAS_VECTOR_REF: u8 = 0x02;
14pub const GRAPH_VECTOR_REF_HEADER_SIZE: usize = 10;
15
16pub const GRAPH_EDGE_HEADER_SIZE_V1: usize = 9;
17pub const GRAPH_EDGE_HEADER_SIZE: usize = 12;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
20pub struct GraphTableRef {
21 pub table_id: u16,
22 pub row_id: u64,
23}
24
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct GraphVectorRef {
27 pub collection: String,
28 pub vector_id: u64,
29}
30
31#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct GraphNodeRecord {
33 pub id: String,
34 pub label: String,
35 pub label_id: u32,
36 pub flags: u8,
37 pub out_edge_count: u32,
38 pub in_edge_count: u32,
39 pub table_ref: Option<GraphTableRef>,
40 pub vector_ref: Option<GraphVectorRef>,
41}
42
43#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct LegacyGraphNodeRecord {
45 pub id: String,
46 pub label: String,
47 pub legacy_type: u8,
48 pub flags: u8,
49 pub out_edge_count: u32,
50 pub in_edge_count: u32,
51 pub table_ref: Option<GraphTableRef>,
52 pub vector_ref: Option<GraphVectorRef>,
53}
54
55#[derive(Debug, Clone, PartialEq)]
56pub struct GraphEdgeRecord {
57 pub source_id: String,
58 pub target_id: String,
59 pub label_id: u32,
60 pub weight: f32,
61}
62
63#[derive(Debug, Clone, PartialEq)]
64pub struct LegacyGraphEdgeRecord {
65 pub source_id: String,
66 pub target_id: String,
67 pub legacy_type: u8,
68 pub weight: f32,
69}
70
71pub fn encode_graph_table_ref(table_ref: GraphTableRef) -> [u8; GRAPH_TABLE_REF_SIZE] {
72 let mut buf = [0u8; GRAPH_TABLE_REF_SIZE];
73 buf[0..2].copy_from_slice(&table_ref.table_id.to_le_bytes());
74 buf[2..10].copy_from_slice(&table_ref.row_id.to_le_bytes());
75 buf
76}
77
78pub fn decode_graph_table_ref(data: &[u8]) -> Option<GraphTableRef> {
79 if data.len() < GRAPH_TABLE_REF_SIZE {
80 return None;
81 }
82 Some(GraphTableRef {
83 table_id: u16::from_le_bytes(data[0..2].try_into().ok()?),
84 row_id: u64::from_le_bytes(data[2..10].try_into().ok()?),
85 })
86}
87
88pub fn encode_graph_node_record_v2(record: &GraphNodeRecord) -> Vec<u8> {
89 let id_bytes = record.id.as_bytes();
90 let label_bytes = record.label.as_bytes();
91 let has_table_ref = record.table_ref.is_some();
92 let has_vector_ref = record.vector_ref.is_some();
93
94 let mut flags =
95 record.flags & !(GRAPH_NODE_FLAG_HAS_TABLE_REF | GRAPH_NODE_FLAG_HAS_VECTOR_REF);
96 if has_table_ref {
97 flags |= GRAPH_NODE_FLAG_HAS_TABLE_REF;
98 }
99 if has_vector_ref {
100 flags |= GRAPH_NODE_FLAG_HAS_VECTOR_REF;
101 }
102
103 let table_ref_size = if has_table_ref {
104 GRAPH_TABLE_REF_SIZE
105 } else {
106 0
107 };
108 let vector_ref_size = record
109 .vector_ref
110 .as_ref()
111 .map(|v| 2 + v.collection.len() + 8)
112 .unwrap_or(0);
113
114 let mut buf = Vec::with_capacity(
115 GRAPH_NODE_HEADER_SIZE
116 + id_bytes.len()
117 + label_bytes.len()
118 + table_ref_size
119 + vector_ref_size,
120 );
121 buf.extend_from_slice(&(id_bytes.len() as u16).to_le_bytes());
122 buf.extend_from_slice(&(label_bytes.len() as u16).to_le_bytes());
123 buf.extend_from_slice(&record.label_id.to_le_bytes());
124 buf.push(flags);
125 buf.extend_from_slice(&(record.out_edge_count as u16).to_le_bytes());
126 buf.extend_from_slice(&(record.in_edge_count as u16).to_le_bytes());
127 buf.extend_from_slice(id_bytes);
128 buf.extend_from_slice(label_bytes);
129
130 if let Some(table_ref) = record.table_ref {
131 buf.extend_from_slice(&encode_graph_table_ref(table_ref));
132 }
133 if let Some(vector_ref) = &record.vector_ref {
134 let collection = vector_ref.collection.as_bytes();
135 buf.extend_from_slice(&(collection.len() as u16).to_le_bytes());
136 buf.extend_from_slice(collection);
137 buf.extend_from_slice(&vector_ref.vector_id.to_le_bytes());
138 }
139 buf
140}
141
142pub fn decode_graph_node_record_v2(data: &[u8]) -> Option<GraphNodeRecord> {
143 if data.len() < GRAPH_NODE_HEADER_SIZE {
144 return None;
145 }
146 let id_len = u16::from_le_bytes(data[0..2].try_into().ok()?) as usize;
147 let label_len = u16::from_le_bytes(data[2..4].try_into().ok()?) as usize;
148 let label_id = u32::from_le_bytes(data[4..8].try_into().ok()?);
149 let flags = data[8];
150 let out_edge_count = u16::from_le_bytes(data[9..11].try_into().ok()?) as u32;
151 let in_edge_count = u16::from_le_bytes(data[11..13].try_into().ok()?) as u32;
152 let (id, label, table_ref, vector_ref) =
153 decode_graph_node_payload(data, GRAPH_NODE_HEADER_SIZE, id_len, label_len, flags)?;
154 Some(GraphNodeRecord {
155 id,
156 label,
157 label_id,
158 flags,
159 out_edge_count,
160 in_edge_count,
161 table_ref,
162 vector_ref,
163 })
164}
165
166pub fn decode_graph_node_record_v1(data: &[u8]) -> Option<LegacyGraphNodeRecord> {
167 if data.len() < GRAPH_NODE_HEADER_SIZE_V1 {
168 return None;
169 }
170 let id_len = u16::from_le_bytes(data[0..2].try_into().ok()?) as usize;
171 let label_len = u16::from_le_bytes(data[2..4].try_into().ok()?) as usize;
172 let legacy_type = data[4];
173 let flags = data[5];
174 let out_edge_count = u16::from_le_bytes(data[6..8].try_into().ok()?) as u32;
175 let in_edge_count = u16::from_le_bytes(data[8..10].try_into().ok()?) as u32;
176 let (id, label, table_ref, vector_ref) =
177 decode_graph_node_payload(data, GRAPH_NODE_HEADER_SIZE_V1, id_len, label_len, flags)?;
178 Some(LegacyGraphNodeRecord {
179 id,
180 label,
181 legacy_type,
182 flags,
183 out_edge_count,
184 in_edge_count,
185 table_ref,
186 vector_ref,
187 })
188}
189
190fn decode_graph_node_payload(
191 data: &[u8],
192 header_size: usize,
193 id_len: usize,
194 label_len: usize,
195 flags: u8,
196) -> Option<(
197 String,
198 String,
199 Option<GraphTableRef>,
200 Option<GraphVectorRef>,
201)> {
202 let has_table_ref = (flags & GRAPH_NODE_FLAG_HAS_TABLE_REF) != 0;
203 let has_vector_ref = (flags & GRAPH_NODE_FLAG_HAS_VECTOR_REF) != 0;
204 let table_ref_size = if has_table_ref {
205 GRAPH_TABLE_REF_SIZE
206 } else {
207 0
208 };
209 let mut offset = header_size + id_len + label_len + table_ref_size;
210 if data.len() < offset {
211 return None;
212 }
213
214 let id = String::from_utf8_lossy(&data[header_size..header_size + id_len]).to_string();
215 let label =
216 String::from_utf8_lossy(&data[header_size + id_len..header_size + id_len + label_len])
217 .to_string();
218 let table_ref = if has_table_ref {
219 let start = header_size + id_len + label_len;
220 decode_graph_table_ref(&data[start..])
221 } else {
222 None
223 };
224 let vector_ref = if has_vector_ref {
225 if data.len() < offset + 2 {
226 return None;
227 }
228 let collection_len = u16::from_le_bytes(data[offset..offset + 2].try_into().ok()?) as usize;
229 offset += 2;
230 if data.len() < offset + collection_len + 8 {
231 return None;
232 }
233 let collection =
234 String::from_utf8_lossy(&data[offset..offset + collection_len]).to_string();
235 offset += collection_len;
236 let vector_id = u64::from_le_bytes(data[offset..offset + 8].try_into().ok()?);
237 Some(GraphVectorRef {
238 collection,
239 vector_id,
240 })
241 } else {
242 None
243 };
244 Some((id, label, table_ref, vector_ref))
245}
246
247pub fn graph_node_record_v2_encoded_size(record: &GraphNodeRecord) -> usize {
248 let table_ref_size = if record.table_ref.is_some() {
249 GRAPH_TABLE_REF_SIZE
250 } else {
251 0
252 };
253 let vector_ref_size = record
254 .vector_ref
255 .as_ref()
256 .map(|v| 2 + v.collection.len() + 8)
257 .unwrap_or(0);
258 GRAPH_NODE_HEADER_SIZE + record.id.len() + record.label.len() + table_ref_size + vector_ref_size
259}
260
261pub fn encode_graph_edge_record_v2(record: &GraphEdgeRecord) -> Vec<u8> {
262 let source_bytes = record.source_id.as_bytes();
263 let target_bytes = record.target_id.as_bytes();
264 let mut buf =
265 Vec::with_capacity(GRAPH_EDGE_HEADER_SIZE + source_bytes.len() + target_bytes.len());
266 buf.extend_from_slice(&(source_bytes.len() as u16).to_le_bytes());
267 buf.extend_from_slice(&(target_bytes.len() as u16).to_le_bytes());
268 buf.extend_from_slice(&record.label_id.to_le_bytes());
269 buf.extend_from_slice(&record.weight.to_le_bytes());
270 buf.extend_from_slice(source_bytes);
271 buf.extend_from_slice(target_bytes);
272 buf
273}
274
275pub fn decode_graph_edge_record_v2(data: &[u8]) -> Option<GraphEdgeRecord> {
276 if data.len() < GRAPH_EDGE_HEADER_SIZE {
277 return None;
278 }
279 let source_len = u16::from_le_bytes(data[0..2].try_into().ok()?) as usize;
280 let target_len = u16::from_le_bytes(data[2..4].try_into().ok()?) as usize;
281 let label_id = u32::from_le_bytes(data[4..8].try_into().ok()?);
282 let weight = f32::from_le_bytes(data[8..12].try_into().ok()?);
283 if data.len() < GRAPH_EDGE_HEADER_SIZE + source_len + target_len {
284 return None;
285 }
286 let source_id =
287 String::from_utf8_lossy(&data[GRAPH_EDGE_HEADER_SIZE..GRAPH_EDGE_HEADER_SIZE + source_len])
288 .to_string();
289 let target_id = String::from_utf8_lossy(
290 &data
291 [GRAPH_EDGE_HEADER_SIZE + source_len..GRAPH_EDGE_HEADER_SIZE + source_len + target_len],
292 )
293 .to_string();
294 Some(GraphEdgeRecord {
295 source_id,
296 target_id,
297 label_id,
298 weight,
299 })
300}
301
302pub fn decode_graph_edge_record_v1(data: &[u8]) -> Option<LegacyGraphEdgeRecord> {
303 if data.len() < GRAPH_EDGE_HEADER_SIZE_V1 {
304 return None;
305 }
306 let source_len = u16::from_le_bytes(data[0..2].try_into().ok()?) as usize;
307 let target_len = u16::from_le_bytes(data[2..4].try_into().ok()?) as usize;
308 let legacy_type = data[4];
309 let weight = f32::from_le_bytes(data[5..9].try_into().ok()?);
310 if data.len() < GRAPH_EDGE_HEADER_SIZE_V1 + source_len + target_len {
311 return None;
312 }
313 let source_id = String::from_utf8_lossy(
314 &data[GRAPH_EDGE_HEADER_SIZE_V1..GRAPH_EDGE_HEADER_SIZE_V1 + source_len],
315 )
316 .to_string();
317 let target_id = String::from_utf8_lossy(
318 &data[GRAPH_EDGE_HEADER_SIZE_V1 + source_len
319 ..GRAPH_EDGE_HEADER_SIZE_V1 + source_len + target_len],
320 )
321 .to_string();
322 Some(LegacyGraphEdgeRecord {
323 source_id,
324 target_id,
325 legacy_type,
326 weight,
327 })
328}
329
330pub fn graph_edge_record_v2_encoded_size(record: &GraphEdgeRecord) -> usize {
331 GRAPH_EDGE_HEADER_SIZE + record.source_id.len() + record.target_id.len()
332}
333
334#[cfg(test)]
335mod tests {
336 use super::*;
337
338 #[test]
339 fn graph_table_ref_round_trips() {
340 let table_ref = GraphTableRef {
341 table_id: 7,
342 row_id: 42,
343 };
344 let bytes = encode_graph_table_ref(table_ref);
345 assert_eq!(decode_graph_table_ref(&bytes), Some(table_ref));
346 }
347
348 #[test]
349 fn graph_node_v2_round_trips_with_refs() {
350 let record = GraphNodeRecord {
351 id: "node-1".to_string(),
352 label: "Node One".to_string(),
353 label_id: 64,
354 flags: 0x80,
355 out_edge_count: 2,
356 in_edge_count: 3,
357 table_ref: Some(GraphTableRef {
358 table_id: 9,
359 row_id: 10,
360 }),
361 vector_ref: Some(GraphVectorRef {
362 collection: "embeddings".to_string(),
363 vector_id: 11,
364 }),
365 };
366 let encoded = encode_graph_node_record_v2(&record);
367 let decoded = decode_graph_node_record_v2(&encoded).unwrap();
368 assert_eq!(decoded.id, record.id);
369 assert_eq!(decoded.label, record.label);
370 assert_eq!(decoded.label_id, record.label_id);
371 assert_eq!(decoded.flags & 0x80, 0x80);
372 assert_eq!(decoded.table_ref, record.table_ref);
373 assert_eq!(decoded.vector_ref, record.vector_ref);
374 }
375
376 #[test]
377 fn graph_edge_v2_round_trips() {
378 let record = GraphEdgeRecord {
379 source_id: "a".to_string(),
380 target_id: "b".to_string(),
381 label_id: 10,
382 weight: 1.5,
383 };
384 let encoded = encode_graph_edge_record_v2(&record);
385 assert_eq!(decode_graph_edge_record_v2(&encoded).unwrap(), record);
386 }
387
388 #[test]
389 fn legacy_records_decode_shape() {
390 let mut node = Vec::new();
391 node.extend_from_slice(&1u16.to_le_bytes());
392 node.extend_from_slice(&1u16.to_le_bytes());
393 node.push(0);
394 node.push(0);
395 node.extend_from_slice(&0u16.to_le_bytes());
396 node.extend_from_slice(&0u16.to_le_bytes());
397 node.extend_from_slice(b"a");
398 node.extend_from_slice(b"b");
399 assert_eq!(decode_graph_node_record_v1(&node).unwrap().legacy_type, 0);
400
401 let mut edge = Vec::new();
402 edge.extend_from_slice(&1u16.to_le_bytes());
403 edge.extend_from_slice(&1u16.to_le_bytes());
404 edge.push(0);
405 edge.extend_from_slice(&1.0f32.to_le_bytes());
406 edge.extend_from_slice(b"a");
407 edge.extend_from_slice(b"b");
408 assert_eq!(decode_graph_edge_record_v1(&edge).unwrap().legacy_type, 0);
409 }
410}