tdcmeehan opened a new pull request, #59288:
URL: https://github.com/apache/spark/pull/59288

   ### 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 rather than 
a switch to be flipped once.
   
   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 closing; otherwise a completed read leaves a closed 
parser reachable from the task, keeping its buffer alive, which matters where 
one task builds many parsers such as an archive read. Both multiline readers 
also memoize the whole-document corrupt-record literal they hand to the parser, 
so it is built once per read instead of once per malformed element.
   
   ### 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 SPARK-3308 top-level-array coverage; 
configuration-dependent SPARK-18352 multiline corrupt-document expectations; 
the archive suites exercising the same matrix through the pre-buffered 
`readStream` path; and the parser-lifetime coverage over empty, empty-array, 
valid, and truncated documents counting rows and `close()` calls through a 
`JsonParserDelegate`.
   
   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.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


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

Reply via email to