Skip to main content

lora_executor/
value.rs

1use lora_analyzer::symbols::VarId;
2use lora_store::{
3    LoraBinary, LoraDate, LoraDateTime, LoraDuration, LoraLocalDateTime, LoraLocalTime, LoraPoint,
4    LoraTime, LoraVector, NodeId, PropertyValue, RelationshipId, VectorValues,
5};
6
7/// A materialised path: alternating node/relationship IDs.
8/// nodes.len() == rels.len() + 1
9#[derive(Debug, Clone, PartialEq)]
10pub struct LoraPath {
11    pub nodes: Vec<NodeId>,
12    pub rels: Vec<RelationshipId>,
13}
14use serde::ser::{SerializeMap, SerializeSeq};
15use serde::{Serialize, Serializer};
16use std::collections::BTreeMap;
17use std::sync::Arc;
18
19#[derive(Debug, Clone, PartialEq)]
20pub enum LoraValue {
21    Null,
22    Bool(bool),
23    Int(i64),
24    Float(f64),
25    String(String),
26    Binary(LoraBinary),
27    List(Vec<LoraValue>),
28    Map(BTreeMap<String, LoraValue>),
29    Node(NodeId),
30    Relationship(RelationshipId),
31    Path(LoraPath),
32    Date(LoraDate),
33    Time(LoraTime),
34    LocalTime(LoraLocalTime),
35    DateTime(LoraDateTime),
36    LocalDateTime(LoraLocalDateTime),
37    Duration(LoraDuration),
38    Point(LoraPoint),
39    Vector(LoraVector),
40}
41
42impl LoraValue {
43    pub fn is_truthy(&self) -> bool {
44        match self {
45            LoraValue::Null => false,
46            LoraValue::Bool(v) => *v,
47            _ => true,
48        }
49    }
50
51    pub fn as_i64(&self) -> Option<i64> {
52        match self {
53            LoraValue::Int(v) => Some(*v),
54            _ => None,
55        }
56    }
57
58    pub fn as_f64(&self) -> Option<f64> {
59        match self {
60            LoraValue::Int(v) => Some(*v as f64),
61            LoraValue::Float(v) => Some(*v),
62            _ => None,
63        }
64    }
65
66    /// Cypher comparison of two temporal values of the same kind, by the
67    /// instant (or time of day) they denote. `None` when either side is not
68    /// a temporal of that kind; durations are not ordered.
69    pub(crate) fn temporal_cmp(&self, other: &LoraValue) -> Option<std::cmp::Ordering> {
70        let (a, b) = match (self, other) {
71            (LoraValue::Date(a), LoraValue::Date(b)) => (a.order_nanos(), b.order_nanos()),
72            (LoraValue::DateTime(a), LoraValue::DateTime(b)) => (a.order_nanos(), b.order_nanos()),
73            (LoraValue::LocalDateTime(a), LoraValue::LocalDateTime(b)) => {
74                (a.order_nanos(), b.order_nanos())
75            }
76            (LoraValue::Time(a), LoraValue::Time(b)) => (a.order_nanos(), b.order_nanos()),
77            (LoraValue::LocalTime(a), LoraValue::LocalTime(b)) => {
78                (a.order_nanos(), b.order_nanos())
79            }
80            _ => return None,
81        };
82        Some(a.cmp(&b))
83    }
84}
85
86impl Serialize for LoraValue {
87    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
88    where
89        S: Serializer,
90    {
91        match self {
92            LoraValue::Null => serializer.serialize_unit(),
93            LoraValue::Bool(v) => serializer.serialize_bool(*v),
94            LoraValue::Int(v) => serializer.serialize_i64(*v),
95            LoraValue::Float(v) => serializer.serialize_f64(*v),
96            LoraValue::String(v) => serializer.serialize_str(v),
97            LoraValue::Binary(v) => serialize_binary(serializer, v),
98
99            LoraValue::List(values) => {
100                let mut seq = serializer.serialize_seq(Some(values.len()))?;
101                for value in values {
102                    seq.serialize_element(value)?;
103                }
104                seq.end()
105            }
106
107            LoraValue::Map(map) => {
108                let mut ser_map = serializer.serialize_map(Some(map.len()))?;
109                for (k, v) in map {
110                    ser_map.serialize_entry(k, v)?;
111                }
112                ser_map.end()
113            }
114
115            // These should ideally not reach output anymore if executor hydrates first.
116            LoraValue::Node(id) => {
117                let mut ser_map = serializer.serialize_map(Some(2))?;
118                ser_map.serialize_entry("kind", "node")?;
119                ser_map.serialize_entry("id", id)?;
120                ser_map.end()
121            }
122
123            LoraValue::Relationship(id) => {
124                let mut ser_map = serializer.serialize_map(Some(2))?;
125                ser_map.serialize_entry("kind", "relationship")?;
126                ser_map.serialize_entry("id", id)?;
127                ser_map.end()
128            }
129
130            LoraValue::Path(path) => {
131                let mut ser_map = serializer.serialize_map(Some(3))?;
132                ser_map.serialize_entry("kind", "path")?;
133                ser_map.serialize_entry("nodes", &path.nodes)?;
134                ser_map.serialize_entry("rels", &path.rels)?;
135                ser_map.end()
136            }
137
138            LoraValue::Date(d) => serializer.serialize_str(&d.to_string()),
139            LoraValue::Time(t) => serializer.serialize_str(&t.to_string()),
140            LoraValue::LocalTime(t) => serializer.serialize_str(&t.to_string()),
141            LoraValue::DateTime(dt) => serializer.serialize_str(&dt.to_string()),
142            LoraValue::LocalDateTime(dt) => serializer.serialize_str(&dt.to_string()),
143            LoraValue::Duration(dur) => serializer.serialize_str(&dur.to_string()),
144            LoraValue::Point(p) => {
145                let len = if p.z.is_some() { 4 } else { 3 };
146                let mut m = serializer.serialize_map(Some(len))?;
147                m.serialize_entry("srid", &p.srid)?;
148                m.serialize_entry("x", &p.x)?;
149                m.serialize_entry("y", &p.y)?;
150                if let Some(z) = p.z {
151                    m.serialize_entry("z", &z)?;
152                }
153                m.end()
154            }
155            LoraValue::Vector(v) => serialize_vector(serializer, v),
156        }
157    }
158}
159
160fn serialize_binary<S: Serializer>(serializer: S, v: &LoraBinary) -> Result<S::Ok, S::Error> {
161    let mut m = serializer.serialize_map(Some(3))?;
162    m.serialize_entry("kind", "binary")?;
163    m.serialize_entry("length", &v.len())?;
164    m.serialize_entry("segments", v.segments())?;
165    m.end()
166}
167
168fn serialize_vector<S: Serializer>(serializer: S, v: &LoraVector) -> Result<S::Ok, S::Error> {
169    let mut m = serializer.serialize_map(Some(4))?;
170    m.serialize_entry("kind", "vector")?;
171    m.serialize_entry("dimension", &v.dimension)?;
172    m.serialize_entry("coordinateType", v.coordinate_type().as_str())?;
173    // Render values using the narrowest numeric type that fits the
174    // storage so downstream consumers (serde_json in particular) can
175    // surface integers vs. floats without losing information.
176    match &v.values {
177        VectorValues::Float64(values) => m.serialize_entry("values", values)?,
178        VectorValues::Float32(values) => {
179            let widened: Vec<f64> = values.iter().map(|x| *x as f64).collect();
180            m.serialize_entry("values", &widened)?;
181        }
182        VectorValues::Integer64(values) => m.serialize_entry("values", values)?,
183        VectorValues::Integer32(values) => {
184            let widened: Vec<i64> = values.iter().map(|x| *x as i64).collect();
185            m.serialize_entry("values", &widened)?;
186        }
187        VectorValues::Integer16(values) => {
188            let widened: Vec<i64> = values.iter().map(|x| *x as i64).collect();
189            m.serialize_entry("values", &widened)?;
190        }
191        VectorValues::Integer8(values) => {
192            let widened: Vec<i64> = values.iter().map(|x| *x as i64).collect();
193            m.serialize_entry("values", &widened)?;
194        }
195    }
196    m.end()
197}
198
199impl From<PropertyValue> for LoraValue {
200    fn from(value: PropertyValue) -> Self {
201        match value {
202            PropertyValue::Null => LoraValue::Null,
203            PropertyValue::Bool(v) => LoraValue::Bool(v),
204            PropertyValue::Int(v) => LoraValue::Int(v),
205            PropertyValue::Float(v) => LoraValue::Float(v),
206            PropertyValue::String(v) => LoraValue::String(v),
207            PropertyValue::Binary(v) => LoraValue::Binary(v),
208            PropertyValue::List(values) => {
209                LoraValue::List(values.into_iter().map(LoraValue::from).collect())
210            }
211            PropertyValue::Map(map) => LoraValue::Map(
212                map.into_iter()
213                    .map(|(k, v)| (k, LoraValue::from(v)))
214                    .collect(),
215            ),
216            PropertyValue::Date(d) => LoraValue::Date(d),
217            PropertyValue::Time(t) => LoraValue::Time(t),
218            PropertyValue::LocalTime(t) => LoraValue::LocalTime(t),
219            PropertyValue::DateTime(dt) => LoraValue::DateTime(dt),
220            PropertyValue::LocalDateTime(dt) => LoraValue::LocalDateTime(dt),
221            PropertyValue::Duration(dur) => LoraValue::Duration(dur),
222            PropertyValue::Point(p) => LoraValue::Point(p),
223            PropertyValue::Vector(v) => LoraValue::Vector(v),
224        }
225    }
226}
227
228/// Build a `LoraValue` from a property read through a view: scalars and
229/// strings directly, other kinds from the value the view hands out.
230impl From<lora_store::ValueRef<'_>> for LoraValue {
231    #[inline]
232    fn from(value: lora_store::ValueRef<'_>) -> Self {
233        use lora_store::ValueRef;
234        match value {
235            ValueRef::Null => LoraValue::Null,
236            ValueRef::Bool(v) => LoraValue::Bool(v),
237            ValueRef::Int(v) => LoraValue::Int(v),
238            ValueRef::Float(v) => LoraValue::Float(v),
239            ValueRef::String(v) => LoraValue::String(v.to_owned()),
240            ValueRef::Other(v) => match v.get() {
241                std::borrow::Cow::Borrowed(v) => LoraValue::from(v),
242                std::borrow::Cow::Owned(v) => LoraValue::from(v),
243            },
244        }
245    }
246}
247
248/// Build a `LoraValue` from a borrowed `PropertyValue` in a single walk,
249/// without the `prop.clone().into()` double traversal.
250impl From<&PropertyValue> for LoraValue {
251    fn from(value: &PropertyValue) -> Self {
252        match value {
253            PropertyValue::Null => LoraValue::Null,
254            PropertyValue::Bool(v) => LoraValue::Bool(*v),
255            PropertyValue::Int(v) => LoraValue::Int(*v),
256            PropertyValue::Float(v) => LoraValue::Float(*v),
257            PropertyValue::String(v) => LoraValue::String(v.clone()),
258            PropertyValue::Binary(v) => LoraValue::Binary(v.clone()),
259            PropertyValue::List(values) => {
260                LoraValue::List(values.iter().map(LoraValue::from).collect())
261            }
262            PropertyValue::Map(map) => LoraValue::Map(
263                map.iter()
264                    .map(|(k, v)| (k.clone(), LoraValue::from(v)))
265                    .collect(),
266            ),
267            PropertyValue::Date(d) => LoraValue::Date(d.clone()),
268            PropertyValue::Time(t) => LoraValue::Time(t.clone()),
269            PropertyValue::LocalTime(t) => LoraValue::LocalTime(t.clone()),
270            PropertyValue::DateTime(dt) => LoraValue::DateTime(dt.clone()),
271            PropertyValue::LocalDateTime(dt) => LoraValue::LocalDateTime(dt.clone()),
272            PropertyValue::Duration(dur) => LoraValue::Duration(dur.clone()),
273            PropertyValue::Point(p) => LoraValue::Point(p.clone()),
274            PropertyValue::Vector(v) => LoraValue::Vector(v.clone()),
275        }
276    }
277}
278
279impl From<LoraValue> for PropertyValue {
280    fn from(value: LoraValue) -> Self {
281        match value {
282            LoraValue::Null => PropertyValue::Null,
283            LoraValue::Bool(v) => PropertyValue::Bool(v),
284            LoraValue::Int(v) => PropertyValue::Int(v),
285            LoraValue::Float(v) => PropertyValue::Float(v),
286            LoraValue::String(v) => PropertyValue::String(v),
287            LoraValue::Binary(v) => PropertyValue::Binary(v),
288            LoraValue::List(values) => {
289                PropertyValue::List(values.into_iter().map(PropertyValue::from).collect())
290            }
291            LoraValue::Map(map) => PropertyValue::Map(
292                map.into_iter()
293                    .map(|(k, v)| (k, PropertyValue::from(v)))
294                    .collect(),
295            ),
296            LoraValue::Node(id) => PropertyValue::String(format!("node:{id}")),
297            LoraValue::Relationship(id) => PropertyValue::String(format!("rel:{id}")),
298            LoraValue::Path(_) => PropertyValue::Null,
299            LoraValue::Date(d) => PropertyValue::Date(d),
300            LoraValue::Time(t) => PropertyValue::Time(t),
301            LoraValue::LocalTime(t) => PropertyValue::LocalTime(t),
302            LoraValue::DateTime(dt) => PropertyValue::DateTime(dt),
303            LoraValue::LocalDateTime(dt) => PropertyValue::LocalDateTime(dt),
304            LoraValue::Duration(dur) => PropertyValue::Duration(dur),
305            LoraValue::Point(p) => PropertyValue::Point(p),
306            LoraValue::Vector(v) => PropertyValue::Vector(v),
307        }
308    }
309}
310
311/// Errors that can arise when converting a `LoraValue` into a
312/// `PropertyValue` for storage on a node or relationship.
313#[derive(Debug, Clone, PartialEq)]
314pub enum PropertyConversionError {
315    /// A list entry contained a VECTOR value. Vectors are first-class
316    /// properties themselves but they cannot be nested inside lists.
317    NestedVectorInList,
318    /// A node, relationship or path, at any depth: graph entities are not
319    /// property values, so they are rejected instead of being stored as a
320    /// `"node:<id>"` string (or, for a path, as null).
321    UnsupportedKind(&'static str),
322}
323
324impl std::fmt::Display for PropertyConversionError {
325    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
326        match self {
327            PropertyConversionError::NestedVectorInList => {
328                write!(f, "lists stored as properties cannot contain VECTOR values")
329            }
330            PropertyConversionError::UnsupportedKind(kind) => {
331                write!(f, "cannot store {kind} as a property")
332            }
333        }
334    }
335}
336
337impl std::error::Error for PropertyConversionError {}
338
339/// Fallible conversion used on every write path
340/// (`set_property_from_expr`, `overwrite_entity_target`,
341/// `mutate_entity_target`, `eval_properties_expr`, plus CREATE /
342/// MERGE). Rejects nodes, relationships and paths at any depth, and
343/// VECTOR values nested inside lists — everything else falls through to
344/// the infallible `From` implementation above. A top-level VECTOR
345/// property is always fine; only LISTs that directly contain a VECTOR
346/// entry are rejected. Index seeks also convert through here: a value
347/// that can't be a property falls back to a scan, where it matches
348/// nothing.
349pub fn lora_value_to_property(value: LoraValue) -> Result<PropertyValue, PropertyConversionError> {
350    /// Visit every nested value and, whenever we cross a `List`, flag the
351    /// `Vector` entries it directly contains. We still recurse through
352    /// `Map` and other `List` values so a vector buried under
353    /// `{inner: [vector(...)]}` is caught too.
354    fn visit(value: &LoraValue, inside_list: bool) -> Result<(), PropertyConversionError> {
355        match value {
356            LoraValue::Vector(_) if inside_list => Err(PropertyConversionError::NestedVectorInList),
357            LoraValue::Node(_) => Err(PropertyConversionError::UnsupportedKind("a node")),
358            LoraValue::Relationship(_) => {
359                Err(PropertyConversionError::UnsupportedKind("a relationship"))
360            }
361            LoraValue::Path(_) => Err(PropertyConversionError::UnsupportedKind("a path")),
362            LoraValue::List(items) => {
363                for item in items {
364                    visit(item, true)?;
365                }
366                Ok(())
367            }
368            LoraValue::Map(m) => {
369                for v in m.values() {
370                    visit(v, inside_list)?;
371                }
372                Ok(())
373            }
374            _ => Ok(()),
375        }
376    }
377
378    visit(&value, false)?;
379    Ok(PropertyValue::from(value))
380}
381
382/// A row slot's value. Large values sit behind an `Arc`, so cloning the row
383/// (every expand candidate, OPTIONAL MATCH merge, UNWIND element, FOREACH
384/// iteration) bumps a refcount instead of copying a list the row only carries
385/// (E-4: a 10k-element list made a 200-row hop ~250x slower). Small values
386/// stay inline: an `Arc` in every slot costs an allocation per insert and a
387/// pointer chase per read, 10-16% on hot paths.
388#[derive(Debug, Clone)]
389enum SlotValue {
390    Inline(LoraValue),
391    Shared(Arc<LoraValue>),
392}
393
394impl SlotValue {
395    #[inline]
396    fn new(value: LoraValue) -> Self {
397        // Ids, scalars and strings, most of what rows hold, are decided
398        // here; only containers walk `is_large`.
399        let large = match &value {
400            LoraValue::String(s) => s.len() >= LARGE_BYTES,
401            LoraValue::List(_)
402            | LoraValue::Map(_)
403            | LoraValue::Path(_)
404            | LoraValue::Binary(_)
405            | LoraValue::Vector(_) => is_large(&value),
406            _ => false,
407        };
408        if large {
409            SlotValue::Shared(Arc::new(value))
410        } else {
411            SlotValue::Inline(value)
412        }
413    }
414
415    #[inline]
416    fn get(&self) -> &LoraValue {
417        match self {
418            SlotValue::Inline(v) => v,
419            SlotValue::Shared(v) => v,
420        }
421    }
422
423    #[inline]
424    fn into_value(self) -> LoraValue {
425        match self {
426            SlotValue::Inline(v) => v,
427            SlotValue::Shared(v) => Arc::try_unwrap(v).unwrap_or_else(|v| (*v).clone()),
428        }
429    }
430}
431
432/// A string at least this long is shared rather than copied.
433const LARGE_BYTES: usize = 256;
434
435impl PartialEq for SlotValue {
436    fn eq(&self, other: &Self) -> bool {
437        self.get() == other.get()
438    }
439}
440
441/// Whether copying `value` costs enough for a row to share it: a list, map
442/// or path with 8+ entries or a nested container, a long string, a binary or
443/// a vector. Looks at no more than 8 elements.
444fn is_large(value: &LoraValue) -> bool {
445    const ENTRIES: usize = 8;
446    let heavy = |v: &LoraValue| match v {
447        LoraValue::List(_)
448        | LoraValue::Map(_)
449        | LoraValue::Path(_)
450        | LoraValue::Vector(_)
451        | LoraValue::Binary(_) => true,
452        LoraValue::String(s) => s.len() >= LARGE_BYTES,
453        _ => false,
454    };
455    match value {
456        LoraValue::List(items) => items.len() >= ENTRIES || items.iter().any(heavy),
457        LoraValue::Map(map) => map.len() >= ENTRIES || map.values().any(heavy),
458        LoraValue::String(s) => s.len() >= LARGE_BYTES,
459        LoraValue::Path(p) => p.nodes.len() >= ENTRIES,
460        LoraValue::Binary(_) | LoraValue::Vector(_) => true,
461        _ => false,
462    }
463}
464
465#[derive(Debug, Clone, PartialEq)]
466struct RowEntry {
467    /// Stored alongside the value so iterators can hand back `&VarId` while
468    /// the slot's position in `entries` remains the source of truth for
469    /// lookups.
470    var: VarId,
471    /// `None` means "use the fallback `_{key}` lazily". This avoids allocating
472    /// a String for every anonymous variable on the insert hot path.
473    ///
474    /// Shared `Arc<str>` rather than `String`: column names are fixed per
475    /// operator, so producers mint one `Arc` per column and every row (and
476    /// every row clone) holds a refcount instead of a private heap copy.
477    name: Option<Arc<str>>,
478    value: SlotValue,
479}
480
481/// Row layout: a positional vector indexed by `VarId.0`. Two reasons this beats
482/// the previous `BTreeMap<VarId, RowEntry>`:
483///
484/// 1. **Cheaper clone.** Per-row clone is on the hottest path of the executor
485///    (every filter, projection, expand, optional-match). A `BTreeMap` clone
486///    allocates one tree node per entry; a `SmallVec` clone is a single
487///    `memcpy` (or zero allocations when the row fits inline).
488/// 2. **O(1) lookup.** `VarId`s are dense `u32`s minted from 0 by the
489///    analyzer's `SymbolTable` (lora-analyzer/src/symbols.rs:23), so the
490///    positional index is exact and tight.
491///
492/// `entries[i] == None` means "VarId(i) is unset"; the cached `len_set`
493/// counter keeps `len()` O(1) without scanning. The inline capacity (`8`)
494/// covers typical query rows without touching the heap.
495#[derive(Debug, Clone, Default, PartialEq)]
496pub struct Row {
497    entries: smallvec::SmallVec<Option<RowEntry>, 8>,
498    len_set: u32,
499}
500
501impl Serialize for Row {
502    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
503    where
504        S: Serializer,
505    {
506        let mut ser_map = serializer.serialize_map(Some(self.len()))?;
507        for entry in self.entries.iter().flatten() {
508            match &entry.name {
509                Some(name) => ser_map.serialize_entry(&**name, entry.value.get())?,
510                None => {
511                    let fallback = format!("_{}", entry.var);
512                    ser_map.serialize_entry(fallback.as_str(), entry.value.get())?;
513                }
514            }
515        }
516        ser_map.end()
517    }
518}
519
520impl Row {
521    pub fn new() -> Self {
522        Self::default()
523    }
524
525    #[inline]
526    pub fn get(&self, key: VarId) -> Option<&LoraValue> {
527        self.slot(key).map(SlotValue::get)
528    }
529
530    /// Returns the column name for `key`, generating the `_{key}` fallback
531    /// on demand for entries inserted without an explicit name.
532    pub fn get_name(&self, key: VarId) -> Option<String> {
533        self.entries
534            .get(key.0 as usize)
535            .and_then(|slot| slot.as_ref())
536            .map(|entry| match &entry.name {
537                Some(n) => n.to_string(),
538                None => format!("_{}", entry.var),
539            })
540    }
541
542    #[inline]
543    pub fn insert(&mut self, key: VarId, value: LoraValue) {
544        self.set_value(key, SlotValue::new(value));
545    }
546
547    #[inline]
548    pub fn insert_named(&mut self, key: VarId, name: impl Into<Arc<str>>, value: LoraValue) {
549        self.set_named(key, name.into(), SlotValue::new(value));
550    }
551
552    /// [`Self::insert`] that never shares the value: for a binding rebound
553    /// per element of a list construct (`reduce`, a comprehension or
554    /// quantifier variable), where the row is reused rather than cloned.
555    #[inline]
556    pub fn insert_inline(&mut self, key: VarId, value: LoraValue) {
557        self.set_value(key, SlotValue::Inline(value));
558    }
559
560    /// [`Self::insert_named`] that never shares the value: for a row about to
561    /// leave the executor (hydrated output), where no clone follows and the
562    /// `Arc` would be an allocation per row for nothing.
563    pub fn insert_named_inline(&mut self, key: VarId, name: impl Into<Arc<str>>, value: LoraValue) {
564        self.set_named(key, name.into(), SlotValue::Inline(value));
565    }
566
567    /// Bind `key` (named `name`) to the value `source` holds for `from`,
568    /// sharing a large value instead of copying it. Returns `false`, and
569    /// changes nothing, when `from` is unset in `source`.
570    pub fn insert_named_from(
571        &mut self,
572        key: VarId,
573        name: impl Into<Arc<str>>,
574        source: &Row,
575        from: VarId,
576    ) -> bool {
577        let Some(value) = source.slot(from).cloned() else {
578            return false;
579        };
580        self.set_named(key, name.into(), value);
581        true
582    }
583
584    /// [`Self::insert_named_from`] with the row itself as the source.
585    pub fn insert_named_from_self(
586        &mut self,
587        key: VarId,
588        name: impl Into<Arc<str>>,
589        from: VarId,
590    ) -> bool {
591        let Some(value) = self.slot(from).cloned() else {
592            return false;
593        };
594        self.set_named(key, name.into(), value);
595        true
596    }
597
598    #[inline]
599    fn slot(&self, key: VarId) -> Option<&SlotValue> {
600        self.entries
601            .get(key.0 as usize)
602            .and_then(|slot| slot.as_ref())
603            .map(|entry| &entry.value)
604    }
605
606    /// Set `key`'s value, keeping any explicit name already stored;
607    /// otherwise leave the name as `None` so the fallback is produced lazily.
608    #[inline]
609    fn set_value(&mut self, key: VarId, value: SlotValue) {
610        let idx = self.ensure_slot(key);
611        match &mut self.entries[idx] {
612            Some(existing) => existing.value = value,
613            slot @ None => {
614                *slot = Some(RowEntry {
615                    var: key,
616                    name: None,
617                    value,
618                });
619                self.len_set += 1;
620            }
621        }
622    }
623
624    #[inline]
625    fn set_named(&mut self, key: VarId, name: Arc<str>, value: SlotValue) {
626        let idx = self.ensure_slot(key);
627        let was_set = self.entries[idx].is_some();
628        self.entries[idx] = Some(RowEntry {
629            var: key,
630            name: Some(name),
631            value,
632        });
633        if !was_set {
634            self.len_set += 1;
635        }
636    }
637
638    pub fn extend_from(&mut self, other: &Row) {
639        // Mirrors the previous `BTreeMap::insert` semantics: every set entry
640        // in `other` overwrites the slot wholesale (name and value), no merge.
641        for entry in other.entries.iter().flatten() {
642            let idx = self.ensure_slot(entry.var);
643            let was_set = self.entries[idx].is_some();
644            self.entries[idx] = Some(entry.clone());
645            if !was_set {
646                self.len_set += 1;
647            }
648        }
649    }
650
651    /// Copy every entry of `other` whose variable is unset in `self`,
652    /// keeping its name as stored (shared `Arc`, or none for anonymous
653    /// variables) rather than materializing the `_{key}` fallback.
654    pub fn fill_missing_from(&mut self, other: &Row) {
655        for entry in other.entries.iter().flatten() {
656            let idx = self.ensure_slot(entry.var);
657            if self.entries[idx].is_none() {
658                self.entries[idx] = Some(entry.clone());
659                self.len_set += 1;
660            }
661        }
662    }
663
664    pub fn iter(&self) -> impl Iterator<Item = (&VarId, &LoraValue)> {
665        self.entries
666            .iter()
667            .flatten()
668            .map(|entry| (&entry.var, entry.value.get()))
669    }
670
671    /// Iterate `(key, name, value)`. The name is a `Cow`: borrowed when an
672    /// explicit name was stored, and owned (lazily formatted as `_{key}`) for
673    /// entries inserted via the anonymous `insert()` path.
674    pub fn iter_named(
675        &self,
676    ) -> impl Iterator<Item = (&VarId, std::borrow::Cow<'_, str>, &LoraValue)> {
677        self.entries.iter().flatten().map(|entry| {
678            let name: std::borrow::Cow<'_, str> = match &entry.name {
679                Some(n) => std::borrow::Cow::Borrowed(&**n),
680                None => std::borrow::Cow::Owned(format!("_{}", entry.var)),
681            };
682            (&entry.var, name, entry.value.get())
683        })
684    }
685
686    /// Consume the row and yield owned `(VarId, name, LoraValue)` triples.
687    /// Used by hydrate_row to avoid cloning values on the projection hot path;
688    /// names come back as the shared `Arc<str>` so re-inserting them into a
689    /// new row costs a refcount, not an allocation.
690    pub fn into_iter_named(self) -> impl Iterator<Item = (VarId, Arc<str>, LoraValue)> {
691        self.entries.into_iter().flatten().map(|entry| {
692            let RowEntry { var, name, value } = entry;
693            (
694                var,
695                name.unwrap_or_else(|| Arc::from(format!("_{var}"))),
696                value.into_value(),
697            )
698        })
699    }
700
701    pub fn len(&self) -> usize {
702        self.len_set as usize
703    }
704
705    pub fn is_empty(&self) -> bool {
706        self.len_set == 0
707    }
708
709    pub fn contains_key(&self, key: VarId) -> bool {
710        self.entries
711            .get(key.0 as usize)
712            .is_some_and(|slot| slot.is_some())
713    }
714
715    /// Grow `entries` so that index `key.0` is in-range. Returns that index.
716    /// New slots are filled with `None` (counted as unset by `len_set`).
717    fn ensure_slot(&mut self, key: VarId) -> usize {
718        let idx = key.0 as usize;
719        if idx >= self.entries.len() {
720            self.entries.resize_with(idx + 1, || None);
721        }
722        idx
723    }
724}
725
726#[derive(Debug, Clone, Copy, PartialEq, Eq)]
727pub enum ResultFormat {
728    Rows,
729    RowArrays,
730    Graph,
731    Combined,
732}
733
734#[derive(Debug, Clone, Copy, PartialEq, Eq)]
735pub struct ExecuteOptions {
736    pub format: ResultFormat,
737}
738
739impl Default for ExecuteOptions {
740    fn default() -> Self {
741        Self {
742            format: ResultFormat::Graph,
743        }
744    }
745}
746
747#[derive(Debug, Clone, Serialize)]
748#[serde(untagged)]
749pub enum QueryResult {
750    Rows(RowsResult),
751    RowArrays(RowArraysResult),
752    Graph(GraphResult),
753    Combined(CombinedResult),
754}
755
756#[derive(Debug, Clone, Serialize)]
757pub struct RowsResult {
758    pub rows: Vec<Row>,
759}
760
761#[derive(Debug, Clone, Serialize)]
762pub struct RowArraysResult {
763    pub columns: Vec<String>,
764    pub rows: Vec<Vec<LoraValue>>,
765}
766
767#[derive(Debug, Clone, Serialize)]
768pub struct GraphResult {
769    pub graph: HydratedGraph,
770}
771
772#[derive(Debug, Clone, Serialize)]
773pub struct CombinedResult {
774    pub columns: Vec<String>,
775    pub data: Vec<CombinedRow>,
776    pub graph: HydratedGraph,
777}
778
779#[derive(Debug, Clone, Serialize)]
780pub struct CombinedRow {
781    pub row: Vec<LoraValue>,
782}
783
784#[derive(Debug, Clone, Serialize, Default)]
785pub struct HydratedGraph {
786    pub nodes: Vec<HydratedNode>,
787    pub relationships: Vec<HydratedRelationship>,
788}
789
790#[derive(Debug, Clone, Serialize, PartialEq)]
791pub struct HydratedNode {
792    pub id: i64,
793    pub labels: Vec<String>,
794    pub properties: BTreeMap<String, LoraValue>,
795}
796
797#[derive(Debug, Clone, Serialize, PartialEq)]
798pub struct HydratedRelationship {
799    pub id: i64,
800    #[serde(rename = "startId")]
801    pub start_id: i64,
802    #[serde(rename = "endId")]
803    pub end_id: i64,
804    #[serde(rename = "type")]
805    pub rel_type: String,
806    pub properties: BTreeMap<String, LoraValue>,
807}
808
809pub fn project_rows(rows: Vec<Row>, options: ExecuteOptions) -> QueryResult {
810    match options.format {
811        ResultFormat::Rows => QueryResult::Rows(RowsResult { rows }),
812
813        ResultFormat::RowArrays => {
814            let columns = infer_columns(&rows);
815            let projected_rows = rows.iter().map(|row| row_to_array(row, &columns)).collect();
816
817            QueryResult::RowArrays(RowArraysResult {
818                columns,
819                rows: projected_rows,
820            })
821        }
822
823        ResultFormat::Graph => QueryResult::Graph(GraphResult {
824            graph: collect_hydrated_graph(&rows),
825        }),
826
827        ResultFormat::Combined => {
828            let columns = infer_columns(&rows);
829            let data = rows
830                .iter()
831                .map(|row| CombinedRow {
832                    row: row_to_array(row, &columns),
833                })
834                .collect();
835
836            QueryResult::Combined(CombinedResult {
837                columns,
838                data,
839                graph: collect_hydrated_graph(&rows),
840            })
841        }
842    }
843}
844
845fn infer_columns(rows: &[Row]) -> Vec<String> {
846    rows.first()
847        .map(|row| {
848            row.iter_named()
849                .map(|(_, name, _)| name.into_owned())
850                .collect::<Vec<_>>()
851        })
852        .unwrap_or_default()
853}
854
855fn row_to_array(row: &Row, columns: &[String]) -> Vec<LoraValue> {
856    // Row entry count is small; a linear scan per column avoids allocating
857    // owned names into an intermediate lookup map.
858    columns
859        .iter()
860        .map(|col| {
861            row.iter_named()
862                .find(|(_, name, _)| name.as_ref() == col.as_str())
863                .map(|(_, _, v)| v.clone())
864                .unwrap_or(LoraValue::Null)
865        })
866        .collect()
867}
868
869fn collect_hydrated_graph(rows: &[Row]) -> HydratedGraph {
870    let mut nodes = BTreeMap::<i64, HydratedNode>::new();
871    let mut relationships = BTreeMap::<i64, HydratedRelationship>::new();
872
873    for row in rows {
874        for (_, _, value) in row.iter_named() {
875            collect_graph_from_value(value, &mut nodes, &mut relationships);
876        }
877    }
878
879    HydratedGraph {
880        nodes: nodes.into_values().collect(),
881        relationships: relationships.into_values().collect(),
882    }
883}
884
885fn collect_graph_from_value(
886    value: &LoraValue,
887    nodes: &mut BTreeMap<i64, HydratedNode>,
888    relationships: &mut BTreeMap<i64, HydratedRelationship>,
889) {
890    match value {
891        LoraValue::List(values) => {
892            for value in values {
893                collect_graph_from_value(value, nodes, relationships);
894            }
895        }
896
897        LoraValue::Map(map) => {
898            if let Some(node) = try_as_hydrated_node(map) {
899                nodes.entry(node.id).or_insert(node);
900                return;
901            }
902
903            if let Some(rel) = try_as_hydrated_relationship(map) {
904                relationships.entry(rel.id).or_insert(rel);
905                return;
906            }
907
908            for value in map.values() {
909                collect_graph_from_value(value, nodes, relationships);
910            }
911        }
912
913        _ => {}
914    }
915}
916
917fn try_as_hydrated_node(map: &BTreeMap<String, LoraValue>) -> Option<HydratedNode> {
918    let id = match map.get("id")? {
919        LoraValue::Int(v) => *v,
920        _ => return None,
921    };
922
923    let labels = match map.get("labels")? {
924        LoraValue::List(values) => values
925            .iter()
926            .map(|v| match v {
927                LoraValue::String(s) => Some(s.clone()),
928                _ => None,
929            })
930            .collect::<Option<Vec<_>>>()?,
931        _ => return None,
932    };
933
934    let properties = match map.get("properties")? {
935        LoraValue::Map(props) => props.clone(),
936        _ => return None,
937    };
938
939    Some(HydratedNode {
940        id,
941        labels,
942        properties,
943    })
944}
945
946fn try_as_hydrated_relationship(map: &BTreeMap<String, LoraValue>) -> Option<HydratedRelationship> {
947    match map.get("kind") {
948        Some(LoraValue::String(kind)) if kind == "relationship" => {}
949        _ => return None,
950    }
951
952    let id = match map.get("id")? {
953        LoraValue::Int(v) => *v,
954        _ => return None,
955    };
956
957    let start_id = match map.get("startId").or_else(|| map.get("src"))? {
958        LoraValue::Int(v) => *v,
959        _ => return None,
960    };
961
962    let end_id = match map.get("endId").or_else(|| map.get("dst"))? {
963        LoraValue::Int(v) => *v,
964        _ => return None,
965    };
966
967    let rel_type = match map.get("type")? {
968        LoraValue::String(s) => s.clone(),
969        _ => return None,
970    };
971
972    let properties = match map.get("properties")? {
973        LoraValue::Map(props) => props.clone(),
974        _ => return None,
975    };
976
977    Some(HydratedRelationship {
978        id,
979        start_id,
980        end_id,
981        rel_type,
982        properties,
983    })
984}
985
986#[cfg(test)]
987mod tests {
988    use super::*;
989
990    fn big() -> LoraValue {
991        LoraValue::List((0..100).map(LoraValue::Int).collect())
992    }
993
994    #[test]
995    fn a_slot_is_no_larger_than_its_value() {
996        // The `Shared` variant fits in `LoraValue`'s niche: rows stay as
997        // compact as before, and clone as one memcpy for inline values.
998        assert_eq!(
999            std::mem::size_of::<SlotValue>(),
1000            std::mem::size_of::<LoraValue>()
1001        );
1002    }
1003
1004    #[test]
1005    fn a_cloned_row_shares_a_large_value_and_copies_a_small_one() {
1006        let mut row = Row::new();
1007        row.insert(VarId(0), big());
1008        row.insert(VarId(1), LoraValue::List(vec![LoraValue::Int(1)]));
1009        let copy = row.clone();
1010        assert!(std::ptr::eq(
1011            row.get(VarId(0)).unwrap(),
1012            copy.get(VarId(0)).unwrap()
1013        ));
1014        assert!(!std::ptr::eq(
1015            row.get(VarId(1)).unwrap(),
1016            copy.get(VarId(1)).unwrap()
1017        ));
1018        assert_eq!(row, copy);
1019    }
1020
1021    #[test]
1022    fn a_shared_value_is_handed_out_whole_by_each_owner() {
1023        let mut row = Row::new();
1024        row.insert_named(VarId(0), "big", big());
1025        let mut projected = Row::new();
1026        assert!(projected.insert_named_from(VarId(1), "alias", &row, VarId(0)));
1027        assert!(!projected.insert_named_from(VarId(2), "missing", &row, VarId(5)));
1028        assert!(std::ptr::eq(
1029            row.get(VarId(0)).unwrap(),
1030            projected.get(VarId(1)).unwrap()
1031        ));
1032        assert_eq!(projected.len(), 1);
1033        let (_, name, value) = projected.into_iter_named().next().unwrap();
1034        assert_eq!(&*name, "alias");
1035        assert_eq!(value, big());
1036        // The original still holds its own copy.
1037        assert_eq!(row.get(VarId(0)), Some(&big()));
1038    }
1039}