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 triple_row(
663 ctx: &Ctx,
664 t: &(SlotTerm, SlotTerm, SlotTerm),
665 (s_id, p_id, o_id): (u32, u32, u32),
666) -> Option<Row> {
667 let dict = ctx.rete.dictionary();
668 let s_val = dict.subject_node(s_id) as i64;
669 let p_val = ctx.resolver.canon_id(pred_tag(p_id));
670 let o_val = dict.object_node(o_id) as i64;
671 let mut row = ctx.slots.empty_row();
672 for (term, val) in [(&t.0, s_val), (&t.1, p_val), (&t.2, o_val)] {
673 if let SlotTerm::Var(i) = term {
674 match row[*i] {
675 Some(Val::Id(existing)) if existing != val => return None,
676 Some(_) => {}
677 None => row[*i] = Some(Val::Id(val)),
678 }
679 }
680 }
681 Some(row)
682}
683
684fn scan_rows<'q>(
689 ctx: &'q Ctx<'q>,
690 index: &'q GraphIndex,
691 t: (SlotTerm, SlotTerm, SlotTerm),
692) -> Option<impl Iterator<Item = Row> + 'q> {
693 let dict = ctx.rete.dictionary();
694 let (sid, pid, oid) = (
695 const_subject(&t.0, dict)?,
696 const_predicate(&t.1)?,
697 const_object(&t.2, dict)?,
698 );
699 Some(
700 index
701 .scan_iter((sid, pid, oid))
702 .filter_map(move |triple| triple_row(ctx, &t, triple)),
703 )
704}
705
706fn pattern_rows(
709 ctx: &Ctx,
710 index: &GraphIndex,
711 t: &(SlotTerm, SlotTerm, SlotTerm),
712) -> Option<(Vec<Row>, Vec<usize>)> {
713 let slots = pattern_slots(t);
714 let mut rel: Vec<Row> = Vec::new();
715 let dict = ctx.rete.dictionary();
716 let (sid, pid, oid) = (
717 const_subject(&t.0, dict)?,
718 const_predicate(&t.1)?,
719 const_object(&t.2, dict)?,
720 );
721 for triple in index.scan_iter((sid, pid, oid)) {
724 if let Some(row) = triple_row(ctx, t, triple) {
725 rel.push(row);
726 }
727 }
728 Some((rel, slots))
729}
730
731fn pattern_estimates(
743 ctx: &Ctx,
744 lowered: &[(SlotTerm, SlotTerm, SlotTerm)],
745 seed: &std::collections::HashSet<usize>,
746) -> Option<Vec<f64>> {
747 let pyr = ctx.rete.pyramid_if_loaded()?;
750 let mut pred: std::collections::HashMap<u32, u64> = std::collections::HashMap::new();
751 for e in &pyr.summary {
752 *pred.entry(e.predicate).or_insert(0) += e.count as u64;
753 }
754 let stats: std::collections::HashMap<u32, &crate::meta::PredStat> = ctx
761 .rete
762 .planner_stats(true)
763 .iter()
764 .map(|s| (s.predicate, s))
765 .collect();
766 let total = ctx.rete.header().quad_count.max(1) as f64;
767 let num_preds = pred.len().max(1) as f64;
768 const SEL_SUBJECT: f64 = 0.001;
770 const SEL_OBJECT: f64 = 0.02;
771 let node_bound = |t: &SlotTerm| match t {
772 SlotTerm::Node(_) => true,
773 SlotTerm::Var(v) => seed.contains(v),
774 SlotTerm::Pred(_) => false,
775 };
776 Some(
777 lowered
778 .iter()
779 .map(|t| {
780 let (base, st) = match t.1 {
785 SlotTerm::Pred(p) => {
786 (*pred.get(&p).unwrap_or(&0) as f64, stats.get(&p).copied())
787 }
788 SlotTerm::Var(v) if seed.contains(&v) => (total / num_preds, None),
789 _ => (total, None),
790 };
791 let mut est = base.max(1.0);
792 if node_bound(&t.0) {
793 est *= match st {
794 Some(s) if s.distinct_subjects > 0 => 1.0 / s.distinct_subjects as f64,
795 _ => SEL_SUBJECT,
796 };
797 }
798 if node_bound(&t.2) {
799 est *= match st {
800 Some(s) if s.distinct_objects > 0 => 1.0 / s.distinct_objects as f64,
801 _ => SEL_OBJECT,
802 };
803 }
804 est.max(1.0)
805 })
806 .collect(),
807 )
808}
809
810pub(crate) fn bgp_min_scan_bytes(
817 ctx: &Ctx,
818 index: &GraphIndex,
819 patterns: &[TriplePattern],
820) -> Option<u64> {
821 let lowered = lower(patterns, ctx)?;
822 lowered
823 .iter()
824 .map(|t| pattern_scan_bytes(ctx, index, t))
825 .min()
826}
827
828fn pattern_scan_bytes(ctx: &Ctx, index: &GraphIndex, t: &(SlotTerm, SlotTerm, SlotTerm)) -> u64 {
845 let dict = ctx.rete.dictionary();
846 match (
847 const_subject(&t.0, dict),
848 const_predicate(&t.1),
849 const_object(&t.2, dict),
850 ) {
851 (Some(s), Some(p), Some(o)) => routed_scan_bytes(index, (s, p, o)),
852 _ => 0,
853 }
854}
855
856pub(crate) fn routed_scan_bytes(index: &GraphIndex, pattern: Pattern) -> u64 {
858 let sections = index.tile_sections();
859 let mut best = u64::MAX;
860 for perm in index.perms().iter() {
864 let tiles = sections[perm.section_index()];
865 best = best.min(match perm.order_pattern(pattern)[0] {
866 Some(id) => tiles
867 .iter()
868 .filter(|tile| {
869 let (lo, hi) = tile.leading_range();
870 lo <= id && id <= hi
871 })
872 .map(Tile::encoded_len)
873 .sum(),
874 None => tiles.iter().map(Tile::encoded_len).sum(),
875 });
876 }
877 best
878}
879
880pub(crate) const FAT_SCAN_ROWS: f64 = (1u64 << 20) as f64;
893
894fn merge_seed_lopsided(
899 ctx: &Ctx,
900 index: &GraphIndex,
901 ta: &(SlotTerm, SlotTerm, SlotTerm),
902 tb: &(SlotTerm, SlotTerm, SlotTerm),
903) -> bool {
904 let est = tile_estimates(ctx, index, &[*ta, *tb], &std::collections::HashSet::new());
905 let (lo, hi) = (est[0].min(est[1]), est[0].max(est[1]));
906 hi >= FAT_SCAN_ROWS && hi / 4.0 >= lo
907}
908
909fn selectivity_order(
916 ctx: &Ctx,
917 index: &GraphIndex,
918 lowered: &[(SlotTerm, SlotTerm, SlotTerm)],
919) -> Vec<usize> {
920 selectivity_order_seeded(ctx, index, lowered, &std::collections::HashSet::new())
921}
922
923fn selectivity_order_seeded(
927 ctx: &Ctx,
928 index: &GraphIndex,
929 lowered: &[(SlotTerm, SlotTerm, SlotTerm)],
930 seed: &std::collections::HashSet<usize>,
931) -> Vec<usize> {
932 let (estimates, per_class) = match pattern_estimates(ctx, lowered, seed) {
940 Some(e) => (Some(e), false),
941 None => (Some(tile_estimates(ctx, index, lowered, seed)), true),
942 };
943 let consts = |t: &(SlotTerm, SlotTerm, SlotTerm)| {
944 [&t.0, &t.1, &t.2]
945 .into_iter()
946 .filter(|x| match x {
947 SlotTerm::Var(v) => seed.contains(v),
948 _ => true,
949 })
950 .count()
951 };
952 let vars = |t: &(SlotTerm, SlotTerm, SlotTerm)| -> Vec<usize> {
953 [&t.0, &t.1, &t.2]
954 .into_iter()
955 .filter_map(|x| match x {
956 SlotTerm::Var(v) => Some(*v),
957 _ => None,
958 })
959 .collect()
960 };
961 let score = |i: usize| -> f64 {
965 match &estimates {
966 Some(e) => -e[i],
967 None => consts(&lowered[i]) as f64,
968 }
969 };
970 let type_pid = ctx.rete.dictionary().predicate_id(crate::file::RDF_TYPE);
978 let is_class_enum = |i: usize, bound: &std::collections::HashSet<usize>| -> bool {
979 let t = &lowered[i];
980 !per_class
981 && type_pid.is_some_and(|tp| matches!(t.1, SlotTerm::Pred(p) if p == tp))
982 && matches!(t.2, SlotTerm::Node(_))
983 && matches!(t.0, SlotTerm::Var(v) if !bound.contains(&v))
984 };
985 let n = lowered.len();
986 let mut remaining: Vec<usize> = (0..n).collect();
987 let mut order: Vec<usize> = Vec::with_capacity(n);
988 let mut bound: std::collections::HashSet<usize> = seed.clone();
989 while !remaining.is_empty() {
990 let best = *remaining
993 .iter()
994 .max_by(|&&a, &&b| {
995 let connected = |i: usize| vars(&lowered[i]).iter().any(|v| bound.contains(v));
996 connected(a)
997 .cmp(&connected(b))
998 .then_with(|| is_class_enum(b, &bound).cmp(&is_class_enum(a, &bound)))
999 .then_with(|| {
1000 score(a)
1001 .partial_cmp(&score(b))
1002 .unwrap_or(std::cmp::Ordering::Equal)
1003 })
1004 .then_with(|| b.cmp(&a))
1005 })
1006 .unwrap();
1007 for v in vars(&lowered[best]) {
1008 bound.insert(v);
1009 }
1010 order.push(best);
1011 remaining.retain(|&i| i != best);
1012 }
1013 order
1014}
1015
1016pub(crate) fn bgp_exists(ctx: &Ctx, index: &GraphIndex, patterns: &[TriplePattern]) -> bool {
1022 let dict = ctx.rete.dictionary();
1023 let Some(lowered) = lower(patterns, ctx) else {
1024 return false;
1025 };
1026 if let [t] = lowered.as_slice() {
1027 let names: Vec<usize> = [&t.0, &t.1, &t.2]
1031 .into_iter()
1032 .filter_map(|x| match x {
1033 SlotTerm::Var(v) => Some(*v),
1034 _ => None,
1035 })
1036 .collect();
1037 let distinct = names
1038 .iter()
1039 .enumerate()
1040 .all(|(i, v)| !names[i + 1..].contains(v));
1041 if distinct {
1042 return match (
1043 const_subject(&t.0, dict),
1044 const_predicate(&t.1),
1045 const_object(&t.2, dict),
1046 ) {
1047 (Some(s), Some(p), Some(o)) => index.scan_iter((s, p, o)).next().is_some(),
1048 _ => false,
1049 };
1050 }
1051 }
1052 !eval_bgp_rows(ctx, index, patterns).is_empty()
1053}
1054
1055pub(crate) struct BgpSolutions<'q> {
1066 scan: Option<Box<dyn Iterator<Item = Row> + 'q>>,
1067 prefix: Vec<Row>,
1068 buckets: HashMap<Vec<Val>, Vec<usize>>,
1070 shared: Vec<usize>,
1071 cartesian: bool,
1072 seed_only: bool,
1075 cur_scan: Option<Row>,
1076 matches: Vec<usize>,
1078 mi: usize,
1079}
1080
1081impl<'q> BgpSolutions<'q> {
1082 fn passthrough(rows: Box<dyn Iterator<Item = Row> + 'q>) -> Self {
1084 BgpSolutions {
1085 scan: Some(rows),
1086 prefix: Vec::new(),
1087 buckets: HashMap::new(),
1088 shared: Vec::new(),
1089 cartesian: false,
1090 seed_only: true,
1091 cur_scan: None,
1092 matches: Vec::new(),
1093 mi: 0,
1094 }
1095 }
1096
1097 fn streaming(
1106 ctx: &'q Ctx<'q>,
1107 index: &'q GraphIndex,
1108 lowered: &[(SlotTerm, SlotTerm, SlotTerm)],
1109 order: &[usize],
1110 est: &[f64],
1111 ) -> Self {
1112 let (mut stream, mut bound, skip): (Box<dyn Iterator<Item = Row> + 'q>, Vec<usize>, usize) =
1113 match (est[order[1]] > limits().stream_seed)
1114 .then(|| merge_stream(ctx, index, lowered[order[0]], lowered[order[1]]))
1115 .flatten()
1116 {
1117 Some((m, slots)) => (m, slots, 2),
1118 None => {
1119 let t = lowered[order[0]];
1120 let Some(scan) = scan_rows(ctx, index, t) else {
1121 return Self::empty();
1122 };
1123 (Box::new(scan), pattern_slots(&t), 1)
1124 }
1125 };
1126 for &i in &order[skip..] {
1127 let t = lowered[i];
1128 let slots = pattern_slots(&t);
1129 let shared: Vec<usize> = slots
1130 .iter()
1131 .copied()
1132 .filter(|s| bound.contains(s))
1133 .collect();
1134 if !shared.is_empty() && est[i] > limits().hash_build {
1135 stream = Box::new(stream.flat_map(move |base| probe_rows(ctx, index, t, base)));
1136 } else {
1137 let Some((rel, _)) = pattern_rows(ctx, index, &t) else {
1140 return Self::empty();
1141 };
1142 let mut table: HashMap<Vec<Val>, Vec<Row>> = HashMap::new();
1143 for r in rel {
1144 let key: Vec<Val> = shared.iter().map(|&s| r[s].clone().unwrap()).collect();
1145 table.entry(key).or_default().push(r);
1146 }
1147 let fill = slots.clone();
1148 stream = Box::new(stream.flat_map(move |base| {
1149 let key: Vec<Val> = shared.iter().map(|&s| base[s].clone().unwrap()).collect();
1150 let out: Vec<Row> = table
1151 .get(&key)
1152 .map(|rs| {
1153 rs.iter()
1154 .map(|r| {
1155 let mut row = base.clone();
1156 for &s in &fill {
1157 row[s] = r[s].clone();
1158 }
1159 row
1160 })
1161 .collect()
1162 })
1163 .unwrap_or_default();
1164 out.into_iter()
1165 }));
1166 }
1167 for s in slots {
1168 if !bound.contains(&s) {
1169 bound.push(s);
1170 }
1171 }
1172 }
1173 Self::passthrough(stream)
1174 }
1175
1176 fn empty() -> Self {
1178 BgpSolutions {
1179 scan: None,
1180 prefix: Vec::new(),
1181 buckets: HashMap::new(),
1182 shared: Vec::new(),
1183 cartesian: false,
1184 seed_only: false,
1185 cur_scan: None,
1186 matches: Vec::new(),
1187 mi: 0,
1188 }
1189 }
1190
1191 pub(crate) fn new(ctx: &'q Ctx<'q>, index: &'q GraphIndex, patterns: &[TriplePattern]) -> Self {
1192 if patterns.is_empty() {
1194 return BgpSolutions {
1195 scan: Some(Box::new(std::iter::once(ctx.slots.empty_row()))),
1196 prefix: vec![ctx.slots.empty_row()],
1197 buckets: HashMap::new(),
1198 shared: Vec::new(),
1199 cartesian: true,
1200 seed_only: true,
1201 cur_scan: None,
1202 matches: Vec::new(),
1203 mi: 0,
1204 };
1205 }
1206 let Some(lowered) = lower(patterns, ctx) else {
1208 return Self::empty();
1209 };
1210 let order = selectivity_order(ctx, index, &lowered);
1213 let scan_est = tile_estimates(ctx, index, &lowered, &std::collections::HashSet::new());
1214 if order.len() >= 2 && scan_est[order[0]] > limits().stream_seed {
1217 return Self::streaming(ctx, index, &lowered, &order, &scan_est);
1218 }
1219 let (&last_i, prefix_is) = order.split_last().unwrap();
1220 let prefix_pats: Vec<TriplePattern> =
1221 prefix_is.iter().map(|&i| patterns[i].clone()).collect();
1222 let prefix = eval_bgp_rows(ctx, index, &prefix_pats);
1223 if prefix.is_empty() {
1224 return Self::empty();
1225 }
1226
1227 let shared: Vec<usize> = pattern_slots(&lowered[last_i])
1231 .into_iter()
1232 .filter(|&s| prefix[0][s].is_some())
1233 .collect();
1234
1235 if !shared.is_empty() && probe_beats_scan(prefix.len(), scan_est[last_i]) {
1239 let t = lowered[last_i];
1240 if index.is_remote() {
1241 prefetch_rows_for(ctx, index, t, &prefix);
1242 }
1243 let stream = prefix
1244 .into_iter()
1245 .flat_map(move |base| probe_rows(ctx, index, t, base));
1246 return Self::passthrough(Box::new(stream));
1247 }
1248
1249 let Some(scan) = scan_rows(ctx, index, lowered[last_i]) else {
1251 return Self::empty();
1252 };
1253 let cartesian = shared.is_empty();
1254 let seed_only = prefix.len() == 1 && prefix[0].iter().all(Option::is_none);
1257 let mut buckets: HashMap<Vec<Val>, Vec<usize>> = HashMap::new();
1258 if !cartesian {
1259 for (i, r) in prefix.iter().enumerate() {
1260 let key: Vec<Val> = shared.iter().map(|&s| r[s].clone().unwrap()).collect();
1261 buckets.entry(key).or_default().push(i);
1262 }
1263 }
1264 BgpSolutions {
1265 scan: Some(Box::new(scan)),
1266 prefix,
1267 buckets,
1268 shared,
1269 cartesian,
1270 seed_only,
1271 cur_scan: None,
1272 matches: Vec::new(),
1273 mi: 0,
1274 }
1275 }
1276}
1277
1278impl Iterator for BgpSolutions<'_> {
1279 type Item = Row;
1280
1281 fn next(&mut self) -> Option<Row> {
1282 if self.seed_only {
1284 return self.scan.as_mut()?.next();
1285 }
1286 loop {
1287 if self.mi < self.matches.len() {
1289 let pi = self.matches[self.mi];
1290 self.mi += 1;
1291 let mut merged = self.prefix[pi].clone();
1292 for (slot, v) in self.cur_scan.as_ref().unwrap().iter().enumerate() {
1293 if v.is_some() {
1294 merged[slot] = v.clone();
1295 }
1296 }
1297 return Some(merged);
1298 }
1299 let s = self.scan.as_mut()?.next()?;
1302 self.matches.clear();
1303 if self.cartesian {
1304 self.matches.extend(0..self.prefix.len());
1305 } else {
1306 let key: Vec<Val> = self.shared.iter().map(|&i| s[i].clone().unwrap()).collect();
1307 if let Some(c) = self.buckets.get(&key) {
1308 self.matches.extend_from_slice(c);
1309 }
1310 }
1311 self.mi = 0;
1312 self.cur_scan = Some(s);
1313 }
1314 }
1315}
1316
1317fn prefetch_rows_for(
1320 ctx: &Ctx,
1321 index: &GraphIndex,
1322 t: (SlotTerm, SlotTerm, SlotTerm),
1323 rows: &[Row],
1324) {
1325 let pats: Vec<Pattern> = rows
1326 .iter()
1327 .filter_map(|base| {
1328 match (
1329 probe_subject(ctx, &t.0, base),
1330 probe_predicate(ctx, &t.1, base),
1331 probe_object(ctx, &t.2, base),
1332 ) {
1333 (Some(s), Some(p), Some(o)) => Some((s, p, o)),
1334 _ => None,
1335 }
1336 })
1337 .collect();
1338 index.prefetch_probe_tiles(&pats);
1339}
1340
1341#[allow(clippy::type_complexity)]
1349fn merge_stream<'q>(
1350 ctx: &'q Ctx<'q>,
1351 index: &'q GraphIndex,
1352 ta: (SlotTerm, SlotTerm, SlotTerm),
1353 tb: (SlotTerm, SlotTerm, SlotTerm),
1354) -> Option<(Box<dyn Iterator<Item = Row> + 'q>, Vec<usize>)> {
1355 let sa = pattern_slots(&ta);
1356 let sb = pattern_slots(&tb);
1357 let shared: Vec<usize> = sa.iter().copied().filter(|s| sb.contains(s)).collect();
1358 let [v] = shared[..] else {
1359 return None;
1360 };
1361 if !matches!(ta.0, SlotTerm::Var(i) if i == v) || !matches!(tb.0, SlotTerm::Var(i) if i == v) {
1362 return None;
1363 }
1364 let dict = ctx.rete.dictionary();
1365 let lower = |t: &(SlotTerm, SlotTerm, SlotTerm)| -> Option<Pattern> {
1366 Some((
1367 const_subject(&t.0, dict)?,
1368 const_predicate(&t.1)?,
1369 const_object(&t.2, dict)?,
1370 ))
1371 };
1372 let side = |t: (SlotTerm, SlotTerm, SlotTerm)| -> Option<KeyedRows<'q>> {
1373 let it = index.scan_iter_sorted_on(lower(&t)?, 0)?;
1374 let keyed: Box<dyn Iterator<Item = (u32, Row)> + 'q> =
1375 Box::new(it.filter_map(move |tri| triple_row(ctx, &t, tri).map(|r| (tri.0, r))));
1376 Some(keyed.peekable())
1377 };
1378 let a = side(ta)?;
1379 let b = side(tb)?;
1380 let mut slots = sa;
1381 for s in sb {
1382 if !slots.contains(&s) {
1383 slots.push(s);
1384 }
1385 }
1386 Some((
1387 Box::new(MergeStream {
1388 a,
1389 b,
1390 ra: Vec::new(),
1391 rb: Vec::new(),
1392 i: 0,
1393 j: 0,
1394 }),
1395 slots,
1396 ))
1397}
1398
1399type KeyedRows<'q> = std::iter::Peekable<Box<dyn Iterator<Item = (u32, Row)> + 'q>>;
1400
1401struct MergeStream<'q> {
1404 a: KeyedRows<'q>,
1405 b: KeyedRows<'q>,
1406 ra: Vec<Row>,
1407 rb: Vec<Row>,
1408 i: usize,
1409 j: usize,
1410}
1411
1412impl Iterator for MergeStream<'_> {
1413 type Item = Row;
1414
1415 fn next(&mut self) -> Option<Row> {
1416 loop {
1417 while self.i < self.ra.len() {
1418 if self.j < self.rb.len() {
1419 let j = self.j;
1420 self.j += 1;
1421 if let Some(m) = crate::row::merge_rows(&self.ra[self.i], &self.rb[j]) {
1422 return Some(m);
1423 }
1424 } else {
1425 self.i += 1;
1426 self.j = 0;
1427 }
1428 }
1429 let ka = self.a.peek()?.0;
1430 let kb = self.b.peek()?.0;
1431 match ka.cmp(&kb) {
1432 std::cmp::Ordering::Less => {
1433 self.a.next();
1434 }
1435 std::cmp::Ordering::Greater => {
1436 self.b.next();
1437 }
1438 std::cmp::Ordering::Equal => {
1439 self.ra.clear();
1440 self.rb.clear();
1441 while let Some((_, r)) = self.a.next_if(|(k, _)| *k == ka) {
1442 self.ra.push(r);
1443 }
1444 while let Some((_, r)) = self.b.next_if(|(k, _)| *k == ka) {
1445 self.rb.push(r);
1446 }
1447 self.i = 0;
1448 self.j = 0;
1449 }
1450 }
1451 }
1452 }
1453}
1454
1455fn probe_subject(ctx: &Ctx, t: &SlotTerm, base: &Row) -> Option<Option<u32>> {
1469 let dict = ctx.rete.dictionary();
1470 match t {
1471 SlotTerm::Node(n) => dict.node_as_subject_id(*n).map(Some),
1472 SlotTerm::Pred(_) => None,
1473 SlotTerm::Var(i) => match &base[*i] {
1474 None => Some(None),
1475 Some(Val::Id(v)) if *v >= 0 => dict.node_as_subject_id(*v as u32).map(Some),
1476 Some(_) => None,
1478 },
1479 }
1480}
1481
1482fn probe_object(ctx: &Ctx, t: &SlotTerm, base: &Row) -> Option<Option<u32>> {
1484 let dict = ctx.rete.dictionary();
1485 match t {
1486 SlotTerm::Node(n) => dict.node_as_object_id(*n).map(Some),
1487 SlotTerm::Pred(_) => None,
1488 SlotTerm::Var(i) => match &base[*i] {
1489 None => Some(None),
1490 Some(Val::Id(v)) if *v >= 0 => dict.node_as_object_id(*v as u32).map(Some),
1491 Some(_) => None,
1492 },
1493 }
1494}
1495
1496fn probe_predicate(ctx: &Ctx, t: &SlotTerm, base: &Row) -> Option<Option<u32>> {
1498 let dict = ctx.rete.dictionary();
1499 match t {
1500 SlotTerm::Pred(p) => Some(Some(*p)),
1501 SlotTerm::Node(_) => None,
1502 SlotTerm::Var(i) => match &base[*i] {
1503 None => Some(None),
1504 Some(Val::Id(v)) if *v < 0 => Some(Some((-v - 1) as u32)),
1505 Some(Val::Id(v)) => ctx
1507 .resolver
1508 .term(*v)
1509 .and_then(|t| dict.predicate_id(&t))
1510 .map(Some),
1511 Some(Val::Str(_)) => None,
1512 },
1513 }
1514}
1515
1516fn probe_rows<'q>(
1520 ctx: &'q Ctx<'q>,
1521 index: &'q GraphIndex,
1522 t: (SlotTerm, SlotTerm, SlotTerm),
1523 base: Row,
1524) -> Box<dyn Iterator<Item = Row> + 'q> {
1525 let (Some(sid), Some(pid), Some(oid)) = (
1526 probe_subject(ctx, &t.0, &base),
1527 probe_predicate(ctx, &t.1, &base),
1528 probe_object(ctx, &t.2, &base),
1529 ) else {
1530 return Box::new(std::iter::empty());
1531 };
1532 let dict = ctx.rete.dictionary();
1533 Box::new(
1534 index
1535 .scan_iter((sid, pid, oid))
1536 .filter_map(move |(s_id, p_id, o_id)| {
1537 let s_val = dict.subject_node(s_id) as i64;
1538 let p_val = ctx.resolver.canon_id(pred_tag(p_id));
1539 let o_val = dict.object_node(o_id) as i64;
1540 let mut row = base.clone();
1541 for (term, val) in [(&t.0, s_val), (&t.1, p_val), (&t.2, o_val)] {
1542 if let SlotTerm::Var(i) = term {
1543 match row[*i] {
1544 Some(Val::Id(existing)) if existing != val => return None,
1545 Some(Val::Id(_)) => {}
1546 Some(Val::Str(_)) => return None,
1547 None => row[*i] = Some(Val::Id(val)),
1548 }
1549 }
1550 }
1551 Some(row)
1552 }),
1553 )
1554}
1555
1556pub(crate) struct ProbePlan {
1558 pats: Vec<(SlotTerm, SlotTerm, SlotTerm)>,
1559}
1560
1561impl ProbePlan {
1562 pub(crate) fn new(
1566 ctx: &Ctx,
1567 index: &GraphIndex,
1568 patterns: &[TriplePattern],
1569 seed_mask: &[bool],
1570 ) -> Option<Self> {
1571 let lowered = lower(patterns, ctx)?;
1572 let seed: std::collections::HashSet<usize> = seed_mask
1573 .iter()
1574 .enumerate()
1575 .filter_map(|(i, b)| b.then_some(i))
1576 .collect();
1577 let order = selectivity_order_seeded(ctx, index, &lowered, &seed);
1578 Some(ProbePlan {
1579 pats: order.into_iter().map(|i| lowered[i]).collect(),
1580 })
1581 }
1582}
1583
1584pub(crate) struct ProbeJoin<'q> {
1587 ctx: &'q Ctx<'q>,
1588 index: &'q GraphIndex,
1589 pats: Vec<(SlotTerm, SlotTerm, SlotTerm)>,
1590 stack: Vec<Box<dyn Iterator<Item = Row> + 'q>>,
1591}
1592
1593impl<'q> ProbeJoin<'q> {
1594 pub(crate) fn new(
1597 ctx: &'q Ctx<'q>,
1598 index: &'q GraphIndex,
1599 patterns: &[TriplePattern],
1600 ) -> Option<Self> {
1601 let plan = ProbePlan::new(ctx, index, patterns, &vec![false; ctx.slots.len()])?;
1602 Some(Self::from_plan(ctx, index, &plan, ctx.slots.empty_row()))
1603 }
1604
1605 pub(crate) fn from_plan(
1607 ctx: &'q Ctx<'q>,
1608 index: &'q GraphIndex,
1609 plan: &ProbePlan,
1610 seed: Row,
1611 ) -> Self {
1612 let pats = plan.pats.clone();
1613 let first = probe_rows(ctx, index, pats[0], seed);
1614 ProbeJoin {
1615 ctx,
1616 index,
1617 pats,
1618 stack: vec![first],
1619 }
1620 }
1621}
1622
1623impl Iterator for ProbeJoin<'_> {
1624 type Item = Row;
1625
1626 fn next(&mut self) -> Option<Row> {
1627 loop {
1628 let depth = self.stack.len();
1629 match self.stack.last_mut()?.next() {
1630 Some(row) => {
1631 if depth == self.pats.len() {
1632 return Some(row);
1633 }
1634 let it = probe_rows(self.ctx, self.index, self.pats[depth], row);
1635 self.stack.push(it);
1636 }
1637 None => {
1638 self.stack.pop();
1639 if self.stack.is_empty() {
1640 return None;
1641 }
1642 }
1643 }
1644 }
1645 }
1646}
1647
1648fn hash_join(
1652 left: Vec<Row>,
1653 left_bound: &[usize],
1654 right: Vec<Row>,
1655 right_slots: &[usize],
1656) -> Vec<Row> {
1657 if left.len() == 1 && left_bound.is_empty() {
1659 return right;
1660 }
1661 if left.is_empty() || right.is_empty() {
1662 return Vec::new();
1663 }
1664 let shared: Vec<usize> = right_slots
1665 .iter()
1666 .copied()
1667 .filter(|s| left_bound.contains(s))
1668 .collect();
1669 let fill = |l: &Row, r: &Row| -> Row {
1670 let mut out = l.clone();
1671 for &s in right_slots {
1672 out[s] = r[s].clone();
1673 }
1674 out
1675 };
1676 if shared.is_empty() {
1677 let mut out = Vec::with_capacity(left.len() * right.len());
1681 for l in &left {
1682 for r in &right {
1683 out.push(fill(l, r));
1684 }
1685 }
1686 return out;
1687 }
1688 let key_of = |b: &Row| -> Vec<Val> { shared.iter().map(|&s| b[s].clone().unwrap()).collect() };
1689 let mut out = Vec::new();
1696 if right.len() <= left.len() {
1697 let mut buckets: HashMap<Vec<Val>, Vec<Row>> = HashMap::new();
1698 for r in right {
1699 buckets.entry(key_of(&r)).or_default().push(r);
1700 }
1701 for l in &left {
1702 if let Some(rs) = buckets.get(&key_of(l)) {
1703 for r in rs {
1704 out.push(fill(l, r));
1705 }
1706 }
1707 }
1708 } else {
1709 let mut buckets: HashMap<Vec<Val>, Vec<Row>> = HashMap::new();
1711 for l in left {
1712 buckets.entry(key_of(&l)).or_default().push(l);
1713 }
1714 for r in &right {
1715 if let Some(ls) = buckets.get(&key_of(r)) {
1716 for l in ls {
1717 let mut row = r.clone();
1718 for &s in left_bound {
1719 if let Some(v) = &l[s] {
1720 row[s] = Some(v.clone());
1721 }
1722 }
1723 out.push(row);
1724 }
1725 }
1726 }
1727 }
1728 out
1729}
1730
1731fn const_subject(t: &SlotTerm, d: &crate::Dictionary) -> Option<Option<u32>> {
1736 match t {
1737 SlotTerm::Node(n) => d.node_as_subject_id(*n).map(Some),
1738 SlotTerm::Pred(_) => None, SlotTerm::Var(_) => Some(None),
1740 }
1741}
1742
1743fn const_object(t: &SlotTerm, d: &crate::Dictionary) -> Option<Option<u32>> {
1744 match t {
1745 SlotTerm::Node(n) => d.node_as_object_id(*n).map(Some),
1746 SlotTerm::Pred(_) => None,
1747 SlotTerm::Var(_) => Some(None),
1748 }
1749}
1750
1751fn const_predicate(t: &SlotTerm) -> Option<Option<u32>> {
1752 match t {
1753 SlotTerm::Pred(p) => Some(Some(*p)),
1754 SlotTerm::Node(_) => None, SlotTerm::Var(_) => Some(None),
1756 }
1757}
1758
1759#[cfg(test)]
1760mod tests {
1761 use super::*;
1762 use crate::dictionary::DictionaryBuilder;
1763 use crate::file::write_file;
1764 use crate::index::GraphIndexBuilder;
1765
1766 fn rete_from(triples: &[(&str, &str, &str)]) -> Vec<u8> {
1767 let mut db = DictionaryBuilder::new();
1768 for (s, p, o) in triples {
1769 db.observe(s, p, o);
1770 }
1771 let dict = db.build();
1772 let mut ib = GraphIndexBuilder::new();
1773 for (s, p, o) in triples {
1774 ib.push(dict.encode(s, p, o).unwrap());
1775 }
1776 write_file(&dict, &ib.build(), false, &[], 0)
1777 }
1778
1779 fn pat(s: &str, p: &str, o: &str) -> TriplePattern {
1780 TriplePattern {
1781 s: PatternTerm::parse(s),
1782 p: PatternTerm::parse(p),
1783 o: PatternTerm::parse(o),
1784 }
1785 }
1786
1787 #[test]
1796 fn scan_bytes_route_a_literal_by_its_object_id() {
1797 let value = "<http://ex/value>";
1798 let owned: Vec<(String, String)> = (0..400)
1799 .map(|i| (format!("<http://ex/s{i:03}>"), format!("\"lit {i:03}\"")))
1800 .collect();
1801 let mut db = DictionaryBuilder::new();
1802 for (s, o) in &owned {
1803 db.observe(s, value, o);
1804 }
1805 let dict = db.build();
1806 let mut ib = GraphIndexBuilder::new().with_tile_budget(256);
1807 for (s, o) in &owned {
1808 ib.push(dict.encode(s, value, o).unwrap());
1809 }
1810 let rete = Rete::open(&write_file(&dict, &ib.build(), false, &[], 0)).unwrap();
1811 let index = rete.default_index();
1812 let whole = routed_scan_bytes(index, (None, rete.dictionary().predicate_id(value), None));
1814 for (_, lit) in owned.iter().step_by(57) {
1815 let q = [pat("?s", value, lit)];
1816 let mut slots = Slots::new();
1817 collect_pattern_slots(&q, &mut slots);
1818 let ctx = Ctx::new(&rete, slots);
1819 let got = bgp_min_scan_bytes(&ctx, index, &q).unwrap();
1820 assert!(got > 0, "{lit} routed to no tile");
1821 assert!(
1822 got < whole,
1823 "{lit} routed to the whole predicate ({got} of {whole})"
1824 );
1825 }
1826 }
1827
1828 #[test]
1834 fn plan_gates_do_not_move_with_the_compression() {
1835 let mut owned: Vec<(String, String, String)> = Vec::new();
1836 for i in 0..3000 {
1837 let s = format!("<http://ex/s{i}>");
1838 owned.push((
1839 s.clone(),
1840 "<http://ex/kind>".into(),
1841 format!("<http://ex/K{}>", i % 7),
1842 ));
1843 owned.push((s.clone(), "<http://ex/value>".into(), format!("\"v {i}\"")));
1844 if i % 50 == 0 {
1845 owned.push((s, "<http://ex/rare>".into(), "<http://ex/R>".into()));
1846 }
1847 }
1848 let mut db = DictionaryBuilder::new();
1849 for (s, p, o) in &owned {
1850 db.observe(s, p, o);
1851 }
1852 let dict = db.build();
1853 let mut ib = GraphIndexBuilder::new().with_tile_budget(1024);
1854 for (s, p, o) in &owned {
1855 ib.push(dict.encode(s, p, o).unwrap());
1856 }
1857 let bytes = write_file(&dict, &ib.build(), false, &[], 0);
1858 let leaked: &'static [u8] = Box::leak(bytes.clone().into_boxed_slice());
1859 let resident = Rete::open(&bytes).unwrap();
1860 let lazy = Rete::open_ranged_lazy(crate::reader::SliceReader::new(leaked)).unwrap();
1861 let shapes: Vec<Vec<TriplePattern>> = vec![
1862 vec![
1863 pat("?s", "<http://ex/rare>", "<http://ex/R>"),
1864 pat("?s", "<http://ex/value>", "?v"),
1865 ],
1866 vec![
1867 pat("?s", "<http://ex/kind>", "<http://ex/K3>"),
1868 pat("?s", "<http://ex/value>", "?v"),
1869 ],
1870 vec![
1871 pat("?s", "<http://ex/kind>", "?k"),
1872 pat("?s", "<http://ex/value>", "?v"),
1873 ],
1874 vec![
1875 pat("?s", "<http://ex/value>", "\"v 7\""),
1876 pat("?s", "<http://ex/kind>", "?k"),
1877 ],
1878 ];
1879 let decisions = |rete: &Rete| -> Vec<(Vec<u64>, bool, Option<u64>)> {
1880 shapes
1881 .iter()
1882 .map(|q| {
1883 let mut slots = Slots::new();
1884 collect_pattern_slots(q, &mut slots);
1885 let ctx = Ctx::new(rete, slots);
1886 let index = rete.default_index();
1887 let lowered = lower(q, &ctx).unwrap();
1888 let est = tile_estimates(&ctx, index, &lowered, &Default::default());
1889 let lopsided = merge_seed_lopsided(&ctx, index, &lowered[0], &lowered[1]);
1890 let min = bgp_min_estimate(&ctx, index, q).map(|e| e as u64);
1891 (est.iter().map(|e| *e as u64).collect(), lopsided, min)
1892 })
1893 .collect()
1894 };
1895 assert_eq!(decisions(&resident), decisions(&lazy));
1896 #[cfg(feature = "compression")]
1898 {
1899 let bytes_of = |rete: &Rete| {
1900 let q = &shapes[2];
1901 let mut slots = Slots::new();
1902 collect_pattern_slots(q, &mut slots);
1903 let ctx = Ctx::new(rete, slots);
1904 bgp_min_scan_bytes(&ctx, rete.default_index(), q)
1905 };
1906 assert_ne!(bytes_of(&resident), bytes_of(&lazy));
1907 }
1908 }
1909
1910 #[test]
1915 fn every_join_path_returns_the_same_solutions() {
1916 let mut owned: Vec<(String, String, String)> = Vec::new();
1917 let mut x: u64 = 7;
1918 let mut next = |m: u64| {
1919 x = x
1920 .wrapping_mul(6364136223846793005)
1921 .wrapping_add(1442695040888963407);
1922 (x >> 33) % m
1923 };
1924 for i in 0..60 {
1925 let s = format!("s{i}");
1926 owned.push((s.clone(), "a".into(), format!("C{}", next(3))));
1927 for p in 0..4 {
1928 for _ in 0..next(3) {
1929 owned.push((s.clone(), format!("p{p}"), format!("s{}", next(60))));
1930 }
1931 }
1932 }
1933 let triples: Vec<(&str, &str, &str)> = owned
1934 .iter()
1935 .map(|(s, p, o)| (s.as_str(), p.as_str(), o.as_str()))
1936 .collect();
1937 let bytes = rete_from(&triples);
1938 let rete = Rete::open(&bytes).unwrap();
1939 let queries: Vec<Vec<TriplePattern>> = vec![
1940 vec![pat("?s", "p0", "?a"), pat("?s", "p1", "?b")],
1941 vec![pat("?s", "a", "C1"), pat("?s", "p0", "?a")],
1942 vec![pat("?s", "p0", "?a"), pat("?a", "p2", "?b")],
1943 vec![pat("s5", "p0", "?a"), pat("?a", "?p", "?o")],
1944 vec![pat("?s", "p0", "?x"), pat("?s", "p1", "?x")],
1945 vec![
1946 pat("?s", "p3", "?o"),
1947 pat("?o", "a", "C0"),
1948 pat("?s", "p1", "?z"),
1949 ],
1950 vec![
1951 pat("?s", "p0", "?a"),
1952 pat("?s", "p1", "?b"),
1953 pat("?b", "p2", "?c"),
1954 ],
1955 ];
1956 let run = |limits: Option<PlanLimits>| -> Vec<Vec<Vec<(String, String)>>> {
1957 TEST_LIMITS.with(|c| c.set(limits));
1958 let out = queries
1959 .iter()
1960 .map(|q| {
1961 let mut slots = crate::row::Slots::new();
1962 collect_pattern_slots(q, &mut slots);
1963 let ctx = Ctx::new(&rete, slots);
1964 let mut rows: Vec<Vec<(String, String)>> =
1965 BgpSolutions::new(&ctx, rete.default_index(), q)
1966 .map(|r| row_to_binding(&ctx, &r).into_iter().collect())
1967 .collect();
1968 rows.sort();
1969 rows
1970 })
1971 .collect();
1972 TEST_LIMITS.with(|c| c.set(None));
1973 out
1974 };
1975 let reference = run(None);
1976 assert!(reference.iter().all(|r| !r.is_empty()), "{reference:?}");
1977 for (name, l) in [
1978 (
1979 "probe everything",
1980 PlanLimits {
1981 probe_floor: 0.0,
1982 stream_seed: f64::MAX,
1983 hash_build: f64::MAX,
1984 },
1985 ),
1986 (
1987 "stream + hash",
1988 PlanLimits {
1989 probe_floor: f64::MAX,
1990 stream_seed: 0.0,
1991 hash_build: f64::MAX,
1992 },
1993 ),
1994 (
1995 "stream + probe",
1996 PlanLimits {
1997 probe_floor: f64::MAX,
1998 stream_seed: 0.0,
1999 hash_build: 0.0,
2000 },
2001 ),
2002 (
2003 "stream, all paths",
2004 PlanLimits {
2005 probe_floor: 0.0,
2006 stream_seed: 0.0,
2007 hash_build: 30.0,
2008 },
2009 ),
2010 ] {
2011 assert_eq!(run(Some(l)), reference, "{name}");
2012 }
2013 }
2014
2015 #[test]
2016 fn single_pattern_binds_variable() {
2017 let bytes = rete_from(&[("Alice", "knows", "Bob"), ("Bob", "knows", "Carol")]);
2018 let rete = Rete::open(&bytes).unwrap();
2019 let sols = eval_bgp(&rete, &[pat("Alice", "knows", "?y")]);
2020 assert_eq!(sols.len(), 1);
2021 assert_eq!(sols[0]["y"], "Bob");
2022 }
2023
2024 #[test]
2025 fn two_hop_join_on_shared_variable() {
2026 let bytes = rete_from(&[
2028 ("Alice", "knows", "Bob"),
2029 ("Bob", "knows", "Carol"),
2030 ("Carol", "knows", "Dave"),
2031 ("Alice", "knows", "Eve"), ]);
2033 let rete = Rete::open(&bytes).unwrap();
2034 let sols = eval_bgp(&rete, &[pat("?x", "knows", "?y"), pat("?y", "knows", "?z")]);
2035 let mut got: Vec<_> = sols
2037 .iter()
2038 .map(|b| (b["x"].clone(), b["y"].clone(), b["z"].clone()))
2039 .collect();
2040 got.sort();
2041 assert_eq!(
2042 got,
2043 vec![
2044 ("Alice".into(), "Bob".into(), "Carol".into()),
2045 ("Bob".into(), "Carol".into(), "Dave".into()),
2046 ]
2047 );
2048 }
2049
2050 #[test]
2051 fn repeated_variable_within_pattern() {
2052 let bytes = rete_from(&[("Alice", "knows", "Bob"), ("Bob", "knows", "Bob")]);
2054 let rete = Rete::open(&bytes).unwrap();
2055 let sols = eval_bgp(&rete, &[pat("?x", "knows", "?x")]);
2056 assert_eq!(sols.len(), 1);
2057 assert_eq!(sols[0]["x"], "Bob");
2058 }
2059
2060 #[test]
2061 fn no_solutions_yields_empty() {
2062 let bytes = rete_from(&[("Alice", "knows", "Bob")]);
2063 let rete = Rete::open(&bytes).unwrap();
2064 assert!(eval_bgp(&rete, &[pat("Alice", "likes", "?y")]).is_empty());
2065 }
2066
2067 #[test]
2074 fn snowflake_type_and_adjacency_matches_reference() {
2075 const TYPE: &str = "<http://www.w3.org/1999/02/22-rdf-syntax-ns#type>";
2076 let mut triples: Vec<(String, String, String)> = Vec::new();
2077 for i in 0..60 {
2079 let s = format!("S{i}");
2080 triples.push((s.clone(), TYPE.into(), "Student".into()));
2081 triples.push((s.clone(), "takesCourse".into(), format!("c{}", i % 5)));
2082 triples.push((s, "takesCourse".into(), format!("c{}", (i + 1) % 5)));
2083 }
2084 for j in 0..5 {
2086 triples.push((format!("c{j}"), TYPE.into(), "Course".into()));
2087 }
2088 triples.push(("Prof".into(), "teacherOf".into(), "c0".into()));
2089 triples.push(("Prof".into(), "teacherOf".into(), "c2".into()));
2090
2091 let refs: Vec<(&str, &str, &str)> = triples
2092 .iter()
2093 .map(|(s, p, o)| (s.as_str(), p.as_str(), o.as_str()))
2094 .collect();
2095 let bytes = rete_from(&refs);
2096 let rete = Rete::open(&bytes).unwrap();
2097
2098 let got: std::collections::BTreeSet<(String, String)> = eval_bgp(
2099 &rete,
2100 &[
2101 pat("?x", TYPE, "Student"),
2102 pat("?y", TYPE, "Course"),
2103 pat("?x", "takesCourse", "?y"),
2104 pat("Prof", "teacherOf", "?y"),
2105 ],
2106 )
2107 .into_iter()
2108 .map(|b| (b["x"].clone(), b["y"].clone()))
2109 .collect();
2110
2111 let taught = ["c0", "c2"];
2113 let mut want = std::collections::BTreeSet::new();
2114 for i in 0..60 {
2115 for c in [i % 5, (i + 1) % 5] {
2116 let course = format!("c{c}");
2117 if taught.contains(&course.as_str()) {
2118 want.insert((format!("S{i}"), course));
2119 }
2120 }
2121 }
2122 assert_eq!(got, want, "snowflake join must match the brute-force set");
2123 assert!(!want.is_empty(), "sanity: the reference set is non-empty");
2124 }
2125
2126 #[test]
2131 fn merge_join_subject_star_cross_product() {
2132 let bytes = rete_from(&[
2133 ("x", "a", "a1"),
2134 ("x", "a", "a2"),
2135 ("x", "b", "b1"),
2136 ("x", "b", "b2"),
2137 ("y", "a", "a3"),
2138 ("y", "b", "b3"),
2139 ("z", "a", "a4"), ]);
2141 let rete = Rete::open(&bytes).unwrap();
2142 let mut got: Vec<(String, String, String)> =
2143 eval_bgp(&rete, &[pat("?x", "a", "?av"), pat("?x", "b", "?bv")])
2144 .iter()
2145 .map(|m| (m["x"].clone(), m["av"].clone(), m["bv"].clone()))
2146 .collect();
2147 got.sort();
2148 let mut want: Vec<(String, String, String)> = [
2149 ("x", "a1", "b1"),
2150 ("x", "a1", "b2"),
2151 ("x", "a2", "b1"),
2152 ("x", "a2", "b2"),
2153 ("y", "a3", "b3"),
2154 ]
2155 .into_iter()
2156 .map(|(a, b, c)| (a.to_string(), b.to_string(), c.to_string()))
2157 .collect();
2158 want.sort();
2159 assert_eq!(got, want, "same-role merge must equal the brute-force join");
2160 }
2161}