This is an automated email from the ASF dual-hosted git repository.
xuzifu666 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 779e37de8f [CALCITE-7915] RelDecorrelator does not recognize IS NOT
DISTINCT FROM as correlation equivalent
779e37de8f is described below
commit 779e37de8fa9a360eb4f0bd46f7206ead01dfced
Author: Yu Xu <[email protected]>
AuthorDate: Thu Nov 5 20:49:29 2026 +0800
[CALCITE-7915] RelDecorrelator does not recognize IS NOT DISTINCT FROM as
correlation equivalent
---
.../apache/calcite/sql2rel/RelDecorrelator.java | 44 ++++++++++++++++++----
.../calcite/sql2rel/RelDecorrelatorTest.java | 37 ++++++++++++++++++
2 files changed, 73 insertions(+), 8 deletions(-)
diff --git a/core/src/main/java/org/apache/calcite/sql2rel/RelDecorrelator.java
b/core/src/main/java/org/apache/calcite/sql2rel/RelDecorrelator.java
index 0701e592ff..1f5272f29d 100644
--- a/core/src/main/java/org/apache/calcite/sql2rel/RelDecorrelator.java
+++ b/core/src/main/java/org/apache/calcite/sql2rel/RelDecorrelator.java
@@ -1757,21 +1757,29 @@ private Frame createFrameWithValueGenerator(RelNode
rel, Frame frame,
* ensuring that we only map the correlation variable to a local field or
* expression from the current relational expression (e.g., a {@link
RexInputRef}),
* rather than to another correlation variable.
+ *
+ * <p>{@code IS NOT DISTINCT FROM} establishes the same equivalence as
+ * {@code =} (it compares values null-safely), so it is also recognized.
*/
private static void findCorrelationEquivalent(CorRef correlation, RexNode e)
throws Util.FoundOne {
switch (e.getKind()) {
case EQUALS:
+ case IS_NOT_DISTINCT_FROM:
final RexCall call = (RexCall) e;
final List<RexNode> operands = call.getOperands();
- if (!RexUtil.containsFieldAccess(operands.get(1))
- && references(operands.get(0), correlation)) {
- throw new Util.FoundOne(operands.get(1));
- }
- if (!RexUtil.containsFieldAccess(operands.get(0))
- && references(operands.get(1), correlation)) {
- throw new Util.FoundOne(operands.get(0));
- }
+ // For IS NOT DISTINCT FROM, binding the correlation to a local operand
+ // is only sound if that operand is NOT NULL. Decorrelation leaves the
+ // original filter in place on the inner side of the join, with the
+ // correlation substituted by its binding, so the filter condition
+ // becomes the residual condition {@code IS NOT DISTINCT FROM(x, x)},
+ // which is always true (hence harmless). The null-safe comparison of
+ // the original condition is instead enforced by the join condition.
+ // If x were nullable, inner rows with x = NULL, which the original
+ // condition accepts when the correlated column is null, would be
+ // rejected by the null-rejecting join condition, changing the result.
+ findEqualityEquivalent(correlation, operands.get(0), operands.get(1),
+ e.getKind() == SqlKind.IS_NOT_DISTINCT_FROM);
break;
case AND:
for (RexNode operand : ((RexCall) e).getOperands()) {
@@ -1783,6 +1791,26 @@ && references(operands.get(1), correlation)) {
}
}
+ /** If one of {@code operand0} and {@code operand1} references the given
+ * correlation, and the other does not contain any {@link RexFieldAccess},
+ * throws the other operand as a {@link
org.apache.calcite.util.Util.FoundOne}.
+ *
+ * <p>If {@code requireNotNullOperand} is true, the non-correlation operand
+ * must additionally be NOT NULL to be accepted. */
+ private static void findEqualityEquivalent(CorRef correlation, RexNode
operand0,
+ RexNode operand1, boolean requireNotNullOperand) throws Util.FoundOne {
+ if (!RexUtil.containsFieldAccess(operand1)
+ && references(operand0, correlation)
+ && !(requireNotNullOperand && operand1.getType().isNullable())) {
+ throw new Util.FoundOne(operand1);
+ }
+ if (!RexUtil.containsFieldAccess(operand0)
+ && references(operand1, correlation)
+ && !(requireNotNullOperand && operand0.getType().isNullable())) {
+ throw new Util.FoundOne(operand0);
+ }
+ }
+
private static boolean references(RexNode e, CorRef correlation) {
switch (e.getKind()) {
case CAST:
diff --git
a/core/src/test/java/org/apache/calcite/sql2rel/RelDecorrelatorTest.java
b/core/src/test/java/org/apache/calcite/sql2rel/RelDecorrelatorTest.java
index d8f1fb54b2..e59b4c163c 100644
--- a/core/src/test/java/org/apache/calcite/sql2rel/RelDecorrelatorTest.java
+++ b/core/src/test/java/org/apache/calcite/sql2rel/RelDecorrelatorTest.java
@@ -657,6 +657,43 @@ public static Frameworks.ConfigBuilder config() {
assertThat(after, hasTree(planAfter));
}
+ /** Test case for <a
href="https://issues.apache.org/jira/browse/CALCITE-7915">[CALCITE-7915]
+ * RelDecorrelator does not recognize IS NOT DISTINCT FROM as correlation
equivalent</a>. */
+ @Test void testCorrelationEquivalentIsNotDistinctFrom() {
+ final RelBuilder builder = RelBuilder.create(config().build());
+ // Simplification is disabled to keep the IS NOT DISTINCT FROM condition
+ // intact (otherwise it would be rewritten in terms of = and IS NULL).
+ final RelBuilder noSimplify = builder.transform(c ->
c.withSimplify(false));
+ final Holder<@Nullable RexCorrelVariable> v = Holder.empty();
+ RelNode before = noSimplify.scan("EMP")
+ .variable(v::set)
+ .scan("DEPT")
+ .filter(
+ noSimplify.call(SqlStdOperatorTable.IS_NOT_DISTINCT_FROM,
+ noSimplify.field(0),
+ noSimplify.field(v.get(), "DEPTNO")))
+ .correlate(JoinRelType.LEFT, v.get().id, noSimplify.field(2, 0,
"DEPTNO"))
+ .build();
+
+ final String planBefore = ""
+ + "LogicalCorrelate(correlation=[$cor0], joinType=[left],
requiredColumns=[{7}])\n"
+ + " LogicalTableScan(table=[[scott, EMP]])\n"
+ + " LogicalFilter(condition=[IS NOT DISTINCT FROM($0,
$cor0.DEPTNO)])\n"
+ + " LogicalTableScan(table=[[scott, DEPT]])\n";
+ assertThat(before, hasTree(planBefore));
+
+ RelNode after = RelDecorrelator.decorrelateQuery(before, builder);
+ // The correlation variable is bound to the local DEPTNO column (found
+ // through the IS NOT DISTINCT FROM condition), so no value generator is
+ // needed; the null-safe comparison is enforced by the join condition.
+ final String planAfter = ""
+ + "LogicalJoin(condition=[=($7, $8)], joinType=[left])\n"
+ + " LogicalTableScan(table=[[scott, EMP]])\n"
+ + " LogicalFilter(condition=[IS NOT DISTINCT FROM($0, $0)])\n"
+ + " LogicalTableScan(table=[[scott, DEPT]])\n";
+ assertThat(after, hasTree(planAfter));
+ }
+
@Test void testCorrelationInSetOp0() {
final FrameworkConfig frameworkConfig = config().build();
final RelBuilder builder = RelBuilder.create(frameworkConfig);