1use std::fmt::Formatter;
21use std::ops::{BitOr, ControlFlow};
22use std::sync::Arc;
23use std::sync::atomic::{AtomicUsize, Ordering};
24use std::task::Poll;
25
26use super::utils::{
27 asymmetric_join_output_partitioning, need_produce_result_in_final,
28 reorder_output_after_swap, swap_join_projection,
29};
30use crate::common::can_project;
31use crate::execution_plan::{EmissionType, boundedness_from_children};
32use crate::joins::SharedBitmapBuilder;
33use crate::joins::utils::{
34 BuildProbeJoinMetrics, ColumnIndex, JoinFilter, OnceAsync, OnceFut,
35 build_join_schema, check_join_is_valid, estimate_join_statistics,
36 need_produce_right_in_final,
37};
38use crate::metrics::{
39 Count, ExecutionPlanMetricsSet, MetricBuilder, MetricType, MetricsSet, RatioMetrics,
40};
41use crate::projection::{
42 EmbeddedProjection, JoinData, ProjectionExec, try_embed_projection,
43 try_pushdown_through_join_with_column_indices,
44};
45use crate::statistics::{ChildStats, StatisticsArgs};
46use crate::{
47 ChildrenPropertiesMode, DisplayAs, DisplayFormatType, Distribution, ExecutionPlan,
48 ExecutionPlanProperties, PlanProperties, RecordBatchStream, ReplaceChildrenOptions,
49 SendableRecordBatchStream, validate_child_count,
50};
51
52use arrow::array::{
53 Array, BooleanArray, BooleanBufferBuilder, RecordBatchOptions, UInt32Array,
54 UInt64Array, new_null_array,
55};
56use arrow::buffer::BooleanBuffer;
57use arrow::compute::{
58 BatchCoalescer, concat_batches, filter, filter_record_batch, not, take,
59};
60use arrow::datatypes::{Schema, SchemaRef};
61use arrow::record_batch::RecordBatch;
62use arrow_schema::DataType;
63use datafusion_common::cast::as_boolean_array;
64use datafusion_common::tree_node::TreeNodeRecursion;
65use datafusion_common::{
66 JoinSide, NullEquality, Result, ScalarValue, Statistics, arrow_err,
67 assert_eq_or_internal_err, internal_datafusion_err, internal_err, project_schema,
68 unwrap_or_internal_err,
69};
70use datafusion_execution::memory_pool::{MemoryConsumer, MemoryReservation};
71use datafusion_execution::{SpillFile, TaskContext};
72use datafusion_expr::JoinType;
73use datafusion_physical_expr::equivalence::{
74 ProjectionMapping, join_equivalence_properties,
75};
76
77use datafusion_physical_expr::projection::{ProjectionRef, combine_projections};
78use futures::{Stream, StreamExt, TryStreamExt};
79use log::debug;
80use parking_lot::Mutex;
81
82use crate::metrics::SpillMetrics;
83use crate::spill::replayable_spill_input::ReplayableStreamSource;
84use crate::spill::spill_manager::SpillManager;
85
86#[expect(rustdoc::private_intra_doc_links)]
87#[derive(Debug)]
194pub struct NestedLoopJoinExec {
195 pub(crate) left: Arc<dyn ExecutionPlan>,
197 pub(crate) right: Arc<dyn ExecutionPlan>,
199 pub(crate) filter: Option<JoinFilter>,
201 pub(crate) join_type: JoinType,
203 join_schema: SchemaRef,
206 build_side_data: OnceAsync<JoinLeftData>,
213 left_spill_data: Arc<OnceAsync<LeftSpillData>>,
220 column_indices: Vec<ColumnIndex>,
222 projection: Option<ProjectionRef>,
224
225 metrics: ExecutionPlanMetricsSet,
227 cache: Arc<PlanProperties>,
229}
230
231pub struct NestedLoopJoinExecBuilder {
233 left: Arc<dyn ExecutionPlan>,
234 right: Arc<dyn ExecutionPlan>,
235 join_type: JoinType,
236 filter: Option<JoinFilter>,
237 projection: Option<ProjectionRef>,
238}
239
240impl NestedLoopJoinExecBuilder {
241 pub fn new(
243 left: Arc<dyn ExecutionPlan>,
244 right: Arc<dyn ExecutionPlan>,
245 join_type: JoinType,
246 ) -> Self {
247 Self {
248 left,
249 right,
250 join_type,
251 filter: None,
252 projection: None,
253 }
254 }
255
256 pub fn with_projection(self, projection: Option<Vec<usize>>) -> Self {
258 self.with_projection_ref(projection.map(Into::into))
259 }
260
261 pub fn with_projection_ref(mut self, projection: Option<ProjectionRef>) -> Self {
263 self.projection = projection;
264 self
265 }
266
267 pub fn with_filter(mut self, filter: Option<JoinFilter>) -> Self {
269 self.filter = filter;
270 self
271 }
272
273 pub fn build(self) -> Result<NestedLoopJoinExec> {
275 let Self {
276 left,
277 right,
278 join_type,
279 filter,
280 projection,
281 } = self;
282
283 let left_schema = left.schema();
284 let right_schema = right.schema();
285 check_join_is_valid(&left_schema, &right_schema, &[])?;
286 let (join_schema, column_indices) =
287 build_join_schema(&left_schema, &right_schema, &join_type);
288 let join_schema = Arc::new(join_schema);
289 let cache = NestedLoopJoinExec::compute_properties(
290 &left,
291 &right,
292 &join_schema,
293 join_type,
294 projection.as_deref(),
295 )?;
296 Ok(NestedLoopJoinExec {
297 left,
298 right,
299 filter,
300 join_type,
301 join_schema,
302 build_side_data: Default::default(),
303 left_spill_data: Arc::new(OnceAsync::default()),
304 column_indices,
305 projection,
306 metrics: Default::default(),
307 cache: Arc::new(cache),
308 })
309 }
310}
311
312impl From<&NestedLoopJoinExec> for NestedLoopJoinExecBuilder {
313 fn from(exec: &NestedLoopJoinExec) -> Self {
314 Self {
315 left: Arc::clone(exec.left()),
316 right: Arc::clone(exec.right()),
317 join_type: exec.join_type,
318 filter: exec.filter.clone(),
319 projection: exec.projection.clone(),
320 }
321 }
322}
323
324impl NestedLoopJoinExec {
325 pub fn try_new(
327 left: Arc<dyn ExecutionPlan>,
328 right: Arc<dyn ExecutionPlan>,
329 filter: Option<JoinFilter>,
330 join_type: &JoinType,
331 projection: Option<Vec<usize>>,
332 ) -> Result<Self> {
333 NestedLoopJoinExecBuilder::new(left, right, *join_type)
334 .with_projection(projection)
335 .with_filter(filter)
336 .build()
337 }
338
339 pub fn left(&self) -> &Arc<dyn ExecutionPlan> {
341 &self.left
342 }
343
344 pub fn right(&self) -> &Arc<dyn ExecutionPlan> {
346 &self.right
347 }
348
349 pub fn filter(&self) -> Option<&JoinFilter> {
351 self.filter.as_ref()
352 }
353
354 pub fn join_type(&self) -> &JoinType {
356 &self.join_type
357 }
358
359 pub fn projection(&self) -> &Option<ProjectionRef> {
360 &self.projection
361 }
362
363 fn compute_properties(
365 left: &Arc<dyn ExecutionPlan>,
366 right: &Arc<dyn ExecutionPlan>,
367 schema: &SchemaRef,
368 join_type: JoinType,
369 projection: Option<&[usize]>,
370 ) -> Result<PlanProperties> {
371 let mut eq_properties = join_equivalence_properties(
373 left.equivalence_properties().clone(),
374 right.equivalence_properties().clone(),
375 &join_type,
376 Arc::clone(schema),
377 &Self::maintains_input_order(join_type),
378 None,
379 &[],
381 )?;
382
383 let mut output_partitioning =
384 asymmetric_join_output_partitioning(left, right, &join_type)?;
385
386 let emission_type = if left.boundedness().is_unbounded() {
387 EmissionType::Final
388 } else if right.pipeline_behavior() == EmissionType::Incremental {
389 match join_type {
390 JoinType::Inner
393 | JoinType::LeftSemi
394 | JoinType::RightSemi
395 | JoinType::Right
396 | JoinType::RightAnti
397 | JoinType::RightMark => EmissionType::Incremental,
398 JoinType::Left
401 | JoinType::LeftAnti
402 | JoinType::LeftMark
403 | JoinType::Full => EmissionType::Both,
404 }
405 } else {
406 right.pipeline_behavior()
407 };
408
409 if let Some(projection) = projection {
410 let projection_mapping = ProjectionMapping::from_indices(projection, schema)?;
412 let out_schema = project_schema(schema, Some(&projection))?;
413 output_partitioning =
414 output_partitioning.project(&projection_mapping, &eq_properties);
415 eq_properties = eq_properties.project(&projection_mapping, out_schema);
416 }
417
418 Ok(PlanProperties::new(
419 eq_properties,
420 output_partitioning,
421 emission_type,
422 boundedness_from_children([left, right]),
423 ))
424 }
425
426 fn maintains_input_order(_join_type: JoinType) -> Vec<bool> {
428 vec![false, false]
429 }
430
431 pub fn contains_projection(&self) -> bool {
432 self.projection.is_some()
433 }
434
435 pub fn with_projection(&self, projection: Option<Vec<usize>>) -> Result<Self> {
436 let projection = projection.map(Into::into);
437 can_project(&self.schema(), projection.as_deref())?;
439 let projection =
440 combine_projections(projection.as_ref(), self.projection.as_ref())?;
441 NestedLoopJoinExecBuilder::from(self)
442 .with_projection_ref(projection)
443 .build()
444 }
445
446 pub fn swap_inputs(&self) -> Result<Arc<dyn ExecutionPlan>> {
455 let left = self.left();
456 let right = self.right();
457 let new_join = NestedLoopJoinExec::try_new(
458 Arc::clone(right),
459 Arc::clone(left),
460 self.filter().map(JoinFilter::swap),
461 &self.join_type().swap(),
462 swap_join_projection(
463 left.schema().fields().len(),
464 right.schema().fields().len(),
465 self.projection.as_deref(),
466 self.join_type(),
467 ),
468 )?;
469
470 let plan: Arc<dyn ExecutionPlan> = if matches!(
473 self.join_type(),
474 JoinType::LeftSemi
475 | JoinType::RightSemi
476 | JoinType::LeftAnti
477 | JoinType::RightAnti
478 | JoinType::LeftMark
479 | JoinType::RightMark
480 ) || self.projection.is_some()
481 {
482 Arc::new(new_join)
483 } else {
484 reorder_output_after_swap(
485 Arc::new(new_join),
486 &self.left().schema(),
487 &self.right().schema(),
488 )?
489 };
490
491 Ok(plan)
492 }
493}
494
495impl DisplayAs for NestedLoopJoinExec {
496 fn fmt_as(&self, t: DisplayFormatType, f: &mut Formatter) -> std::fmt::Result {
497 match t {
498 DisplayFormatType::Default | DisplayFormatType::Verbose => {
499 let display_filter = self.filter.as_ref().map_or_else(
500 || "".to_string(),
501 |f| format!(", filter={}", f.expression()),
502 );
503 let display_projections = if self.contains_projection() {
504 format!(
505 ", projection=[{}]",
506 self.projection
507 .as_ref()
508 .unwrap()
509 .iter()
510 .map(|index| format!(
511 "{}@{}",
512 self.join_schema.fields().get(*index).unwrap().name(),
513 index
514 ))
515 .collect::<Vec<_>>()
516 .join(", ")
517 )
518 } else {
519 "".to_string()
520 };
521 write!(
522 f,
523 "NestedLoopJoinExec: join_type={:?}{}{}",
524 self.join_type, display_filter, display_projections
525 )
526 }
527 DisplayFormatType::TreeRender => {
528 if *self.join_type() != JoinType::Inner {
529 writeln!(f, "join_type={:?}", self.join_type)
530 } else {
531 Ok(())
532 }
533 }
534 }
535 }
536}
537
538impl ExecutionPlan for NestedLoopJoinExec {
539 fn name(&self) -> &'static str {
540 "NestedLoopJoinExec"
541 }
542
543 fn properties(&self) -> &Arc<PlanProperties> {
544 &self.cache
545 }
546
547 fn required_input_distribution(&self) -> Vec<Distribution> {
548 self.input_distribution_requirements().into_per_child()
549 }
550
551 fn input_distribution_requirements(&self) -> crate::InputDistributionRequirements {
552 crate::InputDistributionRequirements::new(vec![
553 Distribution::SinglePartition,
554 Distribution::UnspecifiedDistribution,
555 ])
556 }
557
558 fn maintains_input_order(&self) -> Vec<bool> {
559 Self::maintains_input_order(self.join_type)
560 }
561
562 fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
563 vec![&self.left, &self.right]
564 }
565
566 fn apply_expressions(
567 &self,
568 f: &mut dyn FnMut(&Arc<dyn crate::PhysicalExpr>) -> Result<TreeNodeRecursion>,
569 ) -> Result<TreeNodeRecursion> {
570 crate::apply_expression_roots(
572 self.filter.iter().map(|filter| filter.expression()),
573 f,
574 )
575 }
576
577 fn replace_children(
578 self: Arc<Self>,
579 mut children: Vec<Arc<dyn ExecutionPlan>>,
580 options: ReplaceChildrenOptions,
581 ) -> Result<Arc<dyn ExecutionPlan>> {
582 validate_child_count!(self, children);
583 match options.children_properties {
584 ChildrenPropertiesMode::Keep => {
585 let left = children.swap_remove(0);
586 let right = children.swap_remove(0);
587 Ok(Arc::new(Self {
588 left,
589 right,
590 metrics: ExecutionPlanMetricsSet::new(),
591 build_side_data: Default::default(),
592 left_spill_data: Arc::new(OnceAsync::default()),
593 cache: Arc::clone(&self.cache),
594 filter: self.filter.clone(),
595 join_type: self.join_type,
596 join_schema: Arc::clone(&self.join_schema),
597 column_indices: self.column_indices.clone(),
598 projection: self.projection.clone(),
599 }))
600 }
601 ChildrenPropertiesMode::Recompute => Ok(Arc::new(
602 NestedLoopJoinExecBuilder::new(
603 Arc::clone(&children[0]),
604 Arc::clone(&children[1]),
605 self.join_type,
606 )
607 .with_filter(self.filter.clone())
608 .with_projection_ref(self.projection.clone())
609 .build()?,
610 )),
611 }
612 }
613
614 fn with_new_children(
615 self: Arc<Self>,
616 children: Vec<Arc<dyn ExecutionPlan>>,
617 ) -> Result<Arc<dyn ExecutionPlan>> {
618 self.replace_children(
619 children,
620 ReplaceChildrenOptions::new(ChildrenPropertiesMode::Recompute),
621 )
622 }
623
624 fn with_new_children_and_same_properties(
625 self: Arc<Self>,
626 children: Vec<Arc<dyn ExecutionPlan>>,
627 ) -> Result<Arc<dyn ExecutionPlan>> {
628 self.replace_children(
629 children,
630 ReplaceChildrenOptions::new(ChildrenPropertiesMode::Keep),
631 )
632 }
633
634 fn execute(
635 &self,
636 partition: usize,
637 context: Arc<TaskContext>,
638 ) -> Result<SendableRecordBatchStream> {
639 assert_eq_or_internal_err!(
640 self.left.output_partitioning().partition_count(),
641 1,
642 "Invalid NestedLoopJoinExec, the output partition count of the left child must be 1,\
643 consider using CoalescePartitionsExec or the EnforceDistribution rule"
644 );
645
646 let metrics = NestedLoopJoinMetrics::new(&self.metrics, partition);
647 let batch_size = context.session_config().batch_size();
648
649 let column_indices_after_projection = match self.projection.as_ref() {
651 Some(projection) => projection
652 .iter()
653 .map(|i| self.column_indices[*i].clone())
654 .collect(),
655 None => self.column_indices.clone(),
656 };
657
658 let right_partition_count = self.right().output_partitioning().partition_count();
659
660 let load_reservation =
664 MemoryConsumer::new(format!("NestedLoopJoinLoad[{partition}]"))
665 .register(context.memory_pool());
666
667 let build_side_data = self.build_side_data.try_once(|| {
668 let stream = self.left.execute(0, Arc::clone(&context))?;
669
670 Ok(collect_left_input(
671 stream,
672 metrics.join_metrics.clone(),
673 load_reservation,
674 need_produce_result_in_final(self.join_type),
675 right_partition_count,
676 ))
677 })?;
678
679 let probe_side_data = self.right.execute(partition, Arc::clone(&context))?;
680
681 let full_join_multi_partition =
697 matches!(self.join_type, JoinType::Full) && right_partition_count > 1;
698 let spill_state = if context.runtime_env().disk_manager.tmp_files_enabled()
699 && !full_join_multi_partition
700 {
701 SpillState::Pending {
702 left_plan: Arc::clone(&self.left),
703 task_context: Arc::clone(&context),
704 left_spill_data: Arc::clone(&self.left_spill_data),
705 }
706 } else {
707 SpillState::Disabled
708 };
709
710 Ok(Box::pin(NestedLoopJoinStream::new(
711 self.schema(),
712 self.filter.clone(),
713 self.join_type,
714 probe_side_data,
715 build_side_data,
716 column_indices_after_projection,
717 metrics,
718 batch_size,
719 spill_state,
720 )))
721 }
722
723 fn metrics(&self) -> Option<MetricsSet> {
724 Some(self.metrics.clone_inner())
725 }
726
727 fn child_stats_requests(&self, partition: Option<usize>) -> Vec<ChildStats> {
728 vec![ChildStats::At(None), ChildStats::At(partition)]
731 }
732
733 fn statistics_from_inputs(
734 &self,
735 input_stats: &[Arc<Statistics>],
736 _args: &StatisticsArgs,
737 ) -> Result<Arc<Statistics>> {
738 let join_columns = Vec::new();
746
747 let left_stats = input_stats[0].as_ref().clone();
748 let right_stats = input_stats[1].as_ref().clone();
749
750 let stats = estimate_join_statistics(
751 left_stats,
752 right_stats,
753 &join_columns,
754 NullEquality::NullEqualsNothing,
755 &self.join_type,
756 &self.join_schema,
757 )?;
758
759 Ok(Arc::new(stats.project(self.projection.as_ref())))
760 }
761
762 fn try_swapping_with_projection(
766 &self,
767 projection: &ProjectionExec,
768 ) -> Result<Option<Arc<dyn ExecutionPlan>>> {
769 if self.contains_projection() {
771 return Ok(None);
772 }
773
774 let schema = self.schema();
775 if let Some(JoinData {
776 projected_left_child,
777 projected_right_child,
778 join_filter,
779 ..
780 }) = try_pushdown_through_join_with_column_indices(
781 projection,
782 self.left(),
783 self.right(),
784 &[],
785 &schema,
786 self.filter(),
787 self.column_indices.as_slice(),
788 )? {
789 Ok(Some(Arc::new(NestedLoopJoinExec::try_new(
790 Arc::new(projected_left_child),
791 Arc::new(projected_right_child),
792 join_filter,
793 self.join_type(),
794 None,
796 )?)))
797 } else {
798 try_embed_projection(projection, self)
799 }
800 }
801 #[cfg(feature = "proto")]
802 fn try_to_proto(
803 &self,
804 ctx: &crate::proto::ExecutionPlanEncodeCtx<'_>,
805 ) -> Result<Option<datafusion_proto_models::protobuf::PhysicalPlanNode>> {
806 use datafusion_proto_models::protobuf;
807
808 let left = ctx.encode_child(self.left())?;
809 let right = ctx.encode_child(self.right())?;
810
811 let join_type = crate::joins::proto::join_type_to_proto(*self.join_type());
812
813 let filter = self
814 .filter()
815 .map(|f| crate::joins::proto::join_filter_to_proto(f, ctx))
816 .transpose()?;
817
818 Ok(Some(protobuf::PhysicalPlanNode {
819 physical_plan_type: Some(
820 protobuf::physical_plan_node::PhysicalPlanType::NestedLoopJoin(Box::new(
821 protobuf::NestedLoopJoinExecNode {
822 left: Some(Box::new(left)),
823 right: Some(Box::new(right)),
824 join_type: join_type.into(),
825 filter,
826 projection: match self.projection.as_ref() {
827 None => Vec::new(),
828 Some(v) if v.is_empty() => vec![u32::MAX],
829 Some(v) => v.iter().map(|x| *x as u32).collect(),
830 },
831 },
832 )),
833 ),
834 }))
835 }
836}
837
838#[cfg(feature = "proto")]
839impl NestedLoopJoinExec {
840 pub fn try_from_proto(
841 node: &datafusion_proto_models::protobuf::PhysicalPlanNode,
842 ctx: &crate::proto::ExecutionPlanDecodeCtx<'_>,
843 ) -> Result<Arc<dyn ExecutionPlan>> {
844 use datafusion_proto_models::protobuf;
845
846 let join = crate::expect_plan_variant!(
847 node,
848 protobuf::physical_plan_node::PhysicalPlanType::NestedLoopJoin,
849 "NestedLoopJoinExec",
850 );
851
852 let left = ctx.decode_required_child(
853 join.left.as_deref(),
854 "NestedLoopJoinExec",
855 "left",
856 )?;
857 let right = ctx.decode_required_child(
858 join.right.as_deref(),
859 "NestedLoopJoinExec",
860 "right",
861 )?;
862
863 let join_type = crate::joins::proto::join_type_from_proto(
864 join.join_type,
865 "NestedLoopJoinExec",
866 )?;
867
868 let filter = join
869 .filter
870 .as_ref()
871 .map(|f| {
872 crate::joins::proto::join_filter_from_proto(f, ctx, "NestedLoopJoinExec")
873 })
874 .transpose()?;
875
876 let projection = match join.projection.as_slice() {
877 [] => None,
878 [u32::MAX] => Some(Vec::new()),
879 indices => Some(indices.iter().map(|i| *i as usize).collect()),
880 };
881
882 Ok(Arc::new(NestedLoopJoinExec::try_new(
883 left, right, filter, &join_type, projection,
884 )?))
885 }
886}
887
888impl EmbeddedProjection for NestedLoopJoinExec {
889 fn with_projection(&self, projection: Option<Vec<usize>>) -> Result<Self> {
890 self.with_projection(projection)
891 }
892}
893
894pub(crate) struct JoinLeftData {
896 batch: RecordBatch,
898 bitmap: SharedBitmapBuilder,
900 probe_threads_counter: AtomicUsize,
902 #[expect(dead_code)]
906 reservation: MemoryReservation,
907}
908
909impl JoinLeftData {
910 pub(crate) fn new(
911 batch: RecordBatch,
912 bitmap: SharedBitmapBuilder,
913 probe_threads_counter: AtomicUsize,
914 reservation: MemoryReservation,
915 ) -> Self {
916 Self {
917 batch,
918 bitmap,
919 probe_threads_counter,
920 reservation,
921 }
922 }
923
924 pub(crate) fn batch(&self) -> &RecordBatch {
925 &self.batch
926 }
927
928 pub(crate) fn bitmap(&self) -> &SharedBitmapBuilder {
929 &self.bitmap
930 }
931
932 pub(crate) fn report_probe_completed(&self) -> bool {
935 self.probe_threads_counter.fetch_sub(1, Ordering::Relaxed) == 1
936 }
937}
938
939async fn collect_left_input(
941 stream: SendableRecordBatchStream,
942 join_metrics: BuildProbeJoinMetrics,
943 reservation: MemoryReservation,
944 with_visited_left_side: bool,
945 probe_threads_count: usize,
946) -> Result<JoinLeftData> {
947 let schema = stream.schema();
948
949 let (batches, metrics, reservation) = stream
951 .try_fold(
952 (Vec::new(), join_metrics, reservation),
953 |(mut batches, metrics, reservation), batch| async {
954 let batch_size = batch.get_array_memory_size();
955 reservation.try_grow(batch_size)?;
957 metrics.build_mem_used.add(batch_size);
959 metrics.build_input_batches.add(1);
960 metrics.build_input_rows.add(batch.num_rows());
961 batches.push(batch);
963 Ok((batches, metrics, reservation))
964 },
965 )
966 .await?;
967
968 let merged_batch = concat_batches(&schema, &batches)?;
969
970 let visited_left_side = if with_visited_left_side {
972 let n_rows = merged_batch.num_rows();
973 let buffer_size = n_rows.div_ceil(8);
974 reservation.try_grow(buffer_size)?;
975 metrics.build_mem_used.add(buffer_size);
976
977 let mut buffer = BooleanBufferBuilder::new(n_rows);
978 buffer.append_n(n_rows, false);
979 buffer
980 } else {
981 BooleanBufferBuilder::new(0)
982 };
983
984 Ok(JoinLeftData::new(
985 merged_batch,
986 Mutex::new(visited_left_side),
987 AtomicUsize::new(probe_threads_count),
988 reservation,
989 ))
990}
991
992#[derive(Debug, Clone, Copy)]
995enum NLJState {
996 BufferingLeft,
997 FetchingRight,
998 ProbeRight,
999 EmitRightUnmatched,
1000 ProbeEnd,
1010 EmitLeftUnmatched,
1011 EmitGlobalRightUnmatched,
1016 Done,
1017}
1018pub(crate) struct LeftSpillData {
1024 spill_manager: SpillManager,
1026 spill_file: Arc<dyn SpillFile>,
1028 schema: SchemaRef,
1030}
1031
1032pub(crate) enum SpillState {
1039 Disabled,
1042
1043 Pending {
1047 left_plan: Arc<dyn ExecutionPlan>,
1049 task_context: Arc<TaskContext>,
1051 left_spill_data: Arc<OnceAsync<LeftSpillData>>,
1054 },
1055
1056 Active(Box<SpillStateActive>),
1059}
1060
1061pub(crate) struct SpillStateActive {
1064 left_spill_fut: OnceFut<LeftSpillData>,
1067 left_stream: Option<SendableRecordBatchStream>,
1070 left_schema: Option<SchemaRef>,
1072 reservation: MemoryReservation,
1074 pending_batches: Vec<RecordBatch>,
1076 right_input: ReplayableStreamSource,
1078 global_right_bitmaps: Vec<BooleanBuffer>,
1082 global_right_bitmaps_reservation: MemoryReservation,
1087 right_batch_index: usize,
1089}
1090
1091impl SpillStateActive {
1092 fn merge_current_right_bitmap(&mut self, idx: usize, values: BooleanBuffer) {
1102 if idx >= self.global_right_bitmaps.len() {
1103 let bytes = values.len().div_ceil(8);
1110 self.global_right_bitmaps_reservation.grow(bytes);
1111 self.global_right_bitmaps.push(values);
1112 } else {
1113 self.global_right_bitmaps[idx] =
1116 self.global_right_bitmaps[idx].bitor(&values);
1117 }
1118 }
1119}
1120
1121pub(crate) struct NestedLoopJoinStream {
1122 pub(crate) output_schema: Arc<Schema>,
1133 pub(crate) join_filter: Option<JoinFilter>,
1135 pub(crate) join_type: JoinType,
1137 pub(crate) right_data: Option<SendableRecordBatchStream>,
1140 pub(crate) left_data: OnceFut<JoinLeftData>,
1142 pub(crate) column_indices: Vec<ColumnIndex>,
1155 pub(crate) metrics: NestedLoopJoinMetrics,
1157
1158 batch_size: usize,
1160
1161 should_track_unmatched_right: bool,
1163
1164 state: NLJState,
1170 output_buffer: Box<BatchCoalescer>,
1173 handled_empty_output: bool,
1175
1176 buffered_left_data: Option<Arc<JoinLeftData>>,
1180 left_probe_idx: usize,
1182 left_emit_idx: usize,
1184 left_exhausted: bool,
1187 left_buffered_in_one_pass: bool,
1189
1190 current_right_batch: Option<RecordBatch>,
1194 current_right_batch_matched: Option<BooleanArray>,
1197
1198 spill_state: SpillState,
1200
1201 is_unmatched_left_emitter: bool,
1212}
1213
1214pub(crate) struct NestedLoopJoinMetrics {
1215 pub(crate) join_metrics: BuildProbeJoinMetrics,
1217 pub(crate) selectivity: RatioMetrics,
1219 pub(crate) spill_metrics: SpillMetrics,
1221}
1222
1223impl NestedLoopJoinMetrics {
1224 pub fn new(metrics: &ExecutionPlanMetricsSet, partition: usize) -> Self {
1225 Self {
1226 join_metrics: BuildProbeJoinMetrics::new(partition, metrics),
1227 selectivity: MetricBuilder::new(metrics)
1228 .with_type(MetricType::Summary)
1229 .ratio_metrics("selectivity", partition),
1230 spill_metrics: SpillMetrics::new(metrics, partition),
1231 }
1232 }
1233}
1234
1235impl Stream for NestedLoopJoinStream {
1236 type Item = Result<RecordBatch>;
1237
1238 fn poll_next(
1272 mut self: std::pin::Pin<&mut Self>,
1273 cx: &mut std::task::Context<'_>,
1274 ) -> Poll<Option<Self::Item>> {
1275 loop {
1276 match self.state {
1277 NLJState::BufferingLeft => {
1283 debug!("[NLJState] Entering: {:?}", self.state);
1284 let build_metric = self.metrics.join_metrics.build_time.clone();
1289 let _build_timer = build_metric.timer();
1290
1291 match self.handle_buffering_left(cx) {
1292 ControlFlow::Continue(()) => continue,
1293 ControlFlow::Break(poll) => return poll,
1294 }
1295 }
1296
1297 NLJState::FetchingRight => {
1321 debug!("[NLJState] Entering: {:?}", self.state);
1322 let join_metric = self.metrics.join_metrics.join_time.clone();
1324 let _join_timer = join_metric.timer();
1325
1326 match self.handle_fetching_right(cx) {
1327 ControlFlow::Continue(()) => continue,
1328 ControlFlow::Break(poll) => return poll,
1329 }
1330 }
1331
1332 NLJState::ProbeRight => {
1347 debug!("[NLJState] Entering: {:?}", self.state);
1348
1349 let join_metric = self.metrics.join_metrics.join_time.clone();
1351 let _join_timer = join_metric.timer();
1352
1353 match self.handle_probe_right() {
1354 ControlFlow::Continue(()) => continue,
1355 ControlFlow::Break(poll) => {
1356 return self.metrics.join_metrics.baseline.record_poll(poll);
1357 }
1358 }
1359 }
1360
1361 NLJState::EmitRightUnmatched => {
1368 debug!("[NLJState] Entering: {:?}", self.state);
1369
1370 let join_metric = self.metrics.join_metrics.join_time.clone();
1372 let _join_timer = join_metric.timer();
1373
1374 match self.handle_emit_right_unmatched() {
1375 ControlFlow::Continue(()) => continue,
1376 ControlFlow::Break(poll) => {
1377 return self.metrics.join_metrics.baseline.record_poll(poll);
1378 }
1379 }
1380 }
1381
1382 NLJState::ProbeEnd => {
1390 debug!("[NLJState] Entering: {:?}", self.state);
1391
1392 let join_metric = self.metrics.join_metrics.join_time.clone();
1394 let _join_timer = join_metric.timer();
1395
1396 match self.handle_probe_end() {
1397 ControlFlow::Continue(()) => continue,
1398 ControlFlow::Break(poll) => {
1399 return self.metrics.join_metrics.baseline.record_poll(poll);
1400 }
1401 }
1402 }
1403
1404 NLJState::EmitLeftUnmatched => {
1420 debug!("[NLJState] Entering: {:?}", self.state);
1421
1422 let join_metric = self.metrics.join_metrics.join_time.clone();
1424 let _join_timer = join_metric.timer();
1425
1426 match self.handle_emit_left_unmatched() {
1427 ControlFlow::Continue(()) => continue,
1428 ControlFlow::Break(poll) => {
1429 return self.metrics.join_metrics.baseline.record_poll(poll);
1430 }
1431 }
1432 }
1433
1434 NLJState::EmitGlobalRightUnmatched => {
1440 debug!("[NLJState] Entering: {:?}", self.state);
1441
1442 let join_metric = self.metrics.join_metrics.join_time.clone();
1443 let _join_timer = join_metric.timer();
1444
1445 match self.handle_emit_global_right_unmatched(cx) {
1446 ControlFlow::Continue(()) => continue,
1447 ControlFlow::Break(poll) => {
1448 return self.metrics.join_metrics.baseline.record_poll(poll);
1449 }
1450 }
1451 }
1452
1453 NLJState::Done => {
1455 debug!("[NLJState] Entering: {:?}", self.state);
1456
1457 let join_metric = self.metrics.join_metrics.join_time.clone();
1459 let _join_timer = join_metric.timer();
1460 let poll = self.handle_done();
1464 return self.metrics.join_metrics.baseline.record_poll(poll);
1465 }
1466 }
1467 }
1468 }
1469}
1470
1471impl RecordBatchStream for NestedLoopJoinStream {
1472 fn schema(&self) -> SchemaRef {
1473 Arc::clone(&self.output_schema)
1474 }
1475}
1476
1477impl NestedLoopJoinStream {
1478 #[expect(clippy::too_many_arguments)]
1479 pub(crate) fn new(
1480 schema: Arc<Schema>,
1481 filter: Option<JoinFilter>,
1482 join_type: JoinType,
1483 right_data: SendableRecordBatchStream,
1484 left_data: OnceFut<JoinLeftData>,
1485 column_indices: Vec<ColumnIndex>,
1486 metrics: NestedLoopJoinMetrics,
1487 batch_size: usize,
1488 spill_state: SpillState,
1489 ) -> Self {
1490 Self {
1491 output_schema: Arc::clone(&schema),
1492 join_filter: filter,
1493 join_type,
1494 right_data: Some(right_data),
1495 column_indices,
1496 left_data,
1497 metrics,
1498 buffered_left_data: None,
1499 output_buffer: Box::new(BatchCoalescer::new(schema, batch_size)),
1500 batch_size,
1501 current_right_batch: None,
1502 current_right_batch_matched: None,
1503 state: NLJState::BufferingLeft,
1504 left_probe_idx: 0,
1505 left_emit_idx: 0,
1506 left_exhausted: false,
1507 left_buffered_in_one_pass: true,
1508 handled_empty_output: false,
1509 should_track_unmatched_right: need_produce_right_in_final(join_type),
1510 spill_state,
1511 is_unmatched_left_emitter: false,
1512 }
1513 }
1514
1515 fn is_memory_limited(&self) -> bool {
1517 matches!(self.spill_state, SpillState::Active(_))
1518 }
1519
1520 fn can_fallback_to_spill(&self, error: &datafusion_common::DataFusionError) -> bool {
1522 matches!(self.spill_state, SpillState::Pending { .. })
1523 && matches!(
1524 error.find_root(),
1525 datafusion_common::DataFusionError::ResourcesExhausted(_)
1526 )
1527 }
1528
1529 fn initiate_fallback(&mut self) -> Result<()> {
1535 let (left_plan, context, left_spill_data) =
1537 match std::mem::replace(&mut self.spill_state, SpillState::Disabled) {
1538 SpillState::Pending {
1539 left_plan,
1540 task_context,
1541 left_spill_data,
1542 } => (left_plan, task_context, left_spill_data),
1543 _ => {
1544 return internal_err!(
1545 "initiate_fallback called in non-Pending spill state"
1546 );
1547 }
1548 };
1549
1550 let left_spill_fut = left_spill_data.try_once(|| {
1554 let plan = Arc::clone(&left_plan);
1555 let ctx = Arc::clone(&context);
1556 let spill_metrics = self.metrics.spill_metrics.clone();
1557 Ok(async move {
1558 let mut stream = plan.execute(0, Arc::clone(&ctx))?;
1559 let schema = stream.schema();
1560 let left_spill_manager = SpillManager::new(
1561 ctx.runtime_env(),
1562 spill_metrics,
1563 Arc::clone(&schema),
1564 )
1565 .with_compression_type(ctx.session_config().spill_compression());
1566
1567 let result = left_spill_manager
1568 .spill_record_batch_stream_and_return_max_batch_memory(
1569 &mut stream,
1570 "NestedLoopJoin left spill",
1571 )
1572 .await?;
1573
1574 match result {
1575 Some((file, _max_batch_memory)) => Ok(LeftSpillData {
1576 spill_manager: left_spill_manager,
1577 spill_file: file,
1578 schema,
1579 }),
1580 None => {
1581 internal_err!("Left side produced no data to spill")
1582 }
1583 }
1584 })
1585 })?;
1586
1587 let reservation = MemoryConsumer::new("NestedLoopJoinLoad[fallback]".to_string())
1589 .with_can_spill(true)
1590 .register(context.memory_pool());
1591
1592 let global_right_bitmaps_reservation =
1596 MemoryConsumer::new("NestedLoopJoinGlobalRightBitmaps".to_string())
1597 .register(context.memory_pool());
1598
1599 let right_schema = self
1601 .right_data
1602 .as_ref()
1603 .expect("right_data must be present before fallback")
1604 .schema();
1605 let right_data = self
1606 .right_data
1607 .take()
1608 .expect("right_data must be present before fallback");
1609 let right_spill_manager = SpillManager::new(
1610 context.runtime_env(),
1611 self.metrics.spill_metrics.clone(),
1612 right_schema,
1613 )
1614 .with_compression_type(context.session_config().spill_compression());
1615
1616 self.spill_state = SpillState::Active(Box::new(SpillStateActive {
1617 left_spill_fut,
1618 left_stream: None,
1619 left_schema: None,
1620 reservation,
1621 pending_batches: Vec::new(),
1622 right_input: ReplayableStreamSource::new(
1623 right_data,
1624 right_spill_manager,
1625 "NestedLoopJoin right spill",
1626 ),
1627 global_right_bitmaps: Vec::new(),
1628 global_right_bitmaps_reservation,
1629 right_batch_index: 0,
1630 }));
1631
1632 self.state = NLJState::BufferingLeft;
1635
1636 Ok(())
1637 }
1638
1639 fn handle_buffering_left(
1647 &mut self,
1648 cx: &mut std::task::Context<'_>,
1649 ) -> ControlFlow<Poll<Option<Result<RecordBatch>>>> {
1650 if self.is_memory_limited() {
1651 self.handle_buffering_left_memory_limited(cx)
1652 } else {
1653 match self.left_data.get_shared(cx) {
1655 Poll::Ready(Ok(left_data)) => {
1656 self.buffered_left_data = Some(left_data);
1657 self.left_exhausted = true;
1658 self.state = NLJState::FetchingRight;
1659 ControlFlow::Continue(())
1660 }
1661 Poll::Ready(Err(e)) => {
1662 if self.can_fallback_to_spill(&e) {
1663 debug!(
1664 "NestedLoopJoin: OnceFut failed with OOM, \
1665 falling back to memory-limited mode"
1666 );
1667 match self.initiate_fallback() {
1668 Ok(()) => ControlFlow::Continue(()),
1669 Err(fallback_err) => {
1670 ControlFlow::Break(Poll::Ready(Some(Err(fallback_err))))
1671 }
1672 }
1673 } else {
1674 ControlFlow::Break(Poll::Ready(Some(Err(e))))
1675 }
1676 }
1677 Poll::Pending => ControlFlow::Break(Poll::Pending),
1678 }
1679 }
1680 }
1681
1682 fn handle_buffering_left_memory_limited(
1688 &mut self,
1689 cx: &mut std::task::Context<'_>,
1690 ) -> ControlFlow<Poll<Option<Result<RecordBatch>>>> {
1691 let SpillState::Active(active) = &mut self.spill_state else {
1692 unreachable!(
1693 "handle_buffering_left_memory_limited called without Active spill state"
1694 );
1695 };
1696
1697 if active.left_stream.is_none() {
1701 match active.left_spill_fut.get_shared(cx) {
1702 Poll::Ready(Ok(spill_data)) => {
1703 match spill_data
1704 .spill_manager
1705 .read_spill_as_stream(Arc::clone(&spill_data.spill_file), None)
1706 {
1707 Ok(stream) => {
1708 active.left_schema = Some(Arc::clone(&spill_data.schema));
1709 active.left_stream = Some(stream);
1710 }
1711 Err(e) => {
1712 return ControlFlow::Break(Poll::Ready(Some(Err(e))));
1713 }
1714 }
1715 }
1716 Poll::Ready(Err(e)) => {
1717 return ControlFlow::Break(Poll::Ready(Some(Err(e))));
1718 }
1719 Poll::Pending => {
1720 return ControlFlow::Break(Poll::Pending);
1721 }
1722 }
1723 }
1724
1725 let left_stream = active
1726 .left_stream
1727 .as_mut()
1728 .expect("left_stream must be set after spill future resolves");
1729
1730 loop {
1734 match left_stream.poll_next_unpin(cx) {
1735 Poll::Ready(Some(Ok(batch))) => {
1736 if batch.num_rows() == 0 {
1737 continue;
1738 }
1739 let batch_rows = batch.num_rows();
1740 let batch_size = batch.get_array_memory_size();
1741 let can_grow = active.reservation.try_grow(batch_size).is_ok();
1742
1743 if !can_grow && !active.pending_batches.is_empty() {
1744 active.pending_batches.push(batch);
1748 self.left_exhausted = false;
1749 self.left_buffered_in_one_pass = false;
1750 break;
1751 } else if !can_grow {
1752 active.reservation.grow(batch_size);
1755 }
1756
1757 self.metrics.join_metrics.build_mem_used.add(batch_size);
1758 self.metrics.join_metrics.build_input_batches.add(1);
1759 self.metrics.join_metrics.build_input_rows.add(batch_rows);
1760 active.pending_batches.push(batch);
1761 }
1762 Poll::Ready(Some(Err(e))) => {
1763 return ControlFlow::Break(Poll::Ready(Some(Err(e))));
1764 }
1765 Poll::Ready(None) => {
1766 self.left_exhausted = true;
1768 break;
1769 }
1770 Poll::Pending => {
1771 return ControlFlow::Break(Poll::Pending);
1772 }
1773 }
1774 }
1775
1776 if self.left_exhausted {
1779 active.left_stream = None;
1780 }
1781
1782 if active.pending_batches.is_empty() {
1783 self.left_exhausted = true;
1785 self.state = NLJState::Done;
1786 return ControlFlow::Continue(());
1787 }
1788
1789 let merged_batch = match concat_batches(
1790 active
1791 .left_schema
1792 .as_ref()
1793 .expect("left_schema must be set"),
1794 &active.pending_batches,
1795 ) {
1796 Ok(batch) => batch,
1797 Err(e) => {
1798 return ControlFlow::Break(Poll::Ready(Some(Err(e.into()))));
1799 }
1800 };
1801 active.pending_batches.clear();
1802
1803 let with_visited = need_produce_result_in_final(self.join_type);
1805 let n_rows = merged_batch.num_rows();
1806 let visited_left_side = if with_visited {
1807 let buffer_size = n_rows.div_ceil(8);
1808 active.reservation.grow(buffer_size);
1810 self.metrics.join_metrics.build_mem_used.add(buffer_size);
1811 let mut buffer = BooleanBufferBuilder::new(n_rows);
1812 buffer.append_n(n_rows, false);
1813 buffer
1814 } else {
1815 BooleanBufferBuilder::new(0)
1816 };
1817
1818 let dummy_reservation = active.reservation.new_empty();
1821
1822 let left_data = JoinLeftData::new(
1823 merged_batch,
1824 Mutex::new(visited_left_side),
1825 AtomicUsize::new(1),
1827 dummy_reservation,
1828 );
1829
1830 self.buffered_left_data = Some(Arc::new(left_data));
1831
1832 active.right_batch_index = 0;
1833 match active.right_input.open_pass() {
1834 Ok(stream) => {
1835 self.right_data = Some(stream);
1836 }
1837 Err(e) => {
1838 return ControlFlow::Break(Poll::Ready(Some(Err(e))));
1839 }
1840 }
1841
1842 self.state = NLJState::FetchingRight;
1843 ControlFlow::Continue(())
1844 }
1845
1846 fn handle_fetching_right(
1851 &mut self,
1852 cx: &mut std::task::Context<'_>,
1853 ) -> ControlFlow<Poll<Option<Result<RecordBatch>>>> {
1854 match self
1855 .right_data
1856 .as_mut()
1857 .expect("right_data must be present while fetching right")
1858 .poll_next_unpin(cx)
1859 {
1860 Poll::Ready(result) => match result {
1861 Some(Ok(right_batch)) => {
1862 let right_batch_rows = right_batch.num_rows();
1864 self.metrics.join_metrics.input_rows.add(right_batch_rows);
1865 self.metrics.join_metrics.input_batches.add(1);
1866
1867 if right_batch_rows == 0 {
1869 return ControlFlow::Continue(());
1870 }
1871
1872 self.current_right_batch = Some(right_batch);
1873
1874 if self.should_track_unmatched_right {
1876 let zeroed_buf = BooleanBuffer::new_unset(right_batch_rows);
1877 self.current_right_batch_matched =
1878 Some(BooleanArray::new(zeroed_buf, None));
1879 }
1880
1881 self.left_probe_idx = 0;
1882 self.state = NLJState::ProbeRight;
1883 ControlFlow::Continue(())
1884 }
1885 Some(Err(e)) => ControlFlow::Break(Poll::Ready(Some(Err(e)))),
1886 None => {
1887 self.state = NLJState::ProbeEnd;
1891 ControlFlow::Continue(())
1892 }
1893 },
1894 Poll::Pending => ControlFlow::Break(Poll::Pending),
1895 }
1896 }
1897
1898 fn handle_probe_right(&mut self) -> ControlFlow<Poll<Option<Result<RecordBatch>>>> {
1900 if let Some(poll) = self.maybe_flush_ready_batch() {
1902 return ControlFlow::Break(poll);
1903 }
1904
1905 match self.process_probe_batch() {
1907 Ok(true) => ControlFlow::Continue(()),
1911 Ok(false) => {
1915 self.left_probe_idx = 0;
1917
1918 if let (Ok(left_data), Some(right_batch)) =
1921 (self.get_left_data(), self.current_right_batch.as_ref())
1922 {
1923 let left_rows = left_data.batch().num_rows();
1924 let right_rows = right_batch.num_rows();
1925 self.metrics.selectivity.add_total(left_rows * right_rows);
1926 }
1927
1928 if self.should_track_unmatched_right {
1929 debug_assert!(
1930 self.current_right_batch_matched.is_some(),
1931 "If it's required to track matched rows in the right input, the right bitmap must be present"
1932 );
1933 self.state = NLJState::EmitRightUnmatched;
1934 } else {
1935 self.current_right_batch = None;
1936 self.state = NLJState::FetchingRight;
1937 }
1938 ControlFlow::Continue(())
1939 }
1940 Err(e) => ControlFlow::Break(Poll::Ready(Some(Err(e)))),
1941 }
1942 }
1943
1944 fn handle_emit_right_unmatched(
1951 &mut self,
1952 ) -> ControlFlow<Poll<Option<Result<RecordBatch>>>> {
1953 if self.is_memory_limited() {
1955 debug_assert!(
1956 self.current_right_batch_matched.is_some(),
1957 "right bitmap must be present"
1958 );
1959 let bitmap = std::mem::take(&mut self.current_right_batch_matched)
1960 .expect("right bitmap should be available");
1961 let (values, _nulls) = bitmap.into_parts();
1962
1963 if let SpillState::Active(ref mut active) = self.spill_state {
1964 let idx = active.right_batch_index;
1965 active.merge_current_right_bitmap(idx, values);
1966 active.right_batch_index += 1;
1967 }
1968
1969 self.current_right_batch = None;
1970 self.state = NLJState::FetchingRight;
1971 return ControlFlow::Continue(());
1972 }
1973
1974 if let Some(poll) = self.maybe_flush_ready_batch() {
1977 return ControlFlow::Break(poll);
1978 }
1979
1980 debug_assert!(
1981 self.current_right_batch_matched.is_some()
1982 && self.current_right_batch.is_some(),
1983 "This state is yielding output for unmatched rows in the current right batch, so both the right batch and the bitmap must be present"
1984 );
1985 match self.process_right_unmatched() {
1986 Ok(Some(batch)) => match self.output_buffer.push_batch(batch) {
1987 Ok(()) => {
1988 debug_assert!(self.current_right_batch.is_none());
1989 self.state = NLJState::FetchingRight;
1990 ControlFlow::Continue(())
1991 }
1992 Err(e) => ControlFlow::Break(Poll::Ready(Some(arrow_err!(e)))),
1993 },
1994 Ok(None) => {
1995 debug_assert!(self.current_right_batch.is_none());
1996 self.state = NLJState::FetchingRight;
1997 ControlFlow::Continue(())
1998 }
1999 Err(e) => ControlFlow::Break(Poll::Ready(Some(Err(e)))),
2000 }
2001 }
2002
2003 fn handle_probe_end(&mut self) -> ControlFlow<Poll<Option<Result<RecordBatch>>>> {
2019 let is_emitter = match self.get_left_data() {
2023 Ok(left_data) => left_data.report_probe_completed(),
2024 Err(e) => return ControlFlow::Break(Poll::Ready(Some(Err(e)))),
2025 };
2026 self.is_unmatched_left_emitter = is_emitter;
2027 self.state = NLJState::EmitLeftUnmatched;
2028 ControlFlow::Continue(())
2029 }
2030
2031 fn handle_emit_left_unmatched(
2037 &mut self,
2038 ) -> ControlFlow<Poll<Option<Result<RecordBatch>>>> {
2039 if let Some(poll) = self.maybe_flush_ready_batch() {
2041 return ControlFlow::Break(poll);
2042 }
2043
2044 match self.process_left_unmatched() {
2046 Ok(true) => ControlFlow::Continue(()),
2049 Ok(false) => match self.output_buffer.finish_buffered_batch() {
2051 Ok(()) => {
2052 if let Some(poll) = self.maybe_flush_ready_batch() {
2057 return ControlFlow::Break(poll);
2058 }
2059
2060 if !self.left_exhausted && self.is_memory_limited() {
2061 if let SpillState::Active(ref active) = self.spill_state {
2064 active.reservation.resize(0);
2065 }
2066 self.buffered_left_data = None;
2067 self.left_probe_idx = 0;
2068 self.left_emit_idx = 0;
2069 self.state = NLJState::BufferingLeft;
2074 } else if self.is_memory_limited()
2075 && self.should_track_unmatched_right
2076 {
2077 self.right_data = None;
2084 self.state = NLJState::EmitGlobalRightUnmatched;
2085 } else {
2086 self.state = NLJState::Done;
2087 }
2088 ControlFlow::Continue(())
2089 }
2090 Err(e) => ControlFlow::Break(Poll::Ready(Some(arrow_err!(e)))),
2091 },
2092 Err(e) => ControlFlow::Break(Poll::Ready(Some(Err(e)))),
2093 }
2094 }
2095
2096 fn handle_emit_global_right_unmatched(
2101 &mut self,
2102 cx: &mut std::task::Context<'_>,
2103 ) -> ControlFlow<Poll<Option<Result<RecordBatch>>>> {
2104 if let Some(poll) = self.maybe_flush_ready_batch() {
2106 return ControlFlow::Break(poll);
2107 }
2108
2109 if self.right_data.is_none() {
2111 let SpillState::Active(ref mut active) = self.spill_state else {
2112 unreachable!("EmitGlobalRightUnmatched without Active spill state");
2113 };
2114 active.right_batch_index = 0;
2115 match active.right_input.open_pass() {
2116 Ok(stream) => {
2117 self.right_data = Some(stream);
2118 }
2119 Err(e) => {
2120 return ControlFlow::Break(Poll::Ready(Some(Err(e))));
2121 }
2122 }
2123 }
2124
2125 match self
2127 .right_data
2128 .as_mut()
2129 .expect("right_data must be present")
2130 .poll_next_unpin(cx)
2131 {
2132 Poll::Ready(Some(Ok(right_batch))) => {
2133 if right_batch.num_rows() == 0 {
2134 return ControlFlow::Continue(());
2135 }
2136
2137 let SpillState::Active(ref mut active) = self.spill_state else {
2138 unreachable!();
2139 };
2140 let idx = active.right_batch_index;
2141 active.right_batch_index += 1;
2142
2143 let bitmap = if idx < active.global_right_bitmaps.len() {
2145 BooleanArray::new(active.global_right_bitmaps[idx].clone(), None)
2146 } else {
2147 BooleanArray::new(
2149 BooleanBuffer::new_unset(right_batch.num_rows()),
2150 None,
2151 )
2152 };
2153
2154 let left_schema = Arc::clone(
2155 active
2156 .left_schema
2157 .as_ref()
2158 .expect("left_schema must be set"),
2159 );
2160
2161 match build_unmatched_batch(
2162 &self.output_schema,
2163 &right_batch,
2164 bitmap,
2165 &left_schema,
2166 &self.column_indices,
2167 self.join_type,
2168 JoinSide::Right,
2169 ) {
2170 Ok(Some(batch)) => match self.output_buffer.push_batch(batch) {
2171 Ok(()) => ControlFlow::Continue(()),
2172 Err(e) => ControlFlow::Break(Poll::Ready(Some(arrow_err!(e)))),
2173 },
2174 Ok(None) => ControlFlow::Continue(()),
2175 Err(e) => ControlFlow::Break(Poll::Ready(Some(Err(e)))),
2176 }
2177 }
2178 Poll::Ready(Some(Err(e))) => ControlFlow::Break(Poll::Ready(Some(Err(e)))),
2179 Poll::Ready(None) => {
2180 match self.output_buffer.finish_buffered_batch() {
2182 Ok(()) => {
2183 self.state = NLJState::Done;
2184 ControlFlow::Continue(())
2185 }
2186 Err(e) => ControlFlow::Break(Poll::Ready(Some(arrow_err!(e)))),
2187 }
2188 }
2189 Poll::Pending => ControlFlow::Break(Poll::Pending),
2190 }
2191 }
2192
2193 fn handle_done(&mut self) -> Poll<Option<Result<RecordBatch>>> {
2195 if let Some(poll) = self.maybe_flush_ready_batch() {
2197 return poll;
2198 }
2199
2200 if !self.handled_empty_output {
2206 let zero_count = Count::new();
2207 if *self.metrics.join_metrics.baseline.output_rows() == zero_count {
2208 let empty_batch = RecordBatch::new_empty(Arc::clone(&self.output_schema));
2209 self.handled_empty_output = true;
2210 return Poll::Ready(Some(Ok(empty_batch)));
2211 }
2212 }
2213
2214 Poll::Ready(None)
2215 }
2216
2217 fn process_probe_batch(&mut self) -> Result<bool> {
2224 let left_data = Arc::clone(self.get_left_data()?);
2225 let right_batch = self
2226 .current_right_batch
2227 .as_ref()
2228 .ok_or_else(|| internal_datafusion_err!("Right batch should be available"))?
2229 .clone();
2230
2231 if self.left_probe_idx >= left_data.batch().num_rows() {
2233 return Ok(false);
2234 }
2235
2236 debug_assert_ne!(
2249 right_batch.num_rows(),
2250 0,
2251 "When fetching the right batch, empty batches will be skipped"
2252 );
2253
2254 let l_row_cnt_ratio = self.batch_size / right_batch.num_rows();
2255 if l_row_cnt_ratio > 10 {
2256 let l_row_count = std::cmp::min(
2260 l_row_cnt_ratio,
2261 left_data.batch().num_rows() - self.left_probe_idx,
2262 );
2263
2264 debug_assert!(
2265 l_row_count != 0,
2266 "This function should only be entered when there are remaining left rows to process"
2267 );
2268 let joined_batch = self.process_left_range_join(
2269 &left_data,
2270 &right_batch,
2271 self.left_probe_idx,
2272 l_row_count,
2273 )?;
2274
2275 if let Some(batch) = joined_batch {
2276 self.output_buffer.push_batch(batch)?;
2277 }
2278
2279 self.left_probe_idx += l_row_count;
2280
2281 return Ok(true);
2282 }
2283
2284 let l_idx = self.left_probe_idx;
2285 let joined_batch =
2286 self.process_single_left_row_join(&left_data, &right_batch, l_idx)?;
2287
2288 if let Some(batch) = joined_batch {
2289 self.output_buffer.push_batch(batch)?;
2290 }
2291
2292 self.left_probe_idx += 1;
2296
2297 Ok(true)
2299 }
2300
2301 fn process_left_range_join(
2307 &mut self,
2308 left_data: &JoinLeftData,
2309 right_batch: &RecordBatch,
2310 l_start_index: usize,
2311 l_row_count: usize,
2312 ) -> Result<Option<RecordBatch>> {
2313 let right_rows = right_batch.num_rows();
2319 let total_rows = l_row_count * right_rows;
2320
2321 let left_indices: UInt32Array =
2323 UInt32Array::from_iter_values((0..l_row_count).flat_map(|i| {
2324 std::iter::repeat_n((l_start_index + i) as u32, right_rows)
2325 }));
2326 let right_indices: UInt32Array = UInt32Array::from_iter_values(
2327 (0..l_row_count).flat_map(|_| 0..right_rows as u32),
2328 );
2329
2330 debug_assert!(
2331 left_indices.len() == right_indices.len()
2332 && right_indices.len() == total_rows,
2333 "The length or cartesian product should be (left_size * right_size)",
2334 );
2335
2336 let bitmap_combined = if let Some(filter) = &self.join_filter {
2339 let intermediate_batch = if filter.schema.fields().is_empty() {
2341 create_record_batch_with_empty_schema(
2343 Arc::new((*filter.schema).clone()),
2344 total_rows,
2345 )?
2346 } else {
2347 let mut filter_columns: Vec<Arc<dyn Array>> =
2348 Vec::with_capacity(filter.column_indices().len());
2349 for column_index in filter.column_indices() {
2350 let array = if column_index.side == JoinSide::Left {
2351 let col = left_data.batch().column(column_index.index);
2352 take(col.as_ref(), &left_indices, None)?
2353 } else {
2354 let col = right_batch.column(column_index.index);
2355 take(col.as_ref(), &right_indices, None)?
2356 };
2357 filter_columns.push(array);
2358 }
2359
2360 RecordBatch::try_new(Arc::new((*filter.schema).clone()), filter_columns)?
2361 };
2362
2363 let filter_result = filter
2364 .expression()
2365 .evaluate(&intermediate_batch)?
2366 .into_array(intermediate_batch.num_rows())?;
2367 let filter_arr = as_boolean_array(&filter_result)?;
2368
2369 boolean_mask_from_filter(filter_arr)
2371 } else {
2372 BooleanArray::from(vec![true; total_rows])
2374 };
2375
2376 let mut left_bitmap = if need_produce_result_in_final(self.join_type) {
2381 Some(left_data.bitmap().lock())
2382 } else {
2383 None
2384 };
2385
2386 let mut local_right_bitmap = if self.should_track_unmatched_right {
2390 let mut current_right_batch_bitmap = BooleanBufferBuilder::new(right_rows);
2391 current_right_batch_bitmap.append_n(right_rows, false);
2393 Some(current_right_batch_bitmap)
2394 } else {
2395 None
2396 };
2397
2398 for (i, is_matched) in bitmap_combined.iter().enumerate() {
2400 let is_matched = is_matched.ok_or_else(|| {
2401 internal_datafusion_err!("Must be Some after the previous combining step")
2402 })?;
2403
2404 let l_index = l_start_index + i / right_rows;
2405 let r_index = i % right_rows;
2406
2407 if let Some(bitmap) = left_bitmap.as_mut()
2408 && is_matched
2409 {
2410 bitmap.set_bit(l_index, true);
2412 }
2413
2414 if let Some(bitmap) = local_right_bitmap.as_mut()
2415 && is_matched
2416 {
2417 bitmap.set_bit(r_index, true);
2418 }
2419 }
2420
2421 if self.should_track_unmatched_right {
2423 let global_right_bitmap =
2425 std::mem::take(&mut self.current_right_batch_matched).ok_or_else(
2426 || internal_datafusion_err!("right batch's bitmap should be present"),
2427 )?;
2428 let (buf, nulls) = global_right_bitmap.into_parts();
2429 debug_assert!(nulls.is_none());
2430
2431 let current_right_bitmap = local_right_bitmap
2432 .ok_or_else(|| {
2433 internal_datafusion_err!(
2434 "Should be Some if the current join type requires right bitmap"
2435 )
2436 })?
2437 .finish();
2438 let updated_global_right_bitmap = buf.bitor(¤t_right_bitmap);
2439
2440 self.current_right_batch_matched =
2441 Some(BooleanArray::new(updated_global_right_bitmap, None));
2442 }
2443
2444 if matches!(
2446 self.join_type,
2447 JoinType::LeftAnti
2448 | JoinType::LeftSemi
2449 | JoinType::LeftMark
2450 | JoinType::RightAnti
2451 | JoinType::RightMark
2452 | JoinType::RightSemi
2453 ) {
2454 return Ok(None);
2455 }
2456
2457 if self.output_schema.fields().is_empty() {
2460 let row_count = bitmap_combined.true_count();
2462 return Ok(Some(create_record_batch_with_empty_schema(
2463 Arc::clone(&self.output_schema),
2464 row_count,
2465 )?));
2466 }
2467
2468 let mut out_columns: Vec<Arc<dyn Array>> =
2469 Vec::with_capacity(self.output_schema.fields().len());
2470 for column_index in &self.column_indices {
2471 let array = if column_index.side == JoinSide::Left {
2472 let col = left_data.batch().column(column_index.index);
2473 take(col.as_ref(), &left_indices, None)?
2474 } else {
2475 let col = right_batch.column(column_index.index);
2476 take(col.as_ref(), &right_indices, None)?
2477 };
2478 out_columns.push(array);
2479 }
2480 let pre_filtered =
2481 RecordBatch::try_new(Arc::clone(&self.output_schema), out_columns)?;
2482 let filtered = filter_record_batch(&pre_filtered, &bitmap_combined)?;
2483 Ok(Some(filtered))
2484 }
2485
2486 fn process_single_left_row_join(
2492 &mut self,
2493 left_data: &JoinLeftData,
2494 right_batch: &RecordBatch,
2495 l_index: usize,
2496 ) -> Result<Option<RecordBatch>> {
2497 let right_row_count = right_batch.num_rows();
2498 if right_row_count == 0 {
2499 return Ok(None);
2500 }
2501
2502 let cur_right_bitmap = if let Some(filter) = &self.join_filter {
2503 apply_filter_to_row_join_batch(
2504 left_data.batch(),
2505 l_index,
2506 right_batch,
2507 filter,
2508 )?
2509 } else {
2510 BooleanArray::from(vec![true; right_row_count])
2511 };
2512
2513 self.update_matched_bitmap(l_index, &cur_right_bitmap)?;
2514
2515 if matches!(
2518 self.join_type,
2519 JoinType::LeftAnti
2520 | JoinType::LeftSemi
2521 | JoinType::LeftMark
2522 | JoinType::RightAnti
2523 | JoinType::RightMark
2524 | JoinType::RightSemi
2525 ) {
2526 return Ok(None);
2527 }
2528
2529 if !cur_right_bitmap.has_true() {
2530 Ok(None)
2532 } else {
2533 let join_batch = build_row_join_batch(
2535 &self.output_schema,
2536 left_data.batch(),
2537 l_index,
2538 right_batch,
2539 Some(cur_right_bitmap),
2540 &self.column_indices,
2541 JoinSide::Left,
2542 )?;
2543 Ok(join_batch)
2544 }
2545 }
2546
2547 fn process_left_unmatched(&mut self) -> Result<bool> {
2551 let left_data = self.get_left_data()?;
2552 let left_batch = left_data.batch();
2553
2554 let join_type_no_produce_left = !need_produce_result_in_final(self.join_type);
2560 let finished = self.left_emit_idx >= left_batch.num_rows();
2562
2563 if join_type_no_produce_left || !self.is_unmatched_left_emitter || finished {
2568 return Ok(false);
2569 }
2570
2571 let start_idx = self.left_emit_idx;
2576 let end_idx = std::cmp::min(start_idx + self.batch_size, left_batch.num_rows());
2577
2578 if let Some(batch) =
2579 self.process_left_unmatched_range(left_data, start_idx, end_idx)?
2580 {
2581 self.output_buffer.push_batch(batch)?;
2582 }
2583
2584 self.left_emit_idx = end_idx;
2586
2587 Ok(true)
2589 }
2590
2591 fn process_left_unmatched_range(
2604 &self,
2605 left_data: &JoinLeftData,
2606 start_idx: usize,
2607 end_idx: usize,
2608 ) -> Result<Option<RecordBatch>> {
2609 if start_idx == end_idx {
2610 return Ok(None);
2611 }
2612
2613 let left_batch = left_data.batch();
2616 let left_batch_sliced = left_batch.slice(start_idx, end_idx - start_idx);
2617
2618 let mut bitmap_sliced = BooleanBufferBuilder::new(end_idx - start_idx);
2620 bitmap_sliced.append_n(end_idx - start_idx, false);
2621 let bitmap = left_data.bitmap().lock();
2622 for i in start_idx..end_idx {
2623 assert!(
2624 i - start_idx < bitmap_sliced.capacity(),
2625 "DBG: {start_idx}, {end_idx}"
2626 );
2627 bitmap_sliced.set_bit(i - start_idx, bitmap.get_bit(i));
2628 }
2629 let bitmap_sliced = BooleanArray::new(bitmap_sliced.finish(), None);
2630
2631 let right_schema = self
2632 .right_data
2633 .as_ref()
2634 .expect("right_data must be present when building unmatched batch")
2635 .schema();
2636 build_unmatched_batch(
2637 &self.output_schema,
2638 &left_batch_sliced,
2639 bitmap_sliced,
2640 &right_schema,
2641 &self.column_indices,
2642 self.join_type,
2643 JoinSide::Left,
2644 )
2645 }
2646
2647 fn process_right_unmatched(&mut self) -> Result<Option<RecordBatch>> {
2650 let right_batch_bitmap: BooleanArray =
2652 std::mem::take(&mut self.current_right_batch_matched).ok_or_else(|| {
2653 internal_datafusion_err!("right bitmap should be available")
2654 })?;
2655
2656 let right_batch = self.current_right_batch.take();
2657 let cur_right_batch = unwrap_or_internal_err!(right_batch);
2658
2659 let left_data = self.get_left_data()?;
2660 let left_schema = left_data.batch().schema();
2661
2662 let res = build_unmatched_batch(
2663 &self.output_schema,
2664 &cur_right_batch,
2665 right_batch_bitmap,
2666 &left_schema,
2667 &self.column_indices,
2668 self.join_type,
2669 JoinSide::Right,
2670 );
2671
2672 self.current_right_batch_matched = None;
2674
2675 res
2676 }
2677
2678 fn get_left_data(&self) -> Result<&Arc<JoinLeftData>> {
2682 self.buffered_left_data
2683 .as_ref()
2684 .ok_or_else(|| internal_datafusion_err!("LeftData should be available"))
2685 }
2686
2687 fn maybe_flush_ready_batch(&mut self) -> Option<Poll<Option<Result<RecordBatch>>>> {
2690 if self.output_buffer.has_completed_batch()
2691 && let Some(batch) = self.output_buffer.next_completed_batch()
2692 {
2693 let output_rows = batch.num_rows();
2695 self.metrics.selectivity.add_part(output_rows);
2696
2697 return Some(Poll::Ready(Some(Ok(batch))));
2698 }
2699
2700 None
2701 }
2702
2703 fn update_matched_bitmap(
2719 &mut self,
2720 l_index: usize,
2721 r_matched_bitmap: &BooleanArray,
2722 ) -> Result<()> {
2723 let left_data = self.get_left_data()?;
2724
2725 if need_produce_result_in_final(self.join_type) && r_matched_bitmap.has_true() {
2727 let mut bitmap = left_data.bitmap().lock();
2728 bitmap.set_bit(l_index, true);
2729 }
2730
2731 if self.should_track_unmatched_right {
2733 debug_assert!(self.current_right_batch_matched.is_some());
2734 let right_bitmap = std::mem::take(&mut self.current_right_batch_matched)
2736 .ok_or_else(|| {
2737 internal_datafusion_err!("right batch's bitmap should be present")
2738 })?;
2739 let (buf, nulls) = right_bitmap.into_parts();
2740 debug_assert!(nulls.is_none());
2741 let updated_right_bitmap = buf.bitor(r_matched_bitmap.values());
2742
2743 self.current_right_batch_matched =
2744 Some(BooleanArray::new(updated_right_bitmap, None));
2745 }
2746
2747 Ok(())
2748 }
2749}
2750
2751fn apply_filter_to_row_join_batch(
2757 left_batch: &RecordBatch,
2758 l_index: usize,
2759 right_batch: &RecordBatch,
2760 filter: &JoinFilter,
2761) -> Result<BooleanArray> {
2762 debug_assert!(left_batch.num_rows() != 0 && right_batch.num_rows() != 0);
2763
2764 let intermediate_batch = if filter.schema.fields().is_empty() {
2765 create_record_batch_with_empty_schema(
2768 Arc::new((*filter.schema).clone()),
2769 right_batch.num_rows(),
2770 )?
2771 } else {
2772 build_row_join_batch(
2773 &filter.schema,
2774 left_batch,
2775 l_index,
2776 right_batch,
2777 None,
2778 &filter.column_indices,
2779 JoinSide::Left,
2780 )?
2781 .ok_or_else(|| internal_datafusion_err!("This function assume input batch is not empty, so the intermediate batch can't be empty too"))?
2782 };
2783
2784 let filter_result = filter
2785 .expression()
2786 .evaluate(&intermediate_batch)?
2787 .into_array(intermediate_batch.num_rows())?;
2788 let filter_arr = as_boolean_array(&filter_result)?;
2789
2790 let bitmap_combined = boolean_mask_from_filter(filter_arr);
2792
2793 Ok(bitmap_combined)
2794}
2795
2796#[inline]
2802fn boolean_mask_from_filter(filter_arr: &BooleanArray) -> BooleanArray {
2803 let (values, nulls) = filter_arr.clone().into_parts();
2804 match nulls {
2805 Some(nulls) => BooleanArray::new(nulls.inner() & &values, None),
2806 None => BooleanArray::new(values, None),
2807 }
2808}
2809
2810fn build_row_join_batch(
2858 output_schema: &Schema,
2859 build_side_batch: &RecordBatch,
2860 build_side_index: usize,
2861 probe_side_batch: &RecordBatch,
2862 probe_side_filter: Option<BooleanArray>,
2863 col_indices: &[ColumnIndex],
2865 build_side: JoinSide,
2868) -> Result<Option<RecordBatch>> {
2869 debug_assert!(build_side != JoinSide::None);
2870
2871 let filtered_probe_batch = if let Some(filter) = probe_side_filter {
2874 &filter_record_batch(probe_side_batch, &filter)?
2875 } else {
2876 probe_side_batch
2877 };
2878
2879 if filtered_probe_batch.num_rows() == 0 {
2880 return Ok(None);
2881 }
2882
2883 if output_schema.fields.is_empty() {
2891 return Ok(Some(create_record_batch_with_empty_schema(
2892 Arc::new(output_schema.clone()),
2893 filtered_probe_batch.num_rows(),
2894 )?));
2895 }
2896
2897 let mut columns: Vec<Arc<dyn Array>> =
2898 Vec::with_capacity(output_schema.fields().len());
2899
2900 for column_index in col_indices {
2901 let array = if column_index.side == build_side {
2902 let original_left_array = build_side_batch.column(column_index.index);
2905
2906 match original_left_array.data_type() {
2912 DataType::List(field) | DataType::LargeList(field)
2913 if field.data_type() == &DataType::Utf8View =>
2914 {
2915 let indices_iter = std::iter::repeat_n(
2916 build_side_index as u64,
2917 filtered_probe_batch.num_rows(),
2918 );
2919 let indices_array = UInt64Array::from_iter_values(indices_iter);
2920 take(original_left_array.as_ref(), &indices_array, None)?
2921 }
2922 _ => {
2923 let scalar_value = ScalarValue::try_from_array(
2924 original_left_array.as_ref(),
2925 build_side_index,
2926 )?;
2927 scalar_value.to_array_of_size(filtered_probe_batch.num_rows())?
2928 }
2929 }
2930 } else {
2931 Arc::clone(filtered_probe_batch.column(column_index.index))
2933 };
2934
2935 columns.push(array);
2936 }
2937
2938 Ok(Some(RecordBatch::try_new(
2939 Arc::new(output_schema.clone()),
2940 columns,
2941 )?))
2942}
2943
2944fn build_unmatched_batch_empty_schema(
2951 output_schema: &SchemaRef,
2952 batch_bitmap: &BooleanArray,
2953 join_type: JoinType,
2955) -> Result<Option<RecordBatch>> {
2956 let result_size = match join_type {
2957 JoinType::Left
2958 | JoinType::Right
2959 | JoinType::Full
2960 | JoinType::LeftAnti
2961 | JoinType::RightAnti => batch_bitmap.false_count(),
2962 JoinType::LeftSemi | JoinType::RightSemi => batch_bitmap.true_count(),
2963 JoinType::LeftMark | JoinType::RightMark => batch_bitmap.len(),
2964 _ => unreachable!(),
2965 };
2966
2967 if output_schema.fields().is_empty() {
2968 Ok(Some(create_record_batch_with_empty_schema(
2969 Arc::clone(output_schema),
2970 result_size,
2971 )?))
2972 } else {
2973 Ok(None)
2974 }
2975}
2976
2977fn create_record_batch_with_empty_schema(
2981 schema: SchemaRef,
2982 row_count: usize,
2983) -> Result<RecordBatch> {
2984 let options = RecordBatchOptions::new()
2985 .with_match_field_names(true)
2986 .with_row_count(Some(row_count));
2987
2988 RecordBatch::try_new_with_options(schema, vec![], &options).map_err(|e| {
2989 internal_datafusion_err!("Failed to create empty record batch: {}", e)
2990 })
2991}
2992
2993fn build_unmatched_batch(
3029 output_schema: &SchemaRef,
3030 batch: &RecordBatch,
3031 batch_bitmap: BooleanArray,
3032 another_side_schema: &SchemaRef,
3034 col_indices: &[ColumnIndex],
3035 join_type: JoinType,
3036 batch_side: JoinSide,
3037) -> Result<Option<RecordBatch>> {
3038 debug_assert_ne!(join_type, JoinType::Inner);
3040 debug_assert_ne!(batch_side, JoinSide::None);
3041
3042 if let Some(batch) =
3044 build_unmatched_batch_empty_schema(output_schema, &batch_bitmap, join_type)?
3045 {
3046 return Ok(Some(batch));
3047 }
3048
3049 match join_type {
3050 JoinType::Full | JoinType::Right | JoinType::Left => {
3051 if join_type == JoinType::Right {
3052 debug_assert_eq!(batch_side, JoinSide::Right);
3053 }
3054 if join_type == JoinType::Left {
3055 debug_assert_eq!(batch_side, JoinSide::Left);
3056 }
3057
3058 let flipped_bitmap = not(&batch_bitmap)?;
3061
3062 let left_null_columns: Vec<Arc<dyn Array>> = another_side_schema
3064 .fields()
3065 .iter()
3066 .map(|field| new_null_array(field.data_type(), 1))
3067 .collect();
3068
3069 let nullable_left_schema = Arc::new(Schema::new(
3073 another_side_schema
3074 .fields()
3075 .iter()
3076 .map(|field| (**field).clone().with_nullable(true))
3077 .collect::<Vec<_>>(),
3078 ));
3079 let left_null_batch = if nullable_left_schema.fields.is_empty() {
3080 create_record_batch_with_empty_schema(nullable_left_schema, 0)?
3083 } else {
3084 RecordBatch::try_new(nullable_left_schema, left_null_columns)?
3085 };
3086
3087 debug_assert_ne!(batch_side, JoinSide::None);
3088 let opposite_side = batch_side.negate();
3089
3090 build_row_join_batch(
3091 output_schema,
3092 &left_null_batch,
3093 0,
3094 batch,
3095 Some(flipped_bitmap),
3096 col_indices,
3097 opposite_side,
3098 )
3099 }
3100 JoinType::RightSemi
3101 | JoinType::RightAnti
3102 | JoinType::LeftSemi
3103 | JoinType::LeftAnti => {
3104 if matches!(join_type, JoinType::RightSemi | JoinType::RightAnti) {
3105 debug_assert_eq!(batch_side, JoinSide::Right);
3106 }
3107 if matches!(join_type, JoinType::LeftSemi | JoinType::LeftAnti) {
3108 debug_assert_eq!(batch_side, JoinSide::Left);
3109 }
3110
3111 let bitmap = if matches!(join_type, JoinType::LeftSemi | JoinType::RightSemi)
3112 {
3113 batch_bitmap.clone()
3114 } else {
3115 not(&batch_bitmap)?
3116 };
3117
3118 if !bitmap.has_true() {
3119 return Ok(None);
3120 }
3121
3122 let mut columns: Vec<Arc<dyn Array>> =
3123 Vec::with_capacity(output_schema.fields().len());
3124
3125 for column_index in col_indices {
3126 debug_assert!(column_index.side == batch_side);
3127
3128 let col = batch.column(column_index.index);
3129 let filtered_col = filter(col, &bitmap)?;
3130
3131 columns.push(filtered_col);
3132 }
3133
3134 Ok(Some(RecordBatch::try_new(
3135 Arc::clone(output_schema),
3136 columns,
3137 )?))
3138 }
3139 JoinType::RightMark | JoinType::LeftMark => {
3140 if join_type == JoinType::RightMark {
3141 debug_assert_eq!(batch_side, JoinSide::Right);
3142 }
3143 if join_type == JoinType::LeftMark {
3144 debug_assert_eq!(batch_side, JoinSide::Left);
3145 }
3146
3147 let mut columns: Vec<Arc<dyn Array>> =
3148 Vec::with_capacity(output_schema.fields().len());
3149
3150 let mut right_batch_bitmap_opt = Some(batch_bitmap);
3152
3153 for column_index in col_indices {
3154 if column_index.side == batch_side {
3155 let col = batch.column(column_index.index);
3156
3157 columns.push(Arc::clone(col));
3158 } else if column_index.side == JoinSide::None {
3159 let right_batch_bitmap = std::mem::take(&mut right_batch_bitmap_opt);
3160 match right_batch_bitmap {
3161 Some(right_batch_bitmap) => {
3162 columns.push(Arc::new(right_batch_bitmap))
3163 }
3164 None => unreachable!("Should only be one mark column"),
3165 }
3166 } else {
3167 return internal_err!(
3168 "Not possible to have this join side for RightMark join"
3169 );
3170 }
3171 }
3172
3173 Ok(Some(RecordBatch::try_new(
3174 Arc::clone(output_schema),
3175 columns,
3176 )?))
3177 }
3178 _ => internal_err!(
3179 "If batch is at right side, this function must be handling Full/Right/RightSemi/RightAnti/RightMark joins"
3180 ),
3181 }
3182}
3183
3184#[cfg(test)]
3185pub(crate) mod tests {
3186 use super::*;
3187 use crate::statistics::{StatisticsArgs, StatisticsContext};
3188 use crate::test::{TestMemoryExec, assert_join_metrics};
3189 use crate::{
3190 common, expressions::Column, repartition::RepartitionExec, test::build_table_i32,
3191 };
3192
3193 use arrow::compute::SortOptions;
3194 use arrow::datatypes::{DataType, Field};
3195 use datafusion_common::assert_contains;
3196 use datafusion_common::test_util::batches_to_sort_string;
3197 use datafusion_execution::runtime_env::RuntimeEnvBuilder;
3198 use datafusion_expr::Operator;
3199 use datafusion_physical_expr::expressions::{BinaryExpr, Literal};
3200 use datafusion_physical_expr::{Partitioning, PhysicalExpr};
3201 use datafusion_physical_expr_common::sort_expr::{LexOrdering, PhysicalSortExpr};
3202
3203 use insta::allow_duplicates;
3204 use insta::assert_snapshot;
3205 use rstest::rstest;
3206
3207 fn build_table(
3208 a: (&str, &Vec<i32>),
3209 b: (&str, &Vec<i32>),
3210 c: (&str, &Vec<i32>),
3211 batch_size: Option<usize>,
3212 sorted_column_names: Vec<&str>,
3213 ) -> Arc<dyn ExecutionPlan> {
3214 let batch = build_table_i32(a, b, c);
3215 let schema = batch.schema();
3216
3217 let batches = if let Some(batch_size) = batch_size {
3218 let num_batches = batch.num_rows().div_ceil(batch_size);
3219 (0..num_batches)
3220 .map(|i| {
3221 let start = i * batch_size;
3222 let remaining_rows = batch.num_rows() - start;
3223 batch.slice(start, batch_size.min(remaining_rows))
3224 })
3225 .collect::<Vec<_>>()
3226 } else {
3227 vec![batch]
3228 };
3229
3230 let mut sort_info = vec![];
3231 for name in sorted_column_names {
3232 let index = schema.index_of(name).unwrap();
3233 let sort_expr = PhysicalSortExpr::new(
3234 Arc::new(Column::new(name, index)),
3235 SortOptions::new(false, false),
3236 );
3237 sort_info.push(sort_expr);
3238 }
3239 let mut source = TestMemoryExec::try_new(&[batches], schema, None).unwrap();
3240 if let Some(ordering) = LexOrdering::new(sort_info) {
3241 source = source.try_with_sort_information(vec![ordering]).unwrap();
3242 }
3243
3244 let source = Arc::new(source);
3245 Arc::new(TestMemoryExec::update_cache(&source))
3246 }
3247
3248 fn build_left_table() -> Arc<dyn ExecutionPlan> {
3249 build_table(
3250 ("a1", &vec![5, 9, 11]),
3251 ("b1", &vec![5, 8, 8]),
3252 ("c1", &vec![50, 90, 110]),
3253 None,
3254 Vec::new(),
3255 )
3256 }
3257
3258 fn build_right_table() -> Arc<dyn ExecutionPlan> {
3259 build_table(
3260 ("a2", &vec![12, 2, 10]),
3261 ("b2", &vec![10, 2, 10]),
3262 ("c2", &vec![40, 80, 100]),
3263 None,
3264 Vec::new(),
3265 )
3266 }
3267
3268 fn prepare_join_filter() -> JoinFilter {
3269 let column_indices = vec![
3270 ColumnIndex {
3271 index: 1,
3272 side: JoinSide::Left,
3273 },
3274 ColumnIndex {
3275 index: 1,
3276 side: JoinSide::Right,
3277 },
3278 ];
3279 let intermediate_schema = Schema::new(vec![
3280 Field::new("x", DataType::Int32, true),
3281 Field::new("x", DataType::Int32, true),
3282 ]);
3283 let left_filter = Arc::new(BinaryExpr::new(
3285 Arc::new(Column::new("x", 0)),
3286 Operator::NotEq,
3287 Arc::new(Literal::new(ScalarValue::Int32(Some(8)))),
3288 )) as Arc<dyn PhysicalExpr>;
3289 let right_filter = Arc::new(BinaryExpr::new(
3291 Arc::new(Column::new("x", 1)),
3292 Operator::NotEq,
3293 Arc::new(Literal::new(ScalarValue::Int32(Some(10)))),
3294 )) as Arc<dyn PhysicalExpr>;
3295 let filter_expression =
3306 Arc::new(BinaryExpr::new(left_filter, Operator::And, right_filter))
3307 as Arc<dyn PhysicalExpr>;
3308
3309 JoinFilter::new(
3310 filter_expression,
3311 column_indices,
3312 Arc::new(intermediate_schema),
3313 )
3314 }
3315
3316 pub(crate) async fn multi_partitioned_join_collect(
3317 left: Arc<dyn ExecutionPlan>,
3318 right: Arc<dyn ExecutionPlan>,
3319 join_type: &JoinType,
3320 join_filter: Option<JoinFilter>,
3321 context: Arc<TaskContext>,
3322 ) -> Result<(Vec<String>, Vec<RecordBatch>, MetricsSet)> {
3323 let partition_count = 4;
3324
3325 let right = Arc::new(RepartitionExec::try_new(
3327 right,
3328 Partitioning::RoundRobinBatch(partition_count),
3329 )?) as Arc<dyn ExecutionPlan>;
3330
3331 let nested_loop_join =
3333 NestedLoopJoinExec::try_new(left, right, join_filter, join_type, None)?;
3334 let columns = columns(&nested_loop_join.schema());
3335 let mut batches = vec![];
3336 for i in 0..partition_count {
3337 let stream = nested_loop_join.execute(i, Arc::clone(&context))?;
3338 let more_batches = common::collect(stream).await?;
3339 batches.extend(
3340 more_batches
3341 .into_iter()
3342 .inspect(|b| {
3343 assert!(b.num_rows() <= context.session_config().batch_size())
3344 })
3345 .filter(|b| b.num_rows() > 0)
3346 .collect::<Vec<_>>(),
3347 );
3348 }
3349
3350 let metrics = nested_loop_join.metrics().unwrap();
3351
3352 Ok((columns, batches, metrics))
3353 }
3354
3355 fn new_task_ctx(batch_size: usize) -> Arc<TaskContext> {
3356 let base = TaskContext::default();
3357 let cfg = base.session_config().clone().with_batch_size(batch_size);
3359 Arc::new(base.with_session_config(cfg))
3360 }
3361
3362 #[rstest]
3363 #[tokio::test]
3364 async fn join_inner_with_filter(#[values(1, 2, 16)] batch_size: usize) -> Result<()> {
3365 let task_ctx = new_task_ctx(batch_size);
3366 dbg!(&batch_size);
3367 let left = build_left_table();
3368 let right = build_right_table();
3369 let filter = prepare_join_filter();
3370 let (columns, batches, metrics) = multi_partitioned_join_collect(
3371 left,
3372 right,
3373 &JoinType::Inner,
3374 Some(filter),
3375 task_ctx,
3376 )
3377 .await?;
3378
3379 assert_eq!(columns, vec!["a1", "b1", "c1", "a2", "b2", "c2"]);
3380 allow_duplicates!(assert_snapshot!(batches_to_sort_string(&batches), @r"
3381 +----+----+----+----+----+----+
3382 | a1 | b1 | c1 | a2 | b2 | c2 |
3383 +----+----+----+----+----+----+
3384 | 5 | 5 | 50 | 2 | 2 | 80 |
3385 +----+----+----+----+----+----+
3386 "));
3387
3388 assert_join_metrics!(metrics, 1);
3389
3390 Ok(())
3391 }
3392
3393 #[rstest]
3394 #[tokio::test]
3395 async fn join_left_with_filter(#[values(1, 2, 16)] batch_size: usize) -> Result<()> {
3396 let task_ctx = new_task_ctx(batch_size);
3397 let left = build_left_table();
3398 let right = build_right_table();
3399
3400 let filter = prepare_join_filter();
3401 let (columns, batches, metrics) = multi_partitioned_join_collect(
3402 left,
3403 right,
3404 &JoinType::Left,
3405 Some(filter),
3406 task_ctx,
3407 )
3408 .await?;
3409 assert_eq!(columns, vec!["a1", "b1", "c1", "a2", "b2", "c2"]);
3410 allow_duplicates!(assert_snapshot!(batches_to_sort_string(&batches), @r"
3411 +----+----+-----+----+----+----+
3412 | a1 | b1 | c1 | a2 | b2 | c2 |
3413 +----+----+-----+----+----+----+
3414 | 11 | 8 | 110 | | | |
3415 | 5 | 5 | 50 | 2 | 2 | 80 |
3416 | 9 | 8 | 90 | | | |
3417 +----+----+-----+----+----+----+
3418 "));
3419
3420 assert_join_metrics!(metrics, 3);
3421
3422 Ok(())
3423 }
3424
3425 #[rstest]
3426 #[tokio::test]
3427 async fn join_right_with_filter(#[values(1, 2, 16)] batch_size: usize) -> Result<()> {
3428 let task_ctx = new_task_ctx(batch_size);
3429 let left = build_left_table();
3430 let right = build_right_table();
3431
3432 let filter = prepare_join_filter();
3433 let (columns, batches, metrics) = multi_partitioned_join_collect(
3434 left,
3435 right,
3436 &JoinType::Right,
3437 Some(filter),
3438 task_ctx,
3439 )
3440 .await?;
3441 assert_eq!(columns, vec!["a1", "b1", "c1", "a2", "b2", "c2"]);
3442 allow_duplicates!(assert_snapshot!(batches_to_sort_string(&batches), @r"
3443 +----+----+----+----+----+-----+
3444 | a1 | b1 | c1 | a2 | b2 | c2 |
3445 +----+----+----+----+----+-----+
3446 | | | | 10 | 10 | 100 |
3447 | | | | 12 | 10 | 40 |
3448 | 5 | 5 | 50 | 2 | 2 | 80 |
3449 +----+----+----+----+----+-----+
3450 "));
3451
3452 assert_join_metrics!(metrics, 3);
3453
3454 Ok(())
3455 }
3456
3457 #[rstest]
3458 #[tokio::test]
3459 async fn join_full_with_filter(#[values(1, 2, 16)] batch_size: usize) -> Result<()> {
3460 let task_ctx = new_task_ctx(batch_size);
3461 let left = build_left_table();
3462 let right = build_right_table();
3463
3464 let filter = prepare_join_filter();
3465 let (columns, batches, metrics) = multi_partitioned_join_collect(
3466 left,
3467 right,
3468 &JoinType::Full,
3469 Some(filter),
3470 task_ctx,
3471 )
3472 .await?;
3473 assert_eq!(columns, vec!["a1", "b1", "c1", "a2", "b2", "c2"]);
3474 allow_duplicates!(assert_snapshot!(batches_to_sort_string(&batches), @r"
3475 +----+----+-----+----+----+-----+
3476 | a1 | b1 | c1 | a2 | b2 | c2 |
3477 +----+----+-----+----+----+-----+
3478 | | | | 10 | 10 | 100 |
3479 | | | | 12 | 10 | 40 |
3480 | 11 | 8 | 110 | | | |
3481 | 5 | 5 | 50 | 2 | 2 | 80 |
3482 | 9 | 8 | 90 | | | |
3483 +----+----+-----+----+----+-----+
3484 "));
3485
3486 assert_join_metrics!(metrics, 5);
3487
3488 Ok(())
3489 }
3490
3491 #[rstest]
3492 #[tokio::test]
3493 async fn join_left_semi_with_filter(
3494 #[values(1, 2, 16)] batch_size: usize,
3495 ) -> Result<()> {
3496 let task_ctx = new_task_ctx(batch_size);
3497 let left = build_left_table();
3498 let right = build_right_table();
3499
3500 let filter = prepare_join_filter();
3501 let (columns, batches, metrics) = multi_partitioned_join_collect(
3502 left,
3503 right,
3504 &JoinType::LeftSemi,
3505 Some(filter),
3506 task_ctx,
3507 )
3508 .await?;
3509 assert_eq!(columns, vec!["a1", "b1", "c1"]);
3510 allow_duplicates!(assert_snapshot!(batches_to_sort_string(&batches), @r"
3511 +----+----+----+
3512 | a1 | b1 | c1 |
3513 +----+----+----+
3514 | 5 | 5 | 50 |
3515 +----+----+----+
3516 "));
3517
3518 assert_join_metrics!(metrics, 1);
3519
3520 Ok(())
3521 }
3522
3523 #[rstest]
3524 #[tokio::test]
3525 async fn join_left_anti_with_filter(
3526 #[values(1, 2, 16)] batch_size: usize,
3527 ) -> Result<()> {
3528 let task_ctx = new_task_ctx(batch_size);
3529 let left = build_left_table();
3530 let right = build_right_table();
3531
3532 let filter = prepare_join_filter();
3533 let (columns, batches, metrics) = multi_partitioned_join_collect(
3534 left,
3535 right,
3536 &JoinType::LeftAnti,
3537 Some(filter),
3538 task_ctx,
3539 )
3540 .await?;
3541 assert_eq!(columns, vec!["a1", "b1", "c1"]);
3542 allow_duplicates!(assert_snapshot!(batches_to_sort_string(&batches), @r"
3543 +----+----+-----+
3544 | a1 | b1 | c1 |
3545 +----+----+-----+
3546 | 11 | 8 | 110 |
3547 | 9 | 8 | 90 |
3548 +----+----+-----+
3549 "));
3550
3551 assert_join_metrics!(metrics, 2);
3552
3553 Ok(())
3554 }
3555
3556 #[tokio::test]
3557 async fn join_has_correct_stats() -> Result<()> {
3558 let left = build_left_table();
3559 let right = build_right_table();
3560 let nested_loop_join = NestedLoopJoinExec::try_new(
3561 left,
3562 right,
3563 None,
3564 &JoinType::Left,
3565 Some(vec![1, 2]),
3566 )?;
3567 let stats = StatisticsContext::new()
3568 .compute(&nested_loop_join, &StatisticsArgs::new())?;
3569 assert_eq!(
3570 nested_loop_join.schema().fields().len(),
3571 stats.column_statistics.len(),
3572 );
3573 assert_eq!(2, stats.column_statistics.len());
3574 Ok(())
3575 }
3576
3577 #[rstest]
3578 #[tokio::test]
3579 async fn join_right_semi_with_filter(
3580 #[values(1, 2, 16)] batch_size: usize,
3581 ) -> Result<()> {
3582 let task_ctx = new_task_ctx(batch_size);
3583 let left = build_left_table();
3584 let right = build_right_table();
3585
3586 let filter = prepare_join_filter();
3587 let (columns, batches, metrics) = multi_partitioned_join_collect(
3588 left,
3589 right,
3590 &JoinType::RightSemi,
3591 Some(filter),
3592 task_ctx,
3593 )
3594 .await?;
3595 assert_eq!(columns, vec!["a2", "b2", "c2"]);
3596 allow_duplicates!(assert_snapshot!(batches_to_sort_string(&batches), @r"
3597 +----+----+----+
3598 | a2 | b2 | c2 |
3599 +----+----+----+
3600 | 2 | 2 | 80 |
3601 +----+----+----+
3602 "));
3603
3604 assert_join_metrics!(metrics, 1);
3605
3606 Ok(())
3607 }
3608
3609 #[rstest]
3610 #[tokio::test]
3611 async fn join_right_anti_with_filter(
3612 #[values(1, 2, 16)] batch_size: usize,
3613 ) -> Result<()> {
3614 let task_ctx = new_task_ctx(batch_size);
3615 let left = build_left_table();
3616 let right = build_right_table();
3617
3618 let filter = prepare_join_filter();
3619 let (columns, batches, metrics) = multi_partitioned_join_collect(
3620 left,
3621 right,
3622 &JoinType::RightAnti,
3623 Some(filter),
3624 task_ctx,
3625 )
3626 .await?;
3627 assert_eq!(columns, vec!["a2", "b2", "c2"]);
3628 allow_duplicates!(assert_snapshot!(batches_to_sort_string(&batches), @r"
3629 +----+----+-----+
3630 | a2 | b2 | c2 |
3631 +----+----+-----+
3632 | 10 | 10 | 100 |
3633 | 12 | 10 | 40 |
3634 +----+----+-----+
3635 "));
3636
3637 assert_join_metrics!(metrics, 2);
3638
3639 Ok(())
3640 }
3641
3642 #[rstest]
3643 #[tokio::test]
3644 async fn join_left_mark_with_filter(
3645 #[values(1, 2, 16)] batch_size: usize,
3646 ) -> Result<()> {
3647 let task_ctx = new_task_ctx(batch_size);
3648 let left = build_left_table();
3649 let right = build_right_table();
3650
3651 let filter = prepare_join_filter();
3652 let (columns, batches, metrics) = multi_partitioned_join_collect(
3653 left,
3654 right,
3655 &JoinType::LeftMark,
3656 Some(filter),
3657 task_ctx,
3658 )
3659 .await?;
3660 assert_eq!(columns, vec!["a1", "b1", "c1", "mark"]);
3661 allow_duplicates!(assert_snapshot!(batches_to_sort_string(&batches), @r"
3662 +----+----+-----+-------+
3663 | a1 | b1 | c1 | mark |
3664 +----+----+-----+-------+
3665 | 11 | 8 | 110 | false |
3666 | 5 | 5 | 50 | true |
3667 | 9 | 8 | 90 | false |
3668 +----+----+-----+-------+
3669 "));
3670
3671 assert_join_metrics!(metrics, 3);
3672
3673 Ok(())
3674 }
3675
3676 #[rstest]
3677 #[tokio::test]
3678 async fn join_right_mark_with_filter(
3679 #[values(1, 2, 16)] batch_size: usize,
3680 ) -> Result<()> {
3681 let task_ctx = new_task_ctx(batch_size);
3682 let left = build_left_table();
3683 let right = build_right_table();
3684
3685 let filter = prepare_join_filter();
3686 let (columns, batches, metrics) = multi_partitioned_join_collect(
3687 left,
3688 right,
3689 &JoinType::RightMark,
3690 Some(filter),
3691 task_ctx,
3692 )
3693 .await?;
3694 assert_eq!(columns, vec!["a2", "b2", "c2", "mark"]);
3695
3696 allow_duplicates!(assert_snapshot!(batches_to_sort_string(&batches), @r"
3697 +----+----+-----+-------+
3698 | a2 | b2 | c2 | mark |
3699 +----+----+-----+-------+
3700 | 10 | 10 | 100 | false |
3701 | 12 | 10 | 40 | false |
3702 | 2 | 2 | 80 | true |
3703 +----+----+-----+-------+
3704 "));
3705
3706 assert_join_metrics!(metrics, 3);
3707
3708 Ok(())
3709 }
3710
3711 #[tokio::test]
3712 async fn test_overallocation() -> Result<()> {
3713 let left = build_table(
3714 ("a1", &vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 0]),
3715 ("b1", &vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 0]),
3716 ("c1", &vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 0]),
3717 None,
3718 Vec::new(),
3719 );
3720 let right = build_table(
3721 ("a2", &vec![10, 11]),
3722 ("b2", &vec![12, 13]),
3723 ("c2", &vec![14, 15]),
3724 None,
3725 Vec::new(),
3726 );
3727 let filter = prepare_join_filter();
3728
3729 let fallback_join_types = vec![
3732 JoinType::Inner,
3733 JoinType::Left,
3734 JoinType::LeftSemi,
3735 JoinType::LeftAnti,
3736 JoinType::LeftMark,
3737 JoinType::Right,
3738 JoinType::RightSemi,
3739 JoinType::RightAnti,
3740 JoinType::RightMark,
3741 ];
3742
3743 for join_type in &fallback_join_types {
3744 let runtime = RuntimeEnvBuilder::new()
3745 .with_memory_limit(100, 1.0)
3746 .build_arc()?;
3747 let task_ctx = TaskContext::default().with_runtime(runtime);
3748 let task_ctx = Arc::new(task_ctx);
3749
3750 let _result = multi_partitioned_join_collect(
3752 Arc::clone(&left),
3753 Arc::clone(&right),
3754 join_type,
3755 Some(filter.clone()),
3756 task_ctx,
3757 )
3758 .await?;
3759 }
3760
3761 let runtime = RuntimeEnvBuilder::new()
3765 .with_memory_limit(100, 1.0)
3766 .build_arc()?;
3767 let task_ctx = TaskContext::default().with_runtime(runtime);
3768 let task_ctx = Arc::new(task_ctx);
3769 let err = multi_partitioned_join_collect(
3770 Arc::clone(&left),
3771 Arc::clone(&right),
3772 &JoinType::Full,
3773 Some(filter.clone()),
3774 task_ctx,
3775 )
3776 .await
3777 .unwrap_err();
3778 assert_contains!(err.to_string(), "Resources exhausted");
3779
3780 Ok(())
3781 }
3782
3783 fn columns(schema: &Schema) -> Vec<String> {
3785 schema.fields().iter().map(|f| f.name().clone()).collect()
3786 }
3787
3788 async fn join_collect(
3794 left: Arc<dyn ExecutionPlan>,
3795 right: Arc<dyn ExecutionPlan>,
3796 join_type: &JoinType,
3797 join_filter: Option<JoinFilter>,
3798 context: Arc<TaskContext>,
3799 ) -> Result<(Vec<String>, Vec<RecordBatch>, MetricsSet)> {
3800 let nested_loop_join =
3801 NestedLoopJoinExec::try_new(left, right, join_filter, join_type, None)?;
3802 let columns = columns(&nested_loop_join.schema());
3803 let stream = nested_loop_join.execute(0, context)?;
3804 let batches: Vec<RecordBatch> = common::collect(stream)
3805 .await?
3806 .into_iter()
3807 .filter(|b| b.num_rows() > 0)
3808 .collect();
3809 let metrics = nested_loop_join.metrics().unwrap();
3810 Ok((columns, batches, metrics))
3811 }
3812
3813 fn task_ctx_with_memory_limit(
3815 memory_limit: usize,
3816 batch_size: usize,
3817 ) -> Result<Arc<TaskContext>> {
3818 let runtime = RuntimeEnvBuilder::new()
3819 .with_memory_limit(memory_limit, 1.0)
3820 .build_arc()?;
3821 let cfg = TaskContext::default()
3822 .session_config()
3823 .clone()
3824 .with_batch_size(batch_size);
3825 let task_ctx = TaskContext::default()
3826 .with_runtime(runtime)
3827 .with_session_config(cfg);
3828 Ok(Arc::new(task_ctx))
3829 }
3830
3831 #[tokio::test]
3832 async fn test_nlj_memory_limited_inner_join() -> Result<()> {
3833 let task_ctx = task_ctx_with_memory_limit(50, 16)?;
3835 let left = build_left_table();
3836 let right = build_right_table();
3837 let filter = prepare_join_filter();
3838
3839 let (columns, batches, metrics) =
3840 join_collect(left, right, &JoinType::Inner, Some(filter), task_ctx).await?;
3841
3842 assert_eq!(columns, vec!["a1", "b1", "c1", "a2", "b2", "c2"]);
3843
3844 assert!(
3846 metrics.spill_count().unwrap_or(0) > 0,
3847 "Expected spilling to occur under tight memory limit"
3848 );
3849
3850 allow_duplicates!(assert_snapshot!(batches_to_sort_string(&batches), @r"
3852 +----+----+----+----+----+----+
3853 | a1 | b1 | c1 | a2 | b2 | c2 |
3854 +----+----+----+----+----+----+
3855 | 5 | 5 | 50 | 2 | 2 | 80 |
3856 +----+----+----+----+----+----+
3857 "));
3858 Ok(())
3859 }
3860
3861 #[tokio::test]
3862 async fn test_nlj_memory_limited_left_join() -> Result<()> {
3863 let task_ctx = task_ctx_with_memory_limit(50, 16)?;
3864 let left = build_left_table();
3865 let right = build_right_table();
3866 let filter = prepare_join_filter();
3867
3868 let (columns, batches, metrics) =
3869 join_collect(left, right, &JoinType::Left, Some(filter), task_ctx).await?;
3870
3871 assert_eq!(columns, vec!["a1", "b1", "c1", "a2", "b2", "c2"]);
3872
3873 assert!(
3875 metrics.spill_count().unwrap_or(0) > 0,
3876 "Expected spilling to occur under tight memory limit"
3877 );
3878
3879 allow_duplicates!(assert_snapshot!(batches_to_sort_string(&batches), @r"
3880 +----+----+-----+----+----+----+
3881 | a1 | b1 | c1 | a2 | b2 | c2 |
3882 +----+----+-----+----+----+----+
3883 | 11 | 8 | 110 | | | |
3884 | 5 | 5 | 50 | 2 | 2 | 80 |
3885 | 9 | 8 | 90 | | | |
3886 +----+----+-----+----+----+----+
3887 "));
3888 Ok(())
3889 }
3890
3891 #[tokio::test]
3892 async fn test_nlj_fits_in_memory_no_spill() -> Result<()> {
3893 let task_ctx = task_ctx_with_memory_limit(10_000_000, 16)?;
3895 let left = build_left_table();
3896 let right = build_right_table();
3897 let filter = prepare_join_filter();
3898
3899 let (columns, batches, metrics) =
3900 join_collect(left, right, &JoinType::Inner, Some(filter), task_ctx).await?;
3901
3902 assert_eq!(columns, vec!["a1", "b1", "c1", "a2", "b2", "c2"]);
3903
3904 assert_eq!(
3906 metrics.spill_count().unwrap_or(0),
3907 0,
3908 "Expected no spilling with generous memory limit"
3909 );
3910
3911 allow_duplicates!(assert_snapshot!(batches_to_sort_string(&batches), @r"
3912 +----+----+----+----+----+----+
3913 | a1 | b1 | c1 | a2 | b2 | c2 |
3914 +----+----+----+----+----+----+
3915 | 5 | 5 | 50 | 2 | 2 | 80 |
3916 +----+----+----+----+----+----+
3917 "));
3918 Ok(())
3919 }
3920
3921 #[tokio::test]
3922 async fn test_nlj_memory_limited_empty_inputs() -> Result<()> {
3923 let task_ctx = task_ctx_with_memory_limit(50, 16)?;
3924
3925 let empty_left = build_table(
3927 ("a1", &vec![]),
3928 ("b1", &vec![]),
3929 ("c1", &vec![]),
3930 None,
3931 Vec::new(),
3932 );
3933 let right = build_right_table();
3934 let filter = prepare_join_filter();
3935
3936 let (_columns, batches, _metrics) =
3937 join_collect(empty_left, right, &JoinType::Inner, Some(filter), task_ctx)
3938 .await?;
3939 assert!(batches.is_empty() || batches.iter().all(|b| b.num_rows() == 0));
3940
3941 let task_ctx2 = task_ctx_with_memory_limit(50, 16)?;
3943 let left = build_left_table();
3944 let empty_right = build_table(
3945 ("a2", &vec![]),
3946 ("b2", &vec![]),
3947 ("c2", &vec![]),
3948 None,
3949 Vec::new(),
3950 );
3951 let filter2 = prepare_join_filter();
3952
3953 let (_columns, batches, _metrics) = join_collect(
3954 left,
3955 empty_right,
3956 &JoinType::Inner,
3957 Some(filter2),
3958 task_ctx2,
3959 )
3960 .await?;
3961 assert!(batches.is_empty() || batches.iter().all(|b| b.num_rows() == 0));
3962
3963 Ok(())
3964 }
3965
3966 #[tokio::test]
3967 async fn test_nlj_memory_limited_no_disk_falls_back_to_oom() -> Result<()> {
3968 use datafusion_execution::disk_manager::{DiskManagerBuilder, DiskManagerMode};
3970
3971 let runtime = RuntimeEnvBuilder::new()
3972 .with_memory_limit(100, 1.0)
3973 .with_disk_manager_builder(
3974 DiskManagerBuilder::default().with_mode(DiskManagerMode::Disabled),
3975 )
3976 .build_arc()?;
3977 let task_ctx = Arc::new(TaskContext::default().with_runtime(runtime));
3978
3979 let left = build_left_table();
3980 let right = build_right_table();
3981 let filter = prepare_join_filter();
3982
3983 let err = join_collect(left, right, &JoinType::Inner, Some(filter), task_ctx)
3984 .await
3985 .unwrap_err();
3986
3987 assert_contains!(err.to_string(), "Resources exhausted");
3988 Ok(())
3989 }
3990
3991 #[tokio::test]
3992 async fn test_nlj_memory_limited_right_join() -> Result<()> {
3993 let task_ctx = task_ctx_with_memory_limit(50, 16)?;
3994 let left = build_left_table();
3995 let right = build_right_table();
3996 let filter = prepare_join_filter();
3997
3998 let (columns, batches, metrics) =
3999 join_collect(left, right, &JoinType::Right, Some(filter), task_ctx).await?;
4000
4001 assert_eq!(columns, vec!["a1", "b1", "c1", "a2", "b2", "c2"]);
4002
4003 assert!(
4005 metrics.spill_count().unwrap_or(0) > 0,
4006 "Expected spilling to occur under tight memory limit"
4007 );
4008
4009 allow_duplicates!(assert_snapshot!(batches_to_sort_string(&batches), @r"
4011 +----+----+----+----+----+-----+
4012 | a1 | b1 | c1 | a2 | b2 | c2 |
4013 +----+----+----+----+----+-----+
4014 | | | | 10 | 10 | 100 |
4015 | | | | 12 | 10 | 40 |
4016 | 5 | 5 | 50 | 2 | 2 | 80 |
4017 +----+----+----+----+----+-----+
4018 "));
4019 Ok(())
4020 }
4021
4022 #[tokio::test]
4023 async fn test_nlj_memory_limited_full_join() -> Result<()> {
4024 let task_ctx = task_ctx_with_memory_limit(50, 16)?;
4025 let left = build_left_table();
4026 let right = build_right_table();
4027 let filter = prepare_join_filter();
4028
4029 let (columns, batches, metrics) =
4030 join_collect(left, right, &JoinType::Full, Some(filter), task_ctx).await?;
4031
4032 assert_eq!(columns, vec!["a1", "b1", "c1", "a2", "b2", "c2"]);
4033
4034 assert!(
4036 metrics.spill_count().unwrap_or(0) > 0,
4037 "Expected spilling to occur under tight memory limit"
4038 );
4039
4040 allow_duplicates!(assert_snapshot!(batches_to_sort_string(&batches), @r"
4042 +----+----+-----+----+----+-----+
4043 | a1 | b1 | c1 | a2 | b2 | c2 |
4044 +----+----+-----+----+----+-----+
4045 | | | | 10 | 10 | 100 |
4046 | | | | 12 | 10 | 40 |
4047 | 11 | 8 | 110 | | | |
4048 | 5 | 5 | 50 | 2 | 2 | 80 |
4049 | 9 | 8 | 90 | | | |
4050 +----+----+-----+----+----+-----+
4051 "));
4052 Ok(())
4053 }
4054
4055 #[tokio::test]
4056 async fn test_nlj_memory_limited_right_semi_join() -> Result<()> {
4057 let task_ctx = task_ctx_with_memory_limit(50, 16)?;
4058 let left = build_left_table();
4059 let right = build_right_table();
4060 let filter = prepare_join_filter();
4061
4062 let (columns, batches, metrics) =
4063 join_collect(left, right, &JoinType::RightSemi, Some(filter), task_ctx)
4064 .await?;
4065
4066 assert_eq!(columns, vec!["a2", "b2", "c2"]);
4067
4068 assert!(
4069 metrics.spill_count().unwrap_or(0) > 0,
4070 "Expected spilling to occur under tight memory limit"
4071 );
4072
4073 allow_duplicates!(assert_snapshot!(batches_to_sort_string(&batches), @r"
4075 +----+----+----+
4076 | a2 | b2 | c2 |
4077 +----+----+----+
4078 | 2 | 2 | 80 |
4079 +----+----+----+
4080 "));
4081 Ok(())
4082 }
4083
4084 #[tokio::test]
4085 async fn test_nlj_memory_limited_right_anti_join() -> Result<()> {
4086 let task_ctx = task_ctx_with_memory_limit(50, 16)?;
4087 let left = build_left_table();
4088 let right = build_right_table();
4089 let filter = prepare_join_filter();
4090
4091 let (columns, batches, metrics) =
4092 join_collect(left, right, &JoinType::RightAnti, Some(filter), task_ctx)
4093 .await?;
4094
4095 assert_eq!(columns, vec!["a2", "b2", "c2"]);
4096
4097 assert!(
4098 metrics.spill_count().unwrap_or(0) > 0,
4099 "Expected spilling to occur under tight memory limit"
4100 );
4101
4102 allow_duplicates!(assert_snapshot!(batches_to_sort_string(&batches), @r"
4104 +----+----+-----+
4105 | a2 | b2 | c2 |
4106 +----+----+-----+
4107 | 10 | 10 | 100 |
4108 | 12 | 10 | 40 |
4109 +----+----+-----+
4110 "));
4111 Ok(())
4112 }
4113
4114 #[tokio::test]
4115 async fn test_nlj_memory_limited_right_mark_join() -> Result<()> {
4116 let task_ctx = task_ctx_with_memory_limit(50, 16)?;
4117 let left = build_left_table();
4118 let right = build_right_table();
4119 let filter = prepare_join_filter();
4120
4121 let (columns, batches, metrics) =
4122 join_collect(left, right, &JoinType::RightMark, Some(filter), task_ctx)
4123 .await?;
4124
4125 assert_eq!(columns, vec!["a2", "b2", "c2", "mark"]);
4126
4127 assert!(
4128 metrics.spill_count().unwrap_or(0) > 0,
4129 "Expected spilling to occur under tight memory limit"
4130 );
4131
4132 allow_duplicates!(assert_snapshot!(batches_to_sort_string(&batches), @r"
4134 +----+----+-----+-------+
4135 | a2 | b2 | c2 | mark |
4136 +----+----+-----+-------+
4137 | 10 | 10 | 100 | false |
4138 | 12 | 10 | 40 | false |
4139 | 2 | 2 | 80 | true |
4140 +----+----+-----+-------+
4141 "));
4142 Ok(())
4143 }
4144}