1use serde::{Deserialize, Serialize};
4
5use crate::{
6 DbError, ElementId, IncidenceRecord, ProjectionId, PropertyKeyId, PropertySubject,
7 PropertyValue, RelationId,
8 catalog::{ProjectionDefinition, PropertyFamily},
9 overlay::StateView,
10 projection::GraphProjection,
11 traversal::{self, Direction, Walk},
12 value::parse_value_token,
13};
14
15#[derive(Clone, Debug, Eq, PartialEq)]
21pub struct PreparedQuery {
22 text: String,
24 plan: QueryPlan,
26}
27
28impl PreparedQuery {
29 pub(crate) fn prepare(query: &str, state: &impl StateView) -> Result<Self, DbError> {
40 let trimmed = query.trim();
41 if trimmed.is_empty() {
42 return Err(DbError::EmptyQuery);
43 }
44 let logical = parse_oxql(trimmed)?;
45 let plan = bind_and_lower(logical, state)?;
46 Ok(Self {
47 text: trimmed.to_owned(),
48 plan,
49 })
50 }
51
52 #[must_use]
58 pub fn text(&self) -> &str {
59 &self.text
60 }
61
62 #[must_use]
68 pub fn explain(&self) -> String {
69 self.plan.explain()
70 }
71
72 pub(crate) fn execute(&self, state: &impl StateView) -> Result<QueryResult, DbError> {
83 self.plan.execute(state)
84 }
85}
86
87#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
93pub struct QueryResult {
94 rows: Vec<QueryRow>,
96}
97
98impl QueryResult {
99 #[must_use]
105 pub(crate) const fn new(rows: Vec<QueryRow>) -> Self {
106 Self { rows }
107 }
108
109 #[must_use]
115 pub fn rows(&self) -> &[QueryRow] {
116 &self.rows
117 }
118}
119
120#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
126pub struct QueryRow {
127 pub values: Vec<QueryValue>,
129}
130
131impl QueryRow {
132 #[must_use]
138 pub(crate) fn single(value: QueryValue) -> Self {
139 Self {
140 values: vec![value],
141 }
142 }
143}
144
145#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
151pub enum QueryValue {
152 Element(ElementId),
154 Relation(RelationId),
156 Incidence(IncidenceRecord),
158 Subject(PropertySubject),
160 Property(PropertyValue),
162 Text(String),
164 Projection(ProjectionId),
166}
167
168#[derive(Clone, Debug, Eq, PartialEq)]
170enum QueryPlan {
171 ElementScan,
173 RelationScan,
175 IncidenceScan,
177 ElementLabelScan {
179 label: String,
181 label_id: crate::LabelId,
183 },
184 ElementPropertyEqual {
186 key: String,
188 key_id: PropertyKeyId,
190 value: PropertyValue,
192 },
193 ElementFilter {
195 predicate: BoundPredicate,
197 },
198 RelationTypeScan {
200 relation_type: String,
202 relation_type_id: crate::RelationTypeId,
204 },
205 GraphWalk {
207 projection: ProjectionId,
209 element: ElementId,
211 options: Walk,
213 },
214 CatalogScan,
216}
217
218#[derive(Clone, Debug, Eq, PartialEq)]
220enum BoundPredicate {
221 Compare {
223 key: String,
225 key_id: PropertyKeyId,
227 op: CompareOp,
229 value: PropertyValue,
231 },
232 And(Box<Self>, Box<Self>),
234 Or(Box<Self>, Box<Self>),
236}
237
238impl BoundPredicate {
239 fn explain(&self) -> String {
241 match self {
242 Self::Compare { key, op, value, .. } => format!("{key} {} {value}", op.spelling()),
243 Self::And(left, right) => format!("({} AND {})", left.explain(), right.explain()),
244 Self::Or(left, right) => format!("({} OR {})", left.explain(), right.explain()),
245 }
246 }
247
248 fn evaluate(&self, state: &impl StateView, element: ElementId) -> bool {
252 match self {
253 Self::Compare {
254 key_id, op, value, ..
255 } => state
256 .property(PropertySubject::Element(element), *key_id)
257 .is_some_and(|actual| op.matches(actual.as_ref().cmp(value))),
258 Self::And(left, right) => {
259 left.evaluate(state, element) && right.evaluate(state, element)
260 }
261 Self::Or(left, right) => {
262 left.evaluate(state, element) || right.evaluate(state, element)
263 }
264 }
265 }
266}
267
268impl QueryPlan {
269 fn explain(&self) -> String {
271 match self {
272 Self::ElementScan => "oxql scan elements".to_owned(),
273 Self::RelationScan => "oxql scan relations".to_owned(),
274 Self::IncidenceScan => "oxql scan incidences".to_owned(),
275 Self::ElementLabelScan { label, .. } => {
276 format!("oxql label index lookup elements label={label}")
277 }
278 Self::ElementPropertyEqual { key, value, .. } => {
279 format!("oxql property equality lookup elements {key}={value}")
280 }
281 Self::ElementFilter { predicate } => {
282 format!("oxql element filter {}", predicate.explain())
283 }
284 Self::RelationTypeScan { relation_type, .. } => {
285 format!("oxql relation-type index lookup type={relation_type}")
286 }
287 Self::GraphWalk {
288 projection,
289 element,
290 options,
291 } => format!(
292 "oxql graph projection {projection} walk from {element} depth {} direction {:?} limit {}",
293 options.max_depth, options.direction, options.limit,
294 ),
295 Self::CatalogScan => "catalog metadata scan".to_owned(),
296 }
297 }
298
299 fn execute(&self, state: &impl StateView) -> Result<QueryResult, DbError> {
301 match self {
302 Self::ElementScan => Ok(scan_elements(state)),
303 Self::RelationScan => Ok(scan_relations(state)),
304 Self::IncidenceScan => Ok(scan_incidences(state)),
305 Self::ElementLabelScan { label_id, .. } => {
306 Ok(scan_elements_with_label(state, *label_id))
307 }
308 Self::ElementPropertyEqual { key_id, value, .. } => {
309 scan_elements_with_property(state, *key_id, value)
310 }
311 Self::ElementFilter { predicate } => Ok(filter_elements(state, predicate)),
312 Self::RelationTypeScan {
313 relation_type_id, ..
314 } => Ok(scan_relations_with_type(state, *relation_type_id)),
315 Self::GraphWalk {
316 projection,
317 element,
318 options,
319 } => execute_graph_walk(state, *projection, *element, *options),
320 Self::CatalogScan => Ok(scan_catalog(state)),
321 }
322 }
323}
324
325#[derive(Clone, Debug, Eq, PartialEq)]
333enum LogicalOp {
334 ElementScan,
336 RelationScan,
338 IncidenceScan,
340 CatalogScan,
342 ElementLabelScan {
344 label: String,
346 },
347 RelationTypeScan {
349 relation_type: String,
351 },
352 ElementPropertyEqual {
354 key: String,
356 value: String,
358 },
359 GraphWalk {
361 projection: String,
363 element: String,
365 options: Walk,
367 },
368 ElementWhere {
370 predicate: LogicalPredicate,
372 },
373}
374
375#[derive(Clone, Copy, Debug, Eq, PartialEq)]
377enum CompareOp {
378 Eq,
380 Lt,
382 Le,
384 Gt,
386 Ge,
388}
389
390impl CompareOp {
391 const fn matches(self, ordering: core::cmp::Ordering) -> bool {
394 use core::cmp::Ordering::{Equal, Greater, Less};
395 match self {
396 Self::Eq => matches!(ordering, Equal),
397 Self::Lt => matches!(ordering, Less),
398 Self::Le => matches!(ordering, Less | Equal),
399 Self::Gt => matches!(ordering, Greater),
400 Self::Ge => matches!(ordering, Greater | Equal),
401 }
402 }
403
404 const fn spelling(self) -> &'static str {
406 match self {
407 Self::Eq => "=",
408 Self::Lt => "<",
409 Self::Le => "<=",
410 Self::Gt => ">",
411 Self::Ge => ">=",
412 }
413 }
414}
415
416#[derive(Clone, Debug, Eq, PartialEq)]
422enum LogicalPredicate {
423 Compare {
425 key: String,
427 op: CompareOp,
429 value: String,
431 },
432 And(Box<Self>, Box<Self>),
434 Or(Box<Self>, Box<Self>),
436}
437
438fn parse_oxql(query: &str) -> Result<LogicalOp, DbError> {
440 let tokens = query.split_whitespace().collect::<Vec<_>>();
441 let upper = tokens
442 .iter()
443 .map(|token| token.to_ascii_uppercase())
444 .collect::<Vec<_>>();
445 if upper.len() >= 3
446 && upper[0].as_str() == "MATCH"
447 && upper[1].as_str() == "ELEMENTS"
448 && upper[2].as_str() == "WHERE"
449 {
450 return parse_element_where(&tokens[3..], &upper[3..]);
451 }
452 match upper.as_slice() {
453 [command] if command == "CATALOG" => Ok(LogicalOp::CatalogScan),
454 [verb, family] if verb == "MATCH" && family == "ELEMENTS" => Ok(LogicalOp::ElementScan),
455 [verb, family] if verb == "MATCH" && family == "RELATIONS" => Ok(LogicalOp::RelationScan),
456 [verb, family] if verb == "MATCH" && family == "INCIDENCES" => Ok(LogicalOp::IncidenceScan),
457 [verb, family, has, object, _name]
458 if verb == "MATCH" && family == "ELEMENTS" && has == "HAS" && object == "LABEL" =>
459 {
460 Ok(LogicalOp::ElementLabelScan {
461 label: tokens[4].to_owned(),
462 })
463 }
464 [verb, family, r#type, _name]
465 if verb == "MATCH" && family == "RELATIONS" && r#type == "TYPE" =>
466 {
467 Ok(LogicalOp::RelationTypeScan {
468 relation_type: tokens[3].to_owned(),
469 })
470 }
471 [graph, _projection, neighbors, _element]
472 if graph == "GRAPH" && neighbors == "NEIGHBORS" =>
473 {
474 Ok(LogicalOp::GraphWalk {
475 projection: tokens[1].to_owned(),
476 element: tokens[3].to_owned(),
477 options: Walk::default(),
478 })
479 }
480 [
481 graph,
482 _projection,
483 walk,
484 from,
485 _element,
486 depth,
487 _max_depth,
488 ..,
489 ] if graph == "GRAPH" && walk == "WALK" && from == "FROM" && depth == "DEPTH" => {
490 parse_graph_walk(&tokens, &upper)
491 }
492 _tokens => Err(DbError::unsupported("unsupported OxQL profile query")),
493 }
494}
495
496fn parse_graph_walk(tokens: &[&str], upper: &[String]) -> Result<LogicalOp, DbError> {
498 let max_depth = tokens[6]
499 .parse::<usize>()
500 .map_err(|_error| DbError::unsupported("walk depth must be an integer"))?;
501 let mut options = Walk {
502 max_depth,
503 ..Walk::default()
504 };
505 let mut saw_direction = false;
506 let mut saw_limit = false;
507 let mut index = 7;
508 while index < tokens.len() {
509 let Some(value) = tokens.get(index + 1) else {
510 return Err(DbError::unsupported("walk option requires a value"));
511 };
512 match upper[index].as_str() {
513 "DIRECTION" if !saw_direction => {
514 options.direction = parse_walk_direction(&upper[index + 1])?;
515 saw_direction = true;
516 }
517 "DIRECTION" => return Err(DbError::unsupported("walk direction specified twice")),
518 "LIMIT" if !saw_limit => {
519 options.limit = value
520 .parse::<usize>()
521 .map_err(|_error| DbError::unsupported("walk limit must be an integer"))?;
522 saw_limit = true;
523 }
524 "LIMIT" => return Err(DbError::unsupported("walk limit specified twice")),
525 _option => return Err(DbError::unsupported("unsupported walk option")),
526 }
527 index += 2;
528 }
529 Ok(LogicalOp::GraphWalk {
530 projection: tokens[1].to_owned(),
531 element: tokens[4].to_owned(),
532 options,
533 })
534}
535
536fn parse_walk_direction(direction: &str) -> Result<Direction, DbError> {
538 match direction {
539 "OUTGOING" => Ok(Direction::Outgoing),
540 "INCOMING" => Ok(Direction::Incoming),
541 "BOTH" => Ok(Direction::Both),
542 _direction => Err(DbError::unsupported(
543 "walk direction must be outgoing, incoming, or both",
544 )),
545 }
546}
547
548fn parse_element_where(tokens: &[&str], upper: &[String]) -> Result<LogicalOp, DbError> {
554 let mut cursor = 0;
555 let predicate = parse_predicate_or(tokens, upper, &mut cursor)?;
556 if cursor != tokens.len() {
557 return Err(DbError::unsupported(
558 "trailing tokens after WHERE predicate",
559 ));
560 }
561 match predicate {
562 LogicalPredicate::Compare {
563 key,
564 op: CompareOp::Eq,
565 value,
566 } => Ok(LogicalOp::ElementPropertyEqual { key, value }),
567 predicate => Ok(LogicalOp::ElementWhere { predicate }),
568 }
569}
570
571fn parse_predicate_or(
573 tokens: &[&str],
574 upper: &[String],
575 cursor: &mut usize,
576) -> Result<LogicalPredicate, DbError> {
577 let mut left = parse_predicate_and(tokens, upper, cursor)?;
578 while upper.get(*cursor).map(String::as_str) == Some("OR") {
579 *cursor += 1;
580 let right = parse_predicate_and(tokens, upper, cursor)?;
581 left = LogicalPredicate::Or(Box::new(left), Box::new(right));
582 }
583 Ok(left)
584}
585
586fn parse_predicate_and(
588 tokens: &[&str],
589 upper: &[String],
590 cursor: &mut usize,
591) -> Result<LogicalPredicate, DbError> {
592 let mut left = parse_predicate_factor(tokens, upper, cursor)?;
593 while upper.get(*cursor).map(String::as_str) == Some("AND") {
594 *cursor += 1;
595 let right = parse_predicate_factor(tokens, upper, cursor)?;
596 left = LogicalPredicate::And(Box::new(left), Box::new(right));
597 }
598 Ok(left)
599}
600
601fn parse_predicate_factor(
603 tokens: &[&str],
604 upper: &[String],
605 cursor: &mut usize,
606) -> Result<LogicalPredicate, DbError> {
607 if tokens.get(*cursor) == Some(&"(") {
608 *cursor += 1;
609 let inner = parse_predicate_or(tokens, upper, cursor)?;
610 if tokens.get(*cursor) != Some(&")") {
611 return Err(DbError::unsupported(
612 "unbalanced parentheses in WHERE predicate",
613 ));
614 }
615 *cursor += 1;
616 return Ok(inner);
617 }
618 parse_comparison(tokens, cursor)
619}
620
621fn parse_comparison(tokens: &[&str], cursor: &mut usize) -> Result<LogicalPredicate, DbError> {
623 let key = tokens
624 .get(*cursor)
625 .ok_or_else(|| DbError::unsupported("expected property key in WHERE predicate"))?;
626 let operator = tokens
627 .get(*cursor + 1)
628 .ok_or_else(|| DbError::unsupported("expected comparison operator in WHERE predicate"))?;
629 let value = tokens
630 .get(*cursor + 2)
631 .ok_or_else(|| DbError::unsupported("expected value in WHERE predicate"))?;
632 let op = parse_compare_op(operator)?;
633 *cursor += 3;
634 Ok(LogicalPredicate::Compare {
635 key: (*key).to_owned(),
636 op,
637 value: (*value).to_owned(),
638 })
639}
640
641fn parse_compare_op(token: &str) -> Result<CompareOp, DbError> {
643 match token {
644 "=" => Ok(CompareOp::Eq),
645 "<" => Ok(CompareOp::Lt),
646 "<=" => Ok(CompareOp::Le),
647 ">" => Ok(CompareOp::Gt),
648 ">=" => Ok(CompareOp::Ge),
649 _operator => Err(DbError::unsupported(
650 "comparison operator must be =, <, <=, >, or >=",
651 )),
652 }
653}
654
655fn bind_predicate(
657 predicate: LogicalPredicate,
658 state: &impl StateView,
659) -> Result<BoundPredicate, DbError> {
660 match predicate {
661 LogicalPredicate::Compare { key, op, value } => {
662 let key_id = state
663 .catalog()
664 .property_key_id(&key)
665 .ok_or_else(|| DbError::unsupported(format!("unknown property key {key}")))?;
666 let value = parse_value_token(&value).map_err(DbError::unsupported)?;
667 state.validate_lookup_value_for_family(key_id, PropertyFamily::Element, &value)?;
668 Ok(BoundPredicate::Compare {
669 key,
670 key_id,
671 op,
672 value,
673 })
674 }
675 LogicalPredicate::And(left, right) => Ok(BoundPredicate::And(
676 Box::new(bind_predicate(*left, state)?),
677 Box::new(bind_predicate(*right, state)?),
678 )),
679 LogicalPredicate::Or(left, right) => Ok(BoundPredicate::Or(
680 Box::new(bind_predicate(*left, state)?),
681 Box::new(bind_predicate(*right, state)?),
682 )),
683 }
684}
685
686fn bind_and_lower(op: LogicalOp, state: &impl StateView) -> Result<QueryPlan, DbError> {
692 match op {
693 LogicalOp::ElementScan => Ok(QueryPlan::ElementScan),
694 LogicalOp::RelationScan => Ok(QueryPlan::RelationScan),
695 LogicalOp::IncidenceScan => Ok(QueryPlan::IncidenceScan),
696 LogicalOp::CatalogScan => Ok(QueryPlan::CatalogScan),
697 LogicalOp::ElementLabelScan { label } => {
698 let label_id = state
699 .catalog()
700 .label_id(&label)
701 .ok_or_else(|| DbError::unsupported(format!("unknown label {label}")))?;
702 Ok(QueryPlan::ElementLabelScan { label, label_id })
703 }
704 LogicalOp::RelationTypeScan { relation_type } => {
705 let relation_type_id = state
706 .catalog()
707 .relation_type_id(&relation_type)
708 .ok_or_else(|| {
709 DbError::unsupported(format!("unknown relation type {relation_type}"))
710 })?;
711 Ok(QueryPlan::RelationTypeScan {
712 relation_type,
713 relation_type_id,
714 })
715 }
716 LogicalOp::ElementPropertyEqual { key, value } => {
717 let key_id = state
718 .catalog()
719 .property_key_id(&key)
720 .ok_or_else(|| DbError::unsupported(format!("unknown property key {key}")))?;
721 let value = parse_value_token(&value).map_err(DbError::unsupported)?;
722 state.validate_lookup_value_for_family(key_id, PropertyFamily::Element, &value)?;
723 Ok(QueryPlan::ElementPropertyEqual { key, key_id, value })
724 }
725 LogicalOp::GraphWalk {
726 projection,
727 element,
728 options,
729 } => lower_graph_walk(&projection, &element, options, state),
730 LogicalOp::ElementWhere { predicate } => Ok(QueryPlan::ElementFilter {
731 predicate: bind_predicate(predicate, state)?,
732 }),
733 }
734}
735
736fn lower_graph_walk(
738 projection: &str,
739 element: &str,
740 options: Walk,
741 state: &impl StateView,
742) -> Result<QueryPlan, DbError> {
743 let projection = state
744 .catalog()
745 .projection_id(projection)
746 .ok_or_else(|| DbError::unsupported(format!("unknown projection {projection}")))?;
747 let entry = state
748 .catalog()
749 .projection(projection)
750 .ok_or(DbError::UnknownProjection { id: projection })?;
751 if matches!(&entry.definition, ProjectionDefinition::Hypergraph(_)) {
752 return Err(DbError::invalid_projection("projection is not a graph"));
753 }
754 let raw = element
755 .parse::<u64>()
756 .map_err(|_error| DbError::unsupported("element id must be an integer"))?;
757 Ok(QueryPlan::GraphWalk {
758 projection,
759 element: ElementId::new(raw),
760 options,
761 })
762}
763
764fn scan_elements(state: &impl StateView) -> QueryResult {
766 QueryResult::new(
767 state
768 .elements()
769 .map(|record| QueryRow::single(QueryValue::Element(record.id)))
770 .collect(),
771 )
772}
773
774fn scan_relations(state: &impl StateView) -> QueryResult {
776 QueryResult::new(
777 state
778 .relations()
779 .map(|record| QueryRow::single(QueryValue::Relation(record.id)))
780 .collect(),
781 )
782}
783
784fn scan_incidences(state: &impl StateView) -> QueryResult {
786 QueryResult::new(
787 state
788 .incidences()
789 .map(|record| QueryRow::single(QueryValue::Incidence(*record)))
790 .collect(),
791 )
792}
793
794fn scan_elements_with_label(state: &impl StateView, label_id: crate::LabelId) -> QueryResult {
796 QueryResult::new(
797 state
798 .elements_with_label(label_id)
799 .into_iter()
800 .map(|id| QueryRow::single(QueryValue::Element(id)))
801 .collect(),
802 )
803}
804
805fn scan_elements_with_property(
807 state: &impl StateView,
808 key: PropertyKeyId,
809 value: &PropertyValue,
810) -> Result<QueryResult, DbError> {
811 Ok(QueryResult::new(
812 state
813 .typed_property_equal_for_family(key, PropertyFamily::Element, value)?
814 .into_iter()
815 .filter_map(|subject| match subject {
816 PropertySubject::Element(id) => Some(QueryRow::single(QueryValue::Element(id))),
817 PropertySubject::Relation(_) | PropertySubject::Incidence(_) => None,
818 })
819 .collect(),
820 ))
821}
822
823fn filter_elements(state: &impl StateView, predicate: &BoundPredicate) -> QueryResult {
831 QueryResult::new(
832 state
833 .elements()
834 .filter(|record| predicate.evaluate(state, record.id))
835 .map(|record| QueryRow::single(QueryValue::Element(record.id)))
836 .collect(),
837 )
838}
839
840fn scan_relations_with_type(
842 state: &impl StateView,
843 relation_type: crate::RelationTypeId,
844) -> QueryResult {
845 QueryResult::new(
846 state
847 .relations_with_type(relation_type)
848 .into_iter()
849 .map(|id| QueryRow::single(QueryValue::Relation(id)))
850 .collect(),
851 )
852}
853
854fn scan_catalog(state: &impl StateView) -> QueryResult {
856 let rows = state
857 .catalog()
858 .projections()
859 .map(|entry| QueryRow {
860 values: vec![
861 QueryValue::Projection(entry.id),
862 QueryValue::Text(entry.definition.name().to_owned()),
863 ],
864 })
865 .collect();
866 QueryResult::new(rows)
867}
868
869fn execute_graph_walk(
871 state: &impl StateView,
872 projection: ProjectionId,
873 element: ElementId,
874 options: Walk,
875) -> Result<QueryResult, DbError> {
876 let graph = graph_projection(state, projection)?;
877 let subgraph = traversal::walk_graph_projection(&graph, &[element], options)?;
878 let rows = subgraph
879 .nodes
880 .iter()
881 .map(|node| QueryRow::single(QueryValue::Element(node.element)))
882 .collect();
883 Ok(QueryResult::new(rows))
884}
885
886fn graph_projection(
888 state: &impl StateView,
889 projection: ProjectionId,
890) -> Result<GraphProjection, DbError> {
891 let entry = state
892 .catalog()
893 .projection(projection)
894 .ok_or(DbError::UnknownProjection { id: projection })?;
895 match &entry.definition {
896 ProjectionDefinition::Graph(definition) => {
897 GraphProjection::from_state(state, definition.clone())
898 }
899 ProjectionDefinition::Hypergraph(_definition) => {
900 Err(DbError::invalid_projection("projection is not a graph"))
901 }
902 }
903}