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

cloud-fan 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 4cf0be98c6f3 [SPARK-59417][SQL] Stream multiline top-level JSON arrays
4cf0be98c6f3 is described below

commit 4cf0be98c6f3d5a7cce4e433172ccf196e5393ba
Author: Timothy Meehan <[email protected]>
AuthorDate: Thu Oct 8 17:26:01 2026 +0000

    [SPARK-59417][SQL] Stream multiline top-level JSON arrays
    
    ### What changes were proposed in this pull request?
    
    This is a backport of #58704 to `branch-4.x`. It is a clean cherry-pick of 
the merged commit `f4327af76197334dbf893a6f7ae34e2d4a096c5f`; the only 
conflicts were the regenerated `JsonBenchmark*-results.txt` files, resolved to 
the committed benchmark output (identical benchmark structure plus the two new 
top-level-array sections).
    
    This PR adds the Spark SQL configuration 
`spark.sql.json.enableStreamingTopLevelArray` and the per-read JSON option 
`enableStreamingTopLevelArray`, which overrides it. When enabled, reading a 
multiline JSON file consumes a top-level array of structs lazily from one 
Jackson parser instead of materializing the entire array before returning rows. 
Both default to false. The two paths agree on valid input but differ on 
malformed input, described below, so this is a choice a read makes rath [...]
    
    It also adds an opt-in lazy path to `FailureSafeParser` for failures raised 
while advancing the parser iterator, preserves rows emitted before later 
structural corruption, and adds benchmark cases for both many small elements 
and fewer large elements. Existing eager `FailureSafeParser` callers remain 
unchanged.
    
    A malformed element is resumed from only where the parser cannot have moved 
past it: a scalar element leaves the parser on that scalar's own token, and 
`convertObject` consumes through the closing brace before reporting a partial 
result. Any other container failure can stop anywhere inside its element, so it 
ends the document as before.
    
    Because the streaming iterator holds a `JsonParser` for the life of the 
read, it registers `close()` with the active `TaskContext` completion listener 
in addition to closing eagerly when the array ends or a terminal failure 
occurs. An iterator abandoned before the closing bracket (under a `LIMIT`, for 
example) therefore releases the parser when the task finishes rather than at 
GC. The listener has no removal API, so the parser is held in a cell that every 
close path clears before clos [...]
    
    ### Why are the changes needed?
    
    The existing parser materializes every element of a multiline top-level 
JSON array in memory. Large arrays can therefore require memory proportional to 
the complete document even though Spark consumes the result as an iterator. 
Streaming elements bounds parser-side memory, producing the same rows as the 
eager path for valid input. For malformed input it changes which unit a parse 
mode applies to, which is why the path is opt-in rather than the default.
    
    ### Does this PR introduce _any_ user-facing change?
    
    Yes, when the configuration or the JSON option is enabled. Reading a 
multiline top-level JSON array no longer materializes the complete array before 
producing rows, and the record a parse mode applies to becomes one array 
element rather than the whole document. This follows from streaming: rows 
already handed to the consumer cannot be withdrawn or retroactively stamped, so 
preserving the document as the record would mean buffering to the closing 
bracket.
    
    Reading `[{"a":1},<malformed>,{"a":2}]` under `PERMISSIVE` with schema `a 
int, _corrupt_record string`, writing `D` for the whole document:
    
    | malformed element | disabled | enabled |
    | --- | --- | --- |
    | `{"a":"bad"}` | `(1, D)`, `(null, D)`, `(2, D)` | `(1, null)`, `(null, 
D)`, `(2, null)` |
    | `42` | `(null, D)` | `(1, null)`, `(null, D)`, `(2, null)` |
    | `[1,2]` | `(null, D)` | `(1, null)`, `(null, D)` |
    
    `DROPMALFORMED` drops the corrupt row of each cell, which leaves no rows at 
all when disabled since every row carries the document's corruption. `FAILFAST` 
raises `MALFORMED_RECORD_IN_PARSING.WITHOUT_SUGGESTION` in both columns.
    
    The last row is the element whose failure leaves the parser at an unknown 
position, so enabling the configuration keeps the rows before it and still ends 
the document there. The configuration doc says so.
    
    One consequence reaches valid documents of a shape the eager path never 
supported: a top-level array whose elements are all scalars, such as `[1,2,3]`, 
is malformed against a struct schema in every element, so it reports one 
corrupt record per element where the whole document reported one. Object 
elements already behaved that way, through the partial-result path the eager 
reader has always had.
    
    Valid input is unaffected, and both the configuration and the option 
default to false.
    
    ### How was this patch tested?
    
    The regression coverage from #58704 is carried over unchanged: lazy 
top-level array parsing; the JSON option overriding the session configuration 
in both directions; malformed input after partial output under PERMISSIVE, 
DROPMALFORMED, and FAILFAST with the configuration both disabled and enabled, 
so every cell of the table above is pinned in both columns; the three element 
shapes parameterized over one document (pinning the nested array as the shape 
that must stay terminal); extended [...]
    
    Backport verification: the code, test, and doc changes are byte-identical 
to the merged commit (confirmed by a content-level diff against `f4327af`). 
Compiled `sql/Test/compile` (catalyst + sql/core, main and test) on 
`branch-4.x`. The suites above run here in CI.
    
    ### Was this patch authored or co-authored using generative AI tooling?
    
    Generated-by: Isaac (Claude Opus 4.8)
    
    The code, tests, and docs are an unmodified cherry-pick of #58704 
(generative-AI tooling for the original change is disclosed there). This 
backport — the cherry-pick, the benchmark-file conflict resolution, and this 
description — was prepared with Isaac.
    
    This pull request and its description were written by Isaac.
    
    Closes #59288 from tdcmeehan/tim/sc-173438-stream-json-root-array-4.x.
    
    Authored-by: Timothy Meehan <[email protected]>
    Signed-off-by: Wenchen Fan <[email protected]>
---
 docs/sql-data-sources-json.md                      |   6 +
 .../spark/sql/catalyst/json/JSONOptions.scala      |   5 +
 .../spark/sql/catalyst/json/JacksonParser.scala    | 197 ++++--
 .../sql/catalyst/util/BadRecordException.scala     |   4 +-
 .../sql/catalyst/util/FailureSafeParser.scala      |  77 ++-
 .../org/apache/spark/sql/internal/SQLConf.scala    |  20 +
 .../benchmarks/JsonBenchmark-jdk21-results.txt     | 172 ++---
 .../benchmarks/JsonBenchmark-jdk25-results.txt     | 172 ++---
 sql/core/benchmarks/JsonBenchmark-results.txt      | 172 ++---
 .../datasources/json/JsonDataSource.scala          |  26 +-
 .../datasources/JSONArchiveReadBase.scala          |  70 +-
 .../execution/datasources/json/JsonBenchmark.scala |  51 ++
 .../sql/execution/datasources/json/JsonSuite.scala | 729 ++++++++++++++++++---
 13 files changed, 1293 insertions(+), 408 deletions(-)

diff --git a/docs/sql-data-sources-json.md b/docs/sql-data-sources-json.md
index 6ac5d690868c..aadd3c25a780 100644
--- a/docs/sql-data-sources-json.md
+++ b/docs/sql-data-sources-json.md
@@ -237,6 +237,12 @@ Data source options of JSON can be set via:
     <td>Parse one record, which may span multiple lines, per file. JSON 
built-in functions ignore this option.</td>
     <td>read</td>
   </tr>
+  <tr>
+    <td><code>enableStreamingTopLevelArray</code></td>
+    <td>(value of <code>spark.sql.json.enableStreamingTopLevelArray</code> 
configuration)</td>
+    <td>When <code>multiLine</code> is enabled and a file holds a top-level 
JSON array, read the array's elements one at a time instead of materializing 
the whole array before returning rows. It applies to reads into a struct 
schema, and has no effect on reads using <code>singleVariantColumn</code> or 
<code>explodeEmbeddedArray</code>. While streaming, <code>mode</code> applies 
to an individual element rather than the whole document: 
<code>PERMISSIVE</code> fills the field configured by  [...]
+    <td>read</td>
+  </tr>
   <tr>
     <td><code>allowUnquotedControlChars</code></td>
     <td><code>false</code></td>
diff --git 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/json/JSONOptions.scala
 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/json/JSONOptions.scala
index fba273bac265..98858076236e 100644
--- 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/json/JSONOptions.scala
+++ 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/json/JSONOptions.scala
@@ -244,6 +244,10 @@ class JSONOptions(
   val useUnsafeRow: Boolean = 
parameters.get(USE_UNSAFE_ROW).map(_.toBoolean).getOrElse(
     SQLConf.get.getConf(SQLConf.JSON_USE_UNSAFE_ROW))
 
+  val streamMultilineTopLevelArray: Boolean =
+    
parameters.get(ENABLE_STREAMING_TOP_LEVEL_ARRAY).map(_.toBoolean).getOrElse(
+      SQLConf.get.getConf(SQLConf.JSON_STREAM_MULTILINE_TOP_LEVEL_ARRAY))
+
   /** Build a Jackson [[JsonFactory]] using JSON options. */
   def buildJsonFactory(): JsonFactory = {
     val streamReadConstraints = StreamReadConstraints
@@ -343,6 +347,7 @@ object JSONOptions extends DataSourceOptions {
   val SINGLE_VARIANT_COLUMN = 
newOption(DataSourceOptions.SINGLE_VARIANT_COLUMN)
   val EXPLODE_EMBEDDED_ARRAY = 
newOption(DataSourceOptions.EXPLODE_EMBEDDED_ARRAY)
   val USE_UNSAFE_ROW = newOption("useUnsafeRow")
+  val ENABLE_STREAMING_TOP_LEVEL_ARRAY = 
newOption("enableStreamingTopLevelArray")
   // Options with alternative
   val ENCODING = "encoding"
   val CHARSET = "charset"
diff --git 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/json/JacksonParser.scala
 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/json/JacksonParser.scala
index e1a277764d0f..e87b1d0ca2a8 100644
--- 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/json/JacksonParser.scala
+++ 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/json/JacksonParser.scala
@@ -19,6 +19,7 @@ package org.apache.spark.sql.catalyst.json
 
 import java.io.{ByteArrayOutputStream, CharConversionException}
 import java.nio.charset.MalformedInputException
+import java.util.concurrent.atomic.AtomicReference
 
 import scala.collection.mutable
 import scala.collection.mutable.ArrayBuffer
@@ -28,6 +29,7 @@ import com.fasterxml.jackson.core._
 import org.apache.hadoop.fs.PositionedReadable
 
 import org.apache.spark.SparkUpgradeException
+import org.apache.spark.TaskContext
 import org.apache.spark.internal.Logging
 import org.apache.spark.sql.catalyst.{InternalRow, NoopFilters, StructFilters}
 import org.apache.spark.sql.catalyst.expressions._
@@ -61,6 +63,9 @@ class JacksonParser(
   // `ValueConverter`s for the root schema for all fields in the schema
   private val rootConverter = makeRootConverter(schema)
 
+  // Lazy: a schema that is not a struct never reaches either top-level-array 
path.
+  private lazy val arrayElementConverter = makeConverter(schema)
+
   private val factory = options.buildJsonFactory()
 
   private lazy val timestampFormatter = TimestampFormatter(
@@ -148,7 +153,6 @@ class JacksonParser(
   }
 
   private def makeStructRootConverter(st: StructType): JsonParser => 
Iterable[InternalRow] = {
-    val elementConverter = makeConverter(st)
     val fieldConverters = st.map(_.dataType).map(makeConverter).toArray
     val jsonFilters = if (SQLConf.get.jsonFilterPushDown) {
       new JsonFilters(filters, st)
@@ -171,7 +175,8 @@ class JacksonParser(
         // List([str_a_2,null], [null,str_b_3])
         //
       case START_ARRAY if allowArrayAsStructs =>
-        val array = convertArray(parser, elementConverter, isRoot = true, 
arrayAsStructs = true)
+        val array =
+          convertArray(parser, arrayElementConverter, isRoot = true, 
arrayAsStructs = true)
         // Here, as we support reading top level JSON arrays and take every 
element
         // in such an array as a row, this case is possible.
         if (array.numElements() == 0) {
@@ -700,6 +705,34 @@ class JacksonParser(
     case _ => err
   }
 
+  private def badRecord(error: Throwable, recordLiteral: () => UTF8String): 
BadRecordException =
+    error match {
+      case e: SparkUpgradeException => throw e
+      case e: CharConversionException if options.encoding.isEmpty =>
+        val msg =
+          """JSON parser cannot handle a character in its input.
+            |Specifying encoding as an input option explicitly might help to 
resolve the issue.
+            |""".stripMargin + e.getMessage
+        val wrappedCharException = new CharConversionException(msg)
+        wrappedCharException.initCause(e)
+        BadRecordException(recordLiteral, () => Array.empty, 
wrappedCharException)
+      case PartialResultException(row, cause) =>
+        BadRecordException(recordLiteral, () => Array(row), 
convertCauseForPartialResult(cause))
+      case PartialResultArrayException(rows, cause) =>
+        BadRecordException(recordLiteral, () => rows, cause)
+      case PartialArrayDataResultException(arrayData, cause) =>
+        BadRecordException(
+          recordLiteral,
+          () => Array(InternalRow(arrayData)),
+          convertCauseForPartialResult(cause))
+      case PartialMapDataResultException(mapData, cause) =>
+        BadRecordException(
+          recordLiteral,
+          () => Array(InternalRow(mapData)),
+          convertCauseForPartialResult(cause))
+      case e => BadRecordException(recordLiteral, () => Array.empty, e)
+    }
+
   /**
    * Parse the JSON input to the set of [[InternalRow]]s.
    *
@@ -724,43 +757,131 @@ class JacksonParser(
       }
     } catch {
       case e: SparkUpgradeException => throw e
-      case e @ (_: RuntimeException | _: JsonProcessingException | _: 
MalformedInputException) =>
-        // JSON parser currently doesn't support partial results for corrupted 
records.
-        // For such records, all fields other than the field configured by
-        // `columnNameOfCorruptRecord` are set to `null`.
-        throw BadRecordException(() => recordLiteral(record), () => 
Array.empty, e)
       case e: CharConversionException if options.encoding.isEmpty =>
-        val msg =
-          """JSON parser cannot handle a character in its input.
-            |Specifying encoding as an input option explicitly might help to 
resolve the issue.
-            |""".stripMargin + e.getMessage
-        val wrappedCharException = new CharConversionException(msg)
-        wrappedCharException.initCause(e)
-        throw BadRecordException(() => recordLiteral(record), () => 
Array.empty,
-          wrappedCharException)
-      case PartialResultException(row, cause) =>
-        throw BadRecordException(
-          record = () => recordLiteral(record),
-          partialResults = () => Array(row),
-          convertCauseForPartialResult(cause))
-      case PartialResultArrayException(rows, cause) =>
-        throw BadRecordException(
-          record = () => recordLiteral(record),
-          partialResults = () => rows,
-          cause)
-      // These exceptions should never be thrown outside of JacksonParser.
-      // They are used for the control flow in the parser. We add them here 
for completeness
-      // since they also indicate a bad record.
-      case PartialArrayDataResultException(arrayData, cause) =>
-        throw BadRecordException(
-          record = () => recordLiteral(record),
-          partialResults = () => Array(InternalRow(arrayData)),
-          convertCauseForPartialResult(cause))
-      case PartialMapDataResultException(mapData, cause) =>
-        throw BadRecordException(
-          record = () => recordLiteral(record),
-          partialResults = () => Array(InternalRow(mapData)),
-          convertCauseForPartialResult(cause))
+        throw badRecord(e, () => recordLiteral(record))
+      case e @ (_: RuntimeException | _: JsonProcessingException | _: 
MalformedInputException |
+          _: PartialResultException | _: PartialResultArrayException |
+          _: PartialArrayDataResultException | _: 
PartialMapDataResultException) =>
+        throw badRecord(e, () => recordLiteral(record))
+    }
+  }
+
+  private[sql] def parseIterator[T](
+      record: T,
+      createParser: (JsonFactory, T) => JsonParser,
+      recordLiteral: T => UTF8String): Iterator[InternalRow] = {
+    val streamArray = allowArrayAsStructs && schema.isInstanceOf[StructType] &&
+      options.singleVariantColumn.isEmpty && 
options.explodeEmbeddedArray.isEmpty
+    val jsonParser = try {
+      createParser(factory, record)
+    } catch {
+      case e: SparkUpgradeException => throw e
+      case e: CharConversionException if options.encoding.isEmpty =>
+        throw badRecord(e, () => recordLiteral(record))
+      case e @ (_: RuntimeException | _: JsonProcessingException | _: 
MalformedInputException |
+          _: PartialResultException | _: PartialResultArrayException |
+          _: PartialArrayDataResultException | _: 
PartialMapDataResultException) =>
+        throw badRecord(e, () => recordLiteral(record))
+    }
+    // An archive read builds one parser per entry inside a single task, so a 
closed parser left
+    // reachable from the completion listener keeps its entry's buffer alive 
for the rest of the
+    // task. Every close path takes the parser out of this cell first, so 
exactly one of them closes
+    // it and the listener stops retaining it as soon as one does.
+    val openParser = new AtomicReference(jsonParser)
+    def takeParser(): Option[JsonParser] = Option(openParser.getAndSet(null))
+    // Cleanup must not fail a read that already emitted rows, nor a task 
whose iterator was
+    // abandoned, so the paths that can still succeed close the way 
`tryWithResource` does: quietly.
+    def closeParser(): Unit = takeParser().foreach(parser => 
Utils.closeQuietly(parser))
+    def fail(error: Throwable): Nothing = {
+      try takeParser().foreach(_.close()) catch {
+        case NonFatal(closeError) => error.addSuppressed(closeError)
+      }
+      throw badRecord(error, () => recordLiteral(record))
+    }
+    // Resuming is only safe where the parser cannot have moved past the 
element that failed. A
+    // scalar element leaves it on that scalar's own token, and 
`convertObject` consumes through
+    // END_OBJECT before raising `PartialResultException`; any other container 
failure can have
+    // stopped anywhere inside the element.
+    def resumableAfter(error: Throwable, elementStart: Option[JsonToken]): 
Boolean =
+      options.parseMode != FailFastMode && (elementStart match {
+        case Some(START_OBJECT) | Some(START_ARRAY) => 
error.isInstanceOf[PartialResultException]
+        case Some(_) => error.isInstanceOf[RuntimeException]
+        case None => false
+      })
+    def handleFailure[R](elementStart: Option[JsonToken])(operation: => R): R 
= {
+      try operation catch {
+        case e: SparkUpgradeException => fail(e)
+        case e: CharConversionException if options.encoding.isEmpty => fail(e)
+        case e @ (_: RuntimeException | _: JsonProcessingException | _: 
MalformedInputException |
+            _: PartialResultException | _: PartialResultArrayException |
+            _: PartialArrayDataResultException | _: 
PartialMapDataResultException) =>
+          if (resumableAfter(e, elementStart)) {
+            throw badRecord(e, () => recordLiteral(record)).copy(recoverable = 
true)
+          } else {
+            fail(e)
+          }
+        // Anything else, such as a raw `IOException` off the underlying 
stream, is not a malformed
+        // record and so propagates unwrapped -- but it still has to close 
what the eager path's
+        // `tryWithResource` would have, rather than leaving it to task 
completion.
+        case other: Throwable =>
+          closeParser()
+          throw other
+      }
+    }
+
+    handleFailure(elementStart = None)(jsonParser.nextToken()) match {
+      case null =>
+        closeParser()
+        Iterator.empty
+      case START_ARRAY if streamArray =>
+        // Abandoning the iterator before END_ARRAY (e.g. a LIMIT) reaches no 
eager close.
+        Option(TaskContext.get()).foreach(_.addTaskCompletionListener[Unit](_ 
=> closeParser()))
+        new Iterator[InternalRow] {
+          private var nextRow: InternalRow = _
+          private var prepared = false
+          private var finished = false
+
+          override def hasNext: Boolean = {
+            prepare()
+            !finished
+          }
+
+          override def next(): InternalRow = {
+            prepare()
+            if (finished) throw new NoSuchElementException("next on empty 
iterator")
+            prepared = false
+            nextRow
+          }
+
+          private def prepare(): Unit = {
+            if (prepared || finished) return
+            handleFailure(elementStart = None)(jsonParser.nextToken()) match {
+              case END_ARRAY => finish()
+              // Unreachable today: jackson-core signals end of input inside 
an open array by
+              // throwing JsonEOFException, which handleFailure turns into a 
malformed record.
+              // Kept so a backend that returns null cannot fall through with 
no current token.
+              case null =>
+                fail(new JsonParseException(jsonParser, "Unexpected end of 
top-level array"))
+              case elementStart =>
+                nextRow = handleFailure(Some(elementStart)) {
+                  val row = 
arrayElementConverter(jsonParser).asInstanceOf[InternalRow]
+                  if (row == null) throw 
QueryExecutionErrors.rootConverterReturnNullError()
+                  row
+                }
+                prepared = true
+            }
+          }
+
+          private def finish(): Unit = {
+            finished = true
+            closeParser()
+          }
+        }
+      case _ =>
+        val rows = handleFailure(elementStart = 
None)(rootConverter(jsonParser))
+        if (rows == null) 
fail(QueryExecutionErrors.rootConverterReturnNullError())
+        closeParser()
+        rows.iterator
     }
   }
 }
diff --git 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/util/BadRecordException.scala
 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/util/BadRecordException.scala
index 4fa6a2275e74..54b85a6bde1b 100644
--- 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/util/BadRecordException.scala
+++ 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/util/BadRecordException.scala
@@ -76,11 +76,13 @@ case class PartialResultArrayException(
  * @param cause the actual exception about why the record is bad and can't be 
parsed. It's better
  *                      to use `LazyBadRecordCauseWrapper` here to delay heavy 
cause construction
  *                      until it's needed.
+ * @param recoverable whether the parser can resume at the next record after 
handling this failure
  */
 case class BadRecordException(
     @transient record: () => UTF8String,
     @transient partialResults: () => Array[InternalRow] = () => 
Array.empty[InternalRow],
-    cause: Throwable) extends Exception(cause) {
+    cause: Throwable,
+    recoverable: Boolean = false) extends Exception(cause) {
   override def getStackTrace(): Array[StackTraceElement] = new 
Array[StackTraceElement](0)
   override def fillInStackTrace(): Throwable = this
 }
diff --git 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/util/FailureSafeParser.scala
 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/util/FailureSafeParser.scala
index d9946d1b12ec..3c31e323491c 100644
--- 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/util/FailureSafeParser.scala
+++ 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/util/FailureSafeParser.scala
@@ -59,30 +59,63 @@ class FailureSafeParser[IN](
     try {
       rawParser.apply(input).iterator.map(row => toResultRow(Some(row), () => 
null))
     } catch {
-      case e: BadRecordException => mode match {
-        case PermissiveMode =>
-          val partialResults = e.partialResults()
-          if (partialResults.nonEmpty) {
-            partialResults.iterator.map(row => toResultRow(Some(row), 
e.record))
-          } else {
-            Iterator(toResultRow(None, e.record))
-          }
-        case DropMalformedMode =>
-          Iterator.empty
-        case FailFastMode =>
-          e.getCause match {
-            case _: JsonArraysAsStructsException =>
-              // SPARK-42298 we recreate the exception here to make sure the 
error message
-              // have the record content.
-              throw 
QueryExecutionErrors.cannotParseJsonArraysAsStructsError(e.record().toString)
-            case StringAsDataTypeException(fieldName, fieldValue, dataType) =>
-              throw 
QueryExecutionErrors.cannotParseStringAsDataTypeError(e.record().toString,
-                fieldName, fieldValue, dataType)
-            case causeWrapper: LazyBadRecordCauseWrapper =>
-              throwMalformedRecordsDetectedInRecordParsingError(e, 
causeWrapper.cause())
-            case cause => throwMalformedRecordsDetectedInRecordParsingError(e, 
cause)
+      case e: BadRecordException => parseFailure(e)
+    }
+  }
+
+  def parseIterator(
+      input: IN,
+      iteratorParser: IN => Iterator[InternalRow]): Iterator[InternalRow] = {
+    var delegate = try {
+      iteratorParser.apply(input).map(row => toResultRow(Some(row), () => 
null))
+    } catch {
+      case e: BadRecordException => parseFailure(e)
+    }
+    new Iterator[InternalRow] {
+      private def handleFailure[T](operation: Iterator[InternalRow] => T): T = 
{
+        while (true) {
+          try {
+            return operation(delegate)
+          } catch {
+            case e: BadRecordException =>
+              val source = delegate
+              val recovery = parseFailure(e)
+              delegate = if (e.recoverable) recovery ++ source else recovery
           }
+        }
+        throw new IllegalStateException("unreachable")
       }
+
+      override def hasNext: Boolean = handleFailure(_.hasNext)
+
+      override def next(): InternalRow = handleFailure(_.next())
+    }
+  }
+
+  private def parseFailure(e: BadRecordException): Iterator[InternalRow] = {
+    mode match {
+      case PermissiveMode =>
+        val partialResults = e.partialResults()
+        if (partialResults.nonEmpty) {
+          partialResults.iterator.map(row => toResultRow(Some(row), e.record))
+        } else {
+          Iterator(toResultRow(None, e.record))
+        }
+      case DropMalformedMode =>
+        Iterator.empty
+      case FailFastMode =>
+        e.getCause match {
+          case _: JsonArraysAsStructsException =>
+            // SPARK-42298 we recreate the exception here to make sure the 
error message
+            // has the record content.
+            throw 
QueryExecutionErrors.cannotParseJsonArraysAsStructsError(e.record().toString)
+          case StringAsDataTypeException(fieldName, fieldValue, dataType) =>
+            throw 
QueryExecutionErrors.cannotParseStringAsDataTypeError(e.record().toString,
+              fieldName, fieldValue, dataType)
+          case causeWrapper: LazyBadRecordCauseWrapper =>
+            throwMalformedRecordsDetectedInRecordParsingError(e, 
causeWrapper.cause())
+          case cause => throwMalformedRecordsDetectedInRecordParsingError(e, 
cause)
+        }
     }
   }
 
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 dcae5780c4cc..de2f850f9d14 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
@@ -7186,6 +7186,26 @@ object SQLConf {
       .booleanConf
       .createWithDefault(true)
 
+  val JSON_STREAM_MULTILINE_TOP_LEVEL_ARRAY =
+    buildConf("spark.sql.json.enableStreamingTopLevelArray")
+      .doc("When true, multiline JSON file reads stream the elements of a 
top-level array one at " +
+        "a time instead of materializing the entire array before returning 
rows. This applies " +
+        "only to reads into a struct schema that take top-level arrays as 
structs, and has no " +
+        "effect on reads using the `singleVariantColumn` or 
`explodeEmbeddedArray` option. " +
+        "Streaming also makes an array element, rather than the whole 
document, the record " +
+        "that a parse mode applies to, since rows already emitted cannot be 
withdrawn: " +
+        "PERMISSIVE fills the corrupt record column for the malformed element 
only, leaving " +
+        "it null on the valid rows of the same document, and DROPMALFORMED 
drops that " +
+        "element rather than the whole document. An element whose failure 
leaves the parser " +
+        "at an unknown position, such as a nested value of the wrong shape, 
still ends the " +
+        "document, as does a failure outside any element, such as a syntax 
error between two " +
+        "elements or a missing closing bracket. It can be overwritten by the 
JSON option " +
+        "`enableStreamingTopLevelArray`.")
+      .version("4.4.0")
+      .withBindingPolicy(ConfigBindingPolicy.SESSION)
+      .booleanConf
+      .createWithDefault(false)
+
   val JSON_USE_UNSAFE_ROW =
     buildConf("spark.sql.json.useUnsafeRow")
       .doc("When set to true, use UnsafeRow to represent struct result in the 
JSON parser. It " +
diff --git a/sql/core/benchmarks/JsonBenchmark-jdk21-results.txt 
b/sql/core/benchmarks/JsonBenchmark-jdk21-results.txt
index 2d49ed10a6ed..4f8b1d71af8f 100644
--- a/sql/core/benchmarks/JsonBenchmark-jdk21-results.txt
+++ b/sql/core/benchmarks/JsonBenchmark-jdk21-results.txt
@@ -3,128 +3,142 @@ Benchmark for performance of JSON parsing
 
================================================================================================
 
 Preparing data for benchmarking ...
-OpenJDK 64-Bit Server VM 21.0.10+7-LTS on Linux 6.14.0-1017-azure
-AMD EPYC 7763 64-Core Processor
+OpenJDK 64-Bit Server VM 21.0.12+8-LTS on Linux 6.17.0-1022-azure
+AMD EPYC 9V74 80-Core Processor
 JSON schema inferring:                    Best Time(ms)   Avg Time(ms)   
Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
 
------------------------------------------------------------------------------------------------------------------------
-No encoding                                        2505           2612         
 96          2.0         501.0       1.0X
-UTF-8 is set                                       5362           5380         
 16          0.9        1072.3       0.5X
+No encoding                                        2081           2116         
 47          2.4         416.2       1.0X
+UTF-8 is set                                       5253           5423         
265          1.0        1050.6       0.4X
 
 Preparing data for benchmarking ...
-OpenJDK 64-Bit Server VM 21.0.10+7-LTS on Linux 6.14.0-1017-azure
-AMD EPYC 7763 64-Core Processor
+OpenJDK 64-Bit Server VM 21.0.12+8-LTS on Linux 6.17.0-1022-azure
+AMD EPYC 9V74 80-Core Processor
 count a short column:                     Best Time(ms)   Avg Time(ms)   
Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
 
------------------------------------------------------------------------------------------------------------------------
-No encoding                                        1952           1958         
  5          2.6         390.4       1.0X
-UTF-8 is set                                       4416           4420         
  5          1.1         883.3       0.4X
+No encoding                                        1789           1809         
 31          2.8         357.9       1.0X
+UTF-8 is set                                       4835           4838         
  2          1.0         967.0       0.4X
 
 Preparing data for benchmarking ...
-OpenJDK 64-Bit Server VM 21.0.10+7-LTS on Linux 6.14.0-1017-azure
-AMD EPYC 7763 64-Core Processor
+OpenJDK 64-Bit Server VM 21.0.12+8-LTS on Linux 6.17.0-1022-azure
+AMD EPYC 9V74 80-Core Processor
 count a wide column:                      Best Time(ms)   Avg Time(ms)   
Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
 
------------------------------------------------------------------------------------------------------------------------
-No encoding                                        4546           4551         
  5          0.2        4545.6       1.0X
-UTF-8 is set                                       4369           4375         
 10          0.2        4369.2       1.0X
+No encoding                                        4791           4805         
 14          0.2        4790.5       1.0X
+UTF-8 is set                                       5038           5052         
 15          0.2        5038.0       1.0X
 
 Preparing data for benchmarking ...
-OpenJDK 64-Bit Server VM 21.0.10+7-LTS on Linux 6.14.0-1017-azure
-AMD EPYC 7763 64-Core Processor
+OpenJDK 64-Bit Server VM 21.0.12+8-LTS on Linux 6.17.0-1022-azure
+AMD EPYC 9V74 80-Core Processor
 select wide row:                          Best Time(ms)   Avg Time(ms)   
Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
 
------------------------------------------------------------------------------------------------------------------------
-No encoding                                       10036          10149         
153          0.0      200713.2       1.0X
-UTF-8 is set                                      10794          10832         
 34          0.0      215870.3       0.9X
+No encoding                                       10203          10286         
 96          0.0      204060.2       1.0X
+UTF-8 is set                                      10939          10969         
 40          0.0      218787.9       0.9X
 
 Preparing data for benchmarking ...
-OpenJDK 64-Bit Server VM 21.0.10+7-LTS on Linux 6.14.0-1017-azure
-AMD EPYC 7763 64-Core Processor
+OpenJDK 64-Bit Server VM 21.0.12+8-LTS on Linux 6.17.0-1022-azure
+AMD EPYC 9V74 80-Core Processor
 Select a subset of 10 columns:            Best Time(ms)   Avg Time(ms)   
Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
 
------------------------------------------------------------------------------------------------------------------------
-Select 10 columns                                  1664           1665         
  1          0.6        1663.8       1.0X
-Select 1 column                                    1123           1125         
  3          0.9        1123.1       1.5X
+Select 10 columns                                  1759           1767         
  7          0.6        1759.1       1.0X
+Select 1 column                                    1157           1165         
  8          0.9        1156.7       1.5X
 
 Preparing data for benchmarking ...
-OpenJDK 64-Bit Server VM 21.0.10+7-LTS on Linux 6.14.0-1017-azure
-AMD EPYC 7763 64-Core Processor
+OpenJDK 64-Bit Server VM 21.0.12+8-LTS on Linux 6.17.0-1022-azure
+AMD EPYC 9V74 80-Core Processor
 creation of JSON parser per line:         Best Time(ms)   Avg Time(ms)   
Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
 
------------------------------------------------------------------------------------------------------------------------
-Short column without encoding                       583            586         
  3          1.7         583.3       1.0X
-Short column with UTF-8                            1118           1124         
  7          0.9        1118.2       0.5X
-Wide column without encoding                       5316           5335         
 18          0.2        5316.3       0.1X
-Wide column with UTF-8                             8905           8913         
  7          0.1        8904.8       0.1X
+Short column without encoding                       583            591         
  8          1.7         583.0       1.0X
+Short column with UTF-8                            1288           1291         
  4          0.8        1288.4       0.5X
+Wide column without encoding                       5623           5632         
  8          0.2        5622.8       0.1X
+Wide column with UTF-8                             7468           7483         
 17          0.1        7467.5       0.1X
 
 Preparing data for benchmarking ...
-OpenJDK 64-Bit Server VM 21.0.10+7-LTS on Linux 6.14.0-1017-azure
-AMD EPYC 7763 64-Core Processor
+OpenJDK 64-Bit Server VM 21.0.12+8-LTS on Linux 6.17.0-1022-azure
+AMD EPYC 9V74 80-Core Processor
 JSON functions:                           Best Time(ms)   Avg Time(ms)   
Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
 
------------------------------------------------------------------------------------------------------------------------
-Text read                                            68             70         
  2         14.6          68.3       1.0X
-from_json                                          1085           1089         
  5          0.9        1084.6       0.1X
-json_tuple                                         1060           1061         
  1          0.9        1059.6       0.1X
-get_json_object wholestage off                     1070           1071         
  2          0.9        1070.1       0.1X
-get_json_object wholestage on                       990            994         
  4          1.0         989.6       0.1X
+Text read                                            66             71         
  5         15.2          66.0       1.0X
+from_json                                          1029           1034         
  5          1.0        1029.0       0.1X
+json_tuple                                          971            974         
  3          1.0         971.0       0.1X
+get_json_object wholestage off                      986            994         
 11          1.0         986.0       0.1X
+get_json_object wholestage on                       886            894         
  7          1.1         885.5       0.1X
 
 Preparing data for benchmarking ...
-OpenJDK 64-Bit Server VM 21.0.10+7-LTS on Linux 6.14.0-1017-azure
-AMD EPYC 7763 64-Core Processor
+OpenJDK 64-Bit Server VM 21.0.12+8-LTS on Linux 6.17.0-1022-azure
+AMD EPYC 9V74 80-Core Processor
 Dataset of json strings:                  Best Time(ms)   Avg Time(ms)   
Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
 
------------------------------------------------------------------------------------------------------------------------
-Text read                                           240            241         
  1         20.8          48.1       1.0X
-schema inferring                                   1831           1836         
  4          2.7         366.2       0.1X
-parsing                                            2510           2518         
 11          2.0         502.1       0.1X
+Text read                                           260            261         
  2         19.2          52.0       1.0X
+schema inferring                                   1594           1600         
  9          3.1         318.8       0.2X
+parsing                                            2212           2216         
  4          2.3         442.4       0.1X
 
 Preparing data for benchmarking ...
-OpenJDK 64-Bit Server VM 21.0.10+7-LTS on Linux 6.14.0-1017-azure
-AMD EPYC 7763 64-Core Processor
+OpenJDK 64-Bit Server VM 21.0.12+8-LTS on Linux 6.17.0-1022-azure
+AMD EPYC 9V74 80-Core Processor
 Json files in the per-line mode:          Best Time(ms)   Avg Time(ms)   
Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
 
------------------------------------------------------------------------------------------------------------------------
-Text read                                           618            623         
  5          8.1         123.5       1.0X
-Schema inferring                                   2382           2384         
  2          2.1         476.5       0.3X
-Parsing without charset                            2665           2670         
  5          1.9         533.0       0.2X
-Parsing with UTF-8                                 5166           5180         
 12          1.0        1033.2       0.1X
+Text read                                           605            612         
  7          8.3         121.1       1.0X
+Schema inferring                                   2108           2113         
  8          2.4         421.5       0.3X
+Parsing without charset                            2673           2683         
 13          1.9         534.6       0.2X
+Parsing with UTF-8                                 5951           5955         
  4          0.8        1190.3       0.1X
 
-OpenJDK 64-Bit Server VM 21.0.10+7-LTS on Linux 6.14.0-1017-azure
-AMD EPYC 7763 64-Core Processor
+OpenJDK 64-Bit Server VM 21.0.12+8-LTS on Linux 6.17.0-1022-azure
+AMD EPYC 9V74 80-Core Processor
 Write dates and timestamps:               Best Time(ms)   Avg Time(ms)   
Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
 
------------------------------------------------------------------------------------------------------------------------
-Create a dataset of timestamps                      105            108         
  3          9.5         105.5       1.0X
-to_json(timestamp)                                  555            557         
  2          1.8         555.1       0.2X
-write timestamps to files                           618            626         
 12          1.6         617.8       0.2X
-Create a dataset of dates                           112            117         
  4          8.9         112.2       0.9X
-to_json(date)                                       420            420         
  0          2.4         420.3       0.3X
-write dates to files                                401            403         
  2          2.5         401.4       0.3X
-
-OpenJDK 64-Bit Server VM 21.0.10+7-LTS on Linux 6.14.0-1017-azure
-AMD EPYC 7763 64-Core Processor
+Create a dataset of timestamps                      111            114         
  2          9.0         110.8       1.0X
+to_json(timestamp)                                  509            514         
  7          2.0         509.0       0.2X
+write timestamps to files                           534            538         
  4          1.9         533.7       0.2X
+Create a dataset of dates                           122            129         
  7          8.2         122.3       0.9X
+to_json(date)                                       366            367         
  1          2.7         366.4       0.3X
+write dates to files                                376            383         
  9          2.7         376.2       0.3X
+
+OpenJDK 64-Bit Server VM 21.0.12+8-LTS on Linux 6.17.0-1022-azure
+AMD EPYC 9V74 80-Core Processor
 Read dates and timestamps:                                             Best 
Time(ms)   Avg Time(ms)   Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
 
-----------------------------------------------------------------------------------------------------------------------------------------------------
-read timestamp text from files                                                 
  149            153           7          6.7         148.5       1.0X
-read timestamps from files                                                     
 1052           1059           7          1.0        1051.7       0.1X
-infer timestamps from files                                                    
 1976           1980           4          0.5        1976.4       0.1X
-read date text from files                                                      
  154            157           3          6.5         153.5       1.0X
-read date from files                                                           
  647            655           6          1.5         647.4       0.2X
-timestamp strings                                                              
  146            147           0          6.8         146.5       1.0X
-parse timestamps from Dataset[String]                                          
 1226           1228           2          0.8        1226.2       0.1X
-infer timestamps from Dataset[String]                                          
 2150           2159          12          0.5        2149.5       0.1X
-date strings                                                                   
  198            200           2          5.0         198.3       0.7X
-parse dates from Dataset[String]                                               
  920            923           3          1.1         920.4       0.2X
-from_json(timestamp)                                                           
 1749           1751           4          0.6        1748.6       0.1X
-from_json(date)                                                                
 1464           1468           4          0.7        1464.3       0.1X
-infer error timestamps from Dataset[String] with default format                
 1358           1362           3          0.7        1358.0       0.1X
-infer error timestamps from Dataset[String] with user-provided format          
 1357           1360           3          0.7        1356.8       0.1X
-infer error timestamps from Dataset[String] with legacy format                 
 1384           1389           5          0.7        1383.5       0.1X
-
-OpenJDK 64-Bit Server VM 21.0.10+7-LTS on Linux 6.14.0-1017-azure
-AMD EPYC 7763 64-Core Processor
+read timestamp text from files                                                 
  160            161           1          6.2         160.1       1.0X
+read timestamps from files                                                     
 1109           1117           8          0.9        1108.8       0.1X
+infer timestamps from files                                                    
 2029           2030           2          0.5        2028.8       0.1X
+read date text from files                                                      
  147            149           3          6.8         146.7       1.1X
+read date from files                                                           
  692            701          14          1.4         691.5       0.2X
+timestamp strings                                                              
  144            147           5          7.0         143.7       1.1X
+parse timestamps from Dataset[String]                                          
 1309           1312           3          0.8        1309.5       0.1X
+infer timestamps from Dataset[String]                                          
 2229           2231           3          0.4        2229.3       0.1X
+date strings                                                                   
  206            208           2          4.9         205.8       0.8X
+parse dates from Dataset[String]                                               
  957            960           4          1.0         956.5       0.2X
+from_json(timestamp)                                                           
 1744           1749           5          0.6        1744.0       0.1X
+from_json(date)                                                                
 1433           1437           4          0.7        1433.3       0.1X
+infer error timestamps from Dataset[String] with default format                
 1299           1302           4          0.8        1299.5       0.1X
+infer error timestamps from Dataset[String] with user-provided format          
 1304           1307           4          0.8        1303.9       0.1X
+infer error timestamps from Dataset[String] with legacy format                 
 1335           1337           3          0.7        1334.7       0.1X
+
+OpenJDK 64-Bit Server VM 21.0.12+8-LTS on Linux 6.17.0-1022-azure
+AMD EPYC 9V74 80-Core Processor
 Filters pushdown:                         Best Time(ms)   Avg Time(ms)   
Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
 
------------------------------------------------------------------------------------------------------------------------
-w/o filters                                        6285           6289         
  4          0.0       62852.4       1.0X
-pushdown disabled                                  6190           6193         
  5          0.0       61896.8       1.0X
-w/ filters                                          599            602         
  6          0.2        5989.5      10.5X
+w/o filters                                        5507           5528         
 35          0.0       55073.9       1.0X
+pushdown disabled                                  5300           5309         
  8          0.0       52999.5       1.0X
+w/ filters                                          744            747         
  3          0.1        7443.5       7.4X
 
-OpenJDK 64-Bit Server VM 21.0.10+7-LTS on Linux 6.14.0-1017-azure
-AMD EPYC 7763 64-Core Processor
+OpenJDK 64-Bit Server VM 21.0.12+8-LTS on Linux 6.17.0-1022-azure
+AMD EPYC 9V74 80-Core Processor
 Partial JSON results:                     Best Time(ms)   Avg Time(ms)   
Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
 
------------------------------------------------------------------------------------------------------------------------
-parse invalid JSON                                 2393           2422         
 43          0.0      239282.1       1.0X
+parse invalid JSON                                 2465           2468         
  3          0.0      246496.7       1.0X
+
+OpenJDK 64-Bit Server VM 21.0.12+8-LTS on Linux 6.17.0-1022-azure
+AMD EPYC 9V74 80-Core Processor
+Top-level JSON array with 0-byte payloads:  Best Time(ms)   Avg Time(ms)   
Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
+-------------------------------------------------------------------------------------------------------------------------
+streaming enabled: false                              75             82        
   8          1.3         752.2       1.0X
+streaming enabled: true                               74             78        
   3          1.3         741.4       1.0X
+
+OpenJDK 64-Bit Server VM 21.0.12+8-LTS on Linux 6.17.0-1022-azure
+AMD EPYC 9V74 80-Core Processor
+Top-level JSON array with 65536-byte payloads:  Best Time(ms)   Avg Time(ms)   
Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
+-----------------------------------------------------------------------------------------------------------------------------
+streaming enabled: false                                 136            139    
       3          0.0      135565.5       1.0X
+streaming enabled: true                                  133            134    
       1          0.0      133383.2       1.0X
 
 
diff --git a/sql/core/benchmarks/JsonBenchmark-jdk25-results.txt 
b/sql/core/benchmarks/JsonBenchmark-jdk25-results.txt
index a947a34a0ba8..e2b256f1b924 100644
--- a/sql/core/benchmarks/JsonBenchmark-jdk25-results.txt
+++ b/sql/core/benchmarks/JsonBenchmark-jdk25-results.txt
@@ -3,128 +3,142 @@ Benchmark for performance of JSON parsing
 
================================================================================================
 
 Preparing data for benchmarking ...
-OpenJDK 64-Bit Server VM 25.0.2+10-LTS on Linux 6.14.0-1017-azure
-AMD EPYC 7763 64-Core Processor
+OpenJDK 64-Bit Server VM 25.0.4+7-LTS on Linux 6.17.0-1022-azure
+AMD EPYC 9V74 80-Core Processor
 JSON schema inferring:                    Best Time(ms)   Avg Time(ms)   
Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
 
------------------------------------------------------------------------------------------------------------------------
-No encoding                                        2323           2386         
 96          2.2         464.5       1.0X
-UTF-8 is set                                       4970           4983         
 18          1.0         993.9       0.5X
+No encoding                                        2032           2116         
 98          2.5         406.4       1.0X
+UTF-8 is set                                       4421           4489         
 67          1.1         884.2       0.5X
 
 Preparing data for benchmarking ...
-OpenJDK 64-Bit Server VM 25.0.2+10-LTS on Linux 6.14.0-1017-azure
-AMD EPYC 7763 64-Core Processor
+OpenJDK 64-Bit Server VM 25.0.4+7-LTS on Linux 6.17.0-1022-azure
+AMD EPYC 9V74 80-Core Processor
 count a short column:                     Best Time(ms)   Avg Time(ms)   
Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
 
------------------------------------------------------------------------------------------------------------------------
-No encoding                                        2147           2199         
 67          2.3         429.4       1.0X
-UTF-8 is set                                       4826           4846         
 23          1.0         965.1       0.4X
+No encoding                                        1734           1744         
  9          2.9         346.8       1.0X
+UTF-8 is set                                       3939           3950         
 14          1.3         787.9       0.4X
 
 Preparing data for benchmarking ...
-OpenJDK 64-Bit Server VM 25.0.2+10-LTS on Linux 6.14.0-1017-azure
-AMD EPYC 7763 64-Core Processor
+OpenJDK 64-Bit Server VM 25.0.4+7-LTS on Linux 6.17.0-1022-azure
+AMD EPYC 9V74 80-Core Processor
 count a wide column:                      Best Time(ms)   Avg Time(ms)   
Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
 
------------------------------------------------------------------------------------------------------------------------
-No encoding                                        4544           4564         
 22          0.2        4544.2       1.0X
-UTF-8 is set                                       4411           4424         
 17          0.2        4411.0       1.0X
+No encoding                                        4737           4744         
  6          0.2        4736.7       1.0X
+UTF-8 is set                                       4481           4502         
 22          0.2        4481.0       1.1X
 
 Preparing data for benchmarking ...
-OpenJDK 64-Bit Server VM 25.0.2+10-LTS on Linux 6.14.0-1017-azure
-AMD EPYC 7763 64-Core Processor
+OpenJDK 64-Bit Server VM 25.0.4+7-LTS on Linux 6.17.0-1022-azure
+AMD EPYC 9V74 80-Core Processor
 select wide row:                          Best Time(ms)   Avg Time(ms)   
Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
 
------------------------------------------------------------------------------------------------------------------------
-No encoding                                        9487           9565         
 91          0.0      189740.3       1.0X
-UTF-8 is set                                      10224          10258         
 42          0.0      204476.7       0.9X
+No encoding                                       10142          10236         
 89          0.0      202833.6       1.0X
+UTF-8 is set                                      10400          10421         
 28          0.0      208006.9       1.0X
 
 Preparing data for benchmarking ...
-OpenJDK 64-Bit Server VM 25.0.2+10-LTS on Linux 6.14.0-1017-azure
-AMD EPYC 7763 64-Core Processor
+OpenJDK 64-Bit Server VM 25.0.4+7-LTS on Linux 6.17.0-1022-azure
+AMD EPYC 9V74 80-Core Processor
 Select a subset of 10 columns:            Best Time(ms)   Avg Time(ms)   
Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
 
------------------------------------------------------------------------------------------------------------------------
-Select 10 columns                                  1678           1682         
  6          0.6        1677.6       1.0X
-Select 1 column                                    1198           1200         
  3          0.8        1197.5       1.4X
+Select 10 columns                                  1553           1557         
  4          0.6        1553.1       1.0X
+Select 1 column                                    1154           1193         
 34          0.9        1154.3       1.3X
 
 Preparing data for benchmarking ...
-OpenJDK 64-Bit Server VM 25.0.2+10-LTS on Linux 6.14.0-1017-azure
-AMD EPYC 7763 64-Core Processor
+OpenJDK 64-Bit Server VM 25.0.4+7-LTS on Linux 6.17.0-1022-azure
+AMD EPYC 9V74 80-Core Processor
 creation of JSON parser per line:         Best Time(ms)   Avg Time(ms)   
Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
 
------------------------------------------------------------------------------------------------------------------------
-Short column without encoding                       621            627         
  5          1.6         621.4       1.0X
-Short column with UTF-8                            1178           1184         
 11          0.8        1177.8       0.5X
-Wide column without encoding                       4988           5023         
 31          0.2        4987.6       0.1X
-Wide column with UTF-8                             6444           6510         
 61          0.2        6443.6       0.1X
+Short column without encoding                       470            471         
  2          2.1         470.2       1.0X
+Short column with UTF-8                            1008           1010         
  2          1.0        1007.9       0.5X
+Wide column without encoding                       5437           5467         
 34          0.2        5437.4       0.1X
+Wide column with UTF-8                             7125           7134         
 10          0.1        7125.4       0.1X
 
 Preparing data for benchmarking ...
-OpenJDK 64-Bit Server VM 25.0.2+10-LTS on Linux 6.14.0-1017-azure
-AMD EPYC 7763 64-Core Processor
+OpenJDK 64-Bit Server VM 25.0.4+7-LTS on Linux 6.17.0-1022-azure
+AMD EPYC 9V74 80-Core Processor
 JSON functions:                           Best Time(ms)   Avg Time(ms)   
Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
 
------------------------------------------------------------------------------------------------------------------------
-Text read                                            50             51         
  2         19.9          50.2       1.0X
-from_json                                          1046           1048         
  2          1.0        1045.9       0.0X
-json_tuple                                          931            958         
 23          1.1         931.4       0.1X
-get_json_object wholestage off                      998           1001         
  3          1.0         998.2       0.1X
-get_json_object wholestage on                       913            915         
  4          1.1         912.5       0.1X
+Text read                                            67             71         
  6         15.0          66.7       1.0X
+from_json                                           938            956         
 25          1.1         938.2       0.1X
+json_tuple                                          899            901         
  1          1.1         899.2       0.1X
+get_json_object wholestage off                      952            962         
 10          1.0         952.4       0.1X
+get_json_object wholestage on                       859            862         
  3          1.2         859.0       0.1X
 
 Preparing data for benchmarking ...
-OpenJDK 64-Bit Server VM 25.0.2+10-LTS on Linux 6.14.0-1017-azure
-AMD EPYC 7763 64-Core Processor
+OpenJDK 64-Bit Server VM 25.0.4+7-LTS on Linux 6.17.0-1022-azure
+AMD EPYC 9V74 80-Core Processor
 Dataset of json strings:                  Best Time(ms)   Avg Time(ms)   
Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
 
------------------------------------------------------------------------------------------------------------------------
-Text read                                           210            211         
  1         23.8          42.0       1.0X
-schema inferring                                   1780           1785         
  7          2.8         356.0       0.1X
-parsing                                            2586           2592         
  9          1.9         517.3       0.1X
+Text read                                           224            231         
 11         22.3          44.8       1.0X
+schema inferring                                   1500           1503         
  3          3.3         300.0       0.1X
+parsing                                            2146           2154         
 12          2.3         429.2       0.1X
 
 Preparing data for benchmarking ...
-OpenJDK 64-Bit Server VM 25.0.2+10-LTS on Linux 6.14.0-1017-azure
-AMD EPYC 7763 64-Core Processor
+OpenJDK 64-Bit Server VM 25.0.4+7-LTS on Linux 6.17.0-1022-azure
+AMD EPYC 9V74 80-Core Processor
 Json files in the per-line mode:          Best Time(ms)   Avg Time(ms)   
Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
 
------------------------------------------------------------------------------------------------------------------------
-Text read                                           574            577         
  4          8.7         114.8       1.0X
-Schema inferring                                   2344           2347         
  2          2.1         468.9       0.2X
-Parsing without charset                            2927           2935         
  7          1.7         585.3       0.2X
-Parsing with UTF-8                                 5678           5682         
  4          0.9        1135.5       0.1X
+Text read                                           574            580         
  8          8.7         114.7       1.0X
+Schema inferring                                   1848           1854         
  6          2.7         369.6       0.3X
+Parsing without charset                            2249           2258         
  9          2.2         449.9       0.3X
+Parsing with UTF-8                                 4644           4647         
  3          1.1         928.7       0.1X
 
-OpenJDK 64-Bit Server VM 25.0.2+10-LTS on Linux 6.14.0-1017-azure
-AMD EPYC 7763 64-Core Processor
+OpenJDK 64-Bit Server VM 25.0.4+7-LTS on Linux 6.17.0-1022-azure
+AMD EPYC 9V74 80-Core Processor
 Write dates and timestamps:               Best Time(ms)   Avg Time(ms)   
Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
 
------------------------------------------------------------------------------------------------------------------------
-Create a dataset of timestamps                       99            103         
  4         10.1          99.0       1.0X
-to_json(timestamp)                                  533            535         
  3          1.9         532.7       0.2X
-write timestamps to files                           589            594         
  4          1.7         589.2       0.2X
-Create a dataset of dates                           105            107         
  2          9.5         104.8       0.9X
-to_json(date)                                       398            402         
  3          2.5         398.3       0.2X
-write dates to files                                398            407         
 12          2.5         397.8       0.2X
-
-OpenJDK 64-Bit Server VM 25.0.2+10-LTS on Linux 6.14.0-1017-azure
-AMD EPYC 7763 64-Core Processor
+Create a dataset of timestamps                      106            109         
  3          9.4         106.3       1.0X
+to_json(timestamp)                                  489            490         
  1          2.0         488.6       0.2X
+write timestamps to files                           547            551         
  4          1.8         546.7       0.2X
+Create a dataset of dates                           117            119         
  2          8.6         116.9       0.9X
+to_json(date)                                       344            347         
  3          2.9         344.4       0.3X
+write dates to files                                367            369         
  2          2.7         366.8       0.3X
+
+OpenJDK 64-Bit Server VM 25.0.4+7-LTS on Linux 6.17.0-1022-azure
+AMD EPYC 9V74 80-Core Processor
 Read dates and timestamps:                                             Best 
Time(ms)   Avg Time(ms)   Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
 
-----------------------------------------------------------------------------------------------------------------------------------------------------
-read timestamp text from files                                                 
  143            144           2          7.0         142.6       1.0X
-read timestamps from files                                                     
 1006           1009           3          1.0        1005.7       0.1X
-infer timestamps from files                                                    
 1849           1849           0          0.5        1849.1       0.1X
-read date text from files                                                      
  140            140           0          7.1         140.0       1.0X
-read date from files                                                           
  681            684           5          1.5         680.5       0.2X
-timestamp strings                                                              
  132            134           2          7.6         131.8       1.1X
-parse timestamps from Dataset[String]                                          
 1134           1136           2          0.9        1134.1       0.1X
-infer timestamps from Dataset[String]                                          
 1935           1938           3          0.5        1934.7       0.1X
-date strings                                                                   
  194            198           4          5.1         194.3       0.7X
-parse dates from Dataset[String]                                               
  935            936           1          1.1         935.1       0.2X
-from_json(timestamp)                                                           
 1563           1569           6          0.6        1562.8       0.1X
-from_json(date)                                                                
 1372           1398          35          0.7        1371.8       0.1X
-infer error timestamps from Dataset[String] with default format                
 1335           1338           3          0.7        1335.2       0.1X
-infer error timestamps from Dataset[String] with user-provided format          
 1318           1332          16          0.8        1317.6       0.1X
-infer error timestamps from Dataset[String] with legacy format                 
 1354           1356           1          0.7        1354.1       0.1X
-
-OpenJDK 64-Bit Server VM 25.0.2+10-LTS on Linux 6.14.0-1017-azure
-AMD EPYC 7763 64-Core Processor
+read timestamp text from files                                                 
  150            154           5          6.7         149.6       1.0X
+read timestamps from files                                                     
  878            884           6          1.1         878.4       0.2X
+infer timestamps from files                                                    
 1684           1688           6          0.6        1684.4       0.1X
+read date text from files                                                      
  136            141           6          7.3         136.2       1.1X
+read date from files                                                           
  579            581           2          1.7         579.1       0.3X
+timestamp strings                                                              
  136            142           8          7.4         135.9       1.1X
+parse timestamps from Dataset[String]                                          
 1046           1052           8          1.0        1046.2       0.1X
+infer timestamps from Dataset[String]                                          
 1841           1849           7          0.5        1841.0       0.1X
+date strings                                                                   
  197            198           1          5.1         197.4       0.8X
+parse dates from Dataset[String]                                               
  839            848           7          1.2         839.3       0.2X
+from_json(timestamp)                                                           
 1527           1538          16          0.7        1526.8       0.1X
+from_json(date)                                                                
 1328           1345          29          0.8        1327.9       0.1X
+infer error timestamps from Dataset[String] with default format                
 1186           1189           2          0.8        1186.4       0.1X
+infer error timestamps from Dataset[String] with user-provided format          
 1151           1152           2          0.9        1150.7       0.1X
+infer error timestamps from Dataset[String] with legacy format                 
 1210           1213           6          0.8        1209.8       0.1X
+
+OpenJDK 64-Bit Server VM 25.0.4+7-LTS on Linux 6.17.0-1022-azure
+AMD EPYC 9V74 80-Core Processor
 Filters pushdown:                         Best Time(ms)   Avg Time(ms)   
Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
 
------------------------------------------------------------------------------------------------------------------------
-w/o filters                                        5454           5462         
 11          0.0       54538.3       1.0X
-pushdown disabled                                  5418           5427         
 11          0.0       54175.8       1.0X
-w/ filters                                          584            591         
  6          0.2        5838.7       9.3X
+w/o filters                                        4727           4737         
  9          0.0       47265.0       1.0X
+pushdown disabled                                  4660           4675         
 14          0.0       46602.0       1.0X
+w/ filters                                          700            715         
 14          0.1        7003.7       6.7X
 
-OpenJDK 64-Bit Server VM 25.0.2+10-LTS on Linux 6.14.0-1017-azure
-AMD EPYC 7763 64-Core Processor
+OpenJDK 64-Bit Server VM 25.0.4+7-LTS on Linux 6.17.0-1022-azure
+AMD EPYC 9V74 80-Core Processor
 Partial JSON results:                     Best Time(ms)   Avg Time(ms)   
Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
 
------------------------------------------------------------------------------------------------------------------------
-parse invalid JSON                                 2244           2247         
  4          0.0      224391.1       1.0X
+parse invalid JSON                                 2515           2532         
 16          0.0      251515.4       1.0X
+
+OpenJDK 64-Bit Server VM 25.0.4+7-LTS on Linux 6.17.0-1022-azure
+AMD EPYC 9V74 80-Core Processor
+Top-level JSON array with 0-byte payloads:  Best Time(ms)   Avg Time(ms)   
Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
+-------------------------------------------------------------------------------------------------------------------------
+streaming enabled: false                              74             83        
  10          1.3         742.9       1.0X
+streaming enabled: true                               76             77        
   2          1.3         760.3       1.0X
+
+OpenJDK 64-Bit Server VM 25.0.4+7-LTS on Linux 6.17.0-1022-azure
+AMD EPYC 9V74 80-Core Processor
+Top-level JSON array with 65536-byte payloads:  Best Time(ms)   Avg Time(ms)   
Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
+-----------------------------------------------------------------------------------------------------------------------------
+streaming enabled: false                                 108            111    
       4          0.0      107718.5       1.0X
+streaming enabled: true                                  107            111    
       5          0.0      106816.1       1.0X
 
 
diff --git a/sql/core/benchmarks/JsonBenchmark-results.txt 
b/sql/core/benchmarks/JsonBenchmark-results.txt
index f6b52b4def0c..2241a4c7aed8 100644
--- a/sql/core/benchmarks/JsonBenchmark-results.txt
+++ b/sql/core/benchmarks/JsonBenchmark-results.txt
@@ -3,128 +3,142 @@ Benchmark for performance of JSON parsing
 
================================================================================================
 
 Preparing data for benchmarking ...
-OpenJDK 64-Bit Server VM 17.0.18+8-LTS on Linux 6.14.0-1017-azure
-AMD EPYC 9V74 80-Core Processor
+OpenJDK 64-Bit Server VM 17.0.20+8-LTS on Linux 6.17.0-1022-azure
+AMD EPYC 7763 64-Core Processor
 JSON schema inferring:                    Best Time(ms)   Avg Time(ms)   
Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
 
------------------------------------------------------------------------------------------------------------------------
-No encoding                                        1899           1983         
 91          2.6         379.7       1.0X
-UTF-8 is set                                       5121           5134         
 13          1.0        1024.2       0.4X
+No encoding                                        2438           2486         
 75          2.1         487.7       1.0X
+UTF-8 is set                                       5905           5916         
 12          0.8        1180.9       0.4X
 
 Preparing data for benchmarking ...
-OpenJDK 64-Bit Server VM 17.0.18+8-LTS on Linux 6.14.0-1017-azure
-AMD EPYC 9V74 80-Core Processor
+OpenJDK 64-Bit Server VM 17.0.20+8-LTS on Linux 6.17.0-1022-azure
+AMD EPYC 7763 64-Core Processor
 count a short column:                     Best Time(ms)   Avg Time(ms)   
Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
 
------------------------------------------------------------------------------------------------------------------------
-No encoding                                        1927           1963         
 55          2.6         385.5       1.0X
-UTF-8 is set                                       4451           4462         
 10          1.1         890.2       0.4X
+No encoding                                        2243           2289         
 51          2.2         448.6       1.0X
+UTF-8 is set                                       4872           4892         
 18          1.0         974.4       0.5X
 
 Preparing data for benchmarking ...
-OpenJDK 64-Bit Server VM 17.0.18+8-LTS on Linux 6.14.0-1017-azure
-AMD EPYC 9V74 80-Core Processor
+OpenJDK 64-Bit Server VM 17.0.20+8-LTS on Linux 6.17.0-1022-azure
+AMD EPYC 7763 64-Core Processor
 count a wide column:                      Best Time(ms)   Avg Time(ms)   
Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
 
------------------------------------------------------------------------------------------------------------------------
-No encoding                                        5230           5246         
 16          0.2        5230.0       1.0X
-UTF-8 is set                                       4768           4856         
 88          0.2        4767.9       1.1X
+No encoding                                        3307           3333         
 22          0.3        3307.4       1.0X
+UTF-8 is set                                       4700           4718         
 19          0.2        4699.9       0.7X
 
 Preparing data for benchmarking ...
-OpenJDK 64-Bit Server VM 17.0.18+8-LTS on Linux 6.14.0-1017-azure
-AMD EPYC 9V74 80-Core Processor
+OpenJDK 64-Bit Server VM 17.0.20+8-LTS on Linux 6.17.0-1022-azure
+AMD EPYC 7763 64-Core Processor
 select wide row:                          Best Time(ms)   Avg Time(ms)   
Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
 
------------------------------------------------------------------------------------------------------------------------
-No encoding                                        8996           9163         
215          0.0      179920.8       1.0X
-UTF-8 is set                                       9757           9790         
 31          0.0      195143.4       0.9X
+No encoding                                        9262           9465         
212          0.0      185247.3       1.0X
+UTF-8 is set                                      10776          10800         
 29          0.0      215517.2       0.9X
 
 Preparing data for benchmarking ...
-OpenJDK 64-Bit Server VM 17.0.18+8-LTS on Linux 6.14.0-1017-azure
-AMD EPYC 9V74 80-Core Processor
+OpenJDK 64-Bit Server VM 17.0.20+8-LTS on Linux 6.17.0-1022-azure
+AMD EPYC 7763 64-Core Processor
 Select a subset of 10 columns:            Best Time(ms)   Avg Time(ms)   
Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
 
------------------------------------------------------------------------------------------------------------------------
-Select 10 columns                                  1557           1560         
  3          0.6        1557.2       1.0X
-Select 1 column                                    1184           1196         
 20          0.8        1183.7       1.3X
+Select 10 columns                                  1714           1728         
 13          0.6        1713.9       1.0X
+Select 1 column                                    1262           1266         
  3          0.8        1262.4       1.4X
 
 Preparing data for benchmarking ...
-OpenJDK 64-Bit Server VM 17.0.18+8-LTS on Linux 6.14.0-1017-azure
-AMD EPYC 9V74 80-Core Processor
+OpenJDK 64-Bit Server VM 17.0.20+8-LTS on Linux 6.17.0-1022-azure
+AMD EPYC 7763 64-Core Processor
 creation of JSON parser per line:         Best Time(ms)   Avg Time(ms)   
Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
 
------------------------------------------------------------------------------------------------------------------------
-Short column without encoding                       561            563         
  3          1.8         561.5       1.0X
-Short column with UTF-8                            1140           1146         
  8          0.9        1139.9       0.5X
-Wide column without encoding                       5163           5179         
 23          0.2        5163.2       0.1X
-Wide column with UTF-8                             9810           9833         
 22          0.1        9810.2       0.1X
+Short column without encoding                       666            669         
  3          1.5         665.7       1.0X
+Short column with UTF-8                            1218           1221         
  2          0.8        1218.4       0.5X
+Wide column without encoding                       6437           6459         
 19          0.2        6437.3       0.1X
+Wide column with UTF-8                             7465           7476         
 11          0.1        7465.2       0.1X
 
 Preparing data for benchmarking ...
-OpenJDK 64-Bit Server VM 17.0.18+8-LTS on Linux 6.14.0-1017-azure
-AMD EPYC 9V74 80-Core Processor
+OpenJDK 64-Bit Server VM 17.0.20+8-LTS on Linux 6.17.0-1022-azure
+AMD EPYC 7763 64-Core Processor
 JSON functions:                           Best Time(ms)   Avg Time(ms)   
Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
 
------------------------------------------------------------------------------------------------------------------------
-Text read                                            62             66         
  4         16.2          61.7       1.0X
-from_json                                           969            979         
  9          1.0         968.7       0.1X
-json_tuple                                          905            908         
  4          1.1         905.1       0.1X
-get_json_object wholestage off                      938            942         
  4          1.1         938.3       0.1X
-get_json_object wholestage on                       835            847         
 18          1.2         835.0       0.1X
+Text read                                            59             61         
  2         16.9          59.0       1.0X
+from_json                                          1125           1143         
 17          0.9        1125.3       0.1X
+json_tuple                                         1039           1041         
  3          1.0        1038.7       0.1X
+get_json_object wholestage off                     1054           1066         
 11          0.9        1054.2       0.1X
+get_json_object wholestage on                       971            972         
  1          1.0         971.4       0.1X
 
 Preparing data for benchmarking ...
-OpenJDK 64-Bit Server VM 17.0.18+8-LTS on Linux 6.14.0-1017-azure
-AMD EPYC 9V74 80-Core Processor
+OpenJDK 64-Bit Server VM 17.0.20+8-LTS on Linux 6.17.0-1022-azure
+AMD EPYC 7763 64-Core Processor
 Dataset of json strings:                  Best Time(ms)   Avg Time(ms)   
Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
 
------------------------------------------------------------------------------------------------------------------------
-Text read                                           254            256         
  3         19.7          50.7       1.0X
-schema inferring                                   1504           1524         
 26          3.3         300.8       0.2X
-parsing                                            2391           2421         
 27          2.1         478.2       0.1X
+Text read                                           234            267         
 56         21.4          46.8       1.0X
+schema inferring                                   1908           1921         
 11          2.6         381.6       0.1X
+parsing                                            2595           2596         
  1          1.9         518.9       0.1X
 
 Preparing data for benchmarking ...
-OpenJDK 64-Bit Server VM 17.0.18+8-LTS on Linux 6.14.0-1017-azure
-AMD EPYC 9V74 80-Core Processor
+OpenJDK 64-Bit Server VM 17.0.20+8-LTS on Linux 6.17.0-1022-azure
+AMD EPYC 7763 64-Core Processor
 Json files in the per-line mode:          Best Time(ms)   Avg Time(ms)   
Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
 
------------------------------------------------------------------------------------------------------------------------
-Text read                                           624            652         
 30          8.0         124.8       1.0X
-Schema inferring                                   2032           2036         
  5          2.5         406.5       0.3X
-Parsing without charset                            2527           2529         
  3          2.0         505.4       0.2X
-Parsing with UTF-8                                 5453           5470         
 27          0.9        1090.6       0.1X
+Text read                                           577            580         
  4          8.7         115.4       1.0X
+Schema inferring                                   2454           2468         
 13          2.0         490.8       0.2X
+Parsing without charset                            2973           2978         
  9          1.7         594.7       0.2X
+Parsing with UTF-8                                 5773           5776         
  2          0.9        1154.7       0.1X
 
-OpenJDK 64-Bit Server VM 17.0.18+8-LTS on Linux 6.14.0-1017-azure
-AMD EPYC 9V74 80-Core Processor
+OpenJDK 64-Bit Server VM 17.0.20+8-LTS on Linux 6.17.0-1022-azure
+AMD EPYC 7763 64-Core Processor
 Write dates and timestamps:               Best Time(ms)   Avg Time(ms)   
Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
 
------------------------------------------------------------------------------------------------------------------------
-Create a dataset of timestamps                      107            110         
  4          9.4         106.5       1.0X
-to_json(timestamp)                                  576            582         
  9          1.7         576.3       0.2X
-write timestamps to files                           623            626         
  4          1.6         623.2       0.2X
-Create a dataset of dates                           120            123         
  3          8.3         120.1       0.9X
-to_json(date)                                       391            398         
  7          2.6         391.0       0.3X
-write dates to files                                415            418         
  5          2.4         415.3       0.3X
-
-OpenJDK 64-Bit Server VM 17.0.18+8-LTS on Linux 6.14.0-1017-azure
-AMD EPYC 9V74 80-Core Processor
+Create a dataset of timestamps                      103            106         
  3          9.7         102.7       1.0X
+to_json(timestamp)                                  640            641         
  1          1.6         640.0       0.2X
+write timestamps to files                           685            692         
  6          1.5         685.4       0.1X
+Create a dataset of dates                           120            125         
  4          8.3         120.4       0.9X
+to_json(date)                                       439            446         
  7          2.3         439.2       0.2X
+write dates to files                                434            438         
  4          2.3         434.1       0.2X
+
+OpenJDK 64-Bit Server VM 17.0.20+8-LTS on Linux 6.17.0-1022-azure
+AMD EPYC 7763 64-Core Processor
 Read dates and timestamps:                                             Best 
Time(ms)   Avg Time(ms)   Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
 
-----------------------------------------------------------------------------------------------------------------------------------------------------
-read timestamp text from files                                                 
  163            165           4          6.1         162.8       1.0X
-read timestamps from files                                                     
  987            997          12          1.0         987.2       0.2X
-infer timestamps from files                                                    
 1835           1847          20          0.5        1835.4       0.1X
-read date text from files                                                      
  150            153           3          6.7         149.8       1.1X
-read date from files                                                           
  621            623           2          1.6         621.4       0.3X
-timestamp strings                                                              
  145            146           1          6.9         145.2       1.1X
-parse timestamps from Dataset[String]                                          
 1171           1185          21          0.9        1170.6       0.1X
-infer timestamps from Dataset[String]                                          
 2012           2015           5          0.5        2011.7       0.1X
-date strings                                                                   
  215            217           2          4.6         215.2       0.8X
-parse dates from Dataset[String]                                               
  927            931           5          1.1         927.4       0.2X
-from_json(timestamp)                                                           
 1635           1639           6          0.6        1635.3       0.1X
-from_json(date)                                                                
 1405           1412           6          0.7        1405.0       0.1X
-infer error timestamps from Dataset[String] with default format                
 1273           1276           5          0.8        1273.4       0.1X
-infer error timestamps from Dataset[String] with user-provided format          
 1257           1260           3          0.8        1256.7       0.1X
-infer error timestamps from Dataset[String] with legacy format                 
 1258           1260           3          0.8        1258.0       0.1X
-
-OpenJDK 64-Bit Server VM 17.0.18+8-LTS on Linux 6.14.0-1017-azure
-AMD EPYC 9V74 80-Core Processor
+read timestamp text from files                                                 
  148            153           8          6.7         148.3       1.0X
+read timestamps from files                                                     
 1067           1068           1          0.9        1067.2       0.1X
+infer timestamps from files                                                    
 2003           2013          15          0.5        2003.1       0.1X
+read date text from files                                                      
  141            144           4          7.1         141.2       1.1X
+read date from files                                                           
  750            752           2          1.3         750.4       0.2X
+timestamp strings                                                              
  137            137           1          7.3         136.9       1.1X
+parse timestamps from Dataset[String]                                          
 1195           1201           9          0.8        1194.8       0.1X
+infer timestamps from Dataset[String]                                          
 2093           2097           4          0.5        2093.0       0.1X
+date strings                                                                   
  198            198           1          5.1         197.8       0.7X
+parse dates from Dataset[String]                                               
  954            956           2          1.0         953.6       0.2X
+from_json(timestamp)                                                           
 1659           1677          26          0.6        1659.5       0.1X
+from_json(date)                                                                
 1435           1448          19          0.7        1434.6       0.1X
+infer error timestamps from Dataset[String] with default format                
 1426           1435          12          0.7        1426.0       0.1X
+infer error timestamps from Dataset[String] with user-provided format          
 1437           1448          19          0.7        1437.5       0.1X
+infer error timestamps from Dataset[String] with legacy format                 
 1452           1455           2          0.7        1452.1       0.1X
+
+OpenJDK 64-Bit Server VM 17.0.20+8-LTS on Linux 6.17.0-1022-azure
+AMD EPYC 7763 64-Core Processor
 Filters pushdown:                         Best Time(ms)   Avg Time(ms)   
Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
 
------------------------------------------------------------------------------------------------------------------------
-w/o filters                                        4817           4833         
 15          0.0       48167.4       1.0X
-pushdown disabled                                  4772           4776         
  5          0.0       47721.9       1.0X
-w/ filters                                          695            710         
 16          0.1        6949.2       6.9X
+w/o filters                                        6016           6028         
 10          0.0       60158.9       1.0X
+pushdown disabled                                  5833           5857         
 22          0.0       58329.5       1.0X
+w/ filters                                          667            668         
  1          0.1        6667.9       9.0X
 
-OpenJDK 64-Bit Server VM 17.0.18+8-LTS on Linux 6.14.0-1017-azure
-AMD EPYC 9V74 80-Core Processor
+OpenJDK 64-Bit Server VM 17.0.20+8-LTS on Linux 6.17.0-1022-azure
+AMD EPYC 7763 64-Core Processor
 Partial JSON results:                     Best Time(ms)   Avg Time(ms)   
Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
 
------------------------------------------------------------------------------------------------------------------------
-parse invalid JSON                                 2386           2451         
111          0.0      238564.1       1.0X
+parse invalid JSON                                 2372           2465         
149          0.0      237212.6       1.0X
+
+OpenJDK 64-Bit Server VM 17.0.20+8-LTS on Linux 6.17.0-1022-azure
+AMD EPYC 7763 64-Core Processor
+Top-level JSON array with 0-byte payloads:  Best Time(ms)   Avg Time(ms)   
Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
+-------------------------------------------------------------------------------------------------------------------------
+streaming enabled: false                              77             83        
   6          1.3         772.6       1.0X
+streaming enabled: true                               79             82        
   4          1.3         793.2       1.0X
+
+OpenJDK 64-Bit Server VM 17.0.20+8-LTS on Linux 6.17.0-1022-azure
+AMD EPYC 7763 64-Core Processor
+Top-level JSON array with 65536-byte payloads:  Best Time(ms)   Avg Time(ms)   
Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
+-----------------------------------------------------------------------------------------------------------------------------
+streaming enabled: false                                 160            168    
      13          0.0      159991.4       1.0X
+streaming enabled: true                                  161            162    
       1          0.0      161295.1       1.0X
 
 
diff --git 
a/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/json/JsonDataSource.scala
 
b/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/json/JsonDataSource.scala
index d5be5c7d7272..b39c9acc33da 100644
--- 
a/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/json/JsonDataSource.scala
+++ 
b/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/json/JsonDataSource.scala
@@ -398,7 +398,7 @@ object MultiLineJsonDataSource extends JsonDataSource {
       file: PartitionedFile,
       parser: JacksonParser,
       schema: StructType): Iterator[InternalRow] = {
-    def partitionedFileString(ignored: Any): UTF8String = {
+    lazy val fileLiteral: UTF8String = {
       Utils.tryWithResource {
         Utils.createResourceUninterruptiblyIfInTaskThread {
           CodecStreams.createInputStreamWithCloseResource(conf, file.toPath)
@@ -407,6 +407,7 @@ object MultiLineJsonDataSource extends JsonDataSource {
         UTF8String.fromBytes(inputStream.readAllBytes())
       }
     }
+    def partitionedFileString(ignored: Any): UTF8String = fileLiteral
     val streamParser = parser.options.encoding
       .map(enc => CreateJacksonParser.inputStream(enc, _: JsonFactory, _: 
InputStream))
       .getOrElse(CreateJacksonParser.inputStream(_: JsonFactory, _: 
InputStream))
@@ -417,8 +418,14 @@ object MultiLineJsonDataSource extends JsonDataSource {
       schema,
       parser.options.columnNameOfCorruptRecord)
 
-    safeParser.parse(
-      CodecStreams.createInputStreamWithCloseResource(conf, file.toPath))
+    val input = CodecStreams.createInputStreamWithCloseResource(conf, 
file.toPath)
+    if (parser.options.streamMultilineTopLevelArray) {
+      safeParser.parseIterator(
+        input,
+        input => parser.parseIterator[InputStream](input, streamParser, 
partitionedFileString))
+    } else {
+      safeParser.parse(input)
+    }
   }
 
   override protected def readStream(
@@ -428,16 +435,25 @@ object MultiLineJsonDataSource extends JsonDataSource {
     // The entry is a single JSON document. Buffer its bytes so the 
corrupt-record column can echo
     // the whole document on a parse failure, mirroring `readFile`'s 
`partitionedFileString`.
     val bytes = in.readAllBytes()
+    lazy val documentLiteral: UTF8String = UTF8String.fromBytes(bytes)
     val streamParser = parser.options.encoding
       .map(enc => CreateJacksonParser.inputStream(enc, _: JsonFactory, _: 
InputStream))
       .getOrElse(CreateJacksonParser.inputStream(_: JsonFactory, _: 
InputStream))
 
     val safeParser = new FailureSafeParser[InputStream](
-      input => parser.parse[InputStream](input, streamParser, _ => 
UTF8String.fromBytes(bytes)),
+      input => parser.parse[InputStream](input, streamParser, _ => 
documentLiteral),
       parser.options.parseMode,
       schema,
       parser.options.columnNameOfCorruptRecord)
 
-    safeParser.parse(new ByteArrayInputStream(bytes))
+    val input = new ByteArrayInputStream(bytes)
+    if (parser.options.streamMultilineTopLevelArray) {
+      safeParser.parseIterator(
+        input,
+        input => parser.parseIterator[InputStream](
+          input, streamParser, _ => documentLiteral))
+    } else {
+      safeParser.parse(input)
+    }
   }
 }
diff --git 
a/sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/JSONArchiveReadBase.scala
 
b/sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/JSONArchiveReadBase.scala
index 70de9e5e6317..854fc55faed4 100644
--- 
a/sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/JSONArchiveReadBase.scala
+++ 
b/sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/JSONArchiveReadBase.scala
@@ -21,7 +21,8 @@ import java.io.File
 import java.nio.charset.StandardCharsets
 import java.nio.file.Files
 
-import org.apache.spark.sql.AnalysisException
+import org.apache.spark.SparkException
+import org.apache.spark.sql.{AnalysisException, Row}
 import org.apache.spark.sql.internal.SQLConf
 import org.apache.spark.sql.types.{NullType, StringType}
 
@@ -166,6 +167,73 @@ trait JSONArchiveReadBase extends ArchiveReadSuiteBase {
       extraOptions = Map("multiLine" -> "true"))
   }
 
+  test("JSON: streaming multi-line top-level arrays match a directory read") {
+    withSQLConf(SQLConf.JSON_STREAM_MULTILINE_TOP_LEVEL_ARRAY.key -> "true") {
+      assertArchiveMatchesDir(
+        Seq(
+          "a.json" -> 
jsonBytes("""[{"id":1,"name":"Alice"},{"id":2,"name":"Bob"}]"""),
+          "b.json" -> jsonBytes("""[{"id":3,"name":"Carol"}]""")),
+        extraOptions = Map("multiLine" -> "true"))
+      // Jackson auto-detects UTF-16/32, so only a non-UTF charset shows 
`encoding` is applied.
+      // scalastyle:off nonascii
+      assertArchiveMatchesDir(
+        Seq("a.json" -> 
"[{\"id\":1,\"name\":\"Jos\u00e9\"},{\"id\":2,\"name\":\"Bob\"}]"
+          .getBytes(StandardCharsets.ISO_8859_1)),
+        extraOptions = Map("multiLine" -> "true", "encoding" -> "ISO-8859-1"))
+      // scalastyle:on nonascii
+    }
+  }
+
+  gridTest("JSON: streaming archive arrays resume after a partial element")(
+      Seq("PERMISSIVE", "DROPMALFORMED", "FAILFAST")) { mode =>
+    withSQLConf(SQLConf.JSON_STREAM_MULTILINE_TOP_LEVEL_ARRAY.key -> "true") {
+      val document = """[{"id":"bad","name":"Alice"},{"id":2,"name":"Bob"}]"""
+      withArchiveFile() { archive =>
+        writeArchive(archive, Seq(entryName(0) -> jsonBytes(document)))
+        val df = read(
+          archive.getCanonicalPath,
+          Map("multiLine" -> "true", "mode" -> mode),
+          s"$readSchema, _corrupt_record STRING")
+
+        mode match {
+          case "PERMISSIVE" =>
+            checkAnswer(df, Seq(Row(null, "Alice", document), Row(2, "Bob", 
null)))
+          case "DROPMALFORMED" =>
+            checkAnswer(df, Row(2, "Bob", null))
+          case "FAILFAST" =>
+            val error = intercept[SparkException](df.collect())
+            assert(error.getCause.asInstanceOf[SparkException].getCondition ===
+              "MALFORMED_RECORD_IN_PARSING.WITHOUT_SUGGESTION")
+        }
+      }
+    }
+  }
+
+  gridTest("JSON: streaming archive arrays handle terminal malformed input")(
+      Seq("PERMISSIVE", "DROPMALFORMED", "FAILFAST")) { mode =>
+    withSQLConf(SQLConf.JSON_STREAM_MULTILINE_TOP_LEVEL_ARRAY.key -> "true") {
+      val document = """[{"id":1,"name":"Alice"} {"id":2,"name":"Bob"}]"""
+      withArchiveFile() { archive =>
+        writeArchive(archive, Seq(entryName(0) -> jsonBytes(document)))
+        val df = read(
+          archive.getCanonicalPath,
+          Map("multiLine" -> "true", "mode" -> mode),
+          s"$readSchema, _corrupt_record STRING")
+
+        mode match {
+          case "PERMISSIVE" =>
+            checkAnswer(df, Seq(Row(1, "Alice", null), Row(null, null, 
document)))
+          case "DROPMALFORMED" =>
+            checkAnswer(df, Row(1, "Alice", null))
+          case "FAILFAST" =>
+            val error = intercept[SparkException](df.collect())
+            assert(error.getCause.asInstanceOf[SparkException].getCondition ===
+              "MALFORMED_RECORD_IN_PARSING.WITHOUT_SUGGESTION")
+        }
+      }
+    }
+  }
+
   test("JSON: a malformed record in an archive entry matches a directory read 
(both modes)") {
     // Permissive mode (the default): a malformed record parses to nulls with 
its raw text echoed
     // into `_corrupt_record`. The archive path wires its own 
FailureSafeParser in `readStream` --
diff --git 
a/sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/json/JsonBenchmark.scala
 
b/sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/json/JsonBenchmark.scala
index 94a2ccc41d30..bb779b036156 100644
--- 
a/sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/json/JsonBenchmark.scala
+++ 
b/sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/json/JsonBenchmark.scala
@@ -17,6 +17,8 @@
 package org.apache.spark.sql.execution.datasources.json
 
 import java.io.File
+import java.nio.charset.StandardCharsets
+import java.nio.file.Files
 import java.time.{Instant, LocalDate}
 
 import org.apache.spark.benchmark.Benchmark
@@ -37,6 +39,15 @@ import org.apache.spark.sql.types._
  *   3. generate result:
  *      SPARK_GENERATE_BENCHMARK_FILES=1 build/sbt "sql/Test/runMain <this 
class>"
  *      Results will be written to "benchmarks/JSONBenchmark-results.txt".
+ *
+ *   To measure only the top-level JSON array cases, run it with the 
"top-level-array"
+ *   argument. It skips every other case, so it must not be combined with
+ *   SPARK_GENERATE_BENCHMARK_FILES=1: the results file is truncated before 
the arguments
+ *   are read, so it would be left holding only the top-level array sections.
+ *   1. without sbt:
+ *      bin/spark-submit --class <this class> --jars <spark core test jar>,
+ *        <spark catalyst test jar> <spark sql test jar> top-level-array
+ *   2. build/sbt "sql/Test/runMain <this class> top-level-array"
  * }}}
  */
 object JsonBenchmark extends SqlBasedBenchmark {
@@ -578,8 +589,46 @@ object JsonBenchmark extends SqlBasedBenchmark {
     benchmark.run()
   }
 
+  private def topLevelArrayBenchmark(
+      rowsNum: Int,
+      payloadSize: Int,
+      numIters: Int): Unit = {
+    val payload = "x" * payloadSize
+    val document = (0 until rowsNum)
+      .map(i => s"""{"a":$i,"payload":"$payload"}""")
+      .mkString("[", ",", "]")
+    val schema = new StructType().add("a", IntegerType).add("payload", 
StringType)
+    val benchmark = new Benchmark(
+      s"Top-level JSON array with $payloadSize-byte payloads", rowsNum, output 
= output)
+
+    withTempPath { path =>
+      Files.write(path.toPath, document.getBytes(StandardCharsets.UTF_8))
+
+      Seq(false, true).foreach { enabled =>
+        benchmark.addCase(s"streaming enabled: $enabled", numIters) { _ =>
+          withSQLConf(SQLConf.JSON_STREAM_MULTILINE_TOP_LEVEL_ARRAY.key -> 
enabled.toString) {
+            spark.read
+              .option("multiLine", true)
+              .schema(schema)
+              .json(path.getCanonicalPath)
+              .noop()
+          }
+        }
+      }
+
+      benchmark.run()
+    }
+  }
+
   override def runBenchmarkSuite(mainArgs: Array[String]): Unit = {
     val numIters = 3
+    if (mainArgs.contains("top-level-array")) {
+      runBenchmark("Benchmark for top-level JSON array parsing") {
+        topLevelArrayBenchmark(rowsNum = 100000, payloadSize = 0, numIters = 
numIters)
+        topLevelArrayBenchmark(rowsNum = 1000, payloadSize = 64 * 1024, 
numIters = numIters)
+      }
+      return
+    }
     runBenchmark("Benchmark for performance of JSON parsing") {
       schemaInferring(5 * 1000 * 1000, numIters)
       countShortColumn(5 * 1000 * 1000, numIters)
@@ -595,6 +644,8 @@ object JsonBenchmark extends SqlBasedBenchmark {
       // TODO (SPARK-32325): Add benchmarks for filters with nested column 
attributes.
       filtersPushdownBenchmark(rowsNum = 100 * 1000, numIters)
       partialResultBenchmark(rowsNum = 10000, numIters)
+      topLevelArrayBenchmark(rowsNum = 100000, payloadSize = 0, numIters = 
numIters)
+      topLevelArrayBenchmark(rowsNum = 1000, payloadSize = 64 * 1024, numIters 
= numIters)
     }
   }
 }
diff --git 
a/sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/json/JsonSuite.scala
 
b/sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/json/JsonSuite.scala
index 4cd6783e1df6..538e424a535d 100644
--- 
a/sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/json/JsonSuite.scala
+++ 
b/sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/json/JsonSuite.scala
@@ -23,28 +23,32 @@ import java.nio.file.Files
 import java.sql.{Date, Timestamp}
 import java.time.{Duration, Instant, LocalDate, LocalDateTime, Period, ZoneId, 
ZoneOffset}
 import java.util.Locale
-import java.util.concurrent.atomic.AtomicLong
+import java.util.concurrent.atomic.{AtomicInteger, AtomicLong}
 
-import com.fasterxml.jackson.core.JsonFactory
+import com.fasterxml.jackson.core.{JsonFactory, JsonToken}
+import com.fasterxml.jackson.core.util.JsonParserDelegate
 import org.apache.hadoop.fs.{Path, PathFilter}
 import org.apache.hadoop.io.SequenceFile.CompressionType
 import org.apache.hadoop.io.compress.{CompressionCodecFactory, GzipCodec}
 
 import org.apache.spark.{SparkConf, SparkException, SparkRuntimeException, 
SparkUpgradeException, TestUtils}
 import org.apache.spark.SparkIllegalArgumentException
+import org.apache.spark.TaskContext
 import org.apache.spark.io.ZStdCompressionCodec
+import org.apache.spark.paths.SparkPath
 import org.apache.spark.rdd.RDD
 import org.apache.spark.scheduler.{SparkListener, SparkListenerJobEnd}
 import org.apache.spark.sql.{functions => F, _}
+import org.apache.spark.sql.catalyst.InternalRow
 import org.apache.spark.sql.catalyst.json._
-import org.apache.spark.sql.catalyst.util.{CharsetProvider, DateTimeTestUtils, 
DateTimeUtils, HadoopCompressionCodec}
+import org.apache.spark.sql.catalyst.util.{BadRecordException, 
CharsetProvider, DateTimeTestUtils, DateTimeUtils, HadoopCompressionCodec}
 import org.apache.spark.sql.catalyst.util.HadoopCompressionCodec.GZIP
 import org.apache.spark.sql.catalyst.util.TimestampNanosTestUtils
 import 
org.apache.spark.sql.catalyst.util.TimestampNanosTestUtils.foreachNanosPrecision
 import org.apache.spark.sql.catalyst.util.TypeUtils.toSQLType
 import org.apache.spark.sql.errors.QueryExecutionErrors.toSQLId
 import org.apache.spark.sql.execution.ExternalRDD
-import org.apache.spark.sql.execution.datasources.{CommonFileDataSourceSuite, 
DataSource, InMemoryFileIndex, NoopCache}
+import org.apache.spark.sql.execution.datasources.{CommonFileDataSourceSuite, 
DataSource, InMemoryFileIndex, NoopCache, PartitionedFile}
 import org.apache.spark.sql.execution.datasources.v2.json.JsonScanBuilder
 import org.apache.spark.sql.internal.SQLConf
 import org.apache.spark.sql.test.SharedSparkSession
@@ -54,6 +58,7 @@ import org.apache.spark.sql.types.StructType.fromDDL
 import org.apache.spark.sql.types.TestUDT.{MyDenseVector, MyDenseVectorUDT}
 import org.apache.spark.sql.util.CaseInsensitiveStringMap
 import org.apache.spark.tags.ExtendedSQLTest
+import org.apache.spark.unsafe.types.UTF8String
 import org.apache.spark.util.ArrayImplicits._
 import org.apache.spark.util.Utils
 
@@ -1066,6 +1071,501 @@ abstract class JsonSuite
     }
   }
 
+  gridTest("SPARK-3308 Read multiline top level JSON arrays")(
+      Seq(false, true)) { enabled =>
+    withSQLConf(SQLConf.JSON_STREAM_MULTILINE_TOP_LEVEL_ARRAY.key -> 
enabled.toString) {
+      withTempPath { file =>
+        Files.write(file.toPath, 
"""[{"a":1},{"a":2}]""".getBytes(StandardCharsets.UTF_8))
+        checkAnswer(
+          spark.read.option("multiLine", true).schema("a 
int").json(file.getCanonicalPath),
+          Seq(Row(1), Row(2)))
+      }
+    }
+  }
+
+  gridTest("multiline top level JSON array with singleVariantColumn is not 
streamed")(
+      Seq(false, true)) { enabled =>
+    withSQLConf(SQLConf.JSON_STREAM_MULTILINE_TOP_LEVEL_ARRAY.key -> 
enabled.toString) {
+      withTempPath { file =>
+        Files.write(file.toPath, 
"""[{"a":1},{"a":2}]""".getBytes(StandardCharsets.UTF_8))
+        // singleVariantColumn binds the whole document to one variant, so the 
streaming
+        // predicate excludes it; without that conjunct this yields one row 
per element.
+        checkAnswer(
+          spark.read.option("multiLine", true).option("singleVariantColumn", 
"var")
+            .json(file.getCanonicalPath).selectExpr("to_json(var)"),
+          Seq(Row("""[{"a":1},{"a":2}]""")))
+      }
+    }
+  }
+
+  test("multiline top level JSON arrays are parsed lazily") {
+    val schema = StructType(Seq(StructField("a", IntegerType)))
+    val options = new JSONOptions(Map("multiLine" -> "true"), 
SQLConf.get.sessionLocalTimeZone)
+    val parser = new JacksonParser(schema, options, allowArrayAsStructs = true)
+    val input = new ByteArrayInputStream(
+      s"""[{"a":1},{"a":2,"payload":"${"x" * 
200000}"}]""".getBytes(StandardCharsets.UTF_8))
+    val rows = parser.parseIterator[InputStream](
+      input,
+      CreateJacksonParser.inputStream(_: JsonFactory, _: InputStream),
+      stream => UTF8String.fromBytes(stream.readAllBytes()))
+
+    assert(rows.next().getInt(0) === 1)
+    assert(input.available() > 0)
+  }
+
+  test("non-array JSON is parsed eagerly by parseIterator") {
+    val schema = StructType(Seq(StructField("a", IntegerType)))
+    val options = new JSONOptions(Map("multiLine" -> "true"), 
SQLConf.get.sessionLocalTimeZone)
+    val parser = new JacksonParser(schema, options, allowArrayAsStructs = true)
+    var closed = false
+    val input = new 
ByteArrayInputStream("""{"a":1}""".getBytes(StandardCharsets.UTF_8)) {
+      override def close(): Unit = {
+        closed = true
+        super.close()
+      }
+    }
+
+    val rows = parser.parseIterator[InputStream](
+      input,
+      CreateJacksonParser.inputStream(_: JsonFactory, _: InputStream),
+      stream => UTF8String.fromBytes(stream.readAllBytes()))
+
+    assert(closed)
+    assert(rows.next().getInt(0) === 1)
+    assert(!rows.hasNext)
+  }
+
+  test("non-array partial results close the parser") {
+    val schema = StructType(Seq(StructField("a", IntegerType)))
+    val options = new JSONOptions(Map("multiLine" -> "true"), 
SQLConf.get.sessionLocalTimeZone)
+    val parser = new JacksonParser(schema, options, allowArrayAsStructs = true)
+    var closed = false
+    val input = new 
ByteArrayInputStream("""{"a":"bad"}""".getBytes(StandardCharsets.UTF_8)) {
+      override def close(): Unit = {
+        closed = true
+        super.close()
+      }
+    }
+
+    val error = intercept[BadRecordException] {
+      parser.parseIterator[InputStream](
+        input,
+        CreateJacksonParser.inputStream(_: JsonFactory, _: InputStream),
+        stream => UTF8String.fromBytes(stream.readAllBytes()))
+    }
+
+    assert(closed)
+    assert(!error.recoverable)
+  }
+
+  gridTest("multiline top level JSON array stops retaining a closed parser")(
+      Seq("" -> 0, "[]" -> 0, """[{"a":1},{"a":2}]""" -> 2, """[{"a":1}""" -> 
1)) {
+      case (document, expectedRows) =>
+    val schema = StructType(Seq(StructField("a", IntegerType)))
+    val options = new JSONOptions(Map("multiLine" -> "true"), 
SQLConf.get.sessionLocalTimeZone)
+    val parser = new JacksonParser(schema, options, allowArrayAsStructs = true)
+    val closes = new AtomicInteger(0)
+    val input = new 
ByteArrayInputStream(document.getBytes(StandardCharsets.UTF_8))
+    val context = TaskContext.empty()
+    TaskContext.setTaskContext(context)
+    try {
+      val rows = parser.parseIterator[InputStream](
+        input,
+        (factory: JsonFactory, stream: InputStream) =>
+          new JsonParserDelegate(CreateJacksonParser.inputStream(factory, 
stream)) {
+            override def close(): Unit = {
+              closes.incrementAndGet()
+              super.close()
+            }
+          },
+        stream => UTF8String.fromBytes(stream.readAllBytes()))
+      var produced = 0
+      // The truncated document raises once the array runs out; that error is 
asserted elsewhere.
+      try rows.foreach(_ => produced += 1) catch { case _: BadRecordException 
=> }
+
+      assert(produced === expectedRows)
+      assert(closes.get() === 1)
+      context.markTaskCompleted(None)
+      // A second close here would mean the completion listener still held a 
parser that had already
+      // been closed, which is what keeps an archive entry's buffer alive for 
the rest of the task.
+      assert(closes.get() === 1)
+    } finally {
+      TaskContext.unset()
+    }
+  }
+
+  gridTest("multiline top level JSON array survives a parser close failure")(
+      Seq("exhausted", "abandoned")) { consumption =>
+    val schema = StructType(Seq(StructField("a", IntegerType)))
+    val options = new JSONOptions(Map("multiLine" -> "true"), 
SQLConf.get.sessionLocalTimeZone)
+    val parser = new JacksonParser(schema, options, allowArrayAsStructs = true)
+    val closes = new AtomicInteger(0)
+    val input = new 
ByteArrayInputStream("""[{"a":1},{"a":2}]""".getBytes(StandardCharsets.UTF_8))
+    val context = TaskContext.empty()
+    TaskContext.setTaskContext(context)
+    try {
+      val rows = parser.parseIterator[InputStream](
+        input,
+        (factory: JsonFactory, stream: InputStream) =>
+          new JsonParserDelegate(CreateJacksonParser.inputStream(factory, 
stream)) {
+            override def close(): Unit = {
+              closes.incrementAndGet()
+              throw new IOException("close failed")
+            }
+          },
+        stream => UTF8String.fromBytes(stream.readAllBytes()))
+      // The array's end closes eagerly, an abandoned iterator closes from the 
completion listener,
+      // and neither may surface the failure: rows already emitted cannot be 
withdrawn.
+      if (consumption == "exhausted") {
+        assert(rows.map(_.getInt(0)).toSeq === Seq(1, 2))
+        assert(closes.get() === 1)
+      } else {
+        assert(rows.next().getInt(0) === 1)
+        assert(closes.get() === 0)
+      }
+      context.markTaskCompleted(None)
+      assert(closes.get() === 1)
+    } finally {
+      TaskContext.unset()
+    }
+  }
+
+  gridTest("multiline top level JSON array closes the parser when a raw read 
fails")(
+      Seq("opening the array" -> 1, "advancing to an element" -> 2)) { case 
(_, failingToken) =>
+    val schema = StructType(Seq(StructField("a", IntegerType)))
+    val options = new JSONOptions(Map("multiLine" -> "true"), 
SQLConf.get.sessionLocalTimeZone)
+    val parser = new JacksonParser(schema, options, allowArrayAsStructs = true)
+    val closes = new AtomicInteger(0)
+    val tokens = new AtomicInteger(0)
+    val input = new 
ByteArrayInputStream("""[{"a":1},{"a":2}]""".getBytes(StandardCharsets.UTF_8))
+    val context = TaskContext.empty()
+    TaskContext.setTaskContext(context)
+    try {
+      // A raw I/O failure is none of the types `handleFailure` names, so it 
stays unwrapped rather
+      // than becoming a malformed record -- but it must still close here 
rather than at task
+      // completion, or a task skipping corrupt files holds one parser and 
stream per failed entry.
+      intercept[IOException] {
+        val rows = parser.parseIterator[InputStream](
+          input,
+          (factory: JsonFactory, stream: InputStream) =>
+            new JsonParserDelegate(CreateJacksonParser.inputStream(factory, 
stream)) {
+              override def nextToken(): JsonToken = {
+                if (tokens.incrementAndGet() == failingToken) throw new 
IOException("read failed")
+                super.nextToken()
+              }
+              override def close(): Unit = {
+                closes.incrementAndGet()
+                super.close()
+              }
+            },
+          stream => UTF8String.fromBytes(stream.readAllBytes()))
+        rows.foreach(_ => ())
+      }
+      assert(closes.get() === 1)
+      context.markTaskCompleted(None)
+      assert(closes.get() === 1)
+    } finally {
+      TaskContext.unset()
+    }
+  }
+
+  test("multiline top level JSON array keeps the parse failure primary when 
the close fails") {
+    val schema = StructType(Seq(StructField("a", IntegerType)))
+    val options = new JSONOptions(
+      Map("multiLine" -> "true", "mode" -> "FAILFAST"), 
SQLConf.get.sessionLocalTimeZone)
+    val parser = new JacksonParser(schema, options, allowArrayAsStructs = true)
+    val closes = new AtomicInteger(0)
+    val input = new 
ByteArrayInputStream("""[{"a":1},42]""".getBytes(StandardCharsets.UTF_8))
+    val context = TaskContext.empty()
+    TaskContext.setTaskContext(context)
+    try {
+      val error = intercept[BadRecordException] {
+        val rows = parser.parseIterator[InputStream](
+          input,
+          (factory: JsonFactory, stream: InputStream) =>
+            new JsonParserDelegate(CreateJacksonParser.inputStream(factory, 
stream)) {
+              override def close(): Unit = {
+                closes.incrementAndGet()
+                throw new IOException("close failed")
+              }
+            },
+          stream => UTF8String.fromBytes(stream.readAllBytes()))
+        rows.foreach(_ => ())
+      }
+      // The record is already lost here, so this close reports instead of 
swallowing -- but only
+      // as a detail of the parse failure, never in place of it.
+      assert(!error.cause.isInstanceOf[IOException])
+      assert(error.cause.getSuppressed.map(_.getMessage).toSeq === Seq("close 
failed"))
+      assert(closes.get() === 1)
+      context.markTaskCompleted(None)
+      assert(closes.get() === 1)
+    } finally {
+      TaskContext.unset()
+    }
+  }
+
+  test("multiline top level JSON array closes the parser on task completion") {
+    val schema = StructType(Seq(StructField("a", IntegerType)))
+    val options = new JSONOptions(Map("multiLine" -> "true"), 
SQLConf.get.sessionLocalTimeZone)
+    val parser = new JacksonParser(schema, options, allowArrayAsStructs = true)
+    var closed = false
+    val input = new ByteArrayInputStream(
+      """[{"a":1},{"a":2},{"a":3}]""".getBytes(StandardCharsets.UTF_8)) {
+      override def close(): Unit = {
+        closed = true
+        super.close()
+      }
+    }
+    val context = TaskContext.empty()
+    TaskContext.setTaskContext(context)
+    try {
+      val rows = parser.parseIterator[InputStream](
+        input,
+        CreateJacksonParser.inputStream(_: JsonFactory, _: InputStream),
+        stream => UTF8String.fromBytes(stream.readAllBytes()))
+      assert(rows.next().getInt(0) === 1)
+      assert(!closed)
+      context.markTaskCompleted(None)
+      assert(closed)
+    } finally {
+      TaskContext.unset()
+    }
+  }
+
+  gridTest("multiline top level JSON array keeps rows emitted before malformed 
input")(
+      Seq("PERMISSIVE", "DROPMALFORMED", "FAILFAST")) { mode =>
+    withSQLConf(SQLConf.JSON_STREAM_MULTILINE_TOP_LEVEL_ARRAY.key -> "true") {
+      withTempPath { file =>
+        val document = """[{"a":1} {"a":2}]"""
+        Files.write(file.toPath, document.getBytes(StandardCharsets.UTF_8))
+        val actualSchema = StructType(Seq(StructField("a", IntegerType)))
+        val schema = StructType(Seq(
+          actualSchema.head,
+          StructField("_corrupt_record", StringType)))
+        val options = new JSONOptions(
+          Map("multiLine" -> "true", "mode" -> mode),
+          SQLConf.get.sessionLocalTimeZone,
+          SQLConf.get.columnNameOfCorruptRecord)
+        val parser = new JacksonParser(actualSchema, options, 
allowArrayAsStructs = true)
+        val partitionedFile = PartitionedFile(
+          InternalRow.empty,
+          SparkPath.fromPathString(file.getCanonicalPath),
+          0,
+          file.length())
+        val rows = MultiLineJsonDataSource.readFile(
+          spark.sessionState.newHadoopConf(), partitionedFile, parser, schema)
+
+        assert(rows.next().getInt(0) === 1)
+        mode match {
+          case "PERMISSIVE" =>
+            val corruptRow = rows.next()
+            assert(corruptRow.isNullAt(0))
+            val corruptRecord = corruptRow.getUTF8String(1)
+            assert(corruptRecord != null, corruptRow.toString)
+            assert(corruptRecord.toString === document)
+            assert(!rows.hasNext)
+          case "DROPMALFORMED" =>
+            assert(!rows.hasNext)
+          case "FAILFAST" =>
+            val error = intercept[SparkException](rows.hasNext)
+            assert(error.getCondition === 
"MALFORMED_RECORD_IN_PARSING.WITHOUT_SUGGESTION")
+        }
+      }
+    }
+  }
+
+  gridTest("multiline top level JSON array reports a truncated array as 
malformed")(
+      Seq("PERMISSIVE", "DROPMALFORMED", "FAILFAST")) { mode =>
+    withSQLConf(SQLConf.JSON_STREAM_MULTILINE_TOP_LEVEL_ARRAY.key -> "true") {
+      withTempPath { file =>
+        // No closing bracket: jackson-core throws JsonEOFException from 
nextToken() while the
+        // array context is open, so this takes the malformed-record arm, not 
the null branch.
+        val document = """[{"a":1}"""
+        Files.write(file.toPath, document.getBytes(StandardCharsets.UTF_8))
+        val actualSchema = StructType(Seq(StructField("a", IntegerType)))
+        val schema = StructType(Seq(
+          actualSchema.head,
+          StructField("_corrupt_record", StringType)))
+        val options = new JSONOptions(
+          Map("multiLine" -> "true", "mode" -> mode),
+          SQLConf.get.sessionLocalTimeZone,
+          SQLConf.get.columnNameOfCorruptRecord)
+        val parser = new JacksonParser(actualSchema, options, 
allowArrayAsStructs = true)
+        val partitionedFile = PartitionedFile(
+          InternalRow.empty,
+          SparkPath.fromPathString(file.getCanonicalPath),
+          0,
+          file.length())
+        val rows = MultiLineJsonDataSource.readFile(
+          spark.sessionState.newHadoopConf(), partitionedFile, parser, schema)
+
+        assert(rows.next().getInt(0) === 1)
+        mode match {
+          case "PERMISSIVE" =>
+            val corruptRow = rows.next()
+            assert(corruptRow.isNullAt(0))
+            assert(corruptRow.getUTF8String(1).toString === document)
+            assert(!rows.hasNext)
+          case "DROPMALFORMED" =>
+            assert(!rows.hasNext)
+          case "FAILFAST" =>
+            val error = intercept[SparkException](rows.hasNext)
+            assert(error.getCondition === 
"MALFORMED_RECORD_IN_PARSING.WITHOUT_SUGGESTION")
+        }
+      }
+    }
+  }
+
+  gridTest("multiline top level JSON array parse modes with a malformed 
element")(
+      for {
+        element <- Seq("partial object", "scalar", "null", "nested array")
+        mode <- Seq("PERMISSIVE", "DROPMALFORMED", "FAILFAST")
+        streaming <- Seq(false, true)
+      } yield (element, mode, streaming)) { case (element, mode, streaming) =>
+    withSQLConf(SQLConf.JSON_STREAM_MULTILINE_TOP_LEVEL_ARRAY.key -> 
streaming.toString) {
+      withTempPath { file =>
+        val malformed = element match {
+          case "partial object" => """{"a":"bad"}"""
+          case "scalar" => "42"
+          case "null" => "null"
+          case "nested array" => "[1,2]"
+        }
+        val document = s"""[{"a":1},$malformed,{"a":2}]"""
+        Files.write(file.toPath, document.getBytes(StandardCharsets.UTF_8))
+        val schema = StructType(Seq(
+          StructField("a", IntegerType),
+          StructField("_corrupt_record", StringType)))
+        val df = spark.read
+          .schema(schema)
+          .option("multiLine", true)
+          .option("mode", mode)
+          .json(file.getCanonicalPath)
+
+        if (mode == "FAILFAST") {
+          val error = intercept[SparkException](df.collect())
+          val malformed = error.getCause.asInstanceOf[SparkException]
+          assert(malformed.getCondition === 
"MALFORMED_RECORD_IN_PARSING.WITHOUT_SUGGESTION")
+          // Wrapping the parse failure twice empties partialResults and hides 
the real cause.
+          assert(!malformed.getCause.isInstanceOf[BadRecordException])
+        } else {
+          val expected = if (streaming) {
+            // An array element is the record wherever the parser is left at 
the element boundary: a
+            // partial object has been consumed through END_OBJECT, and a 
scalar was never entered.
+            // A null element is a scalar too, though it is the converter's 
null result rather than
+            // the converter that fails. A nested value of the wrong shape can 
stop anywhere inside
+            // its element, so that one still ends the document.
+            element match {
+              case "nested array" if mode == "PERMISSIVE" => Seq(Row(1, null), 
Row(null, document))
+              case "nested array" => Seq(Row(1, null))
+              case _ if mode == "PERMISSIVE" =>
+                Seq(Row(1, null), Row(null, document), Row(2, null))
+              case _ => Seq(Row(1, null), Row(2, null))
+            }
+          } else if (mode == "PERMISSIVE") {
+            // The whole document is the record, so this keeps only the rows 
the eager array had
+            // accumulated when it failed, each stamped with the document. 
Only a partial object
+            // accumulates any: the other three shapes abandon the array where 
they fail.
+            if (element == "partial object") {
+              Seq(Row(1, document), Row(null, document), Row(2, document))
+            } else {
+              Seq(Row(null, document))
+            }
+          } else {
+            Seq.empty[Row]
+          }
+          checkAnswer(df, expected)
+        }
+      }
+    }
+  }
+
+  gridTest("multiline top level JSON array streaming option overrides the 
session config")(
+      Seq(false, true)) { streaming =>
+    withSQLConf(SQLConf.JSON_STREAM_MULTILINE_TOP_LEVEL_ARRAY.key -> 
(!streaming).toString) {
+      withTempPath { file =>
+        val document = """[{"a":"bad"},{"a":2}]"""
+        Files.write(file.toPath, document.getBytes(StandardCharsets.UTF_8))
+        val schema = StructType(Seq(
+          StructField("a", IntegerType),
+          StructField("_corrupt_record", StringType)))
+        val df = spark.read
+          .schema(schema)
+          .option("multiLine", true)
+          .option("enableStreamingTopLevelArray", streaming)
+          .json(file.getCanonicalPath)
+
+        val expected = if (streaming) {
+          Seq(Row(null, document), Row(2, null))
+        } else {
+          Seq(Row(null, document), Row(2, document))
+        }
+        checkAnswer(df, expected)
+      }
+    }
+  }
+
+  test("multiline top level JSON array reuses the corrupt record literal") {
+    withSQLConf(SQLConf.JSON_STREAM_MULTILINE_TOP_LEVEL_ARRAY.key -> "true") {
+      withTempPath { file =>
+        val document = """[{"a":"bad"},{"a":"also bad"},{"a":2}]"""
+        Files.write(file.toPath, document.getBytes(StandardCharsets.UTF_8))
+        val actualSchema = StructType(Seq(StructField("a", IntegerType)))
+        val schema = StructType(Seq(
+          actualSchema.head,
+          StructField("_corrupt_record", StringType)))
+        val options = new JSONOptions(
+          Map("multiLine" -> "true", "mode" -> "PERMISSIVE"),
+          SQLConf.get.sessionLocalTimeZone,
+          SQLConf.get.columnNameOfCorruptRecord)
+        val parser = new JacksonParser(actualSchema, options, 
allowArrayAsStructs = true)
+        val partitionedFile = PartitionedFile(
+          InternalRow.empty,
+          SparkPath.fromPathString(file.getCanonicalPath),
+          0,
+          file.length())
+        val rows = MultiLineJsonDataSource.readFile(
+          spark.sessionState.newHadoopConf(), partitionedFile, parser, schema)
+
+        assert(rows.next().getUTF8String(1).toString === document)
+        Files.delete(file.toPath)
+        assert(rows.next().getUTF8String(1).toString === document)
+        assert(rows.next().getInt(0) === 2)
+        assert(!rows.hasNext)
+      }
+    }
+  }
+
+  gridTest("multiline JSON parser creation failures honor parse mode")(
+      Seq("PERMISSIVE", "DROPMALFORMED", "FAILFAST")) { mode =>
+    withSQLConf(SQLConf.JSON_STREAM_MULTILINE_TOP_LEVEL_ARRAY.key -> "true") {
+      withTempPath { file =>
+        Files.write(file.toPath, Array[Byte](0, 0, 0xff.toByte, 0xfe.toByte))
+        val schema = StructType(Seq(
+          StructField("a", IntegerType),
+          StructField("_corrupt_record", StringType)))
+        val df = spark.read
+          .schema(schema)
+          .option("multiLine", true)
+          .option("mode", mode)
+          .json(file.getCanonicalPath)
+
+        mode match {
+          case "PERMISSIVE" =>
+            val rows = df.collect()
+            assert(rows.length === 1)
+            assert(rows.head.isNullAt(0))
+            assert(!rows.head.isNullAt(1))
+          case "DROPMALFORMED" =>
+            checkAnswer(df, Nil)
+          case "FAILFAST" =>
+            val error = intercept[SparkException](df.collect())
+            assert(error.getCause.asInstanceOf[SparkException].getCondition ===
+              "MALFORMED_RECORD_IN_PARSING.WITHOUT_SUGGESTION")
+        }
+      }
+    }
+  }
+
   test("Corrupt records: FAILFAST mode") {
     // `FAILFAST` mode should throw an exception for corrupt records.
     checkError(
@@ -2008,100 +2508,117 @@ abstract class JsonSuite
     }
   }
 
-  test("SPARK-18352: Handle multi-line corrupt documents (PERMISSIVE)") {
-    withTempPath { dir =>
-      val path = dir.getCanonicalPath
-      val corruptRecordCount = additionalCorruptRecords.count().toInt
-      assert(corruptRecordCount === 5)
+  gridTest("SPARK-18352: Handle multi-line corrupt documents (PERMISSIVE)")(
+      Seq(false, true)) { enabled =>
+    withSQLConf(SQLConf.JSON_STREAM_MULTILINE_TOP_LEVEL_ARRAY.key -> 
enabled.toString) {
+      withTempPath { dir =>
+        val path = dir.getCanonicalPath
+        val corruptRecordCount = additionalCorruptRecords.count().toInt
+        assert(corruptRecordCount === 5)
 
-      additionalCorruptRecords
-        .toDF("value")
-        // this is the minimum partition count that avoids hash collisions
-        .repartition(corruptRecordCount * 4, F.hash($"value"))
-        .write
-        .text(path)
+        additionalCorruptRecords
+          .toDF("value")
+          // Each record must land in its own file for multiLine to read one 
document per record.
+          // This count hashes the five records apart; a larger one need not 
-- 21 collides.
+          .repartition(corruptRecordCount * 4, F.hash($"value"))
+          .write
+          .text(path)
+
+        val jsonDF = spark.read.option("multiLine", true).option("mode", 
"PERMISSIVE").json(path)
+        // `[1,2,3]` is a top-level array of scalars. Streaming makes each 
element its own record,
+        // so it reports one corrupt record per element where the whole 
document reports one.
+        assert(jsonDF.count() === (if (enabled) corruptRecordCount + 2 else 
corruptRecordCount))
+        assert(jsonDF.schema === new StructType()
+          .add("_corrupt_record", StringType)
+          .add("dummy", StringType))
+        val counts = jsonDF
+          .join(
+            additionalCorruptRecords.toDF("value"),
+            F.regexp_replace($"_corrupt_record", "(^\\s+|\\s+$)", "") === 
F.trim($"value"),
+            "outer")
+          .agg(
+            F.count($"dummy").as("valid"),
+            F.count($"_corrupt_record").as("corrupt"),
+            F.count("*").as("count"))
+        checkAnswer(counts, if (enabled) Row(1, 6, 8) else Row(1, 4, 6))
+      }
+    }
+  }
 
-      val jsonDF = spark.read.option("multiLine", true).option("mode", 
"PERMISSIVE").json(path)
-      assert(jsonDF.count() === corruptRecordCount)
-      assert(jsonDF.schema === new StructType()
-        .add("_corrupt_record", StringType)
-        .add("dummy", StringType))
-      val counts = jsonDF
-        .join(
-          additionalCorruptRecords.toDF("value"),
-          F.regexp_replace($"_corrupt_record", "(^\\s+|\\s+$)", "") === 
F.trim($"value"),
-          "outer")
-        .agg(
-          F.count($"dummy").as("valid"),
-          F.count($"_corrupt_record").as("corrupt"),
-          F.count("*").as("count"))
-      checkAnswer(counts, Row(1, 4, 6))
-    }
-  }
-
-  test("SPARK-19641: Handle multi-line corrupt documents (DROPMALFORMED)") {
-    withTempPath { dir =>
-      val path = dir.getCanonicalPath
-      val corruptRecordCount = additionalCorruptRecords.count().toInt
-      assert(corruptRecordCount === 5)
+  gridTest("SPARK-19641: Handle multi-line corrupt documents (DROPMALFORMED)")(
+      Seq(false, true)) { enabled =>
+    withSQLConf(SQLConf.JSON_STREAM_MULTILINE_TOP_LEVEL_ARRAY.key -> 
enabled.toString) {
+      withTempPath { dir =>
+        val path = dir.getCanonicalPath
+        val corruptRecordCount = additionalCorruptRecords.count().toInt
+        assert(corruptRecordCount === 5)
 
-      additionalCorruptRecords
-        .toDF("value")
-        // this is the minimum partition count that avoids hash collisions
-        .repartition(corruptRecordCount * 4, F.hash($"value"))
-        .write
-        .text(path)
+        additionalCorruptRecords
+          .toDF("value")
+          // Each record must land in its own file for multiLine to read one 
document per record.
+          // This count hashes the five records apart; a larger one need not 
-- 21 collides.
+          .repartition(corruptRecordCount * 4, F.hash($"value"))
+          .write
+          .text(path)
 
-      val jsonDF = spark.read.option("multiLine", true).option("mode", 
"DROPMALFORMED").json(path)
-      checkAnswer(jsonDF, Seq(Row("test")))
+        val jsonDF = spark.read
+          .option("multiLine", true)
+          .option("mode", "DROPMALFORMED")
+          .json(path)
+        checkAnswer(jsonDF, Seq(Row("test")))
+      }
     }
   }
 
-  test("SPARK-18352: Handle multi-line corrupt documents (FAILFAST)") {
-    withTempPath { dir =>
-      val path = dir.getCanonicalPath
-      val corruptRecordCount = additionalCorruptRecords.count().toInt
-      assert(corruptRecordCount === 5)
+  gridTest("SPARK-18352: Handle multi-line corrupt documents (FAILFAST)")(
+      Seq(false, true)) { enabled =>
+    withSQLConf(SQLConf.JSON_STREAM_MULTILINE_TOP_LEVEL_ARRAY.key -> 
enabled.toString) {
+      withTempPath { dir =>
+        val path = dir.getCanonicalPath
+        val corruptRecordCount = additionalCorruptRecords.count().toInt
+        assert(corruptRecordCount === 5)
 
-      additionalCorruptRecords
-        .toDF("value")
-        // this is the minimum partition count that avoids hash collisions
-        .repartition(corruptRecordCount * 4, F.hash($"value"))
-        .write
-        .text(path)
+        additionalCorruptRecords
+          .toDF("value")
+          // Each record must land in its own file for multiLine to read one 
document per record.
+          // This count hashes the five records apart; a larger one need not 
-- 21 collides.
+          .repartition(corruptRecordCount * 4, F.hash($"value"))
+          .write
+          .text(path)
 
-      val schema = new StructType().add("dummy", StringType)
+        val schema = new StructType().add("dummy", StringType)
 
-      // `FAILFAST` mode should throw an exception for corrupt records.
-      checkErrorMatchPVals(
-        exception = intercept[SparkException] {
+        // `FAILFAST` mode should throw an exception for corrupt records.
+        checkErrorMatchPVals(
+          exception = intercept[SparkException] {
+            spark.read
+              .option("multiLine", true)
+              .option("mode", "FAILFAST")
+              .json(path)
+          },
+          condition = "INVALID_JSON_RECORD_TYPE",
+          parameters = Map("failFastMode" -> "FAILFAST", "invalidType" -> 
"\"STRING\"|\"BIGINT\""))
+
+        val ex = intercept[SparkException] {
           spark.read
             .option("multiLine", true)
             .option("mode", "FAILFAST")
+            .schema(schema)
             .json(path)
-        },
-        condition = "INVALID_JSON_RECORD_TYPE",
-        parameters = Map("failFastMode" -> "FAILFAST", "invalidType" -> 
"\"STRING\"|\"BIGINT\""))
-
-      val ex = intercept[SparkException] {
-        spark.read
-          .option("multiLine", true)
-          .option("mode", "FAILFAST")
-          .schema(schema)
-          .json(path)
-          .collect()
+            .collect()
+        }
+        checkErrorMatchPVals(
+          exception = ex,
+          condition = "FAILED_READ_FILE.NO_HINT",
+          parameters = Map("path" -> s".*$path.*"))
+        checkError(
+          exception = ex.getCause.asInstanceOf[SparkException],
+          condition = "MALFORMED_RECORD_IN_PARSING.WITHOUT_SUGGESTION",
+          parameters = Map(
+            "badRecord" -> "[null]",
+            "failFastMode" -> "FAILFAST")
+        )
       }
-      checkErrorMatchPVals(
-        exception = ex,
-        condition = "FAILED_READ_FILE.NO_HINT",
-        parameters = Map("path" -> s".*$path.*"))
-      checkError(
-        exception = ex.getCause.asInstanceOf[SparkException],
-        condition = "MALFORMED_RECORD_IN_PARSING.WITHOUT_SUGGESTION",
-        parameters = Map(
-          "badRecord" -> "[null]",
-          "failFastMode" -> "FAILFAST")
-      )
     }
   }
 
@@ -2404,27 +2921,30 @@ abstract class JsonSuite
     checkAnswer(jsonDF, Seq(Row("Chris", "Baird")))
   }
 
-  test("SPARK-23723: specified encoding is not matched to actual encoding") {
-    val fileName = "test-data/utf16LE.json"
-    val schema = new StructType().add("firstName", StringType).add("lastName", 
StringType)
-    val inputFile = testFile(fileName)
-    val exception = intercept[SparkException] {
-      spark.read.schema(schema)
-        .option("mode", "FAILFAST")
-        .option("multiline", "true")
-        .options(Map("encoding" -> "UTF-16BE"))
-        .json(inputFile)
-        .count()
+  gridTest("SPARK-23723: specified encoding is not matched to actual 
encoding")(
+      Seq(false, true)) { enabled =>
+    withSQLConf(SQLConf.JSON_STREAM_MULTILINE_TOP_LEVEL_ARRAY.key -> 
enabled.toString) {
+      val fileName = "test-data/utf16LE.json"
+      val schema = new StructType().add("firstName", 
StringType).add("lastName", StringType)
+      val inputFile = testFile(fileName)
+      val exception = intercept[SparkException] {
+        spark.read.schema(schema)
+          .option("mode", "FAILFAST")
+          .option("multiline", "true")
+          .options(Map("encoding" -> "UTF-16BE"))
+          .json(inputFile)
+          .count()
+      }
+      checkErrorMatchPVals(
+        exception = exception,
+        condition = "FAILED_READ_FILE.NO_HINT",
+        parameters = Map("path" -> s".*$fileName.*"))
+      checkError(
+        exception = exception.getCause.asInstanceOf[SparkException],
+        condition = "MALFORMED_RECORD_IN_PARSING.WITHOUT_SUGGESTION",
+        parameters = Map("badRecord" -> "[empty row]", "failFastMode" -> 
"FAILFAST")
+      )
     }
-    checkErrorMatchPVals(
-      exception = exception,
-      condition = "FAILED_READ_FILE.NO_HINT",
-      parameters = Map("path" -> s".*$fileName.*"))
-    checkError(
-      exception = exception.getCause.asInstanceOf[SparkException],
-      condition = "MALFORMED_RECORD_IN_PARSING.WITHOUT_SUGGESTION",
-      parameters = Map("badRecord" -> "[empty row]", "failFastMode" -> 
"FAILFAST")
-    )
   }
 
   def checkEncoding(expectedEncoding: String, pathToJsonFiles: String,
@@ -3908,7 +4428,7 @@ abstract class JsonSuite
   }
 
   test("SPARK-40667: validate JSON Options") {
-    assert(JSONOptions.getAllOptions.size == 33)
+    assert(JSONOptions.getAllOptions.size == 34)
     // Please add validation on any new Json options here
     assert(JSONOptions.isValidOption("samplingRatio"))
     assert(JSONOptions.isValidOption("primitivesAsString"))
@@ -3941,6 +4461,7 @@ abstract class JsonSuite
     assert(JSONOptions.isValidOption("singleVariantColumn"))
     assert(JSONOptions.isValidOption("explodeEmbeddedArray"))
     assert(JSONOptions.isValidOption("useUnsafeRow"))
+    assert(JSONOptions.isValidOption("enableStreamingTopLevelArray"))
     assert(JSONOptions.isValidOption("encoding"))
     assert(JSONOptions.isValidOption("charset"))
     // Please add validation on any new Json options with alternative here


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

Reply via email to