1use std::collections::{BTreeMap, HashMap};
18
19use crate::file::Rete;
20use crate::index::{GraphIndex, Pattern};
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
419pub(crate) fn bgp_total_estimate(
427 ctx: &Ctx,
428 index: &GraphIndex,
429 patterns: &[TriplePattern],
430) -> Option<f64> {
431 let lowered = lower(patterns, ctx)?;
432 Some(
433 tile_estimates(ctx, index, &lowered, &std::collections::HashSet::new())
434 .into_iter()
435 .sum(),
436 )
437}
438
439fn is_unbound_class(ctx: &Ctx, t: &(SlotTerm, SlotTerm, SlotTerm), shared: &[usize]) -> bool {
442 let type_pid = ctx.rete.dictionary().predicate_id(crate::file::RDF_TYPE);
443 matches!(
444 t,
445 (SlotTerm::Var(x), SlotTerm::Pred(p), SlotTerm::Var(c))
446 if Some(*p) == type_pid && shared.contains(x) && !shared.contains(c) && x != c
447 )
448}
449
450fn fat_enough(est: f64) -> bool {
454 #[cfg(test)]
455 if TEST_LIMITS.with(|c| c.get()).is_some() {
456 return est >= limits().probe_floor;
457 }
458 est >= FAT_SCAN_ROWS
459}
460
461pub(crate) struct CorrelatedProbe {
468 plan: Option<ProbePlan>,
470 slots: Vec<usize>,
472}
473
474impl CorrelatedProbe {
475 pub(crate) fn new(
479 ctx: &Ctx,
480 index: &GraphIndex,
481 patterns: &[TriplePattern],
482 seed: &[bool],
483 ) -> Option<Self> {
484 if patterns.is_empty() {
485 return None;
486 }
487 let mut slots: Vec<usize> = Vec::new();
488 for p in patterns {
489 for t in [&p.s, &p.p, &p.o] {
490 if let PatternTerm::Var(v) = t {
491 if let Some(i) = ctx.slots.slot(v) {
492 if !slots.contains(&i) {
493 slots.push(i);
494 }
495 }
496 }
497 }
498 }
499 if !slots.iter().any(|&i| seed[i]) {
500 return None;
501 }
502 let Some(est) = bgp_min_estimate(ctx, index, patterns) else {
504 return Some(CorrelatedProbe { plan: None, slots });
505 };
506 if !fat_enough(est) {
507 return None;
508 }
509 Some(CorrelatedProbe {
510 plan: ProbePlan::new(ctx, index, patterns, seed),
511 slots,
512 })
513 }
514
515 pub(crate) fn shares(&self, row: &Row) -> bool {
517 self.slots.iter().any(|&i| row[i].is_some())
518 }
519
520 pub(crate) fn probe(&self, ctx: &Ctx, index: &GraphIndex, row: &Row) -> bool {
524 match &self.plan {
525 None => false,
526 Some(plan) => ProbeJoin::from_plan(ctx, index, plan, row.clone())
527 .next()
528 .is_some(),
529 }
530 }
531}
532
533#[cfg(test)]
536pub(crate) fn with_probe_floor<T>(floor: f64, f: impl FnOnce() -> T) -> T {
537 let saved = TEST_LIMITS.with(|c| c.get());
538 TEST_LIMITS.with(|c| {
539 c.set(Some(PlanLimits {
540 probe_floor: floor,
541 ..limits()
542 }))
543 });
544 let out = f();
545 TEST_LIMITS.with(|c| c.set(saved));
546 out
547}
548
549#[derive(Clone, Copy)]
552struct PlanLimits {
553 probe_floor: f64,
554 stream_seed: f64,
555 hash_build: f64,
556}
557
558#[cfg(test)]
559thread_local! {
560 static TEST_LIMITS: std::cell::Cell<Option<PlanLimits>> = const { std::cell::Cell::new(None) };
561}
562
563fn limits() -> PlanLimits {
564 #[cfg(test)]
565 if let Some(l) = TEST_LIMITS.with(|c| c.get()) {
566 return l;
567 }
568 PlanLimits {
569 probe_floor: PROBE_SCAN_FLOOR,
570 stream_seed: STREAM_SEED_ROWS,
571 hash_build: HASH_BUILD_ROWS,
572 }
573}
574
575const STREAM_SEED_ROWS: f64 = (1u64 << 21) as f64;
583
584const HASH_BUILD_ROWS: f64 = (1u64 << 20) as f64;
587
588fn tile_estimates(
594 ctx: &Ctx,
595 index: &GraphIndex,
596 lowered: &[(SlotTerm, SlotTerm, SlotTerm)],
597 seed: &std::collections::HashSet<usize>,
598) -> Vec<f64> {
599 const SEL_SUBJECT: f64 = 0.001;
600 const SEL_OBJECT: f64 = 0.02;
601 const SEL_PREDICATE: f64 = 0.05;
602 let dict = ctx.rete.dictionary();
603 let total = ctx.rete.index_triples(index);
604 let seeded = |x: &SlotTerm| matches!(x, SlotTerm::Var(v) if seed.contains(v));
605 let needs_stats = lowered
613 .iter()
614 .any(|t| matches!(t.1, SlotTerm::Pred(_)) && (seeded(&t.0) || seeded(&t.2)));
615 let stats: std::collections::HashMap<u32, &crate::meta::PredStat> = if needs_stats {
616 ctx.rete
617 .planner_stats(true)
618 .iter()
619 .map(|s| (s.predicate, s))
620 .collect()
621 } else {
622 std::collections::HashMap::new()
623 };
624 lowered
625 .iter()
626 .map(|t| {
627 let (Some(s), Some(p), Some(o)) = (
628 const_subject(&t.0, dict),
629 const_predicate(&t.1),
630 const_object(&t.2, dict),
631 ) else {
632 return 0.0;
633 };
634 let mut est = index.estimate_matches((s, p, o), total);
635 if est == 0.0 {
636 return 0.0;
637 }
638 let st = match t.1 {
639 SlotTerm::Pred(pid) => stats.get(&pid).copied(),
640 _ => None,
641 };
642 if seeded(&t.0) {
643 est *= match st {
644 Some(st) if st.distinct_subjects > 0 => 1.0 / st.distinct_subjects as f64,
645 _ => SEL_SUBJECT,
646 };
647 }
648 if seeded(&t.1) {
649 est *= SEL_PREDICATE;
650 }
651 if seeded(&t.2) {
652 est *= match st {
653 Some(st) if st.distinct_objects > 0 => 1.0 / st.distinct_objects as f64,
654 _ => SEL_OBJECT,
655 };
656 }
657 est.max(1.0)
658 })
659 .collect()
660}
661
662const REMOTE_PROBE_MIN: usize = 8;
671const REMOTE_PROBE_MAX: usize = 1024;
672
673fn remote_probe_max(index: &GraphIndex) -> usize {
680 REMOTE_PROBE_MAX * index.read_concurrency().clamp(1, 16)
681}
682
683pub(crate) fn prefetch_plan_probes(ctx: &Ctx, index: &GraphIndex, plan: &ProbePlan, rows: &[Row]) {
691 if !index.is_remote() {
692 return;
693 }
694 let Some(t) = plan.pats.first() else {
695 return;
696 };
697 let mut pats: Vec<Pattern> = Vec::new();
698 for base in rows {
699 if let (Some(s), Some(p), Some(o)) = (
700 probe_subject(ctx, &t.0, base),
701 probe_predicate(ctx, &t.1, base),
702 probe_object(ctx, &t.2, base),
703 ) {
704 if s.is_some() || o.is_some() {
705 pats.push((s, p, o));
706 }
707 }
708 }
709 index.prefetch_probe_tiles(&pats);
710}
711
712pub(crate) fn prefetch_subject_probes(
718 ctx: &Ctx,
719 index: &GraphIndex,
720 patterns: &[TriplePattern],
721 sv: &str,
722 subject_ids: &[u32],
723) {
724 if !index.is_remote() {
725 return;
726 }
727 let Some(lowered) = lower(patterns, ctx) else {
728 return;
729 };
730 let Some(slot) = ctx.slots.slot(sv) else {
731 return;
732 };
733 let dict = ctx.rete.dictionary();
734 let mut pats: Vec<Pattern> = Vec::new();
735 for &sid in subject_ids {
736 let mut base = ctx.slots.empty_row();
737 base[slot] = Some(Val::Id(dict.subject_node(sid) as i64));
738 for t in &lowered {
739 if !matches!(t.0, SlotTerm::Var(i) if i == slot) {
740 continue;
741 }
742 if let (Some(s), Some(p), Some(o)) = (
743 probe_subject(ctx, &t.0, &base),
744 probe_predicate(ctx, &t.1, &base),
745 probe_object(ctx, &t.2, &base),
746 ) {
747 pats.push((s, p, o));
748 }
749 }
750 }
751 index.prefetch_probe_tiles(&pats);
752}
753
754fn pattern_is_selective(t: &(SlotTerm, SlotTerm, SlotTerm)) -> bool {
757 matches!(t.0, SlotTerm::Node(_)) || matches!(t.2, SlotTerm::Node(_))
758}
759
760fn pattern_slots(t: &(SlotTerm, SlotTerm, SlotTerm)) -> Vec<usize> {
762 let mut slots: Vec<usize> = Vec::new();
763 for term in [&t.0, &t.1, &t.2] {
764 if let SlotTerm::Var(i) = term {
765 if !slots.contains(i) {
766 slots.push(*i);
767 }
768 }
769 }
770 slots
771}
772
773fn pred_val(ctx: &Ctx, slot: &SlotTerm, p_id: u32) -> i64 {
780 match slot {
781 SlotTerm::Var(_) => ctx.resolver.canon_id(pred_tag(p_id)),
782 _ => pred_tag(p_id),
783 }
784}
785
786fn triple_row(
790 ctx: &Ctx,
791 t: &(SlotTerm, SlotTerm, SlotTerm),
792 (s_id, p_id, o_id): (u32, u32, u32),
793) -> Option<Row> {
794 crate::guard::check();
796 let dict = ctx.rete.dictionary();
797 let s_val = dict.subject_node(s_id) as i64;
798 let p_val = pred_val(ctx, &t.1, p_id);
799 let o_val = dict.object_node(o_id) as i64;
800 let mut row = ctx.slots.empty_row();
801 for (term, val) in [(&t.0, s_val), (&t.1, p_val), (&t.2, o_val)] {
802 if let SlotTerm::Var(i) = term {
803 match row[*i] {
804 Some(Val::Id(existing)) if existing != val => return None,
805 Some(_) => {}
806 None => row[*i] = Some(Val::Id(val)),
807 }
808 }
809 }
810 Some(row)
811}
812
813fn scan_rows<'q>(
818 ctx: &'q Ctx<'q>,
819 index: &'q GraphIndex,
820 t: (SlotTerm, SlotTerm, SlotTerm),
821) -> Option<impl Iterator<Item = Row> + 'q> {
822 let dict = ctx.rete.dictionary();
823 let (sid, pid, oid) = (
824 const_subject(&t.0, dict)?,
825 const_predicate(&t.1)?,
826 const_object(&t.2, dict)?,
827 );
828 Some(
829 index
830 .scan_iter((sid, pid, oid))
831 .filter_map(move |triple| triple_row(ctx, &t, triple)),
832 )
833}
834
835fn pattern_rows(
838 ctx: &Ctx,
839 index: &GraphIndex,
840 t: &(SlotTerm, SlotTerm, SlotTerm),
841) -> Option<(Vec<Row>, Vec<usize>)> {
842 let slots = pattern_slots(t);
843 let mut rel: Vec<Row> = Vec::new();
844 let dict = ctx.rete.dictionary();
845 let (sid, pid, oid) = (
846 const_subject(&t.0, dict)?,
847 const_predicate(&t.1)?,
848 const_object(&t.2, dict)?,
849 );
850 for triple in index.scan_iter((sid, pid, oid)) {
853 if let Some(row) = triple_row(ctx, t, triple) {
854 rel.push(row);
855 }
856 }
857 Some((rel, slots))
858}
859
860fn pattern_estimates(
872 ctx: &Ctx,
873 index: &GraphIndex,
874 lowered: &[(SlotTerm, SlotTerm, SlotTerm)],
875 seed: &std::collections::HashSet<usize>,
876) -> Option<Vec<f64>> {
877 let pyr = ctx.rete.pyramid_if_loaded()?;
880 let mut pred: std::collections::HashMap<u32, u64> = std::collections::HashMap::new();
881 for e in &pyr.summary {
882 *pred.entry(e.predicate).or_insert(0) += e.count as u64;
883 }
884 let stats: std::collections::HashMap<u32, &crate::meta::PredStat> = ctx
891 .rete
892 .planner_stats(true)
893 .iter()
894 .map(|s| (s.predicate, s))
895 .collect();
896 let graph_total = ctx.rete.index_triples(index).max(1) as f64;
899 let total = graph_total;
900 let num_preds = pred.len().max(1) as f64;
901 const SEL_SUBJECT: f64 = 0.001;
903 const SEL_OBJECT: f64 = 0.02;
904 let node_bound = |t: &SlotTerm| match t {
905 SlotTerm::Node(_) => true,
906 SlotTerm::Var(v) => seed.contains(v),
907 SlotTerm::Pred(_) => false,
908 };
909 Some(
910 lowered
911 .iter()
912 .map(|t| {
913 let (base, st) = match t.1 {
918 SlotTerm::Pred(p) => {
919 (*pred.get(&p).unwrap_or(&0) as f64, stats.get(&p).copied())
920 }
921 SlotTerm::Var(v) if seed.contains(&v) => (total / num_preds, None),
922 _ => (total, None),
923 };
924 let mut est = base.min(graph_total).max(1.0);
925 if node_bound(&t.0) {
926 est *= match st {
927 Some(s) if s.distinct_subjects > 0 => 1.0 / s.distinct_subjects as f64,
928 _ => SEL_SUBJECT,
929 };
930 }
931 if node_bound(&t.2) {
932 est *= match st {
933 Some(s) if s.distinct_objects > 0 => 1.0 / s.distinct_objects as f64,
934 _ => SEL_OBJECT,
935 };
936 }
937 est.max(1.0)
938 })
939 .collect(),
940 )
941}
942
943pub(crate) fn bgp_min_scan_bytes(
950 ctx: &Ctx,
951 index: &GraphIndex,
952 patterns: &[TriplePattern],
953) -> Option<u64> {
954 let lowered = lower(patterns, ctx)?;
955 lowered
956 .iter()
957 .map(|t| pattern_scan_bytes(ctx, index, t))
958 .min()
959}
960
961fn pattern_scan_bytes(ctx: &Ctx, index: &GraphIndex, t: &(SlotTerm, SlotTerm, SlotTerm)) -> u64 {
978 let dict = ctx.rete.dictionary();
979 match (
980 const_subject(&t.0, dict),
981 const_predicate(&t.1),
982 const_object(&t.2, dict),
983 ) {
984 (Some(s), Some(p), Some(o)) => routed_scan_bytes(index, (s, p, o)),
985 _ => 0,
986 }
987}
988
989pub(crate) fn routed_scan_bytes(index: &GraphIndex, pattern: Pattern) -> u64 {
991 let sections = index.tile_sections();
992 let mut best = u64::MAX;
993 for perm in index.perms().iter() {
997 let tiles = sections[perm.section_index()];
998 best = best.min(match perm.order_pattern(pattern)[0] {
1001 Some(id) => tiles.routed_bytes(id),
1002 None => tiles.total_bytes(),
1003 });
1004 }
1005 best
1006}
1007
1008pub(crate) const FAT_SCAN_ROWS: f64 = (1u64 << 20) as f64;
1021
1022fn merge_seed_lopsided(
1027 ctx: &Ctx,
1028 index: &GraphIndex,
1029 ta: &(SlotTerm, SlotTerm, SlotTerm),
1030 tb: &(SlotTerm, SlotTerm, SlotTerm),
1031) -> bool {
1032 let est = tile_estimates(ctx, index, &[*ta, *tb], &std::collections::HashSet::new());
1033 let (lo, hi) = (est[0].min(est[1]), est[0].max(est[1]));
1034 hi >= FAT_SCAN_ROWS && hi / 4.0 >= lo
1035}
1036
1037fn selectivity_order(
1044 ctx: &Ctx,
1045 index: &GraphIndex,
1046 lowered: &[(SlotTerm, SlotTerm, SlotTerm)],
1047) -> Vec<usize> {
1048 selectivity_order_seeded(ctx, index, lowered, &std::collections::HashSet::new())
1049}
1050
1051fn selectivity_order_seeded(
1055 ctx: &Ctx,
1056 index: &GraphIndex,
1057 lowered: &[(SlotTerm, SlotTerm, SlotTerm)],
1058 seed: &std::collections::HashSet<usize>,
1059) -> Vec<usize> {
1060 let (estimates, per_class) = match pattern_estimates(ctx, index, lowered, seed) {
1068 Some(e) => (Some(e), false),
1069 None => (Some(tile_estimates(ctx, index, lowered, seed)), true),
1070 };
1071 let consts = |t: &(SlotTerm, SlotTerm, SlotTerm)| {
1072 [&t.0, &t.1, &t.2]
1073 .into_iter()
1074 .filter(|x| match x {
1075 SlotTerm::Var(v) => seed.contains(v),
1076 _ => true,
1077 })
1078 .count()
1079 };
1080 let vars = |t: &(SlotTerm, SlotTerm, SlotTerm)| -> Vec<usize> {
1081 [&t.0, &t.1, &t.2]
1082 .into_iter()
1083 .filter_map(|x| match x {
1084 SlotTerm::Var(v) => Some(*v),
1085 _ => None,
1086 })
1087 .collect()
1088 };
1089 let score = |i: usize| -> f64 {
1093 match &estimates {
1094 Some(e) => -e[i],
1095 None => consts(&lowered[i]) as f64,
1096 }
1097 };
1098 let type_pid = ctx.rete.dictionary().predicate_id(crate::file::RDF_TYPE);
1106 let is_class_enum = |i: usize, bound: &std::collections::HashSet<usize>| -> bool {
1107 let t = &lowered[i];
1108 !per_class
1109 && type_pid.is_some_and(|tp| matches!(t.1, SlotTerm::Pred(p) if p == tp))
1110 && matches!(t.2, SlotTerm::Node(_))
1111 && matches!(t.0, SlotTerm::Var(v) if !bound.contains(&v))
1112 };
1113 let n = lowered.len();
1114 let mut remaining: Vec<usize> = (0..n).collect();
1115 let mut order: Vec<usize> = Vec::with_capacity(n);
1116 let mut bound: std::collections::HashSet<usize> = seed.clone();
1117 while !remaining.is_empty() {
1118 let best = *remaining
1121 .iter()
1122 .max_by(|&&a, &&b| {
1123 let connected = |i: usize| vars(&lowered[i]).iter().any(|v| bound.contains(v));
1124 connected(a)
1125 .cmp(&connected(b))
1126 .then_with(|| is_class_enum(b, &bound).cmp(&is_class_enum(a, &bound)))
1127 .then_with(|| {
1128 score(a)
1129 .partial_cmp(&score(b))
1130 .unwrap_or(std::cmp::Ordering::Equal)
1131 })
1132 .then_with(|| b.cmp(&a))
1133 })
1134 .unwrap();
1135 for v in vars(&lowered[best]) {
1136 bound.insert(v);
1137 }
1138 order.push(best);
1139 remaining.retain(|&i| i != best);
1140 }
1141 order
1142}
1143
1144pub(crate) fn bgp_exists(ctx: &Ctx, index: &GraphIndex, patterns: &[TriplePattern]) -> bool {
1150 let dict = ctx.rete.dictionary();
1151 let Some(lowered) = lower(patterns, ctx) else {
1152 return false;
1153 };
1154 if let [t] = lowered.as_slice() {
1155 let names: Vec<usize> = [&t.0, &t.1, &t.2]
1159 .into_iter()
1160 .filter_map(|x| match x {
1161 SlotTerm::Var(v) => Some(*v),
1162 _ => None,
1163 })
1164 .collect();
1165 let distinct = names
1166 .iter()
1167 .enumerate()
1168 .all(|(i, v)| !names[i + 1..].contains(v));
1169 if distinct {
1170 return match (
1171 const_subject(&t.0, dict),
1172 const_predicate(&t.1),
1173 const_object(&t.2, dict),
1174 ) {
1175 (Some(s), Some(p), Some(o)) => index.scan_iter((s, p, o)).next().is_some(),
1176 _ => false,
1177 };
1178 }
1179 }
1180 !eval_bgp_rows(ctx, index, patterns).is_empty()
1181}
1182
1183pub(crate) struct BgpSolutions<'q> {
1194 scan: Option<Box<dyn Iterator<Item = Row> + 'q>>,
1195 prefix: Vec<Row>,
1196 buckets: HashMap<Vec<Val>, Vec<usize>>,
1198 shared: Vec<usize>,
1199 cartesian: bool,
1200 seed_only: bool,
1203 cur_scan: Option<Row>,
1204 matches: Vec<usize>,
1206 mi: usize,
1207}
1208
1209impl<'q> BgpSolutions<'q> {
1210 fn passthrough(rows: Box<dyn Iterator<Item = Row> + 'q>) -> Self {
1212 BgpSolutions {
1213 scan: Some(rows),
1214 prefix: Vec::new(),
1215 buckets: HashMap::new(),
1216 shared: Vec::new(),
1217 cartesian: false,
1218 seed_only: true,
1219 cur_scan: None,
1220 matches: Vec::new(),
1221 mi: 0,
1222 }
1223 }
1224
1225 fn streaming(
1234 ctx: &'q Ctx<'q>,
1235 index: &'q GraphIndex,
1236 lowered: &[(SlotTerm, SlotTerm, SlotTerm)],
1237 order: &[usize],
1238 est: &[f64],
1239 ) -> Self {
1240 let (mut stream, mut bound, skip): (Box<dyn Iterator<Item = Row> + 'q>, Vec<usize>, usize) =
1241 match (est[order[1]] > limits().stream_seed)
1242 .then(|| merge_stream(ctx, index, lowered[order[0]], lowered[order[1]]))
1243 .flatten()
1244 {
1245 Some((m, slots)) => (m, slots, 2),
1246 None => {
1247 let t = lowered[order[0]];
1248 let Some(scan) = scan_rows(ctx, index, t) else {
1249 return Self::empty();
1250 };
1251 (Box::new(scan), pattern_slots(&t), 1)
1252 }
1253 };
1254 for &i in &order[skip..] {
1255 let t = lowered[i];
1256 let slots = pattern_slots(&t);
1257 let shared: Vec<usize> = slots
1258 .iter()
1259 .copied()
1260 .filter(|s| bound.contains(s))
1261 .collect();
1262 if !shared.is_empty() && est[i] > limits().hash_build {
1263 stream = Box::new(stream.flat_map(move |base| probe_rows(ctx, index, t, base)));
1264 } else {
1265 let Some((rel, _)) = pattern_rows(ctx, index, &t) else {
1268 return Self::empty();
1269 };
1270 let mut table: HashMap<Vec<Val>, Vec<Row>> = HashMap::new();
1271 for r in rel {
1272 let key: Vec<Val> = shared.iter().map(|&s| r[s].clone().unwrap()).collect();
1273 table.entry(key).or_default().push(r);
1274 }
1275 let fill = slots.clone();
1276 stream = Box::new(stream.flat_map(move |base| {
1277 let key: Vec<Val> = shared.iter().map(|&s| base[s].clone().unwrap()).collect();
1278 let out: Vec<Row> = table
1279 .get(&key)
1280 .map(|rs| {
1281 rs.iter()
1282 .map(|r| {
1283 let mut row = base.clone();
1284 for &s in &fill {
1285 row[s] = r[s].clone();
1286 }
1287 row
1288 })
1289 .collect()
1290 })
1291 .unwrap_or_default();
1292 out.into_iter()
1293 }));
1294 }
1295 for s in slots {
1296 if !bound.contains(&s) {
1297 bound.push(s);
1298 }
1299 }
1300 }
1301 Self::passthrough(stream)
1302 }
1303
1304 fn empty() -> Self {
1306 BgpSolutions {
1307 scan: None,
1308 prefix: Vec::new(),
1309 buckets: HashMap::new(),
1310 shared: Vec::new(),
1311 cartesian: false,
1312 seed_only: false,
1313 cur_scan: None,
1314 matches: Vec::new(),
1315 mi: 0,
1316 }
1317 }
1318
1319 pub(crate) fn new(ctx: &'q Ctx<'q>, index: &'q GraphIndex, patterns: &[TriplePattern]) -> Self {
1320 if patterns.is_empty() {
1322 return BgpSolutions {
1323 scan: Some(Box::new(std::iter::once(ctx.slots.empty_row()))),
1324 prefix: vec![ctx.slots.empty_row()],
1325 buckets: HashMap::new(),
1326 shared: Vec::new(),
1327 cartesian: true,
1328 seed_only: true,
1329 cur_scan: None,
1330 matches: Vec::new(),
1331 mi: 0,
1332 };
1333 }
1334 let Some(lowered) = lower(patterns, ctx) else {
1336 return Self::empty();
1337 };
1338 let order = selectivity_order(ctx, index, &lowered);
1341 let scan_est = tile_estimates(ctx, index, &lowered, &std::collections::HashSet::new());
1342 if order.len() >= 2 && scan_est[order[0]] > limits().stream_seed {
1345 return Self::streaming(ctx, index, &lowered, &order, &scan_est);
1346 }
1347 let (&last_i, prefix_is) = order.split_last().unwrap();
1348 let prefix_pats: Vec<TriplePattern> =
1349 prefix_is.iter().map(|&i| patterns[i].clone()).collect();
1350 let prefix = eval_bgp_rows(ctx, index, &prefix_pats);
1351 if prefix.is_empty() {
1352 return Self::empty();
1353 }
1354
1355 let shared: Vec<usize> = pattern_slots(&lowered[last_i])
1359 .into_iter()
1360 .filter(|&s| prefix[0][s].is_some())
1361 .collect();
1362
1363 let class_probe = !shared.is_empty()
1372 && scan_est[last_i] >= limits().probe_floor
1373 && (prefix.len() as f64) <= scan_est[last_i]
1374 && is_unbound_class(ctx, &lowered[last_i], &shared);
1375 if !shared.is_empty() && (probe_beats_scan(prefix.len(), scan_est[last_i]) || class_probe) {
1376 let t = lowered[last_i];
1377 if index.is_remote() {
1378 prefetch_rows_for(ctx, index, t, &prefix);
1379 }
1380 let stream = prefix
1381 .into_iter()
1382 .flat_map(move |base| probe_rows(ctx, index, t, base));
1383 return Self::passthrough(Box::new(stream));
1384 }
1385
1386 let Some(scan) = scan_rows(ctx, index, lowered[last_i]) else {
1388 return Self::empty();
1389 };
1390 let cartesian = shared.is_empty();
1391 let seed_only = prefix.len() == 1 && prefix[0].iter().all(Option::is_none);
1394 let mut buckets: HashMap<Vec<Val>, Vec<usize>> = HashMap::new();
1395 if !cartesian {
1396 for (i, r) in prefix.iter().enumerate() {
1397 let key: Vec<Val> = shared.iter().map(|&s| r[s].clone().unwrap()).collect();
1398 buckets.entry(key).or_default().push(i);
1399 }
1400 }
1401 BgpSolutions {
1402 scan: Some(Box::new(scan)),
1403 prefix,
1404 buckets,
1405 shared,
1406 cartesian,
1407 seed_only,
1408 cur_scan: None,
1409 matches: Vec::new(),
1410 mi: 0,
1411 }
1412 }
1413}
1414
1415impl Iterator for BgpSolutions<'_> {
1416 type Item = Row;
1417
1418 fn next(&mut self) -> Option<Row> {
1419 if self.seed_only {
1421 return self.scan.as_mut()?.next();
1422 }
1423 loop {
1424 if self.mi < self.matches.len() {
1426 let pi = self.matches[self.mi];
1427 self.mi += 1;
1428 let mut merged = self.prefix[pi].clone();
1429 for (slot, v) in self.cur_scan.as_ref().unwrap().iter().enumerate() {
1430 if v.is_some() {
1431 merged[slot] = v.clone();
1432 }
1433 }
1434 return Some(merged);
1435 }
1436 let s = self.scan.as_mut()?.next()?;
1439 self.matches.clear();
1440 if self.cartesian {
1441 self.matches.extend(0..self.prefix.len());
1442 } else {
1443 let key: Vec<Val> = self.shared.iter().map(|&i| s[i].clone().unwrap()).collect();
1444 if let Some(c) = self.buckets.get(&key) {
1445 self.matches.extend_from_slice(c);
1446 }
1447 }
1448 self.mi = 0;
1449 self.cur_scan = Some(s);
1450 }
1451 }
1452}
1453
1454fn prefetch_rows_for(
1457 ctx: &Ctx,
1458 index: &GraphIndex,
1459 t: (SlotTerm, SlotTerm, SlotTerm),
1460 rows: &[Row],
1461) {
1462 let pats: Vec<Pattern> = rows
1463 .iter()
1464 .filter_map(|base| {
1465 match (
1466 probe_subject(ctx, &t.0, base),
1467 probe_predicate(ctx, &t.1, base),
1468 probe_object(ctx, &t.2, base),
1469 ) {
1470 (Some(s), Some(p), Some(o)) => Some((s, p, o)),
1471 _ => None,
1472 }
1473 })
1474 .collect();
1475 index.prefetch_probe_tiles(&pats);
1476}
1477
1478#[allow(clippy::type_complexity)]
1486fn merge_stream<'q>(
1487 ctx: &'q Ctx<'q>,
1488 index: &'q GraphIndex,
1489 ta: (SlotTerm, SlotTerm, SlotTerm),
1490 tb: (SlotTerm, SlotTerm, SlotTerm),
1491) -> Option<(Box<dyn Iterator<Item = Row> + 'q>, Vec<usize>)> {
1492 let sa = pattern_slots(&ta);
1493 let sb = pattern_slots(&tb);
1494 let shared: Vec<usize> = sa.iter().copied().filter(|s| sb.contains(s)).collect();
1495 let [v] = shared[..] else {
1496 return None;
1497 };
1498 if !matches!(ta.0, SlotTerm::Var(i) if i == v) || !matches!(tb.0, SlotTerm::Var(i) if i == v) {
1499 return None;
1500 }
1501 let dict = ctx.rete.dictionary();
1502 let lower = |t: &(SlotTerm, SlotTerm, SlotTerm)| -> Option<Pattern> {
1503 Some((
1504 const_subject(&t.0, dict)?,
1505 const_predicate(&t.1)?,
1506 const_object(&t.2, dict)?,
1507 ))
1508 };
1509 let side = |t: (SlotTerm, SlotTerm, SlotTerm)| -> Option<KeyedRows<'q>> {
1510 let it = index.scan_iter_sorted_on(lower(&t)?, 0)?;
1511 let keyed: Box<dyn Iterator<Item = (u32, Row)> + 'q> =
1512 Box::new(it.filter_map(move |tri| triple_row(ctx, &t, tri).map(|r| (tri.0, r))));
1513 Some(keyed.peekable())
1514 };
1515 let a = side(ta)?;
1516 let b = side(tb)?;
1517 let mut slots = sa;
1518 for s in sb {
1519 if !slots.contains(&s) {
1520 slots.push(s);
1521 }
1522 }
1523 Some((
1524 Box::new(MergeStream {
1525 a,
1526 b,
1527 ra: Vec::new(),
1528 rb: Vec::new(),
1529 i: 0,
1530 j: 0,
1531 }),
1532 slots,
1533 ))
1534}
1535
1536type KeyedRows<'q> = std::iter::Peekable<Box<dyn Iterator<Item = (u32, Row)> + 'q>>;
1537
1538struct MergeStream<'q> {
1541 a: KeyedRows<'q>,
1542 b: KeyedRows<'q>,
1543 ra: Vec<Row>,
1544 rb: Vec<Row>,
1545 i: usize,
1546 j: usize,
1547}
1548
1549impl Iterator for MergeStream<'_> {
1550 type Item = Row;
1551
1552 fn next(&mut self) -> Option<Row> {
1553 loop {
1554 while self.i < self.ra.len() {
1555 if self.j < self.rb.len() {
1556 let j = self.j;
1557 self.j += 1;
1558 if let Some(m) = crate::row::merge_rows(&self.ra[self.i], &self.rb[j]) {
1559 return Some(m);
1560 }
1561 } else {
1562 self.i += 1;
1563 self.j = 0;
1564 }
1565 }
1566 let ka = self.a.peek()?.0;
1567 let kb = self.b.peek()?.0;
1568 match ka.cmp(&kb) {
1569 std::cmp::Ordering::Less => {
1570 self.a.next();
1571 }
1572 std::cmp::Ordering::Greater => {
1573 self.b.next();
1574 }
1575 std::cmp::Ordering::Equal => {
1576 self.ra.clear();
1577 self.rb.clear();
1578 while let Some((_, r)) = self.a.next_if(|(k, _)| *k == ka) {
1579 self.ra.push(r);
1580 }
1581 while let Some((_, r)) = self.b.next_if(|(k, _)| *k == ka) {
1582 self.rb.push(r);
1583 }
1584 self.i = 0;
1585 self.j = 0;
1586 }
1587 }
1588 }
1589 }
1590}
1591
1592fn probe_subject(ctx: &Ctx, t: &SlotTerm, base: &Row) -> Option<Option<u32>> {
1606 let dict = ctx.rete.dictionary();
1607 match t {
1608 SlotTerm::Node(n) => dict.node_as_subject_id(*n).map(Some),
1609 SlotTerm::Pred(_) => None,
1610 SlotTerm::Var(i) => match &base[*i] {
1611 None => Some(None),
1612 Some(Val::Id(v)) if *v >= 0 => dict.node_as_subject_id(*v as u32).map(Some),
1613 Some(_) => None,
1615 },
1616 }
1617}
1618
1619fn probe_object(ctx: &Ctx, t: &SlotTerm, base: &Row) -> Option<Option<u32>> {
1621 let dict = ctx.rete.dictionary();
1622 match t {
1623 SlotTerm::Node(n) => dict.node_as_object_id(*n).map(Some),
1624 SlotTerm::Pred(_) => None,
1625 SlotTerm::Var(i) => match &base[*i] {
1626 None => Some(None),
1627 Some(Val::Id(v)) if *v >= 0 => dict.node_as_object_id(*v as u32).map(Some),
1628 Some(_) => None,
1629 },
1630 }
1631}
1632
1633fn probe_predicate(ctx: &Ctx, t: &SlotTerm, base: &Row) -> Option<Option<u32>> {
1635 let dict = ctx.rete.dictionary();
1636 match t {
1637 SlotTerm::Pred(p) => Some(Some(*p)),
1638 SlotTerm::Node(_) => None,
1639 SlotTerm::Var(i) => match &base[*i] {
1640 None => Some(None),
1641 Some(Val::Id(v)) if *v < 0 => Some(Some((-v - 1) as u32)),
1642 Some(Val::Id(v)) => ctx
1644 .resolver
1645 .term(*v)
1646 .and_then(|t| dict.predicate_id(&t))
1647 .map(Some),
1648 Some(Val::Str(_)) => None,
1649 },
1650 }
1651}
1652
1653fn probe_rows<'q>(
1657 ctx: &'q Ctx<'q>,
1658 index: &'q GraphIndex,
1659 t: (SlotTerm, SlotTerm, SlotTerm),
1660 base: Row,
1661) -> Box<dyn Iterator<Item = Row> + 'q> {
1662 let (Some(sid), Some(pid), Some(oid)) = (
1663 probe_subject(ctx, &t.0, &base),
1664 probe_predicate(ctx, &t.1, &base),
1665 probe_object(ctx, &t.2, &base),
1666 ) else {
1667 return Box::new(std::iter::empty());
1668 };
1669 let dict = ctx.rete.dictionary();
1670 Box::new(
1671 index
1672 .scan_iter((sid, pid, oid))
1673 .filter_map(move |(s_id, p_id, o_id)| {
1674 let s_val = dict.subject_node(s_id) as i64;
1675 let p_val = pred_val(ctx, &t.1, p_id);
1676 let o_val = dict.object_node(o_id) as i64;
1677 let mut row = base.clone();
1678 for (term, val) in [(&t.0, s_val), (&t.1, p_val), (&t.2, o_val)] {
1679 if let SlotTerm::Var(i) = term {
1680 match row[*i] {
1681 Some(Val::Id(existing)) if existing != val => return None,
1682 Some(Val::Id(_)) => {}
1683 Some(Val::Str(_)) => return None,
1684 None => row[*i] = Some(Val::Id(val)),
1685 }
1686 }
1687 }
1688 Some(row)
1689 }),
1690 )
1691}
1692
1693pub(crate) struct ProbePlan {
1695 pats: Vec<(SlotTerm, SlotTerm, SlotTerm)>,
1696}
1697
1698impl ProbePlan {
1699 pub(crate) fn new(
1703 ctx: &Ctx,
1704 index: &GraphIndex,
1705 patterns: &[TriplePattern],
1706 seed_mask: &[bool],
1707 ) -> Option<Self> {
1708 let lowered = lower(patterns, ctx)?;
1709 let seed: std::collections::HashSet<usize> = seed_mask
1710 .iter()
1711 .enumerate()
1712 .filter_map(|(i, b)| b.then_some(i))
1713 .collect();
1714 let order = selectivity_order_seeded(ctx, index, &lowered, &seed);
1715 Some(ProbePlan {
1716 pats: order.into_iter().map(|i| lowered[i]).collect(),
1717 })
1718 }
1719}
1720
1721pub(crate) struct ProbeJoin<'q> {
1724 ctx: &'q Ctx<'q>,
1725 index: &'q GraphIndex,
1726 pats: Vec<(SlotTerm, SlotTerm, SlotTerm)>,
1727 stack: Vec<Box<dyn Iterator<Item = Row> + 'q>>,
1728}
1729
1730impl<'q> ProbeJoin<'q> {
1731 pub(crate) fn new(
1734 ctx: &'q Ctx<'q>,
1735 index: &'q GraphIndex,
1736 patterns: &[TriplePattern],
1737 ) -> Option<Self> {
1738 let plan = ProbePlan::new(ctx, index, patterns, &vec![false; ctx.slots.len()])?;
1739 Some(Self::from_plan(ctx, index, &plan, ctx.slots.empty_row()))
1740 }
1741
1742 pub(crate) fn from_plan(
1744 ctx: &'q Ctx<'q>,
1745 index: &'q GraphIndex,
1746 plan: &ProbePlan,
1747 seed: Row,
1748 ) -> Self {
1749 let pats = plan.pats.clone();
1750 let first = probe_rows(ctx, index, pats[0], seed);
1751 ProbeJoin {
1752 ctx,
1753 index,
1754 pats,
1755 stack: vec![first],
1756 }
1757 }
1758}
1759
1760impl Iterator for ProbeJoin<'_> {
1761 type Item = Row;
1762
1763 fn next(&mut self) -> Option<Row> {
1764 loop {
1765 let depth = self.stack.len();
1766 match self.stack.last_mut()?.next() {
1767 Some(row) => {
1768 if depth == self.pats.len() {
1769 return Some(row);
1770 }
1771 let it = probe_rows(self.ctx, self.index, self.pats[depth], row);
1772 self.stack.push(it);
1773 }
1774 None => {
1775 self.stack.pop();
1776 if self.stack.is_empty() {
1777 return None;
1778 }
1779 }
1780 }
1781 }
1782 }
1783}
1784
1785fn hash_join(
1789 left: Vec<Row>,
1790 left_bound: &[usize],
1791 right: Vec<Row>,
1792 right_slots: &[usize],
1793) -> Vec<Row> {
1794 if left.len() == 1 && left_bound.is_empty() {
1796 return right;
1797 }
1798 if left.is_empty() || right.is_empty() {
1799 return Vec::new();
1800 }
1801 let shared: Vec<usize> = right_slots
1802 .iter()
1803 .copied()
1804 .filter(|s| left_bound.contains(s))
1805 .collect();
1806 let fill = |l: &Row, r: &Row| -> Row {
1807 let mut out = l.clone();
1808 for &s in right_slots {
1809 out[s] = r[s].clone();
1810 }
1811 out
1812 };
1813 if shared.is_empty() {
1814 let mut out = Vec::with_capacity(left.len() * right.len());
1818 for l in &left {
1819 for r in &right {
1820 out.push(fill(l, r));
1821 }
1822 }
1823 return out;
1824 }
1825 let key_of = |b: &Row| -> Vec<Val> { shared.iter().map(|&s| b[s].clone().unwrap()).collect() };
1826 let mut out = Vec::new();
1833 if right.len() <= left.len() {
1834 let mut buckets: HashMap<Vec<Val>, Vec<Row>> = HashMap::new();
1835 for r in right {
1836 buckets.entry(key_of(&r)).or_default().push(r);
1837 }
1838 for l in &left {
1839 if let Some(rs) = buckets.get(&key_of(l)) {
1840 for r in rs {
1841 out.push(fill(l, r));
1842 }
1843 }
1844 }
1845 } else {
1846 let mut buckets: HashMap<Vec<Val>, Vec<Row>> = HashMap::new();
1848 for l in left {
1849 buckets.entry(key_of(&l)).or_default().push(l);
1850 }
1851 for r in &right {
1852 if let Some(ls) = buckets.get(&key_of(r)) {
1853 for l in ls {
1854 let mut row = r.clone();
1855 for &s in left_bound {
1856 if let Some(v) = &l[s] {
1857 row[s] = Some(v.clone());
1858 }
1859 }
1860 out.push(row);
1861 }
1862 }
1863 }
1864 }
1865 out
1866}
1867
1868fn const_subject(t: &SlotTerm, d: &crate::Dictionary) -> Option<Option<u32>> {
1873 match t {
1874 SlotTerm::Node(n) => d.node_as_subject_id(*n).map(Some),
1875 SlotTerm::Pred(_) => None, SlotTerm::Var(_) => Some(None),
1877 }
1878}
1879
1880fn const_object(t: &SlotTerm, d: &crate::Dictionary) -> Option<Option<u32>> {
1881 match t {
1882 SlotTerm::Node(n) => d.node_as_object_id(*n).map(Some),
1883 SlotTerm::Pred(_) => None,
1884 SlotTerm::Var(_) => Some(None),
1885 }
1886}
1887
1888fn const_predicate(t: &SlotTerm) -> Option<Option<u32>> {
1889 match t {
1890 SlotTerm::Pred(p) => Some(Some(*p)),
1891 SlotTerm::Node(_) => None, SlotTerm::Var(_) => Some(None),
1893 }
1894}
1895
1896#[cfg(test)]
1897mod tests {
1898 use super::*;
1899 use crate::dictionary::DictionaryBuilder;
1900 use crate::file::write_file_v05 as write_file;
1901 use crate::index::GraphIndexBuilder;
1902
1903 fn rete_from(triples: &[(&str, &str, &str)]) -> Vec<u8> {
1904 let mut db = DictionaryBuilder::new();
1905 for (s, p, o) in triples {
1906 db.observe(s, p, o);
1907 }
1908 let dict = db.build();
1909 let mut ib = GraphIndexBuilder::new();
1910 for (s, p, o) in triples {
1911 ib.push(dict.encode(s, p, o).unwrap());
1912 }
1913 write_file(&dict, &ib.build(), false, &[], 0)
1914 }
1915
1916 fn pat(s: &str, p: &str, o: &str) -> TriplePattern {
1917 TriplePattern {
1918 s: PatternTerm::parse(s),
1919 p: PatternTerm::parse(p),
1920 o: PatternTerm::parse(o),
1921 }
1922 }
1923
1924 #[test]
1933 fn scan_bytes_route_a_literal_by_its_object_id() {
1934 let value = "<http://ex/value>";
1935 let owned: Vec<(String, String)> = (0..400)
1936 .map(|i| (format!("<http://ex/s{i:03}>"), format!("\"lit {i:03}\"")))
1937 .collect();
1938 let mut db = DictionaryBuilder::new();
1939 for (s, o) in &owned {
1940 db.observe(s, value, o);
1941 }
1942 let dict = db.build();
1943 let mut ib = GraphIndexBuilder::new().with_tile_budget(256);
1944 for (s, o) in &owned {
1945 ib.push(dict.encode(s, value, o).unwrap());
1946 }
1947 let rete = Rete::open(&write_file(&dict, &ib.build(), false, &[], 0)).unwrap();
1948 let index = rete.default_index();
1949 let whole = routed_scan_bytes(index, (None, rete.dictionary().predicate_id(value), None));
1951 for (_, lit) in owned.iter().step_by(57) {
1952 let q = [pat("?s", value, lit)];
1953 let mut slots = Slots::new();
1954 collect_pattern_slots(&q, &mut slots);
1955 let ctx = Ctx::new(&rete, slots);
1956 let got = bgp_min_scan_bytes(&ctx, index, &q).unwrap();
1957 assert!(got > 0, "{lit} routed to no tile");
1958 assert!(
1959 got < whole,
1960 "{lit} routed to the whole predicate ({got} of {whole})"
1961 );
1962 }
1963 }
1964
1965 #[test]
1971 fn plan_gates_do_not_move_with_the_compression() {
1972 let mut owned: Vec<(String, String, String)> = Vec::new();
1973 for i in 0..3000 {
1974 let s = format!("<http://ex/s{i}>");
1975 owned.push((
1976 s.clone(),
1977 "<http://ex/kind>".into(),
1978 format!("<http://ex/K{}>", i % 7),
1979 ));
1980 owned.push((s.clone(), "<http://ex/value>".into(), format!("\"v {i}\"")));
1981 if i % 50 == 0 {
1982 owned.push((s, "<http://ex/rare>".into(), "<http://ex/R>".into()));
1983 }
1984 }
1985 let mut db = DictionaryBuilder::new();
1986 for (s, p, o) in &owned {
1987 db.observe(s, p, o);
1988 }
1989 let dict = db.build();
1990 let mut ib = GraphIndexBuilder::new().with_tile_budget(1024);
1991 for (s, p, o) in &owned {
1992 ib.push(dict.encode(s, p, o).unwrap());
1993 }
1994 let bytes = write_file(&dict, &ib.build(), false, &[], 0);
1995 let leaked: &'static [u8] = Box::leak(bytes.clone().into_boxed_slice());
1996 let resident = Rete::open(&bytes).unwrap();
1997 let lazy = Rete::open_ranged_lazy(crate::reader::SliceReader::new(leaked)).unwrap();
1998 let shapes: Vec<Vec<TriplePattern>> = vec![
1999 vec![
2000 pat("?s", "<http://ex/rare>", "<http://ex/R>"),
2001 pat("?s", "<http://ex/value>", "?v"),
2002 ],
2003 vec![
2004 pat("?s", "<http://ex/kind>", "<http://ex/K3>"),
2005 pat("?s", "<http://ex/value>", "?v"),
2006 ],
2007 vec![
2008 pat("?s", "<http://ex/kind>", "?k"),
2009 pat("?s", "<http://ex/value>", "?v"),
2010 ],
2011 vec![
2012 pat("?s", "<http://ex/value>", "\"v 7\""),
2013 pat("?s", "<http://ex/kind>", "?k"),
2014 ],
2015 ];
2016 let decisions = |rete: &Rete| -> Vec<(Vec<u64>, bool, Option<u64>)> {
2017 shapes
2018 .iter()
2019 .map(|q| {
2020 let mut slots = Slots::new();
2021 collect_pattern_slots(q, &mut slots);
2022 let ctx = Ctx::new(rete, slots);
2023 let index = rete.default_index();
2024 let lowered = lower(q, &ctx).unwrap();
2025 let est = tile_estimates(&ctx, index, &lowered, &Default::default());
2026 let lopsided = merge_seed_lopsided(&ctx, index, &lowered[0], &lowered[1]);
2027 let min = bgp_min_estimate(&ctx, index, q).map(|e| e as u64);
2028 (est.iter().map(|e| *e as u64).collect(), lopsided, min)
2029 })
2030 .collect()
2031 };
2032 assert_eq!(decisions(&resident), decisions(&lazy));
2033 #[cfg(feature = "compression")]
2035 {
2036 let bytes_of = |rete: &Rete| {
2037 let q = &shapes[2];
2038 let mut slots = Slots::new();
2039 collect_pattern_slots(q, &mut slots);
2040 let ctx = Ctx::new(rete, slots);
2041 bgp_min_scan_bytes(&ctx, rete.default_index(), q)
2042 };
2043 assert_ne!(bytes_of(&resident), bytes_of(&lazy));
2044 }
2045 }
2046
2047 #[test]
2057 fn a_fat_predicate_estimates_alike_in_both_layouts() {
2058 let mut owned: Vec<(String, String, String)> = Vec::new();
2059 for i in 0..120_000 {
2060 let s = format!("<http://ex/s{i:06}>");
2061 owned.push((s.clone(), "<http://ex/kind>".into(), "<http://ex/K>".into()));
2062 owned.push((s, "<http://ex/value>".into(), format!("\"v {i}\"")));
2063 }
2064 let mut db = DictionaryBuilder::new();
2065 for (s, p, o) in &owned {
2066 db.observe(s, p, o);
2067 }
2068 let dict = db.build();
2069 let mut ib = GraphIndexBuilder::new().with_tile_budget(256);
2070 for (s, p, o) in &owned {
2071 ib.push(dict.encode(s, p, o).unwrap());
2072 }
2073 let v5 = write_file(&dict, &ib.build(), false, &[], 0);
2074 let v6: &'static [u8] = Box::leak(
2075 crate::v06::write::convert_image(&v5, Default::default())
2076 .unwrap()
2077 .into_boxed_slice(),
2078 );
2079 let leaked5: &'static [u8] = Box::leak(v5.clone().into_boxed_slice());
2080 let flat = Rete::open_ranged_lazy(crate::reader::SliceReader::new(leaked5)).unwrap();
2081 let leafed = Rete::open_ranged_lazy(crate::reader::SliceReader::new(v6)).unwrap();
2082 let q = [pat("?s", "<http://ex/value>", "?v")];
2083 let figures = |rete: &Rete| {
2084 let mut slots = Slots::new();
2085 collect_pattern_slots(&q, &mut slots);
2086 let ctx = Ctx::new(rete, slots);
2087 let index = rete.default_index();
2088 let total = bgp_total_estimate(&ctx, index, &q).unwrap();
2089 (total, probe_row_budget(total))
2090 };
2091 let (est5, budget5) = figures(&flat);
2092 let (est6, budget6) = figures(&leafed);
2093 assert!(est5 >= 70_000.0, "flat estimate {est5}");
2094 assert!(
2095 est6 >= est5 / 2.0 && est6 <= est5 * 2.0,
2096 "0x06 estimates {est6} where 0x05 estimates {est5}"
2097 );
2098 assert!(budget5.is_some_and(|b| b >= 1000), "{budget5:?}");
2100 assert!(budget6.is_some_and(|b| b >= 1000), "{budget6:?}");
2101 }
2102
2103 #[test]
2110 fn a_rare_object_of_a_fat_predicate_estimates_small_in_both_layouts() {
2111 let ty = "<http://www.w3.org/1999/02/22-rdf-syntax-ns#type>";
2112 let mut owned: Vec<(String, String, String)> = Vec::new();
2113 for i in 0..120_000 {
2114 let s = format!("<http://ex/s{i:06}>");
2115 owned.push((s.clone(), ty.into(), "<http://ex/Common>".into()));
2116 owned.push((s.clone(), "<http://ex/other>".into(), format!("\"o {i}\"")));
2117 if i % 3000 == 0 {
2118 owned.push((s, ty.into(), "<http://ex/Rare>".into()));
2119 }
2120 }
2121 let mut db = DictionaryBuilder::new();
2122 for (s, p, o) in &owned {
2123 db.observe(s, p, o);
2124 }
2125 let dict = db.build();
2126 let mut ib = GraphIndexBuilder::new().with_tile_budget(256);
2127 for (s, p, o) in &owned {
2128 ib.push(dict.encode(s, p, o).unwrap());
2129 }
2130 let v5 = write_file(&dict, &ib.build(), false, &[], 0);
2131 let v6: &'static [u8] = Box::leak(
2132 crate::v06::write::convert_image(&v5, Default::default())
2133 .unwrap()
2134 .into_boxed_slice(),
2135 );
2136 let leaked5: &'static [u8] = Box::leak(v5.into_boxed_slice());
2137 let figures = |rete: &Rete| {
2138 let est = |q: &[TriplePattern]| {
2139 let mut slots = Slots::new();
2140 collect_pattern_slots(q, &mut slots);
2141 let ctx = Ctx::new(rete, slots);
2142 bgp_min_estimate(&ctx, rete.default_index(), q).unwrap()
2143 };
2144 (
2145 est(&[pat("?s", ty, "<http://ex/Rare>")]),
2146 est(&[pat("?s", ty, "<http://ex/Common>")]),
2147 est(&[pat("?s", "<http://ex/other>", "?x")]),
2148 )
2149 };
2150 for (name, bytes) in [("0x05", leaked5), ("0x06", v6)] {
2151 let rete = Rete::open_ranged_lazy(crate::reader::SliceReader::new(bytes)).unwrap();
2152 let (rare, common, other) = figures(&rete);
2153 assert!(rare * 20.0 < other, "{name}: rare {rare} vs other {other}");
2154 assert!(
2155 rare * 20.0 < common,
2156 "{name}: rare {rare} vs common {common}"
2157 );
2158 }
2159 }
2160
2161 #[test]
2166 fn a_tile_span_is_the_same_in_both_layouts() {
2167 let ty = "<http://www.w3.org/1999/02/22-rdf-syntax-ns#type>";
2168 let mut owned: Vec<(String, String, String)> = Vec::new();
2169 for i in 0..30_000 {
2170 let s = format!("<http://ex/s{i:06}>");
2171 owned.push((s.clone(), ty.into(), format!("<http://ex/C{}>", i % 7)));
2172 owned.push((s, "<http://ex/other>".into(), format!("\"o {}\"", i % 5000)));
2173 }
2174 let mut db = DictionaryBuilder::new();
2175 for (s, p, o) in &owned {
2176 db.observe(s, p, o);
2177 }
2178 let dict = db.build();
2179 let mut ib = GraphIndexBuilder::new().with_tile_budget(256);
2180 for (s, p, o) in &owned {
2181 ib.push(dict.encode(s, p, o).unwrap());
2182 }
2183 let v5 = write_file(&dict, &ib.build(), false, &[], 0);
2184 let v6 = crate::v06::write::convert_image(&v5, Default::default()).unwrap();
2185 let (v5, v6): (&'static [u8], &'static [u8]) = (
2186 Box::leak(v5.into_boxed_slice()),
2187 Box::leak(v6.into_boxed_slice()),
2188 );
2189 let flat = Rete::open_ranged_lazy(crate::reader::SliceReader::new(v5)).unwrap();
2190 let leafed = Rete::open_ranged_lazy(crate::reader::SliceReader::new(v6)).unwrap();
2191 let (fi, li) = (flat.default_index(), leafed.default_index());
2192 let mut checked = 0;
2193 for perm in fi.perms().iter() {
2194 let si = perm.section_index();
2195 for a in (0..dict.term_count() + 2).step_by(97) {
2196 assert_eq!(
2197 fi.tile_span(si, Some(a)),
2198 li.tile_span(si, Some(a)),
2199 "section {si}, id {a}"
2200 );
2201 checked += 1;
2202 }
2203 assert_eq!(fi.tile_span(si, None), li.tile_span(si, None));
2204 }
2205 assert!(checked > 100);
2206 }
2207
2208 #[test]
2213 fn every_join_path_returns_the_same_solutions() {
2214 let mut owned: Vec<(String, String, String)> = Vec::new();
2215 let mut x: u64 = 7;
2216 let mut next = |m: u64| {
2217 x = x
2218 .wrapping_mul(6364136223846793005)
2219 .wrapping_add(1442695040888963407);
2220 (x >> 33) % m
2221 };
2222 for i in 0..60 {
2223 let s = format!("s{i}");
2224 owned.push((s.clone(), "a".into(), format!("C{}", next(3))));
2225 for p in 0..4 {
2226 for _ in 0..next(3) {
2227 owned.push((s.clone(), format!("p{p}"), format!("s{}", next(60))));
2228 }
2229 }
2230 }
2231 let triples: Vec<(&str, &str, &str)> = owned
2232 .iter()
2233 .map(|(s, p, o)| (s.as_str(), p.as_str(), o.as_str()))
2234 .collect();
2235 let bytes = rete_from(&triples);
2236 let rete = Rete::open(&bytes).unwrap();
2237 let queries: Vec<Vec<TriplePattern>> = vec![
2238 vec![pat("?s", "p0", "?a"), pat("?s", "p1", "?b")],
2239 vec![pat("?s", "a", "C1"), pat("?s", "p0", "?a")],
2240 vec![pat("?s", "p0", "?a"), pat("?a", "p2", "?b")],
2241 vec![pat("s5", "p0", "?a"), pat("?a", "?p", "?o")],
2242 vec![pat("?s", "p0", "?x"), pat("?s", "p1", "?x")],
2243 vec![
2244 pat("?s", "p3", "?o"),
2245 pat("?o", "a", "C0"),
2246 pat("?s", "p1", "?z"),
2247 ],
2248 vec![
2249 pat("?s", "p0", "?a"),
2250 pat("?s", "p1", "?b"),
2251 pat("?b", "p2", "?c"),
2252 ],
2253 ];
2254 let run = |limits: Option<PlanLimits>| -> Vec<Vec<Vec<(String, String)>>> {
2255 TEST_LIMITS.with(|c| c.set(limits));
2256 let out = queries
2257 .iter()
2258 .map(|q| {
2259 let mut slots = crate::row::Slots::new();
2260 collect_pattern_slots(q, &mut slots);
2261 let ctx = Ctx::new(&rete, slots);
2262 let mut rows: Vec<Vec<(String, String)>> =
2263 BgpSolutions::new(&ctx, rete.default_index(), q)
2264 .map(|r| row_to_binding(&ctx, &r).into_iter().collect())
2265 .collect();
2266 rows.sort();
2267 rows
2268 })
2269 .collect();
2270 TEST_LIMITS.with(|c| c.set(None));
2271 out
2272 };
2273 let reference = run(None);
2274 assert!(reference.iter().all(|r| !r.is_empty()), "{reference:?}");
2275 for (name, l) in [
2276 (
2277 "probe everything",
2278 PlanLimits {
2279 probe_floor: 0.0,
2280 stream_seed: f64::MAX,
2281 hash_build: f64::MAX,
2282 },
2283 ),
2284 (
2285 "stream + hash",
2286 PlanLimits {
2287 probe_floor: f64::MAX,
2288 stream_seed: 0.0,
2289 hash_build: f64::MAX,
2290 },
2291 ),
2292 (
2293 "stream + probe",
2294 PlanLimits {
2295 probe_floor: f64::MAX,
2296 stream_seed: 0.0,
2297 hash_build: 0.0,
2298 },
2299 ),
2300 (
2301 "stream, all paths",
2302 PlanLimits {
2303 probe_floor: 0.0,
2304 stream_seed: 0.0,
2305 hash_build: 30.0,
2306 },
2307 ),
2308 ] {
2309 assert_eq!(run(Some(l)), reference, "{name}");
2310 }
2311 }
2312
2313 #[test]
2314 fn single_pattern_binds_variable() {
2315 let bytes = rete_from(&[("Alice", "knows", "Bob"), ("Bob", "knows", "Carol")]);
2316 let rete = Rete::open(&bytes).unwrap();
2317 let sols = eval_bgp(&rete, &[pat("Alice", "knows", "?y")]);
2318 assert_eq!(sols.len(), 1);
2319 assert_eq!(sols[0]["y"], "Bob");
2320 }
2321
2322 #[test]
2323 fn two_hop_join_on_shared_variable() {
2324 let bytes = rete_from(&[
2326 ("Alice", "knows", "Bob"),
2327 ("Bob", "knows", "Carol"),
2328 ("Carol", "knows", "Dave"),
2329 ("Alice", "knows", "Eve"), ]);
2331 let rete = Rete::open(&bytes).unwrap();
2332 let sols = eval_bgp(&rete, &[pat("?x", "knows", "?y"), pat("?y", "knows", "?z")]);
2333 let mut got: Vec<_> = sols
2335 .iter()
2336 .map(|b| (b["x"].clone(), b["y"].clone(), b["z"].clone()))
2337 .collect();
2338 got.sort();
2339 assert_eq!(
2340 got,
2341 vec![
2342 ("Alice".into(), "Bob".into(), "Carol".into()),
2343 ("Bob".into(), "Carol".into(), "Dave".into()),
2344 ]
2345 );
2346 }
2347
2348 #[test]
2349 fn repeated_variable_within_pattern() {
2350 let bytes = rete_from(&[("Alice", "knows", "Bob"), ("Bob", "knows", "Bob")]);
2352 let rete = Rete::open(&bytes).unwrap();
2353 let sols = eval_bgp(&rete, &[pat("?x", "knows", "?x")]);
2354 assert_eq!(sols.len(), 1);
2355 assert_eq!(sols[0]["x"], "Bob");
2356 }
2357
2358 #[test]
2359 fn no_solutions_yields_empty() {
2360 let bytes = rete_from(&[("Alice", "knows", "Bob")]);
2361 let rete = Rete::open(&bytes).unwrap();
2362 assert!(eval_bgp(&rete, &[pat("Alice", "likes", "?y")]).is_empty());
2363 }
2364
2365 #[test]
2372 fn snowflake_type_and_adjacency_matches_reference() {
2373 const TYPE: &str = "<http://www.w3.org/1999/02/22-rdf-syntax-ns#type>";
2374 let mut triples: Vec<(String, String, String)> = Vec::new();
2375 for i in 0..60 {
2377 let s = format!("S{i}");
2378 triples.push((s.clone(), TYPE.into(), "Student".into()));
2379 triples.push((s.clone(), "takesCourse".into(), format!("c{}", i % 5)));
2380 triples.push((s, "takesCourse".into(), format!("c{}", (i + 1) % 5)));
2381 }
2382 for j in 0..5 {
2384 triples.push((format!("c{j}"), TYPE.into(), "Course".into()));
2385 }
2386 triples.push(("Prof".into(), "teacherOf".into(), "c0".into()));
2387 triples.push(("Prof".into(), "teacherOf".into(), "c2".into()));
2388
2389 let refs: Vec<(&str, &str, &str)> = triples
2390 .iter()
2391 .map(|(s, p, o)| (s.as_str(), p.as_str(), o.as_str()))
2392 .collect();
2393 let bytes = rete_from(&refs);
2394 let rete = Rete::open(&bytes).unwrap();
2395
2396 let got: std::collections::BTreeSet<(String, String)> = eval_bgp(
2397 &rete,
2398 &[
2399 pat("?x", TYPE, "Student"),
2400 pat("?y", TYPE, "Course"),
2401 pat("?x", "takesCourse", "?y"),
2402 pat("Prof", "teacherOf", "?y"),
2403 ],
2404 )
2405 .into_iter()
2406 .map(|b| (b["x"].clone(), b["y"].clone()))
2407 .collect();
2408
2409 let taught = ["c0", "c2"];
2411 let mut want = std::collections::BTreeSet::new();
2412 for i in 0..60 {
2413 for c in [i % 5, (i + 1) % 5] {
2414 let course = format!("c{c}");
2415 if taught.contains(&course.as_str()) {
2416 want.insert((format!("S{i}"), course));
2417 }
2418 }
2419 }
2420 assert_eq!(got, want, "snowflake join must match the brute-force set");
2421 assert!(!want.is_empty(), "sanity: the reference set is non-empty");
2422 }
2423
2424 #[test]
2429 fn merge_join_subject_star_cross_product() {
2430 let bytes = rete_from(&[
2431 ("x", "a", "a1"),
2432 ("x", "a", "a2"),
2433 ("x", "b", "b1"),
2434 ("x", "b", "b2"),
2435 ("y", "a", "a3"),
2436 ("y", "b", "b3"),
2437 ("z", "a", "a4"), ]);
2439 let rete = Rete::open(&bytes).unwrap();
2440 let mut got: Vec<(String, String, String)> =
2441 eval_bgp(&rete, &[pat("?x", "a", "?av"), pat("?x", "b", "?bv")])
2442 .iter()
2443 .map(|m| (m["x"].clone(), m["av"].clone(), m["bv"].clone()))
2444 .collect();
2445 got.sort();
2446 let mut want: Vec<(String, String, String)> = [
2447 ("x", "a1", "b1"),
2448 ("x", "a1", "b2"),
2449 ("x", "a2", "b1"),
2450 ("x", "a2", "b2"),
2451 ("y", "a3", "b3"),
2452 ]
2453 .into_iter()
2454 .map(|(a, b, c)| (a.to_string(), b.to_string(), c.to_string()))
2455 .collect();
2456 want.sort();
2457 assert_eq!(got, want, "same-role merge must equal the brute-force join");
2458 }
2459}