LuciferYang commented on code in PR #59697:
URL: https://github.com/apache/spark/pull/59697#discussion_r4441625156
##########
docs/sql-ref-syntax-ddl-create-function.md:
##########
@@ -80,13 +80,22 @@ CREATE [ OR REPLACE ] [ TEMPORARY ] FUNCTION [ IF NOT
EXISTS ]
* **class_name**
Specifies the name of the class that provides the implementation for
function to be created.
- The implementing class should extend one of the base classes as follows:
+ The implementing class should have a public no-arg constructor, and extend
one of the base
+ classes as follows:
+ * Should implement `UnboundFunction` in
`org.apache.spark.sql.connector.catalog.functions`
+ package.
* Should extend `UDF` or `UDAF` in `org.apache.hadoop.hive.ql.exec`
package.
* Should extend `AbstractGenericUDAFResolver`, `GenericUDF`, or
`GenericUDTF` in `org.apache.hadoop.hive.ql.udf.generic` package.
* Should extend `UserDefinedAggregateFunction` in
`org.apache.spark.sql.expressions` package.
+ **Note:** If an `UnboundFunction` binds to a `ScalarFunction` that
implements the magic method
+ `invoke`, and its jar is added at runtime (`ADD JAR`, or `USING JAR`),
queries calling it may
+ fail to compile generated code when run on a thread other than the one
that added the jar,
Review Comment:
The trigger here, "on a thread other than the one that added the jar",
doesn't match its own example: a later line in spark-shell runs on the same
thread, but the REPL resets the context classloader after each line. What
matters is whether the context classloader of the driver thread compiling the
query can see the jar. The note also says only "may fail to compile generated
code". With default configs, whole-stage codegen logs the compile error and
falls back, so results stay correct. Queries that hit an operator without a
fallback, such as `ORDER BY f(c) LIMIT 10`, fail. The PR description covers
both cases, and it would help to have them here too.
Could you open a JIRA for how generated code resolves user classes and link
it from this note? A test with `magic = true` that restores the context
classloader and then calls the function would also pin down the current
behavior. In tests the whole-stage fallback is disabled, so even a plain
`SELECT f(c)` fails there.
##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/catalog/SessionCatalog.scala:
##########
@@ -2073,17 +2077,69 @@ class SessionCatalog(
}
/**
- * Constructs a [[FunctionBuilder]] based on the provided function metadata.
+ * Constructs a [[FunctionBuilder]] and the [[ExpressionInfo]] based on the
provided function
+ * metadata.
*/
- private def makeFunctionBuilder(func: CatalogFunction): FunctionBuilder = {
+ private def makeFunctionBuilderAndInfo(
+ func: CatalogFunction): (FunctionBuilder, ExpressionInfo) = {
+ val clazz = loadFunctionClass(func)
+ val name = func.identifier.unquotedString
+ if (isV2FunctionClass(clazz)) {
+ // A V2 function: bind it to the arguments at each call site. An
aggregate function is
+ // returned as a bare `V2Aggregator`, which the caller wraps into an
`AggregateExpression`
+ // carrying DISTINCT / FILTER. This check goes first so that a class
also implementing a
+ // Hive UDF interface, for other engines sharing the metastore, runs as
a V2 function in
+ // Spark. The alias makes errors and column names use the function name,
like the Hive UDF
+ // wrappers do.
+ val unbound = newV2FunctionInstance(clazz, func.identifier)
+ val builder = (input: Seq[Expression]) => {
+ val expr = V2ExpressionUtils.bindFunction(
+ unbound, input, displayName = Some(func.identifier))
+ expr.setTagValue(FunctionRegistry.FUNC_ALIAS, name)
Review Comment:
A magic method that is an instance method binds to `Invoke(Literal(<function
instance>))`, and `Invoke.sql` doesn't use `prettyName`, so the FUNC_ALIAS set
here doesn't reach the column name. The name is `invoke(<class>@<identity
hash>.invoke(s))`, and when `bind` returns the function itself, as
`SimpleFunction` does, the hash changes with each registration. I tried this:
after `CREATE TEMPORARY VIEW v AS SELECT f(s) FROM t`, running `CREATE OR
REPLACE TEMPORARY FUNCTION f` again with the same class makes `SELECT * FROM v`
fail with `INCOMPATIBLE_VIEW_SCHEMA_CHANGE`.
The comment at L2092 says column names use the function name, which doesn't
hold for magic methods. Could an `Invoke` / `StaticInvoke` carrying FUNC_ALIAS
get its column name from the function name too, with a column-name assertion
for a magic method? If you'd rather keep the current names, the comment should
be narrowed.
##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/catalog/SessionCatalog.scala:
##########
@@ -2167,12 +2228,58 @@ class SessionCatalog(
if (funcDefinition.isUserDefinedFunction) {
UserDefinedFunction.fromCatalogFunction(funcDefinition,
parser).toExpressionInfo
} else {
- makeExprInfoForHiveFunction(funcDefinition)
+ makeExprInfoForClassFunction(funcDefinition)
}
}
registry.registerFunction(identToRegister, resolvedInfo, functionBuilder)
}
+ /**
+ * Builds the [[ExpressionInfo]] of a class-based function (`CREATE FUNCTION
... AS 'class'`).
+ * A V2 function reports its own description, which requires instantiating
it. Otherwise, or if
+ * the class cannot be loaded or instantiated, use the class-name-only info,
so that like a Hive
+ * UDF, a broken function only fails when invoked. The class of a function
with `USING`
+ * resources is not touched, as initializing it may require those resources,
which are only
+ * loaded on first invocation.
+ */
+ private def makeExprInfoForClassFunction(func: CatalogFunction):
ExpressionInfo = {
+ if (func.resources.nonEmpty) {
+ return makeExprInfoForHiveFunction(func)
+ }
+ try {
+ val clazz = Utils.classForName(func.className, initialize = false)
+ if (isV2FunctionClass(clazz)) {
+ makeExprInfoForV2Function(func, newV2FunctionInstance(clazz,
func.identifier))
Review Comment:
For a persistent V2 function without `USING` that this session hasn't
invoked yet, DESCRIBE FUNCTION, `spark.catalog.listFunctions()` and Thrift
`getFunctions` construct an instance here, so the static initializer runs. If
it fails at this point, the JVM marks the class as failed to initialize, and
every later call throws `NoClassDefFoundError: Could not initialize class`
until the driver restarts. For example, if the static initializer uses a class
from a jar added later with `ADD JAR`, a single listing leaves the function
unusable. I reproduced this with a static initializer that throws until a
system property is set. Hive UDFs only go through `initialize = false` here and
are not affected.
The `USING` branch already leaves the class alone. Could these functions get
the class-name-only info too, with the description showing up after the first
invocation? That would mean narrowing item 2 of the user-facing section in the
PR description and updating the tests that read the description without
invoking the function (`CreateV2FunctionSuite` L188 and L212), plus the
metadata-path `checkFatal` calls in the L386 test. Documenting that metadata
reads initialize the class is the other option. I'd like to hear your view
first.
--
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]