This is an automated email from the ASF dual-hosted git repository.

mihaibudiu pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/calcite.git


The following commit(s) were added to refs/heads/main by this push:
     new 4e9750ad7b [CALCITE-7856] MERGE with a correlated sub-query in UPDATE 
SET resolves a target column to the same-named source column
4e9750ad7b is described below

commit 4e9750ad7bb30670143e1fef8cb626da8979ca70
Author: Iurii Gerzhedovich <[email protected]>
AuthorDate: Wed Oct 14 14:20:18 2026 +0300

    [CALCITE-7856] MERGE with a correlated sub-query in UPDATE SET resolves a 
target column to the same-named source column
---
 .../calcite/sql/validate/SqlValidatorImpl.java     | 25 ++++++++++++++++++++--
 .../apache/calcite/sql2rel/SqlToRelConverter.java  |  5 ++++-
 .../apache/calcite/test/SqlToRelConverterTest.java | 11 ++++++++++
 .../apache/calcite/test/SqlToRelConverterTest.xml  | 23 ++++++++++++++++++++
 4 files changed, 61 insertions(+), 3 deletions(-)

diff --git 
a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java 
b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java
index 85d7bc8bb4..fd48733f8c 100644
--- a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java
+++ b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java
@@ -2128,6 +2128,20 @@ && requireNonNull(((SqlNumericLiteral) 
node).bigDecimalValue())
     }
   }
 
+  /** Shuttle that copies every call and identifier of a tree, so that the copy
+   * shares no mutable node with the original. */
+  private static class DeepCopyShuttle extends SqlShuttle {
+    @Override public @Nullable SqlNode visit(SqlCall call) {
+      final CallCopyingArgHandler argHandler = new CallCopyingArgHandler(call, 
true);
+      call.getOperator().acceptCall(this, call, false, argHandler);
+      return argHandler.result();
+    }
+
+    @Override public @Nullable SqlNode visit(SqlIdentifier id) {
+      return id.clone(id.getParserPosition());
+    }
+  }
+
   private static void rewriteMerge(SqlMerge call) {
     SqlNodeList selectList;
     SqlUpdate updateStmt = call.getUpdateCall();
@@ -2135,9 +2149,16 @@ private static void rewriteMerge(SqlMerge call) {
       // if we have an update statement, just clone the select list
       // from the update statement's source since it's the same as
       // what we want for the select list of the merge source -- '*'
-      // followed by the update set expressions
+      // followed by the update set expressions. The list must be a deep copy:
+      // a shallow clone would share sub-queries (and their identifiers) with
+      // the update's own source select, and the validator keeps only one
+      // scope per node, so correlated references would be resolved against
+      // the wrong scope.
       SqlSelect sourceSelect = 
SqlNonNullableAccessors.getSourceSelect(updateStmt);
-      selectList = 
SqlNode.clone(SqlNonNullableAccessors.getSelectList(sourceSelect));
+      selectList =
+          (SqlNodeList) requireNonNull(
+              SqlNonNullableAccessors.getSelectList(sourceSelect)
+                  .accept(new DeepCopyShuttle()));
     } else {
       // otherwise, just use select *
       selectList = new SqlNodeList(SqlParserPos.ZERO);
diff --git 
a/core/src/main/java/org/apache/calcite/sql2rel/SqlToRelConverter.java 
b/core/src/main/java/org/apache/calcite/sql2rel/SqlToRelConverter.java
index 828c3d94ab..0223c778a6 100644
--- a/core/src/main/java/org/apache/calcite/sql2rel/SqlToRelConverter.java
+++ b/core/src/main/java/org/apache/calcite/sql2rel/SqlToRelConverter.java
@@ -4917,8 +4917,11 @@ private RelNode convertMerge(SqlMerge call) {
           Util.skip(project.getProjects(), nSourceFields));
     }
 
+    // Keep the correlation variables set by the source projection: the SET
+    // expressions may contain correlated sub-queries.
     relBuilder.push(join)
-        .project(projects);
+        .project(projects, ImmutableList.of(), false,
+            ((LogicalProject) mergeSourceRel).getVariablesSet());
 
     return LogicalTableModify.create(targetTable, catalogReader,
         relBuilder.build(), LogicalTableModify.Operation.MERGE,
diff --git 
a/core/src/test/java/org/apache/calcite/test/SqlToRelConverterTest.java 
b/core/src/test/java/org/apache/calcite/test/SqlToRelConverterTest.java
index 9dae64864f..4b26c3f284 100644
--- a/core/src/test/java/org/apache/calcite/test/SqlToRelConverterTest.java
+++ b/core/src/test/java/org/apache/calcite/test/SqlToRelConverterTest.java
@@ -4135,6 +4135,17 @@ void checkCorrelatedMapSubQuery(boolean expand) {
     sql(sql).ok();
   }
 
+  /** Tests a MERGE whose UPDATE contains a correlated sub-query referencing a
+   * target column that has the same name as a source column. */
+  @Test void testMergeUpdateCorrelatedSubQuery() {
+    final String sql = "merge into empnullables e\n"
+        + "using (select * from emp) t\n"
+        + "on e.empno = t.empno\n"
+        + "when matched then update\n"
+        + "set sal = (select d.deptno from dept d where d.deptno = e.deptno)";
+    sql(sql).withExpand(false).ok();
+  }
+
   @Test void testSelectView() {
     // translated condition: deptno = 20 and sal > 1000 and empno > 100
     final String sql = "select * from emp_20 where empno > 100";
diff --git 
a/core/src/test/resources/org/apache/calcite/test/SqlToRelConverterTest.xml 
b/core/src/test/resources/org/apache/calcite/test/SqlToRelConverterTest.xml
index 36200c6707..21db30b73e 100644
--- a/core/src/test/resources/org/apache/calcite/test/SqlToRelConverterTest.xml
+++ b/core/src/test/resources/org/apache/calcite/test/SqlToRelConverterTest.xml
@@ -5966,6 +5966,29 @@ LogicalTableModify(table=[[CATALOG, SALES, 
EMPNULLABLES]], operation=[MERGE], up
         LogicalFilter(condition=[IS NULL($7)])
           LogicalTableScan(table=[[CATALOG, SALES, EMP]])
       LogicalTableScan(table=[[CATALOG, SALES, EMPNULLABLES]])
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testMergeUpdateCorrelatedSubQuery">
+    <Resource name="sql">
+      <![CDATA[merge into empnullables e
+using (select * from emp) t
+on e.empno = t.empno
+when matched then update
+set sal = (select d.deptno from dept d where d.deptno = e.deptno)]]>
+    </Resource>
+    <Resource name="plan">
+      <![CDATA[
+LogicalTableModify(table=[[CATALOG, SALES, EMPNULLABLES]], operation=[MERGE], 
updateColumnList=[[SAL]], flattened=[true])
+  LogicalProject(variablesSet=[[$cor0]], EMPNO0=[$9], ENAME0=[$10], 
JOB0=[$11], MGR0=[$12], HIREDATE0=[$13], SAL0=[$14], COMM0=[$15], 
DEPTNO0=[$16], SLACKER0=[$17], $f9=[$SCALAR_QUERY({
+LogicalProject(DEPTNO=[$0])
+  LogicalFilter(condition=[=($0, $cor0.DEPTNO0)])
+    LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
+})])
+    LogicalJoin(condition=[=($9, $0)], joinType=[inner])
+      LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], 
HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8])
+        LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+      LogicalTableScan(table=[[CATALOG, SALES, EMPNULLABLES]])
 ]]>
     </Resource>
   </TestCase>

Reply via email to