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

twalthr pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/flink.git


The following commit(s) were added to refs/heads/master by this push:
     new 21f6a078766 [FLINK-40492][table] Support casting from VARIANT to TIME 
and nanosecond timestamps
21f6a078766 is described below

commit 21f6a0787663c7ab6ede99daf081e47065efc5e4
Author: Moritz Manner <[email protected]>
AuthorDate: Thu Sep 10 15:24:36 2026 +0200

    [FLINK-40492][table] Support casting from VARIANT to TIME and nanosecond 
timestamps
    
    This closes #29080.
---
 docs/content.zh/docs/sql/reference/data-types.md   |   5 +-
 docs/content/docs/sql/reference/data-types.md      |   5 +-
 .../flink/types/variant/BinaryVariantTest.java     |   2 +-
 .../types/logical/utils/LogicalTypeCasts.java      |   1 +
 .../apache/flink/table/utils/DateTimeUtils.java    |  22 ++---
 .../flink/table/types/LogicalTypeCastsTest.java    |   3 +-
 .../casting/VariantToPrimitiveCastRule.java        |  13 ++-
 .../functions/casting/CastRuleProviderTest.java    |   7 +-
 .../planner/functions/casting/CastRulesTest.java   | 101 ++++++++++++++++++++-
 .../table/runtime/functions/VariantCastUtils.java  |  66 +++++++++++---
 10 files changed, 189 insertions(+), 36 deletions(-)

diff --git a/docs/content.zh/docs/sql/reference/data-types.md 
b/docs/content.zh/docs/sql/reference/data-types.md
index fa20d4c273f..26fc40150b2 100644
--- a/docs/content.zh/docs/sql/reference/data-types.md
+++ b/docs/content.zh/docs/sql/reference/data-types.md
@@ -1554,6 +1554,7 @@ to make it fit. Otherwise `CAST` fails and `TRY_CAST` 
returns `NULL`.
 | 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 |
@@ -1572,6 +1573,8 @@ 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 **`TIME`** value keeps only millisecond precision, the resolution Flink's 
runtime `TIME` type
+  supports, so `TIME(4)` through `TIME(9)` behave like `TIME(3)`.
 
 To reach a type the table does not list, wrap the cast in a regular cast. Only 
the inner cast is a
 `VARIANT` cast, so the outer one applies the usual rules and may round, 
truncate, or overflow:
@@ -1874,7 +1877,7 @@ COALESCE(TRY_CAST('non-number' AS INT), 0) --- 结果返回数字 0 的 
INT 格
 | `ROW`                                  |                   Y                 
  |                    N                     |     N     |     N     |     N    
 |     N      |     N     |    N     |    N    |    N     |   N    |   N    |   
   N      |        N        |     N      |    N    |     N      |   N   |  !³   
|      N       |   N   |     N     |    N     |
 | `STRUCTURED`                           |                   Y                 
  |                    N                     |     N     |     N     |     N    
 |     N      |     N     |    N     |    N    |    N     |   N    |   N    |   
   N      |        N        |     N      |    N    |     N      |   N   |   N   
|      !³      |   N   |     N     |    N     |
 | `RAW`                                  |                   Y                 
  |                    !                     |     N     |     N     |     N    
 |     N      |     N     |    N     |    N    |    N     |   N    |   N    |   
   N      |        N        |     N      |    N    |     N      |   N   |   N   
|      N       |  Y⁴   |     N     |    N     |
-| `VARIANT`                              |                   !                 
  |                    !                     |     !     |     !     |     !    
 |     !      |     !     |    !     |    !    |    !     |   !    |   N    |   
   !      |        !        |     N      |   !³    |     N      |  !³   |  !³   
|      !³      |   N   |     Y     |    N     |
+| `VARIANT`                              |                   !                 
  |                    !                     |     !     |     !     |     !    
 |     !      |     !     |    !     |    !    |    !     |   !    |   !    |   
   !      |        !        |     N      |   !³    |     N      |  !³   |  !³   
|      !³      |   N   |     Y     |    N     |
 | `BITMAP`                               |                   Y                 
  |                   Y⁷                     |     N     |     N     |     N    
 |     N      |     N     |    N     |    N    |    N     |   N    |   N    |   
   N      |        N        |     N      |    N    |     N      |   N   |   N   
|      N       |   N   |     N     |    N     |
 
 备注:
diff --git a/docs/content/docs/sql/reference/data-types.md 
b/docs/content/docs/sql/reference/data-types.md
index e62f1997d29..1a687f6b972 100644
--- a/docs/content/docs/sql/reference/data-types.md
+++ b/docs/content/docs/sql/reference/data-types.md
@@ -1562,6 +1562,7 @@ to make it fit. Otherwise `CAST` fails and `TRY_CAST` 
returns `NULL`.
 | 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 |
@@ -1580,6 +1581,8 @@ 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 **`TIME`** value keeps only millisecond precision, the resolution Flink's 
runtime `TIME` type
+  supports, so `TIME(4)` through `TIME(9)` behave like `TIME(3)`.
 
 To reach a type the table does not list, wrap the cast in a regular cast. Only 
the inner cast is a
 `VARIANT` cast, so the outer one applies the usual rules and may round, 
truncate, or overflow:
@@ -1883,7 +1886,7 @@ The matrix below describes the supported cast pairs, 
where "Y" means supported,
 | `ROW`                                  |                   Y                 
  |                    N                     |     N     |     N     |     N    
 |     N      |     N     |    N     |    N    |    N     |   N    |   N    |   
   N      |        N        |     N      |    N    |     N      |   N   |  !³   
|      N       |   N   |     N     |    N     |
 | `STRUCTURED`                           |                   Y                 
  |                    N                     |     N     |     N     |     N    
 |     N      |     N     |    N     |    N    |    N     |   N    |   N    |   
   N      |        N        |     N      |    N    |     N      |   N   |   N   
|      !³      |   N   |     N     |    N     |
 | `RAW`                                  |                   Y                 
  |                    !                     |     N     |     N     |     N    
 |     N      |     N     |    N     |    N    |    N     |   N    |   N    |   
   N      |        N        |     N      |    N    |     N      |   N   |   N   
|      N       |  Y⁴   |     N     |    N     |
-| `VARIANT`                              |                   !                 
  |                    !                     |     !     |     !     |     !    
 |     !      |     !     |    !     |    !    |    !     |   !    |   N    |   
   !      |        !        |     N      |   !³    |     N      |  !³   |  !³   
|      !³      |   N   |     Y     |    N     |
+| `VARIANT`                              |                   !                 
  |                    !                     |     !     |     !     |     !    
 |     !      |     !     |    !     |    !    |    !     |   !    |   !    |   
   !      |        !        |     N      |   !³    |     N      |  !³   |  !³   
|      !³      |   N   |     Y     |    N     |
 | `BITMAP`                               |                   Y                 
  |                   Y⁷                     |     N     |     N     |     N    
 |     N      |     N     |    N     |    N    |    N     |   N    |   N    |   
   N      |        N        |     N      |    N    |     N      |   N   |   N   
|      N       |   N   |     N     |    N     |
 
 Notes:
diff --git 
a/flink-core/src/test/java/org/apache/flink/types/variant/BinaryVariantTest.java
 
b/flink-core/src/test/java/org/apache/flink/types/variant/BinaryVariantTest.java
index 5c4a6453dcc..d0d383604a5 100644
--- 
a/flink-core/src/test/java/org/apache/flink/types/variant/BinaryVariantTest.java
+++ 
b/flink-core/src/test/java/org/apache/flink/types/variant/BinaryVariantTest.java
@@ -128,7 +128,7 @@ class BinaryVariantTest {
 
         // Sub-microsecond precision switches to the nanosecond encoding 
instead of truncating,
         // but getInstant()/getDateTime() still work regardless of which 
encoding was picked.
-        Instant nanoInstant = 
Instant.now().truncatedTo(ChronoUnit.NANOS).plusNanos(123);
+        Instant nanoInstant = 
Instant.now().truncatedTo(ChronoUnit.MICROS).plusNanos(123);
         Variant instantVariant = builder.of(nanoInstant);
         
assertThat(instantVariant.getType()).isEqualTo(Variant.Type.TIMESTAMP_LTZ_NS);
         assertThat(instantVariant.getInstant()).isEqualTo(nanoInstant);
diff --git 
a/flink-table/flink-table-common/src/main/java/org/apache/flink/table/types/logical/utils/LogicalTypeCasts.java
 
b/flink-table/flink-table-common/src/main/java/org/apache/flink/table/types/logical/utils/LogicalTypeCasts.java
index 474e63b3670..e213c477164 100644
--- 
a/flink-table/flink-table-common/src/main/java/org/apache/flink/table/types/logical/utils/LogicalTypeCasts.java
+++ 
b/flink-table/flink-table-common/src/main/java/org/apache/flink/table/types/logical/utils/LogicalTypeCasts.java
@@ -359,6 +359,7 @@ public final class LogicalTypeCasts {
         castTo(TIME_WITHOUT_TIME_ZONE)
                 .implicitFrom(TIME_WITHOUT_TIME_ZONE, 
TIMESTAMP_WITHOUT_TIME_ZONE)
                 .explicitFromFamily(TIME, TIMESTAMP, CHARACTER_STRING)
+                .explicitFrom(VARIANT)
                 .injectiveFrom(WHEN_PRECISION_MATCHES, TIME_WITHOUT_TIME_ZONE)
                 .build();
 
diff --git 
a/flink-table/flink-table-common/src/main/java/org/apache/flink/table/utils/DateTimeUtils.java
 
b/flink-table/flink-table-common/src/main/java/org/apache/flink/table/utils/DateTimeUtils.java
index 8a161873e21..c458e1edac1 100644
--- 
a/flink-table/flink-table-common/src/main/java/org/apache/flink/table/utils/DateTimeUtils.java
+++ 
b/flink-table/flink-table-common/src/main/java/org/apache/flink/table/utils/DateTimeUtils.java
@@ -1725,20 +1725,18 @@ public class DateTimeUtils {
     }
 
     public static TimestampData truncate(TimestampData ts, int precision) {
-        String fraction = Integer.toString(ts.toLocalDateTime().getNano());
-        if (fraction.length() <= precision) {
+        // A timestamp holds at most nine fractional-second digits, so a 
higher precision keeps it
+        // unchanged. Otherwise, the digits beyond the precision are zeroed 
out.
+        if (precision >= 9) {
             return ts;
-        } else {
-            // need to truncate
-            if (precision <= 3) {
-                return TimestampData.fromEpochMillis(
-                        zeroLastDigits(ts.getMillisecond(), 3 - precision));
-            } else {
-                return TimestampData.fromEpochMillis(
-                        ts.getMillisecond(),
-                        (int) zeroLastDigits(ts.getNanoOfMillisecond(), 9 - 
precision));
-            }
         }
+        if (precision <= 3) {
+            return TimestampData.fromEpochMillis(
+                    zeroLastDigits(ts.getMillisecond(), 3 - precision));
+        }
+        return TimestampData.fromEpochMillis(
+                ts.getMillisecond(),
+                (int) zeroLastDigits(ts.getNanoOfMillisecond(), 9 - 
precision));
     }
 
     public static int truncate(int time, int precision) {
diff --git 
a/flink-table/flink-table-common/src/test/java/org/apache/flink/table/types/LogicalTypeCastsTest.java
 
b/flink-table/flink-table-common/src/test/java/org/apache/flink/table/types/LogicalTypeCastsTest.java
index f6022d3f2cb..f696227034e 100644
--- 
a/flink-table/flink-table-common/src/test/java/org/apache/flink/table/types/LogicalTypeCastsTest.java
+++ 
b/flink-table/flink-table-common/src/test/java/org/apache/flink/table/types/LogicalTypeCastsTest.java
@@ -289,10 +289,9 @@ class LogicalTypeCastsTest {
                         true),
                 Arguments.of(new VariantType(), new CharType(), false, true),
                 Arguments.of(new VariantType(), VarCharType.STRING_TYPE, 
false, true),
+                Arguments.of(new VariantType(), new TimeType(), false, true),
                 // variant identity cast is implicit
                 Arguments.of(new VariantType(), new VariantType(), true, true),
-                // TIME has no variant counterpart, so it is not castable from 
variant
-                Arguments.of(new VariantType(), new TimeType(), false, false),
                 // A variant imposes a schema on a constructed target, 
explicit only, recursing on
                 // every leaf, which is itself a VARIANT cast
                 Arguments.of(new VariantType(), new ArrayType(new IntType()), 
false, true),
diff --git 
a/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/functions/casting/VariantToPrimitiveCastRule.java
 
b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/functions/casting/VariantToPrimitiveCastRule.java
index 9b42628498f..4a16d7fce81 100644
--- 
a/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/functions/casting/VariantToPrimitiveCastRule.java
+++ 
b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/functions/casting/VariantToPrimitiveCastRule.java
@@ -43,8 +43,7 @@ import static 
org.apache.flink.table.planner.functions.casting.CastRuleUtils.str
  * the kind itself is not implicit, so a decimal is not read as an integer and 
a {@code TIMESTAMP}
  * is not read as a {@code TIMESTAMP_LTZ}.
  *
- * <p>{@code CHARACTER_STRING} is handled by {@link VariantToStringCastRule}; 
{@code TIME} has no
- * variant counterpart and is unsupported.
+ * <p>{@code CHARACTER_STRING} is handled by {@link VariantToStringCastRule}.
  */
 class VariantToPrimitiveCastRule extends 
AbstractNullAwareCodeGeneratorCastRule<Variant, Object> {
 
@@ -73,6 +72,7 @@ class VariantToPrimitiveCastRule extends 
AbstractNullAwareCodeGeneratorCastRule<
             case BINARY:
             case VARBINARY:
             case DATE:
+            case TIME_WITHOUT_TIME_ZONE:
             case TIMESTAMP_WITHOUT_TIME_ZONE:
             case TIMESTAMP_WITH_LOCAL_TIME_ZONE:
                 return true;
@@ -174,6 +174,15 @@ class VariantToPrimitiveCastRule extends 
AbstractNullAwareCodeGeneratorCastRule<
                         returnVariable,
                         cast("int", methodCall(methodCall(inputTerm, 
"getDate"), "toEpochDay")));
                 break;
+            case TIME_WITHOUT_TIME_ZONE:
+                writer.assignStmt(
+                        returnVariable,
+                        staticCall(
+                                VariantCastUtils.class,
+                                "toTime",
+                                inputTerm,
+                                
LogicalTypeChecks.getPrecision(targetLogicalType)));
+                break;
             case TIMESTAMP_WITHOUT_TIME_ZONE:
                 writer.assignStmt(
                         returnVariable,
diff --git 
a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/functions/casting/CastRuleProviderTest.java
 
b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/functions/casting/CastRuleProviderTest.java
index a743cf9ee55..0bebbd2c815 100644
--- 
a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/functions/casting/CastRuleProviderTest.java
+++ 
b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/functions/casting/CastRuleProviderTest.java
@@ -18,6 +18,7 @@
 
 package org.apache.flink.table.planner.functions.casting;
 
+import org.apache.flink.table.api.DataTypes;
 import org.apache.flink.table.catalog.ObjectIdentifier;
 import org.apache.flink.table.types.logical.ArrayType;
 import org.apache.flink.table.types.logical.CharType;
@@ -132,11 +133,13 @@ class CastRuleProviderTest {
         assertThat(CastRuleProvider.exists(VARIANT, 
DATE().getLogicalType())).isTrue();
         assertThat(CastRuleProvider.exists(VARIANT, 
TIMESTAMP().getLogicalType())).isTrue();
         assertThat(CastRuleProvider.exists(VARIANT, 
TIMESTAMP_LTZ().getLogicalType())).isTrue();
+        assertThat(CastRuleProvider.exists(VARIANT, 
TIME().getLogicalType())).isTrue();
         assertThat(CastRuleProvider.exists(VARIANT, 
BYTES().getLogicalType())).isTrue();
         assertThat(CastRuleProvider.canFail(VARIANT, INT)).isTrue();
 
-        // TIME has no variant counterpart and is not castable
-        assertThat(CastRuleProvider.exists(VARIANT, 
TIME().getLogicalType())).isFalse();
+        // INTERVAL has no VARIANT counterpart, so it is not a castable target
+        assertThat(CastRuleProvider.exists(VARIANT, 
INTERVAL(DataTypes.DAY()).getLogicalType()))
+                .isFalse();
         // character strings keep going through the display-oriented rule
         assertThat(CastRuleProvider.resolve(VARIANT, STRING_TYPE))
                 .isSameAs(VariantToStringCastRule.INSTANCE);
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 f5e98a0fc0a..2aa04aeab2d 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
@@ -1846,6 +1846,24 @@ class CastRulesTest {
                                 CET_CONTEXT,
                                 VARIANT_BUILDER.of(TIMESTAMP.toInstant()),
                                 TIMESTAMP_STRING_CET)
+                        // a time renders at millisecond resolution, the same 
as a regular TIME to
+                        // string cast, so the stored microseconds are 
truncated
+                        .fromCase(
+                                VARIANT(),
+                                Variant.newBuilder().of(LocalTime.of(12, 34, 
56, 123_456_000)),
+                                fromString("12:34:56.123"))
+                        // a nanosecond timestamp keeps its full precision 
when rendered
+                        .fromCase(
+                                VARIANT(),
+                                Variant.newBuilder()
+                                        .of(LocalDateTime.of(2021, 9, 24, 12, 
34, 56, 123_456_789)),
+                                fromString("2021-09-24 12:34:56.123456789"))
+                        .fromCase(
+                                VARIANT(),
+                                CET_CONTEXT,
+                                Variant.newBuilder()
+                                        
.of(Instant.parse("2021-09-24T12:34:56.123456789Z")),
+                                fromString("2021-09-24 14:34:56.123456789"))
                         // a binary value is read as UTF-8, like a regular 
BINARY to string cast
                         .fromCase(
                                 VARIANT(),
@@ -2021,7 +2039,28 @@ class CastRulesTest {
                                 VARIANT(),
                                 VARIANT_BUILDER.of(LocalDate.of(2020, 1, 1)),
                                 (int) LocalDate.of(2020, 1, 1).toEpochDay())
-                        .fail(VARIANT(), VARIANT_BUILDER.of(1), 
TableRuntimeException.class),
+                        .fail(VARIANT(), Variant.newBuilder().of(1), 
TableRuntimeException.class),
+                // 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))
+                        .fromCase(
+                                VARIANT(),
+                                Variant.newBuilder()
+                                        .of(LocalTime.of(12, 0, 
0).plus(Duration.ofMillis(123))),
+                                DateTimeUtils.toInternal(
+                                        LocalTime.of(12, 0, 
0).plus(Duration.ofMillis(123))))
+                        .fromCase(
+                                VARIANT(),
+                                Variant.newBuilder().of(LocalTime.of(12, 0, 0, 
123_456_000)),
+                                DateTimeUtils.toInternal(LocalTime.of(12, 0, 
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.
+                CastTestSpecBuilder.testCastTo(TIME(6))
+                        .fromCase(
+                                VARIANT(),
+                                Variant.newBuilder().of(LocalTime.of(12, 0, 0, 
123_456_789)),
+                                DateTimeUtils.toInternal(LocalTime.of(12, 0, 
0, 123_000_000))),
                 CastTestSpecBuilder.testCastTo(TIMESTAMP())
                         .fromCase(VARIANT(), null, null)
                         .fromCase(
@@ -2029,7 +2068,14 @@ class CastRulesTest {
                                 VARIANT_BUILDER.of(LocalDateTime.of(2020, 1, 
1, 12, 0, 0)),
                                 TimestampData.fromLocalDateTime(
                                         LocalDateTime.of(2020, 1, 1, 12, 0, 
0)))
-                        .fail(VARIANT(), VARIANT_BUILDER.of(1), 
TableRuntimeException.class)
+                        // the default precision keeps microseconds, 
truncating the nanoseconds
+                        .fromCase(
+                                VARIANT(),
+                                Variant.newBuilder()
+                                        .of(LocalDateTime.of(2020, 1, 1, 12, 
0, 0, 123456789)),
+                                TimestampData.fromLocalDateTime(
+                                        LocalDateTime.of(2020, 1, 1, 12, 0, 0, 
123456000)))
+                        .fail(VARIANT(), Variant.newBuilder().of(1), 
TableRuntimeException.class)
                         // a TIMESTAMP_LTZ is a different kind and is not read 
as a TIMESTAMP
                         .fail(
                                 VARIANT(),
@@ -2062,6 +2108,35 @@ class CastRulesTest {
                                         LocalDateTime.of(2020, 1, 1, 12, 0, 0, 
123000000)),
                                 TimestampData.fromLocalDateTime(
                                         LocalDateTime.of(2020, 1, 1, 12, 0, 
0))),
+                // A fraction with leading zeros (.000123456) is still 
truncated to the precision.
+                CastTestSpecBuilder.testCastTo(TIMESTAMP(6))
+                        .fromCase(
+                                VARIANT(),
+                                Variant.newBuilder()
+                                        .of(LocalDateTime.of(2020, 1, 1, 12, 
0, 0, 123_456)),
+                                TimestampData.fromLocalDateTime(
+                                        LocalDateTime.of(2020, 1, 1, 12, 0, 0, 
123_000))),
+                // The cast accepts either storage kind: TIMESTAMP_NS for a 
value that needs
+                // nanosecond precision, plain TIMESTAMP when microseconds 
already hold it exactly.
+                CastTestSpecBuilder.testCastTo(TIMESTAMP(9))
+                        .fromCase(
+                                VARIANT(),
+                                Variant.newBuilder()
+                                        .of(LocalDateTime.of(2020, 1, 1, 12, 
0, 0, 123456789)),
+                                TimestampData.fromLocalDateTime(
+                                        LocalDateTime.of(2020, 1, 1, 12, 0, 0, 
123456789)))
+                        .fromCase(
+                                VARIANT(),
+                                Variant.newBuilder()
+                                        .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(
+                                VARIANT(),
+                                Variant.newBuilder()
+                                        
.of(Instant.ofEpochSecond(1_600_000_000L, 123456789)),
+                                TableRuntimeException.class),
                 CastTestSpecBuilder.testCastTo(TIMESTAMP_LTZ())
                         .fromCase(
                                 VARIANT(),
@@ -2080,6 +2155,28 @@ class CastRulesTest {
                                         Instant.ofEpochSecond(1_600_000_000L, 
123456000)),
                                 TimestampData.fromInstant(
                                         Instant.ofEpochSecond(1_600_000_000L, 
123000000))),
+                // The cast accepts either storage kind: TIMESTAMP_LTZ_NS for 
a value that needs
+                // nanosecond precision, plain TIMESTAMP_LTZ when microseconds 
already hold it
+                // exactly.
+                CastTestSpecBuilder.testCastTo(TIMESTAMP_LTZ(9))
+                        .fromCase(
+                                VARIANT(),
+                                Variant.newBuilder()
+                                        
.of(Instant.ofEpochSecond(1_600_000_000L, 123456789)),
+                                TimestampData.fromInstant(
+                                        Instant.ofEpochSecond(1_600_000_000L, 
123456789)))
+                        .fromCase(
+                                VARIANT(),
+                                Variant.newBuilder()
+                                        
.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(
+                                VARIANT(),
+                                Variant.newBuilder()
+                                        .of(LocalDateTime.of(2020, 1, 1, 12, 
0, 0, 123456789)),
+                                TableRuntimeException.class),
                 // A binary target pads a shorter value and truncates a longer 
one, matching a
                 // regular cast into the same type.
                 CastTestSpecBuilder.testCastTo(BINARY(4))
diff --git 
a/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/functions/VariantCastUtils.java
 
b/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/functions/VariantCastUtils.java
index ddd86cdda64..561da100cee 100644
--- 
a/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/functions/VariantCastUtils.java
+++ 
b/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/functions/VariantCastUtils.java
@@ -58,9 +58,13 @@ public final class VariantCastUtils {
      */
     private static final double LONG_MAGNITUDE_LIMIT = -(double) 
Long.MIN_VALUE;
 
-    /** A variant stores a timestamp with microsecond precision. */
     private static final int TIMESTAMP_PRECISION = 6;
 
+    private static final int TIMESTAMP_NANOS_PRECISION = 9;
+
+    // TIME is millisecond-of-day at runtime, so it renders with three 
fractional-second digits.
+    private static final int TIME_PRECISION = 3;
+
     private VariantCastUtils() {}
 
     /**
@@ -229,24 +233,42 @@ public final class VariantCastUtils {
     }
 
     /**
-     * Reads a timestamp variant as the target {@code TIMESTAMP}. A variant 
keeps microseconds, so
-     * 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}. {@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.
      */
     public static TimestampData toTimestamp(Variant variant, int precision) {
-        if (variant.getType() != Variant.Type.TIMESTAMP) {
-            throw unsupportedKind(variant, String.format("TIMESTAMP(%d)", 
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);
         }
-        return DateTimeUtils.truncate(
-                TimestampData.fromLocalDateTime(variant.getDateTime()), 
precision);
+        throw unsupportedKind(variant, String.format("TIMESTAMP(%d)", 
precision));
     }
 
     /** Reads a timestamp with local time zone variant. See {@link 
#toTimestamp(Variant, int)}. */
     public static TimestampData toTimestampLtz(Variant variant, int precision) 
{
-        if (variant.getType() != Variant.Type.TIMESTAMP_LTZ) {
-            throw unsupportedKind(variant, String.format("TIMESTAMP_LTZ(%d)", 
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);
+        }
+        throw unsupportedKind(variant, String.format("TIMESTAMP_LTZ(%d)", 
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.
+     */
+    public static int toTime(Variant variant, int precision) {
+        if (variant.getType() == Variant.Type.TIME) {
+            return DateTimeUtils.applyTimePrecisionTruncation(
+                    DateTimeUtils.toInternal(variant.getTime()), precision);
         }
-        return 
DateTimeUtils.truncate(TimestampData.fromInstant(variant.getInstant()), 
precision);
+        throw unsupportedKind(variant, String.format("TIME(%d)", precision));
     }
 
     /**
@@ -377,15 +399,26 @@ public final class VariantCastUtils {
             case DATE:
                 value = DateTimeUtils.formatDate((int) 
variant.getDate().toEpochDay());
                 break;
+            case TIME:
+                value =
+                        DateTimeUtils.formatTimestampMillis(
+                                DateTimeUtils.toInternal(variant.getTime()), 
TIME_PRECISION);
+                break;
             case TIMESTAMP:
-                // A wall-clock value needs no zone shift, which is what 
UTC_ZONE achieves here. A
-                // variant keeps microseconds, so the precision is always 6.
+                // A wall-clock value needs no zone shift, which is what 
UTC_ZONE achieves here.
                 value =
                         DateTimeUtils.formatTimestamp(
                                 
TimestampData.fromLocalDateTime(variant.getDateTime()),
                                 DateTimeUtils.UTC_ZONE,
                                 TIMESTAMP_PRECISION);
                 break;
+            case TIMESTAMP_NS:
+                value =
+                        DateTimeUtils.formatTimestamp(
+                                
TimestampData.fromLocalDateTime(variant.getDateTime()),
+                                DateTimeUtils.UTC_ZONE,
+                                TIMESTAMP_NANOS_PRECISION);
+                break;
             case TIMESTAMP_LTZ:
                 value =
                         DateTimeUtils.formatTimestamp(
@@ -393,6 +426,13 @@ public final class VariantCastUtils {
                                 sessionZone,
                                 TIMESTAMP_PRECISION);
                 break;
+            case TIMESTAMP_LTZ_NS:
+                value =
+                        DateTimeUtils.formatTimestamp(
+                                
TimestampData.fromInstant(variant.getInstant()),
+                                sessionZone,
+                                TIMESTAMP_NANOS_PRECISION);
+                break;
             case NULL:
                 // Only reachable for a NOT NULL target. A nullable target 
maps a null-valued
                 // variant to SQL NULL before this method is called.

Reply via email to