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)]
158#[doc(hidden)]
159pub struct HashKey(Vec<Option<Value>>);
160
161impl Hash for HashKey {
162 fn hash<H: Hasher>(&self, state: &mut H) {
163 for value in &self.0 {
164 hash_key_value(value.as_ref(), state);
165 }
166 }
167}
168
169fn hash_key_value<H: Hasher>(value: Option<&Value>, state: &mut H) {
173 match value {
174 Some(Value::String(s)) => {
175 1u8.hash(state);
176 s.hash(state);
177 }
178 Some(Value::Integer(i)) => {
179 2u8.hash(state);
180 i.hash(state);
181 }
182 Some(Value::Float(f)) => {
183 3u8.hash(state);
184 f.to_bits().hash(state);
185 }
186 Some(Value::Boolean(b)) => {
187 4u8.hash(state);
188 b.hash(state);
189 }
190 Some(Value::Uri(u)) => {
191 5u8.hash(state);
192 u.hash(state);
193 }
194 Some(Value::Node(n)) => {
195 6u8.hash(state);
196 n.hash(state);
197 }
198 Some(Value::Edge(e)) => {
199 7u8.hash(state);
200 e.hash(state);
201 }
202 Some(Value::Null) | None => {
203 0u8.hash(state);
204 }
205 }
206}
207
208#[doc(hidden)]
215pub fn key_hash(state: &RandomState, binding: &Binding, vars: &[Var]) -> u64 {
216 let mut hasher = state.build_hasher();
217 for var in vars {
218 hash_key_value(binding.get(var), &mut hasher);
219 }
220 hasher.finish()
221}
222
223#[doc(hidden)]
227pub fn key_matches(key: &HashKey, binding: &Binding, vars: &[Var]) -> bool {
228 key.0.len() == vars.len()
229 && key
230 .0
231 .iter()
232 .zip(vars)
233 .all(|(stored, var)| stored.as_ref() == binding.get(var))
234}
235
236pub fn hash_join(
238 left: Vec<Binding>,
239 right: Vec<Binding>,
240 condition: &JoinCondition,
241 join_type: JoinType,
242) -> Vec<Binding> {
243 let left_keys = condition.left_vars();
244 let right_keys = condition.right_vars();
245
246 if left_keys.is_empty() {
247 return nested_loop_join(left, right, condition, join_type);
249 }
250
251 let (build_side, probe_side, build_keys, probe_keys, is_left_build) =
253 if left.len() <= right.len() {
254 (&left, &right, &left_keys, &right_keys, true)
255 } else {
256 (&right, &left, &right_keys, &left_keys, false)
257 };
258
259 #[allow(clippy::type_complexity)]
265 let mut hash_table: HashMap<u64, Vec<(HashKey, Vec<&Binding>)>> = HashMap::new();
266 let hash_state = RandomState::new();
267 for binding in build_side {
268 let hash = key_hash(&hash_state, binding, build_keys);
269 let bucket = hash_table.entry(hash).or_default();
270 match bucket
271 .iter_mut()
272 .find(|(key, _)| key_matches(key, binding, build_keys))
273 {
274 Some((_, bindings)) => bindings.push(binding),
275 None => bucket.push((extract_key(binding, build_keys), vec![binding])),
276 }
277 }
278
279 let mut results = Vec::new();
281 let mut matched_build: std::collections::HashSet<usize> = std::collections::HashSet::new();
282
283 for probe_binding in probe_side {
284 let matches = hash_table
287 .get(&key_hash(&hash_state, probe_binding, probe_keys))
288 .and_then(|bucket: &Vec<(HashKey, Vec<&Binding>)>| {
289 bucket
290 .iter()
291 .find(|(key, _)| key_matches(key, probe_binding, probe_keys))
292 .map(|(_, bindings)| bindings)
293 });
294
295 let mut had_match = false;
296 if let Some(build_bindings) = matches {
297 for &build_binding in build_bindings {
298 had_match = true;
299
300 if matches!(join_type, JoinType::FullOuter) {
302 let original_idx = build_side
304 .iter()
305 .position(|b| std::ptr::eq(b, build_binding));
306 if let Some(idx) = original_idx {
307 matched_build.insert(idx);
308 }
309 }
310
311 let merged = if is_left_build {
313 merge_bindings(build_binding, probe_binding)
314 } else {
315 merge_bindings(probe_binding, build_binding)
316 };
317 results.push(merged);
318 }
319 }
320
321 if !had_match {
323 match join_type {
324 JoinType::Left if !is_left_build => {
325 results.push(probe_binding.clone());
327 }
328 JoinType::Right if is_left_build => {
329 results.push(probe_binding.clone());
331 }
332 JoinType::FullOuter => {
333 results.push(probe_binding.clone());
334 }
335 _ => {}
336 }
337 }
338 }
339
340 if matches!(join_type, JoinType::FullOuter) {
342 for (idx, binding) in build_side.iter().enumerate() {
343 if !matched_build.contains(&idx) {
344 results.push((*binding).clone());
345 }
346 }
347 }
348
349 match (join_type, is_left_build) {
351 (JoinType::Left, true) => {
352 let mut all_left_matched: std::collections::HashSet<usize> =
354 std::collections::HashSet::new();
355 for binding in &results {
356 for (idx, left_binding) in left.iter().enumerate() {
358 if bindings_match(binding, left_binding, &left_keys) {
359 all_left_matched.insert(idx);
360 }
361 }
362 }
363 for (idx, binding) in left.iter().enumerate() {
364 if !all_left_matched.contains(&idx) {
365 results.push(binding.clone());
366 }
367 }
368 }
369 (JoinType::Right, false) => {
370 let mut all_right_matched: std::collections::HashSet<usize> =
372 std::collections::HashSet::new();
373 for binding in &results {
374 for (idx, right_binding) in right.iter().enumerate() {
375 if bindings_match(binding, right_binding, &right_keys) {
376 all_right_matched.insert(idx);
377 }
378 }
379 }
380 for (idx, binding) in right.iter().enumerate() {
381 if !all_right_matched.contains(&idx) {
382 results.push(binding.clone());
383 }
384 }
385 }
386 _ => {}
387 }
388
389 results
390}
391
392#[doc(hidden)]
393pub fn extract_key(binding: &Binding, vars: &[Var]) -> HashKey {
394 HashKey(vars.iter().map(|v| binding.get(v).cloned()).collect())
395}
396
397fn bindings_match(a: &Binding, b: &Binding, keys: &[Var]) -> bool {
398 keys.iter().all(|k| match (a.get(k), b.get(k)) {
399 (Some(v1), Some(v2)) => v1 == v2,
400 _ => false,
401 })
402}
403
404pub fn nested_loop_join(
410 left: Vec<Binding>,
411 right: Vec<Binding>,
412 condition: &JoinCondition,
413 join_type: JoinType,
414) -> Vec<Binding> {
415 let mut results = Vec::new();
416 let mut left_matched = vec![false; left.len()];
417 let mut right_matched = vec![false; right.len()];
418
419 for (left_idx, left_binding) in left.iter().enumerate() {
420 let mut found_match = false;
421
422 for (right_idx, right_binding) in right.iter().enumerate() {
423 if check_condition(left_binding, right_binding, condition) {
424 found_match = true;
425 left_matched[left_idx] = true;
426 right_matched[right_idx] = true;
427
428 let merged = merge_bindings(left_binding, right_binding);
429 results.push(merged);
430 }
431 }
432
433 if !found_match && matches!(join_type, JoinType::Left | JoinType::FullOuter) {
435 results.push(left_binding.clone());
436 }
437 }
438
439 if matches!(join_type, JoinType::Right | JoinType::FullOuter) {
441 for (right_idx, right_binding) in right.iter().enumerate() {
442 if !right_matched[right_idx] {
443 results.push(right_binding.clone());
444 }
445 }
446 }
447
448 results
449}
450
451fn check_condition(left: &Binding, right: &Binding, condition: &JoinCondition) -> bool {
452 match condition {
453 JoinCondition::Eq(left_var, right_var) => {
454 match (left.get(left_var), right.get(right_var)) {
455 (Some(l), Some(r)) => l == r,
456 _ => false,
457 }
458 }
459 JoinCondition::And(conditions) => {
460 conditions.iter().all(|c| check_condition(left, right, c))
461 }
462 JoinCondition::None => true,
463 }
464}
465
466pub fn merge_join(
472 left: Vec<Binding>,
473 right: Vec<Binding>,
474 condition: &JoinCondition,
475 join_type: JoinType,
476) -> Vec<Binding> {
477 let left_keys = condition.left_vars();
480 let right_keys = condition.right_vars();
481
482 if left_keys.is_empty() || right_keys.is_empty() {
483 return nested_loop_join(left, right, condition, join_type);
484 }
485
486 let mut left_sorted = left;
488 let mut right_sorted = right;
489
490 left_sorted.sort_by(|a, b| compare_by_keys(a, b, &left_keys));
491 right_sorted.sort_by(|a, b| compare_by_keys(a, b, &right_keys));
492
493 let mut results = Vec::new();
494 let mut left_idx = 0;
495 let mut right_idx = 0;
496 let mut left_matched = vec![false; left_sorted.len()];
497 let mut right_matched = vec![false; right_sorted.len()];
498
499 while left_idx < left_sorted.len() && right_idx < right_sorted.len() {
500 let left_key = extract_key(&left_sorted[left_idx], &left_keys);
501 let right_key = extract_key(&right_sorted[right_idx], &right_keys);
502
503 match compare_keys(&left_key, &right_key) {
504 std::cmp::Ordering::Less => {
505 if matches!(join_type, JoinType::Left | JoinType::FullOuter)
507 && !left_matched[left_idx]
508 {
509 results.push(left_sorted[left_idx].clone());
510 }
511 left_idx += 1;
512 }
513 std::cmp::Ordering::Greater => {
514 if matches!(join_type, JoinType::Right | JoinType::FullOuter)
516 && !right_matched[right_idx]
517 {
518 results.push(right_sorted[right_idx].clone());
519 }
520 right_idx += 1;
521 }
522 std::cmp::Ordering::Equal => {
523 let match_start_right = right_idx;
525
526 while right_idx < right_sorted.len() {
528 let current_right_key = extract_key(&right_sorted[right_idx], &right_keys);
529 if compare_keys(&left_key, ¤t_right_key) != std::cmp::Ordering::Equal {
530 break;
531 }
532
533 left_matched[left_idx] = true;
534 right_matched[right_idx] = true;
535
536 let merged = merge_bindings(&left_sorted[left_idx], &right_sorted[right_idx]);
537 results.push(merged);
538 right_idx += 1;
539 }
540
541 left_idx += 1;
543 while left_idx < left_sorted.len() {
544 let current_left_key = extract_key(&left_sorted[left_idx], &left_keys);
545 if compare_keys(¤t_left_key, &left_key) != std::cmp::Ordering::Equal {
546 break;
547 }
548
549 for right_row in right_sorted.iter().take(right_idx).skip(match_start_right) {
551 left_matched[left_idx] = true;
552 let merged = merge_bindings(&left_sorted[left_idx], right_row);
553 results.push(merged);
554 }
555 left_idx += 1;
556 }
557
558 right_idx = match_start_right;
560 if left_idx >= left_sorted.len() || {
561 let next_left_key = extract_key(
562 &left_sorted[left_idx.min(left_sorted.len() - 1)],
563 &left_keys,
564 );
565 compare_keys(&next_left_key, &left_key) != std::cmp::Ordering::Equal
566 } {
567 while right_idx < right_sorted.len() {
569 let current_right_key = extract_key(&right_sorted[right_idx], &right_keys);
570 if compare_keys(&left_key, ¤t_right_key) != std::cmp::Ordering::Equal
571 {
572 break;
573 }
574 right_idx += 1;
575 }
576 }
577 }
578 }
579 }
580
581 while left_idx < left_sorted.len() {
583 if matches!(join_type, JoinType::Left | JoinType::FullOuter) && !left_matched[left_idx] {
584 results.push(left_sorted[left_idx].clone());
585 }
586 left_idx += 1;
587 }
588
589 while right_idx < right_sorted.len() {
590 if matches!(join_type, JoinType::Right | JoinType::FullOuter) && !right_matched[right_idx] {
591 results.push(right_sorted[right_idx].clone());
592 }
593 right_idx += 1;
594 }
595
596 results
597}
598
599fn compare_by_keys(a: &Binding, b: &Binding, keys: &[Var]) -> std::cmp::Ordering {
600 for key in keys {
601 match (a.get(key), b.get(key)) {
602 (Some(av), Some(bv)) => {
603 let cmp = total_compare_values(av, bv);
604 if cmp != std::cmp::Ordering::Equal {
605 return cmp;
606 }
607 }
608 (Some(_), None) => return std::cmp::Ordering::Less,
609 (None, Some(_)) => return std::cmp::Ordering::Greater,
610 (None, None) => {}
611 }
612 }
613 std::cmp::Ordering::Equal
614}
615
616fn compare_keys(a: &HashKey, b: &HashKey) -> std::cmp::Ordering {
617 for (av, bv) in a.0.iter().zip(b.0.iter()) {
618 match (av, bv) {
619 (Some(av), Some(bv)) => {
620 let cmp = total_compare_values(av, bv);
621 if cmp != std::cmp::Ordering::Equal {
622 return cmp;
623 }
624 }
625 (Some(_), None) => return std::cmp::Ordering::Less,
626 (None, Some(_)) => return std::cmp::Ordering::Greater,
627 (None, None) => {}
628 }
629 }
630 std::cmp::Ordering::Equal
631}
632
633fn merge_bindings(left: &Binding, right: &Binding) -> Binding {
639 if let Some(merged) = left.merge(right) {
642 merged
643 } else {
644 left.clone()
646 }
647}
648
649pub fn execute_join(
655 left: Vec<Binding>,
656 right: Vec<Binding>,
657 condition: JoinCondition,
658 join_type: JoinType,
659 stats: Option<JoinStats>,
660) -> Vec<Binding> {
661 let actual_stats = stats.unwrap_or(JoinStats {
663 left_cardinality: left.len(),
664 right_cardinality: right.len(),
665 left_sorted: false,
666 right_sorted: false,
667 condition_selectivity: 1.0,
668 });
669
670 let strategy = choose_strategy(&actual_stats, &condition);
671
672 match strategy {
673 JoinStrategy::Hash => hash_join(left, right, &condition, join_type),
674 JoinStrategy::NestedLoop => nested_loop_join(left, right, &condition, join_type),
675 JoinStrategy::Merge => merge_join(left, right, &condition, join_type),
676 }
677}
678
679#[cfg(test)]
684mod tests {
685 use super::*;
686
687 fn make_binding(pairs: &[(&str, &str)]) -> Binding {
688 if pairs.is_empty() {
690 return Binding::empty();
691 }
692
693 let mut result = Binding::one(Var::new(pairs[0].0), Value::String(pairs[0].1.to_string()));
694
695 for (k, v) in pairs.iter().skip(1) {
696 let next = Binding::one(Var::new(k), Value::String(v.to_string()));
697 result = result.merge(&next).unwrap_or(result);
698 }
699
700 result
701 }
702
703 #[test]
704 fn test_inner_join() {
705 let left = vec![
706 make_binding(&[("id", "1"), ("name", "Alice")]),
707 make_binding(&[("id", "2"), ("name", "Bob")]),
708 make_binding(&[("id", "3"), ("name", "Charlie")]),
709 ];
710
711 let right = vec![
712 make_binding(&[("user_id", "1"), ("score", "100")]),
713 make_binding(&[("user_id", "2"), ("score", "90")]),
714 make_binding(&[("user_id", "4"), ("score", "80")]),
715 ];
716
717 let condition = JoinCondition::Eq(Var::new("id"), Var::new("user_id"));
718 let results = execute_join(left, right, condition, JoinType::Inner, None);
719
720 assert_eq!(results.len(), 2);
721 assert!(results
722 .iter()
723 .any(|b| b.get(&Var::new("name")) == Some(&Value::String("Alice".to_string()))));
724 assert!(results
725 .iter()
726 .any(|b| b.get(&Var::new("name")) == Some(&Value::String("Bob".to_string()))));
727 }
728
729 #[test]
730 fn test_left_join() {
731 let left = vec![
732 make_binding(&[("id", "1"), ("name", "Alice")]),
733 make_binding(&[("id", "2"), ("name", "Bob")]),
734 make_binding(&[("id", "3"), ("name", "Charlie")]),
735 ];
736
737 let right = vec![make_binding(&[("user_id", "1"), ("score", "100")])];
738
739 let condition = JoinCondition::Eq(Var::new("id"), Var::new("user_id"));
740 let results = execute_join(left, right, condition, JoinType::Left, None);
741
742 assert_eq!(results.len(), 3); assert!(results
744 .iter()
745 .any(|b| b.get(&Var::new("name")) == Some(&Value::String("Charlie".to_string()))));
746 }
747
748 #[test]
749 fn test_right_join() {
750 let left = vec![make_binding(&[("id", "1"), ("name", "Alice")])];
751
752 let right = vec![
753 make_binding(&[("user_id", "1"), ("score", "100")]),
754 make_binding(&[("user_id", "2"), ("score", "90")]),
755 make_binding(&[("user_id", "3"), ("score", "80")]),
756 ];
757
758 let condition = JoinCondition::Eq(Var::new("id"), Var::new("user_id"));
759 let results = execute_join(left, right, condition, JoinType::Right, None);
760
761 assert_eq!(results.len(), 3); }
763
764 #[test]
765 fn test_cross_join() {
766 let left = vec![make_binding(&[("a", "1")]), make_binding(&[("a", "2")])];
767
768 let right = vec![
769 make_binding(&[("b", "x")]),
770 make_binding(&[("b", "y")]),
771 make_binding(&[("b", "z")]),
772 ];
773
774 let results = execute_join(left, right, JoinCondition::None, JoinType::Cross, None);
775
776 assert_eq!(results.len(), 6); }
778
779 #[test]
780 fn test_merge_join() {
781 let left = vec![
782 make_binding(&[("id", "1"), ("name", "Alice")]),
783 make_binding(&[("id", "2"), ("name", "Bob")]),
784 ];
785
786 let right = vec![
787 make_binding(&[("id", "1"), ("dept", "Eng")]),
788 make_binding(&[("id", "2"), ("dept", "Sales")]),
789 ];
790
791 let condition = JoinCondition::Eq(Var::new("id"), Var::new("id"));
792 let stats = JoinStats {
793 left_cardinality: 2,
794 right_cardinality: 2,
795 left_sorted: true,
796 right_sorted: true,
797 condition_selectivity: 1.0,
798 };
799
800 let results = execute_join(left, right, condition, JoinType::Inner, Some(stats));
801 assert_eq!(results.len(), 2);
802 }
803
804 #[test]
805 fn test_strategy_selection() {
806 let stats = JoinStats {
808 left_cardinality: 10,
809 right_cardinality: 10,
810 left_sorted: false,
811 right_sorted: false,
812 condition_selectivity: 1.0,
813 };
814 assert_eq!(
815 choose_strategy(&stats, &JoinCondition::Eq(Var::new("a"), Var::new("b"))),
816 JoinStrategy::NestedLoop
817 );
818
819 let stats = JoinStats {
821 left_cardinality: 10000,
822 right_cardinality: 10000,
823 left_sorted: false,
824 right_sorted: false,
825 condition_selectivity: 1.0,
826 };
827 assert_eq!(
828 choose_strategy(&stats, &JoinCondition::Eq(Var::new("a"), Var::new("b"))),
829 JoinStrategy::Hash
830 );
831
832 let stats = JoinStats {
834 left_cardinality: 1000,
835 right_cardinality: 1000,
836 left_sorted: true,
837 right_sorted: true,
838 condition_selectivity: 1.0,
839 };
840 assert_eq!(
841 choose_strategy(&stats, &JoinCondition::Eq(Var::new("a"), Var::new("b"))),
842 JoinStrategy::Merge
843 );
844 }
845
846 fn key_row(k: Option<Value>, tag_var: &str, tag: &str) -> Binding {
851 let row = Binding::one(Var::new(tag_var), Value::String(tag.to_string()));
852 match k {
853 Some(value) => row
854 .merge(&Binding::one(Var::new("k"), value))
855 .expect("no conflicting binding"),
856 None => row,
857 }
858 }
859
860 fn left_row(k: Option<Value>, tag: &str) -> Binding {
861 key_row(k, "lt", tag)
862 }
863
864 fn right_row(k: Option<Value>, tag: &str) -> Binding {
865 key_row(k, "rt", tag)
866 }
867
868 fn fingerprint(binding: &Binding) -> String {
871 let field = |name: &str| match binding.get(&Var::new(name)) {
872 Some(value) => format!("{value:?}"),
873 None => "-".to_string(),
874 };
875 format!("lt={} rt={} k={}", field("lt"), field("rt"), field("k"))
876 }
877
878 fn reference_inner_matches(left: &[Binding], right: &[Binding], keys: &[Var]) -> Vec<String> {
882 let mut out: Vec<String> = Vec::new();
883 for l in left {
884 for r in right {
885 if extract_key(l, keys) == extract_key(r, keys) {
886 out.push(fingerprint(&merge_bindings(l, r)));
887 }
888 }
889 }
890 out.sort();
891 out
892 }
893
894 #[test]
898 fn null_and_absent_keys_collide_in_hash_but_stay_distinct_in_equality() {
899 let keys = [Var::new("k")];
900 let null_row = left_row(Some(Value::Null), "null");
901 let absent_row = left_row(None, "absent");
902
903 let state = RandomState::new();
904 assert_eq!(
905 key_hash(&state, &null_row, &keys),
906 key_hash(&state, &absent_row, &keys),
907 "NULL and an absent column are expected to share a hash bucket"
908 );
909 assert!(key_matches(
910 &extract_key(&null_row, &keys),
911 &null_row,
912 &keys
913 ));
914 assert!(!key_matches(
915 &extract_key(&null_row, &keys),
916 &absent_row,
917 &keys
918 ));
919 assert!(!key_matches(
920 &extract_key(&absent_row, &keys),
921 &null_row,
922 &keys
923 ));
924 }
925
926 #[test]
930 fn hash_join_matches_are_identical_to_the_owned_key_reference() {
931 let left = vec![
932 left_row(Some(Value::String("a".into())), "L-a"),
933 left_row(Some(Value::Integer(1)), "L-1"),
934 left_row(Some(Value::Null), "L-null"),
935 left_row(None, "L-absent"),
936 left_row(Some(Value::String("a".into())), "L-a2"),
937 ];
938 let right = vec![
939 right_row(Some(Value::String("a".into())), "R-a"),
940 right_row(Some(Value::Null), "R-null"),
941 right_row(None, "R-absent"),
942 right_row(Some(Value::Boolean(true)), "R-true"),
943 right_row(Some(Value::Integer(1)), "R-1"),
944 right_row(Some(Value::Integer(1)), "R-1b"),
945 ];
946 let keys = [Var::new("k")];
947 let condition = JoinCondition::Eq(Var::new("k"), Var::new("k"));
948
949 for (l, r) in [
953 (left.clone(), right.clone()),
954 (right.clone(), left.clone()),
955 (left.clone(), right[..left.len()].to_vec()),
956 ] {
957 let expected = reference_inner_matches(&l, &r, &keys);
958
959 let mut actual: Vec<String> = hash_join(l, r, &condition, JoinType::Inner)
960 .iter()
961 .map(fingerprint)
962 .collect();
963 actual.sort();
964
965 assert_eq!(
966 actual, expected,
967 "hash join diverged from the owned-key reference"
968 );
969 }
970 }
971}