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 7fbe13a7bc..17d159d275 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
@@ -43,6 +43,7 @@
import java.util.Calendar;
import java.util.List;
import java.util.Map;
+import java.util.TimeZone;
/**
* Base class for implementing a cursor.
@@ -150,37 +151,45 @@ protected Accessor createAccessor(ColumnMetaData columnMetaData,
throw new AssertionError("bad " + columnMetaData.type.rep);
}
case Types.TIME:
+ // TIME WITH LOCAL TIME ZONE is a standard ISO type without proper JDBC support.
+ // It represents a global instant in time, as opposed to local clock parameters.
+ boolean fixedInstant =
+ "TIME_WITH_LOCAL_TIME_ZONE".equals(columnMetaData.type.getName());
switch (columnMetaData.type.rep) {
case PRIMITIVE_INT:
case INTEGER:
case NUMBER:
- return new TimeFromNumberAccessor(getter, localCalendar);
+ return new TimeFromNumberAccessor(getter, localCalendar, fixedInstant);
case JAVA_SQL_TIME:
- return new TimeAccessor(getter, localCalendar);
+ return new TimeAccessor(getter, localCalendar, fixedInstant);
default:
throw new AssertionError("bad " + columnMetaData.type.rep);
}
case Types.TIMESTAMP:
+ // TIMESTAMP WITH LOCAL TIME ZONE is a standard ISO type without proper JDBC support.
+ // It represents a global instant in time, as opposed to local clock/calendar parameters.
+ fixedInstant =
+ "TIMESTAMP_WITH_LOCAL_TIME_ZONE".equals(columnMetaData.type.getName());
switch (columnMetaData.type.rep) {
case PRIMITIVE_LONG:
case LONG:
case NUMBER:
- return new TimestampFromNumberAccessor(getter, localCalendar);
+ return new TimestampFromNumberAccessor(getter, localCalendar, fixedInstant);
case JAVA_SQL_TIMESTAMP:
- return new TimestampAccessor(getter, localCalendar);
+ return new TimestampAccessor(getter, localCalendar, fixedInstant);
case JAVA_UTIL_DATE:
- return new TimestampFromUtilDateAccessor(getter, localCalendar);
+ return new TimestampFromUtilDateAccessor(getter, localCalendar, fixedInstant);
default:
throw new AssertionError("bad " + columnMetaData.type.rep);
}
- case 2013: // TIME_WITH_TIMEZONE
+ case Types.TIME_WITH_TIMEZONE:
switch (columnMetaData.type.rep) {
case STRING:
return new StringAccessor(getter);
default:
throw new AssertionError("bad " + columnMetaData.type.rep);
}
- case 2014: // TIMESTAMP_WITH_TIMEZONE
+ case Types.TIMESTAMP_WITH_TIMEZONE:
switch (columnMetaData.type.rep) {
case STRING:
return new StringAccessor(getter);
@@ -238,12 +247,18 @@ protected Accessor createAccessor(ColumnMetaData columnMetaData,
public abstract boolean next();
- /** Accesses a timestamp value as a string.
+ /**
+ * Accesses a timestamp value as a string.
* The timestamp is in SQL format (e.g. "2013-09-22 22:30:32"),
- * not Java format ("2013-09-22 22:30:32.123"). */
+ * not Java format ("2013-09-22 22:30:32.123").
+ *
+ *
Note that, when a TIMESTAMP is adjusted to a calendar, the offset is subtracted.
+ * Here, on the other hand, to adjust the string to the calendar (which only happens for type
+ * TIMESTAMP WITH LOCAL TIME ZONE), the offset is added. These are meant to be inverse operations.
+ */
private static String timestampAsString(long v, Calendar calendar) {
if (calendar != null) {
- v -= calendar.getTimeZone().getOffset(v);
+ v += calendar.getTimeZone().getOffset(v);
}
return DateTimeUtils.unixTimestampToString(v);
}
@@ -254,12 +269,18 @@ private static String dateAsString(int v, Calendar calendar) {
return DateTimeUtils.unixDateToString(v);
}
- /** Accesses a time value as a string, e.g. "22:30:32". */
+ /**
+ * Accesses a time value as a string, e.g. "22:30:32".
+ *
+ *
Note that, when a TIME is adjusted to a calendar, the offset is subtracted.
+ * Here, on the other hand, to adjust the string to the calendar (which only happens for type
+ * TIME WITH LOCAL TIME ZONE), the offset is added. These are meant to be inverse operations.
+ */
private static String timeAsString(int v, Calendar calendar) {
if (calendar != null) {
- v -= calendar.getTimeZone().getOffset(v);
+ v += calendar.getTimeZone().getOffset(v);
}
- return DateTimeUtils.unixTimeToString(v);
+ return DateTimeUtils.unixTimeToString(v % (int) DateTimeUtils.MILLIS_PER_DAY);
}
/** Implementation of {@link Cursor.Accessor}. */
@@ -955,10 +976,12 @@ protected Number getNumber() throws SQLException {
*/
static class TimeFromNumberAccessor extends NumberAccessor {
private final Calendar localCalendar;
+ private final boolean fixedInstant;
- TimeFromNumberAccessor(Getter getter, Calendar localCalendar) {
+ TimeFromNumberAccessor(Getter getter, Calendar localCalendar, boolean fixedInstant) {
super(getter, 0);
this.localCalendar = localCalendar;
+ this.fixedInstant = fixedInstant;
}
@Override public Object getObject() throws SQLException {
@@ -970,6 +993,9 @@ static class TimeFromNumberAccessor extends NumberAccessor {
if (v == null) {
return null;
}
+ if (fixedInstant) {
+ calendar = null;
+ }
return DateTimeUtils.unixTimeToSqlTime(v.intValue(), calendar);
}
@@ -978,6 +1004,9 @@ static class TimeFromNumberAccessor extends NumberAccessor {
if (v == null) {
return null;
}
+ if (fixedInstant) {
+ calendar = null;
+ }
return DateTimeUtils.unixTimestampToSqlTimestamp(v.longValue(), calendar);
}
@@ -986,7 +1015,7 @@ static class TimeFromNumberAccessor extends NumberAccessor {
if (v == null) {
return null;
}
- return timeAsString(v.intValue(), null);
+ return timeAsString(v.intValue(), fixedInstant ? localCalendar : null);
}
protected Number getNumber() throws SQLException {
@@ -1013,10 +1042,13 @@ protected Number getNumber() throws SQLException {
*/
static class TimestampFromNumberAccessor extends NumberAccessor {
private final Calendar localCalendar;
+ private final boolean fixedInstant;
- TimestampFromNumberAccessor(Getter getter, Calendar localCalendar) {
+ TimestampFromNumberAccessor(
+ Getter getter, Calendar localCalendar, boolean fixedInstant) {
super(getter, 0);
this.localCalendar = localCalendar;
+ this.fixedInstant = fixedInstant;
}
@Override public Object getObject() throws SQLException {
@@ -1028,6 +1060,9 @@ static class TimestampFromNumberAccessor extends NumberAccessor {
if (v == null) {
return null;
}
+ if (fixedInstant) {
+ calendar = null;
+ }
return DateTimeUtils.unixTimestampToSqlTimestamp(v.longValue(), calendar);
}
@@ -1052,7 +1087,7 @@ static class TimestampFromNumberAccessor extends NumberAccessor {
if (v == null) {
return null;
}
- return timestampAsString(v.longValue(), null);
+ return timestampAsString(v.longValue(), fixedInstant ? localCalendar : null);
}
protected Number getNumber() throws SQLException {
@@ -1130,10 +1165,12 @@ static class DateAccessor extends ObjectAccessor {
*/
static class TimeAccessor extends ObjectAccessor {
private final Calendar localCalendar;
+ private final boolean fixedInstant;
- TimeAccessor(Getter getter, Calendar localCalendar) {
+ TimeAccessor(Getter getter, Calendar localCalendar, boolean fixedInstant) {
super(getter);
this.localCalendar = localCalendar;
+ this.fixedInstant = fixedInstant;
}
@Override public Time getTime(Calendar calendar) throws SQLException {
@@ -1141,7 +1178,7 @@ static class TimeAccessor extends ObjectAccessor {
if (date == null) {
return null;
}
- if (calendar != null) {
+ if (calendar != null && !fixedInstant) {
long v = date.getTime();
v -= calendar.getTimeZone().getOffset(v);
date = new Time(v);
@@ -1154,8 +1191,8 @@ static class TimeAccessor extends ObjectAccessor {
if (time == null) {
return null;
}
- final int unix = DateTimeUtils.sqlTimeToUnixTime(time, localCalendar);
- return timeAsString(unix, null);
+ final int unix = DateTimeUtils.sqlTimeToUnixTime(time, (TimeZone) null);
+ return timeAsString(unix, fixedInstant ? localCalendar : null);
}
@Override public long getLong() throws SQLException {
@@ -1178,10 +1215,12 @@ static class TimeAccessor extends ObjectAccessor {
*/
static class TimestampAccessor extends ObjectAccessor {
private final Calendar localCalendar;
+ private final boolean fixedInstant;
- TimestampAccessor(Getter getter, Calendar localCalendar) {
+ TimestampAccessor(Getter getter, Calendar localCalendar, boolean fixedInstant) {
super(getter);
this.localCalendar = localCalendar;
+ this.fixedInstant = fixedInstant;
}
@Override public Timestamp getTimestamp(Calendar calendar) throws SQLException {
@@ -1189,7 +1228,7 @@ static class TimestampAccessor extends ObjectAccessor {
if (timestamp == null) {
return null;
}
- if (calendar != null) {
+ if (calendar != null && !fixedInstant) {
long v = timestamp.getTime();
v -= calendar.getTimeZone().getOffset(v);
timestamp = new Timestamp(v);
@@ -1219,8 +1258,8 @@ static class TimestampAccessor extends ObjectAccessor {
return null;
}
final long unix =
- DateTimeUtils.sqlTimestampToUnixTimestamp(timestamp, localCalendar);
- return timestampAsString(unix, null);
+ DateTimeUtils.sqlTimestampToUnixTimestamp(timestamp, (TimeZone) null);
+ return timestampAsString(unix, fixedInstant ? localCalendar : null);
}
@Override public long getLong() throws SQLException {
@@ -1241,11 +1280,13 @@ static class TimestampAccessor extends ObjectAccessor {
*/
static class TimestampFromUtilDateAccessor extends ObjectAccessor {
private final Calendar localCalendar;
+ private final boolean fixedInstant;
- TimestampFromUtilDateAccessor(Getter getter,
- Calendar localCalendar) {
+ TimestampFromUtilDateAccessor(
+ Getter getter, Calendar localCalendar, boolean fixedInstant) {
super(getter);
this.localCalendar = localCalendar;
+ this.fixedInstant = fixedInstant;
}
@Override public Timestamp getTimestamp(Calendar calendar) throws SQLException {
@@ -1254,7 +1295,7 @@ static class TimestampFromUtilDateAccessor extends ObjectAccessor {
return null;
}
long v = date.getTime();
- if (calendar != null) {
+ if (calendar != null && !fixedInstant) {
v -= calendar.getTimeZone().getOffset(v);
}
return new Timestamp(v);
@@ -1281,8 +1322,8 @@ static class TimestampFromUtilDateAccessor extends ObjectAccessor {
if (date == null) {
return null;
}
- final long unix = DateTimeUtils.utilDateToUnixTimestamp(date, localCalendar);
- return timestampAsString(unix, null);
+ final long unix = DateTimeUtils.utilDateToUnixTimestamp(date, (TimeZone) null);
+ return timestampAsString(unix, fixedInstant ? localCalendar : null);
}
@Override public long getLong() throws SQLException {
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 b7645b74d9..32ec8ca1f4 100644
--- a/core/src/test/java/org/apache/calcite/avatica/AvaticaResultSetConversionsTest.java
+++ b/core/src/test/java/org/apache/calcite/avatica/AvaticaResultSetConversionsTest.java
@@ -50,14 +50,18 @@
import java.sql.Time;
import java.sql.Timestamp;
import java.sql.Types;
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
import java.util.Properties;
-import java.util.TimeZone;
import static org.hamcrest.CoreMatchers.isA;
import static org.junit.Assert.assertEquals;
@@ -71,6 +75,45 @@
*/
@RunWith(Parameterized.class)
public class AvaticaResultSetConversionsTest {
+
+ // UTC: 2016-10-10 20:18:38.123
+ // October 10 is considered DST in all time zones that observe DST (both hemispheres), so tests
+ // using this value will cover daylight time zone conversion when run in a location that observes
+ // DST. This is just a matter of coverage; all tests must succeed no matter where the host is.
+ private static final long DST_INSTANT = 1476130718123L;
+ private static final String DST_DATE_STRING = "2016-10-10";
+ private static final String DST_TIME_STRING = "20:18:38";
+ private static final String DST_TIMESTAMP_STRING = "2016-10-10 20:18:38";
+
+ // In order to test normalization based on the system default time zone, offset values cannot be
+ // hardcoded; they're subject to change from run to run depending on the host system.
+ private static final long DEFAULT_DST_INSTANT_OFFSET =
+ DateTimeUtils.DEFAULT_ZONE.getOffset(DST_INSTANT);
+ private static final long OFFSET_DST_INSTANT = DST_INSTANT + DEFAULT_DST_INSTANT_OFFSET;
+ private static final String OFFSET_DST_TIMESTAMP_STRING =
+ LocalDateTime.ofInstant(Instant.ofEpochMilli(OFFSET_DST_INSTANT), ZoneId.of("UTC"))
+ .format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss", Locale.ROOT));
+ private static final long REVERSE_OFFSET_DST_INSTANT = DST_INSTANT - DEFAULT_DST_INSTANT_OFFSET;
+
+ // UTC: 2016-11-14 11:32:03.242
+ // There is no date where all time zones (both hemispheres) are on standard time, but all northern
+ // time zones observe standard time by mid-November. Tests using this value may or may not
+ // exercise standard time zone conversion, but this is just a matter of coverage; all tests must
+ // succeed no matter where the host is.
+ private static final long STANDARD_INSTANT = 1479123123242L;
+ private static final long REVERSE_OFFSET_STANDARD_INSTANT =
+ STANDARD_INSTANT - DateTimeUtils.DEFAULT_ZONE.getOffset(STANDARD_INSTANT);
+
+ // UTC: 00:24:36.123
+ private static final long VALID_TIME = 1476123L;
+ private static final long REVERSE_OFFSET_VALID_TIME =
+ VALID_TIME - DateTimeUtils.DEFAULT_ZONE.getOffset(VALID_TIME);
+
+ // UTC: 41:05:12.242
+ private static final long OVERFLOW_TIME = 147912242L;
+ private static final long REVERSE_OFFSET_OVERFLOW_TIME =
+ OVERFLOW_TIME - DateTimeUtils.DEFAULT_ZONE.getOffset(OVERFLOW_TIME);
+
/**
* A fake test driver for test.
*/
@@ -152,18 +195,30 @@ public TestMetaImpl(AvaticaConnection connection) {
ColumnMetaData.scalar(Types.TIME, "TIME",
ColumnMetaData.Rep.JAVA_SQL_TIME),
DatabaseMetaData.columnNoNulls),
- columnMetaData("timestamp", 10,
+ columnMetaData("timestamp_utcOffsetMs", 10,
+ ColumnMetaData.scalar(Types.TIMESTAMP, "TIMESTAMP",
+ ColumnMetaData.Rep.LONG),
+ DatabaseMetaData.columnNoNulls),
+ columnMetaData("timestamp_object", 11,
ColumnMetaData.scalar(Types.TIMESTAMP, "TIMESTAMP",
ColumnMetaData.Rep.JAVA_SQL_TIMESTAMP),
DatabaseMetaData.columnNoNulls),
- columnMetaData("array", 11,
+ columnMetaData("timestamp_ltz_utcOffsetMs", 12,
+ ColumnMetaData.scalar(Types.TIMESTAMP, "TIMESTAMP_WITH_LOCAL_TIME_ZONE",
+ ColumnMetaData.Rep.LONG),
+ DatabaseMetaData.columnNoNulls),
+ columnMetaData("timestamp_ltz_object", 13,
+ ColumnMetaData.scalar(Types.TIMESTAMP, "TIMESTAMP_WITH_LOCAL_TIME_ZONE",
+ ColumnMetaData.Rep.JAVA_SQL_TIMESTAMP),
+ DatabaseMetaData.columnNoNulls),
+ columnMetaData("array", 14,
ColumnMetaData.array(
ColumnMetaData.scalar(Types.INTEGER, "INTEGER",
ColumnMetaData.Rep.PRIMITIVE_INT),
"ARRAY",
ColumnMetaData.Rep.ARRAY),
DatabaseMetaData.columnNoNulls),
- columnMetaData("struct", 12,
+ columnMetaData("struct", 15,
ColumnMetaData.struct(
Arrays.asList(
columnMetaData("int", 0,
@@ -175,36 +230,36 @@ public TestMetaImpl(AvaticaConnection connection) {
ColumnMetaData.Rep.PRIMITIVE_BOOLEAN),
DatabaseMetaData.columnNoNulls))),
DatabaseMetaData.columnNoNulls),
- columnMetaData("bit", 13,
+ columnMetaData("bit", 16,
ColumnMetaData.scalar(Types.BIT, "BIT",
ColumnMetaData.Rep.PRIMITIVE_BOOLEAN),
DatabaseMetaData.columnNoNulls),
- columnMetaData("null", 14,
+ columnMetaData("null", 17,
ColumnMetaData.scalar(Types.NULL, "NULL",
ColumnMetaData.Rep.OBJECT),
DatabaseMetaData.columnNullable),
- columnMetaData("date_array", 15,
+ columnMetaData("date_array", 18,
ColumnMetaData.array(
ColumnMetaData.scalar(Types.DATE, "DATE",
ColumnMetaData.Rep.PRIMITIVE_INT),
"ARRAY",
ColumnMetaData.Rep.ARRAY),
DatabaseMetaData.columnNoNulls),
- columnMetaData("timestamp_array", 16,
+ columnMetaData("timestamp_array", 19,
ColumnMetaData.array(
ColumnMetaData.scalar(Types.TIMESTAMP, "TIMESTAMP",
ColumnMetaData.Rep.PRIMITIVE_LONG),
"ARRAY",
ColumnMetaData.Rep.ARRAY),
DatabaseMetaData.columnNoNulls),
- columnMetaData("time_array", 17,
+ columnMetaData("time_array", 20,
ColumnMetaData.array(
ColumnMetaData.scalar(Types.TIME, "TIME",
ColumnMetaData.Rep.NUMBER),
"ARRAY",
ColumnMetaData.Rep.ARRAY),
DatabaseMetaData.columnNoNulls),
- columnMetaData("decimal_array", 18,
+ columnMetaData("decimal_array", 21,
ColumnMetaData.array(
ColumnMetaData.scalar(Types.DECIMAL, "DECIMAL",
ColumnMetaData.Rep.PRIMITIVE_DOUBLE),
@@ -215,15 +270,18 @@ public TestMetaImpl(AvaticaConnection connection) {
List row = Collections.singletonList(
new Object[] {
true, (byte) 1, (short) 2, 3, 4L, 5.0f, 6.0d, "testvalue",
- new Date(1476130718123L), new Time(1476130718123L),
- new Timestamp(1476130718123L),
+ new Date(DST_INSTANT), new Time(DST_INSTANT),
+ DST_INSTANT,
+ new Timestamp(DST_INSTANT),
+ DST_INSTANT,
+ new Timestamp(DST_INSTANT),
Arrays.asList(1, 2, 3),
new StructImpl(Arrays.asList(42, false)),
true,
null,
Arrays.asList(123, 18234),
- Arrays.asList(1476130718123L, 1479123123242L),
- Arrays.asList(1476123L, 147912242L),
+ Arrays.asList(DST_INSTANT, STANDARD_INSTANT),
+ Arrays.asList(VALID_TIME, OVERFLOW_TIME),
Arrays.asList(1, 1.1)
});
@@ -486,6 +544,15 @@ public void testGetTimestamp(ResultSet resultSet, Calendar calendar) throws SQLE
}
}
+ public void testGetTimestampDefaultCalendar(ResultSet resultSet) throws SQLException {
+ try {
+ g.getTimestamp(resultSet);
+ fail("Was expecting to throw SQLDataException");
+ } catch (Exception e) {
+ assertThat(e, isA((Class) SQLDataException.class)); // success
+ }
+ }
+
public void testGetStruct(ResultSet resultSet) throws SQLException {
try {
g.getStruct(resultSet);
@@ -634,7 +701,7 @@ private DateArrayAccessorTestHelper(Getter g) {
ColumnMetaData.ScalarType intType =
ColumnMetaData.scalar(Types.DATE, "DATE", ColumnMetaData.Rep.PRIMITIVE_INT);
Array expectedArray =
- new ArrayFactoryImpl(TimeZone.getTimeZone("UTC")).createArray(
+ new ArrayFactoryImpl(DateTimeUtils.DEFAULT_ZONE).createArray(
intType, Arrays.asList(123, 18234));
assertTrue(ArrayImpl.equalContents(expectedArray, g.getArray(resultSet)));
}
@@ -652,8 +719,8 @@ private TimeArrayAccessorTestHelper(Getter g) {
ColumnMetaData.ScalarType intType =
ColumnMetaData.scalar(Types.TIME, "TIME", ColumnMetaData.Rep.NUMBER);
Array expectedArray =
- new ArrayFactoryImpl(TimeZone.getTimeZone("UTC")).createArray(
- intType, Arrays.asList(1476123L, 147912242L));
+ new ArrayFactoryImpl(DateTimeUtils.DEFAULT_ZONE).createArray(
+ intType, Arrays.asList(REVERSE_OFFSET_VALID_TIME, REVERSE_OFFSET_OVERFLOW_TIME));
assertTrue(ArrayImpl.equalContents(expectedArray, g.getArray(resultSet)));
}
}
@@ -670,8 +737,8 @@ private TimestampArrayAccessorTestHelper(Getter g) {
ColumnMetaData.ScalarType intType =
ColumnMetaData.scalar(Types.TIMESTAMP, "TIMESTAMP", ColumnMetaData.Rep.PRIMITIVE_LONG);
Array expectedArray =
- new ArrayFactoryImpl(TimeZone.getTimeZone("UTC")).createArray(
- intType, Arrays.asList(1476130718123L, 1479123123242L));
+ new ArrayFactoryImpl(DateTimeUtils.DEFAULT_ZONE).createArray(
+ intType, Arrays.asList(REVERSE_OFFSET_DST_INSTANT, REVERSE_OFFSET_STANDARD_INSTANT));
assertTrue(ArrayImpl.equalContents(expectedArray, g.getArray(resultSet)));
}
}
@@ -986,7 +1053,7 @@ private DateAccessorTestHelper(Getter g) {
}
@Override public void testGetString(ResultSet resultSet) throws SQLException {
- assertEquals("2016-10-10", g.getString(resultSet));
+ assertEquals(DST_DATE_STRING, g.getString(resultSet));
}
@Override public void testGetBoolean(ResultSet resultSet) throws SQLException {
@@ -1010,7 +1077,7 @@ private DateAccessorTestHelper(Getter g) {
}
@Override public void testGetDate(ResultSet resultSet, Calendar calendar) throws SQLException {
- assertEquals(new Date(1476130718123L), g.getDate(resultSet, calendar));
+ assertEquals(new Date(DST_INSTANT), g.getDate(resultSet, calendar));
}
}
@@ -1023,7 +1090,7 @@ private TimeAccessorTestHelper(Getter g) {
}
@Override public void testGetString(ResultSet resultSet) throws SQLException {
- assertEquals("20:18:38", g.getString(resultSet));
+ assertEquals(DST_TIME_STRING, g.getString(resultSet));
}
@Override public void testGetBoolean(ResultSet resultSet) throws SQLException {
@@ -1031,36 +1098,44 @@ private TimeAccessorTestHelper(Getter g) {
}
@Override public void testGetByte(ResultSet resultSet) throws SQLException {
- assertEquals((byte) -85, g.getByte(resultSet));
+ assertEquals((byte) DST_INSTANT, g.getByte(resultSet));
}
@Override public void testGetShort(ResultSet resultSet) throws SQLException {
- assertEquals((short) -20053, g.getShort(resultSet));
+ assertEquals((short) (DST_INSTANT % DateTimeUtils.MILLIS_PER_DAY), g.getShort(resultSet));
}
@Override public void testGetInt(ResultSet resultSet) throws SQLException {
- assertEquals(73118123, g.getInt(resultSet));
+ assertEquals((int) (DST_INSTANT % DateTimeUtils.MILLIS_PER_DAY), g.getInt(resultSet));
}
@Override public void testGetLong(ResultSet resultSet) throws SQLException {
- assertEquals(73118123, g.getLong(resultSet));
+ assertEquals(DST_INSTANT % DateTimeUtils.MILLIS_PER_DAY, g.getLong(resultSet));
}
@Override public void testGetTime(ResultSet resultSet, Calendar calendar) throws SQLException {
- assertEquals(new Time(1476130718123L), g.getTime(resultSet, calendar));
+ assertEquals(new Time(DST_INSTANT), g.getTime(resultSet, calendar));
}
}
/**
* accessor test helper for the timestamp column
*/
- private static final class TimestampAccessorTestHelper extends AccessorTestHelper {
- private TimestampAccessorTestHelper(Getter g) {
+ private static class TimestampAccessorTestHelper extends AccessorTestHelper {
+ protected final String expectedString;
+ protected final long expectedInstantDefaultTimeZone;
+
+ private TimestampAccessorTestHelper(
+ Getter g,
+ String expectedString,
+ long expectedInstantDefaultTimeZone) {
super(g);
+ this.expectedString = expectedString;
+ this.expectedInstantDefaultTimeZone = expectedInstantDefaultTimeZone;
}
@Override public void testGetString(ResultSet resultSet) throws SQLException {
- assertEquals("2016-10-10 20:18:38", g.getString(resultSet));
+ assertEquals(expectedString, g.getString(resultSet));
}
@Override public void testGetBoolean(ResultSet resultSet) throws SQLException {
@@ -1068,32 +1143,66 @@ private TimestampAccessorTestHelper(Getter g) {
}
@Override public void testGetByte(ResultSet resultSet) throws SQLException {
- assertEquals((byte) -85, g.getByte(resultSet));
+ assertEquals((byte) DST_INSTANT, g.getByte(resultSet));
}
@Override public void testGetShort(ResultSet resultSet) throws SQLException {
- assertEquals((short) 16811, g.getShort(resultSet));
+ assertEquals((short) DST_INSTANT, g.getShort(resultSet));
}
@Override public void testGetInt(ResultSet resultSet) throws SQLException {
- assertEquals(-1338031701, g.getInt(resultSet));
+ assertEquals((int) DST_INSTANT, g.getInt(resultSet));
}
@Override public void testGetLong(ResultSet resultSet) throws SQLException {
- assertEquals(1476130718123L, g.getLong(resultSet));
+ assertEquals(DST_INSTANT, g.getLong(resultSet));
}
@Override public void testGetDate(ResultSet resultSet, Calendar calendar) throws SQLException {
- assertEquals(new Date(1476130718123L), g.getDate(resultSet, calendar));
+ // Sanity check: when providing an explicit calendar, this test always uses UTC.
+ assertEquals(calendar.getTimeZone().getRawOffset(), 0);
+ assertEquals(new Date(DST_INSTANT), g.getDate(resultSet, calendar));
}
@Override public void testGetTime(ResultSet resultSet, Calendar calendar) throws SQLException {
- assertEquals(new Time(1476130718123L), g.getTime(resultSet, calendar));
+ // Sanity check: when providing an explicit calendar, this test always uses UTC.
+ assertEquals(calendar.getTimeZone().getRawOffset(), 0);
+ assertEquals(new Time(DST_INSTANT), g.getTime(resultSet, calendar));
}
@Override public void testGetTimestamp(ResultSet resultSet, Calendar calendar)
throws SQLException {
- assertEquals(new Timestamp(1476130718123L), g.getTimestamp(resultSet, calendar));
+ // Sanity check: when providing an explicit calendar, this test always uses UTC.
+ assertEquals(calendar.getTimeZone().getRawOffset(), 0);
+ assertEquals(new Timestamp(DST_INSTANT), g.getTimestamp(resultSet, calendar));
+ }
+
+ @Override public void testGetTimestampDefaultCalendar(ResultSet resultSet)
+ throws SQLException {
+ assertEquals(new Timestamp(expectedInstantDefaultTimeZone), g.getTimestamp(resultSet));
+ }
+ }
+
+ /** Like {@link TimestampAccessorTestHelper} but also allows non-integer number getters. */
+ private static final class TimestampFromNumberAccessorTestHelper
+ extends TimestampAccessorTestHelper {
+ private TimestampFromNumberAccessorTestHelper(
+ Getter g,
+ String expectedString,
+ long expectedInstantDefaultTimeZone) {
+ super(g, expectedString, expectedInstantDefaultTimeZone);
+ }
+
+ @Override public void testGetFloat(ResultSet resultSet) throws SQLException {
+ assertEquals((float) DST_INSTANT, g.getFloat(resultSet), 0);
+ }
+
+ @Override public void testGetDouble(ResultSet resultSet) throws SQLException {
+ assertEquals((double) DST_INSTANT, g.getDouble(resultSet), 0);
+ }
+
+ @Override public void testGetDecimal(ResultSet resultSet) throws SQLException {
+ assertEquals(BigDecimal.valueOf(DST_INSTANT), g.getBigDecimal(resultSet));
}
}
@@ -1110,7 +1219,7 @@ private StringAccessorTestHelper(Getter g) {
}
}
- private static final Calendar DEFAULT_CALENDAR = DateTimeUtils.calendar();
+ private static final Calendar UTC_CALENDAR = DateTimeUtils.calendar();
private static Connection connection = null;
private static ResultSet resultSet = null;
@@ -1118,8 +1227,6 @@ private StringAccessorTestHelper(Getter g) {
@BeforeClass
public static void executeQuery() throws SQLException {
Properties properties = new Properties();
- properties.setProperty("timeZone", "GMT");
-
connection = new TestDriver().connect("jdbc:test", properties);
resultSet = connection.createStatement().executeQuery("SELECT * FROM TABLE");
resultSet.next(); // move to the first record
@@ -1159,23 +1266,49 @@ public static Collection data() {
new DateAccessorTestHelper(new LabelGetter("date")),
new TimeAccessorTestHelper(new OrdinalGetter(10)),
new TimeAccessorTestHelper(new LabelGetter("time")),
- new TimestampAccessorTestHelper(new OrdinalGetter(11)),
- new TimestampAccessorTestHelper(new LabelGetter("timestamp")),
- new ArrayAccessorTestHelper(new OrdinalGetter(12)),
+ // The following four cases test a regular JDBC TIMESTAMP.
+ // The string value is fixed but the instant value can vary according to the time zone.
+ new TimestampFromNumberAccessorTestHelper(
+ new OrdinalGetter(11),
+ DST_TIMESTAMP_STRING, REVERSE_OFFSET_DST_INSTANT),
+ new TimestampFromNumberAccessorTestHelper(
+ new LabelGetter("timestamp_utcOffsetMs"),
+ DST_TIMESTAMP_STRING, REVERSE_OFFSET_DST_INSTANT),
+ new TimestampAccessorTestHelper(
+ new OrdinalGetter(12),
+ DST_TIMESTAMP_STRING, REVERSE_OFFSET_DST_INSTANT),
+ new TimestampAccessorTestHelper(
+ new LabelGetter("timestamp_object"),
+ DST_TIMESTAMP_STRING, REVERSE_OFFSET_DST_INSTANT),
+ // The following four cases test TIMESTAMP WITH LOCAL TIME ZONE.
+ // The instant value is fixed but the string value can vary according to the time zone.
+ new TimestampFromNumberAccessorTestHelper(
+ new OrdinalGetter(13),
+ OFFSET_DST_TIMESTAMP_STRING, DST_INSTANT),
+ new TimestampFromNumberAccessorTestHelper(
+ new LabelGetter("timestamp_ltz_utcOffsetMs"),
+ OFFSET_DST_TIMESTAMP_STRING, DST_INSTANT),
+ new TimestampAccessorTestHelper(
+ new OrdinalGetter(14),
+ OFFSET_DST_TIMESTAMP_STRING, DST_INSTANT),
+ new TimestampAccessorTestHelper(
+ new LabelGetter("timestamp_ltz_object"),
+ OFFSET_DST_TIMESTAMP_STRING, DST_INSTANT),
+ new ArrayAccessorTestHelper(new OrdinalGetter(15)),
new ArrayAccessorTestHelper(new LabelGetter("array")),
- new StructAccessorTestHelper(new OrdinalGetter(13)),
+ new StructAccessorTestHelper(new OrdinalGetter(16)),
new StructAccessorTestHelper(new LabelGetter("struct")),
- new BooleanAccessorTestHelper(new OrdinalGetter(14)),
+ new BooleanAccessorTestHelper(new OrdinalGetter(17)),
new BooleanAccessorTestHelper(new LabelGetter("bit")),
- new NullObjectAccessorTestHelper(new OrdinalGetter(15)),
+ new NullObjectAccessorTestHelper(new OrdinalGetter(18)),
new NullObjectAccessorTestHelper(new LabelGetter("null")),
- new DateArrayAccessorTestHelper(new OrdinalGetter(16)),
+ new DateArrayAccessorTestHelper(new OrdinalGetter(19)),
new DateArrayAccessorTestHelper(new LabelGetter("date_array")),
- new TimestampArrayAccessorTestHelper(new OrdinalGetter(17)),
+ new TimestampArrayAccessorTestHelper(new OrdinalGetter(20)),
new TimestampArrayAccessorTestHelper(new LabelGetter("timestamp_array")),
- new TimeArrayAccessorTestHelper(new OrdinalGetter(18)),
+ new TimeArrayAccessorTestHelper(new OrdinalGetter(21)),
new TimeArrayAccessorTestHelper(new LabelGetter("time_array")),
- new DecimalArrayAccessorTestHelper(new OrdinalGetter(19)),
+ new DecimalArrayAccessorTestHelper(new OrdinalGetter(22)),
new DecimalArrayAccessorTestHelper(new LabelGetter("decimal_array")));
}
@@ -1282,17 +1415,22 @@ public void testGetArray() throws SQLException {
@Test
public void testGetDate() throws SQLException {
- testHelper.testGetDate(resultSet, DEFAULT_CALENDAR);
+ testHelper.testGetDate(resultSet, UTC_CALENDAR);
}
@Test
public void testGetTime() throws SQLException {
- testHelper.testGetTime(resultSet, DEFAULT_CALENDAR);
+ testHelper.testGetTime(resultSet, UTC_CALENDAR);
}
@Test
public void testGetTimestamp() throws SQLException {
- testHelper.testGetTimestamp(resultSet, DEFAULT_CALENDAR);
+ testHelper.testGetTimestamp(resultSet, UTC_CALENDAR);
+ }
+
+ @Test
+ public void testGetTimestampDefaultCalendar() throws SQLException {
+ testHelper.testGetTimestampDefaultCalendar(resultSet);
}
@Test
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
index 145d63f018..7796ab646c 100644
--- a/core/src/test/java/org/apache/calcite/avatica/util/TimeAccessorTest.java
+++ b/core/src/test/java/org/apache/calcite/avatica/util/TimeAccessorTest.java
@@ -36,6 +36,9 @@ public class TimeAccessorTest {
private static final Calendar UTC =
Calendar.getInstance(TimeZone.getTimeZone("UTC"), Locale.ROOT);
+ // UTC+5:30
+ private static final TimeZone IST_ZONE = TimeZone.getTimeZone("Asia/Kolkata");
+
private Cursor.Accessor instance;
private Calendar localCalendar;
private Time value;
@@ -46,77 +49,74 @@ public class TimeAccessorTest {
*/
@Before public void before() {
final AbstractCursor.Getter getter = new LocalGetter();
- localCalendar = Calendar.getInstance(TimeZone.getDefault(), Locale.ROOT);
- instance = new AbstractCursor.TimeAccessor(getter, localCalendar);
+ localCalendar = Calendar.getInstance(IST_ZONE, Locale.ROOT);
+ instance = new AbstractCursor.TimeAccessor(getter, localCalendar, false);
}
/**
- * Test {@code getTime()} returns the same value as the input time for the local calendar.
+ * Test {@code getTime()} returns the same value as the input time for the connection default
+ * calendar.
*/
@Test public void testTime() throws SQLException {
- value = new Time(0L);
+ value = new Time(12345L);
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 {@code getTime()} handles time zone conversions relative to the provided calendar. */
@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(),
+ 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(),
+ assertThat(
+ instance.getTime(plusFiveCal).getTime(),
is(-5 * DateTimeUtils.MILLIS_PER_HOUR));
}
/**
- * Test {@code getString()} returns the same value as the input time.
+ * Test {@code getString()} returns the clock representation in UTC when the connection default
+ * calendar is UTC.
*/
- @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 public void testStringWithUtc() throws SQLException {
+ localCalendar.setTimeZone(UTC.getTimeZone());
+ helpTestGetString();
}
/**
- * Test {@code getString()} when the local calendar is UTC, which may be different from the
- * default time zone.
+ * Test {@code getString()} also returns the clock representation in UTC when the connection
+ * default calendar is *not* UTC.
*/
- @Test public void testStringWithUtc() throws SQLException {
- localCalendar.setTimeZone(UTC.getTimeZone());
+ @Test public void testStringWithDefaultTimeZone() throws SQLException {
+ helpTestGetString();
+ }
+ private void helpTestGetString() throws SQLException {
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"));
+
+ value = new Time(DateTimeUtils.MILLIS_PER_DAY + 1000);
+ assertThat(instance.getString(), is("00:00:01"));
}
/**
- * Test {@code getLong()} returns the same value as the input time.
+ * Test {@code getLong()} returns the same value as the input time's millisecond instant, modulo
+ * the number of milliseconds in a day.
*/
@Test public void testLong() throws SQLException {
- value = new Time(0L);
- assertThat(instance.getLong(), is(0L));
+ value = new Time(5000L);
+ assertThat(instance.getLong(), is(5000L));
- value = Time.valueOf("23:59:59");
- final Time longTime = new Time(instance.getLong());
- assertThat(longTime.toString(), is("23:59:59"));
+ value = new Time(DateTimeUtils.MILLIS_PER_DAY + 1000L);
+ assertThat(instance.getLong(), is(1000L));
}
/**
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
index 87f557dc8f..43ae09767b 100644
--- a/core/src/test/java/org/apache/calcite/avatica/util/TimeFromNumberAccessorTest.java
+++ b/core/src/test/java/org/apache/calcite/avatica/util/TimeFromNumberAccessorTest.java
@@ -24,7 +24,6 @@
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;
@@ -36,9 +35,15 @@
*/
public class TimeFromNumberAccessorTest {
+ private static final Calendar UTC =
+ Calendar.getInstance(TimeZone.getTimeZone("UTC"), Locale.ROOT);
+
+ // UTC+5:30
+ private static final TimeZone IST_ZONE = TimeZone.getTimeZone("Asia/Kolkata");
+
private Cursor.Accessor instance;
private Calendar localCalendar;
- private Object value;
+ private Long value;
/**
* Setup test environment by creating a {@link AbstractCursor.TimeFromNumberAccessor} that reads
@@ -46,102 +51,74 @@ public class TimeFromNumberAccessorTest {
*/
@Before public void before() {
final AbstractCursor.Getter getter = new LocalGetter();
- localCalendar = Calendar.getInstance(TimeZone.getDefault(), Locale.ROOT);
+ localCalendar = Calendar.getInstance(IST_ZONE, Locale.ROOT);
instance = new AbstractCursor.TimeFromNumberAccessor(getter,
- localCalendar);
+ localCalendar, false);
}
/**
- * 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 {@code getTime()} and {@code getTimestamp()} return the same instant as the input time for
+ * the connection default calendar.
*/
@Test public void testTime() throws SQLException {
- value = 0;
- assertThat(instance.getTime(localCalendar), is(Time.valueOf("00:00:00")));
+ value = 12345L;
- value = DateTimeUtils.MILLIS_PER_DAY - 1000;
- assertThat(instance.getTime(localCalendar), is(Time.valueOf("23:59:59")));
+ assertThat(instance.getTime(null), is(new Time(value)));
+ assertThat(instance.getTimestamp(null), is(new Timestamp(value)));
}
/**
- * Test {@code getTime()} handles time zone conversions relative to the local calendar and not
- * UTC.
+ * Test {@code getTime()} and {@code getTimestamp()} handle time zone conversions relative to the
+ * provided calendar.
*/
@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")));
+ value = 0L;
+
+ final TimeZone minusFiveZone = TimeZone.getTimeZone("GMT-5:00");
+ final Calendar minusFiveCal = Calendar.getInstance(minusFiveZone, Locale.ROOT);
+ assertThat(
+ instance.getTime(minusFiveCal),
+ is(new Time(5 * DateTimeUtils.MILLIS_PER_HOUR)));
+ assertThat(
+ instance.getTimestamp(minusFiveCal),
+ is(new Timestamp(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),
+ is(new Time(-5 * DateTimeUtils.MILLIS_PER_HOUR)));
+ assertThat(
+ instance.getTimestamp(plusFiveCal),
+ is(new Timestamp(-5 * DateTimeUtils.MILLIS_PER_HOUR)));
}
/**
- * Test no time zone conversion occurs if the given calendar is {@code null}.
+ * Test {@code getString()} returns the clock representation in UTC when the connection default
+ * calendar is UTC.
*/
- @Test public void testTimeWithNullCalendar() throws SQLException {
- value = 0;
- assertThat(instance.getTime(null).getTime(),
- is(0L));
+ @Test public void testStringWithUtc() throws SQLException {
+ localCalendar.setTimeZone(UTC.getTimeZone());
+ helpTestGetString();
}
/**
- * Test {@code getTimestamp()} returns the same value as the input time.
+ * Test {@code getString()} also returns the clock representation in UTC when the connection
+ * default calendar is *not* UTC.
*/
- @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 public void testStringWithDefaultTimeZone() throws SQLException {
+ helpTestGetString();
}
- /**
- * 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")));
- }
+ private void helpTestGetString() throws SQLException {
+ value = 0L;
+ assertThat(instance.getString(), is("00:00:00"));
- /**
- * 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));
+ value = DateTimeUtils.MILLIS_PER_DAY - 1000;
+ assertThat(instance.getString(), is("23:59:59"));
+
+ value = DateTimeUtils.MILLIS_PER_DAY + 1000;
+ assertThat(instance.getString(), is("00:00:01"));
}
/**
diff --git a/core/src/test/java/org/apache/calcite/avatica/util/TimeWithLocalTimeZoneAccessorTest.java b/core/src/test/java/org/apache/calcite/avatica/util/TimeWithLocalTimeZoneAccessorTest.java
new file mode 100644
index 0000000000..fa01c01dac
--- /dev/null
+++ b/core/src/test/java/org/apache/calcite/avatica/util/TimeWithLocalTimeZoneAccessorTest.java
@@ -0,0 +1,117 @@
+/*
+ * 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 TimeWithLocalTimeZoneAccessorTest {
+
+ private static final Calendar UTC =
+ Calendar.getInstance(TimeZone.getTimeZone("UTC"), Locale.ROOT);
+
+ // UTC+5:30
+ private static final TimeZone IST_ZONE = TimeZone.getTimeZone("Asia/Kolkata");
+
+ 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(IST_ZONE, Locale.ROOT);
+ instance = new AbstractCursor.TimeAccessor(getter, localCalendar, true);
+ }
+
+ /**
+ * Test {@code getTime()} does no time zone conversion because {@code TIME WITH LOCAL TIME ZONE}
+ * represents a global instant in time.
+ */
+ @Test public void testTime() throws SQLException {
+ value = new Time(12345L);
+
+ assertThat(instance.getTime(null), is(value));
+ assertThat(instance.getTime(UTC), is(value));
+ assertThat(instance.getTime(localCalendar), is(value));
+ }
+
+ /**
+ * Test {@code getString()} adjusts the string representation based on the default time zone.
+ */
+ @Test public void testStringWithDefaultTimeZone() throws SQLException {
+ value = new Time(0);
+ assertThat(instance.getString(), is("05:30:00"));
+
+ value = new Time(DateTimeUtils.MILLIS_PER_DAY - 1000);
+ assertThat(instance.getString(), is("05:29:59"));
+ }
+
+ /**
+ * Test {@code getString()} adjusts the string representation based on an explicit 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");
+ assertThat(instance.getLong(), is(value.getTime() % DateTimeUtils.MILLIS_PER_DAY));
+ 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/TimeWithLocalTimeZoneFromNumberAccessorTest.java b/core/src/test/java/org/apache/calcite/avatica/util/TimeWithLocalTimeZoneFromNumberAccessorTest.java
new file mode 100644
index 0000000000..3490c54c75
--- /dev/null
+++ b/core/src/test/java/org/apache/calcite/avatica/util/TimeWithLocalTimeZoneFromNumberAccessorTest.java
@@ -0,0 +1,151 @@
+/*
+ * 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 TimeWithLocalTimeZoneFromNumberAccessorTest {
+
+ private static final Calendar UTC =
+ Calendar.getInstance(TimeZone.getTimeZone("UTC"), Locale.ROOT);
+
+ // UTC+5:30
+ private static final TimeZone IST_ZONE = TimeZone.getTimeZone("Asia/Kolkata");
+
+ private Cursor.Accessor instance;
+ private Calendar localCalendar;
+ private Integer 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(IST_ZONE, Locale.ROOT);
+ instance = new AbstractCursor.TimeFromNumberAccessor(getter,
+ localCalendar, true);
+ }
+
+ /**
+ * Test {@code getString()} adjusts the string representation based on the default time zone.
+ */
+ @Test public void testString() throws SQLException {
+ value = 0;
+ assertThat(instance.getString(), is("05:30:00"));
+
+ value = (int) (DateTimeUtils.MILLIS_PER_DAY - 1000);
+ assertThat(instance.getString(), is("05:29:59"));
+ }
+
+ /**
+ * Test {@code getString()} adjusts the string representation based on an explicit time zone.
+ */
+ @Test public void testStringWithUtc() throws SQLException {
+ localCalendar.setTimeZone(UTC.getTimeZone());
+
+ value = 0;
+ assertThat(instance.getString(), is("00:00:00"));
+
+ value = (int) (DateTimeUtils.MILLIS_PER_DAY - 1000);
+ assertThat(instance.getString(), is("23:59:59"));
+ }
+
+ /**
+ * Test {@code getTime()} does no time zone conversion because {@code TIME WITH LOCAL TIME ZONE}
+ * represents a global instant in time.
+ */
+ @Test public void testTime() throws SQLException {
+ value = 12345;
+
+ assertThat(instance.getTime(null), is(new Time(value)));
+ assertThat(instance.getTime(UTC), is(new Time(value)));
+ assertThat(instance.getTime(localCalendar), is(new Time(value)));
+ }
+
+ /**
+ * Test {@code getTimestamp()} does no time zone conversion because
+ * {@code TIME WITH LOCAL TIME ZONE} represents a global instant in time.
+ */
+ @Test public void testTimestamp() throws SQLException {
+ value = 0;
+ assertThat(instance.getTimestamp(localCalendar),
+ is(new Timestamp(0L)));
+
+ value = (int) (DateTimeUtils.MILLIS_PER_DAY - 1000);
+ assertThat(instance.getTimestamp(localCalendar),
+ is(new Timestamp(DateTimeUtils.MILLIS_PER_DAY - 1000)));
+ }
+
+ /**
+ * Test {@code getTimestamp()} does no time zone conversion because
+ * {@code TIME WITH LOCAL TIME ZONE} represents a global instant in time.
+ */
+ @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(new Timestamp(0L)));
+ assertThat(instance.getTimestamp(Calendar.getInstance(west, Locale.ROOT)),
+ is(new Timestamp(0L)));
+ }
+
+ /**
+ * 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
index 94da748a73..10328156c3 100644
--- a/core/src/test/java/org/apache/calcite/avatica/util/TimestampAccessorTest.java
+++ b/core/src/test/java/org/apache/calcite/avatica/util/TimestampAccessorTest.java
@@ -41,6 +41,33 @@ public class TimestampAccessorTest {
private static final Calendar UTC =
Calendar.getInstance(TimeZone.getTimeZone("UTC"), Locale.ROOT);
+ // UTC+5:30
+ private static final TimeZone IST_ZONE = TimeZone.getTimeZone("Asia/Kolkata");
+
+ // UTC: 2014-09-30 15:28:27.356
+ private static final long DST_INSTANT = 1412090907356L;
+ private static final String DST_STRING = "2014-09-30 15:28:27";
+
+ // UTC: 1500-04-30 12:00:00.123
+ private static final long PRE_GREG_INSTANT = -14820580799877L;
+ private static final String PRE_GREG_STRING = "1500-04-30 12:00:00";
+
+ // These values are used to test timestamps around the Gregorian shift.
+ // Unix timestamps use the proleptic Gregorian calendar (Gregorian applied retroactively).
+ // JDBC uses the Julian calendar and skips 10 days in October 1582 to shift to the Gregorian.
+ // UTC: 1582-10-04 00:00:00
+ private static final long SHIFT_INSTANT_1 = -12219379200000L;
+ private static final String SHIFT_STRING_1 = "1582-10-04 00:00:00";
+ // UTC: 1582-10-05 00:00:00
+ private static final long SHIFT_INSTANT_2 = SHIFT_INSTANT_1 + DateTimeUtils.MILLIS_PER_DAY;
+ private static final String SHIFT_STRING_2 = "1582-10-05 00:00:00";
+ // UTC: 1582-10-16 00:00:00
+ private static final long SHIFT_INSTANT_3 = SHIFT_INSTANT_2 + DateTimeUtils.MILLIS_PER_DAY;
+ private static final String SHIFT_STRING_3 = "1582-10-16 00:00:00";
+ // UTC: 1582-10-17 00:00:00
+ private static final long SHIFT_INSTANT_4 = SHIFT_INSTANT_3 + DateTimeUtils.MILLIS_PER_DAY;
+ private static final String SHIFT_STRING_4 = "1582-10-17 00:00:00";
+
private Cursor.Accessor instance;
private Calendar localCalendar;
private Timestamp value;
@@ -51,26 +78,17 @@ public class TimestampAccessorTest {
*/
@Before public void before() {
final AbstractCursor.Getter getter = new LocalGetter();
- localCalendar = Calendar.getInstance(TimeZone.getDefault(), Locale.ROOT);
- instance = new AbstractCursor.TimestampAccessor(getter, localCalendar);
+ localCalendar = Calendar.getInstance(IST_ZONE, Locale.ROOT);
+ instance = new AbstractCursor.TimestampAccessor(getter, localCalendar, false);
}
/**
- * Test {@code getTimestamp()} returns the same value as the input timestamp for the local
+ * Test {@code getTimestamp()} returns the same instant as the input timestamp for the local
* calendar.
*/
@Test public void testTimestamp() throws SQLException {
- value = new Timestamp(0L);
+ value = new Timestamp(123456L);
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));
}
/**
@@ -82,13 +100,43 @@ public class TimestampAccessorTest {
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));
+ assertThat(
+ instance.getTimestamp(minusFiveCal),
+ is(new Timestamp(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));
+ assertThat(
+ instance.getTimestamp(plusFiveCal),
+ is(new Timestamp(-5 * MILLIS_PER_HOUR)));
+ }
+
+ /**
+ * Test {@code getString()} returns the clock representation in UTC when the connection default
+ * calendar is UTC.
+ */
+ @Test public void testStringWithUtc() throws SQLException {
+ localCalendar.setTimeZone(UTC.getTimeZone());
+ helpTestGetString();
+ }
+
+ /**
+ * Test {@code getString()} also returns the clock representation in UTC when the connection
+ * default calendar is *not* UTC.
+ */
+ @Test public void testStringWithDefaultTimeZone() throws SQLException {
+ helpTestGetString();
+ }
+
+ private void helpTestGetString() throws SQLException {
+ value = new Timestamp(0L);
+ assertThat(instance.getString(), is("1970-01-01 00:00:00"));
+
+ value = new Timestamp(DST_INSTANT);
+ assertThat(instance.getString(), is(DST_STRING));
+
+ value = new Timestamp(PRE_GREG_INSTANT);
+ assertThat(instance.getString(), is(PRE_GREG_STRING));
}
/**
@@ -99,10 +147,10 @@ public class TimestampAccessorTest {
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")));
+ assertThat(instance.getDate(null), 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")));
+ assertThat(instance.getDate(null), is(Date.valueOf("1500-04-30")));
}
/**
@@ -131,10 +179,10 @@ public class TimestampAccessorTest {
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")));
+ assertThat(instance.getTime(null), 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"));
+ assertThat(instance.getTime(null).toString(), is("15:28:27"));
}
/**
@@ -156,17 +204,20 @@ public class TimestampAccessorTest {
}
/**
- * Test {@code getString()} returns the same value as the input timestamp.
+ * Test {@code getString()} always returns the same string, regardless of the connection default
+ * calendar.
*/
@Test public void testString() throws SQLException {
- value = Timestamp.valueOf("1970-01-01 00:00:00");
+ localCalendar.setTimeZone(UTC.getTimeZone());
+
+ value = new Timestamp(0L);
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 = new Timestamp(DST_INSTANT);
+ assertThat(instance.getString(), is(DST_STRING));
- value = Timestamp.valueOf("1500-04-30 12:00:00.123");
- assertThat(instance.getString(), is("1500-04-30 12:00:00"));
+ value = new Timestamp(PRE_GREG_INSTANT);
+ assertThat(instance.getString(), is(PRE_GREG_STRING));
}
/**
@@ -174,28 +225,14 @@ public class TimestampAccessorTest {
* 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"));
+ value = new Timestamp(SHIFT_INSTANT_1);
+ assertThat(instance.getString(), is(SHIFT_STRING_1));
+ value = new Timestamp(SHIFT_INSTANT_2);
+ assertThat(instance.getString(), is(SHIFT_STRING_2));
+ value = new Timestamp(SHIFT_INSTANT_3);
+ assertThat(instance.getString(), is(SHIFT_STRING_3));
+ value = new Timestamp(SHIFT_INSTANT_4);
+ assertThat(instance.getString(), is(SHIFT_STRING_4));
}
/**
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
index f4c06aa939..a7a2ebbb22 100644
--- a/core/src/test/java/org/apache/calcite/avatica/util/TimestampFromNumberAccessorTest.java
+++ b/core/src/test/java/org/apache/calcite/avatica/util/TimestampFromNumberAccessorTest.java
@@ -37,6 +37,14 @@
*/
public class TimestampFromNumberAccessorTest {
+ // UTC: 2014-09-30 15:28:27.356
+ private static final long DST_INSTANT = 1412090907356L;
+ private static final String DST_STRING = "2014-09-30 15:28:27";
+
+ // UTC: 1500-04-30 12:00:00.123 (JULIAN CALENDAR)
+ private static final long PRE_GREG_INSTANT = -14821444799877L;
+ private static final String PRE_GREG_STRING = "1500-04-30 12:00:00";
+
private Cursor.Accessor instance;
private Calendar localCalendar;
private Object value;
@@ -48,8 +56,7 @@ public class TimestampFromNumberAccessorTest {
@Before public void before() {
final AbstractCursor.Getter getter = new LocalGetter();
localCalendar = Calendar.getInstance(TimeZone.getDefault(), Locale.ROOT);
- instance = new AbstractCursor.TimestampFromNumberAccessor(getter,
- localCalendar);
+ instance = new AbstractCursor.TimestampFromNumberAccessor(getter, localCalendar, false);
}
/**
@@ -126,11 +133,11 @@ public class TimestampFromNumberAccessorTest {
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 = DST_INSTANT;
+ assertThat(instance.getString(), is(DST_STRING));
- value = -14821444799877L; // 1500-04-30 12:00:00.123
- assertThat(instance.getString(), is("1500-04-30 12:00:00"));
+ value = PRE_GREG_INSTANT;
+ assertThat(instance.getString(), is(PRE_GREG_STRING));
}
/**
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
index afdd37bc5c..506046c54c 100644
--- a/core/src/test/java/org/apache/calcite/avatica/util/TimestampFromUtilDateAccessorTest.java
+++ b/core/src/test/java/org/apache/calcite/avatica/util/TimestampFromUtilDateAccessorTest.java
@@ -42,6 +42,30 @@ public class TimestampFromUtilDateAccessorTest {
private static final Calendar UTC =
Calendar.getInstance(TimeZone.getTimeZone("UTC"), Locale.ROOT);
+ // UTC: 2014-09-30 15:28:27.356
+ private static final long DST_INSTANT = 1412090907356L;
+ private static final String DST_STRING = "2014-09-30 15:28:27";
+
+ // UTC: 1500-04-30 12:00:00.123
+ private static final long PRE_GREG_INSTANT = -14820580799877L;
+ private static final String PRE_GREG_STRING = "1500-04-30 12:00:00";
+
+ // These values are used to test timestamps around the Gregorian shift.
+ // Unix timestamps use the proleptic Gregorian calendar (Gregorian applied retroactively).
+ // JDBC uses the Julian calendar and skips 10 days in October 1582 to shift to the Gregorian.
+ // UTC: 1582-10-04 00:00:00
+ private static final long SHIFT_INSTANT_1 = -12219379200000L;
+ private static final String SHIFT_STRING_1 = "1582-10-04 00:00:00";
+ // UTC: 1582-10-05 00:00:00
+ private static final long SHIFT_INSTANT_2 = SHIFT_INSTANT_1 + DateTimeUtils.MILLIS_PER_DAY;
+ private static final String SHIFT_STRING_2 = "1582-10-05 00:00:00";
+ // UTC: 1582-10-16 00:00:00
+ private static final long SHIFT_INSTANT_3 = SHIFT_INSTANT_2 + DateTimeUtils.MILLIS_PER_DAY;
+ private static final String SHIFT_STRING_3 = "1582-10-16 00:00:00";
+ // UTC: 1582-10-17 00:00:00
+ private static final long SHIFT_INSTANT_4 = SHIFT_INSTANT_3 + DateTimeUtils.MILLIS_PER_DAY;
+ private static final String SHIFT_STRING_4 = "1582-10-17 00:00:00";
+
private Cursor.Accessor instance;
private Calendar localCalendar;
private Date value;
@@ -53,7 +77,7 @@ public class TimestampFromUtilDateAccessorTest {
@Before public void before() {
final AbstractCursor.Getter getter = new LocalGetter();
localCalendar = Calendar.getInstance(TimeZone.getDefault(), Locale.ROOT);
- instance = new AbstractCursor.TimestampFromUtilDateAccessor(getter, localCalendar);
+ instance = new AbstractCursor.TimestampFromUtilDateAccessor(getter, localCalendar, false);
}
/**
@@ -162,14 +186,14 @@ public class TimestampFromUtilDateAccessorTest {
* 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");
+ value = new Timestamp(0L);
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 = new Timestamp(DST_INSTANT);
+ assertThat(instance.getString(), is(DST_STRING));
- value = Timestamp.valueOf("1500-04-30 12:00:00.123");
- assertThat(instance.getString(), is("1500-04-30 12:00:00"));
+ value = new Timestamp(PRE_GREG_INSTANT);
+ assertThat(instance.getString(), is(PRE_GREG_STRING));
}
/**
@@ -177,12 +201,14 @@ public class TimestampFromUtilDateAccessorTest {
* 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"));
+ value = new Timestamp(SHIFT_INSTANT_1);
+ assertThat(instance.getString(), is(SHIFT_STRING_1));
+ value = new Timestamp(SHIFT_INSTANT_2);
+ assertThat(instance.getString(), is(SHIFT_STRING_2));
+ value = new Timestamp(SHIFT_INSTANT_3);
+ assertThat(instance.getString(), is(SHIFT_STRING_3));
+ value = new Timestamp(SHIFT_INSTANT_4);
+ assertThat(instance.getString(), is(SHIFT_STRING_4));
}
/**
@@ -194,11 +220,11 @@ public class TimestampFromUtilDateAccessorTest {
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(DST_INSTANT);
+ assertThat(instance.getString(), is(DST_STRING));
- value = new Timestamp(-14820580799877L /* 1500-04-30 12:00:00.123 UTC */);
- assertThat(instance.getString(), is("1500-04-30 12:00:00"));
+ value = new Timestamp(PRE_GREG_INSTANT);
+ assertThat(instance.getString(), is(PRE_GREG_STRING));
}
/**
diff --git a/core/src/test/java/org/apache/calcite/avatica/util/TimestampWithLocalTimeZoneAccessorTest.java b/core/src/test/java/org/apache/calcite/avatica/util/TimestampWithLocalTimeZoneAccessorTest.java
new file mode 100644
index 0000000000..c5af5fe4a1
--- /dev/null
+++ b/core/src/test/java/org/apache/calcite/avatica/util/TimestampWithLocalTimeZoneAccessorTest.java
@@ -0,0 +1,281 @@
+/*
+ * 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.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+/**
+ * Test conversions from SQL {@link Timestamp} to JDBC types in
+ * {@link AbstractCursor.TimestampAccessor}.
+ */
+public class TimestampWithLocalTimeZoneAccessorTest {
+
+ private static final Calendar UTC =
+ Calendar.getInstance(TimeZone.getTimeZone("UTC"), Locale.ROOT);
+
+ // UTC+5:30
+ private static final TimeZone IST_ZONE = TimeZone.getTimeZone("Asia/Kolkata");
+
+ // UTC: 2014-09-30 15:28:27.356
+ private static final long DST_INSTANT = 1412090907356L;
+ private static final String DST_STRING = "2014-09-30 15:28:27";
+
+ // UTC: 1500-04-30 12:00:00.123
+ private static final long PRE_GREG_INSTANT = -14820580799877L;
+ private static final String PRE_GREG_STRING = "1500-04-30 12:00:00";
+
+ // These values are used to test timestamps around the Gregorian shift.
+ // Unix timestamps use the proleptic Gregorian calendar (Gregorian applied retroactively).
+ // JDBC uses the Julian calendar and skips 10 days in October 1582 to shift to the Gregorian.
+ // UTC: 1582-10-04 00:00:00
+ private static final long SHIFT_INSTANT_1 = -12219379200000L;
+ private static final String SHIFT_STRING_1 = "1582-10-04 00:00:00";
+ // UTC: 1582-10-05 00:00:00
+ private static final long SHIFT_INSTANT_2 = SHIFT_INSTANT_1 + DateTimeUtils.MILLIS_PER_DAY;
+ private static final String SHIFT_STRING_2 = "1582-10-05 00:00:00";
+ // UTC: 1582-10-16 00:00:00
+ private static final long SHIFT_INSTANT_3 = SHIFT_INSTANT_2 + DateTimeUtils.MILLIS_PER_DAY;
+ private static final String SHIFT_STRING_3 = "1582-10-16 00:00:00";
+ // UTC: 1582-10-17 00:00:00
+ private static final long SHIFT_INSTANT_4 = SHIFT_INSTANT_3 + DateTimeUtils.MILLIS_PER_DAY;
+ private static final String SHIFT_STRING_4 = "1582-10-17 00:00:00";
+
+ 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(IST_ZONE, Locale.ROOT);
+ instance = new AbstractCursor.TimestampAccessor(getter, localCalendar, true);
+ }
+
+ /**
+ * 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(0L));
+
+ final TimeZone plusFiveZone = TimeZone.getTimeZone("GMT+5:00");
+ final Calendar plusFiveCal = Calendar.getInstance(plusFiveZone, Locale.ROOT);
+ assertThat(instance.getTimestamp(plusFiveCal).getTime(),
+ is(0L));
+ }
+
+ /**
+ * 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(0L));
+
+ final TimeZone plusFiveZone = TimeZone.getTimeZone("GMT+5:00");
+ final Calendar plusFiveCal = Calendar.getInstance(plusFiveZone, Locale.ROOT);
+ assertThat(instance.getDate(plusFiveCal).getTime(),
+ is(0L));
+ }
+
+ /**
+ * 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(0L));
+
+ final TimeZone plusFiveZone = TimeZone.getTimeZone("GMT+5:00");
+ final Calendar plusFiveCal = Calendar.getInstance(plusFiveZone, Locale.ROOT);
+ assertThat(instance.getTime(plusFiveCal).getTime(),
+ is(0L));
+ }
+
+ /**
+ * Test {@code getString()} always returns the same string, regardless of the connection default
+ * calendar.
+ */
+ @Test public void testString() throws SQLException {
+ localCalendar.setTimeZone(UTC.getTimeZone());
+
+ value = new Timestamp(0L);
+ assertThat(instance.getString(), is("1970-01-01 00:00:00"));
+
+ value = new Timestamp(DST_INSTANT);
+ assertThat(instance.getString(), is(DST_STRING));
+
+ value = new Timestamp(PRE_GREG_INSTANT);
+ assertThat(instance.getString(), is(PRE_GREG_STRING));
+ }
+
+ /**
+ * Test {@code getString()} shifts between the standard Gregorian calendar and the proleptic
+ * Gregorian calendar.
+ */
+ @Test public void testStringWithGregorianShift() throws SQLException {
+ localCalendar.setTimeZone(UTC.getTimeZone());
+
+ value = new Timestamp(SHIFT_INSTANT_1);
+ assertThat(instance.getString(), is(SHIFT_STRING_1));
+ value = new Timestamp(SHIFT_INSTANT_2);
+ assertThat(instance.getString(), is(SHIFT_STRING_2));
+ value = new Timestamp(SHIFT_INSTANT_3);
+ assertThat(instance.getString(), is(SHIFT_STRING_3));
+ value = new Timestamp(SHIFT_INSTANT_4);
+ assertThat(instance.getString(), is(SHIFT_STRING_4));
+ }
+
+ /**
+ * Test {@code getString()} always returns the same string, regardless of the connection default
+ * 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(DST_INSTANT);
+ assertThat(instance.getString(), is(DST_STRING));
+
+ value = new Timestamp(PRE_GREG_INSTANT);
+ assertThat(instance.getString(), is(PRE_GREG_STRING));
+ }
+
+ /**
+ * Test {@code getString()} supports date range 0001-01-01 to 9999-12-31 required by ANSI SQL.
+ *
+ * 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/TimestampWithLocalTimeZoneFromNumberAccessorTest.java b/core/src/test/java/org/apache/calcite/avatica/util/TimestampWithLocalTimeZoneFromNumberAccessorTest.java
new file mode 100644
index 0000000000..420e2b6997
--- /dev/null
+++ b/core/src/test/java/org/apache/calcite/avatica/util/TimestampWithLocalTimeZoneFromNumberAccessorTest.java
@@ -0,0 +1,353 @@
+/*
+ * 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 TimestampWithLocalTimeZoneFromNumberAccessorTest {
+
+ // UTC+5:30
+ private static final TimeZone IST_ZONE = TimeZone.getTimeZone("Asia/Kolkata");
+
+ // Shifting from the Julian to Gregorian calendar required skipping 10 days.
+ private static final long GREGORIAN_SHIFT = 10 * DateTimeUtils.MILLIS_PER_DAY;
+
+ // UTC: 2014-09-30 15:28:27.356
+ private static final long DST_INSTANT = 1412090907356L;
+ private static final String DST_STRING = "2014-09-30 15:28:27";
+ private static final String DST_OFFSET_STRING = "2014-09-30 20:58:27";
+
+ // UTC: 1500-04-30 12:00:00.123 (JULIAN CALENDAR)
+ private static final long PRE_GREG_INSTANT = -14821444799877L;
+ private static final String PRE_GREG_STRING = "1500-04-30 12:00:00";
+ private static final String PRE_GREG_OFFSET_STRING = "1500-04-30 17:30:00";
+
+ // These values are used to test timestamps around the Gregorian shift.
+ // Unix timestamps use the proleptic Gregorian calendar (Gregorian applied retroactively).
+ // JDBC uses the Julian calendar and skips 10 days in October 1582 to shift to the Gregorian.
+ // UTC: 1582-10-04 00:00:00
+ private static final long SHIFT_INSTANT_1 = -12219379200000L;
+ // UTC: 1582-10-05 00:00:00
+ private static final long SHIFT_INSTANT_2 = SHIFT_INSTANT_1 + DateTimeUtils.MILLIS_PER_DAY;
+ // UTC: 1582-10-16 00:00:00
+ private static final long SHIFT_INSTANT_3 = SHIFT_INSTANT_2 + DateTimeUtils.MILLIS_PER_DAY;
+ // UTC: 1582-10-17 00:00:00
+ private static final long SHIFT_INSTANT_4 = SHIFT_INSTANT_3 + DateTimeUtils.MILLIS_PER_DAY;
+
+ 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(IST_ZONE, Locale.ROOT);
+ instance = new AbstractCursor.TimestampFromNumberAccessor(getter, localCalendar, true);
+ }
+
+ /**
+ * Test {@code getDate()} does no time zone conversion because
+ * {@code TIMESTAMP WITH LOCAL TIME ZONE} represents a global instant in time.
+ */
+ @Test public void testDate() throws SQLException {
+ value = 0L;
+ assertThat(instance.getDate(localCalendar),
+ is(new Date(0L)));
+
+ value = PRE_GREG_INSTANT;
+ assertThat(instance.getDate(localCalendar),
+ is(new Date(PRE_GREG_INSTANT + GREGORIAN_SHIFT)));
+ }
+
+ /**
+ * Test {@code getDate()} does no time zone conversion because
+ * {@code TIMESTAMP WITH LOCAL TIME ZONE} represents a global instant in time.
+ */
+ @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(0L));
+
+ final TimeZone plusFiveZone = TimeZone.getTimeZone("GMT+5:00");
+ final Calendar plusFiveCal = Calendar.getInstance(plusFiveZone, Locale.ROOT);
+ assertThat(instance.getDate(plusFiveCal).getTime(),
+ is(0L));
+ }
+
+ /**
+ * 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()} adjusts the string representation based on the default time zone.
+ */
+ @Test public void testString() throws SQLException {
+ value = 0;
+ assertThat(instance.getString(), is("1970-01-01 05:30:00"));
+
+ value = DST_INSTANT;
+ assertThat(instance.getString(), is(DST_OFFSET_STRING));
+
+ value = PRE_GREG_INSTANT;
+ assertThat(instance.getString(), is(PRE_GREG_OFFSET_STRING));
+ }
+
+ /**
+ * 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);
+ final String offset = String.format(Locale.ROOT, "1582-10-%02d 05:30:00", i);
+ value = DateTimeUtils.timestampStringToUnixDate(str);
+ assertThat(instance.getString(), is(offset));
+ }
+ }
+
+ /**
+ * 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 = DST_INSTANT;
+ assertThat(instance.getString(), is(DST_STRING));
+
+ value = PRE_GREG_INSTANT;
+ assertThat(instance.getString(), is(PRE_GREG_STRING));
+ }
+
+ /**
+ * Test {@code getString()} supports date range 0001-01-01 to 9999-12-31 required by ANSI SQL.
+ */
+ @Test public void testStringWithAnsiDateRange() throws SQLException {
+ // Indian Standard Time is applied retroactively in years prior to 1900.
+ for (int i = 1; i < 1900; ++i) {
+ assertString(
+ String.format(Locale.ROOT, "%04d-01-01 00:00:00", i),
+ String.format(Locale.ROOT, "%04d-01-01 05:30:00", i));
+ }
+ // IST was imposed nationwide by British colonisers in 1906.
+ // Prior to this, Kolkata local time was about UTC+05:21:10.
+ for (int i = 1900; i < 1906; ++i) {
+ assertString(
+ String.format(Locale.ROOT, "%04d-01-01 00:00:00", i),
+ String.format(Locale.ROOT, "%04d-01-01 05:21:10", i));
+ }
+ // Back to IST until 1942.
+ for (int i = 1906; i < 1942; ++i) {
+ assertString(
+ String.format(Locale.ROOT, "%04d-01-01 00:00:00", i),
+ String.format(Locale.ROOT, "%04d-01-01 05:30:00", i));
+ }
+ // As an Allied Nation of World War II, India observed DST year-round from 1942 to 1945,
+ // known as "War Time".
+ for (int i = 1942; i < 1946; ++i) {
+ assertString(
+ String.format(Locale.ROOT, "%04d-01-01 00:00:00", i),
+ String.format(Locale.ROOT, "%04d-01-01 06:30:00", i));
+ }
+ // Back to IST for posterity.
+ for (int i = 1946; i < 10000; ++i) {
+ assertString(
+ String.format(Locale.ROOT, "%04d-01-01 00:00:00", i),
+ String.format(Locale.ROOT, "%04d-01-01 05:30:00", i));
+ }
+ }
+
+ private void assertString(String valueString, String expected) throws SQLException {
+ value = DateTimeUtils.timestampStringToUnixDate(valueString);
+ assertThat(instance.getString(), is(expected));
+ }
+
+ /**
+ * 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), is(new Time(0L)));
+
+ value = DST_INSTANT;
+ assertThat(instance.getTime(localCalendar), is(new Time(DST_INSTANT)));
+ }
+
+ /**
+ * 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(new Time(0L)));
+ assertThat(instance.getTime(Calendar.getInstance(west, Locale.ROOT)),
+ is(new Time(0L)));
+ }
+
+ /**
+ * 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(new Timestamp(0L)));
+
+ value = DST_INSTANT;
+ assertThat(instance.getTimestamp(localCalendar),
+ is(new Timestamp(DST_INSTANT)));
+
+ value = PRE_GREG_INSTANT;
+ assertThat(instance.getTimestamp(localCalendar),
+ is(new Timestamp(PRE_GREG_INSTANT + GREGORIAN_SHIFT)));
+ }
+
+ /**
+ * Test {@code getTimestamp()} shifts between the standard Gregorian calendar and the proleptic
+ * Gregorian calendar.
+ */
+ @Test public void testTimestampWithGregorianShift() throws SQLException {
+ value = SHIFT_INSTANT_1;
+ assertThat(instance.getTimestamp(localCalendar),
+ is(new Timestamp(SHIFT_INSTANT_1 + GREGORIAN_SHIFT)));
+
+ value = SHIFT_INSTANT_2;
+ assertThat(instance.getTimestamp(localCalendar), is(new Timestamp(SHIFT_INSTANT_2)));
+
+ value = SHIFT_INSTANT_3;
+ assertThat(instance.getTimestamp(localCalendar), is(new Timestamp(SHIFT_INSTANT_3)));
+
+ value = SHIFT_INSTANT_4;
+ assertThat(instance.getTimestamp(localCalendar), is(new Timestamp(SHIFT_INSTANT_4)));
+ }
+
+ /**
+ * 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 < 1943; ++i) {
+ assertTimestamp(i, TimeZone.getDefault().getRawOffset());
+ }
+ for (int i = 1943; i < 1946; ++i) {
+ assertTimestamp(i, TimeZone.getDefault().getRawOffset() + DateTimeUtils.MILLIS_PER_HOUR);
+ }
+ for (int i = 1946; i < 1949; ++i) {
+ assertTimestamp(i, TimeZone.getDefault().getRawOffset());
+ }
+ for (int i = 1949; i < 1950; ++i) {
+ assertTimestamp(i, TimeZone.getDefault().getRawOffset() + DateTimeUtils.MILLIS_PER_HOUR);
+ }
+ for (int i = 1950; i < 10000; ++i) {
+ assertTimestamp(i, TimeZone.getDefault().getRawOffset());
+ }
+ }
+
+ private void assertTimestamp(int year, long offset) throws SQLException {
+ final String valueString = String.format(Locale.ROOT, "%04d-01-01 00:00:00.0", year);
+ value = DateTimeUtils.timestampStringToUnixDate(valueString);
+ assertThat(instance.getTimestamp(localCalendar),
+ is(new Timestamp(Timestamp.valueOf(valueString).getTime() + offset)));
+ }
+
+ /**
+ * 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(new Timestamp(0L)));
+ assertThat(instance.getTimestamp(Calendar.getInstance(west, Locale.ROOT)),
+ is(new Timestamp(0L)));
+ }
+
+ /**
+ * 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/TimestampWithLocalTimeZoneFromUtilDateAccessorTest.java b/core/src/test/java/org/apache/calcite/avatica/util/TimestampWithLocalTimeZoneFromUtilDateAccessorTest.java
new file mode 100644
index 0000000000..3ee216f6f4
--- /dev/null
+++ b/core/src/test/java/org/apache/calcite/avatica/util/TimestampWithLocalTimeZoneFromUtilDateAccessorTest.java
@@ -0,0 +1,282 @@
+/*
+ * 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.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+/**
+ * Test conversions from SQL {@link Date} to JDBC types in
+ * {@link AbstractCursor.TimestampFromUtilDateAccessor}.
+ */
+public class TimestampWithLocalTimeZoneFromUtilDateAccessorTest {
+
+ private static final Calendar UTC =
+ Calendar.getInstance(TimeZone.getTimeZone("UTC"), Locale.ROOT);
+
+ // UTC+5:30
+ private static final TimeZone IST_ZONE = TimeZone.getTimeZone("Asia/Kolkata");
+
+ // UTC: 2014-09-30 15:28:27.356
+ private static final long DST_INSTANT = 1412090907356L;
+ private static final String DST_STRING = "2014-09-30 15:28:27";
+ private static final String DST_OFFSET_STRING = "2014-09-30 20:58:27";
+
+ // UTC: 1500-04-30 12:00:00.123
+ private static final long PRE_GREG_INSTANT = -14820580799877L;
+ private static final String PRE_GREG_STRING = "1500-04-30 12:00:00";
+ private static final String PRE_GREG_OFFSET_STRING = "1500-04-30 17:30:00";
+
+ // These values are used to test timestamps around the Gregorian shift.
+ // Unix timestamps use the proleptic Gregorian calendar (Gregorian applied retroactively).
+ // JDBC uses the Julian calendar and skips 10 days in October 1582 to shift to the Gregorian.
+ // UTC: 1582-10-04 00:00:00
+ private static final long SHIFT_INSTANT_1 = -12219379200000L;
+ private static final String SHIFT_OFFSET_STRING_1 = "1582-10-04 05:30:00";
+ // UTC: 1582-10-05 00:00:00
+ private static final long SHIFT_INSTANT_2 = SHIFT_INSTANT_1 + DateTimeUtils.MILLIS_PER_DAY;
+ private static final String SHIFT_OFFSET_STRING_2 = "1582-10-05 05:30:00";
+ // UTC: 1582-10-16 00:00:00
+ private static final long SHIFT_INSTANT_3 = SHIFT_INSTANT_2 + DateTimeUtils.MILLIS_PER_DAY;
+ private static final String SHIFT_OFFSET_STRING_3 = "1582-10-16 05:30:00";
+ // UTC: 1582-10-17 00:00:00
+ private static final long SHIFT_INSTANT_4 = SHIFT_INSTANT_3 + DateTimeUtils.MILLIS_PER_DAY;
+ private static final String SHIFT_OFFSET_STRING_4 = "1582-10-17 05:30:00";
+
+ 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(IST_ZONE, Locale.ROOT);
+ instance = new AbstractCursor.TimestampFromUtilDateAccessor(getter, localCalendar, true);
+ }
+
+ /**
+ * Test {@code getTimestamp()} does no time zone conversion because
+ * {@code TIMESTAMP WITH LOCAL TIME ZONE} represents a global instant in time.
+ */
+ @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()} does no time zone conversion because
+ * {@code TIMESTAMP WITH LOCAL TIME ZONE} represents a global instant in time.
+ */
+ @Test public void testTimestampWithCalendar() throws SQLException {
+ value = new Date(0L);
+
+ final TimeZone minusFiveZone = TimeZone.getTimeZone("GMT-5:00");
+ final Calendar minusFiveCal = Calendar.getInstance(minusFiveZone, Locale.ROOT);
+ assertThat(instance.getTimestamp(minusFiveCal).getTime(),
+ is(0L));
+
+ final TimeZone plusFiveZone = TimeZone.getTimeZone("GMT+5:00");
+ final Calendar plusFiveCal = Calendar.getInstance(plusFiveZone, Locale.ROOT);
+ assertThat(instance.getTimestamp(plusFiveCal).getTime(),
+ is(0L));
+ }
+
+ /**
+ * Test {@code getDate()} does no time zone conversion because
+ * {@code TIMESTAMP WITH LOCAL TIME ZONE} represents a global instant in time.
+ */
+ @Test public void testDate() throws SQLException {
+ value = new Date(0L);
+ assertThat(instance.getDate(null), is(value));
+
+ value = new Date(DST_INSTANT);
+ assertThat(instance.getDate(UTC), is(value));
+
+ value = new Date(PRE_GREG_INSTANT);
+ assertThat(instance.getDate(UTC), is(value));
+ }
+
+ /**
+ * Test {@code getDate()} does no time zone conversion because
+ * {@code TIMESTAMP WITH LOCAL TIME ZONE} represents a global instant in time.
+ */
+ @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(0L));
+
+ final TimeZone plusFiveZone = TimeZone.getTimeZone("GMT+5:00");
+ final Calendar plusFiveCal = Calendar.getInstance(plusFiveZone, Locale.ROOT);
+ assertThat(instance.getDate(plusFiveCal).getTime(),
+ is(0L));
+ }
+
+ /**
+ * Test {@code getTime()} does no time zone conversion because
+ * {@code TIMESTAMP WITH LOCAL TIME ZONE} represents a global instant in time.
+ */
+ @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()} does no time zone conversion because
+ * {@code TIMESTAMP WITH LOCAL TIME ZONE} represents a global instant in time.
+ */
+ @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(new Time(0L)));
+ assertThat(instance.getTime(Calendar.getInstance(west, Locale.ROOT)),
+ is(new Time(0L)));
+ }
+
+ /**
+ * Test {@code getString()} adjusts the string representation based on the default time zone.
+ */
+ @Test public void testStringWithLocalTimeZone() throws SQLException {
+ value = new Timestamp(0L);
+ assertThat(instance.getString(), is("1970-01-01 05:30:00"));
+
+ value = new Timestamp(DST_INSTANT);
+ assertThat(instance.getString(), is(DST_OFFSET_STRING));
+
+ value = new Timestamp(PRE_GREG_INSTANT);
+ assertThat(instance.getString(), is(PRE_GREG_OFFSET_STRING));
+ }
+
+ /**
+ * Test {@code getString()} shifts between the standard Gregorian calendar and the proleptic
+ * Gregorian calendar.
+ */
+ @Test public void testStringWithGregorianShift() throws SQLException {
+ value = new Timestamp(SHIFT_INSTANT_1);
+ assertThat(instance.getString(), is(SHIFT_OFFSET_STRING_1));
+ value = new Timestamp(SHIFT_INSTANT_2);
+ assertThat(instance.getString(), is(SHIFT_OFFSET_STRING_2));
+ value = new Timestamp(SHIFT_INSTANT_3);
+ assertThat(instance.getString(), is(SHIFT_OFFSET_STRING_3));
+ value = new Timestamp(SHIFT_INSTANT_4);
+ assertThat(instance.getString(), is(SHIFT_OFFSET_STRING_4));
+ }
+
+ /**
+ * 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(DST_INSTANT);
+ assertThat(instance.getString(), is(DST_STRING));
+
+ value = new Timestamp(PRE_GREG_INSTANT);
+ assertThat(instance.getString(), is(PRE_GREG_STRING));
+ }
+
+ /**
+ * Test {@code getString()} supports date range 0001-01-01 to 9999-12-31 required by ANSI SQL.
+ *
+ *
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 Date(0L);
+ assertThat(instance.getLong(), is(0L));
+
+ value = new Date(DST_INSTANT);
+ assertThat(instance.getLong(), is(DST_INSTANT));
+
+ value = new Date(PRE_GREG_INSTANT);
+ assertThat(instance.getLong(), is(PRE_GREG_INSTANT));
+ }
+
+ /**
+ * 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;
+ }
+ }
+}