1use std::collections::{BTreeMap, HashMap};
18
19use crate::file::Rete;
20use crate::index::{GraphIndex, Pattern, Tile};
21use crate::row::{Ctx, Row, Slots, Val};
22
23#[derive(Debug, Clone, PartialEq, Eq)]
25pub enum PatternTerm {
26 Var(String),
27 Const(String),
28}
29
30impl PatternTerm {
31 pub fn parse(token: &str) -> Self {
33 if let Some(name) = token.strip_prefix('?') {
34 PatternTerm::Var(name.to_string())
35 } else {
36 PatternTerm::Const(token.to_string())
37 }
38 }
39}
40
41#[derive(Debug, Clone)]
43pub struct TriplePattern {
44 pub s: PatternTerm,
45 pub p: PatternTerm,
46 pub o: PatternTerm,
47}
48
49pub type Binding = BTreeMap<String, String>;
51
52#[derive(Clone, Copy)]
63enum SlotTerm {
64 Var(usize),
65 Node(u32),
66 Pred(u32),
67}
68
69fn pred_tag(p: u32) -> i64 {
70 -(p as i64) - 1
71}
72
73pub(crate) fn collect_pattern_slots(patterns: &[TriplePattern], slots: &mut Slots) {
75 for p in patterns {
76 for t in [&p.s, &p.p, &p.o] {
77 if let PatternTerm::Var(v) = t {
78 slots.add(v);
79 }
80 }
81 }
82}
83
84fn lower(patterns: &[TriplePattern], ctx: &Ctx) -> Option<Vec<(SlotTerm, SlotTerm, SlotTerm)>> {
87 let dict = ctx.rete.dictionary();
88 let node = |t: &PatternTerm| -> Option<SlotTerm> {
89 match t {
90 PatternTerm::Var(v) => ctx.slots.slot(v).map(SlotTerm::Var),
91 PatternTerm::Const(c) => dict.node_of_term(c).map(SlotTerm::Node),
92 }
93 };
94 let pred = |t: &PatternTerm| -> Option<SlotTerm> {
95 match t {
96 PatternTerm::Var(v) => ctx.slots.slot(v).map(SlotTerm::Var),
97 PatternTerm::Const(c) => dict.predicate_id(c).map(SlotTerm::Pred),
98 }
99 };
100 let mut lowered = Vec::with_capacity(patterns.len());
101 for p in patterns {
102 lowered.push((node(&p.s)?, pred(&p.p)?, node(&p.o)?));
103 }
104 Some(lowered)
105}
106
107pub fn eval_bgp(rete: &Rete, patterns: &[TriplePattern]) -> Vec<Binding> {
110 let mut slots = Slots::new();
111 collect_pattern_slots(patterns, &mut slots);
112 let ctx = Ctx::new(rete, slots);
113 eval_bgp_rows(&ctx, rete.default_index(), patterns)
114 .into_iter()
115 .map(|row| row_to_binding(&ctx, &row))
116 .collect()
117}
118
119pub(crate) fn row_to_binding(ctx: &Ctx, row: &Row) -> Binding {
122 let mut b = Binding::new();
123 for (i, v) in row.iter().enumerate() {
124 if let Some(val) = v {
125 if let Some(t) = ctx.resolver.str_once(val) {
126 b.insert(ctx.slots.name(i).to_string(), t);
127 }
128 }
129 }
130 b
131}
132
133pub(crate) fn eval_bgp_rows(ctx: &Ctx, index: &GraphIndex, patterns: &[TriplePattern]) -> Vec<Row> {
136 if patterns.is_empty() {
138 return vec![ctx.slots.empty_row()];
139 }
140 let Some(lowered) = lower(patterns, ctx) else {
142 return Vec::new();
143 };
144
145 let order = selectivity_order(ctx, index, &lowered);
149 let scan_est = tile_estimates(ctx, index, &lowered, &std::collections::HashSet::new());
153 let mut rows: Vec<Row> = vec![ctx.slots.empty_row()];
154 let mut bound: Vec<usize> = Vec::new();
155 let mut merged: [bool; 2] = [false, false];
156
157 if order.len() >= 2 {
162 if let Some((rel, slots)) =
163 try_merge_join(ctx, index, &lowered[order[0]], &lowered[order[1]])
164 {
165 rows = rel;
166 bound = slots;
167 merged = [true, true];
168 }
169 }
170
171 for (k, &idx) in order.iter().enumerate() {
172 if k < 2 && merged[k] {
173 continue; }
175 let t = lowered[idx];
176 let shares_bound = pattern_slots(&t).iter().any(|s| bound.contains(s));
186 let do_probe = !bound.is_empty()
197 && shares_bound
198 && !rows.is_empty()
199 && (probe_beats_scan(rows.len(), scan_est[idx])
200 || if index.is_remote() {
201 rows.len() <= remote_probe_max(index)
202 && (rows.len() <= REMOTE_PROBE_MIN || !pattern_is_selective(&t))
203 } else {
204 rows.len() <= BGP_PROBE_THRESHOLD
205 });
206 if do_probe {
207 let taken = std::mem::take(&mut rows);
208 if index.is_remote() {
214 let pats: Vec<Pattern> = taken
215 .iter()
216 .filter_map(|base| {
217 match (
218 probe_subject(ctx, &t.0, base),
219 probe_predicate(ctx, &t.1, base),
220 probe_object(ctx, &t.2, base),
221 ) {
222 (Some(s), Some(p), Some(o)) => Some((s, p, o)),
223 _ => None,
224 }
225 })
226 .collect();
227 index.prefetch_probe_tiles(&pats);
228 }
229 let mut next: Vec<Row> = Vec::with_capacity(taken.len());
230 for base in taken {
231 next.extend(probe_rows(ctx, index, t, base));
232 }
233 rows = next;
234 } else {
235 let Some((rel, rel_slots)) = pattern_rows(ctx, index, &t) else {
236 return Vec::new();
237 };
238 rows = hash_join(rows, &bound, rel, &rel_slots);
239 }
240 for s in pattern_slots(&t) {
241 if !bound.contains(&s) {
242 bound.push(s);
243 }
244 }
245 if rows.is_empty() {
246 break;
247 }
248 }
249 rows
250}
251
252fn try_merge_join(
264 ctx: &Ctx,
265 index: &GraphIndex,
266 ta: &(SlotTerm, SlotTerm, SlotTerm),
267 tb: &(SlotTerm, SlotTerm, SlotTerm),
268) -> Option<(Vec<Row>, Vec<usize>)> {
269 let dict = ctx.rete.dictionary();
270 let sa = pattern_slots(ta);
271 let sb = pattern_slots(tb);
272 let shared: Vec<usize> = sa.iter().copied().filter(|s| sb.contains(s)).collect();
273 if shared.len() != 1 {
274 return None;
275 }
276 let v = shared[0];
277 let col_of = |t: &(SlotTerm, SlotTerm, SlotTerm)| -> Option<usize> {
278 [&t.0, &t.1, &t.2]
279 .iter()
280 .position(|x| matches!(x, SlotTerm::Var(i) if *i == v))
281 };
282 let ca = col_of(ta)?;
283 let cb = col_of(tb)?;
284 if ca != cb {
285 return None; }
287 let (lo, hi) = {
297 let (a, b) = (pattern_scan_bytes(index, ta), pattern_scan_bytes(index, tb));
298 (a.min(b), a.max(b))
299 };
300 if hi >= FAT_SCAN_BYTES && hi / 4 >= lo {
301 return None;
302 }
303 let lower = |t: &(SlotTerm, SlotTerm, SlotTerm)| -> Option<Pattern> {
304 Some((
305 const_subject(&t.0, dict)?,
306 const_predicate(&t.1)?,
307 const_object(&t.2, dict)?,
308 ))
309 };
310 let collect = |t: &(SlotTerm, SlotTerm, SlotTerm), col: usize| -> Option<Vec<(u32, Row)>> {
313 let pat = lower(t)?;
314 let mut out = Vec::new();
315 for tri in index.scan_iter_sorted_on(pat, col)? {
316 if let Some(r) = triple_row(ctx, t, tri) {
317 out.push(([tri.0, tri.1, tri.2][col], r));
318 }
319 }
320 Some(out)
321 };
322 let rows_a = collect(ta, ca)?;
323 let rows_b = collect(tb, cb)?;
324
325 let mut out: Vec<Row> = Vec::new();
326 let (mut i, mut j) = (0usize, 0usize);
327 while i < rows_a.len() && j < rows_b.len() {
328 match rows_a[i].0.cmp(&rows_b[j].0) {
329 std::cmp::Ordering::Less => i += 1,
330 std::cmp::Ordering::Greater => j += 1,
331 std::cmp::Ordering::Equal => {
332 let key = rows_a[i].0;
333 let (i0, j0) = (i, j);
334 while i < rows_a.len() && rows_a[i].0 == key {
335 i += 1;
336 }
337 while j < rows_b.len() && rows_b[j].0 == key {
338 j += 1;
339 }
340 for (_, ra) in &rows_a[i0..i] {
341 for (_, rb) in &rows_b[j0..j] {
342 let mut row = ra.clone();
345 let mut ok = true;
346 for (slot, val) in rb.iter().enumerate() {
347 if let Some(val) = val {
348 match &row[slot] {
349 Some(existing) if existing != val => {
350 ok = false;
351 break;
352 }
353 _ => row[slot] = Some(val.clone()),
354 }
355 }
356 }
357 if ok {
358 out.push(row);
359 }
360 }
361 }
362 }
363 }
364 }
365 let mut slots = pattern_slots(ta);
366 for s in pattern_slots(tb) {
367 if !slots.contains(&s) {
368 slots.push(s);
369 }
370 }
371 Some((out, slots))
372}
373
374const BGP_PROBE_THRESHOLD: usize = 512;
382
383const PROBE_SCAN_FLOOR: f64 = 65_536.0;
388
389const PROBE_RATIO: f64 = 16.0;
394
395fn probe_beats_scan(rows: usize, scan_est: f64) -> bool {
398 scan_est >= limits().probe_floor && (rows as f64) * PROBE_RATIO <= scan_est
399}
400
401#[derive(Clone, Copy)]
404struct PlanLimits {
405 probe_floor: f64,
406 stream_seed: f64,
407 hash_build: f64,
408}
409
410#[cfg(test)]
411thread_local! {
412 static TEST_LIMITS: std::cell::Cell<Option<PlanLimits>> = const { std::cell::Cell::new(None) };
413}
414
415fn limits() -> PlanLimits {
416 #[cfg(test)]
417 if let Some(l) = TEST_LIMITS.with(|c| c.get()) {
418 return l;
419 }
420 PlanLimits {
421 probe_floor: PROBE_SCAN_FLOOR,
422 stream_seed: STREAM_SEED_ROWS,
423 hash_build: HASH_BUILD_ROWS,
424 }
425}
426
427const STREAM_SEED_ROWS: f64 = (1u64 << 21) as f64;
435
436const HASH_BUILD_ROWS: f64 = (1u64 << 20) as f64;
439
440fn tile_estimates(
446 ctx: &Ctx,
447 index: &GraphIndex,
448 lowered: &[(SlotTerm, SlotTerm, SlotTerm)],
449 seed: &std::collections::HashSet<usize>,
450) -> Vec<f64> {
451 const SEL_SUBJECT: f64 = 0.001;
452 const SEL_OBJECT: f64 = 0.02;
453 const SEL_PREDICATE: f64 = 0.05;
454 let dict = ctx.rete.dictionary();
455 let total = ctx.rete.header().quad_count;
456 let seeded = |x: &SlotTerm| matches!(x, SlotTerm::Var(v) if seed.contains(v));
457 lowered
458 .iter()
459 .map(|t| {
460 let (Some(s), Some(p), Some(o)) = (
461 const_subject(&t.0, dict),
462 const_predicate(&t.1),
463 const_object(&t.2, dict),
464 ) else {
465 return 0.0;
466 };
467 let mut est = index.estimate_matches((s, p, o), total);
468 if est == 0.0 {
469 return 0.0;
470 }
471 if seeded(&t.0) {
472 est *= SEL_SUBJECT;
473 }
474 if seeded(&t.1) {
475 est *= SEL_PREDICATE;
476 }
477 if seeded(&t.2) {
478 est *= SEL_OBJECT;
479 }
480 est.max(1.0)
481 })
482 .collect()
483}
484
485const REMOTE_PROBE_MIN: usize = 8;
494const REMOTE_PROBE_MAX: usize = 1024;
495
496fn remote_probe_max(index: &GraphIndex) -> usize {
503 REMOTE_PROBE_MAX * index.read_concurrency().clamp(1, 16)
504}
505
506pub(crate) fn prefetch_plan_probes(ctx: &Ctx, index: &GraphIndex, plan: &ProbePlan, rows: &[Row]) {
514 if !index.is_remote() {
515 return;
516 }
517 let Some(t) = plan.pats.first() else {
518 return;
519 };
520 let mut pats: Vec<Pattern> = Vec::new();
521 for base in rows {
522 if let (Some(s), Some(p), Some(o)) = (
523 probe_subject(ctx, &t.0, base),
524 probe_predicate(ctx, &t.1, base),
525 probe_object(ctx, &t.2, base),
526 ) {
527 if s.is_some() || o.is_some() {
528 pats.push((s, p, o));
529 }
530 }
531 }
532 index.prefetch_probe_tiles(&pats);
533}
534
535pub(crate) fn prefetch_subject_probes(
541 ctx: &Ctx,
542 index: &GraphIndex,
543 patterns: &[TriplePattern],
544 sv: &str,
545 subject_ids: &[u32],
546) {
547 if !index.is_remote() {
548 return;
549 }
550 let Some(lowered) = lower(patterns, ctx) else {
551 return;
552 };
553 let Some(slot) = ctx.slots.slot(sv) else {
554 return;
555 };
556 let dict = ctx.rete.dictionary();
557 let mut pats: Vec<Pattern> = Vec::new();
558 for &sid in subject_ids {
559 let mut base = ctx.slots.empty_row();
560 base[slot] = Some(Val::Id(dict.subject_node(sid) as i64));
561 for t in &lowered {
562 if !matches!(t.0, SlotTerm::Var(i) if i == slot) {
563 continue;
564 }
565 if let (Some(s), Some(p), Some(o)) = (
566 probe_subject(ctx, &t.0, &base),
567 probe_predicate(ctx, &t.1, &base),
568 probe_object(ctx, &t.2, &base),
569 ) {
570 pats.push((s, p, o));
571 }
572 }
573 }
574 index.prefetch_probe_tiles(&pats);
575}
576
577fn pattern_is_selective(t: &(SlotTerm, SlotTerm, SlotTerm)) -> bool {
580 matches!(t.0, SlotTerm::Node(_)) || matches!(t.2, SlotTerm::Node(_))
581}
582
583fn pattern_slots(t: &(SlotTerm, SlotTerm, SlotTerm)) -> Vec<usize> {
585 let mut slots: Vec<usize> = Vec::new();
586 for term in [&t.0, &t.1, &t.2] {
587 if let SlotTerm::Var(i) = term {
588 if !slots.contains(i) {
589 slots.push(*i);
590 }
591 }
592 }
593 slots
594}
595
596fn triple_row(
600 ctx: &Ctx,
601 t: &(SlotTerm, SlotTerm, SlotTerm),
602 (s_id, p_id, o_id): (u32, u32, u32),
603) -> Option<Row> {
604 let dict = ctx.rete.dictionary();
605 let s_val = dict.subject_node(s_id) as i64;
606 let p_val = ctx.resolver.canon_id(pred_tag(p_id));
607 let o_val = dict.object_node(o_id) as i64;
608 let mut row = ctx.slots.empty_row();
609 for (term, val) in [(&t.0, s_val), (&t.1, p_val), (&t.2, o_val)] {
610 if let SlotTerm::Var(i) = term {
611 match row[*i] {
612 Some(Val::Id(existing)) if existing != val => return None,
613 Some(_) => {}
614 None => row[*i] = Some(Val::Id(val)),
615 }
616 }
617 }
618 Some(row)
619}
620
621fn scan_rows<'q>(
626 ctx: &'q Ctx<'q>,
627 index: &'q GraphIndex,
628 t: (SlotTerm, SlotTerm, SlotTerm),
629) -> Option<impl Iterator<Item = Row> + 'q> {
630 let dict = ctx.rete.dictionary();
631 let (sid, pid, oid) = (
632 const_subject(&t.0, dict)?,
633 const_predicate(&t.1)?,
634 const_object(&t.2, dict)?,
635 );
636 Some(
637 index
638 .scan_iter((sid, pid, oid))
639 .filter_map(move |triple| triple_row(ctx, &t, triple)),
640 )
641}
642
643fn pattern_rows(
646 ctx: &Ctx,
647 index: &GraphIndex,
648 t: &(SlotTerm, SlotTerm, SlotTerm),
649) -> Option<(Vec<Row>, Vec<usize>)> {
650 let slots = pattern_slots(t);
651 let mut rel: Vec<Row> = Vec::new();
652 let dict = ctx.rete.dictionary();
653 let (sid, pid, oid) = (
654 const_subject(&t.0, dict)?,
655 const_predicate(&t.1)?,
656 const_object(&t.2, dict)?,
657 );
658 for triple in index.scan_iter((sid, pid, oid)) {
661 if let Some(row) = triple_row(ctx, t, triple) {
662 rel.push(row);
663 }
664 }
665 Some((rel, slots))
666}
667
668fn pattern_estimates(
680 ctx: &Ctx,
681 lowered: &[(SlotTerm, SlotTerm, SlotTerm)],
682 seed: &std::collections::HashSet<usize>,
683) -> Option<Vec<f64>> {
684 let pyr = ctx.rete.pyramid_if_loaded()?;
687 let mut pred: std::collections::HashMap<u32, u64> = std::collections::HashMap::new();
688 for e in &pyr.summary {
689 *pred.entry(e.predicate).or_insert(0) += e.count as u64;
690 }
691 let stats: std::collections::HashMap<u32, &crate::meta::PredStat> = pyr
694 .predicate_stats
695 .iter()
696 .map(|s| (s.predicate, s))
697 .collect();
698 let total = ctx.rete.header().quad_count.max(1) as f64;
699 let num_preds = pred.len().max(1) as f64;
700 const SEL_SUBJECT: f64 = 0.001;
702 const SEL_OBJECT: f64 = 0.02;
703 let node_bound = |t: &SlotTerm| match t {
704 SlotTerm::Node(_) => true,
705 SlotTerm::Var(v) => seed.contains(v),
706 SlotTerm::Pred(_) => false,
707 };
708 Some(
709 lowered
710 .iter()
711 .map(|t| {
712 let (base, st) = match t.1 {
717 SlotTerm::Pred(p) => {
718 (*pred.get(&p).unwrap_or(&0) as f64, stats.get(&p).copied())
719 }
720 SlotTerm::Var(v) if seed.contains(&v) => (total / num_preds, None),
721 _ => (total, None),
722 };
723 let mut est = base.max(1.0);
724 if node_bound(&t.0) {
725 est *= match st {
726 Some(s) if s.distinct_subjects > 0 => 1.0 / s.distinct_subjects as f64,
727 _ => SEL_SUBJECT,
728 };
729 }
730 if node_bound(&t.2) {
731 est *= match st {
732 Some(s) if s.distinct_objects > 0 => 1.0 / s.distinct_objects as f64,
733 _ => SEL_OBJECT,
734 };
735 }
736 est.max(1.0)
737 })
738 .collect(),
739 )
740}
741
742pub(crate) fn bgp_min_scan_bytes(
749 ctx: &Ctx,
750 index: &GraphIndex,
751 patterns: &[TriplePattern],
752) -> Option<u64> {
753 let lowered = lower(patterns, ctx)?;
754 lowered.iter().map(|t| pattern_scan_bytes(index, t)).min()
755}
756
757fn pattern_scan_bytes(index: &GraphIndex, t: &(SlotTerm, SlotTerm, SlotTerm)) -> u64 {
764 let sections = index.tile_sections();
765 let comp = |role: usize| match role {
766 0 => &t.0,
767 1 => &t.1,
768 _ => &t.2,
769 };
770 let mut best = u64::MAX;
771 for perm in index.perms().iter() {
775 let tiles = sections[perm.section_index()];
776 best = best.min(match comp(perm.roles()[0]) {
777 SlotTerm::Node(id) | SlotTerm::Pred(id) => tiles
778 .iter()
779 .filter(|tile| {
780 let (lo, hi) = tile.leading_range();
781 lo <= *id && *id <= hi
782 })
783 .map(Tile::encoded_len)
784 .sum(),
785 SlotTerm::Var(_) => tiles.iter().map(Tile::encoded_len).sum(),
786 });
787 }
788 best
789}
790
791pub(crate) const FAT_SCAN_BYTES: u64 = 2 << 20;
797
798fn selectivity_order(
805 ctx: &Ctx,
806 index: &GraphIndex,
807 lowered: &[(SlotTerm, SlotTerm, SlotTerm)],
808) -> Vec<usize> {
809 selectivity_order_seeded(ctx, index, lowered, &std::collections::HashSet::new())
810}
811
812fn selectivity_order_seeded(
816 ctx: &Ctx,
817 index: &GraphIndex,
818 lowered: &[(SlotTerm, SlotTerm, SlotTerm)],
819 seed: &std::collections::HashSet<usize>,
820) -> Vec<usize> {
821 let (estimates, per_class) = match pattern_estimates(ctx, lowered, seed) {
829 Some(e) => (Some(e), false),
830 None => (Some(tile_estimates(ctx, index, lowered, seed)), true),
831 };
832 let consts = |t: &(SlotTerm, SlotTerm, SlotTerm)| {
833 [&t.0, &t.1, &t.2]
834 .into_iter()
835 .filter(|x| match x {
836 SlotTerm::Var(v) => seed.contains(v),
837 _ => true,
838 })
839 .count()
840 };
841 let vars = |t: &(SlotTerm, SlotTerm, SlotTerm)| -> Vec<usize> {
842 [&t.0, &t.1, &t.2]
843 .into_iter()
844 .filter_map(|x| match x {
845 SlotTerm::Var(v) => Some(*v),
846 _ => None,
847 })
848 .collect()
849 };
850 let score = |i: usize| -> f64 {
854 match &estimates {
855 Some(e) => -e[i],
856 None => consts(&lowered[i]) as f64,
857 }
858 };
859 let type_pid = ctx.rete.dictionary().predicate_id(crate::file::RDF_TYPE);
867 let is_class_enum = |i: usize, bound: &std::collections::HashSet<usize>| -> bool {
868 let t = &lowered[i];
869 !per_class
870 && type_pid.is_some_and(|tp| matches!(t.1, SlotTerm::Pred(p) if p == tp))
871 && matches!(t.2, SlotTerm::Node(_))
872 && matches!(t.0, SlotTerm::Var(v) if !bound.contains(&v))
873 };
874 let n = lowered.len();
875 let mut remaining: Vec<usize> = (0..n).collect();
876 let mut order: Vec<usize> = Vec::with_capacity(n);
877 let mut bound: std::collections::HashSet<usize> = seed.clone();
878 while !remaining.is_empty() {
879 let best = *remaining
882 .iter()
883 .max_by(|&&a, &&b| {
884 let connected = |i: usize| vars(&lowered[i]).iter().any(|v| bound.contains(v));
885 connected(a)
886 .cmp(&connected(b))
887 .then_with(|| is_class_enum(b, &bound).cmp(&is_class_enum(a, &bound)))
888 .then_with(|| {
889 score(a)
890 .partial_cmp(&score(b))
891 .unwrap_or(std::cmp::Ordering::Equal)
892 })
893 .then_with(|| b.cmp(&a))
894 })
895 .unwrap();
896 for v in vars(&lowered[best]) {
897 bound.insert(v);
898 }
899 order.push(best);
900 remaining.retain(|&i| i != best);
901 }
902 order
903}
904
905pub(crate) fn bgp_exists(ctx: &Ctx, index: &GraphIndex, patterns: &[TriplePattern]) -> bool {
911 let dict = ctx.rete.dictionary();
912 let Some(lowered) = lower(patterns, ctx) else {
913 return false;
914 };
915 if let [t] = lowered.as_slice() {
916 let names: Vec<usize> = [&t.0, &t.1, &t.2]
920 .into_iter()
921 .filter_map(|x| match x {
922 SlotTerm::Var(v) => Some(*v),
923 _ => None,
924 })
925 .collect();
926 let distinct = names
927 .iter()
928 .enumerate()
929 .all(|(i, v)| !names[i + 1..].contains(v));
930 if distinct {
931 return match (
932 const_subject(&t.0, dict),
933 const_predicate(&t.1),
934 const_object(&t.2, dict),
935 ) {
936 (Some(s), Some(p), Some(o)) => index.scan_iter((s, p, o)).next().is_some(),
937 _ => false,
938 };
939 }
940 }
941 !eval_bgp_rows(ctx, index, patterns).is_empty()
942}
943
944pub(crate) struct BgpSolutions<'q> {
955 scan: Option<Box<dyn Iterator<Item = Row> + 'q>>,
956 prefix: Vec<Row>,
957 buckets: HashMap<Vec<Val>, Vec<usize>>,
959 shared: Vec<usize>,
960 cartesian: bool,
961 seed_only: bool,
964 cur_scan: Option<Row>,
965 matches: Vec<usize>,
967 mi: usize,
968}
969
970impl<'q> BgpSolutions<'q> {
971 fn passthrough(rows: Box<dyn Iterator<Item = Row> + 'q>) -> Self {
973 BgpSolutions {
974 scan: Some(rows),
975 prefix: Vec::new(),
976 buckets: HashMap::new(),
977 shared: Vec::new(),
978 cartesian: false,
979 seed_only: true,
980 cur_scan: None,
981 matches: Vec::new(),
982 mi: 0,
983 }
984 }
985
986 fn streaming(
995 ctx: &'q Ctx<'q>,
996 index: &'q GraphIndex,
997 lowered: &[(SlotTerm, SlotTerm, SlotTerm)],
998 order: &[usize],
999 est: &[f64],
1000 ) -> Self {
1001 let (mut stream, mut bound, skip): (Box<dyn Iterator<Item = Row> + 'q>, Vec<usize>, usize) =
1002 match (est[order[1]] > limits().stream_seed)
1003 .then(|| merge_stream(ctx, index, lowered[order[0]], lowered[order[1]]))
1004 .flatten()
1005 {
1006 Some((m, slots)) => (m, slots, 2),
1007 None => {
1008 let t = lowered[order[0]];
1009 let Some(scan) = scan_rows(ctx, index, t) else {
1010 return Self::empty();
1011 };
1012 (Box::new(scan), pattern_slots(&t), 1)
1013 }
1014 };
1015 for &i in &order[skip..] {
1016 let t = lowered[i];
1017 let slots = pattern_slots(&t);
1018 let shared: Vec<usize> = slots
1019 .iter()
1020 .copied()
1021 .filter(|s| bound.contains(s))
1022 .collect();
1023 if !shared.is_empty() && est[i] > limits().hash_build {
1024 stream = Box::new(stream.flat_map(move |base| probe_rows(ctx, index, t, base)));
1025 } else {
1026 let Some((rel, _)) = pattern_rows(ctx, index, &t) else {
1029 return Self::empty();
1030 };
1031 let mut table: HashMap<Vec<Val>, Vec<Row>> = HashMap::new();
1032 for r in rel {
1033 let key: Vec<Val> = shared.iter().map(|&s| r[s].clone().unwrap()).collect();
1034 table.entry(key).or_default().push(r);
1035 }
1036 let fill = slots.clone();
1037 stream = Box::new(stream.flat_map(move |base| {
1038 let key: Vec<Val> = shared.iter().map(|&s| base[s].clone().unwrap()).collect();
1039 let out: Vec<Row> = table
1040 .get(&key)
1041 .map(|rs| {
1042 rs.iter()
1043 .map(|r| {
1044 let mut row = base.clone();
1045 for &s in &fill {
1046 row[s] = r[s].clone();
1047 }
1048 row
1049 })
1050 .collect()
1051 })
1052 .unwrap_or_default();
1053 out.into_iter()
1054 }));
1055 }
1056 for s in slots {
1057 if !bound.contains(&s) {
1058 bound.push(s);
1059 }
1060 }
1061 }
1062 Self::passthrough(stream)
1063 }
1064
1065 fn empty() -> Self {
1067 BgpSolutions {
1068 scan: None,
1069 prefix: Vec::new(),
1070 buckets: HashMap::new(),
1071 shared: Vec::new(),
1072 cartesian: false,
1073 seed_only: false,
1074 cur_scan: None,
1075 matches: Vec::new(),
1076 mi: 0,
1077 }
1078 }
1079
1080 pub(crate) fn new(ctx: &'q Ctx<'q>, index: &'q GraphIndex, patterns: &[TriplePattern]) -> Self {
1081 if patterns.is_empty() {
1083 return BgpSolutions {
1084 scan: Some(Box::new(std::iter::once(ctx.slots.empty_row()))),
1085 prefix: vec![ctx.slots.empty_row()],
1086 buckets: HashMap::new(),
1087 shared: Vec::new(),
1088 cartesian: true,
1089 seed_only: true,
1090 cur_scan: None,
1091 matches: Vec::new(),
1092 mi: 0,
1093 };
1094 }
1095 let Some(lowered) = lower(patterns, ctx) else {
1097 return Self::empty();
1098 };
1099 let order = selectivity_order(ctx, index, &lowered);
1102 let scan_est = tile_estimates(ctx, index, &lowered, &std::collections::HashSet::new());
1103 if order.len() >= 2 && scan_est[order[0]] > limits().stream_seed {
1106 return Self::streaming(ctx, index, &lowered, &order, &scan_est);
1107 }
1108 let (&last_i, prefix_is) = order.split_last().unwrap();
1109 let prefix_pats: Vec<TriplePattern> =
1110 prefix_is.iter().map(|&i| patterns[i].clone()).collect();
1111 let prefix = eval_bgp_rows(ctx, index, &prefix_pats);
1112 if prefix.is_empty() {
1113 return Self::empty();
1114 }
1115
1116 let shared: Vec<usize> = pattern_slots(&lowered[last_i])
1120 .into_iter()
1121 .filter(|&s| prefix[0][s].is_some())
1122 .collect();
1123
1124 if !shared.is_empty() && probe_beats_scan(prefix.len(), scan_est[last_i]) {
1128 let t = lowered[last_i];
1129 if index.is_remote() {
1130 prefetch_rows_for(ctx, index, t, &prefix);
1131 }
1132 let stream = prefix
1133 .into_iter()
1134 .flat_map(move |base| probe_rows(ctx, index, t, base));
1135 return Self::passthrough(Box::new(stream));
1136 }
1137
1138 let Some(scan) = scan_rows(ctx, index, lowered[last_i]) else {
1140 return Self::empty();
1141 };
1142 let cartesian = shared.is_empty();
1143 let seed_only = prefix.len() == 1 && prefix[0].iter().all(Option::is_none);
1146 let mut buckets: HashMap<Vec<Val>, Vec<usize>> = HashMap::new();
1147 if !cartesian {
1148 for (i, r) in prefix.iter().enumerate() {
1149 let key: Vec<Val> = shared.iter().map(|&s| r[s].clone().unwrap()).collect();
1150 buckets.entry(key).or_default().push(i);
1151 }
1152 }
1153 BgpSolutions {
1154 scan: Some(Box::new(scan)),
1155 prefix,
1156 buckets,
1157 shared,
1158 cartesian,
1159 seed_only,
1160 cur_scan: None,
1161 matches: Vec::new(),
1162 mi: 0,
1163 }
1164 }
1165}
1166
1167impl Iterator for BgpSolutions<'_> {
1168 type Item = Row;
1169
1170 fn next(&mut self) -> Option<Row> {
1171 if self.seed_only {
1173 return self.scan.as_mut()?.next();
1174 }
1175 loop {
1176 if self.mi < self.matches.len() {
1178 let pi = self.matches[self.mi];
1179 self.mi += 1;
1180 let mut merged = self.prefix[pi].clone();
1181 for (slot, v) in self.cur_scan.as_ref().unwrap().iter().enumerate() {
1182 if v.is_some() {
1183 merged[slot] = v.clone();
1184 }
1185 }
1186 return Some(merged);
1187 }
1188 let s = self.scan.as_mut()?.next()?;
1191 self.matches.clear();
1192 if self.cartesian {
1193 self.matches.extend(0..self.prefix.len());
1194 } else {
1195 let key: Vec<Val> = self.shared.iter().map(|&i| s[i].clone().unwrap()).collect();
1196 if let Some(c) = self.buckets.get(&key) {
1197 self.matches.extend_from_slice(c);
1198 }
1199 }
1200 self.mi = 0;
1201 self.cur_scan = Some(s);
1202 }
1203 }
1204}
1205
1206fn prefetch_rows_for(
1209 ctx: &Ctx,
1210 index: &GraphIndex,
1211 t: (SlotTerm, SlotTerm, SlotTerm),
1212 rows: &[Row],
1213) {
1214 let pats: Vec<Pattern> = rows
1215 .iter()
1216 .filter_map(|base| {
1217 match (
1218 probe_subject(ctx, &t.0, base),
1219 probe_predicate(ctx, &t.1, base),
1220 probe_object(ctx, &t.2, base),
1221 ) {
1222 (Some(s), Some(p), Some(o)) => Some((s, p, o)),
1223 _ => None,
1224 }
1225 })
1226 .collect();
1227 index.prefetch_probe_tiles(&pats);
1228}
1229
1230#[allow(clippy::type_complexity)]
1238fn merge_stream<'q>(
1239 ctx: &'q Ctx<'q>,
1240 index: &'q GraphIndex,
1241 ta: (SlotTerm, SlotTerm, SlotTerm),
1242 tb: (SlotTerm, SlotTerm, SlotTerm),
1243) -> Option<(Box<dyn Iterator<Item = Row> + 'q>, Vec<usize>)> {
1244 let sa = pattern_slots(&ta);
1245 let sb = pattern_slots(&tb);
1246 let shared: Vec<usize> = sa.iter().copied().filter(|s| sb.contains(s)).collect();
1247 let [v] = shared[..] else {
1248 return None;
1249 };
1250 if !matches!(ta.0, SlotTerm::Var(i) if i == v) || !matches!(tb.0, SlotTerm::Var(i) if i == v) {
1251 return None;
1252 }
1253 let dict = ctx.rete.dictionary();
1254 let lower = |t: &(SlotTerm, SlotTerm, SlotTerm)| -> Option<Pattern> {
1255 Some((
1256 const_subject(&t.0, dict)?,
1257 const_predicate(&t.1)?,
1258 const_object(&t.2, dict)?,
1259 ))
1260 };
1261 let side = |t: (SlotTerm, SlotTerm, SlotTerm)| -> Option<KeyedRows<'q>> {
1262 let it = index.scan_iter_sorted_on(lower(&t)?, 0)?;
1263 let keyed: Box<dyn Iterator<Item = (u32, Row)> + 'q> =
1264 Box::new(it.filter_map(move |tri| triple_row(ctx, &t, tri).map(|r| (tri.0, r))));
1265 Some(keyed.peekable())
1266 };
1267 let a = side(ta)?;
1268 let b = side(tb)?;
1269 let mut slots = sa;
1270 for s in sb {
1271 if !slots.contains(&s) {
1272 slots.push(s);
1273 }
1274 }
1275 Some((
1276 Box::new(MergeStream {
1277 a,
1278 b,
1279 ra: Vec::new(),
1280 rb: Vec::new(),
1281 i: 0,
1282 j: 0,
1283 }),
1284 slots,
1285 ))
1286}
1287
1288type KeyedRows<'q> = std::iter::Peekable<Box<dyn Iterator<Item = (u32, Row)> + 'q>>;
1289
1290struct MergeStream<'q> {
1293 a: KeyedRows<'q>,
1294 b: KeyedRows<'q>,
1295 ra: Vec<Row>,
1296 rb: Vec<Row>,
1297 i: usize,
1298 j: usize,
1299}
1300
1301impl Iterator for MergeStream<'_> {
1302 type Item = Row;
1303
1304 fn next(&mut self) -> Option<Row> {
1305 loop {
1306 while self.i < self.ra.len() {
1307 if self.j < self.rb.len() {
1308 let j = self.j;
1309 self.j += 1;
1310 if let Some(m) = crate::row::merge_rows(&self.ra[self.i], &self.rb[j]) {
1311 return Some(m);
1312 }
1313 } else {
1314 self.i += 1;
1315 self.j = 0;
1316 }
1317 }
1318 let ka = self.a.peek()?.0;
1319 let kb = self.b.peek()?.0;
1320 match ka.cmp(&kb) {
1321 std::cmp::Ordering::Less => {
1322 self.a.next();
1323 }
1324 std::cmp::Ordering::Greater => {
1325 self.b.next();
1326 }
1327 std::cmp::Ordering::Equal => {
1328 self.ra.clear();
1329 self.rb.clear();
1330 while let Some((_, r)) = self.a.next_if(|(k, _)| *k == ka) {
1331 self.ra.push(r);
1332 }
1333 while let Some((_, r)) = self.b.next_if(|(k, _)| *k == ka) {
1334 self.rb.push(r);
1335 }
1336 self.i = 0;
1337 self.j = 0;
1338 }
1339 }
1340 }
1341 }
1342}
1343
1344fn probe_subject(ctx: &Ctx, t: &SlotTerm, base: &Row) -> Option<Option<u32>> {
1358 let dict = ctx.rete.dictionary();
1359 match t {
1360 SlotTerm::Node(n) => dict.node_as_subject_id(*n).map(Some),
1361 SlotTerm::Pred(_) => None,
1362 SlotTerm::Var(i) => match &base[*i] {
1363 None => Some(None),
1364 Some(Val::Id(v)) if *v >= 0 => dict.node_as_subject_id(*v as u32).map(Some),
1365 Some(_) => None,
1367 },
1368 }
1369}
1370
1371fn probe_object(ctx: &Ctx, t: &SlotTerm, base: &Row) -> Option<Option<u32>> {
1373 let dict = ctx.rete.dictionary();
1374 match t {
1375 SlotTerm::Node(n) => dict.node_as_object_id(*n).map(Some),
1376 SlotTerm::Pred(_) => None,
1377 SlotTerm::Var(i) => match &base[*i] {
1378 None => Some(None),
1379 Some(Val::Id(v)) if *v >= 0 => dict.node_as_object_id(*v as u32).map(Some),
1380 Some(_) => None,
1381 },
1382 }
1383}
1384
1385fn probe_predicate(ctx: &Ctx, t: &SlotTerm, base: &Row) -> Option<Option<u32>> {
1387 let dict = ctx.rete.dictionary();
1388 match t {
1389 SlotTerm::Pred(p) => Some(Some(*p)),
1390 SlotTerm::Node(_) => None,
1391 SlotTerm::Var(i) => match &base[*i] {
1392 None => Some(None),
1393 Some(Val::Id(v)) if *v < 0 => Some(Some((-v - 1) as u32)),
1394 Some(Val::Id(v)) => ctx
1396 .resolver
1397 .term(*v)
1398 .and_then(|t| dict.predicate_id(&t))
1399 .map(Some),
1400 Some(Val::Str(_)) => None,
1401 },
1402 }
1403}
1404
1405fn probe_rows<'q>(
1409 ctx: &'q Ctx<'q>,
1410 index: &'q GraphIndex,
1411 t: (SlotTerm, SlotTerm, SlotTerm),
1412 base: Row,
1413) -> Box<dyn Iterator<Item = Row> + 'q> {
1414 let (Some(sid), Some(pid), Some(oid)) = (
1415 probe_subject(ctx, &t.0, &base),
1416 probe_predicate(ctx, &t.1, &base),
1417 probe_object(ctx, &t.2, &base),
1418 ) else {
1419 return Box::new(std::iter::empty());
1420 };
1421 let dict = ctx.rete.dictionary();
1422 Box::new(
1423 index
1424 .scan_iter((sid, pid, oid))
1425 .filter_map(move |(s_id, p_id, o_id)| {
1426 let s_val = dict.subject_node(s_id) as i64;
1427 let p_val = ctx.resolver.canon_id(pred_tag(p_id));
1428 let o_val = dict.object_node(o_id) as i64;
1429 let mut row = base.clone();
1430 for (term, val) in [(&t.0, s_val), (&t.1, p_val), (&t.2, o_val)] {
1431 if let SlotTerm::Var(i) = term {
1432 match row[*i] {
1433 Some(Val::Id(existing)) if existing != val => return None,
1434 Some(Val::Id(_)) => {}
1435 Some(Val::Str(_)) => return None,
1436 None => row[*i] = Some(Val::Id(val)),
1437 }
1438 }
1439 }
1440 Some(row)
1441 }),
1442 )
1443}
1444
1445pub(crate) struct ProbePlan {
1447 pats: Vec<(SlotTerm, SlotTerm, SlotTerm)>,
1448}
1449
1450impl ProbePlan {
1451 pub(crate) fn new(
1455 ctx: &Ctx,
1456 index: &GraphIndex,
1457 patterns: &[TriplePattern],
1458 seed_mask: &[bool],
1459 ) -> Option<Self> {
1460 let lowered = lower(patterns, ctx)?;
1461 let seed: std::collections::HashSet<usize> = seed_mask
1462 .iter()
1463 .enumerate()
1464 .filter_map(|(i, b)| b.then_some(i))
1465 .collect();
1466 let order = selectivity_order_seeded(ctx, index, &lowered, &seed);
1467 Some(ProbePlan {
1468 pats: order.into_iter().map(|i| lowered[i]).collect(),
1469 })
1470 }
1471}
1472
1473pub(crate) struct ProbeJoin<'q> {
1476 ctx: &'q Ctx<'q>,
1477 index: &'q GraphIndex,
1478 pats: Vec<(SlotTerm, SlotTerm, SlotTerm)>,
1479 stack: Vec<Box<dyn Iterator<Item = Row> + 'q>>,
1480}
1481
1482impl<'q> ProbeJoin<'q> {
1483 pub(crate) fn new(
1486 ctx: &'q Ctx<'q>,
1487 index: &'q GraphIndex,
1488 patterns: &[TriplePattern],
1489 ) -> Option<Self> {
1490 let plan = ProbePlan::new(ctx, index, patterns, &vec![false; ctx.slots.len()])?;
1491 Some(Self::from_plan(ctx, index, &plan, ctx.slots.empty_row()))
1492 }
1493
1494 pub(crate) fn from_plan(
1496 ctx: &'q Ctx<'q>,
1497 index: &'q GraphIndex,
1498 plan: &ProbePlan,
1499 seed: Row,
1500 ) -> Self {
1501 let pats = plan.pats.clone();
1502 let first = probe_rows(ctx, index, pats[0], seed);
1503 ProbeJoin {
1504 ctx,
1505 index,
1506 pats,
1507 stack: vec![first],
1508 }
1509 }
1510}
1511
1512impl Iterator for ProbeJoin<'_> {
1513 type Item = Row;
1514
1515 fn next(&mut self) -> Option<Row> {
1516 loop {
1517 let depth = self.stack.len();
1518 match self.stack.last_mut()?.next() {
1519 Some(row) => {
1520 if depth == self.pats.len() {
1521 return Some(row);
1522 }
1523 let it = probe_rows(self.ctx, self.index, self.pats[depth], row);
1524 self.stack.push(it);
1525 }
1526 None => {
1527 self.stack.pop();
1528 if self.stack.is_empty() {
1529 return None;
1530 }
1531 }
1532 }
1533 }
1534 }
1535}
1536
1537fn hash_join(
1541 left: Vec<Row>,
1542 left_bound: &[usize],
1543 right: Vec<Row>,
1544 right_slots: &[usize],
1545) -> Vec<Row> {
1546 if left.len() == 1 && left_bound.is_empty() {
1548 return right;
1549 }
1550 if left.is_empty() || right.is_empty() {
1551 return Vec::new();
1552 }
1553 let shared: Vec<usize> = right_slots
1554 .iter()
1555 .copied()
1556 .filter(|s| left_bound.contains(s))
1557 .collect();
1558 let fill = |l: &Row, r: &Row| -> Row {
1559 let mut out = l.clone();
1560 for &s in right_slots {
1561 out[s] = r[s].clone();
1562 }
1563 out
1564 };
1565 if shared.is_empty() {
1566 let mut out = Vec::with_capacity(left.len() * right.len());
1570 for l in &left {
1571 for r in &right {
1572 out.push(fill(l, r));
1573 }
1574 }
1575 return out;
1576 }
1577 let key_of = |b: &Row| -> Vec<Val> { shared.iter().map(|&s| b[s].clone().unwrap()).collect() };
1578 let mut out = Vec::new();
1585 if right.len() <= left.len() {
1586 let mut buckets: HashMap<Vec<Val>, Vec<Row>> = HashMap::new();
1587 for r in right {
1588 buckets.entry(key_of(&r)).or_default().push(r);
1589 }
1590 for l in &left {
1591 if let Some(rs) = buckets.get(&key_of(l)) {
1592 for r in rs {
1593 out.push(fill(l, r));
1594 }
1595 }
1596 }
1597 } else {
1598 let mut buckets: HashMap<Vec<Val>, Vec<Row>> = HashMap::new();
1600 for l in left {
1601 buckets.entry(key_of(&l)).or_default().push(l);
1602 }
1603 for r in &right {
1604 if let Some(ls) = buckets.get(&key_of(r)) {
1605 for l in ls {
1606 let mut row = r.clone();
1607 for &s in left_bound {
1608 if let Some(v) = &l[s] {
1609 row[s] = Some(v.clone());
1610 }
1611 }
1612 out.push(row);
1613 }
1614 }
1615 }
1616 }
1617 out
1618}
1619
1620fn const_subject(t: &SlotTerm, d: &crate::Dictionary) -> Option<Option<u32>> {
1625 match t {
1626 SlotTerm::Node(n) => d.node_as_subject_id(*n).map(Some),
1627 SlotTerm::Pred(_) => None, SlotTerm::Var(_) => Some(None),
1629 }
1630}
1631
1632fn const_object(t: &SlotTerm, d: &crate::Dictionary) -> Option<Option<u32>> {
1633 match t {
1634 SlotTerm::Node(n) => d.node_as_object_id(*n).map(Some),
1635 SlotTerm::Pred(_) => None,
1636 SlotTerm::Var(_) => Some(None),
1637 }
1638}
1639
1640fn const_predicate(t: &SlotTerm) -> Option<Option<u32>> {
1641 match t {
1642 SlotTerm::Pred(p) => Some(Some(*p)),
1643 SlotTerm::Node(_) => None, SlotTerm::Var(_) => Some(None),
1645 }
1646}
1647
1648#[cfg(test)]
1649mod tests {
1650 use super::*;
1651 use crate::dictionary::DictionaryBuilder;
1652 use crate::file::write_file;
1653 use crate::index::GraphIndexBuilder;
1654
1655 fn rete_from(triples: &[(&str, &str, &str)]) -> Vec<u8> {
1656 let mut db = DictionaryBuilder::new();
1657 for (s, p, o) in triples {
1658 db.observe(s, p, o);
1659 }
1660 let dict = db.build();
1661 let mut ib = GraphIndexBuilder::new();
1662 for (s, p, o) in triples {
1663 ib.push(dict.encode(s, p, o).unwrap());
1664 }
1665 write_file(&dict, &ib.build(), false, &[], 0)
1666 }
1667
1668 fn pat(s: &str, p: &str, o: &str) -> TriplePattern {
1669 TriplePattern {
1670 s: PatternTerm::parse(s),
1671 p: PatternTerm::parse(p),
1672 o: PatternTerm::parse(o),
1673 }
1674 }
1675
1676 #[test]
1681 fn every_join_path_returns_the_same_solutions() {
1682 let mut owned: Vec<(String, String, String)> = Vec::new();
1683 let mut x: u64 = 7;
1684 let mut next = |m: u64| {
1685 x = x
1686 .wrapping_mul(6364136223846793005)
1687 .wrapping_add(1442695040888963407);
1688 (x >> 33) % m
1689 };
1690 for i in 0..60 {
1691 let s = format!("s{i}");
1692 owned.push((s.clone(), "a".into(), format!("C{}", next(3))));
1693 for p in 0..4 {
1694 for _ in 0..next(3) {
1695 owned.push((s.clone(), format!("p{p}"), format!("s{}", next(60))));
1696 }
1697 }
1698 }
1699 let triples: Vec<(&str, &str, &str)> = owned
1700 .iter()
1701 .map(|(s, p, o)| (s.as_str(), p.as_str(), o.as_str()))
1702 .collect();
1703 let bytes = rete_from(&triples);
1704 let rete = Rete::open(&bytes).unwrap();
1705 let queries: Vec<Vec<TriplePattern>> = vec![
1706 vec![pat("?s", "p0", "?a"), pat("?s", "p1", "?b")],
1707 vec![pat("?s", "a", "C1"), pat("?s", "p0", "?a")],
1708 vec![pat("?s", "p0", "?a"), pat("?a", "p2", "?b")],
1709 vec![pat("s5", "p0", "?a"), pat("?a", "?p", "?o")],
1710 vec![pat("?s", "p0", "?x"), pat("?s", "p1", "?x")],
1711 vec![
1712 pat("?s", "p3", "?o"),
1713 pat("?o", "a", "C0"),
1714 pat("?s", "p1", "?z"),
1715 ],
1716 vec![
1717 pat("?s", "p0", "?a"),
1718 pat("?s", "p1", "?b"),
1719 pat("?b", "p2", "?c"),
1720 ],
1721 ];
1722 let run = |limits: Option<PlanLimits>| -> Vec<Vec<Vec<(String, String)>>> {
1723 TEST_LIMITS.with(|c| c.set(limits));
1724 let out = queries
1725 .iter()
1726 .map(|q| {
1727 let mut slots = crate::row::Slots::new();
1728 collect_pattern_slots(q, &mut slots);
1729 let ctx = Ctx::new(&rete, slots);
1730 let mut rows: Vec<Vec<(String, String)>> =
1731 BgpSolutions::new(&ctx, rete.default_index(), q)
1732 .map(|r| row_to_binding(&ctx, &r).into_iter().collect())
1733 .collect();
1734 rows.sort();
1735 rows
1736 })
1737 .collect();
1738 TEST_LIMITS.with(|c| c.set(None));
1739 out
1740 };
1741 let reference = run(None);
1742 assert!(reference.iter().all(|r| !r.is_empty()), "{reference:?}");
1743 for (name, l) in [
1744 (
1745 "probe everything",
1746 PlanLimits {
1747 probe_floor: 0.0,
1748 stream_seed: f64::MAX,
1749 hash_build: f64::MAX,
1750 },
1751 ),
1752 (
1753 "stream + hash",
1754 PlanLimits {
1755 probe_floor: f64::MAX,
1756 stream_seed: 0.0,
1757 hash_build: f64::MAX,
1758 },
1759 ),
1760 (
1761 "stream + probe",
1762 PlanLimits {
1763 probe_floor: f64::MAX,
1764 stream_seed: 0.0,
1765 hash_build: 0.0,
1766 },
1767 ),
1768 (
1769 "stream, all paths",
1770 PlanLimits {
1771 probe_floor: 0.0,
1772 stream_seed: 0.0,
1773 hash_build: 30.0,
1774 },
1775 ),
1776 ] {
1777 assert_eq!(run(Some(l)), reference, "{name}");
1778 }
1779 }
1780
1781 #[test]
1782 fn single_pattern_binds_variable() {
1783 let bytes = rete_from(&[("Alice", "knows", "Bob"), ("Bob", "knows", "Carol")]);
1784 let rete = Rete::open(&bytes).unwrap();
1785 let sols = eval_bgp(&rete, &[pat("Alice", "knows", "?y")]);
1786 assert_eq!(sols.len(), 1);
1787 assert_eq!(sols[0]["y"], "Bob");
1788 }
1789
1790 #[test]
1791 fn two_hop_join_on_shared_variable() {
1792 let bytes = rete_from(&[
1794 ("Alice", "knows", "Bob"),
1795 ("Bob", "knows", "Carol"),
1796 ("Carol", "knows", "Dave"),
1797 ("Alice", "knows", "Eve"), ]);
1799 let rete = Rete::open(&bytes).unwrap();
1800 let sols = eval_bgp(&rete, &[pat("?x", "knows", "?y"), pat("?y", "knows", "?z")]);
1801 let mut got: Vec<_> = sols
1803 .iter()
1804 .map(|b| (b["x"].clone(), b["y"].clone(), b["z"].clone()))
1805 .collect();
1806 got.sort();
1807 assert_eq!(
1808 got,
1809 vec![
1810 ("Alice".into(), "Bob".into(), "Carol".into()),
1811 ("Bob".into(), "Carol".into(), "Dave".into()),
1812 ]
1813 );
1814 }
1815
1816 #[test]
1817 fn repeated_variable_within_pattern() {
1818 let bytes = rete_from(&[("Alice", "knows", "Bob"), ("Bob", "knows", "Bob")]);
1820 let rete = Rete::open(&bytes).unwrap();
1821 let sols = eval_bgp(&rete, &[pat("?x", "knows", "?x")]);
1822 assert_eq!(sols.len(), 1);
1823 assert_eq!(sols[0]["x"], "Bob");
1824 }
1825
1826 #[test]
1827 fn no_solutions_yields_empty() {
1828 let bytes = rete_from(&[("Alice", "knows", "Bob")]);
1829 let rete = Rete::open(&bytes).unwrap();
1830 assert!(eval_bgp(&rete, &[pat("Alice", "likes", "?y")]).is_empty());
1831 }
1832
1833 #[test]
1840 fn snowflake_type_and_adjacency_matches_reference() {
1841 const TYPE: &str = "<http://www.w3.org/1999/02/22-rdf-syntax-ns#type>";
1842 let mut triples: Vec<(String, String, String)> = Vec::new();
1843 for i in 0..60 {
1845 let s = format!("S{i}");
1846 triples.push((s.clone(), TYPE.into(), "Student".into()));
1847 triples.push((s.clone(), "takesCourse".into(), format!("c{}", i % 5)));
1848 triples.push((s, "takesCourse".into(), format!("c{}", (i + 1) % 5)));
1849 }
1850 for j in 0..5 {
1852 triples.push((format!("c{j}"), TYPE.into(), "Course".into()));
1853 }
1854 triples.push(("Prof".into(), "teacherOf".into(), "c0".into()));
1855 triples.push(("Prof".into(), "teacherOf".into(), "c2".into()));
1856
1857 let refs: Vec<(&str, &str, &str)> = triples
1858 .iter()
1859 .map(|(s, p, o)| (s.as_str(), p.as_str(), o.as_str()))
1860 .collect();
1861 let bytes = rete_from(&refs);
1862 let rete = Rete::open(&bytes).unwrap();
1863
1864 let got: std::collections::BTreeSet<(String, String)> = eval_bgp(
1865 &rete,
1866 &[
1867 pat("?x", TYPE, "Student"),
1868 pat("?y", TYPE, "Course"),
1869 pat("?x", "takesCourse", "?y"),
1870 pat("Prof", "teacherOf", "?y"),
1871 ],
1872 )
1873 .into_iter()
1874 .map(|b| (b["x"].clone(), b["y"].clone()))
1875 .collect();
1876
1877 let taught = ["c0", "c2"];
1879 let mut want = std::collections::BTreeSet::new();
1880 for i in 0..60 {
1881 for c in [i % 5, (i + 1) % 5] {
1882 let course = format!("c{c}");
1883 if taught.contains(&course.as_str()) {
1884 want.insert((format!("S{i}"), course));
1885 }
1886 }
1887 }
1888 assert_eq!(got, want, "snowflake join must match the brute-force set");
1889 assert!(!want.is_empty(), "sanity: the reference set is non-empty");
1890 }
1891
1892 #[test]
1897 fn merge_join_subject_star_cross_product() {
1898 let bytes = rete_from(&[
1899 ("x", "a", "a1"),
1900 ("x", "a", "a2"),
1901 ("x", "b", "b1"),
1902 ("x", "b", "b2"),
1903 ("y", "a", "a3"),
1904 ("y", "b", "b3"),
1905 ("z", "a", "a4"), ]);
1907 let rete = Rete::open(&bytes).unwrap();
1908 let mut got: Vec<(String, String, String)> =
1909 eval_bgp(&rete, &[pat("?x", "a", "?av"), pat("?x", "b", "?bv")])
1910 .iter()
1911 .map(|m| (m["x"].clone(), m["av"].clone(), m["bv"].clone()))
1912 .collect();
1913 got.sort();
1914 let mut want: Vec<(String, String, String)> = [
1915 ("x", "a1", "b1"),
1916 ("x", "a1", "b2"),
1917 ("x", "a2", "b1"),
1918 ("x", "a2", "b2"),
1919 ("y", "a3", "b3"),
1920 ]
1921 .into_iter()
1922 .map(|(a, b, c)| (a.to_string(), b.to_string(), c.to_string()))
1923 .collect();
1924 want.sort();
1925 assert_eq!(got, want, "same-role merge must equal the brute-force join");
1926 }
1927}