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 borrowed `PropertyValue` in a single walk. Lets
229/// callers that already hold `&PropertyValue` (property lookups on borrowed
230/// records) skip the `prop.clone().into()` double-traversal.
231impl From<&PropertyValue> for LoraValue {
232    fn from(value: &PropertyValue) -> Self {
233        match value {
234            PropertyValue::Null => LoraValue::Null,
235            PropertyValue::Bool(v) => LoraValue::Bool(*v),
236            PropertyValue::Int(v) => LoraValue::Int(*v),
237            PropertyValue::Float(v) => LoraValue::Float(*v),
238            PropertyValue::String(v) => LoraValue::String(v.clone()),
239            PropertyValue::Binary(v) => LoraValue::Binary(v.clone()),
240            PropertyValue::List(values) => {
241                LoraValue::List(values.iter().map(LoraValue::from).collect())
242            }
243            PropertyValue::Map(map) => LoraValue::Map(
244                map.iter()
245                    .map(|(k, v)| (k.clone(), LoraValue::from(v)))
246                    .collect(),
247            ),
248            PropertyValue::Date(d) => LoraValue::Date(d.clone()),
249            PropertyValue::Time(t) => LoraValue::Time(t.clone()),
250            PropertyValue::LocalTime(t) => LoraValue::LocalTime(t.clone()),
251            PropertyValue::DateTime(dt) => LoraValue::DateTime(dt.clone()),
252            PropertyValue::LocalDateTime(dt) => LoraValue::LocalDateTime(dt.clone()),
253            PropertyValue::Duration(dur) => LoraValue::Duration(dur.clone()),
254            PropertyValue::Point(p) => LoraValue::Point(p.clone()),
255            PropertyValue::Vector(v) => LoraValue::Vector(v.clone()),
256        }
257    }
258}
259
260impl From<LoraValue> for PropertyValue {
261    fn from(value: LoraValue) -> Self {
262        match value {
263            LoraValue::Null => PropertyValue::Null,
264            LoraValue::Bool(v) => PropertyValue::Bool(v),
265            LoraValue::Int(v) => PropertyValue::Int(v),
266            LoraValue::Float(v) => PropertyValue::Float(v),
267            LoraValue::String(v) => PropertyValue::String(v),
268            LoraValue::Binary(v) => PropertyValue::Binary(v),
269            LoraValue::List(values) => {
270                PropertyValue::List(values.into_iter().map(PropertyValue::from).collect())
271            }
272            LoraValue::Map(map) => PropertyValue::Map(
273                map.into_iter()
274                    .map(|(k, v)| (k, PropertyValue::from(v)))
275                    .collect(),
276            ),
277            LoraValue::Node(id) => PropertyValue::String(format!("node:{id}")),
278            LoraValue::Relationship(id) => PropertyValue::String(format!("rel:{id}")),
279            LoraValue::Path(_) => PropertyValue::Null,
280            LoraValue::Date(d) => PropertyValue::Date(d),
281            LoraValue::Time(t) => PropertyValue::Time(t),
282            LoraValue::LocalTime(t) => PropertyValue::LocalTime(t),
283            LoraValue::DateTime(dt) => PropertyValue::DateTime(dt),
284            LoraValue::LocalDateTime(dt) => PropertyValue::LocalDateTime(dt),
285            LoraValue::Duration(dur) => PropertyValue::Duration(dur),
286            LoraValue::Point(p) => PropertyValue::Point(p),
287            LoraValue::Vector(v) => PropertyValue::Vector(v),
288        }
289    }
290}
291
292/// Errors that can arise when converting a `LoraValue` into a
293/// `PropertyValue` for storage on a node or relationship.
294#[derive(Debug, Clone, PartialEq)]
295pub enum PropertyConversionError {
296    /// A list entry contained a VECTOR value. Vectors are first-class
297    /// properties themselves but they cannot be nested inside lists.
298    NestedVectorInList,
299    /// Produced when something that cannot appear on disk (e.g. a `Path`
300    /// value captured by mistake) is asked to be converted — surfaced so
301    /// callers can reject it instead of silently stringifying.
302    #[allow(dead_code)]
303    UnsupportedKind(&'static str),
304}
305
306impl std::fmt::Display for PropertyConversionError {
307    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
308        match self {
309            PropertyConversionError::NestedVectorInList => {
310                write!(f, "lists stored as properties cannot contain VECTOR values")
311            }
312            PropertyConversionError::UnsupportedKind(kind) => {
313                write!(f, "cannot store {kind} as a property")
314            }
315        }
316    }
317}
318
319impl std::error::Error for PropertyConversionError {}
320
321/// Fallible conversion used on every write path
322/// (`set_property_from_expr`, `overwrite_entity_target`,
323/// `mutate_entity_target`, `eval_properties_expr`, plus CREATE /
324/// MERGE). Rejects VECTOR values nested inside lists at any depth —
325/// everything else falls through to the infallible `From`
326/// implementation above. A top-level VECTOR property is always fine;
327/// only LISTs that directly contain a VECTOR entry are rejected.
328pub fn lora_value_to_property(value: LoraValue) -> Result<PropertyValue, PropertyConversionError> {
329    /// Visit every nested value and, whenever we cross a `List`, flag the
330    /// `Vector` entries it directly contains. We still recurse through
331    /// `Map` and other `List` values so a vector buried under
332    /// `{inner: [vector(...)]}` is caught too.
333    fn visit(value: &LoraValue, inside_list: bool) -> Result<(), PropertyConversionError> {
334        match value {
335            LoraValue::Vector(_) if inside_list => Err(PropertyConversionError::NestedVectorInList),
336            LoraValue::List(items) => {
337                for item in items {
338                    visit(item, true)?;
339                }
340                Ok(())
341            }
342            LoraValue::Map(m) => {
343                for v in m.values() {
344                    visit(v, inside_list)?;
345                }
346                Ok(())
347            }
348            _ => Ok(()),
349        }
350    }
351
352    visit(&value, false)?;
353    Ok(PropertyValue::from(value))
354}
355
356/// A row slot's value. Large values sit behind an `Arc`, so cloning the row
357/// (every expand candidate, OPTIONAL MATCH merge, UNWIND element, FOREACH
358/// iteration) bumps a refcount instead of copying a list the row only carries
359/// (E-4: a 10k-element list made a 200-row hop ~250x slower). Small values
360/// stay inline: an `Arc` in every slot costs an allocation per insert and a
361/// pointer chase per read, 10-16% on hot paths.
362#[derive(Debug, Clone)]
363enum SlotValue {
364    Inline(LoraValue),
365    Shared(Arc<LoraValue>),
366}
367
368impl SlotValue {
369    #[inline]
370    fn new(value: LoraValue) -> Self {
371        // Ids, scalars and strings, most of what rows hold, are decided
372        // here; only containers walk `is_large`.
373        let large = match &value {
374            LoraValue::String(s) => s.len() >= LARGE_BYTES,
375            LoraValue::List(_)
376            | LoraValue::Map(_)
377            | LoraValue::Path(_)
378            | LoraValue::Binary(_)
379            | LoraValue::Vector(_) => is_large(&value),
380            _ => false,
381        };
382        if large {
383            SlotValue::Shared(Arc::new(value))
384        } else {
385            SlotValue::Inline(value)
386        }
387    }
388
389    #[inline]
390    fn get(&self) -> &LoraValue {
391        match self {
392            SlotValue::Inline(v) => v,
393            SlotValue::Shared(v) => v,
394        }
395    }
396
397    #[inline]
398    fn into_value(self) -> LoraValue {
399        match self {
400            SlotValue::Inline(v) => v,
401            SlotValue::Shared(v) => Arc::try_unwrap(v).unwrap_or_else(|v| (*v).clone()),
402        }
403    }
404}
405
406/// A string at least this long is shared rather than copied.
407const LARGE_BYTES: usize = 256;
408
409impl PartialEq for SlotValue {
410    fn eq(&self, other: &Self) -> bool {
411        self.get() == other.get()
412    }
413}
414
415/// Whether copying `value` costs enough for a row to share it: a list, map
416/// or path with 8+ entries or a nested container, a long string, a binary or
417/// a vector. Looks at no more than 8 elements.
418fn is_large(value: &LoraValue) -> bool {
419    const ENTRIES: usize = 8;
420    let heavy = |v: &LoraValue| match v {
421        LoraValue::List(_)
422        | LoraValue::Map(_)
423        | LoraValue::Path(_)
424        | LoraValue::Vector(_)
425        | LoraValue::Binary(_) => true,
426        LoraValue::String(s) => s.len() >= LARGE_BYTES,
427        _ => false,
428    };
429    match value {
430        LoraValue::List(items) => items.len() >= ENTRIES || items.iter().any(heavy),
431        LoraValue::Map(map) => map.len() >= ENTRIES || map.values().any(heavy),
432        LoraValue::String(s) => s.len() >= LARGE_BYTES,
433        LoraValue::Path(p) => p.nodes.len() >= ENTRIES,
434        LoraValue::Binary(_) | LoraValue::Vector(_) => true,
435        _ => false,
436    }
437}
438
439#[derive(Debug, Clone, PartialEq)]
440struct RowEntry {
441    /// Stored alongside the value so iterators can hand back `&VarId` while
442    /// the slot's position in `entries` remains the source of truth for
443    /// lookups.
444    var: VarId,
445    /// `None` means "use the fallback `_{key}` lazily". This avoids allocating
446    /// a String for every anonymous variable on the insert hot path.
447    ///
448    /// Shared `Arc<str>` rather than `String`: column names are fixed per
449    /// operator, so producers mint one `Arc` per column and every row (and
450    /// every row clone) holds a refcount instead of a private heap copy.
451    name: Option<Arc<str>>,
452    value: SlotValue,
453}
454
455/// Row layout: a positional vector indexed by `VarId.0`. Two reasons this beats
456/// the previous `BTreeMap<VarId, RowEntry>`:
457///
458/// 1. **Cheaper clone.** Per-row clone is on the hottest path of the executor
459///    (every filter, projection, expand, optional-match). A `BTreeMap` clone
460///    allocates one tree node per entry; a `SmallVec` clone is a single
461///    `memcpy` (or zero allocations when the row fits inline).
462/// 2. **O(1) lookup.** `VarId`s are dense `u32`s minted from 0 by the
463///    analyzer's `SymbolTable` (lora-analyzer/src/symbols.rs:23), so the
464///    positional index is exact and tight.
465///
466/// `entries[i] == None` means "VarId(i) is unset"; the cached `len_set`
467/// counter keeps `len()` O(1) without scanning. The inline capacity (`8`)
468/// covers typical query rows without touching the heap.
469#[derive(Debug, Clone, Default, PartialEq)]
470pub struct Row {
471    entries: smallvec::SmallVec<Option<RowEntry>, 8>,
472    len_set: u32,
473}
474
475impl Serialize for Row {
476    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
477    where
478        S: Serializer,
479    {
480        let mut ser_map = serializer.serialize_map(Some(self.len()))?;
481        for entry in self.entries.iter().flatten() {
482            match &entry.name {
483                Some(name) => ser_map.serialize_entry(&**name, entry.value.get())?,
484                None => {
485                    let fallback = format!("_{}", entry.var);
486                    ser_map.serialize_entry(fallback.as_str(), entry.value.get())?;
487                }
488            }
489        }
490        ser_map.end()
491    }
492}
493
494impl Row {
495    pub fn new() -> Self {
496        Self::default()
497    }
498
499    #[inline]
500    pub fn get(&self, key: VarId) -> Option<&LoraValue> {
501        self.slot(key).map(SlotValue::get)
502    }
503
504    /// Returns the column name for `key`, generating the `_{key}` fallback
505    /// on demand for entries inserted without an explicit name.
506    pub fn get_name(&self, key: VarId) -> Option<String> {
507        self.entries
508            .get(key.0 as usize)
509            .and_then(|slot| slot.as_ref())
510            .map(|entry| match &entry.name {
511                Some(n) => n.to_string(),
512                None => format!("_{}", entry.var),
513            })
514    }
515
516    #[inline]
517    pub fn insert(&mut self, key: VarId, value: LoraValue) {
518        self.set_value(key, SlotValue::new(value));
519    }
520
521    #[inline]
522    pub fn insert_named(&mut self, key: VarId, name: impl Into<Arc<str>>, value: LoraValue) {
523        self.set_named(key, name.into(), SlotValue::new(value));
524    }
525
526    /// [`Self::insert`] that never shares the value: for a binding rebound
527    /// per element of a list construct (`reduce`, a comprehension or
528    /// quantifier variable), where the row is reused rather than cloned.
529    #[inline]
530    pub fn insert_inline(&mut self, key: VarId, value: LoraValue) {
531        self.set_value(key, SlotValue::Inline(value));
532    }
533
534    /// [`Self::insert_named`] that never shares the value: for a row about to
535    /// leave the executor (hydrated output), where no clone follows and the
536    /// `Arc` would be an allocation per row for nothing.
537    pub fn insert_named_inline(&mut self, key: VarId, name: impl Into<Arc<str>>, value: LoraValue) {
538        self.set_named(key, name.into(), SlotValue::Inline(value));
539    }
540
541    /// Bind `key` (named `name`) to the value `source` holds for `from`,
542    /// sharing a large value instead of copying it. Returns `false`, and
543    /// changes nothing, when `from` is unset in `source`.
544    pub fn insert_named_from(
545        &mut self,
546        key: VarId,
547        name: impl Into<Arc<str>>,
548        source: &Row,
549        from: VarId,
550    ) -> bool {
551        let Some(value) = source.slot(from).cloned() else {
552            return false;
553        };
554        self.set_named(key, name.into(), value);
555        true
556    }
557
558    /// [`Self::insert_named_from`] with the row itself as the source.
559    pub fn insert_named_from_self(
560        &mut self,
561        key: VarId,
562        name: impl Into<Arc<str>>,
563        from: VarId,
564    ) -> bool {
565        let Some(value) = self.slot(from).cloned() else {
566            return false;
567        };
568        self.set_named(key, name.into(), value);
569        true
570    }
571
572    #[inline]
573    fn slot(&self, key: VarId) -> Option<&SlotValue> {
574        self.entries
575            .get(key.0 as usize)
576            .and_then(|slot| slot.as_ref())
577            .map(|entry| &entry.value)
578    }
579
580    /// Set `key`'s value, keeping any explicit name already stored;
581    /// otherwise leave the name as `None` so the fallback is produced lazily.
582    #[inline]
583    fn set_value(&mut self, key: VarId, value: SlotValue) {
584        let idx = self.ensure_slot(key);
585        match &mut self.entries[idx] {
586            Some(existing) => existing.value = value,
587            slot @ None => {
588                *slot = Some(RowEntry {
589                    var: key,
590                    name: None,
591                    value,
592                });
593                self.len_set += 1;
594            }
595        }
596    }
597
598    #[inline]
599    fn set_named(&mut self, key: VarId, name: Arc<str>, value: SlotValue) {
600        let idx = self.ensure_slot(key);
601        let was_set = self.entries[idx].is_some();
602        self.entries[idx] = Some(RowEntry {
603            var: key,
604            name: Some(name),
605            value,
606        });
607        if !was_set {
608            self.len_set += 1;
609        }
610    }
611
612    pub fn extend_from(&mut self, other: &Row) {
613        // Mirrors the previous `BTreeMap::insert` semantics: every set entry
614        // in `other` overwrites the slot wholesale (name and value), no merge.
615        for entry in other.entries.iter().flatten() {
616            let idx = self.ensure_slot(entry.var);
617            let was_set = self.entries[idx].is_some();
618            self.entries[idx] = Some(entry.clone());
619            if !was_set {
620                self.len_set += 1;
621            }
622        }
623    }
624
625    /// Copy every entry of `other` whose variable is unset in `self`,
626    /// keeping its name as stored (shared `Arc`, or none for anonymous
627    /// variables) rather than materializing the `_{key}` fallback.
628    pub fn fill_missing_from(&mut self, other: &Row) {
629        for entry in other.entries.iter().flatten() {
630            let idx = self.ensure_slot(entry.var);
631            if self.entries[idx].is_none() {
632                self.entries[idx] = Some(entry.clone());
633                self.len_set += 1;
634            }
635        }
636    }
637
638    pub fn iter(&self) -> impl Iterator<Item = (&VarId, &LoraValue)> {
639        self.entries
640            .iter()
641            .flatten()
642            .map(|entry| (&entry.var, entry.value.get()))
643    }
644
645    /// Iterate `(key, name, value)`. The name is a `Cow`: borrowed when an
646    /// explicit name was stored, and owned (lazily formatted as `_{key}`) for
647    /// entries inserted via the anonymous `insert()` path.
648    pub fn iter_named(
649        &self,
650    ) -> impl Iterator<Item = (&VarId, std::borrow::Cow<'_, str>, &LoraValue)> {
651        self.entries.iter().flatten().map(|entry| {
652            let name: std::borrow::Cow<'_, str> = match &entry.name {
653                Some(n) => std::borrow::Cow::Borrowed(&**n),
654                None => std::borrow::Cow::Owned(format!("_{}", entry.var)),
655            };
656            (&entry.var, name, entry.value.get())
657        })
658    }
659
660    /// Consume the row and yield owned `(VarId, name, LoraValue)` triples.
661    /// Used by hydrate_row to avoid cloning values on the projection hot path;
662    /// names come back as the shared `Arc<str>` so re-inserting them into a
663    /// new row costs a refcount, not an allocation.
664    pub fn into_iter_named(self) -> impl Iterator<Item = (VarId, Arc<str>, LoraValue)> {
665        self.entries.into_iter().flatten().map(|entry| {
666            let RowEntry { var, name, value } = entry;
667            (
668                var,
669                name.unwrap_or_else(|| Arc::from(format!("_{var}"))),
670                value.into_value(),
671            )
672        })
673    }
674
675    pub fn len(&self) -> usize {
676        self.len_set as usize
677    }
678
679    pub fn is_empty(&self) -> bool {
680        self.len_set == 0
681    }
682
683    pub fn contains_key(&self, key: VarId) -> bool {
684        self.entries
685            .get(key.0 as usize)
686            .is_some_and(|slot| slot.is_some())
687    }
688
689    /// Grow `entries` so that index `key.0` is in-range. Returns that index.
690    /// New slots are filled with `None` (counted as unset by `len_set`).
691    fn ensure_slot(&mut self, key: VarId) -> usize {
692        let idx = key.0 as usize;
693        if idx >= self.entries.len() {
694            self.entries.resize_with(idx + 1, || None);
695        }
696        idx
697    }
698}
699
700#[derive(Debug, Clone, Copy, PartialEq, Eq)]
701pub enum ResultFormat {
702    Rows,
703    RowArrays,
704    Graph,
705    Combined,
706}
707
708#[derive(Debug, Clone, Copy, PartialEq, Eq)]
709pub struct ExecuteOptions {
710    pub format: ResultFormat,
711}
712
713impl Default for ExecuteOptions {
714    fn default() -> Self {
715        Self {
716            format: ResultFormat::Graph,
717        }
718    }
719}
720
721#[derive(Debug, Clone, Serialize)]
722#[serde(untagged)]
723pub enum QueryResult {
724    Rows(RowsResult),
725    RowArrays(RowArraysResult),
726    Graph(GraphResult),
727    Combined(CombinedResult),
728}
729
730#[derive(Debug, Clone, Serialize)]
731pub struct RowsResult {
732    pub rows: Vec<Row>,
733}
734
735#[derive(Debug, Clone, Serialize)]
736pub struct RowArraysResult {
737    pub columns: Vec<String>,
738    pub rows: Vec<Vec<LoraValue>>,
739}
740
741#[derive(Debug, Clone, Serialize)]
742pub struct GraphResult {
743    pub graph: HydratedGraph,
744}
745
746#[derive(Debug, Clone, Serialize)]
747pub struct CombinedResult {
748    pub columns: Vec<String>,
749    pub data: Vec<CombinedRow>,
750    pub graph: HydratedGraph,
751}
752
753#[derive(Debug, Clone, Serialize)]
754pub struct CombinedRow {
755    pub row: Vec<LoraValue>,
756}
757
758#[derive(Debug, Clone, Serialize, Default)]
759pub struct HydratedGraph {
760    pub nodes: Vec<HydratedNode>,
761    pub relationships: Vec<HydratedRelationship>,
762}
763
764#[derive(Debug, Clone, Serialize, PartialEq)]
765pub struct HydratedNode {
766    pub id: i64,
767    pub labels: Vec<String>,
768    pub properties: BTreeMap<String, LoraValue>,
769}
770
771#[derive(Debug, Clone, Serialize, PartialEq)]
772pub struct HydratedRelationship {
773    pub id: i64,
774    #[serde(rename = "startId")]
775    pub start_id: i64,
776    #[serde(rename = "endId")]
777    pub end_id: i64,
778    #[serde(rename = "type")]
779    pub rel_type: String,
780    pub properties: BTreeMap<String, LoraValue>,
781}
782
783pub fn project_rows(rows: Vec<Row>, options: ExecuteOptions) -> QueryResult {
784    match options.format {
785        ResultFormat::Rows => QueryResult::Rows(RowsResult { rows }),
786
787        ResultFormat::RowArrays => {
788            let columns = infer_columns(&rows);
789            let projected_rows = rows.iter().map(|row| row_to_array(row, &columns)).collect();
790
791            QueryResult::RowArrays(RowArraysResult {
792                columns,
793                rows: projected_rows,
794            })
795        }
796
797        ResultFormat::Graph => QueryResult::Graph(GraphResult {
798            graph: collect_hydrated_graph(&rows),
799        }),
800
801        ResultFormat::Combined => {
802            let columns = infer_columns(&rows);
803            let data = rows
804                .iter()
805                .map(|row| CombinedRow {
806                    row: row_to_array(row, &columns),
807                })
808                .collect();
809
810            QueryResult::Combined(CombinedResult {
811                columns,
812                data,
813                graph: collect_hydrated_graph(&rows),
814            })
815        }
816    }
817}
818
819fn infer_columns(rows: &[Row]) -> Vec<String> {
820    rows.first()
821        .map(|row| {
822            row.iter_named()
823                .map(|(_, name, _)| name.into_owned())
824                .collect::<Vec<_>>()
825        })
826        .unwrap_or_default()
827}
828
829fn row_to_array(row: &Row, columns: &[String]) -> Vec<LoraValue> {
830    // Row entry count is small; a linear scan per column avoids allocating
831    // owned names into an intermediate lookup map.
832    columns
833        .iter()
834        .map(|col| {
835            row.iter_named()
836                .find(|(_, name, _)| name.as_ref() == col.as_str())
837                .map(|(_, _, v)| v.clone())
838                .unwrap_or(LoraValue::Null)
839        })
840        .collect()
841}
842
843fn collect_hydrated_graph(rows: &[Row]) -> HydratedGraph {
844    let mut nodes = BTreeMap::<i64, HydratedNode>::new();
845    let mut relationships = BTreeMap::<i64, HydratedRelationship>::new();
846
847    for row in rows {
848        for (_, _, value) in row.iter_named() {
849            collect_graph_from_value(value, &mut nodes, &mut relationships);
850        }
851    }
852
853    HydratedGraph {
854        nodes: nodes.into_values().collect(),
855        relationships: relationships.into_values().collect(),
856    }
857}
858
859fn collect_graph_from_value(
860    value: &LoraValue,
861    nodes: &mut BTreeMap<i64, HydratedNode>,
862    relationships: &mut BTreeMap<i64, HydratedRelationship>,
863) {
864    match value {
865        LoraValue::List(values) => {
866            for value in values {
867                collect_graph_from_value(value, nodes, relationships);
868            }
869        }
870
871        LoraValue::Map(map) => {
872            if let Some(node) = try_as_hydrated_node(map) {
873                nodes.entry(node.id).or_insert(node);
874                return;
875            }
876
877            if let Some(rel) = try_as_hydrated_relationship(map) {
878                relationships.entry(rel.id).or_insert(rel);
879                return;
880            }
881
882            for value in map.values() {
883                collect_graph_from_value(value, nodes, relationships);
884            }
885        }
886
887        _ => {}
888    }
889}
890
891fn try_as_hydrated_node(map: &BTreeMap<String, LoraValue>) -> Option<HydratedNode> {
892    let id = match map.get("id")? {
893        LoraValue::Int(v) => *v,
894        _ => return None,
895    };
896
897    let labels = match map.get("labels")? {
898        LoraValue::List(values) => values
899            .iter()
900            .map(|v| match v {
901                LoraValue::String(s) => Some(s.clone()),
902                _ => None,
903            })
904            .collect::<Option<Vec<_>>>()?,
905        _ => return None,
906    };
907
908    let properties = match map.get("properties")? {
909        LoraValue::Map(props) => props.clone(),
910        _ => return None,
911    };
912
913    Some(HydratedNode {
914        id,
915        labels,
916        properties,
917    })
918}
919
920fn try_as_hydrated_relationship(map: &BTreeMap<String, LoraValue>) -> Option<HydratedRelationship> {
921    match map.get("kind") {
922        Some(LoraValue::String(kind)) if kind == "relationship" => {}
923        _ => return None,
924    }
925
926    let id = match map.get("id")? {
927        LoraValue::Int(v) => *v,
928        _ => return None,
929    };
930
931    let start_id = match map.get("startId").or_else(|| map.get("src"))? {
932        LoraValue::Int(v) => *v,
933        _ => return None,
934    };
935
936    let end_id = match map.get("endId").or_else(|| map.get("dst"))? {
937        LoraValue::Int(v) => *v,
938        _ => return None,
939    };
940
941    let rel_type = match map.get("type")? {
942        LoraValue::String(s) => s.clone(),
943        _ => return None,
944    };
945
946    let properties = match map.get("properties")? {
947        LoraValue::Map(props) => props.clone(),
948        _ => return None,
949    };
950
951    Some(HydratedRelationship {
952        id,
953        start_id,
954        end_id,
955        rel_type,
956        properties,
957    })
958}
959
960#[cfg(test)]
961mod tests {
962    use super::*;
963
964    fn big() -> LoraValue {
965        LoraValue::List((0..100).map(LoraValue::Int).collect())
966    }
967
968    #[test]
969    fn a_slot_is_no_larger_than_its_value() {
970        // The `Shared` variant fits in `LoraValue`'s niche: rows stay as
971        // compact as before, and clone as one memcpy for inline values.
972        assert_eq!(
973            std::mem::size_of::<SlotValue>(),
974            std::mem::size_of::<LoraValue>()
975        );
976    }
977
978    #[test]
979    fn a_cloned_row_shares_a_large_value_and_copies_a_small_one() {
980        let mut row = Row::new();
981        row.insert(VarId(0), big());
982        row.insert(VarId(1), LoraValue::List(vec![LoraValue::Int(1)]));
983        let copy = row.clone();
984        assert!(std::ptr::eq(
985            row.get(VarId(0)).unwrap(),
986            copy.get(VarId(0)).unwrap()
987        ));
988        assert!(!std::ptr::eq(
989            row.get(VarId(1)).unwrap(),
990            copy.get(VarId(1)).unwrap()
991        ));
992        assert_eq!(row, copy);
993    }
994
995    #[test]
996    fn a_shared_value_is_handed_out_whole_by_each_owner() {
997        let mut row = Row::new();
998        row.insert_named(VarId(0), "big", big());
999        let mut projected = Row::new();
1000        assert!(projected.insert_named_from(VarId(1), "alias", &row, VarId(0)));
1001        assert!(!projected.insert_named_from(VarId(2), "missing", &row, VarId(5)));
1002        assert!(std::ptr::eq(
1003            row.get(VarId(0)).unwrap(),
1004            projected.get(VarId(1)).unwrap()
1005        ));
1006        assert_eq!(projected.len(), 1);
1007        let (_, name, value) = projected.into_iter_named().next().unwrap();
1008        assert_eq!(&*name, "alias");
1009        assert_eq!(value, big());
1010        // The original still holds its own copy.
1011        assert_eq!(row.get(VarId(0)), Some(&big()));
1012    }
1013}