This is an automated email from the ASF dual-hosted git repository.
mihaibudiu pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/calcite.git
The following commit(s) were added to refs/heads/main by this push:
new c381906ef8 [CALCITE-7642] RelToSqlConverter may generate duplicate
aliases for internal derived relations in case-insensitive dialects
c381906ef8 is described below
commit c381906ef8df530f4ffc171f024b47b3fed61f4f
Author: zzwqqq <[email protected]>
AuthorDate: Tue Jul 21 11:18:07 2026 +0800
[CALCITE-7642] RelToSqlConverter may generate duplicate aliases for
internal derived relations in case-insensitive dialects
---
.../calcite/rel/rel2sql/RelToSqlConverter.java | 11 --
.../apache/calcite/rel/rel2sql/SqlImplementor.java | 192 ++++++++++++++++++---
.../calcite/rel/rel2sql/RelToSqlConverterTest.java | 96 +++++++++++
3 files changed, 263 insertions(+), 36 deletions(-)
diff --git
a/core/src/main/java/org/apache/calcite/rel/rel2sql/RelToSqlConverter.java
b/core/src/main/java/org/apache/calcite/rel/rel2sql/RelToSqlConverter.java
index dc91fca54e..44eec4feb9 100644
--- a/core/src/main/java/org/apache/calcite/rel/rel2sql/RelToSqlConverter.java
+++ b/core/src/main/java/org/apache/calcite/rel/rel2sql/RelToSqlConverter.java
@@ -574,7 +574,6 @@ public Result visit(Filter e) {
final Context context = x.qualifiedContext();
if (selectListRequired(context)) {
final ImmutableList.Builder<SqlNode> selectList =
ImmutableList.builder();
- // Fieldnames are unique since they are created by
SqlValidatorUtil.deriveJoinRowType()
final List<String> uniqueFieldNames =
input.getRowType().getFieldNames();
for (int i = 0; i < context.fieldCount; i++) {
final SqlNode field = context.field(i);
@@ -1584,16 +1583,6 @@ public List<SqlNode> createAsFullOperands(RelDataType
rowType, SqlNode leftOpera
return result;
}
- @Override public void addSelect(List<SqlNode> selectList, SqlNode node,
- RelDataType rowType) {
- String name = rowType.getFieldNames().get(selectList.size());
- @Nullable String alias = SqlValidatorUtil.alias(node);
- if (alias == null || !alias.equals(name)) {
- node = as(node, name);
- }
- selectList.add(node);
- }
-
private void parseCorrelTable(RelNode relNode, Result x) {
for (CorrelationId id : relNode.getVariablesSet()) {
correlTableMap.put(id, x.qualifiedContext());
diff --git
a/core/src/main/java/org/apache/calcite/rel/rel2sql/SqlImplementor.java
b/core/src/main/java/org/apache/calcite/rel/rel2sql/SqlImplementor.java
index 4b42df2eec..b65a797f14 100644
--- a/core/src/main/java/org/apache/calcite/rel/rel2sql/SqlImplementor.java
+++ b/core/src/main/java/org/apache/calcite/rel/rel2sql/SqlImplementor.java
@@ -40,7 +40,9 @@
import org.apache.calcite.rel.type.RelDataType;
import org.apache.calcite.rel.type.RelDataTypeFactory;
import org.apache.calcite.rel.type.RelDataTypeField;
+import org.apache.calcite.rel.type.RelDataTypeFieldImpl;
import org.apache.calcite.rel.type.RelDataTypeSystemImpl;
+import org.apache.calcite.rel.type.RelRecordType;
import org.apache.calcite.rex.RexBuilder;
import org.apache.calcite.rex.RexCall;
import org.apache.calcite.rex.RexCorrelVariable;
@@ -276,6 +278,11 @@ public abstract Result visitInput(RelNode e, int i,
boolean anon,
public void addSelect(List<SqlNode> selectList, SqlNode node,
RelDataType rowType) {
String name = rowType.getFieldNames().get(selectList.size());
+ addSelect(selectList, node, name);
+ }
+
+ private void addSelect(List<SqlNode> selectList, SqlNode node,
+ String name) {
@Nullable String alias = SqlValidatorUtil.alias(node);
if (alias == null || !alias.equals(name)) {
node = as(node, name);
@@ -283,6 +290,20 @@ public void addSelect(List<SqlNode> selectList, SqlNode
node,
selectList.add(node);
}
+ /** Returns a copy of a row type with different field names. */
+ private static RelDataType renameRowTypeFields(RelDataType rowType,
+ List<String> fieldNames) {
+ assert fieldNames.size() == rowType.getFieldCount();
+ final List<RelDataTypeField> fields = new ArrayList<>();
+ final List<RelDataTypeField> oldFields = rowType.getFieldList();
+ for (int i = 0; i < oldFields.size(); i++) {
+ fields.add(
+ new RelDataTypeFieldImpl(fieldNames.get(i), i,
+ oldFields.get(i).getType()));
+ }
+ return new RelRecordType(rowType.getStructKind(), fields,
rowType.isNullable());
+ }
+
/** Convenience method for creating column and table aliases.
*
* <p>{@code AS(e, "c")} creates "e AS c";
@@ -2061,13 +2082,16 @@ private Builder builder(RelNode rel, Set<Clause>
clauses) {
final Set<Clause> clauses2 = ignoreClauses ? ImmutableSet.of() : clauses;
final boolean needNew = needNewSubQuery(rel, this.clauses, clauses2);
assert needNew == this.needNew;
+ final Result input = needNew || node.getKind() != SqlKind.SELECT
+ ? forDerivedRelation()
+ : this;
SqlSelect select;
Expressions.FluentList<Clause> clauseList = Expressions.list();
if (needNew) {
- select = subSelect();
+ select = input.subSelect();
} else {
- select = asSelect();
- clauseList.addAll(this.clauses);
+ select = input.asSelect();
+ clauseList.addAll(input.clauses);
}
clauseList.appendAll(clauses);
final Context newContext;
@@ -2079,19 +2103,19 @@ private Builder builder(RelNode rel, Set<Clause>
clauses) {
newContext = selectListContext(selectList, aliasRef);
} else {
boolean qualified =
- !dialect.hasImplicitTableAlias() || aliases.size() > 1;
+ !dialect.hasImplicitTableAlias() || input.aliases.size() > 1;
// basically, we did a subSelect() since needNew is set and
neededAlias is not null
// now, we need to make sure that we need to update the alias context.
// if our aliases map has a single element: <neededAlias, rowType>,
// then we don't need to rewrite the alias but otherwise, it should be
updated.
if (needNew
&& neededAlias != null
- && (aliases.size() != 1 || !aliases.containsKey(neededAlias))) {
+ && (input.aliases.size() != 1 ||
!input.aliases.containsKey(neededAlias))) {
newAliases =
ImmutableMap.of(neededAlias, rel.getInput(0).getRowType());
newContext = aliasContext(newAliases, qualified);
} else {
- newContext = aliasContext(aliases, qualified);
+ newContext = aliasContext(input.aliases, qualified);
}
if (!dialect.supportGenerateSelectStar(rel.getInput(0))) {
// Rename each expanded column to its (unique) row-type field name.
@@ -2109,8 +2133,34 @@ private Builder builder(RelNode rel, Set<Clause>
clauses) {
select.setSelectList(new SqlNodeList(expandedSelectList, POS));
}
}
+ if (input != this) {
+ restoreOutputFieldNames(rel, select, newContext);
+ }
return new Builder(rel, clauseList, select, newContext, isAnon(),
- needNew && !aliases.containsKey(neededAlias) ? newAliases : aliases);
+ needNew && !input.aliases.containsKey(neededAlias) ? newAliases :
input.aliases);
+ }
+
+ /** Restores field names after an input was renamed for a derived
relation. */
+ private void restoreOutputFieldNames(RelNode rel, SqlSelect select,
+ Context context) {
+ final RelDataType rowType = rel.getRowType();
+ if (!select.getSelectList().equals(SqlNodeList.SINGLETON_STAR)
+ || context.fieldCount != rowType.getFieldCount()) {
+ return;
+ }
+ final List<String> fieldNames = rowType.getFieldNames();
+ // Project internal aliases back to the row type field names.
+ for (int i = 0; i < context.fieldCount; i++) {
+ final @Nullable String name = SqlValidatorUtil.alias(context.field(i));
+ if (name == null || !name.equals(fieldNames.get(i))) {
+ final List<SqlNode> selectList = new ArrayList<>();
+ for (int j = 0; j < context.fieldCount; j++) {
+ addSelect(selectList, context.field(j), rowType);
+ }
+ select.setSelectList(new SqlNodeList(selectList, POS));
+ return;
+ }
+ }
}
/** Returns whether a new sub-query is required. */
@@ -2519,18 +2569,104 @@ public Context qualifiedContext() {
return aliasContext(aliases, true);
}
+ /** Returns a result for use as a derived relation in the FROM clause of an
+ * enclosing query. Field names are made unique according to the dialect so
+ * that the enclosing query can reference them. */
+ private Result forDerivedRelation() {
+ if (neededType == null) {
+ return this;
+ }
+ final List<String> fieldNames =
+ SqlValidatorUtil.uniquify(neededType.getFieldNames(),
+ dialect.isCaseSensitive());
+ if (fieldNames.equals(neededType.getFieldNames())) {
+ return this;
+ }
+ final RelDataType type = renameRowTypeFields(neededType, fieldNames);
+ final SqlNode newNode = withOutputFieldNames(fieldNames);
+ final ImmutableMap.Builder<String, RelDataType> aliasBuilder =
+ ImmutableMap.builder();
+ for (Map.Entry<String, RelDataType> alias : aliases.entrySet()) {
+ aliasBuilder.put(alias.getKey(),
+ alias.getValue() == neededType ? type : alias.getValue());
+ }
+ return new Result(newNode, clauses, neededAlias, type,
aliasBuilder.build(), anon,
+ ignoreClauses, expectedClauses, expectedRel, forceExplicitAlias);
+ }
+
+ /** Returns this result's SQL node with {@code fieldNames} as its output
+ * field names.
+ *
+ * @param fieldNames Output field names
+ */
+ private SqlNode withOutputFieldNames(List<String> fieldNames) {
+ if (node.getKind() == SqlKind.AS) {
+ final SqlCall call = (SqlCall) node;
+ final List<SqlNode> operands = call.getOperandList();
+ // AS operands have the form [relation, relationAlias, fieldAlias0,
...],
+ // so field aliases start at index 2. If there is one alias per output
field,
+ // replace the field aliases with fieldNames.
+ final int fieldAliasStart = 2;
+ if (operands.size() == fieldNames.size() + fieldAliasStart) {
+ final List<SqlNode> newOperands = new ArrayList<>(operands.size());
+ newOperands.add(call.operand(0));
+ newOperands.add(call.operand(1));
+ for (String fieldName : fieldNames) {
+ newOperands.add(new SqlIdentifier(fieldName, POS));
+ }
+ return SqlStdOperatorTable.AS.createCall(POS, newOperands);
+ }
+ }
+ return withSelectFieldNames(fieldNames);
+ }
+
+ /** Returns this result as a SELECT whose items use {@code fieldNames}.
+ *
+ * @param fieldNames Names for the SELECT items
+ */
+ private SqlNode withSelectFieldNames(List<String> fieldNames) {
+ final SqlSelect select = asSelect();
+ final SqlNodeList selectList = select.getSelectList();
+ assert selectList.equals(SqlNodeList.SINGLETON_STAR)
+ || selectList.size() == fieldNames.size();
+ final List<SqlNode> newSelectList = new ArrayList<>();
+ final Context context =
+ aliasContext(aliases, !dialect.hasImplicitTableAlias() ||
aliases.size() > 1);
+ if (selectList.equals(SqlNodeList.SINGLETON_STAR)) {
+ for (int i = 0; i < fieldNames.size(); i++) {
+ addSelect(newSelectList, context.field(i), fieldNames.get(i));
+ }
+ } else {
+ for (int i = 0; i < fieldNames.size(); i++) {
+ SqlNode selectItem = selectList.get(i);
+ if (selectItem.getKind() == SqlKind.AS) {
+ selectItem = ((SqlCall) selectItem).operand(0);
+ }
+ if (selectItem instanceof SqlIdentifier
+ && ((SqlIdentifier) selectItem).isSimple()
+ && aliases.size() > 1) {
+ selectItem = context.field(i);
+ }
+ addSelect(newSelectList, selectItem, fieldNames.get(i));
+ }
+ }
+ select.setSelectList(new SqlNodeList(newSelectList, POS));
+ return select;
+ }
+
/**
* In join, when the left and right nodes have been generated,
* update their alias with 'neededAlias' if not null.
*/
public Result resetAlias() {
- if (neededAlias == null) {
- return this;
- } else {
- return new Result(node, clauses, neededAlias, neededType,
- ImmutableMap.of(neededAlias, castNonNull(neededType)), anon,
ignoreClauses,
- expectedClauses, expectedRel, false);
+ final Result input = forDerivedRelation();
+ if (input.neededAlias == null) {
+ return input;
}
+ return new Result(input.node, input.clauses, input.neededAlias,
input.neededType,
+ ImmutableMap.of(input.neededAlias, castNonNull(input.neededType)),
input.anon,
+ input.ignoreClauses, input.expectedClauses, input.expectedRel,
+ input.forceExplicitAlias);
}
/**
@@ -2540,9 +2676,12 @@ public Result resetAlias() {
* @param type type of the node associated with the alias
*/
public Result resetAlias(String alias, RelDataType type) {
- return new Result(node, clauses, alias, neededType,
- ImmutableMap.of(alias, type), anon, ignoreClauses,
- expectedClauses, expectedRel, false);
+ final Result input = forDerivedRelation();
+ final RelDataType aliasType =
+ input.neededType != null && neededType == type ? input.neededType :
type;
+ return new Result(input.node, input.clauses, alias, input.neededType,
+ ImmutableMap.of(alias, aliasType), input.anon, input.ignoreClauses,
+ input.expectedClauses, input.expectedRel, input.forceExplicitAlias);
}
/**
@@ -2554,17 +2693,20 @@ public Result resetAlias(String alias, RelDataType
type) {
* @return New Result with forced explicit alias
*/
public Result resetAliasForCorrelation(String alias, RelDataType type) {
+ final Result input = forDerivedRelation();
+ final RelDataType aliasType =
+ input.neededType != null && neededType == type ? input.neededType :
type;
return new Result(
- node,
- clauses,
+ input.node,
+ input.clauses,
alias,
- neededType,
- ImmutableMap.of(alias, type),
- anon,
- ignoreClauses,
- expectedClauses,
- expectedRel,
- true); // Force explicit alias
+ input.neededType,
+ ImmutableMap.of(alias, aliasType),
+ input.anon,
+ input.ignoreClauses,
+ input.expectedClauses,
+ input.expectedRel,
+ true);
}
/** Returns a copy of this Result, overriding the value of {@code anon}. */
diff --git
a/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java
b/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java
index 3fe9678233..a534d8c206 100644
---
a/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java
+++
b/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java
@@ -9862,6 +9862,102 @@ private void checkLiteral2(String expression, String
expected) {
}
};
+ /** Test cases for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-7642">[CALCITE-7642]
+ * RelToSqlConverter may generate duplicate aliases for internal derived
relations
+ * in case-insensitive dialects</a>. */
+ @Test void testCaseInsensitiveRootAliases() {
+ final SqlDialect mysqlDialect =
+ new MysqlSqlDialect(
+ MysqlSqlDialect.DEFAULT_CONTEXT.withCaseSensitive(false));
+ relFn(b -> b.values(new String[]{"id", "ID"}, 1, 2).build())
+ .dialect(mysqlDialect)
+ .ok("SELECT 1 AS `id`, 2 AS `ID`");
+ }
+
+ @Test void testCaseInsensitiveDerivedValuesAliases() {
+ final SqlDialect postgresqlDialect =
+ new PostgresqlSqlDialect(
+ PostgresqlSqlDialect.DEFAULT_CONTEXT.withCaseSensitive(false));
+ relFn(b -> b.values(new String[]{"id", "ID"}, 1, 2)
+ .filter(b.equals(b.field(1), b.literal(2)))
+ .build())
+ .dialect(postgresqlDialect)
+ .ok("SELECT \"id\", \"ID0\" AS \"ID\"\n"
+ + "FROM (VALUES (1, 2)) AS \"t\" (\"id\", \"ID0\")\n"
+ + "WHERE \"ID0\" = 2");
+ }
+
+ @Test void testCaseInsensitiveJoinAliases() {
+ final SqlDialect mysqlDialect =
+ new MysqlSqlDialect(
+ MysqlSqlDialect.DEFAULT_CONTEXT.withCaseSensitive(false));
+ relFn(b -> {
+ b.values(new String[]{"id"}, 1);
+ b.values(new String[]{"ID"}, 2);
+ final RelNode left = b.join(JoinRelType.INNER)
+ .project(b.fields(), ImmutableList.of(), true)
+ .build();
+ return b.push(left)
+ .values(new String[]{"x"}, 3)
+ .join(JoinRelType.INNER)
+ .project(ImmutableList.of(b.field(0), b.field(1)),
+ ImmutableList.of(), true)
+ .build();
+ }).dialect(mysqlDialect).ok("SELECT `t1`.`id`, `t1`.`ID0` AS `ID`\n"
+ + "FROM (SELECT `t`.`id`, `t0`.`ID` AS `ID0`\n"
+ + "FROM (SELECT 1 AS `id`) AS `t`,\n"
+ + "(SELECT 2 AS `ID`) AS `t0`) AS `t1`,\n"
+ + "(SELECT 3 AS `x`) AS `t2`");
+ }
+
+ @Test void testCaseInsensitiveCorrelateAliases() {
+ final SqlDialect postgresqlDialect =
+ new PostgresqlSqlDialect(
+ PostgresqlSqlDialect.DEFAULT_CONTEXT.withCaseSensitive(false));
+ relFn(b -> {
+ final Holder<RexCorrelVariable> v = Holder.empty();
+ return b.values(new String[]{"id", "ID"}, 1, 2)
+ .variable(v::set)
+ .values(new String[]{"x"}, 2)
+ .filter(
+ b.equals(b.field("x"),
+ b.getRexBuilder().makeFieldAccess(v.get(), 1)))
+ .correlate(JoinRelType.INNER, v.get().id, b.field(2, 0, 1))
+ .build();
+ }).dialect(postgresqlDialect).ok("SELECT *\n"
+ + "FROM (VALUES (1, 2)) AS \"$cor0\" (\"id\", \"ID0\"),\n"
+ + "LATERAL (SELECT *\n"
+ + "FROM (VALUES (2)) AS \"t0\" (\"x\")\n"
+ + "WHERE \"x\" = \"$cor0\".\"ID0\") AS \"t1\"");
+ }
+
+ @Test void testCaseInsensitiveCorrelatedProjectAliases() {
+ final SqlDialect postgresqlDialect =
+ new PostgresqlSqlDialect(
+ PostgresqlSqlDialect.DEFAULT_CONTEXT.withCaseSensitive(false));
+ relFn(b -> {
+ final Holder<RexCorrelVariable> v = Holder.empty();
+ return b.values(new String[]{"id", "ID"}, 1, 2)
+ .variable(v::set)
+ .project(
+ ImmutableList.of(
+ b.field(0),
+ b.scalarQuery(unused ->
+ b.values(new String[]{"x"}, 2)
+ .filter(
+ b.equals(b.field("x"),
+ b.getRexBuilder().makeFieldAccess(v.get(), 1)))
+ .project(b.field("x"))
+ .build())),
+ ImmutableList.of(), false, ImmutableList.of(v.get().id))
+ .build();
+ }).dialect(postgresqlDialect).ok("SELECT \"id\", (SELECT *\n"
+ + "FROM (VALUES (2)) AS \"t0\" (\"x\")\n"
+ + "WHERE \"x\" = \"t\".\"ID0\") AS \"$f1\"\n"
+ + "FROM (VALUES (1, 2)) AS \"t\" (\"id\", \"ID0\")");
+ }
+
/** Test case for
* <a
href="https://issues.apache.org/jira/browse/CALCITE-7483">[CALCITE-7483]
* RelToSqlConverter generates SELECT * despite
supportGenerateSelectStar</a>.