1use oxgraph_graph::{
4 CanonicalElementIdentity, CanonicalRelationIdentity, EdgeSourceGraph, EdgeTargetGraph,
5 TopologyCounts,
6};
7use serde::{Deserialize, Serialize};
8
9use crate::{
10 DbError, ElementId, IncidenceRecord, ProjectionId, PropertyKeyId, PropertySubject,
11 PropertyValue, RelationId,
12 catalog::{ProjectionDefinition, PropertyFamily},
13 projection::GraphProjection,
14 state::DatabaseState,
15 traversal::{self, TraversalDirection, TraversalOptions},
16 value::parse_value_token,
17};
18
19#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
25pub enum QueryLanguage {
26 Oxql,
28 Cypher,
30}
31
32#[derive(Clone, Debug, Eq, PartialEq)]
38pub struct PreparedQuery {
39 language: QueryLanguage,
41 text: String,
43 plan: QueryPlan,
45}
46
47impl PreparedQuery {
48 pub(crate) fn prepare(
59 language: QueryLanguage,
60 query: &str,
61 state: &DatabaseState,
62 ) -> Result<Self, DbError> {
63 let trimmed = query.trim();
64 if trimmed.is_empty() {
65 return Err(DbError::EmptyQuery);
66 }
67 let logical = match language {
68 QueryLanguage::Oxql => parse_oxql(trimmed)?,
69 QueryLanguage::Cypher => parse_cypher(trimmed)?,
70 };
71 let plan = bind_and_lower(logical, state)?;
72 Ok(Self {
73 language,
74 text: trimmed.to_owned(),
75 plan,
76 })
77 }
78
79 #[must_use]
85 pub const fn language(&self) -> QueryLanguage {
86 self.language
87 }
88
89 #[must_use]
95 pub fn text(&self) -> &str {
96 &self.text
97 }
98
99 #[must_use]
105 pub fn explain(&self) -> String {
106 self.plan.explain()
107 }
108
109 pub(crate) fn execute(&self, state: &DatabaseState) -> Result<QueryResult, DbError> {
120 self.plan.execute(state)
121 }
122}
123
124#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
130pub struct QueryResult {
131 rows: Vec<QueryRow>,
133}
134
135impl QueryResult {
136 #[must_use]
142 pub(crate) const fn new(rows: Vec<QueryRow>) -> Self {
143 Self { rows }
144 }
145
146 #[must_use]
152 pub fn rows(&self) -> &[QueryRow] {
153 &self.rows
154 }
155}
156
157#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
163pub struct QueryRow {
164 pub values: Vec<QueryValue>,
166}
167
168impl QueryRow {
169 #[must_use]
175 pub(crate) fn single(value: QueryValue) -> Self {
176 Self {
177 values: vec![value],
178 }
179 }
180}
181
182#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
188pub enum QueryValue {
189 Element(ElementId),
191 Relation(RelationId),
193 Incidence(IncidenceRecord),
195 Subject(PropertySubject),
197 Property(PropertyValue),
199 Text(String),
201 Projection(ProjectionId),
203}
204
205#[derive(Clone, Debug, Eq, PartialEq)]
207enum QueryPlan {
208 ElementScan,
210 RelationScan,
212 IncidenceScan,
214 ElementLabelScan {
216 label: String,
218 label_id: crate::LabelId,
220 },
221 ElementPropertyEqual {
223 key: String,
225 key_id: PropertyKeyId,
227 value: PropertyValue,
229 },
230 ElementFilter {
232 predicate: BoundPredicate,
234 },
235 RelationTypeScan {
237 relation_type: String,
239 relation_type_id: crate::RelationTypeId,
241 },
242 GraphWalk {
244 projection: ProjectionId,
246 element: ElementId,
248 options: TraversalOptions,
250 },
251 CypherDirectedTriples {
253 projection: ProjectionId,
255 },
256 CatalogScan,
258}
259
260#[derive(Clone, Debug, Eq, PartialEq)]
262enum BoundPredicate {
263 Compare {
265 key: String,
267 key_id: PropertyKeyId,
269 op: CompareOp,
271 value: PropertyValue,
273 },
274 And(Box<Self>, Box<Self>),
276 Or(Box<Self>, Box<Self>),
278}
279
280impl BoundPredicate {
281 fn explain(&self) -> String {
283 match self {
284 Self::Compare { key, op, value, .. } => format!("{key} {} {value}", op.spelling()),
285 Self::And(left, right) => format!("({} AND {})", left.explain(), right.explain()),
286 Self::Or(left, right) => format!("({} OR {})", left.explain(), right.explain()),
287 }
288 }
289
290 fn evaluate(&self, state: &DatabaseState, element: ElementId) -> bool {
294 match self {
295 Self::Compare {
296 key_id, op, value, ..
297 } => state
298 .property(PropertySubject::Element(element), *key_id)
299 .is_some_and(|actual| op.matches(actual.cmp(value))),
300 Self::And(left, right) => {
301 left.evaluate(state, element) && right.evaluate(state, element)
302 }
303 Self::Or(left, right) => {
304 left.evaluate(state, element) || right.evaluate(state, element)
305 }
306 }
307 }
308}
309
310impl QueryPlan {
311 fn explain(&self) -> String {
313 match self {
314 Self::ElementScan => "oxql scan elements".to_owned(),
315 Self::RelationScan => "oxql scan relations".to_owned(),
316 Self::IncidenceScan => "oxql scan incidences".to_owned(),
317 Self::ElementLabelScan { label, .. } => {
318 format!("oxql label index lookup elements label={label}")
319 }
320 Self::ElementPropertyEqual { key, value, .. } => {
321 format!("oxql property equality lookup elements {key}={value}")
322 }
323 Self::ElementFilter { predicate } => {
324 format!("oxql element filter {}", predicate.explain())
325 }
326 Self::RelationTypeScan { relation_type, .. } => {
327 format!("oxql relation-type index lookup type={relation_type}")
328 }
329 Self::GraphWalk {
330 projection,
331 element,
332 options,
333 } => format!(
334 "oxql graph projection {projection} walk from {element} depth {} direction {:?} limit {}",
335 options.max_depth, options.direction, options.limit,
336 ),
337 Self::CypherDirectedTriples { projection } => {
338 format!("cypher directed triple scan projection={projection}")
339 }
340 Self::CatalogScan => "catalog metadata scan".to_owned(),
341 }
342 }
343
344 fn execute(&self, state: &DatabaseState) -> Result<QueryResult, DbError> {
346 match self {
347 Self::ElementScan => Ok(scan_elements(state)),
348 Self::RelationScan => Ok(scan_relations(state)),
349 Self::IncidenceScan => Ok(scan_incidences(state)),
350 Self::ElementLabelScan { label_id, .. } => {
351 Ok(scan_elements_with_label(state, *label_id))
352 }
353 Self::ElementPropertyEqual { key_id, value, .. } => {
354 scan_elements_with_property(state, *key_id, value)
355 }
356 Self::ElementFilter { predicate } => Ok(filter_elements(state, predicate)),
357 Self::RelationTypeScan {
358 relation_type_id, ..
359 } => Ok(scan_relations_with_type(state, *relation_type_id)),
360 Self::GraphWalk {
361 projection,
362 element,
363 options,
364 } => execute_graph_walk(state, *projection, *element, *options),
365 Self::CypherDirectedTriples { projection } => {
366 execute_cypher_triples(state, *projection)
367 }
368 Self::CatalogScan => Ok(scan_catalog(state)),
369 }
370 }
371}
372
373#[derive(Clone, Debug, Eq, PartialEq)]
382enum LogicalOp {
383 ElementScan,
385 RelationScan,
387 IncidenceScan,
389 CatalogScan,
391 ElementLabelScan {
393 label: String,
395 },
396 RelationTypeScan {
398 relation_type: String,
400 },
401 ElementPropertyEqual {
403 key: String,
405 value: String,
407 },
408 GraphWalk {
410 projection: String,
412 element: String,
414 options: TraversalOptions,
416 },
417 CypherDirectedTriples,
419 ElementWhere {
421 predicate: LogicalPredicate,
423 },
424}
425
426#[derive(Clone, Copy, Debug, Eq, PartialEq)]
428enum CompareOp {
429 Eq,
431 Lt,
433 Le,
435 Gt,
437 Ge,
439}
440
441impl CompareOp {
442 const fn matches(self, ordering: core::cmp::Ordering) -> bool {
445 use core::cmp::Ordering::{Equal, Greater, Less};
446 match self {
447 Self::Eq => matches!(ordering, Equal),
448 Self::Lt => matches!(ordering, Less),
449 Self::Le => matches!(ordering, Less | Equal),
450 Self::Gt => matches!(ordering, Greater),
451 Self::Ge => matches!(ordering, Greater | Equal),
452 }
453 }
454
455 const fn spelling(self) -> &'static str {
457 match self {
458 Self::Eq => "=",
459 Self::Lt => "<",
460 Self::Le => "<=",
461 Self::Gt => ">",
462 Self::Ge => ">=",
463 }
464 }
465}
466
467#[derive(Clone, Debug, Eq, PartialEq)]
473enum LogicalPredicate {
474 Compare {
476 key: String,
478 op: CompareOp,
480 value: String,
482 },
483 And(Box<Self>, Box<Self>),
485 Or(Box<Self>, Box<Self>),
487}
488
489fn parse_oxql(query: &str) -> Result<LogicalOp, DbError> {
491 let tokens = query.split_whitespace().collect::<Vec<_>>();
492 let upper = tokens
493 .iter()
494 .map(|token| token.to_ascii_uppercase())
495 .collect::<Vec<_>>();
496 if upper.len() >= 3
497 && upper[0].as_str() == "MATCH"
498 && upper[1].as_str() == "ELEMENTS"
499 && upper[2].as_str() == "WHERE"
500 {
501 return parse_element_where(&tokens[3..], &upper[3..]);
502 }
503 match upper.as_slice() {
504 [command] if command == "CATALOG" => Ok(LogicalOp::CatalogScan),
505 [verb, family] if verb == "MATCH" && family == "ELEMENTS" => Ok(LogicalOp::ElementScan),
506 [verb, family] if verb == "MATCH" && family == "RELATIONS" => Ok(LogicalOp::RelationScan),
507 [verb, family] if verb == "MATCH" && family == "INCIDENCES" => Ok(LogicalOp::IncidenceScan),
508 [verb, family, has, object, _name]
509 if verb == "MATCH" && family == "ELEMENTS" && has == "HAS" && object == "LABEL" =>
510 {
511 Ok(LogicalOp::ElementLabelScan {
512 label: tokens[4].to_owned(),
513 })
514 }
515 [verb, family, r#type, _name]
516 if verb == "MATCH" && family == "RELATIONS" && r#type == "TYPE" =>
517 {
518 Ok(LogicalOp::RelationTypeScan {
519 relation_type: tokens[3].to_owned(),
520 })
521 }
522 [graph, _projection, neighbors, _element]
523 if graph == "GRAPH" && neighbors == "NEIGHBORS" =>
524 {
525 Ok(LogicalOp::GraphWalk {
526 projection: tokens[1].to_owned(),
527 element: tokens[3].to_owned(),
528 options: TraversalOptions::default(),
529 })
530 }
531 [
532 graph,
533 _projection,
534 walk,
535 from,
536 _element,
537 depth,
538 _max_depth,
539 ..,
540 ] if graph == "GRAPH" && walk == "WALK" && from == "FROM" && depth == "DEPTH" => {
541 parse_graph_walk(&tokens, &upper)
542 }
543 _tokens => Err(DbError::unsupported("unsupported OxQL profile query")),
544 }
545}
546
547fn parse_cypher(query: &str) -> Result<LogicalOp, DbError> {
549 if query == "MATCH (n) RETURN n" {
550 return Ok(LogicalOp::ElementScan);
551 }
552 if query == "MATCH (n)-[r]->(m) RETURN n,r,m" {
553 return Ok(LogicalOp::CypherDirectedTriples);
554 }
555 if let Some(label) = query
556 .strip_prefix("MATCH (n:")
557 .and_then(|rest| rest.strip_suffix(") RETURN n"))
558 {
559 return Ok(LogicalOp::ElementLabelScan {
560 label: label.to_owned(),
561 });
562 }
563 Err(DbError::unsupported("unsupported Cypher profile query"))
564}
565
566fn parse_graph_walk(tokens: &[&str], upper: &[String]) -> Result<LogicalOp, DbError> {
568 let max_depth = tokens[6]
569 .parse::<usize>()
570 .map_err(|_error| DbError::unsupported("walk depth must be an integer"))?;
571 let mut options = TraversalOptions {
572 max_depth,
573 ..TraversalOptions::default()
574 };
575 let mut saw_direction = false;
576 let mut saw_limit = false;
577 let mut index = 7;
578 while index < tokens.len() {
579 let Some(value) = tokens.get(index + 1) else {
580 return Err(DbError::unsupported("walk option requires a value"));
581 };
582 match upper[index].as_str() {
583 "DIRECTION" if !saw_direction => {
584 options.direction = parse_walk_direction(&upper[index + 1])?;
585 saw_direction = true;
586 }
587 "DIRECTION" => return Err(DbError::unsupported("walk direction specified twice")),
588 "LIMIT" if !saw_limit => {
589 options.limit = value
590 .parse::<usize>()
591 .map_err(|_error| DbError::unsupported("walk limit must be an integer"))?;
592 saw_limit = true;
593 }
594 "LIMIT" => return Err(DbError::unsupported("walk limit specified twice")),
595 _option => return Err(DbError::unsupported("unsupported walk option")),
596 }
597 index += 2;
598 }
599 Ok(LogicalOp::GraphWalk {
600 projection: tokens[1].to_owned(),
601 element: tokens[4].to_owned(),
602 options,
603 })
604}
605
606fn parse_walk_direction(direction: &str) -> Result<TraversalDirection, DbError> {
608 match direction {
609 "OUTGOING" => Ok(TraversalDirection::Outgoing),
610 "INCOMING" => Ok(TraversalDirection::Incoming),
611 "BOTH" => Ok(TraversalDirection::Both),
612 _direction => Err(DbError::unsupported(
613 "walk direction must be outgoing, incoming, or both",
614 )),
615 }
616}
617
618fn parse_element_where(tokens: &[&str], upper: &[String]) -> Result<LogicalOp, DbError> {
624 let mut cursor = 0;
625 let predicate = parse_predicate_or(tokens, upper, &mut cursor)?;
626 if cursor != tokens.len() {
627 return Err(DbError::unsupported(
628 "trailing tokens after WHERE predicate",
629 ));
630 }
631 match predicate {
632 LogicalPredicate::Compare {
633 key,
634 op: CompareOp::Eq,
635 value,
636 } => Ok(LogicalOp::ElementPropertyEqual { key, value }),
637 predicate => Ok(LogicalOp::ElementWhere { predicate }),
638 }
639}
640
641fn parse_predicate_or(
643 tokens: &[&str],
644 upper: &[String],
645 cursor: &mut usize,
646) -> Result<LogicalPredicate, DbError> {
647 let mut left = parse_predicate_and(tokens, upper, cursor)?;
648 while upper.get(*cursor).map(String::as_str) == Some("OR") {
649 *cursor += 1;
650 let right = parse_predicate_and(tokens, upper, cursor)?;
651 left = LogicalPredicate::Or(Box::new(left), Box::new(right));
652 }
653 Ok(left)
654}
655
656fn parse_predicate_and(
658 tokens: &[&str],
659 upper: &[String],
660 cursor: &mut usize,
661) -> Result<LogicalPredicate, DbError> {
662 let mut left = parse_predicate_factor(tokens, upper, cursor)?;
663 while upper.get(*cursor).map(String::as_str) == Some("AND") {
664 *cursor += 1;
665 let right = parse_predicate_factor(tokens, upper, cursor)?;
666 left = LogicalPredicate::And(Box::new(left), Box::new(right));
667 }
668 Ok(left)
669}
670
671fn parse_predicate_factor(
673 tokens: &[&str],
674 upper: &[String],
675 cursor: &mut usize,
676) -> Result<LogicalPredicate, DbError> {
677 if tokens.get(*cursor) == Some(&"(") {
678 *cursor += 1;
679 let inner = parse_predicate_or(tokens, upper, cursor)?;
680 if tokens.get(*cursor) != Some(&")") {
681 return Err(DbError::unsupported(
682 "unbalanced parentheses in WHERE predicate",
683 ));
684 }
685 *cursor += 1;
686 return Ok(inner);
687 }
688 parse_comparison(tokens, cursor)
689}
690
691fn parse_comparison(tokens: &[&str], cursor: &mut usize) -> Result<LogicalPredicate, DbError> {
693 let key = tokens
694 .get(*cursor)
695 .ok_or_else(|| DbError::unsupported("expected property key in WHERE predicate"))?;
696 let operator = tokens
697 .get(*cursor + 1)
698 .ok_or_else(|| DbError::unsupported("expected comparison operator in WHERE predicate"))?;
699 let value = tokens
700 .get(*cursor + 2)
701 .ok_or_else(|| DbError::unsupported("expected value in WHERE predicate"))?;
702 let op = parse_compare_op(operator)?;
703 *cursor += 3;
704 Ok(LogicalPredicate::Compare {
705 key: (*key).to_owned(),
706 op,
707 value: (*value).to_owned(),
708 })
709}
710
711fn parse_compare_op(token: &str) -> Result<CompareOp, DbError> {
713 match token {
714 "=" => Ok(CompareOp::Eq),
715 "<" => Ok(CompareOp::Lt),
716 "<=" => Ok(CompareOp::Le),
717 ">" => Ok(CompareOp::Gt),
718 ">=" => Ok(CompareOp::Ge),
719 _operator => Err(DbError::unsupported(
720 "comparison operator must be =, <, <=, >, or >=",
721 )),
722 }
723}
724
725fn bind_predicate(
727 predicate: LogicalPredicate,
728 state: &DatabaseState,
729) -> Result<BoundPredicate, DbError> {
730 match predicate {
731 LogicalPredicate::Compare { key, op, value } => {
732 let key_id = state
733 .catalog()
734 .property_key_id(&key)
735 .ok_or_else(|| DbError::unsupported(format!("unknown property key {key}")))?;
736 let value = parse_value_token(&value).map_err(DbError::unsupported)?;
737 state.validate_lookup_value_for_family(key_id, PropertyFamily::Element, &value)?;
738 Ok(BoundPredicate::Compare {
739 key,
740 key_id,
741 op,
742 value,
743 })
744 }
745 LogicalPredicate::And(left, right) => Ok(BoundPredicate::And(
746 Box::new(bind_predicate(*left, state)?),
747 Box::new(bind_predicate(*right, state)?),
748 )),
749 LogicalPredicate::Or(left, right) => Ok(BoundPredicate::Or(
750 Box::new(bind_predicate(*left, state)?),
751 Box::new(bind_predicate(*right, state)?),
752 )),
753 }
754}
755
756fn bind_and_lower(op: LogicalOp, state: &DatabaseState) -> Result<QueryPlan, DbError> {
762 match op {
763 LogicalOp::ElementScan => Ok(QueryPlan::ElementScan),
764 LogicalOp::RelationScan => Ok(QueryPlan::RelationScan),
765 LogicalOp::IncidenceScan => Ok(QueryPlan::IncidenceScan),
766 LogicalOp::CatalogScan => Ok(QueryPlan::CatalogScan),
767 LogicalOp::ElementLabelScan { label } => {
768 let label_id = state
769 .catalog()
770 .label_id(&label)
771 .ok_or_else(|| DbError::unsupported(format!("unknown label {label}")))?;
772 Ok(QueryPlan::ElementLabelScan { label, label_id })
773 }
774 LogicalOp::RelationTypeScan { relation_type } => {
775 let relation_type_id = state
776 .catalog()
777 .relation_type_id(&relation_type)
778 .ok_or_else(|| {
779 DbError::unsupported(format!("unknown relation type {relation_type}"))
780 })?;
781 Ok(QueryPlan::RelationTypeScan {
782 relation_type,
783 relation_type_id,
784 })
785 }
786 LogicalOp::ElementPropertyEqual { key, value } => {
787 let key_id = state
788 .catalog()
789 .property_key_id(&key)
790 .ok_or_else(|| DbError::unsupported(format!("unknown property key {key}")))?;
791 let value = parse_value_token(&value).map_err(DbError::unsupported)?;
792 state.validate_lookup_value_for_family(key_id, PropertyFamily::Element, &value)?;
793 Ok(QueryPlan::ElementPropertyEqual { key, key_id, value })
794 }
795 LogicalOp::GraphWalk {
796 projection,
797 element,
798 options,
799 } => lower_graph_walk(&projection, &element, options, state),
800 LogicalOp::CypherDirectedTriples => Ok(QueryPlan::CypherDirectedTriples {
801 projection: first_graph_projection(state)?,
802 }),
803 LogicalOp::ElementWhere { predicate } => Ok(QueryPlan::ElementFilter {
804 predicate: bind_predicate(predicate, state)?,
805 }),
806 }
807}
808
809fn lower_graph_walk(
811 projection: &str,
812 element: &str,
813 options: TraversalOptions,
814 state: &DatabaseState,
815) -> Result<QueryPlan, DbError> {
816 let projection = state
817 .catalog()
818 .projection_id(projection)
819 .ok_or_else(|| DbError::unsupported(format!("unknown projection {projection}")))?;
820 let entry = state
821 .catalog()
822 .projection(projection)
823 .ok_or(DbError::UnknownProjection { id: projection })?;
824 if matches!(&entry.definition, ProjectionDefinition::Hypergraph(_)) {
825 return Err(DbError::invalid_projection("projection is not a graph"));
826 }
827 let raw = element
828 .parse::<u64>()
829 .map_err(|_error| DbError::unsupported("element id must be an integer"))?;
830 Ok(QueryPlan::GraphWalk {
831 projection,
832 element: ElementId::new(raw),
833 options,
834 })
835}
836
837fn scan_elements(state: &DatabaseState) -> QueryResult {
839 QueryResult::new(
840 state
841 .elements()
842 .map(|record| QueryRow::single(QueryValue::Element(record.id)))
843 .collect(),
844 )
845}
846
847fn scan_relations(state: &DatabaseState) -> QueryResult {
849 QueryResult::new(
850 state
851 .relations()
852 .map(|record| QueryRow::single(QueryValue::Relation(record.id)))
853 .collect(),
854 )
855}
856
857fn scan_incidences(state: &DatabaseState) -> QueryResult {
859 QueryResult::new(
860 state
861 .incidences()
862 .map(|record| QueryRow::single(QueryValue::Incidence(*record)))
863 .collect(),
864 )
865}
866
867fn scan_elements_with_label(state: &DatabaseState, label_id: crate::LabelId) -> QueryResult {
869 QueryResult::new(
870 state
871 .elements_with_label(label_id)
872 .into_iter()
873 .map(|id| QueryRow::single(QueryValue::Element(id)))
874 .collect(),
875 )
876}
877
878fn scan_elements_with_property(
880 state: &DatabaseState,
881 key: PropertyKeyId,
882 value: &PropertyValue,
883) -> Result<QueryResult, DbError> {
884 Ok(QueryResult::new(
885 state
886 .typed_property_equal_for_family(key, PropertyFamily::Element, value)?
887 .into_iter()
888 .filter_map(|subject| match subject {
889 PropertySubject::Element(id) => Some(QueryRow::single(QueryValue::Element(id))),
890 PropertySubject::Relation(_) | PropertySubject::Incidence(_) => None,
891 })
892 .collect(),
893 ))
894}
895
896fn filter_elements(state: &DatabaseState, predicate: &BoundPredicate) -> QueryResult {
904 QueryResult::new(
905 state
906 .elements()
907 .filter(|record| predicate.evaluate(state, record.id))
908 .map(|record| QueryRow::single(QueryValue::Element(record.id)))
909 .collect(),
910 )
911}
912
913fn scan_relations_with_type(
915 state: &DatabaseState,
916 relation_type: crate::RelationTypeId,
917) -> QueryResult {
918 QueryResult::new(
919 state
920 .relations_with_type(relation_type)
921 .into_iter()
922 .map(|id| QueryRow::single(QueryValue::Relation(id)))
923 .collect(),
924 )
925}
926
927fn scan_catalog(state: &DatabaseState) -> QueryResult {
929 let rows = state
930 .catalog()
931 .projections()
932 .map(|entry| QueryRow {
933 values: vec![
934 QueryValue::Projection(entry.id),
935 QueryValue::Text(entry.definition.name().to_owned()),
936 ],
937 })
938 .collect();
939 QueryResult::new(rows)
940}
941
942fn execute_graph_walk(
944 state: &DatabaseState,
945 projection: ProjectionId,
946 element: ElementId,
947 options: TraversalOptions,
948) -> Result<QueryResult, DbError> {
949 let graph = graph_projection(state, projection)?;
950 let traversal = traversal::traverse_graph_projection(&graph, &[element], options)?;
951 let rows = traversal
952 .rows()
953 .iter()
954 .map(|row| QueryRow::single(QueryValue::Element(row.element)))
955 .collect();
956 Ok(QueryResult::new(rows))
957}
958
959fn execute_cypher_triples(
961 state: &DatabaseState,
962 projection: ProjectionId,
963) -> Result<QueryResult, DbError> {
964 let graph = graph_projection(state, projection)?;
965 let mut rows = Vec::with_capacity(graph.relation_count());
966 for index in 0..graph.relation_count() {
967 let edge = projection_relation(index)?;
968 rows.push(QueryRow {
969 values: vec![
970 QueryValue::Element(graph.canonical_element_id(graph.source(edge))),
971 QueryValue::Relation(graph.canonical_relation_id(edge)),
972 QueryValue::Element(graph.canonical_element_id(graph.target(edge))),
973 ],
974 });
975 }
976 Ok(QueryResult::new(rows))
977}
978
979fn graph_projection(
981 state: &DatabaseState,
982 projection: ProjectionId,
983) -> Result<GraphProjection, DbError> {
984 let entry = state
985 .catalog()
986 .projection(projection)
987 .ok_or(DbError::UnknownProjection { id: projection })?;
988 match &entry.definition {
989 ProjectionDefinition::Graph(definition) => {
990 GraphProjection::from_state(state, definition.clone())
991 }
992 ProjectionDefinition::Hypergraph(_definition) => {
993 Err(DbError::invalid_projection("projection is not a graph"))
994 }
995 }
996}
997
998fn first_graph_projection(state: &DatabaseState) -> Result<ProjectionId, DbError> {
1000 state
1001 .catalog()
1002 .projections()
1003 .find_map(|entry| match &entry.definition {
1004 ProjectionDefinition::Graph(_definition) => Some(entry.id),
1005 ProjectionDefinition::Hypergraph(_definition) => None,
1006 })
1007 .ok_or_else(|| DbError::unsupported("Cypher profile requires a graph projection"))
1008}
1009
1010fn projection_relation(index: usize) -> Result<crate::ProjectionRelationId, DbError> {
1012 u32::try_from(index)
1013 .map(crate::ProjectionRelationId::new)
1014 .map_err(|_error| DbError::IdOverflow)
1015}