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 e7ec483951 [CALCITE-7602] ROW(*) loses column names
e7ec483951 is described below

commit e7ec483951f25838f5f561e919791714ce2c7ccb
Author: Mihai Budiu <[email protected]>
AuthorDate: Mon Jun 15 15:05:40 2026 -0700

    [CALCITE-7602] ROW(*) loses column names
    
    Signed-off-by: Mihai Budiu <[email protected]>
---
 .../org/apache/calcite/sql/fun/SqlRowOperator.java |  8 +-
 .../calcite/sql/validate/SqlValidatorImpl.java     | 39 +++++++++-
 .../org/apache/calcite/test/SqlValidatorTest.java  | 86 ++++++++++++++++++++++
 core/src/test/resources/sql/struct.iq              | 52 +++++++++++++
 4 files changed, 180 insertions(+), 5 deletions(-)

diff --git a/core/src/main/java/org/apache/calcite/sql/fun/SqlRowOperator.java 
b/core/src/main/java/org/apache/calcite/sql/fun/SqlRowOperator.java
index f73a3bceb7..eaa77f03d5 100644
--- a/core/src/main/java/org/apache/calcite/sql/fun/SqlRowOperator.java
+++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlRowOperator.java
@@ -63,9 +63,15 @@ public SqlRowOperator(String name) {
     this(name, null);
   }
 
+  /** Returns the explicit field-name aliases, or null if none were specified
+   * (in which case names are auto-generated as {@code EXPR$0}, etc.). */
+  public @Nullable List<@Nullable String> getFieldNames() {
+    return fieldNames;
+  }
+
   /** Constructor for a named ROW operator with explicit field-name aliases.
    * Field names may be null, in which case they are auto-generated. */
-  public SqlRowOperator(String name, @Nullable List<@Nullable String> 
fieldNames) {
+  public SqlRowOperator(String name, @Nullable List<? extends @Nullable 
String> fieldNames) {
     super(name,
         SqlKind.ROW, MDX_PRECEDENCE,
         false,
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 e2762d7926..12b1b27335 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
@@ -95,6 +95,7 @@
 import org.apache.calcite.sql.TableCharacteristic;
 import org.apache.calcite.sql.fun.SqlCase;
 import org.apache.calcite.sql.fun.SqlInternalOperators;
+import org.apache.calcite.sql.fun.SqlRowOperator;
 import org.apache.calcite.sql.fun.SqlStdOperatorTable;
 import org.apache.calcite.sql.parser.SqlParserPos;
 import org.apache.calcite.sql.type.AssignableOperandTypeChecker;
@@ -7684,7 +7685,16 @@ private SqlNode expandStarInRow(SqlNode node) {
       }
       final SelectScope selectScope = (SelectScope) scope;
       final List<SqlNode> expandedOperands = new ArrayList<>();
+      final List<@Nullable String> expandedNames = new ArrayList<>();
       boolean expanded = false;
+
+      // Retrieve field names stored in the operator (from ROW(v AS name, ...) 
syntax).
+      final @Nullable List<@Nullable String> origFieldNames =
+          call.getOperator() instanceof SqlRowOperator
+              ? ((SqlRowOperator) call.getOperator()).getFieldNames()
+              : null;
+
+      int origIdx = 0;
       for (SqlNode operand : call.getOperandList()) {
         final SqlIdentifier starId;
         if (operand instanceof SqlStarExclude) {
@@ -7695,6 +7705,7 @@ private SqlNode expandStarInRow(SqlNode node) {
           starId = null;
         }
         if (starId != null) {
+          final int sizeBefore = expandedOperands.size();
           final boolean expandedStar =
               validator.expandStar(expandedOperands,
                   validator.catalogReader.nameMatcher().createSet(),
@@ -7706,16 +7717,36 @@ private SqlNode expandStarInRow(SqlNode node) {
           if (!expandedStar) {
             throw new AssertionError("Row star expansion failed for " + 
starId);
           }
+          // Each newly added operand is a SqlIdentifier; its last name 
component
+          // is the original column name.
+          for (int i = sizeBefore; i < expandedOperands.size(); i++) {
+            expandedNames.add(SqlValidatorUtil.alias(expandedOperands.get(i)));
+          }
           expanded = true;
-          continue;
+        } else {
+          expandedOperands.add(operand);
+          // Prefer the name from the original named-ROW operator; fall back to
+          // whatever alias can be derived from the operand expression itself.
+          final @Nullable String name =
+              origFieldNames != null && origIdx < origFieldNames.size()
+                  && origFieldNames.get(origIdx) != null
+                  ? origFieldNames.get(origIdx)
+                  : SqlValidatorUtil.alias(operand);
+          expandedNames.add(name);
         }
-        expandedOperands.add(operand);
+        origIdx++;
       }
       if (!expanded) {
         return node;
       }
-      return SqlStdOperatorTable.ROW.createCall(
-          call.getParserPosition(), expandedOperands);
+      // Assign unique names to all fields. The first occurrence of a name 
keeps
+      // it as-is; subsequent duplicates get a compiler-generated suffix based 
on
+      // the original column name (e.g. a second DEPTNO becomes DEPTNO0).
+      final boolean caseSensitive = 
validator.catalogReader.nameMatcher().isCaseSensitive();
+      final List<String> uniqueNames =
+          SqlValidatorUtil.uniquify(expandedNames, 
SqlValidatorUtil.EXPR_SUGGESTER, caseSensitive);
+      return new SqlRowOperator("ROW", uniqueNames)
+          .createCall(call.getParserPosition(), expandedOperands);
     }
 
     protected SqlNode expandDynamicStar(SqlIdentifier id, SqlIdentifier fqId) {
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 0fd03a456b..0e9c533dbd 100644
--- a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
+++ b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
@@ -2197,6 +2197,76 @@ void testLikeAndSimilarFails() {
         .fails("ROW\\(\\* EXCLUDE/EXCEPT list\\) cannot exclude all columns");
   }
 
+  /** Test case for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-7602";>[CALCITE-7602]
+   * ROW(*) loses column names</a>. */
+  @Test void testRowWildcardColumnNames() {
+    // ROW(*) should produce a struct with the original column names
+    sql("select row(*) from emp")
+        .columnType(EMP_RECORD_TYPE);
+    // ROW(T.*) should also preserve column names
+    sql("select row(emp.*) from emp")
+        .columnType(EMP_RECORD_TYPE);
+    // ROW(col, T.*) where the star repeats a column already named: the 
duplicate
+    // gets a compiler-assigned suffix (EMPNO0) rather than losing all names
+    sql("select row(empno, emp.*) from emp")
+        .columnType("RecordType(INTEGER NOT NULL EMPNO,"
+            + " INTEGER NOT NULL EMPNO0,"
+            + " VARCHAR(20) NOT NULL ENAME,"
+            + " VARCHAR(10) NOT NULL JOB,"
+            + " INTEGER MGR,"
+            + " TIMESTAMP(0) NOT NULL HIREDATE,"
+            + " INTEGER NOT NULL SAL,"
+            + " INTEGER NOT NULL COMM,"
+            + " INTEGER NOT NULL DEPTNO,"
+            + " BOOLEAN NOT NULL SLACKER) NOT NULL");
+    // ROW(T1.*, T2.*) with a shared column name (DEPTNO): the second DEPTNO
+    // gets the suffix DEPTNO0 while all other names are preserved
+    sql("select row(emp.*, dept.*) from emp join dept on emp.deptno = 
dept.deptno")
+        .columnType("RecordType(INTEGER NOT NULL EMPNO,"
+            + " VARCHAR(20) NOT NULL ENAME,"
+            + " VARCHAR(10) NOT NULL JOB,"
+            + " INTEGER MGR,"
+            + " TIMESTAMP(0) NOT NULL HIREDATE,"
+            + " INTEGER NOT NULL SAL,"
+            + " INTEGER NOT NULL COMM,"
+            + " INTEGER NOT NULL DEPTNO,"
+            + " BOOLEAN NOT NULL SLACKER,"
+            + " INTEGER NOT NULL DEPTNO0,"
+            + " VARCHAR(10) NOT NULL NAME) NOT NULL");
+    // ROW(* EXCLUDE(col)) should preserve names of the remaining columns
+    sql("select row(* exclude(empno)) from emp")
+        .columnType("RecordType(VARCHAR(20) NOT NULL ENAME,"
+            + " VARCHAR(10) NOT NULL JOB,"
+            + " INTEGER MGR,"
+            + " TIMESTAMP(0) NOT NULL HIREDATE,"
+            + " INTEGER NOT NULL SAL,"
+            + " INTEGER NOT NULL COMM,"
+            + " INTEGER NOT NULL DEPTNO,"
+            + " BOOLEAN NOT NULL SLACKER) NOT NULL");
+    // ROW(T.* EXCLUDE(T.col)) should preserve names of the remaining columns
+    sql("select row(emp.* exclude(emp.empno)) from emp")
+        .columnType("RecordType(VARCHAR(20) NOT NULL ENAME,"
+            + " VARCHAR(10) NOT NULL JOB,"
+            + " INTEGER MGR,"
+            + " TIMESTAMP(0) NOT NULL HIREDATE,"
+            + " INTEGER NOT NULL SAL,"
+            + " INTEGER NOT NULL COMM,"
+            + " INTEGER NOT NULL DEPTNO,"
+            + " BOOLEAN NOT NULL SLACKER) NOT NULL");
+    // Named field combined with star expansion: explicit name takes precedence
+    sql("select row(empno as eno, emp.* exclude(emp.empno)) from emp")
+        .columnType("RecordType(INTEGER NOT NULL ENO,"
+            + " VARCHAR(20) NOT NULL ENAME,"
+            + " VARCHAR(10) NOT NULL JOB,"
+            + " INTEGER MGR,"
+            + " TIMESTAMP(0) NOT NULL HIREDATE,"
+            + " INTEGER NOT NULL SAL,"
+            + " INTEGER NOT NULL COMM,"
+            + " INTEGER NOT NULL DEPTNO,"
+            + " BOOLEAN NOT NULL SLACKER) NOT NULL");
+  }
+
   /** Test case for
    * <a 
href="https://issues.apache.org/jira/browse/CALCITE-7603";>[CALCITE-7603]
    * Support ROW constructors that name fields</a>. */
@@ -2226,6 +2296,22 @@ void testLikeAndSimilarFails() {
         .columnType("INTEGER NOT NULL");
     sql("select t.a.\"EXPR$1\" from (select row(1,2) as a from (values (1))) 
as t")
         .columnType("INTEGER NOT NULL");
+    // After star expansion the ROW carries real column names, so field access
+    // by original name works without quoting EXPR$N ordinals.
+    sql("select row(*).empno from emp")
+        .columnType("INTEGER NOT NULL");
+    sql("select row(emp.*).ename from emp")
+        .columnType("VARCHAR(20) NOT NULL");
+    sql("select row(* exclude(empno)).ename from emp")
+        .columnType("VARCHAR(20) NOT NULL");
+    // When two stars produce a duplicate name the second gets a suffix;
+    // both the original and the suffixed name are accessible.
+    sql("select row(emp.*, dept.*).deptno from emp"
+        + " join dept on emp.deptno = dept.deptno")
+        .columnType("INTEGER NOT NULL");
+    sql("select row(emp.*, dept.*).deptno0 from emp"
+        + " join dept on emp.deptno = dept.deptno")
+        .columnType("INTEGER NOT NULL");
   }
 
   @Test void testRowWithInvalidDotOperation() {
diff --git a/core/src/test/resources/sql/struct.iq 
b/core/src/test/resources/sql/struct.iq
index 9f2f393937..bf25d4cadb 100644
--- a/core/src/test/resources/sql/struct.iq
+++ b/core/src/test/resources/sql/struct.iq
@@ -285,4 +285,56 @@ select row(row(1 as x, 2 as y) as inner_row, 'hello' as 
name).inner_row.x;
 
 !ok
 
+# [CALCITE-7602] ROW(*), ROW(T.*) and ROW(T.* EXCLUDE(cols)) preserve original 
column names
+
+# Field access by original column name on ROW(*)
+select row(*).empno from emp order by empno limit 3;
++-------+
+| EMPNO |
++-------+
+|  7369 |
+|  7499 |
+|  7521 |
++-------+
+(3 rows)
+
+!ok
+
+# Field access by original column name on ROW(T.*)
+select row(emp.*).ename from emp order by empno limit 3;
++-------+
+| ENAME |
++-------+
+| SMITH |
+| ALLEN |
+| WARD  |
++-------+
+(3 rows)
+
+!ok
+
+# Field access on ROW(* EXCLUDE(col)) using a preserved name
+select row(* exclude(empno)).ename from emp order by empno limit 3;
++-------+
+| ENAME |
++-------+
+| SMITH |
+| ALLEN |
+| WARD  |
++-------+
+(3 rows)
+
+!ok
+
+# ROW(T1.*, T2.*): shared column DEPTNO is renamed to DEPTNO0 for the second 
table
+select row(emp.*, dept.*).deptno0 from emp join dept on emp.deptno = 
dept.deptno order by emp.empno limit 1;
++---------+
+| DEPTNO0 |
++---------+
+|      20 |
++---------+
+(1 row)
+
+!ok
+
 # End struct.iq

Reply via email to