This is an automated email from the ASF dual-hosted git repository.
mbudiu 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 69bfec3dd2 [CALCITE-6904] IS_NOT_DISTINCT_FROM is incorrectly handled
by EnumerableJoinRule
69bfec3dd2 is described below
commit 69bfec3dd298a81afb4c1f43f7520ab6f74c6a59
Author: Zhen Chen <[email protected]>
AuthorDate: Tue Apr 8 06:58:23 2025 +0800
[CALCITE-6904] IS_NOT_DISTINCT_FROM is incorrectly handled by
EnumerableJoinRule
---
.../calcite/adapter/enumerable/RexImpTable.java | 47 ++++++++++++++++++++++
.../java/org/apache/calcite/rel/core/Join.java | 2 +-
.../java/org/apache/calcite/rel/core/JoinInfo.java | 18 ++++++++-
.../calcite/rel/rules/LoptOptimizeJoinRule.java | 2 +-
.../calcite/rel/rules/LoptSemiJoinOptimizer.java | 2 +-
.../calcite/rel/rules/SortJoinTransposeRule.java | 2 +-
.../java/org/apache/calcite/test/JdbcTest.java | 18 +++++++++
core/src/test/resources/sql/join.iq | 21 ++++++++++
8 files changed, 107 insertions(+), 5 deletions(-)
diff --git
a/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java
b/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java
index d5b752dace..3364459261 100644
--- a/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java
+++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java
@@ -425,6 +425,7 @@
import static org.apache.calcite.sql.fun.SqlStdOperatorTable.IS_JSON_SCALAR;
import static org.apache.calcite.sql.fun.SqlStdOperatorTable.IS_JSON_VALUE;
import static org.apache.calcite.sql.fun.SqlStdOperatorTable.IS_NOT_A_SET;
+import static
org.apache.calcite.sql.fun.SqlStdOperatorTable.IS_NOT_DISTINCT_FROM;
import static org.apache.calcite.sql.fun.SqlStdOperatorTable.IS_NOT_EMPTY;
import static org.apache.calcite.sql.fun.SqlStdOperatorTable.IS_NOT_FALSE;
import static org.apache.calcite.sql.fun.SqlStdOperatorTable.IS_NOT_JSON_ARRAY;
@@ -1008,6 +1009,7 @@ void populate2() {
define(IS_NOT_TRUE, new IsNotTrueImplementor());
define(IS_FALSE, new IsFalseImplementor());
define(IS_NOT_FALSE, new IsNotFalseImplementor());
+ define(IS_NOT_DISTINCT_FROM, new IsNotDistinctFromImplementor());
// LIKE, ILIKE, RLIKE and SIMILAR
defineReflective(LIKE, BuiltInMethod.LIKE.method,
@@ -4741,6 +4743,51 @@ private static class IsNullImplementor extends
AbstractRexCallImplementor {
}
}
+ /** Implementor for the {@code IS NOT DISTINCT FROM} SQL operator. */
+ private static class IsNotDistinctFromImplementor extends
AbstractRexCallImplementor {
+ IsNotDistinctFromImplementor() {
+ super("is_not_distinct_from", NullPolicy.NONE, false);
+ }
+
+ @Override public RexToLixTranslator.Result implement(final
RexToLixTranslator translator,
+ final RexCall call, final List<RexToLixTranslator.Result> arguments) {
+ final RexToLixTranslator.Result left = arguments.get(0);
+ final RexToLixTranslator.Result right = arguments.get(1);
+
+ // Generated expression:
+ // left IS NULL ?
+ // (right IS NULL ? TRUE : FALSE) : -> when left is null
+ // (right IS NULL ? FALSE : -> when left is not null
+ // left.equals(right)) -> when both are not null,
compare values
+ final Expression valueExpression =
+ Expressions.condition(left.isNullVariable,
+ Expressions.condition(right.isNullVariable, BOXED_TRUE_EXPR,
BOXED_FALSE_EXPR),
+ Expressions.condition(right.isNullVariable, BOXED_FALSE_EXPR,
+ Expressions.equal(left.valueVariable, right.valueVariable)));
+
+ BlockBuilder builder = translator.getBlockBuilder();
+ final ParameterExpression valueVariable =
+ Expressions.parameter(valueExpression.getType(),
+ builder.newName(variableName + "_value"));
+ final ParameterExpression isNullVariable =
+ Expressions.parameter(Boolean.TYPE,
+ builder.newName(variableName + "_isNull"));
+
+ builder.add(
+ Expressions.declare(Modifier.FINAL, valueVariable, valueExpression));
+ builder.add(
+ Expressions.declare(Modifier.FINAL, isNullVariable, FALSE_EXPR));
+
+ return new RexToLixTranslator.Result(isNullVariable, valueVariable);
+ }
+
+ @Override Expression implementSafe(final RexToLixTranslator translator,
+ final RexCall call, final List<Expression> argValueList) {
+ throw new IllegalStateException("This implementSafe should not be
called,"
+ + " please call implement(...)");
+ }
+ }
+
/** Implementor for the {@code IS TRUE} SQL operator. */
private static class IsTrueImplementor extends AbstractRexCallImplementor {
IsTrueImplementor() {
diff --git a/core/src/main/java/org/apache/calcite/rel/core/Join.java
b/core/src/main/java/org/apache/calcite/rel/core/Join.java
index 540c5806be..8016e9f36a 100644
--- a/core/src/main/java/org/apache/calcite/rel/core/Join.java
+++ b/core/src/main/java/org/apache/calcite/rel/core/Join.java
@@ -104,7 +104,7 @@ protected Join(
this.condition = requireNonNull(condition, "condition");
this.variablesSet = ImmutableSet.copyOf(variablesSet);
this.joinType = requireNonNull(joinType, "joinType");
- this.joinInfo = JoinInfo.of(left, right, condition);
+ this.joinInfo = JoinInfo.createWithStrictEquality(left, right, condition);
this.hints = ImmutableList.copyOf(hints);
}
diff --git a/core/src/main/java/org/apache/calcite/rel/core/JoinInfo.java
b/core/src/main/java/org/apache/calcite/rel/core/JoinInfo.java
index 2a33714838..f092a73ea9 100644
--- a/core/src/main/java/org/apache/calcite/rel/core/JoinInfo.java
+++ b/core/src/main/java/org/apache/calcite/rel/core/JoinInfo.java
@@ -38,7 +38,7 @@
* <p>It is useful for the many algorithms that care whether a join has an
* equi-join condition.
*
- * <p>You can create one using {@link #of}, or call
+ * <p>You can create one using {@link #createWithStrictEquality}, or call
* {@link Join#analyzeCondition()}; many kinds of join cache their
* join info, especially those that are equi-joins.
*
@@ -70,6 +70,22 @@ public static JoinInfo of(RelNode left, RelNode right,
RexNode condition) {
ImmutableIntList.copyOf(rightKeys), ImmutableList.copyOf(nonEquiList));
}
+ /** Creates a {@code JoinInfo} by analyzing a condition.
+ * A JoinInfo created using this method only considers
+ * EQUALS operations equi-conditions; in contrast
+ * IS_NOT_DISTINCT_FROM operations are NOT considered equalities;
+ * they are inserted in the nonEquiList. */
+ public static JoinInfo createWithStrictEquality(RelNode left,
+ RelNode right, RexNode condition) {
+ final List<Integer> leftKeys = new ArrayList<>();
+ final List<Integer> rightKeys = new ArrayList<>();
+ final List<RexNode> nonEquiList = new ArrayList<>();
+ RelOptUtil.splitJoinCondition(left, right, condition, leftKeys, rightKeys,
+ null, nonEquiList);
+ return new JoinInfo(ImmutableIntList.copyOf(leftKeys),
+ ImmutableIntList.copyOf(rightKeys), ImmutableList.copyOf(nonEquiList));
+ }
+
/** Creates an equi-join. */
public static JoinInfo of(ImmutableIntList leftKeys,
ImmutableIntList rightKeys) {
diff --git
a/core/src/main/java/org/apache/calcite/rel/rules/LoptOptimizeJoinRule.java
b/core/src/main/java/org/apache/calcite/rel/rules/LoptOptimizeJoinRule.java
index 16cb367ac2..75056dd981 100644
--- a/core/src/main/java/org/apache/calcite/rel/rules/LoptOptimizeJoinRule.java
+++ b/core/src/main/java/org/apache/calcite/rel/rules/LoptOptimizeJoinRule.java
@@ -2053,7 +2053,7 @@ public static boolean isRemovableSelfJoin(Join joinRel) {
*/
private static boolean areSelfJoinKeysUnique(RelMetadataQuery mq,
RelNode leftRel, RelNode rightRel, RexNode joinFilters) {
- final JoinInfo joinInfo = JoinInfo.of(leftRel, rightRel, joinFilters);
+ final JoinInfo joinInfo = JoinInfo.createWithStrictEquality(leftRel,
rightRel, joinFilters);
// Make sure each key on the left maps to the same simple column as the
// corresponding key on the right
diff --git
a/core/src/main/java/org/apache/calcite/rel/rules/LoptSemiJoinOptimizer.java
b/core/src/main/java/org/apache/calcite/rel/rules/LoptSemiJoinOptimizer.java
index 724884641d..e9d5020b0d 100644
--- a/core/src/main/java/org/apache/calcite/rel/rules/LoptSemiJoinOptimizer.java
+++ b/core/src/main/java/org/apache/calcite/rel/rules/LoptSemiJoinOptimizer.java
@@ -264,7 +264,7 @@ private static int isSuitableFilter(
RelNode factRel = multiJoin.getJoinFactor(factIdx);
RelNode dimRel = multiJoin.getJoinFactor(dimIdx);
- final JoinInfo joinInfo = JoinInfo.of(factRel, dimRel, semiJoinCondition);
+ final JoinInfo joinInfo = JoinInfo.createWithStrictEquality(factRel,
dimRel, semiJoinCondition);
assert !joinInfo.leftKeys.isEmpty();
// mutable copies
diff --git
a/core/src/main/java/org/apache/calcite/rel/rules/SortJoinTransposeRule.java
b/core/src/main/java/org/apache/calcite/rel/rules/SortJoinTransposeRule.java
index e3d2d5b3c5..c59437a5b8 100644
--- a/core/src/main/java/org/apache/calcite/rel/rules/SortJoinTransposeRule.java
+++ b/core/src/main/java/org/apache/calcite/rel/rules/SortJoinTransposeRule.java
@@ -77,7 +77,7 @@ public SortJoinTransposeRule(Class<? extends Sort> sortClass,
final Join join = call.rel(1);
final RelMetadataQuery mq = call.getMetadataQuery();
final JoinInfo joinInfo =
- JoinInfo.of(join.getLeft(), join.getRight(), join.getCondition());
+ JoinInfo.createWithStrictEquality(join.getLeft(), join.getRight(),
join.getCondition());
// 1) If join is not a left or right outer, we bail out
// 2) If sort is not a trivial order-by, and if there is
diff --git a/core/src/test/java/org/apache/calcite/test/JdbcTest.java
b/core/src/test/java/org/apache/calcite/test/JdbcTest.java
index e5c861babc..ac1948f2e6 100644
--- a/core/src/test/java/org/apache/calcite/test/JdbcTest.java
+++ b/core/src/test/java/org/apache/calcite/test/JdbcTest.java
@@ -3950,6 +3950,24 @@ public void checkOrderBy(final boolean desc,
.returnsUnordered("empid=150; name=Sebastian");
}
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-6904">[CALCITE-6904]
+ * IS_NOT_DISTINCT_FROM is converted error in EnumerableJoinRule</a>. */
+ @Test void testIsNotDistinctFrom() {
+ final String sql = ""
+ + "select \"t1\".\"commission\" from \"hr\".\"emps\" as \"t1\"\n"
+ + "join\n"
+ + "\"hr\".\"emps\" as \"t2\"\n"
+ + "on \"t1\".\"commission\" is not distinct from
\"t2\".\"commission\"";
+ CalciteAssert.hr()
+ .query(sql)
+ .explainContains("NestedLoopJoin(condition=[IS NOT DISTINCT FROM($0,
$1)]")
+ .returnsUnordered("commission=1000",
+ "commission=250",
+ "commission=500",
+ "commission=null");
+ }
+
@Test void testExcept() {
final String sql = ""
+ "select \"empid\", \"name\" from \"hr\".\"emps\" where
\"deptno\"=10\n"
diff --git a/core/src/test/resources/sql/join.iq
b/core/src/test/resources/sql/join.iq
index 6330a8d746..a5ee6eea15 100644
--- a/core/src/test/resources/sql/join.iq
+++ b/core/src/test/resources/sql/join.iq
@@ -833,4 +833,25 @@ JOIN (VALUES (3, 7), (6, 14), (9, 21), (12, 28), (9, 35))
AS t3(b, d) USING (b);
!ok
+SELECT t1.a
+FROM (VALUES (1), (2)) AS t1(a)
+JOIN (VALUES (1), (null)) AS t2(a)
+ON t1.a IS DISTINCT FROM t2.a;
++---+
+| A |
++---+
+| 1 |
+| 2 |
+| 2 |
++---+
+(3 rows)
+
+!ok
+
+EnumerableCalc(expr#0..1=[{inputs}], A=[$t0])
+ EnumerableNestedLoopJoin(condition=[IS NOT TRUE(=($0, $1))],
joinType=[inner])
+ EnumerableValues(tuples=[[{ 1 }, { 2 }]])
+ EnumerableValues(tuples=[[{ 1 }, { null }]])
+!plan
+
# End join.iq