This is an automated email from the ASF dual-hosted git repository.

cloud-fan pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/spark.git


The following commit(s) were added to refs/heads/master by this push:
     new 529483475e73 [SPARK-58138][SQL] Preserve child output partitioning 
through BIN BY
529483475e73 is described below

commit 529483475e73852e45e9d84152705af6594ac303
Author: Nikolina Vraneš <[email protected]>
AuthorDate: Thu Jul 16 01:14:14 2026 +0800

    [SPARK-58138][SQL] Preserve child output partitioning through BIN BY
    
    ### What changes were proposed in this pull request?
    
    This PR makes `BinByExec` extend `PartitioningPreservingUnaryExecNode` so 
it reports an `outputPartitioning` derived from its child.
    
    A child partitioning is kept only when its keys are in the operator's 
output: a partitioning on a pass-through column survives, while one on a scaled 
`DISTRIBUTE` column is dropped (that column has a fresh `ExprId` absent from 
the output). Keeping the pass-through case is safe because `BIN BY` transforms 
each partition independently and pass-through columns hold the same value 
across an input row's sub-rows.
    
    ### Why are the changes needed?
    
    A downstream operator clustered on a pass-through column that the child 
already partitions on would otherwise get an unnecessary shuffle.
    
    ### Does this PR introduce _any_ user-facing change?
    
    No. `BIN BY` is gated off by default 
(`spark.sql.binByRelationOperator.enabled`).
    
    ### How was this patch tested?
    
    `ProjectedOrderingAndPartitioningSuite`: a pass-through key is preserved as 
`HashPartitioning`, a scaled `DISTRIBUTE` key is dropped to 
`UnknownPartitioning`, and a mixed key is dropped whole.
    
    ### Was this patch authored or co-authored using generative AI tooling?
    
    Generated-by: Claude Code (Anthropic)
    
    Closes #57275 from vranes/bin-by-output-partitioning.
    
    Authored-by: Nikolina Vraneš <[email protected]>
    Signed-off-by: Wenchen Fan <[email protected]>
---
 .../org/apache/spark/sql/execution/BinByExec.scala |  9 ++-
 .../ProjectedOrderingAndPartitioningSuite.scala    | 75 +++++++++++++++++++++-
 2 files changed, 81 insertions(+), 3 deletions(-)

diff --git 
a/sql/core/src/main/scala/org/apache/spark/sql/execution/BinByExec.scala 
b/sql/core/src/main/scala/org/apache/spark/sql/execution/BinByExec.scala
index d95addd93f40..22898f6dfdbd 100644
--- a/sql/core/src/main/scala/org/apache/spark/sql/execution/BinByExec.scala
+++ b/sql/core/src/main/scala/org/apache/spark/sql/execution/BinByExec.scala
@@ -22,7 +22,7 @@ import java.time.ZoneOffset
 import org.apache.spark.SparkException
 import org.apache.spark.rdd.RDD
 import org.apache.spark.sql.catalyst.InternalRow
-import org.apache.spark.sql.catalyst.expressions.{Attribute, AttributeMap, 
AttributeSet, BindReferences, BoundReference, Cast, Expression, 
GenericInternalRow, JoinedRow, Literal, Multiply, UnsafeProjection}
+import org.apache.spark.sql.catalyst.expressions.{Attribute, AttributeMap, 
AttributeSet, BindReferences, BoundReference, Cast, Expression, 
GenericInternalRow, JoinedRow, Literal, Multiply, NamedExpression, 
UnsafeProjection}
 import org.apache.spark.sql.catalyst.util.{DateTimeUtils, TimestampFormatter}
 import org.apache.spark.sql.errors.QueryExecutionErrors
 import org.apache.spark.sql.execution.metric.{SQLMetric, SQLMetrics}
@@ -48,7 +48,7 @@ case class BinByExec(
     appendedAttributes: Seq[Attribute],
     timeZoneId: Option[String],
     child: SparkPlan)
-  extends UnaryExecNode {
+  extends UnaryExecNode with PartitioningPreservingUnaryExecNode {
 
   override lazy val metrics: Map[String, SQLMetric] = Map(
     "numOutputRows" -> SQLMetrics.createMetric(sparkContext, "number of output 
rows"))
@@ -66,6 +66,11 @@ case class BinByExec(
   override def producedAttributes: AttributeSet =
     AttributeSet(scaledDistributeColumns ++ appendedAttributes)
 
+  // The trait keeps a child partitioning only when its keys are in the 
output: a partitioning keyed
+  // on a pass-through column survives; one keyed on a scaled DISTRIBUTE 
column (fresh ExprId) does
+  // not, because that column is absent from the output.
+  override protected def outputExpressions: Seq[NamedExpression] = output
+
   override protected def withNewChildInternal(newChild: SparkPlan): BinByExec =
     copy(child = newChild)
 
diff --git 
a/sql/core/src/test/scala/org/apache/spark/sql/execution/ProjectedOrderingAndPartitioningSuite.scala
 
b/sql/core/src/test/scala/org/apache/spark/sql/execution/ProjectedOrderingAndPartitioningSuite.scala
index a70baece7784..cb5570bfdf2c 100644
--- 
a/sql/core/src/test/scala/org/apache/spark/sql/execution/ProjectedOrderingAndPartitioningSuite.scala
+++ 
b/sql/core/src/test/scala/org/apache/spark/sql/execution/ProjectedOrderingAndPartitioningSuite.scala
@@ -25,7 +25,7 @@ import 
org.apache.spark.sql.connector.catalog.functions.{BucketFunction, YearsFu
 import org.apache.spark.sql.execution.adaptive.AdaptiveSparkPlanHelper
 import org.apache.spark.sql.internal.SQLConf
 import org.apache.spark.sql.test.SharedSparkSession
-import org.apache.spark.sql.types.{IntegerType, StringType}
+import org.apache.spark.sql.types.{DoubleType, IntegerType, StringType, 
TimestampType}
 
 class ProjectedOrderingAndPartitioningSuite
   extends SharedSparkSession with AdaptiveSparkPlanHelper {
@@ -602,6 +602,79 @@ class ProjectedOrderingAndPartitioningSuite
     }
     assert(e.getMessage.contains("partitionKeys"))
   }
+
+  test("SPARK-58138: BIN BY preserves a child partitioning on a pass-through 
column") {
+    val tsStart = AttributeReference("ts_start", TimestampType)()
+    val tsEnd = AttributeReference("ts_end", TimestampType)()
+    val value = AttributeReference("value", DoubleType)()
+    val host = AttributeReference("host", IntegerType)()
+    val binBy = BinByExec(
+      binWidthMicros = 300000000L, originMicros = 0L, rangeStart = tsStart, 
rangeEnd = tsEnd,
+      distributeColumns = Seq(value),
+      scaledDistributeColumns = Seq(AttributeReference("value", DoubleType)()),
+      appendedAttributes = Seq(
+        AttributeReference("bin_start", TimestampType)(),
+        AttributeReference("bin_end", TimestampType)(),
+        AttributeReference("bin_distribute_ratio", DoubleType)()),
+      timeZoneId = None,
+      child = DummyLeafExecWithPartitioning(
+        output = Seq(tsStart, tsEnd, value, host),
+        partitioning = HashPartitioning(Seq(host), 4)))
+
+    binBy.outputPartitioning match {
+      case p: HashPartitioning =>
+        assert(p.expressions === Seq(host))
+        assert(p.numPartitions === 4)
+      case other => fail(s"Expected HashPartitioning, got $other")
+    }
+  }
+
+  test("SPARK-58138: BIN BY drops a child partitioning on a scaled DISTRIBUTE 
column") {
+    val tsStart = AttributeReference("ts_start", TimestampType)()
+    val tsEnd = AttributeReference("ts_end", TimestampType)()
+    val value = AttributeReference("value", DoubleType)()
+    val binBy = BinByExec(
+      binWidthMicros = 300000000L, originMicros = 0L, rangeStart = tsStart, 
rangeEnd = tsEnd,
+      distributeColumns = Seq(value),
+      scaledDistributeColumns = Seq(AttributeReference("value", DoubleType)()),
+      appendedAttributes = Seq(
+        AttributeReference("bin_start", TimestampType)(),
+        AttributeReference("bin_end", TimestampType)(),
+        AttributeReference("bin_distribute_ratio", DoubleType)()),
+      timeZoneId = None,
+      child = DummyLeafExecWithPartitioning(
+        output = Seq(tsStart, tsEnd, value),
+        partitioning = HashPartitioning(Seq(value), 4)))
+
+    binBy.outputPartitioning match {
+      case p: UnknownPartitioning => assert(p.numPartitions === 4)
+      case other => fail(s"Expected UnknownPartitioning, got $other")
+    }
+  }
+
+  test("SPARK-58138: BIN BY drops a mixed pass-through and DISTRIBUTE 
partitioning whole") {
+    val tsStart = AttributeReference("ts_start", TimestampType)()
+    val tsEnd = AttributeReference("ts_end", TimestampType)()
+    val value = AttributeReference("value", DoubleType)()
+    val host = AttributeReference("host", IntegerType)()
+    val binBy = BinByExec(
+      binWidthMicros = 300000000L, originMicros = 0L, rangeStart = tsStart, 
rangeEnd = tsEnd,
+      distributeColumns = Seq(value),
+      scaledDistributeColumns = Seq(AttributeReference("value", DoubleType)()),
+      appendedAttributes = Seq(
+        AttributeReference("bin_start", TimestampType)(),
+        AttributeReference("bin_end", TimestampType)(),
+        AttributeReference("bin_distribute_ratio", DoubleType)()),
+      timeZoneId = None,
+      child = DummyLeafExecWithPartitioning(
+        output = Seq(tsStart, tsEnd, value, host),
+        partitioning = HashPartitioning(Seq(value, host), 4)))
+
+    binBy.outputPartitioning match {
+      case p: UnknownPartitioning => assert(p.numPartitions === 4)
+      case other => fail(s"Expected UnknownPartitioning, got $other")
+    }
+  }
 }
 
 private case class DummyLeafExecWithPartitioning(


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

Reply via email to