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


##########
sql/core/src/test/scala/org/apache/spark/sql/SetPathSuite.scala:
##########
@@ -965,6 +956,36 @@ class SetPathSuite extends SharedSparkSession {
     }
   }
 
+  test("path-driven COUNT rewrite gate: DataFrame count(\"*\") and count(t.*) 
reach the owner " +
+      "probe and expand through a shadowing temp count") {
+    // SQL `count(*)` is normalized to `count(1)` in AstBuilder, so it never 
reaches the analyzer
+    // owner probe. A DataFrame `count("*")` keeps its UnresolvedStar and 
does, as does count(t.*).
+    // A non-1 input distinguishes an incorrect `count(1)` rewrite from 
correct star expansion to
+    // `count(a)` through the temp.
+    withPathEnabled {
+      sql("CREATE TEMPORARY FUNCTION count(x INT) RETURNS INT RETURN x + 100")
+      try {
+        val df = sql("SELECT * FROM VALUES (7) AS t(a)")
+
+        // Builtin-first: count is the builtin, so `count("*")` collapses to 
`count(1)` and returns
+        // the row count (1), while `count(t.*)` hits the single-table-star 
guard.
+        checkAnswer(df.select(functions.count("*")), Row(1))
+        intercept[AnalysisException] {

Review Comment:
   **Non-blocking (P2):** This assertion accepts every `AnalysisException`, so 
it remains green if the single-table-star guard stops raising 
`INVALID_USAGE_OF_STAR_WITH_TABLE_IDENTIFIER_IN_COUNT` and an unrelated 
analyzer path fails instead. Please capture the exception and check its 
condition and message parameters with `checkError` (or the suite's established 
structured-error helper) so the test proves the intended guard fired.



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/FunctionResolution.scala:
##########
@@ -404,6 +388,103 @@ 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)
+      case 3 =>
+        true
+      case _ =>
+        false
+    }
+  }
+
+  // All routed SQL/JSON functions (JSON_ARRAY, JSON_VALUE, JSON_QUERY, 
JSON_EXISTS) forbid a bare

Review Comment:
   **Nit (P3):** The shared guard rejects any direct `Star`, including 
qualified `t.*`, but these lines describe only a bare `*`. Please call this a 
direct star argument (bare `*` or qualified `t.*`) here and in 
`JsonArraySuite`'s `Only a bare * element is rejected` comment, while retaining 
the distinction from a star nested inside another expression.



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/FunctionRegistry.scala:
##########
@@ -950,9 +950,33 @@ object FunctionRegistry {
     expression[SchemaOfJson]("schema_of_json"),
     expression[LengthOfJsonArray]("json_array_length"),
     expression[JsonObjectKeys]("json_object_keys"),
-    expression[JsonTypeof]("json_typeof")
+    expression[JsonTypeof]("json_typeof"),
+    // Built-in forms of the SQL:2016 JSON constructor and path functions, 
resolved for plain calls

Review Comment:
   **Nit (P3):** This says plain calls without SQL/JSON clauses resolve through 
these builders, but `AstBuilder` still directly constructs clause-free 
`JSON_ARRAY`/`JSON_QUERY` in the nested or implicit-JSON cases. Please qualify 
this as the eligible flat clause-free forms and point to the intentional 
SPARK-59243 exception, so the registration guidance matches the accepted 
routing boundary.



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