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