Add support for output MapType[StringType, ArrayType[StringType]] in from_json SQL function [databricks]#15134
Conversation
--------- Signed-off-by: Nghia Truong <nghiat@nvidia.com>
There was a problem hiding this comment.
Pull request overview
Adds GPU support for from_json when the target schema is MAP<STRING,ARRAY<STRING>>, extending the existing MAP<STRING,STRING> path. The implementation uses a “raw extraction” approach (no JSON unescaping/normalization) and documents known CPU/GPU divergences.
Changes:
- Extend
GpuJsonToStructsto extractMAP<STRING,ARRAY<STRING>>viaJSONUtils.extractRawMapFromJsonString(..., MapValueType.ARRAY_OF_STRING). - Update
JsonToStructsGPU override type support/messaging to includeMAP<STRING,ARRAY<STRING>>. - Add integration tests (including corner cases + non-strict xfail probes) and update docs to describe the new supported map schema and its divergences.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| sql-plugin/src/main/scala/org/apache/spark/sql/rapids/GpuJsonToStructs.scala | Adds map-value-type dispatch for MAP<STRING,ARRAY<STRING>> vs MAP<STRING,STRING> raw extraction. |
| sql-plugin/src/main/scala/com/nvidia/spark/rapids/GpuOverrides.scala | Expands from_json type signature + tagging logic and updates fallback message for the new map schema. |
| integration_tests/src/main/python/json_test.py | Adds coverage for MAP<STRING,ARRAY<STRING>> including fallback + corner/xfail probes. |
| docs/supported_ops.md | Updates the supported-ops note for from_json MAP output to include ARRAY<STRING> values. |
| docs/compatibility.md | Documents support for MAP<STRING,ARRAY<STRING>> and clarifies the “raw extraction” divergences. |
Greptile SummaryThis PR extends the GPU
Confidence Score: 5/5Safe to merge — the change is a well-bounded feature addition that follows all established patterns in the codebase and is guarded at both the TypeSig and tagExprForGpu layers. The new MAP<STRING, ARRAY> dispatch arm mirrors the existing MAP<STRING, STRING> path exactly, using the same JNI call site with a new MapValueType argument. Known CPU/GPU divergences (escape handling, nested element re-serialization, numeric token rendering) are fully documented in docs/compatibility.md and covered by dedicated non-strict xfail probes. The defensive throw for unrecognised map value types closes the previously noted silent fall-through. Tests span random data, fixed-literal corner cases, and fallback verification for unsupported element types. No files require special attention. All changed files are clean. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A["from_json(col, schema)"] --> B{TypeSig check in GpuOverrides}
B -->|"MAP nested type not in STRING + ARRAY<STRING>"| C[CPU fallback]
B -->|"Passes TypeSig"| D{tagExprForGpu schema match}
D -->|"MapType(StringType, StringType, _)"| E[GPU: GpuJsonToStructs]
D -->|"MapType(StringType, ArrayType(StringType, _), _)"| E
D -->|"StructType (+ datetime/dup-key checks)"| E
D -->|"anything else"| F["willNotWorkOnGpu → CPU fallback"]
E --> G{doColumnar schema match}
G -->|"MapType(StringType, ArrayType(StringType, _), _)"| H["extractRawMapFromJsonString(MapValueType.ARRAY_OF_STRING)"]
G -->|"MapType(StringType, StringType, _)"| I["extractRawMapFromJsonString(MapValueType.STRING)"]
G -->|"MapType(_, other, _)"| J["throw IllegalArgumentException (defensive guard)"]
G -->|"StructType"| K["fromJSONToStructs + optional datetime convert"]
H --> L["cudf.ColumnVector MAP<STRING, LIST<STRING>>"]
I --> M["cudf.ColumnVector MAP<STRING, STRING>"]
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
A["from_json(col, schema)"] --> B{TypeSig check in GpuOverrides}
B -->|"MAP nested type not in STRING + ARRAY<STRING>"| C[CPU fallback]
B -->|"Passes TypeSig"| D{tagExprForGpu schema match}
D -->|"MapType(StringType, StringType, _)"| E[GPU: GpuJsonToStructs]
D -->|"MapType(StringType, ArrayType(StringType, _), _)"| E
D -->|"StructType (+ datetime/dup-key checks)"| E
D -->|"anything else"| F["willNotWorkOnGpu → CPU fallback"]
E --> G{doColumnar schema match}
G -->|"MapType(StringType, ArrayType(StringType, _), _)"| H["extractRawMapFromJsonString(MapValueType.ARRAY_OF_STRING)"]
G -->|"MapType(StringType, StringType, _)"| I["extractRawMapFromJsonString(MapValueType.STRING)"]
G -->|"MapType(_, other, _)"| J["throw IllegalArgumentException (defensive guard)"]
G -->|"StructType"| K["fromJSONToStructs + optional datetime convert"]
H --> L["cudf.ColumnVector MAP<STRING, LIST<STRING>>"]
I --> M["cudf.ColumnVector MAP<STRING, STRING>"]
Reviews (10): Last reviewed commit: "Merge branch 'main' into from-json-array" | Re-trigger Greptile |
Make the GpuJsonToStructs map dispatch explicit: match MAP<STRING,STRING> directly and throw on any other (currently unreachable) map value type instead of silently extracting it as STRING, so widening the GpuOverrides map gate later fails loudly rather than producing wrong results. Compare the array-valued-map equality test result via map_entries for deterministic ordering, parametrize the array fallback test over ARRAY<INT> and ARRAY<DOUBLE>, and rewrap the GpuJsonToStructs and GpuOverrides comments to the 100-column limit. --------- Signed-off-by: Nghia Truong <nghiat@nvidia.com>
# Conflicts: # sql-plugin/src/main/scala/org/apache/spark/sql/rapids/GpuJsonToStructs.scala
|
regarding the checklist at the top, what is the performance story? testing not required I assume? |
amahussein
left a comment
There was a problem hiding this comment.
Thanks @ttnghia
The schema gating and runtime dispatch look sound, and the null/empty matrix is strong: SQL-null input, JSON-null root, empty map/list/key, null map value, null element, and empty-string element are covered.
My blocking concern is the untested numeric normalization category described inline; the current “only two divergences” claim is not supported by the raw JNI behavior. Please add those cases/update the compatibility contract and run the final build with [databricks].
- Please append
[databricks]to the title before the final build. These common Scala/Python paths do not auto-trigger Databricks CI, while correctness here depends on matching the runtime CPUfrom_jsonbehavior and the PR already identifies version-sensitive duplicate-key behavior. - Performance checklist: Please link those results here and explicitly mark performance testing as covered by NVIDIA/cudf-spark-jni#4741 /not required in this wrapper PR. I do not think a second plugin benchmark is necessary: This PR is dispatch wiring and JNI contains the kernel benchmarks, including array length, null density, mismatch density, and scalar-path regression results.
- NVIDIA/cudf-spark-jni#4794 is not compile-required, but because it fixes the shared
concat_jsonpath, I recommend consuming that fix before the final #15134 validation. CC: @nartal1
|
I've implemented a benchmark for this: Scripts: |
Merged NVIDIA/cudf-spark-jni#4794. |
can we make that part of the PR description to get the visibility? Usually, no one scroll to read comments after the PR is merged. |
MapType[StringType, ArrayType[StringType]] in from_json SQL functionMapType[StringType, ArrayType[StringType]] in from_json SQL function [databricks]
The GPU `from_json` `MAP<STRING,ARRAY<STRING>>` path raw-extracts array element bytes (surrounding quotes stripped, not unescaped or re-serialized), so number tokens can diverge from Spark, which re-renders numbers: non-canonical numeric spellings are kept verbatim (`007`, `1.00000`, `1e2`) where Spark emits `7`, `1.0`, `100.0`, and `NaN`/`Infinity` stay bare under `allowNonNumericNumbers` while Spark quotes them. Document this third corner case in `docs/compatibility.md` and the `GpuJsonToStructs` class doc, and note that canonical tokens (`1`, `1.5`, `true`) already match. Add xfail probes `test_from_json_map_with_arrays_numeric_xfail` and `test_from_json_map_with_arrays_nonnumeric_xfail` pinning the divergence, an options-propagation test `test_from_json_map_with_arrays_options` proving `allowSingleQuotes` and `allowUnquotedControlChars` reach the `ARRAY<STRING>` dispatch, and the canonical decimal `1.5` to the corner-case matches. --------- Signed-off-by: Nghia Truong <nghiat@nvidia.com>
|
build |
@amahussein what is the reason this needs to be tested on Databrick? There is no specific rules/optimization that is changed on Databrick. |
You're right that this PR does not change a Databricks-specific shim or optimization. |
amahussein
left a comment
There was a problem hiding this comment.
The original numeric coverage/documentation, option-plumbing, performance, null/empty, and JNI dependency concerns are now addressed. JNI #4794 also merged with a raw-map-array embedded-NUL test. I found no remaining Scala implementation bug. Before I approve, I would like the new raw-token documentation/xfails qualified for Spark 4 exact-string parsing, plus either a successful Databricks rerun or an explicit waiver for the unrelated spark-protobuf setup failure. The request to link xfails to a tracking issue is a reasonable non-blocking cleanup.
Spark 4.0 introduced `spark.sql.json.enableExactStringParsing` (default `true`), which makes `JacksonParser` return the raw source bytes of non-string JSON tokens for a `StringType` target instead of re-serializing them via `copyCurrentStructure`. The GPU `from_json` raw-map path already emits those raw tokens, so on Spark 4.0+ the CPU matches the GPU for the numeric, non-numeric, and nested-element cases that diverge on Spark 3.5. The previous unconditional non-strict `xfail` probes therefore silently XPASS on Spark 4.0 and give no real coverage there. Gate `test_from_json_map_with_arrays_nested_elements_xfail`, `_numeric_xfail`, and `_nonnumeric_xfail` on `condition=is_before_spark_400()` so Spark 4.0+ runs a hard-equality assertion while Spark 3.5 keeps the tolerant probe. The escape-sequence probe stays unconditional because escaped string elements are `VALUE_STRING` tokens, unescaped via `getText` on every Spark version, so `enableExactStringParsing` does not affect them. Duplicate-key handling is left unchanged as it is orthogonal (`mapKeyDedupPolicy`/version-sensitive). Qualify the `from_json` divergence contract in `docs/compatibility.md` and the `GpuJsonToStructs` Scaladoc to scope the nested and numeric cases to Spark before 4.0, and link the raw-extraction tracking issue NVIDIA#15240 in the affected `xfail` reasons. --------- Signed-off-by: Nghia Truong <nghiat@nvidia.com>
|
build |
|
build |
MapType[StringType, ArrayType[StringType]] in from_json SQL function [databricks]MapType[StringType, ArrayType[StringType]] in from_json SQL function
|
Remove databricks from CICD as it is currently broken. |
|
build |
What is the failure from Databricks and is it caused by the changes here? |
I checked blossom and see that there is not a single databricks build could success recently. Will try databricks again in a bit after the json fix from jni built into snapshot. |
This does not mean that the databricks is the broken one. |
MapType[StringType, ArrayType[StringType]] in from_json SQL functionMapType[StringType, ArrayType[StringType]] in from_json SQL function [databricks]
|
build |
The Spark 4.0.0 CI newly exercised the from_json MAP<STRING,ARRAY<STRING>> raw-map xfails and three failed: the `nested_elements`, `numeric`, and `nonnumeric` probes were gated `condition=is_before_spark_400()` on the belief that the Spark 4.0.0 `enableExactStringParsing` option (default `true`) returns the raw source bytes for non-string tokens, matching the GPU. That belief is wrong: `from_json` on a string column parses via a Reader (the Spark `CreateJacksonParser.utf8String` helper), and the exact-parsing raw-bytes fast path in `JacksonParser` fires only for `Array[Byte]` / `PositionedReadable` (file/binary) sources, never a Reader, so the CPU always re-serializes non-string tokens via `copyCurrentStructure` on every Spark version, identical to 3.5. The nested-object, numeric, and non-numeric element divergences from the raw GPU substrings therefore persist on Spark 4.0.0 too. Drop the `is_before_spark_400()` condition from the three xfails so they xfail on all versions, and repurpose `exact_string_parsing_off_xfail` from a claim that the option reintroduces the divergence to a regression guard that the option is a no-op for from_json on a string column. Correct the same wrong 4.0.0 claim in the `GpuJsonToStructs` class doc and `docs/compatibility.md`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Signed-off-by: Nghia Truong <nghiat@nvidia.com>
|
build |
The `enableExactStringParsing` doc lines added in `cb99e73` exceeded the 100-column scalastyle limit (`file.line.length`) at lines 51-52. Reflow the paragraph to fit; no wording change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Signed-off-by: Nghia Truong <nghiat@nvidia.com>
|
build |
|
Now added databricks back to the tests after tried without databrick. Now confirm that the previous failure was not due to databricks specific. |
|
build |
|
CICD failed due to unrelated issue: |
I don't think this is the root issue. My guess is that this one part of the job that got killed due to a previous failure. |
|
build |
1 similar comment
|
build |
amahussein
left a comment
There was a problem hiding this comment.
Thanks @ttnghia -- The implementation and test concerns are addressed, and I verified that your Spark 4 enableExactStringParsing explanation is correct.
LGTM
This adds support for output type
MapType[StringType, ArrayType[StringType]]infrom_jsonSQL function. Previously, it can only outputMapType[StringType, StringType].Depends on:
MAP<STRING, ARRAY<STRING>>when parsing JSON usingextractRawMapFromJsonStringcudf-spark-jni#4741Benchmark
from_json→MAP<STRING,ARRAY<STRING>>, GPU vs CPU (2M rows, short regime of 1–4 element arrays, isolated method, 16 partitions,concurrentGpuTasks=4, UTC session — required for the GPUfrom_jsonpath):j_narrowj_wideThe kernel-level microbenchmarks (array length, null density, mismatch density, and scalar-path regression) live in the dependency PR NVIDIA/cudf-spark-jni#4741; this table is the plugin-level end-to-end comparison, so a second plugin benchmark is not required in this wrapper PR.
Checklists
Documentation
Testing
(Please provide the names of the existing tests in the PR description.)
Performance