This is an automated email from the ASF dual-hosted git repository.
yiguolei pushed a commit to branch branch-4.2
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/branch-4.2 by this push:
new 982d64dd246 branch-4.2: [fix](fe) Preserve column identity for
forwarded USING keys (branch-4.2) (#69389)
982d64dd246 is described below
commit 982d64dd246c641712fa3be8f7b6fd3f18f93684
Author: 924060929 <[email protected]>
AuthorDate: Thu Oct 22 14:57:58 2026 +0800
branch-4.2: [fix](fe) Preserve column identity for forwarded USING keys
(branch-4.2) (#69389)
Backport of #69388 to branch-4.2. The production repair is unchanged;
the backported tests use Java 8-compatible APIs.
### What problem does this PR solve?
Issue Number: N/A
Related PR: #69388
A qualified reference to a preserved-side USING key can be rejected even
when the same key appears unqualified in GROUP BY. For example:
```sql
SELECT t1.a, COUNT(*)
FROM t1 LEFT JOIN t2 USING(a)
GROUP BY a;
```
The USING-key projection creates a fresh Alias for a column forwarded
directly from the preserved side. SELECT t1.a references the original
ExprId while GROUP BY a references the new one. The same mismatch
affects HAVING and ORDER BY.
Keep the input ExprId for forwarded USING keys while clearing the
qualifier of the unqualified view. Preserve qualified outputs and the
explicit asterisk output list. FULL OUTER JOIN still builds a new
COALESCE expression with its own ExprId, and aggregate validation
remains unchanged.
Sharing the forwarded key identity can repeat a producer ExprId in CTE
output, for example:
```sql
WITH q(x,y,z) AS (
SELECT a,t1.b,t1.a FROM t1 JOIN t2 USING(a)
)
SELECT * FROM q;
```
Keep CTE consumer columns in their original positions using the ordered
consumer-to-producer map. Flattening the reverse multimap groups equal
producer slots and incorrectly changes [x,y,z] into [x,z,y]. Preserve
the ordered map when updating nullability, replacing producer ExprIds
and pruning variant subpaths, so those rewrites cannot reintroduce the
ordering defect.
### Release note
Fix spurious GROUP BY errors when qualified and unqualified references
to a forwarded USING join key are mixed in SELECT, GROUP BY, HAVING, or
ORDER BY. Preserve CTE output column order when the same source column
appears more than once.
### Check List (For Author)
- Test: Unit Test
- Official run-fe-ut.sh: 73 tests, 0 failures/errors across
BindUsingJoinTest, LogicalCTEConsumerTest, AnalyzeCTETest and
EliminateGroupByKeyTest.
- Reproduced the CTE column-order failure before the CTE repair:
expected [x,y,z], actual [x,z,y].
- USING tests cover complete planning across retained sides, mixed
grouping references, HAVING/ORDER BY, expressions, multiple keys,
derived inputs and invalid nullable-side/FULL references.
- Added CTE column-order cases for inner/left/right USING joins, an
actual FE-computed result check for (1,10,1), and repeated-producer
tests covering consumer copies, pruning, nullability changes and
producer-ID replacement.
- Added an EXPLAIN regression case to test_using_join_merge_key; the
same SQL is covered by the FE full-planner test. The regression suite
and BE data execution have not been run locally.
- FE server build through DISABLE_BUILD_UI=ON ./build.sh --fe -j4,
integrated Checkstyle, and git diff --check. Unmodified Web UI excluded.
- Behavior changed: Yes, valid grouping queries plan successfully and
CTE column positions remain intact.
- Does this need documentation: No.
---
.../nereids/rules/analysis/BindExpression.java | 8 +-
.../nereids/rules/rewrite/AdjustNullable.java | 16 ++-
.../nereids/rules/rewrite/EliminateGroupByKey.java | 10 +-
.../rules/rewrite/VariantSubPathPruning.java | 2 +-
.../trees/plans/logical/LogicalCTEConsumer.java | 3 +-
.../nereids/rules/analysis/BindUsingJoinTest.java | 115 +++++++++++++++++----
.../plans/logical/LogicalCTEConsumerTest.java | 104 +++++++++++++++++++
.../query_p0/join/test_using_join_merge_key.groovy | 15 +++
8 files changed, 234 insertions(+), 39 deletions(-)
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/BindExpression.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/BindExpression.java
index 04cc43953ee..b86fabbd9c1 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/BindExpression.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/BindExpression.java
@@ -1057,7 +1057,9 @@ public class BindExpression implements
AnalysisRuleFactory {
ImmutableList.Builder<NamedExpression> projectExprs =
ImmutableList.builder();
ImmutableList.Builder<NamedExpression> asteriskOutputs =
ImmutableList.builder();
- // 1. Add merged key columns (without qualifier)
+ // 1. Add merged key columns (without qualifier). A forwarded key
keeps the preserved side's
+ // ExprId so qualified and unqualified references denote the same
column in GROUP BY/HAVING/ORDER BY.
+ // Only FULL OUTER JOIN computes a new key and therefore needs a new
Alias.
for (int i = 0; i < leftConjunctsSlots.size(); i++) {
Slot leftSlot = leftConjunctsSlots.get(i);
Slot rightSlot = rightConjunctsSlots.get(i);
@@ -1068,10 +1070,10 @@ public class BindExpression implements
AnalysisRuleFactory {
mergeExpr = new Alias(new Coalesce(leftSlot, rightSlot),
colName);
} else if (joinType.isRightJoin() ||
joinType.isRightSemiOrAntiJoin()) {
// RIGHT OUTER / RIGHT SEMI / RIGHT ANTI: use right side
(preserved side)
- mergeExpr = new Alias(rightSlot, colName);
+ mergeExpr = rightSlot.withQualifier(ImmutableList.of());
} else {
// LEFT OUTER / INNER / LEFT SEMI / LEFT ANTI / CROSS: use
left side
- mergeExpr = new Alias(leftSlot, colName);
+ mergeExpr = leftSlot.withQualifier(ImmutableList.of());
}
projectExprs.add(mergeExpr);
asteriskOutputs.add(mergeExpr);
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/AdjustNullable.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/AdjustNullable.java
index c8016a493c6..734779de37a 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/AdjustNullable.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/AdjustNullable.java
@@ -426,15 +426,13 @@ public class AdjustNullable extends
DefaultPlanRewriter<Map<ExprId, Slot>> imple
public Plan visitLogicalCTEConsumer(LogicalCTEConsumer cteConsumer,
Map<ExprId, Slot> replaceMap) {
Map<Slot, Slot> consumerToProducerOutputMap = new LinkedHashMap<>();
Multimap<Slot, Slot> producerToConsumerOutputMap =
LinkedHashMultimap.create();
- for (Slot producerOutputSlot :
cteConsumer.getConsumerToProducerOutputMap().values()) {
- Optional<Slot> newProducerOutputSlot =
updateExpression(producerOutputSlot, replaceMap, true);
- for (Slot consumerOutputSlot :
cteConsumer.getProducerToConsumerOutputMap().get(producerOutputSlot)) {
- Slot slot = newProducerOutputSlot.orElse(producerOutputSlot);
- Slot newConsumerOutputSlot =
consumerOutputSlot.withNullable(slot.nullable());
- producerToConsumerOutputMap.put(slot, newConsumerOutputSlot);
- consumerToProducerOutputMap.put(newConsumerOutputSlot, slot);
- replaceMap.put(newConsumerOutputSlot.getExprId(),
newConsumerOutputSlot);
- }
+ for (Map.Entry<Slot, Slot> entry :
cteConsumer.getConsumerToProducerOutputMap().entrySet()) {
+ Slot producerOutputSlot = entry.getValue();
+ Slot slot = updateExpression(producerOutputSlot, replaceMap,
true).orElse(producerOutputSlot);
+ Slot newConsumerOutputSlot =
entry.getKey().withNullable(slot.nullable());
+ producerToConsumerOutputMap.put(slot, newConsumerOutputSlot);
+ consumerToProducerOutputMap.put(newConsumerOutputSlot, slot);
+ replaceMap.put(newConsumerOutputSlot.getExprId(),
newConsumerOutputSlot);
}
return cteConsumer.withTwoMaps(consumerToProducerOutputMap,
producerToConsumerOutputMap);
}
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/EliminateGroupByKey.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/EliminateGroupByKey.java
index cff93b15b2b..7b9d3b7dd32 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/EliminateGroupByKey.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/EliminateGroupByKey.java
@@ -143,15 +143,15 @@ public class EliminateGroupByKey extends
DefaultPlanRewriter<Map<ExprId, ExprId>
// still references the old ExprId, so we must rebuild both maps with
the new ExprIds.
Map<Slot, Slot> newConsumerToProducer = new LinkedHashMap<>();
Multimap<Slot, Slot> newProducerToConsumer =
LinkedHashMultimap.create();
- for (Slot producerSlot :
cteConsumer.getConsumerToProducerOutputMap().values()) {
+ for (Map.Entry<Slot, Slot> entry :
cteConsumer.getConsumerToProducerOutputMap().entrySet()) {
+ Slot producerSlot = entry.getValue();
ExprId newExprId = resolveExprIdChain(producerSlot.getExprId(),
replaceMap);
Slot effectiveProducerSlot = newExprId != null
? (Slot) producerSlot.withExprId(newExprId)
: producerSlot;
- for (Slot consumerSlot :
cteConsumer.getProducerToConsumerOutputMap().get(producerSlot)) {
- newProducerToConsumer.put(effectiveProducerSlot, consumerSlot);
- newConsumerToProducer.put(consumerSlot, effectiveProducerSlot);
- }
+ Slot consumerSlot = entry.getKey();
+ newProducerToConsumer.put(effectiveProducerSlot, consumerSlot);
+ newConsumerToProducer.put(consumerSlot, effectiveProducerSlot);
}
return cteConsumer.withTwoMaps(newConsumerToProducer,
newProducerToConsumer);
}
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/VariantSubPathPruning.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/VariantSubPathPruning.java
index 3036a2ba4d9..6ce648f95d8 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/VariantSubPathPruning.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/VariantSubPathPruning.java
@@ -422,7 +422,7 @@ public class VariantSubPathPruning implements
CustomRewriter {
.map(ExpressionTrait::getDataType).noneMatch(VariantType.class::isInstance)) {
return cteConsumer;
}
- Map<Slot, Slot> consumerToProducerOutputMap = Maps.newHashMap();
+ Map<Slot, Slot> consumerToProducerOutputMap =
Maps.newLinkedHashMap();
Multimap<Slot, Slot> producerToConsumerOutputMap =
LinkedHashMultimap.create();
Map<Slot, Map<List<String>, SlotReference>> oriSlotToSubPathToSlot
= Maps.newHashMap();
for (Map.Entry<Slot, Slot> consumerToProducer :
cteConsumer.getConsumerToProducerOutputMap().entrySet()) {
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalCTEConsumer.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalCTEConsumer.java
index be2c41d2605..c631b4fdffa 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalCTEConsumer.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalCTEConsumer.java
@@ -53,6 +53,7 @@ public class LogicalCTEConsumer extends LogicalRelation
implements BlockFuncDeps
private final String name;
private final CTEId cteId;
+ // Insertion order is the consumer column order, including repeated
producer slots.
private final Map<Slot, Slot> consumerToProducerOutputMap;
private final Multimap<Slot, Slot> producerToConsumerOutputMap;
@@ -155,7 +156,7 @@ public class LogicalCTEConsumer extends LogicalRelation
implements BlockFuncDeps
@Override
public List<Slot> computeOutput() {
- return ImmutableList.copyOf(producerToConsumerOutputMap.values());
+ return ImmutableList.copyOf(consumerToProducerOutputMap.keySet());
}
@Override
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/analysis/BindUsingJoinTest.java
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/analysis/BindUsingJoinTest.java
index 4fcc69953de..8f447a9bdba 100644
---
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/analysis/BindUsingJoinTest.java
+++
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/analysis/BindUsingJoinTest.java
@@ -17,6 +17,7 @@
package org.apache.doris.nereids.rules.analysis;
+import org.apache.doris.nereids.exceptions.AnalysisException;
import org.apache.doris.nereids.pattern.GeneratedPlanPatterns;
import org.apache.doris.nereids.rules.RulePromise;
import org.apache.doris.nereids.trees.expressions.Alias;
@@ -24,9 +25,12 @@ import org.apache.doris.nereids.trees.expressions.Expression;
import org.apache.doris.nereids.trees.expressions.NamedExpression;
import org.apache.doris.nereids.trees.expressions.Slot;
import org.apache.doris.nereids.trees.expressions.functions.scalar.Coalesce;
+import org.apache.doris.nereids.trees.plans.ComputeResultSet;
+import org.apache.doris.nereids.trees.plans.physical.PhysicalPlan;
import org.apache.doris.nereids.util.PlanChecker;
import org.apache.doris.utframe.TestWithFeService;
+import com.google.common.collect.ImmutableList;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
@@ -120,13 +124,12 @@ class BindUsingJoinTest extends TestWithFeService
implements GeneratedPlanPatter
// ---- 3. Merge key expression checks ----
@Test
- void testInnerJoinUsingMergeKeyIsLeftAlias() {
- String sql = "SELECT * FROM t1 JOIN t2 USING(a)";
- PlanChecker.from(connectContext)
- .analyze(sql)
- .matches(logicalProject().when(project ->
project.getProjects().stream()
- .filter(p -> p instanceof Alias &&
p.getName().equals("a"))
- .anyMatch(alias -> alias.child(0) instanceof Slot)));
+ void testInnerJoinUsingMergeKeyKeepsLeftExprId() {
+ List<Slot> output = PlanChecker.from(connectContext)
+ .analyze("SELECT a, t1.a FROM t1 JOIN t2
USING(a)").getPlan().getOutput();
+ Assertions.assertEquals(output.get(1).getExprId(),
output.get(0).getExprId());
+ Assertions.assertTrue(output.get(0).getQualifier().isEmpty());
+ Assertions.assertTrue(output.get(1).getQualifier().contains("t1"));
}
@Test
@@ -188,19 +191,11 @@ class BindUsingJoinTest extends TestWithFeService
implements GeneratedPlanPatter
@Test
void testRightJoinMergeKeyFromRightSide() {
- String sql = "SELECT * FROM t1 RIGHT JOIN t2 USING(a)";
- PlanChecker.from(connectContext)
- .analyze(sql)
- .matches(logicalProject().when(project ->
project.getProjects().stream()
- .filter(p -> p instanceof Alias &&
p.getName().equals("a"))
- .map(alias -> alias.child(0))
- .filter(Slot.class::isInstance)
- .map(Slot.class::cast)
- .anyMatch(slot -> {
- // Merge key should come from right table (t2)
- return slot.getQualifier().stream()
- .anyMatch(q -> q.contains("t2"));
- })));
+ List<Slot> output = PlanChecker.from(connectContext)
+ .analyze("SELECT a, t2.a FROM t1 RIGHT JOIN t2
USING(a)").getPlan().getOutput();
+ Assertions.assertEquals(output.get(1).getExprId(),
output.get(0).getExprId());
+ Assertions.assertTrue(output.get(0).getQualifier().isEmpty());
+ Assertions.assertTrue(output.get(1).getQualifier().contains("t2"));
}
// ---- 6. RIGHT SEMI JOIN merge key is visible ----
@@ -405,4 +400,84 @@ class BindUsingJoinTest extends TestWithFeService
implements GeneratedPlanPatter
return outputNames.equals(Arrays.asList("ak", "pk", "lv",
"rv", "nv"));
}));
}
+
+ @Test
+ void testUsingJoinGroupByPreservedKey() {
+ String[] joinTypes = {
+ "JOIN", "LEFT JOIN", "LEFT SEMI JOIN", "LEFT ANTI JOIN",
+ "RIGHT JOIN", "RIGHT SEMI JOIN", "RIGHT ANTI JOIN"
+ };
+ for (String joinType : joinTypes) {
+ String key = joinType.startsWith("RIGHT") ? "t2.a" : "t1.a";
+ String from = " FROM t1 " + joinType + " t2 USING(a)";
+ PlanChecker.from(connectContext).checkPlannerResult(
+ "SELECT " + key + ", COUNT(*)" + from + " GROUP BY a");
+ PlanChecker.from(connectContext).checkPlannerResult(
+ "SELECT a, COUNT(*)" + from + " GROUP BY " + key);
+ PlanChecker.from(connectContext).checkPlannerResult(
+ "SELECT COUNT(*)" + from + " GROUP BY a HAVING " + key + "
> 0 ORDER BY " + key);
+ PlanChecker.from(connectContext).checkPlannerResult(
+ "SELECT " + key + " + 1, COUNT(*)" + from + " GROUP BY a +
1");
+ }
+ }
+
+ @Test
+ void testUsingJoinGroupByMultipleKeys() {
+ PlanChecker.from(connectContext).checkPlannerResult(
+ "SELECT l.a, l.b, COUNT(*) FROM t1 l LEFT JOIN t1 r USING(a,b)
GROUP BY a,b");
+ PlanChecker.from(connectContext).checkPlannerResult(
+ "SELECT a, b, COUNT(*) FROM t1 l LEFT JOIN t1 r USING(a,b)
GROUP BY l.a,l.b");
+ }
+
+ @Test
+ void testUsingJoinGroupByDoesNotMergeNullableSide() {
+ String[] queries = {
+ "SELECT t2.a, COUNT(*) FROM t1 LEFT JOIN t2 USING(a) GROUP BY a",
+ "SELECT t1.a, COUNT(*) FROM t1 RIGHT JOIN t2 USING(a) GROUP BY a",
+ "SELECT t1.a, COUNT(*) FROM t1 FULL JOIN t2 USING(a) GROUP BY a",
+ "SELECT a, COUNT(*) FROM t1 FULL JOIN t2 USING(a) GROUP BY t1.a"
+ };
+ for (String sql : queries) {
+ AnalysisException error =
Assertions.assertThrows(AnalysisException.class,
+ () -> PlanChecker.from(connectContext).analyze(sql), sql);
+ Assertions.assertTrue(error.getMessage().contains("must appear in
the GROUP BY"), error.getMessage());
+ }
+ }
+
+ @Test
+ void testUsingJoinGroupByDerivedInput() {
+ PlanChecker.from(connectContext).checkPlannerResult(
+ "WITH l AS (SELECT 1 AS k, 10 AS v UNION ALL SELECT 2, 20), "
+ + "r AS (SELECT 1 AS k UNION ALL SELECT 3) "
+ + "SELECT l.k, SUM(l.v) FROM l LEFT JOIN r USING(k) "
+ + "GROUP BY k HAVING l.k > 0 ORDER BY l.k");
+ }
+
+ @Test
+ void testUsingJoinCteColumnOrder() {
+ String[] joins = {"JOIN", "LEFT JOIN", "RIGHT JOIN"};
+ for (String join : joins) {
+ String source = join.startsWith("RIGHT") ? "t2.a" : "t1.a";
+ String payload = join.startsWith("RIGHT") ? "t2.c" : "t1.b";
+ String sql = "WITH q(x,y,z) AS (SELECT a," + payload + "," + source
+ + " FROM t1 " + join + " t2 USING(a)) SELECT * FROM q";
+ List<Slot> output =
PlanChecker.from(connectContext).analyze(sql).getPlan().getOutput();
+ Assertions.assertEquals(ImmutableList.of("x", "y", "z"),
output.stream()
+ .map(Slot::getName).collect(Collectors.toList()), sql);
+ PlanChecker.from(connectContext).checkPlannerResult(sql);
+ }
+ }
+
+ @Test
+ void testCteRepeatedColumnResults() {
+ String sql = "WITH q(x,y,z) AS (SELECT a,b,a FROM (SELECT 1 AS a,10 AS
b) t) SELECT * FROM q";
+ PlanChecker.from(connectContext).checkPlannerResult(sql, planner -> {
+ PhysicalPlan plan = planner.getPhysicalPlan();
+ Assertions.assertTrue(plan instanceof ComputeResultSet);
+ Assertions.assertEquals(ImmutableList.of(ImmutableList.of("1",
"10", "1")),
+ ((ComputeResultSet)
plan).computeResultInFe(planner.getCascadesContext(), plan.getOutput())
+ .orElseThrow(() -> new AssertionError("Expected
FE-computed CTE result")).getResultRows());
+ });
+ }
+
}
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/logical/LogicalCTEConsumerTest.java
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/logical/LogicalCTEConsumerTest.java
new file mode 100644
index 00000000000..7b9daaf7cd1
--- /dev/null
+++
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/logical/LogicalCTEConsumerTest.java
@@ -0,0 +1,104 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.doris.nereids.trees.plans.logical;
+
+import org.apache.doris.nereids.rules.rewrite.AdjustNullable;
+import org.apache.doris.nereids.rules.rewrite.EliminateGroupByKey;
+import org.apache.doris.nereids.trees.expressions.CTEId;
+import org.apache.doris.nereids.trees.expressions.ExprId;
+import org.apache.doris.nereids.trees.expressions.Slot;
+import org.apache.doris.nereids.trees.expressions.SlotReference;
+import org.apache.doris.nereids.trees.plans.RelationId;
+import org.apache.doris.nereids.types.IntegerType;
+
+import com.google.common.collect.ImmutableList;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+class LogicalCTEConsumerTest {
+ private LogicalCTEConsumer newConsumer() {
+ Slot key = new SlotReference("x", IntegerType.INSTANCE, false);
+ Slot payload = new SlotReference("y", IntegerType.INSTANCE, false);
+ LogicalEmptyRelation producer = new LogicalEmptyRelation(new
RelationId(0),
+ ImmutableList.of(key, payload, key.withName("z")));
+ return new LogicalCTEConsumer(new RelationId(1), new CTEId(0), "q",
producer);
+ }
+
+ private void assertColumnOrder(LogicalCTEConsumer consumer) {
+ List<Slot> output = consumer.getOutput();
+ Assertions.assertEquals(ImmutableList.of("x", "y", "z"),
output.stream()
+ .map(Slot::getName).collect(Collectors.toList()));
+
Assertions.assertEquals(consumer.getProducerSlot(output.get(0)).getExprId(),
+ consumer.getProducerSlot(output.get(2)).getExprId());
+
Assertions.assertNotEquals(consumer.getProducerSlot(output.get(0)).getExprId(),
+ consumer.getProducerSlot(output.get(1)).getExprId());
+ }
+
+ @Test
+ void testRepeatedProducerSlotKeepsColumnOrder() {
+ LogicalCTEConsumer consumer = newConsumer();
+ assertColumnOrder(consumer);
+ assertColumnOrder((LogicalCTEConsumer)
consumer.withGroupExpression(Optional.empty()));
+ assertColumnOrder((LogicalCTEConsumer)
consumer.withGroupExprLogicalPropChildren(
+ Optional.empty(), Optional.empty(), ImmutableList.of()));
+ }
+
+ @Test
+ void testPruneOutputsKeepsColumnOrder() {
+ LogicalCTEConsumer consumer = newConsumer();
+ List<Slot> output =
ImmutableList.copyOf(consumer.getConsumerToProducerOutputMap().keySet());
+ LogicalCTEConsumer pruned = (LogicalCTEConsumer) consumer.pruneOutputs(
+ ImmutableList.of(output.get(1), output.get(2)));
+ Assertions.assertEquals(ImmutableList.of("y", "z"),
pruned.getOutput().stream()
+ .map(Slot::getName).collect(Collectors.toList()));
+ }
+
+ @Test
+ void testAdjustNullableKeepsColumnOrder() {
+ LogicalCTEConsumer consumer = newConsumer();
+ Slot producer =
consumer.getConsumerToProducerOutputMap().values().iterator().next();
+ Map<ExprId, Slot> replaceMap = new HashMap<>();
+ replaceMap.put(producer.getExprId(), producer.withNullable(true));
+ LogicalCTEConsumer updated = (LogicalCTEConsumer) new
AdjustNullable(false)
+ .visitLogicalCTEConsumer(consumer, replaceMap);
+ assertColumnOrder(updated);
+ Assertions.assertTrue(updated.getOutput().get(0).nullable());
+ Assertions.assertTrue(updated.getOutput().get(2).nullable());
+ Assertions.assertFalse(updated.getOutput().get(1).nullable());
+ }
+
+ @Test
+ void testReplaceProducerIdKeepsColumnOrder() {
+ LogicalCTEConsumer consumer = newConsumer();
+ Slot producer =
consumer.getConsumerToProducerOutputMap().values().iterator().next();
+ ExprId newId = new ExprId(999);
+ Map<ExprId, ExprId> replaceMap = new HashMap<>();
+ replaceMap.put(producer.getExprId(), newId);
+ LogicalCTEConsumer updated = (LogicalCTEConsumer) new
EliminateGroupByKey()
+ .visitLogicalCTEConsumer(consumer, replaceMap);
+ assertColumnOrder(updated);
+ Assertions.assertEquals(newId,
updated.getProducerSlot(updated.getOutput().get(0)).getExprId());
+ Assertions.assertEquals(newId,
updated.getProducerSlot(updated.getOutput().get(2)).getExprId());
+ }
+}
diff --git
a/regression-test/suites/query_p0/join/test_using_join_merge_key.groovy
b/regression-test/suites/query_p0/join/test_using_join_merge_key.groovy
index 1247afe8f79..7f61ac8e28e 100644
--- a/regression-test/suites/query_p0/join/test_using_join_merge_key.groovy
+++ b/regression-test/suites/query_p0/join/test_using_join_merge_key.groovy
@@ -136,4 +136,19 @@ suite("test_using_join_merge_key", "query_p0") {
FROM l FULL OUTER JOIN r USING(pk) FULL OUTER JOIN n USING(ak)
ORDER BY l.pk NULLS LAST, r.pk NULLS LAST, n.ak NULLS LAST;
"""
+
+ // Qualified and unqualified references to a forwarded USING key must bind
to the same slot.
+ sql """
+ EXPLAIN
+ WITH l AS (
+ SELECT 1 AS k, 10 AS v UNION ALL SELECT 2, 20
+ ), r AS (
+ SELECT 1 AS k UNION ALL SELECT 3
+ )
+ SELECT l.k, SUM(l.v)
+ FROM l LEFT JOIN r USING(k)
+ GROUP BY k
+ HAVING l.k > 0
+ ORDER BY l.k
+ """
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]