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>.

Reply via email to