dongjoon-hyun commented on code in PR #58979:
URL: https://github.com/apache/spark/pull/58979#discussion_r4077137405


##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/V2ExpressionUtils.scala:
##########
@@ -39,21 +39,30 @@ import org.apache.spark.sql.errors.QueryCompilationErrors
 import org.apache.spark.sql.internal.connector.PartitionPredicateImpl
 import org.apache.spark.sql.types._
 import org.apache.spark.util.ArrayImplicits._
+import org.apache.spark.util.collection.Utils.sequenceToOption
 
 /**
  * A utility class that converts public connector expressions into Catalyst 
expressions.
  */
 object V2ExpressionUtils extends SQLConfHelper with Logging {
   import 
org.apache.spark.sql.connector.catalog.CatalogV2Implicits.MultipartIdentifierHelper
 
+  /**
+   * Non-throwing counterpart to `resolveRef`. Callers that need to gracefully 
degrade when a
+   * reference cannot be resolved (e.g. 
`toCatalystOpt`/`toCatalystTransformOpt`, which a reported
+   * `KeyGroupedPartitioning` relies on to fall back to unknown partitioning 
instead of failing the
+   * query) must use this, not `resolveRef` -- calling the throwing form there 
defeats that
+   * fallback entirely.
+   */
+  def resolveRefOpt[T <: NamedExpression](ref: NamedReference, plan: 
LogicalPlan): Option[T] = {

Review Comment:
   nit. This scaladoc describes the caller-side details at length. A one or two 
line description would be enough, e.g., `Non-throwing variant of resolveRef. 
Returns None if the reference cannot be resolved.` In addition, can we make 
this `private[sql]`?



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/V2ExpressionUtils.scala:
##########
@@ -144,25 +153,23 @@ object V2ExpressionUtils extends SQLConfHelper with 
Logging {
       query: LogicalPlan,
       funCatalogOpt: Option[FunctionCatalog] = None): Option[Expression] = 
trans match {
     case IdentityTransform(ref) =>
-      Some(resolveRef[NamedExpression](ref, query))
+      resolveRefOpt[NamedExpression](ref, query)
     case BucketTransform(numBuckets, refs, sorted)
         if sorted.isEmpty && refs.length == 1 && 
refs.forall(_.isInstanceOf[NamedReference]) =>
-      val resolvedRefs = refs.map(r => resolveRef[NamedExpression](r, query))
       // Create a dummy reference for `numBuckets` here and use that, together 
with `refs`, to
       // look up the V2 function.
       val numBucketsRef = AttributeReference("numBuckets", IntegerType, 
nullable = false)()
-      funCatalogOpt.flatMap { catalog =>
-        loadV2FunctionOpt(catalog, "bucket", Seq(numBucketsRef) ++ 
resolvedRefs).map { bound =>
-          TransformExpression(bound, resolvedRefs, Some(numBuckets))
-        }
-      }
+      for {
+        resolvedRefs <- sequenceToOption(refs.map(r => 
resolveRefOpt[NamedExpression](r, query)))
+        catalog <- funCatalogOpt
+        bound <- loadV2FunctionOpt(catalog, "bucket", Seq(numBucketsRef) ++ 
resolvedRefs)
+      } yield TransformExpression(bound, resolvedRefs, Some(numBuckets))
     case NamedTransform(name, args) =>
-      val catalystArgs = args.map(toCatalyst(_, query, funCatalogOpt))
-      funCatalogOpt.flatMap { catalog =>
-        loadV2FunctionOpt(catalog, name, catalystArgs).map { bound =>
-          TransformExpression(bound, catalystArgs)
-        }
-      }
+      for {
+        catalystArgs <- sequenceToOption(args.map(toCatalystOpt(_, query, 
funCatalogOpt)))

Review Comment:
   This switches `toCatalyst` to `toCatalystOpt` for the arguments, so not only 
unresolvable references but also nested transforms whose function cannot be 
loaded now return `None` instead of throwing. It seems intended, but could you 
mention it in the PR description? Also, unsupported expression types still 
throw from `case _ =>` in `toCatalystOpt`, so the graceful degradation is 
partial.



##########
sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/V2ExpressionUtilsSuite.scala:
##########
@@ -25,16 +25,46 @@ import org.apache.spark.sql.types.StringType
 
 class V2ExpressionUtilsSuite extends SparkFunSuite {
 
+  private val plan = LocalRelation.apply(AttributeReference("a", StringType)())
+
   test("SPARK-39313: toCatalystOrdering should fail if V2Expression can not be 
translated") {
     val supportedV2Sort = SortValue(
       FieldReference("a"), SortDirection.ASCENDING, NullOrdering.NULLS_FIRST)
     val unsupportedV2Sort = supportedV2Sort.copy(
       expression = ApplyTransform("v2Fun", FieldReference("a") :: Nil))
     val exc = intercept[AnalysisException] {
-      V2ExpressionUtils.toCatalystOrdering(
-        Array(supportedV2Sort, unsupportedV2Sort),
-        LocalRelation.apply(AttributeReference("a", StringType)()))
+      V2ExpressionUtils.toCatalystOrdering(Array(supportedV2Sort, 
unsupportedV2Sort), plan)
     }
     assert(exc.message.contains("v2Fun(a) ASC NULLS FIRST is not currently 
supported"))
   }
+
+  test("toCatalystOpt returns None, not throw, for an unresolvable 
FieldReference") {
+    assert(V2ExpressionUtils.toCatalystOpt(FieldReference("missing"), 
plan).isEmpty)
+  }
+
+  test("toCatalystOpt still resolves an existing FieldReference") {

Review Comment:
   nit. This test case looks trivial. Do we need it?



##########
sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/V2ExpressionUtilsSuite.scala:
##########
@@ -25,16 +25,46 @@ import org.apache.spark.sql.types.StringType
 
 class V2ExpressionUtilsSuite extends SparkFunSuite {
 
+  private val plan = LocalRelation.apply(AttributeReference("a", StringType)())
+
   test("SPARK-39313: toCatalystOrdering should fail if V2Expression can not be 
translated") {
     val supportedV2Sort = SortValue(
       FieldReference("a"), SortDirection.ASCENDING, NullOrdering.NULLS_FIRST)
     val unsupportedV2Sort = supportedV2Sort.copy(
       expression = ApplyTransform("v2Fun", FieldReference("a") :: Nil))
     val exc = intercept[AnalysisException] {
-      V2ExpressionUtils.toCatalystOrdering(
-        Array(supportedV2Sort, unsupportedV2Sort),
-        LocalRelation.apply(AttributeReference("a", StringType)()))
+      V2ExpressionUtils.toCatalystOrdering(Array(supportedV2Sort, 
unsupportedV2Sort), plan)
     }
     assert(exc.message.contains("v2Fun(a) ASC NULLS FIRST is not currently 
supported"))
   }
+
+  test("toCatalystOpt returns None, not throw, for an unresolvable 
FieldReference") {

Review Comment:
   Please add the JIRA ID prefix to the new test names, e.g., 
`test("SPARK-59721: ...")`.



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/V2ExpressionUtils.scala:
##########
@@ -131,7 +140,7 @@ object V2ExpressionUtils extends SQLConfHelper with Logging 
{
           SortOrder(catalystChild, toCatalyst(direction), 
toCatalyst(nullOrdering), Seq.empty)
         }
       case ref: FieldReference =>
-        Some(resolveRef[NamedExpression](ref, query))
+        resolveRefOpt[NamedExpression](ref, query)

Review Comment:
   This changes the error of `toCatalyst`/`toCatalystOrdering` too, because 
`toCatalyst` is `toCatalystOpt(...).getOrElse(throw _LEGACY_ERROR_TEMP_3054)`. 
The impact is broader than `toCatalystOrdering` mentioned in the PR description:
   - `DistributionAndOrderingUtils` (write path): the clustering of 
`requiredDistribution` via `toCatalyst` and `requiredOrdering` via 
`toCatalystOrdering`
   - `V2ScanPartitioningAndOrdering.ordering()`: `scan.outputOrdering()`
   
   For writes, when a connector requires a non-existent column, the error 
changes from `_LEGACY_ERROR_TEMP_1137` (`Unable to resolve x given [a,b]`) to 
`_LEGACY_ERROR_TEMP_3054` (`x is not currently supported`), which is misleading.
   
   Could you keep the specific error for the throwing APIs? For example, a 
private helper with a `failOnUnresolved` flag which 
`toCatalyst`/`toCatalystOrdering` use, while only the `*Opt` variants return 
`None`. Note that adding a `FieldReference` case to `toCatalyst` alone is not 
enough because `SortValue` recurses into `toCatalystOpt` for its child.



##########
sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/V2ExpressionUtilsSuite.scala:
##########
@@ -25,16 +25,46 @@ import org.apache.spark.sql.types.StringType
 
 class V2ExpressionUtilsSuite extends SparkFunSuite {
 
+  private val plan = LocalRelation.apply(AttributeReference("a", StringType)())
+
   test("SPARK-39313: toCatalystOrdering should fail if V2Expression can not be 
translated") {
     val supportedV2Sort = SortValue(
       FieldReference("a"), SortDirection.ASCENDING, NullOrdering.NULLS_FIRST)
     val unsupportedV2Sort = supportedV2Sort.copy(
       expression = ApplyTransform("v2Fun", FieldReference("a") :: Nil))
     val exc = intercept[AnalysisException] {
-      V2ExpressionUtils.toCatalystOrdering(
-        Array(supportedV2Sort, unsupportedV2Sort),
-        LocalRelation.apply(AttributeReference("a", StringType)()))
+      V2ExpressionUtils.toCatalystOrdering(Array(supportedV2Sort, 
unsupportedV2Sort), plan)
     }
     assert(exc.message.contains("v2Fun(a) ASC NULLS FIRST is not currently 
supported"))
   }
+
+  test("toCatalystOpt returns None, not throw, for an unresolvable 
FieldReference") {
+    assert(V2ExpressionUtils.toCatalystOpt(FieldReference("missing"), 
plan).isEmpty)
+  }
+
+  test("toCatalystOpt still resolves an existing FieldReference") {
+    assert(V2ExpressionUtils.toCatalystOpt(FieldReference("a"), 
plan).isDefined)
+  }
+
+  test("toCatalystTransformOpt returns None for an unresolvable 
IdentityTransform") {
+    
assert(V2ExpressionUtils.toCatalystTransformOpt(Expressions.identity("missing"),
 plan).isEmpty)
+  }
+
+  test("toCatalystTransformOpt returns None for an unresolvable 
BucketTransform ref") {
+    assert(V2ExpressionUtils.toCatalystTransformOpt(Expressions.bucket(4, 
"missing"), plan).isEmpty)

Review Comment:
   Since `funCatalogOpt` is `None` here (and in the `years` test below), the 
result would be `None` even if the reference were resolvable. It distinguishes 
the old behavior (exception), but could you pass a function catalog so that the 
test proves `None` comes from the resolution failure?



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