Skip to main content

datafusion_physical_plan/joins/
cross_join.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
18//! Defines the cross join plan for loading the left side of the cross join
19//! and producing batches in parallel for the right partitions
20
21use std::{sync::Arc, task::Poll};
22
23use super::utils::{
24    BatchSplitter, BatchTransformer, BuildProbeJoinMetrics, NoopBatchTransformer,
25    OnceAsync, OnceFut, StatefulStreamResult, adjust_right_output_partitioning,
26    reorder_output_after_swap,
27};
28use crate::execution_plan::{EmissionType, boundedness_from_children};
29use crate::metrics::{ExecutionPlanMetricsSet, MetricsSet};
30use crate::projection::{
31    ProjectionExec, join_allows_pushdown, join_table_borders, new_join_children,
32    physical_to_column_exprs,
33};
34use crate::statistics::{ChildStats, StatisticsArgs};
35use crate::stream::EmptyRecordBatchStream;
36use crate::{
37    ChildrenPropertiesMode, ColumnStatistics, DisplayAs, DisplayFormatType, Distribution,
38    ExecutionPlan, ExecutionPlanProperties, PlanProperties, RecordBatchStream,
39    ReplaceChildrenOptions, SendableRecordBatchStream, Statistics, handle_state,
40    validate_child_count,
41};
42
43use arrow::array::{RecordBatch, RecordBatchOptions};
44use arrow::compute::concat_batches;
45use arrow::datatypes::{Fields, Schema, SchemaRef};
46use datafusion_common::stats::Precision;
47use datafusion_common::tree_node::TreeNodeRecursion;
48use datafusion_common::{
49    JoinType, Result, ScalarValue, assert_eq_or_internal_err, internal_err,
50};
51use datafusion_execution::TaskContext;
52use datafusion_execution::memory_pool::{MemoryConsumer, MemoryReservation};
53use datafusion_physical_expr::PhysicalExpr;
54use datafusion_physical_expr::equivalence::join_equivalence_properties;
55
56use async_trait::async_trait;
57use futures::{Stream, StreamExt, TryStreamExt, ready};
58
59/// Data of the left side that is buffered into memory
60#[derive(Debug)]
61struct JoinLeftData {
62    /// Single RecordBatch with all rows from the left side
63    merged_batch: RecordBatch,
64    /// Track memory reservation for merged_batch. Relies on drop
65    /// semantics to release reservation when JoinLeftData is dropped.
66    _reservation: MemoryReservation,
67}
68
69#[expect(rustdoc::private_intra_doc_links)]
70/// Cross Join Execution Plan
71///
72/// This operator is used when there are no predicates between two tables and
73/// returns the Cartesian product of the two tables.
74///
75/// Buffers the left input into memory and then streams batches from each
76/// partition on the right input combining them with the buffered left input
77/// to generate the output.
78///
79/// # Clone / Shared State
80///
81/// Note this structure includes a [`OnceAsync`] that is used to coordinate the
82/// loading of the left side with the processing in each output stream.
83/// Therefore it can not be [`Clone`]
84#[derive(Debug)]
85pub struct CrossJoinExec {
86    /// left (build) side which gets loaded in memory
87    pub left: Arc<dyn ExecutionPlan>,
88    /// right (probe) side which are combined with left side
89    pub right: Arc<dyn ExecutionPlan>,
90    /// The schema once the join is applied
91    schema: SchemaRef,
92    /// Buffered copy of left (build) side in memory.
93    ///
94    /// This structure is *shared* across all output streams.
95    ///
96    /// Each output stream waits on the `OnceAsync` to signal the completion of
97    /// the left side loading.
98    left_fut: OnceAsync<JoinLeftData>,
99    /// Execution plan metrics
100    metrics: ExecutionPlanMetricsSet,
101    /// Properties such as schema, equivalence properties, ordering, partitioning, etc.
102    cache: Arc<PlanProperties>,
103}
104
105impl CrossJoinExec {
106    /// Create a new [CrossJoinExec].
107    pub fn new(left: Arc<dyn ExecutionPlan>, right: Arc<dyn ExecutionPlan>) -> Self {
108        // left then right
109        let (all_columns, metadata) = {
110            let left_schema = left.schema();
111            let right_schema = right.schema();
112            let left_fields = left_schema.fields().iter();
113            let right_fields = right_schema.fields().iter();
114
115            let mut metadata = left_schema.metadata().clone();
116            metadata.extend(right_schema.metadata().clone());
117
118            (
119                left_fields.chain(right_fields).cloned().collect::<Fields>(),
120                metadata,
121            )
122        };
123
124        let schema = Arc::new(Schema::new(all_columns).with_metadata(metadata));
125        let cache = Self::compute_properties(&left, &right, Arc::clone(&schema)).unwrap();
126
127        CrossJoinExec {
128            left,
129            right,
130            schema,
131            left_fut: Default::default(),
132            metrics: ExecutionPlanMetricsSet::default(),
133            cache: Arc::new(cache),
134        }
135    }
136
137    /// left (build) side which gets loaded in memory
138    pub fn left(&self) -> &Arc<dyn ExecutionPlan> {
139        &self.left
140    }
141
142    /// right side which gets combined with left side
143    pub fn right(&self) -> &Arc<dyn ExecutionPlan> {
144        &self.right
145    }
146
147    /// This function creates the cache object that stores the plan properties such as schema, equivalence properties, ordering, partitioning, etc.
148    fn compute_properties(
149        left: &Arc<dyn ExecutionPlan>,
150        right: &Arc<dyn ExecutionPlan>,
151        schema: SchemaRef,
152    ) -> Result<PlanProperties> {
153        // Calculate equivalence properties
154        // TODO: Check equivalence properties of cross join, it may preserve
155        //       ordering in some cases.
156        let eq_properties = join_equivalence_properties(
157            left.equivalence_properties().clone(),
158            right.equivalence_properties().clone(),
159            &JoinType::Full,
160            schema,
161            &[false, false],
162            None,
163            &[],
164        )?;
165
166        // Get output partitioning:
167        // TODO: Optimize the cross join implementation to generate M * N
168        //       partitions.
169        let output_partitioning = adjust_right_output_partitioning(
170            right.output_partitioning(),
171            left.schema().fields.len(),
172        )?;
173
174        Ok(PlanProperties::new(
175            eq_properties,
176            output_partitioning,
177            EmissionType::Final,
178            boundedness_from_children([left, right]),
179        ))
180    }
181
182    /// Returns a new `ExecutionPlan` that computes the same join as this one,
183    /// with the left and right inputs swapped using the  specified
184    /// `partition_mode`.
185    ///
186    /// # Notes:
187    ///
188    /// This function should be called BEFORE inserting any repartitioning
189    /// operators on the join's children. Check [`super::HashJoinExec::swap_inputs`]
190    /// for more details.
191    pub fn swap_inputs(&self) -> Result<Arc<dyn ExecutionPlan>> {
192        let new_join =
193            CrossJoinExec::new(Arc::clone(&self.right), Arc::clone(&self.left));
194        reorder_output_after_swap(
195            Arc::new(new_join),
196            &self.left.schema(),
197            &self.right.schema(),
198        )
199    }
200}
201
202/// Asynchronously collect the result of the left child
203async fn load_left_input(
204    stream: SendableRecordBatchStream,
205    metrics: BuildProbeJoinMetrics,
206    reservation: MemoryReservation,
207) -> Result<JoinLeftData> {
208    let left_schema = stream.schema();
209
210    // Load all batches and count the rows
211    let (batches, _metrics, reservation) = stream
212        .try_fold(
213            (Vec::new(), metrics, reservation),
214            |(mut batches, metrics, reservation), batch| async {
215                let batch_size = batch.get_array_memory_size();
216                // Reserve memory for incoming batch
217                reservation.try_grow(batch_size)?;
218                // Update metrics
219                metrics.build_mem_used.add(batch_size);
220                metrics.build_input_batches.add(1);
221                metrics.build_input_rows.add(batch.num_rows());
222                // Push batch to output
223                batches.push(batch);
224                Ok((batches, metrics, reservation))
225            },
226        )
227        .await?;
228
229    let merged_batch = concat_batches(&left_schema, &batches)?;
230
231    Ok(JoinLeftData {
232        merged_batch,
233        _reservation: reservation,
234    })
235}
236
237impl DisplayAs for CrossJoinExec {
238    fn fmt_as(
239        &self,
240        t: DisplayFormatType,
241        f: &mut std::fmt::Formatter,
242    ) -> std::fmt::Result {
243        match t {
244            DisplayFormatType::Default | DisplayFormatType::Verbose => {
245                write!(f, "CrossJoinExec")
246            }
247            DisplayFormatType::TreeRender => {
248                // no extra info to display
249                Ok(())
250            }
251        }
252    }
253}
254
255impl ExecutionPlan for CrossJoinExec {
256    fn name(&self) -> &'static str {
257        "CrossJoinExec"
258    }
259
260    fn properties(&self) -> &Arc<PlanProperties> {
261        &self.cache
262    }
263
264    fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
265        vec![&self.left, &self.right]
266    }
267
268    fn metrics(&self) -> Option<MetricsSet> {
269        Some(self.metrics.clone_inner())
270    }
271
272    fn apply_expressions(
273        &self,
274        _f: &mut dyn FnMut(&Arc<dyn PhysicalExpr>) -> Result<TreeNodeRecursion>,
275    ) -> Result<TreeNodeRecursion> {
276        // CrossJoin has no join conditions or expressions
277        Ok(TreeNodeRecursion::Continue)
278    }
279
280    fn replace_children(
281        self: Arc<Self>,
282        mut children: Vec<Arc<dyn ExecutionPlan>>,
283        options: ReplaceChildrenOptions,
284    ) -> Result<Arc<dyn ExecutionPlan>> {
285        validate_child_count!(self, children);
286        match options.children_properties {
287            ChildrenPropertiesMode::Keep => {
288                let left = children.swap_remove(0);
289                let right = children.swap_remove(0);
290
291                Ok(Arc::new(Self {
292                    left,
293                    right,
294                    metrics: ExecutionPlanMetricsSet::new(),
295                    left_fut: Default::default(),
296                    cache: Arc::clone(&self.cache),
297                    schema: Arc::clone(&self.schema),
298                }))
299            }
300            ChildrenPropertiesMode::Recompute => Ok(Arc::new(CrossJoinExec::new(
301                Arc::clone(&children[0]),
302                Arc::clone(&children[1]),
303            ))),
304        }
305    }
306
307    fn with_new_children(
308        self: Arc<Self>,
309        children: Vec<Arc<dyn ExecutionPlan>>,
310    ) -> Result<Arc<dyn ExecutionPlan>> {
311        self.replace_children(
312            children,
313            ReplaceChildrenOptions::new(ChildrenPropertiesMode::Recompute),
314        )
315    }
316
317    fn with_new_children_and_same_properties(
318        self: Arc<Self>,
319        children: Vec<Arc<dyn ExecutionPlan>>,
320    ) -> Result<Arc<dyn ExecutionPlan>> {
321        self.replace_children(
322            children,
323            ReplaceChildrenOptions::new(ChildrenPropertiesMode::Keep),
324        )
325    }
326
327    fn reset_state(self: Arc<Self>) -> Result<Arc<dyn ExecutionPlan>> {
328        let new_exec = CrossJoinExec {
329            left: Arc::clone(&self.left),
330            right: Arc::clone(&self.right),
331            schema: Arc::clone(&self.schema),
332            left_fut: Default::default(), // reset the build side!
333            metrics: ExecutionPlanMetricsSet::default(),
334            cache: Arc::clone(&self.cache),
335        };
336        Ok(Arc::new(new_exec))
337    }
338
339    fn required_input_distribution(&self) -> Vec<Distribution> {
340        self.input_distribution_requirements().into_per_child()
341    }
342
343    fn input_distribution_requirements(&self) -> crate::InputDistributionRequirements {
344        crate::InputDistributionRequirements::new(vec![
345            Distribution::SinglePartition,
346            Distribution::UnspecifiedDistribution,
347        ])
348    }
349
350    fn execute(
351        &self,
352        partition: usize,
353        context: Arc<TaskContext>,
354    ) -> Result<SendableRecordBatchStream> {
355        assert_eq_or_internal_err!(
356            self.left.output_partitioning().partition_count(),
357            1,
358            "Invalid CrossJoinExec, the output partition count of the left child must be 1,\
359                 consider using CoalescePartitionsExec or the EnforceDistribution rule"
360        );
361
362        let stream = self.right.execute(partition, Arc::clone(&context))?;
363
364        let join_metrics = BuildProbeJoinMetrics::new(partition, &self.metrics);
365
366        // Initialization of operator-level reservation
367        let reservation =
368            MemoryConsumer::new("CrossJoinExec").register(context.memory_pool());
369
370        let batch_size = context.session_config().batch_size();
371        let enforce_batch_size_in_joins =
372            context.session_config().enforce_batch_size_in_joins();
373
374        let left_fut = self.left_fut.try_once(|| {
375            let left_stream = self.left.execute(0, context)?;
376
377            Ok(load_left_input(
378                left_stream,
379                join_metrics.clone(),
380                reservation,
381            ))
382        })?;
383
384        if enforce_batch_size_in_joins {
385            Ok(Box::pin(CrossJoinStream {
386                schema: Arc::clone(&self.schema),
387                left_fut,
388                right: stream,
389                left_index: 0,
390                join_metrics,
391                state: CrossJoinStreamState::WaitBuildSide,
392                left_data: RecordBatch::new_empty(self.left().schema()),
393                batch_transformer: BatchSplitter::new(batch_size),
394            }))
395        } else {
396            Ok(Box::pin(CrossJoinStream {
397                schema: Arc::clone(&self.schema),
398                left_fut,
399                right: stream,
400                left_index: 0,
401                join_metrics,
402                state: CrossJoinStreamState::WaitBuildSide,
403                left_data: RecordBatch::new_empty(self.left().schema()),
404                batch_transformer: NoopBatchTransformer::new(),
405            }))
406        }
407    }
408
409    fn child_stats_requests(&self, partition: Option<usize>) -> Vec<ChildStats> {
410        // Left side is always broadcast, so it always needs overall stats.
411        // Right side is partitioned, so it needs per-partition stats.
412        vec![ChildStats::At(None), ChildStats::At(partition)]
413    }
414
415    fn statistics_from_inputs(
416        &self,
417        input_stats: &[Arc<Statistics>],
418        _args: &StatisticsArgs,
419    ) -> Result<Arc<Statistics>> {
420        let left_stats = input_stats[0].as_ref().clone();
421        let right_stats = input_stats[1].as_ref().clone();
422
423        Ok(Arc::new(stats_cartesian_product(left_stats, right_stats)))
424    }
425
426    /// Tries to swap the projection with its input [`CrossJoinExec`]. If it can be done,
427    /// it returns the new swapped version having the [`CrossJoinExec`] as the top plan.
428    /// Otherwise, it returns None.
429    fn try_swapping_with_projection(
430        &self,
431        projection: &ProjectionExec,
432    ) -> Result<Option<Arc<dyn ExecutionPlan>>> {
433        // Convert projected PhysicalExpr's to columns. If not possible, we cannot proceed.
434        let Some(projection_as_columns) = physical_to_column_exprs(projection.expr())
435        else {
436            return Ok(None);
437        };
438
439        let (far_right_left_col_ind, far_left_right_col_ind) = join_table_borders(
440            self.left().schema().fields().len(),
441            &projection_as_columns,
442        );
443
444        if !join_allows_pushdown(
445            &projection_as_columns,
446            &self.schema(),
447            far_right_left_col_ind,
448            far_left_right_col_ind,
449        ) {
450            return Ok(None);
451        }
452
453        let (new_left, new_right) = new_join_children(
454            &projection_as_columns,
455            far_right_left_col_ind,
456            far_left_right_col_ind,
457            self.left(),
458            self.right(),
459        )?;
460
461        Ok(Some(Arc::new(CrossJoinExec::new(
462            Arc::new(new_left),
463            Arc::new(new_right),
464        ))))
465    }
466    #[cfg(feature = "proto")]
467    fn try_to_proto(
468        &self,
469        ctx: &crate::proto::ExecutionPlanEncodeCtx<'_>,
470    ) -> Result<Option<datafusion_proto_models::protobuf::PhysicalPlanNode>> {
471        use datafusion_proto_models::protobuf;
472
473        let left = ctx.encode_child(self.left())?;
474        let right = ctx.encode_child(self.right())?;
475
476        Ok(Some(protobuf::PhysicalPlanNode {
477            physical_plan_type: Some(
478                protobuf::physical_plan_node::PhysicalPlanType::CrossJoin(Box::new(
479                    protobuf::CrossJoinExecNode {
480                        left: Some(Box::new(left)),
481                        right: Some(Box::new(right)),
482                    },
483                )),
484            ),
485        }))
486    }
487}
488
489#[cfg(feature = "proto")]
490impl CrossJoinExec {
491    pub fn try_from_proto(
492        node: &datafusion_proto_models::protobuf::PhysicalPlanNode,
493        ctx: &crate::proto::ExecutionPlanDecodeCtx<'_>,
494    ) -> Result<Arc<dyn ExecutionPlan>> {
495        use datafusion_proto_models::protobuf;
496
497        let crossjoin = crate::expect_plan_variant!(
498            node,
499            protobuf::physical_plan_node::PhysicalPlanType::CrossJoin,
500            "CrossJoinExec",
501        );
502
503        let left = ctx.decode_required_child(
504            crossjoin.left.as_deref(),
505            "CrossJoinExec",
506            "left",
507        )?;
508        let right = ctx.decode_required_child(
509            crossjoin.right.as_deref(),
510            "CrossJoinExec",
511            "right",
512        )?;
513
514        Ok(Arc::new(CrossJoinExec::new(left, right)))
515    }
516}
517
518/// [left/right]_col_count are required in case the column statistics are None
519fn stats_cartesian_product(
520    left_stats: Statistics,
521    right_stats: Statistics,
522) -> Statistics {
523    let left_row_count = left_stats.num_rows;
524    let right_row_count = right_stats.num_rows;
525
526    // Calculate global stats
527    let num_rows = left_row_count.multiply(&right_row_count);
528
529    // Each output row includes every left and right column, so the left side is
530    // repeated once per right row and the right side once per left row.
531    let left_byte_size = left_stats.total_byte_size.multiply(&right_row_count);
532    let right_byte_size = right_stats.total_byte_size.multiply(&left_row_count);
533    let total_byte_size = left_byte_size.add(&right_byte_size);
534
535    let left_col_stats = left_stats.column_statistics;
536    let right_col_stats = right_stats.column_statistics;
537
538    // the null counts must be multiplied by the row counts of the other side (if defined)
539    // Min, max and distinct_count on the other hand are invariants.
540    let cross_join_stats = left_col_stats
541        .into_iter()
542        .map(|s| {
543            let widened_sum = s.sum_value.cast_to_sum_type();
544            ColumnStatistics {
545                null_count: s.null_count.multiply(&right_row_count),
546                distinct_count: s.distinct_count,
547                min_value: s.min_value,
548                max_value: s.max_value,
549                sum_value: widened_sum
550                    .get_value()
551                    // Cast the row count into the same type as any existing sum value
552                    .and_then(|v| {
553                        Precision::<ScalarValue>::from(right_row_count)
554                            .cast_to(&v.data_type())
555                            .ok()
556                    })
557                    .map(|row_count| widened_sum.multiply(&row_count))
558                    .unwrap_or(Precision::Absent),
559                byte_size: Precision::Absent,
560            }
561        })
562        .chain(right_col_stats.into_iter().map(|s| {
563            let widened_sum = s.sum_value.cast_to_sum_type();
564            ColumnStatistics {
565                null_count: s.null_count.multiply(&left_row_count),
566                distinct_count: s.distinct_count,
567                min_value: s.min_value,
568                max_value: s.max_value,
569                sum_value: widened_sum
570                    .get_value()
571                    // Cast the row count into the same type as any existing sum value
572                    .and_then(|v| {
573                        Precision::<ScalarValue>::from(left_row_count)
574                            .cast_to(&v.data_type())
575                            .ok()
576                    })
577                    .map(|row_count| widened_sum.multiply(&row_count))
578                    .unwrap_or(Precision::Absent),
579                byte_size: Precision::Absent,
580            }
581        }))
582        .collect();
583
584    Statistics {
585        num_rows,
586        total_byte_size,
587        column_statistics: cross_join_stats,
588    }
589}
590
591/// A stream that issues [RecordBatch]es as they arrive from the right of the join.
592struct CrossJoinStream<T> {
593    /// Input schema
594    schema: Arc<Schema>,
595    /// Future for data from left side
596    left_fut: OnceFut<JoinLeftData>,
597    /// Right side stream
598    right: SendableRecordBatchStream,
599    /// Current value on the left
600    left_index: usize,
601    /// Join execution metrics
602    join_metrics: BuildProbeJoinMetrics,
603    /// State of the stream
604    state: CrossJoinStreamState,
605    /// Left data (copy of the entire buffered left side)
606    left_data: RecordBatch,
607    /// Batch transformer
608    batch_transformer: T,
609}
610
611impl<T: BatchTransformer + Unpin + Send> RecordBatchStream for CrossJoinStream<T> {
612    fn schema(&self) -> SchemaRef {
613        Arc::clone(&self.schema)
614    }
615}
616
617/// Represents states of CrossJoinStream
618enum CrossJoinStreamState {
619    WaitBuildSide,
620    FetchProbeBatch,
621    /// Holds the currently processed right side batch
622    BuildBatches(RecordBatch),
623}
624
625impl CrossJoinStreamState {
626    /// Tries to extract RecordBatch from CrossJoinStreamState enum.
627    /// Returns an error if state is not BuildBatches state.
628    fn try_as_record_batch(&mut self) -> Result<&RecordBatch> {
629        match self {
630            CrossJoinStreamState::BuildBatches(rb) => Ok(rb),
631            _ => internal_err!("Expected RecordBatch in BuildBatches state"),
632        }
633    }
634}
635
636fn build_batch(
637    left_index: usize,
638    batch: &RecordBatch,
639    left_data: &RecordBatch,
640    schema: &Schema,
641) -> Result<RecordBatch> {
642    // Repeat value on the left n times
643    let arrays = left_data
644        .columns()
645        .iter()
646        .map(|arr| {
647            let scalar = ScalarValue::try_from_array(arr, left_index)?;
648            scalar.to_array_of_size(batch.num_rows())
649        })
650        .collect::<Result<Vec<_>>>()?;
651
652    RecordBatch::try_new_with_options(
653        Arc::new(schema.clone()),
654        arrays
655            .iter()
656            .chain(batch.columns().iter())
657            .cloned()
658            .collect(),
659        &RecordBatchOptions::new().with_row_count(Some(batch.num_rows())),
660    )
661    .map_err(Into::into)
662}
663
664#[async_trait]
665impl<T: BatchTransformer + Unpin + Send> Stream for CrossJoinStream<T> {
666    type Item = Result<RecordBatch>;
667
668    fn poll_next(
669        mut self: std::pin::Pin<&mut Self>,
670        cx: &mut std::task::Context<'_>,
671    ) -> Poll<Option<Self::Item>> {
672        self.poll_next_impl(cx)
673    }
674}
675
676impl<T: BatchTransformer> CrossJoinStream<T> {
677    /// Separate implementation function that unpins the [`CrossJoinStream`] so
678    /// that partial borrows work correctly
679    fn poll_next_impl(
680        &mut self,
681        cx: &mut std::task::Context<'_>,
682    ) -> Poll<Option<Result<RecordBatch>>> {
683        loop {
684            return match self.state {
685                CrossJoinStreamState::WaitBuildSide => {
686                    handle_state!(ready!(self.collect_build_side(cx)))
687                }
688                CrossJoinStreamState::FetchProbeBatch => {
689                    handle_state!(ready!(self.fetch_probe_batch(cx)))
690                }
691                CrossJoinStreamState::BuildBatches(_) => {
692                    let poll = handle_state!(self.build_batches());
693                    self.join_metrics.baseline.record_poll(poll)
694                }
695            };
696        }
697    }
698
699    /// Collects build (left) side of the join into the state. In case of an empty build batch,
700    /// the execution terminates. Otherwise, the state is updated to fetch probe (right) batch.
701    fn collect_build_side(
702        &mut self,
703        cx: &mut std::task::Context<'_>,
704    ) -> Poll<Result<StatefulStreamResult<Option<RecordBatch>>>> {
705        let build_timer = self.join_metrics.build_time.timer();
706        let left_data = match ready!(self.left_fut.get(cx)) {
707            Ok(left_data) => left_data,
708            Err(e) => return Poll::Ready(Err(e)),
709        };
710        build_timer.done();
711
712        let left_data = left_data.merged_batch.clone();
713        let result = if left_data.num_rows() == 0 {
714            StatefulStreamResult::Ready(None)
715        } else {
716            self.left_data = left_data;
717            self.state = CrossJoinStreamState::FetchProbeBatch;
718            StatefulStreamResult::Continue
719        };
720        Poll::Ready(Ok(result))
721    }
722
723    /// Fetches the probe (right) batch, updates the metrics, and save the batch in the state.
724    /// Then, the state is updated to build result batches.
725    fn fetch_probe_batch(
726        &mut self,
727        cx: &mut std::task::Context<'_>,
728    ) -> Poll<Result<StatefulStreamResult<Option<RecordBatch>>>> {
729        self.left_index = 0;
730        let right_data = match ready!(self.right.poll_next_unpin(cx)) {
731            Some(Ok(right_data)) => right_data,
732            Some(Err(e)) => return Poll::Ready(Err(e)),
733            None => {
734                // Release the right (probe) input pipeline's resources.
735                let right_schema = self.right.schema();
736                self.right = Box::pin(EmptyRecordBatchStream::new(right_schema));
737                return Poll::Ready(Ok(StatefulStreamResult::Ready(None)));
738            }
739        };
740        self.join_metrics.input_batches.add(1);
741        self.join_metrics.input_rows.add(right_data.num_rows());
742
743        self.state = CrossJoinStreamState::BuildBatches(right_data);
744        Poll::Ready(Ok(StatefulStreamResult::Continue))
745    }
746
747    /// Joins the indexed row of left data with the current probe batch.
748    /// If all the results are produced, the state is set to fetch new probe batch.
749    fn build_batches(&mut self) -> Result<StatefulStreamResult<Option<RecordBatch>>> {
750        let right_batch = self.state.try_as_record_batch()?;
751        if self.left_index < self.left_data.num_rows() {
752            match self.batch_transformer.next() {
753                None => {
754                    let join_timer = self.join_metrics.join_time.timer();
755                    let result = build_batch(
756                        self.left_index,
757                        right_batch,
758                        &self.left_data,
759                        &self.schema,
760                    );
761                    join_timer.done();
762
763                    self.batch_transformer.set_batch(result?);
764                }
765                Some((batch, last)) => {
766                    if last {
767                        self.left_index += 1;
768                    }
769
770                    return Ok(StatefulStreamResult::Ready(Some(batch)));
771                }
772            }
773        } else {
774            self.state = CrossJoinStreamState::FetchProbeBatch;
775        }
776        Ok(StatefulStreamResult::Continue)
777    }
778}
779
780#[cfg(test)]
781mod tests {
782    use super::*;
783    use crate::common;
784    use crate::test::{assert_join_metrics, build_table_scan_i32};
785
786    use datafusion_common::{assert_contains, test_util::batches_to_sort_string};
787    use datafusion_execution::runtime_env::RuntimeEnvBuilder;
788    use insta::assert_snapshot;
789
790    async fn join_collect(
791        left: Arc<dyn ExecutionPlan>,
792        right: Arc<dyn ExecutionPlan>,
793        context: Arc<TaskContext>,
794    ) -> Result<(Vec<String>, Vec<RecordBatch>, MetricsSet)> {
795        let join = CrossJoinExec::new(left, right);
796        let columns_header = columns(&join.schema());
797
798        let stream = join.execute(0, context)?;
799        let batches = common::collect(stream).await?;
800        let metrics = join.metrics().unwrap();
801
802        Ok((columns_header, batches, metrics))
803    }
804
805    #[tokio::test]
806    async fn test_stats_cartesian_product() {
807        let left_row_count = 11;
808        let left_bytes = 23;
809        let right_row_count = 7;
810        let right_bytes = 27;
811
812        let left = Statistics {
813            num_rows: Precision::Exact(left_row_count),
814            total_byte_size: Precision::Exact(left_bytes),
815            column_statistics: vec![
816                ColumnStatistics {
817                    distinct_count: Precision::Exact(5),
818                    max_value: Precision::Exact(ScalarValue::Int64(Some(21))),
819                    min_value: Precision::Exact(ScalarValue::Int64(Some(-4))),
820                    sum_value: Precision::Exact(ScalarValue::Int64(Some(42))),
821                    null_count: Precision::Exact(0),
822                    byte_size: Precision::Absent,
823                },
824                ColumnStatistics {
825                    distinct_count: Precision::Exact(1),
826                    max_value: Precision::Exact(ScalarValue::from("x")),
827                    min_value: Precision::Exact(ScalarValue::from("a")),
828                    sum_value: Precision::Absent,
829                    null_count: Precision::Exact(3),
830                    byte_size: Precision::Absent,
831                },
832            ],
833        };
834
835        let right = Statistics {
836            num_rows: Precision::Exact(right_row_count),
837            total_byte_size: Precision::Exact(right_bytes),
838            column_statistics: vec![ColumnStatistics {
839                distinct_count: Precision::Exact(3),
840                max_value: Precision::Exact(ScalarValue::Int64(Some(12))),
841                min_value: Precision::Exact(ScalarValue::Int64(Some(0))),
842                sum_value: Precision::Exact(ScalarValue::Int64(Some(20))),
843                null_count: Precision::Exact(2),
844                byte_size: Precision::Absent,
845            }],
846        };
847
848        let result = stats_cartesian_product(left, right);
849
850        let expected = Statistics {
851            num_rows: Precision::Exact(left_row_count * right_row_count),
852            total_byte_size: Precision::Exact(
853                left_bytes * right_row_count + right_bytes * left_row_count,
854            ),
855            column_statistics: vec![
856                ColumnStatistics {
857                    distinct_count: Precision::Exact(5),
858                    max_value: Precision::Exact(ScalarValue::Int64(Some(21))),
859                    min_value: Precision::Exact(ScalarValue::Int64(Some(-4))),
860                    sum_value: Precision::Exact(ScalarValue::Int64(Some(
861                        42 * right_row_count as i64,
862                    ))),
863                    null_count: Precision::Exact(0),
864                    byte_size: Precision::Absent,
865                },
866                ColumnStatistics {
867                    distinct_count: Precision::Exact(1),
868                    max_value: Precision::Exact(ScalarValue::from("x")),
869                    min_value: Precision::Exact(ScalarValue::from("a")),
870                    sum_value: Precision::Absent,
871                    null_count: Precision::Exact(3 * right_row_count),
872                    byte_size: Precision::Absent,
873                },
874                ColumnStatistics {
875                    distinct_count: Precision::Exact(3),
876                    max_value: Precision::Exact(ScalarValue::Int64(Some(12))),
877                    min_value: Precision::Exact(ScalarValue::Int64(Some(0))),
878                    sum_value: Precision::Exact(ScalarValue::Int64(Some(
879                        20 * left_row_count as i64,
880                    ))),
881                    null_count: Precision::Exact(2 * left_row_count),
882                    byte_size: Precision::Absent,
883                },
884            ],
885        };
886
887        assert_eq!(result, expected);
888    }
889
890    #[tokio::test]
891    async fn test_stats_cartesian_product_with_unknown_size() {
892        let left_row_count = 11;
893
894        let left = Statistics {
895            num_rows: Precision::Exact(left_row_count),
896            total_byte_size: Precision::Exact(23),
897            column_statistics: vec![
898                ColumnStatistics {
899                    distinct_count: Precision::Exact(5),
900                    max_value: Precision::Exact(ScalarValue::Int64(Some(21))),
901                    min_value: Precision::Exact(ScalarValue::Int64(Some(-4))),
902                    sum_value: Precision::Exact(ScalarValue::Int64(Some(42))),
903                    null_count: Precision::Exact(0),
904                    byte_size: Precision::Absent,
905                },
906                ColumnStatistics {
907                    distinct_count: Precision::Exact(1),
908                    max_value: Precision::Exact(ScalarValue::from("x")),
909                    min_value: Precision::Exact(ScalarValue::from("a")),
910                    sum_value: Precision::Absent,
911                    null_count: Precision::Exact(3),
912                    byte_size: Precision::Absent,
913                },
914            ],
915        };
916
917        let right = Statistics {
918            num_rows: Precision::Absent,
919            total_byte_size: Precision::Absent,
920            column_statistics: vec![ColumnStatistics {
921                distinct_count: Precision::Exact(3),
922                max_value: Precision::Exact(ScalarValue::Int64(Some(12))),
923                min_value: Precision::Exact(ScalarValue::Int64(Some(0))),
924                sum_value: Precision::Exact(ScalarValue::Int64(Some(20))),
925                null_count: Precision::Exact(2),
926                byte_size: Precision::Absent,
927            }],
928        };
929
930        let result = stats_cartesian_product(left, right);
931
932        let expected = Statistics {
933            num_rows: Precision::Absent,
934            total_byte_size: Precision::Absent,
935            column_statistics: vec![
936                ColumnStatistics {
937                    distinct_count: Precision::Exact(5),
938                    max_value: Precision::Exact(ScalarValue::Int64(Some(21))),
939                    min_value: Precision::Exact(ScalarValue::Int64(Some(-4))),
940                    sum_value: Precision::Absent, // we don't know the row count on the right
941                    null_count: Precision::Absent, // we don't know the row count on the right
942                    byte_size: Precision::Absent,
943                },
944                ColumnStatistics {
945                    distinct_count: Precision::Exact(1),
946                    max_value: Precision::Exact(ScalarValue::from("x")),
947                    min_value: Precision::Exact(ScalarValue::from("a")),
948                    sum_value: Precision::Absent,
949                    null_count: Precision::Absent, // we don't know the row count on the right
950                    byte_size: Precision::Absent,
951                },
952                ColumnStatistics {
953                    distinct_count: Precision::Exact(3),
954                    max_value: Precision::Exact(ScalarValue::Int64(Some(12))),
955                    min_value: Precision::Exact(ScalarValue::Int64(Some(0))),
956                    sum_value: Precision::Exact(ScalarValue::Int64(Some(
957                        20 * left_row_count as i64,
958                    ))),
959                    null_count: Precision::Exact(2 * left_row_count),
960                    byte_size: Precision::Absent,
961                },
962            ],
963        };
964
965        assert_eq!(result, expected);
966    }
967
968    #[tokio::test]
969    async fn test_stats_cartesian_product_unsigned_sum_widens_to_u64() {
970        let left_row_count = 2;
971        let right_row_count = 3;
972
973        let left = Statistics {
974            num_rows: Precision::Exact(left_row_count),
975            total_byte_size: Precision::Exact(10),
976            column_statistics: vec![ColumnStatistics {
977                distinct_count: Precision::Exact(2),
978                max_value: Precision::Exact(ScalarValue::UInt32(Some(10))),
979                min_value: Precision::Exact(ScalarValue::UInt32(Some(1))),
980                sum_value: Precision::Exact(ScalarValue::UInt32(Some(7))),
981                null_count: Precision::Exact(0),
982                byte_size: Precision::Absent,
983            }],
984        };
985
986        let right = Statistics {
987            num_rows: Precision::Exact(right_row_count),
988            total_byte_size: Precision::Exact(10),
989            column_statistics: vec![ColumnStatistics {
990                distinct_count: Precision::Exact(3),
991                max_value: Precision::Exact(ScalarValue::UInt32(Some(12))),
992                min_value: Precision::Exact(ScalarValue::UInt32(Some(0))),
993                sum_value: Precision::Exact(ScalarValue::UInt32(Some(11))),
994                null_count: Precision::Exact(0),
995                byte_size: Precision::Absent,
996            }],
997        };
998
999        let result = stats_cartesian_product(left, right);
1000
1001        assert_eq!(
1002            result.column_statistics[0].sum_value,
1003            Precision::Exact(ScalarValue::UInt64(Some(21)))
1004        );
1005        assert_eq!(
1006            result.column_statistics[1].sum_value,
1007            Precision::Exact(ScalarValue::UInt64(Some(22)))
1008        );
1009    }
1010
1011    #[tokio::test]
1012    async fn test_join() -> Result<()> {
1013        let task_ctx = Arc::new(TaskContext::default());
1014
1015        let left = build_table_scan_i32(
1016            ("a1", &vec![1, 2, 3]),
1017            ("b1", &vec![4, 5, 6]),
1018            ("c1", &vec![7, 8, 9]),
1019        );
1020        let right = build_table_scan_i32(
1021            ("a2", &vec![10, 11]),
1022            ("b2", &vec![12, 13]),
1023            ("c2", &vec![14, 15]),
1024        );
1025
1026        let (columns, batches, metrics) = join_collect(left, right, task_ctx).await?;
1027
1028        assert_eq!(columns, vec!["a1", "b1", "c1", "a2", "b2", "c2"]);
1029
1030        assert_snapshot!(batches_to_sort_string(&batches), @r"
1031        +----+----+----+----+----+----+
1032        | a1 | b1 | c1 | a2 | b2 | c2 |
1033        +----+----+----+----+----+----+
1034        | 1  | 4  | 7  | 10 | 12 | 14 |
1035        | 1  | 4  | 7  | 11 | 13 | 15 |
1036        | 2  | 5  | 8  | 10 | 12 | 14 |
1037        | 2  | 5  | 8  | 11 | 13 | 15 |
1038        | 3  | 6  | 9  | 10 | 12 | 14 |
1039        | 3  | 6  | 9  | 11 | 13 | 15 |
1040        +----+----+----+----+----+----+
1041        ");
1042
1043        assert_join_metrics!(metrics, 6);
1044
1045        Ok(())
1046    }
1047
1048    #[tokio::test]
1049    async fn test_overallocation() -> Result<()> {
1050        let runtime = RuntimeEnvBuilder::new()
1051            .with_memory_limit(100, 1.0)
1052            .build_arc()?;
1053        let task_ctx = TaskContext::default().with_runtime(runtime);
1054        let task_ctx = Arc::new(task_ctx);
1055
1056        let left = build_table_scan_i32(
1057            ("a1", &vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 0]),
1058            ("b1", &vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 0]),
1059            ("c1", &vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 0]),
1060        );
1061        let right = build_table_scan_i32(
1062            ("a2", &vec![10, 11]),
1063            ("b2", &vec![12, 13]),
1064            ("c2", &vec![14, 15]),
1065        );
1066
1067        let err = join_collect(left, right, task_ctx).await.unwrap_err();
1068
1069        assert_contains!(
1070            err.to_string(),
1071            "Resources exhausted: Additional allocation failed for CrossJoinExec with top memory consumers (across reservations) as:\n  CrossJoinExec"
1072        );
1073
1074        Ok(())
1075    }
1076
1077    /// Returns the column names on the schema
1078    fn columns(schema: &Schema) -> Vec<String> {
1079        schema.fields().iter().map(|f| f.name().clone()).collect()
1080    }
1081}