Skip to main content

grafeo_core/graph/lpg/
node.rs

1//! Node types for the LPG model.
2//!
3//! Two representations here: [`Node`] is the friendly one with all the data,
4//! [`NodeRecord`] is the compact 32-byte struct for storage.
5
6use std::collections::BTreeMap;
7
8use arcstr::ArcStr;
9use grafeo_common::types::{EpochId, NodeId, PropertyKey, Value};
10use serde::{Deserialize, Serialize};
11use smallvec::SmallVec;
12
13/// A node with its labels and properties fully loaded.
14///
15/// This is what you get back from [`LpgStore::get_node()`](super::LpgStore::get_node).
16/// For bulk operations, the store works with [`NodeRecord`] internally.
17///
18/// # Example
19///
20/// ```
21/// use grafeo_core::graph::lpg::Node;
22/// use grafeo_common::types::NodeId;
23///
24/// let mut person = Node::new(NodeId::new(1));
25/// person.add_label("Person");
26/// person.set_property("name", "Alice");
27/// person.set_property("age", 30i64);
28///
29/// assert!(person.has_label("Person"));
30/// ```
31#[derive(Debug, Clone)]
32pub struct Node {
33    /// Unique identifier.
34    pub id: NodeId,
35    /// Labels attached to this node (inline storage for 1-2 labels).
36    pub labels: SmallVec<[ArcStr; 2]>,
37    /// Properties stored on this node.
38    pub properties: BTreeMap<PropertyKey, Value>,
39}
40
41impl Node {
42    /// Creates a new node with the given ID.
43    #[must_use]
44    pub fn new(id: NodeId) -> Self {
45        Self {
46            id,
47            labels: SmallVec::new(),
48            properties: BTreeMap::new(),
49        }
50    }
51
52    /// Creates a new node with labels.
53    #[must_use]
54    pub fn with_labels(id: NodeId, labels: impl IntoIterator<Item = impl Into<ArcStr>>) -> Self {
55        Self {
56            id,
57            labels: labels.into_iter().map(Into::into).collect(),
58            properties: BTreeMap::new(),
59        }
60    }
61
62    /// Adds a label to this node.
63    pub fn add_label(&mut self, label: impl Into<ArcStr>) {
64        let label = label.into();
65        if !self.labels.iter().any(|l| l.as_str() == label.as_str()) {
66            self.labels.push(label);
67        }
68    }
69
70    /// Removes a label from this node.
71    pub fn remove_label(&mut self, label: &str) -> bool {
72        if let Some(pos) = self.labels.iter().position(|l| l.as_str() == label) {
73            self.labels.remove(pos);
74            true
75        } else {
76            false
77        }
78    }
79
80    /// Checks if this node has the given label.
81    #[must_use]
82    pub fn has_label(&self, label: &str) -> bool {
83        self.labels.iter().any(|l| l.as_str() == label)
84    }
85
86    /// Sets a property on this node.
87    pub fn set_property(&mut self, key: impl Into<PropertyKey>, value: impl Into<Value>) {
88        self.properties.insert(key.into(), value.into());
89    }
90
91    /// Gets a property from this node.
92    #[must_use]
93    pub fn get_property(&self, key: &str) -> Option<&Value> {
94        self.properties.get(&PropertyKey::new(key))
95    }
96
97    /// Removes a property from this node.
98    pub fn remove_property(&mut self, key: &str) -> Option<Value> {
99        self.properties.remove(&PropertyKey::new(key))
100    }
101}
102
103/// The compact storage format for a node - exactly 32 bytes.
104///
105/// You won't interact with this directly most of the time. It's what lives
106/// in memory for each node, with properties and labels stored separately.
107/// The 32-byte size means two records fit in a cache line.
108///
109/// Fields are ordered to minimize padding: u64s first, then u32, then u16s.
110#[repr(C)]
111#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
112pub struct NodeRecord {
113    /// Unique node identifier.
114    pub id: NodeId,
115    /// Epoch this record was created in.
116    pub epoch: EpochId,
117    /// Offset into the property arena.
118    pub props_offset: u32,
119    /// Number of labels on this node (labels stored externally).
120    pub label_count: u16,
121    /// Reserved for future use / alignment.
122    pub _reserved: u16,
123    /// Number of properties.
124    pub props_count: u16,
125    /// Flags (deleted, has_version, etc.).
126    pub flags: NodeFlags,
127    /// Padding to maintain 32-byte size.
128    pub _padding: u32,
129}
130
131impl NodeRecord {
132    /// Flag indicating the node is deleted.
133    pub const FLAG_DELETED: u16 = 1 << 0;
134    /// Flag indicating the node has version history.
135    pub const FLAG_HAS_VERSION: u16 = 1 << 1;
136
137    /// Creates a new node record.
138    #[must_use]
139    pub const fn new(id: NodeId, epoch: EpochId) -> Self {
140        Self {
141            id,
142            label_count: 0,
143            _reserved: 0,
144            props_offset: 0,
145            props_count: 0,
146            flags: NodeFlags(0),
147            epoch,
148            _padding: 0,
149        }
150    }
151
152    /// Checks if this node is deleted.
153    #[must_use]
154    pub const fn is_deleted(&self) -> bool {
155        self.flags.contains(Self::FLAG_DELETED)
156    }
157
158    /// Marks this node as deleted.
159    pub fn set_deleted(&mut self, deleted: bool) {
160        if deleted {
161            self.flags.set(Self::FLAG_DELETED);
162        } else {
163            self.flags.clear(Self::FLAG_DELETED);
164        }
165    }
166
167    /// Returns the number of labels on this node.
168    #[must_use]
169    pub const fn label_count(&self) -> u16 {
170        self.label_count
171    }
172
173    /// Sets the label count.
174    pub fn set_label_count(&mut self, count: u16) {
175        self.label_count = count;
176    }
177}
178
179/// Bit flags packed into a node record.
180///
181/// Check flags with [`contains()`](Self::contains), set with [`set()`](Self::set).
182#[repr(transparent)]
183#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
184pub struct NodeFlags(pub u16);
185
186impl NodeFlags {
187    /// Checks if a flag is set.
188    #[must_use]
189    pub const fn contains(&self, flag: u16) -> bool {
190        (self.0 & flag) != 0
191    }
192
193    /// Sets a flag.
194    pub fn set(&mut self, flag: u16) {
195        self.0 |= flag;
196    }
197
198    /// Clears a flag.
199    pub fn clear(&mut self, flag: u16) {
200        self.0 &= !flag;
201    }
202}
203
204#[cfg(test)]
205mod tests {
206    use super::*;
207
208    #[test]
209    fn test_node_record_size() {
210        // Ensure NodeRecord is exactly 32 bytes
211        assert_eq!(std::mem::size_of::<NodeRecord>(), 32);
212    }
213
214    #[test]
215    fn test_node_labels() {
216        let mut node = Node::new(NodeId::new(1));
217
218        node.add_label("Person");
219        assert!(node.has_label("Person"));
220        assert!(!node.has_label("Animal"));
221
222        node.add_label("Employee");
223        assert!(node.has_label("Employee"));
224
225        // Adding same label again should be idempotent
226        node.add_label("Person");
227        assert_eq!(node.labels.len(), 2);
228
229        // Remove label
230        assert!(node.remove_label("Person"));
231        assert!(!node.has_label("Person"));
232        assert!(!node.remove_label("NotExists"));
233    }
234
235    #[test]
236    fn test_node_properties() {
237        let mut node = Node::new(NodeId::new(1));
238
239        node.set_property("name", "Alice");
240        node.set_property("age", 30i64);
241
242        assert_eq!(
243            node.get_property("name").and_then(|v| v.as_str()),
244            Some("Alice")
245        );
246        assert_eq!(
247            node.get_property("age").and_then(|v| v.as_int64()),
248            Some(30)
249        );
250        assert!(node.get_property("missing").is_none());
251
252        let removed = node.remove_property("name");
253        assert!(removed.is_some());
254        assert!(node.get_property("name").is_none());
255    }
256
257    #[test]
258    fn test_node_record_flags() {
259        let mut record = NodeRecord::new(NodeId::new(1), EpochId::INITIAL);
260
261        assert!(!record.is_deleted());
262        record.set_deleted(true);
263        assert!(record.is_deleted());
264        record.set_deleted(false);
265        assert!(!record.is_deleted());
266    }
267
268    #[test]
269    fn test_node_record_label_count() {
270        let mut record = NodeRecord::new(NodeId::new(1), EpochId::INITIAL);
271
272        assert_eq!(record.label_count(), 0);
273        record.set_label_count(5);
274        assert_eq!(record.label_count(), 5);
275
276        // Can handle large label counts (no 64 limit)
277        record.set_label_count(1000);
278        assert_eq!(record.label_count(), 1000);
279    }
280}