This is an automated email from the ASF dual-hosted git repository.

sunchao pushed a commit to branch branch-4.x
in repository https://gitbox.apache.org/repos/asf/spark.git


The following commit(s) were added to refs/heads/branch-4.x by this push:
     new 1364cb662e62 [SPARK-57990][SQL] Share array-index JSON paths across 
coalesce
1364cb662e62 is described below

commit 1364cb662e624f75e3a9960ec76b06af95036e74
Author: Chao Sun <[email protected]>
AuthorDate: Tue Jul 7 11:56:36 2026 -0700

    [SPARK-57990][SQL] Share array-index JSON paths across coalesce
    
    ## Why are the changes needed?
    
    [SPARK-47670](https://issues.apache.org/jira/browse/SPARK-47670) introduced 
opt-in shared parsing for repeated `get_json_object` calls over the same JSON 
input. [SPARK-57626](https://issues.apache.org/jira/browse/SPARK-57626) 
extended the optimization from top-level fields to repeated literal nested 
object paths.
    
    Array indexes and conditional expressions remain outside the shared-parsing 
path. As described in 
[SPARK-57990](https://issues.apache.org/jira/browse/SPARK-57990), this leaves a 
common object-or-array payload pattern parsing the same JSON repeatedly:
    
    ```sql
    SELECT
      coalesce(
        get_json_object(json, '$.model'),
        get_json_object(json, '$[0].model')) AS model,
      coalesce(
        get_json_object(json, '$.request_id'),
        get_json_object(json, '$[0].request_id')) AS request_id
    FROM events
    ```
    
    For an array-shaped row, each `coalesce` evaluates both extractions. The 
parsing cost therefore continues to scale with the number of projected fields.
    
    ## What changes were proposed in this PR?
    
    This PR extends the existing internal shared parser and optimizer rewrite 
to array-index paths and safe object-or-array `coalesce` fallbacks.
    
    The changes:
    
    - Represent eligible literal JSON paths as named-key and nonnegative 
array-index segments. This supports paths such as `$.items[0].id`, 
`$[0].model`, and `$.matrix[0][1]`.
    - Extend the runtime path trie to traverse object and array roots, nested 
arrays, and mixed object/array paths in one streaming parse.
    - Share compatible `coalesce` branches when every branch is an eligible 
`get_json_object`, optionally wrapped in casts, and every branch reads the same 
JSON attribute.
    - Select only the first object-root and first array-root branch from each 
eligible `coalesce`. These root shapes are mutually exclusive, while later 
same-root fallbacks remain lazy.
    - Keep each selected object/array pair together as one atomic grouping unit 
when prefix conflicts require multiple shared-parser groups.
    - Preserve legacy behavior for duplicate keys and parents, nulls, missing 
indexes, malformed or trailing JSON, non-container intermediate values, raw 
strings, rendering failures, and ancestor/descendant prefix conflicts. Terminal 
array-index JSON null remains the string `"null"`, while object-key JSON null 
remains SQL null.
    - Leave dynamic paths, wildcards, negative, invalid, or overflowing 
indexes, paths deeper than 64 segments, guarded conditionals, and unsafe 
`coalesce` shapes on independent `get_json_object` evaluation.
    - Update the existing configuration description and add optimizer, runtime, 
code-generation, malformed-input, prefix-grouping, and benchmark coverage.
    
    This reuses the existing internal, default-disabled 
`spark.sql.optimizer.getJsonObjectSharedParsing.enabled` configuration. It does 
not add a new expression, configuration, or query migration.
    
    ## How was this PR tested?
    
    The following checks passed locally on JDK 17:
    
    ```bash
    build/sbt \
      "catalyst/testOnly 
org.apache.spark.sql.catalyst.optimizer.OptimizeJsonExprsSuite"
    
    build/sbt \
      "sql/testOnly org.apache.spark.sql.JsonFunctionsSuite"
    
    build/sbt \
      "catalyst/scalastyle" \
      "catalyst/Test/scalastyle" \
      "sql/scalastyle" \
      "sql/Test/scalastyle"
    
    git diff --check
    ```
    
    - `OptimizeJsonExprsSuite`: 31 passed.
    - `JsonFunctionsSuite`: 111 passed.
    - All four Scalastyle targets completed with zero errors and zero warnings.
    - `git diff --check` passed.
    
    The added coverage includes array and mixed object/array paths, safe and 
unsafe `coalesce` shapes, cast preservation, atomic prefix grouping, malformed 
input, duplicate keys and parents, JSON null behavior, missing indexes, 
unsupported paths, and whole-stage code generation.
    
    A microbenchmark was added for 200,000 cached rows, 32 fields, and a 50/50 
mix of object and one-element-array payloads, projecting 2, 4, 8, and 16 
object/array fallback fields. Spark's GitHub Actions benchmark workflow is 
generating the Linux result files before the PR is marked ready for review:
    
    - [JDK 17 
benchmark](https://github.com/sunchao/spark/actions/runs/28842645967)
    - [JDK 21 
benchmark](https://github.com/sunchao/spark/actions/runs/28842650758)
    - [JDK 25 
benchmark](https://github.com/sunchao/spark/actions/runs/28842650019)
    
    ## Does this PR introduce any user-facing change?
    
    Yes, within the unreleased `master` branch only. When the existing internal 
shared-parsing configuration is enabled, eligible repeated array-index paths 
and object-or-array `coalesce` fallbacks now use one shared parse. Query 
results and public APIs are unchanged. With the configuration disabled, 
analyzed and optimized plans remain unchanged. Released Spark versions are 
unaffected.
    
    ## Was this patch authored or co-authored using generative AI tooling?
    
    Generated-by: OpenAI Codex (GPT-5)
    
    Closes #57069 from sunchao/dev/chao/codex/json-array-coalesce-master.
    
    Lead-authored-by: Chao Sun <[email protected]>
    Co-authored-by: sunchao <[email protected]>
    Co-authored-by: Chao Sun <[email protected]>
    Signed-off-by: Chao Sun <[email protected]>
    (cherry picked from commit 64876d2f9668a935516c647565b95f6928cc27ab)
    Signed-off-by: Chao Sun <[email protected]>
---
 .../expressions/json/JsonExpressionEvalUtils.scala | 102 ++++++---
 .../sql/catalyst/expressions/jsonExpressions.scala |  24 ++-
 .../catalyst/optimizer/OptimizeCsvJsonExprs.scala  | 214 ++++++++++++++-----
 .../org/apache/spark/sql/internal/SQLConf.scala    |   5 +-
 .../optimizer/OptimizeJsonExprsSuite.scala         | 227 ++++++++++++++++++++-
 .../SharedJsonParseBenchmark-jdk21-results.txt     |  60 ++++--
 .../SharedJsonParseBenchmark-jdk25-results.txt     |  76 ++++---
 .../SharedJsonParseBenchmark-results.txt           |  60 ++++--
 .../org/apache/spark/sql/JsonFunctionsSuite.scala  | 147 ++++++++++++-
 .../json/SharedJsonParseBenchmark.scala            |  39 ++++
 10 files changed, 804 insertions(+), 150 deletions(-)

diff --git 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/json/JsonExpressionEvalUtils.scala
 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/json/JsonExpressionEvalUtils.scala
index b5d31af45585..e80b4a355b72 100644
--- 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/json/JsonExpressionEvalUtils.scala
+++ 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/json/JsonExpressionEvalUtils.scala
@@ -26,7 +26,7 @@ import com.fasterxml.jackson.core.json.JsonReadFeature
 
 import org.apache.spark.SparkException
 import org.apache.spark.sql.catalyst.InternalRow
-import org.apache.spark.sql.catalyst.expressions.{ExprUtils, 
GenericInternalRow}
+import org.apache.spark.sql.catalyst.expressions.{ExprUtils, 
GenericInternalRow, GetJsonObject}
 import org.apache.spark.sql.catalyst.json.{CreateJacksonParser, 
JacksonGenerator, JacksonParser, JsonInferSchema, JSONOptions}
 import org.apache.spark.sql.catalyst.util.{ArrayData, FailFastMode, 
FailureSafeParser, MapData, PermissiveMode}
 import org.apache.spark.sql.errors.QueryCompilationErrors
@@ -576,26 +576,31 @@ case class GetJsonObjectEvaluator(cachedPath: UTF8String) 
{
 }
 
 /**
- * Evaluates multiple simple named JSON paths in one parse.
+ * Evaluates multiple simple object-key and array-index JSON paths in one 
parse.
  */
 case class MultiGetJsonObjectEvaluator(
     fallbackPaths: Seq[UTF8String],
-    namedPaths: Seq[Seq[String]]) {
+    simplePaths: Seq[Seq[GetJsonObject.SimpleJsonPathSegment]]) {
   import SharedFactory._
 
-  require(fallbackPaths.nonEmpty && namedPaths.length == fallbackPaths.length)
+  require(fallbackPaths.nonEmpty && simplePaths.length == fallbackPaths.length)
 
   @transient
   private lazy val useTopLevelFastPath: Boolean =
-    namedPaths.forall(_.length == 1) && namedPaths.distinct.length == 
namedPaths.length
+    simplePaths.forall {
+      case Seq(_: GetJsonObject.NamedPathSegment) => true
+      case _ => false
+    } && simplePaths.distinct.length == simplePaths.length
 
   @transient
   private lazy val topLevelFieldToOrdinal: Map[String, Int] =
-    namedPaths.zipWithIndex.map { case (path, ordinal) => path.head -> ordinal 
}.toMap
+    simplePaths.zipWithIndex.map { case (path, ordinal) =>
+      path.head.asInstanceOf[GetJsonObject.NamedPathSegment].name -> ordinal
+    }.toMap
 
   @transient
   private lazy val pathTrie: MultiGetJsonObjectEvaluator.PathTrieNode =
-    MultiGetJsonObjectEvaluator.buildPathTrie(namedPaths)
+    MultiGetJsonObjectEvaluator.buildPathTrie(simplePaths)
 
   @transient
   private lazy val nullRow: InternalRow =
@@ -622,23 +627,30 @@ case class MultiGetJsonObjectEvaluator(
     val matched = Array.ofDim[Boolean](fallbackPaths.length)
 
     try {
-      val validObject = Utils.tryWithResource(
+      val validRoot = Utils.tryWithResource(
         CreateJacksonParser.utf8String(jsonFactory, json)) { parser =>
-        if (parser.nextToken() != JsonToken.START_OBJECT) {
-          false
-        } else if (useTopLevelFastPath) {
-          extractTopLevelObject(parser, values, matched)
-        } else {
-          extractObject(parser, pathTrie, values, matched)
+        parser.nextToken() match {
+          case JsonToken.START_OBJECT if pathTrie.namedChildren.isEmpty =>
+            false
+          case JsonToken.START_OBJECT if useTopLevelFastPath =>
+            extractTopLevelObject(parser, values, matched)
+          case JsonToken.START_OBJECT =>
+            extractObject(parser, pathTrie, values, matched)
+          case JsonToken.START_ARRAY if pathTrie.indexedChildren.isEmpty =>
+            false
+          case JsonToken.START_ARRAY =>
+            extractArray(parser, pathTrie, values, matched)
+          case _ =>
+            false
         }
       }
-      if (validObject) {
+      if (validRoot) {
         new GenericInternalRow(values)
       } else {
         nullRow
       }
     } catch {
-      // Every simple named legacy extraction scans through the root object's 
closing token, so a
+      // Every eligible legacy extraction scans through its root container's 
closing token, so a
       // syntax failure makes every sibling null without needing per-path 
reparsing.
       case _: JsonParseException => nullRow
       // A parser-side rendering failure, such as a string-length constraint 
violation, can leave
@@ -681,7 +693,7 @@ case class MultiGetJsonObjectEvaluator(
     var token = parser.nextToken()
     while (valid && token != null && token != JsonToken.END_OBJECT) {
       if (token == JsonToken.FIELD_NAME) {
-        val child = 
node.children.get(parser.currentName).filter(_.hasUnmatched(matched))
+        val child = 
node.namedChildren.get(parser.currentName).filter(_.hasUnmatched(matched))
         val valueToken = parser.nextToken()
         if (child.nonEmpty && valueToken != JsonToken.VALUE_NULL) {
           valid = extractValue(parser, child.get, values, matched)
@@ -698,6 +710,29 @@ case class MultiGetJsonObjectEvaluator(
     valid && token == JsonToken.END_OBJECT
   }
 
+  private def extractArray(
+      parser: JsonParser,
+      node: MultiGetJsonObjectEvaluator.PathTrieNode,
+      values: Array[Any],
+      matched: Array[Boolean]): Boolean = {
+    var valid = true
+    var index = 0L
+    var token = parser.nextToken()
+    while (valid && token != null && token != JsonToken.END_ARRAY) {
+      val child = 
node.indexedChildren.get(index).filter(_.hasUnmatched(matched))
+      if (child.nonEmpty) {
+        valid = extractValue(parser, child.get, values, matched)
+      } else {
+        parser.skipChildren()
+      }
+      if (valid) {
+        token = parser.nextToken()
+        index += 1
+      }
+    }
+    valid && token == JsonToken.END_ARRAY
+  }
+
   private def extractValue(
       parser: JsonParser,
       node: MultiGetJsonObjectEvaluator.PathTrieNode,
@@ -714,6 +749,8 @@ case class MultiGetJsonObjectEvaluator(
       true
     } else if (parser.currentToken == JsonToken.START_OBJECT) {
       extractObject(parser, node, values, matched)
+    } else if (parser.currentToken == JsonToken.START_ARRAY) {
+      extractArray(parser, node, values, matched)
     } else {
       parser.skipChildren()
       true
@@ -794,36 +831,49 @@ case class MultiGetJsonObjectEvaluator(
 object MultiGetJsonObjectEvaluator {
   private final class MutablePathTrieNode {
     val terminalOrdinals: mutable.ArrayBuffer[Int] = mutable.ArrayBuffer.empty
-    val children: mutable.LinkedHashMap[String, MutablePathTrieNode] = 
mutable.LinkedHashMap.empty
+    val namedChildren: mutable.LinkedHashMap[String, MutablePathTrieNode] =
+      mutable.LinkedHashMap.empty
+    val indexedChildren: mutable.LinkedHashMap[Long, MutablePathTrieNode] =
+      mutable.LinkedHashMap.empty
 
     def freeze(): PathTrieNode = {
       require(
-        terminalOrdinals.isEmpty || children.isEmpty,
+        terminalOrdinals.isEmpty || (namedChildren.isEmpty && 
indexedChildren.isEmpty),
         "Shared JSON paths must not be prefixes of one another")
-      val frozenChildren = children.iterator.map { case (name, child) =>
+      val frozenNamedChildren = namedChildren.iterator.map { case (name, 
child) =>
         name -> child.freeze()
       }.toMap
+      val frozenIndexedChildren = indexedChildren.iterator.map { case (index, 
child) =>
+        index -> child.freeze()
+      }.toMap
       val ordinals = (terminalOrdinals.iterator ++
-        
frozenChildren.valuesIterator.flatMap(_.descendantOrdinals.iterator)).toArray
-      PathTrieNode(terminalOrdinals.toArray, frozenChildren, ordinals)
+        
frozenNamedChildren.valuesIterator.flatMap(_.descendantOrdinals.iterator) ++
+        
frozenIndexedChildren.valuesIterator.flatMap(_.descendantOrdinals.iterator)).toArray
+      PathTrieNode(
+        terminalOrdinals.toArray, frozenNamedChildren, frozenIndexedChildren, 
ordinals)
     }
   }
 
   private case class PathTrieNode(
       terminalOrdinals: Array[Int],
-      children: Map[String, PathTrieNode],
+      namedChildren: Map[String, PathTrieNode],
+      indexedChildren: Map[Long, PathTrieNode],
       descendantOrdinals: Array[Int]) {
     def hasUnmatched(matched: Array[Boolean]): Boolean = {
       descendantOrdinals.exists(index => !matched(index))
     }
   }
 
-  private def buildPathTrie(paths: Seq[Seq[String]]): PathTrieNode = {
+  private def buildPathTrie(
+      paths: Seq[Seq[GetJsonObject.SimpleJsonPathSegment]]): PathTrieNode = {
     val root = new MutablePathTrieNode
     paths.zipWithIndex.foreach { case (path, ordinal) =>
       var node = root
-      path.foreach { fieldName =>
-        node = node.children.getOrElseUpdate(fieldName, new 
MutablePathTrieNode)
+      path.foreach {
+        case GetJsonObject.NamedPathSegment(fieldName) =>
+          node = node.namedChildren.getOrElseUpdate(fieldName, new 
MutablePathTrieNode)
+        case GetJsonObject.IndexedPathSegment(index) =>
+          node = node.indexedChildren.getOrElseUpdate(index, new 
MutablePathTrieNode)
       }
       node.terminalOrdinals += ordinal
     }
diff --git 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/jsonExpressions.scala
 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/jsonExpressions.scala
index 471916c68860..b4f41fda42f0 100644
--- 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/jsonExpressions.scala
+++ 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/jsonExpressions.scala
@@ -145,25 +145,31 @@ case class GetJsonObject(json: Expression, path: 
Expression)
 object GetJsonObject {
   import PathInstruction._
 
-  private[sql] def simpleNamedPath(path: UTF8String): Option[Seq[String]] = {
+  private[sql] sealed trait SimpleJsonPathSegment
+  private[sql] case class NamedPathSegment(name: String) extends 
SimpleJsonPathSegment
+  private[sql] case class IndexedPathSegment(index: Long) extends 
SimpleJsonPathSegment
+
+  private[sql] def simplePath(path: UTF8String): 
Option[Seq[SimpleJsonPathSegment]] = {
     try {
       Option(path).flatMap(value => 
JsonPathParser.parse(value.toString)).flatMap { instructions =>
-        val names = instructions.grouped(2).map {
-          case List(Key, Named(fieldName)) => Some(fieldName)
+        val segments = instructions.grouped(2).map {
+          case List(Key, Named(fieldName)) => Some(NamedPathSegment(fieldName))
+          case List(Subscript, Index(index)) if index >= 0 => 
Some(IndexedPathSegment(index))
           case _ => None
         }.toSeq
-        if (names.nonEmpty && names.forall(_.isDefined)) Some(names.flatten) 
else None
+        if (segments.nonEmpty && segments.forall(_.isDefined)) 
Some(segments.flatten) else None
       }
     } catch {
       // Numeric subscripts are parsed as Long and can overflow before the 
parser returns None.
       case _: NumberFormatException => None
     }
   }
+
 }
 
 /**
- * Extracts multiple simple named paths from a JSON string in one parse. This 
is an internal
- * expression used to share sibling [[GetJsonObject]] expressions; unsupported 
and
+ * Extracts multiple simple object-key and array-index paths from a JSON 
string in one parse. This
+ * is an internal expression used to share sibling [[GetJsonObject]] 
expressions; unsupported and
  * prefix-conflicting JSON paths remain as independent GetJsonObject 
expressions.
  */
 case class MultiGetJsonObject(
@@ -194,8 +200,8 @@ case class MultiGetJsonObject(
   final override val nodePatterns: Seq[TreePattern] = Seq(GET_JSON_OBJECT)
 
   @transient
-  private lazy val namedPaths = fallbackPaths.map { path =>
-    GetJsonObject.simpleNamedPath(UTF8String.fromString(path)).getOrElse {
+  private lazy val simplePaths = fallbackPaths.map { path =>
+    GetJsonObject.simplePath(UTF8String.fromString(path)).getOrElse {
       throw new IllegalArgumentException(s"Unsupported shared JSON path: 
$path")
     }
   }
@@ -203,7 +209,7 @@ case class MultiGetJsonObject(
   @transient
   private lazy val evaluator = MultiGetJsonObjectEvaluator(
     fallbackPaths.map(UTF8String.fromString),
-    namedPaths)
+    simplePaths)
 
   override def eval(input: InternalRow): Any = {
     evaluator.evaluate(json.eval(input).asInstanceOf[UTF8String])
diff --git 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/OptimizeCsvJsonExprs.scala
 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/OptimizeCsvJsonExprs.scala
index a95c6807bc2c..ec4ac257b304 100644
--- 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/OptimizeCsvJsonExprs.scala
+++ 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/OptimizeCsvJsonExprs.scala
@@ -44,29 +44,48 @@ import org.apache.spark.unsafe.types.UTF8String
 object OptimizeCsvJsonExprs extends Rule[LogicalPlan] {
   private def nameOfCorruptRecord = conf.columnNameOfCorruptRecord
 
+  private type SimpleJsonPath = Seq[GetJsonObject.SimpleJsonPathSegment]
+  private type SharedJsonCandidate = (GetJsonObject, SimpleJsonPath, String)
+  private type SharedJsonCandidateUnit = Seq[SharedJsonCandidate]
+  private type RequestedJsonPath = (SimpleJsonPath, String)
+  private type RequestedJsonPathUnit = Seq[RequestedJsonPath]
+
   private case class SharedJsonFields(
       json: Expression,
-      paths: Seq[Seq[String]],
+      paths: Seq[SimpleJsonPath],
       alias: Alias) {
-    val ordinalMapping: Map[Seq[String], Int] = paths.zipWithIndex.toMap
+    val ordinalMapping: Map[SimpleJsonPath, Int] = paths.zipWithIndex.toMap
   }
 
   private final class SelectedJsonPathTrieNode {
     var isTerminal: Boolean = false
     var hasSelectedPathInSubtree: Boolean = false
-    val children: mutable.HashMap[String, SelectedJsonPathTrieNode] = 
mutable.HashMap.empty
+    val children: mutable.HashMap[GetJsonObject.SimpleJsonPathSegment, 
SelectedJsonPathTrieNode] =
+      mutable.HashMap.empty
   }
 
   private final class SelectedJsonPathGroup {
     val root = new SelectedJsonPathTrieNode
-    val paths = mutable.ArrayBuffer.empty[(Seq[String], String)]
+    val paths = mutable.ArrayBuffer.empty[RequestedJsonPath]
 
-    def tryAdd(candidate: (Seq[String], String)): Boolean = {
-      val added = addIfNoPrefixConflict(root, candidate._1)
-      if (added) {
-        paths += candidate
+    def tryAdd(unit: RequestedJsonPathUnit): Boolean = {
+      val uniquePaths = mutable.LinkedHashMap.empty[SimpleJsonPath, String]
+      unit.foreach { case (path, fallbackPath) =>
+        uniquePaths.getOrElseUpdate(path, fallbackPath)
+      }
+      val newPaths = uniquePaths.iterator.filterNot { case (path, _) =>
+        containsExactPath(root, path)
+      }.toSeq
+      val conflictsWithGroup = newPaths.exists { case (path, _) =>
+        hasPrefixConflict(root, path)
+      }
+      if (!conflictsWithGroup) {
+        newPaths.foreach { case (path, _) => commitPath(root, path) }
+        paths ++= newPaths
+        true
+      } else {
+        false
       }
-      added
     }
   }
 
@@ -74,46 +93,69 @@ object OptimizeCsvJsonExprs extends Rule[LogicalPlan] {
   // paths retain their existing independent GetJsonObject evaluation.
   private val maxSharedJsonPathDepth = 64
 
-  /** Inserts a path unless a previously selected path is its ancestor or 
descendant. */
-  private def addIfNoPrefixConflict(
+  private def containsExactPath(
       root: SelectedJsonPathTrieNode,
-      path: Seq[String]): Boolean = {
+      path: SimpleJsonPath): Boolean = {
     var node = root
-    val visited = mutable.ArrayBuffer(root)
     var index = 0
-    var hasSelectedPrefix = false
-    while (index < path.length && !hasSelectedPrefix) {
+    while (index < path.length) {
+      node.children.get(path(index)) match {
+        case Some(child) =>
+          node = child
+          index += 1
+        case None =>
+          return false
+      }
+    }
+    node.isTerminal
+  }
+
+  private def hasPrefixConflict(
+      root: SelectedJsonPathTrieNode,
+      path: SimpleJsonPath): Boolean = {
+    var node = root
+    var index = 0
+    while (index < path.length) {
       if (node.isTerminal) {
-        hasSelectedPrefix = true
-      } else {
-        node = node.children.getOrElseUpdate(path(index), new 
SelectedJsonPathTrieNode)
-        visited += node
-        index += 1
+        return true
+      }
+      node.children.get(path(index)) match {
+        case Some(child) =>
+          node = child
+          index += 1
+        case None =>
+          return false
       }
     }
+    !node.isTerminal && node.hasSelectedPathInSubtree
+  }
 
-    val conflicts = hasSelectedPrefix || node.hasSelectedPathInSubtree
-    if (!conflicts) {
-      node.isTerminal = true
-      visited.foreach(_.hasSelectedPathInSubtree = true)
+  private def commitPath(root: SelectedJsonPathTrieNode, path: 
SimpleJsonPath): Unit = {
+    var node = root
+    val visited = mutable.ArrayBuffer(root)
+    path.foreach { segment =>
+      node = node.children.getOrElseUpdate(segment, new 
SelectedJsonPathTrieNode)
+      visited += node
     }
-    !conflicts
+    node.isTerminal = true
+    visited.foreach(_.hasSelectedPathInSubtree = true)
   }
 
-  // First-fit produces the same groups as repeated greedy optimizer passes, 
but does so in one
-  // invocation so parallel prefix chains do not consume one fixed-point 
iteration per depth.
+  // First-fit builds every prefix-free group in one invocation. Coalesce 
candidates are added as
+  // atomic, prefix-free units so their mutually exclusive object/array paths 
always use the same
+  // shared parse.
   private def groupNonConflictingPaths(
-      paths: Iterable[(Seq[String], String)]): Seq[Seq[(Seq[String], String)]] 
= {
+      units: Iterable[RequestedJsonPathUnit]): Seq[Seq[RequestedJsonPath]] = {
     val groups = mutable.ArrayBuffer.empty[SelectedJsonPathGroup]
-    paths.foreach { candidate =>
+    units.foreach { unit =>
       var added = false
       val iterator = groups.iterator
       while (!added && iterator.hasNext) {
-        added = iterator.next().tryAdd(candidate)
+        added = iterator.next().tryAdd(unit)
       }
       if (!added) {
         val group = new SelectedJsonPathGroup
-        require(group.tryAdd(candidate))
+        require(group.tryAdd(unit))
         groups += group
       }
     }
@@ -154,36 +196,35 @@ object OptimizeCsvJsonExprs extends Rule[LogicalPlan] {
   }
 
   /**
-   * Share simple named GetJsonObject paths without changing the 
Hive-compatible semantics of
-   * wildcards or array subscripts. [[MultiGetJsonObject]] preserves the first 
non-null
+   * Share simple named and array-index GetJsonObject paths without changing 
the Hive-compatible
+   * semantics of wildcards. [[MultiGetJsonObject]] preserves the first 
non-null
    * duplicate-key match used by GetJsonObject, unlike JsonTuple. 
Prefix-conflicting paths are
    * placed in separate shared parses so each path retains independent legacy 
evaluation.
    */
   private def shareGetJsonObjects(project: Project): Project = {
-    val candidates = project.projectList.flatMap(collectGetJsonObjectFields)
+    val candidateUnits = 
project.projectList.flatMap(collectGetJsonObjectFields)
     val groups = mutable.ArrayBuffer.empty[
-      (Expression, mutable.ArrayBuffer[(Seq[String], String)])]
+      (Expression, mutable.ArrayBuffer[RequestedJsonPathUnit])]
     val groupsByHash = mutable.HashMap.empty[
-      Int, mutable.ArrayBuffer[(Expression, mutable.ArrayBuffer[(Seq[String], 
String)])]]
+      Int, mutable.ArrayBuffer[(Expression, 
mutable.ArrayBuffer[RequestedJsonPathUnit])]]
 
-    candidates.foreach { case (getJsonObject, pathSegments, path) =>
+    candidateUnits.foreach { unit =>
+      val getJsonObject = unit.head._1
       val bucket = groupsByHash.getOrElseUpdate(
         getJsonObject.json.semanticHash(), mutable.ArrayBuffer.empty)
       bucket.find(_._1.semanticEquals(getJsonObject.json)) match {
-        case Some((_, fields)) => fields += pathSegments -> path
+        case Some((_, fields)) =>
+          fields += unit.map { case (_, pathSegments, path) => pathSegments -> 
path }
         case None =>
-          val group = getJsonObject.json -> mutable.ArrayBuffer(pathSegments 
-> path)
+          val requestedUnit = unit.map { case (_, pathSegments, path) => 
pathSegments -> path }
+          val group = getJsonObject.json -> mutable.ArrayBuffer(requestedUnit)
           bucket += group
           groups += group
       }
     }
 
-    val sharedFields = groups.flatMap { case (json, requestedFields) =>
-      val paths = mutable.LinkedHashMap.empty[Seq[String], String]
-      requestedFields.foreach { case (pathSegments, path) =>
-        paths.getOrElseUpdate(pathSegments, path)
-      }
-      groupNonConflictingPaths(paths).flatMap { nonConflictingPaths =>
+    val sharedFields = groups.flatMap { case (json, requestedUnits) =>
+      groupNonConflictingPaths(requestedUnits).flatMap { nonConflictingPaths =>
         if (nonConflictingPaths.length > 1) {
           val pathSegments = nonConflictingPaths.map(_._1)
           val alias = Alias(
@@ -212,17 +253,22 @@ object OptimizeCsvJsonExprs extends Rule[LogicalPlan] {
   }
 
   private def collectGetJsonObjectFields(
-      expression: Expression): Seq[(GetJsonObject, Seq[String], String)] = {
+      expression: Expression): Seq[SharedJsonCandidateUnit] = {
     expression match {
       case getJsonObject @ GetJsonObject(_: Attribute, Literal(path: 
UTF8String, StringType))
           if getJsonObject.deterministic =>
-        GetJsonObject.simpleNamedPath(path)
+        GetJsonObject.simplePath(path)
           .filter(_.length <= maxSharedJsonPathDepth)
-          .map(pathSegments => (getJsonObject, pathSegments, 
path.toString)).toSeq
+          .map { pathSegments =>
+            Seq((getJsonObject, pathSegments, path.toString))
+          }.toSeq
 
       case _: GetJsonObject =>
         Nil
 
+      case coalesce: Coalesce =>
+        eligibleCoalesceBranches(coalesce).map(_.map(_._2)).toSeq
+
       case other =>
         
getJsonObjectTraversalChild(other).toSeq.flatMap(collectGetJsonObjectFields)
     }
@@ -234,7 +280,7 @@ object OptimizeCsvJsonExprs extends Rule[LogicalPlan] {
     expression match {
       case getJsonObject @ GetJsonObject(json, Literal(path: UTF8String, 
StringType)) =>
         val replacement = for {
-          pathSegments <- GetJsonObject.simpleNamedPath(path)
+          pathSegments <- GetJsonObject.simplePath(path)
           shared <- sharedFieldsByHash.getOrElse(json.semanticHash(), 
Nil).find { candidate =>
             candidate.json.semanticEquals(json) && 
candidate.ordinalMapping.contains(pathSegments)
           }
@@ -244,6 +290,32 @@ object OptimizeCsvJsonExprs extends Rule[LogicalPlan] {
       case _: GetJsonObject =>
         expression
 
+      case coalesce: Coalesce =>
+        eligibleCoalesceBranches(coalesce).map { branches =>
+          val selectedBranches = branches.toMap
+          val firstCandidate = branches.head._2
+          val shared = sharedFieldsByHash
+            .getOrElse(firstCandidate._1.json.semanticHash(), Nil)
+            .find { candidate =>
+              candidate.json.semanticEquals(firstCandidate._1.json) &&
+                branches.forall { case (_, branchCandidate) =>
+                  val (_, pathSegments, _) = branchCandidate
+                  candidate.ordinalMapping.contains(pathSegments)
+                }
+            }
+          shared.map { sharedFields =>
+            val pairSharedFields = Map(
+              firstCandidate._1.json.semanticHash() -> Seq(sharedFields))
+            coalesce.withNewChildren(coalesce.children.zipWithIndex.map { case 
(child, index) =>
+              if (selectedBranches.contains(index)) {
+                rewriteGetJsonObjectFields(child, pairSharedFields)
+              } else {
+                child
+              }
+            })
+          }.getOrElse(coalesce)
+        }.getOrElse(coalesce)
+
       case other =>
         getJsonObjectTraversalChild(other).map { child =>
           other.withNewChildren(
@@ -252,6 +324,50 @@ object OptimizeCsvJsonExprs extends Rule[LogicalPlan] {
     }
   }
 
+  /**
+   * Returns the first object-root and first array-root parser calls from a 
coalesce only when every
+   * branch is a GetJsonObject, optionally wrapped in casts, and all branches 
read the same input
+   * attribute. Only one root shape can match a given input. Later same-shape 
fallbacks and all
+   * casts remain in the outer coalesce, preserving lazy evaluation and branch 
ordering.
+   */
+  private def eligibleCoalesceBranches(
+      coalesce: Coalesce): Option[Seq[(Int, SharedJsonCandidate)]] = {
+    def eligibleBranch(expression: Expression): Option[SharedJsonCandidate] = {
+      expression match {
+        case cast: Cast => eligibleBranch(cast.child)
+        case getJsonObject @ GetJsonObject(_: Attribute, Literal(path: 
UTF8String, StringType))
+            if getJsonObject.deterministic =>
+          GetJsonObject.simplePath(path)
+            .filter(_.length <= maxSharedJsonPathDepth)
+            .map(pathSegments => (getJsonObject, pathSegments, path.toString))
+        case _ => None
+      }
+    }
+
+    val branches = coalesce.children.map(eligibleBranch)
+    if (branches.nonEmpty && branches.forall(_.isDefined)) {
+      val candidates = branches.zipWithIndex.map { case (candidate, index) =>
+        index -> candidate.get
+      }
+      val sameInput = candidates.tail.forall { case (_, candidate) =>
+        candidate._1.json.semanticEquals(candidates.head._2._1.json)
+      }
+      val firstNamed = candidates.find { case (_, candidate) =>
+        candidate._2.head.isInstanceOf[GetJsonObject.NamedPathSegment]
+      }
+      val firstIndexed = candidates.find { case (_, candidate) =>
+        candidate._2.head.isInstanceOf[GetJsonObject.IndexedPathSegment]
+      }
+      if (sameInput && firstNamed.isDefined && firstIndexed.isDefined) {
+        Some(Seq(firstNamed.get, firstIndexed.get).sortBy(_._1))
+      } else {
+        None
+      }
+    } else {
+      None
+    }
+  }
+
   private def getJsonObjectTraversalChild(expression: Expression): 
Option[Expression] = {
     expression match {
       case _: ConditionalExpression | _: And | _: Or | _: In | _: TryEval |
diff --git 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/internal/SQLConf.scala 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/internal/SQLConf.scala
index f41337682378..9a9d5522ce6b 100644
--- a/sql/catalyst/src/main/scala/org/apache/spark/sql/internal/SQLConf.scala
+++ b/sql/catalyst/src/main/scala/org/apache/spark/sql/internal/SQLConf.scala
@@ -3891,8 +3891,9 @@ object SQLConf {
     buildConf("spark.sql.optimizer.getJsonObjectSharedParsing.enabled")
       .internal()
       .doc(s"When true and '${JSON_EXPRESSION_OPTIMIZATION.key}' is also true, 
the optimizer " +
-        "replaces repeated simple named get_json_object paths over the same 
input " +
-        "with one shared parse.")
+        "replaces repeated simple named and array-index get_json_object paths 
over the same " +
+        "input with one shared parse, including mutually exclusive 
object/array coalesce " +
+        "branches.")
       .version("4.3.0")
       .withBindingPolicy(ConfigBindingPolicy.NOT_APPLICABLE)
       .booleanConf
diff --git 
a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/optimizer/OptimizeJsonExprsSuite.scala
 
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/optimizer/OptimizeJsonExprsSuite.scala
index cb551b772ef3..dd0f3dd01327 100644
--- 
a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/optimizer/OptimizeJsonExprsSuite.scala
+++ 
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/optimizer/OptimizeJsonExprsSuite.scala
@@ -228,7 +228,10 @@ class OptimizeJsonExprsSuite extends PlanTest with 
ExpressionEvalHelper {
     assert(!new SQLConf().getJsonObjectSharedParsingEnabled)
     val query = testRelation2.select(
       GetJsonObject($"json", Literal("$.a")).as("a"),
-      GetJsonObject($"json", Literal("$.b")).as("b"))
+      GetJsonObject($"json", Literal("$.b")).as("b"),
+      Coalesce(Seq(
+        GetJsonObject($"json", Literal("$.c")),
+        GetJsonObject($"json", Literal("$[0].c")))).as("c"))
 
     withSQLConf(SQLConf.GET_JSON_OBJECT_SHARED_PARSING_ENABLED.key -> "false") 
{
       comparePlans(Optimizer.execute(query.analyze), query.analyze)
@@ -271,16 +274,205 @@ class OptimizeJsonExprsSuite extends PlanTest with 
ExpressionEvalHelper {
     }
   }
 
-  test("SPARK-57626: leave prefix-conflicting and unsupported paths 
independent") {
+  test("SPARK-57990: share simple array-index get_json_object paths") {
+    val query = testRelation2.select(
+      GetJsonObject($"json", Literal("$[0].a")).as("first_a"),
+      GetJsonObject($"json", Literal("$.items[1].b")).as("second_b"),
+      GetJsonObject($"json", Literal("$[2]")).as("third"))
+    val optimized = Optimizer.execute(query.analyze)
+
+    optimized match {
+      case Project(projectList, Project(innerProjectList, _: LocalRelation)) =>
+        val sharedAlias = innerProjectList.collectFirst {
+          case alias @ Alias(_: MultiGetJsonObject, "_shared_json_paths") => 
alias
+        }.getOrElse(fail(s"Missing shared JSON paths in plan:\n$optimized"))
+        val shared = sharedAlias.child.asInstanceOf[MultiGetJsonObject]
+        assert(shared.fallbackPaths == Seq("$[0].a", "$.items[1].b", "$[2]"))
+
+        val sharedAttr = sharedAlias.toAttribute
+        val extractedFields = projectList.flatMap(_.collect {
+          case getStructField: GetStructField
+              if getStructField.child.semanticEquals(sharedAttr) => 
getStructField
+        })
+        assert(extractedFields.map(_.ordinal) == Seq(0, 1, 2))
+
+      case _ =>
+        fail(s"Expected shared array-index JSON paths below the project, but 
found:\n$optimized")
+    }
+  }
+
+  test("SPARK-57990: share get_json_object paths within coalesce") {
+    val query = testRelation2.select(
+      Coalesce(Seq(
+        Cast(GetJsonObject($"json", Literal("$.a")), IntegerType),
+        Cast(GetJsonObject($"json", Literal("$[0].a")), IntegerType))).as("a"))
+    val optimized = Optimizer.execute(query.analyze)
+
+    optimized match {
+      case Project(Seq(Alias(coalesce: Coalesce, "a")),
+          Project(innerProjectList, _: LocalRelation)) =>
+        val sharedAlias = innerProjectList.collectFirst {
+          case alias @ Alias(_: MultiGetJsonObject, "_shared_json_paths") => 
alias
+        }.getOrElse(fail(s"Missing shared JSON paths in plan:\n$optimized"))
+        val shared = sharedAlias.child.asInstanceOf[MultiGetJsonObject]
+        assert(shared.fallbackPaths == Seq("$.a", "$[0].a"))
+
+        val sharedAttr = sharedAlias.toAttribute
+        assert(coalesce.children.zipWithIndex.forall { case (child, ordinal) =>
+          child match {
+            case Cast(getStructField: GetStructField, IntegerType, _, _) =>
+              getStructField.child.semanticEquals(sharedAttr) && 
getStructField.ordinal == ordinal
+            case _ => false
+          }
+        })
+
+      case _ =>
+        fail(s"Expected shared JSON paths within coalesce, but 
found:\n$optimized")
+    }
+  }
+
+  test("SPARK-57990: share only the first get_json_object path for each 
coalesce root shape") {
+    val query = testRelation2.select(
+      Coalesce(Seq(
+        GetJsonObject($"json", Literal("$.a")),
+        GetJsonObject($"json", Literal("$.b")),
+        GetJsonObject($"json", Literal("$[0].a")),
+        GetJsonObject($"json", Literal("$[0].b")))).as("value"))
+    val optimized = Optimizer.execute(query.analyze)
+
+    optimized match {
+      case Project(Seq(Alias(coalesce: Coalesce, "value")),
+          Project(innerProjectList, _: LocalRelation)) =>
+        val sharedAlias = innerProjectList.collectFirst {
+          case alias @ Alias(_: MultiGetJsonObject, "_shared_json_paths") => 
alias
+        }.getOrElse(fail(s"Missing shared JSON paths in plan:\n$optimized"))
+        val shared = sharedAlias.child.asInstanceOf[MultiGetJsonObject]
+        assert(shared.fallbackPaths == Seq("$.a", "$[0].a"))
+
+        val sharedAttr = sharedAlias.toAttribute
+        coalesce.children.zipWithIndex.foreach {
+          case (getStructField: GetStructField, 0) =>
+            assert(getStructField.child.semanticEquals(sharedAttr))
+            assert(getStructField.ordinal == 0)
+          case (getStructField: GetStructField, 2) =>
+            assert(getStructField.child.semanticEquals(sharedAttr))
+            assert(getStructField.ordinal == 1)
+          case (GetJsonObject(_, Literal(path: UTF8String, StringType)), 1) =>
+            assert(path.toString == "$.b")
+          case (GetJsonObject(_, Literal(path: UTF8String, StringType)), 3) =>
+            assert(path.toString == "$[0].b")
+          case (child, index) =>
+            fail(s"Unexpected coalesce branch $index after sharing: $child")
+        }
+
+      case _ =>
+        fail(s"Expected selectively shared coalesce paths, but 
found:\n$optimized")
+    }
+  }
+
+  test("SPARK-57990: keep coalesce path pairs atomic across prefix groups") {
+    // $.a conflicts with $.a.x but not $[0].x, so both coalesce paths must 
move together.
+    val query = testRelation2.select(
+      GetJsonObject($"json", Literal("$.a")).as("a"),
+      Coalesce(Seq(
+        GetJsonObject($"json", Literal("$[0].x")),
+        GetJsonObject($"json", Literal("$.a.x")))).as("x"),
+      Coalesce(Seq(
+        GetJsonObject($"json", Literal("$[0].y")),
+        GetJsonObject($"json", Literal("$.a.y")))).as("y"))
+    val optimized = Optimizer.execute(query.analyze)
+
+    optimized match {
+      case Project(projectList, Project(innerProjectList, _: LocalRelation)) =>
+        val sharedAliases = innerProjectList.collect {
+          case alias @ Alias(_: MultiGetJsonObject, "_shared_json_paths") => 
alias
+        }
+        assert(sharedAliases.length == 1)
+        val sharedAlias = sharedAliases.head
+        val shared = sharedAlias.child.asInstanceOf[MultiGetJsonObject]
+        assert(shared.fallbackPaths == Seq("$[0].x", "$.a.x", "$[0].y", 
"$.a.y"))
+
+        val sharedAttr = sharedAlias.toAttribute
+        Seq("x", "y").foreach { name =>
+          val coalesce = projectList.collectFirst {
+            case Alias(expression: Coalesce, aliasName) if aliasName == name 
=> expression
+          }.getOrElse(fail(s"Missing $name coalesce in plan:\n$optimized"))
+          assert(coalesce.children.forall {
+            case getStructField: GetStructField =>
+              getStructField.child.semanticEquals(sharedAttr)
+            case _ => false
+          })
+        }
+
+        assert(projectList.exists {
+          case Alias(GetJsonObject(_, Literal(path: UTF8String, StringType)), 
"a") =>
+            path.toString == "$.a"
+          case _ => false
+        })
+
+      case _ =>
+        fail(s"Expected atomic shared coalesce paths, but found:\n$optimized")
+    }
+  }
+
+  test("SPARK-57990: share get_json_object paths within coalesce below an 
outer cast") {
+    val query = testRelation2.select(
+      Cast(Coalesce(Seq(
+        GetJsonObject($"json", Literal("$.a")),
+        GetJsonObject($"json", Literal("$[0].a")))), IntegerType).as("a"))
+    val optimized = Optimizer.execute(query.analyze)
+
+    val shared = optimized.collect {
+      case Project(projectList, _) => projectList.collectFirst {
+        case Alias(shared: MultiGetJsonObject, "_shared_json_paths") => shared
+      }
+    }.flatten.headOption.getOrElse(fail(s"Missing shared JSON paths in 
plan:\n$optimized"))
+    assert(shared.fallbackPaths == Seq("$.a", "$[0].a"))
+
+    optimized match {
+      case Project(Seq(Alias(Cast(coalesce: Coalesce, IntegerType, _, _), 
"a")), _) =>
+        assert(coalesce.children.forall(_.isInstanceOf[GetStructField]))
+      case _ => fail(s"Expected cast around rewritten coalesce, but 
found:\n$optimized")
+    }
+  }
+
+  test("SPARK-57990: do not share unsafe coalesce branches") {
+    val otherJsonAttr = $"other_json".string
+    val relation = LocalRelation(jsonAttr, otherJsonAttr)
+    val queries = Seq(
+      relation.select(Coalesce(Seq(
+        GetJsonObject($"json", Literal("$.a")),
+        GetJsonObject($"json", Literal("$.b")))).as("value")),
+      relation.select(Coalesce(Seq(
+        GetJsonObject($"json", Literal("$.a")),
+        Literal("fallback"),
+        GetJsonObject($"json", Literal("$.b")))).as("value")),
+      relation.select(Coalesce(Seq(
+        GetJsonObject($"json", Literal("$.a")),
+        GetJsonObject($"other_json", Literal("$.b")))).as("value")),
+      relation.select(Coalesce(Seq(
+        GetJsonObject($"json", Literal("$.a")),
+        GetJsonObject($"json", Literal("$.items[*]")))).as("value")))
+
+    queries.foreach { query =>
+      val optimized = Optimizer.execute(query.analyze)
+      assert(!optimized.exists { plan =>
+        plan.expressions.exists(_.exists(_.isInstanceOf[MultiGetJsonObject]))
+      })
+      comparePlans(optimized, query.analyze)
+    }
+  }
+
+  test("SPARK-57990: leave prefix-conflicting and unsupported paths 
independent") {
     val deepPath = (1 to 65).map(index => s"field$index").mkString("$.", ".", 
"")
-    val legacyPaths = Seq("$.a.b", "$.items[0].id", "$.a.*", deepPath)
+    val legacyPaths = Seq("$.a.b", "$.a.*", deepPath)
     val query = testRelation2.select(
       GetJsonObject($"json", Literal("$.a")).as("a"),
       GetJsonObject($"json", Literal(legacyPaths(0))).as("nested"),
       GetJsonObject($"json", Literal("$.c.d")).as("d"),
-      GetJsonObject($"json", Literal(legacyPaths(1))).as("array"),
-      GetJsonObject($"json", Literal(legacyPaths(2))).as("wildcard"),
-      GetJsonObject($"json", Literal(legacyPaths(3))).as("deep"),
+      GetJsonObject($"json", Literal("$.items[0].id")).as("array"),
+      GetJsonObject($"json", Literal(legacyPaths(1))).as("wildcard"),
+      GetJsonObject($"json", Literal(legacyPaths(2))).as("deep"),
       GetJsonObject($"json", Literal("$.e")).as("e"))
     val optimized = Optimizer.execute(query.analyze)
 
@@ -290,7 +482,7 @@ class OptimizeJsonExprsSuite extends PlanTest with 
ExpressionEvalHelper {
       }
     }.flatten.headOption.getOrElse(fail(s"Missing shared JSON paths in 
plan:\n$optimized"))
       .child.asInstanceOf[MultiGetJsonObject]
-    assert(shared.fallbackPaths == Seq("$.a", "$.c.d", "$.e"))
+    assert(shared.fallbackPaths == Seq("$.a", "$.c.d", "$.items[0].id", "$.e"))
 
     val remainingPaths = optimized.expressions.flatMap(_.collect {
       case GetJsonObject(_, Literal(path: UTF8String, StringType)) => 
path.toString
@@ -319,6 +511,27 @@ class OptimizeJsonExprsSuite extends PlanTest with 
ExpressionEvalHelper {
     }))
   }
 
+  test("SPARK-57990: keep array-index prefix conflicts independent") {
+    val query = testRelation2.select(
+      GetJsonObject($"json", Literal("$[0].a")).as("a"),
+      GetJsonObject($"json", Literal("$[0]")).as("first"),
+      GetJsonObject($"json", Literal("$[0].b")).as("b"),
+      GetJsonObject($"json", Literal("$[1]")).as("second"))
+    val optimized = Optimizer.execute(query.analyze)
+
+    val shared = optimized.collect {
+      case Project(projectList, _) => projectList.collectFirst {
+        case alias @ Alias(_: MultiGetJsonObject, "_shared_json_paths") => 
alias
+      }
+    }.flatten.headOption.getOrElse(fail(s"Missing shared JSON paths in 
plan:\n$optimized"))
+      .child.asInstanceOf[MultiGetJsonObject]
+    assert(shared.fallbackPaths == Seq("$[0].a", "$[0].b", "$[1]"))
+    assert(optimized.expressions.exists(_.exists {
+      case GetJsonObject(_, Literal(path: UTF8String, StringType)) => 
path.toString == "$[0]"
+      case _ => false
+    }))
+  }
+
   test("SPARK-57626: share parallel prefix chains in one optimizer 
invocation") {
     def prefixChain(root: String): Seq[String] = (1 to 9).map { depth =>
       (1 to depth).map(index => s"$root$index").mkString("$.", ".", "")
diff --git a/sql/core/benchmarks/SharedJsonParseBenchmark-jdk21-results.txt 
b/sql/core/benchmarks/SharedJsonParseBenchmark-jdk21-results.txt
index f4e2d3112fbb..f8d596422dc1 100644
--- a/sql/core/benchmarks/SharedJsonParseBenchmark-jdk21-results.txt
+++ b/sql/core/benchmarks/SharedJsonParseBenchmark-jdk21-results.txt
@@ -6,56 +6,84 @@ OpenJDK 64-Bit Server VM 21.0.11+10-LTS on Linux 
6.17.0-1018-azure
 AMD EPYC 7763 64-Core Processor
 get_json_object extracting 2 of 32 fields:  Best Time(ms)   Avg Time(ms)   
Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
 
-------------------------------------------------------------------------------------------------------------------------
-shared parsing off                                  1625           1667        
  46          0.1        8125.0       1.0X
-shared parsing on                                    910            933        
  20          0.2        4552.2       1.8X
+shared parsing off                                  1685           1714        
  25          0.1        8426.5       1.0X
+shared parsing on                                    965            977        
  15          0.2        4824.9       1.7X
 
 OpenJDK 64-Bit Server VM 21.0.11+10-LTS on Linux 6.17.0-1018-azure
 AMD EPYC 7763 64-Core Processor
 get_json_object extracting 4 of 32 fields:  Best Time(ms)   Avg Time(ms)   
Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
 
-------------------------------------------------------------------------------------------------------------------------
-shared parsing off                                  3154           3178        
  21          0.1       15770.7       1.0X
-shared parsing on                                   1130           1178        
  45          0.2        5649.4       2.8X
+shared parsing off                                  3308           3389        
  81          0.1       16539.7       1.0X
+shared parsing on                                   1116           1124        
   7          0.2        5579.1       3.0X
 
 OpenJDK 64-Bit Server VM 21.0.11+10-LTS on Linux 6.17.0-1018-azure
 AMD EPYC 7763 64-Core Processor
 get_json_object extracting 8 of 32 fields:  Best Time(ms)   Avg Time(ms)   
Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
 
-------------------------------------------------------------------------------------------------------------------------
-shared parsing off                                  6067           6175        
  93          0.0       30333.8       1.0X
-shared parsing on                                   1234           1265        
  27          0.2        6170.1       4.9X
+shared parsing off                                  6565           6597        
  51          0.0       32824.1       1.0X
+shared parsing on                                   1341           1346        
   6          0.1        6706.1       4.9X
 
 OpenJDK 64-Bit Server VM 21.0.11+10-LTS on Linux 6.17.0-1018-azure
 AMD EPYC 7763 64-Core Processor
 get_json_object extracting 16 of 32 fields:  Best Time(ms)   Avg Time(ms)   
Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
 
--------------------------------------------------------------------------------------------------------------------------
-shared parsing off                                  11938          11946       
   13          0.0       59690.5       1.0X
-shared parsing on                                    1699           1709       
   15          0.1        8493.5       7.0X
+shared parsing off                                  13148          13178       
   30          0.0       65742.5       1.0X
+shared parsing on                                    1800           1820       
   18          0.1        8999.8       7.3X
 
 OpenJDK 64-Bit Server VM 21.0.11+10-LTS on Linux 6.17.0-1018-azure
 AMD EPYC 7763 64-Core Processor
 get_json_object extracting 2 of 32 nested fields:  Best Time(ms)   Avg 
Time(ms)   Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
 
--------------------------------------------------------------------------------------------------------------------------------
-shared parsing off                                         1600           1607 
         10          0.1        8001.8       1.0X
-shared parsing on                                           873            900 
         25          0.2        4367.2       1.8X
+shared parsing off                                         1565           1572 
          6          0.1        7822.6       1.0X
+shared parsing on                                           917            922 
          7          0.2        4587.4       1.7X
 
 OpenJDK 64-Bit Server VM 21.0.11+10-LTS on Linux 6.17.0-1018-azure
 AMD EPYC 7763 64-Core Processor
 get_json_object extracting 4 of 32 nested fields:  Best Time(ms)   Avg 
Time(ms)   Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
 
--------------------------------------------------------------------------------------------------------------------------------
-shared parsing off                                         3120           3156 
         35          0.1       15600.5       1.0X
-shared parsing on                                          1102           1115 
         18          0.2        5509.8       2.8X
+shared parsing off                                         3044           3068 
         30          0.1       15221.3       1.0X
+shared parsing on                                          1125           1131 
         11          0.2        5626.8       2.7X
 
 OpenJDK 64-Bit Server VM 21.0.11+10-LTS on Linux 6.17.0-1018-azure
 AMD EPYC 7763 64-Core Processor
 get_json_object extracting 8 of 32 nested fields:  Best Time(ms)   Avg 
Time(ms)   Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
 
--------------------------------------------------------------------------------------------------------------------------------
-shared parsing off                                         6362           6408 
         46          0.0       31809.2       1.0X
-shared parsing on                                          1347           1367 
         19          0.1        6733.2       4.7X
+shared parsing off                                         6062           6070 
          7          0.0       30310.2       1.0X
+shared parsing on                                          1355           1373 
         22          0.1        6772.6       4.5X
 
 OpenJDK 64-Bit Server VM 21.0.11+10-LTS on Linux 6.17.0-1018-azure
 AMD EPYC 7763 64-Core Processor
 get_json_object extracting 16 of 32 nested fields:  Best Time(ms)   Avg 
Time(ms)   Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
 
---------------------------------------------------------------------------------------------------------------------------------
-shared parsing off                                         12772          
12831          54          0.0       63861.1       1.0X
-shared parsing on                                           1852           
1868          15          0.1        9260.4       6.9X
+shared parsing off                                         12061          
12098          34          0.0       60305.8       1.0X
+shared parsing on                                           1913           
1919           5          0.1        9565.3       6.3X
+
+OpenJDK 64-Bit Server VM 21.0.11+10-LTS on Linux 6.17.0-1018-azure
+AMD EPYC 7763 64-Core Processor
+get_json_object coalescing 2 object/array field paths:  Best Time(ms)   Avg 
Time(ms)   Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
+-------------------------------------------------------------------------------------------------------------------------------------
+shared parsing off                                              2456           
2466          10          0.1       12279.2       1.0X
+shared parsing on                                               1002           
1009           9          0.2        5009.5       2.5X
+
+OpenJDK 64-Bit Server VM 21.0.11+10-LTS on Linux 6.17.0-1018-azure
+AMD EPYC 7763 64-Core Processor
+get_json_object coalescing 4 object/array field paths:  Best Time(ms)   Avg 
Time(ms)   Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
+-------------------------------------------------------------------------------------------------------------------------------------
+shared parsing off                                              4835           
4844           9          0.0       24174.8       1.0X
+shared parsing on                                               1276           
1293          17          0.2        6381.5       3.8X
+
+OpenJDK 64-Bit Server VM 21.0.11+10-LTS on Linux 6.17.0-1018-azure
+AMD EPYC 7763 64-Core Processor
+get_json_object coalescing 8 object/array field paths:  Best Time(ms)   Avg 
Time(ms)   Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
+-------------------------------------------------------------------------------------------------------------------------------------
+shared parsing off                                              9578           
9604          27          0.0       47890.8       1.0X
+shared parsing on                                               1466           
1478          11          0.1        7329.2       6.5X
+
+OpenJDK 64-Bit Server VM 21.0.11+10-LTS on Linux 6.17.0-1018-azure
+AMD EPYC 7763 64-Core Processor
+get_json_object coalescing 16 object/array field paths:  Best Time(ms)   Avg 
Time(ms)   Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
+--------------------------------------------------------------------------------------------------------------------------------------
+shared parsing off                                              19267          
19315          50          0.0       96336.7       1.0X
+shared parsing on                                                1975          
 1992          15          0.1        9874.2       9.8X
 
 
diff --git a/sql/core/benchmarks/SharedJsonParseBenchmark-jdk25-results.txt 
b/sql/core/benchmarks/SharedJsonParseBenchmark-jdk25-results.txt
index 5ab0fd777a7f..5efc7ab149ce 100644
--- a/sql/core/benchmarks/SharedJsonParseBenchmark-jdk25-results.txt
+++ b/sql/core/benchmarks/SharedJsonParseBenchmark-jdk25-results.txt
@@ -3,59 +3,87 @@ Benchmark for sharing repeated get_json_object parsing
 
================================================================================================
 
 OpenJDK 64-Bit Server VM 25.0.3+9-LTS on Linux 6.17.0-1018-azure
-AMD EPYC 9V74 80-Core Processor
+AMD EPYC 7763 64-Core Processor
 get_json_object extracting 2 of 32 fields:  Best Time(ms)   Avg Time(ms)   
Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
 
-------------------------------------------------------------------------------------------------------------------------
-shared parsing off                                  1567           1580        
  18          0.1        7835.8       1.0X
-shared parsing on                                    927            933        
   6          0.2        4634.1       1.7X
+shared parsing off                                  1583           1612        
  36          0.1        7914.7       1.0X
+shared parsing on                                    956            985        
  37          0.2        4780.2       1.7X
 
 OpenJDK 64-Bit Server VM 25.0.3+9-LTS on Linux 6.17.0-1018-azure
-AMD EPYC 9V74 80-Core Processor
+AMD EPYC 7763 64-Core Processor
 get_json_object extracting 4 of 32 fields:  Best Time(ms)   Avg Time(ms)   
Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
 
-------------------------------------------------------------------------------------------------------------------------
-shared parsing off                                  3002           3015        
  14          0.1       15008.5       1.0X
-shared parsing on                                   1187           1218        
  28          0.2        5933.7       2.5X
+shared parsing off                                  3082           3090        
   8          0.1       15407.6       1.0X
+shared parsing on                                   1087           1096        
   8          0.2        5435.4       2.8X
 
 OpenJDK 64-Bit Server VM 25.0.3+9-LTS on Linux 6.17.0-1018-azure
-AMD EPYC 9V74 80-Core Processor
+AMD EPYC 7763 64-Core Processor
 get_json_object extracting 8 of 32 fields:  Best Time(ms)   Avg Time(ms)   
Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
 
-------------------------------------------------------------------------------------------------------------------------
-shared parsing off                                  6027           6030        
   2          0.0       30136.0       1.0X
-shared parsing on                                   1290           1305        
  16          0.2        6449.2       4.7X
+shared parsing off                                  6098           6123        
  29          0.0       30491.0       1.0X
+shared parsing on                                   1300           1321        
  28          0.2        6498.6       4.7X
 
 OpenJDK 64-Bit Server VM 25.0.3+9-LTS on Linux 6.17.0-1018-azure
-AMD EPYC 9V74 80-Core Processor
+AMD EPYC 7763 64-Core Processor
 get_json_object extracting 16 of 32 fields:  Best Time(ms)   Avg Time(ms)   
Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
 
--------------------------------------------------------------------------------------------------------------------------
-shared parsing off                                  12003          12020       
   25          0.0       60015.7       1.0X
-shared parsing on                                    1724           1728       
    5          0.1        8620.3       7.0X
+shared parsing off                                  12234          12259       
   42          0.0       61168.6       1.0X
+shared parsing on                                    1744           1751       
    7          0.1        8717.7       7.0X
 
 OpenJDK 64-Bit Server VM 25.0.3+9-LTS on Linux 6.17.0-1018-azure
-AMD EPYC 9V74 80-Core Processor
+AMD EPYC 7763 64-Core Processor
 get_json_object extracting 2 of 32 nested fields:  Best Time(ms)   Avg 
Time(ms)   Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
 
--------------------------------------------------------------------------------------------------------------------------------
-shared parsing off                                         1620           1637 
         16          0.1        8098.1       1.0X
-shared parsing on                                           966            968 
          2          0.2        4831.3       1.7X
+shared parsing off                                         1615           1627 
         13          0.1        8074.2       1.0X
+shared parsing on                                           931            953 
         19          0.2        4655.2       1.7X
 
 OpenJDK 64-Bit Server VM 25.0.3+9-LTS on Linux 6.17.0-1018-azure
-AMD EPYC 9V74 80-Core Processor
+AMD EPYC 7763 64-Core Processor
 get_json_object extracting 4 of 32 nested fields:  Best Time(ms)   Avg 
Time(ms)   Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
 
--------------------------------------------------------------------------------------------------------------------------------
-shared parsing off                                         3107           3136 
         28          0.1       15532.8       1.0X
-shared parsing on                                          1185           1193 
          7          0.2        5925.4       2.6X
+shared parsing off                                         3164           3178 
         14          0.1       15822.3       1.0X
+shared parsing on                                          1258           1262 
          4          0.2        6287.7       2.5X
 
 OpenJDK 64-Bit Server VM 25.0.3+9-LTS on Linux 6.17.0-1018-azure
-AMD EPYC 9V74 80-Core Processor
+AMD EPYC 7763 64-Core Processor
 get_json_object extracting 8 of 32 nested fields:  Best Time(ms)   Avg 
Time(ms)   Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
 
--------------------------------------------------------------------------------------------------------------------------------
-shared parsing off                                         6162           6178 
         21          0.0       30809.9       1.0X
-shared parsing on                                          1405           1421 
         14          0.1        7025.9       4.4X
+shared parsing off                                         6272           6283 
         10          0.0       31361.5       1.0X
+shared parsing on                                          1473           1476 
          3          0.1        7366.8       4.3X
 
 OpenJDK 64-Bit Server VM 25.0.3+9-LTS on Linux 6.17.0-1018-azure
-AMD EPYC 9V74 80-Core Processor
+AMD EPYC 7763 64-Core Processor
 get_json_object extracting 16 of 32 nested fields:  Best Time(ms)   Avg 
Time(ms)   Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
 
---------------------------------------------------------------------------------------------------------------------------------
-shared parsing off                                         12341          
12350          12          0.0       61704.0       1.0X
-shared parsing on                                           1834           
1846          10          0.1        9171.9       6.7X
+shared parsing off                                         12533          
12549          20          0.0       62667.3       1.0X
+shared parsing on                                           1990           
2010          20          0.1        9950.0       6.3X
+
+OpenJDK 64-Bit Server VM 25.0.3+9-LTS on Linux 6.17.0-1018-azure
+AMD EPYC 7763 64-Core Processor
+get_json_object coalescing 2 object/array field paths:  Best Time(ms)   Avg 
Time(ms)   Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
+-------------------------------------------------------------------------------------------------------------------------------------
+shared parsing off                                              2203           
2221          18          0.1       11012.6       1.0X
+shared parsing on                                                989           
 999          10          0.2        4945.7       2.2X
+
+OpenJDK 64-Bit Server VM 25.0.3+9-LTS on Linux 6.17.0-1018-azure
+AMD EPYC 7763 64-Core Processor
+get_json_object coalescing 4 object/array field paths:  Best Time(ms)   Avg 
Time(ms)   Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
+-------------------------------------------------------------------------------------------------------------------------------------
+shared parsing off                                              4413           
4420           7          0.0       22065.9       1.0X
+shared parsing on                                               1187           
1200          13          0.2        5933.0       3.7X
+
+OpenJDK 64-Bit Server VM 25.0.3+9-LTS on Linux 6.17.0-1018-azure
+AMD EPYC 7763 64-Core Processor
+get_json_object coalescing 8 object/array field paths:  Best Time(ms)   Avg 
Time(ms)   Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
+-------------------------------------------------------------------------------------------------------------------------------------
+shared parsing off                                              8692           
8705          14          0.0       43459.9       1.0X
+shared parsing on                                               1375           
1389          16          0.1        6873.5       6.3X
+
+OpenJDK 64-Bit Server VM 25.0.3+9-LTS on Linux 6.17.0-1018-azure
+AMD EPYC 7763 64-Core Processor
+get_json_object coalescing 16 object/array field paths:  Best Time(ms)   Avg 
Time(ms)   Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
+--------------------------------------------------------------------------------------------------------------------------------------
+shared parsing off                                              17609          
17639          28          0.0       88043.8       1.0X
+shared parsing on                                                1906          
 1913          11          0.1        9527.6       9.2X
 
 
diff --git a/sql/core/benchmarks/SharedJsonParseBenchmark-results.txt 
b/sql/core/benchmarks/SharedJsonParseBenchmark-results.txt
index 6c90a4479c3a..f7982bc9c311 100644
--- a/sql/core/benchmarks/SharedJsonParseBenchmark-results.txt
+++ b/sql/core/benchmarks/SharedJsonParseBenchmark-results.txt
@@ -6,56 +6,84 @@ OpenJDK 64-Bit Server VM 17.0.19+10-LTS on Linux 
6.17.0-1018-azure
 AMD EPYC 7763 64-Core Processor
 get_json_object extracting 2 of 32 fields:  Best Time(ms)   Avg Time(ms)   
Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
 
-------------------------------------------------------------------------------------------------------------------------
-shared parsing off                                  1577           1587        
  15          0.1        7883.7       1.0X
-shared parsing on                                    932            946        
  16          0.2        4661.0       1.7X
+shared parsing off                                  1550           1571        
  25          0.1        7750.2       1.0X
+shared parsing on                                    874            889        
  13          0.2        4370.5       1.8X
 
 OpenJDK 64-Bit Server VM 17.0.19+10-LTS on Linux 6.17.0-1018-azure
 AMD EPYC 7763 64-Core Processor
 get_json_object extracting 4 of 32 fields:  Best Time(ms)   Avg Time(ms)   
Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
 
-------------------------------------------------------------------------------------------------------------------------
-shared parsing off                                  3005           3052        
  41          0.1       15025.5       1.0X
-shared parsing on                                   1074           1075        
   2          0.2        5370.1       2.8X
+shared parsing off                                  2940           2948        
  12          0.1       14701.0       1.0X
+shared parsing on                                   1018           1026        
   7          0.2        5090.3       2.9X
 
 OpenJDK 64-Bit Server VM 17.0.19+10-LTS on Linux 6.17.0-1018-azure
 AMD EPYC 7763 64-Core Processor
 get_json_object extracting 8 of 32 fields:  Best Time(ms)   Avg Time(ms)   
Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
 
-------------------------------------------------------------------------------------------------------------------------
-shared parsing off                                  6090           6104        
  12          0.0       30448.1       1.0X
-shared parsing on                                   1315           1321        
   8          0.2        6575.3       4.6X
+shared parsing off                                  5820           5833        
  20          0.0       29099.7       1.0X
+shared parsing on                                   1256           1257        
   2          0.2        6280.4       4.6X
 
 OpenJDK 64-Bit Server VM 17.0.19+10-LTS on Linux 6.17.0-1018-azure
 AMD EPYC 7763 64-Core Processor
 get_json_object extracting 16 of 32 fields:  Best Time(ms)   Avg Time(ms)   
Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
 
--------------------------------------------------------------------------------------------------------------------------
-shared parsing off                                  12144          12159       
   17          0.0       60719.4       1.0X
-shared parsing on                                    1840           1848       
    7          0.1        9201.7       6.6X
+shared parsing off                                  11588          11623       
   38          0.0       57942.2       1.0X
+shared parsing on                                    1736           1742       
    8          0.1        8681.5       6.7X
 
 OpenJDK 64-Bit Server VM 17.0.19+10-LTS on Linux 6.17.0-1018-azure
 AMD EPYC 7763 64-Core Processor
 get_json_object extracting 2 of 32 nested fields:  Best Time(ms)   Avg 
Time(ms)   Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
 
--------------------------------------------------------------------------------------------------------------------------------
-shared parsing off                                         1498           1509 
          9          0.1        7492.3       1.0X
-shared parsing on                                           894            895 
          1          0.2        4471.6       1.7X
+shared parsing off                                         1585           1595 
          9          0.1        7922.5       1.0X
+shared parsing on                                           914            917 
          3          0.2        4572.3       1.7X
 
 OpenJDK 64-Bit Server VM 17.0.19+10-LTS on Linux 6.17.0-1018-azure
 AMD EPYC 7763 64-Core Processor
 get_json_object extracting 4 of 32 nested fields:  Best Time(ms)   Avg 
Time(ms)   Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
 
--------------------------------------------------------------------------------------------------------------------------------
-shared parsing off                                         2969           2978 
          8          0.1       14845.4       1.0X
-shared parsing on                                          1129           1131 
          4          0.2        5644.8       2.6X
+shared parsing off                                         3134           3151 
         26          0.1       15672.4       1.0X
+shared parsing on                                          1155           1159 
          6          0.2        5774.6       2.7X
 
 OpenJDK 64-Bit Server VM 17.0.19+10-LTS on Linux 6.17.0-1018-azure
 AMD EPYC 7763 64-Core Processor
 get_json_object extracting 8 of 32 nested fields:  Best Time(ms)   Avg 
Time(ms)   Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
 
--------------------------------------------------------------------------------------------------------------------------------
-shared parsing off                                         5841           5847 
          9          0.0       29206.9       1.0X
-shared parsing on                                          1378           1384 
          7          0.1        6889.6       4.2X
+shared parsing off                                         6302           6311 
         14          0.0       31509.0       1.0X
+shared parsing on                                          1377           1382 
         10          0.1        6883.6       4.6X
 
 OpenJDK 64-Bit Server VM 17.0.19+10-LTS on Linux 6.17.0-1018-azure
 AMD EPYC 7763 64-Core Processor
 get_json_object extracting 16 of 32 nested fields:  Best Time(ms)   Avg 
Time(ms)   Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
 
---------------------------------------------------------------------------------------------------------------------------------
-shared parsing off                                         11856          
11904          64          0.0       59278.4       1.0X
-shared parsing on                                           1926           
1936          12          0.1        9630.4       6.2X
+shared parsing off                                         12228          
12247          16          0.0       61139.6       1.0X
+shared parsing on                                           1901           
1907           8          0.1        9504.9       6.4X
+
+OpenJDK 64-Bit Server VM 17.0.19+10-LTS on Linux 6.17.0-1018-azure
+AMD EPYC 7763 64-Core Processor
+get_json_object coalescing 2 object/array field paths:  Best Time(ms)   Avg 
Time(ms)   Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
+-------------------------------------------------------------------------------------------------------------------------------------
+shared parsing off                                              2199           
2203           4          0.1       10994.3       1.0X
+shared parsing on                                                999           
1001           2          0.2        4995.2       2.2X
+
+OpenJDK 64-Bit Server VM 17.0.19+10-LTS on Linux 6.17.0-1018-azure
+AMD EPYC 7763 64-Core Processor
+get_json_object coalescing 4 object/array field paths:  Best Time(ms)   Avg 
Time(ms)   Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
+-------------------------------------------------------------------------------------------------------------------------------------
+shared parsing off                                              4322           
4339          16          0.0       21608.3       1.0X
+shared parsing on                                               1239           
1249           9          0.2        6195.2       3.5X
+
+OpenJDK 64-Bit Server VM 17.0.19+10-LTS on Linux 6.17.0-1018-azure
+AMD EPYC 7763 64-Core Processor
+get_json_object coalescing 8 object/array field paths:  Best Time(ms)   Avg 
Time(ms)   Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
+-------------------------------------------------------------------------------------------------------------------------------------
+shared parsing off                                              8652           
8658           6          0.0       43257.7       1.0X
+shared parsing on                                               1397           
1400           3          0.1        6987.2       6.2X
+
+OpenJDK 64-Bit Server VM 17.0.19+10-LTS on Linux 6.17.0-1018-azure
+AMD EPYC 7763 64-Core Processor
+get_json_object coalescing 16 object/array field paths:  Best Time(ms)   Avg 
Time(ms)   Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
+--------------------------------------------------------------------------------------------------------------------------------------
+shared parsing off                                              17561          
17574          22          0.0       87802.5       1.0X
+shared parsing on                                                1908          
 1908           1          0.1        9537.9       9.2X
 
 
diff --git 
a/sql/core/src/test/scala/org/apache/spark/sql/JsonFunctionsSuite.scala 
b/sql/core/src/test/scala/org/apache/spark/sql/JsonFunctionsSuite.scala
index 396b86144f4d..4bbd3b533d34 100644
--- a/sql/core/src/test/scala/org/apache/spark/sql/JsonFunctionsSuite.scala
+++ b/sql/core/src/test/scala/org/apache/spark/sql/JsonFunctionsSuite.scala
@@ -27,7 +27,8 @@ import com.fasterxml.jackson.core.StreamReadConstraints
 
 import org.apache.spark.{SparkException, SparkRuntimeException}
 import org.apache.spark.sql.catalyst.InternalRow
-import org.apache.spark.sql.catalyst.expressions.{JsonToStructs, Literal, 
MultiGetJsonObject}
+import org.apache.spark.sql.catalyst.expressions.{GetJsonObject, 
JsonToStructs, Literal,
+  MultiGetJsonObject}
 import org.apache.spark.sql.catalyst.expressions.Cast._
 import org.apache.spark.sql.catalyst.util.TimestampNanosTestUtils
 import 
org.apache.spark.sql.catalyst.util.TimestampNanosTestUtils.foreachNanosPrecision
@@ -38,6 +39,7 @@ import org.apache.spark.sql.test.SharedSparkSession
 import org.apache.spark.sql.types._
 import org.apache.spark.sql.types.DayTimeIntervalType.{DAY, HOUR, MINUTE, 
SECOND}
 import org.apache.spark.sql.types.YearMonthIntervalType.{MONTH, YEAR}
+import org.apache.spark.unsafe.types.UTF8String
 
 class JsonFunctionsSuite extends SharedSparkSession {
   import testImplicits._
@@ -167,6 +169,149 @@ class JsonFunctionsSuite extends SharedSparkSession {
       Row("single", "7", "8", "9")))
   }
 
+  test("SPARK-57990: share simple array-index get_json_object paths") {
+    val input = Seq[String](
+      """[{"name":"root-zero","value":"raw"},{"name":"root-one"}]""",
+      
"""{"items":[{"name":"item-zero"},{"name":"item-one","value":{"x":1}}],""" +
+        """"matrix":[[0,"one"]]}""",
+      """{"items":[null,{"name":"after-null","value":[1,2]}],""" +
+        """"matrix":[[0,null]]}""",
+      """{"items":[{"name":"first"}],"items":[{"name":"second-zero"},""" +
+        """{"name":"second-one"}]}""",
+      """{"items":[{"name":null,"name":"after-null"},""" +
+        """{"name":"first","name":"second"}]}""",
+      """{"items":[{"name":"before"},{"bad":"\q"}],"other":1}""",
+      """[{"name":"before"},{"bad":"\q"}]""",
+      """{'items':[{'name':'single'},{'value':7}]}""",
+      """[]""",
+      """{}""",
+      """1""",
+      null)
+
+    def result(jsonOptimization: Boolean, sharedParsing: Boolean): Seq[Row] = {
+      var rows = Seq.empty[Row]
+      withSQLConf(
+          SQLConf.JSON_EXPRESSION_OPTIMIZATION.key -> 
jsonOptimization.toString,
+          SQLConf.GET_JSON_OBJECT_SHARED_PARSING_ENABLED.key -> 
sharedParsing.toString) {
+        val query = input.toDF("json").select(
+          get_json_object($"json", "$[0].name"),
+          get_json_object($"json", "$[1].name"),
+          get_json_object($"json", "$[0].value"),
+          get_json_object($"json", "$.items[0].name"),
+          get_json_object($"json", "$.items[1].name"),
+          get_json_object($"json", "$.items[1].value"),
+          get_json_object($"json", "$.matrix[0][1]"),
+          get_json_object($"json", "$.items[9].name"))
+        val hasSharedParsing = query.queryExecution.optimizedPlan.exists { 
plan =>
+          plan.expressions.exists(_.exists(_.isInstanceOf[MultiGetJsonObject]))
+        }
+        if (jsonOptimization) {
+          assert(hasSharedParsing == sharedParsing)
+        }
+        rows = query.collect().toSeq
+      }
+      rows
+    }
+
+    val legacy = result(jsonOptimization = false, sharedParsing = false)
+    assert(result(jsonOptimization = true, sharedParsing = false) == legacy)
+    assert(result(jsonOptimization = true, sharedParsing = true) == legacy)
+    assert(legacy.take(8) == Seq(
+      Row("root-zero", "root-one", "raw", null, null, null, null, null),
+      Row(null, null, null, "item-zero", "item-one", "{\"x\":1}", "one", null),
+      Row(null, null, null, null, "after-null", "[1,2]", "null", null),
+      Row(null, null, null, "first", "second-one", null, null, null),
+      Row(null, null, null, "after-null", "first", null, null, null),
+      Row(null, null, null, null, null, null, null, null),
+      Row(null, null, null, null, null, null, null, null),
+      Row(null, null, null, "single", null, "7", null, null)))
+  }
+
+  test("SPARK-57990: shared get_json_object preserves object-or-array coalesce 
semantics") {
+    val input = Seq[String](
+      """{"name":"object","nested":[{"name":"object-nested"}]}""",
+      """[{"name":"array","nested":[{"name":"array-nested"}]},{"other":1}]""",
+      """{"name":null,"nested":[null]}""",
+      """[null]""",
+      """1""",
+      """[{"name":"before"},{"bad":"\q"}]""",
+      """{"name":"before","bad":"\q"}""",
+      null)
+
+    def result(sharedParsing: Boolean): Seq[Row] = {
+      var rows = Seq.empty[Row]
+      withSQLConf(
+          SQLConf.JSON_EXPRESSION_OPTIMIZATION.key -> "true",
+          SQLConf.GET_JSON_OBJECT_SHARED_PARSING_ENABLED.key -> 
sharedParsing.toString) {
+        val query = input.toDF("json").select(
+          coalesce(
+            get_json_object($"json", "$.name"),
+            get_json_object($"json", "$[0].name")),
+          coalesce(
+            get_json_object($"json", "$.nested[0].name"),
+            get_json_object($"json", "$[0].nested[0].name")))
+        val hasSharedParsing = query.queryExecution.optimizedPlan.exists { 
plan =>
+          plan.expressions.exists(_.exists(_.isInstanceOf[MultiGetJsonObject]))
+        }
+        assert(hasSharedParsing == sharedParsing)
+        rows = query.collect().toSeq
+      }
+      rows
+    }
+
+    val legacy = result(sharedParsing = false)
+    assert(result(sharedParsing = true) == legacy)
+    assert(legacy == Seq(
+      Row("object", "object-nested"),
+      Row("array", "array-nested"),
+      Row(null, null),
+      Row(null, null),
+      Row(null, null),
+      Row(null, null),
+      Row(null, null),
+      Row(null, null)))
+  }
+
+  test("SPARK-57990: shared get_json_object validates simple array-index 
paths") {
+    import GetJsonObject.{IndexedPathSegment, NamedPathSegment}
+
+    def simplePath(path: String): 
Option[Seq[GetJsonObject.SimpleJsonPathSegment]] = {
+      GetJsonObject.simplePath(UTF8String.fromString(path))
+    }
+
+    assert(simplePath("$.a[0]['b'][2]") == Some(Seq(
+      NamedPathSegment("a"), IndexedPathSegment(0), NamedPathSegment("b"),
+      IndexedPathSegment(2))))
+    Seq("$", "$[*]", "$.a[*]", "$.a[-1]", "$.a[1x]", "$[9223372036854775808]")
+      .foreach(path => assert(simplePath(path).isEmpty, path))
+
+    val expression = MultiGetJsonObject(
+      Literal("""["raw",{"x":1},[2,3],null]"""),
+      Seq("$[0]", "$[1]", "$[1]", "$[2][1]", "$[3]", "$[9]"))
+    val row = expression.eval().asInstanceOf[InternalRow]
+    assert(row.getUTF8String(0).toString == "raw")
+    assert(row.getUTF8String(1).toString == "{\"x\":1}")
+    assert(row.getUTF8String(2).toString == "{\"x\":1}")
+    assert(row.getUTF8String(3).toString == "3")
+    assert(row.getUTF8String(4).toString == "null")
+    assert(row.isNullAt(5))
+    assert(row.getUTF8String(4) == GetJsonObject(
+      Literal("""["raw",{"x":1},[2,3],null]"""), Literal("$[3]")).eval())
+
+    val nestedNull = MultiGetJsonObject(
+      Literal("""{"a":[null],"b":1}"""), Seq("$.a[0]", "$.b")).eval()
+      .asInstanceOf[InternalRow]
+    assert(nestedNull.getUTF8String(0).toString == "null")
+    assert(nestedNull.getUTF8String(0) == GetJsonObject(
+      Literal("""{"a":[null],"b":1}"""), Literal("$.a[0]")).eval())
+    assert(nestedNull.getUTF8String(1).toString == "1")
+
+    val prefixConflict = MultiGetJsonObject(
+      Literal("""[{"x":1}]"""), Seq("$[0]", "$[0].x"))
+    val error = intercept[IllegalArgumentException](prefixConflict.eval())
+    assert(error.getMessage.contains("must not be prefixes"))
+  }
+
   test("SPARK-57626: shared nested get_json_object isolates value rendering 
failures") {
     val invalidSurrogate = "\\" + "uD800"
     val input = Seq(
diff --git 
a/sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/json/SharedJsonParseBenchmark.scala
 
b/sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/json/SharedJsonParseBenchmark.scala
index 48aa5842858b..8a9f74b66b91 100644
--- 
a/sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/json/SharedJsonParseBenchmark.scala
+++ 
b/sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/json/SharedJsonParseBenchmark.scala
@@ -109,6 +109,45 @@ object SharedJsonParseBenchmark extends SqlBasedBenchmark {
       }
 
       nestedData.unpersist()
+
+      val payload = struct(Seq.tabulate(fieldCount) { index =>
+        fieldValue.as(s"field_$index")
+      }: _*)
+      val mixedObjectArrayData = spark.range(0, rows, 1, 4)
+        .select(when(($"id" % 2) === 0, to_json(payload))
+          .otherwise(to_json(array(payload)))
+          .as("json"))
+        .cache()
+      mixedObjectArrayData.count()
+
+      Seq(2, 4, 8, 16).foreach { selectedFieldCount =>
+        val pathBenchmark = new Benchmark(
+          s"get_json_object coalescing $selectedFieldCount object/array field 
paths",
+          rows,
+          output = output)
+
+        def extractPaths(sharedParsing: Boolean): Unit = {
+          withSQLConf(
+              SQLConf.JSON_EXPRESSION_OPTIMIZATION.key -> "true",
+              SQLConf.GET_JSON_OBJECT_SHARED_PARSING_ENABLED.key -> 
sharedParsing.toString) {
+            mixedObjectArrayData.select(Seq.tabulate(selectedFieldCount) { 
index =>
+              coalesce(
+                get_json_object($"json", s"$$.field_$index"),
+                get_json_object($"json", s"$$[0].field_$index"))
+            }: _*).noop()
+          }
+        }
+
+        pathBenchmark.addCase("shared parsing off", 3) { _ =>
+          extractPaths(sharedParsing = false)
+        }
+        pathBenchmark.addCase("shared parsing on", 3) { _ =>
+          extractPaths(sharedParsing = true)
+        }
+        pathBenchmark.run()
+      }
+
+      mixedObjectArrayData.unpersist()
     }
   }
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to