This is an automated email from the ASF dual-hosted git repository.
snuyanzin 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 5d3597c253f [FLINK-37926][table] Support casting from `VARIANT` to
`ARRAY`
5d3597c253f is described below
commit 5d3597c253fcb6383ca74df9c43257c283dc821e
Author: Ramin Gharib <[email protected]>
AuthorDate: Fri Sep 4 14:35:59 2026 +0200
[FLINK-37926][table] Support casting from `VARIANT` to `ARRAY`
---
docs/content.zh/docs/sql/reference/data-types.md | 28 +-
docs/content/docs/sql/reference/data-types.md | 28 +-
.../types/logical/utils/LogicalTypeCasts.java | 8 +
.../flink/table/types/LogicalTypeCastsTest.java | 27 +-
.../AbstractVariantToConstructedCastRule.java | 71 ++++
.../functions/casting/CastRuleProvider.java | 1 +
.../functions/casting/VariantToArrayCastRule.java | 171 +++++++++
.../planner/functions/CastFunctionITCase.java | 121 ++++++-
.../functions/casting/CastRuleProviderTest.java | 21 ++
.../planner/functions/casting/CastRulesTest.java | 384 ++++++++++++++-------
.../table/runtime/functions/VariantCastUtils.java | 21 ++
11 files changed, 741 insertions(+), 140 deletions(-)
diff --git a/docs/content.zh/docs/sql/reference/data-types.md
b/docs/content.zh/docs/sql/reference/data-types.md
index 62266a004eb..b1e938567f2 100644
--- a/docs/content.zh/docs/sql/reference/data-types.md
+++ b/docs/content.zh/docs/sql/reference/data-types.md
@@ -1588,6 +1588,32 @@ rendering. Use `JSON_STRING` for the JSON representation
instead, where a string
`"foo"` and an object or array is serialized. A variant that stores a JSON
`null` casts to SQL
`NULL`.
+A `VARIANT` can also be cast to a constructed target, which imposes a schema
on it. A variant array
+casts to `ARRAY<T>`. The variant must be an array, otherwise the cast fails.
Each element is itself a
+`VARIANT`, so it casts to the element type `T` by the same rules, recursively.
A leaf is never parsed
+either, so a stored string does not reach an integer target. Cast the leaf to
`STRING` first and
+convert with a regular cast.
+
+- Each element casts to `T`. A variant null element maps to SQL `NULL` when
`T` is nullable and fails
+ the cast when `T` is `NOT NULL`. An empty array casts to an empty `ARRAY<T>`.
+- `ARRAY<VARIANT>` is the identity element: it shreds one level and keeps each
element as a variant. A
+ null element stays a variant null rather than becoming SQL `NULL`.
+
+If any element cast fails, the whole cast fails, and `TRY_CAST` returns `NULL`
for the entire value
+rather than a partial result. An element type with no variant counterpart,
such as
+`ARRAY<INTERVAL YEAR TO MONTH>`, is rejected at validation.
+
+The following examples use `a` for `PARSE_JSON('[1, 2, 3]')` and `m` for the
mixed array
+`PARSE_JSON('[1, "a"]')`:
+
+```sql
+CAST(a AS ARRAY<INT>) -- [1, 2, 3]
+CAST(a AS ARRAY<STRING>) -- ['1', '2', '3'], each element rendered like the
scalar cast
+CAST(m AS ARRAY<INT>) -- fails on "a", a stored string is not parsed into
an integer
+CAST(m AS ARRAY<STRING>) -- ['1', 'a'], a heterogeneous array still renders
each element
+CAST(m AS ARRAY<VARIANT>) -- [1, "a"] as variants, one level shredded
+```
+
**Declaration**
{{< tabs "25c30432-8460-441d-a036-9416d8202882" >}}
@@ -1802,7 +1828,7 @@ COALESCE(TRY_CAST('non-number' AS INT), 0) --- 结果返回数字 0 的
INT 格
| `ROW` | Y
| N | N | N | N
| N | N | N | N | N | N | N |
N | N | N | N | N | N | !³
| N | N | N | N |
| `STRUCTURED` | Y
| N | N | N | N
| N | N | N | N | N | N | N |
N | N | N | N | N | N | N
| !³ | N | N | N |
| `RAW` | Y
| ! | N | N | N
| N | N | N | N | N | N | N |
N | N | N | N | N | N | N
| N | Y⁴ | N | N |
-| `VARIANT` | N
| ! | ! | ! | !
| ! | ! | ! | ! | ! | ! | N |
! | ! | N | N | N | N | N
| N | N | Y | N |
+| `VARIANT` | N
| ! | ! | ! | !
| ! | ! | ! | ! | ! | ! | N |
! | ! | N | !³ | N | N | N
| N | N | Y | N |
| `BITMAP` | Y
| Y⁷ | N | N | N
| N | N | N | N | N | N | N |
N | N | N | N | N | N | N
| N | N | N | N |
备注:
diff --git a/docs/content/docs/sql/reference/data-types.md
b/docs/content/docs/sql/reference/data-types.md
index 207c83d2792..442536c212e 100644
--- a/docs/content/docs/sql/reference/data-types.md
+++ b/docs/content/docs/sql/reference/data-types.md
@@ -1596,6 +1596,32 @@ rendering. Use `JSON_STRING` for the JSON representation
instead, where a string
`"foo"` and an object or array is serialized. A variant that stores a JSON
`null` casts to SQL
`NULL`.
+A `VARIANT` can also be cast to a constructed target, which imposes a schema
on it. A variant array
+casts to `ARRAY<T>`. The variant must be an array, otherwise the cast fails.
Each element is itself a
+`VARIANT`, so it casts to the element type `T` by the same rules, recursively.
A leaf is never parsed
+either, so a stored string does not reach an integer target. Cast the leaf to
`STRING` first and
+convert with a regular cast.
+
+- Each element casts to `T`. A variant null element maps to SQL `NULL` when
`T` is nullable and fails
+ the cast when `T` is `NOT NULL`. An empty array casts to an empty `ARRAY<T>`.
+- `ARRAY<VARIANT>` is the identity element: it shreds one level and keeps each
element as a variant. A
+ null element stays a variant null rather than becoming SQL `NULL`.
+
+If any element cast fails, the whole cast fails, and `TRY_CAST` returns `NULL`
for the entire value
+rather than a partial result. An element type with no variant counterpart,
such as
+`ARRAY<INTERVAL YEAR TO MONTH>`, is rejected at validation.
+
+The following examples use `a` for `PARSE_JSON('[1, 2, 3]')` and `m` for the
mixed array
+`PARSE_JSON('[1, "a"]')`:
+
+```sql
+CAST(a AS ARRAY<INT>) -- [1, 2, 3]
+CAST(a AS ARRAY<STRING>) -- ['1', '2', '3'], each element rendered like the
scalar cast
+CAST(m AS ARRAY<INT>) -- fails on "a", a stored string is not parsed into
an integer
+CAST(m AS ARRAY<STRING>) -- ['1', 'a'], a heterogeneous array still renders
each element
+CAST(m AS ARRAY<VARIANT>) -- [1, "a"] as variants, one level shredded
+```
+
**Declaration**
{{< tabs "25c30432-8460-441d-a036-9416d8202882" >}}
@@ -1811,7 +1837,7 @@ The matrix below describes the supported cast pairs,
where "Y" means supported,
| `ROW` | Y
| N | N | N | N
| N | N | N | N | N | N | N |
N | N | N | N | N | N | !³
| N | N | N | N |
| `STRUCTURED` | Y
| N | N | N | N
| N | N | N | N | N | N | N |
N | N | N | N | N | N | N
| !³ | N | N | N |
| `RAW` | Y
| ! | N | N | N
| N | N | N | N | N | N | N |
N | N | N | N | N | N | N
| N | Y⁴ | N | N |
-| `VARIANT` | N
| ! | ! | ! | !
| ! | ! | ! | ! | ! | ! | N |
! | ! | N | N | N | N | N
| N | N | Y | N |
+| `VARIANT` | N
| ! | ! | ! | !
| ! | ! | ! | ! | ! | ! | N |
! | ! | N | !³ | N | N | N
| N | N | Y | N |
| `BITMAP` | Y
| Y⁷ | N | N | N
| N | N | N | N | N | N | N |
N | N | N | N | N | N | N
| N | N | N | N |
Notes:
diff --git
a/flink-table/flink-table-common/src/main/java/org/apache/flink/table/types/logical/utils/LogicalTypeCasts.java
b/flink-table/flink-table-common/src/main/java/org/apache/flink/table/types/logical/utils/LogicalTypeCasts.java
index d22b16399c3..2ab51ba1098 100644
---
a/flink-table/flink-table-common/src/main/java/org/apache/flink/table/types/logical/utils/LogicalTypeCasts.java
+++
b/flink-table/flink-table-common/src/main/java/org/apache/flink/table/types/logical/utils/LogicalTypeCasts.java
@@ -19,6 +19,7 @@
package org.apache.flink.table.types.logical.utils;
import org.apache.flink.annotation.Internal;
+import org.apache.flink.table.types.logical.ArrayType;
import org.apache.flink.table.types.logical.DateType;
import org.apache.flink.table.types.logical.DayTimeIntervalType;
import org.apache.flink.table.types.logical.DistinctType;
@@ -53,6 +54,7 @@ import static
org.apache.flink.table.types.logical.LogicalTypeFamily.NUMERIC;
import static
org.apache.flink.table.types.logical.LogicalTypeFamily.PREDEFINED;
import static org.apache.flink.table.types.logical.LogicalTypeFamily.TIME;
import static org.apache.flink.table.types.logical.LogicalTypeFamily.TIMESTAMP;
+import static org.apache.flink.table.types.logical.LogicalTypeRoot.ARRAY;
import static org.apache.flink.table.types.logical.LogicalTypeRoot.BIGINT;
import static org.apache.flink.table.types.logical.LogicalTypeRoot.BINARY;
import static org.apache.flink.table.types.logical.LogicalTypeRoot.BITMAP;
@@ -666,6 +668,12 @@ public final class LogicalTypeCasts {
return supportsStructuredCasting(
sourceType, targetType, (s, t) -> supportsCasting(s, t,
allowExplicit));
+ } else if (sourceRoot == VARIANT && targetRoot == ARRAY) {
+ // A variant array casts to ARRAY<T> when VARIANT casts to the
single element type T.
+ // Explicit only, so no accidental coercion. Each runtime element
is cast to T by the
+ // array cast rule; a per-element mismatch fails there, not here.
+ return allowExplicit
+ && supportsCasting(sourceType, ((ArrayType)
targetType).getElementType(), true);
} else if (sourceRoot == RAW
&& !targetType.is(BINARY_STRING)
&& !targetType.is(CHARACTER_STRING)
diff --git
a/flink-table/flink-table-common/src/test/java/org/apache/flink/table/types/LogicalTypeCastsTest.java
b/flink-table/flink-table-common/src/test/java/org/apache/flink/table/types/LogicalTypeCastsTest.java
index c51408edd81..07b0f920911 100644
---
a/flink-table/flink-table-common/src/test/java/org/apache/flink/table/types/LogicalTypeCastsTest.java
+++
b/flink-table/flink-table-common/src/test/java/org/apache/flink/table/types/LogicalTypeCastsTest.java
@@ -34,7 +34,7 @@ import org.apache.flink.table.types.logical.FloatType;
import org.apache.flink.table.types.logical.IntType;
import org.apache.flink.table.types.logical.LocalZonedTimestampType;
import org.apache.flink.table.types.logical.LogicalType;
-import org.apache.flink.table.types.logical.MapType;
+import org.apache.flink.table.types.logical.MultisetType;
import org.apache.flink.table.types.logical.NullType;
import org.apache.flink.table.types.logical.RawType;
import org.apache.flink.table.types.logical.RowType;
@@ -59,7 +59,6 @@ import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import java.util.Arrays;
-import java.util.List;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
@@ -289,13 +288,29 @@ class LogicalTypeCastsTest {
Arguments.of(new VariantType(), VarCharType.STRING_TYPE,
false, true),
// variant identity cast is implicit
Arguments.of(new VariantType(), new VariantType(), true, true),
- // TIME and constructed targets are not castable from variant
+ // TIME has no variant counterpart, so it is not castable from
variant
Arguments.of(new VariantType(), new TimeType(), false, false),
- Arguments.of(new VariantType(), new ArrayType(new IntType()),
false, false),
- Arguments.of(new VariantType(), new RowType(List.of()), false,
false),
+ // A variant imposes a schema on a constructed target,
explicit only, recursing on
+ // every leaf, which is itself a VARIANT cast
+ Arguments.of(new VariantType(), new ArrayType(new IntType()),
false, true),
+ Arguments.of(new VariantType(), new ArrayType(new
VariantType()), false, true),
Arguments.of(
new VariantType(),
- new MapType(new IntType(), new CharType()),
+ new ArrayType(new ArrayType(new IntType())),
+ false,
+ true),
+ // A leaf with no variant counterpart makes the whole
constructed cast unsupported
+ Arguments.of(
+ new VariantType(),
+ new ArrayType(
+ new YearMonthIntervalType(
+
YearMonthIntervalType.YearMonthResolution.MONTH)),
+ false,
+ false),
+ // MULTISET has no variant counterpart and stays unsupported
+ Arguments.of(
+ new VariantType(),
+ new MultisetType(VarCharType.STRING_TYPE),
false,
false));
}
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
new file mode 100644
index 00000000000..f003b129ec2
--- /dev/null
+++
b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/functions/casting/AbstractVariantToConstructedCastRule.java
@@ -0,0 +1,71 @@
+/*
+ * 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, or a missing
+ * {@code NOT NULL} field, 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 133542ec6f5..2b2cfc0e056 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
@@ -98,6 +98,7 @@ public class CastRuleProvider {
// Variant rules
.addRule(VariantToStringCastRule.INSTANCE)
.addRule(VariantToPrimitiveCastRule.INSTANCE)
+ .addRule(VariantToArrayCastRule.INSTANCE)
// Bitmap rules
.addRule(BitmapToStringCastRule.INSTANCE)
.addRule(BitmapToBinaryCastRule.INSTANCE)
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
new file mode 100644
index 00000000000..5761cb55044
--- /dev/null
+++
b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/functions/casting/VariantToArrayCastRule.java
@@ -0,0 +1,171 @@
+/*
+ * 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/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 aee2d70c1cf..083fda47dff 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
@@ -76,6 +76,7 @@ import static
org.apache.flink.table.api.DataTypes.TIMESTAMP_LTZ;
import static org.apache.flink.table.api.DataTypes.TINYINT;
import static org.apache.flink.table.api.DataTypes.VARBINARY;
import static org.apache.flink.table.api.DataTypes.VARCHAR;
+import static org.apache.flink.table.api.DataTypes.VARIANT;
import static org.apache.flink.table.api.DataTypes.YEAR;
import static org.apache.flink.table.api.Expressions.$;
import static org.apache.flink.table.api.Expressions.call;
@@ -138,9 +139,13 @@ public class CastFunctionITCase extends
BuiltInFunctionTestBase {
}
private static List<TestSetSpec> variantCasts() {
- // A variant is produced with PARSE_JSON since there is no VARIANT
literal. Numeric casts
- // succeed only when the value is preserved exactly, otherwise CAST
fails and TRY_CAST
- // returns NULL.
+ final List<TestSetSpec> specs = new ArrayList<>();
+ specs.addAll(variantPrimitiveCasts());
+ specs.addAll(variantArrayCasts());
+ return specs;
+ }
+
+ private static List<TestSetSpec> variantPrimitiveCasts() {
return List.of(
TestSetSpec.forExpression("Cast a VARIANT produced by
PARSE_JSON to a primitive")
.onFieldsWithData("unused")
@@ -379,6 +384,116 @@ public class CastFunctionITCase extends
BuiltInFunctionTestBase {
TINYINT()));
}
+ private static List<TestSetSpec> variantArrayCasts() {
+ return List.of(
+ TestSetSpec.forExpression("Cast a VARIANT produced by
PARSE_JSON to an ARRAY")
+ .onFieldsWithData("unused")
+ .andDataTypes(STRING())
+ // ARRAY: each element casts by the same
VARIANT-to-element rule.
+ .testResult(
+ call("PARSE_JSON", "[1, 2,
3]").cast(ARRAY(INT())),
+ "CAST(PARSE_JSON('[1, 2, 3]') AS ARRAY<INT>)",
+ new Integer[] {1, 2, 3},
+ ARRAY(INT()).notNull())
+ // an approximate leaf takes any numeric kind
+ .testResult(
+ call("PARSE_JSON", "[1, 2,
3]").cast(ARRAY(DOUBLE())),
+ "CAST(PARSE_JSON('[1, 2, 3]') AS
ARRAY<DOUBLE>)",
+ new Double[] {1.0, 2.0, 3.0},
+ ARRAY(DOUBLE()).notNull())
+ // each element renders to string like the scalar cast
+ .testResult(
+ call("PARSE_JSON", "[1, 2,
3]").cast(ARRAY(STRING())),
+ "CAST(PARSE_JSON('[1, 2, 3]') AS
ARRAY<STRING>)",
+ new String[] {"1", "2", "3"},
+ ARRAY(STRING()).notNull())
+ // a heterogeneous array renders every element to
string
+ .testResult(
+ call("PARSE_JSON", "[1, \"a\", 2,
\"b\"]").cast(ARRAY(STRING())),
+ "CAST(PARSE_JSON('[1, \"a\", 2, \"b\"]') AS
ARRAY<STRING>)",
+ new String[] {"1", "a", "2", "b"},
+ ARRAY(STRING()).notNull())
+ .testResult(
+ call("PARSE_JSON", "[]").cast(ARRAY(INT())),
+ "CAST(PARSE_JSON('[]') AS ARRAY<INT>)",
+ new Integer[] {},
+ ARRAY(INT()).notNull())
+ // a VARIANT null element maps to SQL NULL for a
nullable element type
+ .testResult(
+ call("PARSE_JSON", "[1, null,
3]").cast(ARRAY(INT())),
+ "CAST(PARSE_JSON('[1, null, 3]') AS
ARRAY<INT>)",
+ new Integer[] {1, null, 3},
+ ARRAY(INT()).notNull())
+ // a VARIANT null element fails a NOT NULL element type
+ .testTableApiRuntimeError(
+ call("PARSE_JSON", "[1, null,
3]").cast(ARRAY(INT().notNull())),
+ "NOT NULL element type")
+ .testSqlRuntimeError(
+ "CAST(PARSE_JSON('[1, null, 3]') AS ARRAY<INT
NOT NULL>)",
+ "NOT NULL element type")
+ // a stored string is never parsed into an integer
+ .testTableApiRuntimeError(
+ call("PARSE_JSON", "[\"1\",
\"2\"]").cast(ARRAY(INT())),
+ "does not change the type")
+ .testSqlRuntimeError(
+ "CAST(PARSE_JSON('[\"1\", \"2\"]') AS
ARRAY<INT>)",
+ "does not change the type")
+ .testResult(
+ call("PARSE_JSON", "[\"1\",
\"2\"]").tryCast(ARRAY(INT())),
+ "TRY_CAST(PARSE_JSON('[\"1\", \"2\"]') AS
ARRAY<INT>)",
+ null,
+ ARRAY(INT()))
+ // a heterogeneous array fails on the first element
that is not an integer
+ .testTableApiRuntimeError(
+ call("PARSE_JSON", "[1, \"a\", 2,
\"b\"]").cast(ARRAY(INT())),
+ "does not change the type")
+ // a fractional element cannot narrow to INT without
dropping digits
+ .testTableApiRuntimeError(
+ call("PARSE_JSON", "[1.23, 2.45,
3.67]").cast(ARRAY(INT())),
+ "lose precision")
+ .testSqlRuntimeError(
+ "CAST(PARSE_JSON('[1.23, 2.45, 3.67]') AS
ARRAY<INT>)",
+ "lose precision")
+ // TRY_CAST swallows the failure and returns NULL for
the whole array
+ .testResult(
+ call("PARSE_JSON", "[1.23, 2.45,
3.67]").tryCast(ARRAY(INT())),
+ "TRY_CAST(PARSE_JSON('[1.23, 2.45, 3.67]') AS
ARRAY<INT>)",
+ null,
+ ARRAY(INT()))
+ // an object is not an array
+ .testTableApiRuntimeError(
+ call(
+ "PARSE_JSON",
+ "{\"id\": 7, \"name\":
\"ada\", \"active\": true}")
+ .cast(ARRAY(INT())),
+ "requires an array")
+ // ARRAY<VARIANT> shreds one level and keeps the
elements as variants, which
+ // then cast back to INT unchanged
+ .testResult(
+ call("PARSE_JSON", "[1, 2, 3]")
+ .cast(ARRAY(VARIANT()))
+ .cast(ARRAY(INT())),
+ "CAST(CAST(PARSE_JSON('[1, 2, 3]') AS
ARRAY<VARIANT>) AS ARRAY<INT>)",
+ new Integer[] {1, 2, 3},
+ ARRAY(INT()).notNull())
+ // the recursion composes for a nested array of arrays
+ .testResult(
+ call("PARSE_JSON", "[[1, 2],
[3]]").cast(ARRAY(ARRAY(INT()))),
+ "CAST(PARSE_JSON('[[1, 2], [3]]') AS
ARRAY<ARRAY<INT>>)",
+ new Integer[][] {{1, 2}, {3}},
+ ARRAY(ARRAY(INT())).notNull())
+ // a top-level VARIANT null casts to SQL NULL for a
nullable target
+ .testResult(
+ call("TRY_PARSE_JSON",
"null").cast(ARRAY(INT())),
+ "CAST(TRY_PARSE_JSON('null') AS ARRAY<INT>)",
+ null,
+ ARRAY(INT()))
+ // an element with no variant counterpart is rejected
at validation
+ .testTableApiValidationError(
+ call("PARSE_JSON",
"[1]").cast(ARRAY(INTERVAL(MONTH()))),
+ "Unsupported cast"));
+ }
+
private static List<TestSetSpec> allTypesBasic() {
return Arrays.asList(
CastTestSpecBuilder.testCastTo(BOOLEAN())
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 adc44c474be..fabe82749f4 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
@@ -27,6 +27,7 @@ import org.apache.flink.table.types.logical.VarCharType;
import org.junit.jupiter.api.Test;
+import static org.apache.flink.table.api.DataTypes.ARRAY;
import static org.apache.flink.table.api.DataTypes.BIGINT;
import static org.apache.flink.table.api.DataTypes.BOOLEAN;
import static org.apache.flink.table.api.DataTypes.BYTES;
@@ -34,6 +35,9 @@ import static org.apache.flink.table.api.DataTypes.DATE;
import static org.apache.flink.table.api.DataTypes.DECIMAL;
import static org.apache.flink.table.api.DataTypes.FIELD;
import static org.apache.flink.table.api.DataTypes.INT;
+import static org.apache.flink.table.api.DataTypes.INTERVAL;
+import static org.apache.flink.table.api.DataTypes.MONTH;
+import static org.apache.flink.table.api.DataTypes.MULTISET;
import static org.apache.flink.table.api.DataTypes.ROW;
import static org.apache.flink.table.api.DataTypes.STRING;
import static org.apache.flink.table.api.DataTypes.STRUCTURED;
@@ -136,4 +140,21 @@ class CastRuleProviderTest {
assertThat(CastRuleProvider.resolve(VARIANT, STRING_TYPE))
.isSameAs(VariantToStringCastRule.INSTANCE);
}
+
+ @Test
+ void testResolveVariantToArray() {
+ assertThat(CastRuleProvider.resolve(VARIANT,
ARRAY(INT()).getLogicalType()))
+ .isSameAs(VariantToArrayCastRule.INSTANCE);
+
+ // the element recurses through the VARIANT rules, including the
identity leaf and nesting
+ assertThat(CastRuleProvider.exists(VARIANT,
ARRAY(VARIANT()).getLogicalType())).isTrue();
+ assertThat(CastRuleProvider.exists(VARIANT,
ARRAY(ARRAY(INT())).getLogicalType())).isTrue();
+ assertThat(CastRuleProvider.canFail(VARIANT,
ARRAY(INT()).getLogicalType())).isTrue();
+
+ // an element with no variant counterpart makes the whole cast
unresolvable
+ assertThat(CastRuleProvider.exists(VARIANT,
ARRAY(INTERVAL(MONTH())).getLogicalType()))
+ .isFalse();
+ // MULTISET has no variant counterpart
+ assertThat(CastRuleProvider.exists(VARIANT,
MULTISET(STRING()).getLogicalType())).isFalse();
+ }
}
diff --git
a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/functions/casting/CastRulesTest.java
b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/functions/casting/CastRulesTest.java
index e00c99c55ce..99d4b5aba14 100644
---
a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/functions/casting/CastRulesTest.java
+++
b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/functions/casting/CastRulesTest.java
@@ -42,6 +42,7 @@ import org.apache.flink.table.types.logical.StructuredType;
import org.apache.flink.table.utils.DateTimeUtils;
import org.apache.flink.types.bitmap.Bitmap;
import org.apache.flink.types.variant.Variant;
+import org.apache.flink.types.variant.VariantBuilder;
import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.TestFactory;
@@ -167,27 +168,83 @@ class CastRulesTest {
/** U+1D54F, one code point but two UTF-16 units and four UTF-8 bytes. */
private static final String NON_BMP = "𝕏";
+ private static final VariantBuilder VARIANT_BUILDER = Variant.newBuilder();
private static final Variant VARIANT_ARRAY =
- Variant.newBuilder()
+ VARIANT_BUILDER
.array()
- .add(Variant.newBuilder().of(1))
- .add(Variant.newBuilder().of("two"))
- .add(Variant.newBuilder().of(false))
- .add(Variant.newBuilder().ofNull())
+ .add(VARIANT_BUILDER.of(1))
+ .add(VARIANT_BUILDER.of("two"))
+ .add(VARIANT_BUILDER.of(false))
+ .add(VARIANT_BUILDER.ofNull())
.build();
private static final Variant VARIANT_OBJECT =
- Variant.newBuilder()
+ VARIANT_BUILDER
.object()
.add(
"k",
- Variant.newBuilder()
+ VARIANT_BUILDER
.array()
- .add(Variant.newBuilder().of(1))
- .add(Variant.newBuilder().of(2))
+ .add(VARIANT_BUILDER.of(1))
+ .add(VARIANT_BUILDER.of(2))
.build())
.build();
+ /** {@code [1, 2, 3]}, the design's running array value. */
+ private static final Variant VARIANT_INT_ARRAY =
+ VARIANT_BUILDER
+ .array()
+ .add(VARIANT_BUILDER.of(1))
+ .add(VARIANT_BUILDER.of(2))
+ .add(VARIANT_BUILDER.of(3))
+ .build();
+
+ /** {@code [1, null, 3]}, an array carrying a VARIANT null element. */
+ private static final Variant VARIANT_INT_ARRAY_WITH_NULL =
+ VARIANT_BUILDER
+ .array()
+ .add(VARIANT_BUILDER.of(1))
+ .add(VARIANT_BUILDER.ofNull())
+ .add(VARIANT_BUILDER.of(3))
+ .build();
+
+ /**
+ * {@code ["1", "22", "333"]}, stored strings of different lengths a
numeric leaf must not
+ * parse.
+ */
+ private static final Variant VARIANT_STRING_ARRAY =
+ VARIANT_BUILDER
+ .array()
+ .add(VARIANT_BUILDER.of("1"))
+ .add(VARIANT_BUILDER.of("22"))
+ .add(VARIANT_BUILDER.of("333"))
+ .build();
+
+ /** {@code [1, "a", 2, "b"]}, a heterogeneous array of integers and
strings. */
+ private static final Variant VARIANT_MIXED_ARRAY =
+ VARIANT_BUILDER
+ .array()
+ .add(VARIANT_BUILDER.of(1))
+ .add(VARIANT_BUILDER.of("a"))
+ .add(VARIANT_BUILDER.of(2))
+ .add(VARIANT_BUILDER.of("b"))
+ .build();
+
+ /** {@code [[1, 2], [3]]}, a nested array of arrays. */
+ private static final Variant VARIANT_NESTED_ARRAY =
+ VARIANT_BUILDER
+ .array()
+ .add(
+ VARIANT_BUILDER
+ .array()
+ .add(VARIANT_BUILDER.of(1))
+ .add(VARIANT_BUILDER.of(2))
+ .build())
+
.add(VARIANT_BUILDER.array().add(VARIANT_BUILDER.of(3)).build())
+ .build();
+
+ private static final Variant VARIANT_EMPTY_ARRAY =
VARIANT_BUILDER.array().build();
+
private static final DataType MY_STRUCTURED_TYPE =
STRUCTURED(
MyStructuredType.class,
@@ -1576,47 +1633,47 @@ class CastRulesTest {
// A character string renders like a regular cast of the
stored kind, so these
// expectations reuse the constants of the native cases above.
CastTestSpecBuilder.testCastTo(STRING())
- .fromCase(VARIANT(), Variant.newBuilder().of(true),
fromString("TRUE"))
- .fromCase(VARIANT(), Variant.newBuilder().of(false),
fromString("FALSE"))
- .fromCase(VARIANT(), Variant.newBuilder().of("foo"),
fromString("foo"))
- .fromCase(VARIANT(), Variant.newBuilder().of(42),
fromString("42"))
+ .fromCase(VARIANT(), VARIANT_BUILDER.of(true),
fromString("TRUE"))
+ .fromCase(VARIANT(), VARIANT_BUILDER.of(false),
fromString("FALSE"))
+ .fromCase(VARIANT(), VARIANT_BUILDER.of("foo"),
fromString("foo"))
+ .fromCase(VARIANT(), VARIANT_BUILDER.of(42),
fromString("42"))
.fromCase(
VARIANT(),
- Variant.newBuilder().of(new
BigDecimal("123.456")),
+ VARIANT_BUILDER.of(new BigDecimal("123.456")),
fromString("123.456"))
// a small scale stays plain instead of turning into
scientific notation
.fromCase(
VARIANT(),
- Variant.newBuilder().of(new
BigDecimal("0.0000000001")),
+ VARIANT_BUILDER.of(new
BigDecimal("0.0000000001")),
fromString("0.0000000001"))
.fromCase(
VARIANT(),
-
Variant.newBuilder().of(LocalDate.parse("2021-09-24")),
+
VARIANT_BUILDER.of(LocalDate.parse("2021-09-24")),
DATE_STRING)
.fromCase(
VARIANT(),
-
Variant.newBuilder().of(TIMESTAMP.toLocalDateTime()),
+
VARIANT_BUILDER.of(TIMESTAMP.toLocalDateTime()),
TIMESTAMP_STRING)
.fromCase(
VARIANT(),
CET_CONTEXT,
- Variant.newBuilder().of(TIMESTAMP.toInstant()),
+ VARIANT_BUILDER.of(TIMESTAMP.toInstant()),
TIMESTAMP_STRING_CET)
// a binary value is read as UTF-8, like a regular
BINARY to string cast
.fromCase(
VARIANT(),
-
Variant.newBuilder().of("hello".getBytes(StandardCharsets.UTF_8)),
+
VARIANT_BUILDER.of("hello".getBytes(StandardCharsets.UTF_8)),
fromString("hello"))
// a multi-byte sequence passes the well-formedness
check unchanged
.fromCase(
VARIANT(),
-
Variant.newBuilder().of("héllo".getBytes(StandardCharsets.UTF_8)),
+
VARIANT_BUILDER.of("héllo".getBytes(StandardCharsets.UTF_8)),
fromString("héllo"))
// bytes that are not valid UTF-8 are rejected rather
than decoded into
// the U+FFFD replacement character
.fail(
VARIANT(),
- Variant.newBuilder().of(INVALID_UTF8),
+ VARIANT_BUILDER.of(INVALID_UTF8),
TableRuntimeException.class)
// an object or an array has no scalar rendering and
fails the cast
.fail(VARIANT(), VARIANT_ARRAY,
TableRuntimeException.class)
@@ -1627,218 +1684,192 @@ class CastRulesTest {
VARIANT(), VARIANT_ARRAY,
fromString("[1,\"two\",false,null]"))
.fromCasePrinting(VARIANT(), VARIANT_OBJECT,
fromString("{\"k\":[1,2]}"))
.fromCasePrinting(
- VARIANT(), Variant.newBuilder().of("foo"),
fromString("\"foo\""))
- .fromCasePrinting(VARIANT(),
Variant.newBuilder().of(42), fromString("42")),
+ VARIANT(), VARIANT_BUILDER.of("foo"),
fromString("\"foo\""))
+ .fromCasePrinting(VARIANT(), VARIANT_BUILDER.of(42),
fromString("42")),
// A bounded character target pads and trims like any other
cast into it, and its
// length counts code points, so a character outside the BMP
fills one position
// even though it occupies two UTF-16 units.
CastTestSpecBuilder.testCastTo(CHAR(1))
- .fromCase(VARIANT(), Variant.newBuilder().of("x"),
fromString("x"))
- .fromCase(VARIANT(), Variant.newBuilder().of(NON_BMP),
fromString(NON_BMP))
+ .fromCase(VARIANT(), VARIANT_BUILDER.of("x"),
fromString("x"))
+ .fromCase(VARIANT(), VARIANT_BUILDER.of(NON_BMP),
fromString(NON_BMP))
.fromCase(
VARIANT(),
- Variant.newBuilder().of(NON_BMP + NON_BMP),
+ VARIANT_BUILDER.of(NON_BMP + NON_BMP),
fromString(NON_BMP))
- .fromCase(
- VARIANT(),
Variant.newBuilder().of("abcdefghij"), fromString("a")),
+ .fromCase(VARIANT(), VARIANT_BUILDER.of("abcdefghij"),
fromString("a")),
CastTestSpecBuilder.testCastTo(CHAR(5))
// shorter than the target, so it is padded to the
fixed width
- .fromCase(VARIANT(), Variant.newBuilder().of("ab"),
fromString("ab "))
- .fromCase(
- VARIANT(),
- Variant.newBuilder().of("abcdefghij"),
- fromString("abcde")),
+ .fromCase(VARIANT(), VARIANT_BUILDER.of("ab"),
fromString("ab "))
+ .fromCase(VARIANT(), VARIANT_BUILDER.of("abcdefghij"),
fromString("abcde")),
CastTestSpecBuilder.testCastTo(VARCHAR(2))
- .fromCase(VARIANT(), Variant.newBuilder().of(NON_BMP),
fromString(NON_BMP))
+ .fromCase(VARIANT(), VARIANT_BUILDER.of(NON_BMP),
fromString(NON_BMP))
.fromCase(
VARIANT(),
- Variant.newBuilder().of(NON_BMP + NON_BMP),
+ VARIANT_BUILDER.of(NON_BMP + NON_BMP),
fromString(NON_BMP + NON_BMP))
// longer than the target, so it is trimmed rather
than rejected, and a
// variable width target is not padded
.fromCase(
VARIANT(),
- Variant.newBuilder().of(NON_BMP + NON_BMP +
NON_BMP),
+ VARIANT_BUILDER.of(NON_BMP + NON_BMP +
NON_BMP),
fromString(NON_BMP + NON_BMP))
- .fromCase(VARIANT(), Variant.newBuilder().of("a"),
fromString("a")),
+ .fromCase(VARIANT(), VARIANT_BUILDER.of("a"),
fromString("a")),
CastTestSpecBuilder.testCastTo(BOOLEAN())
- .fromCase(VARIANT(), Variant.newBuilder().of(true),
true)
- .fromCase(VARIANT(), Variant.newBuilder().of(false),
false)
- .fail(VARIANT(), Variant.newBuilder().of(1),
TableRuntimeException.class),
+ .fromCase(VARIANT(), VARIANT_BUILDER.of(true), true)
+ .fromCase(VARIANT(), VARIANT_BUILDER.of(false), false)
+ .fail(VARIANT(), VARIANT_BUILDER.of(1),
TableRuntimeException.class),
CastTestSpecBuilder.testCastTo(TINYINT())
- .fromCase(VARIANT(), Variant.newBuilder().of((byte)
42), (byte) 42)
+ .fromCase(VARIANT(), VARIANT_BUILDER.of((byte) 42),
(byte) 42)
// a wider integer kind narrows while the value is in
range
- .fromCase(VARIANT(), Variant.newBuilder().of(42),
(byte) 42)
+ .fromCase(VARIANT(), VARIANT_BUILDER.of(42), (byte) 42)
// out of range is rejected instead of wrapping
- .fail(VARIANT(), Variant.newBuilder().of(1000),
TableRuntimeException.class)
- .fail(VARIANT(), Variant.newBuilder().of("x"),
TableRuntimeException.class),
+ .fail(VARIANT(), VARIANT_BUILDER.of(1000),
TableRuntimeException.class)
+ .fail(VARIANT(), VARIANT_BUILDER.of("x"),
TableRuntimeException.class),
CastTestSpecBuilder.testCastTo(SMALLINT())
- .fromCase(VARIANT(), Variant.newBuilder().of((short)
42), (short) 42)
- .fromCase(VARIANT(), Variant.newBuilder().of((byte)
42), (short) 42)
- .fromCase(VARIANT(), Variant.newBuilder().of(1000),
(short) 1000)
- .fail(
- VARIANT(),
- Variant.newBuilder().of(40000),
- TableRuntimeException.class)
- .fail(
- VARIANT(),
- Variant.newBuilder().of(true),
- TableRuntimeException.class),
+ .fromCase(VARIANT(), VARIANT_BUILDER.of((short) 42),
(short) 42)
+ .fromCase(VARIANT(), VARIANT_BUILDER.of((byte) 42),
(short) 42)
+ .fromCase(VARIANT(), VARIANT_BUILDER.of(1000), (short)
1000)
+ .fail(VARIANT(), VARIANT_BUILDER.of(40000),
TableRuntimeException.class)
+ .fail(VARIANT(), VARIANT_BUILDER.of(true),
TableRuntimeException.class),
CastTestSpecBuilder.testCastTo(INT())
- .fromCase(VARIANT(), Variant.newBuilder().of(42), 42)
+ .fromCase(VARIANT(), VARIANT_BUILDER.of(42), 42)
// every integer kind converts as long as the value
fits
- .fromCase(VARIANT(), Variant.newBuilder().of((byte)
42), 42)
- .fromCase(VARIANT(), Variant.newBuilder().of((short)
42), 42)
- .fromCase(VARIANT(), Variant.newBuilder().of(42L), 42)
+ .fromCase(VARIANT(), VARIANT_BUILDER.of((byte) 42), 42)
+ .fromCase(VARIANT(), VARIANT_BUILDER.of((short) 42),
42)
+ .fromCase(VARIANT(), VARIANT_BUILDER.of(42L), 42)
.fail(
VARIANT(),
- Variant.newBuilder().of(2147483648L),
+ VARIANT_BUILDER.of(2147483648L),
TableRuntimeException.class)
// an approximate or decimal kind converts when the
value is integral
- .fromCase(VARIANT(), Variant.newBuilder().of(7.0d), 7)
- .fromCase(VARIANT(), Variant.newBuilder().of(new
BigDecimal("7.0")), 7)
+ .fromCase(VARIANT(), VARIANT_BUILDER.of(7.0d), 7)
+ .fromCase(VARIANT(), VARIANT_BUILDER.of(new
BigDecimal("7.0")), 7)
// a fractional value would have to be rounded away,
so it is rejected
- .fail(VARIANT(), Variant.newBuilder().of(7.2d),
TableRuntimeException.class)
+ .fail(VARIANT(), VARIANT_BUILDER.of(7.2d),
TableRuntimeException.class)
.fail(
VARIANT(),
- Variant.newBuilder().of(new BigDecimal("7.2")),
+ VARIANT_BUILDER.of(new BigDecimal("7.2")),
TableRuntimeException.class)
// a non-numeric variant cannot be cast to a number
- .fail(
- VARIANT(),
- Variant.newBuilder().of("foo"),
- TableRuntimeException.class)
- .fail(
- VARIANT(),
- Variant.newBuilder().of(true),
- TableRuntimeException.class),
+ .fail(VARIANT(), VARIANT_BUILDER.of("foo"),
TableRuntimeException.class)
+ .fail(VARIANT(), VARIANT_BUILDER.of(true),
TableRuntimeException.class),
CastTestSpecBuilder.testCastTo(BIGINT())
- .fromCase(VARIANT(), Variant.newBuilder().of(42L), 42L)
- .fromCase(VARIANT(), Variant.newBuilder().of(42), 42L)
- .fail(VARIANT(), Variant.newBuilder().of("x"),
TableRuntimeException.class),
+ .fromCase(VARIANT(), VARIANT_BUILDER.of(42L), 42L)
+ .fromCase(VARIANT(), VARIANT_BUILDER.of(42), 42L)
+ .fail(VARIANT(), VARIANT_BUILDER.of("x"),
TableRuntimeException.class),
CastTestSpecBuilder.testCastTo(FLOAT())
// every numeric kind reaches an approximate target
- .fromCase(VARIANT(), Variant.newBuilder().of(1.5f),
1.5f)
- .fromCase(VARIANT(), Variant.newBuilder().of(1.5d),
1.5f)
- .fromCase(VARIANT(), Variant.newBuilder().of(3), 3.0f)
+ .fromCase(VARIANT(), VARIANT_BUILDER.of(1.5f), 1.5f)
+ .fromCase(VARIANT(), VARIANT_BUILDER.of(1.5d), 1.5f)
+ .fromCase(VARIANT(), VARIANT_BUILDER.of(3), 3.0f)
.fromCase(
- VARIANT(),
- Variant.newBuilder().of(new
BigDecimal("123.456")),
- 123.456f)
+ VARIANT(), VARIANT_BUILDER.of(new
BigDecimal("123.456")), 123.456f)
// a magnitude a FLOAT cannot represent is still
rejected
- .fail(
- VARIANT(),
- Variant.newBuilder().of(1e40d),
- TableRuntimeException.class)
- .fail(VARIANT(), Variant.newBuilder().of("x"),
TableRuntimeException.class),
+ .fail(VARIANT(), VARIANT_BUILDER.of(1e40d),
TableRuntimeException.class)
+ .fail(VARIANT(), VARIANT_BUILDER.of("x"),
TableRuntimeException.class),
CastTestSpecBuilder.testCastTo(DOUBLE())
- .fromCase(VARIANT(), Variant.newBuilder().of(1.5d),
1.5d)
- .fromCase(VARIANT(), Variant.newBuilder().of(1.5f),
1.5d)
- .fromCase(VARIANT(), Variant.newBuilder().of(3), 3.0d)
+ .fromCase(VARIANT(), VARIANT_BUILDER.of(1.5d), 1.5d)
+ .fromCase(VARIANT(), VARIANT_BUILDER.of(1.5f), 1.5d)
+ .fromCase(VARIANT(), VARIANT_BUILDER.of(3), 3.0d)
.fromCase(
- VARIANT(),
- Variant.newBuilder().of(new
BigDecimal("123.456")),
- 123.456d)
- .fail(VARIANT(), Variant.newBuilder().of("x"),
TableRuntimeException.class),
+ VARIANT(), VARIANT_BUILDER.of(new
BigDecimal("123.456")), 123.456d)
+ .fail(VARIANT(), VARIANT_BUILDER.of("x"),
TableRuntimeException.class),
CastTestSpecBuilder.testCastTo(DECIMAL(5, 2))
.fromCase(VARIANT(), null, null)
.fromCase(
VARIANT(),
- Variant.newBuilder().of(new
BigDecimal("123.45")),
+ VARIANT_BUILDER.of(new BigDecimal("123.45")),
DecimalData.fromBigDecimal(new
BigDecimal("123.45"), 5, 2))
// trailing zeros may be appended to reach the target
scale
.fromCase(
VARIANT(),
- Variant.newBuilder().of(new
BigDecimal("123.4")),
+ VARIANT_BUILDER.of(new BigDecimal("123.4")),
DecimalData.fromBigDecimal(new
BigDecimal("123.40"), 5, 2))
// an integer is exact, so it converts when it fits
.fromCase(
VARIANT(),
- Variant.newBuilder().of(42),
+ VARIANT_BUILDER.of(42),
DecimalData.fromBigDecimal(new
BigDecimal("42.00"), 5, 2))
// a scale that would have to round is rejected
.fail(
VARIANT(),
- Variant.newBuilder().of(new
BigDecimal("123.456")),
+ VARIANT_BUILDER.of(new BigDecimal("123.456")),
TableRuntimeException.class)
// an approximate kind is not read as a decimal
- .fail(VARIANT(), Variant.newBuilder().of(1.5d),
TableRuntimeException.class)
- .fail(VARIANT(), Variant.newBuilder().of("x"),
TableRuntimeException.class),
+ .fail(VARIANT(), VARIANT_BUILDER.of(1.5d),
TableRuntimeException.class)
+ .fail(VARIANT(), VARIANT_BUILDER.of("x"),
TableRuntimeException.class),
CastTestSpecBuilder.testCastTo(BYTES())
.fromCase(VARIANT(), null, null)
.fromCase(
VARIANT(),
- Variant.newBuilder().of(new byte[] {1, 2, 3}),
+ VARIANT_BUILDER.of(new byte[] {1, 2, 3}),
new byte[] {1, 2, 3})
// the raw bytes stay reachable when the character
string cast rejects
// them, which is what makes this the way to inspect
such a value
- .fromCase(VARIANT(),
Variant.newBuilder().of(INVALID_UTF8), INVALID_UTF8)
- .fail(
- VARIANT(),
- Variant.newBuilder().of("foo"),
- TableRuntimeException.class),
+ .fromCase(VARIANT(), VARIANT_BUILDER.of(INVALID_UTF8),
INVALID_UTF8)
+ .fail(VARIANT(), VARIANT_BUILDER.of("foo"),
TableRuntimeException.class),
CastTestSpecBuilder.testCastTo(DATE())
.fromCase(
VARIANT(),
- Variant.newBuilder().of(LocalDate.of(2020, 1,
1)),
+ VARIANT_BUILDER.of(LocalDate.of(2020, 1, 1)),
(int) LocalDate.of(2020, 1, 1).toEpochDay())
- .fail(VARIANT(), Variant.newBuilder().of(1),
TableRuntimeException.class),
+ .fail(VARIANT(), VARIANT_BUILDER.of(1),
TableRuntimeException.class),
CastTestSpecBuilder.testCastTo(TIMESTAMP())
.fromCase(VARIANT(), null, null)
.fromCase(
VARIANT(),
- Variant.newBuilder().of(LocalDateTime.of(2020,
1, 1, 12, 0, 0)),
+ VARIANT_BUILDER.of(LocalDateTime.of(2020, 1,
1, 12, 0, 0)),
TimestampData.fromLocalDateTime(
LocalDateTime.of(2020, 1, 1, 12, 0,
0)))
- .fail(VARIANT(), Variant.newBuilder().of(1),
TableRuntimeException.class)
+ .fail(VARIANT(), VARIANT_BUILDER.of(1),
TableRuntimeException.class)
// a TIMESTAMP_LTZ is a different kind and is not read
as a TIMESTAMP
.fail(
VARIANT(),
-
Variant.newBuilder().of(Instant.ofEpochSecond(1_600_000_000L)),
+
VARIANT_BUILDER.of(Instant.ofEpochSecond(1_600_000_000L)),
TableRuntimeException.class),
// A variant keeps microseconds, so fractional seconds beyond
the target precision
// are truncated, matching a regular cast into a narrower
TIMESTAMP.
CastTestSpecBuilder.testCastTo(TIMESTAMP(3))
.fromCase(
VARIANT(),
- Variant.newBuilder().of(LocalDateTime.of(2020,
1, 1, 12, 0, 0)),
+ VARIANT_BUILDER.of(LocalDateTime.of(2020, 1,
1, 12, 0, 0)),
TimestampData.fromLocalDateTime(
LocalDateTime.of(2020, 1, 1, 12, 0,
0)))
.fromCase(
VARIANT(),
- Variant.newBuilder()
- .of(LocalDateTime.of(2020, 1, 1, 12,
0, 0, 123000000)),
+ VARIANT_BUILDER.of(
+ LocalDateTime.of(2020, 1, 1, 12, 0, 0,
123000000)),
TimestampData.fromLocalDateTime(
LocalDateTime.of(2020, 1, 1, 12, 0, 0,
123000000)))
.fromCase(
VARIANT(),
- Variant.newBuilder()
- .of(LocalDateTime.of(2020, 1, 1, 12,
0, 0, 123456000)),
+ VARIANT_BUILDER.of(
+ LocalDateTime.of(2020, 1, 1, 12, 0, 0,
123456000)),
TimestampData.fromLocalDateTime(
LocalDateTime.of(2020, 1, 1, 12, 0, 0,
123000000))),
CastTestSpecBuilder.testCastTo(TIMESTAMP(0))
.fromCase(
VARIANT(),
- Variant.newBuilder()
- .of(LocalDateTime.of(2020, 1, 1, 12,
0, 0, 123000000)),
+ VARIANT_BUILDER.of(
+ LocalDateTime.of(2020, 1, 1, 12, 0, 0,
123000000)),
TimestampData.fromLocalDateTime(
LocalDateTime.of(2020, 1, 1, 12, 0,
0))),
CastTestSpecBuilder.testCastTo(TIMESTAMP_LTZ())
.fromCase(
VARIANT(),
-
Variant.newBuilder().of(Instant.ofEpochSecond(1_600_000_000L)),
+
VARIANT_BUILDER.of(Instant.ofEpochSecond(1_600_000_000L)),
TimestampData.fromInstant(Instant.ofEpochSecond(1_600_000_000L)))
- .fail(VARIANT(), Variant.newBuilder().of(1),
TableRuntimeException.class)
+ .fail(VARIANT(), VARIANT_BUILDER.of(1),
TableRuntimeException.class)
// a TIMESTAMP is not read as a TIMESTAMP_LTZ either
.fail(
VARIANT(),
- Variant.newBuilder().of(LocalDateTime.of(2020,
1, 1, 12, 0, 0)),
+ VARIANT_BUILDER.of(LocalDateTime.of(2020, 1,
1, 12, 0, 0)),
TableRuntimeException.class),
CastTestSpecBuilder.testCastTo(TIMESTAMP_LTZ(3))
.fromCase(
VARIANT(),
- Variant.newBuilder()
-
.of(Instant.ofEpochSecond(1_600_000_000L, 123456000)),
+ VARIANT_BUILDER.of(
+ Instant.ofEpochSecond(1_600_000_000L,
123456000)),
TimestampData.fromInstant(
Instant.ofEpochSecond(1_600_000_000L,
123000000))),
// A binary target pads a shorter value and truncates a longer
one, matching a
@@ -1846,26 +1877,121 @@ class CastRulesTest {
CastTestSpecBuilder.testCastTo(BINARY(4))
.fromCase(
VARIANT(),
- Variant.newBuilder().of(new byte[] {1, 2, 3,
4}),
+ VARIANT_BUILDER.of(new byte[] {1, 2, 3, 4}),
new byte[] {1, 2, 3, 4})
.fromCase(
VARIANT(),
- Variant.newBuilder().of(new byte[] {1, 2}),
+ VARIANT_BUILDER.of(new byte[] {1, 2}),
new byte[] {1, 2, 0, 0})
.fromCase(
VARIANT(),
- Variant.newBuilder().of(new byte[] {1, 2, 3,
4, 5, 6}),
+ VARIANT_BUILDER.of(new byte[] {1, 2, 3, 4, 5,
6}),
new byte[] {1, 2, 3, 4}),
CastTestSpecBuilder.testCastTo(VARBINARY(4))
// a variable width target is trimmed but never padded
+ .fromCase(
+ VARIANT(), VARIANT_BUILDER.of(new byte[] {1,
2}), new byte[] {1, 2})
+ .fromCase(
+ VARIANT(),
+ VARIANT_BUILDER.of(new byte[] {1, 2, 3, 4, 5,
6}),
+ new byte[] {1, 2, 3, 4}),
+ // From VARIANT to a constructed target. A constructed cast is
the scalar cast
+ // applied to every leaf plus a shape check at each level.
+ CastTestSpecBuilder.testCastTo(ARRAY(INT()))
+ .fromCase(VARIANT(), null, null)
+ .fromCase(
+ VARIANT(),
+ VARIANT_INT_ARRAY,
+ new GenericArrayData(new Integer[] {1, 2, 3}))
+ // a VARIANT null element maps to SQL NULL for a
nullable element type
+ .fromCase(
+ VARIANT(),
+ VARIANT_INT_ARRAY_WITH_NULL,
+ new GenericArrayData(new Integer[] {1, null,
3}))
+ .fromCase(
+ VARIANT(),
+ VARIANT_EMPTY_ARRAY,
+ new GenericArrayData(new Integer[] {}))
+ // a stored string is never parsed into an integer
+ .fail(VARIANT(), VARIANT_STRING_ARRAY,
TableRuntimeException.class)
+ // a heterogeneous array fails on an element that is
not an integer
+ .fail(VARIANT(), VARIANT_MIXED_ARRAY,
TableRuntimeException.class)
+ // an object or a scalar is not an array
+ .fail(VARIANT(), VARIANT_OBJECT,
TableRuntimeException.class)
+ .fail(VARIANT(), VARIANT_BUILDER.of(1),
TableRuntimeException.class),
+ CastTestSpecBuilder.testCastTo(ARRAY(INT().notNull()))
+ .fromCase(
+ VARIANT(),
+ VARIANT_INT_ARRAY,
+ new GenericArrayData(new int[] {1, 2, 3}))
+ // a VARIANT null element fails a NOT NULL element type
+ .fail(VARIANT(), VARIANT_INT_ARRAY_WITH_NULL,
TableRuntimeException.class),
+ CastTestSpecBuilder.testCastTo(ARRAY(STRING()))
+ // each element renders to string like the scalar cast
+ .fromCase(
+ VARIANT(),
+ VARIANT_INT_ARRAY,
+ new GenericArrayData(
+ new Object[] {
+ fromString("1"), fromString("2"),
fromString("3")
+ }))
+ // a heterogeneous array renders every element to
string
.fromCase(
VARIANT(),
- Variant.newBuilder().of(new byte[] {1, 2}),
- new byte[] {1, 2})
+ VARIANT_MIXED_ARRAY,
+ new GenericArrayData(
+ new Object[] {
+ fromString("1"),
+ fromString("a"),
+ fromString("2"),
+ fromString("b")
+ }))
+ // stored strings of different lengths render unchanged
+ .fromCase(
+ VARIANT(),
+ VARIANT_STRING_ARRAY,
+ new GenericArrayData(
+ new Object[] {
+ fromString("1"), fromString("22"),
fromString("333")
+ })),
+ CastTestSpecBuilder.testCastTo(ARRAY(DOUBLE()))
+ // an approximate leaf takes any numeric kind
+ .fromCase(
+ VARIANT(),
+ VARIANT_INT_ARRAY,
+ new GenericArrayData(new Double[] {1.0, 2.0,
3.0})),
+ // the recursion composes: an array of arrays with no special
case
+ CastTestSpecBuilder.testCastTo(ARRAY(ARRAY(INT())))
+ .fromCase(
+ VARIANT(),
+ VARIANT_NESTED_ARRAY,
+ new GenericArrayData(
+ new GenericArrayData[] {
+ new GenericArrayData(new Integer[]
{1, 2}),
+ new GenericArrayData(new Integer[]
{3})
+ })),
+ // an ARRAY<VARIANT> leaf is the identity cast, keeping each
element as a variant
+ CastTestSpecBuilder.testCastTo(ARRAY(VARIANT()))
.fromCase(
VARIANT(),
- Variant.newBuilder().of(new byte[] {1, 2, 3,
4, 5, 6}),
- new byte[] {1, 2, 3, 4}));
+ VARIANT_INT_ARRAY,
+ new GenericArrayData(
+ new Variant[] {
+ VARIANT_INT_ARRAY.getElement(0),
+ VARIANT_INT_ARRAY.getElement(1),
+ VARIANT_INT_ARRAY.getElement(2)
+ }))
+ // the identity cast keeps a VARIANT null element as a
variant null, not a
+ // SQL NULL
+ .fromCase(
+ VARIANT(),
+ VARIANT_INT_ARRAY_WITH_NULL,
+ new GenericArrayData(
+ new Variant[] {
+
VARIANT_INT_ARRAY_WITH_NULL.getElement(0),
+
VARIANT_INT_ARRAY_WITH_NULL.getElement(1),
+
VARIANT_INT_ARRAY_WITH_NULL.getElement(2)
+ })));
}
@TestFactory
diff --git
a/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/functions/VariantCastUtils.java
b/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/functions/VariantCastUtils.java
index 89f112348fe..ac8d550195c 100644
---
a/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/functions/VariantCastUtils.java
+++
b/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/functions/VariantCastUtils.java
@@ -63,6 +63,27 @@ public final class VariantCastUtils {
private VariantCastUtils() {}
+ /**
+ * Reads the size of an array variant, failing when the variant is not an
array. A constructed
+ * cast checks the shape at every level: only an array casts to {@code
ARRAY}.
+ */
+ public static int arraySize(Variant variant, String targetType) {
+ if (variant.isArray()) {
+ return variant.getArraySize();
+ }
+ throw wrongShape(variant, targetType, "an array");
+ }
+
+ private static TableRuntimeException wrongShape(
+ Variant variant, String targetType, String required) {
+ return new TableRuntimeException(
+ String.format(
+ "Cannot cast a VARIANT %s value to %s because the
target requires %s. Only "
+ + "a variant array casts to ARRAY and only a
variant object casts "
+ + "to ROW, STRUCTURED, or MAP.",
+ variant.getType(), targetType, required));
+ }
+
/**
* Reads a numeric variant as a {@code long} and checks it against the
target range. An
* approximate or decimal value is accepted only when it is already
integral, so nothing is