diff --git a/docs/content.zh/docs/sql/reference/data-types.md b/docs/content.zh/docs/sql/reference/data-types.md index 9bc67dd733e92..1f39596b54c1e 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: @@ -1873,7 +1876,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 | !³ | !³ | !³ | 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 8e43c2ec12ee2..865f2d8fd0c44 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: @@ -1882,7 +1885,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 | !³ | !³ | !³ | 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 76c5222c168fb..8f9f35b3bf37d 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 @@ -123,7 +123,7 @@ void testNanosecondPrecisionVariant() { // 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 474e63b36709e..e213c47716435 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 8a161873e21f2..c458e1edac1d9 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 @@ private static void int4(StringBuilder buf, int i) { } 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 f6022d3f2cb20..f696227034e38 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 @@ private static Stream testData() { 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 9b42628498f9d..4a16d7fce8189 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 @@ * 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}. * - *

{@code CHARACTER_STRING} is handled by {@link VariantToStringCastRule}; {@code TIME} has no - * variant counterpart and is unsupported. + *

{@code CHARACTER_STRING} is handled by {@link VariantToStringCastRule}. */ class VariantToPrimitiveCastRule extends AbstractNullAwareCodeGeneratorCastRule { @@ -73,6 +72,7 @@ private static boolean isSupportedTarget(LogicalType targetType) { 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 @@ protected String generateCodeBlockInternal( 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 a743cf9ee5527..0bebbd2c815b7 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 @@ void testResolveVariantToPrimitive() { 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 274626557d172..cb4c8b46826d7 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 @@ -1831,6 +1831,24 @@ Stream testCases() { 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(), @@ -1985,7 +2003,28 @@ Stream testCases() { 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( @@ -1993,7 +2032,14 @@ Stream testCases() { 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(), @@ -2026,6 +2072,35 @@ Stream testCases() { 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(), @@ -2044,6 +2119,28 @@ Stream testCases() { 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 f4920a4d3532c..c26847c101696 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 @@ private static String decimalTarget(int precision, int scale) { } /** - * 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)); } /** @@ -340,15 +362,26 @@ private static String getVariantTypeAsString( 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( @@ -356,6 +389,13 @@ private static String getVariantTypeAsString( 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.