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 5c6c6f40220d [SPARK-57634][SQL] Support TIME type inference in XML
data source
5c6c6f40220d is described below
commit 5c6c6f40220d9f0d26c7ae69c5e64792707e743f
Author: Shrirang Mhalgi <[email protected]>
AuthorDate: Thu Jun 25 10:31:08 2026 +0200
[SPARK-57634][SQL] Support TIME type inference in XML data source
### What changes were proposed in this pull request?
Add TIME type inference to `XmlInferSchema`, completing format parity with
CSV and JSON data sources (SPARK-57572).
### Why are the changes needed?
After SPARK-57572 added TIME inference to CSV and JSON, XML was the only
text-based format without TIME type inference. This was explicitly noted as a
follow-up in that PR's review.
### Does this PR introduce _any_ user-facing change?
Yes. When `spark.sql.timeType.enabled=true` (default), XML values matching
the time format (e.g. `13:31:24.123456`) are now inferred as `TimeType` instead
of `TimestampType`
### How was this patch tested?
Added 4 tests to XmlInferSchemaSuite:
- Basic `TIME` inference
- Disabled when `timeType.enabled=false`
- Negative cases (date/timestamp not misclassified)
- Cross-row merge (`TIME + TIME`, `TIME + string`)
### Was this patch authored or co-authored using generative AI tooling?
Co-Authored using Claude Opus 4.6
Closes #56697 from shrirangmhalgi/SPARK-57634-xml-time-type-inference.
Authored-by: Shrirang Mhalgi <[email protected]>
Signed-off-by: Max Gekk <[email protected]>
---
.../spark/sql/catalyst/xml/XmlInferSchema.scala | 11 ++++-
.../sql-tests/results/xml-functions.sql.out | 4 +-
.../datasources/xml/XmlInferSchemaSuite.scala | 56 +++++++++++++++++++++-
3 files changed, 67 insertions(+), 4 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 a0139e98a266..d6a686659589 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
@@ -38,7 +38,7 @@ import org.apache.spark.internal.Logging
import org.apache.spark.rdd.RDD
import org.apache.spark.sql.catalyst.analysis.TypeCoercion
import org.apache.spark.sql.catalyst.expressions.ExprUtils
-import org.apache.spark.sql.catalyst.util.{DateFormatter, DropMalformedMode,
FailFastMode, ParseMode, PermissiveMode, TimestampFormatter}
+import org.apache.spark.sql.catalyst.util.{DateFormatter, DropMalformedMode,
FailFastMode, ParseMode, PermissiveMode, TimeFormatter, TimestampFormatter}
import org.apache.spark.sql.catalyst.util.LegacyDateFormats.FAST_DATE_FORMAT
import org.apache.spark.sql.errors.QueryExecutionErrors
import org.apache.spark.sql.internal.{LegacyBehaviorPolicy, SQLConf}
@@ -73,6 +73,10 @@ class XmlInferSchema(private val options: XmlOptions,
private val caseSensitive:
legacyFormat = FAST_DATE_FORMAT,
isParsing = true)
+ private lazy val timeFormatter = TimeFormatter(options.timeFormatInRead,
isParsing = true)
+
+ private val isTimeTypeEnabled = SQLConf.get.isTimeTypeEnabled
+
override def equals(obj: Any): Boolean = obj match {
case other: XmlInferSchema =>
options == other.options &&
@@ -324,6 +328,7 @@ class XmlInferSchema(private val options: XmlOptions,
private val caseSensitive:
case v if options.prefersDecimal && decimalTry.isDefined =>
decimalTry.get
case v if isDouble(v) => DoubleType
case v if isBoolean(v) => BooleanType
+ case v if isTimeTypeEnabled && isTime(v) =>
TimeType(TimeType.DEFAULT_PRECISION)
case v if isDate(v) => DateType
case v if timestampNTZTry.isDefined => timestampNTZTry.get
case v if isTimestamp(v) => TimestampType
@@ -549,6 +554,10 @@ class XmlInferSchema(private val options: XmlOptions,
private val caseSensitive:
(allCatch opt dateFormatter.parse(value)).isDefined
}
+ private def isTime(value: String): Boolean = {
+ (allCatch opt timeFormatter.parse(value)).isDefined
+ }
+
/**
* Convert NullType to StringType and remove StructTypes with no fields
*/
diff --git
a/sql/core/src/test/resources/sql-tests/results/xml-functions.sql.out
b/sql/core/src/test/resources/sql-tests/results/xml-functions.sql.out
index eaac972cb2fe..732d1f9e6971 100644
--- a/sql/core/src/test/resources/sql-tests/results/xml-functions.sql.out
+++ b/sql/core/src/test/resources/sql-tests/results/xml-functions.sql.out
@@ -636,7 +636,7 @@ select
schema_of_xml('<record><time>14:30:45</time></record>', map('rowTag', 're
-- !query schema
struct<schema_of_xml(<record><time>14:30:45</time></record>):string>
-- !query output
-STRUCT<time: TIMESTAMP>
+STRUCT<time: TIME(6)>
-- !query
@@ -644,7 +644,7 @@ select
schema_of_xml('<record><time>14:30:45.123456</time></record>', map('rowTa
-- !query schema
struct<schema_of_xml(<record><time>14:30:45.123456</time></record>):string>
-- !query output
-STRUCT<time: TIMESTAMP>
+STRUCT<time: TIME(6)>
-- !query
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 b013b56ccd75..bd675bd62510 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
@@ -28,13 +28,16 @@ import org.apache.spark.sql.test.SharedSparkSession
import org.apache.spark.sql.types.{
ArrayType,
BooleanType,
+ DateType,
DecimalType,
DoubleType,
IntegerType,
LongType,
StringType,
StructField,
- StructType
+ StructType,
+ TimestampType,
+ TimeType
}
class XmlInferSchemaSuite
@@ -646,6 +649,57 @@ class XmlInferSchemaSuite
assert(xmlDF.schema === expectedSchema)
checkAnswer(xmlDF, expectedAns)
}
+
+ test("TIME type inference") {
+ val xmlString = Seq("""<ROW><t>13:31:24.123456</t></ROW>""")
+ val df = readData(xmlString)
+ assert(df.schema === new StructType().add("t",
TimeType(TimeType.DEFAULT_PRECISION)))
+ checkAnswer(df, Row(java.time.LocalTime.of(13, 31, 24, 123456000)))
+ }
+
+ test("TIME type inference - disabled when timeType.enabled is false") {
+ withSQLConf(SQLConf.TIME_TYPE_ENABLED.key -> "false") {
+ val xmlString = Seq("""<ROW><t>13:31:24</t></ROW>""")
+ val df = readData(xmlString)
+ // Falls through to TimestampType when TIME inference is disabled
+ assert(df.schema.fields.head.dataType === TimestampType)
+ }
+ }
+
+ test("TIME type inference - negative cases") {
+ // Date strings should not infer as TIME
+ val xmlDate = Seq("""<ROW><t>2024-01-15</t></ROW>""")
+ val dfDate = readData(xmlDate)
+ assert(dfDate.schema.fields.head.dataType === DateType)
+
+ // Timestamp strings should not infer as TIME
+ val xmlTs = Seq("""<ROW><t>2024-01-15T13:31:24</t></ROW>""")
+ val dfTs = readData(xmlTs)
+ assert(dfTs.schema.fields.head.dataType !=
TimeType(TimeType.DEFAULT_PRECISION))
+ }
+
+ test("TIME type inference - cross-row merge") {
+ // TIME + TIME -> TIME
+ val xmlTime = Seq(
+ """<ROW><t>13:31:24</t></ROW>""",
+ """<ROW><t>09:15:00.123</t></ROW>""")
+ val dfTime = readData(xmlTime)
+ assert(dfTime.schema === new StructType().add("t",
TimeType(TimeType.DEFAULT_PRECISION)))
+
+ // TIME + non-time string -> StringType
+ val xmlMixed = Seq(
+ """<ROW><t>13:31:24</t></ROW>""",
+ """<ROW><t>not-a-time</t></ROW>""")
+ val dfMixed = readData(xmlMixed)
+ assert(dfMixed.schema.fields.head.dataType === StringType)
+
+ // TIME + Date -> StringType (incompatible types widen)
+ val xmlTimeDate = Seq(
+ """<ROW><t>13:31:24</t></ROW>""",
+ """<ROW><t>2024-01-15</t></ROW>""")
+ val dfTimeDate = readData(xmlTimeDate)
+ assert(dfTimeDate.schema.fields.head.dataType === StringType)
+ }
}
trait XmlSchemaInferenceCaseSensitivityTests extends QueryTest {
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]