Skip to main content

reddb_file/
graph_record.rs

1//! Persisted graph node/edge record frames.
2//!
3//! The graph engine owns indexes, label resolution, and page locations. This
4//! module owns the durable byte layout for graph records stored inside pages.
5
6pub 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}