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();
+ }
}