anuragmantri commented on code in PR #58979:
URL: https://github.com/apache/spark/pull/58979#discussion_r4116772248


##########
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:
   Done. Used your wording for the scaladoc and made it `private[sql]`.



##########
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:
   Done.



##########
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:
   Good catch, thanks. Fixed as you suggested: `toCatalyst`/`toCatalystOpt` and 
`toCatalystTransformOpt` now go through private 
`toCatalystCore`/`toCatalystTransformCore` helpers that take a 
`failOnUnresolved` flag. `toCatalyst` (and so `toCatalystOrdering`) passes 
`true` and keeps throwing `_LEGACY_ERROR_TEMP_1137`, while the `*Opt` variants 
pass `false` and return `None`. The flag is threaded through the `SortValue` 
child and the `NamedTransform` args, so the recursion you pointed out keeps the 
specific error too. `resolveRef` itself is unchanged.
   
   The `toCatalystOrdering` test now asserts `_LEGACY_ERROR_TEMP_1137`, and I 
added a direct `toCatalyst` test for an unresolvable `FieldReference`. I also 
removed the side-effect paragraph from the PR description since it no longer 
applies.



##########
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:
   Removed



##########
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:
   Yes, that's intended. I added both points to the PR description. On the 
throwing path, a nested transform whose function can't be loaded still fails 
with `_LEGACY_ERROR_TEMP_3054`, but the message now names the outer transform 
instead of the inner one.



##########
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:
   Good point. The `bucket` and `years` tests now pass a function catalog: a 
small `UnboundFunction` registered under both names in an `InMemoryCatalog`. 
Each test also asserts that the same transform on the existing column `a` 
converts, so the `None` for `missing` can only come 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