peter-toth commented on code in PR #58979:
URL: https://github.com/apache/spark/pull/58979#discussion_r4132086733


##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/V2ExpressionUtils.scala:
##########
@@ -113,25 +128,35 @@ object V2ExpressionUtils extends SQLConfHelper with 
Logging {
       expr: V2Expression,
       query: LogicalPlan,
       funCatalogOpt: Option[FunctionCatalog] = None): Expression =
-    toCatalystOpt(expr, query, funCatalogOpt)
+    toCatalystCore(expr, query, funCatalogOpt, failOnUnresolved = true)
         .getOrElse(throw new AnalysisException(
           errorClass = "_LEGACY_ERROR_TEMP_3054", messageParameters = 
Map("expr" -> expr.toString)))
 
   def toCatalystOpt(
       expr: V2Expression,
       query: LogicalPlan,
-      funCatalogOpt: Option[FunctionCatalog] = None): Option[Expression] = {
+      funCatalogOpt: Option[FunctionCatalog] = None): Option[Expression] =
+    toCatalystCore(expr, query, funCatalogOpt, failOnUnresolved = false)

Review Comment:
   **Finding 1.** The precision fix from 
[r4117400462](https://github.com/apache/spark/pull/58979#discussion_r4117400462)
 landed on `resolveRefOpt`, which is `private[sql]`. `toCatalystOpt` and 
`toCatalystTransformOpt` are the public entry points, and they carry no 
scaladoc, so the split contract is only discoverable three levels down.
   
   Measured on this head:
   
   ```
   toCatalystOpt(FieldReference("missing"), plan)               -> None
   toCatalystOpt(FieldReference("s.missing"), plan)             -> throws 
[FIELD_NOT_FOUND]
   resolveRefOpt(FieldReference("a"), relationWithTwoAs)        -> throws 
[AMBIGUOUS_REFERENCE]
   toCatalystOpt(GeneralScalarExpression("+", [id, 1]), plan)   -> throws "id + 
1 is not currently supported"
   ```
   
   This is not hypothetical. The rule in this PR had to wrap `resolveRefOpt` in 
`try { ... } catch { case _: AnalysisException => true }` for exactly that 
reason, and the next caller will read `toCatalystOpt` the way its name invites. 
A couple of lines each:
   
   ```scala
     /**
      * Variant of `toCatalyst` that returns `None` when a reference cannot be 
resolved. It still
      * throws for a missing nested field or an ambiguous reference (see 
`resolveRefOpt`), and for an
      * expression type this conversion does not handle.
      */
     def toCatalystOpt(
   ```
   
   and the same on `toCatalystTransformOpt`.
   



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/V2ScanPartitioningAndOrdering.scala:
##########
@@ -75,8 +87,34 @@ object V2ScanPartitioningAndOrdering extends 
Rule[LogicalPlan] with Logging {
   private def ordering(plan: LogicalPlan) = plan.transformDownWithPruning(
       _.containsPattern(DATA_SOURCE_V2_SCAN_RELATION)) {
     case d @ ExtractV2ScanInfo(relation, scan: SupportsReportOrdering, _) =>
-      val ordering =
-        V2ExpressionUtils.toCatalystOrdering(scan.outputOrdering(), relation, 
relation.funCatalog)
-      d.copy(ordering = Some(ordering))
+      val unresolvedColumns =
+        unresolvableColumns(scan.outputOrdering().toImmutableArraySeq, 
relation)
+      if (unresolvedColumns.nonEmpty) {
+        logWarning(
+          log"Spark ignores the reported ordering because the ordering columns 
cannot be " +
+            log"resolved: ${MDC(COLUMN_NAMES, unresolvedColumns.mkString(", 
"))}.")
+        d.copy(ordering = None)
+      } else {
+        val ordering =
+          V2ExpressionUtils.toCatalystOrdering(scan.outputOrdering(), 
relation, relation.funCatalog)
+        d.copy(ordering = Some(ordering))
+      }
+  }
+
+  private def unresolvableColumns(
+      exprs: Seq[V2Expression],
+      relation: LogicalPlan): Seq[String] = {
+    def references(expr: V2Expression): Seq[NamedReference] = expr match {
+      case ref: NamedReference => Seq(ref)
+      case other => other.children().toImmutableArraySeq.flatMap(references)
+    }
+    exprs.flatMap(references).distinct.filter { ref =>
+      try {
+        V2ExpressionUtils.resolveRefOpt[NamedExpression](ref, relation).isEmpty
+      } catch {
+        // A missing nested field or an ambiguous name throws instead of 
returning None.
+        case _: AnalysisException => true

Review Comment:
   **Finding 2.** This `catch` absorbs two distinct throws from 
`resolveRefOpt`, and only one of them is exercised. The end-to-end test covers 
`s.missing` (`FIELD_NOT_FOUND`); the ambiguous-reference arm has no test in 
this suite or in `V2ExpressionUtilsSuite`, although the `resolveRefOpt` 
scaladoc and the PR description both name it.
   
   Building a v2 relation with two same-named columns to reach it end to end 
looks like more trouble than it is worth, so the cheap place is 
`V2ExpressionUtilsSuite`, pinning the precondition this `catch` rests on:
   
   ```scala
     test("SPARK-59721: resolveRefOpt throws for an ambiguous reference") {
       val dup = LocalRelation.apply(
         AttributeReference("a", StringType)(), AttributeReference("a", 
StringType)())
       intercept[AnalysisException] {
         V2ExpressionUtils.resolveRefOpt[NamedExpression](FieldReference("a"), 
dup)
       }
     }
   ```
   
   I ran that relation on this head and it throws ``[AMBIGUOUS_REFERENCE] 
Reference `a` is ambiguous, could be: [`a`, `a`]``. `checkError` with 
`condition = "AMBIGUOUS_REFERENCE"` would be the better assertion; I did not 
run it, so the parameter map would need checking.
   



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