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

cloud-fan 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 9d760a91baa1 [SPARK-57884][SQL] Make XML schema inference honor 
preferDate, consistent with CSV
9d760a91baa1 is described below

commit 9d760a91baa140fcf517a6b754ba75df3eab0f1f
Author: Wenchen Fan <[email protected]>
AuthorDate: Mon Jul 13 01:24:59 2026 +0800

    [SPARK-57884][SQL] Make XML schema inference honor preferDate, consistent 
with CSV
    
    ### What changes were proposed in this pull request?
    
    Make XML schema inference honor the `preferDate` option as a gate on 
whether date inference is attempted, matching `CSVInferSchema`.
    
    Currently, `XmlInferSchema.tryParseDouble` and `tryParseTime` fall through 
to `tryParseDate(field)` **unconditionally**, ignoring `options.preferDate`. As 
a result, with `preferDate=false`, a value that matches the (default) date 
format is still inferred as `DateType`. This contradicts the option's purpose — 
`preferDate=false` is meant to disable date inference (date/timestamp inference 
is ambiguous, and the option lets users opt out) — and diverges from the CSV 
datasource, whose `try [...]
    
    This change routes the date attempt through `preferDate`: when `preferDate` 
is true, a bare date still infers as `DateType`; when false, date inference is 
skipped and the value falls through to timestamp inference. TIME inference is 
unaffected (it remains tried ahead of date/timestamp regardless of 
`preferDate`, unchanged from before).
    
    ### Why are the changes needed?
    
    `preferDate` is documented and structured (see 
`XmlOptions.dateFormatInRead`, which is conditioned on `preferDate`) as 
controlling whether date inference happens, but the `tryParse*` control flow 
does not honor it, so a bare ISO date leaks through as `DateType` regardless. 
The CSV datasource — which XML's inference cascade mirrors — already gates date 
inference on `preferDate`. This aligns XML with CSV so `preferDate=false` 
behaves consistently across the two text datasources.
    
    ### Does this PR introduce _any_ user-facing change?
    
    Yes. With `preferDate=false`, XML schema inference no longer infers 
`DateType` for date-shaped values; such values now fall through to timestamp 
inference (as they already do in CSV). With the default `preferDate=true`, 
behavior is unchanged.
    
    ### How was this patch tested?
    
    Updated the `preferDate` tests in `XmlInferSchemaSuite` (core) and 
`XmlInferSchemaTypeCastingSuite` (catalyst) to assert the gated behavior: 
`preferDate=true` -> `DateType`, `preferDate=false` -> `TimestampType`. 
Existing XML inference suites pass.
    
    ### Was this patch authored or co-authored using generative AI tooling?
    
    Generated-by: Claude Code (Anthropic Claude Opus)
    
    Closes #56966 from cloud-fan/SPARK-preferDate-xml-followup.
    
    Authored-by: Wenchen Fan <[email protected]>
    Signed-off-by: Wenchen Fan <[email protected]>
---
 AGENTS.md                                          | 33 ++++++++++++++----
 .../spark/sql/catalyst/xml/XmlInferSchema.scala    |  8 +++--
 .../xml/XmlInferSchemaTypeCastingSuite.scala       | 39 ++++++++++++++++++++--
 .../datasources/xml/XmlInferSchemaSuite.scala      | 15 +++++----
 4 files changed, 76 insertions(+), 19 deletions(-)

diff --git a/AGENTS.md b/AGENTS.md
index 433cff6ccacd..16b9976caba5 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -20,6 +20,15 @@ Spark Connect protocol is defined in proto files under 
`sql/connect/common/src/m
 
 Avoid introducing non-ASCII characters in code or comments. String literals 
may contain non-ASCII when the content requires it (error messages, test data, 
etc.). Identifiers are ASCII by convention. The common failure mode is 
typographic characters (em-dash, smart quotes, ellipsis, non-breaking space) 
sneaking into comments; scalastyle flags some of these. Spot-check before 
committing: `grep -rn -P "[^\x00-\x7F]" <files>`.
 
+Keep source lines within 100 characters — the linters enforce this for Scala, 
Java, and Python, and LLMs commonly overrun it in comments and long 
expressions. A quick scan of just the changed files catches most cases in 
seconds, far cheaper than a CI round trip:
+
+    { git diff --name-only --diff-filter=ACM HEAD; git ls-files --others 
--exclude-standard; } \
+      | grep -E '\.(scala|java|py)$' | sort -u \
+      | xargs -r awk 'length>100 && $0 !~ /^[[:space:]]*(import|package) / && 
$0 !~ /https?:\/\// \
+          {print FILENAME":"FNR": "length" chars"}'
+
+This is only a hint: it approximates the linters' exemptions (imports, URLs) 
rather than matching them exactly, so it can over- or under-report. The linters 
remain the source of truth.
+
 ## Scala Test Base Classes
 
 When writing a new Scala test suite, pick the lowest base class that provides 
what the test actually needs. Spark uses the `AnyFunSuite` ScalaTest style 
throughout, so the bases below are the chain to choose from. Each adds 
capability on top of the previous:
@@ -147,22 +156,32 @@ Run a single test case:
 
 ## Investigating PR CI Failures
 
-Do NOT download full job logs to grep for errors — they are very large and 
slow. Instead, use the test report annotations on the fork.
+Enumerate all failing check runs first, then drill into each by type. Do not 
assume a single failure: a PR can fail tests, linters, and the build at once, 
and these surface through different channels.
 
 Step 1 — Get the fork owner and the latest commit SHA of the PR:
 
     gh api repos/apache/spark/pulls/<PR_NUMBER> --jq '{owner: 
.head.repo.owner.login, sha: .head.sha}'
 
-Step 2 — Find the "Report test results" check run on the fork's commit:
+Step 2 — List every failing check run on the fork's commit. This is the 
complete failure set:
+
+    gh api repos/<OWNER>/spark/commits/<SHA>/check-runs --paginate \
+      --jq '.check_runs[] | select(.conclusion == "failure") | {name, id: .id}'
+
+A passing (or absent) "Report test results" does NOT mean CI is green. That 
check aggregates only test-case failures; linter, license, dependency, MiMa, 
compile, and doc-build failures are separate check runs that produce no test 
annotations. Always work from the list in Step 2, not from any single check.
+
+Step 3 — Drill into each failure according to its kind:
+
+- **Test jobs** (e.g. "Report test results", "Build modules: ..."): fetch 
failure annotations. Each annotation contains the test class, test name, and 
failure message:
 
-    gh api repos/<OWNER>/spark/commits/<SHA>/check-runs \
-      --jq '.check_runs[] | select(.name == "Report test results") | {id: .id, 
annotations: .output.annotations_count}'
+      gh api repos/<OWNER>/spark/check-runs/<CHECK_RUN_ID>/annotations
 
-Step 3 — Fetch failure annotations:
+- **Non-test jobs** (e.g. "Linters, licenses, and dependencies", "Build"): 
find the failed step, then read only that job's log:
 
-    gh api repos/<OWNER>/spark/check-runs/<CHECK_RUN_ID>/annotations
+      gh api repos/<OWNER>/spark/actions/jobs/<JOB_ID> \
+        --jq '{name, steps: [.steps[] | select(.conclusion == "failure") | 
.name]}'
+      gh api repos/<OWNER>/spark/actions/jobs/<JOB_ID>/logs
 
-Each annotation contains the test class, test name, and failure message.
+Avoid downloading the large per-shard *test* job logs — they are very large 
and slow; use the annotations for those. Lint, license, dependency, and build 
job logs are small and fine to read directly when a step fails.
 
 ## Checking PR Merge Status
 
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 fb534062ec28..b4c942326702 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
@@ -682,16 +682,20 @@ class XmlInferSchema(private val options: XmlOptions, 
private val caseSensitive:
     } else if (isTimeTypeEnabled && isTime(field)) {
       // TIME is tried ahead of date/timestamp, matching the ordering of the 
previous cascade.
       TimeType(TimeType.DEFAULT_PRECISION)
-    } else {
+    } else if (options.preferDate) {
       tryParseDate(field)
+    } else {
+      tryParseTimestampNTZ(field)
     }
   }
 
   private def tryParseTime(field: String): DataType = {
     if (isTimeTypeEnabled && isTime(field)) {
       TimeType(TimeType.DEFAULT_PRECISION)
-    } else {
+    } else if (options.preferDate) {
       tryParseDate(field)
+    } else {
+      tryParseTimestampNTZ(field)
     }
   }
 
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 c78171d61565..e5570ddde88f 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
@@ -155,11 +155,44 @@ class XmlInferSchemaTypeCastingSuite extends 
SparkFunSuite with SQLHelper {
     assert(inferSchema.inferFrom("2024-01-15T10:00:00", DateType) == 
TimestampType)
   }
 
-  test("date is inferred regardless of preferDate") {
+  test("preferDate gates date inference (consistent with CSV)") {
+    // preferDate controls whether date inference is attempted, matching 
CSVInferSchema: when true
+    // a bare date infers as DateType; when false date inference is skipped 
and the value falls
+    // through to timestamp inference.
+    assert(newInferSchema(Map("preferDate" -> "true"))
+      .inferFrom("2024-01-15", NullType) == DateType)
+    assert(newInferSchema(Map("preferDate" -> "false"))
+      .inferFrom("2024-01-15", NullType) == TimestampType)
+  }
+
+  test("preferDate gates the incremental temporal re-entry (tryParseTime)") {
+    // Refining a temporal `typeSoFar` re-enters the cascade at 
`tryParseTime`, which must honor
+    // `preferDate` just like the fresh path (`tryParseDouble`). With a 
`Date`-so-far field seeing
+    // another date-only value: preferDate=true re-infers `Date` and the merge 
stays `Date`;
+    // preferDate=false skips date inference so the value falls through to 
`Timestamp`, and the
+    // merge widens `Date` to `Timestamp`. This guards the `tryParseTime` 
branch, which the
+    // incremental-vs-legacy parity test does not cover (it runs only at the 
default
+    // preferDate=true).
+    assert(newInferSchema(Map("preferDate" -> "true"))
+      .inferFrom("2024-01-15", DateType) == DateType)
+    assert(newInferSchema(Map("preferDate" -> "false"))
+      .inferFrom("2024-01-15", DateType) == TimestampType)
+  }
+
+  test("TIME inference precedes the preferDate gate") {
+    // TIME is tried ahead of date/timestamp in both `tryParseDouble` (fresh 
path) and
+    // `tryParseTime` (incremental temporal re-entry), so a TIME-shaped value 
infers as `TimeType`
+    // regardless of `preferDate`. This guards against a refactor that would 
move the TIME guard
+    // inside the `preferDate` branch and silently drop TIME inference when 
preferDate=false.
+    val time = TimeType(TimeType.DEFAULT_PRECISION)
     Seq("true", "false").foreach { preferDate =>
       val inferSchema = newInferSchema(Map("preferDate" -> preferDate))
-      assert(inferSchema.inferFrom("2024-01-15", NullType) == DateType,
-        s"expected DateType with preferDate=$preferDate")
+      // Fresh path (typeSoFar == NullType, enters via tryParseDouble).
+      assert(inferSchema.inferFrom("10:00:00", NullType) == time,
+        s"expected TimeType with preferDate=$preferDate")
+      // Incremental temporal re-entry (typeSoFar == TimeType, enters via 
tryParseTime).
+      assert(inferSchema.inferFrom("10:00:00", time) == time,
+        s"expected TimeType with preferDate=$preferDate")
     }
   }
 }
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 78755d531cd8..2c72825167f0 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
@@ -722,14 +722,15 @@ class XmlInferSchemaSuite
     assert(readData(doubleThenLong).schema.fields.head.dataType === DoubleType)
   }
 
-  test("date is inferred regardless of preferDate") {
+  test("preferDate gates date inference (consistent with CSV)") {
     val xmlDate = Seq("""<ROW><d>2024-01-15</d></ROW>""")
-    // preferDate governs which date formatter is used, not whether date 
inference is attempted.
-    Seq("true", "false").foreach { preferDate =>
-      val df = readData(xmlDate, Map("preferDate" -> preferDate))
-      assert(df.schema.fields.head.dataType === DateType,
-        s"expected DateType with preferDate=$preferDate")
-    }
+    // preferDate controls whether date inference is attempted, matching 
CSVInferSchema: when true
+    // a bare date infers as DateType; when false date inference is skipped 
and the value falls
+    // through to timestamp inference.
+    assert(readData(xmlDate, Map("preferDate" -> "true"))
+      .schema.fields.head.dataType === DateType)
+    assert(readData(xmlDate, Map("preferDate" -> "false"))
+      .schema.fields.head.dataType === TimestampType)
   }
 
   test("incremental type casting yields the same schema as the legacy batch 
path") {


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

Reply via email to