From 8b000d781426b353cfb800f03f24f4eba399c210 Mon Sep 17 00:00:00 2001 From: Will Noble Date: Wed, 15 Feb 2023 15:26:58 -0800 Subject: [PATCH 1/2] Purely cosmetic test refactor --- .../calcite/avatica/util/AbstractCursor.java | 4 +- .../AvaticaResultSetConversionsTest.java | 75 ++++++++++++------- .../avatica/util/TimestampAccessorTest.java | 16 +++- .../util/TimestampFromNumberAccessorTest.java | 19 +++-- .../TimestampFromUtilDateAccessorTest.java | 16 +++- 5 files changed, 88 insertions(+), 42 deletions(-) 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..2a4cd64cec 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 @@ -173,14 +173,14 @@ protected Accessor createAccessor(ColumnMetaData columnMetaData, 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); 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..edc6ad5149 100644 --- a/core/src/test/java/org/apache/calcite/avatica/AvaticaResultSetConversionsTest.java +++ b/core/src/test/java/org/apache/calcite/avatica/AvaticaResultSetConversionsTest.java @@ -71,6 +71,29 @@ */ @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"; + + // 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; + + // UTC: 00:24:36.123 + private static final long VALID_TIME = 1476123L; + + // UTC: 41:05:12.242 + private static final long OVERFLOW_TIME = 147912242L; + /** * A fake test driver for test. */ @@ -215,15 +238,15 @@ 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), + 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) }); @@ -653,7 +676,7 @@ private TimeArrayAccessorTestHelper(Getter g) { ColumnMetaData.scalar(Types.TIME, "TIME", ColumnMetaData.Rep.NUMBER); Array expectedArray = new ArrayFactoryImpl(TimeZone.getTimeZone("UTC")).createArray( - intType, Arrays.asList(1476123L, 147912242L)); + intType, Arrays.asList(VALID_TIME, OVERFLOW_TIME)); assertTrue(ArrayImpl.equalContents(expectedArray, g.getArray(resultSet))); } } @@ -671,7 +694,7 @@ private TimestampArrayAccessorTestHelper(Getter g) { ColumnMetaData.scalar(Types.TIMESTAMP, "TIMESTAMP", ColumnMetaData.Rep.PRIMITIVE_LONG); Array expectedArray = new ArrayFactoryImpl(TimeZone.getTimeZone("UTC")).createArray( - intType, Arrays.asList(1476130718123L, 1479123123242L)); + intType, Arrays.asList(DST_INSTANT, STANDARD_INSTANT)); assertTrue(ArrayImpl.equalContents(expectedArray, g.getArray(resultSet))); } } @@ -986,7 +1009,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 +1033,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 +1046,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,23 +1054,23 @@ 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)); } } @@ -1060,7 +1083,7 @@ private TimestampAccessorTestHelper(Getter g) { } @Override public void testGetString(ResultSet resultSet) throws SQLException { - assertEquals("2016-10-10 20:18:38", g.getString(resultSet)); + assertEquals(DST_TIMESTAMP_STRING, g.getString(resultSet)); } @Override public void testGetBoolean(ResultSet resultSet) throws SQLException { @@ -1068,32 +1091,32 @@ 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)); + 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)); + 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)); + assertEquals(new Timestamp(DST_INSTANT), g.getTimestamp(resultSet, calendar)); } } @@ -1110,7 +1133,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; @@ -1282,17 +1305,17 @@ 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 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..98d20c00f4 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,14 @@ public class TimestampAccessorTest { 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 (PROLEPTIC GREGORIAN CALENDAR) + private static final long PRE_GREG_INSTANT = -14820580799877L; + private static final String PRE_GREG_STRING = "1500-04-30 12:00:00"; + private Cursor.Accessor instance; private Calendar localCalendar; private Timestamp value; @@ -191,11 +199,11 @@ public class TimestampAccessorTest { 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 */); - 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/TimestampFromNumberAccessorTest.java b/core/src/test/java/org/apache/calcite/avatica/util/TimestampFromNumberAccessorTest.java index f4c06aa939..0b2f4c8cb3 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); } /** @@ -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..bdc7db1fa2 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,14 @@ 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 (PROLEPTIC GREGORIAN CALENDAR) + private static final long PRE_GREG_INSTANT = -14820580799877L; + private static final String PRE_GREG_STRING = "1500-04-30 12:00:00"; + private Cursor.Accessor instance; private Calendar localCalendar; private Date value; @@ -194,11 +202,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)); } /** From 63579d6ea8b33ed5e7be16bc4b4903a2b7619c46 Mon Sep 17 00:00:00 2001 From: Will Noble Date: Fri, 13 Jan 2023 18:57:31 -0800 Subject: [PATCH 2/2] [CALCITE-5446] Add support for TIMESTAMP WITH LOCAL TIME ZONE --- .../calcite/avatica/util/AbstractCursor.java | 97 +++-- .../AvaticaResultSetConversionsTest.java | 175 +++++++-- .../avatica/util/TimeAccessorTest.java | 66 ++-- .../util/TimeFromNumberAccessorTest.java | 127 +++---- .../TimeWithLocalTimeZoneAccessorTest.java | 117 ++++++ ...thLocalTimeZoneFromNumberAccessorTest.java | 151 ++++++++ .../avatica/util/TimestampAccessorTest.java | 129 ++++--- .../util/TimestampFromNumberAccessorTest.java | 2 +- .../TimestampFromUtilDateAccessorTest.java | 44 ++- ...imestampWithLocalTimeZoneAccessorTest.java | 281 ++++++++++++++ ...thLocalTimeZoneFromNumberAccessorTest.java | 353 ++++++++++++++++++ ...LocalTimeZoneFromUtilDateAccessorTest.java | 282 ++++++++++++++ 12 files changed, 1594 insertions(+), 230 deletions(-) create mode 100644 core/src/test/java/org/apache/calcite/avatica/util/TimeWithLocalTimeZoneAccessorTest.java create mode 100644 core/src/test/java/org/apache/calcite/avatica/util/TimeWithLocalTimeZoneFromNumberAccessorTest.java create mode 100644 core/src/test/java/org/apache/calcite/avatica/util/TimestampWithLocalTimeZoneAccessorTest.java create mode 100644 core/src/test/java/org/apache/calcite/avatica/util/TimestampWithLocalTimeZoneFromNumberAccessorTest.java create mode 100644 core/src/test/java/org/apache/calcite/avatica/util/TimestampWithLocalTimeZoneFromUtilDateAccessorTest.java 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 2a4cd64cec..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,26 +151,34 @@ 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); } @@ -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 edc6ad5149..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; @@ -81,18 +85,34 @@ public class AvaticaResultSetConversionsTest { 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. @@ -175,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, @@ -198,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), @@ -239,6 +271,9 @@ public TestMetaImpl(AvaticaConnection connection) { new Object[] { true, (byte) 1, (short) 2, 3, 4L, 5.0f, 6.0d, "testvalue", 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)), @@ -509,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); @@ -657,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))); } @@ -675,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(VALID_TIME, OVERFLOW_TIME)); + new ArrayFactoryImpl(DateTimeUtils.DEFAULT_ZONE).createArray( + intType, Arrays.asList(REVERSE_OFFSET_VALID_TIME, REVERSE_OFFSET_OVERFLOW_TIME)); assertTrue(ArrayImpl.equalContents(expectedArray, g.getArray(resultSet))); } } @@ -693,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(DST_INSTANT, STANDARD_INSTANT)); + new ArrayFactoryImpl(DateTimeUtils.DEFAULT_ZONE).createArray( + intType, Arrays.asList(REVERSE_OFFSET_DST_INSTANT, REVERSE_OFFSET_STANDARD_INSTANT)); assertTrue(ArrayImpl.equalContents(expectedArray, g.getArray(resultSet))); } } @@ -1077,13 +1121,21 @@ private TimeAccessorTestHelper(Getter g) { /** * 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(DST_TIMESTAMP_STRING, g.getString(resultSet)); + assertEquals(expectedString, g.getString(resultSet)); } @Override public void testGetBoolean(ResultSet resultSet) throws SQLException { @@ -1107,17 +1159,51 @@ private TimestampAccessorTestHelper(Getter g) { } @Override public void testGetDate(ResultSet resultSet, Calendar calendar) throws SQLException { + // 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 { + // 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 { + // 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)); + } } /** @@ -1141,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 @@ -1182,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"))); } @@ -1318,6 +1428,11 @@ public void testGetTimestamp() throws SQLException { testHelper.testGetTimestamp(resultSet, UTC_CALENDAR); } + @Test + public void testGetTimestampDefaultCalendar() throws SQLException { + testHelper.testGetTimestampDefaultCalendar(resultSet); + } + @Test public void getURL() throws SQLException { testHelper.getURL(resultSet); 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 98d20c00f4..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,14 +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 (PROLEPTIC GREGORIAN CALENDAR) + // 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; @@ -59,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)); } /** @@ -90,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)); } /** @@ -107,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"))); } /** @@ -139,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")); } /** @@ -164,36 +204,10 @@ 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"); - assertThat(instance.getString(), is("1970-01-01 00:00:00")); - - value = Timestamp.valueOf("2014-09-30 15:28:27.356"); - assertThat(instance.getString(), is("2014-09-30 15:28:27")); - - value = Timestamp.valueOf("1500-04-30 12:00:00.123"); - assertThat(instance.getString(), is("1500-04-30 12:00:00")); - } - - /** - * Test {@code getString()} shifts between the standard Gregorian calendar and the proleptic - * Gregorian calendar. - */ - @Test public void testStringWithGregorianShift() throws SQLException { - value = Timestamp.valueOf("1582-10-04 00:00:00"); - assertThat(instance.getString(), is("1582-10-04 00:00:00")); - value = Timestamp.valueOf("1582-10-05 00:00:00"); - assertThat(instance.getString(), is("1582-10-15 00:00:00")); - value = Timestamp.valueOf("1582-10-15 00:00:00"); - assertThat(instance.getString(), is("1582-10-15 00:00:00")); - } - - /** - * Test {@code getString()} returns dates relative to the local calendar. - */ - @Test public void testStringWithUtc() throws SQLException { localCalendar.setTimeZone(UTC.getTimeZone()); value = new Timestamp(0L); @@ -206,6 +220,21 @@ public class TimestampAccessorTest { 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 { + 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()} supports date range 0001-01-01 to 9999-12-31 required by ANSI SQL. * 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 0b2f4c8cb3..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 @@ -56,7 +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); } /** 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 bdc7db1fa2..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 @@ -46,10 +46,26 @@ public class TimestampFromUtilDateAccessorTest { 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 (PROLEPTIC GREGORIAN CALENDAR) + // 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; @@ -61,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); } /** @@ -170,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)); } /** @@ -185,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)); } /** 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; + } + } +}