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


##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/jsonExpressions.scala:
##########
@@ -339,6 +341,286 @@ case class JsonTuple(children: Seq[Expression])
     copy(children = newChildren)
 }
 
+/**
+ * The kind of a single `JSON_TABLE` column.
+ */
+sealed trait JsonTableColumnKind
+object JsonTableColumnKind {
+  /** A `FOR ORDINALITY` column: a 1-based sequential row counter. */
+  case object Ordinality extends JsonTableColumnKind
+  /** A regular value column: extracts the value at `path` and casts it to 
`dataType`. */
+  case object Value extends JsonTableColumnKind
+  /** An `EXISTS` column: true when `path` matches, cast to `dataType`. */
+  case object Exists extends JsonTableColumnKind
+}
+
+/**
+ * A single column definition of a `JSON_TABLE` invocation.
+ *
+ * @param name     the output column name
+ * @param dataType the declared Spark type of the column (LongType for 
ORDINALITY columns)
+ * @param path     the SQL/JSON path relative to a row item; None for 
ORDINALITY columns
+ * @param kind     the column kind (ordinality / value / exists)
+ */
+case class JsonTableColumn(
+    name: String,
+    dataType: DataType,
+    path: Option[String],
+    kind: JsonTableColumnKind)
+
+/**
+ * Behavior when the JSON input is malformed.
+ */
+sealed trait JsonTableErrorMode
+object JsonTableErrorMode {
+  /** Produce no rows on malformed input (the SQL-standard default). */
+  case object NullOnError extends JsonTableErrorMode
+  /** Raise an error on malformed input. */
+  case object ErrorOnError extends JsonTableErrorMode
+}
+
+// scalastyle:off line.size.limit
+/**
+ * The SQL:2016 `JSON_TABLE` table-valued function. Shreds a JSON document 
into a relational table:
+ * the `rowPath` selects a sequence of row items and each [[JsonTableColumn]] 
projects a value out
+ * of each item. Implemented as a [[Generator]] so it plugs into the existing, 
well-tested
+ * [[org.apache.spark.sql.catalyst.plans.logical.Generate]] operator; no new 
execution operator is
+ * introduced.
+ *
+ * Only the flat (non-`NESTED PATH`) subset of the standard is supported. 
Row-source and value
+ * extraction use the token-aware [[JsonTableEvaluator]], which (unlike 
`get_json_object`)
+ * distinguishes a missing path from a JSON `null` value; type coercion reuses 
[[Cast]].
+ *
+ * {{{
+ *   SELECT t.* FROM json_table(
+ *     '{"items":[{"id":1,"n":"a"},{"id":2,"n":"b"}]}',
+ *     '$.items[*]'
+ *     COLUMNS (seq FOR ORDINALITY, id INT PATH '$.id', name STRING PATH '$.n')
+ *   ) AS t;
+ * }}}
+ */
+// scalastyle:on line.size.limit
+case class JsonTable(
+    child: Expression,
+    rowPath: String,
+    columns: Seq[JsonTableColumn],
+    errorMode: JsonTableErrorMode,
+    timeZoneId: Option[String] = None,
+    // Captured at plan-construction time so column casts do not change 
behavior if the session's
+    // ANSI mode is flipped between building the plan and executing it 
(matching `Cast`, which
+    // fixes its eval mode when the expression is constructed).
+    ansiEnabled: Boolean = SQLConf.get.ansiEnabled)
+  extends UnaryExpression
+  with Generator
+  with TimeZoneAwareExpression
+  with CodegenFallback
+  with ImplicitCastInputTypes
+  with QueryErrorsBase {
+
+  // Declared via ImplicitCastInputTypes so the analyzer coerces the JSON 
input to STRING. In
+  // particular an untyped SQL NULL (NullType) is cast to STRING rather than 
rejected, so
+  // `JSON_TABLE(NULL, ...)` reaches the runtime and applies the NULL ON ERROR 
behavior.
+  override def inputTypes: Seq[AbstractDataType] =
+    Seq(StringTypeWithCollation(supportsTrimCollation = true))
+
+  // ORDINALITY columns always hold a non-null counter; value/EXISTS columns 
may be null.
+  override def elementSchema: StructType =
+    StructType(columns.map { c =>
+      val nullable = c.kind != JsonTableColumnKind.Ordinality
+      StructField(c.name, c.dataType, nullable = nullable)
+    })
+
+  override def withTimeZone(timeZoneId: String): TimeZoneAwareExpression =
+    copy(timeZoneId = Option(timeZoneId))
+
+  override def checkInputDataTypes(): TypeCheckResult = {
+    // First the standard input-type check (STRING for the JSON input, with 
NULL coerced).
+    val inputCheck = super.checkInputDataTypes()
+    if (inputCheck.isFailure) {
+      inputCheck
+    } else {
+      // Validate the row path and every column path. A path is valid here iff 
it parses and is
+      // free of wildcards -- except the row path may end in a single `[*]`, 
which is stripped into
+      // `containerInstructions`, so the row path is checked on that 
already-stripped list.
+      val rowPathValid = JsonPathParser.parse(rowPath).isDefined &&
+        !containerInstructions.contains(PathInstruction.Wildcard)
+      val invalid: Option[(String, String)] = if (!rowPathValid) {
+        Some(("row path", rowPath))
+      } else {
+        columns.iterator.collect { case c if c.path.isDefined => (c.name, 
c.path.get) }
+          .collectFirst {
+            // Valid column path: parses and is wildcard-free, i.e. 
hasWildcard == Some(false).
+            case (name, path) if 
!JsonPathParser.hasWildcard(path).contains(false) =>
+              (s"column '$name'", path)
+          }
+      }
+      invalid match {
+        case Some((location, path)) =>
+          DataTypeMismatch(
+            errorSubClass = "INVALID_JSON_TABLE_PATH",
+            messageParameters = Map("location" -> location, "path" -> 
toSQLValue(path)))
+        case None =>
+          // Every value/EXISTS column is produced by casting from a source 
type (StringType for
+          // value columns, BooleanType for EXISTS columns) to the declared 
column type. Reject a
+          // non-castable declared type (e.g. a value column declared 
STRUCT/ARRAY/MAP) here rather
+          // than failing at runtime. Ordinality columns are always LongType 
and need no check.
+          // The castability rules differ between ANSI and non-ANSI mode (e.g. 
BOOLEAN -> TIMESTAMP
+          // is allowed by non-ANSI casts but not ANSI casts), so this must 
use the same eval mode
+          // as the actual per-column `Cast` built in `columnCasts`.
+          def sourceType(c: JsonTableColumn): Option[DataType] = c.kind match {
+            case JsonTableColumnKind.Value => Some(StringType)
+            case JsonTableColumnKind.Exists => Some(BooleanType)
+            case JsonTableColumnKind.Ordinality => None
+          }
+          def castable(src: DataType, target: DataType): Boolean =
+            if (ansiEnabled) Cast.canAnsiCast(src, target) else 
Cast.canCast(src, target)
+          columns.iterator.flatMap(c => sourceType(c).map((c, _)))
+            .collectFirst { case (c, src) if !castable(src, c.dataType) => (c, 
src) } match {
+            case Some((c, srcType)) =>
+              DataTypeMismatch(
+                errorSubClass = "CAST_WITHOUT_SUGGESTION",
+                messageParameters = Map(
+                  "srcType" -> toSQLType(srcType),
+                  "targetType" -> toSQLType(c.dataType)))
+            case None =>
+              TypeCheckResult.TypeCheckSuccess
+          }
+      }
+    }
+  }
+
+  // The row path is `containerRowPath` plus an optional trailing `[*]`. 
Splitting on the parsed
+  // instruction list (rather than the raw string) is whitespace-insensitive 
and unambiguous.
+  // `checkInputDataTypes` guarantees the path parses and is wildcard-free at 
this point.
+  @transient private lazy val (containerInstructions, explodeRoot)
+      : (Seq[PathInstruction], Boolean) = {
+    val parsed = JsonPathParser.parse(rowPath).getOrElse(Nil)
+    parsed match {
+      case init :+ PathInstruction.Subscript :+ PathInstruction.Wildcard =>
+        (init, true)
+      case other =>
+        (other, false)
+    }
+  }
+
+  @transient private lazy val rowEvaluator: JsonTableEvaluator =
+    JsonTableEvaluator(containerInstructions, explodeRoot)
+
+  // Parsed instruction list per column (empty for ordinality columns, which 
have no path).
+  @transient private lazy val columnPaths: Array[Seq[PathInstruction]] =
+    columns.map(c => 
c.path.flatMap(JsonPathParser.parse).getOrElse(Nil)).toArray
+
+  // Prefix trie over the column paths, built once so every row's value/EXISTS 
columns are resolved
+  // in a single traversal of the item rather than one re-parse per column. 
Ordinality columns have
+  // no path and are excluded (their empty path must not be confused with a 
root path `$`, which is
+  // an included column reading the whole item).
+  @transient private lazy val columnTrie: JsonTablePathTrie = {
+    val include = columns.map(_.kind != JsonTableColumnKind.Ordinality).toArray
+    rowEvaluator.buildPathTrie(columnPaths, include)
+  }
+
+  // One reusable Cast per non-ordinality column, evaluated against a 
single-slot mutable input
+  // row. Building the Cast once (over a BoundReference) avoids allocating an 
expression tree per
+  // row/column on the hot path. The source type is BooleanType for EXISTS, 
StringType otherwise.
+  @transient private lazy val columnCasts: Array[Expression] = {
+    val evalMode = EvalMode.fromBoolean(ansiEnabled)
+    columns.map { c =>
+      c.kind match {
+        case JsonTableColumnKind.Ordinality => null
+        case JsonTableColumnKind.Exists =>
+          Cast(BoundReference(0, BooleanType, nullable = false), c.dataType, 
timeZoneId, evalMode)
+        case JsonTableColumnKind.Value =>
+          Cast(BoundReference(0, StringType, nullable = true), c.dataType, 
timeZoneId, evalMode)
+      }
+    }.toArray
+  }
+
+  // Reusable single-slot input row for the per-column casts above.
+  @transient private lazy val castInput: GenericInternalRow = new 
GenericInternalRow(1)
+
+  private def castColumn(i: Int, value: Any): Any = {
+    castInput.update(0, value)
+    columnCasts(i).eval(castInput)
+  }
+
+  private def projectRow(item: UTF8String, ordinal: Long): InternalRow = {
+    // Resolve every value/EXISTS column in a single traversal of the item; 
ordinality slots are
+    // not in the trie and come back as Missing (filled below).
+    val resolved = rowEvaluator.navigateColumns(item, columnTrie, 
columns.length)
+    val values = new Array[Any](columns.length)
+    var i = 0
+    while (i < columns.length) {
+      values(i) = columns(i).kind match {
+        case JsonTableColumnKind.Ordinality =>
+          ordinal
+        case JsonTableColumnKind.Exists =>
+          // Present (including an explicit JSON null) counts as existing; 
only Missing is false.
+          val exists = resolved(i) != JsonPathResult.Missing
+          castColumn(i, exists)
+        case JsonTableColumnKind.Value =>
+          resolved(i) match {
+            // `raw` is a re-parseable JSON fragment; unquote a scalar string 
so the column gets
+            // its content (e.g. `"hi"` -> `hi`), then cast to the declared 
type.
+            case JsonPathResult.Found(raw) => castColumn(i, 
rowEvaluator.unquotedString(raw))

Review Comment:
   Confirmed; non-string fragments now take the parser-free fast path.



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/json/JsonExpressionEvalUtils.scala:
##########
@@ -350,6 +357,462 @@ case class JsonTupleEvaluator(foldableFieldNames: 
Array[Option[String]]) {
   }
 }
 
+/**
+ * The three-state result of navigating a JSON path for `JSON_TABLE`. 
`get_json_object` collapses
+ * "the path is absent" and "the value is JSON null" into a single `null`, 
which is wrong for
+ * `JSON_TABLE`: `EXISTS` must treat a present-but-null value as existing, and 
a value column must
+ * distinguish SQL `NULL` from the literal string `"null"`. This ADT keeps the 
two cases distinct.
+ */
+sealed trait JsonPathResult
+object JsonPathResult {
+  /** The path did not match (the key/index is absent). */
+  case object Missing extends JsonPathResult
+  /** The path matched a JSON `null` literal. */
+  case object NullValue extends JsonPathResult
+  /** The path matched a value; `raw` is its verbatim JSON text (including 
quoted strings). */
+  case class Found(raw: UTF8String) extends JsonPathResult
+}
+
+/**
+ * A prefix trie over the (wildcard-free) column paths of a single 
`JSON_TABLE` invocation, built
+ * once via [[JsonTableEvaluator.buildPathTrie]] and reused for every row. It 
lets
+ * [[JsonTableEvaluator.navigateAll]] resolve all columns in a single 
traversal of a row item
+ * instead of re-parsing the item once per column.
+ *
+ * Each node groups the paths that share a common prefix: `named`/`indexed` 
hold the object-key and
+ * array-index steps to child nodes, and `terminals` lists the result-slot 
indices of the columns
+ * whose path ends exactly at this node.
+ */
+private[expressions] final class JsonTablePathTrie {
+  // Result-slot indices of columns whose path terminates at this node.
+  var terminals: List[Int] = Nil
+  // Object-key children, keyed by field name.
+  val named: mutable.HashMap[String, JsonTablePathTrie] = mutable.HashMap.empty
+  // Array-index children, keyed by index.
+  val indexed: mutable.HashMap[Long, JsonTablePathTrie] = mutable.HashMap.empty
+
+  def hasChildren: Boolean = named.nonEmpty || indexed.nonEmpty
+
+  /** True if no column path was inserted (e.g. an ordinality-only table): 
nothing to resolve. */
+  def isEmpty: Boolean = terminals.isEmpty && !hasChildren
+}
+
+/**
+ * The result of positioning a parser at a JSON path for the `JSON_TABLE` row 
source (see
+ * `positionAt`). Like [[JsonPathResult]] it distinguishes a missing path from 
a JSON `null`, but
+ * `AtValue` leaves the parser on the matched value's first token (rather than 
serializing it) so
+ * the row source can be streamed.
+ */
+sealed trait PositionResult
+object PositionResult {
+  /** The path did not match. */
+  case object Missing extends PositionResult
+  /** The path matched a JSON `null` literal. */
+  case object NullValue extends PositionResult
+  /** The path matched a value; the parser is positioned at its first token. */
+  case object AtValue extends PositionResult
+}
+
+/**
+ * Row-source and column extraction for the SQL `JSON_TABLE` function. Given 
the JSON input, the
+ * (wildcard-free) container path, and whether the row path ended in `[*]`, it 
produces the
+ * per-row JSON documents that the 
[[org.apache.spark.sql.catalyst.expressions.JsonTable]]
+ * generator then projects into columns via [[navigate]].
+ *
+ *   - `$.items[*]` (containerPath `$.items`, `explodeRoot` = true): the 
container must be an
+ *     array; each element becomes a row.
+ *   - `$` or `$.x` (`explodeRoot` = false): the matched value becomes exactly 
one row.
+ *
+ * Unlike `get_json_object`, navigation here is token-aware and distinguishes 
missing keys from
+ * JSON `null` values (see [[JsonPathResult]]).
+ *
+ * The input is required to be exactly one well-formed JSON value (no trailing 
garbage, not
+ * empty); anything else is treated as malformed, so the caller applies the ON 
ERROR behavior
+ * consistently in both modes.
+ */
+case class JsonTableEvaluator(containerPath: Seq[PathInstruction], 
explodeRoot: Boolean) {
+  import PathInstruction._
+  import SharedFactory._
+
+  /**
+   * Returns the per-row JSON documents selected by the row path as an 
iterator, or `None` if the
+   * JSON is null or malformed, or if `[*]` was applied to a non-array (the 
caller maps `None` to
+   * the configured ON ERROR behavior). A well-formed input whose row path 
matches nothing returns
+   * `Some(empty iterator)`.
+   *
+   * The input is first scanned once to validate it is a single well-formed 
JSON value (so trailing
+   * garbage is rejected consistently in both ON ERROR modes -- this pass is 
O(n) tokens and does
+   * not materialize values). For the array (`[*]`) case the elements are then 
serialized one at a
+   * time from a second parser, so the whole expanded payload is never held in 
memory at once.
+   */
+  final def evaluate(json: UTF8String): Option[Iterator[UTF8String]] = {
+    if (json == null || !isSingleWellFormedValue(json)) return None
+    // The parser is positioned at the matched value and, for the array case, 
handed to a lazy
+    // iterator that reads elements directly from it -- the container is never 
serialized whole.
+    // Ownership of `parser` transfers to that iterator (which closes it on 
exhaustion); in every
+    // other branch we close it before returning.
+    val parser = CreateJacksonParser.utf8String(jsonFactory, json)
+    var transferred = false
+    try {
+      parser.nextToken()
+      positionAt(parser, containerPath) match {
+        case PositionResult.Missing =>
+          // Well-formed JSON, but the row path matched nothing: no rows.
+          Some(Iterator.empty)
+        case PositionResult.NullValue =>
+          // The container is JSON null. `[*]` over a non-array is an error; 
otherwise one row.
+          if (explodeRoot) None else 
Some(Iterator.single(UTF8String.fromString("null")))
+        case PositionResult.AtValue =>
+          if (explodeRoot) {
+            // `[*]` requires an array; a non-array match is an error.
+            if (parser.currentToken != JsonToken.START_ARRAY) {
+              None
+            } else {
+              val it = arrayElementIterator(parser) // owns and eventually 
closes `parser`
+              transferred = true
+              Some(it)
+            }
+          } else {
+            Some(Iterator.single(serializeCurrentValue(parser)))
+          }
+      }
+    } catch {
+      case _: JsonProcessingException => None
+    } finally {
+      if (!transferred) parser.close()
+    }
+  }
+
+  /**
+   * Navigates `path` and leaves the parser positioned at the first token of 
the matched value
+   * (returning `AtValue`), or returns `Missing`/`NullValue`. Unlike 
[[navigateTo]] this does not
+   * serialize the value or finish consuming the enclosing containers -- the 
caller either streams
+   * from the current position (array row source) or serializes the single 
matched value.
+   */
+  private def positionAt(parser: JsonParser, path: Seq[PathInstruction]): 
PositionResult = {
+    path match {
+      case Nil =>
+        if (parser.currentToken == JsonToken.VALUE_NULL) 
PositionResult.NullValue
+        else PositionResult.AtValue
+
+      case Key :: Named(name) :: rest =>
+        if (parser.currentToken != JsonToken.START_OBJECT) {
+          skipRest(parser)
+          PositionResult.Missing
+        } else {
+          var token = parser.nextToken()
+          while (token != null && token != JsonToken.END_OBJECT) {
+            if (parser.currentName == name) {
+              parser.nextToken() // move onto the value; stop here (first 
match wins)
+              return positionAt(parser, rest)
+            }
+            parser.nextToken()
+            parser.skipChildren()
+            token = parser.nextToken()
+          }
+          PositionResult.Missing
+        }
+
+      case Subscript :: Index(index) :: rest =>
+        if (parser.currentToken != JsonToken.START_ARRAY) {
+          skipRest(parser)
+          PositionResult.Missing
+        } else {
+          var i = 0L
+          var token = parser.nextToken()
+          while (token != null && token != JsonToken.END_ARRAY) {
+            if (i == index) {
+              return positionAt(parser, rest)
+            }
+            parser.skipChildren()
+            i += 1
+            token = parser.nextToken()
+          }
+          PositionResult.Missing
+        }
+
+      case _ =>
+        // Should not happen: JSON_TABLE paths are validated to be simple and 
wildcard-free.
+        skipRest(parser)
+        PositionResult.Missing
+    }
+  }
+
+  /**
+   * Returns true if the input is exactly one well-formed JSON value with no 
trailing content, so a
+   * valid prefix followed by garbage, or an empty document, is treated as 
malformed (consistently
+   * in both ON ERROR modes).
+   */
+  private def isSingleWellFormedValue(json: UTF8String): Boolean = {
+    try {
+      Utils.tryWithResource(CreateJacksonParser.utf8String(jsonFactory, json)) 
{ parser =>
+        if (parser.nextToken() == null) {
+          false // empty or whitespace-only
+        } else {
+          parser.skipChildren() // consume the first value in full
+          parser.nextToken() == null // nothing must remain after it
+        }
+      }
+    } catch {
+      case _: JsonProcessingException => false
+    }
+  }
+
+  /**
+   * Resolves every column of `trie` against the value at the parser's current 
token in a single
+   * traversal, writing each matched terminal's [[JsonPathResult]] into `out` 
at its slot index.
+   * Only the simple wildcard-free instruction set produced for `JSON_TABLE` 
paths is modeled by the
+   * trie (`Key`/`Named` object steps and `Subscript`/`Index` array steps).
+   *
+   * Slots left untouched keep their initial `Missing`. A matched value is 
stored as its raw JSON
+   * text (`Found.raw`), i.e. strings keep their enclosing quotes so the 
fragment stays
+   * re-parseable; value columns unquote scalar strings afterwards via 
[[JsonTable]]'s extraction.
+   */
+  private def navigateAll(
+      parser: JsonParser,
+      trie: JsonTablePathTrie,
+      out: Array[JsonPathResult]): Unit = {
+    val isNull = parser.currentToken == JsonToken.VALUE_NULL
+
+    if (!trie.hasChildren) {
+      // Leaf node: every column terminates here, so just record the current 
value (or null) and
+      // consume it. This is the common case for disjoint column paths.
+      if (trie.terminals.nonEmpty) {
+        val result = if (isNull) JsonPathResult.NullValue
+          else JsonPathResult.Found(serializeCurrentValue(parser))
+        trie.terminals.foreach(out(_) = result)
+      } else {
+        skipRest(parser)
+      }
+    } else if (trie.terminals.nonEmpty && !isNull) {
+      // A column path both ends here and extends deeper (e.g. `$.a` alongside 
`$.a.b`). Serialize
+      // the value once for the terminals, then re-parse that fragment to 
resolve the deeper
+      // columns -- so the only place a value is parsed more than once is this 
rare prefix overlap,
+      // and even then the descendant columns are still resolved in a single 
sub-traversal.
+      val raw = serializeCurrentValue(parser)
+      val result = JsonPathResult.Found(raw)
+      trie.terminals.foreach(out(_) = result)
+      Utils.tryWithResource(CreateJacksonParser.utf8String(jsonFactory, raw)) 
{ sub =>
+        sub.nextToken()
+        descendInto(sub, trie, out)
+      }
+    } else {
+      // Columns only extend deeper (terminals here, if any over a JSON null, 
stay Missing since a
+      // null has no children to descend into).

Review Comment:
   Confirmed; the comment now describes both terminal and descendant behavior 
accurately.



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/json/JsonExpressionEvalUtils.scala:
##########
@@ -350,6 +357,462 @@ case class JsonTupleEvaluator(foldableFieldNames: 
Array[Option[String]]) {
   }
 }
 
+/**
+ * The three-state result of navigating a JSON path for `JSON_TABLE`. 
`get_json_object` collapses
+ * "the path is absent" and "the value is JSON null" into a single `null`, 
which is wrong for
+ * `JSON_TABLE`: `EXISTS` must treat a present-but-null value as existing, and 
a value column must
+ * distinguish SQL `NULL` from the literal string `"null"`. This ADT keeps the 
two cases distinct.
+ */
+sealed trait JsonPathResult
+object JsonPathResult {
+  /** The path did not match (the key/index is absent). */
+  case object Missing extends JsonPathResult
+  /** The path matched a JSON `null` literal. */
+  case object NullValue extends JsonPathResult
+  /** The path matched a value; `raw` is its verbatim JSON text (including 
quoted strings). */
+  case class Found(raw: UTF8String) extends JsonPathResult
+}
+
+/**
+ * A prefix trie over the (wildcard-free) column paths of a single 
`JSON_TABLE` invocation, built
+ * once via [[JsonTableEvaluator.buildPathTrie]] and reused for every row. It 
lets
+ * [[JsonTableEvaluator.navigateAll]] resolve all columns in a single 
traversal of a row item
+ * instead of re-parsing the item once per column.
+ *
+ * Each node groups the paths that share a common prefix: `named`/`indexed` 
hold the object-key and
+ * array-index steps to child nodes, and `terminals` lists the result-slot 
indices of the columns
+ * whose path ends exactly at this node.
+ */
+private[expressions] final class JsonTablePathTrie {
+  // Result-slot indices of columns whose path terminates at this node.
+  var terminals: List[Int] = Nil
+  // Object-key children, keyed by field name.
+  val named: mutable.HashMap[String, JsonTablePathTrie] = mutable.HashMap.empty
+  // Array-index children, keyed by index.
+  val indexed: mutable.HashMap[Long, JsonTablePathTrie] = mutable.HashMap.empty
+
+  def hasChildren: Boolean = named.nonEmpty || indexed.nonEmpty
+
+  /** True if no column path was inserted (e.g. an ordinality-only table): 
nothing to resolve. */
+  def isEmpty: Boolean = terminals.isEmpty && !hasChildren
+}
+
+/**
+ * The result of positioning a parser at a JSON path for the `JSON_TABLE` row 
source (see
+ * `positionAt`). Like [[JsonPathResult]] it distinguishes a missing path from 
a JSON `null`, but
+ * `AtValue` leaves the parser on the matched value's first token (rather than 
serializing it) so
+ * the row source can be streamed.
+ */
+sealed trait PositionResult
+object PositionResult {
+  /** The path did not match. */
+  case object Missing extends PositionResult
+  /** The path matched a JSON `null` literal. */
+  case object NullValue extends PositionResult
+  /** The path matched a value; the parser is positioned at its first token. */
+  case object AtValue extends PositionResult
+}
+
+/**
+ * Row-source and column extraction for the SQL `JSON_TABLE` function. Given 
the JSON input, the
+ * (wildcard-free) container path, and whether the row path ended in `[*]`, it 
produces the
+ * per-row JSON documents that the 
[[org.apache.spark.sql.catalyst.expressions.JsonTable]]
+ * generator then projects into columns via [[navigate]].

Review Comment:
   Confirmed; the Scaladoc link now resolves to navigateColumns.



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