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 5d17b29d312 [FLINK-40910][table] Reject `VARIANT` where a query needs
equality or ordering
5d17b29d312 is described below
commit 5d17b29d3122476690d657f390e6df56bee57153
Author: Ramin Gharib <[email protected]>
AuthorDate: Thu Oct 8 15:02:14 2026 +0200
[FLINK-40910][table] Reject `VARIANT` where a query needs equality or
ordering
---
docs/content.zh/docs/sql/reference/data-types.md | 30 +-
docs/content/docs/sql/reference/data-types.md | 30 +-
.../types/logical/utils/LogicalTypeChecks.java | 11 +
.../types/logical/utils/LogicalTypeChecksTest.java | 33 ++
.../planner/plan/optimize/KeyTypeValidator.java | 275 ++++++++++++++
.../optimize/CommonSubGraphBasedOptimizer.scala | 3 +
.../nodes/exec/stream/VariantSemanticTest.java | 149 ++++++--
.../plan/optimize/KeyTypeValidatorTest.java | 399 +++++++++++++++++++++
8 files changed, 899 insertions(+), 31 deletions(-)
diff --git a/docs/content.zh/docs/sql/reference/data-types.md
b/docs/content.zh/docs/sql/reference/data-types.md
index e5344621c3f..04accbeb996 100644
--- a/docs/content.zh/docs/sql/reference/data-types.md
+++ b/docs/content.zh/docs/sql/reference/data-types.md
@@ -1705,8 +1705,8 @@ A scalar value can also be cast to a `VARIANT` with
`CAST` or `TRY_CAST`. Only a
- A constructed type casts element by element when its element, field, or
value type casts to
`VARIANT`, for example `ARRAY<INT>` to `ARRAY<VARIANT>`. A `NULL` element
stays a SQL `NULL`. A
whole `ARRAY`, `MAP`, or `ROW` does not cast into a single `VARIANT` yet.
-- Two `VARIANT` values are equal only when their binary encodings match. So as
a `MAP` key or a
- `MULTISET` element, a `1` cast from `INT` does not match a `1` cast from
`BIGINT` or parsed by
+- As a `MAP` key or a `MULTISET` element, two `VARIANT` values match only when
their binary
+ encodings match. So a `1` cast from `INT` does not match a `1` cast from
`BIGINT` or parsed by
`PARSE_JSON('1')`.
```sql
@@ -1720,6 +1720,32 @@ CAST(ARRAY[1, NULL] AS ARRAY<VARIANT>) -- [1,
NULL], each element a VARIAN
CAST(ARRAY[1, 2] AS VARIANT) -- fails at validation, not
supported yet
```
+A `VARIANT` has no equality and no order, because one value has many valid
binary encodings. The
+objects `{"a": 1, "b": 2}` and `{"b": 2, "a": 1}` hold the same value, and so
do a `1` stored as a
+`TINYINT` and a `1` stored as a `BIGINT`. So a query cannot use a `VARIANT`,
or a type that contains
+one, where it compares values:
+
+- as a grouping key, including `GROUPING SETS`, window aggregations, and
`SELECT DISTINCT`
+- as the argument of a `DISTINCT` aggregate, such as `COUNT(DISTINCT v)`
+- in `UNION`, `INTERSECT`, `INTERSECT ALL`, `EXCEPT`, and `EXCEPT ALL`, while
`UNION ALL` is allowed
+- as an `ORDER BY` key
+- as a `PARTITION BY` or `ORDER BY` key of an `OVER` window, of
`MATCH_RECOGNIZE`, or of a table
+ argument
+- as an operand of a comparison such as `=`, `<>`, `<`, `IS DISTINCT FROM`, or
`IN`, which includes
+ join conditions
+
+Such a query fails before it runs. Cast the value to a concrete type first, or
use `TRY_CAST` when
+some values do not fit the type. A `VARIANT` can still be selected, read with
field access, checked
+with `IS NULL`, cast, and passed to aggregate functions such as `COUNT`,
`FIRST_VALUE`, and
+`LAST_VALUE`.
+
+```sql
+SELECT v['a'], COUNT(*) FROM t GROUP BY v['a'] --
fails
+SELECT CAST(v['a'] AS INT), COUNT(*) FROM t GROUP BY CAST(v['a'] AS INT) --
one group per number
+SELECT * FROM t WHERE v['a'] = PARSE_JSON('1') --
fails
+SELECT * FROM t WHERE CAST(v['a'] AS INT) = 1 --
compares the number
+```
+
**Declaration**
{{< tabs "25c30432-8460-441d-a036-9416d8202882" >}}
diff --git a/docs/content/docs/sql/reference/data-types.md
b/docs/content/docs/sql/reference/data-types.md
index c80cb694fc2..66dba8e499d 100644
--- a/docs/content/docs/sql/reference/data-types.md
+++ b/docs/content/docs/sql/reference/data-types.md
@@ -1715,8 +1715,8 @@ A scalar value can also be cast to a `VARIANT` with
`CAST` or `TRY_CAST`. Only a
- A constructed type casts element by element when its element, field, or
value type casts to
`VARIANT`, for example `ARRAY<INT>` to `ARRAY<VARIANT>`. A `NULL` element
stays a SQL `NULL`. A
whole constructed value can also become a single `VARIANT`, see below.
-- Two `VARIANT` values are equal only when their binary encodings match. So as
a `MAP` key or a
- `MULTISET` element, a `1` cast from `INT` does not match a `1` cast from
`BIGINT` or parsed by
+- As a `MAP` key or a `MULTISET` element, two `VARIANT` values match only when
their binary
+ encodings match. So a `1` cast from `INT` does not match a `1` cast from
`BIGINT` or parsed by
`PARSE_JSON('1')`.
```sql
@@ -1763,6 +1763,32 @@ CAST(MAP[1, 'a'] AS VARIANT) -- fails at
validation, a MAP key m
CAST(ARRAY[INTERVAL '1' DAY] AS VARIANT) -- fails at validation
```
+A `VARIANT` has no equality and no order, because one value has many valid
binary encodings. The
+objects `{"a": 1, "b": 2}` and `{"b": 2, "a": 1}` hold the same value, and so
do a `1` stored as a
+`TINYINT` and a `1` stored as a `BIGINT`. So a query cannot use a `VARIANT`,
or a type that contains
+one, where it compares values:
+
+- as a grouping key, including `GROUPING SETS`, window aggregations, and
`SELECT DISTINCT`
+- as the argument of a `DISTINCT` aggregate, such as `COUNT(DISTINCT v)`
+- in `UNION`, `INTERSECT`, `INTERSECT ALL`, `EXCEPT`, and `EXCEPT ALL`, while
`UNION ALL` is allowed
+- as an `ORDER BY` key
+- as a `PARTITION BY` or `ORDER BY` key of an `OVER` window, of
`MATCH_RECOGNIZE`, or of a table
+ argument
+- as an operand of a comparison such as `=`, `<>`, `<`, `IS DISTINCT FROM`, or
`IN`, which includes
+ join conditions
+
+Such a query fails before it runs. Cast the value to a concrete type first, or
use `TRY_CAST` when
+some values do not fit the type. A `VARIANT` can still be selected, read with
field access, checked
+with `IS NULL`, cast, and passed to aggregate functions such as `COUNT`,
`FIRST_VALUE`, and
+`LAST_VALUE`.
+
+```sql
+SELECT v['a'], COUNT(*) FROM t GROUP BY v['a'] --
fails
+SELECT CAST(v['a'] AS INT), COUNT(*) FROM t GROUP BY CAST(v['a'] AS INT) --
one group per number
+SELECT * FROM t WHERE v['a'] = PARSE_JSON('1') --
fails
+SELECT * FROM t WHERE CAST(v['a'] AS INT) = 1 --
compares the number
+```
+
**Declaration**
{{< tabs "25c30432-8460-441d-a036-9416d8202882" >}}
diff --git
a/flink-table/flink-table-common/src/main/java/org/apache/flink/table/types/logical/utils/LogicalTypeChecks.java
b/flink-table/flink-table-common/src/main/java/org/apache/flink/table/types/logical/utils/LogicalTypeChecks.java
index 9c1a791472a..af66e5edc07 100644
---
a/flink-table/flink-table-common/src/main/java/org/apache/flink/table/types/logical/utils/LogicalTypeChecks.java
+++
b/flink-table/flink-table-common/src/main/java/org/apache/flink/table/types/logical/utils/LogicalTypeChecks.java
@@ -257,6 +257,17 @@ public final class LogicalTypeChecks {
firstType.copy(true), secondType.copy(true),
requiredComparison);
}
+ /**
+ * Checks whether a (possibly nested) logical type contains no VARIANT,
which every grouping,
+ * join, partition and sort key and every operand of a comparison operator
needs.
+ *
+ * <p>A VARIANT value has many valid binary encodings, so two equal values
can differ in their
+ * bytes.
+ */
+ public static boolean isComparableKeyType(LogicalType logicalType) {
+ return !hasNested(logicalType, t -> t.is(LogicalTypeRoot.VARIANT));
+ }
+
private static boolean areComparableWithNormalizedNullability(
LogicalType firstType,
LogicalType secondType,
diff --git
a/flink-table/flink-table-common/src/test/java/org/apache/flink/table/types/logical/utils/LogicalTypeChecksTest.java
b/flink-table/flink-table-common/src/test/java/org/apache/flink/table/types/logical/utils/LogicalTypeChecksTest.java
index d0ede0f0fd9..ca9775be893 100644
---
a/flink-table/flink-table-common/src/test/java/org/apache/flink/table/types/logical/utils/LogicalTypeChecksTest.java
+++
b/flink-table/flink-table-common/src/test/java/org/apache/flink/table/types/logical/utils/LogicalTypeChecksTest.java
@@ -30,14 +30,21 @@ import org.apache.flink.table.types.logical.StructuredType;
import org.apache.flink.table.types.utils.TypeConversions;
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.util.Arrays;
import java.util.List;
+import java.util.stream.Stream;
+import static org.apache.flink.table.api.DataTypes.ARRAY;
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.MAP;
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.VARIANT;
import static org.assertj.core.api.Assertions.assertThat;
/** Tests for {@link LogicalTypeChecks}. */
@@ -133,4 +140,30 @@ class LogicalTypeChecksTest {
DataType dataType = ROW(FIELD("f0", INT()), FIELD("f1", STRING()));
assertThat(LogicalTypeChecks.getFieldCount(dataType.getLogicalType())).isEqualTo(2);
}
+
+ @ParameterizedTest(name = "{0}")
+ @MethodSource("comparableKeyTypes")
+ void testIsComparableKeyType(DataType dataType, boolean expected) {
+
assertThat(LogicalTypeChecks.isComparableKeyType(dataType.getLogicalType()))
+ .isEqualTo(expected);
+ }
+
+ private static Stream<Arguments> comparableKeyTypes() {
+ return Stream.of(
+ Arguments.of(INT(), true),
+ Arguments.of(ROW(FIELD("f0", INT())), true),
+ Arguments.of(VARIANT(), false),
+ Arguments.of(ROW(FIELD("f0", VARIANT())), false),
+ Arguments.of(ARRAY(VARIANT()), false),
+ Arguments.of(MAP(STRING(), VARIANT()), false),
+ Arguments.of(
+ TypeConversions.fromLogicalToDataType(
+
StructuredType.newBuilder(ObjectIdentifier.of("cat", "db", "type"))
+ .attributes(
+ List.of(
+ new
StructuredType.StructuredAttribute(
+ "v",
VARIANT().getLogicalType())))
+ .build()),
+ false));
+ }
}
diff --git
a/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/plan/optimize/KeyTypeValidator.java
b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/plan/optimize/KeyTypeValidator.java
new file mode 100644
index 00000000000..bf3cfb57747
--- /dev/null
+++
b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/plan/optimize/KeyTypeValidator.java
@@ -0,0 +1,275 @@
+/*
+ * 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.plan.optimize;
+
+import org.apache.flink.table.api.ValidationException;
+import org.apache.flink.table.planner.calcite.FlinkTypeFactory;
+import org.apache.flink.table.planner.calcite.RexTableArgCall;
+import org.apache.flink.table.planner.plan.nodes.calcite.TableAggregate;
+import org.apache.flink.table.planner.plan.nodes.calcite.WatermarkAssigner;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.utils.LogicalTypeChecks;
+
+import org.apache.calcite.rel.RelCollation;
+import org.apache.calcite.rel.RelFieldCollation;
+import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.RelVisitor;
+import org.apache.calcite.rel.core.Aggregate;
+import org.apache.calcite.rel.core.AggregateCall;
+import org.apache.calcite.rel.core.Match;
+import org.apache.calcite.rel.core.SetOp;
+import org.apache.calcite.rel.core.Sort;
+import org.apache.calcite.rel.core.Union;
+import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.rel.type.RelDataTypeField;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexFieldCollation;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexOver;
+import org.apache.calcite.rex.RexShuttle;
+import org.apache.calcite.rex.RexSubQuery;
+import org.apache.calcite.rex.RexUtil;
+import org.apache.calcite.sql.SqlKind;
+import org.apache.calcite.sql.SqlUtil;
+import org.apache.calcite.sql.fun.SqlQuantifyOperator;
+import org.apache.calcite.util.ImmutableBitSet;
+
+import javax.annotation.Nullable;
+
+import java.util.List;
+
+/**
+ * Rejects queries that group, join, partition, sort or compare values of a
type that is not a
+ * comparable key type, see {@link
LogicalTypeChecks#isComparableKeyType(LogicalType)}.
+ *
+ * <p>The check runs on the logical plan before optimization. Keys that the
optimizer adds later
+ * only match a value with copies of itself and need no check.
+ */
+public final class KeyTypeValidator {
+
+ private static final String EQUALITY = "equality";
+ private static final String ORDER = "order";
+
+ private KeyTypeValidator() {}
+
+ /**
+ * Throws a {@link ValidationException} if a plan groups, joins,
partitions, sorts or compares
+ * values of a type that is not a comparable key type.
+ */
+ public static void validate(final List<RelNode> roots) {
+ roots.forEach(root -> new PlanChecker().go(root));
+ }
+
+ private static final class PlanChecker extends RelVisitor {
+
+ private final RexShuttle expressionChecker = new ExpressionChecker();
+
+ @Override
+ public void visit(final RelNode node, final int ordinal, @Nullable
final RelNode parent) {
+ checkKeys(node);
+ node.accept(expressionChecker);
+ super.visit(node, ordinal, parent);
+ }
+
+ private void checkKeys(final RelNode node) {
+ if (node instanceof Aggregate) {
+ final Aggregate aggregate = (Aggregate) node;
+ final RelDataType inputType =
aggregate.getInput().getRowType();
+ checkFields(inputType, aggregate.getGroupSet(), "as a grouping
key", EQUALITY);
+ for (final AggregateCall call : aggregate.getAggCallList()) {
+ if (call.isDistinct()) {
+ checkFields(
+ inputType,
+ ImmutableBitSet.of(call.getArgList()),
+ "as an argument of a DISTINCT aggregate",
+ EQUALITY);
+ }
+ }
+ } else if (node instanceof TableAggregate) {
+ final TableAggregate aggregate = (TableAggregate) node;
+ checkFields(
+ aggregate.getInput().getRowType(),
+ aggregate.getGroupSet(),
+ "as a grouping key",
+ EQUALITY);
+ } else if (node instanceof SetOp) {
+ final SetOp setOp = (SetOp) node;
+ if (!(setOp instanceof Union && setOp.all)) {
+ final RelDataType rowType = setOp.getRowType();
+ checkFields(
+ rowType,
+ ImmutableBitSet.range(rowType.getFieldCount()),
+ "in " + setOp.kind.sql + (setOp.all ? " ALL" : ""),
+ EQUALITY);
+ }
+ } else if (node instanceof Sort) {
+ final Sort sort = (Sort) node;
+ checkCollation(
+ sort.getInput().getRowType(), sort.getCollation(), "as
an ORDER BY key");
+ } else if (node instanceof Match) {
+ // Match and WatermarkAssigner do not expose their expressions
to a RexShuttle
+ final Match match = (Match) node;
+ final RelDataType inputType = match.getInput().getRowType();
+ checkFields(
+ inputType,
+ match.getPartitionKeys(),
+ "as a PARTITION BY key of MATCH_RECOGNIZE",
+ EQUALITY);
+ checkCollation(
+ inputType, match.getOrderKeys(), "as an ORDER BY key
of MATCH_RECOGNIZE");
+ match.getPatternDefinitions().values().forEach(e ->
e.accept(expressionChecker));
+ match.getMeasures().values().forEach(e ->
e.accept(expressionChecker));
+ } else if (node instanceof WatermarkAssigner) {
+ ((WatermarkAssigner)
node).watermarkExpr().accept(expressionChecker);
+ }
+ }
+
+ private void checkCollation(
+ final RelDataType rowType, final RelCollation collation, final
String usage) {
+ for (final RelFieldCollation fieldCollation :
collation.getFieldCollations()) {
+ checkField(rowType, fieldCollation.getFieldIndex(), usage,
ORDER);
+ }
+ }
+ }
+
+ private static final class ExpressionChecker extends RexShuttle {
+
+ @Override
+ public RexNode visitCall(final RexCall call) {
+ if (call instanceof RexTableArgCall) {
+ final RexTableArgCall tableArg = (RexTableArgCall) call;
+ checkFields(
+ tableArg.getType(),
+ ImmutableBitSet.of(tableArg.getPartitionKeys()),
+ "as a PARTITION BY key of a table argument",
+ EQUALITY);
+ checkFields(
+ tableArg.getType(),
+ ImmutableBitSet.of(tableArg.getOrderKeys()),
+ "as an ORDER BY key of a table argument",
+ ORDER);
+ } else if (call.isA(SqlKind.BINARY_COMPARISON) ||
call.isA(SqlKind.COMPARISON)) {
+ checkComparison(
+ call.getOperands(),
+ call.getOperator().getName(),
+ call.isA(SqlKind.ORDER_COMPARISON) ? ORDER : EQUALITY);
+ }
+ return super.visitCall(call);
+ }
+
+ @Override
+ public RexNode visitOver(final RexOver over) {
+ if (over.isDistinct()) {
+ for (final RexNode operand : over.getOperands()) {
+ checkType(
+ "An expression",
+ operand.getType(),
+ "as an argument of a DISTINCT aggregate",
+ EQUALITY);
+ }
+ }
+ for (final RexNode key : over.getWindow().partitionKeys) {
+ checkType(
+ "An expression",
+ key.getType(),
+ "as a PARTITION BY key of an OVER window",
+ EQUALITY);
+ }
+ for (final RexFieldCollation key : over.getWindow().orderKeys) {
+ checkType(
+ "An expression",
+ key.getKey().getType(),
+ "as an ORDER BY key of an OVER window",
+ ORDER);
+ }
+ return super.visitOver(over);
+ }
+
+ @Override
+ public RexNode visitSubQuery(final RexSubQuery subQuery) {
+ if (subQuery.isA(SqlKind.IN) || subQuery.isA(SqlKind.NOT_IN)) {
+ checkComparison(subQuery.getOperands(),
subQuery.getOperator().getName(), EQUALITY);
+ } else if (subQuery.getOperator() instanceof SqlQuantifyOperator) {
+ final SqlKind comparisonKind =
+ ((SqlQuantifyOperator)
subQuery.getOperator()).comparisonKind;
+ checkComparison(
+ subQuery.getOperands(),
+ subQuery.getOperator().getName(),
+ SqlKind.ORDER_COMPARISON.contains(comparisonKind) ?
ORDER : EQUALITY);
+ }
+ new PlanChecker().go(subQuery.rel);
+ return super.visitSubQuery(subQuery);
+ }
+
+ private static void checkComparison(
+ final List<RexNode> operands, final String operatorName, final
String requirement) {
+ // a comparison with NULL never compares two values
+ if (operands.stream().anyMatch(operand ->
RexUtil.isNullLiteral(operand, true))) {
+ return;
+ }
+ for (final RexNode operand : operands) {
+ checkType(
+ "An expression",
+ operand.getType(),
+ "in a comparison with '" + operatorName + "'",
+ requirement);
+ }
+ }
+ }
+
+ private static void checkFields(
+ final RelDataType rowType,
+ final ImmutableBitSet fields,
+ final String usage,
+ final String requirement) {
+ for (final int field : fields) {
+ checkField(rowType, field, usage, requirement);
+ }
+ }
+
+ private static void checkField(
+ final RelDataType rowType,
+ final int index,
+ final String usage,
+ final String requirement) {
+ final RelDataTypeField field = rowType.getFieldList().get(index);
+ final String name = field.getName();
+ // names such as EXPR$0 or $f0 come from the planner and mean nothing
to the user
+ final String subject =
+ name.startsWith("$") || SqlUtil.isGeneratedAlias(name)
+ ? "An expression"
+ : String.format("Column '%s'", name);
+ checkType(subject, field.getType(), usage, requirement);
+ }
+
+ private static void checkType(
+ final String subject,
+ final RelDataType type,
+ final String usage,
+ final String requirement) {
+ final LogicalType logicalType = FlinkTypeFactory.toLogicalType(type);
+ if (!LogicalTypeChecks.isComparableKeyType(logicalType)) {
+ throw new ValidationException(
+ String.format(
+ "%s of type %s cannot be used %s, because the type
has no %s. "
+ + "Cast the value to a comparable type
first.",
+ subject, logicalType.copy(true).asSummaryString(),
usage, requirement));
+ }
+ }
+}
diff --git
a/flink-table/flink-table-planner/src/main/scala/org/apache/flink/table/planner/plan/optimize/CommonSubGraphBasedOptimizer.scala
b/flink-table/flink-table-planner/src/main/scala/org/apache/flink/table/planner/plan/optimize/CommonSubGraphBasedOptimizer.scala
index 92c7f316ae0..d0eb2603dee 100644
---
a/flink-table/flink-table-planner/src/main/scala/org/apache/flink/table/planner/plan/optimize/CommonSubGraphBasedOptimizer.scala
+++
b/flink-table/flink-table-planner/src/main/scala/org/apache/flink/table/planner/plan/optimize/CommonSubGraphBasedOptimizer.scala
@@ -76,6 +76,9 @@ abstract class CommonSubGraphBasedOptimizer extends Optimizer
{
* a list of RelNode represents an optimized RelNode DAG.
*/
override def optimize(roots: Seq[RelNode]): Seq[RelNode] = {
+ // validate before the optimizer rewrites the keys that the query asks for
+ KeyTypeValidator.validate(toJava(roots))
+
// resolve hints before optimizing
val queryHintsResolver = new QueryHintsResolver()
val resolvedHintRoots = queryHintsResolver.resolve(toJava(roots))
diff --git
a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/VariantSemanticTest.java
b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/VariantSemanticTest.java
index 86e5b660210..c769e326f25 100644
---
a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/VariantSemanticTest.java
+++
b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/VariantSemanticTest.java
@@ -145,16 +145,16 @@ public class VariantSemanticTest extends SemanticTestBase
{
.build())
.setupTableSink(
SinkTestStep.newBuilder("sink_t")
- .addSchema("fv VARIANT", "lv VARIANT", "c
BIGINT", "dc BIGINT")
+ .addSchema("fv VARIANT", "lv VARIANT", "c
BIGINT")
.consumedValues(
- Row.of(V1, V1, 1, 1),
- Row.ofKind(RowKind.UPDATE_BEFORE,
V1, V1, 1, 1),
- Row.ofKind(RowKind.UPDATE_AFTER,
V1, V2, 2, 2),
- Row.ofKind(RowKind.UPDATE_BEFORE,
V1, V2, 2, 2),
- Row.ofKind(RowKind.UPDATE_AFTER,
V1, V2, 3, 2))
+ Row.of(V1, V1, 1),
+ Row.ofKind(RowKind.UPDATE_BEFORE,
V1, V1, 1),
+ Row.ofKind(RowKind.UPDATE_AFTER,
V1, V2, 2),
+ Row.ofKind(RowKind.UPDATE_BEFORE,
V1, V2, 2),
+ Row.ofKind(RowKind.UPDATE_AFTER,
V1, V2, 3))
.build())
.runSql(
- "INSERT INTO sink_t SELECT FIRST_VALUE(v),
LAST_VALUE(v), COUNT(v), COUNT(DISTINCT v) FROM t")
+ "INSERT INTO sink_t SELECT FIRST_VALUE(v),
LAST_VALUE(v), COUNT(v) FROM t")
.build();
static final TableTestProgram BUILTIN_AGG_WITH_RETRACTION =
@@ -172,18 +172,18 @@ public class VariantSemanticTest extends SemanticTestBase
{
.build())
.setupTableSink(
SinkTestStep.newBuilder("sink_t")
- .addSchema("fv VARIANT", "lv VARIANT", "c
BIGINT", "dc BIGINT")
+ .addSchema("fv VARIANT", "lv VARIANT", "c
BIGINT")
.consumedValues(
- Row.of(V1, V1, 1, 1),
- Row.ofKind(RowKind.UPDATE_BEFORE,
V1, V1, 1, 1),
- Row.ofKind(RowKind.UPDATE_AFTER,
V1, V2, 2, 2),
- Row.ofKind(RowKind.UPDATE_BEFORE,
V1, V2, 2, 2),
- Row.ofKind(RowKind.UPDATE_AFTER,
V1, V2, 3, 2),
- Row.ofKind(RowKind.UPDATE_BEFORE,
V1, V2, 3, 2),
- Row.ofKind(RowKind.UPDATE_AFTER,
V2, V2, 2, 1))
+ Row.of(V1, V1, 1),
+ Row.ofKind(RowKind.UPDATE_BEFORE,
V1, V1, 1),
+ Row.ofKind(RowKind.UPDATE_AFTER,
V1, V2, 2),
+ Row.ofKind(RowKind.UPDATE_BEFORE,
V1, V2, 2),
+ Row.ofKind(RowKind.UPDATE_AFTER,
V1, V2, 3),
+ Row.ofKind(RowKind.UPDATE_BEFORE,
V1, V2, 3),
+ Row.ofKind(RowKind.UPDATE_AFTER,
V2, V2, 2))
.build())
.runSql(
- "INSERT INTO sink_t SELECT FIRST_VALUE(v),
LAST_VALUE(v), COUNT(v), COUNT(DISTINCT v) FROM t")
+ "INSERT INTO sink_t SELECT FIRST_VALUE(v),
LAST_VALUE(v), COUNT(v) FROM t")
.build();
static final TableTestProgram VARIANT_AS_UDF_ARG =
@@ -277,25 +277,115 @@ public class VariantSemanticTest extends
SemanticTestBase {
.build();
static final TableTestProgram VARIANT_AS_AGG_KEY =
- TableTestProgram.of("variant-as-agg-key", "validates variant as
agg key")
+ TableTestProgram.of(
+ "variant-as-agg-key", "validates that a variant
cannot be an agg key")
.setupTableSource(
SourceTestStep.newBuilder("t")
.addSchema("k VARIANT", "v INTEGER")
- .producedValues(
- Row.of(BUILDER.of(1), 1),
- Row.of(BUILDER.of(2), 2),
- Row.of(BUILDER.of(1), 2))
+ .producedValues(Row.of(BUILDER.of(1), 1))
.build())
+ .runFailingSql(
+ "SELECT k, SUM(v) AS total FROM t GROUP BY k",
+ ValidationException.class,
+ "Column 'k' of type VARIANT cannot be used as a
grouping key, "
+ + "because the type has no equality. "
+ + "Cast the value to a comparable type
first.")
+ .build();
+
+ static final SourceTestStep OBJECTS_WITH_DIFFERENT_KEYS_SOURCE =
+ SourceTestStep.newBuilder("t")
+ .addSchema("s STRING")
+ .producedValues(
+ Row.of("{\"a\":1,\"b\":2}"),
+ Row.of("{\"b\":2,\"a\":1}"),
+ Row.of("{\"a\":1,\"c\":3}"))
+ .build();
+
+ static final TableTestProgram VARIANT_FIELD_CAST_AS_AGG_KEY =
+ TableTestProgram.of(
+ "variant-field-cast-as-agg-key",
+ "validates that casting a variant field before
grouping forms one group")
+ .setupTableSource(OBJECTS_WITH_DIFFERENT_KEYS_SOURCE)
.setupTableSink(
SinkTestStep.newBuilder("sink_t")
- .addSchema("k VARIANT", "total INTEGER")
+ .addSchema("a INT", "cnt BIGINT")
.consumedValues(
- Row.of(1, 1),
- Row.of(2, 2),
- Row.ofKind(RowKind.UPDATE_BEFORE,
1, 1),
- Row.ofKind(RowKind.UPDATE_AFTER,
1, 3))
+ Row.of(1, 1L),
+ Row.ofKind(RowKind.UPDATE_BEFORE,
1, 1L),
+ Row.ofKind(RowKind.UPDATE_AFTER,
1, 2L),
+ Row.ofKind(RowKind.UPDATE_BEFORE,
1, 2L),
+ Row.ofKind(RowKind.UPDATE_AFTER,
1, 3L))
+ .build())
+ .runSql(
+ "INSERT INTO sink_t SELECT a, COUNT(*) FROM "
+ + "(SELECT CAST(PARSE_JSON(s)['a'] AS INT)
AS a FROM t) GROUP BY a")
+ .build();
+
+ static final TableTestProgram VARIANT_FIELD_AS_AGG_KEY =
+ TableTestProgram.of(
+ "variant-field-as-agg-key",
+ "validates that a variant field cannot be an agg
key")
+ .setupTableSource(OBJECTS_WITH_DIFFERENT_KEYS_SOURCE)
+ .runFailingSql(
+ "SELECT a, COUNT(*) FROM (SELECT
PARSE_JSON(s)['a'] AS a FROM t) GROUP BY a",
+ ValidationException.class,
+ "Column 'a' of type VARIANT cannot be used as a
grouping key")
+ .build();
+
+ static final TableTestProgram VARIANT_OBJECT_AS_DISTINCT_KEY =
+ TableTestProgram.of(
+ "variant-object-as-distinct-key",
+ "validates that a variant cannot be a DISTINCT
key")
+ .setupTableSource(OBJECTS_WITH_DIFFERENT_KEYS_SOURCE)
+ .runFailingSql(
+ "SELECT COUNT(DISTINCT PARSE_JSON(s)) FROM t",
+ ValidationException.class,
+ "of type VARIANT cannot be used as an argument of
a DISTINCT aggregate")
+ .runFailingSql(
+ "SELECT DISTINCT PARSE_JSON(s) FROM t",
+ ValidationException.class,
+ "of type VARIANT cannot be used as a grouping key")
+ .build();
+
+ static final TableTestProgram VARIANT_FIELD_EQUALS_VARIANT =
+ TableTestProgram.of(
+ "variant-field-equals-variant",
+ "validates that variants cannot be compared with =
or IS [NOT] DISTINCT FROM")
+ .setupTableSource(OBJECTS_WITH_DIFFERENT_KEYS_SOURCE)
+ .runFailingSql(
+ "SELECT s FROM t WHERE PARSE_JSON(s)['a'] =
PARSE_JSON('1')",
+ ValidationException.class,
+ "An expression of type VARIANT cannot be used in a
comparison with '='")
+ // the SQL converter expands IS [NOT] DISTINCT FROM into =
+ .runFailingSql(
+ "SELECT s FROM t WHERE PARSE_JSON(s)['a'] IS
DISTINCT FROM PARSE_JSON('1')",
+ ValidationException.class,
+ "An expression of type VARIANT cannot be used in a
comparison with '='")
+ .runFailingSql(
+ "SELECT s FROM t WHERE PARSE_JSON(s)['a'] IS NOT
DISTINCT FROM PARSE_JSON('1')",
+ ValidationException.class,
+ "An expression of type VARIANT cannot be used in a
comparison with '='")
+ .build();
+
+ static final TableTestProgram VARIANT_FIELD_AS_JOIN_KEY =
+ TableTestProgram.of(
+ "variant-field-as-join-key",
+ "validates that a variant field cannot be a join
key")
+ .setupTableSource(
+ SourceTestStep.newBuilder("l")
+ .addSchema("s STRING")
+
.producedValues(Row.of("{\"id\":7,\"x\":1}"))
+ .build())
+ .setupTableSource(
+ SourceTestStep.newBuilder("r")
+ .addSchema("s STRING")
+
.producedValues(Row.of("{\"y\":2,\"id\":7}"))
.build())
- .runSql("INSERT INTO sink_t SELECT k, SUM(v) AS total FROM
t GROUP BY k")
+ .runFailingSql(
+ "SELECT PARSE_JSON(l.s)['x'], PARSE_JSON(r.s)['y']
"
+ + "FROM l JOIN r ON PARSE_JSON(l.s)['id']
= PARSE_JSON(r.s)['id']",
+ ValidationException.class,
+ "An expression of type VARIANT cannot be used in a
comparison with '='")
.build();
public static final SourceTestStep VARIANT_ARRAY_SOURCE =
@@ -512,6 +602,11 @@ public class VariantSemanticTest extends SemanticTestBase {
VARIANT_IN_VIEW,
VARIANT_AS_UDAF_ARG,
VARIANT_AS_AGG_KEY,
+ VARIANT_FIELD_CAST_AS_AGG_KEY,
+ VARIANT_FIELD_AS_AGG_KEY,
+ VARIANT_OBJECT_AS_DISTINCT_KEY,
+ VARIANT_FIELD_EQUALS_VARIANT,
+ VARIANT_FIELD_AS_JOIN_KEY,
VARIANT_ARRAY_ACCESS,
VARIANT_ARRAY_ACCESS_WITH_DIFFERENT_INDEX_TYPES,
VARIANT_OBJECT_ACCESS,
diff --git
a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/optimize/KeyTypeValidatorTest.java
b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/optimize/KeyTypeValidatorTest.java
new file mode 100644
index 00000000000..0703af6f2f6
--- /dev/null
+++
b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/optimize/KeyTypeValidatorTest.java
@@ -0,0 +1,399 @@
+/*
+ * 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.plan.optimize;
+
+import org.apache.flink.api.common.RuntimeExecutionMode;
+import org.apache.flink.table.annotation.ArgumentHint;
+import org.apache.flink.table.annotation.ArgumentTrait;
+import org.apache.flink.table.api.EnvironmentSettings;
+import org.apache.flink.table.api.Over;
+import org.apache.flink.table.api.Table;
+import org.apache.flink.table.api.TableEnvironment;
+import org.apache.flink.table.api.ValidationException;
+import org.apache.flink.table.functions.ProcessTableFunction;
+import org.apache.flink.types.Row;
+
+import org.junit.jupiter.api.Named;
+import org.junit.jupiter.params.ParameterizedTest;
+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.function.Function;
+import java.util.stream.Stream;
+
+import static org.apache.flink.api.common.RuntimeExecutionMode.BATCH;
+import static org.apache.flink.api.common.RuntimeExecutionMode.STREAMING;
+import static org.apache.flink.core.testutils.FlinkAssertions.anyCauseMatches;
+import static org.apache.flink.table.api.Expressions.$;
+import static org.apache.flink.table.api.Expressions.UNBOUNDED_ROW;
+import static
org.apache.flink.table.api.config.ExecutionConfigOptions.TABLE_EXEC_DISABLED_OPERATORS;
+import static org.assertj.core.api.Assertions.assertThatCode;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/** Tests for {@link KeyTypeValidator}. */
+class KeyTypeValidatorTest {
+
+ private static final String GROUPING_KEY =
+ "Column 'v' of type VARIANT cannot be used as a grouping key";
+
+ private static final String DISTINCT_ARGUMENT =
+ "of type VARIANT cannot be used as an argument of a DISTINCT
aggregate";
+
+ private static final String COMPARISON =
+ "An expression of type VARIANT cannot be used in a comparison
with";
+
+ private static final String EQUALS = COMPARISON + " '='";
+
+ private static Stream<Arguments> rejectedQueries() {
+ final Stream<Arguments> bothModes =
+ inBothModes(
+ Arguments.of(
+ "SELECT v, COUNT(*) FROM t GROUP BY v",
+ "Column 'v' of type VARIANT cannot be used as
a grouping key, "
+ + "because the type has no equality. "
+ + "Cast the value to a comparable type
first."),
+ Arguments.of(
+ "SELECT COUNT(*) FROM t GROUP BY r",
+ "Column 'r' of type ROW<`x` VARIANT> cannot be
used as a grouping key"),
+ Arguments.of(
+ "SELECT COUNT(*) FROM t GROUP BY a",
+ "Column 'a' of type ARRAY<VARIANT> cannot be
used as a grouping key"),
+ Arguments.of(
+ "SELECT COUNT(*) FROM t GROUP BY v['a']",
+ "An expression of type VARIANT cannot be used
as a grouping key"),
+ Arguments.of(
+ "SELECT COUNT(*) FROM t GROUP BY GROUPING SETS
((v), (id))",
+ GROUPING_KEY),
+ Arguments.of(
+ "SELECT COUNT(*) FROM t GROUP BY ROLLUP (v,
id)", GROUPING_KEY),
+ Arguments.of("SELECT DISTINCT v FROM t", GROUPING_KEY),
+ Arguments.of(
+ "SELECT COUNT(DISTINCT v) FROM t",
+ "Column 'v' " + DISTINCT_ARGUMENT),
+ Arguments.of(
+ "SELECT COUNT(DISTINCT v) OVER (PARTITION BY
id ORDER BY ts) FROM t",
+ "An expression " + DISTINCT_ARGUMENT),
+ Arguments.of(
+ "SELECT window_start, v, COUNT(*) FROM TABLE("
+ + "TUMBLE(TABLE t, DESCRIPTOR(ts),
INTERVAL '1' MINUTE)) "
+ + "GROUP BY window_start, window_end,
v",
+ GROUPING_KEY),
+ Arguments.of(
+ "SELECT window_start, COUNT(*) FROM TABLE("
+ + "SESSION(TABLE t PARTITION BY v,
DESCRIPTOR(ts), INTERVAL '1' MINUTE)) "
+ + "GROUP BY window_start, window_end",
+ "Column 'v' of type VARIANT cannot be used as
a PARTITION BY key"),
+ Arguments.of(
+ "SELECT v FROM t UNION SELECT v FROM u",
+ "Column 'v' of type VARIANT cannot be used in
UNION"),
+ Arguments.of(
+ "SELECT v FROM t INTERSECT SELECT v FROM u",
+ "Column 'v' of type VARIANT cannot be used in
INTERSECT"),
+ Arguments.of(
+ "SELECT v FROM t INTERSECT ALL SELECT v FROM
u",
+ "Column 'v' of type VARIANT cannot be used in
INTERSECT ALL"),
+ Arguments.of(
+ "SELECT v FROM t EXCEPT SELECT v FROM u",
+ "Column 'v' of type VARIANT cannot be used in
EXCEPT"),
+ Arguments.of(
+ "SELECT v FROM t EXCEPT ALL SELECT v FROM u",
+ "Column 'v' of type VARIANT cannot be used in
EXCEPT ALL"),
+ Arguments.of(
+ "SELECT v FROM t ORDER BY v",
+ "Column 'v' of type VARIANT cannot be used as
an ORDER BY key, "
+ + "because the type has no order."),
+ Arguments.of(
+ "SELECT ts, v FROM t ORDER BY ts, v",
+ "Column 'v' of type VARIANT cannot be used as
an ORDER BY key"),
+ Arguments.of(
+ "SELECT COUNT(*) OVER (PARTITION BY v ORDER BY
ts) FROM t",
+ "An expression of type VARIANT cannot be used
as a PARTITION BY key "
+ + "of an OVER window"),
+ Arguments.of(
+ "SELECT * FROM (SELECT id, ROW_NUMBER() OVER "
+ + "(PARTITION BY v ORDER BY ts) AS rn
FROM t) WHERE rn = 1",
+ "An expression of type VARIANT cannot be used
as a PARTITION BY key "
+ + "of an OVER window"),
+ Arguments.of(
+ "SELECT * FROM (SELECT id, ROW_NUMBER() OVER "
+ + "(PARTITION BY id ORDER BY v) AS rn
FROM t) WHERE rn <= 3",
+ "An expression of type VARIANT cannot be used
as an ORDER BY key "
+ + "of an OVER window"),
+ Arguments.of("SELECT t.id FROM t JOIN u ON t.v = u.v",
EQUALS),
+ Arguments.of("SELECT t.id FROM t JOIN u USING (v)",
EQUALS),
+ Arguments.of("SELECT id FROM t WHERE v =
PARSE_JSON('1')", EQUALS),
+ Arguments.of("SELECT id FROM t WHERE v <> v",
COMPARISON + " '<>'"),
+ Arguments.of(
+ "SELECT id FROM t WHERE v < v",
+ COMPARISON + " '<', because the type has no
order."),
+ Arguments.of(
+ "SELECT id FROM t WHERE v BETWEEN
PARSE_JSON('1') AND PARSE_JSON('2')",
+ COMPARISON),
+ // the SQL converter expands IS [NOT] DISTINCT FROM
into =
+ Arguments.of(
+ "SELECT id FROM t WHERE v IS DISTINCT FROM
PARSE_JSON('1')",
+ EQUALS),
+ Arguments.of(
+ "SELECT id FROM t WHERE v IS NOT DISTINCT FROM
PARSE_JSON('1')",
+ EQUALS),
+ Arguments.of(
+ "SELECT id FROM t WHERE r = r",
+ "An expression of type ROW<`x` VARIANT> cannot
be used in a comparison "
+ + "with '='"),
+ Arguments.of(
+ "SELECT id FROM t WHERE v IN (PARSE_JSON('1'),
PARSE_JSON('2'))",
+ EQUALS),
+ Arguments.of(
+ "SELECT id FROM t WHERE v IN (SELECT v FROM
u)",
+ COMPARISON + " 'IN'"),
+ Arguments.of(
+ "SELECT id FROM t WHERE v > ALL (SELECT v FROM
u)", COMPARISON),
+ Arguments.of(
+ "SELECT id FROM t WHERE EXISTS (SELECT 1 FROM
u WHERE u.v = t.v)",
+ EQUALS),
+ Arguments.of(
+ "SELECT CASE v WHEN PARSE_JSON('1') THEN 1 END
FROM t", EQUALS),
+ Arguments.of("SELECT NULLIF(v, PARSE_JSON('1')) FROM
t", EQUALS),
+ Arguments.of(
+ "SELECT * FROM t MATCH_RECOGNIZE (PARTITION BY
v ORDER BY ts "
+ + "MEASURES A.id AS aid PATTERN (A)
DEFINE A AS A.id > 0)",
+ "Column 'v' of type VARIANT cannot be used as
a PARTITION BY key "
+ + "of MATCH_RECOGNIZE"),
+ Arguments.of(
+ "SELECT * FROM t MATCH_RECOGNIZE (PARTITION BY
id ORDER BY ts, v "
+ + "MEASURES A.id AS aid PATTERN (A)
DEFINE A AS A.id > 0)",
+ "Column 'v' of type VARIANT cannot be used as
an ORDER BY key "
+ + "of MATCH_RECOGNIZE"),
+ Arguments.of(
+ "SELECT * FROM t MATCH_RECOGNIZE (PARTITION BY
id ORDER BY ts "
+ + "MEASURES A.id AS aid PATTERN (A B) "
+ + "DEFINE B AS B.v = A.v)",
+ EQUALS),
+ Arguments.of(
+ "SELECT * FROM f(r => TABLE t PARTITION BY v)",
+ "Column 'v' of type VARIANT cannot be used as
a PARTITION BY key "
+ + "of a table argument"),
+ Arguments.of(
+ "SELECT * FROM f(r => TABLE t PARTITION BY id
ORDER BY (ts, v))",
+ "Column 'v' of type VARIANT cannot be used as
an ORDER BY key "
+ + "of a table argument"));
+ // batch mode does not apply watermarks
+ final Stream<Arguments> streamingOnly =
+ inMode(STREAMING, Arguments.of("SELECT id FROM w", EQUALS));
+ return Stream.concat(bothModes, streamingOnly);
+ }
+
+ private static Stream<Arguments> allowedQueries() {
+ return inBothModes(
+ Arguments.of("SELECT v FROM t UNION ALL SELECT v FROM u"),
+ Arguments.of("SELECT t.v, u.v FROM t JOIN u ON t.id = u.id"),
+ Arguments.of("SELECT v, COUNT(*) OVER (PARTITION BY id ORDER
BY ts) FROM t"),
+ Arguments.of("SELECT v, ts FROM t ORDER BY ts"),
+ Arguments.of("SELECT DISTINCT id, CAST(v AS STRING) FROM t"),
+ Arguments.of("SELECT v FROM t WHERE v IS NOT NULL AND r IS
NULL"),
+ Arguments.of(
+ "SELECT v FROM t WHERE v IS DISTINCT FROM NULL "
+ + "OR v IS NOT DISTINCT FROM NULL"));
+ }
+
+ private static Stream<Arguments> rejectedJoinsPerBatchStrategy() {
+ return Stream.of(
+ Named.of("SortMergeJoin", "HashJoin, NestedLoopJoin"),
+ Named.of(
+ "ShuffleHashJoin",
+ "SortMergeJoin, NestedLoopJoin,
BroadcastHashJoin"),
+ Named.of(
+ "BroadcastHashJoin",
+ "SortMergeJoin, NestedLoopJoin,
ShuffleHashJoin"),
+ Named.of("NestedLoopJoin", "SortMergeJoin, HashJoin"))
+ .flatMap(
+ disabledOperators ->
+ Stream.of(
+ Arguments.of(
+ "SELECT t.id FROM t JOIN u ON
t.v = u.v",
+ disabledOperators),
+ Arguments.of(
+ "SELECT t.id FROM t JOIN u ON
t.v['id'] = u.v['id']",
+ disabledOperators)));
+ }
+
+ private static Stream<Arguments> rejectedTableApiQueries() {
+ final Stream<Arguments> bothModes =
+ inBothModes(
+ tableApi(
+ "groupBy",
+ env ->
+ env.from("t")
+ .groupBy($("v"))
+ .select($("v"),
$("id").count()),
+ GROUPING_KEY),
+ tableApi(
+ "distinct",
+ env -> env.from("t").select($("v")).distinct(),
+ GROUPING_KEY),
+ tableApi(
+ "orderBy",
+ env -> env.from("t").orderBy($("v")),
+ "Column 'v' of type VARIANT cannot be used as
an ORDER BY key"));
+ // the Table API supports INTERSECT and MINUS only on bounded tables
+ final Stream<Arguments> batchOnly =
+ inMode(
+ BATCH,
+ tableApi(
+ "intersect",
+ env ->
+ env.from("t")
+ .select($("v"))
+
.intersect(env.from("u").select($("v"))),
+ "Column 'v' of type VARIANT cannot be used in
INTERSECT"),
+ tableApi(
+ "minus",
+ env ->
+ env.from("t")
+ .select($("v"))
+
.minus(env.from("u").select($("v"))),
+ "Column 'v' of type VARIANT cannot be used in
EXCEPT"));
+ // the Table API supports these OVER windows only in streaming mode
+ final Stream<Arguments> streamingOnly =
+ inMode(
+ STREAMING,
+ tableApi(
+ "over window partitionBy",
+ env ->
+ env.from("t")
+ .window(
+
Over.partitionBy($("v"))
+
.orderBy($("ts"))
+
.preceding(UNBOUNDED_ROW)
+ .as("w"))
+
.select($("id").count().over($("w"))),
+ "An expression of type VARIANT cannot be used
as a PARTITION BY key "
+ + "of an OVER window"),
+ tableApi(
+ "distinct count over window",
+ env ->
+ env.from("t")
+ .window(
+
Over.partitionBy($("id"))
+
.orderBy($("ts"))
+
.preceding(UNBOUNDED_ROW)
+ .as("w"))
+
.select($("v").count().distinct().over($("w"))),
+ "An expression " + DISTINCT_ARGUMENT));
+ return Stream.concat(bothModes, Stream.concat(batchOnly,
streamingOnly));
+ }
+
+ @ParameterizedTest(name = "[{index}] {2}: {0}")
+ @MethodSource("rejectedQueries")
+ void testRejectedQuery(
+ final String sql, final String expectedMessage, final
RuntimeExecutionMode mode) {
+ final TableEnvironment env = createEnvironment(mode);
+ assertThatThrownBy(() -> env.explainSql(sql))
+ .satisfies(anyCauseMatches(ValidationException.class,
expectedMessage));
+ }
+
+ @ParameterizedTest(name = "[{index}] {1}: {0}")
+ @MethodSource("allowedQueries")
+ void testAllowedQuery(final String sql, final RuntimeExecutionMode mode) {
+ final TableEnvironment env = createEnvironment(mode);
+ assertThatCode(() -> env.explainSql(sql)).doesNotThrowAnyException();
+ }
+
+ @ParameterizedTest(name = "[{index}] {1} only: {0}")
+ @MethodSource("rejectedJoinsPerBatchStrategy")
+ void testRejectedJoinPerBatchStrategy(final String sql, final String
disabledOperators) {
+ final TableEnvironment env = createEnvironment(BATCH);
+ env.getConfig().set(TABLE_EXEC_DISABLED_OPERATORS, disabledOperators);
+ assertThatThrownBy(() -> env.explainSql(sql))
+ .satisfies(anyCauseMatches(ValidationException.class, EQUALS));
+ }
+
+ @ParameterizedTest(name = "[{index}] {2}: {0}")
+ @MethodSource("rejectedTableApiQueries")
+ void testRejectedTableApiQuery(
+ final Function<TableEnvironment, Table> query,
+ final String expectedMessage,
+ final RuntimeExecutionMode mode) {
+ final TableEnvironment env = createEnvironment(mode);
+ assertThatThrownBy(() -> query.apply(env).explain())
+ .satisfies(anyCauseMatches(ValidationException.class,
expectedMessage));
+ }
+
+ private static Arguments tableApi(
+ final String name,
+ final Function<TableEnvironment, Table> query,
+ final String expectedMessage) {
+ return Arguments.of(Named.of(name, query), expectedMessage);
+ }
+
+ private static Stream<Arguments> inBothModes(final Arguments... cases) {
+ return Stream.concat(inMode(STREAMING, cases), inMode(BATCH, cases));
+ }
+
+ private static Stream<Arguments> inMode(
+ final RuntimeExecutionMode mode, final Arguments... cases) {
+ return Stream.of(cases).map(c -> withMode(c, mode));
+ }
+
+ private static Arguments withMode(final Arguments arguments, final
RuntimeExecutionMode mode) {
+ final Object[] args = Arrays.copyOf(arguments.get(),
arguments.get().length + 1);
+ args[args.length - 1] = mode;
+ return Arguments.of(args);
+ }
+
+ private static TableEnvironment createEnvironment(final
RuntimeExecutionMode mode) {
+ final TableEnvironment env =
+ TableEnvironment.create(
+ mode == STREAMING
+ ? EnvironmentSettings.inStreamingMode()
+ : EnvironmentSettings.inBatchMode());
+ for (final String table : List.of("t", "u")) {
+ env.executeSql(
+ String.format(
+ "CREATE TABLE %s ("
+ + "id INT, "
+ + "v VARIANT, "
+ + "r ROW<x VARIANT>, "
+ + "a ARRAY<VARIANT>, "
+ + "ts TIMESTAMP(3), "
+ + "WATERMARK FOR ts AS ts"
+ + ") WITH ('connector' = 'values',
'bounded' = '%s')",
+ table, mode == BATCH));
+ }
+ env.executeSql(
+ String.format(
+ "CREATE TABLE w ("
+ + "id INT, "
+ + "v VARIANT, "
+ + "ts TIMESTAMP(3), "
+ + "WATERMARK FOR ts AS "
+ + "CASE WHEN v = PARSE_JSON('1') THEN ts ELSE
ts - INTERVAL '1' SECOND END"
+ + ") WITH ('connector' = 'values', 'bounded' =
'%s')",
+ mode == BATCH));
+ env.createTemporarySystemFunction("f", PartitionedFunction.class);
+ return env;
+ }
+
+ /** A function with a set semantic table argument. */
+ public static class PartitionedFunction extends
ProcessTableFunction<Integer> {
+ public void eval(@ArgumentHint(ArgumentTrait.SET_SEMANTIC_TABLE) final
Row r) {}
+ }
+}