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 855597a5460 [FLINK-40978][table] Cast from VARIANT through
FromVariantConverter
855597a5460 is described below
commit 855597a5460630eb8f72510277bfed7c0be0a004
Author: Ramin Gharib <[email protected]>
AuthorDate: Mon Oct 19 16:38:00 2026 +0200
[FLINK-40978][table] Cast from VARIANT through FromVariantConverter
This closes #29466.
---
.../table/data/utils/FromVariantConverter.java | 299 ++++++++++++++
.../flink/table/data/utils}/VariantCastUtils.java | 7 +-
.../table/data/utils/FromVariantConverterTest.java | 447 +++++++++++++++++++++
.../table/data/utils}/VariantCastUtilsTest.java | 4 +-
.../casting/AbstractCodeGeneratorCastRule.java | 6 +
.../AbstractExpressionCodeGeneratorCastRule.java | 6 +
.../AbstractVariantToConstructedCastRule.java | 71 ----
.../functions/casting/CastRuleProvider.java | 6 +-
.../functions/casting/CodeGeneratorCastRule.java | 7 +
.../functions/casting/FromVariantCastRule.java | 125 ++++++
.../functions/casting/VariantToArrayCastRule.java | 171 --------
.../functions/casting/VariantToMapCastRule.java | 208 ----------
.../casting/VariantToPrimitiveCastRule.java | 241 -----------
.../functions/casting/VariantToRowCastRule.java | 234 -----------
.../functions/casting/VariantToStringCastRule.java | 121 ------
.../planner/codegen/calls/ScalarOperatorGens.scala | 1 +
.../planner/functions/CastFunctionITCase.java | 44 +-
.../functions/casting/CastRuleProviderTest.java | 19 +-
.../flink/table/planner/utils/FilterUtils.java | 80 +---
19 files changed, 970 insertions(+), 1127 deletions(-)
diff --git
a/flink-table/flink-table-common/src/main/java/org/apache/flink/table/data/utils/FromVariantConverter.java
b/flink-table/flink-table-common/src/main/java/org/apache/flink/table/data/utils/FromVariantConverter.java
new file mode 100644
index 00000000000..f612be4bf34
--- /dev/null
+++
b/flink-table/flink-table-common/src/main/java/org/apache/flink/table/data/utils/FromVariantConverter.java
@@ -0,0 +1,299 @@
+/*
+ * 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.data.utils;
+
+import org.apache.flink.annotation.Internal;
+import org.apache.flink.table.api.TableRuntimeException;
+import org.apache.flink.table.data.GenericArrayData;
+import org.apache.flink.table.data.GenericMapData;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.types.logical.ArrayType;
+import org.apache.flink.table.types.logical.DistinctType;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.LogicalTypeFamily;
+import org.apache.flink.table.types.logical.LogicalTypeRoot;
+import org.apache.flink.table.types.logical.MapType;
+import org.apache.flink.table.types.logical.utils.LogicalTypeChecks;
+import org.apache.flink.table.types.logical.utils.LogicalTypeUtils;
+import org.apache.flink.types.variant.BinaryVariant;
+import org.apache.flink.types.variant.Variant;
+import org.apache.flink.types.variant.VariantTypeException;
+
+import java.io.Serializable;
+import java.lang.reflect.Array;
+import java.time.ZoneId;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.TimeZone;
+
+/**
+ * Converts a {@link Variant} into internal data of a SQL type, for {@code
CAST(v AS T)}. {@link
+ * VariantCastUtils} defines how each leaf is read.
+ *
+ * <p>A {@code ROW} takes object fields by name and fails on a missing one. A
nested variant null
+ * becomes {@code NULL}, fails for a {@code NOT NULL} type, and stays a
variant null for a {@code
+ * VARIANT} type. A top-level variant null fails, so a cast to a nullable type
maps it to {@code
+ * NULL} first.
+ */
+@Internal
+public final class FromVariantConverter implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ private final ValueReader reader;
+
+ private FromVariantConverter(final ValueReader reader) {
+ this.reader = reader;
+ }
+
+ /**
+ * Creates a converter for a type that VARIANT casts to.
+ *
+ * @param sessionZone renders a {@code TIMESTAMP_LTZ} value read as a
string
+ * @throws IllegalArgumentException if VARIANT cannot be cast to the type
+ */
+ public static FromVariantConverter create(final LogicalType type, final
ZoneId sessionZone) {
+ return new FromVariantConverter(
+ createReader(type, TimeZone.getTimeZone(sessionZone), false));
+ }
+
+ /**
+ * Converts a variant into internal data.
+ *
+ * @throws TableRuntimeException if a value does not match its type
+ * @throws VariantTypeException if the variant is malformed, or a {@code
BOOLEAN}, {@code DATE}
+ * or binary value has another kind
+ */
+ public Object convert(final Variant variant) {
+ return reader.read(variant);
+ }
+
+ @FunctionalInterface
+ private interface ValueReader extends Serializable {
+ Object read(Variant variant);
+ }
+
+ // With copyVariants, a VARIANT is copied out of its parent rather than
returned as a view.
+ private static ValueReader createReader(
+ final LogicalType type, final TimeZone sessionZone, final boolean
copyVariants) {
+ switch (type.getTypeRoot()) {
+ case BOOLEAN:
+ return Variant::getBoolean;
+ case TINYINT:
+ return variant ->
+ (byte)
+ VariantCastUtils.toIntegral(
+ variant, Byte.MIN_VALUE,
Byte.MAX_VALUE, "TINYINT");
+ case SMALLINT:
+ return variant ->
+ (short)
+ VariantCastUtils.toIntegral(
+ variant, Short.MIN_VALUE,
Short.MAX_VALUE, "SMALLINT");
+ case INTEGER:
+ return variant ->
+ (int)
+ VariantCastUtils.toIntegral(
+ variant, Integer.MIN_VALUE,
Integer.MAX_VALUE, "INTEGER");
+ case BIGINT:
+ return variant ->
+ VariantCastUtils.toIntegral(
+ variant, Long.MIN_VALUE, Long.MAX_VALUE,
"BIGINT");
+ case FLOAT:
+ return VariantCastUtils::toFloat;
+ case DOUBLE:
+ return VariantCastUtils::toDouble;
+ case DECIMAL:
+ {
+ final int precision = LogicalTypeChecks.getPrecision(type);
+ final int scale = LogicalTypeChecks.getScale(type);
+ return variant -> VariantCastUtils.toDecimal(variant,
precision, scale);
+ }
+ case CHAR:
+ case VARCHAR:
+ {
+ final int length = LogicalTypeChecks.getLength(type);
+ final boolean charTarget = type.is(LogicalTypeRoot.CHAR);
+ return variant ->
+ VariantCastUtils.toStringValue(
+ variant, sessionZone, length, charTarget);
+ }
+ case BINARY:
+ case VARBINARY:
+ {
+ final int length = LogicalTypeChecks.getLength(type);
+ final boolean fixedLength =
type.is(LogicalTypeRoot.BINARY);
+ return variant -> VariantCastUtils.toBytes(variant,
length, fixedLength);
+ }
+ case DATE:
+ return variant -> (int) variant.getDate().toEpochDay();
+ case TIME_WITHOUT_TIME_ZONE:
+ {
+ final int precision = LogicalTypeChecks.getPrecision(type);
+ return variant -> VariantCastUtils.toTime(variant,
precision);
+ }
+ case TIMESTAMP_WITHOUT_TIME_ZONE:
+ {
+ final int precision = LogicalTypeChecks.getPrecision(type);
+ return variant -> VariantCastUtils.toTimestamp(variant,
precision);
+ }
+ case TIMESTAMP_WITH_LOCAL_TIME_ZONE:
+ {
+ final int precision = LogicalTypeChecks.getPrecision(type);
+ return variant -> VariantCastUtils.toTimestampLtz(variant,
precision);
+ }
+ case UUID:
+ return VariantCastUtils::toUuid;
+ case VARIANT:
+ return copyVariants ? FromVariantConverter::copyVariant :
variant -> variant;
+ case ARRAY:
+ return createArrayReader((ArrayType) type, sessionZone,
copyVariants);
+ case MAP:
+ return createMapReader((MapType) type, sessionZone,
copyVariants);
+ case ROW:
+ case STRUCTURED_TYPE:
+ return createRowReader(type, sessionZone);
+ case DISTINCT_TYPE:
+ return createReader(
+ ((DistinctType) type).getSourceType(), sessionZone,
copyVariants);
+ default:
+ throw new IllegalArgumentException("Cannot cast VARIANT to " +
type + ".");
+ }
+ }
+
+ // A nested variant null becomes NULL, or fails for a NOT NULL type. A
VARIANT type keeps it.
+ private static ValueReader createChildReader(
+ final LogicalType type,
+ final TimeZone sessionZone,
+ final boolean copyVariants,
+ final String notNullMessage) {
+ final ValueReader reader = createReader(type, sessionZone,
copyVariants);
+ if (type.is(LogicalTypeRoot.VARIANT)) {
+ return reader;
+ }
+ if (type.isNullable()) {
+ return variant -> variant.isNull() ? null : reader.read(variant);
+ }
+ return variant -> {
+ if (variant.isNull()) {
+ throw new TableRuntimeException(notNullMessage);
+ }
+ return reader.read(variant);
+ };
+ }
+
+ private static ValueReader createArrayReader(
+ final ArrayType type, final TimeZone sessionZone, final boolean
copyVariants) {
+ final LogicalType elementType = type.getElementType();
+ final ValueReader elementReader =
+ createChildReader(
+ elementType,
+ sessionZone,
+ copyVariants,
+ "Cannot cast a VARIANT null array element to a NOT
NULL element type.");
+ // A typed array keeps GenericArrayData#toIntArray and its siblings
working.
+ final Class<?> elementClass =
LogicalTypeUtils.toInternalConversionClass(elementType);
+ final String arrayType = type.asSummaryString();
+ return variant -> {
+ final int size = VariantCastUtils.arraySize(variant, arrayType);
+ final Object[] elements = (Object[])
Array.newInstance(elementClass, size);
+ for (int i = 0; i < size; i++) {
+ elements[i] = elementReader.read(variant.getElement(i));
+ }
+ return new GenericArrayData(elements);
+ };
+ }
+
+ private static ValueReader createMapReader(
+ final MapType type, final TimeZone sessionZone, final boolean
copyVariants) {
+ final LogicalType keyType = type.getKeyType();
+ if (!keyType.is(LogicalTypeFamily.CHARACTER_STRING)) {
+ throw new IllegalArgumentException(
+ "Cannot cast VARIANT to " + type + ". A MAP key must be a
character string.");
+ }
+ final int keyLength = LogicalTypeChecks.getLength(keyType);
+ final boolean charKey = keyType.is(LogicalTypeRoot.CHAR);
+ final ValueReader valueReader =
+ createChildReader(
+ type.getValueType(),
+ sessionZone,
+ copyVariants,
+ "Cannot cast a VARIANT null object value to a NOT NULL
map value type.");
+ final String mapType = type.asSummaryString();
+ return variant -> {
+ VariantCastUtils.requireObject(variant, mapType);
+ final List<String> names = variant.getFieldNames();
+ final Map<Object, Object> map = new HashMap<>(names.size());
+ for (final String name : names) {
+ final Object value = valueReader.read(variant.getField(name));
+ map.put(VariantCastUtils.variantKey(name, keyLength, charKey),
value);
+ }
+ return new GenericMapData(map);
+ };
+ }
+
+ // A ROW copies every VARIANT below it, like a value read back from a
binary row.
+ private static ValueReader createRowReader(final LogicalType type, final
TimeZone sessionZone) {
+ final List<LogicalType> fieldTypes =
LogicalTypeChecks.getFieldTypes(type);
+ final String[] fieldNames =
LogicalTypeChecks.getFieldNames(type).toArray(new String[0]);
+ final int fieldCount = fieldTypes.size();
+ final String rowType = type.asSummaryString();
+ final ValueReader[] fieldReaders = new ValueReader[fieldCount];
+ for (int i = 0; i < fieldCount; i++) {
+ fieldReaders[i] =
+ createChildReader(
+ fieldTypes.get(i),
+ sessionZone,
+ true,
+ nullFieldError(rowType, fieldNames[i]));
+ }
+ return variant -> {
+ VariantCastUtils.requireObject(variant, rowType);
+ final GenericRowData row = new GenericRowData(fieldCount);
+ for (int i = 0; i < fieldCount; i++) {
+ // getField returns null for a field the object does not have.
+ final Variant field = variant.getField(fieldNames[i]);
+ if (field == null) {
+ throw new TableRuntimeException(missingFieldError(rowType,
fieldNames[i]));
+ }
+ row.setField(i, fieldReaders[i].read(field));
+ }
+ return row;
+ };
+ }
+
+ private static Variant copyVariant(final Variant variant) {
+ final BinaryVariant view = (BinaryVariant) variant;
+ return new BinaryVariant(view.getValue(), view.getMetadata());
+ }
+
+ private static String missingFieldError(final String rowType, final String
fieldName) {
+ return String.format(
+ "Cannot cast the VARIANT object to %s because the field '%s'
is not present in "
+ + "the VARIANT.",
+ rowType, fieldName);
+ }
+
+ private static String nullFieldError(final String rowType, final String
fieldName) {
+ return String.format(
+ "Cannot cast the VARIANT object to %s because the field '%s'
is a VARIANT null "
+ + "and the target does not accept NULL.",
+ rowType, fieldName);
+ }
+}
diff --git
a/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/functions/VariantCastUtils.java
b/flink-table/flink-table-common/src/main/java/org/apache/flink/table/data/utils/VariantCastUtils.java
similarity index 98%
rename from
flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/functions/VariantCastUtils.java
rename to
flink-table/flink-table-common/src/main/java/org/apache/flink/table/data/utils/VariantCastUtils.java
index 4ff53cc0b93..6c1b9c4118f 100644
---
a/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/functions/VariantCastUtils.java
+++
b/flink-table/flink-table-common/src/main/java/org/apache/flink/table/data/utils/VariantCastUtils.java
@@ -16,16 +16,14 @@
* limitations under the License.
*/
-package org.apache.flink.table.runtime.functions;
+package org.apache.flink.table.data.utils;
import org.apache.flink.annotation.Internal;
import org.apache.flink.table.api.TableRuntimeException;
import org.apache.flink.table.data.DecimalData;
import org.apache.flink.table.data.TimestampData;
import org.apache.flink.table.data.binary.BinaryStringData;
-import org.apache.flink.table.data.binary.BinaryStringDataUtil;
import org.apache.flink.table.data.binary.StringUtf8Utils;
-import org.apache.flink.table.data.utils.ToVariantConverter;
import org.apache.flink.table.types.logical.utils.UuidUtils;
import org.apache.flink.table.utils.DateTimeUtils;
import org.apache.flink.table.utils.EncodingUtils;
@@ -109,8 +107,7 @@ public final class VariantCastUtils {
return key.substring(0, targetLength);
}
if (charTarget && length < targetLength) {
- return BinaryStringDataUtil.concat(
- key, BinaryStringData.blankString(targetLength - length));
+ return BinaryStringData.fromString(name + " ".repeat(targetLength
- length));
}
return key;
}
diff --git
a/flink-table/flink-table-common/src/test/java/org/apache/flink/table/data/utils/FromVariantConverterTest.java
b/flink-table/flink-table-common/src/test/java/org/apache/flink/table/data/utils/FromVariantConverterTest.java
new file mode 100644
index 00000000000..61997885049
--- /dev/null
+++
b/flink-table/flink-table-common/src/test/java/org/apache/flink/table/data/utils/FromVariantConverterTest.java
@@ -0,0 +1,447 @@
+/*
+ * 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.data.utils;
+
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.api.TableRuntimeException;
+import org.apache.flink.table.catalog.ObjectIdentifier;
+import org.apache.flink.table.data.ArrayData;
+import org.apache.flink.table.data.DecimalData;
+import org.apache.flink.table.data.GenericArrayData;
+import org.apache.flink.table.data.GenericMapData;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.MapData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.data.StringData;
+import org.apache.flink.table.data.TimestampData;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.DistinctType;
+import org.apache.flink.table.types.logical.IntType;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.StructuredType;
+import org.apache.flink.table.types.logical.StructuredType.StructuredAttribute;
+import org.apache.flink.table.types.logical.utils.UuidUtils;
+import org.apache.flink.types.variant.BinaryVariant;
+import org.apache.flink.types.variant.Variant;
+import org.apache.flink.types.variant.VariantBuilder;
+import org.apache.flink.util.InstantiationUtil;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.math.BigDecimal;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.ZoneId;
+import java.time.ZoneOffset;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.stream.Stream;
+
+import static org.apache.flink.table.api.DataTypes.FIELD;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.junit.jupiter.params.provider.Arguments.arguments;
+
+class FromVariantConverterTest {
+
+ private static final VariantBuilder BUILDER = Variant.newBuilder();
+
+ private static final LocalDateTime DATE_TIME =
+ LocalDateTime.of(2026, 9, 25, 10, 15, 30, 123_000_000);
+
+ private static final UUID UUID_VALUE =
UUID.fromString("0e1f2a3b-4c5d-6e7f-8091-a2b3c4d5e6f7");
+
+ private static final LogicalType MONEY =
+ DistinctType.newBuilder(ObjectIdentifier.of("cat", "db", "Money"),
new IntType())
+ .build();
+
+ private static final LogicalType ORDER =
+ StructuredType.newBuilder(ObjectIdentifier.of("cat", "db",
"Order"))
+ .attributes(
+ List.of(
+ new StructuredAttribute("id", new
IntType()),
+ new StructuredAttribute("price", MONEY)))
+ .build();
+
+ @Test
+ void testLeavesApplyTheLengthAndPrecisionOfTheirType() {
+ final LogicalType type =
+ DataTypes.ROW(
+ FIELD("bool", DataTypes.BOOLEAN()),
+ FIELD("tiny", DataTypes.TINYINT()),
+ FIELD("small", DataTypes.SMALLINT()),
+ FIELD("int", DataTypes.INT()),
+ FIELD("big", DataTypes.BIGINT()),
+ FIELD("float", DataTypes.FLOAT()),
+ FIELD("double", DataTypes.DOUBLE()),
+ FIELD("decimal", DataTypes.DECIMAL(10, 2)),
+ FIELD("char", DataTypes.CHAR(4)),
+ FIELD("varchar", DataTypes.VARCHAR(2)),
+ FIELD("binary", DataTypes.BINARY(2)),
+ FIELD("bytes", DataTypes.BYTES()),
+ FIELD("date", DataTypes.DATE()),
+ FIELD("time", DataTypes.TIME(0)),
+ FIELD("ts", DataTypes.TIMESTAMP(0)),
+ FIELD("ltz", DataTypes.TIMESTAMP_LTZ(3)),
+ FIELD("uuid", DataTypes.UUID()),
+ FIELD("variant", DataTypes.VARIANT()))
+ .getLogicalType();
+ final Variant variant =
+ BUILDER.object()
+ .add("bool", BUILDER.of(true))
+ .add("tiny", BUILDER.of((byte) 1))
+ .add("small", BUILDER.of((short) 2))
+ .add("int", BUILDER.of(3))
+ .add("big", BUILDER.of(4L))
+ .add("float", BUILDER.of(1.5f))
+ .add("double", BUILDER.of(2.5d))
+ .add("decimal", BUILDER.of(new BigDecimal("12.5")))
+ .add("char", BUILDER.of("ab"))
+ .add("varchar", BUILDER.of("abc"))
+ .add("binary", BUILDER.of(new byte[] {7}))
+ .add("bytes", BUILDER.of(new byte[] {(byte) 0xCA,
(byte) 0xFE}))
+ .add("date", BUILDER.of(LocalDate.of(2026, 9, 25)))
+ .add("time", BUILDER.of(LocalTime.of(10, 15, 30,
123_456_000)))
+ .add("ts", BUILDER.of(DATE_TIME))
+ .add("ltz", BUILDER.of(Instant.ofEpochSecond(0,
123_456_000)))
+ .add("uuid", BUILDER.of(UUID_VALUE))
+ .add("variant", BUILDER.of("raw"))
+ .build();
+
+ final RowData row = (RowData) convert(type, variant);
+
+ assertThat(row.getBoolean(0)).isTrue();
+ assertThat(row.getByte(1)).isEqualTo((byte) 1);
+ assertThat(row.getShort(2)).isEqualTo((short) 2);
+ assertThat(row.getInt(3)).isEqualTo(3);
+ assertThat(row.getLong(4)).isEqualTo(4L);
+ assertThat(row.getFloat(5)).isEqualTo(1.5f);
+ assertThat(row.getDouble(6)).isEqualTo(2.5d);
+ assertThat(row.getDecimal(7, 10, 2)).isEqualTo(decimal("12.50", 10,
2));
+ assertThat(row.getString(8)).isEqualTo(StringData.fromString("ab "));
+ assertThat(row.getString(9)).isEqualTo(StringData.fromString("ab"));
+ assertThat(row.getBinary(10)).containsExactly(7, 0);
+ assertThat(row.getBinary(11)).containsExactly((byte) 0xCA, (byte)
0xFE);
+ assertThat(row.getInt(12)).isEqualTo((int) LocalDate.of(2026, 9,
25).toEpochDay());
+ assertThat(row.getInt(13)).isEqualTo(LocalTime.of(10, 15,
30).toSecondOfDay() * 1000);
+ assertThat(row.getTimestamp(14, 0))
+
.isEqualTo(TimestampData.fromLocalDateTime(DATE_TIME.withNano(0)));
+ assertThat(row.getTimestamp(15,
3)).isEqualTo(TimestampData.fromEpochMillis(123));
+ assertThat(row.getBinary(16)).isEqualTo(UuidUtils.toBytes(UUID_VALUE));
+ assertThat(row.getVariant(17).getString()).isEqualTo("raw");
+ }
+
+ @Test
+ void testIntegerLeavesCheckTheRangeOfTheirType() {
+ assertThatThrownBy(() -> convert(DataTypes.TINYINT(), BUILDER.of(128)))
+ .isInstanceOf(TableRuntimeException.class)
+ .hasMessage("Casting the VARIANT value 128 to TINYINT
overflowed.");
+ assertThatThrownBy(() -> convert(DataTypes.SMALLINT(),
BUILDER.of(32768)))
+ .isInstanceOf(TableRuntimeException.class)
+ .hasMessage("Casting the VARIANT value 32768 to SMALLINT
overflowed.");
+ assertThatThrownBy(() -> convert(DataTypes.INT(),
BUILDER.of(2147483648L)))
+ .isInstanceOf(TableRuntimeException.class)
+ .hasMessage("Casting the VARIANT value 2147483648 to INTEGER
overflowed.");
+ assertThatThrownBy(
+ () ->
+ convert(
+ DataTypes.BIGINT(),
+ BUILDER.of(new
BigDecimal("9223372036854775808"))))
+ .isInstanceOf(TableRuntimeException.class)
+ .hasMessage("Casting the VARIANT value 9223372036854775808 to
BIGINT overflowed.");
+ }
+
+ @Test
+ void testMapKeysFollowTheLengthOfTheKeyType() {
+ final Variant variant = BUILDER.object().add("ab",
BUILDER.of(1)).build();
+
+ final MapData charKey =
+ (MapData) convert(DataTypes.MAP(DataTypes.CHAR(3),
DataTypes.INT()), variant);
+ final MapData varcharKey =
+ (MapData) convert(DataTypes.MAP(DataTypes.VARCHAR(1),
DataTypes.INT()), variant);
+
+
assertThat(charKey.keyArray().getString(0)).isEqualTo(StringData.fromString("ab
"));
+
assertThat(varcharKey.keyArray().getString(0)).isEqualTo(StringData.fromString("a"));
+ }
+
+ @Test
+ void testArrayIsTypedByItsElement() {
+ final Variant variant =
BUILDER.array().add(BUILDER.of(1)).add(BUILDER.of(2)).build();
+
+ final GenericArrayData array =
+ (GenericArrayData)
convert(DataTypes.ARRAY(DataTypes.INT().notNull()), variant);
+
+ assertThat(array.toIntArray()).containsExactly(1, 2);
+ }
+
+ @Test
+ void testNotNullConstructedChildFails() {
+ final Variant array = BUILDER.array().add(BUILDER.ofNull()).build();
+
+ assertThatThrownBy(
+ () ->
+ convert(
+
DataTypes.ARRAY(DataTypes.ARRAY(DataTypes.INT()).notNull()),
+ array))
+ .isInstanceOf(TableRuntimeException.class)
+ .hasMessage("Cannot cast a VARIANT null array element to a NOT
NULL element type.");
+ }
+
+ @Test
+ void testNotNullVariantChildKeepsVariantNull() {
+ final Variant array = BUILDER.array().add(BUILDER.ofNull()).build();
+
+ final ArrayData result =
+ (ArrayData)
convert(DataTypes.ARRAY(DataTypes.VARIANT().notNull()), array);
+
+ assertThat(result.getVariant(0).isNull()).isTrue();
+ }
+
+ @Test
+ void testRowCopiesEveryVariantBelowIt() {
+ final Variant list = BUILDER.array().add(BUILDER.of("x")).build();
+ final Variant object =
+ BUILDER.object()
+ .add("v", BUILDER.of("x"))
+ .add("list", list)
+ .add("map", BUILDER.object().add("k",
BUILDER.of("x")).build())
+ .build();
+ final DataType type =
+ DataTypes.ROW(
+ FIELD("v", DataTypes.VARIANT()),
+ FIELD("list", DataTypes.ARRAY(DataTypes.VARIANT())),
+ FIELD("map", DataTypes.MAP(DataTypes.STRING(),
DataTypes.VARIANT())));
+
+ final RowData row = (RowData) convert(type, object);
+
+ assertThat(row.getVariant(0)).isEqualTo(copyOf(object.getField("v")));
+ assertThat(row.getArray(1).getVariant(0))
+ .isEqualTo(copyOf(object.getField("list").getElement(0)));
+ assertThat(row.getMap(2).valueArray().getVariant(0))
+ .isEqualTo(copyOf(object.getField("map").getField("k")));
+ // outside a ROW, an element stays a view into its parent
+ assertThat(((ArrayData) convert(DataTypes.ARRAY(DataTypes.VARIANT()),
list)).getVariant(0))
+ .isEqualTo(list.getElement(0));
+ }
+
+ @Test
+ void testAbsentVariantFieldFails() {
+ final Variant object = BUILDER.object().add("b",
BUILDER.of(1)).build();
+
+ assertThatThrownBy(() -> convert(DataTypes.ROW(FIELD("a",
DataTypes.VARIANT())), object))
+ .isInstanceOf(TableRuntimeException.class)
+ .hasMessage(
+ "Cannot cast the VARIANT object to ROW<`a` VARIANT>
because the field 'a' "
+ + "is not present in the VARIANT.");
+ }
+
+ @Test
+ void testTopLevelVariantNullReachesTheLeaf() {
+ final Variant nullVariant = BUILDER.ofNull();
+
+ // a cast to a nullable type maps a variant null to NULL before it
calls the converter
+ assertThatThrownBy(() -> convert(DataTypes.INT(), nullVariant))
+ .isInstanceOf(TableRuntimeException.class)
+ .hasMessageStartingWith("Cannot cast a VARIANT NULL value to
INTEGER.");
+ assertThatThrownBy(() -> convert(DataTypes.INT().notNull(),
nullVariant))
+ .isInstanceOf(TableRuntimeException.class)
+ .hasMessageStartingWith("Cannot cast a VARIANT NULL value to
INTEGER.");
+ assertThatThrownBy(() ->
convert(DataTypes.ARRAY(DataTypes.INT()).notNull(), nullVariant))
+ .isInstanceOf(TableRuntimeException.class)
+ .hasMessageStartingWith(
+ "Cannot cast a VARIANT NULL value to ARRAY<INT> NOT
NULL because the "
+ + "target requires an array.");
+ assertThat(convert(DataTypes.VARIANT(),
nullVariant)).isSameAs(nullVariant);
+ }
+
+ @Test
+ void testNestedWrongShapeNamesItsOwnType() {
+ final Variant array = BUILDER.array().add(BUILDER.of(1)).build();
+
+ assertThatThrownBy(() ->
convert(DataTypes.ARRAY(DataTypes.ARRAY(DataTypes.INT())), array))
+ .isInstanceOf(TableRuntimeException.class)
+ .hasMessage(
+ "Cannot cast a VARIANT INT value to ARRAY<INT> because
the target "
+ + "requires an array. Only a variant array
casts to ARRAY and "
+ + "only a variant object casts to ROW,
STRUCTURED, or MAP.");
+ }
+
+ @Test
+ void testStructuredAndDistinctTypes() {
+ final Variant variant =
+ BUILDER.object().add("price", BUILDER.of(42)).add("id",
BUILDER.of(1)).build();
+
+ assertThat(convert(ORDER, variant)).isEqualTo(GenericRowData.of(1,
42));
+ assertThat(convert(MONEY, BUILDER.of(42))).isEqualTo(42);
+ }
+
+ @Test
+ void testCreateRejectsTypesThatVariantDoesNotCastTo() {
+ assertThatThrownBy(
+ () ->
+ FromVariantConverter.create(
+
DataTypes.MULTISET(DataTypes.INT()).getLogicalType(),
+ ZoneOffset.UTC))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Cannot cast VARIANT to MULTISET<INT>.");
+ assertThatThrownBy(
+ () ->
+ FromVariantConverter.create(
+
DataTypes.ARRAY(DataTypes.NULL()).getLogicalType(),
+ ZoneOffset.UTC))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Cannot cast VARIANT to NULL.");
+ assertThatThrownBy(
+ () ->
+ FromVariantConverter.create(
+ DataTypes.MAP(DataTypes.INT(),
DataTypes.STRING())
+ .getLogicalType(),
+ ZoneOffset.UTC))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageEndingWith("A MAP key must be a character string.");
+ }
+
+ @Test
+ void testConverterSurvivesJavaSerialization() throws Exception {
+ final LogicalType type =
+ DataTypes.ROW(
+ FIELD("int", DataTypes.INT()),
+ FIELD("ltz", DataTypes.STRING()),
+ FIELD("list",
DataTypes.ARRAY(DataTypes.INT().notNull())),
+ FIELD("map", DataTypes.MAP(DataTypes.CHAR(2),
DataTypes.INT())),
+ FIELD("variant", DataTypes.VARIANT()),
+ FIELD("order", DataTypes.of(ORDER)))
+ .getLogicalType();
+ final Variant variant =
+ BUILDER.object()
+ .add("int", BUILDER.of(1))
+ .add("ltz", BUILDER.of(Instant.EPOCH))
+ .add("list",
BUILDER.array().add(BUILDER.of(2)).build())
+ .add("map", BUILDER.object().add("k",
BUILDER.ofNull()).build())
+ .add("variant", BUILDER.ofNull())
+ .add(
+ "order",
+ BUILDER.object()
+ .add("id", BUILDER.of(3))
+ .add("price", BUILDER.of(4))
+ .build())
+ .build();
+ final FromVariantConverter converter =
+ FromVariantConverter.create(type, ZoneId.of("Asia/Tokyo"));
+
+ final RowData actual = (RowData)
InstantiationUtil.clone(converter).convert(variant);
+
+ assertThat(actual).isEqualTo(converter.convert(variant));
+ // the session zone survives too
+ assertThat(actual.getString(1).toString()).startsWith("1970-01-01
09:00:00");
+ }
+
+ @ParameterizedTest(name = "{0}")
+ @MethodSource("roundTrips")
+ void testRoundTripThroughToVariantConverter(final DataType type, final
Object value) {
+ final LogicalType logicalType = type.getLogicalType();
+ final Variant variant =
ToVariantConverter.create(logicalType).convert(value);
+
+ assertThat(FromVariantConverter.create(logicalType,
ZoneOffset.UTC).convert(variant))
+ .isEqualTo(value);
+ }
+
+ private static Stream<Arguments> roundTrips() {
+ return Stream.of(
+ arguments(DataTypes.BOOLEAN(), true),
+ arguments(DataTypes.TINYINT(), (byte) -1),
+ arguments(DataTypes.SMALLINT(), (short) 300),
+ arguments(DataTypes.INT(), 70_000),
+ arguments(DataTypes.BIGINT(), Long.MIN_VALUE),
+ arguments(DataTypes.FLOAT(), 1.5f),
+ arguments(DataTypes.DOUBLE(), -2.25d),
+ arguments(DataTypes.DECIMAL(10, 2), decimal("12.50", 10, 2)),
+ arguments(
+ DataTypes.DECIMAL(38, 10),
+ decimal("1234567890123456789012345678.0123456789", 38,
10)),
+ arguments(DataTypes.CHAR(4), StringData.fromString("ab ")),
+ arguments(DataTypes.STRING(), StringData.fromString("héllo
👋")),
+ arguments(DataTypes.BINARY(2), new byte[] {1, 2}),
+ arguments(DataTypes.BYTES(), new byte[] {(byte) 0xCA, (byte)
0xFE}),
+ arguments(DataTypes.DATE(), (int) LocalDate.of(2026, 9,
25).toEpochDay()),
+ arguments(DataTypes.TIME(3), LocalTime.of(10, 15,
30).toSecondOfDay() * 1000 + 123),
+ arguments(DataTypes.TIMESTAMP(3),
TimestampData.fromLocalDateTime(DATE_TIME)),
+ arguments(DataTypes.TIMESTAMP(9),
TimestampData.fromEpochMillis(1, 234_567)),
+ arguments(DataTypes.TIMESTAMP_LTZ(6),
TimestampData.fromEpochMillis(-1, 1_000)),
+ arguments(DataTypes.TIMESTAMP_LTZ(9),
TimestampData.fromEpochMillis(0, 1)),
+ arguments(DataTypes.UUID(), UuidUtils.toBytes(UUID_VALUE)),
+ arguments(
+ DataTypes.ARRAY(DataTypes.INT()),
+ new GenericArrayData(new Object[] {1, null, 3})),
+ arguments(
+ DataTypes.ARRAY(DataTypes.ARRAY(DataTypes.STRING())),
+ new GenericArrayData(
+ new Object[] {
+ new GenericArrayData(new Object[]
{StringData.fromString("a")}),
+ null
+ })),
+ arguments(
+ DataTypes.MAP(DataTypes.STRING(), DataTypes.BIGINT()),
+ mapOf(StringData.fromString("a"), 1L)),
+ arguments(
+ DataTypes.MAP(DataTypes.STRING(), DataTypes.BIGINT()),
+ mapOf(StringData.fromString("a"), null)),
+ arguments(
+ DataTypes.ROW(
+ FIELD("id", DataTypes.INT().notNull()),
+ FIELD("name", DataTypes.STRING()),
+ FIELD("tags",
DataTypes.ARRAY(DataTypes.STRING()))),
+ GenericRowData.of(
+ 7,
+ null,
+ new GenericArrayData(new Object[]
{StringData.fromString("x")}))),
+ arguments(DataTypes.of(ORDER), GenericRowData.of(1, 42)),
+ arguments(DataTypes.of(MONEY), 42));
+ }
+
+ private static Variant copyOf(final Variant view) {
+ final BinaryVariant binary = (BinaryVariant) view;
+ return new BinaryVariant(binary.getValue(), binary.getMetadata());
+ }
+
+ private static DecimalData decimal(final String value, final int
precision, final int scale) {
+ return DecimalData.fromBigDecimal(new BigDecimal(value), precision,
scale);
+ }
+
+ private static GenericMapData mapOf(final Object key, final Object value) {
+ final Map<Object, Object> map = new HashMap<>();
+ map.put(key, value);
+ return new GenericMapData(map);
+ }
+
+ private static Object convert(final DataType dataType, final Variant
variant) {
+ return convert(dataType.getLogicalType(), variant);
+ }
+
+ private static Object convert(final LogicalType type, final Variant
variant) {
+ return FromVariantConverter.create(type,
ZoneOffset.UTC).convert(variant);
+ }
+}
diff --git
a/flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/runtime/functions/VariantCastUtilsTest.java
b/flink-table/flink-table-common/src/test/java/org/apache/flink/table/data/utils/VariantCastUtilsTest.java
similarity index 96%
rename from
flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/runtime/functions/VariantCastUtilsTest.java
rename to
flink-table/flink-table-common/src/test/java/org/apache/flink/table/data/utils/VariantCastUtilsTest.java
index f07452165d3..89db5d619ef 100644
---
a/flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/runtime/functions/VariantCastUtilsTest.java
+++
b/flink-table/flink-table-common/src/test/java/org/apache/flink/table/data/utils/VariantCastUtilsTest.java
@@ -16,7 +16,7 @@
* limitations under the License.
*/
-package org.apache.flink.table.runtime.functions;
+package org.apache.flink.table.data.utils;
import org.apache.flink.types.variant.BinaryVariant;
import org.apache.flink.types.variant.Variant;
@@ -27,7 +27,7 @@ import org.junit.jupiter.api.Test;
import java.time.Instant;
import java.util.TimeZone;
-import static
org.apache.flink.table.runtime.functions.VariantCastUtils.toPrintString;
+import static org.apache.flink.table.data.utils.VariantCastUtils.toPrintString;
import static org.apache.flink.types.variant.BinaryVariantUtil.primitiveHeader;
import static org.apache.flink.types.variant.BinaryVariantUtil.shortStrHeader;
import static org.assertj.core.api.Assertions.assertThat;
diff --git
a/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/functions/casting/AbstractCodeGeneratorCastRule.java
b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/functions/casting/AbstractCodeGeneratorCastRule.java
index 4390717de40..55903ab390e 100644
---
a/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/functions/casting/AbstractCodeGeneratorCastRule.java
+++
b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/functions/casting/AbstractCodeGeneratorCastRule.java
@@ -27,6 +27,7 @@ import
org.apache.flink.table.runtime.typeutils.InternalSerializers;
import org.apache.flink.table.types.logical.LogicalType;
import org.apache.flink.util.FlinkRuntimeException;
+import java.time.ZoneId;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
@@ -208,6 +209,11 @@ abstract class AbstractCodeGeneratorCastRule<IN, OUT>
extends AbstractCastRule<I
+ "\")";
}
+ @Override
+ public ZoneId getSessionZoneId() {
+ return castRuleCtx.getSessionZoneId();
+ }
+
@Override
public String declareVariable(String type, String variablePrefix) {
String variableName = variablePrefix + "$" + variableIndex;
diff --git
a/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/functions/casting/AbstractExpressionCodeGeneratorCastRule.java
b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/functions/casting/AbstractExpressionCodeGeneratorCastRule.java
index feda119a1d6..8d67dfdf9d2 100644
---
a/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/functions/casting/AbstractExpressionCodeGeneratorCastRule.java
+++
b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/functions/casting/AbstractExpressionCodeGeneratorCastRule.java
@@ -24,6 +24,7 @@ import org.apache.flink.table.runtime.generated.CompileUtils;
import org.apache.flink.table.types.logical.LogicalType;
import org.apache.flink.table.types.logical.utils.LogicalTypeUtils;
+import java.time.ZoneId;
import java.util.Collections;
import static
org.apache.flink.table.planner.codegen.CodeGenUtils.boxedTypeTermForType;
@@ -119,6 +120,11 @@ abstract class AbstractExpressionCodeGeneratorCastRule<IN,
OUT>
return "java.util.TimeZone.getTimeZone(\"" +
ctx.getSessionZoneId().getId() + "\")";
}
+ @Override
+ public ZoneId getSessionZoneId() {
+ return ctx.getSessionZoneId();
+ }
+
@Override
public String declareVariable(String type, String variablePrefix) {
throw new UnsupportedOperationException(
diff --git
a/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/functions/casting/AbstractVariantToConstructedCastRule.java
b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/functions/casting/AbstractVariantToConstructedCastRule.java
deleted file mode 100644
index e6d924617f3..00000000000
---
a/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/functions/casting/AbstractVariantToConstructedCastRule.java
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * 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.types.logical.LogicalType;
-import org.apache.flink.table.types.logical.LogicalTypeRoot;
-import org.apache.flink.types.variant.Variant;
-
-import static
org.apache.flink.table.planner.functions.casting.CastRuleUtils.methodCall;
-
-/**
- * Base class for the rules that cast a {@link LogicalTypeRoot#VARIANT} to a
constructed target,
- * imposing a schema on a variant. A constructed cast is the scalar cast
applied to every leaf plus
- * a shape check at each level, so the recursion bottoms out at the same
scalar cast the primitive
- * and string rules perform and no new leaf semantics are introduced.
- *
- * <p>A constructed cast can always fail, on a shape mismatch, an unreadable
leaf, a missing field,
- * or a VARIANT {@code null} in a {@code NOT NULL} position, so {@code
TRY_CAST} wraps the whole
- * value and returns {@code NULL} for any failure rather than a partial result.
- */
-abstract class AbstractVariantToConstructedCastRule<OUT>
- extends AbstractNullAwareCodeGeneratorCastRule<Variant, OUT> {
-
- protected AbstractVariantToConstructedCastRule(CastRulePredicate
predicate) {
- super(predicate);
- }
-
- @Override
- public boolean canFail(LogicalType inputLogicalType, LogicalType
targetLogicalType) {
- return true;
- }
-
- /**
- * Treats a variant that stores a {@code null} as a {@code NULL} input, so
a top-level VARIANT
- * null casts to SQL {@code NULL} before any shape check runs. Only
applied for a nullable
- * target: a {@code NOT NULL} result cannot carry {@code NULL}, so a
null-valued variant then
- * fails the shape check as a regular mismatch.
- */
- @Override
- public CastCodeBlock generateCodeBlock(
- CodeGeneratorCastRule.Context context,
- String inputTerm,
- String inputIsNullTerm,
- LogicalType inputLogicalType,
- LogicalType targetLogicalType) {
- if (!targetLogicalType.isNullable()) {
- return super.generateCodeBlock(
- context, inputTerm, inputIsNullTerm, inputLogicalType,
targetLogicalType);
- }
- final String isNullTerm =
- "(" + inputIsNullTerm + " || " + methodCall(inputTerm,
"isNull") + ")";
- return super.generateCodeBlock(
- context, inputTerm, isNullTerm, inputLogicalType,
targetLogicalType);
- }
-}
diff --git
a/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/functions/casting/CastRuleProvider.java
b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/functions/casting/CastRuleProvider.java
index 485b1ab89b1..dcd6cdb891c 100644
---
a/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/functions/casting/CastRuleProvider.java
+++
b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/functions/casting/CastRuleProvider.java
@@ -96,11 +96,7 @@ public class CastRuleProvider {
.addRule(MapToMapAndMultisetToMultisetCastRule.INSTANCE)
.addRule(RowToRowCastRule.INSTANCE)
// Variant rules
- .addRule(VariantToStringCastRule.INSTANCE)
- .addRule(VariantToPrimitiveCastRule.INSTANCE)
- .addRule(VariantToArrayCastRule.INSTANCE)
- .addRule(VariantToRowCastRule.INSTANCE)
- .addRule(VariantToMapCastRule.INSTANCE)
+ .addRule(FromVariantCastRule.INSTANCE)
.addRule(ToVariantCastRule.INSTANCE)
// Bitmap rules
.addRule(BitmapToStringCastRule.INSTANCE)
diff --git
a/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/functions/casting/CodeGeneratorCastRule.java
b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/functions/casting/CodeGeneratorCastRule.java
index 17a56dcc623..eb05de6b63d 100644
---
a/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/functions/casting/CodeGeneratorCastRule.java
+++
b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/functions/casting/CodeGeneratorCastRule.java
@@ -22,6 +22,8 @@ import org.apache.flink.annotation.Internal;
import org.apache.flink.table.planner.codegen.CodeGeneratorContext;
import org.apache.flink.table.types.logical.LogicalType;
+import java.time.ZoneId;
+
/**
* Cast rule that has code generation capabilities.
*
@@ -62,6 +64,11 @@ public interface CodeGeneratorCastRule<IN, OUT> extends
CastRule<IN, OUT> {
*/
String getSessionTimeZoneTerm();
+ /**
+ * @return the session time zone.
+ */
+ ZoneId getSessionZoneId();
+
/**
* Declare a new variable accessible within the scope of the caller of
{@link
* #generateCodeBlock(Context, String, String, LogicalType,
LogicalType)}.
diff --git
a/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/functions/casting/FromVariantCastRule.java
b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/functions/casting/FromVariantCastRule.java
new file mode 100644
index 00000000000..14985f60337
--- /dev/null
+++
b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/functions/casting/FromVariantCastRule.java
@@ -0,0 +1,125 @@
+/*
+ * 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.data.utils.FromVariantConverter;
+import org.apache.flink.table.data.utils.VariantCastUtils;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.LogicalTypeFamily;
+import org.apache.flink.table.types.logical.LogicalTypeRoot;
+import org.apache.flink.table.types.logical.utils.LogicalTypeCasts;
+import org.apache.flink.types.variant.Variant;
+
+import static
org.apache.flink.table.planner.codegen.CodeGenUtils.boxedTypeTermForType;
+import static
org.apache.flink.table.planner.codegen.calls.BuiltInMethods.BINARY_STRING_DATA_FROM_STRING;
+import static
org.apache.flink.table.planner.functions.casting.CastRuleUtils.cast;
+import static
org.apache.flink.table.planner.functions.casting.CastRuleUtils.methodCall;
+import static
org.apache.flink.table.planner.functions.casting.CastRuleUtils.staticCall;
+import static
org.apache.flink.table.planner.functions.casting.CastRuleUtils.unbox;
+
+/**
+ * Cast rule from {@link LogicalTypeRoot#VARIANT} to every type that {@link
LogicalTypeCasts}
+ * allows. The generated code calls a {@link FromVariantConverter}, which also
trims and pads a
+ * {@code CHAR(n)} or {@code VARCHAR(n)} target. Printing calls {@link
+ * VariantCastUtils#toPrintString} instead, because a printed value must never
fail.
+ */
+class FromVariantCastRule extends
AbstractNullAwareCodeGeneratorCastRule<Variant, Object> {
+
+ static final FromVariantCastRule INSTANCE = new FromVariantCastRule();
+
+ private FromVariantCastRule() {
+
super(CastRulePredicate.builder().predicate(FromVariantCastRule::matches).build());
+ }
+
+ private static boolean matches(final LogicalType input, final LogicalType
target) {
+ return input.is(LogicalTypeRoot.VARIANT)
+ && !target.is(LogicalTypeRoot.VARIANT)
+ && LogicalTypeCasts.supportsExplicitCast(input, target);
+ }
+
+ private static boolean isPrinting(
+ final CodeGeneratorCastRule.Context context, final LogicalType
target) {
+ return context.isPrinting() &&
target.is(LogicalTypeFamily.CHARACTER_STRING);
+ }
+
+ @Override
+ public boolean canFail(
+ final LogicalType inputLogicalType, final LogicalType
targetLogicalType) {
+ return true;
+ }
+
+ /** Maps a variant null to SQL {@code NULL} for a nullable target, except
when printing. */
+ @Override
+ public CastCodeBlock generateCodeBlock(
+ final CodeGeneratorCastRule.Context context,
+ final String inputTerm,
+ final String inputIsNullTerm,
+ final LogicalType inputLogicalType,
+ final LogicalType targetLogicalType) {
+ if (!targetLogicalType.isNullable() || isPrinting(context,
targetLogicalType)) {
+ return super.generateCodeBlock(
+ context, inputTerm, inputIsNullTerm, inputLogicalType,
targetLogicalType);
+ }
+ final String isNullTerm =
+ "(" + inputIsNullTerm + " || " + methodCall(inputTerm,
"isNull") + ")";
+ return super.generateCodeBlock(
+ context, inputTerm, isNullTerm, inputLogicalType,
targetLogicalType);
+ }
+
+ /* Example generated code for INT, inside the null check of the base class:
+
+ result$1 =
((java.lang.Integer)(fromVariantConverter$2.convert(variant$0))).intValue();
+
+ */
+ @Override
+ protected String generateCodeBlockInternal(
+ final CodeGeneratorCastRule.Context context,
+ final String inputTerm,
+ final String returnVariable,
+ final LogicalType inputLogicalType,
+ final LogicalType targetLogicalType) {
+ if (isPrinting(context, targetLogicalType)) {
+ // toPrintString returns a String, which the result has to wrap.
+ return new CastRuleUtils.CodeWriter()
+ .assignStmt(
+ returnVariable,
+ staticCall(
+ BINARY_STRING_DATA_FROM_STRING(),
+ staticCall(
+ VariantCastUtils.class,
+ "toPrintString",
+ inputTerm,
+ context.getSessionTimeZoneTerm())))
+ .toString();
+ }
+ final String converterTerm =
+ context.declareReusableObject(
+ FromVariantConverter.create(targetLogicalType,
context.getSessionZoneId()),
+ "fromVariantConverter");
+ return new CastRuleUtils.CodeWriter()
+ .assignStmt(
+ returnVariable,
+ unbox(
+ cast(
+
boxedTypeTermForType(targetLogicalType),
+ methodCall(converterTerm, "convert",
inputTerm)),
+ targetLogicalType))
+ .toString();
+ }
+}
diff --git
a/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/functions/casting/VariantToArrayCastRule.java
b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/functions/casting/VariantToArrayCastRule.java
deleted file mode 100644
index 5761cb55044..00000000000
---
a/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/functions/casting/VariantToArrayCastRule.java
+++ /dev/null
@@ -1,171 +0,0 @@
-/*
- * 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.data.ArrayData;
-import org.apache.flink.table.data.GenericArrayData;
-import org.apache.flink.table.planner.codegen.CodeGenUtils;
-import org.apache.flink.table.runtime.functions.VariantCastUtils;
-import org.apache.flink.table.types.logical.ArrayType;
-import org.apache.flink.table.types.logical.LogicalType;
-import org.apache.flink.table.types.logical.LogicalTypeRoot;
-import org.apache.flink.types.variant.Variant;
-
-import static org.apache.flink.table.planner.codegen.CodeGenUtils.newName;
-import static
org.apache.flink.table.planner.functions.casting.CastRuleUtils.constructorCall;
-import static
org.apache.flink.table.planner.functions.casting.CastRuleUtils.methodCall;
-import static
org.apache.flink.table.planner.functions.casting.CastRuleUtils.newArray;
-import static
org.apache.flink.table.planner.functions.casting.CastRuleUtils.staticCall;
-import static
org.apache.flink.table.planner.functions.casting.CastRuleUtils.strLiteral;
-
-/**
- * {@link LogicalTypeRoot#VARIANT} to {@link LogicalTypeRoot#ARRAY} cast rule.
- *
- * <p>The variant must be an array, otherwise the cast fails. Each element is
itself a variant and
- * casts to the target element type by the full {@code VARIANT}-to-element
rule, recursively. An
- * element that stores a {@code null} maps to SQL {@code NULL} when the
element type is nullable and
- * fails the cast when it is {@code NOT NULL}. The exception is a {@code
VARIANT} element type:
- * there the element cast is the identity, so a VARIANT null element is kept
as a variant null
- * rather than downgraded to SQL {@code NULL}.
- */
-class VariantToArrayCastRule extends
AbstractVariantToConstructedCastRule<ArrayData> {
-
- static final VariantToArrayCastRule INSTANCE = new
VariantToArrayCastRule();
-
- private VariantToArrayCastRule() {
- super(
- CastRulePredicate.builder()
- .predicate(
- (input, target) ->
- input.is(LogicalTypeRoot.VARIANT)
- &&
target.is(LogicalTypeRoot.ARRAY)
- && CastRuleProvider.resolve(
- input,
- ((ArrayType)
target)
-
.getElementType())
- != null)
- .build());
- }
-
- /* Example generated code for ARRAY<INT>:
-
- int arraySize$2 =
-
org.apache.flink.table.runtime.functions.VariantCastUtils.arraySize(
- variant$1, "ARRAY<INT>");
- java.lang.Integer[] objArray$3 = new java.lang.Integer[arraySize$2];
- for (int i$4 = 0; i$4 < arraySize$2; i$4++) {
- org.apache.flink.types.variant.Variant element$5 =
variant$1.getElement(i$4);
- if (!element$5.isNull()) {
- result$6 =
- ((int)
org.apache.flink.table.runtime.functions.VariantCastUtils.toIntegral(
- element$5, -2147483648L, 2147483647L, "INTEGER"));
- objArray$3[i$4] = result$6;
- }
- }
- result$0 = new org.apache.flink.table.data.GenericArrayData(objArray$3);
-
- A VARIANT null element leaves the slot null (SQL NULL); a NOT NULL element
type throws instead.
-
- */
- @Override
- protected String generateCodeBlockInternal(
- CodeGeneratorCastRule.Context context,
- String inputTerm,
- String returnVariable,
- LogicalType inputLogicalType,
- LogicalType targetLogicalType) {
- final LogicalType elementType = ((ArrayType)
targetLogicalType).getElementType();
- final String elementTypeTerm = arrayElementType(elementType);
- final String sizeTerm = newName(context.getCodeGeneratorContext(),
"arraySize");
- final String arrayTerm = newName(context.getCodeGeneratorContext(),
"objArray");
- final String elementTerm = newName(context.getCodeGeneratorContext(),
"element");
-
- // For a typed element the VARIANT null is handled in the loop below,
so the inner cast is
- // the plain VARIANT-to-element rule on a non-null variant: a nullable
element maps it to
- // SQL NULL, a NOT NULL element fails. For a VARIANT element the inner
cast is the identity,
- // which keeps a variant null as-is.
- final CastCodeBlock elementCast =
- CastRuleProvider.generateAlwaysNonNullCodeBlock(
- context, elementTerm, inputLogicalType, elementType);
-
- return new CastRuleUtils.CodeWriter()
- .declStmt(
- int.class,
- sizeTerm,
- staticCall(
- VariantCastUtils.class,
- "arraySize",
- inputTerm,
-
strLiteral(targetLogicalType.asSummaryString())))
- .declStmt(elementTypeTerm + "[]", arrayTerm,
newArray(elementTypeTerm, sizeTerm))
- .forStmt(
- sizeTerm,
- (index, loopWriter) -> {
- loopWriter.declStmt(
- Variant.class,
- elementTerm,
- methodCall(inputTerm, "getElement",
index));
- if (elementType.is(LogicalTypeRoot.VARIANT)) {
- // The element cast is the identity, so a
VARIANT null element is a
- // valid variant null and is kept as-is rather
than downgraded to
- // SQL NULL, matching the top-level VARIANT
cast.
- loopWriter
- .append(elementCast)
- .assignArrayStmt(
- arrayTerm, index,
elementCast.getReturnTerm());
- return;
- }
- final String isPresent = "!" +
methodCall(elementTerm, "isNull");
- if (elementType.isNullable()) {
- loopWriter.ifStmt(
- isPresent,
- thenWriter ->
- thenWriter
- .append(elementCast)
- .assignArrayStmt(
- arrayTerm,
- index,
-
elementCast.getReturnTerm()));
- } else {
- loopWriter.ifStmt(
- isPresent,
- thenWriter ->
- thenWriter
- .append(elementCast)
- .assignArrayStmt(
- arrayTerm,
- index,
-
elementCast.getReturnTerm()),
- elseWriter ->
- elseWriter.throwStmt(
- "new
org.apache.flink.table.api.TableRuntimeException(\"Cannot cast a VARIANT null
array element to a NOT NULL element type.\")"));
- }
- },
- context.getCodeGeneratorContext())
- .assignStmt(returnVariable,
constructorCall(GenericArrayData.class, arrayTerm))
- .toString();
- }
-
- private static String arrayElementType(LogicalType elementType) {
- if (elementType.isNullable()) {
- return CodeGenUtils.boxedTypeTermForType(elementType);
- }
- return CodeGenUtils.primitiveTypeTermForType(elementType);
- }
-}
diff --git
a/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/functions/casting/VariantToMapCastRule.java
b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/functions/casting/VariantToMapCastRule.java
deleted file mode 100644
index 71a8369de0d..00000000000
---
a/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/functions/casting/VariantToMapCastRule.java
+++ /dev/null
@@ -1,208 +0,0 @@
-/*
- * 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.data.GenericMapData;
-import org.apache.flink.table.data.MapData;
-import org.apache.flink.table.planner.codegen.CodeGeneratorContext;
-import org.apache.flink.table.runtime.functions.VariantCastUtils;
-import org.apache.flink.table.types.logical.LogicalType;
-import org.apache.flink.table.types.logical.LogicalTypeFamily;
-import org.apache.flink.table.types.logical.LogicalTypeRoot;
-import org.apache.flink.table.types.logical.MapType;
-import org.apache.flink.table.types.logical.utils.LogicalTypeChecks;
-import org.apache.flink.types.variant.Variant;
-
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-import static
org.apache.flink.table.planner.codegen.CodeGenUtils.boxedTypeTermForType;
-import static org.apache.flink.table.planner.codegen.CodeGenUtils.className;
-import static org.apache.flink.table.planner.codegen.CodeGenUtils.newName;
-import static
org.apache.flink.table.planner.functions.casting.CastRuleUtils.cast;
-import static
org.apache.flink.table.planner.functions.casting.CastRuleUtils.constructorCall;
-import static
org.apache.flink.table.planner.functions.casting.CastRuleUtils.methodCall;
-import static
org.apache.flink.table.planner.functions.casting.CastRuleUtils.staticCall;
-import static
org.apache.flink.table.planner.functions.casting.CastRuleUtils.strLiteral;
-
-/**
- * {@link LogicalTypeRoot#VARIANT} to {@link LogicalTypeRoot#MAP} cast rule.
- *
- * <p>The variant must be an object. Each field name becomes a key and each
field value casts to the
- * target value type by the full {@code VARIANT}-to-value rule, recursively.
The key type must be a
- * character string, because a variant object's keys are always strings; a
non-string key type is
- * rejected at validation. This is the only way to read an object whose keys
are dynamic or unknown
- * at query time, and {@code MAP<STRING, VARIANT>} is the schemaless read of
an object.
- *
- * <p>A {@link LogicalTypeRoot#VARIANT} value type is the identity, so a
VARIANT null value is kept
- * as a variant null rather than downgraded to SQL {@code NULL}, matching
{@code ARRAY<VARIANT>} and
- * a {@code ROW} with {@code VARIANT} fields.
- */
-class VariantToMapCastRule extends
AbstractVariantToConstructedCastRule<MapData> {
-
- static final VariantToMapCastRule INSTANCE = new VariantToMapCastRule();
-
- private VariantToMapCastRule() {
- super(
- CastRulePredicate.builder()
- .predicate(
- (input, target) ->
- input.is(LogicalTypeRoot.VARIANT)
- &&
target.is(LogicalTypeRoot.MAP)
- && ((MapType) target)
- .getKeyType()
-
.is(LogicalTypeFamily.CHARACTER_STRING)
- && CastRuleProvider.resolve(
- input,
- ((MapType)
target).getValueType())
- != null)
- .build());
- }
-
- /* Example generated code for MAP<`STRING`, `INT`>. Each field name
becomes a key and each value
- runs the leaf cast; a nullable value that is a VARIANT null is left as SQL
NULL:
-
- org.apache.flink.table.runtime.functions.VariantCastUtils.requireObject(
- variant$2, "MAP<STRING, INT>");
- java.util.List fieldNames$4 = variant$2.getFieldNames();
- int size$5 = fieldNames$4.size();
- java.util.Map map$6 = new java.util.HashMap(size$5);
- for (int i = 0; i < size$5; i++) {
- java.lang.String name$7 = (java.lang.String) fieldNames$4.get(i);
- org.apache.flink.types.variant.Variant valueVariant$8 =
variant$2.getField(name$7);
- java.lang.Integer value$9 = null;
- if (!valueVariant$8.isNull()) {
- result$10 =
- ((int)
org.apache.flink.table.runtime.functions.VariantCastUtils.toIntegral(
- valueVariant$8, -2147483648L, 2147483647L,
"INTEGER"));
- value$9 = result$10;
- }
- map$6.put(
-
org.apache.flink.table.runtime.functions.VariantCastUtils.variantKey(
- name$7, 2147483647, false),
- value$9);
- }
- result$3 = new org.apache.flink.table.data.GenericMapData(map$6);
-
- A NOT NULL value type throws instead of leaving value$9 null for a VARIANT
null value. A VARIANT
- value type takes the identity cast unconditionally, so a VARIANT null is
kept as a variant null.
-
- */
- @Override
- protected String generateCodeBlockInternal(
- CodeGeneratorCastRule.Context context,
- String inputTerm,
- String returnVariable,
- LogicalType inputLogicalType,
- LogicalType targetLogicalType) {
- final MapType mapType = (MapType) targetLogicalType;
- final LogicalType keyType = mapType.getKeyType();
- final LogicalType valueType = mapType.getValueType();
- final CodeGeneratorContext codeGeneratorContext =
context.getCodeGeneratorContext();
-
- final int keyLength = LogicalTypeChecks.getLength(keyType);
- final boolean charKey = keyType.is(LogicalTypeRoot.CHAR);
- final String valueTypeTerm = boxedTypeTermForType(valueType);
-
- final String namesTerm = newName(codeGeneratorContext, "fieldNames");
- final String sizeTerm = newName(codeGeneratorContext, "size");
- final String mapTerm = newName(codeGeneratorContext, "map");
- final String nameTerm = newName(codeGeneratorContext, "name");
- final String valueVariantTerm = newName(codeGeneratorContext,
"valueVariant");
- final String valueTerm = newName(codeGeneratorContext, "value");
-
- // The value is guaranteed non-null here, since a VARIANT null is
handled below, so the
- // inner
- // cast is the plain VARIANT-to-value rule. A VARIANT null value maps
to SQL NULL for a
- // nullable value type, or fails the cast for a NOT NULL one.
- final CastCodeBlock valueCast =
- CastRuleProvider.generateAlwaysNonNullCodeBlock(
- context, valueVariantTerm, inputLogicalType,
valueType);
- final String putKey =
- staticCall(
- VariantCastUtils.class,
- "variantKey",
- nameTerm,
- String.valueOf(keyLength),
- charKey);
-
- return new CastRuleUtils.CodeWriter()
- .stmt(
- staticCall(
- VariantCastUtils.class,
- "requireObject",
- inputTerm,
-
strLiteral(targetLogicalType.asSummaryString())))
- .declStmt(className(List.class), namesTerm,
methodCall(inputTerm, "getFieldNames"))
- .declStmt(int.class, sizeTerm, methodCall(namesTerm, "size"))
- .declStmt(className(Map.class), mapTerm,
constructorCall(HashMap.class, sizeTerm))
- .forStmt(
- sizeTerm,
- (index, loopWriter) -> {
- loopWriter
- .declStmt(
- String.class,
- nameTerm,
- cast(
- className(String.class),
- methodCall(namesTerm,
"get", index)))
- .declStmt(
- Variant.class,
- valueVariantTerm,
- methodCall(inputTerm, "getField",
nameTerm))
- .declStmt(valueTypeTerm, valueTerm,
"null");
- final String isPresent = "!" +
methodCall(valueVariantTerm, "isNull");
- if (valueType.is(LogicalTypeRoot.VARIANT)) {
- // The value cast is the identity, so a
present VARIANT null value
- // is
- // a valid variant null and is kept as-is
rather than downgraded to
- // SQL NULL, matching ARRAY<VARIANT> and
ROW<VARIANT>.
- loopWriter
- .append(valueCast)
- .assignStmt(valueTerm,
valueCast.getReturnTerm());
- } else if (valueType.isNullable()) {
- loopWriter.ifStmt(
- isPresent,
- thenWriter ->
- thenWriter
- .append(valueCast)
- .assignStmt(
- valueTerm,
-
valueCast.getReturnTerm()));
- } else {
- loopWriter.ifStmt(
- isPresent,
- thenWriter ->
- thenWriter
- .append(valueCast)
- .assignStmt(
- valueTerm,
-
valueCast.getReturnTerm()),
- elseWriter ->
- elseWriter.throwStmt(
- "new
org.apache.flink.table.api.TableRuntimeException(\"Cannot cast a VARIANT null
object value to a NOT NULL map value type.\")"));
- }
- loopWriter.stmt(methodCall(mapTerm, "put", putKey,
valueTerm));
- },
- codeGeneratorContext)
- .assignStmt(returnVariable,
constructorCall(GenericMapData.class, mapTerm))
- .toString();
- }
-}
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
deleted file mode 100644
index b0bf6703e64..00000000000
---
a/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/functions/casting/VariantToPrimitiveCastRule.java
+++ /dev/null
@@ -1,241 +0,0 @@
-/*
- * 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.planner.functions.casting.CastRuleUtils.CodeWriter;
-import org.apache.flink.table.runtime.functions.VariantCastUtils;
-import org.apache.flink.table.types.logical.DecimalType;
-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.codegen.CodeGenUtils.primitiveTypeTermForType;
-import static
org.apache.flink.table.planner.functions.casting.CastRuleUtils.cast;
-import static
org.apache.flink.table.planner.functions.casting.CastRuleUtils.methodCall;
-import static
org.apache.flink.table.planner.functions.casting.CastRuleUtils.staticCall;
-import static
org.apache.flink.table.planner.functions.casting.CastRuleUtils.strLiteral;
-
-/**
- * {@link LogicalTypeRoot#VARIANT} to primitive type cast rule.
- *
- * <p>A cast succeeds only when the target holds the stored value without
altering it, otherwise it
- * fails {@code CAST} and yields {@code null} for {@code TRY_CAST}. An integer
widens or narrows as
- * long as it stays in range, a {@code DECIMAL} has to fit the precision and
scale without rounding,
- * and a timestamp has to fit the target precision. {@code FLOAT} and {@code
DOUBLE} are approximate
- * by definition, so they take any numeric kind and reject only a magnitude
out of range. Changing
- * 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}.
- */
-class VariantToPrimitiveCastRule extends
AbstractNullAwareCodeGeneratorCastRule<Variant, Object> {
-
- static final VariantToPrimitiveCastRule INSTANCE = new
VariantToPrimitiveCastRule();
-
- private VariantToPrimitiveCastRule() {
- super(
- CastRulePredicate.builder()
- .predicate(
- (input, target) ->
- input.is(LogicalTypeRoot.VARIANT)
- && isSupportedTarget(target))
- .build());
- }
-
- private static boolean isSupportedTarget(LogicalType targetType) {
- switch (targetType.getTypeRoot()) {
- case BOOLEAN:
- case TINYINT:
- case SMALLINT:
- case INTEGER:
- case BIGINT:
- case FLOAT:
- case DOUBLE:
- case DECIMAL:
- 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) {
- return true;
- }
-
- /**
- * Treats a variant that stores a JSON {@code null} as a {@code NULL}
input, so it casts to SQL
- * {@code NULL} instead of failing in the type-specific accessor. Only
applied for a nullable
- * target: a {@code NOT NULL} result cannot carry {@code NULL}, so a
null-valued variant then
- * fails as a regular type mismatch.
- */
- @Override
- public CastCodeBlock generateCodeBlock(
- CodeGeneratorCastRule.Context context,
- String inputTerm,
- String inputIsNullTerm,
- LogicalType inputLogicalType,
- LogicalType targetLogicalType) {
- if (!targetLogicalType.isNullable()) {
- return super.generateCodeBlock(
- context, inputTerm, inputIsNullTerm, inputLogicalType,
targetLogicalType);
- }
- final String isNullTerm =
- "(" + inputIsNullTerm + " || " + methodCall(inputTerm,
"isNull") + ")";
- return super.generateCodeBlock(
- context, inputTerm, isNullTerm, inputLogicalType,
targetLogicalType);
- }
-
- @Override
- protected String generateCodeBlockInternal(
- CodeGeneratorCastRule.Context context,
- String inputTerm,
- String returnVariable,
- LogicalType inputLogicalType,
- LogicalType targetLogicalType) {
- final CodeWriter writer = new CastRuleUtils.CodeWriter();
- switch (targetLogicalType.getTypeRoot()) {
- case BOOLEAN:
- writer.assignStmt(returnVariable, methodCall(inputTerm,
"getBoolean"));
- break;
- case TINYINT:
- case SMALLINT:
- case INTEGER:
- case BIGINT:
- writer.assignStmt(
- returnVariable,
- cast(
- primitiveTypeTermForType(targetLogicalType),
- staticCall(
- VariantCastUtils.class,
- "toIntegral",
- inputTerm,
- // A long literal needs the suffix to
compile, since
- // Long.MIN_VALUE does not fit an int
literal.
- integralMin(targetLogicalType) + "L",
- integralMax(targetLogicalType) + "L",
-
strLiteral(targetLogicalType.getTypeRoot().name()))));
- break;
- case FLOAT:
- writer.assignStmt(
- returnVariable, staticCall(VariantCastUtils.class,
"toFloat", inputTerm));
- break;
- case DOUBLE:
- writer.assignStmt(
- returnVariable, staticCall(VariantCastUtils.class,
"toDouble", inputTerm));
- break;
- case DECIMAL:
- final DecimalType decimalType = (DecimalType)
targetLogicalType;
- writer.assignStmt(
- returnVariable,
- staticCall(
- VariantCastUtils.class,
- "toDecimal",
- inputTerm,
- decimalType.getPrecision(),
- decimalType.getScale()));
- break;
- case BINARY:
- case VARBINARY:
- writer.assignStmt(
- returnVariable,
- staticCall(
- VariantCastUtils.class,
- "toBytes",
- inputTerm,
- LogicalTypeChecks.getLength(targetLogicalType),
- targetLogicalType.is(LogicalTypeRoot.BINARY)));
- break;
- case DATE:
- writer.assignStmt(
- 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,
- staticCall(
- VariantCastUtils.class,
- "toTimestamp",
- inputTerm,
-
LogicalTypeChecks.getPrecision(targetLogicalType)));
- break;
- case TIMESTAMP_WITH_LOCAL_TIME_ZONE:
- writer.assignStmt(
- returnVariable,
- staticCall(
- VariantCastUtils.class,
- "toTimestampLtz",
- inputTerm,
-
LogicalTypeChecks.getPrecision(targetLogicalType)));
- break;
- case UUID:
- writer.assignStmt(
- returnVariable, staticCall(VariantCastUtils.class,
"toUuid", inputTerm));
- break;
- default:
- throw new IllegalArgumentException(
- "Unsupported target type for casting from VARIANT: " +
targetLogicalType);
- }
- return writer.toString();
- }
-
- private static long integralMin(LogicalType target) {
- switch (target.getTypeRoot()) {
- case TINYINT:
- return Byte.MIN_VALUE;
- case SMALLINT:
- return Short.MIN_VALUE;
- case INTEGER:
- return Integer.MIN_VALUE;
- default:
- return Long.MIN_VALUE;
- }
- }
-
- private static long integralMax(LogicalType target) {
- switch (target.getTypeRoot()) {
- case TINYINT:
- return Byte.MAX_VALUE;
- case SMALLINT:
- return Short.MAX_VALUE;
- case INTEGER:
- return Integer.MAX_VALUE;
- default:
- return Long.MAX_VALUE;
- }
- }
-}
diff --git
a/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/functions/casting/VariantToRowCastRule.java
b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/functions/casting/VariantToRowCastRule.java
deleted file mode 100644
index ade8e61f128..00000000000
---
a/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/functions/casting/VariantToRowCastRule.java
+++ /dev/null
@@ -1,234 +0,0 @@
-/*
- * 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.api.TableRuntimeException;
-import org.apache.flink.table.data.RowData;
-import org.apache.flink.table.data.binary.BinaryRowData;
-import org.apache.flink.table.data.writer.BinaryRowWriter;
-import org.apache.flink.table.planner.codegen.CodeGeneratorContext;
-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 java.util.List;
-
-import static org.apache.flink.table.planner.codegen.CodeGenUtils.className;
-import static org.apache.flink.table.planner.codegen.CodeGenUtils.newName;
-import static
org.apache.flink.table.planner.functions.casting.CastRuleUtils.binaryWriterWriteField;
-import static
org.apache.flink.table.planner.functions.casting.CastRuleUtils.binaryWriterWriteNull;
-import static
org.apache.flink.table.planner.functions.casting.CastRuleUtils.constructorCall;
-import static
org.apache.flink.table.planner.functions.casting.CastRuleUtils.methodCall;
-import static
org.apache.flink.table.planner.functions.casting.CastRuleUtils.staticCall;
-import static
org.apache.flink.table.planner.functions.casting.CastRuleUtils.strLiteral;
-
-/**
- * {@link LogicalTypeRoot#VARIANT} to {@link LogicalTypeRoot#ROW} and {@link
- * LogicalTypeRoot#STRUCTURED_TYPE} cast rule.
- *
- * <p>The variant must be an object, otherwise the cast fails. Fields match
<b>by name</b> rather
- * than by position, because a VARIANT object is unordered. Name matching is
case sensitive. A
- * target field absent from the object fails the cast. A field present but set
to a VARIANT null
- * maps to SQL {@code NULL} when it is nullable and fails the cast when it is
{@code NOT NULL}. A
- * {@link LogicalTypeRoot#VARIANT} target field is the exception: there the
field cast is the
- * identity, so a VARIANT null is kept as a variant null rather than
downgraded to SQL {@code NULL}.
- * Object fields the target does not name are dropped, so the row is a
projection. A {@code
- * STRUCTURED} target shares the {@code RowData} representation and is served
by the same rule.
- */
-class VariantToRowCastRule extends
AbstractVariantToConstructedCastRule<RowData> {
-
- static final VariantToRowCastRule INSTANCE = new VariantToRowCastRule();
-
- private VariantToRowCastRule() {
-
super(CastRulePredicate.builder().predicate(VariantToRowCastRule::matches).build());
- }
-
- private static boolean matches(LogicalType input, LogicalType target) {
- if (!(input.is(LogicalTypeRoot.VARIANT)
- && (target.is(LogicalTypeRoot.ROW)
- || target.is(LogicalTypeRoot.STRUCTURED_TYPE)))) {
- return false;
- }
- return LogicalTypeChecks.getFieldTypes(target).stream()
- .allMatch(fieldType -> CastRuleProvider.resolve(input,
fieldType) != null);
- }
-
- /* Example generated code for ROW<`id` INT, `raw` VARIANT>. The typed `id`
field runs the leaf
- cast; the `raw` field runs the identity cast and is written as-is, so a
VARIANT null is kept
- rather than turned into SQL NULL:
-
- org.apache.flink.table.runtime.functions.VariantCastUtils.requireObject(
- variant$2, "ROW<`id` INT, `raw` VARIANT>");
- writer$4.reset();
- org.apache.flink.types.variant.Variant f0Value$5 =
variant$2.getField("id");
- if (f0Value$5 == null) {
- throw new org.apache.flink.table.api.TableRuntimeException(
- "Cannot cast the VARIANT object to ROW<`id` INT, `raw`
VARIANT> because the field "
- + "'id' is not present in the VARIANT.");
- } else {
- if (f0Value$5.isNull()) {
- writer$4.setNullAt(0);
- } else {
- result$6 =
- ((int)
org.apache.flink.table.runtime.functions.VariantCastUtils.toIntegral(
- f0Value$5, -2147483648L, 2147483647L, "INTEGER"));
- if (!isNull$7) {
- writer$4.writeInt(0, result$6);
- } else {
- writer$4.setNullAt(0);
- }
- }
- }
- org.apache.flink.types.variant.Variant f1Value$8 =
variant$2.getField("raw");
- if (f1Value$8 == null) {
- throw new org.apache.flink.table.api.TableRuntimeException(
- "Cannot cast the VARIANT object to ROW<`id` INT, `raw`
VARIANT> because the field "
- + "'raw' is not present in the VARIANT.");
- } else {
- writer$4.writeVariant(1, f1Value$8);
- }
- writer$4.complete();
- result$3 = row$1.copy();
-
- A NOT NULL typed field throws instead of the setNullAt(i) shown above when
its value is a
- VARIANT null.
-
- */
- @Override
- protected String generateCodeBlockInternal(
- CodeGeneratorCastRule.Context context,
- String inputTerm,
- String returnVariable,
- LogicalType inputLogicalType,
- LogicalType targetLogicalType) {
- final List<String> fieldNames =
LogicalTypeChecks.getFieldNames(targetLogicalType);
- final List<LogicalType> fieldTypes =
LogicalTypeChecks.getFieldTypes(targetLogicalType);
- final CodeGeneratorContext codeGeneratorContext =
context.getCodeGeneratorContext();
-
- final String rowTerm = newName(codeGeneratorContext, "row");
- final String writerTerm = newName(codeGeneratorContext, "writer");
- context.declareClassField(
- className(BinaryRowData.class),
- rowTerm,
- constructorCall(BinaryRowData.class, fieldTypes.size()));
- context.declareClassField(
- className(BinaryRowWriter.class),
- writerTerm,
- constructorCall(BinaryRowWriter.class, rowTerm));
-
- final CastRuleUtils.CodeWriter writer =
- new CastRuleUtils.CodeWriter()
- .stmt(
- staticCall(
- VariantCastUtils.class,
- "requireObject",
- inputTerm,
-
strLiteral(targetLogicalType.asSummaryString())))
- .stmt(methodCall(writerTerm, "reset"));
-
- for (int i = 0; i < fieldTypes.size(); i++) {
- final LogicalType fieldType = fieldTypes.get(i);
- final String fieldName = fieldNames.get(i);
- final String indexTerm = String.valueOf(i);
- final String fieldTerm = newName(codeGeneratorContext, "f" +
indexTerm + "Value");
-
- // The field is guaranteed present and non-null here, since a
missing field and a
- // VARIANT
- // null are handled below, so the inner cast is the plain
VARIANT-to-field rule.
- final CastCodeBlock codeBlock =
- CastRuleProvider.generateAlwaysNonNullCodeBlock(
- context, fieldTerm, inputLogicalType, fieldType);
- final String writeField =
- binaryWriterWriteField(
- context, writerTerm, fieldType, indexTerm,
codeBlock.getReturnTerm());
- final String writeNull = binaryWriterWriteNull(writerTerm,
fieldType, indexTerm);
-
- // getField returns Java null for a field the object does not
carry, which always fails
- // the cast.
- writer.declStmt(
- Variant.class,
- fieldTerm,
- methodCall(inputTerm, "getField", strLiteral(fieldName)));
-
- if (fieldType.is(LogicalTypeRoot.VARIANT)) {
- // The field cast is the identity, so a present VARIANT null
field is a valid
- // variant
- // null and is kept as-is rather than downgraded to SQL NULL,
matching
- // ARRAY<VARIANT>
- // and the top-level VARIANT cast. An absent field still fails
the cast.
- writer.ifStmt(
- fieldTerm + " == null",
- absentWriter ->
- absentWriter.throwStmt(
- missingFieldError(fieldName,
targetLogicalType)),
- presentWriter ->
presentWriter.append(codeBlock).stmt(writeField));
- continue;
- }
-
- // A field explicitly set to a VARIANT null returns a variant
whose isNull() is true; it
- // maps to SQL NULL for a nullable field and fails the cast for a
NOT NULL one.
- writer.ifStmt(
- fieldTerm + " == null",
- absentWriter ->
-
absentWriter.throwStmt(missingFieldError(fieldName, targetLogicalType)),
- presentWriter ->
- presentWriter.ifStmt(
- methodCall(fieldTerm, "isNull"),
- nullWriter -> {
- if (fieldType.isNullable()) {
- nullWriter.stmt(writeNull);
- } else {
- nullWriter.throwStmt(
- nullFieldError(fieldName,
targetLogicalType));
- }
- },
- valueWriter ->
- valueWriter
- .append(codeBlock)
- .ifStmt(
- "!" +
codeBlock.getIsNullTerm(),
- thenWriter ->
-
thenWriter.stmt(writeField),
- elseWriter ->
-
elseWriter.stmt(writeNull))));
- }
-
- writer.stmt(methodCall(writerTerm, "complete"))
- .assignStmt(returnVariable, methodCall(rowTerm, "copy"));
- return writer.toString();
- }
-
- private static String missingFieldError(String fieldName, LogicalType
targetType) {
- final String message =
- String.format(
- "Cannot cast the VARIANT object to %s because the
field '%s' is not present in the VARIANT.",
- targetType.asSummaryString(), fieldName);
- return constructorCall(TableRuntimeException.class,
strLiteral(message));
- }
-
- private static String nullFieldError(String fieldName, LogicalType
targetType) {
- final String message =
- String.format(
- "Cannot cast the VARIANT object to %s because the
field '%s' is a VARIANT null and the target does not accept NULL.",
- targetType.asSummaryString(), fieldName);
- return constructorCall(TableRuntimeException.class,
strLiteral(message));
- }
-}
diff --git
a/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/functions/casting/VariantToStringCastRule.java
b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/functions/casting/VariantToStringCastRule.java
deleted file mode 100644
index 955f6530c0f..00000000000
---
a/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/functions/casting/VariantToStringCastRule.java
+++ /dev/null
@@ -1,121 +0,0 @@
-/*
- * 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.data.StringData;
-import org.apache.flink.table.runtime.functions.VariantCastUtils;
-import org.apache.flink.table.types.logical.LogicalType;
-import org.apache.flink.table.types.logical.LogicalTypeFamily;
-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.codegen.calls.BuiltInMethods.BINARY_STRING_DATA_FROM_STRING;
-import static
org.apache.flink.table.planner.functions.casting.CastRuleUtils.methodCall;
-import static
org.apache.flink.table.planner.functions.casting.CastRuleUtils.staticCall;
-
-/**
- * {@link LogicalTypeRoot#VARIANT} to {@link
LogicalTypeFamily#CHARACTER_STRING} cast rule.
- *
- * <p>Renders the scalar value the way a regular SQL cast of the stored kind
would, so a boolean
- * becomes {@code TRUE}, a timestamp uses the SQL format, and a {@code
TIMESTAMP_LTZ} is shifted
- * into the session time zone and a binary value is read as UTF-8. A variant
holding an object or
- * array has no scalar form, so it renders like a regular {@code MAP} or
{@code ARRAY} to string
- * cast, an array as {@code [e1, e2]} and an object as {@code {k1=v1, k2=v2}},
with strings unquoted
- * at every depth. This is a SQL rendering, not JSON; use {@code JSON_STRING}
for the JSON form.
- *
- * <p>A binary value that is not well-formed UTF-8 fails instead of decoding
to {@code U+FFFD}. Cast
- * it to {@code BYTES} to inspect the raw value, or wrap that in {@code
MAKE_VALID_UTF8} to accept
- * the lenient decode explicitly.
- *
- * <p>A value that does not fit a {@code CHAR(n)} or {@code VARCHAR(n)} target
is trimmed, and a
- * {@code CHAR} target pads a shorter value to its fixed width. The length is
enforced here rather
- * than by {@link CharVarCharTrimPadCastRule}, so that a variant storing a
JSON {@code null} still
- * reaches SQL {@code NULL} and so that trimming counts code points rather
than UTF-16 units.
- *
- * <p>Printing uses {@link VariantCastUtils#toPrintString}, which renders the
same way but never
- * fails.
- */
-class VariantToStringCastRule extends
AbstractExpressionCodeGeneratorCastRule<Variant, StringData> {
-
- static final VariantToStringCastRule INSTANCE = new
VariantToStringCastRule();
-
- private VariantToStringCastRule() {
- super(
- CastRulePredicate.builder()
- .input(LogicalTypeRoot.VARIANT)
- .target(LogicalTypeRoot.CHAR)
- .target(LogicalTypeRoot.VARCHAR)
- .build());
- }
-
- @Override
- public boolean canFail(LogicalType inputLogicalType, LogicalType
targetLogicalType) {
- return true;
- }
-
- /**
- * Treats a variant that stores a JSON {@code null} as a {@code NULL}
input, so it casts to SQL
- * {@code NULL} instead of the text {@code null}. Only applied for a
nullable target: a {@code
- * NOT NULL} result cannot carry {@code NULL}, so a null-valued variant
then fails. Printing
- * skips this, so the formatter shows {@code NULL}.
- */
- @Override
- public CastCodeBlock generateCodeBlock(
- CodeGeneratorCastRule.Context context,
- String inputTerm,
- String inputIsNullTerm,
- LogicalType inputLogicalType,
- LogicalType targetLogicalType) {
- if (!targetLogicalType.isNullable() || context.isPrinting()) {
- return super.generateCodeBlock(
- context, inputTerm, inputIsNullTerm, inputLogicalType,
targetLogicalType);
- }
- final String isNullTerm =
- "(" + inputIsNullTerm + " || " + methodCall(inputTerm,
"isNull") + ")";
- return super.generateCodeBlock(
- context, inputTerm, isNullTerm, inputLogicalType,
targetLogicalType);
- }
-
- @Override
- public String generateExpression(
- CodeGeneratorCastRule.Context context,
- String inputTerm,
- LogicalType inputLogicalType,
- LogicalType targetLogicalType) {
- if (context.isPrinting()) {
- // toPrintString returns a String, so it needs the wrap that
toStringValue applies
- // itself.
- return staticCall(
- BINARY_STRING_DATA_FROM_STRING(),
- staticCall(
- VariantCastUtils.class,
- "toPrintString",
- inputTerm,
- context.getSessionTimeZoneTerm()));
- }
- return staticCall(
- VariantCastUtils.class,
- "toStringValue",
- inputTerm,
- context.getSessionTimeZoneTerm(),
- LogicalTypeChecks.getLength(targetLogicalType),
- targetLogicalType.is(LogicalTypeRoot.CHAR));
- }
-}
diff --git
a/flink-table/flink-table-planner/src/main/scala/org/apache/flink/table/planner/codegen/calls/ScalarOperatorGens.scala
b/flink-table/flink-table-planner/src/main/scala/org/apache/flink/table/planner/codegen/calls/ScalarOperatorGens.scala
index bff1d79dd1c..5fabb0ad3a5 100644
---
a/flink-table/flink-table-planner/src/main/scala/org/apache/flink/table/planner/codegen/calls/ScalarOperatorGens.scala
+++
b/flink-table/flink-table-planner/src/main/scala/org/apache/flink/table/planner/codegen/calls/ScalarOperatorGens.scala
@@ -2147,6 +2147,7 @@ object ScalarOperatorGens {
override def isPrinting(): Boolean = false
override def legacyBehaviour(): Boolean =
isLegacyCastBehaviourEnabled(ctx)
override def getSessionTimeZoneTerm: String =
ctx.addReusableSessionTimeZone()
+ override def getSessionZoneId: ZoneId =
TableConfigUtils.getLocalTimeZone(ctx.tableConfig)
override def declareVariable(ty: String, variablePrefix: String): String
=
ctx.addReusableLocalVariable(ty, variablePrefix)
override def declareTypeSerializer(ty: LogicalType): String =
diff --git
a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/functions/CastFunctionITCase.java
b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/functions/CastFunctionITCase.java
index 714df428b58..e3eba6e9a73 100644
---
a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/functions/CastFunctionITCase.java
+++
b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/functions/CastFunctionITCase.java
@@ -518,6 +518,12 @@ public class CastFunctionITCase extends
BuiltInFunctionTestBase {
"CAST(CAST(f5 AS VARIANT) AS
TIMESTAMP_LTZ(3))",
instant,
TIMESTAMP_LTZ(3))
+ // a TIMESTAMP_LTZ renders as a string in the session
time zone
+ .testResult(
+ $("f5").cast(VARIANT()).cast(STRING()),
+ "CAST(CAST(f5 AS VARIANT) AS STRING)",
+ "2026-09-25 18:15:30.123000",
+ STRING())
.testResult(
$("f6").cast(VARIANT()).cast(UUID()),
"CAST(CAST(f6 AS VARIANT) AS UUID)",
@@ -1052,6 +1058,7 @@ public class CastFunctionITCase extends
BuiltInFunctionTestBase {
final String objNull = "{\"id\": 7, \"name\": null}";
final String nested =
"{\"user\": {\"id\": 1, \"since\": \"2020-01-01\"}, \"tags\":
[\"x\", \"y\"]}";
+ final DataType stringRow = ROW(FIELD("d", STRING()));
return List.of(
TestSetSpec.forExpression("Cast a VARIANT produced by
parseJson() to a ROW")
.onFieldsWithData("null")
@@ -1155,7 +1162,42 @@ public class CastFunctionITCase extends
BuiltInFunctionTestBase {
FIELD(
"user",
ROW(FIELD("id", INT()),
FIELD("since", STRING()))),
- FIELD("tags", ARRAY(STRING())))));
+ FIELD("tags", ARRAY(STRING())))),
+ // A ROW cast from VARIANT compares field by field, like a ROW
built with ROW(...):
+ // NaN never equals NaN, and 0.0 equals -0.0. PARSE_JSON
rejects NaN and
+ // infinities, so each VARIANT is built from a row of strings.
Each pair spells its
+ // value differently, because the planner merges equal inputs
and folds x = x to
+ // TRUE.
+ TestSetSpec.forExpression("Compare ROW values cast from
VARIANT")
+ .onFieldsWithData(
+ Row.of("NaN"),
+ Row.of("nan"),
+ Row.of("0.0"),
+ Row.of("-0.0"),
+ Row.of("Infinity"),
+ Row.of("inf"),
+ Row.of("-Infinity"))
+ .andDataTypes(
+ stringRow, stringRow, stringRow, stringRow,
stringRow, stringRow,
+ stringRow)
+ // NaN = NaN
+ .testSqlResult(variantRowsEqual("f0", "f1"), false,
BOOLEAN())
+ // 0.0 = -0.0
+ .testSqlResult(variantRowsEqual("f2", "f3"), true,
BOOLEAN())
+ // Infinity = Infinity
+ .testSqlResult(variantRowsEqual("f4", "f5"), true,
BOOLEAN())
+ // Infinity = -Infinity
+ .testSqlResult(variantRowsEqual("f4", "f6"), false,
BOOLEAN()));
+ }
+
+ /** Casts two rows of strings to {@code ROW<d DOUBLE>} through a VARIANT
and compares them. */
+ private static String variantRowsEqual(final String left, final String
right) {
+ return variantRow(left) + " = " + variantRow(right);
+ }
+
+ private static String variantRow(final String field) {
+ return String.format(
+ "CAST(CAST(CAST(%s AS ROW<d DOUBLE>) AS VARIANT) AS ROW<d
DOUBLE>)", field);
}
private static List<TestSetSpec> variantMapCasts() {
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 5d9915c16cd..3ce28626217 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
@@ -168,10 +168,9 @@ class CastRuleProviderTest {
@Test
void testResolveVariantToPrimitive() {
- assertThat(CastRuleProvider.resolve(VARIANT, INT))
- .isSameAs(VariantToPrimitiveCastRule.INSTANCE);
+ assertThat(CastRuleProvider.resolve(VARIANT,
INT)).isSameAs(FromVariantCastRule.INSTANCE);
assertThat(CastRuleProvider.resolve(VARIANT,
BOOLEAN().getLogicalType()))
- .isSameAs(VariantToPrimitiveCastRule.INSTANCE);
+ .isSameAs(FromVariantCastRule.INSTANCE);
assertThat(CastRuleProvider.exists(VARIANT, DECIMAL(10,
2).getLogicalType())).isTrue();
assertThat(CastRuleProvider.exists(VARIANT,
DATE().getLogicalType())).isTrue();
assertThat(CastRuleProvider.exists(VARIANT,
TIMESTAMP().getLogicalType())).isTrue();
@@ -184,9 +183,11 @@ class CastRuleProviderTest {
// 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
+ // character strings go through the same rule, including a bounded
length
assertThat(CastRuleProvider.resolve(VARIANT, STRING_TYPE))
- .isSameAs(VariantToStringCastRule.INSTANCE);
+ .isSameAs(FromVariantCastRule.INSTANCE);
+ assertThat(CastRuleProvider.resolve(VARIANT, CHAR(3).getLogicalType()))
+ .isSameAs(FromVariantCastRule.INSTANCE);
}
@Test
@@ -317,7 +318,7 @@ class CastRuleProviderTest {
@Test
void testResolveVariantToArray() {
assertThat(CastRuleProvider.resolve(VARIANT,
ARRAY(INT()).getLogicalType()))
- .isSameAs(VariantToArrayCastRule.INSTANCE);
+ .isSameAs(FromVariantCastRule.INSTANCE);
// the element recurses through the VARIANT rules, including the
identity leaf and nesting
assertThat(CastRuleProvider.exists(VARIANT,
ARRAY(VARIANT()).getLogicalType())).isTrue();
@@ -334,10 +335,10 @@ class CastRuleProviderTest {
@Test
void testResolveVariantToRow() {
assertThat(CastRuleProvider.resolve(VARIANT, ROW(FIELD("f0",
INT())).getLogicalType()))
- .isSameAs(VariantToRowCastRule.INSTANCE);
+ .isSameAs(FromVariantCastRule.INSTANCE);
// a structured target shares the ROW rule
assertThat(CastRuleProvider.resolve(VARIANT, STRUCTURED))
- .isSameAs(VariantToRowCastRule.INSTANCE);
+ .isSameAs(FromVariantCastRule.INSTANCE);
// a field with no variant counterpart makes the whole cast
unresolvable
assertThat(
@@ -349,7 +350,7 @@ class CastRuleProviderTest {
@Test
void testResolveVariantToMap() {
assertThat(CastRuleProvider.resolve(VARIANT, MAP(STRING(),
INT()).getLogicalType()))
- .isSameAs(VariantToMapCastRule.INSTANCE);
+ .isSameAs(FromVariantCastRule.INSTANCE);
// the value recurses through the VARIANT rules, including the
identity leaf
assertThat(CastRuleProvider.exists(VARIANT, MAP(STRING(),
VARIANT()).getLogicalType()))
.isTrue();
diff --git
a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/utils/FilterUtils.java
b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/utils/FilterUtils.java
index 6965d55cef7..b2087b32de8 100644
---
a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/utils/FilterUtils.java
+++
b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/utils/FilterUtils.java
@@ -19,6 +19,8 @@
package org.apache.flink.table.planner.utils;
import org.apache.flink.table.api.TableException;
+import org.apache.flink.table.data.StringData;
+import org.apache.flink.table.data.utils.FromVariantConverter;
import org.apache.flink.table.expressions.CallExpression;
import org.apache.flink.table.expressions.Expression;
import org.apache.flink.table.expressions.FieldReferenceExpression;
@@ -27,26 +29,22 @@ import
org.apache.flink.table.expressions.ResolvedExpression;
import org.apache.flink.table.expressions.ValueLiteralExpression;
import org.apache.flink.table.functions.BuiltInFunctionDefinitions;
import org.apache.flink.table.functions.FunctionDefinition;
-import org.apache.flink.table.runtime.functions.VariantCastUtils;
-import org.apache.flink.table.types.logical.CharType;
import org.apache.flink.table.types.logical.LogicalType;
import org.apache.flink.table.types.logical.LogicalTypeFamily;
import org.apache.flink.table.types.logical.LogicalTypeRoot;
import org.apache.flink.table.types.logical.MapType;
-import org.apache.flink.table.types.logical.VarCharType;
import org.apache.flink.types.variant.Variant;
import org.apache.flink.util.Preconditions;
import javax.annotation.Nullable;
import java.lang.reflect.Array;
-import java.util.EnumMap;
+import java.time.ZoneOffset;
+import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
-import java.util.TimeZone;
-import java.util.function.BiFunction;
import java.util.function.Function;
import static org.apache.flink.table.functions.BuiltInFunctionDefinitions.AT;
@@ -58,57 +56,18 @@ import static
org.apache.flink.table.functions.BuiltInFunctionDefinitions.UPPER;
/** Utils for catalog and source to filter partition or row. */
public class FilterUtils {
- private static final TimeZone UTC = TimeZone.getTimeZone("UTC");
-
- /**
- * Casts of a VARIANT element that can be evaluated, by target type, using
the same {@link
- * VariantCastUtils} helpers as the generated code of {@code
VariantToPrimitiveCastRule}.
- */
- private static final Map<LogicalTypeRoot, BiFunction<Variant, LogicalType,
Object>>
- VARIANT_CASTS = new EnumMap<>(LogicalTypeRoot.class);
-
- static {
- VARIANT_CASTS.put(LogicalTypeRoot.BOOLEAN, (variant, type) ->
variant.getBoolean());
- VARIANT_CASTS.put(
- LogicalTypeRoot.TINYINT,
- (variant, type) ->
- (byte)
- VariantCastUtils.toIntegral(
- variant, Byte.MIN_VALUE,
Byte.MAX_VALUE, "TINYINT"));
- VARIANT_CASTS.put(
- LogicalTypeRoot.SMALLINT,
- (variant, type) ->
- (short)
- VariantCastUtils.toIntegral(
- variant, Short.MIN_VALUE,
Short.MAX_VALUE, "SMALLINT"));
- VARIANT_CASTS.put(
- LogicalTypeRoot.INTEGER,
- (variant, type) ->
- (int)
- VariantCastUtils.toIntegral(
- variant, Integer.MIN_VALUE,
Integer.MAX_VALUE, "INT"));
- VARIANT_CASTS.put(
- LogicalTypeRoot.BIGINT,
- (variant, type) ->
- VariantCastUtils.toIntegral(
- variant, Long.MIN_VALUE, Long.MAX_VALUE,
"BIGINT"));
- VARIANT_CASTS.put(
- LogicalTypeRoot.FLOAT, (variant, type) ->
VariantCastUtils.toFloat(variant));
- VARIANT_CASTS.put(
- LogicalTypeRoot.DOUBLE, (variant, type) ->
VariantCastUtils.toDouble(variant));
- VARIANT_CASTS.put(
- LogicalTypeRoot.CHAR,
- (variant, type) ->
- VariantCastUtils.toStringValue(
- variant, UTC, ((CharType)
type).getLength(), true)
- .toString());
- VARIANT_CASTS.put(
- LogicalTypeRoot.VARCHAR,
- (variant, type) ->
- VariantCastUtils.toStringValue(
- variant, UTC, ((VarCharType)
type).getLength(), false)
- .toString());
- }
+ /** Cast targets of a VARIANT element whose values compare with a filter
literal. */
+ private static final Set<LogicalTypeRoot> VARIANT_CAST_TARGETS =
+ EnumSet.of(
+ LogicalTypeRoot.BOOLEAN,
+ LogicalTypeRoot.TINYINT,
+ LogicalTypeRoot.SMALLINT,
+ LogicalTypeRoot.INTEGER,
+ LogicalTypeRoot.BIGINT,
+ LogicalTypeRoot.FLOAT,
+ LogicalTypeRoot.DOUBLE,
+ LogicalTypeRoot.CHAR,
+ LogicalTypeRoot.VARCHAR);
public static boolean shouldPushDown(ResolvedExpression expr, Set<String>
filterableFields) {
if (expr instanceof CallExpression && expr.getChildren().size() == 2) {
@@ -190,7 +149,7 @@ public class FilterUtils {
// A cast makes an element of a VARIANT comparable, e.g. CAST(v['k']
AS INT).
if (isCall(expr, CAST) || isCall(expr, TRY_CAST)) {
final ResolvedExpression element =
expr.getResolvedChildren().get(0);
- return VARIANT_CASTS.containsKey(
+ return VARIANT_CAST_TARGETS.contains(
expr.getOutputDataType().getLogicalType().getTypeRoot())
&&
element.getOutputDataType().getLogicalType().is(LogicalTypeRoot.VARIANT)
&& isElementAccess(element, filterableFields);
@@ -297,7 +256,10 @@ public class FilterUtils {
return null;
}
try {
- return VARIANT_CASTS.get(targetType.getTypeRoot()).apply(variant,
targetType);
+ final Object value =
+ FromVariantConverter.create(targetType,
ZoneOffset.UTC).convert(variant);
+ // A string filter literal is a String.
+ return value instanceof StringData ? value.toString() : value;
} catch (RuntimeException e) {
if (tryCast) {
return null;