Skip to main content

graphos_core/graph/lpg/
node.rs

1//! Node types for the LPG model.
2
3use graphos_common::types::{EpochId, NodeId, PropertyKey, Value};
4use std::collections::BTreeMap;
5use std::sync::Arc;
6
7/// A node in the labeled property graph.
8///
9/// This is the high-level representation of a node with all its data.
10#[derive(Debug, Clone)]
11pub struct Node {
12    /// Unique identifier.
13    pub id: NodeId,
14    /// Labels attached to this node.
15    pub labels: Vec<Arc<str>>,
16    /// Properties stored on this node.
17    pub properties: BTreeMap<PropertyKey, Value>,
18}
19
20impl Node {
21    /// Creates a new node with the given ID.
22    #[must_use]
23    pub fn new(id: NodeId) -> Self {
24        Self {
25            id,
26            labels: Vec::new(),
27            properties: BTreeMap::new(),
28        }
29    }
30
31    /// Creates a new node with labels.
32    #[must_use]
33    pub fn with_labels(id: NodeId, labels: impl IntoIterator<Item = impl Into<Arc<str>>>) -> Self {
34        Self {
35            id,
36            labels: labels.into_iter().map(Into::into).collect(),
37            properties: BTreeMap::new(),
38        }
39    }
40
41    /// Adds a label to this node.
42    pub fn add_label(&mut self, label: impl Into<Arc<str>>) {
43        let label = label.into();
44        if !self.labels.iter().any(|l| l.as_ref() == label.as_ref()) {
45            self.labels.push(label);
46        }
47    }
48
49    /// Removes a label from this node.
50    pub fn remove_label(&mut self, label: &str) -> bool {
51        if let Some(pos) = self.labels.iter().position(|l| l.as_ref() == label) {
52            self.labels.remove(pos);
53            true
54        } else {
55            false
56        }
57    }
58
59    /// Checks if this node has the given label.
60    #[must_use]
61    pub fn has_label(&self, label: &str) -> bool {
62        self.labels.iter().any(|l| l.as_ref() == label)
63    }
64
65    /// Sets a property on this node.
66    pub fn set_property(&mut self, key: impl Into<PropertyKey>, value: impl Into<Value>) {
67        self.properties.insert(key.into(), value.into());
68    }
69
70    /// Gets a property from this node.
71    #[must_use]
72    pub fn get_property(&self, key: &str) -> Option<&Value> {
73        self.properties.get(&PropertyKey::new(key))
74    }
75
76    /// Removes a property from this node.
77    pub fn remove_property(&mut self, key: &str) -> Option<Value> {
78        self.properties.remove(&PropertyKey::new(key))
79    }
80}
81
82/// The compact, cache-line friendly representation of a node.
83///
84/// This struct is exactly 32 bytes and is used for the primary node storage.
85/// Properties are stored separately in columnar format.
86#[repr(C)]
87#[derive(Debug, Clone, Copy)]
88pub struct NodeRecord {
89    /// Unique node identifier.
90    pub id: NodeId,
91    /// Bitmap of label IDs (supports up to 64 labels).
92    pub label_bits: u64,
93    /// Offset into the property arena.
94    pub props_offset: u32,
95    /// Number of properties.
96    pub props_count: u16,
97    /// Flags (deleted, has_version, etc.).
98    pub flags: NodeFlags,
99    /// Epoch this record was created in.
100    pub epoch: EpochId,
101}
102
103impl NodeRecord {
104    /// Flag indicating the node is deleted.
105    pub const FLAG_DELETED: u16 = 1 << 0;
106    /// Flag indicating the node has version history.
107    pub const FLAG_HAS_VERSION: u16 = 1 << 1;
108
109    /// Creates a new node record.
110    #[must_use]
111    pub const fn new(id: NodeId, epoch: EpochId) -> Self {
112        Self {
113            id,
114            label_bits: 0,
115            props_offset: 0,
116            props_count: 0,
117            flags: NodeFlags(0),
118            epoch,
119        }
120    }
121
122    /// Checks if this node is deleted.
123    #[must_use]
124    pub const fn is_deleted(&self) -> bool {
125        self.flags.contains(Self::FLAG_DELETED)
126    }
127
128    /// Marks this node as deleted.
129    pub fn set_deleted(&mut self, deleted: bool) {
130        if deleted {
131            self.flags.set(Self::FLAG_DELETED);
132        } else {
133            self.flags.clear(Self::FLAG_DELETED);
134        }
135    }
136
137    /// Checks if this node has a label by its bit index.
138    #[must_use]
139    pub const fn has_label_bit(&self, bit: u8) -> bool {
140        if bit >= 64 {
141            return false;
142        }
143        (self.label_bits & (1 << bit)) != 0
144    }
145
146    /// Sets a label bit.
147    pub fn set_label_bit(&mut self, bit: u8) {
148        if bit < 64 {
149            self.label_bits |= 1 << bit;
150        }
151    }
152
153    /// Clears a label bit.
154    pub fn clear_label_bit(&mut self, bit: u8) {
155        if bit < 64 {
156            self.label_bits &= !(1 << bit);
157        }
158    }
159
160    /// Returns an iterator over the set label bits.
161    pub fn label_bits_iter(&self) -> impl Iterator<Item = u8> + '_ {
162        (0..64).filter(|&bit| self.has_label_bit(bit))
163    }
164}
165
166/// Flags for a node record.
167#[repr(transparent)]
168#[derive(Debug, Clone, Copy, Default)]
169pub struct NodeFlags(pub u16);
170
171impl NodeFlags {
172    /// Checks if a flag is set.
173    #[must_use]
174    pub const fn contains(&self, flag: u16) -> bool {
175        (self.0 & flag) != 0
176    }
177
178    /// Sets a flag.
179    pub fn set(&mut self, flag: u16) {
180        self.0 |= flag;
181    }
182
183    /// Clears a flag.
184    pub fn clear(&mut self, flag: u16) {
185        self.0 &= !flag;
186    }
187}
188
189#[cfg(test)]
190mod tests {
191    use super::*;
192
193    #[test]
194    fn test_node_record_size() {
195        // Ensure NodeRecord is exactly 32 bytes
196        assert_eq!(std::mem::size_of::<NodeRecord>(), 32);
197    }
198
199    #[test]
200    fn test_node_labels() {
201        let mut node = Node::new(NodeId::new(1));
202
203        node.add_label("Person");
204        assert!(node.has_label("Person"));
205        assert!(!node.has_label("Animal"));
206
207        node.add_label("Employee");
208        assert!(node.has_label("Employee"));
209
210        // Adding same label again should be idempotent
211        node.add_label("Person");
212        assert_eq!(node.labels.len(), 2);
213
214        // Remove label
215        assert!(node.remove_label("Person"));
216        assert!(!node.has_label("Person"));
217        assert!(!node.remove_label("NotExists"));
218    }
219
220    #[test]
221    fn test_node_properties() {
222        let mut node = Node::new(NodeId::new(1));
223
224        node.set_property("name", "Alice");
225        node.set_property("age", 30i64);
226
227        assert_eq!(node.get_property("name").and_then(|v| v.as_str()), Some("Alice"));
228        assert_eq!(node.get_property("age").and_then(|v| v.as_int64()), Some(30));
229        assert!(node.get_property("missing").is_none());
230
231        let removed = node.remove_property("name");
232        assert!(removed.is_some());
233        assert!(node.get_property("name").is_none());
234    }
235
236    #[test]
237    fn test_node_record_flags() {
238        let mut record = NodeRecord::new(NodeId::new(1), EpochId::INITIAL);
239
240        assert!(!record.is_deleted());
241        record.set_deleted(true);
242        assert!(record.is_deleted());
243        record.set_deleted(false);
244        assert!(!record.is_deleted());
245    }
246
247    #[test]
248    fn test_node_record_label_bits() {
249        let mut record = NodeRecord::new(NodeId::new(1), EpochId::INITIAL);
250
251        assert!(!record.has_label_bit(0));
252        record.set_label_bit(0);
253        assert!(record.has_label_bit(0));
254
255        record.set_label_bit(5);
256        record.set_label_bit(63);
257        assert!(record.has_label_bit(5));
258        assert!(record.has_label_bit(63));
259
260        let bits: Vec<_> = record.label_bits_iter().collect();
261        assert_eq!(bits, vec![0, 5, 63]);
262
263        record.clear_label_bit(5);
264        assert!(!record.has_label_bit(5));
265    }
266}