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 8888eab229 [CALCITE-7603] Support ROW constructors that name fields
8888eab229 is described below

commit 8888eab229873dc2bf48f346e25bc0a2744eda93
Author: Mihai Budiu <[email protected]>
AuthorDate: Fri Jun 12 20:04:46 2026 -0700

    [CALCITE-7603] Support ROW constructors that name fields
    
    Signed-off-by: Mihai Budiu <[email protected]>
---
 core/src/main/codegen/templates/Parser.jj          | 123 ++++++++++++++++++---
 .../org/apache/calcite/sql/fun/SqlRowOperator.java |  89 +++++++++++++--
 .../calcite/sql/parser/CoreSqlParserTest.java      |  46 --------
 .../org/apache/calcite/test/SqlValidatorTest.java  |  22 ++++
 core/src/test/resources/sql/struct.iq              |  46 ++++++++
 site/_docs/reference.md                            |  22 ++--
 .../apache/calcite/sql/parser/SqlParserTest.java   |  69 ++++++++++++
 7 files changed, 332 insertions(+), 85 deletions(-)

diff --git a/core/src/main/codegen/templates/Parser.jj 
b/core/src/main/codegen/templates/Parser.jj
index d4b589d6c1..b340415b95 100644
--- a/core/src/main/codegen/templates/Parser.jj
+++ b/core/src/main/codegen/templates/Parser.jj
@@ -109,6 +109,7 @@ import org.apache.calcite.sql.SqlWithItem;
 import org.apache.calcite.sql.fun.SqlCase;
 import org.apache.calcite.sql.fun.SqlInternalOperators;
 import org.apache.calcite.sql.fun.SqlLibraryOperators;
+import org.apache.calcite.sql.fun.SqlRowOperator;
 import org.apache.calcite.sql.fun.SqlStdOperatorTable;
 import org.apache.calcite.sql.fun.SqlTrimFunction;
 import org.apache.calcite.sql.parser.Span;
@@ -2703,12 +2704,14 @@ void AddRowConstructor(List<SqlNode> list) :
 
 /**
  * Parses a row constructor in the context of a VALUES expression.
+ * Supports optional field-name aliases: {@code ROW(expr AS fieldName, ...)}.
  */
 SqlNode RowConstructor() :
 {
-    final SqlNodeList valueList;
+    SqlNodeList valueList;
     final SqlNode value;
     final Span s;
+    final List<SqlNode> nameList = new ArrayList<SqlNode>();
 }
 {
     // hints are necessary here due to common LPAREN prefixes
@@ -2720,15 +2723,24 @@ SqlNode RowConstructor() :
         <LPAREN> { s = span(); }
         <ROW>
         valueList = 
ParenthesizedQueryOrCommaListWithDefault(ExprContext.ACCEPT_NONCURSOR)
-        <RPAREN> { s.add(this); }
+        <RPAREN>
+        {
+            s.add(this);
+            return buildRowCall(s.end(valueList), valueList, null);
+        }
     |
+        // Standard forms: ROW(e1 [AS n1], e2, ...) or (e1 [AS n1], e2, ...)
         LOOKAHEAD(3)
         (
             <ROW> { s = span(); }
         |
             { s = Span.of(); }
         )
-        valueList = 
ParenthesizedQueryOrCommaListWithDefault(ExprContext.ACCEPT_NONCURSOR)
+        { nameList.clear(); }
+        valueList = RowArgListWithParens(ExprContext.ACCEPT_NONCURSOR, 
nameList)
+        {
+            return buildRowCall(s.end(valueList), valueList, nameList);
+        }
     |
         value = Expression(ExprContext.ACCEPT_NONCURSOR)
         {
@@ -2741,15 +2753,9 @@ SqlNode RowConstructor() :
             s = Span.of(value);
             valueList = new SqlNodeList(ImmutableList.of(value),
                 value.getParserPosition());
+            return buildRowCall(s.end(valueList), valueList, null);
         }
     )
-    {
-        // REVIEW jvs 8-Feb-2004: Should we discriminate between scalar
-        // sub-queries inside of ROW and row sub-queries?  The standard does,
-        // but the distinction seems to be purely syntactic.
-        return SqlStdOperatorTable.ROW.createCall(s.end(valueList),
-            (List<SqlNode>) valueList);
-    }
 }
 
 /** Parses a WHERE clause for SELECT, DELETE, and UPDATE. */
@@ -4173,6 +4179,27 @@ SqlKind comp() :
     }
 }
 
+/**
+ * Builds a ROW call from parsed expressions and optional field-name aliases.
+ * Uses the singleton ROW operator when all names are absent, and a 
per-instance
+ * SqlRowOperator (carrying the names) when any AS alias was written.
+ */
+JAVACODE SqlNode buildRowCall(SqlParserPos pos, SqlNodeList exprList, 
List<SqlNode> nameList) {
+  if (nameList != null) {
+    for (int i = 0; i < nameList.size(); i++) {
+      if (nameList.get(i) != null) {
+        final List<String> fieldNames = new ArrayList<String>();
+        for (int j = 0; j < nameList.size(); j++) {
+          SqlNode n = nameList.get(j);
+          fieldNames.add(n instanceof SqlLiteral ? ((SqlLiteral) 
n).getValueAs(String.class) : null);
+        }
+        return new SqlRowOperator("ROW", fieldNames).createCall(pos, 
(List<SqlNode>) exprList.getList());
+      }
+    }
+  }
+  return SqlStdOperatorTable.ROW.createCall(pos, (List<SqlNode>) 
exprList.getList());
+}
+
 /**
  * Parses a unary row expression, or a parenthesized expression of any
  * kind.
@@ -4184,6 +4211,9 @@ SqlNode Expression3(ExprContext exprContext) :
     final SqlNodeList list1;
     final Span s;
     final Span rowSpan;
+    // Populated by RowArgListWithParens: one entry per expression, either a
+    // SqlLiteral string for an AS-aliased field name, or null if unnamed.
+    final List<SqlNode> rowFieldNames = new ArrayList<SqlNode>();
 }
 {
     LOOKAHEAD(2)
@@ -4200,7 +4230,7 @@ SqlNode Expression3(ExprContext exprContext) :
         s = span();
         pushRowValueStar();
     }
-    list = ParenthesizedQueryOrCommaList(exprContext) {
+    list = RowArgListWithParens(exprContext, rowFieldNames) {
         try {
             if (exprContext != ExprContext.ACCEPT_ALL
                 && exprContext != ExprContext.ACCEPT_CURSOR
@@ -4209,7 +4239,7 @@ SqlNode Expression3(ExprContext exprContext) :
                 throw SqlUtil.newContextException(s.end(list),
                     RESOURCE.illegalRowExpression());
             }
-            return SqlStdOperatorTable.ROW.createCall(list);
+            return buildRowCall(list.getParserPosition(), list, rowFieldNames);
         } finally {
             popRowValueStar();
         }
@@ -4219,12 +4249,11 @@ SqlNode Expression3(ExprContext exprContext) :
         <ROW> { rowSpan = span(); pushRowValueStar(); }
     |   { rowSpan = null; }
     )
-    list1 = ParenthesizedQueryOrCommaList(exprContext) {
+    list1 = RowArgListWithParens(exprContext, rowFieldNames) {
         try {
             if (rowSpan != null) {
                 // interpret as row constructor
-                return SqlStdOperatorTable.ROW.createCall(rowSpan.end(list1),
-                    (List<SqlNode>) list1);
+                return buildRowCall(rowSpan.end(list1), list1, rowFieldNames);
             }
         } finally {
             if (rowSpan != null) {
@@ -4280,10 +4309,68 @@ SqlNode Expression3(ExprContext exprContext) :
             return list1.get(0).clone(list1.getParserPosition());
         } else {
             // interpret as row constructor
-            return SqlStdOperatorTable.ROW.createCall(span().end(list1),
-                (List<SqlNode>) list1);
+            return buildRowCall(span().end(list1), list1, rowFieldNames);
+        }
+    }
+}
+
+/**
+ * Parses a parenthesized comma list for ROW constructors.
+ * Supports optional field-name aliases: {@code (expr AS fieldName, ...)}.
+ * Populates {@code nameList} with field names (or null for unnamed fields).
+ * Returns a SqlNodeList of the value expressions.
+ */
+SqlNodeList RowArgListWithParens(ExprContext exprContext, List<SqlNode> 
nameList) :
+{
+    SqlNode e;
+    SqlIdentifier alias;
+    final List<SqlNode> exprList = new ArrayList<SqlNode>();
+    ExprContext firstExprContext = exprContext;
+    final Span s;
+}
+{
+    <LPAREN>
+    {
+        s = span();
+        switch (exprContext) {
+        case ACCEPT_SUB_QUERY:
+            firstExprContext = ExprContext.ACCEPT_NONCURSOR;
+            break;
+        case ACCEPT_CURSOR:
+            firstExprContext = ExprContext.ACCEPT_ALL;
+            break;
         }
     }
+    (
+        e = OrderedQueryOrExpr(firstExprContext)
+        (
+            <AS> alias = SimpleIdentifier()
+            { exprList.add(e); 
nameList.add(SqlLiteral.createCharString(alias.getSimple(), 
alias.getParserPosition())); }
+        |
+            { exprList.add(e); nameList.add(null); }
+        )
+    |
+        e = Default() { exprList.add(e); nameList.add(null); }
+    )
+    (
+        <COMMA>
+        {
+            checkNonQueryExpression(exprContext);
+        }
+        (
+            e = Expression(exprContext)
+            (
+                <AS> alias = SimpleIdentifier()
+                { exprList.add(e); 
nameList.add(SqlLiteral.createCharString(alias.getSimple(), 
alias.getParserPosition())); }
+            |
+                { exprList.add(e); nameList.add(null); }
+            )
+        |
+            e = Default() { exprList.add(e); nameList.add(null); }
+        )
+    )*
+    <RPAREN>
+    { return new SqlNodeList(exprList, s.end(this)); }
 }
 
 /**
@@ -5407,7 +5494,7 @@ SqlNode PeriodConstructor() :
     <COMMA>
     AddExpression(args, ExprContext.ACCEPT_SUB_QUERY)
     <RPAREN> {
-        return SqlStdOperatorTable.ROW.createCall(s.end(this), args);
+        return buildRowCall(s.end(this), new SqlNodeList(args, s.end(this)), 
null);
     }
 }
 
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 b20d381765..f73a3bceb7 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
@@ -20,29 +20,63 @@
 import org.apache.calcite.rel.type.RelDataTypeFactory;
 import org.apache.calcite.sql.SqlCall;
 import org.apache.calcite.sql.SqlKind;
+import org.apache.calcite.sql.SqlNode;
 import org.apache.calcite.sql.SqlOperatorBinding;
 import org.apache.calcite.sql.SqlSpecialOperator;
 import org.apache.calcite.sql.SqlUtil;
 import org.apache.calcite.sql.SqlWriter;
 import org.apache.calcite.sql.type.InferTypes;
 import org.apache.calcite.sql.type.OperandTypes;
+import org.apache.calcite.sql.validate.SqlValidator;
+import org.apache.calcite.sql.validate.SqlValidatorScope;
+import org.apache.calcite.util.ImmutableNullableList;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+import java.util.List;
 
 /**
- * SqlRowOperator represents the special ROW constructor.
+ * SqlRowOperator represents the special ROW constructor, {@code ROW(v1, v2, 
...)}.
  *
- * <p>TODO: describe usage for row-value construction and row-type construction
- * (SQL supports both).
+ * <p>Fields may be given explicit names using AS aliases:
+ * {@code ROW(v1 AS f1, v2 AS f2, ...)}. When aliases are present, a
+ * per-instance operator (rather than the singleton {@link
+ * org.apache.calcite.sql.fun.SqlStdOperatorTable#ROW}) is used to carry the
+ * field names through type inference. After type inference the resulting
+ * {@link org.apache.calcite.rel.type.RelDataType} carries the names, so
+ * downstream code does not need to inspect the operator.
+ *
+ * <p>When no aliases are given, field names are auto-generated
+ * ({@code EXPR$0}, {@code EXPR$1}, …).
  */
 public class SqlRowOperator extends SqlSpecialOperator {
-  //~ Constructors -----------------------------------------------------------
 
+  /**
+   * Optional explicit field names. When null, field names are auto-generated
+   * ({@code EXPR$0}, {@code EXPR$1}, …). Individual entries may be null to
+   * mix named and unnamed fields.
+   */
+  private final @Nullable List<@Nullable String> fieldNames;
+
+  /** Constructor for the singleton (no field-name aliases). */
   public SqlRowOperator(String name) {
+    this(name, null);
+  }
+
+  /** 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) {
     super(name,
         SqlKind.ROW, MDX_PRECEDENCE,
         false,
         null,
         InferTypes.RETURN_TYPE,
         OperandTypes.VARIADIC);
+    if (fieldNames == null) {
+      this.fieldNames = null;
+    } else {
+      this.fieldNames = ImmutableNullableList.copyOf(fieldNames);
+    }
   }
 
   //~ Methods ----------------------------------------------------------------
@@ -50,13 +84,16 @@ public SqlRowOperator(String name) {
   @Override public RelDataType inferReturnType(
       final SqlOperatorBinding opBinding) {
     // The type of a ROW(e1,e2) expression is a record with the types
-    // {e1type,e2type}.  According to the standard, field names are
-    // implementation-defined.
+    // ROW(e1type,e2type).  Field names come from AS aliases when present;
+    // otherwise they are implementation-defined.
     final RelDataTypeFactory typeFactory = opBinding.getTypeFactory();
     final RelDataTypeFactory.Builder builder = typeFactory.builder();
-    for (int index = 0; index < opBinding.getOperandCount(); index++) {
-      builder.add(SqlUtil.deriveAliasFromOrdinal(index),
-          opBinding.getOperandType(index));
+    for (int i = 0; i < opBinding.getOperandCount(); i++) {
+      final String fieldName =
+          fieldNames != null && fieldNames.get(i) != null
+              ? fieldNames.get(i)
+              : SqlUtil.deriveAliasFromOrdinal(i);
+      builder.add(fieldName, opBinding.getOperandType(i));
     }
     final RelDataType recordType = builder.build();
 
@@ -68,12 +105,44 @@ public SqlRowOperator(String name) {
     return typeFactory.createTypeWithNullability(recordType, nullable);
   }
 
+  @Override public RelDataType deriveType(
+      SqlValidator validator,
+      SqlValidatorScope scope,
+      SqlCall call) {
+    if (fieldNames == null) {
+      return super.deriveType(validator, scope, call);
+    }
+    // For named ROW: validate operand types without replacing this operator
+    // via lookupRoutine (which would substitute the singleton and lose field 
names).
+    for (SqlNode operand : call.getOperandList()) {
+      validator.deriveType(scope, operand);
+    }
+    return validateOperands(validator, scope, call);
+  }
+
   @Override public void unparse(
       SqlWriter writer,
       SqlCall call,
       int leftPrec,
       int rightPrec) {
-    SqlUtil.unparseFunctionSyntax(this, writer, call, false);
+    if (fieldNames == null) {
+      SqlUtil.unparseFunctionSyntax(this, writer, call, false);
+      return;
+    }
+    writer.print("ROW");
+    writer.setNeedWhitespace(false);
+    final SqlWriter.Frame frame =
+        writer.startList(SqlWriter.FrameTypeEnum.FUN_CALL, "(", ")");
+    for (int i = 0; i < call.operandCount(); i++) {
+      writer.sep(",");
+      call.operand(i).unparse(writer, 0, 0);
+      final String name = fieldNames.get(i);
+      if (name != null) {
+        writer.keyword("AS");
+        writer.identifier(name, true);
+      }
+    }
+    writer.endList(frame);
   }
 
   // override SqlOperator
diff --git 
a/core/src/test/java/org/apache/calcite/sql/parser/CoreSqlParserTest.java 
b/core/src/test/java/org/apache/calcite/sql/parser/CoreSqlParserTest.java
index 1c90c9484d..c8ad180009 100644
--- a/core/src/test/java/org/apache/calcite/sql/parser/CoreSqlParserTest.java
+++ b/core/src/test/java/org/apache/calcite/sql/parser/CoreSqlParserTest.java
@@ -16,7 +16,6 @@
  */
 package org.apache.calcite.sql.parser;
 
-import org.apache.calcite.avatica.util.Quoting;
 import org.apache.calcite.test.DiffTestCase;
 
 import com.google.common.collect.ImmutableList;
@@ -71,49 +70,4 @@ public class CoreSqlParserTest extends SqlParserTest {
   private boolean isNotSubclass() {
     return this.getClass().equals(CoreSqlParserTest.class);
   }
-
-  /** Test case for
-   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-7364";>[CALCITE-7364]
-   * Support the syntax ROW(T.* EXCLUDE cols) for creating nested ROW 
values</a>. */
-  @Test void testRowStarExclude() {
-    // Use backticks to ensure that sql(q).same() in general
-    final SqlParserFixture f = fixture().withConfig(c -> 
c.withQuoting(Quoting.BACK_TICK));
-    final String empExcludeEmpno = "SELECT (ROW(`EMP`.* EXCLUDE 
(`EMP`.`EMPNO`)))\n"
-        + "FROM `EMP`";
-
-    // Simple star with one excluded column
-    final String starExcludeEmpno = "SELECT (ROW(* EXCLUDE (`EMPNO`)))\n"
-        + "FROM `EMP`";
-    sql("select row(* exclude(empno)) from emp").ok(starExcludeEmpno);
-    f.sql(starExcludeEmpno).same();
-
-    // Table-qualified star with excluded column
-    sql("select row(emp.* exclude(emp.empno)) from emp").ok(empExcludeEmpno);
-    f.sql(empExcludeEmpno).same();
-
-    // EXCEPT is normalized to EXCLUDE on unparse
-    sql("select row(emp.* except(emp.empno)) from emp").ok(empExcludeEmpno);
-
-    // Multiple excluded columns
-    final String starExcludeEmpnoMgr = "SELECT (ROW(* EXCLUDE (`EMPNO`, 
`MGR`)))\n"
-        + "FROM `EMP`";
-    sql("select row(* exclude(empno, mgr)) from emp").ok(starExcludeEmpnoMgr);
-    f.sql(starExcludeEmpnoMgr).same();
-
-    // Mixed: table-qualified star with exclude, plus plain star
-    final String empExcludeEmpnoDeptStar =
-        "SELECT (ROW(`EMP`.* EXCLUDE (`EMP`.`EMPNO`), `DEPT`.*))\n"
-        + "FROM `EMP`\n"
-        + "INNER JOIN `DEPT` ON (`EMP`.`DEPTNO` = `DEPT`.`DEPTNO`)";
-    sql("select row(emp.* exclude(emp.empno), dept.*)"
-            + " from emp join dept on emp.deptno = dept.deptno")
-        .ok(empExcludeEmpnoDeptStar);
-    f.sql(empExcludeEmpnoDeptStar).same();
-
-    // Nested ROW with EXCLUDE
-    final String nestedStarExcludeEmpno = "SELECT (ROW((ROW(* EXCLUDE 
(`EMPNO`)))))\n"
-        + "FROM `EMP`";
-    sql("select row(row(* exclude(empno))) from 
emp").ok(nestedStarExcludeEmpno);
-    f.sql(nestedStarExcludeEmpno).same();
-  }
 }
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 0f40dfbc6d..f473cf60db 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,28 @@ void testLikeAndSimilarFails() {
         .fails("ROW\\(\\* EXCLUDE/EXCEPT list\\) cannot exclude all columns");
   }
 
+  /** Test case for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-7603";>[CALCITE-7603]
+   * Support ROW constructors that name fields</a>. */
+  @Test void testRowWithFieldNames() {
+    // All fields named: the returned type should use the specified names
+    sql("select row(1 as a, 'hello' as b) from emp")
+        .columnType("RecordType(INTEGER NOT NULL A, CHAR(5) NOT NULL B) NOT 
NULL");
+    // Mixed: named and unnamed fields
+    sql("select row(empno as eno, ename) from emp")
+        .columnType("RecordType(INTEGER NOT NULL ENO, VARCHAR(20) NOT NULL 
EXPR$1) NOT NULL");
+    // No names: existing auto-generated behavior unchanged
+    sql("select row(empno, ename) from emp")
+        .columnType("RecordType(INTEGER NOT NULL EXPR$0, VARCHAR(20) NOT NULL 
EXPR$1) NOT NULL");
+    // Access a named field by name
+    sql("select row(empno as eno, ename as en).eno from emp")
+        .columnType("INTEGER NOT NULL");
+    // Nested ROW with named fields
+    sql("select row(row(1 as x, 2 as y) as inner_row) from emp")
+        .columnType("RecordType(RecordType(INTEGER NOT NULL X, INTEGER NOT 
NULL Y)"
+            + " NOT NULL INNER_ROW) NOT NULL");
+  }
+
   @Test void testRowWithValidDot() {
     sql("select ((1,2),(3,4,5)).\"EXPR$1\".\"EXPR$2\"\n from dept")
         .columnType("INTEGER NOT NULL");
diff --git a/core/src/test/resources/sql/struct.iq 
b/core/src/test/resources/sql/struct.iq
index 1d894d254e..198ec492ae 100644
--- a/core/src/test/resources/sql/struct.iq
+++ b/core/src/test/resources/sql/struct.iq
@@ -238,4 +238,50 @@ select row(emp.* exclude(emp.empno), dept.* 
exclude(dept.deptno)) from emp join
 
 !ok
 
+# [CALCITE-7603] Support ROW constructors that name fields
+select row(1 as a, 'hello' as b);
++------------+
+| EXPR$0     |
++------------+
+| {1, hello} |
++------------+
+(1 row)
+
+!ok
+
+# Named field access: .field selects a field from a named-field ROW
+select row(1 as a, 'hello' as b).a;
++--------+
+| EXPR$0 |
++--------+
+|      1 |
++--------+
+(1 row)
+
+!ok
+
+# Named field access on a column expression
+select row(empno as eno, ename as en).eno from emp order by empno limit 3;
++--------+
+| EXPR$0 |
++--------+
+|   7369 |
+|   7499 |
+|   7521 |
++--------+
+(3 rows)
+
+!ok
+
+# Nested named-field ROW, and access to inner field
+select row(row(1 as x, 2 as y) as inner_row, 'hello' as name).inner_row.x;
++--------+
+| EXPR$0 |
++--------+
+|      1 |
++--------+
+(1 row)
+
+!ok
+
 # End struct.iq
diff --git a/site/_docs/reference.md b/site/_docs/reference.md
index 026fa59c47..e546757d0b 100644
--- a/site/_docs/reference.md
+++ b/site/_docs/reference.md
@@ -1797,17 +1797,17 @@ ##### Strategies for Finding Common Type
 
 ### Value constructors
 
-| Operator syntax | Description
-|:--------------- |:-----------
-| ROW (value [, value ]*)  | Creates a row from a list of values.
-| ROW (rowStarItem [, rowStarItem ]*)  | Creates a row from all columns, or 
all columns except those excluded, of one or more tables.
-| (value [, value ]* )     | Creates a row from a list of values.
-| row '[' index ']'        | Returns the element at a particular location in a 
row (1-based index).
-| row '[' name ']'         | Returns the element of a row with a particular 
name.
-| map '[' key ']'     | Returns the element of a map with a particular key.
-| array '[' index ']' | Returns the element at a particular location in an 
array (1-based index).
-| ARRAY '[' value [, value ]* ']' | Creates an array from a list of values.
-| MAP '[' key, value [, key, value ]* ']' | Creates a map from a list of 
key-value pairs.
+| Operator syntax                            | Description
+|:-------------------------------------------|:-----------
+| ROW (value [AS name] [, value [AS name]]*) | Creates a row from a list of 
values.
+| ROW (rowStarItem [, rowStarItem ]*)        | Creates a row from all columns, 
or all columns except those excluded, of one or more tables.
+| (value [, value ]* )                       | Creates a row from a list of 
values.
+| row '[' index ']'                          | Returns the element at a 
particular location in a row (1-based index).
+| row '[' name ']'                           | Returns the element of a row 
with a particular name.
+| map '[' key ']'                            | Returns the element of a map 
with a particular key.
+| array '[' index ']'                        | Returns the element at a 
particular location in an array (1-based index).
+| ARRAY '[' value [, value ]* ']'            | Creates an array from a list of 
values.
+| MAP '[' key, value [, key, value ]* ']'    | Creates a map from a list of 
key-value pairs.
 
 ### Value constructors by query
 
diff --git 
a/testkit/src/main/java/org/apache/calcite/sql/parser/SqlParserTest.java 
b/testkit/src/main/java/org/apache/calcite/sql/parser/SqlParserTest.java
index 4ab6f776ba..a9e3c6b959 100644
--- a/testkit/src/main/java/org/apache/calcite/sql/parser/SqlParserTest.java
+++ b/testkit/src/main/java/org/apache/calcite/sql/parser/SqlParserTest.java
@@ -10242,4 +10242,73 @@ public void checkExpFails(String sql, String expected) 
{
           .fails(expected.replace("$op", op));
     }
   }
+
+  /** Test case for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-7603";>[CALCITE-7603]
+   * Support ROW constructors that name fields</a>. */
+  @Test void testRowWithFieldNames() {
+    // All fields named
+    sql("select row(1 as a, 'hello' as b) from emp")
+        .ok("SELECT (ROW(1 AS `A`, 'hello' AS `B`))\nFROM `EMP`");
+    // Mixed: some fields named, some not
+    sql("select row(1 as a, 2) from emp")
+        .ok("SELECT (ROW(1 AS `A`, 2))\nFROM `EMP`");
+    // No field names (existing behavior unchanged)
+    sql("select row(1, 2) from emp")
+        .ok("SELECT (ROW(1, 2))\nFROM `EMP`");
+    // Expression with AS
+    sql("select row(empno + 1 as eno, ename as en) from emp")
+        .ok("SELECT (ROW((`EMPNO` + 1) AS `ENO`, `ENAME` AS `EN`))\nFROM 
`EMP`");
+    // Round-trip: the canonical form can be re-parsed
+    final SqlParserFixture f = fixture().withConfig(c -> 
c.withQuoting(Quoting.BACK_TICK));
+    f.sql("SELECT (ROW(1 AS `A`, 2))\nFROM `EMP`").same();
+    // AS is not optional in ROW constructors
+    sql("select row(1 ^a^, 'hello' b) from emp")
+        .fails("(?s)Encountered \"a\" at line 1, column 14.*");
+  }
+
+  /** Test case for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-7364";>[CALCITE-7364]
+   * Support the syntax ROW(T.* EXCLUDE cols) for creating nested ROW 
values</a>. */
+  @Test void testRowStarExclude() {
+    // Use backticks to ensure that sql(q).same() in general
+    final SqlParserFixture f = fixture().withConfig(c -> 
c.withQuoting(Quoting.BACK_TICK));
+    final String empExcludeEmpno = "SELECT (ROW(`EMP`.* EXCLUDE 
(`EMP`.`EMPNO`)))\n"
+        + "FROM `EMP`";
+
+    // Simple star with one excluded column
+    final String starExcludeEmpno = "SELECT (ROW(* EXCLUDE (`EMPNO`)))\n"
+        + "FROM `EMP`";
+    sql("select row(* exclude(empno)) from emp").ok(starExcludeEmpno);
+    f.sql(starExcludeEmpno).same();
+
+    // Table-qualified star with excluded column
+    sql("select row(emp.* exclude(emp.empno)) from emp").ok(empExcludeEmpno);
+    f.sql(empExcludeEmpno).same();
+
+    // EXCEPT is normalized to EXCLUDE on unparse
+    sql("select row(emp.* except(emp.empno)) from emp").ok(empExcludeEmpno);
+
+    // Multiple excluded columns
+    final String starExcludeEmpnoMgr = "SELECT (ROW(* EXCLUDE (`EMPNO`, 
`MGR`)))\n"
+        + "FROM `EMP`";
+    sql("select row(* exclude(empno, mgr)) from emp").ok(starExcludeEmpnoMgr);
+    f.sql(starExcludeEmpnoMgr).same();
+
+    // Mixed: table-qualified star with exclude, plus plain star
+    final String empExcludeEmpnoDeptStar =
+        "SELECT (ROW(`EMP`.* EXCLUDE (`EMP`.`EMPNO`), `DEPT`.*))\n"
+            + "FROM `EMP`\n"
+            + "INNER JOIN `DEPT` ON (`EMP`.`DEPTNO` = `DEPT`.`DEPTNO`)";
+    sql("select row(emp.* exclude(emp.empno), dept.*)"
+        + " from emp join dept on emp.deptno = dept.deptno")
+        .ok(empExcludeEmpnoDeptStar);
+    f.sql(empExcludeEmpnoDeptStar).same();
+
+    // Nested ROW with EXCLUDE
+    final String nestedStarExcludeEmpno = "SELECT (ROW((ROW(* EXCLUDE 
(`EMPNO`)))))\n"
+        + "FROM `EMP`";
+    sql("select row(row(* exclude(empno))) from 
emp").ok(nestedStarExcludeEmpno);
+    f.sql(nestedStarExcludeEmpno).same();
+  }
 }

Reply via email to