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


##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/jsonExpressions.scala:
##########
@@ -1314,6 +1315,60 @@ trait ImplicitlyFormattedAsJson extends Expression {
   def emitsImplicitJsonText: Boolean = true
 }
 
+object ImplicitlyFormattedAsJson {
+  /**
+   * Whether `e` lexically emits JSON text -- i.e. carries an implicit 
SQL/JSON `FORMAT JSON` and
+   * should be spliced raw rather than quoted (a nested JSON constructor, or 
`JSON_QUERY` under
+   * `KEEP QUOTES`; see [[ImplicitlyFormattedAsJson]]). Sees through a 
value-preserving `Collate`
+   * (a postfix `... COLLATE c` only annotates the collation of the JSON text, 
e.g.
+   * `JSON_ARRAY(JSON_ARRAY(1) COLLATE UTF8_LCASE)`), but deliberately NOT 
through a `Cast`:
+   * `AstBuilder`/`JsonArray.sql` use a `CAST(... AS STRING)` wrapper 
specifically to detach a value
+   * from implicit `FORMAT JSON`, so unwrapping it would defeat that 
neutralization. The routed/
+   * qualified builders use [[splicesThroughCollationCast]], which 
additionally sees through an
+   * analyzer collation `Cast`.
+   */
+  @scala.annotation.tailrec
+  def isImplicitlyJson(e: Expression): Boolean = e match {
+    case c: Collate => isImplicitlyJson(c.child)
+    case i: ImplicitlyFormattedAsJson => i.emitsImplicitJsonText
+    case _ => false
+  }
+
+  /**
+   * The routed/qualified splice decision made by `JsonArrayExpressionBuilder` 
/
+   * `JsonObjectExpressionBuilder` on a resolved argument. Like 
[[isImplicitlyJson]], but also sees
+   * through an analyzer-inserted collation `Cast`, in any interleaving with 
`Collate`: under an
+   * object/view default collation, `ApplyDefaultCollation` wraps a nested 
producer (also one under
+   * a user `COLLATE`) in a `Cast` to the default `StringType` before the 
builder runs, which would
+   * otherwise hide the producer and quote it. The stock-owned grammar 
wrappers apply the same
+   * predicate when they finalize their flags. Only a genuine collation change 
is unwrapped (the
+   * cast recolors existing JSON text); a user `CAST(... AS STRING)` or the 
`string(...)` alias
+   * still detaches, so the explicit-cast quoting is preserved.
+   */
+  @scala.annotation.tailrec
+  def splicesThroughCollationCast(e: Expression): Boolean = e match {
+    case c: Collate => splicesThroughCollationCast(c.child)
+    case c: Cast if isAnalyzerCollationCast(c) => 
splicesThroughCollationCast(c.child)
+    case i: ImplicitlyFormattedAsJson => i.emitsImplicitJsonText
+    case _ => false
+  }
+
+  private def isAnalyzerCollationCast(c: Cast): Boolean = {
+    // `ApplyDefaultCollation` / `DefaultCollationTypeCoercion` wrap only a 
companion-`StringType`
+    // `DefaultStringProducingExpression` (they skip explicitly collated 
ones), so only that shape
+    // is an analyzer recoloring. User detachments stay quoted: 
`string(json_array(1) COLLATE c)`
+    // (child is a `Collate`), `string(json_array(1 RETURNING STRING COLLATE 
c))` (explicitly
+    // collated child), and `CAST(... AS STRING)` (`USER_SPECIFIED_CAST`). 
Requiring a real
+    // collation change leaves the no-op `string(...)` cast alone.
+    if (!c.child.isInstanceOf[DefaultStringProducingExpression]) return false
+    (c.dataType, c.child.dataType) match {
+      case (to: StringType, from: StringType) =>
+        from.eq(StringType) && to != from && 
!c.containsTag(Cast.USER_SPECIFIED_CAST)

Review Comment:
   Done



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/Analyzer.scala:
##########
@@ -2594,18 +2621,39 @@ class Analyzer(
 
       case q: LogicalPlan =>
         q.transformExpressionsUpWithPruning(
-          _.containsAnyPattern(UNRESOLVED_FUNCTION, GENERATOR),
+          _.containsAnyPattern(UNRESOLVED_FUNCTION, GENERATOR, 
UNRESOLVED_JSON_CONSTRUCTOR),
           ruleId) {
-          case u @ UnresolvedFunction(nameParts, arguments, _, _, _, _, _, _)
-              if functionResolution.hasLambdaAndResolvedArguments(arguments) 
=> withPosition(u) {
-            functionResolution.resolveFunction(u) match {
-              case func: HigherOrderFunction => func
-              case other => other.failAnalysis(
-                errorClass = 
"INVALID_LAMBDA_FUNCTION_CALL.NON_HIGHER_ORDER_FUNCTION",
-                messageParameters = Map(
-                  "class" -> other.getClass.getCanonicalName))
+          case u @ UnresolvedFunction(_, arguments, _, _, _, _, _, _)
+              if functionResolution.hasLambdaAndResolvedArguments(arguments) =>
+            resolveHigherOrderFunction(u)
+
+          // Decide a SQL/JSON constructor's owner in this same bottom-up 
pass, so an enclosing call
+          // and its alias see the result without an extra iteration per 
nesting level.
+          case j: UnresolvedJsonConstructor
+              if functionResolution.routesJsonConstructorToFunction(j) =>

Review Comment:
   Done



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/parser/AstBuilder.scala:
##########
@@ -4391,25 +4391,32 @@ class AstBuilder extends DataTypeAstBuilder
    */
   override def visitJsonArray(ctx: JsonArrayContext): Expression = 
withOrigin(ctx) {
     val arrayValues = ctx.values.asScala.map(v => expression(v.value)).toSeq
-    // Route a flat, clause-free call through routine resolution so it can be 
shadowed. Cheap
-    // clause/nesting checks come first to skip the recursive per-value FORMAT 
scans when a clause
-    // already forces direct construction.
-    val routeThroughResolution =
+    // A clause-free call (no RETURNING, no ON NULL, not a direct JSON 
constructor argument, and no
+    // explicit FORMAT JSON on any value) routes through routine resolution so 
a routine can shadow
+    // it. The cheap clause/nesting checks come first so the recursive 
per-value implicit-JSON scan
+    // runs only when routing is still possible.
+    val clauseFree =
       ctx.returning == null && ctx.nullBehavior == null && 
!isDirectJsonConstructorArgument(ctx) &&
-        !ctx.values.asScala.exists(_.FORMAT() != null) &&
-        !arrayValues.exists(JsonArray.isImplicitlyJson)
-    if (routeThroughResolution) {
+        !ctx.values.asScala.exists(_.FORMAT() != null)
+    if (clauseFree && 
!arrayValues.exists(ImplicitlyFormattedAsJson.isImplicitlyJson)) {
       UnresolvedFunction("json_array", arrayValues, isDistinct = false)
+    } else if (clauseFree) {
+      // Nested clause-free call: route so a routine can shadow it, but carry 
the per-value splice
+      // decision, frozen now from the lexical argument, through resolution 
(see

Review Comment:
   Done



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/jsonExpressions.scala:
##########
@@ -2411,14 +2582,14 @@ case class JsonObjectKeyGuard(child: Expression) 
extends UnaryExpression with No
  *   JSON_OBJECT('id': 7, 'v': NULL ABSENT ON NULL)     -> '{"id":7}'
  *   JSON_OBJECT()                                       -> '{}'
  *
- * A clause-free call that is not itself lexically nested in an enclosing 
constructor's *value*
- * position routes through function resolution and is rebuilt by 
`JsonObjectExpressionBuilder`, so a
- * same-named routine can shadow the built-in `json_object`: always for the 
comma form (a nested
- * producer value routes too, carrying its raw-splice eligibility via 
`JsonImplicitFormatCarrier`),
- * but for the `VALUE` / `:` forms only when no member carries a raw value 
(`rawJson.contains(true)`
- * -- an explicit `FORMAT JSON` or a nested producer in *value* position -- 
keeps it direct).
- * Otherwise it is built directly from the grammar (clause-bearing, or 
value-nested); a nested
- * *key*-position call stays routable (see `AstBuilder.visitJsonObject`).
+ * A clause-free call without explicit `FORMAT JSON`, and not itself lexically 
nested in an
+ * enclosing constructor's *value* position, routes through function 
resolution so a same-named
+ * routine can shadow the built-in `json_object`. A parser-visible nested 
producer value keeps its
+ * raw-splice eligibility in an [[UnresolvedJsonObject]]; a shadow receives 
plain key/value

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