Copilot commented on code in PR #23948:
URL: https://github.com/apache/datafusion/pull/23948#discussion_r3668209530


##########
datafusion/physical-optimizer/src/ensure_requirements/enforce_sorting/mod.rs:
##########
@@ -609,11 +609,40 @@ fn adjust_window_sort_removal(
 /// the plan, some of the remaining `RepartitionExec`s might become 
unnecessary.
 /// Removes such `RepartitionExec`s from the plan as well.
 fn remove_bottleneck_in_subplan(
+    requirements: PlanWithCorrespondingCoalescePartitions,
+) -> Result<PlanWithCorrespondingCoalescePartitions> {
+    // The root is the node `parallelize_sorts` is rewriting (a `SortExec`,
+    // `SortPreservingMergeExec` or `CoalescePartitionsExec`). Its own 
distribution
+    // requirement does not constrain the removal, because the caller drops 
the node and
+    // rebuilds the cascade around the result.
+    remove_bottleneck_in_subplan_impl(requirements, true)
+}
+
+fn remove_bottleneck_in_subplan_impl(
     mut requirements: PlanWithCorrespondingCoalescePartitions,
+    is_root: bool,
 ) -> Result<PlanWithCorrespondingCoalescePartitions> {
     let plan = &requirements.plan;
+    // Below the root, a `CoalescePartitionsExec` feeding a child that requires
+    // `Distribution::SinglePartition` is not an avoidable bottleneck: it is 
what satisfies
+    // that requirement. Removing it leaves the parent with a multi-partition 
input it cannot
+    // accept, and nothing re-runs distribution enforcement afterwards, so the 
plan reaches
+    // `SanityCheckPlan` invalid. The traversal reaches such a node because
+    // `update_coalesce_ctx_children` marks a node as connected when *any* 
child qualifies:
+    // a `CollectLeft` `HashJoinExec` whose probe side is connected is 
descended into even
+    // though its build side must stay single-partition.
+    let removable = |idx: usize| {
+        is_root
+            || !matches!(
+                plan.input_distribution_requirements()
+                    .child_distribution(idx),
+                Some(Distribution::SinglePartition)
+            )
+    };
+    let remove_from_first_child =
+        is_coalesce_partitions(&requirements.children[0].plan) && removable(0);

Review Comment:
   `requirements.children[0]` will panic if `remove_bottleneck_in_subplan_impl` 
is ever called on a node with zero children. Consider adding an early return 
for `requirements.children.is_empty()` or rewriting this to use 
`requirements.children.get(0)` so the traversal is robust even if `node.data` 
is set on a leaf.



##########
datafusion/physical-optimizer/src/ensure_requirements/enforce_sorting/mod.rs:
##########
@@ -609,11 +609,40 @@ fn adjust_window_sort_removal(
 /// the plan, some of the remaining `RepartitionExec`s might become 
unnecessary.
 /// Removes such `RepartitionExec`s from the plan as well.
 fn remove_bottleneck_in_subplan(
+    requirements: PlanWithCorrespondingCoalescePartitions,
+) -> Result<PlanWithCorrespondingCoalescePartitions> {
+    // The root is the node `parallelize_sorts` is rewriting (a `SortExec`,
+    // `SortPreservingMergeExec` or `CoalescePartitionsExec`). Its own 
distribution
+    // requirement does not constrain the removal, because the caller drops 
the node and
+    // rebuilds the cascade around the result.
+    remove_bottleneck_in_subplan_impl(requirements, true)
+}
+
+fn remove_bottleneck_in_subplan_impl(
     mut requirements: PlanWithCorrespondingCoalescePartitions,
+    is_root: bool,
 ) -> Result<PlanWithCorrespondingCoalescePartitions> {
     let plan = &requirements.plan;
+    // Below the root, a `CoalescePartitionsExec` feeding a child that requires
+    // `Distribution::SinglePartition` is not an avoidable bottleneck: it is 
what satisfies
+    // that requirement. Removing it leaves the parent with a multi-partition 
input it cannot
+    // accept, and nothing re-runs distribution enforcement afterwards, so the 
plan reaches
+    // `SanityCheckPlan` invalid. The traversal reaches such a node because
+    // `update_coalesce_ctx_children` marks a node as connected when *any* 
child qualifies:
+    // a `CollectLeft` `HashJoinExec` whose probe side is connected is 
descended into even
+    // though its build side must stay single-partition.
+    let removable = |idx: usize| {
+        is_root
+            || !matches!(
+                plan.input_distribution_requirements()
+                    .child_distribution(idx),
+                Some(Distribution::SinglePartition)
+            )
+    };

Review Comment:
   `plan.input_distribution_requirements()` is invoked each time 
`removable(idx)` runs (including inside the child iteration). If this accessor 
does non-trivial work or allocations, it can add overhead to plan rewrites. 
Consider storing `let dist_reqs = plan.input_distribution_requirements();` once 
and referencing `dist_reqs.child_distribution(idx)`.



##########
datafusion/sqllogictest/test_files/joins.slt:
##########
@@ -5608,3 +5608,77 @@ set datafusion.execution.target_partitions = 4;
 
 statement ok
 reset datafusion.execution.batch_size;
+
+# Regression test: a `CollectLeft` `HashJoinExec` requires `SinglePartition` 
on its build
+# (left) child, and the `CoalescePartitionsExec` that satisfies it must 
survive the
+# sort-parallelization phase of `EnsureRequirements`. It used to be removed 
positionally
+# (the traversal descends into the join because the *probe* side is linked to 
a coalesce),
+# leaving a multi-partition build side that `SanityCheckPlan` rejects with
+# "does not satisfy distribution requirements: SinglePartition".
+
+statement ok
+set datafusion.execution.target_partitions = 8;
+
+# Keep the scan multi-partition as written, i.e. one partition per file.
+statement ok
+set datafusion.optimizer.repartition_file_scans = false;
+
+statement ok
+CREATE TABLE collect_left_src (id INT, ts INT) AS VALUES (1, 10), (2, 20), (3, 
30);
+
+query I
+COPY (SELECT * FROM collect_left_src) TO 
'test_files/scratch/joins/collect_left/0.parquet' STORED AS PARQUET;
+----
+3

Review Comment:
   This test writes Parquet files under `test_files/scratch/...` but does not 
remove them afterwards. If the sqllogictest harness doesn’t reliably clean 
`test_files/scratch` between runs (or if `COPY` doesn’t overwrite on all 
platforms/filesystems), this can cause disk growth or rerun flakiness. If 
supported by the harness, add an explicit cleanup step for the directory/files; 
otherwise consider using a run-unique scratch subdirectory to avoid collisions.



##########
datafusion/core/tests/physical_optimizer/ensure_requirements.rs:
##########
@@ -1252,3 +1267,112 @@ fn test_idempotent_union_projection_sort() {
 
     assert_idempotent(plan);
 }
+
+/// A `CollectLeft` `HashJoinExec` requires `Distribution::SinglePartition` on 
its build
+/// (left) child, so `EnsureRequirements` puts a `CoalescePartitionsExec` on 
top of a
+/// multi-partition build side. Its sort-parallelization phase must not take 
that coalesce
+/// back out again.
+///
+/// The phase descends into a node when *any* of its children is linked to a
+/// `CoalescePartitionsExec` below (`update_coalesce_ctx_children`), so a 
connected probe
+/// side is enough to reach the join, and the removal itself used to look only 
at
+/// `children[0]` without consulting the join's own distribution requirement. 
The result was
+/// a build side left multi-partition with nothing to re-enforce distribution 
afterwards,
+/// which `SanityCheckPlan` then rejected with "does not satisfy distribution 
requirements:
+/// SinglePartition".
+#[test]
+fn test_collect_left_join_keeps_build_side_coalesce() {
+    let build: Arc<dyn ExecutionPlan> = 
Arc::new(MockMultiPartitionExec::new(4));
+    // The probe side carries the `CoalescePartitionsExec` link that makes the 
traversal
+    // descend into the join in the first place.
+    let probe: Arc<dyn ExecutionPlan> = 
Arc::new(CoalescePartitionsExec::new(Arc::new(
+        MockMultiPartitionExec::new(4),
+    )));
+
+    let on = vec![(
+        Arc::new(Column::new("a", 0)) as Arc<dyn PhysicalExpr>,
+        Arc::new(Column::new("a", 0)) as Arc<dyn PhysicalExpr>,
+    )];
+    let join: Arc<dyn ExecutionPlan> = Arc::new(
+        HashJoinExec::try_new(
+            build,
+            probe,
+            on,
+            None,
+            &JoinType::Left,
+            None,
+            PartitionMode::CollectLeft,
+            NullEquality::NullEqualsNothing,
+            false,
+        )
+        .expect("HashJoinExec creation failed"),
+    );
+
+    // A global sort on top is what triggers the sort-parallelization phase.
+    let plan: Arc<dyn ExecutionPlan> =
+        Arc::new(SortExec::new(sort_expr_on("a", 0, false, false), join));
+
+    let optimized = optimize_and_sanity_check(plan).expect("plan failed 
SanityCheckPlan");
+
+    assert_snapshot!(plan_string(&optimized), @r"
+    SortPreservingMergeExec: [a@0 ASC NULLS LAST]
+      SortExec: expr=[a@0 ASC NULLS LAST], preserve_partitioning=[true]
+        HashJoinExec: mode=CollectLeft, join_type=Left, on=[(a@0, a@0)]
+          CoalescePartitionsExec
+            MockMultiPartitionExec
+          RepartitionExec: partitioning=RoundRobinBatch(8), input_partitions=4
+            MockMultiPartitionExec
+    ");

Review Comment:
   These snapshots hard-code `RoundRobinBatch(8)`, which makes the tests 
brittle if `TEST_TARGET_PARTITIONS` / the test config’s target partition count 
changes (even though the behavior being tested is the presence of the 
build-side `CoalescePartitionsExec`). Consider asserting the key structural 
property (build side retains `CoalescePartitionsExec` under a `CollectLeft` 
join) without pinning the entire formatted plan to a specific partition count.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to