cloud-fan commented on code in PR #59453:
URL: https://github.com/apache/spark/pull/59453#discussion_r4269042385
##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/OptimizeCsvJsonExprs.scala:
##########
@@ -472,6 +478,15 @@ object OptimizeCsvJsonExprs extends Rule[LogicalPlan] {
g.copy(child = j.copy(schema = prunedSchema), ordinal = 0, numFields = 1)
}
+ /**
+ * True when `dt` has a MAP whose key type is CHAR or VARCHAR at any nesting.
+ * Only keys: JacksonParser length-checks CHAR/VARCHAR map keys, not map
values.
Review Comment:
**Nit (P3):** The premise here is not accurate. JacksonParser does check
CHAR/VARCHAR map values: its `StringType` converter ends with
`CharVarcharUtils.applyTextParseSemantics` (JacksonParser.scala:379). That pads
CHAR values and raises `EXCEED_LIMIT_LENGTH` for map values too, independent of
`charVarcharStandardSemantics`; the existing `SPARK-59274: JSON map value
overflow keeps EXCEED_LIMIT_LENGTH` test asserts it. Keys differ because the
SPARK-60108 key check (`makeMapKeyChecker`) is gated on the captured flag and
never pads or trims. That is the check this guard exists to preserve. Suggest
stating the actual boundary:
```suggestion
* Only keys: this preserves the SPARK-60108 map-key check, which runs
only under the captured
* standard semantics and never pads or trims. CHAR/VARCHAR values get
pad/length semantics from
* applyTextParseSemantics regardless of that flag and are outside this
guard.
```
**Verification:**
- **Inspection:** The guard documentation matches JacksonParser's key check
(flag-gated, no pad or trim) and its unconditional value semantics.
##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/json/JacksonParser.scala:
##########
@@ -765,6 +771,9 @@ class JacksonParser(
case err: PartialValueException if enablePartialResults =>
badRecordException = badRecordException.orElse(Some(err.cause))
values += err.partialResult
+ case NonFatal(e) if !enablePartialResults =>
Review Comment:
**Nit (P3):** For a top-level JSON array, `makeStructRootConverter` calls
`convertArray(..., isRoot = true, arrayAsStructs = true)`. With partial results
off, a failing element now drains the rest of the root array before rethrowing
the same exception; for a multiLine file that is the rest of the file.
`parse()` (via `tryWithResource`) and the non-streaming `parseIterator` branch
(via `fail()`) then close the parser without reading it. The drain has no
consumer, and the bad-record outcome is unchanged. The mirrored `convertObject`
arm never runs at the root, because the earlier `isRoot ||
enablePartialResults` arm handles it, and the comment below says this arm is
for nested arrays. Suggest excluding the root:
```suggestion
case NonFatal(e) if !isRoot && !enablePartialResults =>
```
**Verification:**
- **Compatibility:** A multiLine top-level array whose early element fails,
with partial results off, still produces the same bad-record outcome in
PERMISSIVE and FAILFAST.
- **Inspection:** The convertArray drain arm excludes the root case,
consistent with its comment and with the convertObject arm.
##########
sql/core/src/test/scala/org/apache/spark/sql/CharVarcharTestSuite.scala:
##########
@@ -3543,6 +3543,81 @@ class BasicCharVarcharTestSuite extends
SharedSparkSession {
}
}
+ test("SPARK-60229: from_json(to_json) keeps CHAR/VARCHAR map key check") {
+ Seq(
+ ("CHAR(3)", "a", "t_spark_60229_char"),
+ ("VARCHAR(3)", "abcd", "t_spark_60229_varchar")
+ ).foreach { case (keyType, badKey, viewName) =>
+ withView(viewName) {
+ withSQLConf(
+ SQLConf.CHAR_VARCHAR_STANDARD_SEMANTICS.key -> "false",
+ SQLConf.PRESERVE_CHAR_VARCHAR_TYPE_INFO.key -> "true") {
+ sql(s"""CREATE TEMPORARY VIEW $viewName AS
+ |SELECT from_json('{"$badKey":1}', 'MAP<$keyType, INT>') AS
m""".stripMargin)
+ }
+ withSQLConf(SQLConf.CHAR_VARCHAR_STANDARD_SEMANTICS.key -> "true") {
+ checkAnswer(
+ sql(s"SELECT from_json(to_json(m), 'MAP<$keyType, INT>') FROM
$viewName"),
+ Row(null))
+ assertUnsupportedJsonMapKey(
+ s"""SELECT from_json(to_json(m), 'MAP<$keyType, INT>', map('mode',
'FAILFAST'))
+ |FROM $viewName""".stripMargin,
+ key = badKey,
+ dataTypeSql = keyType)
+ }
+ }
+ }
+ }
+
+ test("SPARK-60229: nested struct map key rejection does not steal sibling
fields") {
+ withSQLConf(SQLConf.CHAR_VARCHAR_STANDARD_SEMANTICS.key -> "true") {
+ val schema = "s STRUCT<m MAP<CHAR(3), INT>, tail INT>, tail INT"
+ // Inner tail after the map: without draining s, the parent can take
inner tail=5
+ // and then hit s's END_OBJECT, never seeing outer tail=1.
+ val jsonAfter = """{"s":{"m":{"a":1},"tail":5},"tail":1}"""
+ // Inner tail after an already-parsed outer tail: without draining s,
inner
+ // tail=5 overwrites the sibling that was already 1.
+ val jsonBefore = """{"tail":1,"s":{"m":{"a":1},"tail":5}}"""
+ Seq(true, false).foreach { partial =>
+ withSQLConf(SQLConf.JSON_ENABLE_PARTIAL_RESULTS.key ->
partial.toString) {
+ val expected = if (partial) {
+ Row(Row(Row(null, 5), 1))
+ } else {
+ Row(Row(null, 1))
+ }
+ Seq(jsonAfter, jsonBefore).foreach { json =>
+ checkAnswer(sql(s"SELECT from_json('$json', '$schema')"), expected)
+ assertUnsupportedJsonMapKey(
+ s"""SELECT from_json('$json', '$schema', map('mode',
'FAILFAST'))""",
+ key = "a",
+ dataTypeSql = "CHAR(3)")
+ }
+ }
+ }
+
+ // Array element failure must drain to END_ARRAY so an inner field name
cannot
Review Comment:
**Nit (P3):** With partial results off, this case does not depend on the
`convertArray` arm. The element struct's own `convertObject` arm drains through
the element's `END_OBJECT` before `convertArray` sees the error. Without the
`convertArray` arm, the root's `isRoot` arm resumes on that `END_OBJECT`. The
next token is `END_ARRAY`, whose `currentName` is `arr`, so the `arr` converter
fails conversion and the error is recorded first-error-wins. The root then
parses `after=2`. The result is still `Row(1, null, 2)` with the same FAILFAST
map-key error, so nothing fails if the `convertArray` arm is removed or broken.
Suggest rewording the comment to describe what is asserted: a failing array
element does not leak inner field names to the parent, and later siblings are
still parsed. Drop the `convertArray` attribution.
**Verification:**
- **Inspection:** The comment no longer attributes the array case's
protection to the convertArray drain arm.
##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/json/JacksonParser.scala:
##########
@@ -780,6 +789,26 @@ class JacksonParser(
}
}
+ /**
+ * Drain this object or array through `stopOn` and rethrow `cause`.
skipChildren() must run
+ * first: nextUntil(stopOn) can otherwise stop at the failed value's own end
token and leave
+ * parent siblings unconsumed, so an enclosing convertObject would treat
remaining names as
+ * its own. Skip failures are suppressed on `cause` so the original error is
preserved.
+ */
+ private def skipRestAndRethrow(
+ parser: JsonParser, stopOn: JsonToken, cause: Throwable): Nothing = {
+ try {
+ parser.skipChildren()
+ while (nextUntil(parser, stopOn)) {
Review Comment:
**Nit (P3):** This loop stops at the first `stopOn` token after wherever the
child left the parser, so it is correct only when the child failed at its own
depth. `parseVariant` breaks that assumption. `VariantBuilder.buildJson`
recurses into nested objects and throws `VARIANT_DUPLICATE_KEY` from
`finishWritingObject` at an inner `END_OBJECT` (the default is
`spark.sql.variant.allowDuplicateKeys=false`).
Example with `spark.sql.json.enablePartialResults=false`:
```sql
SELECT from_json('{"s":{"v":{"a":{"k":1,"k":2}},"b":7},"b":5}',
's STRUCT<v: VARIANT, b: INT>, b INT')
```
1. The failure leaves the parser on `a`'s `END_OBJECT`.
2. `skipChildren()` is a no-op on an END token, and `nextUntil(END_OBJECT)`
stops at `v`'s `END_OBJECT`, still inside `s`.
3. The root then reads `s.b` as its own `b` and ends at `s`'s `END_OBJECT`,
so the outer `b=5` is never read.
PERMISSIVE returns `{s: null, b: 7}`; before this PR it returned `{s: null,
b: null}`. `MAP<STRING, VARIANT>` hits the same problem through the
`convertMap` arm. The `convertArray` arm is defeated only when the failure sits
inside an array nested in the variant element.
This affects only the internal partial-off mode. The default partial-results
recovery gives the same value for this input, but this PR does not change it.
**Recommended change:** Make the partial-off drain unwind to the failing
container's own boundary. Record the container's nesting depth from the
parser's parsing context when the container starts. On a non-partial child
failure, advance (skipping children) until the parser is back at that depth,
then consume the remaining tokens through the container's END token before
rethrowing. Keep suppressing skip-time failures on the original cause. Add JSON
parsing regression coverage with enablePartialResults=false, in the JSON
functions or CHAR/VARCHAR test families, for VARIANT children whose failure
sits below the variant's top level. Cover a struct field and a map value
failing inside a nested variant object, and an array element failing inside an
array nested in the variant element. The current drain gives a wrong sibling
value for each of these. Also cover an ARRAY<VARIANT> element failing inside a
nested variant object. The current END_ARRAY stop already handles it, but its
result d
epends on convertArray's own drain.
**Why this works:** The stop condition becomes the container's captured
nesting depth instead of the first matching END token. Any contexts a child
leaves open are strictly deeper than the captured depth, so unwinding to it and
then to the container's END token restores alignment wherever the child
stopped. Depth stays meaningful even though Jackson reuses context objects.
**Scope:** Rework the drain helper's stop condition and the way the three
non-partial arms pass it the container boundary, plus deep-failure regression
tests.
**Compatibility:** Partial-results-on recovery and all success-path parsing
remain unchanged.
**Risks:** Jackson recycles child context objects, so comparing context
identity can misidentify a sibling container. Compare nesting depth rather than
context identity. Unwinding after a JsonParseException may throw again and must
stay suppressed on the original cause.
**Constraints:** Leave the enablePartialResults=true recovery paths
unchanged (declared scope). Rethrow the same original cause so FAILFAST still
reports it.
**Success:** With enablePartialResults=false, a nested struct, map, or array
whose child fails at any depth (including a VARIANT duplicate key inside a
nested variant object) leaves the parser on that container's own END token
before rethrowing. from_json('{"s":{"v":{"a":{"k":1,"k":2}},"b":7},"b":5}', 's
STRUCT<v: VARIANT, b: INT>, b INT') returns {s: null, b: 5} in PERMISSIVE with
partial results off, and FAILFAST reports VARIANT_DUPLICATE_KEY. Existing
direct-child failure results in the SPARK-40646, SPARK-44940, SPARK-59274, and
SPARK-60229 tests are unchanged.
**Verification:**
- **Regression:** With partial results off, the outer sibling keeps its own
value and FAILFAST reports the original error when a VARIANT child fails inside
a nested variant object as a struct field or a map value, or inside an array
nested in the variant element as an array element. The current drain gives a
wrong sibling value for each of these. An ARRAY<VARIANT> element failing inside
a nested variant object also keeps the outer sibling, which pins convertArray's
own drain.
- **Compatibility:** Partial-results on and off results for direct-child
nested failures in from_json and the JSON data source are unchanged.
--
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]