From dd0756be56a6d8fcbac7c2570d462fe319cb279b Mon Sep 17 00:00:00 2001 From: xuzifu666 <1206332514@qq.com> Date: Wed, 13 May 2026 16:57:15 +0800 Subject: [PATCH] [CALCITE-7523] Support the syntax SELECT * REPLACE(expr as column) --- .../apache/calcite/test/BabelParserTest.java | 18 ++ .../org/apache/calcite/test/BabelTest.java | 73 ++++++++ babel/src/test/resources/sql/select.iq | 46 +++++ core/src/main/codegen/templates/Parser.jj | 52 ++++++ .../calcite/runtime/CalciteResource.java | 9 + .../apache/calcite/sql/SqlStarReplace.java | 84 +++++++++ .../sql/validate/SqlValidatorImpl.java | 164 ++++++++++++++++++ .../runtime/CalciteResource.properties | 3 + site/_docs/reference.md | 6 +- 9 files changed, 454 insertions(+), 1 deletion(-) create mode 100644 core/src/main/java/org/apache/calcite/sql/SqlStarReplace.java 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 (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); + } + | + { 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 (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)); + } +} + <#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 ]