Implement Spark-compatible `parse_json` / `try_parse_json` (and a Spark VARIANT representation)
Los mantenedores suelen responder en 1 día
Nadie ha tomado este issue todavía.
Evaluación
- Dificultad
- 5/5
- Tiempo estimado
- Más de una semana
- Aptitud para principiantes
- 15/100
Línea de trabajo
Start by reading Spark's VariantBuilder.java and VariantUtil.java alongside common/variant/README.md to pin down the byte layout, then check arrow-rs parquet-variant and parquet-variant-compute at the arrow 60 versions for what can be reused for decoding. The new encoder belongs in crates/spark/src/function/variant/, with parse_json/try_parse_json registered as UDFs and the extension set in return_field_from_args. Done means crates/sqllogictest/test_files/variant/parse_json.slt passes on the examples and edge cases listed here, with to_json available to display values.
Escrito por el modelo de indexación a partir del texto del issue.
Descripción
Part of #273 and apache/datafusion-comet#7300.
What the function does
parse_json(jsonStr) parses a JSON string into a VARIANT value. If the input is not valid JSON, it raises an
error. try_parse_json(jsonStr) does the same but returns NULL for malformed input, duplicate keys, and the size
limit. Both take exactly one STRING argument (any collation, including trim collations). SQL NULL input
returns SQL NULL. parse_json is non-nullable for non-null input and try_parse_json is nullable. Both were
added in Spark 4.0.0 (variant_funcs). Spark 3.x has no VARIANT type.
This is the first VARIANT function, so this issue also covers how datafusion-spark represents the type. Every
other variant function (variant_get, is_variant_null, schema_of_variant, schema_of_variant_agg,
to_variant_object) needs that representation.
A Spark VARIANT is two binaries. Spark implements the Parquet VariantEncoding.md spec as of parquet-format
commit 37b6e8b8 (see common/variant/README.md):
- metadata: a header byte (version
1in the low 4 bits, bit 4 =sorted_strings, bits 6-7 =
offset_size - 1), the dictionary size,size + 1offsets, then the UTF-8 key bytes. - value: a header byte whose low 2 bits are the basic type (primitive, short string, object, array) and whose
upper 6 bits are type info, followed by the content.
Examples
SELECT parse_json('{"a":1,"b":0.8}');
-- {"a":1,"b":0.8}
SELECT try_parse_json('{"a":1,');
-- NULL
SELECT to_json(parse_json('[0.0, 1.00, 1.10, 1.23]'));
-- [0,1,1.1,1.23]
SELECT to_json(parse_json('1.0E10'));
-- 1.0E10
SELECT parse_json('{"a": 1, "b": 2, "a": "3"}');
-- error MALFORMED_RECORD_IN_PARSING.WITHOUT_SUGGESTION (cause: VARIANT_DUPLICATE_KEY)
SELECT try_parse_json('{"a": 1, "b": 2, "a": "3"}');
-- NULL
SET spark.sql.variant.allowDuplicateKeys=true;
SELECT to_json(parse_json('{"c": [], "b": 0, "a": null, "a": {"x": 0, "x": 1}, "b": 1, "b": 2, "c": [3]}'));
-- {"a":{"x":1},"b":2,"c":[3]}
SELECT try_parse_json('{1:2}'), try_parse_json('{"a":[a,b,c]}');
-- NULL NULL
These come from the ExpressionDescription, VariantEndToEndSuite and VariantExpressionEvalUtilsSuite.
Edge cases to test
Byte-level expectations are in VariantExpressionEvalUtilsSuite."parseJson positive". Port them as unit tests.
- Integers use the smallest of INT1, INT2, INT4 and INT8:
127is INT1,128is INT2,-32769is INT4 and
2147483648is INT8. An integer outsidei64(for example9223372036854775808) becomes DECIMAL16 with scale
0 if it has at most 38 digits. Otherwise it becomes DOUBLE (1followed by 38 zeros is DOUBLE1E38). - Non-integers become a decimal only if the text contains just
-,.and digits and fits precision/scale 38.
The original scale is kept:1.0is stored as DECIMAL4 with scale 1 and unscaled 10. DECIMAL4 is used when
precision and scale are both <= 9, DECIMAL8 when they are <= 18, and DECIMAL16 otherwise (99999.9999is
DECIMAL4,99999.99999is DECIMAL8). Anything with an exponent (1e10,1E0) is DOUBLE, and so are values
over 38 digits. - Strings of up to 63 UTF-8 bytes use the short-string header. Longer strings use LONG_STR with a 4-byte length.
- Objects:
- Field ids and offsets are sorted by key, but the field data stays in document order. For
{"z": 1, "y": 2, "x": "3"}the ids are2,1,0and the offsets are4,2,0,6. - Keys are compared with Java
String.compareTo, which is UTF-16 code-unit order, not UTF-8 byte order. Test
keys that mix supplementary characters (U+10000 and above) with U+E000-U+FFFF. - Id and offset widths are the minimum that fits, and the size field becomes 4 bytes above 255 fields.
- Field ids and offsets are sorted by key, but the field data stays in document order. For
- The metadata dictionary is in first-seen document order, without duplicates.
sorted_stringsis never set,
and the offset width is the minimum that fits. Spark writes{}and scalars with metadata[0x01, 0, 0]. - Duplicate keys:
spark.sql.variant.allowDuplicateKeysis internal and defaults tofalse. TheVARIANT_DUPLICATE_KEYerror
is wrapped asMALFORMED_RECORD_IN_PARSING.WITHOUT_SUGGESTIONwithbadRecordset to the input and
failFastMode=FAILFAST.- With
true, the last occurrence wins and the dead bytes are compacted.
- Malformed input such as
"",[,+1,1a, single quotes,NaNand leading zeros gives the same
MALFORMED_RECORD_IN_PARSING.WITHOUT_SUGGESTIONerror. Spark uses a default JacksonJsonFactory, so all the
lenient features are off. - Trailing content: Spark calls
nextToken()once and never checks for end of input. By code reading,
'{"a":1} x'may parse successfully. Confirm this against Spark before asserting it. - Size limit: value and metadata are each limited to 128 MiB (16 MiB under
SPARK_TESTING). Going over raises
VARIANT_SIZE_LIMIT("Cannot build variant bigger than 128.0 MiB inparse_json."), andtry_parse_json
returns NULL. Jackson'sStreamReadConstraintsdefaults (string length, number length, nesting depth) can fail
first, with the malformed-record error instead. - Lone UTF-16 surrogates such as
"\uD835"and{"\uDC00":1}are rejected with the malformed-record error when
spark.sql.variant.validateUnicodeInJsonParsingis true (internal, default true). Withfalsethey are
silently replaced by U+FFFD. Valid surrogate pairs are accepted. - Test scalar and column inputs, NULL, the empty string, whitespace-only input, and deep nesting.
Differences across Spark versions
- 3.4 and 3.5 have no VARIANT.
- 4.0.4, 4.1.3 and 4.2.0 share the same
ParseJsonand builder logic. - Only the 4.2.0 tree checked here has the lone-surrogate validation and its config (tagged
.version("4.3.0")).
4.0.4 and 4.1.3 accept lone surrogates and replace them with U+FFFD. - Jackson is 2.18.6 in 4.0 and 2.21.2 in 4.1 and 4.2. That can change the parser constraint limits.
Implementation notes
- Spark:
ParseJsonandParseJsonExpressionBuilder/TryParseJsonExpressionBuilderin
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/variant/variantExpressions.scala.
VariantExpressionEvalUtils.parseJsonis in the same package. The encoder is
common/variant/src/main/java/org/apache/spark/types/variant/VariantBuilder.java, and the constants and
SIZE_LIMITare inVariantUtil.java. - Arrow representation:
- Use the canonical Arrow extension type
arrow.parquet.variant, which arrow-rs implements as
parquet_variant_compute::VariantType. It is aStructwithmetadataandvaluebinary children (Binary,
LargeBinaryorBinaryView), plus an optionaltyped_valuechild when shredded. - Spark's own
ArrowUtilsmapsVariantTypetoStruct<value: Binary not null, metadata: Binary not null>.
Themetadatachild carries field metadatavariant=trueand there is no extension name. Engines that
exchange Arrow with Spark have to translate between the two. - The UDF sets the extension in
return_field_from_args.
- Use the canonical Arrow extension type
- arrow-rs has
parquet-variant,parquet-variant-computeandparquet-variant-json, but its encoding is not
byte-identical to Spark's. The local arrow-rs is 58.2. datafusion-spark is on arrow 60, so use the matching
crate versions.parquet-variant-json::append_jsonusesserde_json. Non-integers and integers outsidei64become DOUBLE,
never decimal.- Duplicate keys silently keep the last value.
- Keys are visited in
serde_json::Maporder, which is sorted unless thepreserve_orderfeature is unified in.
That changes the dictionary order. serde_jsonstops at nesting depth 128 and rejects trailing content.WritableMetadataBuildersetssorted_stringswhen keys happen to arrive in sorted order, including any
single-key dictionary. Spark never sets it.ObjectBuilder::finishsorts by Ruststrbyte order, not UTF-16 order.- When duplicates are allowed, the old bytes are left in place, so sizes differ.
- I found no size limit in the builder.
- Spark reads any valid encoding (
VariantExpressionSuite."to_json valid input"), so readers only need logical
equivalence. Field order is the exception. Spark'sschema_of_variantvalidates sorting with UTF-16 compare
and throwsMALFORMED_VARIANT, andgetFieldByKeybinary-searches objects with 32 or more fields. A writer
that sorts by UTF-8 bytes can therefore produce values that Spark rejects. The size limit also depends on
Spark's exact byte layout. - Recommendation:
- Reuse
parquet_variant::Variantfor decoding. - Write a Spark-specific JSON-to-variant encoder in a new
crates/spark/src/function/variant/module, or a
configuredVariantBuilderwith Spark's number classification, document-order dictionary, UTF-16 key
ordering, duplicate handling, clearedsorted_stringsand the 128 MiB limit. - Use a JSON tokenizer that exposes the raw number text.
- Reuse
- DataFusion core has no VARIANT support or
parse_json. - apache/datafusion-comet#5438 tracks native Variant support in Comet.
- Add
crates/sqllogictest/test_files/variant/parse_json.slt. It will needto_jsonor a cast to string to
display values. Document both names with#[user_doc].
This issue was generated by an LLM (Claude) from the Apache Spark source code and tests. Examples come from Spark's ExpressionDescription examples, test suites and .sql.out golden files, but please verify them against Spark before relying on them.
- Lenguaje dominante
- Rust
- Estrellas
- 7
- Forks
- 15
- Merge medio
- 16 h 43 min
- PR fusionados (30 d)
- 26
Preparar el entorno
- Sin Dockerfile ni archivo de Docker Compose
- Sin plantilla de pull request
- Leer la guía de contribución
Primeros pasos
- Lee el issue completo y luego la guía de contribución del proyecto.
- Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
- Haz un fork del repositorio y trabaja en una rama.
- Abre un pull request que haga referencia al número del issue.
Más de apache/datafusion-spark
-
enhancement
Dificultad 2/5 1-3 horas Aptitud para principiantes 70/100
apache/datafusion-spark#287 ·
Los mantenedores suelen responder en 1 día
-
enhancement good first issue
Dificultad 2/5 1-3 horas Aptitud para principiantes 80/100
apache/datafusion-spark#266 ·
Los mantenedores suelen responder en 1 día
-
[Bug] make_dt_interval returns NULL for more than about 1.49 million days, Spark returns the intervalPosiblemente ocupada @Subodh-17 la tomó hoy. Abierto
Dificultad 2/5 1-3 horas Aptitud para principiantes 78/100
apache/datafusion-spark#109 ·
Los mantenedores suelen responder en 1 día
-
enhancement
Dificultad 5/5 Más de una semana Aptitud para principiantes 18/100
apache/datafusion-spark#299 ·
Los mantenedores suelen responder en 1 día
-
enhancement
Dificultad 4/5 3-5 días Aptitud para principiantes 35/100
apache/datafusion-spark#298 ·
Los mantenedores suelen responder en 1 día
Todos los issues de apache/datafusion-spark
Issues similares
-
comp-rbac
Dificultad 2/5 1-3 horas Aptitud para principiantes 68/100
ClickHouse/ClickHouse#131875 ·
Los mantenedores suelen responder en 1 día
-
bug(python): read_transaction raises TypeError on a Rewrite with rewritten indicesPosiblemente ocupada @Borg93 la tomó hoy. Abiertobug
Dificultad 2/5 1-3 horas Aptitud para principiantes 75/100
lance-format/lance#11117 ·
Los mantenedores suelen responder en 2 días
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 70/100
storytold/photocraft#15232 · 1 comentario ·
Los mantenedores suelen responder en 1 día
-
forge: fork preflight reads still send `requireCanonical: false`, which Rootstock rejects (-32603)Posiblemente ocupada @Ynyesto la tomó hoy. Abierto
Dificultad 2/5 1-3 horas Aptitud para principiantes 72/100
foundry-rs/foundry#18485 ·
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 70/100
bondybondbond/rushcut#238 · 1 comentario ·