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 3791cb906879 [SPARK-57303][SQL] Store-assignment and up-cast rules for
nanosecond-precision timestamp types
3791cb906879 is described below
commit 3791cb9068795d42d3bb5b4c4ea9b99563181085
Author: Maxim Gekk <[email protected]>
AuthorDate: Fri Jun 26 21:35:02 2026 +0200
[SPARK-57303][SQL] Store-assignment and up-cast rules for
nanosecond-precision timestamp types
### What changes were proposed in this pull request?
This PR defines a precision-safe store-assignment / up-cast contract for
the whole LTZ/NTZ timestamp family - the microsecond types (`TIMESTAMP` /
`TIMESTAMP_NTZ`) and their nanosecond-precision counterparts
(`TIMESTAMP_LTZ(p)` / `TIMESTAMP_NTZ(p)`, `p` in `[7, 9]`) - using a single
notion of effective fractional-second precision (micros = 6, nanos = `p`).
For any ordered pair of timestamp-family types (including across the
LTZ/NTZ boundary, which Spark already treats as a mutual up-cast for the micro
types):
- target precision `>=` source precision: lossless widening -> up-cast
(STRICT) and ANSI-store-assignable;
- target precision `<` source precision: lossy narrowing -> not an up-cast,
blocked under ANSI so it can never silently truncate.
`DATE <-> nanos` is aligned to the micro `DATE <-> TIMESTAMP` behavior:
`DATE -> nanos` is a lossless widening (up-cast + ANSI-store-assignable), while
`nanos -> DATE` drops the time-of-day (not an up-cast, but still
ANSI-store-assignable). LEGACY policy and explicit `CAST` are unchanged (they
still truncate on narrowing). `TIME <-> timestamp` is unchanged and stays
consistent with `TIME <-> micros` (never an up-cast, ANSI-store-assignable both
ways).
Concretely:
- New shared `private[sql] object TimestampFamily` (`sql/api`) with
`fractionalPrecision(dt): Option[Int]` plus `isLtz` / `isNtz`, reused across
the rule sites (no type-hierarchy change, so no MiMa impact).
- `UpCastRule.canUpCast`: a single lossless-widening arm for the family
(subsuming the existing `TimestampType <-> TimestampNTZType` cases), plus a
generalized `DATE -> family` widening arm.
- `Cast.canANSIStoreAssign`: replaced the piecemeal per-subtask arms with
one family narrowing block built on the shared helper, before the generic
`DatetimeType` arm.
- `TypeCoercionHelper.findWiderDateTimeType`: refactored onto the shared
helper (behavior-preserving) and updated the now-stale comment, since
common-type resolution and the cast rules now agree on admissibility.
### Why are the changes needed?
Before this change, the nanosecond timestamp types fell through the generic
`(_: DatetimeType, _: DatetimeType)` arm in `Cast.canANSIStoreAssign` (risking
silent sub-microsecond truncation handled only narrowly), and they were absent
from `UpCastRule.canUpCast`, so STRICT store assignment and up-cast resolution
rejected even lossless widening. This PR gives the family a complete,
precision-safe contract consistent with the microsecond precedent.
### Does this PR introduce _any_ user-facing change?
No. The nanosecond-precision timestamp types are unreleased (`Unstable`),
so this only affects behavior within the unreleased branch.
### How was this patch tested?
- Updated the `SPARK-57293` / `SPARK-57490` / cross-family / micro-boundary
contract tests in `CastSuiteBase` to the precision-safe widening model.
- Added a full-matrix predicate test over all 8 timestamp-family types
asserting `canUpCast` and `canANSIStoreAssign` are true iff target precision
`>=` source precision, plus `DATE` and `TIME` consistency anchors.
- Ran `CastSuite`, `CastWithAnsiOn/Off`, `TypeCoercionSuite`,
`AnsiTypeCoercionSuite`, `DataTypeWriteCompatibilitySuite`,
`V2WriteAnalysisSuite`, and `SQLQueryTestSuite` (cast / try_cast / nanos /
typeCoercion) - all pass with no golden-file changes.
- Added coverage for two downstream consumers of `canUpCast` that the
predicate-level tests do not reach: a `CastSuiteBase` test that
`Cast.nullable`'s try-cast branch follows up-cast admissibility for the
timestamp family (non-null preserved on widening, conservatively nullable on
narrowing), and a new `GeneratedColumnExpressionSuite` asserting
`GeneratedColumnExpression.validate` accepts a lossless widening generation
expression and rejects a lossy narrowing.
### Note on scope
`canUpCast` / `canANSIStoreAssign` feed several consumers beyond up-cast
resolution and store assignment (generated-column validation, subquery
decorrelation, V2 expression pushdown, `Cast` try-cast nullability, and the
Spark Connect `ArrowVectorReader` guard). The widening relaxation here is
lossless and applies uniformly to all of them. One follow-up item: nanosecond
timestamp types are not yet supported over Spark Connect (no `ConnectTypeOps` /
vector reader), so `ArrowVectorReader [...]
### Was this patch authored or co-authored using generative AI tooling?
Generated-by: Cursor (Claude Opus 4.8)
Closes #56810 from MaxGekk/nanos-store-assignment.
Authored-by: Maxim Gekk <[email protected]>
Signed-off-by: Max Gekk <[email protected]>
---
.../apache/spark/sql/types/TimestampFamily.scala | 49 +++++++
.../org/apache/spark/sql/types/UpCastRule.scala | 18 ++-
.../sql/catalyst/analysis/TypeCoercionHelper.scala | 33 ++---
.../spark/sql/catalyst/expressions/Cast.scala | 31 +----
.../sql/catalyst/expressions/CastSuiteBase.scala | 141 ++++++++++++++++-----
.../logical/GeneratedColumnExpressionSuite.scala | 50 ++++++++
6 files changed, 240 insertions(+), 82 deletions(-)
diff --git
a/sql/api/src/main/scala/org/apache/spark/sql/types/TimestampFamily.scala
b/sql/api/src/main/scala/org/apache/spark/sql/types/TimestampFamily.scala
new file mode 100644
index 000000000000..c10670b14c3b
--- /dev/null
+++ b/sql/api/src/main/scala/org/apache/spark/sql/types/TimestampFamily.scala
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.spark.sql.types
+
+/**
+ * Shared classification helpers for the LTZ/NTZ timestamp family: the
microsecond types
+ * ([[TimestampType]] / [[TimestampNTZType]]) and their nanosecond-precision
counterparts
+ * ([[TimestampLTZNanosType]] / [[TimestampNTZNanosType]]). Centralizes the
notion of effective
+ * fractional-second precision and time-zone family so that up-cast resolution
([[UpCastRule]]),
+ * ANSI store assignment, and common-type resolution all agree.
+ */
+private[sql] object TimestampFamily {
+
+ /**
+ * The effective fractional-second precision of a timestamp-family type, or
[[None]] for types
+ * that are not on the timestamp fractional-precision axis (DATE, TIME, and
everything else).
+ * The microsecond types [[TimestampType]] / [[TimestampNTZType]] have
precision 6; the
+ * nanosecond types carry their own precision `p` in [7, 9].
+ */
+ def fractionalPrecision(dt: DataType): Option[Int] = dt match {
+ case TimestampType | TimestampNTZType => Some(6)
+ case t: TimestampLTZNanosType => Some(t.precision)
+ case t: TimestampNTZNanosType => Some(t.precision)
+ case _ => None
+ }
+
+ /** Whether `dt` is a local-time-zone (instant) timestamp: micro
[[TimestampType]] or nanos. */
+ def isLtz(dt: DataType): Boolean =
+ dt.isInstanceOf[TimestampType] || dt.isInstanceOf[TimestampLTZNanosType]
+
+ /** Whether `dt` is a no-time-zone (local) timestamp: micro
[[TimestampNTZType]] or nanos. */
+ def isNtz(dt: DataType): Boolean =
+ dt.isInstanceOf[TimestampNTZType] || dt.isInstanceOf[TimestampNTZNanosType]
+}
diff --git a/sql/api/src/main/scala/org/apache/spark/sql/types/UpCastRule.scala
b/sql/api/src/main/scala/org/apache/spark/sql/types/UpCastRule.scala
index 6272cb03bd79..54de45f6eb8c 100644
--- a/sql/api/src/main/scala/org/apache/spark/sql/types/UpCastRule.scala
+++ b/sql/api/src/main/scala/org/apache/spark/sql/types/UpCastRule.scala
@@ -36,10 +36,20 @@ private[sql] object UpCastRule {
case (from: NumericType, to: DecimalType) if to.isWiderThan(from) => true
case (from: DecimalType, to: NumericType) if from.isTighterThan(to) => true
case (f, t) if legalNumericPrecedence(f, t) => true
- case (DateType, TimestampType) => true
- case (DateType, TimestampNTZType) => true
- case (TimestampNTZType, TimestampType) => true
- case (TimestampType, TimestampNTZType) => true
+ // Widening DATE -> timestamp family (micro or nanos, LTZ or NTZ) is
lossless; the reverse
+ // (timestamp -> DATE) drops the time-of-day and is not matched here, so
it stays a non-up-cast.
+ case (DateType, t) if TimestampFamily.fractionalPrecision(t).isDefined =>
true
+ // Lossless widening within the timestamp family: target fractional-second
precision >= source.
+ // Covers micros <-> nanos and the cross-family LTZ <-> NTZ pairs
(mirroring how the micro
+ // TimestampType <-> TimestampNTZType pair is a mutual up-cast). Same-type
equal precision is
+ // short-circuited by `from == to` above; cross-family equal precision
(e.g. LTZ(7) <-> NTZ(7))
+ // is admitted here by the `<=`. The guard keeps non-timestamp pairs
falling through to the
+ // cases below; lossy narrowing falls through to `case _ => false`.
+ case (f, t)
+ if TimestampFamily
+ .fractionalPrecision(f)
+ .exists(fp => TimestampFamily.fractionalPrecision(t).exists(fp <=
_)) =>
+ true
case (s1: StringType, s2: StringType) =>
StringHelper.isMoreConstrained(s1, s2)
// TODO: allow upcast from int/double/decimal to char/varchar of
sufficient length
diff --git
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/TypeCoercionHelper.scala
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/TypeCoercionHelper.scala
index e6329e465e00..942a6be948d8 100644
---
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/TypeCoercionHelper.scala
+++
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/TypeCoercionHelper.scala
@@ -83,6 +83,7 @@ import org.apache.spark.sql.types.{
StringType,
StringTypeExpression,
StructType,
+ TimestampFamily,
TimestampLTZNanosType,
TimestampNTZNanosType,
TimestampNTZType,
@@ -264,27 +265,21 @@ abstract class TypeCoercionHelper {
// The (family, precision) pair then maps back to a concrete type:
precision 6 yields the
// micro type, precision in [7, 9] yields the nanos type.
//
- // Note: this common-type resolution is intentionally more permissive
than the nanosecond
- // conversion rules in Cast.canUpCast / Cast.canANSIStoreAssign, which
keep cross-family and
- // DATE <-> nanos casts explicit-CAST-only while the nanos types are
unreleased (SPARK-57323
- // etc.). Coercion here mirrors the microsecond precedent so that UNION
/ CASE / coalesce /
- // IN / comparison resolve a common type the same way they do for the
micro families; the
- // stricter explicit-only stance is deliberately scoped to up-cast and
store assignment, not
- // to common-type resolution.
+ // Note: common-type resolution here is symmetric and widens to the
maximum precision, while
+ // Cast.canUpCast / Cast.canANSIStoreAssign are directional (they block
lossy narrowing). Both
+ // now agree on admissibility across the timestamp family -- including
the cross-family
+ // LTZ <-> NTZ pairs and DATE <-> nanos (SPARK-57303) -- mirroring the
microsecond precedent
+ // so that UNION / CASE / coalesce / IN / comparison resolve a common
type the same way they
+ // do for the micro families.
case _ =>
- // Fractional-seconds precision of the microsecond timestamp types;
the nanos types carry
- // 7-9. DATE has no time component and is treated as the micro
precision so that
- // DATE <-> micro widens to the micro type and DATE <-> nanos to the
nanos type.
+ // Fractional-seconds precision of the timestamp family (micros: 6,
nanos: 7-9). DATE has no
+ // time component and is treated as the micro precision (getOrElse) so
that DATE <-> micro
+ // widens to the micro type and DATE <-> nanos to the nanos type.
val MicrosPrecision = 6
- def isLtz(d: DatetimeType): Boolean =
- d.isInstanceOf[TimestampType] ||
d.isInstanceOf[TimestampLTZNanosType]
- def isNtz(d: DatetimeType): Boolean =
- d.isInstanceOf[TimestampNTZType] ||
d.isInstanceOf[TimestampNTZNanosType]
- def precisionOf(d: DatetimeType): Int = d match {
- case t: TimestampLTZNanosType => t.precision
- case t: TimestampNTZNanosType => t.precision
- case _ => MicrosPrecision // DateType / TimestampType /
TimestampNTZType
- }
+ def isLtz(d: DatetimeType): Boolean = TimestampFamily.isLtz(d)
+ def isNtz(d: DatetimeType): Boolean = TimestampFamily.isNtz(d)
+ def precisionOf(d: DatetimeType): Int =
+ TimestampFamily.fractionalPrecision(d).getOrElse(MicrosPrecision)
// Beyond TimeType (handled above), the only datetime types are DATE
and the micro/nanos
// timestamp families. Guard so that a future DatetimeType subtype
fails fast here instead
// of being silently mis-widened (treated as a family-neutral
precision-6 type and folded
diff --git
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/Cast.scala
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/Cast.scala
index 346047a8ba82..96ebe62b76ad 100644
---
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/Cast.scala
+++
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/Cast.scala
@@ -500,30 +500,13 @@ object Cast extends QueryErrorsBase {
case (_: NumericType, _: NumericType) => true
case (_: AtomicType, _: StringType) => true
case (_: CalendarIntervalType, _: StringType) => true
- // SPARK-57490: same-family cross-precision nanosecond casts: widening
(e.g. TIMESTAMP_NTZ(7) ->
- // TIMESTAMP_NTZ(9)) is lossless and allowed as a silent store assignment,
while narrowing
- // (e.g. (9) -> (7)) drops sub-microsecond digits and stays explicit-only.
Equal precision is
- // handled by the `from == to` short-circuit above; micros -> nanos
widening (e.g. TIMESTAMP_NTZ
- // -> TIMESTAMP_NTZ(9)) is lossless and falls to the catch-all below.
- case (f: TimestampNTZNanosType, t: TimestampNTZNanosType) => f.precision
<= t.precision
- case (f: TimestampLTZNanosType, t: TimestampLTZNanosType) => f.precision
<= t.precision
- // SPARK-57323: DATE <-> nanosecond-precision timestamp requires an
explicit CAST in both
- // directions (nanos -> DATE drops fields; DATE -> nanos is lossless but
kept explicit-only
- // while the nanos types are unreleased). Stricter than micro DATE <->
TIMESTAMP[_NTZ], which
- // the catch-all below allows.
- case (DateType, _: AnyTimestampNanoType) => false
- case (_: AnyTimestampNanoType, DateType) => false
- // SPARK-57293/57511: narrowing any nanosecond timestamp to a microsecond
timestamp drops the
- // sub-microsecond digits, and cross-family casts additionally reinterpret
the value against the
- // session time zone; both stay explicit-only rather than silent store
assignments while the
- // nanos types are unreleased. This covers same-family narrowing (nanos ->
micro), cross-family
- // nanos <-> nanos, and the mixed micro/nanos pairs at the precision-6
boundary; everything
- // matched here is explicit-only. The all-micro TIMESTAMP <->
TIMESTAMP_NTZ pair and micros ->
- // nanos same-family widening stay store-assignable via the catch-all
below.
- case (_: AnyTimestampNanoType, t) if AnyTimestampType.acceptsType(t) =>
false
- case (TimestampType, _: TimestampNTZNanosType) => false
- case (TimestampNTZType, _: TimestampLTZNanosType) => false
- case (_: AnyTimestampNanoType, _: AnyTimestampNanoType) => false
+ // SPARK-57303: block lossy narrowing across the whole timestamp family
(LTZ/NTZ, micros and
+ // nanos, including the cross-family LTZ <-> NTZ pairs) so store
assignment never silently drops
+ // sub-microsecond digits. Lossless widening, equal precision, and DATE
<-> timestamp (DATE has
+ // no fractional precision, so it never matches here) all fall through to
the DatetimeType arm
+ // below, mirroring the micro TIMESTAMP <-> TIMESTAMP_NTZ behavior.
+ case (f, t) if TimestampFamily.fractionalPrecision(f)
+ .exists(fp => TimestampFamily.fractionalPrecision(t).exists(fp > _))
=> false
// SPARK-57585: widening a TIME(p) to a larger precision is lossless and
allowed as a silent
// store assignment, while narrowing (e.g. TIME(6) -> TIME(3)) drops
fractional-seconds digits
// and stays explicit-CAST-only. Equal precision is handled by the `from
== to` short-circuit.
diff --git
a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/CastSuiteBase.scala
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/CastSuiteBase.scala
index 23d5783155fd..8f37f17c1069 100644
---
a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/CastSuiteBase.scala
+++
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/CastSuiteBase.scala
@@ -754,12 +754,14 @@ abstract class CastSuiteBase extends SparkFunSuite with
ExpressionEvalHelper {
}
}
- test("SPARK-57293: nanos<->micros store-assignment and up-cast contract") {
+ test("SPARK-57303: nanos<->micros store-assignment and up-cast contract") {
foreachNanosPrecision { p =>
- // Explicit-only: neither direction is an up-cast, so STRICT store
assignment rejects both.
- assert(!Cast.canUpCast(TimestampNTZType, TimestampNTZNanosType(p)))
+ // Lossless widening micros -> nanos(p) is an up-cast, mirroring the
micro precedent where a
+ // lower-precision timestamp widens to a higher-precision one.
+ assert(Cast.canUpCast(TimestampNTZType, TimestampNTZNanosType(p)))
+ assert(Cast.canUpCast(TimestampType, TimestampLTZNanosType(p)))
+ // Lossy narrowing nanos(p) -> micros drops sub-microsecond digits, so
it is not an up-cast.
assert(!Cast.canUpCast(TimestampNTZNanosType(p), TimestampNTZType))
- assert(!Cast.canUpCast(TimestampType, TimestampLTZNanosType(p)))
assert(!Cast.canUpCast(TimestampLTZNanosType(p), TimestampType))
// ANSI store assignment allows the lossless widening micros -> nanos(p)
...
@@ -769,18 +771,17 @@ abstract class CastSuiteBase extends SparkFunSuite with
ExpressionEvalHelper {
assert(!Cast.canANSIStoreAssign(TimestampNTZNanosType(p),
TimestampNTZType))
assert(!Cast.canANSIStoreAssign(TimestampLTZNanosType(p), TimestampType))
- // SPARK-57323: DATE <-> nanos requires an explicit CAST in both
directions, so STRICT
- // store assignment and ANSI store assignment both reject it. STRICT
goes through
- // Cast.canUpCast, so the assertions below also guard against a future
blanket datetime arm
- // in UpCastRule silently turning this into a safe store assignment.
- assert(!Cast.canUpCast(DateType, TimestampNTZNanosType(p)))
+ // SPARK-57303: DATE <-> nanos mirrors micro DATE <-> TIMESTAMP[_NTZ].
The lossless widening
+ // DATE -> nanos is an up-cast and ANSI-store-assignable; the lossy
nanos -> DATE drops the
+ // time-of-day, so it is not an up-cast but is still
ANSI-store-assignable.
+ assert(Cast.canUpCast(DateType, TimestampNTZNanosType(p)))
assert(!Cast.canUpCast(TimestampNTZNanosType(p), DateType))
- assert(!Cast.canUpCast(DateType, TimestampLTZNanosType(p)))
+ assert(Cast.canUpCast(DateType, TimestampLTZNanosType(p)))
assert(!Cast.canUpCast(TimestampLTZNanosType(p), DateType))
- assert(!Cast.canANSIStoreAssign(DateType, TimestampNTZNanosType(p)))
- assert(!Cast.canANSIStoreAssign(TimestampNTZNanosType(p), DateType))
- assert(!Cast.canANSIStoreAssign(DateType, TimestampLTZNanosType(p)))
- assert(!Cast.canANSIStoreAssign(TimestampLTZNanosType(p), DateType))
+ assert(Cast.canANSIStoreAssign(DateType, TimestampNTZNanosType(p)))
+ assert(Cast.canANSIStoreAssign(TimestampNTZNanosType(p), DateType))
+ assert(Cast.canANSIStoreAssign(DateType, TimestampLTZNanosType(p)))
+ assert(Cast.canANSIStoreAssign(TimestampLTZNanosType(p), DateType))
}
}
@@ -789,10 +790,11 @@ abstract class CastSuiteBase extends SparkFunSuite with
ExpressionEvalHelper {
p1 <- TimestampNTZNanosType.MIN_PRECISION to
TimestampNTZNanosType.MAX_PRECISION
p2 <- TimestampNTZNanosType.MIN_PRECISION to
TimestampNTZNanosType.MAX_PRECISION
} {
- // Cross-precision nanos casts are never up-casts (only equal precision
is, via from == to),
- // matching the micros <-> nanos precedent above; STRICT store
assignment rejects them.
- assert(Cast.canUpCast(TimestampNTZNanosType(p1),
TimestampNTZNanosType(p2)) == (p1 == p2))
- assert(Cast.canUpCast(TimestampLTZNanosType(p1),
TimestampLTZNanosType(p2)) == (p1 == p2))
+ // Lossless widening (p1 <= p2) is an up-cast; lossy narrowing (p1 > p2)
is not, matching the
+ // micros <-> nanos precedent above. STRICT store assignment accepts
widening, rejects
+ // narrowing.
+ assert(Cast.canUpCast(TimestampNTZNanosType(p1),
TimestampNTZNanosType(p2)) == (p1 <= p2))
+ assert(Cast.canUpCast(TimestampLTZNanosType(p1),
TimestampLTZNanosType(p2)) == (p1 <= p2))
// ANSI store assignment allows lossless widening (p1 <= p2) and equal
precision, but blocks
// lossy narrowing (p1 > p2) to avoid silently dropping sub-microsecond
digits.
assert(Cast.canANSIStoreAssign(TimestampNTZNanosType(p1),
TimestampNTZNanosType(p2)) ==
@@ -829,12 +831,13 @@ abstract class CastSuiteBase extends SparkFunSuite with
ExpressionEvalHelper {
assert(Cast.canCast(ntz, ltz))
assert(Cast.canAnsiCast(ltz, ntz))
assert(Cast.canAnsiCast(ntz, ltz))
- // The cross-family reinterpretation against the session zone is never a
safe up-cast.
- assert(!Cast.canUpCast(ltz, ntz))
- assert(!Cast.canUpCast(ntz, ltz))
- // They stay explicit-only: never silent store assignments (mirroring
the other nanos casts).
- assert(!Cast.canANSIStoreAssign(ltz, ntz))
- assert(!Cast.canANSIStoreAssign(ntz, ltz))
+ // SPARK-57303: the cross-family LTZ <-> NTZ pair is treated on the
precision axis like the
+ // micro TIMESTAMP <-> TIMESTAMP_NTZ pair: widening (target precision >=
source) is an up-cast
+ // and ANSI-store-assignable, while lossy narrowing is neither.
+ assert(Cast.canUpCast(ltz, ntz) == (p <= q))
+ assert(Cast.canUpCast(ntz, ltz) == (q <= p))
+ assert(Cast.canANSIStoreAssign(ltz, ntz) == (p <= q))
+ assert(Cast.canANSIStoreAssign(ntz, ltz) == (q <= p))
// The conversion depends on the session time zone in both directions.
assert(Cast.needsTimeZone(ltz, ntz))
assert(Cast.needsTimeZone(ntz, ltz))
@@ -847,31 +850,99 @@ abstract class CastSuiteBase extends SparkFunSuite with
ExpressionEvalHelper {
test("cross-family nanos cast: micro boundary (precision 6) admissibility
and store contract") {
// TIMESTAMP_LTZ(6) = TIMESTAMP and TIMESTAMP_NTZ(6) = TIMESTAMP_NTZ, so
the precision-6
- // cross-family casts are the mixed micro/nanos pairs covered here.
+ // cross-family casts are the mixed micro/nanos pairs covered here. Each
entry pairs a cast with
+ // whether it is a lossless widening (target precision >= source); p in
[7, 9] always widens
+ // from the micro side (6) and always narrows to it.
foreachNanosPrecision { p =>
val pairs = Seq(
- (TimestampType: DataType, TimestampNTZNanosType(p): DataType), //
LTZ(6) -> NTZ(p)
- (TimestampNTZNanosType(p): DataType, TimestampType: DataType), //
NTZ(p) -> LTZ(6)
- (TimestampNTZType: DataType, TimestampLTZNanosType(p): DataType),//
NTZ(6) -> LTZ(p)
- (TimestampLTZNanosType(p): DataType, TimestampNTZType: DataType))//
LTZ(p) -> NTZ(6)
- pairs.foreach { case (from, to) =>
- // Explicit casts are allowed (ANSI and non-ANSI), but are never safe
up-casts and never
- // silent store assignments, and they depend on the session time zone.
+ (TimestampType: DataType, TimestampNTZNanosType(p): DataType, true),
// LTZ(6) -> NTZ(p)
+ (TimestampNTZNanosType(p): DataType, TimestampType: DataType, false),
// NTZ(p) -> LTZ(6)
+ (TimestampNTZType: DataType, TimestampLTZNanosType(p): DataType,
true),// NTZ(6) -> LTZ(p)
+ (TimestampLTZNanosType(p): DataType, TimestampNTZType: DataType,
false))// LTZ(p) -> NTZ(6)
+ pairs.foreach { case (from, to, widening) =>
+ // Explicit casts are allowed (ANSI and non-ANSI) and depend on the
session time zone.
assert(Cast.canCast(from, to))
assert(Cast.canAnsiCast(from, to))
- assert(!Cast.canUpCast(from, to))
- assert(!Cast.canANSIStoreAssign(from, to))
+ // SPARK-57303: widening is an up-cast and store-assignable; narrowing
is neither.
+ assert(Cast.canUpCast(from, to) == widening)
+ assert(Cast.canANSIStoreAssign(from, to) == widening)
assert(Cast.needsTimeZone(from, to))
// Null-safe like the micro TIMESTAMP <-> TIMESTAMP_NTZ pair.
assert(!Cast.forceNullable(from, to))
}
}
// Sanity: the all-micro TIMESTAMP <-> TIMESTAMP_NTZ pair (precision 6 <->
6) stays a silent
- // store assignment, unlike the mixed micro/nanos pairs above.
+ // store assignment (equal precision).
assert(Cast.canANSIStoreAssign(TimestampType, TimestampNTZType))
assert(Cast.canANSIStoreAssign(TimestampNTZType, TimestampType))
}
+ test("SPARK-57303: full timestamp-family up-cast and store-assignment
precision matrix") {
+ // The micro/nanos LTZ/NTZ timestamp types with their effective
fractional-second precision
+ // (micros: 6, nanos: 7-9), across both time-zone families.
+ val tsTypes: Seq[DataType] =
+ Seq(TimestampType, TimestampNTZType) ++
+ (TimestampLTZNanosType.MIN_PRECISION to
TimestampLTZNanosType.MAX_PRECISION).flatMap { p =>
+ Seq(TimestampLTZNanosType(p), TimestampNTZNanosType(p))
+ }
+ def precisionOf(dt: DataType): Int = dt match {
+ case t: TimestampLTZNanosType => t.precision
+ case t: TimestampNTZNanosType => t.precision
+ case _ => 6
+ }
+ // For every ordered pair, canUpCast and canANSIStoreAssign are true iff
the target precision is
+ // >= the source precision (lossless widening or equal precision), false
for lossy narrowing.
+ for {
+ from <- tsTypes
+ to <- tsTypes
+ } {
+ val widening = precisionOf(from) <= precisionOf(to)
+ withClue(s"$from -> $to: ") {
+ assert(Cast.canUpCast(from, to) == widening)
+ assert(Cast.canANSIStoreAssign(from, to) == widening)
+ }
+ }
+
+ // DATE anchors (micros and nanos): DATE -> ts is a lossless widening
(up-cast + store-assign);
+ // ts -> DATE drops the time-of-day (not an up-cast) but stays
ANSI-store-assignable.
+ tsTypes.foreach { ts =>
+ assert(Cast.canUpCast(DateType, ts), s"DATE -> $ts should be an up-cast")
+ assert(Cast.canANSIStoreAssign(DateType, ts), s"DATE -> $ts should be
store-assignable")
+ assert(!Cast.canUpCast(ts, DateType), s"$ts -> DATE should not be an
up-cast")
+ assert(Cast.canANSIStoreAssign(ts, DateType), s"$ts -> DATE should be
store-assignable")
+ }
+
+ // TIME anchors: TIME is intentionally outside the timestamp family, so
TIME <-> ts matches the
+ // micro TIME <-> TIMESTAMP behavior - never an up-cast, but
ANSI-store-assignable both ways.
+ for {
+ tq <- TimeType.MIN_PRECISION to TimeType.MAX_PRECISION
+ ts <- tsTypes
+ } {
+ val time = TimeType(tq)
+ assert(!Cast.canUpCast(time, ts), s"$time -> $ts should not be an
up-cast")
+ assert(!Cast.canUpCast(ts, time), s"$ts -> $time should not be an
up-cast")
+ assert(Cast.canANSIStoreAssign(time, ts), s"$time -> $ts should be
store-assignable")
+ assert(Cast.canANSIStoreAssign(ts, time), s"$ts -> $time should be
store-assignable")
+ }
+ }
+
+ test("SPARK-57303: try-cast nullability follows up-cast admissibility for
the timestamp family") {
+ // `Cast.nullable`'s try-cast branch keys on `Cast.canUpCast`: an up-cast
(lossless widening
+ // within the timestamp family, or DATE -> ts) never fails, so a non-null
child stays non-null;
+ // a lossy narrowing is not an up-cast, so the try-cast is conservatively
nullable.
+ def tryCast(from: DataType, to: DataType): Cast =
+ Cast(AttributeReference("c", from, nullable = false)(), to, evalMode =
EvalMode.TRY)
+ foreachNanosPrecision { p =>
+ // Lossless widening micros -> nanos(p) and DATE -> nanos(p): non-null
child stays non-null.
+ assert(!tryCast(TimestampNTZType, TimestampNTZNanosType(p)).nullable)
+ assert(!tryCast(TimestampType, TimestampLTZNanosType(p)).nullable)
+ assert(!tryCast(DateType, TimestampNTZNanosType(p)).nullable)
+ // Lossy narrowing nanos(p) -> micros is not an up-cast, so the try-cast
is nullable.
+ assert(tryCast(TimestampNTZNanosType(p), TimestampNTZType).nullable)
+ assert(tryCast(TimestampLTZNanosType(p), TimestampType).nullable)
+ }
+ }
+
test("SPARK-40389: canUpCast: return false if casting decimal to integral
types can cause" +
" overflow") {
Seq(ByteType, ShortType, IntegerType, LongType).foreach { integralType =>
diff --git
a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/plans/logical/GeneratedColumnExpressionSuite.scala
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/plans/logical/GeneratedColumnExpressionSuite.scala
new file mode 100644
index 000000000000..de11b78452cc
--- /dev/null
+++
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/plans/logical/GeneratedColumnExpressionSuite.scala
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.spark.sql.catalyst.plans.logical
+
+import org.apache.spark.SparkFunSuite
+import org.apache.spark.sql.AnalysisException
+import org.apache.spark.sql.catalyst.expressions.Literal
+import org.apache.spark.sql.types.{DataType, TimestampLTZNanosType,
TimestampNTZNanosType}
+import org.apache.spark.sql.types.{TimestampNTZType, TimestampType}
+
+class GeneratedColumnExpressionSuite extends SparkFunSuite {
+
+ private def genExpr(childType: DataType): GeneratedColumnExpression =
+ GeneratedColumnExpression(Literal.create(null, childType), "<gen expr>")
+
+ test("SPARK-57303: validate accepts a lossless widening to a nanosecond
timestamp column") {
+ // The generation expression's type is up-castable to the column type, so
validate() succeeds:
+ // micros -> nanos is a lossless widening up-cast (Cast.canUpCast).
+ (TimestampNTZNanosType.MIN_PRECISION to
TimestampNTZNanosType.MAX_PRECISION).foreach { p =>
+ genExpr(TimestampNTZType).validate("c", TimestampNTZNanosType(p),
allColumns = Seq.empty)
+ genExpr(TimestampType).validate("c", TimestampLTZNanosType(p),
allColumns = Seq.empty)
+ }
+ }
+
+ test("SPARK-57303: validate rejects a lossy narrowing from a nanosecond
timestamp column") {
+ // nanos -> micros drops sub-microsecond digits and is not an up-cast, so
validate() rejects it.
+ (TimestampNTZNanosType.MIN_PRECISION to
TimestampNTZNanosType.MAX_PRECISION).foreach { p =>
+ val ex = intercept[AnalysisException] {
+ genExpr(TimestampNTZNanosType(p)).validate("c", TimestampNTZType,
allColumns = Seq.empty)
+ }
+ assert(ex.getCondition == "UNSUPPORTED_EXPRESSION_GENERATED_COLUMN")
+ assert(ex.getMessageParameters.get("reason").contains("incompatible with
column data type"))
+ }
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]