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 if merge_seed_lopsided(ctx, index, ta, tb) {
297 return None;
298 }
299 let lower = |t: &(SlotTerm, SlotTerm, SlotTerm)| -> Option<Pattern> {
300 Some((
301 const_subject(&t.0, dict)?,
302 const_predicate(&t.1)?,
303 const_object(&t.2, dict)?,
304 ))
305 };
306 let collect = |t: &(SlotTerm, SlotTerm, SlotTerm), col: usize| -> Option<Vec<(u32, Row)>> {
309 let pat = lower(t)?;
310 let mut out = Vec::new();
311 for tri in index.scan_iter_sorted_on(pat, col)? {
312 if let Some(r) = triple_row(ctx, t, tri) {
313 out.push(([tri.0, tri.1, tri.2][col], r));
314 }
315 }
316 Some(out)
317 };
318 let rows_a = collect(ta, ca)?;
319 let rows_b = collect(tb, cb)?;
320
321 let mut out: Vec<Row> = Vec::new();
322 let (mut i, mut j) = (0usize, 0usize);
323 while i < rows_a.len() && j < rows_b.len() {
324 match rows_a[i].0.cmp(&rows_b[j].0) {
325 std::cmp::Ordering::Less => i += 1,
326 std::cmp::Ordering::Greater => j += 1,
327 std::cmp::Ordering::Equal => {
328 let key = rows_a[i].0;
329 let (i0, j0) = (i, j);
330 while i < rows_a.len() && rows_a[i].0 == key {
331 i += 1;
332 }
333 while j < rows_b.len() && rows_b[j].0 == key {
334 j += 1;
335 }
336 for (_, ra) in &rows_a[i0..i] {
337 for (_, rb) in &rows_b[j0..j] {
338 let mut row = ra.clone();
341 let mut ok = true;
342 for (slot, val) in rb.iter().enumerate() {
343 if let Some(val) = val {
344 match &row[slot] {
345 Some(existing) if existing != val => {
346 ok = false;
347 break;
348 }
349 _ => row[slot] = Some(val.clone()),
350 }
351 }
352 }
353 if ok {
354 out.push(row);
355 }
356 }
357 }
358 }
359 }
360 }
361 let mut slots = pattern_slots(ta);
362 for s in pattern_slots(tb) {
363 if !slots.contains(&s) {
364 slots.push(s);
365 }
366 }
367 Some((out, slots))
368}
369
370const BGP_PROBE_THRESHOLD: usize = 512;
378
379const PROBE_SCAN_FLOOR: f64 = 65_536.0;
384
385const PROBE_RATIO: f64 = 16.0;
390
391fn probe_beats_scan(rows: usize, scan_est: f64) -> bool {
394 scan_est >= limits().probe_floor && (rows as f64) * PROBE_RATIO <= scan_est
395}
396
397pub(crate) fn probe_row_budget(est: f64) -> Option<usize> {
402 (est >= limits().probe_floor).then(|| (est / PROBE_RATIO) as usize)
403}
404
405pub(crate) fn bgp_min_estimate(
409 ctx: &Ctx,
410 index: &GraphIndex,
411 patterns: &[TriplePattern],
412) -> Option<f64> {
413 let lowered = lower(patterns, ctx)?;
414 tile_estimates(ctx, index, &lowered, &std::collections::HashSet::new())
415 .into_iter()
416 .reduce(f64::min)
417}
418
419#[cfg(test)]
422pub(crate) fn with_probe_floor<T>(floor: f64, f: impl FnOnce() -> T) -> T {
423 let saved = TEST_LIMITS.with(|c| c.get());
424 TEST_LIMITS.with(|c| {
425 c.set(Some(PlanLimits {
426 probe_floor: floor,
427 ..limits()
428 }))
429 });
430 let out = f();
431 TEST_LIMITS.with(|c| c.set(saved));
432 out
433}
434
435#[derive(Clone, Copy)]
438struct PlanLimits {
439 probe_floor: f64,
440 stream_seed: f64,
441 hash_build: f64,
442}
443
444#[cfg(test)]
445thread_local! {
446 static TEST_LIMITS: std::cell::Cell<Option<PlanLimits>> = const { std::cell::Cell::new(None) };
447}
448
449fn limits() -> PlanLimits {
450 #[cfg(test)]
451 if let Some(l) = TEST_LIMITS.with(|c| c.get()) {
452 return l;
453 }
454 PlanLimits {
455 probe_floor: PROBE_SCAN_FLOOR,
456 stream_seed: STREAM_SEED_ROWS,
457 hash_build: HASH_BUILD_ROWS,
458 }
459}
460
461const STREAM_SEED_ROWS: f64 = (1u64 << 21) as f64;
469
470const HASH_BUILD_ROWS: f64 = (1u64 << 20) as f64;
473
474fn tile_estimates(
480 ctx: &Ctx,
481 index: &GraphIndex,
482 lowered: &[(SlotTerm, SlotTerm, SlotTerm)],
483 seed: &std::collections::HashSet<usize>,
484) -> Vec<f64> {
485 const SEL_SUBJECT: f64 = 0.001;
486 const SEL_OBJECT: f64 = 0.02;
487 const SEL_PREDICATE: f64 = 0.05;
488 let dict = ctx.rete.dictionary();
489 let total = ctx.rete.header().quad_count;
490 let seeded = |x: &SlotTerm| matches!(x, SlotTerm::Var(v) if seed.contains(v));
491 let needs_stats = lowered
499 .iter()
500 .any(|t| matches!(t.1, SlotTerm::Pred(_)) && (seeded(&t.0) || seeded(&t.2)));
501 let stats: std::collections::HashMap<u32, &crate::meta::PredStat> = if needs_stats {
502 ctx.rete
503 .planner_stats(true)
504 .iter()
505 .map(|s| (s.predicate, s))
506 .collect()
507 } else {
508 std::collections::HashMap::new()
509 };
510 lowered
511 .iter()
512 .map(|t| {
513 let (Some(s), Some(p), Some(o)) = (
514 const_subject(&t.0, dict),
515 const_predicate(&t.1),
516 const_object(&t.2, dict),
517 ) else {
518 return 0.0;
519 };
520 let mut est = index.estimate_matches((s, p, o), total);
521 if est == 0.0 {
522 return 0.0;
523 }
524 let st = match t.1 {
525 SlotTerm::Pred(pid) => stats.get(&pid).copied(),
526 _ => None,
527 };
528 if seeded(&t.0) {
529 est *= match st {
530 Some(st) if st.distinct_subjects > 0 => 1.0 / st.distinct_subjects as f64,
531 _ => SEL_SUBJECT,
532 };
533 }
534 if seeded(&t.1) {
535 est *= SEL_PREDICATE;
536 }
537 if seeded(&t.2) {
538 est *= match st {
539 Some(st) if st.distinct_objects > 0 => 1.0 / st.distinct_objects as f64,
540 _ => SEL_OBJECT,
541 };
542 }
543 est.max(1.0)
544 })
545 .collect()
546}
547
548const REMOTE_PROBE_MIN: usize = 8;
557const REMOTE_PROBE_MAX: usize = 1024;
558
559fn remote_probe_max(index: &GraphIndex) -> usize {
566 REMOTE_PROBE_MAX * index.read_concurrency().clamp(1, 16)
567}
568
569pub(crate) fn prefetch_plan_probes(ctx: &Ctx, index: &GraphIndex, plan: &ProbePlan, rows: &[Row]) {
577 if !index.is_remote() {
578 return;
579 }
580 let Some(t) = plan.pats.first() else {
581 return;
582 };
583 let mut pats: Vec<Pattern> = Vec::new();
584 for base in rows {
585 if let (Some(s), Some(p), Some(o)) = (
586 probe_subject(ctx, &t.0, base),
587 probe_predicate(ctx, &t.1, base),
588 probe_object(ctx, &t.2, base),
589 ) {
590 if s.is_some() || o.is_some() {
591 pats.push((s, p, o));
592 }
593 }
594 }
595 index.prefetch_probe_tiles(&pats);
596}
597
598pub(crate) fn prefetch_subject_probes(
604 ctx: &Ctx,
605 index: &GraphIndex,
606 patterns: &[TriplePattern],
607 sv: &str,
608 subject_ids: &[u32],
609) {
610 if !index.is_remote() {
611 return;
612 }
613 let Some(lowered) = lower(patterns, ctx) else {
614 return;
615 };
616 let Some(slot) = ctx.slots.slot(sv) else {
617 return;
618 };
619 let dict = ctx.rete.dictionary();
620 let mut pats: Vec<Pattern> = Vec::new();
621 for &sid in subject_ids {
622 let mut base = ctx.slots.empty_row();
623 base[slot] = Some(Val::Id(dict.subject_node(sid) as i64));
624 for t in &lowered {
625 if !matches!(t.0, SlotTerm::Var(i) if i == slot) {
626 continue;
627 }
628 if let (Some(s), Some(p), Some(o)) = (
629 probe_subject(ctx, &t.0, &base),
630 probe_predicate(ctx, &t.1, &base),
631 probe_object(ctx, &t.2, &base),
632 ) {
633 pats.push((s, p, o));
634 }
635 }
636 }
637 index.prefetch_probe_tiles(&pats);
638}
639
640fn pattern_is_selective(t: &(SlotTerm, SlotTerm, SlotTerm)) -> bool {
643 matches!(t.0, SlotTerm::Node(_)) || matches!(t.2, SlotTerm::Node(_))
644}
645
646fn pattern_slots(t: &(SlotTerm, SlotTerm, SlotTerm)) -> Vec<usize> {
648 let mut slots: Vec<usize> = Vec::new();
649 for term in [&t.0, &t.1, &t.2] {
650 if let SlotTerm::Var(i) = term {
651 if !slots.contains(i) {
652 slots.push(*i);
653 }
654 }
655 }
656 slots
657}
658
659fn pred_val(ctx: &Ctx, slot: &SlotTerm, p_id: u32) -> i64 {
666 match slot {
667 SlotTerm::Var(_) => ctx.resolver.canon_id(pred_tag(p_id)),
668 _ => pred_tag(p_id),
669 }
670}
671
672fn triple_row(
676 ctx: &Ctx,
677 t: &(SlotTerm, SlotTerm, SlotTerm),
678 (s_id, p_id, o_id): (u32, u32, u32),
679) -> Option<Row> {
680 let dict = ctx.rete.dictionary();
681 let s_val = dict.subject_node(s_id) as i64;
682 let p_val = pred_val(ctx, &t.1, p_id);
683 let o_val = dict.object_node(o_id) as i64;
684 let mut row = ctx.slots.empty_row();
685 for (term, val) in [(&t.0, s_val), (&t.1, p_val), (&t.2, o_val)] {
686 if let SlotTerm::Var(i) = term {
687 match row[*i] {
688 Some(Val::Id(existing)) if existing != val => return None,
689 Some(_) => {}
690 None => row[*i] = Some(Val::Id(val)),
691 }
692 }
693 }
694 Some(row)
695}
696
697fn scan_rows<'q>(
702 ctx: &'q Ctx<'q>,
703 index: &'q GraphIndex,
704 t: (SlotTerm, SlotTerm, SlotTerm),
705) -> Option<impl Iterator<Item = Row> + 'q> {
706 let dict = ctx.rete.dictionary();
707 let (sid, pid, oid) = (
708 const_subject(&t.0, dict)?,
709 const_predicate(&t.1)?,
710 const_object(&t.2, dict)?,
711 );
712 Some(
713 index
714 .scan_iter((sid, pid, oid))
715 .filter_map(move |triple| triple_row(ctx, &t, triple)),
716 )
717}
718
719fn pattern_rows(
722 ctx: &Ctx,
723 index: &GraphIndex,
724 t: &(SlotTerm, SlotTerm, SlotTerm),
725) -> Option<(Vec<Row>, Vec<usize>)> {
726 let slots = pattern_slots(t);
727 let mut rel: Vec<Row> = Vec::new();
728 let dict = ctx.rete.dictionary();
729 let (sid, pid, oid) = (
730 const_subject(&t.0, dict)?,
731 const_predicate(&t.1)?,
732 const_object(&t.2, dict)?,
733 );
734 for triple in index.scan_iter((sid, pid, oid)) {
737 if let Some(row) = triple_row(ctx, t, triple) {
738 rel.push(row);
739 }
740 }
741 Some((rel, slots))
742}
743
744fn pattern_estimates(
756 ctx: &Ctx,
757 lowered: &[(SlotTerm, SlotTerm, SlotTerm)],
758 seed: &std::collections::HashSet<usize>,
759) -> Option<Vec<f64>> {
760 let pyr = ctx.rete.pyramid_if_loaded()?;
763 let mut pred: std::collections::HashMap<u32, u64> = std::collections::HashMap::new();
764 for e in &pyr.summary {
765 *pred.entry(e.predicate).or_insert(0) += e.count as u64;
766 }
767 let stats: std::collections::HashMap<u32, &crate::meta::PredStat> = ctx
774 .rete
775 .planner_stats(true)
776 .iter()
777 .map(|s| (s.predicate, s))
778 .collect();
779 let total = ctx.rete.header().quad_count.max(1) as f64;
780 let num_preds = pred.len().max(1) as f64;
781 const SEL_SUBJECT: f64 = 0.001;
783 const SEL_OBJECT: f64 = 0.02;
784 let node_bound = |t: &SlotTerm| match t {
785 SlotTerm::Node(_) => true,
786 SlotTerm::Var(v) => seed.contains(v),
787 SlotTerm::Pred(_) => false,
788 };
789 Some(
790 lowered
791 .iter()
792 .map(|t| {
793 let (base, st) = match t.1 {
798 SlotTerm::Pred(p) => {
799 (*pred.get(&p).unwrap_or(&0) as f64, stats.get(&p).copied())
800 }
801 SlotTerm::Var(v) if seed.contains(&v) => (total / num_preds, None),
802 _ => (total, None),
803 };
804 let mut est = base.max(1.0);
805 if node_bound(&t.0) {
806 est *= match st {
807 Some(s) if s.distinct_subjects > 0 => 1.0 / s.distinct_subjects as f64,
808 _ => SEL_SUBJECT,
809 };
810 }
811 if node_bound(&t.2) {
812 est *= match st {
813 Some(s) if s.distinct_objects > 0 => 1.0 / s.distinct_objects as f64,
814 _ => SEL_OBJECT,
815 };
816 }
817 est.max(1.0)
818 })
819 .collect(),
820 )
821}
822
823pub(crate) fn bgp_min_scan_bytes(
830 ctx: &Ctx,
831 index: &GraphIndex,
832 patterns: &[TriplePattern],
833) -> Option<u64> {
834 let lowered = lower(patterns, ctx)?;
835 lowered
836 .iter()
837 .map(|t| pattern_scan_bytes(ctx, index, t))
838 .min()
839}
840
841fn pattern_scan_bytes(ctx: &Ctx, index: &GraphIndex, t: &(SlotTerm, SlotTerm, SlotTerm)) -> u64 {
858 let dict = ctx.rete.dictionary();
859 match (
860 const_subject(&t.0, dict),
861 const_predicate(&t.1),
862 const_object(&t.2, dict),
863 ) {
864 (Some(s), Some(p), Some(o)) => routed_scan_bytes(index, (s, p, o)),
865 _ => 0,
866 }
867}
868
869pub(crate) fn routed_scan_bytes(index: &GraphIndex, pattern: Pattern) -> u64 {
871 let sections = index.tile_sections();
872 let mut best = u64::MAX;
873 for perm in index.perms().iter() {
877 let tiles = sections[perm.section_index()];
878 best = best.min(match perm.order_pattern(pattern)[0] {
879 Some(id) => tiles
880 .iter()
881 .filter(|tile| {
882 let (lo, hi) = tile.leading_range();
883 lo <= id && id <= hi
884 })
885 .map(Tile::encoded_len)
886 .sum(),
887 None => tiles.iter().map(Tile::encoded_len).sum(),
888 });
889 }
890 best
891}
892
893pub(crate) const FAT_SCAN_ROWS: f64 = (1u64 << 20) as f64;
906
907fn merge_seed_lopsided(
912 ctx: &Ctx,
913 index: &GraphIndex,
914 ta: &(SlotTerm, SlotTerm, SlotTerm),
915 tb: &(SlotTerm, SlotTerm, SlotTerm),
916) -> bool {
917 let est = tile_estimates(ctx, index, &[*ta, *tb], &std::collections::HashSet::new());
918 let (lo, hi) = (est[0].min(est[1]), est[0].max(est[1]));
919 hi >= FAT_SCAN_ROWS && hi / 4.0 >= lo
920}
921
922fn selectivity_order(
929 ctx: &Ctx,
930 index: &GraphIndex,
931 lowered: &[(SlotTerm, SlotTerm, SlotTerm)],
932) -> Vec<usize> {
933 selectivity_order_seeded(ctx, index, lowered, &std::collections::HashSet::new())
934}
935
936fn selectivity_order_seeded(
940 ctx: &Ctx,
941 index: &GraphIndex,
942 lowered: &[(SlotTerm, SlotTerm, SlotTerm)],
943 seed: &std::collections::HashSet<usize>,
944) -> Vec<usize> {
945 let (estimates, per_class) = match pattern_estimates(ctx, lowered, seed) {
953 Some(e) => (Some(e), false),
954 None => (Some(tile_estimates(ctx, index, lowered, seed)), true),
955 };
956 let consts = |t: &(SlotTerm, SlotTerm, SlotTerm)| {
957 [&t.0, &t.1, &t.2]
958 .into_iter()
959 .filter(|x| match x {
960 SlotTerm::Var(v) => seed.contains(v),
961 _ => true,
962 })
963 .count()
964 };
965 let vars = |t: &(SlotTerm, SlotTerm, SlotTerm)| -> Vec<usize> {
966 [&t.0, &t.1, &t.2]
967 .into_iter()
968 .filter_map(|x| match x {
969 SlotTerm::Var(v) => Some(*v),
970 _ => None,
971 })
972 .collect()
973 };
974 let score = |i: usize| -> f64 {
978 match &estimates {
979 Some(e) => -e[i],
980 None => consts(&lowered[i]) as f64,
981 }
982 };
983 let type_pid = ctx.rete.dictionary().predicate_id(crate::file::RDF_TYPE);
991 let is_class_enum = |i: usize, bound: &std::collections::HashSet<usize>| -> bool {
992 let t = &lowered[i];
993 !per_class
994 && type_pid.is_some_and(|tp| matches!(t.1, SlotTerm::Pred(p) if p == tp))
995 && matches!(t.2, SlotTerm::Node(_))
996 && matches!(t.0, SlotTerm::Var(v) if !bound.contains(&v))
997 };
998 let n = lowered.len();
999 let mut remaining: Vec<usize> = (0..n).collect();
1000 let mut order: Vec<usize> = Vec::with_capacity(n);
1001 let mut bound: std::collections::HashSet<usize> = seed.clone();
1002 while !remaining.is_empty() {
1003 let best = *remaining
1006 .iter()
1007 .max_by(|&&a, &&b| {
1008 let connected = |i: usize| vars(&lowered[i]).iter().any(|v| bound.contains(v));
1009 connected(a)
1010 .cmp(&connected(b))
1011 .then_with(|| is_class_enum(b, &bound).cmp(&is_class_enum(a, &bound)))
1012 .then_with(|| {
1013 score(a)
1014 .partial_cmp(&score(b))
1015 .unwrap_or(std::cmp::Ordering::Equal)
1016 })
1017 .then_with(|| b.cmp(&a))
1018 })
1019 .unwrap();
1020 for v in vars(&lowered[best]) {
1021 bound.insert(v);
1022 }
1023 order.push(best);
1024 remaining.retain(|&i| i != best);
1025 }
1026 order
1027}
1028
1029pub(crate) fn bgp_exists(ctx: &Ctx, index: &GraphIndex, patterns: &[TriplePattern]) -> bool {
1035 let dict = ctx.rete.dictionary();
1036 let Some(lowered) = lower(patterns, ctx) else {
1037 return false;
1038 };
1039 if let [t] = lowered.as_slice() {
1040 let names: Vec<usize> = [&t.0, &t.1, &t.2]
1044 .into_iter()
1045 .filter_map(|x| match x {
1046 SlotTerm::Var(v) => Some(*v),
1047 _ => None,
1048 })
1049 .collect();
1050 let distinct = names
1051 .iter()
1052 .enumerate()
1053 .all(|(i, v)| !names[i + 1..].contains(v));
1054 if distinct {
1055 return match (
1056 const_subject(&t.0, dict),
1057 const_predicate(&t.1),
1058 const_object(&t.2, dict),
1059 ) {
1060 (Some(s), Some(p), Some(o)) => index.scan_iter((s, p, o)).next().is_some(),
1061 _ => false,
1062 };
1063 }
1064 }
1065 !eval_bgp_rows(ctx, index, patterns).is_empty()
1066}
1067
1068pub(crate) struct BgpSolutions<'q> {
1079 scan: Option<Box<dyn Iterator<Item = Row> + 'q>>,
1080 prefix: Vec<Row>,
1081 buckets: HashMap<Vec<Val>, Vec<usize>>,
1083 shared: Vec<usize>,
1084 cartesian: bool,
1085 seed_only: bool,
1088 cur_scan: Option<Row>,
1089 matches: Vec<usize>,
1091 mi: usize,
1092}
1093
1094impl<'q> BgpSolutions<'q> {
1095 fn passthrough(rows: Box<dyn Iterator<Item = Row> + 'q>) -> Self {
1097 BgpSolutions {
1098 scan: Some(rows),
1099 prefix: Vec::new(),
1100 buckets: HashMap::new(),
1101 shared: Vec::new(),
1102 cartesian: false,
1103 seed_only: true,
1104 cur_scan: None,
1105 matches: Vec::new(),
1106 mi: 0,
1107 }
1108 }
1109
1110 fn streaming(
1119 ctx: &'q Ctx<'q>,
1120 index: &'q GraphIndex,
1121 lowered: &[(SlotTerm, SlotTerm, SlotTerm)],
1122 order: &[usize],
1123 est: &[f64],
1124 ) -> Self {
1125 let (mut stream, mut bound, skip): (Box<dyn Iterator<Item = Row> + 'q>, Vec<usize>, usize) =
1126 match (est[order[1]] > limits().stream_seed)
1127 .then(|| merge_stream(ctx, index, lowered[order[0]], lowered[order[1]]))
1128 .flatten()
1129 {
1130 Some((m, slots)) => (m, slots, 2),
1131 None => {
1132 let t = lowered[order[0]];
1133 let Some(scan) = scan_rows(ctx, index, t) else {
1134 return Self::empty();
1135 };
1136 (Box::new(scan), pattern_slots(&t), 1)
1137 }
1138 };
1139 for &i in &order[skip..] {
1140 let t = lowered[i];
1141 let slots = pattern_slots(&t);
1142 let shared: Vec<usize> = slots
1143 .iter()
1144 .copied()
1145 .filter(|s| bound.contains(s))
1146 .collect();
1147 if !shared.is_empty() && est[i] > limits().hash_build {
1148 stream = Box::new(stream.flat_map(move |base| probe_rows(ctx, index, t, base)));
1149 } else {
1150 let Some((rel, _)) = pattern_rows(ctx, index, &t) else {
1153 return Self::empty();
1154 };
1155 let mut table: HashMap<Vec<Val>, Vec<Row>> = HashMap::new();
1156 for r in rel {
1157 let key: Vec<Val> = shared.iter().map(|&s| r[s].clone().unwrap()).collect();
1158 table.entry(key).or_default().push(r);
1159 }
1160 let fill = slots.clone();
1161 stream = Box::new(stream.flat_map(move |base| {
1162 let key: Vec<Val> = shared.iter().map(|&s| base[s].clone().unwrap()).collect();
1163 let out: Vec<Row> = table
1164 .get(&key)
1165 .map(|rs| {
1166 rs.iter()
1167 .map(|r| {
1168 let mut row = base.clone();
1169 for &s in &fill {
1170 row[s] = r[s].clone();
1171 }
1172 row
1173 })
1174 .collect()
1175 })
1176 .unwrap_or_default();
1177 out.into_iter()
1178 }));
1179 }
1180 for s in slots {
1181 if !bound.contains(&s) {
1182 bound.push(s);
1183 }
1184 }
1185 }
1186 Self::passthrough(stream)
1187 }
1188
1189 fn empty() -> Self {
1191 BgpSolutions {
1192 scan: None,
1193 prefix: Vec::new(),
1194 buckets: HashMap::new(),
1195 shared: Vec::new(),
1196 cartesian: false,
1197 seed_only: false,
1198 cur_scan: None,
1199 matches: Vec::new(),
1200 mi: 0,
1201 }
1202 }
1203
1204 pub(crate) fn new(ctx: &'q Ctx<'q>, index: &'q GraphIndex, patterns: &[TriplePattern]) -> Self {
1205 if patterns.is_empty() {
1207 return BgpSolutions {
1208 scan: Some(Box::new(std::iter::once(ctx.slots.empty_row()))),
1209 prefix: vec![ctx.slots.empty_row()],
1210 buckets: HashMap::new(),
1211 shared: Vec::new(),
1212 cartesian: true,
1213 seed_only: true,
1214 cur_scan: None,
1215 matches: Vec::new(),
1216 mi: 0,
1217 };
1218 }
1219 let Some(lowered) = lower(patterns, ctx) else {
1221 return Self::empty();
1222 };
1223 let order = selectivity_order(ctx, index, &lowered);
1226 let scan_est = tile_estimates(ctx, index, &lowered, &std::collections::HashSet::new());
1227 if order.len() >= 2 && scan_est[order[0]] > limits().stream_seed {
1230 return Self::streaming(ctx, index, &lowered, &order, &scan_est);
1231 }
1232 let (&last_i, prefix_is) = order.split_last().unwrap();
1233 let prefix_pats: Vec<TriplePattern> =
1234 prefix_is.iter().map(|&i| patterns[i].clone()).collect();
1235 let prefix = eval_bgp_rows(ctx, index, &prefix_pats);
1236 if prefix.is_empty() {
1237 return Self::empty();
1238 }
1239
1240 let shared: Vec<usize> = pattern_slots(&lowered[last_i])
1244 .into_iter()
1245 .filter(|&s| prefix[0][s].is_some())
1246 .collect();
1247
1248 if !shared.is_empty() && probe_beats_scan(prefix.len(), scan_est[last_i]) {
1252 let t = lowered[last_i];
1253 if index.is_remote() {
1254 prefetch_rows_for(ctx, index, t, &prefix);
1255 }
1256 let stream = prefix
1257 .into_iter()
1258 .flat_map(move |base| probe_rows(ctx, index, t, base));
1259 return Self::passthrough(Box::new(stream));
1260 }
1261
1262 let Some(scan) = scan_rows(ctx, index, lowered[last_i]) else {
1264 return Self::empty();
1265 };
1266 let cartesian = shared.is_empty();
1267 let seed_only = prefix.len() == 1 && prefix[0].iter().all(Option::is_none);
1270 let mut buckets: HashMap<Vec<Val>, Vec<usize>> = HashMap::new();
1271 if !cartesian {
1272 for (i, r) in prefix.iter().enumerate() {
1273 let key: Vec<Val> = shared.iter().map(|&s| r[s].clone().unwrap()).collect();
1274 buckets.entry(key).or_default().push(i);
1275 }
1276 }
1277 BgpSolutions {
1278 scan: Some(Box::new(scan)),
1279 prefix,
1280 buckets,
1281 shared,
1282 cartesian,
1283 seed_only,
1284 cur_scan: None,
1285 matches: Vec::new(),
1286 mi: 0,
1287 }
1288 }
1289}
1290
1291impl Iterator for BgpSolutions<'_> {
1292 type Item = Row;
1293
1294 fn next(&mut self) -> Option<Row> {
1295 if self.seed_only {
1297 return self.scan.as_mut()?.next();
1298 }
1299 loop {
1300 if self.mi < self.matches.len() {
1302 let pi = self.matches[self.mi];
1303 self.mi += 1;
1304 let mut merged = self.prefix[pi].clone();
1305 for (slot, v) in self.cur_scan.as_ref().unwrap().iter().enumerate() {
1306 if v.is_some() {
1307 merged[slot] = v.clone();
1308 }
1309 }
1310 return Some(merged);
1311 }
1312 let s = self.scan.as_mut()?.next()?;
1315 self.matches.clear();
1316 if self.cartesian {
1317 self.matches.extend(0..self.prefix.len());
1318 } else {
1319 let key: Vec<Val> = self.shared.iter().map(|&i| s[i].clone().unwrap()).collect();
1320 if let Some(c) = self.buckets.get(&key) {
1321 self.matches.extend_from_slice(c);
1322 }
1323 }
1324 self.mi = 0;
1325 self.cur_scan = Some(s);
1326 }
1327 }
1328}
1329
1330fn prefetch_rows_for(
1333 ctx: &Ctx,
1334 index: &GraphIndex,
1335 t: (SlotTerm, SlotTerm, SlotTerm),
1336 rows: &[Row],
1337) {
1338 let pats: Vec<Pattern> = rows
1339 .iter()
1340 .filter_map(|base| {
1341 match (
1342 probe_subject(ctx, &t.0, base),
1343 probe_predicate(ctx, &t.1, base),
1344 probe_object(ctx, &t.2, base),
1345 ) {
1346 (Some(s), Some(p), Some(o)) => Some((s, p, o)),
1347 _ => None,
1348 }
1349 })
1350 .collect();
1351 index.prefetch_probe_tiles(&pats);
1352}
1353
1354#[allow(clippy::type_complexity)]
1362fn merge_stream<'q>(
1363 ctx: &'q Ctx<'q>,
1364 index: &'q GraphIndex,
1365 ta: (SlotTerm, SlotTerm, SlotTerm),
1366 tb: (SlotTerm, SlotTerm, SlotTerm),
1367) -> Option<(Box<dyn Iterator<Item = Row> + 'q>, Vec<usize>)> {
1368 let sa = pattern_slots(&ta);
1369 let sb = pattern_slots(&tb);
1370 let shared: Vec<usize> = sa.iter().copied().filter(|s| sb.contains(s)).collect();
1371 let [v] = shared[..] else {
1372 return None;
1373 };
1374 if !matches!(ta.0, SlotTerm::Var(i) if i == v) || !matches!(tb.0, SlotTerm::Var(i) if i == v) {
1375 return None;
1376 }
1377 let dict = ctx.rete.dictionary();
1378 let lower = |t: &(SlotTerm, SlotTerm, SlotTerm)| -> Option<Pattern> {
1379 Some((
1380 const_subject(&t.0, dict)?,
1381 const_predicate(&t.1)?,
1382 const_object(&t.2, dict)?,
1383 ))
1384 };
1385 let side = |t: (SlotTerm, SlotTerm, SlotTerm)| -> Option<KeyedRows<'q>> {
1386 let it = index.scan_iter_sorted_on(lower(&t)?, 0)?;
1387 let keyed: Box<dyn Iterator<Item = (u32, Row)> + 'q> =
1388 Box::new(it.filter_map(move |tri| triple_row(ctx, &t, tri).map(|r| (tri.0, r))));
1389 Some(keyed.peekable())
1390 };
1391 let a = side(ta)?;
1392 let b = side(tb)?;
1393 let mut slots = sa;
1394 for s in sb {
1395 if !slots.contains(&s) {
1396 slots.push(s);
1397 }
1398 }
1399 Some((
1400 Box::new(MergeStream {
1401 a,
1402 b,
1403 ra: Vec::new(),
1404 rb: Vec::new(),
1405 i: 0,
1406 j: 0,
1407 }),
1408 slots,
1409 ))
1410}
1411
1412type KeyedRows<'q> = std::iter::Peekable<Box<dyn Iterator<Item = (u32, Row)> + 'q>>;
1413
1414struct MergeStream<'q> {
1417 a: KeyedRows<'q>,
1418 b: KeyedRows<'q>,
1419 ra: Vec<Row>,
1420 rb: Vec<Row>,
1421 i: usize,
1422 j: usize,
1423}
1424
1425impl Iterator for MergeStream<'_> {
1426 type Item = Row;
1427
1428 fn next(&mut self) -> Option<Row> {
1429 loop {
1430 while self.i < self.ra.len() {
1431 if self.j < self.rb.len() {
1432 let j = self.j;
1433 self.j += 1;
1434 if let Some(m) = crate::row::merge_rows(&self.ra[self.i], &self.rb[j]) {
1435 return Some(m);
1436 }
1437 } else {
1438 self.i += 1;
1439 self.j = 0;
1440 }
1441 }
1442 let ka = self.a.peek()?.0;
1443 let kb = self.b.peek()?.0;
1444 match ka.cmp(&kb) {
1445 std::cmp::Ordering::Less => {
1446 self.a.next();
1447 }
1448 std::cmp::Ordering::Greater => {
1449 self.b.next();
1450 }
1451 std::cmp::Ordering::Equal => {
1452 self.ra.clear();
1453 self.rb.clear();
1454 while let Some((_, r)) = self.a.next_if(|(k, _)| *k == ka) {
1455 self.ra.push(r);
1456 }
1457 while let Some((_, r)) = self.b.next_if(|(k, _)| *k == ka) {
1458 self.rb.push(r);
1459 }
1460 self.i = 0;
1461 self.j = 0;
1462 }
1463 }
1464 }
1465 }
1466}
1467
1468fn probe_subject(ctx: &Ctx, t: &SlotTerm, base: &Row) -> Option<Option<u32>> {
1482 let dict = ctx.rete.dictionary();
1483 match t {
1484 SlotTerm::Node(n) => dict.node_as_subject_id(*n).map(Some),
1485 SlotTerm::Pred(_) => None,
1486 SlotTerm::Var(i) => match &base[*i] {
1487 None => Some(None),
1488 Some(Val::Id(v)) if *v >= 0 => dict.node_as_subject_id(*v as u32).map(Some),
1489 Some(_) => None,
1491 },
1492 }
1493}
1494
1495fn probe_object(ctx: &Ctx, t: &SlotTerm, base: &Row) -> Option<Option<u32>> {
1497 let dict = ctx.rete.dictionary();
1498 match t {
1499 SlotTerm::Node(n) => dict.node_as_object_id(*n).map(Some),
1500 SlotTerm::Pred(_) => None,
1501 SlotTerm::Var(i) => match &base[*i] {
1502 None => Some(None),
1503 Some(Val::Id(v)) if *v >= 0 => dict.node_as_object_id(*v as u32).map(Some),
1504 Some(_) => None,
1505 },
1506 }
1507}
1508
1509fn probe_predicate(ctx: &Ctx, t: &SlotTerm, base: &Row) -> Option<Option<u32>> {
1511 let dict = ctx.rete.dictionary();
1512 match t {
1513 SlotTerm::Pred(p) => Some(Some(*p)),
1514 SlotTerm::Node(_) => None,
1515 SlotTerm::Var(i) => match &base[*i] {
1516 None => Some(None),
1517 Some(Val::Id(v)) if *v < 0 => Some(Some((-v - 1) as u32)),
1518 Some(Val::Id(v)) => ctx
1520 .resolver
1521 .term(*v)
1522 .and_then(|t| dict.predicate_id(&t))
1523 .map(Some),
1524 Some(Val::Str(_)) => None,
1525 },
1526 }
1527}
1528
1529fn probe_rows<'q>(
1533 ctx: &'q Ctx<'q>,
1534 index: &'q GraphIndex,
1535 t: (SlotTerm, SlotTerm, SlotTerm),
1536 base: Row,
1537) -> Box<dyn Iterator<Item = Row> + 'q> {
1538 let (Some(sid), Some(pid), Some(oid)) = (
1539 probe_subject(ctx, &t.0, &base),
1540 probe_predicate(ctx, &t.1, &base),
1541 probe_object(ctx, &t.2, &base),
1542 ) else {
1543 return Box::new(std::iter::empty());
1544 };
1545 let dict = ctx.rete.dictionary();
1546 Box::new(
1547 index
1548 .scan_iter((sid, pid, oid))
1549 .filter_map(move |(s_id, p_id, o_id)| {
1550 let s_val = dict.subject_node(s_id) as i64;
1551 let p_val = pred_val(ctx, &t.1, p_id);
1552 let o_val = dict.object_node(o_id) as i64;
1553 let mut row = base.clone();
1554 for (term, val) in [(&t.0, s_val), (&t.1, p_val), (&t.2, o_val)] {
1555 if let SlotTerm::Var(i) = term {
1556 match row[*i] {
1557 Some(Val::Id(existing)) if existing != val => return None,
1558 Some(Val::Id(_)) => {}
1559 Some(Val::Str(_)) => return None,
1560 None => row[*i] = Some(Val::Id(val)),
1561 }
1562 }
1563 }
1564 Some(row)
1565 }),
1566 )
1567}
1568
1569pub(crate) struct ProbePlan {
1571 pats: Vec<(SlotTerm, SlotTerm, SlotTerm)>,
1572}
1573
1574impl ProbePlan {
1575 pub(crate) fn new(
1579 ctx: &Ctx,
1580 index: &GraphIndex,
1581 patterns: &[TriplePattern],
1582 seed_mask: &[bool],
1583 ) -> Option<Self> {
1584 let lowered = lower(patterns, ctx)?;
1585 let seed: std::collections::HashSet<usize> = seed_mask
1586 .iter()
1587 .enumerate()
1588 .filter_map(|(i, b)| b.then_some(i))
1589 .collect();
1590 let order = selectivity_order_seeded(ctx, index, &lowered, &seed);
1591 Some(ProbePlan {
1592 pats: order.into_iter().map(|i| lowered[i]).collect(),
1593 })
1594 }
1595}
1596
1597pub(crate) struct ProbeJoin<'q> {
1600 ctx: &'q Ctx<'q>,
1601 index: &'q GraphIndex,
1602 pats: Vec<(SlotTerm, SlotTerm, SlotTerm)>,
1603 stack: Vec<Box<dyn Iterator<Item = Row> + 'q>>,
1604}
1605
1606impl<'q> ProbeJoin<'q> {
1607 pub(crate) fn new(
1610 ctx: &'q Ctx<'q>,
1611 index: &'q GraphIndex,
1612 patterns: &[TriplePattern],
1613 ) -> Option<Self> {
1614 let plan = ProbePlan::new(ctx, index, patterns, &vec![false; ctx.slots.len()])?;
1615 Some(Self::from_plan(ctx, index, &plan, ctx.slots.empty_row()))
1616 }
1617
1618 pub(crate) fn from_plan(
1620 ctx: &'q Ctx<'q>,
1621 index: &'q GraphIndex,
1622 plan: &ProbePlan,
1623 seed: Row,
1624 ) -> Self {
1625 let pats = plan.pats.clone();
1626 let first = probe_rows(ctx, index, pats[0], seed);
1627 ProbeJoin {
1628 ctx,
1629 index,
1630 pats,
1631 stack: vec![first],
1632 }
1633 }
1634}
1635
1636impl Iterator for ProbeJoin<'_> {
1637 type Item = Row;
1638
1639 fn next(&mut self) -> Option<Row> {
1640 loop {
1641 let depth = self.stack.len();
1642 match self.stack.last_mut()?.next() {
1643 Some(row) => {
1644 if depth == self.pats.len() {
1645 return Some(row);
1646 }
1647 let it = probe_rows(self.ctx, self.index, self.pats[depth], row);
1648 self.stack.push(it);
1649 }
1650 None => {
1651 self.stack.pop();
1652 if self.stack.is_empty() {
1653 return None;
1654 }
1655 }
1656 }
1657 }
1658 }
1659}
1660
1661fn hash_join(
1665 left: Vec<Row>,
1666 left_bound: &[usize],
1667 right: Vec<Row>,
1668 right_slots: &[usize],
1669) -> Vec<Row> {
1670 if left.len() == 1 && left_bound.is_empty() {
1672 return right;
1673 }
1674 if left.is_empty() || right.is_empty() {
1675 return Vec::new();
1676 }
1677 let shared: Vec<usize> = right_slots
1678 .iter()
1679 .copied()
1680 .filter(|s| left_bound.contains(s))
1681 .collect();
1682 let fill = |l: &Row, r: &Row| -> Row {
1683 let mut out = l.clone();
1684 for &s in right_slots {
1685 out[s] = r[s].clone();
1686 }
1687 out
1688 };
1689 if shared.is_empty() {
1690 let mut out = Vec::with_capacity(left.len() * right.len());
1694 for l in &left {
1695 for r in &right {
1696 out.push(fill(l, r));
1697 }
1698 }
1699 return out;
1700 }
1701 let key_of = |b: &Row| -> Vec<Val> { shared.iter().map(|&s| b[s].clone().unwrap()).collect() };
1702 let mut out = Vec::new();
1709 if right.len() <= left.len() {
1710 let mut buckets: HashMap<Vec<Val>, Vec<Row>> = HashMap::new();
1711 for r in right {
1712 buckets.entry(key_of(&r)).or_default().push(r);
1713 }
1714 for l in &left {
1715 if let Some(rs) = buckets.get(&key_of(l)) {
1716 for r in rs {
1717 out.push(fill(l, r));
1718 }
1719 }
1720 }
1721 } else {
1722 let mut buckets: HashMap<Vec<Val>, Vec<Row>> = HashMap::new();
1724 for l in left {
1725 buckets.entry(key_of(&l)).or_default().push(l);
1726 }
1727 for r in &right {
1728 if let Some(ls) = buckets.get(&key_of(r)) {
1729 for l in ls {
1730 let mut row = r.clone();
1731 for &s in left_bound {
1732 if let Some(v) = &l[s] {
1733 row[s] = Some(v.clone());
1734 }
1735 }
1736 out.push(row);
1737 }
1738 }
1739 }
1740 }
1741 out
1742}
1743
1744fn const_subject(t: &SlotTerm, d: &crate::Dictionary) -> Option<Option<u32>> {
1749 match t {
1750 SlotTerm::Node(n) => d.node_as_subject_id(*n).map(Some),
1751 SlotTerm::Pred(_) => None, SlotTerm::Var(_) => Some(None),
1753 }
1754}
1755
1756fn const_object(t: &SlotTerm, d: &crate::Dictionary) -> Option<Option<u32>> {
1757 match t {
1758 SlotTerm::Node(n) => d.node_as_object_id(*n).map(Some),
1759 SlotTerm::Pred(_) => None,
1760 SlotTerm::Var(_) => Some(None),
1761 }
1762}
1763
1764fn const_predicate(t: &SlotTerm) -> Option<Option<u32>> {
1765 match t {
1766 SlotTerm::Pred(p) => Some(Some(*p)),
1767 SlotTerm::Node(_) => None, SlotTerm::Var(_) => Some(None),
1769 }
1770}
1771
1772#[cfg(test)]
1773mod tests {
1774 use super::*;
1775 use crate::dictionary::DictionaryBuilder;
1776 use crate::file::write_file;
1777 use crate::index::GraphIndexBuilder;
1778
1779 fn rete_from(triples: &[(&str, &str, &str)]) -> Vec<u8> {
1780 let mut db = DictionaryBuilder::new();
1781 for (s, p, o) in triples {
1782 db.observe(s, p, o);
1783 }
1784 let dict = db.build();
1785 let mut ib = GraphIndexBuilder::new();
1786 for (s, p, o) in triples {
1787 ib.push(dict.encode(s, p, o).unwrap());
1788 }
1789 write_file(&dict, &ib.build(), false, &[], 0)
1790 }
1791
1792 fn pat(s: &str, p: &str, o: &str) -> TriplePattern {
1793 TriplePattern {
1794 s: PatternTerm::parse(s),
1795 p: PatternTerm::parse(p),
1796 o: PatternTerm::parse(o),
1797 }
1798 }
1799
1800 #[test]
1809 fn scan_bytes_route_a_literal_by_its_object_id() {
1810 let value = "<http://ex/value>";
1811 let owned: Vec<(String, String)> = (0..400)
1812 .map(|i| (format!("<http://ex/s{i:03}>"), format!("\"lit {i:03}\"")))
1813 .collect();
1814 let mut db = DictionaryBuilder::new();
1815 for (s, o) in &owned {
1816 db.observe(s, value, o);
1817 }
1818 let dict = db.build();
1819 let mut ib = GraphIndexBuilder::new().with_tile_budget(256);
1820 for (s, o) in &owned {
1821 ib.push(dict.encode(s, value, o).unwrap());
1822 }
1823 let rete = Rete::open(&write_file(&dict, &ib.build(), false, &[], 0)).unwrap();
1824 let index = rete.default_index();
1825 let whole = routed_scan_bytes(index, (None, rete.dictionary().predicate_id(value), None));
1827 for (_, lit) in owned.iter().step_by(57) {
1828 let q = [pat("?s", value, lit)];
1829 let mut slots = Slots::new();
1830 collect_pattern_slots(&q, &mut slots);
1831 let ctx = Ctx::new(&rete, slots);
1832 let got = bgp_min_scan_bytes(&ctx, index, &q).unwrap();
1833 assert!(got > 0, "{lit} routed to no tile");
1834 assert!(
1835 got < whole,
1836 "{lit} routed to the whole predicate ({got} of {whole})"
1837 );
1838 }
1839 }
1840
1841 #[test]
1847 fn plan_gates_do_not_move_with_the_compression() {
1848 let mut owned: Vec<(String, String, String)> = Vec::new();
1849 for i in 0..3000 {
1850 let s = format!("<http://ex/s{i}>");
1851 owned.push((
1852 s.clone(),
1853 "<http://ex/kind>".into(),
1854 format!("<http://ex/K{}>", i % 7),
1855 ));
1856 owned.push((s.clone(), "<http://ex/value>".into(), format!("\"v {i}\"")));
1857 if i % 50 == 0 {
1858 owned.push((s, "<http://ex/rare>".into(), "<http://ex/R>".into()));
1859 }
1860 }
1861 let mut db = DictionaryBuilder::new();
1862 for (s, p, o) in &owned {
1863 db.observe(s, p, o);
1864 }
1865 let dict = db.build();
1866 let mut ib = GraphIndexBuilder::new().with_tile_budget(1024);
1867 for (s, p, o) in &owned {
1868 ib.push(dict.encode(s, p, o).unwrap());
1869 }
1870 let bytes = write_file(&dict, &ib.build(), false, &[], 0);
1871 let leaked: &'static [u8] = Box::leak(bytes.clone().into_boxed_slice());
1872 let resident = Rete::open(&bytes).unwrap();
1873 let lazy = Rete::open_ranged_lazy(crate::reader::SliceReader::new(leaked)).unwrap();
1874 let shapes: Vec<Vec<TriplePattern>> = vec![
1875 vec![
1876 pat("?s", "<http://ex/rare>", "<http://ex/R>"),
1877 pat("?s", "<http://ex/value>", "?v"),
1878 ],
1879 vec![
1880 pat("?s", "<http://ex/kind>", "<http://ex/K3>"),
1881 pat("?s", "<http://ex/value>", "?v"),
1882 ],
1883 vec![
1884 pat("?s", "<http://ex/kind>", "?k"),
1885 pat("?s", "<http://ex/value>", "?v"),
1886 ],
1887 vec![
1888 pat("?s", "<http://ex/value>", "\"v 7\""),
1889 pat("?s", "<http://ex/kind>", "?k"),
1890 ],
1891 ];
1892 let decisions = |rete: &Rete| -> Vec<(Vec<u64>, bool, Option<u64>)> {
1893 shapes
1894 .iter()
1895 .map(|q| {
1896 let mut slots = Slots::new();
1897 collect_pattern_slots(q, &mut slots);
1898 let ctx = Ctx::new(rete, slots);
1899 let index = rete.default_index();
1900 let lowered = lower(q, &ctx).unwrap();
1901 let est = tile_estimates(&ctx, index, &lowered, &Default::default());
1902 let lopsided = merge_seed_lopsided(&ctx, index, &lowered[0], &lowered[1]);
1903 let min = bgp_min_estimate(&ctx, index, q).map(|e| e as u64);
1904 (est.iter().map(|e| *e as u64).collect(), lopsided, min)
1905 })
1906 .collect()
1907 };
1908 assert_eq!(decisions(&resident), decisions(&lazy));
1909 #[cfg(feature = "compression")]
1911 {
1912 let bytes_of = |rete: &Rete| {
1913 let q = &shapes[2];
1914 let mut slots = Slots::new();
1915 collect_pattern_slots(q, &mut slots);
1916 let ctx = Ctx::new(rete, slots);
1917 bgp_min_scan_bytes(&ctx, rete.default_index(), q)
1918 };
1919 assert_ne!(bytes_of(&resident), bytes_of(&lazy));
1920 }
1921 }
1922
1923 #[test]
1928 fn every_join_path_returns_the_same_solutions() {
1929 let mut owned: Vec<(String, String, String)> = Vec::new();
1930 let mut x: u64 = 7;
1931 let mut next = |m: u64| {
1932 x = x
1933 .wrapping_mul(6364136223846793005)
1934 .wrapping_add(1442695040888963407);
1935 (x >> 33) % m
1936 };
1937 for i in 0..60 {
1938 let s = format!("s{i}");
1939 owned.push((s.clone(), "a".into(), format!("C{}", next(3))));
1940 for p in 0..4 {
1941 for _ in 0..next(3) {
1942 owned.push((s.clone(), format!("p{p}"), format!("s{}", next(60))));
1943 }
1944 }
1945 }
1946 let triples: Vec<(&str, &str, &str)> = owned
1947 .iter()
1948 .map(|(s, p, o)| (s.as_str(), p.as_str(), o.as_str()))
1949 .collect();
1950 let bytes = rete_from(&triples);
1951 let rete = Rete::open(&bytes).unwrap();
1952 let queries: Vec<Vec<TriplePattern>> = vec![
1953 vec![pat("?s", "p0", "?a"), pat("?s", "p1", "?b")],
1954 vec![pat("?s", "a", "C1"), pat("?s", "p0", "?a")],
1955 vec![pat("?s", "p0", "?a"), pat("?a", "p2", "?b")],
1956 vec![pat("s5", "p0", "?a"), pat("?a", "?p", "?o")],
1957 vec![pat("?s", "p0", "?x"), pat("?s", "p1", "?x")],
1958 vec![
1959 pat("?s", "p3", "?o"),
1960 pat("?o", "a", "C0"),
1961 pat("?s", "p1", "?z"),
1962 ],
1963 vec![
1964 pat("?s", "p0", "?a"),
1965 pat("?s", "p1", "?b"),
1966 pat("?b", "p2", "?c"),
1967 ],
1968 ];
1969 let run = |limits: Option<PlanLimits>| -> Vec<Vec<Vec<(String, String)>>> {
1970 TEST_LIMITS.with(|c| c.set(limits));
1971 let out = queries
1972 .iter()
1973 .map(|q| {
1974 let mut slots = crate::row::Slots::new();
1975 collect_pattern_slots(q, &mut slots);
1976 let ctx = Ctx::new(&rete, slots);
1977 let mut rows: Vec<Vec<(String, String)>> =
1978 BgpSolutions::new(&ctx, rete.default_index(), q)
1979 .map(|r| row_to_binding(&ctx, &r).into_iter().collect())
1980 .collect();
1981 rows.sort();
1982 rows
1983 })
1984 .collect();
1985 TEST_LIMITS.with(|c| c.set(None));
1986 out
1987 };
1988 let reference = run(None);
1989 assert!(reference.iter().all(|r| !r.is_empty()), "{reference:?}");
1990 for (name, l) in [
1991 (
1992 "probe everything",
1993 PlanLimits {
1994 probe_floor: 0.0,
1995 stream_seed: f64::MAX,
1996 hash_build: f64::MAX,
1997 },
1998 ),
1999 (
2000 "stream + hash",
2001 PlanLimits {
2002 probe_floor: f64::MAX,
2003 stream_seed: 0.0,
2004 hash_build: f64::MAX,
2005 },
2006 ),
2007 (
2008 "stream + probe",
2009 PlanLimits {
2010 probe_floor: f64::MAX,
2011 stream_seed: 0.0,
2012 hash_build: 0.0,
2013 },
2014 ),
2015 (
2016 "stream, all paths",
2017 PlanLimits {
2018 probe_floor: 0.0,
2019 stream_seed: 0.0,
2020 hash_build: 30.0,
2021 },
2022 ),
2023 ] {
2024 assert_eq!(run(Some(l)), reference, "{name}");
2025 }
2026 }
2027
2028 #[test]
2029 fn single_pattern_binds_variable() {
2030 let bytes = rete_from(&[("Alice", "knows", "Bob"), ("Bob", "knows", "Carol")]);
2031 let rete = Rete::open(&bytes).unwrap();
2032 let sols = eval_bgp(&rete, &[pat("Alice", "knows", "?y")]);
2033 assert_eq!(sols.len(), 1);
2034 assert_eq!(sols[0]["y"], "Bob");
2035 }
2036
2037 #[test]
2038 fn two_hop_join_on_shared_variable() {
2039 let bytes = rete_from(&[
2041 ("Alice", "knows", "Bob"),
2042 ("Bob", "knows", "Carol"),
2043 ("Carol", "knows", "Dave"),
2044 ("Alice", "knows", "Eve"), ]);
2046 let rete = Rete::open(&bytes).unwrap();
2047 let sols = eval_bgp(&rete, &[pat("?x", "knows", "?y"), pat("?y", "knows", "?z")]);
2048 let mut got: Vec<_> = sols
2050 .iter()
2051 .map(|b| (b["x"].clone(), b["y"].clone(), b["z"].clone()))
2052 .collect();
2053 got.sort();
2054 assert_eq!(
2055 got,
2056 vec![
2057 ("Alice".into(), "Bob".into(), "Carol".into()),
2058 ("Bob".into(), "Carol".into(), "Dave".into()),
2059 ]
2060 );
2061 }
2062
2063 #[test]
2064 fn repeated_variable_within_pattern() {
2065 let bytes = rete_from(&[("Alice", "knows", "Bob"), ("Bob", "knows", "Bob")]);
2067 let rete = Rete::open(&bytes).unwrap();
2068 let sols = eval_bgp(&rete, &[pat("?x", "knows", "?x")]);
2069 assert_eq!(sols.len(), 1);
2070 assert_eq!(sols[0]["x"], "Bob");
2071 }
2072
2073 #[test]
2074 fn no_solutions_yields_empty() {
2075 let bytes = rete_from(&[("Alice", "knows", "Bob")]);
2076 let rete = Rete::open(&bytes).unwrap();
2077 assert!(eval_bgp(&rete, &[pat("Alice", "likes", "?y")]).is_empty());
2078 }
2079
2080 #[test]
2087 fn snowflake_type_and_adjacency_matches_reference() {
2088 const TYPE: &str = "<http://www.w3.org/1999/02/22-rdf-syntax-ns#type>";
2089 let mut triples: Vec<(String, String, String)> = Vec::new();
2090 for i in 0..60 {
2092 let s = format!("S{i}");
2093 triples.push((s.clone(), TYPE.into(), "Student".into()));
2094 triples.push((s.clone(), "takesCourse".into(), format!("c{}", i % 5)));
2095 triples.push((s, "takesCourse".into(), format!("c{}", (i + 1) % 5)));
2096 }
2097 for j in 0..5 {
2099 triples.push((format!("c{j}"), TYPE.into(), "Course".into()));
2100 }
2101 triples.push(("Prof".into(), "teacherOf".into(), "c0".into()));
2102 triples.push(("Prof".into(), "teacherOf".into(), "c2".into()));
2103
2104 let refs: Vec<(&str, &str, &str)> = triples
2105 .iter()
2106 .map(|(s, p, o)| (s.as_str(), p.as_str(), o.as_str()))
2107 .collect();
2108 let bytes = rete_from(&refs);
2109 let rete = Rete::open(&bytes).unwrap();
2110
2111 let got: std::collections::BTreeSet<(String, String)> = eval_bgp(
2112 &rete,
2113 &[
2114 pat("?x", TYPE, "Student"),
2115 pat("?y", TYPE, "Course"),
2116 pat("?x", "takesCourse", "?y"),
2117 pat("Prof", "teacherOf", "?y"),
2118 ],
2119 )
2120 .into_iter()
2121 .map(|b| (b["x"].clone(), b["y"].clone()))
2122 .collect();
2123
2124 let taught = ["c0", "c2"];
2126 let mut want = std::collections::BTreeSet::new();
2127 for i in 0..60 {
2128 for c in [i % 5, (i + 1) % 5] {
2129 let course = format!("c{c}");
2130 if taught.contains(&course.as_str()) {
2131 want.insert((format!("S{i}"), course));
2132 }
2133 }
2134 }
2135 assert_eq!(got, want, "snowflake join must match the brute-force set");
2136 assert!(!want.is_empty(), "sanity: the reference set is non-empty");
2137 }
2138
2139 #[test]
2144 fn merge_join_subject_star_cross_product() {
2145 let bytes = rete_from(&[
2146 ("x", "a", "a1"),
2147 ("x", "a", "a2"),
2148 ("x", "b", "b1"),
2149 ("x", "b", "b2"),
2150 ("y", "a", "a3"),
2151 ("y", "b", "b3"),
2152 ("z", "a", "a4"), ]);
2154 let rete = Rete::open(&bytes).unwrap();
2155 let mut got: Vec<(String, String, String)> =
2156 eval_bgp(&rete, &[pat("?x", "a", "?av"), pat("?x", "b", "?bv")])
2157 .iter()
2158 .map(|m| (m["x"].clone(), m["av"].clone(), m["bv"].clone()))
2159 .collect();
2160 got.sort();
2161 let mut want: Vec<(String, String, String)> = [
2162 ("x", "a1", "b1"),
2163 ("x", "a1", "b2"),
2164 ("x", "a2", "b1"),
2165 ("x", "a2", "b2"),
2166 ("y", "a3", "b3"),
2167 ]
2168 .into_iter()
2169 .map(|(a, b, c)| (a.to_string(), b.to_string(), c.to_string()))
2170 .collect();
2171 want.sort();
2172 assert_eq!(got, want, "same-role merge must equal the brute-force join");
2173 }
2174}