Skip to main content

core_query/
result.rs

1use core_storage::Value;
2use std::collections::BTreeMap;
3
4/// What a result column holds when it is a bare graph variable (`RETURN n`,
5/// `RETURN r`, `RETURN p`) or a list of them (`collect(n)`, `[a, b]`,
6/// `p.nodes`, `p.relationships`). Any other column (a property, an
7/// expression, any other aggregate, a value alias) has no kind.
8///
9/// The column's values are rendered as every surface has always seen them —
10/// a node as its key string, a relationship as a `{type, start, end}` map, a
11/// path as a `{nodes, relationships, length}` map — so a caller that ignores
12/// the kind reads exactly what it read before. A protocol that has graph
13/// structures of its own (Bolt) reads the kind, and the [`Entity`] details
14/// when they were asked for (see [`crate::cypher::with_entity_detail`]).
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum EntityKind {
17    Node,
18    Relationship,
19    Path,
20    /// A list of nodes, each rendered as its key.
21    NodeList,
22    /// A list of relationships, each rendered as its map.
23    RelationshipList,
24    /// A list of paths, each rendered as its map.
25    PathList,
26}
27
28impl EntityKind {
29    /// The kind of a list of elements of this kind; `None` for a list of
30    /// lists, which has no kind.
31    pub fn list_of(self) -> Option<EntityKind> {
32        match self {
33            EntityKind::Node => Some(EntityKind::NodeList),
34            EntityKind::Relationship => Some(EntityKind::RelationshipList),
35            EntityKind::Path => Some(EntityKind::PathList),
36            _ => None,
37        }
38    }
39
40    /// The kind of one element, for a list kind.
41    pub fn element(self) -> Option<EntityKind> {
42        match self {
43            EntityKind::NodeList => Some(EntityKind::Node),
44            EntityKind::RelationshipList => Some(EntityKind::Relationship),
45            EntityKind::PathList => Some(EntityKind::Path),
46            _ => None,
47        }
48    }
49}
50
51/// The element id of the node `key`: what `elementId(n)` returns and what a
52/// Bolt driver reads as `node.element_id`. Opaque but stable: match it, do not
53/// parse it.
54pub fn node_element_id(key: &str) -> String {
55    format!("n:{key}")
56}
57
58/// The element id of the relationship `start -[rel_type]-> end`: what
59/// `elementId(r)` returns and what a Bolt driver reads as
60/// `relationship.element_id`. Opaque but stable; a `\` or `|` inside a part is
61/// escaped, so two different relationships never share an id.
62pub fn relationship_element_id(rel_type: &str, start: &str, end: &str) -> String {
63    let escape = |part: &str| part.replace('\\', "\\\\").replace('|', "\\|");
64    format!("r:{}|{}|{}", escape(rel_type), escape(start), escape(end))
65}
66
67/// A node as it stood when the query read it.
68#[derive(Debug, Clone, PartialEq)]
69pub struct NodeEntity {
70    /// The node's dense id: assigned once, never reused after a delete, and
71    /// kept across a rename.
72    pub id: u32,
73    pub key: String,
74    pub label: String,
75    /// The node's properties as the query's view saw them (a role-scoped
76    /// view sees a node only when the role may, and then all of it).
77    pub properties: BTreeMap<String, Value>,
78}
79
80/// A relationship as it stood when the query read it. `start` and `end` are
81/// the node keys of its stored direction.
82#[derive(Debug, Clone, PartialEq)]
83pub struct RelationshipEntity {
84    pub rel_type: String,
85    pub start: String,
86    pub end: String,
87    /// The dense ids of the start and end nodes.
88    pub start_id: u32,
89    pub end_id: u32,
90    pub properties: BTreeMap<String, Value>,
91}
92
93/// A path: its nodes in walk order (`nodes.len() == relationships.len() + 1`)
94/// and its relationships, each in its stored direction.
95#[derive(Debug, Clone, PartialEq)]
96pub struct PathEntity {
97    pub nodes: Vec<NodeEntity>,
98    pub relationships: Vec<RelationshipEntity>,
99}
100
101/// The full graph element behind one cell of an [`EntityKind`] column.
102#[derive(Debug, Clone, PartialEq)]
103pub enum Entity {
104    Node(NodeEntity),
105    Relationship(RelationshipEntity),
106    Path(PathEntity),
107    /// The elements of a list column, one per list element; `None` for an
108    /// element that is not one (a null).
109    List(Vec<Option<Entity>>),
110}
111
112/// The entity kind that a computed expression (a map literal, a mixed list, or
113/// a function expression that returns a node) carries at any depth. Used
114/// internally to tag columns whose value contains graph elements that the flat
115/// [`EntityKind`] cannot represent.
116///
117/// This is crate-private: callers that need entity details use [`ValueEntityTree`].
118#[derive(Debug, Clone, PartialEq)]
119pub(crate) enum ValueKindTag {
120    Node,
121    Relationship,
122    Path,
123    /// A list where every element has the same entity kind (e.g. the result of
124    /// `nodes(p)` or `collect(n)` when nested inside another expression).
125    UniformList(EntityKind),
126    /// A list whose elements have per-element kinds, potentially heterogeneous
127    /// (e.g. `[n, r]`, `[n, 1]`). `None` means that element has no entity kind.
128    MixedList(Vec<Option<ValueKindTag>>),
129    /// A map literal with at least one field that holds an entity.
130    Map(Vec<(String, ValueKindTag)>),
131}
132
133/// The resolved graph element tree alongside a computed value: a node,
134/// relationship, path, list of them (possibly mixed), or a map whose fields
135/// include graph elements. Used by the Bolt layer to send structures for entity
136/// values that live inside maps or heterogeneous lists.
137///
138/// Unlike [`Entity`], which has no `Map` variant (it predates this feature and
139/// is `pub` without `#[non_exhaustive]`), `ValueEntityTree` can represent a
140/// map field whose value is a graph element.
141///
142/// Marked `#[non_exhaustive]` so future graph value types can be added without
143/// a breaking change.
144#[non_exhaustive]
145#[derive(Debug, Clone, PartialEq)]
146pub enum ValueEntityTree {
147    Node(NodeEntity),
148    Relationship(RelationshipEntity),
149    Path(PathEntity),
150    List(Vec<Option<ValueEntityTree>>),
151    Map(Vec<(String, ValueEntityTree)>),
152}
153
154/// What a write statement changed, counted per effect: the numbers a client
155/// reads as the statement's summary. Every field counts this statement only.
156///
157/// The one-row write result (`created`, `properties_set`, `deleted`,
158/// `relationships_created`) keeps its documented meaning; these counters are
159/// finer: deletes are split into nodes and relationships, and
160/// `properties_set` includes the properties a `CREATE` or a `MERGE` create
161/// arm writes with the node.
162#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
163pub struct WriteStats {
164    pub nodes_created: i64,
165    pub nodes_deleted: i64,
166    pub relationships_created: i64,
167    pub relationships_deleted: i64,
168    pub properties_set: i64,
169    /// One per node created: a node is created with its one label.
170    pub labels_added: i64,
171    pub indexes_added: i64,
172    pub indexes_removed: i64,
173    pub constraints_added: i64,
174    pub constraints_removed: i64,
175}
176
177impl WriteStats {
178    /// Whether the statement changed anything.
179    pub fn contains_updates(&self) -> bool {
180        *self != Self::default()
181    }
182}
183
184/// Heap-allocated sidecar for entity tree data, boxed to keep [`ResultSet`]
185/// small. Only allocated for queries whose columns include computed expressions
186/// with embedded graph elements (maps, mixed lists, function expressions).
187#[derive(Debug, Clone)]
188struct EntityTreeSidecar {
189    /// Per-column entity kind tag trees. One per column; `None` for columns
190    /// without nested entity structure.
191    kind_trees: Vec<Option<ValueKindTag>>,
192    /// Row-major value entity trees, resolved from `kind_trees` when entity
193    /// detail is requested. `None` until attached by `set_value_entity_trees`.
194    entity_trees: Option<Vec<Option<ValueEntityTree>>>,
195}
196
197/// A query result: named columns and rows of values.
198///
199/// Beside the rows it may carry three things no renderer has to read: the
200/// [`EntityKind`] of each bare-variable column, the [`Entity`] behind each of
201/// their cells (only when asked for), and a write's [`WriteStats`]. Equality
202/// compares the columns and rows alone, the table every surface renders.
203#[derive(Debug, Clone)]
204pub struct ResultSet {
205    columns: Vec<String>,
206    rows: Vec<Vec<Option<Value>>>,
207    kinds: Vec<Option<EntityKind>>,
208    /// Row-major, `columns.len()` cells per row; `None` until attached.
209    entities: Option<Vec<Option<Entity>>>,
210    /// Boxed entity tree sidecar; `None` when no column has an entity kind tree.
211    trees: Option<Box<EntityTreeSidecar>>,
212    /// Boxed: most results are reads, and a `ResultSet` is held inline in
213    /// other types.
214    stats: Option<Box<WriteStats>>,
215}
216
217impl PartialEq for ResultSet {
218    fn eq(&self, other: &Self) -> bool {
219        self.columns == other.columns && self.rows == other.rows
220    }
221}
222
223impl ResultSet {
224    pub fn new(columns: Vec<String>) -> Self {
225        let kinds = vec![None; columns.len()];
226        Self {
227            columns,
228            rows: Vec::new(),
229            kinds,
230            entities: None,
231            trees: None,
232            stats: None,
233        }
234    }
235
236    pub fn push_row(&mut self, row: Vec<Option<Value>>) {
237        assert_eq!(
238            row.len(),
239            self.columns.len(),
240            "row arity does not match ResultSet columns"
241        );
242        self.rows.push(row);
243        if let Some(entities) = self.entities.as_mut() {
244            entities.resize(self.rows.len() * self.columns.len(), None);
245        }
246        if let Some(sidecar) = self.trees.as_mut() {
247            if let Some(entity_trees) = sidecar.entity_trees.as_mut() {
248                entity_trees.resize(self.rows.len() * self.columns.len(), None);
249            }
250        }
251    }
252
253    pub fn columns(&self) -> &[String] {
254        &self.columns
255    }
256
257    pub fn len(&self) -> usize {
258        self.rows.len()
259    }
260
261    pub fn is_empty(&self) -> bool {
262        self.rows.is_empty()
263    }
264
265    /// # Panics — if `i >= self.len()`.
266    pub fn row(&self, i: usize) -> &[Option<Value>] {
267        &self.rows[i]
268    }
269
270    pub fn get(&self, i: usize, col: &str) -> Option<&Value> {
271        let idx = self.columns.iter().position(|c| c == col)?;
272        self.rows.get(i)?.get(idx)?.as_ref()
273    }
274
275    /// The kind of column `col`, when it is a bare graph variable.
276    pub fn column_kind(&self, col: usize) -> Option<EntityKind> {
277        self.kinds.get(col).copied().flatten()
278    }
279
280    /// The kind of every column, in column order.
281    pub fn column_kinds(&self) -> &[Option<EntityKind>] {
282        &self.kinds
283    }
284
285    /// Set the kind of every column. Extra entries are dropped and missing
286    /// ones are `None`, so the list always matches the columns.
287    pub fn set_column_kinds(&mut self, mut kinds: Vec<Option<EntityKind>>) {
288        kinds.resize(self.columns.len(), None);
289        self.kinds = kinds;
290        // If a sidecar exists, keep its kind_trees length in sync.
291        if let Some(sidecar) = self.trees.as_mut() {
292            sidecar.kind_trees.resize(self.columns.len(), None);
293        }
294    }
295
296    /// The graph element behind cell (`row`, `col`), when entity details were
297    /// attached and the cell holds one.
298    pub fn entity(&self, row: usize, col: usize) -> Option<&Entity> {
299        if col >= self.columns.len() {
300            return None;
301        }
302        self.entities
303            .as_ref()?
304            .get(row * self.columns.len() + col)?
305            .as_ref()
306    }
307
308    /// Whether entity details are attached.
309    pub fn has_entities(&self) -> bool {
310        self.entities.is_some()
311    }
312
313    /// Attach the graph element behind each cell, row-major.
314    ///
315    /// # Panics — if `entities.len()` is not `len() * columns().len()`.
316    pub fn set_entities(&mut self, entities: Vec<Option<Entity>>) {
317        assert_eq!(
318            entities.len(),
319            self.rows.len() * self.columns.len(),
320            "entity table does not match the ResultSet"
321        );
322        self.entities = Some(entities);
323    }
324
325    /// The per-column entity kind tag tree, for a column whose value contains
326    /// graph elements that the flat `EntityKind` cannot represent (e.g. a map
327    /// or mixed list). `None` when the column has no such structure.
328    pub(crate) fn value_kind_tree(&self, col: usize) -> Option<&ValueKindTag> {
329        self.trees.as_ref()?.kind_trees.get(col)?.as_ref()
330    }
331
332    /// Set the per-column entity kind tag trees. Called by the executor.
333    /// Allocates the sidecar only when at least one entry is `Some`.
334    pub(crate) fn set_value_kind_trees(&mut self, mut kind_trees: Vec<Option<ValueKindTag>>) {
335        kind_trees.resize(self.columns.len(), None);
336        if kind_trees.iter().any(Option::is_some) {
337            match self.trees.as_mut() {
338                Some(sidecar) => sidecar.kind_trees = kind_trees,
339                None => {
340                    self.trees = Some(Box::new(EntityTreeSidecar {
341                        kind_trees,
342                        entity_trees: None,
343                    }));
344                }
345            }
346        } else if let Some(sidecar) = self.trees.as_mut() {
347            // Sidecar already exists (for entity_trees); keep kind_trees in sync.
348            sidecar.kind_trees = kind_trees;
349        }
350    }
351
352    /// The resolved entity tree for cell (`row`, `col`), when entity details
353    /// were attached for a column that holds a computed value with embedded
354    /// graph elements (a map, a mixed list, `head(collect(n))`, etc.).
355    pub fn value_entity_tree(&self, row: usize, col: usize) -> Option<&ValueEntityTree> {
356        if col >= self.columns.len() {
357            return None;
358        }
359        self.trees
360            .as_ref()?
361            .entity_trees
362            .as_ref()?
363            .get(row * self.columns.len() + col)?
364            .as_ref()
365    }
366
367    /// Attach the entity tree for each cell, row-major.
368    ///
369    /// # Panics — if `entity_trees.len()` is not `len() * columns().len()`.
370    pub(crate) fn set_value_entity_trees(&mut self, entity_trees: Vec<Option<ValueEntityTree>>) {
371        assert_eq!(
372            entity_trees.len(),
373            self.rows.len() * self.columns.len(),
374            "value entity tree table does not match the ResultSet"
375        );
376        match self.trees.as_mut() {
377            Some(sidecar) => sidecar.entity_trees = Some(entity_trees),
378            None => {
379                self.trees = Some(Box::new(EntityTreeSidecar {
380                    kind_trees: vec![None; self.columns.len()],
381                    entity_trees: Some(entity_trees),
382                }));
383            }
384        }
385    }
386
387    /// Whether any column has a value kind tree (meaning the result may contain
388    /// embedded graph elements in maps or mixed lists).
389    pub(crate) fn has_value_kind_trees(&self) -> bool {
390        self.trees
391            .as_ref()
392            .is_some_and(|s| s.kind_trees.iter().any(Option::is_some))
393    }
394
395    /// What the write statement that produced this result changed.
396    pub fn stats(&self) -> Option<&WriteStats> {
397        self.stats.as_deref()
398    }
399
400    /// Record what the write statement that produced this result changed.
401    pub fn with_stats(mut self, stats: WriteStats) -> Self {
402        self.stats = Some(Box::new(stats));
403        self
404    }
405}
406
407#[cfg(test)]
408mod tests {
409    use super::{Entity, EntityKind, NodeEntity, ResultSet, WriteStats};
410    use core_storage::Value;
411
412    #[test]
413    fn resultset_basics() {
414        let mut rs = ResultSet::new(vec!["a".into(), "b".into()]);
415        rs.push_row(vec![Some(Value::Int(1)), None]);
416        assert_eq!(rs.len(), 1);
417        assert_eq!(rs.get(0, "a"), Some(&Value::Int(1)));
418        assert_eq!(rs.get(0, "b"), None);
419        assert_eq!(rs.get(0, "zz"), None);
420        assert_eq!(rs.row(0)[1], None);
421    }
422
423    #[test]
424    #[should_panic]
425    fn resultset_arity_mismatch_panics() {
426        let mut rs = ResultSet::new(vec!["a".into()]);
427        rs.push_row(vec![]);
428    }
429
430    #[test]
431    fn resultset_empty_and_columns() {
432        let rs = ResultSet::new(vec!["a".into()]);
433        assert!(rs.is_empty());
434        assert_eq!(rs.len(), 0);
435        assert_eq!(rs.columns(), &["a".to_string()]);
436    }
437
438    #[test]
439    fn kinds_entities_and_stats_ride_beside_the_table_and_equality_ignores_them() {
440        let mut rs = ResultSet::new(vec!["n".into(), "x".into()]);
441        rs.push_row(vec![Some(Value::Str("k".into())), Some(Value::Int(1))]);
442        let plain = rs.clone();
443        rs.set_column_kinds(vec![Some(EntityKind::Node)]);
444        assert_eq!(rs.column_kinds(), &[Some(EntityKind::Node), None]);
445        let node = Entity::Node(NodeEntity {
446            id: 0,
447            key: "k".into(),
448            label: "L".into(),
449            properties: Default::default(),
450        });
451        rs.set_entities(vec![Some(node.clone()), None]);
452        assert_eq!(rs.entity(0, 0), Some(&node));
453        assert_eq!(rs.entity(0, 1), None);
454        assert_eq!(rs.entity(0, 2), None);
455        let rs = rs.with_stats(WriteStats {
456            nodes_created: 1,
457            ..Default::default()
458        });
459        assert!(rs.stats().unwrap().contains_updates());
460        assert!(!WriteStats::default().contains_updates());
461        assert_eq!(rs, plain);
462    }
463}