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#[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 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 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 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
228impl 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#[derive(Debug, Clone, PartialEq)]
295pub enum PropertyConversionError {
296 NestedVectorInList,
299 #[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
321pub fn lora_value_to_property(value: LoraValue) -> Result<PropertyValue, PropertyConversionError> {
329 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 var: VarId,
362 name: Option<Arc<str>>,
369 value: LoraValue,
370}
371
372#[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 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 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 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 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 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 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 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 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}