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

xuzifu666 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 142bbe22bc [CALCITE-7523] Support the syntax SELECT * REPLACE(expr as 
column)
142bbe22bc is described below

commit 142bbe22bcfec20685601861594825342d288240
Author: Yu Xu <[email protected]>
AuthorDate: Wed May 13 16:57:15 2026 +0800

    [CALCITE-7523] Support the syntax SELECT * REPLACE(expr as column)
---
 .../org/apache/calcite/test/BabelParserTest.java   |  18 +++
 .../java/org/apache/calcite/test/BabelTest.java    |  73 +++++++++
 babel/src/test/resources/sql/select.iq             |  46 ++++++
 core/src/main/codegen/templates/Parser.jj          |  52 +++++++
 .../apache/calcite/runtime/CalciteResource.java    |   9 ++
 .../org/apache/calcite/sql/SqlStarReplace.java     |  84 +++++++++++
 .../calcite/sql/validate/SqlValidatorImpl.java     | 164 +++++++++++++++++++++
 .../calcite/runtime/CalciteResource.properties     |   3 +
 site/_docs/reference.md                            |   6 +-
 9 files changed, 454 insertions(+), 1 deletion(-)

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 b24b619027..48172f7d7f 100644
--- a/babel/src/test/java/org/apache/calcite/test/BabelParserTest.java
+++ b/babel/src/test/java/org/apache/calcite/test/BabelParserTest.java
@@ -167,6 +167,24 @@ class BabelParserTest extends SqlParserTest {
     sql(sql3).ok(expected3);
   }
 
+  /** Test case for <a 
href="https://issues.apache.org/jira/browse/CALCITE-7532";>
+   * [CALCITE-7532] Support the syntax SELECT * REPLACE(expr as column)</a>.
+   * */
+  @Test void testStarReplace() {
+    final String sql = "select * replace(empno + 1 as empno) from emp";
+    final String expected = "SELECT * REPLACE ((`EMPNO` + 1) AS `EMPNO`)\n"
+        + "FROM `EMP`";
+    sql(sql).ok(expected);
+
+    final String sql2 = "select e.* replace(e.empno + 1 as e.empno, e.sal * 2 
as e.sal)"
+        + " from emp e join dept d on e.deptno = d.deptno";
+    final String expected2 = "SELECT `E`.* REPLACE ((`E`.`EMPNO` + 1) AS 
`E`.`EMPNO`,"
+        + " (`E`.`SAL` * 2) AS `E`.`SAL`)\n"
+        + "FROM `EMP` AS `E`\n"
+        + "INNER JOIN `DEPT` AS `D` ON (`E`.`DEPTNO` = `D`.`DEPTNO`)";
+    sql(sql2).ok(expected2);
+  }
+
   /** 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 aff1aee660..1deda157fa 100644
--- a/babel/src/test/java/org/apache/calcite/test/BabelTest.java
+++ b/babel/src/test/java/org/apache/calcite/test/BabelTest.java
@@ -275,6 +275,79 @@ names, is(
         .fails("SELECT \\* EXCLUDE/EXCEPT list cannot exclude all columns");
   }
 
+  /** Test case for <a 
href="https://issues.apache.org/jira/browse/CALCITE-7532";>
+   * [CALCITE-7532] Support the syntax SELECT * REPLACE(expr as column)</a>. */
+  @Test void testStarReplaceValidation() {
+    final SqlValidatorFixture fixture = Fixtures.forValidator()
+        .withParserConfig(p -> 
p.withParserFactory(SqlBabelParserImpl.FACTORY));
+
+    fixture.withSql("select * replace(empno + 1 as empno) from emp")
+        .type(type -> {
+          final List<String> names = type.getFieldList().stream()
+              .map(RelDataTypeField::getName)
+              .collect(Collectors.toList());
+          assertThat(
+              names, is(
+                  ImmutableList.of("EMPNO", "ENAME", "JOB", "MGR",
+              "HIREDATE", "SAL", "COMM", "DEPTNO", "SLACKER")));
+          // Verify that EMPNO type is still INTEGER (or similar)
+          
assertThat(type.getFieldList().get(0).getType().getSqlTypeName().getName(),
+              is("INTEGER"));
+        });
+
+    fixture.withSql("select * replace(empno + 1 as empno, sal * 2 as sal) from 
emp")
+        .type(type -> {
+          final List<String> names = type.getFieldList().stream()
+              .map(RelDataTypeField::getName)
+              .collect(Collectors.toList());
+          assertThat(
+              names, is(
+                  ImmutableList.of("EMPNO", "ENAME", "JOB", "MGR",
+              "HIREDATE", "SAL", "COMM", "DEPTNO", "SLACKER")));
+        });
+
+    // REPLACE with a completely different type
+    fixture.withSql("select * replace('fixed' as empno) from emp")
+        .type(type -> {
+          final List<String> names = type.getFieldList().stream()
+              .map(RelDataTypeField::getName)
+              .collect(Collectors.toList());
+          assertThat(
+              names, is(
+                  ImmutableList.of("EMPNO", "ENAME", "JOB", "MGR",
+              "HIREDATE", "SAL", "COMM", "DEPTNO", "SLACKER")));
+          // EMPNO was INTEGER, now replaced by a CHAR literal
+          
assertThat(type.getFieldList().get(0).getType().getSqlTypeName().getName(),
+              is("CHAR"));
+        });
+
+    // Same column replaced twice
+    fixture.withSql("select * replace(empno + 1 as empno, 'fixed' as ^empno^) 
from emp")
+        .fails("SELECT \\* REPLACE list contains duplicate column\\(s\\): 
EMPNO");
+
+    // Unknown column in REPLACE list
+    fixture.withSql("select * replace(empno + 1 as ^foo^) from emp")
+        .fails("SELECT \\* REPLACE list contains unknown column\\(s\\): FOO");
+
+    // Table-qualified star with REPLACE
+    fixture.withSql("select e.* replace(e.empno + 1 as e.empno)"
+            + " 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("EMPNO", "ENAME", "JOB", "MGR",
+              "HIREDATE", "SAL", "COMM", "DEPTNO", "SLACKER")));
+        });
+
+    // REPLACE with unknown qualified column
+    fixture.withSql("select e.* replace(e.empno + 1 as ^d.deptno^)"
+            + " from emp e join dept d on e.deptno = d.deptno")
+        .fails("SELECT \\* REPLACE list contains unknown column\\(s\\): 
DEPTNO");
+  }
+
   /** 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 c969ad7655..f94905c16e 100755
--- a/babel/src/test/resources/sql/select.iq
+++ b/babel/src/test/resources/sql/select.iq
@@ -286,4 +286,50 @@ select d1.* except(d1.dname) from dept d1 except(select 
d2.* except(d2.dname) fr
 
 !ok
 
+# SELECT * REPLACE(expr AS column)
+select * replace(empno + 1 as empno) from emp where empno = 7369;
++-------+-------+-------+------+------------+--------+------+--------+
+| EMPNO | ENAME | JOB   | MGR  | HIREDATE   | SAL    | COMM | DEPTNO |
++-------+-------+-------+------+------------+--------+------+--------+
+|  7370 | SMITH | CLERK | 7902 | 1980-12-17 | 800.00 |      |     20 |
++-------+-------+-------+------+------------+--------+------+--------+
+(1 row)
+
+!ok
+
+select * replace(sal * 2 as sal, upper(ename) as ename) from emp where empno = 
7369;
++-------+-------+-------+------+------------+---------+------+--------+
+| EMPNO | ENAME | JOB   | MGR  | HIREDATE   | SAL     | COMM | DEPTNO |
++-------+-------+-------+------+------------+---------+------+--------+
+|  7369 | SMITH | CLERK | 7902 | 1980-12-17 | 1600.00 |      |     20 |
++-------+-------+-------+------+------------+---------+------+--------+
+(1 row)
+
+!ok
+
+select e.* replace(e.empno + 1 as e.empno)
+from emp e join dept d on e.deptno = d.deptno
+where e.empno = 7782;
++-------+-------+---------+------+------------+---------+------+--------+
+| EMPNO | ENAME | JOB     | MGR  | HIREDATE   | SAL     | COMM | DEPTNO |
++-------+-------+---------+------+------------+---------+------+--------+
+|  7783 | CLARK | MANAGER | 7839 | 1981-06-09 | 2450.00 |      |     10 |
++-------+-------+---------+------+------------+---------+------+--------+
+(1 row)
+
+!ok
+
+select empno replace(empno + 1 as empno) from emp;
+REPLACE clause must follow a STAR expression
+!error
+
+select * replace(empno + 1 as foo) from emp;
+SELECT * REPLACE list contains unknown column(s): FOO
+!error
+
+select e.* replace(e.empno + 1 as d.deptno)
+from emp e join dept d on e.deptno = d.deptno;
+SELECT * REPLACE list contains unknown column(s): DEPTNO
+!error
+
 # End select.iq
diff --git a/core/src/main/codegen/templates/Parser.jj 
b/core/src/main/codegen/templates/Parser.jj
index 7e54fb72f0..b89ca5c5f5 100644
--- a/core/src/main/codegen/templates/Parser.jj
+++ b/core/src/main/codegen/templates/Parser.jj
@@ -93,6 +93,7 @@ import org.apache.calcite.sql.SqlSelect;
 import org.apache.calcite.sql.SqlByRewriter;
 import org.apache.calcite.sql.SqlSelectKeyword;
 import org.apache.calcite.sql.SqlStarExclude;
+import org.apache.calcite.sql.SqlStarReplace;
 import org.apache.calcite.sql.SqlSetOption;
 import org.apache.calcite.sql.SqlSnapshot;
 import org.apache.calcite.sql.SqlTableRef;
@@ -2019,6 +2020,7 @@ SqlNode SelectExpression() :
 {
     SqlNode e;
     SqlNodeList excludeList;
+    SqlNodeList replaceList;
 }
 {
     (
@@ -2029,6 +2031,7 @@ 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(),
@@ -2045,10 +2048,30 @@ SqlNode SelectExpression() :
             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;
+            if (!sqlIdentifier.isStar()) {
+                throw 
SqlUtil.newContextException(replaceList.getParserPosition(),
+                    RESOURCE.selectReplaceRequiresStar());
+            }
+            final SqlParserPos 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;
@@ -2069,6 +2092,35 @@ SqlNodeList StarExcludeList() :
         return new SqlNodeList(list, s.end(this));
     }
 }
+</#if>
+
+<#if (parser.includeStarExclude!default.parser.includeStarExclude)>
+SqlNodeList StarReplaceList() :
+{
+    final Span s;
+    final List<SqlNode> list = new ArrayList<SqlNode>();
+    SqlNode expr;
+    SqlIdentifier id;
+}
+{
+    <REPLACE> <LPAREN> { s = span(); }
+    expr = Expression(ExprContext.ACCEPT_SUB_QUERY) <AS> id = 
CompoundIdentifier() {
+        list.add(SqlStdOperatorTable.AS.createCall(
+            SqlParserPos.sum(ImmutableList.of(expr.getParserPosition(),
+                id.getParserPosition())), expr, id));
+    }
+    (
+        <COMMA> expr = Expression(ExprContext.ACCEPT_SUB_QUERY) <AS> id = 
CompoundIdentifier() {
+            list.add(SqlStdOperatorTable.AS.createCall(
+                SqlParserPos.sum(ImmutableList.of(expr.getParserPosition(),
+                    id.getParserPosition())), expr, id));
+        }
+    )*
+    <RPAREN> {
+        return new SqlNodeList(list, s.end(this));
+    }
+}
+</#if>
 <#else>
 /**
  * Parses one unaliased expression in a select list.
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 baf5fca10e..e00643a567 100644
--- a/core/src/main/java/org/apache/calcite/runtime/CalciteResource.java
+++ b/core/src/main/java/org/apache/calcite/runtime/CalciteResource.java
@@ -810,12 +810,21 @@ ExInst<CalciteException> 
illegalArgumentForTableFunctionCall(String a0,
   @BaseMessage("EXCLUDE/EXCEPT clause must follow a STAR expression")
   ExInst<CalciteException> selectExcludeRequiresStar();
 
+  @BaseMessage("REPLACE clause must follow a STAR expression")
+  ExInst<CalciteException> selectReplaceRequiresStar();
+
   @BaseMessage("SELECT * EXCLUDE/EXCEPT list contains unknown column(s): {0}")
   ExInst<SqlValidatorException> 
selectStarExcludeListContainsUnknownColumns(String columns);
 
   @BaseMessage("SELECT * EXCLUDE/EXCEPT list cannot exclude all columns")
   ExInst<SqlValidatorException> selectStarExcludeCannotExcludeAllColumns();
 
+  @BaseMessage("SELECT * REPLACE list contains unknown column(s): {0}")
+  ExInst<SqlValidatorException> 
selectStarReplaceListContainsUnknownColumns(String columns);
+
+  @BaseMessage("SELECT * REPLACE list contains duplicate column(s): {0}")
+  ExInst<SqlValidatorException> 
selectStarReplaceListContainsDuplicateColumns(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/SqlStarReplace.java 
b/core/src/main/java/org/apache/calcite/sql/SqlStarReplace.java
new file mode 100644
index 0000000000..b5149a6403
--- /dev/null
+++ b/core/src/main/java/org/apache/calcite/sql/SqlStarReplace.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 * REPLACE(expr AS column, ...)}.
+ */
+public class SqlStarReplace extends SqlCall {
+  public static final SqlOperator OPERATOR =
+      new SqlSpecialOperator("SELECT_STAR_REPLACE", SqlKind.OTHER) {
+        @SuppressWarnings("argument.type.incompatible")
+        @Override public SqlCall createCall(
+            @Nullable SqlLiteral functionQualifier,
+            SqlParserPos pos,
+            @Nullable SqlNode... operands) {
+          return new SqlStarReplace(
+              pos,
+              (SqlIdentifier) operands[0],
+              (SqlNodeList) operands[1]);
+        }
+      };
+
+  private final SqlIdentifier starIdentifier;
+  private final SqlNodeList replaceList;
+
+  public SqlStarReplace(SqlParserPos pos, SqlIdentifier starIdentifier,
+      SqlNodeList replaceList) {
+    super(pos);
+    this.starIdentifier = requireNonNull(starIdentifier, "starIdentifier");
+    this.replaceList = requireNonNull(replaceList, "replaceList");
+  }
+
+  public SqlIdentifier getStarIdentifier() {
+    return starIdentifier;
+  }
+
+  public SqlNodeList getReplaceList() {
+    return replaceList;
+  }
+
+  @Override public SqlOperator getOperator() {
+    return OPERATOR;
+  }
+
+  @Override public SqlKind getKind() {
+    return OPERATOR.getKind();
+  }
+
+  @Override public List<SqlNode> getOperandList() {
+    return ImmutableList.of(starIdentifier, replaceList);
+  }
+
+  @Override public void unparse(SqlWriter writer, int leftPrec, int rightPrec) 
{
+    starIdentifier.unparse(writer, leftPrec, rightPrec);
+    writer.sep("REPLACE");
+    final SqlWriter.Frame frame = writer.startList("(", ")");
+    replaceList.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 33cdeadc80..1d2c94d11a 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
@@ -80,6 +80,7 @@
 import org.apache.calcite.sql.SqlSelectKeyword;
 import org.apache.calcite.sql.SqlSnapshot;
 import org.apache.calcite.sql.SqlStarExclude;
+import org.apache.calcite.sql.SqlStarReplace;
 import org.apache.calcite.sql.SqlSyntax;
 import org.apache.calcite.sql.SqlTableFunction;
 import org.apache.calcite.sql.SqlUnknownLiteral;
@@ -123,7 +124,9 @@
 import org.apache.calcite.util.trace.CalciteTrace;
 
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
 import com.google.common.collect.Sets;
 
 import org.apiguardian.api.API;
@@ -645,13 +648,21 @@ private boolean expandStar(List<SqlNode> selectItems, 
Set<String> aliases,
       SelectScope scope, SqlNode node) {
     final SqlIdentifier identifier;
     final SqlNodeList excludeList;
+    final SqlNodeList replaceList;
     if (node instanceof SqlStarExclude) {
       final SqlStarExclude starExclude = (SqlStarExclude) node;
       identifier = starExclude.getStarIdentifier();
       excludeList = starExclude.getExcludeList();
+      replaceList = null;
+    } else if (node instanceof SqlStarReplace) {
+      final SqlStarReplace starReplace = (SqlStarReplace) node;
+      identifier = starReplace.getStarIdentifier();
+      excludeList = null;
+      replaceList = starReplace.getReplaceList();
     } else if (node instanceof SqlIdentifier) {
       identifier = (SqlIdentifier) node;
       excludeList = null;
+      replaceList = null;
     } else {
       return false;
     }
@@ -663,6 +674,38 @@ private boolean expandStar(List<SqlNode> selectItems, 
Set<String> aliases,
     final boolean[] excludeMatched = new boolean[excludeIdentifiers.size()];
     final SqlNameMatcher nameMatcher =
         scope.validator.catalogReader.nameMatcher();
+    if (replaceList != null) {
+      final Set<String> replaceSeen = new HashSet<>();
+      for (SqlNode replaceNode : replaceList) {
+        final SqlCall call = (SqlCall) replaceNode;
+        final SqlIdentifier aliasId = (SqlIdentifier) call.operand(1);
+        final String aliasName =
+            aliasId.isSimple() ? aliasId.getSimple()
+                : aliasId.names.get(aliasId.names.size() - 1);
+        if (!replaceSeen.add(aliasName.toUpperCase(Locale.ROOT))) {
+          throw newValidationError(aliasId,
+              
RESOURCE.selectStarReplaceListContainsDuplicateColumns(aliasName));
+        }
+        if (!aliasId.isSimple()) {
+          final int starPrefixSize = identifier.names.size() - 1;
+          final int aliasPrefixSize = aliasId.names.size() - 1;
+          if (aliasPrefixSize != starPrefixSize) {
+            throw newValidationError(aliasId,
+                
RESOURCE.selectStarReplaceListContainsUnknownColumns(aliasName));
+          }
+          for (int i = 0; i < starPrefixSize; i++) {
+            if (!nameMatcher.matches(identifier.names.get(i), 
aliasId.names.get(i))) {
+              throw newValidationError(aliasId,
+                  
RESOURCE.selectStarReplaceListContainsUnknownColumns(aliasName));
+            }
+          }
+        }
+      }
+    }
+    final Map<String, SqlNode> replaceMap = extractReplaceMap(replaceList);
+    final boolean[] replaceMatched =
+        replaceMap.isEmpty() ? new boolean[0]
+            : new boolean[replaceMap.size()];
     final int originalSize = selectItems.size();
     final SqlParserPos startPosition = identifier.getParserPosition();
     final int fieldsBeforeStar = fields.size();
@@ -709,6 +752,27 @@ private boolean expandStar(List<SqlNode> selectItems, 
Set<String> aliases,
             if (shouldExcludeField(excludeList, exp, nameMatcher)) {
               continue;
             }
+            final SqlNode replacement =
+                findReplacement(columnName, replaceMap, nameMatcher);
+            if (replacement != null) {
+              recordReplaceMatch(columnName, replaceMap, nameMatcher, 
replaceMatched);
+              final SqlNode aliasedReplacement =
+                  SqlStdOperatorTable.AS.createCall(
+                      SqlParserPos.sum(
+                          ImmutableList.of(
+                          replacement.getParserPosition(),
+                          exp.getParserPosition())),
+                      replacement,
+                      new SqlIdentifier(columnName, exp.getParserPosition()));
+              addToSelectList(
+                  selectItems,
+                  aliases,
+                  fields,
+                  aliasedReplacement,
+                  scope,
+                  includeSystemVars);
+              continue;
+            }
             // Don't add expanded rolled up columns
             if (!isRolledUpColumn(exp, scope)) {
               addOrExpandField(
@@ -745,6 +809,7 @@ private boolean expandStar(List<SqlNode> selectItems, 
Set<String> aliases,
       throwIfUnknownExcludeColumns(excludeIdentifiers, excludeMatched);
       throwIfExcludeEliminatesAllColumns(excludeIdentifiers, fieldsBeforeStar,
           fields, identifier);
+      throwIfUnknownReplaceColumns(replaceMap, replaceMatched);
       return true;
 
     default:
@@ -781,6 +846,31 @@ private boolean expandStar(List<SqlNode> selectItems, 
Set<String> aliases,
           if (shouldExcludeField(excludeList, columnId, resolvedNameMatcher)) {
             continue;
           }
+          final SqlNode replacement =
+              findReplacement(columnName, replaceMap, resolvedNameMatcher);
+          if (replacement != null) {
+            recordReplaceMatch(columnName, replaceMap, resolvedNameMatcher,
+                replaceMatched);
+            final SqlNode aliasedReplacement =
+                SqlStdOperatorTable.AS.createCall(
+                    SqlParserPos.sum(
+                        ImmutableList.of(
+                        replacement.getParserPosition(),
+                        columnId.getParserPosition())),
+                    replacement,
+                    new SqlIdentifier(columnName, 
columnId.getParserPosition()));
+            addToSelectList(
+                selectItems,
+                aliases,
+                fields,
+                aliasedReplacement,
+                scope,
+                includeSystemVars);
+            continue;
+          }
+          // No replacement for this column; keep the original field.
+          // If the REPLACE list contains unknown columns, they will be
+          // reported by throwIfUnknownReplaceColumns after the loop.
           // TODO: do real implicit collation here
           addOrExpandField(
               selectItems,
@@ -797,6 +887,7 @@ private boolean expandStar(List<SqlNode> selectItems, 
Set<String> aliases,
       throwIfUnknownExcludeColumns(excludeIdentifiers, excludeMatched);
       throwIfExcludeEliminatesAllColumns(excludeIdentifiers, fieldsBeforeStar,
           fields, identifier);
+      throwIfUnknownReplaceColumns(replaceMap, replaceMatched);
       return true;
     }
   }
@@ -903,6 +994,79 @@ private void 
throwIfExcludeEliminatesAllColumns(List<SqlIdentifier> excludeIdent
     }
   }
 
+  private static Map<String, SqlNode> extractReplaceMap(@Nullable SqlNodeList 
replaceList) {
+    if (replaceList == null) {
+      return ImmutableMap.of();
+    }
+    final ImmutableMap.Builder<String, SqlNode> builder = 
ImmutableMap.builder();
+    for (SqlNode node : replaceList) {
+      assert node instanceof SqlCall;
+      final SqlCall call = (SqlCall) node;
+      assert call.getOperator() == SqlStdOperatorTable.AS
+          && call.operandCount() == 2;
+      final SqlNode nameNode = call.operand(1);
+      assert nameNode instanceof SqlIdentifier;
+      final SqlIdentifier nameId = (SqlIdentifier) nameNode;
+      builder.put(nameId.isSimple() ? nameId.getSimple()
+          : nameId.names.get(nameId.names.size() - 1), call);
+    }
+    return builder.build();
+  }
+
+  private static @Nullable SqlNode findReplacement(String columnName,
+      Map<String, SqlNode> replaceMap, SqlNameMatcher nameMatcher) {
+    for (Map.Entry<String, SqlNode> entry : replaceMap.entrySet()) {
+      if (nameMatcher.matches(entry.getKey(), columnName)) {
+        final SqlNode value = entry.getValue();
+        return value instanceof SqlCall ? ((SqlCall) value).operand(0) : value;
+      }
+    }
+    return null;
+  }
+
+  private static void recordReplaceMatch(String columnName,
+      Map<String, SqlNode> replaceMap, SqlNameMatcher nameMatcher,
+      boolean[] matched) {
+    int i = 0;
+    for (Map.Entry<String, SqlNode> entry : replaceMap.entrySet()) {
+      if (!matched[i]
+          && nameMatcher.matches(entry.getKey(), columnName)) {
+        matched[i] = true;
+      }
+      i++;
+    }
+  }
+
+  private void throwIfUnknownReplaceColumns(Map<String, SqlNode> replaceMap,
+      boolean[] replaceMatched) {
+    if (replaceMap.isEmpty()) {
+      return;
+    }
+    final List<String> unknownReplaceNames = new ArrayList<>();
+    int firstUnknownIndex = -1;
+    int i = 0;
+    for (Map.Entry<String, SqlNode> entry : replaceMap.entrySet()) {
+      if (!replaceMatched[i]) {
+        if (firstUnknownIndex < 0) {
+          firstUnknownIndex = i;
+        }
+        unknownReplaceNames.add(entry.getKey());
+      }
+      i++;
+    }
+    if (firstUnknownIndex >= 0) {
+      final SqlNode firstUnknownExpr =
+          Iterables.get(replaceMap.values(), firstUnknownIndex);
+      final SqlNode errorNode = firstUnknownExpr instanceof SqlCall
+          ? ((SqlCall) firstUnknownExpr).operand(1)
+          : firstUnknownExpr;
+      throw newValidationError(
+          errorNode,
+          RESOURCE.selectStarReplaceListContainsUnknownColumns(
+              String.join(", ", unknownReplaceNames)));
+    }
+  }
+
   protected SqlNode maybeCast(SqlNode node, RelDataType currentType,
       RelDataType desiredType) {
     return SqlTypeUtil.equalSansNullability(typeFactory, currentType, 
desiredType)
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 ac137d058e..49a63bc4ef 100644
--- 
a/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties
+++ 
b/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties
@@ -267,7 +267,10 @@ 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/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}
+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
 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
diff --git a/site/_docs/reference.md b/site/_docs/reference.md
index d91b083072..bdf4ada199 100644
--- a/site/_docs/reference.md
+++ b/site/_docs/reference.md
@@ -244,9 +244,13 @@ ## Grammar
       *
   |   * EXCLUDE '(' column [, column ]* ')'
 
+starWithReplace:
+      *
+  |   * REPLACE '(' expression AS column [, expression AS 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` (or the alias `EXCEPT`) 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 enab [...]
+* `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.
 
 projectItem:
       expression [ [ AS ] columnAlias ]

Reply via email to