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