diff -urN --exclude='*.class' --exclude='*.jar' pgjdbc.clean/org/postgresql/core/BaseConnection.java pgjdbc/org/postgresql/core/BaseConnection.java --- pgjdbc.clean/org/postgresql/core/BaseConnection.java 2006-08-06 21:11:33.000000000 +0300 +++ pgjdbc/org/postgresql/core/BaseConnection.java 2006-11-19 20:13:31.000000000 +0200 @@ -151,4 +151,12 @@ // Get the bind-string-as-varchar config flag public boolean getStringVarcharFlag(); + + /** + * Returns true if binary encoding for date/time/timestamp/interval uses + * double, false if encoding is long. + * + * @return True if double should be used to encode the binary date formats. + */ + public boolean getBinaryDateTimeUseDouble(); } diff -urN --exclude='*.class' --exclude='*.jar' pgjdbc.clean/org/postgresql/core/Oid.java pgjdbc/org/postgresql/core/Oid.java --- pgjdbc.clean/org/postgresql/core/Oid.java 2005-12-04 22:14:26.000000000 +0200 +++ pgjdbc/org/postgresql/core/Oid.java 2006-11-19 20:13:31.000000000 +0200 @@ -9,6 +9,8 @@ */ package org.postgresql.core; +import java.lang.reflect.Field; + /** * Provides constants for well-known backend OIDs for the types we commonly * use. @@ -39,5 +41,26 @@ public static final int INTERVAL = 1186; public static final int CHAR = 18; // This is not char(N), this is "char" a single byte type. public static final int VARBIT = 1562; + + /** + * Returns the name of the oid as string. + * + * @param oid The oid to convert to name. + * @return The name of the oid or "" if oid no constant for + * oid value has been defined. + */ + public static String toString(int oid) { + try { + Field[] fields = Oid.class.getFields(); + for (int i = 0; i < fields.length; ++i) { + if (fields[i].getInt(null) == oid) { + return fields[i].getName(); + } + } + } catch (IllegalAccessException e) { + // never happens + } + return ""; + } } diff -urN --exclude='*.class' --exclude='*.jar' pgjdbc.clean/org/postgresql/core/ProtocolConnection.java pgjdbc/org/postgresql/core/ProtocolConnection.java --- pgjdbc.clean/org/postgresql/core/ProtocolConnection.java 2005-06-21 21:07:08.000000000 +0300 +++ pgjdbc/org/postgresql/core/ProtocolConnection.java 2006-11-19 20:13:31.000000000 +0200 @@ -12,6 +12,7 @@ import org.postgresql.PGNotification; import java.sql.*; +import java.util.BitSet; /** * Provides access to protocol-level connection operations. @@ -120,4 +121,19 @@ * @return the version of the implementation */ public int getProtocolVersion(); + + /** + * Returns true if server uses integer instead of double for binary + * date and time encodings. + * + * @return the server integer_datetime setting. + */ + public boolean getIntegerDateTimes(); + + /** + * Sets the oids that should be received using binary encoding. + * + * @param useBinaryForOids The oids to request with binary encoding. + */ + public void setBinaryOids(BitSet useBinaryForOids); } diff -urN --exclude='*.class' --exclude='*.jar' pgjdbc.clean/org/postgresql/core/Query.java pgjdbc/org/postgresql/core/Query.java --- pgjdbc.clean/org/postgresql/core/Query.java 2005-01-11 10:25:43.000000000 +0200 +++ pgjdbc/org/postgresql/core/Query.java 2006-11-19 20:13:31.000000000 +0200 @@ -54,4 +54,20 @@ * A closed Query should not be executed. */ void close(); + + /** + * Sets the fields that this query will return. + * + * @param fields The fields that this query will return. + */ + void setFields(Field[] fields); + + /** + * Returns the fields that this query will return. If the result set fields + * are not known returns null. + * + * @return the fields that this query will return. + */ + Field[] getFields(); + } diff -urN --exclude='*.class' --exclude='*.jar' pgjdbc.clean/org/postgresql/core/v2/ProtocolConnectionImpl.java pgjdbc/org/postgresql/core/v2/ProtocolConnectionImpl.java --- pgjdbc.clean/org/postgresql/core/v2/ProtocolConnectionImpl.java 2005-12-02 05:05:07.000000000 +0200 +++ pgjdbc/org/postgresql/core/v2/ProtocolConnectionImpl.java 2006-11-19 20:13:31.000000000 +0200 @@ -13,6 +13,7 @@ import java.sql.*; import java.io.IOException; import java.util.ArrayList; +import java.util.BitSet; import org.postgresql.PGNotification; import org.postgresql.core.*; @@ -193,6 +194,13 @@ return 2; } + public boolean getIntegerDateTimes() { + return false; + } + + public void setBinaryOids(BitSet ignored) { + } + private String serverVersion; private int cancelPid; private int cancelKey; diff -urN --exclude='*.class' --exclude='*.jar' pgjdbc.clean/org/postgresql/core/v2/V2Query.java pgjdbc/org/postgresql/core/v2/V2Query.java --- pgjdbc.clean/org/postgresql/core/v2/V2Query.java 2006-11-02 17:31:14.000000000 +0200 +++ pgjdbc/org/postgresql/core/v2/V2Query.java 2006-11-19 20:13:31.000000000 +0200 @@ -95,6 +95,13 @@ public void close() { } + public Field[] getFields() { + return null; + } + + public void setFields(Field[] fields) { + } + String[] getFragments() { return fragments; } diff -urN --exclude='*.class' --exclude='*.jar' pgjdbc.clean/org/postgresql/core/v3/CompositeQuery.java pgjdbc/org/postgresql/core/v3/CompositeQuery.java --- pgjdbc.clean/org/postgresql/core/v3/CompositeQuery.java 2005-01-11 10:25:44.000000000 +0200 +++ pgjdbc/org/postgresql/core/v3/CompositeQuery.java 2006-11-19 20:13:31.000000000 +0200 @@ -56,6 +56,13 @@ return subqueries; } + public Field[] getFields() { + return null; + } + + public void setFields(Field[] fields) { + } + private final SimpleQuery[] subqueries; private final int[] offsets; } diff -urN --exclude='*.class' --exclude='*.jar' pgjdbc.clean/org/postgresql/core/v3/ConnectionFactoryImpl.java pgjdbc/org/postgresql/core/v3/ConnectionFactoryImpl.java --- pgjdbc.clean/org/postgresql/core/v3/ConnectionFactoryImpl.java 2005-11-24 04:29:21.000000000 +0200 +++ pgjdbc/org/postgresql/core/v3/ConnectionFactoryImpl.java 2006-11-19 20:13:31.000000000 +0200 @@ -470,6 +470,10 @@ throw new PSQLException(GT.tr("Protocol error. Session setup failed."), PSQLState.CONNECTION_UNABLE_TO_CONNECT); pgStream.setEncoding(Encoding.getDatabaseEncoding("UNICODE")); } + else if (name.equals("integer_datetimes")) + { + protoConnection.setIntegerDateTimes(value.equals("on")); + } break; diff -urN --exclude='*.class' --exclude='*.jar' pgjdbc.clean/org/postgresql/core/v3/ProtocolConnectionImpl.java pgjdbc/org/postgresql/core/v3/ProtocolConnectionImpl.java --- pgjdbc.clean/org/postgresql/core/v3/ProtocolConnectionImpl.java 2005-12-02 05:05:09.000000000 +0200 +++ pgjdbc/org/postgresql/core/v3/ProtocolConnectionImpl.java 2006-11-19 20:13:31.000000000 +0200 @@ -16,6 +16,7 @@ import java.sql.*; import java.io.IOException; import java.util.ArrayList; +import java.util.BitSet; import java.util.Properties; /** @@ -189,6 +190,30 @@ return 3; } + public void setIntegerDateTimes(boolean state) { + integerDateTimes = state; + } + + public boolean getIntegerDateTimes() { + return integerDateTimes; + } + + public boolean useBinaryForReceive(int oid) { + return useBinaryForOids.get(oid); + } + + public void setBinaryOids(BitSet oids) { + useBinaryForOids.clear(); + useBinaryForOids.or(oids); + } + + /** + * Bit set that has a bit set for each oid which should be received + * using binary format. + */ + private final BitSet useBinaryForOids = new BitSet(); + + private boolean integerDateTimes; private String serverVersion; private int cancelPid; private int cancelKey; diff -urN --exclude='*.class' --exclude='*.jar' pgjdbc.clean/org/postgresql/core/v3/QueryExecutorImpl.java pgjdbc/org/postgresql/core/v3/QueryExecutorImpl.java --- pgjdbc.clean/org/postgresql/core/v3/QueryExecutorImpl.java 2006-11-02 17:31:14.000000000 +0200 +++ pgjdbc/org/postgresql/core/v3/QueryExecutorImpl.java 2006-11-19 22:19:13.000000000 +0200 @@ -805,12 +805,23 @@ encodedSize += (long)4 + params.getV3Length(i); } + int numBinaryFields = 0; + Field[] fields = query.getFields(); + if (fields != null) { + for (int i = 0; i < fields.length; ++i) { + if (useBinary(fields[i])) { + numBinaryFields = fields.length; + break; + } + } + } + encodedSize = 4 + (encodedPortalName == null ? 0 : encodedPortalName.length) + 1 + (encodedStatementName == null ? 0 : encodedStatementName.length) + 1 + 2 + params.getParameterCount() * 2 + 2 + encodedSize - + 2; + + 2 + numBinaryFields * 2; // backend's MaxAllocSize is the largest message that can // be received from a client. If we have a bigger value @@ -865,7 +876,11 @@ } } - pgStream.SendInteger2(0); // # of result format codes (0) + pgStream.SendInteger2(numBinaryFields); // # of result format codes + for (int i = 0; i < numBinaryFields; ++i) { + pgStream.SendInteger2(useBinary(fields[i]) ? Field.BINARY_FORMAT : + Field.TEXT_FORMAT); + } pendingBindQueue.add(portal); @@ -874,6 +889,20 @@ throw bindException; } } + + /** + * Returns true if the specified field should be retrieved using binary + * encoding. + * + * @param field The field whose Oid type to analyse. + * @return True if {@link Field#BINARY_FORMAT} should be used, false if + * {@link Field#BINARY_FORMAT}. + */ + private boolean useBinary(Field field) { + int oid = field.getOID(); +// System.out.println(Oid.toString(oid)); + return protoConnection.useBinaryForReceive(oid); + } private void sendDescribePortal(Portal portal) throws IOException { // diff -urN --exclude='*.class' --exclude='*.jar' pgjdbc.clean/org/postgresql/core/v3/SimpleQuery.java pgjdbc/org/postgresql/core/v3/SimpleQuery.java --- pgjdbc.clean/org/postgresql/core/v3/SimpleQuery.java 2006-08-06 21:02:45.000000000 +0300 +++ pgjdbc/org/postgresql/core/v3/SimpleQuery.java 2006-11-19 20:13:31.000000000 +0200 @@ -22,6 +22,10 @@ * @author Oliver Jowett (oliver@opencloud.com) */ class SimpleQuery implements V3Query { + + + private Field[] fields; + SimpleQuery(String[] fragments) { this.fragments = fragments; } @@ -54,6 +58,15 @@ unprepare(); } + + public Field[] getFields() { + return fields; + } + + public void setFields(Field[] fields) { + this.fields = fields; + } + // // V3Query // diff -urN --exclude='*.class' --exclude='*.jar' pgjdbc.clean/org/postgresql/jdbc2/AbstractJdbc2Connection.java pgjdbc/org/postgresql/jdbc2/AbstractJdbc2Connection.java --- pgjdbc.clean/org/postgresql/jdbc2/AbstractJdbc2Connection.java 2006-11-05 08:14:18.000000000 +0200 +++ pgjdbc/org/postgresql/jdbc2/AbstractJdbc2Connection.java 2006-11-19 20:13:31.000000000 +0200 @@ -13,9 +13,9 @@ import java.io.PrintWriter; import java.sql.*; import java.util.*; + import org.postgresql.core.*; import org.postgresql.Driver; -import org.postgresql.PGConnection; import org.postgresql.PGNotification; import org.postgresql.fastpath.Fastpath; import org.postgresql.largeobject.LargeObjectManager; @@ -117,6 +117,15 @@ { } + int binaryEncoding = 1; + try + { + binaryEncoding = Integer.parseInt(info.getProperty("binaryEncoding", "1")); + } + catch (Exception e) + { + } + //Print out the driver version number if (logger.logInfo()) logger.info(Driver.getVersion()); @@ -126,11 +135,43 @@ this.dbVersionNumber = protoConnection.getServerVersion(); this.compatible = info.getProperty("compatible", Driver.MAJORVERSION + "." + Driver.MINORVERSION); + BitSet useBinaryForOids = new BitSet(); + if (binaryEncoding > 0 && protoConnection.getProtocolVersion() >= 3) { + // currently supported binary encodings + useBinaryForOids.set(Oid.BYTEA); + useBinaryForOids.set(Oid.TIME); + useBinaryForOids.set(Oid.DATE); + useBinaryForOids.set(Oid.TIMETZ); + useBinaryForOids.set(Oid.TIMESTAMP); + useBinaryForOids.set(Oid.TIMESTAMPTZ); + } + if (protoConnection.getIntegerDateTimes()) { + // integer binary encoding not supported yet for these +// useBinaryForOids.clear(Oid.TIMESTAMP); +// useBinaryForOids.clear(Oid.TIMESTAMPTZ); + } + protoConnection.setBinaryOids(useBinaryForOids); + if (logger.logDebug()) { logger.debug(" compatible = " + compatible); logger.debug(" loglevel = " + logLevel); logger.debug(" prepare threshold = " + prepareThreshold); + logger.debug(" integer date/time = " + protoConnection.getIntegerDateTimes()); + StringBuffer sb = new StringBuffer(); + sb.append( " binary encoded types = "); + int oid = -1; + boolean found = false; + while ((oid = useBinaryForOids.nextSetBit(oid + 1)) != -1) { + sb.append(Oid.toString(oid)); + sb.append(','); + found = true; + } + sb.setLength(sb.length() - 1); + if (!found) { + sb.append(" "); + } + logger.debug(sb.toString()); } // @@ -1081,4 +1122,11 @@ { return bindStringAsVarchar; } + + /** + * {@inheritDoc} + */ + public boolean getBinaryDateTimeUseDouble() { + return !protoConnection.getIntegerDateTimes(); + } } diff -urN --exclude='*.class' --exclude='*.jar' pgjdbc.clean/org/postgresql/jdbc2/AbstractJdbc2ResultSet.java pgjdbc/org/postgresql/jdbc2/AbstractJdbc2ResultSet.java --- pgjdbc.clean/org/postgresql/jdbc2/AbstractJdbc2ResultSet.java 2006-11-05 09:45:16.000000000 +0200 +++ pgjdbc/org/postgresql/jdbc2/AbstractJdbc2ResultSet.java 2006-11-19 20:13:31.000000000 +0200 @@ -387,6 +387,16 @@ public java.sql.Date getDate(int i, java.util.Calendar cal) throws SQLException { this.checkResultSet(i); + + int col = i - 1; + + if (this_row[col] == null) { + return null; + } + + if (fields[col].getFormat() == Field.BINARY_FORMAT) { + return connection.getTimestampUtils().toDateBin(this_row[col]); + } if (cal != null) cal = (Calendar)cal.clone(); @@ -398,6 +408,17 @@ public Time getTime(int i, java.util.Calendar cal) throws SQLException { this.checkResultSet(i); + int col = i - 1; + + if (this_row[col] == null) { + return null; + } + + if (fields[col].getFormat() == Field.BINARY_FORMAT) { + TimeZone tz = cal == null ? null : cal.getTimeZone(); + return connection.getTimestampUtils().toTimeBin(tz, this_row[col], + connection.getBinaryDateTimeUseDouble()); + } if (cal != null) cal = (Calendar)cal.clone(); @@ -410,6 +431,21 @@ { this.checkResultSet(i); + int col = i - 1; + + if (this_row[col] == null) { + return null; + } + + if (fields[col].getFormat() == Field.BINARY_FORMAT) { + boolean hasTimeZone = fields[col].getOID() == Oid.TIMESTAMPTZ; + TimeZone tz = cal == null ? null : cal.getTimeZone(); + return connection.getTimestampUtils().toTimestampBin(tz, + this_row[col], + connection.getBinaryDateTimeUseDouble(), + hasTimeZone); + } + if (cal != null) cal = (Calendar)cal.clone(); @@ -1853,6 +1889,15 @@ wasNullFlag = (this_row[columnIndex - 1] == null); if (wasNullFlag) return null; + + // try to convert binary fields to their text format + if (fields[columnIndex - 1].getFormat() == Field.BINARY_FORMAT) { + Object obj = getObject(columnIndex); + if (obj == null) { + return null; + } + return trimString(columnIndex, obj.toString()); + } Encoding encoding = connection.getEncoding(); try diff -urN --exclude='*.class' --exclude='*.jar' pgjdbc.clean/org/postgresql/jdbc2/AbstractJdbc2Statement.java pgjdbc/org/postgresql/jdbc2/AbstractJdbc2Statement.java --- pgjdbc.clean/org/postgresql/jdbc2/AbstractJdbc2Statement.java 2006-11-05 07:58:22.000000000 +0200 +++ pgjdbc/org/postgresql/jdbc2/AbstractJdbc2Statement.java 2006-11-19 20:13:31.000000000 +0200 @@ -191,6 +191,9 @@ try { ResultSet rs = AbstractJdbc2Statement.this.createResultSet(fromQuery, fields, tuples, cursor); + if (preparedQuery != null) { + preparedQuery.setFields(fields); + } append(new ResultWrapper(rs)); } catch (SQLException e) diff -urN --exclude='*.class' --exclude='*.jar' pgjdbc.clean/org/postgresql/jdbc2/TimestampUtils.java pgjdbc/org/postgresql/jdbc2/TimestampUtils.java --- pgjdbc.clean/org/postgresql/jdbc2/TimestampUtils.java 2006-04-29 05:33:46.000000000 +0300 +++ pgjdbc/org/postgresql/jdbc2/TimestampUtils.java 2006-11-19 20:13:31.000000000 +0200 @@ -14,21 +14,23 @@ import java.util.Calendar; import java.util.GregorianCalendar; import java.util.TimeZone; -import java.util.SimpleTimeZone; - import org.postgresql.PGStatement; +import org.postgresql.core.Oid; import org.postgresql.util.GT; import org.postgresql.util.PSQLState; import org.postgresql.util.PSQLException; + + /** * Misc utils for handling time and date values. */ public class TimestampUtils { - private StringBuffer sbuf = new StringBuffer(); + private final StringBuffer sbuf = new StringBuffer(); - private Calendar defaultCal = new GregorianCalendar(); + private final Calendar defaultCal = new GregorianCalendar(); + private final TimeZone defaultTz = defaultCal.getTimeZone(); private Calendar calCache; private int calCacheZone; @@ -598,4 +600,186 @@ return '\0'; } + /** + * Returns the SQL Date object matching the given bytes with + * {@link Oid#DATE}. + * + * @param bytes The binary encoded date value. + * @return The parsed date object. + * @throws PSQLException If binary format could not be parsed. + */ + public Date toDateBin(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 = + ((bytes[0] & 255) << 24) + + ((bytes[1] & 255) << 16) + + ((bytes[2] & 255) << 8) + + ((bytes[3] & 255) ); + + // TODO: Need special infinite handling? + + long secs = toJavaSecs(days * 86400L); + + return new Date(secs * 1000); + } + + /** + * 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. + * @param useDouble True if the binary value is a double, false if long. + * @return The parsed time object. + * @throws PSQLException If binary format could not be parsed. + */ + public Time toTimeBin(TimeZone tz, byte[] bytes, boolean usesDouble) 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; + + // TODO: Need special infinite handling? + + long t = + ((long)(bytes[0] & 255) << 56) + + ((long)(bytes[1] & 255) << 48) + + ((long)(bytes[2] & 255) << 40) + + ((long)(bytes[3] & 255) << 32) + + ((long)(bytes[4] & 255) << 24) + + ((long)(bytes[5] & 255) << 16) + + ((long)(bytes[6] & 255) << 8) + + ((long)(bytes[7] & 255) ); + + if (usesDouble) { + + double time = Double.longBitsToDouble(t); + + millis = (long) (time * 1000); + } else { + millis = t / 1000; + } + + if (bytes.length == 12) { + timeOffset = + ((bytes[ 8] & 255) << 24) + + ((bytes[ 9] & 255) << 16) + + ((bytes[10] & 255) << 8) + + ((bytes[11] & 255) ); + timeOffset *= -1000; + } else { + if (tz == null) { + tz = defaultTz; + } + + timeOffset = tz.getRawOffset(); + } + + return new Time(millis - timeOffset); + } + + /** + * Returns the SQL Timestamp object matching the given bytes with + * {@link Oid#TIMESTAMP} or {@link Oid#TIMESTAMPTZ}. + * + * TODO: Has not been tested with {@link Oid#TIMESTAMP}. + * + * @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 useDouble True if the binary value is a double, false if long. + * @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 usesDouble, boolean timestamptz) + throws PSQLException { + + if (bytes.length != 8) { + throw new PSQLException(GT.tr("Unsupported binary encoding of {0}.", + "timestamp"), PSQLState.BAD_DATETIME_FORMAT); + } + + long t = + ((long)(bytes[0] & 255) << 56) + + ((long)(bytes[1] & 255) << 48) + + ((long)(bytes[2] & 255) << 40) + + ((long)(bytes[3] & 255) << 32) + + ((long)(bytes[4] & 255) << 24) + + ((long)(bytes[5] & 255) << 16) + + ((long)(bytes[6] & 255) << 8) + + ((long)(bytes[7] & 255) ); + + + + // TODO: Need special infinite handling? + + long secs; + int nanos; + int offset; + + if (usesDouble) { + + double time = Double.longBitsToDouble(t); + + secs = (long) time; + nanos = (int) ((time - secs) * 1000000); + } else { + secs = t / 1000000; + nanos = (int) (t - secs * 1000000); + } + if (nanos < 0) { + secs--; + nanos += 1000000; + } + nanos *= 1000; + + secs = toJavaSecs(secs); + if (timestamptz) { + offset = 0; + } else { + if (tz == null) { + tz = defaultTz; + } + offset = tz.getRawOffset(); + } + + Timestamp ts = new Timestamp(secs * 1000 - offset); + ts.setNanos(nanos); + return ts; + } + + /** + * 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. + * + * @param secs Postgresql seconds. + * @return Java seconds. + */ + private long toJavaSecs(long secs) { + // postgres epoc to java epoc + secs += 946684800L; + + // Julian/Greagorian 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; + } + }