AHeise commented on code in PR #29311: URL: https://github.com/apache/flink/pull/29311#discussion_r4152827265
########## flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/functions/casting/PrimitiveToVariantCastRule.java: ########## @@ -0,0 +1,175 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.flink.table.planner.functions.casting; + +import org.apache.flink.table.runtime.functions.VariantCastUtils; +import org.apache.flink.table.types.logical.LogicalType; +import org.apache.flink.table.types.logical.LogicalTypeRoot; +import org.apache.flink.table.types.logical.utils.LogicalTypeChecks; +import org.apache.flink.types.variant.Variant; + +import static org.apache.flink.table.planner.functions.casting.CastRuleUtils.staticCall; + +/** + * Cast rule from a primitive type to {@link LogicalTypeRoot#VARIANT}. + * + * <p>The value keeps the kind of its SQL type. For example, a {@code BIGINT} is stored as a {@code + * BIGINT}, even when it would fit a smaller kind. A {@code NaN} or infinite {@code FLOAT} or {@code + * DOUBLE} is stored as is, although {@code PARSE_JSON} rejects it. Some types can fail for some + * values, see {@link #canFail}. + */ +class PrimitiveToVariantCastRule extends AbstractExpressionCodeGeneratorCastRule<Object, Variant> { + + static final PrimitiveToVariantCastRule INSTANCE = new PrimitiveToVariantCastRule(); + + /** A character takes up to 4 bytes in UTF-8, which a declared length counts as one. */ + private static final int MAX_UTF8_BYTES_PER_CHAR = 4; + + private PrimitiveToVariantCastRule() { + super( + CastRulePredicate.builder() + .predicate( + (input, target) -> + target.is(LogicalTypeRoot.VARIANT) + && isSupportedSource(input)) + .build()); + } + + private static boolean isSupportedSource(LogicalType inputType) { + switch (inputType.getTypeRoot()) { + case BOOLEAN: + case TINYINT: + case SMALLINT: + case INTEGER: + case BIGINT: + case FLOAT: + case DOUBLE: + case DECIMAL: + case CHAR: + case VARCHAR: + case BINARY: + case VARBINARY: + case DATE: + case TIME_WITHOUT_TIME_ZONE: + case TIMESTAMP_WITHOUT_TIME_ZONE: + case TIMESTAMP_WITH_LOCAL_TIME_ZONE: + case UUID: + return true; + default: + return false; + } + } + + /** + * Returns whether a value of the input type can fail the cast, so that {@code TRY_CAST} returns + * {@code NULL} for it instead of failing. Two kinds of types can fail: + * + * <ul> + * <li>A {@code TIMESTAMP(p)} or {@code TIMESTAMP_LTZ(p)} with {@code p > 6} is stored with + * nanoseconds, which only cover 1677-09-21 to 2262-04-11. + * <li>A string or binary value over {@link VariantCastUtils#MAX_PAYLOAD_BYTES} does not fit + * into a {@code VARIANT}. Only a type whose declared length allows such a value can fail. + * </ul> + * + * <p>Every other type never fails. + */ + @Override + public boolean canFail(LogicalType inputLogicalType, LogicalType targetLogicalType) { Review Comment: FYI: Flink doesn't enforce a declared `VARCHAR(n)`/`VARBINARY(n)` length at runtime; a source can hand over longer values. A `VARCHAR(100)` holding more than 16 MiB would still throw under `TRY_CAST`. That's pathological, and I'm fine with relying on the declared length. Maybe say so in the Javadoc. ########## flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/functions/CastFunctionITCase.java: ########## @@ -432,6 +435,272 @@ private static List<TestSetSpec> variantPrimitiveCasts() { TINYINT())); } + private static List<TestSetSpec> primitiveToVariantCasts() { + final VariantBuilder builder = Variant.newBuilder(); + final LocalDateTime nanos = LocalDateTime.parse("2026-09-25T10:15:30.123456789"); + final Instant instant = Instant.parse("2026-09-25T10:15:30.123Z"); + return List.of( + // A value keeps the kind of its SQL type, so an integer keeps its width, and a + // string is wrapped rather than parsed. + CastTestSpecBuilder.testCastTo(VARIANT()) + .fromCase(BOOLEAN(), true, builder.of(true)) + .fromCase(INT(), 42, builder.of(42)) + .fromCase(BIGINT(), 1L, builder.of(1L)) + .fromCase(BIGINT(), 10000000000L, builder.of(10000000000L)) + .fromCase(DOUBLE(), 1.5d, builder.of(1.5d)) + .fromCase( + DECIMAL(4, 2), + new BigDecimal("12.50"), + builder.of(new BigDecimal("12.50"))) + .fromCase(STRING(), "{\"a\":1}", builder.of("{\"a\":1}")) + .fromCase( + DATE(), + LocalDate.parse("2026-09-25"), + builder.of(LocalDate.parse("2026-09-25"))) + .fromCase(TIMESTAMP(9), nanos, builder.of(nanos)) + .fromCase(TIMESTAMP_LTZ(3), instant, builder.of(instant)) + .fromCase(UUID(), DEFAULT_UUID, builder.of(DEFAULT_UUID)) + .fromCase(INT(), null, null) + // a type without a VARIANT kind is rejected at validation + .failValidation(INTERVAL(MONTH()), Period.ofMonths(2)) + .build(), + TestSetSpec.forExpression("Cast a primitive to VARIANT and back") + .onFieldsWithData( + 42L, + new BigDecimal("12.50"), + "hello", + LocalTime.of(10, 15, 30), + nanos, + instant, + DEFAULT_UUID, + new byte[] {1, 2, 3}) + .andDataTypes( + BIGINT(), + DECIMAL(4, 2), + STRING(), + TIME(), + TIMESTAMP(9), + TIMESTAMP_LTZ(3), + UUID(), + BYTES()) + .testResult( + $("f0").cast(VARIANT()).cast(BIGINT()), + "CAST(CAST(f0 AS VARIANT) AS BIGINT)", + 42L, + BIGINT()) + .testResult( + $("f1").cast(VARIANT()).cast(DECIMAL(4, 2)), + "CAST(CAST(f1 AS VARIANT) AS DECIMAL(4, 2))", + new BigDecimal("12.50"), + DECIMAL(4, 2)) + .testResult( + $("f2").cast(VARIANT()).cast(STRING()), + "CAST(CAST(f2 AS VARIANT) AS STRING)", + "hello", + STRING()) + .testResult( + $("f3").cast(VARIANT()).cast(TIME()), + "CAST(CAST(f3 AS VARIANT) AS TIME)", + LocalTime.of(10, 15, 30), + TIME()) + .testResult( + $("f4").cast(VARIANT()).cast(TIMESTAMP(9)), + "CAST(CAST(f4 AS VARIANT) AS TIMESTAMP(9))", + nanos, + TIMESTAMP(9)) + .testResult( + $("f5").cast(VARIANT()).cast(TIMESTAMP_LTZ(3)), + "CAST(CAST(f5 AS VARIANT) AS TIMESTAMP_LTZ(3))", + instant, + TIMESTAMP_LTZ(3)) + .testResult( + $("f6").cast(VARIANT()).cast(UUID()), + "CAST(CAST(f6 AS VARIANT) AS UUID)", + DEFAULT_UUID, + UUID()) + .testResult( + $("f7").cast(VARIANT()).cast(BYTES()), + "CAST(CAST(f7 AS VARIANT) AS BYTES)", + new byte[] {1, 2, 3}, + BYTES()), + TestSetSpec.forExpression("Cast TIME to VARIANT and back") + .onFieldsWithData( + LocalTime.of(12, 34, 56, 123_000_000), + LocalTime.MIDNIGHT, + LocalTime.of(23, 59, 59, 999_000_000)) + .andDataTypes(TIME(3), TIME(0), TIME(3)) + .testResult( + $("f0").cast(VARIANT()).cast(TIME(3)), + "CAST(CAST(f0 AS VARIANT) AS TIME(3))", + LocalTime.of(12, 34, 56, 123_000_000), + TIME(3)) + .testResult( + $("f1").cast(VARIANT()).cast(TIME(0)), + "CAST(CAST(f1 AS VARIANT) AS TIME(0))", + LocalTime.MIDNIGHT, + TIME(0)) + .testResult( + $("f2").cast(VARIANT()).cast(TIME(3)), + "CAST(CAST(f2 AS VARIANT) AS TIME(3))", + LocalTime.of(23, 59, 59, 999_000_000), + TIME(3)), + // a VARIANT holds at most 16 MiB, so a longer string fails CAST and TRY_CAST + // returns NULL + TestSetSpec.forExpression("Cast a string over the size limit to VARIANT") + .onFieldsWithData("x") + .andDataTypes(STRING()) + .testSqlRuntimeError( + "CAST(REPEAT(f0, 17000000) AS VARIANT)", + TableRuntimeException.class, + "A VARIANT is limited to 16 MiB") + .testTableApiRuntimeError( + $("f0").repeat(17000000).cast(VARIANT()), + TableRuntimeException.class, + "A VARIANT is limited to 16 MiB") + .testResult( + $("f0").repeat(17000000).tryCast(VARIANT()), + "TRY_CAST(REPEAT(f0, 17000000) AS VARIANT)", + null, + VARIANT()), + // nanoseconds only cover 1677-09-21 to 2262-04-11, while microseconds hold any year + TestSetSpec.forExpression("Cast a late TIMESTAMP to VARIANT") + .onFieldsWithData( + LocalDateTime.parse("3000-01-01T00:00"), + LocalDateTime.parse("3000-01-01T00:00")) + .andDataTypes(TIMESTAMP(9), TIMESTAMP(6)) + .testSqlRuntimeError( + "CAST(f0 AS VARIANT)", + TableRuntimeException.class, + "1677-09-21 to 2262-04-11") + .testTableApiRuntimeError( + $("f0").cast(VARIANT()), + TableRuntimeException.class, + "1677-09-21 to 2262-04-11") + .testResult( + $("f0").tryCast(VARIANT()), + "TRY_CAST(f0 AS VARIANT)", + null, + VARIANT()) + .testResult( + $("f1").cast(VARIANT()).cast(TIMESTAMP(6)), + "CAST(CAST(f1 AS VARIANT) AS TIMESTAMP(6))", + LocalDateTime.parse("3000-01-01T00:00"), + TIMESTAMP(6)), + TestSetSpec.forExpression("Cast a literal to VARIANT") + .onFieldsWithData(0) + .testResult( + lit(42).cast(VARIANT()), + "CAST(42 AS VARIANT)", + builder.of(42), + VARIANT().notNull()) + .testResult( + lit("hello").cast(VARIANT()), + "CAST('hello' AS VARIANT)", + builder.of("hello"), + VARIANT().notNull()) + // a NULL literal casts to a SQL NULL, not to a variant null + .testSqlResult("CAST(NULL AS VARIANT)", null, VARIANT()) + .testSqlResult("TRY_CAST(NULL AS VARIANT)", null, VARIANT()), + // a VARIANT is not limited to JSON, so NaN and infinity are kept and round-trip + TestSetSpec.forExpression("Cast a NaN literal to VARIANT and back") + .onFieldsWithData(0) + .testResult( + lit("NaN").cast(DOUBLE()).cast(VARIANT()).cast(DOUBLE()), + "CAST(CAST(CAST('NaN' AS DOUBLE) AS VARIANT) AS DOUBLE)", + Double.NaN, + DOUBLE()) + .testResult( + lit("NaN").cast(DOUBLE()).cast(VARIANT()).cast(STRING()), + "CAST(CAST(CAST('NaN' AS DOUBLE) AS VARIANT) AS STRING)", + "NaN", + STRING()), + TestSetSpec.forExpression("Cast a non-finite number to VARIANT and back") + // the non-finite values are produced at runtime from strings + .onFieldsWithData("NaN", "Infinity", "-Infinity") + .andDataTypes(STRING(), STRING(), STRING()) + .testResult( + $("f0").cast(DOUBLE()).cast(VARIANT()).cast(DOUBLE()), + "CAST(CAST(CAST(f0 AS DOUBLE) AS VARIANT) AS DOUBLE)", + Double.NaN, + DOUBLE()) + .testResult( + $("f1").cast(FLOAT()).cast(VARIANT()).cast(FLOAT()), + "CAST(CAST(CAST(f1 AS FLOAT) AS VARIANT) AS FLOAT)", + Float.POSITIVE_INFINITY, + FLOAT()) + .testResult( + $("f2").cast(DOUBLE()).cast(VARIANT()).cast(FLOAT()), + "CAST(CAST(CAST(f2 AS DOUBLE) AS VARIANT) AS FLOAT)", + Float.NEGATIVE_INFINITY, + FLOAT()) + // an integer cannot hold NaN, so that cast still fails + .testSqlRuntimeError( + "CAST(CAST(CAST(f0 AS DOUBLE) AS VARIANT) AS INT)", + TableRuntimeException.class, + "overflowed"), + TestSetSpec.forExpression("Cast narrow and fixed-width types to VARIANT and back") + .onFieldsWithData((byte) 42, (short) 1000) + .andDataTypes(TINYINT(), SMALLINT()) + .testResult( + $("f0").cast(VARIANT()).cast(TINYINT()), + "CAST(CAST(f0 AS VARIANT) AS TINYINT)", + (byte) 42, + TINYINT()) + .testResult( + $("f1").cast(VARIANT()).cast(SMALLINT()), + "CAST(CAST(f1 AS VARIANT) AS SMALLINT)", + (short) 1000, + SMALLINT()) + // a CHAR(n) is stored with its padding, so the spaces survive + .testResult( + lit("ab").cast(CHAR(4)).cast(VARIANT()).cast(STRING()), + "CAST(CAST(CAST('ab' AS CHAR(4)) AS VARIANT) AS STRING)", + "ab ", + STRING()) + .testResult( + lit("ab").cast(CHAR(4)).cast(VARIANT()).cast(CHAR(4)), + "CAST(CAST(CAST('ab' AS CHAR(4)) AS VARIANT) AS CHAR(4))", + "ab ", + CHAR(4)) + // a BINARY(n) is stored with its zero padding + .testResult( + lit(new byte[] {1, 2}) + .cast(BINARY(4)) + .cast(VARIANT()) + .cast(BYTES()), + "CAST(CAST(CAST(X'0102' AS BINARY(4)) AS VARIANT) AS BYTES)", + new byte[] {1, 2, 0, 0}, + BYTES()), + // a constructed type casts element by element when each child casts to VARIANT + TestSetSpec.forExpression("Cast a constructed type to one of VARIANT and back") Review Comment: nit: "to one of VARIANT" reads oddly. Maybe "Cast a constructed type to VARIANT elements and back"? ########## flink-table/flink-table-common/src/main/java/org/apache/flink/table/types/logical/utils/LogicalTypeCasts.java: ########## @@ -403,6 +403,33 @@ public final class LogicalTypeCasts { .explicitFromFamily(EXACT_NUMERIC, CHARACTER_STRING) .build(); + // ----------------------------------------------------------------------------------------- + // VARIANT type + // ----------------------------------------------------------------------------------------- + + // Only a type with a VARIANT kind that holds its value without loss casts to VARIANT. + castTo(VARIANT) Review Comment: The element-wise argument also covers MAP keys and MULTISET elements, so `CAST(MAP[1, 'a'] AS MAP<VARIANT, STRING>)` and `MULTISET<INT>` → `MULTISET<VARIANT>` validate as well. Neither is tested. `BinaryVariant#equals`/`hashCode` compare bytes, so a key cast from `INT` doesn't match the same number from `BIGINT` or `PARSE_JSON('1')` (TINYINT), although the description treats integer widths as one equivalence class. `MAP<VARIANT, ...>` was already declarable, so this isn't new per se, but the cast makes it easy to produce. Should we reject a VARIANT key/element here until key equality is defined, or at least add a test and a sentence in the docs? ########## flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/functions/CastFunctionITCase.java: ########## @@ -432,6 +435,272 @@ private static List<TestSetSpec> variantPrimitiveCasts() { TINYINT())); } + private static List<TestSetSpec> primitiveToVariantCasts() { + final VariantBuilder builder = Variant.newBuilder(); + final LocalDateTime nanos = LocalDateTime.parse("2026-09-25T10:15:30.123456789"); + final Instant instant = Instant.parse("2026-09-25T10:15:30.123Z"); + return List.of( + // A value keeps the kind of its SQL type, so an integer keeps its width, and a + // string is wrapped rather than parsed. + CastTestSpecBuilder.testCastTo(VARIANT()) + .fromCase(BOOLEAN(), true, builder.of(true)) + .fromCase(INT(), 42, builder.of(42)) + .fromCase(BIGINT(), 1L, builder.of(1L)) + .fromCase(BIGINT(), 10000000000L, builder.of(10000000000L)) + .fromCase(DOUBLE(), 1.5d, builder.of(1.5d)) + .fromCase( + DECIMAL(4, 2), + new BigDecimal("12.50"), + builder.of(new BigDecimal("12.50"))) + .fromCase(STRING(), "{\"a\":1}", builder.of("{\"a\":1}")) + .fromCase( + DATE(), + LocalDate.parse("2026-09-25"), + builder.of(LocalDate.parse("2026-09-25"))) + .fromCase(TIMESTAMP(9), nanos, builder.of(nanos)) + .fromCase(TIMESTAMP_LTZ(3), instant, builder.of(instant)) + .fromCase(UUID(), DEFAULT_UUID, builder.of(DEFAULT_UUID)) + .fromCase(INT(), null, null) + // a type without a VARIANT kind is rejected at validation + .failValidation(INTERVAL(MONTH()), Period.ofMonths(2)) + .build(), + TestSetSpec.forExpression("Cast a primitive to VARIANT and back") + .onFieldsWithData( + 42L, + new BigDecimal("12.50"), + "hello", + LocalTime.of(10, 15, 30), + nanos, + instant, + DEFAULT_UUID, + new byte[] {1, 2, 3}) + .andDataTypes( + BIGINT(), + DECIMAL(4, 2), + STRING(), + TIME(), + TIMESTAMP(9), + TIMESTAMP_LTZ(3), + UUID(), + BYTES()) + .testResult( + $("f0").cast(VARIANT()).cast(BIGINT()), + "CAST(CAST(f0 AS VARIANT) AS BIGINT)", + 42L, + BIGINT()) + .testResult( + $("f1").cast(VARIANT()).cast(DECIMAL(4, 2)), + "CAST(CAST(f1 AS VARIANT) AS DECIMAL(4, 2))", + new BigDecimal("12.50"), + DECIMAL(4, 2)) + .testResult( + $("f2").cast(VARIANT()).cast(STRING()), + "CAST(CAST(f2 AS VARIANT) AS STRING)", + "hello", + STRING()) + .testResult( + $("f3").cast(VARIANT()).cast(TIME()), + "CAST(CAST(f3 AS VARIANT) AS TIME)", + LocalTime.of(10, 15, 30), + TIME()) + .testResult( + $("f4").cast(VARIANT()).cast(TIMESTAMP(9)), + "CAST(CAST(f4 AS VARIANT) AS TIMESTAMP(9))", + nanos, + TIMESTAMP(9)) + .testResult( + $("f5").cast(VARIANT()).cast(TIMESTAMP_LTZ(3)), + "CAST(CAST(f5 AS VARIANT) AS TIMESTAMP_LTZ(3))", + instant, + TIMESTAMP_LTZ(3)) + .testResult( + $("f6").cast(VARIANT()).cast(UUID()), + "CAST(CAST(f6 AS VARIANT) AS UUID)", + DEFAULT_UUID, + UUID()) + .testResult( + $("f7").cast(VARIANT()).cast(BYTES()), + "CAST(CAST(f7 AS VARIANT) AS BYTES)", + new byte[] {1, 2, 3}, + BYTES()), + TestSetSpec.forExpression("Cast TIME to VARIANT and back") + .onFieldsWithData( + LocalTime.of(12, 34, 56, 123_000_000), + LocalTime.MIDNIGHT, + LocalTime.of(23, 59, 59, 999_000_000)) + .andDataTypes(TIME(3), TIME(0), TIME(3)) + .testResult( + $("f0").cast(VARIANT()).cast(TIME(3)), + "CAST(CAST(f0 AS VARIANT) AS TIME(3))", + LocalTime.of(12, 34, 56, 123_000_000), + TIME(3)) + .testResult( + $("f1").cast(VARIANT()).cast(TIME(0)), + "CAST(CAST(f1 AS VARIANT) AS TIME(0))", + LocalTime.MIDNIGHT, + TIME(0)) + .testResult( + $("f2").cast(VARIANT()).cast(TIME(3)), + "CAST(CAST(f2 AS VARIANT) AS TIME(3))", + LocalTime.of(23, 59, 59, 999_000_000), + TIME(3)), + // a VARIANT holds at most 16 MiB, so a longer string fails CAST and TRY_CAST + // returns NULL + TestSetSpec.forExpression("Cast a string over the size limit to VARIANT") Review Comment: nit: Each `REPEAT(f0, 17000000)` builds a 17 MB string, and the spec runs it three times. `VariantCastUtilsTest` already pins the exact boundary. A single SQL `TRY_CAST` row would be enough to cover the codegen wrapping and keep the IT lighter. ########## flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/functions/casting/PrimitiveToVariantCastRule.java: ########## @@ -0,0 +1,149 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.flink.table.planner.functions.casting; + +import org.apache.flink.table.runtime.functions.VariantCastUtils; +import org.apache.flink.table.types.logical.LogicalType; +import org.apache.flink.table.types.logical.LogicalTypeRoot; +import org.apache.flink.table.types.logical.utils.LogicalTypeChecks; +import org.apache.flink.types.variant.Variant; + +import static org.apache.flink.table.planner.functions.casting.CastRuleUtils.staticCall; + +/** + * Primitive type to {@link LogicalTypeRoot#VARIANT} cast rule. + * + * <p>The value keeps the kind of its SQL type, so a {@code BIGINT} is stored as a {@code BIGINT} + * even when it would fit a smaller one. Unlike {@code PARSE_JSON}, a {@code NaN} or infinite {@code + * FLOAT} or {@code DOUBLE} is accepted, since a {@code VARIANT} is not limited to what JSON can + * express. A timestamp keeps its declared precision, so a {@code TIMESTAMP(p)} with {@code p} above + * 6 is stored with nanoseconds. That kind only covers 1677-09-21 to 2262-04-11, so such a cast + * fails for a value outside that range. Every other cast never fails. + */ +class PrimitiveToVariantCastRule extends AbstractExpressionCodeGeneratorCastRule<Object, Variant> { + + static final PrimitiveToVariantCastRule INSTANCE = new PrimitiveToVariantCastRule(); + + /** The highest precision a VARIANT stores with microseconds, above it nanoseconds are used. */ + private static final int MAX_MICROS_PRECISION = 6; + + private PrimitiveToVariantCastRule() { + super( + CastRulePredicate.builder() + .predicate( + (input, target) -> + target.is(LogicalTypeRoot.VARIANT) + && isSupportedSource(input)) + .build()); + } + + private static boolean isSupportedSource(LogicalType inputType) { + switch (inputType.getTypeRoot()) { + case BOOLEAN: + case TINYINT: + case SMALLINT: + case INTEGER: + case BIGINT: + case FLOAT: + case DOUBLE: + case DECIMAL: + case CHAR: + case VARCHAR: + case BINARY: + case VARBINARY: + case DATE: + case TIME_WITHOUT_TIME_ZONE: + case TIMESTAMP_WITHOUT_TIME_ZONE: + case TIMESTAMP_WITH_LOCAL_TIME_ZONE: + case UUID: + return true; + default: + return false; + } + } + + @Override + public boolean canFail(LogicalType inputLogicalType, LogicalType targetLogicalType) { Review Comment: Verified, the boundary is exact and `VariantCastUtilsTest` pins both sides. ########## flink-table/flink-table-common/src/main/java/org/apache/flink/table/types/logical/utils/LogicalTypeCasts.java: ########## @@ -403,6 +403,33 @@ public final class LogicalTypeCasts { .explicitFromFamily(EXACT_NUMERIC, CHARACTER_STRING) .build(); + // ----------------------------------------------------------------------------------------- + // VARIANT type + // ----------------------------------------------------------------------------------------- + + // Only a type with a VARIANT kind that holds its value without loss casts to VARIANT. + castTo(VARIANT) Review Comment: Fair, element-wise is consistent with the other constructed casts. One follow-up about MAP keys in the new review. ########## docs/content/docs/sql/reference/data-types.md: ########## @@ -1677,6 +1678,45 @@ CAST(o AS MAP<STRING, VARIANT>) -- values kept as variants, the variant null in CAST(o AS MAP<INT, STRING>) -- fails at validation, a MAP key must be a character string ``` +A scalar value can also be cast to a `VARIANT` with `CAST` or `TRY_CAST`. Only a type that a +`VARIANT` kind holds without loss is supported, and any other type, such as `INTERVAL`, `RAW`, or +`BITMAP`, is rejected at validation. The value keeps the kind of its SQL type: + +| Input type | Stored `VARIANT` kind | +|--------------------------------------------|-------------------------------------------------| +| `BOOLEAN` | `BOOLEAN` | +| `TINYINT`, `SMALLINT`, `INTEGER`, `BIGINT` | `TINYINT`, `SMALLINT`, `INT`, `BIGINT` | +| `FLOAT`, `DOUBLE`, `DECIMAL` | `FLOAT`, `DOUBLE`, `DECIMAL` | +| `CHAR`, `VARCHAR`, `STRING` | `STRING` | +| `BINARY`, `VARBINARY`, `BYTES` | `BYTES` | +| `DATE`, `TIME`, `UUID` | `DATE`, `TIME`, `UUID` | +| `TIMESTAMP`, `TIMESTAMP_LTZ` | `TIMESTAMP`, `TIMESTAMP_LTZ` | + +- An integer keeps the width of its SQL type, so a `BIGINT` is stored as a `BIGINT` even when the + value would fit a smaller kind. `PARSE_JSON('1')` instead picks the smallest kind, a `TINYINT`. + Either way it casts back to any integer type that holds the value. +- A character string is stored as a `STRING` and is never parsed. Use `PARSE_JSON` to parse JSON text. +- A `TIMESTAMP(p)` or `TIMESTAMP_LTZ(p)` keeps its declared precision. Up to a precision of 6 it is + stored with microseconds, and above with nanoseconds, even when the value has no digits below a + microsecond. Nanoseconds only cover 1677-09-21 to 2262-04-11, so for a precision above 6 a value + outside that range fails the cast, and `TRY_CAST` returns `NULL`. +- A `NaN` or infinite `FLOAT` or `DOUBLE` is stored as is, although `PARSE_JSON` rejects them. A Review Comment: Thanks, looks good. ########## docs/content/docs/sql/reference/data-types.md: ########## @@ -1677,6 +1678,45 @@ CAST(o AS MAP<STRING, VARIANT>) -- values kept as variants, the variant null in CAST(o AS MAP<INT, STRING>) -- fails at validation, a MAP key must be a character string ``` +A scalar value can also be cast to a `VARIANT` with `CAST` or `TRY_CAST`. Only a type that a +`VARIANT` kind holds without loss is supported, and any other type, such as `INTERVAL`, `RAW`, or +`BITMAP`, is rejected at validation. The value keeps the kind of its SQL type: + +| Input type | Stored `VARIANT` kind | +|--------------------------------------------|-------------------------------------------------| +| `BOOLEAN` | `BOOLEAN` | +| `TINYINT`, `SMALLINT`, `INTEGER`, `BIGINT` | `TINYINT`, `SMALLINT`, `INT`, `BIGINT` | +| `FLOAT`, `DOUBLE`, `DECIMAL` | `FLOAT`, `DOUBLE`, `DECIMAL` | +| `CHAR`, `VARCHAR`, `STRING` | `STRING` | +| `BINARY`, `VARBINARY`, `BYTES` | `BYTES` | +| `DATE`, `TIME`, `UUID` | `DATE`, `TIME`, `UUID` | +| `TIMESTAMP`, `TIMESTAMP_LTZ` | `TIMESTAMP`, `TIMESTAMP_LTZ` | + +- An integer keeps the width of its SQL type, so a `BIGINT` is stored as a `BIGINT` even when the + value would fit a smaller kind. `PARSE_JSON('1')` instead picks the smallest kind, a `TINYINT`. + Either way it casts back to any integer type that holds the value. +- A character string is stored as a `STRING` and is never parsed. Use `PARSE_JSON` to parse JSON text. +- A `TIMESTAMP(p)` or `TIMESTAMP_LTZ(p)` keeps its declared precision. Up to a precision of 6 it is Review Comment: Makes sense. Strict first, relax later is the safer direction. -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
