diff --git a/devdoc/jdp/jdp-2019-03-time-zone-support.md b/devdoc/jdp/jdp-2019-03-time-zone-support.md index ccdc9594cd..92f549615c 100644 --- a/devdoc/jdp/jdp-2019-03-time-zone-support.md +++ b/devdoc/jdp/jdp-2019-03-time-zone-support.md @@ -167,6 +167,43 @@ Jaybird 4 will support Java 7 and higher, and Java 7 does not include `java.time 11. The driver-side session time zone for deriving time/timestamp values will also be applied when connecting to earlier Firebird versions. + +12. Support legacy types `java.sql.Time`, `java.sql.Timestamp` for the `WITH + TIME ZONE` types. + + Although not specified by JDBC, this option allows for some flexibility for + applications or libraries still expecting `java.sql.*` types. It will also + ease migration between Java 7 and 8 or higher. + + Firebird to Java conversion will derive the `OffsetDateTime` and then convert + this to epoch milliseconds, which is then used for constructing + `java.sql.Time` or `java.sql.Timestamp`. This will yield slightly different + results compared to `TIME WITHOUT TIME ZONE` type as there the conversion + applies 1970-01-01 instead of the current date. + + Java to Firebird conversion will derive an `OffsetDateTime` using + `toLocalTime()` and the current date, or `toLocalDateTime()`, combined with + the default JVM time zone (it will not apply `sessionTimeZone`). + + Methods with parameters of type `java.util.Calendar` will ignore the + calendar. + +13. Support legacy types `java.sql.Date` for the `TIMESTAMP WITH TIME ZONE` type. + + Although not specified by JDBC, this option allows for some flexibility for + applications or libraries still expecting `java.sql.*` types. It will also + ease migration between Java 7 and 8 or higher. + + Firebird to Java conversion will derive the `OffsetDateTime` and then convert + this to epoch milliseconds, which is then used for constructing + `java.sql.Date`. + + Java to Firebird conversion will derive an `OffsetDateTime` + using `toLocalDate()` at start of day combined with the default JVM time + zone (it will not apply `sessionTimeZone`). + + Methods with parameters of type `java.util.Calendar` will ignore the + calendar. ### Open options or questions @@ -204,25 +241,9 @@ Time zone support in Jaybird will not include the following: If there is demand, we can always add it in later. -4. Support `java.sql.Time` and `java.sql.Timestamp` for `WITH TIME ZONE` types. - - Although not specified by JDBC, this option allows for some flexibility for - applications or libraries still expecting `java.sql.*` types. It will also - ease migration between Java 7 and 8 or higher. - - Decided to prefer to abandon support for the legacy types. If people really - need to use legacy types, they can use `set time zone bind legacy`. - -5. Support `java.sql.Date` for `TIMESTAMP WITH TIME ZONE` type. - - Although not specified by JDBC, this option allows for some flexibility for - applications or libraries still expecting `java.sql.*` types. It will also - ease migration between Java 7 and 8 or higher. - - Possible confusion/ambiguity. See also section 4.6 in ISO-9075-2:2016. +4. _removed, see decision 12_ - Decided to prefer to abandon support for the legacy types, see also rejected - option 4. +5. _removed, see decision 13_ 6. Support `OffsetTime`/`OffsetDateTime` on `WITHOUT TIME ZONE` types. diff --git a/src/documentation/release_notes.md b/src/documentation/release_notes.md index e841309602..b733143a94 100644 --- a/src/documentation/release_notes.md +++ b/src/documentation/release_notes.md @@ -818,13 +818,23 @@ marked with * are not defined in JDBC) - On get, if the value is a named zone, it will derive the offset using the current date - `java.time.OffsetDateTime` - - On set the date information is removed - On get the current date is added + - On set the date information is removed - `java.lang.String` - On get applies `OffsetTime.toString()` (eg `13:25:13.1+01:00`) - On set tries the default parse format of either `OffsetTime` or `OffsetDateTime` (eg `13:25:13.1+01:00` or `2019-03-10T13:25:13+01:00`) and then sets as that type +- `java.sql.Time` + - On get obtains `java.time.OffsetDateTime`, converts this to epoch + milliseconds and uses `new java.sql.Time(millis)` + - On set applies `toLocalTime()`, combines this with `LocalDate.now()` + and then derives the offset time for the default JVM time zone +- `java.sql.Timestamp` + - On get obtains `java.time.OffsetDateTime`, converts this to epoch + milliseconds and uses `new java.sql.Timestamp(millis)` + - On set applies `toLocalDateTime()` and derives the offset time for the + default JVM time zone `TIMESTAMP WITH TIME ZONE`: @@ -837,12 +847,42 @@ marked with * are not defined in JDBC) - On set tries the default parse format of either `OffsetTime` or `OffsetDateTime` (eg `13:25:13.1+01:00` or `2019-03-10T13:25:13+01:00`) and then sets as that type +- `java.sql.Time` + - On get obtains `java.time.OffsetDateTime`, converts this to epoch + milliseconds and uses `new java.sql.Time(millis)` + - On set applies `toLocalTime()`, combines this with `LocalDate.now()` + and then derives the offset date time for the default JVM time zone +- `java.sql.Timestamp` + - On get obtains `java.time.OffsetDateTime`, converts this to epoch + milliseconds and uses `new java.sql.Timestamp(millis)` + - On set applies `toLocalDateTime()` and derives the offset date time for the + default JVM time zone +- `java.sql.Date` + - On get obtains `java.time.OffsetDateTime`, converts this to epoch + milliseconds and uses `new java.sql.Date(millis)` + - On set applies `toLocalDate()` at start of day and derives the offset date + time for the default JVM time zone + +#### Support for legacy JDBC date/time types #### + +For the `WITH TIME ZONE` types, JDBC does not define support for the legacy JDBC +types (`java.sql.Time`, `java.sql.Timestamp` and `java.sql.Date`). To ease the +transition and potential compatibility with tools and libraries, Jaybird does +provide support. However, we strongly recommend to avoid using these types. + +Compared to the `WITHOUT TIME ZONE` types, there may be small discrepancies in +values as Jaybird uses 1970-01-01 for `WITHOUT TIME ZONE`, while for `WITH TIME +ZONE` it uses the current date. If this is problematic, then either apply the +necessary conversions yourself, enable legacy time zone bind, or define or cast +your columns as `TIME` or `TIMESTAMP`. + +#### No support for other java.time types #### -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 apply the necessary conversions yourself, enable legacy -time zone bind, or define or cast your columns as `TIME` or `TIMESTAMP`. +The types `java.time.LocalTime`, `java.time.LocalDateTime` and +`java.time.LocalDate` are not supported. Supporting these types would be +ambiguous. If you need to 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`, @@ -897,17 +937,21 @@ Other examples include values generated in triggers and default value clauses. #### Session time zone for conversion #### -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. +For `WITHOUT TIME ZONE` types, the session time zone will 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. + +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. 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. +For `WITH TIME ZONE` types, the session time zone has no effect on the conversion +to the legacy JDBC date/time types: to offset date/time is converted to epoch +milliseconds and used to construct these legacy types directly. Executing `SET TIME ZONE ` statements after connect will change the session time zone on the server, but Jaybird will continue to use the session @@ -925,9 +969,8 @@ 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`. +- Time zone fields do not 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 diff --git a/src/jdbc_42/org/firebirdsql/jdbc/field/AbstractWithTimeZoneField.java b/src/jdbc_42/org/firebirdsql/jdbc/field/AbstractWithTimeZoneField.java new file mode 100644 index 0000000000..f9185763d5 --- /dev/null +++ b/src/jdbc_42/org/firebirdsql/jdbc/field/AbstractWithTimeZoneField.java @@ -0,0 +1,174 @@ +/* + * 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 org.firebirdsql.gds.ng.tz.TimeZoneDatatypeCoder; + +import java.sql.SQLException; +import java.sql.SQLNonTransientException; +import java.time.*; +import java.util.Calendar; + +import static org.firebirdsql.jdbc.JavaTypeNameConstants.OFFSET_DATE_TIME_CLASS_NAME; +import static org.firebirdsql.jdbc.JavaTypeNameConstants.OFFSET_TIME_CLASS_NAME; + +/** + * Superclass for {@link FBTimeTzField}, {@link FBTimestampTzField} to handle legacy date/time types and common behaviour. + * + * @author Mark Rotteveel + */ +abstract class AbstractWithTimeZoneField extends FBField { + + private ZoneId defaultZoneId; + + AbstractWithTimeZoneField(FieldDescriptor fieldDescriptor, FieldDataProvider dataProvider, int requiredType) + throws SQLException { + super(fieldDescriptor, dataProvider, requiredType); + } + + abstract OffsetDateTime getOffsetDateTime() throws SQLException; + + abstract void setOffsetDateTime(OffsetDateTime offsetDateTime) throws SQLException; + + abstract OffsetTime getOffsetTime() throws SQLException; + + abstract void setOffsetTime(OffsetTime offsetTime) throws SQLException; + + @SuppressWarnings("unchecked") + @Override + public T getObject(Class type) throws SQLException { + if (type == null) { + throw new SQLNonTransientException("getObject called with type null"); + } + switch (type.getName()) { + case OFFSET_TIME_CLASS_NAME: + return (T) getOffsetTime(); + case OFFSET_DATE_TIME_CLASS_NAME: + return (T) getOffsetDateTime(); + } + return super.getObject(type); + } + + @Override + public void setObject(Object value) throws SQLException { + if (value == null) { + setNull(); + return; + } + + if (value instanceof OffsetTime) { + setOffsetTime((OffsetTime) value); + } else if (value instanceof OffsetDateTime) { + setOffsetDateTime((OffsetDateTime) value); + } else { + super.setObject(value); + } + } + + @Override + public java.sql.Time getTime() throws SQLException { + OffsetDateTime offsetDateTime = getOffsetDateTime(); + if (offsetDateTime == null) { + return null; + } + return new java.sql.Time(offsetDateTime.toInstant().toEpochMilli()); + } + + @Override + public java.sql.Time getTime(Calendar cal) throws SQLException { + // Intentionally ignoring calendar, see jdp-2019-03 + return getTime(); + } + + @Override + public void setTime(java.sql.Time value) throws SQLException { + if (value == null) { + setNull(); + return; + } + + OffsetDateTime offsetDateTime = ZonedDateTime.of(LocalDate.now(), value.toLocalTime(), getDefaultZoneId()) + .toOffsetDateTime(); + setOffsetDateTime(offsetDateTime); + } + + @Override + public void setTime(java.sql.Time value, Calendar cal) throws SQLException { + // Intentionally ignoring calendar, see jdp-2019-03 + setTime(value); + } + + @Override + public java.sql.Timestamp getTimestamp() throws SQLException { + OffsetDateTime offsetDateTime = getOffsetDateTime(); + if (offsetDateTime == null) { + return null; + } + return new java.sql.Timestamp(offsetDateTime.toInstant().toEpochMilli()); + } + + @Override + public java.sql.Timestamp getTimestamp(Calendar cal) throws SQLException { + // Intentionally ignoring calendar, see jdp-2019-03 + return getTimestamp(); + } + + @Override + public void setTimestamp(java.sql.Timestamp value) throws SQLException { + if (value == null) { + setNull(); + return; + } + + OffsetDateTime offsetDateTime = value.toLocalDateTime() + .atZone(getDefaultZoneId()) + .toOffsetDateTime(); + setOffsetDateTime(offsetDateTime); + } + + @Override + public void setTimestamp(java.sql.Timestamp value, Calendar cal) throws SQLException { + // Intentionally ignoring calendar, see jdp-2019-03 + setTimestamp(value); + } + + final TimeZoneDatatypeCoder getTimeZoneDatatypeCoder() { + return TimeZoneDatatypeCoder.getInstanceFor(getDatatypeCoder()); + } + + final ZoneId getDefaultZoneId() { + if (defaultZoneId != null) { + return defaultZoneId; + } + return defaultZoneId = ZoneId.systemDefault(); + } + + void setStringParse(String value) throws SQLException { + // TODO Better way to do this? + // TODO More lenient parsing? + if (value.indexOf('T') != -1) { + OffsetDateTime offsetDateTime = OffsetDateTime.parse(value.trim()); + setOffsetDateTime(offsetDateTime); + } else { + OffsetTime offsetTime = OffsetTime.parse(value.trim()); + setOffsetTime(offsetTime); + } + } +} diff --git a/src/jdbc_42/org/firebirdsql/jdbc/field/FBTimeTzField.java b/src/jdbc_42/org/firebirdsql/jdbc/field/FBTimeTzField.java index c4efa4b360..f3253f012c 100644 --- a/src/jdbc_42/org/firebirdsql/jdbc/field/FBTimeTzField.java +++ b/src/jdbc_42/org/firebirdsql/jdbc/field/FBTimeTzField.java @@ -19,42 +19,40 @@ package org.firebirdsql.jdbc.field; import org.firebirdsql.gds.ng.fields.FieldDescriptor; -import org.firebirdsql.gds.ng.tz.TimeZoneDatatypeCoder; import java.sql.SQLException; -import java.sql.SQLNonTransientException; import java.time.OffsetDateTime; import java.time.OffsetTime; import java.time.ZoneOffset; import java.time.format.DateTimeParseException; -import static org.firebirdsql.jdbc.JavaTypeNameConstants.OFFSET_DATE_TIME_CLASS_NAME; -import static org.firebirdsql.jdbc.JavaTypeNameConstants.OFFSET_TIME_CLASS_NAME; - /** * Field for {@code TIME WITH TIME ZONE}. * * @author Mark Rotteveel * @since 4.0 */ -public class FBTimeTzField extends FBField { +class FBTimeTzField extends AbstractWithTimeZoneField { FBTimeTzField(FieldDescriptor fieldDescriptor, FieldDataProvider dataProvider, int requiredType) throws SQLException { super(fieldDescriptor, dataProvider, requiredType); } - private OffsetTime getOffsetTime() throws SQLException { + @Override + OffsetTime getOffsetTime() throws SQLException { if (isNull()) return null; return getTimeZoneDatatypeCoder().decodeTimeTz(getFieldData()); } - private void setOffsetTime(OffsetTime offsetTime) { + @Override + void setOffsetTime(OffsetTime offsetTime) { setFieldData(getTimeZoneDatatypeCoder().encodeTimeTz(offsetTime)); } - private OffsetDateTime getOffsetDateTime() throws SQLException { + @Override + OffsetDateTime getOffsetDateTime() throws SQLException { OffsetTime offsetTime = getOffsetTime(); if (offsetTime == null) { return null; @@ -66,7 +64,8 @@ private OffsetDateTime getOffsetDateTime() throws SQLException { return OffsetDateTime.of(today.toLocalDate(), offsetTime.toLocalTime(), offset); } - private void setOffsetDateTime(OffsetDateTime offsetDateTime) { + @Override + void setOffsetDateTime(OffsetDateTime offsetDateTime) { setOffsetTime(offsetDateTime.toOffsetTime()); } @@ -75,37 +74,6 @@ public Object getObject() throws SQLException { return getOffsetTime(); } - @SuppressWarnings("unchecked") - @Override - public T getObject(Class type) throws SQLException { - if (type == null) { - throw new SQLNonTransientException("getObject called with type null"); - } - switch (type.getName()) { - case OFFSET_TIME_CLASS_NAME: - return (T) getOffsetTime(); - case OFFSET_DATE_TIME_CLASS_NAME: - return (T) getOffsetDateTime(); - } - return super.getObject(type); - } - - @Override - public void setObject(Object value) throws SQLException { - if (value == null) { - setNull(); - return; - } - - if (value instanceof OffsetTime) { - setOffsetTime((OffsetTime) value); - } else if (value instanceof OffsetDateTime) { - setOffsetDateTime((OffsetDateTime) value); - } else { - super.setObject(value); - } - } - @Override public String getString() throws SQLException { if (isNull()) return null; @@ -121,22 +89,10 @@ public void setString(String value) throws SQLException { } try { - // TODO Better way to do this? - // TODO More lenient parsing? - if (value.indexOf('T') != -1) { - OffsetDateTime offsetDateTime = OffsetDateTime.parse(value.trim()); - setOffsetDateTime(offsetDateTime); - } else { - OffsetTime offsetTime = OffsetTime.parse(value.trim()); - setOffsetTime(offsetTime); - } + setStringParse(value); } catch (DateTimeParseException e) { throw new TypeConversionException("Unable to convert value '" + value + "' to type TIME WITH TIME ZONE", e); } } - private TimeZoneDatatypeCoder getTimeZoneDatatypeCoder() { - return TimeZoneDatatypeCoder.getInstanceFor(getDatatypeCoder()); - } - } diff --git a/src/jdbc_42/org/firebirdsql/jdbc/field/FBTimestampTzField.java b/src/jdbc_42/org/firebirdsql/jdbc/field/FBTimestampTzField.java index 1f772c25b3..b587020fbd 100644 --- a/src/jdbc_42/org/firebirdsql/jdbc/field/FBTimestampTzField.java +++ b/src/jdbc_42/org/firebirdsql/jdbc/field/FBTimestampTzField.java @@ -19,17 +19,13 @@ package org.firebirdsql.jdbc.field; import org.firebirdsql.gds.ng.fields.FieldDescriptor; -import org.firebirdsql.gds.ng.tz.TimeZoneDatatypeCoder; import java.sql.SQLException; -import java.sql.SQLNonTransientException; import java.time.OffsetDateTime; import java.time.OffsetTime; import java.time.ZoneOffset; import java.time.format.DateTimeParseException; - -import static org.firebirdsql.jdbc.JavaTypeNameConstants.OFFSET_DATE_TIME_CLASS_NAME; -import static org.firebirdsql.jdbc.JavaTypeNameConstants.OFFSET_TIME_CLASS_NAME; +import java.util.Calendar; /** * Field for {@code TIMESTAMP WITH TIME ZONE}. @@ -37,29 +33,33 @@ * @author Mark Rotteveel * @since 4.0 */ -class FBTimestampTzField extends FBField { +class FBTimestampTzField extends AbstractWithTimeZoneField { FBTimestampTzField(FieldDescriptor fieldDescriptor, FieldDataProvider dataProvider, int requiredType) throws SQLException { super(fieldDescriptor, dataProvider, requiredType); } - private OffsetDateTime getOffsetDateTime() throws SQLException { + @Override + OffsetDateTime getOffsetDateTime() throws SQLException { if (isNull()) return null; return getTimeZoneDatatypeCoder().decodeTimestampTz(getFieldData()); } - private void setOffsetDateTime(OffsetDateTime offsetDateTime) { + @Override + void setOffsetDateTime(OffsetDateTime offsetDateTime) { setFieldData(getTimeZoneDatatypeCoder().encodeTimestampTz(offsetDateTime)); } - private OffsetTime getOffsetTime() throws SQLException { + @Override + OffsetTime getOffsetTime() throws SQLException { OffsetDateTime offsetDateTime = getOffsetDateTime(); return offsetDateTime != null ? offsetDateTime.toOffsetTime() : null; } - private void setOffsetTime(OffsetTime offsetTime) throws SQLException { + @Override + void setOffsetTime(OffsetTime offsetTime) { // We need to base on a date to determine value, we use the current date; this will be inconsistent depending // on the date, but this aligns closest with Firebird behaviour and SQL standard ZoneOffset offset = offsetTime.getOffset(); @@ -74,35 +74,39 @@ public Object getObject() throws SQLException { return getOffsetDateTime(); } - @SuppressWarnings("unchecked") @Override - public T getObject(Class type) throws SQLException { - if (type == null) { - throw new SQLNonTransientException("getObject called with type null"); - } - switch (type.getName()) { - case OFFSET_DATE_TIME_CLASS_NAME: - return (T) getOffsetDateTime(); - case OFFSET_TIME_CLASS_NAME: - return (T) getOffsetTime(); + public java.sql.Date getDate() throws SQLException { + OffsetDateTime offsetDateTime = getOffsetDateTime(); + if (offsetDateTime == null) { + return null; } - return super.getObject(type); + return new java.sql.Date(offsetDateTime.toInstant().toEpochMilli()); } @Override - public void setObject(Object value) throws SQLException { + public java.sql.Date getDate(Calendar cal) throws SQLException { + // Intentionally ignoring calendar, see jdp-2019-03 + return getDate(); + } + + @Override + public void setDate(java.sql.Date value) throws SQLException { if (value == null) { setNull(); return; } - if (value instanceof OffsetDateTime) { - setOffsetDateTime((OffsetDateTime) value); - } else if (value instanceof OffsetTime) { - setOffsetTime((OffsetTime) value); - } else { - super.setObject(value); - } + OffsetDateTime offsetDateTime = value.toLocalDate() + .atStartOfDay() + .atZone(getDefaultZoneId()) + .toOffsetDateTime(); + setOffsetDateTime(offsetDateTime); + } + + @Override + public void setDate(java.sql.Date value, Calendar cal) throws SQLException { + // Intentionally ignoring calendar, see jdp-2019-03 + setDate(value); } @Override @@ -120,21 +124,10 @@ public void setString(String value) throws SQLException { } try { - // TODO Better way to do this? - // TODO More lenient parsing? - if (value.indexOf('T') != -1) { - OffsetDateTime offsetDateTime = OffsetDateTime.parse(value.trim()); - setOffsetDateTime(offsetDateTime); - } else { - OffsetTime offsetTime = OffsetTime.parse(value.trim()); - setOffsetTime(offsetTime); - } + setStringParse(value); } catch (DateTimeParseException e) { throw new TypeConversionException("Unable to convert value '" + value + "' to type TIMESTAMP WITH TIME ZONE", e); } } - private TimeZoneDatatypeCoder getTimeZoneDatatypeCoder() { - return TimeZoneDatatypeCoder.getInstanceFor(getDatatypeCoder()); - } } diff --git a/src/main/org/firebirdsql/jdbc/field/FBField.java b/src/main/org/firebirdsql/jdbc/field/FBField.java index ef8411c40f..071362f382 100644 --- a/src/main/org/firebirdsql/jdbc/field/FBField.java +++ b/src/main/org/firebirdsql/jdbc/field/FBField.java @@ -522,8 +522,9 @@ public T getObject(Class type) throws SQLException { if (isNull()) { return null; } else { + Timestamp timestamp = getTimestamp(); Calendar calendar = GregorianCalendar.getInstance(); - calendar.setTimeInMillis(getTimestamp().getTime()); + calendar.setTimeInMillis(timestamp.getTime()); return (T) calendar; } case CLOB_CLASS_NAME: diff --git a/src/test_42/org/firebirdsql/jdbc/field/FBTimeTzFieldTest.java b/src/test_42/org/firebirdsql/jdbc/field/FBTimeTzFieldTest.java index c5252ff01c..c6b3603d76 100644 --- a/src/test_42/org/firebirdsql/jdbc/field/FBTimeTzFieldTest.java +++ b/src/test_42/org/firebirdsql/jdbc/field/FBTimeTzFieldTest.java @@ -25,9 +25,8 @@ import java.sql.SQLException; import java.sql.Types; -import java.time.OffsetDateTime; -import java.time.OffsetTime; -import java.time.ZoneOffset; +import java.time.*; +import java.util.Calendar; import static org.firebirdsql.util.ByteArrayHelper.fromHexString; import static org.junit.Assert.assertEquals; @@ -120,6 +119,174 @@ public void setObject_String() throws SQLException { field.setString(TIMETZ); } + @Test + @Override + public void getTimeNonNull() throws SQLException { + toReturnNonNullOffsetTime(); + + LocalDate localDate = OffsetDateTime.now(TIMETZ_OFFSETTIME.getOffset()).toLocalDate(); + long expectedMillis = TIMETZ_OFFSETTIME.atDate(localDate).toInstant().toEpochMilli(); + + assertEquals("Unexpected value for getTime()", new java.sql.Time(expectedMillis), field.getTime()); + } + + @Test + @Override + public void getTimeCalendarNonNull() throws SQLException { + toReturnNonNullOffsetTime(); + + Calendar calendar = Calendar.getInstance(getOneHourBehindTimeZone()); + LocalDate localDate = OffsetDateTime.now(TIMETZ_OFFSETTIME.getOffset()).toLocalDate(); + long expectedMillis = TIMETZ_OFFSETTIME.atDate(localDate).toInstant().toEpochMilli(); + + assertEquals("Unexpected value for getTime(Calendar)", new java.sql.Time(expectedMillis), + field.getTime(calendar)); + } + + @Test + @Override + public void getObject_java_sql_Time() throws SQLException { + toReturnNonNullOffsetTime(); + + LocalDate localDate = OffsetDateTime.now(TIMETZ_OFFSETTIME.getOffset()).toLocalDate(); + long expectedMillis = TIMETZ_OFFSETTIME.atDate(localDate).toInstant().toEpochMilli(); + + assertEquals("Unexpected value for getObject(java.sql.Time.class)", new java.sql.Time(expectedMillis), + field.getObject(java.sql.Time.class)); + } + + @Test + @Override + public void setTimeNonNull() throws SQLException { + OffsetDateTime offsetDateTime = ZonedDateTime + .of(LocalDate.now(), LocalTime.parse("16:12:01"), ZoneId.systemDefault()) + .toOffsetDateTime(); + setOffsetTimeExpectations(offsetDateTime.toOffsetTime()); + + field.setTime(java.sql.Time.valueOf("16:12:01")); + } + + @Test + @Override + public void setTimeCalendarNonNull() throws SQLException { + OffsetDateTime offsetDateTime = ZonedDateTime + .of(LocalDate.now(), LocalTime.parse("16:12:01"), ZoneId.systemDefault()) + .toOffsetDateTime(); + setOffsetTimeExpectations(offsetDateTime.toOffsetTime()); + + Calendar calendar = Calendar.getInstance(getOneHourBehindTimeZone()); + + field.setTime(java.sql.Time.valueOf("16:12:01"), calendar); + } + + @Test + public void setObject_java_sql_Time() throws SQLException { + OffsetDateTime offsetDateTime = ZonedDateTime + .of(LocalDate.now(), LocalTime.parse("16:12:01"), ZoneId.systemDefault()) + .toOffsetDateTime(); + setOffsetTimeExpectations(offsetDateTime.toOffsetTime()); + + field.setObject(java.sql.Time.valueOf("16:12:01")); + } + + @Test + @Override + public void getTimestampNonNull() throws SQLException { + toReturnNonNullOffsetTime(); + + LocalDate localDate = OffsetDateTime.now(TIMETZ_OFFSETTIME.getOffset()).toLocalDate(); + long expectedMillis = TIMETZ_OFFSETTIME.atDate(localDate).toInstant().toEpochMilli(); + + assertEquals("Unexpected value for getTimestamp()", new java.sql.Timestamp(expectedMillis), + field.getTimestamp()); + } + + @Test + @Override + public void getTimestampCalendarNonNull() throws SQLException { + toReturnNonNullOffsetTime(); + + Calendar calendar = Calendar.getInstance(getOneHourBehindTimeZone()); + LocalDate localDate = OffsetDateTime.now(TIMETZ_OFFSETTIME.getOffset()).toLocalDate(); + long expectedMillis = TIMETZ_OFFSETTIME.atDate(localDate).toInstant().toEpochMilli(); + + assertEquals("Unexpected value for getTimestamp(Calendar)", new java.sql.Timestamp(expectedMillis), + field.getTimestamp(calendar)); + } + + @Test + @Override + public void getObject_java_sql_Timestamp() throws SQLException { + toReturnNonNullOffsetTime(); + + LocalDate localDate = OffsetDateTime.now(TIMETZ_OFFSETTIME.getOffset()).toLocalDate(); + long expectedMillis = TIMETZ_OFFSETTIME.atDate(localDate).toInstant().toEpochMilli(); + + assertEquals("Unexpected value for getObject(java.sql.Timestamp.class)", new java.sql.Timestamp(expectedMillis), + field.getObject(java.sql.Timestamp.class)); + } + + @Test + @Override + public void getObject_java_util_Date() throws SQLException { + toReturnNonNullOffsetTime(); + + LocalDate localDate = OffsetDateTime.now(TIMETZ_OFFSETTIME.getOffset()).toLocalDate(); + long expectedMillis = TIMETZ_OFFSETTIME.atDate(localDate).toInstant().toEpochMilli(); + + // NOTE: This actually returns a java.sql.Timestamp + assertEquals("Unexpected value for getObject(java.util.Date.class)", new java.sql.Timestamp(expectedMillis), + field.getObject(java.util.Date.class)); + } + + @Test + @Override + public void getObject_Calendar() throws SQLException { + toReturnNonNullOffsetTime(); + + LocalDate localDate = OffsetDateTime.now(TIMETZ_OFFSETTIME.getOffset()).toLocalDate(); + long expectedMillis = TIMETZ_OFFSETTIME.atDate(localDate).toInstant().toEpochMilli(); + Calendar expectedCalendar = Calendar.getInstance(); + expectedCalendar.setTimeInMillis(expectedMillis); + + assertEquals("Unexpected value for getObject(java.util.Calendar.class)", expectedCalendar, + field.getObject(java.util.Calendar.class)); + } + + @Test + @Override + public void setTimestampNonNull() throws SQLException { + OffsetDateTime offsetDateTime = ZonedDateTime + .of(LocalDateTime.parse("2019-03-09T07:45:51.1234"), ZoneId.systemDefault()) + .toOffsetDateTime(); + setOffsetTimeExpectations(offsetDateTime.toOffsetTime()); + + field.setTimestamp(java.sql.Timestamp.valueOf("2019-03-09 07:45:51.1234")); + } + + @Test + @Override + public void setTimestampCalendarNonNull() throws SQLException { + OffsetDateTime offsetDateTime = ZonedDateTime + .of(LocalDateTime.parse("2019-03-09T07:45:51.1234"), ZoneId.systemDefault()) + .toOffsetDateTime(); + setOffsetTimeExpectations(offsetDateTime.toOffsetTime()); + + Calendar calendar = Calendar.getInstance(getOneHourBehindTimeZone()); + + field.setTimestamp(java.sql.Timestamp.valueOf("2019-03-09 07:45:51.1234"), calendar); + } + + @Test + public void setObject_java_sql_Timestamp() throws SQLException { + OffsetDateTime offsetDateTime = ZonedDateTime + .of(LocalDateTime.parse("2019-03-09T07:45:51.1234"), ZoneId.systemDefault()) + .toOffsetDateTime(); + setOffsetTimeExpectations(offsetDateTime.toOffsetTime()); + + field.setObject(java.sql.Timestamp.valueOf("2019-03-09 07:45:51.1234")); + } + @Test @Override public void getStringNonNull() throws SQLException { diff --git a/src/test_42/org/firebirdsql/jdbc/field/FBTimestampTzFieldTest.java b/src/test_42/org/firebirdsql/jdbc/field/FBTimestampTzFieldTest.java index 4f14e760f2..e84ae2e47f 100644 --- a/src/test_42/org/firebirdsql/jdbc/field/FBTimestampTzFieldTest.java +++ b/src/test_42/org/firebirdsql/jdbc/field/FBTimestampTzFieldTest.java @@ -24,10 +24,10 @@ import org.junit.Test; import java.sql.SQLException; +import java.sql.Timestamp; import java.sql.Types; -import java.time.OffsetDateTime; -import java.time.OffsetTime; -import java.time.ZoneOffset; +import java.time.*; +import java.util.Calendar; import static org.firebirdsql.util.ByteArrayHelper.fromHexString; import static org.junit.Assert.assertEquals; @@ -120,6 +120,223 @@ public void setObject_String() throws SQLException { field.setString(TIMESTAMPTZ); } + @Test + @Override + public void getTimeNonNull() throws SQLException { + toReturnNonNullOffsetDateTime(); + + long expectedMillis = TIMESTAMPTZ_OFFSETDATETIME.toInstant().toEpochMilli(); + + assertEquals("Unexpected value for getTime()", new java.sql.Time(expectedMillis), field.getTime()); + } + + @Test + @Override + public void getTimeCalendarNonNull() throws SQLException { + toReturnNonNullOffsetDateTime(); + + Calendar calendar = Calendar.getInstance(getOneHourBehindTimeZone()); + long expectedMillis = TIMESTAMPTZ_OFFSETDATETIME.toInstant().toEpochMilli(); + + assertEquals("Unexpected value for getTime(Calendar)", new java.sql.Time(expectedMillis), + field.getTime(calendar)); + } + + @Test + @Override + public void getObject_java_sql_Time() throws SQLException { + toReturnNonNullOffsetDateTime(); + + long expectedMillis = TIMESTAMPTZ_OFFSETDATETIME.toInstant().toEpochMilli(); + + assertEquals("Unexpected value for getObject(java.sql.Time.class)", new java.sql.Time(expectedMillis), + field.getObject(java.sql.Time.class)); + } + + @Test + @Override + public void setTimeNonNull() throws SQLException { + OffsetDateTime expectedTime = ZonedDateTime + .of(LocalDate.now(), LocalTime.parse("16:12:01"), ZoneId.systemDefault()) + .toOffsetDateTime(); + setOffsetDateTimeExpectations(expectedTime); + + field.setTime(java.sql.Time.valueOf("16:12:01")); + } + + @Test + @Override + public void setTimeCalendarNonNull() throws SQLException { + OffsetDateTime expectedTime = ZonedDateTime + .of(LocalDate.now(), LocalTime.parse("16:12:01"), ZoneId.systemDefault()) + .toOffsetDateTime(); + setOffsetDateTimeExpectations(expectedTime); + + Calendar calendar = Calendar.getInstance(getOneHourBehindTimeZone()); + + field.setTime(java.sql.Time.valueOf("16:12:01"), calendar); + } + + @Test + public void setObject_java_sql_Time() throws SQLException { + OffsetDateTime expectedTime = ZonedDateTime + .of(LocalDate.now(), LocalTime.parse("16:12:01"), ZoneId.systemDefault()) + .toOffsetDateTime(); + setOffsetDateTimeExpectations(expectedTime); + + field.setObject(java.sql.Time.valueOf("16:12:01")); + } + + @Test + @Override + public void getTimestampNonNull() throws SQLException { + toReturnNonNullOffsetDateTime(); + + long expectedMillis = TIMESTAMPTZ_OFFSETDATETIME.toInstant().toEpochMilli(); + + assertEquals("Unexpected value for getTimestamp()", new java.sql.Timestamp(expectedMillis), + field.getTimestamp()); + } + + @Test + @Override + public void getTimestampCalendarNonNull() throws SQLException { + toReturnNonNullOffsetDateTime(); + + Calendar calendar = Calendar.getInstance(getOneHourBehindTimeZone()); + long expectedMillis = TIMESTAMPTZ_OFFSETDATETIME.toInstant().toEpochMilli(); + + assertEquals("Unexpected value for getTimestamp(Calendar)", new java.sql.Timestamp(expectedMillis), + field.getTimestamp(calendar)); + } + + @Test + @Override + public void getObject_java_sql_Timestamp() throws SQLException { + toReturnNonNullOffsetDateTime(); + + long expectedMillis = TIMESTAMPTZ_OFFSETDATETIME.toInstant().toEpochMilli(); + + assertEquals("Unexpected value for getObject(java.sql.Timestamp.class)", new java.sql.Timestamp(expectedMillis), + field.getObject(java.sql.Timestamp.class)); + } + + @Test + @Override + public void getObject_java_util_Date() throws SQLException { + toReturnNonNullOffsetDateTime(); + + long expectedMillis = TIMESTAMPTZ_OFFSETDATETIME.toInstant().toEpochMilli(); + + // NOTE: This actually returns a java.sql.Timestamp + assertEquals("Unexpected value for getObject(java.util.Date.class)", new java.sql.Timestamp(expectedMillis), + field.getObject(java.util.Date.class)); + } + + @Test + @Override + public void getObject_Calendar() throws SQLException { + toReturnNonNullOffsetDateTime(); + + long expectedMillis = TIMESTAMPTZ_OFFSETDATETIME.toInstant().toEpochMilli(); + Calendar expectedCalendar = Calendar.getInstance(); + expectedCalendar.setTimeInMillis(expectedMillis); + + assertEquals("Unexpected value for getObject(java.util.Calendar)", expectedCalendar, + field.getObject(java.util.Calendar.class)); + } + + @Test + @Override + public void setTimestampNonNull() throws SQLException { + OffsetDateTime expectedTime = LocalDateTime.parse("2019-03-09T07:45:51.1234") + .atZone(ZoneId.systemDefault()) + .toOffsetDateTime(); + setOffsetDateTimeExpectations(expectedTime); + + field.setTimestamp(Timestamp.valueOf("2019-03-09 07:45:51.1234")); + } + + @Test + @Override + public void setTimestampCalendarNonNull() throws SQLException { + OffsetDateTime expectedTime = LocalDateTime.parse("2019-03-09T07:45:51.1234") + .atZone(ZoneId.systemDefault()) + .toOffsetDateTime(); + setOffsetDateTimeExpectations(expectedTime); + + Calendar calendar = Calendar.getInstance(getOneHourBehindTimeZone()); + + field.setTimestamp(Timestamp.valueOf("2019-03-09 07:45:51.1234"), calendar); + } + + @Test + public void setObject_java_sql_Timestamp() throws SQLException { + OffsetDateTime expectedTime = LocalDateTime.parse("2019-03-09T07:45:51.1234") + .atZone(ZoneId.systemDefault()) + .toOffsetDateTime(); + setOffsetDateTimeExpectations(expectedTime); + + field.setObject(Timestamp.valueOf("2019-03-09 07:45:51.1234")); + } + + @Test + @Override + public void getDateNonNull() throws SQLException { + toReturnNonNullOffsetDateTime(); + + long expectedMillis = TIMESTAMPTZ_OFFSETDATETIME.toInstant().toEpochMilli(); + + assertEquals("Unexpected value for getDate()", new java.sql.Date(expectedMillis), field.getDate()); + } + + @Test + @Override + public void getDateCalendarNonNull() throws SQLException { + toReturnNonNullOffsetDateTime(); + + Calendar calendar = Calendar.getInstance(getOneHourBehindTimeZone()); + long expectedMillis = TIMESTAMPTZ_OFFSETDATETIME.toInstant().toEpochMilli(); + + assertEquals("Unexpected value for getDate(Calendar)", new java.sql.Date(expectedMillis), + field.getDate(calendar)); + } + + @Test + @Override + public void getObject_java_sql_Date() throws SQLException { + toReturnNonNullOffsetDateTime(); + + long expectedMillis = TIMESTAMPTZ_OFFSETDATETIME.toInstant().toEpochMilli(); + + assertEquals("Unexpected value for getObject(java.sql.Date.class)", new java.sql.Date(expectedMillis), + field.getObject(java.sql.Date.class)); + } + + @Test + @Override + public void setDateNonNull() throws SQLException { + OffsetDateTime expectedTime = LocalDateTime.parse("2019-03-09T00:00:00") + .atZone(ZoneId.systemDefault()) + .toOffsetDateTime(); + setOffsetDateTimeExpectations(expectedTime); + + field.setDate(java.sql.Date.valueOf("2019-03-09")); + } + + @Test + @Override + public void setDateCalendarNonNull() throws SQLException { + OffsetDateTime expectedTime = LocalDateTime.parse("2019-03-09T00:00:00") + .atZone(ZoneId.systemDefault()) + .toOffsetDateTime(); + setOffsetDateTimeExpectations(expectedTime); + + Calendar calendar = Calendar.getInstance(getOneHourBehindTimeZone()); + + field.setDate(java.sql.Date.valueOf("2019-03-09"), calendar); + } + @Test @Override public void getStringNonNull() throws SQLException {