yadavay-amzn commented on code in PR #56719:
URL: https://github.com/apache/spark/pull/56719#discussion_r3583737944


##########
sql/core/src/test/scala/org/apache/spark/sql/JoinSuite.scala:
##########
@@ -58,6 +59,117 @@ class JoinSuite extends SharedSparkSession with 
AdaptiveSparkPlanHelper
     assert(planned.size === 1)
   }
 
+  test("SPARK-57648: spread rows rejected by a safe left outer join residual") 
{
+    withSQLConf(
+        SQLConf.ADAPTIVE_EXECUTION_ENABLED.key -> "false",

Review Comment:
   Non-blocking: the tests all run with AQE off, but it's on by default, and 
AQE is where re-planning and coalescing (`CoalescedNullAwareHashPartitioning`) 
happen - and where the expression tag is most likely to get dropped. Might be 
worth one case with AQE on that checks the answer and the spreading.



##########
sql/core/src/test/scala/org/apache/spark/sql/JoinSuite.scala:
##########
@@ -58,6 +59,117 @@ class JoinSuite extends SharedSparkSession with 
AdaptiveSparkPlanHelper
     assert(planned.size === 1)
   }
 
+  test("SPARK-57648: spread rows rejected by a safe left outer join residual") 
{
+    withSQLConf(
+        SQLConf.ADAPTIVE_EXECUTION_ENABLED.key -> "false",
+        SQLConf.AUTO_BROADCASTJOIN_THRESHOLD.key -> "-1",
+        SQLConf.SHUFFLE_PARTITIONS.key -> "4",
+        SQLConf.SHUFFLE_SPREAD_NULL_JOIN_KEYS_ENABLED.key -> "true") {
+      val left = Seq(
+        (1, java.lang.Boolean.TRUE, "match"),
+        (1, java.lang.Boolean.FALSE, "false"),
+        (2, null.asInstanceOf[java.lang.Boolean], "null")).toDF("k", 
"eligible", "lv").as("l")
+      val right = Seq((1, "right-1"), (2, "right-2")).toDF("k", "rv").as("r")
+      val joined = left.join(
+        right, $"l.k" === $"r.k" && $"l.eligible", "left_outer")
+
+      val sortMergeJoin = joined.queryExecution.sparkPlan.collectFirst {
+        case join: SortMergeJoinExec => join
+      }.getOrElse(fail("Expected a sort-merge join"))
+      assert(sortMergeJoin.leftKeys.size == 2)
+      assert(sortMergeJoin.rightKeys.size == 2)
+      assert(sortMergeJoin.leftKeys.last.isInstanceOf[If])
+      assert(sortMergeJoin.rightKeys.last == Literal.TrueLiteral)
+      assert(sortMergeJoin.condition.nonEmpty)
+      assert(sortMergeJoin.requiredChildDistribution.forall {
+        case ClusteredDistribution(_, true, _, _) => true
+        case _ => false
+      })
+
+      val compoundPredicate = !($"l.lv".isin("false")) ||
+        (functions.coalesce($"l.eligible", functions.lit(false)) && 
$"l.lv".isNotNull)
+      val compoundJoin = left.join(
+        right, $"l.k" === $"r.k" && compoundPredicate, "left_outer")
+      val compoundSortMergeJoin = 
compoundJoin.queryExecution.sparkPlan.collectFirst {
+        case join: SortMergeJoinExec => join
+      }.getOrElse(fail("Expected a sort-merge join"))
+      assert(compoundSortMergeJoin.leftKeys.size == 2)
+      assert(compoundSortMergeJoin.leftKeys.last.isInstanceOf[If])
+
+      checkAnswer(joined, Seq(
+        Row(1, true, "match", 1, "right-1"),
+        Row(1, false, "false", null, null),
+        Row(2, null, "null", null, null)))
+
+      val shufflePartitionings = joined.queryExecution.executedPlan.collect {
+        case exchange: ShuffleExchangeExec => exchange.outputPartitioning
+      }

Review Comment:
   Non-blocking: this checks the plan uses a 2-expression 
`NullAwareHashPartitioning`, but not that the rows actually spread - which is 
the point of the change. A version that added the guard but didn't spread would 
still pass. Maybe throw a bunch of residual-false rows on one hot key and check 
they land on more than one partition.



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/SparkStrategies.scala:
##########
@@ -286,7 +324,12 @@ abstract class SparkStrategies extends 
QueryPlanner[SparkPlan] {
         def createSortMergeJoin() = {
           if (canMerge(joinType) && RowOrdering.isOrderable(leftKeys)) {
             Some(Seq(joins.SortMergeJoinExec(
-              leftKeys, rightKeys, joinType, nonEquiCond, planLater(left), 
planLater(right))))
+              shuffledLeftKeys,

Review Comment:
   Nit: this checks `RowOrdering.isOrderable(leftKeys)` (the original keys) but 
builds the join with `shuffledLeftKeys`. Doesn't matter today since the guard 
is Boolean and always orderable, but the two would be out of sync if the guard 
type ever changed - might be cleaner to check `shuffledLeftKeys`.



##########
sql/core/src/test/scala/org/apache/spark/sql/JoinSuite.scala:
##########
@@ -58,6 +59,117 @@ class JoinSuite extends SharedSparkSession with 
AdaptiveSparkPlanHelper
     assert(planned.size === 1)
   }
 
+  test("SPARK-57648: spread rows rejected by a safe left outer join residual") 
{
+    withSQLConf(
+        SQLConf.ADAPTIVE_EXECUTION_ENABLED.key -> "false",
+        SQLConf.AUTO_BROADCASTJOIN_THRESHOLD.key -> "-1",
+        SQLConf.SHUFFLE_PARTITIONS.key -> "4",
+        SQLConf.SHUFFLE_SPREAD_NULL_JOIN_KEYS_ENABLED.key -> "true") {
+      val left = Seq(
+        (1, java.lang.Boolean.TRUE, "match"),
+        (1, java.lang.Boolean.FALSE, "false"),
+        (2, null.asInstanceOf[java.lang.Boolean], "null")).toDF("k", 
"eligible", "lv").as("l")
+      val right = Seq((1, "right-1"), (2, "right-2")).toDF("k", "rv").as("r")
+      val joined = left.join(
+        right, $"l.k" === $"r.k" && $"l.eligible", "left_outer")
+
+      val sortMergeJoin = joined.queryExecution.sparkPlan.collectFirst {
+        case join: SortMergeJoinExec => join
+      }.getOrElse(fail("Expected a sort-merge join"))
+      assert(sortMergeJoin.leftKeys.size == 2)
+      assert(sortMergeJoin.rightKeys.size == 2)
+      assert(sortMergeJoin.leftKeys.last.isInstanceOf[If])
+      assert(sortMergeJoin.rightKeys.last == Literal.TrueLiteral)
+      assert(sortMergeJoin.condition.nonEmpty)
+      assert(sortMergeJoin.requiredChildDistribution.forall {
+        case ClusteredDistribution(_, true, _, _) => true
+        case _ => false
+      })
+
+      val compoundPredicate = !($"l.lv".isin("false")) ||
+        (functions.coalesce($"l.eligible", functions.lit(false)) && 
$"l.lv".isNotNull)
+      val compoundJoin = left.join(
+        right, $"l.k" === $"r.k" && compoundPredicate, "left_outer")
+      val compoundSortMergeJoin = 
compoundJoin.queryExecution.sparkPlan.collectFirst {
+        case join: SortMergeJoinExec => join
+      }.getOrElse(fail("Expected a sort-merge join"))
+      assert(compoundSortMergeJoin.leftKeys.size == 2)
+      assert(compoundSortMergeJoin.leftKeys.last.isInstanceOf[If])
+
+      checkAnswer(joined, Seq(
+        Row(1, true, "match", 1, "right-1"),
+        Row(1, false, "false", null, null),
+        Row(2, null, "null", null, null)))
+
+      val shufflePartitionings = joined.queryExecution.executedPlan.collect {
+        case exchange: ShuffleExchangeExec => exchange.outputPartitioning
+      }
+      assert(shufflePartitionings.size == 2)
+      assert(shufflePartitionings.forall {
+        case NullAwareHashPartitioning(expressions, _) => expressions.size == 2
+        case _ => false
+      })
+    }
+  }
+
+  test("SPARK-57648: spread unmatchable rows for shuffled hash join") {
+    withSQLConf(
+        SQLConf.ADAPTIVE_EXECUTION_ENABLED.key -> "false",
+        SQLConf.AUTO_BROADCASTJOIN_THRESHOLD.key -> "-1",
+        SQLConf.SHUFFLE_PARTITIONS.key -> "4",
+        SQLConf.SHUFFLE_SPREAD_NULL_JOIN_KEYS_ENABLED.key -> "true") {
+      val left = Seq((1, true), (1, false)).toDF("k", "eligible").as("l")
+      val right = Seq((1, "right-1")).toDF("k", 
"rv").as("r").hint("SHUFFLE_HASH")
+      val joined = left.join(
+        right, $"l.k" === $"r.k" && $"l.eligible", "left_outer")
+

Review Comment:
   Non-blocking: every test has one right row per key, so we don't cover a 
match producing multiple rows. Something like left `(1,true)` against right 
`(1,"a"),(1,"b")` expecting two rows would confirm the guard doesn't break the 
matched fan-out.



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