Commit c585f81
committed
[SPARK-58133][SQL] XML schema inference must not skip empty values to avoid data loss
### What changes were proposed in this pull request?
In `XmlInferSchema.inferFrom`, stop skipping empty values during schema inference. Previously the guard was:
```scala
if (value == null || value.isEmpty || value == options.nullValue) {
return typeSoFar
}
```
This PR drops the `value.isEmpty` term:
```scala
if (value == null || value == options.nullValue) {
return typeSoFar
}
```
so an empty value now falls through the type cascade to `StringType`. As a result, a field mixing empty and numeric values widens to `StringType` instead of being inferred as the numeric type.
### Why are the changes needed?
XML schema inference was copied from `CSVInferSchema.inferField`, which does early-return on `field.isEmpty`. But the two datasources have a crucial difference at parse time:
- CSV defaults `nullValue` to `""`, so an empty field is read as null by the parser (`UnivocityParser.nullSafeDatum`). Its inference can safely skip empty values because they never reach a numeric converter.
- XML defaults `nullValue` to `null`, so an empty value is **not** read as null. If a column mixing empty and numeric values inferred `LongType`, `StaxXmlParser` would call `convertTo("", LongType)`, which throws `NumberFormatException`. In `PERMISSIVE` mode (the default), the malformed-record error recovery can then silently consume sibling XML events, **dropping records** — a silent data-loss bug.
Concretely, for input
```xml
<ROW><entry Code="001">first</entry><entry Code="002">second</entry></ROW>
<ROW><entry Code="">third</entry><entry Code="003">fourth</entry></ROW>
<ROW><entry Code="004">fifth</entry><entry Code="">sixth</entry></ROW>
```
`entry._Code` was inferred as `LongType` (from the numeric `Code` values, empty skipped), and reading then dropped records when it hit the empty `Code=""`. After this change `_Code` infers as `StringType` and all rows are read.
By letting empty values fall through to `StringType`, inference stays consistent with how the XML parser actually reads the data.
### Does this PR introduce _any_ user-facing change?
Yes. With the default `nullValue` (unset), a field that mixes empty and typed (e.g. numeric) values is now inferred as `StringType` instead of the typed type. This prevents the silent record-dropping described above. Fields that are entirely typed, or that use a non-default `nullValue` covering the empty token, are unaffected. Empty *elements* (`<c/>`) are still read as `NullType` by the parser and are unaffected.
### How was this patch tested?
Added two tests to `XmlInferSchemaSuite`:
- `SPARK-58133: empty values are not skipped during inference (no data loss)` — asserts that an attribute mixing empty and numeric values infers `StringType` and that all rows/entries survive (no PERMISSIVE-mode record dropping).
- `SPARK-58133: empty and numeric element values still infer via NullType` — documents that empty elements keep inferring `LongType` (via the parser's `NullType` path), pinning the element/attribute asymmetry.
Existing XML inference suites pass.
### Was this patch authored or co-authored using generative AI tooling?
Generated-by: Claude Code (Anthropic Claude Opus)
Closes #57265 from cloud-fan/cloud-fan/xml-infer-empty-dataloss.
Authored-by: Wenchen Fan <wenchen@databricks.com>
Signed-off-by: Wenchen Fan <wenchen@databricks.com>1 parent 6b719b3 commit c585f81
3 files changed
Lines changed: 57 additions & 6 deletions
File tree
- sql
- catalyst/src
- main/scala/org/apache/spark/sql/catalyst/xml
- test/scala/org/apache/spark/sql/catalyst/xml
- core/src/test/scala/org/apache/spark/sql/execution/datasources/xml
Lines changed: 11 additions & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
371 | 371 | | |
372 | 372 | | |
373 | 373 | | |
374 | | - | |
| 374 | + | |
| 375 | + | |
| 376 | + | |
| 377 | + | |
| 378 | + | |
| 379 | + | |
| 380 | + | |
| 381 | + | |
| 382 | + | |
| 383 | + | |
| 384 | + | |
375 | 385 | | |
376 | 386 | | |
377 | 387 | | |
| |||
sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/xml/XmlInferSchemaTypeCastingSuite.scala
Lines changed: 10 additions & 5 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
37 | 37 | | |
38 | 38 | | |
39 | 39 | | |
40 | | - | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
41 | 43 | | |
42 | 44 | | |
43 | 45 | | |
| |||
100 | 102 | | |
101 | 103 | | |
102 | 104 | | |
103 | | - | |
| 105 | + | |
104 | 106 | | |
105 | 107 | | |
106 | | - | |
107 | | - | |
108 | | - | |
| 108 | + | |
109 | 109 | | |
110 | 110 | | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
111 | 116 | | |
112 | 117 | | |
113 | 118 | | |
| |||
Lines changed: 36 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
652 | 652 | | |
653 | 653 | | |
654 | 654 | | |
| 655 | + | |
| 656 | + | |
| 657 | + | |
| 658 | + | |
| 659 | + | |
| 660 | + | |
| 661 | + | |
| 662 | + | |
| 663 | + | |
| 664 | + | |
| 665 | + | |
| 666 | + | |
| 667 | + | |
| 668 | + | |
| 669 | + | |
| 670 | + | |
| 671 | + | |
| 672 | + | |
| 673 | + | |
| 674 | + | |
| 675 | + | |
| 676 | + | |
| 677 | + | |
| 678 | + | |
| 679 | + | |
| 680 | + | |
| 681 | + | |
| 682 | + | |
| 683 | + | |
| 684 | + | |
| 685 | + | |
| 686 | + | |
| 687 | + | |
| 688 | + | |
| 689 | + | |
| 690 | + | |
655 | 691 | | |
656 | 692 | | |
657 | 693 | | |
| |||
0 commit comments