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

xuzifu666 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 f229a2af3e [CALCITE-5261] UNION(ALL) inside of the CURSOR throws an 
exception while validating the query
f229a2af3e is described below

commit f229a2af3ea1bc622861c9ad132145cc50a99b38
Author: Yu Xu <[email protected]>
AuthorDate: Mon Jun 29 20:45:12 2026 +0800

    [CALCITE-5261] UNION(ALL) inside of the CURSOR throws an exception while 
validating the query
---
 .../calcite/sql/fun/SqlCursorConstructor.java      |  8 ++--
 .../apache/calcite/sql/validate/SqlValidator.java  |  8 ++--
 .../calcite/sql/validate/SqlValidatorImpl.java     | 50 +++++++++++++++-------
 .../apache/calcite/test/SqlToRelConverterTest.java | 14 ++++++
 .../org/apache/calcite/test/SqlValidatorTest.java  | 36 ++++++++++++++++
 .../apache/calcite/test/SqlToRelConverterTest.xml  | 32 ++++++++++++++
 6 files changed, 125 insertions(+), 23 deletions(-)

diff --git 
a/core/src/main/java/org/apache/calcite/sql/fun/SqlCursorConstructor.java 
b/core/src/main/java/org/apache/calcite/sql/fun/SqlCursorConstructor.java
index 7d23c4123e..cb6fb9ae14 100644
--- a/core/src/main/java/org/apache/calcite/sql/fun/SqlCursorConstructor.java
+++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlCursorConstructor.java
@@ -19,7 +19,7 @@
 import org.apache.calcite.rel.type.RelDataType;
 import org.apache.calcite.sql.SqlCall;
 import org.apache.calcite.sql.SqlKind;
-import org.apache.calcite.sql.SqlSelect;
+import org.apache.calcite.sql.SqlNode;
 import org.apache.calcite.sql.SqlSpecialOperator;
 import org.apache.calcite.sql.SqlWriter;
 import org.apache.calcite.sql.type.OperandTypes;
@@ -50,9 +50,9 @@ public SqlCursorConstructor() {
       SqlValidator validator,
       SqlValidatorScope scope,
       SqlCall call) {
-    SqlSelect subSelect = call.operand(0);
-    validator.declareCursor(subSelect, scope);
-    subSelect.validateExpr(validator, scope);
+    final SqlNode query = call.operand(0);
+    validator.declareCursor(query, scope);
+    query.validateExpr(validator, scope);
     return super.deriveType(validator, scope, call);
   }
 
diff --git 
a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidator.java 
b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidator.java
index 3846ed5daf..813247376d 100644
--- a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidator.java
+++ b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidator.java
@@ -635,12 +635,12 @@ SqlNodeList expandStar(SqlNodeList selectList, SqlSelect 
query,
   SqlValidatorScope getEmptyScope();
 
   /**
-   * Declares a SELECT expression as a cursor.
+   * Declares a query expression as a cursor.
    *
-   * @param select select expression associated with the cursor
-   * @param scope  scope of the parent query associated with the cursor
+   * @param query query expression associated with the cursor
+   * @param scope scope of the parent query associated with the cursor
    */
-  void declareCursor(SqlSelect select, SqlValidatorScope scope);
+  void declareCursor(SqlNode query, SqlValidatorScope scope);
 
   /**
    * Pushes a new instance of a function call on to a function call stack.
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 37f6712e90..26faab77bc 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
@@ -415,27 +415,47 @@ public SqlConformance getConformance() {
     return new SqlNodeList(list, SqlParserPos.ZERO);
   }
 
-  @Override public void declareCursor(SqlSelect select,
+  @Override public void declareCursor(SqlNode query,
       SqlValidatorScope parentScope) {
-    cursorSet.add(select);
+    cursorSet.add(query);
 
-    // add the cursor to a map that maps the cursor to its select based on
+    // add the cursor to a map that maps the cursor to its query based on
     // the position of the cursor relative to other cursors in that call
     FunctionParamInfo funcParamInfo =
         requireNonNull(functionCallStack.peek(), "functionCall");
-    Map<Integer, SqlSelect> cursorMap = funcParamInfo.cursorPosToSelectMap;
+    Map<Integer, SqlNode> cursorMap = funcParamInfo.cursorPosToQueryMap;
     final int cursorCount = cursorMap.size();
-    cursorMap.put(cursorCount, select);
+    cursorMap.put(cursorCount, query);
 
-    // create a namespace associated with the result of the select
+    // create a namespace associated with the result of the query
     // that is the argument to the cursor constructor; register it
     // with a scope corresponding to the cursor
-    SelectScope cursorScope =
-        new SelectScope(parentScope, getEmptyScope(), select);
-    clauseScopes.put(IdPair.of(select, Clause.CURSOR), cursorScope);
-    final SelectNamespace selectNs = createSelectNamespace(select, select);
-    final String alias = SqlValidatorUtil.alias(select, nextGeneratedId++);
-    registerNamespace(cursorScope, alias, selectNs, false);
+    final SqlValidatorNamespace ns;
+    final SqlValidatorScope cursorScope;
+    if (query instanceof SqlSelect) {
+      SqlSelect select = (SqlSelect) query;
+      cursorScope = new SelectScope(parentScope, getEmptyScope(), select);
+      clauseScopes.put(IdPair.of(select, Clause.CURSOR), cursorScope);
+      ns = createSelectNamespace(select, select);
+    } else {
+      final SqlCall call = (SqlCall) query;
+      cursorScope = new ListScope(parentScope) {
+        @Override public SqlNode getNode() {
+          return call;
+        }
+      };
+      if (query.isA(SqlKind.SET_QUERY)) {
+        ns = createSetopNamespace(call, call);
+      } else if (query.getKind() == SqlKind.VALUES) {
+        ns = new TableConstructorNamespace(this, call, cursorScope, call);
+      } else if (query.getKind() == SqlKind.WITH) {
+        ns = new WithNamespace(this, (SqlWith) call, call);
+      } else {
+        throw Util.unexpected(query.getKind());
+      }
+    }
+    final String alias = SqlValidatorUtil.alias(query, nextGeneratedId++);
+    registerNamespace(cursorScope, alias, ns, false);
   }
 
   @Override public void pushFunctionCall() {
@@ -8469,10 +8489,10 @@ public IdInfo(SqlValidatorScope scope, SqlIdentifier 
id) {
   protected static class FunctionParamInfo {
     /**
      * Maps a cursor (based on its position relative to other cursor
-     * parameters within a function call) to the SELECT associated with the
+     * parameters within a function call) to the query associated with the
      * cursor.
      */
-    public final Map<Integer, SqlSelect> cursorPosToSelectMap;
+    public final Map<Integer, SqlNode> cursorPosToQueryMap;
 
     /**
      * Maps a column list parameter to the parent cursor parameter it
@@ -8481,7 +8501,7 @@ protected static class FunctionParamInfo {
     public final Map<String, String> columnListParamToParentCursorMap;
 
     public FunctionParamInfo() {
-      cursorPosToSelectMap = new HashMap<>();
+      cursorPosToQueryMap = new HashMap<>();
       columnListParamToParentCursorMap = new HashMap<>();
     }
   }
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 1d5833c516..1016acc410 100644
--- a/core/src/test/java/org/apache/calcite/test/SqlToRelConverterTest.java
+++ b/core/src/test/java/org/apache/calcite/test/SqlToRelConverterTest.java
@@ -1886,6 +1886,20 @@ public static void checkActualAndReferenceFiles() {
     sql(sql).withDecorrelate(false).ok();
   }
 
+  /** Test case for CURSOR containing UNION ALL. */
+  @Test void testCollectionTableWithCursorParamUnion() {
+    final String sql = "select * from table(dedup("
+        + "cursor(select ename from emp union all select ename from emp), 
'NAME'))";
+    sql(sql).withDecorrelate(false).ok();
+  }
+
+  /** Test case for CURSOR containing UNION (distinct). */
+  @Test void testCollectionTableWithCursorParamUnionDistinct() {
+    final String sql = "select * from table(dedup("
+        + "cursor(select ename from emp union select ename from emp), 
'NAME'))";
+    sql(sql).withDecorrelate(false).ok();
+  }
+
   @Test void testUnnest() {
     final String sql = "select*from unnest(multiset[1,2])";
     sql(sql).ok();
diff --git a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java 
b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
index 0cf1966f99..0fb4fa4055 100644
--- a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
+++ b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
@@ -34,6 +34,7 @@
 import org.apache.calcite.sql.SqlFunctionCategory;
 import org.apache.calcite.sql.SqlIdentifier;
 import org.apache.calcite.sql.SqlKind;
+import org.apache.calcite.sql.SqlLiteral;
 import org.apache.calcite.sql.SqlNode;
 import org.apache.calcite.sql.SqlNodeList;
 import org.apache.calcite.sql.SqlOperator;
@@ -47,6 +48,7 @@
 import org.apache.calcite.sql.fun.SqlStdOperatorTable;
 import org.apache.calcite.sql.parser.SqlParseException;
 import org.apache.calcite.sql.parser.SqlParser;
+import org.apache.calcite.sql.parser.SqlParserPos;
 import org.apache.calcite.sql.type.ArraySqlType;
 import org.apache.calcite.sql.type.OperandTypes;
 import org.apache.calcite.sql.type.ReturnTypes;
@@ -63,6 +65,7 @@
 import org.apache.calcite.sql.validate.SqlValidator;
 import org.apache.calcite.sql.validate.SqlValidatorCatalogReader;
 import org.apache.calcite.sql.validate.SqlValidatorImpl;
+import org.apache.calcite.sql.validate.SqlValidatorScope;
 import org.apache.calcite.sql.validate.SqlValidatorUtil;
 import org.apache.calcite.test.catalog.CountingFactory;
 import org.apache.calcite.test.catalog.MockCatalogReaderSimple;
@@ -101,12 +104,14 @@
 
 import static org.apache.calcite.test.Matchers.isCharset;
 
+import static org.hamcrest.CoreMatchers.containsString;
 import static org.hamcrest.CoreMatchers.equalTo;
 import static org.hamcrest.CoreMatchers.is;
 import static org.hamcrest.CoreMatchers.nullValue;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.hasToString;
 import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.junit.jupiter.api.Assertions.fail;
 import static org.junit.jupiter.api.Assumptions.assumeTrue;
@@ -10089,11 +10094,42 @@ void testGroupExpressionEquivalenceParams() {
             + "`CATALOG`.`SALES`.`EMP` AS `EMP`");
   }
 
+  /** Test case for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-5261";>[CALCITE-5261]
+   * UNION(ALL) inside of the CURSOR throws an exception while validating the 
query</a>.
+   * */
   @Test void testCollectionTableWithCursorParam() {
     sql("select * from table(dedup(cursor(select * from emp),'ename'))")
         .type("RecordType(VARCHAR(1024) NOT NULL NAME) NOT NULL");
     sql("select * from table(dedup(cursor(select * from ^bloop^),'ename'))")
         .fails("Object 'BLOOP' not found");
+    sql("select * from table(dedup(cursor(select ename from emp union all "
+        + "select ename from emp), 'ename'))")
+        .type("RecordType(VARCHAR(1024) NOT NULL NAME) NOT NULL");
+    sql("select * from table(dedup(cursor(select ename from emp union "
+        + "select ename from emp), 'ename'))")
+        .type("RecordType(VARCHAR(1024) NOT NULL NAME) NOT NULL");
+    sql("select * from table(dedup(cursor(values ('a'), ('b')), 'COLUMN0'))")
+        .type("RecordType(VARCHAR(1024) NOT NULL NAME) NOT NULL");
+    sql("select * from table(dedup(cursor(with cte as (select ename from emp) "
+        + "select * from cte), 'ENAME'))")
+        .type("RecordType(VARCHAR(1024) NOT NULL NAME) NOT NULL");
+  }
+
+  @Test void testDeclareCursorUnexpectedKind() {
+    final SqlValidator validator = fixture().factory.createValidator();
+    validator.pushFunctionCall();
+    final SqlCall query =
+        new SqlBasicCall(SqlStdOperatorTable.EXPLICIT_TABLE,
+        new SqlNodeList(
+            ImmutableList.of(SqlLiteral.createNull(SqlParserPos.ZERO)),
+            SqlParserPos.ZERO),
+        SqlParserPos.ZERO);
+    final SqlValidatorScope scope = validator.getEmptyScope();
+    final AssertionError error =
+        assertThrows(AssertionError.class, () ->
+            ((SqlValidatorImpl) validator).declareCursor(query, scope));
+    assertThat(error.getMessage(), containsString("EXPLICIT_TABLE"));
   }
 
   @Test void testTemporalTable() {
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 90b3c09a24..bd6a2246a1 100644
--- a/core/src/test/resources/org/apache/calcite/test/SqlToRelConverterTest.xml
+++ b/core/src/test/resources/org/apache/calcite/test/SqlToRelConverterTest.xml
@@ -898,6 +898,38 @@ LogicalProject(NAME=[$0])
       LogicalTableScan(table=[[CATALOG, SALES, EMP]])
     LogicalProject(NAME=[$1])
       LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testCollectionTableWithCursorParamUnion">
+    <Resource name="sql">
+      <![CDATA[select * from table(dedup(cursor(select ename from emp union 
all select ename from emp), 'NAME'))]]>
+    </Resource>
+    <Resource name="plan">
+      <![CDATA[
+LogicalProject(NAME=[$0])
+  LogicalTableFunctionScan(invocation=[DEDUP(CAST($0):CURSOR NOT NULL, 
'NAME')], rowType=[RecordType(VARCHAR(1024) NAME)])
+    LogicalUnion(all=[true])
+      LogicalProject(ENAME=[$1])
+        LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+      LogicalProject(ENAME=[$1])
+        LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testCollectionTableWithCursorParamUnionDistinct">
+    <Resource name="sql">
+      <![CDATA[select * from table(dedup(cursor(select ename from emp union 
select ename from emp), 'NAME'))]]>
+    </Resource>
+    <Resource name="plan">
+      <![CDATA[
+LogicalProject(NAME=[$0])
+  LogicalTableFunctionScan(invocation=[DEDUP(CAST($0):CURSOR NOT NULL, 
'NAME')], rowType=[RecordType(VARCHAR(1024) NAME)])
+    LogicalUnion(all=[false])
+      LogicalProject(ENAME=[$1])
+        LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+      LogicalProject(ENAME=[$1])
+        LogicalTableScan(table=[[CATALOG, SALES, EMP]])
 ]]>
     </Resource>
   </TestCase>

Reply via email to