Index: pgjdbc/org/postgresql/core/ProtocolConnection.java =================================================================== --- pgjdbc.orig/org/postgresql/core/ProtocolConnection.java +++ pgjdbc/org/postgresql/core/ProtocolConnection.java @@ -140,4 +140,12 @@ public interface ProtocolConnection { * @param useBinaryForOids The oids to request with binary encoding. */ public void setBinaryReceiveOids(BitSet useBinaryForOids); + + /** + * Returns true if server uses integer instead of double for binary + * date and time encodings. + * + * @return the server integer_datetime setting. + */ + public boolean getIntegerDateTimes(); } Index: pgjdbc/org/postgresql/core/v2/ProtocolConnectionImpl.java =================================================================== --- pgjdbc.orig/org/postgresql/core/v2/ProtocolConnectionImpl.java +++ pgjdbc/org/postgresql/core/v2/ProtocolConnectionImpl.java @@ -206,6 +206,11 @@ class ProtocolConnectionImpl implements // ignored for v2 connections } + public boolean getIntegerDateTimes() { + // not supported in v2 protocol + return false; + } + private String serverVersion; private int cancelPid; private int cancelKey; Index: pgjdbc/org/postgresql/core/v3/ConnectionFactoryImpl.java =================================================================== --- pgjdbc.orig/org/postgresql/core/v3/ConnectionFactoryImpl.java +++ pgjdbc/org/postgresql/core/v3/ConnectionFactoryImpl.java @@ -11,6 +11,7 @@ package org.postgresql.core.v3; import java.util.Properties; +import java.util.TimeZone; import java.sql.*; import java.io.IOException; @@ -99,7 +100,8 @@ public class ConnectionFactoryImpl exten { "database", database }, { "client_encoding", "UTF8" }, { "DateStyle", "ISO" }, - { "extra_float_digits", "2" } + { "extra_float_digits", "2" }, + { "TimeZone", createPostgresTimeZone() }, }; sendStartupPacket(newStream, params, logger); @@ -167,6 +169,32 @@ public class ConnectionFactoryImpl exten } } + /** + * Convert Java time zone to postgres time zone. + * All others stay the same except that GMT+nn changes to GMT-nn and + * vise versa. + * + * @return The current JVM time zone in postgresql format. + */ + private String createPostgresTimeZone() { + String tz = TimeZone.getDefault().getID(); + if (tz.length() <= 3 || !tz.startsWith("GMT")) { + return tz; + } + char sign = tz.charAt(3); + String start; + if (sign == '+') { + start = "GMT-"; + } else if (sign == '-') { + start = "GMT+"; + } else { + // unknown type + return tz; + } + + return start + tz.substring(4); + } + private PGStream enableSSL(PGStream pgStream, boolean requireSSL, Properties info, Logger logger) throws IOException, SQLException { if (logger.logDebug()) logger.debug(" FE=> SSLRequest"); @@ -507,6 +535,15 @@ public class ConnectionFactoryImpl exten else throw new PSQLException(GT.tr("Protocol error. Session setup failed."), PSQLState.PROTOCOL_VIOLATION); } + else if (name.equals("integer_datetimes")) + { + if (value.equals("on")) + protoConnection.setIntegerDateTimes(true); + else if (value.equals("off")) + protoConnection.setIntegerDateTimes(false); + else + throw new PSQLException(GT.tr("Protocol error. Session setup failed."), PSQLState.PROTOCOL_VIOLATION); + } break; Index: pgjdbc/org/postgresql/core/v3/ProtocolConnectionImpl.java =================================================================== --- pgjdbc.orig/org/postgresql/core/v3/ProtocolConnectionImpl.java +++ pgjdbc/org/postgresql/core/v3/ProtocolConnectionImpl.java @@ -210,6 +210,20 @@ class ProtocolConnectionImpl implements useBinaryForOids.or(oids); } + public void setIntegerDateTimes(boolean state) { + integerDateTimes = state; + } + + public boolean getIntegerDateTimes() { + return integerDateTimes; + } + + /** + * True if server uses integers for date and time fields. False if + * server uses double. + */ + private boolean integerDateTimes; + /** * Bit set that has a bit set for each oid which should be received * using binary format. Index: pgjdbc/org/postgresql/jdbc2/AbstractJdbc2Connection.java =================================================================== --- pgjdbc.orig/org/postgresql/jdbc2/AbstractJdbc2Connection.java +++ pgjdbc/org/postgresql/jdbc2/AbstractJdbc2Connection.java @@ -148,6 +148,19 @@ public abstract class AbstractJdbc2Conne binaryOids.set(Oid.INT8); binaryOids.set(Oid.FLOAT4); binaryOids.set(Oid.FLOAT8); + binaryOids.set(Oid.TIME); + binaryOids.set(Oid.DATE); + binaryOids.set(Oid.TIMETZ); + binaryOids.set(Oid.TIMESTAMP); + binaryOids.set(Oid.TIMESTAMPTZ); + } + // the pre 8.0 servers do not disclose their internal encoding for + // time fields so do not try to use them. + if (!haveMinimumCompatibleVersion("8.0")) { + binaryOids.clear(Oid.TIME); + binaryOids.clear(Oid.TIMETZ); + binaryOids.clear(Oid.TIMESTAMP); + binaryOids.clear(Oid.TIMESTAMPTZ); } // split for receive and send for better control @@ -156,6 +169,13 @@ public abstract class AbstractJdbc2Conne useBinaryReceiveForOids = new BitSet(); useBinaryReceiveForOids.or(binaryOids); + /* + * Does not pass unit tests because unit tests expect setDate to have + * millisecond accuracy whereas the binary transfer only supports + * date accuracy. + */ + useBinarySendForOids.clear(Oid.DATE); + protoConnection.setBinaryReceiveOids(useBinaryReceiveForOids); if (logger.logDebug()) @@ -165,6 +185,7 @@ public abstract class AbstractJdbc2Conne logger.debug(" prepare threshold = " + prepareThreshold); logger.debug(" types using binary send = " + oidsToString(useBinarySendForOids)); logger.debug(" types using binary receive = " + oidsToString(useBinaryReceiveForOids)); + logger.debug(" integer date/time = " + protoConnection.getIntegerDateTimes()); } // @@ -185,7 +206,8 @@ public abstract class AbstractJdbc2Conne } // Initialize timestamp stuff - timestampUtils = new TimestampUtils(haveMinimumServerVersion("7.4"), haveMinimumServerVersion("8.2")); + timestampUtils = new TimestampUtils(haveMinimumServerVersion("7.4"), haveMinimumServerVersion("8.2"), + !protoConnection.getIntegerDateTimes()); // Initialize common queries. commitQuery = getQueryExecutor().createSimpleQuery("COMMIT"); Index: pgjdbc/org/postgresql/jdbc2/AbstractJdbc2ResultSet.java =================================================================== --- pgjdbc.orig/org/postgresql/jdbc2/AbstractJdbc2ResultSet.java +++ pgjdbc/org/postgresql/jdbc2/AbstractJdbc2ResultSet.java @@ -21,6 +21,7 @@ import java.sql.*; import java.util.HashMap; import java.util.Iterator; import java.util.StringTokenizer; +import java.util.TimeZone; import java.util.Vector; import java.util.Calendar; import java.util.Locale; @@ -404,6 +405,22 @@ public abstract class AbstractJdbc2Resul if (wasNullFlag) return null; + if (isBinary(i)) { + int col = i - 1; + int oid = fields[col].getOID(); + TimeZone tz = cal == null ? null : cal.getTimeZone(); + if (oid == Oid.DATE) { + return connection.getTimestampUtils().toDateBin(tz, this_row[col]); + } else if (oid == Oid.TIMESTAMP || oid == Oid.TIMESTAMPTZ) { + // JDBC spec says getDate of Timestamp must be supported + return connection.getTimestampUtils().convertToDate(getTimestamp(i, cal), tz); + } else { + throw new PSQLException (GT.tr("Cannot convert the column of type {0} to requested type {1}.", + new Object[]{Oid.toString(oid), "date"}), + PSQLState.DATA_TYPE_MISMATCH); + } + } + if (cal != null) cal = (Calendar)cal.clone(); @@ -417,6 +434,22 @@ public abstract class AbstractJdbc2Resul if (wasNullFlag) return null; + if (isBinary(i)) { + int col = i - 1; + int oid = fields[col].getOID(); + TimeZone tz = cal == null ? null : cal.getTimeZone(); + if (oid == Oid.TIME || oid == Oid.TIMETZ) { + return connection.getTimestampUtils().toTimeBin(tz, this_row[col]); + } else if (oid == Oid.TIMESTAMP || oid == Oid.TIMESTAMPTZ) { + // JDBC spec says getTime of Timestamp must be supported + return connection.getTimestampUtils().convertToTime(getTimestamp(i, cal), tz); + } else { + throw new PSQLException (GT.tr("Cannot convert the column of type {0} to requested type {1}.", + new Object[]{Oid.toString(oid), "time"}), + PSQLState.DATA_TYPE_MISMATCH); + } + } + if (cal != null) cal = (Calendar)cal.clone(); @@ -430,6 +463,29 @@ public abstract class AbstractJdbc2Resul if (wasNullFlag) return null; + if (isBinary(i)) { + int col = i - 1; + int oid = fields[col].getOID(); + if (oid == Oid.TIMESTAMPTZ || oid == Oid.TIMESTAMP) { + boolean hasTimeZone = oid == Oid.TIMESTAMPTZ; + TimeZone tz = cal == null ? null : cal.getTimeZone(); + return connection.getTimestampUtils().toTimestampBin(tz, this_row[col], hasTimeZone); + } else { + // JDBC spec says getTimestamp of Time and Date must be supported + long millis; + if (oid == Oid.TIME || oid == Oid.TIMETZ) { + millis = getTime(i, cal).getTime(); + } else if (oid == Oid.DATE) { + millis = getDate(i, cal).getTime(); + } else { + throw new PSQLException (GT.tr("Cannot convert the column of type {0} to requested type {1}.", + new Object[]{Oid.toString(oid), "timestamp"}), + PSQLState.DATA_TYPE_MISMATCH); + } + return new Timestamp(millis); + } + } + if (cal != null) cal = (Calendar)cal.clone(); @@ -1862,6 +1918,10 @@ public abstract class AbstractJdbc2Resul 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); + } return trimString(columnIndex, obj.toString()); } Index: pgjdbc/org/postgresql/jdbc2/AbstractJdbc2Statement.java =================================================================== --- pgjdbc.orig/org/postgresql/jdbc2/AbstractJdbc2Statement.java +++ pgjdbc/org/postgresql/jdbc2/AbstractJdbc2Statement.java @@ -17,6 +17,7 @@ import java.math.*; import java.sql.*; import java.util.ArrayList; import java.util.TimerTask; +import java.util.TimeZone; import java.util.Vector; import java.util.Calendar; @@ -3132,6 +3133,13 @@ public abstract class AbstractJdbc2State return; } + if (connection.binaryTransferSend(Oid.DATE)) { + byte[] val = new byte[4]; + TimeZone tz = cal != null ? cal.getTimeZone() : null; + connection.getTimestampUtils().toBinDate(tz, val, d); + preparedParameters.setBinaryParameter(i, val, Oid.DATE); + return; + } if (cal != null) cal = (Calendar)cal.clone(); Index: pgjdbc/org/postgresql/jdbc2/TimestampUtils.java =================================================================== --- pgjdbc.orig/org/postgresql/jdbc2/TimestampUtils.java +++ pgjdbc/org/postgresql/jdbc2/TimestampUtils.java @@ -17,6 +17,8 @@ import java.util.TimeZone; import java.util.SimpleTimeZone; import org.postgresql.PGStatement; +import org.postgresql.core.Oid; +import org.postgresql.util.ByteConverter; import org.postgresql.util.GT; import org.postgresql.util.PSQLState; import org.postgresql.util.PSQLException; @@ -26,9 +28,15 @@ import org.postgresql.util.PSQLException * Misc utils for handling time and date values. */ public class TimestampUtils { + /** + * Number of milliseconds in one day. + */ + private static final int ONEDAY = 24 * 3600 * 1000; + private StringBuffer sbuf = new StringBuffer(); private Calendar defaultCal = new GregorianCalendar(); + private final TimeZone defaultTz = defaultCal.getTimeZone(); private Calendar calCache; private int calCacheZone; @@ -36,9 +44,15 @@ public class TimestampUtils { private final boolean min74; private final boolean min82; - TimestampUtils(boolean min74, boolean min82) { + /** + * True if the backend uses doubles for time values. False if long is used. + */ + private final boolean usesDouble; + + TimestampUtils(boolean min74, boolean min82, boolean usesDouble) { this.min74 = min74; this.min82 = min82; + this.usesDouble = usesDouble; } private Calendar getCalendar(int sign, int hr, int min, int sec) { @@ -622,4 +636,277 @@ public class TimestampUtils { return '\0'; } + /** + * Returns the SQL Date object matching the given bytes with + * {@link Oid#DATE}. + * + * @param tz The timezone used. + * @param bytes The binary encoded date value. + * @return The parsed date object. + * @throws PSQLException If binary format could not be parsed. + */ + public Date toDateBin(TimeZone tz, byte[] bytes) throws PSQLException { + if (bytes.length != 4) { + throw new PSQLException(GT.tr("Unsupported binary encoding of {0}.", + "date"), PSQLState.BAD_DATETIME_FORMAT); + } + int days = ByteConverter.int4(bytes, 0); + if (tz == null) { + tz = defaultTz; + } + long secs = toJavaSecs(days * 86400L); + long millis = secs * 1000L; + int offset = tz.getOffset(millis); + return new Date(millis - offset); + } + + /** + * Returns the SQL Time object matching the given bytes with + * {@link Oid#TIME} or {@link Oid#TIMETZ}. + * + * @param tz The timezone used when received data is {@link Oid#TIME}, + * ignored if data already contains {@link Oid#TIMETZ}. + * @param bytes The binary encoded time value. + * @return The parsed time object. + * @throws PSQLException If binary format could not be parsed. + */ + public Time toTimeBin(TimeZone tz, byte[] bytes) throws PSQLException { + if ((bytes.length != 8 && bytes.length != 12)) { + throw new PSQLException(GT.tr("Unsupported binary encoding of {0}.", + "time"), PSQLState.BAD_DATETIME_FORMAT); + } + + long millis; + int timeOffset; + + if (usesDouble) { + double time = ByteConverter.float8(bytes, 0); + + millis = (long) (time * 1000); + } else { + long time = ByteConverter.int8(bytes, 0); + + millis = time / 1000; + } + + if (bytes.length == 12) { + timeOffset = ByteConverter.int4(bytes, 8); + timeOffset *= -1000; + } else { + if (tz == null) { + tz = defaultTz; + } + + timeOffset = tz.getOffset(millis); + } + + millis -= timeOffset; + return new Time(millis); + } + + /** + * Returns the SQL Timestamp object matching the given bytes with + * {@link Oid#TIMESTAMP} or {@link Oid#TIMESTAMPTZ}. + * + * @param tz The timezone used when received data is {@link Oid#TIMESTAMP}, + * ignored if data already contains {@link Oid#TIMESTAMPTZ}. + * @param bytes The binary encoded timestamp value. + * @param timestamptz True if the binary is in GMT. + * @return The parsed timestamp object. + * @throws PSQLException If binary format could not be parsed. + */ + public Timestamp toTimestampBin(TimeZone tz, byte[] bytes, boolean timestamptz) + throws PSQLException { + + if (bytes.length != 8) { + throw new PSQLException(GT.tr("Unsupported binary encoding of {0}.", + "timestamp"), PSQLState.BAD_DATETIME_FORMAT); + } + + long secs; + int nanos; + + if (usesDouble) { + double time = ByteConverter.float8(bytes, 0); + if (time == Double.POSITIVE_INFINITY) { + return new Timestamp(PGStatement.DATE_POSITIVE_INFINITY); + } else if (time == Double.NEGATIVE_INFINITY) { + return new Timestamp(PGStatement.DATE_NEGATIVE_INFINITY); + } + + secs = (long) time; + nanos = (int) ((time - secs) * 1000000); + } else { + long time = ByteConverter.int8(bytes, 0); + + // compatibility with text based receiving, not strictly necessary + // and can actually be confusing because there are timestamps + // that are larger than infinite + if (time == Long.MAX_VALUE) { + return new Timestamp(PGStatement.DATE_POSITIVE_INFINITY); + } else if (time == Long.MIN_VALUE) { + return new Timestamp(PGStatement.DATE_NEGATIVE_INFINITY); + } + + secs = time / 1000000; + nanos = (int) (time - secs * 1000000); + } + if (nanos < 0) { + secs--; + nanos += 1000000; + } + nanos *= 1000; + + secs = toJavaSecs(secs); + long millis = secs * 1000L; + if (!timestamptz) { + if (tz == null) { + tz = defaultTz; + } + millis -= tz.getOffset(millis); + } + + Timestamp ts = new Timestamp(millis); + ts.setNanos(nanos); + return ts; + } + + /** + * Extracts the date part from a timestamp. + * + * @param timestamp The timestamp from which to extract the date. + * @param tz The time zone of the date. + * @return The extracted date. + */ + public Date convertToDate(Timestamp timestamp, TimeZone tz) { + long millis = timestamp.getTime(); + // no adjustments for the inifity hack values + if (millis <= PGStatement.DATE_NEGATIVE_INFINITY || + millis >= PGStatement.DATE_POSITIVE_INFINITY) { + return new Date(millis); + } + if (tz == null) { + tz = defaultTz; + } + int offset = tz.getOffset(millis); + long timePart = millis % ONEDAY; + if (timePart + offset >= ONEDAY) { + millis += ONEDAY; + } + millis -= timePart; + millis -= offset; + + return new Date(millis); + } + + /** + * Extracts the time part from a timestamp. + * + * @param timestamp The timestamp from which to extract the time. + * @param tz The time zone of the time. + * @return The extracted time. + */ + public Time convertToTime(Timestamp timestamp, TimeZone tz) { + long millis = timestamp.getTime(); + if (tz == null) { + tz = defaultTz; + } + int offset = tz.getOffset(millis); + long low = - tz.getOffset(millis); + long high = low + ONEDAY; + if (millis < low) { + do { millis += ONEDAY; } while (millis < low); + } else if (millis >= high) { + do { millis -= ONEDAY; } while (millis > high); + } + + return new Time(millis); + } + + /** + * Returns the given time value as String matching what the + * current postgresql server would send in text mode. + */ + public String timeToString(java.util.Date time) { + long millis = time.getTime(); + if (millis <= PGStatement.DATE_NEGATIVE_INFINITY) { + return "-infinity"; + } + if (millis >= PGStatement.DATE_POSITIVE_INFINITY) { + return "infinity"; + } + return time.toString(); + } + + /** + * Converts the given postgresql seconds to java seconds. + * Reverse engineered by inserting varying dates to postgresql + * and tuning the formula until the java dates matched. + * See {@link #toPgSecs} for the reverse operation. + * + * @param secs Postgresql seconds. + * @return Java seconds. + */ + private static long toJavaSecs(long secs) { + // postgres epoc to java epoc + secs += 946684800L; + + // Julian/Gregorian calendar cutoff point + if (secs < -12219292800L) { // October 4, 1582 -> October 15, 1582 + secs += 86400 * 10; + if (secs < -14825808000L) { // 1500-02-28 -> 1500-03-01 + int extraLeaps = (int) ((secs + 14825808000L) / 3155760000L); + extraLeaps--; + extraLeaps -= extraLeaps / 4; + secs += extraLeaps * 86400L; + } + } + return secs; + } + + /** + * Converts the given java seconds to postgresql seconds. + * See {@link #toJavaSecs} for the reverse operation. + * The conversion is valid for any year 100 BC onwards. + * + * @param secs Postgresql seconds. + * @return Java seconds. + */ + private static long toPgSecs(long secs) { + // java epoc to postgres epoc + secs -= 946684800L; + + // Julian/Greagorian calendar cutoff point + if (secs < -13165977600L) { // October 15, 1582 -> October 4, 1582 + secs -= 86400 * 10; + if (secs < -15773356800L) { // 1500-03-01 -> 1500-02-28 + int years = (int) ((secs + 15773356800L) / -3155823050L); + years++; + years -= years/4; + secs += years * 86400; + } + } + + return secs; + } + + /** + * Converts the SQL Date to binary representation for {@link Oid#DATE}. + * + * @param tz The timezone used. + * @param bytes The binary encoded date value. + * @throws PSQLException If binary format could not be parsed. + */ + public void toBinDate(TimeZone tz, byte[] bytes, Date value) throws PSQLException { + long millis = value.getTime(); + + if (tz == null) { + tz = defaultTz; + } + millis += tz.getOffset(millis); + + long secs = toPgSecs(millis / 1000); + ByteConverter.int4(bytes, 0, (int) (secs / 86400)); + } + }