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#[derive(Debug, Clone, PartialEq)]
357struct RowEntry {
358    /// Stored alongside the value so iterators can hand back `&VarId` while
359    /// the slot's position in `entries` remains the source of truth for
360    /// lookups.
361    var: VarId,
362    /// `None` means "use the fallback `_{key}` lazily". This avoids allocating
363    /// a String for every anonymous variable on the insert hot path.
364    ///
365    /// Shared `Arc<str>` rather than `String`: column names are fixed per
366    /// operator, so producers mint one `Arc` per column and every row (and
367    /// every row clone) holds a refcount instead of a private heap copy.
368    name: Option<Arc<str>>,
369    value: LoraValue,
370}
371
372/// Row layout: a positional vector indexed by `VarId.0`. Two reasons this beats
373/// the previous `BTreeMap<VarId, RowEntry>`:
374///
375/// 1. **Cheaper clone.** Per-row clone is on the hottest path of the executor
376///    (every filter, projection, expand, optional-match). A `BTreeMap` clone
377///    allocates one tree node per entry; a `SmallVec` clone is a single
378///    `memcpy` (or zero allocations when the row fits inline).
379/// 2. **O(1) lookup.** `VarId`s are dense `u32`s minted from 0 by the
380///    analyzer's `SymbolTable` (lora-analyzer/src/symbols.rs:23), so the
381///    positional index is exact and tight.
382///
383/// `entries[i] == None` means "VarId(i) is unset"; the cached `len_set`
384/// counter keeps `len()` O(1) without scanning. The inline capacity (`8`)
385/// covers typical query rows without touching the heap.
386#[derive(Debug, Clone, Default, PartialEq)]
387pub struct Row {
388    entries: smallvec::SmallVec<Option<RowEntry>, 8>,
389    len_set: u32,
390}
391
392impl Serialize for Row {
393    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
394    where
395        S: Serializer,
396    {
397        let mut ser_map = serializer.serialize_map(Some(self.len()))?;
398        for entry in self.entries.iter().flatten() {
399            match &entry.name {
400                Some(name) => ser_map.serialize_entry(&**name, &entry.value)?,
401                None => {
402                    let fallback = format!("_{}", entry.var);
403                    ser_map.serialize_entry(fallback.as_str(), &entry.value)?;
404                }
405            }
406        }
407        ser_map.end()
408    }
409}
410
411impl Row {
412    pub fn new() -> Self {
413        Self::default()
414    }
415
416    pub fn get(&self, key: VarId) -> Option<&LoraValue> {
417        self.entries
418            .get(key.0 as usize)
419            .and_then(|slot| slot.as_ref())
420            .map(|entry| &entry.value)
421    }
422
423    /// Returns the column name for `key`, generating the `_{key}` fallback
424    /// on demand for entries inserted without an explicit name.
425    pub fn get_name(&self, key: VarId) -> Option<String> {
426        self.entries
427            .get(key.0 as usize)
428            .and_then(|slot| slot.as_ref())
429            .map(|entry| match &entry.name {
430                Some(n) => n.to_string(),
431                None => format!("_{}", entry.var),
432            })
433    }
434
435    pub fn insert(&mut self, key: VarId, value: LoraValue) {
436        // Preserve any previously-set explicit name when overwriting an entry;
437        // otherwise leave name as None so the fallback is produced lazily.
438        let idx = self.ensure_slot(key);
439        match &mut self.entries[idx] {
440            Some(existing) => existing.value = value,
441            slot @ None => {
442                *slot = Some(RowEntry {
443                    var: key,
444                    name: None,
445                    value,
446                });
447                self.len_set += 1;
448            }
449        }
450    }
451
452    pub fn insert_named(&mut self, key: VarId, name: impl Into<Arc<str>>, value: LoraValue) {
453        let idx = self.ensure_slot(key);
454        let was_set = self.entries[idx].is_some();
455        self.entries[idx] = Some(RowEntry {
456            var: key,
457            name: Some(name.into()),
458            value,
459        });
460        if !was_set {
461            self.len_set += 1;
462        }
463    }
464
465    pub fn extend_from(&mut self, other: &Row) {
466        // Mirrors the previous `BTreeMap::insert` semantics: every set entry
467        // in `other` overwrites the slot wholesale (name and value), no merge.
468        for entry in other.entries.iter().flatten() {
469            let idx = self.ensure_slot(entry.var);
470            let was_set = self.entries[idx].is_some();
471            self.entries[idx] = Some(entry.clone());
472            if !was_set {
473                self.len_set += 1;
474            }
475        }
476    }
477
478    /// Copy every entry of `other` whose variable is unset in `self`,
479    /// keeping its name as stored (shared `Arc`, or none for anonymous
480    /// variables) rather than materializing the `_{key}` fallback.
481    pub fn fill_missing_from(&mut self, other: &Row) {
482        for entry in other.entries.iter().flatten() {
483            let idx = self.ensure_slot(entry.var);
484            if self.entries[idx].is_none() {
485                self.entries[idx] = Some(entry.clone());
486                self.len_set += 1;
487            }
488        }
489    }
490
491    pub fn iter(&self) -> impl Iterator<Item = (&VarId, &LoraValue)> {
492        self.entries
493            .iter()
494            .flatten()
495            .map(|entry| (&entry.var, &entry.value))
496    }
497
498    /// Iterate `(key, name, value)`. The name is a `Cow`: borrowed when an
499    /// explicit name was stored, and owned (lazily formatted as `_{key}`) for
500    /// entries inserted via the anonymous `insert()` path.
501    pub fn iter_named(
502        &self,
503    ) -> impl Iterator<Item = (&VarId, std::borrow::Cow<'_, str>, &LoraValue)> {
504        self.entries.iter().flatten().map(|entry| {
505            let name: std::borrow::Cow<'_, str> = match &entry.name {
506                Some(n) => std::borrow::Cow::Borrowed(&**n),
507                None => std::borrow::Cow::Owned(format!("_{}", entry.var)),
508            };
509            (&entry.var, name, &entry.value)
510        })
511    }
512
513    /// Consume the row and yield owned `(VarId, name, LoraValue)` triples.
514    /// Used by hydrate_row to avoid cloning values on the projection hot path;
515    /// names come back as the shared `Arc<str>` so re-inserting them into a
516    /// new row costs a refcount, not an allocation.
517    pub fn into_iter_named(self) -> impl Iterator<Item = (VarId, Arc<str>, LoraValue)> {
518        self.entries.into_iter().flatten().map(|entry| {
519            let RowEntry { var, name, value } = entry;
520            (
521                var,
522                name.unwrap_or_else(|| Arc::from(format!("_{var}"))),
523                value,
524            )
525        })
526    }
527
528    pub fn len(&self) -> usize {
529        self.len_set as usize
530    }
531
532    pub fn is_empty(&self) -> bool {
533        self.len_set == 0
534    }
535
536    pub fn contains_key(&self, key: VarId) -> bool {
537        self.entries
538            .get(key.0 as usize)
539            .is_some_and(|slot| slot.is_some())
540    }
541
542    /// Grow `entries` so that index `key.0` is in-range. Returns that index.
543    /// New slots are filled with `None` (counted as unset by `len_set`).
544    fn ensure_slot(&mut self, key: VarId) -> usize {
545        let idx = key.0 as usize;
546        if idx >= self.entries.len() {
547            self.entries.resize_with(idx + 1, || None);
548        }
549        idx
550    }
551}
552
553#[derive(Debug, Clone, Copy, PartialEq, Eq)]
554pub enum ResultFormat {
555    Rows,
556    RowArrays,
557    Graph,
558    Combined,
559}
560
561#[derive(Debug, Clone, Copy, PartialEq, Eq)]
562pub struct ExecuteOptions {
563    pub format: ResultFormat,
564}
565
566impl Default for ExecuteOptions {
567    fn default() -> Self {
568        Self {
569            format: ResultFormat::Graph,
570        }
571    }
572}
573
574#[derive(Debug, Clone, Serialize)]
575#[serde(untagged)]
576pub enum QueryResult {
577    Rows(RowsResult),
578    RowArrays(RowArraysResult),
579    Graph(GraphResult),
580    Combined(CombinedResult),
581}
582
583#[derive(Debug, Clone, Serialize)]
584pub struct RowsResult {
585    pub rows: Vec<Row>,
586}
587
588#[derive(Debug, Clone, Serialize)]
589pub struct RowArraysResult {
590    pub columns: Vec<String>,
591    pub rows: Vec<Vec<LoraValue>>,
592}
593
594#[derive(Debug, Clone, Serialize)]
595pub struct GraphResult {
596    pub graph: HydratedGraph,
597}
598
599#[derive(Debug, Clone, Serialize)]
600pub struct CombinedResult {
601    pub columns: Vec<String>,
602    pub data: Vec<CombinedRow>,
603    pub graph: HydratedGraph,
604}
605
606#[derive(Debug, Clone, Serialize)]
607pub struct CombinedRow {
608    pub row: Vec<LoraValue>,
609}
610
611#[derive(Debug, Clone, Serialize, Default)]
612pub struct HydratedGraph {
613    pub nodes: Vec<HydratedNode>,
614    pub relationships: Vec<HydratedRelationship>,
615}
616
617#[derive(Debug, Clone, Serialize, PartialEq)]
618pub struct HydratedNode {
619    pub id: i64,
620    pub labels: Vec<String>,
621    pub properties: BTreeMap<String, LoraValue>,
622}
623
624#[derive(Debug, Clone, Serialize, PartialEq)]
625pub struct HydratedRelationship {
626    pub id: i64,
627    #[serde(rename = "startId")]
628    pub start_id: i64,
629    #[serde(rename = "endId")]
630    pub end_id: i64,
631    #[serde(rename = "type")]
632    pub rel_type: String,
633    pub properties: BTreeMap<String, LoraValue>,
634}
635
636pub fn project_rows(rows: Vec<Row>, options: ExecuteOptions) -> QueryResult {
637    match options.format {
638        ResultFormat::Rows => QueryResult::Rows(RowsResult { rows }),
639
640        ResultFormat::RowArrays => {
641            let columns = infer_columns(&rows);
642            let projected_rows = rows.iter().map(|row| row_to_array(row, &columns)).collect();
643
644            QueryResult::RowArrays(RowArraysResult {
645                columns,
646                rows: projected_rows,
647            })
648        }
649
650        ResultFormat::Graph => QueryResult::Graph(GraphResult {
651            graph: collect_hydrated_graph(&rows),
652        }),
653
654        ResultFormat::Combined => {
655            let columns = infer_columns(&rows);
656            let data = rows
657                .iter()
658                .map(|row| CombinedRow {
659                    row: row_to_array(row, &columns),
660                })
661                .collect();
662
663            QueryResult::Combined(CombinedResult {
664                columns,
665                data,
666                graph: collect_hydrated_graph(&rows),
667            })
668        }
669    }
670}
671
672fn infer_columns(rows: &[Row]) -> Vec<String> {
673    rows.first()
674        .map(|row| {
675            row.iter_named()
676                .map(|(_, name, _)| name.into_owned())
677                .collect::<Vec<_>>()
678        })
679        .unwrap_or_default()
680}
681
682fn row_to_array(row: &Row, columns: &[String]) -> Vec<LoraValue> {
683    // Row entry count is small; a linear scan per column avoids allocating
684    // owned names into an intermediate lookup map.
685    columns
686        .iter()
687        .map(|col| {
688            row.iter_named()
689                .find(|(_, name, _)| name.as_ref() == col.as_str())
690                .map(|(_, _, v)| v.clone())
691                .unwrap_or(LoraValue::Null)
692        })
693        .collect()
694}
695
696fn collect_hydrated_graph(rows: &[Row]) -> HydratedGraph {
697    let mut nodes = BTreeMap::<i64, HydratedNode>::new();
698    let mut relationships = BTreeMap::<i64, HydratedRelationship>::new();
699
700    for row in rows {
701        for (_, _, value) in row.iter_named() {
702            collect_graph_from_value(value, &mut nodes, &mut relationships);
703        }
704    }
705
706    HydratedGraph {
707        nodes: nodes.into_values().collect(),
708        relationships: relationships.into_values().collect(),
709    }
710}
711
712fn collect_graph_from_value(
713    value: &LoraValue,
714    nodes: &mut BTreeMap<i64, HydratedNode>,
715    relationships: &mut BTreeMap<i64, HydratedRelationship>,
716) {
717    match value {
718        LoraValue::List(values) => {
719            for value in values {
720                collect_graph_from_value(value, nodes, relationships);
721            }
722        }
723
724        LoraValue::Map(map) => {
725            if let Some(node) = try_as_hydrated_node(map) {
726                nodes.entry(node.id).or_insert(node);
727                return;
728            }
729
730            if let Some(rel) = try_as_hydrated_relationship(map) {
731                relationships.entry(rel.id).or_insert(rel);
732                return;
733            }
734
735            for value in map.values() {
736                collect_graph_from_value(value, nodes, relationships);
737            }
738        }
739
740        _ => {}
741    }
742}
743
744fn try_as_hydrated_node(map: &BTreeMap<String, LoraValue>) -> Option<HydratedNode> {
745    let id = match map.get("id")? {
746        LoraValue::Int(v) => *v,
747        _ => return None,
748    };
749
750    let labels = match map.get("labels")? {
751        LoraValue::List(values) => values
752            .iter()
753            .map(|v| match v {
754                LoraValue::String(s) => Some(s.clone()),
755                _ => None,
756            })
757            .collect::<Option<Vec<_>>>()?,
758        _ => return None,
759    };
760
761    let properties = match map.get("properties")? {
762        LoraValue::Map(props) => props.clone(),
763        _ => return None,
764    };
765
766    Some(HydratedNode {
767        id,
768        labels,
769        properties,
770    })
771}
772
773fn try_as_hydrated_relationship(map: &BTreeMap<String, LoraValue>) -> Option<HydratedRelationship> {
774    match map.get("kind") {
775        Some(LoraValue::String(kind)) if kind == "relationship" => {}
776        _ => return None,
777    }
778
779    let id = match map.get("id")? {
780        LoraValue::Int(v) => *v,
781        _ => return None,
782    };
783
784    let start_id = match map.get("startId").or_else(|| map.get("src"))? {
785        LoraValue::Int(v) => *v,
786        _ => return None,
787    };
788
789    let end_id = match map.get("endId").or_else(|| map.get("dst"))? {
790        LoraValue::Int(v) => *v,
791        _ => return None,
792    };
793
794    let rel_type = match map.get("type")? {
795        LoraValue::String(s) => s.clone(),
796        _ => return None,
797    };
798
799    let properties = match map.get("properties")? {
800        LoraValue::Map(props) => props.clone(),
801        _ => return None,
802    };
803
804    Some(HydratedRelationship {
805        id,
806        start_id,
807        end_id,
808        rel_type,
809        properties,
810    })
811}