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;
17
18#[derive(Debug, Clone, PartialEq)]
19pub enum LoraValue {
20    Null,
21    Bool(bool),
22    Int(i64),
23    Float(f64),
24    String(String),
25    Binary(LoraBinary),
26    List(Vec<LoraValue>),
27    Map(BTreeMap<String, LoraValue>),
28    Node(NodeId),
29    Relationship(RelationshipId),
30    Path(LoraPath),
31    Date(LoraDate),
32    Time(LoraTime),
33    LocalTime(LoraLocalTime),
34    DateTime(LoraDateTime),
35    LocalDateTime(LoraLocalDateTime),
36    Duration(LoraDuration),
37    Point(LoraPoint),
38    Vector(LoraVector),
39}
40
41impl LoraValue {
42    pub fn is_truthy(&self) -> bool {
43        match self {
44            LoraValue::Null => false,
45            LoraValue::Bool(v) => *v,
46            _ => true,
47        }
48    }
49
50    pub fn as_i64(&self) -> Option<i64> {
51        match self {
52            LoraValue::Int(v) => Some(*v),
53            _ => None,
54        }
55    }
56
57    pub fn as_f64(&self) -> Option<f64> {
58        match self {
59            LoraValue::Int(v) => Some(*v as f64),
60            LoraValue::Float(v) => Some(*v),
61            _ => None,
62        }
63    }
64}
65
66impl Serialize for LoraValue {
67    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
68    where
69        S: Serializer,
70    {
71        match self {
72            LoraValue::Null => serializer.serialize_unit(),
73            LoraValue::Bool(v) => serializer.serialize_bool(*v),
74            LoraValue::Int(v) => serializer.serialize_i64(*v),
75            LoraValue::Float(v) => serializer.serialize_f64(*v),
76            LoraValue::String(v) => serializer.serialize_str(v),
77            LoraValue::Binary(v) => serialize_binary(serializer, v),
78
79            LoraValue::List(values) => {
80                let mut seq = serializer.serialize_seq(Some(values.len()))?;
81                for value in values {
82                    seq.serialize_element(value)?;
83                }
84                seq.end()
85            }
86
87            LoraValue::Map(map) => {
88                let mut ser_map = serializer.serialize_map(Some(map.len()))?;
89                for (k, v) in map {
90                    ser_map.serialize_entry(k, v)?;
91                }
92                ser_map.end()
93            }
94
95            // These should ideally not reach output anymore if executor hydrates first.
96            LoraValue::Node(id) => {
97                let mut ser_map = serializer.serialize_map(Some(2))?;
98                ser_map.serialize_entry("kind", "node")?;
99                ser_map.serialize_entry("id", id)?;
100                ser_map.end()
101            }
102
103            LoraValue::Relationship(id) => {
104                let mut ser_map = serializer.serialize_map(Some(2))?;
105                ser_map.serialize_entry("kind", "relationship")?;
106                ser_map.serialize_entry("id", id)?;
107                ser_map.end()
108            }
109
110            LoraValue::Path(path) => {
111                let mut ser_map = serializer.serialize_map(Some(3))?;
112                ser_map.serialize_entry("kind", "path")?;
113                ser_map.serialize_entry("nodes", &path.nodes)?;
114                ser_map.serialize_entry("rels", &path.rels)?;
115                ser_map.end()
116            }
117
118            LoraValue::Date(d) => serializer.serialize_str(&d.to_string()),
119            LoraValue::Time(t) => serializer.serialize_str(&t.to_string()),
120            LoraValue::LocalTime(t) => serializer.serialize_str(&t.to_string()),
121            LoraValue::DateTime(dt) => serializer.serialize_str(&dt.to_string()),
122            LoraValue::LocalDateTime(dt) => serializer.serialize_str(&dt.to_string()),
123            LoraValue::Duration(dur) => serializer.serialize_str(&dur.to_string()),
124            LoraValue::Point(p) => {
125                let len = if p.z.is_some() { 4 } else { 3 };
126                let mut m = serializer.serialize_map(Some(len))?;
127                m.serialize_entry("srid", &p.srid)?;
128                m.serialize_entry("x", &p.x)?;
129                m.serialize_entry("y", &p.y)?;
130                if let Some(z) = p.z {
131                    m.serialize_entry("z", &z)?;
132                }
133                m.end()
134            }
135            LoraValue::Vector(v) => serialize_vector(serializer, v),
136        }
137    }
138}
139
140fn serialize_binary<S: Serializer>(serializer: S, v: &LoraBinary) -> Result<S::Ok, S::Error> {
141    let mut m = serializer.serialize_map(Some(3))?;
142    m.serialize_entry("kind", "binary")?;
143    m.serialize_entry("length", &v.len())?;
144    m.serialize_entry("segments", v.segments())?;
145    m.end()
146}
147
148fn serialize_vector<S: Serializer>(serializer: S, v: &LoraVector) -> Result<S::Ok, S::Error> {
149    let mut m = serializer.serialize_map(Some(4))?;
150    m.serialize_entry("kind", "vector")?;
151    m.serialize_entry("dimension", &v.dimension)?;
152    m.serialize_entry("coordinateType", v.coordinate_type().as_str())?;
153    // Render values using the narrowest numeric type that fits the
154    // storage so downstream consumers (serde_json in particular) can
155    // surface integers vs. floats without losing information.
156    match &v.values {
157        VectorValues::Float64(values) => m.serialize_entry("values", values)?,
158        VectorValues::Float32(values) => {
159            let widened: Vec<f64> = values.iter().map(|x| *x as f64).collect();
160            m.serialize_entry("values", &widened)?;
161        }
162        VectorValues::Integer64(values) => m.serialize_entry("values", values)?,
163        VectorValues::Integer32(values) => {
164            let widened: Vec<i64> = values.iter().map(|x| *x as i64).collect();
165            m.serialize_entry("values", &widened)?;
166        }
167        VectorValues::Integer16(values) => {
168            let widened: Vec<i64> = values.iter().map(|x| *x as i64).collect();
169            m.serialize_entry("values", &widened)?;
170        }
171        VectorValues::Integer8(values) => {
172            let widened: Vec<i64> = values.iter().map(|x| *x as i64).collect();
173            m.serialize_entry("values", &widened)?;
174        }
175    }
176    m.end()
177}
178
179impl From<PropertyValue> for LoraValue {
180    fn from(value: PropertyValue) -> Self {
181        match value {
182            PropertyValue::Null => LoraValue::Null,
183            PropertyValue::Bool(v) => LoraValue::Bool(v),
184            PropertyValue::Int(v) => LoraValue::Int(v),
185            PropertyValue::Float(v) => LoraValue::Float(v),
186            PropertyValue::String(v) => LoraValue::String(v),
187            PropertyValue::Binary(v) => LoraValue::Binary(v),
188            PropertyValue::List(values) => {
189                LoraValue::List(values.into_iter().map(LoraValue::from).collect())
190            }
191            PropertyValue::Map(map) => LoraValue::Map(
192                map.into_iter()
193                    .map(|(k, v)| (k, LoraValue::from(v)))
194                    .collect(),
195            ),
196            PropertyValue::Date(d) => LoraValue::Date(d),
197            PropertyValue::Time(t) => LoraValue::Time(t),
198            PropertyValue::LocalTime(t) => LoraValue::LocalTime(t),
199            PropertyValue::DateTime(dt) => LoraValue::DateTime(dt),
200            PropertyValue::LocalDateTime(dt) => LoraValue::LocalDateTime(dt),
201            PropertyValue::Duration(dur) => LoraValue::Duration(dur),
202            PropertyValue::Point(p) => LoraValue::Point(p),
203            PropertyValue::Vector(v) => LoraValue::Vector(v),
204        }
205    }
206}
207
208/// Build a `LoraValue` from a borrowed `PropertyValue` in a single walk. Lets
209/// callers that already hold `&PropertyValue` (property lookups on borrowed
210/// records) skip the `prop.clone().into()` double-traversal.
211impl From<&PropertyValue> for LoraValue {
212    fn from(value: &PropertyValue) -> Self {
213        match value {
214            PropertyValue::Null => LoraValue::Null,
215            PropertyValue::Bool(v) => LoraValue::Bool(*v),
216            PropertyValue::Int(v) => LoraValue::Int(*v),
217            PropertyValue::Float(v) => LoraValue::Float(*v),
218            PropertyValue::String(v) => LoraValue::String(v.clone()),
219            PropertyValue::Binary(v) => LoraValue::Binary(v.clone()),
220            PropertyValue::List(values) => {
221                LoraValue::List(values.iter().map(LoraValue::from).collect())
222            }
223            PropertyValue::Map(map) => LoraValue::Map(
224                map.iter()
225                    .map(|(k, v)| (k.clone(), LoraValue::from(v)))
226                    .collect(),
227            ),
228            PropertyValue::Date(d) => LoraValue::Date(d.clone()),
229            PropertyValue::Time(t) => LoraValue::Time(t.clone()),
230            PropertyValue::LocalTime(t) => LoraValue::LocalTime(t.clone()),
231            PropertyValue::DateTime(dt) => LoraValue::DateTime(dt.clone()),
232            PropertyValue::LocalDateTime(dt) => LoraValue::LocalDateTime(dt.clone()),
233            PropertyValue::Duration(dur) => LoraValue::Duration(dur.clone()),
234            PropertyValue::Point(p) => LoraValue::Point(p.clone()),
235            PropertyValue::Vector(v) => LoraValue::Vector(v.clone()),
236        }
237    }
238}
239
240impl From<LoraValue> for PropertyValue {
241    fn from(value: LoraValue) -> Self {
242        match value {
243            LoraValue::Null => PropertyValue::Null,
244            LoraValue::Bool(v) => PropertyValue::Bool(v),
245            LoraValue::Int(v) => PropertyValue::Int(v),
246            LoraValue::Float(v) => PropertyValue::Float(v),
247            LoraValue::String(v) => PropertyValue::String(v),
248            LoraValue::Binary(v) => PropertyValue::Binary(v),
249            LoraValue::List(values) => {
250                PropertyValue::List(values.into_iter().map(PropertyValue::from).collect())
251            }
252            LoraValue::Map(map) => PropertyValue::Map(
253                map.into_iter()
254                    .map(|(k, v)| (k, PropertyValue::from(v)))
255                    .collect(),
256            ),
257            LoraValue::Node(id) => PropertyValue::String(format!("node:{id}")),
258            LoraValue::Relationship(id) => PropertyValue::String(format!("rel:{id}")),
259            LoraValue::Path(_) => PropertyValue::Null,
260            LoraValue::Date(d) => PropertyValue::Date(d),
261            LoraValue::Time(t) => PropertyValue::Time(t),
262            LoraValue::LocalTime(t) => PropertyValue::LocalTime(t),
263            LoraValue::DateTime(dt) => PropertyValue::DateTime(dt),
264            LoraValue::LocalDateTime(dt) => PropertyValue::LocalDateTime(dt),
265            LoraValue::Duration(dur) => PropertyValue::Duration(dur),
266            LoraValue::Point(p) => PropertyValue::Point(p),
267            LoraValue::Vector(v) => PropertyValue::Vector(v),
268        }
269    }
270}
271
272/// Errors that can arise when converting a `LoraValue` into a
273/// `PropertyValue` for storage on a node or relationship.
274#[derive(Debug, Clone, PartialEq)]
275pub enum PropertyConversionError {
276    /// A list entry contained a VECTOR value. Vectors are first-class
277    /// properties themselves but they cannot be nested inside lists.
278    NestedVectorInList,
279    /// Produced when something that cannot appear on disk (e.g. a `Path`
280    /// value captured by mistake) is asked to be converted — surfaced so
281    /// callers can reject it instead of silently stringifying.
282    #[allow(dead_code)]
283    UnsupportedKind(&'static str),
284}
285
286impl std::fmt::Display for PropertyConversionError {
287    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
288        match self {
289            PropertyConversionError::NestedVectorInList => {
290                write!(f, "lists stored as properties cannot contain VECTOR values")
291            }
292            PropertyConversionError::UnsupportedKind(kind) => {
293                write!(f, "cannot store {kind} as a property")
294            }
295        }
296    }
297}
298
299impl std::error::Error for PropertyConversionError {}
300
301/// Fallible conversion used on every write path
302/// (`set_property_from_expr`, `overwrite_entity_target`,
303/// `mutate_entity_target`, `eval_properties_expr`, plus CREATE /
304/// MERGE). Rejects VECTOR values nested inside lists at any depth —
305/// everything else falls through to the infallible `From`
306/// implementation above. A top-level VECTOR property is always fine;
307/// only LISTs that directly contain a VECTOR entry are rejected.
308pub fn lora_value_to_property(value: LoraValue) -> Result<PropertyValue, PropertyConversionError> {
309    /// Visit every nested value and, whenever we cross a `List`, flag the
310    /// `Vector` entries it directly contains. We still recurse through
311    /// `Map` and other `List` values so a vector buried under
312    /// `{inner: [vector(...)]}` is caught too.
313    fn visit(value: &LoraValue, inside_list: bool) -> Result<(), PropertyConversionError> {
314        match value {
315            LoraValue::Vector(_) if inside_list => Err(PropertyConversionError::NestedVectorInList),
316            LoraValue::List(items) => {
317                for item in items {
318                    visit(item, true)?;
319                }
320                Ok(())
321            }
322            LoraValue::Map(m) => {
323                for v in m.values() {
324                    visit(v, inside_list)?;
325                }
326                Ok(())
327            }
328            _ => Ok(()),
329        }
330    }
331
332    visit(&value, false)?;
333    Ok(PropertyValue::from(value))
334}
335
336#[derive(Debug, Clone, PartialEq)]
337struct RowEntry {
338    /// Stored alongside the value so iterators can hand back `&VarId` while
339    /// the slot's position in `entries` remains the source of truth for
340    /// lookups.
341    var: VarId,
342    /// `None` means "use the fallback `_{key}` lazily". This avoids allocating
343    /// a String for every anonymous variable on the insert hot path.
344    name: Option<String>,
345    value: LoraValue,
346}
347
348/// Row layout: a positional vector indexed by `VarId.0`. Two reasons this beats
349/// the previous `BTreeMap<VarId, RowEntry>`:
350///
351/// 1. **Cheaper clone.** Per-row clone is on the hottest path of the executor
352///    (every filter, projection, expand, optional-match). A `BTreeMap` clone
353///    allocates one tree node per entry; a `SmallVec` clone is a single
354///    `memcpy` (or zero allocations when the row fits inline).
355/// 2. **O(1) lookup.** `VarId`s are dense `u32`s minted from 0 by the
356///    analyzer's `SymbolTable` (lora-analyzer/src/symbols.rs:23), so the
357///    positional index is exact and tight.
358///
359/// `entries[i] == None` means "VarId(i) is unset"; the cached `len_set`
360/// counter keeps `len()` O(1) without scanning. The inline capacity (`8`)
361/// covers typical query rows without touching the heap.
362#[derive(Debug, Clone, Default, PartialEq)]
363pub struct Row {
364    entries: smallvec::SmallVec<Option<RowEntry>, 8>,
365    len_set: u32,
366}
367
368impl Serialize for Row {
369    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
370    where
371        S: Serializer,
372    {
373        let mut ser_map = serializer.serialize_map(Some(self.len()))?;
374        for entry in self.entries.iter().flatten() {
375            match &entry.name {
376                Some(name) => ser_map.serialize_entry(name.as_str(), &entry.value)?,
377                None => {
378                    let fallback = format!("_{}", entry.var);
379                    ser_map.serialize_entry(fallback.as_str(), &entry.value)?;
380                }
381            }
382        }
383        ser_map.end()
384    }
385}
386
387impl Row {
388    pub fn new() -> Self {
389        Self::default()
390    }
391
392    pub fn get(&self, key: VarId) -> Option<&LoraValue> {
393        self.entries
394            .get(key.0 as usize)
395            .and_then(|slot| slot.as_ref())
396            .map(|entry| &entry.value)
397    }
398
399    /// Returns the column name for `key`, generating the `_{key}` fallback
400    /// on demand for entries inserted without an explicit name.
401    pub fn get_name(&self, key: VarId) -> Option<String> {
402        self.entries
403            .get(key.0 as usize)
404            .and_then(|slot| slot.as_ref())
405            .map(|entry| match &entry.name {
406                Some(n) => n.clone(),
407                None => format!("_{}", entry.var),
408            })
409    }
410
411    pub fn insert(&mut self, key: VarId, value: LoraValue) {
412        // Preserve any previously-set explicit name when overwriting an entry;
413        // otherwise leave name as None so the fallback is produced lazily.
414        let idx = self.ensure_slot(key);
415        match &mut self.entries[idx] {
416            Some(existing) => existing.value = value,
417            slot @ None => {
418                *slot = Some(RowEntry {
419                    var: key,
420                    name: None,
421                    value,
422                });
423                self.len_set += 1;
424            }
425        }
426    }
427
428    pub fn insert_named(&mut self, key: VarId, name: impl Into<String>, value: LoraValue) {
429        let idx = self.ensure_slot(key);
430        let was_set = self.entries[idx].is_some();
431        self.entries[idx] = Some(RowEntry {
432            var: key,
433            name: Some(name.into()),
434            value,
435        });
436        if !was_set {
437            self.len_set += 1;
438        }
439    }
440
441    pub fn extend_from(&mut self, other: &Row) {
442        // Mirrors the previous `BTreeMap::insert` semantics: every set entry
443        // in `other` overwrites the slot wholesale (name and value), no merge.
444        for entry in other.entries.iter().flatten() {
445            let idx = self.ensure_slot(entry.var);
446            let was_set = self.entries[idx].is_some();
447            self.entries[idx] = Some(entry.clone());
448            if !was_set {
449                self.len_set += 1;
450            }
451        }
452    }
453
454    pub fn iter(&self) -> impl Iterator<Item = (&VarId, &LoraValue)> {
455        self.entries
456            .iter()
457            .flatten()
458            .map(|entry| (&entry.var, &entry.value))
459    }
460
461    /// Iterate `(key, name, value)`. The name is a `Cow`: borrowed when an
462    /// explicit name was stored, and owned (lazily formatted as `_{key}`) for
463    /// entries inserted via the anonymous `insert()` path.
464    pub fn iter_named(
465        &self,
466    ) -> impl Iterator<Item = (&VarId, std::borrow::Cow<'_, str>, &LoraValue)> {
467        self.entries.iter().flatten().map(|entry| {
468            let name: std::borrow::Cow<'_, str> = match &entry.name {
469                Some(n) => std::borrow::Cow::Borrowed(n.as_str()),
470                None => std::borrow::Cow::Owned(format!("_{}", entry.var)),
471            };
472            (&entry.var, name, &entry.value)
473        })
474    }
475
476    /// Consume the row and yield owned `(VarId, name, LoraValue)` triples.
477    /// Used by hydrate_row to avoid cloning values on the projection hot path.
478    pub fn into_iter_named(self) -> impl Iterator<Item = (VarId, String, LoraValue)> {
479        self.entries.into_iter().flatten().map(|entry| {
480            let RowEntry { var, name, value } = entry;
481            (var, name.unwrap_or_else(|| format!("_{var}")), value)
482        })
483    }
484
485    pub fn len(&self) -> usize {
486        self.len_set as usize
487    }
488
489    pub fn is_empty(&self) -> bool {
490        self.len_set == 0
491    }
492
493    pub fn contains_key(&self, key: VarId) -> bool {
494        self.entries
495            .get(key.0 as usize)
496            .is_some_and(|slot| slot.is_some())
497    }
498
499    /// Grow `entries` so that index `key.0` is in-range. Returns that index.
500    /// New slots are filled with `None` (counted as unset by `len_set`).
501    fn ensure_slot(&mut self, key: VarId) -> usize {
502        let idx = key.0 as usize;
503        if idx >= self.entries.len() {
504            self.entries.resize_with(idx + 1, || None);
505        }
506        idx
507    }
508}
509
510#[derive(Debug, Clone, Copy, PartialEq, Eq)]
511pub enum ResultFormat {
512    Rows,
513    RowArrays,
514    Graph,
515    Combined,
516}
517
518#[derive(Debug, Clone, Copy, PartialEq, Eq)]
519pub struct ExecuteOptions {
520    pub format: ResultFormat,
521}
522
523impl Default for ExecuteOptions {
524    fn default() -> Self {
525        Self {
526            format: ResultFormat::Graph,
527        }
528    }
529}
530
531#[derive(Debug, Clone, Serialize)]
532#[serde(untagged)]
533pub enum QueryResult {
534    Rows(RowsResult),
535    RowArrays(RowArraysResult),
536    Graph(GraphResult),
537    Combined(CombinedResult),
538}
539
540#[derive(Debug, Clone, Serialize)]
541pub struct RowsResult {
542    pub rows: Vec<Row>,
543}
544
545#[derive(Debug, Clone, Serialize)]
546pub struct RowArraysResult {
547    pub columns: Vec<String>,
548    pub rows: Vec<Vec<LoraValue>>,
549}
550
551#[derive(Debug, Clone, Serialize)]
552pub struct GraphResult {
553    pub graph: HydratedGraph,
554}
555
556#[derive(Debug, Clone, Serialize)]
557pub struct CombinedResult {
558    pub columns: Vec<String>,
559    pub data: Vec<CombinedRow>,
560    pub graph: HydratedGraph,
561}
562
563#[derive(Debug, Clone, Serialize)]
564pub struct CombinedRow {
565    pub row: Vec<LoraValue>,
566}
567
568#[derive(Debug, Clone, Serialize, Default)]
569pub struct HydratedGraph {
570    pub nodes: Vec<HydratedNode>,
571    pub relationships: Vec<HydratedRelationship>,
572}
573
574#[derive(Debug, Clone, Serialize, PartialEq)]
575pub struct HydratedNode {
576    pub id: i64,
577    pub labels: Vec<String>,
578    pub properties: BTreeMap<String, LoraValue>,
579}
580
581#[derive(Debug, Clone, Serialize, PartialEq)]
582pub struct HydratedRelationship {
583    pub id: i64,
584    #[serde(rename = "startId")]
585    pub start_id: i64,
586    #[serde(rename = "endId")]
587    pub end_id: i64,
588    #[serde(rename = "type")]
589    pub rel_type: String,
590    pub properties: BTreeMap<String, LoraValue>,
591}
592
593pub fn project_rows(rows: Vec<Row>, options: ExecuteOptions) -> QueryResult {
594    match options.format {
595        ResultFormat::Rows => QueryResult::Rows(RowsResult { rows }),
596
597        ResultFormat::RowArrays => {
598            let columns = infer_columns(&rows);
599            let projected_rows = rows.iter().map(|row| row_to_array(row, &columns)).collect();
600
601            QueryResult::RowArrays(RowArraysResult {
602                columns,
603                rows: projected_rows,
604            })
605        }
606
607        ResultFormat::Graph => QueryResult::Graph(GraphResult {
608            graph: collect_hydrated_graph(&rows),
609        }),
610
611        ResultFormat::Combined => {
612            let columns = infer_columns(&rows);
613            let data = rows
614                .iter()
615                .map(|row| CombinedRow {
616                    row: row_to_array(row, &columns),
617                })
618                .collect();
619
620            QueryResult::Combined(CombinedResult {
621                columns,
622                data,
623                graph: collect_hydrated_graph(&rows),
624            })
625        }
626    }
627}
628
629fn infer_columns(rows: &[Row]) -> Vec<String> {
630    rows.first()
631        .map(|row| {
632            row.iter_named()
633                .map(|(_, name, _)| name.into_owned())
634                .collect::<Vec<_>>()
635        })
636        .unwrap_or_default()
637}
638
639fn row_to_array(row: &Row, columns: &[String]) -> Vec<LoraValue> {
640    // Row entry count is small; a linear scan per column avoids allocating
641    // owned names into an intermediate lookup map.
642    columns
643        .iter()
644        .map(|col| {
645            row.iter_named()
646                .find(|(_, name, _)| name.as_ref() == col.as_str())
647                .map(|(_, _, v)| v.clone())
648                .unwrap_or(LoraValue::Null)
649        })
650        .collect()
651}
652
653fn collect_hydrated_graph(rows: &[Row]) -> HydratedGraph {
654    let mut nodes = BTreeMap::<i64, HydratedNode>::new();
655    let mut relationships = BTreeMap::<i64, HydratedRelationship>::new();
656
657    for row in rows {
658        for (_, _, value) in row.iter_named() {
659            collect_graph_from_value(value, &mut nodes, &mut relationships);
660        }
661    }
662
663    HydratedGraph {
664        nodes: nodes.into_values().collect(),
665        relationships: relationships.into_values().collect(),
666    }
667}
668
669fn collect_graph_from_value(
670    value: &LoraValue,
671    nodes: &mut BTreeMap<i64, HydratedNode>,
672    relationships: &mut BTreeMap<i64, HydratedRelationship>,
673) {
674    match value {
675        LoraValue::List(values) => {
676            for value in values {
677                collect_graph_from_value(value, nodes, relationships);
678            }
679        }
680
681        LoraValue::Map(map) => {
682            if let Some(node) = try_as_hydrated_node(map) {
683                nodes.entry(node.id).or_insert(node);
684                return;
685            }
686
687            if let Some(rel) = try_as_hydrated_relationship(map) {
688                relationships.entry(rel.id).or_insert(rel);
689                return;
690            }
691
692            for value in map.values() {
693                collect_graph_from_value(value, nodes, relationships);
694            }
695        }
696
697        _ => {}
698    }
699}
700
701fn try_as_hydrated_node(map: &BTreeMap<String, LoraValue>) -> Option<HydratedNode> {
702    let id = match map.get("id")? {
703        LoraValue::Int(v) => *v,
704        _ => return None,
705    };
706
707    let labels = match map.get("labels")? {
708        LoraValue::List(values) => values
709            .iter()
710            .map(|v| match v {
711                LoraValue::String(s) => Some(s.clone()),
712                _ => None,
713            })
714            .collect::<Option<Vec<_>>>()?,
715        _ => return None,
716    };
717
718    let properties = match map.get("properties")? {
719        LoraValue::Map(props) => props.clone(),
720        _ => return None,
721    };
722
723    Some(HydratedNode {
724        id,
725        labels,
726        properties,
727    })
728}
729
730fn try_as_hydrated_relationship(map: &BTreeMap<String, LoraValue>) -> Option<HydratedRelationship> {
731    match map.get("kind") {
732        Some(LoraValue::String(kind)) if kind == "relationship" => {}
733        _ => return None,
734    }
735
736    let id = match map.get("id")? {
737        LoraValue::Int(v) => *v,
738        _ => return None,
739    };
740
741    let start_id = match map.get("startId").or_else(|| map.get("src"))? {
742        LoraValue::Int(v) => *v,
743        _ => return None,
744    };
745
746    let end_id = match map.get("endId").or_else(|| map.get("dst"))? {
747        LoraValue::Int(v) => *v,
748        _ => return None,
749    };
750
751    let rel_type = match map.get("type")? {
752        LoraValue::String(s) => s.clone(),
753        _ => return None,
754    };
755
756    let properties = match map.get("properties")? {
757        LoraValue::Map(props) => props.clone(),
758        _ => return None,
759    };
760
761    Some(HydratedRelationship {
762        id,
763        start_id,
764        end_id,
765        rel_type,
766        properties,
767    })
768}