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 4a23ab12063 [FLINK-40672][table] Preserve nullability when casting
VARIANT values
4a23ab12063 is described below
commit 4a23ab12063d4d1354fae905f01e47b9777c402e
Author: dylanhz <[email protected]>
AuthorDate: Tue Sep 22 21:15:26 2026 +0800
[FLINK-40672][table] Preserve nullability when casting VARIANT values
This closes #29202.
---
.../functions/BuiltInFunctionDefinitions.java | 3 +-
.../strategies/SpecificTypeStrategies.java | 16 +++
.../table/types/inference/TypeStrategiesTest.java | 24 ++++
.../apache/calcite/sql/fun/SqlCastFunction.java | 33 +++++
.../converter/converters/CastConverter.java | 10 +-
.../planner/functions/CastFunctionITCase.java | 142 ++++++++++++---------
6 files changed, 163 insertions(+), 65 deletions(-)
diff --git
a/flink-table/flink-table-common/src/main/java/org/apache/flink/table/functions/BuiltInFunctionDefinitions.java
b/flink-table/flink-table-common/src/main/java/org/apache/flink/table/functions/BuiltInFunctionDefinitions.java
index 53ec8d16078..08cc761375b 100644
---
a/flink-table/flink-table-common/src/main/java/org/apache/flink/table/functions/BuiltInFunctionDefinitions.java
+++
b/flink-table/flink-table-common/src/main/java/org/apache/flink/table/functions/BuiltInFunctionDefinitions.java
@@ -3477,8 +3477,7 @@ public final class BuiltInFunctionDefinitions {
.callSyntax("CAST", SqlCallSyntax.CAST)
.kind(SCALAR)
.inputTypeStrategy(SpecificInputTypeStrategies.CAST)
- .outputTypeStrategy(
- nullableIfArgs(ConstantArgumentCount.to(0),
TypeStrategies.argument(1)))
+ .outputTypeStrategy(SpecificTypeStrategies.CAST)
.build();
public static final BuiltInFunctionDefinition TRY_CAST =
diff --git
a/flink-table/flink-table-common/src/main/java/org/apache/flink/table/types/inference/strategies/SpecificTypeStrategies.java
b/flink-table/flink-table-common/src/main/java/org/apache/flink/table/types/inference/strategies/SpecificTypeStrategies.java
index e3f8aa35327..00a82abb95d 100644
---
a/flink-table/flink-table-common/src/main/java/org/apache/flink/table/types/inference/strategies/SpecificTypeStrategies.java
+++
b/flink-table/flink-table-common/src/main/java/org/apache/flink/table/types/inference/strategies/SpecificTypeStrategies.java
@@ -26,6 +26,7 @@ import org.apache.flink.table.types.DataType;
import org.apache.flink.table.types.KeyValueDataType;
import org.apache.flink.table.types.inference.TypeStrategies;
import org.apache.flink.table.types.inference.TypeStrategy;
+import org.apache.flink.table.types.logical.LogicalType;
import org.apache.flink.table.types.logical.LogicalTypeRoot;
import java.util.List;
@@ -43,6 +44,21 @@ public final class SpecificTypeStrategies {
/** See {@link UnusedTypeStrategy}. */
public static final TypeStrategy UNUSED = new UnusedTypeStrategy();
+ /** Type strategy specific for {@link BuiltInFunctionDefinitions#CAST}. */
+ public static final TypeStrategy CAST =
+ callContext -> {
+ final LogicalType sourceType =
+
callContext.getArgumentDataTypes().get(0).getLogicalType();
+ final DataType targetType =
callContext.getArgumentDataTypes().get(1);
+ final boolean nullable =
+ sourceType.isNullable()
+ || (sourceType.is(LogicalTypeRoot.VARIANT)
+ && !targetType
+ .getLogicalType()
+ .is(LogicalTypeRoot.VARIANT));
+ return Optional.of(nullable ? targetType.nullable() :
targetType.notNull());
+ };
+
/** See {@link RowTypeStrategy}. */
public static final TypeStrategy ROW = new RowTypeStrategy();
diff --git
a/flink-table/flink-table-common/src/test/java/org/apache/flink/table/types/inference/TypeStrategiesTest.java
b/flink-table/flink-table-common/src/test/java/org/apache/flink/table/types/inference/TypeStrategiesTest.java
index 1dddc69720f..05012202ab8 100644
---
a/flink-table/flink-table-common/src/test/java/org/apache/flink/table/types/inference/TypeStrategiesTest.java
+++
b/flink-table/flink-table-common/src/test/java/org/apache/flink/table/types/inference/TypeStrategiesTest.java
@@ -32,6 +32,7 @@ import static
org.apache.flink.table.types.inference.TypeStrategies.explicit;
import static
org.apache.flink.table.types.inference.TypeStrategies.nullableIfAllArgs;
import static
org.apache.flink.table.types.inference.TypeStrategies.nullableIfArgs;
import static
org.apache.flink.table.types.inference.TypeStrategies.varyingString;
+import static
org.apache.flink.table.types.inference.strategies.SpecificTypeStrategies.CAST;
import static
org.apache.flink.table.types.inference.strategies.SpecificTypeStrategies.PERCENTILE;
/** Tests for built-in {@link TypeStrategies}. */
@@ -40,6 +41,29 @@ class TypeStrategiesTest extends TypeStrategiesTestBase {
@Override
protected Stream<TestSpec> testData() {
return Stream.of(
+ TestSpec.forStrategy("Casting a non-null VARIANT to a scalar",
CAST)
+ .inputTypes(DataTypes.VARIANT().notNull(),
DataTypes.INT())
+ .expectDataType(DataTypes.INT()),
+ TestSpec.forStrategy("Casting a nullable VARIANT to a scalar",
CAST)
+ .inputTypes(DataTypes.VARIANT(),
DataTypes.INT().notNull())
+ .expectDataType(DataTypes.INT()),
+ TestSpec.forStrategy("Casting VARIANT preserves nested
nullability", CAST)
+ .inputTypes(
+ DataTypes.VARIANT().notNull(),
+
DataTypes.ARRAY(DataTypes.INT().notNull()).notNull())
+
.expectDataType(DataTypes.ARRAY(DataTypes.INT().notNull())),
+ TestSpec.forStrategy("Casting a non-null VARIANT to VARIANT",
CAST)
+ .inputTypes(DataTypes.VARIANT().notNull(),
DataTypes.VARIANT())
+ .expectDataType(DataTypes.VARIANT().notNull()),
+ TestSpec.forStrategy("Casting a nullable VARIANT to VARIANT",
CAST)
+ .inputTypes(DataTypes.VARIANT(),
DataTypes.VARIANT().notNull())
+ .expectDataType(DataTypes.VARIANT()),
+ TestSpec.forStrategy("Casting a non-null scalar", CAST)
+ .inputTypes(DataTypes.INT().notNull(),
DataTypes.BIGINT())
+ .expectDataType(DataTypes.BIGINT().notNull()),
+ TestSpec.forStrategy("Casting a nullable scalar", CAST)
+ .inputTypes(DataTypes.INT(),
DataTypes.BIGINT().notNull())
+ .expectDataType(DataTypes.BIGINT()),
// missing strategy with arbitrary argument
TypeStrategiesTestBase.TestSpec.forStrategy(MISSING)
.inputTypes(DataTypes.INT())
diff --git
a/flink-table/flink-table-planner/src/main/java/org/apache/calcite/sql/fun/SqlCastFunction.java
b/flink-table/flink-table-planner/src/main/java/org/apache/calcite/sql/fun/SqlCastFunction.java
index 478b57b3247..bd5ba1067a8 100644
---
a/flink-table/flink-table-planner/src/main/java/org/apache/calcite/sql/fun/SqlCastFunction.java
+++
b/flink-table/flink-table-planner/src/main/java/org/apache/calcite/sql/fun/SqlCastFunction.java
@@ -77,6 +77,11 @@ import static org.apache.calcite.util.Static.RESOURCE;
* #checkOperandTypes(SqlCallBinding, boolean)} and new method {@link
#canCastFrom(RelDataType,
* RelDataType)}.
*
+ * <p>Flink's {@link #deriveType(RelDataTypeFactory, RelDataType, RelDataType,
boolean)} preserves
+ * the target type's nested nullability instead of delegating to {@link
+ * #createTypeWithNullabilityFromExpr(RelDataTypeFactory, RelDataType,
RelDataType, boolean)}. The
+ * VARIANT rules introduced in Calcite 1.39.0 are also applied in {@code
deriveType}.
+ *
* @see SqlCastOperator
*/
public class SqlCastFunction extends SqlFunction {
@@ -148,6 +153,21 @@ public class SqlCastFunction extends SqlFunction {
RelDataType expressionType,
RelDataType targetType,
boolean safe) {
+ // Flink modification: apply the same VARIANT nullability rules as
+ // createTypeWithNullabilityFromExpr.
+ if (targetType.getSqlTypeName() == SqlTypeName.VARIANT) {
+ // A variant can be cast from any other type, and it inherits
+ // the nullability of the source.
+ // Note that the order of this test and the next one is important.
+ return typeFactory.createTypeWithNullability(targetType,
expressionType.isNullable());
+ }
+
+ if (expressionType.getSqlTypeName() == SqlTypeName.VARIANT) {
+ // A variant can be cast to any other type, but the result
+ // is always nullable, like in the case of a safe cast.
+ return typeFactory.createTypeWithNullability(targetType, true);
+ }
+
return typeFactory.createTypeWithNullability(
targetType, expressionType.isNullable() || safe);
}
@@ -159,6 +179,19 @@ public class SqlCastFunction extends SqlFunction {
boolean safe) {
boolean isNullable = expressionType.isNullable() || safe;
+ if (targetType.getSqlTypeName() == SqlTypeName.VARIANT) {
+ // A variant can be cast from any other type, and it inherits
+ // the nullability of the source.
+ // Note that the order of this test and the next one is important.
+ return typeFactory.createTypeWithNullability(targetType,
expressionType.isNullable());
+ }
+
+ if (expressionType.getSqlTypeName() == SqlTypeName.VARIANT) {
+ // A variant can be cast to any other type, but the result
+ // is always nullable, like in the case of a safe cast.
+ return typeFactory.createTypeWithNullability(targetType, true);
+ }
+
if (isCollection(expressionType)) {
RelDataType expressionElementType =
expressionType.getComponentType();
RelDataType targetElementType = targetType.getComponentType();
diff --git
a/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/expressions/converter/converters/CastConverter.java
b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/expressions/converter/converters/CastConverter.java
index 384a4f12908..06f2a01b3b5 100644
---
a/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/expressions/converter/converters/CastConverter.java
+++
b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/expressions/converter/converters/CastConverter.java
@@ -23,6 +23,8 @@ import org.apache.flink.table.expressions.CallExpression;
import org.apache.flink.table.expressions.TypeLiteralExpression;
import org.apache.flink.table.functions.BuiltInFunctionDefinitions;
import
org.apache.flink.table.planner.expressions.converter.CallExpressionConvertRule;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.LogicalTypeRoot;
import org.apache.calcite.rel.type.RelDataType;
import org.apache.calcite.rex.RexNode;
@@ -36,10 +38,14 @@ class CastConverter extends CustomizedConverter {
final RexNode child = context.toRexNode(call.getChildren().get(0));
final TypeLiteralExpression targetType = (TypeLiteralExpression)
call.getChildren().get(1);
+ final DataType inputType =
call.getResolvedChildren().get(0).getOutputDataType();
+ final DataType resultType =
+ inputType.getLogicalType().is(LogicalTypeRoot.VARIANT)
+ ? call.getOutputDataType()
+ : targetType.getOutputDataType();
final RelDataType targetRelDataType =
context.getTypeFactory()
- .createFieldTypeFromLogicalType(
-
targetType.getOutputDataType().getLogicalType());
+
.createFieldTypeFromLogicalType(resultType.getLogicalType());
return context.getRelBuilder()
.getRexBuilder()
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 09ada039cbb..e67bc261696 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
@@ -166,46 +166,52 @@ public class CastFunctionITCase extends
BuiltInFunctionTestBase {
private static List<TestSetSpec> variantPrimitiveCasts() {
return List.of(
TestSetSpec.forExpression("Cast a VARIANT produced by
parseJson() to a primitive")
- .onFieldsWithData("unused")
- .andDataTypes(STRING())
+ .onFieldsWithData("null")
+ .andDataTypes(STRING().notNull())
+ // A non-null VARIANT can contain JSON null.
+ .testResult(
+ $("f0").parseJson().cast(INT()),
+ "CAST(PARSE_JSON(f0) AS INT)",
+ null,
+ INT())
// An integer converts to any integer target while the
value stays in
// range, and to FLOAT or DOUBLE which are approximate
by definition.
.testResult(
lit("42").parseJson().cast(TINYINT()),
"CAST(PARSE_JSON('42') AS TINYINT)",
(byte) 42,
- TINYINT().notNull())
+ TINYINT())
.testResult(
lit("42").parseJson().cast(SMALLINT()),
"CAST(PARSE_JSON('42') AS SMALLINT)",
(short) 42,
- SMALLINT().notNull())
+ SMALLINT())
.testResult(
lit("42").parseJson().cast(INT()),
"CAST(PARSE_JSON('42') AS INT)",
42,
- INT().notNull())
+ INT())
.testResult(
lit("42").parseJson().cast(BIGINT()),
"CAST(PARSE_JSON('42') AS BIGINT)",
42L,
- BIGINT().notNull())
+ BIGINT())
.testResult(
lit("42").parseJson().cast(FLOAT()),
"CAST(PARSE_JSON('42') AS FLOAT)",
42.0f,
- FLOAT().notNull())
+ FLOAT())
.testResult(
lit("42").parseJson().cast(DOUBLE()),
"CAST(PARSE_JSON('42') AS DOUBLE)",
42.0d,
- DOUBLE().notNull())
+ DOUBLE())
// An out-of-range value is rejected rather than
wrapped.
.testResult(
lit("1000").parseJson().cast(SMALLINT()),
"CAST(PARSE_JSON('1000') AS SMALLINT)",
(short) 1000,
- SMALLINT().notNull())
+ SMALLINT())
.testTableApiRuntimeError(
lit("1000").parseJson().cast(TINYINT()),
"overflowed")
.testSqlRuntimeError("CAST(PARSE_JSON('1000') AS
TINYINT)", "overflowed")
@@ -219,7 +225,7 @@ public class CastFunctionITCase extends
BuiltInFunctionTestBase {
lit("7.0").parseJson().cast(INT()),
"CAST(PARSE_JSON('7.0') AS INT)",
7,
- INT().notNull())
+ INT())
// A fractional value is rejected, since converting
would drop digits.
.testTableApiRuntimeError(
lit("123.456").parseJson().cast(INT()), "lose
precision")
@@ -233,7 +239,7 @@ public class CastFunctionITCase extends
BuiltInFunctionTestBase {
lit("123.456").parseJson().cast(DECIMAL(6, 3)),
"CAST(PARSE_JSON('123.456') AS DECIMAL(6, 3))",
new BigDecimal("123.456"),
- DECIMAL(6, 3).notNull())
+ DECIMAL(6, 3))
.testTableApiRuntimeError(
lit("123.456").parseJson().cast(DECIMAL(6,
2)), "lose precision")
.testResult(
@@ -253,18 +259,18 @@ public class CastFunctionITCase extends
BuiltInFunctionTestBase {
lit("42").parseJson().cast(DECIMAL(5, 2)),
"CAST(PARSE_JSON('42') AS DECIMAL(5, 2))",
new BigDecimal("42.00"),
- DECIMAL(5, 2).notNull())
+ DECIMAL(5, 2))
// A decimal reaches an approximate target, where
losing digits is expected.
.testResult(
lit("123.456").parseJson().cast(FLOAT()),
"CAST(PARSE_JSON('123.456') AS FLOAT)",
123.456f,
- FLOAT().notNull())
+ FLOAT())
.testResult(
lit("123.456").parseJson().cast(DOUBLE()),
"CAST(PARSE_JSON('123.456') AS DOUBLE)",
123.456d,
- DOUBLE().notNull())
+ DOUBLE())
// A magnitude the target cannot represent is still
rejected.
.testTableApiRuntimeError(
lit("1e40").parseJson().cast(FLOAT()),
"overflowed")
@@ -277,37 +283,37 @@ public class CastFunctionITCase extends
BuiltInFunctionTestBase {
lit("1e20").parseJson().cast(DOUBLE()),
"CAST(PARSE_JSON('1e20') AS DOUBLE)",
1e20,
- DOUBLE().notNull())
+ DOUBLE())
.testResult(
lit("true").parseJson().cast(BOOLEAN()),
"CAST(PARSE_JSON('true') AS BOOLEAN)",
true,
- BOOLEAN().notNull())
+ BOOLEAN())
// CAST returns the raw scalar value (string unquoted)
.testResult(
lit("\"foo\"").parseJson().cast(STRING()),
"CAST(PARSE_JSON('\"foo\"') AS STRING)",
"foo",
- STRING().notNull())
+ STRING())
.testResult(
lit("123.456").parseJson().cast(STRING()),
"CAST(PARSE_JSON('123.456') AS STRING)",
"123.456",
- STRING().notNull())
+ STRING())
// The rendering matches a regular cast of the stored
kind, so a boolean
// becomes TRUE rather than the JSON true.
.testResult(
lit("true").parseJson().cast(STRING()),
"CAST(PARSE_JSON('true') AS STRING)",
"TRUE",
- STRING().notNull())
+ STRING())
// An object or array has no scalar form, so it
renders like a regular ARRAY
// or MAP to string cast, with strings unquoted at
every depth.
.testResult(
lit("[\"a\",
\"b\"]").parseJson().cast(STRING()),
"CAST(PARSE_JSON('[\"a\", \"b\"]') AS STRING)",
"[a, b]",
- STRING().notNull())
+ STRING())
.testResult(
lit("[\"a\",
\"b\"]").parseJson().tryCast(STRING()),
"TRY_CAST(PARSE_JSON('[\"a\", \"b\"]') AS
STRING)",
@@ -317,7 +323,7 @@ public class CastFunctionITCase extends
BuiltInFunctionTestBase {
lit("{\"a\": 1}").parseJson().cast(STRING()),
"CAST(PARSE_JSON('{\"a\": 1}') AS STRING)",
"{a=1}",
- STRING().notNull())
+ STRING())
.testResult(
lit("{\"a\":
1}").parseJson().tryCast(STRING()),
"TRY_CAST(PARSE_JSON('{\"a\": 1}') AS STRING)",
@@ -329,12 +335,12 @@ public class CastFunctionITCase extends
BuiltInFunctionTestBase {
lit("\"ab\"").parseJson().cast(VARCHAR(3)),
"CAST(PARSE_JSON('\"ab\"') AS VARCHAR(3))",
"ab",
- VARCHAR(3).notNull())
+ VARCHAR(3))
.testResult(
lit("\"foobar\"").parseJson().cast(VARCHAR(3)),
"CAST(PARSE_JSON('\"foobar\"') AS VARCHAR(3))",
"foo",
- VARCHAR(3).notNull())
+ VARCHAR(3))
.testResult(
lit("\"foobar\"").parseJson().tryCast(VARCHAR(3)),
"TRY_CAST(PARSE_JSON('\"foobar\"') AS
VARCHAR(3))",
@@ -344,17 +350,17 @@ public class CastFunctionITCase extends
BuiltInFunctionTestBase {
lit("\"abc\"").parseJson().cast(CHAR(3)),
"CAST(PARSE_JSON('\"abc\"') AS CHAR(3))",
"abc",
- CHAR(3).notNull())
+ CHAR(3))
.testResult(
lit("\"abcdef\"").parseJson().cast(CHAR(3)),
"CAST(PARSE_JSON('\"abcdef\"') AS CHAR(3))",
"abc",
- CHAR(3).notNull())
+ CHAR(3))
.testResult(
lit("\"ab\"").parseJson().cast(CHAR(5)),
"CAST(PARSE_JSON('\"ab\"') AS CHAR(5))",
"ab ",
- CHAR(5).notNull())
+ CHAR(5))
.testResult(
lit("\"ab\"").parseJson().tryCast(CHAR(5)),
"TRY_CAST(PARSE_JSON('\"ab\"') AS CHAR(5))",
@@ -366,19 +372,19 @@ public class CastFunctionITCase extends
BuiltInFunctionTestBase {
lit("[\"a\", null,
1]").parseJson().cast(STRING()),
"CAST(PARSE_JSON('[\"a\", null, 1]') AS
STRING)",
"[a, NULL, 1]",
- STRING().notNull())
+ STRING())
.testResult(
lit("{\"k\": [\"a\",
\"b\"]}").parseJson().cast(STRING()),
"CAST(PARSE_JSON('{\"k\": [\"a\", \"b\"]}') AS
STRING)",
"{k=[a, b]}",
- STRING().notNull())
+ STRING())
// A container renders in full and is then trimmed to
a bounded target, the
// same as any other value longer than the target.
.testResult(
lit("[1, 2, 3]").parseJson().cast(VARCHAR(5)),
"CAST(PARSE_JSON('[1, 2, 3]') AS VARCHAR(5))",
"[1, 2",
- VARCHAR(5).notNull())
+ VARCHAR(5))
// A variant holding a JSON null casts to SQL NULL,
not to the text 'null'.
// The length of that text must not be checked against
the target either.
.testResult(
@@ -407,8 +413,7 @@ public class CastFunctionITCase extends
BuiltInFunctionTestBase {
"TRY_CAST(PARSE_JSON('\"foo\"') AS INT)",
null,
INT())
- // A variant that stores a JSON null casts to SQL NULL
when the target is
- // nullable: a nullable variant source, or TRY_CAST
which forces nullable.
+ // A variant that stores a JSON null casts to SQL NULL.
.testResult(
lit("null").tryParseJson().cast(INT()),
"CAST(TRY_PARSE_JSON('null') AS INT)",
@@ -430,43 +435,49 @@ public class CastFunctionITCase extends
BuiltInFunctionTestBase {
private static List<TestSetSpec> variantArrayCasts() {
return List.of(
TestSetSpec.forExpression("Cast a VARIANT produced by
parseJson() to an ARRAY")
- .onFieldsWithData("unused")
- .andDataTypes(STRING())
+ .onFieldsWithData("null")
+ .andDataTypes(STRING().notNull())
+ // A non-null VARIANT can contain JSON null.
+ .testResult(
+
$("f0").parseJson().cast(ARRAY(INT().notNull())),
+ "CAST(PARSE_JSON(f0) AS ARRAY<INT NOT NULL>)",
+ null,
+ ARRAY(INT().notNull()))
// ARRAY: each element casts by the same
VARIANT-to-element rule.
.testResult(
lit("[1, 2,
3]").parseJson().cast(ARRAY(INT())),
"CAST(PARSE_JSON('[1, 2, 3]') AS ARRAY<INT>)",
new Integer[] {1, 2, 3},
- ARRAY(INT()).notNull())
+ ARRAY(INT()))
// an approximate leaf takes any numeric kind
.testResult(
lit("[1, 2,
3]").parseJson().cast(ARRAY(DOUBLE())),
"CAST(PARSE_JSON('[1, 2, 3]') AS
ARRAY<DOUBLE>)",
new Double[] {1.0, 2.0, 3.0},
- ARRAY(DOUBLE()).notNull())
+ ARRAY(DOUBLE()))
// each element renders to string like the scalar cast
.testResult(
lit("[1, 2,
3]").parseJson().cast(ARRAY(STRING())),
"CAST(PARSE_JSON('[1, 2, 3]') AS
ARRAY<STRING>)",
new String[] {"1", "2", "3"},
- ARRAY(STRING()).notNull())
+ ARRAY(STRING()))
// a heterogeneous array renders every element to
string
.testResult(
lit("[1, \"a\", 2,
\"b\"]").parseJson().cast(ARRAY(STRING())),
"CAST(PARSE_JSON('[1, \"a\", 2, \"b\"]') AS
ARRAY<STRING>)",
new String[] {"1", "a", "2", "b"},
- ARRAY(STRING()).notNull())
+ ARRAY(STRING()))
.testResult(
lit("[]").parseJson().cast(ARRAY(INT())),
"CAST(PARSE_JSON('[]') AS ARRAY<INT>)",
new Integer[] {},
- ARRAY(INT()).notNull())
+ ARRAY(INT()))
// a VARIANT null element maps to SQL NULL for a
nullable element type
.testResult(
lit("[1, null,
3]").parseJson().cast(ARRAY(INT())),
"CAST(PARSE_JSON('[1, null, 3]') AS
ARRAY<INT>)",
new Integer[] {1, null, 3},
- ARRAY(INT()).notNull())
+ ARRAY(INT()))
// a VARIANT null element fails a NOT NULL element type
.testTableApiRuntimeError(
lit("[1, null,
3]").parseJson().cast(ARRAY(INT().notNull())),
@@ -518,13 +529,13 @@ public class CastFunctionITCase extends
BuiltInFunctionTestBase {
.cast(ARRAY(INT())),
"CAST(CAST(PARSE_JSON('[1, 2, 3]') AS
ARRAY<VARIANT>) AS ARRAY<INT>)",
new Integer[] {1, 2, 3},
- ARRAY(INT()).notNull())
+ ARRAY(INT()))
// the recursion composes for a nested array of arrays
.testResult(
lit("[[1, 2],
[3]]").parseJson().cast(ARRAY(ARRAY(INT()))),
"CAST(PARSE_JSON('[[1, 2], [3]]') AS
ARRAY<ARRAY<INT>>)",
new Integer[][] {{1, 2}, {3}},
- ARRAY(ARRAY(INT())).notNull())
+ ARRAY(ARRAY(INT())))
// a top-level VARIANT null casts to SQL NULL for a
nullable target
.testResult(
lit("null").tryParseJson().cast(ARRAY(INT())),
@@ -593,21 +604,27 @@ public class CastFunctionITCase extends
BuiltInFunctionTestBase {
"{\"user\": {\"id\": 1, \"since\": \"2020-01-01\"}, \"tags\":
[\"x\", \"y\"]}";
return List.of(
TestSetSpec.forExpression("Cast a VARIANT produced by
parseJson() to a ROW")
- .onFieldsWithData("unused")
- .andDataTypes(STRING())
+ .onFieldsWithData("null")
+ .andDataTypes(STRING().notNull())
+ // A non-null VARIANT can contain JSON null.
+ .testResult(
+ $("f0").parseJson().cast(ROW(FIELD("id",
INT().notNull()))),
+ "CAST(PARSE_JSON(f0) AS ROW<`id` INT NOT
NULL>)",
+ null,
+ ROW(FIELD("id", INT().notNull())))
// ROW: fields match by name, order is free
.testResult(
lit(obj).parseJson()
.cast(ROW(FIELD("id", INT()),
FIELD("name", STRING()))),
"CAST(PARSE_JSON('" + obj + "') AS ROW<`id`
INT, `name` STRING>)",
Row.of(7, "ada"),
- ROW(FIELD("id", INT()), FIELD("name",
STRING())).notNull())
+ ROW(FIELD("id", INT()), FIELD("name",
STRING())))
.testResult(
lit(obj).parseJson()
.cast(ROW(FIELD("name", STRING()),
FIELD("id", INT()))),
"CAST(PARSE_JSON('" + obj + "') AS ROW<`name`
STRING, `id` INT>)",
Row.of("ada", 7),
- ROW(FIELD("name", STRING()), FIELD("id",
INT())).notNull())
+ ROW(FIELD("name", STRING()), FIELD("id",
INT())))
// a field absent from the object fails the cast
.testSqlRuntimeError(
"CAST(PARSE_JSON('"
@@ -624,7 +641,7 @@ public class CastFunctionITCase extends
BuiltInFunctionTestBase {
+ objNull
+ "') AS ROW<`id` INT, `name`
STRING>)",
Row.of(7, null),
- ROW(FIELD("id", INT()), FIELD("name",
STRING())).notNull())
+ ROW(FIELD("id", INT()), FIELD("name",
STRING())))
// and fails when that field is NOT NULL
.testSqlRuntimeError(
"CAST(PARSE_JSON('"
@@ -637,7 +654,7 @@ public class CastFunctionITCase extends
BuiltInFunctionTestBase {
lit(obj).parseJson().cast(ROW(FIELD("id",
INT()))),
"CAST(PARSE_JSON('" + obj + "') AS ROW<`id`
INT>)",
Row.of(7),
- ROW(FIELD("id", INT())).notNull())
+ ROW(FIELD("id", INT())))
// an array is not an object
.testTableApiRuntimeError(
lit("[1, 2,
3]").parseJson().cast(ROW(FIELD("id", INT()))),
@@ -653,7 +670,7 @@ public class CastFunctionITCase extends
BuiltInFunctionTestBase {
+ "') AS ROW<`id` VARIANT, `name`
VARIANT>)"
+ " AS ROW<`id` INT, `name` STRING>)",
Row.of(7, "ada"),
- ROW(FIELD("id", INT()), FIELD("name",
STRING())).notNull())
+ ROW(FIELD("id", INT()), FIELD("name",
STRING())))
// a variant null field is kept as a variant null, so
casting it back to a
// concrete nullable type yields SQL NULL
.testResult(
@@ -666,7 +683,7 @@ public class CastFunctionITCase extends
BuiltInFunctionTestBase {
+ "') AS ROW<`id` VARIANT, `name`
VARIANT>)"
+ " AS ROW<`id` INT, `name` STRING>)",
Row.of(7, null),
- ROW(FIELD("id", INT()), FIELD("name",
STRING())).notNull())
+ ROW(FIELD("id", INT()), FIELD("name",
STRING())))
// the recursion composes for nested rows and arrays
.testResult(
lit(nested)
@@ -685,13 +702,10 @@ public class CastFunctionITCase extends
BuiltInFunctionTestBase {
+ " `tags` ARRAY<STRING>>)",
Row.of(Row.of(1, "2020-01-01"), new String[]
{"x", "y"}),
ROW(
- FIELD(
- "user",
- ROW(
- FIELD("id",
INT()),
- FIELD("since",
STRING()))),
- FIELD("tags", ARRAY(STRING())))
- .notNull()));
+ FIELD(
+ "user",
+ ROW(FIELD("id", INT()),
FIELD("since", STRING()))),
+ FIELD("tags", ARRAY(STRING())))));
}
private static List<TestSetSpec> variantMapCasts() {
@@ -700,8 +714,14 @@ public class CastFunctionITCase extends
BuiltInFunctionTestBase {
final String mixed = "{\"a\": 1, \"b\": \"x\"}";
return List.of(
TestSetSpec.forExpression("Cast a VARIANT produced by
parseJson() to a MAP")
- .onFieldsWithData("unused")
- .andDataTypes(STRING())
+ .onFieldsWithData("null")
+ .andDataTypes(STRING().notNull())
+ // A non-null VARIANT can contain JSON null.
+ .testResult(
+ $("f0").parseJson().cast(MAP(STRING(),
INT().notNull())),
+ "CAST(PARSE_JSON(f0) AS MAP<STRING, INT NOT
NULL>)",
+ null,
+ MAP(STRING(), INT().notNull()))
// MAP: each field name becomes a key, each value
casts to V
.testResult(
lit(obj).parseJson().cast(MAP(STRING(),
STRING())),
@@ -710,18 +730,18 @@ public class CastFunctionITCase extends
BuiltInFunctionTestBase {
entry("id", "7"),
entry("name", "ada"),
entry("active", "TRUE")),
- MAP(STRING(), STRING()).notNull())
+ MAP(STRING(), STRING()))
.testResult(
lit("{}").parseJson().cast(MAP(STRING(),
INT())),
"CAST(PARSE_JSON('{}') AS MAP<STRING, INT>)",
map(),
- MAP(STRING(), INT()).notNull())
+ MAP(STRING(), INT()))
// a value present but set to a variant null maps to
SQL NULL when nullable
.testResult(
lit(objNull).parseJson().cast(MAP(STRING(),
STRING())),
"CAST(PARSE_JSON('" + objNull + "') AS
MAP<STRING, STRING>)",
map(entry("id", "7"), entry("email", null)),
- MAP(STRING(), STRING()).notNull())
+ MAP(STRING(), STRING()))
// and fails when the value type is NOT NULL
.testTableApiRuntimeError(
lit(objNull).parseJson().cast(MAP(STRING(),
STRING().notNull())),