Repository: pgjdbc/pgjdbc
Pull request #1612 (2019-11-20 13:57:06)

fix: pginterval to take iso8601 strings
this is a work in progress looking for comments on how to deal with things like months > 12 should we rationalize this before sending to the backend?

AppVeyorBot: :white_check_mark: [Build pgjdbc 1.0.527 completed](https://ci.appveyor.com/project/davecramer/pgjdbc/builds/28994985) (commit https://github.com/pgjdbc/pgjdbc/commit/3b5e3466b3 by @davecramer)
codecov-io: # [Codecov](https://codecov.io/gh/pgjdbc/pgjdbc/pull/1612?src=pr&el=h1) Report
> Merging [#1612](https://codecov.io/gh/pgjdbc/pgjdbc/pull/1612?src=pr&el=desc) into [master](https://codecov.io/gh/pgjdbc/pgjdbc/commit/00fa448587532cc219977679bb8c573a1dcae11c?src=pr&el=desc) will **increase** coverage by `0.03%`.
> The diff coverage is `85.41%`.



```diff
@@             Coverage Diff              @@
##             master    #1612      +/-   ##
============================================
+ Coverage        69%   69.03%   +0.03%     
- Complexity     4010     4022      +12     
============================================
  Files           179      179              
  Lines         16664    16705      +41     
  Branches       2716     2731      +15     
============================================
+ Hits          11499    11533      +34     
- Misses         3907     3908       +1     
- Partials       1258     1264       +6
```

RhodiumToad: No particular reason to normalize interval parts; the backend just stores months,days,microseconds anyway and converts to those.
davecramer: @RhodiumToad  In general I'd like to be able to retrieve what we send. If we have something like 30 months when we get it back it will be different. Not sure that matters, and not sure I want to go to the trouble to make it work. Thanks for your feedback.
AppVeyorBot: :white_check_mark: [Build pgjdbc 1.0.528 completed](https://ci.appveyor.com/project/davecramer/pgjdbc/builds/29032965) (commit https://github.com/pgjdbc/pgjdbc/commit/836a4d8647 by @davecramer)

Patch:
diff --git a/pgjdbc/src/main/java/org/postgresql/PGProperty.java b/pgjdbc/src/main/java/org/postgresql/PGProperty.java
index 9e99dc7a..87705c3a 100644
--- a/pgjdbc/src/main/java/org/postgresql/PGProperty.java
+++ b/pgjdbc/src/main/java/org/postgresql/PGProperty.java
@@ -10,6 +10,7 @@ import org.postgresql.util.GT;
 import org.postgresql.util.PSQLException;
 import org.postgresql.util.PSQLState;
 
+import java.sql.Connection;
 import java.sql.DriverPropertyInfo;
 import java.util.Properties;
 
@@ -443,7 +444,19 @@ public enum PGProperty {
           + "to the database specified in the dbname parameter, "
           + "which will allow the connection to be used for logical replication "
           + "from that database. "
-          + "(backend >= 9.4)");
+          + "(backend >= 9.4)"),
+
+  /**
+   * Connection parameter to control behavior when
+   * {@link Connection#setReadOnly(boolean)} is set to {@code true}.
+   */
+  READ_ONLY_MODE("readOnlyMode", "transaction",
+      "Controls the behavior when a connection is set to be read only, one of 'ignore', 'transaction', or 'always' "
+          + "When 'ignore', setting readOnly has no effect. "
+          + "When 'transaction' setting readOnly to 'true' will cause transactions to BEGIN READ ONLY if autocommit is 'false'. "
+          + "When 'always' setting readOnly to 'true' will set the session to READ ONLY if autoCommit is 'true' "
+          + "and the transaction to BEGIN READ ONLY if autocommit is 'false'.",
+      false, "ignore", "transaction", "always");
 
   private final String name;
   private final String defaultValue;
diff --git a/pgjdbc/src/main/java/org/postgresql/core/BaseConnection.java b/pgjdbc/src/main/java/org/postgresql/core/BaseConnection.java
index 1d316a07..d9f2282b 100644
--- a/pgjdbc/src/main/java/org/postgresql/core/BaseConnection.java
+++ b/pgjdbc/src/main/java/org/postgresql/core/BaseConnection.java
@@ -6,6 +6,7 @@
 package org.postgresql.core;
 
 import org.postgresql.PGConnection;
+import org.postgresql.PGProperty;
 import org.postgresql.jdbc.FieldMetadata;
 import org.postgresql.jdbc.TimestampUtils;
 import org.postgresql.util.LruCache;
@@ -202,4 +203,13 @@ public interface BaseConnection extends PGConnection, Connection {
    * @param flushCacheOnDeallocate true if statement cache should be reset when "deallocate/discard" message observed
    */
   void setFlushCacheOnDeallocate(boolean flushCacheOnDeallocate);
+
+  /**
+   * Indicates if statements to backend should be hinted as read only.
+   *
+   * @return Indication if hints to backend (such as when transaction begins)
+   *         should be read only.
+   * @see PGProperty#READ_ONLY_MODE
+   */
+  boolean hintReadOnly();
 }
diff --git a/pgjdbc/src/main/java/org/postgresql/core/QueryExecutor.java b/pgjdbc/src/main/java/org/postgresql/core/QueryExecutor.java
index 8d95e8d2..a2d23823 100644
--- a/pgjdbc/src/main/java/org/postgresql/core/QueryExecutor.java
+++ b/pgjdbc/src/main/java/org/postgresql/core/QueryExecutor.java
@@ -121,6 +121,11 @@ public interface QueryExecutor extends TypeTransferModeRegistry {
 
   int MAX_SAVE_POINTS = 1000;
 
+  /**
+   * Flag indicating that when beginning a transaction, it should be read only.
+   */
+  int QUERY_READ_ONLY_HINT = 2048;
+
   /**
    * Execute a Query, passing results to a provided ResultHandler.
    *
diff --git a/pgjdbc/src/main/java/org/postgresql/core/v3/QueryExecutorImpl.java b/pgjdbc/src/main/java/org/postgresql/core/v3/QueryExecutorImpl.java
index 48ca673b..4e85f02a 100644
--- a/pgjdbc/src/main/java/org/postgresql/core/v3/QueryExecutorImpl.java
+++ b/pgjdbc/src/main/java/org/postgresql/core/v3/QueryExecutorImpl.java
@@ -371,9 +371,7 @@ public class QueryExecutorImpl extends QueryExecutorBase {
               // PostgreSQL does not support bind, exec, simple, sync message flow,
               // so we force autosavepoint to use simple if the main query is using simple
               | QUERY_EXECUTE_AS_SIMPLE);
-
       return true;
-
     }
     return false;
   }
@@ -548,7 +546,9 @@ public class QueryExecutorImpl extends QueryExecutorBase {
 
     beginFlags = updateQueryMode(beginFlags);
 
-    sendOneQuery(beginTransactionQuery, SimpleQuery.NO_PARAMETERS, 0, 0, beginFlags);
+    final SimpleQuery beginQuery = ((flags & QueryExecutor.QUERY_READ_ONLY_HINT) == 0) ? beginTransactionQuery : beginReadOnlyTransactionQuery;
+
+    sendOneQuery(beginQuery, SimpleQuery.NO_PARAMETERS, 0, 0, beginFlags);
 
     // Insert a handler that intercepts the BEGIN.
     return new ResultHandlerDelegate(delegateHandler) {
@@ -1145,8 +1145,8 @@ public class QueryExecutorImpl extends QueryExecutorBase {
             try {
               if (op == null) {
                 throw new PSQLException(GT
-                  .tr("Received CommandComplete ''{0}'' without an active copy operation", status),
-                  PSQLState.OBJECT_NOT_IN_STATE);
+                    .tr("Received CommandComplete ''{0}'' without an active copy operation", status),
+                    PSQLState.OBJECT_NOT_IN_STATE);
               }
               op.handleCommandStatus(status);
             } catch (SQLException se) {
@@ -1171,7 +1171,7 @@ public class QueryExecutorImpl extends QueryExecutorBase {
 
             if (op != null) {
               error = new PSQLException(GT.tr("Got CopyInResponse from server during an active {0}",
-                op.getClass().getName()), PSQLState.OBJECT_NOT_IN_STATE);
+                  op.getClass().getName()), PSQLState.OBJECT_NOT_IN_STATE);
             }
 
             op = new CopyInImpl();
@@ -1184,8 +1184,7 @@ public class QueryExecutorImpl extends QueryExecutorBase {
             LOGGER.log(Level.FINEST, " <=BE CopyOutResponse");
 
             if (op != null) {
-              error =
-                new PSQLException(GT.tr("Got CopyOutResponse from server during an active {0}",
+              error = new PSQLException(GT.tr("Got CopyOutResponse from server during an active {0}",
                   op.getClass().getName()), PSQLState.OBJECT_NOT_IN_STATE);
             }
 
@@ -1199,8 +1198,7 @@ public class QueryExecutorImpl extends QueryExecutorBase {
             LOGGER.log(Level.FINEST, " <=BE CopyBothResponse");
 
             if (op != null) {
-              error =
-                new PSQLException(GT.tr("Got CopyBothResponse from server during an active {0}",
+              error = new PSQLException(GT.tr("Got CopyBothResponse from server during an active {0}",
                   op.getClass().getName()), PSQLState.OBJECT_NOT_IN_STATE);
             }
 
@@ -1220,11 +1218,11 @@ public class QueryExecutorImpl extends QueryExecutorBase {
             byte[] buf = pgStream.receive(len);
             if (op == null) {
               error = new PSQLException(GT.tr("Got CopyData without an active copy operation"),
-                PSQLState.OBJECT_NOT_IN_STATE);
+                  PSQLState.OBJECT_NOT_IN_STATE);
             } else if (!(op instanceof CopyOut)) {
               error = new PSQLException(
-                GT.tr("Unexpected copydata from server for {0}", op.getClass().getName()),
-                PSQLState.COMMUNICATION_ERROR);
+                  GT.tr("Unexpected copydata from server for {0}", op.getClass().getName()),
+                  PSQLState.COMMUNICATION_ERROR);
             } else {
               op.handleCopydata(buf);
             }
@@ -1242,7 +1240,7 @@ public class QueryExecutorImpl extends QueryExecutorBase {
 
             if (!(op instanceof CopyOut)) {
               error = new PSQLException("Got CopyDone while not copying from server",
-                PSQLState.OBJECT_NOT_IN_STATE);
+                  PSQLState.OBJECT_NOT_IN_STATE);
             }
 
             // keep receiving since we expect a CommandComplete
@@ -1283,7 +1281,7 @@ public class QueryExecutorImpl extends QueryExecutorBase {
 
           default:
             throw new IOException(
-              GT.tr("Unexpected packet type during copy: {0}", Integer.toString(c)));
+                GT.tr("Unexpected packet type during copy: {0}", Integer.toString(c)));
         }
 
         // Collect errors into a neat chain for completeness
@@ -2792,6 +2790,11 @@ public class QueryExecutorImpl extends QueryExecutorBase {
           new NativeQuery("BEGIN", new int[0], false, SqlCommand.BLANK),
           null, false);
 
+  private final SimpleQuery beginReadOnlyTransactionQuery =
+      new SimpleQuery(
+          new NativeQuery("BEGIN READ ONLY", new int[0], false, SqlCommand.BLANK),
+          null, false);
+
   private final SimpleQuery emptyQuery =
       new SimpleQuery(
           new NativeQuery("", new int[0], false,
@@ -2815,7 +2818,4 @@ public class QueryExecutorImpl extends QueryExecutorBase {
       new SimpleQuery(
           new NativeQuery("ROLLBACK TO SAVEPOINT PGJDBC_AUTOSAVE", new int[0], false, SqlCommand.BLANK),
           null, false);
-
-
-
 }
diff --git a/pgjdbc/src/main/java/org/postgresql/ds/common/BaseDataSource.java b/pgjdbc/src/main/java/org/postgresql/ds/common/BaseDataSource.java
index f4be8197..8baa1fb9 100644
--- a/pgjdbc/src/main/java/org/postgresql/ds/common/BaseDataSource.java
+++ b/pgjdbc/src/main/java/org/postgresql/ds/common/BaseDataSource.java
@@ -912,6 +912,22 @@ public abstract class BaseDataSource implements CommonDataSource, Referenceable
     PGProperty.READ_ONLY.set(properties, readOnly);
   }
 
+  /**
+   * @return The behavior when set read only
+   * @see PGProperty#READ_ONLY_MODE
+   */
+  public String getReadOnlyMode() {
+    return PGProperty.READ_ONLY_MODE.get(properties);
+  }
+
+  /**
+   * @param mode the behavior when set read only
+   * @see PGProperty#READ_ONLY_MODE
+   */
+  public void setReadOnlyMode(String mode) {
+    PGProperty.READ_ONLY_MODE.set(properties, mode);
+  }
+
   /**
    * @return true if driver should log unclosed connections
    * @see PGProperty#LOG_UNCLOSED_CONNECTIONS
diff --git a/pgjdbc/src/main/java/org/postgresql/jdbc/PgConnection.java b/pgjdbc/src/main/java/org/postgresql/jdbc/PgConnection.java
index d9f4debc..2e7b9f2c 100644
--- a/pgjdbc/src/main/java/org/postgresql/jdbc/PgConnection.java
+++ b/pgjdbc/src/main/java/org/postgresql/jdbc/PgConnection.java
@@ -81,6 +81,12 @@ public class PgConnection implements BaseConnection {
   private static final SQLPermission SQL_PERMISSION_ABORT = new SQLPermission("callAbort");
   private static final SQLPermission SQL_PERMISSION_NETWORK_TIMEOUT = new SQLPermission("setNetworkTimeout");
 
+  private enum ReadOnlyBehavior {
+    ignore,
+    transaction,
+    always;
+  }
+
   //
   // Data initialized on construction:
   //
@@ -89,6 +95,8 @@ public class PgConnection implements BaseConnection {
   /* URL we were created via */
   private final String creatingURL;
 
+  private final ReadOnlyBehavior readOnlyBehavior;
+
   private Throwable openStackTrace;
 
   /* Actual network handler */
@@ -99,6 +107,10 @@ public class PgConnection implements BaseConnection {
   /* Query that runs ROLLBACK */
   private final Query rollbackQuery;
 
+  private final CachedQuery setSessionReadOnly;
+
+  private final CachedQuery setSessionNotReadOnly;
+
   private final TypeInfo typeCache;
 
   private boolean disableColumnSanitiser = false;
@@ -184,6 +196,8 @@ public class PgConnection implements BaseConnection {
 
     this.creatingURL = url;
 
+    this.readOnlyBehavior = getReadOnlyBehavior(PGProperty.READ_ONLY_MODE.get(info));
+
     setDefaultFetchSize(PGProperty.DEFAULT_ROW_FETCH_SIZE.getInt(info));
 
     setPrepareThreshold(PGProperty.PREPARE_THRESHOLD.getInt(info));
@@ -199,6 +213,9 @@ public class PgConnection implements BaseConnection {
       LOGGER.log(Level.WARNING, "Unsupported Server Version: {0}", queryExecutor.getServerVersion());
     }
 
+    setSessionReadOnly = createQuery("SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY", false, true);
+    setSessionNotReadOnly = createQuery("SET SESSION CHARACTERISTICS AS TRANSACTION READ WRITE", false, true);
+
     // Set read-only early if requested
     if (PGProperty.READ_ONLY.getBoolean(info)) {
       setReadOnly(true);
@@ -292,6 +309,18 @@ public class PgConnection implements BaseConnection {
     replicationConnection = PGProperty.REPLICATION.get(info) != null;
   }
 
+  private static ReadOnlyBehavior getReadOnlyBehavior(String property) {
+    try {
+      return ReadOnlyBehavior.valueOf(property);
+    } catch (IllegalArgumentException e) {
+      try {
+        return ReadOnlyBehavior.valueOf(property.toLowerCase(Locale.US));
+      } catch (IllegalArgumentException e2) {
+        return ReadOnlyBehavior.transaction;
+      }
+    }
+  }
+
   private static Set<Integer> getBinaryOids(Properties info) throws PSQLException {
     boolean binaryTransfer = PGProperty.BINARY_TRANSFER.getBoolean(info);
     // Formats that currently have binary protocol support
@@ -453,6 +482,24 @@ public class PgConnection implements BaseConnection {
     stmt.close();
   }
 
+  void execSQLUpdate(CachedQuery query) throws SQLException {
+    BaseStatement stmt = (BaseStatement) createStatement();
+    if (stmt.executeWithFlags(query, QueryExecutor.QUERY_NO_METADATA | QueryExecutor.QUERY_NO_RESULTS
+        | QueryExecutor.QUERY_SUPPRESS_BEGIN)) {
+      throw new PSQLException(GT.tr("A result was returned when none was expected."),
+          PSQLState.TOO_MANY_RESULTS);
+    }
+
+    // Transfer warnings to the connection, since the user never
+    // has a chance to see the statement itself.
+    SQLWarning warnings = stmt.getWarnings();
+    if (warnings != null) {
+      addWarning(warnings);
+    }
+
+    stmt.close();
+  }
+
   /**
    * <p>In SQL, a result table can be retrieved through a cursor that is named. The current row of a
    * result can be updated or deleted using a positioned update/delete statement that references the
@@ -705,10 +752,8 @@ public class PgConnection implements BaseConnection {
           PSQLState.ACTIVE_SQL_TRANSACTION);
     }
 
-    if (readOnly != this.readOnly) {
-      String readOnlySql
-             = "SET SESSION CHARACTERISTICS AS TRANSACTION " + (readOnly ? "READ ONLY" : "READ WRITE");
-      execSQLUpdate(readOnlySql); // nb: no BEGIN triggered.
+    if (readOnly != this.readOnly && autoCommit && this.readOnlyBehavior == ReadOnlyBehavior.always) {
+      execSQLUpdate(readOnly ? setSessionReadOnly : setSessionNotReadOnly);
     }
 
     this.readOnly = readOnly;
@@ -721,6 +766,11 @@ public class PgConnection implements BaseConnection {
     return readOnly;
   }
 
+  @Override
+  public boolean hintReadOnly() {
+    return readOnly && readOnlyBehavior != ReadOnlyBehavior.ignore;
+  }
+
   @Override
   public void setAutoCommit(boolean autoCommit) throws SQLException {
     checkClosed();
@@ -733,6 +783,21 @@ public class PgConnection implements BaseConnection {
       commit();
     }
 
+    // if the connection is read only, we need to make sure session settings are
+    // correct when autocommit status changed
+    if (this.readOnly && readOnlyBehavior == ReadOnlyBehavior.always) {
+      // if we are turning on autocommit, we need to set session
+      // to read only
+      if (autoCommit) {
+        this.autoCommit = true;
+        execSQLUpdate(setSessionReadOnly);
+      } else {
+        // if we are turning auto commit off, we need to
+        // disable session
+        execSQLUpdate(setSessionNotReadOnly);
+      }
+    }
+
     this.autoCommit = autoCommit;
     LOGGER.log(Level.FINE, "  setAutoCommit = {0}", autoCommit);
   }
diff --git a/pgjdbc/src/main/java/org/postgresql/jdbc/PgDatabaseMetaData.java b/pgjdbc/src/main/java/org/postgresql/jdbc/PgDatabaseMetaData.java
index 26440aec..b2f69a6f 100644
--- a/pgjdbc/src/main/java/org/postgresql/jdbc/PgDatabaseMetaData.java
+++ b/pgjdbc/src/main/java/org/postgresql/jdbc/PgDatabaseMetaData.java
@@ -1595,7 +1595,7 @@ public class PgDatabaseMetaData implements DatabaseMetaData {
         /*
         From the docs if typtypmod is -1
          */
-        int domainLength = rs.getInt("typtypmod");
+        int typtypmod = rs.getInt("typtypmod");
         decimalDigits = connection.getTypeInfo().getScale(baseTypeOid, typeMod);
         /*
         From the postgres docs:
@@ -1604,10 +1604,13 @@ public class PgDatabaseMetaData implements DatabaseMetaData {
         if it is -1 then get the precision from the basetype. This doesn't help if the basetype is
         a domain, but for actual types this will return the correct value.
          */
-        if ( domainLength == -1 ) {
+        if ( typtypmod == -1 ) {
           columnSize = connection.getTypeInfo().getPrecision(baseTypeOid, typeMod);
+        } else if (baseTypeOid == Oid.NUMERIC ) {
+          decimalDigits = connection.getTypeInfo().getScale(baseTypeOid, typtypmod);
+          columnSize = connection.getTypeInfo().getPrecision(baseTypeOid, typtypmod);
         } else {
-          columnSize = domainLength;
+          columnSize = typtypmod;
         }
       } else {
         decimalDigits = connection.getTypeInfo().getScale(typeOid, typeMod);
diff --git a/pgjdbc/src/main/java/org/postgresql/jdbc/PgStatement.java b/pgjdbc/src/main/java/org/postgresql/jdbc/PgStatement.java
index efc0e336..3a3ee015 100644
--- a/pgjdbc/src/main/java/org/postgresql/jdbc/PgStatement.java
+++ b/pgjdbc/src/main/java/org/postgresql/jdbc/PgStatement.java
@@ -408,6 +408,9 @@ public class PgStatement implements Statement, BaseStatement {
     if (connection.getAutoCommit()) {
       flags |= QueryExecutor.QUERY_SUPPRESS_BEGIN;
     }
+    if (connection.hintReadOnly()) {
+      flags |= QueryExecutor.QUERY_READ_ONLY_HINT;
+    }
 
     // updateable result sets do not yet support binary updates
     if (concurrency != ResultSet.CONCUR_READ_ONLY) {
@@ -810,6 +813,9 @@ public class PgStatement implements Statement, BaseStatement {
     if (connection.getAutoCommit()) {
       flags |= QueryExecutor.QUERY_SUPPRESS_BEGIN;
     }
+    if (connection.hintReadOnly()) {
+      flags |= QueryExecutor.QUERY_READ_ONLY_HINT;
+    }
 
     BatchResultHandler handler;
     handler = createBatchHandler(queries, parameterLists);
diff --git a/pgjdbc/src/main/java/org/postgresql/util/PGInterval.java b/pgjdbc/src/main/java/org/postgresql/util/PGInterval.java
index c1861b9d..0e9ccfad 100644
--- a/pgjdbc/src/main/java/org/postgresql/util/PGInterval.java
+++ b/pgjdbc/src/main/java/org/postgresql/util/PGInterval.java
@@ -7,8 +7,6 @@ package org.postgresql.util;
 
 import java.io.Serializable;
 import java.sql.SQLException;
-import java.text.DecimalFormat;
-import java.text.DecimalFormatSymbols;
 import java.util.Calendar;
 import java.util.Date;
 import java.util.StringTokenizer;
@@ -19,20 +17,12 @@ import java.util.StringTokenizer;
 public class PGInterval extends PGobject implements Serializable, Cloneable {
 
   private int years;
-  private int months;
-  private int days;
-  private int hours;
-  private int minutes;
-  private double seconds;
-
-  private static final DecimalFormat secondsFormat;
-
-  static {
-    secondsFormat = new DecimalFormat("0.00####");
-    DecimalFormatSymbols dfs = secondsFormat.getDecimalFormatSymbols();
-    dfs.setDecimalSeparator('.');
-    secondsFormat.setDecimalFormatSymbols(dfs);
-  }
+  private byte months;
+  private byte days;
+  private byte hours;
+  private byte minutes;
+  private int wholeSeconds;
+  private int microSeconds;
 
   /**
    * required by the driver.
@@ -53,6 +43,66 @@ public class PGInterval extends PGobject implements Serializable, Cloneable {
     setValue(value);
   }
 
+  private int lookAhead(String value, int position, String find) {
+    char [] tokens = find.toCharArray();
+    int found = -1;
+
+    for ( int i = 0; i < tokens.length; i++ ) {
+      found = value.indexOf(tokens[i], position);
+      if ( found > 0 ) {
+        return found;
+      }
+    }
+    return found;
+  }
+
+  private void parseISO8601Format(String value) {
+    int number = 0;
+    String dateValue;
+    String timeValue = null;
+
+    int hasTime = value.indexOf('T');
+    if ( hasTime > 0 ) {
+      /* skip over the P */
+      dateValue = value.substring(1,hasTime);
+      timeValue = value.substring(hasTime + 1);
+    } else {
+      /* skip over the P */
+      dateValue = value.substring(1);
+    }
+
+    for ( int i = 0; i < dateValue.length(); i++ ) {
+      int lookAhead = lookAhead(dateValue, i, "YMD");
+      if (lookAhead > 0) {
+        number = Integer.parseInt(dateValue.substring(i, lookAhead));
+        if (dateValue.charAt(lookAhead) == 'Y') {
+          setYears(number);
+        } else if (dateValue.charAt(lookAhead) == 'M') {
+          setMonths(number);
+        } else if (dateValue.charAt(lookAhead) == 'D') {
+          setDays(number);
+        }
+        i = lookAhead;
+      }
+    }
+    if ( timeValue != null ) {
+      for (int i = 0; i < timeValue.length(); i++) {
+        int lookAhead = lookAhead(timeValue, i, "HMS");
+        if (lookAhead > 0) {
+          number = Integer.parseInt(timeValue.substring(i, lookAhead));
+          if (timeValue.charAt(lookAhead) == 'H') {
+            setHours(number);
+          } else if (timeValue.charAt(lookAhead) == 'M') {
+            setMinutes(number);
+          } else if (timeValue.charAt(lookAhead) == 'S') {
+            setSeconds(number);
+          }
+          i = lookAhead;
+        }
+      }
+    }
+  }
+
   /**
    * Initializes all values of this interval to the specified values.
    *
@@ -77,10 +127,13 @@ public class PGInterval extends PGobject implements Serializable, Cloneable {
    * @throws SQLException Is thrown if the string representation has an unknown format
    */
   public void setValue(String value) throws SQLException {
-    final boolean ISOFormat = !value.startsWith("@");
-
+    final boolean PostgresFormat = !value.startsWith("@");
+    if (value.startsWith("P")) {
+      parseISO8601Format(value);
+      return;
+    }
     // Just a simple '0'
-    if (!ISOFormat && value.length() == 3 && value.charAt(2) == '0') {
+    if (!PostgresFormat && value.length() == 3 && value.charAt(2) == '0') {
       setValue(0, 0, 0, 0, 0, 0.0);
       return;
     }
@@ -153,7 +206,7 @@ public class PGInterval extends PGobject implements Serializable, Cloneable {
           PSQLState.NUMERIC_CONSTANT_OUT_OF_RANGE, e);
     }
 
-    if (!ISOFormat && value.endsWith("ago")) {
+    if (!PostgresFormat && value.endsWith("ago")) {
       // Inverse the leading sign
       setValue(-years, -months, -days, -hours, -minutes, -seconds);
     } else {
@@ -191,7 +244,7 @@ public class PGInterval extends PGobject implements Serializable, Cloneable {
         + days + " days "
         + hours + " hours "
         + minutes + " mins "
-        + secondsFormat.format(seconds) + " secs";
+        + wholeSeconds + '.' + microSeconds + " secs";
   }
 
   /**
@@ -227,7 +280,7 @@ public class PGInterval extends PGobject implements Serializable, Cloneable {
    * @param months months to set
    */
   public void setMonths(int months) {
-    this.months = months;
+    this.months = (byte) months;
   }
 
   /**
@@ -245,7 +298,7 @@ public class PGInterval extends PGobject implements Serializable, Cloneable {
    * @param days days to set
    */
   public void setDays(int days) {
-    this.days = days;
+    this.days = (byte)days;
   }
 
   /**
@@ -263,7 +316,7 @@ public class PGInterval extends PGobject implements Serializable, Cloneable {
    * @param hours hours to set
    */
   public void setHours(int hours) {
-    this.hours = hours;
+    this.hours = (byte)hours;
   }
 
   /**
@@ -281,7 +334,7 @@ public class PGInterval extends PGobject implements Serializable, Cloneable {
    * @param minutes minutes to set
    */
   public void setMinutes(int minutes) {
-    this.minutes = minutes;
+    this.minutes = (byte)minutes;
   }
 
   /**
@@ -290,7 +343,22 @@ public class PGInterval extends PGobject implements Serializable, Cloneable {
    * @return seconds represented by this interval
    */
   public double getSeconds() {
-    return seconds;
+    if ( microSeconds < 0) {
+      if ( wholeSeconds == 0 ) {
+        return Double.parseDouble("-0." + -microSeconds);
+      } else {
+        return Double.parseDouble("" + wholeSeconds + '.' + -microSeconds);
+      }
+    }
+    return Double.parseDouble("" + wholeSeconds + '.' + microSeconds );
+  }
+
+  public int getWholeSeconds() {
+    return wholeSeconds;
+  }
+
+  public int getMicroSeconds() {
+    return microSeconds;
   }
 
   /**
@@ -299,7 +367,23 @@ public class PGInterval extends PGobject implements Serializable, Cloneable {
    * @param seconds seconds to set
    */
   public void setSeconds(double seconds) {
-    this.seconds = seconds;
+    String str = Double.toString(seconds);
+    int decimal = str.indexOf('.');
+    if (decimal > 0) {
+
+      /* how many 10's do we need to multiply by to get microseconds */
+      String micSeconds = str.substring(decimal + 1);
+      int power = 6 - micSeconds.length();
+
+      microSeconds = Integer.parseInt(micSeconds) * (int)Math.pow(10,power);
+      wholeSeconds = Integer.parseInt(str.substring(0,decimal));
+    } else {
+      microSeconds = 0;
+      wholeSeconds = Integer.parseInt(str);
+    }
+    if ( seconds < 0 ) {
+      microSeconds = -microSeconds;
+    }
   }
 
   /**
@@ -308,10 +392,8 @@ public class PGInterval extends PGobject implements Serializable, Cloneable {
    * @param cal Calendar instance to add to
    */
   public void add(Calendar cal) {
-    // Avoid precision loss
-    // Be aware postgres doesn't return more than 60 seconds - no overflow can happen
-    final int microseconds = (int) (getSeconds() * 1000000.0);
-    final int milliseconds = (microseconds + ((microseconds < 0) ? -500 : 500)) / 1000;
+
+    final int milliseconds = (microSeconds + ((microSeconds < 0) ? -500 : 500)) / 1000 + wholeSeconds * 1000;
 
     cal.add(Calendar.MILLISECOND, milliseconds);
     cal.add(Calendar.MINUTE, getMinutes());
@@ -413,7 +495,8 @@ public class PGInterval extends PGobject implements Serializable, Cloneable {
         && pgi.days == days
         && pgi.hours == hours
         && pgi.minutes == minutes
-        && Double.doubleToLongBits(pgi.seconds) == Double.doubleToLongBits(seconds);
+        && pgi.wholeSeconds == wholeSeconds
+        && pgi.microSeconds == microSeconds;
   }
 
   /**
@@ -421,8 +504,9 @@ public class PGInterval extends PGobject implements Serializable, Cloneable {
    *
    * @return hashCode
    */
+  @Override
   public int hashCode() {
-    return ((((((7 * 31 + (int) Double.doubleToLongBits(seconds)) * 31 + minutes) * 31 + hours) * 31
+    return (((((((8 * 31 + microSeconds) * 31 + wholeSeconds) * 31 + minutes) * 31 + hours) * 31
         + days) * 31 + months) * 31 + years) * 31;
   }
 


Test patch:
diff --git a/pgjdbc/src/test/java/org/postgresql/test/jdbc2/ConnectionTest.java b/pgjdbc/src/test/java/org/postgresql/test/jdbc2/ConnectionTest.java
index a0e211b9..0b5ca798 100644
--- a/pgjdbc/src/test/java/org/postgresql/test/jdbc2/ConnectionTest.java
+++ b/pgjdbc/src/test/java/org/postgresql/test/jdbc2/ConnectionTest.java
@@ -6,11 +6,13 @@
 package org.postgresql.test.jdbc2;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
+import org.postgresql.PGProperty;
 import org.postgresql.jdbc.PgConnection;
 import org.postgresql.test.TestUtil;
 
@@ -26,6 +28,7 @@ import java.sql.SQLWarning;
 import java.sql.Statement;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.Properties;
 
 /**
  * TestCase to test the internal functionality of org.postgresql.jdbc2.Connection and it's
@@ -155,6 +158,214 @@ public class ConnectionTest {
     TestUtil.closeDB(con);
   }
 
+  /*
+   * Tests for session and transaction read only behavior with "always" read only mode.
+   */
+  @Test
+  public void testReadOnly_always() throws Exception {
+    final Properties props = new Properties();
+    PGProperty.READ_ONLY_MODE.set(props, "always");
+    con = TestUtil.openDB(props);
+    Statement st;
+    ResultSet rs;
+
+    con.setAutoCommit(true);
+    con.setReadOnly(true);
+    assertTrue(con.getAutoCommit());
+    assertTrue(con.isReadOnly());
+
+    // Now test insert with auto commit true and read only
+    st = con.createStatement();
+    try {
+      st.executeUpdate("insert into test_a (imagename,image,id) values ('comttest',1234,5678)");
+      fail("insert should have failed when read only");
+    } catch (SQLException e) {
+      assertStringContains(e.getMessage(), "read-only");
+    }
+
+    con.setAutoCommit(false);
+
+    // auto commit false and read only
+    try {
+      st.executeUpdate("insert into test_a (imagename,image,id) values ('comttest',1234,5678)");
+      fail("insert should have failed when read only");
+    } catch (SQLException e) {
+      assertStringContains(e.getMessage(), "read-only");
+    }
+
+    try {
+      con.setReadOnly(false);
+      fail("cannot set read only during transaction");
+    } catch (SQLException e) {
+      assertStringContains(e.getMessage(), "Cannot change transaction read-only");
+    }
+
+    // end the transaction
+    con.rollback();
+
+    // disable read only
+    con.setReadOnly(false);
+
+    assertEquals(1, st.executeUpdate("insert into test_a (imagename,image,id) values ('comttest',1234,5678)"));
+
+    // Now update image to 9876 and commit
+    st.executeUpdate("update test_a set image=9876 where id=5678");
+    con.commit();
+
+    // back to read only for successful query
+    con.setReadOnly(true);
+    rs = st.executeQuery("select image from test_a where id=5678");
+    assertTrue(rs.next());
+    assertEquals(9876, rs.getInt(1));
+    rs.close();
+
+    // Now try to change with auto commit false
+    try {
+      st.executeUpdate("update test_a set image=1111 where id=5678");
+      fail("update should fail when read only");
+    } catch (SQLException e) {
+      assertStringContains(e.getMessage(), "read-only");
+      con.rollback();
+    }
+
+    // test that value did not change
+    rs = st.executeQuery("select image from test_a where id=5678");
+    assertTrue(rs.next());
+    assertEquals(9876, rs.getInt(1)); // Should not change!
+    rs.close();
+
+    // repeat attempt to chagne with auto commit true
+    con.setAutoCommit(true);
+
+    try {
+      st.executeUpdate("update test_a set image=1111 where id=5678");
+      fail("update should fail when read only");
+    } catch (SQLException e) {
+      assertStringContains(e.getMessage(), "read-only");
+    }
+
+    // test that value did not change
+    rs = st.executeQuery("select image from test_a where id=5678");
+    assertTrue(rs.next());
+    assertEquals(9876, rs.getInt(1)); // Should not change!
+    rs.close();
+
+    TestUtil.closeDB(con);
+  }
+
+  /*
+   * Tests for session and transaction read only behavior with "ignore" read only mode.
+   */
+  @Test
+  public void testReadOnly_ignore() throws Exception {
+    final Properties props = new Properties();
+    PGProperty.READ_ONLY_MODE.set(props, "ignore");
+    con = TestUtil.openDB(props);
+    Statement st;
+    ResultSet rs;
+
+    con.setAutoCommit(true);
+    con.setReadOnly(true);
+    assertTrue(con.getAutoCommit());
+    assertTrue(con.isReadOnly());
+
+    // Now test insert with auto commit true and read only
+    st = con.createStatement();
+    assertEquals(1, st.executeUpdate("insert into test_a (imagename,image,id) values ('comttest',1234,5678)"));
+    con.setAutoCommit(false);
+
+    // Now update image to 9876 and commit
+    st.executeUpdate("update test_a set image=9876 where id=5678");
+
+    // back to read only for successful query
+    rs = st.executeQuery("select image from test_a where id=5678");
+    assertTrue(rs.next());
+    assertEquals(9876, rs.getInt(1));
+    rs.close();
+
+    con.rollback();
+
+    // test that value did not change
+    rs = st.executeQuery("select image from test_a where id=5678");
+    assertTrue(rs.next());
+    assertEquals(1234, rs.getInt(1)); // Should not change!
+    rs.close();
+
+    TestUtil.closeDB(con);
+  }
+
+  /*
+   * Tests for session and transaction read only behavior with "transaction" read only mode.
+   */
+  @Test
+  public void testReadOnly_transaction() throws Exception {
+    final Properties props = new Properties();
+    PGProperty.READ_ONLY_MODE.set(props, "transaction");
+    con = TestUtil.openDB(props);
+    Statement st;
+    ResultSet rs;
+
+    con.setAutoCommit(false);
+    con.setReadOnly(true);
+    assertFalse(con.getAutoCommit());
+    assertTrue(con.isReadOnly());
+
+    // Test insert with auto commit false and read only
+    st = con.createStatement();
+    try {
+      st.executeUpdate("insert into test_a (imagename,image,id) values ('comttest',1234,5678)");
+      fail("insert should have failed when read only");
+    } catch (SQLException e) {
+      assertStringContains(e.getMessage(), "read-only");
+    }
+
+    con.rollback();
+
+    con.setAutoCommit(true);
+    assertTrue(con.isReadOnly());
+    //with autocommit true and read only, can still insert
+    assertEquals(1, st.executeUpdate("insert into test_a (imagename,image,id) values ('comttest',1234,5678)"));
+
+    // Now update image to 9876
+    st.executeUpdate("update test_a set image=9876 where id=5678");
+
+    //successful query
+    rs = st.executeQuery("select image from test_a where id=5678");
+    assertTrue(rs.next());
+    assertEquals(9876, rs.getInt(1));
+    rs.close();
+
+    con.setAutoCommit(false);
+    // Now try to change with auto commit false
+    try {
+      st.executeUpdate("update test_a set image=1111 where id=5678");
+      fail("update should fail when read only");
+    } catch (SQLException e) {
+      assertStringContains(e.getMessage(), "read-only");
+    }
+
+    con.rollback();
+
+    // test that value did not change
+    rs = st.executeQuery("select image from test_a where id=5678");
+    assertTrue(rs.next());
+    assertEquals(9876, rs.getInt(1)); // Should not change!
+    rs.close();
+
+    // repeat attempt to chagne with auto commit true
+    con.setAutoCommit(true);
+
+    assertEquals(1, st.executeUpdate("update test_a set image=1111 where id=5678"));
+
+    // test that value did not change
+    rs = st.executeQuery("select image from test_a where id=5678");
+    assertTrue(rs.next());
+    assertEquals(1111, rs.getInt(1)); // Should not change!
+    rs.close();
+
+    TestUtil.closeDB(con);
+  }
+
   /*
    * Simple test to see if isClosed works.
    */
@@ -296,4 +507,10 @@ public class ConnectionTest {
     con.close();
     con.close();
   }
+
+  private static void assertStringContains(String orig, String toContain) {
+    if (!orig.contains(toContain)) {
+      fail("expected [" + orig + ']' + "to contain [" + toContain + "].");
+    }
+  }
 }
diff --git a/pgjdbc/src/test/java/org/postgresql/test/jdbc2/DatabaseMetaDataTest.java b/pgjdbc/src/test/java/org/postgresql/test/jdbc2/DatabaseMetaDataTest.java
index e8e03140..9d244726 100644
--- a/pgjdbc/src/test/java/org/postgresql/test/jdbc2/DatabaseMetaDataTest.java
+++ b/pgjdbc/src/test/java/org/postgresql/test/jdbc2/DatabaseMetaDataTest.java
@@ -84,7 +84,9 @@ public class DatabaseMetaDataTest {
 
     TestUtil.createDomain(con, "nndom", "int not null");
     TestUtil.createDomain(con, "varbit2", "varbit(3)");
-    TestUtil.createTable(con, "domaintable", "id nndom, v varbit2");
+    TestUtil.createDomain(con, "float83", "numeric(8,3)");
+
+    TestUtil.createTable(con, "domaintable", "id nndom, v varbit2, f float83");
     stmt.close();
   }
 
@@ -112,6 +114,9 @@ public class DatabaseMetaDataTest {
     stmt.execute("DROP FUNCTION f3(int, varchar)");
     TestUtil.dropTable(con, "domaintable");
     TestUtil.dropDomain(con, "nndom");
+    TestUtil.dropDomain(con, "varbit2");
+    TestUtil.dropDomain(con, "float83");
+
 
     TestUtil.closeDB(con);
   }
@@ -672,6 +677,7 @@ public class DatabaseMetaDataTest {
     assertEquals("id", rs.getString("COLUMN_NAME"));
     assertEquals("NO", rs.getString("IS_NULLABLE"));
     assertTrue(rs.next());
+    assertTrue(rs.next());
     assertTrue(!rs.next());
   }
 
@@ -685,6 +691,11 @@ public class DatabaseMetaDataTest {
     assertTrue(rs.next());
     assertEquals("v", rs.getString("COLUMN_NAME"));
     assertEquals(3, rs.getInt("COLUMN_SIZE"));
+    assertTrue(rs.next());
+    assertEquals("f", rs.getString("COLUMN_NAME"));
+    assertEquals(8, rs.getInt("COLUMN_SIZE"));
+    assertEquals( 3, rs.getInt("DECIMAL_DIGITS"));
+
   }
 
   @Test
diff --git a/pgjdbc/src/test/java/org/postgresql/test/jdbc2/DateTest.java b/pgjdbc/src/test/java/org/postgresql/test/jdbc2/DateTest.java
index 1f54965d..c7da9294 100644
--- a/pgjdbc/src/test/java/org/postgresql/test/jdbc2/DateTest.java
+++ b/pgjdbc/src/test/java/org/postgresql/test/jdbc2/DateTest.java
@@ -67,6 +67,7 @@ public class DateTest {
     assertEquals(1, stmt.executeUpdate(TestUtil.insertSQL("testdate", "'3456-01-01'")));
     assertEquals(1, stmt.executeUpdate(TestUtil.insertSQL("testdate", "'0101-01-01 BC'")));
 
+
     /* dateTest() contains all of the tests */
     dateTest();
 
diff --git a/pgjdbc/src/test/java/org/postgresql/test/jdbc2/IntervalTest.java b/pgjdbc/src/test/java/org/postgresql/test/jdbc2/IntervalTest.java
index 80395c44..56b58784 100644
--- a/pgjdbc/src/test/java/org/postgresql/test/jdbc2/IntervalTest.java
+++ b/pgjdbc/src/test/java/org/postgresql/test/jdbc2/IntervalTest.java
@@ -107,7 +107,7 @@ public class IntervalTest {
     PGInterval interval = new PGInterval("1 year 3 months");
     String coercedStringValue = interval.toString();
 
-    assertEquals("1 years 3 mons 0 days 0 hours 0 mins 0.00 secs", coercedStringValue);
+    assertEquals("1 years 3 mons 0 days 0 hours 0 mins 0.0 secs", coercedStringValue);
   }
 
   @Test
@@ -256,7 +256,7 @@ public class IntervalTest {
   }
 
   @Test
-  public void testISODate() throws Exception {
+  public void testPostgresDate() throws Exception {
     Date date = getStartCalendar().getTime();
     Date date2 = getStartCalendar().getTime();
 
@@ -269,6 +269,33 @@ public class IntervalTest {
     assertEquals(date2, date);
   }
 
+  @Test
+  public void testISO8601() throws Exception {
+    PGInterval pgi = new PGInterval("P1Y2M3DT4H5M6S");
+    assertEquals(1, pgi.getYears() );
+    assertEquals(2, pgi.getMonths() );
+    assertEquals(3, pgi.getDays() );
+    assertEquals(4, pgi.getHours() );
+    assertEquals( 5, pgi.getMinutes() );
+    assertEquals( 6, pgi.getSeconds(), .1 );
+
+    pgi = new PGInterval("P-1Y2M3DT4H5M6S");
+    assertEquals(-1, pgi.getYears());
+
+    pgi = new PGInterval("P1Y2M");
+    assertEquals(1,pgi.getYears());
+    assertEquals(2, pgi.getMonths());
+    assertEquals(0, pgi.getDays());
+
+    pgi = new PGInterval("P3DT4H5M6S");
+    assertEquals(0,pgi.getYears());
+
+    pgi = new PGInterval("P-1Y-2M3DT-4H-5M-6S");
+    assertEquals(-1, pgi.getYears());
+    assertEquals(-2, pgi.getMonths());
+    assertEquals(-4, pgi.getHours());
+  }
+
   private java.sql.Date makeDate(int y, int m, int d) {
     return new java.sql.Date(y - 1900, m - 1, d);
   }
diff --git a/pgjdbc/src/test/java/org/postgresql/test/jdbc4/jdbc41/GetObjectTest.java b/pgjdbc/src/test/java/org/postgresql/test/jdbc4/jdbc41/GetObjectTest.java
index bcdc541e..d20a400b 100644
--- a/pgjdbc/src/test/java/org/postgresql/test/jdbc4/jdbc41/GetObjectTest.java
+++ b/pgjdbc/src/test/java/org/postgresql/test/jdbc4/jdbc41/GetObjectTest.java
@@ -338,6 +338,36 @@ public class GetObjectTest {
     }
   }
 
+  @Test
+  public void testGetNullDate() throws SQLException {
+    Statement stmt = conn.createStatement();
+    stmt.executeUpdate(TestUtil.insertSQL("table1","date_column","NULL"));
+
+    ResultSet rs = stmt.executeQuery(TestUtil.selectSQL("table1", "date_column"));
+    try {
+      assertTrue(rs.next());
+      Date date = rs.getObject(1,Date.class);
+      assertTrue(rs.wasNull());
+    } finally {
+      rs.close();
+    }
+  }
+
+  @Test
+  public void testGetNullTimestamp() throws SQLException {
+    Statement stmt = conn.createStatement();
+    stmt.executeUpdate(TestUtil.insertSQL("table1","timestamp_without_time_zone_column","NULL"));
+
+    ResultSet rs = stmt.executeQuery(TestUtil.selectSQL("table1", "timestamp_without_time_zone_column"));
+    try {
+      assertTrue(rs.next());
+      java.util.Date ts = rs.getObject(1, java.util.Date.class);
+      assertTrue(rs.wasNull());
+    } finally {
+      rs.close();
+    }
+  }
+
   /**
    * Test the behavior getObject for time columns.
    */