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:
+ *
+ * - Identifies the "Domain" of values from the left and right inputs that are relevant
+ * to the sub-query.
+ * - Constructs a "Computation Domain" by cross-joining the distinct keys from the left
+ * and right domains.
+ * - Remaps the sub-query to operate on this Computation Domain.
+ * - Rewrites the sub-query using the standard {@link #apply} method, but applied to the
+ * Domain.
+ * - Re-integrates the result of the sub-query rewrite back into the original Join structure,
+ * ensuring correct join types and conditions are maintained.
+ *
+ *
+ * @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.