Skip to main content

graphmind/graph/
node.rs

1//! # Node Implementation -- Multi-Label Vertices with MVCC Versioning
2//!
3//! A [`Node`] is the fundamental entity in the property graph. Unlike a row in a
4//! relational database -- which belongs to exactly one table -- a graph node can
5//! carry **multiple labels** simultaneously (e.g., a node can be both `:Person`
6//! and `:Employee`). This is stored as a `HashSet<Label>`, giving O(1)
7//! membership checks when the query engine filters by label.
8//!
9//! ## Schema-free properties
10//!
11//! Each node owns a [`PropertyMap`] (`HashMap<String, PropertyValue>`) that acts
12//! as a schema-free attribute store. Any node can have any set of properties
13//! regardless of its labels -- there is no rigid column schema to satisfy. This
14//! flexibility is one of the key advantages of graph databases over RDBMS for
15//! heterogeneous, evolving data models.
16//!
17//! ## MVCC (Multi-Version Concurrency Control)
18//!
19//! Each node carries a `version: u64` field that is incremented on mutation.
20//! In the storage layer ([`GraphStore`](super::store::GraphStore)), nodes are
21//! stored in a `Vec<Vec<Node>>` arena where the inner `Vec` holds successive
22//! versions of the same node. This enables **snapshot isolation**: a reader
23//! operating at version V sees only node versions <= V, and is never blocked by
24//! a concurrent writer creating version V+1. MVCC is the same concurrency
25//! strategy used by PostgreSQL, Oracle, and most modern databases.
26//!
27//! ## Identity semantics
28//!
29//! `PartialEq` and `Hash` are implemented on `id` alone (not properties or
30//! labels). Two `Node` values with the same `NodeId` are considered equal
31//! regardless of their other fields -- this is consistent with graph database
32//! semantics where identity is determined by the node's unique identifier.
33//!
34//! ## Requirements coverage
35//!
36//! - REQ-GRAPH-002: Nodes with labels
37//! - REQ-GRAPH-004: Properties on nodes
38//! - REQ-GRAPH-006: Multiple labels per node
39
40use super::property::{PropertyMap, PropertyValue};
41use super::types::{Label, NodeId};
42use serde::{Deserialize, Serialize};
43use std::collections::HashSet;
44
45/// A node in the property graph
46///
47/// Nodes can have:
48/// - A unique ID
49/// - Multiple labels (REQ-GRAPH-006)
50/// - Properties (key-value pairs)
51/// - Creation and update timestamps
52#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct Node {
54    /// Unique identifier for this node
55    pub id: NodeId,
56
57    /// Version for MVCC
58    pub version: u64,
59
60    /// Set of labels for this node (supports multiple labels)
61    pub labels: HashSet<Label>,
62
63    /// Properties associated with this node
64    pub properties: PropertyMap,
65
66    /// Creation timestamp (Unix milliseconds)
67    pub created_at: i64,
68
69    /// Last update timestamp (Unix milliseconds)
70    pub updated_at: i64,
71}
72
73impl Node {
74    /// Create a new node with a single label
75    pub fn new(id: NodeId, label: impl Into<Label>) -> Self {
76        let now = chrono::Utc::now().timestamp_millis();
77        let mut labels = HashSet::new();
78        labels.insert(label.into());
79
80        Node {
81            id,
82            version: 1,
83            labels,
84            properties: PropertyMap::new(),
85            created_at: now,
86            updated_at: now,
87        }
88    }
89
90    /// Create a new node with multiple labels (REQ-GRAPH-006)
91    pub fn new_with_labels(id: NodeId, labels: Vec<Label>) -> Self {
92        let now = chrono::Utc::now().timestamp_millis();
93        let label_set: HashSet<Label> = labels.into_iter().collect();
94
95        Node {
96            id,
97            version: 1,
98            labels: label_set,
99            properties: PropertyMap::new(),
100            created_at: now,
101            updated_at: now,
102        }
103    }
104
105    /// Create a new node with labels and properties
106    pub fn new_with_properties(id: NodeId, labels: Vec<Label>, properties: PropertyMap) -> Self {
107        let now = chrono::Utc::now().timestamp_millis();
108        let label_set: HashSet<Label> = labels.into_iter().collect();
109
110        Node {
111            id,
112            version: 1,
113            labels: label_set,
114            properties,
115            created_at: now,
116            updated_at: now,
117        }
118    }
119
120    /// Add a label to this node
121    pub fn add_label(&mut self, label: impl Into<Label>) {
122        self.labels.insert(label.into());
123        self.update_timestamp();
124    }
125
126    /// Remove a label from this node
127    pub fn remove_label(&mut self, label: &Label) -> bool {
128        let removed = self.labels.remove(label);
129        if removed {
130            self.update_timestamp();
131        }
132        removed
133    }
134
135    /// Check if node has a specific label
136    pub fn has_label(&self, label: &Label) -> bool {
137        self.labels.contains(label)
138    }
139
140    /// Get all labels
141    pub fn get_labels(&self) -> Vec<&Label> {
142        self.labels.iter().collect()
143    }
144
145    /// Set a property value
146    pub fn set_property(
147        &mut self,
148        key: impl Into<String>,
149        value: impl Into<PropertyValue>,
150    ) -> Option<PropertyValue> {
151        let old = self.properties.insert(key.into(), value.into());
152        self.update_timestamp();
153        old
154    }
155
156    /// Get a property value
157    pub fn get_property(&self, key: &str) -> Option<&PropertyValue> {
158        self.properties.get(key)
159    }
160
161    /// Remove a property
162    pub fn remove_property(&mut self, key: &str) -> Option<PropertyValue> {
163        let removed = self.properties.remove(key);
164        if removed.is_some() {
165            self.update_timestamp();
166        }
167        removed
168    }
169
170    /// Check if property exists
171    pub fn has_property(&self, key: &str) -> bool {
172        self.properties.contains_key(key)
173    }
174
175    /// Update the modification timestamp
176    fn update_timestamp(&mut self) {
177        self.updated_at = chrono::Utc::now().timestamp_millis();
178    }
179
180    /// Get number of properties
181    pub fn property_count(&self) -> usize {
182        self.properties.len()
183    }
184
185    /// Get number of labels
186    pub fn label_count(&self) -> usize {
187        self.labels.len()
188    }
189}
190
191// Add chrono dependency
192mod chrono {
193    pub struct Utc;
194    impl Utc {
195        pub fn now() -> DateTime {
196            DateTime
197        }
198    }
199    pub struct DateTime;
200    impl DateTime {
201        pub fn timestamp_millis(&self) -> i64 {
202            // Use system time for now
203            std::time::SystemTime::now()
204                .duration_since(std::time::UNIX_EPOCH)
205                .unwrap()
206                .as_millis() as i64
207        }
208    }
209}
210
211impl PartialEq for Node {
212    fn eq(&self, other: &Self) -> bool {
213        self.id == other.id
214    }
215}
216
217impl Eq for Node {}
218
219impl std::hash::Hash for Node {
220    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
221        self.id.hash(state);
222    }
223}
224
225#[cfg(test)]
226mod tests {
227    use super::*;
228
229    #[test]
230    fn test_create_node_single_label() {
231        // REQ-GRAPH-002: Nodes with labels
232        let node = Node::new(NodeId::new(1), "Person");
233        assert_eq!(node.id, NodeId::new(1));
234        assert_eq!(node.labels.len(), 1);
235        assert!(node.has_label(&Label::new("Person")));
236    }
237
238    #[test]
239    fn test_create_node_multiple_labels() {
240        // REQ-GRAPH-006: Multiple labels per node
241        let labels = vec![Label::new("Person"), Label::new("Employee")];
242        let node = Node::new_with_labels(NodeId::new(2), labels);
243
244        assert_eq!(node.label_count(), 2);
245        assert!(node.has_label(&Label::new("Person")));
246        assert!(node.has_label(&Label::new("Employee")));
247    }
248
249    #[test]
250    fn test_add_remove_labels() {
251        let mut node = Node::new(NodeId::new(3), "Person");
252
253        // Add label
254        node.add_label("Employee");
255        assert_eq!(node.label_count(), 2);
256        assert!(node.has_label(&Label::new("Employee")));
257
258        // Remove label
259        let removed = node.remove_label(&Label::new("Person"));
260        assert!(removed);
261        assert_eq!(node.label_count(), 1);
262        assert!(!node.has_label(&Label::new("Person")));
263    }
264
265    #[test]
266    fn test_node_properties() {
267        // REQ-GRAPH-004: Properties on nodes
268        let mut node = Node::new(NodeId::new(4), "Person");
269
270        // Set properties
271        node.set_property("name", "Alice");
272        node.set_property("age", 30i64);
273        node.set_property("active", true);
274
275        // Get properties
276        assert_eq!(
277            node.get_property("name").unwrap().as_string(),
278            Some("Alice")
279        );
280        assert_eq!(node.get_property("age").unwrap().as_integer(), Some(30));
281        assert_eq!(
282            node.get_property("active").unwrap().as_boolean(),
283            Some(true)
284        );
285        assert_eq!(node.property_count(), 3);
286
287        // Remove property
288        let removed = node.remove_property("age");
289        assert!(removed.is_some());
290        assert_eq!(node.property_count(), 2);
291        assert!(!node.has_property("age"));
292    }
293
294    #[test]
295    fn test_node_with_properties() {
296        // REQ-GRAPH-005: Multiple data types
297        let mut props = PropertyMap::new();
298        props.insert("name".to_string(), "Bob".into());
299        props.insert("age".to_string(), 25i64.into());
300        props.insert("score".to_string(), 95.5.into());
301
302        let node = Node::new_with_properties(NodeId::new(5), vec![Label::new("Student")], props);
303
304        assert_eq!(node.property_count(), 3);
305        assert_eq!(node.get_property("name").unwrap().as_string(), Some("Bob"));
306        assert_eq!(node.get_property("age").unwrap().as_integer(), Some(25));
307        assert_eq!(node.get_property("score").unwrap().as_float(), Some(95.5));
308    }
309
310    #[test]
311    fn test_node_timestamps() {
312        let node = Node::new(NodeId::new(6), "Test");
313        assert!(node.created_at > 0);
314        assert_eq!(node.created_at, node.updated_at);
315
316        std::thread::sleep(std::time::Duration::from_millis(10));
317        let mut node2 = node.clone();
318        node2.set_property("key", "value");
319
320        assert!(node2.updated_at > node.updated_at);
321    }
322
323    #[test]
324    fn test_node_equality() {
325        let node1 = Node::new(NodeId::new(7), "Person");
326        let node2 = Node::new(NodeId::new(7), "Person");
327        let node3 = Node::new(NodeId::new(8), "Person");
328
329        assert_eq!(node1, node2); // Same ID
330        assert_ne!(node1, node3); // Different ID
331    }
332}