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 2023/01/25 07:39:17 UTC

[calcite] branch main updated (f8f8a51e2c -> a326bd2d0e)

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

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


 discard f8f8a51e2c [CALCITE-5424] Customize handling of literals based on type system
 discard d9eeba4422 [CALCITE-5450] Allow 'WEEK(weekday)' time frame as argument to functions such as EXTRACT, DATE_TRUNC
 discard 016dcd46ee [CALCITE-5495] Allow WEEK and QUARTER in INTERVAL literals
 discard ba35438c9d remove premature tests
 discard d5737b4525 Factor interval tests out of SqlParserTest and SqlValidatorTest and into new class IntervalTest
     new b623215e2f Factor interval tests out of SqlParserTest and SqlValidatorTest and into new class IntervalTest
     new b8ccac153a [CALCITE-5495] Allow WEEK and QUARTER in INTERVAL literals
     new 7fb8578059 [CALCITE-5450] Allow 'WEEK(weekday)' time frame as argument to functions such as EXTRACT, DATE_TRUNC
     new a326bd2d0e [CALCITE-5424] Customize handling of literals based on type system

This update added new revisions after undoing existing revisions.
That is to say, some revisions that were in the old version of the
branch are not in the new version.  This situation occurs
when a user --force pushes a change and generates a repository
containing something like this:

 * -- * -- B -- O -- O -- O   (f8f8a51e2c)
            \
             N -- N -- N   refs/heads/main (a326bd2d0e)

You should already have received notification emails for all of the O
revisions, and so the following emails describe only the N revisions
from the common base, B.

Any revisions marked "omit" are not gone; other references still
refer to them.  Any revisions marked "discard" are gone forever.

The 4 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:


[calcite] 03/04: [CALCITE-5450] Allow 'WEEK(weekday)' time frame as argument to functions such as EXTRACT, DATE_TRUNC

Posted by jh...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit 7fb8578059b964981d333072895e51fcab3f5893
Author: Oliver Lee <ol...@google.com>
AuthorDate: Wed Jan 11 17:42:24 2023 +0000

    [CALCITE-5450] Allow 'WEEK(weekday)' time frame as argument to functions such as EXTRACT, DATE_TRUNC
    
    In OperandTypes, add strategies for matching DATE, TIME,
    TIMESTAMP frames; DATE and TIMESTAMP frames include
    ISOYEAR, WEEK(THURSDAY).
    
    Enable tests for ISOYEAR, now that the upgrade to Avatica
    1.23 has brought in the fix to [CALCITE-5369], ISOYEAR in
    FLOOR and CEIL functions.
---
 babel/src/test/resources/sql/big-query.iq          | 30 +++++--
 core/src/main/codegen/templates/Parser.jj          | 95 ++++++++++++++--------
 .../calcite/adapter/enumerable/RexImpTable.java    |  1 +
 .../org/apache/calcite/rel/type/TimeFrames.java    | 24 +++++-
 .../calcite/sql/fun/SqlLibraryOperators.java       | 35 +-------
 .../sql/type/IntervalOperandTypeChecker.java       | 11 ++-
 .../org/apache/calcite/sql/type/OperandTypes.java  | 68 +++++++++++++++-
 .../org/apache/calcite/test/SqlValidatorTest.java  | 30 +++++++
 .../org/apache/calcite/test/TimeFrameTest.java     | 10 +++
 site/_docs/reference.md                            | 11 ++-
 .../apache/calcite/sql/parser/SqlParserTest.java   | 14 +++-
 .../org/apache/calcite/test/SqlOperatorTest.java   | 30 +++++++
 12 files changed, 270 insertions(+), 89 deletions(-)

diff --git a/babel/src/test/resources/sql/big-query.iq b/babel/src/test/resources/sql/big-query.iq
index 53a337bfd1..4b1c4bc501 100755
--- a/babel/src/test/resources/sql/big-query.iq
+++ b/babel/src/test/resources/sql/big-query.iq
@@ -1827,22 +1827,34 @@ SELECT TIMESTAMP_DIFF("2001-02-01 01:00:00", "2001-02-01 00:00:01", HOUR) AS neg
 # Returns DATE
 WITH Dates AS (
   SELECT DATE_TRUNC(DATE '2008-12-25', YEAR) as d , "year" as frame UNION ALL
+  SELECT DATE_TRUNC(DATE '2008-12-25', ISOYEAR), "isoyear" UNION ALL
   SELECT DATE_TRUNC(DATE '2008-12-25', QUARTER), "quarter" UNION ALL
   SELECT DATE_TRUNC(DATE '2008-12-25', MONTH), "month" UNION ALL
+  SELECT DATE_TRUNC(DATE '2008-12-25', WEEK), "week" UNION ALL
+  SELECT DATE_TRUNC(DATE '2008-12-25', WEEK(SUNDAY)), "week(sunday)" UNION ALL
+  SELECT DATE_TRUNC(DATE '2008-12-25', WEEK(MONDAY)), "week(monday)" UNION ALL
+  SELECT DATE_TRUNC(DATE '2008-12-25', WEEK(TUESDAY)), "week(tuesday)" UNION ALL
+  SELECT DATE_TRUNC(DATE '2008-12-25', ISOWEEK), "isoweek" UNION ALL
   SELECT DATE_TRUNC(DATE '2008-12-25', DAY), "day"
 )
 SELECT
   *
 FROM Dates;
-+------------+---------+
-| d          | frame   |
-+------------+---------+
-| 2008-01-01 | year    |
-| 2008-10-01 | quarter |
-| 2008-12-01 | month   |
-| 2008-12-25 | day     |
-+------------+---------+
-(4 rows)
++------------+---------------+
+| d          | frame         |
++------------+---------------+
+| 2008-01-01 | year          |
+| 2007-12-31 | isoyear       |
+| 2008-10-01 | quarter       |
+| 2008-12-01 | month         |
+| 2008-12-21 | week          |
+| 2008-12-21 | week(sunday)  |
+| 2008-12-22 | week(monday)  |
+| 2008-12-23 | week(tuesday) |
+| 2008-12-22 | isoweek       |
+| 2008-12-25 | day           |
++------------+---------------+
+(10 rows)
 
 !ok
 
diff --git a/core/src/main/codegen/templates/Parser.jj b/core/src/main/codegen/templates/Parser.jj
index b9db2b83ce..0514b815e2 100644
--- a/core/src/main/codegen/templates/Parser.jj
+++ b/core/src/main/codegen/templates/Parser.jj
@@ -5023,59 +5023,75 @@ SqlIntervalQualifier IntervalQualifierStart() :
     }
 }
 
-/**
- * Parses time unit for CEIL and FLOOR functions.
+/** Parses a built-in time unit (e.g. "YEAR")
+ * or user-defined time frame (e.g. "MINUTE15")
+ * and in each case returns a {@link SqlIntervalQualifier}.
  *
- * <p>Includes NANOSECOND, MILLISECOND, which were previously only allowed in
- * the EXTRACT function.
+ * <p>The units are used in several functions, incuding CEIL, FLOOR, EXTRACT.
+ * Includes NANOSECOND, MILLISECOND, which were previously allowed in EXTRACT
+ * but not CEIL, FLOOR.
+ *
+ * <p>Includes {@code WEEK} and {@code WEEK(SUNDAY)} through
+  {@code WEEK(SATURDAY)}.
  *
  * <p>Does not include SQL_TSI_DAY, SQL_TSI_FRAC_SECOND etc. These will be
  * parsed as identifiers and can be resolved in the validator if they are
  * registered as abbreviations in your time frame set.
  */
-TimeUnit TimeUnit() :
-{
-}
-{
-    <NANOSECOND> { return TimeUnit.NANOSECOND; }
-|   <MICROSECOND> { return TimeUnit.MICROSECOND; }
-|   <MILLISECOND> { return TimeUnit.MILLISECOND; }
-|   <SECOND> { return TimeUnit.SECOND; }
-|   <MINUTE> { return TimeUnit.MINUTE; }
-|   <HOUR> { return TimeUnit.HOUR; }
-|   <DAY> { return TimeUnit.DAY; }
-|   <DOW> { return TimeUnit.DOW; }
-|   <DOY> { return TimeUnit.DOY; }
-|   <ISODOW> { return TimeUnit.ISODOW; }
-|   <ISOYEAR> { return TimeUnit.ISOYEAR; }
-|   <WEEK> { return TimeUnit.WEEK; }
-|   <MONTH> { return TimeUnit.MONTH; }
-|   <QUARTER> { return TimeUnit.QUARTER; }
-|   <YEAR> { return TimeUnit.YEAR; }
-|   <EPOCH> { return TimeUnit.EPOCH; }
-|   <DECADE> { return TimeUnit.DECADE; }
-|   <CENTURY> { return TimeUnit.CENTURY; }
-|   <MILLENNIUM> { return TimeUnit.MILLENNIUM; }
-}
-
-/** Parses a built-in time unit (e.g. "YEAR")
- * or user-defined time frame (e.g. "MINUTE15")
- * and in each case returns a {@link SqlIntervalQualifier}. */
 SqlIntervalQualifier TimeUnitOrName() : {
+    final Span span;
+    final String w;
     final TimeUnit unit;
     final SqlIdentifier unitName;
 }
 {
-    LOOKAHEAD(1)
-    unit = TimeUnit() {
-        return new SqlIntervalQualifier(unit, null, getPos());
-    }
+    LOOKAHEAD(2)
+    <NANOSECOND> { return new SqlIntervalQualifier(TimeUnit.NANOSECOND, null, getPos()); }
+|   <MICROSECOND> { return new SqlIntervalQualifier(TimeUnit.MICROSECOND, null, getPos()); }
+|   <MILLISECOND> { return new SqlIntervalQualifier(TimeUnit.MILLISECOND, null, getPos()); }
+|   <SECOND> { return new SqlIntervalQualifier(TimeUnit.SECOND, null, getPos()); }
+|   <MINUTE> { return new SqlIntervalQualifier(TimeUnit.MINUTE, null, getPos()); }
+|   <HOUR> { return new SqlIntervalQualifier(TimeUnit.HOUR, null, getPos()); }
+|   <DAY> { return new SqlIntervalQualifier(TimeUnit.DAY, null, getPos()); }
+|   <DOW> { return new SqlIntervalQualifier(TimeUnit.DOW, null, getPos()); }
+|   <DOY> { return new SqlIntervalQualifier(TimeUnit.DOY, null, getPos()); }
+|   <ISODOW> { return new SqlIntervalQualifier(TimeUnit.ISODOW, null, getPos()); }
+|   <ISOYEAR> { return new SqlIntervalQualifier(TimeUnit.ISOYEAR, null, getPos()); }
+|   <WEEK> { span = span(); }
+    (
+        LOOKAHEAD(2)
+        <LPAREN> w = weekdayName() <RPAREN> {
+            return new SqlIntervalQualifier(w, span.end(this));
+        }
+    |
+        { return new SqlIntervalQualifier(TimeUnit.WEEK, null, getPos()); }
+    )
+|   <MONTH> { return new SqlIntervalQualifier(TimeUnit.MONTH, null, getPos()); }
+|   <QUARTER> { return new SqlIntervalQualifier(TimeUnit.QUARTER, null, getPos()); }
+|   <YEAR> { return new SqlIntervalQualifier(TimeUnit.YEAR, null, getPos()); }
+|   <EPOCH> { return new SqlIntervalQualifier(TimeUnit.EPOCH, null, getPos()); }
+|   <DECADE> { return new SqlIntervalQualifier(TimeUnit.DECADE, null, getPos()); }
+|   <CENTURY> { return new SqlIntervalQualifier(TimeUnit.CENTURY, null, getPos()); }
+|   <MILLENNIUM> { return new SqlIntervalQualifier(TimeUnit.MILLENNIUM, null, getPos()); }
 |   unitName = SimpleIdentifier() {
         return new SqlIntervalQualifier(unitName.getSimple(),
             unitName.getParserPosition());
     }
 }
 
+String weekdayName() :
+{
+}
+{
+    <SUNDAY> { return "WEEK_SUNDAY"; }
+|   <MONDAY> { return "WEEK_MONDAY"; }
+|   <TUESDAY> { return "WEEK_TUESDAY"; }
+|   <WEDNESDAY> { return "WEEK_WEDNESDAY"; }
+|   <THURSDAY> { return "WEEK_THURSDAY"; }
+|   <FRIDAY> { return "WEEK_FRIDAY"; }
+|   <SATURDAY> { return "WEEK_SATURDAY"; }
+}
+
 /**
  * Parses a dynamic parameter marker.
  */
@@ -7712,6 +7728,7 @@ SqlPostfixOperator PostfixRowOperator() :
 |   < FRAC_SECOND: "FRAC_SECOND" >
 |   < FRAME_ROW: "FRAME_ROW" >
 |   < FREE: "FREE" >
+|   < FRIDAY: "FRIDAY" >
 |   < FROM: "FROM" > { beforeTableName(); }
 |   < FULL: "FULL" >
 |   < FUNCTION: "FUNCTION" >
@@ -7830,6 +7847,7 @@ SqlPostfixOperator PostfixRowOperator() :
 |   < MOD: "MOD" >
 |   < MODIFIES: "MODIFIES" >
 |   < MODULE: "MODULE" >
+|   < MONDAY: "MONDAY" >
 |   < MONTH: "MONTH" >
 |   < MONTHS: "MONTHS" >
 |   < MORE_: "MORE" >
@@ -7977,6 +7995,7 @@ SqlPostfixOperator PostfixRowOperator() :
 |   < ROW_NUMBER: "ROW_NUMBER" >
 |   < ROWS: "ROWS" >
 |   < RUNNING: "RUNNING" >
+|   < SATURDAY: "SATURDAY" >
 |   < SAVEPOINT: "SAVEPOINT" >
 |   < SCALAR: "SCALAR" >
 |   < SCALE: "SCALE" >
@@ -8090,6 +8109,7 @@ SqlPostfixOperator PostfixRowOperator() :
 |   < SUBSTRING_REGEX: "SUBSTRING_REGEX" >
 |   < SUCCEEDS: "SUCCEEDS" >
 |   < SUM: "SUM" >
+|   < SUNDAY: "SUNDAY" >
 |   < SYMMETRIC: "SYMMETRIC" >
 |   < SYSTEM: "SYSTEM" >
 |   < SYSTEM_TIME: "SYSTEM_TIME" >
@@ -8099,6 +8119,7 @@ SqlPostfixOperator PostfixRowOperator() :
 |   < TABLESAMPLE: "TABLESAMPLE" >
 |   < TEMPORARY: "TEMPORARY" >
 |   < THEN: "THEN" >
+|   < THURSDAY: "THURSDAY" >
 |   < TIES: "TIES" >
 |   < TIME: "TIME" >
 |   < TIME_DIFF: "TIME_DIFF" >
@@ -8132,6 +8153,7 @@ SqlPostfixOperator PostfixRowOperator() :
 |   < TRIM_ARRAY: "TRIM_ARRAY" >
 |   < TRUE: "TRUE" >
 |   < TRUNCATE: "TRUNCATE" >
+|   < TUESDAY: "TUESDAY" >
 |   < TUMBLE: "TUMBLE" >
 |   < TYPE: "TYPE" >
 |   < UESCAPE: "UESCAPE" >
@@ -8169,6 +8191,7 @@ SqlPostfixOperator PostfixRowOperator() :
 |   < VERSION: "VERSION" >
 |   < VERSIONING: "VERSIONING" >
 |   < VIEW: "VIEW" >
+|   < WEDNESDAY: "WEDNESDAY" >
 |   < WEEK: "WEEK" >
 |   < WEEKS: "WEEKS" >
 |   < WHEN: "WHEN" >
diff --git a/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java b/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java
index c4845a7d29..35d563e23e 100644
--- a/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java
+++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java
@@ -2181,6 +2181,7 @@ public class RexImpTable {
             "timeUnitRange");
         switch (timeUnitRange) {
         case YEAR:
+        case ISOYEAR:
         case QUARTER:
         case MONTH:
         case WEEK:
diff --git a/core/src/main/java/org/apache/calcite/rel/type/TimeFrames.java b/core/src/main/java/org/apache/calcite/rel/type/TimeFrames.java
index 8eb4bd13a0..f69e66f3c6 100644
--- a/core/src/main/java/org/apache/calcite/rel/type/TimeFrames.java
+++ b/core/src/main/java/org/apache/calcite/rel/type/TimeFrames.java
@@ -19,12 +19,14 @@ package org.apache.calcite.rel.type;
 import org.apache.calcite.avatica.util.DateTimeUtils;
 import org.apache.calcite.avatica.util.TimeUnit;
 import org.apache.calcite.avatica.util.TimeUnitRange;
+import org.apache.calcite.linq4j.Ord;
 import org.apache.calcite.util.MonotonicSupplier;
 import org.apache.calcite.util.Pair;
 import org.apache.calcite.util.TimestampString;
 
 import org.apache.commons.math3.fraction.BigFraction;
 
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableMultimap;
 import com.google.common.collect.Iterables;
@@ -36,6 +38,7 @@ import java.util.Calendar;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.LinkedHashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.function.Supplier;
@@ -51,8 +54,19 @@ public class TimeFrames {
   private TimeFrames() {
   }
 
+  /** The names of the frames that are WEEK starting on each week day.
+   * Entry 0 is "WEEK_SUNDAY" and entry 6 is "WEEK_SATURDAY". */
+  public static final List<String> WEEK_FRAME_NAMES =
+      ImmutableList.of("WEEK_SUNDAY",
+          "WEEK_MONDAY",
+          "WEEK_TUESDAY",
+          "WEEK_WEDNESDAY",
+          "WEEK_THURSDAY",
+          "WEEK_FRIDAY",
+          "WEEK_SATURDAY");
+
   /** The core time frame set. Includes the time frames for all Avatica time
-   * units plus ISOWEEK:
+   * units plus ISOWEEK and week offset for each week day:
    *
    * <ul>
    *   <li>SECOND, and multiples MINUTE, HOUR, DAY, WEEK (starts on a Sunday),
@@ -60,6 +74,8 @@ public class TimeFrames {
    *   quotients DOY, DOW;
    *   <li>MONTH, and multiples QUARTER, YEAR, DECADE, CENTURY, MILLENNIUM;
    *   <li>ISOYEAR, and sub-unit ISOWEEK (starts on a Monday), quotient ISODOW;
+   *   <li>WEEK(<i>weekday</i>) with <i>weekday</i> being one of
+   *   SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY.
    * </ul>
    *
    * <p>Does not include EPOCH.
@@ -94,12 +110,18 @@ public class TimeFrames {
     b.addSub("ISOWEEK", false, 7, TimeUnit.DAY.name(),
         new TimestampString(1970, 1, 5, 0, 0, 0)); // a monday
 
+    // Add "WEEK(SUNDAY)" through "WEEK(SATURDAY)"
+    Ord.forEach(WEEK_FRAME_NAMES, (frameName, i) ->
+        b.addSub(frameName, false, 7,
+            "DAY", new TimestampString(1970, 1, 4 + i, 0, 0, 0)));
+
     b.addQuotient(TimeUnit.DOY, TimeUnit.DAY, TimeUnit.YEAR);
     b.addQuotient(TimeUnit.DOW, TimeUnit.DAY, TimeUnit.WEEK);
     b.addQuotient(TimeUnit.ISODOW.name(), TimeUnit.DAY.name(), "ISOWEEK");
 
     b.addRollup(TimeUnit.DAY, TimeUnit.MONTH);
     b.addRollup("ISOWEEK", TimeUnit.ISOYEAR.name());
+
     return b;
   }
 
diff --git a/core/src/main/java/org/apache/calcite/sql/fun/SqlLibraryOperators.java b/core/src/main/java/org/apache/calcite/sql/fun/SqlLibraryOperators.java
index 8e82dd9e80..6d36db0b3e 100644
--- a/core/src/main/java/org/apache/calcite/sql/fun/SqlLibraryOperators.java
+++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlLibraryOperators.java
@@ -16,7 +16,6 @@
  */
 package org.apache.calcite.sql.fun;
 
-import org.apache.calcite.avatica.util.TimeUnitRange;
 import org.apache.calcite.rel.type.RelDataType;
 import org.apache.calcite.rel.type.RelDataTypeFactory;
 import org.apache.calcite.sql.SqlAggFunction;
@@ -43,13 +42,10 @@ import org.apache.calcite.sql.type.SqlTypeTransforms;
 import org.apache.calcite.util.Litmus;
 import org.apache.calcite.util.Optionality;
 
-import com.google.common.collect.ImmutableSet;
-
 import org.checkerframework.checker.nullness.qual.Nullable;
 
 import java.util.ArrayList;
 import java.util.List;
-import java.util.Set;
 
 import static org.apache.calcite.sql.fun.SqlLibrary.BIG_QUERY;
 import static org.apache.calcite.sql.fun.SqlLibrary.CALCITE;
@@ -672,31 +668,6 @@ public abstract class SqlLibraryOperators {
           OperandTypes.STRING_STRING,
           SqlFunctionCategory.TIMEDATE);
 
-  private static final Set<TimeUnitRange> TIME_UNITS =
-      ImmutableSet.of(TimeUnitRange.HOUR,
-          TimeUnitRange.MINUTE,
-          TimeUnitRange.SECOND);
-
-  private static final Set<TimeUnitRange> MONTH_UNITS =
-      ImmutableSet.of(TimeUnitRange.MILLENNIUM,
-          TimeUnitRange.CENTURY,
-          TimeUnitRange.DECADE,
-          TimeUnitRange.YEAR,
-          TimeUnitRange.QUARTER,
-          TimeUnitRange.MONTH);
-
-  private static final Set<TimeUnitRange> DAY_UNITS =
-      ImmutableSet.of(TimeUnitRange.WEEK,
-          TimeUnitRange.DAY);
-
-  private static final Set<TimeUnitRange> DATE_UNITS =
-      ImmutableSet.<TimeUnitRange>builder()
-          .addAll(MONTH_UNITS).addAll(DAY_UNITS).build();
-
-  private static final Set<TimeUnitRange> TIMESTAMP_UNITS =
-      ImmutableSet.<TimeUnitRange>builder()
-          .addAll(DATE_UNITS).addAll(TIME_UNITS).build();
-
   /** The "TIMESTAMP_ADD(timestamp, interval)" function (BigQuery), the
    * two-argument variant of the built-in
    * {@link SqlStdOperatorTable#TIMESTAMP_ADD TIMESTAMPADD} function, which has
@@ -745,7 +716,7 @@ public abstract class SqlLibraryOperators {
       SqlBasicFunction.create("DATE_TRUNC",
           ReturnTypes.DATE_NULLABLE,
           OperandTypes.sequence("'DATE_TRUNC(<DATE>, <DATETIME_INTERVAL>)'",
-              OperandTypes.DATE, OperandTypes.interval(DATE_UNITS)),
+              OperandTypes.DATE, OperandTypes.dateInterval()),
           SqlFunctionCategory.TIMEDATE);
 
   /** The "TIME_SUB(time, interval)" function (BigQuery);
@@ -767,7 +738,7 @@ public abstract class SqlLibraryOperators {
       SqlBasicFunction.create("TIME_TRUNC",
           ReturnTypes.TIME_NULLABLE,
           OperandTypes.sequence("'TIME_TRUNC(<TIME>, <DATETIME_INTERVAL>)'",
-              OperandTypes.TIME, OperandTypes.interval(TIME_UNITS)),
+              OperandTypes.TIME, OperandTypes.timeInterval()),
           SqlFunctionCategory.TIMEDATE);
 
   /** The "TIMESTAMP_SUB(timestamp, interval)" function (BigQuery);
@@ -790,7 +761,7 @@ public abstract class SqlLibraryOperators {
           ReturnTypes.TIMESTAMP_NULLABLE,
           OperandTypes.sequence(
               "'TIMESTAMP_TRUNC(<TIMESTAMP>, <DATETIME_INTERVAL>)'",
-              OperandTypes.TIMESTAMP, OperandTypes.interval(TIMESTAMP_UNITS)),
+              OperandTypes.TIMESTAMP, OperandTypes.timestampInterval()),
           SqlFunctionCategory.TIMEDATE);
 
   /** The "TIMESTAMP_SECONDS(bigint)" function; returns a TIMESTAMP value
diff --git a/core/src/main/java/org/apache/calcite/sql/type/IntervalOperandTypeChecker.java b/core/src/main/java/org/apache/calcite/sql/type/IntervalOperandTypeChecker.java
index 9d99b5b574..f86299ebbf 100644
--- a/core/src/main/java/org/apache/calcite/sql/type/IntervalOperandTypeChecker.java
+++ b/core/src/main/java/org/apache/calcite/sql/type/IntervalOperandTypeChecker.java
@@ -16,7 +16,6 @@
  */
 package org.apache.calcite.sql.type;
 
-import org.apache.calcite.avatica.util.TimeUnitRange;
 import org.apache.calcite.sql.SqlCallBinding;
 import org.apache.calcite.sql.SqlIntervalQualifier;
 import org.apache.calcite.sql.SqlNode;
@@ -24,17 +23,17 @@ import org.apache.calcite.sql.SqlOperator;
 import org.apache.calcite.util.Static;
 import org.apache.calcite.util.Util;
 
-import com.google.common.collect.ImmutableSet;
+import java.util.function.Predicate;
 
 /**
  * Parameter type-checking strategy whether the operand must be an interval.
  */
 public class IntervalOperandTypeChecker implements SqlSingleOperandTypeChecker {
 
-  private final ImmutableSet<TimeUnitRange> unitSet;
+  private final Predicate<SqlIntervalQualifier> predicate;
 
-  IntervalOperandTypeChecker(ImmutableSet<TimeUnitRange> unitSet) {
-    this.unitSet = unitSet;
+  IntervalOperandTypeChecker(Predicate<SqlIntervalQualifier> predicate) {
+    this.predicate = predicate;
   }
 
   @Override public boolean checkSingleOperandType(SqlCallBinding callBinding,
@@ -42,7 +41,7 @@ public class IntervalOperandTypeChecker implements SqlSingleOperandTypeChecker {
     final SqlNode operand = callBinding.operand(iFormalOperand);
     if (operand instanceof SqlIntervalQualifier) {
       final SqlIntervalQualifier interval = (SqlIntervalQualifier) operand;
-      if (unitSet.contains(interval.timeUnitRange)) {
+      if (predicate.test(interval)) {
         return true;
       }
       if (throwOnFailure) {
diff --git a/core/src/main/java/org/apache/calcite/sql/type/OperandTypes.java b/core/src/main/java/org/apache/calcite/sql/type/OperandTypes.java
index a2bc22178b..82f813c10d 100644
--- a/core/src/main/java/org/apache/calcite/sql/type/OperandTypes.java
+++ b/core/src/main/java/org/apache/calcite/sql/type/OperandTypes.java
@@ -20,6 +20,7 @@ import org.apache.calcite.avatica.util.TimeUnitRange;
 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.TimeFrames;
 import org.apache.calcite.sql.SqlCallBinding;
 import org.apache.calcite.sql.SqlLiteral;
 import org.apache.calcite.sql.SqlNode;
@@ -39,6 +40,7 @@ import java.math.BigDecimal;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
+import java.util.Set;
 import java.util.function.BiFunction;
 import java.util.function.Function;
 import java.util.function.IntFunction;
@@ -66,6 +68,39 @@ import static org.apache.calcite.util.Static.RESOURCE;
  * @see org.apache.calcite.sql.type.InferTypes
  */
 public abstract class OperandTypes {
+  private static final Set<TimeUnitRange> TIME_UNITS =
+      ImmutableSet.of(TimeUnitRange.HOUR,
+          TimeUnitRange.MINUTE,
+          TimeUnitRange.SECOND);
+
+  private static final Set<TimeUnitRange> MONTH_UNITS =
+      ImmutableSet.of(TimeUnitRange.MILLENNIUM,
+          TimeUnitRange.CENTURY,
+          TimeUnitRange.DECADE,
+          TimeUnitRange.YEAR,
+          TimeUnitRange.ISOYEAR,
+          TimeUnitRange.QUARTER,
+          TimeUnitRange.MONTH);
+
+  private static final Set<TimeUnitRange> DAY_UNITS =
+      ImmutableSet.of(TimeUnitRange.WEEK,
+          TimeUnitRange.DAY);
+
+  private static final Set<TimeUnitRange> DATE_UNITS =
+      ImmutableSet.<TimeUnitRange>builder()
+          .addAll(MONTH_UNITS).addAll(DAY_UNITS).build();
+
+  private static final Set<TimeUnitRange> TIMESTAMP_UNITS =
+      ImmutableSet.<TimeUnitRange>builder()
+          .addAll(DATE_UNITS).addAll(TIME_UNITS).build();
+
+  private static final Set<String> WEEK_FRAMES =
+      ImmutableSet.<String>builder()
+          .addAll(TimeFrames.WEEK_FRAME_NAMES)
+          .add("ISOWEEK")
+          .add("WEEK")
+          .build();
+
   private OperandTypes() {
   }
 
@@ -102,7 +137,37 @@ public abstract class OperandTypes {
    */
   public static SqlSingleOperandTypeChecker interval(
       Iterable<TimeUnitRange> ranges) {
-    return new IntervalOperandTypeChecker(ImmutableSet.copyOf(ranges));
+    final Set<TimeUnitRange> set = ImmutableSet.copyOf(ranges);
+    return new IntervalOperandTypeChecker(intervalQualifier ->
+        set.contains(intervalQualifier.timeUnitRange));
+  }
+
+  /**
+   * Creates a checker for DATE intervals (YEAR, WEEK, ISOWEEK,
+   * WEEK_WEDNESDAY, etc.)
+   */
+  public static SqlSingleOperandTypeChecker dateInterval() {
+    return new IntervalOperandTypeChecker(intervalQualifier ->
+        DATE_UNITS.contains(intervalQualifier.timeUnitRange)
+            || WEEK_FRAMES.contains(intervalQualifier.timeFrameName));
+  }
+
+  /**
+   * Creates a checker for TIME intervals (HOUR, SECOND, etc.)
+   */
+  public static SqlSingleOperandTypeChecker timeInterval() {
+    return new IntervalOperandTypeChecker(intervalQualifier ->
+        TIME_UNITS.contains(intervalQualifier.timeUnitRange));
+  }
+
+  /**
+   * Creates a checker for TIMESTAMP intervals (YEAR, WEEK, ISOWEEK,
+   * WEEK_WEDNESDAY, HOUR, SECOND, etc.)
+   */
+  public static SqlSingleOperandTypeChecker timestampInterval() {
+    return new IntervalOperandTypeChecker(intervalQualifier ->
+        TIMESTAMP_UNITS.contains(intervalQualifier.timeUnitRange)
+            || WEEK_FRAMES.contains(intervalQualifier.timeFrameName));
   }
 
   /**
@@ -797,7 +862,6 @@ public abstract class OperandTypes {
         }
       };
 
-
   /**
    * Checker for record just has one field.
    */
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 05d3789076..7fcf6cf5a1 100644
--- a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
+++ b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
@@ -2708,6 +2708,36 @@ public class SqlValidatorTest extends SqlValidatorTestCase {
     invalidCodes.forEach(invalidConsumer);
   }
 
+  /** Checks parsing of built-in functions that accept time unit
+   *  Checks WEEK(WEEKDAY)
+   * <p>Override if your parser supports more such functions. */
+  @Test void checkWeekdayCustomTimeFrames() {
+    SqlValidatorFixture f = fixture()
+        .withOperatorTable(operatorTableFor(SqlLibrary.BIG_QUERY));
+
+    // Check that each valid code passes each query that it should.
+    final String ds = "DATE '2022-12-25'";
+    Consumer<String> validConsumer = weekday -> {
+      f.withSql("select date_trunc(" + ds + ", " + weekday + ")").ok();
+    };
+    validConsumer.accept("WEEK");
+    validConsumer.accept("WEEK(SUNDAY)");
+    validConsumer.accept("WEEK(MONDAY)");
+    validConsumer.accept("WEEK(TUESDAY)");
+    validConsumer.accept("WEEK(WEDNESDAY)");
+    validConsumer.accept("WEEK(THURSDAY)");
+    validConsumer.accept("WEEK(FRIDAY)");
+    validConsumer.accept("WEEK(SUNDAY)");
+
+    // Check that each invalid code fails each query that it should.
+    Consumer<String> invalidConsumer = weekday -> {
+      String errorMessage = "'" + weekday + "' is not a valid time frame";
+      f.withSql("select date_trunc(" + ds + ", ^" + weekday + "^)")
+          .fails(errorMessage);
+    };
+    invalidConsumer.accept("A");
+  }
+
   public void checkWinFuncExpWithWinClause(
       String sql,
       String expectedMsgPattern) {
diff --git a/core/src/test/java/org/apache/calcite/test/TimeFrameTest.java b/core/src/test/java/org/apache/calcite/test/TimeFrameTest.java
index 046b94ffd6..e3f6f959fa 100644
--- a/core/src/test/java/org/apache/calcite/test/TimeFrameTest.java
+++ b/core/src/test/java/org/apache/calcite/test/TimeFrameTest.java
@@ -229,6 +229,9 @@ public class TimeFrameTest {
     f.checkDateFloor("1970-08-03", f.isoWeek, is("1970-08-03")); // monday
     f.checkDateFloor("1970-08-04", f.isoWeek, is("1970-08-03")); // tuesday
 
+    f.checkDateFloor("1970-08-04", "WEEK_MONDAY", is("1970-08-03")); // tuesday
+    f.checkDateFloor("1970-08-04", "WEEK_TUESDAY", is("1970-08-04")); // tuesday
+
     f.checkTimestampFloor("1970-01-01 01:23:45", HOUR,
         0, is("1970-01-01 01:00:00"));
     f.checkTimestampFloor("1970-01-01 01:23:45", MINUTE,
@@ -577,6 +580,13 @@ public class TimeFrameTest {
           unixDateToString(outDate), matcher);
     }
 
+    void checkDateFloor(String in, String timeFrameName, Matcher<String> matcher) {
+      int inDate = dateStringToUnixDate(in);
+      int outDate = timeFrameSet.floorDate(inDate, timeFrameSet.get(timeFrameName));
+      assertThat("floor(" + in + " to " + timeFrameName + ")",
+          unixDateToString(outDate), matcher);
+    }
+
     void checkTimestampFloor(String in, TimeUnit unit, int precision,
         Matcher<String> matcher) {
       long inTs = timestampStringToUnixDate(in);
diff --git a/site/_docs/reference.md b/site/_docs/reference.md
index 011890b412..65c4a0eaa7 100644
--- a/site/_docs/reference.md
+++ b/site/_docs/reference.md
@@ -627,6 +627,7 @@ FOUND,
 FRAC_SECOND,
 **FRAME_ROW**,
 **FREE**,
+**FRIDAY**,
 **FROM**,
 **FULL**,
 **FUNCTION**,
@@ -746,6 +747,7 @@ MINVALUE,
 **MOD**,
 **MODIFIES**,
 **MODULE**,
+**MONDAY**,
 **MONTH**,
 MONTHS,
 MORE,
@@ -893,6 +895,7 @@ ROUTINE_SCHEMA,
 ROW_COUNT,
 **ROW_NUMBER**,
 **RUNNING**,
+**SATURDAY**,
 **SAVEPOINT**,
 SCALAR,
 SCALE,
@@ -1005,6 +1008,7 @@ SUBSTITUTE,
 **SUBSTRING_REGEX**,
 **SUCCEEDS**,
 **SUM**,
+**SUNDAY**,
 **SYMMETRIC**,
 **SYSTEM**,
 **SYSTEM_TIME**,
@@ -1014,6 +1018,7 @@ SUBSTITUTE,
 TABLE_NAME,
 TEMPORARY,
 **THEN**,
+**THURSDAY**,
 TIES,
 **TIME**,
 **TIMESTAMP**,
@@ -1047,6 +1052,7 @@ TRIGGER_SCHEMA,
 **TRIM_ARRAY**,
 **TRUE**,
 **TRUNCATE**,
+**TUESDAY**,
 TUMBLE,
 TYPE,
 **UESCAPE**,
@@ -1084,6 +1090,7 @@ UTF8,
 VERSION,
 **VERSIONING**,
 VIEW,
+**WEDNESDAY**,
 WEEK,
 WEEKS,
 **WHEN**,
@@ -1168,9 +1175,9 @@ Note:
 * GEOMETRY is allowed only in certain
   [conformance levels]({{ site.apiRoot }}/org/apache/calcite/sql/validate/SqlConformance.html#allowGeometry--).
 * Interval literals may only use time units
-  YEAR, MONTH, DAY, HOUR, MINUTE and SECOND. In certain
+  YEAR, QUARTER, MONTH, WEEK, DAY, HOUR, MINUTE and SECOND. In certain
   [conformance levels]({{ site.apiRoot }}/org/apache/calcite/sql/validate/SqlConformance.html#allowPluralTimeUnits--),
-  we also allow their plurals, YEARS, MONTHS, DAYS, HOURS, MINUTES and SECONDS.
+  we also allow their plurals, YEARS, QUARTERS, MONTHS, WEEKS, DAYS, HOURS, MINUTES and SECONDS.
 
 ### Non-scalar types
 
diff --git a/testkit/src/main/java/org/apache/calcite/sql/parser/SqlParserTest.java b/testkit/src/main/java/org/apache/calcite/sql/parser/SqlParserTest.java
index 00dcddc4f4..9b5fc4aca2 100644
--- a/testkit/src/main/java/org/apache/calcite/sql/parser/SqlParserTest.java
+++ b/testkit/src/main/java/org/apache/calcite/sql/parser/SqlParserTest.java
@@ -270,6 +270,7 @@ public class SqlParserTest {
       "FOUND",                         "92", "99",
       "FRAME_ROW",                                                 "2014", "c",
       "FREE",                                "99", "2003", "2011", "2014", "c",
+      "FRIDAY",                                                            "c",
       "FROM",                          "92", "99", "2003", "2011", "2014", "c",
       "FULL",                          "92", "99", "2003", "2011", "2014", "c",
       "FUNCTION",                      "92", "99", "2003", "2011", "2014", "c",
@@ -361,6 +362,7 @@ public class SqlParserTest {
       "MOD",                                               "2011", "2014", "c",
       "MODIFIES",                            "99", "2003", "2011", "2014", "c",
       "MODULE",                        "92", "99", "2003", "2011", "2014", "c",
+      "MONDAY",                                                            "c",
       "MONTH",                         "92", "99", "2003", "2011", "2014", "c",
       "MULTISET",                                  "2003", "2011", "2014", "c",
       "NAMES",                         "92", "99",
@@ -465,6 +467,7 @@ public class SqlParserTest {
       "ROWS",                          "92", "99", "2003", "2011", "2014", "c",
       "ROW_NUMBER",                                        "2011", "2014", "c",
       "RUNNING",                                                   "2014", "c",
+      "SATURDAY",                                                          "c",
       "SAVEPOINT",                           "99", "2003", "2011", "2014", "c",
       "SCHEMA",                        "92", "99",
       "SCOPE",                               "99", "2003", "2011", "2014", "c",
@@ -509,6 +512,7 @@ public class SqlParserTest {
       "SUBSTRING_REGEX",                                   "2011", "2014", "c",
       "SUCCEEDS",                                                  "2014", "c",
       "SUM",                           "92",               "2011", "2014", "c",
+      "SUNDAY",                                                            "c",
       "SYMMETRIC",                           "99", "2003", "2011", "2014", "c",
       "SYSTEM",                              "99", "2003", "2011", "2014", "c",
       "SYSTEM_TIME",                                               "2014", "c",
@@ -517,6 +521,7 @@ public class SqlParserTest {
       "TABLESAMPLE",                               "2003", "2011", "2014", "c",
       "TEMPORARY",                     "92", "99",
       "THEN",                          "92", "99", "2003", "2011", "2014", "c",
+      "THURSDAY",                                                          "c",
       "TIME",                          "92", "99", "2003", "2011", "2014", "c",
       "TIMESTAMP",                     "92", "99", "2003", "2011", "2014", "c",
       "TIMEZONE_HOUR",                 "92", "99", "2003", "2011", "2014", "c",
@@ -534,6 +539,7 @@ public class SqlParserTest {
       "TRIM_ARRAY",                                        "2011", "2014", "c",
       "TRUE",                          "92", "99", "2003", "2011", "2014", "c",
       "TRUNCATE",                                          "2011", "2014", "c",
+      "TUESDAY",                                                           "c",
       "UESCAPE",                                           "2011", "2014", "c",
       "UNDER",                               "99",
       "UNDO",                          "92", "99", "2003",
@@ -560,6 +566,7 @@ public class SqlParserTest {
       "VERSIONING",                                        "2011", "2014", "c",
       "VERSIONS",                                          "2011",
       "VIEW",                          "92", "99",
+      "WEDNESDAY",                                                         "c",
       "WHEN",                          "92", "99", "2003", "2011", "2014", "c",
       "WHENEVER",                      "92", "99", "2003", "2011", "2014", "c",
       "WHERE",                         "92", "99", "2003", "2011", "2014", "c",
@@ -6468,7 +6475,7 @@ public class SqlParserTest {
         .fails("(?s)Encountered \"to\".*");
   }
 
-  /** Tests that EXTRACT, FLOOR, CEIL functions accept abbreviations for
+  /** Tests that EXTRACT, FLOOR, CEIL, DATE_TRUNC functions accept abbreviations for
    * time units (such as "Y" for "YEAR") when configured via
    * {@link Config#timeUnitCodes()}. */
   @Test protected void testTimeUnitCodes() {
@@ -6502,6 +6509,11 @@ public class SqlParserTest {
     expr("ceiling(d to microsecond)").ok("CEIL(`D` TO MICROSECOND)");
     expr("extract(nanosecond from d)").ok("EXTRACT(NANOSECOND FROM `D`)");
     expr("extract(microsecond from d)").ok("EXTRACT(MICROSECOND FROM `D`)");
+
+    // As for FLOOR, so for DATE_TRUNC.
+    expr("date_trunc(d , year)").ok("DATE_TRUNC(`D`, YEAR)");
+    expr("date_trunc(d , y)").ok("DATE_TRUNC(`D`, `Y`)");
+    expr("date_trunc(d , week(tuesday))").ok("DATE_TRUNC(`D`, `WEEK_TUESDAY`)");
   }
 
   @Test void testGeometry() {
diff --git a/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java b/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
index 1e5734be39..3d03cd2cb3 100644
--- a/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
+++ b/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
@@ -8289,12 +8289,18 @@ public class SqlOperatorTest {
     f.checkScalar("timestamp_sub(timestamp '2016-02-24 12:42:25', interval 2 week)",
         "2016-02-10 12:42:25",
         "TIMESTAMP(0) NOT NULL");
+    f.checkScalar("timestamp_sub(timestamp '2016-02-24 12:42:25', interval 2 weeks)",
+        "2016-02-10 12:42:25",
+        "TIMESTAMP(0) NOT NULL");
     f.checkScalar("timestamp_sub(timestamp '2016-02-24 12:42:25', interval 1 month)",
         "2016-01-24 12:42:25",
         "TIMESTAMP(0) NOT NULL");
     f.checkScalar("timestamp_sub(timestamp '2016-02-24 12:42:25', interval 1 quarter)",
         "2015-11-24 12:42:25",
         "TIMESTAMP(0) NOT NULL");
+    f.checkScalar("timestamp_sub(timestamp '2016-02-24 12:42:25', interval 1 quarters)",
+        "2015-11-24 12:42:25",
+        "TIMESTAMP(0) NOT NULL");
     f.checkScalar("timestamp_sub(timestamp '2016-02-24 12:42:25', interval 1 year)",
         "2015-02-24 12:42:25",
         "TIMESTAMP(0) NOT NULL");
@@ -8354,6 +8360,9 @@ public class SqlOperatorTest {
     f.checkScalar("date_sub(date '2016-02-24', interval 1 week)",
         "2016-02-17",
         "DATE NOT NULL");
+    f.checkScalar("date_sub(date '2016-02-24', interval 2 weeks)",
+        "2016-02-10",
+        "DATE NOT NULL");
     f.checkScalar("date_sub(date '2020-10-17', interval 0 week)",
         "2020-10-17",
         "DATE NOT NULL");
@@ -8363,6 +8372,9 @@ public class SqlOperatorTest {
     f.checkScalar("date_sub(date '2016-02-24', interval 1 quarter)",
         "2015-11-24",
         "DATE NOT NULL");
+    f.checkScalar("date_sub(date '2016-02-24', interval 2 quarters)",
+        "2015-08-24",
+        "DATE NOT NULL");
     f.checkScalar("date_sub(date '2016-02-24', interval 5 year)",
         "2011-02-24",
         "DATE NOT NULL");
@@ -8530,12 +8542,30 @@ public class SqlOperatorTest {
         "2015-02-19", "DATE NOT NULL");
     f.checkScalar("date_trunc(date '2015-02-19', week)",
         "2015-02-15", "DATE NOT NULL");
+    f.checkScalar("date_trunc(date '2015-02-19', isoweek)",
+        "2015-02-16", "DATE NOT NULL");
+    f.checkScalar("date_trunc(date '2015-02-19', week(sunday))",
+        "2015-02-15", "DATE NOT NULL");
+    f.checkScalar("date_trunc(date '2015-02-19', week(monday))",
+        "2015-02-16", "DATE NOT NULL");
+    f.checkScalar("date_trunc(date '2015-02-19', week(tuesday))",
+        "2015-02-17", "DATE NOT NULL");
+    f.checkScalar("date_trunc(date '2015-02-19', week(wednesday))",
+        "2015-02-18", "DATE NOT NULL");
+    f.checkScalar("date_trunc(date '2015-02-19', week(thursday))",
+        "2015-02-19", "DATE NOT NULL");
+    f.checkScalar("date_trunc(date '2015-02-19', week(friday))",
+        "2015-02-13", "DATE NOT NULL");
+    f.checkScalar("date_trunc(date '2015-02-19', week(saturday))",
+        "2015-02-14", "DATE NOT NULL");
     f.checkScalar("date_trunc(date '2015-02-19', month)",
         "2015-02-01", "DATE NOT NULL");
     f.checkScalar("date_trunc(date '2015-02-19', quarter)",
         "2015-01-01", "DATE NOT NULL");
     f.checkScalar("date_trunc(date '2015-02-19', year)",
         "2015-01-01", "DATE NOT NULL");
+    f.checkScalar("date_trunc(date '2015-02-19', isoyear)",
+        "2014-12-29", "DATE NOT NULL");
   }
 
   @Test void testDenseRankFunc() {


[calcite] 01/04: Factor interval tests out of SqlParserTest and SqlValidatorTest and into new class IntervalTest

Posted by jh...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit b623215e2f033a5303b3d4dc01be42ae9d942c8e
Author: Julian Hyde <jh...@apache.org>
AuthorDate: Fri Dec 9 01:48:32 2022 -0800

    Factor interval tests out of SqlParserTest and SqlValidatorTest and into new class IntervalTest
    
    This change allows interval tests to be shared between the
    parser and validator test without maintaining duplicate
    test code.
---
 .../org/apache/calcite/test/SqlValidatorTest.java  | 1797 +------------------
 .../calcite/sql/parser/SqlParserFixture.java       |    6 +-
 .../apache/calcite/sql/parser/SqlParserTest.java   | 1651 +-----------------
 .../java/org/apache/calcite/test/IntervalTest.java | 1834 ++++++++++++++++++++
 4 files changed, 1920 insertions(+), 3368 deletions(-)

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 4dc72e96ae..05d3789076 100644
--- a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
+++ b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
@@ -793,10 +793,10 @@ public class SqlValidatorTest extends SqlValidatorTestCase {
         .fails(ANY);
   }
 
-  // FIXME jvs 2-Feb-2005: all collation-related tests are disabled due to
-  // dtbug 280
+  // FIXME jvs 2-Feb-2005:
 
-  public void _testSimpleCollate() {
+  @Disabled("all collation-related tests are disabled due to dtbug 280")
+  void testSimpleCollate() {
     expr("'s' collate latin1$en$1").ok();
     expr("'s' collate latin1$en$1")
         .columnType("CHAR(1)");
@@ -808,19 +808,22 @@ public class SqlValidatorTest extends SqlValidatorTestCase {
             is(SqlCollation.Coercibility.EXPLICIT));
   }
 
-  public void _testCharsetAndCollateMismatch() {
+  @Disabled("all collation-related tests are disabled due to dtbug 280")
+  void testCharsetAndCollateMismatch() {
     // todo
     expr("_UTF16's' collate latin1$en$1")
         .fails("?");
   }
 
-  public void _testDyadicCollateCompare() {
+  @Disabled("all collation-related tests are disabled due to dtbug 280")
+  void testDyadicCollateCompare() {
     expr("'s' collate latin1$en$1 < 't'").ok();
     expr("'t' > 's' collate latin1$en$1").ok();
     expr("'s' collate latin1$en$1 <> 't' collate latin1$en$1").ok();
   }
 
-  public void _testDyadicCompareCollateFails() {
+  @Disabled("all collation-related tests are disabled due to dtbug 280")
+  void testDyadicCompareCollateFails() {
     // two different explicit collations. difference in strength
     expr("'s' collate latin1$en$1 <= 't' collate latin1$en$2")
         .fails("(?s).*Two explicit different collations.*are illegal.*");
@@ -830,7 +833,8 @@ public class SqlValidatorTest extends SqlValidatorTestCase {
         .fails("(?s).*Two explicit different collations.*are illegal.*");
   }
 
-  public void _testDyadicCollateOperator() {
+  @Disabled("all collation-related tests are disabled due to dtbug 280")
+  void testDyadicCollateOperator() {
     sql("'a' || 'b'")
         .assertCollation(is("ISO-8859-1$en_US$primary"),
             is(SqlCollation.Coercibility.COERCIBLE));
@@ -911,7 +915,8 @@ public class SqlValidatorTest extends SqlValidatorTestCase {
         .fails("(?s).*not comparable to each other.*");
   }
 
-  public void _testConvertAndTranslate() {
+  @Disabled
+  void testConvertAndTranslate() {
     expr("convert('abc' using conversion)").ok();
     expr("translate('abc' using translation)").ok();
   }
@@ -1053,7 +1058,8 @@ public class SqlValidatorTest extends SqlValidatorTestCase {
         .columnType("BOOLEAN NOT NULL");
   }
 
-  public void _testLikeAndSimilarFails() {
+  @Disabled
+  void testLikeAndSimilarFails() {
     expr("'a' like _UTF16'b'  escape 'c'")
         .fails("(?s).*Operands _ISO-8859-1.a. COLLATE ISO-8859-1.en_US.primary,"
             + " _SHIFT_JIS.b..*");
@@ -2181,1700 +2187,6 @@ public class SqlValidatorTest extends SqlValidatorTestCase {
         .assertInterval(is(3_903_123L));
   }
 
-  /**
-   * Runs tests for INTERVAL... YEAR that should pass both parser and
-   * validator. A substantially identical set of tests exists in
-   * SqlParserTest, and any changes here should be synchronized there.
-   * Similarly, any changes to tests here should be echoed appropriately to
-   * each of the other 12 subTestIntervalXXXPositive() tests.
-   */
-  void subTestIntervalYearPositive() {
-    // default precision
-    expr("INTERVAL '1' YEAR")
-        .columnType("INTERVAL YEAR NOT NULL");
-    expr("INTERVAL '99' YEAR")
-        .columnType("INTERVAL YEAR NOT NULL");
-
-    // explicit precision equal to default
-    expr("INTERVAL '1' YEAR(2)")
-        .columnType("INTERVAL YEAR(2) NOT NULL");
-    expr("INTERVAL '99' YEAR(2)")
-        .columnType("INTERVAL YEAR(2) NOT NULL");
-
-    // max precision
-    expr("INTERVAL '2147483647' YEAR(10)")
-        .columnType("INTERVAL YEAR(10) NOT NULL");
-
-    // min precision
-    expr("INTERVAL '0' YEAR(1)")
-        .columnType("INTERVAL YEAR(1) NOT NULL");
-
-    // alternate precision
-    expr("INTERVAL '1234' YEAR(4)")
-        .columnType("INTERVAL YEAR(4) NOT NULL");
-
-    // sign
-    expr("INTERVAL '+1' YEAR")
-        .columnType("INTERVAL YEAR NOT NULL");
-    expr("INTERVAL '-1' YEAR")
-        .columnType("INTERVAL YEAR NOT NULL");
-    expr("INTERVAL +'1' YEAR")
-        .columnType("INTERVAL YEAR NOT NULL");
-    expr("INTERVAL +'+1' YEAR")
-        .columnType("INTERVAL YEAR NOT NULL");
-    expr("INTERVAL +'-1' YEAR")
-        .columnType("INTERVAL YEAR NOT NULL");
-    expr("INTERVAL -'1' YEAR")
-        .columnType("INTERVAL YEAR NOT NULL");
-    expr("INTERVAL -'+1' YEAR")
-        .columnType("INTERVAL YEAR NOT NULL");
-    expr("INTERVAL -'-1' YEAR")
-        .columnType("INTERVAL YEAR NOT NULL");
-  }
-
-  /**
-   * Runs tests for INTERVAL... YEAR TO MONTH that should pass both parser and
-   * validator. A substantially identical set of tests exists in
-   * SqlParserTest, and any changes here should be synchronized there.
-   * Similarly, any changes to tests here should be echoed appropriately to
-   * each of the other 12 subTestIntervalXXXPositive() tests.
-   */
-  void subTestIntervalYearToMonthPositive() {
-    // default precision
-    expr("INTERVAL '1-2' YEAR TO MONTH")
-        .columnType("INTERVAL YEAR TO MONTH NOT NULL");
-    expr("INTERVAL '99-11' YEAR TO MONTH")
-        .columnType("INTERVAL YEAR TO MONTH NOT NULL");
-    expr("INTERVAL '99-0' YEAR TO MONTH")
-        .columnType("INTERVAL YEAR TO MONTH NOT NULL");
-
-    // explicit precision equal to default
-    expr("INTERVAL '1-2' YEAR(2) TO MONTH")
-        .columnType("INTERVAL YEAR(2) TO MONTH NOT NULL");
-    expr("INTERVAL '99-11' YEAR(2) TO MONTH")
-        .columnType("INTERVAL YEAR(2) TO MONTH NOT NULL");
-    expr("INTERVAL '99-0' YEAR(2) TO MONTH")
-        .columnType("INTERVAL YEAR(2) TO MONTH NOT NULL");
-
-    // max precision
-    expr("INTERVAL '2147483647-11' YEAR(10) TO MONTH")
-        .columnType("INTERVAL YEAR(10) TO MONTH NOT NULL");
-
-    // min precision
-    expr("INTERVAL '0-0' YEAR(1) TO MONTH")
-        .columnType("INTERVAL YEAR(1) TO MONTH NOT NULL");
-
-    // alternate precision
-    expr("INTERVAL '2006-2' YEAR(4) TO MONTH")
-        .columnType("INTERVAL YEAR(4) TO MONTH NOT NULL");
-
-    // sign
-    expr("INTERVAL '-1-2' YEAR TO MONTH")
-        .columnType("INTERVAL YEAR TO MONTH NOT NULL");
-    expr("INTERVAL '+1-2' YEAR TO MONTH")
-        .columnType("INTERVAL YEAR TO MONTH NOT NULL");
-    expr("INTERVAL +'1-2' YEAR TO MONTH")
-        .columnType("INTERVAL YEAR TO MONTH NOT NULL");
-    expr("INTERVAL +'-1-2' YEAR TO MONTH")
-        .columnType("INTERVAL YEAR TO MONTH NOT NULL");
-    expr("INTERVAL +'+1-2' YEAR TO MONTH")
-        .columnType("INTERVAL YEAR TO MONTH NOT NULL");
-    expr("INTERVAL -'1-2' YEAR TO MONTH")
-        .columnType("INTERVAL YEAR TO MONTH NOT NULL");
-    expr("INTERVAL -'-1-2' YEAR TO MONTH")
-        .columnType("INTERVAL YEAR TO MONTH NOT NULL");
-    expr("INTERVAL -'+1-2' YEAR TO MONTH")
-        .columnType("INTERVAL YEAR TO MONTH NOT NULL");
-  }
-
-  /**
-   * Runs tests for INTERVAL... MONTH that should pass both parser and
-   * validator. A substantially identical set of tests exists in
-   * SqlParserTest, and any changes here should be synchronized there.
-   * Similarly, any changes to tests here should be echoed appropriately to
-   * each of the other 12 subTestIntervalXXXPositive() tests.
-   */
-  void subTestIntervalMonthPositive() {
-    // default precision
-    expr("INTERVAL '1' MONTH")
-        .columnType("INTERVAL MONTH NOT NULL");
-    expr("INTERVAL '99' MONTH")
-        .columnType("INTERVAL MONTH NOT NULL");
-
-    // explicit precision equal to default
-    expr("INTERVAL '1' MONTH(2)")
-        .columnType("INTERVAL MONTH(2) NOT NULL");
-    expr("INTERVAL '99' MONTH(2)")
-        .columnType("INTERVAL MONTH(2) NOT NULL");
-
-    // max precision
-    expr("INTERVAL '2147483647' MONTH(10)")
-        .columnType("INTERVAL MONTH(10) NOT NULL");
-
-    // min precision
-    expr("INTERVAL '0' MONTH(1)")
-        .columnType("INTERVAL MONTH(1) NOT NULL");
-
-    // alternate precision
-    expr("INTERVAL '1234' MONTH(4)")
-        .columnType("INTERVAL MONTH(4) NOT NULL");
-
-    // sign
-    expr("INTERVAL '+1' MONTH")
-        .columnType("INTERVAL MONTH NOT NULL");
-    expr("INTERVAL '-1' MONTH")
-        .columnType("INTERVAL MONTH NOT NULL");
-    expr("INTERVAL +'1' MONTH")
-        .columnType("INTERVAL MONTH NOT NULL");
-    expr("INTERVAL +'+1' MONTH")
-        .columnType("INTERVAL MONTH NOT NULL");
-    expr("INTERVAL +'-1' MONTH")
-        .columnType("INTERVAL MONTH NOT NULL");
-    expr("INTERVAL -'1' MONTH")
-        .columnType("INTERVAL MONTH NOT NULL");
-    expr("INTERVAL -'+1' MONTH")
-        .columnType("INTERVAL MONTH NOT NULL");
-    expr("INTERVAL -'-1' MONTH")
-        .columnType("INTERVAL MONTH NOT NULL");
-  }
-
-  /**
-   * Runs tests for INTERVAL... DAY that should pass both parser and
-   * validator. A substantially identical set of tests exists in
-   * SqlParserTest, and any changes here should be synchronized there.
-   * Similarly, any changes to tests here should be echoed appropriately to
-   * each of the other 12 subTestIntervalXXXPositive() tests.
-   */
-  void subTestIntervalDayPositive() {
-    // default precision
-    expr("INTERVAL '1' DAY")
-        .columnType("INTERVAL DAY NOT NULL");
-    expr("INTERVAL '99' DAY")
-        .columnType("INTERVAL DAY NOT NULL");
-
-    // explicit precision equal to default
-    expr("INTERVAL '1' DAY(2)")
-        .columnType("INTERVAL DAY(2) NOT NULL");
-    expr("INTERVAL '99' DAY(2)")
-        .columnType("INTERVAL DAY(2) NOT NULL");
-
-    // max precision
-    expr("INTERVAL '2147483647' DAY(10)")
-        .columnType("INTERVAL DAY(10) NOT NULL");
-
-    // min precision
-    expr("INTERVAL '0' DAY(1)")
-        .columnType("INTERVAL DAY(1) NOT NULL");
-
-    // alternate precision
-    expr("INTERVAL '1234' DAY(4)")
-        .columnType("INTERVAL DAY(4) NOT NULL");
-
-    // sign
-    expr("INTERVAL '+1' DAY")
-        .columnType("INTERVAL DAY NOT NULL");
-    expr("INTERVAL '-1' DAY")
-        .columnType("INTERVAL DAY NOT NULL");
-    expr("INTERVAL +'1' DAY")
-        .columnType("INTERVAL DAY NOT NULL");
-    expr("INTERVAL +'+1' DAY")
-        .columnType("INTERVAL DAY NOT NULL");
-    expr("INTERVAL +'-1' DAY")
-        .columnType("INTERVAL DAY NOT NULL");
-    expr("INTERVAL -'1' DAY")
-        .columnType("INTERVAL DAY NOT NULL");
-    expr("INTERVAL -'+1' DAY")
-        .columnType("INTERVAL DAY NOT NULL");
-    expr("INTERVAL -'-1' DAY")
-        .columnType("INTERVAL DAY NOT NULL");
-  }
-
-  void subTestIntervalDayToHourPositive() {
-    // default precision
-    expr("INTERVAL '1 2' DAY TO HOUR")
-        .columnType("INTERVAL DAY TO HOUR NOT NULL");
-    expr("INTERVAL '99 23' DAY TO HOUR")
-        .columnType("INTERVAL DAY TO HOUR NOT NULL");
-    expr("INTERVAL '99 0' DAY TO HOUR")
-        .columnType("INTERVAL DAY TO HOUR NOT NULL");
-
-    // explicit precision equal to default
-    expr("INTERVAL '1 2' DAY(2) TO HOUR")
-        .columnType("INTERVAL DAY(2) TO HOUR NOT NULL");
-    expr("INTERVAL '99 23' DAY(2) TO HOUR")
-        .columnType("INTERVAL DAY(2) TO HOUR NOT NULL");
-    expr("INTERVAL '99 0' DAY(2) TO HOUR")
-        .columnType("INTERVAL DAY(2) TO HOUR NOT NULL");
-
-    // max precision
-    expr("INTERVAL '2147483647 23' DAY(10) TO HOUR")
-        .columnType("INTERVAL DAY(10) TO HOUR NOT NULL");
-
-    // min precision
-    expr("INTERVAL '0 0' DAY(1) TO HOUR")
-        .columnType("INTERVAL DAY(1) TO HOUR NOT NULL");
-
-    // alternate precision
-    expr("INTERVAL '2345 2' DAY(4) TO HOUR")
-        .columnType("INTERVAL DAY(4) TO HOUR NOT NULL");
-
-    // sign
-    expr("INTERVAL '-1 2' DAY TO HOUR")
-        .columnType("INTERVAL DAY TO HOUR NOT NULL");
-    expr("INTERVAL '+1 2' DAY TO HOUR")
-        .columnType("INTERVAL DAY TO HOUR NOT NULL");
-    expr("INTERVAL +'1 2' DAY TO HOUR")
-        .columnType("INTERVAL DAY TO HOUR NOT NULL");
-    expr("INTERVAL +'-1 2' DAY TO HOUR")
-        .columnType("INTERVAL DAY TO HOUR NOT NULL");
-    expr("INTERVAL +'+1 2' DAY TO HOUR")
-        .columnType("INTERVAL DAY TO HOUR NOT NULL");
-    expr("INTERVAL -'1 2' DAY TO HOUR")
-        .columnType("INTERVAL DAY TO HOUR NOT NULL");
-    expr("INTERVAL -'-1 2' DAY TO HOUR")
-        .columnType("INTERVAL DAY TO HOUR NOT NULL");
-    expr("INTERVAL -'+1 2' DAY TO HOUR")
-        .columnType("INTERVAL DAY TO HOUR NOT NULL");
-  }
-
-  /**
-   * Runs tests for INTERVAL... DAY TO MINUTE that should pass both parser and
-   * validator. A substantially identical set of tests exists in
-   * SqlParserTest, and any changes here should be synchronized there.
-   * Similarly, any changes to tests here should be echoed appropriately to
-   * each of the other 12 subTestIntervalXXXPositive() tests.
-   */
-  void subTestIntervalDayToMinutePositive() {
-    // default precision
-    expr("INTERVAL '1 2:3' DAY TO MINUTE")
-        .columnType("INTERVAL DAY TO MINUTE NOT NULL");
-    expr("INTERVAL '99 23:59' DAY TO MINUTE")
-        .columnType("INTERVAL DAY TO MINUTE NOT NULL");
-    expr("INTERVAL '99 0:0' DAY TO MINUTE")
-        .columnType("INTERVAL DAY TO MINUTE NOT NULL");
-
-    // explicit precision equal to default
-    expr("INTERVAL '1 2:3' DAY(2) TO MINUTE")
-        .columnType("INTERVAL DAY(2) TO MINUTE NOT NULL");
-    expr("INTERVAL '99 23:59' DAY(2) TO MINUTE")
-        .columnType("INTERVAL DAY(2) TO MINUTE NOT NULL");
-    expr("INTERVAL '99 0:0' DAY(2) TO MINUTE")
-        .columnType("INTERVAL DAY(2) TO MINUTE NOT NULL");
-
-    // max precision
-    expr("INTERVAL '2147483647 23:59' DAY(10) TO MINUTE")
-        .columnType("INTERVAL DAY(10) TO MINUTE NOT NULL");
-
-    // min precision
-    expr("INTERVAL '0 0:0' DAY(1) TO MINUTE")
-        .columnType("INTERVAL DAY(1) TO MINUTE NOT NULL");
-
-    // alternate precision
-    expr("INTERVAL '2345 6:7' DAY(4) TO MINUTE")
-        .columnType("INTERVAL DAY(4) TO MINUTE NOT NULL");
-
-    // sign
-    expr("INTERVAL '-1 2:3' DAY TO MINUTE")
-        .columnType("INTERVAL DAY TO MINUTE NOT NULL");
-    expr("INTERVAL '+1 2:3' DAY TO MINUTE")
-        .columnType("INTERVAL DAY TO MINUTE NOT NULL");
-    expr("INTERVAL +'1 2:3' DAY TO MINUTE")
-        .columnType("INTERVAL DAY TO MINUTE NOT NULL");
-    expr("INTERVAL +'-1 2:3' DAY TO MINUTE")
-        .columnType("INTERVAL DAY TO MINUTE NOT NULL");
-    expr("INTERVAL +'+1 2:3' DAY TO MINUTE")
-        .columnType("INTERVAL DAY TO MINUTE NOT NULL");
-    expr("INTERVAL -'1 2:3' DAY TO MINUTE")
-        .columnType("INTERVAL DAY TO MINUTE NOT NULL");
-    expr("INTERVAL -'-1 2:3' DAY TO MINUTE")
-        .columnType("INTERVAL DAY TO MINUTE NOT NULL");
-    expr("INTERVAL -'+1 2:3' DAY TO MINUTE")
-        .columnType("INTERVAL DAY TO MINUTE NOT NULL");
-  }
-
-  /**
-   * Runs tests for INTERVAL... DAY TO SECOND that should pass both parser and
-   * validator. A substantially identical set of tests exists in
-   * SqlParserTest, and any changes here should be synchronized there.
-   * Similarly, any changes to tests here should be echoed appropriately to
-   * each of the other 12 subTestIntervalXXXPositive() tests.
-   */
-  void subTestIntervalDayToSecondPositive() {
-    // default precision
-    expr("INTERVAL '1 2:3:4' DAY TO SECOND")
-        .columnType("INTERVAL DAY TO SECOND NOT NULL");
-    expr("INTERVAL '99 23:59:59' DAY TO SECOND")
-        .columnType("INTERVAL DAY TO SECOND NOT NULL");
-    expr("INTERVAL '99 0:0:0' DAY TO SECOND")
-        .columnType("INTERVAL DAY TO SECOND NOT NULL");
-    expr("INTERVAL '99 23:59:59.999999' DAY TO SECOND")
-        .columnType("INTERVAL DAY TO SECOND NOT NULL");
-    expr("INTERVAL '99 0:0:0.0' DAY TO SECOND")
-        .columnType("INTERVAL DAY TO SECOND NOT NULL");
-
-    // explicit precision equal to default
-    expr("INTERVAL '1 2:3:4' DAY(2) TO SECOND")
-        .columnType("INTERVAL DAY(2) TO SECOND NOT NULL");
-    expr("INTERVAL '99 23:59:59' DAY(2) TO SECOND")
-        .columnType("INTERVAL DAY(2) TO SECOND NOT NULL");
-    expr("INTERVAL '99 0:0:0' DAY(2) TO SECOND")
-        .columnType("INTERVAL DAY(2) TO SECOND NOT NULL");
-    expr("INTERVAL '99 23:59:59.999999' DAY TO SECOND(6)")
-        .columnType("INTERVAL DAY TO SECOND(6) NOT NULL");
-    expr("INTERVAL '99 0:0:0.0' DAY TO SECOND(6)")
-        .columnType("INTERVAL DAY TO SECOND(6) NOT NULL");
-
-    // max precision
-    expr("INTERVAL '2147483647 23:59:59' DAY(10) TO SECOND")
-        .columnType("INTERVAL DAY(10) TO SECOND NOT NULL");
-    expr("INTERVAL '2147483647 23:59:59.999999999' DAY(10) TO SECOND(9)")
-        .columnType("INTERVAL DAY(10) TO SECOND(9) NOT NULL");
-
-    // min precision
-    expr("INTERVAL '0 0:0:0' DAY(1) TO SECOND")
-        .columnType("INTERVAL DAY(1) TO SECOND NOT NULL");
-    expr("INTERVAL '0 0:0:0.0' DAY(1) TO SECOND(1)")
-        .columnType("INTERVAL DAY(1) TO SECOND(1) NOT NULL");
-
-    // alternate precision
-    expr("INTERVAL '2345 6:7:8' DAY(4) TO SECOND")
-        .columnType("INTERVAL DAY(4) TO SECOND NOT NULL");
-    expr("INTERVAL '2345 6:7:8.9012' DAY(4) TO SECOND(4)")
-        .columnType("INTERVAL DAY(4) TO SECOND(4) NOT NULL");
-
-    // sign
-    expr("INTERVAL '-1 2:3:4' DAY TO SECOND")
-        .columnType("INTERVAL DAY TO SECOND NOT NULL");
-    expr("INTERVAL '+1 2:3:4' DAY TO SECOND")
-        .columnType("INTERVAL DAY TO SECOND NOT NULL");
-    expr("INTERVAL +'1 2:3:4' DAY TO SECOND")
-        .columnType("INTERVAL DAY TO SECOND NOT NULL");
-    expr("INTERVAL +'-1 2:3:4' DAY TO SECOND")
-        .columnType("INTERVAL DAY TO SECOND NOT NULL");
-    expr("INTERVAL +'+1 2:3:4' DAY TO SECOND")
-        .columnType("INTERVAL DAY TO SECOND NOT NULL");
-    expr("INTERVAL -'1 2:3:4' DAY TO SECOND")
-        .columnType("INTERVAL DAY TO SECOND NOT NULL");
-    expr("INTERVAL -'-1 2:3:4' DAY TO SECOND")
-        .columnType("INTERVAL DAY TO SECOND NOT NULL");
-    expr("INTERVAL -'+1 2:3:4' DAY TO SECOND")
-        .columnType("INTERVAL DAY TO SECOND NOT NULL");
-  }
-
-  /**
-   * Runs tests for INTERVAL... HOUR that should pass both parser and
-   * validator. A substantially identical set of tests exists in
-   * SqlParserTest, and any changes here should be synchronized there.
-   * Similarly, any changes to tests here should be echoed appropriately to
-   * each of the other 12 subTestIntervalXXXPositive() tests.
-   */
-  void subTestIntervalHourPositive() {
-    // default precision
-    expr("INTERVAL '1' HOUR")
-        .columnType("INTERVAL HOUR NOT NULL");
-    expr("INTERVAL '99' HOUR")
-        .columnType("INTERVAL HOUR NOT NULL");
-
-    // explicit precision equal to default
-    expr("INTERVAL '1' HOUR(2)")
-        .columnType("INTERVAL HOUR(2) NOT NULL");
-    expr("INTERVAL '99' HOUR(2)")
-        .columnType("INTERVAL HOUR(2) NOT NULL");
-
-    // max precision
-    expr("INTERVAL '2147483647' HOUR(10)")
-        .columnType("INTERVAL HOUR(10) NOT NULL");
-
-    // min precision
-    expr("INTERVAL '0' HOUR(1)")
-        .columnType("INTERVAL HOUR(1) NOT NULL");
-
-    // alternate precision
-    expr("INTERVAL '1234' HOUR(4)")
-        .columnType("INTERVAL HOUR(4) NOT NULL");
-
-    // sign
-    expr("INTERVAL '+1' HOUR")
-        .columnType("INTERVAL HOUR NOT NULL");
-    expr("INTERVAL '-1' HOUR")
-        .columnType("INTERVAL HOUR NOT NULL");
-    expr("INTERVAL +'1' HOUR")
-        .columnType("INTERVAL HOUR NOT NULL");
-    expr("INTERVAL +'+1' HOUR")
-        .columnType("INTERVAL HOUR NOT NULL");
-    expr("INTERVAL +'-1' HOUR")
-        .columnType("INTERVAL HOUR NOT NULL");
-    expr("INTERVAL -'1' HOUR")
-        .columnType("INTERVAL HOUR NOT NULL");
-    expr("INTERVAL -'+1' HOUR")
-        .columnType("INTERVAL HOUR NOT NULL");
-    expr("INTERVAL -'-1' HOUR")
-        .columnType("INTERVAL HOUR NOT NULL");
-  }
-
-  /**
-   * Runs tests for INTERVAL... HOUR TO MINUTE that should pass both parser
-   * and validator. A substantially identical set of tests exists in
-   * SqlParserTest, and any changes here should be synchronized there.
-   * Similarly, any changes to tests here should be echoed appropriately to
-   * each of the other 12 subTestIntervalXXXPositive() tests.
-   */
-  void subTestIntervalHourToMinutePositive() {
-    // default precision
-    expr("INTERVAL '2:3' HOUR TO MINUTE")
-        .columnType("INTERVAL HOUR TO MINUTE NOT NULL");
-    expr("INTERVAL '23:59' HOUR TO MINUTE")
-        .columnType("INTERVAL HOUR TO MINUTE NOT NULL");
-    expr("INTERVAL '99:0' HOUR TO MINUTE")
-        .columnType("INTERVAL HOUR TO MINUTE NOT NULL");
-
-    // explicit precision equal to default
-    expr("INTERVAL '2:3' HOUR(2) TO MINUTE")
-        .columnType("INTERVAL HOUR(2) TO MINUTE NOT NULL");
-    expr("INTERVAL '23:59' HOUR(2) TO MINUTE")
-        .columnType("INTERVAL HOUR(2) TO MINUTE NOT NULL");
-    expr("INTERVAL '99:0' HOUR(2) TO MINUTE")
-        .columnType("INTERVAL HOUR(2) TO MINUTE NOT NULL");
-
-    // max precision
-    expr("INTERVAL '2147483647:59' HOUR(10) TO MINUTE")
-        .columnType("INTERVAL HOUR(10) TO MINUTE NOT NULL");
-
-    // min precision
-    expr("INTERVAL '0:0' HOUR(1) TO MINUTE")
-        .columnType("INTERVAL HOUR(1) TO MINUTE NOT NULL");
-
-    // alternate precision
-    expr("INTERVAL '2345:7' HOUR(4) TO MINUTE")
-        .columnType("INTERVAL HOUR(4) TO MINUTE NOT NULL");
-
-    // sign
-    expr("INTERVAL '-1:3' HOUR TO MINUTE")
-        .columnType("INTERVAL HOUR TO MINUTE NOT NULL");
-    expr("INTERVAL '+1:3' HOUR TO MINUTE")
-        .columnType("INTERVAL HOUR TO MINUTE NOT NULL");
-    expr("INTERVAL +'2:3' HOUR TO MINUTE")
-        .columnType("INTERVAL HOUR TO MINUTE NOT NULL");
-    expr("INTERVAL +'-2:3' HOUR TO MINUTE")
-        .columnType("INTERVAL HOUR TO MINUTE NOT NULL");
-    expr("INTERVAL +'+2:3' HOUR TO MINUTE")
-        .columnType("INTERVAL HOUR TO MINUTE NOT NULL");
-    expr("INTERVAL -'2:3' HOUR TO MINUTE")
-        .columnType("INTERVAL HOUR TO MINUTE NOT NULL");
-    expr("INTERVAL -'-2:3' HOUR TO MINUTE")
-        .columnType("INTERVAL HOUR TO MINUTE NOT NULL");
-    expr("INTERVAL -'+2:3' HOUR TO MINUTE")
-        .columnType("INTERVAL HOUR TO MINUTE NOT NULL");
-  }
-
-  /**
-   * Runs tests for INTERVAL... HOUR TO SECOND that should pass both parser
-   * and validator. A substantially identical set of tests exists in
-   * SqlParserTest, and any changes here should be synchronized there.
-   * Similarly, any changes to tests here should be echoed appropriately to
-   * each of the other 12 subTestIntervalXXXPositive() tests.
-   */
-  void subTestIntervalHourToSecondPositive() {
-    // default precision
-    expr("INTERVAL '2:3:4' HOUR TO SECOND")
-        .columnType("INTERVAL HOUR TO SECOND NOT NULL");
-    expr("INTERVAL '23:59:59' HOUR TO SECOND")
-        .columnType("INTERVAL HOUR TO SECOND NOT NULL");
-    expr("INTERVAL '99:0:0' HOUR TO SECOND")
-        .columnType("INTERVAL HOUR TO SECOND NOT NULL");
-    expr("INTERVAL '23:59:59.999999' HOUR TO SECOND")
-        .columnType("INTERVAL HOUR TO SECOND NOT NULL");
-    expr("INTERVAL '99:0:0.0' HOUR TO SECOND")
-        .columnType("INTERVAL HOUR TO SECOND NOT NULL");
-
-    // explicit precision equal to default
-    expr("INTERVAL '2:3:4' HOUR(2) TO SECOND")
-        .columnType("INTERVAL HOUR(2) TO SECOND NOT NULL");
-    expr("INTERVAL '99:59:59' HOUR(2) TO SECOND")
-        .columnType("INTERVAL HOUR(2) TO SECOND NOT NULL");
-    expr("INTERVAL '99:0:0' HOUR(2) TO SECOND")
-        .columnType("INTERVAL HOUR(2) TO SECOND NOT NULL");
-    expr("INTERVAL '99:59:59.999999' HOUR TO SECOND(6)")
-        .columnType("INTERVAL HOUR TO SECOND(6) NOT NULL");
-    expr("INTERVAL '99:0:0.0' HOUR TO SECOND(6)")
-        .columnType("INTERVAL HOUR TO SECOND(6) NOT NULL");
-
-    // max precision
-    expr("INTERVAL '2147483647:59:59' HOUR(10) TO SECOND")
-        .columnType("INTERVAL HOUR(10) TO SECOND NOT NULL");
-    expr("INTERVAL '2147483647:59:59.999999999' HOUR(10) TO SECOND(9)")
-        .columnType("INTERVAL HOUR(10) TO SECOND(9) NOT NULL");
-
-    // min precision
-    expr("INTERVAL '0:0:0' HOUR(1) TO SECOND")
-        .columnType("INTERVAL HOUR(1) TO SECOND NOT NULL");
-    expr("INTERVAL '0:0:0.0' HOUR(1) TO SECOND(1)")
-        .columnType("INTERVAL HOUR(1) TO SECOND(1) NOT NULL");
-
-    // alternate precision
-    expr("INTERVAL '2345:7:8' HOUR(4) TO SECOND")
-        .columnType("INTERVAL HOUR(4) TO SECOND NOT NULL");
-    expr("INTERVAL '2345:7:8.9012' HOUR(4) TO SECOND(4)")
-        .columnType("INTERVAL HOUR(4) TO SECOND(4) NOT NULL");
-
-    // sign
-    expr("INTERVAL '-2:3:4' HOUR TO SECOND")
-        .columnType("INTERVAL HOUR TO SECOND NOT NULL");
-    expr("INTERVAL '+2:3:4' HOUR TO SECOND")
-        .columnType("INTERVAL HOUR TO SECOND NOT NULL");
-    expr("INTERVAL +'2:3:4' HOUR TO SECOND")
-        .columnType("INTERVAL HOUR TO SECOND NOT NULL");
-    expr("INTERVAL +'-2:3:4' HOUR TO SECOND")
-        .columnType("INTERVAL HOUR TO SECOND NOT NULL");
-    expr("INTERVAL +'+2:3:4' HOUR TO SECOND")
-        .columnType("INTERVAL HOUR TO SECOND NOT NULL");
-    expr("INTERVAL -'2:3:4' HOUR TO SECOND")
-        .columnType("INTERVAL HOUR TO SECOND NOT NULL");
-    expr("INTERVAL -'-2:3:4' HOUR TO SECOND")
-        .columnType("INTERVAL HOUR TO SECOND NOT NULL");
-    expr("INTERVAL -'+2:3:4' HOUR TO SECOND")
-        .columnType("INTERVAL HOUR TO SECOND NOT NULL");
-  }
-
-  /**
-   * Runs tests for INTERVAL... MINUTE that should pass both parser and
-   * validator. A substantially identical set of tests exists in
-   * SqlParserTest, and any changes here should be synchronized there.
-   * Similarly, any changes to tests here should be echoed appropriately to
-   * each of the other 12 subTestIntervalXXXPositive() tests.
-   */
-  void subTestIntervalMinutePositive() {
-    // default precision
-    expr("INTERVAL '1' MINUTE")
-        .columnType("INTERVAL MINUTE NOT NULL");
-    expr("INTERVAL '99' MINUTE")
-        .columnType("INTERVAL MINUTE NOT NULL");
-
-    // explicit precision equal to default
-    expr("INTERVAL '1' MINUTE(2)")
-        .columnType("INTERVAL MINUTE(2) NOT NULL");
-    expr("INTERVAL '99' MINUTE(2)")
-        .columnType("INTERVAL MINUTE(2) NOT NULL");
-
-    // max precision
-    expr("INTERVAL '2147483647' MINUTE(10)")
-        .columnType("INTERVAL MINUTE(10) NOT NULL");
-
-    // min precision
-    expr("INTERVAL '0' MINUTE(1)")
-        .columnType("INTERVAL MINUTE(1) NOT NULL");
-
-    // alternate precision
-    expr("INTERVAL '1234' MINUTE(4)")
-        .columnType("INTERVAL MINUTE(4) NOT NULL");
-
-    // sign
-    expr("INTERVAL '+1' MINUTE")
-        .columnType("INTERVAL MINUTE NOT NULL");
-    expr("INTERVAL '-1' MINUTE")
-        .columnType("INTERVAL MINUTE NOT NULL");
-    expr("INTERVAL +'1' MINUTE")
-        .columnType("INTERVAL MINUTE NOT NULL");
-    expr("INTERVAL +'+1' MINUTE")
-        .columnType("INTERVAL MINUTE NOT NULL");
-    expr("INTERVAL +'-1' MINUTE")
-        .columnType("INTERVAL MINUTE NOT NULL");
-    expr("INTERVAL -'1' MINUTE")
-        .columnType("INTERVAL MINUTE NOT NULL");
-    expr("INTERVAL -'+1' MINUTE")
-        .columnType("INTERVAL MINUTE NOT NULL");
-    expr("INTERVAL -'-1' MINUTE")
-        .columnType("INTERVAL MINUTE NOT NULL");
-  }
-
-  /**
-   * Runs tests for INTERVAL... MINUTE TO SECOND that should pass both parser
-   * and validator. A substantially identical set of tests exists in
-   * SqlParserTest, and any changes here should be synchronized there.
-   * Similarly, any changes to tests here should be echoed appropriately to
-   * each of the other 12 subTestIntervalXXXPositive() tests.
-   */
-  void subTestIntervalMinuteToSecondPositive() {
-    // default precision
-    expr("INTERVAL '2:4' MINUTE TO SECOND")
-        .columnType("INTERVAL MINUTE TO SECOND NOT NULL");
-    expr("INTERVAL '59:59' MINUTE TO SECOND")
-        .columnType("INTERVAL MINUTE TO SECOND NOT NULL");
-    expr("INTERVAL '99:0' MINUTE TO SECOND")
-        .columnType("INTERVAL MINUTE TO SECOND NOT NULL");
-    expr("INTERVAL '59:59.999999' MINUTE TO SECOND")
-        .columnType("INTERVAL MINUTE TO SECOND NOT NULL");
-    expr("INTERVAL '99:0.0' MINUTE TO SECOND")
-        .columnType("INTERVAL MINUTE TO SECOND NOT NULL");
-
-    // explicit precision equal to default
-    expr("INTERVAL '2:4' MINUTE(2) TO SECOND")
-        .columnType("INTERVAL MINUTE(2) TO SECOND NOT NULL");
-    expr("INTERVAL '99:59' MINUTE(2) TO SECOND")
-        .columnType("INTERVAL MINUTE(2) TO SECOND NOT NULL");
-    expr("INTERVAL '99:0' MINUTE(2) TO SECOND")
-        .columnType("INTERVAL MINUTE(2) TO SECOND NOT NULL");
-    expr("INTERVAL '99:59.999999' MINUTE TO SECOND(6)")
-        .columnType("INTERVAL MINUTE TO SECOND(6) NOT NULL");
-    expr("INTERVAL '99:0.0' MINUTE TO SECOND(6)")
-        .columnType("INTERVAL MINUTE TO SECOND(6) NOT NULL");
-
-    // max precision
-    expr("INTERVAL '2147483647:59' MINUTE(10) TO SECOND")
-        .columnType("INTERVAL MINUTE(10) TO SECOND NOT NULL");
-    expr("INTERVAL '2147483647:59.999999999' MINUTE(10) TO SECOND(9)")
-        .columnType("INTERVAL MINUTE(10) TO SECOND(9) NOT NULL");
-
-    // min precision
-    expr("INTERVAL '0:0' MINUTE(1) TO SECOND")
-        .columnType("INTERVAL MINUTE(1) TO SECOND NOT NULL");
-    expr("INTERVAL '0:0.0' MINUTE(1) TO SECOND(1)")
-        .columnType("INTERVAL MINUTE(1) TO SECOND(1) NOT NULL");
-
-    // alternate precision
-    expr("INTERVAL '2345:8' MINUTE(4) TO SECOND")
-        .columnType("INTERVAL MINUTE(4) TO SECOND NOT NULL");
-    expr("INTERVAL '2345:7.8901' MINUTE(4) TO SECOND(4)")
-        .columnType("INTERVAL MINUTE(4) TO SECOND(4) NOT NULL");
-
-    // sign
-    expr("INTERVAL '-3:4' MINUTE TO SECOND")
-        .columnType("INTERVAL MINUTE TO SECOND NOT NULL");
-    expr("INTERVAL '+3:4' MINUTE TO SECOND")
-        .columnType("INTERVAL MINUTE TO SECOND NOT NULL");
-    expr("INTERVAL +'3:4' MINUTE TO SECOND")
-        .columnType("INTERVAL MINUTE TO SECOND NOT NULL");
-    expr("INTERVAL +'-3:4' MINUTE TO SECOND")
-        .columnType("INTERVAL MINUTE TO SECOND NOT NULL");
-    expr("INTERVAL +'+3:4' MINUTE TO SECOND")
-        .columnType("INTERVAL MINUTE TO SECOND NOT NULL");
-    expr("INTERVAL -'3:4' MINUTE TO SECOND")
-        .columnType("INTERVAL MINUTE TO SECOND NOT NULL");
-    expr("INTERVAL -'-3:4' MINUTE TO SECOND")
-        .columnType("INTERVAL MINUTE TO SECOND NOT NULL");
-    expr("INTERVAL -'+3:4' MINUTE TO SECOND")
-        .columnType("INTERVAL MINUTE TO SECOND NOT NULL");
-  }
-
-  /**
-   * Runs tests for INTERVAL... SECOND that should pass both parser and
-   * validator. A substantially identical set of tests exists in
-   * SqlParserTest, and any changes here should be synchronized there.
-   * Similarly, any changes to tests here should be echoed appropriately to
-   * each of the other 12 subTestIntervalXXXPositive() tests.
-   */
-  void subTestIntervalSecondPositive() {
-    // default precision
-    expr("INTERVAL '1' SECOND")
-        .columnType("INTERVAL SECOND NOT NULL");
-    expr("INTERVAL '99' SECOND")
-        .columnType("INTERVAL SECOND NOT NULL");
-
-    // explicit precision equal to default
-    expr("INTERVAL '1' SECOND(2)")
-        .columnType("INTERVAL SECOND(2) NOT NULL");
-    expr("INTERVAL '99' SECOND(2)")
-        .columnType("INTERVAL SECOND(2) NOT NULL");
-    expr("INTERVAL '1' SECOND(2, 6)")
-        .columnType("INTERVAL SECOND(2, 6) NOT NULL");
-    expr("INTERVAL '99' SECOND(2, 6)")
-        .columnType("INTERVAL SECOND(2, 6) NOT NULL");
-
-    // max precision
-    expr("INTERVAL '2147483647' SECOND(10)")
-        .columnType("INTERVAL SECOND(10) NOT NULL");
-    expr("INTERVAL '2147483647.999999999' SECOND(10, 9)")
-        .columnType("INTERVAL SECOND(10, 9) NOT NULL");
-
-    // min precision
-    expr("INTERVAL '0' SECOND(1)")
-        .columnType("INTERVAL SECOND(1) NOT NULL");
-    expr("INTERVAL '0.0' SECOND(1, 1)")
-        .columnType("INTERVAL SECOND(1, 1) NOT NULL");
-
-    // alternate precision
-    expr("INTERVAL '1234' SECOND(4)")
-        .columnType("INTERVAL SECOND(4) NOT NULL");
-    expr("INTERVAL '1234.56789' SECOND(4, 5)")
-        .columnType("INTERVAL SECOND(4, 5) NOT NULL");
-
-    // sign
-    expr("INTERVAL '+1' SECOND")
-        .columnType("INTERVAL SECOND NOT NULL");
-    expr("INTERVAL '-1' SECOND")
-        .columnType("INTERVAL SECOND NOT NULL");
-    expr("INTERVAL +'1' SECOND")
-        .columnType("INTERVAL SECOND NOT NULL");
-    expr("INTERVAL +'+1' SECOND")
-        .columnType("INTERVAL SECOND NOT NULL");
-    expr("INTERVAL +'-1' SECOND")
-        .columnType("INTERVAL SECOND NOT NULL");
-    expr("INTERVAL -'1' SECOND")
-        .columnType("INTERVAL SECOND NOT NULL");
-    expr("INTERVAL -'+1' SECOND")
-        .columnType("INTERVAL SECOND NOT NULL");
-    expr("INTERVAL -'-1' SECOND")
-        .columnType("INTERVAL SECOND NOT NULL");
-  }
-
-  /**
-   * Runs tests for INTERVAL... YEAR that should pass parser but fail
-   * validator. A substantially identical set of tests exists in
-   * SqlParserTest, and any changes here should be synchronized there.
-   * Similarly, any changes to tests here should be echoed appropriately to
-   * each of the other 12 subTestIntervalXXXNegative() tests.
-   */
-  void subTestIntervalYearNegative() {
-    // Qualifier - field mismatches
-    wholeExpr("INTERVAL '-' YEAR")
-        .fails("Illegal interval literal format '-' for INTERVAL YEAR.*");
-    wholeExpr("INTERVAL '1-2' YEAR")
-        .fails("Illegal interval literal format '1-2' for INTERVAL YEAR.*");
-    wholeExpr("INTERVAL '1.2' YEAR")
-        .fails("Illegal interval literal format '1.2' for INTERVAL YEAR.*");
-    wholeExpr("INTERVAL '1 2' YEAR")
-        .fails("Illegal interval literal format '1 2' for INTERVAL YEAR.*");
-    wholeExpr("INTERVAL '1-2' YEAR(2)")
-        .fails("Illegal interval literal format '1-2' for INTERVAL YEAR\\(2\\)");
-    wholeExpr("INTERVAL 'bogus text' YEAR")
-        .fails("Illegal interval literal format 'bogus text' for INTERVAL YEAR.*");
-
-    // negative field values
-    wholeExpr("INTERVAL '--1' YEAR")
-        .fails("Illegal interval literal format '--1' for INTERVAL YEAR.*");
-
-    // Field value out of range
-    //  (default, explicit default, alt, neg alt, max, neg max)
-    wholeExpr("INTERVAL '100' YEAR")
-        .fails("Interval field value 100 exceeds precision of YEAR\\(2\\) field.*");
-    wholeExpr("INTERVAL '100' YEAR(2)")
-        .fails("Interval field value 100 exceeds precision of YEAR\\(2\\) field.*");
-    wholeExpr("INTERVAL '1000' YEAR(3)")
-        .fails("Interval field value 1,000 exceeds precision of YEAR\\(3\\) field.*");
-    wholeExpr("INTERVAL '-1000' YEAR(3)")
-        .fails("Interval field value -1,000 exceeds precision of YEAR\\(3\\) field.*");
-    wholeExpr("INTERVAL '2147483648' YEAR(10)")
-        .fails("Interval field value 2,147,483,648 exceeds precision of "
-            + "YEAR\\(10\\) field.*");
-    wholeExpr("INTERVAL '-2147483648' YEAR(10)")
-        .fails("Interval field value -2,147,483,648 exceeds precision of "
-            + "YEAR\\(10\\) field");
-
-    // precision > maximum
-    expr("INTERVAL '1' ^YEAR(11)^")
-        .fails("Interval leading field precision '11' out of range for "
-            + "INTERVAL YEAR\\(11\\)");
-
-    // precision < minimum allowed)
-    // note: parser will catch negative values, here we
-    // just need to check for 0
-    expr("INTERVAL '0' ^YEAR(0)^")
-        .fails("Interval leading field precision '0' out of range for "
-            + "INTERVAL YEAR\\(0\\)");
-  }
-
-  /**
-   * Runs tests for INTERVAL... YEAR TO MONTH that should pass parser but fail
-   * validator. A substantially identical set of tests exists in
-   * SqlParserTest, and any changes here should be synchronized there.
-   * Similarly, any changes to tests here should be echoed appropriately to
-   * each of the other 12 subTestIntervalXXXNegative() tests.
-   */
-  void subTestIntervalYearToMonthNegative() {
-    // Qualifier - field mismatches
-    wholeExpr("INTERVAL '-' YEAR TO MONTH")
-        .fails("Illegal interval literal format '-' for INTERVAL YEAR TO MONTH");
-    wholeExpr("INTERVAL '1' YEAR TO MONTH")
-        .fails("Illegal interval literal format '1' for INTERVAL YEAR TO MONTH");
-    wholeExpr("INTERVAL '1:2' YEAR TO MONTH")
-        .fails("Illegal interval literal format '1:2' for INTERVAL YEAR TO MONTH");
-    wholeExpr("INTERVAL '1.2' YEAR TO MONTH")
-        .fails("Illegal interval literal format '1.2' for INTERVAL YEAR TO MONTH");
-    wholeExpr("INTERVAL '1 2' YEAR TO MONTH")
-        .fails("Illegal interval literal format '1 2' for INTERVAL YEAR TO MONTH");
-    wholeExpr("INTERVAL '1:2' YEAR(2) TO MONTH")
-        .fails("Illegal interval literal format '1:2' for "
-            + "INTERVAL YEAR\\(2\\) TO MONTH");
-    wholeExpr("INTERVAL 'bogus text' YEAR TO MONTH")
-        .fails("Illegal interval literal format 'bogus text' for "
-            + "INTERVAL YEAR TO MONTH");
-
-    // negative field values
-    wholeExpr("INTERVAL '--1-2' YEAR TO MONTH")
-        .fails("Illegal interval literal format '--1-2' for "
-            + "INTERVAL YEAR TO MONTH");
-    wholeExpr("INTERVAL '1--2' YEAR TO MONTH")
-        .fails("Illegal interval literal format '1--2' for "
-            + "INTERVAL YEAR TO MONTH");
-
-    // Field value out of range
-    //  (default, explicit default, alt, neg alt, max, neg max)
-    //  plus >max value for mid/end fields
-    wholeExpr("INTERVAL '100-0' YEAR TO MONTH")
-        .fails("Interval field value 100 exceeds precision of YEAR\\(2\\) field.*");
-    wholeExpr("INTERVAL '100-0' YEAR(2) TO MONTH")
-        .fails("Interval field value 100 exceeds precision of YEAR\\(2\\) field.*");
-    wholeExpr("INTERVAL '1000-0' YEAR(3) TO MONTH")
-        .fails("Interval field value 1,000 exceeds precision of YEAR\\(3\\) field.*");
-    wholeExpr("INTERVAL '-1000-0' YEAR(3) TO MONTH")
-        .fails("Interval field value -1,000 exceeds precision of YEAR\\(3\\) field.*");
-    wholeExpr("INTERVAL '2147483648-0' YEAR(10) TO MONTH")
-        .fails("Interval field value 2,147,483,648 exceeds precision of YEAR\\(10\\) field.*");
-    wholeExpr("INTERVAL '-2147483648-0' YEAR(10) TO MONTH")
-        .fails("Interval field value -2,147,483,648 exceeds precision of YEAR\\(10\\) field.*");
-    wholeExpr("INTERVAL '1-12' YEAR TO MONTH")
-        .fails("Illegal interval literal format '1-12' for INTERVAL YEAR TO MONTH.*");
-
-    // precision > maximum
-    expr("INTERVAL '1-1' ^YEAR(11) TO MONTH^")
-        .fails("Interval leading field precision '11' out of range for "
-            + "INTERVAL YEAR\\(11\\) TO MONTH");
-
-    // precision < minimum allowed)
-    // note: parser will catch negative values, here we
-    // just need to check for 0
-    expr("INTERVAL '0-0' ^YEAR(0) TO MONTH^")
-        .fails("Interval leading field precision '0' out of range for "
-            + "INTERVAL YEAR\\(0\\) TO MONTH");
-  }
-
-  /**
-   * Runs tests for INTERVAL... MONTH that should pass parser but fail
-   * validator. A substantially identical set of tests exists in
-   * SqlParserTest, and any changes here should be synchronized there.
-   * Similarly, any changes to tests here should be echoed appropriately to
-   * each of the other 12 subTestIntervalXXXNegative() tests.
-   */
-  void subTestIntervalMonthNegative() {
-    // Qualifier - field mismatches
-    wholeExpr("INTERVAL '-' MONTH")
-        .fails("Illegal interval literal format '-' for INTERVAL MONTH.*");
-    wholeExpr("INTERVAL '1-2' MONTH")
-        .fails("Illegal interval literal format '1-2' for INTERVAL MONTH.*");
-    wholeExpr("INTERVAL '1.2' MONTH")
-        .fails("Illegal interval literal format '1.2' for INTERVAL MONTH.*");
-    wholeExpr("INTERVAL '1 2' MONTH")
-        .fails("Illegal interval literal format '1 2' for INTERVAL MONTH.*");
-    wholeExpr("INTERVAL '1-2' MONTH(2)")
-        .fails("Illegal interval literal format '1-2' for INTERVAL MONTH\\(2\\)");
-    wholeExpr("INTERVAL 'bogus text' MONTH")
-        .fails("Illegal interval literal format 'bogus text' for INTERVAL MONTH.*");
-
-    // negative field values
-    wholeExpr("INTERVAL '--1' MONTH")
-        .fails("Illegal interval literal format '--1' for INTERVAL MONTH.*");
-
-    // Field value out of range
-    //  (default, explicit default, alt, neg alt, max, neg max)
-    wholeExpr("INTERVAL '100' MONTH")
-        .fails("Interval field value 100 exceeds precision of MONTH\\(2\\) field.*");
-    wholeExpr("INTERVAL '100' MONTH(2)")
-        .fails("Interval field value 100 exceeds precision of MONTH\\(2\\) field.*");
-    wholeExpr("INTERVAL '1000' MONTH(3)")
-        .fails("Interval field value 1,000 exceeds precision of MONTH\\(3\\) field.*");
-    wholeExpr("INTERVAL '-1000' MONTH(3)")
-        .fails("Interval field value -1,000 exceeds precision of MONTH\\(3\\) field.*");
-    wholeExpr("INTERVAL '2147483648' MONTH(10)")
-        .fails("Interval field value 2,147,483,648 exceeds precision of MONTH\\(10\\) field.*");
-    wholeExpr("INTERVAL '-2147483648' MONTH(10)")
-        .fails("Interval field value -2,147,483,648 exceeds precision of MONTH\\(10\\) field.*");
-
-    // precision > maximum
-    expr("INTERVAL '1' ^MONTH(11)^")
-        .fails("Interval leading field precision '11' out of range for "
-            + "INTERVAL MONTH\\(11\\)");
-
-    // precision < minimum allowed)
-    // note: parser will catch negative values, here we
-    // just need to check for 0
-    expr("INTERVAL '0' ^MONTH(0)^")
-        .fails("Interval leading field precision '0' out of range for "
-            + "INTERVAL MONTH\\(0\\)");
-  }
-
-  /**
-   * Runs tests for INTERVAL... DAY that should pass parser but fail
-   * validator. A substantially identical set of tests exists in
-   * SqlParserTest, and any changes here should be synchronized there.
-   * Similarly, any changes to tests here should be echoed appropriately to
-   * each of the other 12 subTestIntervalXXXNegative() tests.
-   */
-  void subTestIntervalDayNegative() {
-    // Qualifier - field mismatches
-    wholeExpr("INTERVAL '-' DAY")
-        .fails("Illegal interval literal format '-' for INTERVAL DAY.*");
-    wholeExpr("INTERVAL '1-2' DAY")
-        .fails("Illegal interval literal format '1-2' for INTERVAL DAY.*");
-    wholeExpr("INTERVAL '1.2' DAY")
-        .fails("Illegal interval literal format '1.2' for INTERVAL DAY.*");
-    wholeExpr("INTERVAL '1 2' DAY")
-        .fails("Illegal interval literal format '1 2' for INTERVAL DAY.*");
-    wholeExpr("INTERVAL '1:2' DAY")
-        .fails("Illegal interval literal format '1:2' for INTERVAL DAY.*");
-    wholeExpr("INTERVAL '1-2' DAY(2)")
-        .fails("Illegal interval literal format '1-2' for INTERVAL DAY\\(2\\)");
-    wholeExpr("INTERVAL 'bogus text' DAY")
-        .fails("Illegal interval literal format 'bogus text' for INTERVAL DAY.*");
-
-    // negative field values
-    wholeExpr("INTERVAL '--1' DAY")
-        .fails("Illegal interval literal format '--1' for INTERVAL DAY.*");
-
-    // Field value out of range
-    //  (default, explicit default, alt, neg alt, max, neg max)
-    wholeExpr("INTERVAL '100' DAY")
-        .fails("Interval field value 100 exceeds precision of DAY\\(2\\) field.*");
-    wholeExpr("INTERVAL '100' DAY(2)")
-        .fails("Interval field value 100 exceeds precision of DAY\\(2\\) field.*");
-    wholeExpr("INTERVAL '1000' DAY(3)")
-        .fails("Interval field value 1,000 exceeds precision of DAY\\(3\\) field.*");
-    wholeExpr("INTERVAL '-1000' DAY(3)")
-        .fails("Interval field value -1,000 exceeds precision of DAY\\(3\\) field.*");
-    wholeExpr("INTERVAL '2147483648' DAY(10)")
-        .fails("Interval field value 2,147,483,648 exceeds precision of "
-            + "DAY\\(10\\) field.*");
-    wholeExpr("INTERVAL '-2147483648' DAY(10)")
-        .fails("Interval field value -2,147,483,648 exceeds precision of "
-            + "DAY\\(10\\) field.*");
-
-    // precision > maximum
-    expr("INTERVAL '1' ^DAY(11)^")
-        .fails("Interval leading field precision '11' out of range for "
-            + "INTERVAL DAY\\(11\\)");
-
-    // precision < minimum allowed)
-    // note: parser will catch negative values, here we
-    // just need to check for 0
-    expr("INTERVAL '0' ^DAY(0)^")
-        .fails("Interval leading field precision '0' out of range for "
-            + "INTERVAL DAY\\(0\\)");
-  }
-
-  /**
-   * Runs tests for INTERVAL... DAY TO HOUR that should pass parser but fail
-   * validator. A substantially identical set of tests exists in
-   * SqlParserTest, and any changes here should be synchronized there.
-   * Similarly, any changes to tests here should be echoed appropriately to
-   * each of the other 12 subTestIntervalXXXNegative() tests.
-   */
-  void subTestIntervalDayToHourNegative() {
-    // Qualifier - field mismatches
-    wholeExpr("INTERVAL '-' DAY TO HOUR")
-        .fails("Illegal interval literal format '-' for INTERVAL DAY TO HOUR");
-    wholeExpr("INTERVAL '1' DAY TO HOUR")
-        .fails("Illegal interval literal format '1' for INTERVAL DAY TO HOUR");
-    wholeExpr("INTERVAL '1:2' DAY TO HOUR")
-        .fails("Illegal interval literal format '1:2' for INTERVAL DAY TO HOUR");
-    wholeExpr("INTERVAL '1.2' DAY TO HOUR")
-        .fails("Illegal interval literal format '1.2' for INTERVAL DAY TO HOUR");
-    wholeExpr("INTERVAL '1 x' DAY TO HOUR")
-        .fails("Illegal interval literal format '1 x' for INTERVAL DAY TO HOUR");
-    wholeExpr("INTERVAL ' ' DAY TO HOUR")
-        .fails("Illegal interval literal format ' ' for INTERVAL DAY TO HOUR");
-    wholeExpr("INTERVAL '1:2' DAY(2) TO HOUR")
-        .fails("Illegal interval literal format '1:2' for "
-            + "INTERVAL DAY\\(2\\) TO HOUR");
-    wholeExpr("INTERVAL 'bogus text' DAY TO HOUR")
-        .fails("Illegal interval literal format 'bogus text' for "
-            + "INTERVAL DAY TO HOUR");
-
-    // negative field values
-    wholeExpr("INTERVAL '--1 1' DAY TO HOUR")
-        .fails("Illegal interval literal format '--1 1' for INTERVAL DAY TO HOUR");
-    wholeExpr("INTERVAL '1 -1' DAY TO HOUR")
-        .fails("Illegal interval literal format '1 -1' for INTERVAL DAY TO HOUR");
-
-    // Field value out of range
-    //  (default, explicit default, alt, neg alt, max, neg max)
-    //  plus >max value for mid/end fields
-    wholeExpr("INTERVAL '100 0' DAY TO HOUR")
-        .fails("Interval field value 100 exceeds precision of DAY\\(2\\) field.*");
-    wholeExpr("INTERVAL '100 0' DAY(2) TO HOUR")
-        .fails("Interval field value 100 exceeds precision of DAY\\(2\\) field.*");
-    wholeExpr("INTERVAL '1000 0' DAY(3) TO HOUR")
-        .fails("Interval field value 1,000 exceeds precision of DAY\\(3\\) field.*");
-    wholeExpr("INTERVAL '-1000 0' DAY(3) TO HOUR")
-        .fails("Interval field value -1,000 exceeds precision of DAY\\(3\\) field.*");
-    wholeExpr("INTERVAL '2147483648 0' DAY(10) TO HOUR")
-        .fails("Interval field value 2,147,483,648 exceeds precision of DAY\\(10\\) field.*");
-    wholeExpr("INTERVAL '-2147483648 0' DAY(10) TO HOUR")
-        .fails("Interval field value -2,147,483,648 exceeds precision of "
-            + "DAY\\(10\\) field.*");
-    wholeExpr("INTERVAL '1 24' DAY TO HOUR")
-        .fails("Illegal interval literal format '1 24' for INTERVAL DAY TO HOUR.*");
-
-    // precision > maximum
-    expr("INTERVAL '1 1' ^DAY(11) TO HOUR^")
-        .fails("Interval leading field precision '11' out of range for "
-            + "INTERVAL DAY\\(11\\) TO HOUR");
-
-    // precision < minimum allowed)
-    // note: parser will catch negative values, here we
-    // just need to check for 0
-    expr("INTERVAL '0 0' ^DAY(0) TO HOUR^")
-        .fails("Interval leading field precision '0' out of range for INTERVAL DAY\\(0\\) TO HOUR");
-  }
-
-  /**
-   * Runs tests for INTERVAL... DAY TO MINUTE that should pass parser but fail
-   * validator. A substantially identical set of tests exists in
-   * SqlParserTest, and any changes here should be synchronized there.
-   * Similarly, any changes to tests here should be echoed appropriately to
-   * each of the other 12 subTestIntervalXXXNegative() tests.
-   */
-  void subTestIntervalDayToMinuteNegative() {
-    // Qualifier - field mismatches
-    wholeExpr("INTERVAL ' :' DAY TO MINUTE")
-        .fails("Illegal interval literal format ' :' for INTERVAL DAY TO MINUTE");
-    wholeExpr("INTERVAL '1' DAY TO MINUTE")
-        .fails("Illegal interval literal format '1' for INTERVAL DAY TO MINUTE");
-    wholeExpr("INTERVAL '1 2' DAY TO MINUTE")
-        .fails("Illegal interval literal format '1 2' for INTERVAL DAY TO MINUTE");
-    wholeExpr("INTERVAL '1:2' DAY TO MINUTE")
-        .fails("Illegal interval literal format '1:2' for INTERVAL DAY TO MINUTE");
-    wholeExpr("INTERVAL '1.2' DAY TO MINUTE")
-        .fails("Illegal interval literal format '1.2' for INTERVAL DAY TO MINUTE");
-    wholeExpr("INTERVAL 'x 1:1' DAY TO MINUTE")
-        .fails("Illegal interval literal format 'x 1:1' for INTERVAL DAY TO MINUTE");
-    wholeExpr("INTERVAL '1 x:1' DAY TO MINUTE")
-        .fails("Illegal interval literal format '1 x:1' for INTERVAL DAY TO MINUTE");
-    wholeExpr("INTERVAL '1 1:x' DAY TO MINUTE")
-        .fails("Illegal interval literal format '1 1:x' for INTERVAL DAY TO MINUTE");
-    wholeExpr("INTERVAL '1 1:2:3' DAY TO MINUTE")
-        .fails("Illegal interval literal format '1 1:2:3' for INTERVAL DAY TO MINUTE");
-    wholeExpr("INTERVAL '1 1:1:1.2' DAY TO MINUTE")
-        .fails("Illegal interval literal format '1 1:1:1.2' for INTERVAL DAY TO MINUTE");
-    wholeExpr("INTERVAL '1 1:2:3' DAY(2) TO MINUTE")
-        .fails("Illegal interval literal format '1 1:2:3' for "
-            + "INTERVAL DAY\\(2\\) TO MINUTE");
-    wholeExpr("INTERVAL '1 1' DAY(2) TO MINUTE")
-        .fails("Illegal interval literal format '1 1' for "
-            + "INTERVAL DAY\\(2\\) TO MINUTE");
-    wholeExpr("INTERVAL 'bogus text' DAY TO MINUTE")
-        .fails("Illegal interval literal format 'bogus text' for "
-            + "INTERVAL DAY TO MINUTE");
-
-    // negative field values
-    wholeExpr("INTERVAL '--1 1:1' DAY TO MINUTE")
-        .fails("Illegal interval literal format '--1 1:1' for INTERVAL DAY TO MINUTE");
-    wholeExpr("INTERVAL '1 -1:1' DAY TO MINUTE")
-        .fails("Illegal interval literal format '1 -1:1' for INTERVAL DAY TO MINUTE");
-    wholeExpr("INTERVAL '1 1:-1' DAY TO MINUTE")
-        .fails("Illegal interval literal format '1 1:-1' for INTERVAL DAY TO MINUTE");
-
-    // Field value out of range
-    //  (default, explicit default, alt, neg alt, max, neg max)
-    //  plus >max value for mid/end fields
-    wholeExpr("INTERVAL '100 0:0' DAY TO MINUTE")
-        .fails("Interval field value 100 exceeds precision of DAY\\(2\\) field.*");
-    wholeExpr("INTERVAL '100 0:0' DAY(2) TO MINUTE")
-        .fails("Interval field value 100 exceeds precision of DAY\\(2\\) field.*");
-    wholeExpr("INTERVAL '1000 0:0' DAY(3) TO MINUTE")
-        .fails("Interval field value 1,000 exceeds precision of DAY\\(3\\) field.*");
-    wholeExpr("INTERVAL '-1000 0:0' DAY(3) TO MINUTE")
-        .fails("Interval field value -1,000 exceeds precision of DAY\\(3\\) field.*");
-    wholeExpr("INTERVAL '2147483648 0:0' DAY(10) TO MINUTE")
-        .fails("Interval field value 2,147,483,648 exceeds precision of "
-            + "DAY\\(10\\) field.*");
-    wholeExpr("INTERVAL '-2147483648 0:0' DAY(10) TO MINUTE")
-        .fails("Interval field value -2,147,483,648 exceeds precision of "
-            + "DAY\\(10\\) field.*");
-    wholeExpr("INTERVAL '1 24:1' DAY TO MINUTE")
-        .fails("Illegal interval literal format '1 24:1' for "
-            + "INTERVAL DAY TO MINUTE.*");
-    wholeExpr("INTERVAL '1 1:60' DAY TO MINUTE")
-        .fails("Illegal interval literal format '1 1:60' for INTERVAL DAY TO MINUTE.*");
-
-    // precision > maximum
-    expr("INTERVAL '1 1:1' ^DAY(11) TO MINUTE^")
-        .fails("Interval leading field precision '11' out of range for "
-            + "INTERVAL DAY\\(11\\) TO MINUTE");
-
-    // precision < minimum allowed)
-    // note: parser will catch negative values, here we
-    // just need to check for 0
-    expr("INTERVAL '0 0' ^DAY(0) TO MINUTE^")
-        .fails("Interval leading field precision '0' out of range for "
-            + "INTERVAL DAY\\(0\\) TO MINUTE");
-  }
-
-  /**
-   * Runs tests for INTERVAL... DAY TO SECOND that should pass parser but fail
-   * validator. A substantially identical set of tests exists in
-   * SqlParserTest, and any changes here should be synchronized there.
-   * Similarly, any changes to tests here should be echoed appropriately to
-   * each of the other 12 subTestIntervalXXXNegative() tests.
-   */
-  void subTestIntervalDayToSecondNegative() {
-    // Qualifier - field mismatches
-    wholeExpr("INTERVAL ' ::' DAY TO SECOND")
-        .fails("Illegal interval literal format ' ::' for INTERVAL DAY TO SECOND");
-    wholeExpr("INTERVAL ' ::.' DAY TO SECOND")
-        .fails("Illegal interval literal format ' ::\\.' for INTERVAL DAY TO SECOND");
-    wholeExpr("INTERVAL '1' DAY TO SECOND")
-        .fails("Illegal interval literal format '1' for INTERVAL DAY TO SECOND");
-    wholeExpr("INTERVAL '1 2' DAY TO SECOND")
-        .fails("Illegal interval literal format '1 2' for INTERVAL DAY TO SECOND");
-    wholeExpr("INTERVAL '1:2' DAY TO SECOND")
-        .fails("Illegal interval literal format '1:2' for "
-            + "INTERVAL DAY TO SECOND");
-    wholeExpr("INTERVAL '1.2' DAY TO SECOND")
-        .fails("Illegal interval literal format '1\\.2' for "
-            + "INTERVAL DAY TO SECOND");
-    wholeExpr("INTERVAL '1 1:2' DAY TO SECOND")
-        .fails("Illegal interval literal format '1 1:2' for "
-            + "INTERVAL DAY TO SECOND");
-    wholeExpr("INTERVAL '1 1:2:x' DAY TO SECOND")
-        .fails("Illegal interval literal format '1 1:2:x' for "
-            + "INTERVAL DAY TO SECOND");
-    wholeExpr("INTERVAL '1:2:3' DAY TO SECOND")
-        .fails("Illegal interval literal format '1:2:3' for "
-            + "INTERVAL DAY TO SECOND");
-    wholeExpr("INTERVAL '1:1:1.2' DAY TO SECOND")
-        .fails("Illegal interval literal format '1:1:1\\.2' for "
-            + "INTERVAL DAY TO SECOND");
-    wholeExpr("INTERVAL '1 1:2' DAY(2) TO SECOND")
-        .fails("Illegal interval literal format '1 1:2' for "
-            + "INTERVAL DAY\\(2\\) TO SECOND");
-    wholeExpr("INTERVAL '1 1' DAY(2) TO SECOND")
-        .fails("Illegal interval literal format '1 1' for "
-            + "INTERVAL DAY\\(2\\) TO SECOND");
-    wholeExpr("INTERVAL 'bogus text' DAY TO SECOND")
-        .fails("Illegal interval literal format 'bogus text' for "
-            + "INTERVAL DAY TO SECOND");
-    wholeExpr("INTERVAL '2345 6:7:8901' DAY TO SECOND(4)")
-        .fails("Illegal interval literal format '2345 6:7:8901' for "
-            + "INTERVAL DAY TO SECOND\\(4\\)");
-
-    // negative field values
-    wholeExpr("INTERVAL '--1 1:1:1' DAY TO SECOND")
-        .fails("Illegal interval literal format '--1 1:1:1' for "
-            + "INTERVAL DAY TO SECOND");
-    wholeExpr("INTERVAL '1 -1:1:1' DAY TO SECOND")
-        .fails("Illegal interval literal format '1 -1:1:1' for "
-            + "INTERVAL DAY TO SECOND");
-    wholeExpr("INTERVAL '1 1:-1:1' DAY TO SECOND")
-        .fails("Illegal interval literal format '1 1:-1:1' for "
-            + "INTERVAL DAY TO SECOND");
-    wholeExpr("INTERVAL '1 1:1:-1' DAY TO SECOND")
-        .fails("Illegal interval literal format '1 1:1:-1' for "
-            + "INTERVAL DAY TO SECOND");
-    wholeExpr("INTERVAL '1 1:1:1.-1' DAY TO SECOND")
-        .fails("Illegal interval literal format '1 1:1:1.-1' for "
-            + "INTERVAL DAY TO SECOND");
-
-    // Field value out of range
-    //  (default, explicit default, alt, neg alt, max, neg max)
-    //  plus >max value for mid/end fields
-    wholeExpr("INTERVAL '100 0' DAY TO SECOND")
-        .fails("Illegal interval literal format '100 0' for "
-            + "INTERVAL DAY TO SECOND.*");
-    wholeExpr("INTERVAL '100 0' DAY(2) TO SECOND")
-        .fails("Illegal interval literal format '100 0' for "
-            + "INTERVAL DAY\\(2\\) TO SECOND.*");
-    wholeExpr("INTERVAL '1000 0' DAY(3) TO SECOND")
-        .fails("Illegal interval literal format '1000 0' for "
-            + "INTERVAL DAY\\(3\\) TO SECOND.*");
-    wholeExpr("INTERVAL '-1000 0' DAY(3) TO SECOND")
-        .fails("Illegal interval literal format '-1000 0' for "
-            + "INTERVAL DAY\\(3\\) TO SECOND.*");
-    wholeExpr("INTERVAL '2147483648 1:1:0' DAY(10) TO SECOND")
-        .fails("Interval field value 2,147,483,648 exceeds precision of "
-            + "DAY\\(10\\) field.*");
-    wholeExpr("INTERVAL '-2147483648 1:1:0' DAY(10) TO SECOND")
-        .fails("Interval field value -2,147,483,648 exceeds precision of "
-            + "DAY\\(10\\) field.*");
-    wholeExpr("INTERVAL '2147483648 0' DAY(10) TO SECOND")
-        .fails("Illegal interval literal format '2147483648 0' for "
-            + "INTERVAL DAY\\(10\\) TO SECOND.*");
-    wholeExpr("INTERVAL '-2147483648 0' DAY(10) TO SECOND")
-        .fails("Illegal interval literal format '-2147483648 0' for "
-            + "INTERVAL DAY\\(10\\) TO SECOND.*");
-    wholeExpr("INTERVAL '1 24:1:1' DAY TO SECOND")
-        .fails("Illegal interval literal format '1 24:1:1' for "
-            + "INTERVAL DAY TO SECOND.*");
-    wholeExpr("INTERVAL '1 1:60:1' DAY TO SECOND")
-        .fails("Illegal interval literal format '1 1:60:1' for "
-            + "INTERVAL DAY TO SECOND.*");
-    wholeExpr("INTERVAL '1 1:1:60' DAY TO SECOND")
-        .fails("Illegal interval literal format '1 1:1:60' for "
-            + "INTERVAL DAY TO SECOND.*");
-    wholeExpr("INTERVAL '1 1:1:1.0000001' DAY TO SECOND")
-        .fails("Illegal interval literal format '1 1:1:1\\.0000001' for "
-            + "INTERVAL DAY TO SECOND.*");
-    wholeExpr("INTERVAL '1 1:1:1.0001' DAY TO SECOND(3)")
-        .fails("Illegal interval literal format '1 1:1:1\\.0001' for "
-            + "INTERVAL DAY TO SECOND\\(3\\).*");
-
-    // precision > maximum
-    expr("INTERVAL '1 1' ^DAY(11) TO SECOND^")
-        .fails("Interval leading field precision '11' out of range for "
-            + "INTERVAL DAY\\(11\\) TO SECOND");
-    expr("INTERVAL '1 1' ^DAY TO SECOND(10)^")
-        .fails("Interval fractional second precision '10' out of range for "
-            + "INTERVAL DAY TO SECOND\\(10\\)");
-
-    // precision < minimum allowed)
-    // note: parser will catch negative values, here we
-    // just need to check for 0
-    expr("INTERVAL '0 0:0:0' ^DAY(0) TO SECOND^")
-        .fails("Interval leading field precision '0' out of range for "
-            + "INTERVAL DAY\\(0\\) TO SECOND");
-    expr("INTERVAL '0 0:0:0' ^DAY TO SECOND(0)^")
-        .fails("Interval fractional second precision '0' out of range for "
-            + "INTERVAL DAY TO SECOND\\(0\\)");
-  }
-
-  /**
-   * Runs tests for INTERVAL... HOUR that should pass parser but fail
-   * validator. A substantially identical set of tests exists in
-   * SqlParserTest, and any changes here should be synchronized there.
-   * Similarly, any changes to tests here should be echoed appropriately to
-   * each of the other 12 subTestIntervalXXXNegative() tests.
-   */
-  void subTestIntervalHourNegative() {
-    // Qualifier - field mismatches
-    wholeExpr("INTERVAL '-' HOUR")
-        .fails("Illegal interval literal format '-' for INTERVAL HOUR.*");
-    wholeExpr("INTERVAL '1-2' HOUR")
-        .fails("Illegal interval literal format '1-2' for INTERVAL HOUR.*");
-    wholeExpr("INTERVAL '1.2' HOUR")
-        .fails("Illegal interval literal format '1.2' for INTERVAL HOUR.*");
-    wholeExpr("INTERVAL '1 2' HOUR")
-        .fails("Illegal interval literal format '1 2' for INTERVAL HOUR.*");
-    wholeExpr("INTERVAL '1:2' HOUR")
-        .fails("Illegal interval literal format '1:2' for INTERVAL HOUR.*");
-    wholeExpr("INTERVAL '1-2' HOUR(2)")
-        .fails("Illegal interval literal format '1-2' for INTERVAL HOUR\\(2\\)");
-    wholeExpr("INTERVAL 'bogus text' HOUR")
-        .fails("Illegal interval literal format 'bogus text' for "
-            + "INTERVAL HOUR.*");
-
-    // negative field values
-    wholeExpr("INTERVAL '--1' HOUR")
-        .fails("Illegal interval literal format '--1' for INTERVAL HOUR.*");
-
-    // Field value out of range
-    //  (default, explicit default, alt, neg alt, max, neg max)
-    wholeExpr("INTERVAL '100' HOUR")
-        .fails("Interval field value 100 exceeds precision of "
-            + "HOUR\\(2\\) field.*");
-    wholeExpr("INTERVAL '100' HOUR(2)")
-        .fails("Interval field value 100 exceeds precision of "
-            + "HOUR\\(2\\) field.*");
-    wholeExpr("INTERVAL '1000' HOUR(3)")
-        .fails("Interval field value 1,000 exceeds precision of "
-            + "HOUR\\(3\\) field.*");
-    wholeExpr("INTERVAL '-1000' HOUR(3)")
-        .fails("Interval field value -1,000 exceeds precision of "
-            + "HOUR\\(3\\) field.*");
-    wholeExpr("INTERVAL '2147483648' HOUR(10)")
-        .fails("Interval field value 2,147,483,648 exceeds precision of "
-            + "HOUR\\(10\\) field.*");
-    wholeExpr("INTERVAL '-2147483648' HOUR(10)")
-        .fails("Interval field value -2,147,483,648 exceeds precision of "
-            + "HOUR\\(10\\) field.*");
-
-    // precision > maximum
-    expr("INTERVAL '1' ^HOUR(11)^")
-        .fails("Interval leading field precision '11' out of range for "
-            + "INTERVAL HOUR\\(11\\)");
-
-    // precision < minimum allowed)
-    // note: parser will catch negative values, here we
-    // just need to check for 0
-    expr("INTERVAL '0' ^HOUR(0)^")
-        .fails("Interval leading field precision '0' out of range for "
-            + "INTERVAL HOUR\\(0\\)");
-  }
-
-  /**
-   * Runs tests for INTERVAL... HOUR TO MINUTE that should pass parser but
-   * fail validator. A substantially identical set of tests exists in
-   * SqlParserTest, and any changes here should be synchronized there.
-   * Similarly, any changes to tests here should be echoed appropriately to
-   * each of the other 12 subTestIntervalXXXNegative() tests.
-   */
-  void subTestIntervalHourToMinuteNegative() {
-    // Qualifier - field mismatches
-    wholeExpr("INTERVAL ':' HOUR TO MINUTE")
-        .fails("Illegal interval literal format ':' for INTERVAL HOUR TO MINUTE");
-    wholeExpr("INTERVAL '1' HOUR TO MINUTE")
-        .fails("Illegal interval literal format '1' for INTERVAL HOUR TO MINUTE");
-    wholeExpr("INTERVAL '1:x' HOUR TO MINUTE")
-        .fails("Illegal interval literal format '1:x' for INTERVAL HOUR TO MINUTE");
-    wholeExpr("INTERVAL '1.2' HOUR TO MINUTE")
-        .fails("Illegal interval literal format '1.2' for INTERVAL HOUR TO MINUTE");
-    wholeExpr("INTERVAL '1 2' HOUR TO MINUTE")
-        .fails("Illegal interval literal format '1 2' for INTERVAL HOUR TO MINUTE");
-    wholeExpr("INTERVAL '1:2:3' HOUR TO MINUTE")
-        .fails("Illegal interval literal format '1:2:3' for INTERVAL HOUR TO MINUTE");
-    wholeExpr("INTERVAL '1 2' HOUR(2) TO MINUTE")
-        .fails("Illegal interval literal format '1 2' for "
-            + "INTERVAL HOUR\\(2\\) TO MINUTE");
-    wholeExpr("INTERVAL 'bogus text' HOUR TO MINUTE")
-        .fails("Illegal interval literal format 'bogus text' for "
-            + "INTERVAL HOUR TO MINUTE");
-
-    // negative field values
-    wholeExpr("INTERVAL '--1:1' HOUR TO MINUTE")
-        .fails("Illegal interval literal format '--1:1' for INTERVAL HOUR TO MINUTE");
-    wholeExpr("INTERVAL '1:-1' HOUR TO MINUTE")
-        .fails("Illegal interval literal format '1:-1' for INTERVAL HOUR TO MINUTE");
-
-    // Field value out of range
-    //  (default, explicit default, alt, neg alt, max, neg max)
-    //  plus >max value for mid/end fields
-    wholeExpr("INTERVAL '100:0' HOUR TO MINUTE")
-        .fails("Interval field value 100 exceeds precision of HOUR\\(2\\) field.*");
-    wholeExpr("INTERVAL '100:0' HOUR(2) TO MINUTE")
-        .fails("Interval field value 100 exceeds precision of HOUR\\(2\\) field.*");
-    wholeExpr("INTERVAL '1000:0' HOUR(3) TO MINUTE")
-        .fails("Interval field value 1,000 exceeds precision of HOUR\\(3\\) field.*");
-    wholeExpr("INTERVAL '-1000:0' HOUR(3) TO MINUTE")
-        .fails("Interval field value -1,000 exceeds precision of HOUR\\(3\\) field.*");
-    wholeExpr("INTERVAL '2147483648:0' HOUR(10) TO MINUTE")
-        .fails("Interval field value 2,147,483,648 exceeds precision of HOUR\\(10\\) field.*");
-    wholeExpr("INTERVAL '-2147483648:0' HOUR(10) TO MINUTE")
-        .fails("Interval field value -2,147,483,648 exceeds precision of HOUR\\(10\\) field.*");
-    wholeExpr("INTERVAL '1:60' HOUR TO MINUTE")
-        .fails("Illegal interval literal format '1:60' for INTERVAL HOUR TO MINUTE.*");
-
-    // precision > maximum
-    expr("INTERVAL '1:1' ^HOUR(11) TO MINUTE^")
-        .fails("Interval leading field precision '11' out of range for "
-            + "INTERVAL HOUR\\(11\\) TO MINUTE");
-
-    // precision < minimum allowed)
-    // note: parser will catch negative values, here we
-    // just need to check for 0
-    expr("INTERVAL '0:0' ^HOUR(0) TO MINUTE^")
-        .fails("Interval leading field precision '0' out of range for "
-            + "INTERVAL HOUR\\(0\\) TO MINUTE");
-  }
-
-  /**
-   * Runs tests for INTERVAL... HOUR TO SECOND that should pass parser but
-   * fail validator. A substantially identical set of tests exists in
-   * SqlParserTest, and any changes here should be synchronized there.
-   * Similarly, any changes to tests here should be echoed appropriately to
-   * each of the other 12 subTestIntervalXXXNegative() tests.
-   */
-  void subTestIntervalHourToSecondNegative() {
-    // Qualifier - field mismatches
-    wholeExpr("INTERVAL '::' HOUR TO SECOND")
-        .fails("Illegal interval literal format '::' for INTERVAL HOUR TO SECOND");
-    wholeExpr("INTERVAL '::.' HOUR TO SECOND")
-        .fails("Illegal interval literal format '::\\.' for INTERVAL HOUR TO SECOND");
-    wholeExpr("INTERVAL '1' HOUR TO SECOND")
-        .fails("Illegal interval literal format '1' for INTERVAL HOUR TO SECOND");
-    wholeExpr("INTERVAL '1 2' HOUR TO SECOND")
-        .fails("Illegal interval literal format '1 2' for INTERVAL HOUR TO SECOND");
-    wholeExpr("INTERVAL '1:2' HOUR TO SECOND")
-        .fails("Illegal interval literal format '1:2' for INTERVAL HOUR TO SECOND");
-    wholeExpr("INTERVAL '1.2' HOUR TO SECOND")
-        .fails("Illegal interval literal format '1\\.2' for INTERVAL HOUR TO SECOND");
-    wholeExpr("INTERVAL '1 1:2' HOUR TO SECOND")
-        .fails("Illegal interval literal format '1 1:2' for INTERVAL HOUR TO SECOND");
-    wholeExpr("INTERVAL '1:2:x' HOUR TO SECOND")
-        .fails("Illegal interval literal format '1:2:x' for INTERVAL HOUR TO SECOND");
-    wholeExpr("INTERVAL '1:x:3' HOUR TO SECOND")
-        .fails("Illegal interval literal format '1:x:3' for INTERVAL HOUR TO SECOND");
-    wholeExpr("INTERVAL '1:1:1.x' HOUR TO SECOND")
-        .fails("Illegal interval literal format '1:1:1\\.x' for INTERVAL HOUR TO SECOND");
-    wholeExpr("INTERVAL '1 1:2' HOUR(2) TO SECOND")
-        .fails("Illegal interval literal format '1 1:2' for INTERVAL HOUR\\(2\\) TO SECOND");
-    wholeExpr("INTERVAL '1 1' HOUR(2) TO SECOND")
-        .fails("Illegal interval literal format '1 1' for INTERVAL HOUR\\(2\\) TO SECOND");
-    wholeExpr("INTERVAL 'bogus text' HOUR TO SECOND")
-        .fails("Illegal interval literal format 'bogus text' for INTERVAL HOUR TO SECOND");
-    wholeExpr("INTERVAL '6:7:8901' HOUR TO SECOND(4)")
-        .fails("Illegal interval literal format '6:7:8901' for INTERVAL HOUR TO SECOND\\(4\\)");
-
-    // negative field values
-    wholeExpr("INTERVAL '--1:1:1' HOUR TO SECOND")
-        .fails("Illegal interval literal format '--1:1:1' for INTERVAL HOUR TO SECOND");
-    wholeExpr("INTERVAL '1:-1:1' HOUR TO SECOND")
-        .fails("Illegal interval literal format '1:-1:1' for INTERVAL HOUR TO SECOND");
-    wholeExpr("INTERVAL '1:1:-1' HOUR TO SECOND")
-        .fails("Illegal interval literal format '1:1:-1' for INTERVAL HOUR TO SECOND");
-    wholeExpr("INTERVAL '1:1:1.-1' HOUR TO SECOND")
-        .fails("Illegal interval literal format '1:1:1\\.-1' for INTERVAL HOUR TO SECOND");
-
-    // Field value out of range
-    //  (default, explicit default, alt, neg alt, max, neg max)
-    //  plus >max value for mid/end fields
-    wholeExpr("INTERVAL '100:0:0' HOUR TO SECOND")
-        .fails("Interval field value 100 exceeds precision of "
-            + "HOUR\\(2\\) field.*");
-    wholeExpr("INTERVAL '100:0:0' HOUR(2) TO SECOND")
-        .fails("Interval field value 100 exceeds precision of "
-            + "HOUR\\(2\\) field.*");
-    wholeExpr("INTERVAL '1000:0:0' HOUR(3) TO SECOND")
-        .fails("Interval field value 1,000 exceeds precision of "
-            + "HOUR\\(3\\) field.*");
-    wholeExpr("INTERVAL '-1000:0:0' HOUR(3) TO SECOND")
-        .fails("Interval field value -1,000 exceeds precision of "
-            + "HOUR\\(3\\) field.*");
-    wholeExpr("INTERVAL '2147483648:0:0' HOUR(10) TO SECOND")
-        .fails("Interval field value 2,147,483,648 exceeds precision of "
-            + "HOUR\\(10\\) field.*");
-    wholeExpr("INTERVAL '-2147483648:0:0' HOUR(10) TO SECOND")
-        .fails("Interval field value -2,147,483,648 exceeds precision of "
-            + "HOUR\\(10\\) field.*");
-    wholeExpr("INTERVAL '1:60:1' HOUR TO SECOND")
-        .fails("Illegal interval literal format '1:60:1' for "
-            + "INTERVAL HOUR TO SECOND.*");
-    wholeExpr("INTERVAL '1:1:60' HOUR TO SECOND")
-        .fails("Illegal interval literal format '1:1:60' for "
-            + "INTERVAL HOUR TO SECOND.*");
-    wholeExpr("INTERVAL '1:1:1.0000001' HOUR TO SECOND")
-        .fails("Illegal interval literal format '1:1:1\\.0000001' for "
-            + "INTERVAL HOUR TO SECOND.*");
-    wholeExpr("INTERVAL '1:1:1.0001' HOUR TO SECOND(3)")
-        .fails("Illegal interval literal format '1:1:1\\.0001' for "
-            + "INTERVAL HOUR TO SECOND\\(3\\).*");
-
-    // precision > maximum
-    expr("INTERVAL '1:1:1' ^HOUR(11) TO SECOND^")
-        .fails("Interval leading field precision '11' out of range for "
-            + "INTERVAL HOUR\\(11\\) TO SECOND");
-    expr("INTERVAL '1:1:1' ^HOUR TO SECOND(10)^")
-        .fails("Interval fractional second precision '10' out of range for "
-            + "INTERVAL HOUR TO SECOND\\(10\\)");
-
-    // precision < minimum allowed)
-    // note: parser will catch negative values, here we
-    // just need to check for 0
-    expr("INTERVAL '0:0:0' ^HOUR(0) TO SECOND^")
-        .fails("Interval leading field precision '0' out of range for "
-            + "INTERVAL HOUR\\(0\\) TO SECOND");
-    expr("INTERVAL '0:0:0' ^HOUR TO SECOND(0)^")
-        .fails("Interval fractional second precision '0' out of range for "
-            + "INTERVAL HOUR TO SECOND\\(0\\)");
-  }
-
-  /**
-   * Runs tests for INTERVAL... MINUTE that should pass parser but fail
-   * validator. A substantially identical set of tests exists in
-   * SqlParserTest, and any changes here should be synchronized there.
-   * Similarly, any changes to tests here should be echoed appropriately to
-   * each of the other 12 subTestIntervalXXXNegative() tests.
-   */
-  void subTestIntervalMinuteNegative() {
-    // Qualifier - field mismatches
-    wholeExpr("INTERVAL '-' MINUTE")
-        .fails("Illegal interval literal format '-' for INTERVAL MINUTE.*");
-    wholeExpr("INTERVAL '1-2' MINUTE")
-        .fails("Illegal interval literal format '1-2' for INTERVAL MINUTE.*");
-    wholeExpr("INTERVAL '1.2' MINUTE")
-        .fails("Illegal interval literal format '1.2' for INTERVAL MINUTE.*");
-    wholeExpr("INTERVAL '1 2' MINUTE")
-        .fails("Illegal interval literal format '1 2' for INTERVAL MINUTE.*");
-    wholeExpr("INTERVAL '1:2' MINUTE")
-        .fails("Illegal interval literal format '1:2' for INTERVAL MINUTE.*");
-    wholeExpr("INTERVAL '1-2' MINUTE(2)")
-        .fails("Illegal interval literal format '1-2' for INTERVAL MINUTE\\(2\\)");
-    wholeExpr("INTERVAL 'bogus text' MINUTE")
-        .fails("Illegal interval literal format 'bogus text' for INTERVAL MINUTE.*");
-
-    // negative field values
-    wholeExpr("INTERVAL '--1' MINUTE")
-        .fails("Illegal interval literal format '--1' for INTERVAL MINUTE.*");
-
-    // Field value out of range
-    //  (default, explicit default, alt, neg alt, max, neg max)
-    wholeExpr("INTERVAL '100' MINUTE")
-        .fails("Interval field value 100 exceeds precision of MINUTE\\(2\\) field.*");
-    wholeExpr("INTERVAL '100' MINUTE(2)")
-        .fails("Interval field value 100 exceeds precision of MINUTE\\(2\\) field.*");
-    wholeExpr("INTERVAL '1000' MINUTE(3)")
-        .fails("Interval field value 1,000 exceeds precision of MINUTE\\(3\\) field.*");
-    wholeExpr("INTERVAL '-1000' MINUTE(3)")
-        .fails("Interval field value -1,000 exceeds precision of MINUTE\\(3\\) field.*");
-    wholeExpr("INTERVAL '2147483648' MINUTE(10)")
-        .fails("Interval field value 2,147,483,648 exceeds precision of MINUTE\\(10\\) field.*");
-    wholeExpr("INTERVAL '-2147483648' MINUTE(10)")
-        .fails("Interval field value -2,147,483,648 exceeds precision of MINUTE\\(10\\) field.*");
-
-    // precision > maximum
-    expr("INTERVAL '1' ^MINUTE(11)^")
-        .fails("Interval leading field precision '11' out of range for "
-            + "INTERVAL MINUTE\\(11\\)");
-
-    // precision < minimum allowed)
-    // note: parser will catch negative values, here we
-    // just need to check for 0
-    expr("INTERVAL '0' ^MINUTE(0)^")
-        .fails("Interval leading field precision '0' out of range for "
-            + "INTERVAL MINUTE\\(0\\)");
-  }
-
-  /**
-   * Runs tests for INTERVAL... MINUTE TO SECOND that should pass parser but
-   * fail validator. A substantially identical set of tests exists in
-   * SqlParserTest, and any changes here should be synchronized there.
-   * Similarly, any changes to tests here should be echoed appropriately to
-   * each of the other 12 subTestIntervalXXXNegative() tests.
-   */
-  void subTestIntervalMinuteToSecondNegative() {
-    // Qualifier - field mismatches
-    wholeExpr("INTERVAL ':' MINUTE TO SECOND")
-        .fails("Illegal interval literal format ':' for INTERVAL MINUTE TO SECOND");
-    wholeExpr("INTERVAL ':.' MINUTE TO SECOND")
-        .fails("Illegal interval literal format ':\\.' for INTERVAL MINUTE TO SECOND");
-    wholeExpr("INTERVAL '1' MINUTE TO SECOND")
-        .fails("Illegal interval literal format '1' for INTERVAL MINUTE TO SECOND");
-    wholeExpr("INTERVAL '1 2' MINUTE TO SECOND")
-        .fails("Illegal interval literal format '1 2' for INTERVAL MINUTE TO SECOND");
-    wholeExpr("INTERVAL '1.2' MINUTE TO SECOND")
-        .fails("Illegal interval literal format '1\\.2' for INTERVAL MINUTE TO SECOND");
-    wholeExpr("INTERVAL '1 1:2' MINUTE TO SECOND")
-        .fails("Illegal interval literal format '1 1:2' for INTERVAL MINUTE TO SECOND");
-    wholeExpr("INTERVAL '1:x' MINUTE TO SECOND")
-        .fails("Illegal interval literal format '1:x' for INTERVAL MINUTE TO SECOND");
-    wholeExpr("INTERVAL 'x:3' MINUTE TO SECOND")
-        .fails("Illegal interval literal format 'x:3' for INTERVAL MINUTE TO SECOND");
-    wholeExpr("INTERVAL '1:1.x' MINUTE TO SECOND")
-        .fails("Illegal interval literal format '1:1\\.x' for INTERVAL MINUTE TO SECOND");
-    wholeExpr("INTERVAL '1 1:2' MINUTE(2) TO SECOND")
-        .fails("Illegal interval literal format '1 1:2' for INTERVAL MINUTE\\(2\\) TO SECOND");
-    wholeExpr("INTERVAL '1 1' MINUTE(2) TO SECOND")
-        .fails("Illegal interval literal format '1 1' for INTERVAL MINUTE\\(2\\) TO SECOND");
-    wholeExpr("INTERVAL 'bogus text' MINUTE TO SECOND")
-        .fails("Illegal interval literal format 'bogus text' for INTERVAL MINUTE TO SECOND");
-    wholeExpr("INTERVAL '7:8901' MINUTE TO SECOND(4)")
-        .fails("Illegal interval literal format '7:8901' for INTERVAL MINUTE TO SECOND\\(4\\)");
-
-    // negative field values
-    wholeExpr("INTERVAL '--1:1' MINUTE TO SECOND")
-        .fails("Illegal interval literal format '--1:1' for INTERVAL MINUTE TO SECOND");
-    wholeExpr("INTERVAL '1:-1' MINUTE TO SECOND")
-        .fails("Illegal interval literal format '1:-1' for INTERVAL MINUTE TO SECOND");
-    wholeExpr("INTERVAL '1:1.-1' MINUTE TO SECOND")
-        .fails("Illegal interval literal format '1:1.-1' for INTERVAL MINUTE TO SECOND");
-
-    // Field value out of range
-    //  (default, explicit default, alt, neg alt, max, neg max)
-    //  plus >max value for mid/end fields
-    wholeExpr("INTERVAL '100:0' MINUTE TO SECOND")
-        .fails("Interval field value 100 exceeds precision of MINUTE\\(2\\) field.*");
-    wholeExpr("INTERVAL '100:0' MINUTE(2) TO SECOND")
-        .fails("Interval field value 100 exceeds precision of MINUTE\\(2\\) field.*");
-    wholeExpr("INTERVAL '1000:0' MINUTE(3) TO SECOND")
-        .fails("Interval field value 1,000 exceeds precision of MINUTE\\(3\\) field.*");
-    wholeExpr("INTERVAL '-1000:0' MINUTE(3) TO SECOND")
-        .fails("Interval field value -1,000 exceeds precision of MINUTE\\(3\\) field.*");
-    wholeExpr("INTERVAL '2147483648:0' MINUTE(10) TO SECOND")
-        .fails("Interval field value 2,147,483,648 exceeds precision of MINUTE\\(10\\) field.*");
-    wholeExpr("INTERVAL '-2147483648:0' MINUTE(10) TO SECOND")
-        .fails("Interval field value -2,147,483,648 exceeds precision of MINUTE\\(10\\) field.*");
-    wholeExpr("INTERVAL '1:60' MINUTE TO SECOND")
-        .fails("Illegal interval literal format '1:60' for"
-            + " INTERVAL MINUTE TO SECOND.*");
-    wholeExpr("INTERVAL '1:1.0000001' MINUTE TO SECOND")
-        .fails("Illegal interval literal format '1:1\\.0000001' for"
-            + " INTERVAL MINUTE TO SECOND.*");
-    wholeExpr("INTERVAL '1:1:1.0001' MINUTE TO SECOND(3)")
-        .fails("Illegal interval literal format '1:1:1\\.0001' for"
-            + " INTERVAL MINUTE TO SECOND\\(3\\).*");
-
-    // precision > maximum
-    expr("INTERVAL '1:1' ^MINUTE(11) TO SECOND^")
-        .fails("Interval leading field precision '11' out of range for"
-            + " INTERVAL MINUTE\\(11\\) TO SECOND");
-    expr("INTERVAL '1:1' ^MINUTE TO SECOND(10)^")
-        .fails("Interval fractional second precision '10' out of range for"
-            + " INTERVAL MINUTE TO SECOND\\(10\\)");
-
-    // precision < minimum allowed)
-    // note: parser will catch negative values, here we
-    // just need to check for 0
-    expr("INTERVAL '0:0' ^MINUTE(0) TO SECOND^")
-        .fails("Interval leading field precision '0' out of range for"
-            + " INTERVAL MINUTE\\(0\\) TO SECOND");
-    expr("INTERVAL '0:0' ^MINUTE TO SECOND(0)^")
-        .fails("Interval fractional second precision '0' out of range for"
-            + " INTERVAL MINUTE TO SECOND\\(0\\)");
-  }
-
-  /**
-   * Runs tests for INTERVAL... SECOND that should pass parser but fail
-   * validator. A substantially identical set of tests exists in
-   * SqlParserTest, and any changes here should be synchronized there.
-   * Similarly, any changes to tests here should be echoed appropriately to
-   * each of the other 12 subTestIntervalXXXNegative() tests.
-   */
-  void subTestIntervalSecondNegative() {
-    // Qualifier - field mismatches
-    wholeExpr("INTERVAL ':' SECOND")
-        .fails("Illegal interval literal format ':' for INTERVAL SECOND.*");
-    wholeExpr("INTERVAL '.' SECOND")
-        .fails("Illegal interval literal format '\\.' for INTERVAL SECOND.*");
-    wholeExpr("INTERVAL '1-2' SECOND")
-        .fails("Illegal interval literal format '1-2' for INTERVAL SECOND.*");
-    wholeExpr("INTERVAL '1.x' SECOND")
-        .fails("Illegal interval literal format '1\\.x' for INTERVAL SECOND.*");
-    wholeExpr("INTERVAL 'x.1' SECOND")
-        .fails("Illegal interval literal format 'x\\.1' for INTERVAL SECOND.*");
-    wholeExpr("INTERVAL '1 2' SECOND")
-        .fails("Illegal interval literal format '1 2' for INTERVAL SECOND.*");
-    wholeExpr("INTERVAL '1:2' SECOND")
-        .fails("Illegal interval literal format '1:2' for INTERVAL SECOND.*");
-    wholeExpr("INTERVAL '1-2' SECOND(2)")
-        .fails("Illegal interval literal format '1-2' for INTERVAL SECOND\\(2\\)");
-    wholeExpr("INTERVAL 'bogus text' SECOND")
-        .fails("Illegal interval literal format 'bogus text' for INTERVAL SECOND.*");
-
-    // negative field values
-    wholeExpr("INTERVAL '--1' SECOND")
-        .fails("Illegal interval literal format '--1' for INTERVAL SECOND.*");
-    wholeExpr("INTERVAL '1.-1' SECOND")
-        .fails("Illegal interval literal format '1.-1' for INTERVAL SECOND.*");
-
-    // Field value out of range
-    //  (default, explicit default, alt, neg alt, max, neg max)
-    wholeExpr("INTERVAL '100' SECOND")
-        .fails("Interval field value 100 exceeds precision of SECOND\\(2\\) field.*");
-    wholeExpr("INTERVAL '100' SECOND(2)")
-        .fails("Interval field value 100 exceeds precision of SECOND\\(2\\) field.*");
-    wholeExpr("INTERVAL '1000' SECOND(3)")
-        .fails("Interval field value 1,000 exceeds precision of SECOND\\(3\\) field.*");
-    wholeExpr("INTERVAL '-1000' SECOND(3)")
-        .fails("Interval field value -1,000 exceeds precision of SECOND\\(3\\) field.*");
-    wholeExpr("INTERVAL '2147483648' SECOND(10)")
-        .fails("Interval field value 2,147,483,648 exceeds precision of SECOND\\(10\\) field.*");
-    wholeExpr("INTERVAL '-2147483648' SECOND(10)")
-        .fails("Interval field value -2,147,483,648 exceeds precision of SECOND\\(10\\) field.*");
-    wholeExpr("INTERVAL '1.0000001' SECOND")
-        .fails("Illegal interval literal format '1\\.0000001' for INTERVAL SECOND.*");
-    wholeExpr("INTERVAL '1.0000001' SECOND(2)")
-        .fails("Illegal interval literal format '1\\.0000001' for INTERVAL SECOND\\(2\\).*");
-    wholeExpr("INTERVAL '1.0001' SECOND(2, 3)")
-        .fails("Illegal interval literal format '1\\.0001' for INTERVAL SECOND\\(2, 3\\).*");
-    wholeExpr("INTERVAL '1.0000000001' SECOND(2, 9)")
-        .fails("Illegal interval literal format '1\\.0000000001' for"
-            + " INTERVAL SECOND\\(2, 9\\).*");
-
-    // precision > maximum
-    expr("INTERVAL '1' ^SECOND(11)^")
-        .fails("Interval leading field precision '11' out of range for"
-            + " INTERVAL SECOND\\(11\\)");
-    expr("INTERVAL '1.1' ^SECOND(1, 10)^")
-        .fails("Interval fractional second precision '10' out of range for"
-            + " INTERVAL SECOND\\(1, 10\\)");
-
-    // precision < minimum allowed)
-    // note: parser will catch negative values, here we
-    // just need to check for 0
-    expr("INTERVAL '0' ^SECOND(0)^")
-        .fails("Interval leading field precision '0' out of range for"
-            + " INTERVAL SECOND\\(0\\)");
-    expr("INTERVAL '0' ^SECOND(1, 0)^")
-        .fails("Interval fractional second precision '0' out of range for"
-            + " INTERVAL SECOND\\(1, 0\\)");
-  }
-
   @Test void testDatetimePlusNullInterval() {
     expr("TIME '8:8:8' + cast(NULL AS interval hour)").columnType("TIME(0)");
     expr("TIME '8:8:8' + cast(NULL AS interval YEAR)").columnType("TIME(0)");
@@ -3907,46 +2219,34 @@ public class SqlValidatorTest extends SqlValidatorTestCase {
       assertThat(typeName.getDefaultScale(), is(6));
     }
 
-    // Tests that should pass both parser and validator
-    subTestIntervalYearPositive();
-    subTestIntervalYearToMonthPositive();
-    subTestIntervalMonthPositive();
-    subTestIntervalDayPositive();
-    subTestIntervalDayToHourPositive();
-    subTestIntervalDayToMinutePositive();
-    subTestIntervalDayToSecondPositive();
-    subTestIntervalHourPositive();
-    subTestIntervalHourToMinutePositive();
-    subTestIntervalHourToSecondPositive();
-    subTestIntervalMinutePositive();
-    subTestIntervalMinuteToSecondPositive();
-    subTestIntervalSecondPositive();
-
-    // Tests that should pass parser but fail validator
-    subTestIntervalYearNegative();
-    subTestIntervalYearToMonthNegative();
-    subTestIntervalMonthNegative();
-    subTestIntervalDayNegative();
-    subTestIntervalDayToHourNegative();
-    subTestIntervalDayToMinuteNegative();
-    subTestIntervalDayToSecondNegative();
-    subTestIntervalHourNegative();
-    subTestIntervalHourToMinuteNegative();
-    subTestIntervalHourToSecondNegative();
-    subTestIntervalMinuteNegative();
-    subTestIntervalMinuteToSecondNegative();
-    subTestIntervalSecondNegative();
-
-    // Miscellaneous
-    // fractional value is not OK, even if it is 0
-    wholeExpr("INTERVAL '1.0' HOUR")
-        .fails("Illegal interval literal format '1.0' for INTERVAL HOUR");
-    // only seconds are allowed to have a fractional part
-    expr("INTERVAL '1.0' SECOND")
-        .columnType("INTERVAL SECOND NOT NULL");
-    // leading zeros do not cause precision to be exceeded
-    expr("INTERVAL '0999' MONTH(3)")
-        .columnType("INTERVAL MONTH(3) NOT NULL");
+    final SqlValidatorFixture f = fixture();
+    final IntervalTest.Fixture intervalFixture = new IntervalTest.Fixture() {
+      @Override public IntervalTest.Fixture2 expr(String s) {
+        return getFixture2(f.withExpr(s));
+      }
+
+      @Override public IntervalTest.Fixture2 wholeExpr(String s) {
+        return getFixture2(f.withExpr(s).withWhole(true));
+      }
+
+      private IntervalTest.Fixture2 getFixture2(SqlValidatorFixture f2) {
+        return new IntervalTest.Fixture2() {
+          @Override public void fails(String message) {
+            f2.fails(message);
+          }
+
+          @Override public void columnType(String expectedType) {
+            f2.columnType(expectedType);
+          }
+
+          @Override public IntervalTest.Fixture2 assertParse(String expected) {
+            return this;
+          }
+        };
+      }
+    };
+
+    new IntervalTest(intervalFixture).testAll();
   }
 
   @Test void testIntervalExpression() {
@@ -4415,7 +2715,8 @@ public class SqlValidatorTest extends SqlValidatorTestCase {
   }
 
   // test window partition clause. See SQL 2003 specification for detail
-  public void _testWinPartClause() {
+  @Disabled
+  void testWinPartClause() {
     win("window w as (w2 order by deptno), w2 as (^rang^e 100 preceding)")
         .fails("Referenced window cannot have framing declarations");
     // Test specified collation, window clause syntax rule 4,5.
@@ -7589,8 +5890,8 @@ public class SqlValidatorTest extends SqlValidatorTestCase {
         .fails("Expression 'EMP\\.EMPNO' is not being grouped");
   }
 
-  // todo: enable when correlating variables work
-  public void _testGroupExpressionEquivalenceCorrelated() {
+  @Disabled("todo: enable when correlating variables work")
+  void testGroupExpressionEquivalenceCorrelated() {
     // dname comes from dept, so it is constant within the sub-query, and
     // is so is a valid expr in a group-by query
     sql("select * from dept where exists ("
@@ -7599,8 +5900,8 @@ public class SqlValidatorTest extends SqlValidatorTestCase {
         + "select dname + empno + 1 from emp group by empno, dept.deptno)").ok();
   }
 
-  // todo: enable when params are implemented
-  public void _testGroupExpressionEquivalenceParams() {
+  @Disabled("todo: enable when params are implemented")
+  void testGroupExpressionEquivalenceParams() {
     sql("select cast(? as integer) from emp group by cast(? as integer)").ok();
   }
 
diff --git a/testkit/src/main/java/org/apache/calcite/sql/parser/SqlParserFixture.java b/testkit/src/main/java/org/apache/calcite/sql/parser/SqlParserFixture.java
index a94f1471f9..34fc7ac114 100644
--- a/testkit/src/main/java/org/apache/calcite/sql/parser/SqlParserFixture.java
+++ b/testkit/src/main/java/org/apache/calcite/sql/parser/SqlParserFixture.java
@@ -72,17 +72,17 @@ public class SqlParserFixture {
   }
 
   public SqlParserFixture same() {
-    return ok_(sap.sql);
+    return compare(sap.sql);
   }
 
   public SqlParserFixture ok(String expected) {
     if (expected.equals(sap.sql)) {
       throw new AssertionError("you should call same()");
     }
-    return ok_(expected);
+    return compare(expected);
   }
 
-  private SqlParserFixture ok_(String expected) {
+  public SqlParserFixture compare(String expected) {
     final UnaryOperator<String> converter = SqlParserTest.linux(convertToLinux);
     if (expression) {
       tester.checkExp(factory, sap, converter, expected, parserChecker);
diff --git a/testkit/src/main/java/org/apache/calcite/sql/parser/SqlParserTest.java b/testkit/src/main/java/org/apache/calcite/sql/parser/SqlParserTest.java
index 6e1f83b76c..d6e449055f 100644
--- a/testkit/src/main/java/org/apache/calcite/sql/parser/SqlParserTest.java
+++ b/testkit/src/main/java/org/apache/calcite/sql/parser/SqlParserTest.java
@@ -37,6 +37,7 @@ import org.apache.calcite.sql.test.SqlTests;
 import org.apache.calcite.sql.util.SqlShuttle;
 import org.apache.calcite.sql.validate.SqlConformanceEnum;
 import org.apache.calcite.test.DiffTestCase;
+import org.apache.calcite.test.IntervalTest;
 import org.apache.calcite.tools.Hoist;
 import org.apache.calcite.util.Bug;
 import org.apache.calcite.util.ConversionUtil;
@@ -5258,10 +5259,8 @@ public class SqlParserTest {
     expr("TIME '12:01:01.01023456789'").same();
 
     // Timestamp literals
-    expr("TIMESTAMP '2004-12-01 12:01:01'")
-        .ok("TIMESTAMP '2004-12-01 12:01:01'");
-    expr("TIMESTAMP '2004-12-01 12:01:01.1'")
-        .ok("TIMESTAMP '2004-12-01 12:01:01.1'");
+    expr("TIMESTAMP '2004-12-01 12:01:01'").same();
+    expr("TIMESTAMP '2004-12-01 12:01:01.1'").same();
     expr("TIMESTAMP '2004-12-01 12:01:01.'")
         .ok("TIMESTAMP '2004-12-01 12:01:01'");
     expr("TIMESTAMP  '2004-12-01 12:01:01.010234567890'")
@@ -5834,952 +5833,6 @@ public class SqlParserTest {
         .ok("(MAP[])");
   }
 
-  /**
-   * Runs tests for INTERVAL... YEAR that should pass both parser and
-   * validator. A substantially identical set of tests exists in
-   * SqlValidatorTest, and any changes here should be synchronized there.
-   * Similarly, any changes to tests here should be echoed appropriately to
-   * each of the other 12 subTestIntervalXXXPositive() tests.
-   */
-  public void subTestIntervalYearPositive() {
-    // default precision
-    expr("interval '1' year")
-        .ok("INTERVAL '1' YEAR");
-    expr("interval '99' year")
-        .ok("INTERVAL '99' YEAR");
-
-    // explicit precision equal to default
-    expr("interval '1' year(2)")
-        .ok("INTERVAL '1' YEAR(2)");
-    expr("interval '99' year(2)")
-        .ok("INTERVAL '99' YEAR(2)");
-
-    // max precision
-    expr("interval '2147483647' year(10)")
-        .ok("INTERVAL '2147483647' YEAR(10)");
-
-    // min precision
-    expr("interval '0' year(1)")
-        .ok("INTERVAL '0' YEAR(1)");
-
-    // alternate precision
-    expr("interval '1234' year(4)")
-        .ok("INTERVAL '1234' YEAR(4)");
-
-    // sign
-    expr("interval '+1' year")
-        .ok("INTERVAL '+1' YEAR");
-    expr("interval '-1' year")
-        .ok("INTERVAL '-1' YEAR");
-    expr("interval +'1' year")
-        .ok("INTERVAL '1' YEAR");
-    expr("interval +'+1' year")
-        .ok("INTERVAL '+1' YEAR");
-    expr("interval +'-1' year")
-        .ok("INTERVAL '-1' YEAR");
-    expr("interval -'1' year")
-        .ok("INTERVAL -'1' YEAR");
-    expr("interval -'+1' year")
-        .ok("INTERVAL -'+1' YEAR");
-    expr("interval -'-1' year")
-        .ok("INTERVAL -'-1' YEAR");
-  }
-
-  /**
-   * Runs tests for INTERVAL... YEAR TO MONTH that should pass both parser and
-   * validator. A substantially identical set of tests exists in
-   * SqlValidatorTest, and any changes here should be synchronized there.
-   * Similarly, any changes to tests here should be echoed appropriately to
-   * each of the other 12 subTestIntervalXXXPositive() tests.
-   */
-  public void subTestIntervalYearToMonthPositive() {
-    // default precision
-    expr("interval '1-2' year to month")
-        .ok("INTERVAL '1-2' YEAR TO MONTH");
-    expr("interval '99-11' year to month")
-        .ok("INTERVAL '99-11' YEAR TO MONTH");
-    expr("interval '99-0' year to month")
-        .ok("INTERVAL '99-0' YEAR TO MONTH");
-
-    // explicit precision equal to default
-    expr("interval '1-2' year(2) to month")
-        .ok("INTERVAL '1-2' YEAR(2) TO MONTH");
-    expr("interval '99-11' year(2) to month")
-        .ok("INTERVAL '99-11' YEAR(2) TO MONTH");
-    expr("interval '99-0' year(2) to month")
-        .ok("INTERVAL '99-0' YEAR(2) TO MONTH");
-
-    // max precision
-    expr("interval '2147483647-11' year(10) to month")
-        .ok("INTERVAL '2147483647-11' YEAR(10) TO MONTH");
-
-    // min precision
-    expr("interval '0-0' year(1) to month")
-        .ok("INTERVAL '0-0' YEAR(1) TO MONTH");
-
-    // alternate precision
-    expr("interval '2006-2' year(4) to month")
-        .ok("INTERVAL '2006-2' YEAR(4) TO MONTH");
-
-    // sign
-    expr("interval '-1-2' year to month")
-        .ok("INTERVAL '-1-2' YEAR TO MONTH");
-    expr("interval '+1-2' year to month")
-        .ok("INTERVAL '+1-2' YEAR TO MONTH");
-    expr("interval +'1-2' year to month")
-        .ok("INTERVAL '1-2' YEAR TO MONTH");
-    expr("interval +'-1-2' year to month")
-        .ok("INTERVAL '-1-2' YEAR TO MONTH");
-    expr("interval +'+1-2' year to month")
-        .ok("INTERVAL '+1-2' YEAR TO MONTH");
-    expr("interval -'1-2' year to month")
-        .ok("INTERVAL -'1-2' YEAR TO MONTH");
-    expr("interval -'-1-2' year to month")
-        .ok("INTERVAL -'-1-2' YEAR TO MONTH");
-    expr("interval -'+1-2' year to month")
-        .ok("INTERVAL -'+1-2' YEAR TO MONTH");
-  }
-
-  /**
-   * Runs tests for INTERVAL... MONTH that should pass both parser and
-   * validator. A substantially identical set of tests exists in
-   * SqlValidatorTest, and any changes here should be synchronized there.
-   * Similarly, any changes to tests here should be echoed appropriately to
-   * each of the other 12 subTestIntervalXXXPositive() tests.
-   */
-  public void subTestIntervalMonthPositive() {
-    // default precision
-    expr("interval '1' month")
-        .ok("INTERVAL '1' MONTH");
-    expr("interval '99' month")
-        .ok("INTERVAL '99' MONTH");
-
-    // explicit precision equal to default
-    expr("interval '1' month(2)")
-        .ok("INTERVAL '1' MONTH(2)");
-    expr("interval '99' month(2)")
-        .ok("INTERVAL '99' MONTH(2)");
-
-    // max precision
-    expr("interval '2147483647' month(10)")
-        .ok("INTERVAL '2147483647' MONTH(10)");
-
-    // min precision
-    expr("interval '0' month(1)")
-        .ok("INTERVAL '0' MONTH(1)");
-
-    // alternate precision
-    expr("interval '1234' month(4)")
-        .ok("INTERVAL '1234' MONTH(4)");
-
-    // sign
-    expr("interval '+1' month")
-        .ok("INTERVAL '+1' MONTH");
-    expr("interval '-1' month")
-        .ok("INTERVAL '-1' MONTH");
-    expr("interval +'1' month")
-        .ok("INTERVAL '1' MONTH");
-    expr("interval +'+1' month")
-        .ok("INTERVAL '+1' MONTH");
-    expr("interval +'-1' month")
-        .ok("INTERVAL '-1' MONTH");
-    expr("interval -'1' month")
-        .ok("INTERVAL -'1' MONTH");
-    expr("interval -'+1' month")
-        .ok("INTERVAL -'+1' MONTH");
-    expr("interval -'-1' month")
-        .ok("INTERVAL -'-1' MONTH");
-  }
-
-  /**
-   * Runs tests for INTERVAL... DAY that should pass both parser and
-   * validator. A substantially identical set of tests exists in
-   * SqlValidatorTest, and any changes here should be synchronized there.
-   * Similarly, any changes to tests here should be echoed appropriately to
-   * each of the other 12 subTestIntervalXXXPositive() tests.
-   */
-  public void subTestIntervalDayPositive() {
-    // default precision
-    expr("interval '1' day")
-        .ok("INTERVAL '1' DAY");
-    expr("interval '99' day")
-        .ok("INTERVAL '99' DAY");
-
-    // explicit precision equal to default
-    expr("interval '1' day(2)")
-        .ok("INTERVAL '1' DAY(2)");
-    expr("interval '99' day(2)")
-        .ok("INTERVAL '99' DAY(2)");
-
-    // max precision
-    expr("interval '2147483647' day(10)")
-        .ok("INTERVAL '2147483647' DAY(10)");
-
-    // min precision
-    expr("interval '0' day(1)")
-        .ok("INTERVAL '0' DAY(1)");
-
-    // alternate precision
-    expr("interval '1234' day(4)")
-        .ok("INTERVAL '1234' DAY(4)");
-
-    // sign
-    expr("interval '+1' day")
-        .ok("INTERVAL '+1' DAY");
-    expr("interval '-1' day")
-        .ok("INTERVAL '-1' DAY");
-    expr("interval +'1' day")
-        .ok("INTERVAL '1' DAY");
-    expr("interval +'+1' day")
-        .ok("INTERVAL '+1' DAY");
-    expr("interval +'-1' day")
-        .ok("INTERVAL '-1' DAY");
-    expr("interval -'1' day")
-        .ok("INTERVAL -'1' DAY");
-    expr("interval -'+1' day")
-        .ok("INTERVAL -'+1' DAY");
-    expr("interval -'-1' day")
-        .ok("INTERVAL -'-1' DAY");
-  }
-
-  /**
-   * Runs tests for INTERVAL... DAY TO HOUR that should pass both parser and
-   * validator. A substantially identical set of tests exists in
-   * SqlValidatorTest, and any changes here should be synchronized there.
-   * Similarly, any changes to tests here should be echoed appropriately to
-   * each of the other 12 subTestIntervalXXXPositive() tests.
-   */
-  public void subTestIntervalDayToHourPositive() {
-    // default precision
-    expr("interval '1 2' day to hour")
-        .ok("INTERVAL '1 2' DAY TO HOUR");
-    expr("interval '99 23' day to hour")
-        .ok("INTERVAL '99 23' DAY TO HOUR");
-    expr("interval '99 0' day to hour")
-        .ok("INTERVAL '99 0' DAY TO HOUR");
-
-    // explicit precision equal to default
-    expr("interval '1 2' day(2) to hour")
-        .ok("INTERVAL '1 2' DAY(2) TO HOUR");
-    expr("interval '99 23' day(2) to hour")
-        .ok("INTERVAL '99 23' DAY(2) TO HOUR");
-    expr("interval '99 0' day(2) to hour")
-        .ok("INTERVAL '99 0' DAY(2) TO HOUR");
-
-    // max precision
-    expr("interval '2147483647 23' day(10) to hour")
-        .ok("INTERVAL '2147483647 23' DAY(10) TO HOUR");
-
-    // min precision
-    expr("interval '0 0' day(1) to hour")
-        .ok("INTERVAL '0 0' DAY(1) TO HOUR");
-
-    // alternate precision
-    expr("interval '2345 2' day(4) to hour")
-        .ok("INTERVAL '2345 2' DAY(4) TO HOUR");
-
-    // sign
-    expr("interval '-1 2' day to hour")
-        .ok("INTERVAL '-1 2' DAY TO HOUR");
-    expr("interval '+1 2' day to hour")
-        .ok("INTERVAL '+1 2' DAY TO HOUR");
-    expr("interval +'1 2' day to hour")
-        .ok("INTERVAL '1 2' DAY TO HOUR");
-    expr("interval +'-1 2' day to hour")
-        .ok("INTERVAL '-1 2' DAY TO HOUR");
-    expr("interval +'+1 2' day to hour")
-        .ok("INTERVAL '+1 2' DAY TO HOUR");
-    expr("interval -'1 2' day to hour")
-        .ok("INTERVAL -'1 2' DAY TO HOUR");
-    expr("interval -'-1 2' day to hour")
-        .ok("INTERVAL -'-1 2' DAY TO HOUR");
-    expr("interval -'+1 2' day to hour")
-        .ok("INTERVAL -'+1 2' DAY TO HOUR");
-  }
-
-  /**
-   * Runs tests for INTERVAL... DAY TO MINUTE that should pass both parser and
-   * validator. A substantially identical set of tests exists in
-   * SqlValidatorTest, and any changes here should be synchronized there.
-   * Similarly, any changes to tests here should be echoed appropriately to
-   * each of the other 12 subTestIntervalXXXPositive() tests.
-   */
-  public void subTestIntervalDayToMinutePositive() {
-    // default precision
-    expr("interval '1 2:3' day to minute")
-        .ok("INTERVAL '1 2:3' DAY TO MINUTE");
-    expr("interval '99 23:59' day to minute")
-        .ok("INTERVAL '99 23:59' DAY TO MINUTE");
-    expr("interval '99 0:0' day to minute")
-        .ok("INTERVAL '99 0:0' DAY TO MINUTE");
-
-    // explicit precision equal to default
-    expr("interval '1 2:3' day(2) to minute")
-        .ok("INTERVAL '1 2:3' DAY(2) TO MINUTE");
-    expr("interval '99 23:59' day(2) to minute")
-        .ok("INTERVAL '99 23:59' DAY(2) TO MINUTE");
-    expr("interval '99 0:0' day(2) to minute")
-        .ok("INTERVAL '99 0:0' DAY(2) TO MINUTE");
-
-    // max precision
-    expr("interval '2147483647 23:59' day(10) to minute")
-        .ok("INTERVAL '2147483647 23:59' DAY(10) TO MINUTE");
-
-    // min precision
-    expr("interval '0 0:0' day(1) to minute")
-        .ok("INTERVAL '0 0:0' DAY(1) TO MINUTE");
-
-    // alternate precision
-    expr("interval '2345 6:7' day(4) to minute")
-        .ok("INTERVAL '2345 6:7' DAY(4) TO MINUTE");
-
-    // sign
-    expr("interval '-1 2:3' day to minute")
-        .ok("INTERVAL '-1 2:3' DAY TO MINUTE");
-    expr("interval '+1 2:3' day to minute")
-        .ok("INTERVAL '+1 2:3' DAY TO MINUTE");
-    expr("interval +'1 2:3' day to minute")
-        .ok("INTERVAL '1 2:3' DAY TO MINUTE");
-    expr("interval +'-1 2:3' day to minute")
-        .ok("INTERVAL '-1 2:3' DAY TO MINUTE");
-    expr("interval +'+1 2:3' day to minute")
-        .ok("INTERVAL '+1 2:3' DAY TO MINUTE");
-    expr("interval -'1 2:3' day to minute")
-        .ok("INTERVAL -'1 2:3' DAY TO MINUTE");
-    expr("interval -'-1 2:3' day to minute")
-        .ok("INTERVAL -'-1 2:3' DAY TO MINUTE");
-    expr("interval -'+1 2:3' day to minute")
-        .ok("INTERVAL -'+1 2:3' DAY TO MINUTE");
-  }
-
-  /**
-   * Runs tests for INTERVAL... DAY TO SECOND that should pass both parser and
-   * validator. A substantially identical set of tests exists in
-   * SqlValidatorTest, and any changes here should be synchronized there.
-   * Similarly, any changes to tests here should be echoed appropriately to
-   * each of the other 12 subTestIntervalXXXPositive() tests.
-   */
-  public void subTestIntervalDayToSecondPositive() {
-    // default precision
-    expr("interval '1 2:3:4' day to second")
-        .ok("INTERVAL '1 2:3:4' DAY TO SECOND");
-    expr("interval '99 23:59:59' day to second")
-        .ok("INTERVAL '99 23:59:59' DAY TO SECOND");
-    expr("interval '99 0:0:0' day to second")
-        .ok("INTERVAL '99 0:0:0' DAY TO SECOND");
-    expr("interval '99 23:59:59.999999' day to second")
-        .ok("INTERVAL '99 23:59:59.999999' DAY TO SECOND");
-    expr("interval '99 0:0:0.0' day to second")
-        .ok("INTERVAL '99 0:0:0.0' DAY TO SECOND");
-
-    // explicit precision equal to default
-    expr("interval '1 2:3:4' day(2) to second")
-        .ok("INTERVAL '1 2:3:4' DAY(2) TO SECOND");
-    expr("interval '99 23:59:59' day(2) to second")
-        .ok("INTERVAL '99 23:59:59' DAY(2) TO SECOND");
-    expr("interval '99 0:0:0' day(2) to second")
-        .ok("INTERVAL '99 0:0:0' DAY(2) TO SECOND");
-    expr("interval '99 23:59:59.999999' day to second(6)")
-        .ok("INTERVAL '99 23:59:59.999999' DAY TO SECOND(6)");
-    expr("interval '99 0:0:0.0' day to second(6)")
-        .ok("INTERVAL '99 0:0:0.0' DAY TO SECOND(6)");
-
-    // max precision
-    expr("interval '2147483647 23:59:59' day(10) to second")
-        .ok("INTERVAL '2147483647 23:59:59' DAY(10) TO SECOND");
-    expr("interval '2147483647 23:59:59.999999999' day(10) to second(9)")
-        .ok("INTERVAL '2147483647 23:59:59.999999999' DAY(10) TO SECOND(9)");
-
-    // min precision
-    expr("interval '0 0:0:0' day(1) to second")
-        .ok("INTERVAL '0 0:0:0' DAY(1) TO SECOND");
-    expr("interval '0 0:0:0.0' day(1) to second(1)")
-        .ok("INTERVAL '0 0:0:0.0' DAY(1) TO SECOND(1)");
-
-    // alternate precision
-    expr("interval '2345 6:7:8' day(4) to second")
-        .ok("INTERVAL '2345 6:7:8' DAY(4) TO SECOND");
-    expr("interval '2345 6:7:8.9012' day(4) to second(4)")
-        .ok("INTERVAL '2345 6:7:8.9012' DAY(4) TO SECOND(4)");
-
-    // sign
-    expr("interval '-1 2:3:4' day to second")
-        .ok("INTERVAL '-1 2:3:4' DAY TO SECOND");
-    expr("interval '+1 2:3:4' day to second")
-        .ok("INTERVAL '+1 2:3:4' DAY TO SECOND");
-    expr("interval +'1 2:3:4' day to second")
-        .ok("INTERVAL '1 2:3:4' DAY TO SECOND");
-    expr("interval +'-1 2:3:4' day to second")
-        .ok("INTERVAL '-1 2:3:4' DAY TO SECOND");
-    expr("interval +'+1 2:3:4' day to second")
-        .ok("INTERVAL '+1 2:3:4' DAY TO SECOND");
-    expr("interval -'1 2:3:4' day to second")
-        .ok("INTERVAL -'1 2:3:4' DAY TO SECOND");
-    expr("interval -'-1 2:3:4' day to second")
-        .ok("INTERVAL -'-1 2:3:4' DAY TO SECOND");
-    expr("interval -'+1 2:3:4' day to second")
-        .ok("INTERVAL -'+1 2:3:4' DAY TO SECOND");
-  }
-
-  /**
-   * Runs tests for INTERVAL... HOUR that should pass both parser and
-   * validator. A substantially identical set of tests exists in
-   * SqlValidatorTest, and any changes here should be synchronized there.
-   * Similarly, any changes to tests here should be echoed appropriately to
-   * each of the other 12 subTestIntervalXXXPositive() tests.
-   */
-  public void subTestIntervalHourPositive() {
-    // default precision
-    expr("interval '1' hour")
-        .ok("INTERVAL '1' HOUR");
-    expr("interval '99' hour")
-        .ok("INTERVAL '99' HOUR");
-
-    // explicit precision equal to default
-    expr("interval '1' hour(2)")
-        .ok("INTERVAL '1' HOUR(2)");
-    expr("interval '99' hour(2)")
-        .ok("INTERVAL '99' HOUR(2)");
-
-    // max precision
-    expr("interval '2147483647' hour(10)")
-        .ok("INTERVAL '2147483647' HOUR(10)");
-
-    // min precision
-    expr("interval '0' hour(1)")
-        .ok("INTERVAL '0' HOUR(1)");
-
-    // alternate precision
-    expr("interval '1234' hour(4)")
-        .ok("INTERVAL '1234' HOUR(4)");
-
-    // sign
-    expr("interval '+1' hour")
-        .ok("INTERVAL '+1' HOUR");
-    expr("interval '-1' hour")
-        .ok("INTERVAL '-1' HOUR");
-    expr("interval +'1' hour")
-        .ok("INTERVAL '1' HOUR");
-    expr("interval +'+1' hour")
-        .ok("INTERVAL '+1' HOUR");
-    expr("interval +'-1' hour")
-        .ok("INTERVAL '-1' HOUR");
-    expr("interval -'1' hour")
-        .ok("INTERVAL -'1' HOUR");
-    expr("interval -'+1' hour")
-        .ok("INTERVAL -'+1' HOUR");
-    expr("interval -'-1' hour")
-        .ok("INTERVAL -'-1' HOUR");
-  }
-
-  /**
-   * Runs tests for INTERVAL... HOUR TO MINUTE that should pass both parser
-   * and validator. A substantially identical set of tests exists in
-   * SqlValidatorTest, and any changes here should be synchronized there.
-   * Similarly, any changes to tests here should be echoed appropriately to
-   * each of the other 12 subTestIntervalXXXPositive() tests.
-   */
-  public void subTestIntervalHourToMinutePositive() {
-    // default precision
-    expr("interval '2:3' hour to minute")
-        .ok("INTERVAL '2:3' HOUR TO MINUTE");
-    expr("interval '23:59' hour to minute")
-        .ok("INTERVAL '23:59' HOUR TO MINUTE");
-    expr("interval '99:0' hour to minute")
-        .ok("INTERVAL '99:0' HOUR TO MINUTE");
-
-    // explicit precision equal to default
-    expr("interval '2:3' hour(2) to minute")
-        .ok("INTERVAL '2:3' HOUR(2) TO MINUTE");
-    expr("interval '23:59' hour(2) to minute")
-        .ok("INTERVAL '23:59' HOUR(2) TO MINUTE");
-    expr("interval '99:0' hour(2) to minute")
-        .ok("INTERVAL '99:0' HOUR(2) TO MINUTE");
-
-    // max precision
-    expr("interval '2147483647:59' hour(10) to minute")
-        .ok("INTERVAL '2147483647:59' HOUR(10) TO MINUTE");
-
-    // min precision
-    expr("interval '0:0' hour(1) to minute")
-        .ok("INTERVAL '0:0' HOUR(1) TO MINUTE");
-
-    // alternate precision
-    expr("interval '2345:7' hour(4) to minute")
-        .ok("INTERVAL '2345:7' HOUR(4) TO MINUTE");
-
-    // sign
-    expr("interval '-1:3' hour to minute")
-        .ok("INTERVAL '-1:3' HOUR TO MINUTE");
-    expr("interval '+1:3' hour to minute")
-        .ok("INTERVAL '+1:3' HOUR TO MINUTE");
-    expr("interval +'2:3' hour to minute")
-        .ok("INTERVAL '2:3' HOUR TO MINUTE");
-    expr("interval +'-2:3' hour to minute")
-        .ok("INTERVAL '-2:3' HOUR TO MINUTE");
-    expr("interval +'+2:3' hour to minute")
-        .ok("INTERVAL '+2:3' HOUR TO MINUTE");
-    expr("interval -'2:3' hour to minute")
-        .ok("INTERVAL -'2:3' HOUR TO MINUTE");
-    expr("interval -'-2:3' hour to minute")
-        .ok("INTERVAL -'-2:3' HOUR TO MINUTE");
-    expr("interval -'+2:3' hour to minute")
-        .ok("INTERVAL -'+2:3' HOUR TO MINUTE");
-  }
-
-  /**
-   * Runs tests for INTERVAL... HOUR TO SECOND that should pass both parser
-   * and validator. A substantially identical set of tests exists in
-   * SqlValidatorTest, and any changes here should be synchronized there.
-   * Similarly, any changes to tests here should be echoed appropriately to
-   * each of the other 12 subTestIntervalXXXPositive() tests.
-   */
-  public void subTestIntervalHourToSecondPositive() {
-    // default precision
-    expr("interval '2:3:4' hour to second")
-        .ok("INTERVAL '2:3:4' HOUR TO SECOND");
-    expr("interval '23:59:59' hour to second")
-        .ok("INTERVAL '23:59:59' HOUR TO SECOND");
-    expr("interval '99:0:0' hour to second")
-        .ok("INTERVAL '99:0:0' HOUR TO SECOND");
-    expr("interval '23:59:59.999999' hour to second")
-        .ok("INTERVAL '23:59:59.999999' HOUR TO SECOND");
-    expr("interval '99:0:0.0' hour to second")
-        .ok("INTERVAL '99:0:0.0' HOUR TO SECOND");
-
-    // explicit precision equal to default
-    expr("interval '2:3:4' hour(2) to second")
-        .ok("INTERVAL '2:3:4' HOUR(2) TO SECOND");
-    expr("interval '99:59:59' hour(2) to second")
-        .ok("INTERVAL '99:59:59' HOUR(2) TO SECOND");
-    expr("interval '99:0:0' hour(2) to second")
-        .ok("INTERVAL '99:0:0' HOUR(2) TO SECOND");
-    expr("interval '23:59:59.999999' hour to second(6)")
-        .ok("INTERVAL '23:59:59.999999' HOUR TO SECOND(6)");
-    expr("interval '99:0:0.0' hour to second(6)")
-        .ok("INTERVAL '99:0:0.0' HOUR TO SECOND(6)");
-
-    // max precision
-    expr("interval '2147483647:59:59' hour(10) to second")
-        .ok("INTERVAL '2147483647:59:59' HOUR(10) TO SECOND");
-    expr("interval '2147483647:59:59.999999999' hour(10) to second(9)")
-        .ok("INTERVAL '2147483647:59:59.999999999' HOUR(10) TO SECOND(9)");
-
-    // min precision
-    expr("interval '0:0:0' hour(1) to second")
-        .ok("INTERVAL '0:0:0' HOUR(1) TO SECOND");
-    expr("interval '0:0:0.0' hour(1) to second(1)")
-        .ok("INTERVAL '0:0:0.0' HOUR(1) TO SECOND(1)");
-
-    // alternate precision
-    expr("interval '2345:7:8' hour(4) to second")
-        .ok("INTERVAL '2345:7:8' HOUR(4) TO SECOND");
-    expr("interval '2345:7:8.9012' hour(4) to second(4)")
-        .ok("INTERVAL '2345:7:8.9012' HOUR(4) TO SECOND(4)");
-
-    // sign
-    expr("interval '-2:3:4' hour to second")
-        .ok("INTERVAL '-2:3:4' HOUR TO SECOND");
-    expr("interval '+2:3:4' hour to second")
-        .ok("INTERVAL '+2:3:4' HOUR TO SECOND");
-    expr("interval +'2:3:4' hour to second")
-        .ok("INTERVAL '2:3:4' HOUR TO SECOND");
-    expr("interval +'-2:3:4' hour to second")
-        .ok("INTERVAL '-2:3:4' HOUR TO SECOND");
-    expr("interval +'+2:3:4' hour to second")
-        .ok("INTERVAL '+2:3:4' HOUR TO SECOND");
-    expr("interval -'2:3:4' hour to second")
-        .ok("INTERVAL -'2:3:4' HOUR TO SECOND");
-    expr("interval -'-2:3:4' hour to second")
-        .ok("INTERVAL -'-2:3:4' HOUR TO SECOND");
-    expr("interval -'+2:3:4' hour to second")
-        .ok("INTERVAL -'+2:3:4' HOUR TO SECOND");
-  }
-
-  /**
-   * Runs tests for INTERVAL... MINUTE that should pass both parser and
-   * validator. A substantially identical set of tests exists in
-   * SqlValidatorTest, and any changes here should be synchronized there.
-   * Similarly, any changes to tests here should be echoed appropriately to
-   * each of the other 12 subTestIntervalXXXPositive() tests.
-   */
-  public void subTestIntervalMinutePositive() {
-    // default precision
-    expr("interval '1' minute")
-        .ok("INTERVAL '1' MINUTE");
-    expr("interval '99' minute")
-        .ok("INTERVAL '99' MINUTE");
-
-    // explicit precision equal to default
-    expr("interval '1' minute(2)")
-        .ok("INTERVAL '1' MINUTE(2)");
-    expr("interval '99' minute(2)")
-        .ok("INTERVAL '99' MINUTE(2)");
-
-    // max precision
-    expr("interval '2147483647' minute(10)")
-        .ok("INTERVAL '2147483647' MINUTE(10)");
-
-    // min precision
-    expr("interval '0' minute(1)")
-        .ok("INTERVAL '0' MINUTE(1)");
-
-    // alternate precision
-    expr("interval '1234' minute(4)")
-        .ok("INTERVAL '1234' MINUTE(4)");
-
-    // sign
-    expr("interval '+1' minute")
-        .ok("INTERVAL '+1' MINUTE");
-    expr("interval '-1' minute")
-        .ok("INTERVAL '-1' MINUTE");
-    expr("interval +'1' minute")
-        .ok("INTERVAL '1' MINUTE");
-    expr("interval +'+1' minute")
-        .ok("INTERVAL '+1' MINUTE");
-    expr("interval +'+1' minute")
-        .ok("INTERVAL '+1' MINUTE");
-    expr("interval -'1' minute")
-        .ok("INTERVAL -'1' MINUTE");
-    expr("interval -'+1' minute")
-        .ok("INTERVAL -'+1' MINUTE");
-    expr("interval -'-1' minute")
-        .ok("INTERVAL -'-1' MINUTE");
-  }
-
-  /**
-   * Runs tests for INTERVAL... MINUTE TO SECOND that should pass both parser
-   * and validator. A substantially identical set of tests exists in
-   * SqlValidatorTest, and any changes here should be synchronized there.
-   * Similarly, any changes to tests here should be echoed appropriately to
-   * each of the other 12 subTestIntervalXXXPositive() tests.
-   */
-  public void subTestIntervalMinuteToSecondPositive() {
-    // default precision
-    expr("interval '2:4' minute to second")
-        .ok("INTERVAL '2:4' MINUTE TO SECOND");
-    expr("interval '59:59' minute to second")
-        .ok("INTERVAL '59:59' MINUTE TO SECOND");
-    expr("interval '99:0' minute to second")
-        .ok("INTERVAL '99:0' MINUTE TO SECOND");
-    expr("interval '59:59.999999' minute to second")
-        .ok("INTERVAL '59:59.999999' MINUTE TO SECOND");
-    expr("interval '99:0.0' minute to second")
-        .ok("INTERVAL '99:0.0' MINUTE TO SECOND");
-
-    // explicit precision equal to default
-    expr("interval '2:4' minute(2) to second")
-        .ok("INTERVAL '2:4' MINUTE(2) TO SECOND");
-    expr("interval '59:59' minute(2) to second")
-        .ok("INTERVAL '59:59' MINUTE(2) TO SECOND");
-    expr("interval '99:0' minute(2) to second")
-        .ok("INTERVAL '99:0' MINUTE(2) TO SECOND");
-    expr("interval '99:59.999999' minute to second(6)")
-        .ok("INTERVAL '99:59.999999' MINUTE TO SECOND(6)");
-    expr("interval '99:0.0' minute to second(6)")
-        .ok("INTERVAL '99:0.0' MINUTE TO SECOND(6)");
-
-    // max precision
-    expr("interval '2147483647:59' minute(10) to second")
-        .ok("INTERVAL '2147483647:59' MINUTE(10) TO SECOND");
-    expr("interval '2147483647:59.999999999' minute(10) to second(9)")
-        .ok("INTERVAL '2147483647:59.999999999' MINUTE(10) TO SECOND(9)");
-
-    // min precision
-    expr("interval '0:0' minute(1) to second")
-        .ok("INTERVAL '0:0' MINUTE(1) TO SECOND");
-    expr("interval '0:0.0' minute(1) to second(1)")
-        .ok("INTERVAL '0:0.0' MINUTE(1) TO SECOND(1)");
-
-    // alternate precision
-    expr("interval '2345:8' minute(4) to second")
-        .ok("INTERVAL '2345:8' MINUTE(4) TO SECOND");
-    expr("interval '2345:7.8901' minute(4) to second(4)")
-        .ok("INTERVAL '2345:7.8901' MINUTE(4) TO SECOND(4)");
-
-    // sign
-    expr("interval '-3:4' minute to second")
-        .ok("INTERVAL '-3:4' MINUTE TO SECOND");
-    expr("interval '+3:4' minute to second")
-        .ok("INTERVAL '+3:4' MINUTE TO SECOND");
-    expr("interval +'3:4' minute to second")
-        .ok("INTERVAL '3:4' MINUTE TO SECOND");
-    expr("interval +'-3:4' minute to second")
-        .ok("INTERVAL '-3:4' MINUTE TO SECOND");
-    expr("interval +'+3:4' minute to second")
-        .ok("INTERVAL '+3:4' MINUTE TO SECOND");
-    expr("interval -'3:4' minute to second")
-        .ok("INTERVAL -'3:4' MINUTE TO SECOND");
-    expr("interval -'-3:4' minute to second")
-        .ok("INTERVAL -'-3:4' MINUTE TO SECOND");
-    expr("interval -'+3:4' minute to second")
-        .ok("INTERVAL -'+3:4' MINUTE TO SECOND");
-  }
-
-  /**
-   * Runs tests for INTERVAL... SECOND that should pass both parser and
-   * validator. A substantially identical set of tests exists in
-   * SqlValidatorTest, and any changes here should be synchronized there.
-   * Similarly, any changes to tests here should be echoed appropriately to
-   * each of the other 12 subTestIntervalXXXPositive() tests.
-   */
-  public void subTestIntervalSecondPositive() {
-    // default precision
-    expr("interval '1' second")
-        .ok("INTERVAL '1' SECOND");
-    expr("interval '99' second")
-        .ok("INTERVAL '99' SECOND");
-
-    // explicit precision equal to default
-    expr("interval '1' second(2)")
-        .ok("INTERVAL '1' SECOND(2)");
-    expr("interval '99' second(2)")
-        .ok("INTERVAL '99' SECOND(2)");
-    expr("interval '1' second(2,6)")
-        .ok("INTERVAL '1' SECOND(2, 6)");
-    expr("interval '99' second(2,6)")
-        .ok("INTERVAL '99' SECOND(2, 6)");
-
-    // max precision
-    expr("interval '2147483647' second(10)")
-        .ok("INTERVAL '2147483647' SECOND(10)");
-    expr("interval '2147483647.999999999' second(9,9)")
-        .ok("INTERVAL '2147483647.999999999' SECOND(9, 9)");
-
-    // min precision
-    expr("interval '0' second(1)")
-        .ok("INTERVAL '0' SECOND(1)");
-    expr("interval '0.0' second(1,1)")
-        .ok("INTERVAL '0.0' SECOND(1, 1)");
-
-    // alternate precision
-    expr("interval '1234' second(4)")
-        .ok("INTERVAL '1234' SECOND(4)");
-    expr("interval '1234.56789' second(4,5)")
-        .ok("INTERVAL '1234.56789' SECOND(4, 5)");
-
-    // sign
-    expr("interval '+1' second")
-        .ok("INTERVAL '+1' SECOND");
-    expr("interval '-1' second")
-        .ok("INTERVAL '-1' SECOND");
-    expr("interval +'1' second")
-        .ok("INTERVAL '1' SECOND");
-    expr("interval +'+1' second")
-        .ok("INTERVAL '+1' SECOND");
-    expr("interval +'-1' second")
-        .ok("INTERVAL '-1' SECOND");
-    expr("interval -'1' second")
-        .ok("INTERVAL -'1' SECOND");
-    expr("interval -'+1' second")
-        .ok("INTERVAL -'+1' SECOND");
-    expr("interval -'-1' second")
-        .ok("INTERVAL -'-1' SECOND");
-  }
-
-  /**
-   * Runs tests for INTERVAL... YEAR that should pass parser but fail
-   * validator. A substantially identical set of tests exists in
-   * SqlValidatorTest, and any changes here should be synchronized there.
-   * Similarly, any changes to tests here should be echoed appropriately to
-   * each of the other 12 subTestIntervalXXXFailsValidation() tests.
-   */
-  public void subTestIntervalYearFailsValidation() {
-    // Qualifier - field mismatches
-    expr("INTERVAL '-' YEAR").same();
-    expr("INTERVAL '1-2' YEAR").same();
-    expr("INTERVAL '1.2' YEAR").same();
-    expr("INTERVAL '1 2' YEAR").same();
-    expr("INTERVAL '1-2' YEAR(2)").same();
-    expr("INTERVAL 'bogus text' YEAR").same();
-
-    // negative field values
-    expr("INTERVAL '--1' YEAR").same();
-
-    // Field value out of range
-    //  (default, explicit default, alt, neg alt, max, neg max)
-    expr("INTERVAL '100' YEAR")
-        .ok("INTERVAL '100' YEAR");
-    expr("INTERVAL '100' YEAR(2)").same();
-    expr("INTERVAL '1000' YEAR(3)").same();
-    expr("INTERVAL '-1000' YEAR(3)").same();
-    expr("INTERVAL '2147483648' YEAR(10)").same();
-    expr("INTERVAL '-2147483648' YEAR(10)").same();
-
-    // precision > maximum
-    expr("INTERVAL '1' YEAR(11)").same();
-
-    // precision < minimum allowed)
-    // note: parser will catch negative values, here we
-    // just need to check for 0
-    expr("INTERVAL '0' YEAR(0)").same();
-  }
-
-  /**
-   * Runs tests for INTERVAL... YEAR TO MONTH that should pass parser but fail
-   * validator. A substantially identical set of tests exists in
-   * SqlValidatorTest, and any changes here should be synchronized there.
-   * Similarly, any changes to tests here should be echoed appropriately to
-   * each of the other 12 subTestIntervalXXXFailsValidation() tests.
-   */
-  public void subTestIntervalYearToMonthFailsValidation() {
-    // Qualifier - field mismatches
-    expr("INTERVAL '-' YEAR TO MONTH")
-        .ok("INTERVAL '-' YEAR TO MONTH");
-    expr("INTERVAL '1' YEAR TO MONTH")
-        .ok("INTERVAL '1' YEAR TO MONTH");
-    expr("INTERVAL '1:2' YEAR TO MONTH")
-        .ok("INTERVAL '1:2' YEAR TO MONTH");
-    expr("INTERVAL '1.2' YEAR TO MONTH")
-        .ok("INTERVAL '1.2' YEAR TO MONTH");
-    expr("INTERVAL '1 2' YEAR TO MONTH")
-        .ok("INTERVAL '1 2' YEAR TO MONTH");
-    expr("INTERVAL '1:2' YEAR(2) TO MONTH")
-        .ok("INTERVAL '1:2' YEAR(2) TO MONTH");
-    expr("INTERVAL 'bogus text' YEAR TO MONTH")
-        .ok("INTERVAL 'bogus text' YEAR TO MONTH");
-
-    // negative field values
-    expr("INTERVAL '--1-2' YEAR TO MONTH")
-        .ok("INTERVAL '--1-2' YEAR TO MONTH");
-    expr("INTERVAL '1--2' YEAR TO MONTH")
-        .ok("INTERVAL '1--2' YEAR TO MONTH");
-
-    // Field value out of range
-    //  (default, explicit default, alt, neg alt, max, neg max)
-    //  plus >max value for mid/end fields
-    expr("INTERVAL '100-0' YEAR TO MONTH")
-        .ok("INTERVAL '100-0' YEAR TO MONTH");
-    expr("INTERVAL '100-0' YEAR(2) TO MONTH")
-        .ok("INTERVAL '100-0' YEAR(2) TO MONTH");
-    expr("INTERVAL '1000-0' YEAR(3) TO MONTH")
-        .ok("INTERVAL '1000-0' YEAR(3) TO MONTH");
-    expr("INTERVAL '-1000-0' YEAR(3) TO MONTH")
-        .ok("INTERVAL '-1000-0' YEAR(3) TO MONTH");
-    expr("INTERVAL '2147483648-0' YEAR(10) TO MONTH")
-        .ok("INTERVAL '2147483648-0' YEAR(10) TO MONTH");
-    expr("INTERVAL '-2147483648-0' YEAR(10) TO MONTH")
-        .ok("INTERVAL '-2147483648-0' YEAR(10) TO MONTH");
-    expr("INTERVAL '1-12' YEAR TO MONTH")
-        .ok("INTERVAL '1-12' YEAR TO MONTH");
-
-    // precision > maximum
-    expr("INTERVAL '1-1' YEAR(11) TO MONTH")
-        .ok("INTERVAL '1-1' YEAR(11) TO MONTH");
-
-    // precision < minimum allowed)
-    // note: parser will catch negative values, here we
-    // just need to check for 0
-    expr("INTERVAL '0-0' YEAR(0) TO MONTH")
-        .ok("INTERVAL '0-0' YEAR(0) TO MONTH");
-  }
-
-  /**
-   * Runs tests for INTERVAL... MONTH that should pass parser but fail
-   * validator. A substantially identical set of tests exists in
-   * SqlValidatorTest, and any changes here should be synchronized there.
-   * Similarly, any changes to tests here should be echoed appropriately to
-   * each of the other 12 subTestIntervalXXXFailsValidation() tests.
-   */
-  public void subTestIntervalMonthFailsValidation() {
-    // Qualifier - field mismatches
-    expr("INTERVAL '-' MONTH")
-        .ok("INTERVAL '-' MONTH");
-    expr("INTERVAL '1-2' MONTH")
-        .ok("INTERVAL '1-2' MONTH");
-    expr("INTERVAL '1.2' MONTH")
-        .ok("INTERVAL '1.2' MONTH");
-    expr("INTERVAL '1 2' MONTH")
-        .ok("INTERVAL '1 2' MONTH");
-    expr("INTERVAL '1-2' MONTH(2)")
-        .ok("INTERVAL '1-2' MONTH(2)");
-    expr("INTERVAL 'bogus text' MONTH")
-        .ok("INTERVAL 'bogus text' MONTH");
-
-    // negative field values
-    expr("INTERVAL '--1' MONTH")
-        .ok("INTERVAL '--1' MONTH");
-
-    // Field value out of range
-    //  (default, explicit default, alt, neg alt, max, neg max)
-    expr("INTERVAL '100' MONTH")
-        .ok("INTERVAL '100' MONTH");
-    expr("INTERVAL '100' MONTH(2)")
-        .ok("INTERVAL '100' MONTH(2)");
-    expr("INTERVAL '1000' MONTH(3)")
-        .ok("INTERVAL '1000' MONTH(3)");
-    expr("INTERVAL '-1000' MONTH(3)")
-        .ok("INTERVAL '-1000' MONTH(3)");
-    expr("INTERVAL '2147483648' MONTH(10)")
-        .ok("INTERVAL '2147483648' MONTH(10)");
-    expr("INTERVAL '-2147483648' MONTH(10)")
-        .ok("INTERVAL '-2147483648' MONTH(10)");
-
-    // precision > maximum
-    expr("INTERVAL '1' MONTH(11)")
-        .ok("INTERVAL '1' MONTH(11)");
-
-    // precision < minimum allowed)
-    // note: parser will catch negative values, here we
-    // just need to check for 0
-    expr("INTERVAL '0' MONTH(0)")
-        .ok("INTERVAL '0' MONTH(0)");
-  }
-
-  /**
-   * Runs tests for INTERVAL... DAY that should pass parser but fail
-   * validator. A substantially identical set of tests exists in
-   * SqlValidatorTest, and any changes here should be synchronized there.
-   * Similarly, any changes to tests here should be echoed appropriately to
-   * each of the other 12 subTestIntervalXXXFailsValidation() tests.
-   */
-  public void subTestIntervalDayFailsValidation() {
-    // Qualifier - field mismatches
-    expr("INTERVAL '-' DAY")
-        .ok("INTERVAL '-' DAY");
-    expr("INTERVAL '1-2' DAY")
-        .ok("INTERVAL '1-2' DAY");
-    expr("INTERVAL '1.2' DAY")
-        .ok("INTERVAL '1.2' DAY");
-    expr("INTERVAL '1 2' DAY")
-        .ok("INTERVAL '1 2' DAY");
-    expr("INTERVAL '1:2' DAY")
-        .ok("INTERVAL '1:2' DAY");
-    expr("INTERVAL '1-2' DAY(2)")
-        .ok("INTERVAL '1-2' DAY(2)");
-    expr("INTERVAL 'bogus text' DAY")
-        .ok("INTERVAL 'bogus text' DAY");
-
-    // negative field values
-    expr("INTERVAL '--1' DAY")
-        .ok("INTERVAL '--1' DAY");
-
-    // Field value out of range
-    //  (default, explicit default, alt, neg alt, max, neg max)
-    expr("INTERVAL '100' DAY")
-        .ok("INTERVAL '100' DAY");
-    expr("INTERVAL '100' DAY(2)")
-        .ok("INTERVAL '100' DAY(2)");
-    expr("INTERVAL '1000' DAY(3)")
-        .ok("INTERVAL '1000' DAY(3)");
-    expr("INTERVAL '-1000' DAY(3)")
-        .ok("INTERVAL '-1000' DAY(3)");
-    expr("INTERVAL '2147483648' DAY(10)")
-        .ok("INTERVAL '2147483648' DAY(10)");
-    expr("INTERVAL '-2147483648' DAY(10)")
-        .ok("INTERVAL '-2147483648' DAY(10)");
-
-    // precision > maximum
-    expr("INTERVAL '1' DAY(11)")
-        .ok("INTERVAL '1' DAY(11)");
-
-    // precision < minimum allowed)
-    // note: parser will catch negative values, here we
-    // just need to check for 0
-    expr("INTERVAL '0' DAY(0)")
-        .ok("INTERVAL '0' DAY(0)");
-  }
-
   @Test void testVisitSqlInsertWithSqlShuttle() {
     final String sql = "insert into emps select * from emps";
     final SqlNode sqlNode = sql(sql).node();
@@ -6840,646 +5893,6 @@ public class SqlParserTest {
     assertNotSame(sqlNodeVisited, sqlNode);
   }
 
-  /**
-   * Runs tests for INTERVAL... DAY TO HOUR that should pass parser but fail
-   * validator. A substantially identical set of tests exists in
-   * SqlValidatorTest, and any changes here should be synchronized there.
-   * Similarly, any changes to tests here should be echoed appropriately to
-   * each of the other 12 subTestIntervalXXXFailsValidation() tests.
-   */
-  public void subTestIntervalDayToHourFailsValidation() {
-    // Qualifier - field mismatches
-    expr("INTERVAL '-' DAY TO HOUR")
-        .ok("INTERVAL '-' DAY TO HOUR");
-    expr("INTERVAL '1' DAY TO HOUR")
-        .ok("INTERVAL '1' DAY TO HOUR");
-    expr("INTERVAL '1:2' DAY TO HOUR")
-        .ok("INTERVAL '1:2' DAY TO HOUR");
-    expr("INTERVAL '1.2' DAY TO HOUR")
-        .ok("INTERVAL '1.2' DAY TO HOUR");
-    expr("INTERVAL '1 x' DAY TO HOUR")
-        .ok("INTERVAL '1 x' DAY TO HOUR");
-    expr("INTERVAL ' ' DAY TO HOUR")
-        .ok("INTERVAL ' ' DAY TO HOUR");
-    expr("INTERVAL '1:2' DAY(2) TO HOUR")
-        .ok("INTERVAL '1:2' DAY(2) TO HOUR");
-    expr("INTERVAL 'bogus text' DAY TO HOUR")
-        .ok("INTERVAL 'bogus text' DAY TO HOUR");
-
-    // negative field values
-    expr("INTERVAL '--1 1' DAY TO HOUR")
-        .ok("INTERVAL '--1 1' DAY TO HOUR");
-    expr("INTERVAL '1 -1' DAY TO HOUR")
-        .ok("INTERVAL '1 -1' DAY TO HOUR");
-
-    // Field value out of range
-    //  (default, explicit default, alt, neg alt, max, neg max)
-    //  plus >max value for mid/end fields
-    expr("INTERVAL '100 0' DAY TO HOUR")
-        .ok("INTERVAL '100 0' DAY TO HOUR");
-    expr("INTERVAL '100 0' DAY(2) TO HOUR")
-        .ok("INTERVAL '100 0' DAY(2) TO HOUR");
-    expr("INTERVAL '1000 0' DAY(3) TO HOUR")
-        .ok("INTERVAL '1000 0' DAY(3) TO HOUR");
-    expr("INTERVAL '-1000 0' DAY(3) TO HOUR")
-        .ok("INTERVAL '-1000 0' DAY(3) TO HOUR");
-    expr("INTERVAL '2147483648 0' DAY(10) TO HOUR")
-        .ok("INTERVAL '2147483648 0' DAY(10) TO HOUR");
-    expr("INTERVAL '-2147483648 0' DAY(10) TO HOUR")
-        .ok("INTERVAL '-2147483648 0' DAY(10) TO HOUR");
-    expr("INTERVAL '1 24' DAY TO HOUR")
-        .ok("INTERVAL '1 24' DAY TO HOUR");
-
-    // precision > maximum
-    expr("INTERVAL '1 1' DAY(11) TO HOUR")
-        .ok("INTERVAL '1 1' DAY(11) TO HOUR");
-
-    // precision < minimum allowed)
-    // note: parser will catch negative values, here we
-    // just need to check for 0
-    expr("INTERVAL '0 0' DAY(0) TO HOUR")
-        .ok("INTERVAL '0 0' DAY(0) TO HOUR");
-  }
-
-  /**
-   * Runs tests for INTERVAL... DAY TO MINUTE that should pass parser but fail
-   * validator. A substantially identical set of tests exists in
-   * SqlValidatorTest, and any changes here should be synchronized there.
-   * Similarly, any changes to tests here should be echoed appropriately to
-   * each of the other 12 subTestIntervalXXXFailsValidation() tests.
-   */
-  public void subTestIntervalDayToMinuteFailsValidation() {
-    // Qualifier - field mismatches
-    expr("INTERVAL ' :' DAY TO MINUTE")
-        .ok("INTERVAL ' :' DAY TO MINUTE");
-    expr("INTERVAL '1' DAY TO MINUTE")
-        .ok("INTERVAL '1' DAY TO MINUTE");
-    expr("INTERVAL '1 2' DAY TO MINUTE")
-        .ok("INTERVAL '1 2' DAY TO MINUTE");
-    expr("INTERVAL '1:2' DAY TO MINUTE")
-        .ok("INTERVAL '1:2' DAY TO MINUTE");
-    expr("INTERVAL '1.2' DAY TO MINUTE")
-        .ok("INTERVAL '1.2' DAY TO MINUTE");
-    expr("INTERVAL 'x 1:1' DAY TO MINUTE")
-        .ok("INTERVAL 'x 1:1' DAY TO MINUTE");
-    expr("INTERVAL '1 x:1' DAY TO MINUTE")
-        .ok("INTERVAL '1 x:1' DAY TO MINUTE");
-    expr("INTERVAL '1 1:x' DAY TO MINUTE")
-        .ok("INTERVAL '1 1:x' DAY TO MINUTE");
-    expr("INTERVAL '1 1:2:3' DAY TO MINUTE")
-        .ok("INTERVAL '1 1:2:3' DAY TO MINUTE");
-    expr("INTERVAL '1 1:1:1.2' DAY TO MINUTE")
-        .ok("INTERVAL '1 1:1:1.2' DAY TO MINUTE");
-    expr("INTERVAL '1 1:2:3' DAY(2) TO MINUTE")
-        .ok("INTERVAL '1 1:2:3' DAY(2) TO MINUTE");
-    expr("INTERVAL '1 1' DAY(2) TO MINUTE")
-        .ok("INTERVAL '1 1' DAY(2) TO MINUTE");
-    expr("INTERVAL 'bogus text' DAY TO MINUTE")
-        .ok("INTERVAL 'bogus text' DAY TO MINUTE");
-
-    // negative field values
-    expr("INTERVAL '--1 1:1' DAY TO MINUTE")
-        .ok("INTERVAL '--1 1:1' DAY TO MINUTE");
-    expr("INTERVAL '1 -1:1' DAY TO MINUTE")
-        .ok("INTERVAL '1 -1:1' DAY TO MINUTE");
-    expr("INTERVAL '1 1:-1' DAY TO MINUTE")
-        .ok("INTERVAL '1 1:-1' DAY TO MINUTE");
-
-    // Field value out of range
-    //  (default, explicit default, alt, neg alt, max, neg max)
-    //  plus >max value for mid/end fields
-    expr("INTERVAL '100 0' DAY TO MINUTE")
-        .ok("INTERVAL '100 0' DAY TO MINUTE");
-    expr("INTERVAL '100 0' DAY(2) TO MINUTE")
-        .ok("INTERVAL '100 0' DAY(2) TO MINUTE");
-    expr("INTERVAL '1000 0' DAY(3) TO MINUTE")
-        .ok("INTERVAL '1000 0' DAY(3) TO MINUTE");
-    expr("INTERVAL '-1000 0' DAY(3) TO MINUTE")
-        .ok("INTERVAL '-1000 0' DAY(3) TO MINUTE");
-    expr("INTERVAL '2147483648 0' DAY(10) TO MINUTE")
-        .ok("INTERVAL '2147483648 0' DAY(10) TO MINUTE");
-    expr("INTERVAL '-2147483648 0' DAY(10) TO MINUTE")
-        .ok("INTERVAL '-2147483648 0' DAY(10) TO MINUTE");
-    expr("INTERVAL '1 24:1' DAY TO MINUTE")
-        .ok("INTERVAL '1 24:1' DAY TO MINUTE");
-    expr("INTERVAL '1 1:60' DAY TO MINUTE")
-        .ok("INTERVAL '1 1:60' DAY TO MINUTE");
-
-    // precision > maximum
-    expr("INTERVAL '1 1' DAY(11) TO MINUTE")
-        .ok("INTERVAL '1 1' DAY(11) TO MINUTE");
-
-    // precision < minimum allowed)
-    // note: parser will catch negative values, here we
-    // just need to check for 0
-    expr("INTERVAL '0 0' DAY(0) TO MINUTE")
-        .ok("INTERVAL '0 0' DAY(0) TO MINUTE");
-  }
-
-  /**
-   * Runs tests for INTERVAL... DAY TO SECOND that should pass parser but fail
-   * validator. A substantially identical set of tests exists in
-   * SqlValidatorTest, and any changes here should be synchronized there.
-   * Similarly, any changes to tests here should be echoed appropriately to
-   * each of the other 12 subTestIntervalXXXFailsValidation() tests.
-   */
-  public void subTestIntervalDayToSecondFailsValidation() {
-    // Qualifier - field mismatches
-    expr("INTERVAL ' ::' DAY TO SECOND")
-        .ok("INTERVAL ' ::' DAY TO SECOND");
-    expr("INTERVAL ' ::.' DAY TO SECOND")
-        .ok("INTERVAL ' ::.' DAY TO SECOND");
-    expr("INTERVAL '1' DAY TO SECOND")
-        .ok("INTERVAL '1' DAY TO SECOND");
-    expr("INTERVAL '1 2' DAY TO SECOND")
-        .ok("INTERVAL '1 2' DAY TO SECOND");
-    expr("INTERVAL '1:2' DAY TO SECOND")
-        .ok("INTERVAL '1:2' DAY TO SECOND");
-    expr("INTERVAL '1.2' DAY TO SECOND")
-        .ok("INTERVAL '1.2' DAY TO SECOND");
-    expr("INTERVAL '1 1:2' DAY TO SECOND")
-        .ok("INTERVAL '1 1:2' DAY TO SECOND");
-    expr("INTERVAL '1 1:2:x' DAY TO SECOND")
-        .ok("INTERVAL '1 1:2:x' DAY TO SECOND");
-    expr("INTERVAL '1:2:3' DAY TO SECOND")
-        .ok("INTERVAL '1:2:3' DAY TO SECOND");
-    expr("INTERVAL '1:1:1.2' DAY TO SECOND")
-        .ok("INTERVAL '1:1:1.2' DAY TO SECOND");
-    expr("INTERVAL '1 1:2' DAY(2) TO SECOND")
-        .ok("INTERVAL '1 1:2' DAY(2) TO SECOND");
-    expr("INTERVAL '1 1' DAY(2) TO SECOND")
-        .ok("INTERVAL '1 1' DAY(2) TO SECOND");
-    expr("INTERVAL 'bogus text' DAY TO SECOND")
-        .ok("INTERVAL 'bogus text' DAY TO SECOND");
-    expr("INTERVAL '2345 6:7:8901' DAY TO SECOND(4)")
-        .ok("INTERVAL '2345 6:7:8901' DAY TO SECOND(4)");
-
-    // negative field values
-    expr("INTERVAL '--1 1:1:1' DAY TO SECOND")
-        .ok("INTERVAL '--1 1:1:1' DAY TO SECOND");
-    expr("INTERVAL '1 -1:1:1' DAY TO SECOND")
-        .ok("INTERVAL '1 -1:1:1' DAY TO SECOND");
-    expr("INTERVAL '1 1:-1:1' DAY TO SECOND")
-        .ok("INTERVAL '1 1:-1:1' DAY TO SECOND");
-    expr("INTERVAL '1 1:1:-1' DAY TO SECOND")
-        .ok("INTERVAL '1 1:1:-1' DAY TO SECOND");
-    expr("INTERVAL '1 1:1:1.-1' DAY TO SECOND")
-        .ok("INTERVAL '1 1:1:1.-1' DAY TO SECOND");
-
-    // Field value out of range
-    //  (default, explicit default, alt, neg alt, max, neg max)
-    //  plus >max value for mid/end fields
-    expr("INTERVAL '100 0' DAY TO SECOND")
-        .ok("INTERVAL '100 0' DAY TO SECOND");
-    expr("INTERVAL '100 0' DAY(2) TO SECOND")
-        .ok("INTERVAL '100 0' DAY(2) TO SECOND");
-    expr("INTERVAL '1000 0' DAY(3) TO SECOND")
-        .ok("INTERVAL '1000 0' DAY(3) TO SECOND");
-    expr("INTERVAL '-1000 0' DAY(3) TO SECOND")
-        .ok("INTERVAL '-1000 0' DAY(3) TO SECOND");
-    expr("INTERVAL '2147483648 0' DAY(10) TO SECOND")
-        .ok("INTERVAL '2147483648 0' DAY(10) TO SECOND");
-    expr("INTERVAL '-2147483648 0' DAY(10) TO SECOND")
-        .ok("INTERVAL '-2147483648 0' DAY(10) TO SECOND");
-    expr("INTERVAL '1 24:1:1' DAY TO SECOND")
-        .ok("INTERVAL '1 24:1:1' DAY TO SECOND");
-    expr("INTERVAL '1 1:60:1' DAY TO SECOND")
-        .ok("INTERVAL '1 1:60:1' DAY TO SECOND");
-    expr("INTERVAL '1 1:1:60' DAY TO SECOND")
-        .ok("INTERVAL '1 1:1:60' DAY TO SECOND");
-    expr("INTERVAL '1 1:1:1.0000001' DAY TO SECOND")
-        .ok("INTERVAL '1 1:1:1.0000001' DAY TO SECOND");
-    expr("INTERVAL '1 1:1:1.0001' DAY TO SECOND(3)")
-        .ok("INTERVAL '1 1:1:1.0001' DAY TO SECOND(3)");
-
-    // precision > maximum
-    expr("INTERVAL '1 1' DAY(11) TO SECOND")
-        .ok("INTERVAL '1 1' DAY(11) TO SECOND");
-    expr("INTERVAL '1 1' DAY TO SECOND(10)")
-        .ok("INTERVAL '1 1' DAY TO SECOND(10)");
-
-    // precision < minimum allowed)
-    // note: parser will catch negative values, here we
-    // just need to check for 0
-    expr("INTERVAL '0 0:0:0' DAY(0) TO SECOND")
-        .ok("INTERVAL '0 0:0:0' DAY(0) TO SECOND");
-    expr("INTERVAL '0 0:0:0' DAY TO SECOND(0)")
-        .ok("INTERVAL '0 0:0:0' DAY TO SECOND(0)");
-  }
-
-  /**
-   * Runs tests for INTERVAL... HOUR that should pass parser but fail
-   * validator. A substantially identical set of tests exists in
-   * SqlValidatorTest, and any changes here should be synchronized there.
-   * Similarly, any changes to tests here should be echoed appropriately to
-   * each of the other 12 subTestIntervalXXXFailsValidation() tests.
-   */
-  public void subTestIntervalHourFailsValidation() {
-    // Qualifier - field mismatches
-    expr("INTERVAL '-' HOUR")
-        .ok("INTERVAL '-' HOUR");
-    expr("INTERVAL '1-2' HOUR")
-        .ok("INTERVAL '1-2' HOUR");
-    expr("INTERVAL '1.2' HOUR")
-        .ok("INTERVAL '1.2' HOUR");
-    expr("INTERVAL '1 2' HOUR")
-        .ok("INTERVAL '1 2' HOUR");
-    expr("INTERVAL '1:2' HOUR")
-        .ok("INTERVAL '1:2' HOUR");
-    expr("INTERVAL '1-2' HOUR(2)")
-        .ok("INTERVAL '1-2' HOUR(2)");
-    expr("INTERVAL 'bogus text' HOUR")
-        .ok("INTERVAL 'bogus text' HOUR");
-
-    // negative field values
-    expr("INTERVAL '--1' HOUR")
-        .ok("INTERVAL '--1' HOUR");
-
-    // Field value out of range
-    //  (default, explicit default, alt, neg alt, max, neg max)
-    expr("INTERVAL '100' HOUR")
-        .ok("INTERVAL '100' HOUR");
-    expr("INTERVAL '100' HOUR(2)")
-        .ok("INTERVAL '100' HOUR(2)");
-    expr("INTERVAL '1000' HOUR(3)")
-        .ok("INTERVAL '1000' HOUR(3)");
-    expr("INTERVAL '-1000' HOUR(3)")
-        .ok("INTERVAL '-1000' HOUR(3)");
-    expr("INTERVAL '2147483648' HOUR(10)")
-        .ok("INTERVAL '2147483648' HOUR(10)");
-    expr("INTERVAL '-2147483648' HOUR(10)")
-        .ok("INTERVAL '-2147483648' HOUR(10)");
-
-    // negative field values
-    expr("INTERVAL '--1' HOUR")
-        .ok("INTERVAL '--1' HOUR");
-
-    // precision > maximum
-    expr("INTERVAL '1' HOUR(11)")
-        .ok("INTERVAL '1' HOUR(11)");
-
-    // precision < minimum allowed)
-    // note: parser will catch negative values, here we
-    // just need to check for 0
-    expr("INTERVAL '0' HOUR(0)")
-        .ok("INTERVAL '0' HOUR(0)");
-  }
-
-  /**
-   * Runs tests for INTERVAL... HOUR TO MINUTE that should pass parser but
-   * fail validator. A substantially identical set of tests exists in
-   * SqlValidatorTest, and any changes here should be synchronized there.
-   * Similarly, any changes to tests here should be echoed appropriately to
-   * each of the other 12 subTestIntervalXXXFailsValidation() tests.
-   */
-  public void subTestIntervalHourToMinuteFailsValidation() {
-    // Qualifier - field mismatches
-    expr("INTERVAL ':' HOUR TO MINUTE")
-        .ok("INTERVAL ':' HOUR TO MINUTE");
-    expr("INTERVAL '1' HOUR TO MINUTE")
-        .ok("INTERVAL '1' HOUR TO MINUTE");
-    expr("INTERVAL '1:x' HOUR TO MINUTE")
-        .ok("INTERVAL '1:x' HOUR TO MINUTE");
-    expr("INTERVAL '1.2' HOUR TO MINUTE")
-        .ok("INTERVAL '1.2' HOUR TO MINUTE");
-    expr("INTERVAL '1 2' HOUR TO MINUTE")
-        .ok("INTERVAL '1 2' HOUR TO MINUTE");
-    expr("INTERVAL '1:2:3' HOUR TO MINUTE")
-        .ok("INTERVAL '1:2:3' HOUR TO MINUTE");
-    expr("INTERVAL '1 2' HOUR(2) TO MINUTE")
-        .ok("INTERVAL '1 2' HOUR(2) TO MINUTE");
-    expr("INTERVAL 'bogus text' HOUR TO MINUTE")
-        .ok("INTERVAL 'bogus text' HOUR TO MINUTE");
-
-    // negative field values
-    expr("INTERVAL '--1:1' HOUR TO MINUTE")
-        .ok("INTERVAL '--1:1' HOUR TO MINUTE");
-    expr("INTERVAL '1:-1' HOUR TO MINUTE")
-        .ok("INTERVAL '1:-1' HOUR TO MINUTE");
-
-    // Field value out of range
-    //  (default, explicit default, alt, neg alt, max, neg max)
-    //  plus >max value for mid/end fields
-    expr("INTERVAL '100:0' HOUR TO MINUTE")
-        .ok("INTERVAL '100:0' HOUR TO MINUTE");
-    expr("INTERVAL '100:0' HOUR(2) TO MINUTE")
-        .ok("INTERVAL '100:0' HOUR(2) TO MINUTE");
-    expr("INTERVAL '1000:0' HOUR(3) TO MINUTE")
-        .ok("INTERVAL '1000:0' HOUR(3) TO MINUTE");
-    expr("INTERVAL '-1000:0' HOUR(3) TO MINUTE")
-        .ok("INTERVAL '-1000:0' HOUR(3) TO MINUTE");
-    expr("INTERVAL '2147483648:0' HOUR(10) TO MINUTE")
-        .ok("INTERVAL '2147483648:0' HOUR(10) TO MINUTE");
-    expr("INTERVAL '-2147483648:0' HOUR(10) TO MINUTE")
-        .ok("INTERVAL '-2147483648:0' HOUR(10) TO MINUTE");
-    expr("INTERVAL '1:24' HOUR TO MINUTE")
-        .ok("INTERVAL '1:24' HOUR TO MINUTE");
-
-    // precision > maximum
-    expr("INTERVAL '1:1' HOUR(11) TO MINUTE")
-        .ok("INTERVAL '1:1' HOUR(11) TO MINUTE");
-
-    // precision < minimum allowed)
-    // note: parser will catch negative values, here we
-    // just need to check for 0
-    expr("INTERVAL '0:0' HOUR(0) TO MINUTE")
-        .ok("INTERVAL '0:0' HOUR(0) TO MINUTE");
-  }
-
-  /**
-   * Runs tests for INTERVAL... HOUR TO SECOND that should pass parser but
-   * fail validator. A substantially identical set of tests exists in
-   * SqlValidatorTest, and any changes here should be synchronized there.
-   * Similarly, any changes to tests here should be echoed appropriately to
-   * each of the other 12 subTestIntervalXXXFailsValidation() tests.
-   */
-  public void subTestIntervalHourToSecondFailsValidation() {
-    // Qualifier - field mismatches
-    expr("INTERVAL '::' HOUR TO SECOND")
-        .ok("INTERVAL '::' HOUR TO SECOND");
-    expr("INTERVAL '::.' HOUR TO SECOND")
-        .ok("INTERVAL '::.' HOUR TO SECOND");
-    expr("INTERVAL '1' HOUR TO SECOND")
-        .ok("INTERVAL '1' HOUR TO SECOND");
-    expr("INTERVAL '1 2' HOUR TO SECOND")
-        .ok("INTERVAL '1 2' HOUR TO SECOND");
-    expr("INTERVAL '1:2' HOUR TO SECOND")
-        .ok("INTERVAL '1:2' HOUR TO SECOND");
-    expr("INTERVAL '1.2' HOUR TO SECOND")
-        .ok("INTERVAL '1.2' HOUR TO SECOND");
-    expr("INTERVAL '1 1:2' HOUR TO SECOND")
-        .ok("INTERVAL '1 1:2' HOUR TO SECOND");
-    expr("INTERVAL '1:2:x' HOUR TO SECOND")
-        .ok("INTERVAL '1:2:x' HOUR TO SECOND");
-    expr("INTERVAL '1:x:3' HOUR TO SECOND")
-        .ok("INTERVAL '1:x:3' HOUR TO SECOND");
-    expr("INTERVAL '1:1:1.x' HOUR TO SECOND")
-        .ok("INTERVAL '1:1:1.x' HOUR TO SECOND");
-    expr("INTERVAL '1 1:2' HOUR(2) TO SECOND")
-        .ok("INTERVAL '1 1:2' HOUR(2) TO SECOND");
-    expr("INTERVAL '1 1' HOUR(2) TO SECOND")
-        .ok("INTERVAL '1 1' HOUR(2) TO SECOND");
-    expr("INTERVAL 'bogus text' HOUR TO SECOND")
-        .ok("INTERVAL 'bogus text' HOUR TO SECOND");
-    expr("INTERVAL '6:7:8901' HOUR TO SECOND(4)")
-        .ok("INTERVAL '6:7:8901' HOUR TO SECOND(4)");
-
-    // negative field values
-    expr("INTERVAL '--1:1:1' HOUR TO SECOND")
-        .ok("INTERVAL '--1:1:1' HOUR TO SECOND");
-    expr("INTERVAL '1:-1:1' HOUR TO SECOND")
-        .ok("INTERVAL '1:-1:1' HOUR TO SECOND");
-    expr("INTERVAL '1:1:-1' HOUR TO SECOND")
-        .ok("INTERVAL '1:1:-1' HOUR TO SECOND");
-    expr("INTERVAL '1:1:1.-1' HOUR TO SECOND")
-        .ok("INTERVAL '1:1:1.-1' HOUR TO SECOND");
-
-    // Field value out of range
-    //  (default, explicit default, alt, neg alt, max, neg max)
-    //  plus >max value for mid/end fields
-    expr("INTERVAL '100:0:0' HOUR TO SECOND")
-        .ok("INTERVAL '100:0:0' HOUR TO SECOND");
-    expr("INTERVAL '100:0:0' HOUR(2) TO SECOND")
-        .ok("INTERVAL '100:0:0' HOUR(2) TO SECOND");
-    expr("INTERVAL '1000:0:0' HOUR(3) TO SECOND")
-        .ok("INTERVAL '1000:0:0' HOUR(3) TO SECOND");
-    expr("INTERVAL '-1000:0:0' HOUR(3) TO SECOND")
-        .ok("INTERVAL '-1000:0:0' HOUR(3) TO SECOND");
-    expr("INTERVAL '2147483648:0:0' HOUR(10) TO SECOND")
-        .ok("INTERVAL '2147483648:0:0' HOUR(10) TO SECOND");
-    expr("INTERVAL '-2147483648:0:0' HOUR(10) TO SECOND")
-        .ok("INTERVAL '-2147483648:0:0' HOUR(10) TO SECOND");
-    expr("INTERVAL '1:60:1' HOUR TO SECOND")
-        .ok("INTERVAL '1:60:1' HOUR TO SECOND");
-    expr("INTERVAL '1:1:60' HOUR TO SECOND")
-        .ok("INTERVAL '1:1:60' HOUR TO SECOND");
-    expr("INTERVAL '1:1:1.0000001' HOUR TO SECOND")
-        .ok("INTERVAL '1:1:1.0000001' HOUR TO SECOND");
-    expr("INTERVAL '1:1:1.0001' HOUR TO SECOND(3)")
-        .ok("INTERVAL '1:1:1.0001' HOUR TO SECOND(3)");
-
-    // precision > maximum
-    expr("INTERVAL '1:1:1' HOUR(11) TO SECOND")
-        .ok("INTERVAL '1:1:1' HOUR(11) TO SECOND");
-    expr("INTERVAL '1:1:1' HOUR TO SECOND(10)")
-        .ok("INTERVAL '1:1:1' HOUR TO SECOND(10)");
-
-    // precision < minimum allowed)
-    // note: parser will catch negative values, here we
-    // just need to check for 0
-    expr("INTERVAL '0:0:0' HOUR(0) TO SECOND")
-        .ok("INTERVAL '0:0:0' HOUR(0) TO SECOND");
-    expr("INTERVAL '0:0:0' HOUR TO SECOND(0)")
-        .ok("INTERVAL '0:0:0' HOUR TO SECOND(0)");
-  }
-
-  /**
-   * Runs tests for INTERVAL... MINUTE that should pass parser but fail
-   * validator. A substantially identical set of tests exists in
-   * SqlValidatorTest, and any changes here should be synchronized there.
-   * Similarly, any changes to tests here should be echoed appropriately to
-   * each of the other 12 subTestIntervalXXXFailsValidation() tests.
-   */
-  public void subTestIntervalMinuteFailsValidation() {
-    // Qualifier - field mismatches
-    expr("INTERVAL '-' MINUTE")
-        .ok("INTERVAL '-' MINUTE");
-    expr("INTERVAL '1-2' MINUTE")
-        .ok("INTERVAL '1-2' MINUTE");
-    expr("INTERVAL '1.2' MINUTE")
-        .ok("INTERVAL '1.2' MINUTE");
-    expr("INTERVAL '1 2' MINUTE")
-        .ok("INTERVAL '1 2' MINUTE");
-    expr("INTERVAL '1:2' MINUTE")
-        .ok("INTERVAL '1:2' MINUTE");
-    expr("INTERVAL '1-2' MINUTE(2)")
-        .ok("INTERVAL '1-2' MINUTE(2)");
-    expr("INTERVAL 'bogus text' MINUTE")
-        .ok("INTERVAL 'bogus text' MINUTE");
-
-    // negative field values
-    expr("INTERVAL '--1' MINUTE")
-        .ok("INTERVAL '--1' MINUTE");
-
-    // Field value out of range
-    //  (default, explicit default, alt, neg alt, max, neg max)
-    expr("INTERVAL '100' MINUTE")
-        .ok("INTERVAL '100' MINUTE");
-    expr("INTERVAL '100' MINUTE(2)")
-        .ok("INTERVAL '100' MINUTE(2)");
-    expr("INTERVAL '1000' MINUTE(3)")
-        .ok("INTERVAL '1000' MINUTE(3)");
-    expr("INTERVAL '-1000' MINUTE(3)")
-        .ok("INTERVAL '-1000' MINUTE(3)");
-    expr("INTERVAL '2147483648' MINUTE(10)")
-        .ok("INTERVAL '2147483648' MINUTE(10)");
-    expr("INTERVAL '-2147483648' MINUTE(10)")
-        .ok("INTERVAL '-2147483648' MINUTE(10)");
-
-    // precision > maximum
-    expr("INTERVAL '1' MINUTE(11)")
-        .ok("INTERVAL '1' MINUTE(11)");
-
-    // precision < minimum allowed)
-    // note: parser will catch negative values, here we
-    // just need to check for 0
-    expr("INTERVAL '0' MINUTE(0)")
-        .ok("INTERVAL '0' MINUTE(0)");
-  }
-
-  /**
-   * Runs tests for INTERVAL... MINUTE TO SECOND that should pass parser but
-   * fail validator. A substantially identical set of tests exists in
-   * SqlValidatorTest, and any changes here should be synchronized there.
-   * Similarly, any changes to tests here should be echoed appropriately to
-   * each of the other 12 subTestIntervalXXXFailsValidation() tests.
-   */
-  public void subTestIntervalMinuteToSecondFailsValidation() {
-    // Qualifier - field mismatches
-    expr("INTERVAL ':' MINUTE TO SECOND")
-        .ok("INTERVAL ':' MINUTE TO SECOND");
-    expr("INTERVAL ':.' MINUTE TO SECOND")
-        .ok("INTERVAL ':.' MINUTE TO SECOND");
-    expr("INTERVAL '1' MINUTE TO SECOND")
-        .ok("INTERVAL '1' MINUTE TO SECOND");
-    expr("INTERVAL '1 2' MINUTE TO SECOND")
-        .ok("INTERVAL '1 2' MINUTE TO SECOND");
-    expr("INTERVAL '1.2' MINUTE TO SECOND")
-        .ok("INTERVAL '1.2' MINUTE TO SECOND");
-    expr("INTERVAL '1 1:2' MINUTE TO SECOND")
-        .ok("INTERVAL '1 1:2' MINUTE TO SECOND");
-    expr("INTERVAL '1:x' MINUTE TO SECOND")
-        .ok("INTERVAL '1:x' MINUTE TO SECOND");
-    expr("INTERVAL 'x:3' MINUTE TO SECOND")
-        .ok("INTERVAL 'x:3' MINUTE TO SECOND");
-    expr("INTERVAL '1:1.x' MINUTE TO SECOND")
-        .ok("INTERVAL '1:1.x' MINUTE TO SECOND");
-    expr("INTERVAL '1 1:2' MINUTE(2) TO SECOND")
-        .ok("INTERVAL '1 1:2' MINUTE(2) TO SECOND");
-    expr("INTERVAL '1 1' MINUTE(2) TO SECOND")
-        .ok("INTERVAL '1 1' MINUTE(2) TO SECOND");
-    expr("INTERVAL 'bogus text' MINUTE TO SECOND")
-        .ok("INTERVAL 'bogus text' MINUTE TO SECOND");
-    expr("INTERVAL '7:8901' MINUTE TO SECOND(4)")
-        .ok("INTERVAL '7:8901' MINUTE TO SECOND(4)");
-
-    // negative field values
-    expr("INTERVAL '--1:1' MINUTE TO SECOND")
-        .ok("INTERVAL '--1:1' MINUTE TO SECOND");
-    expr("INTERVAL '1:-1' MINUTE TO SECOND")
-        .ok("INTERVAL '1:-1' MINUTE TO SECOND");
-    expr("INTERVAL '1:1.-1' MINUTE TO SECOND")
-        .ok("INTERVAL '1:1.-1' MINUTE TO SECOND");
-
-    // Field value out of range
-    //  (default, explicit default, alt, neg alt, max, neg max)
-    //  plus >max value for mid/end fields
-    expr("INTERVAL '100:0' MINUTE TO SECOND")
-        .ok("INTERVAL '100:0' MINUTE TO SECOND");
-    expr("INTERVAL '100:0' MINUTE(2) TO SECOND")
-        .ok("INTERVAL '100:0' MINUTE(2) TO SECOND");
-    expr("INTERVAL '1000:0' MINUTE(3) TO SECOND")
-        .ok("INTERVAL '1000:0' MINUTE(3) TO SECOND");
-    expr("INTERVAL '-1000:0' MINUTE(3) TO SECOND")
-        .ok("INTERVAL '-1000:0' MINUTE(3) TO SECOND");
-    expr("INTERVAL '2147483648:0' MINUTE(10) TO SECOND")
-        .ok("INTERVAL '2147483648:0' MINUTE(10) TO SECOND");
-    expr("INTERVAL '-2147483648:0' MINUTE(10) TO SECOND")
-        .ok("INTERVAL '-2147483648:0' MINUTE(10) TO SECOND");
-    expr("INTERVAL '1:60' MINUTE TO SECOND")
-        .ok("INTERVAL '1:60' MINUTE TO SECOND");
-    expr("INTERVAL '1:1.0000001' MINUTE TO SECOND")
-        .ok("INTERVAL '1:1.0000001' MINUTE TO SECOND");
-    expr("INTERVAL '1:1:1.0001' MINUTE TO SECOND(3)")
-        .ok("INTERVAL '1:1:1.0001' MINUTE TO SECOND(3)");
-
-    // precision > maximum
-    expr("INTERVAL '1:1' MINUTE(11) TO SECOND")
-        .ok("INTERVAL '1:1' MINUTE(11) TO SECOND");
-    expr("INTERVAL '1:1' MINUTE TO SECOND(10)")
-        .ok("INTERVAL '1:1' MINUTE TO SECOND(10)");
-
-    // precision < minimum allowed)
-    // note: parser will catch negative values, here we
-    // just need to check for 0
-    expr("INTERVAL '0:0' MINUTE(0) TO SECOND")
-        .ok("INTERVAL '0:0' MINUTE(0) TO SECOND");
-    expr("INTERVAL '0:0' MINUTE TO SECOND(0)")
-        .ok("INTERVAL '0:0' MINUTE TO SECOND(0)");
-  }
-
-  /**
-   * Runs tests for INTERVAL... SECOND that should pass parser but fail
-   * validator. A substantially identical set of tests exists in
-   * SqlValidatorTest, and any changes here should be synchronized there.
-   * Similarly, any changes to tests here should be echoed appropriately to
-   * each of the other 12 subTestIntervalXXXFailsValidation() tests.
-   */
-  public void subTestIntervalSecondFailsValidation() {
-    // Qualifier - field mismatches
-    expr("INTERVAL ':' SECOND")
-        .ok("INTERVAL ':' SECOND");
-    expr("INTERVAL '.' SECOND")
-        .ok("INTERVAL '.' SECOND");
-    expr("INTERVAL '1-2' SECOND")
-        .ok("INTERVAL '1-2' SECOND");
-    expr("INTERVAL '1.x' SECOND")
-        .ok("INTERVAL '1.x' SECOND");
-    expr("INTERVAL 'x.1' SECOND")
-        .ok("INTERVAL 'x.1' SECOND");
-    expr("INTERVAL '1 2' SECOND")
-        .ok("INTERVAL '1 2' SECOND");
-    expr("INTERVAL '1:2' SECOND")
-        .ok("INTERVAL '1:2' SECOND");
-    expr("INTERVAL '1-2' SECOND(2)")
-        .ok("INTERVAL '1-2' SECOND(2)");
-    expr("INTERVAL 'bogus text' SECOND")
-        .ok("INTERVAL 'bogus text' SECOND");
-
-    // negative field values
-    expr("INTERVAL '--1' SECOND")
-        .ok("INTERVAL '--1' SECOND");
-    expr("INTERVAL '1.-1' SECOND")
-        .ok("INTERVAL '1.-1' SECOND");
-
-    // Field value out of range
-    //  (default, explicit default, alt, neg alt, max, neg max)
-    expr("INTERVAL '100' SECOND")
-        .ok("INTERVAL '100' SECOND");
-    expr("INTERVAL '100' SECOND(2)")
-        .ok("INTERVAL '100' SECOND(2)");
-    expr("INTERVAL '1000' SECOND(3)")
-        .ok("INTERVAL '1000' SECOND(3)");
-    expr("INTERVAL '-1000' SECOND(3)")
-        .ok("INTERVAL '-1000' SECOND(3)");
-    expr("INTERVAL '2147483648' SECOND(10)")
-        .ok("INTERVAL '2147483648' SECOND(10)");
-    expr("INTERVAL '-2147483648' SECOND(10)")
-        .ok("INTERVAL '-2147483648' SECOND(10)");
-    expr("INTERVAL '1.0000001' SECOND")
-        .ok("INTERVAL '1.0000001' SECOND");
-    expr("INTERVAL '1.0000001' SECOND(2)")
-        .ok("INTERVAL '1.0000001' SECOND(2)");
-    expr("INTERVAL '1.0001' SECOND(2, 3)")
-        .ok("INTERVAL '1.0001' SECOND(2, 3)");
-    expr("INTERVAL '1.000000001' SECOND(2, 9)")
-        .ok("INTERVAL '1.000000001' SECOND(2, 9)");
-
-    // precision > maximum
-    expr("INTERVAL '1' SECOND(11)")
-        .ok("INTERVAL '1' SECOND(11)");
-    expr("INTERVAL '1.1' SECOND(1, 10)")
-        .ok("INTERVAL '1.1' SECOND(1, 10)");
-
-    // precision < minimum allowed)
-    // note: parser will catch negative values, here we
-    // just need to check for 0
-    expr("INTERVAL '0' SECOND(0)")
-        .ok("INTERVAL '0' SECOND(0)");
-    expr("INTERVAL '0' SECOND(1, 0)")
-        .ok("INTERVAL '0' SECOND(1, 0)");
-  }
-
   /**
    * Runs tests for each of the thirteen different main types of INTERVAL
    * qualifiers (YEAR, YEAR TO MONTH, etc.) Tests in this section fall into
@@ -7495,33 +5908,37 @@ public class SqlParserTest {
    * any changes here should be synchronized there.
    */
   @Test void testIntervalLiterals() {
-    subTestIntervalYearPositive();
-    subTestIntervalYearToMonthPositive();
-    subTestIntervalMonthPositive();
-    subTestIntervalDayPositive();
-    subTestIntervalDayToHourPositive();
-    subTestIntervalDayToMinutePositive();
-    subTestIntervalDayToSecondPositive();
-    subTestIntervalHourPositive();
-    subTestIntervalHourToMinutePositive();
-    subTestIntervalHourToSecondPositive();
-    subTestIntervalMinutePositive();
-    subTestIntervalMinuteToSecondPositive();
-    subTestIntervalSecondPositive();
-
-    subTestIntervalYearFailsValidation();
-    subTestIntervalYearToMonthFailsValidation();
-    subTestIntervalMonthFailsValidation();
-    subTestIntervalDayFailsValidation();
-    subTestIntervalDayToHourFailsValidation();
-    subTestIntervalDayToMinuteFailsValidation();
-    subTestIntervalDayToSecondFailsValidation();
-    subTestIntervalHourFailsValidation();
-    subTestIntervalHourToMinuteFailsValidation();
-    subTestIntervalHourToSecondFailsValidation();
-    subTestIntervalMinuteFailsValidation();
-    subTestIntervalMinuteToSecondFailsValidation();
-    subTestIntervalSecondFailsValidation();
+    final SqlParserFixture f = fixture();
+    final IntervalTest.Fixture intervalFixture = new IntervalTest.Fixture() {
+      @Override public IntervalTest.Fixture2 expr(String sql) {
+        final SqlParserFixture f2 = f.sql(sql).expression(true);
+        return getFixture2(f2, f2.sap.sql);
+      }
+
+      @Override public IntervalTest.Fixture2 wholeExpr(String sql) {
+        return expr(sql);
+      }
+
+      private IntervalTest.Fixture2 getFixture2(SqlParserFixture f2,
+          String expectedSql) {
+        return new IntervalTest.Fixture2() {
+          @Override public void fails(String message) {
+            f2.compare(expectedSql);
+          }
+
+          @Override public void columnType(String expectedType) {
+            f2.compare(expectedSql);
+          }
+
+          @Override public IntervalTest.Fixture2 assertParse(
+              String expectedSql) {
+            return getFixture2(f2, expectedSql);
+          }
+        };
+      }
+    };
+
+    new IntervalTest(intervalFixture).testAll();
   }
 
   @Test void testUnparseableIntervalQualifiers() {
diff --git a/testkit/src/main/java/org/apache/calcite/test/IntervalTest.java b/testkit/src/main/java/org/apache/calcite/test/IntervalTest.java
new file mode 100644
index 0000000000..1f6d5ed30d
--- /dev/null
+++ b/testkit/src/main/java/org/apache/calcite/test/IntervalTest.java
@@ -0,0 +1,1834 @@
+/*
+ * 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.test;
+
+/** Test cases for intervals.
+ *
+ * <p>Called, with varying implementations of {@link Fixture},
+ * from both parser and validator test.
+ */
+public class IntervalTest {
+  private final Fixture f;
+
+  public IntervalTest(Fixture fixture) {
+    this.f = fixture;
+  }
+
+  /** Runs all tests. */
+  public void testAll() {
+    // Tests that should pass both parser and validator
+    subTestIntervalYearPositive();
+    subTestIntervalYearToMonthPositive();
+    subTestIntervalMonthPositive();
+    subTestIntervalDayPositive();
+    subTestIntervalDayToHourPositive();
+    subTestIntervalDayToMinutePositive();
+    subTestIntervalDayToSecondPositive();
+    subTestIntervalHourPositive();
+    subTestIntervalHourToMinutePositive();
+    subTestIntervalHourToSecondPositive();
+    subTestIntervalMinutePositive();
+    subTestIntervalMinuteToSecondPositive();
+    subTestIntervalSecondPositive();
+
+    // Tests that should pass parser but fail validator
+    subTestIntervalYearNegative();
+    subTestIntervalYearToMonthNegative();
+    subTestIntervalMonthNegative();
+    subTestIntervalDayNegative();
+    subTestIntervalDayToHourNegative();
+    subTestIntervalDayToMinuteNegative();
+    subTestIntervalDayToSecondNegative();
+    subTestIntervalHourNegative();
+    subTestIntervalHourToMinuteNegative();
+    subTestIntervalHourToSecondNegative();
+    subTestIntervalMinuteNegative();
+    subTestIntervalMinuteToSecondNegative();
+    subTestIntervalSecondNegative();
+
+    subTestMisc();
+  }
+
+  /**
+   * Runs tests for INTERVAL... YEAR that should pass both parser and
+   * validator. A substantially identical set of tests exists in
+   * SqlParserTest, and any changes here should be synchronized there.
+   * Similarly, any changes to tests here should be echoed appropriately to
+   * each of the other 12 subTestIntervalXXXPositive() tests.
+   */
+  public void subTestIntervalYearPositive() {
+    // default precision
+    f.expr("INTERVAL '1' YEAR")
+        .columnType("INTERVAL YEAR NOT NULL");
+    f.expr("INTERVAL '99' YEAR")
+        .columnType("INTERVAL YEAR NOT NULL");
+
+    // explicit precision equal to default
+    f.expr("INTERVAL '1' YEAR(2)")
+        .columnType("INTERVAL YEAR(2) NOT NULL");
+    f.expr("INTERVAL '99' YEAR(2)")
+        .columnType("INTERVAL YEAR(2) NOT NULL");
+
+    // max precision
+    f.expr("INTERVAL '2147483647' YEAR(10)")
+        .columnType("INTERVAL YEAR(10) NOT NULL");
+
+    // min precision
+    f.expr("INTERVAL '0' YEAR(1)")
+        .columnType("INTERVAL YEAR(1) NOT NULL");
+
+    // alternate precision
+    f.expr("INTERVAL '1234' YEAR(4)")
+        .columnType("INTERVAL YEAR(4) NOT NULL");
+
+    // sign
+    f.expr("INTERVAL '+1' YEAR")
+        .columnType("INTERVAL YEAR NOT NULL");
+    f.expr("INTERVAL '-1' YEAR")
+        .columnType("INTERVAL YEAR NOT NULL");
+    f.expr("INTERVAL +'1' YEAR")
+        .assertParse("INTERVAL '1' YEAR")
+        .columnType("INTERVAL YEAR NOT NULL");
+    f.expr("INTERVAL +'+1' YEAR")
+        .assertParse("INTERVAL '+1' YEAR")
+        .columnType("INTERVAL YEAR NOT NULL");
+    f.expr("INTERVAL +'-1' YEAR")
+        .assertParse("INTERVAL '-1' YEAR")
+        .columnType("INTERVAL YEAR NOT NULL");
+    f.expr("INTERVAL -'1' YEAR")
+        .columnType("INTERVAL YEAR NOT NULL");
+    f.expr("INTERVAL -'+1' YEAR")
+        .columnType("INTERVAL YEAR NOT NULL");
+    f.expr("INTERVAL -'-1' YEAR")
+        .columnType("INTERVAL YEAR NOT NULL");
+  }
+
+  /**
+   * Runs tests for INTERVAL... YEAR TO MONTH that should pass both parser and
+   * validator. A substantially identical set of tests exists in
+   * SqlParserTest, and any changes here should be synchronized there.
+   * Similarly, any changes to tests here should be echoed appropriately to
+   * each of the other 12 subTestIntervalXXXPositive() tests.
+   */
+  public void subTestIntervalYearToMonthPositive() {
+    // default precision
+    f.expr("INTERVAL '1-2' YEAR TO MONTH")
+        .columnType("INTERVAL YEAR TO MONTH NOT NULL");
+    f.expr("INTERVAL '99-11' YEAR TO MONTH")
+        .columnType("INTERVAL YEAR TO MONTH NOT NULL");
+    f.expr("INTERVAL '99-0' YEAR TO MONTH")
+        .columnType("INTERVAL YEAR TO MONTH NOT NULL");
+
+    // explicit precision equal to default
+    f.expr("INTERVAL '1-2' YEAR(2) TO MONTH")
+        .columnType("INTERVAL YEAR(2) TO MONTH NOT NULL");
+    f.expr("INTERVAL '99-11' YEAR(2) TO MONTH")
+        .columnType("INTERVAL YEAR(2) TO MONTH NOT NULL");
+    f.expr("INTERVAL '99-0' YEAR(2) TO MONTH")
+        .columnType("INTERVAL YEAR(2) TO MONTH NOT NULL");
+
+    // max precision
+    f.expr("INTERVAL '2147483647-11' YEAR(10) TO MONTH")
+        .columnType("INTERVAL YEAR(10) TO MONTH NOT NULL");
+
+    // min precision
+    f.expr("INTERVAL '0-0' YEAR(1) TO MONTH")
+        .columnType("INTERVAL YEAR(1) TO MONTH NOT NULL");
+
+    // alternate precision
+    f.expr("INTERVAL '2006-2' YEAR(4) TO MONTH")
+        .columnType("INTERVAL YEAR(4) TO MONTH NOT NULL");
+
+    // sign
+    f.expr("INTERVAL '-1-2' YEAR TO MONTH")
+        .columnType("INTERVAL YEAR TO MONTH NOT NULL");
+    f.expr("INTERVAL '+1-2' YEAR TO MONTH")
+        .columnType("INTERVAL YEAR TO MONTH NOT NULL");
+    f.expr("INTERVAL +'1-2' YEAR TO MONTH")
+        .assertParse("INTERVAL '1-2' YEAR TO MONTH")
+        .columnType("INTERVAL YEAR TO MONTH NOT NULL");
+    f.expr("INTERVAL +'-1-2' YEAR TO MONTH")
+        .assertParse("INTERVAL '-1-2' YEAR TO MONTH")
+        .columnType("INTERVAL YEAR TO MONTH NOT NULL");
+    f.expr("INTERVAL +'+1-2' YEAR TO MONTH")
+        .assertParse("INTERVAL '+1-2' YEAR TO MONTH")
+        .columnType("INTERVAL YEAR TO MONTH NOT NULL");
+    f.expr("INTERVAL -'1-2' YEAR TO MONTH")
+        .columnType("INTERVAL YEAR TO MONTH NOT NULL");
+    f.expr("INTERVAL -'-1-2' YEAR TO MONTH")
+        .columnType("INTERVAL YEAR TO MONTH NOT NULL");
+    f.expr("INTERVAL -'+1-2' YEAR TO MONTH")
+        .columnType("INTERVAL YEAR TO MONTH NOT NULL");
+  }
+
+  /**
+   * Runs tests for INTERVAL... MONTH that should pass both parser and
+   * validator. A substantially identical set of tests exists in
+   * SqlParserTest, and any changes here should be synchronized there.
+   * Similarly, any changes to tests here should be echoed appropriately to
+   * each of the other 12 subTestIntervalXXXPositive() tests.
+   */
+  public void subTestIntervalMonthPositive() {
+    // default precision
+    f.expr("INTERVAL '1' MONTH")
+        .columnType("INTERVAL MONTH NOT NULL");
+    f.expr("INTERVAL '99' MONTH")
+        .columnType("INTERVAL MONTH NOT NULL");
+
+    // explicit precision equal to default
+    f.expr("INTERVAL '1' MONTH(2)")
+        .columnType("INTERVAL MONTH(2) NOT NULL");
+    f.expr("INTERVAL '99' MONTH(2)")
+        .columnType("INTERVAL MONTH(2) NOT NULL");
+
+    // max precision
+    f.expr("INTERVAL '2147483647' MONTH(10)")
+        .columnType("INTERVAL MONTH(10) NOT NULL");
+
+    // min precision
+    f.expr("INTERVAL '0' MONTH(1)")
+        .columnType("INTERVAL MONTH(1) NOT NULL");
+
+    // alternate precision
+    f.expr("INTERVAL '1234' MONTH(4)")
+        .columnType("INTERVAL MONTH(4) NOT NULL");
+
+    // sign
+    f.expr("INTERVAL '+1' MONTH")
+        .columnType("INTERVAL MONTH NOT NULL");
+    f.expr("INTERVAL '-1' MONTH")
+        .columnType("INTERVAL MONTH NOT NULL");
+    f.expr("INTERVAL +'1' MONTH")
+        .assertParse("INTERVAL '1' MONTH")
+        .columnType("INTERVAL MONTH NOT NULL");
+    f.expr("INTERVAL +'+1' MONTH")
+        .assertParse("INTERVAL '+1' MONTH")
+        .columnType("INTERVAL MONTH NOT NULL");
+    f.expr("INTERVAL +'-1' MONTH")
+        .assertParse("INTERVAL '-1' MONTH")
+        .columnType("INTERVAL MONTH NOT NULL");
+    f.expr("INTERVAL -'1' MONTH")
+        .columnType("INTERVAL MONTH NOT NULL");
+    f.expr("INTERVAL -'+1' MONTH")
+        .columnType("INTERVAL MONTH NOT NULL");
+    f.expr("INTERVAL -'-1' MONTH")
+        .columnType("INTERVAL MONTH NOT NULL");
+  }
+
+  /**
+   * Runs tests for INTERVAL... DAY that should pass both parser and
+   * validator. A substantially identical set of tests exists in
+   * SqlParserTest, and any changes here should be synchronized there.
+   * Similarly, any changes to tests here should be echoed appropriately to
+   * each of the other 12 subTestIntervalXXXPositive() tests.
+   */
+  public void subTestIntervalDayPositive() {
+    // default precision
+    f.expr("INTERVAL '1' DAY")
+        .columnType("INTERVAL DAY NOT NULL");
+    f.expr("INTERVAL '99' DAY")
+        .columnType("INTERVAL DAY NOT NULL");
+
+    // explicit precision equal to default
+    f.expr("INTERVAL '1' DAY(2)")
+        .columnType("INTERVAL DAY(2) NOT NULL");
+    f.expr("INTERVAL '99' DAY(2)")
+        .columnType("INTERVAL DAY(2) NOT NULL");
+
+    // max precision
+    f.expr("INTERVAL '2147483647' DAY(10)")
+        .columnType("INTERVAL DAY(10) NOT NULL");
+
+    // min precision
+    f.expr("INTERVAL '0' DAY(1)")
+        .columnType("INTERVAL DAY(1) NOT NULL");
+
+    // alternate precision
+    f.expr("INTERVAL '1234' DAY(4)")
+        .columnType("INTERVAL DAY(4) NOT NULL");
+
+    // sign
+    f.expr("INTERVAL '+1' DAY")
+        .columnType("INTERVAL DAY NOT NULL");
+    f.expr("INTERVAL '-1' DAY")
+        .columnType("INTERVAL DAY NOT NULL");
+    f.expr("INTERVAL +'1' DAY")
+        .assertParse("INTERVAL '1' DAY")
+        .columnType("INTERVAL DAY NOT NULL");
+    f.expr("INTERVAL +'+1' DAY")
+        .assertParse("INTERVAL '+1' DAY")
+        .columnType("INTERVAL DAY NOT NULL");
+    f.expr("INTERVAL +'-1' DAY")
+        .assertParse("INTERVAL '-1' DAY")
+        .columnType("INTERVAL DAY NOT NULL");
+    f.expr("INTERVAL -'1' DAY")
+        .columnType("INTERVAL DAY NOT NULL");
+    f.expr("INTERVAL -'+1' DAY")
+        .columnType("INTERVAL DAY NOT NULL");
+    f.expr("INTERVAL -'-1' DAY")
+        .columnType("INTERVAL DAY NOT NULL");
+  }
+
+  public void subTestIntervalDayToHourPositive() {
+    // default precision
+    f.expr("INTERVAL '1 2' DAY TO HOUR")
+        .columnType("INTERVAL DAY TO HOUR NOT NULL");
+    f.expr("INTERVAL '99 23' DAY TO HOUR")
+        .columnType("INTERVAL DAY TO HOUR NOT NULL");
+    f.expr("INTERVAL '99 0' DAY TO HOUR")
+        .columnType("INTERVAL DAY TO HOUR NOT NULL");
+
+    // explicit precision equal to default
+    f.expr("INTERVAL '1 2' DAY(2) TO HOUR")
+        .columnType("INTERVAL DAY(2) TO HOUR NOT NULL");
+    f.expr("INTERVAL '99 23' DAY(2) TO HOUR")
+        .columnType("INTERVAL DAY(2) TO HOUR NOT NULL");
+    f.expr("INTERVAL '99 0' DAY(2) TO HOUR")
+        .columnType("INTERVAL DAY(2) TO HOUR NOT NULL");
+
+    // max precision
+    f.expr("INTERVAL '2147483647 23' DAY(10) TO HOUR")
+        .columnType("INTERVAL DAY(10) TO HOUR NOT NULL");
+
+    // min precision
+    f.expr("INTERVAL '0 0' DAY(1) TO HOUR")
+        .columnType("INTERVAL DAY(1) TO HOUR NOT NULL");
+
+    // alternate precision
+    f.expr("INTERVAL '2345 2' DAY(4) TO HOUR")
+        .columnType("INTERVAL DAY(4) TO HOUR NOT NULL");
+
+    // sign
+    f.expr("INTERVAL '-1 2' DAY TO HOUR")
+        .columnType("INTERVAL DAY TO HOUR NOT NULL");
+    f.expr("INTERVAL '+1 2' DAY TO HOUR")
+        .columnType("INTERVAL DAY TO HOUR NOT NULL");
+    f.expr("INTERVAL +'1 2' DAY TO HOUR")
+        .assertParse("INTERVAL '1 2' DAY TO HOUR")
+        .columnType("INTERVAL DAY TO HOUR NOT NULL");
+    f.expr("INTERVAL +'-1 2' DAY TO HOUR")
+        .assertParse("INTERVAL '-1 2' DAY TO HOUR")
+        .columnType("INTERVAL DAY TO HOUR NOT NULL");
+    f.expr("INTERVAL +'+1 2' DAY TO HOUR")
+        .assertParse("INTERVAL '+1 2' DAY TO HOUR")
+        .columnType("INTERVAL DAY TO HOUR NOT NULL");
+    f.expr("INTERVAL -'1 2' DAY TO HOUR")
+        .columnType("INTERVAL DAY TO HOUR NOT NULL");
+    f.expr("INTERVAL -'-1 2' DAY TO HOUR")
+        .columnType("INTERVAL DAY TO HOUR NOT NULL");
+    f.expr("INTERVAL -'+1 2' DAY TO HOUR")
+        .columnType("INTERVAL DAY TO HOUR NOT NULL");
+  }
+
+  /**
+   * Runs tests for INTERVAL... DAY TO MINUTE that should pass both parser and
+   * validator. A substantially identical set of tests exists in
+   * SqlParserTest, and any changes here should be synchronized there.
+   * Similarly, any changes to tests here should be echoed appropriately to
+   * each of the other 12 subTestIntervalXXXPositive() tests.
+   */
+  public void subTestIntervalDayToMinutePositive() {
+    // default precision
+    f.expr("INTERVAL '1 2:3' DAY TO MINUTE")
+        .columnType("INTERVAL DAY TO MINUTE NOT NULL");
+    f.expr("INTERVAL '99 23:59' DAY TO MINUTE")
+        .columnType("INTERVAL DAY TO MINUTE NOT NULL");
+    f.expr("INTERVAL '99 0:0' DAY TO MINUTE")
+        .columnType("INTERVAL DAY TO MINUTE NOT NULL");
+
+    // explicit precision equal to default
+    f.expr("INTERVAL '1 2:3' DAY(2) TO MINUTE")
+        .columnType("INTERVAL DAY(2) TO MINUTE NOT NULL");
+    f.expr("INTERVAL '99 23:59' DAY(2) TO MINUTE")
+        .columnType("INTERVAL DAY(2) TO MINUTE NOT NULL");
+    f.expr("INTERVAL '99 0:0' DAY(2) TO MINUTE")
+        .columnType("INTERVAL DAY(2) TO MINUTE NOT NULL");
+
+    // max precision
+    f.expr("INTERVAL '2147483647 23:59' DAY(10) TO MINUTE")
+        .columnType("INTERVAL DAY(10) TO MINUTE NOT NULL");
+
+    // min precision
+    f.expr("INTERVAL '0 0:0' DAY(1) TO MINUTE")
+        .columnType("INTERVAL DAY(1) TO MINUTE NOT NULL");
+
+    // alternate precision
+    f.expr("INTERVAL '2345 6:7' DAY(4) TO MINUTE")
+        .columnType("INTERVAL DAY(4) TO MINUTE NOT NULL");
+
+    // sign
+    f.expr("INTERVAL '-1 2:3' DAY TO MINUTE")
+        .columnType("INTERVAL DAY TO MINUTE NOT NULL");
+    f.expr("INTERVAL '+1 2:3' DAY TO MINUTE")
+        .assertParse("INTERVAL '+1 2:3' DAY TO MINUTE")
+        .columnType("INTERVAL DAY TO MINUTE NOT NULL");
+    f.expr("INTERVAL +'1 2:3' DAY TO MINUTE")
+        .assertParse("INTERVAL '1 2:3' DAY TO MINUTE")
+        .columnType("INTERVAL DAY TO MINUTE NOT NULL");
+    f.expr("INTERVAL +'-1 2:3' DAY TO MINUTE")
+        .assertParse("INTERVAL '-1 2:3' DAY TO MINUTE")
+        .columnType("INTERVAL DAY TO MINUTE NOT NULL");
+    f.expr("INTERVAL +'+1 2:3' DAY TO MINUTE")
+        .assertParse("INTERVAL '+1 2:3' DAY TO MINUTE")
+        .columnType("INTERVAL DAY TO MINUTE NOT NULL");
+    f.expr("INTERVAL -'1 2:3' DAY TO MINUTE")
+        .columnType("INTERVAL DAY TO MINUTE NOT NULL");
+    f.expr("INTERVAL -'-1 2:3' DAY TO MINUTE")
+        .columnType("INTERVAL DAY TO MINUTE NOT NULL");
+    f.expr("INTERVAL -'+1 2:3' DAY TO MINUTE")
+        .columnType("INTERVAL DAY TO MINUTE NOT NULL");
+  }
+
+  /**
+   * Runs tests for INTERVAL... DAY TO SECOND that should pass both parser and
+   * validator. A substantially identical set of tests exists in
+   * SqlParserTest, and any changes here should be synchronized there.
+   * Similarly, any changes to tests here should be echoed appropriately to
+   * each of the other 12 subTestIntervalXXXPositive() tests.
+   */
+  public void subTestIntervalDayToSecondPositive() {
+    // default precision
+    f.expr("INTERVAL '1 2:3:4' DAY TO SECOND")
+        .columnType("INTERVAL DAY TO SECOND NOT NULL");
+    f.expr("INTERVAL '99 23:59:59' DAY TO SECOND")
+        .columnType("INTERVAL DAY TO SECOND NOT NULL");
+    f.expr("INTERVAL '99 0:0:0' DAY TO SECOND")
+        .columnType("INTERVAL DAY TO SECOND NOT NULL");
+    f.expr("INTERVAL '99 23:59:59.999999' DAY TO SECOND")
+        .columnType("INTERVAL DAY TO SECOND NOT NULL");
+    f.expr("INTERVAL '99 0:0:0.0' DAY TO SECOND")
+        .columnType("INTERVAL DAY TO SECOND NOT NULL");
+
+    // explicit precision equal to default
+    f.expr("INTERVAL '1 2:3:4' DAY(2) TO SECOND")
+        .columnType("INTERVAL DAY(2) TO SECOND NOT NULL");
+    f.expr("INTERVAL '99 23:59:59' DAY(2) TO SECOND")
+        .columnType("INTERVAL DAY(2) TO SECOND NOT NULL");
+    f.expr("INTERVAL '99 0:0:0' DAY(2) TO SECOND")
+        .columnType("INTERVAL DAY(2) TO SECOND NOT NULL");
+    f.expr("INTERVAL '99 23:59:59.999999' DAY TO SECOND(6)")
+        .columnType("INTERVAL DAY TO SECOND(6) NOT NULL");
+    f.expr("INTERVAL '99 0:0:0.0' DAY TO SECOND(6)")
+        .columnType("INTERVAL DAY TO SECOND(6) NOT NULL");
+
+    // max precision
+    f.expr("INTERVAL '2147483647 23:59:59' DAY(10) TO SECOND")
+        .columnType("INTERVAL DAY(10) TO SECOND NOT NULL");
+    f.expr("INTERVAL '2147483647 23:59:59.999999999' DAY(10) TO SECOND(9)")
+        .columnType("INTERVAL DAY(10) TO SECOND(9) NOT NULL");
+
+    // min precision
+    f.expr("INTERVAL '0 0:0:0' DAY(1) TO SECOND")
+        .columnType("INTERVAL DAY(1) TO SECOND NOT NULL");
+    f.expr("INTERVAL '0 0:0:0.0' DAY(1) TO SECOND(1)")
+        .columnType("INTERVAL DAY(1) TO SECOND(1) NOT NULL");
+
+    // alternate precision
+    f.expr("INTERVAL '2345 6:7:8' DAY(4) TO SECOND")
+        .columnType("INTERVAL DAY(4) TO SECOND NOT NULL");
+    f.expr("INTERVAL '2345 6:7:8.9012' DAY(4) TO SECOND(4)")
+        .columnType("INTERVAL DAY(4) TO SECOND(4) NOT NULL");
+
+    // sign
+    f.expr("INTERVAL '-1 2:3:4' DAY TO SECOND")
+        .columnType("INTERVAL DAY TO SECOND NOT NULL");
+    f.expr("INTERVAL '+1 2:3:4' DAY TO SECOND")
+        .columnType("INTERVAL DAY TO SECOND NOT NULL");
+    f.expr("INTERVAL +'1 2:3:4' DAY TO SECOND")
+        .assertParse("INTERVAL '1 2:3:4' DAY TO SECOND")
+        .columnType("INTERVAL DAY TO SECOND NOT NULL");
+    f.expr("INTERVAL +'-1 2:3:4' DAY TO SECOND")
+        .assertParse("INTERVAL '-1 2:3:4' DAY TO SECOND")
+        .columnType("INTERVAL DAY TO SECOND NOT NULL");
+    f.expr("INTERVAL +'+1 2:3:4' DAY TO SECOND")
+        .assertParse("INTERVAL '+1 2:3:4' DAY TO SECOND")
+        .columnType("INTERVAL DAY TO SECOND NOT NULL");
+    f.expr("INTERVAL -'1 2:3:4' DAY TO SECOND")
+        .columnType("INTERVAL DAY TO SECOND NOT NULL");
+    f.expr("INTERVAL -'-1 2:3:4' DAY TO SECOND")
+        .columnType("INTERVAL DAY TO SECOND NOT NULL");
+    f.expr("INTERVAL -'+1 2:3:4' DAY TO SECOND")
+        .columnType("INTERVAL DAY TO SECOND NOT NULL");
+  }
+
+  /**
+   * Runs tests for INTERVAL... HOUR that should pass both parser and
+   * validator. A substantially identical set of tests exists in
+   * SqlParserTest, and any changes here should be synchronized there.
+   * Similarly, any changes to tests here should be echoed appropriately to
+   * each of the other 12 subTestIntervalXXXPositive() tests.
+   */
+  public void subTestIntervalHourPositive() {
+    // default precision
+    f.expr("INTERVAL '1' HOUR")
+        .columnType("INTERVAL HOUR NOT NULL");
+    f.expr("INTERVAL '99' HOUR")
+        .columnType("INTERVAL HOUR NOT NULL");
+
+    // explicit precision equal to default
+    f.expr("INTERVAL '1' HOUR(2)")
+        .columnType("INTERVAL HOUR(2) NOT NULL");
+    f.expr("INTERVAL '99' HOUR(2)")
+        .columnType("INTERVAL HOUR(2) NOT NULL");
+
+    // max precision
+    f.expr("INTERVAL '2147483647' HOUR(10)")
+        .columnType("INTERVAL HOUR(10) NOT NULL");
+
+    // min precision
+    f.expr("INTERVAL '0' HOUR(1)")
+        .columnType("INTERVAL HOUR(1) NOT NULL");
+
+    // alternate precision
+    f.expr("INTERVAL '1234' HOUR(4)")
+        .columnType("INTERVAL HOUR(4) NOT NULL");
+
+    // sign
+    f.expr("INTERVAL '+1' HOUR")
+        .columnType("INTERVAL HOUR NOT NULL");
+    f.expr("INTERVAL '-1' HOUR")
+        .columnType("INTERVAL HOUR NOT NULL");
+    f.expr("INTERVAL +'1' HOUR")
+        .assertParse("INTERVAL '1' HOUR")
+        .columnType("INTERVAL HOUR NOT NULL");
+    f.expr("INTERVAL +'+1' HOUR")
+        .assertParse("INTERVAL '+1' HOUR")
+        .columnType("INTERVAL HOUR NOT NULL");
+    f.expr("INTERVAL +'-1' HOUR")
+        .assertParse("INTERVAL '-1' HOUR")
+        .columnType("INTERVAL HOUR NOT NULL");
+    f.expr("INTERVAL -'1' HOUR")
+        .columnType("INTERVAL HOUR NOT NULL");
+    f.expr("INTERVAL -'+1' HOUR")
+        .columnType("INTERVAL HOUR NOT NULL");
+    f.expr("INTERVAL -'-1' HOUR")
+        .columnType("INTERVAL HOUR NOT NULL");
+  }
+
+  /**
+   * Runs tests for INTERVAL... HOUR TO MINUTE that should pass both parser
+   * and validator. A substantially identical set of tests exists in
+   * SqlParserTest, and any changes here should be synchronized there.
+   * Similarly, any changes to tests here should be echoed appropriately to
+   * each of the other 12 subTestIntervalXXXPositive() tests.
+   */
+  public void subTestIntervalHourToMinutePositive() {
+    // default precision
+    f.expr("INTERVAL '2:3' HOUR TO MINUTE")
+        .columnType("INTERVAL HOUR TO MINUTE NOT NULL");
+    f.expr("INTERVAL '23:59' HOUR TO MINUTE")
+        .columnType("INTERVAL HOUR TO MINUTE NOT NULL");
+    f.expr("INTERVAL '99:0' HOUR TO MINUTE")
+        .columnType("INTERVAL HOUR TO MINUTE NOT NULL");
+
+    // explicit precision equal to default
+    f.expr("INTERVAL '2:3' HOUR(2) TO MINUTE")
+        .columnType("INTERVAL HOUR(2) TO MINUTE NOT NULL");
+    f.expr("INTERVAL '23:59' HOUR(2) TO MINUTE")
+        .columnType("INTERVAL HOUR(2) TO MINUTE NOT NULL");
+    f.expr("INTERVAL '99:0' HOUR(2) TO MINUTE")
+        .columnType("INTERVAL HOUR(2) TO MINUTE NOT NULL");
+
+    // max precision
+    f.expr("INTERVAL '2147483647:59' HOUR(10) TO MINUTE")
+        .columnType("INTERVAL HOUR(10) TO MINUTE NOT NULL");
+
+    // min precision
+    f.expr("INTERVAL '0:0' HOUR(1) TO MINUTE")
+        .columnType("INTERVAL HOUR(1) TO MINUTE NOT NULL");
+
+    // alternate precision
+    f.expr("INTERVAL '2345:7' HOUR(4) TO MINUTE")
+        .columnType("INTERVAL HOUR(4) TO MINUTE NOT NULL");
+
+    // sign
+    f.expr("INTERVAL '-1:3' HOUR TO MINUTE")
+        .columnType("INTERVAL HOUR TO MINUTE NOT NULL");
+    f.expr("INTERVAL '+1:3' HOUR TO MINUTE")
+        .columnType("INTERVAL HOUR TO MINUTE NOT NULL");
+    f.expr("INTERVAL +'2:3' HOUR TO MINUTE")
+        .assertParse("INTERVAL '2:3' HOUR TO MINUTE")
+        .columnType("INTERVAL HOUR TO MINUTE NOT NULL");
+    f.expr("INTERVAL +'-2:3' HOUR TO MINUTE")
+        .assertParse("INTERVAL '-2:3' HOUR TO MINUTE")
+        .columnType("INTERVAL HOUR TO MINUTE NOT NULL");
+    f.expr("INTERVAL +'+2:3' HOUR TO MINUTE")
+        .assertParse("INTERVAL '+2:3' HOUR TO MINUTE")
+        .columnType("INTERVAL HOUR TO MINUTE NOT NULL");
+    f.expr("INTERVAL -'2:3' HOUR TO MINUTE")
+        .columnType("INTERVAL HOUR TO MINUTE NOT NULL");
+    f.expr("INTERVAL -'-2:3' HOUR TO MINUTE")
+        .columnType("INTERVAL HOUR TO MINUTE NOT NULL");
+    f.expr("INTERVAL -'+2:3' HOUR TO MINUTE")
+        .columnType("INTERVAL HOUR TO MINUTE NOT NULL");
+  }
+
+  /**
+   * Runs tests for INTERVAL... HOUR TO SECOND that should pass both parser
+   * and validator. A substantially identical set of tests exists in
+   * SqlParserTest, and any changes here should be synchronized there.
+   * Similarly, any changes to tests here should be echoed appropriately to
+   * each of the other 12 subTestIntervalXXXPositive() tests.
+   */
+  public void subTestIntervalHourToSecondPositive() {
+    // default precision
+    f.expr("INTERVAL '2:3:4' HOUR TO SECOND")
+        .columnType("INTERVAL HOUR TO SECOND NOT NULL");
+    f.expr("INTERVAL '23:59:59' HOUR TO SECOND")
+        .columnType("INTERVAL HOUR TO SECOND NOT NULL");
+    f.expr("INTERVAL '99:0:0' HOUR TO SECOND")
+        .columnType("INTERVAL HOUR TO SECOND NOT NULL");
+    f.expr("INTERVAL '23:59:59.999999' HOUR TO SECOND")
+        .columnType("INTERVAL HOUR TO SECOND NOT NULL");
+    f.expr("INTERVAL '99:0:0.0' HOUR TO SECOND")
+        .columnType("INTERVAL HOUR TO SECOND NOT NULL");
+
+    // explicit precision equal to default
+    f.expr("INTERVAL '2:3:4' HOUR(2) TO SECOND")
+        .columnType("INTERVAL HOUR(2) TO SECOND NOT NULL");
+    f.expr("INTERVAL '99:59:59' HOUR(2) TO SECOND")
+        .columnType("INTERVAL HOUR(2) TO SECOND NOT NULL");
+    f.expr("INTERVAL '99:0:0' HOUR(2) TO SECOND")
+        .columnType("INTERVAL HOUR(2) TO SECOND NOT NULL");
+    f.expr("INTERVAL '99:59:59.999999' HOUR TO SECOND(6)")
+        .columnType("INTERVAL HOUR TO SECOND(6) NOT NULL");
+    f.expr("INTERVAL '99:0:0.0' HOUR TO SECOND(6)")
+        .columnType("INTERVAL HOUR TO SECOND(6) NOT NULL");
+
+    // max precision
+    f.expr("INTERVAL '2147483647:59:59' HOUR(10) TO SECOND")
+        .columnType("INTERVAL HOUR(10) TO SECOND NOT NULL");
+    f.expr("INTERVAL '2147483647:59:59.999999999' HOUR(10) TO SECOND(9)")
+        .columnType("INTERVAL HOUR(10) TO SECOND(9) NOT NULL");
+
+    // min precision
+    f.expr("INTERVAL '0:0:0' HOUR(1) TO SECOND")
+        .columnType("INTERVAL HOUR(1) TO SECOND NOT NULL");
+    f.expr("INTERVAL '0:0:0.0' HOUR(1) TO SECOND(1)")
+        .columnType("INTERVAL HOUR(1) TO SECOND(1) NOT NULL");
+
+    // alternate precision
+    f.expr("INTERVAL '2345:7:8' HOUR(4) TO SECOND")
+        .columnType("INTERVAL HOUR(4) TO SECOND NOT NULL");
+    f.expr("INTERVAL '2345:7:8.9012' HOUR(4) TO SECOND(4)")
+        .columnType("INTERVAL HOUR(4) TO SECOND(4) NOT NULL");
+
+    // sign
+    f.expr("INTERVAL '-2:3:4' HOUR TO SECOND")
+        .columnType("INTERVAL HOUR TO SECOND NOT NULL");
+    f.expr("INTERVAL '+2:3:4' HOUR TO SECOND")
+        .columnType("INTERVAL HOUR TO SECOND NOT NULL");
+    f.expr("INTERVAL +'2:3:4' HOUR TO SECOND")
+        .assertParse("INTERVAL '2:3:4' HOUR TO SECOND")
+        .columnType("INTERVAL HOUR TO SECOND NOT NULL");
+    f.expr("INTERVAL +'-2:3:4' HOUR TO SECOND")
+        .assertParse("INTERVAL '-2:3:4' HOUR TO SECOND")
+        .columnType("INTERVAL HOUR TO SECOND NOT NULL");
+    f.expr("INTERVAL +'+2:3:4' HOUR TO SECOND")
+        .assertParse("INTERVAL '+2:3:4' HOUR TO SECOND")
+        .columnType("INTERVAL HOUR TO SECOND NOT NULL");
+    f.expr("INTERVAL -'2:3:4' HOUR TO SECOND")
+        .columnType("INTERVAL HOUR TO SECOND NOT NULL");
+    f.expr("INTERVAL -'-2:3:4' HOUR TO SECOND")
+        .columnType("INTERVAL HOUR TO SECOND NOT NULL");
+    f.expr("INTERVAL -'+2:3:4' HOUR TO SECOND")
+        .columnType("INTERVAL HOUR TO SECOND NOT NULL");
+  }
+
+  /**
+   * Runs tests for INTERVAL... MINUTE that should pass both parser and
+   * validator. A substantially identical set of tests exists in
+   * SqlParserTest, and any changes here should be synchronized there.
+   * Similarly, any changes to tests here should be echoed appropriately to
+   * each of the other 12 subTestIntervalXXXPositive() tests.
+   */
+  public void subTestIntervalMinutePositive() {
+    // default precision
+    f.expr("INTERVAL '1' MINUTE")
+        .columnType("INTERVAL MINUTE NOT NULL");
+    f.expr("INTERVAL '99' MINUTE")
+        .columnType("INTERVAL MINUTE NOT NULL");
+
+    // explicit precision equal to default
+    f.expr("INTERVAL '1' MINUTE(2)")
+        .columnType("INTERVAL MINUTE(2) NOT NULL");
+    f.expr("INTERVAL '99' MINUTE(2)")
+        .columnType("INTERVAL MINUTE(2) NOT NULL");
+
+    // max precision
+    f.expr("INTERVAL '2147483647' MINUTE(10)")
+        .columnType("INTERVAL MINUTE(10) NOT NULL");
+
+    // min precision
+    f.expr("INTERVAL '0' MINUTE(1)")
+        .columnType("INTERVAL MINUTE(1) NOT NULL");
+
+    // alternate precision
+    f.expr("INTERVAL '1234' MINUTE(4)")
+        .columnType("INTERVAL MINUTE(4) NOT NULL");
+
+    // sign
+    f.expr("INTERVAL '+1' MINUTE")
+        .columnType("INTERVAL MINUTE NOT NULL");
+    f.expr("INTERVAL '-1' MINUTE")
+        .columnType("INTERVAL MINUTE NOT NULL");
+    f.expr("INTERVAL +'1' MINUTE")
+        .assertParse("INTERVAL '1' MINUTE")
+        .columnType("INTERVAL MINUTE NOT NULL");
+    f.expr("INTERVAL +'+1' MINUTE")
+        .assertParse("INTERVAL '+1' MINUTE")
+        .columnType("INTERVAL MINUTE NOT NULL");
+    f.expr("INTERVAL +'-1' MINUTE")
+        .assertParse("INTERVAL '-1' MINUTE")
+        .columnType("INTERVAL MINUTE NOT NULL");
+    f.expr("INTERVAL -'1' MINUTE")
+        .columnType("INTERVAL MINUTE NOT NULL");
+    f.expr("INTERVAL -'+1' MINUTE")
+        .columnType("INTERVAL MINUTE NOT NULL");
+    f.expr("INTERVAL -'-1' MINUTE")
+        .columnType("INTERVAL MINUTE NOT NULL");
+  }
+
+  /**
+   * Runs tests for INTERVAL... MINUTE TO SECOND that should pass both parser
+   * and validator. A substantially identical set of tests exists in
+   * SqlParserTest, and any changes here should be synchronized there.
+   * Similarly, any changes to tests here should be echoed appropriately to
+   * each of the other 12 subTestIntervalXXXPositive() tests.
+   */
+  public void subTestIntervalMinuteToSecondPositive() {
+    // default precision
+    f.expr("INTERVAL '2:4' MINUTE TO SECOND")
+        .columnType("INTERVAL MINUTE TO SECOND NOT NULL");
+    f.expr("INTERVAL '59:59' MINUTE TO SECOND")
+        .columnType("INTERVAL MINUTE TO SECOND NOT NULL");
+    f.expr("INTERVAL '99:0' MINUTE TO SECOND")
+        .columnType("INTERVAL MINUTE TO SECOND NOT NULL");
+    f.expr("INTERVAL '59:59.999999' MINUTE TO SECOND")
+        .columnType("INTERVAL MINUTE TO SECOND NOT NULL");
+    f.expr("INTERVAL '99:0.0' MINUTE TO SECOND")
+        .columnType("INTERVAL MINUTE TO SECOND NOT NULL");
+
+    // explicit precision equal to default
+    f.expr("INTERVAL '2:4' MINUTE(2) TO SECOND")
+        .columnType("INTERVAL MINUTE(2) TO SECOND NOT NULL");
+    f.expr("INTERVAL '99:59' MINUTE(2) TO SECOND")
+        .columnType("INTERVAL MINUTE(2) TO SECOND NOT NULL");
+    f.expr("INTERVAL '99:0' MINUTE(2) TO SECOND")
+        .columnType("INTERVAL MINUTE(2) TO SECOND NOT NULL");
+    f.expr("INTERVAL '99:59.999999' MINUTE TO SECOND(6)")
+        .columnType("INTERVAL MINUTE TO SECOND(6) NOT NULL");
+    f.expr("INTERVAL '99:0.0' MINUTE TO SECOND(6)")
+        .columnType("INTERVAL MINUTE TO SECOND(6) NOT NULL");
+
+    // max precision
+    f.expr("INTERVAL '2147483647:59' MINUTE(10) TO SECOND")
+        .columnType("INTERVAL MINUTE(10) TO SECOND NOT NULL");
+    f.expr("INTERVAL '2147483647:59.999999999' MINUTE(10) TO SECOND(9)")
+        .columnType("INTERVAL MINUTE(10) TO SECOND(9) NOT NULL");
+
+    // min precision
+    f.expr("INTERVAL '0:0' MINUTE(1) TO SECOND")
+        .columnType("INTERVAL MINUTE(1) TO SECOND NOT NULL");
+    f.expr("INTERVAL '0:0.0' MINUTE(1) TO SECOND(1)")
+        .columnType("INTERVAL MINUTE(1) TO SECOND(1) NOT NULL");
+
+    // alternate precision
+    f.expr("INTERVAL '2345:8' MINUTE(4) TO SECOND")
+        .columnType("INTERVAL MINUTE(4) TO SECOND NOT NULL");
+    f.expr("INTERVAL '2345:7.8901' MINUTE(4) TO SECOND(4)")
+        .columnType("INTERVAL MINUTE(4) TO SECOND(4) NOT NULL");
+
+    // sign
+    f.expr("INTERVAL '-3:4' MINUTE TO SECOND")
+        .columnType("INTERVAL MINUTE TO SECOND NOT NULL");
+    f.expr("INTERVAL '+3:4' MINUTE TO SECOND")
+        .columnType("INTERVAL MINUTE TO SECOND NOT NULL");
+    f.expr("INTERVAL +'3:4' MINUTE TO SECOND")
+        .assertParse("INTERVAL '3:4' MINUTE TO SECOND")
+        .columnType("INTERVAL MINUTE TO SECOND NOT NULL");
+    f.expr("INTERVAL +'-3:4' MINUTE TO SECOND")
+        .assertParse("INTERVAL '-3:4' MINUTE TO SECOND")
+        .columnType("INTERVAL MINUTE TO SECOND NOT NULL");
+    f.expr("INTERVAL +'+3:4' MINUTE TO SECOND")
+        .assertParse("INTERVAL '+3:4' MINUTE TO SECOND")
+        .columnType("INTERVAL MINUTE TO SECOND NOT NULL");
+    f.expr("INTERVAL -'3:4' MINUTE TO SECOND")
+        .columnType("INTERVAL MINUTE TO SECOND NOT NULL");
+    f.expr("INTERVAL -'-3:4' MINUTE TO SECOND")
+        .columnType("INTERVAL MINUTE TO SECOND NOT NULL");
+    f.expr("INTERVAL -'+3:4' MINUTE TO SECOND")
+        .columnType("INTERVAL MINUTE TO SECOND NOT NULL");
+  }
+
+  /**
+   * Runs tests for INTERVAL... SECOND that should pass both parser and
+   * validator. A substantially identical set of tests exists in
+   * SqlParserTest, and any changes here should be synchronized there.
+   * Similarly, any changes to tests here should be echoed appropriately to
+   * each of the other 12 subTestIntervalXXXPositive() tests.
+   */
+  public void subTestIntervalSecondPositive() {
+    // default precision
+    f.expr("INTERVAL '1' SECOND")
+        .columnType("INTERVAL SECOND NOT NULL");
+    f.expr("INTERVAL '99' SECOND")
+        .columnType("INTERVAL SECOND NOT NULL");
+
+    // explicit precision equal to default
+    f.expr("INTERVAL '1' SECOND(2)")
+        .columnType("INTERVAL SECOND(2) NOT NULL");
+    f.expr("INTERVAL '99' SECOND(2)")
+        .columnType("INTERVAL SECOND(2) NOT NULL");
+    f.expr("INTERVAL '1' SECOND(2, 6)")
+        .columnType("INTERVAL SECOND(2, 6) NOT NULL");
+    f.expr("INTERVAL '99' SECOND(2, 6)")
+        .columnType("INTERVAL SECOND(2, 6) NOT NULL");
+
+    // max precision
+    f.expr("INTERVAL '2147483647' SECOND(10)")
+        .columnType("INTERVAL SECOND(10) NOT NULL");
+    f.expr("INTERVAL '2147483647.999999999' SECOND(10, 9)")
+        .columnType("INTERVAL SECOND(10, 9) NOT NULL");
+
+    // min precision
+    f.expr("INTERVAL '0' SECOND(1)")
+        .columnType("INTERVAL SECOND(1) NOT NULL");
+    f.expr("INTERVAL '0.0' SECOND(1, 1)")
+        .columnType("INTERVAL SECOND(1, 1) NOT NULL");
+
+    // alternate precision
+    f.expr("INTERVAL '1234' SECOND(4)")
+        .columnType("INTERVAL SECOND(4) NOT NULL");
+    f.expr("INTERVAL '1234.56789' SECOND(4, 5)")
+        .columnType("INTERVAL SECOND(4, 5) NOT NULL");
+
+    // sign
+    f.expr("INTERVAL '+1' SECOND")
+        .columnType("INTERVAL SECOND NOT NULL");
+    f.expr("INTERVAL '-1' SECOND")
+        .columnType("INTERVAL SECOND NOT NULL");
+    f.expr("INTERVAL +'1' SECOND")
+        .assertParse("INTERVAL '1' SECOND")
+        .columnType("INTERVAL SECOND NOT NULL");
+    f.expr("INTERVAL +'+1' SECOND")
+        .assertParse("INTERVAL '+1' SECOND")
+        .columnType("INTERVAL SECOND NOT NULL");
+    f.expr("INTERVAL +'-1' SECOND")
+        .assertParse("INTERVAL '-1' SECOND")
+        .columnType("INTERVAL SECOND NOT NULL");
+    f.expr("INTERVAL -'1' SECOND")
+        .columnType("INTERVAL SECOND NOT NULL");
+    f.expr("INTERVAL -'+1' SECOND")
+        .columnType("INTERVAL SECOND NOT NULL");
+    f.expr("INTERVAL -'-1' SECOND")
+        .columnType("INTERVAL SECOND NOT NULL");
+  }
+
+  /**
+   * Runs tests for INTERVAL... YEAR that should pass parser but fail
+   * validator. A substantially identical set of tests exists in
+   * SqlParserTest, and any changes here should be synchronized there.
+   * Similarly, any changes to tests here should be echoed appropriately to
+   * each of the other 12 subTestIntervalXXXNegative() tests.
+   */
+  public void subTestIntervalYearNegative() {
+    // Qualifier - field mismatches
+    f.wholeExpr("INTERVAL '-' YEAR")
+        .fails("Illegal interval literal format '-' for INTERVAL YEAR.*");
+    f.wholeExpr("INTERVAL '1-2' YEAR")
+        .fails("Illegal interval literal format '1-2' for INTERVAL YEAR.*");
+    f.wholeExpr("INTERVAL '1.2' YEAR")
+        .fails("Illegal interval literal format '1.2' for INTERVAL YEAR.*");
+    f.wholeExpr("INTERVAL '1 2' YEAR")
+        .fails("Illegal interval literal format '1 2' for INTERVAL YEAR.*");
+    f.wholeExpr("INTERVAL '1-2' YEAR(2)")
+        .fails("Illegal interval literal format '1-2' for INTERVAL YEAR\\(2\\)");
+    f.wholeExpr("INTERVAL 'bogus text' YEAR")
+        .fails("Illegal interval literal format 'bogus text' for INTERVAL YEAR.*");
+
+    // negative field values
+    f.wholeExpr("INTERVAL '--1' YEAR")
+        .fails("Illegal interval literal format '--1' for INTERVAL YEAR.*");
+
+    // Field value out of range
+    //  (default, explicit default, alt, neg alt, max, neg max)
+    f.wholeExpr("INTERVAL '100' YEAR")
+        .fails("Interval field value 100 exceeds precision of YEAR\\(2\\) field.*");
+    f.wholeExpr("INTERVAL '100' YEAR(2)")
+        .fails("Interval field value 100 exceeds precision of YEAR\\(2\\) field.*");
+    f.wholeExpr("INTERVAL '1000' YEAR(3)")
+        .fails("Interval field value 1,000 exceeds precision of YEAR\\(3\\) field.*");
+    f.wholeExpr("INTERVAL '-1000' YEAR(3)")
+        .fails("Interval field value -1,000 exceeds precision of YEAR\\(3\\) field.*");
+    f.wholeExpr("INTERVAL '2147483648' YEAR(10)")
+        .fails("Interval field value 2,147,483,648 exceeds precision of "
+            + "YEAR\\(10\\) field.*");
+    f.wholeExpr("INTERVAL '-2147483648' YEAR(10)")
+        .fails("Interval field value -2,147,483,648 exceeds precision of "
+            + "YEAR\\(10\\) field");
+
+    // precision > maximum
+    f.expr("INTERVAL '1' ^YEAR(11)^")
+        .fails("Interval leading field precision '11' out of range for "
+            + "INTERVAL YEAR\\(11\\)");
+
+    // precision < minimum allowed)
+    // note: parser will catch negative values, here we
+    // just need to check for 0
+    f.expr("INTERVAL '0' ^YEAR(0)^")
+        .fails("Interval leading field precision '0' out of range for "
+            + "INTERVAL YEAR\\(0\\)");
+  }
+
+  /**
+   * Runs tests for INTERVAL... YEAR TO MONTH that should pass parser but fail
+   * validator. A substantially identical set of tests exists in
+   * SqlParserTest, and any changes here should be synchronized there.
+   * Similarly, any changes to tests here should be echoed appropriately to
+   * each of the other 12 subTestIntervalXXXNegative() tests.
+   */
+  public void subTestIntervalYearToMonthNegative() {
+    // Qualifier - field mismatches
+    f.wholeExpr("INTERVAL '-' YEAR TO MONTH")
+        .fails("Illegal interval literal format '-' for INTERVAL YEAR TO MONTH");
+    f.wholeExpr("INTERVAL '1' YEAR TO MONTH")
+        .fails("Illegal interval literal format '1' for INTERVAL YEAR TO MONTH");
+    f.wholeExpr("INTERVAL '1:2' YEAR TO MONTH")
+        .fails("Illegal interval literal format '1:2' for INTERVAL YEAR TO MONTH");
+    f.wholeExpr("INTERVAL '1.2' YEAR TO MONTH")
+        .fails("Illegal interval literal format '1.2' for INTERVAL YEAR TO MONTH");
+    f.wholeExpr("INTERVAL '1 2' YEAR TO MONTH")
+        .fails("Illegal interval literal format '1 2' for INTERVAL YEAR TO MONTH");
+    f.wholeExpr("INTERVAL '1:2' YEAR(2) TO MONTH")
+        .fails("Illegal interval literal format '1:2' for "
+            + "INTERVAL YEAR\\(2\\) TO MONTH");
+    f.wholeExpr("INTERVAL 'bogus text' YEAR TO MONTH")
+        .fails("Illegal interval literal format 'bogus text' for "
+            + "INTERVAL YEAR TO MONTH");
+
+    // negative field values
+    f.wholeExpr("INTERVAL '--1-2' YEAR TO MONTH")
+        .fails("Illegal interval literal format '--1-2' for "
+            + "INTERVAL YEAR TO MONTH");
+    f.wholeExpr("INTERVAL '1--2' YEAR TO MONTH")
+        .fails("Illegal interval literal format '1--2' for "
+            + "INTERVAL YEAR TO MONTH");
+
+    // Field value out of range
+    //  (default, explicit default, alt, neg alt, max, neg max)
+    //  plus >max value for mid/end fields
+    f.wholeExpr("INTERVAL '100-0' YEAR TO MONTH")
+        .fails("Interval field value 100 exceeds precision of YEAR\\(2\\) field.*");
+    f.wholeExpr("INTERVAL '100-0' YEAR(2) TO MONTH")
+        .fails("Interval field value 100 exceeds precision of YEAR\\(2\\) field.*");
+    f.wholeExpr("INTERVAL '1000-0' YEAR(3) TO MONTH")
+        .fails("Interval field value 1,000 exceeds precision of YEAR\\(3\\) field.*");
+    f.wholeExpr("INTERVAL '-1000-0' YEAR(3) TO MONTH")
+        .fails("Interval field value -1,000 exceeds precision of YEAR\\(3\\) field.*");
+    f.wholeExpr("INTERVAL '2147483648-0' YEAR(10) TO MONTH")
+        .fails("Interval field value 2,147,483,648 exceeds precision of YEAR\\(10\\) field.*");
+    f.wholeExpr("INTERVAL '-2147483648-0' YEAR(10) TO MONTH")
+        .fails("Interval field value -2,147,483,648 exceeds precision of YEAR\\(10\\) field.*");
+    f.wholeExpr("INTERVAL '1-12' YEAR TO MONTH")
+        .fails("Illegal interval literal format '1-12' for INTERVAL YEAR TO MONTH.*");
+
+    // precision > maximum
+    f.expr("INTERVAL '1-1' ^YEAR(11) TO MONTH^")
+        .fails("Interval leading field precision '11' out of range for "
+            + "INTERVAL YEAR\\(11\\) TO MONTH");
+
+    // precision < minimum allowed)
+    // note: parser will catch negative values, here we
+    // just need to check for 0
+    f.expr("INTERVAL '0-0' ^YEAR(0) TO MONTH^")
+        .fails("Interval leading field precision '0' out of range for "
+            + "INTERVAL YEAR\\(0\\) TO MONTH");
+  }
+
+  /**
+   * Runs tests for INTERVAL... MONTH that should pass parser but fail
+   * validator. A substantially identical set of tests exists in
+   * SqlParserTest, and any changes here should be synchronized there.
+   * Similarly, any changes to tests here should be echoed appropriately to
+   * each of the other 12 subTestIntervalXXXNegative() tests.
+   */
+  public void subTestIntervalMonthNegative() {
+    // Qualifier - field mismatches
+    f.wholeExpr("INTERVAL '-' MONTH")
+        .fails("Illegal interval literal format '-' for INTERVAL MONTH.*");
+    f.wholeExpr("INTERVAL '1-2' MONTH")
+        .fails("Illegal interval literal format '1-2' for INTERVAL MONTH.*");
+    f.wholeExpr("INTERVAL '1.2' MONTH")
+        .fails("Illegal interval literal format '1.2' for INTERVAL MONTH.*");
+    f.wholeExpr("INTERVAL '1 2' MONTH")
+        .fails("Illegal interval literal format '1 2' for INTERVAL MONTH.*");
+    f.wholeExpr("INTERVAL '1-2' MONTH(2)")
+        .fails("Illegal interval literal format '1-2' for INTERVAL MONTH\\(2\\)");
+    f.wholeExpr("INTERVAL 'bogus text' MONTH")
+        .fails("Illegal interval literal format 'bogus text' for INTERVAL MONTH.*");
+
+    // negative field values
+    f.wholeExpr("INTERVAL '--1' MONTH")
+        .fails("Illegal interval literal format '--1' for INTERVAL MONTH.*");
+
+    // Field value out of range
+    //  (default, explicit default, alt, neg alt, max, neg max)
+    f.wholeExpr("INTERVAL '100' MONTH")
+        .fails("Interval field value 100 exceeds precision of MONTH\\(2\\) field.*");
+    f.wholeExpr("INTERVAL '100' MONTH(2)")
+        .fails("Interval field value 100 exceeds precision of MONTH\\(2\\) field.*");
+    f.wholeExpr("INTERVAL '1000' MONTH(3)")
+        .fails("Interval field value 1,000 exceeds precision of MONTH\\(3\\) field.*");
+    f.wholeExpr("INTERVAL '-1000' MONTH(3)")
+        .fails("Interval field value -1,000 exceeds precision of MONTH\\(3\\) field.*");
+    f.wholeExpr("INTERVAL '2147483648' MONTH(10)")
+        .fails("Interval field value 2,147,483,648 exceeds precision of MONTH\\(10\\) field.*");
+    f.wholeExpr("INTERVAL '-2147483648' MONTH(10)")
+        .fails("Interval field value -2,147,483,648 exceeds precision of MONTH\\(10\\) field.*");
+
+    // precision > maximum
+    f.expr("INTERVAL '1' ^MONTH(11)^")
+        .fails("Interval leading field precision '11' out of range for "
+            + "INTERVAL MONTH\\(11\\)");
+
+    // precision < minimum allowed)
+    // note: parser will catch negative values, here we
+    // just need to check for 0
+    f.expr("INTERVAL '0' ^MONTH(0)^")
+        .fails("Interval leading field precision '0' out of range for "
+            + "INTERVAL MONTH\\(0\\)");
+  }
+
+  /**
+   * Runs tests for INTERVAL... DAY that should pass parser but fail
+   * validator. A substantially identical set of tests exists in
+   * SqlParserTest, and any changes here should be synchronized there.
+   * Similarly, any changes to tests here should be echoed appropriately to
+   * each of the other 12 subTestIntervalXXXNegative() tests.
+   */
+  public void subTestIntervalDayNegative() {
+    // Qualifier - field mismatches
+    f.wholeExpr("INTERVAL '-' DAY")
+        .fails("Illegal interval literal format '-' for INTERVAL DAY.*");
+    f.wholeExpr("INTERVAL '1-2' DAY")
+        .fails("Illegal interval literal format '1-2' for INTERVAL DAY.*");
+    f.wholeExpr("INTERVAL '1.2' DAY")
+        .fails("Illegal interval literal format '1.2' for INTERVAL DAY.*");
+    f.wholeExpr("INTERVAL '1 2' DAY")
+        .fails("Illegal interval literal format '1 2' for INTERVAL DAY.*");
+    f.wholeExpr("INTERVAL '1:2' DAY")
+        .fails("Illegal interval literal format '1:2' for INTERVAL DAY.*");
+    f.wholeExpr("INTERVAL '1-2' DAY(2)")
+        .fails("Illegal interval literal format '1-2' for INTERVAL DAY\\(2\\)");
+    f.wholeExpr("INTERVAL 'bogus text' DAY")
+        .fails("Illegal interval literal format 'bogus text' for INTERVAL DAY.*");
+
+    // negative field values
+    f.wholeExpr("INTERVAL '--1' DAY")
+        .fails("Illegal interval literal format '--1' for INTERVAL DAY.*");
+
+    // Field value out of range
+    //  (default, explicit default, alt, neg alt, max, neg max)
+    f.wholeExpr("INTERVAL '100' DAY")
+        .fails("Interval field value 100 exceeds precision of DAY\\(2\\) field.*");
+    f.wholeExpr("INTERVAL '100' DAY(2)")
+        .fails("Interval field value 100 exceeds precision of DAY\\(2\\) field.*");
+    f.wholeExpr("INTERVAL '1000' DAY(3)")
+        .fails("Interval field value 1,000 exceeds precision of DAY\\(3\\) field.*");
+    f.wholeExpr("INTERVAL '-1000' DAY(3)")
+        .fails("Interval field value -1,000 exceeds precision of DAY\\(3\\) field.*");
+    f.wholeExpr("INTERVAL '2147483648' DAY(10)")
+        .fails("Interval field value 2,147,483,648 exceeds precision of "
+            + "DAY\\(10\\) field.*");
+    f.wholeExpr("INTERVAL '-2147483648' DAY(10)")
+        .fails("Interval field value -2,147,483,648 exceeds precision of "
+            + "DAY\\(10\\) field.*");
+
+    // precision > maximum
+    f.expr("INTERVAL '1' ^DAY(11)^")
+        .fails("Interval leading field precision '11' out of range for "
+            + "INTERVAL DAY\\(11\\)");
+
+    // precision < minimum allowed)
+    // note: parser will catch negative values, here we
+    // just need to check for 0
+    f.expr("INTERVAL '0' ^DAY(0)^")
+        .fails("Interval leading field precision '0' out of range for "
+            + "INTERVAL DAY\\(0\\)");
+  }
+
+  /**
+   * Runs tests for INTERVAL... DAY TO HOUR that should pass parser but fail
+   * validator. A substantially identical set of tests exists in
+   * SqlParserTest, and any changes here should be synchronized there.
+   * Similarly, any changes to tests here should be echoed appropriately to
+   * each of the other 12 subTestIntervalXXXNegative() tests.
+   */
+  public void subTestIntervalDayToHourNegative() {
+    // Qualifier - field mismatches
+    f.wholeExpr("INTERVAL '-' DAY TO HOUR")
+        .fails("Illegal interval literal format '-' for INTERVAL DAY TO HOUR");
+    f.wholeExpr("INTERVAL '1' DAY TO HOUR")
+        .fails("Illegal interval literal format '1' for INTERVAL DAY TO HOUR");
+    f.wholeExpr("INTERVAL '1:2' DAY TO HOUR")
+        .fails("Illegal interval literal format '1:2' for INTERVAL DAY TO HOUR");
+    f.wholeExpr("INTERVAL '1.2' DAY TO HOUR")
+        .fails("Illegal interval literal format '1.2' for INTERVAL DAY TO HOUR");
+    f.wholeExpr("INTERVAL '1 x' DAY TO HOUR")
+        .fails("Illegal interval literal format '1 x' for INTERVAL DAY TO HOUR");
+    f.wholeExpr("INTERVAL ' ' DAY TO HOUR")
+        .fails("Illegal interval literal format ' ' for INTERVAL DAY TO HOUR");
+    f.wholeExpr("INTERVAL '1:2' DAY(2) TO HOUR")
+        .fails("Illegal interval literal format '1:2' for "
+            + "INTERVAL DAY\\(2\\) TO HOUR");
+    f.wholeExpr("INTERVAL 'bogus text' DAY TO HOUR")
+        .fails("Illegal interval literal format 'bogus text' for "
+            + "INTERVAL DAY TO HOUR");
+
+    // negative field values
+    f.wholeExpr("INTERVAL '--1 1' DAY TO HOUR")
+        .fails("Illegal interval literal format '--1 1' for INTERVAL DAY TO HOUR");
+    f.wholeExpr("INTERVAL '1 -1' DAY TO HOUR")
+        .fails("Illegal interval literal format '1 -1' for INTERVAL DAY TO HOUR");
+
+    // Field value out of range
+    //  (default, explicit default, alt, neg alt, max, neg max)
+    //  plus >max value for mid/end fields
+    f.wholeExpr("INTERVAL '100 0' DAY TO HOUR")
+        .fails("Interval field value 100 exceeds precision of DAY\\(2\\) field.*");
+    f.wholeExpr("INTERVAL '100 0' DAY(2) TO HOUR")
+        .fails("Interval field value 100 exceeds precision of DAY\\(2\\) field.*");
+    f.wholeExpr("INTERVAL '1000 0' DAY(3) TO HOUR")
+        .fails("Interval field value 1,000 exceeds precision of DAY\\(3\\) field.*");
+    f.wholeExpr("INTERVAL '-1000 0' DAY(3) TO HOUR")
+        .fails("Interval field value -1,000 exceeds precision of DAY\\(3\\) field.*");
+    f.wholeExpr("INTERVAL '2147483648 0' DAY(10) TO HOUR")
+        .fails("Interval field value 2,147,483,648 exceeds precision of DAY\\(10\\) field.*");
+    f.wholeExpr("INTERVAL '-2147483648 0' DAY(10) TO HOUR")
+        .fails("Interval field value -2,147,483,648 exceeds precision of "
+            + "DAY\\(10\\) field.*");
+    f.wholeExpr("INTERVAL '1 24' DAY TO HOUR")
+        .fails("Illegal interval literal format '1 24' for INTERVAL DAY TO HOUR.*");
+
+    // precision > maximum
+    f.expr("INTERVAL '1 1' ^DAY(11) TO HOUR^")
+        .fails("Interval leading field precision '11' out of range for "
+            + "INTERVAL DAY\\(11\\) TO HOUR");
+
+    // precision < minimum allowed)
+    // note: parser will catch negative values, here we
+    // just need to check for 0
+    f.expr("INTERVAL '0 0' ^DAY(0) TO HOUR^")
+        .fails("Interval leading field precision '0' out of range for INTERVAL DAY\\(0\\) TO HOUR");
+  }
+
+  /**
+   * Runs tests for INTERVAL... DAY TO MINUTE that should pass parser but fail
+   * validator. A substantially identical set of tests exists in
+   * SqlParserTest, and any changes here should be synchronized there.
+   * Similarly, any changes to tests here should be echoed appropriately to
+   * each of the other 12 subTestIntervalXXXNegative() tests.
+   */
+  public void subTestIntervalDayToMinuteNegative() {
+    // Qualifier - field mismatches
+    f.wholeExpr("INTERVAL ' :' DAY TO MINUTE")
+        .fails("Illegal interval literal format ' :' for INTERVAL DAY TO MINUTE");
+    f.wholeExpr("INTERVAL '1' DAY TO MINUTE")
+        .fails("Illegal interval literal format '1' for INTERVAL DAY TO MINUTE");
+    f.wholeExpr("INTERVAL '1 2' DAY TO MINUTE")
+        .fails("Illegal interval literal format '1 2' for INTERVAL DAY TO MINUTE");
+    f.wholeExpr("INTERVAL '1:2' DAY TO MINUTE")
+        .fails("Illegal interval literal format '1:2' for INTERVAL DAY TO MINUTE");
+    f.wholeExpr("INTERVAL '1.2' DAY TO MINUTE")
+        .fails("Illegal interval literal format '1.2' for INTERVAL DAY TO MINUTE");
+    f.wholeExpr("INTERVAL 'x 1:1' DAY TO MINUTE")
+        .fails("Illegal interval literal format 'x 1:1' for INTERVAL DAY TO MINUTE");
+    f.wholeExpr("INTERVAL '1 x:1' DAY TO MINUTE")
+        .fails("Illegal interval literal format '1 x:1' for INTERVAL DAY TO MINUTE");
+    f.wholeExpr("INTERVAL '1 1:x' DAY TO MINUTE")
+        .fails("Illegal interval literal format '1 1:x' for INTERVAL DAY TO MINUTE");
+    f.wholeExpr("INTERVAL '1 1:2:3' DAY TO MINUTE")
+        .fails("Illegal interval literal format '1 1:2:3' for INTERVAL DAY TO MINUTE");
+    f.wholeExpr("INTERVAL '1 1:1:1.2' DAY TO MINUTE")
+        .fails("Illegal interval literal format '1 1:1:1.2' for INTERVAL DAY TO MINUTE");
+    f.wholeExpr("INTERVAL '1 1:2:3' DAY(2) TO MINUTE")
+        .fails("Illegal interval literal format '1 1:2:3' for "
+            + "INTERVAL DAY\\(2\\) TO MINUTE");
+    f.wholeExpr("INTERVAL '1 1' DAY(2) TO MINUTE")
+        .fails("Illegal interval literal format '1 1' for "
+            + "INTERVAL DAY\\(2\\) TO MINUTE");
+    f.wholeExpr("INTERVAL 'bogus text' DAY TO MINUTE")
+        .fails("Illegal interval literal format 'bogus text' for "
+            + "INTERVAL DAY TO MINUTE");
+
+    // negative field values
+    f.wholeExpr("INTERVAL '--1 1:1' DAY TO MINUTE")
+        .fails("Illegal interval literal format '--1 1:1' for INTERVAL DAY TO MINUTE");
+    f.wholeExpr("INTERVAL '1 -1:1' DAY TO MINUTE")
+        .fails("Illegal interval literal format '1 -1:1' for INTERVAL DAY TO MINUTE");
+    f.wholeExpr("INTERVAL '1 1:-1' DAY TO MINUTE")
+        .fails("Illegal interval literal format '1 1:-1' for INTERVAL DAY TO MINUTE");
+
+    // Field value out of range
+    //  (default, explicit default, alt, neg alt, max, neg max)
+    //  plus >max value for mid/end fields
+    f.wholeExpr("INTERVAL '100 0:0' DAY TO MINUTE")
+        .fails("Interval field value 100 exceeds precision of DAY\\(2\\) field.*");
+    f.wholeExpr("INTERVAL '100 0:0' DAY(2) TO MINUTE")
+        .fails("Interval field value 100 exceeds precision of DAY\\(2\\) field.*");
+    f.wholeExpr("INTERVAL '1000 0:0' DAY(3) TO MINUTE")
+        .fails("Interval field value 1,000 exceeds precision of DAY\\(3\\) field.*");
+    f.wholeExpr("INTERVAL '-1000 0:0' DAY(3) TO MINUTE")
+        .fails("Interval field value -1,000 exceeds precision of DAY\\(3\\) field.*");
+    f.wholeExpr("INTERVAL '2147483648 0:0' DAY(10) TO MINUTE")
+        .fails("Interval field value 2,147,483,648 exceeds precision of "
+            + "DAY\\(10\\) field.*");
+    f.wholeExpr("INTERVAL '-2147483648 0:0' DAY(10) TO MINUTE")
+        .fails("Interval field value -2,147,483,648 exceeds precision of "
+            + "DAY\\(10\\) field.*");
+    f.wholeExpr("INTERVAL '1 24:1' DAY TO MINUTE")
+        .fails("Illegal interval literal format '1 24:1' for "
+            + "INTERVAL DAY TO MINUTE.*");
+    f.wholeExpr("INTERVAL '1 1:60' DAY TO MINUTE")
+        .fails("Illegal interval literal format '1 1:60' for INTERVAL DAY TO MINUTE.*");
+
+    // precision > maximum
+    f.expr("INTERVAL '1 1:1' ^DAY(11) TO MINUTE^")
+        .fails("Interval leading field precision '11' out of range for "
+            + "INTERVAL DAY\\(11\\) TO MINUTE");
+
+    // precision < minimum allowed)
+    // note: parser will catch negative values, here we
+    // just need to check for 0
+    f.expr("INTERVAL '0 0' ^DAY(0) TO MINUTE^")
+        .fails("Interval leading field precision '0' out of range for "
+            + "INTERVAL DAY\\(0\\) TO MINUTE");
+  }
+
+  /**
+   * Runs tests for INTERVAL... DAY TO SECOND that should pass parser but fail
+   * validator. A substantially identical set of tests exists in
+   * SqlParserTest, and any changes here should be synchronized there.
+   * Similarly, any changes to tests here should be echoed appropriately to
+   * each of the other 12 subTestIntervalXXXNegative() tests.
+   */
+  public void subTestIntervalDayToSecondNegative() {
+    // Qualifier - field mismatches
+    f.wholeExpr("INTERVAL ' ::' DAY TO SECOND")
+        .fails("Illegal interval literal format ' ::' for INTERVAL DAY TO SECOND");
+    f.wholeExpr("INTERVAL ' ::.' DAY TO SECOND")
+        .fails("Illegal interval literal format ' ::\\.' for INTERVAL DAY TO SECOND");
+    f.wholeExpr("INTERVAL '1' DAY TO SECOND")
+        .fails("Illegal interval literal format '1' for INTERVAL DAY TO SECOND");
+    f.wholeExpr("INTERVAL '1 2' DAY TO SECOND")
+        .fails("Illegal interval literal format '1 2' for INTERVAL DAY TO SECOND");
+    f.wholeExpr("INTERVAL '1:2' DAY TO SECOND")
+        .fails("Illegal interval literal format '1:2' for "
+            + "INTERVAL DAY TO SECOND");
+    f.wholeExpr("INTERVAL '1.2' DAY TO SECOND")
+        .fails("Illegal interval literal format '1\\.2' for "
+            + "INTERVAL DAY TO SECOND");
+    f.wholeExpr("INTERVAL '1 1:2' DAY TO SECOND")
+        .fails("Illegal interval literal format '1 1:2' for "
+            + "INTERVAL DAY TO SECOND");
+    f.wholeExpr("INTERVAL '1 1:2:x' DAY TO SECOND")
+        .fails("Illegal interval literal format '1 1:2:x' for "
+            + "INTERVAL DAY TO SECOND");
+    f.wholeExpr("INTERVAL '1:2:3' DAY TO SECOND")
+        .fails("Illegal interval literal format '1:2:3' for "
+            + "INTERVAL DAY TO SECOND");
+    f.wholeExpr("INTERVAL '1:1:1.2' DAY TO SECOND")
+        .fails("Illegal interval literal format '1:1:1\\.2' for "
+            + "INTERVAL DAY TO SECOND");
+    f.wholeExpr("INTERVAL '1 1:2' DAY(2) TO SECOND")
+        .fails("Illegal interval literal format '1 1:2' for "
+            + "INTERVAL DAY\\(2\\) TO SECOND");
+    f.wholeExpr("INTERVAL '1 1' DAY(2) TO SECOND")
+        .fails("Illegal interval literal format '1 1' for "
+            + "INTERVAL DAY\\(2\\) TO SECOND");
+    f.wholeExpr("INTERVAL 'bogus text' DAY TO SECOND")
+        .fails("Illegal interval literal format 'bogus text' for "
+            + "INTERVAL DAY TO SECOND");
+    f.wholeExpr("INTERVAL '2345 6:7:8901' DAY TO SECOND(4)")
+        .fails("Illegal interval literal format '2345 6:7:8901' for "
+            + "INTERVAL DAY TO SECOND\\(4\\)");
+
+    // negative field values
+    f.wholeExpr("INTERVAL '--1 1:1:1' DAY TO SECOND")
+        .fails("Illegal interval literal format '--1 1:1:1' for "
+            + "INTERVAL DAY TO SECOND");
+    f.wholeExpr("INTERVAL '1 -1:1:1' DAY TO SECOND")
+        .fails("Illegal interval literal format '1 -1:1:1' for "
+            + "INTERVAL DAY TO SECOND");
+    f.wholeExpr("INTERVAL '1 1:-1:1' DAY TO SECOND")
+        .fails("Illegal interval literal format '1 1:-1:1' for "
+            + "INTERVAL DAY TO SECOND");
+    f.wholeExpr("INTERVAL '1 1:1:-1' DAY TO SECOND")
+        .fails("Illegal interval literal format '1 1:1:-1' for "
+            + "INTERVAL DAY TO SECOND");
+    f.wholeExpr("INTERVAL '1 1:1:1.-1' DAY TO SECOND")
+        .fails("Illegal interval literal format '1 1:1:1.-1' for "
+            + "INTERVAL DAY TO SECOND");
+
+    // Field value out of range
+    //  (default, explicit default, alt, neg alt, max, neg max)
+    //  plus >max value for mid/end fields
+    f.wholeExpr("INTERVAL '100 0' DAY TO SECOND")
+        .fails("Illegal interval literal format '100 0' for "
+            + "INTERVAL DAY TO SECOND.*");
+    f.wholeExpr("INTERVAL '100 0' DAY(2) TO SECOND")
+        .fails("Illegal interval literal format '100 0' for "
+            + "INTERVAL DAY\\(2\\) TO SECOND.*");
+    f.wholeExpr("INTERVAL '1000 0' DAY(3) TO SECOND")
+        .fails("Illegal interval literal format '1000 0' for "
+            + "INTERVAL DAY\\(3\\) TO SECOND.*");
+    f.wholeExpr("INTERVAL '-1000 0' DAY(3) TO SECOND")
+        .fails("Illegal interval literal format '-1000 0' for "
+            + "INTERVAL DAY\\(3\\) TO SECOND.*");
+    f.wholeExpr("INTERVAL '2147483648 1:1:0' DAY(10) TO SECOND")
+        .fails("Interval field value 2,147,483,648 exceeds precision of "
+            + "DAY\\(10\\) field.*");
+    f.wholeExpr("INTERVAL '-2147483648 1:1:0' DAY(10) TO SECOND")
+        .fails("Interval field value -2,147,483,648 exceeds precision of "
+            + "DAY\\(10\\) field.*");
+    f.wholeExpr("INTERVAL '2147483648 0' DAY(10) TO SECOND")
+        .fails("Illegal interval literal format '2147483648 0' for "
+            + "INTERVAL DAY\\(10\\) TO SECOND.*");
+    f.wholeExpr("INTERVAL '-2147483648 0' DAY(10) TO SECOND")
+        .fails("Illegal interval literal format '-2147483648 0' for "
+            + "INTERVAL DAY\\(10\\) TO SECOND.*");
+    f.wholeExpr("INTERVAL '1 24:1:1' DAY TO SECOND")
+        .fails("Illegal interval literal format '1 24:1:1' for "
+            + "INTERVAL DAY TO SECOND.*");
+    f.wholeExpr("INTERVAL '1 1:60:1' DAY TO SECOND")
+        .fails("Illegal interval literal format '1 1:60:1' for "
+            + "INTERVAL DAY TO SECOND.*");
+    f.wholeExpr("INTERVAL '1 1:1:60' DAY TO SECOND")
+        .fails("Illegal interval literal format '1 1:1:60' for "
+            + "INTERVAL DAY TO SECOND.*");
+    f.wholeExpr("INTERVAL '1 1:1:1.0000001' DAY TO SECOND")
+        .fails("Illegal interval literal format '1 1:1:1\\.0000001' for "
+            + "INTERVAL DAY TO SECOND.*");
+    f.wholeExpr("INTERVAL '1 1:1:1.0001' DAY TO SECOND(3)")
+        .fails("Illegal interval literal format '1 1:1:1\\.0001' for "
+            + "INTERVAL DAY TO SECOND\\(3\\).*");
+
+    // precision > maximum
+    f.expr("INTERVAL '1 1' ^DAY(11) TO SECOND^")
+        .fails("Interval leading field precision '11' out of range for "
+            + "INTERVAL DAY\\(11\\) TO SECOND");
+    f.expr("INTERVAL '1 1' ^DAY TO SECOND(10)^")
+        .fails("Interval fractional second precision '10' out of range for "
+            + "INTERVAL DAY TO SECOND\\(10\\)");
+
+    // precision < minimum allowed)
+    // note: parser will catch negative values, here we
+    // just need to check for 0
+    f.expr("INTERVAL '0 0:0:0' ^DAY(0) TO SECOND^")
+        .fails("Interval leading field precision '0' out of range for "
+            + "INTERVAL DAY\\(0\\) TO SECOND");
+    f.expr("INTERVAL '0 0:0:0' ^DAY TO SECOND(0)^")
+        .fails("Interval fractional second precision '0' out of range for "
+            + "INTERVAL DAY TO SECOND\\(0\\)");
+  }
+
+  /**
+   * Runs tests for INTERVAL... HOUR that should pass parser but fail
+   * validator. A substantially identical set of tests exists in
+   * SqlParserTest, and any changes here should be synchronized there.
+   * Similarly, any changes to tests here should be echoed appropriately to
+   * each of the other 12 subTestIntervalXXXNegative() tests.
+   */
+  public void subTestIntervalHourNegative() {
+    // Qualifier - field mismatches
+    f.wholeExpr("INTERVAL '-' HOUR")
+        .fails("Illegal interval literal format '-' for INTERVAL HOUR.*");
+    f.wholeExpr("INTERVAL '1-2' HOUR")
+        .fails("Illegal interval literal format '1-2' for INTERVAL HOUR.*");
+    f.wholeExpr("INTERVAL '1.2' HOUR")
+        .fails("Illegal interval literal format '1.2' for INTERVAL HOUR.*");
+    f.wholeExpr("INTERVAL '1 2' HOUR")
+        .fails("Illegal interval literal format '1 2' for INTERVAL HOUR.*");
+    f.wholeExpr("INTERVAL '1:2' HOUR")
+        .fails("Illegal interval literal format '1:2' for INTERVAL HOUR.*");
+    f.wholeExpr("INTERVAL '1-2' HOUR(2)")
+        .fails("Illegal interval literal format '1-2' for INTERVAL HOUR\\(2\\)");
+    f.wholeExpr("INTERVAL 'bogus text' HOUR")
+        .fails("Illegal interval literal format 'bogus text' for "
+            + "INTERVAL HOUR.*");
+
+    // negative field values
+    f.wholeExpr("INTERVAL '--1' HOUR")
+        .fails("Illegal interval literal format '--1' for INTERVAL HOUR.*");
+
+    // Field value out of range
+    //  (default, explicit default, alt, neg alt, max, neg max)
+    f.wholeExpr("INTERVAL '100' HOUR")
+        .fails("Interval field value 100 exceeds precision of "
+            + "HOUR\\(2\\) field.*");
+    f.wholeExpr("INTERVAL '100' HOUR(2)")
+        .fails("Interval field value 100 exceeds precision of "
+            + "HOUR\\(2\\) field.*");
+    f.wholeExpr("INTERVAL '1000' HOUR(3)")
+        .fails("Interval field value 1,000 exceeds precision of "
+            + "HOUR\\(3\\) field.*");
+    f.wholeExpr("INTERVAL '-1000' HOUR(3)")
+        .fails("Interval field value -1,000 exceeds precision of "
+            + "HOUR\\(3\\) field.*");
+    f.wholeExpr("INTERVAL '2147483648' HOUR(10)")
+        .fails("Interval field value 2,147,483,648 exceeds precision of "
+            + "HOUR\\(10\\) field.*");
+    f.wholeExpr("INTERVAL '-2147483648' HOUR(10)")
+        .fails("Interval field value -2,147,483,648 exceeds precision of "
+            + "HOUR\\(10\\) field.*");
+
+    // precision > maximum
+    f.expr("INTERVAL '1' ^HOUR(11)^")
+        .fails("Interval leading field precision '11' out of range for "
+            + "INTERVAL HOUR\\(11\\)");
+
+    // precision < minimum allowed)
+    // note: parser will catch negative values, here we
+    // just need to check for 0
+    f.expr("INTERVAL '0' ^HOUR(0)^")
+        .fails("Interval leading field precision '0' out of range for "
+            + "INTERVAL HOUR\\(0\\)");
+  }
+
+  /**
+   * Runs tests for INTERVAL... HOUR TO MINUTE that should pass parser but
+   * fail validator. A substantially identical set of tests exists in
+   * SqlParserTest, and any changes here should be synchronized there.
+   * Similarly, any changes to tests here should be echoed appropriately to
+   * each of the other 12 subTestIntervalXXXNegative() tests.
+   */
+  public void subTestIntervalHourToMinuteNegative() {
+    // Qualifier - field mismatches
+    f.wholeExpr("INTERVAL ':' HOUR TO MINUTE")
+        .fails("Illegal interval literal format ':' for INTERVAL HOUR TO MINUTE");
+    f.wholeExpr("INTERVAL '1' HOUR TO MINUTE")
+        .fails("Illegal interval literal format '1' for INTERVAL HOUR TO MINUTE");
+    f.wholeExpr("INTERVAL '1:x' HOUR TO MINUTE")
+        .fails("Illegal interval literal format '1:x' for INTERVAL HOUR TO MINUTE");
+    f.wholeExpr("INTERVAL '1.2' HOUR TO MINUTE")
+        .fails("Illegal interval literal format '1.2' for INTERVAL HOUR TO MINUTE");
+    f.wholeExpr("INTERVAL '1 2' HOUR TO MINUTE")
+        .fails("Illegal interval literal format '1 2' for INTERVAL HOUR TO MINUTE");
+    f.wholeExpr("INTERVAL '1:2:3' HOUR TO MINUTE")
+        .fails("Illegal interval literal format '1:2:3' for INTERVAL HOUR TO MINUTE");
+    f.wholeExpr("INTERVAL '1 2' HOUR(2) TO MINUTE")
+        .fails("Illegal interval literal format '1 2' for "
+            + "INTERVAL HOUR\\(2\\) TO MINUTE");
+    f.wholeExpr("INTERVAL 'bogus text' HOUR TO MINUTE")
+        .fails("Illegal interval literal format 'bogus text' for "
+            + "INTERVAL HOUR TO MINUTE");
+
+    // negative field values
+    f.wholeExpr("INTERVAL '--1:1' HOUR TO MINUTE")
+        .fails("Illegal interval literal format '--1:1' for INTERVAL HOUR TO MINUTE");
+    f.wholeExpr("INTERVAL '1:-1' HOUR TO MINUTE")
+        .fails("Illegal interval literal format '1:-1' for INTERVAL HOUR TO MINUTE");
+
+    // Field value out of range
+    //  (default, explicit default, alt, neg alt, max, neg max)
+    //  plus >max value for mid/end fields
+    f.wholeExpr("INTERVAL '100:0' HOUR TO MINUTE")
+        .fails("Interval field value 100 exceeds precision of HOUR\\(2\\) field.*");
+    f.wholeExpr("INTERVAL '100:0' HOUR(2) TO MINUTE")
+        .fails("Interval field value 100 exceeds precision of HOUR\\(2\\) field.*");
+    f.wholeExpr("INTERVAL '1000:0' HOUR(3) TO MINUTE")
+        .fails("Interval field value 1,000 exceeds precision of HOUR\\(3\\) field.*");
+    f.wholeExpr("INTERVAL '-1000:0' HOUR(3) TO MINUTE")
+        .fails("Interval field value -1,000 exceeds precision of HOUR\\(3\\) field.*");
+    f.wholeExpr("INTERVAL '2147483648:0' HOUR(10) TO MINUTE")
+        .fails("Interval field value 2,147,483,648 exceeds precision of HOUR\\(10\\) field.*");
+    f.wholeExpr("INTERVAL '-2147483648:0' HOUR(10) TO MINUTE")
+        .fails("Interval field value -2,147,483,648 exceeds precision of HOUR\\(10\\) field.*");
+    f.wholeExpr("INTERVAL '1:60' HOUR TO MINUTE")
+        .fails("Illegal interval literal format '1:60' for INTERVAL HOUR TO MINUTE.*");
+
+    // precision > maximum
+    f.expr("INTERVAL '1:1' ^HOUR(11) TO MINUTE^")
+        .fails("Interval leading field precision '11' out of range for "
+            + "INTERVAL HOUR\\(11\\) TO MINUTE");
+
+    // precision < minimum allowed)
+    // note: parser will catch negative values, here we
+    // just need to check for 0
+    f.expr("INTERVAL '0:0' ^HOUR(0) TO MINUTE^")
+        .fails("Interval leading field precision '0' out of range for "
+            + "INTERVAL HOUR\\(0\\) TO MINUTE");
+  }
+
+  /**
+   * Runs tests for INTERVAL... HOUR TO SECOND that should pass parser but
+   * fail validator. A substantially identical set of tests exists in
+   * SqlParserTest, and any changes here should be synchronized there.
+   * Similarly, any changes to tests here should be echoed appropriately to
+   * each of the other 12 subTestIntervalXXXNegative() tests.
+   */
+  public void subTestIntervalHourToSecondNegative() {
+    // Qualifier - field mismatches
+    f.wholeExpr("INTERVAL '::' HOUR TO SECOND")
+        .fails("Illegal interval literal format '::' for INTERVAL HOUR TO SECOND");
+    f.wholeExpr("INTERVAL '::.' HOUR TO SECOND")
+        .fails("Illegal interval literal format '::\\.' for INTERVAL HOUR TO SECOND");
+    f.wholeExpr("INTERVAL '1' HOUR TO SECOND")
+        .fails("Illegal interval literal format '1' for INTERVAL HOUR TO SECOND");
+    f.wholeExpr("INTERVAL '1 2' HOUR TO SECOND")
+        .fails("Illegal interval literal format '1 2' for INTERVAL HOUR TO SECOND");
+    f.wholeExpr("INTERVAL '1:2' HOUR TO SECOND")
+        .fails("Illegal interval literal format '1:2' for INTERVAL HOUR TO SECOND");
+    f.wholeExpr("INTERVAL '1.2' HOUR TO SECOND")
+        .fails("Illegal interval literal format '1\\.2' for INTERVAL HOUR TO SECOND");
+    f.wholeExpr("INTERVAL '1 1:2' HOUR TO SECOND")
+        .fails("Illegal interval literal format '1 1:2' for INTERVAL HOUR TO SECOND");
+    f.wholeExpr("INTERVAL '1:2:x' HOUR TO SECOND")
+        .fails("Illegal interval literal format '1:2:x' for INTERVAL HOUR TO SECOND");
+    f.wholeExpr("INTERVAL '1:x:3' HOUR TO SECOND")
+        .fails("Illegal interval literal format '1:x:3' for INTERVAL HOUR TO SECOND");
+    f.wholeExpr("INTERVAL '1:1:1.x' HOUR TO SECOND")
+        .fails("Illegal interval literal format '1:1:1\\.x' for INTERVAL HOUR TO SECOND");
+    f.wholeExpr("INTERVAL '1 1:2' HOUR(2) TO SECOND")
+        .fails("Illegal interval literal format '1 1:2' for INTERVAL HOUR\\(2\\) TO SECOND");
+    f.wholeExpr("INTERVAL '1 1' HOUR(2) TO SECOND")
+        .fails("Illegal interval literal format '1 1' for INTERVAL HOUR\\(2\\) TO SECOND");
+    f.wholeExpr("INTERVAL 'bogus text' HOUR TO SECOND")
+        .fails("Illegal interval literal format 'bogus text' for INTERVAL HOUR TO SECOND");
+    f.wholeExpr("INTERVAL '6:7:8901' HOUR TO SECOND(4)")
+        .fails("Illegal interval literal format '6:7:8901' for INTERVAL HOUR TO SECOND\\(4\\)");
+
+    // negative field values
+    f.wholeExpr("INTERVAL '--1:1:1' HOUR TO SECOND")
+        .fails("Illegal interval literal format '--1:1:1' for INTERVAL HOUR TO SECOND");
+    f.wholeExpr("INTERVAL '1:-1:1' HOUR TO SECOND")
+        .fails("Illegal interval literal format '1:-1:1' for INTERVAL HOUR TO SECOND");
+    f.wholeExpr("INTERVAL '1:1:-1' HOUR TO SECOND")
+        .fails("Illegal interval literal format '1:1:-1' for INTERVAL HOUR TO SECOND");
+    f.wholeExpr("INTERVAL '1:1:1.-1' HOUR TO SECOND")
+        .fails("Illegal interval literal format '1:1:1\\.-1' for INTERVAL HOUR TO SECOND");
+
+    // Field value out of range
+    //  (default, explicit default, alt, neg alt, max, neg max)
+    //  plus >max value for mid/end fields
+    f.wholeExpr("INTERVAL '100:0:0' HOUR TO SECOND")
+        .fails("Interval field value 100 exceeds precision of "
+            + "HOUR\\(2\\) field.*");
+    f.wholeExpr("INTERVAL '100:0:0' HOUR(2) TO SECOND")
+        .fails("Interval field value 100 exceeds precision of "
+            + "HOUR\\(2\\) field.*");
+    f.wholeExpr("INTERVAL '1000:0:0' HOUR(3) TO SECOND")
+        .fails("Interval field value 1,000 exceeds precision of "
+            + "HOUR\\(3\\) field.*");
+    f.wholeExpr("INTERVAL '-1000:0:0' HOUR(3) TO SECOND")
+        .fails("Interval field value -1,000 exceeds precision of "
+            + "HOUR\\(3\\) field.*");
+    f.wholeExpr("INTERVAL '2147483648:0:0' HOUR(10) TO SECOND")
+        .fails("Interval field value 2,147,483,648 exceeds precision of "
+            + "HOUR\\(10\\) field.*");
+    f.wholeExpr("INTERVAL '-2147483648:0:0' HOUR(10) TO SECOND")
+        .fails("Interval field value -2,147,483,648 exceeds precision of "
+            + "HOUR\\(10\\) field.*");
+    f.wholeExpr("INTERVAL '1:60:1' HOUR TO SECOND")
+        .fails("Illegal interval literal format '1:60:1' for "
+            + "INTERVAL HOUR TO SECOND.*");
+    f.wholeExpr("INTERVAL '1:1:60' HOUR TO SECOND")
+        .fails("Illegal interval literal format '1:1:60' for "
+            + "INTERVAL HOUR TO SECOND.*");
+    f.wholeExpr("INTERVAL '1:1:1.0000001' HOUR TO SECOND")
+        .fails("Illegal interval literal format '1:1:1\\.0000001' for "
+            + "INTERVAL HOUR TO SECOND.*");
+    f.wholeExpr("INTERVAL '1:1:1.0001' HOUR TO SECOND(3)")
+        .fails("Illegal interval literal format '1:1:1\\.0001' for "
+            + "INTERVAL HOUR TO SECOND\\(3\\).*");
+
+    // precision > maximum
+    f.expr("INTERVAL '1:1:1' ^HOUR(11) TO SECOND^")
+        .fails("Interval leading field precision '11' out of range for "
+            + "INTERVAL HOUR\\(11\\) TO SECOND");
+    f.expr("INTERVAL '1:1:1' ^HOUR TO SECOND(10)^")
+        .fails("Interval fractional second precision '10' out of range for "
+            + "INTERVAL HOUR TO SECOND\\(10\\)");
+
+    // precision < minimum allowed)
+    // note: parser will catch negative values, here we
+    // just need to check for 0
+    f.expr("INTERVAL '0:0:0' ^HOUR(0) TO SECOND^")
+        .fails("Interval leading field precision '0' out of range for "
+            + "INTERVAL HOUR\\(0\\) TO SECOND");
+    f.expr("INTERVAL '0:0:0' ^HOUR TO SECOND(0)^")
+        .fails("Interval fractional second precision '0' out of range for "
+            + "INTERVAL HOUR TO SECOND\\(0\\)");
+  }
+
+  /**
+   * Runs tests for INTERVAL... MINUTE that should pass parser but fail
+   * validator. A substantially identical set of tests exists in
+   * SqlParserTest, and any changes here should be synchronized there.
+   * Similarly, any changes to tests here should be echoed appropriately to
+   * each of the other 12 subTestIntervalXXXNegative() tests.
+   */
+  public void subTestIntervalMinuteNegative() {
+    // Qualifier - field mismatches
+    f.wholeExpr("INTERVAL '-' MINUTE")
+        .fails("Illegal interval literal format '-' for INTERVAL MINUTE.*");
+    f.wholeExpr("INTERVAL '1-2' MINUTE")
+        .fails("Illegal interval literal format '1-2' for INTERVAL MINUTE.*");
+    f.wholeExpr("INTERVAL '1.2' MINUTE")
+        .fails("Illegal interval literal format '1.2' for INTERVAL MINUTE.*");
+    f.wholeExpr("INTERVAL '1 2' MINUTE")
+        .fails("Illegal interval literal format '1 2' for INTERVAL MINUTE.*");
+    f.wholeExpr("INTERVAL '1:2' MINUTE")
+        .fails("Illegal interval literal format '1:2' for INTERVAL MINUTE.*");
+    f.wholeExpr("INTERVAL '1-2' MINUTE(2)")
+        .fails("Illegal interval literal format '1-2' for INTERVAL MINUTE\\(2\\)");
+    f.wholeExpr("INTERVAL 'bogus text' MINUTE")
+        .fails("Illegal interval literal format 'bogus text' for INTERVAL MINUTE.*");
+
+    // negative field values
+    f.wholeExpr("INTERVAL '--1' MINUTE")
+        .fails("Illegal interval literal format '--1' for INTERVAL MINUTE.*");
+
+    // Field value out of range
+    //  (default, explicit default, alt, neg alt, max, neg max)
+    f.wholeExpr("INTERVAL '100' MINUTE")
+        .fails("Interval field value 100 exceeds precision of MINUTE\\(2\\) field.*");
+    f.wholeExpr("INTERVAL '100' MINUTE(2)")
+        .fails("Interval field value 100 exceeds precision of MINUTE\\(2\\) field.*");
+    f.wholeExpr("INTERVAL '1000' MINUTE(3)")
+        .fails("Interval field value 1,000 exceeds precision of MINUTE\\(3\\) field.*");
+    f.wholeExpr("INTERVAL '-1000' MINUTE(3)")
+        .fails("Interval field value -1,000 exceeds precision of MINUTE\\(3\\) field.*");
+    f.wholeExpr("INTERVAL '2147483648' MINUTE(10)")
+        .fails("Interval field value 2,147,483,648 exceeds precision of MINUTE\\(10\\) field.*");
+    f.wholeExpr("INTERVAL '-2147483648' MINUTE(10)")
+        .fails("Interval field value -2,147,483,648 exceeds precision of MINUTE\\(10\\) field.*");
+
+    // precision > maximum
+    f.expr("INTERVAL '1' ^MINUTE(11)^")
+        .fails("Interval leading field precision '11' out of range for "
+            + "INTERVAL MINUTE\\(11\\)");
+
+    // precision < minimum allowed)
+    // note: parser will catch negative values, here we
+    // just need to check for 0
+    f.expr("INTERVAL '0' ^MINUTE(0)^")
+        .fails("Interval leading field precision '0' out of range for "
+            + "INTERVAL MINUTE\\(0\\)");
+  }
+
+  /**
+   * Runs tests for INTERVAL... MINUTE TO SECOND that should pass parser but
+   * fail validator. A substantially identical set of tests exists in
+   * SqlParserTest, and any changes here should be synchronized there.
+   * Similarly, any changes to tests here should be echoed appropriately to
+   * each of the other 12 subTestIntervalXXXNegative() tests.
+   */
+  public void subTestIntervalMinuteToSecondNegative() {
+    // Qualifier - field mismatches
+    f.wholeExpr("INTERVAL ':' MINUTE TO SECOND")
+        .fails("Illegal interval literal format ':' for INTERVAL MINUTE TO SECOND");
+    f.wholeExpr("INTERVAL ':.' MINUTE TO SECOND")
+        .fails("Illegal interval literal format ':\\.' for INTERVAL MINUTE TO SECOND");
+    f.wholeExpr("INTERVAL '1' MINUTE TO SECOND")
+        .fails("Illegal interval literal format '1' for INTERVAL MINUTE TO SECOND");
+    f.wholeExpr("INTERVAL '1 2' MINUTE TO SECOND")
+        .fails("Illegal interval literal format '1 2' for INTERVAL MINUTE TO SECOND");
+    f.wholeExpr("INTERVAL '1.2' MINUTE TO SECOND")
+        .fails("Illegal interval literal format '1\\.2' for INTERVAL MINUTE TO SECOND");
+    f.wholeExpr("INTERVAL '1 1:2' MINUTE TO SECOND")
+        .fails("Illegal interval literal format '1 1:2' for INTERVAL MINUTE TO SECOND");
+    f.wholeExpr("INTERVAL '1:x' MINUTE TO SECOND")
+        .fails("Illegal interval literal format '1:x' for INTERVAL MINUTE TO SECOND");
+    f.wholeExpr("INTERVAL 'x:3' MINUTE TO SECOND")
+        .fails("Illegal interval literal format 'x:3' for INTERVAL MINUTE TO SECOND");
+    f.wholeExpr("INTERVAL '1:1.x' MINUTE TO SECOND")
+        .fails("Illegal interval literal format '1:1\\.x' for INTERVAL MINUTE TO SECOND");
+    f.wholeExpr("INTERVAL '1 1:2' MINUTE(2) TO SECOND")
+        .fails("Illegal interval literal format '1 1:2' for INTERVAL MINUTE\\(2\\) TO SECOND");
+    f.wholeExpr("INTERVAL '1 1' MINUTE(2) TO SECOND")
+        .fails("Illegal interval literal format '1 1' for INTERVAL MINUTE\\(2\\) TO SECOND");
+    f.wholeExpr("INTERVAL 'bogus text' MINUTE TO SECOND")
+        .fails("Illegal interval literal format 'bogus text' for INTERVAL MINUTE TO SECOND");
+    f.wholeExpr("INTERVAL '7:8901' MINUTE TO SECOND(4)")
+        .fails("Illegal interval literal format '7:8901' for INTERVAL MINUTE TO SECOND\\(4\\)");
+
+    // negative field values
+    f.wholeExpr("INTERVAL '--1:1' MINUTE TO SECOND")
+        .fails("Illegal interval literal format '--1:1' for INTERVAL MINUTE TO SECOND");
+    f.wholeExpr("INTERVAL '1:-1' MINUTE TO SECOND")
+        .fails("Illegal interval literal format '1:-1' for INTERVAL MINUTE TO SECOND");
+    f.wholeExpr("INTERVAL '1:1.-1' MINUTE TO SECOND")
+        .fails("Illegal interval literal format '1:1.-1' for INTERVAL MINUTE TO SECOND");
+
+    // Field value out of range
+    //  (default, explicit default, alt, neg alt, max, neg max)
+    //  plus >max value for mid/end fields
+    f.wholeExpr("INTERVAL '100:0' MINUTE TO SECOND")
+        .fails("Interval field value 100 exceeds precision of MINUTE\\(2\\) field.*");
+    f.wholeExpr("INTERVAL '100:0' MINUTE(2) TO SECOND")
+        .fails("Interval field value 100 exceeds precision of MINUTE\\(2\\) field.*");
+    f.wholeExpr("INTERVAL '1000:0' MINUTE(3) TO SECOND")
+        .fails("Interval field value 1,000 exceeds precision of MINUTE\\(3\\) field.*");
+    f.wholeExpr("INTERVAL '-1000:0' MINUTE(3) TO SECOND")
+        .fails("Interval field value -1,000 exceeds precision of MINUTE\\(3\\) field.*");
+    f.wholeExpr("INTERVAL '2147483648:0' MINUTE(10) TO SECOND")
+        .fails("Interval field value 2,147,483,648 exceeds precision of MINUTE\\(10\\) field.*");
+    f.wholeExpr("INTERVAL '-2147483648:0' MINUTE(10) TO SECOND")
+        .fails("Interval field value -2,147,483,648 exceeds precision of MINUTE\\(10\\) field.*");
+    f.wholeExpr("INTERVAL '1:60' MINUTE TO SECOND")
+        .fails("Illegal interval literal format '1:60' for"
+            + " INTERVAL MINUTE TO SECOND.*");
+    f.wholeExpr("INTERVAL '1:1.0000001' MINUTE TO SECOND")
+        .fails("Illegal interval literal format '1:1\\.0000001' for"
+            + " INTERVAL MINUTE TO SECOND.*");
+    f.wholeExpr("INTERVAL '1:1:1.0001' MINUTE TO SECOND(3)")
+        .fails("Illegal interval literal format '1:1:1\\.0001' for"
+            + " INTERVAL MINUTE TO SECOND\\(3\\).*");
+
+    // precision > maximum
+    f.expr("INTERVAL '1:1' ^MINUTE(11) TO SECOND^")
+        .fails("Interval leading field precision '11' out of range for"
+            + " INTERVAL MINUTE\\(11\\) TO SECOND");
+    f.expr("INTERVAL '1:1' ^MINUTE TO SECOND(10)^")
+        .fails("Interval fractional second precision '10' out of range for"
+            + " INTERVAL MINUTE TO SECOND\\(10\\)");
+
+    // precision < minimum allowed)
+    // note: parser will catch negative values, here we
+    // just need to check for 0
+    f.expr("INTERVAL '0:0' ^MINUTE(0) TO SECOND^")
+        .fails("Interval leading field precision '0' out of range for"
+            + " INTERVAL MINUTE\\(0\\) TO SECOND");
+    f.expr("INTERVAL '0:0' ^MINUTE TO SECOND(0)^")
+        .fails("Interval fractional second precision '0' out of range for"
+            + " INTERVAL MINUTE TO SECOND\\(0\\)");
+  }
+
+  /**
+   * Runs tests for INTERVAL... SECOND that should pass parser but fail
+   * validator. A substantially identical set of tests exists in
+   * SqlParserTest, and any changes here should be synchronized there.
+   * Similarly, any changes to tests here should be echoed appropriately to
+   * each of the other 12 subTestIntervalXXXNegative() tests.
+   */
+  public void subTestIntervalSecondNegative() {
+    // Qualifier - field mismatches
+    f.wholeExpr("INTERVAL ':' SECOND")
+        .fails("Illegal interval literal format ':' for INTERVAL SECOND.*");
+    f.wholeExpr("INTERVAL '.' SECOND")
+        .fails("Illegal interval literal format '\\.' for INTERVAL SECOND.*");
+    f.wholeExpr("INTERVAL '1-2' SECOND")
+        .fails("Illegal interval literal format '1-2' for INTERVAL SECOND.*");
+    f.wholeExpr("INTERVAL '1.x' SECOND")
+        .fails("Illegal interval literal format '1\\.x' for INTERVAL SECOND.*");
+    f.wholeExpr("INTERVAL 'x.1' SECOND")
+        .fails("Illegal interval literal format 'x\\.1' for INTERVAL SECOND.*");
+    f.wholeExpr("INTERVAL '1 2' SECOND")
+        .fails("Illegal interval literal format '1 2' for INTERVAL SECOND.*");
+    f.wholeExpr("INTERVAL '1:2' SECOND")
+        .fails("Illegal interval literal format '1:2' for INTERVAL SECOND.*");
+    f.wholeExpr("INTERVAL '1-2' SECOND(2)")
+        .fails("Illegal interval literal format '1-2' for INTERVAL SECOND\\(2\\)");
+    f.wholeExpr("INTERVAL 'bogus text' SECOND")
+        .fails("Illegal interval literal format 'bogus text' for INTERVAL SECOND.*");
+
+    // negative field values
+    f.wholeExpr("INTERVAL '--1' SECOND")
+        .fails("Illegal interval literal format '--1' for INTERVAL SECOND.*");
+    f.wholeExpr("INTERVAL '1.-1' SECOND")
+        .fails("Illegal interval literal format '1.-1' for INTERVAL SECOND.*");
+
+    // Field value out of range
+    //  (default, explicit default, alt, neg alt, max, neg max)
+    f.wholeExpr("INTERVAL '100' SECOND")
+        .fails("Interval field value 100 exceeds precision of SECOND\\(2\\) field.*");
+    f.wholeExpr("INTERVAL '100' SECOND(2)")
+        .fails("Interval field value 100 exceeds precision of SECOND\\(2\\) field.*");
+    f.wholeExpr("INTERVAL '1000' SECOND(3)")
+        .fails("Interval field value 1,000 exceeds precision of SECOND\\(3\\) field.*");
+    f.wholeExpr("INTERVAL '-1000' SECOND(3)")
+        .fails("Interval field value -1,000 exceeds precision of SECOND\\(3\\) field.*");
+    f.wholeExpr("INTERVAL '2147483648' SECOND(10)")
+        .fails("Interval field value 2,147,483,648 exceeds precision of SECOND\\(10\\) field.*");
+    f.wholeExpr("INTERVAL '-2147483648' SECOND(10)")
+        .fails("Interval field value -2,147,483,648 exceeds precision of SECOND\\(10\\) field.*");
+    f.wholeExpr("INTERVAL '1.0000001' SECOND")
+        .fails("Illegal interval literal format '1\\.0000001' for INTERVAL SECOND.*");
+    f.wholeExpr("INTERVAL '1.0000001' SECOND(2)")
+        .fails("Illegal interval literal format '1\\.0000001' for INTERVAL SECOND\\(2\\).*");
+    f.wholeExpr("INTERVAL '1.0001' SECOND(2, 3)")
+        .fails("Illegal interval literal format '1\\.0001' for INTERVAL SECOND\\(2, 3\\).*");
+    f.wholeExpr("INTERVAL '1.0000000001' SECOND(2, 9)")
+        .fails("Illegal interval literal format '1\\.0000000001' for"
+            + " INTERVAL SECOND\\(2, 9\\).*");
+
+    // precision > maximum
+    f.expr("INTERVAL '1' ^SECOND(11)^")
+        .fails("Interval leading field precision '11' out of range for"
+            + " INTERVAL SECOND\\(11\\)");
+    f.expr("INTERVAL '1.1' ^SECOND(1, 10)^")
+        .fails("Interval fractional second precision '10' out of range for"
+            + " INTERVAL SECOND\\(1, 10\\)");
+
+    // precision < minimum allowed)
+    // note: parser will catch negative values, here we
+    // just need to check for 0
+    f.expr("INTERVAL '0' ^SECOND(0)^")
+        .fails("Interval leading field precision '0' out of range for"
+            + " INTERVAL SECOND\\(0\\)");
+    f.expr("INTERVAL '0' ^SECOND(1, 0)^")
+        .fails("Interval fractional second precision '0' out of range for"
+            + " INTERVAL SECOND\\(1, 0\\)");
+  }
+
+  public void subTestMisc() {
+    // Miscellaneous
+    // fractional value is not OK, even if it is 0
+    f.wholeExpr("INTERVAL '1.0' HOUR")
+        .fails("Illegal interval literal format '1.0' for INTERVAL HOUR");
+    // only seconds are allowed to have a fractional part
+    f.expr("INTERVAL '1.0' SECOND")
+        .columnType("INTERVAL SECOND NOT NULL");
+    // leading zeros do not cause precision to be exceeded
+    f.expr("INTERVAL '0999' MONTH(3)")
+        .columnType("INTERVAL MONTH(3) NOT NULL");
+  }
+
+  /** Fluent interface for binding an expression to create a fixture that can
+   * be used to validate, check AST, or check type. */
+  public interface Fixture {
+    Fixture2 expr(String s);
+    Fixture2 wholeExpr(String s);
+  }
+
+  /** Fluent interface to validate an expression. */
+  public interface Fixture2 {
+    /** Checks that the expression is valid in the parser
+     * but invalid (with the given error message) in the validator. */
+    void fails(String expected);
+
+    /** Checks that the expression is valid in the parser and validator
+     * and has the given column type. */
+    void columnType(String expectedType);
+
+    /** Checks that the expression parses successfully and produces the given
+     * SQL when unparsed. */
+    Fixture2 assertParse(String expectedAst);
+  }
+}


[calcite] 02/04: [CALCITE-5495] Allow WEEK and QUARTER in INTERVAL literals

Posted by jh...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit b8ccac153a5be6db72fcbcbda983a83d40b7932b
Author: Tanner Clary <ta...@google.com>
AuthorDate: Wed Jan 11 17:42:24 2023 +0000

    [CALCITE-5495] Allow WEEK and QUARTER in INTERVAL literals
---
 core/src/main/codegen/default_config.fmpp          |   2 +
 core/src/main/codegen/templates/Parser.jj          |  28 +++++
 .../apache/calcite/sql/SqlIntervalQualifier.java   |  76 ++++++++++++
 site/_docs/reference.md                            |   2 +
 .../apache/calcite/sql/parser/SqlParserTest.java   |   6 +-
 .../java/org/apache/calcite/test/IntervalTest.java | 135 +++++++++++++++++++++
 .../org/apache/calcite/test/SqlOperatorTest.java   |  15 +++
 7 files changed, 262 insertions(+), 2 deletions(-)

diff --git a/core/src/main/codegen/default_config.fmpp b/core/src/main/codegen/default_config.fmpp
index 31002559f4..c1847a2541 100644
--- a/core/src/main/codegen/default_config.fmpp
+++ b/core/src/main/codegen/default_config.fmpp
@@ -214,6 +214,7 @@ parser: {
     "PRIVILEGES"
     "PUBLIC"
     "QUARTER"
+    "QUARTERS"
     "READ"
     "RELATIVE"
     "REPEATABLE"
@@ -350,6 +351,7 @@ parser: {
     "VERSION"
     "VIEW"
     "WEEK"
+    "WEEKS"
     "WORK"
     "WRAPPER"
     "WRITE"
diff --git a/core/src/main/codegen/templates/Parser.jj b/core/src/main/codegen/templates/Parser.jj
index 25935f7d4e..b9db2b83ce 100644
--- a/core/src/main/codegen/templates/Parser.jj
+++ b/core/src/main/codegen/templates/Parser.jj
@@ -4853,6 +4853,15 @@ TimeUnit Year() :
     <YEARS> { return warn(TimeUnit.YEAR); }
 }
 
+TimeUnit Quarter() :
+{
+}
+{
+    <QUARTER> { return TimeUnit.QUARTER; }
+|
+    <QUARTERS> { return warn(TimeUnit.QUARTER); }
+}
+
 TimeUnit Month() :
 {
 }
@@ -4862,6 +4871,15 @@ TimeUnit Month() :
     <MONTHS> { return warn(TimeUnit.MONTH); }
 }
 
+TimeUnit Week() :
+{
+}
+{
+    <WEEK> { return TimeUnit.WEEK; }
+|
+    <WEEKS> { return warn(TimeUnit.WEEK); }
+}
+
 TimeUnit Day() :
 {
 }
@@ -4913,9 +4931,15 @@ SqlIntervalQualifier IntervalQualifier() :
             LOOKAHEAD(2) <TO> end = Month()
         |   { end = null; }
         )
+    |
+        start = Quarter() { s = span(); } startPrec = PrecisionOpt()
+        { end = null; }
     |
         start = Month() { s = span(); } startPrec = PrecisionOpt()
         { end = null; }
+    |
+        start = Week() { s = span(); } startPrec = PrecisionOpt()
+        { end = null; }
     |
         start = Day() { s = span(); } startPrec = PrecisionOpt()
         (
@@ -4977,7 +5001,9 @@ SqlIntervalQualifier IntervalQualifierStart() :
     (
         (
             start = Year()
+        |   start = Quarter()
         |   start = Month()
+        |   start = Week()
         |   start = Day()
         |   start = Hour()
         |   start = Minute()
@@ -7901,6 +7927,7 @@ SqlPostfixOperator PostfixRowOperator() :
 |   < PROCEDURE: "PROCEDURE" >
 |   < PUBLIC: "PUBLIC" >
 |   < QUARTER: "QUARTER" >
+|   < QUARTERS: "QUARTERS" >
 |   < RANGE: "RANGE" >
 |   < RANK: "RANK" >
 |   < READ: "READ" >
@@ -8143,6 +8170,7 @@ SqlPostfixOperator PostfixRowOperator() :
 |   < VERSIONING: "VERSIONING" >
 |   < VIEW: "VIEW" >
 |   < WEEK: "WEEK" >
+|   < WEEKS: "WEEKS" >
 |   < WHEN: "WHEN" >
 |   < WHENEVER: "WHENEVER" >
 |   < WHERE: "WHERE" >
diff --git a/core/src/main/java/org/apache/calcite/sql/SqlIntervalQualifier.java b/core/src/main/java/org/apache/calcite/sql/SqlIntervalQualifier.java
index 1bf7980b95..4c20fea592 100644
--- a/core/src/main/java/org/apache/calcite/sql/SqlIntervalQualifier.java
+++ b/core/src/main/java/org/apache/calcite/sql/SqlIntervalQualifier.java
@@ -594,6 +594,76 @@ public class SqlIntervalQualifier extends SqlNode {
     }
   }
 
+  /**
+   * Validates an INTERVAL literal against a QUARTER interval qualifier.
+   *
+   * @throws org.apache.calcite.runtime.CalciteContextException if the interval
+   * value is illegal
+   */
+  private int[] evaluateIntervalLiteralAsQuarter(
+      RelDataTypeSystem typeSystem, int sign,
+      String value,
+      String originalValue,
+      SqlParserPos pos) {
+    BigDecimal quarter;
+
+    // validate as QUARTER(startPrecision), e.g. 'MM'
+    String intervalPattern = "(\\d+)";
+
+    Matcher m = Pattern.compile(intervalPattern).matcher(value);
+    if (m.matches()) {
+      // Break out  field values
+      try {
+        quarter = parseField(m, 1);
+      } catch (NumberFormatException e) {
+        throw invalidValueException(pos, originalValue);
+      }
+
+      // Validate individual fields
+      checkLeadFieldInRange(typeSystem, sign, quarter, TimeUnit.QUARTER, pos);
+
+      // package values up for return
+      return fillIntervalValueArray(sign, ZERO, quarter);
+    } else {
+      throw invalidValueException(pos, originalValue);
+    }
+  }
+
+  /**
+   * Validates an INTERVAL literal against a WEEK interval qualifier.
+   *
+   * @throws org.apache.calcite.runtime.CalciteContextException if the interval
+   * value is illegal
+   */
+  private int[] evaluateIntervalLiteralAsWeek(
+      RelDataTypeSystem typeSystem, int sign,
+      String value,
+      String originalValue,
+      SqlParserPos pos) {
+    BigDecimal week;
+
+    // validate as WEEK(startPrecision), e.g. 'MM'
+    String intervalPattern = "(\\d+)";
+
+    Matcher m = Pattern.compile(intervalPattern).matcher(value);
+    if (m.matches()) {
+      // Break out  field values
+      try {
+        week = parseField(m, 1);
+      } catch (NumberFormatException e) {
+        throw invalidValueException(pos, originalValue);
+      }
+
+      // Validate individual fields
+      checkLeadFieldInRange(typeSystem, sign, week, TimeUnit.WEEK, pos);
+
+      // package values up for return
+      return fillIntervalValueArray(sign, ZERO, week);
+    } else {
+      throw invalidValueException(pos, originalValue);
+    }
+  }
+
   /**
    * Validates an INTERVAL literal against a DAY interval qualifier.
    *
@@ -1149,6 +1219,12 @@ public class SqlIntervalQualifier extends SqlNode {
     case MONTH:
       return evaluateIntervalLiteralAsMonth(typeSystem, sign, value, value0,
           pos);
+    case QUARTER:
+      return evaluateIntervalLiteralAsQuarter(typeSystem, sign, value, value0,
+          pos);
+    case WEEK:
+      return evaluateIntervalLiteralAsWeek(typeSystem, sign, value, value0,
+          pos);
     case DAY:
       return evaluateIntervalLiteralAsDay(typeSystem, sign, value, value0, pos);
     case DAY_TO_HOUR:
diff --git a/site/_docs/reference.md b/site/_docs/reference.md
index 5d8534a9fc..011890b412 100644
--- a/site/_docs/reference.md
+++ b/site/_docs/reference.md
@@ -843,6 +843,7 @@ PRIVILEGES,
 **PROCEDURE**,
 PUBLIC,
 QUARTER,
+QUARTERS,
 **RANGE**,
 **RANK**,
 READ,
@@ -1084,6 +1085,7 @@ VERSION,
 **VERSIONING**,
 VIEW,
 WEEK,
+WEEKS,
 **WHEN**,
 **WHENEVER**,
 **WHERE**,
diff --git a/testkit/src/main/java/org/apache/calcite/sql/parser/SqlParserTest.java b/testkit/src/main/java/org/apache/calcite/sql/parser/SqlParserTest.java
index d6e449055f..00dcddc4f4 100644
--- a/testkit/src/main/java/org/apache/calcite/sql/parser/SqlParserTest.java
+++ b/testkit/src/main/java/org/apache/calcite/sql/parser/SqlParserTest.java
@@ -5954,8 +5954,12 @@ public class SqlParserTest {
             + "    \"MINUTES\" \\.\\.\\.\n"
             + "    \"MONTH\" \\.\\.\\.\n"
             + "    \"MONTHS\" \\.\\.\\.\n"
+            + "    \"QUARTER\" \\.\\.\\.\n"
+            + "    \"QUARTERS\" \\.\\.\\.\n"
             + "    \"SECOND\" \\.\\.\\.\n"
             + "    \"SECONDS\" \\.\\.\\.\n"
+            + "    \"WEEK\" \\.\\.\\.\n"
+            + "    \"WEEKS\" \\.\\.\\.\n"
             + "    \"YEAR\" \\.\\.\\.\n"
             + "    \"YEARS\" \\.\\.\\.\n"
             + "    ");
@@ -6320,8 +6324,6 @@ public class SqlParserTest {
         .fails(ANY);
     expr("INTERVAL '10' ^DECADE^")
         .fails(ANY);
-    expr("INTERVAL '4' ^QUARTER^")
-        .fails(ANY);
   }
 
   /** Tests that plural time units are allowed when not in strict mode. */
diff --git a/testkit/src/main/java/org/apache/calcite/test/IntervalTest.java b/testkit/src/main/java/org/apache/calcite/test/IntervalTest.java
index 1f6d5ed30d..1c808db319 100644
--- a/testkit/src/main/java/org/apache/calcite/test/IntervalTest.java
+++ b/testkit/src/main/java/org/apache/calcite/test/IntervalTest.java
@@ -44,6 +44,9 @@ public class IntervalTest {
     subTestIntervalMinutePositive();
     subTestIntervalMinuteToSecondPositive();
     subTestIntervalSecondPositive();
+    subTestIntervalWeekPositive();
+    subTestIntervalQuarterPositive();
+    subTestIntervalPlural();
 
     // Tests that should pass parser but fail validator
     subTestIntervalYearNegative();
@@ -960,6 +963,138 @@ public class IntervalTest {
             + "INTERVAL YEAR\\(0\\) TO MONTH");
   }
 
+  /**
+   * Runs tests for INTERVAL... WEEK that should pass both parser and
+   * validator. A substantially identical set of tests exists in
+   * SqlValidatorTest, and any changes here should be synchronized there.
+   * Similarly, any changes to tests here should be echoed appropriately to
+   * each of the other 12 subTestIntervalXXXPositive() tests.
+   */
+  public void subTestIntervalWeekPositive() {
+    // default precision
+    f.expr("INTERVAL '1' WEEK")
+        .columnType("INTERVAL WEEK NOT NULL");
+    f.expr("INTERVAL '99' WEEK")
+        .columnType("INTERVAL WEEK NOT NULL");
+
+    // explicit precision equal to default
+    f.expr("INTERVAL '1' WEEK(2)")
+        .columnType("INTERVAL WEEK(2) NOT NULL");
+    f.expr("INTERVAL '99' WEEK(2)")
+        .columnType("INTERVAL WEEK(2) NOT NULL");
+
+    // max precision
+    f.expr("INTERVAL '2147483647' WEEK(10)")
+        .columnType("INTERVAL WEEK(10) NOT NULL");
+
+    // min precision
+    f.expr("INTERVAL '0' WEEK(1)")
+        .columnType("INTERVAL WEEK(1) NOT NULL");
+
+    // alternate precision
+    f.expr("INTERVAL '1234' WEEK(4)")
+        .columnType("INTERVAL WEEK(4) NOT NULL");
+
+    // sign
+    f.expr("INTERVAL '+1' WEEK")
+        .columnType("INTERVAL WEEK NOT NULL");
+    f.expr("INTERVAL '-1' WEEK")
+        .columnType("INTERVAL WEEK NOT NULL");
+    f.expr("INTERVAL +'1' WEEK")
+        .assertParse("INTERVAL '1' WEEK")
+        .columnType("INTERVAL WEEK NOT NULL");
+    f.expr("INTERVAL +'+1' WEEK")
+        .assertParse("INTERVAL '+1' WEEK")
+        .columnType("INTERVAL WEEK NOT NULL");
+    f.expr("INTERVAL +'-1' WEEK")
+        .assertParse("INTERVAL '-1' WEEK")
+        .columnType("INTERVAL WEEK NOT NULL");
+    f.expr("INTERVAL -'1' WEEK")
+        .columnType("INTERVAL WEEK NOT NULL");
+    f.expr("INTERVAL -'+1' WEEK")
+        .columnType("INTERVAL WEEK NOT NULL");
+    f.expr("INTERVAL -'-1' WEEK")
+        .columnType("INTERVAL WEEK NOT NULL");
+  }
+
+  /**
+   * Runs tests for INTERVAL... QUARTER that should pass both parser and
+   * validator. A substantially identical set of tests exists in
+   * SqlValidatorTest, and any changes here should be synchronized there.
+   * Similarly, any changes to tests here should be echoed appropriately to
+   * each of the other 12 subTestIntervalXXXPositive() tests.
+   */
+  public void subTestIntervalQuarterPositive() {
+    // default precision
+    f.expr("INTERVAL '1' QUARTER")
+        .columnType("INTERVAL QUARTER NOT NULL");
+    f.expr("INTERVAL '99' QUARTER")
+        .columnType("INTERVAL QUARTER NOT NULL");
+
+    // explicit precision equal to default
+    f.expr("INTERVAL '1' QUARTER(2)")
+        .columnType("INTERVAL QUARTER(2) NOT NULL");
+    f.expr("INTERVAL '99' QUARTER(2)")
+        .columnType("INTERVAL QUARTER(2) NOT NULL");
+
+    // max precision
+    f.expr("INTERVAL '2147483647' QUARTER(10)")
+        .columnType("INTERVAL QUARTER(10) NOT NULL");
+
+    // min precision
+    f.expr("INTERVAL '0' QUARTER(1)")
+        .columnType("INTERVAL QUARTER(1) NOT NULL");
+
+    // alternate precision
+    f.expr("INTERVAL '1234' QUARTER(4)")
+        .columnType("INTERVAL QUARTER(4) NOT NULL");
+
+    // sign
+    f.expr("INTERVAL '+1' QUARTER")
+        .columnType("INTERVAL QUARTER NOT NULL");
+    f.expr("INTERVAL '-1' QUARTER")
+        .columnType("INTERVAL QUARTER NOT NULL");
+    f.expr("INTERVAL +'1' QUARTER")
+        .assertParse("INTERVAL '1' QUARTER")
+        .columnType("INTERVAL QUARTER NOT NULL");
+    f.expr("INTERVAL +'+1' QUARTER")
+        .assertParse("INTERVAL '+1' QUARTER")
+        .columnType("INTERVAL QUARTER NOT NULL");
+    f.expr("INTERVAL +'-1' QUARTER")
+        .assertParse("INTERVAL '-1' QUARTER")
+        .columnType("INTERVAL QUARTER NOT NULL");
+    f.expr("INTERVAL -'1' QUARTER")
+        .columnType("INTERVAL QUARTER NOT NULL");
+    f.expr("INTERVAL -'+1' QUARTER")
+        .columnType("INTERVAL QUARTER NOT NULL");
+    f.expr("INTERVAL -'-1' QUARTER")
+        .columnType("INTERVAL QUARTER NOT NULL");
+  }
+
+  public void subTestIntervalPlural() {
+    f.expr("INTERVAL '+2' SECONDS")
+        .assertParse("INTERVAL '+2' SECOND")
+        .columnType("INTERVAL SECOND NOT NULL");
+    f.expr("INTERVAL '+2' HOURS")
+        .assertParse("INTERVAL '+2' HOUR")
+        .columnType("INTERVAL HOUR NOT NULL");
+    f.expr("INTERVAL '+2' DAYS")
+        .assertParse("INTERVAL '+2' DAY")
+        .columnType("INTERVAL DAY NOT NULL");
+    f.expr("INTERVAL '+2' WEEKS")
+        .assertParse("INTERVAL '+2' WEEK")
+        .columnType("INTERVAL WEEK NOT NULL");
+    f.expr("INTERVAL '+2' QUARTERS")
+        .assertParse("INTERVAL '+2' QUARTER")
+        .columnType("INTERVAL QUARTER NOT NULL");
+    f.expr("INTERVAL '+2' MONTHS")
+        .assertParse("INTERVAL '+2' MONTH")
+        .columnType("INTERVAL MONTH NOT NULL");
+    f.expr("INTERVAL '+2' YEARS")
+        .assertParse("INTERVAL '+2' YEAR")
+        .columnType("INTERVAL YEAR NOT NULL");
+  }
+
   /**
    * Runs tests for INTERVAL... MONTH that should pass parser but fail
    * validator. A substantially identical set of tests exists in
diff --git a/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java b/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
index a93771e3a0..1e5734be39 100644
--- a/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
+++ b/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
@@ -8286,9 +8286,15 @@ public class SqlOperatorTest {
     f.checkScalar("timestamp_sub(timestamp '2016-02-24 12:42:25', interval 1 day)",
         "2016-02-23 12:42:25",
         "TIMESTAMP(0) NOT NULL");
+    f.checkScalar("timestamp_sub(timestamp '2016-02-24 12:42:25', interval 2 week)",
+        "2016-02-10 12:42:25",
+        "TIMESTAMP(0) NOT NULL");
     f.checkScalar("timestamp_sub(timestamp '2016-02-24 12:42:25', interval 1 month)",
         "2016-01-24 12:42:25",
         "TIMESTAMP(0) NOT NULL");
+    f.checkScalar("timestamp_sub(timestamp '2016-02-24 12:42:25', interval 1 quarter)",
+        "2015-11-24 12:42:25",
+        "TIMESTAMP(0) NOT NULL");
     f.checkScalar("timestamp_sub(timestamp '2016-02-24 12:42:25', interval 1 year)",
         "2015-02-24 12:42:25",
         "TIMESTAMP(0) NOT NULL");
@@ -8345,9 +8351,18 @@ public class SqlOperatorTest {
     f.checkScalar("date_sub(date '2016-02-24', interval 2 day)",
         "2016-02-22",
         "DATE NOT NULL");
+    f.checkScalar("date_sub(date '2016-02-24', interval 1 week)",
+        "2016-02-17",
+        "DATE NOT NULL");
+    f.checkScalar("date_sub(date '2020-10-17', interval 0 week)",
+        "2020-10-17",
+        "DATE NOT NULL");
     f.checkScalar("date_sub(date '2016-02-24', interval 3 month)",
         "2015-11-24",
         "DATE NOT NULL");
+    f.checkScalar("date_sub(date '2016-02-24', interval 1 quarter)",
+        "2015-11-24",
+        "DATE NOT NULL");
     f.checkScalar("date_sub(date '2016-02-24', interval 5 year)",
         "2011-02-24",
         "DATE NOT NULL");


[calcite] 04/04: [CALCITE-5424] Customize handling of literals based on type system

Posted by jh...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit a326bd2d0e0b4b6b3336f10217b0ecbb79522239
Author: Julian Hyde <jh...@apache.org>
AuthorDate: Fri Dec 9 01:54:54 2022 -0800

    [CALCITE-5424] Customize handling of literals based on type system
    
    Literals introduced by the keyword DATE, TIME, DATETIME,
    TIMESTAMP, TIMESTAMP WITH LOCAL TIME ZONE are represented by
    the parser by new class SqlUnknownLiteral. Determining the
    actual type is deferred until validation time; the validator
    determines the actual type based on the type system's type
    alias map, and then validates the character string.
    
    Close apache/calcite#3036
    
    Co-authored-by: Julian Hyde <jh...@apache.org>
    Co-authored-by: Oliver Lee <ol...@google.com>
---
 .../org/apache/calcite/test/BabelParserTest.java   | 11 ++--
 core/src/main/codegen/templates/Parser.jj          | 16 +++++-
 .../apache/calcite/rel/rel2sql/SqlImplementor.java |  2 +-
 .../calcite/rex/RexToSqlNodeConverterImpl.java     |  2 +-
 .../java/org/apache/calcite/sql/SqlLiteral.java    | 41 +++++++++++++-
 .../apache/calcite/sql/SqlTimestampLiteral.java    | 10 ++--
 .../org/apache/calcite/sql/SqlUnknownLiteral.java  | 64 ++++++++++++++++++++++
 .../apache/calcite/sql/parser/SqlParserUtil.java   | 16 +++++-
 .../org/apache/calcite/sql/type/SqlTypeName.java   | 18 +++++-
 .../apache/calcite/sql/validate/SqlValidator.java  |  7 +++
 .../calcite/sql/validate/SqlValidatorImpl.java     | 27 ++++++++-
 .../calcite/sql2rel/SqlNodeToRexConverterImpl.java |  4 ++
 .../apache/calcite/sql/test/SqlAdvisorTest.java    |  1 +
 .../org/apache/calcite/test/SqlValidatorTest.java  | 31 +++++++++++
 core/src/test/resources/sql/misc.iq                | 14 +++++
 .../org/apache/calcite/test/ServerParserTest.java  |  2 +-
 site/_docs/reference.md                            |  1 +
 .../apache/calcite/sql/parser/SqlParserTest.java   | 36 +++++++-----
 .../org/apache/calcite/test/SqlOperatorTest.java   |  4 +-
 19 files changed, 268 insertions(+), 39 deletions(-)

diff --git a/babel/src/test/java/org/apache/calcite/test/BabelParserTest.java b/babel/src/test/java/org/apache/calcite/test/BabelParserTest.java
index 04e4bb13cf..9cccc5e448 100644
--- a/babel/src/test/java/org/apache/calcite/test/BabelParserTest.java
+++ b/babel/src/test/java/org/apache/calcite/test/BabelParserTest.java
@@ -213,15 +213,16 @@ class BabelParserTest extends SqlParserTest {
   /** PostgreSQL and Redshift allow TIMESTAMP literals that contain only a
    * date part. */
   @Test void testShortTimestampLiteral() {
+    // Parser doesn't actually check the contents of the string. The validator
+    // will convert it to '1969-07-20 00:00:00', when it has decided that
+    // TIMESTAMP maps to the TIMESTAMP type.
     sql("select timestamp '1969-07-20'")
-        .ok("SELECT TIMESTAMP '1969-07-20 00:00:00'");
+        .ok("SELECT TIMESTAMP '1969-07-20'");
     // PostgreSQL allows the following. We should too.
     sql("select ^timestamp '1969-07-20 1:2'^")
-        .fails("Illegal TIMESTAMP literal '1969-07-20 1:2': not in format "
-            + "'yyyy-MM-dd HH:mm:ss'"); // PostgreSQL gives 1969-07-20 01:02:00
+        .ok("SELECT TIMESTAMP '1969-07-20 1:2'");
     sql("select ^timestamp '1969-07-20:23:'^")
-        .fails("Illegal TIMESTAMP literal '1969-07-20:23:': not in format "
-            + "'yyyy-MM-dd HH:mm:ss'"); // PostgreSQL gives 1969-07-20 23:00:00
+        .ok("SELECT TIMESTAMP '1969-07-20:23:'");
   }
 
   /** Tests parsing PostgreSQL-style "::" cast operator. */
diff --git a/core/src/main/codegen/templates/Parser.jj b/core/src/main/codegen/templates/Parser.jj
index 0514b815e2..4db4d15083 100644
--- a/core/src/main/codegen/templates/Parser.jj
+++ b/core/src/main/codegen/templates/Parser.jj
@@ -4609,15 +4609,24 @@ SqlLiteral DateTimeLiteral() :
     }
 |
     <DATE> { s = span(); } p = SimpleStringLiteral() {
-      return SqlParserUtil.parseDateLiteral(p, s.end(this));
+      return SqlLiteral.createUnknown("DATE", p, s.end(this));
+    }
+|
+    <DATETIME> { s = span(); } p = SimpleStringLiteral() {
+        return SqlLiteral.createUnknown("DATETIME", p, s.end(this));
     }
 |
     <TIME> { s = span(); } p = SimpleStringLiteral() {
-        return SqlParserUtil.parseTimeLiteral(p, s.end(this));
+      return SqlLiteral.createUnknown("TIME", p, s.end(this));
     }
 |
+    LOOKAHEAD(2)
     <TIMESTAMP> { s = span(); } p = SimpleStringLiteral() {
-        return SqlParserUtil.parseTimestampLiteral(p, s.end(this));
+        return SqlLiteral.createUnknown("TIMESTAMP", p, s.end(this));
+    }
+|
+    <TIMESTAMP> { s = span(); } <WITH> <LOCAL> <TIME> <ZONE> p = SimpleStringLiteral() {
+        return SqlLiteral.createUnknown("TIMESTAMP WITH LOCAL TIME ZONE", p, s.end(this));
     }
 }
 
@@ -7644,6 +7653,7 @@ SqlPostfixOperator PostfixRowOperator() :
 |   < DATABASE: "DATABASE" >
 |   < DATE: "DATE" >
 |   < DATE_TRUNC: "DATE_TRUNC" >
+|   < DATETIME: "DATETIME" >
 |   < DATETIME_INTERVAL_CODE: "DATETIME_INTERVAL_CODE" >
 |   < DATETIME_INTERVAL_PRECISION: "DATETIME_INTERVAL_PRECISION" >
 |   < DAY: "DAY" >
diff --git a/core/src/main/java/org/apache/calcite/rel/rel2sql/SqlImplementor.java b/core/src/main/java/org/apache/calcite/rel/rel2sql/SqlImplementor.java
index 39a93bc9f8..36d28435a5 100644
--- a/core/src/main/java/org/apache/calcite/rel/rel2sql/SqlImplementor.java
+++ b/core/src/main/java/org/apache/calcite/rel/rel2sql/SqlImplementor.java
@@ -1409,7 +1409,7 @@ public abstract class SqlImplementor {
       return SqlLiteral.createTime(castNonNull(literal.getValueAs(TimeString.class)),
           literal.getType().getPrecision(), POS);
     case TIMESTAMP:
-      return SqlLiteral.createTimestamp(
+      return SqlLiteral.createTimestamp(typeName,
           castNonNull(literal.getValueAs(TimestampString.class)),
           literal.getType().getPrecision(), POS);
     case ANY:
diff --git a/core/src/main/java/org/apache/calcite/rex/RexToSqlNodeConverterImpl.java b/core/src/main/java/org/apache/calcite/rex/RexToSqlNodeConverterImpl.java
index 426ff6d8f9..d18d7745ae 100644
--- a/core/src/main/java/org/apache/calcite/rex/RexToSqlNodeConverterImpl.java
+++ b/core/src/main/java/org/apache/calcite/rex/RexToSqlNodeConverterImpl.java
@@ -85,7 +85,7 @@ public class RexToSqlNodeConverterImpl implements RexToSqlNodeConverter {
     // Timestamp
     if (SqlTypeFamily.TIMESTAMP.getTypeNames().contains(
         literal.getTypeName())) {
-      return SqlLiteral.createTimestamp(
+      return SqlLiteral.createTimestamp(literal.getTypeName(),
           requireNonNull(literal.getValueAs(TimestampString.class),
               "literal.getValueAs(TimestampString.class)"),
           0,
diff --git a/core/src/main/java/org/apache/calcite/sql/SqlLiteral.java b/core/src/main/java/org/apache/calcite/sql/SqlLiteral.java
index ffcce09d17..e60c000196 100644
--- a/core/src/main/java/org/apache/calcite/sql/SqlLiteral.java
+++ b/core/src/main/java/org/apache/calcite/sql/SqlLiteral.java
@@ -205,6 +205,7 @@ public class SqlLiteral extends SqlNode {
     case TIME:
       return value instanceof TimeString;
     case TIMESTAMP:
+    case TIMESTAMP_WITH_LOCAL_TIME_ZONE:
       return value instanceof TimestampString;
     case INTERVAL_YEAR:
     case INTERVAL_YEAR_MONTH:
@@ -229,6 +230,8 @@ public class SqlLiteral extends SqlNode {
           || (value instanceof SqlSampleSpec);
     case MULTISET:
       return true;
+    case UNKNOWN:
+      return value instanceof String;
     case INTEGER: // not allowed -- use Decimal
     case VARCHAR: // not allowed -- use Char
     case VARBINARY: // not allowed -- use Binary
@@ -826,6 +829,28 @@ public class SqlLiteral extends SqlNode {
     }
   }
 
+  /** Creates a literal whose type is unknown until validation time.
+   * The literal has a tag that looks like a type name, but the tag cannot be
+   * resolved until validation time, when we have the mapping from type aliases
+   * to types.
+   *
+   * <p>For example,
+   * <blockquote>{@code
+   * TIMESTAMP '1969-07-20 22:56:00'
+   * }</blockquote>
+   * calls {@code createUnknown("TIMESTAMP", "1969-07-20 22:56:00")}; at
+   * validate time, we may discover that "TIMESTAMP" maps to the type
+   * "TIMESTAMP WITH LOCAL TIME ZONE".
+   *
+   * @param tag Type name, e.g. "TIMESTAMP", "TIMESTAMP WITH LOCAL TIME ZONE"
+   * @param value String encoding of the value
+   * @param pos Parser position
+   */
+  public static SqlLiteral createUnknown(String tag, String value,
+      SqlParserPos pos) {
+    return new SqlUnknownLiteral(tag, value, pos);
+  }
+
   @Deprecated // to be removed before 2.0
   public static SqlDateLiteral createDate(
       Calendar calendar,
@@ -844,15 +869,25 @@ public class SqlLiteral extends SqlNode {
       Calendar calendar,
       int precision,
       SqlParserPos pos) {
-    return createTimestamp(TimestampString.fromCalendarFields(calendar),
-        precision, pos);
+    return createTimestamp(SqlTypeName.TIMESTAMP,
+        TimestampString.fromCalendarFields(calendar), precision, pos);
   }
 
+  @Deprecated // to be removed before 2.0
   public static SqlTimestampLiteral createTimestamp(
       TimestampString ts,
       int precision,
       SqlParserPos pos) {
-    return new SqlTimestampLiteral(ts, precision, false, pos);
+    return createTimestamp(SqlTypeName.TIMESTAMP, ts, precision, pos);
+  }
+
+  /** Creates a TIMESTAMP or TIMESTAMP WITH TIME ZONE literal. */
+  public static SqlTimestampLiteral createTimestamp(
+      SqlTypeName typeName,
+      TimestampString ts,
+      int precision,
+      SqlParserPos pos) {
+    return new SqlTimestampLiteral(ts, precision, typeName, pos);
   }
 
   @Deprecated // to be removed before 2.0
diff --git a/core/src/main/java/org/apache/calcite/sql/SqlTimestampLiteral.java b/core/src/main/java/org/apache/calcite/sql/SqlTimestampLiteral.java
index 62869df839..ac169cad30 100644
--- a/core/src/main/java/org/apache/calcite/sql/SqlTimestampLiteral.java
+++ b/core/src/main/java/org/apache/calcite/sql/SqlTimestampLiteral.java
@@ -34,9 +34,11 @@ public class SqlTimestampLiteral extends SqlAbstractDateTimeLiteral {
   //~ Constructors -----------------------------------------------------------
 
   SqlTimestampLiteral(TimestampString ts, int precision,
-      boolean hasTimeZone, SqlParserPos pos) {
-    super(ts, hasTimeZone, SqlTypeName.TIMESTAMP, precision, pos);
+      SqlTypeName typeName, SqlParserPos pos) {
+    super(ts, false, typeName, precision, pos);
     Preconditions.checkArgument(this.precision >= 0);
+    Preconditions.checkArgument(typeName == SqlTypeName.TIMESTAMP
+        || typeName == SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE);
   }
 
   //~ Methods ----------------------------------------------------------------
@@ -45,11 +47,11 @@ public class SqlTimestampLiteral extends SqlAbstractDateTimeLiteral {
     return new SqlTimestampLiteral(
         (TimestampString) Objects.requireNonNull(value, "value"),
         precision,
-        hasTimeZone, pos);
+        getTypeName(), pos);
   }
 
   @Override public String toString() {
-    return "TIMESTAMP '" + toFormattedString() + "'";
+    return getTypeName() + " '" + toFormattedString() + "'";
   }
 
   /**
diff --git a/core/src/main/java/org/apache/calcite/sql/SqlUnknownLiteral.java b/core/src/main/java/org/apache/calcite/sql/SqlUnknownLiteral.java
new file mode 100644
index 0000000000..56fc375d1d
--- /dev/null
+++ b/core/src/main/java/org/apache/calcite/sql/SqlUnknownLiteral.java
@@ -0,0 +1,64 @@
+/*
+ * 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.sql.parser.SqlParserPos;
+import org.apache.calcite.sql.parser.SqlParserUtil;
+import org.apache.calcite.sql.type.SqlTypeName;
+import org.apache.calcite.util.NlsString;
+import org.apache.calcite.util.Util;
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * Literal whose type is not yet known.
+ */
+public class SqlUnknownLiteral extends SqlLiteral {
+  public final String tag;
+
+  SqlUnknownLiteral(String tag, String value, SqlParserPos pos) {
+    super(requireNonNull(value, "value"), SqlTypeName.UNKNOWN, pos);
+    this.tag = requireNonNull(tag, "tag");
+  }
+
+  @Override public String getValue() {
+    return (String) requireNonNull(super.getValue(), "value");
+  }
+
+  @Override public void unparse(SqlWriter writer, int leftPrec, int rightPrec) {
+    final NlsString nlsString = new NlsString(getValue(), null, null);
+    writer.keyword(tag);
+    writer.literal(nlsString.asSql(true, true, writer.getDialect()));
+  }
+
+
+  /** Converts this unknown literal to a literal of known type. */
+  public SqlLiteral resolve(SqlTypeName typeName) {
+    switch (typeName) {
+    case DATE:
+      return SqlParserUtil.parseDateLiteral(getValue(), pos);
+    case TIME:
+      return SqlParserUtil.parseTimeLiteral(getValue(), pos);
+    case TIMESTAMP:
+      return SqlParserUtil.parseTimestampLiteral(getValue(), pos);
+    case TIMESTAMP_WITH_LOCAL_TIME_ZONE:
+      return SqlParserUtil.parseTimestampWithLocalTimeZoneLiteral(getValue(), pos);
+    default:
+      throw Util.unexpected(typeName);
+    }
+  }
+}
diff --git a/core/src/main/java/org/apache/calcite/sql/parser/SqlParserUtil.java b/core/src/main/java/org/apache/calcite/sql/parser/SqlParserUtil.java
index c3f69220bb..270f897808 100644
--- a/core/src/main/java/org/apache/calcite/sql/parser/SqlParserUtil.java
+++ b/core/src/main/java/org/apache/calcite/sql/parser/SqlParserUtil.java
@@ -40,6 +40,7 @@ import org.apache.calcite.sql.SqlTimestampLiteral;
 import org.apache.calcite.sql.SqlUtil;
 import org.apache.calcite.sql.fun.SqlStdOperatorTable;
 import org.apache.calcite.sql.parser.impl.SqlParserImpl;
+import org.apache.calcite.sql.type.SqlTypeName;
 import org.apache.calcite.util.DateString;
 import org.apache.calcite.util.PrecedenceClimbingParser;
 import org.apache.calcite.util.TimeString;
@@ -338,6 +339,17 @@ public final class SqlParserUtil {
 
   public static SqlTimestampLiteral parseTimestampLiteral(String s,
       SqlParserPos pos) {
+    return parseTimestampLiteral(SqlTypeName.TIMESTAMP, s, pos);
+  }
+
+  public static SqlTimestampLiteral parseTimestampWithLocalTimeZoneLiteral(
+      String s, SqlParserPos pos) {
+    return parseTimestampLiteral(SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE, s,
+        pos);
+  }
+
+  private static SqlTimestampLiteral parseTimestampLiteral(SqlTypeName typeName,
+      String s, SqlParserPos pos) {
     final Format format = Format.get();
     DateTimeUtils.PrecisionTime pt = null;
     // Allow timestamp literals with and without time fields (as does
@@ -352,13 +364,13 @@ public final class SqlParserUtil {
     }
     if (pt == null) {
       throw SqlUtil.newContextException(pos,
-          RESOURCE.illegalLiteral("TIMESTAMP", s,
+          RESOURCE.illegalLiteral(typeName.getName().replace('_', ' '), s,
               RESOURCE.badFormat(DateTimeUtils.TIMESTAMP_FORMAT_STRING).str()));
     }
     final TimestampString ts =
         TimestampString.fromCalendarFields(pt.getCalendar())
             .withFraction(pt.getFraction());
-    return SqlLiteral.createTimestamp(ts, pt.getPrecision(), pos);
+    return SqlLiteral.createTimestamp(typeName, ts, pt.getPrecision(), pos);
   }
 
   public static SqlIntervalLiteral parseIntervalLiteral(SqlParserPos pos,
diff --git a/core/src/main/java/org/apache/calcite/sql/type/SqlTypeName.java b/core/src/main/java/org/apache/calcite/sql/type/SqlTypeName.java
index 4849fa1e9f..526cad95de 100644
--- a/core/src/main/java/org/apache/calcite/sql/type/SqlTypeName.java
+++ b/core/src/main/java/org/apache/calcite/sql/type/SqlTypeName.java
@@ -298,6 +298,14 @@ public enum SqlTypeName {
     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;
   }
@@ -945,7 +953,7 @@ public enum SqlTypeName {
           ? TimeString.fromCalendarFields((Calendar) o)
           : (TimeString) o, 0 /* todo */, pos);
     case TIMESTAMP:
-      return SqlLiteral.createTimestamp(o instanceof Calendar
+      return SqlLiteral.createTimestamp(this, o instanceof Calendar
           ? TimestampString.fromCalendarFields((Calendar) o)
           : (TimestampString) o, 0 /* todo */, pos);
     default:
@@ -955,7 +963,13 @@ public enum SqlTypeName {
 
   /** Returns the name of this type. */
   public String getName() {
-    return toString();
+    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('_', ' ');
   }
 
   /**
diff --git a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidator.java b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidator.java
index 50e2151f1e..4c05824ce7 100644
--- a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidator.java
+++ b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidator.java
@@ -705,6 +705,13 @@ public interface SqlValidator {
    */
   SqlNode expand(SqlNode expr, SqlValidatorScope scope);
 
+  /** Resolves a literal.
+   *
+   * <p>Usually returns the literal unchanged, but if the literal is of type
+   * {@link org.apache.calcite.sql.type.SqlTypeName#UNKNOWN} looks up its type
+   * and converts to the appropriate literal subclass. */
+  SqlLiteral resolveLiteral(SqlLiteral literal);
+
   /**
    * Returns whether a field is a system field. Such fields may have
    * particular properties such as sortedness and nullability.
diff --git a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java
index bcc1dc9776..99e52c5e4e 100644
--- a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java
+++ b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java
@@ -76,6 +76,7 @@ import org.apache.calcite.sql.SqlSelectKeyword;
 import org.apache.calcite.sql.SqlSnapshot;
 import org.apache.calcite.sql.SqlSyntax;
 import org.apache.calcite.sql.SqlTableFunction;
+import org.apache.calcite.sql.SqlUnknownLiteral;
 import org.apache.calcite.sql.SqlUnpivot;
 import org.apache.calcite.sql.SqlUnresolvedFunction;
 import org.apache.calcite.sql.SqlUpdate;
@@ -6145,6 +6146,26 @@ public class SqlValidatorImpl implements SqlValidatorWithHints {
     assert feature.getProperties().get("FeatureDefinition") != null;
   }
 
+  @Override public SqlLiteral resolveLiteral(SqlLiteral literal) {
+    switch (literal.getTypeName()) {
+    case UNKNOWN:
+      final SqlUnknownLiteral unknownLiteral = (SqlUnknownLiteral) literal;
+      final SqlIdentifier identifier =
+          new SqlIdentifier(unknownLiteral.tag, SqlParserPos.ZERO);
+      final @Nullable RelDataType type = catalogReader.getNamedType(identifier);
+      final SqlTypeName typeName;
+      if (type != null) {
+        typeName = type.getSqlTypeName();
+      } else {
+        typeName = SqlTypeName.lookup(unknownLiteral.tag);
+      }
+      return unknownLiteral.resolve(typeName);
+
+    default:
+      return literal;
+    }
+  }
+
   public SqlNode expandSelectExpr(SqlNode expr,
       SelectScope scope, SqlSelect select) {
     final Expander expander = new SelectExpander(this, scope, select);
@@ -6443,7 +6464,7 @@ public class SqlValidatorImpl implements SqlValidatorWithHints {
     }
 
     @Override public RelDataType visit(SqlLiteral literal) {
-      return literal.createSqlType(typeFactory);
+      return resolveLiteral(literal).createSqlType(typeFactory);
     }
 
     @Override public RelDataType visit(SqlCall call) {
@@ -6592,6 +6613,10 @@ public class SqlValidatorImpl implements SqlValidatorWithHints {
       return expandedExpr;
     }
 
+    @Override public @Nullable SqlNode visit(SqlLiteral literal) {
+      return validator.resolveLiteral(literal);
+    }
+
     @Override protected SqlNode visitScoped(SqlCall call) {
       switch (call.getKind()) {
       case SCALAR_QUERY:
diff --git a/core/src/main/java/org/apache/calcite/sql2rel/SqlNodeToRexConverterImpl.java b/core/src/main/java/org/apache/calcite/sql2rel/SqlNodeToRexConverterImpl.java
index 987f777a6b..9a8bc7e04b 100644
--- a/core/src/main/java/org/apache/calcite/sql2rel/SqlNodeToRexConverterImpl.java
+++ b/core/src/main/java/org/apache/calcite/sql2rel/SqlNodeToRexConverterImpl.java
@@ -137,6 +137,10 @@ public class SqlNodeToRexConverterImpl implements SqlNodeToRexConverter {
       return rexBuilder.makeIntervalLiteral(
           literal.getValueAs(BigDecimal.class),
           sqlIntervalQualifier);
+
+    case UNKNOWN:
+      return convertLiteral(cx, cx.getValidator().resolveLiteral(literal));
+
     default:
       throw Util.unexpected(literal.getTypeName());
     }
diff --git a/core/src/test/java/org/apache/calcite/sql/test/SqlAdvisorTest.java b/core/src/test/java/org/apache/calcite/sql/test/SqlAdvisorTest.java
index 9f47e6b28c..7cc1f882e0 100644
--- a/core/src/test/java/org/apache/calcite/sql/test/SqlAdvisorTest.java
+++ b/core/src/test/java/org/apache/calcite/sql/test/SqlAdvisorTest.java
@@ -162,6 +162,7 @@ class SqlAdvisorTest extends SqlValidatorTestCase {
           "KEYWORD(CURRENT_USER)",
           "KEYWORD(CURSOR)",
           "KEYWORD(DATE)",
+          "KEYWORD(DATETIME)",
           "KEYWORD(DENSE_RANK)",
           "KEYWORD(ELEMENT)",
           "KEYWORD(EVERY)",
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 7fcf6cf5a1..26d5b3cfe1 100644
--- a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
+++ b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
@@ -251,6 +251,37 @@ public class SqlValidatorTest extends SqlValidatorTestCase {
         .columnType("BOOLEAN");
   }
 
+  /** Tests that date-time literals with invalid strings are considered invalid.
+   * Originally the parser did that checking, but now the parser creates a
+   * {@link org.apache.calcite.sql.SqlUnknownLiteral} and the checking is
+   * deferred to the validator. */
+  @Test void testLiteral() {
+    expr("^DATE '12/21/99'^")
+        .fails("(?s).*Illegal DATE literal.*");
+    expr("^TIME '1230:33'^")
+        .fails("(?s).*Illegal TIME literal.*");
+    expr("^TIME '12:00:00 PM'^")
+        .fails("(?s).*Illegal TIME literal.*");
+    expr("^TIMESTAMP '12-21-99, 12:30:00'^")
+        .fails("(?s).*Illegal TIMESTAMP literal.*");
+    expr("^TIMESTAMP WITH LOCAL TIME ZONE '12-21-99, 12:30:00'^")
+        .fails("(?s).*Illegal TIMESTAMP WITH LOCAL TIME ZONE literal.*");
+  }
+
+  /** PostgreSQL and Redshift allow TIMESTAMP literals that contain only a
+   * date part. */
+  @Test void testShortTimestampLiteral() {
+    sql("select timestamp '1969-07-20'")
+        .ok();
+    // PostgreSQL allows the following. We should too.
+    sql("select ^timestamp '1969-07-20 1:2'^")
+        .fails("Illegal TIMESTAMP literal '1969-07-20 1:2': not in format "
+            + "'yyyy-MM-dd HH:mm:ss'"); // PostgreSQL gives 1969-07-20 01:02:00
+    sql("select ^timestamp '1969-07-20:23:'^")
+        .fails("Illegal TIMESTAMP literal '1969-07-20:23:': not in format "
+            + "'yyyy-MM-dd HH:mm:ss'"); // PostgreSQL gives 1969-07-20 23:00:00
+  }
+
   @Test void testBooleans() {
     sql("select TRUE OR unknowN from (values(true))").ok();
     sql("select false AND unknown from (values(true))").ok();
diff --git a/core/src/test/resources/sql/misc.iq b/core/src/test/resources/sql/misc.iq
index 8761d0c632..8ce073905f 100644
--- a/core/src/test/resources/sql/misc.iq
+++ b/core/src/test/resources/sql/misc.iq
@@ -2359,4 +2359,18 @@ null
 14
 !ok
 
+# TIMESTAMP literals without a time part are OK.
+SELECT TIMESTAMP '1969-07-20' AS ts;
+TS
+1969-07-20 00:00:00
+!ok
+
+# Short TIMESTAMP literals are equivalent to long TIMESTAMP literals
+SELECT TIMESTAMP '1969-07-20' + i AS ts
+FROM (VALUES (INTERVAL '1' DAY)) AS t (i)
+GROUP BY TIMESTAMP '1969-07-20 00:00:00' + i;
+TS
+1969-07-21 00:00:00
+!ok
+
 # End misc.iq
diff --git a/server/src/test/java/org/apache/calcite/test/ServerParserTest.java b/server/src/test/java/org/apache/calcite/test/ServerParserTest.java
index de03ef7317..64ab59ef9c 100644
--- a/server/src/test/java/org/apache/calcite/test/ServerParserTest.java
+++ b/server/src/test/java/org/apache/calcite/test/ServerParserTest.java
@@ -74,7 +74,7 @@ class ServerParserTest extends SqlParserTest {
         + "OPTIONS (`ABOOLEAN` TRUE,"
         + " `ANINTEGER` -45,"
         + " `ADATE` DATE '1970-03-21',"
-        + " `quoted.id` TIMESTAMP '1970-03-21 12:04:56.78',"
+        + " `quoted.id` TIMESTAMP '1970-03-21 12:4:56.78',"
         + " `ASTRING` 'foo''bar')";
     sql(sql).ok(expected);
   }
diff --git a/site/_docs/reference.md b/site/_docs/reference.md
index 65c4a0eaa7..4de83ccc24 100644
--- a/site/_docs/reference.md
+++ b/site/_docs/reference.md
@@ -542,6 +542,7 @@ CURSOR_NAME,
 DATA,
 DATABASE,
 **DATE**,
+**DATETIME**,
 DATETIME_INTERVAL_CODE,
 DATETIME_INTERVAL_PRECISION,
 DATE_TRUNC,
diff --git a/testkit/src/main/java/org/apache/calcite/sql/parser/SqlParserTest.java b/testkit/src/main/java/org/apache/calcite/sql/parser/SqlParserTest.java
index 9b5fc4aca2..4481485d40 100644
--- a/testkit/src/main/java/org/apache/calcite/sql/parser/SqlParserTest.java
+++ b/testkit/src/main/java/org/apache/calcite/sql/parser/SqlParserTest.java
@@ -27,6 +27,7 @@ import org.apache.calcite.sql.SqlNode;
 import org.apache.calcite.sql.SqlNodeList;
 import org.apache.calcite.sql.SqlSelect;
 import org.apache.calcite.sql.SqlSetOption;
+import org.apache.calcite.sql.SqlUnknownLiteral;
 import org.apache.calcite.sql.SqlWriterConfig;
 import org.apache.calcite.sql.dialect.AnsiSqlDialect;
 import org.apache.calcite.sql.dialect.SparkSqlDialect;
@@ -34,6 +35,7 @@ import org.apache.calcite.sql.parser.SqlParser.Config;
 import org.apache.calcite.sql.pretty.SqlPrettyWriter;
 import org.apache.calcite.sql.test.SqlTestFactory;
 import org.apache.calcite.sql.test.SqlTests;
+import org.apache.calcite.sql.type.SqlTypeName;
 import org.apache.calcite.sql.util.SqlShuttle;
 import org.apache.calcite.sql.validate.SqlConformanceEnum;
 import org.apache.calcite.test.DiffTestCase;
@@ -207,6 +209,7 @@ public class SqlParserTest {
       "CYCLE",                               "99", "2003", "2011", "2014", "c",
       "DATA",                                "99",
       "DATE",                          "92", "99", "2003", "2011", "2014", "c",
+      "DATETIME",                                                          "c",
       "DAY",                           "92", "99", "2003", "2011", "2014", "c",
       "DAYS",                                              "2011",
       "DEALLOCATE",                    "92", "99", "2003", "2011", "2014", "c",
@@ -5259,8 +5262,7 @@ public class SqlParserTest {
 
     // Time literals
     expr("TIME '12:01:01'").same();
-    expr("TIME '12:01:01.'")
-        .ok("TIME '12:01:01'");
+    expr("TIME '12:01:01.'").same();
     expr("TIME '12:01:01.000'").same();
     expr("TIME '12:01:01.001'").same();
     expr("TIME '12:01:01.01023456789'").same();
@@ -5268,21 +5270,23 @@ public class SqlParserTest {
     // Timestamp literals
     expr("TIMESTAMP '2004-12-01 12:01:01'").same();
     expr("TIMESTAMP '2004-12-01 12:01:01.1'").same();
-    expr("TIMESTAMP '2004-12-01 12:01:01.'")
-        .ok("TIMESTAMP '2004-12-01 12:01:01'");
+    expr("TIMESTAMP '2004-12-01 12:01:01.'").same();
     expr("TIMESTAMP  '2004-12-01 12:01:01.010234567890'")
         .ok("TIMESTAMP '2004-12-01 12:01:01.010234567890'");
     expr("TIMESTAMP '2004-12-01 12:01:01.01023456789'").same();
 
-    // Failures.
-    sql("^DATE '12/21/99'^")
-        .fails("(?s).*Illegal DATE literal.*");
-    sql("^TIME '1230:33'^")
-        .fails("(?s).*Illegal TIME literal.*");
-    sql("^TIME '12:00:00 PM'^")
-        .fails("(?s).*Illegal TIME literal.*");
-    sql("^TIMESTAMP '12-21-99, 12:30:00'^")
-        .fails("(?s).*Illegal TIMESTAMP literal.*");
+    // Datetime, Timestamp with local time zone literals.
+    expr("DATETIME '2004-12-01 12:01:01'")
+        .same();
+
+    // Value strings that are illegal for their type are considered valid at
+    // parse time, invalid at validate time. See SqlValidatorTest.testLiteral.
+    expr("^DATE '12/21/99'^").same();
+    expr("^TIME '1230:33'^").same();
+    expr("^TIME '12:00:00 PM'^").same();
+    expr("TIMESTAMP '12-21-99, 12:30:00'").same();
+    expr("TIMESTAMP WITH LOCAL TIME ZONE '12-21-99, 12:30:00'").same();
+    expr("DATETIME '12-21-99, 12:30:00'").same();
   }
 
   /**
@@ -8850,8 +8854,12 @@ public class SqlParserTest {
   protected static String varToStr(Hoist.Variable v) {
     if (v.node instanceof SqlLiteral) {
       SqlLiteral literal = (SqlLiteral) v.node;
+      SqlTypeName typeName = literal.getTypeName();
       return "[" + v.ordinal
-          + ":" + literal.getTypeName()
+          + ":"
+          + (typeName == SqlTypeName.UNKNOWN
+              ? ((SqlUnknownLiteral) literal).tag
+              : typeName.getName())
           + ":" + literal.toValue()
           + "]";
     } else {
diff --git a/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java b/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
index 3d03cd2cb3..b56c566567 100644
--- a/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
+++ b/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
@@ -9814,8 +9814,8 @@ public class SqlOperatorTest {
         return SqlLiteral.createCharString(value.toString(), SqlParserPos.ZERO);
       case TIMESTAMP:
         TimestampString ts = TimestampString.fromMillisSinceEpoch((Long) value);
-        return SqlLiteral.createTimestamp(ts, type.getPrecision(),
-            SqlParserPos.ZERO);
+        return SqlLiteral.createTimestamp(type.getSqlTypeName(), ts,
+            type.getPrecision(), SqlParserPos.ZERO);
       default:
         throw new AssertionError(type);
       }