-
Notifications
You must be signed in to change notification settings - Fork 29.4k
[SPARK-59144][SQL] Route plain SQL/JSON constructor calls through routine resolution #58450
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -76,22 +76,6 @@ class FunctionResolution( | |
| nameParts.length == 3 && | ||
| nameParts.head.equalsIgnoreCase(CatalogManager.SYSTEM_CATALOG_NAME) | ||
|
|
||
| /** | ||
| * True iff `system.session` is searched before `system.builtin` in the effective SQL PATH. | ||
| * | ||
| * Drives the `count(*) -> count(1)` rewrite (which must skip transformation when a temp | ||
| * `count` shadows the builtin) and the `SessionCatalog` security check that blocks creating | ||
| * a temp function with a builtin's name. Reads the live PATH via `CatalogManager` and | ||
| * applies the same kinds extraction that drives `SessionCatalog`'s fast-path provider, so | ||
| * the predicate stays in sync with the lookup loop's actual order. Uses the consolidated | ||
| * snapshot helper (SPARK-56939) so the (catalog, namespace, path) triple is observed | ||
| * atomically. | ||
| */ | ||
| def isSessionBeforeBuiltinInPath: Boolean = { | ||
| catalogManager.sessionFunctionKindsForUnqualifiedResolution().headOption | ||
| .contains(org.apache.spark.sql.catalyst.catalog.SessionCatalog.Temp) | ||
| } | ||
|
|
||
| /** | ||
| * Produces the ordered list of candidate names for resolution. Expansion happens in two cases: | ||
| * | ||
|
|
@@ -404,6 +388,87 @@ class FunctionResolution( | |
| } | ||
| } | ||
|
|
||
| /** | ||
| * Returns whether an unqualified function name reaches `system.builtin` before any temp or | ||
| * persistent function in the effective SQL PATH. When a temp or persistent function shadows the | ||
| * builtin, special-syntax rewrites (e.g. `count(*) -> count(1)`) must not fire, since the name no | ||
| * longer refers to Spark's builtin. | ||
| */ | ||
| def unqualifiedFunctionResolvesToBuiltinBeforeAnyShadow(functionName: String): Boolean = { | ||
| // Walk the PATH in order and stop at the first entry that owns the name. The default order puts | ||
| // system.builtin first, so the common case returns on the first entry with no catalog lookup; | ||
| // only a custom PATH that lists a persistent catalog ahead of system.builtin reaches the probe | ||
| // below (one lookup per such preceding entry, recomputed on each call -- not cached). | ||
| sqlResolutionPathEntriesForAnalysis.foreach { pathEntry => | ||
| val candidate = pathEntry :+ functionName | ||
| FunctionResolution.sessionNamespaceKind(candidate) match { | ||
| case Some(org.apache.spark.sql.catalyst.catalog.SessionCatalog.Builtin) => | ||
| return true | ||
| case Some(org.apache.spark.sql.catalyst.catalog.SessionCatalog.Temp) => | ||
| if (v1SessionCatalog.isTemporaryFunction(FunctionIdentifier(functionName))) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Blocking (P1): This raw Recommended change: Make the builtin-ownership probe reuse SessionCatalog's stored-view-aware temporary-function visibility semantics. Why this works: Replace the raw registry existence check with a side-effect-free lookup that applies AnalysisContext.referredTempFunctionNames exactly as actual scalar resolution does. Scope: FunctionResolution's system.session branch plus focused catalyst and SQL stored-view coverage. Compatibility: This preserves normal session lookup behavior and only restores builtin-only syntax handling when a temporary function is not visible to the stored view. Risks: The probe must not build or execute a function expression merely to test ownership. Scalar and table-function visibility must remain consistent with the actual resolver path. Constraints: Honor the view's recorded temporary-function names and frozen SQL PATH. Keep fixed-point and single-pass analyzer ownership decisions aligned. Success: For stored views, the precheck and actual resolver choose the same first visible owner, and builtin JSON star syntax is rejected even when an unrelated invisible temp function exists. |
||
| return false | ||
| } | ||
| case None => | ||
| if (persistentFunctionExists(candidate)) { | ||
| return false | ||
| } | ||
| } | ||
| } | ||
| false | ||
| } | ||
|
|
||
| /** | ||
| * Returns true when a function reference resolves to the system built-in with the requested name. | ||
| * This mirrors [[resolveFunction]] for special parser/analyzer rewrites that must run only for | ||
| * Spark's built-ins. In particular, two-part `builtin.name` is not always a system built-in: | ||
| * with `spark.sql.legacy.persistentCatalogFirst=true`, an existing persistent | ||
| * `current_catalog.builtin.name` takes precedence. | ||
| */ | ||
| def functionNameResolvesToBuiltin(nameParts: Seq[String], expectedName: String): Boolean = { | ||
| if (!FunctionRegistry.functionSet.contains( | ||
| FunctionRegistry.builtinFunctionIdentifier(expectedName)) || | ||
| !FunctionResolution.isUnqualifiedOrBuiltinFunctionName(nameParts, expectedName)) { | ||
| return false | ||
| } | ||
| nameParts.length match { | ||
| case 1 => | ||
| unqualifiedFunctionResolvesToBuiltinBeforeAnyShadow(nameParts.head) | ||
| case 2 => | ||
| conf.prioritizeSystemCatalog || !persistentFunctionExists(nameParts) | ||
| case 3 => | ||
| true | ||
| case _ => | ||
| false | ||
| } | ||
| } | ||
|
|
||
| private val starDisallowedJsonConstructors = | ||
| Set("json_array", "json_exists", "json_query", "json_value") | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This set has to stay in sync with two other lists, the four expressionBuilder(...) registrations in FunctionRegistry and the four routed grammar branches in AstBuilder. A future fifth clause-free constructor added to the grammar + registry but not here would silently lose the bare-* rejection (it'd fall through to normal star expansion). Please consider deriving this from a shared constant. |
||
|
|
||
| /** True if `nameParts` resolves to a built-in SQL/JSON constructor that forbids bare `*`. */ | ||
| def resolvesToStarDisallowedJsonConstructor(nameParts: Seq[String]): Boolean = | ||
| starDisallowedJsonConstructors.exists(functionNameResolvesToBuiltin(nameParts, _)) | ||
|
|
||
| private def persistentFunctionExists(nameParts: Seq[String]): Boolean = { | ||
| try { | ||
| // Expand through the view's frozen catalog/namespace exactly as `resolveFunctionCandidate` | ||
| // does, so the shadow probe queries the same catalog the real resolver would inside a view. | ||
| relationResolution.expandIdentifier(nameParts) match { | ||
| case CatalogAndIdentifier(catalog, ident) => | ||
| catalog.asFunctionCatalog.functionExists(ident) | ||
| case _ => | ||
| false | ||
| } | ||
| } catch { | ||
| case _: NoSuchFunctionException | ||
| | _: NoSuchNamespaceException | ||
| | _: CatalogNotFoundException => | ||
| false | ||
| case e: AnalysisException if e.getCondition == "FORBIDDEN_OPERATION" => | ||
| false | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Determines the type/location of a function (builtin, temporary, persistent, etc.). | ||
| * This is used by the LookupFunctions analyzer rule for early validation and optimization. | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Blocking (P1): This path-aware count gate is unreachable for an unqualified SQL
count(*):AstBuilderhas already replaced the Star withLiteral(1). With a persistentcount(x INT) = x + 100beforesystem.builtin,SELECT count(*) FROM VALUES (7) AS t(a)consequently returns 101 instead of expanding the user routine's argument toaand returning 107. Please leave the Star unresolved in the parser, normalize tocount(1)only after these ownership checks select the builtin, and update the parser/SetPath tests with a non-1 input so they distinguish the two paths.Recommended change: Move unqualified count(*) normalization from AstBuilder to the ownership-aware analyzer paths.
Why this works: Preserve UnresolvedStar through parsing, then convert it to Literal(1) only when functionNameResolvesToBuiltin confirms that Spark's builtin count owns the call; otherwise allow ordinary star expansion for the selected routine.
Scope: AstBuilder, fixed-point and single-pass count-star handling, parser expectations, and SetPathSuite coverage.
Compatibility: Builtin count(*) keeps its existing semantics, while user-defined count routines on SQL PATH receive the arguments ordinary function resolution specifies.
Risks: Changing the unresolved parse shape can expose ordering differences between the two analyzers. Qualified stars and the legacy single-table-star count configuration must retain their existing validation.
Constraints: Preserve distinct-count and qualified-function behavior. Use a non-1 and nullable input in regression coverage so count(1), count(column), and star expansion remain distinguishable.
Success: Builtin count(*) still counts rows, while a persistent count routine before system.builtin receives the expanded input column in both analyzer modes.