-
Notifications
You must be signed in to change notification settings - Fork 29.4k
[SPARK-59417][SQL] Stream multiline top-level JSON arrays #58704
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
af30314
b6b8da4
7a5f618
a0a668d
94fe058
9c1cb0a
2083ba7
488871b
e51b32a
5999f6b
5072e17
cdde5a7
9ee7165
dd76c16
75561b8
adecfb4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7034,6 +7034,25 @@ object SQLConf { | |
| .booleanConf | ||
| .createWithDefault(true) | ||
|
|
||
| val JSON_STREAM_MULTILINE_TOP_LEVEL_ARRAY = | ||
| buildConf("spark.sql.json.enableStreamingTopLevelArray") | ||
| .internal() | ||
| .doc("When true, multiline JSON reads stream the elements of a top-level array one at a " + | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit (P3): This description makes the setting sound unconditional, but the parser only selects the streaming path when neither singleVariantColumn nor explodeEmbeddedArray is active. With either option, the top-level array is still converted eagerly. Please qualify the description so it states the actual option scope.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. The doc now states the scope: streaming applies only to reads into a struct schema that take top-level arrays as structs, and reads using |
||
| "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; reads using the " + | ||
| "`singleVariantColumn` or `explodeEmbeddedArray` option are never streamed. 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. 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 " + | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Non-blocking (P2): An incompatible scalar element reaches this terminal RuntimeException arm even though its token is already at a safe top-level array-element boundary. FailureSafeParser then replaces the source iterator with only the recovery result, so PERMISSIVE and DROPMALFORMED reads of an input such as [{"a":1},42,{"a":2}] silently lose the final valid row. Please classify this scalar-boundary failure as recoverable while keeping nested object and array mismatches terminal.
Recommended change: Classify only scalar element-type conversion failures that remain at the current top-level element boundary as recoverable in non-FAILFAST modes, and cover scalars before, between, and after valid objects across parse modes and archive/file paths.
Why this works: Inspect the current element token and error category at the array-loop owner. Convert a safe scalar mismatch into a recoverable BadRecordException so FailureSafeParser emits or drops that element and resumes the preserved iterator; keep nested containers and structurally ambiguous failures terminal.
Scope: Complete element-boundary recovery without broadening recovery to undefined Jackson token positions.
Compatibility: Existing partial-object recovery, terminal malformed-document handling, corrupt-record text, and the disabled eager path remain unchanged.
Risks: An overly broad classification could resume from inside a malformed nested container and duplicate or misparse later tokens. An overly narrow classification could continue dropping later valid objects after other safely consumed scalar forms.
Constraints: Resume only when parser position is known to be at a complete top-level element boundary. FAILFAST must still fail immediately. Terminal structural corruption must still close and stop the iterator.
Success: In PERMISSIVE mode, an incompatible scalar produces one recovery row and later valid objects exactly once. In DROPMALFORMED mode, the incompatible scalar is omitted and later valid objects remain. In FAILFAST mode, the first incompatible scalar still raises the established malformed-record error. Nested or structurally ambiguous failures remain terminal and close the parser.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done in dd76c16. Resumability is now decided by where the parser is left rather than by the exception alone.
resumableAftertakes the element's start token: a scalar start makes anyRuntimeExceptionrecoverable, since the parser is still on that scalar's own token; a container start still requiresPartialResultException, which is the only container failure that provably drained toEND_OBJECT.FailFastModeis excluded as before.[{"a":1},42,{"a":2}]now yields(1,null),(null,D),(2,null)under PERMISSIVE and(1,null),(2,null)under DROPMALFORMED.The parse-mode grid is parameterized over the malformed element's shape — a bad object field, a scalar, a nested array — against one document
[{"a":1},<malformed>,{"a":2}], with the nested array as the negative control that has to stay terminal. A classification broad enough to resume from inside a container fails there.Two notes on the framing. First, one consequence reaches beyond the input you cited: an all-scalar array such as
[1,2,3]is malformed in every element against a struct schema, so it now reports one corrupt record per element rather than one per document. The existing SPARK-18352 multiline expectations are updated and made configuration-dependent for that. Object elements already behaved this way, through the partial-result path the eager reader has always had.Second, on "silently lose the final valid row": for that input the disabled path loses more, not less — it yields a single
(null,D)and drops both valid rows, because the scalar failure escapesconvertArraybefore any element is accumulated. So the enabled path was already ahead of the one it replaces. The real defect was that element granularity was promised for every malformed element and delivered only for objects, which is what this fixes.