1use shifty_algebra::Path;
37use spargebra::Query;
38use spargebra::algebra::{Expression, Function, GraphPattern, PropertyPathExpression};
39use spargebra::term::{NamedNode, NamedNodePattern, Term, TermPattern, TriplePattern};
40use std::collections::{HashMap, HashSet};
41
42pub type OpId = u32;
44pub type VarId = u32;
46
47#[derive(Debug, Clone)]
50pub enum ScanTerm {
51 Var(VarId),
52 Const(Term),
53}
54
55#[derive(Debug, Clone)]
58pub enum GraphScan {
59 Default,
60 Named(NamedNode),
61}
62
63#[derive(Debug, Clone)]
65pub struct TripleScan {
66 pub subject: ScanTerm,
67 pub predicate: ScanTerm,
68 pub object: ScanTerm,
69 pub graph: GraphScan,
70}
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
77pub enum ClosureKind {
78 Star,
80 Plus,
82 Opt,
84}
85
86#[derive(Debug, Clone)]
92pub struct PathScan {
93 pub subject: ScanTerm,
94 pub object: ScanTerm,
95 pub step: Path,
96 pub kind: ClosureKind,
97 pub graph: GraphScan,
98}
99
100#[derive(Debug, Clone)]
105pub enum ExprPlan {
106 Var(VarId),
107 Const(Term),
108 Bound(VarId),
109 Not(Box<ExprPlan>),
110 And(Box<ExprPlan>, Box<ExprPlan>),
111 Or(Box<ExprPlan>, Box<ExprPlan>),
112 SameTerm(Box<ExprPlan>, Box<ExprPlan>),
113 Str(Box<ExprPlan>),
115 StrStarts(Box<ExprPlan>, Box<ExprPlan>),
117 Equal(Box<ExprPlan>, Box<ExprPlan>),
122 Exists(OpId),
126}
127
128#[derive(Debug, Clone)]
131pub enum NativeOp {
132 InputFocus,
134 Scan { input: OpId, pattern: TripleScan },
136 PathScan { input: OpId, scan: PathScan },
138 Union { left: OpId, right: OpId },
140 Filter { input: OpId, expr: ExprPlan },
142 Extend {
144 input: OpId,
145 var: VarId,
146 expr: ExprPlan,
147 },
148 Project { input: OpId, vars: Vec<VarId> },
150 Distinct { input: OpId },
152}
153
154#[derive(Debug, Clone, Copy, PartialEq, Eq)]
157pub enum QueryForm {
158 Select,
159 Ask,
160}
161
162#[derive(Debug, Clone, Default)]
166pub struct PlanStats {
167 pub total_triples: u64,
168 pub distinct_subjects: u64,
169 pub distinct_objects: u64,
170 pub distinct_predicates: u64,
171 pub predicate_cardinality: HashMap<Term, u64>,
173}
174
175#[derive(Debug, Clone)]
177pub struct NativeQueryPlan {
178 pub nodes: Vec<NativeOp>,
179 pub root: OpId,
180 pub form: QueryForm,
181 pub var_names: Vec<String>,
183 pub focus_var: VarId,
185}
186
187impl NativeQueryPlan {
188 pub fn var_id(&self, name: &str) -> Option<VarId> {
190 self.var_names
191 .iter()
192 .position(|n| n == name)
193 .map(|i| i as VarId)
194 }
195}
196
197const FOCUS_VAR: &str = "this";
201
202struct Builder {
203 nodes: Vec<NativeOp>,
204 var_ids: HashMap<String, VarId>,
205 var_names: Vec<String>,
206 fresh: u32,
207}
208
209impl Builder {
210 fn new() -> Self {
211 Self {
212 nodes: Vec::new(),
213 var_ids: HashMap::new(),
214 var_names: Vec::new(),
215 fresh: 0,
216 }
217 }
218
219 fn var(&mut self, name: &str) -> VarId {
220 if let Some(&id) = self.var_ids.get(name) {
221 return id;
222 }
223 let id = self.var_names.len() as VarId;
224 self.var_names.push(name.to_string());
225 self.var_ids.insert(name.to_string(), id);
226 id
227 }
228
229 fn fresh_var(&mut self) -> VarId {
233 let name = format!("?path{}", self.fresh);
234 self.fresh += 1;
235 self.var(&name)
236 }
237
238 fn push(&mut self, op: NativeOp) -> OpId {
239 let id = self.nodes.len() as OpId;
240 self.nodes.push(op);
241 id
242 }
243
244 fn focus_var(&self) -> VarId {
245 *self
246 .var_ids
247 .get(FOCUS_VAR)
248 .expect("focus var registered first")
249 }
250}
251
252pub fn lower_query(query: &Query) -> Result<NativeQueryPlan, String> {
256 lower_query_with_stats(query, None)
257}
258
259pub fn lower_query_with_stats(
267 query: &Query,
268 stats: Option<&PlanStats>,
269) -> Result<NativeQueryPlan, String> {
270 let (pattern, form) = match query {
271 Query::Select { pattern, .. } => (pattern, QueryForm::Select),
272 Query::Ask { pattern, .. } => (pattern, QueryForm::Ask),
273 Query::Construct { .. } => return Err("CONSTRUCT query".into()),
274 Query::Describe { .. } => return Err("DESCRIBE query".into()),
275 };
276
277 let mut b = Builder::new();
278 let focus_var = b.var(FOCUS_VAR);
279 let input = b.push(NativeOp::InputFocus);
280 let root = lower_pattern(&mut b, pattern, input, &GraphScan::Default, stats)?;
281
282 Ok(NativeQueryPlan {
283 nodes: b.nodes,
284 root,
285 form,
286 var_names: b.var_names,
287 focus_var,
288 })
289}
290
291fn lower_pattern(
294 b: &mut Builder,
295 pattern: &GraphPattern,
296 input: OpId,
297 graph: &GraphScan,
298 stats: Option<&PlanStats>,
299) -> Result<OpId, String> {
300 match pattern {
301 GraphPattern::Bgp { patterns } => {
302 let mut scans: Vec<TripleScan> = patterns
303 .iter()
304 .map(|tp| lower_triple(b, tp, graph))
305 .collect::<Result<_, _>>()?;
306 if let Some(s) = stats {
307 reorder_bgp(&mut scans, b.focus_var(), s);
308 }
309 let mut current = input;
310 for scan in scans {
311 current = b.push(NativeOp::Scan {
312 input: current,
313 pattern: scan,
314 });
315 }
316 Ok(current)
317 }
318 GraphPattern::Join { left, right } => {
319 if let Some(s) = stats {
324 let mut leaves = Vec::new();
325 if collect_join_leaves(pattern, &mut leaves) && leaves.len() >= 2 {
326 return lower_join_leaves(b, leaves, input, graph, s);
327 }
328 }
329 let l = lower_pattern(b, left, input, graph, stats)?;
330 lower_pattern(b, right, l, graph, stats)
331 }
332 GraphPattern::Union { left, right } => {
333 let l = lower_pattern(b, left, input, graph, stats)?;
334 let r = lower_pattern(b, right, input, graph, stats)?;
335 Ok(b.push(NativeOp::Union { left: l, right: r }))
336 }
337 GraphPattern::Filter { expr, inner } => {
338 let i = lower_pattern(b, inner, input, graph, stats)?;
339 let e = lower_expr(b, expr, graph, stats)?;
340 Ok(b.push(NativeOp::Filter { input: i, expr: e }))
341 }
342 GraphPattern::Extend {
343 inner,
344 variable,
345 expression,
346 } => {
347 let i = lower_pattern(b, inner, input, graph, stats)?;
348 let e = lower_expr(b, expression, graph, stats)?;
349 let var = b.var(variable.as_str());
350 Ok(b.push(NativeOp::Extend {
351 input: i,
352 var,
353 expr: e,
354 }))
355 }
356 GraphPattern::Project { inner, variables } => {
357 let i = lower_pattern(b, inner, input, graph, stats)?;
358 let vars = variables.iter().map(|v| b.var(v.as_str())).collect();
359 Ok(b.push(NativeOp::Project { input: i, vars }))
360 }
361 GraphPattern::Distinct { inner } => {
362 let i = lower_pattern(b, inner, input, graph, stats)?;
363 Ok(b.push(NativeOp::Distinct { input: i }))
364 }
365 GraphPattern::Graph { name, inner } => match name {
366 NamedNodePattern::NamedNode(nn) => {
367 lower_pattern(b, inner, input, &GraphScan::Named(nn.clone()), stats)
368 }
369 NamedNodePattern::Variable(_) => Err("variable GRAPH name".into()),
370 },
371 GraphPattern::Path {
372 subject,
373 path,
374 object,
375 } => {
376 let s = lower_term_pattern(b, subject)?;
377 let o = lower_term_pattern(b, object)?;
378 lower_path(b, s, path, o, input, graph)
379 }
380 GraphPattern::Reduced { .. } => Err("REDUCED".into()),
381 GraphPattern::Values { .. } => Err("inline VALUES".into()),
382 GraphPattern::OrderBy { .. } => Err("ORDER BY".into()),
383 GraphPattern::Slice { .. } => Err("LIMIT/OFFSET".into()),
384 GraphPattern::Group { .. } => Err("aggregates (GROUP BY)".into()),
385 GraphPattern::Service { .. } => Err("SERVICE".into()),
386 GraphPattern::LeftJoin { .. } => Err("OPTIONAL".into()),
387 GraphPattern::Minus { .. } => Err("MINUS".into()),
388 GraphPattern::Lateral { .. } => Err("LATERAL".into()),
389 }
390}
391
392fn reorder_bgp(scans: &mut Vec<TripleScan>, focus_var: VarId, stats: &PlanStats) {
401 if scans.len() < 2 {
402 return;
403 }
404 let mut bound: HashSet<VarId> = std::iter::once(focus_var).collect();
405 let mut ordered: Vec<TripleScan> = Vec::with_capacity(scans.len());
406 let mut remaining: Vec<TripleScan> = std::mem::take(scans);
407
408 while !remaining.is_empty() {
409 let best = remaining
410 .iter()
411 .enumerate()
412 .min_by_key(|(_, s)| estimate_scan_cost(s, &bound, stats))
413 .map(|(i, _)| i)
414 .unwrap();
415 let scan = remaining.remove(best);
416 for v in scan_free_vars(&scan, &bound) {
417 bound.insert(v);
418 }
419 ordered.push(scan);
420 }
421 *scans = ordered;
422}
423
424fn scan_free_vars(scan: &TripleScan, bound: &HashSet<VarId>) -> Vec<VarId> {
427 [&scan.subject, &scan.predicate, &scan.object]
428 .into_iter()
429 .filter_map(|t| {
430 if let ScanTerm::Var(v) = t {
431 (!bound.contains(v)).then_some(*v)
432 } else {
433 None
434 }
435 })
436 .collect()
437}
438
439fn estimate_scan_cost(scan: &TripleScan, bound: &HashSet<VarId>, stats: &PlanStats) -> u64 {
444 let s = is_bound(&scan.subject, bound);
445 let p = is_bound(&scan.predicate, bound);
446 let o = is_bound(&scan.object, bound);
447 let total = stats.total_triples.max(1);
448 let ds = stats.distinct_subjects.max(1);
449 let dp = stats.distinct_predicates.max(1);
450 let dobj = stats.distinct_objects.max(1);
451
452 match (s, p, o) {
453 (true, true, true) => 1,
454 (true, true, false) => {
455 pred_card(&scan.predicate, stats).unwrap_or(total / dp) / ds + 1
457 }
458 (false, true, true) => pred_card(&scan.predicate, stats).unwrap_or(total / dp) / dobj + 1,
459 (false, true, false) => pred_card(&scan.predicate, stats).unwrap_or(total / dp),
460 (true, false, false) => total / ds,
461 (false, false, true) => total / dobj,
462 (true, false, true) | (false, false, false) => total,
464 }
465}
466
467fn is_bound(term: &ScanTerm, bound: &HashSet<VarId>) -> bool {
468 match term {
469 ScanTerm::Const(_) => true,
470 ScanTerm::Var(v) => bound.contains(v),
471 }
472}
473
474fn pred_card(term: &ScanTerm, stats: &PlanStats) -> Option<u64> {
478 match term {
479 ScanTerm::Const(t) => Some(*stats.predicate_cardinality.get(t).unwrap_or(&0)),
480 ScanTerm::Var(_) => None,
481 }
482}
483
484fn collect_join_leaves<'a>(pattern: &'a GraphPattern, out: &mut Vec<&'a GraphPattern>) -> bool {
490 match pattern {
491 GraphPattern::Join { left, right } => {
492 collect_join_leaves(left, out) && collect_join_leaves(right, out)
493 }
494 GraphPattern::Bgp { .. } | GraphPattern::Path { .. } => {
495 out.push(pattern);
496 true
497 }
498 _ => false,
499 }
500}
501
502fn lower_join_leaves(
509 b: &mut Builder,
510 mut leaves: Vec<&GraphPattern>,
511 input: OpId,
512 graph: &GraphScan,
513 stats: &PlanStats,
514) -> Result<OpId, String> {
515 let mut bound: HashSet<String> = std::iter::once(FOCUS_VAR.to_string()).collect();
516 let mut current = input;
517 while !leaves.is_empty() {
518 let costs: Vec<u64> = leaves
519 .iter()
520 .map(|p| estimate_pattern_cost(p, &bound, stats))
521 .collect();
522 let best = costs
523 .iter()
524 .enumerate()
525 .min_by_key(|(_, c)| *c)
526 .map(|(i, _)| i)
527 .unwrap();
528 let pat = leaves.remove(best);
529 bound.extend(pattern_new_vars(pat, &bound));
530 current = lower_pattern(b, pat, current, graph, Some(stats))?;
531 }
532 Ok(current)
533}
534
535fn pattern_new_vars(pattern: &GraphPattern, bound: &HashSet<String>) -> Vec<String> {
537 let mut vars: HashSet<String> = HashSet::new();
538 match pattern {
539 GraphPattern::Bgp { patterns } => {
540 for tp in patterns {
541 match &tp.subject {
542 TermPattern::Variable(v) if !bound.contains(v.as_str()) => {
543 vars.insert(v.as_str().to_string());
544 }
545 TermPattern::BlankNode(bn) => {
548 let name = format!("_:{}", bn.as_str());
549 if !bound.contains(&name) {
550 vars.insert(name);
551 }
552 }
553 _ => {}
554 }
555 if let NamedNodePattern::Variable(v) = &tp.predicate
556 && !bound.contains(v.as_str())
557 {
558 vars.insert(v.as_str().to_string());
559 }
560 match &tp.object {
561 TermPattern::Variable(v) if !bound.contains(v.as_str()) => {
562 vars.insert(v.as_str().to_string());
563 }
564 TermPattern::BlankNode(bn) => {
565 let name = format!("_:{}", bn.as_str());
566 if !bound.contains(&name) {
567 vars.insert(name);
568 }
569 }
570 _ => {}
571 }
572 }
573 }
574 GraphPattern::Path {
575 subject, object, ..
576 } => {
577 match subject {
578 TermPattern::Variable(v) if !bound.contains(v.as_str()) => {
579 vars.insert(v.as_str().to_string());
580 }
581 TermPattern::BlankNode(bn) => {
582 let name = format!("_:{}", bn.as_str());
583 if !bound.contains(&name) {
584 vars.insert(name);
585 }
586 }
587 _ => {}
588 }
589 match object {
590 TermPattern::Variable(v) if !bound.contains(v.as_str()) => {
591 vars.insert(v.as_str().to_string());
592 }
593 TermPattern::BlankNode(bn) => {
594 let name = format!("_:{}", bn.as_str());
595 if !bound.contains(&name) {
596 vars.insert(name);
597 }
598 }
599 _ => {}
600 }
601 }
602 _ => {}
603 }
604 vars.into_iter().collect()
605}
606
607fn estimate_pattern_cost(
611 pattern: &GraphPattern,
612 bound: &HashSet<String>,
613 stats: &PlanStats,
614) -> u64 {
615 let total = stats.total_triples.max(1);
616 let ds = stats.distinct_subjects.max(1);
617 let dp = stats.distinct_predicates.max(1);
618 let dobj = stats.distinct_objects.max(1);
619
620 match pattern {
621 GraphPattern::Bgp { patterns } => {
622 let tp_cost = |tp: &TriplePattern, sim_bound: &HashSet<String>| -> u64 {
627 let s = tp_name_is_bound(&tp.subject, sim_bound);
628 let p = nnp_name_is_bound(&tp.predicate, sim_bound);
629 let o = tp_name_is_bound(&tp.object, sim_bound);
630 let pred_est = match &tp.predicate {
631 NamedNodePattern::NamedNode(nn) => stats
632 .predicate_cardinality
633 .get(&Term::NamedNode(nn.clone()))
634 .copied()
635 .unwrap_or(total / dp),
636 NamedNodePattern::Variable(_) => total / dp,
637 };
638 match (s, p, o) {
639 (true, true, true) => 1,
640 (true, true, false) => pred_est / ds + 1,
641 (false, true, true) => pred_est / dobj + 1,
642 (false, true, false) => pred_est,
643 (true, false, false) => total / ds,
644 (false, false, true) => total / dobj,
645 _ => total,
646 }
647 };
648 let mut sim_bound = bound.clone();
649 let mut remaining: Vec<&TriplePattern> = patterns.iter().collect();
650 let mut output: u64 = 1;
651 let mut first = true;
652 while !remaining.is_empty() {
653 let best = remaining
654 .iter()
655 .enumerate()
656 .min_by_key(|(_, tp)| tp_cost(tp, &sim_bound))
657 .map(|(i, _)| i)
658 .unwrap();
659 let tp = remaining.remove(best);
660 let step_cost = tp_cost(tp, &sim_bound);
661 let s_bound = tp_name_is_bound(&tp.subject, &sim_bound);
667 if first || !s_bound {
668 output = output.saturating_mul(step_cost);
669 }
670 first = false;
671 match &tp.subject {
672 TermPattern::Variable(v) => {
673 sim_bound.insert(v.as_str().to_string());
674 }
675 TermPattern::BlankNode(bn) => {
676 sim_bound.insert(format!("_:{}", bn.as_str()));
677 }
678 _ => {}
679 }
680 if let NamedNodePattern::Variable(v) = &tp.predicate {
681 sim_bound.insert(v.as_str().to_string());
682 }
683 match &tp.object {
684 TermPattern::Variable(v) => {
685 sim_bound.insert(v.as_str().to_string());
686 }
687 TermPattern::BlankNode(bn) => {
688 sim_bound.insert(format!("_:{}", bn.as_str()));
689 }
690 _ => {}
691 }
692 }
693 output
694 }
695
696 GraphPattern::Path {
697 subject, object, ..
698 } => {
699 let s = tp_name_is_bound(subject, bound);
700 let o = tp_name_is_bound(object, bound);
701 match (s, o) {
702 (true, true) => 1,
703 (true, false) | (false, true) => total / ds / 2 + 1,
704 (false, false) => total,
705 }
706 }
707
708 _ => total,
709 }
710}
711
712fn tp_name_is_bound(tp: &TermPattern, bound: &HashSet<String>) -> bool {
713 match tp {
714 TermPattern::Variable(v) => bound.contains(v.as_str()),
715 TermPattern::BlankNode(bn) => bound.contains(&format!("_:{}", bn.as_str())),
720 _ => true, }
722}
723
724fn nnp_name_is_bound(nnp: &NamedNodePattern, bound: &HashSet<String>) -> bool {
725 match nnp {
726 NamedNodePattern::NamedNode(_) => true,
727 NamedNodePattern::Variable(v) => bound.contains(v.as_str()),
728 }
729}
730
731fn lower_triple(
732 b: &mut Builder,
733 tp: &TriplePattern,
734 graph: &GraphScan,
735) -> Result<TripleScan, String> {
736 Ok(TripleScan {
737 subject: lower_term_pattern(b, &tp.subject)?,
738 predicate: lower_named_node_pattern(b, &tp.predicate)?,
739 object: lower_term_pattern(b, &tp.object)?,
740 graph: graph.clone(),
741 })
742}
743
744fn lower_term_pattern(b: &mut Builder, tp: &TermPattern) -> Result<ScanTerm, String> {
745 match tp {
746 TermPattern::Variable(v) => Ok(ScanTerm::Var(b.var(v.as_str()))),
747 TermPattern::BlankNode(bn) => Ok(ScanTerm::Var(b.var(&format!("_:{}", bn.as_str())))),
749 TermPattern::NamedNode(n) => Ok(ScanTerm::Const(Term::NamedNode(n.clone()))),
750 TermPattern::Literal(l) => Ok(ScanTerm::Const(Term::Literal(l.clone()))),
751 #[allow(unreachable_patterns)]
752 _ => Err("rdf-star triple term".into()),
753 }
754}
755
756fn lower_named_node_pattern(b: &mut Builder, np: &NamedNodePattern) -> Result<ScanTerm, String> {
757 match np {
758 NamedNodePattern::NamedNode(n) => Ok(ScanTerm::Const(Term::NamedNode(n.clone()))),
759 NamedNodePattern::Variable(v) => Ok(ScanTerm::Var(b.var(v.as_str()))),
760 }
761}
762
763fn lower_path(
769 b: &mut Builder,
770 subject: ScanTerm,
771 path: &PropertyPathExpression,
772 object: ScanTerm,
773 input: OpId,
774 graph: &GraphScan,
775) -> Result<OpId, String> {
776 match path {
777 PropertyPathExpression::NamedNode(p) => {
778 let pattern = TripleScan {
779 subject,
780 predicate: ScanTerm::Const(Term::NamedNode(p.clone())),
781 object,
782 graph: graph.clone(),
783 };
784 Ok(b.push(NativeOp::Scan { input, pattern }))
785 }
786 PropertyPathExpression::Reverse(p) => lower_path(b, object, p, subject, input, graph),
788 PropertyPathExpression::Sequence(p1, p2) => {
790 let mid = ScanTerm::Var(b.fresh_var());
791 let first = lower_path(b, subject, p1, mid.clone(), input, graph)?;
792 lower_path(b, mid, p2, object, first, graph)
793 }
794 PropertyPathExpression::Alternative(p1, p2) => {
796 let left = lower_path(b, subject.clone(), p1, object.clone(), input, graph)?;
797 let right = lower_path(b, subject, p2, object, input, graph)?;
798 Ok(b.push(NativeOp::Union { left, right }))
799 }
800 PropertyPathExpression::ZeroOrMore(p) => {
801 lower_closure(b, subject, p, object, ClosureKind::Star, input, graph)
802 }
803 PropertyPathExpression::OneOrMore(p) => {
804 lower_closure(b, subject, p, object, ClosureKind::Plus, input, graph)
805 }
806 PropertyPathExpression::ZeroOrOne(p) => {
807 lower_closure(b, subject, p, object, ClosureKind::Opt, input, graph)
808 }
809 PropertyPathExpression::NegatedPropertySet(_) => Err("negated property set".into()),
810 }
811}
812
813fn property_path_to_algebra(path: &PropertyPathExpression) -> Option<Path> {
816 match path {
817 PropertyPathExpression::NamedNode(n) => Some(Path::Pred(n.clone())),
818 PropertyPathExpression::Reverse(p) => {
819 property_path_to_algebra(p).map(|inner| inner.inverse())
820 }
821 PropertyPathExpression::Sequence(a, b) => {
822 let la = property_path_to_algebra(a)?;
823 let lb = property_path_to_algebra(b)?;
824 Some(Path::seq(vec![la, lb]))
825 }
826 PropertyPathExpression::Alternative(a, b) => {
827 let la = property_path_to_algebra(a)?;
828 let lb = property_path_to_algebra(b)?;
829 Some(Path::alt(vec![la, lb]))
830 }
831 PropertyPathExpression::ZeroOrMore(p) => {
832 property_path_to_algebra(p).map(|inner| inner.star())
833 }
834 PropertyPathExpression::OneOrMore(p) => {
835 property_path_to_algebra(p).map(|inner| inner.one_or_more())
836 }
837 PropertyPathExpression::ZeroOrOne(p) => {
838 property_path_to_algebra(p).map(|inner| inner.zero_or_one())
839 }
840 PropertyPathExpression::NegatedPropertySet(_) => None,
841 }
842}
843
844fn lower_closure(
848 b: &mut Builder,
849 subject: ScanTerm,
850 p: &PropertyPathExpression,
851 object: ScanTerm,
852 kind: ClosureKind,
853 input: OpId,
854 graph: &GraphScan,
855) -> Result<OpId, String> {
856 let step = property_path_to_algebra(p).ok_or("negated property set in closure")?;
857 let scan = PathScan {
858 subject,
859 object,
860 step,
861 kind,
862 graph: graph.clone(),
863 };
864 Ok(b.push(NativeOp::PathScan { input, scan }))
865}
866
867fn lower_expr(
874 b: &mut Builder,
875 expr: &Expression,
876 graph: &GraphScan,
877 stats: Option<&PlanStats>,
878) -> Result<ExprPlan, String> {
879 match expr {
880 Expression::Variable(v) => Ok(ExprPlan::Var(b.var(v.as_str()))),
881 Expression::NamedNode(n) => Ok(ExprPlan::Const(Term::NamedNode(n.clone()))),
882 Expression::Literal(l) => Ok(ExprPlan::Const(Term::Literal(l.clone()))),
883 Expression::Bound(v) => Ok(ExprPlan::Bound(b.var(v.as_str()))),
884 Expression::Not(a) => Ok(ExprPlan::Not(Box::new(lower_expr(b, a, graph, stats)?))),
885 Expression::And(a, c) => Ok(ExprPlan::And(
886 Box::new(lower_expr(b, a, graph, stats)?),
887 Box::new(lower_expr(b, c, graph, stats)?),
888 )),
889 Expression::Or(a, c) => Ok(ExprPlan::Or(
890 Box::new(lower_expr(b, a, graph, stats)?),
891 Box::new(lower_expr(b, c, graph, stats)?),
892 )),
893 Expression::SameTerm(a, c) => Ok(ExprPlan::SameTerm(
894 Box::new(lower_expr(b, a, graph, stats)?),
895 Box::new(lower_expr(b, c, graph, stats)?),
896 )),
897 Expression::Equal(a, c) => Ok(ExprPlan::Equal(
898 Box::new(lower_expr(b, a, graph, stats)?),
899 Box::new(lower_expr(b, c, graph, stats)?),
900 )),
901 Expression::Exists(pattern) => {
905 let leaf = b.push(NativeOp::InputFocus);
906 let root = lower_pattern(b, pattern, leaf, graph, stats)?;
907 Ok(ExprPlan::Exists(root))
908 }
909 Expression::Greater(..)
910 | Expression::GreaterOrEqual(..)
911 | Expression::Less(..)
912 | Expression::LessOrEqual(..) => Err("ordered comparison".into()),
913 Expression::In(..) => Err("IN".into()),
914 Expression::Add(..)
915 | Expression::Subtract(..)
916 | Expression::Multiply(..)
917 | Expression::Divide(..)
918 | Expression::UnaryPlus(_)
919 | Expression::UnaryMinus(_) => Err("arithmetic".into()),
920 Expression::If(..) => Err("IF".into()),
921 Expression::Coalesce(_) => Err("COALESCE".into()),
922 Expression::FunctionCall(function, args) => match (function, args.as_slice()) {
923 (Function::Str, [arg]) => {
924 Ok(ExprPlan::Str(Box::new(lower_expr(b, arg, graph, stats)?)))
925 }
926 (Function::StrStarts, [text, prefix]) => Ok(ExprPlan::StrStarts(
927 Box::new(lower_expr(b, text, graph, stats)?),
928 Box::new(lower_expr(b, prefix, graph, stats)?),
929 )),
930 _ => Err("function call".into()),
931 },
932 }
933}
934
935#[cfg(test)]
936mod tests {
937 use super::*;
938 use spargebra::SparqlParser;
939
940 fn parse(q: &str) -> Query {
941 SparqlParser::new().parse_query(q).unwrap()
942 }
943
944 #[test]
945 fn lowers_simple_bgp_select() {
946 let q = parse("SELECT ?value WHERE { ?this <http://ex/p> ?value }");
947 let plan = lower_query(&q).expect("should lower");
948 assert_eq!(plan.form, QueryForm::Select);
949 assert!(matches!(plan.nodes[0], NativeOp::InputFocus));
951 assert!(
952 plan.nodes
953 .iter()
954 .any(|op| matches!(op, NativeOp::Scan { .. }))
955 );
956 assert!(plan.var_names.iter().any(|n| n == "value"));
957 }
958
959 #[test]
960 fn lowers_ask() {
961 let q = parse("ASK { ?this <http://ex/p> ?o }");
962 let plan = lower_query(&q).expect("should lower");
963 assert_eq!(plan.form, QueryForm::Ask);
964 }
965
966 #[test]
967 fn lowers_union() {
968 let q = parse(
969 "SELECT ?o WHERE { { ?this <http://ex/p> ?o } UNION { ?this <http://ex/q> ?o } }",
970 );
971 let plan = lower_query(&q).expect("should lower");
972 assert!(
973 plan.nodes
974 .iter()
975 .any(|op| matches!(op, NativeOp::Union { .. }))
976 );
977 }
978
979 #[test]
980 fn lowers_safe_filter() {
981 let q = parse("ASK { ?this <http://ex/p> ?o FILTER (bound(?o) && !sameTerm(?o, ?this)) }");
982 let plan = lower_query(&q).expect("should lower");
983 assert!(
984 plan.nodes
985 .iter()
986 .any(|op| matches!(op, NativeOp::Filter { .. }))
987 );
988 }
989
990 #[test]
991 fn lowers_strstarts_over_str() {
992 let q = parse("ASK { ?this ?p ?o FILTER (STRSTARTS(STR(?p), \"http://ex/\")) }");
993 lower_query(&q).expect("STRSTARTS(STR(...), literal) should lower");
994 }
995
996 #[test]
997 fn arbitrary_length_path_lowers_to_pathscan() {
998 let q = parse("ASK { ?this <http://ex/p>* ?o }");
999 let plan = lower_query(&q).expect("should lower");
1000 assert!(plan.nodes.iter().any(|op| matches!(
1001 op,
1002 NativeOp::PathScan { scan, .. } if scan.kind == ClosureKind::Star
1003 )));
1004 }
1005
1006 #[test]
1007 fn sequence_path_decomposes_to_scans() {
1008 let q = parse("SELECT ?o WHERE { ?this <http://ex/p>/<http://ex/q> ?o }");
1010 let plan = lower_query(&q).expect("should lower");
1011 assert_eq!(
1012 plan.nodes
1013 .iter()
1014 .filter(|op| matches!(op, NativeOp::Scan { .. }))
1015 .count(),
1016 2
1017 );
1018 assert!(
1019 !plan
1020 .nodes
1021 .iter()
1022 .any(|op| matches!(op, NativeOp::PathScan { .. }))
1023 );
1024 }
1025
1026 #[test]
1027 fn not_exists_filter_lowers() {
1028 let q = parse("ASK { ?this <http://ex/p> ?o FILTER NOT EXISTS { ?o <http://ex/q> ?w } }");
1029 let plan = lower_query(&q).expect("should lower");
1030 assert!(
1031 plan.nodes
1032 .iter()
1033 .any(|op| matches!(op, NativeOp::Filter { .. }))
1034 );
1035 }
1036
1037 #[test]
1038 fn negated_property_set_falls_back() {
1039 let q = parse("ASK { ?this !<http://ex/p> ?o }");
1040 assert!(lower_query(&q).is_err());
1041 }
1042
1043 #[test]
1044 fn value_equality_lowers() {
1045 let q = parse("ASK { ?this <http://ex/p> ?o FILTER (?o = <http://ex/target>) }");
1046 lower_query(&q).expect("= over IRIs should lower");
1047 }
1048
1049 #[test]
1050 fn optional_falls_back() {
1051 let q =
1052 parse("SELECT ?o WHERE { ?this <http://ex/p> ?o OPTIONAL { ?o <http://ex/q> ?w } }");
1053 assert!(lower_query(&q).is_err());
1054 }
1055
1056 #[test]
1057 fn fixed_graph_block_lowers() {
1058 let q = parse("ASK { GRAPH <urn:g> { ?this <http://ex/p> ?o } }");
1059 let plan = lower_query(&q).expect("should lower");
1060 assert!(plan.nodes.iter().any(
1061 |op| matches!(op, NativeOp::Scan { pattern, .. } if matches!(pattern.graph, GraphScan::Named(_)))
1062 ));
1063 }
1064
1065 #[test]
1068 fn bgp_reorder_moves_selective_scan_first() {
1069 let q = parse("SELECT ?x WHERE { ?x ?y ?z . ?this <http://ex/rare> ?x }");
1074 let rare = Term::NamedNode(NamedNode::new_unchecked("http://ex/rare"));
1075 let mut pcard = HashMap::new();
1076 pcard.insert(rare.clone(), 1u64);
1077 let stats = PlanStats {
1078 total_triples: 100_000,
1079 distinct_subjects: 10_000,
1080 distinct_objects: 10_000,
1081 distinct_predicates: 50,
1082 predicate_cardinality: pcard,
1083 };
1084 let plan = lower_query_with_stats(&q, Some(&stats)).expect("should lower");
1085 let scans: Vec<&TripleScan> = plan
1087 .nodes
1088 .iter()
1089 .filter_map(|op| {
1090 if let NativeOp::Scan { pattern, .. } = op {
1091 Some(pattern)
1092 } else {
1093 None
1094 }
1095 })
1096 .collect();
1097 let rare_pos = scans
1099 .iter()
1100 .position(|s| matches!(&s.predicate, ScanTerm::Const(t) if t == &rare));
1101 let free_pos = scans
1102 .iter()
1103 .position(|s| matches!(&s.predicate, ScanTerm::Var(_)));
1104 assert!(
1105 rare_pos < free_pos,
1106 "expected rare-predicate scan first; got rare={rare_pos:?} free={free_pos:?}",
1107 );
1108 }
1109
1110 #[test]
1115 fn join_flattens_three_way_nested() {
1116 use spargebra::term::{TriplePattern, Variable};
1117
1118 let nn = |s: &str| NamedNode::new_unchecked(s);
1119 let var = |s: &str| Variable::new_unchecked(s);
1120 let bgp = |s: &str, p: &str, o: &str| GraphPattern::Bgp {
1121 patterns: vec![TriplePattern {
1122 subject: TermPattern::Variable(var(s)),
1123 predicate: NamedNodePattern::NamedNode(nn(p)),
1124 object: TermPattern::Variable(var(o)),
1125 }],
1126 };
1127
1128 let bgp1 = bgp("this", "http://ex/common", "x");
1136 let bgp2 = bgp("x", "http://ex/medium", "y");
1137 let bgp3 = bgp("x", "http://ex/rare", "y");
1138
1139 let query = Query::Ask {
1140 pattern: GraphPattern::Join {
1141 left: Box::new(GraphPattern::Join {
1142 left: Box::new(bgp1),
1143 right: Box::new(bgp2),
1144 }),
1145 right: Box::new(bgp3),
1146 },
1147 dataset: None,
1148 base_iri: None,
1149 };
1150
1151 let mut pcard = HashMap::new();
1152 pcard.insert(Term::NamedNode(nn("http://ex/common")), 10_000u64);
1153 pcard.insert(Term::NamedNode(nn("http://ex/medium")), 10_000u64);
1154 pcard.insert(Term::NamedNode(nn("http://ex/rare")), 1u64);
1155 let stats = PlanStats {
1156 total_triples: 100_000,
1157 distinct_subjects: 10_000,
1158 distinct_objects: 10_000,
1159 distinct_predicates: 50,
1160 predicate_cardinality: pcard,
1161 };
1162
1163 let plan = lower_query_with_stats(&query, Some(&stats)).expect("should lower");
1164
1165 let rare = Term::NamedNode(nn("http://ex/rare"));
1166 let scans: Vec<&TripleScan> = plan
1167 .nodes
1168 .iter()
1169 .filter_map(|op| {
1170 if let NativeOp::Scan { pattern, .. } = op {
1171 Some(pattern)
1172 } else {
1173 None
1174 }
1175 })
1176 .collect();
1177
1178 assert_eq!(scans.len(), 3, "expected 3 scans; got {}", scans.len());
1179 assert!(
1180 matches!(&scans[0].predicate, ScanTerm::Const(t) if t == &rare),
1181 "rare-predicate scan should be first; got predicates: {:?}",
1182 scans.iter().map(|s| &s.predicate).collect::<Vec<_>>()
1183 );
1184 }
1185
1186 #[test]
1190 fn join_reorders_path_before_bgp_when_cheaper() {
1191 let q = parse(
1194 "ASK { ?this ?p ?v \
1195 . ?p <http://www.w3.org/1999/02/22-rdf-syntax-ns#type>* <http://ex/X> }",
1196 );
1197 let stats = PlanStats {
1198 total_triples: 100_000,
1199 distinct_subjects: 10_000,
1200 distinct_objects: 10_000,
1201 distinct_predicates: 50,
1202 predicate_cardinality: HashMap::new(),
1203 };
1204 let plan = lower_query_with_stats(&q, Some(&stats)).expect("should lower");
1205 let path_inputs_focus = plan
1207 .nodes
1208 .iter()
1209 .any(|op| matches!(op, NativeOp::PathScan { input, .. } if *input == 0));
1210 assert!(
1211 path_inputs_focus,
1212 "PathScan should be evaluated first (input = InputFocus)"
1213 );
1214 }
1215}