This is an automated email from the ASF dual-hosted git repository.
xiedeyantu pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/calcite.git
The following commit(s) were added to refs/heads/main by this push:
new 5529cdf86b [CALCITE-6451] Improve Nullability Derivation for Intersect
and Minus
5529cdf86b is described below
commit 5529cdf86bc54b750572bcca775a87fcb61c6443
Author: Zhen Chen <[email protected]>
AuthorDate: Sat Jun 6 10:13:38 2026 +0800
[CALCITE-6451] Improve Nullability Derivation for Intersect and Minus
Co-authored-by: Victor Barua <[email protected]>
---
.../org/apache/calcite/rel/core/Intersect.java | 11 +
.../java/org/apache/calcite/rel/core/Minus.java | 10 +
.../java/org/apache/calcite/rel/core/SetOp.java | 4 +
.../calcite/rel/rules/IntersectToDistinctRule.java | 7 +
.../calcite/rel/rules/MinusToDistinctRule.java | 4 +
.../org/apache/calcite/sql/SqlSetOperator.java | 4 +-
.../calcite/sql/fun/SqlStdOperatorTable.java | 12 +-
.../org/apache/calcite/sql/type/ReturnTypes.java | 105 +++++++
.../org/apache/calcite/test/RelBuilderTest.java | 350 +++++++++++++++++++++
.../org/apache/calcite/test/RelOptRulesTest.xml | 17 +-
core/src/test/resources/sql/planner.iq | 25 +-
.../java/org/apache/calcite/test/Matchers.java | 13 +
12 files changed, 535 insertions(+), 27 deletions(-)
diff --git a/core/src/main/java/org/apache/calcite/rel/core/Intersect.java
b/core/src/main/java/org/apache/calcite/rel/core/Intersect.java
index e6ea2f6c24..8bed0bfd0d 100644
--- a/core/src/main/java/org/apache/calcite/rel/core/Intersect.java
+++ b/core/src/main/java/org/apache/calcite/rel/core/Intersect.java
@@ -22,7 +22,10 @@
import org.apache.calcite.rel.RelNode;
import org.apache.calcite.rel.hint.RelHint;
import org.apache.calcite.rel.metadata.RelMetadataQuery;
+import org.apache.calcite.rel.type.RelDataType;
import org.apache.calcite.sql.SqlKind;
+import org.apache.calcite.sql.type.ReturnTypes;
+import org.apache.calcite.util.Util;
import java.util.Collections;
import java.util.List;
@@ -79,4 +82,12 @@ protected Intersect(RelInput input) {
dRows *= 0.25;
return dRows;
}
+
+ @Override protected RelDataType deriveRowType() {
+ // An output column is only nullable if it is nullable in ALL the inputs.
+ return ReturnTypes.deriveNullabilityForIntersect(
+ getCluster().getTypeFactory(),
+ deriveLeastRestrictiveRowType(),
+ Util.transform(getInputs(), RelNode::getRowType));
+ }
}
diff --git a/core/src/main/java/org/apache/calcite/rel/core/Minus.java
b/core/src/main/java/org/apache/calcite/rel/core/Minus.java
index 3a68945acc..badf20474a 100644
--- a/core/src/main/java/org/apache/calcite/rel/core/Minus.java
+++ b/core/src/main/java/org/apache/calcite/rel/core/Minus.java
@@ -23,7 +23,9 @@
import org.apache.calcite.rel.hint.RelHint;
import org.apache.calcite.rel.metadata.RelMdUtil;
import org.apache.calcite.rel.metadata.RelMetadataQuery;
+import org.apache.calcite.rel.type.RelDataType;
import org.apache.calcite.sql.SqlKind;
+import org.apache.calcite.sql.type.ReturnTypes;
import java.util.Collections;
import java.util.List;
@@ -60,4 +62,12 @@ protected Minus(RelInput input) {
@Override public double estimateRowCount(RelMetadataQuery mq) {
return RelMdUtil.getMinusRowCount(mq, this);
}
+
+ @Override protected RelDataType deriveRowType() {
+ // The nullability of the output columns is the same as that of the
primary input.
+ return ReturnTypes.deriveNullabilityForExcept(
+ getCluster().getTypeFactory(),
+ deriveLeastRestrictiveRowType(),
+ getInput(0).getRowType());
+ }
}
diff --git a/core/src/main/java/org/apache/calcite/rel/core/SetOp.java
b/core/src/main/java/org/apache/calcite/rel/core/SetOp.java
index 04e1409e78..9af6dd2ca9 100644
--- a/core/src/main/java/org/apache/calcite/rel/core/SetOp.java
+++ b/core/src/main/java/org/apache/calcite/rel/core/SetOp.java
@@ -114,6 +114,10 @@ public abstract SetOp copy(
}
@Override protected RelDataType deriveRowType() {
+ return deriveLeastRestrictiveRowType();
+ }
+
+ protected RelDataType deriveLeastRestrictiveRowType() {
final List<RelDataType> inputRowTypes =
Util.transform(inputs, RelNode::getRowType);
final RelDataType rowType =
diff --git
a/core/src/main/java/org/apache/calcite/rel/rules/IntersectToDistinctRule.java
b/core/src/main/java/org/apache/calcite/rel/rules/IntersectToDistinctRule.java
index a8d2713e09..394f8e0885 100644
---
a/core/src/main/java/org/apache/calcite/rel/rules/IntersectToDistinctRule.java
+++
b/core/src/main/java/org/apache/calcite/rel/rules/IntersectToDistinctRule.java
@@ -140,6 +140,10 @@ public void onMatchAggregateOnUnion(RelOptRuleCall call) {
// Project all but the last added field (e.g. count_i{n})
relBuilder.project(skipLast(relBuilder.fields(), branchCount));
+
+ // ensure the nullabilities of columns in the new relation match those of
the input relation
+ relBuilder.convert(intersect.getRowType(), false);
+
call.transformTo(relBuilder.build());
}
@@ -206,6 +210,9 @@ public void onMatchAggregatePushdown(RelOptRuleCall call) {
// Project all but the last field
relBuilder.project(Util.skipLast(relBuilder.fields()));
+ // ensure the nullabilities of columns in the new relation match those of
the input relation
+ relBuilder.convert(intersect.getRowType(), false);
+
// the schema for intersect distinct matches that of the relation,
// built here with an extra last column for the count,
// which is projected out by the final project we added
diff --git
a/core/src/main/java/org/apache/calcite/rel/rules/MinusToDistinctRule.java
b/core/src/main/java/org/apache/calcite/rel/rules/MinusToDistinctRule.java
index 7b14f2e946..59eaf0b60c 100644
--- a/core/src/main/java/org/apache/calcite/rel/rules/MinusToDistinctRule.java
+++ b/core/src/main/java/org/apache/calcite/rel/rules/MinusToDistinctRule.java
@@ -159,6 +159,10 @@ public MinusToDistinctRule(Class<? extends Minus>
minusClass,
relBuilder.filter(filters.build());
relBuilder.project(Util.first(relBuilder.fields(), originalFieldCnt));
+
+ // ensure the nullabilities of columns in the new relation match those of
the minus output
+ relBuilder.convert(minus.getRowType(), false);
+
call.transformTo(relBuilder.build());
}
diff --git a/core/src/main/java/org/apache/calcite/sql/SqlSetOperator.java
b/core/src/main/java/org/apache/calcite/sql/SqlSetOperator.java
index 6d0f48c4f4..0ce69276f7 100644
--- a/core/src/main/java/org/apache/calcite/sql/SqlSetOperator.java
+++ b/core/src/main/java/org/apache/calcite/sql/SqlSetOperator.java
@@ -24,6 +24,8 @@
import org.apache.calcite.sql.validate.SqlValidator;
import org.apache.calcite.sql.validate.SqlValidatorScope;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
/**
* SqlSetOperator represents a relational set theory operator (UNION,
INTERSECT,
* MINUS). These are binary operators, but with an extra boolean attribute
@@ -59,7 +61,7 @@ public SqlSetOperator(
int prec,
boolean all,
SqlReturnTypeInference returnTypeInference,
- SqlOperandTypeInference operandTypeInference,
+ @Nullable SqlOperandTypeInference operandTypeInference,
SqlOperandTypeChecker operandTypeChecker) {
super(
name,
diff --git
a/core/src/main/java/org/apache/calcite/sql/fun/SqlStdOperatorTable.java
b/core/src/main/java/org/apache/calcite/sql/fun/SqlStdOperatorTable.java
index 494fafeccc..b1f0c0d504 100644
--- a/core/src/main/java/org/apache/calcite/sql/fun/SqlStdOperatorTable.java
+++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlStdOperatorTable.java
@@ -121,16 +121,20 @@ public class SqlStdOperatorTable extends
ReflectiveSqlOperatorTable {
new SqlSetOperator("UNION ALL", SqlKind.UNION, 12, true);
public static final SqlSetOperator EXCEPT =
- new SqlSetOperator("EXCEPT", SqlKind.EXCEPT, 12, false);
+ new SqlSetOperator("EXCEPT", SqlKind.EXCEPT, 12, false,
+ ReturnTypes.LEAST_RESTRICTIVE_EXCEPT, null, OperandTypes.SET_OP);
public static final SqlSetOperator EXCEPT_ALL =
- new SqlSetOperator("EXCEPT ALL", SqlKind.EXCEPT, 12, true);
+ new SqlSetOperator("EXCEPT ALL", SqlKind.EXCEPT, 12, true,
+ ReturnTypes.LEAST_RESTRICTIVE_EXCEPT, null, OperandTypes.SET_OP);
public static final SqlSetOperator INTERSECT =
- new SqlSetOperator("INTERSECT", SqlKind.INTERSECT, 14, false);
+ new SqlSetOperator("INTERSECT", SqlKind.INTERSECT, 14, false,
+ ReturnTypes.LEAST_RESTRICTIVE_INTERSECT, null, OperandTypes.SET_OP);
public static final SqlSetOperator INTERSECT_ALL =
- new SqlSetOperator("INTERSECT ALL", SqlKind.INTERSECT, 14, true);
+ new SqlSetOperator("INTERSECT ALL", SqlKind.INTERSECT, 14, true,
+ ReturnTypes.LEAST_RESTRICTIVE_INTERSECT, null, OperandTypes.SET_OP);
/**
* The {@code MULTISET UNION DISTINCT} operator.
diff --git a/core/src/main/java/org/apache/calcite/sql/type/ReturnTypes.java
b/core/src/main/java/org/apache/calcite/sql/type/ReturnTypes.java
index 27f1a6fc72..9e32e0563d 100644
--- a/core/src/main/java/org/apache/calcite/sql/type/ReturnTypes.java
+++ b/core/src/main/java/org/apache/calcite/sql/type/ReturnTypes.java
@@ -34,9 +34,12 @@
import org.apache.calcite.util.Glossary;
import org.apache.calcite.util.Util;
+import com.google.common.collect.ImmutableList;
+
import org.checkerframework.checker.nullness.qual.Nullable;
import java.util.AbstractList;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.UnaryOperator;
@@ -608,6 +611,108 @@ public static SqlCall stripSeparator(SqlCall call) {
.leastRestrictive(opBinding.collectOperandTypes());
}
+ /**
+ * Refines the nullability of {@code base} using INTERSECT semantics: a
+ * column is NOT NULL if it is NOT NULL in all of the
+ * {@code inputTypes} ("AND" semantics across inputs).
+ *
+ * <p>For ROW (struct) types, this recursively refines nullability of nested
+ * fields, ensuring the "AND" semantics apply at every level of nesting.
+ */
+ public static RelDataType deriveNullabilityForIntersect(
+ RelDataTypeFactory typeFactory,
+ RelDataType base,
+ List<RelDataType> inputTypes) {
+ return deriveNullable(typeFactory, base, inputTypes);
+ }
+
+ /**
+ * Refines the nullability of {@code base} using EXCEPT/MINUS semantics:
+ * each column's nullability matches that of the primary (first) input.
+ * For ROW types, this is applied recursively at all nesting levels.
+ */
+ public static RelDataType deriveNullabilityForExcept(
+ RelDataTypeFactory typeFactory,
+ RelDataType base,
+ RelDataType primaryInputType) {
+ return deriveNullable(typeFactory, base,
+ ImmutableList.of(primaryInputType));
+ }
+
+ /**
+ * Returns whether all types in the list are nullable (AND semantics).
+ */
+ private static boolean allNullable(List<RelDataType> types) {
+ for (RelDataType type : types) {
+ if (!type.isNullable()) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Recursively refines nullability using AND semantics:
+ * a column is NOT NULL if it is NOT NULL in ALL {@code inputTypes}.
+ * For ROW types, this is applied recursively to all nested fields.
+ */
+ private static RelDataType deriveNullable(
+ RelDataTypeFactory typeFactory,
+ RelDataType type,
+ List<RelDataType> inputTypes) {
+ if (type.isStruct()) {
+ final RelDataTypeFactory.Builder builder =
+ new RelDataTypeFactory.Builder(typeFactory);
+ final List<RelDataTypeField> fields = type.getFieldList();
+ for (int i = 0; i < fields.size(); i++) {
+ final List<RelDataType> fieldInputTypes = new ArrayList<>();
+ for (RelDataType inputType : inputTypes) {
+ fieldInputTypes.add(inputType.getFieldList().get(i).getType());
+ }
+ builder.add(fields.get(i).getName(),
+ deriveNullable(typeFactory,
+ fields.get(i).getType(),
+ fieldInputTypes))
+ .nullable(allNullable(fieldInputTypes));
+ }
+ return builder.build();
+ }
+ return typeFactory.createTypeWithNullability(type,
allNullable(inputTypes));
+ }
+
+ /**
+ * Type-inference strategy for INTERSECT. Computes the least restrictive row
+ * type across all inputs, then refines nullability: a column is NOT NULL if
+ * it is NOT NULL in at least one input ("AND" semantics across inputs).
+ */
+ public static final SqlReturnTypeInference LEAST_RESTRICTIVE_INTERSECT =
+ andThen(SqlTypeTransforms.FROM_MEASURE_IF::apply, opBinding -> {
+ final List<RelDataType> inputTypes = opBinding.collectOperandTypes();
+ final RelDataType base =
+ opBinding.getTypeFactory().leastRestrictive(inputTypes);
+ if (base == null) {
+ return null;
+ }
+ return deriveNullabilityForIntersect(opBinding.getTypeFactory(), base,
inputTypes);
+ });
+
+ /**
+ * Type-inference strategy for EXCEPT/MINUS. Computes the least restrictive
+ * row type across all inputs, then refines nullability: a column's
+ * nullability matches that of the first (primary) input.
+ */
+ public static final SqlReturnTypeInference LEAST_RESTRICTIVE_EXCEPT =
+ andThen(SqlTypeTransforms.FROM_MEASURE_IF::apply, opBinding -> {
+ final List<RelDataType> inputTypes = opBinding.collectOperandTypes();
+ final RelDataType base =
+ opBinding.getTypeFactory().leastRestrictive(inputTypes);
+ if (base == null) {
+ return null;
+ }
+ return deriveNullabilityForExcept(
+ opBinding.getTypeFactory(), base, inputTypes.get(0));
+ });
+
/**
* Type-inference strategy for NVL2 function. It returns the least
restrictive type
* between the second and third operands.
diff --git a/core/src/test/java/org/apache/calcite/test/RelBuilderTest.java
b/core/src/test/java/org/apache/calcite/test/RelBuilderTest.java
index f1a4f1c271..e53f1027f7 100644
--- a/core/src/test/java/org/apache/calcite/test/RelBuilderTest.java
+++ b/core/src/test/java/org/apache/calcite/test/RelBuilderTest.java
@@ -131,6 +131,7 @@
import static org.apache.calcite.test.Matchers.hasExpandedTree;
import static org.apache.calcite.test.Matchers.hasFieldNames;
import static org.apache.calcite.test.Matchers.hasHints;
+import static org.apache.calcite.test.Matchers.hasRelDataType;
import static org.apache.calcite.test.Matchers.hasTree;
import static org.hamcrest.CoreMatchers.allOf;
@@ -2346,6 +2347,72 @@ private static RelNode groupIdRel(RelBuilder builder,
boolean extra) {
assertThat(root, hasTree(expected));
}
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-6451">[CALCITE-6451]
+ * Improve Nullability Derivation for Intersect and Minus</a>. */
+ @ParameterizedTest
+ @ValueSource(booleans = {false, true})
+ void testUnionTypeDerivation(boolean all) {
+ final RelBuilder builder = RelBuilder.create(config().build());
+
+ RelDataType input1RowType =
+ new RelDataTypeFactory.Builder(builder.getTypeFactory())
+ .add("a", SqlTypeName.BIGINT)
+ .nullable(false)
+ .add("b", SqlTypeName.BIGINT)
+ .nullable(false)
+ .add("c", SqlTypeName.BIGINT)
+ .nullable(true)
+ .add("d", SqlTypeName.BIGINT)
+ .nullable(true)
+ .build();
+
+ RelDataType input2RowType =
+ new RelDataTypeFactory.Builder(builder.getTypeFactory())
+ .add("a", SqlTypeName.BIGINT)
+ .nullable(false)
+ .add("b", SqlTypeName.BIGINT)
+ .nullable(false)
+ .add("c", SqlTypeName.BIGINT)
+ .nullable(false)
+ .add("d", SqlTypeName.BIGINT)
+ .nullable(false)
+ .build();
+
+ RelDataType input3RowType =
+ new RelDataTypeFactory.Builder(builder.getTypeFactory())
+ .add("a", SqlTypeName.BIGINT)
+ .nullable(false)
+ .add("b", SqlTypeName.BIGINT)
+ .nullable(true)
+ .add("c", SqlTypeName.BIGINT)
+ .nullable(false)
+ .add("d", SqlTypeName.BIGINT)
+ .nullable(true)
+ .build();
+
+ RelNode root =
+ builder
+ .values(input1RowType)
+ .values(input2RowType)
+ .values(input3RowType)
+ .union(all, 3)
+ .build();
+
+ RelDataType expectedRowType =
+ new RelDataTypeFactory.Builder(builder.getTypeFactory())
+ .add("a", SqlTypeName.BIGINT)
+ .nullable(false)
+ .add("b", SqlTypeName.BIGINT)
+ .nullable(true)
+ .add("c", SqlTypeName.BIGINT)
+ .nullable(true)
+ .add("d", SqlTypeName.BIGINT)
+ .nullable(true)
+ .build();
+ assertThat(root.getRowType(), hasRelDataType(expectedRowType));
+ }
+
/** Test case for
* <a
href="https://issues.apache.org/jira/browse/CALCITE-1522">[CALCITE-1522]
* Fix error message for SetOp with incompatible args</a>. */
@@ -2550,6 +2617,72 @@ private static RelNode groupIdRel(RelBuilder builder,
boolean extra) {
assertThat(root, hasTree(expected));
}
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-6451">[CALCITE-6451]
+ * Improve Nullability Derivation for Intersect and Minus</a>. */
+ @ParameterizedTest
+ @ValueSource(booleans = {false, true})
+ void testIntersectTypeDerivation(boolean all) {
+ final RelBuilder builder = RelBuilder.create(config().build());
+
+ RelDataType input1RowType =
+ new RelDataTypeFactory.Builder(builder.getTypeFactory())
+ .add("a", SqlTypeName.BIGINT)
+ .nullable(false)
+ .add("b", SqlTypeName.BIGINT)
+ .nullable(false)
+ .add("c", SqlTypeName.BIGINT)
+ .nullable(true)
+ .add("d", SqlTypeName.BIGINT)
+ .nullable(true)
+ .build();
+
+ RelDataType input2RowType =
+ new RelDataTypeFactory.Builder(builder.getTypeFactory())
+ .add("a", SqlTypeName.BIGINT)
+ .nullable(true)
+ .add("b", SqlTypeName.BIGINT)
+ .nullable(true)
+ .add("c", SqlTypeName.BIGINT)
+ .nullable(true)
+ .add("d", SqlTypeName.BIGINT)
+ .nullable(true)
+ .build();
+
+ RelDataType input3RowType =
+ new RelDataTypeFactory.Builder(builder.getTypeFactory())
+ .add("a", SqlTypeName.BIGINT)
+ .nullable(false)
+ .add("b", SqlTypeName.BIGINT)
+ .nullable(true)
+ .add("c", SqlTypeName.BIGINT)
+ .nullable(false)
+ .add("d", SqlTypeName.BIGINT)
+ .nullable(true)
+ .build();
+
+ RelNode root =
+ builder
+ .values(input1RowType)
+ .values(input2RowType)
+ .values(input3RowType)
+ .intersect(all, 3)
+ .build();
+
+ RelDataType expectedRowType =
+ new RelDataTypeFactory.Builder(builder.getTypeFactory())
+ .add("a", SqlTypeName.BIGINT)
+ .nullable(false)
+ .add("b", SqlTypeName.BIGINT)
+ .nullable(false)
+ .add("c", SqlTypeName.BIGINT)
+ .nullable(false)
+ .add("d", SqlTypeName.BIGINT)
+ .nullable(true)
+ .build();
+ assertThat(root.getRowType(), hasRelDataType(expectedRowType));
+ }
+
@Test void testExcept() {
// Equivalent SQL:
// SELECT empno FROM emp
@@ -2577,6 +2710,223 @@ private static RelNode groupIdRel(RelBuilder builder,
boolean extra) {
assertThat(root, hasTree(expected));
}
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-6451">[CALCITE-6451]
+ * Improve Nullability Derivation for Intersect and Minus</a>. */
+ @ParameterizedTest
+ @ValueSource(booleans = {false, true})
+ void testExceptTypeDerivation(boolean all) {
+ final RelBuilder builder = RelBuilder.create(config().build());
+
+ RelDataType primaryRowType =
+ new RelDataTypeFactory.Builder(builder.getTypeFactory())
+ .add("a", SqlTypeName.BIGINT)
+ .nullable(false)
+ .add("b", SqlTypeName.BIGINT)
+ .nullable(false)
+ .add("c", SqlTypeName.BIGINT)
+ .nullable(true)
+ .add("d", SqlTypeName.BIGINT)
+ .nullable(true)
+ .build();
+
+ RelDataType secondaryRowType =
+ new RelDataTypeFactory.Builder(builder.getTypeFactory())
+ .add("a", SqlTypeName.BIGINT)
+ .nullable(false)
+ .add("b", SqlTypeName.BIGINT)
+ .nullable(true)
+ .add("c", SqlTypeName.BIGINT)
+ .nullable(false)
+ .add("d", SqlTypeName.BIGINT)
+ .nullable(true)
+ .build();
+
+ RelNode root =
+ builder.values(primaryRowType)
+ .values(secondaryRowType)
+ .minus(all)
+ .build();
+
+ assertThat(root.getRowType(), hasRelDataType(primaryRowType));
+ }
+
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-6451">[CALCITE-6451]
+ * Improve Nullability Derivation for Intersect and Minus</a>. */
+ @ParameterizedTest
+ @ValueSource(booleans = {false, true})
+ void testExceptTypeDerivationWithRowField(boolean all) {
+ final RelBuilder builder = RelBuilder.create(config().build());
+
+ // a BIGINT NOT NULL, b ROW(x INT NOT NULL, y VARCHAR NULL) NOT NULL
+ RelDataType primaryRowType =
+ new RelDataTypeFactory.Builder(builder.getTypeFactory())
+ .add("a", SqlTypeName.BIGINT)
+ .nullable(false)
+ .add("b", builder.getTypeFactory().builder()
+ .add("x", SqlTypeName.INTEGER)
+ .nullable(false)
+ .add("y", SqlTypeName.VARCHAR, 10)
+ .nullable(true)
+ .build())
+ .nullableRecord(false)
+ .build();
+
+ // a BIGINT NULL, b ROW(x INT NULL, y VARCHAR NOT NULL) NOT NULL
+ RelDataType secondaryRowType =
+ new RelDataTypeFactory.Builder(builder.getTypeFactory())
+ .add("a", SqlTypeName.BIGINT)
+ .nullable(true)
+ .add("b", builder.getTypeFactory().builder()
+ .add("x", SqlTypeName.INTEGER)
+ .nullable(true)
+ .add("y", SqlTypeName.VARCHAR, 10)
+ .nullable(false)
+ .build())
+ .nullableRecord(false)
+ .build();
+
+ RelNode root =
+ builder.values(primaryRowType)
+ .values(secondaryRowType)
+ .minus(all)
+ .build();
+
+ // follows primary input's nullability
+ assertThat(root.getRowType(), hasRelDataType(primaryRowType));
+ }
+
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-6451">[CALCITE-6451]
+ * Improve Nullability Derivation for Intersect and Minus</a>. */
+ @ParameterizedTest
+ @ValueSource(booleans = {false, true})
+ void testIntersectTypeDerivationWithRowField(boolean all) {
+ final RelBuilder builder = RelBuilder.create(config().build());
+
+ // a BIGINT NOT NULL, b ROW(x INT NOT NULL, y VARCHAR NULL) NOT NULL
+ RelDataType input1RowType =
+ new RelDataTypeFactory.Builder(builder.getTypeFactory())
+ .add("a", SqlTypeName.BIGINT)
+ .nullable(false)
+ .add("b", builder.getTypeFactory().builder()
+ .add("x", SqlTypeName.INTEGER)
+ .nullable(false)
+ .add("y", SqlTypeName.VARCHAR, 10)
+ .nullable(true)
+ .build())
+ .nullable(false)
+ .build();
+
+ // a BIGINT NULL, b ROW(x INT NULL, y VARCHAR NOT NULL) NOT NULL
+ RelDataType input2RowType =
+ new RelDataTypeFactory.Builder(builder.getTypeFactory())
+ .add("a", SqlTypeName.BIGINT)
+ .nullable(true)
+ .add("b", builder.getTypeFactory().builder()
+ .add("x", SqlTypeName.INTEGER)
+ .nullable(true)
+ .add("y", SqlTypeName.VARCHAR, 10)
+ .nullable(false)
+ .build())
+ .nullable(false)
+ .build();
+
+ RelNode root =
+ builder
+ .values(input1RowType)
+ .values(input2RowType)
+ .intersect(all)
+ .build();
+
+ // a BIGINT NOT NULL
+ // a BIGINT NULL
+ // => a BIGINT NOT NULL
+ //
+ // b ROW(x INT NOT NULL, y VARCHAR NULL) NOT NULL
+ // b ROW(x INT NULL, y VARCHAR NOT NULL) NOT NULL
+ // => b ROW(x INT NOT NULL, y VARCHAR NOT NULL) NOT NULL
+ RelDataType expectedRowType =
+ new RelDataTypeFactory.Builder(builder.getTypeFactory())
+ .add("a", SqlTypeName.BIGINT)
+ .nullable(false)
+ .add("b", builder.getTypeFactory().builder()
+ .add("x", SqlTypeName.INTEGER)
+ .nullable(false)
+ .add("y", SqlTypeName.VARCHAR, 10)
+ .nullable(false)
+ .build())
+ .nullable(false)
+ .build();
+ assertThat(root.getRowType(), hasRelDataType(expectedRowType));
+ }
+
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-6451">[CALCITE-6451]
+ * Improve Nullability Derivation for Intersect and Minus</a>. */
+ @ParameterizedTest
+ @ValueSource(booleans = {false, true})
+ void testUnionTypeDerivationWithRowField(boolean all) {
+ final RelBuilder builder = RelBuilder.create(config().build());
+
+ // a BIGINT NOT NULL, b ROW(x INT NOT NULL, y VARCHAR NULL) NOT NULL
+ RelDataType input1RowType =
+ new RelDataTypeFactory.Builder(builder.getTypeFactory())
+ .add("a", SqlTypeName.BIGINT)
+ .nullable(false)
+ .add("b", builder.getTypeFactory().builder()
+ .add("x", SqlTypeName.INTEGER)
+ .nullable(true)
+ .add("y", SqlTypeName.VARCHAR, 10)
+ .nullable(true)
+ .build())
+ .nullable(true)
+ .build();
+
+ // a BIGINT NULL, b ROW(x INT NULL, y VARCHAR NOT NULL) NOT NULL
+ RelDataType input2RowType =
+ new RelDataTypeFactory.Builder(builder.getTypeFactory())
+ .add("a", SqlTypeName.BIGINT)
+ .nullable(true)
+ .add("b", builder.getTypeFactory().builder()
+ .add("x", SqlTypeName.INTEGER)
+ .nullable(true)
+ .add("y", SqlTypeName.VARCHAR, 10)
+ .nullable(false)
+ .build())
+ .nullable(false)
+ .build();
+
+ RelNode root =
+ builder
+ .values(input1RowType)
+ .values(input2RowType)
+ .union(all)
+ .build();
+
+ // a BIGINT NOT NULL
+ // a BIGINT NULL
+ // => a BIGINT NULL
+ //
+ // b ROW(x INT NULL, y VARCHAR NULL) NULL
+ // b ROW(x INT NULL, y VARCHAR NOT NULL) NOT NULL
+ // => b ROW(x INT NULL, y VARCHAR NULL) NULL
+ RelDataType expectedRowType =
+ new RelDataTypeFactory.Builder(builder.getTypeFactory())
+ .add("a", SqlTypeName.BIGINT)
+ .nullable(true)
+ .add("b", builder.getTypeFactory().builder()
+ .add("x", SqlTypeName.INTEGER)
+ .nullable(true)
+ .add("y", SqlTypeName.VARCHAR, 10)
+ .nullable(true)
+ .build())
+ .nullable(true)
+ .build();
+ assertThat(root.getRowType(), hasRelDataType(expectedRowType));
+ }
+
/** Tests building a simple join. Also checks {@link RelBuilder#size()}
* at every step. */
@Test void testJoin() {
diff --git
a/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml
b/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml
index c782bb913c..c2fb106555 100644
--- a/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml
+++ b/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml
@@ -7319,15 +7319,14 @@ LogicalIntersect(all=[false])
LogicalAggregate(group=[{0}])
LogicalJoin(condition=[IS NOT DISTINCT FROM($0, $1)], joinType=[semi])
LogicalJoin(condition=[IS NOT DISTINCT FROM($0, $1)], joinType=[semi])
- LogicalProject(ENAME=[CAST($0):VARCHAR])
+ LogicalProject(ENAME=[CAST($0):VARCHAR NOT NULL])
LogicalProject(ENAME=[$1])
LogicalFilter(condition=[=($7, 10)])
LogicalTableScan(table=[[CATALOG, SALES, EMP]])
- LogicalProject(ENAME=[CAST($0):VARCHAR])
- LogicalProject(DEPTNO=[CAST($7):VARCHAR NOT NULL])
- LogicalFilter(condition=[OR(=($1, 'a'), =($1, 'b'))])
- LogicalTableScan(table=[[CATALOG, SALES, EMP]])
- LogicalProject(ENAME=[CAST($0):VARCHAR])
+ LogicalProject(DEPTNO=[CAST($7):VARCHAR NOT NULL])
+ LogicalFilter(condition=[OR(=($1, 'a'), =($1, 'b'))])
+ LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+ LogicalProject(ENAME=[CAST($0):VARCHAR NOT NULL])
LogicalProject(ENAME=[$1])
LogicalTableScan(table=[[CATALOG, SALES, EMPNULLABLES]])
]]>
@@ -10453,10 +10452,10 @@ LogicalMinus(all=[false])
</Resource>
<Resource name="planAfter">
<![CDATA[
-LogicalProject(ENAME=[CAST($0):VARCHAR])
+LogicalProject(ENAME=[CAST($0):VARCHAR NOT NULL])
LogicalAggregate(group=[{0}])
- LogicalJoin(condition=[IS NOT DISTINCT FROM(CAST($0):VARCHAR,
CAST($1):VARCHAR)], joinType=[anti])
- LogicalJoin(condition=[=(CAST($0):VARCHAR, $1)], joinType=[anti])
+ LogicalJoin(condition=[IS NOT DISTINCT FROM(CAST($0):VARCHAR NOT NULL,
CAST($1):VARCHAR NOT NULL)], joinType=[anti])
+ LogicalJoin(condition=[=(CAST($0):VARCHAR NOT NULL, $1)],
joinType=[anti])
LogicalProject(ENAME=[$1])
LogicalFilter(condition=[=($7, 10)])
LogicalTableScan(table=[[CATALOG, SALES, EMP]])
diff --git a/core/src/test/resources/sql/planner.iq
b/core/src/test/resources/sql/planner.iq
index 16168b820a..141b31d714 100644
--- a/core/src/test/resources/sql/planner.iq
+++ b/core/src/test/resources/sql/planner.iq
@@ -247,13 +247,13 @@ select a from (values (1.0), (4.0), (null)) as t3 (a);
EnumerableAggregate(group=[{0}])
EnumerableHashJoin(condition=[IS NOT DISTINCT FROM($0, $1)], joinType=[semi])
- EnumerableCalc(expr#0=[{inputs}], expr#1=[CAST($t0):DECIMAL(11, 1)],
A=[$t1])
+ EnumerableCalc(expr#0=[{inputs}], expr#1=[CAST($t0):DECIMAL(11, 1) NOT
NULL], A=[$t1])
EnumerableHashJoin(condition=[IS NOT DISTINCT FROM($0, $1)],
joinType=[semi])
- EnumerableCalc(expr#0=[{inputs}], expr#1=[CAST($t0):DECIMAL(11, 1)],
A=[$t1])
+ EnumerableCalc(expr#0=[{inputs}], expr#1=[CAST($t0):DECIMAL(11, 1) NOT
NULL], A=[$t1])
EnumerableValues(tuples=[[{ 1.0 }, { 2.0 }, { 3.0 }, { 4.0 }, { 5.0
}]])
- EnumerableCalc(expr#0=[{inputs}], expr#1=[CAST($t0):DECIMAL(11, 1)],
A=[$t1])
+ EnumerableCalc(expr#0=[{inputs}], expr#1=[CAST($t0):DECIMAL(11, 1) NOT
NULL], A=[$t1])
EnumerableValues(tuples=[[{ 1 }, { 2 }]])
- EnumerableCalc(expr#0=[{inputs}], expr#1=[CAST($t0):DECIMAL(11, 1)],
A=[$t1])
+ EnumerableCalc(expr#0=[{inputs}], expr#1=[CAST($t0):DECIMAL(11, 1) NOT
NULL], A=[$t1])
EnumerableValues(tuples=[[{ 1.0 }, { 4.0 }, { null }]])
!plan
!set planner-rules original
@@ -302,16 +302,15 @@ select a from (values (1.0), (4.0), (null)) as t3 (a);
!ok
-EnumerableCalc(expr#0..1=[{inputs}], expr#2=[CAST($t0):DECIMAL(11, 1)],
A=[$t2])
+EnumerableAggregate(group=[{0}])
EnumerableHashJoin(condition=[=($1, $3)], joinType=[anti])
- EnumerableCalc(expr#0=[{inputs}], expr#1=[CAST($t0):DECIMAL(11, 1)],
proj#0..1=[{exprs}])
- EnumerableAggregate(group=[{0}])
- EnumerableNestedLoopJoin(condition=[=(CAST($0):DECIMAL(11, 1) NOT
NULL, CAST($1):DECIMAL(11, 1) NOT NULL)], joinType=[anti])
- EnumerableCalc(expr#0=[{inputs}], expr#1=[CAST($t0):DECIMAL(11, 1)
NOT NULL], A=[$t1])
- EnumerableValues(tuples=[[{ 1.0 }, { 2.0 }, { 3.0 }, { 4.0 }, {
5.0 }]])
- EnumerableCalc(expr#0=[{inputs}], expr#1=[CAST($t0):DECIMAL(11, 1)
NOT NULL], A=[$t1])
- EnumerableValues(tuples=[[{ 1 }, { 2 }]])
- EnumerableCalc(expr#0=[{inputs}], expr#1=[CAST($t0):DECIMAL(11, 1)],
A=[$t1], A0=[$t1])
+ EnumerableCalc(expr#0=[{inputs}], expr#1=[CAST($t0):DECIMAL(11, 1) NOT
NULL], proj#0..1=[{exprs}])
+ EnumerableNestedLoopJoin(condition=[=(CAST($0):DECIMAL(11, 1) NOT NULL,
CAST($1):DECIMAL(11, 1) NOT NULL)], joinType=[anti])
+ EnumerableCalc(expr#0=[{inputs}], expr#1=[CAST($t0):DECIMAL(11, 1) NOT
NULL], A=[$t1])
+ EnumerableValues(tuples=[[{ 1.0 }, { 2.0 }, { 3.0 }, { 4.0 }, { 5.0
}]])
+ EnumerableCalc(expr#0=[{inputs}], expr#1=[CAST($t0):DECIMAL(11, 1) NOT
NULL], A=[$t1])
+ EnumerableValues(tuples=[[{ 1 }, { 2 }]])
+ EnumerableCalc(expr#0=[{inputs}], expr#1=[CAST($t0):DECIMAL(11, 1)],
expr#2=[CAST($t0):DECIMAL(11, 1) NOT NULL], A=[$t1], A0=[$t2])
EnumerableValues(tuples=[[{ 1.0 }, { 4.0 }, { null }]])
!plan
!set planner-rules original
diff --git a/testkit/src/main/java/org/apache/calcite/test/Matchers.java
b/testkit/src/main/java/org/apache/calcite/test/Matchers.java
index d410affe73..0ae23f31f1 100644
--- a/testkit/src/main/java/org/apache/calcite/test/Matchers.java
+++ b/testkit/src/main/java/org/apache/calcite/test/Matchers.java
@@ -20,6 +20,7 @@
import org.apache.calcite.rel.RelNode;
import org.apache.calcite.rel.RelValidityChecker;
import org.apache.calcite.rel.hint.Hintable;
+import org.apache.calcite.rel.type.RelDataType;
import org.apache.calcite.rex.RexNode;
import org.apache.calcite.sql.SqlExplainLevel;
import org.apache.calcite.util.TestUtil;
@@ -35,6 +36,7 @@
import org.hamcrest.Matcher;
import org.hamcrest.TypeSafeMatcher;
import org.hamcrest.core.Is;
+import org.hamcrest.core.IsEqual;
import org.hamcrest.core.StringContains;
import java.lang.reflect.Array;
@@ -263,6 +265,17 @@ public static Matcher<RelNode> hasFieldNames(String
fieldNames) {
}
};
}
+
+ /**
+ * Creates a Matcher that matches a {@link RelDataType} if its
+ * {@link RelDataType#getFullTypeString()} is equal to that of the given
{@code relDataType}.
+ */
+ public static Matcher<RelDataType> hasRelDataType(RelDataType relDataType) {
+ return compose(
+ IsEqual.equalTo(relDataType.getFullTypeString()),
+ RelDataType::getFullTypeString);
+ }
+
/**
* Creates a Matcher that matches a {@link RelNode} if its string
* representation, after converting Windows-style line endings ("\r\n")