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

danny0405 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 5212d6c  [CALCITE-3233] Support Row type for SqlDataTypeSpec
5212d6c is described below

commit 5212d6c47e36995943f4d955a1714bf03eb08e7e
Author: yuzhao.cyz <[email protected]>
AuthorDate: Fri Aug 9 14:45:07 2019 +0800

    [CALCITE-3233] Support Row type for SqlDataTypeSpec
---
 core/src/main/codegen/config.fmpp                  |   2 +
 core/src/main/codegen/templates/Parser.jj          |  86 +++++++++++++++-
 .../org/apache/calcite/sql/SqlDataTypeSpec.java    |  72 +++++++++-----
 .../org/apache/calcite/sql/SqlRowTypeSpec.java     | 109 +++++++++++++++++++++
 .../org/apache/calcite/sql/SqlTypeNameSpec.java    |  44 +++++++++
 .../apache/calcite/sql/parser/SqlParserTest.java   |  19 ++++
 .../org/apache/calcite/test/SqlValidatorTest.java  |  31 ++++++
 .../org/apache/calcite/test/catalog/Fixture.java   |  50 ++++++++++
 .../test/catalog/MockCatalogReaderExtended.java    |  13 +++
 server/src/main/codegen/includes/parserImpls.ftl   |  16 +--
 site/_docs/reference.md                            |  78 +++++++++++++--
 11 files changed, 474 insertions(+), 46 deletions(-)

diff --git a/core/src/main/codegen/config.fmpp 
b/core/src/main/codegen/config.fmpp
index 8fc9cce..4708633 100644
--- a/core/src/main/codegen/config.fmpp
+++ b/core/src/main/codegen/config.fmpp
@@ -374,6 +374,8 @@ data: {
 
     # List of methods for parsing custom data types.
     # Return type of method implementation should be "SqlIdentifier".
+    # Return "SqlTypeNameSpec" if you want to customize sql unparsing
+    # and data type deriving.
     # Example: SqlParseTimeStampZ().
     dataTypeParserMethods: [
     ]
diff --git a/core/src/main/codegen/templates/Parser.jj 
b/core/src/main/codegen/templates/Parser.jj
index 6c19c34..44fffed 100644
--- a/core/src/main/codegen/templates/Parser.jj
+++ b/core/src/main/codegen/templates/Parser.jj
@@ -82,6 +82,7 @@ import org.apache.calcite.sql.SqlOperator;
 import org.apache.calcite.sql.SqlOrderBy;
 import org.apache.calcite.sql.SqlPostfixOperator;
 import org.apache.calcite.sql.SqlPrefixOperator;
+import org.apache.calcite.sql.SqlRowTypeSpec;
 import org.apache.calcite.sql.SqlSampleSpec;
 import org.apache.calcite.sql.SqlSelect;
 import org.apache.calcite.sql.SqlSelectKeyword;
@@ -89,6 +90,7 @@ import org.apache.calcite.sql.SqlSetOption;
 import org.apache.calcite.sql.SqlSnapshot;
 import org.apache.calcite.sql.SqlTimeLiteral;
 import org.apache.calcite.sql.SqlTimestampLiteral;
+import org.apache.calcite.sql.SqlTypeNameSpec;
 import org.apache.calcite.sql.SqlUnnestOperator;
 import org.apache.calcite.sql.SqlUpdate;
 import org.apache.calcite.sql.SqlUtil;
@@ -4591,13 +4593,13 @@ SqlDataTypeSpec DataType() :
 SqlIdentifier TypeName() :
 {
     final SqlTypeName sqlTypeName;
-    final SqlIdentifier typeName;
+    final SqlIdentifier typeName; // typeName can be a SqlIdentifier or 
SqlTypeNameSpec
     final Span s = Span.of();
 }
 {
     (
 <#-- additional types are included here -->
-<#-- make custom data types in front of Calcite core data types -->
+<#-- put custom data types in front of Calcite core data types -->
 <#list parser.dataTypeParserMethods as method>
         LOOKAHEAD(2)
         typeName = ${method}
@@ -4611,6 +4613,8 @@ SqlIdentifier TypeName() :
         LOOKAHEAD(2)
         typeName = CollectionsTypeName()
     |
+        typeName = RowTypeName()
+    |
         typeName = CompoundIdentifier()
     )
     {
@@ -4739,6 +4743,84 @@ SqlIdentifier CollectionsTypeName() :
 }
 
 /**
+* Parse a nullable option, default is true.
+*/
+boolean NullableOptDefaultTrue() :
+{
+}
+{
+    <NULL> { return true; }
+|
+    <NOT> <NULL> { return false; }
+|
+    { return true; }
+}
+
+/**
+* Parse a nullable option, default is false.
+*/
+boolean NullableOptDefaultFalse() :
+{
+}
+{
+    <NULL> { return true; }
+|
+    <NOT> <NULL> { return false; }
+|
+    { return false; }
+}
+
+/**
+* Parse a "name1 type1 [NULL | NOT NULL], name2 type2 [NULL | NOT NULL] ..." 
list,
+* the field type default is not nullable.
+*/
+void FieldNameTypeCommaList(
+        List<SqlIdentifier> fieldNames,
+        List<SqlDataTypeSpec> fieldTypes) :
+{
+    SqlIdentifier fName;
+    SqlDataTypeSpec fType;
+    boolean nullable;
+}
+{
+    fName = SimpleIdentifier()
+    fType = DataType()
+    nullable = NullableOptDefaultFalse()
+    {
+        fieldNames.add(fName);
+        fieldTypes.add(fType.withNullable(nullable));
+    }
+    (
+        <COMMA>
+        fName = SimpleIdentifier()
+        fType = DataType()
+        nullable = NullableOptDefaultFalse()
+        {
+            fieldNames.add(fName);
+            fieldTypes.add(fType.withNullable(nullable));
+        }
+    )*
+}
+
+/**
+* Parse Row type with format: Row(name1 type1, name2 type2).
+* Every item type can have suffix of `NULL` or `NOT NULL` to indicate if this 
type is nullable.
+* i.e. Row(f0 int not null, f1 varchar null).
+*/
+SqlIdentifier RowTypeName() :
+{
+    List<SqlIdentifier> fieldNames = new ArrayList<SqlIdentifier>();
+    List<SqlDataTypeSpec> fieldTypes = new ArrayList<SqlDataTypeSpec>();
+}
+{
+    <ROW>
+    <LPAREN> FieldNameTypeCommaList(fieldNames, fieldTypes) <RPAREN>
+    {
+        return new SqlRowTypeSpec(getPos(), fieldNames, fieldTypes);
+    }
+}
+
+/**
  * Parses a CURSOR(query) expression.  The parser allows these
  * anywhere, but the validator restricts them to appear only as
  * arguments to table functions.
diff --git a/core/src/main/java/org/apache/calcite/sql/SqlDataTypeSpec.java 
b/core/src/main/java/org/apache/calcite/sql/SqlDataTypeSpec.java
index 0f4f70e..ff21221 100644
--- a/core/src/main/java/org/apache/calcite/sql/SqlDataTypeSpec.java
+++ b/core/src/main/java/org/apache/calcite/sql/SqlDataTypeSpec.java
@@ -42,15 +42,19 @@ import static org.apache.calcite.util.Static.RESOURCE;
  *
  * <p>todo: This should really be a subtype of {@link SqlCall}.</p>
  *
- * <p>In its full glory, we will have to support complex type expressions
+ * <p>we support complex type expressions
  * like:</p>
  *
  * <blockquote><code>ROW(<br>
- *   NUMBER(5, 2) NOT NULL AS foo,<br>
- *   ROW(BOOLEAN AS b, MyUDT NOT NULL AS i) AS rec)</code></blockquote>
+ *   foo NUMBER(5, 2) NOT NULL,<br>
+ *   rec ROW(b BOOLEAN, i MyUDT NOT NULL))</code></blockquote>
  *
- * <p>Currently it only supports simple datatypes like CHAR, VARCHAR and 
DOUBLE,
+ * <p>Internally we use {@link SqlTypeNameSpec} to specify such complex data 
types.
+ *
+ * <p>We support simple data types like CHAR, VARCHAR and DOUBLE,
  * with optional precision and scale.</p>
+ *
+ * <p>Internally we use {@link SqlIdentifier} to specify simple data types.
  */
 public class SqlDataTypeSpec extends SqlNode {
   //~ Instance fields --------------------------------------------------------
@@ -212,7 +216,12 @@ public class SqlDataTypeSpec extends SqlNode {
       int leftPrec,
       int rightPrec) {
     String name = typeName.getSimple();
-    if (SqlTypeName.get(name) != null) {
+    if (typeName instanceof SqlTypeNameSpec) {
+      typeName.unparse(writer, leftPrec, rightPrec);
+      if (collectionsTypeName != null) {
+        writer.keyword(collectionsTypeName.getSimple());
+      }
+    } else if (SqlTypeName.get(name) != null) {
       SqlTypeName sqlTypeName = SqlTypeName.get(name);
 
       // we have a built-in data type
@@ -327,25 +336,33 @@ public class SqlDataTypeSpec extends SqlNode {
     if (!typeName.isSimple()) {
       return null;
     }
-    final String name = typeName.getSimple();
-    final SqlTypeName sqlTypeName = SqlTypeName.get(name);
-    if (sqlTypeName == null) {
-      return null;
-    }
-
-    // NOTE jvs 15-Jan-2009:  earlier validation is supposed to
-    // have caught these, which is why it's OK for them
-    // to be assertions rather than user-level exceptions.
     RelDataType type;
-    if ((precision >= 0) && (scale >= 0)) {
-      assert sqlTypeName.allowsPrecScale(true, true);
-      type = typeFactory.createSqlType(sqlTypeName, precision, scale);
-    } else if (precision >= 0) {
-      assert sqlTypeName.allowsPrecNoScale();
-      type = typeFactory.createSqlType(sqlTypeName, precision);
+    if (typeName instanceof SqlTypeNameSpec) {
+      // Create type directly if this typeName is a SqlTypeNameSpec.
+      type = createTypeFromTypeNameSpec(typeFactory, (SqlTypeNameSpec) 
typeName);
+      if (type == null) {
+        return null;
+      }
     } else {
-      assert sqlTypeName.allowsNoPrecNoScale();
-      type = typeFactory.createSqlType(sqlTypeName);
+      final String name = typeName.getSimple();
+      final SqlTypeName sqlTypeName = SqlTypeName.get(name);
+      if (sqlTypeName == null) {
+        return null;
+      }
+
+      // NOTE jvs 15-Jan-2009:  earlier validation is supposed to
+      // have caught these, which is why it's OK for them
+      // to be assertions rather than user-level exceptions.
+      if ((precision >= 0) && (scale >= 0)) {
+        assert sqlTypeName.allowsPrecScale(true, true);
+        type = typeFactory.createSqlType(sqlTypeName, precision, scale);
+      } else if (precision >= 0) {
+        assert sqlTypeName.allowsPrecNoScale();
+        type = typeFactory.createSqlType(sqlTypeName, precision);
+      } else {
+        assert sqlTypeName.allowsNoPrecNoScale();
+        type = typeFactory.createSqlType(sqlTypeName);
+      }
     }
 
     if (SqlTypeUtil.inCharFamily(type)) {
@@ -399,6 +416,17 @@ public class SqlDataTypeSpec extends SqlNode {
 
     return type;
   }
+
+  /**
+   * Create type from the type name specification directly.
+   * @param typeFactory type factory.
+   * @return the type.
+   */
+  private RelDataType createTypeFromTypeNameSpec(
+      RelDataTypeFactory typeFactory,
+      SqlTypeNameSpec typeNameSpec) {
+    return typeNameSpec.deriveType(typeFactory);
+  }
 }
 
 // End SqlDataTypeSpec.java
diff --git a/core/src/main/java/org/apache/calcite/sql/SqlRowTypeSpec.java 
b/core/src/main/java/org/apache/calcite/sql/SqlRowTypeSpec.java
new file mode 100644
index 0000000..e75166c
--- /dev/null
+++ b/core/src/main/java/org/apache/calcite/sql/SqlRowTypeSpec.java
@@ -0,0 +1,109 @@
+/*
+ * 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.
+ */
+package org.apache.calcite.sql;
+
+import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.rel.type.RelDataTypeFactory;
+import org.apache.calcite.sql.parser.SqlParserPos;
+import org.apache.calcite.sql.type.SqlTypeName;
+import org.apache.calcite.util.Pair;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+/**
+ * A sql type specification of row type, the grammar definition in SQL-2011 
IWD 9075-2:201?(E)
+ * 6.1 &lt;data type&gt; is as following:
+ * <blockquote><pre>
+ * &lt;row type&gt; ::=
+ *   ROW &lt;row type body&gt;
+ * &lt;row type body&gt; ::=
+ *   &lt;left paren&gt; &lt;field definition&gt;
+ *   [ { &lt;comma&gt; &lt;field definition&gt; }... ]
+ *   &lt;right paren&gt;
+ *
+ * &lt;field definition&gt; ::=
+ *   &lt;field name&gt; &lt;data type&gt;
+ * </pre></blockquote>
+ *
+ * <p>We also support to add a [ NULL | NOT NULL ] suffix for every field 
type, i.e.
+ * Row(f0 int null, f1 varchar not null), the default is not nullable.
+ */
+public class SqlRowTypeSpec extends SqlTypeNameSpec {
+
+  private final List<SqlIdentifier> fieldNames;
+  private final List<SqlDataTypeSpec> fieldTypes;
+
+  /**
+   * Creates a row type specification.
+   *
+   * @param pos        The parser position.
+   * @param fieldNames The field names.
+   * @param fieldTypes The field data types.
+   */
+  public SqlRowTypeSpec(
+      SqlParserPos pos,
+      List<SqlIdentifier> fieldNames,
+      List<SqlDataTypeSpec> fieldTypes) {
+    super(SqlTypeName.ROW.getName(), pos);
+    Objects.requireNonNull(fieldNames);
+    Objects.requireNonNull(fieldTypes);
+    assert fieldNames.size() > 0; // there must be at least one field.
+    this.fieldNames = fieldNames;
+    this.fieldTypes = fieldTypes;
+  }
+
+  public List<SqlIdentifier> getFieldNames() {
+    return fieldNames;
+  }
+
+  public List<SqlDataTypeSpec> getFieldTypes() {
+    return fieldTypes;
+  }
+
+  public int getArity() {
+    return fieldNames.size();
+  }
+
+  @Override public void unparse(SqlWriter writer, int leftPrec, int rightPrec) 
{
+    writer.print(SqlTypeName.ROW.getName());
+    SqlWriter.Frame frame = writer.startList(SqlWriter.FrameTypeEnum.FUN_CALL, 
"(", ")");
+    for (Pair<SqlIdentifier, SqlDataTypeSpec> p : Pair.zip(this.fieldNames, 
this.fieldTypes)) {
+      writer.sep(",", false);
+      p.left.unparse(writer, 0, 0);
+      p.right.unparse(writer, leftPrec, rightPrec);
+      if (p.right.getNullable() != null && p.right.getNullable()) {
+        // Row fields default is not nullable.
+        writer.print("NULL");
+      }
+    }
+    writer.endList(frame);
+  }
+
+  @Override public RelDataType deriveType(RelDataTypeFactory typeFactory) {
+    return typeFactory.createStructType(
+        fieldTypes.stream()
+            .map(dt -> dt.deriveType(typeFactory))
+            .collect(Collectors.toList()),
+        fieldNames.stream()
+            .map(SqlIdentifier::toString)
+            .collect(Collectors.toList()));
+  }
+}
+
+// End SqlRowTypeSpec.java
diff --git a/core/src/main/java/org/apache/calcite/sql/SqlTypeNameSpec.java 
b/core/src/main/java/org/apache/calcite/sql/SqlTypeNameSpec.java
new file mode 100644
index 0000000..18a4b90
--- /dev/null
+++ b/core/src/main/java/org/apache/calcite/sql/SqlTypeNameSpec.java
@@ -0,0 +1,44 @@
+/*
+ * 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.
+ */
+package org.apache.calcite.sql;
+
+import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.rel.type.RelDataTypeFactory;
+import org.apache.calcite.sql.parser.SqlParserPos;
+
+/**
+ * A <code>SqlTypeNameSpec</code> is a type name that allows user to
+ * customize sql node unparsing and data type deriving.
+ *
+ * <p>To customize sql node unparsing, override the method {@link 
#unparse(SqlWriter, int, int)}.
+ */
+public abstract class SqlTypeNameSpec extends SqlIdentifier {
+
+  /**
+   * Creates a {@code SqlTypeNameSpec}.
+   *
+   * @param name Name of the type.
+   * @param pos  Parser position, must not be null.
+   */
+  SqlTypeNameSpec(String name, SqlParserPos pos) {
+    super(name, pos);
+  }
+
+  public abstract RelDataType deriveType(RelDataTypeFactory typeFactory);
+}
+
+// End SqlTypeNameSpec.java
diff --git 
a/core/src/test/java/org/apache/calcite/sql/parser/SqlParserTest.java 
b/core/src/test/java/org/apache/calcite/sql/parser/SqlParserTest.java
index 3070da0..f45d2fe 100644
--- a/core/src/test/java/org/apache/calcite/sql/parser/SqlParserTest.java
+++ b/core/src/test/java/org/apache/calcite/sql/parser/SqlParserTest.java
@@ -4710,6 +4710,25 @@ public class SqlParserTest {
         "(?s).*Encountered \"<\" at line 1, column 20.\n.*");
   }
 
+  @Test public void testCastAsRowType() {
+    checkExp("cast(a as row(f0 int, f1 varchar))",
+        "CAST(`A` AS ROW(`F0` INTEGER, `F1` VARCHAR))");
+    checkExp("cast(a as row(f0 int not null, f1 varchar null))",
+        "CAST(`A` AS ROW(`F0` INTEGER, `F1` VARCHAR NULL))");
+//    // test nested row type.
+    checkExp("cast(a as row("
+        + "f0 row(ff0 int not null, ff1 varchar null) null, "
+        + "f1 timestamp not null))",
+        "CAST(`A` AS ROW("
+            + "`F0` ROW(`FF0` INTEGER, `FF1` VARCHAR NULL) NULL, "
+            + "`F1` TIMESTAMP))");
+    // test row type in collection data types.
+    checkExp("cast(a as row(f0 bigint not null, f1 decimal null) array)",
+        "CAST(`A` AS ROW(`F0` BIGINT, `F1` DECIMAL NULL) ARRAY)");
+    checkExp("cast(a as row(f0 varchar not null, f1 timestamp null) multiset)",
+        "CAST(`A` AS ROW(`F0` VARCHAR, `F1` TIMESTAMP NULL) MULTISET)");
+  }
+
   @Test public void testMapValueConstructor() {
     checkExp("map[1, 'x', 2, 'y']", "(MAP[1, 'x', 2, 'y'])");
     checkExp("map [1, 'x', 2, 'y']", "(MAP[1, 'x', 2, 'y'])");
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 d92e78b..6c7921f 100644
--- a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
+++ b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
@@ -7710,6 +7710,37 @@ public class SqlValidatorTest extends 
SqlValidatorTestCase {
         .columnType("VARCHAR(5) NOT NULL ARRAY NOT NULL");
   }
 
+  @Test public void testCastAsRowType() {
+    sql("select cast(a as row(f0 int, f1 varchar)) from COMPLEXTYPES.CTC_T1")
+        .withExtendedCatalog()
+        .columnType("RecordType(INTEGER NOT NULL F0, VARCHAR NOT NULL F1) NOT 
NULL");
+    sql("select cast(b as row(f0 int not null, f1 varchar null))\n"
+            + "from COMPLEXTYPES.CTC_T1")
+        .withExtendedCatalog()
+        .columnType("RecordType(INTEGER NOT NULL F0, VARCHAR F1) NOT NULL");
+    // test nested row type.
+    sql("select "
+            + "cast(c as row("
+            + "f0 row(ff0 int not null, ff1 varchar null) null, "
+            + "f1 timestamp not null))"
+            + " from COMPLEXTYPES.CTC_T1")
+        .withExtendedCatalog()
+        .columnType("RecordType("
+            + "RecordType(INTEGER FF0, VARCHAR FF1) F0, "
+            + "TIMESTAMP(0) NOT NULL F1) NOT NULL");
+    // test row type in collection data types.
+    sql("select cast(d as row(f0 bigint not null, f1 decimal null) array)\n"
+        + "from COMPLEXTYPES.CTC_T1")
+        .withExtendedCatalog()
+        .columnType("RecordType(BIGINT NOT NULL F0, DECIMAL(19, 0) F1) NOT 
NULL "
+            + "ARRAY NOT NULL");
+    sql("select cast(e as row(f0 varchar not null, f1 timestamp null) 
multiset)\n"
+        + "from COMPLEXTYPES.CTC_T1")
+        .withExtendedCatalog()
+        .columnType("RecordType(VARCHAR NOT NULL F0, TIMESTAMP(0) F1) NOT NULL 
"
+            + "MULTISET NOT NULL");
+  }
+
   @Test public void testMultisetConstructor() {
     sql("select multiset[1,null,2] as a from (values (1))")
         .columnType("INTEGER MULTISET NOT NULL");
diff --git a/core/src/test/java/org/apache/calcite/test/catalog/Fixture.java 
b/core/src/test/java/org/apache/calcite/test/catalog/Fixture.java
index 77451bc..45471bf 100644
--- a/core/src/test/java/org/apache/calcite/test/catalog/Fixture.java
+++ b/core/src/test/java/org/apache/calcite/test/catalog/Fixture.java
@@ -19,6 +19,7 @@ package org.apache.calcite.test.catalog;
 import org.apache.calcite.rel.type.RelDataType;
 import org.apache.calcite.rel.type.RelDataTypeComparability;
 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.StructKind;
 import org.apache.calcite.sql.SqlIdentifier;
@@ -27,6 +28,7 @@ import org.apache.calcite.sql.type.ObjectSqlType;
 import org.apache.calcite.sql.type.SqlTypeName;
 
 import java.util.Arrays;
+import java.util.stream.Collectors;
 
 /** Types used during initialization. */
 final class Fixture {
@@ -34,6 +36,8 @@ final class Fixture {
   final RelDataType intTypeNull;
   final RelDataType bigintType;
   final RelDataType bigintTypeNull;
+  final RelDataType decimalType;
+  final RelDataType varcharType;
   final RelDataType varchar10Type;
   final RelDataType varchar10TypeNull;
   final RelDataType varchar20Type;
@@ -51,12 +55,24 @@ final class Fixture {
   final RelDataType empRecordType;
   final RelDataType empListType;
   final ObjectSqlType addressType;
+  // Row(f0 int, f1 varchar)
+  final RelDataType recordType1;
+  // Row(f0 int not null, f1 varchar null)
+  final RelDataType recordType2;
+  // Row(f0 Row(ff0 int not null, ff1 varchar null) null, f1 timestamp not 
null)
+  final RelDataType recordType3;
+  // Row(f0 bigint not null, f1 decimal null) array
+  final RelDataType recordType4;
+  // Row(f0 varchar not null, f1 timestamp null) multiset
+  final RelDataType recordType5;
 
   Fixture(RelDataTypeFactory typeFactory) {
     intType = typeFactory.createSqlType(SqlTypeName.INTEGER);
     intTypeNull = typeFactory.createTypeWithNullability(intType, true);
     bigintType = typeFactory.createSqlType(SqlTypeName.BIGINT);
     bigintTypeNull = typeFactory.createTypeWithNullability(bigintType, true);
+    decimalType = typeFactory.createSqlType(SqlTypeName.DECIMAL);
+    varcharType = typeFactory.createSqlType(SqlTypeName.VARCHAR);
     varchar10Type = typeFactory.createSqlType(SqlTypeName.VARCHAR, 10);
     varchar10TypeNull = typeFactory.createTypeWithNullability(varchar10Type, 
true);
     varchar20Type = typeFactory.createSqlType(SqlTypeName.VARCHAR, 20);
@@ -124,6 +140,40 @@ final class Fixture {
                 new RelDataTypeFieldImpl("ZIP", 2, intType),
                 new RelDataTypeFieldImpl("STATE", 3, varchar20Type)),
             RelDataTypeComparability.NONE);
+    // Row(f0 int, f1 varchar)
+    recordType1 = typeFactory.createStructType(
+        Arrays.asList(intType, varcharType),
+        Arrays.asList("f0", "f1"));
+    // Row(f0 int not null, f1 varchar null)
+    recordType2 = typeFactory.createStructType(
+        Arrays.asList(intType,
+            nullable(typeFactory, varcharType)),
+        Arrays.asList("f0", "f1"));
+    // Row(f0 Row(ff0 int not null, ff1 varchar null) null, f1 timestamp not 
null)
+    recordType3 = typeFactory.createStructType(
+        Arrays.asList(
+            nullable(typeFactory,
+                typeFactory.createStructType(
+                    recordType2.getFieldList().stream()
+                        
.map(RelDataTypeField::getType).collect(Collectors.toList()),
+                    Arrays.asList("ff0", "ff1"))), timestampType),
+        Arrays.asList("f0", "f1"));
+    // Row(f0 bigint not null, f1 decimal null) array
+    recordType4 = typeFactory.createArrayType(
+        typeFactory.createStructType(
+            Arrays.asList(bigintType, nullable(typeFactory, decimalType)),
+            Arrays.asList("f0", "f1")),
+        -1);
+    // Row(f0 varchar not null, f1 timestamp null) multiset
+    recordType5 = typeFactory.createMultisetType(
+        typeFactory.createStructType(
+            Arrays.asList(varcharType, nullable(typeFactory, timestampType)),
+            Arrays.asList("f0", "f1")),
+        -1);
+  }
+
+  private static RelDataType nullable(RelDataTypeFactory typeFactory, 
RelDataType type) {
+    return typeFactory.createTypeWithNullability(type, true);
   }
 }
 
diff --git 
a/core/src/test/java/org/apache/calcite/test/catalog/MockCatalogReaderExtended.java
 
b/core/src/test/java/org/apache/calcite/test/catalog/MockCatalogReaderExtended.java
index 4311369..1d3f249 100644
--- 
a/core/src/test/java/org/apache/calcite/test/catalog/MockCatalogReaderExtended.java
+++ 
b/core/src/test/java/org/apache/calcite/test/catalog/MockCatalogReaderExtended.java
@@ -137,6 +137,19 @@ public class MockCatalogReaderExtended extends 
MockCatalogReaderSimple {
     registerTable(virtualColumnsTable1);
     registerTable(virtualColumnsTable2);
 
+    // Register table with complex data type rows.
+    MockSchema complexTypeColumnsSchema = new MockSchema("COMPLEXTYPES");
+    registerSchema(complexTypeColumnsSchema);
+    final MockTable complexTypeColumnsTable =
+        MockTable.create(this, complexTypeColumnsSchema, "CTC_T1",
+            false, 100);
+    complexTypeColumnsTable.addColumn("A", f.recordType1);
+    complexTypeColumnsTable.addColumn("B", f.recordType2);
+    complexTypeColumnsTable.addColumn("C", f.recordType3);
+    complexTypeColumnsTable.addColumn("D", f.recordType4);
+    complexTypeColumnsTable.addColumn("E", f.recordType5);
+    registerTable(complexTypeColumnsTable);
+
     return this;
   }
 }
diff --git a/server/src/main/codegen/includes/parserImpls.ftl 
b/server/src/main/codegen/includes/parserImpls.ftl
index e135152..6fa9c3b 100644
--- a/server/src/main/codegen/includes/parserImpls.ftl
+++ b/server/src/main/codegen/includes/parserImpls.ftl
@@ -131,13 +131,7 @@ void TableElement(List<SqlNode> list) :
     LOOKAHEAD(2) id = SimpleIdentifier()
     (
         type = DataType()
-        (
-            <NULL> { nullable = true; }
-        |
-            <NOT> <NULL> { nullable = false; }
-        |
-            { nullable = true; }
-        )
+        nullable = NullableOptDefaultTrue()
         (
             [ <GENERATED> <ALWAYS> ] <AS> <LPAREN>
             e = Expression(ExprContext.ACCEPT_SUB_QUERY) <RPAREN>
@@ -219,13 +213,7 @@ void AttributeDef(List<SqlNode> list) :
     id = SimpleIdentifier()
     (
         type = DataType()
-        (
-            <NULL> { nullable = true; }
-        |
-            <NOT> <NULL> { nullable = false; }
-        |
-            { nullable = true; }
-        )
+        nullable = NullableOptDefaultTrue()
     )
     [ <DEFAULT_> e = Expression(ExprContext.ACCEPT_SUB_QUERY) ]
     {
diff --git a/site/_docs/reference.md b/site/_docs/reference.md
index d6f8a31..6df7bd0 100644
--- a/site/_docs/reference.md
+++ b/site/_docs/reference.md
@@ -1064,14 +1064,19 @@ Note:
 
 ### Non-scalar types
 
-| Type     | Description
-|:-------- |:-----------------------------------------------------------
-| ANY      | A value of an unknown type
-| ROW      | Row with 1 or more columns
-| MAP      | Collection of keys mapped to values
-| MULTISET | Unordered collection that may contain duplicates
-| ARRAY    | Ordered, contiguous collection that may contain duplicates
-| CURSOR   | Cursor over the result of executing a query
+| Type     | Description                | Example literals
+|:-------- |:---------------------------|:---------------
+| ANY      | A value of an unknown type |
+| ROW      | Row with 1 or more columns | Example: Row(f0 int null, f1 varchar)
+| MAP      | Collection of keys mapped to values |
+| MULTISET | Unordered collection that may contain duplicates | Example: int 
multiset
+| ARRAY    | Ordered, contiguous collection that may contain duplicates | 
Example: varchar(10) array
+| CURSOR   | Cursor over the result of executing a query |
+
+Note:
+
+* Every `ROW` column type can have an optional [ NULL | NOT NULL ] suffix
+  to indicate if this column type is nullable, default is not nullable.
 
 ### Spatial types
 
@@ -1311,6 +1316,63 @@ Not implemented:
 |:--------------- | :----------
 | CAST(value AS type) | Converts a value to a given type.
 
+Supported data types:
+
+{% highlight sql %}
+type:
+      typeName [ '(' precision [, scale] ')' ]
+      [ CHARACTER SET charSetName ]
+      [ collectionsTypeName ]
+
+typeName:
+      sqlTypeName
+  |   collectionsTypeName
+  |   rowTypeName
+  |   compoundIdentifier
+
+sqlTypeName:
+      char
+  |   varchar
+  |   DATE
+  |   TIME
+  |   TIMESTAMP
+  |   GEOMETRY
+  |   decimal
+  |   BOOLEAN
+  |   integer
+  |   BINARY
+  |   varbinary
+  |   TINYINT
+  |   SMALLINT
+  |   BIGINT
+  |   REAL
+  |   double
+  |   FLOAT
+  |   ANY
+
+collectionsTypeName:
+      ARRAY | MULTISET
+
+rowTypeName:
+      ROW '('
+        fieldName1 fieldType1 [ [ NULL | NOT NULL ] ]
+        [ , fieldName2 fieldType2 [ [ NULL | NOT NULL ] ] ]*
+        ')'
+
+char:
+      CHARACTER | CHAR
+varchar:
+      char VARYING | VARCHAR
+decimal:
+      DECIMAL | DEC | NUMERIC
+integer:
+      INTEGER | INT
+varbinary:
+      BINARY VARYING | VARBINARY
+double:
+      DOUBLE [PRECISION]
+{% endhighlight %}
+
 ### Value constructors
 
 | Operator syntax | Description

Reply via email to