Skip to content

Commit

Permalink
JDBC-540 Implement client-side session time zone behaviour
Browse files Browse the repository at this point in the history
  • Loading branch information
mrotteveel committed Mar 30, 2019
1 parent cd8302e commit abc2737
Show file tree
Hide file tree
Showing 8 changed files with 303 additions and 129 deletions.
113 changes: 77 additions & 36 deletions src/documentation/release_notes.md
Expand Up @@ -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
Expand Down Expand Up @@ -841,47 +841,77 @@ 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
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:
<zone name>`) 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: <zone name>`) 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 <zone name>` 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 ###

Expand All @@ -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
--------------------------------------------------
Expand Down
35 changes: 35 additions & 0 deletions src/main/org/firebirdsql/gds/impl/GDSHelper.java
Expand Up @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -278,6 +283,36 @@ public String getJavaEncoding() {
return database.getEncodingFactory().getDefaultEncodingDefinition().getJavaEncodingName();
}

/**
* Get the session time zone as configured in the connection property.
* <p>
* NOTE: This is not necessarily the actual server time zone.
* </p>
*
* @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;
Expand Down
Expand Up @@ -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.
Expand All @@ -35,11 +36,6 @@
public abstract class AbstractParameterConverter<D extends AbstractConnection<IConnectionProperties, ?>, S extends AbstractConnection<IServiceProperties, ?>>
implements ParameterConverter<D, S> {

/**
* 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());
Expand Down
4 changes: 4 additions & 0 deletions src/main/org/firebirdsql/gds/ng/IConnectionProperties.java
Expand Up @@ -37,6 +37,10 @@
*/
public interface IConnectionProperties extends IAttachProperties<IConnectionProperties> {

/**
* 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;

Expand Down
@@ -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 <a href="mailto:mrotteveel@users.sourceforge.net">Mark Rotteveel</a>
*/
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();
}

}

0 comments on commit abc2737

Please sign in to comment.