This is an automated email from the ASF dual-hosted git repository.
924060929 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/master by this push:
new 89f2ed38996 [fix](fe) Preserve column identity for forwarded USING
keys (#69388)
89f2ed38996 is described below
commit 89f2ed389964a47894629d678244b821e18a830d
Author: 924060929 <[email protected]>
AuthorDate: Mon Oct 26 17:23:53 2026 +0800
[fix](fe) Preserve column identity for forwarded USING keys (#69388)
### What problem does this PR solve?
Issue Number: N/A
Related PR: #64718
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.
---
.../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 | 114 +++++++++++++++++----
.../plans/logical/LogicalCTEConsumerTest.java | 104 +++++++++++++++++++
.../query_p0/join/test_using_join_merge_key.groovy | 15 +++
8 files changed, 233 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 456b7ca1892..6b79a728f7d 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
@@ -1053,7 +1053,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);
@@ -1064,10 +1066,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 d0d3d33822d..73c2fe24c7d 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
@@ -421,15 +421,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 df4ffb7581e..e12301f7fde 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
@@ -54,6 +54,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;
@@ -159,7 +160,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 28eef6bb1b4..d3f485fda57 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,6 +25,8 @@ 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;
@@ -119,13 +122,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
@@ -187,19 +189,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 ----
@@ -404,4 +398,84 @@ class BindUsingJoinTest extends TestWithFeService
implements GeneratedPlanPatter
return outputNames.equals(List.of("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(List.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(List.of(List.of("1", "10", "1")),
+ ((ComputeResultSet)
plan).computeResultInFe(planner.getCascadesContext(), plan.getOutput())
+ .orElseThrow().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..6a20f2074cb
--- /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(List.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(List.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]