ganeshashree commented on code in PR #58450:
URL: https://github.com/apache/spark/pull/58450#discussion_r3946355813


##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/jsonExpressions.scala:
##########
@@ -1651,6 +1651,132 @@ object JsonArray {
   }
 }
 
+// Built-in forms for the plain SQL/JSON constructor and path-function calls 
that `AstBuilder`
+// routes through function resolution. Each rebuilds its expression with the 
standard clause
+// defaults when unshadowed.
+
+@ExpressionDescription(
+  usage = "_FUNC_([expr[, ...]]) - Returns a JSON array string with NULL 
elements dropped.",
+  arguments = """
+    Arguments:
+      * expr - the elements to place in the array.
+  """,
+  examples = """
+    Examples:
+      > SELECT _FUNC_(1, 'x', true);
+       [1,"x",true]
+      > SELECT _FUNC_(1, NULL, 3);
+       [1,3]
+      > SELECT _FUNC_();
+       []
+  """,
+  since = "4.4.0",
+  group = "json_funcs")
+object JsonArrayExpressionBuilder extends ExpressionBuilder {
+  override def build(funcName: String, expressions: Seq[Expression]): 
Expression = {
+    // A routed call carries no lexical FORMAT JSON, so every element is a 
plain value (quoted).
+    // Splicing a nested constructor is only reachable via `JSON_ARRAY(...)` 
syntax (which freezes
+    // the decision lexically).
+    // TODO(SPARK-59243): splice nested constructors reached through 
routed/qualified calls.
+    val flags = expressions.map(_ => false)
+    JsonArray(expressions, flags, flags, JsonConstructorNullBehavior.Absent, 
StringType)
+  }
+}
+
+/**
+ * Shared builder for the plain `JSON_VALUE` / `JSON_QUERY` / `JSON_EXISTS` 
forms. The path must
+ * be a foldable string expression; the parser-only SQL/JSON syntax supplies a 
string literal,
+ * while ordinary function-call syntax can reach this builder with any 
constant string expression.
+ */
+abstract class JsonPathExpressionBuilder extends ExpressionBuilder {
+  protected def buildWithPath(jsonExpr: Expression, path: String): Expression
+
+  override final def build(funcName: String, expressions: Seq[Expression]): 
Expression = {
+    if (expressions.length != 2) {
+      throw QueryCompilationErrors.wrongNumArgsError(funcName, Seq(2), 
expressions.length)
+    }
+    val pathExpr = expressions(1)
+    pathExpr.dataType match {
+      case _: StringType if pathExpr.foldable =>
+        // TODO(SPARK-59244): wrap eval() so a foldable-but-throwing path 
re-throws as a clean
+        // invalid-argument analysis error naming `path`.
+        val pathValue = pathExpr.eval()
+        if (pathValue == null) {
+          throw QueryCompilationErrors.unexpectedNullError("path", pathExpr)
+        }
+        buildWithPath(expressions.head, pathValue.toString)
+      case _: StringType =>
+        throw QueryCompilationErrors.nonFoldableArgumentError(
+          funcName, "path", pathExpr.dataType)
+      case _ =>
+        throw QueryCompilationErrors.unexpectedInputDataTypeError(
+          funcName, 2, StringType, pathExpr)
+    }
+  }
+}
+
+@ExpressionDescription(
+  usage = "_FUNC_(jsonStr, path) - Extracts a SQL scalar as a string.",
+  arguments = """
+    Arguments:
+      * jsonStr - a JSON string.
+      * path - a SQL/JSON path expression given as a foldable string 
expression.

Review Comment:
   Done.



##########
sql/core/src/test/scala/org/apache/spark/sql/JsonArraySuite.scala:
##########
@@ -466,18 +492,272 @@ class JsonArraySuite extends QueryTest with 
SharedSparkSession {
     }
   }
 
+  test("plain call goes through routine resolution and can be shadowed via SET 
PATH") {
+    // `withUserDefinedFunction` is unusable here: its cleanup asserts the 
name no longer resolves,
+    // but `json_array` is now a registered built-in, so drop the temporary 
routine explicitly.
+    withSQLConf(
+      SQLConf.PATH_ENABLED.key -> "true",
+      SQLConf.SESSION_FUNCTION_RESOLUTION_ORDER.key -> "second") {
+      try {
+        sql("CREATE TEMPORARY FUNCTION json_array(a INT, b STRING) RETURNS 
STRING " +
+          "RETURN 'shadowed'")
+        sql("CREATE TEMPORARY FUNCTION json_value(a STRING, b STRING) RETURNS 
STRING " +
+          "RETURN 'shadowed'")
+        sql("CREATE TEMPORARY FUNCTION json_query(a STRING, b STRING) RETURNS 
STRING " +
+          "RETURN 'shadowed'")
+        sql("CREATE TEMPORARY FUNCTION json_exists(a STRING, b STRING) RETURNS 
BOOLEAN " +
+          "RETURN false")
+        sql("SET PATH = system.session, system.builtin")
+        // A plain call is an ordinary function call, so the temporary routine 
(ahead of
+        // system.builtin on the path) shadows the built-in constructor.
+        checkAnswer(sql("SELECT json_array(1, 'x')"), Row("shadowed"))
+        checkAnswer(sql("SELECT json_array(*) FROM VALUES (1, 'x') AS t(a, 
b)"), Row("shadowed"))
+        // The clause-bearing form is not a function call, so it stays the 
built-in constructor.
+        checkAnswer(sql("SELECT json_array('x' NULL ON NULL)"), 
Row("""["x"]"""))
+        // Nested JSON-producing children stay on the direct-construction 
path, so they are not
+        // shadowed. This preserves JSON_ARRAY's parse-time splice decisions.
+        checkAnswer(sql("SELECT json_array(json_array(1))"), Row("[[1]]"))
+        checkAnswer(
+          sql("""SELECT json_array(json_query('{"a":{"x":1}}', '$.a'))"""),
+          Row("""[{"x":1}]"""))
+        // Plain scalar and predicate children are still ordinary function 
calls. Use an explicit
+        // outer NULL clause to keep the parent on the direct path while the 
children are shadowed.
+        checkAnswer(
+          sql("""SELECT json_array(json_value('{"a":"x"}', '$.a') NULL ON 
NULL)"""),
+          Row("""["shadowed"]"""))
+        checkAnswer(
+          sql("""SELECT json_array(json_exists('{"a":1}', '$.a') NULL ON 
NULL)"""),
+          Row("[false]"))
+      } finally {
+        sql("SET PATH = DEFAULT_PATH")
+        sql("DROP TEMPORARY FUNCTION IF EXISTS json_array")
+        sql("DROP TEMPORARY FUNCTION IF EXISTS json_value")
+        sql("DROP TEMPORARY FUNCTION IF EXISTS json_query")
+        sql("DROP TEMPORARY FUNCTION IF EXISTS json_exists")
+      }
+    }
+  }
+
+  test("qualified plain JSON_ARRAY resolves to the built-in constructor") {
+    checkAnswer(sql("SELECT builtin.json_array(1, 'x')"), Row("""[1,"x"]"""))
+    checkAnswer(sql("SELECT system.builtin.json_array(1, 'x')"), 
Row("""[1,"x"]"""))
+  }
+
+  test("a nested JSON constructor through a routed JSON_ARRAY call is quoted, 
not spliced") {
+    // A routed (plain or qualified) call carries no lexical FORMAT JSON, so a 
nested JSON
+    // constructor argument is treated as a plain value and quoted, unlike the 
JSON_ARRAY(...)
+    // grammar which splices it (see the unqualified 
`json_array(json_array(1))` -> `[[1]]` cases
+    // above). A nested constructor reaches the routed builder only via a 
qualified outer call:

Review Comment:
   Done.



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