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-avatica.git


The following commit(s) were added to refs/heads/main by this push:
     new dbe9b1d8c [CALCITE-2989] Use ISO calendar when converting between 
java.sql types and UNIX timestamps
dbe9b1d8c is described below

commit dbe9b1d8c2e53474eb40cfaf5721aceca3bdb57f
Author: Greg Hart <[email protected]>
AuthorDate: Mon Nov 14 17:33:26 2022 -0800

    [CALCITE-2989] Use ISO calendar when converting between java.sql types and 
UNIX timestamps
    
    Close apache/calcite-avatica#197
---
 .../apache/calcite/avatica/remote/TypedValue.java  |  34 +-
 .../calcite/avatica/util/AbstractCursor.java       | 151 +++---
 .../apache/calcite/avatica/util/DateTimeUtils.java | 240 +++++++++
 .../avatica/AvaticaResultSetConversionsTest.java   |   4 +-
 .../calcite/avatica/remote/TypedValueTest.java     |  37 +-
 .../calcite/avatica/util/DateAccessorTest.java     | 155 ++++++
 .../avatica/util/DateFromNumberAccessorTest.java   | 254 +++++++++
 .../calcite/avatica/util/DateTimeUtilsTest.java    | 565 ++++++++++++++++++++-
 .../calcite/avatica/util/TimeAccessorTest.java     | 134 +++++
 .../avatica/util/TimeFromNumberAccessorTest.java   | 159 ++++++
 .../avatica/util/TimestampAccessorTest.java        | 248 +++++++++
 .../util/TimestampFromNumberAccessorTest.java      | 278 ++++++++++
 .../util/TimestampFromUtilDateAccessorTest.java    | 253 +++++++++
 13 files changed, 2417 insertions(+), 95 deletions(-)

diff --git 
a/core/src/main/java/org/apache/calcite/avatica/remote/TypedValue.java 
b/core/src/main/java/org/apache/calcite/avatica/remote/TypedValue.java
index 969172fdf..142a06b31 100644
--- a/core/src/main/java/org/apache/calcite/avatica/remote/TypedValue.java
+++ b/core/src/main/java/org/apache/calcite/avatica/remote/TypedValue.java
@@ -332,15 +332,13 @@ public class TypedValue {
     case BYTE_STRING:
       return ByteString.ofBase64((String) value).getBytes();
     case JAVA_UTIL_DATE:
-      return new java.util.Date(adjust((Number) value, calendar));
+      return DateTimeUtils.unixTimestampToUtilDate((Long) value, calendar);
     case JAVA_SQL_DATE:
-      return new java.sql.Date(
-          adjust(((Number) value).longValue() * DateTimeUtils.MILLIS_PER_DAY,
-              calendar));
+      return DateTimeUtils.unixDateToSqlDate((Integer) value, calendar);
     case JAVA_SQL_TIME:
-      return new java.sql.Time(adjust((Number) value, calendar));
+      return DateTimeUtils.unixTimeToSqlTime((Integer) value, calendar);
     case JAVA_SQL_TIMESTAMP:
-      return new java.sql.Timestamp(adjust((Number) value, calendar));
+      return DateTimeUtils.unixTimestampToSqlTimestamp((Long) value, calendar);
     case ARRAY:
       if (null == value) {
         return null;
@@ -373,14 +371,6 @@ public class TypedValue {
     }
   }
 
-  private static long adjust(Number number, Calendar calendar) {
-    long t = number.longValue();
-    if (calendar != null) {
-      t -= calendar.getTimeZone().getOffset(t);
-    }
-    return t;
-  }
-
   private static Object jdbcToSerial(ColumnMetaData.Rep rep, Object value,
       Calendar calendar) {
     return jdbcToSerial(rep, value, calendar, null);
@@ -397,21 +387,13 @@ public class TypedValue {
     case BYTE_STRING:
       return new ByteString((byte[]) value).toBase64String();
     case JAVA_UTIL_DATE:
+      return DateTimeUtils.utilDateToUnixTimestamp((Date) value, calendar);
     case JAVA_SQL_TIMESTAMP:
+      return DateTimeUtils.sqlTimestampToUnixTimestamp((Timestamp) value, 
calendar);
     case JAVA_SQL_DATE:
+      return DateTimeUtils.sqlDateToUnixDate((java.sql.Date) value, calendar);
     case JAVA_SQL_TIME:
-      long t = ((Date) value).getTime();
-      if (calendar != null) {
-        t += calendar.getTimeZone().getOffset(t);
-      }
-      switch (rep) {
-      case JAVA_SQL_DATE:
-        return (int) Math.floorDiv(t, DateTimeUtils.MILLIS_PER_DAY);
-      case JAVA_SQL_TIME:
-        return (int) Math.floorMod(t, DateTimeUtils.MILLIS_PER_DAY);
-      default:
-        return t;
-      }
+      return DateTimeUtils.sqlTimeToUnixTime((Time) value, calendar);
     case ARRAY:
       Array array = (Array) value;
       Objects.requireNonNull(componentType, "Component Type must not be null 
for ARRAYs");
diff --git 
a/core/src/main/java/org/apache/calcite/avatica/util/AbstractCursor.java 
b/core/src/main/java/org/apache/calcite/avatica/util/AbstractCursor.java
index c580246ea..ffffc39cb 100644
--- a/core/src/main/java/org/apache/calcite/avatica/util/AbstractCursor.java
+++ b/core/src/main/java/org/apache/calcite/avatica/util/AbstractCursor.java
@@ -145,7 +145,7 @@ public abstract class AbstractCursor implements Cursor {
       case NUMBER:
         return new DateFromNumberAccessor(getter, localCalendar);
       case JAVA_SQL_DATE:
-        return new DateAccessor(getter);
+        return new DateAccessor(getter, localCalendar);
       default:
         throw new AssertionError("bad " + columnMetaData.type.rep);
       }
@@ -156,7 +156,7 @@ public abstract class AbstractCursor implements Cursor {
       case NUMBER:
         return new TimeFromNumberAccessor(getter, localCalendar);
       case JAVA_SQL_TIME:
-        return new TimeAccessor(getter);
+        return new TimeAccessor(getter, localCalendar);
       default:
         throw new AssertionError("bad " + columnMetaData.type.rep);
       }
@@ -167,7 +167,7 @@ public abstract class AbstractCursor implements Cursor {
       case NUMBER:
         return new TimestampFromNumberAccessor(getter, localCalendar);
       case JAVA_SQL_TIMESTAMP:
-        return new TimestampAccessor(getter);
+        return new TimestampAccessor(getter, localCalendar);
       case JAVA_UTIL_DATE:
         return new TimestampFromUtilDateAccessor(getter, localCalendar);
       default:
@@ -262,27 +262,6 @@ public abstract class AbstractCursor implements Cursor {
     return DateTimeUtils.unixTimeToString(v);
   }
 
-  private static Date longToDate(long v, Calendar calendar) {
-    if (calendar != null) {
-      v -= calendar.getTimeZone().getOffset(v);
-    }
-    return new Date(v);
-  }
-
-  static Time intToTime(int v, Calendar calendar) {
-    if (calendar != null) {
-      v -= calendar.getTimeZone().getOffset(v);
-    }
-    return new Time(v);
-  }
-
-  static Timestamp longToTimestamp(long v, Calendar calendar) {
-    if (calendar != null) {
-      v -= calendar.getTimeZone().getOffset(v);
-    }
-    return new Timestamp(v);
-  }
-
   /** Implementation of {@link Cursor.Accessor}. */
   static class AccessorImpl implements Accessor {
     protected final Getter getter;
@@ -908,11 +887,15 @@ public abstract class AbstractCursor implements Cursor {
    * Accessor that assumes that the underlying value is a DATE,
    * in its default representation {@code int};
    * corresponds to {@link java.sql.Types#DATE}.
+   *
+   * <p>For backwards compatibility, timezone offsets are calculated in 
relation to the local
+   * timezone instead of to UTC. Providing the default calendar will return 
the underlying value
+   * unmodified. This differs from {@link DateAccessor} which uses the 
timezone offset from UTC.
    */
-  private static class DateFromNumberAccessor extends NumberAccessor {
+  static class DateFromNumberAccessor extends NumberAccessor {
     private final Calendar localCalendar;
 
-    private DateFromNumberAccessor(Getter getter, Calendar localCalendar) {
+    DateFromNumberAccessor(Getter getter, Calendar localCalendar) {
       super(getter, 0);
       this.localCalendar = localCalendar;
     }
@@ -926,7 +909,7 @@ public abstract class AbstractCursor implements Cursor {
       if (v == null) {
         return null;
       }
-      return longToDate(v.longValue() * DateTimeUtils.MILLIS_PER_DAY, 
calendar);
+      return DateTimeUtils.unixDateToSqlDate(v.intValue(), calendar);
     }
 
     @Override public Timestamp getTimestamp(Calendar calendar) throws 
SQLException {
@@ -934,7 +917,8 @@ public abstract class AbstractCursor implements Cursor {
       if (v == null) {
         return null;
       }
-      return longToTimestamp(v.longValue() * DateTimeUtils.MILLIS_PER_DAY,
+      return DateTimeUtils.unixTimestampToSqlTimestamp(
+          v.longValue() * DateTimeUtils.MILLIS_PER_DAY,
           calendar);
     }
 
@@ -964,11 +948,15 @@ public abstract class AbstractCursor implements Cursor {
    * Accessor that assumes that the underlying value is a Time,
    * in its default representation {@code int};
    * corresponds to {@link java.sql.Types#TIME}.
+   *
+   * <p>For backwards compatibility, timezone offsets are calculated in 
relation to the local
+   * timezone instead of to UTC. Passing in the default calendar will return 
the underlying value
+   * unmodified. This differs from {@link TimeAccessor} which uses the 
timezone offset from UTC.
    */
-  private static class TimeFromNumberAccessor extends NumberAccessor {
+  static class TimeFromNumberAccessor extends NumberAccessor {
     private final Calendar localCalendar;
 
-    private TimeFromNumberAccessor(Getter getter, Calendar localCalendar) {
+    TimeFromNumberAccessor(Getter getter, Calendar localCalendar) {
       super(getter, 0);
       this.localCalendar = localCalendar;
     }
@@ -982,7 +970,7 @@ public abstract class AbstractCursor implements Cursor {
       if (v == null) {
         return null;
       }
-      return intToTime(v.intValue(), calendar);
+      return DateTimeUtils.unixTimeToSqlTime(v.intValue(), calendar);
     }
 
     @Override public Timestamp getTimestamp(Calendar calendar) throws 
SQLException {
@@ -990,7 +978,7 @@ public abstract class AbstractCursor implements Cursor {
       if (v == null) {
         return null;
       }
-      return longToTimestamp(v.longValue(), calendar);
+      return DateTimeUtils.unixTimestampToSqlTimestamp(v.longValue(), 
calendar);
     }
 
     @Override public String getString() throws SQLException {
@@ -1017,11 +1005,16 @@ public abstract class AbstractCursor implements Cursor {
    * Accessor that assumes that the underlying value is a TIMESTAMP,
    * in its default representation {@code long};
    * corresponds to {@link java.sql.Types#TIMESTAMP}.
+   *
+   * <p>For backwards compatibility, timezone offsets are calculated in 
relation to the local
+   * timezone instead of to UTC. Passing in the default calendar will return 
the underlying value
+   * unmodified. This differs from {@link TimestampAccessor} which uses the 
timezone offset from
+   * UTC.
    */
-  private static class TimestampFromNumberAccessor extends NumberAccessor {
+  static class TimestampFromNumberAccessor extends NumberAccessor {
     private final Calendar localCalendar;
 
-    private TimestampFromNumberAccessor(Getter getter, Calendar localCalendar) 
{
+    TimestampFromNumberAccessor(Getter getter, Calendar localCalendar) {
       super(getter, 0);
       this.localCalendar = localCalendar;
     }
@@ -1035,7 +1028,7 @@ public abstract class AbstractCursor implements Cursor {
       if (v == null) {
         return null;
       }
-      return longToTimestamp(v.longValue(), calendar);
+      return DateTimeUtils.unixTimestampToSqlTimestamp(v.longValue(), 
calendar);
     }
 
     @Override public Date getDate(Calendar calendar) throws SQLException {
@@ -1051,9 +1044,7 @@ public abstract class AbstractCursor implements Cursor {
       if (timestamp == null) {
         return null;
       }
-      return new Time(
-          Math.floorMod(timestamp.getTime(),
-              DateTimeUtils.MILLIS_PER_DAY));
+      return new Time(timestamp.getTime());
     }
 
     @Override public String getString() throws SQLException {
@@ -1080,10 +1071,19 @@ public abstract class AbstractCursor implements Cursor {
    * Accessor that assumes that the underlying value is a DATE,
    * represented as a java.sql.Date;
    * corresponds to {@link java.sql.Types#DATE}.
+   *
+   * <p>Since the underlying value does not provide a timezone, timezone 
offsets are calculated in
+   * relation to UTC. Passing in the default calendar will convert the date 
from UTC to local time.
+   * This differs from {@link DateFromNumberAccessor} which calculates 
timezone offsets in relation
+   * to local time, and {@link Date} which provides its value in local time 
unless otherwise
+   * specified.
    */
-  private static class DateAccessor extends ObjectAccessor {
-    private DateAccessor(Getter getter) {
+  static class DateAccessor extends ObjectAccessor {
+    private final Calendar localCalendar;
+
+    DateAccessor(Getter getter, Calendar localCalendar) {
       super(getter);
+      this.localCalendar = localCalendar;
     }
 
     @Override public Date getDate(Calendar calendar) throws SQLException {
@@ -1100,11 +1100,13 @@ public abstract class AbstractCursor implements Cursor {
     }
 
     @Override public String getString() throws SQLException {
-      final int v = getInt();
-      if (v == 0 && wasNull()) {
+      final java.sql.Date date = getDate(null);
+      if (date == null) {
         return null;
       }
-      return dateAsString(v, null);
+      final int unix = DateTimeUtils.sqlDateToUnixDate(date, localCalendar);
+      return dateAsString(unix, null);
+
     }
 
     @Override public long getLong() throws SQLException {
@@ -1119,10 +1121,19 @@ public abstract class AbstractCursor implements Cursor {
    * Accessor that assumes that the underlying value is a TIME,
    * represented as a java.sql.Time;
    * corresponds to {@link java.sql.Types#TIME}.
+   *
+   * <p>Since the underlying value does not provide a timezone, timezone 
offsets are calculated in
+   * relation to UTC. Passing in the default calendar will convert the time 
from UTC to local time.
+   * This differs from {@link TimeFromNumberAccessor} which calculates 
timezone offsets in relation
+   * to local time, and {@link Time} which provides its value in local time 
unless otherwise
+   * specified.
    */
-  private static class TimeAccessor extends ObjectAccessor {
-    private TimeAccessor(Getter getter) {
+  static class TimeAccessor extends ObjectAccessor {
+    private final Calendar localCalendar;
+
+    TimeAccessor(Getter getter, Calendar localCalendar) {
       super(getter);
+      this.localCalendar = localCalendar;
     }
 
     @Override public Time getTime(Calendar calendar) throws SQLException {
@@ -1139,11 +1150,12 @@ public abstract class AbstractCursor implements Cursor {
     }
 
     @Override public String getString() throws SQLException {
-      final int v = getInt();
-      if (v == 0 && wasNull()) {
+      final Time time = getTime(null);
+      if (time == null) {
         return null;
       }
-      return timeAsString(v, null);
+      final int unix = DateTimeUtils.sqlTimeToUnixTime(time, localCalendar);
+      return timeAsString(unix, null);
     }
 
     @Override public long getLong() throws SQLException {
@@ -1157,10 +1169,19 @@ public abstract class AbstractCursor implements Cursor {
    * Accessor that assumes that the underlying value is a TIMESTAMP,
    * represented as a java.sql.Timestamp;
    * corresponds to {@link java.sql.Types#TIMESTAMP}.
+   *
+   * <p>Since the underlying value does not provide a timezone, timezone 
offsets are calculated in
+   * relation to UTC. Passing in the default calendar will convert the 
timestamp from UTC to local
+   * time. This differs from {@link TimeFromNumberAccessor} which calculates 
timezone offsets in
+   * relation to local time, and {@link Timestamp} which provides its value in 
local time unless
+   * otherwise specified.
    */
-  private static class TimestampAccessor extends ObjectAccessor {
-    private TimestampAccessor(Getter getter) {
+  static class TimestampAccessor extends ObjectAccessor {
+    private final Calendar localCalendar;
+
+    TimestampAccessor(Getter getter, Calendar localCalendar) {
       super(getter);
+      this.localCalendar = localCalendar;
     }
 
     @Override public Timestamp getTimestamp(Calendar calendar) throws 
SQLException {
@@ -1189,17 +1210,17 @@ public abstract class AbstractCursor implements Cursor {
       if (timestamp == null) {
         return null;
       }
-      return new Time(
-          Math.floorMod(timestamp.getTime(),
-              DateTimeUtils.MILLIS_PER_DAY));
+      return new Time(timestamp.getTime());
     }
 
     @Override public String getString() throws SQLException {
-      final long v = getLong();
-      if (v == 0 && wasNull()) {
+      final Timestamp timestamp = getTimestamp(null);
+      if (timestamp == null) {
         return null;
       }
-      return timestampAsString(v, null);
+      final long unix =
+          DateTimeUtils.sqlTimestampToUnixTimestamp(timestamp, localCalendar);
+      return timestampAsString(unix, null);
     }
 
     @Override public long getLong() throws SQLException {
@@ -1212,11 +1233,16 @@ public abstract class AbstractCursor implements Cursor {
    * Accessor that assumes that the underlying value is a TIMESTAMP,
    * represented as a java.util.Date;
    * corresponds to {@link java.sql.Types#TIMESTAMP}.
+   *
+   * <p>Since the underlying value does not provide a timezone, timezone 
offsets are calculated in
+   * relation to UTC. Passing in the default calendar will convert the 
timestamp from UTC to local
+   * time. This differs from {@link java.util.Date} which provides its value 
in local time unless
+   * otherwise specified.
    */
-  private static class TimestampFromUtilDateAccessor extends ObjectAccessor {
+  static class TimestampFromUtilDateAccessor extends ObjectAccessor {
     private final Calendar localCalendar;
 
-    private TimestampFromUtilDateAccessor(Getter getter,
+    TimestampFromUtilDateAccessor(Getter getter,
         Calendar localCalendar) {
       super(getter);
       this.localCalendar = localCalendar;
@@ -1247,17 +1273,16 @@ public abstract class AbstractCursor implements Cursor {
       if (timestamp == null) {
         return null;
       }
-      return new Time(
-          Math.floorMod(timestamp.getTime(),
-              DateTimeUtils.MILLIS_PER_DAY));
+      return new Time(timestamp.getTime());
     }
 
     @Override public String getString() throws SQLException {
-      java.util.Date date  = (java.util.Date) getObject();
+      final java.util.Date date = (java.util.Date) getObject();
       if (date == null) {
         return null;
       }
-      return timestampAsString(date.getTime(), null);
+      final long unix = DateTimeUtils.utilDateToUnixTimestamp(date, 
localCalendar);
+      return timestampAsString(unix, null);
     }
 
     @Override public long getLong() throws SQLException {
diff --git 
a/core/src/main/java/org/apache/calcite/avatica/util/DateTimeUtils.java 
b/core/src/main/java/org/apache/calcite/avatica/util/DateTimeUtils.java
index 64db2e95a..144d024ce 100644
--- a/core/src/main/java/org/apache/calcite/avatica/util/DateTimeUtils.java
+++ b/core/src/main/java/org/apache/calcite/avatica/util/DateTimeUtils.java
@@ -16,10 +16,16 @@
  */
 package org.apache.calcite.avatica.util;
 
+import java.sql.Time;
+import java.sql.Timestamp;
 import java.text.DateFormat;
 import java.text.NumberFormat;
 import java.text.ParsePosition;
 import java.text.SimpleDateFormat;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.ZoneOffset;
+import java.time.temporal.ChronoField;
 import java.util.Calendar;
 import java.util.Date;
 import java.util.Locale;
@@ -91,6 +97,11 @@ public class DateTimeUtils {
    */
   public static final long SECONDS_PER_DAY = 86_400; // = 24 * 60 * 60
 
+  /**
+   * The number of nanoseconds in a millisecond.
+   */
+  public static final long NANOS_PER_MILLI = 1000000L;
+
   /**
    * Calendar set to the epoch (1970-01-01 00:00:00 UTC). Useful for
    * initializing other values. Calendars are not immutable, so be careful not
@@ -1160,6 +1171,235 @@ public class DateTimeUtils {
     return OFFSET_DATE_TIME_HANDLER.stringValue(o);
   }
 
+  /**
+   * Calculates the unix date as the number of days since January 1st, 1970 
UTC for the given SQL
+   * date.
+   *
+   * @see #sqlDateToUnixDate(java.sql.Date, TimeZone)
+   */
+  public static int sqlDateToUnixDate(java.sql.Date date, Calendar calendar) {
+    return sqlDateToUnixDate(date, calendar != null ? calendar.getTimeZone() : 
null);
+  }
+
+  /**
+   * Calculates the unix date as the number of days since January 1st, 1970 
UTC for the given SQL
+   * date.
+   *
+   * <p>The {@link java.sql.Date} class uses the standard Gregorian calendar 
which switches from
+   * the Julian calendar to the Gregorian calendar in October 1582. For 
compatibility with
+   * ISO-8601, the value is converted to a {@link LocalDate} which uses the 
proleptic Gregorian
+   * calendar.
+   *
+   * <p>For backwards compatibility, timezone offsets are calculated in 
relation to the local
+   * timezone instead of UTC. Providing the default timezone or {@code null} 
will return the date
+   * unmodified.
+   *
+   * <p>If the date contains a partial day, it will be rounded to a full day 
depending on the
+   * milliseconds value. If the milliseconds value is positive, it will be 
rounded down to the
+   * closest full day. If the milliseconds value is negative, it will be 
rounded up to the closest
+   * full day.
+   */
+  public static int sqlDateToUnixDate(java.sql.Date date, TimeZone timeZone) {
+    final long time = date.getTime();
+
+    // Convert from standard Gregorian calendar to ISO calendar system
+    // Use a SQL timestamp to include the time offset from UTC in the unix 
timestamp
+    final LocalDateTime dateTime = new Timestamp(time).toLocalDateTime();
+    long unixTimestamp = dateTime.toEpochSecond(ZoneOffset.UTC)
+        * DateTimeUtils.MILLIS_PER_SECOND
+        + dateTime.get(ChronoField.MILLI_OF_SECOND);
+
+    // Calculate timezone offset in relation to local time
+    if (timeZone != null) {
+      unixTimestamp += timeZone.getOffset(time);
+    }
+    unixTimestamp -= DEFAULT_ZONE.getOffset(time);
+
+    return (int) (unixTimestamp / DateTimeUtils.MILLIS_PER_DAY);
+  }
+
+  /**
+   * Converts the given unix date to a SQL date.
+   *
+   * <p>The unix date should be the number of days since January 1st, 1970 UTC 
using the proleptic
+   * Gregorian calendar as defined by ISO-8601. The returned {@link 
java.sql.Date} object will use
+   * the standard Gregorian calendar which switches from the Julian calendar 
to the Gregorian
+   * calendar in October 1582. This conversion is handled by the {@link 
java.sql.Date} class when
+   * converting from a {@link LocalDate} object.
+   *
+   * <p>For backwards compatibility, timezone offsets are calculated in 
relation to the local
+   * timezone instead of UTC. Providing the default timezone or {@code null} 
will return the date
+   * unmodified.
+   */
+  public static java.sql.Date unixDateToSqlDate(int date, Calendar calendar) {
+    // Convert unix date from the ISO calendar system to the standard 
Gregorian calendar
+    final LocalDate localDate = LocalDate.ofEpochDay(date);
+    final java.sql.Date sqlDate = java.sql.Date.valueOf(localDate);
+
+    // Calculate timezone offset in relation to local time
+    final long time = sqlDate.getTime();
+    final int offset = calendar != null ? 
calendar.getTimeZone().getOffset(time) : 0;
+    sqlDate.setTime(time + DEFAULT_ZONE.getOffset(time) - offset);
+
+    return sqlDate;
+  }
+
+  /**
+   * Calculates the unix date as the number of milliseconds since January 1st, 
1970 UTC for the
+   * given date.
+   *
+   * @see #utilDateToUnixTimestamp(Date, TimeZone)
+   */
+  public static long utilDateToUnixTimestamp(Date date, Calendar calendar) {
+    return utilDateToUnixTimestamp(date, calendar != null ? 
calendar.getTimeZone() : null);
+  }
+
+  /**
+   * Calculates the unix date as the number of milliseconds since January 1st, 
1970 UTC for the
+   * given date.
+   *
+   * <p>The {@link Date} class uses the standard Gregorian calendar which 
switches from the Julian
+   * calendar to the Gregorian calendar in October 1582. For compatibility 
with ISO-8601, the value
+   * is converted to a {@code java.time} object which uses the proleptic 
Gregorian calendar.
+   *
+   * <p>For backwards compatibility, timezone offsets are calculated in 
relation to the local
+   * timezone instead of UTC. Providing the default timezone or {@code null} 
will return the date
+   * unmodified.
+   */
+  public static long utilDateToUnixTimestamp(Date date, TimeZone timeZone) {
+    final Timestamp timestamp = new Timestamp(date.getTime());
+    return sqlTimestampToUnixTimestamp(timestamp, timeZone);
+  }
+
+  /**
+   * Converts the given unix timestamp to a Java date.
+   *
+   * <p>The unix timestamp should be the number of milliseconds since January 
1st, 1970 UTC using
+   * the proleptic Gregorian calendar as defined by ISO-8601. The returned 
{@link Date} object will
+   * use the standard Gregorian calendar which switches from the Julian 
calendar to the Gregorian
+   * calendar in October 1582. This conversion is handled by the {@code 
java.sql} package when
+   * converting from a {@code java.time} object.
+   *
+   * <p>For backwards compatibility, timezone offsets are calculated in 
relation to the local
+   * timezone instead of UTC. Providing the default timezone or {@code null} 
will return the date
+   * unmodified.
+   */
+  public static Date unixTimestampToUtilDate(long timestamp, Calendar 
calendar) {
+    final Timestamp sqlTimestamp = unixTimestampToSqlTimestamp(timestamp, 
calendar);
+    return new Date(sqlTimestamp.getTime());
+  }
+
+  /**
+   * Calculates the unix time as the number of milliseconds since the previous 
day in UTC for the
+   * given SQL time.
+   *
+   * @see #sqlTimeToUnixTime(Time, TimeZone)
+   */
+  public static int sqlTimeToUnixTime(Time time, Calendar calendar) {
+    return sqlTimeToUnixTime(time, calendar != null ? calendar.getTimeZone() : 
null);
+  }
+
+  /**
+   * Calculates the unix time as the number of milliseconds since the previous 
day in UTC for the
+   * given SQL time.
+   *
+   * <p>For backwards compatibility, timezone offsets are calculated in 
relation to the local
+   * timezone instead of UTC. Providing the default timezone or {@code null} 
will return the time
+   * unmodified.
+   */
+  public static int sqlTimeToUnixTime(Time time, TimeZone timeZone) {
+    long unixTime = time.getTime();
+    if (timeZone != null) {
+      unixTime += timeZone.getOffset(unixTime);
+    }
+    return (int) Math.floorMod(unixTime, MILLIS_PER_DAY);
+  }
+
+  /**
+   * Converts the given unix time to a SQL time.
+   *
+   * <p>The unix time should be the number of milliseconds since the previous 
day in UTC.
+   *
+   * <p>For backwards compatibility, timezone offsets are calculated in 
relation to the local
+   * timezone instead of UTC. Providing the default timezone or {@code null} 
will return the time
+   * unmodified.
+   */
+  public static Time unixTimeToSqlTime(int time, Calendar calendar) {
+    if (calendar != null) {
+      time -= calendar.getTimeZone().getOffset(time);
+    }
+    return new Time(time);
+  }
+
+  /**
+   * Calculates the unix date as the number of milliseconds since January 1st, 
1970 UTC for the
+   * given SQL timestamp.
+   *
+   * @see #sqlTimestampToUnixTimestamp(Timestamp, TimeZone)
+   */
+  public static long sqlTimestampToUnixTimestamp(Timestamp timestamp, Calendar 
calendar) {
+    return sqlTimestampToUnixTimestamp(timestamp, calendar != null ? 
calendar.getTimeZone() : null);
+  }
+
+  /**
+   * Calculates the unix date as the number of milliseconds since January 1st, 
1970 UTC for the
+   * given SQL timestamp.
+   *
+   * <p>The {@link Timestamp} class uses the standard Gregorian calendar which 
switches from the
+   * Julian calendar to the Gregorian calendar in October 1582. For 
compatibility with ISO-8601,
+   * the value is converted to a {@link LocalDateTime} which uses the 
proleptic Gregorian calendar.
+   *
+   * <p>For backwards compatibility, timezone offsets are calculated in 
relation to the local
+   * timezone instead of UTC. Providing the default timezone or {@code null} 
will return the
+   * timestamp unmodified.
+   */
+  public static long sqlTimestampToUnixTimestamp(Timestamp timestamp, TimeZone 
timeZone) {
+    final long time = timestamp.getTime();
+
+    // Convert SQL timestamp from standard Gregorian calendar to ISO calendar 
system
+    final LocalDateTime dateTime = timestamp.toLocalDateTime();
+    long unixTimestamp = dateTime.toEpochSecond(ZoneOffset.UTC)
+        * DateTimeUtils.MILLIS_PER_SECOND
+        + dateTime.get(ChronoField.MILLI_OF_SECOND);
+
+    // Calculate timezone offset in relation to local time
+    if (timeZone != null) {
+      unixTimestamp += timeZone.getOffset(time);
+    }
+    unixTimestamp -= DEFAULT_ZONE.getOffset(time);
+
+    return unixTimestamp;
+  }
+
+  /**
+   * Converts the given unix timestamp to a SQL timestamp.
+   *
+   * <p>The unix timestamp should be the number of milliseconds since January 
1st, 1970 UTC using
+   * the proleptic Gregorian calendar as defined by ISO-8601. The returned 
{@link Timestamp} object
+   * will use the standard Gregorian calendar which switches from the Julian 
calendar to the
+   * Gregorian calendar in October 1582. This conversion is handled by the 
{@link Timestamp} class
+   * when converting from a {@link LocalDateTime} object.
+   *
+   * <p>For backwards compatibility, timezone offsets are calculated in 
relation to the local
+   * timezone instead of UTC. Providing the default timezone or {@code null} 
will return the
+   * timestamp unmodified.
+   */
+  public static Timestamp unixTimestampToSqlTimestamp(long timestamp, Calendar 
calendar) {
+    // Convert unix timestamp from the ISO calendar system to the standard 
Gregorian calendar
+    final LocalDateTime localDateTime = LocalDateTime.ofEpochSecond(
+        Math.floorDiv(timestamp, DateTimeUtils.MILLIS_PER_SECOND),
+        (int) (Math.floorMod(timestamp, DateTimeUtils.MILLIS_PER_SECOND) * 
NANOS_PER_MILLI),
+        ZoneOffset.UTC);
+    final Timestamp sqlTimestamp = Timestamp.valueOf(localDateTime);
+
+    // Calculate timezone offset in relation to local time
+    final long time = sqlTimestamp.getTime();
+    final int offset = calendar != null ? 
calendar.getTimeZone().getOffset(time) : 0;
+    sqlTimestamp.setTime(time + DEFAULT_ZONE.getOffset(time) - offset);
+
+    return sqlTimestamp;
+  }
+
   //~ Inner Classes ----------------------------------------------------------
 
   /**
diff --git 
a/core/src/test/java/org/apache/calcite/avatica/AvaticaResultSetConversionsTest.java
 
b/core/src/test/java/org/apache/calcite/avatica/AvaticaResultSetConversionsTest.java
index b8df74e0a..b7645b74d 100644
--- 
a/core/src/test/java/org/apache/calcite/avatica/AvaticaResultSetConversionsTest.java
+++ 
b/core/src/test/java/org/apache/calcite/avatica/AvaticaResultSetConversionsTest.java
@@ -1088,9 +1088,7 @@ public class AvaticaResultSetConversionsTest {
     }
 
     @Override public void testGetTime(ResultSet resultSet, Calendar calendar) 
throws SQLException {
-      // how come both are different? DST...
-      //assertEquals(new Time(1476130718123L), g.getTime(label, calendar));
-      assertEquals(new Time(73118123L), g.getTime(resultSet, calendar));
+      assertEquals(new Time(1476130718123L), g.getTime(resultSet, calendar));
     }
 
     @Override public void testGetTimestamp(ResultSet resultSet, Calendar 
calendar)
diff --git 
a/core/src/test/java/org/apache/calcite/avatica/remote/TypedValueTest.java 
b/core/src/test/java/org/apache/calcite/avatica/remote/TypedValueTest.java
index 3c0575e10..335fa8a1a 100644
--- a/core/src/test/java/org/apache/calcite/avatica/remote/TypedValueTest.java
+++ b/core/src/test/java/org/apache/calcite/avatica/remote/TypedValueTest.java
@@ -32,10 +32,15 @@ import org.junit.Test;
 
 import java.math.BigDecimal;
 import java.sql.Array;
+import java.sql.Date;
+import java.sql.Time;
+import java.sql.Timestamp;
 import java.sql.Types;
 import java.util.Arrays;
 import java.util.Calendar;
 import java.util.List;
+import java.util.Locale;
+import java.util.TimeZone;
 
 import static org.hamcrest.CoreMatchers.instanceOf;
 import static org.hamcrest.CoreMatchers.is;
@@ -143,22 +148,50 @@ public class TypedValueTest {
   @Test public void testSqlDate() {
     // days since epoch
     serializeAndEqualityCheck(TypedValue.ofLocal(Rep.JAVA_SQL_DATE, 25));
+
+    // From JDBC to local
+    final Calendar calendar = Calendar.getInstance(TimeZone.getDefault(), 
Locale.ROOT);
+    final TypedValue value =
+        TypedValue.ofJdbc(Rep.JAVA_SQL_DATE, Date.valueOf("1500-04-30"), 
calendar);
+    assertThat(value.value, is(-171545));
   }
 
   @Test public void testUtilDate() {
-    serializeAndEqualityCheck(
-        TypedValue.ofLocal(Rep.JAVA_UTIL_DATE, System.currentTimeMillis()));
+    final long time = System.currentTimeMillis();
+    serializeAndEqualityCheck(TypedValue.ofLocal(Rep.JAVA_UTIL_DATE, time));
+
+    // From JDBC to local
+    final Calendar calendar = Calendar.getInstance(TimeZone.getDefault(), 
Locale.ROOT);
+    final TypedValue value = TypedValue.ofJdbc(
+        Rep.JAVA_UTIL_DATE,
+        new Timestamp(time - calendar.getTimeZone().getOffset(time)),
+        calendar);
+    assertThat(value.value, is(time));
   }
 
   @Test public void testSqlTime() {
     // millis since epoch
     serializeAndEqualityCheck(
         TypedValue.ofLocal(Rep.JAVA_SQL_TIME, 42 * 1024 * 1024));
+
+    // From JDBC to local
+    final Calendar calendar = Calendar.getInstance(TimeZone.getDefault(), 
Locale.ROOT);
+    final TypedValue value =
+        TypedValue.ofJdbc(Rep.JAVA_SQL_TIME, Time.valueOf("00:00:00"), 
calendar);
+    assertThat(value.value, is(0));
   }
 
   @Test public void testSqlTimestamp() {
     serializeAndEqualityCheck(
         TypedValue.ofLocal(Rep.JAVA_SQL_TIMESTAMP, 42L * 1024 * 1024 * 1024));
+
+    // From JDBC to local
+    final Calendar calendar = Calendar.getInstance(TimeZone.getDefault(), 
Locale.ROOT);
+    final TypedValue value = TypedValue.ofJdbc(
+        Rep.JAVA_SQL_TIMESTAMP,
+        Timestamp.valueOf("1500-04-30 15:28:27.356"),
+        calendar);
+    assertThat(value.value, is(-14821432292644L));
   }
 
   @Test public void testLegacyDecimalParsing() {
diff --git 
a/core/src/test/java/org/apache/calcite/avatica/util/DateAccessorTest.java 
b/core/src/test/java/org/apache/calcite/avatica/util/DateAccessorTest.java
new file mode 100644
index 000000000..d53976c1d
--- /dev/null
+++ b/core/src/test/java/org/apache/calcite/avatica/util/DateAccessorTest.java
@@ -0,0 +1,155 @@
+/*
+ * 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.avatica.util;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.sql.Date;
+import java.sql.SQLException;
+import java.util.Calendar;
+import java.util.Locale;
+import java.util.TimeZone;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+/**
+ * Test conversions from SQL {@link java.sql.Date} to JDBC types in
+ * {@link AbstractCursor.DateAccessor}.
+ */
+public class DateAccessorTest {
+
+  private static final Calendar UTC =
+      Calendar.getInstance(TimeZone.getTimeZone("UTC"), Locale.ROOT);
+
+  private Cursor.Accessor instance;
+  private Calendar localCalendar;
+  private Date value;
+
+  /**
+   * Setup test environment by creating a {@link AbstractCursor.DateAccessor} 
that reads from the
+   * instance variable {@code value}.
+   */
+  @Before public void before() {
+    final AbstractCursor.Getter getter = new LocalGetter();
+    localCalendar = Calendar.getInstance(TimeZone.getDefault(), Locale.ROOT);
+    instance = new AbstractCursor.DateAccessor(getter, localCalendar);
+  }
+
+  /**
+   * Test {@code getDate()} returns the same value as the input date.
+   */
+  @Test public void testDate() throws SQLException {
+    value = new Date(0L);
+    assertThat(instance.getDate(null), is(value));
+
+    value = Date.valueOf("1970-01-01");
+    assertThat(instance.getDate(UTC), is(value));
+
+    value = Date.valueOf("1500-04-30");
+    assertThat(instance.getDate(UTC), is(value));
+  }
+
+  /**
+   * Test {@code getDate()} handles time zone conversions relative to the 
local calendar and not
+   * UTC.
+   */
+  @Test public void testDateWithCalendar() throws SQLException {
+    value = new Date(0L);
+
+    final TimeZone minusFiveZone = TimeZone.getTimeZone("GMT-5:00");
+    final Calendar minusFiveCal = Calendar.getInstance(minusFiveZone, 
Locale.ROOT);
+    assertThat(instance.getDate(minusFiveCal).getTime(),
+        is(5 * DateTimeUtils.MILLIS_PER_HOUR));
+
+    final TimeZone plusFiveZone = TimeZone.getTimeZone("GMT+5:00");
+    final Calendar plusFiveCal = Calendar.getInstance(plusFiveZone, 
Locale.ROOT);
+    assertThat(instance.getDate(plusFiveCal).getTime(),
+        is(-5 * DateTimeUtils.MILLIS_PER_HOUR));
+  }
+
+  /**
+   * Test {@code getString()} supports date range 0001-01-01 to 9999-12-31 
required by ANSI SQL.
+   *
+   * <p>This test only uses the UTC time zone because some time zones don't 
have a January 1st
+   * 12:00am for every year.
+   */
+  @Test public void testStringWithAnsiDateRange() throws SQLException {
+    localCalendar.setTimeZone(UTC.getTimeZone());
+
+    final Calendar utcCal = (Calendar) UTC.clone();
+    utcCal.set(1, Calendar.JANUARY, 1, 0, 0, 0);
+    utcCal.set(Calendar.MILLISECOND, 0);
+
+    for (int i = 2; i <= 9999; ++i) {
+      utcCal.set(Calendar.YEAR, i);
+      value = new Date(utcCal.getTimeInMillis());
+      assertThat(instance.getString(),
+          is(String.format(Locale.ROOT, "%04d-01-01", i)));
+    }
+  }
+
+  /**
+   * Test {@code getString()} returns the same value as the input date.
+   */
+  @Test public void testStringWithLocalTimeZone() throws SQLException {
+    value = Date.valueOf("1970-01-01");
+    assertThat(instance.getString(), is("1970-01-01"));
+
+    value = Date.valueOf("1500-04-30");
+    assertThat(instance.getString(), is("1500-04-30"));
+  }
+
+  /**
+   * Test {@code getString()} returns dates relative to the local calendar.
+   */
+  @Test public void testStringWithUtc() throws SQLException {
+    localCalendar.setTimeZone(UTC.getTimeZone());
+
+    value = new Date(0L);
+    assertThat(instance.getString(), is("1970-01-01"));
+
+    value = new Date(-14820624000000L /* 1500-04-30 */);
+    assertThat(instance.getString(), is("1500-04-30"));
+  }
+
+  /**
+   * Test {@code getLong()} returns the same value as the input date.
+   */
+  @Test public void testLong() throws SQLException {
+    value = new Date(0L);
+    assertThat(instance.getLong(), is(0L));
+
+    value = Date.valueOf("1500-04-30");
+    final Date longDate = new Date(instance.getLong() * 
DateTimeUtils.MILLIS_PER_DAY);
+    assertThat(longDate.toString(), is("1500-04-30"));
+  }
+
+  /**
+   * Returns the value from the test instance to the accessor.
+   */
+  private class LocalGetter implements AbstractCursor.Getter {
+    @Override public Object getObject() {
+      return value;
+    }
+
+    @Override public boolean wasNull() {
+      return value == null;
+    }
+  }
+}
diff --git 
a/core/src/test/java/org/apache/calcite/avatica/util/DateFromNumberAccessorTest.java
 
b/core/src/test/java/org/apache/calcite/avatica/util/DateFromNumberAccessorTest.java
new file mode 100644
index 000000000..eebbd93e0
--- /dev/null
+++ 
b/core/src/test/java/org/apache/calcite/avatica/util/DateFromNumberAccessorTest.java
@@ -0,0 +1,254 @@
+/*
+ * 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.avatica.util;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.sql.Date;
+import java.sql.SQLException;
+import java.sql.Timestamp;
+import java.util.Calendar;
+import java.util.Locale;
+import java.util.SimpleTimeZone;
+import java.util.TimeZone;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+/**
+ * Test conversions from SQL DATE as the number of days since 1970-01-01 to 
JDBC types in
+ * {@link AbstractCursor.DateFromNumberAccessor}.
+ */
+public class DateFromNumberAccessorTest {
+
+  private Cursor.Accessor instance;
+  private Calendar localCalendar;
+  private Object value;
+
+  /**
+   * Setup test environment by creating a {@link 
AbstractCursor.DateFromNumberAccessor} that reads
+   * from the instance variable {@code value}.
+   */
+  @Before public void before() {
+    final AbstractCursor.Getter getter = new LocalGetter();
+    localCalendar = Calendar.getInstance(TimeZone.getDefault(), Locale.ROOT);
+    instance = new AbstractCursor.DateFromNumberAccessor(getter,
+        localCalendar);
+  }
+
+  /**
+   * Test {@code getDate()} returns the same value as the input date for the 
local calendar.
+   */
+  @Test public void testDateWithLocalTimeZone() throws SQLException {
+    value = 0;
+    assertThat(instance.getDate(localCalendar), 
is(Date.valueOf("1970-01-01")));
+
+    value = DateTimeUtils.dateStringToUnixDate("1500-04-30");
+    assertThat(instance.getDate(localCalendar), 
is(Date.valueOf("1500-04-30")));
+  }
+
+  /**
+   * Test {@code getDate()} shifts between the standard Gregorian calendar and 
the proleptic
+   * Gregorian calendar.
+   */
+  @Test public void testDateWithGregorianShift() throws SQLException {
+    value = DateTimeUtils.dateStringToUnixDate("1582-10-04");
+    assertThat(instance.getDate(localCalendar), 
is(Date.valueOf("1582-10-04")));
+
+    value = DateTimeUtils.dateStringToUnixDate("1582-10-05");
+    assertThat(instance.getDate(localCalendar), 
is(Date.valueOf("1582-10-15")));
+
+    value = DateTimeUtils.dateStringToUnixDate("1582-10-15");
+    assertThat(instance.getDate(localCalendar), 
is(Date.valueOf("1582-10-15")));
+  }
+
+  /**
+   * Test {@code getDate()} supports date range 0001-01-01 to 9999-12-31 
required by ANSI SQL.
+   */
+  @Test public void testDateWithAnsiDateRange() throws SQLException {
+    for (int i = 1; i <= 9999; ++i) {
+      final String str = String.format(Locale.ROOT, "%04d-01-01", i);
+      value = DateTimeUtils.dateStringToUnixDate(str);
+      assertThat(instance.getDate(localCalendar), is(Date.valueOf(str)));
+    }
+  }
+
+  /**
+   * Test {@code getDate()} handles time zone conversions relative to the 
local calendar and not
+   * UTC.
+   */
+  @Test public void testDateWithCalendar() throws SQLException {
+    final int offset = localCalendar.getTimeZone().getOffset(0);
+    final TimeZone east = new SimpleTimeZone(
+        offset + (int) DateTimeUtils.MILLIS_PER_HOUR,
+        "EAST");
+    final TimeZone west = new SimpleTimeZone(
+        offset - (int) DateTimeUtils.MILLIS_PER_HOUR,
+        "WEST");
+
+    value = 0;
+    assertThat(instance.getDate(Calendar.getInstance(east, Locale.ROOT)),
+        is(Timestamp.valueOf("1969-12-31 23:00:00")));
+    assertThat(instance.getDate(Calendar.getInstance(west, Locale.ROOT)),
+        is(Timestamp.valueOf("1970-01-01 01:00:00")));
+  }
+
+  /**
+   * Test no time zone conversion occurs if the given calendar is {@code null}.
+   */
+  @Test public void testDateWithNullCalendar() throws SQLException {
+    value = 0;
+    assertThat(instance.getDate(null).getTime(),
+        is(0L));
+  }
+
+  /**
+   * Test {@code getDate()} when the local calendar is UTC, which may be 
different from the default
+   * time zone.
+   */
+  @Test public void testDateWithUtcLocalCalendar() throws SQLException {
+    localCalendar.setTimeZone(TimeZone.getTimeZone("UTC"));
+
+    value = 0;
+    assertThat(instance.getDate(localCalendar).getTime(), is(0L));
+
+    // Dates before the Gregorian cutoff should be returned using the standard 
Gregorian calendar
+    // that's used by java.sql.Date and java.util.Calendar
+    value = DateTimeUtils.dateStringToUnixDate("1500-04-30");
+    assertThat(instance.getDate(localCalendar).getTime(), is(-14820624000000L 
/* 1500-04-30 */));
+  }
+
+  /**
+   * Test {@code getString()} returns the same value as the input date.
+   */
+  @Test public void testString() throws SQLException {
+    value = 0;
+    assertThat(instance.getString(), is("1970-01-01"));
+
+    value = DateTimeUtils.dateStringToUnixDate("1500-04-30");
+    assertThat(instance.getString(), is("1500-04-30"));
+  }
+
+  /**
+   * Test {@code getString()} shifts between the standard Gregorian calendar 
and the proleptic
+   * Gregorian calendar.
+   */
+  @Test public void testStringWithGregorianShift() throws SQLException {
+    for (int i = 4; i <= 15; ++i) {
+      final String str = String.format(Locale.ROOT, "1582-10-%02d", i);
+      value = DateTimeUtils.dateStringToUnixDate(str);
+      assertThat(instance.getString(), is(str));
+    }
+  }
+
+  /**
+   * Test {@code getString()} supports date range 0001-01-01 to 9999-12-31 
required by ANSI SQL.
+   */
+  @Test public void testStringWithAnsiDateRange() throws SQLException {
+    for (int i = 1; i <= 9999; ++i) {
+      final String str = String.format(Locale.ROOT, "%04d-01-01", i);
+      value = DateTimeUtils.dateStringToUnixDate(str);
+      assertThat(instance.getString(), is(str));
+    }
+  }
+
+  /**
+   * Test {@code getTimestamp()} returns the same value as the input date.
+   */
+  @Test public void testTimestamp() throws SQLException {
+    value = 0;
+    assertThat(instance.getTimestamp(localCalendar),
+        is(Timestamp.valueOf("1970-01-01 00:00:00.0")));
+
+    value = DateTimeUtils.dateStringToUnixDate("1500-04-30");
+    assertThat(instance.getTimestamp(localCalendar),
+        is(Timestamp.valueOf("1500-04-30 00:00:00.0")));
+  }
+
+  /**
+   * Test {@code getTimestamp()} shifts between the standard Gregorian 
calendar and the proleptic
+   * Gregorian calendar.
+   */
+  @Test public void testTimestampWithGregorianShift() throws SQLException {
+    value = DateTimeUtils.dateStringToUnixDate("1582-10-04");
+    assertThat(instance.getTimestamp(localCalendar),
+        is(Timestamp.valueOf("1582-10-04 00:00:00.0")));
+
+    value = DateTimeUtils.dateStringToUnixDate("1582-10-05");
+    assertThat(instance.getTimestamp(localCalendar),
+        is(Timestamp.valueOf("1582-10-15 00:00:00.0")));
+
+    value = DateTimeUtils.dateStringToUnixDate("1582-10-15");
+    assertThat(instance.getTimestamp(localCalendar),
+        is(Timestamp.valueOf("1582-10-15 00:00:00.0")));
+  }
+
+  /**
+   * Test {@code getTimestamp()} supports date range 0001-01-01 to 9999-12-31 
required by ANSI SQL.
+   */
+  @Test public void testTimestampWithAnsiDateRange() throws SQLException {
+    for (int i = 1; i <= 9999; ++i) {
+      final String str = String.format(Locale.ROOT, "%04d-01-01", i);
+      value = DateTimeUtils.dateStringToUnixDate(str);
+      assertThat(instance.getTimestamp(localCalendar),
+          is(Timestamp.valueOf(str + " 00:00:00.0")));
+    }
+  }
+
+  /**
+   * Test {@code getTimestamp()} handles time zone conversions relative to the 
local calendar and
+   * not UTC.
+   */
+  @Test public void testTimestampWithCalendar() throws SQLException {
+    final int offset = localCalendar.getTimeZone().getOffset(0);
+    final TimeZone east = new SimpleTimeZone(
+        offset + (int) DateTimeUtils.MILLIS_PER_HOUR,
+        "EAST");
+    final TimeZone west = new SimpleTimeZone(
+        offset - (int) DateTimeUtils.MILLIS_PER_HOUR,
+        "WEST");
+
+    value = 0;
+    assertThat(instance.getTimestamp(Calendar.getInstance(east, Locale.ROOT)),
+        is(Timestamp.valueOf("1969-12-31 23:00:00.0")));
+    assertThat(instance.getTimestamp(Calendar.getInstance(west, Locale.ROOT)),
+        is(Timestamp.valueOf("1970-01-01 01:00:00.0")));
+  }
+
+  /**
+   * Test no time zone conversion occurs if the given calendar is {@code null}.
+   */
+  @Test public void testTimestampWithNullCalendar() throws SQLException {
+    value = 0;
+    assertThat(instance.getTimestamp(null).getTime(),
+        is(0L));
+  }
+
+  /**
+   * Returns the value from the test instance to the accessor.
+   */
+  private class LocalGetter implements AbstractCursor.Getter {
+    @Override public Object getObject() {
+      return value;
+    }
+
+    @Override public boolean wasNull() {
+      return value == null;
+    }
+  }
+}
diff --git 
a/core/src/test/java/org/apache/calcite/avatica/util/DateTimeUtilsTest.java 
b/core/src/test/java/org/apache/calcite/avatica/util/DateTimeUtilsTest.java
index bad4e51b7..acc08949e 100644
--- a/core/src/test/java/org/apache/calcite/avatica/util/DateTimeUtilsTest.java
+++ b/core/src/test/java/org/apache/calcite/avatica/util/DateTimeUtilsTest.java
@@ -19,30 +19,45 @@ package org.apache.calcite.avatica.util;
 import org.junit.Test;
 
 import java.math.BigDecimal;
+import java.sql.Time;
+import java.sql.Timestamp;
 import java.text.SimpleDateFormat;
 import java.time.LocalDate;
 import java.time.temporal.IsoFields;
 import java.util.Calendar;
+import java.util.Date;
 import java.util.Locale;
+import java.util.TimeZone;
 
 import static org.apache.calcite.avatica.util.DateTimeUtils.EPOCH_JULIAN;
+import static org.apache.calcite.avatica.util.DateTimeUtils.MILLIS_PER_DAY;
+import static org.apache.calcite.avatica.util.DateTimeUtils.MILLIS_PER_HOUR;
+import static org.apache.calcite.avatica.util.DateTimeUtils.MILLIS_PER_SECOND;
 import static org.apache.calcite.avatica.util.DateTimeUtils.addMonths;
 import static 
org.apache.calcite.avatica.util.DateTimeUtils.dateStringToUnixDate;
 import static org.apache.calcite.avatica.util.DateTimeUtils.digitCount;
 import static 
org.apache.calcite.avatica.util.DateTimeUtils.intervalDayTimeToString;
 import static 
org.apache.calcite.avatica.util.DateTimeUtils.intervalYearMonthToString;
+import static org.apache.calcite.avatica.util.DateTimeUtils.sqlDateToUnixDate;
+import static org.apache.calcite.avatica.util.DateTimeUtils.sqlTimeToUnixTime;
+import static 
org.apache.calcite.avatica.util.DateTimeUtils.sqlTimestampToUnixTimestamp;
 import static org.apache.calcite.avatica.util.DateTimeUtils.subtractMonths;
 import static 
org.apache.calcite.avatica.util.DateTimeUtils.timeStringToUnixDate;
 import static 
org.apache.calcite.avatica.util.DateTimeUtils.timestampStringToUnixDate;
 import static org.apache.calcite.avatica.util.DateTimeUtils.unixDateCeil;
 import static org.apache.calcite.avatica.util.DateTimeUtils.unixDateExtract;
 import static org.apache.calcite.avatica.util.DateTimeUtils.unixDateFloor;
+import static org.apache.calcite.avatica.util.DateTimeUtils.unixDateToSqlDate;
 import static org.apache.calcite.avatica.util.DateTimeUtils.unixDateToString;
 import static org.apache.calcite.avatica.util.DateTimeUtils.unixTimeExtract;
+import static org.apache.calcite.avatica.util.DateTimeUtils.unixTimeToSqlTime;
 import static org.apache.calcite.avatica.util.DateTimeUtils.unixTimeToString;
 import static org.apache.calcite.avatica.util.DateTimeUtils.unixTimestamp;
 import static 
org.apache.calcite.avatica.util.DateTimeUtils.unixTimestampExtract;
+import static 
org.apache.calcite.avatica.util.DateTimeUtils.unixTimestampToSqlTimestamp;
 import static 
org.apache.calcite.avatica.util.DateTimeUtils.unixTimestampToString;
+import static 
org.apache.calcite.avatica.util.DateTimeUtils.unixTimestampToUtilDate;
+import static 
org.apache.calcite.avatica.util.DateTimeUtils.utilDateToUnixTimestamp;
 import static org.apache.calcite.avatica.util.DateTimeUtils.ymdToJulian;
 import static org.apache.calcite.avatica.util.DateTimeUtils.ymdToUnixDate;
 
@@ -50,15 +65,17 @@ import static org.hamcrest.CoreMatchers.anyOf;
 import static org.hamcrest.CoreMatchers.equalTo;
 import static org.hamcrest.CoreMatchers.is;
 import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertSame;
-import static org.junit.Assert.assertThat;
 import static org.junit.Assert.assertTrue;
 
 /**
  * Tests for {@link DateTimeUtils}.
  */
 public class DateTimeUtilsTest {
+  private static final Calendar CALENDAR = 
Calendar.getInstance(TimeZone.getDefault(), Locale.ROOT);
+
   @Test public void testEasyLog10() {
     assertEquals(1, digitCount(0));
     assertEquals(1, digitCount(1));
@@ -1042,6 +1059,552 @@ public class DateTimeUtilsTest {
     // 1906/01/01 is a Monday.
     assertThat(unixDateFloor(TimeUnitRange.ISOYEAR, y1906), is(y1906));
   }
+
+  /**
+   * Test that a date in the local time zone converts to a unix timestamp in 
UTC.
+   */
+  @Test public void testSqlDateToUnixDateWithLocalTimeZone() {
+    assertThat(sqlDateToUnixDate(java.sql.Date.valueOf("1970-01-01"), 
CALENDAR), is(0));
+    assertThat(sqlDateToUnixDate(java.sql.Date.valueOf("1500-04-30"), 
CALENDAR),
+        is(dateStringToUnixDate("1500-04-30")));
+  }
+
+  /**
+   * Test rounding up dates before the unix epoch (1970-01-01).
+   *
+   * <p>Calcite depends on this rounding behaviour for some of its tests. This 
ensures that
+   * {@code new Date(0L)} will return "1970-01-01" regardless of the default 
timezone.
+   */
+  @Test public void testSqlDateToUnixDateRounding() {
+    final java.sql.Date partial = java.sql.Date.valueOf("1969-12-31");
+    partial.setTime(partial.getTime() + 1);
+    assertThat(sqlDateToUnixDate(partial, CALENDAR), is(0));
+  }
+
+  /**
+   * Test that no time zone conversion happens when the given calendar is 
{@code null}.
+   */
+  @Test public void testSqlDateToUnixDateWithNullCalendar() {
+    assertThat(sqlDateToUnixDate(new java.sql.Date(0L), (Calendar) null), 
is(0));
+    assertThat(sqlDateToUnixDate(new java.sql.Date(-MILLIS_PER_DAY), 
(Calendar) null), is(-1));
+
+    final Calendar julianCal = 
Calendar.getInstance(TimeZone.getTimeZone("UTC"), Locale.ROOT);
+    julianCal.set(1500, Calendar.APRIL, 30, 0, 0, 0);
+    julianCal.set(Calendar.MILLISECOND, 0);
+    assertThat(
+        sqlDateToUnixDate(new java.sql.Date(julianCal.getTimeInMillis()), 
(Calendar) null),
+        is(-171545 /* 1500-04-30 in ISO calendar */));
+  }
+
+  /**
+   * Test using a custom {@link Calendar} to calculate the unix timestamp. 
Dates created by a
+   * {@link Calendar} should be converted to a unix date in the given time 
zone. Dates created by a
+   * {@link java.sql.Date} method should be converted relative to the local 
time and not UTC.
+   */
+  @Test public void testSqlDateToUnixDateWithCustomCalendar() {
+    final java.sql.Date epoch = java.sql.Date.valueOf("1970-01-01");
+
+    final Calendar utcCal = Calendar.getInstance(TimeZone.getTimeZone("UTC"), 
Locale.ROOT);
+    utcCal.set(1970, Calendar.JANUARY, 1, 0, 0, 0);
+    utcCal.set(Calendar.MILLISECOND, 0);
+    assertThat(sqlDateToUnixDate(new java.sql.Date(utcCal.getTimeInMillis()), 
utcCal), is(0));
+
+    final TimeZone minusDayZone = TimeZone.getDefault();
+    minusDayZone.setRawOffset((int) (minusDayZone.getOffset(0L) - 
MILLIS_PER_DAY));
+    final Calendar minusDayCal = Calendar.getInstance(minusDayZone, 
Locale.ROOT);
+    assertThat(sqlDateToUnixDate(epoch, minusDayCal), is(-1));
+
+    final TimeZone plusDayZone = TimeZone.getDefault();
+    plusDayZone.setRawOffset((int) (plusDayZone.getOffset(0L) + 
MILLIS_PER_DAY));
+    final Calendar plusDayCal = Calendar.getInstance(plusDayZone, Locale.ROOT);
+    assertThat(sqlDateToUnixDate(epoch, plusDayCal), is(1));
+  }
+
+  /**
+   * Test calendar conversion from the standard Gregorian calendar used by 
{@code java.sql} and the
+   * proleptic Gregorian calendar used by unix timestamps.
+   */
+  @Test public void testSqlDateToUnixDateWithGregorianShift() {
+    assertThat(sqlDateToUnixDate(java.sql.Date.valueOf("1582-10-04"), 
CALENDAR),
+        is(dateStringToUnixDate("1582-10-04")));
+    assertThat(sqlDateToUnixDate(java.sql.Date.valueOf("1582-10-05"), 
CALENDAR),
+        is(dateStringToUnixDate("1582-10-15")));
+    assertThat(sqlDateToUnixDate(java.sql.Date.valueOf("1582-10-15"), 
CALENDAR),
+        is(dateStringToUnixDate("1582-10-15")));
+  }
+
+  /**
+   * Test date range 0001-01-01 to 9999-12-31 required by ANSI SQL.
+   *
+   * <p>Java may not be able to represent 0001-01-01 UTC depending on the 
default time zone. If the
+   * date would fall outside of Anno Domini (AD) when converted to the default 
time zone, that date
+   * should not be tested.
+   *
+   * <p>Not every time zone has a January 1st 12:00am, so this test only uses 
the UTC Time zone.
+   */
+  @Test public void testSqlDateToUnixDateWithAnsiDateRange() {
+    final Calendar ansiCal = Calendar.getInstance(TimeZone.getTimeZone("UTC"), 
Locale.ROOT);
+
+    // Test 0001-01-02 UTC to avoid issues with Java modifying 0001-01-01 when 
outside AD
+    ansiCal.set(1, Calendar.JANUARY, 2, 0, 0, 0);
+    ansiCal.set(Calendar.MILLISECOND, 0);
+    assertThat("Converts 0001-01-02 from SQL to unix date",
+        sqlDateToUnixDate(new java.sql.Date(ansiCal.getTimeInMillis()), 
ansiCal),
+        is(dateStringToUnixDate("0001-01-02")));
+
+    // Test remaining years 0002-01-01 to 9999-01-01
+    ansiCal.set(Calendar.DATE, 1);
+    for (int i = 2; i <= 9999; ++i) {
+      final String str = String.format(Locale.ROOT, "%04d-01-01", i);
+      ansiCal.set(Calendar.YEAR, i);
+      assertThat("Converts '" + str + "' from SQL to unix date",
+          sqlDateToUnixDate(new java.sql.Date(ansiCal.getTimeInMillis()), 
ansiCal),
+          is(dateStringToUnixDate(str)));
+    }
+
+    // Test end of range 9999-12-31
+    ansiCal.set(9999, Calendar.DECEMBER, 31, 0, 0, 0);
+    ansiCal.set(Calendar.MILLISECOND, 0);
+    assertThat("Converts 9999-12-31 from SQL to unix date",
+        sqlDateToUnixDate(new java.sql.Date(ansiCal.getTimeInMillis()), 
ansiCal),
+        is(dateStringToUnixDate("9999-12-31")));
+  }
+
+  /**
+   * Test that a unix timestamp converts to a date in the local time zone.
+   */
+  @Test public void testUnixDateToSqlDateWithLocalTimeZone() {
+    assertThat(unixDateToSqlDate(0, CALENDAR), 
is(java.sql.Date.valueOf("1970-01-01")));
+    assertThat(unixDateToSqlDate(dateStringToUnixDate("1500-04-30"), CALENDAR),
+        is(java.sql.Date.valueOf("1500-04-30")));
+  }
+
+  /**
+   * Test that no time zone conversion happens when the given calendar is 
{@code null}.
+   */
+  @Test public void testUnixDateToSqlDateWithNullCalendar() {
+    assertThat(unixDateToSqlDate(0, null), is(new java.sql.Date(0L)));
+    assertThat(unixDateToSqlDate(-1, null), is(new 
java.sql.Date(-MILLIS_PER_DAY)));
+
+    final Calendar julianCal = 
Calendar.getInstance(TimeZone.getTimeZone("UTC"), Locale.ROOT);
+    julianCal.set(1500, Calendar.APRIL, 30, 0, 0, 0);
+    julianCal.set(Calendar.MILLISECOND, 0);
+    assertThat(
+        unixDateToSqlDate(-171545 /* 1500-04-30 in ISO calendar */, null),
+        is(new java.sql.Date(julianCal.getTimeInMillis())));
+  }
+
+  /**
+   * Test using a custom {@link Calendar} to calculate the SQL date. Dates 
created by a
+   * {@link Calendar} should be converted to a SQL date in the given time 
zone. Otherwise, unix
+   * dates should be converted relative to the local time and not UTC.
+   */
+  @Test public void testUnixDateToSqlDateWithCustomCalendar() {
+    final Calendar utcCal = Calendar.getInstance(TimeZone.getTimeZone("UTC"), 
Locale.ROOT);
+    utcCal.set(1970, Calendar.JANUARY, 1, 0, 0, 0);
+    utcCal.set(Calendar.MILLISECOND, 0);
+    assertThat(unixDateToSqlDate(0, utcCal).getTime(), 
is(utcCal.getTimeInMillis()));
+
+    final TimeZone minusFiveZone = TimeZone.getDefault();
+    minusFiveZone.setRawOffset((int) (minusFiveZone.getOffset(0L) - 5 * 
MILLIS_PER_HOUR));
+    final Calendar minusFiveCal = Calendar.getInstance(minusFiveZone, 
Locale.ROOT);
+    assertThat(unixDateToSqlDate(0, minusFiveCal).toString(), 
is("1970-01-01"));
+
+    final TimeZone plusFiveZone = TimeZone.getDefault();
+    plusFiveZone.setRawOffset((int) (plusFiveZone.getOffset(0L) + 5 * 
MILLIS_PER_HOUR));
+    final Calendar plusFiveCal = Calendar.getInstance(plusFiveZone, 
Locale.ROOT);
+    assertThat(unixDateToSqlDate(0, plusFiveCal).toString(), is("1969-12-31"));
+  }
+
+  /**
+   * Test calendar conversion from the standard Gregorian calendar used by 
{@code java.sql} and the
+   * proleptic Gregorian calendar used by unix timestamps.
+   */
+  @Test public void testUnixDateToSqlDateWithGregorianShift() {
+    assertThat(unixDateToSqlDate(dateStringToUnixDate("1582-10-04"), CALENDAR),
+        is(java.sql.Date.valueOf("1582-10-04")));
+    assertThat(unixDateToSqlDate(dateStringToUnixDate("1582-10-05"), CALENDAR),
+        is(java.sql.Date.valueOf("1582-10-15")));
+    assertThat(unixDateToSqlDate(dateStringToUnixDate("1582-10-15"), CALENDAR),
+        is(java.sql.Date.valueOf("1582-10-15")));
+  }
+
+  /**
+   * Test date range 0001-01-01 to 9999-12-31 required by ANSI SQL.
+   *
+   * <p>Java may not be able to represent all dates depending on the default 
time zone, but both
+   * the expected and actual assertion values handles that in the same way.
+   */
+  @Test public void testUnixDateToSqlDateWithAnsiDateRange() {
+    for (int i = 1; i <= 9999; ++i) {
+      final String str = String.format(Locale.ROOT, "%04d-01-01", i);
+      assertThat(unixDateToSqlDate(dateStringToUnixDate(str), CALENDAR),
+          is(java.sql.Date.valueOf(str)));
+    }
+
+    assertThat(unixDateToSqlDate(dateStringToUnixDate("9999-12-31"), CALENDAR),
+        is(java.sql.Date.valueOf("9999-12-31")));
+  }
+
+  /**
+   * Test that a {@link java.util.Date} in the local time zone converts to a 
unix timestamp in UTC.
+   */
+  @Test public void testUtilDateToUnixTimestampWithLocalTimeZone() {
+    final Date unixEpoch = new Date(-CALENDAR.getTimeZone().getOffset(0L));
+    assertThat(utilDateToUnixTimestamp(unixEpoch, CALENDAR), is(0L));
+
+    final long currentTime = System.currentTimeMillis();
+    final Date currentDate = new Date(currentTime);
+    assertThat(utilDateToUnixTimestamp(currentDate, CALENDAR),
+        is(currentTime + CALENDAR.getTimeZone().getOffset(currentTime)));
+  }
+
+  /**
+   * Test that no time zone conversion happens when the given calendar is 
{@code null}.
+   */
+  @Test public void testUtilDateToUnixTimestampWithNullCalendar() {
+    assertThat(utilDateToUnixTimestamp(new Date(0L), (Calendar) null),
+        is(0L));
+
+    final Calendar julianCal = 
Calendar.getInstance(TimeZone.getTimeZone("UTC"), Locale.ROOT);
+    julianCal.set(1500, Calendar.APRIL, 30, 0, 0, 0);
+    julianCal.set(Calendar.MILLISECOND, 0);
+    assertThat(
+        utilDateToUnixTimestamp(new Date(julianCal.getTimeInMillis()), 
(Calendar) null),
+        is(-171545 * MILLIS_PER_DAY /* 1500-04-30 in ISO calendar */));
+  }
+
+  /**
+   * Test using a custom {@link Calendar} to calculate the unix timestamp. 
Dates created by a
+   * {@link Calendar} should be converted to a unix date in the given time 
zone.
+   */
+  @Test public void testUtilDateToUnixTimestampWithCustomCalendar() {
+    final Calendar utcCal = Calendar.getInstance(TimeZone.getTimeZone("UTC"), 
Locale.ROOT);
+    utcCal.set(1970, Calendar.JANUARY, 1, 0, 0, 0);
+    utcCal.set(Calendar.MILLISECOND, 0);
+    assertThat(utilDateToUnixTimestamp(new Date(utcCal.getTimeInMillis()), 
utcCal), is(0L));
+  }
+
+  /**
+   * Test that a unix timestamp converts to a date in the local time zone.
+   */
+  @Test public void testUnixTimestampToUtilDateWithLocalTimeZone() {
+    final Date unixEpoch = new Date(-CALENDAR.getTimeZone().getOffset(0L));
+    assertThat(unixTimestampToUtilDate(0L, CALENDAR), is(unixEpoch));
+
+    final long currentTime = System.currentTimeMillis();
+    final Date currentDate = new Date(currentTime);
+    assertThat(unixTimestampToUtilDate(
+            currentTime + CALENDAR.getTimeZone().getOffset(currentTime),
+            CALENDAR),
+        is(currentDate));
+  }
+
+  /**
+   * Test that no time zone conversion happens when the given calendar is 
{@code null}.
+   */
+  @Test public void testUnixTimestampToUtilDateWithNullCalendar() {
+    assertThat(unixTimestampToUtilDate(0L, null), is(new Date(0L)));
+
+    final Calendar julianCal = 
Calendar.getInstance(TimeZone.getTimeZone("UTC"), Locale.ROOT);
+    julianCal.set(1500, Calendar.APRIL, 30, 0, 0, 0);
+    julianCal.set(Calendar.MILLISECOND, 0);
+    assertThat(
+        unixTimestampToUtilDate(-171545 * MILLIS_PER_DAY /* 1500-04-30 in ISO 
calendar */, null),
+        is(new Date(julianCal.getTimeInMillis())));
+  }
+
+  /**
+   * Test using a custom {@link Calendar} to calculate the {@link Date}. Dates 
created by a
+   * {@link Calendar} should be converted to a {@link Date} in the given time 
zone.
+   */
+  @Test public void testUnixTimestampToUtilDateWithCustomCalendar() {
+    final Calendar utcCal = Calendar.getInstance(TimeZone.getTimeZone("UTC"), 
Locale.ROOT);
+    utcCal.set(1970, Calendar.JANUARY, 1, 0, 0, 0);
+    utcCal.set(Calendar.MILLISECOND, 0);
+    assertThat(unixTimestampToUtilDate(0, utcCal).getTime(), 
is(utcCal.getTimeInMillis()));
+  }
+
+  /**
+   * Test that a time in the local time zone converts to a unix time in UTC.
+   */
+  @Test public void testSqlTimeToUnixTimeWithLocalTimeZone() {
+    assertEquals(0, sqlTimeToUnixTime(Time.valueOf("00:00:00"), CALENDAR));
+    assertEquals(MILLIS_PER_DAY - MILLIS_PER_SECOND,
+        sqlTimeToUnixTime(Time.valueOf("23:59:59"), CALENDAR));
+  }
+
+  /**
+   * Test that no time zone conversion happens when the given calendar is 
{@code null}.
+   */
+  @Test public void testSqlTimeToUnixTimeWithNullCalendar() {
+    assertEquals(0L, sqlTimeToUnixTime(new Time(0L), (Calendar) null));
+
+    final long endOfDay = MILLIS_PER_DAY - MILLIS_PER_SECOND;
+    assertEquals(endOfDay, sqlTimeToUnixTime(new Time(endOfDay), (Calendar) 
null));
+  }
+
+  /**
+   * Test using a custom {@link Calendar} to calculate the unix time. Times 
created by a
+   * {@link Calendar} should be converted to a unix time in the given time 
zone. Times created by a
+   * {@link Time} method should be converted relative to the local time and 
not UTC.
+   */
+  @Test public void testSqlTimeToUnixTimeWithCustomCalendar() {
+    final Time epoch = Time.valueOf("00:00:00");
+
+    final Calendar utcCal = Calendar.getInstance(TimeZone.getTimeZone("UTC"), 
Locale.ROOT);
+    utcCal.set(1970, Calendar.JANUARY, 1, 0, 0, 0);
+    utcCal.set(Calendar.MILLISECOND, 0);
+    assertThat(sqlTimeToUnixTime(new Time(utcCal.getTimeInMillis()), utcCal), 
is(0));
+
+    final TimeZone minusFiveZone = TimeZone.getDefault();
+    minusFiveZone.setRawOffset((int) (minusFiveZone.getOffset(0L) - 5 * 
MILLIS_PER_HOUR));
+    final Calendar minusFiveCal = Calendar.getInstance(minusFiveZone, 
Locale.ROOT);
+    assertEquals(19 * MILLIS_PER_HOUR, sqlTimeToUnixTime(epoch, minusFiveCal));
+
+    final TimeZone plusFiveZone = TimeZone.getDefault();
+    plusFiveZone.setRawOffset((int) (plusFiveZone.getOffset(0L) + 5 * 
MILLIS_PER_HOUR));
+    final Calendar plusFiveCal = Calendar.getInstance(plusFiveZone, 
Locale.ROOT);
+    assertEquals(5 * MILLIS_PER_HOUR, sqlTimeToUnixTime(epoch, plusFiveCal));
+  }
+
+  /**
+   * Test that a unix time converts to a SQL time in the local time zone.
+   */
+  @Test public void testUnixTimeToSqlTimeWithLocalTimeZone() {
+    assertThat(unixTimeToSqlTime(0, CALENDAR), is(Time.valueOf("00:00:00")));
+    assertThat(unixTimeToSqlTime((int) (MILLIS_PER_DAY - MILLIS_PER_SECOND), 
CALENDAR),
+        is(Time.valueOf("23:59:59")));
+  }
+
+  /**
+   * Test that no time zone conversion happens when the given calendar is 
{@code null}.
+   */
+  @Test public void testUnixTimeToSqlTimeWithNullCalendar() {
+    assertThat(unixTimeToSqlTime(0, null), is(new Time(0L)));
+
+    final int endOfDay = (int) (MILLIS_PER_DAY - MILLIS_PER_SECOND);
+    assertThat(unixTimeToSqlTime(endOfDay, null), is(new Time(endOfDay)));
+  }
+
+  /**
+   * Test using a custom {@link Calendar} to calculate the SQL time. Times 
created by a
+   * {@link Calendar} should be converted to a SQL time in the given time 
zone. Otherwise, unix
+   * times should be converted relative to the local time and not UTC.
+   */
+  @Test public void testUnixTimeToSqlTimeWithCustomCalendar() {
+    final Calendar utcCal = Calendar.getInstance(TimeZone.getTimeZone("UTC"), 
Locale.ROOT);
+    utcCal.set(1970, Calendar.JANUARY, 1, 0, 0, 0);
+    utcCal.set(Calendar.MILLISECOND, 0);
+    assertThat(unixTimeToSqlTime(0, utcCal).getTime(), 
is(utcCal.getTimeInMillis()));
+
+    final TimeZone minusFiveZone = TimeZone.getDefault();
+    minusFiveZone.setRawOffset((int) (minusFiveZone.getOffset(0L) - 5 * 
MILLIS_PER_HOUR));
+    final Calendar minusFiveCal = Calendar.getInstance(minusFiveZone, 
Locale.ROOT);
+    assertThat(unixTimeToSqlTime(0, minusFiveCal).toString(), is("05:00:00"));
+
+    final TimeZone plusFiveZone = TimeZone.getDefault();
+    plusFiveZone.setRawOffset((int) (plusFiveZone.getOffset(0L) + 5 * 
MILLIS_PER_HOUR));
+    final Calendar plusFiveCal = Calendar.getInstance(plusFiveZone, 
Locale.ROOT);
+    assertThat(unixTimeToSqlTime(0, plusFiveCal).toString(), is("19:00:00"));
+  }
+
+  /**
+   * Test that a timestamp in the local time zone converts to a unix timestamp 
in UTC.
+   */
+  @Test public void testSqlTimestampToUnixTimestampWithLocalTimeZone() {
+    assertThat(sqlTimestampToUnixTimestamp(Timestamp.valueOf("1970-01-01 
00:00:00"), CALENDAR),
+        is(0L));
+    assertThat(sqlTimestampToUnixTimestamp(Timestamp.valueOf("2014-09-30 
15:28:27.356"), CALENDAR),
+        is(timestampStringToUnixDate("2014-09-30 15:28:27.356")));
+    assertThat(sqlTimestampToUnixTimestamp(Timestamp.valueOf("1500-04-30 
12:00:00.123"), CALENDAR),
+        is(timestampStringToUnixDate("1500-04-30 12:00:00.123")));
+  }
+
+  /**
+   * Test that no time zone conversion happens when the given calendar is 
{@code null}.
+   */
+  @Test public void testSqlTimestampToUnixTimestampWithNullCalendar() {
+    assertThat(sqlTimestampToUnixTimestamp(new Timestamp(0L), (Calendar) 
null), is(0L));
+
+    final Calendar julianCal = 
Calendar.getInstance(TimeZone.getTimeZone("UTC"), Locale.ROOT);
+    julianCal.set(1500, Calendar.APRIL, 30, 0, 0, 0);
+    julianCal.set(Calendar.MILLISECOND, 0);
+    assertThat(
+        sqlTimestampToUnixTimestamp(new 
Timestamp(julianCal.getTimeInMillis()), (Calendar) null),
+        is(-171545 * MILLIS_PER_DAY /* 1500-04-30 in ISO calendar */));
+  }
+
+  /**
+   * Test using a custom {@link Calendar} to calculate the unix timestamp. 
Timestamps created by a
+   * {@link Calendar} should be converted to a unix timestamp in the given 
time zone. Timestamps
+   * created by a {@link Timestamp} method should be converted relative to the 
local time and not
+   * UTC.
+   */
+  @Test public void testSqlTimestampToUnixTimestampWithCustomCalendar() {
+    final Timestamp epoch = java.sql.Timestamp.valueOf("1970-01-01 00:00:00");
+
+    final Calendar utcCal = Calendar.getInstance(TimeZone.getTimeZone("UTC"), 
Locale.ROOT);
+    utcCal.set(1970, Calendar.JANUARY, 1, 0, 0, 0);
+    utcCal.set(Calendar.MILLISECOND, 0);
+    assertThat(sqlTimestampToUnixTimestamp(new 
Timestamp(utcCal.getTimeInMillis()), utcCal),
+        is(0L));
+
+    final TimeZone minusFiveZone = TimeZone.getDefault();
+    minusFiveZone.setRawOffset((int) (minusFiveZone.getOffset(0L) - 5 * 
MILLIS_PER_HOUR));
+    final Calendar minusFiveCal = Calendar.getInstance(minusFiveZone, 
Locale.ROOT);
+    assertThat(sqlTimestampToUnixTimestamp(epoch, minusFiveCal),
+        is(-5 * MILLIS_PER_HOUR));
+
+    final TimeZone plusFiveZone = TimeZone.getDefault();
+    plusFiveZone.setRawOffset((int) (plusFiveZone.getOffset(0L) + 5 * 
MILLIS_PER_HOUR));
+    final Calendar plusFiveCal = Calendar.getInstance(plusFiveZone, 
Locale.ROOT);
+    assertThat(sqlTimestampToUnixTimestamp(epoch, plusFiveCal),
+        is(5 * MILLIS_PER_HOUR));
+  }
+
+  /**
+   * Test calendar conversion from the standard Gregorian calendar used by 
{@code java.sql} and the
+   * proleptic Gregorian calendar used by unix timestamps.
+   */
+  @Test public void testSqlTimestampToUnixTimestampWithGregorianShift() {
+    assertThat(sqlTimestampToUnixTimestamp(Timestamp.valueOf("1582-10-05 
00:00:00"), CALENDAR),
+        is(timestampStringToUnixDate("1582-10-15 00:00:00")));
+    assertThat(sqlTimestampToUnixTimestamp(Timestamp.valueOf("1582-10-04 
00:00:00"), CALENDAR),
+        is(timestampStringToUnixDate("1582-10-04 00:00:00")));
+    assertThat(sqlTimestampToUnixTimestamp(Timestamp.valueOf("1582-10-15 
00:00:00"), CALENDAR),
+        is(timestampStringToUnixDate("1582-10-15 00:00:00")));
+  }
+
+  /**
+   * Test date range 0001-01-01 to 9999-12-31 required by ANSI SQL.
+   *
+   * <p>Java may not be able to represent 0001-01-01 UTC depending on the 
default time zone. If the
+   * date would fall outside of Anno Domini (AD) when converted to the default 
time zone, that date
+   * should not be tested.
+   *
+   * <p>Not every time zone has a January 1st 12:00am, so this test only uses 
the UTC Time zone.
+   */
+  @Test public void testSqlTimestampToUnixTimestampWithAnsiDateRange() {
+    final Calendar ansiCal = Calendar.getInstance(TimeZone.getTimeZone("UTC"), 
Locale.ROOT);
+
+    // Test 0001-01-02 UTC to avoid issues with Java modifying 0001-01-01 when 
outside AD
+    ansiCal.set(1, Calendar.JANUARY, 2, 0, 0, 0);
+    ansiCal.set(Calendar.MILLISECOND, 0);
+    assertThat("Converts 0001-01-02 from SQL to unix timestamp",
+        sqlTimestampToUnixTimestamp(new Timestamp(ansiCal.getTimeInMillis()), 
ansiCal),
+        is(timestampStringToUnixDate("0001-01-02 00:00:00")));
+
+    // Test remaining years 0002-01-01 to 9999-01-01
+    ansiCal.set(Calendar.DATE, 1);
+    for (int i = 2; i <= 9999; ++i) {
+      final String str = String.format(Locale.ROOT, "%04d-01-01 00:00:00", i);
+      ansiCal.set(Calendar.YEAR, i);
+      assertThat("Converts '" + str + "' from SQL to unix timestamp",
+          sqlTimestampToUnixTimestamp(new 
Timestamp(ansiCal.getTimeInMillis()), ansiCal),
+          is(timestampStringToUnixDate(str)));
+    }
+
+    // Test end of range 9999-12-31
+    ansiCal.set(9999, Calendar.DECEMBER, 31, 0, 0, 0);
+    ansiCal.set(Calendar.MILLISECOND, 0);
+    assertThat("Converts 9999-12-31 from SQL to unix date",
+        sqlTimestampToUnixTimestamp(new Timestamp(ansiCal.getTimeInMillis()), 
ansiCal),
+        is(timestampStringToUnixDate("9999-12-31 00:00:00")));
+  }
+
+  /**
+   * Test that a unix timestamp converts to a SQL timestamp in the local time 
zone.
+   */
+  @Test public void testUnixTimestampToSqlTimestampWithLocalTimeZone() {
+    assertThat(unixTimestampToSqlTimestamp(0L, CALENDAR),
+        is(Timestamp.valueOf("1970-01-01 00:00:00.0")));
+    assertThat(unixTimestampToSqlTimestamp(1412090907356L, CALENDAR),
+        is(Timestamp.valueOf("2014-09-30 15:28:27.356")));
+    assertThat(unixTimestampToSqlTimestamp(-14821444799877L, CALENDAR),
+        is(Timestamp.valueOf("1500-04-30 12:00:00.123")));
+    assertThat(unixTimestampToSqlTimestamp(
+            timestampStringToUnixDate("1500-04-30 12:00:00.123"),
+            CALENDAR),
+        is(Timestamp.valueOf("1500-04-30 12:00:00.123")));
+  }
+
+  /**
+   * Test that no time zone conversion happens when the given calendar is 
{@code null}.
+   */
+  @Test public void testUnixTimestampToSqlTimestampWithNullCalendar() {
+    assertThat(unixTimestampToSqlTimestamp(0L, null), is(new Timestamp(0L)));
+
+    final Calendar julianCal = 
Calendar.getInstance(TimeZone.getTimeZone("UTC"), Locale.ROOT);
+    julianCal.set(1500, Calendar.APRIL, 30, 0, 0, 0);
+    julianCal.set(Calendar.MILLISECOND, 0);
+    assertThat(
+        unixTimestampToSqlTimestamp(
+            -171545 * MILLIS_PER_DAY /* 1500-04-30 in ISO calendar */,
+            null),
+        is(new Timestamp(julianCal.getTimeInMillis())));
+  }
+
+  /**
+   * Test using a custom {@link Calendar} to calculate the SQL timestamp. 
Timestamps created by a
+   * {@link Calendar} should be converted to a SQL timestamp in the given time 
zone. Otherwise,
+   * unix timestamps should be converted relative to the local time and not 
UTC.
+   */
+  @Test public void testUnixTimestampToSqlTimestampWithCustomCalendar() {
+    final Calendar utcCal = Calendar.getInstance(TimeZone.getTimeZone("UTC"), 
Locale.ROOT);
+    utcCal.set(1970, Calendar.JANUARY, 1, 0, 0, 0);
+    utcCal.set(Calendar.MILLISECOND, 0);
+    assertThat(unixTimestampToSqlTimestamp(0L, utcCal).getTime(), 
is(utcCal.getTimeInMillis()));
+
+    final TimeZone minusFiveZone = TimeZone.getDefault();
+    minusFiveZone.setRawOffset((int) (minusFiveZone.getOffset(0L) - 5 * 
MILLIS_PER_HOUR));
+    final Calendar minusFiveCal = Calendar.getInstance(minusFiveZone, 
Locale.ROOT);
+    assertThat(unixTimestampToSqlTimestamp(0L, minusFiveCal),
+        is(Timestamp.valueOf("1970-01-01 05:00:00")));
+
+    final TimeZone plusFiveZone = TimeZone.getDefault();
+    plusFiveZone.setRawOffset((int) (plusFiveZone.getOffset(0L) + 5 * 
MILLIS_PER_HOUR));
+    final Calendar plusFiveCal = Calendar.getInstance(plusFiveZone, 
Locale.ROOT);
+    assertThat(unixTimestampToSqlTimestamp(0L, plusFiveCal),
+        is(Timestamp.valueOf("1969-12-31 19:00:00")));
+  }
+
+  /**
+   * Test calendar conversion from the standard Gregorian calendar used by 
{@code java.sql} and the
+   * proleptic Gregorian calendar used by unix timestamps.
+   */
+  @Test public void testUnixTimestampToSqlTimestampWithGregorianShift() {
+    assertThat(unixTimestampToSqlTimestamp(
+            timestampStringToUnixDate("1582-10-04 00:00:00"),
+            CALENDAR),
+        is(Timestamp.valueOf("1582-10-04 00:00:00.0")));
+    assertThat(unixTimestampToSqlTimestamp(
+            timestampStringToUnixDate("1582-10-05 00:00:00"),
+            CALENDAR),
+        is(Timestamp.valueOf("1582-10-15 00:00:00.0")));
+    assertThat(unixTimestampToSqlTimestamp(
+            timestampStringToUnixDate("1582-10-15 00:00:00"),
+            CALENDAR),
+        is(Timestamp.valueOf("1582-10-15 00:00:00.0")));
+  }
+
+  /**
+   * Test date range 0001-01-01 to 9999-12-31 required by ANSI SQL.
+   *
+   * <p>Java may not be able to represent all dates depending on the default 
time zone, but both
+   * the expected and actual assertion values handles that in the same way.
+   */
+  @Test public void testUnixTimestampToSqlTimestampWithAnsiDateRange() {
+    for (int i = 1; i <= 9999; ++i) {
+      final String str = String.format(Locale.ROOT, "%04d-01-01 00:00:00", i);
+      assertThat(unixTimestampToSqlTimestamp(timestampStringToUnixDate(str), 
CALENDAR),
+          is(Timestamp.valueOf(str)));
+    }
+
+    assertThat(
+        unixTimestampToSqlTimestamp(timestampStringToUnixDate("9999-12-31 
00:00:00"), CALENDAR),
+        is(Timestamp.valueOf("9999-12-31 00:00:00")));
+  }
 }
 
 // End DateTimeUtilsTest.java
diff --git 
a/core/src/test/java/org/apache/calcite/avatica/util/TimeAccessorTest.java 
b/core/src/test/java/org/apache/calcite/avatica/util/TimeAccessorTest.java
new file mode 100644
index 000000000..145d63f01
--- /dev/null
+++ b/core/src/test/java/org/apache/calcite/avatica/util/TimeAccessorTest.java
@@ -0,0 +1,134 @@
+/*
+ * 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.avatica.util;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.sql.SQLException;
+import java.sql.Time;
+import java.util.Calendar;
+import java.util.Locale;
+import java.util.TimeZone;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+/**
+ * Test conversions from SQL {@link Time} to JDBC types in {@link 
AbstractCursor.TimeAccessor}.
+ */
+public class TimeAccessorTest {
+
+  private static final Calendar UTC =
+      Calendar.getInstance(TimeZone.getTimeZone("UTC"), Locale.ROOT);
+
+  private Cursor.Accessor instance;
+  private Calendar localCalendar;
+  private Time value;
+
+  /**
+   * Setup test environment by creating a {@link AbstractCursor.TimeAccessor} 
that reads from the
+   * instance variable {@code value}.
+   */
+  @Before public void before() {
+    final AbstractCursor.Getter getter = new LocalGetter();
+    localCalendar = Calendar.getInstance(TimeZone.getDefault(), Locale.ROOT);
+    instance = new AbstractCursor.TimeAccessor(getter, localCalendar);
+  }
+
+  /**
+   * Test {@code getTime()} returns the same value as the input time for the 
local calendar.
+   */
+  @Test public void testTime() throws SQLException {
+    value = new Time(0L);
+    assertThat(instance.getTime(null), is(value));
+
+    value = Time.valueOf("00:00:00");
+    assertThat(instance.getTime(UTC), is(value));
+
+    value = Time.valueOf("23:59:59");
+    assertThat(instance.getTime(UTC), is(value));
+  }
+
+  /**
+   * Test {@code getTime()} handles time zone conversions relative to the 
local calendar and not
+   * UTC.
+   */
+  @Test public void testTimeWithCalendar() throws SQLException {
+    value = new Time(0L);
+
+    final TimeZone minusFiveZone = TimeZone.getTimeZone("GMT-5:00");
+    final Calendar minusFiveCal = Calendar.getInstance(minusFiveZone, 
Locale.ROOT);
+    assertThat(instance.getTime(minusFiveCal).getTime(),
+        is(5 * DateTimeUtils.MILLIS_PER_HOUR));
+
+    final TimeZone plusFiveZone = TimeZone.getTimeZone("GMT+5:00");
+    final Calendar plusFiveCal = Calendar.getInstance(plusFiveZone, 
Locale.ROOT);
+    assertThat(instance.getTime(plusFiveCal).getTime(),
+        is(-5 * DateTimeUtils.MILLIS_PER_HOUR));
+  }
+
+  /**
+   * Test {@code getString()} returns the same value as the input time.
+   */
+  @Test public void testStringWithLocalTimeZone() throws SQLException {
+    value = Time.valueOf("00:00:00");
+    assertThat(instance.getString(), is("00:00:00"));
+
+    value = Time.valueOf("23:59:59");
+    assertThat(instance.getString(), is("23:59:59"));
+  }
+
+  /**
+   * Test {@code getString()} when the local calendar is UTC, which may be 
different from the
+   * default time zone.
+   */
+  @Test public void testStringWithUtc() throws SQLException {
+    localCalendar.setTimeZone(UTC.getTimeZone());
+
+    value = new Time(0L);
+    assertThat(instance.getString(), is("00:00:00"));
+
+    value = new Time(DateTimeUtils.MILLIS_PER_DAY - 1000);
+    assertThat(instance.getString(), is("23:59:59"));
+  }
+
+  /**
+   * Test {@code getLong()} returns the same value as the input time.
+   */
+  @Test public void testLong() throws SQLException {
+    value = new Time(0L);
+    assertThat(instance.getLong(), is(0L));
+
+    value = Time.valueOf("23:59:59");
+    final Time longTime = new Time(instance.getLong());
+    assertThat(longTime.toString(), is("23:59:59"));
+  }
+
+  /**
+   * Returns the value from the test instance to the accessor.
+   */
+  private class LocalGetter implements AbstractCursor.Getter {
+    @Override public Object getObject() {
+      return value;
+    }
+
+    @Override public boolean wasNull() {
+      return value == null;
+    }
+  }
+}
diff --git 
a/core/src/test/java/org/apache/calcite/avatica/util/TimeFromNumberAccessorTest.java
 
b/core/src/test/java/org/apache/calcite/avatica/util/TimeFromNumberAccessorTest.java
new file mode 100644
index 000000000..87f557dc8
--- /dev/null
+++ 
b/core/src/test/java/org/apache/calcite/avatica/util/TimeFromNumberAccessorTest.java
@@ -0,0 +1,159 @@
+/*
+ * 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.avatica.util;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.sql.SQLException;
+import java.sql.Time;
+import java.sql.Timestamp;
+import java.util.Calendar;
+import java.util.Locale;
+import java.util.SimpleTimeZone;
+import java.util.TimeZone;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+/**
+ * Test conversions from SQL TIME as the number of milliseconds since 
1970-01-01 00:00:00 to JDBC
+ * types in {@link AbstractCursor.TimeFromNumberAccessor}.
+ */
+public class TimeFromNumberAccessorTest {
+
+  private Cursor.Accessor instance;
+  private Calendar localCalendar;
+  private Object value;
+
+  /**
+   * Setup test environment by creating a {@link 
AbstractCursor.TimeFromNumberAccessor} that reads
+   * from the instance variable {@code value}.
+   */
+  @Before public void before() {
+    final AbstractCursor.Getter getter = new LocalGetter();
+    localCalendar = Calendar.getInstance(TimeZone.getDefault(), Locale.ROOT);
+    instance = new AbstractCursor.TimeFromNumberAccessor(getter,
+        localCalendar);
+  }
+
+  /**
+   * Test {@code getString()} returns the same value as the input time.
+   */
+  @Test public void testString() throws SQLException {
+    value = 0;
+    assertThat(instance.getString(), is("00:00:00"));
+
+    value = DateTimeUtils.MILLIS_PER_DAY - 1000;
+    assertThat(instance.getString(), is("23:59:59"));
+  }
+
+  /**
+   * Test {@code getTime()} returns the same value as the input time for the 
local calendar.
+   */
+  @Test public void testTime() throws SQLException {
+    value = 0;
+    assertThat(instance.getTime(localCalendar), is(Time.valueOf("00:00:00")));
+
+    value = DateTimeUtils.MILLIS_PER_DAY - 1000;
+    assertThat(instance.getTime(localCalendar), is(Time.valueOf("23:59:59")));
+  }
+
+  /**
+   * Test {@code getTime()} handles time zone conversions relative to the 
local calendar and not
+   * UTC.
+   */
+  @Test public void testTimeWithCalendar() throws SQLException {
+    final int offset = localCalendar.getTimeZone().getOffset(0);
+    final TimeZone east = new SimpleTimeZone(
+        offset + (int) DateTimeUtils.MILLIS_PER_HOUR,
+        "EAST");
+    final TimeZone west = new SimpleTimeZone(
+        offset - (int) DateTimeUtils.MILLIS_PER_HOUR,
+        "WEST");
+
+    value = 0;
+    assertThat(instance.getTime(Calendar.getInstance(east, Locale.ROOT)),
+        is(Timestamp.valueOf("1969-12-31 23:00:00")));
+    assertThat(instance.getTime(Calendar.getInstance(west, Locale.ROOT)),
+        is(Timestamp.valueOf("1970-01-01 01:00:00")));
+  }
+
+  /**
+   * Test no time zone conversion occurs if the given calendar is {@code null}.
+   */
+  @Test public void testTimeWithNullCalendar() throws SQLException {
+    value = 0;
+    assertThat(instance.getTime(null).getTime(),
+        is(0L));
+  }
+
+  /**
+   * Test {@code getTimestamp()} returns the same value as the input time.
+   */
+  @Test public void testTimestamp() throws SQLException {
+    value = 0;
+    assertThat(instance.getTimestamp(localCalendar),
+        is(Timestamp.valueOf("1970-01-01 00:00:00.0")));
+
+    value = DateTimeUtils.MILLIS_PER_DAY - 1000;
+    assertThat(instance.getTimestamp(localCalendar),
+        is(Timestamp.valueOf("1970-01-01 23:59:59.0")));
+  }
+
+  /**
+   * Test {@code getTimestamp()} handles time zone conversions relative to the 
local calendar and
+   * not UTC.
+   */
+  @Test public void testTimestampWithCalendar() throws SQLException {
+    final int offset = localCalendar.getTimeZone().getOffset(0);
+    final TimeZone east = new SimpleTimeZone(
+        offset + (int) DateTimeUtils.MILLIS_PER_HOUR,
+        "EAST");
+    final TimeZone west = new SimpleTimeZone(
+        offset - (int) DateTimeUtils.MILLIS_PER_HOUR,
+        "WEST");
+
+    value = 0;
+    assertThat(instance.getTimestamp(Calendar.getInstance(east, Locale.ROOT)),
+        is(Timestamp.valueOf("1969-12-31 23:00:00.0")));
+    assertThat(instance.getTimestamp(Calendar.getInstance(west, Locale.ROOT)),
+        is(Timestamp.valueOf("1970-01-01 01:00:00.0")));
+  }
+
+  /**
+   * Test no time zone conversion occurs if the given calendar is {@code null}.
+   */
+  @Test public void testTimestampWithNullCalendar() throws SQLException {
+    value = 0;
+    assertThat(instance.getTimestamp(null).getTime(),
+        is(0L));
+  }
+
+  /**
+   * Returns the value from the test instance to the accessor.
+   */
+  private class LocalGetter implements AbstractCursor.Getter {
+    @Override public Object getObject() {
+      return value;
+    }
+
+    @Override public boolean wasNull() {
+      return value == null;
+    }
+  }
+}
diff --git 
a/core/src/test/java/org/apache/calcite/avatica/util/TimestampAccessorTest.java 
b/core/src/test/java/org/apache/calcite/avatica/util/TimestampAccessorTest.java
new file mode 100644
index 000000000..94da748a7
--- /dev/null
+++ 
b/core/src/test/java/org/apache/calcite/avatica/util/TimestampAccessorTest.java
@@ -0,0 +1,248 @@
+/*
+ * 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.avatica.util;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.sql.Date;
+import java.sql.SQLException;
+import java.sql.Time;
+import java.sql.Timestamp;
+import java.util.Calendar;
+import java.util.Locale;
+import java.util.TimeZone;
+
+import static org.apache.calcite.avatica.util.DateTimeUtils.*;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+/**
+ * Test conversions from SQL {@link Timestamp} to JDBC types in
+ * {@link AbstractCursor.TimestampAccessor}.
+ */
+public class TimestampAccessorTest {
+
+  private static final Calendar UTC =
+      Calendar.getInstance(TimeZone.getTimeZone("UTC"), Locale.ROOT);
+
+  private Cursor.Accessor instance;
+  private Calendar localCalendar;
+  private Timestamp value;
+
+  /**
+   * Setup test environment by creating a {@link 
AbstractCursor.TimestampAccessor} that reads from
+   * the instance variable {@code value}.
+   */
+  @Before public void before() {
+    final AbstractCursor.Getter getter = new LocalGetter();
+    localCalendar = Calendar.getInstance(TimeZone.getDefault(), Locale.ROOT);
+    instance = new AbstractCursor.TimestampAccessor(getter, localCalendar);
+  }
+
+  /**
+   * Test {@code getTimestamp()} returns the same value as the input timestamp 
for the local
+   * calendar.
+   */
+  @Test public void testTimestamp() throws SQLException {
+    value = new Timestamp(0L);
+    assertThat(instance.getTimestamp(null), is(value));
+
+    value = Timestamp.valueOf("1970-01-01 00:00:00");
+    assertThat(instance.getTimestamp(UTC), is(value));
+
+    value = Timestamp.valueOf("2014-09-30 15:28:27.356");
+    assertThat(instance.getTimestamp(UTC), is(value));
+
+    value = Timestamp.valueOf("1500-04-30 12:00:00.123");
+    assertThat(instance.getTimestamp(UTC), is(value));
+  }
+
+  /**
+   * Test {@code getTimestamp()} handles time zone conversions relative to the 
local calendar and
+   * not UTC.
+   */
+  @Test public void testTimestampWithCalendar() throws SQLException {
+    value = new Timestamp(0L);
+
+    final TimeZone minusFiveZone = TimeZone.getTimeZone("GMT-5:00");
+    final Calendar minusFiveCal = Calendar.getInstance(minusFiveZone, 
Locale.ROOT);
+    assertThat(instance.getTimestamp(minusFiveCal).getTime(),
+        is(5 * MILLIS_PER_HOUR));
+
+    final TimeZone plusFiveZone = TimeZone.getTimeZone("GMT+5:00");
+    final Calendar plusFiveCal = Calendar.getInstance(plusFiveZone, 
Locale.ROOT);
+    assertThat(instance.getTimestamp(plusFiveCal).getTime(),
+        is(-5 * MILLIS_PER_HOUR));
+  }
+
+  /**
+   * Test {@code getDate()} returns the same value as the input timestamp for 
the local calendar.
+   */
+  @Test public void testDate() throws SQLException {
+    value = new Timestamp(0L);
+    assertThat(instance.getDate(null), is(new Date(0L)));
+
+    value = Timestamp.valueOf("1970-01-01 00:00:00");
+    assertThat(instance.getDate(UTC), is(Date.valueOf("1970-01-01")));
+
+    value = Timestamp.valueOf("1500-04-30 00:00:00");
+    assertThat(instance.getDate(UTC), is(Date.valueOf("1500-04-30")));
+  }
+
+  /**
+   * Test {@code getDate()} handles time zone conversions relative to the 
local calendar and not
+   * UTC.
+   */
+  @Test public void testDateWithCalendar() throws SQLException {
+    value = new Timestamp(0L);
+
+    final TimeZone minusFiveZone = TimeZone.getTimeZone("GMT-5:00");
+    final Calendar minusFiveCal = Calendar.getInstance(minusFiveZone, 
Locale.ROOT);
+    assertThat(instance.getDate(minusFiveCal).getTime(),
+        is(5 * DateTimeUtils.MILLIS_PER_HOUR));
+
+    final TimeZone plusFiveZone = TimeZone.getTimeZone("GMT+5:00");
+    final Calendar plusFiveCal = Calendar.getInstance(plusFiveZone, 
Locale.ROOT);
+    assertThat(instance.getDate(plusFiveCal).getTime(),
+        is(-5 * DateTimeUtils.MILLIS_PER_HOUR));
+  }
+
+  /**
+   * Test {@code getTime()} returns the same value as the input timestamp for 
the local calendar.
+   */
+  @Test public void testTime() throws SQLException {
+    value = new Timestamp(0L);
+    assertThat(instance.getTime(null), is(new Time(0L)));
+
+    value = Timestamp.valueOf("1970-01-01 00:00:00");
+    assertThat(instance.getTime(UTC), is(Time.valueOf("00:00:00")));
+
+    value = Timestamp.valueOf("2014-09-30 15:28:27.356");
+    assertThat(instance.getTime(UTC).toString(), is("15:28:27"));
+  }
+
+  /**
+   * Test {@code getTime()} handles time zone conversions relative to the 
local calendar and not
+   * UTC.
+   */
+  @Test public void testTimeWithCalendar() throws SQLException {
+    value = new Timestamp(0L);
+
+    final TimeZone minusFiveZone = TimeZone.getTimeZone("GMT-5:00");
+    final Calendar minusFiveCal = Calendar.getInstance(minusFiveZone, 
Locale.ROOT);
+    assertThat(instance.getTime(minusFiveCal).getTime(),
+        is(5 * DateTimeUtils.MILLIS_PER_HOUR));
+
+    final TimeZone plusFiveZone = TimeZone.getTimeZone("GMT+5:00");
+    final Calendar plusFiveCal = Calendar.getInstance(plusFiveZone, 
Locale.ROOT);
+    assertThat(instance.getTime(plusFiveCal).getTime(),
+        is(-5 * DateTimeUtils.MILLIS_PER_HOUR));
+  }
+
+  /**
+   * Test {@code getString()} returns the same value as the input timestamp.
+   */
+  @Test public void testString() throws SQLException {
+    value = Timestamp.valueOf("1970-01-01 00:00:00");
+    assertThat(instance.getString(), is("1970-01-01 00:00:00"));
+
+    value = Timestamp.valueOf("2014-09-30 15:28:27.356");
+    assertThat(instance.getString(), is("2014-09-30 15:28:27"));
+
+    value = Timestamp.valueOf("1500-04-30 12:00:00.123");
+    assertThat(instance.getString(), is("1500-04-30 12:00:00"));
+  }
+
+  /**
+   * Test {@code getString()} shifts between the standard Gregorian calendar 
and the proleptic
+   * Gregorian calendar.
+   */
+  @Test public void testStringWithGregorianShift() throws SQLException {
+    value = Timestamp.valueOf("1582-10-04 00:00:00");
+    assertThat(instance.getString(), is("1582-10-04 00:00:00"));
+    value = Timestamp.valueOf("1582-10-05 00:00:00");
+    assertThat(instance.getString(), is("1582-10-15 00:00:00"));
+    value = Timestamp.valueOf("1582-10-15 00:00:00");
+    assertThat(instance.getString(), is("1582-10-15 00:00:00"));
+  }
+
+  /**
+   * Test {@code getString()} returns dates relative to the local calendar.
+   */
+  @Test public void testStringWithUtc() throws SQLException {
+    localCalendar.setTimeZone(UTC.getTimeZone());
+
+    value = new Timestamp(0L);
+    assertThat(instance.getString(), is("1970-01-01 00:00:00"));
+
+    value = new Timestamp(1412090907356L /* 2014-09-30 15:28:27.356 UTC */);
+    assertThat(instance.getString(), is("2014-09-30 15:28:27"));
+
+    value = new Timestamp(-14820580799877L /* 1500-04-30 12:00:00.123 */);
+    assertThat(instance.getString(), is("1500-04-30 12:00:00"));
+  }
+
+  /**
+   * Test {@code getString()} supports date range 0001-01-01 to 9999-12-31 
required by ANSI SQL.
+   *
+   * <p>This test only uses the UTC time zone because some time zones don't 
have a January 1st
+   * 12:00am for every year.
+   */
+  @Test public void testStringWithAnsiDateRange() throws SQLException {
+    localCalendar.setTimeZone(UTC.getTimeZone());
+
+    final Calendar utcCal = (Calendar) UTC.clone();
+    utcCal.set(1, Calendar.JANUARY, 1, 0, 0, 0);
+    utcCal.set(Calendar.MILLISECOND, 0);
+
+    for (int i = 2; i <= 9999; ++i) {
+      utcCal.set(Calendar.YEAR, i);
+      value = new Timestamp(utcCal.getTimeInMillis());
+      assertThat(instance.getString(),
+          is(String.format(Locale.ROOT, "%04d-01-01 00:00:00", i)));
+    }
+  }
+
+  /**
+   * Test {@code getLong()} returns the same value as the input timestamp.
+   */
+  @Test public void testLong() throws SQLException {
+    value = new Timestamp(0L);
+    assertThat(instance.getLong(), is(0L));
+
+    value = Timestamp.valueOf("2014-09-30 15:28:27.356");
+    assertThat(instance.getLong(), is(value.getTime()));
+
+    value = Timestamp.valueOf("1500-04-30 00:00:00");
+    assertThat(instance.getLong(), is(value.getTime()));
+  }
+
+  /**
+   * Returns the value from the test instance to the accessor.
+   */
+  private class LocalGetter implements AbstractCursor.Getter {
+    @Override public Object getObject() {
+      return value;
+    }
+
+    @Override public boolean wasNull() {
+      return value == null;
+    }
+  }
+}
diff --git 
a/core/src/test/java/org/apache/calcite/avatica/util/TimestampFromNumberAccessorTest.java
 
b/core/src/test/java/org/apache/calcite/avatica/util/TimestampFromNumberAccessorTest.java
new file mode 100644
index 000000000..f4c06aa93
--- /dev/null
+++ 
b/core/src/test/java/org/apache/calcite/avatica/util/TimestampFromNumberAccessorTest.java
@@ -0,0 +1,278 @@
+/*
+ * 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.avatica.util;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.sql.Date;
+import java.sql.SQLException;
+import java.sql.Time;
+import java.sql.Timestamp;
+import java.util.Calendar;
+import java.util.Locale;
+import java.util.SimpleTimeZone;
+import java.util.TimeZone;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+/**
+ * Test conversions from SQL TIMESTAMP as the number of milliseconds since 
1970-01-01 00:00:00 to
+ * JDBC types in {@link AbstractCursor.TimestampFromNumberAccessor}.
+ */
+public class TimestampFromNumberAccessorTest {
+
+  private Cursor.Accessor instance;
+  private Calendar localCalendar;
+  private Object value;
+
+  /**
+   * Setup test environment by creating a {@link 
AbstractCursor.TimestampFromNumberAccessor} that
+   * reads from the instance variable {@code value}.
+   */
+  @Before public void before() {
+    final AbstractCursor.Getter getter = new LocalGetter();
+    localCalendar = Calendar.getInstance(TimeZone.getDefault(), Locale.ROOT);
+    instance = new AbstractCursor.TimestampFromNumberAccessor(getter,
+        localCalendar);
+  }
+
+  /**
+   * Test {@code getDate()} returns the same value as the input timestamp for 
the local calendar.
+   */
+  @Test public void testDate() throws SQLException {
+    value = 0L;
+    assertThat(instance.getDate(localCalendar),
+        is(Date.valueOf("1970-01-01")));
+
+    value = DateTimeUtils.timestampStringToUnixDate("1500-04-30 12:00:00");
+    assertThat(instance.getDate(localCalendar),
+        is(Timestamp.valueOf("1500-04-30 12:00:00")));
+  }
+
+  /**
+   * Test {@code getDate()} handles time zone conversions relative to the 
local calendar and not
+   * UTC.
+   */
+  @Test public void testDateWithCalendar() throws SQLException {
+    value = 0L;
+
+    final TimeZone minusFiveZone = TimeZone.getTimeZone("GMT-5:00");
+    final Calendar minusFiveCal = Calendar.getInstance(minusFiveZone, 
Locale.ROOT);
+    assertThat(instance.getDate(minusFiveCal).getTime(),
+        is(5 * DateTimeUtils.MILLIS_PER_HOUR));
+
+    final TimeZone plusFiveZone = TimeZone.getTimeZone("GMT+5:00");
+    final Calendar plusFiveCal = Calendar.getInstance(plusFiveZone, 
Locale.ROOT);
+    assertThat(instance.getDate(plusFiveCal).getTime(),
+        is(-5 * DateTimeUtils.MILLIS_PER_HOUR));
+  }
+
+  /**
+   * Test no time zone conversion occurs if the given calendar is {@code null}.
+   */
+  @Test public void testDateWithNullCalendar() throws SQLException {
+    value = 0;
+    assertThat(instance.getDate(null), is(new Date(0L)));
+  }
+
+  /**
+   * Test {@code getString()} returns the same value as the input timestamp.
+   */
+  @Test public void testString() throws SQLException {
+    value = 0;
+    assertThat(instance.getString(), is("1970-01-01 00:00:00"));
+
+    value = DateTimeUtils.timestampStringToUnixDate("2014-09-30 15:28:27.356");
+    assertThat(instance.getString(), is("2014-09-30 15:28:27"));
+
+    value = DateTimeUtils.timestampStringToUnixDate("1500-04-30 12:00:00.123");
+    assertThat(instance.getString(), is("1500-04-30 12:00:00"));
+  }
+
+  /**
+   * Test {@code getString()} shifts between the standard Gregorian calendar 
and the proleptic
+   * Gregorian calendar.
+   */
+  @Test public void testStringWithGregorianShift() throws SQLException {
+    for (int i = 4; i <= 15; ++i) {
+      final String str = String.format(Locale.ROOT, "1582-10-%02d 00:00:00", 
i);
+      value = DateTimeUtils.timestampStringToUnixDate(str);
+      assertThat(instance.getString(), is(str));
+    }
+  }
+
+  /**
+   * Test {@code getString()} returns timestamps relative to the local 
calendar.
+   */
+  @Test public void testStringWithUtc() throws SQLException {
+    localCalendar.setTimeZone(TimeZone.getTimeZone("UTC"));
+
+    value = 0L;
+    assertThat(instance.getString(), is("1970-01-01 00:00:00"));
+
+    value = 1412090907356L;  // 2014-09-30 15:28:27.356 UTC
+    assertThat(instance.getString(), is("2014-09-30 15:28:27"));
+
+    value = -14821444799877L;  // 1500-04-30 12:00:00.123
+    assertThat(instance.getString(), is("1500-04-30 12:00:00"));
+  }
+
+  /**
+   * Test {@code getString()} supports date range 0001-01-01 to 9999-12-31 
required by ANSI SQL.
+   *
+   * <p>This test only uses the UTC time zone because some time zones don't 
have a January 1st
+   * 12:00am for every year.
+   */
+  @Test public void testStringWithAnsiDateRange() throws SQLException {
+    for (int i = 1; i <= 9999; ++i) {
+      final String str = String.format(Locale.ROOT, "%04d-01-01 00:00:00", i);
+      value = DateTimeUtils.timestampStringToUnixDate(str);
+      assertThat(instance.getString(), is(str));
+    }
+  }
+
+  /**
+   * Test {@code getTime()} returns the same value as the input timestamp for 
the local calendar.
+   */
+  @Test public void testTime() throws SQLException {
+    value = 0L;
+    assertThat(instance.getTime(localCalendar).toString(), is("00:00:00"));
+
+    value = DateTimeUtils.timestampStringToUnixDate("2014-09-30 15:28:27.356");
+    assertThat(instance.getTime(localCalendar).toString(), is("15:28:27"));
+  }
+
+  /**
+   * Test {@code getTime()} handles time zone conversions relative to the 
local calendar and not
+   * UTC.
+   */
+  @Test public void testTimeWithCalendar() throws SQLException {
+    final int offset = localCalendar.getTimeZone().getOffset(0);
+    final TimeZone east = new SimpleTimeZone(
+        offset + (int) DateTimeUtils.MILLIS_PER_HOUR,
+        "EAST");
+    final TimeZone west = new SimpleTimeZone(
+        offset - (int) DateTimeUtils.MILLIS_PER_HOUR,
+        "WEST");
+
+    value = 0;
+    assertThat(instance.getTime(Calendar.getInstance(east, Locale.ROOT)),
+        is(Timestamp.valueOf("1969-12-31 23:00:00")));
+    assertThat(instance.getTime(Calendar.getInstance(west, Locale.ROOT)),
+        is(Timestamp.valueOf("1970-01-01 01:00:00")));
+  }
+
+  /**
+   * Test no time zone conversion occurs if the given calendar is {@code null}.
+   */
+  @Test public void testTimeWithNullCalendar() throws SQLException {
+    value = 0;
+    assertThat(instance.getTime(null), is(new Time(0L)));
+  }
+
+  /**
+   * Test {@code getTimestamp()} returns the same value as the input timestamp 
for the local
+   * calendar.
+   */
+  @Test public void testTimestamp() throws SQLException {
+    value = 0L;
+    assertThat(instance.getTimestamp(localCalendar),
+        is(Timestamp.valueOf("1970-01-01 00:00:00.0")));
+
+    value = DateTimeUtils.timestampStringToUnixDate("2014-09-30 15:28:27.356");
+    assertThat(instance.getTimestamp(localCalendar),
+        is(Timestamp.valueOf("2014-09-30 15:28:27.356")));
+
+    value = DateTimeUtils.timestampStringToUnixDate("1500-04-30 12:00:00");
+    assertThat(instance.getTimestamp(localCalendar),
+        is(Timestamp.valueOf("1500-04-30 12:00:00.0")));
+  }
+
+  /**
+   * Test {@code getTimestamp()} shifts between the standard Gregorian 
calendar and the proleptic
+   * Gregorian calendar.
+   */
+  @Test public void testTimestampWithGregorianShift() throws SQLException {
+    value = DateTimeUtils.timestampStringToUnixDate("1582-10-04 00:00:00");
+    assertThat(instance.getTimestamp(localCalendar),
+        is(Timestamp.valueOf("1582-10-04 00:00:00.0")));
+
+    value = DateTimeUtils.timestampStringToUnixDate("1582-10-05 00:00:00");
+    assertThat(instance.getTimestamp(localCalendar),
+        is(Timestamp.valueOf("1582-10-15 00:00:00.0")));
+
+    value = DateTimeUtils.timestampStringToUnixDate("1582-10-15 00:00:00");
+    assertThat(instance.getTimestamp(localCalendar),
+        is(Timestamp.valueOf("1582-10-15 00:00:00.0")));
+  }
+
+  /**
+   * Test {@code getTimestamp()} supports date range 0001-01-01 to 9999-12-31 
required by ANSI SQL.
+   */
+  @Test public void testTimestampWithAnsiDateRange() throws SQLException {
+    for (int i = 1; i <= 9999; ++i) {
+      final String str = String.format(Locale.ROOT, "%04d-01-01 00:00:00.0", 
i);
+      value = DateTimeUtils.timestampStringToUnixDate(str);
+      assertThat(instance.getTimestamp(localCalendar),
+          is(Timestamp.valueOf(str)));
+    }
+  }
+
+  /**
+   * Test {@code getTimestamp()} handles time zone conversions relative to the 
local calendar and
+   * not UTC.
+   */
+  @Test public void testTimestampWithCalendar() throws SQLException {
+    final int offset = localCalendar.getTimeZone().getOffset(0);
+    final TimeZone east = new SimpleTimeZone(
+        offset + (int) DateTimeUtils.MILLIS_PER_HOUR,
+        "EAST");
+    final TimeZone west = new SimpleTimeZone(
+        offset - (int) DateTimeUtils.MILLIS_PER_HOUR,
+        "WEST");
+
+    value = 0;
+    assertThat(instance.getTimestamp(Calendar.getInstance(east, Locale.ROOT)),
+        is(Timestamp.valueOf("1969-12-31 23:00:00.0")));
+    assertThat(instance.getTimestamp(Calendar.getInstance(west, Locale.ROOT)),
+        is(Timestamp.valueOf("1970-01-01 01:00:00.0")));
+  }
+
+  /**
+   * Test no time zone conversion occurs if the given calendar is {@code null}.
+   */
+  @Test public void testTimestampWithNullCalendar() throws SQLException {
+    value = 0;
+    assertThat(instance.getTimestamp(null).getTime(),
+        is(0L));
+  }
+
+  /**
+   * Returns the value from the test instance to the accessor.
+   */
+  private class LocalGetter implements AbstractCursor.Getter {
+    @Override public Object getObject() {
+      return value;
+    }
+
+    @Override public boolean wasNull() {
+      return value == null;
+    }
+  }
+}
diff --git 
a/core/src/test/java/org/apache/calcite/avatica/util/TimestampFromUtilDateAccessorTest.java
 
b/core/src/test/java/org/apache/calcite/avatica/util/TimestampFromUtilDateAccessorTest.java
new file mode 100644
index 000000000..afdd37bc5
--- /dev/null
+++ 
b/core/src/test/java/org/apache/calcite/avatica/util/TimestampFromUtilDateAccessorTest.java
@@ -0,0 +1,253 @@
+/*
+ * 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.avatica.util;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.sql.SQLException;
+import java.sql.Time;
+import java.sql.Timestamp;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.Locale;
+import java.util.SimpleTimeZone;
+import java.util.TimeZone;
+
+import static org.apache.calcite.avatica.util.DateTimeUtils.MILLIS_PER_HOUR;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+/**
+ * Test conversions from SQL {@link java.util.Date} to JDBC types in
+ * {@link AbstractCursor.TimestampFromUtilDateAccessor}.
+ */
+public class TimestampFromUtilDateAccessorTest {
+
+  private static final Calendar UTC =
+      Calendar.getInstance(TimeZone.getTimeZone("UTC"), Locale.ROOT);
+
+  private Cursor.Accessor instance;
+  private Calendar localCalendar;
+  private Date value;
+
+  /**
+   * Setup test environment by creating a {@link 
AbstractCursor.TimestampFromUtilDateAccessor} that
+   * reads from the instance variable {@code value}.
+   */
+  @Before public void before() {
+    final AbstractCursor.Getter getter = new LocalGetter();
+    localCalendar = Calendar.getInstance(TimeZone.getDefault(), Locale.ROOT);
+    instance = new AbstractCursor.TimestampFromUtilDateAccessor(getter, 
localCalendar);
+  }
+
+  /**
+   * Test {@code getTimestamp()} returns the same value as the input timestamp 
for the local
+   * calendar.
+   */
+  @Test public void testTimestamp() throws SQLException {
+    value = new Timestamp(0L);
+    assertThat(instance.getTimestamp(null), is(value));
+
+    value = Timestamp.valueOf("1970-01-01 00:00:00");
+    assertThat(instance.getTimestamp(UTC), is(value));
+
+    value = Timestamp.valueOf("2014-09-30 15:28:27.356");
+    assertThat(instance.getTimestamp(UTC), is(value));
+
+    value = Timestamp.valueOf("1500-04-30 12:00:00.123");
+    assertThat(instance.getTimestamp(UTC), is(value));
+  }
+
+  /**
+   * Test {@code getTimestamp()} handles time zone conversions relative to the 
local calendar and
+   * not UTC.
+   */
+  @Test public void testTimestampWithCalendar() throws SQLException {
+    value = new Timestamp(0L);
+
+    final TimeZone minusFiveZone = TimeZone.getTimeZone("GMT-5:00");
+    final Calendar minusFiveCal = Calendar.getInstance(minusFiveZone, 
Locale.ROOT);
+    assertThat(instance.getTimestamp(minusFiveCal).getTime(),
+        is(5 * MILLIS_PER_HOUR));
+
+    final TimeZone plusFiveZone = TimeZone.getTimeZone("GMT+5:00");
+    final Calendar plusFiveCal = Calendar.getInstance(plusFiveZone, 
Locale.ROOT);
+    assertThat(instance.getTimestamp(plusFiveCal).getTime(),
+        is(-5 * MILLIS_PER_HOUR));
+  }
+
+  /**
+   * Test {@code getDate()} returns the same value as the input timestamp for 
the local calendar.
+   */
+  @Test public void testDate() throws SQLException {
+    value = new java.sql.Date(0L);
+    assertThat(instance.getDate(null), is(value));
+
+    value = java.sql.Date.valueOf("1970-01-01");
+    assertThat(instance.getDate(UTC), is(value));
+
+    value = java.sql.Date.valueOf("1500-04-30");
+    assertThat(instance.getDate(UTC), is(value));
+  }
+
+  /**
+   * Test {@code getDate()} handles time zone conversions relative to the 
local calendar and not
+   * UTC.
+   */
+  @Test public void testDateWithCalendar() throws SQLException {
+    value = new java.sql.Date(0L);
+
+    final TimeZone minusFiveZone = TimeZone.getTimeZone("GMT-5:00");
+    final Calendar minusFiveCal = Calendar.getInstance(minusFiveZone, 
Locale.ROOT);
+    assertThat(instance.getDate(minusFiveCal).getTime(),
+        is(5 * DateTimeUtils.MILLIS_PER_HOUR));
+
+    final TimeZone plusFiveZone = TimeZone.getTimeZone("GMT+5:00");
+    final Calendar plusFiveCal = Calendar.getInstance(plusFiveZone, 
Locale.ROOT);
+    assertThat(instance.getDate(plusFiveCal).getTime(),
+        is(-5 * DateTimeUtils.MILLIS_PER_HOUR));
+  }
+
+  /**
+   * Test {@code getTime()} returns the same value as the input timestamp for 
the local calendar.
+   */
+  @Test public void testTime() throws SQLException {
+    value = new Time(0L);
+    assertThat(instance.getTime(null), is(value));
+
+    value = Time.valueOf("00:00:00");
+    assertThat(instance.getTime(UTC), is(value));
+
+    value = Time.valueOf("23:59:59");
+    assertThat(instance.getTime(UTC).toString(), is("23:59:59"));
+  }
+
+  /**
+   * Test {@code getTime()} handles time zone conversions relative to the 
local calendar and not
+   * UTC.
+   */
+  @Test public void testTimeWithCalendar() throws SQLException {
+    final int offset = localCalendar.getTimeZone().getOffset(0);
+    final TimeZone east = new SimpleTimeZone(
+        offset + (int) DateTimeUtils.MILLIS_PER_HOUR,
+        "EAST");
+    final TimeZone west = new SimpleTimeZone(
+        offset - (int) DateTimeUtils.MILLIS_PER_HOUR,
+        "WEST");
+
+    value = new Time(0L);
+    assertThat(instance.getTime(Calendar.getInstance(east, Locale.ROOT)),
+        is(Timestamp.valueOf("1969-12-31 23:00:00")));
+    assertThat(instance.getTime(Calendar.getInstance(west, Locale.ROOT)),
+        is(Timestamp.valueOf("1970-01-01 01:00:00")));
+  }
+
+  /**
+   * Test {@code getString()} returns the same value as the input timestamp.
+   */
+  @Test public void testStringWithLocalTimeZone() throws SQLException {
+    value = Timestamp.valueOf("1970-01-01 00:00:00");
+    assertThat(instance.getString(), is("1970-01-01 00:00:00"));
+
+    value = Timestamp.valueOf("2014-09-30 15:28:27.356");
+    assertThat(instance.getString(), is("2014-09-30 15:28:27"));
+
+    value = Timestamp.valueOf("1500-04-30 12:00:00.123");
+    assertThat(instance.getString(), is("1500-04-30 12:00:00"));
+  }
+
+  /**
+   * Test {@code getString()} shifts between the standard Gregorian calendar 
and the proleptic
+   * Gregorian calendar.
+   */
+  @Test public void testStringWithGregorianShift() throws SQLException {
+    value = Timestamp.valueOf("1582-10-04 00:00:00");
+    assertThat(instance.getString(), is("1582-10-04 00:00:00"));
+    value = Timestamp.valueOf("1582-10-05 00:00:00");
+    assertThat(instance.getString(), is("1582-10-15 00:00:00"));
+    value = Timestamp.valueOf("1582-10-15 00:00:00");
+    assertThat(instance.getString(), is("1582-10-15 00:00:00"));
+  }
+
+  /**
+   * Test {@code getString()} returns timestamps relative to the local 
calendar.
+   */
+  @Test public void testStringWithUtc() throws SQLException {
+    localCalendar.setTimeZone(UTC.getTimeZone());
+
+    value = new Timestamp(0L);
+    assertThat(instance.getString(), is("1970-01-01 00:00:00"));
+
+    value = new Timestamp(1412090907356L /* 2014-09-30 15:28:27.356 UTC */);
+    assertThat(instance.getString(), is("2014-09-30 15:28:27"));
+
+    value = new Timestamp(-14820580799877L /* 1500-04-30 12:00:00.123 UTC */);
+    assertThat(instance.getString(), is("1500-04-30 12:00:00"));
+  }
+
+  /**
+   * Test {@code getString()} supports date range 0001-01-01 to 9999-12-31 
required by ANSI SQL.
+   *
+   * <p>This test only uses the UTC time zone because some time zones don't 
have a January 1st
+   * 12:00am for every year.
+   */
+  @Test public void testStringWithAnsiDateRange() throws SQLException {
+    localCalendar.setTimeZone(UTC.getTimeZone());
+
+    final Calendar utcCal = (Calendar) UTC.clone();
+    utcCal.set(1, Calendar.JANUARY, 1, 0, 0, 0);
+    utcCal.set(Calendar.MILLISECOND, 0);
+
+    for (int i = 2; i <= 9999; ++i) {
+      utcCal.set(Calendar.YEAR, i);
+      value = new Timestamp(utcCal.getTimeInMillis());
+      assertThat(instance.getString(),
+          is(String.format(Locale.ROOT, "%04d-01-01 00:00:00", i)));
+    }
+  }
+
+  /**
+   * Test {@code getLong()} returns the same value as the input timestamp.
+   */
+  @Test public void testLong() throws SQLException {
+    value = new Timestamp(0L);
+    assertThat(instance.getLong(), is((long) 
-localCalendar.getTimeZone().getOffset(0L)));
+
+    value = Timestamp.valueOf("2014-09-30 15:28:27.356");
+    assertThat(instance.getLong(),
+        is(value.getTime() - 
localCalendar.getTimeZone().getOffset(value.getTime())));
+
+    value = Timestamp.valueOf("1500-04-30 00:00:00");
+    assertThat(instance.getLong(),
+        is(value.getTime() - 
localCalendar.getTimeZone().getOffset(value.getTime())));
+  }
+
+  /**
+   * Returns the value from the test instance to the accessor.
+   */
+  private class LocalGetter implements AbstractCursor.Getter {
+    @Override public Object getObject() {
+      return value;
+    }
+
+    @Override public boolean wasNull() {
+      return value == null;
+    }
+  }
+}

Reply via email to