diff --git a/src/docs/asciidoc/release_notes.adoc b/src/docs/asciidoc/release_notes.adoc index 50c7f1099..486d3006c 100644 --- a/src/docs/asciidoc/release_notes.adoc +++ b/src/docs/asciidoc/release_notes.adoc @@ -41,6 +41,7 @@ This may affect custom protocol implementations extending `AbstractWireOperation This change also improves performance of `updateRow()`, `insertRow()`, `deleteRow()` and `refreshRow()`. The best row identifier or `RDB$DB_KEY` were detected _each time_ when calling `updateRow()`, `insertRow()`, `deleteRow()`, or `refreshRow()`. This has been improved so this detection is done once, and in a way that non-updatable result sets can now be downgraded to `CONCUR_READ_ONLY` instead of throwing an exception when performing the modification. +* Fixed: Use of offset timezone names (e.g. `+05:00`) for `sessionTimeZone` would result in a warning being logged, and an incorrect conversion applied (in UTC instead of the offset) when using the legacy time types -- backported from Jaybird 6 (https://github.com/FirebirdSQL/jaybird/issues/787[#787]) * ... [#jaybird-5-0-3-changelog] diff --git a/src/main/org/firebirdsql/gds/impl/GDSHelper.java b/src/main/org/firebirdsql/gds/impl/GDSHelper.java index f3ef959f4..62d009a93 100644 --- a/src/main/org/firebirdsql/gds/impl/GDSHelper.java +++ b/src/main/org/firebirdsql/gds/impl/GDSHelper.java @@ -31,6 +31,8 @@ import java.sql.SQLException; import java.util.TimeZone; +import java.util.function.Predicate; +import java.util.regex.Pattern; import static java.util.Objects.requireNonNull; import static org.firebirdsql.jaybird.props.PropertyConstants.SESSION_TIME_ZONE_SERVER; @@ -40,6 +42,9 @@ */ public final class GDSHelper { + private static final Predicate OFFSET_ZONE_NAME_PREDICATE = + Pattern.compile("[+-]\\d{2}:\\d{2}").asMatchPredicate(); + /** * @deprecated will be removed in Jaybird 6, use {@link org.firebirdsql.jaybird.props.PropertyConstants#DEFAULT_BLOB_BUFFER_SIZE} */ @@ -268,7 +273,7 @@ private TimeZone initSessionTimeZone() { if (sessionTimeZoneName == null || SESSION_TIME_ZONE_SERVER.equalsIgnoreCase(sessionTimeZoneName)) { return sessionTimeZone = TimeZone.getDefault(); } - TimeZone timeZone = TimeZone.getTimeZone(sessionTimeZoneName); + TimeZone timeZone = getTimeZone(sessionTimeZoneName); if ("GMT".equals(timeZone.getID()) && !"GMT".equalsIgnoreCase(sessionTimeZoneName)) { LoggerFactory.getLogger(getClass()).warnf("TimeZone fallback to GMT from %s; possible cause: value of " + "sessionTimeZone unknown in Java. Time and Timestamp values may yield unexpected values. " @@ -277,6 +282,13 @@ private TimeZone initSessionTimeZone() { return sessionTimeZone = timeZone; } + private static TimeZone getTimeZone(String timeZoneName) { + if (OFFSET_ZONE_NAME_PREDICATE.test(timeZoneName)) { + timeZoneName = "GMT" + timeZoneName; + } + return TimeZone.getTimeZone(timeZoneName); + } + /** * @see FbAttachment#withLock() */ diff --git a/src/test/org/firebirdsql/jdbc/SessionTimeZoneTest.java b/src/test/org/firebirdsql/jdbc/SessionTimeZoneTest.java index ff1653a96..8a593aa76 100644 --- a/src/test/org/firebirdsql/jdbc/SessionTimeZoneTest.java +++ b/src/test/org/firebirdsql/jdbc/SessionTimeZoneTest.java @@ -20,11 +20,14 @@ import org.firebirdsql.common.extension.UsesDatabaseExtension; import org.firebirdsql.gds.ISCConstants; +import org.firebirdsql.jaybird.props.PropertyNames; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import java.sql.*; import java.text.SimpleDateFormat; +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; import java.util.Date; import java.util.Properties; import java.util.TimeZone; @@ -36,6 +39,7 @@ import static org.hamcrest.CoreMatchers.startsWith; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assumptions.assumeTrue; @@ -111,6 +115,34 @@ void errorOnInvalidTimeZoneName() { fbMessageStartsWith(ISCConstants.isc_invalid_timezone_region, "does_not_exist"))); } + + /** + * Rationale: Firebird expects offset name {@code [+-]HH:MM}, while Java expects {@code GMT[+-]HH:MM}. + */ + @Test + void verifyOffsetTimeZoneBehaviour() throws Exception { + final String firebirdZoneName = "+05:17"; + Properties props = getDefaultPropertiesForConnection(); + props.setProperty(PropertyNames.sessionTimeZone, firebirdZoneName); + try (Connection connection = DriverManager.getConnection(getUrl(), props); + Statement stmt = connection.createStatement(); + ResultSet rs = stmt.executeQuery( + "select\n" + + " rdb$get_context('SYSTEM', 'SESSION_TIMEZONE') as TZ_NAME,\n" + + " localtimestamp as LOCAL_TS\n" + + "from RDB$DATABASE")) { + assertTrue(rs.next(), "expected a row"); + assertEquals(firebirdZoneName, rs.getString(1)); + Timestamp timestampValue = rs.getTimestamp(2); + assertEquals((double) System.currentTimeMillis(), timestampValue.getTime(), 1000, + "Unexpected value with offset time zone"); + + assertNotEquals(LocalDateTime.now().truncatedTo(ChronoUnit.MINUTES), + rs.getObject(2, LocalDateTime.class).truncatedTo(ChronoUnit.MINUTES), + "sanity check: expected a value not equal to the current default zone"); + } + } + @SuppressWarnings("SameParameterValue") private void checkForTimeZone(String zoneName, String timeValue, String expectedLocal, String expectedZonedString) throws SQLException {