Skip to main content

graphmind/query/executor/
record.rs

1//! # Records: The Data Unit of Query Execution
2//!
3//! A [`Record`] is a set of **variable bindings** -- a mapping from variable names (like
4//! `n`, `r`, `m` in `MATCH (n)-[r]->(m)`) to [`Value`]s. It is the graph-database analog
5//! of a "row" in a relational database, except that columns are named variables rather
6//! than positional indices. Records flow through the Volcano iterator pipeline one at a
7//! time, accumulating bindings as they pass through operators (e.g., `ExpandOperator` adds
8//! the target node binding to an existing record that already contains the source node).
9//!
10//! ## Late Materialization (ADR-012)
11//!
12//! The most important optimization in this module is **late materialization**. Instead of
13//! cloning an entire `Node` (with all its properties, labels, and metadata) when a scan
14//! produces a result, we store only a `Value::NodeRef(id)` -- a 64-bit integer. The full
15//! node data is resolved **on demand** via [`Value::resolve_property(prop, store)`] only
16//! when a property is actually needed (e.g., in a WHERE filter or RETURN projection).
17//!
18//! This matters enormously for traversal queries. Consider `MATCH (a)-[:KNOWS]->(b)-[:KNOWS]->(c)`:
19//! the ExpandOperator traverses through `b` nodes, but if the query only returns `c.name`,
20//! the `b` nodes never need their properties loaded. Late materialization turns what would
21//! be O(n * avg_properties) memory into O(n * 8 bytes).
22//!
23//! ## Semantic Equality: `NodeRef(id) == Node(id, _)`
24//!
25//! The [`Value`] enum has both lazy (`NodeRef`) and materialized (`Node`) variants for the
26//! same logical entity. This creates a subtle correctness requirement: the `JoinOperator`
27//! uses hash-based lookups to match records from two sides of a join. If the left side
28//! produces `NodeRef(42)` and the right side produces `Node(42, <data>)`, they must be
29//! considered **equal** and must produce the **same hash** -- otherwise the join silently
30//! drops valid matches.
31//!
32//! This is why [`PartialEq`] and [`Hash`] are implemented **manually** instead of derived.
33//! The derive macro would compare all fields (including the `Node` data), breaking the
34//! semantic equivalence. The manual implementation compares only the identity (the `NodeId`),
35//! and the hash function uses a discriminant tag (0 for nodes, 1 for edges) plus the ID,
36//! ensuring the **hash consistency invariant**: if `a == b`, then `hash(a) == hash(b)`.
37//!
38//! [`RecordBatch`] is the final output container -- a vector of [`Record`]s plus column
39//! names, returned to the caller after query execution completes.
40
41use crate::graph::{Edge, EdgeId, EdgeType, GraphStore, Node, NodeId, PropertyValue};
42use std::collections::HashMap;
43use std::hash::{Hash, Hasher};
44
45/// A single record flowing through the query pipeline
46#[derive(Debug, Clone)]
47pub struct Record {
48    /// Variable bindings (variable name -> value)
49    bindings: HashMap<String, Value>,
50}
51
52/// Value types that can be bound to variables in a query record.
53///
54/// The key design choice here is the **late materialization hierarchy**:
55///
56/// - **`NodeRef(id)`** -- a lazy reference. Stores only the 64-bit `NodeId`. Produced by
57///   scan and expand operators. Extremely cheap to create (no heap allocation, no cloning).
58///   Properties are resolved on demand via `resolve_property(prop, store)`.
59///
60/// - **`Node(id, node)`** -- a fully materialized node. Contains a clone of the `Node`
61///   struct with all labels and properties. Produced by `ProjectOperator` when the RETURN
62///   clause requests `RETURN n` (the entire node), triggering full materialization.
63///
64/// The same lazy/eager split exists for edges: `EdgeRef(id, src, tgt, type)` carries the
65/// structural data (endpoints and type) without property clones, while `Edge(id, edge)`
66/// is fully materialized.
67///
68/// `Property(PropertyValue)` wraps scalar values (strings, integers, floats, booleans,
69/// datetimes, arrays, maps) that result from property access (`n.name`) or literal
70/// expressions. `Path` stores ordered sequences of node/edge IDs for named path patterns
71/// like `p = (a)-[]->(b)`. `Null` represents the absence of a value, following Cypher's
72/// three-valued logic (true/false/null).
73#[derive(Debug, Clone)]
74pub enum Value {
75    /// A fully materialized node
76    Node(NodeId, Node),
77    /// A lazy node reference (no property clone)
78    NodeRef(NodeId),
79    /// A fully materialized edge
80    Edge(EdgeId, Edge),
81    /// A lazy edge reference (structural data only, no property clone)
82    EdgeRef(EdgeId, NodeId, NodeId, EdgeType),
83    /// A property value
84    Property(PropertyValue),
85    /// A path (ordered sequence of node/edge IDs)
86    Path {
87        nodes: Vec<NodeId>,
88        edges: Vec<EdgeId>,
89    },
90    /// Null
91    Null,
92}
93
94// NodeRef(id) == Node(id, _) — compare by ID only for nodes/edges
95impl PartialEq for Value {
96    fn eq(&self, other: &Self) -> bool {
97        match (self, other) {
98            // Node variants compare by ID
99            (Value::Node(id1, _), Value::Node(id2, _)) => id1 == id2,
100            (Value::NodeRef(id1), Value::NodeRef(id2)) => id1 == id2,
101            (Value::Node(id1, _), Value::NodeRef(id2))
102            | (Value::NodeRef(id2), Value::Node(id1, _)) => id1 == id2,
103            // Edge variants compare by ID
104            (Value::Edge(id1, _), Value::Edge(id2, _)) => id1 == id2,
105            (Value::EdgeRef(id1, ..), Value::EdgeRef(id2, ..)) => id1 == id2,
106            (Value::Edge(id1, _), Value::EdgeRef(id2, ..))
107            | (Value::EdgeRef(id2, ..), Value::Edge(id1, _)) => id1 == id2,
108            // Property and Null
109            (Value::Property(p1), Value::Property(p2)) => p1 == p2,
110            // Path
111            (
112                Value::Path {
113                    nodes: n1,
114                    edges: e1,
115                },
116                Value::Path {
117                    nodes: n2,
118                    edges: e2,
119                },
120            ) => n1 == n2 && e1 == e2,
121            (Value::Null, Value::Null) => true,
122            _ => false,
123        }
124    }
125}
126
127impl Eq for Value {}
128
129impl Hash for Value {
130    fn hash<H: Hasher>(&self, state: &mut H) {
131        // Use semantic tags so NodeRef and Node hash the same
132        match self {
133            Value::Node(id, _) | Value::NodeRef(id) => {
134                0u8.hash(state);
135                id.hash(state);
136            }
137            Value::Edge(id, _) | Value::EdgeRef(id, ..) => {
138                1u8.hash(state);
139                id.hash(state);
140            }
141            Value::Property(p) => {
142                2u8.hash(state);
143                p.hash(state);
144            }
145            Value::Path { nodes, edges } => {
146                3u8.hash(state);
147                nodes.hash(state);
148                edges.hash(state);
149            }
150            Value::Null => {
151                4u8.hash(state);
152            }
153        }
154    }
155}
156
157impl Record {
158    /// Create a new empty record
159    pub fn new() -> Self {
160        Self {
161            bindings: HashMap::new(),
162        }
163    }
164
165    /// Bind a variable to a value
166    pub fn bind(&mut self, variable: String, value: Value) {
167        self.bindings.insert(variable, value);
168    }
169
170    /// Get a bound value
171    pub fn get(&self, variable: &str) -> Option<&Value> {
172        self.bindings.get(variable)
173    }
174
175    /// Get all bindings
176    pub fn bindings(&self) -> &HashMap<String, Value> {
177        &self.bindings
178    }
179
180    /// Check if a variable is bound
181    pub fn has(&self, variable: &str) -> bool {
182        self.bindings.contains_key(variable)
183    }
184
185    /// Merge another record into this one
186    pub fn merge(&mut self, other: Record) {
187        self.bindings.extend(other.bindings);
188    }
189
190    /// Clone with only specified variables
191    pub fn project(&self, variables: &[String]) -> Record {
192        let mut new_record = Record::new();
193        for var in variables {
194            if let Some(value) = self.bindings.get(var) {
195                new_record.bind(var.clone(), value.clone());
196            }
197        }
198        new_record
199    }
200}
201
202impl Default for Record {
203    fn default() -> Self {
204        Self::new()
205    }
206}
207
208impl Value {
209    /// Get as node if this is a fully materialized node value
210    pub fn as_node(&self) -> Option<(NodeId, &Node)> {
211        match self {
212            Value::Node(id, node) => Some((*id, node)),
213            _ => None,
214        }
215    }
216
217    /// Get as edge if this is a fully materialized edge value
218    pub fn as_edge(&self) -> Option<(EdgeId, &Edge)> {
219        match self {
220            Value::Edge(id, edge) => Some((*id, edge)),
221            _ => None,
222        }
223    }
224
225    /// Get as property if this is a property value
226    pub fn as_property(&self) -> Option<&PropertyValue> {
227        match self {
228            Value::Property(prop) => Some(prop),
229            _ => None,
230        }
231    }
232
233    /// Check if this is null
234    pub fn is_null(&self) -> bool {
235        matches!(self, Value::Null)
236    }
237
238    /// Extract NodeId from any node variant (Node or NodeRef)
239    pub fn node_id(&self) -> Option<NodeId> {
240        match self {
241            Value::Node(id, _) | Value::NodeRef(id) => Some(*id),
242            _ => None,
243        }
244    }
245
246    /// Extract EdgeId from any edge variant (Edge or EdgeRef)
247    pub fn edge_id(&self) -> Option<EdgeId> {
248        match self {
249            Value::Edge(id, _) => Some(*id),
250            Value::EdgeRef(id, ..) => Some(*id),
251            _ => None,
252        }
253    }
254
255    /// Extract edge endpoints from any edge variant
256    pub fn edge_endpoints(&self) -> Option<(NodeId, NodeId)> {
257        match self {
258            Value::Edge(_, edge) => Some((edge.source, edge.target)),
259            Value::EdgeRef(_, src, tgt, _) => Some((*src, *tgt)),
260            _ => None,
261        }
262    }
263
264    /// Extract edge type from any edge variant
265    pub fn edge_type(&self) -> Option<&EdgeType> {
266        match self {
267            Value::Edge(_, edge) => Some(&edge.edge_type),
268            Value::EdgeRef(_, _, _, et) => Some(et),
269            _ => None,
270        }
271    }
272
273    /// Check if this represents a node (Node or NodeRef)
274    pub fn is_node(&self) -> bool {
275        matches!(self, Value::Node(..) | Value::NodeRef(..))
276    }
277
278    /// Check if this represents an edge (Edge or EdgeRef)
279    pub fn is_edge(&self) -> bool {
280        matches!(self, Value::Edge(..) | Value::EdgeRef(..))
281    }
282
283    /// Materialize a NodeRef into a full Node by looking it up in the store.
284    /// Returns self unchanged if already materialized or not a node variant.
285    pub fn materialize_node(self, store: &GraphStore) -> Self {
286        match self {
287            Value::NodeRef(id) => {
288                if let Some(node) = store.get_node(id) {
289                    Value::Node(id, node.clone())
290                } else {
291                    Value::Null
292                }
293            }
294            other => other,
295        }
296    }
297
298    /// Materialize an EdgeRef into a full Edge by looking it up in the store.
299    /// Returns self unchanged if already materialized or not an edge variant.
300    pub fn materialize_edge(self, store: &GraphStore) -> Self {
301        match self {
302            Value::EdgeRef(id, ..) => {
303                if let Some(edge) = store.get_edge(id) {
304                    Value::Edge(id, edge.clone())
305                } else {
306                    Value::Null
307                }
308            }
309            other => other,
310        }
311    }
312
313    /// Resolve a property from this value, using columnar store first, then
314    /// falling back to materialized node/edge properties or store lookup for refs.
315    pub fn resolve_property(&self, property: &str, store: &GraphStore) -> PropertyValue {
316        match self {
317            Value::Node(id, node) => {
318                let prop = store
319                    .node_columns
320                    .get_property(id.as_u64() as usize, property);
321                if !prop.is_null() {
322                    prop
323                } else {
324                    node.get_property(property)
325                        .cloned()
326                        .unwrap_or(PropertyValue::Null)
327                }
328            }
329            Value::NodeRef(id) => {
330                let prop = store
331                    .node_columns
332                    .get_property(id.as_u64() as usize, property);
333                if !prop.is_null() {
334                    prop
335                } else if let Some(node) = store.get_node(*id) {
336                    node.get_property(property)
337                        .cloned()
338                        .unwrap_or(PropertyValue::Null)
339                } else {
340                    PropertyValue::Null
341                }
342            }
343            Value::Edge(id, edge) => {
344                let prop = store
345                    .edge_columns
346                    .get_property(id.as_u64() as usize, property);
347                if !prop.is_null() {
348                    prop
349                } else {
350                    edge.get_property(property)
351                        .cloned()
352                        .unwrap_or(PropertyValue::Null)
353                }
354            }
355            Value::EdgeRef(id, ..) => {
356                let prop = store
357                    .edge_columns
358                    .get_property(id.as_u64() as usize, property);
359                if !prop.is_null() {
360                    prop
361                } else if let Some(edge) = store.get_edge(*id) {
362                    edge.get_property(property)
363                        .cloned()
364                        .unwrap_or(PropertyValue::Null)
365                } else {
366                    PropertyValue::Null
367                }
368            }
369            _ => PropertyValue::Null,
370        }
371    }
372}
373
374/// A batch of records (result set)
375#[derive(Debug)]
376pub struct RecordBatch {
377    /// All records in the batch
378    pub records: Vec<Record>,
379    /// Column names for the result
380    pub columns: Vec<String>,
381}
382
383impl RecordBatch {
384    /// Create a new empty batch
385    pub fn new(columns: Vec<String>) -> Self {
386        Self {
387            records: Vec::new(),
388            columns,
389        }
390    }
391
392    /// Get number of records
393    pub fn len(&self) -> usize {
394        self.records.len()
395    }
396
397    /// Check if empty
398    pub fn is_empty(&self) -> bool {
399        self.records.is_empty()
400    }
401
402    /// Add a record
403    pub fn push(&mut self, record: Record) {
404        self.records.push(record);
405    }
406
407    /// Get a record by index
408    pub fn get(&self, index: usize) -> Option<&Record> {
409        self.records.get(index)
410    }
411}
412
413#[cfg(test)]
414mod tests {
415    use super::*;
416    use crate::graph::Label;
417
418    #[test]
419    fn test_record_creation() {
420        let record = Record::new();
421        assert_eq!(record.bindings().len(), 0);
422    }
423
424    #[test]
425    fn test_record_binding() {
426        let mut record = Record::new();
427        let node = Node::new(NodeId::new(1), Label::new("Person"));
428
429        record.bind("n".to_string(), Value::Node(NodeId::new(1), node));
430
431        assert!(record.has("n"));
432        assert!(record.get("n").is_some());
433    }
434
435    #[test]
436    fn test_record_merge() {
437        let mut record1 = Record::new();
438        let mut record2 = Record::new();
439
440        record1.bind("a".to_string(), Value::Property(PropertyValue::Integer(1)));
441        record2.bind("b".to_string(), Value::Property(PropertyValue::Integer(2)));
442
443        record1.merge(record2);
444
445        assert!(record1.has("a"));
446        assert!(record1.has("b"));
447    }
448
449    #[test]
450    fn test_record_project() {
451        let mut record = Record::new();
452        record.bind("a".to_string(), Value::Property(PropertyValue::Integer(1)));
453        record.bind("b".to_string(), Value::Property(PropertyValue::Integer(2)));
454        record.bind("c".to_string(), Value::Property(PropertyValue::Integer(3)));
455
456        let projected = record.project(&vec!["a".to_string(), "c".to_string()]);
457
458        assert!(projected.has("a"));
459        assert!(!projected.has("b"));
460        assert!(projected.has("c"));
461    }
462
463    #[test]
464    fn test_value_types() {
465        let node_val = Value::Node(
466            NodeId::new(1),
467            Node::new(NodeId::new(1), Label::new("Test")),
468        );
469        assert!(node_val.as_node().is_some());
470        assert!(node_val.as_edge().is_none());
471
472        let prop_val = Value::Property(PropertyValue::String("test".to_string()));
473        assert!(prop_val.as_property().is_some());
474
475        let null_val = Value::Null;
476        assert!(null_val.is_null());
477    }
478
479    #[test]
480    fn test_record_batch() {
481        let mut batch = RecordBatch::new(vec!["n".to_string(), "m".to_string()]);
482        assert_eq!(batch.len(), 0);
483        assert!(batch.is_empty());
484
485        batch.push(Record::new());
486        assert_eq!(batch.len(), 1);
487        assert!(!batch.is_empty());
488    }
489
490    // ========== Batch 5: Additional Record Tests ==========
491
492    #[test]
493    fn test_as_edge() {
494        let edge = crate::graph::Edge::new(
495            EdgeId::new(1),
496            NodeId::new(10),
497            NodeId::new(20),
498            crate::graph::EdgeType::new("KNOWS"),
499        );
500        let val = Value::Edge(EdgeId::new(1), edge);
501        let (eid, e) = val.as_edge().unwrap();
502        assert_eq!(eid, EdgeId::new(1));
503        assert_eq!(e.source, NodeId::new(10));
504        assert_eq!(e.target, NodeId::new(20));
505
506        // Non-edge variants return None
507        assert!(Value::Null.as_edge().is_none());
508        assert!(Value::NodeRef(NodeId::new(1)).as_edge().is_none());
509    }
510
511    #[test]
512    fn test_node_id() {
513        // From Node
514        let node = Node::new(NodeId::new(5), Label::new("Person"));
515        let val = Value::Node(NodeId::new(5), node);
516        assert_eq!(val.node_id(), Some(NodeId::new(5)));
517
518        // From NodeRef
519        let val = Value::NodeRef(NodeId::new(7));
520        assert_eq!(val.node_id(), Some(NodeId::new(7)));
521
522        // Non-node variants
523        assert!(Value::Null.node_id().is_none());
524        assert!(Value::Property(PropertyValue::Integer(42))
525            .node_id()
526            .is_none());
527    }
528
529    #[test]
530    fn test_edge_id() {
531        // From Edge
532        let edge = crate::graph::Edge::new(
533            EdgeId::new(3),
534            NodeId::new(1),
535            NodeId::new(2),
536            crate::graph::EdgeType::new("E"),
537        );
538        let val = Value::Edge(EdgeId::new(3), edge);
539        assert_eq!(val.edge_id(), Some(EdgeId::new(3)));
540
541        // From EdgeRef
542        let val = Value::EdgeRef(
543            EdgeId::new(4),
544            NodeId::new(1),
545            NodeId::new(2),
546            crate::graph::EdgeType::new("E"),
547        );
548        assert_eq!(val.edge_id(), Some(EdgeId::new(4)));
549
550        // Non-edge
551        assert!(Value::Null.edge_id().is_none());
552    }
553
554    #[test]
555    fn test_edge_endpoints() {
556        // From Edge
557        let edge = crate::graph::Edge::new(
558            EdgeId::new(1),
559            NodeId::new(10),
560            NodeId::new(20),
561            crate::graph::EdgeType::new("E"),
562        );
563        let val = Value::Edge(EdgeId::new(1), edge);
564        assert_eq!(
565            val.edge_endpoints(),
566            Some((NodeId::new(10), NodeId::new(20)))
567        );
568
569        // From EdgeRef
570        let val = Value::EdgeRef(
571            EdgeId::new(1),
572            NodeId::new(30),
573            NodeId::new(40),
574            crate::graph::EdgeType::new("E"),
575        );
576        assert_eq!(
577            val.edge_endpoints(),
578            Some((NodeId::new(30), NodeId::new(40)))
579        );
580
581        // Non-edge
582        assert!(Value::Null.edge_endpoints().is_none());
583    }
584
585    #[test]
586    fn test_edge_type_accessor() {
587        let edge = crate::graph::Edge::new(
588            EdgeId::new(1),
589            NodeId::new(1),
590            NodeId::new(2),
591            crate::graph::EdgeType::new("KNOWS"),
592        );
593        let val = Value::Edge(EdgeId::new(1), edge);
594        assert_eq!(val.edge_type().unwrap().as_str(), "KNOWS");
595
596        let val = Value::EdgeRef(
597            EdgeId::new(1),
598            NodeId::new(1),
599            NodeId::new(2),
600            crate::graph::EdgeType::new("LIKES"),
601        );
602        assert_eq!(val.edge_type().unwrap().as_str(), "LIKES");
603
604        assert!(Value::Null.edge_type().is_none());
605    }
606
607    #[test]
608    fn test_is_node_is_edge() {
609        let node = Node::new(NodeId::new(1), Label::new("A"));
610        assert!(Value::Node(NodeId::new(1), node).is_node());
611        assert!(Value::NodeRef(NodeId::new(1)).is_node());
612        assert!(!Value::Null.is_node());
613        assert!(!Value::Property(PropertyValue::Integer(1)).is_node());
614
615        let edge = crate::graph::Edge::new(
616            EdgeId::new(1),
617            NodeId::new(1),
618            NodeId::new(2),
619            crate::graph::EdgeType::new("E"),
620        );
621        assert!(Value::Edge(EdgeId::new(1), edge).is_edge());
622        assert!(Value::EdgeRef(
623            EdgeId::new(1),
624            NodeId::new(1),
625            NodeId::new(2),
626            crate::graph::EdgeType::new("E"),
627        )
628        .is_edge());
629        assert!(!Value::Null.is_edge());
630    }
631
632    #[test]
633    fn test_materialize_node() {
634        let mut store = GraphStore::new();
635        let id = store.create_node("Person");
636        store.get_node_mut(id).unwrap().set_property(
637            "name".to_string(),
638            PropertyValue::String("Alice".to_string()),
639        );
640
641        // NodeRef materializes to Node
642        let val = Value::NodeRef(id).materialize_node(&store);
643        match &val {
644            Value::Node(nid, node) => {
645                assert_eq!(*nid, id);
646                assert!(node.labels.contains(&Label::new("Person")));
647            }
648            _ => panic!("Expected Value::Node after materialization"),
649        }
650
651        // Already materialized stays the same
652        let node = store.get_node(id).unwrap().clone();
653        let val = Value::Node(id, node).materialize_node(&store);
654        assert!(matches!(val, Value::Node(..)));
655
656        // Non-existent NodeRef becomes Null
657        let val = Value::NodeRef(NodeId::new(9999)).materialize_node(&store);
658        assert!(val.is_null());
659
660        // Non-node value is returned unchanged
661        let val = Value::Property(PropertyValue::Integer(42)).materialize_node(&store);
662        assert!(matches!(val, Value::Property(..)));
663    }
664
665    #[test]
666    fn test_materialize_edge() {
667        let mut store = GraphStore::new();
668        let a = store.create_node("A");
669        let b = store.create_node("B");
670        let eid = store.create_edge(a, b, "KNOWS").unwrap();
671
672        // EdgeRef materializes to Edge
673        let val = Value::EdgeRef(eid, a, b, crate::graph::EdgeType::new("KNOWS"))
674            .materialize_edge(&store);
675        match &val {
676            Value::Edge(id, edge) => {
677                assert_eq!(*id, eid);
678                assert_eq!(edge.source, a);
679                assert_eq!(edge.target, b);
680            }
681            _ => panic!("Expected Value::Edge after materialization"),
682        }
683
684        // Non-existent EdgeRef becomes Null
685        let val = Value::EdgeRef(EdgeId::new(9999), a, b, crate::graph::EdgeType::new("X"))
686            .materialize_edge(&store);
687        assert!(val.is_null());
688
689        // Non-edge value is returned unchanged
690        let val = Value::Null.materialize_edge(&store);
691        assert!(val.is_null());
692    }
693
694    #[test]
695    fn test_resolve_property_node() {
696        let mut store = GraphStore::new();
697        let id = store.create_node("Person");
698        store.get_node_mut(id).unwrap().set_property(
699            "name".to_string(),
700            PropertyValue::String("Alice".to_string()),
701        );
702
703        // Resolve from Node (materialized)
704        let node = store.get_node(id).unwrap().clone();
705        let val = Value::Node(id, node);
706        let prop = val.resolve_property("name", &store);
707        assert_eq!(prop, PropertyValue::String("Alice".to_string()));
708
709        // Missing property returns Null
710        let prop = val.resolve_property("missing", &store);
711        assert_eq!(prop, PropertyValue::Null);
712    }
713
714    #[test]
715    fn test_resolve_property_noderef() {
716        let mut store = GraphStore::new();
717        let id = store.create_node("Person");
718        store
719            .get_node_mut(id)
720            .unwrap()
721            .set_property("age".to_string(), PropertyValue::Integer(30));
722
723        let val = Value::NodeRef(id);
724        let prop = val.resolve_property("age", &store);
725        assert_eq!(prop, PropertyValue::Integer(30));
726
727        // Non-existent NodeRef
728        let val = Value::NodeRef(NodeId::new(9999));
729        let prop = val.resolve_property("age", &store);
730        assert_eq!(prop, PropertyValue::Null);
731    }
732
733    #[test]
734    fn test_resolve_property_edge() {
735        let mut store = GraphStore::new();
736        let a = store.create_node("A");
737        let b = store.create_node("B");
738
739        let mut props = std::collections::HashMap::new();
740        props.insert("since".to_string(), PropertyValue::Integer(2020));
741        let eid = store
742            .create_edge_with_properties(a, b, "KNOWS", props)
743            .unwrap();
744
745        // From Edge
746        let edge = store.get_edge(eid).unwrap().clone();
747        let val = Value::Edge(eid, edge);
748        let prop = val.resolve_property("since", &store);
749        assert_eq!(prop, PropertyValue::Integer(2020));
750    }
751
752    #[test]
753    fn test_resolve_property_edgeref() {
754        let mut store = GraphStore::new();
755        let a = store.create_node("A");
756        let b = store.create_node("B");
757
758        let mut props = std::collections::HashMap::new();
759        props.insert("weight".to_string(), PropertyValue::Float(0.5));
760        let eid = store
761            .create_edge_with_properties(a, b, "KNOWS", props)
762            .unwrap();
763
764        let val = Value::EdgeRef(eid, a, b, crate::graph::EdgeType::new("KNOWS"));
765        let prop = val.resolve_property("weight", &store);
766        assert_eq!(prop, PropertyValue::Float(0.5));
767
768        // Non-existent EdgeRef
769        let val = Value::EdgeRef(EdgeId::new(9999), a, b, crate::graph::EdgeType::new("X"));
770        let prop = val.resolve_property("weight", &store);
771        assert_eq!(prop, PropertyValue::Null);
772    }
773
774    #[test]
775    fn test_resolve_property_non_node_edge() {
776        let store = GraphStore::new();
777        let val = Value::Null;
778        assert_eq!(
779            val.resolve_property("anything", &store),
780            PropertyValue::Null
781        );
782
783        let val = Value::Property(PropertyValue::Integer(42));
784        assert_eq!(val.resolve_property("x", &store), PropertyValue::Null);
785    }
786
787    #[test]
788    fn test_record_batch_get() {
789        let mut batch = RecordBatch::new(vec!["n".to_string()]);
790        let mut r1 = Record::new();
791        r1.bind("n".to_string(), Value::Property(PropertyValue::Integer(1)));
792        let mut r2 = Record::new();
793        r2.bind("n".to_string(), Value::Property(PropertyValue::Integer(2)));
794        batch.push(r1);
795        batch.push(r2);
796
797        assert!(batch.get(0).is_some());
798        assert!(batch.get(1).is_some());
799        assert!(batch.get(2).is_none()); // out of bounds
800
801        let r = batch.get(0).unwrap();
802        assert_eq!(
803            r.get("n").unwrap().as_property(),
804            Some(&PropertyValue::Integer(1))
805        );
806    }
807
808    #[test]
809    fn test_record_bindings() {
810        let mut r = Record::new();
811        r.bind("x".to_string(), Value::Property(PropertyValue::Integer(1)));
812        r.bind("y".to_string(), Value::Null);
813
814        let bindings = r.bindings();
815        assert_eq!(bindings.len(), 2);
816        assert!(bindings.contains_key("x"));
817        assert!(bindings.contains_key("y"));
818    }
819
820    #[test]
821    fn test_record_default() {
822        let r = Record::default();
823        assert_eq!(r.bindings().len(), 0);
824    }
825
826    #[test]
827    fn test_value_partial_eq_cross_variant() {
828        // Node == NodeRef with same ID
829        let node = Node::new(NodeId::new(5), Label::new("A"));
830        let v1 = Value::Node(NodeId::new(5), node.clone());
831        let v2 = Value::NodeRef(NodeId::new(5));
832        assert_eq!(v1, v2);
833        assert_eq!(v2, v1);
834
835        // Different IDs
836        let v3 = Value::NodeRef(NodeId::new(6));
837        assert_ne!(v1, v3);
838
839        // Edge == EdgeRef with same ID
840        let edge = crate::graph::Edge::new(
841            EdgeId::new(1),
842            NodeId::new(1),
843            NodeId::new(2),
844            crate::graph::EdgeType::new("E"),
845        );
846        let ev1 = Value::Edge(EdgeId::new(1), edge);
847        let ev2 = Value::EdgeRef(
848            EdgeId::new(1),
849            NodeId::new(1),
850            NodeId::new(2),
851            crate::graph::EdgeType::new("E"),
852        );
853        assert_eq!(ev1, ev2);
854        assert_eq!(ev2, ev1);
855
856        // Different types don't equal
857        assert_ne!(v1, ev1);
858        assert_ne!(Value::Null, v1);
859
860        // Path equality
861        let p1 = Value::Path {
862            nodes: vec![NodeId::new(1)],
863            edges: vec![EdgeId::new(1)],
864        };
865        let p2 = Value::Path {
866            nodes: vec![NodeId::new(1)],
867            edges: vec![EdgeId::new(1)],
868        };
869        let p3 = Value::Path {
870            nodes: vec![NodeId::new(2)],
871            edges: vec![EdgeId::new(1)],
872        };
873        assert_eq!(p1, p2);
874        assert_ne!(p1, p3);
875    }
876
877    #[test]
878    fn test_value_hash_cross_variant() {
879        use std::collections::hash_map::DefaultHasher;
880
881        fn hash_value(v: &Value) -> u64 {
882            let mut hasher = DefaultHasher::new();
883            v.hash(&mut hasher);
884            hasher.finish()
885        }
886
887        // Node and NodeRef with same ID should hash the same
888        let node = Node::new(NodeId::new(5), Label::new("A"));
889        let v1 = Value::Node(NodeId::new(5), node);
890        let v2 = Value::NodeRef(NodeId::new(5));
891        assert_eq!(hash_value(&v1), hash_value(&v2));
892
893        // Edge and EdgeRef with same ID should hash the same
894        let edge = crate::graph::Edge::new(
895            EdgeId::new(3),
896            NodeId::new(1),
897            NodeId::new(2),
898            crate::graph::EdgeType::new("E"),
899        );
900        let ev1 = Value::Edge(EdgeId::new(3), edge);
901        let ev2 = Value::EdgeRef(
902            EdgeId::new(3),
903            NodeId::new(1),
904            NodeId::new(2),
905            crate::graph::EdgeType::new("E"),
906        );
907        assert_eq!(hash_value(&ev1), hash_value(&ev2));
908
909        // Different variant types should have different hashes
910        assert_ne!(hash_value(&v1), hash_value(&ev1));
911        assert_ne!(hash_value(&Value::Null), hash_value(&v1));
912    }
913}