cloud-fan commented on code in PR #58450:
URL: https://github.com/apache/spark/pull/58450#discussion_r4024451316


##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/FunctionResolution.scala:
##########
@@ -404,6 +388,110 @@ 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 handling that only applies to Spark's builtins 
must not fire, since the
+   * name no longer refers to the builtin -- e.g. rejecting a direct star 
(bare `*` or qualified
+   * `t.*`) in a routed SQL/JSON function or the `count(tbl.*)` guard. 
Parser-built `count(*)` is
+   * normalized to `count(1)` in `AstBuilder` so it skips this probe, but a 
DataFrame `count("*")`
+   * keeps its star and does reach the probe during analyzer normalization.
+   *
+   * Precondition: `functionName` must already be known to be a stock built-in 
name (as
+   * `functionNameResolvesToBuiltin` ensures by checking 
`FunctionRegistry.functionSet` first). This
+   * returns true as soon as the PATH reaches `system.builtin`, without 
verifying that
+   * `system.builtin` actually defines a function of this name, so calling it 
for a non-builtin name
+   * would wrongly report builtin ownership.
+   */
+  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) =>
+          // A visible temp scalar function shadows the builtin; a visible 
temp *table* function
+          // makes scalar resolution terminal at this PATH entry 
(NOT_A_SCALAR_FUNCTION). Either way
+          // the name never reaches system.builtin, mirroring 
`resolveFunctionCandidate`.
+          val ident = FunctionIdentifier(functionName)
+          if (v1SessionCatalog.isTemporaryScalarFunctionVisible(ident) ||
+              v1SessionCatalog.isTemporaryTableFunctionVisible(ident)) {
+            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)

Review Comment:
   **Non-blocking (P2):** Function ownership is read here before direct-star 
expansion, but later `ResolveFunctions` performs another lookup. If a visible 
temporary or persistent shadow is dropped between the two, this pass expands 
away `*`; the later lookup falls through to the stock SQL/JSON builder, which 
no longer sees a `Star` and therefore skips `INVALID_USAGE_OF_STAR_OR_REGEX`. 
Please bind the selected owner across preprocessing and resolution (or 
otherwise preserve the direct-star fact), and cover the mutation-between-phases 
interleaving deterministically in both analyzer paths.
   
   **Recommended change:** Return and bind the selected function candidate when 
star preprocessing makes an owner-dependent decision. When a shadow owns the 
call, expand its star arguments while retaining that selected temp or fully 
qualified persistent candidate (or an equivalent stable owner token) for later 
resolution; if it disappears, fail that candidate instead of falling through to 
the stock builtin. Keep immediate rejection when the selected owner is Spark's 
stock SQL/JSON builder, and add deterministic owner-change regression coverage 
for both analyzer strategies.
   
   **Why this works:** Replace the Boolean-only probe/handoff with an 
owner-aware result shared by Analyzer and FunctionResolverUtils. Persist the 
chosen candidate on the unresolved call or consume an equivalent stable 
resolution object so later ResolveFunctions cannot re-enter the PATH fallback 
chain after direct-star information has been destroyed.
   
   **Scope:** Make owner-dependent direct-star preprocessing and later routine 
resolution consume one bound PATH decision.
   
   **Compatibility:** Eligible flat clause-free SQL/JSON calls retain existing 
shadowing, qualification, direct-star, view, and injected-replacement 
semantics; the documented nested/routed FORMAT JSON limitation remains 
unchanged.
   
   **Risks:** Binding a candidate must preserve view-frozen catalog/namespace 
expansion and not change normal shadow error messages unnecessarily. The 
fixed-point and single-pass analyzers must encode the same bound owner and must 
not accidentally bind parser-normalized count(*) or unrelated functions.
   
   **Constraints:** Do not hold SessionCatalog or CatalogManager locks across 
analysis or external catalog calls. Preserve scalar-versus-table terminal 
ownership, stock injected-function identity, persistentCatalogFirst, and 
original multipart qualification. Do not broaden this repair into the 
SPARK-59243 nested FORMAT JSON transport change.
   
   **Success:** A direct star is rejected whenever the bound selected owner is 
Spark's stock routed SQL/JSON builder. A shadow selected during preprocessing 
receives ordinary expanded arguments while it remains available. Removing or 
replacing that shadow before later resolution cannot make the stock builtin 
consume the already-expanded arguments; the query either uses the bound owner 
or fails that owner cleanly. Fixed-point and single-pass analysis produce the 
same owner-dependent outcome.



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/FunctionResolution.scala:
##########
@@ -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

Review Comment:
   You're right: AstBuilder already performed this owner-blind count(*) 
normalization before the PR, so it is not a regression here. The revised 
comments and retained-Star coverage accurately separate that existing behavior 
from this change.
   
   <!-- SPARK_DEV_REVIEW_REPLY 
{"feedback_id":"inline:3926332217","thread_id":"inline:3926332217","verdict_sha256":"099a08e1f5fb8b3aa3541849912f78385fb176647654fbb037b4eb3635852ff3"}
 -->



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/parser/AstBuilder.scala:
##########
@@ -4160,23 +4160,46 @@ class AstBuilder extends DataTypeAstBuilder
       (JsonValueBehavior.Default, Some(expression(d.defaultExpr)))
   }
 
+  // A clause-free JSON_ARRAY / JSON_QUERY that is a top-level JSON_ARRAY 
element stays on the
+  // direct path. Those expressions emit JSON text implicitly, and the parent 
JSON_ARRAY must see
+  // that lexical fact before analyzer rewrites can wrap the child in a Cast.
+  private def isTopLevelJsonArrayElement(ctx: RuleContext): Boolean = {
+    @scala.annotation.tailrec
+    def loop(parent: RuleContext): Boolean = parent match {
+      case null => false
+      case _: JsonArrayValueContext => true
+      case _: ExpressionContext | _: ValueExpressionDefaultContext |
+          _: ParenthesizedExpressionContext | _: CollateContext =>
+        loop(parent.getParent)
+      case p: PredicatedContext if p.predicate() == null =>
+        loop(parent.getParent)
+      case _ => false
+    }
+    loop(ctx.getParent)
+  }
+
   /**
    * Create a [[JsonValue]] expression for the SQL:2016 `JSON_VALUE` scalar 
function. The `ON EMPTY`
    * / `ON ERROR` clauses default to `NULL` when absent, per the standard.
    */
   override def visitJsonValue(ctx: JsonValueContext): Expression = 
withOrigin(ctx) {
     val jsonExpr = expression(ctx.jsonExpr)
     val path = string(visitStringLit(ctx.path))
-    // Default RETURNING type is STRING. Normalize CHAR/VARCHAR to STRING for 
the cast, as the value
-    // is produced by a `Cast` to the declared type (a raw CHAR/VARCHAR target 
has no encoder).
-    val returning = Option(ctx.returning)
-      .map(dt => 
CharVarcharUtils.replaceCharVarcharWithStringForCast(typedVisit[DataType](dt)))
-      .getOrElse(StringType)
-    val (onEmpty, emptyDefault) = Option(ctx.emptyBehavior)
-      .map(buildJsonValueBehavior).getOrElse((JsonValueBehavior.Null, None))
-    val (onError, errorDefault) = Option(ctx.errorBehavior)
-      .map(buildJsonValueBehavior).getOrElse((JsonValueBehavior.Null, None))
-    JsonValue(jsonExpr, path, returning, onEmpty, onError, emptyDefault, 
errorDefault)
+    if (ctx.returning == null && ctx.emptyBehavior == null && 
ctx.errorBehavior == null) {

Review Comment:
   Thanks for clarifying the boundary. The PR now documents the 
canonical-SQL/routed-nesting limitation and tracks it in SPARK-59243, so I'm 
okay deferring that broader rendering change from this shadowing-only patch.
   
   <!-- SPARK_DEV_REVIEW_REPLY 
{"feedback_id":"inline:3933755510","thread_id":"inline:3933755510","verdict_sha256":"099a08e1f5fb8b3aa3541849912f78385fb176647654fbb037b4eb3635852ff3"}
 -->



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