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 37e92aa23ae..ed43131cd32 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 @@ -33,6 +33,7 @@ import org.apache.calcite.rex.LogicVisitor; import org.apache.calcite.rex.RexBuilder; import org.apache.calcite.rex.RexCorrelVariable; +import org.apache.calcite.rex.RexFieldAccess; import org.apache.calcite.rex.RexInputRef; import org.apache.calcite.rex.RexLiteral; import org.apache.calcite.rex.RexNode; @@ -47,15 +48,19 @@ import org.apache.calcite.sql2rel.RelDecorrelator; import org.apache.calcite.tools.RelBuilder; import org.apache.calcite.util.ImmutableBitSet; +import org.apache.calcite.util.Litmus; import org.apache.calcite.util.Pair; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import org.immutables.value.Value; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.stream.Collectors; @@ -967,10 +972,8 @@ private static void matchJoin(SubQueryRemoveRule rule, RelOptRuleCall call) { boolean inputIntersectsRightSide = inputSet.intersects(ImmutableBitSet.range(nFieldsLeft, nFieldsLeft + nFieldsRight)); if (inputIntersectsLeftSide && inputIntersectsRightSide) { - // The current existential rewrite needs to make join with one side of the origin join and - // generate a new condition to replace the on clause. But for RexNode whose operands are - // on either side of the join, we can't push them into join. So this rewriting is not - // supported. + rewriteSubQueryOnDomain(rule, call, e, join, nFieldsLeft, nFieldsRight, + inputSet, builder, variablesSet); return; } @@ -1079,6 +1082,233 @@ private static void matchJoin(SubQueryRemoveRule rule, RelOptRuleCall call) { call.transformTo(builder.build()); } + /** + * Rewrites a sub-query that references columns from both the left and right inputs of a Join. + * + *

This method handles the complex case where a sub-query in a Join condition is correlated + * with both sides of the Join. It performs the following steps: + *

    + *
  1. Identifies the "Domain" of values from the left and right inputs that are relevant + * to the sub-query.
  2. + *
  3. Constructs a "Computation Domain" by cross-joining the distinct keys from the left + * and right domains.
  4. + *
  5. Remaps the sub-query to operate on this Computation Domain.
  6. + *
  7. Rewrites the sub-query using the standard {@link #apply} method, but applied to the + * Domain.
  8. + *
  9. Re-integrates the result of the sub-query rewrite back into the original Join structure, + * ensuring correct join types and conditions are maintained.
  10. + *
+ * + * @param rule The rule instance + * @param call The rule call + * @param e The sub-query to rewrite + * @param join The join containing the sub-query + * @param nFieldsLeft Number of fields in the left input + * @param nFieldsRight Number of fields in the right input + * @param inputSet BitSet of columns used by the sub-query + * @param builder The RelBuilder + * @param variablesSet Set of correlation variables used by the sub-query + */ + private static void rewriteSubQueryOnDomain(SubQueryRemoveRule rule, + RelOptRuleCall call, + RexSubQuery e, + Join join, + int nFieldsLeft, + int nFieldsRight, + ImmutableBitSet inputSet, + RelBuilder builder, + Set variablesSet) { + // Map to store the offset of each correlation variable + final Map idToOffset = new HashMap<>(); + // Helper to determine offset for each correlation variable + e.rel.accept(new CorrelationOffsetFinder(idToOffset, join, nFieldsLeft)); + + // 1. Identify which columns from Left and Right are used by the subquery. + // These will form the "Domain" on which the subquery is calculated. + final ImmutableBitSet leftUsed = + inputSet.intersect(ImmutableBitSet.range(0, nFieldsLeft)); + final ImmutableBitSet rightUsed = + inputSet.intersect(ImmutableBitSet.range(nFieldsLeft, nFieldsLeft + nFieldsRight)); + + // 2. Build the "Computation Domain". + // This is a Cross Join of the distinct keys from Left and Right. + // Domain = Distinct(Project(LeftUsed)) x Distinct(Project(RightUsed)) + + // 2a. Left Domain + builder.push(join.getLeft()); + builder.project(builder.fields(leftUsed)); + builder.distinct(); + + // 2b. Right Domain + builder.push(join.getRight()); + // We must shift the bitset to be 0-based for the Right input + ImmutableBitSet rightUsedShifted = rightUsed.shift(-nFieldsLeft); + builder.project(builder.fields(rightUsedShifted)); + builder.distinct(); + + // 2c. Create Domain Cross Join + builder.join(JoinRelType.INNER, builder.literal(true)); + + // 3. Remap the SubQuery to run on the Domain. + // We need to map original field indices to their new positions in the Domain. + // Original: [LeftFields... | RightFields...] + // Domain: [LeftUsed... | RightUsed...] + final Map mapping = new HashMap<>(); + int targetIdx = 0; + for (int source : leftUsed) { + mapping.put(source, targetIdx++); + } + for (int source : rightUsed) { + mapping.put(source, targetIdx++); + } + + final RexBuilder rexBuilder = builder.getRexBuilder(); + final CorrelationId domainCorrId = join.getCluster().createCorrel(); + final RexNode domainCorrVar = rexBuilder.makeCorrel(builder.peek().getRowType(), domainCorrId); + + // Shuttle to replace InputRefs and Correlations with references to the Domain + RexShuttle shuttle = new InputRefAndCorrelationReplacer(mapping, variablesSet, idToOffset); + // Create the new subquery with operands remapped to the Domain + RexNode newSubQueryNode = e.accept(shuttle); + + // Rewrite e.rel to use domainCorrId + RelNode newRel = + e.rel.accept( + new DomainRewriter(variablesSet, idToOffset, mapping, rexBuilder, domainCorrVar)); + + if (newSubQueryNode instanceof RexSubQuery) { + newSubQueryNode = ((RexSubQuery) newSubQueryNode).clone(newRel); + } + + // We introduced a new correlation variable domainCorrId. + Set newVariablesSet = ImmutableSet.of(domainCorrId); + + final RelOptUtil.Logic logic = + LogicVisitor.find(join.getJoinType().generatesNullsOnRight() + ? RelOptUtil.Logic.TRUE_FALSE_UNKNOWN : RelOptUtil.Logic.TRUE, + ImmutableList.of(join.getCondition()), e); + + // 4. Apply the standard rewriting rule to the Domain. + // The builder is currently sitting on the Domain Join. + // 'target' is the CASE expression (or similar) resulting from the rewrite. + // The builder stack now has the result of the rewrite (e.g. Domain Left Join Aggregate). + assert newSubQueryNode instanceof RexSubQuery; + final RexNode target = + rule.apply((RexSubQuery) newSubQueryNode, newVariablesSet, logic, builder, + 1, builder.peek().getRowType().getFieldCount(), 0); + + // The target references the Domain Result (which is currently at the top of the builder). + // In the final plan, the Domain Result will be joined to the right of the original inputs. + // Furthermore, since we use a LEFT JOIN, the Domain Result columns become nullable. + // So we need to shift the references in target AND make them nullable. + final int offset = nFieldsLeft + nFieldsRight; + final RexShuttle shiftAndNullableShuttle = new RexShuttle() { + @Override public RexNode visitInputRef(RexInputRef inputRef) { + // Shift the index + int newIndex = inputRef.getIndex() + offset; + return new RexInputRef(newIndex, inputRef.getType()); + } + }; + final RexNode shiftedTarget = target.accept(shiftAndNullableShuttle); + + // 5. Re-integrate with Original Inputs + // Stack has: [RewriteResult] + RelNode domainResult = builder.build(); + + // Rebuild the original Join structure + // We want to construct: Left JOIN (Right JOIN Domain) ON ... + // This preserves the JoinRelType of the original join. + JoinRelType joinType = join.getJoinType(); + if (joinType == JoinRelType.RIGHT) { + // Symmetric to LEFT/INNER/FULL but attached to Left + builder.push(join.getLeft()); + builder.push(domainResult); + + // Join Left and Domain on Left Keys + List leftJoinConditions = new ArrayList<>(); + int domainIdx = 0; // Left Keys are at start of Domain + for (int source : leftUsed) { + leftJoinConditions.add( + builder.equals( + builder.field(2, 0, source), + builder.field(2, 1, domainIdx++))); + } + builder.join(JoinRelType.INNER, builder.and(leftJoinConditions)); + + // Now Join Right + builder.push(join.getRight()); + // Stack: (Left+Domain), Right + + // Join Condition: Original + Right Keys match + List rightJoinConditions = new ArrayList<>(); + // Domain starts after Left. Right Keys in Domain are after Left Keys. + int domainRightKeyIdx = nFieldsLeft + leftUsed.cardinality(); + for (int source : rightUsed) { + // Right input (index 1) + RexInputRef field = builder.field(2, 1, source - nFieldsLeft); + // (Left+Domain) input (index 0) + RexInputRef field1 = builder.field(2, 0, domainRightKeyIdx++); + rightJoinConditions.add(builder.equals(field, field1)); + } + + RexShuttle replaceShuttle = new ReplaceSubQueryShuttle(e, shiftedTarget); + RexNode newJoinCondition = join.getCondition().accept(replaceShuttle); + + builder.join(joinType, builder.and(builder.and(rightJoinConditions), newJoinCondition)); + + builder.project(fields(builder, nFieldsLeft + nFieldsRight)); + } else { + // For INNER, LEFT, FULL join, we can attach Domain to Right, then Join Left. + // 1. Build (Right JOIN Domain) + builder.push(join.getRight()); + builder.push(domainResult); + + // Join Right and Domain on Right Keys + // Domain layout: [LeftKeys, RightKeys] + List rightJoinConditions = new ArrayList<>(); + // Skip Left Keys + int domainIdx = leftUsed.cardinality(); + for (int source : rightUsed) { + rightJoinConditions.add( + builder.equals( + builder.field(2, 0, source - nFieldsLeft), // Right input + builder.field(2, 1, domainIdx++))); // Domain input + } + // We use INNER join here to expand Right with Domain values. + // Since Domain contains all distinct Right keys, this is safe. + builder.join(JoinRelType.INNER, builder.and(rightJoinConditions)); + + // 2. Join Left with (Right JOIN Domain) + RelNode rightWithDomain = builder.build(); + builder.push(join.getLeft()); + builder.push(rightWithDomain); + + // Join Condition: Original Condition (rewritten) AND Left.LeftKeys = Domain.LeftKeys + List leftJoinConditions = new ArrayList<>(); + // In (Right+Domain), Domain fields start after Right fields + int domainStartInCombined = nFieldsRight; + int domainLeftKeyIdx = domainStartInCombined; // Left Keys are at start of Domain + + for (int source : leftUsed) { + // Left input + RexInputRef field = builder.field(2, 0, source); + // (Right+Domain) input + RexInputRef field1 = builder.field(2, 1, domainLeftKeyIdx++); + leftJoinConditions.add(builder.equals(field, field1)); + } + + RexShuttle replaceShuttle = new ReplaceSubQueryShuttle(e, shiftedTarget); + RexNode newJoinCondition = join.getCondition().accept(replaceShuttle); + + builder.join(joinType, builder.and(builder.and(leftJoinConditions), newJoinCondition)); + + // Project original fields (remove Domain columns) + builder.project(fields(builder, nFieldsLeft + nFieldsRight)); + } + + call.transformTo(builder.build()); + } + private static void matchFilterEnableMarkJoin(SubQueryRemoveRule rule, RelOptRuleCall call) { final Filter filter = call.rel(0); final Set variablesSet = filter.getVariablesSet(); @@ -1212,6 +1442,125 @@ private static class ReplaceSubQueryShuttle extends RexShuttle { return subQuery.equals(this.subQuery) ? replacement : subQuery; } } + + /** + * Shuttle that finds correlation variables and determines their offset. + */ + private static class CorrelationOffsetFinder extends RelHomogeneousShuttle { + private final Map idToOffset; + private final Join join; + private final int nFieldsLeft; + + CorrelationOffsetFinder(Map idToOffset, Join join, int nFieldsLeft) { + this.idToOffset = idToOffset; + this.join = join; + this.nFieldsLeft = nFieldsLeft; + } + + @Override public RelNode visit(RelNode other) { + other.accept(new RexShuttle() { + @Override public RexNode visitCorrelVariable(RexCorrelVariable correlVariable) { + if (!idToOffset.containsKey(correlVariable.id)) { + // Check if type matches Left + if (RelOptUtil.eq("type1", correlVariable.getType(), + "type2", join.getLeft().getRowType(), Litmus.IGNORE)) { + idToOffset.put(correlVariable.id, 0); + } else if (RelOptUtil.eq("type1", correlVariable.getType(), + "type2", join.getRight().getRowType(), Litmus.IGNORE)) { + idToOffset.put(correlVariable.id, nFieldsLeft); + } else { + // Default to 0 if unknown + idToOffset.put(correlVariable.id, 0); + } + } + return super.visitCorrelVariable(correlVariable); + } + }); + return super.visit(other); + } + } + + /** + * Shuttle that replaces InputRefs and Correlations with references to the Domain. + */ + private static class InputRefAndCorrelationReplacer extends RexShuttle { + private final Map mapping; + private final Set variablesSet; + private final Map idToOffset; + + InputRefAndCorrelationReplacer(Map mapping, + Set variablesSet, Map idToOffset) { + this.mapping = mapping; + this.variablesSet = variablesSet; + this.idToOffset = idToOffset; + } + + @Override public RexNode visitInputRef(RexInputRef inputRef) { + Integer newIndex = mapping.get(inputRef.getIndex()); + if (newIndex != null) { + return new RexInputRef(newIndex, inputRef.getType()); + } + return super.visitInputRef(inputRef); + } + + @Override public RexNode visitFieldAccess(RexFieldAccess fieldAccess) { + RexNode refExpr = fieldAccess.getReferenceExpr(); + if (refExpr instanceof RexCorrelVariable) { + CorrelationId id = ((RexCorrelVariable) refExpr).id; + if (variablesSet.contains(id)) { + int fieldIdx = fieldAccess.getField().getIndex(); + int offset = idToOffset.getOrDefault(id, 0); + Integer newIndex = mapping.get(fieldIdx + offset); + if (newIndex != null) { + return new RexInputRef(newIndex, fieldAccess.getType()); + } + } + } + return super.visitFieldAccess(fieldAccess); + } + } + + /** + * Shuttle that rewrites RelNodes to use the Domain correlation variable. + */ + private static class DomainRewriter extends RelHomogeneousShuttle { + private final Set variablesSet; + private final Map idToOffset; + private final Map mapping; + private final RexBuilder rexBuilder; + private final RexNode domainCorrVar; + + DomainRewriter(Set variablesSet, Map idToOffset, + Map mapping, RexBuilder rexBuilder, RexNode domainCorrVar) { + this.variablesSet = variablesSet; + this.idToOffset = idToOffset; + this.mapping = mapping; + this.rexBuilder = rexBuilder; + this.domainCorrVar = domainCorrVar; + } + + @Override public RelNode visit(RelNode other) { + return super.visit( + other.accept(new RexShuttle() { + @Override public RexNode visitFieldAccess(RexFieldAccess fieldAccess) { + RexNode refExpr = fieldAccess.getReferenceExpr(); + if (refExpr instanceof RexCorrelVariable) { + CorrelationId id = ((RexCorrelVariable) refExpr).id; + if (variablesSet.contains(id)) { + int fieldIdx = fieldAccess.getField().getIndex(); + int offset = idToOffset.getOrDefault(id, 0); + Integer newIndex = mapping.get(fieldIdx + offset); + if (newIndex != null) { + return rexBuilder.makeFieldAccess(domainCorrVar, newIndex); + } + } + } + return super.visitFieldAccess(fieldAccess); + } + })); + } + } + /** Rule configuration. */ @Value.Immutable(singleton = false) public interface Config extends RelRule.Config { 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 7ddaeaf4202..505710311a9 100644 --- a/core/src/test/java/org/apache/calcite/sql2rel/RelDecorrelatorTest.java +++ b/core/src/test/java/org/apache/calcite/sql2rel/RelDecorrelatorTest.java @@ -1492,6 +1492,94 @@ public static Frameworks.ConfigBuilder config() { assertThat(after, hasTree(planAfter)); } + /** Test case for [CALCITE-7278] + * Correlated subqueries in the join condition cannot reference both join inputs. */ + @Test void test7278() { + final FrameworkConfig frameworkConfig = config().build(); + final RelBuilder builder = RelBuilder.create(frameworkConfig); + final RelOptCluster cluster = builder.getCluster(); + final Planner planner = Frameworks.getPlanner(frameworkConfig); + final String sql = "" + + "SELECT d.dname\n" + + "FROM dept d\n" + + "JOIN emp e\n" + + " ON NOT EXISTS (\n" + + " SELECT 1\n" + + " FROM emp e2\n" + + " WHERE e2.deptno = d.deptno\n" + + " AND e2.empno > e.empno)"; + final RelNode originalRel; + try { + final SqlNode parse = planner.parse(sql); + final SqlNode validate = planner.validate(parse); + originalRel = planner.rel(validate).rel; + } catch (Exception e) { + throw TestUtil.rethrow(e); + } + + final HepProgram hepProgram = HepProgram.builder() + .addRuleCollection( + ImmutableList.of( + // SubQuery program rules + CoreRules.FILTER_SUB_QUERY_TO_CORRELATE, + CoreRules.PROJECT_SUB_QUERY_TO_CORRELATE, + CoreRules.JOIN_SUB_QUERY_TO_CORRELATE)) + .build(); + final Program program = + Programs.of(hepProgram, true, + requireNonNull(cluster.getMetadataProvider())); + final RelNode before = + program.run(cluster.getPlanner(), originalRel, cluster.traitSet(), + Collections.emptyList(), Collections.emptyList()); + final String planBefore = "" + + "LogicalProject(DNAME=[$1])\n" + + " LogicalProject(DEPTNO=[$0], DNAME=[$1], LOC=[$2], EMPNO=[$3], ENAME=[$4], JOB=[$5], MGR=[$6], HIREDATE=[$7], SAL=[$8], COMM=[$9], DEPTNO0=[$10])\n" + + " LogicalJoin(condition=[AND(=($0, $11), IS NULL($13))], joinType=[inner])\n" + + " LogicalTableScan(table=[[scott, DEPT]])\n" + + " LogicalJoin(condition=[=($0, $9)], joinType=[inner])\n" + + " LogicalTableScan(table=[[scott, EMP]])\n" + + " LogicalCorrelate(correlation=[$cor2], joinType=[left], requiredColumns=[{0, 1}])\n" + + " LogicalJoin(condition=[true], joinType=[inner])\n" + + " LogicalProject(DEPTNO=[$0])\n" + + " LogicalTableScan(table=[[scott, DEPT]])\n" + + " LogicalProject(EMPNO=[$0])\n" + + " LogicalTableScan(table=[[scott, EMP]])\n" + + " LogicalAggregate(group=[{0}])\n" + + " LogicalProject(i=[true])\n" + + " LogicalFilter(condition=[AND(=($7, $cor2.DEPTNO), >($0, $cor2.EMPNO))])\n" + + " LogicalTableScan(table=[[scott, EMP]])\n"; + assertThat(before, hasTree(planBefore)); + + // Decorrelate without any rules, just "purely" decorrelation algorithm on RelDecorrelator + final RelNode after = + RelDecorrelator.decorrelateQuery(before, builder, RuleSets.ofList(Collections.emptyList()), + RuleSets.ofList(Collections.emptyList())); + final String planAfter = "" + + "LogicalProject(DNAME=[$1])\n" + + " LogicalJoin(condition=[=($0, $11)], joinType=[inner])\n" + + " LogicalTableScan(table=[[scott, DEPT]])\n" + + " LogicalFilter(condition=[IS NULL($12)])\n" + + " LogicalJoin(condition=[=($0, $9)], joinType=[inner])\n" + + " LogicalTableScan(table=[[scott, EMP]])\n" + + " LogicalJoin(condition=[AND(=($0, $2), =($1, $3))], joinType=[left])\n" + + " LogicalJoin(condition=[true], joinType=[inner])\n" + + " LogicalProject(DEPTNO=[$0])\n" + + " LogicalTableScan(table=[[scott, DEPT]])\n" + + " LogicalProject(EMPNO=[$0])\n" + + " LogicalTableScan(table=[[scott, EMP]])\n" + + " LogicalProject(DEPTNO0=[$0], EMPNO0=[$1], $f2=[true])\n" + + " LogicalAggregate(group=[{0, 1}])\n" + + " LogicalProject(DEPTNO0=[$8], EMPNO0=[$9])\n" + + " LogicalJoin(condition=[AND(=($7, $8), >($0, $9))], joinType=[inner])\n" + + " LogicalTableScan(table=[[scott, EMP]])\n" + + " LogicalJoin(condition=[true], joinType=[inner])\n" + + " LogicalProject(DEPTNO=[$0])\n" + + " LogicalTableScan(table=[[scott, DEPT]])\n" + + " LogicalProject(EMPNO=[$0])\n" + + " LogicalTableScan(table=[[scott, EMP]])\n"; + assertThat(after, hasTree(planAfter)); + } + /** Test case for [CALCITE-7379] * LHS correlated variables are shadowed by nullable RHS outputs in LEFT JOIN. */ @Test void testDecorrelateFullJoinCorVarShadowing() { diff --git a/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java b/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java index 81f07a6f6a5..cfbcf314e57 100644 --- a/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java +++ b/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java @@ -10427,7 +10427,7 @@ public interface Config extends RelRule.Config { + "emp.deptno + dept.deptno >= SOME(SELECT deptno FROM dept)"; sql(sql) .withRule(CoreRules.JOIN_SUB_QUERY_TO_CORRELATE) - .checkUnchanged(); + .check(); } /** Test case for 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 103744e3118..d3f9557646a 100644 --- a/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml +++ b/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml @@ -8737,6 +8737,27 @@ LogicalProject(DEPTNO=[$0]) })], joinType=[inner]) LogicalTableScan(table=[[CATALOG, SALES, EMP]]) LogicalTableScan(table=[[CATALOG, SALES, DEPT]]) +]]> + + + =(+($11, $12), $13)), IS NOT TRUE(OR(IS NULL($16), =($14, 0)))), AND(IS TRUE(>($14, $15)), null, IS NOT TRUE(OR(IS NULL($16), =($14, 0))), IS NOT TRUE(>=(+($11, $12), $13))), AND(>=(+($11, $12), $13), IS NOT TRUE(OR(IS NULL($16), =($14, 0))), IS NOT TRUE(>=(+($11, $12), $13)), IS NOT TRUE(>($14, $15))))):BOOLEAN NOT NULL)], joinType=[inner]) + LogicalTableScan(table=[[CATALOG, SALES, EMP]]) + LogicalJoin(condition=[=($0, $3)], joinType=[inner]) + LogicalTableScan(table=[[CATALOG, SALES, DEPT]]) + LogicalJoin(condition=[true], joinType=[left], variablesSet=[[$cor0]]) + LogicalJoin(condition=[true], joinType=[inner]) + LogicalAggregate(group=[{0}]) + LogicalProject(DEPTNO=[$7]) + LogicalTableScan(table=[[CATALOG, SALES, EMP]]) + LogicalProject(DEPTNO=[$0]) + LogicalTableScan(table=[[CATALOG, SALES, DEPT]]) + LogicalProject(m=[$0], c=[$1], d=[$1], trueLiteral=[$2]) + LogicalAggregate(group=[{}], m=[MIN($0)], c=[COUNT()], trueLiteral=[LITERAL_AGG(true)]) + LogicalProject(DEPTNO=[$0]) + LogicalTableScan(table=[[CATALOG, SALES, DEPT]]) ]]> diff --git a/core/src/test/resources/sql/sub-query.iq b/core/src/test/resources/sql/sub-query.iq index a9440f6f43b..d262833c555 100644 --- a/core/src/test/resources/sql/sub-query.iq +++ b/core/src/test/resources/sql/sub-query.iq @@ -8056,6 +8056,149 @@ WHERE deptno NOT IN ( !set trimfields true !use scott +# [CALCITE-7278] Correlated subqueries in the join condition cannot reference both join inputs +# Verified against PostgreSQL. https://onecompiler.com/postgresql/44bx3j67t +SELECT empno +FROM emp +JOIN dept +on emp.deptno + dept.deptno >= SOME(SELECT deptno FROM dept); ++-------+ +| EMPNO | ++-------+ +| 7369 | +| 7369 | +| 7369 | +| 7369 | +| 7499 | +| 7499 | +| 7499 | +| 7499 | +| 7521 | +| 7521 | +| 7521 | +| 7521 | +| 7566 | +| 7566 | +| 7566 | +| 7566 | +| 7654 | +| 7654 | +| 7654 | +| 7654 | +| 7698 | +| 7698 | +| 7698 | +| 7698 | +| 7782 | +| 7782 | +| 7782 | +| 7782 | +| 7788 | +| 7788 | +| 7788 | +| 7788 | +| 7839 | +| 7839 | +| 7839 | +| 7839 | +| 7844 | +| 7844 | +| 7844 | +| 7844 | +| 7876 | +| 7876 | +| 7876 | +| 7876 | +| 7900 | +| 7900 | +| 7900 | +| 7900 | +| 7902 | +| 7902 | +| 7902 | +| 7902 | +| 7934 | +| 7934 | +| 7934 | +| 7934 | ++-------+ +(56 rows) + +!ok + +# [CALCITE-7278] Correlated subqueries in the join condition cannot reference both join inputs +# Verified against PostgreSQL. https://onecompiler.com/postgresql/44bx3j67t +SELECT d.dname +FROM dept d +JOIN emp e +ON d.deptno = e.deptno + AND NOT EXISTS ( + SELECT 1 + FROM emp e2 + WHERE e2.deptno = d.deptno AND e2.empno > e.empno); ++------------+ +| DNAME | ++------------+ +| ACCOUNTING | +| RESEARCH | +| SALES | ++------------+ +(3 rows) + +!ok + +# [CALCITE-7278] Correlated subqueries in the join condition cannot reference both join inputs +# Verified against PostgreSQL. https://onecompiler.com/postgresql/44bx3j67t +SELECT * +FROM dept d +LEFT JOIN emp e + ON d.deptno = e.deptno + OR e.job IN ( + SELECT b.job + FROM bonus b + WHERE b.sal = d.deptno); ++--------+------------+----------+-------+--------+-----------+------+------------+---------+---------+---------+ +| DEPTNO | DNAME | LOC | EMPNO | ENAME | JOB | MGR | HIREDATE | SAL | COMM | DEPTNO0 | ++--------+------------+----------+-------+--------+-----------+------+------------+---------+---------+---------+ +| 10 | ACCOUNTING | NEW YORK | 7782 | CLARK | MANAGER | 7839 | 1981-06-09 | 2450.00 | | 10 | +| 10 | ACCOUNTING | NEW YORK | 7839 | KING | PRESIDENT | | 1981-11-17 | 5000.00 | | 10 | +| 10 | ACCOUNTING | NEW YORK | 7934 | MILLER | CLERK | 7782 | 1982-01-23 | 1300.00 | | 10 | +| 20 | RESEARCH | DALLAS | 7369 | SMITH | CLERK | 7902 | 1980-12-17 | 800.00 | | 20 | +| 20 | RESEARCH | DALLAS | 7566 | JONES | MANAGER | 7839 | 1981-02-04 | 2975.00 | | 20 | +| 20 | RESEARCH | DALLAS | 7788 | SCOTT | ANALYST | 7566 | 1987-04-19 | 3000.00 | | 20 | +| 20 | RESEARCH | DALLAS | 7876 | ADAMS | CLERK | 7788 | 1987-05-23 | 1100.00 | | 20 | +| 20 | RESEARCH | DALLAS | 7902 | FORD | ANALYST | 7566 | 1981-12-03 | 3000.00 | | 20 | +| 30 | SALES | CHICAGO | 7499 | ALLEN | SALESMAN | 7698 | 1981-02-20 | 1600.00 | 300.00 | 30 | +| 30 | SALES | CHICAGO | 7521 | WARD | SALESMAN | 7698 | 1981-02-22 | 1250.00 | 500.00 | 30 | +| 30 | SALES | CHICAGO | 7654 | MARTIN | SALESMAN | 7698 | 1981-09-28 | 1250.00 | 1400.00 | 30 | +| 30 | SALES | CHICAGO | 7698 | BLAKE | MANAGER | 7839 | 1981-01-05 | 2850.00 | | 30 | +| 30 | SALES | CHICAGO | 7844 | TURNER | SALESMAN | 7698 | 1981-09-08 | 1500.00 | 0.00 | 30 | +| 30 | SALES | CHICAGO | 7900 | JAMES | CLERK | 7698 | 1981-12-03 | 950.00 | | 30 | +| 40 | OPERATIONS | BOSTON | | | | | | | | | ++--------+------------+----------+-------+--------+-----------+------+------------+---------+---------+---------+ +(15 rows) + +!ok + +# [CALCITE-7278] Correlated subqueries in the join condition cannot reference both join inputs +# Verified against PostgreSQL. https://onecompiler.com/postgresql/44bx3j67t +select Header.Name +from ( VALUES (1, 'A'), (2, 'B')) as Header(Id, Name) +join (values (11, 1), (12, 1), (21, 2)) as Version(Id, Parent) +on not exists (select 1 + from (values (11, 1), (12, 1), (21, 2)) as Version2(Id, Parent) + where Version2.Parent = Header.Id and Version2.Id > Version.Id); ++------+ +| NAME | ++------+ +| A | +| A | +| B | ++------+ +(3 rows) + +!ok + # [CALCITE-7379] LHS correlated variables are shadowed by nullable RHS outputs in LEFT JOIN # Correlated scalar subquery with LEFT JOIN. # The correlation variable (d.deptno) is used in the RHS of the join.