diff --git a/src/documentation/release_notes.md b/src/documentation/release_notes.md index ef67803f8c..e841309602 100644 --- a/src/documentation/release_notes.md +++ b/src/documentation/release_notes.md @@ -781,8 +781,8 @@ Firebird 4 time zone support ---------------------------- Added support for the Firebird 4 `TIME WITH TIME ZONE` and `TIMESTAMP WITH TIME -ZONE` types. See the Firebird 4 release notes (and `doc/sql.extensions/README.time_zone.md` -in the Firebird installation) for details on these types. +ZONE` types. See the Firebird 4 release notes and `doc/sql.extensions/README.time_zone.md` +in the Firebird installation for details on these types. The time zone types are supported under Java 8 and higher, using the Java 8 (or higher) version of Jaybird. Time zone types are not supported under Java 7 and @@ -841,8 +841,9 @@ marked with * are not defined in JDBC) The legacy JDBC types `java.sql.Time`, `java.sql.Timestamp` and `java.sql.Date` are not supported, nor are `java.time.LocalTime`, `java.time.LocalDateTime` or `java.time.LocalDate`. Supporting these types would be ambiguous. If you need to -use these, then either enable legacy time zone bind, or define or cast your -columns as `TIME` or `TIMESTAMP`. **NOTE: This is not final yet** +use these, then either apply the necessary conversions yourself, enable legacy +time zone bind, or define or cast your columns as `TIME` or `TIMESTAMP`. +**NOTE: This is not final yet** Jaybird also does not support non-standard extensions like `java.time.Instant`, or `java.time.ZonedDateTime`. If there is interest, we may add them in the @@ -850,38 +851,67 @@ future. ### Connection property sessionTimeZone ### -The connection property `sessionTimeZone` (alias `session_time_zone`) specifies -the Firebird 4 session time zone (see `SET TIME ZONE` in Firebird 4 -documentation). By default, Jaybird will use the JVM default time zone as -reported by `java.util.TimeZone.getDefault().getID()`. +The connection property `sessionTimeZone` (alias `session_time_zone`) does two +things: -The session time zone is used for conversion from `WITH TIME ZONE` values to -`WITHOUT TIME ZONE` values (ie using cast or with legacy time zone bind), and -for the value of `LOCALTIME`, `LOCALTIMESTAMP` `CURRENT_TIME` and +1. specifies the Firebird 4 session time zone (see Firebird 4 documentation), +2. specifies the time zone to use when converting values to the legacy JDBC + datetime types (all Firebird version). + +By default, Jaybird will use the JVM default time zone as reported by +`java.util.TimeZone.getDefault().getID()`. Using the JVM default time zone as +the default is the best option in the light of JDBC requirements with regard to +`java.sql.Time` and `java.sql.Timestamp` using the JVM default time zone. + +To use the default server time zone and the old behaviour to use the JVM default +time zone, set the connection property to `server`. This will result in the +conversion behaviour of Jaybird 3 and earlier. Be aware that this is +inconsistent if Firebird and Java are in different time zones. + +#### Firebird 4 session time zone #### + +The session time zone is used for conversion between `WITH TIME ZONE` values +and `WITHOUT TIME ZONE` values (ie using cast or with legacy time zone bind), +and for the value of `LOCALTIME`, `LOCALTIMESTAMP` `CURRENT_TIME` and `CURRENT_TIMESTAMP`, and other uses of the session time zone as documented in the Firebird 4 documentation. +The value of `sessionTimeZone` must be supported by Firebird 4. It is possible +that time zone identifiers used by Java are not supported by Firebird. If +Firebird does not know the session time zone, error (`Invalid time zone region: +`) is reported on connect. + The use of the JVM default time zone as the default session time zone will result in subtly different behaviour compared to previous versions of Jaybird -and - even with Jaybird 4 - Firebird 3 or earlier, as `LOCALTIMESTAMP` (etc) -values will now reflect the time in the JVM time zone and not the server time -zone. To use the server time zone and the old behaviour, set the connection -property to `server`. +and - even with Jaybird 4 - Firebird 3 or earlier, as current time values like +`LOCALTIMESTAMP` (etc) will now reflect the time in the JVM time zone, and not +the server time zone rebased on the JVM default time zone. + +As an example, with a Firebird in Europe/London and a Java application in +Europe/Amsterdam with Firebird time 12:00, in Jaybird 3, the Java application +will report this time as 12:00, in Jaybird 4 with Firebird 4, this will now +report 13:00, as that is the time in Amsterdam if it is 12:00 in London +(ignoring potential DST start/end differences). -Using the JVM default time zone as the default is the best option in the light -of JDBC requirements with regard to `java.sql.Time` and `java.sql.Timestamp` -using the JVM default time zone. +Other examples include values generated in triggers and default value clauses. -The value of `sessionTimeZone` must be supported by Firebird. It is possible -that time zone identifiers used by Java are not supported by Firebird. In that -case an error (`Invalid time zone region: `) is reported, and you -will need to specify a valid value for `sessionTimeZone`. +#### Session time zone for conversion #### -TODO: Behaviour for deciding `Time`/`Timestamp` value based on session time zone... +The session time zone will also be used to derive the `java.sql.Time`, +`java.sql.Timestamp` and `java.sql.Date` values. This is also done for +Firebird 3 and earlier. -NOTE: Currently, Jaybird will still use the JVM default time zone when deriving -`java.sql.Time` and `java.sql.Timestamp` values even if another session time -zone was specified (this will be changed). +We strongly suggest that you use `java.time.LocalTime`, +`java.time.LocalDateTime` and `java.time.LocalDate` types instead of these +legacy datetime types. + +If Java does not know the session time zone, no error is reported, but when +retrieving `java.sql.Time`, `java.sql.Timestamp` or `java.sql.Date` a warning is +logged and conversion will happen in GMT, which might yield unexpected values. + +Executing `SET TIME ZONE ` statements after connect will change the +session time zone on the server, but Jaybird will continue to use the session +time zone set in the connection property for these conversions. ### Time zone support for CONVERT ### @@ -895,16 +925,27 @@ In addition to the standard-defined types, it also supports the type names ### Caveats for time zone types ### -- Time zone fields do not support the legacy JDBC types `java.sql.Time`, -`java.sql.Timestamp`, `java.sql.Date`, nor do they support `java.time.LocalDate`, -`java.time.LocalTime`, `java.time.LocalDateTime`. **NOTE: This is not final yet** -- Firebird 4 redefines `CURRENT_TIME` and `CURRENT_TIMESTAMP` to return a `WITH -TIME ZONE` type. Use `LOCALTIME` and `LOCALTIMESTAMP` (introduced in Firebird -3.0.4) if you want to ensure a `WITHOUT TIME ZONE` type is used. -- The database metadata will always return JDBC 4.2 compatible information on -time zone types, even on Java 7, and even when legacy time zone bind is set. For -Java 7 compatibility the JDBC 4.2 `java.sql.Types` constants `TIME_WITH_TIMEZONE` -and `TIMESTAMP_WITH_TIMEZONE` are also defined in `org.firebirdsql.jdbc.JaybirdTypeCodes`. +- Time zone fields do not support the legacy JDBC types `java.sql.Time`, + `java.sql.Timestamp`, `java.sql.Date`, nor do they support + `java.time.LocalDate`, `java.time.LocalTime`, `java.time.LocalDateTime`. + **NOTE: This is not final yet** + +- Firebird 4 redefines `CURRENT_TIME` and `CURRENT_TIMESTAMP` to return a + `WITH TIME ZONE` type. Use `LOCALTIME` and `LOCALTIMESTAMP` (introduced in + Firebird 3.0.4) if you want to ensure a `WITHOUT TIME ZONE` type is used. + +- The database metadata will always return JDBC 4.2 compatible information on + time zone types, even on Java 7, and even when legacy time zone bind is set. + For Java 7 compatibility the JDBC 4.2 `java.sql.Types` constants + `TIME_WITH_TIMEZONE` and `TIMESTAMP_WITH_TIMEZONE` are also defined in + `org.firebirdsql.jdbc.JaybirdTypeCodes`. + +- The default `sessionTimeZone` is set to the JVM default time zone, this may + result in different application behavior for `DATE`, `TIME` and `TIMESTAMP`, + including values generated in triggers and default value clauses. To prevent + this, either switch those types to a `WITH TIME ZONE` type, or set the + `sessionTimeZone` to `server` or to the actual time zone of the Firebird + server. JDBC DatabaseMetaData.getPseudoColumns implemented -------------------------------------------------- diff --git a/src/main/org/firebirdsql/gds/impl/GDSHelper.java b/src/main/org/firebirdsql/gds/impl/GDSHelper.java index 2551f2f5ca..f12ab9be3c 100644 --- a/src/main/org/firebirdsql/gds/impl/GDSHelper.java +++ b/src/main/org/firebirdsql/gds/impl/GDSHelper.java @@ -27,8 +27,12 @@ import org.firebirdsql.gds.*; import org.firebirdsql.gds.ng.*; import org.firebirdsql.jdbc.Synchronizable; +import org.firebirdsql.logging.LoggerFactory; import java.sql.SQLException; +import java.util.TimeZone; + +import static org.firebirdsql.gds.ng.IConnectionProperties.SESSION_TIME_ZONE_SERVER; /** * Helper class for all GDS-related operations. @@ -40,6 +44,7 @@ public final class GDSHelper implements Synchronizable { private final FbDatabase database; private final Object syncObject; private FbTransaction transaction; + private TimeZone sessionTimeZone; /** * Create instance of this class. @@ -278,6 +283,36 @@ public String getJavaEncoding() { return database.getEncodingFactory().getDefaultEncodingDefinition().getJavaEncodingName(); } + /** + * Get the session time zone as configured in the connection property. + *

+ * NOTE: This is not necessarily the actual server time zone. + *

+ * + * @return Value of connection property {@code sessionTimeZone} + */ + public TimeZone getSessionTimeZone() { + if (sessionTimeZone == null) { + return initSessionTimeZone(); + } + return sessionTimeZone; + } + + private TimeZone initSessionTimeZone() { + String sessionTimeZoneName = database.getConnectionProperties().getSessionTimeZone(); + if (sessionTimeZoneName == null || SESSION_TIME_ZONE_SERVER.equalsIgnoreCase(sessionTimeZoneName)) { + return sessionTimeZone = TimeZone.getDefault(); + } + TimeZone timeZone = TimeZone.getTimeZone(sessionTimeZoneName); + if ("GMT".equals(timeZone.getID()) && !"GMT".equalsIgnoreCase(sessionTimeZoneName)) { + String message = "TimeZone fallback to GMT from " + sessionTimeZoneName + + "; possible cause: value of sessionTimeZone unknown in Java. Time and Timestamp values may " + + "yield unexpected values. Consider setting a different value for sessionTimeZone."; + LoggerFactory.getLogger(getClass()).warn(message); + } + return sessionTimeZone = timeZone; + } + @Override public Object getSynchronizationObject() { return syncObject; diff --git a/src/main/org/firebirdsql/gds/ng/AbstractParameterConverter.java b/src/main/org/firebirdsql/gds/ng/AbstractParameterConverter.java index bce444a720..56c429c912 100644 --- a/src/main/org/firebirdsql/gds/ng/AbstractParameterConverter.java +++ b/src/main/org/firebirdsql/gds/ng/AbstractParameterConverter.java @@ -25,6 +25,7 @@ import java.sql.SQLException; import static org.firebirdsql.gds.ISCConstants.*; +import static org.firebirdsql.gds.ng.IConnectionProperties.SESSION_TIME_ZONE_SERVER; /** * Abstract class for behavior common to {@code ParameterConverter} implementations. @@ -35,11 +36,6 @@ public abstract class AbstractParameterConverter, S extends AbstractConnection> implements ParameterConverter { - /** - * Value for {@code sessionTimeZone} that indicates the session time zone should not be set and use server default. - */ - private static final String SESSION_TIME_ZONE_SERVER = "server"; - protected DatabaseParameterBuffer createDatabaseParameterBuffer(final D connection) { return new DatabaseParameterBufferImp(DatabaseParameterBufferImp.DpbMetaData.DPB_VERSION_1, connection.getEncoding()); diff --git a/src/main/org/firebirdsql/gds/ng/IConnectionProperties.java b/src/main/org/firebirdsql/gds/ng/IConnectionProperties.java index ac6bf89461..de82f51546 100644 --- a/src/main/org/firebirdsql/gds/ng/IConnectionProperties.java +++ b/src/main/org/firebirdsql/gds/ng/IConnectionProperties.java @@ -37,6 +37,10 @@ */ public interface IConnectionProperties extends IAttachProperties { + /** + * Value for {@code sessionTimeZone} that indicates the session time zone should not be set and use server default. + */ + String SESSION_TIME_ZONE_SERVER = "server"; short DEFAULT_DIALECT = 3; int DEFAULT_BUFFERS_NUMBER = 0; diff --git a/src/main/org/firebirdsql/jdbc/field/AbstractWithoutTimeZoneField.java b/src/main/org/firebirdsql/jdbc/field/AbstractWithoutTimeZoneField.java new file mode 100644 index 0000000000..2dc6c7905c --- /dev/null +++ b/src/main/org/firebirdsql/jdbc/field/AbstractWithoutTimeZoneField.java @@ -0,0 +1,89 @@ +/* + * Firebird Open Source JavaEE Connector - JDBC Driver + * + * Distributable under LGPL license. + * You may obtain a copy of the License at http://www.gnu.org/copyleft/lgpl.html + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * LGPL License for more details. + * + * This file was created by members of the firebird development team. + * All individual contributions remain the Copyright (C) of those + * individuals. Contributors to this file are either listed here or + * can be obtained from a source control history command. + * + * All rights reserved. + */ +package org.firebirdsql.jdbc.field; + +import org.firebirdsql.gds.ng.fields.FieldDescriptor; + +import java.sql.SQLException; +import java.sql.Time; +import java.sql.Timestamp; +import java.util.Calendar; +import java.util.TimeZone; + +/** + * Common superclass for {@link FBTimeField} and {@link FBTimestampField} to handle session time zone. + * + * @author Mark Rotteveel + */ +abstract class AbstractWithoutTimeZoneField extends FBField { + + private Calendar calendar; + + AbstractWithoutTimeZoneField(FieldDescriptor fieldDescriptor, FieldDataProvider dataProvider, int requiredType) + throws SQLException { + super(fieldDescriptor, dataProvider, requiredType); + } + + @Override + public final Time getTime() throws SQLException { + if (isNull()) return null; + + return getTime(getCalendar()); + } + + @Override + public final Timestamp getTimestamp() throws SQLException { + if (isNull()) return null; + + return getTimestamp(getCalendar()); + } + + @Override + public final void setTime(Time value) throws SQLException { + if (value == null) { + setNull(); + return; + } + + setTime(value, getCalendar()); + } + + @Override + public final void setTimestamp(Timestamp value) throws SQLException { + if (value == null) { + setNull(); + return; + } + + setTimestamp(value, getCalendar()); + } + + Calendar getCalendar() { + if (calendar == null) { + return initCalendar(); + } + return calendar; + } + + private Calendar initCalendar() { + TimeZone sessionTimeZone = gdsHelper != null ? gdsHelper.getSessionTimeZone() : null; + return calendar = sessionTimeZone != null ? Calendar.getInstance(sessionTimeZone) : Calendar.getInstance(); + } + +} diff --git a/src/main/org/firebirdsql/jdbc/field/FBTimeField.java b/src/main/org/firebirdsql/jdbc/field/FBTimeField.java index 4cbe7bcea1..94624621e3 100644 --- a/src/main/org/firebirdsql/jdbc/field/FBTimeField.java +++ b/src/main/org/firebirdsql/jdbc/field/FBTimeField.java @@ -28,12 +28,12 @@ import java.sql.Time; /** - * Describe class FBTimeField here. + * Field implementation for {@code TIME (WITHOUT TIME ZONE)}. * * @author Roman Rokytskyy * @author Mark Rotteveel */ -final class FBTimeField extends FBField { +final class FBTimeField extends AbstractWithoutTimeZoneField { FBTimeField(FieldDescriptor fieldDescriptor, FieldDataProvider dataProvider, int requiredType) throws SQLException { super(fieldDescriptor, dataProvider, requiredType); @@ -41,7 +41,7 @@ final class FBTimeField extends FBField { public String getString() throws SQLException { if (isNull()) return null; - return String.valueOf(getDatatypeCoder().decodeTime(getFieldData())); + return String.valueOf(getTime()); } public Time getTime(Calendar cal) throws SQLException { @@ -49,21 +49,11 @@ public Time getTime(Calendar cal) throws SQLException { return getDatatypeCoder().decodeTimeCalendar(getFieldData(), cal); } - public Time getTime() throws SQLException { - if (isNull()) return null; - return getDatatypeCoder().decodeTime(getFieldData()); - } - public Timestamp getTimestamp(Calendar cal) throws SQLException { if (isNull()) return null; return new java.sql.Timestamp(getDatatypeCoder().decodeTimeCalendar(getFieldData(), cal).getTime()); } - public Timestamp getTimestamp() throws SQLException { - if (isNull()) return null; - return new Timestamp(getTime().getTime()); - } - //--- setXXX methods public void setString(String value) throws SQLException { @@ -74,9 +64,7 @@ public void setString(String value) throws SQLException { try { setTime(Time.valueOf(value)); } catch (RuntimeException e) { - final TypeConversionException conversionException = new TypeConversionException(TIME_CONVERSION_ERROR); - conversionException.initCause(e); - throw conversionException; + throw new TypeConversionException(TIME_CONVERSION_ERROR, e); } } @@ -88,14 +76,6 @@ public void setTimestamp(Timestamp value, Calendar cal) throws SQLException { setFieldData(getDatatypeCoder().encodeTimeCalendar(new java.sql.Time(value.getTime()), cal)); } - public void setTimestamp(Timestamp value) throws SQLException { - if (value == null) { - setNull(); - return; - } - setTime(new Time(value.getTime())); - } - public void setTime(Time value, Calendar cal) throws SQLException { if (value == null) { setNull(); @@ -105,15 +85,6 @@ public void setTime(Time value, Calendar cal) throws SQLException { setFieldData(getDatatypeCoder().encodeTimeCalendar(value, cal)); } - public void setTime(Time value) throws SQLException { - if (value == null) { - setNull(); - return; - } - - setFieldData(getDatatypeCoder().encodeTime(value)); - } - @Override public DatatypeCoder.RawDateTimeStruct getRawDateTimeStruct() throws SQLException { if (isNull()) return null; diff --git a/src/main/org/firebirdsql/jdbc/field/FBTimestampField.java b/src/main/org/firebirdsql/jdbc/field/FBTimestampField.java index f8dd4988a2..83f7ecb737 100644 --- a/src/main/org/firebirdsql/jdbc/field/FBTimestampField.java +++ b/src/main/org/firebirdsql/jdbc/field/FBTimestampField.java @@ -28,12 +28,12 @@ import java.util.Calendar; /** - * Describe class FBTimestampField here. + * Field implementation for {@code TIMESTAMP (WITHOUT TIME ZONE)}. * * @author Roman Rokytskyy * @author Mark Rotteveel */ -class FBTimestampField extends FBField { +class FBTimestampField extends AbstractWithoutTimeZoneField { FBTimestampField(FieldDescriptor fieldDescriptor, FieldDataProvider dataProvider, int requiredType) throws SQLException { @@ -43,7 +43,7 @@ class FBTimestampField extends FBField { public String getString() throws SQLException { if (isNull()) return null; - return String.valueOf(getDatatypeCoder().decodeTimestamp(getFieldData())); + return String.valueOf(getTimestamp()); } public Date getDate(Calendar cal) throws SQLException { @@ -52,10 +52,11 @@ public Date getDate(Calendar cal) throws SQLException { return new java.sql.Date(getDatatypeCoder().decodeTimestampCalendar(getFieldData(), cal).getTime()); } + @Override public Date getDate() throws SQLException { if (isNull()) return null; - return new Date(getTimestamp().getTime()); + return getDate(getCalendar()); } public Time getTime(Calendar cal) throws SQLException { @@ -64,24 +65,12 @@ public Time getTime(Calendar cal) throws SQLException { return new java.sql.Time(getDatatypeCoder().decodeTimestampCalendar(getFieldData(), cal).getTime()); } - public Time getTime() throws SQLException { - if (isNull()) return null; - - return new Time(getTimestamp().getTime()); - } - public Timestamp getTimestamp(Calendar cal) throws SQLException { if (isNull()) return null; return getDatatypeCoder().decodeTimestampCalendar(getFieldData(), cal); } - public Timestamp getTimestamp() throws SQLException { - if (isNull()) return null; - - return getDatatypeCoder().decodeTimestamp(getFieldData()); - } - public void setString(String value) throws SQLException { if (value == null) { setNull(); @@ -100,13 +89,14 @@ public void setDate(Date value, Calendar cal) throws SQLException { setFieldData(getDatatypeCoder().encodeTimestampCalendar(new java.sql.Timestamp(value.getTime()), cal)); } - public void setDate(Date value) throws SQLException { + @Override + public final void setDate(Date value) throws SQLException { if (value == null) { setNull(); return; } - setTimestamp(new Timestamp(value.getTime())); + setDate(value, getCalendar()); } public void setTime(Time value, Calendar cal) throws SQLException { @@ -118,15 +108,6 @@ public void setTime(Time value, Calendar cal) throws SQLException { setFieldData(getDatatypeCoder().encodeTimestampCalendar(new java.sql.Timestamp(value.getTime()), cal)); } - public void setTime(Time value) throws SQLException { - if (value == null) { - setNull(); - return; - } - - setTimestamp(new Timestamp(value.getTime())); - } - public void setTimestamp(Timestamp value, Calendar cal) throws SQLException { if (value == null) { setNull(); @@ -136,15 +117,6 @@ public void setTimestamp(Timestamp value, Calendar cal) throws SQLException { setFieldData(getDatatypeCoder().encodeTimestampCalendar(value, cal)); } - public void setTimestamp(Timestamp value) throws SQLException { - if (value == null) { - setNull(); - return; - } - - setFieldData(getDatatypeCoder().encodeTimestamp(value)); - } - @Override public DatatypeCoder.RawDateTimeStruct getRawDateTimeStruct() throws SQLException { if (isNull()) return null; diff --git a/src/test/org/firebirdsql/jdbc/SessionTimeZoneTest.java b/src/test/org/firebirdsql/jdbc/SessionTimeZoneTest.java index 543db39b0c..eb7f113f5d 100644 --- a/src/test/org/firebirdsql/jdbc/SessionTimeZoneTest.java +++ b/src/test/org/firebirdsql/jdbc/SessionTimeZoneTest.java @@ -20,13 +20,14 @@ import org.firebirdsql.common.rules.UsesDatabase; import org.firebirdsql.gds.ISCConstants; -import org.junit.BeforeClass; import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import java.sql.*; +import java.time.LocalDateTime; +import java.time.ZoneId; import java.util.Properties; import java.util.TimeZone; @@ -43,43 +44,63 @@ public class SessionTimeZoneTest { @ClassRule - public static final UsesDatabase usesDatabase = UsesDatabase.noDatabase(); + public static final UsesDatabase usesDatabase = UsesDatabase.usesDatabase(); @Rule public final ExpectedException expectedException = ExpectedException.none(); - @BeforeClass - public static void requireTimeZones() throws Exception { - assumeTrue("Requires time zone support", getDefaultSupportInfo().supportsTimeZones()); - usesDatabase.createDefaultDatabase(); + @Test + public void withTimeZone_appliesServerDefaultTimeZone() throws Exception { + requireTimeZoneSupport(); + checkForTimeZone("server", "timestamp'2019-03-23 14:05:12.12' at local", "2019-03-23 14:05:12.12", + "2019-03-23 14:05:12.1200"); } @Test - public void appliesServerDefaultTimeZone() throws Exception { - checkForTimeZone("server", "timestamp'2019-03-23 14:05:12.12' at local", "2019-03-23 14:05:12.12", - "2019-03-23 14:05:12.12"); + public void withoutTimeZone_appliesServerDefaultTimeZone() throws Exception { + long expectedMillis = getMillis("2019-03-23T14:05:12.12", ZoneId.systemDefault()); + checkForWithoutTimeZone("server", "timestamp'2019-03-23 14:05:12.12'", expectedMillis); } @Test - public void appliesJvmDefaultTimeZone() throws Exception { + public void withTimeZone_appliesJvmDefaultTimeZone() throws Exception { + requireTimeZoneSupport(); + // NOTE: Can potentially fail if Java default TimeZone id does not exist in Firebird checkForTimeZone(null, "timestamp'2019-03-23 14:05:12.12' at local", "2019-03-23 14:05:12.12", "2019-03-23 14:05:12.1200 " + TimeZone.getDefault().getID()); } @Test - public void appliesSpecificTimeZone() throws Exception { - checkForTimeZone("America/New_York", "timestamp'2019-03-23 14:05:12.12' at local", "2019-03-23 14:05:12.12", + public void withoutTimeZone_appliesJvmDefaultTimeZone() throws Exception { + long expectedMillis = getMillis("2019-03-23T14:05:12.12", ZoneId.systemDefault()); + checkForWithoutTimeZone(null, "timestamp'2019-03-23 14:05:12.12'", expectedMillis); + } + + @Test + public void withTimeZone_appliesSpecificTimeZone() throws Exception { + requireTimeZoneSupport(); + long expectedMillis = getMillis("2019-03-23T14:05:12.12", ZoneId.of("America/New_York")); + checkForTimeZone("America/New_York", "timestamp'2019-03-23 14:05:12.12' at local", expectedMillis, "2019-03-23 14:05:12.1200 America/New_York"); } @Test - public void appliesSpecificTimeZone_differentValue() throws Exception { - checkForTimeZone("America/New_York", "timestamp'2019-03-23 14:05:12.12 Europe/Amsterdam'", - "2019-03-23 09:05:12.12", "2019-03-23 14:05:12.1200 Europe/Amsterdam"); + public void withoutTimeZone_appliesSpecificTimeZone() throws Exception { + long expectedMillis = getMillis("2019-03-23T14:05:12.12", ZoneId.of("America/New_York")); + checkForWithoutTimeZone("America/New_York", "timestamp'2019-03-23 14:05:12.12'", expectedMillis); + } + + @Test + public void withTimeZone_appliesSpecificTimeZone_differentValue() throws Exception { + requireTimeZoneSupport(); + long expectedMillis = getMillis("2019-03-23T14:05:12.12", ZoneId.of("Europe/Amsterdam")); + checkForTimeZone("America/Buenos_Aires", "timestamp'2019-03-23 14:05:12.12 Europe/Amsterdam'", + expectedMillis, "2019-03-23 14:05:12.1200 Europe/Amsterdam"); } @Test public void errorOnInvalidTimeZoneName() throws Exception { + requireTimeZoneSupport(); Properties props = getDefaultPropertiesForConnection(); props.setProperty("sessionTimeZone", "does_not_exist"); @@ -105,12 +126,10 @@ private void checkForTimeZone(String zoneName, String timeValue, String expected // TODO Replace with connection property once implemented stmt.execute("set time zone bind legacy"); - try (ResultSet rs = stmt.executeQuery( - "select " + timeValue + ", " - + "cast(" + timeValue + " as varchar(100)) from rdb$database")) { + try (ResultSet rs = stmt.executeQuery("select " + timeValue + ", " + + "cast(" + timeValue + " as varchar(100)) from rdb$database")) { assertTrue("expected a row", rs.next()); Timestamp timestamp = rs.getTimestamp(1); - // TODO Need to revise expectedLocal values if we take time zone into account for deriving Time/Timestamp values assertEquals(expectedLocal, timestamp.toString()); // Need to take into account server-defined zone which we can't know in advance assertThat(rs.getString(2), startsWith(expectedZonedString)); @@ -118,5 +137,52 @@ private void checkForTimeZone(String zoneName, String timeValue, String expected } } + private void checkForTimeZone(String zoneName, String timeValue, long expectedMillis, String expectedZonedString) + throws SQLException { + Properties props = getDefaultPropertiesForConnection(); + if (zoneName != null) { + props.setProperty("sessionTimeZone", zoneName); + } + try (Connection connection = DriverManager.getConnection(getUrl(), props); + Statement stmt = connection.createStatement()) { + // TODO Replace with connection property once implemented + stmt.execute("set time zone bind legacy"); + + try (ResultSet rs = stmt.executeQuery("select " + timeValue + ", " + + "cast(" + timeValue + " as varchar(100)) from rdb$database")) { + assertTrue("expected a row", rs.next()); + Timestamp timestamp = rs.getTimestamp(1); + assertEquals(expectedMillis, timestamp.getTime()); + // Need to take into account server-defined zone which we can't know in advance + assertThat(rs.getString(2), startsWith(expectedZonedString)); + } + } + } + + private void checkForWithoutTimeZone(String zoneName, String timeValue, long expectedMillis) + throws SQLException { + Properties props = getDefaultPropertiesForConnection(); + if (zoneName != null) { + props.setProperty("sessionTimeZone", zoneName); + } + try (Connection connection = DriverManager.getConnection(getUrl(), props); + Statement stmt = connection.createStatement()) { + + try (ResultSet rs = stmt.executeQuery("select " + timeValue + " from rdb$database")) { + assertTrue("expected a row", rs.next()); + Timestamp timestamp = rs.getTimestamp(1); + assertEquals(expectedMillis, timestamp.getTime()); + } + } + } + + private static long getMillis(String localDateTimeString, ZoneId zone) { + return LocalDateTime.parse(localDateTimeString).atZone(zone) + .toInstant().toEpochMilli(); + } + + private static void requireTimeZoneSupport() { + assumeTrue("Requires time zone support", getDefaultSupportInfo().supportsTimeZones()); + } }