HyukjinKwon commented on code in PR #59406:
URL: https://github.com/apache/spark/pull/59406#discussion_r4274637665
##########
docs/sql-data-sources-xml.md:
##########
@@ -184,7 +184,14 @@ Data source options of XML can be set via:
<tr>
<td><code>dateFormat</code></td>
<td><code>yyyy-MM-dd</code></td>
- <td>Sets the string that indicates a date format. Custom date formats
follow the formats at <a
href="https://spark.apache.org/docs/latest/sql-ref-datetime-pattern.html">
datetime pattern</a>. This applies to date type.</td>
+ <td>Sets the string that indicates a date format. Custom date formats
follow the formats at <a
href="https://spark.apache.org/docs/latest/sql-ref-datetime-pattern.html">
datetime pattern</a>. This applies to date type.
+ With <code>spark.sql.legacy.timeParserPolicy=LEGACY</code>, an explicit
format and a Gregorian
+ <code>locale</code>, XML date reads try ISO dates before this pattern for
spark-xml compatibility.
+ Non-Gregorian calendars continue to use the configured pattern. Writes
still use this pattern,
+ so ISO-ambiguous formats such as <code>yyyy-dd-MM</code> do not
necessarily round-trip under
+ <code>LEGACY</code>. <code>CORRECTED</code> and <code>EXCEPTION</code>
keep configured-format
+ precedence; <code>EXCEPTION</code> does not report differences when both
interpretations
Review Comment:
**Nit (P3):** This caveat, and the matching sentence in
`docs/sql-migration-guide.md:35`, covers only the case where both
interpretations parse. Under `LEGACY`, the ISO pre-pass also accepts ISO-shaped
values that the configured pattern rejects, and `EXCEPTION` doesn't report
those either.
For example, take `dateFormat=dd/MM/yyyy` and `<d>2024-03-04</d>`:
- `LEGACY` returns 2024-03-04 from `parseIsoDate`.
- Under `EXCEPTION`, the java.time parser fails.
`DateTimeFormatterHelper.checkParsedDiff` then retries only the
configured-pattern `FastDateFormat`, which also rejects the value; I checked
`dd/MM/yyyy`, `yyyyMMdd` and `dd.MM.yyyy` with commons-lang3 3.17.0. The
original parse error is rethrown, so the value becomes NULL or a corrupt record
in `PERMISSIVE` and an error in `FAILFAST`, with no `SparkUpgradeException`.
A user who switches from `LEGACY` to `EXCEPTION` to find changed results
would get silent NULLs for these values. The code matches the existing CSV/JSON
LEGACY fallback, which has the same `EXCEPTION` blind spot, so only the wording
needs to change. For example, in both docs: "`EXCEPTION` does not report XML
ISO-first differences, including ISO dates that the configured pattern
rejects." A test with a non-ISO `dateFormat` and an ISO-shaped value across the
three policies would pin this boundary.
**Verification:**
- **Inspection:** The XML dateFormat option text and the migration note
describe the EXCEPTION boundary as covering every LEGACY ISO-first divergence,
including ISO-only values, and match each other.
- **Behavior:** XML datasource coverage with a non-ISO dateFormat (e.g.
dd/MM/yyyy) and an ISO-shaped value asserts LEGACY returns the ISO date, and
CORRECTED and EXCEPTION yield NULL in PERMISSIVE and fail in FAILFAST without a
SparkUpgradeException.
- **Compatibility:** The existing SPARK-60201 policy and mode expectations
for yyyy-dd-MM still hold unchanged.
##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/xml/StaxXmlParser.scala:
##########
@@ -1172,6 +1180,23 @@ class XmlTokenizer(
}
object StaxXmlParser {
+ private[xml] def parseIsoDate(value: String): Option[Int] = {
+ if (value.nonEmpty && Character.isDigit(value.head)) {
+ val position = new ParsePosition(0)
+ val parsed = DateTimeFormatter.ISO_DATE.parseUnresolved(value, position)
Review Comment:
**Nit (P3):** The `Try` on line 1188 doesn't cover `parseUnresolved`. That
method returns `null` for ordinary parse errors, but its contract also declares
`@throws DateTimeException`, and `ISO_DATE`'s offset parser throws when the
offset is out of range. For example, `2020-01-01+25:00` throws
`DateTimeException: Value out of range: Hour[0-23], Minute[0-59],
Second[0-59]`; I reproduced this on JDK 11, 17 and 25.
The exception escapes `parseIsoDate`, so `parseXmlDate` never reaches
`dateFormatter.parse`. Under `LEGACY` with `dateFormat=yyyy-MM-dd` or
`yyyy-dd-MM`, the merge target reads this value as 2020-01-01, because
`FastDateFormat` ignores the trailing text. With this change, the record
handler (lines 245-250) turns the exception into a `BadRecordException`. The
row becomes a corrupt record in `PERMISSIVE`, is dropped in `DROPMALFORMED`,
and fails the read in `FAILFAST`. This affects only `LEGACY` reads with an
explicit `dateFormat` and a Gregorian locale; `CORRECTED` and `EXCEPTION` never
run the pre-check.
These are invalid `xs:date` values, so this should be rare. Still, every
value the ISO path can't accept is supposed to fall back to the configured
pattern. Catching `DateTimeException` around `parseUnresolved` keeps that
fallback total, and it combines naturally with the range-check change suggested
on line 1188.
See **Shared repair plan 1** in the review body.
##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/xml/StaxXmlParser.scala:
##########
@@ -1172,6 +1180,23 @@ class XmlTokenizer(
}
object StaxXmlParser {
+ private[xml] def parseIsoDate(value: String): Option[Int] = {
+ if (value.nonEmpty && Character.isDigit(value.head)) {
+ val position = new ParsePosition(0)
+ val parsed = DateTimeFormatter.ISO_DATE.parseUnresolved(value, position)
+ if (parsed != null && position.getIndex == value.length) {
+ Try(LocalDate.parse(value, DateTimeFormatter.ISO_DATE)).toOption.map {
date =>
Review Comment:
**Nit (P3):** The new `parseUnresolved` pre-check avoids the exception for
non-ISO shapes. Values that are ISO-shaped but invalid still depend on
`Try(LocalDate.parse(...))` to reject them, though, so each one builds and
throws a `DateTimeParseException` with a stack trace.
With `yyyy-dd-MM`, the ISO-ambiguous pattern this PR targets, every value
whose day is 13-31 looks like an ISO date with an invalid month, for example
`2024-31-01`. That is 19 of 31 possible days, so in such a column most values
pay for an exception before the configured pattern parses them. In a local JDK
17 microbenchmark of this pre-check at stack depth 60, `2024-31-01` took about
6.5 us, compared with about 0.7 us for `2024-03-04`. Executors with deeper
stacks pay more. Parsed results don't change, and this applies only to `LEGACY`
reads with an explicit `dateFormat` and a Gregorian locale.
The description says the pre-check doesn't throw for routine inputs, which
is how the earlier per-value cost question was answered. `parseUnresolved`
already returns the parsed fields. Range-checking them before building the date
(month 1-12, and a day within that month's length for the parsed year) would
let these values return `None` without throwing.
See **Shared repair plan 1** in the review body.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]