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;
+ }
+ }
+}