1use std::collections::HashSet;
19use std::fmt;
20use std::mem::size_of;
21use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
22use std::sync::{Arc, OnceLock};
23use std::vec;
24
25use crate::execution_plan::{
26 EmissionType, boundedness_from_children, has_same_children_properties,
27 plan_contains_expression_id, stub_properties,
28};
29use crate::filter_pushdown::{
30 ChildFilterDescription, ChildPushdownResult, FilterDescription, FilterPushdownPhase,
31 FilterPushdownPropagation,
32};
33use crate::joins::Map;
34use crate::joins::array_map::ArrayMap;
35use crate::joins::hash_join::inlist_builder::build_struct_inlist_values;
36use crate::joins::hash_join::shared_bounds::{
37 ColumnBounds, PartitionBounds, PushdownStrategy, SharedBuildAccumulator,
38};
39use crate::joins::hash_join::stream::{
40 BuildSide, BuildSideInitialState, HashJoinStream, HashJoinStreamState,
41};
42use crate::joins::join_hash_map::{JoinHashMapU32, JoinHashMapU64};
43use crate::joins::utils::{
44 OnceAsync, OnceFut, asymmetric_join_output_partitioning, reorder_output_after_swap,
45 swap_join_projection, update_hash,
46};
47use crate::joins::{JoinOn, JoinOnRef, PartitionMode, SharedBitmapBuilder};
48use crate::metrics::{Count, MetricBuilder, MetricCategory};
49use crate::projection::{
50 EmbeddedProjection, JoinData, ProjectionExec, try_embed_projection,
51 try_pushdown_through_join_with_column_indices,
52};
53use crate::repartition::REPARTITION_RANDOM_STATE;
54use crate::statistics::{ChildStats, StatisticsArgs};
55use crate::{
56 ChildrenPropertiesMode, ExecutionPlanProperties, ReplaceChildrenOptions,
57 validate_child_count,
58};
59use crate::{
60 DisplayAs, DisplayFormatType, Distribution, ExecutionPlan,
61 InputDistributionRequirements, Partitioning, PlanProperties,
62 SendableRecordBatchStream, Statistics,
63 common::can_project,
64 joins::utils::{
65 BuildProbeJoinMetrics, ColumnIndex, JoinFilter, JoinHashMapType,
66 build_join_schema, check_join_is_valid, estimate_join_statistics,
67 need_produce_result_in_final, symmetric_join_output_partitioning,
68 },
69 metrics::{ExecutionPlanMetricsSet, MetricsSet},
70};
71
72use arrow::array::{ArrayRef, BooleanBufferBuilder};
73use arrow::compute::concat_batches;
74use arrow::datatypes::SchemaRef;
75use arrow::record_batch::RecordBatch;
76use arrow::util::bit_util;
77use arrow_schema::{DataType, Schema};
78use datafusion_common::config::ConfigOptions;
79use datafusion_common::tree_node::TreeNodeRecursion;
80use datafusion_common::utils::memory::{RecordBatchMemoryCounter, estimate_memory_size};
81use datafusion_common::{
82 JoinSide, JoinType, NullEquality, Result, assert_or_internal_err, internal_err,
83 plan_err, project_schema,
84};
85use datafusion_execution::TaskContext;
86use datafusion_execution::memory_pool::{MemoryConsumer, MemoryReservation};
87use datafusion_expr::Accumulator;
88use datafusion_functions_aggregate_common::min_max::{MaxAccumulator, MinAccumulator};
89use datafusion_physical_expr::equivalence::{
90 ProjectionMapping, join_equivalence_properties,
91};
92use datafusion_physical_expr::expressions::{Column, DynamicFilterPhysicalExpr, lit};
93use datafusion_physical_expr::projection::{ProjectionRef, combine_projections};
94use datafusion_physical_expr::{PhysicalExpr, PhysicalExprRef};
95
96use datafusion_common::hash_utils::RandomState;
97use datafusion_physical_expr_common::physical_expr::fmt_sql;
98use datafusion_physical_expr_common::utils::evaluate_expressions_to_arrays;
99use futures::TryStreamExt;
100use parking_lot::Mutex;
101
102use super::partitioned_hash_eval::SeededRandomState;
103
104pub(crate) const HASH_JOIN_SEED: SeededRandomState =
106 SeededRandomState::with_seed(12210250226015887276);
107
108const ARRAY_MAP_CREATED_COUNT_METRIC_NAME: &str = "array_map_created_count";
109
110#[expect(clippy::too_many_arguments)]
111fn try_create_array_map(
112 bounds: &Option<PartitionBounds>,
113 schema: &SchemaRef,
114 batches: &[RecordBatch],
115 on_left: &[PhysicalExprRef],
116 reservation: &mut MemoryReservation,
117 perfect_hash_join_small_build_threshold: usize,
118 perfect_hash_join_min_key_density: f64,
119 null_equality: NullEquality,
120) -> Result<Option<(ArrayMap, RecordBatch, Vec<ArrayRef>)>> {
121 if on_left.len() != 1 {
122 return Ok(None);
123 }
124
125 if null_equality == NullEquality::NullEqualsNull {
126 for batch in batches.iter() {
127 let arrays = evaluate_expressions_to_arrays(on_left, batch)?;
128 if arrays[0].null_count() > 0 {
129 return Ok(None);
130 }
131 }
132 }
133
134 let (min_val, max_val) = if let Some(bounds) = bounds {
135 let (min_val, max_val) = if let Some(cb) = bounds.get_column_bounds(0) {
136 (cb.min.clone(), cb.max.clone())
137 } else {
138 return Ok(None);
139 };
140
141 if min_val.is_null() || max_val.is_null() {
142 return Ok(None);
143 }
144
145 if min_val > max_val {
146 return internal_err!("min_val>max_val");
147 }
148
149 if let Some((mi, ma)) =
150 ArrayMap::key_to_u64(&min_val).zip(ArrayMap::key_to_u64(&max_val))
151 {
152 (mi, ma)
153 } else {
154 return Ok(None);
155 }
156 } else {
157 return Ok(None);
158 };
159
160 let range = ArrayMap::calculate_range(min_val, max_val);
161 let num_row: usize = batches.iter().map(|x| x.num_rows()).sum();
162
163 if num_row >= u32::MAX as usize {
165 return Ok(None);
166 }
167
168 if range == usize::MAX as u64 {
171 return Ok(None);
172 }
173
174 let dense_ratio = (num_row as f64) / ((range + 1) as f64);
175
176 if range >= perfect_hash_join_small_build_threshold as u64
177 && dense_ratio <= perfect_hash_join_min_key_density
178 {
179 return Ok(None);
180 }
181
182 let mem_size = ArrayMap::estimate_memory_size(min_val, max_val, num_row);
183 reservation.try_grow(mem_size)?;
184
185 let batch = concat_batches(schema, batches)?;
186 let left_values = evaluate_expressions_to_arrays(on_left, &batch)?;
187
188 let array_map = ArrayMap::try_new(&left_values[0], min_val, max_val)?;
189
190 Ok(Some((array_map, batch, left_values)))
191}
192
193pub(super) struct JoinLeftData {
195 pub(super) map: Arc<Map>,
198 batch: RecordBatch,
200 values: Vec<ArrayRef>,
202 visited_indices_bitmap: SharedBitmapBuilder,
204 probe_threads_counter: AtomicUsize,
207 _reservation: MemoryReservation,
212 pub(super) bounds: Option<PartitionBounds>,
216 pub(super) membership: PushdownStrategy,
219 pub(super) probe_side_non_empty: AtomicBool,
222 pub(super) probe_side_has_null: AtomicBool,
224}
225
226impl JoinLeftData {
227 pub(super) fn map(&self) -> &Map {
229 &self.map
230 }
231
232 pub(super) fn batch(&self) -> &RecordBatch {
234 &self.batch
235 }
236
237 pub(super) fn has_build_rows(&self) -> bool {
242 self.batch().num_rows() > 0
243 }
244
245 pub(super) fn has_matchable_build_rows(&self) -> bool {
251 !self.map().is_empty()
252 }
253
254 pub(super) fn values(&self) -> &[ArrayRef] {
256 &self.values
257 }
258
259 pub(super) fn visited_indices_bitmap(&self) -> &SharedBitmapBuilder {
261 &self.visited_indices_bitmap
262 }
263
264 pub(super) fn membership(&self) -> &PushdownStrategy {
266 &self.membership
267 }
268
269 pub(super) fn report_probe_completed(&self) -> bool {
272 self.probe_threads_counter.fetch_sub(1, Ordering::Relaxed) == 1
273 }
274}
275
276pub struct HashJoinExecBuilder {
289 exec: HashJoinExec,
290 preserve_properties: bool,
291}
292
293impl HashJoinExecBuilder {
294 pub fn new(
296 left: Arc<dyn ExecutionPlan>,
297 right: Arc<dyn ExecutionPlan>,
298 on: Vec<(PhysicalExprRef, PhysicalExprRef)>,
299 join_type: JoinType,
300 ) -> Self {
301 Self {
302 exec: HashJoinExec {
303 left,
304 right,
305 on,
306 filter: None,
307 join_type,
308 left_fut: Default::default(),
309 random_state: HASH_JOIN_SEED,
310 mode: PartitionMode::Auto,
311 fetch: None,
312 metrics: ExecutionPlanMetricsSet::new(),
313 projection: None,
314 column_indices: vec![],
315 null_equality: NullEquality::NullEqualsNothing,
316 null_aware: false,
317 dynamic_filter: None,
318 cache: stub_properties(),
320 join_schema: Arc::new(Schema::empty()),
321 },
322 preserve_properties: false,
325 }
326 }
327
328 pub fn with_type(mut self, join_type: JoinType) -> Self {
330 self.exec.join_type = join_type;
331 self.preserve_properties = false;
332 self
333 }
334
335 pub fn with_projection(self, projection: Option<Vec<usize>>) -> Self {
337 self.with_projection_ref(projection.map(Into::into))
338 }
339
340 pub fn with_projection_ref(mut self, projection: Option<ProjectionRef>) -> Self {
342 self.exec.projection = projection;
343 self.preserve_properties = false;
344 self
345 }
346
347 pub fn with_filter(mut self, filter: Option<JoinFilter>) -> Self {
349 self.exec.filter = filter;
350 self
351 }
352
353 pub fn with_on(mut self, on: Vec<(PhysicalExprRef, PhysicalExprRef)>) -> Self {
355 self.exec.on = on;
356 self.preserve_properties = false;
357 self
358 }
359
360 pub fn with_partition_mode(mut self, mode: PartitionMode) -> Self {
362 self.exec.mode = mode;
363 self.preserve_properties = false;
364 self
365 }
366
367 pub fn with_null_equality(mut self, null_equality: NullEquality) -> Self {
369 self.exec.null_equality = null_equality;
370 self
371 }
372
373 pub fn with_null_aware(mut self, null_aware: bool) -> Self {
375 self.exec.null_aware = null_aware;
376 self
377 }
378
379 pub fn with_fetch(mut self, fetch: Option<usize>) -> Self {
381 self.exec.fetch = fetch;
382 self
383 }
384
385 pub fn recompute_properties(mut self) -> Self {
387 self.preserve_properties = false;
388 self
389 }
390
391 pub fn with_new_children(
393 mut self,
394 mut children: Vec<Arc<dyn ExecutionPlan>>,
395 ) -> Result<Self> {
396 assert_or_internal_err!(
397 children.len() == 2,
398 "wrong number of children passed into `HashJoinExecBuilder`"
399 );
400 self.preserve_properties &= has_same_children_properties(&self.exec, &children)?;
401 self.exec.right = children.swap_remove(1);
402 self.exec.left = children.swap_remove(0);
403 Ok(self)
404 }
405
406 pub fn reset_state(mut self) -> Self {
408 self.exec.left_fut = Default::default();
409 self.exec.dynamic_filter = None;
410 self.exec.metrics = ExecutionPlanMetricsSet::new();
411 self
412 }
413
414 pub fn build_exec(self) -> Result<Arc<dyn ExecutionPlan>> {
416 self.build().map(|p| Arc::new(p) as _)
417 }
418
419 pub fn build(self) -> Result<HashJoinExec> {
421 let Self {
422 exec,
423 preserve_properties,
424 } = self;
425
426 if exec.null_aware {
428 let join_type = exec.join_type();
429 if !matches!(join_type, JoinType::LeftAnti) {
430 return plan_err!(
431 "null_aware can only be true for LeftAnti joins, got {join_type}"
432 );
433 }
434 let on = exec.on();
435 if on.len() != 1 {
436 return plan_err!(
437 "null_aware anti join only supports single column join key, got {} columns",
438 on.len()
439 );
440 }
441 }
442
443 if preserve_properties {
444 return Ok(exec);
445 }
446
447 let HashJoinExec {
448 left,
449 right,
450 on,
451 filter,
452 join_type,
453 left_fut,
454 random_state,
455 mode,
456 metrics,
457 projection,
458 null_equality,
459 null_aware,
460 dynamic_filter,
461 fetch,
462 join_schema: _,
464 column_indices: _,
465 cache: _,
466 } = exec;
467
468 let left_schema = left.schema();
469 let right_schema = right.schema();
470 if on.is_empty() {
471 return plan_err!("On constraints in HashJoinExec should be non-empty");
472 }
473
474 check_join_is_valid(&left_schema, &right_schema, &on)?;
475 let (join_schema, column_indices) =
476 build_join_schema(&left_schema, &right_schema, &join_type);
477
478 let join_schema = Arc::new(join_schema);
479
480 can_project(&join_schema, projection.as_deref())?;
482
483 let cache = HashJoinExec::compute_properties(
484 &left,
485 &right,
486 &join_schema,
487 join_type,
488 &on,
489 mode,
490 projection.as_deref(),
491 )?;
492
493 Ok(HashJoinExec {
494 left,
495 right,
496 on,
497 filter,
498 join_type,
499 join_schema,
500 left_fut,
501 random_state,
502 mode,
503 metrics,
504 projection,
505 column_indices,
506 null_equality,
507 null_aware,
508 cache: Arc::new(cache),
509 dynamic_filter,
510 fetch,
511 })
512 }
513
514 fn with_dynamic_filter(mut self, filter: Option<HashJoinExecDynamicFilter>) -> Self {
515 self.exec.dynamic_filter = filter;
516 self
517 }
518}
519
520impl From<&HashJoinExec> for HashJoinExecBuilder {
521 fn from(exec: &HashJoinExec) -> Self {
522 Self {
523 exec: HashJoinExec {
524 left: Arc::clone(exec.left()),
525 right: Arc::clone(exec.right()),
526 on: exec.on.clone(),
527 filter: exec.filter.clone(),
528 join_type: exec.join_type,
529 join_schema: Arc::clone(&exec.join_schema),
530 left_fut: Arc::clone(&exec.left_fut),
531 random_state: exec.random_state.clone(),
532 mode: exec.mode,
533 metrics: exec.metrics.clone(),
534 projection: exec.projection.clone(),
535 column_indices: exec.column_indices.clone(),
536 null_equality: exec.null_equality,
537 null_aware: exec.null_aware,
538 cache: Arc::clone(&exec.cache),
539 dynamic_filter: exec.dynamic_filter.clone(),
540 fetch: exec.fetch,
541 },
542 preserve_properties: true,
543 }
544 }
545}
546
547#[expect(rustdoc::private_intra_doc_links)]
548pub struct HashJoinExec {
740 pub left: Arc<dyn ExecutionPlan>,
742 pub right: Arc<dyn ExecutionPlan>,
744 pub on: Vec<(PhysicalExprRef, PhysicalExprRef)>,
746 pub filter: Option<JoinFilter>,
748 pub join_type: JoinType,
750 join_schema: SchemaRef,
753 left_fut: Arc<OnceAsync<JoinLeftData>>,
760 random_state: SeededRandomState,
762 pub mode: PartitionMode,
764 metrics: ExecutionPlanMetricsSet,
766 pub projection: Option<ProjectionRef>,
768 column_indices: Vec<ColumnIndex>,
770 pub null_equality: NullEquality,
772 pub null_aware: bool,
774 cache: Arc<PlanProperties>,
776 dynamic_filter: Option<HashJoinExecDynamicFilter>,
780 fetch: Option<usize>,
782}
783
784#[derive(Clone)]
785struct HashJoinExecDynamicFilter {
786 filter: Arc<DynamicFilterPhysicalExpr>,
788 build_accumulator: OnceLock<Arc<SharedBuildAccumulator>>,
791}
792
793impl fmt::Debug for HashJoinExec {
794 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
795 f.debug_struct("HashJoinExec")
796 .field("left", &self.left)
797 .field("right", &self.right)
798 .field("on", &self.on)
799 .field("filter", &self.filter)
800 .field("join_type", &self.join_type)
801 .field("join_schema", &self.join_schema)
802 .field("left_fut", &self.left_fut)
803 .field("random_state", &self.random_state)
804 .field("mode", &self.mode)
805 .field("metrics", &self.metrics)
806 .field("projection", &self.projection)
807 .field("column_indices", &self.column_indices)
808 .field("null_equality", &self.null_equality)
809 .field("cache", &self.cache)
810 .finish()
812 }
813}
814
815impl EmbeddedProjection for HashJoinExec {
816 fn with_projection(&self, projection: Option<Vec<usize>>) -> Result<Self> {
817 self.with_projection(projection)
818 }
819}
820
821impl HashJoinExec {
822 #[expect(clippy::too_many_arguments)]
827 pub fn try_new(
828 left: Arc<dyn ExecutionPlan>,
829 right: Arc<dyn ExecutionPlan>,
830 on: JoinOn,
831 filter: Option<JoinFilter>,
832 join_type: &JoinType,
833 projection: Option<Vec<usize>>,
834 partition_mode: PartitionMode,
835 null_equality: NullEquality,
836 null_aware: bool,
837 ) -> Result<Self> {
838 HashJoinExecBuilder::new(left, right, on, *join_type)
839 .with_filter(filter)
840 .with_projection(projection)
841 .with_partition_mode(partition_mode)
842 .with_null_equality(null_equality)
843 .with_null_aware(null_aware)
844 .build()
845 }
846
847 pub fn builder(&self) -> HashJoinExecBuilder {
853 self.into()
854 }
855
856 fn create_dynamic_filter(on: &JoinOn) -> Arc<DynamicFilterPhysicalExpr> {
857 let right_keys: Vec<_> = on.iter().map(|(_, r)| Arc::clone(r)).collect();
860 Arc::new(DynamicFilterPhysicalExpr::new(right_keys, lit(true)))
862 }
863
864 fn allow_join_dynamic_filter_pushdown(&self, config: &ConfigOptions) -> bool {
865 let (_, probe_preserved) = self.join_type.on_lr_is_preserved();
866 if !probe_preserved || !config.optimizer.enable_join_dynamic_filter_pushdown {
867 return false;
868 }
869
870 if self.null_aware
875 && self.on.iter().any(|(build_key, _)| {
876 build_key.nullable(&self.left.schema()).unwrap_or(true)
877 })
878 {
879 return false;
880 }
881
882 if config.optimizer.preserve_file_partitions > 0
892 && self.mode == PartitionMode::Partitioned
893 && matches!(
894 (
895 self.left.output_partitioning(),
896 self.right.output_partitioning()
897 ),
898 (Partitioning::Hash(_, _), Partitioning::Hash(_, _))
899 )
900 {
901 return false;
902 }
903
904 if self.mode == PartitionMode::Partitioned
905 && !self.has_partitioned_dynamic_filter_routing()
906 {
907 return false;
908 }
909
910 true
911 }
912
913 fn has_partitioned_dynamic_filter_routing(&self) -> bool {
914 match (
915 self.left.output_partitioning(),
916 self.right.output_partitioning(),
917 ) {
918 (
919 Partitioning::Hash(_, left_partition_count),
920 Partitioning::Hash(_, right_partition_count),
921 ) => left_partition_count == right_partition_count,
922 (Partitioning::Range(_), Partitioning::Range(_)) => {
923 let children = [self.left.as_ref(), self.right.as_ref()];
924 matches!(
925 self.input_distribution_requirements()
926 .unsatisfied_co_partitioned_children(self.name(), &children),
927 Ok(unsatisfied) if unsatisfied.is_empty()
928 )
929 }
930 (left_partitioning, right_partitioning) => {
931 left_partitioning.partition_count() == 1
932 && right_partitioning.partition_count() == 1
933 }
934 }
935 }
936
937 pub fn left(&self) -> &Arc<dyn ExecutionPlan> {
939 &self.left
940 }
941
942 pub fn right(&self) -> &Arc<dyn ExecutionPlan> {
944 &self.right
945 }
946
947 pub fn on(&self) -> &[(PhysicalExprRef, PhysicalExprRef)] {
949 &self.on
950 }
951
952 pub fn filter(&self) -> Option<&JoinFilter> {
954 self.filter.as_ref()
955 }
956
957 pub fn join_type(&self) -> &JoinType {
959 &self.join_type
960 }
961
962 pub fn join_schema(&self) -> &SchemaRef {
965 &self.join_schema
966 }
967
968 pub fn partition_mode(&self) -> &PartitionMode {
970 &self.mode
971 }
972
973 pub fn null_equality(&self) -> NullEquality {
975 self.null_equality
976 }
977
978 #[deprecated(
980 since = "55.0.0",
981 note = "Use ExecutionPlan::dynamic_expressions_produced instead"
982 )]
983 pub fn dynamic_filter_expr(&self) -> Option<&Arc<DynamicFilterPhysicalExpr>> {
984 self.dynamic_filter.as_ref().map(|df| &df.filter)
985 }
986
987 pub fn with_dynamic_filter_expr(
994 mut self,
995 filter: Arc<DynamicFilterPhysicalExpr>,
996 ) -> Result<Self> {
997 let probe_schema = self.right.schema();
998 for child in filter.children() {
999 child.data_type(&probe_schema)?;
1000 }
1001 self.dynamic_filter = Some(HashJoinExecDynamicFilter {
1002 filter,
1003 build_accumulator: OnceLock::new(),
1006 });
1007 Ok(self)
1008 }
1009
1010 fn maintains_input_order(join_type: JoinType) -> Vec<bool> {
1012 vec![
1013 false,
1014 matches!(
1015 join_type,
1016 JoinType::Inner
1017 | JoinType::Right
1018 | JoinType::RightAnti
1019 | JoinType::RightSemi
1020 | JoinType::RightMark
1021 ),
1022 ]
1023 }
1024
1025 pub fn probe_side() -> JoinSide {
1027 JoinSide::Right
1029 }
1030
1031 pub fn contains_projection(&self) -> bool {
1033 self.projection.is_some()
1034 }
1035
1036 pub fn with_projection(&self, projection: Option<Vec<usize>>) -> Result<Self> {
1038 let projection = projection.map(Into::into);
1039 can_project(&self.schema(), projection.as_deref())?;
1041 let projection =
1042 combine_projections(projection.as_ref(), self.projection.as_ref())?;
1043 self.builder().with_projection_ref(projection).build()
1044 }
1045
1046 fn compute_properties(
1048 left: &Arc<dyn ExecutionPlan>,
1049 right: &Arc<dyn ExecutionPlan>,
1050 schema: &SchemaRef,
1051 join_type: JoinType,
1052 on: JoinOnRef,
1053 mode: PartitionMode,
1054 projection: Option<&[usize]>,
1055 ) -> Result<PlanProperties> {
1056 let mut eq_properties = join_equivalence_properties(
1058 left.equivalence_properties().clone(),
1059 right.equivalence_properties().clone(),
1060 &join_type,
1061 Arc::clone(schema),
1062 &Self::maintains_input_order(join_type),
1063 Some(Self::probe_side()),
1064 on,
1065 )?;
1066
1067 let mut output_partitioning = match mode {
1068 PartitionMode::CollectLeft => {
1069 asymmetric_join_output_partitioning(left, right, &join_type)?
1070 }
1071 PartitionMode::Auto => Partitioning::UnknownPartitioning(
1072 right.output_partitioning().partition_count(),
1073 ),
1074 PartitionMode::Partitioned => {
1075 symmetric_join_output_partitioning(left, right, &join_type)?
1076 }
1077 };
1078
1079 let emission_type = if left.boundedness().is_unbounded() {
1080 EmissionType::Final
1081 } else if right.pipeline_behavior() == EmissionType::Incremental {
1082 match join_type {
1083 JoinType::Inner
1086 | JoinType::LeftSemi
1087 | JoinType::RightSemi
1088 | JoinType::Right
1089 | JoinType::RightAnti
1090 | JoinType::RightMark => EmissionType::Incremental,
1091 JoinType::Left
1094 | JoinType::LeftAnti
1095 | JoinType::LeftMark
1096 | JoinType::Full => EmissionType::Both,
1097 }
1098 } else {
1099 right.pipeline_behavior()
1100 };
1101
1102 if let Some(projection) = projection {
1104 let projection_mapping = ProjectionMapping::from_indices(projection, schema)?;
1106 let out_schema = project_schema(schema, Some(&projection))?;
1107 output_partitioning =
1108 output_partitioning.project(&projection_mapping, &eq_properties);
1109 eq_properties = eq_properties.project(&projection_mapping, out_schema);
1110 }
1111
1112 Ok(PlanProperties::new(
1113 eq_properties,
1114 output_partitioning,
1115 emission_type,
1116 boundedness_from_children([left, right]),
1117 ))
1118 }
1119
1120 pub fn swap_inputs(
1144 &self,
1145 partition_mode: PartitionMode,
1146 ) -> Result<Arc<dyn ExecutionPlan>> {
1147 assert_or_internal_err!(
1148 self.dynamic_filter.is_none(),
1149 "Cannot swap HashJoinExec inputs after dynamic filters have been constructed. \
1150 Optimizer rules that reorder join inputs must run before optimizer rules `FilterPushdown::new_post_optimization()`"
1151 );
1152
1153 let left = self.left();
1154 let right = self.right();
1155 let new_join = self
1156 .builder()
1157 .with_type(self.join_type.swap())
1158 .with_new_children(vec![Arc::clone(right), Arc::clone(left)])?
1159 .with_on(
1160 self.on()
1161 .iter()
1162 .map(|(l, r)| (Arc::clone(r), Arc::clone(l)))
1163 .collect(),
1164 )
1165 .with_filter(self.filter().map(JoinFilter::swap))
1166 .with_projection(swap_join_projection(
1167 left.schema().fields().len(),
1168 right.schema().fields().len(),
1169 self.projection.as_deref(),
1170 self.join_type(),
1171 ))
1172 .with_partition_mode(partition_mode)
1173 .build()?;
1174 if matches!(
1176 self.join_type(),
1177 JoinType::LeftSemi
1178 | JoinType::RightSemi
1179 | JoinType::LeftAnti
1180 | JoinType::RightAnti
1181 | JoinType::LeftMark
1182 | JoinType::RightMark
1183 ) || self.projection.is_some()
1184 {
1185 Ok(Arc::new(new_join))
1186 } else {
1187 reorder_output_after_swap(Arc::new(new_join), &left.schema(), &right.schema())
1188 }
1189 }
1190}
1191
1192impl DisplayAs for HashJoinExec {
1193 fn fmt_as(&self, t: DisplayFormatType, f: &mut fmt::Formatter) -> fmt::Result {
1194 match t {
1195 DisplayFormatType::Default | DisplayFormatType::Verbose => {
1196 let display_filter = self.filter.as_ref().map_or_else(
1197 || "".to_string(),
1198 |f| format!(", filter={}", f.expression()),
1199 );
1200 let display_projections = if self.contains_projection() {
1201 format!(
1202 ", projection=[{}]",
1203 self.projection
1204 .as_ref()
1205 .unwrap()
1206 .iter()
1207 .map(|index| format!(
1208 "{}@{}",
1209 self.join_schema.fields().get(*index).unwrap().name(),
1210 index
1211 ))
1212 .collect::<Vec<_>>()
1213 .join(", ")
1214 )
1215 } else {
1216 "".to_string()
1217 };
1218 let display_null_equality =
1219 if self.null_equality() == NullEquality::NullEqualsNull {
1220 ", NullsEqual: true"
1221 } else {
1222 ""
1223 };
1224 let display_fetch = self
1225 .fetch
1226 .map_or_else(String::new, |f| format!(", fetch={f}"));
1227 let display_null_aware =
1228 if self.null_aware { ", null_aware" } else { "" };
1229 let on = self
1230 .on
1231 .iter()
1232 .map(|(c1, c2)| format!("({c1}, {c2})"))
1233 .collect::<Vec<String>>()
1234 .join(", ");
1235 write!(
1236 f,
1237 "HashJoinExec: mode={:?}, join_type={:?}, on=[{}]{}{}{}{}{}",
1238 self.mode,
1239 self.join_type,
1240 on,
1241 display_filter,
1242 display_projections,
1243 display_null_equality,
1244 display_fetch,
1245 display_null_aware,
1246 )
1247 }
1248 DisplayFormatType::TreeRender => {
1249 let on = self
1250 .on
1251 .iter()
1252 .map(|(c1, c2)| {
1253 format!("({} = {})", fmt_sql(c1.as_ref()), fmt_sql(c2.as_ref()))
1254 })
1255 .collect::<Vec<String>>()
1256 .join(", ");
1257
1258 if *self.join_type() != JoinType::Inner {
1259 writeln!(f, "join_type={:?}", self.join_type)?;
1260 }
1261
1262 writeln!(f, "on={on}")?;
1263
1264 if self.null_equality() == NullEquality::NullEqualsNull {
1265 writeln!(f, "NullsEqual: true")?;
1266 }
1267
1268 if self.null_aware {
1269 writeln!(f, "null_aware")?;
1270 }
1271
1272 if let Some(filter) = self.filter.as_ref() {
1273 writeln!(f, "filter={filter}")?;
1274 }
1275
1276 if let Some(fetch) = self.fetch {
1277 writeln!(f, "fetch={fetch}")?;
1278 }
1279
1280 Ok(())
1281 }
1282 }
1283 }
1284}
1285
1286impl ExecutionPlan for HashJoinExec {
1287 fn name(&self) -> &'static str {
1288 "HashJoinExec"
1289 }
1290
1291 fn properties(&self) -> &Arc<PlanProperties> {
1292 &self.cache
1293 }
1294
1295 fn required_input_distribution(&self) -> Vec<Distribution> {
1296 self.input_distribution_requirements().into_per_child()
1297 }
1298
1299 fn input_distribution_requirements(&self) -> InputDistributionRequirements {
1300 match self.mode {
1301 PartitionMode::Partitioned => {
1302 let (left_expr, right_expr) = self
1303 .on
1304 .iter()
1305 .map(|(l, r)| (Arc::clone(l), Arc::clone(r)))
1306 .unzip();
1307 InputDistributionRequirements::co_partitioned(vec![
1308 Distribution::KeyPartitioned(left_expr),
1309 Distribution::KeyPartitioned(right_expr),
1310 ])
1311 }
1312 PartitionMode::CollectLeft => InputDistributionRequirements::new(vec![
1313 Distribution::SinglePartition,
1314 Distribution::UnspecifiedDistribution,
1315 ]),
1316 PartitionMode::Auto => InputDistributionRequirements::new(vec![
1317 Distribution::UnspecifiedDistribution,
1318 Distribution::UnspecifiedDistribution,
1319 ]),
1320 }
1321 }
1322
1323 fn maintains_input_order(&self) -> Vec<bool> {
1340 Self::maintains_input_order(self.join_type)
1341 }
1342
1343 fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
1344 vec![&self.left, &self.right]
1345 }
1346
1347 fn apply_expressions(
1348 &self,
1349 f: &mut dyn FnMut(&Arc<dyn PhysicalExpr>) -> Result<TreeNodeRecursion>,
1350 ) -> Result<TreeNodeRecursion> {
1351 let join_keys = self
1352 .on
1353 .iter()
1354 .flat_map(|(left, right)| [Arc::clone(left), Arc::clone(right)]);
1355 let filter = self
1356 .filter
1357 .iter()
1358 .map(|filter| Arc::clone(filter.expression()));
1359 let dynamic_filter = self.dynamic_filter.iter().map(|dynamic_filter| {
1360 Arc::<DynamicFilterPhysicalExpr>::clone(&dynamic_filter.filter)
1361 as Arc<dyn PhysicalExpr>
1362 });
1363 crate::apply_expression_roots(join_keys.chain(filter).chain(dynamic_filter), f)
1364 }
1365
1366 fn dynamic_expressions_produced(&self) -> Vec<Arc<dyn PhysicalExpr>> {
1367 self.dynamic_filter
1368 .iter()
1369 .map(|dynamic_filter| {
1370 Arc::<DynamicFilterPhysicalExpr>::clone(&dynamic_filter.filter)
1371 as Arc<dyn PhysicalExpr>
1372 })
1373 .collect()
1374 }
1375
1376 fn replace_children(
1382 self: Arc<Self>,
1383 children: Vec<Arc<dyn ExecutionPlan>>,
1384 options: ReplaceChildrenOptions,
1385 ) -> Result<Arc<dyn ExecutionPlan>> {
1386 validate_child_count!(self, children);
1387 match options.children_properties {
1388 ChildrenPropertiesMode::Keep => {
1389 self.builder().with_new_children(children)?.build_exec()
1390 }
1391 ChildrenPropertiesMode::Recompute => self
1392 .builder()
1393 .recompute_properties()
1394 .with_new_children(children)?
1395 .build_exec(),
1396 }
1397 }
1398
1399 fn with_new_children(
1400 self: Arc<Self>,
1401 children: Vec<Arc<dyn ExecutionPlan>>,
1402 ) -> Result<Arc<dyn ExecutionPlan>> {
1403 self.replace_children(
1404 children,
1405 ReplaceChildrenOptions::new(ChildrenPropertiesMode::Recompute),
1406 )
1407 }
1408
1409 fn reset_state(self: Arc<Self>) -> Result<Arc<dyn ExecutionPlan>> {
1410 self.builder().reset_state().build_exec()
1411 }
1412
1413 fn execute(
1414 &self,
1415 partition: usize,
1416 context: Arc<TaskContext>,
1417 ) -> Result<SendableRecordBatchStream> {
1418 let on_left = self
1419 .on
1420 .iter()
1421 .map(|on| Arc::clone(&on.0))
1422 .collect::<Vec<_>>();
1423 let left_partitions = self.left.output_partitioning().partition_count();
1424 let right_partitions = self.right.output_partitioning().partition_count();
1425
1426 assert_or_internal_err!(
1427 self.mode != PartitionMode::Partitioned
1428 || left_partitions == right_partitions,
1429 "Invalid HashJoinExec, partition count mismatch {left_partitions}!={right_partitions},\
1430 consider using RepartitionExec"
1431 );
1432
1433 assert_or_internal_err!(
1434 self.mode != PartitionMode::CollectLeft || left_partitions == 1,
1435 "Invalid HashJoinExec, the output partition count of the left child must be 1 in CollectLeft mode,\
1436 consider using CoalescePartitionsExec or the EnforceDistribution rule"
1437 );
1438
1439 let enable_dynamic_filter_pushdown = if self
1442 .allow_join_dynamic_filter_pushdown(context.session_config().options())
1443 {
1444 self.dynamic_filter
1445 .as_ref()
1446 .and_then(|df| df.filter.expression_id())
1447 .map(|id| plan_contains_expression_id(&self.right, id))
1448 .transpose()?
1449 .unwrap_or(false)
1450 } else {
1451 false
1452 };
1453
1454 let join_metrics = BuildProbeJoinMetrics::new(partition, &self.metrics);
1455
1456 let array_map_created_count = MetricBuilder::new(&self.metrics)
1457 .with_category(MetricCategory::Rows)
1458 .counter(ARRAY_MAP_CREATED_COUNT_METRIC_NAME, partition);
1459
1460 let repartition_random_state = REPARTITION_RANDOM_STATE;
1463 let build_accumulator = enable_dynamic_filter_pushdown
1464 .then(|| {
1465 self.dynamic_filter.as_ref().map(|df| {
1466 let filter = Arc::clone(&df.filter);
1467 let on_right = self
1468 .on
1469 .iter()
1470 .map(|(_, right_expr)| Arc::clone(right_expr))
1471 .collect::<Vec<_>>();
1472 Some(Arc::clone(df.build_accumulator.get_or_init(|| {
1473 Arc::new(SharedBuildAccumulator::new_from_partition_mode(
1474 self.mode,
1475 self.left.as_ref(),
1476 self.right.as_ref(),
1477 filter,
1478 on_right,
1479 repartition_random_state,
1480 self.null_equality,
1481 self.null_aware,
1482 ))
1483 })))
1484 })
1485 })
1486 .flatten()
1487 .flatten();
1488
1489 let left_fut = match self.mode {
1490 PartitionMode::CollectLeft => self.left_fut.try_once(|| {
1491 let left_stream = self.left.execute(0, Arc::clone(&context))?;
1492
1493 let reservation =
1494 MemoryConsumer::new("HashJoinInput").register(context.memory_pool());
1495
1496 Ok(collect_left_input(
1497 self.random_state.random_state().clone(),
1498 left_stream,
1499 on_left.clone(),
1500 join_metrics.clone(),
1501 reservation,
1502 need_produce_result_in_final(self.join_type),
1503 self.right().output_partitioning().partition_count(),
1504 enable_dynamic_filter_pushdown,
1505 Arc::clone(context.session_config().options()),
1506 self.null_equality,
1507 array_map_created_count,
1508 ))
1509 })?,
1510 PartitionMode::Partitioned => {
1511 let left_stream = self.left.execute(partition, Arc::clone(&context))?;
1512
1513 let reservation =
1514 MemoryConsumer::new(format!("HashJoinInput[{partition}]"))
1515 .register(context.memory_pool());
1516 OnceFut::new(collect_left_input(
1517 self.random_state.random_state().clone(),
1518 left_stream,
1519 on_left.clone(),
1520 join_metrics.clone(),
1521 reservation,
1522 need_produce_result_in_final(self.join_type),
1523 1,
1524 enable_dynamic_filter_pushdown,
1525 Arc::clone(context.session_config().options()),
1526 self.null_equality,
1527 array_map_created_count,
1528 ))
1529 }
1530 PartitionMode::Auto => {
1531 return plan_err!(
1532 "Invalid HashJoinExec, unsupported PartitionMode {:?} in execute()",
1533 PartitionMode::Auto
1534 );
1535 }
1536 };
1537
1538 let batch_size = context.session_config().batch_size();
1539
1540 let right_stream = self.right.execute(partition, context)?;
1543
1544 let column_indices_after_projection = match self.projection.as_ref() {
1546 Some(projection) => projection
1547 .iter()
1548 .map(|i| self.column_indices[*i].clone())
1549 .collect(),
1550 None => self.column_indices.clone(),
1551 };
1552
1553 let on_right = self
1554 .on
1555 .iter()
1556 .map(|(_, right_expr)| Arc::clone(right_expr))
1557 .collect::<Vec<_>>();
1558
1559 Ok(Box::pin(HashJoinStream::new(
1560 partition,
1561 self.schema(),
1562 on_right,
1563 self.filter.clone(),
1564 self.join_type,
1565 right_stream,
1566 self.random_state.random_state().clone(),
1567 join_metrics,
1568 column_indices_after_projection,
1569 self.null_equality,
1570 HashJoinStreamState::WaitBuildSide,
1571 BuildSide::Initial(BuildSideInitialState { left_fut }),
1572 batch_size,
1573 vec![],
1574 self.right.output_ordering().is_some(),
1575 build_accumulator,
1576 self.mode,
1577 self.null_aware,
1578 self.fetch,
1579 )))
1580 }
1581
1582 fn metrics(&self) -> Option<MetricsSet> {
1583 Some(self.metrics.clone_inner())
1584 }
1585
1586 fn child_stats_requests(&self, partition: Option<usize>) -> Vec<ChildStats> {
1587 match (partition, self.mode) {
1588 (Some(_), PartitionMode::CollectLeft) => {
1591 vec![ChildStats::At(None), ChildStats::At(partition)]
1592 }
1593 (Some(_), PartitionMode::Partitioned) => {
1596 vec![ChildStats::At(partition), ChildStats::At(partition)]
1597 }
1598 (None, _) => vec![ChildStats::At(None), ChildStats::At(None)],
1600 (Some(_), PartitionMode::Auto) => {
1603 vec![ChildStats::At(None), ChildStats::At(None)]
1604 }
1605 }
1606 }
1607
1608 fn statistics_from_inputs(
1609 &self,
1610 input_stats: &[Arc<Statistics>],
1611 _args: &StatisticsArgs,
1612 ) -> Result<Arc<Statistics>> {
1613 let left_stats = Arc::clone(&input_stats[0]);
1614 let right_stats = Arc::clone(&input_stats[1]);
1615 let stats = estimate_join_statistics(
1616 Arc::unwrap_or_clone(left_stats),
1617 Arc::unwrap_or_clone(right_stats),
1618 &self.on,
1619 self.null_equality,
1620 &self.join_type,
1621 &self.join_schema,
1622 )?;
1623 let stats = stats.project(self.projection.as_ref());
1625 Ok(Arc::new(stats.with_fetch(self.fetch, 0, 1)?))
1627 }
1628
1629 fn try_swapping_with_projection(
1633 &self,
1634 projection: &ProjectionExec,
1635 ) -> Result<Option<Arc<dyn ExecutionPlan>>> {
1636 if self.contains_projection() {
1638 return Ok(None);
1639 }
1640
1641 let schema = self.schema();
1642 if let Some(JoinData {
1643 projected_left_child,
1644 projected_right_child,
1645 join_filter,
1646 join_on,
1647 }) = try_pushdown_through_join_with_column_indices(
1648 projection,
1649 self.left(),
1650 self.right(),
1651 self.on(),
1652 &schema,
1653 self.filter(),
1654 self.column_indices.as_slice(),
1655 )? {
1656 self.builder()
1657 .with_new_children(vec![
1658 Arc::new(projected_left_child),
1659 Arc::new(projected_right_child),
1660 ])?
1661 .with_on(join_on)
1662 .with_filter(join_filter)
1663 .with_projection(None)
1665 .build_exec()
1666 .map(Some)
1667 } else {
1668 try_embed_projection(projection, self)
1669 }
1670 }
1671
1672 fn gather_filters_for_pushdown(
1673 &self,
1674 phase: FilterPushdownPhase,
1675 parent_filters: Vec<Arc<dyn PhysicalExpr>>,
1676 config: &ConfigOptions,
1677 ) -> Result<FilterDescription> {
1678 let (left_preserved, right_preserved) = lr_is_preserved(self.join_type);
1692
1693 let column_indices: Vec<ColumnIndex> = match self.projection.as_ref() {
1695 Some(projection) => projection
1696 .iter()
1697 .map(|i| self.column_indices[*i].clone())
1698 .collect(),
1699 None => self.column_indices.clone(),
1700 };
1701
1702 let (mut left_allowed, mut right_allowed) = (HashSet::new(), HashSet::new());
1703 column_indices
1704 .iter()
1705 .enumerate()
1706 .for_each(|(output_idx, ci)| {
1707 match ci.side {
1708 JoinSide::Left => left_allowed.insert(output_idx),
1709 JoinSide::Right => right_allowed.insert(output_idx),
1710 JoinSide::None => false,
1712 };
1713 });
1714
1715 match self.join_type {
1719 JoinType::LeftSemi => {
1720 let left_key_indices: HashSet<usize> = self
1721 .on
1722 .iter()
1723 .filter_map(|(left_key, _)| {
1724 left_key.downcast_ref::<Column>().map(|c| c.index())
1725 })
1726 .collect();
1727 for (output_idx, ci) in column_indices.iter().enumerate() {
1728 if ci.side == JoinSide::Left && left_key_indices.contains(&ci.index) {
1729 right_allowed.insert(output_idx);
1730 }
1731 }
1732 }
1733 JoinType::RightSemi => {
1734 let right_key_indices: HashSet<usize> = self
1735 .on
1736 .iter()
1737 .filter_map(|(_, right_key)| {
1738 right_key.downcast_ref::<Column>().map(|c| c.index())
1739 })
1740 .collect();
1741 for (output_idx, ci) in column_indices.iter().enumerate() {
1742 if ci.side == JoinSide::Right && right_key_indices.contains(&ci.index)
1743 {
1744 left_allowed.insert(output_idx);
1745 }
1746 }
1747 }
1748 _ => {}
1749 }
1750
1751 let left_child = if left_preserved {
1752 ChildFilterDescription::from_child_with_allowed_indices(
1753 &parent_filters,
1754 left_allowed,
1755 self.left(),
1756 )?
1757 } else {
1758 ChildFilterDescription::all_unsupported(&parent_filters)
1759 };
1760
1761 let mut right_child = if right_preserved {
1762 ChildFilterDescription::from_child_with_allowed_indices(
1763 &parent_filters,
1764 right_allowed,
1765 self.right(),
1766 )?
1767 } else {
1768 ChildFilterDescription::all_unsupported(&parent_filters)
1769 };
1770
1771 if phase == FilterPushdownPhase::Post
1778 && self.dynamic_filter.is_none()
1779 && self.allow_join_dynamic_filter_pushdown(config)
1780 {
1781 let dynamic_filter = Self::create_dynamic_filter(&self.on);
1783 right_child = right_child.with_self_filter(dynamic_filter);
1784 }
1785
1786 Ok(FilterDescription::new()
1787 .with_child(left_child)
1788 .with_child(right_child))
1789 }
1790
1791 fn handle_child_pushdown_result(
1792 &self,
1793 _phase: FilterPushdownPhase,
1794 child_pushdown_result: ChildPushdownResult,
1795 _config: &ConfigOptions,
1796 ) -> Result<FilterPushdownPropagation<Arc<dyn ExecutionPlan>>> {
1797 let mut result = FilterPushdownPropagation::if_any(child_pushdown_result.clone());
1798 assert_eq!(child_pushdown_result.self_filters.len(), 2); let right_child_self_filters = &child_pushdown_result.self_filters[1]; if let Some(filter) = right_child_self_filters.first() {
1802 let predicate = Arc::clone(&filter.predicate);
1805 if let Ok(dynamic_filter) =
1806 Arc::downcast::<DynamicFilterPhysicalExpr>(predicate)
1807 {
1808 let new_node = self
1810 .builder()
1811 .with_dynamic_filter(Some(HashJoinExecDynamicFilter {
1812 filter: dynamic_filter,
1813 build_accumulator: OnceLock::new(),
1814 }))
1815 .build_exec()?;
1816 result = result.with_updated_node(new_node);
1817 }
1818 }
1819 Ok(result)
1820 }
1821
1822 fn supports_limit_pushdown(&self) -> bool {
1823 false
1827 }
1828
1829 fn fetch(&self) -> Option<usize> {
1830 self.fetch
1831 }
1832
1833 fn with_fetch(&self, limit: Option<usize>) -> Option<Arc<dyn ExecutionPlan>> {
1834 self.builder()
1835 .with_fetch(limit)
1836 .build()
1837 .ok()
1838 .map(|exec| Arc::new(exec) as _)
1839 }
1840 #[cfg(feature = "proto")]
1841 fn try_to_proto(
1842 &self,
1843 ctx: &crate::proto::ExecutionPlanEncodeCtx<'_>,
1844 ) -> Result<Option<datafusion_proto_models::protobuf::PhysicalPlanNode>> {
1845 use datafusion_proto_models::protobuf;
1846
1847 let left = ctx.encode_child(self.left())?;
1848 let right = ctx.encode_child(self.right())?;
1849
1850 let on = self
1851 .on()
1852 .iter()
1853 .map(|(l, r)| -> Result<protobuf::JoinOn> {
1854 Ok(protobuf::JoinOn {
1855 left: Some(ctx.encode_expr(l)?),
1856 right: Some(ctx.encode_expr(r)?),
1857 })
1858 })
1859 .collect::<Result<Vec<_>>>()?;
1860
1861 let join_type = crate::joins::proto::join_type_to_proto(*self.join_type());
1862 let null_equality =
1863 crate::joins::proto::null_equality_to_proto(self.null_equality());
1864 let partition_mode = match self.partition_mode() {
1867 PartitionMode::CollectLeft => protobuf::PartitionMode::CollectLeft,
1868 PartitionMode::Partitioned => protobuf::PartitionMode::Partitioned,
1869 PartitionMode::Auto => protobuf::PartitionMode::Auto,
1870 };
1871
1872 let filter = self
1873 .filter()
1874 .map(|f| crate::joins::proto::join_filter_to_proto(f, ctx))
1875 .transpose()?;
1876
1877 let dynamic_filter = self
1878 .dynamic_expressions_produced()
1879 .into_iter()
1880 .next()
1881 .map(|expr| ctx.encode_expr(&expr))
1882 .transpose()?;
1883
1884 Ok(Some(protobuf::PhysicalPlanNode {
1885 physical_plan_type: Some(
1886 protobuf::physical_plan_node::PhysicalPlanType::HashJoin(Box::new(
1887 protobuf::HashJoinExecNode {
1888 left: Some(Box::new(left)),
1889 right: Some(Box::new(right)),
1890 on,
1891 join_type: join_type.into(),
1892 partition_mode: partition_mode.into(),
1893 null_equality: null_equality.into(),
1894 filter,
1895 projection: match self.projection.as_ref() {
1903 None => Vec::new(),
1904 Some(v) if v.is_empty() => vec![u32::MAX],
1905 Some(v) => v.iter().map(|x| *x as u32).collect(),
1906 },
1907 null_aware: self.null_aware,
1908 dynamic_filter,
1909 fetch: self.fetch.map(|f| f as u64),
1910 },
1911 )),
1912 ),
1913 }))
1914 }
1915}
1916
1917#[cfg(feature = "proto")]
1918impl HashJoinExec {
1919 pub fn try_from_proto(
1921 node: &datafusion_proto_models::protobuf::PhysicalPlanNode,
1922 ctx: &crate::proto::ExecutionPlanDecodeCtx<'_>,
1923 ) -> Result<Arc<dyn ExecutionPlan>> {
1924 use datafusion_common::{internal_datafusion_err, plan_datafusion_err};
1925 use datafusion_proto_models::protobuf;
1926 use std::any::Any;
1927
1928 let hashjoin = crate::expect_plan_variant!(
1929 node,
1930 protobuf::physical_plan_node::PhysicalPlanType::HashJoin,
1931 "HashJoinExec",
1932 );
1933
1934 let left =
1935 ctx.decode_required_child(hashjoin.left.as_deref(), "HashJoinExec", "left")?;
1936 let right = ctx.decode_required_child(
1937 hashjoin.right.as_deref(),
1938 "HashJoinExec",
1939 "right",
1940 )?;
1941 let left_schema = left.schema();
1942 let right_schema = right.schema();
1943
1944 let on: Vec<(PhysicalExprRef, PhysicalExprRef)> = hashjoin
1945 .on
1946 .iter()
1947 .map(|col| {
1948 let l = ctx.decode_required_expr(
1949 col.left.as_ref(),
1950 left_schema.as_ref(),
1951 "HashJoinExec",
1952 "on.left",
1953 )?;
1954 let r = ctx.decode_required_expr(
1955 col.right.as_ref(),
1956 right_schema.as_ref(),
1957 "HashJoinExec",
1958 "on.right",
1959 )?;
1960 Ok((l, r))
1961 })
1962 .collect::<Result<_>>()?;
1963
1964 let join_type = crate::joins::proto::join_type_from_proto(
1965 hashjoin.join_type,
1966 "HashJoinExec",
1967 )?;
1968 let null_equality = crate::joins::proto::null_equality_from_proto(
1969 hashjoin.null_equality,
1970 "HashJoinExec",
1971 )?;
1972 let partition_mode = match protobuf::PartitionMode::try_from(
1975 hashjoin.partition_mode,
1976 )
1977 .map_err(|_| {
1978 internal_datafusion_err!(
1979 "HashJoinExec: unknown PartitionMode {}",
1980 hashjoin.partition_mode
1981 )
1982 })? {
1983 protobuf::PartitionMode::CollectLeft => PartitionMode::CollectLeft,
1984 protobuf::PartitionMode::Partitioned => PartitionMode::Partitioned,
1985 protobuf::PartitionMode::Auto => PartitionMode::Auto,
1986 };
1987
1988 let filter = hashjoin
1989 .filter
1990 .as_ref()
1991 .map(|f| crate::joins::proto::join_filter_from_proto(f, ctx, "HashJoinExec"))
1992 .transpose()?;
1993
1994 let projection = match hashjoin.projection.as_slice() {
1996 [] => None,
1997 [u32::MAX] => Some(Vec::new()),
1998 indices => Some(indices.iter().map(|i| *i as usize).collect()),
1999 };
2000
2001 let fetch = hashjoin
2012 .fetch
2013 .map(|f| {
2014 usize::try_from(f).map_err(|_| {
2015 plan_datafusion_err!(
2016 "HashJoinExec: fetch value {f} cannot be represented as usize on this target"
2017 )
2018 })
2019 })
2020 .transpose()?;
2021
2022 let mut hash_join = HashJoinExecBuilder::new(left, right, on, join_type)
2023 .with_filter(filter)
2024 .with_projection(projection)
2025 .with_partition_mode(partition_mode)
2026 .with_null_equality(null_equality)
2027 .with_null_aware(hashjoin.null_aware)
2028 .with_fetch(fetch)
2029 .build()?;
2030
2031 if let Some(dynamic_filter_proto) = &hashjoin.dynamic_filter {
2032 let dynamic_filter_expr =
2035 ctx.decode_expr(dynamic_filter_proto, right_schema.as_ref())?;
2036 let df = (dynamic_filter_expr as Arc<dyn Any + Send + Sync>)
2037 .downcast::<DynamicFilterPhysicalExpr>()
2038 .map_err(|_| {
2039 internal_datafusion_err!(
2040 "HashJoinExec dynamic_filter did not decode to a DynamicFilterPhysicalExpr"
2041 )
2042 })?;
2043 hash_join = hash_join.with_dynamic_filter_expr(df)?;
2044 }
2045
2046 Ok(Arc::new(hash_join))
2047 }
2048}
2049
2050fn lr_is_preserved(join_type: JoinType) -> (bool, bool) {
2056 match join_type {
2057 JoinType::Inner => (true, true),
2058 JoinType::Left => (true, false),
2059 JoinType::Right => (false, true),
2060 JoinType::Full => (false, false),
2061 JoinType::LeftSemi | JoinType::RightSemi => (true, true),
2063 JoinType::LeftAnti | JoinType::LeftMark => (true, false),
2064 JoinType::RightAnti | JoinType::RightMark => (false, true),
2065 }
2066}
2067
2068struct CollectLeftAccumulator {
2078 expr: Arc<dyn PhysicalExpr>,
2080 min: MinAccumulator,
2082 max: MaxAccumulator,
2084}
2085
2086impl CollectLeftAccumulator {
2087 fn try_new(expr: Arc<dyn PhysicalExpr>, schema: &SchemaRef) -> Result<Self> {
2096 fn dictionary_value_type(data_type: &DataType) -> DataType {
2098 match data_type {
2099 DataType::Dictionary(_, value_type) => {
2100 dictionary_value_type(value_type.as_ref())
2101 }
2102 _ => data_type.clone(),
2103 }
2104 }
2105
2106 let data_type = expr
2107 .data_type(schema)
2108 .map(|dt| dictionary_value_type(&dt))?;
2110 Ok(Self {
2111 expr,
2112 min: MinAccumulator::try_new(&data_type)?,
2113 max: MaxAccumulator::try_new(&data_type)?,
2114 })
2115 }
2116
2117 fn update_batch(&mut self, batch: &RecordBatch) -> Result<()> {
2128 let array = self.expr.evaluate(batch)?.into_array(batch.num_rows())?;
2129 self.min.update_batch(std::slice::from_ref(&array))?;
2130 self.max.update_batch(std::slice::from_ref(&array))?;
2131 Ok(())
2132 }
2133
2134 fn evaluate(mut self) -> Result<ColumnBounds> {
2141 Ok(ColumnBounds::new(
2142 self.min.evaluate()?,
2143 self.max.evaluate()?,
2144 ))
2145 }
2146}
2147
2148struct BuildSideState {
2150 batches: Vec<RecordBatch>,
2151 num_rows: usize,
2152 metrics: BuildProbeJoinMetrics,
2153 reservation: MemoryReservation,
2154 bounds_accumulators: Option<Vec<CollectLeftAccumulator>>,
2155 memory_counter: RecordBatchMemoryCounter,
2159}
2160
2161impl BuildSideState {
2162 fn try_new(
2164 metrics: BuildProbeJoinMetrics,
2165 reservation: MemoryReservation,
2166 on_left: Vec<Arc<dyn PhysicalExpr>>,
2167 schema: &SchemaRef,
2168 should_compute_dynamic_filters: bool,
2169 ) -> Result<Self> {
2170 Ok(Self {
2171 batches: Vec::new(),
2172 num_rows: 0,
2173 metrics,
2174 reservation,
2175 memory_counter: RecordBatchMemoryCounter::new(),
2176 bounds_accumulators: should_compute_dynamic_filters
2177 .then(|| {
2178 on_left
2179 .into_iter()
2180 .map(|expr| CollectLeftAccumulator::try_new(expr, schema))
2181 .collect::<Result<Vec<_>>>()
2182 })
2183 .transpose()?,
2184 })
2185 }
2186}
2187
2188fn should_collect_min_max_for_perfect_hash(
2189 on_left: &[PhysicalExprRef],
2190 schema: &SchemaRef,
2191) -> Result<bool> {
2192 if on_left.len() != 1 {
2193 return Ok(false);
2194 }
2195
2196 let expr = &on_left[0];
2197 let data_type = expr.data_type(schema)?;
2198 Ok(ArrayMap::is_supported_type(&data_type))
2199}
2200
2201#[expect(clippy::too_many_arguments)]
2230async fn collect_left_input(
2231 random_state: RandomState,
2232 left_stream: SendableRecordBatchStream,
2233 on_left: Vec<PhysicalExprRef>,
2234 metrics: BuildProbeJoinMetrics,
2235 reservation: MemoryReservation,
2236 with_visited_indices_bitmap: bool,
2237 probe_threads_count: usize,
2238 should_compute_dynamic_filters: bool,
2239 config: Arc<ConfigOptions>,
2240 null_equality: NullEquality,
2241 array_map_created_count: Count,
2242) -> Result<JoinLeftData> {
2243 let schema = left_stream.schema();
2244
2245 let should_collect_min_max_for_phj =
2246 should_collect_min_max_for_perfect_hash(&on_left, &schema)?;
2247
2248 let initial = BuildSideState::try_new(
2249 metrics,
2250 reservation,
2251 on_left.clone(),
2252 &schema,
2253 should_compute_dynamic_filters || should_collect_min_max_for_phj,
2254 )?;
2255
2256 let state = left_stream
2257 .try_fold(initial, |mut state, batch| async move {
2258 if let Some(ref mut accumulators) = state.bounds_accumulators {
2260 for accumulator in accumulators {
2261 accumulator.update_batch(&batch)?;
2262 }
2263 }
2264
2265 let batch_size = state.memory_counter.count_batch(&batch);
2267 state.reservation.try_grow(batch_size)?;
2269 state.metrics.build_mem_used.add(batch_size);
2271 state.metrics.build_input_batches.add(1);
2272 state.metrics.build_input_rows.add(batch.num_rows());
2273 state.num_rows += batch.num_rows();
2275 state.batches.push(batch);
2277 Ok(state)
2278 })
2279 .await?;
2280
2281 let BuildSideState {
2283 batches,
2284 num_rows,
2285 metrics,
2286 mut reservation,
2287 bounds_accumulators,
2288 memory_counter: _,
2289 } = state;
2290
2291 let mut bounds = match bounds_accumulators {
2293 Some(accumulators) if num_rows > 0 => {
2294 let bounds = accumulators
2295 .into_iter()
2296 .map(CollectLeftAccumulator::evaluate)
2297 .collect::<Result<Vec<_>>>()?;
2298 Some(PartitionBounds::new(bounds))
2299 }
2300 _ => None,
2301 };
2302
2303 let (join_hash_map, batch, left_values) =
2304 if let Some((array_map, batch, left_value)) = try_create_array_map(
2305 &bounds,
2306 &schema,
2307 &batches,
2308 &on_left,
2309 &mut reservation,
2310 config.execution.perfect_hash_join_small_build_threshold,
2311 config.execution.perfect_hash_join_min_key_density,
2312 null_equality,
2313 )? {
2314 array_map_created_count.add(1);
2315 metrics.build_mem_used.add(array_map.size());
2316
2317 (Map::ArrayMap(array_map), batch, left_value)
2318 } else {
2319 let fixed_size_u32 = size_of::<JoinHashMapU32>();
2322 let fixed_size_u64 = size_of::<JoinHashMapU64>();
2323
2324 let mut hashmap: Box<dyn JoinHashMapType> = if num_rows > u32::MAX as usize {
2328 let estimated_hashtable_size =
2329 estimate_memory_size::<(u64, u64)>(num_rows, fixed_size_u64)?;
2330 reservation.try_grow(estimated_hashtable_size)?;
2331 metrics.build_mem_used.add(estimated_hashtable_size);
2332 Box::new(JoinHashMapU64::with_capacity(num_rows))
2333 } else {
2334 let estimated_hashtable_size =
2335 estimate_memory_size::<(u32, u64)>(num_rows, fixed_size_u32)?;
2336 reservation.try_grow(estimated_hashtable_size)?;
2337 metrics.build_mem_used.add(estimated_hashtable_size);
2338 Box::new(JoinHashMapU32::with_capacity(num_rows))
2339 };
2340
2341 let mut hashes_buffer = Vec::new();
2342 let mut offset = 0;
2343
2344 let batches_iter = batches.iter().rev();
2345
2346 for batch in batches_iter.clone() {
2348 hashes_buffer.clear();
2349 hashes_buffer.resize(batch.num_rows(), 0);
2350 update_hash(
2351 &on_left,
2352 batch,
2353 &mut *hashmap,
2354 offset,
2355 &random_state,
2356 &mut hashes_buffer,
2357 0,
2358 true,
2359 null_equality,
2360 )?;
2361 offset += batch.num_rows();
2362 }
2363
2364 let batch = concat_batches(&schema, batches_iter.clone())?;
2366
2367 let left_values = evaluate_expressions_to_arrays(&on_left, &batch)?;
2368
2369 (Map::HashMap(hashmap), batch, left_values)
2370 };
2371
2372 let visited_indices_bitmap = if with_visited_indices_bitmap {
2374 let bitmap_size = bit_util::ceil(batch.num_rows(), 8);
2375 reservation.try_grow(bitmap_size)?;
2376 metrics.build_mem_used.add(bitmap_size);
2377
2378 let mut bitmap_buffer = BooleanBufferBuilder::new(batch.num_rows());
2379 bitmap_buffer.append_n(num_rows, false);
2380 bitmap_buffer
2381 } else {
2382 BooleanBufferBuilder::new(0)
2383 };
2384
2385 let map = Arc::new(join_hash_map);
2386
2387 let membership = if num_rows == 0 {
2388 PushdownStrategy::Empty
2389 } else {
2390 let estimated_size = left_values
2394 .iter()
2395 .map(|arr| arr.get_array_memory_size())
2396 .sum::<usize>();
2397 if left_values.is_empty()
2398 || left_values[0].is_empty()
2399 || estimated_size > config.optimizer.hash_join_inlist_pushdown_max_size
2400 || map.num_of_distinct_key()
2401 > config
2402 .optimizer
2403 .hash_join_inlist_pushdown_max_distinct_values
2404 {
2405 PushdownStrategy::Map(Arc::clone(&map))
2406 } else if let Some(in_list_values) = build_struct_inlist_values(&left_values)? {
2407 PushdownStrategy::InList(in_list_values)
2408 } else {
2409 PushdownStrategy::Map(Arc::clone(&map))
2410 }
2411 };
2412
2413 if should_collect_min_max_for_phj && !should_compute_dynamic_filters {
2414 bounds = None;
2415 }
2416
2417 let data = JoinLeftData {
2418 map,
2419 batch,
2420 values: left_values,
2421 visited_indices_bitmap: Mutex::new(visited_indices_bitmap),
2422 probe_threads_counter: AtomicUsize::new(probe_threads_count),
2423 _reservation: reservation,
2424 bounds,
2425 membership,
2426 probe_side_non_empty: AtomicBool::new(false),
2427 probe_side_has_null: AtomicBool::new(false),
2428 };
2429
2430 Ok(data)
2431}
2432
2433#[cfg(test)]
2434mod tests {
2435 use super::*;
2436
2437 fn assert_phj_used(metrics: &MetricsSet, use_phj: bool) {
2438 if use_phj {
2439 assert!(
2440 metrics
2441 .sum_by_name(ARRAY_MAP_CREATED_COUNT_METRIC_NAME)
2442 .expect("should have array_map_created_count metrics")
2443 .as_usize()
2444 >= 1
2445 );
2446 } else {
2447 assert_eq!(
2448 metrics
2449 .sum_by_name(ARRAY_MAP_CREATED_COUNT_METRIC_NAME)
2450 .map(|v| v.as_usize())
2451 .unwrap_or(0),
2452 0
2453 )
2454 }
2455 }
2456
2457 fn build_schema_and_on() -> Result<(SchemaRef, SchemaRef, JoinOn)> {
2458 let left_schema = Arc::new(Schema::new(vec![
2459 Field::new("a1", DataType::Int32, true),
2460 Field::new("b1", DataType::Int32, true),
2461 ]));
2462 let right_schema = Arc::new(Schema::new(vec![
2463 Field::new("a2", DataType::Int32, true),
2464 Field::new("b1", DataType::Int32, true),
2465 ]));
2466 let on = vec![(
2467 Arc::new(Column::new_with_schema("b1", &left_schema)?) as _,
2468 Arc::new(Column::new_with_schema("b1", &right_schema)?) as _,
2469 )];
2470 Ok((left_schema, right_schema, on))
2471 }
2472
2473 use crate::coalesce_partitions::CoalescePartitionsExec;
2474 use crate::execution_plan::Boundedness;
2475 use crate::filter::FilterExecBuilder;
2476 use crate::joins::hash_join::stream::lookup_join_hashmap;
2477 use crate::test::{TestMemoryExec, assert_join_metrics};
2478 use crate::{ChildrenPropertiesMode, ReplaceChildrenOptions};
2479 use crate::{
2480 common, expressions::Column, repartition::RepartitionExec, test::build_table_i32,
2481 test::exec::MockExec,
2482 };
2483
2484 use arrow::array::{
2485 Date32Array, Int32Array, Int64Array, StructArray, UInt32Array, UInt64Array,
2486 };
2487 use arrow::buffer::NullBuffer;
2488 use arrow::datatypes::{DataType, Field};
2489 use datafusion_common::hash_utils::create_hashes;
2490 use datafusion_common::test_util::{batches_to_sort_string, batches_to_string};
2491 use datafusion_common::{
2492 ScalarValue, assert_batches_eq, assert_batches_sorted_eq, assert_contains,
2493 exec_err, internal_err,
2494 };
2495 use datafusion_execution::config::SessionConfig;
2496 use datafusion_execution::runtime_env::RuntimeEnvBuilder;
2497 use datafusion_expr::Operator;
2498 use datafusion_physical_expr::expressions::{BinaryExpr, Literal};
2499 use datafusion_physical_expr::{
2500 EquivalenceProperties, PhysicalSortExpr, RangePartitioning, SplitPoint,
2501 };
2502 use hashbrown::HashTable;
2503 use insta::{allow_duplicates, assert_snapshot};
2504 use rstest::*;
2505 use rstest_reuse::*;
2506
2507 #[derive(Debug)]
2508 struct PartitionedTestExec {
2509 cache: Arc<PlanProperties>,
2510 }
2511
2512 impl PartitionedTestExec {
2513 fn try_new(schema: SchemaRef, partitioning: Partitioning) -> Result<Self> {
2514 Ok(Self {
2515 cache: Arc::new(PlanProperties::new(
2516 EquivalenceProperties::new(Arc::clone(&schema)),
2517 partitioning,
2518 EmissionType::Incremental,
2519 Boundedness::Bounded,
2520 )),
2521 })
2522 }
2523 }
2524
2525 impl DisplayAs for PartitionedTestExec {
2526 fn fmt_as(&self, _t: DisplayFormatType, f: &mut fmt::Formatter) -> fmt::Result {
2527 write!(f, "PartitionedTestExec")
2528 }
2529 }
2530
2531 impl ExecutionPlan for PartitionedTestExec {
2532 fn name(&self) -> &'static str {
2533 "PartitionedTestExec"
2534 }
2535
2536 fn properties(&self) -> &Arc<PlanProperties> {
2537 &self.cache
2538 }
2539
2540 fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
2541 vec![]
2542 }
2543
2544 fn apply_expressions(
2545 &self,
2546 _f: &mut dyn FnMut(&Arc<dyn PhysicalExpr>) -> Result<TreeNodeRecursion>,
2547 ) -> Result<TreeNodeRecursion> {
2548 Ok(TreeNodeRecursion::Continue)
2549 }
2550
2551 fn replace_children(
2552 self: Arc<Self>,
2553 _: Vec<Arc<dyn ExecutionPlan>>,
2554 _: ReplaceChildrenOptions,
2555 ) -> Result<Arc<dyn ExecutionPlan>> {
2556 Ok(self)
2557 }
2558
2559 fn with_new_children(
2560 self: Arc<Self>,
2561 children: Vec<Arc<dyn ExecutionPlan>>,
2562 ) -> Result<Arc<dyn ExecutionPlan>> {
2563 self.replace_children(
2564 children,
2565 ReplaceChildrenOptions::new(ChildrenPropertiesMode::Recompute),
2566 )
2567 }
2568
2569 fn execute(
2570 &self,
2571 _partition: usize,
2572 _context: Arc<TaskContext>,
2573 ) -> Result<SendableRecordBatchStream> {
2574 unreachable!()
2575 }
2576 }
2577
2578 fn div_ceil(a: usize, b: usize) -> usize {
2579 a.div_ceil(b)
2580 }
2581
2582 #[template]
2583 #[rstest]
2584 fn hash_join_exec_configs(
2585 #[values(8192, 10, 5, 2, 1)] batch_size: usize,
2586 #[values(true, false)] use_perfect_hash_join_as_possible: bool,
2587 ) {
2588 }
2589
2590 fn prepare_task_ctx(
2591 batch_size: usize,
2592 use_perfect_hash_join_as_possible: bool,
2593 ) -> Arc<TaskContext> {
2594 let mut session_config = SessionConfig::default().with_batch_size(batch_size);
2595
2596 if use_perfect_hash_join_as_possible {
2597 session_config
2598 .options_mut()
2599 .execution
2600 .perfect_hash_join_small_build_threshold = 819200;
2601 session_config
2602 .options_mut()
2603 .execution
2604 .perfect_hash_join_min_key_density = 0.0;
2605 } else {
2606 session_config
2607 .options_mut()
2608 .execution
2609 .perfect_hash_join_small_build_threshold = 0;
2610 session_config
2611 .options_mut()
2612 .execution
2613 .perfect_hash_join_min_key_density = f64::INFINITY;
2614 }
2615 Arc::new(TaskContext::default().with_session_config(session_config))
2616 }
2617
2618 fn build_table(
2619 a: (&str, &Vec<i32>),
2620 b: (&str, &Vec<i32>),
2621 c: (&str, &Vec<i32>),
2622 ) -> Arc<dyn ExecutionPlan> {
2623 let batch = build_table_i32(a, b, c);
2624 let schema = batch.schema();
2625 TestMemoryExec::try_new_exec(&[vec![batch]], schema, None).unwrap()
2626 }
2627
2628 fn build_table_two_cols(
2630 a: (&str, &Vec<Option<i32>>),
2631 b: (&str, &Vec<Option<i32>>),
2632 ) -> Arc<dyn ExecutionPlan> {
2633 let schema = Arc::new(Schema::new(vec![
2634 Field::new(a.0, DataType::Int32, true),
2635 Field::new(b.0, DataType::Int32, true),
2636 ]));
2637 let batch = RecordBatch::try_new(
2638 Arc::clone(&schema),
2639 vec![
2640 Arc::new(Int32Array::from(a.1.clone())),
2641 Arc::new(Int32Array::from(b.1.clone())),
2642 ],
2643 )
2644 .unwrap();
2645 TestMemoryExec::try_new_exec(&[vec![batch]], schema, None).unwrap()
2646 }
2647
2648 fn join(
2649 left: Arc<dyn ExecutionPlan>,
2650 right: Arc<dyn ExecutionPlan>,
2651 on: JoinOn,
2652 join_type: &JoinType,
2653 null_equality: NullEquality,
2654 ) -> Result<HashJoinExec> {
2655 HashJoinExec::try_new(
2656 left,
2657 right,
2658 on,
2659 None,
2660 join_type,
2661 None,
2662 PartitionMode::CollectLeft,
2663 null_equality,
2664 false,
2665 )
2666 }
2667
2668 fn join_with_filter(
2669 left: Arc<dyn ExecutionPlan>,
2670 right: Arc<dyn ExecutionPlan>,
2671 on: JoinOn,
2672 filter: JoinFilter,
2673 join_type: &JoinType,
2674 null_equality: NullEquality,
2675 ) -> Result<HashJoinExec> {
2676 HashJoinExec::try_new(
2677 left,
2678 right,
2679 on,
2680 Some(filter),
2681 join_type,
2682 None,
2683 PartitionMode::CollectLeft,
2684 null_equality,
2685 false,
2686 )
2687 }
2688
2689 fn empty_build_with_probe_error_inputs()
2690 -> (Arc<dyn ExecutionPlan>, Arc<dyn ExecutionPlan>, JoinOn) {
2691 let left_batch =
2692 build_table_i32(("a1", &vec![]), ("b1", &vec![]), ("c1", &vec![]));
2693 let left_schema = left_batch.schema();
2694 let left: Arc<dyn ExecutionPlan> = TestMemoryExec::try_new_exec(
2695 &[vec![left_batch]],
2696 Arc::clone(&left_schema),
2697 None,
2698 )
2699 .unwrap();
2700
2701 let err = exec_err!("bad data error");
2702 let right_batch =
2703 build_table_i32(("a2", &vec![]), ("b1", &vec![]), ("c2", &vec![]));
2704 let right_schema = right_batch.schema();
2705 let on = vec![(
2706 Arc::new(Column::new_with_schema("b1", &left_schema).unwrap()) as _,
2707 Arc::new(Column::new_with_schema("b1", &right_schema).unwrap()) as _,
2708 )];
2709 let right: Arc<dyn ExecutionPlan> = Arc::new(
2710 MockExec::new(vec![Ok(right_batch), err], right_schema)
2711 .with_use_task(false)
2712 .with_unknown_statistics(),
2716 );
2717
2718 (left, right, on)
2719 }
2720
2721 async fn assert_empty_build_probe_behavior(
2722 join_types: &[JoinType],
2723 expect_probe_error: bool,
2724 with_filter: bool,
2725 ) {
2726 let (left, right, on) = empty_build_with_probe_error_inputs();
2727 let filter = prepare_join_filter();
2728
2729 for join_type in join_types {
2730 let join = if with_filter {
2731 join_with_filter(
2732 Arc::clone(&left),
2733 Arc::clone(&right),
2734 on.clone(),
2735 filter.clone(),
2736 join_type,
2737 NullEquality::NullEqualsNothing,
2738 )
2739 .unwrap()
2740 } else {
2741 join(
2742 Arc::clone(&left),
2743 Arc::clone(&right),
2744 on.clone(),
2745 join_type,
2746 NullEquality::NullEqualsNothing,
2747 )
2748 .unwrap()
2749 };
2750
2751 let result = common::collect(
2752 join.execute(0, Arc::new(TaskContext::default())).unwrap(),
2753 )
2754 .await;
2755
2756 if expect_probe_error {
2757 let result_string = result.unwrap_err().to_string();
2758 assert!(
2759 result_string.contains("bad data error"),
2760 "actual: {result_string}"
2761 );
2762 } else {
2763 let batches = result.unwrap();
2764 assert!(
2765 batches.is_empty(),
2766 "expected no output batches for {join_type}, got {batches:?}"
2767 );
2768 }
2769 }
2770 }
2771
2772 fn hash_join_with_dynamic_filter(
2773 left: Arc<dyn ExecutionPlan>,
2774 right: Arc<dyn ExecutionPlan>,
2775 on: JoinOn,
2776 join_type: JoinType,
2777 ) -> Result<(HashJoinExec, Arc<DynamicFilterPhysicalExpr>)> {
2778 hash_join_with_dynamic_filter_and_mode(
2779 left,
2780 right,
2781 on,
2782 join_type,
2783 PartitionMode::CollectLeft,
2784 )
2785 }
2786
2787 fn hash_join_with_dynamic_filter_and_mode(
2788 left: Arc<dyn ExecutionPlan>,
2789 right: Arc<dyn ExecutionPlan>,
2790 on: JoinOn,
2791 join_type: JoinType,
2792 mode: PartitionMode,
2793 ) -> Result<(HashJoinExec, Arc<DynamicFilterPhysicalExpr>)> {
2794 let dynamic_filter = HashJoinExec::create_dynamic_filter(&on);
2795 let consumer: Arc<dyn PhysicalExpr> = Arc::clone(&dynamic_filter) as _;
2796 let right = Arc::new(FilterExecBuilder::new(consumer, right).build()?);
2797 let mut join = HashJoinExec::try_new(
2798 left,
2799 right,
2800 on,
2801 None,
2802 &join_type,
2803 None,
2804 mode,
2805 NullEquality::NullEqualsNothing,
2806 false,
2807 )?;
2808 join.dynamic_filter = Some(HashJoinExecDynamicFilter {
2809 filter: Arc::clone(&dynamic_filter),
2810 build_accumulator: OnceLock::new(),
2811 });
2812
2813 Ok((join, dynamic_filter))
2814 }
2815
2816 async fn join_collect(
2817 left: Arc<dyn ExecutionPlan>,
2818 right: Arc<dyn ExecutionPlan>,
2819 on: JoinOn,
2820 join_type: &JoinType,
2821 null_equality: NullEquality,
2822 context: Arc<TaskContext>,
2823 ) -> Result<(Vec<String>, Vec<RecordBatch>, MetricsSet)> {
2824 let join = join(left, right, on, join_type, null_equality)?;
2825 let columns_header = columns(&join.schema());
2826
2827 let stream = join.execute(0, context)?;
2828 let batches = common::collect(stream).await?;
2829 let metrics = join.metrics().unwrap();
2830
2831 Ok((columns_header, batches, metrics))
2832 }
2833
2834 async fn partitioned_join_collect(
2835 left: Arc<dyn ExecutionPlan>,
2836 right: Arc<dyn ExecutionPlan>,
2837 on: JoinOn,
2838 join_type: &JoinType,
2839 null_equality: NullEquality,
2840 context: Arc<TaskContext>,
2841 ) -> Result<(Vec<String>, Vec<RecordBatch>, MetricsSet)> {
2842 join_collect_with_partition_mode(
2843 left,
2844 right,
2845 on,
2846 join_type,
2847 PartitionMode::Partitioned,
2848 null_equality,
2849 context,
2850 )
2851 .await
2852 }
2853
2854 async fn join_collect_with_partition_mode(
2855 left: Arc<dyn ExecutionPlan>,
2856 right: Arc<dyn ExecutionPlan>,
2857 on: JoinOn,
2858 join_type: &JoinType,
2859 partition_mode: PartitionMode,
2860 null_equality: NullEquality,
2861 context: Arc<TaskContext>,
2862 ) -> Result<(Vec<String>, Vec<RecordBatch>, MetricsSet)> {
2863 let partition_count = 4;
2864
2865 let (left_expr, right_expr) = on
2866 .iter()
2867 .map(|(l, r)| (Arc::clone(l), Arc::clone(r)))
2868 .unzip();
2869
2870 let left_repartitioned: Arc<dyn ExecutionPlan> = match partition_mode {
2871 PartitionMode::CollectLeft => Arc::new(CoalescePartitionsExec::new(left)),
2872 PartitionMode::Partitioned => Arc::new(RepartitionExec::try_new(
2873 left,
2874 Partitioning::Hash(left_expr, partition_count),
2875 )?),
2876 PartitionMode::Auto => {
2877 return internal_err!("Unexpected PartitionMode::Auto in join tests");
2878 }
2879 };
2880
2881 let right_repartitioned: Arc<dyn ExecutionPlan> = match partition_mode {
2882 PartitionMode::CollectLeft => {
2883 let partition_column_name = right.schema().field(0).name().clone();
2884 let partition_expr = vec![Arc::new(Column::new_with_schema(
2885 &partition_column_name,
2886 &right.schema(),
2887 )?) as _];
2888 Arc::new(RepartitionExec::try_new(
2889 right,
2890 Partitioning::Hash(partition_expr, partition_count),
2891 )?) as _
2892 }
2893 PartitionMode::Partitioned => Arc::new(RepartitionExec::try_new(
2894 right,
2895 Partitioning::Hash(right_expr, partition_count),
2896 )?),
2897 PartitionMode::Auto => {
2898 return internal_err!("Unexpected PartitionMode::Auto in join tests");
2899 }
2900 };
2901
2902 let join = HashJoinExec::try_new(
2903 left_repartitioned,
2904 right_repartitioned,
2905 on,
2906 None,
2907 join_type,
2908 None,
2909 partition_mode,
2910 null_equality,
2911 false,
2912 )?;
2913
2914 let columns = columns(&join.schema());
2915
2916 let mut batches = vec![];
2917 for i in 0..partition_count {
2918 let stream = join.execute(i, Arc::clone(&context))?;
2919 let more_batches = common::collect(stream).await?;
2920 batches.extend(
2921 more_batches
2922 .into_iter()
2923 .filter(|b| b.num_rows() > 0)
2924 .collect::<Vec<_>>(),
2925 );
2926 }
2927 let metrics = join.metrics().unwrap();
2928
2929 Ok((columns, batches, metrics))
2930 }
2931
2932 #[apply(hash_join_exec_configs)]
2933 #[tokio::test]
2934 async fn join_inner_one(
2935 batch_size: usize,
2936 use_perfect_hash_join_as_possible: bool,
2937 ) -> Result<()> {
2938 let task_ctx = prepare_task_ctx(batch_size, use_perfect_hash_join_as_possible);
2939 let left = build_table(
2940 ("a1", &vec![1, 2, 3]),
2941 ("b1", &vec![4, 5, 5]), ("c1", &vec![7, 8, 9]),
2943 );
2944 let right = build_table(
2945 ("a2", &vec![10, 20, 30]),
2946 ("b1", &vec![4, 5, 6]),
2947 ("c2", &vec![70, 80, 90]),
2948 );
2949
2950 let on = vec![(
2951 Arc::new(Column::new_with_schema("b1", &left.schema())?) as _,
2952 Arc::new(Column::new_with_schema("b1", &right.schema())?) as _,
2953 )];
2954
2955 let (columns, batches, metrics) = join_collect(
2956 Arc::clone(&left),
2957 Arc::clone(&right),
2958 on.clone(),
2959 &JoinType::Inner,
2960 NullEquality::NullEqualsNothing,
2961 task_ctx,
2962 )
2963 .await?;
2964
2965 assert_eq!(columns, vec!["a1", "b1", "c1", "a2", "b1", "c2"]);
2966
2967 allow_duplicates! {
2968 assert_snapshot!(batches_to_string(&batches), @r"
2970 +----+----+----+----+----+----+
2971 | a1 | b1 | c1 | a2 | b1 | c2 |
2972 +----+----+----+----+----+----+
2973 | 1 | 4 | 7 | 10 | 4 | 70 |
2974 | 2 | 5 | 8 | 20 | 5 | 80 |
2975 | 3 | 5 | 9 | 20 | 5 | 80 |
2976 +----+----+----+----+----+----+
2977 ");
2978 }
2979
2980 assert_join_metrics!(metrics, 3);
2981 assert_phj_used(&metrics, use_perfect_hash_join_as_possible);
2982
2983 Ok(())
2984 }
2985
2986 #[apply(hash_join_exec_configs)]
2987 #[tokio::test]
2988 async fn partitioned_join_inner_one(
2989 batch_size: usize,
2990 use_perfect_hash_join_as_possible: bool,
2991 ) -> Result<()> {
2992 let task_ctx = prepare_task_ctx(batch_size, use_perfect_hash_join_as_possible);
2993 let left = build_table(
2994 ("a1", &vec![1, 2, 3]),
2995 ("b1", &vec![4, 5, 5]), ("c1", &vec![7, 8, 9]),
2997 );
2998 let right = build_table(
2999 ("a2", &vec![10, 20, 30]),
3000 ("b1", &vec![4, 5, 6]),
3001 ("c2", &vec![70, 80, 90]),
3002 );
3003 let on = vec![(
3004 Arc::new(Column::new_with_schema("b1", &left.schema())?) as _,
3005 Arc::new(Column::new_with_schema("b1", &right.schema())?) as _,
3006 )];
3007
3008 let (columns, batches, metrics) = partitioned_join_collect(
3009 Arc::clone(&left),
3010 Arc::clone(&right),
3011 on.clone(),
3012 &JoinType::Inner,
3013 NullEquality::NullEqualsNothing,
3014 task_ctx,
3015 )
3016 .await?;
3017
3018 assert_eq!(columns, vec!["a1", "b1", "c1", "a2", "b1", "c2"]);
3019
3020 allow_duplicates! {
3021 assert_snapshot!(batches_to_sort_string(&batches), @r"
3022 +----+----+----+----+----+----+
3023 | a1 | b1 | c1 | a2 | b1 | c2 |
3024 +----+----+----+----+----+----+
3025 | 1 | 4 | 7 | 10 | 4 | 70 |
3026 | 2 | 5 | 8 | 20 | 5 | 80 |
3027 | 3 | 5 | 9 | 20 | 5 | 80 |
3028 +----+----+----+----+----+----+
3029 ");
3030 }
3031
3032 assert_join_metrics!(metrics, 3);
3033 assert_phj_used(&metrics, use_perfect_hash_join_as_possible);
3034
3035 Ok(())
3036 }
3037
3038 #[tokio::test]
3039 async fn join_inner_one_no_shared_column_names() -> Result<()> {
3040 let task_ctx = Arc::new(TaskContext::default());
3041 let left = build_table(
3042 ("a1", &vec![1, 2, 3]),
3043 ("b1", &vec![4, 5, 5]), ("c1", &vec![7, 8, 9]),
3045 );
3046 let right = build_table(
3047 ("a2", &vec![10, 20, 30]),
3048 ("b2", &vec![4, 5, 6]),
3049 ("c2", &vec![70, 80, 90]),
3050 );
3051 let on = vec![(
3052 Arc::new(Column::new_with_schema("b1", &left.schema())?) as _,
3053 Arc::new(Column::new_with_schema("b2", &right.schema())?) as _,
3054 )];
3055
3056 let (columns, batches, metrics) = join_collect(
3057 left,
3058 right,
3059 on,
3060 &JoinType::Inner,
3061 NullEquality::NullEqualsNothing,
3062 task_ctx,
3063 )
3064 .await?;
3065
3066 assert_eq!(columns, vec!["a1", "b1", "c1", "a2", "b2", "c2"]);
3067
3068 allow_duplicates! {
3070 assert_snapshot!(batches_to_string(&batches), @r"
3071 +----+----+----+----+----+----+
3072 | a1 | b1 | c1 | a2 | b2 | c2 |
3073 +----+----+----+----+----+----+
3074 | 1 | 4 | 7 | 10 | 4 | 70 |
3075 | 2 | 5 | 8 | 20 | 5 | 80 |
3076 | 3 | 5 | 9 | 20 | 5 | 80 |
3077 +----+----+----+----+----+----+
3078 ");
3079 }
3080
3081 assert_join_metrics!(metrics, 3);
3082
3083 Ok(())
3084 }
3085
3086 #[tokio::test]
3087 async fn join_inner_one_randomly_ordered() -> Result<()> {
3088 let task_ctx = Arc::new(TaskContext::default());
3089 let left = build_table(
3090 ("a1", &vec![0, 3, 2, 1]),
3091 ("b1", &vec![4, 5, 5, 4]),
3092 ("c1", &vec![6, 9, 8, 7]),
3093 );
3094 let right = build_table(
3095 ("a2", &vec![20, 30, 10]),
3096 ("b2", &vec![5, 6, 4]),
3097 ("c2", &vec![80, 90, 70]),
3098 );
3099 let on = vec![(
3100 Arc::new(Column::new_with_schema("b1", &left.schema())?) as _,
3101 Arc::new(Column::new_with_schema("b2", &right.schema())?) as _,
3102 )];
3103
3104 let (columns, batches, metrics) = join_collect(
3105 left,
3106 right,
3107 on,
3108 &JoinType::Inner,
3109 NullEquality::NullEqualsNothing,
3110 task_ctx,
3111 )
3112 .await?;
3113
3114 assert_eq!(columns, vec!["a1", "b1", "c1", "a2", "b2", "c2"]);
3115
3116 allow_duplicates! {
3118 assert_snapshot!(batches_to_string(&batches), @r"
3119 +----+----+----+----+----+----+
3120 | a1 | b1 | c1 | a2 | b2 | c2 |
3121 +----+----+----+----+----+----+
3122 | 3 | 5 | 9 | 20 | 5 | 80 |
3123 | 2 | 5 | 8 | 20 | 5 | 80 |
3124 | 0 | 4 | 6 | 10 | 4 | 70 |
3125 | 1 | 4 | 7 | 10 | 4 | 70 |
3126 +----+----+----+----+----+----+
3127 ");
3128 }
3129
3130 assert_join_metrics!(metrics, 4);
3131
3132 Ok(())
3133 }
3134
3135 #[apply(hash_join_exec_configs)]
3136 #[tokio::test]
3137 async fn join_inner_two(
3138 batch_size: usize,
3139 use_perfect_hash_join_as_possible: bool,
3140 ) -> Result<()> {
3141 let task_ctx = prepare_task_ctx(batch_size, use_perfect_hash_join_as_possible);
3142 let left = build_table(
3143 ("a1", &vec![1, 2, 2]),
3144 ("b2", &vec![1, 2, 2]),
3145 ("c1", &vec![7, 8, 9]),
3146 );
3147 let right = build_table(
3148 ("a1", &vec![1, 2, 3]),
3149 ("b2", &vec![1, 2, 2]),
3150 ("c2", &vec![70, 80, 90]),
3151 );
3152 let on = vec![
3153 (
3154 Arc::new(Column::new_with_schema("a1", &left.schema())?) as _,
3155 Arc::new(Column::new_with_schema("a1", &right.schema())?) as _,
3156 ),
3157 (
3158 Arc::new(Column::new_with_schema("b2", &left.schema())?) as _,
3159 Arc::new(Column::new_with_schema("b2", &right.schema())?) as _,
3160 ),
3161 ];
3162
3163 let (columns, batches, metrics) = join_collect(
3164 left,
3165 right,
3166 on,
3167 &JoinType::Inner,
3168 NullEquality::NullEqualsNothing,
3169 task_ctx,
3170 )
3171 .await?;
3172
3173 assert_eq!(columns, vec!["a1", "b2", "c1", "a1", "b2", "c2"]);
3174
3175 let expected_batch_count = if cfg!(not(feature = "force_hash_collisions")) {
3176 let mut expected_batch_count = div_ceil(3, batch_size);
3179 if batch_size == 1 {
3180 expected_batch_count += 1;
3181 }
3182 expected_batch_count
3183 } else {
3184 div_ceil(9, batch_size)
3187 };
3188
3189 assert!(
3191 batches.len() <= expected_batch_count,
3192 "expected at most {expected_batch_count} batches, got {}",
3193 batches.len()
3194 );
3195
3196 allow_duplicates! {
3198 assert_snapshot!(batches_to_string(&batches), @r"
3199 +----+----+----+----+----+----+
3200 | a1 | b2 | c1 | a1 | b2 | c2 |
3201 +----+----+----+----+----+----+
3202 | 1 | 1 | 7 | 1 | 1 | 70 |
3203 | 2 | 2 | 8 | 2 | 2 | 80 |
3204 | 2 | 2 | 9 | 2 | 2 | 80 |
3205 +----+----+----+----+----+----+
3206 ");
3207 }
3208
3209 assert_join_metrics!(metrics, 3);
3210
3211 Ok(())
3212 }
3213
3214 #[apply(hash_join_exec_configs)]
3216 #[tokio::test]
3217 async fn join_inner_one_two_parts_left(
3218 batch_size: usize,
3219 use_perfect_hash_join_as_possible: bool,
3220 ) -> Result<()> {
3221 let task_ctx = prepare_task_ctx(batch_size, use_perfect_hash_join_as_possible);
3222 let batch1 = build_table_i32(
3223 ("a1", &vec![1, 2]),
3224 ("b2", &vec![1, 2]),
3225 ("c1", &vec![7, 8]),
3226 );
3227 let batch2 =
3228 build_table_i32(("a1", &vec![2]), ("b2", &vec![2]), ("c1", &vec![9]));
3229 let schema = batch1.schema();
3230 let left =
3231 TestMemoryExec::try_new_exec(&[vec![batch1], vec![batch2]], schema, None)
3232 .unwrap();
3233 let left = Arc::new(CoalescePartitionsExec::new(left));
3234
3235 let right = build_table(
3236 ("a1", &vec![1, 2, 3]),
3237 ("b2", &vec![1, 2, 2]),
3238 ("c2", &vec![70, 80, 90]),
3239 );
3240 let on = vec![
3241 (
3242 Arc::new(Column::new_with_schema("a1", &left.schema())?) as _,
3243 Arc::new(Column::new_with_schema("a1", &right.schema())?) as _,
3244 ),
3245 (
3246 Arc::new(Column::new_with_schema("b2", &left.schema())?) as _,
3247 Arc::new(Column::new_with_schema("b2", &right.schema())?) as _,
3248 ),
3249 ];
3250
3251 let (columns, batches, metrics) = join_collect(
3252 left,
3253 right,
3254 on,
3255 &JoinType::Inner,
3256 NullEquality::NullEqualsNothing,
3257 task_ctx,
3258 )
3259 .await?;
3260
3261 assert_eq!(columns, vec!["a1", "b2", "c1", "a1", "b2", "c2"]);
3262
3263 let expected_batch_count = if cfg!(not(feature = "force_hash_collisions")) {
3264 let mut expected_batch_count = div_ceil(3, batch_size);
3267 if batch_size == 1 {
3268 expected_batch_count += 1;
3269 }
3270 expected_batch_count
3271 } else {
3272 div_ceil(9, batch_size)
3275 };
3276
3277 assert!(
3279 batches.len() <= expected_batch_count,
3280 "expected at most {expected_batch_count} batches, got {}",
3281 batches.len()
3282 );
3283
3284 allow_duplicates! {
3286 assert_snapshot!(batches_to_string(&batches), @r"
3287 +----+----+----+----+----+----+
3288 | a1 | b2 | c1 | a1 | b2 | c2 |
3289 +----+----+----+----+----+----+
3290 | 1 | 1 | 7 | 1 | 1 | 70 |
3291 | 2 | 2 | 8 | 2 | 2 | 80 |
3292 | 2 | 2 | 9 | 2 | 2 | 80 |
3293 +----+----+----+----+----+----+
3294 ");
3295 }
3296
3297 assert_join_metrics!(metrics, 3);
3298
3299 Ok(())
3300 }
3301
3302 #[tokio::test]
3303 async fn join_inner_one_two_parts_left_randomly_ordered() -> Result<()> {
3304 let task_ctx = Arc::new(TaskContext::default());
3305 let batch1 = build_table_i32(
3306 ("a1", &vec![0, 3]),
3307 ("b1", &vec![4, 5]),
3308 ("c1", &vec![6, 9]),
3309 );
3310 let batch2 = build_table_i32(
3311 ("a1", &vec![2, 1]),
3312 ("b1", &vec![5, 4]),
3313 ("c1", &vec![8, 7]),
3314 );
3315 let schema = batch1.schema();
3316
3317 let left =
3318 TestMemoryExec::try_new_exec(&[vec![batch1], vec![batch2]], schema, None)
3319 .unwrap();
3320 let left = Arc::new(CoalescePartitionsExec::new(left));
3321 let right = build_table(
3322 ("a2", &vec![20, 30, 10]),
3323 ("b2", &vec![5, 6, 4]),
3324 ("c2", &vec![80, 90, 70]),
3325 );
3326 let on = vec![(
3327 Arc::new(Column::new_with_schema("b1", &left.schema())?) as _,
3328 Arc::new(Column::new_with_schema("b2", &right.schema())?) as _,
3329 )];
3330
3331 let (columns, batches, metrics) = join_collect(
3332 left,
3333 right,
3334 on,
3335 &JoinType::Inner,
3336 NullEquality::NullEqualsNothing,
3337 task_ctx,
3338 )
3339 .await?;
3340
3341 assert_eq!(columns, vec!["a1", "b1", "c1", "a2", "b2", "c2"]);
3342
3343 allow_duplicates! {
3345 assert_snapshot!(batches_to_string(&batches), @r"
3346 +----+----+----+----+----+----+
3347 | a1 | b1 | c1 | a2 | b2 | c2 |
3348 +----+----+----+----+----+----+
3349 | 3 | 5 | 9 | 20 | 5 | 80 |
3350 | 2 | 5 | 8 | 20 | 5 | 80 |
3351 | 0 | 4 | 6 | 10 | 4 | 70 |
3352 | 1 | 4 | 7 | 10 | 4 | 70 |
3353 +----+----+----+----+----+----+
3354 ");
3355 }
3356
3357 assert_join_metrics!(metrics, 4);
3358
3359 Ok(())
3360 }
3361
3362 #[apply(hash_join_exec_configs)]
3364 #[tokio::test]
3365 async fn join_inner_one_two_parts_right(
3366 batch_size: usize,
3367 use_perfect_hash_join_as_possible: bool,
3368 ) -> Result<()> {
3369 let task_ctx = prepare_task_ctx(batch_size, use_perfect_hash_join_as_possible);
3370 let left = build_table(
3371 ("a1", &vec![1, 2, 3]),
3372 ("b1", &vec![4, 5, 5]), ("c1", &vec![7, 8, 9]),
3374 );
3375
3376 let batch1 = build_table_i32(
3377 ("a2", &vec![10, 20]),
3378 ("b1", &vec![4, 6]),
3379 ("c2", &vec![70, 80]),
3380 );
3381 let batch2 =
3382 build_table_i32(("a2", &vec![30]), ("b1", &vec![5]), ("c2", &vec![90]));
3383 let schema = batch1.schema();
3384 let right =
3385 TestMemoryExec::try_new_exec(&[vec![batch1], vec![batch2]], schema, None)
3386 .unwrap();
3387
3388 let on = vec![(
3389 Arc::new(Column::new_with_schema("b1", &left.schema())?) as _,
3390 Arc::new(Column::new_with_schema("b1", &right.schema())?) as _,
3391 )];
3392
3393 let join = join(
3394 left,
3395 right,
3396 on,
3397 &JoinType::Inner,
3398 NullEquality::NullEqualsNothing,
3399 )?;
3400
3401 let columns = columns(&join.schema());
3402 assert_eq!(columns, vec!["a1", "b1", "c1", "a2", "b1", "c2"]);
3403
3404 let stream = join.execute(0, Arc::clone(&task_ctx))?;
3406 let batches = common::collect(stream).await?;
3407
3408 let expected_batch_count = if cfg!(not(feature = "force_hash_collisions")) {
3409 let mut expected_batch_count = div_ceil(1, batch_size);
3412 if batch_size == 1 {
3413 expected_batch_count += 1;
3414 }
3415 expected_batch_count
3416 } else {
3417 div_ceil(6, batch_size)
3420 };
3421 assert!(
3423 batches.len() <= expected_batch_count,
3424 "expected at most {expected_batch_count} batches, got {}",
3425 batches.len()
3426 );
3427
3428 allow_duplicates! {
3430 assert_snapshot!(batches_to_string(&batches), @r"
3431 +----+----+----+----+----+----+
3432 | a1 | b1 | c1 | a2 | b1 | c2 |
3433 +----+----+----+----+----+----+
3434 | 1 | 4 | 7 | 10 | 4 | 70 |
3435 +----+----+----+----+----+----+
3436 ");
3437 }
3438
3439 let stream = join.execute(1, Arc::clone(&task_ctx))?;
3441 let batches = common::collect(stream).await?;
3442
3443 let expected_batch_count = if cfg!(not(feature = "force_hash_collisions")) {
3444 div_ceil(2, batch_size)
3446 } else {
3447 div_ceil(3, batch_size)
3450 };
3451 assert!(
3453 batches.len() <= expected_batch_count,
3454 "expected at most {expected_batch_count} batches, got {}",
3455 batches.len()
3456 );
3457
3458 allow_duplicates! {
3460 assert_snapshot!(batches_to_string(&batches), @r"
3461 +----+----+----+----+----+----+
3462 | a1 | b1 | c1 | a2 | b1 | c2 |
3463 +----+----+----+----+----+----+
3464 | 2 | 5 | 8 | 30 | 5 | 90 |
3465 | 3 | 5 | 9 | 30 | 5 | 90 |
3466 +----+----+----+----+----+----+
3467 ");
3468 }
3469
3470 let metrics = join.metrics().unwrap();
3471 assert_phj_used(&metrics, use_perfect_hash_join_as_possible);
3472
3473 Ok(())
3474 }
3475
3476 fn build_table_two_batches(
3477 a: (&str, &Vec<i32>),
3478 b: (&str, &Vec<i32>),
3479 c: (&str, &Vec<i32>),
3480 ) -> Arc<dyn ExecutionPlan> {
3481 let batch = build_table_i32(a, b, c);
3482 let schema = batch.schema();
3483 TestMemoryExec::try_new_exec(&[vec![batch.clone(), batch]], schema, None).unwrap()
3484 }
3485
3486 #[apply(hash_join_exec_configs)]
3487 #[tokio::test]
3488 async fn join_left_multi_batch(
3489 batch_size: usize,
3490 use_perfect_hash_join_as_possible: bool,
3491 ) -> Result<()> {
3492 let task_ctx = prepare_task_ctx(batch_size, use_perfect_hash_join_as_possible);
3493 let left = build_table(
3494 ("a1", &vec![1, 2, 3]),
3495 ("b1", &vec![4, 5, 7]), ("c1", &vec![7, 8, 9]),
3497 );
3498 let right = build_table_two_batches(
3499 ("a2", &vec![10, 20, 30]),
3500 ("b1", &vec![4, 5, 6]),
3501 ("c2", &vec![70, 80, 90]),
3502 );
3503 let on = vec![(
3504 Arc::new(Column::new_with_schema("b1", &left.schema()).unwrap()) as _,
3505 Arc::new(Column::new_with_schema("b1", &right.schema()).unwrap()) as _,
3506 )];
3507
3508 let join = join(
3509 Arc::clone(&left),
3510 Arc::clone(&right),
3511 on.clone(),
3512 &JoinType::Left,
3513 NullEquality::NullEqualsNothing,
3514 )
3515 .unwrap();
3516
3517 let columns = columns(&join.schema());
3518 assert_eq!(columns, vec!["a1", "b1", "c1", "a2", "b1", "c2"]);
3519
3520 let (_, batches, metrics) = join_collect(
3521 Arc::clone(&left),
3522 Arc::clone(&right),
3523 on.clone(),
3524 &JoinType::Left,
3525 NullEquality::NullEqualsNothing,
3526 task_ctx,
3527 )
3528 .await?;
3529
3530 allow_duplicates! {
3531 assert_snapshot!(batches_to_sort_string(&batches), @r"
3532 +----+----+----+----+----+----+
3533 | a1 | b1 | c1 | a2 | b1 | c2 |
3534 +----+----+----+----+----+----+
3535 | 1 | 4 | 7 | 10 | 4 | 70 |
3536 | 1 | 4 | 7 | 10 | 4 | 70 |
3537 | 2 | 5 | 8 | 20 | 5 | 80 |
3538 | 2 | 5 | 8 | 20 | 5 | 80 |
3539 | 3 | 7 | 9 | | | |
3540 +----+----+----+----+----+----+
3541 ");
3542 }
3543
3544 assert_phj_used(&metrics, use_perfect_hash_join_as_possible);
3545 return Ok(());
3546 }
3547
3548 #[apply(hash_join_exec_configs)]
3549 #[tokio::test]
3550 async fn join_full_multi_batch(
3551 batch_size: usize,
3552 use_perfect_hash_join_as_possible: bool,
3553 ) {
3554 let task_ctx = prepare_task_ctx(batch_size, use_perfect_hash_join_as_possible);
3555 let left = build_table(
3556 ("a1", &vec![1, 2, 3]),
3557 ("b1", &vec![4, 5, 7]), ("c1", &vec![7, 8, 9]),
3559 );
3560 let right = build_table_two_batches(
3562 ("a2", &vec![10, 20, 30]),
3563 ("b2", &vec![4, 5, 6]),
3564 ("c2", &vec![70, 80, 90]),
3565 );
3566 let on = vec![(
3567 Arc::new(Column::new_with_schema("b1", &left.schema()).unwrap()) as _,
3568 Arc::new(Column::new_with_schema("b2", &right.schema()).unwrap()) as _,
3569 )];
3570
3571 let join = join(
3572 left,
3573 right,
3574 on,
3575 &JoinType::Full,
3576 NullEquality::NullEqualsNothing,
3577 )
3578 .unwrap();
3579
3580 let columns = columns(&join.schema());
3581 assert_eq!(columns, vec!["a1", "b1", "c1", "a2", "b2", "c2"]);
3582
3583 let stream = join.execute(0, task_ctx).unwrap();
3584 let batches = common::collect(stream).await.unwrap();
3585 let metrics = join.metrics().unwrap();
3586
3587 allow_duplicates! {
3588 assert_snapshot!(batches_to_sort_string(&batches), @r"
3589 +----+----+----+----+----+----+
3590 | a1 | b1 | c1 | a2 | b2 | c2 |
3591 +----+----+----+----+----+----+
3592 | | | | 30 | 6 | 90 |
3593 | | | | 30 | 6 | 90 |
3594 | 1 | 4 | 7 | 10 | 4 | 70 |
3595 | 1 | 4 | 7 | 10 | 4 | 70 |
3596 | 2 | 5 | 8 | 20 | 5 | 80 |
3597 | 2 | 5 | 8 | 20 | 5 | 80 |
3598 | 3 | 7 | 9 | | | |
3599 +----+----+----+----+----+----+
3600 ");
3601 }
3602
3603 assert_phj_used(&metrics, use_perfect_hash_join_as_possible);
3604 }
3605
3606 #[apply(hash_join_exec_configs)]
3607 #[tokio::test]
3608 async fn join_left_empty_right(
3609 batch_size: usize,
3610 use_perfect_hash_join_as_possible: bool,
3611 ) {
3612 let task_ctx = prepare_task_ctx(batch_size, use_perfect_hash_join_as_possible);
3613 let left = build_table(
3614 ("a1", &vec![1, 2, 3]),
3615 ("b1", &vec![4, 5, 7]),
3616 ("c1", &vec![7, 8, 9]),
3617 );
3618 let right = build_table_i32(("a2", &vec![]), ("b1", &vec![]), ("c2", &vec![]));
3619 let on = vec![(
3620 Arc::new(Column::new_with_schema("b1", &left.schema()).unwrap()) as _,
3621 Arc::new(Column::new_with_schema("b1", &right.schema()).unwrap()) as _,
3622 )];
3623 let schema = right.schema();
3624 let right = TestMemoryExec::try_new_exec(&[vec![right]], schema, None).unwrap();
3625 let join = join(
3626 left,
3627 right,
3628 on,
3629 &JoinType::Left,
3630 NullEquality::NullEqualsNothing,
3631 )
3632 .unwrap();
3633
3634 let columns = columns(&join.schema());
3635 assert_eq!(columns, vec!["a1", "b1", "c1", "a2", "b1", "c2"]);
3636
3637 let stream = join.execute(0, task_ctx).unwrap();
3638 let batches = common::collect(stream).await.unwrap();
3639 let metrics = join.metrics().unwrap();
3640
3641 allow_duplicates! {
3642 assert_snapshot!(batches_to_sort_string(&batches), @r"
3643 +----+----+----+----+----+----+
3644 | a1 | b1 | c1 | a2 | b1 | c2 |
3645 +----+----+----+----+----+----+
3646 | 1 | 4 | 7 | | | |
3647 | 2 | 5 | 8 | | | |
3648 | 3 | 7 | 9 | | | |
3649 +----+----+----+----+----+----+
3650 ");
3651 }
3652
3653 assert_phj_used(&metrics, use_perfect_hash_join_as_possible);
3654 }
3655
3656 #[apply(hash_join_exec_configs)]
3657 #[tokio::test]
3658 async fn join_full_empty_right(
3659 batch_size: usize,
3660 use_perfect_hash_join_as_possible: bool,
3661 ) {
3662 let task_ctx = prepare_task_ctx(batch_size, use_perfect_hash_join_as_possible);
3663 let left = build_table(
3664 ("a1", &vec![1, 2, 3]),
3665 ("b1", &vec![4, 5, 7]),
3666 ("c1", &vec![7, 8, 9]),
3667 );
3668 let right = build_table_i32(("a2", &vec![]), ("b2", &vec![]), ("c2", &vec![]));
3669 let on = vec![(
3670 Arc::new(Column::new_with_schema("b1", &left.schema()).unwrap()) as _,
3671 Arc::new(Column::new_with_schema("b2", &right.schema()).unwrap()) as _,
3672 )];
3673 let schema = right.schema();
3674 let right = TestMemoryExec::try_new_exec(&[vec![right]], schema, None).unwrap();
3675 let join = join(
3676 left,
3677 right,
3678 on,
3679 &JoinType::Full,
3680 NullEquality::NullEqualsNothing,
3681 )
3682 .unwrap();
3683
3684 let columns = columns(&join.schema());
3685 assert_eq!(columns, vec!["a1", "b1", "c1", "a2", "b2", "c2"]);
3686
3687 let stream = join.execute(0, task_ctx).unwrap();
3688 let batches = common::collect(stream).await.unwrap();
3689 let metrics = join.metrics().unwrap();
3690
3691 allow_duplicates! {
3692 assert_snapshot!(batches_to_sort_string(&batches), @r"
3693 +----+----+----+----+----+----+
3694 | a1 | b1 | c1 | a2 | b2 | c2 |
3695 +----+----+----+----+----+----+
3696 | 1 | 4 | 7 | | | |
3697 | 2 | 5 | 8 | | | |
3698 | 3 | 7 | 9 | | | |
3699 +----+----+----+----+----+----+
3700 ");
3701 }
3702
3703 assert_phj_used(&metrics, use_perfect_hash_join_as_possible);
3704 }
3705
3706 #[apply(hash_join_exec_configs)]
3707 #[tokio::test]
3708 async fn join_left_one(
3709 batch_size: usize,
3710 use_perfect_hash_join_as_possible: bool,
3711 ) -> Result<()> {
3712 let task_ctx = prepare_task_ctx(batch_size, use_perfect_hash_join_as_possible);
3713 let left = build_table(
3714 ("a1", &vec![1, 2, 3]),
3715 ("b1", &vec![4, 5, 7]), ("c1", &vec![7, 8, 9]),
3717 );
3718 let right = build_table(
3719 ("a2", &vec![10, 20, 30]),
3720 ("b1", &vec![4, 5, 6]),
3721 ("c2", &vec![70, 80, 90]),
3722 );
3723 let on = vec![(
3724 Arc::new(Column::new_with_schema("b1", &left.schema())?) as _,
3725 Arc::new(Column::new_with_schema("b1", &right.schema())?) as _,
3726 )];
3727
3728 let (columns, batches, metrics) = join_collect(
3729 Arc::clone(&left),
3730 Arc::clone(&right),
3731 on.clone(),
3732 &JoinType::Left,
3733 NullEquality::NullEqualsNothing,
3734 task_ctx,
3735 )
3736 .await?;
3737
3738 assert_eq!(columns, vec!["a1", "b1", "c1", "a2", "b1", "c2"]);
3739
3740 allow_duplicates! {
3741 assert_snapshot!(batches_to_sort_string(&batches), @r"
3742 +----+----+----+----+----+----+
3743 | a1 | b1 | c1 | a2 | b1 | c2 |
3744 +----+----+----+----+----+----+
3745 | 1 | 4 | 7 | 10 | 4 | 70 |
3746 | 2 | 5 | 8 | 20 | 5 | 80 |
3747 | 3 | 7 | 9 | | | |
3748 +----+----+----+----+----+----+
3749 ");
3750 }
3751
3752 assert_join_metrics!(metrics, 3);
3753 assert_phj_used(&metrics, use_perfect_hash_join_as_possible);
3754
3755 Ok(())
3756 }
3757
3758 #[rstest]
3763 #[tokio::test]
3764 async fn join_all_null_build_keys(
3765 #[values(PartitionMode::CollectLeft, PartitionMode::Partitioned)]
3766 partition_mode: PartitionMode,
3767 ) -> Result<()> {
3768 let left = build_table_two_cols(
3769 ("a1", &vec![Some(1), Some(2)]),
3770 ("b1", &vec![None, None]), );
3772 let right = build_table_two_cols(
3773 ("a2", &vec![Some(10), Some(20), Some(30)]),
3774 ("b1", &vec![Some(4), None, Some(6)]),
3775 );
3776 let on = vec![(
3777 Arc::new(Column::new_with_schema("b1", &left.schema())?) as _,
3778 Arc::new(Column::new_with_schema("b1", &right.schema())?) as _,
3779 )];
3780
3781 for join_type in [
3782 JoinType::Inner,
3783 JoinType::Left,
3784 JoinType::Right,
3785 JoinType::Full,
3786 JoinType::LeftSemi,
3787 JoinType::LeftAnti,
3788 JoinType::RightSemi,
3789 JoinType::RightAnti,
3790 JoinType::LeftMark,
3791 JoinType::RightMark,
3792 ] {
3793 let (_, batches, metrics) = join_collect_with_partition_mode(
3794 Arc::clone(&left),
3795 Arc::clone(&right),
3796 on.clone(),
3797 &join_type,
3798 partition_mode,
3799 NullEquality::NullEqualsNothing,
3800 Arc::new(TaskContext::default()),
3801 )
3802 .await?;
3803
3804 let probe_rows = metrics
3810 .sum_by_name("input_rows")
3811 .map(|v| v.as_usize())
3812 .unwrap_or(0);
3813 if join_type.empty_map_produces_empty_result() {
3814 assert_eq!(
3815 probe_rows, 0,
3816 "{join_type} should skip the probe side for an all-NULL build"
3817 );
3818 } else {
3819 assert!(probe_rows > 0, "{join_type} must scan the probe side");
3820 }
3821
3822 match join_type {
3823 JoinType::Inner | JoinType::LeftSemi | JoinType::RightSemi => {
3824 let num_rows: usize = batches.iter().map(|b| b.num_rows()).sum();
3825 assert_eq!(num_rows, 0, "unexpected rows for {join_type}");
3826 }
3827 JoinType::Left => {
3828 allow_duplicates! {
3829 assert_snapshot!(batches_to_sort_string(&batches), @r"
3830 +----+----+----+----+
3831 | a1 | b1 | a2 | b1 |
3832 +----+----+----+----+
3833 | 1 | | | |
3834 | 2 | | | |
3835 +----+----+----+----+
3836 ");
3837 }
3838 }
3839 JoinType::Right => {
3840 allow_duplicates! {
3841 assert_snapshot!(batches_to_sort_string(&batches), @r"
3842 +----+----+----+----+
3843 | a1 | b1 | a2 | b1 |
3844 +----+----+----+----+
3845 | | | 10 | 4 |
3846 | | | 20 | |
3847 | | | 30 | 6 |
3848 +----+----+----+----+
3849 ");
3850 }
3851 }
3852 JoinType::Full => {
3853 allow_duplicates! {
3854 assert_snapshot!(batches_to_sort_string(&batches), @r"
3855 +----+----+----+----+
3856 | a1 | b1 | a2 | b1 |
3857 +----+----+----+----+
3858 | | | 10 | 4 |
3859 | | | 20 | |
3860 | | | 30 | 6 |
3861 | 1 | | | |
3862 | 2 | | | |
3863 +----+----+----+----+
3864 ");
3865 }
3866 }
3867 JoinType::LeftAnti => {
3868 allow_duplicates! {
3869 assert_snapshot!(batches_to_sort_string(&batches), @r"
3870 +----+----+
3871 | a1 | b1 |
3872 +----+----+
3873 | 1 | |
3874 | 2 | |
3875 +----+----+
3876 ");
3877 }
3878 }
3879 JoinType::RightAnti => {
3880 allow_duplicates! {
3881 assert_snapshot!(batches_to_sort_string(&batches), @r"
3882 +----+----+
3883 | a2 | b1 |
3884 +----+----+
3885 | 10 | 4 |
3886 | 20 | |
3887 | 30 | 6 |
3888 +----+----+
3889 ");
3890 }
3891 }
3892 JoinType::LeftMark => {
3893 allow_duplicates! {
3894 assert_snapshot!(batches_to_sort_string(&batches), @r"
3895 +----+----+-------+
3896 | a1 | b1 | mark |
3897 +----+----+-------+
3898 | 1 | | false |
3899 | 2 | | false |
3900 +----+----+-------+
3901 ");
3902 }
3903 }
3904 JoinType::RightMark => {
3905 allow_duplicates! {
3906 assert_snapshot!(batches_to_sort_string(&batches), @r"
3907 +----+----+-------+
3908 | a2 | b1 | mark |
3909 +----+----+-------+
3910 | 10 | 4 | false |
3911 | 20 | | false |
3912 | 30 | 6 | false |
3913 +----+----+-------+
3914 ");
3915 }
3916 }
3917 }
3918 }
3919
3920 Ok(())
3921 }
3922
3923 #[apply(hash_join_exec_configs)]
3924 #[tokio::test]
3925 async fn partitioned_join_left_one(
3926 batch_size: usize,
3927 use_perfect_hash_join_as_possible: bool,
3928 ) -> Result<()> {
3929 let task_ctx = prepare_task_ctx(batch_size, use_perfect_hash_join_as_possible);
3930 let left = build_table(
3931 ("a1", &vec![1, 2, 3]),
3932 ("b1", &vec![4, 5, 7]), ("c1", &vec![7, 8, 9]),
3934 );
3935 let right = build_table(
3936 ("a2", &vec![10, 20, 30]),
3937 ("b1", &vec![4, 5, 6]),
3938 ("c2", &vec![70, 80, 90]),
3939 );
3940 let on = vec![(
3941 Arc::new(Column::new_with_schema("b1", &left.schema())?) as _,
3942 Arc::new(Column::new_with_schema("b1", &right.schema())?) as _,
3943 )];
3944
3945 let (columns, batches, metrics) = partitioned_join_collect(
3946 Arc::clone(&left),
3947 Arc::clone(&right),
3948 on.clone(),
3949 &JoinType::Left,
3950 NullEquality::NullEqualsNothing,
3951 task_ctx,
3952 )
3953 .await?;
3954
3955 assert_eq!(columns, vec!["a1", "b1", "c1", "a2", "b1", "c2"]);
3956
3957 allow_duplicates! {
3958 assert_snapshot!(batches_to_sort_string(&batches), @r"
3959 +----+----+----+----+----+----+
3960 | a1 | b1 | c1 | a2 | b1 | c2 |
3961 +----+----+----+----+----+----+
3962 | 1 | 4 | 7 | 10 | 4 | 70 |
3963 | 2 | 5 | 8 | 20 | 5 | 80 |
3964 | 3 | 7 | 9 | | | |
3965 +----+----+----+----+----+----+
3966 ");
3967 }
3968
3969 assert_join_metrics!(metrics, 3);
3970 assert_phj_used(&metrics, use_perfect_hash_join_as_possible);
3971
3972 Ok(())
3973 }
3974
3975 fn build_semi_anti_left_table() -> Arc<dyn ExecutionPlan> {
3976 build_table(
3979 ("a1", &vec![1, 3, 5, 7, 9, 11, 13]),
3980 ("b1", &vec![1, 3, 5, 7, 8, 8, 10]),
3981 ("c1", &vec![10, 30, 50, 70, 90, 110, 130]),
3982 )
3983 }
3984
3985 fn build_semi_anti_right_table() -> Arc<dyn ExecutionPlan> {
3986 build_table(
3989 ("a2", &vec![8, 12, 6, 2, 10, 4]),
3990 ("b2", &vec![8, 10, 6, 2, 10, 4]),
3991 ("c2", &vec![20, 40, 60, 80, 100, 120]),
3992 )
3993 }
3994
3995 #[apply(hash_join_exec_configs)]
3996 #[tokio::test]
3997 async fn join_left_semi(
3998 batch_size: usize,
3999 use_perfect_hash_join_as_possible: bool,
4000 ) -> Result<()> {
4001 let task_ctx = prepare_task_ctx(batch_size, use_perfect_hash_join_as_possible);
4002 let left = build_semi_anti_left_table();
4003 let right = build_semi_anti_right_table();
4004 let on = vec![(
4006 Arc::new(Column::new_with_schema("b1", &left.schema())?) as _,
4007 Arc::new(Column::new_with_schema("b2", &right.schema())?) as _,
4008 )];
4009
4010 let join = join(
4011 left,
4012 right,
4013 on,
4014 &JoinType::LeftSemi,
4015 NullEquality::NullEqualsNothing,
4016 )?;
4017
4018 let columns = columns(&join.schema());
4019 assert_eq!(columns, vec!["a1", "b1", "c1"]);
4020
4021 let stream = join.execute(0, task_ctx)?;
4022 let batches = common::collect(stream).await?;
4023
4024 allow_duplicates! {
4026 assert_snapshot!(batches_to_sort_string(&batches), @r"
4027 +----+----+-----+
4028 | a1 | b1 | c1 |
4029 +----+----+-----+
4030 | 11 | 8 | 110 |
4031 | 13 | 10 | 130 |
4032 | 9 | 8 | 90 |
4033 +----+----+-----+
4034 ");
4035 }
4036
4037 let metrics = join.metrics().unwrap();
4038 assert_phj_used(&metrics, use_perfect_hash_join_as_possible);
4039
4040 Ok(())
4041 }
4042
4043 #[apply(hash_join_exec_configs)]
4044 #[tokio::test]
4045 async fn join_left_semi_with_filter(
4046 batch_size: usize,
4047 use_perfect_hash_join_as_possible: bool,
4048 ) -> Result<()> {
4049 let task_ctx = prepare_task_ctx(batch_size, use_perfect_hash_join_as_possible);
4050 let left = build_semi_anti_left_table();
4051 let right = build_semi_anti_right_table();
4052
4053 let on = vec![(
4055 Arc::new(Column::new_with_schema("b1", &left.schema())?) as _,
4056 Arc::new(Column::new_with_schema("b2", &right.schema())?) as _,
4057 )];
4058
4059 let column_indices = vec![ColumnIndex {
4060 index: 0,
4061 side: JoinSide::Right,
4062 }];
4063 let intermediate_schema =
4064 Schema::new(vec![Field::new("x", DataType::Int32, true)]);
4065
4066 let filter_expression = Arc::new(BinaryExpr::new(
4067 Arc::new(Column::new("x", 0)),
4068 Operator::NotEq,
4069 Arc::new(Literal::new(ScalarValue::Int32(Some(10)))),
4070 )) as Arc<dyn PhysicalExpr>;
4071
4072 let filter = JoinFilter::new(
4073 filter_expression,
4074 column_indices.clone(),
4075 Arc::new(intermediate_schema.clone()),
4076 );
4077
4078 let join = join_with_filter(
4079 Arc::clone(&left),
4080 Arc::clone(&right),
4081 on.clone(),
4082 filter,
4083 &JoinType::LeftSemi,
4084 NullEquality::NullEqualsNothing,
4085 )?;
4086
4087 let columns_header = columns(&join.schema());
4088 assert_eq!(columns_header.clone(), vec!["a1", "b1", "c1"]);
4089
4090 let stream = join.execute(0, Arc::clone(&task_ctx))?;
4091 let batches = common::collect(stream).await?;
4092
4093 allow_duplicates! {
4094 assert_snapshot!(batches_to_sort_string(&batches), @r"
4095 +----+----+-----+
4096 | a1 | b1 | c1 |
4097 +----+----+-----+
4098 | 11 | 8 | 110 |
4099 | 13 | 10 | 130 |
4100 | 9 | 8 | 90 |
4101 +----+----+-----+
4102 ");
4103 }
4104
4105 let metrics = join.metrics().unwrap();
4106 assert_phj_used(&metrics, use_perfect_hash_join_as_possible);
4107
4108 let filter_expression = Arc::new(BinaryExpr::new(
4110 Arc::new(Column::new("x", 0)),
4111 Operator::Gt,
4112 Arc::new(Literal::new(ScalarValue::Int32(Some(10)))),
4113 )) as Arc<dyn PhysicalExpr>;
4114 let filter = JoinFilter::new(
4115 filter_expression,
4116 column_indices,
4117 Arc::new(intermediate_schema),
4118 );
4119
4120 let join = join_with_filter(
4121 left,
4122 right,
4123 on,
4124 filter,
4125 &JoinType::LeftSemi,
4126 NullEquality::NullEqualsNothing,
4127 )?;
4128
4129 let columns_header = columns(&join.schema());
4130 assert_eq!(columns_header, vec!["a1", "b1", "c1"]);
4131
4132 let stream = join.execute(0, task_ctx)?;
4133 let batches = common::collect(stream).await?;
4134
4135 allow_duplicates! {
4136 assert_snapshot!(batches_to_sort_string(&batches), @r"
4137 +----+----+-----+
4138 | a1 | b1 | c1 |
4139 +----+----+-----+
4140 | 13 | 10 | 130 |
4141 +----+----+-----+
4142 ");
4143 }
4144
4145 let metrics = join.metrics().unwrap();
4146 assert_phj_used(&metrics, use_perfect_hash_join_as_possible);
4147
4148 Ok(())
4149 }
4150
4151 #[apply(hash_join_exec_configs)]
4152 #[tokio::test]
4153 async fn join_right_semi(
4154 batch_size: usize,
4155 use_perfect_hash_join_as_possible: bool,
4156 ) -> Result<()> {
4157 let task_ctx = prepare_task_ctx(batch_size, use_perfect_hash_join_as_possible);
4158 let left = build_semi_anti_left_table();
4159 let right = build_semi_anti_right_table();
4160
4161 let on = vec![(
4163 Arc::new(Column::new_with_schema("b1", &left.schema())?) as _,
4164 Arc::new(Column::new_with_schema("b2", &right.schema())?) as _,
4165 )];
4166
4167 let join = join(
4168 left,
4169 right,
4170 on,
4171 &JoinType::RightSemi,
4172 NullEquality::NullEqualsNothing,
4173 )?;
4174
4175 let columns = columns(&join.schema());
4176 assert_eq!(columns, vec!["a2", "b2", "c2"]);
4177
4178 let stream = join.execute(0, task_ctx)?;
4179 let batches = common::collect(stream).await?;
4180
4181 allow_duplicates! {
4183 assert_snapshot!(batches_to_string(&batches), @r"
4184 +----+----+-----+
4185 | a2 | b2 | c2 |
4186 +----+----+-----+
4187 | 8 | 8 | 20 |
4188 | 12 | 10 | 40 |
4189 | 10 | 10 | 100 |
4190 +----+----+-----+
4191 ");
4192 }
4193
4194 let metrics = join.metrics().unwrap();
4195 assert_phj_used(&metrics, use_perfect_hash_join_as_possible);
4196
4197 Ok(())
4198 }
4199
4200 #[apply(hash_join_exec_configs)]
4201 #[tokio::test]
4202 async fn join_right_semi_with_filter(
4203 batch_size: usize,
4204 use_perfect_hash_join_as_possible: bool,
4205 ) -> Result<()> {
4206 let task_ctx = prepare_task_ctx(batch_size, use_perfect_hash_join_as_possible);
4207 let left = build_semi_anti_left_table();
4208 let right = build_semi_anti_right_table();
4209
4210 let on = vec![(
4212 Arc::new(Column::new_with_schema("b1", &left.schema())?) as _,
4213 Arc::new(Column::new_with_schema("b2", &right.schema())?) as _,
4214 )];
4215
4216 let column_indices = vec![ColumnIndex {
4217 index: 0,
4218 side: JoinSide::Left,
4219 }];
4220 let intermediate_schema =
4221 Schema::new(vec![Field::new("x", DataType::Int32, true)]);
4222
4223 let filter_expression = Arc::new(BinaryExpr::new(
4224 Arc::new(Column::new("x", 0)),
4225 Operator::NotEq,
4226 Arc::new(Literal::new(ScalarValue::Int32(Some(9)))),
4227 )) as Arc<dyn PhysicalExpr>;
4228
4229 let filter = JoinFilter::new(
4230 filter_expression,
4231 column_indices.clone(),
4232 Arc::new(intermediate_schema.clone()),
4233 );
4234
4235 let join = join_with_filter(
4236 Arc::clone(&left),
4237 Arc::clone(&right),
4238 on.clone(),
4239 filter,
4240 &JoinType::RightSemi,
4241 NullEquality::NullEqualsNothing,
4242 )?;
4243
4244 let columns = columns(&join.schema());
4245 assert_eq!(columns, vec!["a2", "b2", "c2"]);
4246
4247 let stream = join.execute(0, Arc::clone(&task_ctx))?;
4248 let batches = common::collect(stream).await?;
4249
4250 allow_duplicates! {
4252 assert_snapshot!(batches_to_string(&batches), @r"
4253 +----+----+-----+
4254 | a2 | b2 | c2 |
4255 +----+----+-----+
4256 | 8 | 8 | 20 |
4257 | 12 | 10 | 40 |
4258 | 10 | 10 | 100 |
4259 +----+----+-----+
4260 ");
4261 }
4262
4263 let metrics = join.metrics().unwrap();
4264 assert_phj_used(&metrics, use_perfect_hash_join_as_possible);
4265
4266 let filter_expression = Arc::new(BinaryExpr::new(
4268 Arc::new(Column::new("x", 0)),
4269 Operator::Gt,
4270 Arc::new(Literal::new(ScalarValue::Int32(Some(11)))),
4271 )) as Arc<dyn PhysicalExpr>;
4272
4273 let filter = JoinFilter::new(
4274 filter_expression,
4275 column_indices,
4276 Arc::new(intermediate_schema.clone()),
4277 );
4278
4279 let join = join_with_filter(
4280 left,
4281 right,
4282 on,
4283 filter,
4284 &JoinType::RightSemi,
4285 NullEquality::NullEqualsNothing,
4286 )?;
4287 let stream = join.execute(0, task_ctx)?;
4288 let batches = common::collect(stream).await?;
4289
4290 allow_duplicates! {
4292 assert_snapshot!(batches_to_string(&batches), @r"
4293 +----+----+-----+
4294 | a2 | b2 | c2 |
4295 +----+----+-----+
4296 | 12 | 10 | 40 |
4297 | 10 | 10 | 100 |
4298 +----+----+-----+
4299 ");
4300 }
4301
4302 let metrics = join.metrics().unwrap();
4303 assert_phj_used(&metrics, use_perfect_hash_join_as_possible);
4304
4305 Ok(())
4306 }
4307
4308 #[apply(hash_join_exec_configs)]
4309 #[tokio::test]
4310 async fn join_left_anti(
4311 batch_size: usize,
4312 use_perfect_hash_join_as_possible: bool,
4313 ) -> Result<()> {
4314 let task_ctx = prepare_task_ctx(batch_size, use_perfect_hash_join_as_possible);
4315 let left = build_semi_anti_left_table();
4316 let right = build_semi_anti_right_table();
4317 let on = vec![(
4319 Arc::new(Column::new_with_schema("b1", &left.schema())?) as _,
4320 Arc::new(Column::new_with_schema("b2", &right.schema())?) as _,
4321 )];
4322
4323 let join = join(
4324 left,
4325 right,
4326 on,
4327 &JoinType::LeftAnti,
4328 NullEquality::NullEqualsNothing,
4329 )?;
4330
4331 let columns = columns(&join.schema());
4332 assert_eq!(columns, vec!["a1", "b1", "c1"]);
4333
4334 let stream = join.execute(0, task_ctx)?;
4335 let batches = common::collect(stream).await?;
4336
4337 allow_duplicates! {
4338 assert_snapshot!(batches_to_sort_string(&batches), @r"
4339 +----+----+----+
4340 | a1 | b1 | c1 |
4341 +----+----+----+
4342 | 1 | 1 | 10 |
4343 | 3 | 3 | 30 |
4344 | 5 | 5 | 50 |
4345 | 7 | 7 | 70 |
4346 +----+----+----+
4347 ");
4348 }
4349
4350 let metrics = join.metrics().unwrap();
4351 assert_phj_used(&metrics, use_perfect_hash_join_as_possible);
4352
4353 Ok(())
4354 }
4355
4356 #[apply(hash_join_exec_configs)]
4357 #[tokio::test]
4358 async fn join_left_anti_with_filter(
4359 batch_size: usize,
4360 use_perfect_hash_join_as_possible: bool,
4361 ) -> Result<()> {
4362 let task_ctx = prepare_task_ctx(batch_size, use_perfect_hash_join_as_possible);
4363 let left = build_semi_anti_left_table();
4364 let right = build_semi_anti_right_table();
4365 let on = vec![(
4367 Arc::new(Column::new_with_schema("b1", &left.schema())?) as _,
4368 Arc::new(Column::new_with_schema("b2", &right.schema())?) as _,
4369 )];
4370
4371 let column_indices = vec![ColumnIndex {
4372 index: 0,
4373 side: JoinSide::Right,
4374 }];
4375 let intermediate_schema =
4376 Schema::new(vec![Field::new("x", DataType::Int32, true)]);
4377 let filter_expression = Arc::new(BinaryExpr::new(
4378 Arc::new(Column::new("x", 0)),
4379 Operator::NotEq,
4380 Arc::new(Literal::new(ScalarValue::Int32(Some(8)))),
4381 )) as Arc<dyn PhysicalExpr>;
4382
4383 let filter = JoinFilter::new(
4384 filter_expression,
4385 column_indices.clone(),
4386 Arc::new(intermediate_schema.clone()),
4387 );
4388
4389 let join = join_with_filter(
4390 Arc::clone(&left),
4391 Arc::clone(&right),
4392 on.clone(),
4393 filter,
4394 &JoinType::LeftAnti,
4395 NullEquality::NullEqualsNothing,
4396 )?;
4397
4398 let columns_header = columns(&join.schema());
4399 assert_eq!(columns_header, vec!["a1", "b1", "c1"]);
4400
4401 let stream = join.execute(0, Arc::clone(&task_ctx))?;
4402 let batches = common::collect(stream).await?;
4403
4404 allow_duplicates! {
4405 assert_snapshot!(batches_to_sort_string(&batches), @r"
4406 +----+----+-----+
4407 | a1 | b1 | c1 |
4408 +----+----+-----+
4409 | 1 | 1 | 10 |
4410 | 11 | 8 | 110 |
4411 | 3 | 3 | 30 |
4412 | 5 | 5 | 50 |
4413 | 7 | 7 | 70 |
4414 | 9 | 8 | 90 |
4415 +----+----+-----+
4416 ");
4417 }
4418
4419 let metrics = join.metrics().unwrap();
4420 assert_phj_used(&metrics, use_perfect_hash_join_as_possible);
4421
4422 let filter_expression = Arc::new(BinaryExpr::new(
4424 Arc::new(Column::new("x", 0)),
4425 Operator::NotEq,
4426 Arc::new(Literal::new(ScalarValue::Int32(Some(8)))),
4427 )) as Arc<dyn PhysicalExpr>;
4428
4429 let filter = JoinFilter::new(
4430 filter_expression,
4431 column_indices,
4432 Arc::new(intermediate_schema),
4433 );
4434
4435 let join = join_with_filter(
4436 left,
4437 right,
4438 on,
4439 filter,
4440 &JoinType::LeftAnti,
4441 NullEquality::NullEqualsNothing,
4442 )?;
4443
4444 let columns_header = columns(&join.schema());
4445 assert_eq!(columns_header, vec!["a1", "b1", "c1"]);
4446
4447 let stream = join.execute(0, task_ctx)?;
4448 let batches = common::collect(stream).await?;
4449
4450 allow_duplicates! {
4451 assert_snapshot!(batches_to_sort_string(&batches), @r"
4452 +----+----+-----+
4453 | a1 | b1 | c1 |
4454 +----+----+-----+
4455 | 1 | 1 | 10 |
4456 | 11 | 8 | 110 |
4457 | 3 | 3 | 30 |
4458 | 5 | 5 | 50 |
4459 | 7 | 7 | 70 |
4460 | 9 | 8 | 90 |
4461 +----+----+-----+
4462 ");
4463 }
4464
4465 let metrics = join.metrics().unwrap();
4466 assert_phj_used(&metrics, use_perfect_hash_join_as_possible);
4467
4468 Ok(())
4469 }
4470
4471 #[apply(hash_join_exec_configs)]
4472 #[tokio::test]
4473 async fn join_right_anti(
4474 batch_size: usize,
4475 use_perfect_hash_join_as_possible: bool,
4476 ) -> Result<()> {
4477 let task_ctx = prepare_task_ctx(batch_size, use_perfect_hash_join_as_possible);
4478 let left = build_semi_anti_left_table();
4479 let right = build_semi_anti_right_table();
4480 let on = vec![(
4481 Arc::new(Column::new_with_schema("b1", &left.schema())?) as _,
4482 Arc::new(Column::new_with_schema("b2", &right.schema())?) as _,
4483 )];
4484
4485 let join = join(
4486 left,
4487 right,
4488 on,
4489 &JoinType::RightAnti,
4490 NullEquality::NullEqualsNothing,
4491 )?;
4492
4493 let columns = columns(&join.schema());
4494 assert_eq!(columns, vec!["a2", "b2", "c2"]);
4495
4496 let stream = join.execute(0, task_ctx)?;
4497 let batches = common::collect(stream).await?;
4498
4499 allow_duplicates! {
4501 assert_snapshot!(batches_to_string(&batches), @r"
4502 +----+----+-----+
4503 | a2 | b2 | c2 |
4504 +----+----+-----+
4505 | 6 | 6 | 60 |
4506 | 2 | 2 | 80 |
4507 | 4 | 4 | 120 |
4508 +----+----+-----+
4509 ");
4510 }
4511
4512 let metrics = join.metrics().unwrap();
4513 assert_phj_used(&metrics, use_perfect_hash_join_as_possible);
4514
4515 Ok(())
4516 }
4517
4518 #[apply(hash_join_exec_configs)]
4519 #[tokio::test]
4520 async fn join_right_anti_with_filter(
4521 batch_size: usize,
4522 use_perfect_hash_join_as_possible: bool,
4523 ) -> Result<()> {
4524 let task_ctx = prepare_task_ctx(batch_size, use_perfect_hash_join_as_possible);
4525 let left = build_semi_anti_left_table();
4526 let right = build_semi_anti_right_table();
4527 let on = vec![(
4529 Arc::new(Column::new_with_schema("b1", &left.schema())?) as _,
4530 Arc::new(Column::new_with_schema("b2", &right.schema())?) as _,
4531 )];
4532
4533 let column_indices = vec![ColumnIndex {
4534 index: 0,
4535 side: JoinSide::Left,
4536 }];
4537 let intermediate_schema =
4538 Schema::new(vec![Field::new("x", DataType::Int32, true)]);
4539
4540 let filter_expression = Arc::new(BinaryExpr::new(
4541 Arc::new(Column::new("x", 0)),
4542 Operator::NotEq,
4543 Arc::new(Literal::new(ScalarValue::Int32(Some(13)))),
4544 )) as Arc<dyn PhysicalExpr>;
4545
4546 let filter = JoinFilter::new(
4547 filter_expression,
4548 column_indices,
4549 Arc::new(intermediate_schema.clone()),
4550 );
4551
4552 let join = join_with_filter(
4553 Arc::clone(&left),
4554 Arc::clone(&right),
4555 on.clone(),
4556 filter,
4557 &JoinType::RightAnti,
4558 NullEquality::NullEqualsNothing,
4559 )?;
4560
4561 let columns_header = columns(&join.schema());
4562 assert_eq!(columns_header, vec!["a2", "b2", "c2"]);
4563
4564 let stream = join.execute(0, Arc::clone(&task_ctx))?;
4565 let batches = common::collect(stream).await?;
4566
4567 allow_duplicates! {
4569 assert_snapshot!(batches_to_string(&batches), @r"
4570 +----+----+-----+
4571 | a2 | b2 | c2 |
4572 +----+----+-----+
4573 | 12 | 10 | 40 |
4574 | 6 | 6 | 60 |
4575 | 2 | 2 | 80 |
4576 | 10 | 10 | 100 |
4577 | 4 | 4 | 120 |
4578 +----+----+-----+
4579 ");
4580 }
4581
4582 let metrics = join.metrics().unwrap();
4583 assert_phj_used(&metrics, use_perfect_hash_join_as_possible);
4584
4585 let column_indices = vec![ColumnIndex {
4587 index: 1,
4588 side: JoinSide::Right,
4589 }];
4590 let filter_expression = Arc::new(BinaryExpr::new(
4591 Arc::new(Column::new("x", 0)),
4592 Operator::NotEq,
4593 Arc::new(Literal::new(ScalarValue::Int32(Some(8)))),
4594 )) as Arc<dyn PhysicalExpr>;
4595
4596 let filter = JoinFilter::new(
4597 filter_expression,
4598 column_indices,
4599 Arc::new(intermediate_schema),
4600 );
4601
4602 let join = join_with_filter(
4603 left,
4604 right,
4605 on,
4606 filter,
4607 &JoinType::RightAnti,
4608 NullEquality::NullEqualsNothing,
4609 )?;
4610
4611 let columns_header = columns(&join.schema());
4612 assert_eq!(columns_header, vec!["a2", "b2", "c2"]);
4613
4614 let stream = join.execute(0, task_ctx)?;
4615 let batches = common::collect(stream).await?;
4616
4617 allow_duplicates! {
4619 assert_snapshot!(batches_to_string(&batches), @r"
4620 +----+----+-----+
4621 | a2 | b2 | c2 |
4622 +----+----+-----+
4623 | 8 | 8 | 20 |
4624 | 6 | 6 | 60 |
4625 | 2 | 2 | 80 |
4626 | 4 | 4 | 120 |
4627 +----+----+-----+
4628 ");
4629 }
4630
4631 let metrics = join.metrics().unwrap();
4632 assert_phj_used(&metrics, use_perfect_hash_join_as_possible);
4633
4634 Ok(())
4635 }
4636
4637 #[apply(hash_join_exec_configs)]
4638 #[tokio::test]
4639 async fn join_right_one(
4640 batch_size: usize,
4641 use_perfect_hash_join_as_possible: bool,
4642 ) -> Result<()> {
4643 let task_ctx = prepare_task_ctx(batch_size, use_perfect_hash_join_as_possible);
4644 let left = build_table(
4645 ("a1", &vec![1, 2, 3]),
4646 ("b1", &vec![4, 5, 7]),
4647 ("c1", &vec![7, 8, 9]),
4648 );
4649 let right = build_table(
4650 ("a2", &vec![10, 20, 30]),
4651 ("b1", &vec![4, 5, 6]), ("c2", &vec![70, 80, 90]),
4653 );
4654 let on = vec![(
4655 Arc::new(Column::new_with_schema("b1", &left.schema())?) as _,
4656 Arc::new(Column::new_with_schema("b1", &right.schema())?) as _,
4657 )];
4658
4659 let (columns, batches, metrics) = join_collect(
4660 left,
4661 right,
4662 on,
4663 &JoinType::Right,
4664 NullEquality::NullEqualsNothing,
4665 task_ctx,
4666 )
4667 .await?;
4668
4669 assert_eq!(columns, vec!["a1", "b1", "c1", "a2", "b1", "c2"]);
4670
4671 allow_duplicates! {
4672 assert_snapshot!(batches_to_sort_string(&batches), @r"
4673 +----+----+----+----+----+----+
4674 | a1 | b1 | c1 | a2 | b1 | c2 |
4675 +----+----+----+----+----+----+
4676 | | | | 30 | 6 | 90 |
4677 | 1 | 4 | 7 | 10 | 4 | 70 |
4678 | 2 | 5 | 8 | 20 | 5 | 80 |
4679 +----+----+----+----+----+----+
4680 ");
4681 }
4682
4683 assert_join_metrics!(metrics, 3);
4684 assert_phj_used(&metrics, use_perfect_hash_join_as_possible);
4685
4686 Ok(())
4687 }
4688
4689 #[apply(hash_join_exec_configs)]
4690 #[tokio::test]
4691 async fn partitioned_join_right_one(
4692 batch_size: usize,
4693 use_perfect_hash_join_as_possible: bool,
4694 ) -> Result<()> {
4695 let task_ctx = prepare_task_ctx(batch_size, use_perfect_hash_join_as_possible);
4696 let left = build_table(
4697 ("a1", &vec![1, 2, 3]),
4698 ("b1", &vec![4, 5, 7]),
4699 ("c1", &vec![7, 8, 9]),
4700 );
4701 let right = build_table(
4702 ("a2", &vec![10, 20, 30]),
4703 ("b1", &vec![4, 5, 6]), ("c2", &vec![70, 80, 90]),
4705 );
4706 let on = vec![(
4707 Arc::new(Column::new_with_schema("b1", &left.schema())?) as _,
4708 Arc::new(Column::new_with_schema("b1", &right.schema())?) as _,
4709 )];
4710
4711 let (columns, batches, metrics) = partitioned_join_collect(
4712 left,
4713 right,
4714 on,
4715 &JoinType::Right,
4716 NullEquality::NullEqualsNothing,
4717 task_ctx,
4718 )
4719 .await?;
4720
4721 assert_eq!(columns, vec!["a1", "b1", "c1", "a2", "b1", "c2"]);
4722
4723 allow_duplicates! {
4724 assert_snapshot!(batches_to_sort_string(&batches), @r"
4725 +----+----+----+----+----+----+
4726 | a1 | b1 | c1 | a2 | b1 | c2 |
4727 +----+----+----+----+----+----+
4728 | | | | 30 | 6 | 90 |
4729 | 1 | 4 | 7 | 10 | 4 | 70 |
4730 | 2 | 5 | 8 | 20 | 5 | 80 |
4731 +----+----+----+----+----+----+
4732 ");
4733 }
4734
4735 assert_join_metrics!(metrics, 3);
4736 assert_phj_used(&metrics, use_perfect_hash_join_as_possible);
4737
4738 Ok(())
4739 }
4740
4741 #[apply(hash_join_exec_configs)]
4742 #[tokio::test]
4743 async fn join_full_one(
4744 batch_size: usize,
4745 use_perfect_hash_join_as_possible: bool,
4746 ) -> Result<()> {
4747 let task_ctx = prepare_task_ctx(batch_size, use_perfect_hash_join_as_possible);
4748 let left = build_table(
4749 ("a1", &vec![1, 2, 3]),
4750 ("b1", &vec![4, 5, 7]), ("c1", &vec![7, 8, 9]),
4752 );
4753 let right = build_table(
4754 ("a2", &vec![10, 20, 30]),
4755 ("b2", &vec![4, 5, 6]),
4756 ("c2", &vec![70, 80, 90]),
4757 );
4758 let on = vec![(
4759 Arc::new(Column::new_with_schema("b1", &left.schema()).unwrap()) as _,
4760 Arc::new(Column::new_with_schema("b2", &right.schema()).unwrap()) as _,
4761 )];
4762
4763 let join = join(
4764 left,
4765 right,
4766 on,
4767 &JoinType::Full,
4768 NullEquality::NullEqualsNothing,
4769 )?;
4770
4771 let columns = columns(&join.schema());
4772 assert_eq!(columns, vec!["a1", "b1", "c1", "a2", "b2", "c2"]);
4773
4774 let stream = join.execute(0, task_ctx)?;
4775 let batches = common::collect(stream).await?;
4776
4777 allow_duplicates! {
4778 assert_snapshot!(batches_to_sort_string(&batches), @r"
4779 +----+----+----+----+----+----+
4780 | a1 | b1 | c1 | a2 | b2 | c2 |
4781 +----+----+----+----+----+----+
4782 | | | | 30 | 6 | 90 |
4783 | 1 | 4 | 7 | 10 | 4 | 70 |
4784 | 2 | 5 | 8 | 20 | 5 | 80 |
4785 | 3 | 7 | 9 | | | |
4786 +----+----+----+----+----+----+
4787 ");
4788 }
4789
4790 let metrics = join.metrics().unwrap();
4791 assert_phj_used(&metrics, use_perfect_hash_join_as_possible);
4792
4793 Ok(())
4794 }
4795
4796 #[apply(hash_join_exec_configs)]
4797 #[tokio::test]
4798 async fn join_left_mark(
4799 batch_size: usize,
4800 use_perfect_hash_join_as_possible: bool,
4801 ) -> Result<()> {
4802 let task_ctx = prepare_task_ctx(batch_size, use_perfect_hash_join_as_possible);
4803 let left = build_table(
4804 ("a1", &vec![1, 2, 3]),
4805 ("b1", &vec![4, 5, 7]), ("c1", &vec![7, 8, 9]),
4807 );
4808 let right = build_table(
4809 ("a2", &vec![10, 20, 30]),
4810 ("b1", &vec![4, 5, 6]),
4811 ("c2", &vec![70, 80, 90]),
4812 );
4813 let on = vec![(
4814 Arc::new(Column::new_with_schema("b1", &left.schema())?) as _,
4815 Arc::new(Column::new_with_schema("b1", &right.schema())?) as _,
4816 )];
4817
4818 let (columns, batches, metrics) = join_collect(
4819 Arc::clone(&left),
4820 Arc::clone(&right),
4821 on.clone(),
4822 &JoinType::LeftMark,
4823 NullEquality::NullEqualsNothing,
4824 task_ctx,
4825 )
4826 .await?;
4827
4828 assert_eq!(columns, vec!["a1", "b1", "c1", "mark"]);
4829
4830 allow_duplicates! {
4831 assert_snapshot!(batches_to_sort_string(&batches), @r"
4832 +----+----+----+-------+
4833 | a1 | b1 | c1 | mark |
4834 +----+----+----+-------+
4835 | 1 | 4 | 7 | true |
4836 | 2 | 5 | 8 | true |
4837 | 3 | 7 | 9 | false |
4838 +----+----+----+-------+
4839 ");
4840 }
4841
4842 assert_join_metrics!(metrics, 3);
4843 assert_phj_used(&metrics, use_perfect_hash_join_as_possible);
4844
4845 Ok(())
4846 }
4847
4848 #[apply(hash_join_exec_configs)]
4849 #[tokio::test]
4850 async fn partitioned_join_left_mark(
4851 batch_size: usize,
4852 use_perfect_hash_join_as_possible: bool,
4853 ) -> Result<()> {
4854 let task_ctx = prepare_task_ctx(batch_size, use_perfect_hash_join_as_possible);
4855 let left = build_table(
4856 ("a1", &vec![1, 2, 3]),
4857 ("b1", &vec![4, 5, 7]), ("c1", &vec![7, 8, 9]),
4859 );
4860 let right = build_table(
4861 ("a2", &vec![10, 20, 30, 40]),
4862 ("b1", &vec![4, 4, 5, 6]),
4863 ("c2", &vec![60, 70, 80, 90]),
4864 );
4865 let on = vec![(
4866 Arc::new(Column::new_with_schema("b1", &left.schema())?) as _,
4867 Arc::new(Column::new_with_schema("b1", &right.schema())?) as _,
4868 )];
4869
4870 let (columns, batches, metrics) = partitioned_join_collect(
4871 Arc::clone(&left),
4872 Arc::clone(&right),
4873 on.clone(),
4874 &JoinType::LeftMark,
4875 NullEquality::NullEqualsNothing,
4876 task_ctx,
4877 )
4878 .await?;
4879
4880 assert_eq!(columns, vec!["a1", "b1", "c1", "mark"]);
4881
4882 allow_duplicates! {
4883 assert_snapshot!(batches_to_sort_string(&batches), @r"
4884 +----+----+----+-------+
4885 | a1 | b1 | c1 | mark |
4886 +----+----+----+-------+
4887 | 1 | 4 | 7 | true |
4888 | 2 | 5 | 8 | true |
4889 | 3 | 7 | 9 | false |
4890 +----+----+----+-------+
4891 ");
4892 }
4893
4894 assert_join_metrics!(metrics, 3);
4895 assert_phj_used(&metrics, use_perfect_hash_join_as_possible);
4896
4897 Ok(())
4898 }
4899
4900 #[apply(hash_join_exec_configs)]
4901 #[tokio::test]
4902 async fn join_right_mark(
4903 batch_size: usize,
4904 use_perfect_hash_join_as_possible: bool,
4905 ) -> Result<()> {
4906 let task_ctx = prepare_task_ctx(batch_size, use_perfect_hash_join_as_possible);
4907 let left = build_table(
4908 ("a1", &vec![1, 2, 3]),
4909 ("b1", &vec![4, 5, 7]), ("c1", &vec![7, 8, 9]),
4911 );
4912 let right = build_table(
4913 ("a2", &vec![10, 20, 30]),
4914 ("b1", &vec![4, 5, 6]), ("c2", &vec![70, 80, 90]),
4916 );
4917 let on = vec![(
4918 Arc::new(Column::new_with_schema("b1", &left.schema())?) as _,
4919 Arc::new(Column::new_with_schema("b1", &right.schema())?) as _,
4920 )];
4921
4922 let (columns, batches, metrics) = join_collect(
4923 Arc::clone(&left),
4924 Arc::clone(&right),
4925 on.clone(),
4926 &JoinType::RightMark,
4927 NullEquality::NullEqualsNothing,
4928 task_ctx,
4929 )
4930 .await?;
4931
4932 assert_eq!(columns, vec!["a2", "b1", "c2", "mark"]);
4933
4934 let expected = [
4935 "+----+----+----+-------+",
4936 "| a2 | b1 | c2 | mark |",
4937 "+----+----+----+-------+",
4938 "| 10 | 4 | 70 | true |",
4939 "| 20 | 5 | 80 | true |",
4940 "| 30 | 6 | 90 | false |",
4941 "+----+----+----+-------+",
4942 ];
4943 assert_batches_sorted_eq!(expected, &batches);
4944
4945 assert_join_metrics!(metrics, 3);
4946 assert_phj_used(&metrics, use_perfect_hash_join_as_possible);
4947
4948 Ok(())
4949 }
4950
4951 #[apply(hash_join_exec_configs)]
4952 #[tokio::test]
4953 async fn partitioned_join_right_mark(
4954 batch_size: usize,
4955 use_perfect_hash_join_as_possible: bool,
4956 ) -> Result<()> {
4957 let task_ctx = prepare_task_ctx(batch_size, use_perfect_hash_join_as_possible);
4958 let left = build_table(
4959 ("a1", &vec![1, 2, 3]),
4960 ("b1", &vec![4, 5, 7]), ("c1", &vec![7, 8, 9]),
4962 );
4963 let right = build_table(
4964 ("a2", &vec![10, 20, 30, 40]),
4965 ("b1", &vec![4, 4, 5, 6]), ("c2", &vec![60, 70, 80, 90]),
4967 );
4968 let on = vec![(
4969 Arc::new(Column::new_with_schema("b1", &left.schema())?) as _,
4970 Arc::new(Column::new_with_schema("b1", &right.schema())?) as _,
4971 )];
4972
4973 let (columns, batches, metrics) = partitioned_join_collect(
4974 Arc::clone(&left),
4975 Arc::clone(&right),
4976 on.clone(),
4977 &JoinType::RightMark,
4978 NullEquality::NullEqualsNothing,
4979 task_ctx,
4980 )
4981 .await?;
4982
4983 assert_eq!(columns, vec!["a2", "b1", "c2", "mark"]);
4984
4985 let expected = [
4986 "+----+----+----+-------+",
4987 "| a2 | b1 | c2 | mark |",
4988 "+----+----+----+-------+",
4989 "| 10 | 4 | 60 | true |",
4990 "| 20 | 4 | 70 | true |",
4991 "| 30 | 5 | 80 | true |",
4992 "| 40 | 6 | 90 | false |",
4993 "+----+----+----+-------+",
4994 ];
4995 assert_batches_sorted_eq!(expected, &batches);
4996
4997 assert_join_metrics!(metrics, 4);
4998 assert_phj_used(&metrics, use_perfect_hash_join_as_possible);
4999
5000 Ok(())
5001 }
5002
5003 #[test]
5004 fn join_with_hash_collisions_64() -> Result<()> {
5005 let mut hashmap_left = HashTable::with_capacity(4);
5006 let left = build_table_i32(
5007 ("a", &vec![10, 20]),
5008 ("x", &vec![100, 200]),
5009 ("y", &vec![200, 300]),
5010 );
5011
5012 let random_state = RandomState::with_seed(0);
5013 let hashes_buff = &mut vec![0; left.num_rows()];
5014 let hashes = create_hashes([&left.columns()[0]], &random_state, hashes_buff)?;
5015
5016 hashmap_left.insert_unique(hashes[0], (hashes[0], 1), |(h, _)| *h);
5021 hashmap_left.insert_unique(hashes[0], (hashes[0], 2), |(h, _)| *h);
5022
5023 hashmap_left.insert_unique(hashes[1], (hashes[1], 1), |(h, _)| *h);
5024 hashmap_left.insert_unique(hashes[1], (hashes[1], 2), |(h, _)| *h);
5025
5026 let next = vec![2, 0];
5027
5028 let right = build_table_i32(
5029 ("a", &vec![10, 20]),
5030 ("b", &vec![0, 0]),
5031 ("c", &vec![30, 40]),
5032 );
5033
5034 let key_column: PhysicalExprRef = Arc::new(Column::new("a", 0)) as _;
5036
5037 let join_hash_map = JoinHashMapU64::new(hashmap_left, next);
5038
5039 let left_keys_values = key_column.evaluate(&left)?.into_array(left.num_rows())?;
5040 let right_keys_values =
5041 key_column.evaluate(&right)?.into_array(right.num_rows())?;
5042 let mut hashes_buffer = vec![0; right.num_rows()];
5043 create_hashes([&right_keys_values], &random_state, &mut hashes_buffer)?;
5044
5045 let mut probe_indices_buffer = Vec::new();
5046 let mut build_indices_buffer = Vec::new();
5047 let (l, r, _) = lookup_join_hashmap(
5048 &join_hash_map,
5049 &[left_keys_values],
5050 &[right_keys_values],
5051 NullEquality::NullEqualsNothing,
5052 &hashes_buffer,
5053 None,
5054 8192,
5055 (0, None),
5056 &mut probe_indices_buffer,
5057 &mut build_indices_buffer,
5058 )?;
5059
5060 let left_ids: UInt64Array = vec![0, 1].into();
5061
5062 let right_ids: UInt32Array = vec![0, 1].into();
5063
5064 assert_eq!(left_ids, l);
5065
5066 assert_eq!(right_ids, r);
5067
5068 Ok(())
5069 }
5070
5071 #[test]
5072 fn join_with_hash_collisions_u32() -> Result<()> {
5073 let mut hashmap_left = HashTable::with_capacity(4);
5074 let left = build_table_i32(
5075 ("a", &vec![10, 20]),
5076 ("x", &vec![100, 200]),
5077 ("y", &vec![200, 300]),
5078 );
5079
5080 let random_state = RandomState::with_seed(0);
5081 let hashes_buff = &mut vec![0; left.num_rows()];
5082 let hashes = create_hashes([&left.columns()[0]], &random_state, hashes_buff)?;
5083
5084 hashmap_left.insert_unique(hashes[0], (hashes[0], 1u32), |(h, _)| *h);
5085 hashmap_left.insert_unique(hashes[0], (hashes[0], 2u32), |(h, _)| *h);
5086 hashmap_left.insert_unique(hashes[1], (hashes[1], 1u32), |(h, _)| *h);
5087 hashmap_left.insert_unique(hashes[1], (hashes[1], 2u32), |(h, _)| *h);
5088
5089 let next: Vec<u32> = vec![2, 0];
5090
5091 let right = build_table_i32(
5092 ("a", &vec![10, 20]),
5093 ("b", &vec![0, 0]),
5094 ("c", &vec![30, 40]),
5095 );
5096
5097 let key_column: PhysicalExprRef = Arc::new(Column::new("a", 0)) as _;
5098
5099 let join_hash_map = JoinHashMapU32::new(hashmap_left, next);
5100
5101 let left_keys_values = key_column.evaluate(&left)?.into_array(left.num_rows())?;
5102 let right_keys_values =
5103 key_column.evaluate(&right)?.into_array(right.num_rows())?;
5104 let mut hashes_buffer = vec![0; right.num_rows()];
5105 create_hashes([&right_keys_values], &random_state, &mut hashes_buffer)?;
5106
5107 let mut probe_indices_buffer = Vec::new();
5108 let mut build_indices_buffer = Vec::new();
5109 let (l, r, _) = lookup_join_hashmap(
5110 &join_hash_map,
5111 &[left_keys_values],
5112 &[right_keys_values],
5113 NullEquality::NullEqualsNothing,
5114 &hashes_buffer,
5115 None,
5116 8192,
5117 (0, None),
5118 &mut probe_indices_buffer,
5119 &mut build_indices_buffer,
5120 )?;
5121
5122 let left_ids: UInt64Array = vec![0, 1].into();
5124 let right_ids: UInt32Array = vec![0, 1].into();
5125
5126 assert_eq!(left_ids, l);
5127 assert_eq!(right_ids, r);
5128
5129 Ok(())
5130 }
5131
5132 #[tokio::test]
5133 async fn join_with_duplicated_column_names() -> Result<()> {
5134 let task_ctx = Arc::new(TaskContext::default());
5135 let left = build_table(
5136 ("a", &vec![1, 2, 3]),
5137 ("b", &vec![4, 5, 7]),
5138 ("c", &vec![7, 8, 9]),
5139 );
5140 let right = build_table(
5141 ("a", &vec![10, 20, 30]),
5142 ("b", &vec![1, 2, 7]),
5143 ("c", &vec![70, 80, 90]),
5144 );
5145 let on = vec![(
5146 Arc::new(Column::new_with_schema("a", &left.schema()).unwrap()) as _,
5148 Arc::new(Column::new_with_schema("b", &right.schema()).unwrap()) as _,
5149 )];
5150
5151 let join = join(
5152 left,
5153 right,
5154 on,
5155 &JoinType::Inner,
5156 NullEquality::NullEqualsNothing,
5157 )?;
5158
5159 let columns = columns(&join.schema());
5160 assert_eq!(columns, vec!["a", "b", "c", "a", "b", "c"]);
5161
5162 let stream = join.execute(0, task_ctx)?;
5163 let batches = common::collect(stream).await?;
5164
5165 allow_duplicates! {
5166 assert_snapshot!(batches_to_sort_string(&batches), @r"
5167 +---+---+---+----+---+----+
5168 | a | b | c | a | b | c |
5169 +---+---+---+----+---+----+
5170 | 1 | 4 | 7 | 10 | 1 | 70 |
5171 | 2 | 5 | 8 | 20 | 2 | 80 |
5172 +---+---+---+----+---+----+
5173 ");
5174 }
5175
5176 Ok(())
5177 }
5178
5179 fn prepare_join_filter() -> JoinFilter {
5180 let column_indices = vec![
5181 ColumnIndex {
5182 index: 2,
5183 side: JoinSide::Left,
5184 },
5185 ColumnIndex {
5186 index: 2,
5187 side: JoinSide::Right,
5188 },
5189 ];
5190 let intermediate_schema = Schema::new(vec![
5191 Field::new("c", DataType::Int32, true),
5192 Field::new("c", DataType::Int32, true),
5193 ]);
5194 let filter_expression = Arc::new(BinaryExpr::new(
5195 Arc::new(Column::new("c", 0)),
5196 Operator::Gt,
5197 Arc::new(Column::new("c", 1)),
5198 )) as Arc<dyn PhysicalExpr>;
5199
5200 JoinFilter::new(
5201 filter_expression,
5202 column_indices,
5203 Arc::new(intermediate_schema),
5204 )
5205 }
5206
5207 #[apply(hash_join_exec_configs)]
5208 #[tokio::test]
5209 async fn join_inner_with_filter(
5210 batch_size: usize,
5211 use_perfect_hash_join_as_possible: bool,
5212 ) -> Result<()> {
5213 let task_ctx = prepare_task_ctx(batch_size, use_perfect_hash_join_as_possible);
5214 let left = build_table(
5215 ("a", &vec![0, 1, 2, 2]),
5216 ("b", &vec![4, 5, 7, 8]),
5217 ("c", &vec![7, 8, 9, 1]),
5218 );
5219 let right = build_table(
5220 ("a", &vec![10, 20, 30, 40]),
5221 ("b", &vec![2, 2, 3, 4]),
5222 ("c", &vec![7, 5, 6, 4]),
5223 );
5224 let on = vec![(
5225 Arc::new(Column::new_with_schema("a", &left.schema()).unwrap()) as _,
5226 Arc::new(Column::new_with_schema("b", &right.schema()).unwrap()) as _,
5227 )];
5228 let filter = prepare_join_filter();
5229
5230 let join = join_with_filter(
5231 left,
5232 right,
5233 on,
5234 filter,
5235 &JoinType::Inner,
5236 NullEquality::NullEqualsNothing,
5237 )?;
5238
5239 let columns = columns(&join.schema());
5240 assert_eq!(columns, vec!["a", "b", "c", "a", "b", "c"]);
5241
5242 let stream = join.execute(0, task_ctx)?;
5243 let batches = common::collect(stream).await?;
5244
5245 allow_duplicates! {
5246 assert_snapshot!(batches_to_sort_string(&batches), @r"
5247 +---+---+---+----+---+---+
5248 | a | b | c | a | b | c |
5249 +---+---+---+----+---+---+
5250 | 2 | 7 | 9 | 10 | 2 | 7 |
5251 | 2 | 7 | 9 | 20 | 2 | 5 |
5252 +---+---+---+----+---+---+
5253 ");
5254 }
5255
5256 let metrics = join.metrics().unwrap();
5257 assert_phj_used(&metrics, use_perfect_hash_join_as_possible);
5258
5259 Ok(())
5260 }
5261
5262 #[apply(hash_join_exec_configs)]
5263 #[tokio::test]
5264 async fn join_left_with_filter(
5265 batch_size: usize,
5266 use_perfect_hash_join_as_possible: bool,
5267 ) -> Result<()> {
5268 let task_ctx = prepare_task_ctx(batch_size, use_perfect_hash_join_as_possible);
5269 let left = build_table(
5270 ("a", &vec![0, 1, 2, 2]),
5271 ("b", &vec![4, 5, 7, 8]),
5272 ("c", &vec![7, 8, 9, 1]),
5273 );
5274 let right = build_table(
5275 ("a", &vec![10, 20, 30, 40]),
5276 ("b", &vec![2, 2, 3, 4]),
5277 ("c", &vec![7, 5, 6, 4]),
5278 );
5279 let on = vec![(
5280 Arc::new(Column::new_with_schema("a", &left.schema()).unwrap()) as _,
5281 Arc::new(Column::new_with_schema("b", &right.schema()).unwrap()) as _,
5282 )];
5283 let filter = prepare_join_filter();
5284
5285 let join = join_with_filter(
5286 left,
5287 right,
5288 on,
5289 filter,
5290 &JoinType::Left,
5291 NullEquality::NullEqualsNothing,
5292 )?;
5293
5294 let columns = columns(&join.schema());
5295 assert_eq!(columns, vec!["a", "b", "c", "a", "b", "c"]);
5296
5297 let stream = join.execute(0, task_ctx)?;
5298 let batches = common::collect(stream).await?;
5299
5300 allow_duplicates! {
5301 assert_snapshot!(batches_to_sort_string(&batches), @r"
5302 +---+---+---+----+---+---+
5303 | a | b | c | a | b | c |
5304 +---+---+---+----+---+---+
5305 | 0 | 4 | 7 | | | |
5306 | 1 | 5 | 8 | | | |
5307 | 2 | 7 | 9 | 10 | 2 | 7 |
5308 | 2 | 7 | 9 | 20 | 2 | 5 |
5309 | 2 | 8 | 1 | | | |
5310 +---+---+---+----+---+---+
5311 ");
5312 }
5313
5314 let metrics = join.metrics().unwrap();
5315 assert_phj_used(&metrics, use_perfect_hash_join_as_possible);
5316
5317 Ok(())
5318 }
5319
5320 #[apply(hash_join_exec_configs)]
5321 #[tokio::test]
5322 async fn join_right_with_filter(
5323 batch_size: usize,
5324 use_perfect_hash_join_as_possible: bool,
5325 ) -> Result<()> {
5326 let task_ctx = prepare_task_ctx(batch_size, use_perfect_hash_join_as_possible);
5327 let left = build_table(
5328 ("a", &vec![0, 1, 2, 2]),
5329 ("b", &vec![4, 5, 7, 8]),
5330 ("c", &vec![7, 8, 9, 1]),
5331 );
5332 let right = build_table(
5333 ("a", &vec![10, 20, 30, 40]),
5334 ("b", &vec![2, 2, 3, 4]),
5335 ("c", &vec![7, 5, 6, 4]),
5336 );
5337 let on = vec![(
5338 Arc::new(Column::new_with_schema("a", &left.schema()).unwrap()) as _,
5339 Arc::new(Column::new_with_schema("b", &right.schema()).unwrap()) as _,
5340 )];
5341 let filter = prepare_join_filter();
5342
5343 let join = join_with_filter(
5344 left,
5345 right,
5346 on,
5347 filter,
5348 &JoinType::Right,
5349 NullEquality::NullEqualsNothing,
5350 )?;
5351
5352 let columns = columns(&join.schema());
5353 assert_eq!(columns, vec!["a", "b", "c", "a", "b", "c"]);
5354
5355 let stream = join.execute(0, task_ctx)?;
5356 let batches = common::collect(stream).await?;
5357
5358 allow_duplicates! {
5359 assert_snapshot!(batches_to_sort_string(&batches), @r"
5360 +---+---+---+----+---+---+
5361 | a | b | c | a | b | c |
5362 +---+---+---+----+---+---+
5363 | | | | 30 | 3 | 6 |
5364 | | | | 40 | 4 | 4 |
5365 | 2 | 7 | 9 | 10 | 2 | 7 |
5366 | 2 | 7 | 9 | 20 | 2 | 5 |
5367 +---+---+---+----+---+---+
5368 ");
5369 }
5370
5371 let metrics = join.metrics().unwrap();
5372 assert_phj_used(&metrics, use_perfect_hash_join_as_possible);
5373
5374 Ok(())
5375 }
5376
5377 #[apply(hash_join_exec_configs)]
5378 #[tokio::test]
5379 async fn join_full_with_filter(
5380 batch_size: usize,
5381 use_perfect_hash_join_as_possible: bool,
5382 ) -> Result<()> {
5383 let task_ctx = prepare_task_ctx(batch_size, use_perfect_hash_join_as_possible);
5384 let left = build_table(
5385 ("a", &vec![0, 1, 2, 2]),
5386 ("b", &vec![4, 5, 7, 8]),
5387 ("c", &vec![7, 8, 9, 1]),
5388 );
5389 let right = build_table(
5390 ("a", &vec![10, 20, 30, 40]),
5391 ("b", &vec![2, 2, 3, 4]),
5392 ("c", &vec![7, 5, 6, 4]),
5393 );
5394 let on = vec![(
5395 Arc::new(Column::new_with_schema("a", &left.schema()).unwrap()) as _,
5396 Arc::new(Column::new_with_schema("b", &right.schema()).unwrap()) as _,
5397 )];
5398 let filter = prepare_join_filter();
5399
5400 let join = join_with_filter(
5401 left,
5402 right,
5403 on,
5404 filter,
5405 &JoinType::Full,
5406 NullEquality::NullEqualsNothing,
5407 )?;
5408
5409 let columns = columns(&join.schema());
5410 assert_eq!(columns, vec!["a", "b", "c", "a", "b", "c"]);
5411
5412 let stream = join.execute(0, task_ctx)?;
5413 let batches = common::collect(stream).await?;
5414
5415 let expected = [
5416 "+---+---+---+----+---+---+",
5417 "| a | b | c | a | b | c |",
5418 "+---+---+---+----+---+---+",
5419 "| | | | 30 | 3 | 6 |",
5420 "| | | | 40 | 4 | 4 |",
5421 "| 2 | 7 | 9 | 10 | 2 | 7 |",
5422 "| 2 | 7 | 9 | 20 | 2 | 5 |",
5423 "| 0 | 4 | 7 | | | |",
5424 "| 1 | 5 | 8 | | | |",
5425 "| 2 | 8 | 1 | | | |",
5426 "+---+---+---+----+---+---+",
5427 ];
5428 assert_batches_sorted_eq!(expected, &batches);
5429
5430 let metrics = join.metrics().unwrap();
5431 assert_phj_used(&metrics, use_perfect_hash_join_as_possible);
5432
5433 Ok(())
5451 }
5452
5453 #[tokio::test]
5455 async fn test_collect_left_multiple_partitions_join() -> Result<()> {
5456 let task_ctx = Arc::new(TaskContext::default());
5457 let left = build_table(
5458 ("a1", &vec![1, 2, 3]),
5459 ("b1", &vec![4, 5, 7]),
5460 ("c1", &vec![7, 8, 9]),
5461 );
5462 let right = build_table(
5463 ("a2", &vec![10, 20, 30]),
5464 ("b2", &vec![4, 5, 6]),
5465 ("c2", &vec![70, 80, 90]),
5466 );
5467 let on = vec![(
5468 Arc::new(Column::new_with_schema("b1", &left.schema()).unwrap()) as _,
5469 Arc::new(Column::new_with_schema("b2", &right.schema()).unwrap()) as _,
5470 )];
5471
5472 let expected_inner = vec![
5473 "+----+----+----+----+----+----+",
5474 "| a1 | b1 | c1 | a2 | b2 | c2 |",
5475 "+----+----+----+----+----+----+",
5476 "| 1 | 4 | 7 | 10 | 4 | 70 |",
5477 "| 2 | 5 | 8 | 20 | 5 | 80 |",
5478 "+----+----+----+----+----+----+",
5479 ];
5480 let expected_left = vec![
5481 "+----+----+----+----+----+----+",
5482 "| a1 | b1 | c1 | a2 | b2 | c2 |",
5483 "+----+----+----+----+----+----+",
5484 "| 1 | 4 | 7 | 10 | 4 | 70 |",
5485 "| 2 | 5 | 8 | 20 | 5 | 80 |",
5486 "| 3 | 7 | 9 | | | |",
5487 "+----+----+----+----+----+----+",
5488 ];
5489 let expected_right = vec![
5490 "+----+----+----+----+----+----+",
5491 "| a1 | b1 | c1 | a2 | b2 | c2 |",
5492 "+----+----+----+----+----+----+",
5493 "| | | | 30 | 6 | 90 |",
5494 "| 1 | 4 | 7 | 10 | 4 | 70 |",
5495 "| 2 | 5 | 8 | 20 | 5 | 80 |",
5496 "+----+----+----+----+----+----+",
5497 ];
5498 let expected_full = vec![
5499 "+----+----+----+----+----+----+",
5500 "| a1 | b1 | c1 | a2 | b2 | c2 |",
5501 "+----+----+----+----+----+----+",
5502 "| | | | 30 | 6 | 90 |",
5503 "| 1 | 4 | 7 | 10 | 4 | 70 |",
5504 "| 2 | 5 | 8 | 20 | 5 | 80 |",
5505 "| 3 | 7 | 9 | | | |",
5506 "+----+----+----+----+----+----+",
5507 ];
5508 let expected_left_semi = vec![
5509 "+----+----+----+",
5510 "| a1 | b1 | c1 |",
5511 "+----+----+----+",
5512 "| 1 | 4 | 7 |",
5513 "| 2 | 5 | 8 |",
5514 "+----+----+----+",
5515 ];
5516 let expected_left_anti = vec![
5517 "+----+----+----+",
5518 "| a1 | b1 | c1 |",
5519 "+----+----+----+",
5520 "| 3 | 7 | 9 |",
5521 "+----+----+----+",
5522 ];
5523 let expected_right_semi = vec![
5524 "+----+----+----+",
5525 "| a2 | b2 | c2 |",
5526 "+----+----+----+",
5527 "| 10 | 4 | 70 |",
5528 "| 20 | 5 | 80 |",
5529 "+----+----+----+",
5530 ];
5531 let expected_right_anti = vec![
5532 "+----+----+----+",
5533 "| a2 | b2 | c2 |",
5534 "+----+----+----+",
5535 "| 30 | 6 | 90 |",
5536 "+----+----+----+",
5537 ];
5538 let expected_left_mark = vec![
5539 "+----+----+----+-------+",
5540 "| a1 | b1 | c1 | mark |",
5541 "+----+----+----+-------+",
5542 "| 1 | 4 | 7 | true |",
5543 "| 2 | 5 | 8 | true |",
5544 "| 3 | 7 | 9 | false |",
5545 "+----+----+----+-------+",
5546 ];
5547 let expected_right_mark = vec![
5548 "+----+----+----+-------+",
5549 "| a2 | b2 | c2 | mark |",
5550 "+----+----+----+-------+",
5551 "| 10 | 4 | 70 | true |",
5552 "| 20 | 5 | 80 | true |",
5553 "| 30 | 6 | 90 | false |",
5554 "+----+----+----+-------+",
5555 ];
5556
5557 let test_cases = vec![
5558 (JoinType::Inner, expected_inner),
5559 (JoinType::Left, expected_left),
5560 (JoinType::Right, expected_right),
5561 (JoinType::Full, expected_full),
5562 (JoinType::LeftSemi, expected_left_semi),
5563 (JoinType::LeftAnti, expected_left_anti),
5564 (JoinType::RightSemi, expected_right_semi),
5565 (JoinType::RightAnti, expected_right_anti),
5566 (JoinType::LeftMark, expected_left_mark),
5567 (JoinType::RightMark, expected_right_mark),
5568 ];
5569
5570 for (join_type, expected) in test_cases {
5571 let (_, batches, metrics) = join_collect_with_partition_mode(
5572 Arc::clone(&left),
5573 Arc::clone(&right),
5574 on.clone(),
5575 &join_type,
5576 PartitionMode::CollectLeft,
5577 NullEquality::NullEqualsNothing,
5578 Arc::clone(&task_ctx),
5579 )
5580 .await?;
5581 assert_batches_sorted_eq!(expected, &batches);
5582 assert_join_metrics!(metrics, expected.len() - 4);
5583 }
5584
5585 Ok(())
5586 }
5587
5588 #[tokio::test]
5589 async fn join_date32() -> Result<()> {
5590 let schema = Arc::new(Schema::new(vec![
5591 Field::new("date", DataType::Date32, false),
5592 Field::new("n", DataType::Int32, false),
5593 ]));
5594
5595 let dates: ArrayRef = Arc::new(Date32Array::from(vec![19107, 19108, 19109]));
5596 let n: ArrayRef = Arc::new(Int32Array::from(vec![1, 2, 3]));
5597 let batch = RecordBatch::try_new(Arc::clone(&schema), vec![dates, n])?;
5598 let left =
5599 TestMemoryExec::try_new_exec(&[vec![batch]], Arc::clone(&schema), None)
5600 .unwrap();
5601 let dates: ArrayRef = Arc::new(Date32Array::from(vec![19108, 19108, 19109]));
5602 let n: ArrayRef = Arc::new(Int32Array::from(vec![4, 5, 6]));
5603 let batch = RecordBatch::try_new(Arc::clone(&schema), vec![dates, n])?;
5604 let right = TestMemoryExec::try_new_exec(&[vec![batch]], schema, None).unwrap();
5605 let on = vec![(
5606 Arc::new(Column::new_with_schema("date", &left.schema()).unwrap()) as _,
5607 Arc::new(Column::new_with_schema("date", &right.schema()).unwrap()) as _,
5608 )];
5609
5610 let join = join(
5611 left,
5612 right,
5613 on,
5614 &JoinType::Inner,
5615 NullEquality::NullEqualsNothing,
5616 )?;
5617
5618 let task_ctx = Arc::new(TaskContext::default());
5619 let stream = join.execute(0, task_ctx)?;
5620 let batches = common::collect(stream).await?;
5621
5622 allow_duplicates! {
5623 assert_snapshot!(batches_to_sort_string(&batches), @r"
5624 +------------+---+------------+---+
5625 | date | n | date | n |
5626 +------------+---+------------+---+
5627 | 2022-04-26 | 2 | 2022-04-26 | 4 |
5628 | 2022-04-26 | 2 | 2022-04-26 | 5 |
5629 | 2022-04-27 | 3 | 2022-04-27 | 6 |
5630 +------------+---+------------+---+
5631 ");
5632 }
5633
5634 Ok(())
5635 }
5636
5637 #[tokio::test]
5638 async fn join_with_error_right() {
5639 let left = build_table(
5640 ("a1", &vec![1, 2, 3]),
5641 ("b1", &vec![4, 5, 7]),
5642 ("c1", &vec![7, 8, 9]),
5643 );
5644
5645 let err = exec_err!("bad data error");
5648 let right = build_table_i32(("a2", &vec![]), ("b1", &vec![]), ("c2", &vec![]));
5649
5650 let on = vec![(
5651 Arc::new(Column::new_with_schema("b1", &left.schema()).unwrap()) as _,
5652 Arc::new(Column::new_with_schema("b1", &right.schema()).unwrap()) as _,
5653 )];
5654 let schema = right.schema();
5655 let right = build_table_i32(("a2", &vec![]), ("b1", &vec![]), ("c2", &vec![]));
5656 let right_input = Arc::new(MockExec::new(vec![Ok(right), err], schema));
5657
5658 let join_types = vec![
5659 JoinType::Inner,
5660 JoinType::Left,
5661 JoinType::Right,
5662 JoinType::Full,
5663 JoinType::LeftSemi,
5664 JoinType::LeftAnti,
5665 JoinType::RightSemi,
5666 JoinType::RightAnti,
5667 ];
5668
5669 for join_type in join_types {
5670 let join = join(
5671 Arc::clone(&left),
5672 Arc::clone(&right_input) as Arc<dyn ExecutionPlan>,
5673 on.clone(),
5674 &join_type,
5675 NullEquality::NullEqualsNothing,
5676 )
5677 .unwrap();
5678 let task_ctx = Arc::new(TaskContext::default());
5679
5680 let stream = join.execute(0, task_ctx).unwrap();
5681
5682 let result_string = common::collect(stream).await.unwrap_err().to_string();
5684 assert!(
5685 result_string.contains("bad data error"),
5686 "actual: {result_string}"
5687 );
5688 }
5689 }
5690
5691 #[tokio::test]
5692 async fn join_does_not_consume_probe_when_empty_build_fixes_output() {
5693 assert_empty_build_probe_behavior(
5694 &[
5695 JoinType::Inner,
5696 JoinType::Left,
5697 JoinType::LeftSemi,
5698 JoinType::LeftAnti,
5699 JoinType::LeftMark,
5700 JoinType::RightSemi,
5701 ],
5702 false,
5703 false,
5704 )
5705 .await;
5706 }
5707
5708 #[tokio::test]
5709 async fn join_does_not_consume_probe_when_empty_build_fixes_output_with_filter() {
5710 assert_empty_build_probe_behavior(
5711 &[
5712 JoinType::Inner,
5713 JoinType::Left,
5714 JoinType::LeftSemi,
5715 JoinType::LeftAnti,
5716 JoinType::LeftMark,
5717 JoinType::RightSemi,
5718 ],
5719 false,
5720 true,
5721 )
5722 .await;
5723 }
5724
5725 #[tokio::test]
5726 async fn join_still_consumes_probe_when_empty_build_needs_probe_rows() {
5727 assert_empty_build_probe_behavior(
5728 &[
5729 JoinType::Right,
5730 JoinType::Full,
5731 JoinType::RightAnti,
5732 JoinType::RightMark,
5733 ],
5734 true,
5735 false,
5736 )
5737 .await;
5738 }
5739
5740 #[tokio::test]
5741 async fn join_still_consumes_probe_when_empty_build_needs_probe_rows_with_filter() {
5742 assert_empty_build_probe_behavior(
5743 &[
5744 JoinType::Right,
5745 JoinType::Full,
5746 JoinType::RightAnti,
5747 JoinType::RightMark,
5748 ],
5749 true,
5750 true,
5751 )
5752 .await;
5753 }
5754
5755 #[tokio::test]
5756 async fn join_split_batch() {
5757 let left = build_table(
5758 ("a1", &vec![1, 2, 3, 4]),
5759 ("b1", &vec![1, 1, 1, 1]),
5760 ("c1", &vec![0, 0, 0, 0]),
5761 );
5762 let right = build_table(
5763 ("a2", &vec![10, 20, 30, 40, 50]),
5764 ("b2", &vec![1, 1, 1, 1, 1]),
5765 ("c2", &vec![0, 0, 0, 0, 0]),
5766 );
5767 let on = vec![(
5768 Arc::new(Column::new_with_schema("b1", &left.schema()).unwrap()) as _,
5769 Arc::new(Column::new_with_schema("b2", &right.schema()).unwrap()) as _,
5770 )];
5771
5772 let join_types = vec![
5773 JoinType::Inner,
5774 JoinType::Left,
5775 JoinType::Right,
5776 JoinType::Full,
5777 JoinType::RightSemi,
5778 JoinType::RightAnti,
5779 JoinType::LeftSemi,
5780 JoinType::LeftAnti,
5781 ];
5782 let expected_resultset_records = 20;
5783 let common_result = [
5784 "+----+----+----+----+----+----+",
5785 "| a1 | b1 | c1 | a2 | b2 | c2 |",
5786 "+----+----+----+----+----+----+",
5787 "| 1 | 1 | 0 | 10 | 1 | 0 |",
5788 "| 2 | 1 | 0 | 10 | 1 | 0 |",
5789 "| 3 | 1 | 0 | 10 | 1 | 0 |",
5790 "| 4 | 1 | 0 | 10 | 1 | 0 |",
5791 "| 1 | 1 | 0 | 20 | 1 | 0 |",
5792 "| 2 | 1 | 0 | 20 | 1 | 0 |",
5793 "| 3 | 1 | 0 | 20 | 1 | 0 |",
5794 "| 4 | 1 | 0 | 20 | 1 | 0 |",
5795 "| 1 | 1 | 0 | 30 | 1 | 0 |",
5796 "| 2 | 1 | 0 | 30 | 1 | 0 |",
5797 "| 3 | 1 | 0 | 30 | 1 | 0 |",
5798 "| 4 | 1 | 0 | 30 | 1 | 0 |",
5799 "| 1 | 1 | 0 | 40 | 1 | 0 |",
5800 "| 2 | 1 | 0 | 40 | 1 | 0 |",
5801 "| 3 | 1 | 0 | 40 | 1 | 0 |",
5802 "| 4 | 1 | 0 | 40 | 1 | 0 |",
5803 "| 1 | 1 | 0 | 50 | 1 | 0 |",
5804 "| 2 | 1 | 0 | 50 | 1 | 0 |",
5805 "| 3 | 1 | 0 | 50 | 1 | 0 |",
5806 "| 4 | 1 | 0 | 50 | 1 | 0 |",
5807 "+----+----+----+----+----+----+",
5808 ];
5809 let left_batch = [
5810 "+----+----+----+",
5811 "| a1 | b1 | c1 |",
5812 "+----+----+----+",
5813 "| 1 | 1 | 0 |",
5814 "| 2 | 1 | 0 |",
5815 "| 3 | 1 | 0 |",
5816 "| 4 | 1 | 0 |",
5817 "+----+----+----+",
5818 ];
5819 let right_batch = [
5820 "+----+----+----+",
5821 "| a2 | b2 | c2 |",
5822 "+----+----+----+",
5823 "| 10 | 1 | 0 |",
5824 "| 20 | 1 | 0 |",
5825 "| 30 | 1 | 0 |",
5826 "| 40 | 1 | 0 |",
5827 "| 50 | 1 | 0 |",
5828 "+----+----+----+",
5829 ];
5830 let right_empty = [
5831 "+----+----+----+",
5832 "| a2 | b2 | c2 |",
5833 "+----+----+----+",
5834 "+----+----+----+",
5835 ];
5836 let left_empty = [
5837 "+----+----+----+",
5838 "| a1 | b1 | c1 |",
5839 "+----+----+----+",
5840 "+----+----+----+",
5841 ];
5842
5843 for join_type in join_types {
5845 for batch_size in (1..21).rev() {
5846 let task_ctx = prepare_task_ctx(batch_size, true);
5847
5848 let join = join(
5849 Arc::clone(&left),
5850 Arc::clone(&right),
5851 on.clone(),
5852 &join_type,
5853 NullEquality::NullEqualsNothing,
5854 )
5855 .unwrap();
5856
5857 let stream = join.execute(0, task_ctx).unwrap();
5858 let batches = common::collect(stream).await.unwrap();
5859
5860 let expected_batch_count = match join_type {
5865 JoinType::Inner
5866 | JoinType::Right
5867 | JoinType::RightSemi
5868 | JoinType::RightAnti => {
5869 div_ceil(expected_resultset_records, batch_size)
5870 }
5871 _ => div_ceil(expected_resultset_records, batch_size) + 1,
5872 };
5873 assert!(
5875 batches.len() <= expected_batch_count,
5876 "expected at most {expected_batch_count} output batches for {join_type} join with batch_size = {batch_size}, got {}",
5877 batches.len()
5878 );
5879
5880 let expected = match join_type {
5881 JoinType::RightSemi => right_batch.to_vec(),
5882 JoinType::RightAnti => right_empty.to_vec(),
5883 JoinType::LeftSemi => left_batch.to_vec(),
5884 JoinType::LeftAnti => left_empty.to_vec(),
5885 _ => common_result.to_vec(),
5886 };
5887 if batches.is_empty() {
5890 assert!(
5892 matches!(join_type, JoinType::RightAnti | JoinType::LeftAnti),
5893 "Unexpected empty result for {join_type} join"
5894 );
5895 } else {
5896 assert_batches_eq!(expected, &batches);
5897 }
5898 }
5899 }
5900 }
5901
5902 #[tokio::test]
5903 async fn single_partition_join_overallocation() -> Result<()> {
5904 let left = build_table(
5905 ("a1", &vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 0]),
5906 ("b1", &vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 0]),
5907 ("c1", &vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 0]),
5908 );
5909 let right = build_table(
5910 ("a2", &vec![10, 11]),
5911 ("b2", &vec![12, 13]),
5912 ("c2", &vec![14, 15]),
5913 );
5914 let on = vec![(
5915 Arc::new(Column::new_with_schema("a1", &left.schema()).unwrap()) as _,
5916 Arc::new(Column::new_with_schema("b2", &right.schema()).unwrap()) as _,
5917 )];
5918
5919 let join_types = vec![
5920 JoinType::Inner,
5921 JoinType::Left,
5922 JoinType::Right,
5923 JoinType::Full,
5924 JoinType::LeftSemi,
5925 JoinType::LeftAnti,
5926 JoinType::RightSemi,
5927 JoinType::RightAnti,
5928 JoinType::LeftMark,
5929 JoinType::RightMark,
5930 ];
5931
5932 for join_type in join_types {
5933 let runtime = RuntimeEnvBuilder::new()
5934 .with_memory_limit(100, 1.0)
5935 .build_arc()?;
5936 let task_ctx = TaskContext::default().with_runtime(runtime);
5937 let task_ctx = Arc::new(task_ctx);
5938
5939 let join = join(
5940 Arc::clone(&left),
5941 Arc::clone(&right),
5942 on.clone(),
5943 &join_type,
5944 NullEquality::NullEqualsNothing,
5945 )?;
5946
5947 let stream = join.execute(0, task_ctx)?;
5948 let err = common::collect(stream).await.unwrap_err();
5949
5950 assert_contains!(
5952 err.to_string(),
5953 "Resources exhausted: Additional allocation failed for HashJoinInput with top memory consumers (across reservations) as:\n HashJoinInput"
5954 );
5955
5956 assert_contains!(
5957 err.to_string(),
5958 "Failed to allocate additional 120.0 B for HashJoinInput"
5959 );
5960 }
5961
5962 Ok(())
5963 }
5964
5965 #[tokio::test]
5966 async fn build_side_sliced_batches_memory_accounting() -> Result<()> {
5967 let n = 4096;
5973 let v: Vec<i32> = (0..n).collect();
5974 let parent = build_table_i32(("a1", &v), ("b1", &v), ("c1", &v));
5975 let slices: Vec<RecordBatch> =
5976 (0..16).map(|i| parent.slice(i * 256, 256)).collect();
5977 let left =
5978 TestMemoryExec::try_new_exec(&[slices], parent.schema(), None).unwrap();
5979
5980 let right_batch = build_table_i32(
5981 ("a2", &vec![10, 11]),
5982 ("b2", &vec![0, 1]),
5983 ("c2", &vec![14, 15]),
5984 );
5985 let right = TestMemoryExec::try_new_exec(
5986 &[vec![right_batch.clone()]],
5987 right_batch.schema(),
5988 None,
5989 )
5990 .unwrap();
5991 let on = vec![(
5992 Arc::new(Column::new_with_schema("b1", &parent.schema())?) as _,
5993 Arc::new(Column::new_with_schema("b2", &right_batch.schema())?) as _,
5994 )];
5995
5996 let runtime = RuntimeEnvBuilder::new()
5999 .with_memory_limit(400_000, 1.0)
6000 .build_arc()?;
6001 let task_ctx = TaskContext::default().with_runtime(runtime);
6002 let task_ctx = Arc::new(task_ctx);
6003
6004 let join = join(
6005 left,
6006 right,
6007 on,
6008 &JoinType::Inner,
6009 NullEquality::NullEqualsNothing,
6010 )?;
6011
6012 let stream = join.execute(0, task_ctx)?;
6013 let batches = common::collect(stream).await?;
6014 let num_rows: usize = batches.iter().map(|b| b.num_rows()).sum();
6015 assert_eq!(num_rows, 2);
6016
6017 Ok(())
6018 }
6019
6020 #[tokio::test]
6021 async fn partitioned_join_overallocation() -> Result<()> {
6022 let left_batch = build_table_i32(
6025 ("a1", &vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 0]),
6026 ("b1", &vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 0]),
6027 ("c1", &vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 0]),
6028 );
6029 let left = TestMemoryExec::try_new_exec(
6030 &[vec![left_batch.clone()], vec![left_batch.clone()]],
6031 left_batch.schema(),
6032 None,
6033 )
6034 .unwrap();
6035 let right_batch = build_table_i32(
6036 ("a2", &vec![10, 11]),
6037 ("b2", &vec![12, 13]),
6038 ("c2", &vec![14, 15]),
6039 );
6040 let right = TestMemoryExec::try_new_exec(
6041 &[vec![right_batch.clone()], vec![right_batch.clone()]],
6042 right_batch.schema(),
6043 None,
6044 )
6045 .unwrap();
6046 let on = vec![(
6047 Arc::new(Column::new_with_schema("b1", &left_batch.schema())?) as _,
6048 Arc::new(Column::new_with_schema("b2", &right_batch.schema())?) as _,
6049 )];
6050
6051 let join_types = vec![
6052 JoinType::Inner,
6053 JoinType::Left,
6054 JoinType::Right,
6055 JoinType::Full,
6056 JoinType::LeftSemi,
6057 JoinType::LeftAnti,
6058 JoinType::RightSemi,
6059 JoinType::RightAnti,
6060 ];
6061
6062 for join_type in join_types {
6063 let runtime = RuntimeEnvBuilder::new()
6064 .with_memory_limit(100, 1.0)
6065 .build_arc()?;
6066 let session_config = SessionConfig::default().with_batch_size(50);
6067 let task_ctx = TaskContext::default()
6068 .with_session_config(session_config)
6069 .with_runtime(runtime);
6070 let task_ctx = Arc::new(task_ctx);
6071
6072 let join = HashJoinExec::try_new(
6073 Arc::clone(&left) as Arc<dyn ExecutionPlan>,
6074 Arc::clone(&right) as Arc<dyn ExecutionPlan>,
6075 on.clone(),
6076 None,
6077 &join_type,
6078 None,
6079 PartitionMode::Partitioned,
6080 NullEquality::NullEqualsNothing,
6081 false,
6082 )?;
6083
6084 let stream = join.execute(1, task_ctx)?;
6085 let err = common::collect(stream).await.unwrap_err();
6086
6087 assert_contains!(
6089 err.to_string(),
6090 "Resources exhausted: Additional allocation failed for HashJoinInput[1] with top memory consumers (across reservations) as:\n HashJoinInput[1]"
6091 );
6092
6093 assert_contains!(
6094 err.to_string(),
6095 "Failed to allocate additional 120.0 B for HashJoinInput[1]"
6096 );
6097 }
6098
6099 Ok(())
6100 }
6101
6102 fn build_table_struct(
6103 struct_name: &str,
6104 field_name_and_values: (&str, &Vec<Option<i32>>),
6105 nulls: Option<NullBuffer>,
6106 ) -> Arc<dyn ExecutionPlan> {
6107 let (field_name, values) = field_name_and_values;
6108 let inner_fields = vec![Field::new(field_name, DataType::Int32, true)];
6109 let schema = Schema::new(vec![Field::new(
6110 struct_name,
6111 DataType::Struct(inner_fields.clone().into()),
6112 nulls.is_some(),
6113 )]);
6114
6115 let batch = RecordBatch::try_new(
6116 Arc::new(schema),
6117 vec![Arc::new(StructArray::new(
6118 inner_fields.into(),
6119 vec![Arc::new(Int32Array::from(values.clone()))],
6120 nulls,
6121 ))],
6122 )
6123 .unwrap();
6124 let schema_ref = batch.schema();
6125 TestMemoryExec::try_new_exec(&[vec![batch]], schema_ref, None).unwrap()
6126 }
6127
6128 #[tokio::test]
6129 async fn join_on_struct() -> Result<()> {
6130 let task_ctx = Arc::new(TaskContext::default());
6131 let left =
6132 build_table_struct("n1", ("a", &vec![None, Some(1), Some(2), Some(3)]), None);
6133 let right =
6134 build_table_struct("n2", ("a", &vec![None, Some(1), Some(2), Some(4)]), None);
6135 let on = vec![(
6136 Arc::new(Column::new_with_schema("n1", &left.schema())?) as _,
6137 Arc::new(Column::new_with_schema("n2", &right.schema())?) as _,
6138 )];
6139
6140 let (columns, batches, metrics) = join_collect(
6141 left,
6142 right,
6143 on,
6144 &JoinType::Inner,
6145 NullEquality::NullEqualsNothing,
6146 task_ctx,
6147 )
6148 .await?;
6149
6150 assert_eq!(columns, vec!["n1", "n2"]);
6151
6152 allow_duplicates! {
6153 assert_snapshot!(batches_to_string(&batches), @r"
6154 +--------+--------+
6155 | n1 | n2 |
6156 +--------+--------+
6157 | {a: } | {a: } |
6158 | {a: 1} | {a: 1} |
6159 | {a: 2} | {a: 2} |
6160 +--------+--------+
6161 ");
6162 }
6163
6164 assert_join_metrics!(metrics, 3);
6165
6166 Ok(())
6167 }
6168
6169 #[tokio::test]
6170 async fn join_on_struct_with_nulls() -> Result<()> {
6171 let task_ctx = Arc::new(TaskContext::default());
6172 let left =
6173 build_table_struct("n1", ("a", &vec![None]), Some(NullBuffer::new_null(1)));
6174 let right =
6175 build_table_struct("n2", ("a", &vec![None]), Some(NullBuffer::new_null(1)));
6176 let on = vec![(
6177 Arc::new(Column::new_with_schema("n1", &left.schema())?) as _,
6178 Arc::new(Column::new_with_schema("n2", &right.schema())?) as _,
6179 )];
6180
6181 let (_, batches_null_eq, metrics) = join_collect(
6182 Arc::clone(&left),
6183 Arc::clone(&right),
6184 on.clone(),
6185 &JoinType::Inner,
6186 NullEquality::NullEqualsNull,
6187 Arc::clone(&task_ctx),
6188 )
6189 .await?;
6190
6191 allow_duplicates! {
6192 assert_snapshot!(batches_to_sort_string(&batches_null_eq), @r"
6193 +----+----+
6194 | n1 | n2 |
6195 +----+----+
6196 | | |
6197 +----+----+
6198 ");
6199 }
6200
6201 assert_join_metrics!(metrics, 1);
6202
6203 let (_, batches_null_neq, metrics) = join_collect(
6204 left,
6205 right,
6206 on,
6207 &JoinType::Inner,
6208 NullEquality::NullEqualsNothing,
6209 task_ctx,
6210 )
6211 .await?;
6212
6213 assert_join_metrics!(metrics, 0);
6214
6215 if batches_null_neq.is_empty() {
6218 } else {
6220 let expected_null_neq =
6221 ["+----+----+", "| n1 | n2 |", "+----+----+", "+----+----+"];
6222 assert_batches_eq!(expected_null_neq, &batches_null_neq);
6223 }
6224
6225 Ok(())
6226 }
6227
6228 fn columns(schema: &Schema) -> Vec<String> {
6230 schema.fields().iter().map(|f| f.name().clone()).collect()
6231 }
6232
6233 #[tokio::test]
6235 async fn test_hash_join_marks_filter_complete() -> Result<()> {
6236 let task_ctx = Arc::new(TaskContext::default());
6237 let left = build_table(
6238 ("a1", &vec![1, 2, 3]),
6239 ("b1", &vec![4, 5, 6]),
6240 ("c1", &vec![7, 8, 9]),
6241 );
6242 let right = build_table(
6243 ("a2", &vec![10, 20, 30]),
6244 ("b1", &vec![4, 5, 6]),
6245 ("c2", &vec![70, 80, 90]),
6246 );
6247
6248 let on = vec![(
6249 Arc::new(Column::new_with_schema("b1", &left.schema())?) as _,
6250 Arc::new(Column::new_with_schema("b1", &right.schema())?) as _,
6251 )];
6252
6253 let (join, dynamic_filter) =
6254 hash_join_with_dynamic_filter(left, right, on, JoinType::Inner)?;
6255
6256 let stream = join.execute(0, task_ctx)?;
6258 let _batches = common::collect(stream).await?;
6259
6260 dynamic_filter.wait_complete().await;
6263
6264 Ok(())
6265 }
6266
6267 #[tokio::test]
6269 async fn test_hash_join_marks_filter_complete_empty_build_side() -> Result<()> {
6270 let task_ctx = Arc::new(TaskContext::default());
6271 let left = build_table(("a1", &vec![]), ("b1", &vec![]), ("c1", &vec![]));
6273 let right = build_table(
6274 ("a2", &vec![10, 20, 30]),
6275 ("b1", &vec![4, 5, 6]),
6276 ("c2", &vec![70, 80, 90]),
6277 );
6278
6279 let on = vec![(
6280 Arc::new(Column::new_with_schema("b1", &left.schema())?) as _,
6281 Arc::new(Column::new_with_schema("b1", &right.schema())?) as _,
6282 )];
6283
6284 let (join, dynamic_filter) =
6285 hash_join_with_dynamic_filter(left, right, on, JoinType::Inner)?;
6286
6287 let stream = join.execute(0, task_ctx)?;
6289 let _batches = common::collect(stream).await?;
6290
6291 dynamic_filter.wait_complete().await;
6294
6295 Ok(())
6296 }
6297
6298 #[tokio::test]
6299 async fn test_partitioned_dynamic_filter_reports_empty_canceled_partitions()
6300 -> Result<()> {
6301 let mut session_config = SessionConfig::default();
6302 session_config
6303 .options_mut()
6304 .optimizer
6305 .enable_dynamic_filter_pushdown = true;
6306 let task_ctx =
6307 Arc::new(TaskContext::default().with_session_config(session_config));
6308
6309 let child_left_schema = Arc::new(Schema::new(vec![
6310 Field::new("child_left_payload", DataType::Int32, false),
6311 Field::new("child_key", DataType::Int32, false),
6312 Field::new("child_left_extra", DataType::Int32, false),
6313 ]));
6314 let child_right_schema = Arc::new(Schema::new(vec![
6315 Field::new("child_right_payload", DataType::Int32, false),
6316 Field::new("child_right_key", DataType::Int32, false),
6317 Field::new("child_right_extra", DataType::Int32, false),
6318 ]));
6319 let parent_left_schema = Arc::new(Schema::new(vec![
6320 Field::new("parent_payload", DataType::Int32, false),
6321 Field::new("parent_key", DataType::Int32, false),
6322 Field::new("parent_extra", DataType::Int32, false),
6323 ]));
6324
6325 let child_left: Arc<dyn ExecutionPlan> = TestMemoryExec::try_new_exec(
6326 &[
6327 vec![build_table_i32(
6328 ("child_left_payload", &vec![10]),
6329 ("child_key", &vec![0]),
6330 ("child_left_extra", &vec![100]),
6331 )],
6332 vec![build_table_i32(
6333 ("child_left_payload", &vec![11]),
6334 ("child_key", &vec![1]),
6335 ("child_left_extra", &vec![101]),
6336 )],
6337 vec![build_table_i32(
6338 ("child_left_payload", &vec![12]),
6339 ("child_key", &vec![2]),
6340 ("child_left_extra", &vec![102]),
6341 )],
6342 vec![build_table_i32(
6343 ("child_left_payload", &vec![13]),
6344 ("child_key", &vec![3]),
6345 ("child_left_extra", &vec![103]),
6346 )],
6347 ],
6348 Arc::clone(&child_left_schema),
6349 None,
6350 )?;
6351 let child_right: Arc<dyn ExecutionPlan> = TestMemoryExec::try_new_exec(
6352 &[
6353 vec![build_table_i32(
6354 ("child_right_payload", &vec![20]),
6355 ("child_right_key", &vec![0]),
6356 ("child_right_extra", &vec![200]),
6357 )],
6358 vec![build_table_i32(
6359 ("child_right_payload", &vec![21]),
6360 ("child_right_key", &vec![1]),
6361 ("child_right_extra", &vec![201]),
6362 )],
6363 vec![build_table_i32(
6364 ("child_right_payload", &vec![22]),
6365 ("child_right_key", &vec![2]),
6366 ("child_right_extra", &vec![202]),
6367 )],
6368 vec![build_table_i32(
6369 ("child_right_payload", &vec![23]),
6370 ("child_right_key", &vec![3]),
6371 ("child_right_extra", &vec![203]),
6372 )],
6373 ],
6374 Arc::clone(&child_right_schema),
6375 None,
6376 )?;
6377 let parent_left: Arc<dyn ExecutionPlan> = TestMemoryExec::try_new_exec(
6378 &[
6379 vec![build_table_i32(
6380 ("parent_payload", &vec![30]),
6381 ("parent_key", &vec![0]),
6382 ("parent_extra", &vec![300]),
6383 )],
6384 vec![RecordBatch::new_empty(Arc::clone(&parent_left_schema))],
6385 vec![build_table_i32(
6386 ("parent_payload", &vec![32]),
6387 ("parent_key", &vec![2]),
6388 ("parent_extra", &vec![302]),
6389 )],
6390 vec![RecordBatch::new_empty(Arc::clone(&parent_left_schema))],
6391 ],
6392 Arc::clone(&parent_left_schema),
6393 None,
6394 )?;
6395
6396 let child_on = vec![(
6397 Arc::new(Column::new_with_schema("child_key", &child_left_schema)?) as _,
6398 Arc::new(Column::new_with_schema(
6399 "child_right_key",
6400 &child_right_schema,
6401 )?) as _,
6402 )];
6403 let (child_join, _child_dynamic_filter) = hash_join_with_dynamic_filter_and_mode(
6404 child_left,
6405 child_right,
6406 child_on,
6407 JoinType::Inner,
6408 PartitionMode::Partitioned,
6409 )?;
6410 let child_join: Arc<dyn ExecutionPlan> = Arc::new(child_join);
6411
6412 let parent_on = vec![(
6413 Arc::new(Column::new_with_schema("parent_key", &parent_left_schema)?) as _,
6414 Arc::new(Column::new_with_schema("child_key", &child_join.schema())?) as _,
6415 )];
6416 let parent_join = HashJoinExec::try_new(
6417 parent_left,
6418 child_join,
6419 parent_on,
6420 None,
6421 &JoinType::RightSemi,
6422 None,
6423 PartitionMode::Partitioned,
6424 NullEquality::NullEqualsNothing,
6425 false,
6426 )?;
6427
6428 let batches = tokio::time::timeout(
6429 std::time::Duration::from_secs(5),
6430 crate::execution_plan::collect(Arc::new(parent_join), task_ctx),
6431 )
6432 .await
6433 .expect("partitioned right-semi join should not hang")?;
6434
6435 assert_batches_sorted_eq!(
6436 [
6437 "+--------------------+-----------+------------------+---------------------+-----------------+-------------------+",
6438 "| child_left_payload | child_key | child_left_extra | child_right_payload | child_right_key | child_right_extra |",
6439 "+--------------------+-----------+------------------+---------------------+-----------------+-------------------+",
6440 "| 10 | 0 | 100 | 20 | 0 | 200 |",
6441 "| 12 | 2 | 102 | 22 | 2 | 202 |",
6442 "+--------------------+-----------+------------------+---------------------+-----------------+-------------------+",
6443 ],
6444 &batches
6445 );
6446
6447 Ok(())
6448 }
6449
6450 #[tokio::test]
6451 async fn test_hash_join_skips_probe_on_empty_build_after_partition_bounds_report()
6452 -> Result<()> {
6453 let task_ctx = Arc::new(TaskContext::default());
6454 let (left, right, on) = empty_build_with_probe_error_inputs();
6455
6456 let (join, dynamic_filter) =
6460 hash_join_with_dynamic_filter(left, right, on, JoinType::Inner)?;
6461
6462 let stream = join.execute(0, task_ctx)?;
6463 let batches = common::collect(stream).await?;
6464 assert!(batches.is_empty());
6465
6466 dynamic_filter.wait_complete().await;
6467
6468 Ok(())
6469 }
6470
6471 #[tokio::test]
6472 async fn test_perfect_hash_join_with_negative_numbers() -> Result<()> {
6473 let task_ctx = prepare_task_ctx(8192, true);
6474 let (left_schema, right_schema, on) = build_schema_and_on()?;
6475
6476 let left_batch = RecordBatch::try_new(
6477 Arc::clone(&left_schema),
6478 vec![
6479 Arc::new(Int32Array::from(vec![1, 2, 3])) as ArrayRef,
6480 Arc::new(Int32Array::from(vec![-1, 0, 1])) as ArrayRef,
6481 ],
6482 )?;
6483 let left = TestMemoryExec::try_new_exec(&[vec![left_batch]], left_schema, None)?;
6484
6485 let right_batch = RecordBatch::try_new(
6486 Arc::clone(&right_schema),
6487 vec![
6488 Arc::new(Int32Array::from(vec![10, 20, 30, 40])) as ArrayRef,
6489 Arc::new(Int32Array::from(vec![1, -1, 0, 2])) as ArrayRef,
6490 ],
6491 )?;
6492 let right =
6493 TestMemoryExec::try_new_exec(&[vec![right_batch]], right_schema, None)?;
6494
6495 let (columns, batches, metrics) = join_collect(
6496 left,
6497 right,
6498 on,
6499 &JoinType::Inner,
6500 NullEquality::NullEqualsNothing,
6501 task_ctx,
6502 )
6503 .await?;
6504
6505 assert_eq!(columns, vec!["a1", "b1", "a2", "b1"]);
6506
6507 assert_batches_sorted_eq!(
6508 [
6509 "+----+----+----+----+",
6510 "| a1 | b1 | a2 | b1 |",
6511 "+----+----+----+----+",
6512 "| 1 | -1 | 20 | -1 |",
6513 "| 2 | 0 | 30 | 0 |",
6514 "| 3 | 1 | 10 | 1 |",
6515 "+----+----+----+----+",
6516 ],
6517 &batches
6518 );
6519
6520 assert_phj_used(&metrics, true);
6521
6522 Ok(())
6523 }
6524
6525 #[tokio::test]
6526 async fn test_perfect_hash_join_overflow_full_int64_range() -> Result<()> {
6527 let task_ctx = prepare_task_ctx(8192, true);
6528 let schema = Arc::new(Schema::new(vec![Field::new("a", DataType::Int64, true)]));
6529 let batch = RecordBatch::try_new(
6530 Arc::clone(&schema),
6531 vec![Arc::new(Int64Array::from(vec![i64::MIN, i64::MAX]))],
6532 )?;
6533 let left = TestMemoryExec::try_new_exec(
6534 &[vec![batch.clone()]],
6535 Arc::clone(&schema),
6536 None,
6537 )?;
6538 let right = TestMemoryExec::try_new_exec(&[vec![batch]], schema, None)?;
6539 let on: JoinOn = vec![(
6540 Arc::new(Column::new_with_schema("a", &left.schema())?) as _,
6541 Arc::new(Column::new_with_schema("a", &right.schema())?) as _,
6542 )];
6543 let (_columns, batches, _metrics) = join_collect(
6544 left,
6545 right,
6546 on,
6547 &JoinType::Inner,
6548 NullEquality::NullEqualsNothing,
6549 task_ctx,
6550 )
6551 .await?;
6552 let total_rows: usize = batches.iter().map(|b| b.num_rows()).sum();
6553 assert_eq!(total_rows, 2);
6554 Ok(())
6555 }
6556
6557 #[apply(hash_join_exec_configs)]
6558 #[tokio::test]
6559 async fn test_phj_null_equals_null_build_no_nulls_probe_has_nulls(
6560 batch_size: usize,
6561 use_perfect_hash_join_as_possible: bool,
6562 ) -> Result<()> {
6563 let task_ctx = prepare_task_ctx(batch_size, use_perfect_hash_join_as_possible);
6564 let (left_schema, right_schema, on) = build_schema_and_on()?;
6565
6566 let left_batch = RecordBatch::try_new(
6567 Arc::clone(&left_schema),
6568 vec![
6569 Arc::new(Int32Array::from(vec![1, 2])) as ArrayRef,
6570 Arc::new(Int32Array::from(vec![10, 20])) as ArrayRef,
6571 ],
6572 )?;
6573 let left = TestMemoryExec::try_new_exec(&[vec![left_batch]], left_schema, None)?;
6574
6575 let right_batch = RecordBatch::try_new(
6576 Arc::clone(&right_schema),
6577 vec![
6578 Arc::new(Int32Array::from(vec![3, 4])) as ArrayRef,
6579 Arc::new(Int32Array::from(vec![Some(10), None])) as ArrayRef,
6580 ],
6581 )?;
6582 let right =
6583 TestMemoryExec::try_new_exec(&[vec![right_batch]], right_schema, None)?;
6584
6585 let (columns, batches, metrics) = join_collect(
6586 left,
6587 right,
6588 on,
6589 &JoinType::Inner,
6590 NullEquality::NullEqualsNull,
6591 task_ctx,
6592 )
6593 .await?;
6594
6595 assert_eq!(columns, vec!["a1", "b1", "a2", "b1"]);
6596 assert_batches_sorted_eq!(
6597 [
6598 "+----+----+----+----+",
6599 "| a1 | b1 | a2 | b1 |",
6600 "+----+----+----+----+",
6601 "| 1 | 10 | 3 | 10 |",
6602 "+----+----+----+----+",
6603 ],
6604 &batches
6605 );
6606
6607 assert_phj_used(&metrics, use_perfect_hash_join_as_possible);
6608
6609 Ok(())
6610 }
6611
6612 #[apply(hash_join_exec_configs)]
6613 #[tokio::test]
6614 async fn test_phj_null_equals_nothing_build_probe_all_have_nulls(
6615 batch_size: usize,
6616 use_perfect_hash_join_as_possible: bool,
6617 ) -> Result<()> {
6618 let task_ctx = prepare_task_ctx(batch_size, use_perfect_hash_join_as_possible);
6619 let (left_schema, right_schema, on) = build_schema_and_on()?;
6620
6621 let left_batch = RecordBatch::try_new(
6622 Arc::clone(&left_schema),
6623 vec![
6624 Arc::new(Int32Array::from(vec![Some(1), Some(2)])) as ArrayRef,
6625 Arc::new(Int32Array::from(vec![Some(10), None])) as ArrayRef,
6626 ],
6627 )?;
6628 let left = TestMemoryExec::try_new_exec(&[vec![left_batch]], left_schema, None)?;
6629
6630 let right_batch = RecordBatch::try_new(
6631 Arc::clone(&right_schema),
6632 vec![
6633 Arc::new(Int32Array::from(vec![Some(3), Some(4)])) as ArrayRef,
6634 Arc::new(Int32Array::from(vec![Some(10), None])) as ArrayRef,
6635 ],
6636 )?;
6637 let right =
6638 TestMemoryExec::try_new_exec(&[vec![right_batch]], right_schema, None)?;
6639
6640 let (columns, batches, metrics) = join_collect(
6641 left,
6642 right,
6643 on,
6644 &JoinType::Inner,
6645 NullEquality::NullEqualsNothing,
6646 task_ctx,
6647 )
6648 .await?;
6649
6650 assert_eq!(columns, vec!["a1", "b1", "a2", "b1"]);
6651 assert_batches_sorted_eq!(
6652 [
6653 "+----+----+----+----+",
6654 "| a1 | b1 | a2 | b1 |",
6655 "+----+----+----+----+",
6656 "| 1 | 10 | 3 | 10 |",
6657 "+----+----+----+----+",
6658 ],
6659 &batches
6660 );
6661
6662 assert_phj_used(&metrics, use_perfect_hash_join_as_possible);
6663
6664 Ok(())
6665 }
6666
6667 #[tokio::test]
6668 async fn test_phj_null_equals_null_build_have_nulls() -> Result<()> {
6669 let task_ctx = prepare_task_ctx(8192, true);
6670 let (left_schema, right_schema, on) = build_schema_and_on()?;
6671
6672 let left_batch = RecordBatch::try_new(
6673 Arc::clone(&left_schema),
6674 vec![
6675 Arc::new(Int32Array::from(vec![Some(1), Some(2), Some(3)])) as ArrayRef,
6676 Arc::new(Int32Array::from(vec![Some(10), Some(20), None])) as ArrayRef,
6677 ],
6678 )?;
6679 let left = TestMemoryExec::try_new_exec(&[vec![left_batch]], left_schema, None)?;
6680
6681 let right_batch = RecordBatch::try_new(
6682 Arc::clone(&right_schema),
6683 vec![
6684 Arc::new(Int32Array::from(vec![Some(3), Some(4)])) as ArrayRef,
6685 Arc::new(Int32Array::from(vec![Some(10), Some(30)])) as ArrayRef,
6686 ],
6687 )?;
6688 let right =
6689 TestMemoryExec::try_new_exec(&[vec![right_batch]], right_schema, None)?;
6690
6691 let (columns, batches, metrics) = join_collect(
6692 left,
6693 right,
6694 on,
6695 &JoinType::Inner,
6696 NullEquality::NullEqualsNull,
6697 task_ctx,
6698 )
6699 .await?;
6700
6701 assert_eq!(columns, vec!["a1", "b1", "a2", "b1"]);
6702 assert_batches_sorted_eq!(
6703 [
6704 "+----+----+----+----+",
6705 "| a1 | b1 | a2 | b1 |",
6706 "+----+----+----+----+",
6707 "| 1 | 10 | 3 | 10 |",
6708 "+----+----+----+----+",
6709 ],
6710 &batches
6711 );
6712
6713 assert_phj_used(&metrics, false);
6714
6715 Ok(())
6716 }
6717
6718 #[apply(hash_join_exec_configs)]
6721 #[tokio::test]
6722 async fn test_null_aware_anti_join_probe_null(batch_size: usize) -> Result<()> {
6723 let task_ctx = prepare_task_ctx(batch_size, false);
6724
6725 let left = build_table_two_cols(
6727 ("c1", &vec![Some(1), Some(2), Some(3), Some(4)]),
6728 ("dummy", &vec![Some(10), Some(20), Some(30), Some(40)]),
6729 );
6730
6731 let right = build_table_two_cols(
6733 ("c2", &vec![Some(1), Some(2), Some(3), None]),
6734 ("dummy", &vec![Some(100), Some(200), Some(300), Some(400)]),
6735 );
6736
6737 let on = vec![(
6738 Arc::new(Column::new_with_schema("c1", &left.schema())?) as _,
6739 Arc::new(Column::new_with_schema("c2", &right.schema())?) as _,
6740 )];
6741
6742 let join = HashJoinExec::try_new(
6744 left,
6745 right,
6746 on,
6747 None,
6748 &JoinType::LeftAnti,
6749 None,
6750 PartitionMode::CollectLeft,
6751 NullEquality::NullEqualsNothing,
6752 true, )?;
6754
6755 let stream = join.execute(0, task_ctx)?;
6756 let batches = common::collect(stream).await?;
6757
6758 allow_duplicates! {
6760 assert_snapshot!(batches_to_sort_string(&batches), @r"
6761 ++
6762 ++
6763 ");
6764 }
6765 Ok(())
6766 }
6767
6768 #[apply(hash_join_exec_configs)]
6771 #[tokio::test]
6772 async fn test_null_aware_anti_join_build_null(batch_size: usize) -> Result<()> {
6773 let task_ctx = prepare_task_ctx(batch_size, false);
6774
6775 let left = build_table_two_cols(
6777 ("c1", &vec![Some(1), Some(4), None]),
6778 ("dummy", &vec![Some(10), Some(40), Some(0)]),
6779 );
6780
6781 let right = build_table_two_cols(
6783 ("c2", &vec![Some(1), Some(2), Some(3)]),
6784 ("dummy", &vec![Some(100), Some(200), Some(300)]),
6785 );
6786
6787 let on = vec![(
6788 Arc::new(Column::new_with_schema("c1", &left.schema())?) as _,
6789 Arc::new(Column::new_with_schema("c2", &right.schema())?) as _,
6790 )];
6791
6792 let join = HashJoinExec::try_new(
6794 left,
6795 right,
6796 on,
6797 None,
6798 &JoinType::LeftAnti,
6799 None,
6800 PartitionMode::CollectLeft,
6801 NullEquality::NullEqualsNothing,
6802 true, )?;
6804
6805 let stream = join.execute(0, task_ctx)?;
6806 let batches = common::collect(stream).await?;
6807
6808 allow_duplicates! {
6810 assert_snapshot!(batches_to_sort_string(&batches), @r"
6811 +----+-------+
6812 | c1 | dummy |
6813 +----+-------+
6814 | 4 | 40 |
6815 +----+-------+
6816 ");
6817 }
6818 Ok(())
6819 }
6820
6821 #[apply(hash_join_exec_configs)]
6823 #[tokio::test]
6824 async fn test_null_aware_anti_join_no_nulls(batch_size: usize) -> Result<()> {
6825 let task_ctx = prepare_task_ctx(batch_size, false);
6826
6827 let left = build_table_two_cols(
6829 ("c1", &vec![Some(1), Some(2), Some(4), Some(5)]),
6830 ("dummy", &vec![Some(10), Some(20), Some(40), Some(50)]),
6831 );
6832
6833 let right = build_table_two_cols(
6835 ("c2", &vec![Some(1), Some(2), Some(3)]),
6836 ("dummy", &vec![Some(100), Some(200), Some(300)]),
6837 );
6838
6839 let on = vec![(
6840 Arc::new(Column::new_with_schema("c1", &left.schema())?) as _,
6841 Arc::new(Column::new_with_schema("c2", &right.schema())?) as _,
6842 )];
6843
6844 let join = HashJoinExec::try_new(
6846 left,
6847 right,
6848 on,
6849 None,
6850 &JoinType::LeftAnti,
6851 None,
6852 PartitionMode::CollectLeft,
6853 NullEquality::NullEqualsNothing,
6854 true, )?;
6856
6857 let stream = join.execute(0, task_ctx)?;
6858 let batches = common::collect(stream).await?;
6859
6860 allow_duplicates! {
6862 assert_snapshot!(batches_to_sort_string(&batches), @r"
6863 +----+-------+
6864 | c1 | dummy |
6865 +----+-------+
6866 | 4 | 40 |
6867 | 5 | 50 |
6868 +----+-------+
6869 ");
6870 }
6871 Ok(())
6872 }
6873
6874 #[tokio::test]
6876 async fn test_null_aware_validation_wrong_join_type() {
6877 let left =
6878 build_table_two_cols(("c1", &vec![Some(1)]), ("dummy", &vec![Some(10)]));
6879 let right =
6880 build_table_two_cols(("c2", &vec![Some(1)]), ("dummy", &vec![Some(100)]));
6881
6882 let on = vec![(
6883 Arc::new(Column::new_with_schema("c1", &left.schema()).unwrap()) as _,
6884 Arc::new(Column::new_with_schema("c2", &right.schema()).unwrap()) as _,
6885 )];
6886
6887 let result = HashJoinExec::try_new(
6889 left,
6890 right,
6891 on,
6892 None,
6893 &JoinType::Inner,
6894 None,
6895 PartitionMode::CollectLeft,
6896 NullEquality::NullEqualsNothing,
6897 true, );
6899
6900 assert!(result.is_err());
6901 assert!(
6902 result
6903 .unwrap_err()
6904 .to_string()
6905 .contains("null_aware can only be true for LeftAnti joins")
6906 );
6907 }
6908
6909 #[tokio::test]
6911 async fn test_null_aware_validation_multi_column() {
6912 let left = build_table(("a", &vec![1]), ("b", &vec![2]), ("c", &vec![3]));
6913 let right = build_table(("x", &vec![1]), ("y", &vec![2]), ("z", &vec![3]));
6914
6915 let on = vec![
6917 (
6918 Arc::new(Column::new_with_schema("a", &left.schema()).unwrap()) as _,
6919 Arc::new(Column::new_with_schema("x", &right.schema()).unwrap()) as _,
6920 ),
6921 (
6922 Arc::new(Column::new_with_schema("b", &left.schema()).unwrap()) as _,
6923 Arc::new(Column::new_with_schema("y", &right.schema()).unwrap()) as _,
6924 ),
6925 ];
6926
6927 let result = HashJoinExec::try_new(
6929 left,
6930 right,
6931 on,
6932 None,
6933 &JoinType::LeftAnti,
6934 None,
6935 PartitionMode::CollectLeft,
6936 NullEquality::NullEqualsNothing,
6937 true, );
6939
6940 assert!(result.is_err());
6941 assert!(
6942 result
6943 .unwrap_err()
6944 .to_string()
6945 .contains("null_aware anti join only supports single column join key")
6946 );
6947 }
6948
6949 #[test]
6950 fn test_lr_is_preserved() {
6951 assert_eq!(lr_is_preserved(JoinType::Inner), (true, true));
6952 assert_eq!(lr_is_preserved(JoinType::Left), (true, false));
6953 assert_eq!(lr_is_preserved(JoinType::Right), (false, true));
6954 assert_eq!(lr_is_preserved(JoinType::Full), (false, false));
6955 assert_eq!(lr_is_preserved(JoinType::LeftSemi), (true, true));
6956 assert_eq!(lr_is_preserved(JoinType::LeftAnti), (true, false));
6957 assert_eq!(lr_is_preserved(JoinType::LeftMark), (true, false));
6958 assert_eq!(lr_is_preserved(JoinType::RightSemi), (true, true));
6959 assert_eq!(lr_is_preserved(JoinType::RightAnti), (false, true));
6960 assert_eq!(lr_is_preserved(JoinType::RightMark), (false, true));
6961 }
6962
6963 #[test]
6964 fn test_with_dynamic_filter() -> Result<()> {
6965 let (_, _, on) = build_schema_and_on()?;
6966 let left = build_table(("a1", &vec![1]), ("b1", &vec![1]), ("c1", &vec![1]));
6967 let right = build_table(("a2", &vec![1]), ("b1", &vec![1]), ("c2", &vec![1]));
6968
6969 let join = HashJoinExec::try_new(
6970 left,
6971 right,
6972 on,
6973 None,
6974 &JoinType::Inner,
6975 None,
6976 PartitionMode::CollectLeft,
6977 NullEquality::NullEqualsNothing,
6978 false,
6979 )?;
6980 assert!(join.dynamic_expressions_produced().is_empty());
6981
6982 let df = Arc::new(DynamicFilterPhysicalExpr::new(
6983 vec![Arc::new(Column::new("b1", 1)) as _],
6984 lit(true),
6985 ));
6986 let join = join.with_dynamic_filter_expr(Arc::clone(&df))?;
6987
6988 let produced = join.dynamic_expressions_produced();
6989 assert_eq!(produced.len(), 1);
6990 assert_eq!(
6991 produced[0]
6992 .expression_id()
6993 .expect("DynamicFilterPhysicalExpr always has an expression_id"),
6994 df.expression_id()
6995 .expect("DynamicFilterPhysicalExpr always has an expression_id"),
6996 );
6997 Ok(())
6998 }
6999
7000 #[test]
7001 fn test_swap_inputs_rejects_dynamic_filter() -> Result<()> {
7002 let left = build_table(
7003 ("l_key", &vec![1]),
7004 ("l_payload", &vec![10]),
7005 ("l_other", &vec![100]),
7006 );
7007 let right = build_table(
7008 ("r_payload", &vec![20]),
7009 ("r_key", &vec![1]),
7010 ("r_other", &vec![200]),
7011 );
7012 let on = vec![(
7013 Arc::new(Column::new_with_schema("l_key", &left.schema())?) as _,
7014 Arc::new(Column::new_with_schema("r_key", &right.schema())?) as _,
7015 )];
7016
7017 let dynamic_filter = HashJoinExec::create_dynamic_filter(&on);
7018 let join = HashJoinExec::try_new(
7019 left,
7020 right,
7021 on,
7022 None,
7023 &JoinType::LeftSemi,
7024 None,
7025 PartitionMode::CollectLeft,
7026 NullEquality::NullEqualsNothing,
7027 false,
7028 )?
7029 .with_dynamic_filter_expr(dynamic_filter)?;
7030
7031 let err = join.swap_inputs(PartitionMode::CollectLeft).unwrap_err();
7032 assert_contains!(
7033 err.to_string(),
7034 "Cannot swap HashJoinExec inputs after dynamic filters have been constructed"
7035 );
7036 Ok(())
7037 }
7038
7039 #[test]
7040 fn test_dynamic_filter_pushdown_allowed_for_null_equal_join() -> Result<()> {
7041 let (_, _, on) = build_schema_and_on()?;
7042 let left = build_table(("a1", &vec![1]), ("b1", &vec![1]), ("c1", &vec![1]));
7043 let right = build_table(("a2", &vec![1]), ("b1", &vec![1]), ("c2", &vec![1]));
7044
7045 let mut session_config = SessionConfig::default();
7046 session_config
7047 .options_mut()
7048 .optimizer
7049 .enable_join_dynamic_filter_pushdown = true;
7050
7051 let join = HashJoinExec::try_new(
7052 left,
7053 right,
7054 on,
7055 None,
7056 &JoinType::RightSemi,
7057 None,
7058 PartitionMode::CollectLeft,
7059 NullEquality::NullEqualsNull,
7060 false,
7061 )?;
7062
7063 assert!(join.allow_join_dynamic_filter_pushdown(session_config.options()));
7066
7067 Ok(())
7068 }
7069
7070 #[test]
7071 fn test_dynamic_filter_pushdown_rejects_null_aware_nullable_build_key() -> Result<()>
7072 {
7073 let left = build_table_two_cols(
7074 ("a1", &vec![Some(1), None]),
7075 ("b1", &vec![Some(1), Some(2)]),
7076 );
7077 let right = build_table_two_cols(
7078 ("a2", &vec![Some(2), Some(3)]),
7079 ("b2", &vec![Some(1), Some(2)]),
7080 );
7081 let on = vec![(
7082 Arc::new(Column::new_with_schema("a1", &left.schema())?) as _,
7083 Arc::new(Column::new_with_schema("a2", &right.schema())?) as _,
7084 )];
7085
7086 let mut session_config = SessionConfig::default();
7087 session_config
7088 .options_mut()
7089 .optimizer
7090 .enable_join_dynamic_filter_pushdown = true;
7091
7092 let join = HashJoinExec::try_new(
7093 left,
7094 right,
7095 on,
7096 None,
7097 &JoinType::LeftAnti,
7098 None,
7099 PartitionMode::CollectLeft,
7100 NullEquality::NullEqualsNothing,
7101 true,
7102 )?;
7103
7104 assert!(!join.allow_join_dynamic_filter_pushdown(session_config.options()));
7105
7106 Ok(())
7107 }
7108
7109 #[test]
7110 fn test_dynamic_filter_pushdown_allows_null_aware_non_null_build_key() -> Result<()> {
7111 let left = build_table(("a1", &vec![1]), ("b1", &vec![1]), ("c1", &vec![1]));
7114 let right = build_table(("a2", &vec![2]), ("b2", &vec![2]), ("c2", &vec![2]));
7115 let on = vec![(
7116 Arc::new(Column::new_with_schema("a1", &left.schema())?) as _,
7117 Arc::new(Column::new_with_schema("a2", &right.schema())?) as _,
7118 )];
7119
7120 let mut session_config = SessionConfig::default();
7121 session_config
7122 .options_mut()
7123 .optimizer
7124 .enable_join_dynamic_filter_pushdown = true;
7125
7126 let join = HashJoinExec::try_new(
7127 left,
7128 right,
7129 on,
7130 None,
7131 &JoinType::LeftAnti,
7132 None,
7133 PartitionMode::CollectLeft,
7134 NullEquality::NullEqualsNothing,
7135 true,
7136 )?;
7137
7138 assert!(join.allow_join_dynamic_filter_pushdown(session_config.options()));
7139
7140 Ok(())
7141 }
7142
7143 fn range_partitioned_dynamic_filter_test_join(
7144 left_split: i32,
7145 right_split: i32,
7146 ) -> Result<(HashJoinExec, JoinOn)> {
7147 let (left_schema, right_schema, on) = build_schema_and_on()?;
7148 let left_partitioning = Partitioning::Range(RangePartitioning::try_new(
7149 [PhysicalSortExpr {
7150 expr: Arc::clone(&on[0].0),
7151 options: Default::default(),
7152 }]
7153 .into(),
7154 vec![SplitPoint::new(vec![ScalarValue::Int32(Some(left_split))])],
7155 )?);
7156 let right_partitioning = Partitioning::Range(RangePartitioning::try_new(
7157 [PhysicalSortExpr {
7158 expr: Arc::clone(&on[0].1),
7159 options: Default::default(),
7160 }]
7161 .into(),
7162 vec![SplitPoint::new(vec![ScalarValue::Int32(Some(right_split))])],
7163 )?);
7164 let left = Arc::new(PartitionedTestExec::try_new(
7165 left_schema,
7166 left_partitioning,
7167 )?);
7168 let right = Arc::new(PartitionedTestExec::try_new(
7169 right_schema,
7170 right_partitioning,
7171 )?);
7172
7173 let join = HashJoinExec::try_new(
7174 left,
7175 right,
7176 on.clone(),
7177 None,
7178 &JoinType::Inner,
7179 None,
7180 PartitionMode::Partitioned,
7181 NullEquality::NullEqualsNothing,
7182 false,
7183 )?;
7184 Ok((join, on))
7185 }
7186
7187 fn with_hash_partitioned_children(
7188 join: &HashJoinExec,
7189 on: &JoinOn,
7190 ) -> Result<HashJoinExec> {
7191 join.builder()
7192 .with_new_children(vec![
7193 Arc::new(PartitionedTestExec::try_new(
7194 join.left().schema(),
7195 Partitioning::Hash(vec![Arc::clone(&on[0].0)], 2),
7196 )?),
7197 Arc::new(PartitionedTestExec::try_new(
7198 join.right().schema(),
7199 Partitioning::Hash(vec![Arc::clone(&on[0].1)], 2),
7200 )?),
7201 ])?
7202 .build()
7203 }
7204
7205 #[test]
7206 fn test_partitioned_dynamic_filter_pushdown_allows_supported_partitioning()
7207 -> Result<()> {
7208 let (range_join, on) = range_partitioned_dynamic_filter_test_join(10, 10)?;
7209 let hash_join = with_hash_partitioned_children(&range_join, &on)?;
7210 let mut session_config = SessionConfig::default();
7211 session_config
7212 .options_mut()
7213 .optimizer
7214 .enable_join_dynamic_filter_pushdown = true;
7215
7216 assert!(range_join.allow_join_dynamic_filter_pushdown(session_config.options()));
7217 assert!(hash_join.allow_join_dynamic_filter_pushdown(session_config.options()));
7218
7219 session_config
7220 .options_mut()
7221 .optimizer
7222 .preserve_file_partitions = 1;
7223 assert!(range_join.allow_join_dynamic_filter_pushdown(session_config.options()));
7224
7225 Ok(())
7226 }
7227
7228 #[test]
7229 fn test_partitioned_dynamic_filter_pushdown_rejects_unsupported_partitioning()
7230 -> Result<()> {
7231 let (range_join, on) = range_partitioned_dynamic_filter_test_join(10, 10)?;
7232 let hash_join = with_hash_partitioned_children(&range_join, &on)?;
7233 let (mismatched_range_join, _) =
7234 range_partitioned_dynamic_filter_test_join(10, 11)?;
7235 let mut session_config = SessionConfig::default();
7236 session_config
7237 .options_mut()
7238 .optimizer
7239 .enable_join_dynamic_filter_pushdown = true;
7240
7241 assert!(
7242 !mismatched_range_join
7243 .allow_join_dynamic_filter_pushdown(session_config.options())
7244 );
7245
7246 session_config
7247 .options_mut()
7248 .optimizer
7249 .preserve_file_partitions = 1;
7250 assert!(!hash_join.allow_join_dynamic_filter_pushdown(session_config.options()));
7251
7252 Ok(())
7253 }
7254
7255 #[test]
7256 fn test_with_dynamic_filter_rejects_invalid_columns() -> Result<()> {
7257 let (_, _, on) = build_schema_and_on()?;
7258 let left = build_table(("a1", &vec![1]), ("b1", &vec![1]), ("c1", &vec![1]));
7259 let right = build_table(("a2", &vec![1]), ("b1", &vec![1]), ("c2", &vec![1]));
7260
7261 let join = HashJoinExec::try_new(
7262 left,
7263 right,
7264 on,
7265 None,
7266 &JoinType::Inner,
7267 None,
7268 PartitionMode::CollectLeft,
7269 NullEquality::NullEqualsNothing,
7270 false,
7271 )?;
7272
7273 let df = Arc::new(DynamicFilterPhysicalExpr::new(
7275 vec![Arc::new(Column::new("bad", 99)) as _],
7276 lit(true),
7277 ));
7278 assert!(join.with_dynamic_filter_expr(df).is_err());
7279 Ok(())
7280 }
7281}