You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@flink.apache.org by sx...@apache.org on 2025/06/20 23:41:41 UTC

(flink) 01/03: [FLINK-37923][sql] Patch calcite to support variant type

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

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

commit 1b062ddc7d720089b691ee8fb4d93809ff4492cb
Author: sxnan <su...@gmail.com>
AuthorDate: Wed Jun 4 14:29:37 2025 +0800

    [FLINK-37923][sql] Patch calcite to support variant type
---
 .../src/main/codegen/templates/Parser.jj           |    3 +
 .../org/apache/calcite/sql/type/SqlTypeFamily.java |  280 ++++++
 .../org/apache/calcite/sql/type/SqlTypeName.java   | 1055 ++++++++++++++++++++
 3 files changed, 1338 insertions(+)

diff --git a/flink-table/flink-sql-parser/src/main/codegen/templates/Parser.jj b/flink-table/flink-sql-parser/src/main/codegen/templates/Parser.jj
index 206b1ecf483..1a2adac6cbc 100644
--- a/flink-table/flink-sql-parser/src/main/codegen/templates/Parser.jj
+++ b/flink-table/flink-sql-parser/src/main/codegen/templates/Parser.jj
@@ -5680,6 +5680,8 @@ SqlTypeNameSpec SqlTypeName1(Span s) :
         [ <PRECISION> ] { sqlTypeName = SqlTypeName.DOUBLE; }
     |
         <FLOAT> { s.add(this); sqlTypeName = SqlTypeName.FLOAT; }
+    |
+        <VARIANT> { s.add(this); sqlTypeName = SqlTypeName.VARIANT; }
     )
     {
         return new SqlBasicTypeNameSpec(sqlTypeName, s.end(this));
@@ -8369,6 +8371,7 @@ SqlPostfixOperator PostfixRowOperator() :
 |   < VAR_SAMP: "VAR_SAMP" >
 |   < VARBINARY: "VARBINARY" >
 |   < VARCHAR: "VARCHAR" >
+|   < VARIANT: "VARIANT" >
 |   < VARYING: "VARYING" >
 |   < VERSION: "VERSION" >
 |   < VERSIONING: "VERSIONING" >
diff --git a/flink-table/flink-sql-parser/src/main/java/org/apache/calcite/sql/type/SqlTypeFamily.java b/flink-table/flink-sql-parser/src/main/java/org/apache/calcite/sql/type/SqlTypeFamily.java
new file mode 100644
index 00000000000..94cca219d92
--- /dev/null
+++ b/flink-table/flink-sql-parser/src/main/java/org/apache/calcite/sql/type/SqlTypeFamily.java
@@ -0,0 +1,280 @@
+/*
+ * 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.type;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import org.apache.calcite.avatica.util.TimeUnit;
+import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.rel.type.RelDataTypeFactory;
+import org.apache.calcite.rel.type.RelDataTypeFamily;
+import org.apache.calcite.sql.SqlIntervalQualifier;
+import org.apache.calcite.sql.SqlWindow;
+import org.apache.calcite.sql.parser.SqlParserPos;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+import java.sql.Types;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * SqlTypeFamily provides SQL type categorization.
+ *
+ * <p>The <em>primary</em> family categorization is a complete disjoint partitioning of SQL types
+ * into families, where two types are members of the same primary family iff instances of the two
+ * types can be the operands of an SQL equality predicate such as <code>WHERE v1 = v2</code>.
+ * Primary families are returned by RelDataType.getFamily().
+ *
+ * <p>There is also a <em>secondary</em> family categorization which overlaps with the primary
+ * categorization. It is used in type strategies for more specific or more general categorization
+ * than the primary families. Secondary families are never returned by RelDataType.getFamily().
+ *
+ * <p>This class was copied over from Calcite to support variant type(CALCITE-4918). When upgrading
+ * to Calcite 1.39.0 version, please remove the entire class.
+ */
+public enum SqlTypeFamily implements RelDataTypeFamily {
+    // Primary families.
+    CHARACTER,
+    BINARY,
+    NUMERIC,
+    DATE,
+    TIME,
+    TIMESTAMP,
+    BOOLEAN,
+    INTERVAL_YEAR_MONTH,
+    INTERVAL_DAY_TIME,
+
+    // Secondary families.
+
+    STRING,
+    APPROXIMATE_NUMERIC,
+    EXACT_NUMERIC,
+    DECIMAL,
+    INTEGER,
+    DATETIME,
+    DATETIME_INTERVAL,
+    MULTISET,
+    ARRAY,
+    MAP,
+    NULL,
+    ANY,
+    CURSOR,
+    COLUMN_LIST,
+    GEO,
+    VARIANT,
+    /** Like ANY, but do not even validate the operand. It may not be an expression. */
+    IGNORE;
+
+    private static final Map<Integer, SqlTypeFamily> JDBC_TYPE_TO_FAMILY =
+            ImmutableMap.<Integer, SqlTypeFamily>builder()
+                    // Not present:
+                    // SqlTypeName.MULTISET shares Types.ARRAY with SqlTypeName.ARRAY;
+                    // SqlTypeName.MAP has no corresponding JDBC type
+                    // SqlTypeName.COLUMN_LIST has no corresponding JDBC type
+                    .put(Types.BIT, NUMERIC)
+                    .put(Types.TINYINT, NUMERIC)
+                    .put(Types.SMALLINT, NUMERIC)
+                    .put(Types.BIGINT, NUMERIC)
+                    .put(Types.INTEGER, NUMERIC)
+                    .put(Types.NUMERIC, NUMERIC)
+                    .put(Types.DECIMAL, NUMERIC)
+                    .put(Types.FLOAT, NUMERIC)
+                    .put(Types.REAL, NUMERIC)
+                    .put(Types.DOUBLE, NUMERIC)
+                    .put(Types.CHAR, CHARACTER)
+                    .put(Types.VARCHAR, CHARACTER)
+                    .put(Types.LONGVARCHAR, CHARACTER)
+                    .put(Types.CLOB, CHARACTER)
+                    .put(Types.BINARY, BINARY)
+                    .put(Types.VARBINARY, BINARY)
+                    .put(Types.LONGVARBINARY, BINARY)
+                    .put(Types.BLOB, BINARY)
+                    .put(Types.DATE, DATE)
+                    .put(Types.TIME, TIME)
+                    .put(ExtraSqlTypes.TIME_WITH_TIMEZONE, TIME)
+                    .put(Types.TIMESTAMP, TIMESTAMP)
+                    .put(ExtraSqlTypes.TIMESTAMP_WITH_TIMEZONE, TIMESTAMP)
+                    .put(Types.BOOLEAN, BOOLEAN)
+                    .put(ExtraSqlTypes.REF_CURSOR, CURSOR)
+                    .put(Types.ARRAY, ARRAY)
+                    .put(Types.JAVA_OBJECT, VARIANT)
+                    .build();
+
+    /**
+     * Gets the primary family containing a JDBC type.
+     *
+     * @param jdbcType the JDBC type of interest
+     * @return containing family
+     */
+    public static @Nullable SqlTypeFamily getFamilyForJdbcType(int jdbcType) {
+        return JDBC_TYPE_TO_FAMILY.get(jdbcType);
+    }
+
+    /**
+     * For this type family, returns the allow types of the difference between two values of this
+     * family.
+     *
+     * <p>Equivalently, given an {@code ORDER BY} expression with one key, returns the allowable
+     * type families of the difference between two keys.
+     *
+     * <p>Example 1. For {@code ORDER BY empno}, a NUMERIC, the difference between two {@code empno}
+     * values is also NUMERIC.
+     *
+     * <p>Example 2. For {@code ORDER BY hireDate}, a DATE, the difference between two {@code
+     * hireDate} values might be an INTERVAL_DAY_TIME or INTERVAL_YEAR_MONTH.
+     *
+     * <p>The result determines whether a {@link SqlWindow} with a {@code RANGE} is valid (for
+     * example, {@code OVER (ORDER BY empno RANGE 10} is valid because {@code 10} is numeric); and
+     * whether a call to {@link org.apache.calcite.sql.fun.SqlStdOperatorTable#PERCENTILE_CONT
+     * PERCENTILE_CONT} is valid (for example, {@code PERCENTILE_CONT(0.25)} ORDER BY (hireDate)} is
+     * valid because {@code hireDate} values may be interpolated by adding values of type {@code
+     * INTERVAL_DAY_TIME}.
+     */
+    public List<SqlTypeFamily> allowableDifferenceTypes() {
+        switch (this) {
+            case NUMERIC:
+                return ImmutableList.of(NUMERIC);
+            case DATE:
+            case TIME:
+            case TIMESTAMP:
+                return ImmutableList.of(INTERVAL_DAY_TIME, INTERVAL_YEAR_MONTH);
+            default:
+                return ImmutableList.of();
+        }
+    }
+
+    /** Returns the collection of {@link SqlTypeName}s included in this family. */
+    public Collection<SqlTypeName> getTypeNames() {
+        switch (this) {
+            case CHARACTER:
+                return SqlTypeName.CHAR_TYPES;
+            case BINARY:
+                return SqlTypeName.BINARY_TYPES;
+            case NUMERIC:
+                return SqlTypeName.NUMERIC_TYPES;
+            case DECIMAL:
+                return ImmutableList.of(SqlTypeName.DECIMAL);
+            case DATE:
+                return ImmutableList.of(SqlTypeName.DATE);
+            case TIME:
+                return ImmutableList.of(SqlTypeName.TIME, SqlTypeName.TIME_WITH_LOCAL_TIME_ZONE);
+            case TIMESTAMP:
+                return ImmutableList.of(
+                        SqlTypeName.TIMESTAMP, SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE);
+            case BOOLEAN:
+                return SqlTypeName.BOOLEAN_TYPES;
+            case INTERVAL_YEAR_MONTH:
+                return SqlTypeName.YEAR_INTERVAL_TYPES;
+            case INTERVAL_DAY_TIME:
+                return SqlTypeName.DAY_INTERVAL_TYPES;
+            case STRING:
+                return SqlTypeName.STRING_TYPES;
+            case APPROXIMATE_NUMERIC:
+                return SqlTypeName.APPROX_TYPES;
+            case EXACT_NUMERIC:
+                return SqlTypeName.EXACT_TYPES;
+            case INTEGER:
+                return SqlTypeName.INT_TYPES;
+            case DATETIME:
+                return SqlTypeName.DATETIME_TYPES;
+            case DATETIME_INTERVAL:
+                return SqlTypeName.INTERVAL_TYPES;
+            case GEO:
+                return SqlTypeName.GEOMETRY_TYPES;
+            case MULTISET:
+                return ImmutableList.of(SqlTypeName.MULTISET);
+            case ARRAY:
+                return ImmutableList.of(SqlTypeName.ARRAY);
+            case MAP:
+                return ImmutableList.of(SqlTypeName.MAP);
+            case NULL:
+                return ImmutableList.of(SqlTypeName.NULL);
+            case ANY:
+                return SqlTypeName.ALL_TYPES;
+            case CURSOR:
+                return ImmutableList.of(SqlTypeName.CURSOR);
+            case COLUMN_LIST:
+                return ImmutableList.of(SqlTypeName.COLUMN_LIST);
+            case VARIANT:
+                return ImmutableList.of(SqlTypeName.VARIANT);
+            default:
+                throw new IllegalArgumentException();
+        }
+    }
+
+    /** Return the default {@link RelDataType} that belongs to this family. */
+    public @Nullable RelDataType getDefaultConcreteType(RelDataTypeFactory factory) {
+        switch (this) {
+            case CHARACTER:
+                return factory.createSqlType(SqlTypeName.VARCHAR);
+            case BINARY:
+                return factory.createSqlType(SqlTypeName.VARBINARY);
+            case NUMERIC:
+                return SqlTypeUtil.getMaxPrecisionScaleDecimal(factory);
+            case DATE:
+                return factory.createSqlType(SqlTypeName.DATE);
+            case TIME:
+                return factory.createSqlType(SqlTypeName.TIME);
+            case TIMESTAMP:
+                return factory.createSqlType(SqlTypeName.TIMESTAMP);
+            case BOOLEAN:
+                return factory.createSqlType(SqlTypeName.BOOLEAN);
+            case STRING:
+                return factory.createSqlType(SqlTypeName.VARCHAR);
+            case APPROXIMATE_NUMERIC:
+                return factory.createSqlType(SqlTypeName.DOUBLE);
+            case EXACT_NUMERIC:
+                return SqlTypeUtil.getMaxPrecisionScaleDecimal(factory);
+            case INTEGER:
+                return factory.createSqlType(SqlTypeName.BIGINT);
+            case DECIMAL:
+                return factory.createSqlType(SqlTypeName.DECIMAL);
+            case DATETIME:
+                return factory.createSqlType(SqlTypeName.TIMESTAMP);
+            case INTERVAL_DAY_TIME:
+                return factory.createSqlIntervalType(
+                        new SqlIntervalQualifier(TimeUnit.DAY, TimeUnit.SECOND, SqlParserPos.ZERO));
+            case INTERVAL_YEAR_MONTH:
+                return factory.createSqlIntervalType(
+                        new SqlIntervalQualifier(TimeUnit.YEAR, TimeUnit.MONTH, SqlParserPos.ZERO));
+            case GEO:
+                return factory.createSqlType(SqlTypeName.GEOMETRY);
+            case MULTISET:
+                return factory.createMultisetType(factory.createSqlType(SqlTypeName.ANY), -1);
+            case ARRAY:
+                return factory.createArrayType(factory.createSqlType(SqlTypeName.ANY), -1);
+            case MAP:
+                return factory.createMapType(
+                        factory.createSqlType(SqlTypeName.ANY),
+                        factory.createSqlType(SqlTypeName.ANY));
+            case NULL:
+                return factory.createSqlType(SqlTypeName.NULL);
+            case CURSOR:
+                return factory.createSqlType(SqlTypeName.CURSOR);
+            case COLUMN_LIST:
+                return factory.createSqlType(SqlTypeName.COLUMN_LIST);
+            default:
+                return null;
+        }
+    }
+
+    public boolean contains(RelDataType type) {
+        return SqlTypeUtil.isOfSameTypeName(getTypeNames(), type);
+    }
+}
diff --git a/flink-table/flink-sql-parser/src/main/java/org/apache/calcite/sql/type/SqlTypeName.java b/flink-table/flink-sql-parser/src/main/java/org/apache/calcite/sql/type/SqlTypeName.java
new file mode 100644
index 00000000000..618df6b5bee
--- /dev/null
+++ b/flink-table/flink-sql-parser/src/main/java/org/apache/calcite/sql/type/SqlTypeName.java
@@ -0,0 +1,1055 @@
+/*
+ * 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.type;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Sets;
+import org.apache.calcite.avatica.util.TimeUnit;
+import org.apache.calcite.sql.SqlLiteral;
+import org.apache.calcite.sql.parser.SqlParserPos;
+import org.apache.calcite.util.DateString;
+import org.apache.calcite.util.TimeString;
+import org.apache.calcite.util.TimestampString;
+import org.apache.calcite.util.Util;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+import java.math.BigDecimal;
+import java.sql.Types;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Enumeration of the type names which can be used to construct a SQL type. Rationale for this
+ * class's existence (instead of just using the standard java.sql.Type ordinals):
+ *
+ * <ul>
+ *   <li>{@link Types} does not include all SQL2003 data-types;
+ *   <li>SqlTypeName provides a type-safe enumeration;
+ *   <li>SqlTypeName provides a place to hang extra information such as whether the type carries
+ *       precision and scale.
+ * </ul>
+ *
+ * <p>This class was copied over from Calcite to support variant type(CALCITE-4918). When upgrading
+ * to Calcite 1.39.0 version, please remove the entire class.
+ */
+public enum SqlTypeName {
+    BOOLEAN(PrecScale.NO_NO, false, Types.BOOLEAN, SqlTypeFamily.BOOLEAN),
+    TINYINT(PrecScale.NO_NO, false, Types.TINYINT, SqlTypeFamily.NUMERIC),
+    SMALLINT(PrecScale.NO_NO, false, Types.SMALLINT, SqlTypeFamily.NUMERIC),
+    INTEGER(PrecScale.NO_NO, false, Types.INTEGER, SqlTypeFamily.NUMERIC),
+    BIGINT(PrecScale.NO_NO, false, Types.BIGINT, SqlTypeFamily.NUMERIC),
+    DECIMAL(
+            PrecScale.NO_NO | PrecScale.YES_NO | PrecScale.YES_YES,
+            false,
+            Types.DECIMAL,
+            SqlTypeFamily.NUMERIC),
+    FLOAT(PrecScale.NO_NO, false, Types.FLOAT, SqlTypeFamily.NUMERIC),
+    REAL(PrecScale.NO_NO, false, Types.REAL, SqlTypeFamily.NUMERIC),
+    DOUBLE(PrecScale.NO_NO, false, Types.DOUBLE, SqlTypeFamily.NUMERIC),
+    DATE(PrecScale.NO_NO, false, Types.DATE, SqlTypeFamily.DATE),
+    TIME(PrecScale.NO_NO | PrecScale.YES_NO, false, Types.TIME, SqlTypeFamily.TIME),
+    TIME_WITH_LOCAL_TIME_ZONE(
+            PrecScale.NO_NO | PrecScale.YES_NO, false, Types.OTHER, SqlTypeFamily.TIME),
+    TIMESTAMP(PrecScale.NO_NO | PrecScale.YES_NO, false, Types.TIMESTAMP, SqlTypeFamily.TIMESTAMP),
+    TIMESTAMP_WITH_LOCAL_TIME_ZONE(
+            PrecScale.NO_NO | PrecScale.YES_NO, false, Types.TIMESTAMP, SqlTypeFamily.TIMESTAMP),
+    INTERVAL_YEAR(PrecScale.NO_NO, false, Types.OTHER, SqlTypeFamily.INTERVAL_YEAR_MONTH),
+    INTERVAL_YEAR_MONTH(PrecScale.NO_NO, false, Types.OTHER, SqlTypeFamily.INTERVAL_YEAR_MONTH),
+    INTERVAL_MONTH(PrecScale.NO_NO, false, Types.OTHER, SqlTypeFamily.INTERVAL_YEAR_MONTH),
+    INTERVAL_DAY(
+            PrecScale.NO_NO | PrecScale.YES_NO | PrecScale.YES_YES,
+            false,
+            Types.OTHER,
+            SqlTypeFamily.INTERVAL_DAY_TIME),
+    INTERVAL_DAY_HOUR(
+            PrecScale.NO_NO | PrecScale.YES_NO | PrecScale.YES_YES,
+            false,
+            Types.OTHER,
+            SqlTypeFamily.INTERVAL_DAY_TIME),
+    INTERVAL_DAY_MINUTE(
+            PrecScale.NO_NO | PrecScale.YES_NO | PrecScale.YES_YES,
+            false,
+            Types.OTHER,
+            SqlTypeFamily.INTERVAL_DAY_TIME),
+    INTERVAL_DAY_SECOND(
+            PrecScale.NO_NO | PrecScale.YES_NO | PrecScale.YES_YES,
+            false,
+            Types.OTHER,
+            SqlTypeFamily.INTERVAL_DAY_TIME),
+    INTERVAL_HOUR(
+            PrecScale.NO_NO | PrecScale.YES_NO | PrecScale.YES_YES,
+            false,
+            Types.OTHER,
+            SqlTypeFamily.INTERVAL_DAY_TIME),
+    INTERVAL_HOUR_MINUTE(
+            PrecScale.NO_NO | PrecScale.YES_NO | PrecScale.YES_YES,
+            false,
+            Types.OTHER,
+            SqlTypeFamily.INTERVAL_DAY_TIME),
+    INTERVAL_HOUR_SECOND(
+            PrecScale.NO_NO | PrecScale.YES_NO | PrecScale.YES_YES,
+            false,
+            Types.OTHER,
+            SqlTypeFamily.INTERVAL_DAY_TIME),
+    INTERVAL_MINUTE(
+            PrecScale.NO_NO | PrecScale.YES_NO | PrecScale.YES_YES,
+            false,
+            Types.OTHER,
+            SqlTypeFamily.INTERVAL_DAY_TIME),
+    INTERVAL_MINUTE_SECOND(
+            PrecScale.NO_NO | PrecScale.YES_NO | PrecScale.YES_YES,
+            false,
+            Types.OTHER,
+            SqlTypeFamily.INTERVAL_DAY_TIME),
+    INTERVAL_SECOND(
+            PrecScale.NO_NO | PrecScale.YES_NO | PrecScale.YES_YES,
+            false,
+            Types.OTHER,
+            SqlTypeFamily.INTERVAL_DAY_TIME),
+    CHAR(PrecScale.NO_NO | PrecScale.YES_NO, false, Types.CHAR, SqlTypeFamily.CHARACTER),
+    VARCHAR(PrecScale.NO_NO | PrecScale.YES_NO, false, Types.VARCHAR, SqlTypeFamily.CHARACTER),
+    BINARY(PrecScale.NO_NO | PrecScale.YES_NO, false, Types.BINARY, SqlTypeFamily.BINARY),
+    VARBINARY(PrecScale.NO_NO | PrecScale.YES_NO, false, Types.VARBINARY, SqlTypeFamily.BINARY),
+    NULL(PrecScale.NO_NO, true, Types.NULL, SqlTypeFamily.NULL),
+    UNKNOWN(PrecScale.NO_NO, true, Types.NULL, SqlTypeFamily.NULL),
+    ANY(
+            PrecScale.NO_NO | PrecScale.YES_NO | PrecScale.YES_YES,
+            true,
+            Types.JAVA_OBJECT,
+            SqlTypeFamily.ANY),
+    SYMBOL(PrecScale.NO_NO, true, Types.OTHER, null),
+    MULTISET(PrecScale.NO_NO, false, Types.ARRAY, SqlTypeFamily.MULTISET),
+    ARRAY(PrecScale.NO_NO, false, Types.ARRAY, SqlTypeFamily.ARRAY),
+    MAP(PrecScale.NO_NO, false, Types.OTHER, SqlTypeFamily.MAP),
+    DISTINCT(PrecScale.NO_NO, false, Types.DISTINCT, null),
+    STRUCTURED(PrecScale.NO_NO, false, Types.STRUCT, null),
+    ROW(PrecScale.NO_NO, false, Types.STRUCT, null),
+    OTHER(PrecScale.NO_NO, false, Types.OTHER, null),
+    CURSOR(PrecScale.NO_NO, false, ExtraSqlTypes.REF_CURSOR, SqlTypeFamily.CURSOR),
+    COLUMN_LIST(PrecScale.NO_NO, false, Types.OTHER + 2, SqlTypeFamily.COLUMN_LIST),
+    DYNAMIC_STAR(
+            PrecScale.NO_NO | PrecScale.YES_NO | PrecScale.YES_YES,
+            true,
+            Types.JAVA_OBJECT,
+            SqlTypeFamily.ANY),
+    /**
+     * Spatial type. Though not standard, it is common to several DBs, so we do not flag it
+     * 'special' (internal).
+     */
+    GEOMETRY(PrecScale.NO_NO, false, ExtraSqlTypes.GEOMETRY, SqlTypeFamily.GEO),
+    MEASURE(PrecScale.NO_NO, true, Types.OTHER, SqlTypeFamily.ANY),
+    SARG(PrecScale.NO_NO, true, Types.OTHER, SqlTypeFamily.ANY),
+    /**
+     * VARIANT data type, a dynamically-typed value that can have at runtime any of the other data
+     * types in this table.
+     */
+    VARIANT(PrecScale.NO_NO, false, Types.OTHER, SqlTypeFamily.VARIANT);
+
+    public static final int MAX_DATETIME_PRECISION = 3;
+
+    // Minimum and default interval precisions are  defined by SQL2003
+    // Maximum interval precisions are implementation dependent,
+    //  but must be at least the default value
+    public static final int DEFAULT_INTERVAL_START_PRECISION = 2;
+    public static final int DEFAULT_INTERVAL_FRACTIONAL_SECOND_PRECISION = 6;
+    public static final int MIN_INTERVAL_START_PRECISION = 1;
+    public static final int MIN_INTERVAL_FRACTIONAL_SECOND_PRECISION = 1;
+    public static final int MAX_INTERVAL_START_PRECISION = 10;
+    public static final int MAX_INTERVAL_FRACTIONAL_SECOND_PRECISION = 9;
+
+    // Cached map of enum values
+    private static final Map<String, SqlTypeName> VALUES_MAP =
+            Util.enumConstants(SqlTypeName.class);
+
+    // categorizations used by SqlTypeFamily definitions
+
+    // you probably want to use JDK 1.5 support for treating enumeration
+    // as collection instead; this is only here to support
+    // SqlTypeFamily.ANY
+    public static final List<SqlTypeName> ALL_TYPES =
+            ImmutableList.of(
+                    BOOLEAN,
+                    INTEGER,
+                    VARCHAR,
+                    DATE,
+                    TIME,
+                    TIMESTAMP,
+                    NULL,
+                    DECIMAL,
+                    ANY,
+                    CHAR,
+                    BINARY,
+                    VARBINARY,
+                    TINYINT,
+                    SMALLINT,
+                    BIGINT,
+                    REAL,
+                    DOUBLE,
+                    SYMBOL,
+                    INTERVAL_YEAR,
+                    INTERVAL_YEAR_MONTH,
+                    INTERVAL_MONTH,
+                    INTERVAL_DAY,
+                    INTERVAL_DAY_HOUR,
+                    INTERVAL_DAY_MINUTE,
+                    INTERVAL_DAY_SECOND,
+                    INTERVAL_HOUR,
+                    INTERVAL_HOUR_MINUTE,
+                    INTERVAL_HOUR_SECOND,
+                    INTERVAL_MINUTE,
+                    INTERVAL_MINUTE_SECOND,
+                    INTERVAL_SECOND,
+                    TIME_WITH_LOCAL_TIME_ZONE,
+                    TIMESTAMP_WITH_LOCAL_TIME_ZONE,
+                    FLOAT,
+                    MULTISET,
+                    DISTINCT,
+                    STRUCTURED,
+                    ROW,
+                    CURSOR,
+                    COLUMN_LIST,
+                    VARIANT);
+
+    public static final List<SqlTypeName> BOOLEAN_TYPES = ImmutableList.of(BOOLEAN);
+
+    public static final List<SqlTypeName> BINARY_TYPES = ImmutableList.of(BINARY, VARBINARY);
+
+    public static final List<SqlTypeName> INT_TYPES =
+            ImmutableList.of(TINYINT, SMALLINT, INTEGER, BIGINT);
+
+    public static final List<SqlTypeName> EXACT_TYPES =
+            combine(INT_TYPES, ImmutableList.of(DECIMAL));
+
+    public static final List<SqlTypeName> APPROX_TYPES = ImmutableList.of(FLOAT, REAL, DOUBLE);
+
+    public static final List<SqlTypeName> NUMERIC_TYPES = combine(EXACT_TYPES, APPROX_TYPES);
+
+    public static final List<SqlTypeName> FRACTIONAL_TYPES =
+            combine(APPROX_TYPES, ImmutableList.of(DECIMAL));
+
+    public static final List<SqlTypeName> CHAR_TYPES = ImmutableList.of(CHAR, VARCHAR);
+
+    public static final List<SqlTypeName> STRING_TYPES = combine(CHAR_TYPES, BINARY_TYPES);
+
+    public static final List<SqlTypeName> GEOMETRY_TYPES = ImmutableList.of(GEOMETRY);
+
+    public static final List<SqlTypeName> DATETIME_TYPES =
+            ImmutableList.of(
+                    DATE,
+                    TIME,
+                    TIME_WITH_LOCAL_TIME_ZONE,
+                    TIMESTAMP,
+                    TIMESTAMP_WITH_LOCAL_TIME_ZONE);
+
+    public static final Set<SqlTypeName> YEAR_INTERVAL_TYPES =
+            Sets.immutableEnumSet(
+                    SqlTypeName.INTERVAL_YEAR,
+                    SqlTypeName.INTERVAL_YEAR_MONTH,
+                    SqlTypeName.INTERVAL_MONTH);
+
+    public static final Set<SqlTypeName> DAY_INTERVAL_TYPES =
+            Sets.immutableEnumSet(
+                    SqlTypeName.INTERVAL_DAY,
+                    SqlTypeName.INTERVAL_DAY_HOUR,
+                    SqlTypeName.INTERVAL_DAY_MINUTE,
+                    SqlTypeName.INTERVAL_DAY_SECOND,
+                    SqlTypeName.INTERVAL_HOUR,
+                    SqlTypeName.INTERVAL_HOUR_MINUTE,
+                    SqlTypeName.INTERVAL_HOUR_SECOND,
+                    SqlTypeName.INTERVAL_MINUTE,
+                    SqlTypeName.INTERVAL_MINUTE_SECOND,
+                    SqlTypeName.INTERVAL_SECOND);
+
+    public static final Set<SqlTypeName> INTERVAL_TYPES =
+            Sets.immutableEnumSet(Iterables.concat(YEAR_INTERVAL_TYPES, DAY_INTERVAL_TYPES));
+
+    /** The possible types of a time frame argument to a function such as {@code TIMESTAMP_DIFF}. */
+    public static final Set<SqlTypeName> TIME_FRAME_TYPES =
+            Sets.immutableEnumSet(Iterables.concat(INTERVAL_TYPES, ImmutableList.of(SYMBOL)));
+
+    private static final Map<Integer, SqlTypeName> JDBC_TYPE_TO_NAME =
+            ImmutableMap.<Integer, SqlTypeName>builder()
+                    .put(Types.TINYINT, TINYINT)
+                    .put(Types.SMALLINT, SMALLINT)
+                    .put(Types.BIGINT, BIGINT)
+                    .put(Types.INTEGER, INTEGER)
+                    .put(Types.NUMERIC, DECIMAL) // REVIEW
+                    .put(Types.DECIMAL, DECIMAL)
+                    .put(Types.FLOAT, FLOAT)
+                    .put(Types.REAL, REAL)
+                    .put(Types.DOUBLE, DOUBLE)
+                    .put(Types.CHAR, CHAR)
+                    .put(Types.VARCHAR, VARCHAR)
+
+                    // TODO: provide real support for these eventually
+                    .put(ExtraSqlTypes.NCHAR, CHAR)
+                    .put(ExtraSqlTypes.NVARCHAR, VARCHAR)
+
+                    // TODO: additional types not yet supported. See ExtraSqlTypes.
+                    // .put(Types.LONGVARCHAR, Longvarchar)
+                    // .put(Types.CLOB, Clob)
+                    // .put(Types.LONGVARBINARY, Longvarbinary)
+                    // .put(Types.BLOB, Blob)
+                    // .put(Types.LONGNVARCHAR, Longnvarchar)
+                    // .put(Types.NCLOB, Nclob)
+                    // .put(Types.ROWID, Rowid)
+                    // .put(Types.SQLXML, Sqlxml)
+
+                    .put(Types.BINARY, BINARY)
+                    .put(Types.VARBINARY, VARBINARY)
+                    .put(Types.DATE, DATE)
+                    .put(Types.TIME, TIME)
+                    .put(Types.TIMESTAMP, TIMESTAMP)
+                    .put(Types.BIT, BOOLEAN)
+                    .put(Types.BOOLEAN, BOOLEAN)
+                    .put(Types.DISTINCT, DISTINCT)
+                    .put(Types.STRUCT, STRUCTURED)
+                    .put(Types.ARRAY, ARRAY)
+                    .build();
+
+    /** Bitwise-or of flags indicating allowable precision/scale combinations. */
+    private final int signatures;
+
+    /**
+     * Returns true if not of a "pure" standard sql type. "Inpure" types are {@link #ANY}, {@link
+     * #NULL} and {@link #SYMBOL}
+     */
+    private final boolean special;
+
+    private final int jdbcOrdinal;
+    private final @Nullable SqlTypeFamily family;
+
+    SqlTypeName(int signatures, boolean special, int jdbcType, @Nullable SqlTypeFamily family) {
+        this.signatures = signatures;
+        this.special = special;
+        this.jdbcOrdinal = jdbcType;
+        this.family = family;
+    }
+
+    /**
+     * Looks up a type name from its name.
+     *
+     * @return Type name, or null if not found
+     */
+    public static @Nullable SqlTypeName get(String name) {
+        if (false) {
+            // The following code works OK, but the spurious exceptions are
+            // annoying.
+            try {
+                return SqlTypeName.valueOf(name);
+            } catch (IllegalArgumentException e) {
+                return null;
+            }
+        }
+        return VALUES_MAP.get(name);
+    }
+
+    /**
+     * Returns the SqlTypeName value whose name or {@link #getSpaceName()} matches the given name,
+     * or throws {@link IllegalArgumentException}; never returns null.
+     */
+    public static SqlTypeName lookup(String tag) {
+        String tag2 = tag.replace(' ', '_');
+        return valueOf(tag2);
+    }
+
+    public boolean allowsNoPrecNoScale() {
+        return (signatures & PrecScale.NO_NO) != 0;
+    }
+
+    public boolean allowsPrecNoScale() {
+        return (signatures & PrecScale.YES_NO) != 0;
+    }
+
+    public boolean allowsPrec() {
+        return allowsPrecScale(true, true) || allowsPrecScale(true, false);
+    }
+
+    public boolean allowsScale() {
+        return allowsPrecScale(true, true);
+    }
+
+    /**
+     * Returns whether this type can be specified with a given combination of precision and scale.
+     * For example,
+     *
+     * <ul>
+     *   <li><code>Varchar.allowsPrecScale(true, false)</code> returns <code>
+     * true</code>, because the VARCHAR type allows a precision parameter, as in <code>VARCHAR(10)
+     *       </code>.
+     *   <li><code>Varchar.allowsPrecScale(true, true)</code> returns <code>
+     * true</code>, because the VARCHAR type does not allow a precision and a scale parameter, as in
+     *       <code>VARCHAR(10, 4)</code>.
+     *   <li><code>allowsPrecScale(false, true)</code> returns <code>false</code> for every type.
+     * </ul>
+     *
+     * @param precision Whether the precision/length field is part of the type specification
+     * @param scale Whether the scale field is part of the type specification
+     * @return Whether this combination of precision/scale is valid
+     */
+    public boolean allowsPrecScale(boolean precision, boolean scale) {
+        int mask =
+                precision
+                        ? (scale ? PrecScale.YES_YES : PrecScale.YES_NO)
+                        : (scale ? 0 : PrecScale.NO_NO);
+        return (signatures & mask) != 0;
+    }
+
+    public boolean isSpecial() {
+        return special;
+    }
+
+    /** Returns the ordinal from {@link Types} corresponding to this SqlTypeName. */
+    public int getJdbcOrdinal() {
+        return jdbcOrdinal;
+    }
+
+    private static List<SqlTypeName> combine(List<SqlTypeName> list0, List<SqlTypeName> list1) {
+        return ImmutableList.<SqlTypeName>builder().addAll(list0).addAll(list1).build();
+    }
+
+    /**
+     * Returns the default scale for this type if supported, otherwise -1 if scale is either
+     * unsupported or must be specified explicitly.
+     */
+    public int getDefaultScale() {
+        switch (this) {
+            case DECIMAL:
+                return 0;
+            case INTERVAL_YEAR:
+            case INTERVAL_YEAR_MONTH:
+            case INTERVAL_MONTH:
+            case INTERVAL_DAY:
+            case INTERVAL_DAY_HOUR:
+            case INTERVAL_DAY_MINUTE:
+            case INTERVAL_DAY_SECOND:
+            case INTERVAL_HOUR:
+            case INTERVAL_HOUR_MINUTE:
+            case INTERVAL_HOUR_SECOND:
+            case INTERVAL_MINUTE:
+            case INTERVAL_MINUTE_SECOND:
+            case INTERVAL_SECOND:
+                return DEFAULT_INTERVAL_FRACTIONAL_SECOND_PRECISION;
+            default:
+                return -1;
+        }
+    }
+
+    /**
+     * Gets the SqlTypeFamily containing this SqlTypeName.
+     *
+     * @return containing family, or null for none (SYMBOL, DISTINCT, STRUCTURED, ROW, OTHER)
+     */
+    public @Nullable SqlTypeFamily getFamily() {
+        return family;
+    }
+
+    /**
+     * Gets the SqlTypeName corresponding to a JDBC type.
+     *
+     * @param jdbcType the JDBC type of interest
+     * @return corresponding SqlTypeName, or null if the type is not known
+     */
+    public static @Nullable SqlTypeName getNameForJdbcType(int jdbcType) {
+        return JDBC_TYPE_TO_NAME.get(jdbcType);
+    }
+
+    /**
+     * Returns the limit of this datatype. For example,
+     *
+     * <table border="1">
+     * <caption>Datatype limits</caption>
+     * <tr>
+     * <th>Datatype</th>
+     * <th>sign</th>
+     * <th>limit</th>
+     * <th>beyond</th>
+     * <th>precision</th>
+     * <th>scale</th>
+     * <th>Returns</th>
+     * </tr>
+     * <tr>
+     * <td>Integer</td>
+     * <td>true</td>
+     * <td>true</td>
+     * <td>false</td>
+     * <td>-1</td>
+     * <td>-1</td>
+     * <td>2147483647 (2 ^ 31 -1 = MAXINT)</td>
+     * </tr>
+     * <tr>
+     * <td>Integer</td>
+     * <td>true</td>
+     * <td>true</td>
+     * <td>true</td>
+     * <td>-1</td>
+     * <td>-1</td>
+     * <td>2147483648 (2 ^ 31 = MAXINT + 1)</td>
+     * </tr>
+     * <tr>
+     * <td>Integer</td>
+     * <td>false</td>
+     * <td>true</td>
+     * <td>false</td>
+     * <td>-1</td>
+     * <td>-1</td>
+     * <td>-2147483648 (-2 ^ 31 = MININT)</td>
+     * </tr>
+     * <tr>
+     * <td>Boolean</td>
+     * <td>true</td>
+     * <td>true</td>
+     * <td>false</td>
+     * <td>-1</td>
+     * <td>-1</td>
+     * <td>TRUE</td>
+     * </tr>
+     * <tr>
+     * <td>Varchar</td>
+     * <td>true</td>
+     * <td>true</td>
+     * <td>false</td>
+     * <td>10</td>
+     * <td>-1</td>
+     * <td>'ZZZZZZZZZZ'</td>
+     * </tr>
+     * </table>
+     *
+     * @param sign If true, returns upper limit, otherwise lower limit
+     * @param limit If true, returns value at or near to overflow; otherwise value at or near to
+     *     underflow
+     * @param beyond If true, returns the value just beyond the limit, otherwise the value at the
+     *     limit
+     * @param precision Precision, or -1 if not applicable
+     * @param scale Scale, or -1 if not applicable
+     * @return Limit value
+     */
+    public @Nullable Object getLimit(
+            boolean sign, Limit limit, boolean beyond, int precision, int scale) {
+        assert allowsPrecScale(precision != -1, scale != -1) : this;
+        if (limit == Limit.ZERO) {
+            if (beyond) {
+                return null;
+            }
+            sign = true;
+        }
+        Calendar calendar;
+
+        switch (this) {
+            case BOOLEAN:
+                switch (limit) {
+                    case ZERO:
+                        return false;
+                    case UNDERFLOW:
+                        return null;
+                    case OVERFLOW:
+                        if (beyond || !sign) {
+                            return null;
+                        } else {
+                            return true;
+                        }
+                    default:
+                        throw Util.unexpected(limit);
+                }
+
+            case TINYINT:
+                return getNumericLimit(2, 8, sign, limit, beyond);
+
+            case SMALLINT:
+                return getNumericLimit(2, 16, sign, limit, beyond);
+
+            case INTEGER:
+                return getNumericLimit(2, 32, sign, limit, beyond);
+
+            case BIGINT:
+                return getNumericLimit(2, 64, sign, limit, beyond);
+
+            case DECIMAL:
+                BigDecimal decimal = getNumericLimit(10, precision, sign, limit, beyond);
+                if (decimal == null) {
+                    return null;
+                }
+
+                // Decimal values must fit into 64 bits. So, the maximum value of
+                // a DECIMAL(19, 0) is 2^63 - 1, not 10^19 - 1.
+                switch (limit) {
+                    case OVERFLOW:
+                        final BigDecimal other =
+                                (BigDecimal) BIGINT.getLimit(sign, limit, beyond, -1, -1);
+                        if (other != null && decimal.compareTo(other) == (sign ? 1 : -1)) {
+                            decimal = other;
+                        }
+                        break;
+                    default:
+                        break;
+                }
+
+                // Apply scale.
+                if (scale == 0) {
+                    // do nothing
+                } else if (scale > 0) {
+                    decimal = decimal.divide(BigDecimal.TEN.pow(scale));
+                } else {
+                    decimal = decimal.multiply(BigDecimal.TEN.pow(-scale));
+                }
+                return decimal;
+
+            case CHAR:
+            case VARCHAR:
+                if (!sign) {
+                    return null; // this type does not have negative values
+                }
+                StringBuilder buf = new StringBuilder();
+                switch (limit) {
+                    case ZERO:
+                        break;
+                    case UNDERFLOW:
+                        if (beyond) {
+                            // There is no value between the empty string and the
+                            // smallest non-empty string.
+                            return null;
+                        }
+                        buf.append("a");
+                        break;
+                    case OVERFLOW:
+                        for (int i = 0; i < precision; ++i) {
+                            buf.append("Z");
+                        }
+                        if (beyond) {
+                            buf.append("Z");
+                        }
+                        break;
+                    default:
+                        break;
+                }
+                return buf.toString();
+
+            case BINARY:
+            case VARBINARY:
+                if (!sign) {
+                    return null; // this type does not have negative values
+                }
+                byte[] bytes;
+                switch (limit) {
+                    case ZERO:
+                        bytes = new byte[0];
+                        break;
+                    case UNDERFLOW:
+                        if (beyond) {
+                            // There is no value between the empty string and the
+                            // smallest value.
+                            return null;
+                        }
+                        bytes = new byte[] {0x00};
+                        break;
+                    case OVERFLOW:
+                        bytes = new byte[precision + (beyond ? 1 : 0)];
+                        Arrays.fill(bytes, (byte) 0xff);
+                        break;
+                    default:
+                        throw Util.unexpected(limit);
+                }
+                return bytes;
+
+            case DATE:
+                calendar = Util.calendar();
+                switch (limit) {
+                    case ZERO:
+                        // The epoch.
+                        calendar.set(Calendar.YEAR, 1970);
+                        calendar.set(Calendar.MONTH, 0);
+                        calendar.set(Calendar.DAY_OF_MONTH, 1);
+                        break;
+                    case UNDERFLOW:
+                        return null;
+                    case OVERFLOW:
+                        if (beyond) {
+                            // It is impossible to represent an invalid year as a date
+                            // literal. SQL dates are represented as 'yyyy-mm-dd', and
+                            // 1 <= yyyy <= 9999 is valid. There is no year 0: the year
+                            // before 1AD is 1BC, so SimpleDateFormat renders the day
+                            // before 0001-01-01 (AD) as 0001-12-31 (BC), which looks
+                            // like a valid date.
+                            return null;
+                        }
+
+                        // "SQL:2003 6.1 <data type> Access Rules 6" says that year is
+                        // between 1 and 9999, and days/months are the valid Gregorian
+                        // calendar values for these years.
+                        if (sign) {
+                            calendar.set(Calendar.YEAR, 9999);
+                            calendar.set(Calendar.MONTH, 11);
+                            calendar.set(Calendar.DAY_OF_MONTH, 31);
+                        } else {
+                            calendar.set(Calendar.YEAR, 1);
+                            calendar.set(Calendar.MONTH, 0);
+                            calendar.set(Calendar.DAY_OF_MONTH, 1);
+                        }
+                        break;
+                    default:
+                        break;
+                }
+                calendar.set(Calendar.HOUR_OF_DAY, 0);
+                calendar.set(Calendar.MINUTE, 0);
+                calendar.set(Calendar.SECOND, 0);
+                return calendar;
+
+            case TIME:
+                if (!sign) {
+                    return null; // this type does not have negative values
+                }
+                if (beyond) {
+                    return null; // invalid values are impossible to represent
+                }
+                calendar = Util.calendar();
+                switch (limit) {
+                    case ZERO:
+
+                        // The epoch.
+                        calendar.set(Calendar.HOUR_OF_DAY, 0);
+                        calendar.set(Calendar.MINUTE, 0);
+                        calendar.set(Calendar.SECOND, 0);
+                        calendar.set(Calendar.MILLISECOND, 0);
+                        break;
+                    case UNDERFLOW:
+                        return null;
+                    case OVERFLOW:
+                        calendar.set(Calendar.HOUR_OF_DAY, 23);
+                        calendar.set(Calendar.MINUTE, 59);
+                        calendar.set(Calendar.SECOND, 59);
+                        int millis =
+                                (precision >= 3)
+                                        ? 999
+                                        : ((precision == 2) ? 990 : ((precision == 1) ? 900 : 0));
+                        calendar.set(Calendar.MILLISECOND, millis);
+                        break;
+                    default:
+                        break;
+                }
+                return calendar;
+
+            case TIMESTAMP:
+                calendar = Util.calendar();
+                switch (limit) {
+                    case ZERO:
+                        // The epoch.
+                        calendar.set(Calendar.YEAR, 1970);
+                        calendar.set(Calendar.MONTH, 0);
+                        calendar.set(Calendar.DAY_OF_MONTH, 1);
+                        calendar.set(Calendar.HOUR_OF_DAY, 0);
+                        calendar.set(Calendar.MINUTE, 0);
+                        calendar.set(Calendar.SECOND, 0);
+                        calendar.set(Calendar.MILLISECOND, 0);
+                        break;
+                    case UNDERFLOW:
+                        return null;
+                    case OVERFLOW:
+                        if (beyond) {
+                            // It is impossible to represent an invalid year as a date
+                            // literal. SQL dates are represented as 'yyyy-mm-dd', and
+                            // 1 <= yyyy <= 9999 is valid. There is no year 0: the year
+                            // before 1AD is 1BC, so SimpleDateFormat renders the day
+                            // before 0001-01-01 (AD) as 0001-12-31 (BC), which looks
+                            // like a valid date.
+                            return null;
+                        }
+
+                        // "SQL:2003 6.1 <data type> Access Rules 6" says that year is
+                        // between 1 and 9999, and days/months are the valid Gregorian
+                        // calendar values for these years.
+                        if (sign) {
+                            calendar.set(Calendar.YEAR, 9999);
+                            calendar.set(Calendar.MONTH, 11);
+                            calendar.set(Calendar.DAY_OF_MONTH, 31);
+                            calendar.set(Calendar.HOUR_OF_DAY, 23);
+                            calendar.set(Calendar.MINUTE, 59);
+                            calendar.set(Calendar.SECOND, 59);
+                            int millis =
+                                    (precision >= 3)
+                                            ? 999
+                                            : ((precision == 2)
+                                                    ? 990
+                                                    : ((precision == 1) ? 900 : 0));
+                            calendar.set(Calendar.MILLISECOND, millis);
+                        } else {
+                            calendar.set(Calendar.YEAR, 1);
+                            calendar.set(Calendar.MONTH, 0);
+                            calendar.set(Calendar.DAY_OF_MONTH, 1);
+                            calendar.set(Calendar.HOUR_OF_DAY, 0);
+                            calendar.set(Calendar.MINUTE, 0);
+                            calendar.set(Calendar.SECOND, 0);
+                            calendar.set(Calendar.MILLISECOND, 0);
+                        }
+                        break;
+                    default:
+                        break;
+                }
+                return calendar;
+
+            default:
+                throw Util.unexpected(this);
+        }
+    }
+
+    /**
+     * Returns the minimum precision (or length) allowed for this type, or -1 if precision/length
+     * are not applicable for this type.
+     *
+     * @return Minimum allowed precision
+     */
+    public int getMinPrecision() {
+        switch (this) {
+            case DECIMAL:
+            case VARCHAR:
+            case CHAR:
+            case VARBINARY:
+            case BINARY:
+            case TIME:
+            case TIME_WITH_LOCAL_TIME_ZONE:
+            case TIMESTAMP:
+            case TIMESTAMP_WITH_LOCAL_TIME_ZONE:
+                return 1;
+            case INTERVAL_YEAR:
+            case INTERVAL_YEAR_MONTH:
+            case INTERVAL_MONTH:
+            case INTERVAL_DAY:
+            case INTERVAL_DAY_HOUR:
+            case INTERVAL_DAY_MINUTE:
+            case INTERVAL_DAY_SECOND:
+            case INTERVAL_HOUR:
+            case INTERVAL_HOUR_MINUTE:
+            case INTERVAL_HOUR_SECOND:
+            case INTERVAL_MINUTE:
+            case INTERVAL_MINUTE_SECOND:
+            case INTERVAL_SECOND:
+                return MIN_INTERVAL_START_PRECISION;
+            default:
+                return -1;
+        }
+    }
+
+    /**
+     * Returns the minimum scale (or fractional second precision in the case of intervals) allowed
+     * for this type, or -1 if precision/length are not applicable for this type.
+     *
+     * @return Minimum allowed scale
+     */
+    public int getMinScale() {
+        switch (this) {
+                // TODO: Minimum numeric scale for decimal
+            case INTERVAL_YEAR:
+            case INTERVAL_YEAR_MONTH:
+            case INTERVAL_MONTH:
+            case INTERVAL_DAY:
+            case INTERVAL_DAY_HOUR:
+            case INTERVAL_DAY_MINUTE:
+            case INTERVAL_DAY_SECOND:
+            case INTERVAL_HOUR:
+            case INTERVAL_HOUR_MINUTE:
+            case INTERVAL_HOUR_SECOND:
+            case INTERVAL_MINUTE:
+            case INTERVAL_MINUTE_SECOND:
+            case INTERVAL_SECOND:
+                return MIN_INTERVAL_FRACTIONAL_SECOND_PRECISION;
+            default:
+                return -1;
+        }
+    }
+
+    /**
+     * Returns {@code HOUR} for {@code HOUR TO SECOND} and {@code HOUR}, {@code SECOND} for {@code
+     * SECOND}.
+     */
+    public TimeUnit getStartUnit() {
+        switch (this) {
+            case INTERVAL_YEAR:
+            case INTERVAL_YEAR_MONTH:
+                return TimeUnit.YEAR;
+            case INTERVAL_MONTH:
+                return TimeUnit.MONTH;
+            case INTERVAL_DAY:
+            case INTERVAL_DAY_HOUR:
+            case INTERVAL_DAY_MINUTE:
+            case INTERVAL_DAY_SECOND:
+                return TimeUnit.DAY;
+            case INTERVAL_HOUR:
+            case INTERVAL_HOUR_MINUTE:
+            case INTERVAL_HOUR_SECOND:
+                return TimeUnit.HOUR;
+            case INTERVAL_MINUTE:
+            case INTERVAL_MINUTE_SECOND:
+                return TimeUnit.MINUTE;
+            case INTERVAL_SECOND:
+                return TimeUnit.SECOND;
+            default:
+                throw new AssertionError(this);
+        }
+    }
+
+    /** Returns {@code SECOND} for both {@code HOUR TO SECOND} and {@code SECOND}. */
+    public TimeUnit getEndUnit() {
+        switch (this) {
+            case INTERVAL_YEAR:
+                return TimeUnit.YEAR;
+            case INTERVAL_YEAR_MONTH:
+            case INTERVAL_MONTH:
+                return TimeUnit.MONTH;
+            case INTERVAL_DAY:
+                return TimeUnit.DAY;
+            case INTERVAL_DAY_HOUR:
+            case INTERVAL_HOUR:
+                return TimeUnit.HOUR;
+            case INTERVAL_DAY_MINUTE:
+            case INTERVAL_HOUR_MINUTE:
+            case INTERVAL_MINUTE:
+                return TimeUnit.MINUTE;
+            case INTERVAL_DAY_SECOND:
+            case INTERVAL_HOUR_SECOND:
+            case INTERVAL_MINUTE_SECOND:
+            case INTERVAL_SECOND:
+                return TimeUnit.SECOND;
+            default:
+                throw new AssertionError(this);
+        }
+    }
+
+    public boolean isYearMonth() {
+        switch (this) {
+            case INTERVAL_YEAR:
+            case INTERVAL_YEAR_MONTH:
+            case INTERVAL_MONTH:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    /** Limit. */
+    public enum Limit {
+        ZERO,
+        UNDERFLOW,
+        OVERFLOW
+    }
+
+    private static @Nullable BigDecimal getNumericLimit(
+            int radix, int exponent, boolean sign, Limit limit, boolean beyond) {
+        switch (limit) {
+            case OVERFLOW:
+
+                // 2-based schemes run from -2^(N-1) to 2^(N-1)-1 e.g. -128 to +127
+                // 10-based schemas run from -(10^N-1) to 10^N-1 e.g. -99 to +99
+                final BigDecimal bigRadix = BigDecimal.valueOf(radix);
+                if (radix == 2) {
+                    --exponent;
+                }
+                BigDecimal decimal = bigRadix.pow(exponent);
+                if (sign || (radix != 2)) {
+                    decimal = decimal.subtract(BigDecimal.ONE);
+                }
+                if (beyond) {
+                    decimal = decimal.add(BigDecimal.ONE);
+                }
+                if (!sign) {
+                    decimal = decimal.negate();
+                }
+                return decimal;
+            case UNDERFLOW:
+                return beyond ? null : (sign ? BigDecimal.ONE : BigDecimal.ONE.negate());
+            case ZERO:
+                return BigDecimal.ZERO;
+            default:
+                throw Util.unexpected(limit);
+        }
+    }
+
+    public SqlLiteral createLiteral(Object o, SqlParserPos pos) {
+        switch (this) {
+            case BOOLEAN:
+                return SqlLiteral.createBoolean((Boolean) o, pos);
+            case TINYINT:
+            case SMALLINT:
+            case INTEGER:
+            case BIGINT:
+            case DECIMAL:
+                return SqlLiteral.createExactNumeric(o.toString(), pos);
+            case VARCHAR:
+            case CHAR:
+                return SqlLiteral.createCharString((String) o, pos);
+            case VARBINARY:
+            case BINARY:
+                return SqlLiteral.createBinaryString((byte[]) o, pos);
+            case DATE:
+                return SqlLiteral.createDate(
+                        o instanceof Calendar
+                                ? DateString.fromCalendarFields((Calendar) o)
+                                : (DateString) o,
+                        pos);
+            case TIME:
+                return SqlLiteral.createTime(
+                        o instanceof Calendar
+                                ? TimeString.fromCalendarFields((Calendar) o)
+                                : (TimeString) o,
+                        0 /* todo */,
+                        pos);
+            case TIMESTAMP:
+                return SqlLiteral.createTimestamp(
+                        this,
+                        o instanceof Calendar
+                                ? TimestampString.fromCalendarFields((Calendar) o)
+                                : (TimestampString) o,
+                        0 /* todo */,
+                        pos);
+            default:
+                throw Util.unexpected(this);
+        }
+    }
+
+    /** Returns the name of this type. */
+    public String getName() {
+        return name();
+    }
+
+    /**
+     * Returns the name of this type, with underscores converted to spaces, for example "TIMESTAMP
+     * WITH LOCAL TIME ZONE", "DATE".
+     */
+    public String getSpaceName() {
+        return name().replace('_', ' ');
+    }
+
+    /**
+     * Flags indicating precision/scale combinations.
+     *
+     * <p>Note: for intervals:
+     *
+     * <ul>
+     *   <li>precision = start (leading field) precision
+     *   <li>scale = fractional second precision
+     * </ul>
+     */
+    private interface PrecScale {
+        int NO_NO = 1;
+        int YES_NO = 2;
+        int YES_YES = 4;
+    }
+}