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 2f096ff2ec [CALCITE-6776] Multiple expanded `IS NOT DISTINCT FROM` cannot be collapsed back 2f096ff2ec is described below commit 2f096ff2eccd98065227c812af09ea949fb719dd Author: Lino Rosa <l...@narrative.io> AuthorDate: Thu Jan 9 09:25:15 2025 -0500 [CALCITE-6776] Multiple expanded `IS NOT DISTINCT FROM` cannot be collapsed back --- .../java/org/apache/calcite/plan/RelOptUtil.java | 37 +-- .../org/apache/calcite/plan/RelOptUtilTest.java | 255 +++++++++++++++++++++ 2 files changed, 275 insertions(+), 17 deletions(-) diff --git a/core/src/main/java/org/apache/calcite/plan/RelOptUtil.java b/core/src/main/java/org/apache/calcite/plan/RelOptUtil.java index 64c8e9c069..0c73d0c00e 100644 --- a/core/src/main/java/org/apache/calcite/plan/RelOptUtil.java +++ b/core/src/main/java/org/apache/calcite/plan/RelOptUtil.java @@ -1748,32 +1748,35 @@ public abstract class RelOptUtil { * and * {@link #splitJoinCondition(List, List, RexNode, List, List, List, List)}. * - * <p>If the given expr <code>call</code> is an expanded version of + * <p>If the given expr <code>rexCall</code> contains an expanded version of * {@code IS NOT DISTINCT FROM} function call, collapses it and return a * {@code IS NOT DISTINCT FROM} function call. * * <p>For example: {@code t1.key IS NOT DISTINCT FROM t2.key} - * can rewritten in expanded form as + * can be rewritten in expanded form as * {@code t1.key = t2.key OR (t1.key IS NULL AND t2.key IS NULL)}. * - * @param call Function expression to try collapsing + * @param rexCall Function expression to try collapsing * @param rexBuilder {@link RexBuilder} instance to create new {@link RexCall} instances. - * @return If the given function is an expanded IS NOT DISTINCT FROM function call, - * return a IS NOT DISTINCT FROM function call. Otherwise return the input - * function call as it is. + * @return A function where all IS NOT DISTINCT FROM are collapsed. */ - public static RexCall collapseExpandedIsNotDistinctFromExpr(final RexCall call, + public static RexCall collapseExpandedIsNotDistinctFromExpr(final RexCall rexCall, final RexBuilder rexBuilder) { - switch (call.getKind()) { - case OR: - return doCollapseExpandedIsNotDistinctFromOrExpr(call, rexBuilder); - - case CASE: - return doCollapseExpandedIsNotDistinctFromCaseExpr(call, rexBuilder); - - default: - return call; - } + final RexShuttle shuttle = new RexShuttle() { + @Override public RexNode visitCall(RexCall call) { + RexCall recursivelyExpanded = (RexCall) super.visitCall(call); + + switch (recursivelyExpanded.getKind()) { + case OR: + return doCollapseExpandedIsNotDistinctFromOrExpr(recursivelyExpanded, rexBuilder); + case CASE: + return doCollapseExpandedIsNotDistinctFromCaseExpr(recursivelyExpanded, rexBuilder); + default: + return recursivelyExpanded; + } + } + }; + return (RexCall) rexCall.accept(shuttle); } private static RexCall doCollapseExpandedIsNotDistinctFromOrExpr(final RexCall call, diff --git a/core/src/test/java/org/apache/calcite/plan/RelOptUtilTest.java b/core/src/test/java/org/apache/calcite/plan/RelOptUtilTest.java index 95a33027c5..ab058c55a2 100644 --- a/core/src/test/java/org/apache/calcite/plan/RelOptUtilTest.java +++ b/core/src/test/java/org/apache/calcite/plan/RelOptUtilTest.java @@ -34,6 +34,7 @@ import org.apache.calcite.rel.type.RelDataTypeFactory; import org.apache.calcite.rel.type.RelDataTypeField; import org.apache.calcite.rel.type.RelDataTypeSystem; import org.apache.calcite.rex.RexBuilder; +import org.apache.calcite.rex.RexCall; import org.apache.calcite.rex.RexInputRef; import org.apache.calcite.rex.RexNode; import org.apache.calcite.schema.SchemaPlus; @@ -420,6 +421,260 @@ class RelOptUtilTest { assertThat(actRightKeys, is(expRightKeys)); } + /** + * Test that {@link RelOptUtil#collapseExpandedIsNotDistinctFromExpr(RexCall, RexBuilder)} + * collapses an expanded version of IS NOT DISTINCT using OR. + */ + @Test void testCollapseExpandedIsNotDistinctFromUsingOr() { + final RexBuilder rexBuilder = relBuilder.getRexBuilder(); + + final RexNode leftEmpNo = + RexInputRef.of(empScan.getRowType().getFieldNames().indexOf("EMPNO"), + empDeptJoinRelFields); + final RexNode rightEmpNo = + RexInputRef.of(empRow.getFieldCount() + deptRow.getFieldNames().indexOf("EMPNO"), + empDeptJoinRelFields); + + // OR(AND(IS NULL($0), IS NULL($7)), IS TRUE(=($0, $7))) + RexNode expanded = relBuilder.isNotDistinctFrom(leftEmpNo, rightEmpNo); + + // IS NOT DISTINCT FROM($0, $7) + RexNode collapsed = + RelOptUtil.collapseExpandedIsNotDistinctFromExpr((RexCall) expanded, rexBuilder); + + RexNode expected = + rexBuilder.makeCall(SqlStdOperatorTable.IS_NOT_DISTINCT_FROM, leftEmpNo, rightEmpNo); + + assertThat(collapsed, is(expected)); + } + + /** + * Test that {@link RelOptUtil#collapseExpandedIsNotDistinctFromExpr(RexCall, RexBuilder)} + * collapses an expanded version of IS NOT DISTINCT using CASE. + */ + @Test void testCollapseExpandedIsNotDistinctFromUsingCase() { + final RexBuilder rexBuilder = relBuilder.getRexBuilder(); + + final RexNode leftEmpNo = + RexInputRef.of(empScan.getRowType().getFieldNames().indexOf("EMPNO"), + empDeptJoinRelFields); + final RexNode rightEmpNo = + RexInputRef.of(empRow.getFieldCount() + deptRow.getFieldNames().indexOf("EMPNO"), + empDeptJoinRelFields); + + // CASE(IS NULL($0), IS NULL($7), IS NULL($7), IS NULL($0), =($0, $7)) + RexNode expanded = + relBuilder.call(SqlStdOperatorTable.CASE, relBuilder.isNull(leftEmpNo), + relBuilder.isNull(rightEmpNo), + relBuilder.isNull(rightEmpNo), + relBuilder.isNull(leftEmpNo), + relBuilder.equals(leftEmpNo, rightEmpNo)); + + // IS NOT DISTINCT FROM($0, $7) + RexNode collapsed = + RelOptUtil.collapseExpandedIsNotDistinctFromExpr((RexCall) expanded, rexBuilder); + + RexNode expected = + rexBuilder.makeCall(SqlStdOperatorTable.IS_NOT_DISTINCT_FROM, leftEmpNo, rightEmpNo); + + assertThat(collapsed, is(expected)); + } + + /** + * Test that {@link RelOptUtil#collapseExpandedIsNotDistinctFromExpr(RexCall, RexBuilder)} + * collapses an expression with expanded versions of IS NOT DISTINCT using OR and CASE. + */ + @Test void testCollapseExpandedIsNotDistinctFromUsingOrAndCase() { + final RexBuilder rexBuilder = relBuilder.getRexBuilder(); + + final RexNode leftEmpNo = + RexInputRef.of(empScan.getRowType().getFieldNames().indexOf("EMPNO"), + empDeptJoinRelFields); + final RexNode rightEmpNo = + RexInputRef.of(empRow.getFieldCount() + deptRow.getFieldNames().indexOf("EMPNO"), + empDeptJoinRelFields); + + final RexNode leftDeptNo = + RexInputRef.of(empScan.getRowType().getFieldNames().indexOf("DEPTNO"), + empDeptJoinRelFields); + final RexNode rightDeptNo = + RexInputRef.of(empRow.getFieldCount() + deptRow.getFieldNames().indexOf("DEPTNO"), + empDeptJoinRelFields); + + // An IS NOT DISTINCT FROM expanded in "CASE" shape + // CASE(IS NULL($0), IS NULL($7), IS NULL($7), IS NULL($0), =($0, $7)) + RexNode expandedCase = + relBuilder.call(SqlStdOperatorTable.CASE, relBuilder.isNull(leftEmpNo), + relBuilder.isNull(rightEmpNo), + relBuilder.isNull(rightEmpNo), + relBuilder.isNull(leftEmpNo), + relBuilder.equals(leftEmpNo, rightEmpNo)); + + // An IS NOT DISTINCT FROM expanded in "OR" shape + // OR(AND(IS NULL($7), IS NULL($8)), =($7, $8)) + RexNode expandedOr = + relBuilder.call( + SqlStdOperatorTable.OR, relBuilder.call(SqlStdOperatorTable.AND, + relBuilder.isNull(leftDeptNo), + relBuilder.isNull(rightDeptNo)), + relBuilder.call(SqlStdOperatorTable.EQUALS, leftDeptNo, rightDeptNo)); + + // AND( + // OR(AND(IS NULL($7), IS NULL($8)), =($7, $8)), + // CASE(IS NULL($0), IS NULL($7), IS NULL($7), IS NULL($0), =($0, $7)) + // ) + RexNode expanded = relBuilder.and(expandedOr, expandedCase); + + // AND(IS NOT DISTINCT FROM($7, $8), IS NOT DISTINCT FROM($0, $7)) + RexNode collapsed = + RelOptUtil.collapseExpandedIsNotDistinctFromExpr((RexCall) expanded, rexBuilder); + + RexNode expected = + rexBuilder.makeCall( + // Expected is nullable because `expandedCase` is nullable + relBuilder.getTypeFactory().createTypeWithNullability(expanded.getType(), true), + SqlStdOperatorTable.AND, + ImmutableList.of( + rexBuilder.makeCall( + SqlStdOperatorTable.IS_NOT_DISTINCT_FROM, + leftEmpNo, + rightEmpNo), + rexBuilder.makeCall( + SqlStdOperatorTable.IS_NOT_DISTINCT_FROM, + leftDeptNo, + rightDeptNo))); + + assertThat(collapsed, is(expected)); + } + + /** + * Test that {@link RelOptUtil#collapseExpandedIsNotDistinctFromExpr(RexCall, RexBuilder)} + * recursively collapses expanded versions of IS NOT DISTINCT. + */ + @Test void testCollapseExpandedIsNotDistinctFromRecursively() { + final RexBuilder rexBuilder = relBuilder.getRexBuilder(); + + final RexNode leftEmpNo = + RexInputRef.of(empScan.getRowType().getFieldNames().indexOf("EMPNO"), + empDeptJoinRelFields); + final RexNode rightEmpNo = + RexInputRef.of(empRow.getFieldCount() + deptRow.getFieldNames().indexOf("EMPNO"), + empDeptJoinRelFields); + + // OR( + // AND( + // IS NULL(OR(AND(IS NULL($0), IS NULL($7)), IS TRUE(=($0, $7)))), + // IS NULL(OR(AND(IS NULL($0), IS NULL($7)), IS TRUE(=($0, $7))))), + // IS TRUE(=( + // OR(AND(IS NULL($0), IS NULL($7)), IS TRUE(=($0, $7))), + // OR(AND(IS NULL($0), IS NULL($7)), IS TRUE(=($0, $7))) + // ) + // ) + // ) + RexNode expanded = + relBuilder.isNotDistinctFrom( + relBuilder.isNotDistinctFrom(leftEmpNo, rightEmpNo), + relBuilder.isNotDistinctFrom(leftEmpNo, rightEmpNo)); + + // IS NOT DISTINCT FROM(IS NOT DISTINCT FROM($0, $7), IS NOT DISTINCT FROM($0, $7)) + RexNode collapsed = + RelOptUtil.collapseExpandedIsNotDistinctFromExpr((RexCall) expanded, rexBuilder); + + RexNode expected = + rexBuilder.makeCall(SqlStdOperatorTable.IS_NOT_DISTINCT_FROM, + rexBuilder.makeCall(SqlStdOperatorTable.IS_NOT_DISTINCT_FROM, + leftEmpNo, + rightEmpNo), + + rexBuilder.makeCall(SqlStdOperatorTable.IS_NOT_DISTINCT_FROM, + leftEmpNo, + rightEmpNo)); + + assertThat(collapsed, is(expected)); + } + + /** + * Test that {@link RelOptUtil#collapseExpandedIsNotDistinctFromExpr(RexCall, RexBuilder)} + * will collapse IS NOT DISTINCT FROM nested within RexNodes. + */ + @Test void testCollapseExpandedIsNotDistinctFromInsideRexNode() { + final RexBuilder rexBuilder = relBuilder.getRexBuilder(); + + final RexNode leftEmpNo = + RexInputRef.of(empScan.getRowType().getFieldNames().indexOf("EMPNO"), + empDeptJoinRelFields); + final RexNode rightEmpNo = + RexInputRef.of(empRow.getFieldCount() + deptRow.getFieldNames().indexOf("EMPNO"), + empDeptJoinRelFields); + + RexNode expandedIsNotDistinctFrom = relBuilder.isNotDistinctFrom(leftEmpNo, rightEmpNo); + // NULLIF( + // NOT(OR(AND(IS NULL($0), IS NULL($7)), IS TRUE(=($0, $7)))), + // IS NOT NULL(OR(AND(IS NULL($0), IS NULL($7)), IS TRUE(=($0, $7)))) + // ) + RexNode expanded = + relBuilder.call(SqlStdOperatorTable.NULLIF, + relBuilder.not(expandedIsNotDistinctFrom), + relBuilder.isNotNull(expandedIsNotDistinctFrom)); + + // NULLIF(NOT(IS NOT DISTINCT FROM($0, $7)), IS NOT NULL(IS NOT DISTINCT FROM($0, $7))) + RexNode collapsed = + RelOptUtil.collapseExpandedIsNotDistinctFromExpr((RexCall) expanded, rexBuilder); + + RexNode collapsedIsNotDistinctFrom = + rexBuilder.makeCall(SqlStdOperatorTable.IS_NOT_DISTINCT_FROM, leftEmpNo, rightEmpNo); + RexNode expected = + rexBuilder.makeCall( + SqlStdOperatorTable.NULLIF, + relBuilder.not(collapsedIsNotDistinctFrom), + relBuilder.isNotNull(collapsedIsNotDistinctFrom)); + + assertThat(collapsed, is(expected)); + } + + /** + * Test that {@link RelOptUtil#collapseExpandedIsNotDistinctFromExpr(RexCall, RexBuilder)} + * can handle collapsing IS NOT DISTINCT FROM composed of other RexNodes. + */ + @Test void testCollapseExpandedIsNotDistinctFromOnContainingRexNodes() { + final RexBuilder rexBuilder = relBuilder.getRexBuilder(); + + final RexNode leftEmpNo = + RexInputRef.of(empScan.getRowType().getFieldNames().indexOf("EMPNO"), + empDeptJoinRelFields); + final RexNode rightEmpNo = + RexInputRef.of(empRow.getFieldCount() + deptRow.getFieldNames().indexOf("EMPNO"), + empDeptJoinRelFields); + + final RexNode leftDeptNo = + RexInputRef.of(empScan.getRowType().getFieldNames().indexOf("DEPTNO"), + empDeptJoinRelFields); + final RexNode rightDeptNo = + RexInputRef.of(empRow.getFieldCount() + deptRow.getFieldNames().indexOf("DEPTNO"), + empDeptJoinRelFields); + + // OR( + // AND(IS NULL(NULLIF($0, $7)), IS NULL(NULLIF($7, $8))), + // IS TRUE(=(NULLIF($0, $7), NULLIF($7, $8))) + // ) + RexNode expanded = + relBuilder.isNotDistinctFrom( + relBuilder.call(SqlStdOperatorTable.NULLIF, leftEmpNo, rightEmpNo), + relBuilder.call(SqlStdOperatorTable.NULLIF, leftDeptNo, rightDeptNo)); + + // IS NOT DISTINCT FROM(NULLIF($0, $7), NULLIF($7, $8)) + RexNode collapsed = + RelOptUtil.collapseExpandedIsNotDistinctFromExpr((RexCall) expanded, rexBuilder); + + RexNode expected = + rexBuilder.makeCall( + SqlStdOperatorTable.IS_NOT_DISTINCT_FROM, + rexBuilder.makeCall(SqlStdOperatorTable.NULLIF, leftEmpNo, rightEmpNo), + rexBuilder.makeCall(SqlStdOperatorTable.NULLIF, leftDeptNo, rightDeptNo)); + + assertThat(collapsed, is(expected)); + } + /** * Tests {@link RelOptUtil#pushDownJoinConditions(org.apache.calcite.rel.core.Join, RelBuilder)} * where the join condition contains a complex expression.