cloud-fan commented on code in PR #58419:
URL: https://github.com/apache/spark/pull/58419#discussion_r4000488270


##########
sql/core/src/main/scala/org/apache/spark/sql/execution/basicPhysicalOperators.scala:
##########
@@ -1035,13 +1039,79 @@ case class UnionExec(children: Seq[SparkPlan]) extends 
SparkPlan with CodegenSup
     }
   }
 
-  // True when the codegen path applies: `outputPartitioning` is 
`UnknownPartitioning`,
-  // and `unionedInputRDD` matches the semantics of `sparkContext.union(...)` 
in `unionRDDs`.
-  // A `KeyedPartitioning` union also uses `sparkContext.union(...)` in 
`unionRDDs`, but
-  // codegen is disabled for it (`supportCodegenFailureReason` reports 
"partitioning-aware"):
-  // the per-partition key descriptor is consumed by a downstream 
`GroupPartitionsExec`, and
-  // keeping these unions out of whole-stage codegen matches the 
`HashPartitioning` union case.
-  private[sql] def isPlainUnion: Boolean = 
outputPartitioning.isInstanceOf[UnknownPartitioning]
+  /**
+   * True when this union behaves as a plain concatenation, so 
`unionedInputRDD` matches the
+   * semantics of `sparkContext.union(...)` in `unionRDDs`. It satisfies the 
partitioning gate on
+   * the codegen path, not the whole of it: `supportCodegenFailureReason` 
still applies its other
+   * checks. When this union merges its children's `KeyedPartitioning` 
instead, it concatenates all
+   * the same, but codegen stays off, with the reason "partitioning-aware", 
because a downstream
+   * `GroupPartitionsExec` consumes its key descriptor.
+   *
+   * Stamped, because the answer moves under its consumers.
+   * `InMemoryTableScanExec.outputPartitioning` reports `UnknownPartitioning` 
while its inner
+   * `AdaptiveSparkPlanExec` has no final plan, so a union can look plain when
+   * `CollapseCodegenStages` gates on it and partitioning-aware by the time 
the stage runs. The
+   * shell that gate builds wraps a `withNewChildren` copy where a child had 
to be adapted, and a
+   * copy that re-derived here came back with empty `metrics` while 
`doProduce` asked `metricTerm`
+   * for `numOutputRows`. A fresh copy inherits the answer instead, since 
`withNewChildren` ends in
+   * `copyTagsFrom`.
+   *
+   * `UNION_OUTPUT_PARTITIONING` is read where the decision is stamped rather 
than in
+   * `rawPartitioning`, so it too is fixed once the plan is prepared: `conf` 
is live, and a plan
+   * must execute by the partitioning it was planned against.
+   *
+   * A read before `StampUnionDecisions` answers from the children as they are 
then, and does not
+   * write, so observing an unprepared plan cannot decide anything for the 
prepared one.
+   */
+  private[execution] def isPlainUnion: Boolean = 
stampedDecisions.map(_.plainUnion).getOrElse {
+    !conf.getConf(SQLConf.UNION_OUTPUT_PARTITIONING) ||

Review Comment:
   **Non-blocking (P2):** `EnsureRequirements` and this later stamp sample 
`UNION_OUTPUT_PARTITIONING` separately. Because the shared `SQLConf` can be 
updated concurrently, a true-to-false flip between them lets the parent omit an 
exchange using a concrete union partitioning, then freezes plain concatenating 
execution here. An aggregate above the union can consequently emit duplicate 
groups. Please capture the configuration input before `EnsureRequirements` and 
use that same value when deriving the post-requirements decision.
   
   **Recommended change:** Introduce a preparation-only configuration-input 
snapshot for UnionExec before each initial EnsureRequirements pass, then derive 
and publish the final plain-layout decision after requirements using that same 
captured input. Add deterministic concurrency coverage that changes the session 
setting between those phases.
   
   **Why this works:** A pre-requirements rule records only the relevant 
configuration inputs on each unstamped UnionExec without deriving child 
partitioning. isPlainUnion consults a completed decision first, then the 
preparation input snapshot, and only uses the live session configuration before 
preparation. EnsureRequirements therefore plans against the captured setting; 
its rebuilt tagless copies inherit the input through normal tag propagation; 
the following StampUnionDecisions combines that same setting with the 
post-requirements child partitioning into the existing immutable decision. 
Fresh unions created by later supported hooks continue to be stamped at their 
post-hook barriers, and write-once completed decisions remain unchanged.
   
   **Scope:** sql/core/src/main/scala/org/apache/spark/sql/execution, 
sql/core/src/test/scala/org/apache/spark/sql/execution
   
   **Compatibility:** Keep the preparation-time configuration lifecycle, the 
partitioning decision derived after EnsureRequirements, the late-extension 
barriers, and the accepted exchange-elision, bucket-scan fusion, and 
skew-coalescing tradeoffs.
   
   **Risks:** If the pre-requirements snapshot derives rawPartitioning as well 
as configuration, it can freeze the pre-exchange answer and lose valid 
partitioning-aware execution. If requirements rebuilds a UnionExec without 
propagating the input snapshot, the two phases can diverge again. Late 
extension-created unions must retain their existing post-hook stamping behavior 
and must not overwrite an earlier completed decision.
   
   **Constraints:** Reads of an unprepared plan outside physical preparation 
remain non-mutating and reflect live state. Do not hold the shared SQLConf 
settings monitor across physical rule execution. Preserve the existing 
write-once decision, codegen-copy propagation, and documented result-preserving 
optimization tradeoffs. Capture configuration inputs without capturing 
child-derived partitioning before EnsureRequirements.
   
   **Success:** EnsureRequirements and final UnionExec execution use the same 
UNION_OUTPUT_PARTITIONING value even when another thread changes the session 
setting between their rule invocations. A parent exchange elided from a 
concrete union partitioning cannot be followed by plain concatenating execution 
because of an intervening configuration update. Pre-preparation inspection does 
not pin a decision for the prepared clone, and plans first prepared after an 
ordinary non-concurrent configuration change still observe the new value. Late 
extension barriers and rebuilt codegen-shell copies preserve the intended 
completed decision without restamping it.



-- 
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