This is an automated email from the ASF dual-hosted git repository.

jhyde pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/calcite.git


The following commit(s) were added to refs/heads/master by this push:
     new c2527cc  [CALCITE-4305] Implicit column alias for single-column 
VALUES, and UNNEST of ARRAY and MULTISET constructors
c2527cc is described below

commit c2527ccf440f7750bfbabd2063c402440c5b32a0
Author: Julian Hyde <[email protected]>
AuthorDate: Fri Oct 23 23:48:32 2020 -0700

    [CALCITE-4305] Implicit column alias for single-column VALUES, and UNNEST 
of ARRAY and MULTISET constructors
---
 .../calcite/sql/validate/AliasNamespace.java       |  76 ++++---
 .../calcite/sql/validate/SqlValidatorImpl.java     |   8 +-
 .../org/apache/calcite/test/SqlValidatorTest.java  |  36 +++-
 core/src/test/resources/sql/unnest.iq              | 226 +++++++++++++++++++++
 4 files changed, 318 insertions(+), 28 deletions(-)

diff --git 
a/core/src/main/java/org/apache/calcite/sql/validate/AliasNamespace.java 
b/core/src/main/java/org/apache/calcite/sql/validate/AliasNamespace.java
index 38222d7..fc29736 100644
--- a/core/src/main/java/org/apache/calcite/sql/validate/AliasNamespace.java
+++ b/core/src/main/java/org/apache/calcite/sql/validate/AliasNamespace.java
@@ -25,9 +25,9 @@ import org.apache.calcite.sql.SqlNode;
 import org.apache.calcite.sql.SqlNodeList;
 import org.apache.calcite.sql.fun.SqlStdOperatorTable;
 import org.apache.calcite.sql.parser.SqlParserPos;
+import org.apache.calcite.util.Pair;
 import org.apache.calcite.util.Util;
 
-import java.util.ArrayList;
 import java.util.List;
 
 import static org.apache.calcite.util.Static.RESOURCE;
@@ -60,42 +60,66 @@ public class AliasNamespace extends AbstractNamespace {
     super(validator, enclosingNode);
     this.call = call;
     assert call.getOperator() == SqlStdOperatorTable.AS;
+    assert call.operandCount() >= 2;
   }
 
   //~ Methods ----------------------------------------------------------------
 
+  @Override public boolean supportsModality(SqlModality modality) {
+    final List<SqlNode> operands = call.getOperandList();
+    final SqlValidatorNamespace childNs =
+        validator.getNamespace(operands.get(0));
+    return childNs.supportsModality(modality);
+  }
+
   @Override protected RelDataType validateImpl(RelDataType targetRowType) {
-    final List<String> nameList = new ArrayList<>();
     final List<SqlNode> operands = call.getOperandList();
     final SqlValidatorNamespace childNs =
         validator.getNamespace(operands.get(0));
     final RelDataType rowType = childNs.getRowTypeSansSystemColumns();
-    final List<SqlNode> columnNames = Util.skip(operands, 2);
-    for (final SqlNode operand : columnNames) {
-      String name = ((SqlIdentifier) operand).getSimple();
-      if (nameList.contains(name)) {
-        throw validator.newValidationError(operand,
-            RESOURCE.aliasListDuplicate(name));
+    final RelDataType aliasedType;
+    if (operands.size() == 2) {
+      // 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.
+      if (rowType.getFieldCount() == 1) {
+        aliasedType = validator.getTypeFactory().builder()
+            .kind(rowType.getStructKind())
+            .add(((SqlIdentifier) operands.get(1)).getSimple(),
+                rowType.getFieldList().get(0).getType())
+            .build();
+      } else {
+        aliasedType = rowType;
       }
-      nameList.add(name);
-    }
-    if (nameList.size() != rowType.getFieldCount()) {
-      // Position error over all column names
-      final SqlNode node = operands.size() == 3
-          ? operands.get(2)
-          : new SqlNodeList(columnNames, SqlParserPos.sum(columnNames));
-      throw validator.newValidationError(node,
-          RESOURCE.aliasListDegree(rowType.getFieldCount(), getString(rowType),
-              nameList.size()));
-    }
-    final List<RelDataType> typeList = new ArrayList<>();
-    for (RelDataTypeField field : rowType.getFieldList()) {
-      typeList.add(field.getType());
+    } else {
+      // Alias is 'AS t (c0, ..., cN)'
+      final List<SqlNode> columnNames = Util.skip(operands, 2);
+      final List<String> nameList =
+          Util.transform(columnNames, operand ->
+              ((SqlIdentifier) operand).getSimple());
+      final int i = Util.firstDuplicate(nameList);
+      if (i >= 0) {
+        final SqlIdentifier id = (SqlIdentifier) columnNames.get(i);
+        throw validator.newValidationError(id,
+            RESOURCE.aliasListDuplicate(id.getSimple()));
+      }
+      if (columnNames.size() != rowType.getFieldCount()) {
+        // Position error over all column names
+        final SqlNode node = operands.size() == 3
+            ? operands.get(2)
+            : new SqlNodeList(columnNames, SqlParserPos.sum(columnNames));
+        throw validator.newValidationError(node,
+            RESOURCE.aliasListDegree(rowType.getFieldCount(),
+                getString(rowType), columnNames.size()));
+      }
+      aliasedType = validator.getTypeFactory().builder()
+          .addAll(
+              Util.transform(rowType.getFieldList(), f ->
+                  Pair.of(nameList.get(f.getIndex()), f.getType())))
+          .kind(rowType.getStructKind())
+          .build();
     }
-    final RelDataType aliasedType = 
validator.getTypeFactory().createStructType(
-        rowType.getStructKind(),
-        typeList,
-        nameList);
 
     // As per suggestion in CALCITE-4085, JavaType has its special nullability 
handling.
     if (rowType instanceof RelDataTypeFactoryImpl.JavaType) {
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 ba35baf..1c823d1 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
@@ -2186,8 +2186,14 @@ public class SqlValidatorImpl implements 
SqlValidatorWithHints {
       if (alias == null) {
         alias = call.operand(1).toString();
       }
-      final boolean needAlias = call.operandCount() > 2;
       expr = call.operand(0);
+      final boolean needAlias = call.operandCount() > 2
+          || expr.getKind() == SqlKind.VALUES
+          || expr.getKind() == SqlKind.UNNEST
+          && (((SqlCall) expr).operand(0).getKind()
+                  == SqlKind.ARRAY_VALUE_CONSTRUCTOR
+              || ((SqlCall) expr).operand(0).getKind()
+                  == SqlKind.MULTISET_VALUE_CONSTRUCTOR);
       newExpr =
           registerFrom(
               parentScope,
diff --git a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java 
b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
index 57b80a4..beb29f8 100644
--- a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
+++ b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
@@ -7989,6 +7989,38 @@ public class SqlValidatorTest extends 
SqlValidatorTestCase {
         .type("RecordType(INTEGER NOT NULL KEY, INTEGER NOT NULL VALUE) NOT 
NULL");
   }
 
+  /** Test case for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-4305";>[CALCITE-4305]
+   * Implicit column alias for single-column UNNEST</a>. */
+  @Test void testUnnestAlias() {
+    final String expectedType = "RecordType(CHAR(6) NOT NULL FRUIT) NOT NULL";
+
+    // When UNNEST produces a single column, and you use an alias for the
+    // relation, that alias becomes the name of the column.
+    sql("select fruit.* from UNNEST(array ['apple', 'banana']) as fruit")
+        .type(expectedType);
+
+    // The magic doesn't happen if the query is not an UNNEST.
+    // In this case, the query is a SELECT.
+    sql("SELECT fruit.*\n"
+        + "FROM (\n"
+        + "  SELECT * FROM UNNEST(array ['apple', 'banana']) as x) as fruit")
+        .type("RecordType(CHAR(6) NOT NULL X) NOT NULL");
+
+    // The magic doesn't happen if the UNNEST yields more than one column.
+    sql("select * from UNNEST(array [('apple', 1), ('banana', 2)]) as fruit")
+        .type("RecordType(CHAR(6) NOT NULL EXPR$0, INTEGER NOT NULL EXPR$1) "
+            + "NOT NULL");
+
+    // VALUES gets the same treatment as ARRAY. (Unlike PostgreSQL.)
+    sql("select * from (values ('apple'), ('banana')) as fruit")
+        .type("RecordType(CHAR(6) NOT NULL FRUIT) NOT NULL");
+
+    // UNNEST MULTISET gets the same treatment as UNNEST ARRAY.
+    sql("select * from unnest(multiset [1, 2, 1]) as f")
+        .type("RecordType(INTEGER NOT NULL F) NOT NULL");
+  }
+
   @Test void testCorrelationJoin() {
     sql("select *,"
         + "         multiset(select * from emp where deptno=dept.deptno) "
@@ -10336,7 +10368,9 @@ public class SqlValidatorTest extends 
SqlValidatorTestCase {
   }
 
   @Test void testStreamValues() {
-    sql("select stream * from (^values 1^) as e")
+    sql("select stream * from (^values 1) as e^")
+        .fails(cannotConvertToStream("E"));
+    sql("select stream * from (^values 1) as e (c)^")
         .fails(cannotConvertToStream("E"));
     sql("select stream orderId from orders\n"
         + "union all\n"
diff --git a/core/src/test/resources/sql/unnest.iq 
b/core/src/test/resources/sql/unnest.iq
new file mode 100644
index 0000000..77e1598
--- /dev/null
+++ b/core/src/test/resources/sql/unnest.iq
@@ -0,0 +1,226 @@
+# unnest.iq - Tests for UNNEST
+#
+# 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.
+#
+!use scott
+!set outputformat mysql
+
+select *
+from unnest(array [(1, 'a'), (2, 'b')]) as t (x, y);
++---+---+
+| X | Y |
++---+---+
+| 1 | a |
+| 2 | b |
++---+---+
+(2 rows)
+
+!ok
+
+select *
+from UNNEST(array ['apple', 'banana']) as fruit (fruit);
++--------+
+| FRUIT  |
++--------+
+| apple  |
+| banana |
++--------+
+(2 rows)
+
+!ok
+
+# When UNNEST produces a single column, and you use an alias for the
+# relation, that alias becomes the name of the column.
+select *
+from UNNEST(array ['apple', 'banana']) as fruit;
++--------+
+| FRUIT  |
++--------+
+| apple  |
+| banana |
++--------+
+(2 rows)
+
+!ok
+
+select fruit.*
+from UNNEST(array ['apple', 'banana']) as fruit;
++--------+
+| FRUIT  |
++--------+
+| apple  |
+| banana |
++--------+
+(2 rows)
+
+!ok
+
+select fruit
+from UNNEST(array ['apple', 'banana']) as fruit;
++--------+
+| FRUIT  |
++--------+
+| apple  |
+| banana |
++--------+
+(2 rows)
+
+!ok
+
+select fruit.fruit
+from UNNEST(array ['apple', 'banana']) as fruit;
++--------+
+| FRUIT  |
++--------+
+| apple  |
+| banana |
++--------+
+(2 rows)
+
+!ok
+
+# As previous, but quoted
+select *
+from UNNEST(array ['apple', 'banana']) as "fruit";
++--------+
+| fruit  |
++--------+
+| apple  |
+| banana |
++--------+
+(2 rows)
+
+!ok
+
+# If UNNEST is not the direct child of the AS, aliasing doesn't happen.
+SELECT fruit.*
+FROM (
+  SELECT *
+  FROM UNNEST(array ['apple', 'banana']) as x) as fruit;
++--------+
+| X      |
++--------+
+| apple  |
+| banana |
++--------+
+(2 rows)
+
+!ok
+
+# If UNNEST applies to a value that is not an array constructor,
+# aliasing doesn't happen. Thus the last column is 'EXPR$0', not 'z'.
+SELECT *
+FROM (
+    SELECT x, collect(y) as ys
+    FROM (VALUES (1, 1), (2, 2), (1, 3)) AS t (x, y)
+    GROUP BY x) AS u,
+  UNNEST(u.ys) AS z;
++---+--------+--------+
+| X | YS     | EXPR$0 |
++---+--------+--------+
+| 1 | [1, 3] |      1 |
+| 1 | [1, 3] |      3 |
+| 2 | [2]    |      2 |
++---+--------+--------+
+(3 rows)
+
+!ok
+
+# If VALUES is not the direct child of the AS, aliasing doesn't happen.
+SELECT fruit.*
+FROM (
+  SELECT *
+  FROM (VALUES 'apple', 'banana') as x) as fruit;
++--------+
+| X      |
++--------+
+| apple  |
+| banana |
++--------+
+(2 rows)
+
+!ok
+
+# If UNNEST yields more than one column, the aliasing doesn't happen.
+select *
+from UNNEST(array [('apple', 1), ('banana', 2)]) as fruit;
++--------+--------+
+| EXPR$0 | EXPR$1 |
++--------+--------+
+| apple  |      1 |
+| banana |      2 |
++--------+--------+
+(2 rows)
+
+!ok
+
+# MULTISET gets the same treatment as UNNEST.
+select *
+from unnest(multiset [1, 2, 1]) as v;
++---+
+| V |
++---+
+| 1 |
+| 1 |
+| 2 |
++---+
+(3 rows)
+
+!ok
+
+# VALUES gets the same treatment as UNNEST.
+#
+# This goes beyond PostgreSQL. In PostgreSQL, the following
+# query yields a column called "column1", not "fruit":
+#     select * from (values (1), (2)) as fruit
+#
+select *
+from (values ('apple'), ('banana')) as fruit;
++--------+
+| FRUIT  |
++--------+
+| apple  |
+| banana |
++--------+
+(2 rows)
+
+!ok
+
+select upper(f)
+from (values 'apple', 'banana', 'pear') as f;
++--------+
+| EXPR$0 |
++--------+
+| APPLE  |
+| BANANA |
+| PEAR   |
++--------+
+(3 rows)
+
+!ok
+
+SELECT AVG(x) as "avg"
+FROM UNNEST(array [0, 2, 4, 4, 5]) as x;
++-----+
+| avg |
++-----+
+|   3 |
++-----+
+(1 row)
+
+!ok
+
+# End unnest.iq

Reply via email to