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

RocMarshal pushed a commit to branch release-2.4
in repository https://gitbox.apache.org/repos/asf/flink.git


The following commit(s) were added to refs/heads/release-2.4 by this push:
     new 6427ac6dea2 [FLINK-41281][table] Allow CAST from a VARIANT timestamp 
to the other temporal types
6427ac6dea2 is described below

commit 6427ac6dea2dffde587aaac9243c74131db49840
Author: Ramin Gharib <[email protected]>
AuthorDate: Wed Nov 4 08:52:24 2026 +0100

    [FLINK-41281][table] Allow CAST from a VARIANT timestamp to the other 
temporal types
    
    * [FLINK-41281][table] Allow CAST from a VARIANT timestamp to the other 
temporal types (#29690)
    
    A stored TIMESTAMP or TIMESTAMP_LTZ, including the nanosecond kinds, now 
casts to TIMESTAMP, TIMESTAMP_LTZ, DATE and TIME like a regular cast from its 
SQL type, using the session time zone and the same DateTimeUtils helpers. A 
stored DATE or TIME still casts only to its own type, since the other datetime 
types need a part the value does not have.
    
    A DATE target that gets another kind now fails with the standard VARIANT 
cast message instead of a raw VariantTypeException.
    
    Generated-by: Claude Code 2.1.317 (Claude Opus 5.5)
    (cherry picked from commit 4cdc6d83a9db7f82a99610d6382bfb762bc65f7d)
---
 docs/content.zh/docs/sql/reference/data-types.md   |  30 +++---
 docs/content/docs/sql/reference/data-types.md      |  30 +++---
 .../table/data/utils/FromVariantConverter.java     |  16 +--
 .../flink/table/data/utils/VariantCastUtils.java   | 113 ++++++++++++++++-----
 .../table/data/utils/FromVariantConverterTest.java |  11 ++
 .../planner/functions/casting/CastRulesTest.java   |  83 ++++++++++++---
 6 files changed, 211 insertions(+), 72 deletions(-)

diff --git a/docs/content.zh/docs/sql/reference/data-types.md 
b/docs/content.zh/docs/sql/reference/data-types.md
index 703185f11de..7fcc013d36c 100644
--- a/docs/content.zh/docs/sql/reference/data-types.md
+++ b/docs/content.zh/docs/sql/reference/data-types.md
@@ -1553,18 +1553,18 @@ A `VARIANT` can be converted to a scalar type with 
`CAST` or `TRY_CAST`. A cast
 the target holds the stored value without reinterpreting it, so a value is 
never wrapped or rounded
 to make it fit. Otherwise `CAST` fails and `TRY_CAST` returns `NULL`.
 
-| Stored kind     | Succeeds for                                        |
-|-----------------|-----------------------------------------------------|
-| numeric kinds   | any numeric target that holds the value             |
-| `BOOLEAN`       | `BOOLEAN`                                           |
-| `DATE`          | `DATE`                                              |
-| `TIME`          | `TIME(p)`                                           |
-| `TIMESTAMP`     | `TIMESTAMP(p)`                                      |
-| `TIMESTAMP_LTZ` | `TIMESTAMP_LTZ(p)`                                  |
-| `BYTES`         | `BINARY(n)`, `VARBINARY(n)`, and a character string |
-| `UUID`          | `UUID`                                              |
-| any scalar      | `STRING`, `CHAR(n)`, `VARCHAR(n)`                   |
-| `NULL`          | SQL `NULL` for any nullable target                  |
+| Stored kind     | Succeeds for                                          |
+|-----------------|-------------------------------------------------------|
+| numeric kinds   | any numeric target that holds the value               |
+| `BOOLEAN`       | `BOOLEAN`                                             |
+| `DATE`          | `DATE`                                                |
+| `TIME`          | `TIME(p)`                                             |
+| `TIMESTAMP`     | `TIMESTAMP(p)`, `TIMESTAMP_LTZ(p)`, `DATE`, `TIME(p)` |
+| `TIMESTAMP_LTZ` | `TIMESTAMP_LTZ(p)`, `TIMESTAMP(p)`, `DATE`, `TIME(p)` |
+| `BYTES`         | `BINARY(n)`, `VARBINARY(n)`, and a character string   |
+| `UUID`          | `UUID`                                                |
+| any scalar      | `STRING`, `CHAR(n)`, `VARCHAR(n)`                     |
+| `NULL`          | SQL `NULL` for any nullable target                    |
 
 The conditions above mean:
 
@@ -1578,6 +1578,12 @@ The conditions above mean:
 - A **length or precision** is adjusted the same way a regular cast into that 
type would: a value
   longer than the target is trimmed, fractional seconds beyond the target 
precision are truncated,
   and the fixed width types `CHAR(n)` and `BINARY(n)` pad a shorter value.
+- A **`TIMESTAMP`** or **`TIMESTAMP_LTZ`** value reaches the other datetime 
types the way a regular
+  cast from its type does, in the session time zone. With 
`table.local-time-zone` set to
+  `Europe/Berlin`, the instant `2024-01-01 23:30:00.123Z` reaches 
`TIMESTAMP(3)` as
+  `2024-01-02 00:30:00.123`, `DATE` as `2024-01-02`, and `TIME(3)` as 
`00:30:00.123`. A `DATE` or
+  `TIME` value reaches only its own type, since any other datetime type needs 
a time of day or a
+  date that the value does not have.
 - A **`TIME`** value keeps only millisecond precision, the resolution Flink's 
runtime `TIME` type
   supports, so `TIME(4)` through `TIME(9)` behave like `TIME(3)`.
 
diff --git a/docs/content/docs/sql/reference/data-types.md 
b/docs/content/docs/sql/reference/data-types.md
index 1ac289a0402..edc6919f92f 100644
--- a/docs/content/docs/sql/reference/data-types.md
+++ b/docs/content/docs/sql/reference/data-types.md
@@ -1561,18 +1561,18 @@ A `VARIANT` can be converted to a scalar type with 
`CAST` or `TRY_CAST`. A cast
 the target holds the stored value without reinterpreting it, so a value is 
never wrapped or rounded
 to make it fit. Otherwise `CAST` fails and `TRY_CAST` returns `NULL`.
 
-| Stored kind     | Succeeds for                                        |
-|-----------------|-----------------------------------------------------|
-| numeric kinds   | any numeric target that holds the value             |
-| `BOOLEAN`       | `BOOLEAN`                                           |
-| `DATE`          | `DATE`                                              |
-| `TIME`          | `TIME(p)`                                           |
-| `TIMESTAMP`     | `TIMESTAMP(p)`                                      |
-| `TIMESTAMP_LTZ` | `TIMESTAMP_LTZ(p)`                                  |
-| `BYTES`         | `BINARY(n)`, `VARBINARY(n)`, and a character string |
-| `UUID`          | `UUID`                                              |
-| any scalar      | `STRING`, `CHAR(n)`, `VARCHAR(n)`                   |
-| `NULL`          | SQL `NULL` for any nullable target                  |
+| Stored kind     | Succeeds for                                          |
+|-----------------|-------------------------------------------------------|
+| numeric kinds   | any numeric target that holds the value               |
+| `BOOLEAN`       | `BOOLEAN`                                             |
+| `DATE`          | `DATE`                                                |
+| `TIME`          | `TIME(p)`                                             |
+| `TIMESTAMP`     | `TIMESTAMP(p)`, `TIMESTAMP_LTZ(p)`, `DATE`, `TIME(p)` |
+| `TIMESTAMP_LTZ` | `TIMESTAMP_LTZ(p)`, `TIMESTAMP(p)`, `DATE`, `TIME(p)` |
+| `BYTES`         | `BINARY(n)`, `VARBINARY(n)`, and a character string   |
+| `UUID`          | `UUID`                                                |
+| any scalar      | `STRING`, `CHAR(n)`, `VARCHAR(n)`                     |
+| `NULL`          | SQL `NULL` for any nullable target                    |
 
 The conditions above mean:
 
@@ -1586,6 +1586,12 @@ The conditions above mean:
 - A **length or precision** is adjusted the same way a regular cast into that 
type would: a value
   longer than the target is trimmed, fractional seconds beyond the target 
precision are truncated,
   and the fixed width types `CHAR(n)` and `BINARY(n)` pad a shorter value.
+- A **`TIMESTAMP`** or **`TIMESTAMP_LTZ`** value reaches the other datetime 
types the way a regular
+  cast from its type does, in the session time zone. With 
`table.local-time-zone` set to
+  `Europe/Berlin`, the instant `2024-01-01 23:30:00.123Z` reaches 
`TIMESTAMP(3)` as
+  `2024-01-02 00:30:00.123`, `DATE` as `2024-01-02`, and `TIME(3)` as 
`00:30:00.123`. A `DATE` or
+  `TIME` value reaches only its own type, since any other datetime type needs 
a time of day or a
+  date that the value does not have.
 - A **`TIME`** value keeps only millisecond precision, the resolution Flink's 
runtime `TIME` type
   supports, so `TIME(4)` through `TIME(9)` behave like `TIME(3)`.
 
diff --git 
a/flink-table/flink-table-common/src/main/java/org/apache/flink/table/data/utils/FromVariantConverter.java
 
b/flink-table/flink-table-common/src/main/java/org/apache/flink/table/data/utils/FromVariantConverter.java
index e2909990a75..8056eac0df6 100644
--- 
a/flink-table/flink-table-common/src/main/java/org/apache/flink/table/data/utils/FromVariantConverter.java
+++ 
b/flink-table/flink-table-common/src/main/java/org/apache/flink/table/data/utils/FromVariantConverter.java
@@ -67,7 +67,8 @@ public final class FromVariantConverter implements 
Serializable {
     /**
      * Creates a converter for a type that VARIANT casts to.
      *
-     * @param sessionZone renders a {@code TIMESTAMP_LTZ} value read as a 
string
+     * @param sessionZone the session time zone, for a cast between {@code 
TIMESTAMP_LTZ} and a type
+     *     without a time zone
      * @throws IllegalArgumentException if VARIANT cannot be cast to the type
      */
     public static FromVariantConverter create(final LogicalType type, final 
ZoneId sessionZone) {
@@ -79,8 +80,8 @@ public final class FromVariantConverter implements 
Serializable {
      * Converts a variant into internal data.
      *
      * @throws TableRuntimeException if a value does not match its type
-     * @throws VariantTypeException if the variant is malformed, or a {@code 
BOOLEAN}, {@code DATE}
-     *     or binary value has another kind
+     * @throws VariantTypeException if the variant is malformed, or a {@code 
BOOLEAN} or binary
+     *     value has another kind
      */
     public Object convert(final Variant variant) {
         return reader.read(variant);
@@ -143,21 +144,22 @@ public final class FromVariantConverter implements 
Serializable {
                     return variant -> VariantCastUtils.toBytes(variant, 
length, fixedLength);
                 }
             case DATE:
-                return variant -> (int) variant.getDate().toEpochDay();
+                return variant -> VariantCastUtils.toDate(variant, 
sessionZone);
             case TIME_WITHOUT_TIME_ZONE:
                 {
                     final int precision = LogicalTypeChecks.getPrecision(type);
-                    return variant -> VariantCastUtils.toTime(variant, 
precision);
+                    return variant -> VariantCastUtils.toTime(variant, 
precision, sessionZone);
                 }
             case TIMESTAMP_WITHOUT_TIME_ZONE:
                 {
                     final int precision = LogicalTypeChecks.getPrecision(type);
-                    return variant -> VariantCastUtils.toTimestamp(variant, 
precision);
+                    return variant -> VariantCastUtils.toTimestamp(variant, 
precision, sessionZone);
                 }
             case TIMESTAMP_WITH_LOCAL_TIME_ZONE:
                 {
                     final int precision = LogicalTypeChecks.getPrecision(type);
-                    return variant -> VariantCastUtils.toTimestampLtz(variant, 
precision);
+                    return variant ->
+                            VariantCastUtils.toTimestampLtz(variant, 
precision, sessionZone);
                 }
             case UUID:
                 return VariantCastUtils::toUuid;
diff --git 
a/flink-table/flink-table-common/src/main/java/org/apache/flink/table/data/utils/VariantCastUtils.java
 
b/flink-table/flink-table-common/src/main/java/org/apache/flink/table/data/utils/VariantCastUtils.java
index 6c1b9c4118f..532505ec3da 100644
--- 
a/flink-table/flink-table-common/src/main/java/org/apache/flink/table/data/utils/VariantCastUtils.java
+++ 
b/flink-table/flink-table-common/src/main/java/org/apache/flink/table/data/utils/VariantCastUtils.java
@@ -48,6 +48,11 @@ import static 
org.apache.flink.table.data.utils.ToVariantConverter.TIMESTAMP_PRE
  * exception to exactness: they are approximate by definition, so they accept 
any numeric kind and
  * reject only a magnitude they cannot represent at all.
  *
+ * <p>A timestamp kind reaches every datetime type the way a regular cast from 
its SQL type does,
+ * using the session time zone between {@code TIMESTAMP_LTZ} and a type 
without one. {@code DATE}
+ * and {@code TIME} keep only the date or the time of day. A {@code DATE} or 
{@code TIME} value
+ * reaches only its own type, because any other datetime type needs a part the 
value does not have.
+ *
  * <p>A length or a precision is not part of the value in that sense, so it 
follows the rules of a
  * regular cast into the same type. A value longer than the target is trimmed, 
fractional seconds
  * beyond the target precision are truncated, and the fixed width targets 
{@code CHAR(n)} and {@code
@@ -240,42 +245,98 @@ public final class VariantCastUtils {
     }
 
     /**
-     * Reads a timestamp variant as the target {@code TIMESTAMP}. {@link 
Variant#getDateTime()}
-     * accepts both the microsecond ({@link Variant.Type#TIMESTAMP}) and 
nanosecond ({@link
-     * Variant.Type#TIMESTAMP_NS}) encodings. Fractional seconds beyond the 
target precision are
-     * truncated, the same as a regular {@code TIMESTAMP} to {@code 
TIMESTAMP(p)} cast.
+     * Reads a timestamp variant as the target {@code TIMESTAMP}. A {@code 
TIMESTAMP_LTZ} value is
+     * shifted into the session time zone, the same as a regular {@code 
TIMESTAMP_LTZ} to {@code
+     * TIMESTAMP} cast. Fractional seconds beyond the target precision are 
truncated.
      */
-    public static TimestampData toTimestamp(Variant variant, int precision) {
-        final Variant.Type type = variant.getType();
-        if (type == Variant.Type.TIMESTAMP || type == 
Variant.Type.TIMESTAMP_NS) {
-            return DateTimeUtils.truncate(
-                    TimestampData.fromLocalDateTime(variant.getDateTime()), 
precision);
+    public static TimestampData toTimestamp(Variant variant, int precision, 
TimeZone sessionZone) {
+        final TimestampData timestamp;
+        switch (variant.getType()) {
+            case TIMESTAMP:
+            case TIMESTAMP_NS:
+                timestamp = 
TimestampData.fromLocalDateTime(variant.getDateTime());
+                break;
+            case TIMESTAMP_LTZ:
+            case TIMESTAMP_LTZ_NS:
+                timestamp =
+                        DateTimeUtils.timestampWithLocalZoneToTimestamp(
+                                
TimestampData.fromInstant(variant.getInstant()), sessionZone);
+                break;
+            default:
+                throw unsupportedKind(variant, String.format("TIMESTAMP(%d)", 
precision));
         }
-        throw unsupportedKind(variant, String.format("TIMESTAMP(%d)", 
precision));
+        return DateTimeUtils.truncate(timestamp, precision);
     }
 
-    /** Reads a timestamp with local time zone variant. See {@link 
#toTimestamp(Variant, int)}. */
-    public static TimestampData toTimestampLtz(Variant variant, int precision) 
{
-        final Variant.Type type = variant.getType();
-        if (type == Variant.Type.TIMESTAMP_LTZ || type == 
Variant.Type.TIMESTAMP_LTZ_NS) {
-            return DateTimeUtils.truncate(
-                    TimestampData.fromInstant(variant.getInstant()), 
precision);
+    /**
+     * Reads a timestamp variant as the target {@code TIMESTAMP_LTZ}. A {@code 
TIMESTAMP} value is
+     * read as a wall-clock time in the session time zone, the same as a 
regular {@code TIMESTAMP}
+     * to {@code TIMESTAMP_LTZ} cast. Fractional seconds beyond the target 
precision are truncated.
+     */
+    public static TimestampData toTimestampLtz(
+            Variant variant, int precision, TimeZone sessionZone) {
+        final TimestampData timestamp;
+        switch (variant.getType()) {
+            case TIMESTAMP_LTZ:
+            case TIMESTAMP_LTZ_NS:
+                timestamp = TimestampData.fromInstant(variant.getInstant());
+                break;
+            case TIMESTAMP:
+            case TIMESTAMP_NS:
+                timestamp =
+                        DateTimeUtils.timestampToTimestampWithLocalZone(
+                                
TimestampData.fromLocalDateTime(variant.getDateTime()),
+                                sessionZone);
+                break;
+            default:
+                throw unsupportedKind(variant, 
String.format("TIMESTAMP_LTZ(%d)", precision));
         }
-        throw unsupportedKind(variant, String.format("TIMESTAMP_LTZ(%d)", 
precision));
+        return DateTimeUtils.truncate(timestamp, precision);
     }
 
     /**
-     * Reads a time variant as the target {@code TIME}. The runtime TIME 
representation is
-     * millisecond-of-day, so a variant's microseconds are dropped and any 
fractional seconds beyond
-     * the target precision are then truncated, the same as a regular {@code 
TIME} to {@code
-     * TIME(p)} cast.
+     * Reads a date or timestamp variant as the target {@code DATE}. A 
timestamp keeps its date,
+     * read in the session time zone for a {@code TIMESTAMP_LTZ}.
      */
-    public static int toTime(Variant variant, int precision) {
-        if (variant.getType() == Variant.Type.TIME) {
-            return DateTimeUtils.applyTimePrecisionTruncation(
-                    DateTimeUtils.toInternal(variant.getTime()), precision);
+    public static int toDate(Variant variant, TimeZone sessionZone) {
+        switch (variant.getType()) {
+            case DATE:
+                return DateTimeUtils.toInternal(variant.getDate());
+            case TIMESTAMP:
+            case TIMESTAMP_NS:
+                return 
DateTimeUtils.toInternal(variant.getDateTime().toLocalDate());
+            case TIMESTAMP_LTZ:
+            case TIMESTAMP_LTZ_NS:
+                return DateTimeUtils.timestampWithLocalZoneToDate(
+                        TimestampData.fromInstant(variant.getInstant()), 
sessionZone);
+            default:
+                throw unsupportedKind(variant, "DATE");
+        }
+    }
+
+    /**
+     * Reads a time or timestamp variant as the target {@code TIME}. A 
timestamp keeps its time of
+     * day, read in the session time zone for a {@code TIMESTAMP_LTZ}. The 
runtime TIME
+     * representation is millisecond-of-day, so a variant's microseconds are 
dropped and any
+     * fractional seconds beyond the target precision are then truncated, the 
same as a regular
+     * {@code TIME} to {@code TIME(p)} cast.
+     */
+    public static int toTime(Variant variant, int precision, TimeZone 
sessionZone) {
+        switch (variant.getType()) {
+            case TIME:
+                return DateTimeUtils.applyTimePrecisionTruncation(
+                        DateTimeUtils.toInternal(variant.getTime()), 
precision);
+            case TIMESTAMP:
+            case TIMESTAMP_NS:
+                return DateTimeUtils.timestampWithoutLocalZoneToTime(
+                        
TimestampData.fromLocalDateTime(variant.getDateTime()), precision);
+            case TIMESTAMP_LTZ:
+            case TIMESTAMP_LTZ_NS:
+                return DateTimeUtils.timestampWithLocalZoneToTime(
+                        TimestampData.fromInstant(variant.getInstant()), 
sessionZone, precision);
+            default:
+                throw unsupportedKind(variant, String.format("TIME(%d)", 
precision));
         }
-        throw unsupportedKind(variant, String.format("TIME(%d)", precision));
     }
 
     /**
diff --git 
a/flink-table/flink-table-common/src/test/java/org/apache/flink/table/data/utils/FromVariantConverterTest.java
 
b/flink-table/flink-table-common/src/test/java/org/apache/flink/table/data/utils/FromVariantConverterTest.java
index 61997885049..d98aebe1cf0 100644
--- 
a/flink-table/flink-table-common/src/test/java/org/apache/flink/table/data/utils/FromVariantConverterTest.java
+++ 
b/flink-table/flink-table-common/src/test/java/org/apache/flink/table/data/utils/FromVariantConverterTest.java
@@ -330,6 +330,7 @@ class FromVariantConverterTest {
                 DataTypes.ROW(
                                 FIELD("int", DataTypes.INT()),
                                 FIELD("ltz", DataTypes.STRING()),
+                                FIELD("ts", DataTypes.TIMESTAMP(0)),
                                 FIELD("list", 
DataTypes.ARRAY(DataTypes.INT().notNull())),
                                 FIELD("map", DataTypes.MAP(DataTypes.CHAR(2), 
DataTypes.INT())),
                                 FIELD("variant", DataTypes.VARIANT()),
@@ -339,6 +340,7 @@ class FromVariantConverterTest {
                 BUILDER.object()
                         .add("int", BUILDER.of(1))
                         .add("ltz", BUILDER.of(Instant.EPOCH))
+                        .add("ts", BUILDER.of(Instant.EPOCH))
                         .add("list", 
BUILDER.array().add(BUILDER.of(2)).build())
                         .add("map", BUILDER.object().add("k", 
BUILDER.ofNull()).build())
                         .add("variant", BUILDER.ofNull())
@@ -357,6 +359,8 @@ class FromVariantConverterTest {
         assertThat(actual).isEqualTo(converter.convert(variant));
         // the session zone survives too
         assertThat(actual.getString(1).toString()).startsWith("1970-01-01 
09:00:00");
+        assertThat(actual.getTimestamp(2, 0))
+                
.isEqualTo(TimestampData.fromLocalDateTime(LocalDateTime.of(1970, 1, 1, 9, 0)));
     }
 
     @ParameterizedTest(name = "{0}")
@@ -387,6 +391,13 @@ class FromVariantConverterTest {
                 arguments(DataTypes.BINARY(2), new byte[] {1, 2}),
                 arguments(DataTypes.BYTES(), new byte[] {(byte) 0xCA, (byte) 
0xFE}),
                 arguments(DataTypes.DATE(), (int) LocalDate.of(2026, 9, 
25).toEpochDay()),
+                // dates are proleptic Gregorian: the earliest DATE, a day the 
1582 calendar switch
+                // skipped, and a leap day of 1600
+                arguments(DataTypes.DATE(), (int) LocalDate.of(0, 1, 
1).toEpochDay()),
+                arguments(DataTypes.DATE(), (int) LocalDate.of(1582, 10, 
10).toEpochDay()),
+                arguments(
+                        DataTypes.TIMESTAMP(6),
+                        TimestampData.fromLocalDateTime(LocalDateTime.of(1600, 
2, 29, 12, 0))),
                 arguments(DataTypes.TIME(3), LocalTime.of(10, 15, 
30).toSecondOfDay() * 1000 + 123),
                 arguments(DataTypes.TIMESTAMP(3), 
TimestampData.fromLocalDateTime(DATE_TIME)),
                 arguments(DataTypes.TIMESTAMP(9), 
TimestampData.fromEpochMillis(1, 234_567)),
diff --git 
a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/functions/casting/CastRulesTest.java
 
b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/functions/casting/CastRulesTest.java
index 49e9f6ccace..1b05d848899 100644
--- 
a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/functions/casting/CastRulesTest.java
+++ 
b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/functions/casting/CastRulesTest.java
@@ -2316,7 +2316,24 @@ class CastRulesTest {
                                 VARIANT(),
                                 VARIANT_BUILDER.of(LocalDate.of(2020, 1, 1)),
                                 (int) LocalDate.of(2020, 1, 1).toEpochDay())
-                        .fail(VARIANT(), Variant.newBuilder().of(1), 
TableRuntimeException.class),
+                        // a timestamp keeps its date, in the session time 
zone for a TIMESTAMP_LTZ
+                        .fromCase(
+                                VARIANT(),
+                                CET_CONTEXT,
+                                
VARIANT_BUILDER.of(Instant.parse("2020-01-01T23:30:00Z")),
+                                (int) LocalDate.of(2020, 1, 2).toEpochDay())
+                        // before 1970 and the 1582 calendar switch, the 
timestamp keeps its day
+                        .fromCase(
+                                VARIANT(),
+                                VARIANT_BUILDER.of(LocalDateTime.of(1582, 10, 
4, 23, 0)),
+                                (int) LocalDate.of(1582, 10, 4).toEpochDay())
+                        .fail(VARIANT(), Variant.newBuilder().of(1), 
TableRuntimeException.class)
+                        // a TIME has no date
+                        .fail(
+                                VARIANT(),
+                                VARIANT_BUILDER.of(LocalTime.of(12, 0)),
+                                TableRuntimeException.class,
+                                "Cannot cast a VARIANT TIME value to DATE."),
                 // A variant keeps microseconds for TIME, so fractional 
seconds beyond the target
                 // precision are truncated, matching a regular cast into a 
narrower TIME.
                 CastTestSpecBuilder.testCastTo(TIME(3))
@@ -2330,6 +2347,18 @@ class CastRulesTest {
                                 VARIANT(),
                                 Variant.newBuilder().of(LocalTime.of(12, 0, 0, 
123_456_000)),
                                 DateTimeUtils.toInternal(LocalTime.of(12, 0, 
0, 123_000_000)))
+                        // a timestamp keeps its time of day, in the session 
time zone for a
+                        // TIMESTAMP_LTZ only
+                        .fromCase(
+                                VARIANT(),
+                                CET_CONTEXT,
+                                VARIANT_BUILDER.of(LocalDateTime.of(2020, 1, 
1, 23, 30)),
+                                DateTimeUtils.toInternal(LocalTime.of(23, 30)))
+                        .fromCase(
+                                VARIANT(),
+                                CET_CONTEXT,
+                                
VARIANT_BUILDER.of(Instant.parse("2020-01-01T23:30:00.123456Z")),
+                                DateTimeUtils.toInternal(LocalTime.of(0, 30, 
0, 123_000_000)))
                         .fail(VARIANT(), Variant.newBuilder().of(1), 
TableRuntimeException.class),
                 // TIME has no runtime representation finer than milliseconds, 
so a target
                 // precision above 3 truncates no further than TIME(3) already 
does.
@@ -2352,12 +2381,20 @@ class CastRulesTest {
                                         .of(LocalDateTime.of(2020, 1, 1, 12, 
0, 0, 123456789)),
                                 TimestampData.fromLocalDateTime(
                                         LocalDateTime.of(2020, 1, 1, 12, 0, 0, 
123456000)))
+                        // a TIMESTAMP_LTZ is shifted into the session time 
zone
+                        .fromCase(
+                                VARIANT(),
+                                CET_CONTEXT,
+                                
VARIANT_BUILDER.of(Instant.parse("2020-01-01T23:30:00Z")),
+                                TimestampData.fromLocalDateTime(
+                                        LocalDateTime.of(2020, 1, 2, 0, 30)))
                         .fail(VARIANT(), Variant.newBuilder().of(1), 
TableRuntimeException.class)
-                        // a TIMESTAMP_LTZ is a different kind and is not read 
as a TIMESTAMP
+                        // a TIME has no date
                         .fail(
                                 VARIANT(),
-                                
VARIANT_BUILDER.of(Instant.ofEpochSecond(1_600_000_000L)),
-                                TableRuntimeException.class),
+                                VARIANT_BUILDER.of(LocalTime.of(12, 0)),
+                                TableRuntimeException.class,
+                                "Cannot cast a VARIANT TIME value to 
TIMESTAMP(6)."),
                 // A variant keeps microseconds, so fractional seconds beyond 
the target precision
                 // are truncated, matching a regular cast into a narrower 
TIMESTAMP.
                 CastTestSpecBuilder.testCastTo(TIMESTAMP(3))
@@ -2384,7 +2421,14 @@ class CastRulesTest {
                                 VARIANT_BUILDER.of(
                                         LocalDateTime.of(2020, 1, 1, 12, 0, 0, 
123000000)),
                                 TimestampData.fromLocalDateTime(
-                                        LocalDateTime.of(2020, 1, 1, 12, 0, 
0))),
+                                        LocalDateTime.of(2020, 1, 1, 12, 0, 
0)))
+                        // before 1970, truncating still moves toward the past
+                        .fromCase(
+                                VARIANT(),
+                                VARIANT_BUILDER.of(
+                                        LocalDateTime.of(1969, 12, 31, 23, 59, 
58, 500_000_000)),
+                                TimestampData.fromLocalDateTime(
+                                        LocalDateTime.of(1969, 12, 31, 23, 59, 
58))),
                 // A fraction with leading zeros (.000123456) is still 
truncated to the precision.
                 CastTestSpecBuilder.testCastTo(TIMESTAMP(6))
                         .fromCase(
@@ -2408,23 +2452,31 @@ class CastRulesTest {
                                         .of(LocalDateTime.of(2020, 1, 1, 12, 
0, 0, 123456000)),
                                 TimestampData.fromLocalDateTime(
                                         LocalDateTime.of(2020, 1, 1, 12, 0, 0, 
123456000)))
-                        // a TIMESTAMP_LTZ_NS is a different kind and is not 
read as a TIMESTAMP
-                        .fail(
+                        // a TIMESTAMP_LTZ_NS keeps its nanoseconds
+                        .fromCase(
                                 VARIANT(),
                                 Variant.newBuilder()
-                                        
.of(Instant.ofEpochSecond(1_600_000_000L, 123456789)),
-                                TableRuntimeException.class),
+                                        
.of(Instant.parse("2020-01-01T12:00:00.123456789Z")),
+                                TimestampData.fromLocalDateTime(
+                                        LocalDateTime.of(2020, 1, 1, 12, 0, 0, 
123456789))),
                 CastTestSpecBuilder.testCastTo(TIMESTAMP_LTZ())
                         .fromCase(
                                 VARIANT(),
                                 
VARIANT_BUILDER.of(Instant.ofEpochSecond(1_600_000_000L)),
                                 
TimestampData.fromInstant(Instant.ofEpochSecond(1_600_000_000L)))
+                        // a TIMESTAMP is read as a wall-clock time in the 
session time zone
+                        .fromCase(
+                                VARIANT(),
+                                CET_CONTEXT,
+                                VARIANT_BUILDER.of(LocalDateTime.of(2020, 1, 
1, 12, 0, 0)),
+                                
TimestampData.fromInstant(Instant.parse("2020-01-01T11:00:00Z")))
                         .fail(VARIANT(), VARIANT_BUILDER.of(1), 
TableRuntimeException.class)
-                        // a TIMESTAMP is not read as a TIMESTAMP_LTZ either
+                        // a DATE has no time of day
                         .fail(
                                 VARIANT(),
-                                VARIANT_BUILDER.of(LocalDateTime.of(2020, 1, 
1, 12, 0, 0)),
-                                TableRuntimeException.class),
+                                VARIANT_BUILDER.of(LocalDate.of(2020, 1, 1)),
+                                TableRuntimeException.class,
+                                "Cannot cast a VARIANT DATE value to 
TIMESTAMP_LTZ(6)."),
                 CastTestSpecBuilder.testCastTo(TIMESTAMP_LTZ(3))
                         .fromCase(
                                 VARIANT(),
@@ -2448,12 +2500,13 @@ class CastRulesTest {
                                         
.of(Instant.ofEpochSecond(1_600_000_000L, 123456000)),
                                 TimestampData.fromInstant(
                                         Instant.ofEpochSecond(1_600_000_000L, 
123456000)))
-                        // a TIMESTAMP_NS is a different kind and is not read 
as a TIMESTAMP_LTZ
-                        .fail(
+                        // a TIMESTAMP_NS keeps its nanoseconds
+                        .fromCase(
                                 VARIANT(),
                                 Variant.newBuilder()
                                         .of(LocalDateTime.of(2020, 1, 1, 12, 
0, 0, 123456789)),
-                                TableRuntimeException.class),
+                                TimestampData.fromInstant(
+                                        
Instant.parse("2020-01-01T12:00:00.123456789Z"))),
                 // A binary target pads a shorter value and truncates a longer 
one, matching a
                 // regular cast into the same type.
                 CastTestSpecBuilder.testCastTo(BINARY(4))

Reply via email to