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 1a3173d52a [CALCITE-7364] Support the syntax ROW(T.* EXCLUDE cols) for
creating nested ROW values
1a3173d52a is described below
commit 1a3173d52a41683acca60fb09a31ad6ee25e587e
Author: Mihai Budiu <[email protected]>
AuthorDate: Wed Jun 10 16:44:19 2026 -0700
[CALCITE-7364] Support the syntax ROW(T.* EXCLUDE cols) for creating nested
ROW values
Signed-off-by: Mihai Budiu <[email protected]>
---
babel/src/main/codegen/config.fmpp | 1 -
core/src/main/codegen/default_config.fmpp | 1 -
core/src/main/codegen/templates/Parser.jj | 86 ++++++++++++++--------
.../apache/calcite/runtime/CalciteResource.java | 6 ++
.../org/apache/calcite/sql/SqlStarExclude.java | 5 +-
.../calcite/sql/validate/SqlValidatorImpl.java | 75 +++++++++++--------
.../calcite/runtime/CalciteResource.properties | 2 +
.../calcite/sql/parser/CoreSqlParserTest.java | 46 ++++++++++++
.../org/apache/calcite/test/SqlValidatorTest.java | 39 ++++++++++
core/src/test/resources/sql/struct.iq | 51 +++++++++++++
site/_docs/reference.md | 10 ++-
11 files changed, 256 insertions(+), 66 deletions(-)
diff --git a/babel/src/main/codegen/config.fmpp
b/babel/src/main/codegen/config.fmpp
index 001bdf2e10..30c2ce7d65 100644
--- a/babel/src/main/codegen/config.fmpp
+++ b/babel/src/main/codegen/config.fmpp
@@ -617,7 +617,6 @@ data: {
includePosixOperators: true
includeParsingStringLiteralAsArrayLiteral: true
includeIntervalWithoutQualifier: true
- includeStarExclude: true
includeSelectBy: true
}
}
diff --git a/core/src/main/codegen/default_config.fmpp
b/core/src/main/codegen/default_config.fmpp
index 56d17b8279..a2547273cb 100644
--- a/core/src/main/codegen/default_config.fmpp
+++ b/core/src/main/codegen/default_config.fmpp
@@ -460,5 +460,4 @@ parser: {
includeAdditionalDeclarations: false
includeParsingStringLiteralAsArrayLiteral: false
includeIntervalWithoutQualifier: false
- includeStarExclude: false
}
diff --git a/core/src/main/codegen/templates/Parser.jj
b/core/src/main/codegen/templates/Parser.jj
index 0aacdb9ff7..d4b589d6c1 100644
--- a/core/src/main/codegen/templates/Parser.jj
+++ b/core/src/main/codegen/templates/Parser.jj
@@ -2026,7 +2026,6 @@ void AddSelectItem(List<SqlNode> list) :
)
}
-<#if (parser.includeStarExclude!default.parser.includeStarExclude)>
/**
* Parses one unaliased expression in a select list.
*/
@@ -2035,6 +2034,8 @@ SqlNode SelectExpression() :
SqlNode e;
SqlNodeList excludeList;
SqlNodeList replaceList;
+ SqlIdentifier sqlIdentifier;
+ SqlParserPos pos;
}
{
(
@@ -2045,47 +2046,42 @@ SqlNode SelectExpression() :
e = Expression(ExprContext.ACCEPT_SUB_QUERY)
)
(
-<#if (parser.includeStarExclude!default.parser.includeStarExclude)>
excludeList = StarExcludeList() {
if (!(e instanceof SqlIdentifier)) {
throw
SqlUtil.newContextException(excludeList.getParserPosition(),
RESOURCE.selectExcludeRequiresStar());
}
- final SqlIdentifier sqlIdentifier = (SqlIdentifier) e;
+ sqlIdentifier = (SqlIdentifier) e;
if (!sqlIdentifier.isStar()) {
throw
SqlUtil.newContextException(excludeList.getParserPosition(),
RESOURCE.selectExcludeRequiresStar());
}
- final SqlParserPos pos = SqlParserPos.sum(
+ pos = SqlParserPos.sum(
ImmutableList.of(sqlIdentifier.getParserPosition(),
excludeList.getParserPosition()));
return new SqlStarExclude(pos, sqlIdentifier, excludeList);
}
|
-</#if>
-<#if (parser.includeStarExclude!default.parser.includeStarExclude)>
replaceList = StarReplaceList() {
if (!(e instanceof SqlIdentifier)) {
throw
SqlUtil.newContextException(replaceList.getParserPosition(),
RESOURCE.selectReplaceRequiresStar());
}
- final SqlIdentifier sqlIdentifier = (SqlIdentifier) e;
+ sqlIdentifier = (SqlIdentifier) e;
if (!sqlIdentifier.isStar()) {
throw
SqlUtil.newContextException(replaceList.getParserPosition(),
RESOURCE.selectReplaceRequiresStar());
}
- final SqlParserPos pos = SqlParserPos.sum(
+ pos = SqlParserPos.sum(
ImmutableList.of(sqlIdentifier.getParserPosition(),
replaceList.getParserPosition()));
return new SqlStarReplace(pos, sqlIdentifier, replaceList);
}
|
-</#if>
{ return e; }
)
}
-<#if (parser.includeStarExclude!default.parser.includeStarExclude)>
SqlNodeList StarExcludeList() :
{
final Span s;
@@ -2106,9 +2102,7 @@ SqlNodeList StarExcludeList() :
return new SqlNodeList(list, s.end(this));
}
}
-</#if>
-<#if (parser.includeStarExclude!default.parser.includeStarExclude)>
SqlNodeList StarReplaceList() :
{
final Span s;
@@ -2134,25 +2128,6 @@ SqlNodeList StarReplaceList() :
return new SqlNodeList(list, s.end(this));
}
}
-</#if>
-<#else>
-/**
- * Parses one unaliased expression in a select list.
- */
-SqlNode SelectExpression() :
-{
- SqlNode e;
-}
-{
- <STAR> {
- return SqlIdentifier.star(getPos());
- }
-|
- e = Expression(ExprContext.ACCEPT_SUB_QUERY) {
- return e;
- }
-}
-</#if>
SqlLiteral Natural() :
{
@@ -4548,12 +4523,42 @@ SqlCall PercentileFunctionCall() :
}
+/**
+ * Parses an EXCLUDE or EXCEPT clause following a star inside a ROW
constructor,
+ * e.g. {@code ROW(* EXCLUDE(col1, col2))} or {@code ROW(t.* EXCEPT(t.col))}.
+ *
+ * @param starIdentifier the star (e.g. "*" or "t.*") that precedes the
EXCLUDE clause
+ */
+SqlNode RowStarExclude(SqlIdentifier starIdentifier) :
+{
+ SqlIdentifier id; // current column identifier being parsed
+ final List<SqlNode> list = new ArrayList<SqlNode>();
+ Span s;
+}
+{
+ (<EXCLUDE> | <EXCEPT>)
+ <LPAREN> { s = span(); }
+ id = CompoundIdentifier() { list.add(id); }
+ (
+ <COMMA> id = CompoundIdentifier() { list.add(id); }
+ )*
+ <RPAREN> {
+ final SqlNodeList excludeList = new SqlNodeList(list, s.end(this));
+ return new SqlStarExclude(
+ SqlParserPos.sum(ImmutableList.of(
+ starIdentifier.getParserPosition(),
+ excludeList.getParserPosition())),
+ starIdentifier, excludeList);
+ }
+}
+
/**
* Parses an atomic row expression.
*/
SqlNode AtomicRowExpression() :
{
final SqlNode e;
+ SqlNode rowStar;
}
{
(
@@ -4584,10 +4589,27 @@ SqlNode AtomicRowExpression() :
|
e = ContextVariable()
|
+ // Parses "t.*" or "t.* EXCLUDE (col, ...)" inside a ROW constructor
e = CompoundIdentifier()
+ (
+ LOOKAHEAD({ allowRowValueStar()
+ && (getToken(1).kind == EXCLUDE || getToken(1).kind == EXCEPT)
})
+ rowStar = RowStarExclude((SqlIdentifier) e) { return rowStar; }
+ |
+ {}
+ )
|
+ // Parses "*" or "* EXCLUDE (col, ...)" inside a ROW constructor
LOOKAHEAD({ allowRowValueStar() })
- <STAR> { return SqlIdentifier.star(getPos()); }
+ <STAR> {
+ final SqlIdentifier starId = SqlIdentifier.star(getPos());
+ }
+ (
+ LOOKAHEAD((<EXCLUDE> | <EXCEPT>))
+ rowStar = RowStarExclude(starId) { return rowStar; }
+ |
+ { return starId; }
+ )
|
e = NewSpecification()
|
diff --git a/core/src/main/java/org/apache/calcite/runtime/CalciteResource.java
b/core/src/main/java/org/apache/calcite/runtime/CalciteResource.java
index a9cb02d065..ea60315ae6 100644
--- a/core/src/main/java/org/apache/calcite/runtime/CalciteResource.java
+++ b/core/src/main/java/org/apache/calcite/runtime/CalciteResource.java
@@ -816,9 +816,15 @@ ExInst<CalciteException>
illegalArgumentForTableFunctionCall(String a0,
@BaseMessage("SELECT * EXCLUDE/EXCEPT list contains unknown column(s): {0}")
ExInst<SqlValidatorException>
selectStarExcludeListContainsUnknownColumns(String columns);
+ @BaseMessage("ROW(* EXCLUDE/EXCEPT list) contains unknown column(s): {0}")
+ ExInst<SqlValidatorException>
rowStarExcludeListContainsUnknownColumns(String columns);
+
@BaseMessage("SELECT * EXCLUDE/EXCEPT list cannot exclude all columns")
ExInst<SqlValidatorException> selectStarExcludeCannotExcludeAllColumns();
+ @BaseMessage("ROW(* EXCLUDE/EXCEPT list) cannot exclude all columns")
+ ExInst<SqlValidatorException> rowStarExcludeCannotExcludeAllColumns();
+
@BaseMessage("SELECT * REPLACE list contains unknown column(s): {0}")
ExInst<SqlValidatorException>
selectStarReplaceListContainsUnknownColumns(String columns);
diff --git a/core/src/main/java/org/apache/calcite/sql/SqlStarExclude.java
b/core/src/main/java/org/apache/calcite/sql/SqlStarExclude.java
index 884c4a8463..fc1d7f2988 100644
--- a/core/src/main/java/org/apache/calcite/sql/SqlStarExclude.java
+++ b/core/src/main/java/org/apache/calcite/sql/SqlStarExclude.java
@@ -27,11 +27,12 @@
import static java.util.Objects.requireNonNull;
/**
- * Represents {@code SELECT * EXCLUDE(...)}.
+ * Represents the arguments of {@code SELECT * EXCLUDE(...) or SELECT ROW(*
EXCLUDE(...))},
+ * without the SELECT itself.
*/
public class SqlStarExclude extends SqlCall {
public static final SqlOperator OPERATOR =
- new SqlSpecialOperator("SELECT_STAR_EXCLUDE", SqlKind.OTHER) {
+ new SqlSpecialOperator("STAR_EXCLUDE", SqlKind.OTHER) {
@SuppressWarnings("argument.type.incompatible")
@Override public SqlCall createCall(
@Nullable SqlLiteral functionQualifier,
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 f7388bdeb3..e2762d7926 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
@@ -481,7 +481,7 @@ private boolean expandSelectItem(final SqlNode selectItem,
SqlSelect select,
} else {
final SelectScope scope = (SelectScope) getWhereScope(select);
if (expandStar(selectItems, aliases, fields, includeSystemVars, scope,
- selectItem)) {
+ selectItem, false)) {
return true;
}
@@ -670,7 +670,7 @@ private void validateNoQualifiedCommonColumns(SqlNodeList
nodeList,
private boolean expandStar(List<SqlNode> selectItems, Set<String> aliases,
PairList<String, RelDataType> fields, boolean includeSystemVars,
- SelectScope scope, SqlNode node) {
+ SelectScope scope, SqlNode node, boolean inRowContext) {
final SqlIdentifier identifier;
final SqlNodeList excludeList;
final SqlNodeList replaceList;
@@ -831,9 +831,9 @@ private boolean expandStar(List<SqlNode> selectItems,
Set<String> aliases,
int offset = Math.min(calculatePermuteOffset(selectItems),
originalSize);
new Permute(from, offset).permute(selectItems, fields);
}
- throwIfUnknownExcludeColumns(excludeIdentifiers, excludeMatched);
+ throwIfUnknownExcludeColumns(excludeIdentifiers, excludeMatched,
inRowContext);
throwIfExcludeEliminatesAllColumns(excludeIdentifiers, fieldsBeforeStar,
- fields, identifier);
+ fields, identifier, inRowContext);
throwIfUnknownReplaceColumns(replaceMap, replaceMatched);
return true;
@@ -909,9 +909,9 @@ private boolean expandStar(List<SqlNode> selectItems,
Set<String> aliases,
} else {
throw newValidationError(prefixId, RESOURCE.starRequiresRecordType());
}
- throwIfUnknownExcludeColumns(excludeIdentifiers, excludeMatched);
+ throwIfUnknownExcludeColumns(excludeIdentifiers, excludeMatched,
inRowContext);
throwIfExcludeEliminatesAllColumns(excludeIdentifiers, fieldsBeforeStar,
- fields, identifier);
+ fields, identifier, inRowContext);
throwIfUnknownReplaceColumns(replaceMap, replaceMatched);
return true;
}
@@ -987,7 +987,7 @@ && matchesExcludeIdentifier(columnId,
excludeIdentifiers.get(i), nameMatcher)) {
}
private void throwIfUnknownExcludeColumns(List<SqlIdentifier>
excludeIdentifiers,
- boolean[] excludeMatched) {
+ boolean[] excludeMatched, boolean inRowContext) {
if (excludeIdentifiers.isEmpty()) {
return;
}
@@ -1002,20 +1002,24 @@ private void
throwIfUnknownExcludeColumns(List<SqlIdentifier> excludeIdentifiers
}
}
if (firstUnknownIndex >= 0) {
+ final String columns = String.join(", ", unknownExcludeNames);
throw newValidationError(
excludeIdentifiers.get(firstUnknownIndex),
- RESOURCE.selectStarExcludeListContainsUnknownColumns(
- String.join(", ", unknownExcludeNames)));
+ inRowContext
+ ? RESOURCE.rowStarExcludeListContainsUnknownColumns(columns)
+ : RESOURCE.selectStarExcludeListContainsUnknownColumns(columns));
}
}
private void throwIfExcludeEliminatesAllColumns(List<SqlIdentifier>
excludeIdentifiers,
int fieldsBeforeStar, PairList<String, RelDataType> fields,
- SqlIdentifier identifier) {
+ SqlIdentifier identifier, boolean inRowContext) {
if (!excludeIdentifiers.isEmpty()
&& fields.size() == fieldsBeforeStar) {
throw newValidationError(identifier,
- RESOURCE.selectStarExcludeCannotExcludeAllColumns());
+ inRowContext
+ ? RESOURCE.rowStarExcludeCannotExcludeAllColumns()
+ : RESOURCE.selectStarExcludeCannotExcludeAllColumns());
}
}
@@ -1113,7 +1117,8 @@ private boolean addOrExpandField(List<SqlNode>
selectItems, Set<String> aliases,
fields,
includeSystemVars,
scope,
- starExp);
+ starExp,
+ false);
return true;
default:
addToSelectList(
@@ -7633,6 +7638,11 @@ public static boolean isAmbiguousException(Exception ex)
{
default:
break;
}
+ // SqlStarExclude is expanded by expandStarInRow at the ROW level;
+ // its exclude identifiers must not be resolved as regular column
references.
+ if (call instanceof SqlStarExclude) {
+ return call;
+ }
// Only visits arguments which are expressions. We don't want to
// qualify non-expressions such as 'x' in 'empno * 5 AS x'.
CallCopyingArgHandler argHandler =
@@ -7663,8 +7673,9 @@ private SqlNode expandStarInRow(SqlNode node) {
if (!(scope instanceof SelectScope)) {
// Check if any operand is a star identifier before throwing error
for (SqlNode operand : call.getOperandList()) {
- if (operand instanceof SqlIdentifier
- && ((SqlIdentifier) operand).isStar()) {
+ if (operand instanceof SqlStarExclude
+ || (operand instanceof SqlIdentifier
+ && ((SqlIdentifier) operand).isStar())) {
throw validator.newValidationError(node,
RESOURCE.rowStarNotAllowed());
}
@@ -7675,22 +7686,28 @@ private SqlNode expandStarInRow(SqlNode node) {
final List<SqlNode> expandedOperands = new ArrayList<>();
boolean expanded = false;
for (SqlNode operand : call.getOperandList()) {
- if (operand instanceof SqlIdentifier) {
- final SqlIdentifier identifier = (SqlIdentifier) operand;
- if (identifier.isStar()) {
- final boolean expandedStar =
- validator.expandStar(expandedOperands,
- validator.catalogReader.nameMatcher().createSet(),
- PairList.of(),
- false,
- selectScope,
- identifier);
- if (!expandedStar) {
- throw new AssertionError("Row star expansion failed for " +
identifier);
- }
- expanded = true;
- continue;
+ final SqlIdentifier starId;
+ if (operand instanceof SqlStarExclude) {
+ starId = ((SqlStarExclude) operand).getStarIdentifier();
+ } else if (operand instanceof SqlIdentifier && ((SqlIdentifier)
operand).isStar()) {
+ starId = (SqlIdentifier) operand;
+ } else {
+ starId = null;
+ }
+ if (starId != null) {
+ final boolean expandedStar =
+ validator.expandStar(expandedOperands,
+ validator.catalogReader.nameMatcher().createSet(),
+ PairList.of(),
+ false,
+ selectScope,
+ operand,
+ true);
+ if (!expandedStar) {
+ throw new AssertionError("Row star expansion failed for " +
starId);
}
+ expanded = true;
+ continue;
}
expandedOperands.add(operand);
}
diff --git
a/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties
b/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties
index 056aeb7b07..8906e45121 100644
---
a/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties
+++
b/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties
@@ -269,9 +269,11 @@ SelectStarRequiresFrom=SELECT * requires a FROM clause
SelectExcludeRequiresStar=EXCLUDE/EXCEPT clause must follow a STAR expression
SelectReplaceRequiresStar=REPLACE clause must follow a STAR expression
SelectStarExcludeListContainsUnknownColumns=SELECT * EXCLUDE/EXCEPT list
contains unknown column(s): {0}
+RowStarExcludeListContainsUnknownColumns=ROW(* EXCLUDE/EXCEPT list) contains
unknown column(s): {0}
SelectStarReplaceListContainsUnknownColumns=SELECT * REPLACE list contains
unknown column(s): {0}
SelectStarReplaceListContainsDuplicateColumns=SELECT * REPLACE list contains
duplicate column(s): {0}
SelectStarExcludeCannotExcludeAllColumns=SELECT * EXCLUDE/EXCEPT list cannot
exclude all columns
+RowStarExcludeCannotExcludeAllColumns=ROW(* EXCLUDE/EXCEPT list) cannot
exclude all columns
GroupFunctionMustAppearInGroupByClause=Group function ''{0}'' can only appear
in GROUP BY clause
AuxiliaryWithoutMatchingGroupCall=Call to auxiliary group function ''{0}''
must have matching call to group function ''{1}'' in GROUP BY clause
PivotAggMalformed=Measure expression in PIVOT must use aggregate function
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 c8ad180009..1c90c9484d 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,6 +16,7 @@
*/
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;
@@ -70,4 +71,49 @@ 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 c60c2917ed..0f40dfbc6d 100644
--- a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
+++ b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
@@ -2156,6 +2156,45 @@ void testLikeAndSimilarFails() {
sql("select row(*) from emp").ok();
sql("select row(emp.*) from emp").ok();
sql("select row(emp.*, dept.*) from emp join dept on emp.deptno =
dept.deptno").ok();
+ // Nested ROW with star
+ sql("select row(row(*)) from emp").ok();
+ sql("select row(row(emp.*)) from emp").ok();
+ }
+
+ /** 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 testRowWildcardExclude() {
+ sql("select row(* exclude(empno)) from emp").ok();
+ sql("select row(* exclude(empno, deptno)) from emp").ok();
+ sql("select row(emp.* exclude(emp.empno)) from emp").ok();
+ // EXCEPT is a synonym for EXCLUDE
+ sql("select row(emp.* except(emp.empno)) from emp").ok();
+ sql("select row(emp.* exclude(emp.empno), dept.*)"
+ + " from emp join dept on emp.deptno = dept.deptno").ok();
+ // Nested ROW with EXCLUDE
+ sql("select row(row(* exclude(empno))) from emp").ok();
+ sql("select row(row(emp.* exclude(emp.empno))) from emp").ok();
+ // Multiple nested ROWs, one with EXCLUDE
+ sql("select row(row(* exclude(empno)), row(dept.*)) "
+ + "from emp join dept on emp.deptno = dept.deptno").ok();
+ // Unknown column in exclude list
+ sql("select row(* exclude(^foo^)) from emp")
+ .fails("ROW\\(\\* EXCLUDE/EXCEPT list\\) contains unknown
column\\(s\\): FOO");
+ // Unknown column in nested ROW exclude list
+ sql("select row(row(* exclude(^foo^))) from emp")
+ .fails("ROW\\(\\* EXCLUDE/EXCEPT list\\) contains unknown
column\\(s\\): FOO");
+ // Unknown column in table-qualified exclude list
+ sql("select row(emp.* exclude(^emp.foo^)) from emp")
+ .fails("ROW\\(\\* EXCLUDE/EXCEPT list\\) contains unknown
column\\(s\\): EMP\\.FOO");
+ // Excluding all columns from a ROW expression is not allowed
+ sql("select row(^*^ exclude(empno, ename, job, mgr, hiredate, sal, comm,"
+ + " deptno, slacker)) from emp")
+ .fails("ROW\\(\\* EXCLUDE/EXCEPT list\\) cannot exclude all columns");
+ // Excluding all columns via qualified name is not allowed
+ sql("select row(^emp.*^ exclude(emp.empno, emp.ename, emp.job, emp.mgr,"
+ + " emp.hiredate, emp.sal, emp.comm, emp.deptno, emp.slacker)) from
emp")
+ .fails("ROW\\(\\* EXCLUDE/EXCEPT list\\) cannot exclude all columns");
}
@Test void testRowWithValidDot() {
diff --git a/core/src/test/resources/sql/struct.iq
b/core/src/test/resources/sql/struct.iq
index 61c8921573..1d894d254e 100644
--- a/core/src/test/resources/sql/struct.iq
+++ b/core/src/test/resources/sql/struct.iq
@@ -187,4 +187,55 @@ select row(d.*, row(d.*)) from dept d limit 1;
!ok
+# [CALCITE-7364] Support the syntax ROW(T.* EXCLUDE cols) for creating nested
ROW values
+select row(* exclude(empno)) from emp order by empno limit 1;
++----------------------------------------------------+
+| EXPR$0 |
++----------------------------------------------------+
+| {SMITH, CLERK, 7902, 1980-12-17, 800.00, null, 20} |
++----------------------------------------------------+
+(1 row)
+
+!ok
+
+select row(emp.* exclude(emp.empno)) from emp order by empno limit 1;
++----------------------------------------------------+
+| EXPR$0 |
++----------------------------------------------------+
+| {SMITH, CLERK, 7902, 1980-12-17, 800.00, null, 20} |
++----------------------------------------------------+
+(1 row)
+
+!ok
+
+select row(* exclude(empno, mgr)) from emp order by empno limit 1;
++----------------------------------------------+
+| EXPR$0 |
++----------------------------------------------+
+| {SMITH, CLERK, 1980-12-17, 800.00, null, 20} |
++----------------------------------------------+
+(1 row)
+
+!ok
+
+select row(emp.* exclude(emp.empno), dept.*) from emp join dept on emp.deptno
= dept.deptno order by emp.empno limit 1;
++--------------------------------------------------------------------------+
+| EXPR$0 |
++--------------------------------------------------------------------------+
+| {SMITH, CLERK, 7902, 1980-12-17, 800.00, null, 20, 20, RESEARCH, DALLAS} |
++--------------------------------------------------------------------------+
+(1 row)
+
+!ok
+
+select row(emp.* exclude(emp.empno), dept.* exclude(dept.deptno)) from emp
join dept on emp.deptno = dept.deptno order by emp.empno limit 1;
++----------------------------------------------------------------------+
+| EXPR$0 |
++----------------------------------------------------------------------+
+| {SMITH, CLERK, 7902, 1980-12-17, 800.00, null, 20, RESEARCH, DALLAS} |
++----------------------------------------------------------------------+
+(1 row)
+
+!ok
+
# End struct.iq
diff --git a/site/_docs/reference.md b/site/_docs/reference.md
index e50acf936f..026fa59c47 100644
--- a/site/_docs/reference.md
+++ b/site/_docs/reference.md
@@ -248,9 +248,16 @@ ## Grammar
*
| * REPLACE '(' expression AS column [, expression AS column ]* ')'
+rowStarItem:
+ *
+ | tableAlias . *
+ | * { EXCLUDE | EXCEPT } '(' column [, column ]* ')'
+ | tableAlias . * { EXCLUDE | EXCEPT } '(' column [, column ]* ')'
+
Note:
-* `SELECT * EXCLUDE (...)` and `SELECT * REPLACE (...)` are recognized only
when the Babel parser is enabled. `EXCLUDE` (or the alias `EXCEPT`) removes the
specified columns from the star expansion; `REPLACE` substitutes the given
expressions for the matching columns while keeping the original column order.
For `REPLACE`, the column alias must either be a simple identifier or, for a
table-qualified star such as `t.*`, a qualified identifier whose prefix matches
the star's table alias.
+* `EXCLUDE` (or the alias `EXCEPT`) removes the specified columns from the
star expansion; `REPLACE` substitutes the given expressions for the matching
columns while keeping the original column order. For `REPLACE`, the column
alias must either be a simple identifier or, for a table-qualified star such as
`t.*`, a qualified identifier whose prefix matches the star's table alias.
+* `ROW(rowStarItem [, rowStarItem ]*)` creates a nested ROW from all columns
(or all columns except the excluded ones) of one or more tables. `EXCEPT` is an
alias for `EXCLUDE` in this context.
projectItem:
expression [ [ AS ] columnAlias ]
@@ -1793,6 +1800,7 @@ ### 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.