Skip to main content

datafusion_physical_plan/joins/hash_join/
exec.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18use 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
104/// Hard-coded seed to ensure hash values from the hash join differ from `RepartitionExec`, avoiding collisions.
105pub(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    // TODO: support create ArrayMap<u64>
164    if num_row >= u32::MAX as usize {
165        return Ok(None);
166    }
167
168    // When the key range spans the full integer domain (e.g. i64::MIN to i64::MAX),
169    // range is u64::MAX and `range + 1` below would overflow.
170    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
193/// HashTable and input data for the left (build side) of a join
194pub(super) struct JoinLeftData {
195    /// The hash table with indices into `batch`
196    /// Arc is used to allow sharing with SharedBuildAccumulator for hash map pushdown
197    pub(super) map: Arc<Map>,
198    /// The input rows for the build side
199    batch: RecordBatch,
200    /// The build side on expressions values
201    values: Vec<ArrayRef>,
202    /// Shared bitmap builder for visited left indices
203    visited_indices_bitmap: SharedBitmapBuilder,
204    /// Counter of running probe-threads, potentially
205    /// able to update `visited_indices_bitmap`
206    probe_threads_counter: AtomicUsize,
207    /// We need to keep this field to maintain accurate memory accounting, even though we don't directly use it.
208    /// Without holding onto this reservation, the recorded memory usage would become inconsistent with actual usage.
209    /// This could hide potential out-of-memory issues, especially when upstream operators increase their memory consumption.
210    /// The MemoryReservation ensures proper tracking of memory resources throughout the join operation's lifecycle.
211    _reservation: MemoryReservation,
212    /// Bounds computed from the build side for dynamic filter pushdown.
213    /// If the partition is empty (no rows) this will be None.
214    /// If the partition has some rows this will be Some with the bounds for each join key column.
215    pub(super) bounds: Option<PartitionBounds>,
216    /// Membership testing strategy for filter pushdown
217    /// Contains either InList values for small build sides or hash table reference for large build sides
218    pub(super) membership: PushdownStrategy,
219    /// Shared atomic flag indicating if any probe partition saw data (for null-aware anti joins)
220    /// This is shared across all probe partitions to provide global knowledge
221    pub(super) probe_side_non_empty: AtomicBool,
222    /// Shared atomic flag indicating if any probe partition saw NULL in join keys (for null-aware anti joins)
223    pub(super) probe_side_has_null: AtomicBool,
224}
225
226impl JoinLeftData {
227    /// return a reference to the map
228    pub(super) fn map(&self) -> &Map {
229        &self.map
230    }
231
232    /// returns a reference to the build side batch
233    pub(super) fn batch(&self) -> &RecordBatch {
234        &self.batch
235    }
236
237    /// Returns `true` if the build side physically contains rows.
238    ///
239    /// This is distinct from [`Self::has_matchable_build_rows`]: a build side
240    /// can hold rows while its hash map is empty (see that method).
241    pub(super) fn has_build_rows(&self) -> bool {
242        self.batch().num_rows() > 0
243    }
244
245    /// Returns `true` if the build-side hash map has any matchable entries.
246    ///
247    /// Under [`NullEquality::NullEqualsNothing`] build rows whose join key is
248    /// NULL are omitted from the map, so this can be `false` even when
249    /// [`Self::has_build_rows`] is `true`.
250    pub(super) fn has_matchable_build_rows(&self) -> bool {
251        !self.map().is_empty()
252    }
253
254    /// returns a reference to the build side expressions values
255    pub(super) fn values(&self) -> &[ArrayRef] {
256        &self.values
257    }
258
259    /// returns a reference to the visited indices bitmap
260    pub(super) fn visited_indices_bitmap(&self) -> &SharedBitmapBuilder {
261        &self.visited_indices_bitmap
262    }
263
264    /// returns a reference to the InList values for filter pushdown
265    pub(super) fn membership(&self) -> &PushdownStrategy {
266        &self.membership
267    }
268
269    /// Decrements the counter of running threads, and returns `true`
270    /// if caller is the last running thread
271    pub(super) fn report_probe_completed(&self) -> bool {
272        self.probe_threads_counter.fetch_sub(1, Ordering::Relaxed) == 1
273    }
274}
275
276/// Helps to build [`HashJoinExec`].
277///
278/// Builder can be created from an existing [`HashJoinExec`] using [`From::from`].
279/// In this case, all its fields are inherited. If a field that affects the node's
280/// properties is modified, they will be automatically recomputed during the build.
281///
282/// # Adding setters
283///
284/// When adding a new setter, it is necessary to ensure that the `preserve_properties`
285/// flag is set to false if modifying the field requires a recomputation of the plan's
286/// properties.
287///
288pub struct HashJoinExecBuilder {
289    exec: HashJoinExec,
290    preserve_properties: bool,
291}
292
293impl HashJoinExecBuilder {
294    /// Make a new [`HashJoinExecBuilder`].
295    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                // Will be computed at when plan will be built.
319                cache: stub_properties(),
320                join_schema: Arc::new(Schema::empty()),
321            },
322            // As `exec` is initialized with stub properties,
323            // they will be properly computed when plan will be built.
324            preserve_properties: false,
325        }
326    }
327
328    /// Set join type.
329    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    /// Set projection from the vector.
336    pub fn with_projection(self, projection: Option<Vec<usize>>) -> Self {
337        self.with_projection_ref(projection.map(Into::into))
338    }
339
340    /// Set projection from the shared reference.
341    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    /// Set optional filter.
348    pub fn with_filter(mut self, filter: Option<JoinFilter>) -> Self {
349        self.exec.filter = filter;
350        self
351    }
352
353    /// Set expressions to join on.
354    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    /// Set partition mode.
361    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    /// Set null equality property.
368    pub fn with_null_equality(mut self, null_equality: NullEquality) -> Self {
369        self.exec.null_equality = null_equality;
370        self
371    }
372
373    /// Set null aware property.
374    pub fn with_null_aware(mut self, null_aware: bool) -> Self {
375        self.exec.null_aware = null_aware;
376        self
377    }
378
379    /// Set fetch property.
380    pub fn with_fetch(mut self, fetch: Option<usize>) -> Self {
381        self.exec.fetch = fetch;
382        self
383    }
384
385    /// Require to recompute plan properties.
386    pub fn recompute_properties(mut self) -> Self {
387        self.preserve_properties = false;
388        self
389    }
390
391    /// Replace children.
392    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    /// Reset runtime state.
407    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    /// Build result as a dyn execution plan.
415    pub fn build_exec(self) -> Result<Arc<dyn ExecutionPlan>> {
416        self.build().map(|p| Arc::new(p) as _)
417    }
418
419    /// Build resulting execution plan.
420    pub fn build(self) -> Result<HashJoinExec> {
421        let Self {
422            exec,
423            preserve_properties,
424        } = self;
425
426        // Validate null_aware flag
427        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            // Recomputed.
463            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        // Check if the projection is valid.
481        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)]
548/// Join execution plan: Evaluates equijoin predicates in parallel on multiple
549/// partitions using a hash table and an optional filter list to apply post
550/// join.
551///
552/// # Join Expressions
553///
554/// This implementation is optimized for evaluating equijoin predicates  (
555/// `<col1> = <col2>`) expressions, which are represented as a list of `Columns`
556/// in [`Self::on`].
557///
558/// Non-equality predicates, which can not pushed down to a join inputs (e.g.
559/// `<col1> != <col2>`) are known as "filter expressions" and are evaluated
560/// after the equijoin predicates.
561///
562/// # ArrayMap Optimization
563///
564/// For joins with a single integer-based join key, `HashJoinExec` may use an [`ArrayMap`]
565/// (also known as a "perfect hash join") instead of a general-purpose hash map.
566/// This optimization is used when:
567/// 1. There is exactly one join key.
568/// 2. The join key is an integer type up to 64 bits wide that can be losslessly converted
569///    to `u64` (128-bit integer types such as `i128` and `u128` are not supported).
570/// 3. The range of keys is small enough (controlled by `perfect_hash_join_small_build_threshold`)
571///    OR the keys are sufficiently dense (controlled by `perfect_hash_join_min_key_density`).
572/// 4. build_side.num_rows() < u32::MAX
573/// 5. NullEqualsNothing || (NullEqualsNull && build side doesn't contain null)
574///
575/// See [`try_create_array_map`] for more details.
576///
577/// Note that when using [`PartitionMode::Partitioned`], the build side is split into multiple
578/// partitions. This can cause a dense build side to become sparse within each partition,
579/// potentially disabling this optimization.
580///
581/// For example, consider:
582/// ```sql
583/// SELECT t1.value, t2.value
584/// FROM range(10000) AS t1
585/// JOIN range(10000) AS t2
586///   ON t1.value = t2.value;
587/// ```
588/// With 24 partitions, each partition will only receive a subset of the 10,000 rows.
589/// The first partition might contain values like `3, 10, 18, 39, 43`, which are sparse
590/// relative to the original range, even though the overall data set is dense.
591///
592/// # "Build Side" vs "Probe Side"
593///
594/// HashJoin takes two inputs, which are referred to as the "build" and the
595/// "probe". The build side is the first child, and the probe side is the second
596/// child.
597///
598/// The two inputs are treated differently and it is VERY important that the
599/// *smaller* input is placed on the build side to minimize the work of creating
600/// the hash table.
601///
602/// ```text
603///          ┌───────────┐
604///          │ HashJoin  │
605///          │           │
606///          └───────────┘
607///              │   │
608///        ┌─────┘   └─────┐
609///        ▼               ▼
610/// ┌────────────┐  ┌─────────────┐
611/// │   Input    │  │    Input    │
612/// │    [0]     │  │     [1]     │
613/// └────────────┘  └─────────────┘
614///
615///  "build side"    "probe side"
616/// ```
617///
618/// Execution proceeds in 2 stages:
619///
620/// 1. the **build phase** creates a hash table from the tuples of the build side,
621///    and single concatenated batch containing data from all fetched record batches.
622///    Resulting hash table stores hashed join-key fields for each row as a key, and
623///    indices of corresponding rows in concatenated batch.
624///
625/// When using the standard `JoinHashMap`, hash join uses LIFO data structure as a hash table,
626/// and in order to retain original build-side input order while obtaining data during probe phase,
627/// hash table is updated by iterating batch sequence in reverse order -- it allows to
628/// keep rows with smaller indices "on the top" of hash table, and still maintain
629/// correct indexing for concatenated build-side data batch.
630///
631/// Example of build phase for 3 record batches:
632///
633///
634/// ```text
635///
636///  Original build-side data   Inserting build-side values into hashmap    Concatenated build-side batch
637///                                                                         ┌───────────────────────────┐
638///                             hashmap.insert(row-hash, row-idx + offset)  │                      idx  │
639///            ┌───────┐                                                    │          ┌───────┐        │
640///            │ Row 1 │        1) update_hash for batch 3 with offset 0    │          │ Row 6 │    0   │
641///   Batch 1  │       │           - hashmap.insert(Row 7, idx 1)           │ Batch 3  │       │        │
642///            │ Row 2 │           - hashmap.insert(Row 6, idx 0)           │          │ Row 7 │    1   │
643///            └───────┘                                                    │          └───────┘        │
644///                                                                         │                           │
645///            ┌───────┐                                                    │          ┌───────┐        │
646///            │ Row 3 │        2) update_hash for batch 2 with offset 2    │          │ Row 3 │    2   │
647///            │       │           - hashmap.insert(Row 5, idx 4)           │          │       │        │
648///   Batch 2  │ Row 4 │           - hashmap.insert(Row 4, idx 3)           │ Batch 2  │ Row 4 │    3   │
649///            │       │           - hashmap.insert(Row 3, idx 2)           │          │       │        │
650///            │ Row 5 │                                                    │          │ Row 5 │    4   │
651///            └───────┘                                                    │          └───────┘        │
652///                                                                         │                           │
653///            ┌───────┐                                                    │          ┌───────┐        │
654///            │ Row 6 │        3) update_hash for batch 1 with offset 5    │          │ Row 1 │    5   │
655///   Batch 3  │       │           - hashmap.insert(Row 2, idx 6)           │ Batch 1  │       │        │
656///            │ Row 7 │           - hashmap.insert(Row 1, idx 5)           │          │ Row 2 │    6   │
657///            └───────┘                                                    │          └───────┘        │
658///                                                                         │                           │
659///                                                                         └───────────────────────────┘
660/// ```
661///
662/// 2. the **probe phase** where the tuples of the probe side are streamed
663///    through, checking for matches of the join keys in the hash table.
664///
665/// ```text
666///                 ┌────────────────┐          ┌────────────────┐
667///                 │ ┌─────────┐    │          │ ┌─────────┐    │
668///                 │ │  Hash   │    │          │ │  Hash   │    │
669///                 │ │  Table  │    │          │ │  Table  │    │
670///                 │ │(keys are│    │          │ │(keys are│    │
671///                 │ │equi join│    │          │ │equi join│    │  Stage 2: batches from
672///  Stage 1: the   │ │columns) │    │          │ │columns) │    │    the probe side are
673/// *entire* build  │ │         │    │          │ │         │    │  streamed through, and
674///  side is read   │ └─────────┘    │          │ └─────────┘    │   checked against the
675/// into the hash   │      ▲         │          │          ▲     │   contents of the hash
676///     table       │       HashJoin │          │  HashJoin      │          table
677///                 └──────┼─────────┘          └──────────┼─────┘
678///             ─ ─ ─ ─ ─ ─                                 ─ ─ ─ ─ ─ ─ ─
679///            │                                                         │
680///
681///            │                                                         │
682///     ┌────────────┐                                            ┌────────────┐
683///     │RecordBatch │                                            │RecordBatch │
684///     └────────────┘                                            └────────────┘
685///     ┌────────────┐                                            ┌────────────┐
686///     │RecordBatch │                                            │RecordBatch │
687///     └────────────┘                                            └────────────┘
688///           ...                                                       ...
689///     ┌────────────┐                                            ┌────────────┐
690///     │RecordBatch │                                            │RecordBatch │
691///     └────────────┘                                            └────────────┘
692///
693///        build side                                                probe side
694/// ```
695///
696/// # Example "Optimal" Plans
697///
698/// The differences in the inputs means that for classic "Star Schema Query",
699/// the optimal plan will be a **"Right Deep Tree"** . A Star Schema Query is
700/// one where there is one large table and several smaller "dimension" tables,
701/// joined on `Foreign Key = Primary Key` predicates.
702///
703/// A "Right Deep Tree" looks like this large table as the probe side on the
704/// lowest join:
705///
706/// ```text
707///             ┌───────────┐
708///             │ HashJoin  │
709///             │           │
710///             └───────────┘
711///                 │   │
712///         ┌───────┘   └──────────┐
713///         ▼                      ▼
714/// ┌───────────────┐        ┌───────────┐
715/// │ small table 1 │        │ HashJoin  │
716/// │  "dimension"  │        │           │
717/// └───────────────┘        └───┬───┬───┘
718///                   ┌──────────┘   └───────┐
719///                   │                      │
720///                   ▼                      ▼
721///           ┌───────────────┐        ┌───────────┐
722///           │ small table 2 │        │ HashJoin  │
723///           │  "dimension"  │        │           │
724///           └───────────────┘        └───┬───┬───┘
725///                               ┌────────┘   └────────┐
726///                               │                     │
727///                               ▼                     ▼
728///                       ┌───────────────┐     ┌───────────────┐
729///                       │ small table 3 │     │  large table  │
730///                       │  "dimension"  │     │    "fact"     │
731///                       └───────────────┘     └───────────────┘
732/// ```
733///
734/// # Clone / Shared State
735///
736/// Note this structure includes a [`OnceAsync`] that is used to coordinate the
737/// loading of the left side with the processing in each output stream.
738/// Therefore it can not be [`Clone`]
739pub struct HashJoinExec {
740    /// left (build) side which gets hashed
741    pub left: Arc<dyn ExecutionPlan>,
742    /// right (probe) side which are filtered by the hash table
743    pub right: Arc<dyn ExecutionPlan>,
744    /// Set of equijoin columns from the relations: `(left_col, right_col)`
745    pub on: Vec<(PhysicalExprRef, PhysicalExprRef)>,
746    /// Filters which are applied while finding matching rows
747    pub filter: Option<JoinFilter>,
748    /// How the join is performed (`OUTER`, `INNER`, etc)
749    pub join_type: JoinType,
750    /// The schema after join. Please be careful when using this schema,
751    /// if there is a projection, the schema isn't the same as the output schema.
752    join_schema: SchemaRef,
753    /// Future that consumes left input and builds the hash table
754    ///
755    /// For CollectLeft partition mode, this structure is *shared* across all output streams.
756    ///
757    /// Each output stream waits on the `OnceAsync` to signal the completion of
758    /// the hash table creation.
759    left_fut: Arc<OnceAsync<JoinLeftData>>,
760    /// Shared the `SeededRandomState` for the hashing algorithm (seeds preserved for serialization)
761    random_state: SeededRandomState,
762    /// Partitioning mode to use
763    pub mode: PartitionMode,
764    /// Execution metrics
765    metrics: ExecutionPlanMetricsSet,
766    /// The projection indices of the columns in the output schema of join
767    pub projection: Option<ProjectionRef>,
768    /// Information of index and left / right placement of columns
769    column_indices: Vec<ColumnIndex>,
770    /// The equality null-handling behavior of the join algorithm.
771    pub null_equality: NullEquality,
772    /// Flag to indicate if this is a null-aware anti join
773    pub null_aware: bool,
774    /// Cache holding plan properties like equivalences, output partitioning etc.
775    cache: Arc<PlanProperties>,
776    /// Dynamic filter for pushing down to the probe side
777    /// Set when dynamic filter pushdown is detected in handle_child_pushdown_result.
778    /// HashJoinExec also needs to keep a shared bounds accumulator for coordinating updates.
779    dynamic_filter: Option<HashJoinExecDynamicFilter>,
780    /// Maximum number of rows to return
781    fetch: Option<usize>,
782}
783
784#[derive(Clone)]
785struct HashJoinExecDynamicFilter {
786    /// Dynamic filter that we'll update with the results of the build side once that is done.
787    filter: Arc<DynamicFilterPhysicalExpr>,
788    /// Build accumulator to collect build-side information (hash maps and/or bounds) from each partition.
789    /// It is lazily initialized during execution to make sure we use the actual execution time partition counts.
790    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            // Explicitly exclude dynamic_filter to avoid runtime state differences in tests
811            .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    /// Tries to create a new [`HashJoinExec`].
823    ///
824    /// # Error
825    /// This function errors when it is not possible to join the left and right sides on keys `on`.
826    #[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    /// Create a builder based on the existing [`HashJoinExec`].
848    ///
849    /// Returned builder preserves all existing fields. If a field requiring properties
850    /// recomputation is modified, this will be done automatically during the node build.
851    ///
852    pub fn builder(&self) -> HashJoinExecBuilder {
853        self.into()
854    }
855
856    fn create_dynamic_filter(on: &JoinOn) -> Arc<DynamicFilterPhysicalExpr> {
857        // Extract the right-side keys (probe side keys) from the `on` clauses
858        // Dynamic filter will be created from build side values (left side) and applied to probe side (right side)
859        let right_keys: Vec<_> = on.iter().map(|(_, r)| Arc::clone(r)).collect();
860        // Initialize with a placeholder expression (true) that will be updated when the hash table is built
861        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        // A null-aware anti join emits a build-side NULL only when the probe
871        // is truly empty. The pushed filter can empty the probe by pruning
872        // every row, which would surface that NULL wrongly. A NOT NULL build
873        // key cannot produce such a NULL, so the filter stays there.
874        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        // `preserve_file_partitions` can report Hive-style file groups as Hash
883        // partitioned even though their partition indexes do not follow the
884        // hash router used by partitioned dynamic filters. Reject Hash inputs
885        // because the metadata cannot distinguish those scans from a real hash
886        // repartition. Compatible Range inputs remain safe because matching
887        // ordering and split points align each build filter with its probe
888        // partition. Other unsupported layouts are rejected.
889        // Follow-up work: enable dynamic filtering for preserve_file_partitioned scans (issue #20195).
890        // https://github.com/apache/datafusion/issues/20195
891        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    /// left (build) side which gets hashed
938    pub fn left(&self) -> &Arc<dyn ExecutionPlan> {
939        &self.left
940    }
941
942    /// right (probe) side which are filtered by the hash table
943    pub fn right(&self) -> &Arc<dyn ExecutionPlan> {
944        &self.right
945    }
946
947    /// Set of common columns used to join on
948    pub fn on(&self) -> &[(PhysicalExprRef, PhysicalExprRef)] {
949        &self.on
950    }
951
952    /// Filters applied before join output
953    pub fn filter(&self) -> Option<&JoinFilter> {
954        self.filter.as_ref()
955    }
956
957    /// How the join is performed
958    pub fn join_type(&self) -> &JoinType {
959        &self.join_type
960    }
961
962    /// The schema after join. Please be careful when using this schema,
963    /// if there is a projection, the schema isn't the same as the output schema.
964    pub fn join_schema(&self) -> &SchemaRef {
965        &self.join_schema
966    }
967
968    /// The partitioning mode of this hash join
969    pub fn partition_mode(&self) -> &PartitionMode {
970        &self.mode
971    }
972
973    /// Get null_equality
974    pub fn null_equality(&self) -> NullEquality {
975        self.null_equality
976    }
977
978    /// Returns the dynamic filter expression produced by this hash join, if set.
979    #[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    /// Set the dynamic filter on this hash join.
988    ///
989    /// Resets any internal state that depends on any existing dynamic filter.
990    ///
991    /// Validates that the filter's children reference valid columns in
992    /// the probe (right) side's schema.
993    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            // Initialize with an empty accumulator which will be lazily populated
1004            // during execution.
1005            build_accumulator: OnceLock::new(),
1006        });
1007        Ok(self)
1008    }
1009
1010    /// Calculate order preservation flags for this hash join.
1011    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    /// Get probe side information for the hash join.
1026    pub fn probe_side() -> JoinSide {
1027        // In current implementation right side is always probe side.
1028        JoinSide::Right
1029    }
1030
1031    /// Return whether the join contains a projection
1032    pub fn contains_projection(&self) -> bool {
1033        self.projection.is_some()
1034    }
1035
1036    /// Return new instance of [HashJoinExec] with the given projection.
1037    pub fn with_projection(&self, projection: Option<Vec<usize>>) -> Result<Self> {
1038        let projection = projection.map(Into::into);
1039        //  check if the projection is valid
1040        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    /// This function creates the cache object that stores the plan properties such as schema, equivalence properties, ordering, partitioning, etc.
1047    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        // Calculate equivalence properties:
1057        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                // If we only need to generate matched rows from the probe side,
1084                // we can emit rows incrementally.
1085                JoinType::Inner
1086                | JoinType::LeftSemi
1087                | JoinType::RightSemi
1088                | JoinType::Right
1089                | JoinType::RightAnti
1090                | JoinType::RightMark => EmissionType::Incremental,
1091                // If we need to generate unmatched rows from the *build side*,
1092                // we need to emit them at the end.
1093                JoinType::Left
1094                | JoinType::LeftAnti
1095                | JoinType::LeftMark
1096                | JoinType::Full => EmissionType::Both,
1097            }
1098        } else {
1099            right.pipeline_behavior()
1100        };
1101
1102        // If contains projection, update the PlanProperties.
1103        if let Some(projection) = projection {
1104            // construct a map from the input expressions to the output expression of the Projection
1105            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    /// Returns a new `ExecutionPlan` that computes the same join as this one,
1121    /// with the left and right inputs swapped using the  specified
1122    /// `partition_mode`.
1123    ///
1124    /// # Notes:
1125    ///
1126    /// This function is public so other downstream projects can use it to
1127    /// construct `HashJoinExec` with right side as the build side.
1128    ///
1129    /// For using this interface directly, please refer to below:
1130    ///
1131    /// Hash join execution may require specific input partitioning (for example,
1132    /// the left child may have a single partition while the right child has multiple).
1133    ///
1134    /// Calling this function on join nodes whose children have already been repartitioned
1135    /// (e.g., after a `RepartitionExec` has been inserted) may break the partitioning
1136    /// requirements of the hash join. Therefore, ensure you call this function
1137    /// before inserting any repartitioning operators on the join's children.
1138    ///
1139    /// In DataFusion's default SQL interface, this function is used by the `JoinSelection`
1140    /// physical optimizer rule to determine a good join order, which is
1141    /// executed before the `EnforceDistribution` rule (the rule that may
1142    /// insert `RepartitionExec` operators).
1143    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        // In case of anti / semi joins or if there is embedded projection in HashJoinExec, output column order is preserved, no need to add projection again
1175        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    // For [JoinType::Inner] and [JoinType::RightSemi] in hash joins, the probe phase initiates by
1324    // applying the hash function to convert the join key(s) in each row into a hash value from the
1325    // probe side table in the order they're arranged. The hash value is used to look up corresponding
1326    // entries in the hash table that was constructed from the build side table during the build phase.
1327    //
1328    // Because of the immediate generation of result rows once a match is found,
1329    // the output of the join tends to follow the order in which the rows were read from
1330    // the probe side table. This is simply due to the sequence in which the rows were processed.
1331    // Hence, it appears that the hash join is preserving the order of the probe side.
1332    //
1333    // Meanwhile, in the case of a [JoinType::RightAnti] hash join,
1334    // the unmatched rows from the probe side are also kept in order.
1335    // This is because the **`RightAnti`** join is designed to return rows from the right
1336    // (probe side) table that have no match in the left (build side) table. Because the rows
1337    // are processed sequentially in the probe phase, and unmatched rows are directly output
1338    // as results, these results tend to retain the order of the probe side table.
1339    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    /// Creates a new HashJoinExec with different children while preserving configuration.
1377    ///
1378    /// This method is called during query optimization when the optimizer creates new
1379    /// plan nodes. Importantly, it creates a fresh bounds_accumulator via `try_new`
1380    /// rather than cloning the existing one because partitioning may have changed.
1381    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        // Only compute a dynamic filter when the probe subtree contains a consumer.
1440        // Searching from `self` would always find the producer expression owned by this join.
1441        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        // Initialize build_accumulator lazily with runtime partition counts (only if enabled)
1461        // Use RepartitionExec's random state (seeds: 0,0,0,0) for partition routing
1462        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        // we have the batches and the hash map with their keys. We can how create a stream
1541        // over the right that uses this information to issue new batches.
1542        let right_stream = self.right.execute(partition, context)?;
1543
1544        // update column indices to reflect the projection
1545        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            // Left side is broadcast, so it always needs overall stats
1589            // Right side is partitioned, so it needs per-partition stats
1590            (Some(_), PartitionMode::CollectLeft) => {
1591                vec![ChildStats::At(None), ChildStats::At(partition)]
1592            }
1593            // For Partitioned mode, both sides are hash-partitioned symmetrically,
1594            // so each output partition uses the matching partition from both sides.
1595            (Some(_), PartitionMode::Partitioned) => {
1596                vec![ChildStats::At(partition), ChildStats::At(partition)]
1597            }
1598            // Overall stats requested, look up overall child stats.
1599            (None, _) => vec![ChildStats::At(None), ChildStats::At(None)],
1600            // Auto mode hasn't decided partitioning yet, so it needs
1601            // overall stats from both sides.
1602            (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        // Project statistics if there is a projection
1624        let stats = stats.project(self.projection.as_ref());
1625        // Apply fetch limit to statistics
1626        Ok(Arc::new(stats.with_fetch(self.fetch, 0, 1)?))
1627    }
1628
1629    /// Tries to push `projection` down through `hash_join`. If possible, performs the
1630    /// pushdown and returns a new [`HashJoinExec`] as the top plan which has projections
1631    /// as its children. Otherwise, returns `None`.
1632    fn try_swapping_with_projection(
1633        &self,
1634        projection: &ProjectionExec,
1635    ) -> Result<Option<Arc<dyn ExecutionPlan>>> {
1636        // TODO: currently if there is projection in HashJoinExec, we can't push down projection to left or right input. Maybe we can pushdown the mixed projection later.
1637        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                // Returned early if projection is not None
1664                .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        // This is the physical-plan equivalent of `push_down_all_join` in
1679        // `datafusion/optimizer/src/push_down_filter.rs`. That function uses `lr_is_preserved`
1680        // to decide which parent predicates can be pushed past a logical join to its children,
1681        // then checks column references to route each predicate to the correct side.
1682        //
1683        // We apply the same two-level logic here:
1684        // 1. `lr_is_preserved` gates whether a side is eligible at all.
1685        // 2. For each filter, we check that all column references belong to the
1686        //    target child (using `column_indices` to map output column positions
1687        //    to join sides). This is critical for correctness: name-based matching
1688        //    alone (as done by `ChildFilterDescription::from_child`) can incorrectly
1689        //    push filters when different join sides have columns with the same name
1690        //    (e.g. nested mark joins both producing "mark" columns).
1691        let (left_preserved, right_preserved) = lr_is_preserved(self.join_type);
1692
1693        // Build the set of allowed column indices for each side
1694        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                    // Mark columns - don't allow pushdown to either side
1711                    JoinSide::None => false,
1712                };
1713            });
1714
1715        // For semi joins, filters on output join keys can also be pushed to the
1716        // non-output side: every emitted row has an equal key there. This is not
1717        // true for anti joins, whose emitted rows have no match.
1718        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        // Add dynamic filters in Post phase if enabled. Skip when this join
1772        // already carries a dynamic filter from a previous pass — the shared
1773        // `Arc<DynamicFilterPhysicalExpr>` is still wired into the probe-side
1774        // scan's predicate, and re-creating it would AND a fresh duplicate
1775        // onto every Post-phase invocation (apache/datafusion-ballista#1359
1776        // surfaces this in AQE replan loops).
1777        if phase == FilterPushdownPhase::Post
1778            && self.dynamic_filter.is_none()
1779            && self.allow_join_dynamic_filter_pushdown(config)
1780        {
1781            // Add actual dynamic filter to right side (probe side)
1782            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); // Should always be 2, we have 2 children
1799        let right_child_self_filters = &child_pushdown_result.self_filters[1]; // We only push down filters to the right child
1800        // We expect 0 or 1 self filters
1801        if let Some(filter) = right_child_self_filters.first() {
1802            // Note that we don't check PushdDownPredicate::discrimnant because even if nothing said
1803            // "yes, I can fully evaluate this filter" things might still use it for statistics -> it's worth updating
1804            let predicate = Arc::clone(&filter.predicate);
1805            if let Ok(dynamic_filter) =
1806                Arc::downcast::<DynamicFilterPhysicalExpr>(predicate)
1807            {
1808                // We successfully pushed down our self filter - we need to make a new node with the dynamic filter
1809                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        // Hash join execution plan does not support pushing limit down through to children
1824        // because the children don't know about the join condition and can't
1825        // determine how many rows to produce
1826        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        // `PartitionMode` is specific to `HashJoinExec`, so its conversion stays
1865        // inline (by-name on purpose: the enums are numbered differently).
1866        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                        // Proto3 `repeated` cannot distinguish `None` from
1896                        // `Some(vec![])`. `Some(vec![])` (reachable via
1897                        // `try_embed_projection` for e.g. `SELECT count(1) … JOIN …`)
1898                        // changes the output schema, so it is encoded with the
1899                        // single-element sentinel `[u32::MAX]` (never a valid column
1900                        // index); every other state is sent as-is. See
1901                        // `try_from_proto` for the matching decoder.
1902                        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    /// Reconstruct a [`HashJoinExec`] from its protobuf representation.
1920    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        // `PartitionMode` is specific to `HashJoinExec`, so its conversion stays
1973        // inline (by-name on purpose: the enums are numbered differently).
1974        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        // Preserve the empty-projection sentinel written by `try_to_proto`.
1995        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        // Restore the row limit that `limit_pushdown` may have pushed into the
2002        // join. The field is presence-tracked, so a message written before it
2003        // existed decodes to `None` (no limit) rather than to `Some(0)`.
2004        //
2005        // The conversion is checked, not `as usize`: `fetch` is a `u64` on the
2006        // wire but a `usize` in the plan, and on a 32-bit target `as usize`
2007        // truncates. A fetch of `1 << 32` would become `0` -- not merely a
2008        // wrong limit but the worst one, silently turning the query into an
2009        // empty result. Report the out-of-range value instead. Please do not
2010        // "simplify" this back to `as usize`.
2011        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            // The dynamic filter is a `DynamicFilterPhysicalExpr` over the probe
2033            // (right) side; decode against the right schema then downcast.
2034            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
2050/// Determines which sides of a join are "preserved" for filter pushdown.
2051///
2052/// A preserved side means filters on that side's columns can be safely pushed
2053/// below the join. This mostly mirrors the logical optimizer's `lr_is_preserved`;
2054/// semi joins additionally allow join-key filters on the non-output side.
2055fn 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        // Callers restrict the non-output side of semi joins to join-key columns.
2062        JoinType::LeftSemi | JoinType::RightSemi => (true, true),
2063        JoinType::LeftAnti | JoinType::LeftMark => (true, false),
2064        JoinType::RightAnti | JoinType::RightMark => (false, true),
2065    }
2066}
2067
2068/// Accumulator for collecting min/max bounds from build-side data during hash join.
2069///
2070/// This struct encapsulates the logic for progressively computing column bounds
2071/// (minimum and maximum values) for a specific join key expression as batches
2072/// are processed during the build phase of a hash join.
2073///
2074/// The bounds are used for dynamic filter pushdown optimization, where filters
2075/// based on the actual data ranges can be pushed down to the probe side to
2076/// eliminate unnecessary data early.
2077struct CollectLeftAccumulator {
2078    /// The physical expression to evaluate for each batch
2079    expr: Arc<dyn PhysicalExpr>,
2080    /// Accumulator for tracking the minimum value across all batches
2081    min: MinAccumulator,
2082    /// Accumulator for tracking the maximum value across all batches
2083    max: MaxAccumulator,
2084}
2085
2086impl CollectLeftAccumulator {
2087    /// Creates a new accumulator for tracking bounds of a join key expression.
2088    ///
2089    /// # Arguments
2090    /// * `expr` - The physical expression to track bounds for
2091    /// * `schema` - The schema of the input data
2092    ///
2093    /// # Returns
2094    /// A new `CollectLeftAccumulator` instance configured for the expression's data type
2095    fn try_new(expr: Arc<dyn PhysicalExpr>, schema: &SchemaRef) -> Result<Self> {
2096        /// Recursively unwraps dictionary types to get the underlying value type.
2097        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            // Min/Max can operate on dictionary data but expect to be initialized with the underlying value type
2109            .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    /// Updates the accumulators with values from a new batch.
2118    ///
2119    /// Evaluates the expression on the batch and updates both min and max
2120    /// accumulators with the resulting values.
2121    ///
2122    /// # Arguments
2123    /// * `batch` - The record batch to process
2124    ///
2125    /// # Returns
2126    /// Ok(()) if the update succeeds, or an error if expression evaluation fails
2127    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    /// Finalizes the accumulation and returns the computed bounds.
2135    ///
2136    /// Consumes self to extract the final min and max values from the accumulators.
2137    ///
2138    /// # Returns
2139    /// The `ColumnBounds` containing the minimum and maximum values observed
2140    fn evaluate(mut self) -> Result<ColumnBounds> {
2141        Ok(ColumnBounds::new(
2142            self.min.evaluate()?,
2143            self.max.evaluate()?,
2144        ))
2145    }
2146}
2147
2148/// State for collecting the build-side data during hash join
2149struct BuildSideState {
2150    batches: Vec<RecordBatch>,
2151    num_rows: usize,
2152    metrics: BuildProbeJoinMetrics,
2153    reservation: MemoryReservation,
2154    bounds_accumulators: Option<Vec<CollectLeftAccumulator>>,
2155    /// Counts the memory of `batches` for `reservation`. Batches can share
2156    /// underlying buffers (e.g. when the input emits zero-copy slices of one
2157    /// larger batch), so each buffer must be reserved only once.
2158    memory_counter: RecordBatchMemoryCounter,
2159}
2160
2161impl BuildSideState {
2162    /// Create a new BuildSideState with optional accumulators for bounds computation
2163    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/// Collects all batches from the left (build) side stream and creates a hash map for joining.
2202///
2203/// This function is responsible for:
2204/// 1. Consuming the entire left stream and collecting all batches into memory
2205/// 2. Building a hash map from the join key columns for efficient probe operations
2206/// 3. Computing bounds for dynamic filter pushdown (if enabled)
2207/// 4. Preparing visited indices bitmap for certain join types
2208///
2209/// # Parameters
2210/// * `random_state` - Random state for consistent hashing across partitions
2211/// * `left_stream` - Stream of record batches from the build side
2212/// * `on_left` - Physical expressions for the left side join keys
2213/// * `metrics` - Metrics collector for tracking memory usage and row counts
2214/// * `reservation` - Memory reservation tracker for the hash table and data
2215/// * `with_visited_indices_bitmap` - Whether to track visited indices (for outer joins)
2216/// * `probe_threads_count` - Number of threads that will probe this hash table
2217/// * `should_compute_dynamic_filters` - Whether to compute min/max bounds for dynamic filtering
2218///
2219/// # Dynamic Filter Coordination
2220/// When `should_compute_dynamic_filters` is true, this function computes the min/max bounds
2221/// for each join key column but does NOT update the dynamic filter. Instead, the
2222/// bounds are stored in the returned `JoinLeftData` and later coordinated by
2223/// `SharedBuildAccumulator` to ensure all partitions contribute their bounds
2224/// before updating the filter exactly once.
2225///
2226/// # Returns
2227/// `JoinLeftData` containing the hash map, consolidated batch, join key values,
2228/// visited indices bitmap, and computed bounds (if requested).
2229#[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            // Update accumulators if computing bounds
2259            if let Some(ref mut accumulators) = state.bounds_accumulators {
2260                for accumulator in accumulators {
2261                    accumulator.update_batch(&batch)?;
2262                }
2263            }
2264
2265            // Decide if we spill or not
2266            let batch_size = state.memory_counter.count_batch(&batch);
2267            // Reserve memory for incoming batch
2268            state.reservation.try_grow(batch_size)?;
2269            // Update metrics
2270            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            // Update row count
2274            state.num_rows += batch.num_rows();
2275            // Push batch to output
2276            state.batches.push(batch);
2277            Ok(state)
2278        })
2279        .await?;
2280
2281    // Extract fields from state
2282    let BuildSideState {
2283        batches,
2284        num_rows,
2285        metrics,
2286        mut reservation,
2287        bounds_accumulators,
2288        memory_counter: _,
2289    } = state;
2290
2291    // Compute bounds
2292    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            // Estimation of memory size, required for hashtable, prior to allocation.
2320            // Final result can be verified using `RawTable.allocation_info()`
2321            let fixed_size_u32 = size_of::<JoinHashMapU32>();
2322            let fixed_size_u64 = size_of::<JoinHashMapU64>();
2323
2324            // Use `u32` indices for the JoinHashMap when num_rows ≤ u32::MAX, otherwise use the
2325            // `u64` indice variant
2326            // Arc is used instead of Box to allow sharing with SharedBuildAccumulator for hash map pushdown
2327            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            // Updating hashmap starting from the last batch
2347            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            // Merge all batches into a single batch, so we can directly index into the arrays
2365            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    // Reserve additional memory for visited indices bitmap and create shared builder
2373    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        // If the build side is small enough we can use IN list pushdown.
2391        // If it's too big we fall back to pushing down a reference to the hash table.
2392        // See `PushdownStrategy` for more details.
2393        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    /// Build a table with two columns supporting nullable values
2629    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                // The planted error must only surface if the probe side is
2713                // polled, not when a parent node computes statistics during
2714                // planning.
2715                .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]), // this has a repetition
2942            ("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            // Inner join output is expected to preserve both inputs order
2969            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]), // this has a repetition
2996            ("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]), // this has a repetition
3044            ("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        // Inner join output is expected to preserve both inputs order
3069        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        // Inner join output is expected to preserve both inputs order
3117        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            // Expected number of hash table matches = 3
3177            // in case batch_size is 1 - additional empty batch for remaining 3-2 row
3178            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            // With hash collisions enabled, all records will match each other
3185            // and filtered later.
3186            div_ceil(9, batch_size)
3187        };
3188
3189        // With batch coalescing, we may have fewer batches than expected
3190        assert!(
3191            batches.len() <= expected_batch_count,
3192            "expected at most {expected_batch_count} batches, got {}",
3193            batches.len()
3194        );
3195
3196        // Inner join output is expected to preserve both inputs order
3197        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    /// Test where the left has 2 parts, the right with 1 part => 1 part
3215    #[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            // Expected number of hash table matches = 3
3265            // in case batch_size is 1 - additional empty batch for remaining 3-2 row
3266            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            // With hash collisions enabled, all records will match each other
3273            // and filtered later.
3274            div_ceil(9, batch_size)
3275        };
3276
3277        // With batch coalescing, we may have fewer batches than expected
3278        assert!(
3279            batches.len() <= expected_batch_count,
3280            "expected at most {expected_batch_count} batches, got {}",
3281            batches.len()
3282        );
3283
3284        // Inner join output is expected to preserve both inputs order
3285        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        // Inner join output is expected to preserve both inputs order
3344        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    /// Test where the left has 1 part, the right has 2 parts => 2 parts
3363    #[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]), // this has a repetition
3373            ("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        // first part
3405        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            // Expected number of hash table matches for first right batch = 1
3410            // and additional empty batch for non-joined 20-6-80
3411            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            // With hash collisions enabled, all records will match each other
3418            // and filtered later.
3419            div_ceil(6, batch_size)
3420        };
3421        // With batch coalescing, we may have fewer batches than expected
3422        assert!(
3423            batches.len() <= expected_batch_count,
3424            "expected at most {expected_batch_count} batches, got {}",
3425            batches.len()
3426        );
3427
3428        // Inner join output is expected to preserve both inputs order
3429        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        // second part
3440        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            // Expected number of hash table matches for second right batch = 2
3445            div_ceil(2, batch_size)
3446        } else {
3447            // With hash collisions enabled, all records will match each other
3448            // and filtered later.
3449            div_ceil(3, batch_size)
3450        };
3451        // With batch coalescing, we may have fewer batches than expected
3452        assert!(
3453            batches.len() <= expected_batch_count,
3454            "expected at most {expected_batch_count} batches, got {}",
3455            batches.len()
3456        );
3457
3458        // Inner join output is expected to preserve both inputs order
3459        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]), // 7 does not exist on the right
3496            ("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]), // 7 does not exist on the right
3558            ("c1", &vec![7, 8, 9]),
3559        );
3560        // create two identical batches for the right side
3561        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]), // 7 does not exist on the right
3716            ("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    /// Under NullEqualsNothing, NULL join keys are not inserted into the hash
3759    /// map, so a build side whose keys are all NULL produces an empty map even
3760    /// though it contains rows. Join types that emit unmatched build rows must
3761    /// still produce them from the visited bitmap.
3762    #[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]), // all build-side join keys are NULL
3771        );
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            // For join types whose output requires a build-side match, an
3805            // empty map guarantees an empty result, so `state_after_build_ready`
3806            // completes the stream without ever fetching a probe batch (probe
3807            // `input_rows` stays 0). All other join types must still scan the
3808            // probe side. `input_rows` is summed across every partition.
3809            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]), // 7 does not exist on the right
3933            ("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        // just two line match
3977        // b1 = 10
3978        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        // just two line match
3987        // b2 = 10
3988        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        // left_table left semi join right_table on left_table.b1 = right_table.b2
4005        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        // ignore the order
4025        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        // left_table left semi join right_table on left_table.b1 = right_table.b2 and right_table.a2 != 10
4054        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        // left_table left semi join right_table on left_table.b1 = right_table.b2 and right_table.a2 > 10
4109        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        // left_table right semi join right_table on left_table.b1 = right_table.b2
4162        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        // RightSemi join output is expected to preserve right input order
4182        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        // left_table right semi join right_table on left_table.b1 = right_table.b2 on left_table.a1!=9
4211        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        // RightSemi join output is expected to preserve right input order
4251        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        // left_table right semi join right_table on left_table.b1 = right_table.b2 on left_table.a1!=9
4267        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        // RightSemi join output is expected to preserve right input order
4291        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        // left_table left anti join right_table on left_table.b1 = right_table.b2
4318        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        // left_table left anti join right_table on left_table.b1 = right_table.b2 and right_table.a2!=8
4366        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        // left_table left anti join right_table on left_table.b1 = right_table.b2 and right_table.a2 != 13
4423        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        // RightAnti join output is expected to preserve right input order
4500        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        // left_table right anti join right_table on left_table.b1 = right_table.b2 and left_table.a1!=13
4528        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        // RightAnti join output is expected to preserve right input order
4568        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        // left_table right anti join right_table on left_table.b1 = right_table.b2 and right_table.b2!=8
4586        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        // RightAnti join output is expected to preserve right input order
4618        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]), // 6 does not exist on the left
4652            ("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]), // 6 does not exist on the left
4704            ("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]), // 7 does not exist on the right
4751            ("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]), // 7 does not exist on the right
4806            ("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]), // 7 does not exist on the right
4858            ("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]), // 7 does not exist on the right
4910            ("c1", &vec![7, 8, 9]),
4911        );
4912        let right = build_table(
4913            ("a2", &vec![10, 20, 30]),
4914            ("b1", &vec![4, 5, 6]), // 6 does not exist on the left
4915            ("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]), // 7 does not exist on the right
4961            ("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]), // 6 does not exist on the left
4966            ("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        // Maps both values to both indices (1 and 2, representing input 0 and 1)
5017        // 0 -> (0, 1)
5018        // 1 -> (0, 2)
5019        // The equality check will make sure only hashes[0] maps to 0 and hashes[1] maps to 1
5020        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        // Join key column for both join sides
5035        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        // We still expect to match rows 0 and 1 on both sides
5123        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            // join on a=b so there are duplicate column names on unjoined columns
5147            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        // THIS MIGRATION HALTED DUE TO ISSUE #15312
5434        //allow_duplicates! {
5435        //    assert_snapshot!(batches_to_sort_string(&batches), @r#"
5436        //    +---+---+---+----+---+---+
5437        //    | a | b | c | a  | b | c |
5438        //    +---+---+---+----+---+---+
5439        //    |   |   |   | 30 | 3 | 6 |
5440        //    |   |   |   | 40 | 4 | 4 |
5441        //    | 2 | 7 | 9 | 10 | 2 | 7 |
5442        //    | 2 | 7 | 9 | 20 | 2 | 5 |
5443        //    | 0 | 4 | 7 |    |   |   |
5444        //    | 1 | 5 | 8 |    |   |   |
5445        //    | 2 | 8 | 1 |    |   |   |
5446        //    +---+---+---+----+---+---+
5447        //        "#)
5448        //}
5449
5450        Ok(())
5451    }
5452
5453    /// Test for parallelized HashJoinExec with PartitionMode::CollectLeft
5454    #[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        // right input stream returns one good batch and then one error.
5646        // The error should be returned.
5647        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            // Expect that an error is returned
5683            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        // validation of partial join results output for different batch_size setting
5844        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                // For inner/right join expected batch count equals dev_ceil result,
5861                // as there is no need to append non-joined build side data.
5862                // For other join types it'll be div_ceil + 1 -- for additional batch
5863                // containing not visited build side rows (empty in this test case).
5864                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                // With batch coalescing, we may have fewer batches than expected
5874                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                // For anti joins with empty results, we may get zero batches
5888                // (with coalescing) instead of one empty batch with schema
5889                if batches.is_empty() {
5890                    // Verify this is an expected empty result case
5891                    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            // Asserting that operator-level reservation attempting to overallocate
5951            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        // The build side emits zero-copy slices of one large batch, as e.g. an
5968        // aggregate emitting its output in batch_size chunks does. The buffers
5969        // shared by the slices must be reserved once in total, not once per
5970        // slice: per-slice accounting reserves number_of_slices x parent size
5971        // and aborts queries that fit in memory with room to spare.
5972        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        // Enough for the parent batch (~48KB) plus the join hash table, but far
5997        // below the ~768KB that per-slice accounting would reserve
5998        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        // Prepare partitioned inputs for HashJoinExec
6023        // No need to adjust partitioning, as execution should fail with `Resources exhausted` error
6024        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            // Asserting that stream-level reservation attempting to overallocate
6088            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        // With batch coalescing, empty results may not emit any batches
6216        // Check that either we have no batches, or an empty batch with proper schema
6217        if batches_null_neq.is_empty() {
6218            // This is fine - no output rows
6219        } 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    /// Returns the column names on the schema
6229    fn columns(schema: &Schema) -> Vec<String> {
6230        schema.fields().iter().map(|f| f.name().clone()).collect()
6231    }
6232
6233    /// This test verifies that the dynamic filter is marked as complete after HashJoinExec finishes building the hash table.
6234    #[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        // Execute the join
6257        let stream = join.execute(0, task_ctx)?;
6258        let _batches = common::collect(stream).await?;
6259
6260        // After the join completes, the dynamic filter should be marked as complete
6261        // wait_complete() should return immediately
6262        dynamic_filter.wait_complete().await;
6263
6264        Ok(())
6265    }
6266
6267    /// This test verifies that the dynamic filter is marked as complete even when the build side is empty.
6268    #[tokio::test]
6269    async fn test_hash_join_marks_filter_complete_empty_build_side() -> Result<()> {
6270        let task_ctx = Arc::new(TaskContext::default());
6271        // Empty left side (build side)
6272        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        // Execute the join
6288        let stream = join.execute(0, task_ctx)?;
6289        let _batches = common::collect(stream).await?;
6290
6291        // Even with empty build side, the dynamic filter should be marked as complete
6292        // wait_complete() should return immediately
6293        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        // Keep an extra consumer reference so execute() enables dynamic filter pushdown
6457        // and enters the WaitPartitionBoundsReport path before deciding whether to poll
6458        // the probe side.
6459        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    /// Test null-aware anti join when probe side (right) contains NULL
6719    /// Expected: no rows should be output (NULL in subquery means all results are unknown)
6720    #[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        // Build left table (rows to potentially output)
6726        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        // Build right table (subquery with NULL)
6732        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        // Create null-aware anti join
6743        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, // null_aware = true
6753        )?;
6754
6755        let stream = join.execute(0, task_ctx)?;
6756        let batches = common::collect(stream).await?;
6757
6758        // Expected: empty result (probe side has NULL, so no rows should be output)
6759        allow_duplicates! {
6760            assert_snapshot!(batches_to_sort_string(&batches), @r"
6761            ++
6762            ++
6763            ");
6764        }
6765        Ok(())
6766    }
6767
6768    /// Test null-aware anti join when build side (left) contains NULL keys
6769    /// Expected: rows with NULL keys should not be output
6770    #[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        // Build left table with NULL key (this row should not be output)
6776        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        // Build right table (no NULL, so probe-side check passes)
6782        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        // Create null-aware anti join
6793        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, // null_aware = true
6803        )?;
6804
6805        let stream = join.execute(0, task_ctx)?;
6806        let batches = common::collect(stream).await?;
6807
6808        // Expected: only c1=4 (not c1=1 which matches, not c1=NULL)
6809        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    /// Test null-aware anti join with no NULLs (should work like regular anti join)
6822    #[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        // Build left table (no NULLs)
6828        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        // Build right table (no NULLs)
6834        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        // Create null-aware anti join
6845        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, // null_aware = true
6855        )?;
6856
6857        let stream = join.execute(0, task_ctx)?;
6858        let batches = common::collect(stream).await?;
6859
6860        // Expected: c1=4 and c1=5 (they don't match anything in right)
6861        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    /// Test that null_aware validation rejects non-LeftAnti join types
6875    #[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        // Try to create null-aware Inner join (should fail)
6888        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, // null_aware = true (invalid for Inner join)
6898        );
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    /// Test that null_aware validation rejects multi-column joins
6910    #[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        // Try multi-column join
6916        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        // Try to create null-aware anti join with 2 columns (should fail)
6928        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, // null_aware = true (invalid for multi-column)
6938        );
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        // Null-equal joins keep dynamic filter pushdown: the pushed predicate carries an
7064        // `IS NULL` disjunct so a probe-side NULL still reaches the join.
7065        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        // A NOT NULL build key cannot surface a build-side NULL, so the
7112        // pushdown must stay enabled.
7113        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        // Column index 99 is out of bounds for the right (probe) side schema.
7274        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}