Repository navigation
Commit 116f05c
committed
[SPARK-60825][SQL] Fix missing validation of datetime partition values when ANSI mode is off
### What changes were proposed in this pull request?
This PR fixes `PartitioningUtils.castPartValueToDesiredType` to validate a `DATE`, `TIME` or timestamp partition value regardless of `spark.sql.ansi.enabled`, as SPARK-60797 did for `DECIMAL`.
```scala
case DateType =>
- Cast(Literal(value), DateType, Some(zoneId.getId)).eval()
- case tt: TimeType => Cast(Literal(unescapePathName(value)), tt).eval()
+ Cast(Literal(value), DateType, Some(zoneId.getId), ansiEnabled = true).eval()
+ case tt: TimeType => Cast(Literal(unescapePathName(value)), tt, ansiEnabled = true).eval()
// Timestamp types
case dt if AnyTimestampType.acceptsType(dt) || AnyTimestampNanoType.acceptsType(dt) =>
- Try {
- Cast(Literal(unescapePathName(value)), dt, Some(zoneId.getId)).eval()
- }.getOrElse {
- Cast(Cast(Literal(value), DateType, Some(zoneId.getId)), dt).eval()
- }
+ Cast(Literal(unescapePathName(value)), dt, Some(zoneId.getId), ansiEnabled = true).eval()
case it: AnsiIntervalType =>
- Cast(Literal(unescapePathName(value)), it).eval()
+ Cast(Literal(unescapePathName(value)), it, ansiEnabled = true).eval()
```
`Cast` is in ANSI mode so that a value that cannot be cast fails in any session. `parsePartitions` already handles that failure according to `spark.sql.sources.validatePartitionColumns`. For a valid value, the ANSI cast returns the same value as before, because each ANSI string parser is the non-ANSI one that throws an error instead of returning `NULL`. The interval case is in ANSI mode too, for consistency. A cast from a string to an interval type already fails in both modes, so it does not change.
The timestamp case no longer falls back to a cast to `DATE`. A cast from a string to a timestamp type already accepts a date without a time, such as `2020-01-01`, so the fallback mattered only for a value with a valid date and an invalid time. For `TIMESTAMP` and `TIMESTAMP_LTZ(p)`, it failed anyway, because the outer cast from `DATE` has no time zone. For `TIMESTAMP_NTZ` and `TIMESTAMP_NTZ(p)`, it returned the start of the day, so a value such as `2020-01-01 25:00:00` passed the validation in an ANSI session. Removing it is part of the fix, not a cleanup. With the first cast in ANSI mode, that cast fails in a non-ANSI session too, so a remaining fallback would make such a value the start of the day in every session.
### Why are the changes needed?
When a file source reads a partition directory with a user-specified `DATE`, `TIME` or timestamp type in a session with `spark.sql.ansi.enabled=false`, a value that cannot be cast to the type becomes `NULL`, even when `spark.sql.sources.validatePartitionColumns` is `true`, the default. In such a session, `Cast` returns `NULL` instead of failing, so `parsePartitions` never sees an error.
```scala
spark.conf.set("spark.sql.ansi.enabled", false)
spark.range(1).selectExpr("id AS v").write.parquet(s"$base/a=abc")
spark.read.schema("v LONG, a DATE").parquet(base).select("a").collect()
// Expected: INVALID_PARTITION_VALUE, Actual: [null]
```
The SQL migration guide has stated since Spark 3.0 that "An exception is thrown if the validation fails". `INT`, `BOOLEAN`, `DECIMAL` and interval partition values already fail in both modes.
This is a regression from SPARK-34314 in [v3.2.0](https://github.com/apache/spark/releases/tag/v3.2.0) (2021-10-06), which removed the `null` check after the cast. I reproduced it for `DATE`, `TIMESTAMP` and `TIMESTAMP_NTZ` with [v4.0.4](https://github.com/apache/spark/releases/tag/v4.0.4) (2026-07-12), [v4.1.3](https://github.com/apache/spark/releases/tag/v4.1.3) (2026-07-11), [v4.2.0](https://github.com/apache/spark/releases/tag/v4.2.0) (2026-07-11) and [v4.3.0](https://github.com/apache/spark/releases/tag/v4.3.0) (2026-10-27).
### Does this PR introduce _any_ user-facing change?
Yes. These are the results of reading each directory with `spark.sql.sources.validatePartitionColumns=true`.
| Directory | Type | Before, ANSI on | Before, ANSI off | After, both modes |
|---|---|---|---|---|
| `a=abc` | `DATE`, `TIME(6)`, `TIMESTAMP`, `TIMESTAMP_NTZ` | `INVALID_PARTITION_VALUE` | `NULL` | `INVALID_PARTITION_VALUE` |
| `a=2020-01-01 25%3A00%3A00` | `TIMESTAMP` | `INVALID_PARTITION_VALUE` | `NULL` | `INVALID_PARTITION_VALUE` |
| `a=2020-01-01 25%3A00%3A00` | `TIMESTAMP_NTZ` | `2020-01-01 00:00:00` | `NULL` | `INVALID_PARTITION_VALUE` |
With `spark.sql.sources.validatePartitionColumns=false`, all of them are `NULL` after this PR. Valid values, including a date without a time read as a timestamp type, are read as before in both modes. Partitions tracked in the catalog are not affected, because `CatalogTablePartition.toRow` casts their values separately.
The SQL migration guide describes this change.
### How was this patch tested?
Pass the CIs with the newly added test case, which fails without the fix. `FileIndexSuite` reads `a=foo` as `DATE`, `TIME(6)` and `TIMESTAMP`, and `a=2020-01-01 25%3A00%3A00` as `TIMESTAMP_NTZ`, in both ANSI modes, with `spark.sql.sources.validatePartitionColumns` on and off. It also reads `a=2020-01-01` as `TIMESTAMP_NTZ` in both ANSI modes and checks that the value is the start of the day.
```
$ build/sbt "sql/testOnly *FileIndexSuite -- -z SPARK-60825"
```
I also ran a temporary suite, which is not part of this PR. It reads 19 directory names as `DATE`, `TIME(6)`, `TIMESTAMP`, `TIMESTAMP_NTZ`, `TIMESTAMP_LTZ(9)`, `TIMESTAMP_NTZ(9)`, two interval types, `INT`, `BOOLEAN` and `DECIMAL(5,2)`, in both ANSI modes, with the validation on and off.
- After this PR, the two ANSI modes return the same result for all 418 combinations of directory, type and validation. Before this PR, they differed only for the datetime types.
- With ANSI mode on, the only results that changed are those of `TIMESTAMP_NTZ` and `TIMESTAMP_NTZ(9)` for the four directories with a valid date and an invalid time, such as `a=2020-01-01 xyz`.
- With ANSI mode off, the only results that changed are `NULL` values that became `INVALID_PARTITION_VALUE` with the validation on.
It also casts 32 values to each timestamp type, with the cast in this PR and with the removed fallback alone. The fallback returned a value only for `TIMESTAMP_NTZ` and `TIMESTAMP_NTZ(9)`. The cast in this PR accepts each of those values, such as `2020`, `2020-1-1` or `2020-01-01 12%3A34`, except the values whose time part does not parse, such as `2020-01-01T` or `2020-01-01 UTC`.
### Was this patch authored or co-authored using generative AI tooling?
Generated-by: Claude Opus 5.5
Closes #60043 from dongjoon-hyun/SPARK-60825.
Authored-by: Dongjoon Hyun <dongjoon@apache.org>
Signed-off-by: Dongjoon Hyun <dongjoon@apache.org>1 parent 718e56f commit 116f05c
3 files changed
Lines changed: 67 additions & 11 deletions
File tree
- docs
- sql/core/src
- main/scala/org/apache/spark/sql/execution/datasources
- test/scala/org/apache/spark/sql/execution/datasources
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
31 | 31 | | |
32 | 32 | | |
33 | 33 | | |
| 34 | + | |
34 | 35 | | |
35 | 36 | | |
36 | 37 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
588 | 588 | | |
589 | 589 | | |
590 | 590 | | |
591 | | - | |
592 | | - | |
| 591 | + | |
| 592 | + | |
| 593 | + | |
593 | 594 | | |
594 | 595 | | |
595 | | - | |
596 | | - | |
| 596 | + | |
| 597 | + | |
597 | 598 | | |
598 | 599 | | |
599 | | - | |
600 | | - | |
601 | | - | |
602 | | - | |
603 | | - | |
| 600 | + | |
604 | 601 | | |
605 | | - | |
| 602 | + | |
606 | 603 | | |
607 | 604 | | |
608 | 605 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
19 | 19 | | |
20 | 20 | | |
21 | 21 | | |
| 22 | + | |
22 | 23 | | |
23 | 24 | | |
24 | 25 | | |
| |||
37 | 38 | | |
38 | 39 | | |
39 | 40 | | |
40 | | - | |
| 41 | + | |
41 | 42 | | |
42 | 43 | | |
43 | 44 | | |
| |||
215 | 216 | | |
216 | 217 | | |
217 | 218 | | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
218 | 276 | | |
219 | 277 | | |
220 | 278 | | |
| |||
0 commit comments