This is an automated email from the ASF dual-hosted git repository.

MaxGekk 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 b1fc6eb16620 [SPARK-58104][SQL] Make XML schema inference honor the 
nullValue option, consistent with CSV
b1fc6eb16620 is described below

commit b1fc6eb166206df6babb52cb9ed61726122b493d
Author: Wenchen Fan <[email protected]>
AuthorDate: Tue Jul 14 08:46:18 2026 +0200

    [SPARK-58104][SQL] Make XML schema inference honor the nullValue option, 
consistent with CSV
    
    ### What changes were proposed in this pull request?
    
    Gate XML schema inference on the `nullValue` option. In 
`XmlInferSchema.inferFrom(datum, typeSoFar)`, a value equal to 
`options.nullValue` now preserves `typeSoFar` (is skipped for type inference) 
instead of being inferred by its string content:
    
    ```scala
    if (value == null || value.isEmpty || value == options.nullValue) {
      return typeSoFar
    }
    ```
    
    This matches `CSVInferSchema.inferField`, which already gates on 
`nullValue`:
    
    ```scala
    if (field == null || field.isEmpty || field == options.nullValue) {
      typeSoFar
    } else { ... }
    ```
    
    ### Why are the changes needed?
    
    The XML **parser** already treats a value equal to `nullValue` as null when 
reading (see the `options.nullValue` checks in `StaxXmlParser`), but schema 
**inference** did not skip it — so a value equal to `nullValue` was inferred 
from its string content. This produces an inference-vs-parse inconsistency: the 
inferred type does not reflect how the data is actually read.
    
    Concretely, with `nullValue=1` and input `<ROW>1</ROW>`, inference produced 
`LongType` for the value tag while the parser read the value as `null`. It also 
diverges from the CSV datasource, whose `inferField` gates date/numeric/etc. 
inference on `nullValue`. XML inference is meant to mirror CSV (the two text 
datasources share the same `tryParse*` cascade design), so this aligns the two.
    
    ### Does this PR introduce _any_ user-facing change?
    
    Yes. With a non-default `nullValue`, XML schema inference no longer infers 
a type from values equal to `nullValue`. For example, with `nullValue=1`, 
`<ROW>1</ROW>` previously inferred the value tag as `LongType` (while the data 
read as null); it now infers `StringType` (the canonicalized type of an 
all-null field) and still reads null. With the default `nullValue` (unset), 
behavior is unchanged.
    
    ### How was this patch tested?
    
    - Added a unit test in `XmlInferSchemaTypeCastingSuite` ("A value matching 
the nullValue option keeps the type inferred so far") asserting that a 
`nullValue`-matching value preserves `typeSoFar` from 
`NullType`/`LongType`/`TimestampType`, that a non-null value is still inferred 
normally, and that a numeric-looking `nullValue` token (`nullValue=0`, value 
`0`) is treated as null rather than inferred as `LongType`.
    - Updated the existing `XmlInferSchemaSuite` end-to-end test "value tag - 
equals to null value" to the corrected schema (`StringType` instead of 
`LongType`), reflecting that the all-null value tag now canonicalizes to 
`StringType`.
    - Existing XML inference suites pass.
    
    ### Was this patch authored or co-authored using generative AI tooling?
    
    Generated-by: Claude Code (Anthropic Claude Opus)
    
    Closes #57229 from cloud-fan/xml-infer-nullvalue.
    
    Authored-by: Wenchen Fan <[email protected]>
    Signed-off-by: Max Gekk <[email protected]>
---
 .../spark/sql/catalyst/xml/XmlInferSchema.scala      |  7 ++++++-
 .../xml/XmlInferSchemaTypeCastingSuite.scala         | 20 ++++++++++++++++++--
 .../datasources/xml/XmlInferSchemaSuite.scala        |  7 ++++---
 3 files changed, 28 insertions(+), 6 deletions(-)

diff --git 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/xml/XmlInferSchema.scala
 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/xml/XmlInferSchema.scala
index 234de9160421..9df0ebcec7ae 100644
--- 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/xml/XmlInferSchema.scala
+++ 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/xml/XmlInferSchema.scala
@@ -366,7 +366,12 @@ class XmlInferSchema(private val options: XmlOptions, 
private val caseSensitive:
     if (!options.inferSchema) {
       return StringType
     }
-    if (value == null || value.isEmpty) {
+    // A value matching the `nullValue` option carries no type information, so 
it preserves the
+    // type inferred so far -- matching `CSVInferSchema.inferField`. The XML 
parser already reads
+    // such values as null (see `StaxXmlParser`), so inference must skip them 
too; otherwise a
+    // column that is entirely `nullValue`s (or mixes them with a typed value) 
would infer the
+    // string content of the token instead of ignoring it.
+    if (value == null || value.isEmpty || value == options.nullValue) {
       return typeSoFar
     }
 
diff --git 
a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/xml/XmlInferSchemaTypeCastingSuite.scala
 
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/xml/XmlInferSchemaTypeCastingSuite.scala
index e5570ddde88f..94cd261ae185 100644
--- 
a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/xml/XmlInferSchemaTypeCastingSuite.scala
+++ 
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/xml/XmlInferSchemaTypeCastingSuite.scala
@@ -104,14 +104,30 @@ class XmlInferSchemaTypeCastingSuite extends 
SparkFunSuite with SQLHelper {
     val inferSchema = newInferSchema(Map.empty[String, String])
 
     // A genuinely empty/null value carries no type information, so 
`typeSoFar` is preserved.
-    // (Unlike CSV, XML inference does not treat the `nullValue` option as 
null here; a value
-    // equal to `nullValue` is inferred by its content, matching the 
pre-existing behavior.)
     assert(inferSchema.inferFrom("", NullType) == NullType)
     assert(inferSchema.inferFrom("", LongType) == LongType)
     assert(inferSchema.inferFrom(null, DoubleType) == DoubleType)
     assert(inferSchema.inferFrom(null, TimestampType) == TimestampType)
   }
 
+  test("A value matching the nullValue option keeps the type inferred so far") 
{
+    // A token equal to the `nullValue` option is read as null by the parser, 
so inference must
+    // ignore it and preserve `typeSoFar` -- matching 
`CSVInferSchema.inferField`. Otherwise a
+    // `nullValue` token would be inferred by its string content and 
(incorrectly) widen the field.
+    val inferSchema = newInferSchema(Map("nullValue" -> "NA"))
+    assert(inferSchema.inferFrom("NA", NullType) == NullType)
+    assert(inferSchema.inferFrom("NA", LongType) == LongType)
+    assert(inferSchema.inferFrom("NA", TimestampType) == TimestampType)
+    // A non-null value is still inferred by content as usual.
+    assert(inferSchema.inferFrom("5", NullType) == LongType)
+
+    // The nullValue token itself, when it happens to look like another type, 
is still treated as
+    // null rather than inferred as that type.
+    val inferSchemaNumericNull = newInferSchema(Map("nullValue" -> "0"))
+    assert(inferSchemaNumericNull.inferFrom("0", NullType) == NullType)
+    assert(inferSchemaNumericNull.inferFrom("1", NullType) == LongType)
+  }
+
   test("Refining from a wider type never narrows and is consistent with fresh 
inference") {
     val inferSchema = newInferSchema(Map.empty[String, String])
     // Once a field is Double, an integral value keeps it Double (does not 
narrow to Long).
diff --git 
a/sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/xml/XmlInferSchemaSuite.scala
 
b/sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/xml/XmlInferSchemaSuite.scala
index 18cfe17d433e..e1579a53b9dd 100644
--- 
a/sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/xml/XmlInferSchemaSuite.scala
+++ 
b/sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/xml/XmlInferSchemaSuite.scala
@@ -641,12 +641,13 @@ class XmlInferSchemaSuite
   }
 
   test("value tag - equals to null value") {
-    // we don't consider options.nullValue during schema inference
+    // A value tag equal to the nullValue option is treated as null during 
inference (matching
+    // CSVInferSchema and the parser, which reads it as null), so it carries 
no type and the
+    // all-null field canonicalizes to StringType rather than being inferred 
as LongType.
     val xmlDF = readData(valueTagIsNullValue, Map("nullValue" -> "1"))
     val expectedSchema = new StructType()
-      .add(valueTagName, LongType)
+      .add(valueTagName, StringType)
     val expectedAns = Seq(Row(null))
-    // nullValue option is used during parsing
     assert(xmlDF.schema === expectedSchema)
     checkAnswer(xmlDF, expectedAns)
   }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to