Skip to content

Commit

Permalink
fix: binary timestamptz -> getString should add +XX zone offset to te…
Browse files Browse the repository at this point in the history
…xt representation

Note: tstz in binary is sent without timezone, thus arameterStatus(TimeZone = GMT-01:00) is used to track connection's time zone

closes #130
  • Loading branch information
vlsi committed Mar 22, 2016
1 parent f3b0fd0 commit 1e1f3c4
Show file tree
Hide file tree
Showing 9 changed files with 167 additions and 17 deletions.
13 changes: 13 additions & 0 deletions pgjdbc/src/main/java/org/postgresql/core/ProtocolConnection.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import java.sql.SQLException;
import java.sql.SQLWarning;
import java.util.Set;
import java.util.TimeZone;

/**
* Provides access to protocol-level connection operations.
Expand Down Expand Up @@ -173,4 +174,16 @@ public interface ProtocolConnection {
* Abort at network level without sending the Terminate message to the backend.
*/
public void abort();

/**
* Return TimestampUtils that is aware of connection-specific {@code TimeZone} value.
*
* @return timestampUtils instance
*/

/**
* Returns backend timezone in java format.
* @return backend timezone in java format.
*/
TimeZone getTimeZone();
}
16 changes: 16 additions & 0 deletions pgjdbc/src/main/java/org/postgresql/core/Provider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package org.postgresql.core;

/**
* Represents a provider of results.
*
* @param <T> the type of results provided by this provider
*/
public interface Provider<T> {

/**
* Gets a result.
*
* @return a result
*/
T get();
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import java.sql.SQLWarning;
import java.util.ArrayList;
import java.util.Set;
import java.util.TimeZone;

/**
* V2 implementation of ProtocolConnection.
Expand Down Expand Up @@ -228,6 +229,11 @@ public void abort() {
closed = true;
}

@Override
public TimeZone getTimeZone() {
return TimeZone.getDefault();
}

private String serverVersion;
private int serverVersionNum = 0;
private int cancelPid;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import java.util.HashSet;
import java.util.Properties;
import java.util.Set;
import java.util.TimeZone;


/**
Expand Down Expand Up @@ -235,6 +236,15 @@ public void abort() {
closed = true;
}

public void setTimeZone(TimeZone timeZone) {
this.timeZone = timeZone;
}

@Override
public TimeZone getTimeZone() {
return timeZone;
}

/**
* True if server uses integers for date and time fields. False if server uses double.
*/
Expand Down Expand Up @@ -264,4 +274,9 @@ public void abort() {
private final Logger logger;

private final int connectTimeout;

/**
* TimeZone of the current connection (TimeZone backend parameter)
*/
private TimeZone timeZone;
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import org.postgresql.core.ResultHandler;
import org.postgresql.core.Utils;
import org.postgresql.jdbc.BatchResultHandler;
import org.postgresql.jdbc.TimestampUtils;
import org.postgresql.util.GT;
import org.postgresql.util.PSQLException;
import org.postgresql.util.PSQLState;
Expand Down Expand Up @@ -2067,6 +2068,10 @@ protected void processResults(ResultHandler handler, int flags) throws IOExcepti
endQuery = true;
}
}

if ("TimeZone".equals(name)) {
protoConnection.setTimeZone(TimestampUtils.parseBackendTimeZone(value));
}
break;
}

Expand Down
10 changes: 9 additions & 1 deletion pgjdbc/src/main/java/org/postgresql/jdbc/PgConnection.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import org.postgresql.core.Logger;
import org.postgresql.core.Oid;
import org.postgresql.core.ProtocolConnection;
import org.postgresql.core.Provider;
import org.postgresql.core.Query;
import org.postgresql.core.QueryExecutor;
import org.postgresql.core.ResultCursor;
Expand Down Expand Up @@ -69,6 +70,7 @@
import java.util.Properties;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.TimeZone;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.Executor;
Expand Down Expand Up @@ -318,7 +320,13 @@ public PgConnection(HostSpec[] hostSpecs, String user, String database, Properti

// Initialize timestamp stuff
timestampUtils = new TimestampUtils(haveMinimumServerVersion(ServerVersion.v7_4),
haveMinimumServerVersion(ServerVersion.v8_2), !protoConnection.getIntegerDateTimes());
haveMinimumServerVersion(ServerVersion.v8_2), !protoConnection.getIntegerDateTimes(),
new Provider<TimeZone>() {
@Override
public TimeZone get() {
return protoConnection.getTimeZone();
}
});

// Initialize common queries.
commitQuery = getQueryExecutor().createSimpleQuery("COMMIT");
Expand Down
7 changes: 5 additions & 2 deletions pgjdbc/src/main/java/org/postgresql/jdbc/PgResultSet.java
Original file line number Diff line number Diff line change
Expand Up @@ -1884,13 +1884,16 @@ public String getString(int columnIndex) throws SQLException {

// varchar in binary is same as text, other binary fields are converted to their text format
if (isBinary(columnIndex) && getSQLType(columnIndex) != Types.VARCHAR) {
Object obj = internalGetObject(columnIndex, fields[columnIndex - 1]);
Field field = fields[columnIndex - 1];
Object obj = internalGetObject(columnIndex, field);
if (obj == null) {
return null;
}
// hack to be compatible with text protocol
if (obj instanceof java.util.Date) {
return connection.getTimestampUtils().timeToString((java.util.Date) obj);
int oid = field.getOID();
return connection.getTimestampUtils().timeToString((java.util.Date) obj,
oid == Oid.TIMESTAMPTZ || oid == Oid.TIMETZ);
}
if ("hstore".equals(getPGType(columnIndex))) {
return HStoreConverter.toString((Map<?, ?>) obj);
Expand Down
100 changes: 86 additions & 14 deletions pgjdbc/src/main/java/org/postgresql/jdbc/TimestampUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

import org.postgresql.PGStatement;
import org.postgresql.core.Oid;
import org.postgresql.core.Provider;
import org.postgresql.util.ByteConverter;
import org.postgresql.util.GT;
import org.postgresql.util.PSQLException;
Expand All @@ -30,6 +31,7 @@
//#endif
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.SimpleTimeZone;
import java.util.TimeZone;

Expand All @@ -44,12 +46,34 @@ public class TimestampUtils {
private static final int ONEDAY = 24 * 3600 * 1000;
private static final char[] ZEROS = {'0', '0', '0', '0', '0', '0', '0', '0', '0'};
private static final char[][] NUMBERS;
private static final HashMap<String, TimeZone> GMT_ZONES = new HashMap<String, TimeZone>();

static {
// The expected maximum value is 60 (seconds), so 64 is used "just in case"
NUMBERS = new char[64][];
for (int i = 0; i < NUMBERS.length; i++) {
NUMBERS[i] = Integer.toString(i).toCharArray();
NUMBERS[i] = ((i < 10 ? "0" : "") + Integer.toString(i)).toCharArray();
}

// Backend's gmt-3 means GMT+03 in Java. Here a map is created so gmt-3 can be converted to
// java TimeZone
for (int i = -12; i <= 14; i++) {
TimeZone timeZone;
String pgZoneName;
if (i == 0) {
timeZone = TimeZone.getTimeZone("GMT");
pgZoneName = "GMT";
} else {
timeZone = TimeZone.getTimeZone("GMT" + (i <= 0 ? "+" : "-") + Math.abs(i));
pgZoneName = "GMT" + (i >= 0 ? "+" : "-");
}

if (i == 0) {
GMT_ZONES.put(pgZoneName, timeZone);
continue;
}
GMT_ZONES.put(pgZoneName + Math.abs(i), timeZone);
GMT_ZONES.put(pgZoneName + NUMBERS[Math.abs(i)], timeZone);
}
}

Expand All @@ -70,11 +94,14 @@ public class TimestampUtils {
* True if the backend uses doubles for time values. False if long is used.
*/
private final boolean usesDouble;
private final Provider<TimeZone> timeZoneProvider;

TimestampUtils(boolean min74, boolean min82, boolean usesDouble) {
TimestampUtils(boolean min74, boolean min82, boolean usesDouble,
Provider<TimeZone> timeZoneProvider) {
this.min74 = min74;
this.min82 = min82;
this.usesDouble = usesDouble;
this.timeZoneProvider = timeZoneProvider;
}

private Calendar getCalendar(int sign, int hr, int min, int sec) {
Expand Down Expand Up @@ -433,6 +460,11 @@ private Calendar setupCalendar(Calendar cal) {
}

public synchronized String toString(Calendar cal, Timestamp x) {
return toString(cal, x, true);
}

public synchronized String toString(Calendar cal, Timestamp x,
boolean withTimeZone) {
if (x.getTime() == PGStatement.DATE_POSITIVE_INFINITY) {
return "infinity";
} else if (x.getTime() == PGStatement.DATE_NEGATIVE_INFINITY) {
Expand All @@ -447,13 +479,20 @@ public synchronized String toString(Calendar cal, Timestamp x) {
appendDate(sbuf, cal);
sbuf.append(' ');
appendTime(sbuf, cal, x.getNanos());
appendTimeZone(sbuf, cal);
if (withTimeZone) {
appendTimeZone(sbuf, cal);
}
appendEra(sbuf, cal);

return sbuf.toString();
}

public synchronized String toString(Calendar cal, Date x) {
return toString(cal, x, true);
}

public synchronized String toString(Calendar cal, Date x,
boolean withTimeZone) {
if (x.getTime() == PGStatement.DATE_POSITIVE_INFINITY) {
return "infinity";
} else if (x.getTime() == PGStatement.DATE_NEGATIVE_INFINITY) {
Expand All @@ -467,12 +506,20 @@ public synchronized String toString(Calendar cal, Date x) {

appendDate(sbuf, cal);
appendEra(sbuf, cal);
appendTimeZone(sbuf, cal);
if (withTimeZone) {
sbuf.append(' ');
appendTimeZone(sbuf, cal);
}

return sbuf.toString();
}

public synchronized String toString(Calendar cal, Time x) {
return toString(cal, x, true);
}

public synchronized String toString(Calendar cal, Time x,
boolean withTimeZone) {
cal = setupCalendar(cal);
cal.setTime(x);

Expand All @@ -481,7 +528,7 @@ public synchronized String toString(Calendar cal, Time x) {
appendTime(sbuf, cal, cal.get(Calendar.MILLISECOND) * 1000000);

// The 'time' parser for <= 7.3 doesn't like timezones.
if (min74) {
if (min74 && withTimeZone) {
appendTimeZone(sbuf, cal);
}

Expand Down Expand Up @@ -533,6 +580,9 @@ private static void appendTime(StringBuilder sb, int hours, int minutes, int sec
// a two digit fractional second, but we don't need to support 7.1
// anymore and getting the version number here is difficult.
//
if (nanos == 0) {
return;
}
sb.append('.');
int len = sb.length();
sb.append(nanos / 1000); // append microseconds
Expand All @@ -554,15 +604,18 @@ private void appendTimeZone(StringBuilder sb, int offset) {
int mins = (absoff - hours * 60 * 60) / 60;
int secs = absoff - hours * 60 * 60 - mins * 60;

sb.append((offset >= 0) ? " +" : " -");
sb.append((offset >= 0) ? "+" : "-");

sb.append(NUMBERS[hours]);

if (mins == 0 && secs == 0) {
return;
}
sb.append(':');

sb.append(NUMBERS[mins]);

if (min82) {
if (min82 && secs != 0) {
sb.append(':');
sb.append(NUMBERS[secs]);
}
Expand Down Expand Up @@ -1061,17 +1114,22 @@ public Time convertToTime(long millis, TimeZone tz) {
* in text mode.
*
* @param time time value
* @param withTimeZone whether timezone should be added
* @return given time value as String
*/
public String timeToString(java.util.Date time) {
long millis = time.getTime();
if (millis <= PGStatement.DATE_NEGATIVE_INFINITY) {
return "-infinity";
public String timeToString(java.util.Date time, boolean withTimeZone) {
Calendar cal = null;
if (withTimeZone) {
cal = calendarWithUserTz;
cal.setTimeZone(timeZoneProvider.get());
}
if (millis >= PGStatement.DATE_POSITIVE_INFINITY) {
return "infinity";
if (time instanceof Timestamp) {
return toString(cal, (Timestamp) time, withTimeZone);
}
return time.toString();
if (time instanceof Time) {
return toString(cal, (Time) time, withTimeZone);
}
return toString(cal, (Date) time, withTimeZone);
}

/**
Expand Down Expand Up @@ -1150,4 +1208,18 @@ public void toBinDate(TimeZone tz, byte[] bytes, Date value) throws PSQLExceptio
ByteConverter.int4(bytes, 0, (int) (secs / 86400));
}

/**
* Converts backend's TimeZone parameter to java format.
* Notable difference: backend's gmt-3 is GMT+03 in Java.
* @return java TimeZone
*/
public static TimeZone parseBackendTimeZone(String timeZone) {
if (timeZone.startsWith("GMT")) {
TimeZone tz = GMT_ZONES.get(timeZone);
if (tz != null) {
return tz;
}
}
return TimeZone.getTimeZone(timeZone);
}
}
Loading

0 comments on commit 1e1f3c4

Please sign in to comment.