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]

Reply via email to