This is an automated email from the ASF dual-hosted git repository.
HyukjinKwon pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/spark.git
The following commit(s) were added to refs/heads/master by this push:
new ccc8f69ce6be [SPARK-60103][SQL] Length-check XML CHAR/VARCHAR map keys
without pad or trim
ccc8f69ce6be is described below
commit ccc8f69ce6be5a879c193788c1d12d7ea9073e22
Author: Serge Rielau <[email protected]>
AuthorDate: Sun Oct 11 13:45:01 2026 +0900
[SPARK-60103][SQL] Length-check XML CHAR/VARCHAR map keys without pad or
trim
### What changes were proposed in this pull request?
When `spark.sql.charVarchar.standardSemantics.enabled` is true, `from_xml`
and the XML datasource length-check XML names used as `MAP<CHAR(n), _>` /
`MAP<VARCHAR(n), _>` keys without rewriting them. This matches SPARK-60108 for
JSON.
The checked names are element names, `attributePrefix` plus attribute names
(default `_`), and the `valueTag` (default `_VALUE`) when mixed text is present.
- `CHAR(n)` keys must already be exactly `n` characters (no pad).
- `VARCHAR(n)` keys must already be at most `n` characters (no trim).
- Exact repeated names last-win. `spark.sql.mapKeyDedupPolicy` is not
applied.
- Length failures follow the XML parse mode (`PERMISSIVE` / `FAILFAST`).
Mismatched keys raise a new `UNSUPPORTED_XML_CHAR_VARCHAR_MAP_KEY`
condition (SQLSTATE `0A000`) instead of `EXCEED_LIMIT_LENGTH`, because pad/trim
of map keys is not supported. XML CHAR/VARCHAR values still use the same pad
and `EXCEED_LIMIT_LENGTH` checks as other parsed text. JSON, CSV, and TRANSFORM
are unchanged.
Struct-field maps still go through `convertField`. Empty, attribute-only,
and text-only `MAP<STRING, _>` elements stay SQL NULL or a malformed record, as
on master. `convertMap` is used for non-empty string-family keys (including
CHAR/VARCHAR) and owns element bounding so a key-check failure still drains
`ARRAY<MAP<...>>` siblings.
This PR drops `convertConstrainedMap` and `DuplicateMapKeyUtils`.
SPARK-59722-style assign-after-parse is not used.
JIRA: https://issues.apache.org/jira/browse/SPARK-60103
### Why are the changes needed?
SPARK-59274 padded and trimmed CHAR/VARCHAR XML map keys during the walk,
then applied `mapKeyDedupPolicy` to invented collisions. SPARK-59722 would have
done the same via write-side assignment after a STRING parse; that approach was
abandoned for JSON in favor of SPARK-60108. XML names are the key identity, so
they should be length-checked in place rather than rewritten.
### Does this PR introduce _any_ user-facing change?
Yes, under `spark.sql.charVarchar.standardSemantics.enabled=true`:
```sql
SELECT from_xml('<ROW><m><a>1</a></m></ROW>', 'm MAP<CHAR(2), INT>',
map('mode', 'FAILFAST'));
-- [UNSUPPORTED_XML_CHAR_VARCHAR_MAP_KEY] XML map keys of CHAR or VARCHAR
cannot be padded or trimmed.
-- The key 'a' is not valid for type "CHAR(2)". SQLSTATE: 0A000
```
Exact-width CHAR keys and in-limit VARCHAR keys are kept as-is. Repeated
names last-win. When the standard-semantics flag is false and
`spark.sql.preserveCharVarcharTypeInfo` is true, short CHAR keys are padded and
over-length CHAR or VARCHAR keys raise `EXCEED_LIMIT_LENGTH`.
Documented in `docs/sql-migration-guide.md` (Spark SQL 4.3 to 4.4). In
Spark 4.3, `from_xml` and XML reader schemas rejected CHAR/VARCHAR with
`UNSUPPORTED_CHAR_OR_VARCHAR_AS_STRING`, or replaced them with STRING when
`spark.sql.legacy.charVarcharAsString` was true.
### How was this patch tested?
Added SPARK-60103 coverage in `BasicCharVarcharTestSuite` (too-short CHAR,
exact-width CHAR, CHAR/VARCHAR overflow, last-wins duplicates, prefixed
attributes and `valueTag` in mixed content, PERMISSIVE vs FAILFAST, nested
maps, `ARRAY<MAP>`, collated keys, `from_xml` and the XML datasource,
empty/attribute-only/text-only `MAP<STRING>`, `format("xml")` `MAP<INT>`,
flag-off pad/overflow including over-length CHAR). Also re-ran SPARK-59274 XML
value, sibling, and `rowTag` tests.
Ran:
```
JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64 build/sbt
-Dsbt.override.build.repos=true \
'sql/testOnly org.apache.spark.sql.BasicCharVarcharTestSuite -- -z
SPARK-60103 -z "SPARK-59274: from_json/csv/xml" -z "SPARK-59274: ordinary
STRING" -z "SPARK-59274: collated" -z "SPARK-59274: XML rowTag" -z
"SPARK-59274: mixed XML" -z "SPARK-59274: nested XML"'
```
### Was this patch authored or co-authored using generative AI tooling?
Generated-by: Cursor Grok 4.6
Closes #59316 from srielau/SPARK-60103.
Authored-by: Serge Rielau <[email protected]>
Signed-off-by: Hyukjin Kwon <[email protected]>
---
.../src/main/resources/error/error-conditions.json | 7 +
docs/sql-migration-guide.md | 1 +
.../sql/catalyst/util/DuplicateMapKeyUtils.scala | 82 ----
.../spark/sql/catalyst/xml/StaxXmlParser.scala | 180 ++++-----
.../spark/sql/errors/QueryExecutionErrors.scala | 14 +
.../org/apache/spark/sql/internal/SQLConf.scala | 8 +-
.../apache/spark/sql/CharVarcharTestSuite.scala | 429 ++++++++++++++-------
7 files changed, 376 insertions(+), 345 deletions(-)
diff --git a/common/utils/src/main/resources/error/error-conditions.json
b/common/utils/src/main/resources/error/error-conditions.json
index 94d6b897b18c..7d17404b29a4 100644
--- a/common/utils/src/main/resources/error/error-conditions.json
+++ b/common/utils/src/main/resources/error/error-conditions.json
@@ -9896,6 +9896,13 @@
],
"sqlState" : "0A000"
},
+ "UNSUPPORTED_XML_CHAR_VARCHAR_MAP_KEY" : {
+ "message" : [
+ "XML map keys of CHAR or VARCHAR cannot be padded or trimmed.",
+ "The key <key> is not valid for type <dataType>."
+ ],
+ "sqlState" : "0A000"
+ },
"UNTYPED_SCALA_UDF" : {
"message" : [
"You're using untyped Scala UDF, which does not have the input type
information. Spark may blindly pass null to the Scala closure with
primitive-type argument, and the closure will see the default value of the Java
type for the null argument, e.g. `udf((x: Int) => x, IntegerType)`, the result
is 0 for null input. To get rid of this error, you could:",
diff --git a/docs/sql-migration-guide.md b/docs/sql-migration-guide.md
index f9c7bf477122..c459a028da55 100644
--- a/docs/sql-migration-guide.md
+++ b/docs/sql-migration-guide.md
@@ -29,6 +29,7 @@ license: |
## Upgrading from Spark SQL 4.3 to 4.4
- Since Spark 4.4, when `spark.sql.preserveCharVarcharTypeInfo` is true and
`spark.sql.charVarchar.standardSemantics.enabled` is false, ORC reads that
apply a CHAR/VARCHAR schema over STRING storage return the stored values
without ORC truncation, matching Parquet. Previously the ORC reader requested
`char(n)`/`varchar(n)` and truncated STRING-stored values to `n`. Read-side
length checks (`EXCEED_LIMIT_LENGTH`) apply only when
`spark.sql.charVarchar.standardSemantics.enabled` is true.
+- Since Spark 4.4, when `spark.sql.charVarchar.standardSemantics.enabled` is
true, XML names used as `MAP<CHAR(n), _>` or `MAP<VARCHAR(n), _>` keys in
`from_xml` and the XML datasource are length-checked without padding or
trimming. The checked names are element names, `attributePrefix` plus attribute
names (default prefix `_`), and the `valueTag` (default `_VALUE`) when mixed
text is present. A `CHAR(n)` key must already be exactly `n` characters, and a
`VARCHAR(n)` key must already be [...]
- Since Spark 4.4, the options maps passed to `from_csv`, `to_csv`,
`schema_of_csv`, `from_json`, `to_json`, `schema_of_json`, `from_xml`,
`to_xml`, and `schema_of_xml` must be foldable after replacing
`RuntimeReplaceable` expressions. Previously, Spark evaluated non-foldable
options during analysis, which allowed some constant expressions but could fail
with an internal error or incorrectly evaluate row-dependent expressions. To
allow deterministic and row-independent non-foldable optio [...]
- Since Spark 4.4, when an already-analyzed Data Source V2 query is refreshed
after a compatible schema change, connectors can return more data columns from
the current table schema in `Scan.readSchema()` than requested by
`SupportsPushDownRequiredColumns.pruneColumns`. Previously, this partial
pruning could fail planning because the scan reported columns absent from the
analyzed relation output.
- Since Spark 4.4, when an already-analyzed Data Source V2 query is refreshed,
if rebinding an expanded struct used as a map key causes distinct current keys
to collide under the analyzed schema, the query fails with `DUPLICATED_MAP_KEY`
by default instead of returning duplicate keys. With
`spark.sql.mapKeyDedupPolicy=LAST_WIN`, the last value is kept.
diff --git
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/util/DuplicateMapKeyUtils.scala
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/util/DuplicateMapKeyUtils.scala
deleted file mode 100644
index fd179bc73d05..000000000000
---
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/util/DuplicateMapKeyUtils.scala
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.apache.spark.sql.catalyst.util
-
-import scala.collection.mutable
-
-import org.apache.spark.SparkRuntimeException
-import org.apache.spark.sql.errors.QueryExecutionErrors
-import org.apache.spark.sql.internal.SQLConf
-import org.apache.spark.sql.types.{DataType, StringType}
-import org.apache.spark.unsafe.types.UTF8String
-import org.apache.spark.util.SparkErrorUtils
-
-private[sql] object DuplicateMapKeyUtils {
- def cause(exception: Throwable): Option[SparkRuntimeException] = {
- SparkErrorUtils.getRootCause(exception) match {
- case cause: SparkRuntimeException if cause.getCondition ==
"DUPLICATED_MAP_KEY" =>
- Some(cause)
- case _ => None
- }
- }
-
- def unapply(exception: Throwable): Option[SparkRuntimeException] =
cause(exception)
-
- /**
- * Builds an XML map with a constrained CHAR/VARCHAR key type from a raw-key
last-wins
- * accumulator. Failed values still occupy a slot so normalized collisions
are visible.
- *
- * CHAR/VARCHAR keys: repeated raw XML keys keep the last value, then
- * `spark.sql.mapKeyDedupPolicy` applies to normalized keys.
- *
- * Example: parsing `a` and `a ` as CHAR(2) keys raises
- * DUPLICATED_MAP_KEY under EXCEPTION and keeps `a ` -> 2 under LAST_WIN.
- */
- def buildConstrainedMap(
- lastEntries: mutable.LinkedHashMap[UTF8String, (UTF8String,
Option[Any])],
- keyType: DataType,
- valueType: DataType): MapData = {
- if (SQLConf.get.getConf(SQLConf.MAP_KEY_DEDUP_POLICY) ==
- SQLConf.MapKeyDedupPolicy.EXCEPTION) {
- val distinctKeys = keyType match {
- case stringType: StringType if stringType.supportsBinaryEquality =>
- new java.util.HashSet[Any]()
- case _ =>
- new java.util.TreeSet[Any](TypeUtils.getInterpretedOrdering(keyType))
- }
- val keys = mutable.ArrayBuffer.empty[Any]
- val values = mutable.ArrayBuffer.empty[Any]
- lastEntries.valuesIterator.foreach { case (key, value) =>
- if (!distinctKeys.add(key)) {
- throw QueryExecutionErrors.duplicateMapKeyFoundError(key)
- }
- value.foreach { v =>
- keys += key
- values += v
- }
- }
- ArrayBasedMapData(keys.toArray, values.toArray)
- } else {
- val builder = new ArrayBasedMapBuilder(keyType, valueType)
- lastEntries.valuesIterator.foreach { case (normalizedKey, value) =>
- value.foreach(builder.put(normalizedKey, _))
- }
- builder.build()
- }
- }
-}
diff --git
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/xml/StaxXmlParser.scala
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/xml/StaxXmlParser.scala
index f0923d405436..4f388fdf4933 100644
---
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/xml/StaxXmlParser.scala
+++
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/xml/StaxXmlParser.scala
@@ -26,7 +26,6 @@ import javax.xml.stream.events._
import javax.xml.transform.stream.StreamSource
import javax.xml.validation.Schema
-import scala.collection.mutable
import scala.collection.mutable.ArrayBuffer
import scala.jdk.CollectionConverters._
import scala.util.Try
@@ -42,7 +41,7 @@ import org.apache.spark.{SparkIllegalArgumentException,
SparkUpgradeException}
import org.apache.spark.internal.Logging
import org.apache.spark.sql.catalyst.InternalRow
import org.apache.spark.sql.catalyst.expressions.{ExprUtils,
GenericInternalRow, ToStringBase}
-import org.apache.spark.sql.catalyst.util.{ArrayBasedMapData,
BadRecordException, CharVarcharUtils, DateFormatter, DropMalformedMode,
DuplicateMapKeyUtils, FailureSafeParser, GenericArrayData, MapData, ParseMode,
PartialResultArrayException, PartialResultException, PermissiveMode,
TimeFormatter, TimestampFormatter}
+import org.apache.spark.sql.catalyst.util.{ArrayBasedMapData,
BadRecordException, CharVarcharUtils, DateFormatter, DropMalformedMode,
FailureSafeParser, GenericArrayData, MapData, ParseMode,
PartialResultArrayException, PartialResultException, PermissiveMode,
TimeFormatter, TimestampFormatter}
import org.apache.spark.sql.catalyst.util.LegacyDateFormats.FAST_DATE_FORMAT
import org.apache.spark.sql.catalyst.xml.StaxXmlParser.convertStream
import org.apache.spark.sql.errors.QueryExecutionErrors
@@ -86,6 +85,9 @@ class StaxXmlParser(
private val caseSensitive = SQLConf.get.caseSensitiveAnalysis
+ // CHAR/VARCHAR XML map keys are length-checked without pad or trim under
this flag.
+ private val charVarcharStandardSemantics =
SQLConf.get.charVarcharStandardSemantics
+
/**
* Limits a view of an event stream to one element whose start event has
already been consumed.
* Closing or draining this view consumes the matching end event without
closing the underlying
@@ -233,7 +235,6 @@ class StaxXmlParser(
// ValidatorUtil.newValidator throws this when the JAXP implementation
cannot
// disable external access; that is an environment error, not a bad
record.
case e: UnsupportedOperationException => throw e
- case DuplicateMapKeyUtils(e) => throw e
case e@(_: RuntimeException | _: XMLStreamException | _:
MalformedInputException
| _: SAXException) =>
// XML parser currently doesn't support partial results for corrupted
records.
@@ -330,16 +331,11 @@ class StaxXmlParser(
throw BadRecordException(xmlLiteral, () => Array.empty,
wrappedCharException)
case PartialResultException(row, cause) =>
- DuplicateMapKeyUtils.cause(cause) match {
- case Some(e) => throw e
- case None =>
- throw BadRecordException(record = xmlLiteral, partialResults = ()
=> Array(row), cause)
- }
+ throw BadRecordException(record = xmlLiteral, partialResults = () =>
Array(row), cause)
case PartialResultArrayException(rows, cause) =>
throw BadRecordException(record = xmlLiteral, partialResults = () =>
rows, cause)
case e: Throwable =>
SparkErrorUtils.getRootCause(e) match {
- case DuplicateMapKeyUtils(duplicate) => throw duplicate
case _: FileNotFoundException if options.ignoreMissingFiles =>
logWarning("Skipped missing file", e)
parser.close()
@@ -383,9 +379,10 @@ class StaxXmlParser(
startElementName: String,
attributes: Array[Attribute]): Any = dt match {
case st: StructType => convertObject(parser, st)
- case MapType(StringType, vt, _) => convertMap(parser, vt, attributes)
- case MapType(kt @ (_: CharType | _: VarcharType), vt, _) =>
- convertConstrainedMap(parser, kt, vt, attributes)
+ // CHAR/VARCHAR extend StringType. Non-string keys (for example MAP<INT,
_> through
+ // format("xml").load()) must not reach convertMap:
applyTextParseSemantics would keep
+ // UTF8String keys and fail later with ClassCastException.
+ case MapType(kt: StringType, vt, _) => convertMap(parser, vt,
attributes, kt)
case ArrayType(st, _) => convertField(parser, st, startElementName)
case VariantType =>
StaxXmlParser.convertVariant(parser, attributes, options)
@@ -445,109 +442,80 @@ class StaxXmlParser(
}
/**
- * Parse an object as map.
+ * Parse an object as a Map.
+ *
+ * When `spark.sql.charVarchar.standardSemantics.enabled` is true, XML names
used as
+ * CHAR/VARCHAR keys are length-checked without rewriting: CHAR keys must
already be
+ * exactly n characters, and VARCHAR keys must already be at most n
characters.
+ * Padding, trimming, and mapKeyDedupPolicy are not applied. With the flag
off,
+ * short CHAR keys are padded and over-length CHAR or VARCHAR keys raise
+ * EXCEED_LIMIT_LENGTH when first-class CHAR/VARCHAR types reach the parser.
+ * Repeated names last-win on binary equality (collation is not consulted),
matching
+ * ordinary MAP<STRING, ...> XML maps.
+ *
+ * Example (flag on): `from_xml('<ROW><m><ab>1</ab></m></ROW>',
+ * 'm MAP<CHAR(2), INT>')` keeps key `ab`; `MAP<CHAR(4), INT>` raises
+ * `UNSUPPORTED_XML_CHAR_VARCHAR_MAP_KEY`.
+ *
+ * This method owns element bounding so a key-check failure still consumes
the current
+ * map element (ARRAY<MAP<...>> and nested maps included).
*/
private def convertMap(
parser: XMLEventReader,
valueType: DataType,
- attributes: Array[Attribute]): MapData = {
- val kvPairs = ArrayBuffer.empty[(UTF8String, Any)]
- attributes.foreach { attr =>
- kvPairs += (UTF8String.fromString(options.attributePrefix +
attr.getName.getLocalPart)
- -> convertTo(attr.getValue, valueType))
- }
- var shouldStop = false
- while (!shouldStop) {
- parser.nextEvent match {
- case e: StartElement =>
- val key = StaxXmlParserUtils.getName(e.asStartElement.getName,
options)
- kvPairs +=
- (UTF8String.fromString(key) -> convertField(parser, valueType,
key))
- case c: Characters if !c.isWhiteSpace =>
- // Create a value tag field for it
- kvPairs +=
- // TODO: We don't support array value tags in maps yet.
- (UTF8String.fromString(options.valueTag) -> convertTo(c.getData,
valueType))
- case _: EndElement | _: EndDocument =>
- shouldStop = true
- case _ => // do nothing
+ attributes: Array[Attribute],
+ keyType: DataType): MapData = {
+ val bounded = new ElementBoundedEventReader(parser)
+ try {
+ val kvPairs = ArrayBuffer.empty[(UTF8String, Any)]
+ attributes.foreach { attr =>
+ val key = convertXmlMapKey(
+ options.attributePrefix + attr.getName.getLocalPart, keyType)
+ kvPairs += (key -> convertTo(attr.getValue, valueType))
+ }
+ var shouldStop = false
+ while (!shouldStop) {
+ bounded.nextEvent match {
+ case e: StartElement =>
+ val rawName = StaxXmlParserUtils.getName(e.asStartElement.getName,
options)
+ val key = convertXmlMapKey(rawName, keyType)
+ kvPairs += (key -> convertField(bounded, valueType, rawName))
+ case c: Characters if !c.isWhiteSpace =>
+ // Create a value tag field for it
+ kvPairs +=
+ // TODO: We don't support array value tags in maps yet.
+ (convertXmlMapKey(options.valueTag, keyType) ->
convertTo(c.getData, valueType))
+ case _: EndElement | _: EndDocument =>
+ shouldStop = true
+ case _ => // do nothing
+ }
}
+ ArrayBasedMapData(kvPairs.toMap)
+ } finally {
+ bounded.drain()
}
- ArrayBasedMapData(kvPairs.toMap)
}
- private def convertConstrainedMap(
- parser: XMLEventReader,
- keyType: DataType,
- valueType: DataType,
- attributes: Array[Attribute]): MapData = {
- val lastEntries =
- mutable.LinkedHashMap.empty[UTF8String, (UTF8String, Option[Any])]
- var badMapException: Option[Throwable] = None
- def mapKey(raw: UTF8String): UTF8String = {
- CharVarcharUtils.applyTextParseSemantics(raw, keyType)
- }
- def appendPair(rawKey: String, value: Option[Any]): Unit = {
- try {
- val rawKeyUtf8 = UTF8String.fromString(rawKey)
- lastEntries.remove(rawKeyUtf8)
- lastEntries.update(rawKeyUtf8, (mapKey(rawKeyUtf8), value))
- } catch {
- case NonFatal(e) => badMapException = badMapException.orElse(Some(e))
- }
- }
- attributes.foreach { attr =>
- val value = try {
- Some(convertTo(attr.getValue, valueType))
- } catch {
- case e: SparkUpgradeException => throw e
- case NonFatal(e) =>
- badMapException = badMapException.orElse(Some(e))
- None
- }
- appendPair(options.attributePrefix + attr.getName.getLocalPart, value)
- }
- var shouldStop = false
- while (!shouldStop) {
- parser.nextEvent match {
- case e: StartElement =>
- val rawKey = StaxXmlParserUtils.getName(e.asStartElement.getName,
options)
- val entryParser = new ElementBoundedEventReader(parser)
- val value = {
- try {
- Some(convertField(entryParser, valueType, rawKey))
- } catch {
- case e: SparkUpgradeException => throw e
- case DuplicateMapKeyUtils(e) => throw e
- case NonFatal(e) =>
- badMapException = badMapException.orElse(Some(e))
- None
- } finally {
- entryParser.drain()
- }
- }
- appendPair(rawKey, value)
- case c: Characters if !c.isWhiteSpace =>
- // Create a value tag field for it
- // TODO: We don't support array value tags in maps yet.
- val value = try {
- Some(convertTo(c.getData, valueType))
- } catch {
- case e: SparkUpgradeException => throw e
- case NonFatal(e) =>
- badMapException = badMapException.orElse(Some(e))
- None
- }
- appendPair(options.valueTag, value)
- case _: EndElement | _: EndDocument =>
- shouldStop = true
- case _ => // do nothing
+ /**
+ * Length-check a CHAR/VARCHAR XML name used as a map key. Unlike value
assignment,
+ * this does not pad or trim. STRING keys and the flag-off path still use
+ * [[CharVarcharUtils.applyTextParseSemantics]].
+ */
+ private def convertXmlMapKey(rawName: String, keyType: DataType): UTF8String
= {
+ val key = UTF8String.fromString(rawName)
+ if (charVarcharStandardSemantics) {
+ keyType match {
+ case c: CharType if key.numChars() != c.length =>
+ throw QueryExecutionErrors.unsupportedXmlCharVarcharMapKey(key, c)
+ case _: CharType => key
+ case v: VarcharType if key.numChars() > v.length =>
+ throw QueryExecutionErrors.unsupportedXmlCharVarcharMapKey(key, v)
+ case _: VarcharType => key
+ case _ => CharVarcharUtils.applyTextParseSemantics(key, keyType)
}
+ } else {
+ CharVarcharUtils.applyTextParseSemantics(key, keyType)
}
- val mapData = DuplicateMapKeyUtils.buildConstrainedMap(
- lastEntries, keyType, valueType)
- badMapException.foreach(throw _)
- mapData
}
/**
@@ -583,7 +551,6 @@ class StaxXmlParser(
row(i) = convertTo(v, schema(i).dataType)
} catch {
case e: SparkUpgradeException => throw e
- case DuplicateMapKeyUtils(e) => throw e
case NonFatal(e) => firstError = firstError.orElse(Some(e))
}
}
@@ -722,7 +689,6 @@ class StaxXmlParser(
}
} catch {
case e: SparkUpgradeException => throw e
- case DuplicateMapKeyUtils(e) => throw e
case NonFatal(e) =>
// TODO: we don't support partial results now
badRecordException = badRecordException.orElse(Some(e))
diff --git
a/sql/catalyst/src/main/scala/org/apache/spark/sql/errors/QueryExecutionErrors.scala
b/sql/catalyst/src/main/scala/org/apache/spark/sql/errors/QueryExecutionErrors.scala
index b8bfd00cb2c8..090e73c264cc 100644
---
a/sql/catalyst/src/main/scala/org/apache/spark/sql/errors/QueryExecutionErrors.scala
+++
b/sql/catalyst/src/main/scala/org/apache/spark/sql/errors/QueryExecutionErrors.scala
@@ -2739,6 +2739,20 @@ private[sql] object QueryExecutionErrors extends
QueryErrorsBase with ExecutionE
)
}
+ /**
+ * Restricting CHAR/VARCHAR XML map keys is a parser limitation (SQLSTATE
0A000),
+ * but this is a SparkRuntimeException so XML parse modes can handle it:
PERMISSIVE
+ * wraps a bad record and FAILFAST surfaces the failure.
+ */
+ def unsupportedXmlCharVarcharMapKey(
+ key: UTF8String, dataType: DataType): SparkRuntimeException = {
+ new SparkRuntimeException(
+ errorClass = "UNSUPPORTED_XML_CHAR_VARCHAR_MAP_KEY",
+ messageParameters = Map(
+ "key" -> toSQLValue(key, StringType),
+ "dataType" -> toSQLType(dataType)))
+ }
+
def timestampAddOverflowError(micros: Long, amount: Long, unit: String):
ArithmeticException = {
new SparkArithmeticException(
errorClass = "DATETIME_OVERFLOW",
diff --git
a/sql/catalyst/src/main/scala/org/apache/spark/sql/internal/SQLConf.scala
b/sql/catalyst/src/main/scala/org/apache/spark/sql/internal/SQLConf.scala
index 3a72fd622a2a..a6ff1e548f7b 100644
--- a/sql/catalyst/src/main/scala/org/apache/spark/sql/internal/SQLConf.scala
+++ b/sql/catalyst/src/main/scala/org/apache/spark/sql/internal/SQLConf.scala
@@ -6953,12 +6953,8 @@ object SQLConf {
val MAP_KEY_DEDUP_POLICY = buildConf("spark.sql.mapKeyDedupPolicy")
.doc("The policy to deduplicate map keys in built-in functions: CreateMap,
MapFromArrays, " +
- "MapFromEntries, StringToMap, MapConcat and TransformKeys. The policy
also applies in " +
- "schema-driven XML parsing, including from_xml and the XML data source,
when CHAR/VARCHAR " +
- "keys normalize to the same value. " +
- "EXCEPTION fails the query when duplicate keys are detected. LAST_WIN
makes the last " +
- "inserted key take precedence. Repeated raw XML map keys retain their
historical " +
- "last-wins behavior. Ordinary STRING keys retain each parser's
historical behavior.")
+ "MapFromEntries, StringToMap, MapConcat and TransformKeys. EXCEPTION
fails the query " +
+ "when duplicate keys are detected. LAST_WIN makes the last inserted key
take precedence.")
.version("3.0.0")
.enumConf(MapKeyDedupPolicy)
.createWithDefault(MapKeyDedupPolicy.EXCEPTION)
diff --git
a/sql/core/src/test/scala/org/apache/spark/sql/CharVarcharTestSuite.scala
b/sql/core/src/test/scala/org/apache/spark/sql/CharVarcharTestSuite.scala
index c203fa1d8c34..f9f5062b1dfc 100644
--- a/sql/core/src/test/scala/org/apache/spark/sql/CharVarcharTestSuite.scala
+++ b/sql/core/src/test/scala/org/apache/spark/sql/CharVarcharTestSuite.scala
@@ -1044,24 +1044,26 @@ class BasicCharVarcharTestSuite extends
SharedSparkSession {
parameters = Map("limit" -> expectedLimit))
}
- private def assertDuplicateMapKey(query: String, expectedKey: String = "a
"): Unit = {
- assertDuplicateMapKeyError(sql(query).collect(), expectedKey)
+ private def assertUnsupportedXmlMapKey(
+ query: String, key: String, dataTypeSql: String): Unit = {
+ assertUnsupportedXmlMapKeyError(sql(query).collect(), key, dataTypeSql)
}
- private def assertDuplicateMapKeyError(body: => Any, expectedKey: String =
"a "): Unit = {
+ private def assertUnsupportedXmlMapKeyError(
+ body: => Any, key: String, dataTypeSql: String): Unit = {
val error = intercept[Exception] { body }
- val duplicateError = Iterator.iterate[Throwable](error)(_.getCause)
+ val cause = Iterator.iterate[Throwable](error)(_.getCause)
.takeWhile(_ != null)
.collectFirst {
- case e: SparkRuntimeException if e.getCondition ==
"DUPLICATED_MAP_KEY" => e
+ case e: SparkRuntimeException
+ if e.getCondition == "UNSUPPORTED_XML_CHAR_VARCHAR_MAP_KEY" => e
}
- .getOrElse(fail("expected DUPLICATED_MAP_KEY cause", error))
+ .getOrElse(fail("expected UNSUPPORTED_XML_CHAR_VARCHAR_MAP_KEY cause",
error))
checkError(
- exception = duplicateError,
- condition = "DUPLICATED_MAP_KEY",
- parameters = Map(
- "key" -> expectedKey,
- "mapKeyDedupPolicy" -> "\"spark.sql.mapKeyDedupPolicy\""))
+ exception = cause,
+ condition = "UNSUPPORTED_XML_CHAR_VARCHAR_MAP_KEY",
+ sqlState = "0A000",
+ parameters = Map("key" -> s"'$key'", "dataType" ->
s""""$dataTypeSql""""))
}
test("user-specified schema in cast") {
@@ -2795,9 +2797,12 @@ class BasicCharVarcharTestSuite extends
SharedSparkSession {
s"SELECT from_xml('<ROW><a>abcdef</a></ROW>', 'a $dataType', " +
"map('mode', 'FAILFAST'))")
}
+ checkAnswer(
+ sql("SELECT from_xml('<ROW><m><ab>1</ab></m></ROW>', 'm MAP<CHAR(2),
INT>')"),
+ Row(Row(Map("ab" -> 1))))
checkAnswer(
sql("SELECT from_xml('<ROW><m><ab>1</ab></m></ROW>', 'm MAP<CHAR(4),
INT>')"),
- Row(Row(Map("ab " -> 1))))
+ Row(Row(null)))
checkAnswer(
sql(
"""SELECT from_xml(
@@ -2837,7 +2842,7 @@ class BasicCharVarcharTestSuite extends
SharedSparkSession {
| 'xs_any ARRAY<CHAR(5)>',
| map('wildcardColName', 'xs_any', 'mode',
'FAILFAST'))""".stripMargin)
withTempPath { path =>
-
Seq("<ROW><m><a>1</a></m></ROW>").toDS().write.text(path.getCanonicalPath)
+
Seq("<ROW><m><ab>1</ab></m></ROW>").toDS().write.text(path.getCanonicalPath)
val xmlDataFrame = spark.read
.option("rowTag", "ROW")
.schema("m MAP<CHAR(2), INT>")
@@ -2845,7 +2850,7 @@ class BasicCharVarcharTestSuite extends
SharedSparkSession {
assert(
xmlDataFrame.schema("m").dataType ===
MapType(CharType(2), IntegerType, valueContainsNull = true))
- checkAnswer(xmlDataFrame, Row(Map("a " -> 1)))
+ checkAnswer(xmlDataFrame, Row(Map("ab" -> 1)))
}
withTempPath { path =>
Seq("""{"c":"ab"}""").toDS().write.text(path.getCanonicalPath)
@@ -2896,155 +2901,291 @@ class BasicCharVarcharTestSuite extends
SharedSparkSession {
}
}
- test("SPARK-59274: normalized XML CHAR map keys honor the dedup policy") {
+ test("SPARK-60103: XML CHAR/VARCHAR map keys are not padded or trimmed") {
withSQLConf(SQLConf.CHAR_VARCHAR_STANDARD_SEMANTICS.key -> "true") {
- val xmlQuery =
- """SELECT from_xml(
- | '<ROW><m><a>1</a>9</m></ROW>',
- | 'm MAP<CHAR(2), INT>',
- | map('valueTag', 'a ')).m""".stripMargin
- val varcharXmlQuery =
- """SELECT from_xml(
- | '<ROW><m><ab>1</ab>2</m></ROW>',
- | 'm MAP<VARCHAR(2), INT>',
- | map('valueTag', 'ab ')).m""".stripMargin
- val exactCharXmlQuery =
+ checkAnswer(
+ sql("SELECT from_xml('<ROW><m><ab>1</ab></m></ROW>', 'm MAP<CHAR(2),
INT>').m"),
+ Row(Map("ab" -> 1)))
+ checkAnswer(
+ sql("SELECT from_xml('<ROW><m><ab>1</ab></m></ROW>', 'm
MAP<VARCHAR(3), INT>').m"),
+ Row(Map("ab" -> 1)))
+ checkAnswer(
+ sql("""SELECT from_xml(
+ | '<ROW><m><ab>1</ab><AB>2</AB></m></ROW>',
+ | 'm MAP<VARCHAR(2), INT>').m""".stripMargin),
+ Row(Map("ab" -> 1, "AB" -> 2)))
+ checkAnswer(
+ sql("""SELECT from_xml(
+ | '<ROW><m><ab>1</ab><ab>2</ab></m></ROW>',
+ | 'm MAP<CHAR(2), INT>').m""".stripMargin),
+ Row(Map("ab" -> 2)))
+ checkAnswer(
+ sql("""SELECT from_xml(
+ | '<ROW><m><outer><xy>1</xy></outer></m></ROW>',
+ | 'm MAP<STRING, MAP<CHAR(2), INT>>').m""".stripMargin),
+ Row(Map("outer" -> Map("xy" -> 1))))
+
+ // A key that fails length-check still lets later struct fields parse.
+ checkAnswer(
+ sql("""SELECT from_xml(
+ | '<ROW><m><a>1</a></m><tail>2</tail></ROW>',
+ | 'm MAP<CHAR(2), INT>, tail INT')""".stripMargin),
+ Row(Row(null, 2)))
+ checkAnswer(
+ sql("""SELECT from_xml(
+ | '<ROW><m><outer><z>1</z></outer></m><tail>2</tail></ROW>',
+ | 'm MAP<STRING, MAP<CHAR(2), INT>>, tail INT')""".stripMargin),
+ Row(Row(null, 2)))
+ assertUnsupportedXmlMapKey(
"""SELECT from_xml(
- | '<ROW><m><a>1</a><a>2</a></m></ROW>',
- | 'm MAP<CHAR(2), INT>').m""".stripMargin
- val exactVarcharXmlQuery =
+ | '<ROW><m><outer><z>1</z></outer></m></ROW>',
+ | 'm MAP<STRING, MAP<CHAR(2), INT>>',
+ | map('mode', 'FAILFAST'))""".stripMargin,
+ key = "z",
+ dataTypeSql = "CHAR(2)")
+
+ // ARRAY<MAP<...>> uses convertField's MapType arm; a short key must
still keep tail.
+ checkAnswer(
+ sql("""SELECT from_xml(
+ | '<ROW><m><a>1</a></m><m><ab>2</ab></m><tail>9</tail></ROW>',
+ | 'm ARRAY<MAP<CHAR(2), INT>>, tail INT')""".stripMargin),
+ Row(Row(Seq(Map("ab" -> 2)), 9)))
+ assertUnsupportedXmlMapKey(
"""SELECT from_xml(
- | '<ROW><m><ab>1</ab><ab>2</ab></m></ROW>',
- | 'm MAP<VARCHAR(2), INT>').m""".stripMargin
- val interleavedCharXmlQuery =
- """SELECT map_entries(from_xml(
- | '<ROW><m><a>1</a>2<a>3</a></m></ROW>',
- | 'm MAP<CHAR(2), INT>',
- | map('valueTag', 'a ')).m)""".stripMargin
- val badXmlKeyThenSiblingQuery =
+ | '<ROW><m><a>1</a></m><m><ab>2</ab></m></ROW>',
+ | 'm ARRAY<MAP<CHAR(2), INT>>',
+ | map('mode', 'FAILFAST'))""".stripMargin,
+ key = "a",
+ dataTypeSql = "CHAR(2)")
+
+ // Default valueTag is "_VALUE" (6 characters). Text-only elements are
malformed
+ // records (convertField), so use mixed content to reach convertMap.
+ checkAnswer(
+ sql("SELECT from_xml('<ROW><m><ab>1</ab>xy</m></ROW>', 'm MAP<CHAR(2),
STRING>')"),
+ Row(Row(null)))
+ assertUnsupportedXmlMapKey(
"""SELECT from_xml(
- | '<ROW><m><abc>1</abc></m><tail>2</tail></ROW>',
- | 'm MAP<CHAR(2), INT>, tail INT').tail""".stripMargin
- val badXmlKeyBeforeDuplicateQuery =
+ | '<ROW><m><ab>1</ab>xy</m></ROW>',
+ | 'm MAP<CHAR(2), STRING>',
+ | map('mode', 'FAILFAST'))""".stripMargin,
+ key = "_VALUE",
+ dataTypeSql = "CHAR(2)")
+
+ // Too-short CHAR, CHAR overflow, and VARCHAR overflow are not
EXCEED_LIMIT_LENGTH.
+ checkAnswer(
+ sql("SELECT from_xml('<ROW><m><a>1</a></m></ROW>', 'm MAP<CHAR(2),
INT>')"),
+ Row(Row(null)))
+ checkAnswer(
+ sql("SELECT from_xml('<ROW><m><abc>1</abc></m></ROW>', 'm MAP<CHAR(2),
INT>')"),
+ Row(Row(null)))
+ checkAnswer(
+ sql("SELECT from_xml('<ROW><m><abcd>1</abcd></m></ROW>', 'm
MAP<VARCHAR(3), INT>')"),
+ Row(Row(null)))
+ assertUnsupportedXmlMapKey(
"""SELECT from_xml(
- | '<ROW><m><abc>0</abc><a>1</a>2</m></ROW>',
+ | '<ROW><m><a>1</a></m></ROW>',
| 'm MAP<CHAR(2), INT>',
- | map('valueTag', 'a ')).m""".stripMargin
- val malformedXmlValueBeforeDuplicateQuery =
+ | map('mode', 'FAILFAST'))""".stripMargin,
+ key = "a",
+ dataTypeSql = "CHAR(2)")
+ assertUnsupportedXmlMapKey(
"""SELECT from_xml(
- | '<ROW><m><a>bad</a>2</m></ROW>',
+ | '<ROW><m><abc>1</abc></m></ROW>',
| 'm MAP<CHAR(2), INT>',
- | map('valueTag', 'a ')).m""".stripMargin
- val ignoreCorruptXmlQuery =
+ | map('mode', 'FAILFAST'))""".stripMargin,
+ key = "abc",
+ dataTypeSql = "CHAR(2)")
+ assertUnsupportedXmlMapKey(
"""SELECT from_xml(
- | '<ROW><m><a>1</a>2</m></ROW>',
+ | '<ROW><m><abcd>1</abcd></m></ROW>',
+ | 'm MAP<VARCHAR(3), INT>',
+ | map('mode', 'FAILFAST'))""".stripMargin,
+ key = "abcd",
+ dataTypeSql = "VARCHAR(3)")
+
+ checkAnswer(
+ sql("""SELECT from_xml(
+ | '<ROW><m a="1"><bc>2</bc></m></ROW>',
| 'm MAP<CHAR(2), INT>',
- | map('valueTag', 'a ', 'ignoreCorruptFiles',
'true')).m""".stripMargin
- val attributePaddingQuery =
- """SELECT from_xml(
- | '<ROW><m a="1"><b>2</b></m></ROW>',
+ | map('attributePrefix', ''))""".stripMargin),
+ Row(Row(null)))
+ checkAnswer(
+ sql("""SELECT from_xml(
+ | '<ROW><m ab="1"><cd>2</cd></m></ROW>',
| 'm MAP<CHAR(2), INT>',
- | map('attributePrefix', '')).m""".stripMargin
- val attributeOverflowQuery =
- """SELECT from_xml(
- | '<ROW><m abc="1"><b>2</b></m></ROW>',
+ | map('attributePrefix', ''))""".stripMargin),
+ Row(Row(Map("ab" -> 1, "cd" -> 2))))
+ checkAnswer(
+ sql("""SELECT from_xml(
+ | '<ROW><m abc="1"><cd>2</cd></m></ROW>',
| 'm MAP<VARCHAR(2), INT>',
- | map('attributePrefix', '')).m""".stripMargin
- val attributeOverflowFailfastQuery =
+ | map('attributePrefix', ''))""".stripMargin),
+ Row(Row(null)))
+ assertUnsupportedXmlMapKey(
"""SELECT from_xml(
- | '<ROW><m abc="1"><b>2</b></m></ROW>',
+ | '<ROW><m abc="1"><cd>2</cd></m></ROW>',
| 'm MAP<VARCHAR(2), INT>',
- | map('attributePrefix', '', 'mode', 'FAILFAST')).m""".stripMargin
- val attributeCollisionQuery =
+ | map('attributePrefix', '', 'mode', 'FAILFAST'))""".stripMargin,
+ key = "abc",
+ dataTypeSql = "VARCHAR(2)")
+
+ // Default attributePrefix "_" is part of the key: local name ab is key
_ab.
+ // Attribute-only elements are SQL NULL (convertField EndElement), so
include a child.
+ checkAnswer(
+ sql("SELECT from_xml('<ROW><m ab=\"1\"><xy>2</xy></m></ROW>', 'm
MAP<CHAR(2), INT>')"),
+ Row(Row(null)))
+ checkAnswer(
+ sql("SELECT from_xml('<ROW><m ab=\"1\"><xyz>2</xyz></m></ROW>', 'm
MAP<CHAR(3), INT>')"),
+ Row(Row(Map("_ab" -> 1, "xyz" -> 2))))
+ assertUnsupportedXmlMapKey(
"""SELECT from_xml(
- | '<ROW><m a="1"><b>0</b>2</m></ROW>',
+ | '<ROW><m ab="1"><xy>2</xy></m></ROW>',
| 'm MAP<CHAR(2), INT>',
- | map('attributePrefix', '', 'valueTag', 'a ')).m""".stripMargin
-
- assertDuplicateMapKey(xmlQuery)
- assertDuplicateMapKey(varcharXmlQuery, expectedKey = "ab")
- checkAnswer(sql(exactCharXmlQuery), Row(Map("a " -> 2)))
- checkAnswer(sql(exactVarcharXmlQuery), Row(Map("ab" -> 2)))
- assertDuplicateMapKey(interleavedCharXmlQuery)
- assertDuplicateMapKey(badXmlKeyBeforeDuplicateQuery)
- assertDuplicateMapKey(malformedXmlValueBeforeDuplicateQuery)
- assertDuplicateMapKey(ignoreCorruptXmlQuery)
- checkAnswer(sql(badXmlKeyThenSiblingQuery), Row(2))
- checkAnswer(sql(attributePaddingQuery), Row(Map("a " -> 1, "b " -> 2)))
- checkAnswer(sql(attributeOverflowQuery), Row(null))
- assertParseExceedLimit(attributeOverflowFailfastQuery, expectedLimit =
"2")
- assertDuplicateMapKey(attributeCollisionQuery)
+ | map('mode', 'FAILFAST'))""".stripMargin,
+ key = "_ab",
+ dataTypeSql = "CHAR(2)")
+
+ // MAP<STRING> empty, attribute-only, and text-only elements stay SQL
NULL, matching
+ // convertField on master. ARRAY<MAP> empty elements are null entries,
not {}.
+ checkAnswer(
+ sql("SELECT from_xml('<ROW><m></m></ROW>', 'm MAP<STRING, INT>')"),
+ Row(Row(null)))
+ checkAnswer(
+ sql("SELECT from_xml('<ROW><m a=\"1\"></m></ROW>', 'm MAP<STRING,
INT>')"),
+ Row(Row(null)))
+ checkAnswer(
+ sql("SELECT from_xml('<ROW><m>xy</m></ROW>', 'm MAP<STRING,
STRING>')"),
+ Row(Row(null)))
+ checkAnswer(
+ sql("""SELECT from_xml(
+ | '<ROW><m></m><tail>9</tail></ROW>',
+ | 'm ARRAY<MAP<STRING, INT>>, tail INT')""".stripMargin),
+ Row(Row(Seq(null), 9)))
withTempPath { path =>
-
Seq("<ROW><m><a>1</a>2</m></ROW>").toDS().write.text(path.getCanonicalPath)
- def readXmlMap(): DataFrame = spark.read
+ Seq(
+
"""<ROWS><ROW><m><ab>1</ab></m></ROW><ROW><m><a>1</a></m></ROW></ROWS>"""
+ ).toDS().write.text(path.getCanonicalPath)
+ val schema = "m MAP<CHAR(2), INT>"
+ val permissive = spark.read
.option("rowTag", "ROW")
- .option("valueTag", "a ")
- .schema("m MAP<CHAR(2), INT>")
+ .schema(schema)
.xml(path.getCanonicalPath)
+ checkAnswer(permissive, Seq(Row(Map("ab" -> 1)), Row(null)))
+ val failFast = spark.read
+ .option("rowTag", "ROW")
+ .option("mode", "FAILFAST")
+ .schema(schema)
+ .xml(path.getCanonicalPath)
+ assertUnsupportedXmlMapKeyError(failFast.collect(), key = "a",
dataTypeSql = "CHAR(2)")
+ val ignoreCorrupt = spark.read
+ .option("rowTag", "ROW")
+ .option("ignoreCorruptFiles", "true")
+ .schema(schema)
+ .xml(path.getCanonicalPath)
+ checkAnswer(ignoreCorrupt, Seq(Row(Map("ab" -> 1)), Row(null)))
+ }
- assertDuplicateMapKeyError(readXmlMap().collect())
+ withTempPath { path =>
+ Seq(
+
"""<ROWS><ROW><m><a>1</a></m></ROW><ROW><m><ab>2</ab></m></ROW></ROWS>"""
+ ).toDS().write.text(path.getCanonicalPath)
+ val arraySchema = "m ARRAY<MAP<CHAR(2), INT>>"
+ val arrayPermissive = spark.read
+ .option("rowTag", "ROW")
+ .schema(arraySchema)
+ .xml(path.getCanonicalPath)
+ checkAnswer(arrayPermissive, Seq(Row(null), Row(Seq(Map("ab" -> 2)))))
+ }
- withSQLConf(
- SQLConf.MAP_KEY_DEDUP_POLICY.key ->
SQLConf.MapKeyDedupPolicy.LAST_WIN.toString) {
- checkAnswer(readXmlMap(), Row(Map("a " -> 2)))
- }
+ // format("xml").load() skips INVALID_XML_MAP_KEY_TYPE. Non-string keys
must stay a
+ // malformed record (MatchError), not a mistyped map that fails with
ClassCastException.
+ withTempPath { path =>
+ Seq("""<ROWS><ROW><m><a>x</a></m></ROW></ROWS>""")
+ .toDS().write.text(path.getCanonicalPath)
+ val intKeySchema = "m MAP<INT, STRING>"
+ val permissiveInt = spark.read
+ .format("xml")
+ .option("rowTag", "ROW")
+ .schema(intKeySchema)
+ .load(path.getCanonicalPath)
+ checkAnswer(permissiveInt, Seq(Row(null)))
+ val failFastInt = spark.read
+ .format("xml")
+ .option("rowTag", "ROW")
+ .option("mode", "FAILFAST")
+ .schema(intKeySchema)
+ .load(path.getCanonicalPath)
+ val error = intercept[Exception] { failFastInt.collect() }
+ val causes =
Iterator.iterate[Throwable](error)(_.getCause).takeWhile(_ != null)
+ assert(
+ !causes.exists(_.isInstanceOf[ClassCastException]),
+ s"MAP<INT, STRING> XML read must not ClassCastException: $error")
}
withSQLConf(
- SQLConf.MAP_KEY_DEDUP_POLICY.key ->
SQLConf.MapKeyDedupPolicy.LAST_WIN.toString) {
- checkAnswer(sql(xmlQuery), Row(Map("a " -> 9)))
- checkAnswer(sql(varcharXmlQuery), Row(Map("ab" -> 2)))
- checkAnswer(sql(exactCharXmlQuery), Row(Map("a " -> 2)))
- checkAnswer(sql(exactVarcharXmlQuery), Row(Map("ab" -> 2)))
- checkAnswer(sql(interleavedCharXmlQuery), Row(Seq(Row("a ", 3))))
- checkAnswer(sql(badXmlKeyBeforeDuplicateQuery), Row(null))
- checkAnswer(sql(malformedXmlValueBeforeDuplicateQuery), Row(null))
- checkAnswer(sql(ignoreCorruptXmlQuery), Row(Map("a " -> 2)))
- checkAnswer(sql(attributeCollisionQuery), Row(Map("a " -> 2, "b " ->
0)))
+ SQLConf.MAP_KEY_DEDUP_POLICY.key ->
SQLConf.MapKeyDedupPolicy.EXCEPTION.toString) {
+ checkAnswer(
+ sql("""SELECT from_xml(
+ | '<ROW><m><ab>1</ab><ab>2</ab></m></ROW>',
+ | 'm MAP<VARCHAR(2), INT>').m""".stripMargin),
+ Row(Map("ab" -> 2)))
}
- }
- }
- test("SPARK-59274: pretty-printed XML map failures preserve parser
position") {
- withSQLConf(SQLConf.CHAR_VARCHAR_STANDARD_SEMANTICS.key -> "true") {
- val overflowQueries = Seq("CHAR(5)", "VARCHAR(5)").map { valueType =>
- s"""SELECT from_xml(
- | '<ROW>
- | <m>
- | <b>abcdef</b>
- | <a>x</a>y
- | </m>
- | <tail>9</tail>
- | </ROW>',
- | 'm MAP<CHAR(2), $valueType>, tail INT',
- | map('valueTag', 'a '))""".stripMargin
- }
- val nestedFailureQuery =
+ // Value overflow still uses EXCEED_LIMIT_LENGTH. A last-win duplicate
whose winning
+ // value overflows nulls the map, matching the non-duplicate case.
+ checkAnswer(
+ sql("""SELECT from_xml(
+ | '<ROW><m><ab>x</ab><ab>abcdef</ab></m></ROW>',
+ | 'm MAP<CHAR(2), CHAR(2)>')""".stripMargin),
+ Row(Row(null)))
+ assertParseExceedLimit(
"""SELECT from_xml(
- | '<ROW>
- | <m>
- | <b><x>bad</x></b>
- | <a><x>1</x></a>ignored
- | </m>
- | <tail>9</tail>
- | </ROW>',
- | 'm MAP<CHAR(2), MAP<CHAR(2), INT>>, tail INT',
- | map('valueTag', 'a '))""".stripMargin
-
- (overflowQueries :+ nestedFailureQuery).foreach { query =>
- // A duplicate, rather than an AssertionError from XML recovery, wins
under EXCEPTION.
- withClue(s"$query: ") {
- assertDuplicateMapKey(query)
- }
+ | '<ROW><m><ab>x</ab><ab>abcdef</ab></m></ROW>',
+ | 'm MAP<CHAR(2), CHAR(2)>',
+ | map('mode', 'FAILFAST'))""".stripMargin,
+ expectedLimit = "2")
+
+ withSQLConf(SQLConf.ALLOW_COLLATIONS_IN_MAP_KEYS.key -> "true") {
+ checkAnswer(
+ sql("""SELECT from_xml(
+ | '<ROW><m><ab>1</ab><AB>2</AB></m></ROW>',
+ | 'm MAP<VARCHAR(2) COLLATE UTF8_LCASE, INT>').m""".stripMargin),
+ Row(Map("ab" -> 1, "AB" -> 2)))
}
+ }
- withSQLConf(
- SQLConf.MAP_KEY_DEDUP_POLICY.key ->
SQLConf.MapKeyDedupPolicy.LAST_WIN.toString) {
- // The failed map remains null, but its complete element is consumed
and the `tail` field
- // is parsed.
- (overflowQueries :+ nestedFailureQuery).foreach { query =>
- checkAnswer(sql(query), Row(Row(null, 9)))
+ withSQLConf(
+ SQLConf.CHAR_VARCHAR_STANDARD_SEMANTICS.key -> "false",
+ SQLConf.PRESERVE_CHAR_VARCHAR_TYPE_INFO.key -> "true") {
+ checkAnswer(
+ sql("SELECT from_xml('<ROW><m><a>1</a></m></ROW>', 'm MAP<CHAR(3),
INT>')"),
+ Row(Row(Map("a " -> 1))))
+ assertParseExceedLimit(
+ """SELECT from_xml(
+ | '<ROW><m><abcd>1</abcd></m></ROW>',
+ | 'm MAP<CHAR(3), INT>',
+ | map('mode', 'FAILFAST'))""".stripMargin,
+ expectedLimit = "3")
+ assertParseExceedLimit(
+ """SELECT from_xml(
+ | '<ROW><m><abcd>1</abcd></m></ROW>',
+ | 'm MAP<VARCHAR(3), INT>',
+ | map('mode', 'FAILFAST'))""".stripMargin,
+ expectedLimit = "3")
+ }
+ }
+
+ test("SPARK-60103: XML map value overflow keeps siblings") {
+ withSQLConf(SQLConf.CHAR_VARCHAR_STANDARD_SEMANTICS.key -> "true") {
+ Seq("CHAR(5)", "VARCHAR(5)").foreach { valueType =>
+ val xml = "<ROW><m><xy>abcdef</xy></m><tail>9</tail></ROW>"
+ val schema = s"m MAP<CHAR(2), $valueType>, tail INT"
+ withClue(valueType) {
+ checkAnswer(sql(s"SELECT from_xml('$xml', '$schema')"),
Row(Row(null, 9)))
+ assertParseExceedLimit(
+ s"SELECT from_xml('$xml', '$schema', map('mode', 'FAILFAST'))",
+ expectedLimit = "5")
}
}
}
@@ -3194,27 +3335,15 @@ class BasicCharVarcharTestSuite extends
SharedSparkSession {
}
}
- test("SPARK-59274: collated CHAR/VARCHAR map keys honor the dedup policy") {
+ test("SPARK-59274: collated XML CHAR/VARCHAR map keys last-win on exact
names") {
withSQLConf(
SQLConf.CHAR_VARCHAR_STANDARD_SEMANTICS.key -> "true",
SQLConf.ALLOW_COLLATIONS_IN_MAP_KEYS.key -> "true") {
- val xmlQuery =
- """SELECT from_xml(
- | '<ROW><m><a>1</a><A>2</A></m></ROW>',
- | 'm MAP<CHAR(2) COLLATE UTF8_LCASE, INT>').m""".stripMargin
- val varcharXmlQuery =
- """SELECT from_xml(
+ checkAnswer(
+ sql("""SELECT from_xml(
| '<ROW><m><ab>1</ab><AB>2</AB></m></ROW>',
- | 'm MAP<VARCHAR(2) COLLATE UTF8_LCASE, INT>').m""".stripMargin
-
- assertDuplicateMapKey(xmlQuery, expectedKey = "A ")
- assertDuplicateMapKey(varcharXmlQuery, expectedKey = "AB")
-
- withSQLConf(
- SQLConf.MAP_KEY_DEDUP_POLICY.key ->
SQLConf.MapKeyDedupPolicy.LAST_WIN.toString) {
- checkAnswer(sql(xmlQuery), Row(Map("a " -> 2)))
- checkAnswer(sql(varcharXmlQuery), Row(Map("ab" -> 2)))
- }
+ | 'm MAP<CHAR(2) COLLATE UTF8_LCASE, INT>').m""".stripMargin),
+ Row(Map("ab" -> 1, "AB" -> 2)))
}
}
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]