[CALCITE-6138] Parser does not accept TIMESTAMP WITH TIME ZONE as a data type

Signed-off-by: Mihai Budiu <mbudiu@feldera.com>
diff --git a/cassandra/src/main/java/org/apache/calcite/adapter/cassandra/CassandraFilter.java b/cassandra/src/main/java/org/apache/calcite/adapter/cassandra/CassandraFilter.java
index aeb749a..117c79a 100644
--- a/cassandra/src/main/java/org/apache/calcite/adapter/cassandra/CassandraFilter.java
+++ b/cassandra/src/main/java/org/apache/calcite/adapter/cassandra/CassandraFilter.java
@@ -36,6 +36,7 @@
 import org.apache.calcite.sql.type.SqlTypeName;
 import org.apache.calcite.util.DateString;
 import org.apache.calcite.util.TimestampString;
+import org.apache.calcite.util.TimestampWithTimeZoneString;
 import org.apache.calcite.util.Util;
 
 import org.checkerframework.checker.nullness.qual.Nullable;
@@ -192,6 +193,9 @@
     private static Object literalValue(RexLiteral literal) {
       Comparable<?> value = RexLiteral.value(literal);
       switch (literal.getTypeName()) {
+      case TIMESTAMP_TZ:
+        assert value instanceof TimestampWithTimeZoneString;
+        return value.toString();
       case TIMESTAMP:
       case TIMESTAMP_WITH_LOCAL_TIME_ZONE:
         assert value instanceof TimestampString;
diff --git a/core/src/main/codegen/templates/Parser.jj b/core/src/main/codegen/templates/Parser.jj
index 78f9f59..542b164 100644
--- a/core/src/main/codegen/templates/Parser.jj
+++ b/core/src/main/codegen/templates/Parser.jj
@@ -4787,6 +4787,7 @@
 {
     final String p;
     final Span s;
+    boolean local = false;
 }
 {
     <LBRACE_D> <QUOTED_STRING> {
@@ -4818,6 +4819,7 @@
         return SqlLiteral.createUnknown("DATETIME", p, s.end(this));
     }
 |
+    LOOKAHEAD(2)
     <TIME> { s = span(); } p = SimpleStringLiteral() {
       return SqlLiteral.createUnknown("TIME", p, s.end(this));
     }
@@ -4827,8 +4829,14 @@
         return SqlLiteral.createUnknown("TIMESTAMP", p, s.end(this));
     }
 |
-    <TIMESTAMP> { s = span(); } <WITH> <LOCAL> <TIME> <ZONE> p = SimpleStringLiteral() {
-        return SqlLiteral.createUnknown("TIMESTAMP WITH LOCAL TIME ZONE", p, s.end(this));
+    LOOKAHEAD(2)
+    <TIME> { s = span(); } <WITH> ( <LOCAL> { local = true; } )? <TIME> <ZONE> p = SimpleStringLiteral() {
+        return SqlLiteral.createUnknown("TIME WITH " + (local ? "LOCAL " : "") + "TIME ZONE", p, s.end(this));
+    }
+|
+    LOOKAHEAD(2)
+    <TIMESTAMP> { s = span(); } <WITH> ( <LOCAL> { local = true; } )? <TIME> <ZONE> p = SimpleStringLiteral() {
+        return SqlLiteral.createUnknown("TIMESTAMP WITH " + (local ? "LOCAL " : "") + "TIME ZONE", p, s.end(this));
     }
 }
 
@@ -6112,7 +6120,6 @@
 {
     int precision = -1;
     SqlTypeName typeName;
-    boolean withLocalTimeZone = false;
     final Span s;
 }
 {
@@ -6124,25 +6131,15 @@
     LOOKAHEAD(2)
     <TIME> { s = span(); }
     precision = PrecisionOpt()
-    withLocalTimeZone = TimeZoneOpt()
+    typeName = TimeZoneOpt(true)
     {
-        if (withLocalTimeZone) {
-            typeName = SqlTypeName.TIME_WITH_LOCAL_TIME_ZONE;
-        } else {
-            typeName = SqlTypeName.TIME;
-        }
         return new SqlBasicTypeNameSpec(typeName, precision, s.end(this));
     }
 |
     <TIMESTAMP> { s = span(); }
     precision = PrecisionOpt()
-    withLocalTimeZone = TimeZoneOpt()
+    typeName = TimeZoneOpt(false)
     {
-        if (withLocalTimeZone) {
-            typeName = SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE;
-        } else {
-            typeName = SqlTypeName.TIMESTAMP;
-        }
         return new SqlBasicTypeNameSpec(typeName, precision, s.end(this));
     }
 }
@@ -6163,23 +6160,22 @@
 
 /**
 * Parse a time zone suffix for DateTime types. According to SQL-2011,
-* "with time zone" and "without time zone" belong to standard SQL but we
-* only implement the "without time zone".
-*
-* <p>We also support "with local time zone".
-*
-* @return true if this is "with local time zone".
+* "with time zone" and "without time zone" belong to standard SQL.
+* We also support "with local time zone".
 */
-boolean TimeZoneOpt() :
+SqlTypeName TimeZoneOpt(boolean timeType) :
 {
+    boolean local = false;
 }
 {
     LOOKAHEAD(3)
-    <WITHOUT> <TIME> <ZONE> { return false; }
+    <WITHOUT> <TIME> <ZONE> { return timeType ? SqlTypeName.TIME : SqlTypeName.TIMESTAMP;  }
 |
-    <WITH> <LOCAL> <TIME> <ZONE> { return true; }
+    <WITH> ( <LOCAL> { local = true; } )? <TIME> <ZONE> {
+        return timeType ? (local ? SqlTypeName.TIME_WITH_LOCAL_TIME_ZONE : SqlTypeName.TIME_TZ)
+                        : (local ? SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE : SqlTypeName.TIMESTAMP_TZ); }
 |
-    { return false; }
+    { return timeType ? SqlTypeName.TIME : SqlTypeName.TIMESTAMP; }
 }
 
 /**
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 e37120e..1bc88f1 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
@@ -2451,6 +2451,7 @@
       // Search does not include TZ so this conversion is okay
       case TIMESTAMP:
       case TIMESTAMP_WITH_LOCAL_TIME_ZONE:
+      case TIMESTAMP_TZ:
         expr = Expressions.call(BuiltInMethod.UNIX_TIMESTAMP_TO_STRING.method, expr);
         break;
       default:
@@ -2633,6 +2634,7 @@
                       translator.getRoot()));
           // fall through
         case TIMESTAMP:
+        case TIMESTAMP_TZ:
           type = long.class;
           floorMethod = custom ? customTimestampMethod : timestampMethod;
           preFloor = true;
@@ -2713,6 +2715,7 @@
       final Expression operand2 = argValueList.get(2);
       switch (call.getType().getSqlTypeName()) {
       case TIMESTAMP_WITH_LOCAL_TIME_ZONE:
+      case TIMESTAMP_TZ:
       case TIMESTAMP:
         return Expressions.call(customTimestampMethod, translator.getRoot(),
             operand0, operand1, operand2);
@@ -2745,6 +2748,7 @@
     private Method getMethod(RexCall call) {
       switch (call.operands.get(1).getType().getSqlTypeName()) {
       case TIMESTAMP_WITH_LOCAL_TIME_ZONE:
+      case TIMESTAMP_TZ:
       case TIMESTAMP:
         return customTimestampMethod;
       default:
@@ -3106,6 +3110,7 @@
                   Expressions.call(BuiltInMethod.TIME_ZONE.method,
                       translator.getRoot()));
           // fall through
+        case TIMESTAMP_TZ:
         case TIMESTAMP:
           operand =
               Expressions.call(BuiltInMethod.FLOOR_DIV.method, operand,
@@ -3136,6 +3141,7 @@
                   Expressions.constant(TimeUnit.DAY.multiplier.longValue()));
           // fall through
         case TIMESTAMP:
+        case TIMESTAMP_TZ:
           // convert to seconds
           return Expressions.divide(operand,
               Expressions.constant(TimeUnit.SECOND.multiplier.longValue()));
diff --git a/core/src/main/java/org/apache/calcite/jdbc/JavaTypeFactoryImpl.java b/core/src/main/java/org/apache/calcite/jdbc/JavaTypeFactoryImpl.java
index 05ed581..1b015fe 100644
--- a/core/src/main/java/org/apache/calcite/jdbc/JavaTypeFactoryImpl.java
+++ b/core/src/main/java/org/apache/calcite/jdbc/JavaTypeFactoryImpl.java
@@ -180,6 +180,7 @@
       case DATE:
       case TIME:
       case TIME_WITH_LOCAL_TIME_ZONE:
+      case TIME_TZ:
       case INTEGER:
       case INTERVAL_YEAR:
       case INTERVAL_YEAR_MONTH:
@@ -187,6 +188,7 @@
         return type.isNullable() ? Integer.class : int.class;
       case TIMESTAMP:
       case TIMESTAMP_WITH_LOCAL_TIME_ZONE:
+      case TIMESTAMP_TZ:
       case BIGINT:
       case INTERVAL_DAY:
       case INTERVAL_DAY_HOUR:
diff --git a/core/src/main/java/org/apache/calcite/rel/metadata/RelMdSize.java b/core/src/main/java/org/apache/calcite/rel/metadata/RelMdSize.java
index 903a65f..f2d2c6a 100644
--- a/core/src/main/java/org/apache/calcite/rel/metadata/RelMdSize.java
+++ b/core/src/main/java/org/apache/calcite/rel/metadata/RelMdSize.java
@@ -302,6 +302,7 @@
     case DATE:
     case TIME:
     case TIME_WITH_LOCAL_TIME_ZONE:
+    case TIME_TZ:
     case INTERVAL_YEAR:
     case INTERVAL_YEAR_MONTH:
     case INTERVAL_MONTH:
@@ -311,6 +312,7 @@
     case FLOAT: // sic
     case TIMESTAMP:
     case TIMESTAMP_WITH_LOCAL_TIME_ZONE:
+    case TIMESTAMP_TZ:
     case INTERVAL_DAY:
     case INTERVAL_DAY_HOUR:
     case INTERVAL_DAY_MINUTE:
@@ -364,6 +366,7 @@
     case DATE:
     case TIME:
     case TIME_WITH_LOCAL_TIME_ZONE:
+    case TIME_TZ:
     case INTERVAL_YEAR:
     case INTERVAL_YEAR_MONTH:
     case INTERVAL_MONTH:
@@ -373,6 +376,7 @@
     case DOUBLE:
     case TIMESTAMP:
     case TIMESTAMP_WITH_LOCAL_TIME_ZONE:
+    case TIMESTAMP_TZ:
     case INTERVAL_DAY:
     case INTERVAL_DAY_HOUR:
     case INTERVAL_DAY_MINUTE:
diff --git a/core/src/main/java/org/apache/calcite/rel/rules/DateRangeRules.java b/core/src/main/java/org/apache/calcite/rel/rules/DateRangeRules.java
index 9c51470..d720624 100644
--- a/core/src/main/java/org/apache/calcite/rel/rules/DateRangeRules.java
+++ b/core/src/main/java/org/apache/calcite/rel/rules/DateRangeRules.java
@@ -575,7 +575,14 @@
         ts = TimestampString.fromCalendarFields(calendar);
         p = operand.getType().getPrecision();
         return rexBuilder.makeTimestampLiteral(ts, p);
-      case TIMESTAMP_WITH_LOCAL_TIME_ZONE:
+      case TIMESTAMP_TZ: {
+        ts = TimestampString.fromCalendarFields(calendar);
+        final TimeZone tz = calendar.getTimeZone();
+        final TimestampWithTimeZoneString localTs = new TimestampWithTimeZoneString(ts, tz);
+        p = operand.getType().getPrecision();
+        return rexBuilder.makeTimestampTzLiteral(localTs, p);
+      }
+      case TIMESTAMP_WITH_LOCAL_TIME_ZONE: {
         ts = TimestampString.fromCalendarFields(calendar);
         final TimeZone tz = TimeZone.getTimeZone(this.timeZone);
         final TimestampString localTs =
@@ -584,6 +591,7 @@
                 .getLocalTimestampString();
         p = operand.getType().getPrecision();
         return rexBuilder.makeTimestampWithLocalTimeZoneLiteral(localTs, p);
+      }
       case DATE:
         final DateString d = DateString.fromCalendarFields(calendar);
         return rexBuilder.makeDateLiteral(d);
@@ -646,6 +654,12 @@
 
     private Calendar timestampValue(RexLiteral timeLiteral) {
       switch (timeLiteral.getTypeName()) {
+      case TIMESTAMP_TZ:
+        TimestampWithTimeZoneString value =
+            requireNonNull(timeLiteral.getValueAs(TimestampWithTimeZoneString.class));
+        return Util.calendar(
+            value.getLocalTimestampString().getMillisSinceEpoch(),
+            value.getTimeZone());
       case TIMESTAMP_WITH_LOCAL_TIME_ZONE:
         final TimeZone tz = TimeZone.getTimeZone(this.timeZone);
         return Util.calendar(
diff --git a/core/src/main/java/org/apache/calcite/rel/rules/materialize/MaterializedViewAggregateRule.java b/core/src/main/java/org/apache/calcite/rel/rules/materialize/MaterializedViewAggregateRule.java
index 289c088..579efc333 100644
--- a/core/src/main/java/org/apache/calcite/rel/rules/materialize/MaterializedViewAggregateRule.java
+++ b/core/src/main/java/org/apache/calcite/rel/rules/materialize/MaterializedViewAggregateRule.java
@@ -775,7 +775,8 @@
       exprsLineage.put(expr, i);
       SqlTypeName sqlTypeName = expr.getType().getSqlTypeName();
       if (sqlTypeName == SqlTypeName.TIMESTAMP
-          || sqlTypeName == SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE) {
+          || sqlTypeName == SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE
+          || sqlTypeName == SqlTypeName.TIMESTAMP_TZ) {
         timestampExprs.add(expr);
       }
     }
diff --git a/core/src/main/java/org/apache/calcite/rel/type/RelDataTypeSystemImpl.java b/core/src/main/java/org/apache/calcite/rel/type/RelDataTypeSystemImpl.java
index 55702ce..d4a9cac 100644
--- a/core/src/main/java/org/apache/calcite/rel/type/RelDataTypeSystemImpl.java
+++ b/core/src/main/java/org/apache/calcite/rel/type/RelDataTypeSystemImpl.java
@@ -101,10 +101,12 @@
       return 15;
     case TIME:
     case TIME_WITH_LOCAL_TIME_ZONE:
+    case TIME_TZ:
     case DATE:
       return 0; // SQL99 part 2 section 6.1 syntax rule 30
     case TIMESTAMP:
     case TIMESTAMP_WITH_LOCAL_TIME_ZONE:
+    case TIMESTAMP_TZ:
       // farrago supports only 0 (see
       // SqlTypeName.getDefaultPrecision), but it should be 6
       // (microseconds) per SQL99 part 2 section 6.1 syntax rule 30.
@@ -126,8 +128,10 @@
       return 65536;
     case TIME:
     case TIME_WITH_LOCAL_TIME_ZONE:
+    case TIME_TZ:
     case TIMESTAMP:
     case TIMESTAMP_WITH_LOCAL_TIME_ZONE:
+    case TIMESTAMP_TZ:
       return SqlTypeName.MAX_DATETIME_PRECISION;
     case INTERVAL_YEAR:
     case INTERVAL_YEAR_MONTH:
@@ -168,6 +172,8 @@
       return isPrefix ? "TIMESTAMP '" : "'";
     case TIMESTAMP_WITH_LOCAL_TIME_ZONE:
       return isPrefix ? "TIMESTAMP WITH LOCAL TIME ZONE '" : "'";
+    case TIMESTAMP_TZ:
+      return isPrefix ? "TIMESTAMP WITH TIME ZONE '" : "'";
     case INTERVAL_DAY:
     case INTERVAL_DAY_HOUR:
     case INTERVAL_DAY_MINUTE:
@@ -187,6 +193,8 @@
       return isPrefix ? "TIME '" : "'";
     case TIME_WITH_LOCAL_TIME_ZONE:
       return isPrefix ? "TIME WITH LOCAL TIME ZONE '" : "'";
+    case TIME_TZ:
+      return isPrefix ? "TIME WITH TIME ZONE '" : "'";
     case DATE:
       return isPrefix ? "DATE '" : "'";
     case ARRAY:
diff --git a/core/src/main/java/org/apache/calcite/rex/RexBuilder.java b/core/src/main/java/org/apache/calcite/rex/RexBuilder.java
index dab524f..ecbe67e 100644
--- a/core/src/main/java/org/apache/calcite/rex/RexBuilder.java
+++ b/core/src/main/java/org/apache/calcite/rex/RexBuilder.java
@@ -48,7 +48,9 @@
 import org.apache.calcite.util.Pair;
 import org.apache.calcite.util.Sarg;
 import org.apache.calcite.util.TimeString;
+import org.apache.calcite.util.TimeWithTimeZoneString;
 import org.apache.calcite.util.TimestampString;
+import org.apache.calcite.util.TimestampWithTimeZoneString;
 import org.apache.calcite.util.Util;
 
 import com.google.common.collect.ImmutableList;
@@ -1015,6 +1017,14 @@
       }
       o = ((TimeString) o).round(p);
       break;
+    case TIME_TZ:
+      assert o instanceof TimeWithTimeZoneString;
+      p = type.getPrecision();
+      if (p == RelDataType.PRECISION_NOT_SPECIFIED) {
+        p = 0;
+      }
+      o = ((TimeWithTimeZoneString) o).round(p);
+      break;
     case TIMESTAMP:
     case TIMESTAMP_WITH_LOCAL_TIME_ZONE:
       assert o instanceof TimestampString;
@@ -1024,6 +1034,14 @@
       }
       o = ((TimestampString) o).round(p);
       break;
+    case TIMESTAMP_TZ:
+      assert o instanceof TimestampWithTimeZoneString;
+      p = type.getPrecision();
+      if (p == RelDataType.PRECISION_NOT_SPECIFIED) {
+        p = 0;
+      }
+      o = ((TimestampWithTimeZoneString) o).round(p);
+      break;
     default:
       break;
     }
@@ -1270,6 +1288,16 @@
         SqlTypeName.TIME_WITH_LOCAL_TIME_ZONE);
   }
 
+  /** Creates a Time with time-zone literal. */
+  public RexLiteral makeTimeTzLiteral(
+      TimeWithTimeZoneString time,
+      int precision) {
+    return makeLiteral(
+        requireNonNull(time, "time"),
+        typeFactory.createSqlType(SqlTypeName.TIME_TZ, precision),
+        SqlTypeName.TIME_TZ);
+  }
+
   // CHECKSTYLE: IGNORE 1
   /** @deprecated Use {@link #makeTimestampLiteral(TimestampString, int)}. */
   @Deprecated // to be removed before 2.0
@@ -1301,6 +1329,15 @@
         SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE);
   }
 
+  public RexLiteral makeTimestampTzLiteral(
+      TimestampWithTimeZoneString timestamp,
+      int precision) {
+    return makeLiteral(
+        requireNonNull(timestamp, "timestamp"),
+        typeFactory.createSqlType(SqlTypeName.TIMESTAMP_TZ, precision),
+        SqlTypeName.TIMESTAMP_TZ);
+  }
+
   /**
    * Creates a literal representing an interval type, for example
    * {@code YEAR TO MONTH} or {@code DOW}.
@@ -1544,8 +1581,12 @@
       return DateTimeUtils.ZERO_CALENDAR;
     case TIME_WITH_LOCAL_TIME_ZONE:
       return new TimeString(0, 0, 0);
+    case TIME_TZ:
+      return new TimeWithTimeZoneString(0, 0, 0, "GMT+00");
     case TIMESTAMP_WITH_LOCAL_TIME_ZONE:
       return new TimestampString(0, 1, 1, 0, 0, 0);
+    case TIMESTAMP_TZ:
+      return new TimestampWithTimeZoneString(0, 1, 1, 0, 0, 0, "GMT+00");
     default:
       throw Util.unexpected(type.getSqlTypeName());
     }
@@ -1662,12 +1703,17 @@
       return makeTimeLiteral((TimeString) value, type.getPrecision());
     case TIME_WITH_LOCAL_TIME_ZONE:
       return makeTimeWithLocalTimeZoneLiteral((TimeString) value, type.getPrecision());
+    case TIME_TZ:
+      return makeTimeTzLiteral((TimeWithTimeZoneString) value, type.getPrecision());
     case DATE:
       return makeDateLiteral((DateString) value);
     case TIMESTAMP:
       return makeTimestampLiteral((TimestampString) value, type.getPrecision());
     case TIMESTAMP_WITH_LOCAL_TIME_ZONE:
       return makeTimestampWithLocalTimeZoneLiteral((TimestampString) value, type.getPrecision());
+    case TIMESTAMP_TZ:
+      return makeTimestampTzLiteral(
+          (TimestampWithTimeZoneString) value, type.getPrecision());
     case INTERVAL_YEAR:
     case INTERVAL_YEAR_MONTH:
     case INTERVAL_MONTH:
@@ -1827,6 +1873,14 @@
       } else {
         return TimeString.fromMillisOfDay((Integer) o);
       }
+    case TIME_TZ:
+      if (o instanceof TimeWithTimeZoneString) {
+        return o;
+      } else if (o instanceof Calendar) {
+        return TimeWithTimeZoneString.fromCalendarFields((Calendar) o);
+      } else {
+        throw new AssertionError("Value does not contain time zone");
+      }
     case TIME_WITH_LOCAL_TIME_ZONE:
       if (o instanceof TimeString) {
         return o;
@@ -1861,6 +1915,14 @@
       } else {
         return TimestampString.fromMillisSinceEpoch((Long) o);
       }
+    case TIMESTAMP_TZ:
+      if (o instanceof TimestampWithTimeZoneString) {
+        return o;
+      } else if (o instanceof Calendar) {
+        return TimestampWithTimeZoneString.fromCalendarFields((Calendar) o);
+      } else {
+        throw new AssertionError("Value does not contain time zone");
+      }
     default:
       return o;
     }
diff --git a/core/src/main/java/org/apache/calcite/rex/RexLiteral.java b/core/src/main/java/org/apache/calcite/rex/RexLiteral.java
index 30f0366..1e8cd76 100644
--- a/core/src/main/java/org/apache/calcite/rex/RexLiteral.java
+++ b/core/src/main/java/org/apache/calcite/rex/RexLiteral.java
@@ -39,7 +39,9 @@
 import org.apache.calcite.util.NlsString;
 import org.apache.calcite.util.Sarg;
 import org.apache.calcite.util.TimeString;
+import org.apache.calcite.util.TimeWithTimeZoneString;
 import org.apache.calcite.util.TimestampString;
+import org.apache.calcite.util.TimestampWithTimeZoneString;
 import org.apache.calcite.util.Util;
 
 import com.google.common.collect.ImmutableList;
@@ -334,13 +336,15 @@
     case DATE:
       return value instanceof DateString;
     case TIME:
-      return value instanceof TimeString;
     case TIME_WITH_LOCAL_TIME_ZONE:
       return value instanceof TimeString;
+    case TIME_TZ:
+      return value instanceof TimeWithTimeZoneString;
     case TIMESTAMP:
-      return value instanceof TimestampString;
     case TIMESTAMP_WITH_LOCAL_TIME_ZONE:
       return value instanceof TimestampString;
+    case TIMESTAMP_TZ:
+      return value instanceof TimestampWithTimeZoneString;
     case INTERVAL_YEAR:
     case INTERVAL_YEAR_MONTH:
     case INTERVAL_MONTH:
@@ -696,11 +700,19 @@
       assert value instanceof TimeString;
       sb.append(value.toString());
       break;
+    case TIME_TZ:
+      assert value instanceof TimeWithTimeZoneString;
+      sb.append(value);
+      break;
     case TIMESTAMP:
     case TIMESTAMP_WITH_LOCAL_TIME_ZONE:
       assert value instanceof TimestampString;
       sb.append(value.toString());
       break;
+    case TIMESTAMP_TZ:
+      assert value instanceof TimestampWithTimeZoneString;
+      sb.append(value);
+      break;
     case INTERVAL_YEAR:
     case INTERVAL_YEAR_MONTH:
     case INTERVAL_MONTH:
@@ -958,10 +970,12 @@
     case DECIMAL:
     case TIMESTAMP:
     case TIMESTAMP_WITH_LOCAL_TIME_ZONE:
+    case TIMESTAMP_TZ:
       return getValueAs(Long.class);
     case DATE:
     case TIME:
     case TIME_WITH_LOCAL_TIME_ZONE:
+    case TIME_TZ:
       return getValueAs(Integer.class);
     default:
       return value;
@@ -1100,6 +1114,11 @@
         return clazz.cast(((TimeString) value).getMillisOfDay());
       }
       break;
+    case TIME_TZ:
+      if (clazz == Integer.class) {
+        return clazz.cast(((TimeWithTimeZoneString) value).getLocalTimeString().getMillisOfDay());
+      }
+      break;
     case TIMESTAMP:
       if (clazz == Long.class) {
         // Milliseconds since 1970-01-01 00:00:00
@@ -1109,6 +1128,16 @@
         return clazz.cast(((TimestampString) value).toCalendar());
       }
       break;
+    case TIMESTAMP_TZ:
+      if (clazz == Long.class) {
+        return clazz.cast(((TimestampWithTimeZoneString) value)
+            .getLocalTimestampString()
+            .getMillisSinceEpoch());
+      } else if (clazz == Calendar.class) {
+        TimestampWithTimeZoneString ts = (TimestampWithTimeZoneString) value;
+        return clazz.cast(ts.getLocalTimestampString().toCalendar(ts.getTimeZone()));
+      }
+      break;
     case TIMESTAMP_WITH_LOCAL_TIME_ZONE:
       if (clazz == Long.class) {
         // Milliseconds since 1970-01-01 00:00:00
diff --git a/core/src/main/java/org/apache/calcite/sql/SqlAbstractDateTimeLiteral.java b/core/src/main/java/org/apache/calcite/sql/SqlAbstractDateTimeLiteral.java
index 7706c0b..8d97af0 100644
--- a/core/src/main/java/org/apache/calcite/sql/SqlAbstractDateTimeLiteral.java
+++ b/core/src/main/java/org/apache/calcite/sql/SqlAbstractDateTimeLiteral.java
@@ -56,7 +56,7 @@
   //~ Methods ----------------------------------------------------------------
 
   /** Converts this literal to a {@link TimestampString}. */
-  protected TimestampString getTimestamp() {
+  public TimestampString getTimestamp() {
     return (TimestampString) requireNonNull(value, "value");
   }
 
diff --git a/core/src/main/java/org/apache/calcite/sql/SqlBasicTypeNameSpec.java b/core/src/main/java/org/apache/calcite/sql/SqlBasicTypeNameSpec.java
index 547bdfb..9b0434d 100644
--- a/core/src/main/java/org/apache/calcite/sql/SqlBasicTypeNameSpec.java
+++ b/core/src/main/java/org/apache/calcite/sql/SqlBasicTypeNameSpec.java
@@ -158,9 +158,9 @@
     // instead of direct unparsing with enum name.
     // i.e. TIME_WITH_LOCAL_TIME_ZONE(3)
     // would be unparsed as "time(3) with local time zone".
-    final boolean isWithLocalTimeZone = isWithLocalTimeZoneDef(sqlTypeName);
-    if (isWithLocalTimeZone) {
-      writer.keyword(stripLocalTimeZoneDef(sqlTypeName).name());
+    final boolean isWithTimeZone = isWithTimeZoneDef(sqlTypeName);
+    if (isWithTimeZone) {
+      writer.keyword(stripTimeZoneDef(sqlTypeName).name());
     } else {
       writer.keyword(getTypeName().getSimple());
     }
@@ -176,8 +176,12 @@
       writer.endList(frame);
     }
 
-    if (isWithLocalTimeZone) {
-      writer.keyword("WITH LOCAL TIME ZONE");
+    if (isWithTimeZone) {
+      writer.keyword("WITH");
+      if (isWithLocalTimeZoneDef(sqlTypeName)) {
+        writer.keyword("LOCAL");
+      }
+      writer.keyword("TIME ZONE");
     }
 
     if (writer.getDialect().supportsCharSet() && charSetName != null) {
@@ -245,16 +249,23 @@
     }
   }
 
+  private static boolean isWithTimeZoneDef(SqlTypeName typeName) {
+    return SqlTypeName.TZ_TYPES.contains(typeName);
+  }
+
   /**
-   * Remove the local time zone definition of the {@code typeName}.
+   * Remove the local time zone definition
+   * or the time zone definition of the {@code typeName}.
    *
    * @param typeName Type name
-   * @return new type name without local time zone definition
+   * @return new type name without (local) time zone definition
    */
-  private static SqlTypeName stripLocalTimeZoneDef(SqlTypeName typeName) {
+  private static SqlTypeName stripTimeZoneDef(SqlTypeName typeName) {
     switch (typeName) {
+    case TIME_TZ:
     case TIME_WITH_LOCAL_TIME_ZONE:
       return SqlTypeName.TIME;
+    case TIMESTAMP_TZ:
     case TIMESTAMP_WITH_LOCAL_TIME_ZONE:
       return SqlTypeName.TIMESTAMP;
     default:
diff --git a/core/src/main/java/org/apache/calcite/sql/SqlJdbcDataTypeName.java b/core/src/main/java/org/apache/calcite/sql/SqlJdbcDataTypeName.java
index eaf47de..7046fee 100644
--- a/core/src/main/java/org/apache/calcite/sql/SqlJdbcDataTypeName.java
+++ b/core/src/main/java/org/apache/calcite/sql/SqlJdbcDataTypeName.java
@@ -37,8 +37,10 @@
   SQL_DATE(SqlTypeName.DATE),
   SQL_TIME(SqlTypeName.TIME),
   SQL_TIME_WITH_LOCAL_TIME_ZONE(SqlTypeName.TIME_WITH_LOCAL_TIME_ZONE),
+  SQL_TIME_WITH_TIME_ZONE(SqlTypeName.TIME_TZ),
   SQL_TIMESTAMP(SqlTypeName.TIMESTAMP),
   SQL_TIMESTAMP_WITH_LOCAL_TIME_ZONE(SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE),
+  SQL_TIMESTAMP_WITH_TIME_ZONE(SqlTypeName.TIMESTAMP_TZ),
   SQL_DECIMAL(SqlTypeName.DECIMAL),
   SQL_NUMERIC(SqlTypeName.DECIMAL),
   SQL_BOOLEAN(SqlTypeName.BOOLEAN),
diff --git a/core/src/main/java/org/apache/calcite/sql/SqlLiteral.java b/core/src/main/java/org/apache/calcite/sql/SqlLiteral.java
index 67c5d00..ae41cbe 100644
--- a/core/src/main/java/org/apache/calcite/sql/SqlLiteral.java
+++ b/core/src/main/java/org/apache/calcite/sql/SqlLiteral.java
@@ -37,7 +37,9 @@
 import org.apache.calcite.util.Litmus;
 import org.apache.calcite.util.NlsString;
 import org.apache.calcite.util.TimeString;
+import org.apache.calcite.util.TimeWithTimeZoneString;
 import org.apache.calcite.util.TimestampString;
+import org.apache.calcite.util.TimestampWithTimeZoneString;
 import org.apache.calcite.util.Util;
 
 import org.checkerframework.checker.nullness.qual.Nullable;
@@ -205,10 +207,15 @@
     case DATE:
       return value instanceof DateString;
     case TIME:
+    case TIME_WITH_LOCAL_TIME_ZONE:
       return value instanceof TimeString;
+    case TIME_TZ:
+      return value instanceof TimeWithTimeZoneString;
     case TIMESTAMP:
     case TIMESTAMP_WITH_LOCAL_TIME_ZONE:
       return value instanceof TimestampString;
+    case TIMESTAMP_TZ:
+      return value instanceof TimestampWithTimeZoneString;
     case INTERVAL_YEAR:
     case INTERVAL_YEAR_MONTH:
     case INTERVAL_MONTH:
@@ -893,6 +900,14 @@
     return new SqlTimestampLiteral(ts, precision, typeName, pos);
   }
 
+  /** Creates a TIMESTAMP WITH TIME ZONE literal. */
+  public static SqlTimestampTzLiteral createTimestamp(
+      TimestampWithTimeZoneString ts,
+      int precision,
+      SqlParserPos pos) {
+    return new SqlTimestampTzLiteral(ts, precision, pos);
+  }
+
   @Deprecated // to be removed before 2.0
   public static SqlTimeLiteral createTime(
       Calendar calendar,
@@ -908,6 +923,12 @@
     return new SqlTimeLiteral(t, precision, false, pos);
   }
 
+  public static SqlTimeTzLiteral createTime(
+      TimeWithTimeZoneString t,
+      int precision,
+      SqlParserPos pos) {
+    return new SqlTimeTzLiteral(t, precision, pos);
+  }
   /**
    * Creates an interval literal.
    *
diff --git a/core/src/main/java/org/apache/calcite/sql/SqlTimeTzLiteral.java b/core/src/main/java/org/apache/calcite/sql/SqlTimeTzLiteral.java
new file mode 100644
index 0000000..19dd05f
--- /dev/null
+++ b/core/src/main/java/org/apache/calcite/sql/SqlTimeTzLiteral.java
@@ -0,0 +1,70 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.calcite.sql;
+
+import org.apache.calcite.sql.parser.SqlParserPos;
+import org.apache.calcite.sql.type.SqlTypeName;
+import org.apache.calcite.util.TimeWithTimeZoneString;
+
+import com.google.common.base.Preconditions;
+
+import java.util.Objects;
+
+/**
+ * A SQL literal representing a TIME WITH TIME ZONE value, for example <code>TIME WITH TIME ZONE
+ * '14:33:44.567 GMT+08'</code>.
+ *
+ * <p>Create values using {@link SqlLiteral#createTime}.
+ */
+public class SqlTimeTzLiteral extends SqlAbstractDateTimeLiteral {
+  //~ Constructors -----------------------------------------------------------
+
+  SqlTimeTzLiteral(TimeWithTimeZoneString t, int precision,
+                             SqlParserPos pos) {
+    super(t, true, SqlTypeName.TIME_TZ, precision, pos);
+    Preconditions.checkArgument(this.precision >= 0);
+  }
+
+  //~ Methods ----------------------------------------------------------------
+
+  /** Converts this literal to a {@link TimeWithTimeZoneString}. */
+  protected TimeWithTimeZoneString getTime() {
+    return (TimeWithTimeZoneString) Objects.requireNonNull(value, "value");
+  }
+
+  @Override public SqlTimeTzLiteral clone(SqlParserPos pos) {
+    return new SqlTimeTzLiteral(getTime(), precision, pos);
+  }
+
+  @Override public String toString() {
+    return "TIME WITH TIME ZONE '" + toFormattedString() + "'";
+  }
+
+  /**
+   * Returns e.g. '03:05:67.456 GMT+00:00'.
+   */
+  @Override public String toFormattedString() {
+    return getTime().toString(precision);
+  }
+
+  @Override public void unparse(
+      SqlWriter writer,
+      int leftPrec,
+      int rightPrec) {
+    writer.getDialect().unparseDateTimeLiteral(writer, this, leftPrec, rightPrec);
+  }
+}
diff --git a/core/src/main/java/org/apache/calcite/sql/SqlTimestampLiteral.java b/core/src/main/java/org/apache/calcite/sql/SqlTimestampLiteral.java
index d70b677..8d1e6ec 100644
--- a/core/src/main/java/org/apache/calcite/sql/SqlTimestampLiteral.java
+++ b/core/src/main/java/org/apache/calcite/sql/SqlTimestampLiteral.java
@@ -26,7 +26,7 @@
 
 /**
  * A SQL literal representing a TIMESTAMP value, for example <code>TIMESTAMP
- * '1969-07-21 03:15 GMT'</code>.
+ * '1969-07-21 03:15'</code>.
  *
  * <p>Create values using {@link SqlLiteral#createTimestamp}.
  */
diff --git a/core/src/main/java/org/apache/calcite/sql/SqlTimestampTzLiteral.java b/core/src/main/java/org/apache/calcite/sql/SqlTimestampTzLiteral.java
new file mode 100644
index 0000000..0e79038
--- /dev/null
+++ b/core/src/main/java/org/apache/calcite/sql/SqlTimestampTzLiteral.java
@@ -0,0 +1,70 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.calcite.sql;
+
+import org.apache.calcite.sql.parser.SqlParserPos;
+import org.apache.calcite.sql.type.SqlTypeName;
+import org.apache.calcite.util.TimestampWithTimeZoneString;
+
+import com.google.common.base.Preconditions;
+
+import java.util.Objects;
+
+/**
+ * A SQL literal representing a TIMESTAMP WITH TIME ZONE value, for example <code>TIMESTAMP
+ * '1969-07-21 03:15 GMT+00:00'</code>.
+ *
+ * <p>Create values using {@link SqlLiteral#createTimestamp}.
+ */
+public class SqlTimestampTzLiteral extends SqlAbstractDateTimeLiteral {
+  //~ Constructors -----------------------------------------------------------
+
+  SqlTimestampTzLiteral(TimestampWithTimeZoneString ts, int precision, SqlParserPos pos) {
+    super(ts, false, SqlTypeName.TIMESTAMP_TZ, precision, pos);
+    Preconditions.checkArgument(this.precision >= 0);
+  }
+
+  //~ Methods ----------------------------------------------------------------
+
+  @Override public SqlTimestampTzLiteral clone(SqlParserPos pos) {
+    return new SqlTimestampTzLiteral(
+        getTimestampTz(), precision, pos);
+  }
+
+  @Override public String toString() {
+    return getTypeName() + " '" + toFormattedString() + "'";
+  }
+
+  @Override public String toFormattedString() {
+    TimestampWithTimeZoneString ts = getTimestampTz();
+    if (precision > 0) {
+      ts = ts.round(precision);
+    }
+    return ts.toString(precision);
+  }
+
+  TimestampWithTimeZoneString getTimestampTz() {
+    return (TimestampWithTimeZoneString) Objects.requireNonNull(value, "value");
+  }
+
+  @Override public void unparse(
+      SqlWriter writer,
+      int leftPrec,
+      int rightPrec) {
+    writer.getDialect().unparseDateTimeLiteral(writer, this, leftPrec, rightPrec);
+  }
+}
diff --git a/core/src/main/java/org/apache/calcite/sql/SqlUnknownLiteral.java b/core/src/main/java/org/apache/calcite/sql/SqlUnknownLiteral.java
index a6cbbbb..a0a811e 100644
--- a/core/src/main/java/org/apache/calcite/sql/SqlUnknownLiteral.java
+++ b/core/src/main/java/org/apache/calcite/sql/SqlUnknownLiteral.java
@@ -57,10 +57,14 @@
       return SqlParserUtil.parseDateLiteral(getValue(), pos);
     case TIME:
       return SqlParserUtil.parseTimeLiteral(getValue(), pos);
+    case TIME_TZ:
+      return SqlParserUtil.parseTimeTzLiteral(getValue(), pos);
     case TIMESTAMP:
       return SqlParserUtil.parseTimestampLiteral(getValue(), pos);
     case TIMESTAMP_WITH_LOCAL_TIME_ZONE:
       return SqlParserUtil.parseTimestampWithLocalTimeZoneLiteral(getValue(), pos);
+    case TIMESTAMP_TZ:
+      return SqlParserUtil.parseTimestampTzLiteral(getValue(), pos);
     default:
       throw Util.unexpected(typeName);
     }
diff --git a/core/src/main/java/org/apache/calcite/sql/parser/SqlParserUtil.java b/core/src/main/java/org/apache/calcite/sql/parser/SqlParserUtil.java
index ca3c0a7..44fecaa 100644
--- a/core/src/main/java/org/apache/calcite/sql/parser/SqlParserUtil.java
+++ b/core/src/main/java/org/apache/calcite/sql/parser/SqlParserUtil.java
@@ -36,7 +36,9 @@
 import org.apache.calcite.sql.SqlPrefixOperator;
 import org.apache.calcite.sql.SqlSpecialOperator;
 import org.apache.calcite.sql.SqlTimeLiteral;
+import org.apache.calcite.sql.SqlTimeTzLiteral;
 import org.apache.calcite.sql.SqlTimestampLiteral;
+import org.apache.calcite.sql.SqlTimestampTzLiteral;
 import org.apache.calcite.sql.SqlUtil;
 import org.apache.calcite.sql.fun.SqlStdOperatorTable;
 import org.apache.calcite.sql.parser.impl.SqlParserImpl;
@@ -44,7 +46,9 @@
 import org.apache.calcite.util.DateString;
 import org.apache.calcite.util.PrecedenceClimbingParser;
 import org.apache.calcite.util.TimeString;
+import org.apache.calcite.util.TimeWithTimeZoneString;
 import org.apache.calcite.util.TimestampString;
+import org.apache.calcite.util.TimestampWithTimeZoneString;
 import org.apache.calcite.util.Util;
 import org.apache.calcite.util.trace.CalciteTrace;
 
@@ -65,6 +69,7 @@
 import java.util.List;
 import java.util.Locale;
 import java.util.StringTokenizer;
+import java.util.TimeZone;
 import java.util.function.Predicate;
 import java.util.regex.Pattern;
 
@@ -352,6 +357,31 @@
     return SqlLiteral.createTime(t, pt.getPrecision(), pos);
   }
 
+  public static SqlTimeTzLiteral parseTimeTzLiteral(
+      String s, SqlParserPos pos) {
+    // We expect the string to end in a timezone.
+    final int lastSpace = s.lastIndexOf(" ");
+    DateTimeUtils.PrecisionTime pt = null;
+    if (lastSpace >= 0) {
+      final String timeZone = s.substring(lastSpace + 1);
+      final String time = s.substring(0, lastSpace);
+
+      final TimeZone tz = TimeZone.getTimeZone(timeZone);
+      if (tz != null) {
+        pt =
+            DateTimeUtils.parsePrecisionDateTimeLiteral(time, Format.get().time, tz, -1);
+      }
+    }
+    if (pt == null) {
+      throw SqlUtil.newContextException(pos,
+          RESOURCE.illegalLiteral("TIME WITH TIME ZONE", s,
+              RESOURCE.badFormat(DateTimeUtils.TIME_FORMAT_STRING).str()));
+    }
+    final TimeWithTimeZoneString t = TimeWithTimeZoneString.fromCalendarFields(pt.getCalendar())
+        .withFraction(pt.getFraction());
+    return SqlLiteral.createTime(t, pt.getPrecision(), pos);
+  }
+
   public static SqlTimestampLiteral parseTimestampLiteral(String s,
       SqlParserPos pos) {
     return parseTimestampLiteral(SqlTypeName.TIMESTAMP, s, pos);
@@ -363,6 +393,25 @@
         pos);
   }
 
+  public static SqlTimestampTzLiteral parseTimestampTzLiteral(
+      String s, SqlParserPos pos) {
+    // We expect the string to end in a timezone.
+    int lastSpace = s.lastIndexOf(" ");
+    if (lastSpace >= 0) {
+      final String timeZone = s.substring(lastSpace + 1);
+      final String timestamp = s.substring(0, lastSpace);
+      TimeZone tz = TimeZone.getTimeZone(timeZone);
+      if (tz != null) {
+        SqlTimestampLiteral ts = parseTimestampLiteral(SqlTypeName.TIMESTAMP, timestamp, pos);
+        TimestampWithTimeZoneString tsz = new TimestampWithTimeZoneString(ts.getTimestamp(), tz);
+        return SqlLiteral.createTimestamp(tsz, ts.getPrec(), pos);
+      }
+    }
+    throw SqlUtil.newContextException(pos,
+        RESOURCE.illegalLiteral("TIMESTAMP WITH TIME ZONE", s,
+            RESOURCE.badFormat(DateTimeUtils.TIMESTAMP_FORMAT_STRING).str()));
+  }
+
   private static SqlTimestampLiteral parseTimestampLiteral(SqlTypeName typeName,
       String s, SqlParserPos pos) {
     final Format format = Format.get();
diff --git a/core/src/main/java/org/apache/calcite/sql/type/ReturnTypes.java b/core/src/main/java/org/apache/calcite/sql/type/ReturnTypes.java
index 342284d..386ed96 100644
--- a/core/src/main/java/org/apache/calcite/sql/type/ReturnTypes.java
+++ b/core/src/main/java/org/apache/calcite/sql/type/ReturnTypes.java
@@ -377,6 +377,13 @@
       explicit(SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE);
 
   /**
+   * Type-inference strategy whereby the result type of a call is TIMESTAMP
+   * WITH TIME ZONE.
+   */
+  public static final SqlReturnTypeInference TIMESTAMP_TZ =
+      explicit(SqlTypeName.TIMESTAMP_TZ);
+
+  /**
    * Type-inference strategy whereby the result type of a call is nullable
    * TIMESTAMP WITH LOCAL TIME ZONE.
    */
diff --git a/core/src/main/java/org/apache/calcite/sql/type/SqlTypeAssignmentRule.java b/core/src/main/java/org/apache/calcite/sql/type/SqlTypeAssignmentRule.java
index 6167c4d..c77f0b1 100644
--- a/core/src/main/java/org/apache/calcite/sql/type/SqlTypeAssignmentRule.java
+++ b/core/src/main/java/org/apache/calcite/sql/type/SqlTypeAssignmentRule.java
@@ -175,6 +175,9 @@
     rules.add(SqlTypeName.TIME_WITH_LOCAL_TIME_ZONE,
         EnumSet.of(SqlTypeName.TIME_WITH_LOCAL_TIME_ZONE));
 
+    // TIME WITH TIME ZONE is assignable from...
+    rules.add(SqlTypeName.TIME_TZ, EnumSet.of(SqlTypeName.TIME_TZ));
+
     // TIMESTAMP is assignable from ...
     rules.add(SqlTypeName.TIMESTAMP, EnumSet.of(SqlTypeName.TIMESTAMP));
 
@@ -182,6 +185,9 @@
     rules.add(SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE,
         EnumSet.of(SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE));
 
+    // TIMESTAMP WITH TIME ZONE is assignable from...
+    rules.add(SqlTypeName.TIMESTAMP_TZ, EnumSet.of(SqlTypeName.TIMESTAMP_TZ));
+
     // GEOMETRY is assignable from ...
     rule.clear();
     rule.add(SqlTypeName.GEOMETRY);
diff --git a/core/src/main/java/org/apache/calcite/sql/type/SqlTypeCoercionRule.java b/core/src/main/java/org/apache/calcite/sql/type/SqlTypeCoercionRule.java
index 7370c1d..4ab42df 100644
--- a/core/src/main/java/org/apache/calcite/sql/type/SqlTypeCoercionRule.java
+++ b/core/src/main/java/org/apache/calcite/sql/type/SqlTypeCoercionRule.java
@@ -120,6 +120,7 @@
     rule.add(SqlTypeName.VARCHAR);
     rule.add(SqlTypeName.TIMESTAMP);
     rule.add(SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE);
+    rule.add(SqlTypeName.TIMESTAMP_TZ);
 
     coerceRules.add(SqlTypeName.TINYINT, rule);
     coerceRules.add(SqlTypeName.SMALLINT, rule);
@@ -176,8 +177,11 @@
             .add(SqlTypeName.BOOLEAN)
             .add(SqlTypeName.DATE)
             .add(SqlTypeName.TIME)
+            .add(SqlTypeName.TIME_WITH_LOCAL_TIME_ZONE)
+            .add(SqlTypeName.TIME_TZ)
             .add(SqlTypeName.TIMESTAMP)
             .add(SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE)
+            .add(SqlTypeName.TIMESTAMP_TZ)
             .addAll(SqlTypeName.BINARY_TYPES)
             .addAll(SqlTypeName.NUMERIC_TYPES)
             .addAll(SqlTypeName.INTERVAL_TYPES)
@@ -191,8 +195,11 @@
             .add(SqlTypeName.BOOLEAN)
             .add(SqlTypeName.DATE)
             .add(SqlTypeName.TIME)
+            .add(SqlTypeName.TIME_WITH_LOCAL_TIME_ZONE)
+            .add(SqlTypeName.TIME_TZ)
             .add(SqlTypeName.TIMESTAMP)
             .add(SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE)
+            .add(SqlTypeName.TIMESTAMP_TZ)
             .addAll(SqlTypeName.BINARY_TYPES)
             .addAll(SqlTypeName.NUMERIC_TYPES)
             .addAll(SqlTypeName.INTERVAL_TYPES)
@@ -214,6 +221,7 @@
         coerceRules.copyValues(SqlTypeName.DATE)
             .add(SqlTypeName.TIMESTAMP)
             .add(SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE)
+            .add(SqlTypeName.TIMESTAMP_TZ)
             .add(SqlTypeName.CHAR)
             .add(SqlTypeName.VARCHAR)
             .addAll(SqlTypeName.BINARY_TYPES)
@@ -223,8 +231,10 @@
     coerceRules.add(SqlTypeName.TIME,
         coerceRules.copyValues(SqlTypeName.TIME)
             .add(SqlTypeName.TIME_WITH_LOCAL_TIME_ZONE)
+            .add(SqlTypeName.TIME_TZ)
             .add(SqlTypeName.TIMESTAMP)
             .add(SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE)
+            .add(SqlTypeName.TIMESTAMP_TZ)
             .add(SqlTypeName.CHAR)
             .add(SqlTypeName.VARCHAR)
             .addAll(SqlTypeName.BINARY_TYPES)
@@ -233,9 +243,24 @@
     // TIME WITH LOCAL TIME ZONE is castable from...
     coerceRules.add(SqlTypeName.TIME_WITH_LOCAL_TIME_ZONE,
         coerceRules.copyValues(SqlTypeName.TIME_WITH_LOCAL_TIME_ZONE)
+            .add(SqlTypeName.TIME_TZ)
             .add(SqlTypeName.TIME)
             .add(SqlTypeName.TIMESTAMP)
             .add(SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE)
+            .add(SqlTypeName.TIMESTAMP_TZ)
+            .add(SqlTypeName.CHAR)
+            .add(SqlTypeName.VARCHAR)
+            .addAll(SqlTypeName.BINARY_TYPES)
+            .build());
+
+    // TIME WITH TIME ZONE is castable from...
+    coerceRules.add(SqlTypeName.TIME_TZ,
+        coerceRules.copyValues(SqlTypeName.TIME_TZ)
+            .add(SqlTypeName.TIME_WITH_LOCAL_TIME_ZONE)
+            .add(SqlTypeName.TIME)
+            .add(SqlTypeName.TIMESTAMP)
+            .add(SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE)
+            .add(SqlTypeName.TIMESTAMP_TZ)
             .add(SqlTypeName.CHAR)
             .add(SqlTypeName.VARCHAR)
             .addAll(SqlTypeName.BINARY_TYPES)
@@ -245,9 +270,11 @@
     coerceRules.add(SqlTypeName.TIMESTAMP,
         coerceRules.copyValues(SqlTypeName.TIMESTAMP)
             .add(SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE)
+            .add(SqlTypeName.TIMESTAMP_TZ)
             .add(SqlTypeName.DATE)
             .add(SqlTypeName.TIME)
             .add(SqlTypeName.TIME_WITH_LOCAL_TIME_ZONE)
+            .add(SqlTypeName.TIME_TZ)
             .add(SqlTypeName.CHAR)
             .add(SqlTypeName.VARCHAR)
             .addAll(SqlTypeName.BINARY_TYPES)
@@ -257,10 +284,27 @@
     // TIMESTAMP WITH LOCAL TIME ZONE is castable from...
     coerceRules.add(SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE,
         coerceRules.copyValues(SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE)
+            .add(SqlTypeName.TIMESTAMP_TZ)
             .add(SqlTypeName.TIMESTAMP)
             .add(SqlTypeName.DATE)
             .add(SqlTypeName.TIME)
             .add(SqlTypeName.TIME_WITH_LOCAL_TIME_ZONE)
+            .add(SqlTypeName.TIME_TZ)
+            .add(SqlTypeName.CHAR)
+            .add(SqlTypeName.VARCHAR)
+            .addAll(SqlTypeName.BINARY_TYPES)
+            .addAll(SqlTypeName.NUMERIC_TYPES)
+            .build());
+
+    // TIMESTAMP WITH TIME ZONE is castable from...
+    coerceRules.add(SqlTypeName.TIMESTAMP_TZ,
+        coerceRules.copyValues(SqlTypeName.TIMESTAMP_TZ)
+            .add(SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE)
+            .add(SqlTypeName.TIMESTAMP)
+            .add(SqlTypeName.DATE)
+            .add(SqlTypeName.TIME)
+            .add(SqlTypeName.TIME_WITH_LOCAL_TIME_ZONE)
+            .add(SqlTypeName.TIME_TZ)
             .add(SqlTypeName.CHAR)
             .add(SqlTypeName.VARCHAR)
             .addAll(SqlTypeName.BINARY_TYPES)
@@ -292,6 +336,7 @@
     rule.add(SqlTypeName.BOOLEAN);
     rule.add(SqlTypeName.TIMESTAMP);
     rule.add(SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE);
+    rule.add(SqlTypeName.TIMESTAMP_TZ);
 
     coerceRules.add(SqlTypeName.TINYINT, rule);
     coerceRules.add(SqlTypeName.SMALLINT, rule);
diff --git a/core/src/main/java/org/apache/calcite/sql/type/SqlTypeFamily.java b/core/src/main/java/org/apache/calcite/sql/type/SqlTypeFamily.java
index 58b6167..0509022 100644
--- a/core/src/main/java/org/apache/calcite/sql/type/SqlTypeFamily.java
+++ b/core/src/main/java/org/apache/calcite/sql/type/SqlTypeFamily.java
@@ -179,9 +179,13 @@
     case DATE:
       return ImmutableList.of(SqlTypeName.DATE);
     case TIME:
-      return ImmutableList.of(SqlTypeName.TIME, SqlTypeName.TIME_WITH_LOCAL_TIME_ZONE);
+      return ImmutableList.of(SqlTypeName.TIME,
+          SqlTypeName.TIME_WITH_LOCAL_TIME_ZONE,
+          SqlTypeName.TIME_TZ);
     case TIMESTAMP:
-      return ImmutableList.of(SqlTypeName.TIMESTAMP, SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE);
+      return ImmutableList.of(SqlTypeName.TIMESTAMP,
+          SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE,
+          SqlTypeName.TIMESTAMP_TZ);
     case BOOLEAN:
       return SqlTypeName.BOOLEAN_TYPES;
     case INTERVAL_YEAR_MONTH:
diff --git a/core/src/main/java/org/apache/calcite/sql/type/SqlTypeName.java b/core/src/main/java/org/apache/calcite/sql/type/SqlTypeName.java
index 442e5ce..0c856e3 100644
--- a/core/src/main/java/org/apache/calcite/sql/type/SqlTypeName.java
+++ b/core/src/main/java/org/apache/calcite/sql/type/SqlTypeName.java
@@ -65,12 +65,16 @@
   DATE(PrecScale.NO_NO, false, Types.DATE, SqlTypeFamily.DATE),
   TIME(PrecScale.NO_NO | PrecScale.YES_NO, false, Types.TIME,
       SqlTypeFamily.TIME),
-  TIME_WITH_LOCAL_TIME_ZONE(PrecScale.NO_NO | PrecScale.YES_NO, false, Types.OTHER,
+  TIME_WITH_LOCAL_TIME_ZONE(PrecScale.NO_NO | PrecScale.YES_NO, false, Types.TIME,
+      SqlTypeFamily.TIME),
+  TIME_TZ(PrecScale.NO_NO | PrecScale.YES_NO, false, Types.TIME,
       SqlTypeFamily.TIME),
   TIMESTAMP(PrecScale.NO_NO | PrecScale.YES_NO, false, Types.TIMESTAMP,
       SqlTypeFamily.TIMESTAMP),
   TIMESTAMP_WITH_LOCAL_TIME_ZONE(PrecScale.NO_NO | PrecScale.YES_NO, false,
       Types.TIMESTAMP, SqlTypeFamily.TIMESTAMP),
+  TIMESTAMP_TZ(PrecScale.NO_NO | PrecScale.YES_NO, false,
+      Types.TIMESTAMP, SqlTypeFamily.TIMESTAMP),
   INTERVAL_YEAR(PrecScale.NO_NO, false, Types.OTHER,
       SqlTypeFamily.INTERVAL_YEAR_MONTH),
   INTERVAL_YEAR_MONTH(PrecScale.NO_NO, false, Types.OTHER,
@@ -159,7 +163,8 @@
           INTERVAL_DAY, INTERVAL_DAY_HOUR, INTERVAL_DAY_MINUTE,
           INTERVAL_DAY_SECOND, INTERVAL_HOUR, INTERVAL_HOUR_MINUTE,
           INTERVAL_HOUR_SECOND, INTERVAL_MINUTE, INTERVAL_MINUTE_SECOND,
-          INTERVAL_SECOND, TIME_WITH_LOCAL_TIME_ZONE, TIMESTAMP_WITH_LOCAL_TIME_ZONE,
+          INTERVAL_SECOND, TIME_WITH_LOCAL_TIME_ZONE, TIME_TZ,
+          TIMESTAMP_WITH_LOCAL_TIME_ZONE, TIMESTAMP_TZ,
           FLOAT, MULTISET, DISTINCT, STRUCTURED, ROW, CURSOR, COLUMN_LIST);
 
   public static final List<SqlTypeName> BOOLEAN_TYPES =
@@ -193,8 +198,13 @@
       ImmutableList.of(GEOMETRY);
 
   public static final List<SqlTypeName> DATETIME_TYPES =
-      ImmutableList.of(DATE, TIME, TIME_WITH_LOCAL_TIME_ZONE,
-          TIMESTAMP, TIMESTAMP_WITH_LOCAL_TIME_ZONE);
+      ImmutableList.of(DATE, TIME, TIME_WITH_LOCAL_TIME_ZONE, TIME_TZ,
+          TIMESTAMP, TIMESTAMP_WITH_LOCAL_TIME_ZONE, TIMESTAMP_TZ);
+
+  /** Types that contain time zone information. */
+  public static final List<SqlTypeName> TZ_TYPES =
+      ImmutableList.of(TIME_WITH_LOCAL_TIME_ZONE, TIME_TZ,
+          TIMESTAMP_WITH_LOCAL_TIME_ZONE, TIMESTAMP_TZ);
 
   public static final Set<SqlTypeName> YEAR_INTERVAL_TYPES =
       Sets.immutableEnumSet(SqlTypeName.INTERVAL_YEAR,
@@ -309,7 +319,12 @@
    * matches the given name, or throws {@link IllegalArgumentException}; never
    * returns null. */
   public static SqlTypeName lookup(String tag) {
-    String tag2 = tag.replace(' ', '_');
+    // Special handling for TIME WITH TIME ZONE and
+    // TIMESTAMP WITH TIME ZONE, whose type names are TIME_TZ and TIMESTAMP_TZ.
+    // We know that the type name is always uppercase, because it is
+    // inserted in the tag by the parser.
+    final String tag1 = tag.replace("WITH TIME ZONE", "TZ");
+    final String tag2 = tag1.replace(' ', '_');
     return valueOf(tag2);
   }
 
@@ -776,8 +791,10 @@
     case BINARY:
     case TIME:
     case TIME_WITH_LOCAL_TIME_ZONE:
+    case TIME_TZ:
     case TIMESTAMP:
     case TIMESTAMP_WITH_LOCAL_TIME_ZONE:
+    case TIMESTAMP_TZ:
       return 1;
     case INTERVAL_YEAR:
     case INTERVAL_YEAR_MONTH:
diff --git a/core/src/main/java/org/apache/calcite/sql2rel/SqlNodeToRexConverterImpl.java b/core/src/main/java/org/apache/calcite/sql2rel/SqlNodeToRexConverterImpl.java
index 313fda1..2b99128 100644
--- a/core/src/main/java/org/apache/calcite/sql2rel/SqlNodeToRexConverterImpl.java
+++ b/core/src/main/java/org/apache/calcite/sql2rel/SqlNodeToRexConverterImpl.java
@@ -32,6 +32,7 @@
 import org.apache.calcite.util.NlsString;
 import org.apache.calcite.util.TimeString;
 import org.apache.calcite.util.TimestampString;
+import org.apache.calcite.util.TimestampWithTimeZoneString;
 import org.apache.calcite.util.Util;
 
 import java.math.BigDecimal;
@@ -124,6 +125,10 @@
       return rexBuilder.makeTimestampWithLocalTimeZoneLiteral(
           literal.getValueAs(TimestampString.class),
           ((SqlTimestampLiteral) literal).getPrec());
+    case TIMESTAMP_TZ:
+      return rexBuilder.makeTimestampTzLiteral(
+          literal.getValueAs(TimestampWithTimeZoneString.class),
+          ((SqlTimestampLiteral) literal).getPrec());
     case TIME:
       return rexBuilder.makeTimeLiteral(
           literal.getValueAs(TimeString.class),
diff --git a/core/src/main/java/org/apache/calcite/util/TimeWithTimeZoneString.java b/core/src/main/java/org/apache/calcite/util/TimeWithTimeZoneString.java
index a57e949..2087b79 100644
--- a/core/src/main/java/org/apache/calcite/util/TimeWithTimeZoneString.java
+++ b/core/src/main/java/org/apache/calcite/util/TimeWithTimeZoneString.java
@@ -74,6 +74,12 @@
     return withFraction(DateTimeStringUtils.pad(3, millis));
   }
 
+  /** Creates a TimeWithTimeZoneString from a Calendar. */
+  public static TimeWithTimeZoneString fromCalendarFields(Calendar calendar) {
+    TimeString ts = TimeString.fromCalendarFields(calendar);
+    return new TimeWithTimeZoneString(ts, calendar.getTimeZone());
+  }
+
   /** Sets the fraction field of a {@code TimeString} to a given number
    * of nanoseconds. Nukes the value set via {@link #withMillis(int)}.
    *
diff --git a/core/src/main/java/org/apache/calcite/util/TimestampString.java b/core/src/main/java/org/apache/calcite/util/TimestampString.java
index 45d7eb9..c6c7c18 100644
--- a/core/src/main/java/org/apache/calcite/util/TimestampString.java
+++ b/core/src/main/java/org/apache/calcite/util/TimestampString.java
@@ -25,6 +25,7 @@
 import org.checkerframework.checker.nullness.qual.Nullable;
 
 import java.util.Calendar;
+import java.util.TimeZone;
 import java.util.regex.Pattern;
 
 import static com.google.common.base.Preconditions.checkArgument;
@@ -222,6 +223,10 @@
     return Util.calendar(getMillisSinceEpoch());
   }
 
+  public Calendar toCalendar(TimeZone timeZone) {
+    return Util.calendar(getMillisSinceEpoch(), timeZone);
+  }
+
   /** Converts this TimestampString to a string, truncated or padded with
    * zeros to a given precision. */
   public String toString(int precision) {
diff --git a/core/src/main/java/org/apache/calcite/util/TimestampWithTimeZoneString.java b/core/src/main/java/org/apache/calcite/util/TimestampWithTimeZoneString.java
index 8e31656..e3c08bb 100644
--- a/core/src/main/java/org/apache/calcite/util/TimestampWithTimeZoneString.java
+++ b/core/src/main/java/org/apache/calcite/util/TimestampWithTimeZoneString.java
@@ -100,6 +100,12 @@
         localDateTime.withFraction(fraction), timeZone);
   }
 
+  /** Creates a TimestampWithTimeZoneString from a Calendar. */
+  public static TimestampWithTimeZoneString fromCalendarFields(Calendar calendar) {
+    TimestampString ts = TimestampString.fromCalendarFields(calendar);
+    return new TimestampWithTimeZoneString(ts, calendar.getTimeZone());
+  }
+
   public TimestampWithTimeZoneString withTimeZone(TimeZone timeZone) {
     if (this.timeZone.equals(timeZone)) {
       return this;
@@ -193,4 +199,7 @@
     return localDateTime;
   }
 
+  public TimeZone getTimeZone() {
+    return timeZone;
+  }
 }
diff --git a/core/src/main/java/org/apache/calcite/util/Util.java b/core/src/main/java/org/apache/calcite/util/Util.java
index 2beda0a..0674009 100644
--- a/core/src/main/java/org/apache/calcite/util/Util.java
+++ b/core/src/main/java/org/apache/calcite/util/Util.java
@@ -2582,8 +2582,7 @@
     }
   }
 
-  /** Creates a {@link Calendar} in the UTC time zone and root locale.
-   * Does not use the time zone or locale. */
+  /** Creates a {@link Calendar} in the UTC time zone and root locale. */
   public static Calendar calendar() {
     return Calendar.getInstance(DateTimeUtils.UTC_ZONE, Locale.ROOT);
   }
@@ -2596,6 +2595,13 @@
     return calendar;
   }
 
+  /** Creates a {@link Calendar} in the specified time zone. */
+  public static Calendar calendar(long millis, TimeZone timeZone) {
+    Calendar calendar = Calendar.getInstance(timeZone, Locale.ROOT);
+    calendar.setTimeInMillis(millis);
+    return calendar;
+  }
+
   /**
    * Returns a {@code Collector} that accumulates the input elements into a
    * Guava {@link ImmutableList} via a {@link ImmutableList.Builder}.
diff --git a/core/src/test/java/org/apache/calcite/jdbc/CalciteRemoteDriverTest.java b/core/src/test/java/org/apache/calcite/jdbc/CalciteRemoteDriverTest.java
index e0dce8b..45c8935 100644
--- a/core/src/test/java/org/apache/calcite/jdbc/CalciteRemoteDriverTest.java
+++ b/core/src/test/java/org/apache/calcite/jdbc/CalciteRemoteDriverTest.java
@@ -319,7 +319,7 @@
     CalciteAssert.hr()
         .with(CalciteRemoteDriverTest::getRemoteConnection)
         .metaData(CalciteRemoteDriverTest::getTypeInfo)
-        .returns(CalciteAssert.checkResultCount(is(41)));
+        .returns(CalciteAssert.checkResultCount(is(43)));
   }
 
   @Test void testRemoteTableTypes() {
diff --git a/core/src/test/java/org/apache/calcite/rex/RexBuilderTest.java b/core/src/test/java/org/apache/calcite/rex/RexBuilderTest.java
index bcba216..9db5f8e 100644
--- a/core/src/test/java/org/apache/calcite/rex/RexBuilderTest.java
+++ b/core/src/test/java/org/apache/calcite/rex/RexBuilderTest.java
@@ -39,6 +39,7 @@
 import org.apache.calcite.util.Litmus;
 import org.apache.calcite.util.NlsString;
 import org.apache.calcite.util.TimeString;
+import org.apache.calcite.util.TimeWithTimeZoneString;
 import org.apache.calcite.util.TimestampString;
 import org.apache.calcite.util.TimestampWithTimeZoneString;
 import org.apache.calcite.util.Util;
@@ -438,6 +439,77 @@
     assertThat(literal.getValue3() instanceof Long, is(true));
   }
 
+  /** Tests
+   * {@link RexBuilder#makeTimestampTzLiteral(TimestampWithTimeZoneString, int)}. */
+  @Test void testTimestampTzLiterals() {
+    final RelDataTypeFactory typeFactory =
+        new SqlTypeFactoryImpl(RelDataTypeSystem.DEFAULT);
+    final RelDataType timestampType =
+        typeFactory.createSqlType(SqlTypeName.TIMESTAMP_TZ);
+    final RelDataType timestampType3 =
+        typeFactory.createSqlType(SqlTypeName.TIMESTAMP_TZ, 3);
+    final RelDataType timestampType9 =
+        typeFactory.createSqlType(SqlTypeName.TIMESTAMP_TZ, 9);
+    final RelDataType timestampType18 =
+        typeFactory.createSqlType(SqlTypeName.TIMESTAMP_TZ, 18);
+    final RexBuilder builder = new RexBuilder(typeFactory);
+
+    // The new way
+    final TimestampWithTimeZoneString ts =
+        new TimestampWithTimeZoneString(1969, 7, 21, 2, 56, 15,
+            TimeZone.getTimeZone("PST").getID());
+    checkTimestampTz(builder.makeLiteral(ts, timestampType));
+
+    // Now with milliseconds
+    final TimestampWithTimeZoneString ts2 = ts.withMillis(56);
+    assertThat(ts2, hasToString("1969-07-21 02:56:15.056 PST"));
+    final RexLiteral literal2 =
+        builder.makeLiteral(ts2, timestampType3);
+    assertThat(literal2.getValue(), hasToString("1969-07-21 02:56:15.056 PST"));
+
+    // Now with nanoseconds
+    final TimestampWithTimeZoneString ts3 = ts.withNanos(56);
+    final RexLiteral literal3 =
+        builder.makeLiteral(ts3, timestampType9);
+    assertThat(literal3.getValueAs(TimestampWithTimeZoneString.class),
+        hasToString("1969-07-21 02:56:15 PST"));
+    final TimestampWithTimeZoneString ts3b = ts.withNanos(2345678);
+    final RexLiteral literal3b =
+        builder.makeLiteral(ts3b, timestampType9);
+    assertThat(literal3b.getValueAs(TimestampWithTimeZoneString.class),
+        hasToString("1969-07-21 02:56:15.002 PST"));
+
+    // Now with a very long fraction
+    final TimestampWithTimeZoneString ts4 = ts.withFraction("102030405060708090102");
+    final RexLiteral literal4 =
+        builder.makeLiteral(ts4, timestampType18);
+    assertThat(literal4.getValueAs(TimestampWithTimeZoneString.class),
+        hasToString("1969-07-21 02:56:15.102 PST"));
+
+    // toString
+    assertThat(ts2.round(1), hasToString("1969-07-21 02:56:15 PST"));
+    assertThat(ts2.round(2), hasToString("1969-07-21 02:56:15.05 PST"));
+    assertThat(ts2.round(3), hasToString("1969-07-21 02:56:15.056 PST"));
+    assertThat(ts2.round(4), hasToString("1969-07-21 02:56:15.056 PST"));
+
+    assertThat(ts2.toString(6), is("1969-07-21 02:56:15.056000 PST"));
+    assertThat(ts2.toString(1), is("1969-07-21 02:56:15.0 PST"));
+    assertThat(ts2.toString(0), is("1969-07-21 02:56:15 PST"));
+
+    assertThat(ts2.round(0), hasToString("1969-07-21 02:56:15 PST"));
+    assertThat(ts2.round(0).toString(0), is("1969-07-21 02:56:15 PST"));
+    assertThat(ts2.round(0).toString(1), is("1969-07-21 02:56:15.0 PST"));
+    assertThat(ts2.round(0).toString(2), is("1969-07-21 02:56:15.00 PST"));
+  }
+
+  private void checkTimestampTz(RexLiteral literal) {
+    assertThat(literal,
+        hasToString("1969-07-21 02:56:15 PST:TIMESTAMP_TZ(0)"));
+    assertThat(literal.getValue() instanceof TimestampWithTimeZoneString, is(true));
+    assertThat(literal.getValue2() instanceof Long, is(true));
+    assertThat(literal.getValue3() instanceof Long, is(true));
+  }
+
   /** Tests {@link RexBuilder#makeTimeLiteral(TimeString, int)}. */
   @Test void testTimeLiteral() {
     final RelDataTypeFactory typeFactory =
@@ -827,7 +899,11 @@
         type2rexLiteral.apply(typeFactory.createSqlType(SqlTypeName.TIME_WITH_LOCAL_TIME_ZONE),
             relDataType -> new TimeString(0, 0, 0)),
         type2rexLiteral.apply(typeFactory.createSqlType(SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE),
-            relDataType -> new TimestampString(0, 1, 1, 0, 0, 0)));
+            relDataType -> new TimestampString(0, 1, 1, 0, 0, 0)),
+        type2rexLiteral.apply(typeFactory.createSqlType(SqlTypeName.TIME_TZ),
+            relDataType -> new TimeWithTimeZoneString(0, 0, 0, "GMT+00:00")),
+        type2rexLiteral.apply(typeFactory.createSqlType(SqlTypeName.TIMESTAMP_TZ),
+            relDataType -> new TimestampWithTimeZoneString(0, 1, 1, 0, 0, 0, "GMT+00:00")));
   }
 
   /** Test case for
diff --git a/core/src/test/java/org/apache/calcite/sql/type/SqlTypeFactoryTest.java b/core/src/test/java/org/apache/calcite/sql/type/SqlTypeFactoryTest.java
index 5a0d965..a85fff1 100644
--- a/core/src/test/java/org/apache/calcite/sql/type/SqlTypeFactoryTest.java
+++ b/core/src/test/java/org/apache/calcite/sql/type/SqlTypeFactoryTest.java
@@ -284,7 +284,9 @@
     checkCreateSqlTypeWithPrecision(f.typeFactory, SqlTypeName.TIME);
     checkCreateSqlTypeWithPrecision(f.typeFactory, SqlTypeName.TIMESTAMP);
     checkCreateSqlTypeWithPrecision(f.typeFactory, SqlTypeName.TIME_WITH_LOCAL_TIME_ZONE);
+    checkCreateSqlTypeWithPrecision(f.typeFactory, SqlTypeName.TIME_TZ);
     checkCreateSqlTypeWithPrecision(f.typeFactory, SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE);
+    checkCreateSqlTypeWithPrecision(f.typeFactory, SqlTypeName.TIMESTAMP_TZ);
   }
 
   private void checkCreateSqlTypeWithPrecision(
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 1308aa9..21e2e91 100644
--- a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
+++ b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
@@ -278,8 +278,11 @@
         .fails("(?s).*Illegal TIME literal.*");
     expr("^TIMESTAMP '12-21-99, 12:30:00'^")
         .fails("(?s).*Illegal TIMESTAMP literal.*");
+
     expr("^TIMESTAMP WITH LOCAL TIME ZONE '12-21-99, 12:30:00'^")
         .fails("(?s).*Illegal TIMESTAMP WITH LOCAL TIME ZONE literal.*");
+    expr("^TIMESTAMP WITH TIME ZONE '12-21-99, 12:30:00'^")
+        .fails("(?s).*Illegal TIMESTAMP literal.*");
   }
 
   /** PostgreSQL and Redshift allow TIMESTAMP literals that contain only a
@@ -1310,12 +1313,16 @@
         .columnType("TIME(0) NOT NULL");
     expr("cast('abc' as time with local time zone)")
         .columnType("TIME_WITH_LOCAL_TIME_ZONE(0) NOT NULL");
+    expr("cast('abc' as time with time zone)")
+        .columnType("TIME_TZ(0) NOT NULL");
     expr("cast('abc' as time(3))")
         .columnType("TIME(3) NOT NULL");
     expr("cast('abc' as time(3) without time zone)")
         .columnType("TIME(3) NOT NULL");
     expr("cast('abc' as time(3) with local time zone)")
         .columnType("TIME_WITH_LOCAL_TIME_ZONE(3) NOT NULL");
+    expr("cast('abc' as time(3) with time zone)")
+        .columnType("TIME_TZ(3) NOT NULL");
     // test cast to timestamp type.
     expr("cast('abc' as timestamp)")
         .columnType("TIMESTAMP(0) NOT NULL");
@@ -1323,12 +1330,16 @@
         .columnType("TIMESTAMP(0) NOT NULL");
     expr("cast('abc' as timestamp with local time zone)")
         .columnType("TIMESTAMP_WITH_LOCAL_TIME_ZONE(0) NOT NULL");
+    expr("cast('abc' as timestamp with time zone)")
+        .columnType("TIMESTAMP_TZ(0) NOT NULL");
     expr("cast('abc' as timestamp(3))")
         .columnType("TIMESTAMP(3) NOT NULL");
     expr("cast('abc' as timestamp(3) without time zone)")
         .columnType("TIMESTAMP(3) NOT NULL");
     expr("cast('abc' as timestamp(3) with local time zone)")
         .columnType("TIMESTAMP_WITH_LOCAL_TIME_ZONE(3) NOT NULL");
+    expr("cast('abc' as timestamp(3) with time zone)")
+        .columnType("TIMESTAMP_TZ(3) NOT NULL");
   }
 
   @Test void testCastRegisteredType() {
diff --git a/site/_docs/reference.md b/site/_docs/reference.md
index 21edb54..d8712e5 100644
--- a/site/_docs/reference.md
+++ b/site/_docs/reference.md
@@ -1167,9 +1167,11 @@
 | VARBINARY(n), BINARY VARYING(n) | Variable-length binary string | As BINARY(n)
 | DATE        | Date                      | Example: DATE '1969-07-20'
 | TIME        | Time of day               | Example: TIME '20:17:40'
+| TIME WITH LOCAL TIME ZONE | Time of day with local time zone | Example: TIME WITH LOCAL TIME ZONE '20:17:40'
+| TIME WITH TIME ZONE | Time of day with time zone | Example: TIME '20:17:40 GMT+08'
 | TIMESTAMP [ WITHOUT TIME ZONE ] | Date and time | Example: TIMESTAMP '1969-07-20 20:17:40'
-| TIMESTAMP WITH LOCAL TIME ZONE | Date and time with local time zone | Example: TIMESTAMP '1969-07-20 20:17:40 America/Los Angeles'
-| TIMESTAMP WITH TIME ZONE | Date and time with time zone | Example: TIMESTAMP '1969-07-20 20:17:40 America/Los Angeles'
+| TIMESTAMP WITH LOCAL TIME ZONE | Date and time with local time zone | Example: TIMESTAMP WITH LOCAL TIME ZONE '1969-07-20 20:17:40'
+| TIMESTAMP WITH TIME ZONE | Date and time with time zone | Example: TIMESTAMP WITH TIME ZONE '1969-07-20 20:17:40 America/Los Angeles'
 | INTERVAL timeUnit [ TO timeUnit ] | Date time interval | Examples: INTERVAL '1-5' YEAR TO MONTH, INTERVAL '45' DAY, INTERVAL '1 2:34:56.789' DAY TO SECOND
 | GEOMETRY | Geometry | Examples: ST_GeomFromText('POINT (30 10)')
 
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 a8ed3e6..c5bce8b 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
@@ -1783,7 +1783,7 @@
         .ok("CEIL((`X` + INTERVAL '1:20' MINUTE TO SECOND) TO MILLENNIUM)");
   }
 
-  @Test void testCast() {
+  @Test public void testCast() {
     expr("cast(x as boolean)")
         .ok("CAST(`X` AS BOOLEAN)");
     expr("cast(x as integer)")
@@ -1798,10 +1798,14 @@
         .ok("CAST(`X` AS TIME)");
     expr("cast(x as time with local time zone)")
         .ok("CAST(`X` AS TIME WITH LOCAL TIME ZONE)");
+    expr("cast(x as time with time zone)")
+        .ok("CAST(`X` AS TIME WITH TIME ZONE)");
     expr("cast(x as timestamp without time zone)")
         .ok("CAST(`X` AS TIMESTAMP)");
     expr("cast(x as timestamp with local time zone)")
         .ok("CAST(`X` AS TIMESTAMP WITH LOCAL TIME ZONE)");
+    expr("cast(x as timestamp with time zone)")
+        .ok("CAST(`X` AS TIMESTAMP WITH TIME ZONE)");
     expr("cast(x as time(0))")
         .ok("CAST(`X` AS TIME(0))");
     expr("cast(x as time(0) without time zone)")
@@ -1846,14 +1850,6 @@
   }
 
   @Test void testCastFails() {
-    expr("cast(x as time with ^time^ zone)")
-        .fails("(?s).*Encountered \"time\" at .*");
-    expr("cast(x as time(0) with ^time^ zone)")
-        .fails("(?s).*Encountered \"time\" at .*");
-    expr("cast(x as timestamp with ^time^ zone)")
-        .fails("(?s).*Encountered \"time\" at .*");
-    expr("cast(x as timestamp(0) with ^time^ zone)")
-        .fails("(?s).*Encountered \"time\" at .*");
     expr("cast(x as varchar(10) ^with^ local time zone)")
         .fails("(?s).*Encountered \"with\" at line 1, column 23.\n.*");
     expr("cast(x as varchar(10) ^without^ time zone)")
@@ -5550,8 +5546,11 @@
     expr("^DATE '12/21/99'^").same();
     expr("^TIME '1230:33'^").same();
     expr("^TIME '12:00:00 PM'^").same();
+    expr("^TIME WITH LOCAL TIME ZONE '12:00:00 PM'^").same();
+    expr("^TIME WITH TIME ZONE '12:00:00 PM GMT+0:00'^").same();
     expr("TIMESTAMP '12-21-99, 12:30:00'").same();
     expr("TIMESTAMP WITH LOCAL TIME ZONE '12-21-99, 12:30:00'").same();
+    expr("TIMESTAMP WITH TIME ZONE '12-21-99, 12:30:00 GMT+0:00'").same();
     expr("DATETIME '12-21-99, 12:30:00'").same();
   }