[CALCITE-6015] AssertionError during optimization of EXTRACT expression

Signed-off-by: Mihai Budiu <mbudiu@feldera.com>
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 9cabfcb..6bf8f18 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
@@ -3128,7 +3128,8 @@
       case MILLISECOND:
       case MICROSECOND:
       case NANOSECOND:
-        if (sqlTypeName == SqlTypeName.DATE) {
+        if (sqlTypeName == SqlTypeName.DATE
+            || SqlTypeName.YEAR_INTERVAL_TYPES.contains(sqlTypeName)) {
           return Expressions.constant(0L);
         }
         operand = mod(operand, TimeUnit.MINUTE.multiplier.longValue(), !isIntervalType);
@@ -3155,9 +3156,6 @@
                       translator.getRoot()));
           return Expressions.divide(operand,
               Expressions.constant(TimeUnit.SECOND.multiplier.longValue()));
-        case INTERVAL_YEAR:
-        case INTERVAL_YEAR_MONTH:
-        case INTERVAL_MONTH:
         case INTERVAL_DAY:
         case INTERVAL_DAY_HOUR:
         case INTERVAL_DAY_MINUTE:
@@ -3168,6 +3166,11 @@
         case INTERVAL_MINUTE:
         case INTERVAL_MINUTE_SECOND:
         case INTERVAL_SECOND:
+          return Expressions.divide(operand,
+              Expressions.constant(TimeUnit.SECOND.multiplier.longValue()));
+        case INTERVAL_YEAR:
+        case INTERVAL_YEAR_MONTH:
+        case INTERVAL_MONTH:
           // no convertlet conversion, pass it as extract
           throw new AssertionError("unexpected " + sqlTypeName);
         default:
diff --git a/core/src/main/java/org/apache/calcite/sql/SqlCall.java b/core/src/main/java/org/apache/calcite/sql/SqlCall.java
index 9eba738..617fa46 100755
--- a/core/src/main/java/org/apache/calcite/sql/SqlCall.java
+++ b/core/src/main/java/org/apache/calcite/sql/SqlCall.java
@@ -191,7 +191,7 @@
    * Returns a string describing the actual argument types of a call, e.g.
    * "SUBSTR(VARCHAR(12), NUMBER(3,2), INTEGER)".
    */
-  protected String getCallSignature(
+  public String getCallSignature(
       SqlValidator validator,
       @Nullable SqlValidatorScope scope) {
     List<String> signatureList = new ArrayList<>();
diff --git a/core/src/main/java/org/apache/calcite/sql/fun/SqlExtractFunction.java b/core/src/main/java/org/apache/calcite/sql/fun/SqlExtractFunction.java
index 07789aa..1c8a20b 100644
--- a/core/src/main/java/org/apache/calcite/sql/fun/SqlExtractFunction.java
+++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlExtractFunction.java
@@ -17,6 +17,7 @@
 package org.apache.calcite.sql.fun;
 
 import org.apache.calcite.avatica.util.TimeUnitRange;
+import org.apache.calcite.rel.type.RelDataType;
 import org.apache.calcite.sql.SqlCall;
 import org.apache.calcite.sql.SqlFunction;
 import org.apache.calcite.sql.SqlFunctionCategory;
@@ -26,12 +27,16 @@
 import org.apache.calcite.sql.SqlWriter;
 import org.apache.calcite.sql.type.OperandTypes;
 import org.apache.calcite.sql.type.ReturnTypes;
+import org.apache.calcite.sql.type.SqlTypeName;
 import org.apache.calcite.sql.validate.SqlMonotonicity;
 import org.apache.calcite.sql.validate.SqlValidator;
 import org.apache.calcite.sql.validate.SqlValidatorScope;
 import org.apache.calcite.util.Util;
 
+import com.google.common.collect.ImmutableSet;
+
 import static org.apache.calcite.sql.validate.SqlNonNullableAccessors.getOperandLiteralValueOrThrow;
+import static org.apache.calcite.util.Static.RESOURCE;
 
 /**
  * The SQL <code>EXTRACT</code> operator. Extracts a specified field value from
@@ -70,6 +75,59 @@
     writer.endFunCall(frame);
   }
 
+  // List of types that support EXTRACT(X, ...) where X is MONTH or larger
+  private static final ImmutableSet<SqlTypeName> MONTH_AND_ABOVE_TYPES =
+      new ImmutableSet.Builder<SqlTypeName>()
+      .add(SqlTypeName.DATE)
+      .add(SqlTypeName.TIMESTAMP)
+      .add(SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE)
+      .addAll(SqlTypeName.YEAR_INTERVAL_TYPES)
+      .build();
+
+  // List of types that support EXTRACT(X, ...) where X is between DAY and WEEK
+  private static final ImmutableSet<SqlTypeName> DAY_TO_WEEK_TYPES =
+      new ImmutableSet.Builder<SqlTypeName>()
+          .add(SqlTypeName.DATE)
+          .add(SqlTypeName.TIMESTAMP)
+          .add(SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE)
+          .build();
+
+  // List of types that support EXTRACT(EPOCH, ...)
+  private static final ImmutableSet<SqlTypeName> EPOCH_TYPES =
+      new ImmutableSet.Builder<SqlTypeName>()
+          .add(SqlTypeName.DATE)
+          .add(SqlTypeName.TIMESTAMP)
+          .add(SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE)
+          .addAll(SqlTypeName.YEAR_INTERVAL_TYPES)
+          .addAll(SqlTypeName.DAY_INTERVAL_TYPES)
+          .build();
+
+  // List of types that support EXTRACT(DAY, ...)
+  private static final ImmutableSet<SqlTypeName> DAY_TYPES =
+      new ImmutableSet.Builder<SqlTypeName>()
+          .add(SqlTypeName.DATE)
+          .add(SqlTypeName.TIMESTAMP)
+          .add(SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE)
+          .add(SqlTypeName.INTERVAL_DAY)
+          .add(SqlTypeName.INTERVAL_DAY_HOUR)
+          .add(SqlTypeName.INTERVAL_DAY_MINUTE)
+          .add(SqlTypeName.INTERVAL_DAY_SECOND)
+          .addAll(SqlTypeName.YEAR_INTERVAL_TYPES)
+          .build();
+
+  // List of types that support EXTRACT(X, ...) where X is
+  // between HOUR and NANOSECOND
+  private static final ImmutableSet<SqlTypeName> HOUR_TO_NANOSECOND_TYPES =
+      new ImmutableSet.Builder<SqlTypeName>()
+          .add(SqlTypeName.DATE)
+          .add(SqlTypeName.TIMESTAMP)
+          .add(SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE)
+          .add(SqlTypeName.TIME)
+          .add(SqlTypeName.TIME_WITH_LOCAL_TIME_ZONE)
+          .addAll(SqlTypeName.YEAR_INTERVAL_TYPES)
+          .addAll(SqlTypeName.DAY_INTERVAL_TYPES)
+          .build();
+
   @Override public void validateCall(SqlCall call, SqlValidator validator,
       SqlValidatorScope scope, SqlValidatorScope operandScope) {
     super.validateCall(call, validator, scope, operandScope);
@@ -83,8 +141,65 @@
     //    startUnit = EPOCH and timeFrameName = 'MINUTE15'.
     //
     // If the latter, check that timeFrameName is valid.
-    validator.validateTimeFrame(
-        (SqlIntervalQualifier) call.getOperandList().get(0));
+    SqlIntervalQualifier qualifier = call.operand(0);
+    validator.validateTimeFrame(qualifier);
+    TimeUnitRange range = qualifier.timeUnitRange;
+
+    RelDataType type = validator.getValidatedNodeTypeIfKnown(call.operand(1));
+    if (type == null) {
+      return;
+    }
+
+    SqlTypeName typeName = type.getSqlTypeName();
+    boolean legal;
+    switch (range) {
+    case YEAR:
+    case MONTH:
+    case ISOYEAR:
+    case QUARTER:
+    case DECADE:
+    case CENTURY:
+    case MILLENNIUM:
+      legal = MONTH_AND_ABOVE_TYPES.contains(typeName);
+      break;
+    case WEEK:
+    case DOW:
+    case ISODOW:
+    case DOY:
+      legal = DAY_TO_WEEK_TYPES.contains(typeName);
+      break;
+    case EPOCH:
+      legal = EPOCH_TYPES.contains(typeName);
+      break;
+    case DAY:
+      legal = DAY_TYPES.contains(typeName);
+      break;
+    case HOUR:
+    case MINUTE:
+    case SECOND:
+    case MILLISECOND:
+    case MICROSECOND:
+    case NANOSECOND:
+      legal = HOUR_TO_NANOSECOND_TYPES.contains(typeName);
+      break;
+    case YEAR_TO_MONTH:
+    case DAY_TO_HOUR:
+    case DAY_TO_MINUTE:
+    case DAY_TO_SECOND:
+    case HOUR_TO_MINUTE:
+    case HOUR_TO_SECOND:
+    case MINUTE_TO_SECOND:
+    default:
+      legal = false;
+      break;
+    }
+
+    if (!legal) {
+      throw validator.newValidationError(call,
+          RESOURCE.canNotApplyOp2Type(call.getOperator().getName(),
+              call.getCallSignature(validator, scope),
+              call.getOperator().getAllowedSignatures()));
+    }
   }
 
   @Override public SqlMonotonicity getMonotonicity(SqlOperatorBinding call) {
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 67367ba..c7a85cd 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
@@ -1100,7 +1100,8 @@
       family(SqlTypeFamily.DATETIME_INTERVAL, SqlTypeFamily.DATETIME);
 
   public static final SqlSingleOperandTypeChecker INTERVALINTERVAL_INTERVALDATETIME =
-      INTERVAL_SAME_SAME.or(INTERVAL_DATETIME);
+      INTERVAL_SAME_SAME.or(INTERVAL_DATETIME)
+          .or(family(SqlTypeFamily.INTERVAL_DAY_TIME, SqlTypeFamily.INTERVAL_YEAR_MONTH));
 
   // TODO: datetime+interval checking missing
   // TODO: interval+datetime checking missing
diff --git a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java
index 8612da7..9572cf4 100644
--- a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java
+++ b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java
@@ -1869,7 +1869,7 @@
     }
     final SqlNode original = originalExprs.get(node);
     if (original != null && original != node) {
-      return getValidatedNodeType(original);
+      return getValidatedNodeTypeIfKnown(original);
     }
     if (node instanceof SqlIdentifier) {
       return getCatalogReader().getNamedType((SqlIdentifier) node);
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 21e2e91..11c2edf 100644
--- a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
+++ b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
@@ -7067,9 +7067,8 @@
         .columnType("BIGINT NOT NULL");
     expr("extract(minute from interval '1.1' second)").ok();
     expr("extract(year from DATE '2008-2-2')").ok();
+    expr("extract(minute from interval '11' month)").ok();
 
-    wholeExpr("extract(minute from interval '11' month)")
-        .fails("(?s).*Cannot apply.*");
     wholeExpr("extract(year from interval '11' second)")
         .fails("(?s).*Cannot apply.*");
   }
diff --git a/site/_docs/history.md b/site/_docs/history.md
index 553ca1c..f3140e7 100644
--- a/site/_docs/history.md
+++ b/site/_docs/history.md
@@ -43,6 +43,10 @@
 #### Breaking Changes
 {: #breaking-1-37-0}
 
+* In the context of [CALCITE-6015] the visibility of the method
+`SqlCall.getCallSignature` has been converted from `protected` to `public`.
+ Any subclass overriding it will need to be adjusted accordingly.
+
 Compatibility: This release is tested on Linux, macOS, Microsoft Windows;
 using JDK/OpenJDK versions 8 to 19;
 Guava versions 21.0 to 32.1.3-jre;
diff --git a/testkit/src/main/java/org/apache/calcite/sql/test/SqlOperatorFixture.java b/testkit/src/main/java/org/apache/calcite/sql/test/SqlOperatorFixture.java
index f87208c..af4264e 100644
--- a/testkit/src/main/java/org/apache/calcite/sql/test/SqlOperatorFixture.java
+++ b/testkit/src/main/java/org/apache/calcite/sql/test/SqlOperatorFixture.java
@@ -84,14 +84,6 @@
   // TODO: Change message
   String BAD_DATETIME_MESSAGE = "(?s).*";
 
-  // Error messages when an invalid time unit is given as
-  // input to extract for a particular input type.
-  String INVALID_EXTRACT_UNIT_CONVERTLET_ERROR =
-      "Was not expecting value '.*' for enumeration.*";
-
-  String INVALID_EXTRACT_UNIT_VALIDATION_ERROR =
-      "Cannot apply 'EXTRACT' to arguments of type .*'\n.*";
-
   String LITERAL_OUT_OF_RANGE_MESSAGE =
       "(?s).*Numeric literal.*out of range.*";
 
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 5a772c8..023de7c 100644
--- a/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
+++ b/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
@@ -129,8 +129,6 @@
 import static org.apache.calcite.sql.test.SqlOperatorFixture.DIVISION_BY_ZERO_MESSAGE;
 import static org.apache.calcite.sql.test.SqlOperatorFixture.INVALID_ARGUMENTS_NUMBER;
 import static org.apache.calcite.sql.test.SqlOperatorFixture.INVALID_CHAR_MESSAGE;
-import static org.apache.calcite.sql.test.SqlOperatorFixture.INVALID_EXTRACT_UNIT_CONVERTLET_ERROR;
-import static org.apache.calcite.sql.test.SqlOperatorFixture.INVALID_EXTRACT_UNIT_VALIDATION_ERROR;
 import static org.apache.calcite.sql.test.SqlOperatorFixture.LITERAL_OUT_OF_RANGE_MESSAGE;
 import static org.apache.calcite.sql.test.SqlOperatorFixture.OUT_OF_RANGE_MESSAGE;
 import static org.apache.calcite.sql.test.SqlOperatorFixture.WRONG_FORMAT_MESSAGE;
@@ -10626,64 +10624,45 @@
     f.setFor(SqlStdOperatorTable.EXTRACT, VM_FENNEL, VM_JAVA);
 
     if (TODO) {
-      // Not supported, fails in type validation because the extract
-      // unit is not YearMonth interval type.
-
       f.checkScalar("extract(epoch from interval '4-2' year to month)",
           // number of seconds elapsed since timestamp
           // '1970-01-01 00:00:00' + input interval
           "131328000", "BIGINT NOT NULL");
-
-      f.checkScalar("extract(second from interval '4-2' year to month)",
-          "0", "BIGINT NOT NULL");
-
-      f.checkScalar("extract(millisecond from "
-          + "interval '4-2' year to month)", "0", "BIGINT NOT NULL");
-
-      f.checkScalar("extract(microsecond "
-          + "from interval '4-2' year to month)", "0", "BIGINT NOT NULL");
-
-      f.checkScalar("extract(nanosecond from "
-          + "interval '4-2' year to month)", "0", "BIGINT NOT NULL");
-
-      f.checkScalar("extract(minute from interval '4-2' year to month)",
-          "0", "BIGINT NOT NULL");
-
-      f.checkScalar("extract(hour from interval '4-2' year to month)",
-          "0", "BIGINT NOT NULL");
-
-      f.checkScalar("extract(day from interval '4-2' year to month)",
-          "0", "BIGINT NOT NULL");
     }
+    f.checkScalar("extract(second from interval '4-2' year to month)",
+        "0", "BIGINT NOT NULL");
+    f.checkScalar("extract(millisecond from "
+        + "interval '4-2' year to month)", "0", "BIGINT NOT NULL");
+    f.checkScalar("extract(microsecond "
+        + "from interval '4-2' year to month)", "0", "BIGINT NOT NULL");
+    f.checkScalar("extract(nanosecond from "
+        + "interval '4-2' year to month)", "0", "BIGINT NOT NULL");
+    f.checkScalar("extract(minute from interval '4-2' year to month)",
+        "0", "BIGINT NOT NULL");
+    f.checkScalar("extract(hour from interval '4-2' year to month)",
+        "0", "BIGINT NOT NULL");
+    f.checkScalar("extract(day from interval '4-2' year to month)",
+        "0", "BIGINT NOT NULL");
 
-    // Postgres doesn't support DOW, ISODOW, DOY and WEEK on INTERVAL YEAR MONTH type.
-    // SQL standard doesn't have extract units for DOW, ISODOW, DOY and WEEK.
-    if (Bug.CALCITE_2539_FIXED) {
-      f.checkFails("extract(doy from interval '4-2' year to month)",
-          INVALID_EXTRACT_UNIT_VALIDATION_ERROR, false);
-      f.checkFails("^extract(dow from interval '4-2' year to month)^",
-          INVALID_EXTRACT_UNIT_VALIDATION_ERROR, false);
-      f.checkFails("^extract(week from interval '4-2' year to month)^",
-          INVALID_EXTRACT_UNIT_VALIDATION_ERROR, false);
-      f.checkFails("^extract(isodow from interval '4-2' year to month)^",
-          INVALID_EXTRACT_UNIT_VALIDATION_ERROR, false);
-    }
+    final String fail = "Cannot apply 'EXTRACT' to arguments of type 'EXTRACT\\(<.*> "
+        + "FROM <INTERVAL YEAR TO MONTH>\\)'\\. Supported form\\(s\\): "
+        + ".*\\n.*\\n.*";
+
+    f.checkFails("^extract(doy from interval '4-2' year to month)^", fail, false);
+    f.checkFails("^extract(dow from interval '4-2' year to month)^", fail, false);
+    f.checkFails("^extract(isodow from interval '4-2' year to month)^", fail, false);
+    f.checkFails("^extract(week from interval '4-2' year to month)^", fail, false);
 
     f.checkScalar("extract(month from interval '4-2' year to month)",
         "2", "BIGINT NOT NULL");
-
     f.checkScalar("extract(quarter from interval '4-2' year to month)",
         "1", "BIGINT NOT NULL");
-
     f.checkScalar("extract(year from interval '4-2' year to month)",
         "4", "BIGINT NOT NULL");
-
     f.checkScalar("extract(decade from "
         + "interval '426-3' year(3) to month)", "42", "BIGINT NOT NULL");
-
     f.checkScalar("extract(century from "
         + "interval '426-3' year(3) to month)", "4", "BIGINT NOT NULL");
-
     f.checkScalar("extract(millennium from "
         + "interval '2005-3' year(4) to month)", "2", "BIGINT NOT NULL");
   }
@@ -10692,15 +10671,11 @@
     final SqlOperatorFixture f = fixture();
     f.setFor(SqlStdOperatorTable.EXTRACT, VM_FENNEL, VM_JAVA);
 
-    if (TODO) {
-      // Not implemented in operator test
-      f.checkScalar("extract(epoch from "
-              + "interval '2 3:4:5.678' day to second)",
-          // number of seconds elapsed since timestamp
-          // '1970-01-01 00:00:00' + input interval
-          "183845.678",
-          "BIGINT NOT NULL");
-    }
+    f.checkScalar("extract(epoch from interval '2 3:4:5.678' day to second)",
+        // number of seconds elapsed since timestamp
+        // '1970-01-01 00:00:00' + input interval
+        "183845",
+        "BIGINT NOT NULL");
 
     f.checkScalar("extract(millisecond from "
             + "interval '2 3:4:5.678' day to second)",
@@ -10737,46 +10712,19 @@
         "2",
         "BIGINT NOT NULL");
 
-    // Postgres doesn't support DOW, ISODOW, DOY and WEEK on INTERVAL DAY TIME type.
-    // SQL standard doesn't have extract units for DOW, ISODOW, DOY and WEEK.
-    f.checkFails("extract(doy from interval '2 3:4:5.678' day to second)",
-        INVALID_EXTRACT_UNIT_CONVERTLET_ERROR, true);
-    f.checkFails("extract(dow from interval '2 3:4:5.678' day to second)",
-        INVALID_EXTRACT_UNIT_CONVERTLET_ERROR, true);
-    f.checkFails("extract(week from interval '2 3:4:5.678' day to second)",
-        INVALID_EXTRACT_UNIT_CONVERTLET_ERROR, true);
-    f.checkFails("extract(isodow from interval '2 3:4:5.678' day to second)",
-        INVALID_EXTRACT_UNIT_CONVERTLET_ERROR, true);
+    final String fail = "Cannot apply 'EXTRACT' to arguments of type 'EXTRACT\\(<.*> "
+        + "FROM <INTERVAL DAY TO SECOND>\\)'\\. Supported form\\(s\\): .*\\n.*\\n.*";
 
-    f.checkFails("^extract(month from interval '2 3:4:5.678' day to second)^",
-        "(?s)Cannot apply 'EXTRACT' to arguments of type 'EXTRACT\\(<INTERVAL "
-            + "MONTH> FROM <INTERVAL DAY TO SECOND>\\)'\\. Supported "
-            + "form\\(s\\):.*",
-        false);
-
-    f.checkFails("^extract(quarter from interval '2 3:4:5.678' day to second)^",
-        "(?s)Cannot apply 'EXTRACT' to arguments of type 'EXTRACT\\(<INTERVAL "
-            + "QUARTER> FROM <INTERVAL DAY TO SECOND>\\)'\\. Supported "
-            + "form\\(s\\):.*",
-        false);
-
-    f.checkFails("^extract(year from interval '2 3:4:5.678' day to second)^",
-        "(?s)Cannot apply 'EXTRACT' to arguments of type 'EXTRACT\\(<INTERVAL "
-            + "YEAR> FROM <INTERVAL DAY TO SECOND>\\)'\\. Supported "
-            + "form\\(s\\):.*",
-        false);
-
-    f.checkFails("^extract(isoyear from interval '2 3:4:5.678' day to second)^",
-        "(?s)Cannot apply 'EXTRACT' to arguments of type 'EXTRACT\\(<INTERVAL "
-            + "ISOYEAR> FROM <INTERVAL DAY TO SECOND>\\)'\\. Supported "
-            + "form\\(s\\):.*",
-        false);
-
-    f.checkFails("^extract(century from interval '2 3:4:5.678' day to second)^",
-        "(?s)Cannot apply 'EXTRACT' to arguments of type 'EXTRACT\\(<INTERVAL "
-            + "CENTURY> FROM <INTERVAL DAY TO SECOND>\\)'\\. Supported "
-            + "form\\(s\\):.*",
-        false);
+    f.checkFails("^extract(doy from interval '2 3:4:5.678' day to second)^", fail, false);
+    f.checkFails("^extract(dow from interval '2 3:4:5.678' day to second)^", fail, false);
+    f.checkFails("^extract(week from interval '2 3:4:5.678' day to second)^", fail, false);
+    f.checkFails("^extract(isodow from interval '2 3:4:5.678' day to second)^", fail, false);
+    f.checkFails("^extract(month from interval '2 3:4:5.678' day to second)^", fail, false);
+    f.checkFails("^extract(quarter from interval '2 3:4:5.678' day to second)^", fail, false);
+    f.checkFails("^extract(year from interval '2 3:4:5.678' day to second)^", fail, false);
+    f.checkFails("^extract(isoyear from interval '2 3:4:5.678' day to second)^", fail, false);
+    f.checkFails("^extract(century from interval '2 3:4:5.678' day to second)^", fail, false);
+    f.checkFails("^extract(millennium from interval '2 3:4:5.678' day to second)^", fail, false);
   }
 
   @Test void testExtractDate() {
@@ -10862,6 +10810,44 @@
         "3", "BIGINT NOT NULL");
   }
 
+  @Test void testExtractTime() {
+    final SqlOperatorFixture f = fixture();
+    f.setFor(SqlStdOperatorTable.EXTRACT, VM_FENNEL, VM_JAVA);
+
+    final String fail = "Cannot apply 'EXTRACT' to arguments of type 'EXTRACT\\(<.*> "
+        + "FROM <TIME\\(0\\)>\\)'\\. "
+        + "Supported form\\(s\\): .*\\n.*\\n.*";
+
+    f.checkFails("extract(^a^ from time '12:34:56')",
+        "'A' is not a valid time frame", false);
+    f.checkFails("^extract(epoch from time '12:34:56')^",
+        fail, false);
+
+    f.checkScalar("extract(second from time '12:34:56')",
+        "56", "BIGINT NOT NULL");
+    f.checkScalar("extract(millisecond from time '12:34:56')",
+        "56000", "BIGINT NOT NULL");
+    f.checkScalar("extract(microsecond from time '12:34:56')",
+        "56000000", "BIGINT NOT NULL");
+    f.checkScalar("extract(nanosecond from time '12:34:56')",
+        "56000000000", "BIGINT NOT NULL");
+    f.checkScalar("extract(minute from time '12:34:56')",
+        "34", "BIGINT NOT NULL");
+    f.checkScalar("extract(hour from time '12:34:56')",
+        "12", "BIGINT NOT NULL");
+    f.checkFails("^extract(day from time '12:34:56')^", fail, false);
+    f.checkFails("^extract(month from time '12:34:56')^", fail, false);
+    f.checkFails("^extract(quarter from time '12:34:56')^", fail, false);
+    f.checkFails("^extract(year from time '12:34:56')^", fail, false);
+    f.checkFails("^extract(isoyear from time '12:34:56')^", fail, false);
+    f.checkFails("^extract(doy from time '12:34:56')^", fail, false);
+    f.checkFails("^extract(dow from time '12:34:56')^", fail, false);
+    f.checkFails("^extract(week from time '12:34:56')^", fail, false);
+    f.checkFails("^extract(decade from time '12:34:56')^", fail, false);
+    f.checkFails("^extract(century from time '12:34:56')^", fail, false);
+    f.checkFails("^extract(millennium from time '12:34:56')^", fail, false);
+  }
+
   @Test void testExtractTimestamp() {
     final SqlOperatorFixture f = fixture();
     f.setFor(SqlStdOperatorTable.EXTRACT, VM_FENNEL, VM_JAVA);