1use 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#[derive(Debug)]
61struct JoinLeftData {
62 merged_batch: RecordBatch,
64 _reservation: MemoryReservation,
67}
68
69#[expect(rustdoc::private_intra_doc_links)]
70#[derive(Debug)]
85pub struct CrossJoinExec {
86 pub left: Arc<dyn ExecutionPlan>,
88 pub right: Arc<dyn ExecutionPlan>,
90 schema: SchemaRef,
92 left_fut: OnceAsync<JoinLeftData>,
99 metrics: ExecutionPlanMetricsSet,
101 cache: Arc<PlanProperties>,
103}
104
105impl CrossJoinExec {
106 pub fn new(left: Arc<dyn ExecutionPlan>, right: Arc<dyn ExecutionPlan>) -> Self {
108 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 pub fn left(&self) -> &Arc<dyn ExecutionPlan> {
139 &self.left
140 }
141
142 pub fn right(&self) -> &Arc<dyn ExecutionPlan> {
144 &self.right
145 }
146
147 fn compute_properties(
149 left: &Arc<dyn ExecutionPlan>,
150 right: &Arc<dyn ExecutionPlan>,
151 schema: SchemaRef,
152 ) -> Result<PlanProperties> {
153 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 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 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
202async fn load_left_input(
204 stream: SendableRecordBatchStream,
205 metrics: BuildProbeJoinMetrics,
206 reservation: MemoryReservation,
207) -> Result<JoinLeftData> {
208 let left_schema = stream.schema();
209
210 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 reservation.try_grow(batch_size)?;
218 metrics.build_mem_used.add(batch_size);
220 metrics.build_input_batches.add(1);
221 metrics.build_input_rows.add(batch.num_rows());
222 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 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 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(), 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 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 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 fn try_swapping_with_projection(
430 &self,
431 projection: &ProjectionExec,
432 ) -> Result<Option<Arc<dyn ExecutionPlan>>> {
433 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
518fn 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 let num_rows = left_row_count.multiply(&right_row_count);
528
529 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 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 .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 .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
591struct CrossJoinStream<T> {
593 schema: Arc<Schema>,
595 left_fut: OnceFut<JoinLeftData>,
597 right: SendableRecordBatchStream,
599 left_index: usize,
601 join_metrics: BuildProbeJoinMetrics,
603 state: CrossJoinStreamState,
605 left_data: RecordBatch,
607 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
617enum CrossJoinStreamState {
619 WaitBuildSide,
620 FetchProbeBatch,
621 BuildBatches(RecordBatch),
623}
624
625impl CrossJoinStreamState {
626 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 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 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 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 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 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 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, null_count: Precision::Absent, 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, 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 fn columns(schema: &Schema) -> Vec<String> {
1079 schema.fields().iter().map(|f| f.name().clone()).collect()
1080 }
1081}