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:32:41 UTC

[calcite] branch main updated (13ef58b506 -> d9eeba4422)

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 13ef58b506 [CALCITE-5450] Allow 'WEEK(weekday)' time frame as argument to functions such as EXTRACT, DATE_TRUNC; allow WEEK and QUARTER in interval literals
     new f03dbbebbe Refactor SqlParserTest
     new d5737b4525 Factor interval tests out of SqlParserTest and SqlValidatorTest and into new class IntervalTest
     new ba35438c9d remove premature tests
     new 016dcd46ee [CALCITE-5495] Allow WEEK and QUARTER in INTERVAL literals
     new d9eeba4422 [CALCITE-5450] Allow 'WEEK(weekday)' time frame as argument to functions such as EXTRACT, DATE_TRUNC

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   (13ef58b506)
            \
             N -- N -- N   refs/heads/main (d9eeba4422)

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 5 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:
 .../apache/calcite/sql/SqlIntervalQualifier.java   |   76 +
 .../org/apache/calcite/test/SqlValidatorTest.java  | 1797 +-----------------
 .../calcite/sql/parser/SqlParserFixture.java       |    9 +-
 .../apache/calcite/sql/parser/SqlParserTest.java   | 1898 +------------------
 .../java/org/apache/calcite/test/IntervalTest.java | 1969 ++++++++++++++++++++
 .../org/apache/calcite/test/SqlOperatorTest.java   |    7 -
 6 files changed, 2176 insertions(+), 3580 deletions(-)
 create mode 100644 testkit/src/main/java/org/apache/calcite/test/IntervalTest.java


[calcite] 05/05: [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 d9eeba4422960832053dac45a4d3efcb22e24528
Author: Tanner Clary <ta...@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/05: Refactor SqlParserTest

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 f03dbbebbec08a220404e13231af4600e98cc372
Author: Julian Hyde <jh...@apache.org>
AuthorDate: Thu Dec 8 22:28:39 2022 -0800

    Refactor SqlParserTest
    
    If the output of a parser test case is the same as its
    input, it now must call same().
---
 .../calcite/sql/parser/SqlParserFixture.java       |   9 +-
 .../apache/calcite/sql/parser/SqlParserTest.java   | 153 ++++++++-------------
 2 files changed, 62 insertions(+), 100 deletions(-)

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 9c45c2bbba..a94f1471f9 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,10 +72,17 @@ public class SqlParserFixture {
   }
 
   public SqlParserFixture same() {
-    return ok(sap.sql);
+    return ok_(sap.sql);
   }
 
   public SqlParserFixture ok(String expected) {
+    if (expected.equals(sap.sql)) {
+      throw new AssertionError("you should call same()");
+    }
+    return ok_(expected);
+  }
+
+  private SqlParserFixture ok_(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 223a2b0bd7..6e1f83b76c 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
@@ -1965,7 +1965,7 @@ public class SqlParserTest {
   }
 
   @Test void testReverseSolidus() {
-    expr("'\\'").ok("'\\'");
+    expr("'\\'").same();
   }
 
   @Test void testSubstring() {
@@ -2440,7 +2440,7 @@ public class SqlParserTest {
         .fails("(?s).*Encountered.*");
 
     f.sql("^\"^x`y`z\"").fails("(?s).*Encountered.*");
-    f.sql("`x``y``z`").ok("`x``y``z`");
+    f.sql("`x``y``z`").same();
     f.sql("`x\\`^y^\\`z`").fails("(?s).*Encountered.*");
 
     f.sql("myMap[field] + myArray[1 + 2]")
@@ -3268,26 +3268,20 @@ public class SqlParserTest {
             + "FROM `EMP`");
 
     // Even though it looks like a date, it's just a string.
-    expr("'2004-06-01'")
-        .ok("'2004-06-01'");
+    expr("'2004-06-01'").same();
     expr("-.25")
         .ok("-0.25");
     expr("TIMESTAMP '2004-06-01 15:55:55'").same();
     expr("TIMESTAMP '2004-06-01 15:55:55.900'").same();
-    expr("TIMESTAMP '2004-06-01 15:55:55.1234'")
-        .ok("TIMESTAMP '2004-06-01 15:55:55.1234'");
-    expr("TIMESTAMP '2004-06-01 15:55:55.1236'")
-        .ok("TIMESTAMP '2004-06-01 15:55:55.1236'");
-    expr("TIMESTAMP '2004-06-01 15:55:55.9999'")
-        .ok("TIMESTAMP '2004-06-01 15:55:55.9999'");
+    expr("TIMESTAMP '2004-06-01 15:55:55.1234'").same();
+    expr("TIMESTAMP '2004-06-01 15:55:55.1236'").same();
+    expr("TIMESTAMP '2004-06-01 15:55:55.9999'").same();
     expr("NULL").same();
   }
 
   @Test void testContinuedLiteral() {
-    expr("'abba'\n'abba'")
-        .ok("'abba'\n'abba'");
-    expr("'abba'\n'0001'")
-        .ok("'abba'\n'0001'");
+    expr("'abba'\n'abba'").same();
+    expr("'abba'\n'0001'").same();
     expr("N'yabba'\n'dabba'\n'doo'")
         .ok("_ISO-8859-1'yabba'\n'dabba'\n'doo'");
     expr("_iso-8859-1'yabba'\n'dabba'\n'don''t'")
@@ -3819,16 +3813,16 @@ public class SqlParserTest {
   // expressions
   @Test void testParseNumber() {
     // Exacts
-    expr("1").ok("1");
+    expr("1").same();
     expr("+1.").ok("1");
-    expr("-1").ok("-1");
+    expr("-1").same();
     expr("- -1").ok("1");
-    expr("1.0").ok("1.0");
-    expr("-3.2").ok("-3.2");
+    expr("1.0").same();
+    expr("-3.2").same();
     expr("1.").ok("1");
     expr(".1").ok("0.1");
-    expr("2500000000").ok("2500000000");
-    expr("5000000000").ok("5000000000");
+    expr("2500000000").same();
+    expr("5000000000").same();
 
     // Approximates
     expr("1e1").ok("1E1");
@@ -3941,14 +3935,10 @@ public class SqlParserTest {
   }
 
   @Test void testQuotesInString() {
-    expr("'a''b'")
-        .ok("'a''b'");
-    expr("'''x'")
-        .ok("'''x'");
-    expr("''")
-        .ok("''");
-    expr("'Quoted strings aren''t \"hard\"'")
-        .ok("'Quoted strings aren''t \"hard\"'");
+    expr("'a''b'").same();
+    expr("'''x'").same();
+    expr("''").same();
+    expr("'Quoted strings aren''t \"hard\"'").same();
   }
 
   @Test void testScalarQueryInWhere() {
@@ -4931,12 +4921,10 @@ public class SqlParserTest {
         .ok("_ISO-8859-1'is it a plane? no it''s superman!'");
     expr("n'lowercase n'")
         .ok("_ISO-8859-1'lowercase n'");
-    expr("'boring string'")
-        .ok("'boring string'");
+    expr("'boring string'").same();
     expr("_iSo-8859-1'bye'")
         .ok("_ISO-8859-1'bye'");
-    expr("'three'\n' blind'\n' mice'")
-        .ok("'three'\n' blind'\n' mice'");
+    expr("'three'\n' blind'\n' mice'").same();
     expr("'three' -- comment\n' blind'\n' mice'")
         .ok("'three'\n' blind'\n' mice'");
     expr("N'bye' \t\r\f\f\n' bye'")
@@ -4947,15 +4935,13 @@ public class SqlParserTest {
         .ok("_UTF8'hi'");
 
     // newline in string literal
-    expr("'foo\rbar'")
-        .ok("'foo\rbar'");
-    expr("'foo\nbar'")
-        .ok("'foo\nbar'");
+    expr("'foo\rbar'").same();
+    expr("'foo\nbar'").same();
 
     expr("'foo\r\nbar'")
         // prevent test infrastructure from converting '\r\n' to '\n'
         .withConvertToLinux(false)
-        .ok("'foo\r\nbar'");
+        .same();
   }
 
   @Test void testStringLiteralFails() {
@@ -5211,80 +5197,65 @@ public class SqlParserTest {
   // check date/time functions.
   @Test void testTimeDate() {
     // CURRENT_TIME - returns time w/ timezone
-    expr("CURRENT_TIME(3)")
-        .ok("CURRENT_TIME(3)");
+    expr("CURRENT_TIME(3)").same();
 
     // checkFails("SELECT CURRENT_TIME() FROM foo",
     //     "SELECT CURRENT_TIME() FROM `FOO`");
 
-    expr("CURRENT_TIME")
-        .ok("CURRENT_TIME");
+    expr("CURRENT_TIME").same();
     expr("CURRENT_TIME(x+y)")
         .ok("CURRENT_TIME((`X` + `Y`))");
 
     // LOCALTIME returns time w/o TZ
-    expr("LOCALTIME(3)")
-        .ok("LOCALTIME(3)");
+    expr("LOCALTIME(3)").same();
 
     // checkFails("SELECT LOCALTIME() FROM foo",
     //     "SELECT LOCALTIME() FROM `FOO`");
 
-    expr("LOCALTIME")
-        .ok("LOCALTIME");
+    expr("LOCALTIME").same();
     expr("LOCALTIME(x+y)")
         .ok("LOCALTIME((`X` + `Y`))");
 
     // LOCALTIMESTAMP - returns timestamp w/o TZ
-    expr("LOCALTIMESTAMP(3)")
-        .ok("LOCALTIMESTAMP(3)");
+    expr("LOCALTIMESTAMP(3)").same();
 
     // checkFails("SELECT LOCALTIMESTAMP() FROM foo",
     //     "SELECT LOCALTIMESTAMP() FROM `FOO`");
 
-    expr("LOCALTIMESTAMP")
-        .ok("LOCALTIMESTAMP");
+    expr("LOCALTIMESTAMP").same();
     expr("LOCALTIMESTAMP(x+y)")
         .ok("LOCALTIMESTAMP((`X` + `Y`))");
 
     // CURRENT_DATE - returns DATE
-    expr("CURRENT_DATE(3)")
-        .ok("CURRENT_DATE(3)");
+    expr("CURRENT_DATE(3)").same();
 
     // checkFails("SELECT CURRENT_DATE() FROM foo",
     //     "SELECT CURRENT_DATE() FROM `FOO`");
-    expr("CURRENT_DATE")
-        .ok("CURRENT_DATE");
+    expr("CURRENT_DATE").same();
 
     // checkFails("SELECT CURRENT_DATE(x+y) FROM foo",
     //     "CURRENT_DATE((`X` + `Y`))");
 
     // CURRENT_TIMESTAMP - returns timestamp w/ TZ
-    expr("CURRENT_TIMESTAMP(3)")
-        .ok("CURRENT_TIMESTAMP(3)");
+    expr("CURRENT_TIMESTAMP(3)").same();
 
     // checkFails("SELECT CURRENT_TIMESTAMP() FROM foo",
     //     "SELECT CURRENT_TIMESTAMP() FROM `FOO`");
 
-    expr("CURRENT_TIMESTAMP")
-        .ok("CURRENT_TIMESTAMP");
+    expr("CURRENT_TIMESTAMP").same();
     expr("CURRENT_TIMESTAMP(x+y)")
         .ok("CURRENT_TIMESTAMP((`X` + `Y`))");
 
     // Date literals
-    expr("DATE '2004-12-01'")
-        .ok("DATE '2004-12-01'");
+    expr("DATE '2004-12-01'").same();
 
     // Time literals
-    expr("TIME '12:01:01'")
-        .ok("TIME '12:01:01'");
+    expr("TIME '12:01:01'").same();
     expr("TIME '12:01:01.'")
         .ok("TIME '12:01:01'");
-    expr("TIME '12:01:01.000'")
-        .ok("TIME '12:01:01.000'");
-    expr("TIME '12:01:01.001'")
-        .ok("TIME '12:01:01.001'");
-    expr("TIME '12:01:01.01023456789'")
-        .ok("TIME '12:01:01.01023456789'");
+    expr("TIME '12:01:01.000'").same();
+    expr("TIME '12:01:01.001'").same();
+    expr("TIME '12:01:01.01023456789'").same();
 
     // Timestamp literals
     expr("TIMESTAMP '2004-12-01 12:01:01'")
@@ -5314,10 +5285,8 @@ public class SqlParserTest {
   @Test void testDateTimeCast() {
     //   checkExp("CAST(DATE '2001-12-21' AS CHARACTER VARYING)",
     // "CAST(2001-12-21)");
-    expr("CAST('2001-12-21' AS DATE)")
-        .ok("CAST('2001-12-21' AS DATE)");
-    expr("CAST(12 AS DATE)")
-        .ok("CAST(12 AS DATE)");
+    expr("CAST('2001-12-21' AS DATE)").same();
+    expr("CAST(12 AS DATE)").same();
     sql("CAST('2000-12-21' AS DATE ^NOT^ NULL)")
         .fails("(?s).*Encountered \"NOT\" at line 1, column 27.*");
     sql("CAST('foo' as ^1^)")
@@ -6617,47 +6586,33 @@ public class SqlParserTest {
    */
   public void subTestIntervalYearFailsValidation() {
     // Qualifier - field mismatches
-    expr("INTERVAL '-' YEAR")
-        .ok("INTERVAL '-' YEAR");
-    expr("INTERVAL '1-2' YEAR")
-        .ok("INTERVAL '1-2' YEAR");
-    expr("INTERVAL '1.2' YEAR")
-        .ok("INTERVAL '1.2' YEAR");
-    expr("INTERVAL '1 2' YEAR")
-        .ok("INTERVAL '1 2' YEAR");
-    expr("INTERVAL '1-2' YEAR(2)")
-        .ok("INTERVAL '1-2' YEAR(2)");
-    expr("INTERVAL 'bogus text' YEAR")
-        .ok("INTERVAL 'bogus text' YEAR");
+    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")
-        .ok("INTERVAL '--1' YEAR");
+    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)")
-        .ok("INTERVAL '100' YEAR(2)");
-    expr("INTERVAL '1000' YEAR(3)")
-        .ok("INTERVAL '1000' YEAR(3)");
-    expr("INTERVAL '-1000' YEAR(3)")
-        .ok("INTERVAL '-1000' YEAR(3)");
-    expr("INTERVAL '2147483648' YEAR(10)")
-        .ok("INTERVAL '2147483648' YEAR(10)");
-    expr("INTERVAL '-2147483648' YEAR(10)")
-        .ok("INTERVAL '-2147483648' YEAR(10)");
+    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)")
-        .ok("INTERVAL '1' YEAR(11)");
+    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)")
-        .ok("INTERVAL '0' YEAR(0)");
+    expr("INTERVAL '0' YEAR(0)").same();
   }
 
   /**


[calcite] 02/05: 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 d5737b4525f67da525bd888444536f04687482d2
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 | 1957 ++++++++++++++++++++
 4 files changed, 2043 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..56e8d6976c
--- /dev/null
+++ b/testkit/src/main/java/org/apache/calcite/test/IntervalTest.java
@@ -0,0 +1,1957 @@
+/*
+ * 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();
+    subTestIntervalWeekPositive();
+    subTestIntervalQuarterPositive();
+    subTestIntervalPlural();
+
+    // 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... 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 '1' WEEK");
+    f.expr("interval '99' week")
+        .columnType("INTERVAL '99' WEEK");
+
+    // explicit precision equal to default
+    f.expr("interval '1' week(2)")
+        .columnType("INTERVAL '1' WEEK(2)");
+    f.expr("interval '99' week(2)")
+        .columnType("INTERVAL '99' WEEK(2)");
+
+    // max precision
+    f.expr("interval '2147483647' week(10)")
+        .columnType("INTERVAL '2147483647' WEEK(10)");
+
+    // min precision
+    f.expr("interval '0' week(1)")
+        .columnType("INTERVAL '0' WEEK(1)");
+
+    // alternate precision
+    f.expr("interval '1234' week(4)")
+        .columnType("INTERVAL '1234' WEEK(4)");
+
+    // sign
+    f.expr("interval '+1' week")
+        .columnType("INTERVAL '+1' WEEK");
+    f.expr("interval '-1' week")
+        .columnType("INTERVAL '-1' WEEK");
+    f.expr("interval +'1' week")
+        .columnType("INTERVAL '1' WEEK");
+    f.expr("interval +'+1' week")
+        .columnType("INTERVAL '+1' WEEK");
+    f.expr("interval +'-1' week")
+        .columnType("INTERVAL '-1' WEEK");
+    f.expr("interval -'1' week")
+        .columnType("INTERVAL -'1' WEEK");
+    f.expr("interval -'+1' week")
+        .columnType("INTERVAL -'+1' WEEK");
+    f.expr("interval -'-1' week")
+        .columnType("INTERVAL -'-1' WEEK");
+  }
+
+  /**
+   * 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 '1' QUARTER");
+    f.expr("interval '99' quarter")
+        .columnType("INTERVAL '99' QUARTER");
+
+    // explicit precision equal to default
+    f.expr("interval '1' quarter(2)")
+        .columnType("INTERVAL '1' QUARTER(2)");
+    f.expr("interval '99' quarter(2)")
+        .columnType("INTERVAL '99' QUARTER(2)");
+
+    // max precision
+    f.expr("interval '2147483647' quarter(10)")
+        .columnType("INTERVAL '2147483647' QUARTER(10)");
+
+    // min precision
+    f.expr("interval '0' quarter(1)")
+        .columnType("INTERVAL '0' QUARTER(1)");
+
+    // alternate precision
+    f.expr("interval '1234' quarter(4)")
+        .columnType("INTERVAL '1234' QUARTER(4)");
+
+    // sign
+    f.expr("interval '+1' quarter")
+        .columnType("INTERVAL '+1' QUARTER");
+    f.expr("interval '-1' quarter")
+        .columnType("INTERVAL '-1' QUARTER");
+    f.expr("interval +'1' quarter")
+        .columnType("INTERVAL '1' QUARTER");
+    f.expr("interval +'+1' quarter")
+        .columnType("INTERVAL '+1' QUARTER");
+    f.expr("interval +'-1' quarter")
+        .columnType("INTERVAL '-1' QUARTER");
+    f.expr("interval -'1' quarter")
+        .columnType("INTERVAL -'1' QUARTER");
+    f.expr("interval -'+1' quarter")
+        .columnType("INTERVAL -'+1' QUARTER");
+    f.expr("interval -'-1' quarter")
+        .columnType("INTERVAL -'-1' QUARTER");
+  }
+
+  public void subTestIntervalPlural() {
+    f.expr("interval '+2' seconds")
+        .columnType("INTERVAL '+2' SECOND");
+    f.expr("interval '+2' hours")
+        .columnType("INTERVAL '+2' HOUR");
+    f.expr("interval '+2' days")
+        .columnType("INTERVAL '+2' DAY");
+    f.expr("interval '+2' weeks")
+        .columnType("INTERVAL '+2' WEEK");
+    f.expr("interval '+2' quarters")
+        .columnType("INTERVAL '+2' QUARTER");
+    f.expr("interval '+2' months")
+        .columnType("INTERVAL '+2' MONTH");
+    f.expr("interval '+2' years")
+        .columnType("INTERVAL '+2' YEAR");
+  }
+
+  /**
+   * 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] 03/05: remove premature tests

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 ba35438c9d755aaa06dc353b8d9f5164608b36cd
Author: Julian Hyde <jh...@apache.org>
AuthorDate: Tue Jan 24 23:17:22 2023 -0800

    remove premature tests
---
 .../java/org/apache/calcite/test/IntervalTest.java | 123 ---------------------
 1 file changed, 123 deletions(-)

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 56e8d6976c..1f6d5ed30d 100644
--- a/testkit/src/main/java/org/apache/calcite/test/IntervalTest.java
+++ b/testkit/src/main/java/org/apache/calcite/test/IntervalTest.java
@@ -44,9 +44,6 @@ public class IntervalTest {
     subTestIntervalMinutePositive();
     subTestIntervalMinuteToSecondPositive();
     subTestIntervalSecondPositive();
-    subTestIntervalWeekPositive();
-    subTestIntervalQuarterPositive();
-    subTestIntervalPlural();
 
     // Tests that should pass parser but fail validator
     subTestIntervalYearNegative();
@@ -963,126 +960,6 @@ 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 '1' WEEK");
-    f.expr("interval '99' week")
-        .columnType("INTERVAL '99' WEEK");
-
-    // explicit precision equal to default
-    f.expr("interval '1' week(2)")
-        .columnType("INTERVAL '1' WEEK(2)");
-    f.expr("interval '99' week(2)")
-        .columnType("INTERVAL '99' WEEK(2)");
-
-    // max precision
-    f.expr("interval '2147483647' week(10)")
-        .columnType("INTERVAL '2147483647' WEEK(10)");
-
-    // min precision
-    f.expr("interval '0' week(1)")
-        .columnType("INTERVAL '0' WEEK(1)");
-
-    // alternate precision
-    f.expr("interval '1234' week(4)")
-        .columnType("INTERVAL '1234' WEEK(4)");
-
-    // sign
-    f.expr("interval '+1' week")
-        .columnType("INTERVAL '+1' WEEK");
-    f.expr("interval '-1' week")
-        .columnType("INTERVAL '-1' WEEK");
-    f.expr("interval +'1' week")
-        .columnType("INTERVAL '1' WEEK");
-    f.expr("interval +'+1' week")
-        .columnType("INTERVAL '+1' WEEK");
-    f.expr("interval +'-1' week")
-        .columnType("INTERVAL '-1' WEEK");
-    f.expr("interval -'1' week")
-        .columnType("INTERVAL -'1' WEEK");
-    f.expr("interval -'+1' week")
-        .columnType("INTERVAL -'+1' WEEK");
-    f.expr("interval -'-1' week")
-        .columnType("INTERVAL -'-1' WEEK");
-  }
-
-  /**
-   * 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 '1' QUARTER");
-    f.expr("interval '99' quarter")
-        .columnType("INTERVAL '99' QUARTER");
-
-    // explicit precision equal to default
-    f.expr("interval '1' quarter(2)")
-        .columnType("INTERVAL '1' QUARTER(2)");
-    f.expr("interval '99' quarter(2)")
-        .columnType("INTERVAL '99' QUARTER(2)");
-
-    // max precision
-    f.expr("interval '2147483647' quarter(10)")
-        .columnType("INTERVAL '2147483647' QUARTER(10)");
-
-    // min precision
-    f.expr("interval '0' quarter(1)")
-        .columnType("INTERVAL '0' QUARTER(1)");
-
-    // alternate precision
-    f.expr("interval '1234' quarter(4)")
-        .columnType("INTERVAL '1234' QUARTER(4)");
-
-    // sign
-    f.expr("interval '+1' quarter")
-        .columnType("INTERVAL '+1' QUARTER");
-    f.expr("interval '-1' quarter")
-        .columnType("INTERVAL '-1' QUARTER");
-    f.expr("interval +'1' quarter")
-        .columnType("INTERVAL '1' QUARTER");
-    f.expr("interval +'+1' quarter")
-        .columnType("INTERVAL '+1' QUARTER");
-    f.expr("interval +'-1' quarter")
-        .columnType("INTERVAL '-1' QUARTER");
-    f.expr("interval -'1' quarter")
-        .columnType("INTERVAL -'1' QUARTER");
-    f.expr("interval -'+1' quarter")
-        .columnType("INTERVAL -'+1' QUARTER");
-    f.expr("interval -'-1' quarter")
-        .columnType("INTERVAL -'-1' QUARTER");
-  }
-
-  public void subTestIntervalPlural() {
-    f.expr("interval '+2' seconds")
-        .columnType("INTERVAL '+2' SECOND");
-    f.expr("interval '+2' hours")
-        .columnType("INTERVAL '+2' HOUR");
-    f.expr("interval '+2' days")
-        .columnType("INTERVAL '+2' DAY");
-    f.expr("interval '+2' weeks")
-        .columnType("INTERVAL '+2' WEEK");
-    f.expr("interval '+2' quarters")
-        .columnType("INTERVAL '+2' QUARTER");
-    f.expr("interval '+2' months")
-        .columnType("INTERVAL '+2' MONTH");
-    f.expr("interval '+2' years")
-        .columnType("INTERVAL '+2' YEAR");
-  }
-
   /**
    * Runs tests for INTERVAL... MONTH that should pass parser but fail
    * validator. A substantially identical set of tests exists in


[calcite] 04/05: [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 016dcd46ee90022904720fa9c51db8e15dcef4e9
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");