This is an automated email from the ASF dual-hosted git repository.
morrysnow 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 1cac2c502a4 [fix](fe) Fix duplicate RelationId bug caused by subquery
in simple case when (#61813)
1cac2c502a4 is described below
commit 1cac2c502a412de104b4e3c92cbecc1c8f8a2126
Author: morrySnow <[email protected]>
AuthorDate: Fri Mar 27 21:21:45 2026 +0800
[fix](fe) Fix duplicate RelationId bug caused by subquery in simple case
when (#61813)
### What problem does this PR solve?
Problem Summary:
When parsing a simple CASE expression with a subquery as the value
(e.g., `CASE (SELECT sum(col) FROM t) WHEN 1 THEN 2 WHEN 3 THEN 4 END`),
the parser (`LogicalPlanBuilder.visitSimpleCase`) duplicated the same
subquery
expression into multiple `EqualTo` nodes across WhenClauses. Since the
subquery
carried a fixed `RelationId`, this created duplicate RelationIds in the
plan tree.
Later, different SlotIds were assigned to the same `SlotReference` in
different
copies, preventing them from being merged back into one subquery. This
caused
the **"groupExpression already exists in memo"** error.
**How to reproduce:**
```sql
CREATE TABLE test (
big_key int,
int_1 int
) PROPERTIES (
"replication_allocation" = "tag.location.default: 1"
);
select case (select sum(int_1) from test)
when 1 then 2
when 3 then 4
else 5
end a3 from test;
-- Error: groupExpression already exists in memo, maybe a bug
```
**Solution:**
The fix defers `EqualTo` construction from parse time to analysis time:
1. Added a `value` field to `CaseWhen` expression, stored as
`children[0]` when present. Children layout: `[value?, WhenClause+,
defaultValue?]`
2. `LogicalPlanBuilder.visitSimpleCase` now passes the value to
`CaseWhen` directly instead of wrapping each WhenClause operand in
`EqualTo(value, operand)`
3. `ExpressionAnalyzer.visitCaseWhen` handles simple case: analyzes the
value once, then constructs `EqualTo(analyzedValue, analyzedOperand)`
per WhenClause with proper type coercion via
`TypeCoercionUtils.processComparisonPredicate`, producing a standard
searched-case `CaseWhen` (no value field). This ensures the subquery is
analyzed exactly once with a single `RelationId`.
After analysis, all downstream rules see a standard searched-case
`CaseWhen` — no changes needed elsewhere.
### Release note
Fixed a bug where using a scalar subquery as the value in a simple CASE
expression (e.g., `CASE (SELECT ...) WHEN val THEN ...`) caused a
"groupExpression already exists in memo" planning error.
Co-authored-by: Copilot <[email protected]>
---
.../doris/nereids/parser/LogicalPlanBuilder.java | 6 +-
.../nereids/rules/analysis/ExpressionAnalyzer.java | 26 +++
.../doris/nereids/trees/expressions/CaseWhen.java | 114 +++++++++----
.../nereids/trees/expressions/CaseWhenTest.java | 184 +++++++++++++++++++++
.../subquery/test_subquery_in_simple_case.out | 36 ++++
.../subquery/test_subquery_in_simple_case.groovy | 82 +++++++++
6 files changed, 410 insertions(+), 38 deletions(-)
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/LogicalPlanBuilder.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/LogicalPlanBuilder.java
index 2a4c7fa7fea..b7af838ee5b 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/LogicalPlanBuilder.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/LogicalPlanBuilder.java
@@ -3278,12 +3278,12 @@ public class LogicalPlanBuilder extends
DorisParserBaseVisitor<Object> {
public Expression visitSimpleCase(DorisParser.SimpleCaseContext context) {
Expression e = getExpression(context.value);
List<WhenClause> whenClauses = context.whenClause().stream()
- .map(w -> new WhenClause(new EqualTo(e,
getExpression(w.condition)), getExpression(w.result)))
+ .map(w -> new WhenClause(getExpression(w.condition),
getExpression(w.result)))
.collect(ImmutableList.toImmutableList());
if (context.elseExpression == null) {
- return new CaseWhen(whenClauses);
+ return new CaseWhen(e, whenClauses);
}
- return new CaseWhen(whenClauses,
getExpression(context.elseExpression));
+ return new CaseWhen(e, whenClauses,
getExpression(context.elseExpression));
}
/**
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/ExpressionAnalyzer.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/ExpressionAnalyzer.java
index 3d1faf318e6..e2566c8427f 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/ExpressionAnalyzer.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/ExpressionAnalyzer.java
@@ -875,6 +875,32 @@ public class ExpressionAnalyzer extends
SubExprAnalyzer<ExpressionRewriteContext
@Override
public Expression visitCaseWhen(CaseWhen caseWhen,
ExpressionRewriteContext context) {
+ if (caseWhen.getValue().isPresent()) {
+ // Simple case: CASE value WHEN cond THEN result ...
+ // Analyze value once, then construct EqualTo(analyzedValue,
analyzedCond) per WhenClause
+ Expression analyzedValue = caseWhen.getValue().get().accept(this,
context);
+
+ List<WhenClause> newWhenClauses = new ArrayList<>();
+ for (WhenClause whenClause : caseWhen.getWhenClauses()) {
+ Expression operand = whenClause.getOperand().accept(this,
context);
+ Expression result = whenClause.getResult().accept(this,
context);
+ Expression equalTo =
TypeCoercionUtils.processComparisonPredicate(
+ new EqualTo(analyzedValue, operand));
+ newWhenClauses.add(new WhenClause(equalTo, result));
+ }
+
+ CaseWhen newCaseWhen;
+ if (caseWhen.getDefaultValue().isPresent()) {
+ Expression analyzedDefault =
caseWhen.getDefaultValue().get().accept(this, context);
+ newCaseWhen = new CaseWhen(newWhenClauses, analyzedDefault);
+ } else {
+ newCaseWhen = new CaseWhen(newWhenClauses);
+ }
+ newCaseWhen.checkLegalityBeforeTypeCoercion();
+ return TypeCoercionUtils.processCaseWhen(newCaseWhen);
+ }
+
+ // Searched case: standard handling
Builder<Expression> rewrittenChildren =
ImmutableList.builderWithExpectedSize(caseWhen.arity());
for (Expression child : caseWhen.children()) {
rewrittenChildren.add(child.accept(this, context));
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/CaseWhen.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/CaseWhen.java
index ddb3491e244..4c81393e6b0 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/CaseWhen.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/CaseWhen.java
@@ -17,7 +17,6 @@
package org.apache.doris.nereids.trees.expressions;
-import org.apache.doris.nereids.exceptions.AnalysisException;
import org.apache.doris.nereids.exceptions.UnboundException;
import org.apache.doris.nereids.trees.expressions.visitor.ExpressionVisitor;
import org.apache.doris.nereids.types.DataType;
@@ -37,33 +36,69 @@ import java.util.function.Supplier;
* The internal representation of
* CASE [expr] WHEN expr THEN expr [WHEN expr THEN expr ...] [ELSE expr] END
* Each When/Then is stored as two consecutive children (whenExpr, thenExpr).
- * If a case expr is given, convert it to equalTo(caseExpr, whenExpr) and set
it to whenExpr.
+ * If a case expr is given (simple case), the value is stored as the first
child.
+ * During analysis, the value is consumed: EqualTo(value, whenCondition) is
constructed
+ * for each WhenClause, producing a standard searched case form.
* If an else expr is given then it is the last child.
+ *
+ * Children layout: [value?, WhenClause+, defaultValue?]
*/
public class CaseWhen extends Expression implements NeedSessionVarGuard {
+ private final Optional<Expression> value;
private final List<WhenClause> whenClauses;
private final Optional<Expression> defaultValue;
- private Supplier<List<DataType>> dataTypesForCoercion;
+ private final Supplier<List<DataType>> dataTypesForCoercion;
public CaseWhen(List<WhenClause> whenClauses) {
super((List) whenClauses);
+ this.value = Optional.empty();
this.whenClauses =
ImmutableList.copyOf(Objects.requireNonNull(whenClauses));
- defaultValue = Optional.empty();
+ this.defaultValue = Optional.empty();
this.dataTypesForCoercion = computeDataTypesForCoercion();
}
- /** CaseWhen */
+ /** CaseWhen with default value (searched case) */
public CaseWhen(List<WhenClause> whenClauses, Expression defaultValue) {
super(ImmutableList.<Expression>builderWithExpectedSize(whenClauses.size() + 1)
.addAll(whenClauses)
.add(defaultValue)
.build());
+ this.value = Optional.empty();
this.whenClauses =
ImmutableList.copyOf(Objects.requireNonNull(whenClauses));
this.defaultValue = Optional.of(Objects.requireNonNull(defaultValue));
this.dataTypesForCoercion = computeDataTypesForCoercion();
}
+ /** Simple case: CASE value WHEN ... */
+ public CaseWhen(Expression value, List<WhenClause> whenClauses) {
+
super(ImmutableList.<Expression>builderWithExpectedSize(whenClauses.size() + 1)
+ .add(Objects.requireNonNull(value))
+ .addAll(whenClauses)
+ .build());
+ this.value = Optional.of(value);
+ this.whenClauses =
ImmutableList.copyOf(Objects.requireNonNull(whenClauses));
+ this.defaultValue = Optional.empty();
+ this.dataTypesForCoercion = computeDataTypesForCoercion();
+ }
+
+ /** Simple case with default value: CASE value WHEN ... ELSE ... END */
+ public CaseWhen(Expression value, List<WhenClause> whenClauses, Expression
defaultValue) {
+
super(ImmutableList.<Expression>builderWithExpectedSize(whenClauses.size() + 2)
+ .add(Objects.requireNonNull(value))
+ .addAll(whenClauses)
+ .add(defaultValue)
+ .build());
+ this.value = Optional.of(value);
+ this.whenClauses =
ImmutableList.copyOf(Objects.requireNonNull(whenClauses));
+ this.defaultValue = Optional.of(Objects.requireNonNull(defaultValue));
+ this.dataTypesForCoercion = computeDataTypesForCoercion();
+ }
+
+ public Optional<Expression> getValue() {
+ return value;
+ }
+
public List<WhenClause> getWhenClauses() {
return whenClauses;
}
@@ -83,7 +118,7 @@ public class CaseWhen extends Expression implements
NeedSessionVarGuard {
@Override
public DataType getDataType() {
- return child(0).getDataType();
+ return whenClauses.get(0).getDataType();
}
@Override
@@ -99,13 +134,11 @@ public class CaseWhen extends Expression implements
NeedSessionVarGuard {
@Override
public String toString() {
StringBuilder output = new StringBuilder("CASE");
- for (Expression child : children()) {
- if (child instanceof WhenClause) {
- output.append(child.toString());
- } else {
- output.append(" ELSE ").append(child.toString());
- }
+ value.ifPresent(v -> output.append(" ").append(v.toString()));
+ for (WhenClause whenClause : whenClauses) {
+ output.append(whenClause.toString());
}
+ defaultValue.ifPresent(dv -> output.append(" ELSE
").append(dv.toString()));
output.append(" END");
return output.toString();
}
@@ -113,13 +146,11 @@ public class CaseWhen extends Expression implements
NeedSessionVarGuard {
@Override
public String toDigest() {
StringBuilder sb = new StringBuilder("CASE");
- for (Expression child : children()) {
- if (child instanceof WhenClause) {
- sb.append(child.toDigest());
- } else {
- sb.append(" ELSE ").append(child.toDigest());
- }
+ value.ifPresent(v -> sb.append(" ").append(v.toDigest()));
+ for (WhenClause whenClause : whenClauses) {
+ sb.append(whenClause.toDigest());
}
+ defaultValue.ifPresent(dv -> sb.append(" ELSE
").append(dv.toDigest()));
sb.append(" END");
return sb.toString();
}
@@ -127,13 +158,11 @@ public class CaseWhen extends Expression implements
NeedSessionVarGuard {
@Override
public String computeToSql() throws UnboundException {
StringBuilder output = new StringBuilder("CASE");
- for (Expression child : children()) {
- if (child instanceof WhenClause) {
- output.append(child.toSql());
- } else {
- output.append(" ELSE ").append(child.toSql());
- }
+ value.ifPresent(v -> output.append(" ").append(v.toSql()));
+ for (WhenClause whenClause : whenClauses) {
+ output.append(whenClause.toSql());
}
+ defaultValue.ifPresent(dv -> output.append(" ELSE
").append(dv.toSql()));
output.append(" END");
return output.toString();
}
@@ -141,21 +170,36 @@ public class CaseWhen extends Expression implements
NeedSessionVarGuard {
@Override
public CaseWhen withChildren(List<Expression> children) {
Preconditions.checkArgument(!children.isEmpty(), "case when should has
at least 1 child");
+ int i = 0;
+ Expression value = null;
+ // First non-WhenClause child before any WhenClause is the simple case
value.
+ // Note: value is always consumed during analysis phase; post-analysis,
+ // the first child is always a WhenClause, so this branch is only
taken pre-analysis.
+ if (i < children.size() && !(children.get(i) instanceof WhenClause)) {
+ value = children.get(i);
+ i++;
+ }
List<WhenClause> whenClauseList = new ArrayList<>();
+ while (i < children.size() && children.get(i) instanceof WhenClause) {
+ whenClauseList.add((WhenClause) children.get(i));
+ i++;
+ }
+ Preconditions.checkArgument(!whenClauseList.isEmpty(), "case when
should has at least 1 when clause");
Expression defaultValue = null;
- for (int i = 0; i < children.size(); i++) {
- if (children.get(i) instanceof WhenClause) {
- whenClauseList.add((WhenClause) children.get(i));
- } else if (children.size() - 1 == i) {
- defaultValue = children.get(i);
- } else {
- throw new AnalysisException("The children format needs to be
[WhenClause+, DefaultValue?]");
- }
+ if (i < children.size()) {
+ defaultValue = children.get(i);
+ i++;
}
- if (defaultValue == null) {
- return new CaseWhen(whenClauseList);
+ Preconditions.checkArgument(i == children.size(),
+ "The children format needs to be [Value?, WhenClause+,
DefaultValue?]");
+ if (value != null) {
+ return defaultValue != null
+ ? new CaseWhen(value, whenClauseList, defaultValue)
+ : new CaseWhen(value, whenClauseList);
}
- return new CaseWhen(whenClauseList, defaultValue);
+ return defaultValue != null
+ ? new CaseWhen(whenClauseList, defaultValue)
+ : new CaseWhen(whenClauseList);
}
private Supplier<List<DataType>> computeDataTypesForCoercion() {
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/CaseWhenTest.java
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/CaseWhenTest.java
new file mode 100644
index 00000000000..5f71ddf8345
--- /dev/null
+++
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/CaseWhenTest.java
@@ -0,0 +1,184 @@
+// 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.expressions;
+
+import org.apache.doris.nereids.trees.expressions.literal.IntegerLiteral;
+import org.apache.doris.utframe.TestWithFeService;
+
+import com.google.common.collect.ImmutableList;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+public class CaseWhenTest extends TestWithFeService {
+
+ @Override
+ protected void runBeforeAll() throws Exception {
+ createDatabase("test");
+ connectContext.setDatabase("test");
+ createTable("create table t1 (\n"
+ + " a int,\n"
+ + " b int\n"
+ + ")\n"
+ + "distributed by hash(a) buckets 4\n"
+ + "properties(\n"
+ + " \"replication_num\"=\"1\"\n"
+ + ")");
+ }
+
+ @Test
+ void testSimpleCaseValueField() {
+ IntegerLiteral value = new IntegerLiteral(1);
+ WhenClause wc1 = new WhenClause(new IntegerLiteral(1), new
IntegerLiteral(10));
+ WhenClause wc2 = new WhenClause(new IntegerLiteral(2), new
IntegerLiteral(20));
+ IntegerLiteral defaultVal = new IntegerLiteral(99);
+
+ // Simple case with value and default
+ CaseWhen withValue = new CaseWhen(value, ImmutableList.of(wc1, wc2),
defaultVal);
+ Assertions.assertTrue(withValue.getValue().isPresent());
+ Assertions.assertEquals(value, withValue.getValue().get());
+ Assertions.assertEquals(2, withValue.getWhenClauses().size());
+ Assertions.assertTrue(withValue.getDefaultValue().isPresent());
+ Assertions.assertEquals(defaultVal, withValue.getDefaultValue().get());
+
+ // Children layout: [value, WhenClause1, WhenClause2, defaultValue]
+ List<Expression> children = withValue.children();
+ Assertions.assertEquals(4, children.size());
+ Assertions.assertEquals(value, children.get(0));
+ Assertions.assertInstanceOf(WhenClause.class, children.get(1));
+ Assertions.assertInstanceOf(WhenClause.class, children.get(2));
+ Assertions.assertEquals(defaultVal, children.get(3));
+
+ // Simple case without default
+ CaseWhen withValueNoDefault = new CaseWhen(value,
ImmutableList.of(wc1, wc2));
+ Assertions.assertTrue(withValueNoDefault.getValue().isPresent());
+
Assertions.assertFalse(withValueNoDefault.getDefaultValue().isPresent());
+ Assertions.assertEquals(3, withValueNoDefault.children().size());
+
+ // Searched case (no value)
+ CaseWhen searchedCase = new CaseWhen(ImmutableList.of(wc1, wc2));
+ Assertions.assertFalse(searchedCase.getValue().isPresent());
+ Assertions.assertEquals(2, searchedCase.children().size());
+
+ // Searched case with default
+ CaseWhen searchedWithDefault = new CaseWhen(ImmutableList.of(wc1,
wc2), defaultVal);
+ Assertions.assertFalse(searchedWithDefault.getValue().isPresent());
+
Assertions.assertTrue(searchedWithDefault.getDefaultValue().isPresent());
+ Assertions.assertEquals(3, searchedWithDefault.children().size());
+ }
+
+ @Test
+ void testSimpleCaseWithChildren() {
+ IntegerLiteral value = new IntegerLiteral(1);
+ WhenClause wc1 = new WhenClause(new IntegerLiteral(1), new
IntegerLiteral(10));
+ WhenClause wc2 = new WhenClause(new IntegerLiteral(2), new
IntegerLiteral(20));
+ IntegerLiteral defaultVal = new IntegerLiteral(99);
+
+ // withChildren roundtrip for simple case with default
+ CaseWhen original = new CaseWhen(value, ImmutableList.of(wc1, wc2),
defaultVal);
+ CaseWhen rebuilt = original.withChildren(original.children());
+ Assertions.assertTrue(rebuilt.getValue().isPresent());
+ Assertions.assertEquals(2, rebuilt.getWhenClauses().size());
+ Assertions.assertTrue(rebuilt.getDefaultValue().isPresent());
+ Assertions.assertEquals(original.children().size(),
rebuilt.children().size());
+
+ // withChildren roundtrip for simple case without default
+ CaseWhen noDefault = new CaseWhen(value, ImmutableList.of(wc1, wc2));
+ CaseWhen rebuiltNoDefault =
noDefault.withChildren(noDefault.children());
+ Assertions.assertTrue(rebuiltNoDefault.getValue().isPresent());
+ Assertions.assertFalse(rebuiltNoDefault.getDefaultValue().isPresent());
+
+ // withChildren roundtrip for searched case
+ CaseWhen searched = new CaseWhen(ImmutableList.of(wc1, wc2),
defaultVal);
+ CaseWhen rebuiltSearched = searched.withChildren(searched.children());
+ Assertions.assertFalse(rebuiltSearched.getValue().isPresent());
+ Assertions.assertTrue(rebuiltSearched.getDefaultValue().isPresent());
+ Assertions.assertEquals(2, rebuiltSearched.getWhenClauses().size());
+ }
+
+ @Test
+ void testSimpleCaseToSql() {
+ IntegerLiteral value = new IntegerLiteral(1);
+ WhenClause wc1 = new WhenClause(new IntegerLiteral(1), new
IntegerLiteral(10));
+ WhenClause wc2 = new WhenClause(new IntegerLiteral(2), new
IntegerLiteral(20));
+ IntegerLiteral defaultVal = new IntegerLiteral(99);
+
+ // Simple case: CASE value WHEN cond THEN result ... ELSE default END
+ CaseWhen simpleCaseWhen = new CaseWhen(value, ImmutableList.of(wc1,
wc2), defaultVal);
+ String sql = simpleCaseWhen.toSql();
+ Assertions.assertTrue(sql.startsWith("CASE 1"), "Simple case SQL
should start with 'CASE 1', got: " + sql);
+ Assertions.assertTrue(sql.contains("WHEN"), "SQL should contain WHEN:
" + sql);
+ Assertions.assertTrue(sql.contains("THEN"), "SQL should contain THEN:
" + sql);
+ Assertions.assertTrue(sql.contains("ELSE"), "SQL should contain ELSE:
" + sql);
+ Assertions.assertTrue(sql.endsWith("END"), "SQL should end with END: "
+ sql);
+
+ // Searched case: CASE WHEN cond THEN result ... END
+ CaseWhen searchedCaseWhen = new CaseWhen(ImmutableList.of(wc1, wc2));
+ String searchedSql = searchedCaseWhen.toSql();
+ Assertions.assertTrue(searchedSql.startsWith("CASE WHEN"),
+ "Searched case SQL should start with 'CASE WHEN', got: " +
searchedSql);
+ Assertions.assertFalse(searchedSql.contains("ELSE"),
+ "Searched case without default should not contain ELSE: " +
searchedSql);
+
+ // toString should behave similarly
+ String str = simpleCaseWhen.toString();
+ Assertions.assertTrue(str.startsWith("CASE 1"), "toString should start
with 'CASE 1', got: " + str);
+ Assertions.assertTrue(str.endsWith("END"), "toString should end with
END: " + str);
+ }
+
+ @Test
+ void testParseSimpleCase() {
+ // Parse a simple case expression and verify it produces a CaseWhen
with value
+ String sql = "select case a when 1 then 2 else 3 end from t1";
+ // Nereids planner is enabled by default; verify the SQL can be parsed
and analyzed
+ Assertions.assertDoesNotThrow(() -> getSQLPlanner(sql));
+ }
+
+ @Test
+ void testSimpleCaseWithSubqueryPlanning() {
+ // This is the key bug test: simple case with a subquery as value.
+ // Previously, the parser would inline the value into each WhenClause
+ // as EqualTo(subquery, literal), causing the same subquery RelationId
+ // to appear multiple times, leading to "groupExpression already exists
+ // in memo" errors during planning.
+ String sql = "select case (select sum(b) from t1) "
+ + "when 1 then 2 "
+ + "when 3 then 4 "
+ + "else 5 end from t1";
+ // This should not throw - verifying it can be planned
+ Assertions.assertDoesNotThrow(() -> getSQLPlanner(sql),
+ "Simple case with subquery value should plan successfully
without duplicate RelationId error");
+ }
+
+ @Test
+ void testSearchedCaseStillWorks() {
+ // Regression check: searched case (no value) should still work
correctly
+ String sql = "select case when a = 1 then 2 when a = 3 then 4 else 5
end from t1";
+ Assertions.assertDoesNotThrow(() -> getSQLPlanner(sql),
+ "Searched case should still work correctly");
+ }
+
+ @Test
+ void testSimpleCaseWithColumnRef() {
+ // Non-subquery simple case should also work
+ String sql = "select case a when 1 then 'one' when 2 then 'two' else
'other' end from t1";
+ Assertions.assertDoesNotThrow(() -> getSQLPlanner(sql),
+ "Simple case with column reference should plan successfully");
+ }
+}
diff --git
a/regression-test/data/nereids_p0/subquery/test_subquery_in_simple_case.out
b/regression-test/data/nereids_p0/subquery/test_subquery_in_simple_case.out
new file mode 100644
index 00000000000..8329dfe332c
--- /dev/null
+++ b/regression-test/data/nereids_p0/subquery/test_subquery_in_simple_case.out
@@ -0,0 +1,36 @@
+-- This file is automatically generated. You should know what you did if you
want to edit this
+-- !case1 --
+5
+5
+5
+
+-- !case2 --
+match
+match
+match
+
+-- !case3 --
+one
+other
+three
+
+-- !case4 --
+5
+5
+5
+
+-- !case5 --
+yes
+yes
+yes
+
+-- !case6 --
+nested_match
+nested_match
+nested_match
+
+-- !case7 --
+big
+big
+big
+
diff --git
a/regression-test/suites/nereids_p0/subquery/test_subquery_in_simple_case.groovy
b/regression-test/suites/nereids_p0/subquery/test_subquery_in_simple_case.groovy
new file mode 100644
index 00000000000..a951bd80f99
--- /dev/null
+++
b/regression-test/suites/nereids_p0/subquery/test_subquery_in_simple_case.groovy
@@ -0,0 +1,82 @@
+// 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.
+
+// Regression test for duplicate RelationId bug in simple CASE WHEN with
subqueries.
+// The bug caused "groupExpression already exists in memo" error because the
simple
+// case value (a subquery) was duplicated into multiple EqualTo nodes at parse
time.
+
+suite("test_subquery_in_simple_case") {
+ sql "SET enable_nereids_planner=true"
+ sql "SET enable_fallback_to_original_planner=false"
+
+ sql "DROP TABLE IF EXISTS test_simple_case_subquery"
+ sql """
+ CREATE TABLE test_simple_case_subquery (
+ big_key int,
+ int_1 int
+ )
+ DISTRIBUTED BY HASH(big_key) BUCKETS 3
+ PROPERTIES (
+ "replication_allocation" = "tag.location.default: 1"
+ )
+ """
+ sql "INSERT INTO test_simple_case_subquery VALUES (1, 1), (2, 3), (3, 5)"
+
+ // Case 1: Exact reproduce case from bug report.
+ // sum(int_1)=9, CASE 9 WHEN 1 THEN 2 WHEN 3 THEN 4 ELSE 5 END → 5 for all
3 rows.
+ order_qt_case1 """
+ select case (select sum(int_1) from test_simple_case_subquery) when 1
then 2 when 3 then 4 else 5 end a3 from test_simple_case_subquery
+ """
+
+ // Case 2: Simple case with scalar subquery, multiple WHEN clauses, no
ELSE.
+ // sum(int_1)=9, CASE 9 WHEN 9 THEN 'match' WHEN 0 THEN 'zero' END →
'match' for all 3 rows.
+ order_qt_case2 """
+ select case (select sum(int_1) from test_simple_case_subquery) when 9
then 'match' when 0 then 'zero' end from test_simple_case_subquery
+ """
+
+ // Case 3: Simple case with column reference (non-subquery baseline).
+ // Row-by-row: int_1=1→'one', int_1=3→'three', int_1=5→'other'.
+ order_qt_case3 """
+ select case int_1 when 1 then 'one' when 3 then 'three' else 'other'
end from test_simple_case_subquery
+ """
+
+ // Case 4: Simple case with subquery in value AND subquery in result.
+ // sum(int_1)=9, CASE 9 WHEN 9 THEN max(int_1)=5 ELSE 0 END → 5 for all 3
rows.
+ order_qt_case4 """
+ select case (select sum(int_1) from test_simple_case_subquery) when 9
then (select max(int_1) from test_simple_case_subquery) else 0 end from
test_simple_case_subquery
+ """
+
+ // Case 5: Single WHEN clause with subquery value (edge case).
+ // sum(int_1)=9, CASE 9 WHEN 9 THEN 'yes' ELSE 'no' END → 'yes' for all 3
rows.
+ order_qt_case5 """
+ select case (select sum(int_1) from test_simple_case_subquery) when 9
then 'yes' else 'no' end from test_simple_case_subquery
+ """
+
+ // Case 6: Nested case with subquery.
+ // count(*)=3, sum(int_1)=9.
+ // CASE 3 WHEN 3 THEN (CASE 9 WHEN 9 THEN 'nested_match' ELSE
'nested_other' END) ELSE 'outer_other' END
+ // → 'nested_match' for all 3 rows.
+ order_qt_case6 """
+ select case (select count(*) from test_simple_case_subquery) when 3
then case (select sum(int_1) from test_simple_case_subquery) when 9 then
'nested_match' else 'nested_other' end else 'outer_other' end from
test_simple_case_subquery
+ """
+
+ // Case 7: Searched case with subquery (should still work, not affected by
change).
+ // sum(int_1)=9, CASE WHEN 9 > 5 THEN 'big' ELSE 'small' END → 'big' for
all 3 rows.
+ order_qt_case7 """
+ select case when (select sum(int_1) from test_simple_case_subquery) >
5 then 'big' else 'small' end from test_simple_case_subquery
+ """
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]