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))