hudi-agent commented on code in PR #19853:
URL: https://github.com/apache/hudi/pull/19853#discussion_r3963069880


##########
hudi-spark-datasource/hudi-spark/src/main/scala/org/apache/spark/sql/hudi/command/procedures/HoodieProcedureFilterUtils.scala:
##########
@@ -389,6 +416,73 @@ object HoodieProcedureFilterUtils {
     }
   }
 
+  // Resolves a function not covered by the hardcoded table above via Spark's 
own FunctionRegistry.
+  // A resolved result is only usable if it can actually be eval()'d one row 
at a time, which
+  // several categories of otherwise-valid expressions cannot: 
RuntimeReplaceable placeholders
+  // (nvl, ifnull, left, right) need substitution the analyzer normally 
performs but skips here,
+  // and can themselves unwrap to another RuntimeReplaceable (regexp_substr -> 
NullIf) so the
+  // unwrap has to run to a fixed point; aggregates (percentile, collect_list) 
only make sense
+  // across real aggregation; generators (explode, inline) only work inside a 
projection;
+  // non-deterministic functions (rand, uuid, spark_partition_id) expect 
per-partition
+  // initialization; window/grouping-only builtins (current_user, lag, lead, 
...) are Unevaluable
+  // outside their normal context; and a type mismatch the analyzer's 
implicit-cast pass would
+  // normally have caught still fails checkInputDataTypes - checked both on 
the raw lookup result
+  // (its own declared input-type contract, e.g. split_part's, is otherwise 
discarded once
+  // unwrapped) and again after unwrapping and widening (e.g. nvl(ts, 0) only 
becomes checkable
+  // once it's the Coalesce(ts, 0) the hardcoded coalesce(ts, 0) case would 
already have widened).
+  // Anything in one of those categories is treated as still-unresolved so it 
falls through to the
+  // existing rejection path instead of silently dropping every row.
+  private def resolveViaFunctionRegistry(unresolvedFunc: UnresolvedFunction, 
sparkSession: SparkSession): Expression = {
+    Try {
+      // Filter expressions only ever call plain builtins. FunctionRegistry 
registers builtins
+      // with no database, so a db-qualified or 3+ part name (db.func, 
catalog.db.func) can only
+      // be resolved by guessing which part is the real function name - that 
risks matching an
+      // unrelated same-named function, so those are left unresolved instead.
+      val resolved = unresolvedFunc.nameParts match {
+        case Seq(funcName) =>
+          
sparkSession.sessionState.functionRegistry.lookupFunction(FunctionIdentifier(funcName),
 unresolvedFunc.arguments)
+        case _ => unresolvedFunc
+      }
+      // lookupFunction alone skips the analyzer's own implicit-cast rule, so 
a wrapper declaring
+      // a real input-type contract (nvl needing matching operand types, 
split_part needing
+      // string/string/int, ...) sees its raw, uncast arguments here. Casting 
via that same rule
+      // before checking the contract lets a fixable mismatch (nvl(ts, 0), a 
Long/Int pair) widen
+      // the way coalesce(ts, 0) already does, while a genuine mismatch 
(split_part's delimiter
+      // passed as Int, which nothing implicit-casts to String) still fails as 
it should.
+      val castedResolved = applyImplicitCasts(resolved)

Review Comment:
   🤖 `applyImplicitCasts` only runs `ImplicitTypeCasts`, but the analyzer runs 
the whole `TypeCoercion` rule set — so calls that depend on any of the other 
rules get rejected here even though Spark accepts them. `concat(id, 'x')` is 
the clearest one: `ConcatCoercion` casts non-binary children to string, so 
`select concat(1, 'x')` returns `1x` in Spark 3.x, but here 
`Concat.checkInputDataTypes` fails and the filter is rejected. `if(cond, ts, 
0)` (`IfCoercion`) and `greatest`/`least`/`array_contains` 
(`FunctionArgumentConversion`) look like they'd hit the same thing. Could the 
pass run the fuller rule list, or at least the handful most likely to show up 
in filters?
   
   <sub><i>⚠️ AI-generated; verify before applying. React 👍/👎 to flag 
quality.</i></sub>



##########
hudi-spark-datasource/hudi-spark/src/main/scala/org/apache/spark/sql/hudi/command/procedures/HoodieProcedureFilterUtils.scala:
##########
@@ -410,9 +504,19 @@ object HoodieProcedureFilterUtils {
       case Success(result) => result
       // Spark raises SparkArithmeticException for an overflowing ANSI cast or 
arithmetic, and
       // SparkNumberFormatException or SparkDateTimeException for an ANSI cast 
of a malformed
-      // string; each extends the matching JDK type. Swallowing one would 
silently drop a row the
-      // same query keeps, so let it out and let the caller fail the way the 
equivalent query does.
-      case Failure(e @ (_: ArithmeticException | _: NumberFormatException | _: 
DateTimeException)) => throw e
+      // string; each extends the matching JDK type. SparkThrowable covers the 
equivalent runtime
+      // errors from registry-resolved functions (to_number/bit_get 
out-of-range, ...), and
+      // IllegalArgumentException covers a bad regex pattern, both of which 
the hardcoded table
+      // never reached before the registry fallback existed. Swallowing any of 
these would
+      // silently drop a row the same query keeps, so let them out and let the 
caller fail the
+      // way the equivalent query does. Only for an otherwise-evaluable 
expression, though: a
+      // caller that skips validateFilterExpression and evaluates a genuinely 
unsupported function
+      // directly still no-matches (calling eval() on the leftover 
UnresolvedFunction/Unevaluable
+      // node raises the same SparkThrowable-family INTERNAL_ERROR for an 
entirely different,
+      // expected reason), so this method stays safe to call on its own.
+      case Failure(e @ (_: ArithmeticException | _: NumberFormatException | _: 
DateTimeException
+        | _: SparkThrowable | _: IllegalArgumentException)) if 
!boundExpr.exists(_.isInstanceOf[Unevaluable]) =>

Review Comment:
   🤖 The new `if !boundExpr.exists(_.isInstanceOf[Unevaluable])` guard also 
applies to the three pre-existing ANSI cases, which were rethrown 
unconditionally before. So `cast(name as int) = 1 OR no_such_fn(name) = 'x'` 
under ANSI now swallows the `NumberFormatException` and drops the row instead 
of surfacing it. Reachable only when a caller skips `validateFilterExpression`, 
but was the narrowing of the existing three types intentional, or should the 
guard apply only to the two newly-added types?
   
   <sub><i>⚠️ AI-generated; verify before applying. React 👍/👎 to flag 
quality.</i></sub>



##########
hudi-spark-datasource/hudi-spark/src/main/scala/org/apache/spark/sql/hudi/command/procedures/HoodieProcedureFilterUtils.scala:
##########
@@ -389,6 +416,73 @@ object HoodieProcedureFilterUtils {
     }
   }
 
+  // Resolves a function not covered by the hardcoded table above via Spark's 
own FunctionRegistry.
+  // A resolved result is only usable if it can actually be eval()'d one row 
at a time, which
+  // several categories of otherwise-valid expressions cannot: 
RuntimeReplaceable placeholders
+  // (nvl, ifnull, left, right) need substitution the analyzer normally 
performs but skips here,
+  // and can themselves unwrap to another RuntimeReplaceable (regexp_substr -> 
NullIf) so the
+  // unwrap has to run to a fixed point; aggregates (percentile, collect_list) 
only make sense
+  // across real aggregation; generators (explode, inline) only work inside a 
projection;
+  // non-deterministic functions (rand, uuid, spark_partition_id) expect 
per-partition
+  // initialization; window/grouping-only builtins (current_user, lag, lead, 
...) are Unevaluable
+  // outside their normal context; and a type mismatch the analyzer's 
implicit-cast pass would
+  // normally have caught still fails checkInputDataTypes - checked both on 
the raw lookup result
+  // (its own declared input-type contract, e.g. split_part's, is otherwise 
discarded once
+  // unwrapped) and again after unwrapping and widening (e.g. nvl(ts, 0) only 
becomes checkable
+  // once it's the Coalesce(ts, 0) the hardcoded coalesce(ts, 0) case would 
already have widened).
+  // Anything in one of those categories is treated as still-unresolved so it 
falls through to the
+  // existing rejection path instead of silently dropping every row.
+  private def resolveViaFunctionRegistry(unresolvedFunc: UnresolvedFunction, 
sparkSession: SparkSession): Expression = {
+    Try {
+      // Filter expressions only ever call plain builtins. FunctionRegistry 
registers builtins
+      // with no database, so a db-qualified or 3+ part name (db.func, 
catalog.db.func) can only
+      // be resolved by guessing which part is the real function name - that 
risks matching an
+      // unrelated same-named function, so those are left unresolved instead.
+      val resolved = unresolvedFunc.nameParts match {
+        case Seq(funcName) =>
+          
sparkSession.sessionState.functionRegistry.lookupFunction(FunctionIdentifier(funcName),
 unresolvedFunc.arguments)
+        case _ => unresolvedFunc
+      }
+      // lookupFunction alone skips the analyzer's own implicit-cast rule, so 
a wrapper declaring
+      // a real input-type contract (nvl needing matching operand types, 
split_part needing
+      // string/string/int, ...) sees its raw, uncast arguments here. Casting 
via that same rule
+      // before checking the contract lets a fixable mismatch (nvl(ts, 0), a 
Long/Int pair) widen
+      // the way coalesce(ts, 0) already does, while a genuine mismatch 
(split_part's delimiter
+      // passed as Int, which nothing implicit-casts to String) still fails as 
it should.
+      val castedResolved = applyImplicitCasts(resolved)
+      // Checked against checkInputDataTypes only, not the resolved flag: a 
RuntimeReplaceable

Review Comment:
   🤖 nit: this comment block (and the one on `resolveViaFunctionRegistry` 
itself) is quite dense to parse in one pass — might be worth trimming to the 
couple of non-obvious points (why RuntimeReplaceable needs a fixed-point 
unwrap, why 3+ part names are rejected) and moving the rest into shorter inline 
comments near the specific checks they explain.
   
   <sub><i>⚠️ AI-generated; verify before applying. React 👍/👎 to flag 
quality.</i></sub>



##########
hudi-spark-datasource/hudi-spark/src/main/scala/org/apache/spark/sql/hudi/command/procedures/HoodieProcedureFilterUtils.scala:
##########
@@ -389,6 +416,73 @@ object HoodieProcedureFilterUtils {
     }
   }
 
+  // Resolves a function not covered by the hardcoded table above via Spark's 
own FunctionRegistry.
+  // A resolved result is only usable if it can actually be eval()'d one row 
at a time, which
+  // several categories of otherwise-valid expressions cannot: 
RuntimeReplaceable placeholders
+  // (nvl, ifnull, left, right) need substitution the analyzer normally 
performs but skips here,
+  // and can themselves unwrap to another RuntimeReplaceable (regexp_substr -> 
NullIf) so the
+  // unwrap has to run to a fixed point; aggregates (percentile, collect_list) 
only make sense
+  // across real aggregation; generators (explode, inline) only work inside a 
projection;
+  // non-deterministic functions (rand, uuid, spark_partition_id) expect 
per-partition
+  // initialization; window/grouping-only builtins (current_user, lag, lead, 
...) are Unevaluable
+  // outside their normal context; and a type mismatch the analyzer's 
implicit-cast pass would
+  // normally have caught still fails checkInputDataTypes - checked both on 
the raw lookup result
+  // (its own declared input-type contract, e.g. split_part's, is otherwise 
discarded once
+  // unwrapped) and again after unwrapping and widening (e.g. nvl(ts, 0) only 
becomes checkable
+  // once it's the Coalesce(ts, 0) the hardcoded coalesce(ts, 0) case would 
already have widened).
+  // Anything in one of those categories is treated as still-unresolved so it 
falls through to the
+  // existing rejection path instead of silently dropping every row.
+  private def resolveViaFunctionRegistry(unresolvedFunc: UnresolvedFunction, 
sparkSession: SparkSession): Expression = {
+    Try {
+      // Filter expressions only ever call plain builtins. FunctionRegistry 
registers builtins
+      // with no database, so a db-qualified or 3+ part name (db.func, 
catalog.db.func) can only
+      // be resolved by guessing which part is the real function name - that 
risks matching an
+      // unrelated same-named function, so those are left unresolved instead.
+      val resolved = unresolvedFunc.nameParts match {
+        case Seq(funcName) =>
+          
sparkSession.sessionState.functionRegistry.lookupFunction(FunctionIdentifier(funcName),
 unresolvedFunc.arguments)
+        case _ => unresolvedFunc
+      }

Review Comment:
   🤖 nit: `resolveViaFunctionRegistry` now does lookup, implicit-cast, a first 
checkInputDataTypes, RuntimeReplaceable unwrap-to-fixed-point, coercion, and a 
second suitability check all in one method — might be worth pulling the 
unwrap-to-fixed-point + "still unsupported" check into its own small helper 
(e.g. `finalizeRegistryResolution`) so each step reads as a single 
responsibility.
   
   <sub><i>⚠️ AI-generated; verify before applying. React 👍/👎 to flag 
quality.</i></sub>



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

Reply via email to