You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@calcite.apache.org by jh...@apache.org on 2021/12/18 05:19:53 UTC

[calcite] 01/02: [CALCITE-4946] Add method RelBuilder.size()

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

commit 607c45b7a57b66e50f0a4826b5a80df043199f79
Author: Julian Hyde <jh...@apache.org>
AuthorDate: Thu Jul 22 13:32:48 2021 -0500

    [CALCITE-4946] Add method RelBuilder.size()
    
    Implement RelJson.toJson(Object) for an object of type
    RelDataType.
    
    Continuing the work started in [CALCITE-4719], implement
    RexSubQuery.array and array query constructor, e.g.
    
      SELECT ARRAY (SELECT ... FROM ...) AS lineItems
      FROM Orders
    
    Generalize Collect to be able to produce columns of type
    ARRAY and MAP, not just MULTISET.
    
    Add couple of test cases for USING.
---
 .../adapter/enumerable/EnumerableCollect.java      |  41 +++++-
 .../adapter/enumerable/EnumerableCollectRule.java  |   9 +-
 .../java/org/apache/calcite/rel/core/Collect.java  | 150 +++++++++++++++++----
 .../apache/calcite/rel/externalize/RelJson.java    |   2 +
 .../apache/calcite/rel/mutable/MutableRels.java    |  15 ++-
 .../calcite/rel/rules/SubQueryRemoveRule.java      |  35 ++++-
 .../java/org/apache/calcite/rex/RexSubQuery.java   |   9 +-
 .../calcite/sql/fun/SqlArrayQueryConstructor.java  |   3 +-
 .../calcite/sql/fun/SqlMapQueryConstructor.java    |   3 +-
 .../sql/fun/SqlMultisetQueryConstructor.java       |  61 +++------
 .../org/apache/calcite/sql/type/ReturnTypes.java   |   9 ++
 .../apache/calcite/sql/type/SqlTypeTransforms.java |  11 ++
 .../org/apache/calcite/sql/type/SqlTypeUtil.java   |  11 ++
 .../apache/calcite/sql2rel/SqlToRelConverter.java  |  46 +++++--
 .../java/org/apache/calcite/tools/RelBuilder.java  |   5 +
 .../org/apache/calcite/plan/RelWriterTest.java     |  63 +++++++++
 .../java/org/apache/calcite/test/JdbcTest.java     |  18 +++
 .../org/apache/calcite/test/RelBuilderTest.java    |  16 ++-
 .../apache/calcite/test/SqlToRelConverterTest.java |  56 +++++++-
 .../apache/calcite/test/SqlToRelConverterTest.xml  | 120 ++++++++++++++++-
 core/src/test/resources/sql/join.iq                |  42 ++++++
 21 files changed, 614 insertions(+), 111 deletions(-)

diff --git a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableCollect.java b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableCollect.java
index ff7bf05..da50dff 100644
--- a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableCollect.java
+++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableCollect.java
@@ -23,21 +23,54 @@ import org.apache.calcite.plan.RelOptCluster;
 import org.apache.calcite.plan.RelTraitSet;
 import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.core.Collect;
+import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.sql.type.SqlTypeName;
 import org.apache.calcite.util.BuiltInMethod;
 
 /** Implementation of {@link org.apache.calcite.rel.core.Collect} in
  * {@link org.apache.calcite.adapter.enumerable.EnumerableConvention enumerable calling convention}. */
 public class EnumerableCollect extends Collect implements EnumerableRel {
+  /**
+   * Creates an EnumerableCollect.
+   *
+   * <p>Use {@link #create} unless you know what you're doing.
+   *
+   * @param cluster   Cluster
+   * @param traitSet  Trait set
+   * @param input     Input relational expression
+   * @param rowType   Row type
+   */
   public EnumerableCollect(RelOptCluster cluster, RelTraitSet traitSet,
-      RelNode child, String fieldName) {
-    super(cluster, traitSet, child, fieldName);
+      RelNode input, RelDataType rowType) {
+    super(cluster, traitSet, input, rowType);
     assert getConvention() instanceof EnumerableConvention;
-    assert getConvention() == child.getConvention();
+    assert getConvention() == input.getConvention();
+  }
+
+  @Deprecated // to be removed before 2.0
+  public EnumerableCollect(RelOptCluster cluster, RelTraitSet traitSet,
+      RelNode input, String fieldName) {
+    this(cluster, traitSet, input,
+        deriveRowType(cluster.getTypeFactory(), SqlTypeName.MULTISET, fieldName,
+            input.getRowType()));
+  }
+
+  /**
+   * Creates an EnumerableCollect.
+   *
+   * @param input          Input relational expression
+   * @param rowType        Row type
+   */
+  public static Collect create(RelNode input, RelDataType rowType) {
+    final RelOptCluster cluster = input.getCluster();
+    final RelTraitSet traitSet =
+        cluster.traitSet().replace(EnumerableConvention.INSTANCE);
+    return new EnumerableCollect(cluster, traitSet, input, rowType);
   }
 
   @Override public EnumerableCollect copy(RelTraitSet traitSet,
       RelNode newInput) {
-    return new EnumerableCollect(getCluster(), traitSet, newInput, fieldName);
+    return new EnumerableCollect(getCluster(), traitSet, newInput, rowType());
   }
 
   @Override public Result implement(EnumerableRelImplementor implementor, Prefer pref) {
diff --git a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableCollectRule.java b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableCollectRule.java
index d7b486a..79fa21f 100644
--- a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableCollectRule.java
+++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableCollectRule.java
@@ -17,7 +17,6 @@
 package org.apache.calcite.adapter.enumerable;
 
 import org.apache.calcite.plan.Convention;
-import org.apache.calcite.plan.RelTraitSet;
 import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.convert.ConverterRule;
 import org.apache.calcite.rel.core.Collect;
@@ -42,14 +41,10 @@ class EnumerableCollectRule extends ConverterRule {
 
   @Override public RelNode convert(RelNode rel) {
     final Collect collect = (Collect) rel;
-    final RelTraitSet traitSet =
-        collect.getTraitSet().replace(EnumerableConvention.INSTANCE);
     final RelNode input = collect.getInput();
-    return new EnumerableCollect(
-        rel.getCluster(),
-        traitSet,
+    return EnumerableCollect.create(
         convert(input,
             input.getTraitSet().replace(EnumerableConvention.INSTANCE)),
-        collect.getFieldName());
+        collect.getRowType());
   }
 }
diff --git a/core/src/main/java/org/apache/calcite/rel/core/Collect.java b/core/src/main/java/org/apache/calcite/rel/core/Collect.java
index d44613b..28b83c5 100644
--- a/core/src/main/java/org/apache/calcite/rel/core/Collect.java
+++ b/core/src/main/java/org/apache/calcite/rel/core/Collect.java
@@ -25,8 +25,11 @@ import org.apache.calcite.rel.RelWriter;
 import org.apache.calcite.rel.SingleRel;
 import org.apache.calcite.rel.type.RelDataType;
 import org.apache.calcite.rel.type.RelDataTypeFactory;
+import org.apache.calcite.sql.type.SqlTypeName;
 import org.apache.calcite.sql.type.SqlTypeUtil;
 
+import com.google.common.collect.Iterables;
+
 import java.util.List;
 
 import static java.util.Objects.requireNonNull;
@@ -37,33 +40,56 @@ import static java.util.Objects.requireNonNull;
  * <p>Rules:</p>
  *
  * <ul>
- * <li>{@code net.sf.farrago.fennel.rel.FarragoMultisetSplitterRule}
+ * <li>{@link org.apache.calcite.rel.rules.SubQueryRemoveRule}
  * creates a Collect from a call to
- * {@link org.apache.calcite.sql.fun.SqlMultisetValueConstructor} or to
+ * {@link org.apache.calcite.sql.fun.SqlArrayQueryConstructor},
+ * {@link org.apache.calcite.sql.fun.SqlMapQueryConstructor}, or
  * {@link org.apache.calcite.sql.fun.SqlMultisetQueryConstructor}.</li>
  * </ul>
  */
 public class Collect extends SingleRel {
   //~ Instance fields --------------------------------------------------------
 
-  protected final String fieldName;
-
   //~ Constructors -----------------------------------------------------------
 
   /**
    * Creates a Collect.
    *
+   * <p>Use {@link #create} unless you know what you're doing.
+   *
    * @param cluster   Cluster
-   * @param child     Child relational expression
-   * @param fieldName Name of the sole output field
+   * @param traitSet  Trait set
+   * @param input     Input relational expression
+   * @param rowType   Row type
    */
+  protected Collect(
+      RelOptCluster cluster,
+      RelTraitSet traitSet,
+      RelNode input,
+      RelDataType rowType) {
+    super(cluster, traitSet, input);
+    this.rowType = requireNonNull(rowType, "rowType");
+    final SqlTypeName collectionType = getCollectionType(rowType);
+    switch (collectionType) {
+    case ARRAY:
+    case MAP:
+    case MULTISET:
+      break;
+    default:
+      throw new IllegalArgumentException("not a collection type "
+          + collectionType);
+    }
+  }
+
+  @Deprecated // to be removed before 2.0
   public Collect(
       RelOptCluster cluster,
       RelTraitSet traitSet,
-      RelNode child,
+      RelNode input,
       String fieldName) {
-    super(cluster, traitSet, child);
-    this.fieldName = fieldName;
+    this(cluster, traitSet, input,
+        deriveRowType(cluster.getTypeFactory(), SqlTypeName.MULTISET, fieldName,
+            input.getRowType()));
   }
 
   /**
@@ -71,11 +97,48 @@ public class Collect extends SingleRel {
    */
   public Collect(RelInput input) {
     this(input.getCluster(), input.getTraitSet(), input.getInput(),
-        requireNonNull(input.getString("field"), "field"));
+        deriveRowType(input.getCluster().getTypeFactory(), SqlTypeName.MULTISET,
+            requireNonNull(input.getString("field"), "field"),
+            input.getInput().getRowType()));
   }
 
   //~ Methods ----------------------------------------------------------------
 
+  /**
+   * Creates a Collect.
+   *
+   * @param input          Input relational expression
+   * @param rowType        Row type
+   */
+  public static Collect create(RelNode input, RelDataType rowType) {
+    final RelOptCluster cluster = input.getCluster();
+    final RelTraitSet traitSet =
+        cluster.traitSet().replace(Convention.NONE);
+    return new Collect(cluster, traitSet, input, rowType);
+  }
+
+  /**
+   * Creates a Collect.
+   *
+   * @param input          Input relational expression
+   * @param collectionType ARRAY, MAP or MULTISET
+   * @param fieldName      Name of the sole output field
+   */
+  public static Collect create(RelNode input,
+      SqlTypeName collectionType,
+      String fieldName) {
+    return create(input,
+        deriveRowType(input.getCluster().getTypeFactory(), collectionType,
+            fieldName, input.getRowType()));
+  }
+
+  /** Returns the row type, guaranteed not null.
+   * (The row type is never null after initialization, but
+   * CheckerFramework can't deduce that references are safe.) */
+  protected final RelDataType rowType() {
+    return requireNonNull(rowType, "rowType");
+  }
+
   @Override public final RelNode copy(RelTraitSet traitSet,
       List<RelNode> inputs) {
     return copy(traitSet, sole(inputs));
@@ -83,12 +146,12 @@ public class Collect extends SingleRel {
 
   public RelNode copy(RelTraitSet traitSet, RelNode input) {
     assert traitSet.containsIfApplicable(Convention.NONE);
-    return new Collect(getCluster(), traitSet, input, fieldName);
+    return new Collect(getCluster(), traitSet, input, rowType());
   }
 
   @Override public RelWriter explainTerms(RelWriter pw) {
     return super.explainTerms(pw)
-        .item("field", fieldName);
+        .item("field", getFieldName());
   }
 
   /**
@@ -97,32 +160,67 @@ public class Collect extends SingleRel {
    * @return name of the sole output field
    */
   public String getFieldName() {
-    return fieldName;
+    return Iterables.getOnlyElement(rowType().getFieldList()).getName();
+  }
+
+  /** Returns the collection type (ARRAY, MAP, or MULTISET). */
+  public SqlTypeName getCollectionType() {
+    return getCollectionType(rowType());
+  }
+
+  private static SqlTypeName getCollectionType(RelDataType rowType) {
+    return Iterables.getOnlyElement(rowType.getFieldList())
+        .getType().getSqlTypeName();
   }
 
   @Override protected RelDataType deriveRowType() {
-    return deriveCollectRowType(this, fieldName);
+    // this method should never be called; rowType is always set
+    throw new UnsupportedOperationException();
   }
 
   /**
-   * Derives the output type of a collect relational expression.
+   * Derives the output row type of a Collect relational expression.
    *
    * @param rel       relational expression
    * @param fieldName name of sole output field
-   * @return output type of a collect relational expression
+   * @return output row type of a Collect relational expression
    */
+  @Deprecated // to be removed before 2.0
   public static RelDataType deriveCollectRowType(
       SingleRel rel,
       String fieldName) {
-    RelDataType childType = rel.getInput().getRowType();
-    assert childType.isStruct();
-    final RelDataTypeFactory typeFactory = rel.getCluster().getTypeFactory();
-    RelDataType ret =
-        SqlTypeUtil.createMultisetType(
-            typeFactory,
-            childType,
-            false);
-    ret = typeFactory.builder().add(fieldName, ret).build();
-    return typeFactory.createTypeWithNullability(ret, false);
+    RelDataType inputType = rel.getInput().getRowType();
+    assert inputType.isStruct();
+    return deriveRowType(rel.getCluster().getTypeFactory(),
+        SqlTypeName.MULTISET, fieldName, inputType);
+  }
+
+  /**
+   * Derives the output row type of a Collect relational expression.
+   *
+   * @param typeFactory    Type factory
+   * @param collectionType MULTISET, ARRAY or MAP
+   * @param fieldName      Name of sole output field
+   * @param elementType    Element type
+   * @return output row type of a Collect relational expression
+   */
+  public static RelDataType deriveRowType(RelDataTypeFactory typeFactory,
+      SqlTypeName collectionType, String fieldName, RelDataType elementType) {
+    final RelDataType type1;
+    switch (collectionType) {
+    case ARRAY:
+      type1 = SqlTypeUtil.createArrayType(typeFactory, elementType, false);
+      break;
+    case MULTISET:
+      type1 = SqlTypeUtil.createMultisetType(typeFactory, elementType, false);
+      break;
+    case MAP:
+      type1 = SqlTypeUtil.createMapTypeFromRecord(typeFactory, elementType);
+      break;
+    default:
+      throw new AssertionError(collectionType);
+    }
+    return typeFactory.createTypeWithNullability(
+        typeFactory.builder().add(fieldName, type1).build(), false);
   }
 }
diff --git a/core/src/main/java/org/apache/calcite/rel/externalize/RelJson.java b/core/src/main/java/org/apache/calcite/rel/externalize/RelJson.java
index 5894618..320c44f 100644
--- a/core/src/main/java/org/apache/calcite/rel/externalize/RelJson.java
+++ b/core/src/main/java/org/apache/calcite/rel/externalize/RelJson.java
@@ -417,7 +417,9 @@ public class RelJson {
     if (node.getType().isStruct()) {
       map = jsonBuilder().map();
       map.put("fields", toJson(node.getType()));
+      map.put("nullable", node.getType().isNullable());
     } else {
+      //noinspection unchecked
       map = (Map<String, @Nullable Object>) toJson(node.getType());
     }
     map.put("name", node.getName());
diff --git a/core/src/main/java/org/apache/calcite/rel/mutable/MutableRels.java b/core/src/main/java/org/apache/calcite/rel/mutable/MutableRels.java
index ee6f6c1..ab8902e 100644
--- a/core/src/main/java/org/apache/calcite/rel/mutable/MutableRels.java
+++ b/core/src/main/java/org/apache/calcite/rel/mutable/MutableRels.java
@@ -53,6 +53,7 @@ import org.apache.calcite.rel.type.RelDataType;
 import org.apache.calcite.rex.RexInputRef;
 import org.apache.calcite.rex.RexNode;
 import org.apache.calcite.rex.RexUtil;
+import org.apache.calcite.sql.type.SqlTypeName;
 import org.apache.calcite.tools.RelBuilder;
 import org.apache.calcite.util.Util;
 import org.apache.calcite.util.mapping.Mapping;
@@ -181,14 +182,14 @@ public abstract class MutableRels {
    * Construct expression list of Project by the given fields of the input.
    */
   public static List<RexNode> createProjects(final MutableRel child,
-      final List<RexNode> projs) {
-    List<RexNode> rexNodeList = new ArrayList<>(projs.size());
-    for (int i = 0; i < projs.size(); i++) {
-      if (projs.get(i) instanceof RexInputRef) {
-        RexInputRef rexInputRef = (RexInputRef) projs.get(i);
+      final List<RexNode> projects) {
+    List<RexNode> rexNodeList = new ArrayList<>(projects.size());
+    for (RexNode project : projects) {
+      if (project instanceof RexInputRef) {
+        RexInputRef rexInputRef = (RexInputRef) project;
         rexNodeList.add(RexInputRef.of(rexInputRef.getIndex(), child.rowType));
       } else {
-        rexNodeList.add(projs.get(i));
+        rexNodeList.add(project);
       }
     }
     return rexNodeList;
@@ -251,7 +252,7 @@ public abstract class MutableRels {
     case COLLECT: {
       final MutableCollect collect = (MutableCollect) node;
       final RelNode child = fromMutable(collect.getInput(), relBuilder);
-      return new Collect(collect.cluster, child.getTraitSet(), child, collect.fieldName);
+      return Collect.create(child, SqlTypeName.MULTISET, collect.fieldName);
     }
     case UNCOLLECT: {
       final MutableUncollect uncollect = (MutableUncollect) node;
diff --git a/core/src/main/java/org/apache/calcite/rel/rules/SubQueryRemoveRule.java b/core/src/main/java/org/apache/calcite/rel/rules/SubQueryRemoveRule.java
index db25992..ba2eeb5 100644
--- a/core/src/main/java/org/apache/calcite/rel/rules/SubQueryRemoveRule.java
+++ b/core/src/main/java/org/apache/calcite/rel/rules/SubQueryRemoveRule.java
@@ -20,6 +20,7 @@ import org.apache.calcite.plan.RelOptRuleCall;
 import org.apache.calcite.plan.RelOptUtil;
 import org.apache.calcite.plan.RelRule;
 import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.core.Collect;
 import org.apache.calcite.rel.core.Correlate;
 import org.apache.calcite.rel.core.CorrelationId;
 import org.apache.calcite.rel.core.Filter;
@@ -39,6 +40,7 @@ import org.apache.calcite.sql.SqlAggFunction;
 import org.apache.calcite.sql.SqlKind;
 import org.apache.calcite.sql.fun.SqlQuantifyOperator;
 import org.apache.calcite.sql.fun.SqlStdOperatorTable;
+import org.apache.calcite.sql.type.SqlTypeName;
 import org.apache.calcite.sql2rel.RelDecorrelator;
 import org.apache.calcite.tools.RelBuilder;
 import org.apache.calcite.util.ImmutableBitSet;
@@ -91,6 +93,15 @@ public class SubQueryRemoveRule
     switch (e.getKind()) {
     case SCALAR_QUERY:
       return rewriteScalarQuery(e, variablesSet, builder, inputCount, offset);
+    case ARRAY_QUERY_CONSTRUCTOR:
+      return rewriteCollection(e, SqlTypeName.ARRAY, variablesSet, builder,
+          inputCount, offset);
+    case MAP_QUERY_CONSTRUCTOR:
+      return rewriteCollection(e, SqlTypeName.MAP, variablesSet, builder,
+          inputCount, offset);
+    case MULTISET_QUERY_CONSTRUCTOR:
+      return rewriteCollection(e, SqlTypeName.MULTISET, variablesSet, builder,
+          inputCount, offset);
     case SOME:
       return rewriteSome(e, variablesSet, builder);
     case IN:
@@ -108,7 +119,7 @@ public class SubQueryRemoveRule
    * Rewrites a scalar sub-query into an
    * {@link org.apache.calcite.rel.core.Aggregate}.
    *
-   * @param e            IN sub-query to rewrite
+   * @param e            Scalar sub-query to rewrite
    * @param variablesSet A set of variables used by a relational
    *                     expression of the specified RexSubQuery
    * @param builder      Builder
@@ -132,6 +143,28 @@ public class SubQueryRemoveRule
   }
 
   /**
+   * Rewrites a sub-query into a
+   * {@link org.apache.calcite.rel.core.Collect}.
+   *
+   * @param e            Sub-query to rewrite
+   * @param collectionType Collection type (ARRAY, MAP, MULTISET)
+   * @param variablesSet A set of variables used by a relational
+   *                     expression of the specified RexSubQuery
+   * @param builder      Builder
+   * @param offset       Offset to shift {@link RexInputRef}
+   * @return Expression that may be used to replace the RexSubQuery
+   */
+  private static RexNode rewriteCollection(RexSubQuery e,
+      SqlTypeName collectionType, Set<CorrelationId> variablesSet,
+      RelBuilder builder, int inputCount, int offset) {
+    builder.push(e.rel);
+    builder.push(
+        Collect.create(builder.build(), collectionType, "x"));
+    builder.join(JoinRelType.INNER, builder.literal(true), variablesSet);
+    return field(builder, inputCount, offset);
+  }
+
+  /**
    * Rewrites a SOME sub-query into a {@link Join}.
    *
    * @param e            SOME sub-query to rewrite
diff --git a/core/src/main/java/org/apache/calcite/rex/RexSubQuery.java b/core/src/main/java/org/apache/calcite/rex/RexSubQuery.java
index e1db6f8..564e2b6 100644
--- a/core/src/main/java/org/apache/calcite/rex/RexSubQuery.java
+++ b/core/src/main/java/org/apache/calcite/rex/RexSubQuery.java
@@ -27,6 +27,7 @@ import org.apache.calcite.sql.fun.SqlQuantifyOperator;
 import org.apache.calcite.sql.fun.SqlStdOperatorTable;
 import org.apache.calcite.sql.type.SqlTypeName;
 
+import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableList;
 
 import org.checkerframework.checker.nullness.qual.Nullable;
@@ -139,8 +140,14 @@ public class RexSubQuery extends RexCall {
   /** Creates a MAP sub-query. */
   public static RexSubQuery map(RelNode rel) {
     final RelDataTypeFactory typeFactory = rel.getCluster().getTypeFactory();
+    final RelDataType rowType = rel.getRowType();
+    Preconditions.checkArgument(rowType.getFieldCount() == 2,
+        "MAP requires exactly two fields, got %s; row type %s",
+        rowType.getFieldCount(), rowType);
+    final List<RelDataTypeField> fieldList = rowType.getFieldList();
     final RelDataType type =
-        typeFactory.createMultisetType(rel.getRowType(), -1L);
+        typeFactory.createMapType(fieldList.get(0).getType(),
+            fieldList.get(1).getType());
     return new RexSubQuery(type, SqlStdOperatorTable.MAP_QUERY,
         ImmutableList.of(), rel);
   }
diff --git a/core/src/main/java/org/apache/calcite/sql/fun/SqlArrayQueryConstructor.java b/core/src/main/java/org/apache/calcite/sql/fun/SqlArrayQueryConstructor.java
index 9444c2c..810df29 100644
--- a/core/src/main/java/org/apache/calcite/sql/fun/SqlArrayQueryConstructor.java
+++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlArrayQueryConstructor.java
@@ -17,6 +17,7 @@
 package org.apache.calcite.sql.fun;
 
 import org.apache.calcite.sql.SqlKind;
+import org.apache.calcite.sql.type.SqlTypeTransforms;
 
 /**
  * Definition of the SQL:2003 standard ARRAY query constructor, <code>
@@ -26,6 +27,6 @@ public class SqlArrayQueryConstructor extends SqlMultisetQueryConstructor {
   //~ Constructors -----------------------------------------------------------
 
   public SqlArrayQueryConstructor() {
-    super("ARRAY", SqlKind.ARRAY_QUERY_CONSTRUCTOR);
+    super("ARRAY", SqlKind.ARRAY_QUERY_CONSTRUCTOR, SqlTypeTransforms.TO_ARRAY);
   }
 }
diff --git a/core/src/main/java/org/apache/calcite/sql/fun/SqlMapQueryConstructor.java b/core/src/main/java/org/apache/calcite/sql/fun/SqlMapQueryConstructor.java
index ed21052..3daee84 100644
--- a/core/src/main/java/org/apache/calcite/sql/fun/SqlMapQueryConstructor.java
+++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlMapQueryConstructor.java
@@ -17,6 +17,7 @@
 package org.apache.calcite.sql.fun;
 
 import org.apache.calcite.sql.SqlKind;
+import org.apache.calcite.sql.type.SqlTypeTransforms;
 
 /**
  * Definition of the MAP query constructor, <code>
@@ -28,6 +29,6 @@ public class SqlMapQueryConstructor extends SqlMultisetQueryConstructor {
   //~ Constructors -----------------------------------------------------------
 
   public SqlMapQueryConstructor() {
-    super("MAP", SqlKind.MAP_QUERY_CONSTRUCTOR);
+    super("MAP", SqlKind.MAP_QUERY_CONSTRUCTOR, SqlTypeTransforms.TO_MAP);
   }
 }
diff --git a/core/src/main/java/org/apache/calcite/sql/fun/SqlMultisetQueryConstructor.java b/core/src/main/java/org/apache/calcite/sql/fun/SqlMultisetQueryConstructor.java
index 6aa3ce1..2391bba 100644
--- a/core/src/main/java/org/apache/calcite/sql/fun/SqlMultisetQueryConstructor.java
+++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlMultisetQueryConstructor.java
@@ -17,23 +17,21 @@
 package org.apache.calcite.sql.fun;
 
 import org.apache.calcite.rel.type.RelDataType;
-import org.apache.calcite.rel.type.RelDataTypeFactory;
 import org.apache.calcite.sql.SqlCall;
 import org.apache.calcite.sql.SqlCallBinding;
 import org.apache.calcite.sql.SqlKind;
-import org.apache.calcite.sql.SqlOperatorBinding;
 import org.apache.calcite.sql.SqlSelect;
 import org.apache.calcite.sql.SqlSpecialOperator;
 import org.apache.calcite.sql.SqlWriter;
 import org.apache.calcite.sql.type.OperandTypes;
 import org.apache.calcite.sql.type.ReturnTypes;
+import org.apache.calcite.sql.type.SqlTypeTransform;
+import org.apache.calcite.sql.type.SqlTypeTransforms;
 import org.apache.calcite.sql.type.SqlTypeUtil;
 import org.apache.calcite.sql.validate.SqlValidator;
 import org.apache.calcite.sql.validate.SqlValidatorNamespace;
 import org.apache.calcite.sql.validate.SqlValidatorScope;
 
-import org.checkerframework.checker.nullness.qual.Nullable;
-
 import java.util.List;
 
 import static org.apache.calcite.util.Static.RESOURCE;
@@ -47,51 +45,31 @@ import static java.util.Objects.requireNonNull;
  * @see SqlMultisetValueConstructor
  */
 public class SqlMultisetQueryConstructor extends SqlSpecialOperator {
+
+  final SqlTypeTransform typeTransform;
+
   //~ Constructors -----------------------------------------------------------
 
   public SqlMultisetQueryConstructor() {
-    this("MULTISET", SqlKind.MULTISET_QUERY_CONSTRUCTOR);
+    this("MULTISET", SqlKind.MULTISET_QUERY_CONSTRUCTOR,
+        SqlTypeTransforms.TO_MULTISET);
   }
 
-  protected SqlMultisetQueryConstructor(String name, SqlKind kind) {
-    super(
-        name,
-        kind, MDX_PRECEDENCE,
-        false,
-        ReturnTypes.ARG0,
-        null,
-        OperandTypes.VARIADIC);
+  protected SqlMultisetQueryConstructor(String name, SqlKind kind,
+      SqlTypeTransform typeTransform) {
+    super(name, kind, MDX_PRECEDENCE, false,
+        ReturnTypes.ARG0.andThen(typeTransform), null, OperandTypes.VARIADIC);
+    this.typeTransform = typeTransform;
   }
 
   //~ Methods ----------------------------------------------------------------
 
-  @Override public RelDataType inferReturnType(
-      SqlOperatorBinding opBinding) {
-    RelDataType type =
-        getComponentType(
-            opBinding.getTypeFactory(),
-            opBinding.collectOperandTypes());
-    requireNonNull(type, "inferred multiset query element type");
-    return SqlTypeUtil.createMultisetType(
-        opBinding.getTypeFactory(),
-        type,
-        false);
-  }
-
-  private static @Nullable RelDataType getComponentType(
-      RelDataTypeFactory typeFactory,
-      List<RelDataType> argTypes) {
-    return typeFactory.leastRestrictive(argTypes);
-  }
-
   @Override public boolean checkOperandTypes(
       SqlCallBinding callBinding,
       boolean throwOnFailure) {
     final List<RelDataType> argTypes = SqlTypeUtil.deriveType(callBinding, callBinding.operands());
     final RelDataType componentType =
-        getComponentType(
-            callBinding.getTypeFactory(),
-            argTypes);
+        callBinding.getTypeFactory().leastRestrictive(argTypes);
     if (null == componentType) {
       if (throwOnFailure) {
         throw callBinding.newValidationError(RESOURCE.needSameTypeParameter());
@@ -107,13 +85,12 @@ public class SqlMultisetQueryConstructor extends SqlSpecialOperator {
       SqlCall call) {
     SqlSelect subSelect = call.operand(0);
     subSelect.validateExpr(validator, scope);
-    SqlValidatorNamespace ns = validator.getNamespace(subSelect);
-    assert  ns != null : "namespace is missing for " + subSelect;
-    assert null != ns.getRowType();
-    return SqlTypeUtil.createMultisetType(
-        validator.getTypeFactory(),
-        ns.getRowType(),
-        false);
+    final SqlValidatorNamespace ns =
+        requireNonNull(validator.getNamespace(subSelect),
+            () -> "namespace is missing for " + subSelect);
+    final RelDataType rowType = requireNonNull(ns.getRowType(), "rowType");
+    final SqlCallBinding opBinding = new SqlCallBinding(validator, scope, call);
+    return typeTransform.transformType(opBinding, rowType);
   }
 
   @Override public void unparse(
diff --git a/core/src/main/java/org/apache/calcite/sql/type/ReturnTypes.java b/core/src/main/java/org/apache/calcite/sql/type/ReturnTypes.java
index 5594878..3512ba3 100644
--- a/core/src/main/java/org/apache/calcite/sql/type/ReturnTypes.java
+++ b/core/src/main/java/org/apache/calcite/sql/type/ReturnTypes.java
@@ -507,6 +507,15 @@ public abstract class ReturnTypes {
       ARG0.andThen(SqlTypeTransforms.TO_ARRAY);
 
   /**
+   * Returns a MAP type.
+   *
+   * <p>For example, given {@code Record(f0: INTEGER, f1: DATE)}, returns
+   * {@code (INTEGER, DATE) MAP}.
+   */
+  public static final SqlReturnTypeInference TO_MAP =
+      ARG0.andThen(SqlTypeTransforms.TO_MAP);
+
+  /**
    * Type-inference strategy whereby the result type of a call is
    * {@link #ARG0_INTERVAL_NULLABLE} and {@link #LEAST_RESTRICTIVE}. These rules
    * are used for integer division.
diff --git a/core/src/main/java/org/apache/calcite/sql/type/SqlTypeTransforms.java b/core/src/main/java/org/apache/calcite/sql/type/SqlTypeTransforms.java
index b33fdd0..92165f5 100644
--- a/core/src/main/java/org/apache/calcite/sql/type/SqlTypeTransforms.java
+++ b/core/src/main/java/org/apache/calcite/sql/type/SqlTypeTransforms.java
@@ -183,6 +183,17 @@ public abstract class SqlTypeTransforms {
           opBinding.getTypeFactory().createArrayType(typeToTransform, -1);
 
   /**
+   * Parameter type-inference transform strategy that converts a two-field
+   * record type to a MAP type.
+   *
+   * @see org.apache.calcite.rel.type.RelDataTypeFactory#createMapType
+   */
+  public static final SqlTypeTransform TO_MAP =
+      (opBinding, typeToTransform) ->
+          SqlTypeUtil.createMapTypeFromRecord(opBinding.getTypeFactory(),
+              typeToTransform);
+
+  /**
    * Parameter type-inference transform strategy where a derived type must be
    * a struct type with precisely one field and the returned type is the type
    * of that field.
diff --git a/core/src/main/java/org/apache/calcite/sql/type/SqlTypeUtil.java b/core/src/main/java/org/apache/calcite/sql/type/SqlTypeUtil.java
index c4b2cfc..a226f77 100644
--- a/core/src/main/java/org/apache/calcite/sql/type/SqlTypeUtil.java
+++ b/core/src/main/java/org/apache/calcite/sql/type/SqlTypeUtil.java
@@ -1116,6 +1116,17 @@ public abstract class SqlTypeUtil {
     return typeFactory.createTypeWithNullability(ret, nullable);
   }
 
+  /** Creates a MAP type from a record type. The record type must have exactly
+   * two fields. */
+  public static RelDataType createMapTypeFromRecord(
+      RelDataTypeFactory typeFactory, RelDataType type) {
+    Preconditions.checkArgument(type.getFieldCount() == 2,
+        "MAP requires exactly two fields, got %s; row type %s",
+        type.getFieldCount(), type);
+    return createMapType(typeFactory, type.getFieldList().get(0).getType(),
+        type.getFieldList().get(1).getType(), false);
+  }
+
   /**
    * Adds collation and charset to a character type, returns other types
    * unchanged.
diff --git a/core/src/main/java/org/apache/calcite/sql2rel/SqlToRelConverter.java b/core/src/main/java/org/apache/calcite/sql2rel/SqlToRelConverter.java
index 852e923..355614a 100644
--- a/core/src/main/java/org/apache/calcite/sql2rel/SqlToRelConverter.java
+++ b/core/src/main/java/org/apache/calcite/sql2rel/SqlToRelConverter.java
@@ -18,7 +18,6 @@ package org.apache.calcite.sql2rel;
 
 import org.apache.calcite.avatica.util.Spaces;
 import org.apache.calcite.linq4j.Ord;
-import org.apache.calcite.plan.Convention;
 import org.apache.calcite.plan.RelOptCluster;
 import org.apache.calcite.plan.RelOptPlanner;
 import org.apache.calcite.plan.RelOptSamplingParameters;
@@ -1109,9 +1108,14 @@ public class SqlToRelConverter {
       convertCursor(bb, subQuery);
       return;
 
+    case ARRAY_QUERY_CONSTRUCTOR:
+    case MAP_QUERY_CONSTRUCTOR:
     case MULTISET_QUERY_CONSTRUCTOR:
+      if (!config.isExpand()) {
+        return;
+      }
+      // fall through
     case MULTISET_VALUE_CONSTRUCTOR:
-    case ARRAY_QUERY_CONSTRUCTOR:
       rel = convertMultisets(ImmutableList.of(subQuery.node), bb);
       subQuery.expr = bb.register(rel, JoinRelType.INNER);
       return;
@@ -1843,6 +1847,7 @@ public class SqlToRelConverter {
     case MULTISET_QUERY_CONSTRUCTOR:
     case MULTISET_VALUE_CONSTRUCTOR:
     case ARRAY_QUERY_CONSTRUCTOR:
+    case MAP_QUERY_CONSTRUCTOR:
     case CURSOR:
     case SCALAR_QUERY:
       if (!registerOnlyScalarSubQueries
@@ -4232,6 +4237,7 @@ public class SqlToRelConverter {
         break;
       case MULTISET_QUERY_CONSTRUCTOR:
       case ARRAY_QUERY_CONSTRUCTOR:
+      case MAP_QUERY_CONSTRUCTOR:
         final RelRoot root = convertQuery(call.operand(0), false, true);
         input = root.rel;
         break;
@@ -4244,13 +4250,14 @@ public class SqlToRelConverter {
         joinList.add(lastList);
       }
       lastList = new ArrayList<>();
-      Collect collect =
-          new Collect(
-              cluster,
-              cluster.traitSetOf(Convention.NONE),
-              requireNonNull(input, "input"),
-              castNonNull(validator().deriveAlias(call, i)));
-      joinList.add(collect);
+      final SqlTypeName typeName =
+          requireNonNull(validator, "validator")
+              .getValidatedNodeType(call)
+              .getSqlTypeName();
+      relBuilder.push(
+          Collect.create(requireNonNull(input, "input"),
+              typeName, castNonNull(validator().deriveAlias(call, i))));
+      joinList.add(relBuilder.build());
     }
 
     if (joinList.size() == 0) {
@@ -5045,6 +5052,24 @@ public class SqlToRelConverter {
           root = convertQueryRecursive(query, false, null);
           return RexSubQuery.scalar(root.rel);
 
+        case ARRAY_QUERY_CONSTRUCTOR:
+          call = (SqlCall) expr;
+          query = Iterables.getOnlyElement(call.getOperandList());
+          root = convertQueryRecursive(query, false, null);
+          return RexSubQuery.array(root.rel);
+
+        case MAP_QUERY_CONSTRUCTOR:
+          call = (SqlCall) expr;
+          query = Iterables.getOnlyElement(call.getOperandList());
+          root = convertQueryRecursive(query, false, null);
+          return RexSubQuery.map(root.rel);
+
+        case MULTISET_QUERY_CONSTRUCTOR:
+          call = (SqlCall) expr;
+          query = Iterables.getOnlyElement(call.getOperandList());
+          root = convertQueryRecursive(query, false, null);
+          return RexSubQuery.multiset(root.rel);
+
         default:
           break;
         }
@@ -5070,6 +5095,9 @@ public class SqlToRelConverter {
       case SELECT:
       case EXISTS:
       case SCALAR_QUERY:
+      case ARRAY_QUERY_CONSTRUCTOR:
+      case MAP_QUERY_CONSTRUCTOR:
+      case MULTISET_QUERY_CONSTRUCTOR:
         subQuery = getSubQuery(expr);
         assert subQuery != null;
         rex = subQuery.expr;
diff --git a/core/src/main/java/org/apache/calcite/tools/RelBuilder.java b/core/src/main/java/org/apache/calcite/tools/RelBuilder.java
index 3eb759d..7d4b408 100644
--- a/core/src/main/java/org/apache/calcite/tools/RelBuilder.java
+++ b/core/src/main/java/org/apache/calcite/tools/RelBuilder.java
@@ -357,6 +357,11 @@ public class RelBuilder {
     return this;
   }
 
+  /** Returns the size of the stack. */
+  public int size() {
+    return stack.size();
+  }
+
   /** Returns the final relational expression.
    *
    * <p>Throws if the stack is empty.
diff --git a/core/src/test/java/org/apache/calcite/plan/RelWriterTest.java b/core/src/test/java/org/apache/calcite/plan/RelWriterTest.java
index 06d3c96..103ef55 100644
--- a/core/src/test/java/org/apache/calcite/plan/RelWriterTest.java
+++ b/core/src/test/java/org/apache/calcite/plan/RelWriterTest.java
@@ -30,6 +30,7 @@ import org.apache.calcite.rel.core.AggregateCall;
 import org.apache.calcite.rel.core.JoinRelType;
 import org.apache.calcite.rel.core.TableModify;
 import org.apache.calcite.rel.core.TableScan;
+import org.apache.calcite.rel.externalize.RelJson;
 import org.apache.calcite.rel.externalize.RelJsonReader;
 import org.apache.calcite.rel.externalize.RelJsonWriter;
 import org.apache.calcite.rel.logical.LogicalAggregate;
@@ -39,6 +40,7 @@ import org.apache.calcite.rel.logical.LogicalProject;
 import org.apache.calcite.rel.logical.LogicalTableModify;
 import org.apache.calcite.rel.logical.LogicalTableScan;
 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.rex.RexBuilder;
 import org.apache.calcite.rex.RexCorrelVariable;
@@ -64,6 +66,7 @@ import org.apache.calcite.tools.Frameworks;
 import org.apache.calcite.tools.RelBuilder;
 import org.apache.calcite.util.Holder;
 import org.apache.calcite.util.ImmutableBitSet;
+import org.apache.calcite.util.JsonBuilder;
 import org.apache.calcite.util.TestUtil;
 
 import com.google.common.collect.ImmutableList;
@@ -86,6 +89,7 @@ import java.util.stream.Stream;
 import static org.apache.calcite.test.Matchers.isLinux;
 
 import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.notNullValue;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 
@@ -425,6 +429,65 @@ class RelWriterTest {
     return Stream.of(SqlExplainFormat.TEXT, SqlExplainFormat.DOT);
   }
 
+  /** Unit test for {@link RelJson#toJson(Object)} for an object of type
+   * {@link RelDataType}. */
+  @Test void testTypeJson() {
+    int i = Frameworks.withPlanner((cluster, relOptSchema, rootSchema) -> {
+      final RelDataTypeFactory typeFactory = cluster.getTypeFactory();
+      final RelDataType type = typeFactory.builder()
+          .add("i", typeFactory.createSqlType(SqlTypeName.INTEGER))
+          .nullable(false)
+          .add("v", typeFactory.createSqlType(SqlTypeName.VARCHAR, 9))
+          .nullable(true)
+          .add("r", typeFactory.builder()
+              .add("d", typeFactory.createSqlType(SqlTypeName.DATE))
+              .nullable(false)
+              .build())
+          .nullableRecord(false)
+          .build();
+      final JsonBuilder jsonBuilder = new JsonBuilder();
+      final RelJson json = new RelJson(jsonBuilder);
+      final Object o = json.toJson(type);
+      assertThat(o, notNullValue());
+      final String s = jsonBuilder.toJsonString(o);
+      final String expectedJson = "{\n"
+          + "  \"fields\": [\n"
+          + "    {\n"
+          + "      \"type\": \"INTEGER\",\n"
+          + "      \"nullable\": false,\n"
+          + "      \"name\": \"i\"\n"
+          + "    },\n"
+          + "    {\n"
+          + "      \"type\": \"VARCHAR\",\n"
+          + "      \"nullable\": true,\n"
+          + "      \"precision\": 9,\n"
+          + "      \"name\": \"v\"\n"
+          + "    },\n"
+          + "    {\n"
+          + "      \"fields\": {\n"
+          + "        \"fields\": [\n"
+          + "          {\n"
+          + "            \"type\": \"DATE\",\n"
+          + "            \"nullable\": false,\n"
+          + "            \"name\": \"d\"\n"
+          + "          }\n"
+          + "        ],\n"
+          + "        \"nullable\": false\n"
+          + "      },\n"
+          + "      \"nullable\": false,\n"
+          + "      \"name\": \"r\"\n"
+          + "    }\n"
+          + "  ],\n"
+          + "  \"nullable\": false\n"
+          + "}";
+      assertThat(s, is(expectedJson));
+      final RelDataType type2 = json.toType(typeFactory, o);
+      assertThat(type2, is(type));
+      return 0;
+    });
+    assertThat(i, is(0));
+  }
+
   /**
    * Unit test for {@link org.apache.calcite.rel.externalize.RelJsonWriter} on
    * a simple tree of relational expressions, consisting of a table and a
diff --git a/core/src/test/java/org/apache/calcite/test/JdbcTest.java b/core/src/test/java/org/apache/calcite/test/JdbcTest.java
index 251555b..b7370bd 100644
--- a/core/src/test/java/org/apache/calcite/test/JdbcTest.java
+++ b/core/src/test/java/org/apache/calcite/test/JdbcTest.java
@@ -243,6 +243,16 @@ public class JdbcTest {
     return Stream.of("text", "dot");
   }
 
+  /** Runs a task (such as a test) with and without expansion. */
+  static void forEachExpand(Runnable r) {
+    try (TryThreadLocal.Memo ignored = Prepare.THREAD_EXPAND.push(false)) {
+      r.run();
+    }
+    try (TryThreadLocal.Memo ignored = Prepare.THREAD_EXPAND.push(true)) {
+      r.run();
+    }
+  }
+
   /** Tests a modifiable view. */
   @Test void testModelWithModifiableView() throws Exception {
     final List<Employee> employees = new ArrayList<>();
@@ -2082,6 +2092,10 @@ public class JdbcTest {
   }
 
   @Test void testMultisetQuery() {
+    forEachExpand(this::checkMultisetQuery);
+  }
+
+  void checkMultisetQuery() {
     CalciteAssert.hr()
         .query("select multiset(\n"
             + "  select \"deptno\", \"empid\" from \"hr\".\"emps\") as a\n"
@@ -2090,6 +2104,10 @@ public class JdbcTest {
   }
 
   @Test void testMultisetQueryWithSingleColumn() {
+    forEachExpand(this::checkMultisetQueryWithSingleColumn);
+  }
+
+  void checkMultisetQueryWithSingleColumn() {
     CalciteAssert.hr()
         .query("select multiset(\n"
             + "  select \"deptno\" from \"hr\".\"emps\") as a\n"
diff --git a/core/src/test/java/org/apache/calcite/test/RelBuilderTest.java b/core/src/test/java/org/apache/calcite/test/RelBuilderTest.java
index 2edf701..9f08de9 100644
--- a/core/src/test/java/org/apache/calcite/test/RelBuilderTest.java
+++ b/core/src/test/java/org/apache/calcite/test/RelBuilderTest.java
@@ -2194,6 +2194,8 @@ public class RelBuilderTest {
     assertThat(root, hasTree(expected));
   }
 
+  /** Tests building a simple join. Also checks {@link RelBuilder#size()}
+   * at every step. */
   @Test void testJoin() {
     // Equivalent SQL:
     //   SELECT *
@@ -2201,15 +2203,21 @@ public class RelBuilderTest {
     //   JOIN dept ON emp.deptno = dept.deptno
     final RelBuilder builder = RelBuilder.create(config().build());
     RelNode root =
-        builder.scan("EMP")
+        builder.let(b -> assertSize(b, is(0)))
+            .scan("EMP")
+            .let(b -> assertSize(b, is(1)))
             .filter(
                 builder.call(SqlStdOperatorTable.IS_NULL,
                     builder.field("COMM")))
+            .let(b -> assertSize(b, is(1)))
             .scan("DEPT")
+            .let(b -> assertSize(b, is(2)))
             .join(JoinRelType.INNER,
                 builder.equals(builder.field(2, 0, "DEPTNO"),
                     builder.field(2, 1, "DEPTNO")))
+            .let(b -> assertSize(b, is(1)))
             .build();
+    assertThat(builder.size(), is(0));
     final String expected = ""
         + "LogicalJoin(condition=[=($7, $8)], joinType=[inner])\n"
         + "  LogicalFilter(condition=[IS NULL($6)])\n"
@@ -2218,6 +2226,12 @@ public class RelBuilderTest {
     assertThat(root, hasTree(expected));
   }
 
+  private static RelBuilder assertSize(RelBuilder b,
+      Matcher<Integer> sizeMatcher) {
+    assertThat(b.size(), sizeMatcher);
+    return b;
+  }
+
   /** Same as {@link #testJoin} using USING. */
   @Test void testJoinUsing() {
     final RelBuilder builder = RelBuilder.create(config().build());
diff --git a/core/src/test/java/org/apache/calcite/test/SqlToRelConverterTest.java b/core/src/test/java/org/apache/calcite/test/SqlToRelConverterTest.java
index 07ab07e..c73881a 100644
--- a/core/src/test/java/org/apache/calcite/test/SqlToRelConverterTest.java
+++ b/core/src/test/java/org/apache/calcite/test/SqlToRelConverterTest.java
@@ -1525,6 +1525,21 @@ class SqlToRelConverterTest extends SqlToRelTestBase {
     sql("select*from unnest(array(select*from dept))").ok();
   }
 
+  @Test void testUnnestArrayNoExpand() {
+    final String sql = "select name,\n"
+        + "    array (select *\n"
+        + "        from emp\n"
+        + "        where deptno = dept.deptno) as emp_array,\n"
+        + "    multiset (select *\n"
+        + "        from emp\n"
+        + "        where deptno = dept.deptno) as emp_multiset,\n"
+        + "    map (select empno, job\n"
+        + "        from emp\n"
+        + "        where deptno = dept.deptno) as job_map\n"
+        + "from dept";
+    sql(sql).expand(false).ok();
+  }
+
   @Test void testUnnestWithOrdinality() {
     final String sql =
         "select*from unnest(array(select*from dept)) with ordinality";
@@ -1552,17 +1567,50 @@ class SqlToRelConverterTest extends SqlToRelTestBase {
   }
 
   @Test void testCorrelationJoin() {
+    checkCorrelationJoin(true);
+  }
+
+  @Test void testCorrelationJoinRex() {
+    checkCorrelationJoin(false);
+  }
+
+  void checkCorrelationJoin(boolean expand) {
     final String sql = "select *,\n"
         + "  multiset(select * from emp where deptno=dept.deptno) as empset\n"
         + "from dept";
-    sql(sql).ok();
+    sql(sql).expand(expand).ok();
   }
 
-  @Test void testCorrelationJoinRex() {
+  @Test void testCorrelatedArraySubQuery() {
+    checkCorrelatedArraySubQuery(true);
+  }
+
+  @Test void testCorrelatedArraySubQueryRex() {
+    checkCorrelatedArraySubQuery(false);
+  }
+
+  void checkCorrelatedArraySubQuery(boolean expand) {
     final String sql = "select *,\n"
-        + "  multiset(select * from emp where deptno=dept.deptno) as empset\n"
+        + "    array (select * from emp\n"
+        + "        where deptno = dept.deptno) as empset\n"
         + "from dept";
-    sql(sql).expand(false).ok();
+    sql(sql).expand(expand).ok();
+  }
+
+  @Test void testCorrelatedMapSubQuery() {
+    checkCorrelatedMapSubQuery(true);
+  }
+
+  @Test void testCorrelatedMapSubQueryRex() {
+    checkCorrelatedMapSubQuery(false);
+  }
+
+  void checkCorrelatedMapSubQuery(boolean expand) {
+    final String sql = "select *,\n"
+        + "  map (select empno, job\n"
+        + "       from emp where deptno = dept.deptno) as jobMap\n"
+        + "from dept";
+    sql(sql).expand(expand).ok();
   }
 
   /** Test case for
diff --git a/core/src/test/resources/org/apache/calcite/test/SqlToRelConverterTest.xml b/core/src/test/resources/org/apache/calcite/test/SqlToRelConverterTest.xml
index 06adac4..0213f4e 100644
--- a/core/src/test/resources/org/apache/calcite/test/SqlToRelConverterTest.xml
+++ b/core/src/test/resources/org/apache/calcite/test/SqlToRelConverterTest.xml
@@ -590,6 +590,80 @@ LogicalProject(DEPTNO=[$0], ENAME=[$1])
 ]]>
     </Resource>
   </TestCase>
+  <TestCase name="testCorrelatedArraySubQuery">
+    <Resource name="sql">
+      <![CDATA[select *,
+    array (select * from emp
+        where deptno = dept.deptno) as empset
+from dept]]>
+    </Resource>
+    <Resource name="plan">
+      <![CDATA[
+LogicalProject(DEPTNO=[$0], NAME=[$1], EMPSET=[$2])
+  LogicalCorrelate(correlation=[$cor0], joinType=[inner], requiredColumns=[{0}])
+    LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
+    Collect(field=[EXPR$0])
+      LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8])
+        LogicalFilter(condition=[=($7, $cor0.DEPTNO)])
+          LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testCorrelatedArraySubQueryRex">
+    <Resource name="sql">
+      <![CDATA[select *,
+    array (select * from emp
+        where deptno = dept.deptno) as empset
+from dept]]>
+    </Resource>
+    <Resource name="plan">
+      <![CDATA[
+LogicalProject(DEPTNO=[$0], NAME=[$1], EMPSET=[ARRAY({
+LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8])
+  LogicalFilter(condition=[=($7, $cor0.DEPTNO)])
+    LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+})])
+  LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testCorrelatedMapSubQuery">
+    <Resource name="sql">
+      <![CDATA[select *,
+  map (select empno, job
+       from emp where deptno = dept.deptno) as jobMap
+from dept]]>
+    </Resource>
+    <Resource name="plan">
+      <![CDATA[
+LogicalProject(DEPTNO=[$0], NAME=[$1], JOBMAP=[$2])
+  LogicalCorrelate(correlation=[$cor0], joinType=[inner], requiredColumns=[{0}])
+    LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
+    Collect(field=[EXPR$0])
+      LogicalProject(EMPNO=[$0], JOB=[$2])
+        LogicalFilter(condition=[=($7, $cor0.DEPTNO)])
+          LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testCorrelatedMapSubQueryRex">
+    <Resource name="sql">
+      <![CDATA[select *,
+  map (select empno, job
+       from emp where deptno = dept.deptno) as jobMap
+from dept]]>
+    </Resource>
+    <Resource name="plan">
+      <![CDATA[
+LogicalProject(DEPTNO=[$0], NAME=[$1], JOBMAP=[MAP({
+LogicalProject(EMPNO=[$0], JOB=[$2])
+  LogicalFilter(condition=[=($7, $cor0.DEPTNO)])
+    LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+})])
+  LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
+]]>
+    </Resource>
+  </TestCase>
   <TestCase name="testCorrelatedSubQueryInAggregate">
     <Resource name="sql">
       <![CDATA[SELECT SUM(
@@ -832,13 +906,12 @@ from dept]]>
     </Resource>
     <Resource name="plan">
       <![CDATA[
-LogicalProject(DEPTNO=[$0], NAME=[$1], EMPSET=[$2])
-  LogicalCorrelate(correlation=[$cor0], joinType=[inner], requiredColumns=[{0}])
-    LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
-    Collect(field=[EXPR$0])
-      LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8])
-        LogicalFilter(condition=[=($7, $cor0.DEPTNO)])
-          LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+LogicalProject(DEPTNO=[$0], NAME=[$1], EMPSET=[MULTISET({
+LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8])
+  LogicalFilter(condition=[=($7, $cor0.DEPTNO)])
+    LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+})])
+  LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
 ]]>
     </Resource>
   </TestCase>
@@ -6965,6 +7038,39 @@ LogicalProject(DEPTNO=[$0], EMPNO_AVG=[$7])
 ]]>
     </Resource>
   </TestCase>
+  <TestCase name="testUnnestArrayNoExpand">
+    <Resource name="sql">
+      <![CDATA[select name,
+    array (select *
+        from emp
+        where deptno = dept.deptno) as emp_array,
+    multiset (select *
+        from emp
+        where deptno = dept.deptno) as emp_multiset,
+    map (select empno, job
+        from emp
+        where deptno = dept.deptno) as job_map
+from dept]]>
+    </Resource>
+    <Resource name="plan">
+      <![CDATA[
+LogicalProject(NAME=[$1], EMP_ARRAY=[ARRAY({
+LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8])
+  LogicalFilter(condition=[=($7, $cor0.DEPTNO)])
+    LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+})], EMP_MULTISET=[MULTISET({
+LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8])
+  LogicalFilter(condition=[=($7, $cor1.DEPTNO)])
+    LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+})], JOB_MAP=[MAP({
+LogicalProject(EMPNO=[$0], JOB=[$2])
+  LogicalFilter(condition=[=($7, $cor2.DEPTNO)])
+    LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+})])
+  LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
+]]>
+    </Resource>
+  </TestCase>
   <TestCase name="testUnnestArrayPlan">
     <Resource name="sql">
       <![CDATA[select d.deptno, e2.empno
diff --git a/core/src/test/resources/sql/join.iq b/core/src/test/resources/sql/join.iq
index ccab10b..cd8b36b 100644
--- a/core/src/test/resources/sql/join.iq
+++ b/core/src/test/resources/sql/join.iq
@@ -71,6 +71,48 @@ EnumerableNestedLoopJoin(condition=[OR(=($1, $3), =(CAST($0):CHAR(11) NOT NULL,
 
 !use scott
 
+# Full join with USING
+select *
+from (select * from emp where deptno <> 10) as e
+full join (select * from dept where deptno <> 20) as d
+  using (deptno);
++--------+-------+--------+----------+------+------------+---------+---------+------------+----------+
+| DEPTNO | EMPNO | ENAME  | JOB      | MGR  | HIREDATE   | SAL     | COMM    | DNAME      | LOC      |
++--------+-------+--------+----------+------+------------+---------+---------+------------+----------+
+|     10 |       |        |          |      |            |         |         | ACCOUNTING | NEW YORK |
+|     20 |  7369 | SMITH  | CLERK    | 7902 | 1980-12-17 |  800.00 |         |            |          |
+|     20 |  7566 | JONES  | MANAGER  | 7839 | 1981-02-04 | 2975.00 |         |            |          |
+|     20 |  7788 | SCOTT  | ANALYST  | 7566 | 1987-04-19 | 3000.00 |         |            |          |
+|     20 |  7876 | ADAMS  | CLERK    | 7788 | 1987-05-23 | 1100.00 |         |            |          |
+|     20 |  7902 | FORD   | ANALYST  | 7566 | 1981-12-03 | 3000.00 |         |            |          |
+|     30 |  7499 | ALLEN  | SALESMAN | 7698 | 1981-02-20 | 1600.00 |  300.00 | SALES      | CHICAGO  |
+|     30 |  7521 | WARD   | SALESMAN | 7698 | 1981-02-22 | 1250.00 |  500.00 | SALES      | CHICAGO  |
+|     30 |  7654 | MARTIN | SALESMAN | 7698 | 1981-09-28 | 1250.00 | 1400.00 | SALES      | CHICAGO  |
+|     30 |  7698 | BLAKE  | MANAGER  | 7839 | 1981-01-05 | 2850.00 |         | SALES      | CHICAGO  |
+|     30 |  7844 | TURNER | SALESMAN | 7698 | 1981-09-08 | 1500.00 |    0.00 | SALES      | CHICAGO  |
+|     30 |  7900 | JAMES  | CLERK    | 7698 | 1981-12-03 |  950.00 |         | SALES      | CHICAGO  |
+|     40 |       |        |          |      |            |         |         | OPERATIONS | BOSTON   |
++--------+-------+--------+----------+------+------------+---------+---------+------------+----------+
+(13 rows)
+
+!ok
+
+# Unqualified column names and USING
+select distinct deptno, dept.deptno, emp.deptno
+from emp
+right join dept using (deptno);
++--------+--------+--------+
+| DEPTNO | DEPTNO | DEPTNO |
++--------+--------+--------+
+|     10 |     10 |     10 |
+|     20 |     20 |     20 |
+|     30 |     30 |     30 |
+|     40 |     40 |        |
++--------+--------+--------+
+(4 rows)
+
+!ok
+
 # Push aggregate through join
 select distinct dept.deptno, emp.deptno
 from "scott".emp join "scott".dept using (deptno);