LukaZdravic commented on code in PR #58823:
URL: https://github.com/apache/spark/pull/58823#discussion_r4018054540


##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/plans/logical/basicLogicalOperators.scala:
##########
@@ -2688,12 +2688,39 @@ object AsOfJoin {
       rightOperand: Expression,
       normalizedOp: MatchComparisonOperator)
       : (Expression, Expression, Seq[Expression], Seq[Expression]) = {
+    val (coercedLeft, coercedRight) = coerceMatchLeafOperands(leftOperand, 
rightOperand)
     val (asOfCondition, orderExpression) =
-      buildMatchExpressions(leftOperand, rightOperand, normalizedOp)
-    val (leftSortExprs, rightSortExprs) = matchSortExpressions(leftOperand, 
rightOperand)
+      buildMatchExpressions(coercedLeft, coercedRight, normalizedOp)
+    val (leftSortExprs, rightSortExprs) = matchSortExpressions(coercedLeft, 
coercedRight)
     (asOfCondition, orderExpression, leftSortExprs, rightSortExprs)
   }
 
+  /**
+   * Casts a scalar operand pair to the common type the comparison operator 
would use, so the
+   * comparison, ordering distance, and per-side sort keys all agree. This is 
required for a
+   * string vs DATE/TIMESTAMP/number pair: the sort-merge scan sorts the right 
buffer by its raw
+   * sort key, and only a shared type keeps that order consistent with the 
coerced comparison.
+   * STRUCT and ARRAY operands are left untouched; their sort key is the whole 
value, so a
+   * per-field cast could not reach it.

Review Comment:
   Both modes are covered end-to-end in `AsOfJoinSQLSuite`: the `DATE vs 
STRING` and `INT vs STRING` tests each run under `ansi=true` and `ansi=false` 
via `withSQLConf`, and the INT case asserts the per-mode sort-key type (LONG 
under ANSI, INT otherwise), which pins the mode-dependent common-type 
selection. Those tests set the conf explicitly, so they do not depend on the 
SQLConf default — a default change would not hide a regression. I left the 
catalyst unit assertions as-is to avoid duplicating that coverage.
   



##########
sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/plans/logical/AsOfJoinMatchConditionTypesSuite.scala:
##########
@@ -29,9 +29,39 @@ class AsOfJoinMatchConditionTypesSuite extends SparkFunSuite 
{
     assert(!MatchConditionTypes.usesStructDecomposition(IntegerType, LongType))
   }
 
-  test("string and temporal types are incompatible") {
-    assert(!MatchConditionTypes.areOperandsCompatible(StringType, 
TimestampType))
-    assert(!MatchConditionTypes.areOperandsCompatible(DateType, StringType))
+  test("scalar string and temporal types coerce like the comparison operator") 
{
+    // SPARK-59527: a scalar string vs DATE/TIMESTAMP pair is accepted and 
coerced, matching `>=`.
+    assert(MatchConditionTypes.areOperandsCompatible(StringType, 
TimestampType))
+    assert(MatchConditionTypes.areOperandsCompatible(DateType, StringType))
+    // The shared common type is the temporal type (string is cast to it), not 
string, so the
+    // sort-merge sort key and the comparison order agree.
+    assert(MatchConditionTypes.matchComparisonCommonType(DateType, 
StringType).contains(DateType))
+    assert(
+      MatchConditionTypes.matchComparisonCommonType(StringType, TimestampType)
+        .contains(TimestampType))
+  }

Review Comment:
   `areFieldTypesCompatible` is also called recursively on struct fields and 
array elements (via `arePositionalStructsCompatible` / 
`areArrayElementsCompatible`), which the top-level guard in 
`areOperandsCompatible` has not validated — so it has to keep its own 
`isValidOperandType` check. The outer guard is still needed for the scalar 
branches, because `matchComparisonCommonType` / `findWiderTypeForTwo` do not 
verify operand orderability themselves. Neither check is safely removable, so I 
kept both; the top-level double-check on composite operands is a cheap, 
intentional overlap.
   



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/plans/logical/basicLogicalOperators.scala:
##########
@@ -2703,7 +2730,56 @@ object AsOfJoin {
     def isValidOperandType(dataType: DataType): Boolean =
       RowOrdering.isOrderable(dataType) && !containsEmptyStructType(dataType)
 
+    /**
+     * Top-level operand compatibility. A scalar pair is compatible when the 
comparison operator
+     * would accept it, so DATE/TIMESTAMP vs STRING (and string vs number) 
coerce like `>=`.
+     * STRUCT/ARRAY operands keep the stricter widening rule via 
[[areFieldTypesCompatible]],
+     * because their sort key is the whole value and cannot carry a per-field 
string cast.
+     */
     def areOperandsCompatible(leftType: DataType, rightType: DataType): 
Boolean = {
+      if (!isValidOperandType(leftType) || !isValidOperandType(rightType)) {
+        false
+      } else {
+        (leftType, rightType) match {
+          case (_: StructType, _) | (_, _: StructType) | (_: ArrayType, _) | 
(_, _: ArrayType) =>
+            areFieldTypesCompatible(leftType, rightType)
+          case _ =>
+            matchComparisonCommonType(leftType, rightType).isDefined ||
+              TypeCoercion.findWiderTypeForTwo(leftType, rightType).isDefined
+        }

Review Comment:
   `collectFirst { case j: AsOfJoin => j }.get` is the established pattern 
throughout `AsOfJoinSQLSuite` (every analysis test uses it), so I kept it for 
consistency with the surrounding tests. If the suite owners prefer 
`getOrElse(fail(...))`, I am happy to switch them all in a follow-up.
   



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