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 f458a174da6c..2061d5f6752a 100644
--- a/babel/src/test/java/org/apache/calcite/test/BabelParserTest.java
+++ b/babel/src/test/java/org/apache/calcite/test/BabelParserTest.java
@@ -162,6 +162,24 @@ class BabelParserTest extends SqlParserTest {
sql(sql3).ok(expected3);
}
+ /** Test case for
+ * [CALCITE-7532] Support the syntax SELECT * REPLACE(expr as column).
+ * */
+ @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 6970280ed436..56e5287c4874 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
+ * [CALCITE-7532] Support the syntax SELECT * REPLACE(expr as column). */
+ @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 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 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 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 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 c969ad76559e..f94905c16e9e 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 16b80218e59d..8152817cd9a8 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;
@@ -2017,6 +2018,7 @@ SqlNode SelectExpression() :
{
SqlNode e;
SqlNodeList excludeList;
+ SqlNodeList replaceList;
}
{
(
@@ -2027,6 +2029,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(),
@@ -2043,10 +2046,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;
@@ -2067,6 +2090,35 @@ SqlNodeList StarExcludeList() :
return new SqlNodeList(list, s.end(this));
}
}
+#if>
+
+<#if (parser.includeStarExclude!default.parser.includeStarExclude)>
+SqlNodeList StarReplaceList() :
+{
+ final Span s;
+ final List list = new ArrayList();
+ SqlNode expr;
+ SqlIdentifier id;
+}
+{
+ { s = span(); }
+ expr = Expression(ExprContext.ACCEPT_SUB_QUERY) id = CompoundIdentifier() {
+ list.add(SqlStdOperatorTable.AS.createCall(
+ SqlParserPos.sum(ImmutableList.of(expr.getParserPosition(),
+ id.getParserPosition())), expr, id));
+ }
+ (
+ expr = Expression(ExprContext.ACCEPT_SUB_QUERY) id = CompoundIdentifier() {
+ list.add(SqlStdOperatorTable.AS.createCall(
+ SqlParserPos.sum(ImmutableList.of(expr.getParserPosition(),
+ id.getParserPosition())), expr, id));
+ }
+ )*
+ {
+ 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 baf5fca10e04..e00643a56734 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 illegalArgumentForTableFunctionCall(String a0,
@BaseMessage("EXCLUDE/EXCEPT clause must follow a STAR expression")
ExInst selectExcludeRequiresStar();
+ @BaseMessage("REPLACE clause must follow a STAR expression")
+ ExInst selectReplaceRequiresStar();
+
@BaseMessage("SELECT * EXCLUDE/EXCEPT list contains unknown column(s): {0}")
ExInst selectStarExcludeListContainsUnknownColumns(String columns);
@BaseMessage("SELECT * EXCLUDE/EXCEPT list cannot exclude all columns")
ExInst selectStarExcludeCannotExcludeAllColumns();
+ @BaseMessage("SELECT * REPLACE list contains unknown column(s): {0}")
+ ExInst selectStarReplaceListContainsUnknownColumns(String columns);
+
+ @BaseMessage("SELECT * REPLACE list contains duplicate column(s): {0}")
+ ExInst selectStarReplaceListContainsDuplicateColumns(String columns);
+
@BaseMessage("Group function ''{0}'' can only appear in GROUP BY clause")
ExInst 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 000000000000..b5149a640379
--- /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 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 6881bae03525..158e811e2c20 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 selectItems, Set 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 selectItems, Set aliases,
final boolean[] excludeMatched = new boolean[excludeIdentifiers.size()];
final SqlNameMatcher nameMatcher =
scope.validator.catalogReader.nameMatcher();
+ if (replaceList != null) {
+ final Set 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 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 selectItems, Set 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 selectItems, Set aliases,
throwIfUnknownExcludeColumns(excludeIdentifiers, excludeMatched);
throwIfExcludeEliminatesAllColumns(excludeIdentifiers, fieldsBeforeStar,
fields, identifier);
+ throwIfUnknownReplaceColumns(replaceMap, replaceMatched);
return true;
default:
@@ -781,6 +846,31 @@ private boolean expandStar(List selectItems, Set 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 selectItems, Set aliases,
throwIfUnknownExcludeColumns(excludeIdentifiers, excludeMatched);
throwIfExcludeEliminatesAllColumns(excludeIdentifiers, fieldsBeforeStar,
fields, identifier);
+ throwIfUnknownReplaceColumns(replaceMap, replaceMatched);
return true;
}
}
@@ -903,6 +994,79 @@ private void throwIfExcludeEliminatesAllColumns(List excludeIdent
}
}
+ private static Map extractReplaceMap(@Nullable SqlNodeList replaceList) {
+ if (replaceList == null) {
+ return ImmutableMap.of();
+ }
+ final ImmutableMap.Builder 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 replaceMap, SqlNameMatcher nameMatcher) {
+ for (Map.Entry 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 replaceMap, SqlNameMatcher nameMatcher,
+ boolean[] matched) {
+ int i = 0;
+ for (Map.Entry entry : replaceMap.entrySet()) {
+ if (!matched[i]
+ && nameMatcher.matches(entry.getKey(), columnName)) {
+ matched[i] = true;
+ }
+ i++;
+ }
+ }
+
+ private void throwIfUnknownReplaceColumns(Map replaceMap,
+ boolean[] replaceMatched) {
+ if (replaceMap.isEmpty()) {
+ return;
+ }
+ final List unknownReplaceNames = new ArrayList<>();
+ int firstUnknownIndex = -1;
+ int i = 0;
+ for (Map.Entry 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 ac137d058e70..49a63bc4efdf 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 1c13f10324dc..22634686f631 100644
--- a/site/_docs/reference.md
+++ b/site/_docs/reference.md
@@ -244,9 +244,13 @@ starWithExclude:
*
| * 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 enables the same syntax for other components that need it.
+* `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 ]