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

zhenchen 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 935d676425 [CALCITE-7310] Support the syntax SELECT * EXCLUDE(columns)
935d676425 is described below

commit 935d6764258584b5c30f761042669d3a268bf44f
Author: Zhen Chen <[email protected]>
AuthorDate: Tue Dec 9 06:42:44 2025 +0800

    [CALCITE-7310] Support the syntax SELECT * EXCLUDE(columns)
---
 babel/src/main/codegen/config.fmpp                 |   1 +
 .../org/apache/calcite/test/BabelParserTest.java   |  25 +++++
 .../java/org/apache/calcite/test/BabelTest.java    |  51 +++++++++
 babel/src/test/resources/sql/select.iq             |  53 +++++++++
 core/src/main/codegen/default_config.fmpp          |   1 +
 core/src/main/codegen/templates/Parser.jj          |  61 +++++++++++
 .../apache/calcite/runtime/CalciteResource.java    |   6 ++
 .../org/apache/calcite/sql/SqlStarExclude.java     |  84 +++++++++++++++
 .../calcite/sql/validate/SqlValidatorImpl.java     | 118 +++++++++++++++++++--
 .../calcite/runtime/CalciteResource.properties     |   2 +
 site/_docs/reference.md                            |  12 ++-
 11 files changed, 406 insertions(+), 8 deletions(-)

diff --git a/babel/src/main/codegen/config.fmpp 
b/babel/src/main/codegen/config.fmpp
index c41f28bd71..b9c4a1c6ee 100644
--- a/babel/src/main/codegen/config.fmpp
+++ b/babel/src/main/codegen/config.fmpp
@@ -617,6 +617,7 @@ data: {
     includePosixOperators: true
     includeParsingStringLiteralAsArrayLiteral: true
     includeIntervalWithoutQualifier: true
+    includeStarExclude: true
   }
 }
 
diff --git a/babel/src/test/java/org/apache/calcite/test/BabelParserTest.java 
b/babel/src/test/java/org/apache/calcite/test/BabelParserTest.java
index a0fb721aef..e3d49d2bb6 100644
--- a/babel/src/test/java/org/apache/calcite/test/BabelParserTest.java
+++ b/babel/src/test/java/org/apache/calcite/test/BabelParserTest.java
@@ -136,6 +136,31 @@ class BabelParserTest extends SqlParserTest {
             + "FROM \"t\"");
   }
 
+  /** Test case for <a 
href="https://issues.apache.org/jira/browse/CALCITE-7310";>
+   * [CALCITE-7310] Support the syntax SELECT * EXCLUDE(columns)</a>. */
+  @Test void testStarExclude() {
+    final String sql = "select * exclude(empno) from emp";
+    final String expected = "SELECT * EXCLUDE (`EMPNO`)\n"
+        + "FROM `EMP`";
+    sql(sql).ok(expected);
+
+    final String sql2 = "select e.* exclude(e.empno, e.ename, e.job, e.mgr, 
d.deptno)"
+        + " from emp e join dept d on e.deptno = d.deptno";
+    final String expected2 = "SELECT `E`.* EXCLUDE (`E`.`EMPNO`, `E`.`ENAME`,"
+        + " `E`.`JOB`, `E`.`MGR`, `D`.`DEPTNO`)\n"
+        + "FROM `EMP` AS `E`\n"
+        + "INNER JOIN `DEPT` AS `D` ON (`E`.`DEPTNO` = `D`.`DEPTNO`)";
+    sql(sql2).ok(expected2);
+
+    final String sql3 = "select e.* exclude(e.empno, e.ename, e.job, e.mgr, 
d.deptno),"
+        + " d.* exclude(d.dname) from emp e join dept d on e.deptno = 
d.deptno";
+    final String expected3 = "SELECT `E`.* EXCLUDE (`E`.`EMPNO`, `E`.`ENAME`,"
+        + " `E`.`JOB`, `E`.`MGR`, `D`.`DEPTNO`), `D`.* EXCLUDE (`D`.`DNAME`)\n"
+        + "FROM `EMP` AS `E`\n"
+        + "INNER JOIN `DEPT` AS `D` ON (`E`.`DEPTNO` = `D`.`DEPTNO`)";
+    sql(sql3).ok(expected3);
+  }
+
   /** Tests that there are no reserved keywords. */
   @Disabled
   @Test void testKeywords() {
diff --git a/babel/src/test/java/org/apache/calcite/test/BabelTest.java 
b/babel/src/test/java/org/apache/calcite/test/BabelTest.java
index 1feb2e6020..9368a7ab71 100644
--- a/babel/src/test/java/org/apache/calcite/test/BabelTest.java
+++ b/babel/src/test/java/org/apache/calcite/test/BabelTest.java
@@ -18,6 +18,7 @@
 
 import org.apache.calcite.config.CalciteConnectionProperty;
 import org.apache.calcite.rel.type.DelegatingTypeSystem;
+import org.apache.calcite.rel.type.RelDataTypeField;
 import org.apache.calcite.rel.type.TimeFrameSet;
 import org.apache.calcite.sql.SqlOperatorTable;
 import org.apache.calcite.sql.fun.SqlLibrary;
@@ -26,6 +27,8 @@
 import org.apache.calcite.sql.parser.babel.SqlBabelParserImpl;
 import org.apache.calcite.sql.validate.SqlConformanceEnum;
 
+import com.google.common.collect.ImmutableList;
+
 import org.junit.jupiter.api.Test;
 
 import java.sql.Connection;
@@ -35,8 +38,10 @@
 import java.sql.SQLException;
 import java.sql.Statement;
 import java.sql.Types;
+import java.util.List;
 import java.util.Properties;
 import java.util.function.UnaryOperator;
+import java.util.stream.Collectors;
 
 import static org.hamcrest.CoreMatchers.is;
 import static org.hamcrest.MatcherAssert.assertThat;
@@ -159,6 +164,52 @@ private void checkInfixCast(Statement statement, String 
typeName, int sqlType)
         .fails("(?s).*Encountered \":\" at .*");
   }
 
+  /** Test case for <a 
href="https://issues.apache.org/jira/browse/CALCITE-7310";>
+   * [CALCITE-7310] Support the syntax SELECT * EXCLUDE(columns)</a>. */
+  @Test void testStarExcludeValidation() {
+    final SqlValidatorFixture fixture = Fixtures.forValidator()
+        .withParserConfig(p -> 
p.withParserFactory(SqlBabelParserImpl.FACTORY));
+
+    fixture.withSql("select * exclude(empno, deptno) from emp")
+        .type(type -> {
+          final List<String> names = type.getFieldList().stream()
+              .map(RelDataTypeField::getName)
+              .collect(Collectors.toList());
+          assertThat(
+              names, is(
+                  ImmutableList.of("ENAME", "JOB", "MGR", "HIREDATE", "SAL", 
"COMM", "SLACKER")));
+        });
+
+    fixture.withSql("select * exclude (empno, ^foo^) from emp")
+        .fails("SELECT \\* EXCLUDE list contains unknown column\\(s\\): FOO");
+
+    fixture.withSql("select e.* exclude(e.empno, e.ename, e.job, e.mgr)"
+            + " from emp e join dept d on e.deptno = d.deptno")
+        .type(type -> {
+          final List<String> names = type.getFieldList().stream()
+              .map(RelDataTypeField::getName)
+              .collect(Collectors.toList());
+          assertThat(
+              names, is(
+                  ImmutableList.of("HIREDATE", "SAL", "COMM", "DEPTNO", 
"SLACKER")));
+        });
+
+    fixture.withSql("select e.* exclude(e.empno, e.ename, e.job, e.mgr, 
^d.deptno^)"
+            + " from emp e join dept d on e.deptno = d.deptno")
+        .fails("SELECT \\* EXCLUDE list contains unknown column\\(s\\): 
D.DEPTNO");
+
+    fixture.withSql("select e.* exclude(e.empno, e.ename, e.job, e.mgr), d.* 
exclude(d.name)"
+            + " from emp e join dept d on e.deptno = d.deptno")
+        .type(type -> {
+          final List<String> names = type.getFieldList().stream()
+              .map(RelDataTypeField::getName)
+              .collect(Collectors.toList());
+          assertThat(
+              names, is(
+                  ImmutableList.of("HIREDATE", "SAL", "COMM", "DEPTNO", 
"SLACKER", "DEPTNO0")));
+        });
+  }
+
   /** Tests that DATEADD, DATEDIFF, DATEPART, DATE_PART allow custom time
    * frames. */
   @Test void testTimeFrames() {
diff --git a/babel/src/test/resources/sql/select.iq 
b/babel/src/test/resources/sql/select.iq
index 9c234d1a04..6b02cca8e3 100755
--- a/babel/src/test/resources/sql/select.iq
+++ b/babel/src/test/resources/sql/select.iq
@@ -107,4 +107,57 @@ select 1.0 % 2;
 
 !ok
 
+# [CALCITE-7310] Support the syntax SELECT * EXCLUDE(columns)
+select * exclude(empno, ename, job, mgr) from emp limit 1;
++------------+--------+------+--------+
+| HIREDATE   | SAL    | COMM | DEPTNO |
++------------+--------+------+--------+
+| 1980-12-17 | 800.00 |      |     20 |
++------------+--------+------+--------+
+(1 row)
+
+!ok
+
+select * exclude(empno, ename, job, mgr, mgr) from emp limit 1;
++------------+--------+------+--------+
+| HIREDATE   | SAL    | COMM | DEPTNO |
++------------+--------+------+--------+
+| 1980-12-17 | 800.00 |      |     20 |
++------------+--------+------+--------+
+(1 row)
+
+!ok
+
+select e.*, d.* from emp e join dept d on e.deptno = d.deptno limit 1;
++-------+-------+---------+------+------------+---------+------+--------+---------+------------+----------+
+| EMPNO | ENAME | JOB     | MGR  | HIREDATE   | SAL     | COMM | DEPTNO | 
DEPTNO0 | DNAME      | LOC      |
++-------+-------+---------+------+------------+---------+------+--------+---------+------------+----------+
+|  7782 | CLARK | MANAGER | 7839 | 1981-06-09 | 2450.00 |      |     10 |      
10 | ACCOUNTING | NEW YORK |
++-------+-------+---------+------+------------+---------+------+--------+---------+------------+----------+
+(1 row)
+
+!ok
+
+select e.* exclude(e.empno, e.ename, e.job, e.mgr)
+from emp e join dept d on e.deptno = d.deptno limit 1;
++------------+--------+------+--------+
+| HIREDATE   | SAL    | COMM | DEPTNO |
++------------+--------+------+--------+
+| 1980-12-17 | 800.00 |      |     20 |
++------------+--------+------+--------+
+(1 row)
+
+!ok
+
+select e.* exclude(e.empno, e.ename, e.job, e.mgr), d.* exclude(d.dname)
+from emp e join dept d on e.deptno = d.deptno limit 1;
++------------+---------+------+--------+---------+----------+
+| HIREDATE   | SAL     | COMM | DEPTNO | DEPTNO0 | LOC      |
++------------+---------+------+--------+---------+----------+
+| 1981-06-09 | 2450.00 |      |     10 |      10 | NEW YORK |
++------------+---------+------+--------+---------+----------+
+(1 row)
+
+!ok
+
 # End select.iq
diff --git a/core/src/main/codegen/default_config.fmpp 
b/core/src/main/codegen/default_config.fmpp
index a2547273cb..56d17b8279 100644
--- a/core/src/main/codegen/default_config.fmpp
+++ b/core/src/main/codegen/default_config.fmpp
@@ -460,4 +460,5 @@ 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 95d34d4eb6..a6f50f1f12 100644
--- a/core/src/main/codegen/templates/Parser.jj
+++ b/core/src/main/codegen/templates/Parser.jj
@@ -91,6 +91,7 @@ import org.apache.calcite.sql.SqlRowTypeNameSpec;
 import org.apache.calcite.sql.SqlSampleSpec;
 import org.apache.calcite.sql.SqlSelect;
 import org.apache.calcite.sql.SqlSelectKeyword;
+import org.apache.calcite.sql.SqlStarExclude;
 import org.apache.calcite.sql.SqlSetOption;
 import org.apache.calcite.sql.SqlSnapshot;
 import org.apache.calcite.sql.SqlTableRef;
@@ -1974,6 +1975,65 @@ void AddSelectItem(List<SqlNode> list) :
     )
 }
 
+<#if (parser.includeStarExclude!default.parser.includeStarExclude)>
+/**
+ * Parses one unaliased expression in a select list.
+ */
+SqlNode SelectExpression() :
+{
+    SqlNode e;
+    SqlNodeList excludeList;
+}
+{
+    (
+        <STAR> {
+            e = SqlIdentifier.star(getPos());
+        }
+    |
+        e = Expression(ExprContext.ACCEPT_SUB_QUERY)
+    )
+    (
+        excludeList = StarExcludeList() {
+            if (!(e instanceof SqlIdentifier)) {
+                throw 
SqlUtil.newContextException(excludeList.getParserPosition(),
+                    RESOURCE.selectExcludeRequiresStar());
+            }
+            final SqlIdentifier sqlIdentifier = (SqlIdentifier) e;
+            if (!sqlIdentifier.isStar()) {
+                throw 
SqlUtil.newContextException(excludeList.getParserPosition(),
+                    RESOURCE.selectExcludeRequiresStar());
+            }
+            final SqlParserPos pos = SqlParserPos.sum(
+                ImmutableList.of(sqlIdentifier.getParserPosition(),
+                    excludeList.getParserPosition()));
+            return new SqlStarExclude(pos, sqlIdentifier, excludeList);
+        }
+    |
+        { return e; }
+    )
+}
+
+SqlNodeList StarExcludeList() :
+{
+    final Span s;
+    final List<SqlNode> list = new ArrayList<SqlNode>();
+    SqlIdentifier id;
+}
+{
+    <EXCLUDE> <LPAREN> { s = span(); }
+    id = CompoundIdentifier() {
+        list.add(id);
+    }
+    (
+        <COMMA> id = CompoundIdentifier() {
+            list.add(id);
+        }
+    )*
+    <RPAREN> {
+        return new SqlNodeList(list, s.end(this));
+    }
+}
+<#else>
 /**
  * Parses one unaliased expression in a select list.
  */
@@ -1990,6 +2050,7 @@ SqlNode SelectExpression() :
         return e;
     }
 }
+</#if>
 
 SqlLiteral Natural() :
 {
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 809bdcce70..d35f1a030e 100644
--- a/core/src/main/java/org/apache/calcite/runtime/CalciteResource.java
+++ b/core/src/main/java/org/apache/calcite/runtime/CalciteResource.java
@@ -807,6 +807,12 @@ ExInst<CalciteException> 
illegalArgumentForTableFunctionCall(String a0,
   @BaseMessage("SELECT * requires a FROM clause")
   ExInst<SqlValidatorException> selectStarRequiresFrom();
 
+  @BaseMessage("EXCLUDE clause must follow a STAR expression")
+  ExInst<CalciteException> selectExcludeRequiresStar();
+
+  @BaseMessage("SELECT * EXCLUDE list contains unknown column(s): {0}")
+  ExInst<SqlValidatorException> 
selectStarExcludeListContainsUnknownColumns(String columns);
+
   @BaseMessage("Group function ''{0}'' can only appear in GROUP BY clause")
   ExInst<SqlValidatorException> groupFunctionMustAppearInGroupByClause(String 
funcName);
 
diff --git a/core/src/main/java/org/apache/calcite/sql/SqlStarExclude.java 
b/core/src/main/java/org/apache/calcite/sql/SqlStarExclude.java
new file mode 100644
index 0000000000..884c4a8463
--- /dev/null
+++ b/core/src/main/java/org/apache/calcite/sql/SqlStarExclude.java
@@ -0,0 +1,84 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.calcite.sql;
+
+import org.apache.calcite.sql.parser.SqlParserPos;
+
+import com.google.common.collect.ImmutableList;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+import java.util.List;
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * Represents {@code SELECT * EXCLUDE(...)}.
+ */
+public class SqlStarExclude extends SqlCall {
+  public static final SqlOperator OPERATOR =
+      new SqlSpecialOperator("SELECT_STAR_EXCLUDE", SqlKind.OTHER) {
+        @SuppressWarnings("argument.type.incompatible")
+        @Override public SqlCall createCall(
+            @Nullable SqlLiteral functionQualifier,
+            SqlParserPos pos,
+            @Nullable SqlNode... operands) {
+          return new SqlStarExclude(
+              pos,
+              (SqlIdentifier) operands[0],
+              (SqlNodeList) operands[1]);
+        }
+      };
+
+  private final SqlIdentifier starIdentifier;
+  private final SqlNodeList excludeList;
+
+  public SqlStarExclude(SqlParserPos pos, SqlIdentifier starIdentifier,
+      SqlNodeList excludeList) {
+    super(pos);
+    this.starIdentifier = requireNonNull(starIdentifier, "starIdentifier");
+    this.excludeList = requireNonNull(excludeList, "excludeList");
+  }
+
+  public SqlIdentifier getStarIdentifier() {
+    return starIdentifier;
+  }
+
+  public SqlNodeList getExcludeList() {
+    return excludeList;
+  }
+
+  @Override public SqlOperator getOperator() {
+    return OPERATOR;
+  }
+
+  @Override public SqlKind getKind() {
+    return OPERATOR.getKind();
+  }
+
+  @Override public List<SqlNode> getOperandList() {
+    return ImmutableList.of(starIdentifier, excludeList);
+  }
+
+  @Override public void unparse(SqlWriter writer, int leftPrec, int rightPrec) 
{
+    starIdentifier.unparse(writer, leftPrec, rightPrec);
+    writer.sep("EXCLUDE");
+    final SqlWriter.Frame frame = writer.startList("(", ")");
+    excludeList.unparse(writer, 0, 0);
+    writer.endList(frame);
+  }
+}
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 96d46174d4..172921babf 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
@@ -79,6 +79,7 @@
 import org.apache.calcite.sql.SqlSelect;
 import org.apache.calcite.sql.SqlSelectKeyword;
 import org.apache.calcite.sql.SqlSnapshot;
+import org.apache.calcite.sql.SqlStarExclude;
 import org.apache.calcite.sql.SqlSyntax;
 import org.apache.calcite.sql.SqlTableFunction;
 import org.apache.calcite.sql.SqlUnknownLiteral;
@@ -639,13 +640,26 @@ private static void validateQualifiedCommonColumn(SqlJoin 
join,
   private boolean expandStar(List<SqlNode> selectItems, Set<String> aliases,
       PairList<String, RelDataType> fields, boolean includeSystemVars,
       SelectScope scope, SqlNode node) {
-    if (!(node instanceof SqlIdentifier)) {
+    final SqlIdentifier identifier;
+    final SqlNodeList excludeList;
+    if (node instanceof SqlStarExclude) {
+      final SqlStarExclude starExclude = (SqlStarExclude) node;
+      identifier = starExclude.getStarIdentifier();
+      excludeList = starExclude.getExcludeList();
+    } else if (node instanceof SqlIdentifier) {
+      identifier = (SqlIdentifier) node;
+      excludeList = null;
+    } else {
       return false;
     }
-    final SqlIdentifier identifier = (SqlIdentifier) node;
     if (!identifier.isStar()) {
       return false;
     }
+    final List<SqlIdentifier> excludeIdentifiers =
+        excludeList == null ? Collections.emptyList() : 
extractExcludeIdentifiers(excludeList);
+    final boolean[] excludeMatched = new boolean[excludeIdentifiers.size()];
+    final SqlNameMatcher nameMatcher =
+        scope.validator.catalogReader.nameMatcher();
     final int originalSize = selectItems.size();
     final SqlParserPos startPosition = identifier.getParserPosition();
     switch (identifier.names.size()) {
@@ -687,6 +701,10 @@ private boolean expandStar(List<SqlNode> selectItems, 
Set<String> aliases,
                 new SqlIdentifier(
                     ImmutableList.of(child.name, columnName),
                     startPosition);
+            recordExcludeMatches(excludeIdentifiers, exp, nameMatcher, 
excludeMatched);
+            if (shouldExcludeField(excludeList, exp, nameMatcher)) {
+              continue;
+            }
             // Don't add expanded rolled up columns
             if (!isRolledUpColumn(exp, scope)) {
               addOrExpandField(
@@ -720,15 +738,16 @@ 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);
       return true;
 
     default:
       final SqlIdentifier prefixId = identifier.skipLast(1);
       final SqlValidatorScope.ResolvedImpl resolved =
           new SqlValidatorScope.ResolvedImpl();
-      final SqlNameMatcher nameMatcher =
+      final SqlNameMatcher resolvedNameMatcher =
           scope.validator.catalogReader.nameMatcher();
-      scope.resolve(prefixId.names, nameMatcher, true, resolved);
+      scope.resolve(prefixId.names, resolvedNameMatcher, true, resolved);
       if (resolved.count() == 0) {
         // e.g. "select s.t.* from e"
         // or "select r.* from e"
@@ -749,6 +768,13 @@ private boolean expandStar(List<SqlNode> selectItems, 
Set<String> aliases,
         for (RelDataTypeField field : rowType.getFieldList()) {
           String columnName = field.getName();
 
+          final SqlIdentifier columnId =
+              prefixId.plus(columnName, startPosition);
+          recordExcludeMatches(excludeIdentifiers, columnId, 
resolvedNameMatcher,
+              excludeMatched);
+          if (shouldExcludeField(excludeList, columnId, resolvedNameMatcher)) {
+            continue;
+          }
           // TODO: do real implicit collation here
           addOrExpandField(
               selectItems,
@@ -756,12 +782,13 @@ private boolean expandStar(List<SqlNode> selectItems, 
Set<String> aliases,
               fields,
               includeSystemVars,
               scope,
-              prefixId.plus(columnName, startPosition),
+              columnId,
               field);
         }
       } else {
         throw newValidationError(prefixId, RESOURCE.starRequiresRecordType());
       }
+      throwIfUnknownExcludeColumns(excludeIdentifiers, excludeMatched);
       return true;
     }
   }
@@ -778,6 +805,86 @@ private static int calculatePermuteOffset(List<SqlNode> 
selectItems) {
     return 0;
   }
 
+  private static boolean matchesExcludeNames(List<String> identifierNames,
+      List<String> excludedIdentifierNames, SqlNameMatcher nameMatcher) {
+    if (excludedIdentifierNames.size() > identifierNames.size()) {
+      return false;
+    }
+    final int offset = identifierNames.size() - excludedIdentifierNames.size();
+    for (int i = 0; i < excludedIdentifierNames.size(); i++) {
+      if (!nameMatcher.matches(identifierNames.get(offset + i),
+          excludedIdentifierNames.get(i))) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  private static boolean shouldExcludeField(@Nullable SqlNodeList excludeList,
+      SqlIdentifier columnId, SqlNameMatcher nameMatcher) {
+    if (excludeList == null) {
+      return false;
+    }
+    for (SqlNode node : excludeList) {
+      assert node instanceof SqlIdentifier;
+      if (matchesExcludeIdentifier(columnId, (SqlIdentifier) node, 
nameMatcher)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  private static boolean matchesExcludeIdentifier(SqlIdentifier columnId,
+      SqlIdentifier excludeIdentifier, SqlNameMatcher nameMatcher) {
+    return matchesExcludeNames(columnId.names, excludeIdentifier.names, 
nameMatcher);
+  }
+
+  private static List<SqlIdentifier> extractExcludeIdentifiers(@Nullable 
SqlNodeList excludeList) {
+    if (excludeList == null) {
+      return ImmutableList.of();
+    }
+    final ImmutableList.Builder<SqlIdentifier> builder = 
ImmutableList.builder();
+    for (SqlNode node : excludeList) {
+      if (node instanceof SqlIdentifier) {
+        builder.add((SqlIdentifier) node);
+      }
+    }
+    return builder.build();
+  }
+
+  private static void recordExcludeMatches(List<SqlIdentifier> 
excludeIdentifiers,
+      SqlIdentifier columnId, SqlNameMatcher nameMatcher, boolean[] matched) {
+    for (int i = 0; i < excludeIdentifiers.size(); i++) {
+      if (!matched[i]
+          && matchesExcludeIdentifier(columnId, excludeIdentifiers.get(i), 
nameMatcher)) {
+        matched[i] = true;
+      }
+    }
+  }
+
+  private void throwIfUnknownExcludeColumns(List<SqlIdentifier> 
excludeIdentifiers,
+      boolean[] excludeMatched) {
+    if (excludeIdentifiers.isEmpty()) {
+      return;
+    }
+    final List<String> unknownExcludeNames = new ArrayList<>();
+    int firstUnknownIndex = -1;
+    for (int i = 0; i < excludeIdentifiers.size(); i++) {
+      if (!excludeMatched[i]) {
+        if (firstUnknownIndex < 0) {
+          firstUnknownIndex = i;
+        }
+        unknownExcludeNames.add(excludeIdentifiers.get(i).toString());
+      }
+    }
+    if (firstUnknownIndex >= 0) {
+      throw newValidationError(
+          excludeIdentifiers.get(firstUnknownIndex),
+          RESOURCE.selectStarExcludeListContainsUnknownColumns(
+              String.join(", ", unknownExcludeNames)));
+    }
+  }
+
   private SqlNode maybeCast(SqlNode node, RelDataType currentType,
       RelDataType desiredType) {
     return SqlTypeUtil.equalSansNullability(typeFactory, currentType, 
desiredType)
@@ -801,7 +908,6 @@ private boolean addOrExpandField(List<SqlNode> selectItems, 
Set<String> aliases,
           scope,
           starExp);
       return true;
-
     default:
       addToSelectList(
           selectItems,
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 124e133a14..4cc492795e 100644
--- 
a/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties
+++ 
b/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties
@@ -266,6 +266,8 @@ CannotStreamResultsForNonStreamingInputs=Cannot stream 
results of a query with n
 MinusNotAllowed=MINUS is not allowed under the current SQL conformance level
 SelectMissingFrom=SELECT must have a FROM clause
 SelectStarRequiresFrom=SELECT * requires a FROM clause
+SelectExcludeRequiresStar=EXCLUDE clause must follow a STAR expression
+SelectStarExcludeListContainsUnknownColumns=SELECT * EXCLUDE list contains 
unknown column(s): {0}
 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/site/_docs/reference.md b/site/_docs/reference.md
index b07ba0c8fa..7017d4565d 100644
--- a/site/_docs/reference.md
+++ b/site/_docs/reference.md
@@ -205,8 +205,8 @@ ## Grammar
       expression [ ASC | DESC ] [ NULLS FIRST | NULLS LAST ]
 
 select:
-      SELECT [ hintComment ] [ STREAM ] [ ALL | DISTINCT ]
-          { * | projectItem [, projectItem ]* }
+  SELECT [ hintComment ] [ STREAM ] [ ALL | DISTINCT ]
+  { starWithExclude | projectItem [, projectItem ]* }
       FROM tableExpression
       [ WHERE booleanExpression ]
       [ GROUP BY [ ALL | DISTINCT ] { groupItem [, groupItem ]* } ]
@@ -218,6 +218,14 @@ ## Grammar
       SELECT [ ALL | DISTINCT ]
           { * | projectItem [, projectItem ]* }
 
+starWithExclude:
+      *
+  |   * EXCLUDE '(' column [, column ]* ')'
+
+Note:
+
+* `SELECT * EXCLUDE (...)` is recognized only when the Babel parser is 
enabled. It sets the generated parser configuration flag `includeStarExclude` 
to `true` (the standard parser leaves that flag `false`), which allows a `STAR` 
token followed by `EXCLUDE` and a parenthesized identifier list to be parsed 
into a `SqlStarExclude` node and ensures validators respect the exclusion list 
when expanding the projection. Reusing the same parser configuration elsewhere 
enables the same syntax for  [...]
+
 projectItem:
       expression [ [ AS ] columnAlias ]
   |   tableAlias . *

Reply via email to