1use std::collections::hash_map::RandomState;
29use std::collections::HashMap;
30use std::hash::{BuildHasher, Hash, Hasher};
31
32use super::super::engine::binding::{Binding, Value, Var};
33use super::value_compare::total_compare_values;
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub enum JoinType {
42 Inner,
44 Left,
46 Right,
48 Cross,
50 FullOuter,
52}
53
54#[derive(Debug, Clone)]
56pub enum JoinCondition {
57 Eq(Var, Var),
59 And(Vec<JoinCondition>),
61 None,
63}
64
65impl JoinCondition {
66 pub fn left_vars(&self) -> Vec<Var> {
68 match self {
69 JoinCondition::Eq(left, _) => vec![left.clone()],
70 JoinCondition::And(conditions) => {
71 conditions.iter().flat_map(|c| c.left_vars()).collect()
72 }
73 JoinCondition::None => Vec::new(),
74 }
75 }
76
77 pub fn right_vars(&self) -> Vec<Var> {
79 match self {
80 JoinCondition::Eq(_, right) => vec![right.clone()],
81 JoinCondition::And(conditions) => {
82 conditions.iter().flat_map(|c| c.right_vars()).collect()
83 }
84 JoinCondition::None => Vec::new(),
85 }
86 }
87}
88
89#[derive(Debug, Clone, Copy, PartialEq, Eq)]
95pub enum JoinStrategy {
96 Hash,
98 NestedLoop,
100 Merge,
102}
103
104#[derive(Debug, Clone)]
106pub struct JoinStats {
107 pub left_cardinality: usize,
108 pub right_cardinality: usize,
109 pub left_sorted: bool,
110 pub right_sorted: bool,
111 pub condition_selectivity: f64,
112}
113
114impl Default for JoinStats {
115 fn default() -> Self {
116 Self {
117 left_cardinality: 0,
118 right_cardinality: 0,
119 left_sorted: false,
120 right_sorted: false,
121 condition_selectivity: 1.0,
122 }
123 }
124}
125
126pub fn choose_strategy(stats: &JoinStats, condition: &JoinCondition) -> JoinStrategy {
128 if matches!(condition, JoinCondition::None) {
130 return JoinStrategy::NestedLoop;
131 }
132
133 if stats.left_sorted && stats.right_sorted {
135 return JoinStrategy::Merge;
136 }
137
138 let total = stats.left_cardinality * stats.right_cardinality;
140 if total < 1000 {
141 return JoinStrategy::NestedLoop;
142 }
143
144 if matches!(condition, JoinCondition::Eq(_, _) | JoinCondition::And(_)) {
146 return JoinStrategy::Hash;
147 }
148
149 JoinStrategy::NestedLoop
150}
151
152#[derive(Clone, PartialEq, Eq)]
158pub(crate) struct HashKey(Vec<Option<Value>>);
159
160impl Hash for HashKey {
161 fn hash<H: Hasher>(&self, state: &mut H) {
162 for value in &self.0 {
163 hash_key_value(value.as_ref(), state);
164 }
165 }
166}
167
168fn hash_key_value<H: Hasher>(value: Option<&Value>, state: &mut H) {
172 match value {
173 Some(Value::String(s)) => {
174 1u8.hash(state);
175 s.hash(state);
176 }
177 Some(Value::Integer(i)) => {
178 2u8.hash(state);
179 i.hash(state);
180 }
181 Some(Value::Float(f)) => {
182 3u8.hash(state);
183 f.to_bits().hash(state);
184 }
185 Some(Value::Boolean(b)) => {
186 4u8.hash(state);
187 b.hash(state);
188 }
189 Some(Value::Uri(u)) => {
190 5u8.hash(state);
191 u.hash(state);
192 }
193 Some(Value::Node(n)) => {
194 6u8.hash(state);
195 n.hash(state);
196 }
197 Some(Value::Edge(e)) => {
198 7u8.hash(state);
199 e.hash(state);
200 }
201 Some(Value::Null) | None => {
202 0u8.hash(state);
203 }
204 }
205}
206
207pub(crate) fn key_hash(state: &RandomState, binding: &Binding, vars: &[Var]) -> u64 {
214 let mut hasher = state.build_hasher();
215 for var in vars {
216 hash_key_value(binding.get(var), &mut hasher);
217 }
218 hasher.finish()
219}
220
221pub(crate) fn key_matches(key: &HashKey, binding: &Binding, vars: &[Var]) -> bool {
225 key.0.len() == vars.len()
226 && key
227 .0
228 .iter()
229 .zip(vars)
230 .all(|(stored, var)| stored.as_ref() == binding.get(var))
231}
232
233pub fn hash_join(
235 left: Vec<Binding>,
236 right: Vec<Binding>,
237 condition: &JoinCondition,
238 join_type: JoinType,
239) -> Vec<Binding> {
240 let left_keys = condition.left_vars();
241 let right_keys = condition.right_vars();
242
243 if left_keys.is_empty() {
244 return nested_loop_join(left, right, condition, join_type);
246 }
247
248 let (build_side, probe_side, build_keys, probe_keys, is_left_build) =
250 if left.len() <= right.len() {
251 (&left, &right, &left_keys, &right_keys, true)
252 } else {
253 (&right, &left, &right_keys, &left_keys, false)
254 };
255
256 #[allow(clippy::type_complexity)]
262 let mut hash_table: HashMap<u64, Vec<(HashKey, Vec<&Binding>)>> = HashMap::new();
263 let hash_state = RandomState::new();
264 for binding in build_side {
265 let hash = key_hash(&hash_state, binding, build_keys);
266 let bucket = hash_table.entry(hash).or_default();
267 match bucket
268 .iter_mut()
269 .find(|(key, _)| key_matches(key, binding, build_keys))
270 {
271 Some((_, bindings)) => bindings.push(binding),
272 None => bucket.push((extract_key(binding, build_keys), vec![binding])),
273 }
274 }
275
276 let mut results = Vec::new();
278 let mut matched_build: std::collections::HashSet<usize> = std::collections::HashSet::new();
279
280 for (probe_idx, probe_binding) in probe_side.iter().enumerate() {
281 let matches = hash_table
284 .get(&key_hash(&hash_state, probe_binding, probe_keys))
285 .and_then(|bucket: &Vec<(HashKey, Vec<&Binding>)>| {
286 bucket
287 .iter()
288 .find(|(key, _)| key_matches(key, probe_binding, probe_keys))
289 .map(|(_, bindings)| bindings)
290 });
291
292 let mut had_match = false;
293 if let Some(build_bindings) = matches {
294 for (build_idx, &build_binding) in build_bindings.iter().enumerate() {
295 had_match = true;
296
297 if matches!(join_type, JoinType::FullOuter) {
299 let original_idx = build_side
301 .iter()
302 .position(|b| std::ptr::eq(b, build_binding));
303 if let Some(idx) = original_idx {
304 matched_build.insert(idx);
305 }
306 }
307
308 let merged = if is_left_build {
310 merge_bindings(build_binding, probe_binding)
311 } else {
312 merge_bindings(probe_binding, build_binding)
313 };
314 results.push(merged);
315 }
316 }
317
318 if !had_match {
320 match join_type {
321 JoinType::Left if !is_left_build => {
322 results.push(probe_binding.clone());
324 }
325 JoinType::Right if is_left_build => {
326 results.push(probe_binding.clone());
328 }
329 JoinType::FullOuter => {
330 results.push(probe_binding.clone());
331 }
332 _ => {}
333 }
334 }
335 }
336
337 if matches!(join_type, JoinType::FullOuter) {
339 for (idx, binding) in build_side.iter().enumerate() {
340 if !matched_build.contains(&idx) {
341 results.push((*binding).clone());
342 }
343 }
344 }
345
346 match (join_type, is_left_build) {
348 (JoinType::Left, true) => {
349 let mut all_left_matched: std::collections::HashSet<usize> =
351 std::collections::HashSet::new();
352 for binding in &results {
353 for (idx, left_binding) in left.iter().enumerate() {
355 if bindings_match(binding, left_binding, &left_keys) {
356 all_left_matched.insert(idx);
357 }
358 }
359 }
360 for (idx, binding) in left.iter().enumerate() {
361 if !all_left_matched.contains(&idx) {
362 results.push(binding.clone());
363 }
364 }
365 }
366 (JoinType::Right, false) => {
367 let mut all_right_matched: std::collections::HashSet<usize> =
369 std::collections::HashSet::new();
370 for binding in &results {
371 for (idx, right_binding) in right.iter().enumerate() {
372 if bindings_match(binding, right_binding, &right_keys) {
373 all_right_matched.insert(idx);
374 }
375 }
376 }
377 for (idx, binding) in right.iter().enumerate() {
378 if !all_right_matched.contains(&idx) {
379 results.push(binding.clone());
380 }
381 }
382 }
383 _ => {}
384 }
385
386 results
387}
388
389pub(crate) fn extract_key(binding: &Binding, vars: &[Var]) -> HashKey {
390 HashKey(vars.iter().map(|v| binding.get(v).cloned()).collect())
391}
392
393fn bindings_match(a: &Binding, b: &Binding, keys: &[Var]) -> bool {
394 keys.iter().all(|k| match (a.get(k), b.get(k)) {
395 (Some(v1), Some(v2)) => v1 == v2,
396 _ => false,
397 })
398}
399
400pub fn nested_loop_join(
406 left: Vec<Binding>,
407 right: Vec<Binding>,
408 condition: &JoinCondition,
409 join_type: JoinType,
410) -> Vec<Binding> {
411 let mut results = Vec::new();
412 let mut left_matched = vec![false; left.len()];
413 let mut right_matched = vec![false; right.len()];
414
415 for (left_idx, left_binding) in left.iter().enumerate() {
416 let mut found_match = false;
417
418 for (right_idx, right_binding) in right.iter().enumerate() {
419 if check_condition(left_binding, right_binding, condition) {
420 found_match = true;
421 left_matched[left_idx] = true;
422 right_matched[right_idx] = true;
423
424 let merged = merge_bindings(left_binding, right_binding);
425 results.push(merged);
426 }
427 }
428
429 if !found_match && matches!(join_type, JoinType::Left | JoinType::FullOuter) {
431 results.push(left_binding.clone());
432 }
433 }
434
435 if matches!(join_type, JoinType::Right | JoinType::FullOuter) {
437 for (right_idx, right_binding) in right.iter().enumerate() {
438 if !right_matched[right_idx] {
439 results.push(right_binding.clone());
440 }
441 }
442 }
443
444 results
445}
446
447fn check_condition(left: &Binding, right: &Binding, condition: &JoinCondition) -> bool {
448 match condition {
449 JoinCondition::Eq(left_var, right_var) => {
450 match (left.get(left_var), right.get(right_var)) {
451 (Some(l), Some(r)) => l == r,
452 _ => false,
453 }
454 }
455 JoinCondition::And(conditions) => {
456 conditions.iter().all(|c| check_condition(left, right, c))
457 }
458 JoinCondition::None => true,
459 }
460}
461
462pub fn merge_join(
468 left: Vec<Binding>,
469 right: Vec<Binding>,
470 condition: &JoinCondition,
471 join_type: JoinType,
472) -> Vec<Binding> {
473 let left_keys = condition.left_vars();
476 let right_keys = condition.right_vars();
477
478 if left_keys.is_empty() || right_keys.is_empty() {
479 return nested_loop_join(left, right, condition, join_type);
480 }
481
482 let mut left_sorted = left;
484 let mut right_sorted = right;
485
486 left_sorted.sort_by(|a, b| compare_by_keys(a, b, &left_keys));
487 right_sorted.sort_by(|a, b| compare_by_keys(a, b, &right_keys));
488
489 let mut results = Vec::new();
490 let mut left_idx = 0;
491 let mut right_idx = 0;
492 let mut left_matched = vec![false; left_sorted.len()];
493 let mut right_matched = vec![false; right_sorted.len()];
494
495 while left_idx < left_sorted.len() && right_idx < right_sorted.len() {
496 let left_key = extract_key(&left_sorted[left_idx], &left_keys);
497 let right_key = extract_key(&right_sorted[right_idx], &right_keys);
498
499 match compare_keys(&left_key, &right_key) {
500 std::cmp::Ordering::Less => {
501 if matches!(join_type, JoinType::Left | JoinType::FullOuter)
503 && !left_matched[left_idx]
504 {
505 results.push(left_sorted[left_idx].clone());
506 }
507 left_idx += 1;
508 }
509 std::cmp::Ordering::Greater => {
510 if matches!(join_type, JoinType::Right | JoinType::FullOuter)
512 && !right_matched[right_idx]
513 {
514 results.push(right_sorted[right_idx].clone());
515 }
516 right_idx += 1;
517 }
518 std::cmp::Ordering::Equal => {
519 let match_start_right = right_idx;
521
522 while right_idx < right_sorted.len() {
524 let current_right_key = extract_key(&right_sorted[right_idx], &right_keys);
525 if compare_keys(&left_key, ¤t_right_key) != std::cmp::Ordering::Equal {
526 break;
527 }
528
529 left_matched[left_idx] = true;
530 right_matched[right_idx] = true;
531
532 let merged = merge_bindings(&left_sorted[left_idx], &right_sorted[right_idx]);
533 results.push(merged);
534 right_idx += 1;
535 }
536
537 left_idx += 1;
539 while left_idx < left_sorted.len() {
540 let current_left_key = extract_key(&left_sorted[left_idx], &left_keys);
541 if compare_keys(¤t_left_key, &left_key) != std::cmp::Ordering::Equal {
542 break;
543 }
544
545 for right_row in right_sorted.iter().take(right_idx).skip(match_start_right) {
547 left_matched[left_idx] = true;
548 let merged = merge_bindings(&left_sorted[left_idx], right_row);
549 results.push(merged);
550 }
551 left_idx += 1;
552 }
553
554 right_idx = match_start_right;
556 if left_idx >= left_sorted.len() || {
557 let next_left_key = extract_key(
558 &left_sorted[left_idx.min(left_sorted.len() - 1)],
559 &left_keys,
560 );
561 compare_keys(&next_left_key, &left_key) != std::cmp::Ordering::Equal
562 } {
563 while right_idx < right_sorted.len() {
565 let current_right_key = extract_key(&right_sorted[right_idx], &right_keys);
566 if compare_keys(&left_key, ¤t_right_key) != std::cmp::Ordering::Equal
567 {
568 break;
569 }
570 right_idx += 1;
571 }
572 }
573 }
574 }
575 }
576
577 while left_idx < left_sorted.len() {
579 if matches!(join_type, JoinType::Left | JoinType::FullOuter) && !left_matched[left_idx] {
580 results.push(left_sorted[left_idx].clone());
581 }
582 left_idx += 1;
583 }
584
585 while right_idx < right_sorted.len() {
586 if matches!(join_type, JoinType::Right | JoinType::FullOuter) && !right_matched[right_idx] {
587 results.push(right_sorted[right_idx].clone());
588 }
589 right_idx += 1;
590 }
591
592 results
593}
594
595fn compare_by_keys(a: &Binding, b: &Binding, keys: &[Var]) -> std::cmp::Ordering {
596 for key in keys {
597 match (a.get(key), b.get(key)) {
598 (Some(av), Some(bv)) => {
599 let cmp = total_compare_values(av, bv);
600 if cmp != std::cmp::Ordering::Equal {
601 return cmp;
602 }
603 }
604 (Some(_), None) => return std::cmp::Ordering::Less,
605 (None, Some(_)) => return std::cmp::Ordering::Greater,
606 (None, None) => {}
607 }
608 }
609 std::cmp::Ordering::Equal
610}
611
612fn compare_keys(a: &HashKey, b: &HashKey) -> std::cmp::Ordering {
613 for (av, bv) in a.0.iter().zip(b.0.iter()) {
614 match (av, bv) {
615 (Some(av), Some(bv)) => {
616 let cmp = total_compare_values(av, bv);
617 if cmp != std::cmp::Ordering::Equal {
618 return cmp;
619 }
620 }
621 (Some(_), None) => return std::cmp::Ordering::Less,
622 (None, Some(_)) => return std::cmp::Ordering::Greater,
623 (None, None) => {}
624 }
625 }
626 std::cmp::Ordering::Equal
627}
628
629fn merge_bindings(left: &Binding, right: &Binding) -> Binding {
635 if let Some(merged) = left.merge(right) {
638 merged
639 } else {
640 left.clone()
642 }
643}
644
645pub fn execute_join(
651 left: Vec<Binding>,
652 right: Vec<Binding>,
653 condition: JoinCondition,
654 join_type: JoinType,
655 stats: Option<JoinStats>,
656) -> Vec<Binding> {
657 let actual_stats = stats.unwrap_or(JoinStats {
659 left_cardinality: left.len(),
660 right_cardinality: right.len(),
661 left_sorted: false,
662 right_sorted: false,
663 condition_selectivity: 1.0,
664 });
665
666 let strategy = choose_strategy(&actual_stats, &condition);
667
668 match strategy {
669 JoinStrategy::Hash => hash_join(left, right, &condition, join_type),
670 JoinStrategy::NestedLoop => nested_loop_join(left, right, &condition, join_type),
671 JoinStrategy::Merge => merge_join(left, right, &condition, join_type),
672 }
673}
674
675#[cfg(test)]
680mod tests {
681 use super::*;
682
683 fn make_binding(pairs: &[(&str, &str)]) -> Binding {
684 if pairs.is_empty() {
686 return Binding::empty();
687 }
688
689 let mut result = Binding::one(Var::new(pairs[0].0), Value::String(pairs[0].1.to_string()));
690
691 for (k, v) in pairs.iter().skip(1) {
692 let next = Binding::one(Var::new(k), Value::String(v.to_string()));
693 result = result.merge(&next).unwrap_or(result);
694 }
695
696 result
697 }
698
699 #[test]
700 fn test_inner_join() {
701 let left = vec![
702 make_binding(&[("id", "1"), ("name", "Alice")]),
703 make_binding(&[("id", "2"), ("name", "Bob")]),
704 make_binding(&[("id", "3"), ("name", "Charlie")]),
705 ];
706
707 let right = vec![
708 make_binding(&[("user_id", "1"), ("score", "100")]),
709 make_binding(&[("user_id", "2"), ("score", "90")]),
710 make_binding(&[("user_id", "4"), ("score", "80")]),
711 ];
712
713 let condition = JoinCondition::Eq(Var::new("id"), Var::new("user_id"));
714 let results = execute_join(left, right, condition, JoinType::Inner, None);
715
716 assert_eq!(results.len(), 2);
717 assert!(results
718 .iter()
719 .any(|b| b.get(&Var::new("name")) == Some(&Value::String("Alice".to_string()))));
720 assert!(results
721 .iter()
722 .any(|b| b.get(&Var::new("name")) == Some(&Value::String("Bob".to_string()))));
723 }
724
725 #[test]
726 fn test_left_join() {
727 let left = vec![
728 make_binding(&[("id", "1"), ("name", "Alice")]),
729 make_binding(&[("id", "2"), ("name", "Bob")]),
730 make_binding(&[("id", "3"), ("name", "Charlie")]),
731 ];
732
733 let right = vec![make_binding(&[("user_id", "1"), ("score", "100")])];
734
735 let condition = JoinCondition::Eq(Var::new("id"), Var::new("user_id"));
736 let results = execute_join(left, right, condition, JoinType::Left, None);
737
738 assert_eq!(results.len(), 3); assert!(results
740 .iter()
741 .any(|b| b.get(&Var::new("name")) == Some(&Value::String("Charlie".to_string()))));
742 }
743
744 #[test]
745 fn test_right_join() {
746 let left = vec![make_binding(&[("id", "1"), ("name", "Alice")])];
747
748 let right = vec![
749 make_binding(&[("user_id", "1"), ("score", "100")]),
750 make_binding(&[("user_id", "2"), ("score", "90")]),
751 make_binding(&[("user_id", "3"), ("score", "80")]),
752 ];
753
754 let condition = JoinCondition::Eq(Var::new("id"), Var::new("user_id"));
755 let results = execute_join(left, right, condition, JoinType::Right, None);
756
757 assert_eq!(results.len(), 3); }
759
760 #[test]
761 fn test_cross_join() {
762 let left = vec![make_binding(&[("a", "1")]), make_binding(&[("a", "2")])];
763
764 let right = vec![
765 make_binding(&[("b", "x")]),
766 make_binding(&[("b", "y")]),
767 make_binding(&[("b", "z")]),
768 ];
769
770 let results = execute_join(left, right, JoinCondition::None, JoinType::Cross, None);
771
772 assert_eq!(results.len(), 6); }
774
775 #[test]
776 fn test_merge_join() {
777 let left = vec![
778 make_binding(&[("id", "1"), ("name", "Alice")]),
779 make_binding(&[("id", "2"), ("name", "Bob")]),
780 ];
781
782 let right = vec![
783 make_binding(&[("id", "1"), ("dept", "Eng")]),
784 make_binding(&[("id", "2"), ("dept", "Sales")]),
785 ];
786
787 let condition = JoinCondition::Eq(Var::new("id"), Var::new("id"));
788 let stats = JoinStats {
789 left_cardinality: 2,
790 right_cardinality: 2,
791 left_sorted: true,
792 right_sorted: true,
793 condition_selectivity: 1.0,
794 };
795
796 let results = execute_join(left, right, condition, JoinType::Inner, Some(stats));
797 assert_eq!(results.len(), 2);
798 }
799
800 #[test]
801 fn test_strategy_selection() {
802 let stats = JoinStats {
804 left_cardinality: 10,
805 right_cardinality: 10,
806 left_sorted: false,
807 right_sorted: false,
808 condition_selectivity: 1.0,
809 };
810 assert_eq!(
811 choose_strategy(&stats, &JoinCondition::Eq(Var::new("a"), Var::new("b"))),
812 JoinStrategy::NestedLoop
813 );
814
815 let stats = JoinStats {
817 left_cardinality: 10000,
818 right_cardinality: 10000,
819 left_sorted: false,
820 right_sorted: false,
821 condition_selectivity: 1.0,
822 };
823 assert_eq!(
824 choose_strategy(&stats, &JoinCondition::Eq(Var::new("a"), Var::new("b"))),
825 JoinStrategy::Hash
826 );
827
828 let stats = JoinStats {
830 left_cardinality: 1000,
831 right_cardinality: 1000,
832 left_sorted: true,
833 right_sorted: true,
834 condition_selectivity: 1.0,
835 };
836 assert_eq!(
837 choose_strategy(&stats, &JoinCondition::Eq(Var::new("a"), Var::new("b"))),
838 JoinStrategy::Merge
839 );
840 }
841
842 fn key_row(k: Option<Value>, tag_var: &str, tag: &str) -> Binding {
847 let row = Binding::one(Var::new(tag_var), Value::String(tag.to_string()));
848 match k {
849 Some(value) => row
850 .merge(&Binding::one(Var::new("k"), value))
851 .expect("no conflicting binding"),
852 None => row,
853 }
854 }
855
856 fn left_row(k: Option<Value>, tag: &str) -> Binding {
857 key_row(k, "lt", tag)
858 }
859
860 fn right_row(k: Option<Value>, tag: &str) -> Binding {
861 key_row(k, "rt", tag)
862 }
863
864 fn fingerprint(binding: &Binding) -> String {
867 let field = |name: &str| match binding.get(&Var::new(name)) {
868 Some(value) => format!("{value:?}"),
869 None => "-".to_string(),
870 };
871 format!("lt={} rt={} k={}", field("lt"), field("rt"), field("k"))
872 }
873
874 fn reference_inner_matches(left: &[Binding], right: &[Binding], keys: &[Var]) -> Vec<String> {
878 let mut out: Vec<String> = Vec::new();
879 for l in left {
880 for r in right {
881 if extract_key(l, keys) == extract_key(r, keys) {
882 out.push(fingerprint(&merge_bindings(l, r)));
883 }
884 }
885 }
886 out.sort();
887 out
888 }
889
890 #[test]
894 fn null_and_absent_keys_collide_in_hash_but_stay_distinct_in_equality() {
895 let keys = [Var::new("k")];
896 let null_row = left_row(Some(Value::Null), "null");
897 let absent_row = left_row(None, "absent");
898
899 let state = RandomState::new();
900 assert_eq!(
901 key_hash(&state, &null_row, &keys),
902 key_hash(&state, &absent_row, &keys),
903 "NULL and an absent column are expected to share a hash bucket"
904 );
905 assert!(key_matches(
906 &extract_key(&null_row, &keys),
907 &null_row,
908 &keys
909 ));
910 assert!(!key_matches(
911 &extract_key(&null_row, &keys),
912 &absent_row,
913 &keys
914 ));
915 assert!(!key_matches(
916 &extract_key(&absent_row, &keys),
917 &null_row,
918 &keys
919 ));
920 }
921
922 #[test]
926 fn hash_join_matches_are_identical_to_the_owned_key_reference() {
927 let left = vec![
928 left_row(Some(Value::String("a".into())), "L-a"),
929 left_row(Some(Value::Integer(1)), "L-1"),
930 left_row(Some(Value::Null), "L-null"),
931 left_row(None, "L-absent"),
932 left_row(Some(Value::String("a".into())), "L-a2"),
933 ];
934 let right = vec![
935 right_row(Some(Value::String("a".into())), "R-a"),
936 right_row(Some(Value::Null), "R-null"),
937 right_row(None, "R-absent"),
938 right_row(Some(Value::Boolean(true)), "R-true"),
939 right_row(Some(Value::Integer(1)), "R-1"),
940 right_row(Some(Value::Integer(1)), "R-1b"),
941 ];
942 let keys = [Var::new("k")];
943 let condition = JoinCondition::Eq(Var::new("k"), Var::new("k"));
944
945 for (l, r) in [
949 (left.clone(), right.clone()),
950 (right.clone(), left.clone()),
951 (left.clone(), right[..left.len()].to_vec()),
952 ] {
953 let expected = reference_inner_matches(&l, &r, &keys);
954
955 let mut actual: Vec<String> = hash_join(l, r, &condition, JoinType::Inner)
956 .iter()
957 .map(fingerprint)
958 .collect();
959 actual.sort();
960
961 assert_eq!(
962 actual, expected,
963 "hash join diverged from the owned-key reference"
964 );
965 }
966 }
967}