vranes commented on code in PR #59102:
URL: https://github.com/apache/spark/pull/59102#discussion_r4133628651


##########
sql/core/src/test/scala/org/apache/spark/sql/AsOfJoinSortMergeSQLSuite.scala:
##########
@@ -204,6 +204,26 @@ class AsOfJoinSortMergeSQLSuite extends QueryTest
       Row(180.15) :: Nil)
   }
 
+  test("TIMESTAMP MATCH_CONDITION with legacy intervals") {
+    // TIMESTAMP - TIMESTAMP is then a CalendarInterval, which has no 
ordering. Backward and

Review Comment:
   nit: This comment should be simplified or removed.



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/joins/SortMergeAsOfJoinExec.scala:
##########
@@ -207,7 +197,8 @@ private[joins] class SortMergeAsOfJoinScanner(
   private val leftKeyProj = UnsafeProjection.create(leftKeys, leftOutput)
   private val rightKeyProj = UnsafeProjection.create(rightKeys, rightOutput)
 
-  private val distanceOrdering =
+  // Lazy for the same reason as boundOrderExpr.
+  private lazy val distanceOrdering =

Review Comment:
   nit: Can `distanceOrdering` be next to `boundOrderExpr`? That way we can 
have just one comment for both of them.



##########
sql/core/src/test/scala/org/apache/spark/sql/AsOfJoinSortMergeSQLSuite.scala:
##########
@@ -204,6 +204,26 @@ class AsOfJoinSortMergeSQLSuite extends QueryTest
       Row(180.15) :: Nil)
   }
 
+  test("TIMESTAMP MATCH_CONDITION with legacy intervals") {
+    // TIMESTAMP - TIMESTAMP is then a CalendarInterval, which has no 
ordering. Backward and
+    // Forward never read that distance, so they must still run.
+    withSQLConf(SQLConf.LEGACY_INTERVAL_ENABLED.key -> "true") {
+      for ((op, tag) <- Seq(">=" -> "r9", ">" -> "r9", "<=" -> "r11", "<" -> 
"r11")) {

Review Comment:
   This test has only one qualifying row per direction, so its coverage is 
limited. Could it have multiple qualifying rows, including an exact match?



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/joins/SortMergeAsOfJoinExec.scala:
##########
@@ -92,34 +92,26 @@ case class SortMergeAsOfJoinExec(
 
   override def outputOrdering: Seq[SortOrder] = left.outputOrdering
 
-  // Backward joins scan the right-side buffer forward and keep the last match.
-  // Detect from asOfCondition so composite MATCH_CONDITION sort keys still 
work.
-  private val isBackwardJoin: Boolean =
-    isBackwardAsOfCondition(asOfCondition, left.output, right.output)
-
-  private def isBackwardAsOfCondition(
-      condition: Expression,
-      leftOutput: Seq[Attribute],
-      rightOutput: Seq[Attribute]): Boolean = {
-    val leftAttrs = AttributeSet(leftOutput)
-    val rightAttrs = AttributeSet(rightOutput)
-    def isBackwardPredicate(expr: Expression): Boolean = expr match {
-      case GreaterThanOrEqual(l, r)
-          if l.references.subsetOf(leftAttrs) && 
r.references.subsetOf(rightAttrs) =>
-        true
-      case GreaterThan(l, r)
-          if l.references.subsetOf(leftAttrs) && 
r.references.subsetOf(rightAttrs) =>
-        true
-      case And(c, _) => isBackwardPredicate(c)
-      case _ => false
+  // Detect from asOfCondition so composite MATCH_CONDITION sort keys still 
work. Only a first
+  // `left op right` conjunct counts. Nearest has none: its condition is TRUE, 
NOT(l = r), or
+  // starts with `right >= left - tolerance`.

Review Comment:
   nit: This comment should be simplified or removed



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/joins/SortMergeAsOfJoinExec.scala:
##########
@@ -424,10 +406,38 @@ private[joins] class SortMergeAsOfJoinScanner(
   }
 
   /**
-   * Forward scan for Forward/Nearest joins: distance-based termination.
-   * Stop when distance starts increasing past the minimum found so far.
+   * Forward joins: returns the first row that passes both conditions. The 
buffer is sorted
+   * ascending, so that row is the closest and no distance is needed. The scan 
stops once the
+   * as-of condition turns false after being true, since it stays false after 
that.
    */
-  private def findBestForwardNearest(leftRow: InternalRow): InternalRow = {
+  private def findFirstForward(leftRow: InternalRow): InternalRow = {
+    val iter = rightGroupBuffer.generateIterator()
+    var asOfSeen = false
+
+    joinedRow.withLeft(leftRow)
+    while (iter.hasNext) {
+      val rightRow = iter.next()
+      joinedRow.withRight(rightRow)
+
+      if (holds(boundAsOfCond)) {
+        asOfSeen = true
+        if (residualHolds) {
+          // No copy: the caller projects this row before the buffer iterator 
moves again.
+          return rightRow
+        }
+      } else if (asOfSeen) {
+        // Past the tolerance bound, so no later row passes the as-of 
condition.
+        return null
+      }
+    }
+    null
+  }
+
+  /**
+   * Nearest joins: keeps the row with the smallest distance. The scan stops 
once the
+   * distance starts to grow past the minimum found so far.

Review Comment:
   nit: This should say "once the distance stops decreasing", since the code 
also stops on a tie.



##########
sql/core/src/test/scala/org/apache/spark/sql/AsOfJoinSortMergeSQLSuite.scala:
##########
@@ -728,6 +748,111 @@ class AsOfJoinSortMergeSQLSuite extends QueryTest
       Row(Seq(1, 6)) :: Nil)
   }
 
+  test("forward MATCH_CONDITION with NULL array elements picks the smallest 
right row") {
+    // NULL elements sort first: [null] < [null, null] < [5].
+    val nullInt = "CAST(NULL AS INT)"
+    for {
+      (left, rights) <- Seq(

Review Comment:
   nit: Could one-item Seq generators in these tests be plain values? The 
operator loops can stay.



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/joins/SortMergeAsOfJoinExec.scala:
##########
@@ -424,10 +406,38 @@ private[joins] class SortMergeAsOfJoinScanner(
   }
 
   /**
-   * Forward scan for Forward/Nearest joins: distance-based termination.
-   * Stop when distance starts increasing past the minimum found so far.
+   * Forward joins: returns the first row that passes both conditions. The 
buffer is sorted

Review Comment:
   nit: No need to mention the distance approach here.
   
   Proposed comment:
   
   ```suggestion
     * Forward joins return the first row that passes the as-of and residual 
conditions.
      * With a tolerance, if the scan enters the allowed range but finds no 
match, it
      * stops at the first row beyond that range, as later sorted rows cannot 
qualify.
   ```
   
   Backward coment:
   
   ```suggestion
     /**
      * Backward joins keep the last row that passes the as-of and residual 
conditions.
      * After saving a match, the scan stops at the first row that fails the 
as-of
      * condition, as later sorted rows cannot satisfy it.
      */
   ```



##########
sql/core/src/test/scala/org/apache/spark/sql/SortMergeAsOfJoinSuite.scala:
##########
@@ -685,6 +685,26 @@ class SortMergeAsOfJoinSuite extends QueryTest
     )
   }
 
+  test("forward join - residual rejects every row within the tolerance") {

Review Comment:
   Could we add a left row (4, 30) to this test? It should match the existing 
right row (6, 20), and the current (5, 10) row remains without a match.



##########
sql/core/src/test/scala/org/apache/spark/sql/AsOfJoinSortMergeSQLSuite.scala:
##########
@@ -728,6 +748,111 @@ class AsOfJoinSortMergeSQLSuite extends QueryTest
       Row(Seq(1, 6)) :: Nil)
   }
 
+  test("forward MATCH_CONDITION with NULL array elements picks the smallest 
right row") {
+    // NULL elements sort first: [null] < [null, null] < [5].
+    val nullInt = "CAST(NULL AS INT)"
+    for {
+      (left, rights) <- Seq(
+        s"ARRAY($nullInt)" -> s"(ARRAY($nullInt, $nullInt), 'nn'), (ARRAY(5), 
'five')")
+      op <- Seq("<=", "<")
+    } {
+      checkSortMergeAsOf(
+        sql(
+          s"""
+             |SELECT r.tag
+             |FROM VALUES ($left) AS t(a)
+             |ASOF JOIN VALUES $rights AS r(a, tag)
+             |  MATCH_CONDITION (t.a $op r.a)
+             |""".stripMargin),
+        Row("nn") :: Nil)
+    }
+  }
+
+  test("forward MATCH_CONDITION with a NULL struct field picks the smallest 
right row") {
+    // A NULL field sorts first, so {null, 7} < {x, 1}.
+    for {
+      (nullField, value) <- Seq("CAST(NULL AS INT)" -> "0")
+      (left, right) <- Seq("t.k" -> "r.k", "(t.k.f, t.k.seq)" -> "(r.k.f, 
r.k.seq)")
+      op <- Seq("<=", "<")
+    } {
+      checkSortMergeAsOf(
+        sql(
+          s"""
+             |SELECT r.tag
+             |FROM VALUES (named_struct('f', $nullField, 'seq', 5)) AS t(k)
+             |ASOF JOIN VALUES
+             |  (named_struct('f', $nullField, 'seq', 7), 's7'),
+             |  (named_struct('f', $value, 'seq', 1), 's1') AS r(k, tag)
+             |  MATCH_CONDITION ($left $op $right)
+             |""".stripMargin),
+        Row("s7") :: Nil)
+    }
+  }
+
+  test("forward MATCH_CONDITION with a leading STRING field picks the smallest 
right row") {
+    // {c, 1} < {d, 0}. The STRING field distance is 1 for both, so a distance 
picks {d, 0}.
+    for {
+      (left, right) <- Seq("t.k" -> "r.k", "(t.k.s, t.k.n)" -> "(r.k.s, 
r.k.n)")
+      op <- Seq("<=", "<")
+    } {
+      checkSortMergeAsOf(
+        sql(
+          s"""
+             |SELECT r.tag
+             |FROM VALUES (named_struct('s', 'b', 'n', 5)) AS t(k)
+             |ASOF JOIN VALUES
+             |  (named_struct('s', 'c', 'n', 1), 'c1'),
+             |  (named_struct('s', 'd', 'n', 0), 'd0') AS r(k, tag)
+             |  MATCH_CONDITION ($left $op $right)
+             |""".stripMargin),
+        Row("c1") :: Nil)
+    }
+  }
+
+  test("forward MATCH_CONDITION skips a closer row that fails a non-equi ON 
predicate") {
+    checkSortMergeAsOf(
+      sql(
+        """
+          |SELECT r.k
+          |FROM VALUES (10, 5) AS t(k, lim)
+          |ASOF JOIN VALUES (12, 9), (15, 1), (20, 0) AS r(k, v)
+          |  MATCH_CONDITION (t.k <= r.k)
+          |  ON r.v < t.lim
+          |""".stripMargin),
+      Row(15) :: Nil)
+  }
+
+  // The far row makes (right - left) too big for the type, so a 
distance-based scan would fail.
+  Seq(
+    ("INT", "-100", "-50", "2147483647"),
+    ("BIGINT", "-100", "-50", "9223372036854775807"),
+    ("DECIMAL(38, 0)", "-99999999999999999999999999999999999999", "1", "2"),

Review Comment:
   nit: This test fails on the near row first, not the far one like the comment 
says.



##########
sql/core/src/test/scala/org/apache/spark/sql/AsOfJoinSortMergeSQLSuite.scala:
##########
@@ -728,6 +748,111 @@ class AsOfJoinSortMergeSQLSuite extends QueryTest
       Row(Seq(1, 6)) :: Nil)
   }
 
+  test("forward MATCH_CONDITION with NULL array elements picks the smallest 
right row") {

Review Comment:
   For this and 2 cases below: could we include a right key equal to the left 
key to distinguish <= from < properly?



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/joins/SortMergeAsOfJoinExec.scala:
##########
@@ -189,7 +177,9 @@ private[joins] class SortMergeAsOfJoinScanner(
   }
 
   private val boundAsOfCond = bindReference(asOfCondition, joinedOutput)
-  private val boundOrderExpr = bindReference(orderExpression, joinedOutput)
+  // Lazy, since only Nearest reads the distance. Its type can have no 
ordering, for example
+  // CalendarInterval from TIMESTAMP - TIMESTAMP with 
spark.sql.legacy.interval.enabled.

Review Comment:
   nit: Remove "Its type can have no ordering, for example CalendarInterval 
from TIMESTAMP - TIMESTAMP with spark.sql.legacy.interval.enabled."



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