vlsi commented on code in PR #5271:
URL: https://github.com/apache/calcite/pull/5271#discussion_r4149677928


##########
core/src/main/java/org/apache/calcite/rel/type/SingleColumnAliasRelDataType.java:
##########
@@ -59,7 +63,7 @@ public SingleColumnAliasRelDataType(RelDataType original, 
RelDataType alias) {
   }
 
   @Override public int getFieldCount() {
-    return 1;
+    return original.getFieldCount();
   }

Review Comment:
   `getField` looks up a name in `original` first and in the alias second. With 
a single-field `original`, both lookups find the same column, so the order did 
not matter. With `WITH ORDINALITY`, `original` has a second field, and a name 
that matches it wins over the alias:
   
   | Query | Base | This PR |
   | --- | --- | --- |
   | `select * from R with ordinality as ordinality` | `Column 'ORDINALITY' is 
ambiguous` | `(A, ORDINALITY)` |
   | `select ordinality from R with ordinality as ordinality` | `Column 
'ORDINALITY' is ambiguous` | `(ORDINALITY)`, the plan projects `$1` |
   | `select ordinality.a from R with ordinality as ordinality` | `Column 'A' 
not found in table 'ORDINALITY'` | `(A)` |
   
   The base failed because its row type had two fields named `ORDINALITY`, so 
the new results look like an improvement. What no test pins is that the 
unqualified `ordinality` means the ordinality column and not the data column: 
swapping the two lookups here leaves the suite green. Please add the second 
query to `testUnnestAlias`, and state the lookup order in the Javadoc: a name 
that matches a field of the wrapped type resolves to that field, and the alias 
applies only to a name that matches none.



##########
core/src/main/java/org/apache/calcite/rel/type/SingleColumnAliasRelDataType.java:
##########
@@ -27,20 +27,24 @@
 import java.util.List;
 
 /**
- * Specific type of RelDataType that corresponds to a single column table,
- * where column can have alias.
+ * Specific type of RelDataType where the first column can also be
+ * referenced by an alias.
  *
  * <p>For instance:
  * <blockquote><pre>select rmp, rmp.i from table(ramp(3)) as 
rmp;</pre></blockquote>
  *
+ * <p>{@code original} may have more than one field, for example a
+ * struct-array UNNEST WITH ORDINALITY, whose second field is ORDINALITY;
+ * only the first field gets the extra alias name.

Review Comment:
   - "struct-array UNNEST" is the term that was replaced in `AliasNamespace` in 
this round. "UNNEST WITH ORDINALITY of a collection of single-field ROW values" 
says the same with words the code uses.
   - `{@code original}` refers to a constructor parameter the class comment 
does not introduce. "The wrapped type may have more than one field" reads 
without the constructor.
   - The paragraph says which field gets the alias, but not which name wins 
when the alias equals the name of another field (see the comment on 
`getFieldCount`).



##########
core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java:
##########
@@ -10101,6 +10101,72 @@ void testGroupExpressionEquivalenceParams() {
     sql("select * from (SELECT ARRAY['banana'] as fruits) as t, 
UNNEST(t.fruits)")
         .type("RecordType(CHAR(6) NOT NULL ARRAY NOT NULL FRUITS, "
             + "CHAR(6) NOT NULL EXPR$0) NOT NULL").ok();
+
+    // Test case for [CALCITE-7791] UNNEST of a single-field ROW array loses
+    // the field name when aliased. The field name "A" survives
+    // star-expansion. The alias "D" still names the column and
+    // qualifies the field.
+    sql("select d, d.a\n"
+        + "from unnest(cast(array[row(1)] as row(a integer) array)) as d")
+        .type("RecordType(INTEGER NOT NULL D, INTEGER NOT NULL A) NOT NULL");
+
+    // A sub-query operand keeps the field name as well.
+    sql("select *\n"
+        + "from unnest(\n"
+        + "  (select cast(array[row(1)] as row(a integer) array)\n"
+        + "   from (values (1)))) as d")
+        .type("RecordType(INTEGER NOT NULL A) NOT NULL");
+
+    // WITH ORDINALITY keeps the field name and the alias still
+    // names the data column.
+    sql("select *\n"
+        + "from unnest(cast(array[row(1)] as row(a integer) array))\n"
+        + "  with ordinality as d")
+        .type("RecordType(INTEGER NOT NULL A, INTEGER NOT NULL ORDINALITY) NOT 
NULL");
+    sql("select d, d.a\n"
+        + "from unnest(cast(array[row(1)] as row(a integer) array))\n"
+        + "  with ordinality as d")
+        .type("RecordType(INTEGER NOT NULL D, INTEGER NOT NULL A) NOT NULL");

Review Comment:
   A few `WITH ORDINALITY` queries that this round fixed have no test:
   
   | Query | Base | `f407a58297` | This PR |
   | --- | --- | --- | --- |
   | `select d from unnest((select cast(array[row(1)] as row(a integer) array) 
from (values (1)))) with ordinality as d` | `(D)` | `Column 'D' not found in 
any table` | `(D)` |
   | `select * from R with ordinality as d join (values 1) t(d) using (d)` | 
`(D, ORDINALITY)` | `Column 'D' not found in any table` | `(A, ORDINALITY)` |
   | a correlated `lateral unnest(t.x) with ordinality as d` that selects `d, 
d.a, d.ordinality` | `Column 'A' not found in table 'D'` | `Column 'D' not 
found in any table` | resolves, `LogicalCorrelate` in the plan |
   
   The sub-query row is the third row of the table in the [WITH ORDINALITY 
thread](https://github.com/apache/calcite/pull/5271#discussion_r4136654657). 
Please add at least that one and the `USING (d)` one next to these two cases. 
`SqlToRelConverterTest` pins the plan only for a plain UNNEST; a `WITH 
ORDINALITY` case there would show `LogicalProject(D=[$0], A=[$0], 
ORDINALITY=[$1])`.



##########
core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java:
##########
@@ -10101,6 +10101,72 @@ void testGroupExpressionEquivalenceParams() {
     sql("select * from (SELECT ARRAY['banana'] as fruits) as t, 
UNNEST(t.fruits)")
         .type("RecordType(CHAR(6) NOT NULL ARRAY NOT NULL FRUITS, "
             + "CHAR(6) NOT NULL EXPR$0) NOT NULL").ok();
+
+    // Test case for [CALCITE-7791] UNNEST of a single-field ROW array loses
+    // the field name when aliased. The field name "A" survives
+    // star-expansion. The alias "D" still names the column and
+    // qualifies the field.
+    sql("select d, d.a\n"
+        + "from unnest(cast(array[row(1)] as row(a integer) array)) as d")
+        .type("RecordType(INTEGER NOT NULL D, INTEGER NOT NULL A) NOT NULL");
+
+    // A sub-query operand keeps the field name as well.
+    sql("select *\n"
+        + "from unnest(\n"
+        + "  (select cast(array[row(1)] as row(a integer) array)\n"
+        + "   from (values (1)))) as d")
+        .type("RecordType(INTEGER NOT NULL A) NOT NULL");
+
+    // WITH ORDINALITY keeps the field name and the alias still
+    // names the data column.
+    sql("select *\n"
+        + "from unnest(cast(array[row(1)] as row(a integer) array))\n"
+        + "  with ordinality as d")
+        .type("RecordType(INTEGER NOT NULL A, INTEGER NOT NULL ORDINALITY) NOT 
NULL");
+    sql("select d, d.a\n"
+        + "from unnest(cast(array[row(1)] as row(a integer) array))\n"
+        + "  with ordinality as d")
+        .type("RecordType(INTEGER NOT NULL D, INTEGER NOT NULL A) NOT NULL");
+
+    // UNNEST MULTISET gets the same treatment.
+    sql("select *\n"
+        + "from unnest(cast(multiset[row(1)] as row(a integer) multiset)) as 
d")
+        .type("RecordType(INTEGER NOT NULL A) NOT NULL");
+
+    // NATURAL JOIN and USING match the field name "A", not the alias "D".
+    // A column named after the alias does not count as a match.

Review Comment:
   "NATURAL JOIN and USING match the field name "A", not the alias "D"" holds 
for `NATURAL JOIN` only. The `USING (d)` case at the end of this group shows 
that `USING` accepts the alias too. For example: "NATURAL JOIN matches the 
field name "A", not the alias "D". USING accepts either name.", keeping the 
next line as it is.



##########
core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java:
##########
@@ -10101,6 +10101,72 @@ void testGroupExpressionEquivalenceParams() {
     sql("select * from (SELECT ARRAY['banana'] as fruits) as t, 
UNNEST(t.fruits)")
         .type("RecordType(CHAR(6) NOT NULL ARRAY NOT NULL FRUITS, "
             + "CHAR(6) NOT NULL EXPR$0) NOT NULL").ok();
+
+    // Test case for [CALCITE-7791] UNNEST of a single-field ROW array loses
+    // the field name when aliased. The field name "A" survives
+    // star-expansion. The alias "D" still names the column and
+    // qualifies the field.
+    sql("select d, d.a\n"
+        + "from unnest(cast(array[row(1)] as row(a integer) array)) as d")
+        .type("RecordType(INTEGER NOT NULL D, INTEGER NOT NULL A) NOT NULL");
+
+    // A sub-query operand keeps the field name as well.
+    sql("select *\n"
+        + "from unnest(\n"
+        + "  (select cast(array[row(1)] as row(a integer) array)\n"
+        + "   from (values (1)))) as d")
+        .type("RecordType(INTEGER NOT NULL A) NOT NULL");
+
+    // WITH ORDINALITY keeps the field name and the alias still
+    // names the data column.
+    sql("select *\n"
+        + "from unnest(cast(array[row(1)] as row(a integer) array))\n"
+        + "  with ordinality as d")
+        .type("RecordType(INTEGER NOT NULL A, INTEGER NOT NULL ORDINALITY) NOT 
NULL");
+    sql("select d, d.a\n"
+        + "from unnest(cast(array[row(1)] as row(a integer) array))\n"
+        + "  with ordinality as d")
+        .type("RecordType(INTEGER NOT NULL D, INTEGER NOT NULL A) NOT NULL");
+
+    // UNNEST MULTISET gets the same treatment.
+    sql("select *\n"
+        + "from unnest(cast(multiset[row(1)] as row(a integer) multiset)) as 
d")
+        .type("RecordType(INTEGER NOT NULL A) NOT NULL");
+
+    // NATURAL JOIN and USING match the field name "A", not the alias "D".
+    // A column named after the alias does not count as a match.
+    sql("select *\n"
+        + "from unnest(cast(array[row(1)] as row(a integer) array)) as d\n"
+        + "natural join (select 1 as a) as t")
+        .type("RecordType(INTEGER NOT NULL A) NOT NULL");
+    sql("select *\n"
+        + "from unnest(cast(array[row(1)] as row(a integer) array)) as d\n"
+        + "natural join (select 1 as d) as t")
+        .type("RecordType(INTEGER NOT NULL A, INTEGER NOT NULL D) NOT NULL");
+    sql("select *\n"
+        + "from unnest(cast(array[row(1)] as row(a integer) array)) as d\n"
+        + "join (values (1)) as t (a) using (a)")
+        .type("RecordType(INTEGER NOT NULL A) NOT NULL");
+    // USING also accepts the alias, which resolves through the field.

Review Comment:
   "which resolves through the field" does not say what the reader should 
check. The assertion shows the outcome, so the comment can state it: "USING (d) 
matches the alias, and the output column is named A."



##########
core/src/main/java/org/apache/calcite/sql/validate/AliasNamespace.java:
##########
@@ -100,29 +100,36 @@ protected AliasNamespace(
     final RelDataType aliasedType;
     if (operands.size() == 2) {
       final SqlNode node = operands.get(0);
-      // Alias is 'AS t' (no column list).
-      // If the sub-query is UNNEST or VALUES,
-      // and the sub-query has one column,
-      // then the namespace's sole column is named after the alias.
+      // 'AS t': the sole column is named after the alias, unless it is a
+      // field of the collection's ROW element. COLLECTION_TABLE's sole
+      // column keeps its own name the same way.
+      final boolean isStructUnnest = node.getKind() == SqlKind.UNNEST
+          && keepsElementFieldName((SqlCall) node);

Review Comment:
   - "the sole column is named after the alias, unless it is a field of the 
collection's ROW element" says that such a field does not get the alias. It 
gets both names: the field keeps its own name for star expansion, and the alias 
names it as well.
   - The comment now covers the `WITH ORDINALITY` branch too, where there are 
two columns, so "the sole column" is too narrow.
   - The method is now `keepsElementFieldName`, and the variable that stores 
its result is still `isStructUnnest`. The same name for both would be easier to 
follow.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to