diff --git a/core/src/main/java/org/apache/calcite/rel/rules/SubQueryRemoveRule.java b/core/src/main/java/org/apache/calcite/rel/rules/SubQueryRemoveRule.java index 74cab5798022..c92bd214b878 100644 --- a/core/src/main/java/org/apache/calcite/rel/rules/SubQueryRemoveRule.java +++ b/core/src/main/java/org/apache/calcite/rel/rules/SubQueryRemoveRule.java @@ -19,7 +19,6 @@ import org.apache.calcite.plan.RelOptRuleCall; import org.apache.calcite.plan.RelOptUtil; import org.apache.calcite.plan.RelRule; -import org.apache.calcite.rel.RelHomogeneousShuttle; import org.apache.calcite.rel.RelNode; import org.apache.calcite.rel.core.Collect; import org.apache.calcite.rel.core.Correlate; @@ -30,8 +29,10 @@ import org.apache.calcite.rel.core.Project; import org.apache.calcite.rel.core.TableFunctionScan; import org.apache.calcite.rel.logical.LogicalCorrelate; +import org.apache.calcite.rel.logical.LogicalJoin; 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.rex.LogicVisitor; import org.apache.calcite.rex.RexBuilder; import org.apache.calcite.rex.RexCorrelVariable; @@ -46,17 +47,23 @@ import org.apache.calcite.sql.SqlOperator; import org.apache.calcite.sql.fun.SqlQuantifyOperator; import org.apache.calcite.sql.fun.SqlStdOperatorTable; +import org.apache.calcite.sql.validate.SqlValidatorUtil; +import org.apache.calcite.sql2rel.DeduplicateCorrelateVariables; import org.apache.calcite.sql2rel.RelDecorrelator; import org.apache.calcite.tools.RelBuilder; import org.apache.calcite.util.ImmutableBitSet; import org.apache.calcite.util.Pair; +import org.apache.calcite.util.mapping.Mapping; +import org.apache.calcite.util.mapping.MappingType; +import org.apache.calcite.util.mapping.Mappings; import com.google.common.collect.ImmutableList; -import com.google.common.collect.Iterables; +import com.google.common.collect.ImmutableSet; import org.immutables.value.Value; import java.util.ArrayList; +import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import java.util.stream.Collectors; @@ -1105,44 +1112,79 @@ private static void matchTableFunctionScan(SubQueryRemoveRule rule, private static void matchJoin(SubQueryRemoveRule rule, RelOptRuleCall call) { final Join join = call.rel(0); final RelBuilder builder = call.builder(); + // rewrites one sub-query per call and relies on the rule firing repeatedly final RexSubQuery e = requireNonNull(RexUtil.SubQueryFinder.find(join.getCondition())); - ImmutableBitSet inputSet = RelOptUtil.InputFinder.bits(e.getOperands(), null); - int nFieldsLeft = join.getLeft().getRowType().getFieldCount(); - int nFieldsRight = join.getRight().getRowType().getFieldCount(); - - // Correlation columns should also be considered. - // For example: - // LogicalJoin - // left right - // | | - // LogicalProject.NONE.[0, 1] LogicalValues.NONE.[0] - // RecordType(INTEGER DEPTNO, CHAR(11) DNAME) RecordType(INTEGER DEPTNO) - // - // and subquery: $SCALAR_QUERY with correlate - // LogicalProject(DEPTNO=[$1]) - // LogicalFilter(condition=[=(CAST($0):CHAR(11) NOT NULL, $cor0.DNAME)]) - // - // In such a case $cor0.DNAME need to be accounted as input form left side. - final Set variablesSet = RelOptUtil.getVariablesUsed(e.rel); - variablesSet.retainAll(join.getVariablesSet()); - for (CorrelationId id : variablesSet) { - ImmutableBitSet requiredColumns = RelOptUtil.correlationColumns(id, e.rel); - inputSet = ImmutableBitSet.union(ImmutableList.of(requiredColumns, inputSet)); + final int nFieldsLeft = join.getLeft().getRowType().getFieldCount(); + final int nFieldsRight = join.getRight().getRowType().getFieldCount(); + + final ImmutableBitSet subqueryUsedFields = + RelOptUtil.InputFinder.analyze(e, join.getVariablesSet()).build(); + + final boolean inputIntersectsLeftSide = + subqueryUsedFields.intersects(ImmutableBitSet.range(0, nFieldsLeft)); + final boolean inputIntersectsRightSide = + subqueryUsedFields.intersects( + ImmutableBitSet.range(nFieldsLeft, nFieldsLeft + nFieldsRight)); + if (inputIntersectsLeftSide && inputIntersectsRightSide + && join.getJoinType() != JoinRelType.INNER) { + // Rewriting requires flattening the join into a cross-product first (see below). + // That transformation is only valid for INNER JOIN (A JOIN B ON c ≡ (A × B) WHERE c). + // For OUTER JOINs the semantics differ (NULL-padding), so we bail out. We bail + // out before creating any correlation id, so that an abandoned match does not + // shift the ids of unrelated plans. + return; } - boolean inputIntersectsLeftSide = inputSet.intersects(ImmutableBitSet.range(0, nFieldsLeft)); - boolean inputIntersectsRightSide = - inputSet.intersects(ImmutableBitSet.range(nFieldsLeft, nFieldsLeft + nFieldsRight)); - if (inputIntersectsLeftSide && inputIntersectsRightSide) { - if (join.getJoinType() != JoinRelType.INNER) { - // Rewriting requires flattening the join into a cross-product first (see below). - // That transformation is only valid for INNER JOIN (A JOIN B ON c ≡ (A × B) WHERE c). - // For OUTER JOINs the semantics differ (NULL-padding), so we bail out. - return; + // This rule rewrites one sub-query, "e", per call; the other sub-queries + // remain in the condition, to be rewritten by later calls. Split the + // correlation variables of the join accordingly: + // * subQueryVariables, used by "e", are bound by the rewrite of "e" + // (rule.apply), against the row type of one input or of the + // cross-product; + // * remainingVariables, used by the other sub-queries, must still be + // defined by the relational expression that replaces the join, against + // its joint row type; otherwise those sub-queries would become + // uncorrelated, with their variables undefined. + // A variable in both sets would be defined twice, with different row types, + // so "e" gets fresh ids for such variables (see renameSharedVariables), + // whereas the other sub-queries keep the original ids. Variables that are + // bound by an outer scope are not the join's, and are left alone. + // + // Sub-queries are compared by value, as in ReplaceSubQueryShuttle: those + // equal to "e" are all rewritten at once, so none of them remains. The + // condition still contains "e" with its original ids, so "e", not + // unsharedSubQuery, is used to locate the sub-query in the condition. + final RexUtil.SubQueryCollector collector = new RexUtil.SubQueryCollector(true); + join.getCondition().accept(collector); + final List otherSubQueries = new ArrayList<>(); + for (RexSubQuery subQuery : collector.getSubQueries()) { + if (!subQuery.equals(e)) { + otherSubQueries.add(subQuery); } + } + final Set subQueryVariables = new LinkedHashSet<>(join.getVariablesSet()); + subQueryVariables.retainAll(RelOptUtil.getVariablesUsed(e.rel)); + final Set remainingVariables = new LinkedHashSet<>(join.getVariablesSet()); + remainingVariables.retainAll(RelOptUtil.getVariablesUsed(otherSubQueries)); + + final Set sharedVariables = new LinkedHashSet<>(subQueryVariables); + sharedVariables.retainAll(remainingVariables); + final RexSubQuery unsharedSubQuery; + final Set variablesSet; + if (sharedVariables.isEmpty()) { + unsharedSubQuery = e; + variablesSet = subQueryVariables; + } else { + final Pair> unshared = + renameSharedVariables(e, subQueryVariables, sharedVariables, builder); + unsharedSubQuery = (RexSubQuery) unshared.left; + variablesSet = unshared.right; + } + + if (inputIntersectsLeftSide && inputIntersectsRightSide) { // The sub-query operands span both sides of the join, e.g.: // SELECT empno FROM emp JOIN dept // ON emp.deptno + dept.deptno >= SOME(SELECT deptno FROM dept) @@ -1175,14 +1217,21 @@ private static void matchJoin(SubQueryRemoveRule rule, RelOptRuleCall call) { final RelOptUtil.Logic logic = LogicVisitor.find(RelOptUtil.Logic.TRUE, ImmutableList.of(join.getCondition()), e); final RexNode target = - rule.apply(e, variablesSet, logic, builder, 1, count, 0); + rule.apply(unsharedSubQuery, variablesSet, logic, builder, 1, count, 0); // Step 3 – replace the sub-query in the ON condition with the expression (target) // produced by rule.apply (e.g. a CASE expression for SOME, IS NOT NULL for EXISTS), // then materialize it as a Filter (valid because the cross-product is already in place). final RexShuttle shuttle = new ReplaceSubQueryShuttle(e, target); - final RexNode newCond = shuttle.apply(join.getCondition()); - builder.filter(newCond); + RexNode newCond = shuttle.apply(join.getCondition()); + + // Keep the variables of the remaining sub-queries defined on the new Filter. + // The auxiliary fields are appended after both inputs, so field indexes do not + // change; only the row type that the variables reference is updated. + newCond = + remapCorrelationVariables(newCond, remainingVariables, + builder.peek().getRowType(), 0, 0, builder.getRexBuilder()); + builder.filter(remainingVariables, newCond); // Step 4 – project away the auxiliary columns introduced by the sub-query rewrite, // restoring the original output schema (fields 0 .. count-1). @@ -1199,16 +1248,31 @@ private static void matchJoin(SubQueryRemoveRule rule, RelOptRuleCall call) { ? RelOptUtil.Logic.TRUE_FALSE_UNKNOWN : RelOptUtil.Logic.TRUE, ImmutableList.of(join.getCondition()), e); + // The sub-query's variables now reference the left input only; the fields + // that it uses are at the same indexes, so only the row type changes. + final RexSubQuery subQuery = + (RexSubQuery) remapCorrelationVariables(unsharedSubQuery, variablesSet, + join.getLeft().getRowType(), 0, 0, builder.getRexBuilder()); final RexNode target = - rule.apply(e, variablesSet, logic, builder, 1, nFieldsLeft, 0); + rule.apply(subQuery, variablesSet, logic, builder, 1, nFieldsLeft, 0); final RexShuttle shuttle = new ReplaceSubQueryShuttle(e, target); + final int nFieldsNewLeft = builder.fields().size(); final RexNode newCond = shuttle.apply( RexUtil.shift(join.getCondition(), nFieldsLeft, - builder.fields().size() - nFieldsLeft)); - builder.push(join.getRight()); - builder.join(join.getJoinType(), newCond); + nFieldsNewLeft - nFieldsLeft)); + + if (remainingVariables.isEmpty()) { + builder.push(join.getRight()); + builder.join(join.getJoinType(), newCond); + } else { + // The remaining sub-queries reference right-side fields through the join's + // correlation variables, so their indexes must be shifted past the auxiliary + // fields that the rewrite appended to the left input. + rebuildJoin(builder, join, builder.build(), join.getRight(), newCond, + remainingVariables, nFieldsLeft, nFieldsNewLeft - nFieldsLeft); + } final int nFields = builder.fields().size(); ImmutableList fields = @@ -1223,58 +1287,12 @@ private static void matchJoin(SubQueryRemoveRule rule, RelOptRuleCall call) { ? RelOptUtil.Logic.TRUE_FALSE_UNKNOWN : RelOptUtil.Logic.TRUE, ImmutableList.of(join.getCondition()), e); - RexSubQuery subQuery = e; + RexSubQuery subQuery = unsharedSubQuery; if (!variablesSet.isEmpty()) { - // Original correlates reference joint row type, but we are about to create - // new join of original right side and correlated sub-query. Therefore we have - // to adjust correlated variables in following way: - // 1) new correlation variable must reference row type of right side only - // 2) field index must be shifted on the size of the left side - // Example: - // SELECT e1.* - // FROM emp e1 - // JOIN dept d - // ON e1.deptno = d.deptno - // AND d.deptno IN ( - // SELECT e3.empno - // FROM emp e3 - // WHERE d.deptno > e3.comm - // ) - // ORDER BY e1.empno, e1.deptno; - // - // LogicalJoin(condition=[AND(=($7, $8), IN(CAST($8):SMALLINT NOT NULL, { - // LogicalProject(EMPNO=[$0]) - // LogicalFilter(condition=[>(CAST($cor0.DEPTNO0):DECIMAL(7, 2) NOT NULL, $6)]) - // LogicalTableScan(table=[[scott, EMP]]) - // }))], joinType=[inner]) - // LogicalTableScan(table=[[scott, EMP]]) - // LogicalProject(DEPTNO=[$0]) - // LogicalTableScan(table=[[scott, DEPT]]) - // - // Rewrite to: - // - // LogicalProject(EMPNO=[$0], ENAME=[$1], ..., COMM=[$6], DEPTNO=[$7], DEPTNO0=[$8]) - // LogicalJoin(condition=[=($7, $8)], joinType=[inner]) - // LogicalTableScan(table=[[scott, EMP]]) - // LogicalFilter(condition=[=(CAST($0):SMALLINT NOT NULL, $1)]) - // LogicalCorrelate(correlation=[$cor0], joinType=[inner], requiredColumns=[{0}]) - // LogicalProject(DEPTNO=[$0]) - // LogicalTableScan(table=[[scott, DEPT]]) - // LogicalProject(EMPNO=[$0]) - // LogicalFilter(condition=[>(CAST($cor0.DEPTNO):DECIMAL(7, 2) NOT NULL, $6)]) - // LogicalTableScan(table=[[scott, EMP]]) - CorrelationId id = Iterables.getOnlyElement(variablesSet); - RexBuilder rexBuilder = builder.getRexBuilder(); - - RelNode newSubQueryRel = e.rel.accept(new RelHomogeneousShuttle() { - @Override public RelNode visit(RelNode other) { - RelNode node = - RexUtil.shiftFieldAccess(rexBuilder, other, id, join.getRight(), -nFieldsLeft); - return super.visit(node); - } - }); - subQuery = e.clone(newSubQueryRel); + subQuery = + (RexSubQuery) remapCorrelationVariables(subQuery, variablesSet, + join.getRight().getRowType(), 0, -nFieldsLeft, builder.getRexBuilder()); } subQuery = @@ -1285,17 +1303,159 @@ private static void matchJoin(SubQueryRemoveRule rule, RelOptRuleCall call) { rule.apply(subQuery, variablesSet, logic, builder, 1, nFieldsRight, 0); final RexShuttle shuttle = new ReplaceSubQueryShuttle(e, RexUtil.shift(target, nFieldsLeft)); - RelNode newRight = builder.build(); - builder.push(join.getLeft()); - builder.push(newRight); + final RelNode newRight = builder.build(); + final RexNode newCond = shuttle.apply(join.getCondition()); - builder.join(join.getJoinType(), shuttle.apply(join.getCondition())); + if (remainingVariables.isEmpty()) { + builder.push(join.getLeft()); + builder.push(newRight); + builder.join(join.getJoinType(), newCond); + } else { + // The auxiliary fields are appended after the right-side fields, so the + // indexes that the remaining sub-queries use do not change (hence start + // and offset 0). + rebuildJoin(builder, join, join.getLeft(), newRight, newCond, + remainingVariables, 0, 0); + } builder.project(fields(builder, nFields)); } call.transformTo(builder.build()); } + /** Gives {@code node} fresh correlation ids for {@code sharedVariables}, a + * subset of {@code variablesSet}, and returns the rewritten expression along + * with its new set of variables. + * + *

Visits the complete expression, including operands and relational + * expressions of nested sub-queries. Otherwise a sub-query in an operand + * could retain an id that its new binder no longer defines. Variables that + * are not shared, including those bound by an outer scope, are unchanged. + * + *

Rewriting the sub-query binds its variables to a new {@link Correlate}, + * whereas the remaining sub-queries need the + * original join to keep defining the same variables over the joint row + * type. For example, in + * + *

{@code
+   * SELECT e.ename
+   * FROM emp e
+   * JOIN dept d
+   *   ON EXISTS (SELECT 1 FROM emp s WHERE s.mgr = e.empno)
+   *   AND EXISTS (SELECT 1 FROM emp s WHERE s.ename = d.dname)
+   * }
+ * + *

both sub-queries use {@code $cor0}. Rewriting the first one renames its + * {@code $cor0} to a fresh id, so that {@code $cor0} is not defined twice: by + * the Correlate over EMP (8 fields) and by the Join over EMP + auxiliary field + * + DEPT (12 fields). */ + private static Pair> renameSharedVariables( + RexNode node, Set variablesSet, + Set sharedVariables, RelBuilder builder) { + final Set newVariablesSet = new LinkedHashSet<>(variablesSet); + for (CorrelationId id : sharedVariables) { + final CorrelationId newId = builder.getCluster().createCorrel(); + node = + DeduplicateCorrelateVariables.go(builder.getRexBuilder(), newId, + ImmutableSet.of(id), node); + newVariablesSet.remove(id); + newVariablesSet.add(newId); + } + return Pair.of(node, newVariablesSet); + } + + /** Creates the join that replaces {@code join} once one of the sub-queries in + * its condition has been rewritten, and pushes it onto {@code builder}. + * + *

The sub-queries that are left in {@code condition} use the join's + * correlation variables {@code remainingVariables}, so the new join must keep + * defining them, otherwise they would be treated as uncorrelated and left + * undefined. Their field indexes are remapped as {@code start} and + * {@code offset} describe (see {@link #remapCorrelationVariables}), because + * the rewrite may have added auxiliary fields to one of the inputs. */ + private static void rebuildJoin(RelBuilder builder, Join join, RelNode left, + RelNode right, RexNode condition, Set remainingVariables, + int start, int offset) { + // The correlation row type comes from createJoinType rather than from the + // join's output, because the ON condition sees both inputs even for the + // SEMI and ANTI joins that do not output the right one. + final RelDataType correlRowType = + SqlValidatorUtil.createJoinType(builder.getTypeFactory(), + left.getRowType(), right.getRowType(), null, + join.getSystemFieldList()); + final RexNode newCondition = + remapCorrelationVariables(condition, remainingVariables, correlRowType, + start, offset, builder.getRexBuilder()); + + // Not RelBuilder#join: given a non-empty variablesSet it may create a + // Correlate (and a Correlate has no room for a condition that references + // both inputs). Not Join#copy either, because that would keep the original, + // now stale, variablesSet. + builder.push( + LogicalJoin.create(left, right, join.getHints(), newCondition, + remainingVariables, join.getJoinType(), join.isSemiJoinDone(), + ImmutableList.copyOf(join.getSystemFieldList()))); + } + + /** Rewrites references to the correlation variables {@code ids} inside the + * sub-queries of {@code node} so that they reference a row of type + * {@code rowType}, shifting fields whose index is at least {@code start} by + * {@code offset}, as {@link RexUtil#shift(RexNode, int, int)} does for input + * references. An {@code offset} of 0 leaves the indexes alone and only + * changes the row type that the variables reference. + * + *

For example, in + * + *

{@code
+   * SELECT e.ename
+   * FROM emp e
+   * LEFT JOIN dept d
+   *   ON e.deptno IN (SELECT s.deptno FROM emp s)
+   *   AND e.deptno = d.deptno
+   *   AND EXISTS (SELECT 1 FROM emp s WHERE s.ename = d.dname)
+   * }
+ * + *

{@code $cor0} is a row of EMP (fields 0..7) + DEPT (fields 8..10), so + * {@code $cor0.DNAME} is field 9. Rewriting the IN appends 4 auxiliary fields + * to the left input, which moves DEPT to fields 12..14; with {@code start = 8} + * and {@code offset = 4}, {@code $cor0.DNAME} becomes field 13 of 15. + * + *

Fields that the shift pushes outside {@code rowType}, which can only + * happen when {@code offset} is negative, are left unmapped, so that a + * reference to one of them is an error rather than a silently wrong index. */ + private static RexNode remapCorrelationVariables(RexNode node, + Set ids, RelDataType rowType, int start, int offset, + RexBuilder rexBuilder) { + if (ids.isEmpty()) { + return node; + } + final int targetCount = rowType.getFieldCount(); + final Mapping mapping; + if (offset == 0) { + mapping = Mappings.createIdentity(targetCount); + } else { + // A mapping from the old positions of the correlation row to the new + // ones; fields before "start" keep their index, the others move by + // "offset". + mapping = + Mappings.create(MappingType.PARTIAL_FUNCTION, targetCount - offset, + targetCount); + for (int source = 0; source < mapping.getSourceCount(); source++) { + final int target = source < start ? source : source + offset; + if (target >= 0 && target < targetCount) { + mapping.set(source, target); + } + } + } + for (CorrelationId id : ids) { + node = + node.accept( + new RelOptUtil.RexCorrelVariableMapShuttle(id, rowType, mapping, + rexBuilder)); + } + return node; + } + private static void matchFilterEnableMarkJoin(SubQueryRemoveRule rule, RelOptRuleCall call) { final Filter filter = call.rel(0); final Set variablesSet = filter.getVariablesSet(); diff --git a/core/src/main/java/org/apache/calcite/sql2rel/DeduplicateCorrelateVariables.java b/core/src/main/java/org/apache/calcite/sql2rel/DeduplicateCorrelateVariables.java index b965b48f6443..2fc9b62c1b41 100644 --- a/core/src/main/java/org/apache/calcite/sql2rel/DeduplicateCorrelateVariables.java +++ b/core/src/main/java/org/apache/calcite/sql2rel/DeduplicateCorrelateVariables.java @@ -57,6 +57,16 @@ public static RelNode go(RexBuilder builder, CorrelationId canonicalId, ImmutableSet.copyOf(alternateIds))); } + /** Rewrites an expression, including operands and relational expressions of + * nested sub-queries, replacing alternate correlation variables with a + * canonical correlation variable. */ + public static RexNode go(RexBuilder builder, CorrelationId canonicalId, + Iterable alternateIds, RexNode node) { + return node.accept( + new DeduplicateCorrelateVariables(builder, canonicalId, + ImmutableSet.copyOf(alternateIds)).dedupRex); + } + @Override public RelNode visit(RelNode other) { RelNode next = super.visit(other); return next.accept(dedupRex); 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 3bdfbd2625fd..59996e80c2c8 100644 --- a/core/src/test/java/org/apache/calcite/plan/RelOptUtilTest.java +++ b/core/src/test/java/org/apache/calcite/plan/RelOptUtilTest.java @@ -47,6 +47,7 @@ import org.apache.calcite.sql.parser.SqlParser; import org.apache.calcite.sql.type.SqlTypeFactoryImpl; import org.apache.calcite.sql.type.SqlTypeName; +import org.apache.calcite.sql2rel.DeduplicateCorrelateVariables; import org.apache.calcite.sql2rel.RexRewritingRelShuttle; import org.apache.calcite.test.CalciteAssert; import org.apache.calcite.tools.Frameworks; @@ -1003,6 +1004,53 @@ private void splitJoinConditionHelper(RexNode joinCond, List expLeftKey assertThat(castNode2.explain(), is(expectNode2.explain())); } + @Test void testDeduplicateCorrelationsInRexExpression() { + final RexBuilder rexBuilder = relBuilder.getRexBuilder(); + final RelOptCluster cluster = relBuilder.getCluster(); + final CorrelationId oldId = cluster.createCorrel(); + final CorrelationId newId = cluster.createCorrel(); + final CorrelationId outerId = cluster.createCorrel(); + final int deptNo = empRow.getFieldNames().indexOf("DEPTNO"); + final RexNode oldRef = + rexBuilder.makeFieldAccess(rexBuilder.makeCorrel(empRow, oldId), deptNo); + final RexNode outerRef = + rexBuilder.makeFieldAccess(rexBuilder.makeCorrel(empRow, outerId), deptNo); + final RelNode innerRel = relBuilder.push(empScan) + .filter(relBuilder.equals(relBuilder.field("DEPTNO"), oldRef)) + .project(relBuilder.field("DEPTNO")) + .build(); + final RexSubQuery nested = RexSubQuery.in(innerRel, ImmutableList.of(oldRef)); + final RelNode outerRel = relBuilder.push(deptScan) + .filter( + relBuilder.and(nested, + relBuilder.equals(relBuilder.field("DEPTNO"), oldRef), + relBuilder.equals(relBuilder.field("DEPTNO"), outerRef))) + .project(relBuilder.field("DEPTNO")) + .build(); + final RexNode inSubQuery = + RexSubQuery.in(outerRel, ImmutableList.of(RexSubQuery.scalar(innerRel))); + final RexNode expression = + relBuilder.and(relBuilder.equals(oldRef, outerRef), inSubQuery); + final RexNode rewritten = + DeduplicateCorrelateVariables.go(rexBuilder, newId, ImmutableSet.of(oldId), expression); + final RelNode rewrittenProject = + relBuilder.push(empScan).project(rewritten).build(); + assertThat(RelOptUtil.getVariablesUsed(rewrittenProject), + is(ImmutableSet.of(newId, outerId))); + final RelNode originalProject = + relBuilder.push(empScan).project(expression).build(); + assertThat(RelOptUtil.getVariablesUsed(originalProject), + is(ImmutableSet.of(oldId, outerId))); + + final Project expected = + (Project) DeduplicateCorrelateVariables.go(rexBuilder, newId, + ImmutableSet.of(oldId), relBuilder.push(empScan).project(expression).build()); + assertThat(rewritten, is(expected.getProjects().get(0))); + final RexNode rewrittenTwice = + DeduplicateCorrelateVariables.go(rexBuilder, newId, ImmutableSet.of(oldId), rewritten); + assertThat(rewrittenTwice, is(rewritten)); + } + @Test void testRemapCorrelHandlesNestedSubQueries() { // Equivalent SQL: // select diff --git a/core/src/test/resources/sql/sub-query.iq b/core/src/test/resources/sql/sub-query.iq index 97ddb5c65d51..e3048f3cced8 100644 --- a/core/src/test/resources/sql/sub-query.iq +++ b/core/src/test/resources/sql/sub-query.iq @@ -11169,6 +11169,51 @@ ORDER BY empno; !ok +# [CALCITE-7882] JOIN ON condition with an uncorrelated sub-query followed by +# a correlated EXISTS fails with "Unable to implement" +# A scalar sub-query and a correlated EXISTS, in both orders +select e.ename +from emp as e join dept as d +on e.deptno = d.deptno + and e.sal < (select max(s.sal) from emp as s) + and exists (select 1 from emp as s where s.mgr = e.empno) +order by e.ename; ++-------+ +| ENAME | ++-------+ +| BLAKE | +| CLARK | +| FORD | +| JONES | +| SCOTT | ++-------+ +(5 rows) + +!ok + +# Several sub-queries in one ON, OR, NOT EXISTS, GROUP BY, IN lists, +# LATERAL, a sub-query in FROM, composite keys, and correlation with +# an outer query +select e.ename, d.dname +from emp as e join dept as d +on e.deptno = d.deptno + and e.sal < (select max(s.sal) from emp as s) + and exists (select 1 from emp as s where s.mgr = e.empno) + and e.mgr not in (select s.mgr from emp as s where s.job = 'CLERK') +order by e.ename; ++-------+------------+ +| ENAME | DNAME | ++-------+------------+ +| BLAKE | SALES | +| CLARK | ACCOUNTING | +| FORD | RESEARCH | +| JONES | RESEARCH | +| SCOTT | RESEARCH | ++-------+------------+ +(5 rows) + +!ok + # [CALCITE-7887] SubQueryRemoveRule fails for a sub-query inside a window function # SELECT list with a sub-query correlated to the current scope followed by # one correlated to an outer scope. SubQueryRemoveRule used to rewrite one