Repository: pgjdbc/pgjdbc
Pull request #1575 (2019-09-23 21:18:22)

Fix exception on PGCopyOutputStream.close() after endCopy() (#1574)
### All Submissions:

* [x] Have you followed the guidelines in our [Contributing](https://github.com/pgjdbc/pgjdbc/blob/master/CONTRIBUTING.md) document?
* [x] Have you checked to ensure there aren't other open [Pull Requests](../../pulls) for the same update/change?

<!-- You can erase any parts of this template not applicable to your Pull Request. -->

### Changes to Existing Features:

* [ ] Does this break existing behaviour? If so please explain.
* [x] Have you added an explanation of what your changes do and why you'd like us to include them?
* [x] Have you written new tests for your core changes, as applicable?
* [x] Have you successfully run tests with your changes locally?
---

Implemented a fix for `PGCopyOutputStream.close()` throwing an exception after invoking `endCopy()` (issue #1574). The fix is done similar how `PGCopyInputStream` handles `close()`.

While writing the fix noticed that there is no sane way to determine whether the copy operation has ended or not. The `isActive()` method throws a `NullPointerException` after `close()` since `op` field is `null`, should return `false` like done in `PGCopyInputStream.isActive()`.

Besides that added a check for `flush()` so that it throws the same `IOException` with "This copy stream is closed." message like done in `write()` methods, instead of a `NullPointerException`.

AppVeyorBot: :x: [Build pgjdbc 1.0.462 failed](https://ci.appveyor.com/project/davecramer/pgjdbc/builds/27611205) (commit https://github.com/pgjdbc/pgjdbc/commit/124d2d4c0d by @virtual-machinist)
virtual-machinist: ```
Failed tests: 
  IsValidTest.testIsValidRemoteClose:100 The first connection should now be invalid
```
Seems a bit flaky to me, fails in other PRs too.
AppVeyorBot: :x: [Build pgjdbc 1.0.463 failed](https://ci.appveyor.com/project/davecramer/pgjdbc/builds/27611578) (commit https://github.com/pgjdbc/pgjdbc/commit/27f47188f3 by @virtual-machinist)
virtual-machinist: Literally the only thing I changed was import order for checkstyle and one of `IsValidTest.testIsValidRemoteClose` miraculously passed. :grinning: The other one still failed though.
davecramer: Ya that test seems to fail randomly. For now I have been ignoring it. 
AppVeyorBot: :white_check_mark: [Build pgjdbc 1.0.463 completed](https://ci.appveyor.com/project/davecramer/pgjdbc/builds/27625936) (commit https://github.com/pgjdbc/pgjdbc/commit/27f47188f3 by @virtual-machinist)

Patch:
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index 4efe774b..fcff6cd3 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -9,7 +9,7 @@
 
 1. [ ] Does your submission pass tests?
 2. [ ] Does mvn checkstyle:check pass ?
-3. [ ] Have you added your new test classes to an existing test suite?
+3. [ ] Have you added your new test classes to an existing test suite in alphabetical order?
 
 ### Changes to Existing Features:
 
diff --git a/README.md b/README.md
index bfe9c89d..4fcf6a0f 100644
--- a/README.md
+++ b/README.md
@@ -148,7 +148,7 @@ In addition to the standard connection parameters the driver supports a number o
 | socketFactory                 | String  | null    | Specify a socket factory for socket creation |
 | socketFactoryArg (deprecated) | String  | null    | Argument forwarded to constructor of SocketFactory class. |
 | autosave                      | String  | never   | Specifies what the driver should do if a query fails, possible values: always, never, conservative |
-| cleanupSavePoints             | Boolean | false   | In Autosave mode the driver sets a SAVEPOINT for every query. It is possible to exhaust the server shared buffers. Setting this to true will release each SAVEPOINT at the cost of an additional round trip. |
+| cleanupSavepoints             | Boolean | false   | In Autosave mode the driver sets a SAVEPOINT for every query. It is possible to exhaust the server shared buffers. Setting this to true will release each SAVEPOINT at the cost of an additional round trip. |
 | preferQueryMode               | String  | extended | Specifies which mode is used to execute queries to database, possible values: extended, extendedForPrepared, extendedCacheEverything, simple |
 | reWriteBatchedInserts         | Boolean | false  | Enable optimization to rewrite and collapse compatible INSERT statements that are batched. |
 
diff --git a/docs/documentation/92/query.md b/docs/documentation/92/query.md
index 88fa831e..89ee5e51 100644
--- a/docs/documentation/92/query.md
+++ b/docs/documentation/92/query.md
@@ -76,7 +76,7 @@ exhausted the next block of rows is retrieved by repositioning the cursor.
 * The `Connection` must not be in autocommit mode. The backend closes cursors at
 	the end of transactions, so in autocommit mode the backend will have
 	closed the cursor before anything can be fetched from it.
-*The `Statement` must be created with a `ResultSet` type of `ResultSet.TYPE_FORWARD_ONLY`.
+* The `Statement` must be created with a `ResultSet` type of `ResultSet.TYPE_FORWARD_ONLY`.
 	This is the default, so no code will need to be rewritten to take advantage
 	of this, but it also means that you cannot scroll backwards or otherwise
 	jump around in the `ResultSet`.
diff --git a/docs/documentation/93/query.md b/docs/documentation/93/query.md
index 88fa831e..89ee5e51 100644
--- a/docs/documentation/93/query.md
+++ b/docs/documentation/93/query.md
@@ -76,7 +76,7 @@ exhausted the next block of rows is retrieved by repositioning the cursor.
 * The `Connection` must not be in autocommit mode. The backend closes cursors at
 	the end of transactions, so in autocommit mode the backend will have
 	closed the cursor before anything can be fetched from it.
-*The `Statement` must be created with a `ResultSet` type of `ResultSet.TYPE_FORWARD_ONLY`.
+* The `Statement` must be created with a `ResultSet` type of `ResultSet.TYPE_FORWARD_ONLY`.
 	This is the default, so no code will need to be rewritten to take advantage
 	of this, but it also means that you cannot scroll backwards or otherwise
 	jump around in the `ResultSet`.
diff --git a/docs/documentation/94/query.md b/docs/documentation/94/query.md
index 88fa831e..89ee5e51 100644
--- a/docs/documentation/94/query.md
+++ b/docs/documentation/94/query.md
@@ -76,7 +76,7 @@ exhausted the next block of rows is retrieved by repositioning the cursor.
 * The `Connection` must not be in autocommit mode. The backend closes cursors at
 	the end of transactions, so in autocommit mode the backend will have
 	closed the cursor before anything can be fetched from it.
-*The `Statement` must be created with a `ResultSet` type of `ResultSet.TYPE_FORWARD_ONLY`.
+* The `Statement` must be created with a `ResultSet` type of `ResultSet.TYPE_FORWARD_ONLY`.
 	This is the default, so no code will need to be rewritten to take advantage
 	of this, but it also means that you cannot scroll backwards or otherwise
 	jump around in the `ResultSet`.
diff --git a/docs/documentation/head/connect.md b/docs/documentation/head/connect.md
index 20d817c6..b412da03 100644
--- a/docs/documentation/head/connect.md
+++ b/docs/documentation/head/connect.md
@@ -93,6 +93,8 @@ Connection conn = DriverManager.getConnection(url);
 	of it specifies a SSL connection. However, for compatibility with future
 	versions, the value "true" is preferred. For more information see [Chapter
 	4, *Using SSL*](ssl.html).
+
+    Setting up the certificates and keys for ssl connection can be tricky see [The test documentation](https://github.com/pgjdbc/pgjdbc/blob/master/certdir/README.md) for detailed examples.
  
 * **sslfactory** = String
 
@@ -120,20 +122,30 @@ Connection conn = DriverManager.getConnection(url);
 
 	Provide the full path for the certificate file. Defaults to /defaultdir/postgresql.crt
 
+    It can be a PEM encoded X509v3 certificate
+
 	*Note:* defaultdir is ${user.home}/.postgresql/ in *nix systems and %appdata%/postgresql/ on windows 
 
 * **sslkey** = String
 
 	Provide the full path for the key file. Defaults to /defaultdir/postgresql.pk8. 
 	
-	*Note:* The key file **must** be in [DER format](https://wiki.openssl.org/index.php/DER). A PEM key can be converted to DER format using the openssl command:
+	*Note:* The key file **must** be in [PKCS-8](https://en.wikipedia.org/wiki/PKCS_8) [DER format](https://wiki.openssl.org/index.php/DER). A PEM key can be converted to DER format using the openssl command:
 	
-	`openssl pkcs8 -topk8 -inform PEM -in my.key -outform DER -out my.key.der`
+	`openssl pkcs8 -topk8 -inform PEM -in my.key -outform DER -out my.key.der -v1 PBE-MD5-DES`
+
+    *Note:* The use of -v1 PBE-MD5-DES might be inadequate in environments where high level of security is needed and the key is not protected
+    by other means (e.g. access control of the OS), or the key file is transmitted in untrusted channels.
+    We are depending on the cryptography providers provided by the java runtime. The solution documented here is known to work at
+    the time of writing. If you have stricter security needs, please see https://stackoverflow.com/questions/58488774/configure-tomcat-hibernate-to-have-a-cryptographic-provider-supporting-1-2-840-1
+    for a discussion of the problem and information on choosing a better cipher suite.
 
 * **sslrootcert** = String
 
 	File name of the SSL root certificate. Defaults to defaultdir/root.crt
 
+    It can be a PEM encoded X509v3 certificate
+
 * **sslhostnameverifier** = String
 
 	Class name of hostname verifier. Defaults to using `org.postgresql.ssl.PGjdbcHostnameVerifier`
@@ -208,7 +220,7 @@ Connection conn = DriverManager.getConnection(url);
 
     The default is `never` 
 
-* **cleanupSavePoints** = boolean
+* **cleanupSavepoints** = boolean
 
     Determines if the SAVEPOINT created in autosave mode is released prior to the statement. This is
     done to avoid running out of shared buffers on the server in the case where 1000's of queries are
diff --git a/docs/documentation/head/prepare.md b/docs/documentation/head/prepare.md
index d57b5253..b7b55991 100644
--- a/docs/documentation/head/prepare.md
+++ b/docs/documentation/head/prepare.md
@@ -21,5 +21,5 @@ default, and you must set `tcpip_socket = true` in the `postgresql.conf` file.
 Once you have made sure the server is correctly listening for TCP/IP connections
 the next step is to verify that users are allowed to connect to the server. Client
 authentication is setup in `pg_hba.conf`. Refer to the main PostgreSQL™ documentation
-for details. The JDBC driver supports the `trust`, `ident`, `password`, `md5`, and
-`crypt` authentication methods.
\ No newline at end of file
+for details. The JDBC driver supports the `trust`, `ident`, `password`, `md5`, `gss` and
+`crypt` authentication methods.
diff --git a/docs/documentation/head/query.md b/docs/documentation/head/query.md
index d6fde7f6..2bd8214c 100644
--- a/docs/documentation/head/query.md
+++ b/docs/documentation/head/query.md
@@ -81,7 +81,7 @@ exhausted the next block of rows is retrieved by repositioning the cursor.
 * The `Connection` must not be in autocommit mode. The backend closes cursors at
 	the end of transactions, so in autocommit mode the backend will have
 	closed the cursor before anything can be fetched from it.
-*The `Statement` must be created with a `ResultSet` type of `ResultSet.TYPE_FORWARD_ONLY`.
+* The `Statement` must be created with a `ResultSet` type of `ResultSet.TYPE_FORWARD_ONLY`.
 	This is the default, so no code will need to be rewritten to take advantage
 	of this, but it also means that you cannot scroll backwards or otherwise
 	jump around in the `ResultSet`.
diff --git a/pgjdbc/src/main/java/org/postgresql/copy/PGCopyOutputStream.java b/pgjdbc/src/main/java/org/postgresql/copy/PGCopyOutputStream.java
index ce5459c8..520c645e 100644
--- a/pgjdbc/src/main/java/org/postgresql/copy/PGCopyOutputStream.java
+++ b/pgjdbc/src/main/java/org/postgresql/copy/PGCopyOutputStream.java
@@ -101,17 +101,20 @@ public class PGCopyOutputStream extends OutputStream implements CopyIn {
       return;
     }
 
-    try {
-      endCopy();
-    } catch (SQLException se) {
-      IOException ioe = new IOException("Ending write to copy failed.");
-      ioe.initCause(se);
-      throw ioe;
+    if (op.isActive()) {
+      try {
+        endCopy();
+      } catch (SQLException se) {
+        IOException ioe = new IOException("Ending write to copy failed.");
+        ioe.initCause(se);
+        throw ioe;
+      }
     }
     op = null;
   }
 
   public void flush() throws IOException {
+    checkClosed();
     try {
       op.writeToCopy(copyBuffer, 0, at);
       at = 0;
@@ -154,7 +157,7 @@ public class PGCopyOutputStream extends OutputStream implements CopyIn {
   }
 
   public boolean isActive() {
-    return op.isActive();
+    return op != null && op.isActive();
   }
 
   public void flushCopy() throws SQLException {
diff --git a/pgjdbc/src/main/java/org/postgresql/core/ResultHandler.java b/pgjdbc/src/main/java/org/postgresql/core/ResultHandler.java
index 4b9e01c4..cb25afdf 100644
--- a/pgjdbc/src/main/java/org/postgresql/core/ResultHandler.java
+++ b/pgjdbc/src/main/java/org/postgresql/core/ResultHandler.java
@@ -48,7 +48,7 @@ public interface ResultHandler {
    * @param insertOID for a single-row INSERT query, the OID of the newly inserted row; 0 if not
    *        available.
    */
-  void handleCommandStatus(String status, int updateCount, long insertOID);
+  void handleCommandStatus(String status, long updateCount, long insertOID);
 
   /**
    * Called when a warning is emitted.
diff --git a/pgjdbc/src/main/java/org/postgresql/core/ResultHandlerBase.java b/pgjdbc/src/main/java/org/postgresql/core/ResultHandlerBase.java
index d16da9e8..c6dd206d 100644
--- a/pgjdbc/src/main/java/org/postgresql/core/ResultHandlerBase.java
+++ b/pgjdbc/src/main/java/org/postgresql/core/ResultHandlerBase.java
@@ -30,7 +30,7 @@ public class ResultHandlerBase implements ResultHandler {
   }
 
   @Override
-  public void handleCommandStatus(String status, int updateCount, long insertOID) {
+  public void handleCommandStatus(String status, long updateCount, long insertOID) {
   }
 
   @Override
diff --git a/pgjdbc/src/main/java/org/postgresql/core/ResultHandlerDelegate.java b/pgjdbc/src/main/java/org/postgresql/core/ResultHandlerDelegate.java
index 34e96d18..926e59bc 100644
--- a/pgjdbc/src/main/java/org/postgresql/core/ResultHandlerDelegate.java
+++ b/pgjdbc/src/main/java/org/postgresql/core/ResultHandlerDelegate.java
@@ -31,7 +31,7 @@ public class ResultHandlerDelegate implements ResultHandler {
   }
 
   @Override
-  public void handleCommandStatus(String status, int updateCount, long insertOID) {
+  public void handleCommandStatus(String status, long updateCount, long insertOID) {
     if (delegate != null) {
       delegate.handleCommandStatus(status, updateCount, insertOID);
     }
diff --git a/pgjdbc/src/main/java/org/postgresql/core/v3/ConnectionFactoryImpl.java b/pgjdbc/src/main/java/org/postgresql/core/v3/ConnectionFactoryImpl.java
index e342cb25..662cc263 100644
--- a/pgjdbc/src/main/java/org/postgresql/core/v3/ConnectionFactoryImpl.java
+++ b/pgjdbc/src/main/java/org/postgresql/core/v3/ConnectionFactoryImpl.java
@@ -90,9 +90,6 @@ public class ConnectionFactoryImpl extends ConnectionFactory {
 
     PGStream newStream = new PGStream(socketFactory, hostSpec, connectTimeout);
 
-    // Construct and send an ssl startup packet if requested.
-    newStream = enableSSL(newStream, sslMode, info, connectTimeout);
-
     // Set the socket timeout if the "socketTimeout" property has been set.
     int socketTimeout = PGProperty.SOCKET_TIMEOUT.getInt(info);
     if (socketTimeout > 0) {
@@ -134,6 +131,9 @@ public class ConnectionFactoryImpl extends ConnectionFactory {
       LOGGER.log(Level.FINE, "Send Buffer Size is {0}", newStream.getSocket().getSendBufferSize());
     }
 
+    // Construct and send an ssl startup packet if requested.
+    newStream = enableSSL(newStream, sslMode, info, connectTimeout);
+
     List<String[]> paramList = getParametersForStartup(user, database, info);
     sendStartupPacket(newStream, paramList);
 
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 6c5cc1e6..13326c98 100644
--- a/pgjdbc/src/main/java/org/postgresql/core/v3/QueryExecutorImpl.java
+++ b/pgjdbc/src/main/java/org/postgresql/core/v3/QueryExecutorImpl.java
@@ -51,7 +51,6 @@ import java.net.SocketException;
 import java.net.SocketTimeoutException;
 import java.sql.SQLException;
 import java.sql.SQLWarning;
-import java.sql.Statement;
 import java.util.ArrayDeque;
 import java.util.ArrayList;
 import java.util.Collections;
@@ -519,6 +518,9 @@ public class QueryExecutorImpl extends QueryExecutorBase {
 
     try {
       handler.handleCompletion();
+      if (cleanupSavePoints) {
+        releaseSavePoint(autosave, flags);
+      }
     } catch (SQLException e) {
       rollbackIfRequired(autosave, e);
     }
@@ -558,7 +560,8 @@ public class QueryExecutorImpl extends QueryExecutorBase {
         }
       }
 
-      public void handleCommandStatus(String status, int updateCount, long insertOID) {
+      @Override
+      public void handleCommandStatus(String status, long updateCount, long insertOID) {
         if (!sawBegin) {
           sawBegin = true;
           if (!status.equals("BEGIN")) {
@@ -600,7 +603,8 @@ public class QueryExecutorImpl extends QueryExecutorBase {
       ResultHandler handler = new ResultHandlerBase() {
         private boolean sawBegin = false;
 
-        public void handleCommandStatus(String status, int updateCount, long insertOID) {
+        @Override
+        public void handleCommandStatus(String status, long updateCount, long insertOID) {
           if (!sawBegin) {
             if (!status.equals("BEGIN")) {
               handleError(
@@ -614,6 +618,7 @@ public class QueryExecutorImpl extends QueryExecutorBase {
           }
         }
 
+        @Override
         public void handleWarning(SQLWarning warning) {
           // we don't want to ignore warnings and it would be tricky
           // to chain them back to the connection, so since we don't
@@ -2408,7 +2413,8 @@ public class QueryExecutorImpl extends QueryExecutorBase {
     // (if the fetch returns no rows, we see just a CommandStatus..)
     final ResultHandler delegateHandler = handler;
     handler = new ResultHandlerDelegate(delegateHandler) {
-      public void handleCommandStatus(String status, int updateCount, long insertOID) {
+      @Override
+      public void handleCommandStatus(String status, long updateCount, long insertOID) {
         handleResultRows(portal.getQuery(), null, new ArrayList<byte[][]>(), null);
       }
     };
@@ -2539,16 +2545,7 @@ public class QueryExecutorImpl extends QueryExecutorBase {
     long oid = commandCompleteParser.getOid();
     long count = commandCompleteParser.getRows();
 
-    int countAsInt = 0;
-    if (count > Integer.MAX_VALUE) {
-      // If command status indicates that we've modified more than Integer.MAX_VALUE rows
-      // then we set the result count to reflect that we cannot provide the actual number
-      // due to the JDBC field being an int rather than a long.
-      countAsInt = Statement.SUCCESS_NO_INFO;
-    } else if (count > 0) {
-      countAsInt = (int) count;
-    }
-    handler.handleCommandStatus(status, countAsInt, oid);
+    handler.handleCommandStatus(status, count, oid);
   }
 
   private void receiveRFQ() throws IOException {
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 ab64f022..64e9f04b 100644
--- a/pgjdbc/src/main/java/org/postgresql/ds/common/BaseDataSource.java
+++ b/pgjdbc/src/main/java/org/postgresql/ds/common/BaseDataSource.java
@@ -23,6 +23,7 @@ import java.io.PrintWriter;
 import java.sql.Connection;
 import java.sql.DriverManager;
 import java.sql.SQLException;
+import java.util.Arrays;
 import java.util.Properties;
 import java.util.logging.Level;
 import java.util.logging.Logger;
@@ -39,16 +40,16 @@ import javax.sql.CommonDataSource;
  *
  * @author Aaron Mulder (ammulder@chariotsolutions.com)
  */
-public abstract class BaseDataSource implements CommonDataSource, Referenceable {
 
+public abstract class BaseDataSource implements CommonDataSource, Referenceable {
   private static final Logger LOGGER = Logger.getLogger(BaseDataSource.class.getName());
 
   // Standard properties, defined in the JDBC 2.0 Optional Package spec
-  private String serverName = "localhost";
+  private String[] serverNames = new String[] {"localhost"};
   private String databaseName = "";
   private String user;
   private String password;
-  private int portNumber = 0;
+  private int[] portNumbers = new int[] {0};
 
   // Map for all other properties
   private Properties properties = new Properties();
@@ -129,9 +130,20 @@ public abstract class BaseDataSource implements CommonDataSource, Referenceable
    * Gets the name of the host the PostgreSQL database is running on.
    *
    * @return name of the host the PostgreSQL database is running on
+   * @deprecated use {@link #getServerNames()}
    */
+  @Deprecated
   public String getServerName() {
-    return serverName;
+    return serverNames[0];
+  }
+
+  /**
+   * Gets the name of the host(s) the PostgreSQL database is running on.
+   *
+   * @return name of the host(s) the PostgreSQL database is running on
+   */
+  public String[] getServerNames() {
+    return serverNames;
   }
 
   /**
@@ -139,12 +151,30 @@ public abstract class BaseDataSource implements CommonDataSource, Referenceable
    * only affect future calls to getConnection. The default value is <tt>localhost</tt>.
    *
    * @param serverName name of the host the PostgreSQL database is running on
+   * @deprecated use {@link #setServerNames(String[])}
    */
+  @Deprecated
   public void setServerName(String serverName) {
-    if (serverName == null || serverName.equals("")) {
-      this.serverName = "localhost";
+    this.setServerNames(new String[] { serverName });
+  }
+
+  /**
+   * Sets the name of the host(s) the PostgreSQL database is running on. If this is changed, it will
+   * only affect future calls to getConnection. The default value is <tt>localhost</tt>.
+   *
+   * @param serverNames name of the host(s) the PostgreSQL database is running on
+   */
+  public void setServerNames(String[] serverNames) {
+    if (serverNames == null || serverNames.length == 0) {
+      this.serverNames = new String[] {"localhost"};
     } else {
-      this.serverName = serverName;
+      serverNames = Arrays.copyOf(serverNames, serverNames.length);
+      for (int i = 0; i < serverNames.length; i++) {
+        if (serverNames[i] == null || serverNames[i].equals("")) {
+          serverNames[i] = "localhost";
+        }
+      }
+      this.serverNames = serverNames;
     }
   }
 
@@ -221,20 +251,50 @@ public abstract class BaseDataSource implements CommonDataSource, Referenceable
    * Gets the port which the PostgreSQL server is listening on for TCP/IP connections.
    *
    * @return The port, or 0 if the default port will be used.
+   * @deprecated use {@link #getPortNumbers()}
    */
+  @Deprecated
   public int getPortNumber() {
-    return portNumber;
+    if (portNumbers == null || portNumbers.length == 0) {
+      return 0;
+    }
+    return portNumbers[0];
+  }
+
+  /**
+   * Gets the port(s) which the PostgreSQL server is listening on for TCP/IP connections.
+   *
+   * @return The port(s), or 0 if the default port will be used.
+   */
+  public int[] getPortNumbers() {
+    return portNumbers;
   }
 
   /**
-   * Gets the port which the PostgreSQL server is listening on for TCP/IP connections. Be sure the
+   * Sets the port which the PostgreSQL server is listening on for TCP/IP connections. Be sure the
    * -i flag is passed to postmaster when PostgreSQL is started. If this is not set, or set to 0,
    * the default port will be used.
    *
    * @param portNumber port which the PostgreSQL server is listening on for TCP/IP
+   * @deprecated use {@link #setPortNumbers(int[])}
    */
+  @Deprecated
   public void setPortNumber(int portNumber) {
-    this.portNumber = portNumber;
+    setPortNumbers(new int[] { portNumber });
+  }
+
+  /**
+   * Sets the port(s) which the PostgreSQL server is listening on for TCP/IP connections. Be sure the
+   * -i flag is passed to postmaster when PostgreSQL is started. If this is not set, or set to 0,
+   * the default port will be used.
+   *
+   * @param portNumbers port(s) which the PostgreSQL server is listening on for TCP/IP
+   */
+  public void setPortNumbers(int[] portNumbers) {
+    if (portNumbers == null || portNumbers.length == 0) {
+      portNumbers = new int[] { 0 };
+    }
+    this.portNumbers = Arrays.copyOf(portNumbers, portNumbers.length);
   }
 
   /**
@@ -1085,9 +1145,14 @@ public abstract class BaseDataSource implements CommonDataSource, Referenceable
   public String getUrl() {
     StringBuilder url = new StringBuilder(100);
     url.append("jdbc:postgresql://");
-    url.append(serverName);
-    if (portNumber != 0) {
-      url.append(":").append(portNumber);
+    for (int i = 0; i < serverNames.length; i++) {
+      if (i > 0) {
+        url.append(",");
+      }
+      url.append(serverNames[i]);
+      if (portNumbers != null && portNumbers.length >= i && portNumbers[i] != 0) {
+        url.append(":").append(portNumbers[i]);
+      }
     }
     url.append("/").append(URLCoder.encode(databaseName));
 
@@ -1179,23 +1244,28 @@ public abstract class BaseDataSource implements CommonDataSource, Referenceable
     }
     switch (property) {
       case PG_HOST:
-        serverName = value;
+        setServerNames(value.split(","));
         break;
       case PG_PORT:
-        try {
-          portNumber = Integer.parseInt(value);
-        } catch (NumberFormatException e) {
-          portNumber = 0;
+        String[] ps = value.split(",");
+        int[] ports = new int[ps.length];
+        for (int i = 0 ; i < ps.length; i++) {
+          try {
+            ports[i] = Integer.parseInt(ps[i]);
+          } catch (NumberFormatException e) {
+            ports[i] = 0;
+          }
         }
+        setPortNumbers(ports);
         break;
       case PG_DBNAME:
-        databaseName = value;
+        setDatabaseName(value);
         break;
       case USER:
-        user = value;
+        setUser(value);
         break;
       case PASSWORD:
-        password = value;
+        setPassword(value);
         break;
       default:
         properties.setProperty(property.getName(), value);
@@ -1213,10 +1283,25 @@ public abstract class BaseDataSource implements CommonDataSource, Referenceable
 
   public Reference getReference() throws NamingException {
     Reference ref = createReference();
-    ref.add(new StringRefAddr("serverName", serverName));
-    if (portNumber != 0) {
-      ref.add(new StringRefAddr("portNumber", Integer.toString(portNumber)));
+    StringBuilder serverString = new StringBuilder();
+    for (int i = 0; i < serverNames.length; i++) {
+      if (i > 0) {
+        serverString.append(",");
+      }
+      String serverName = serverNames[i];
+      serverString.append(serverName);
+    }
+    ref.add(new StringRefAddr("serverName", serverString.toString()));
+
+    StringBuilder portString = new StringBuilder();
+    for (int i = 0; i < portNumbers.length; i++) {
+      if (i > 0) {
+        portString.append(",");
+      }
+      int p = portNumbers[i];
+      portString.append(Integer.toString(p));
     }
+    ref.add(new StringRefAddr("portNumber", portString.toString()));
     ref.add(new StringRefAddr("databaseName", databaseName));
     if (user != null) {
       ref.add(new StringRefAddr("user", user));
@@ -1236,11 +1321,22 @@ public abstract class BaseDataSource implements CommonDataSource, Referenceable
 
   public void setFromReference(Reference ref) {
     databaseName = getReferenceProperty(ref, "databaseName");
-    String port = getReferenceProperty(ref, "portNumber");
-    if (port != null) {
-      portNumber = Integer.parseInt(port);
+    String portNumberString = getReferenceProperty(ref, "portNumber");
+    if (portNumberString != null) {
+      String[] ps = portNumberString.split(",");
+      int[] ports = new int[ps.length];
+      for (int i = 0; i < ps.length; i++) {
+        try {
+          ports[i] = Integer.parseInt(ps[i]);
+        } catch (NumberFormatException e) {
+          ports[i] = 0;
+        }
+      }
+      setPortNumbers(ports);
+    } else {
+      setPortNumbers(null);
     }
-    serverName = getReferenceProperty(ref, "serverName");
+    setServerNames(getReferenceProperty(ref, "serverName").split(","));
 
     for (PGProperty property : PGProperty.values()) {
       setProperty(property, getReferenceProperty(ref, property.getName()));
@@ -1256,21 +1352,21 @@ public abstract class BaseDataSource implements CommonDataSource, Referenceable
   }
 
   protected void writeBaseObject(ObjectOutputStream out) throws IOException {
-    out.writeObject(serverName);
+    out.writeObject(serverNames);
     out.writeObject(databaseName);
     out.writeObject(user);
     out.writeObject(password);
-    out.writeInt(portNumber);
+    out.writeObject(portNumbers);
 
     out.writeObject(properties);
   }
 
   protected void readBaseObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
-    serverName = (String) in.readObject();
+    serverNames = (String[]) in.readObject();
     databaseName = (String) in.readObject();
     user = (String) in.readObject();
     password = (String) in.readObject();
-    portNumber = in.readInt();
+    portNumbers = (int[]) in.readObject();
 
     properties = (Properties) in.readObject();
   }
diff --git a/pgjdbc/src/main/java/org/postgresql/jdbc/BatchResultHandler.java b/pgjdbc/src/main/java/org/postgresql/jdbc/BatchResultHandler.java
index 5cf84ff3..b88807c7 100644
--- a/pgjdbc/src/main/java/org/postgresql/jdbc/BatchResultHandler.java
+++ b/pgjdbc/src/main/java/org/postgresql/jdbc/BatchResultHandler.java
@@ -28,16 +28,17 @@ import java.util.List;
  * Internal class, it is not a part of public API.
  */
 public class BatchResultHandler extends ResultHandlerBase {
-  private PgStatement pgStatement;
+
+  private final PgStatement pgStatement;
   private int resultIndex = 0;
 
   private final Query[] queries;
-  private final int[] updateCounts;
+  private final long[] longUpdateCounts;
   private final ParameterList[] parameterLists;
   private final boolean expectGeneratedKeys;
   private PgResultSet generatedKeys;
   private int committedRows; // 0 means no rows committed. 1 means row 0 was committed, and so on
-  private List<List<byte[][]>> allGeneratedRows;
+  private final List<List<byte[][]>> allGeneratedRows;
   private List<byte[][]> latestGeneratedRows;
   private PgResultSet latestGeneratedKeysRs;
 
@@ -46,11 +47,12 @@ public class BatchResultHandler extends ResultHandlerBase {
     this.pgStatement = pgStatement;
     this.queries = queries;
     this.parameterLists = parameterLists;
-    this.updateCounts = new int[queries.length];
+    this.longUpdateCounts = new long[queries.length];
     this.expectGeneratedKeys = expectGeneratedKeys;
     this.allGeneratedRows = !expectGeneratedKeys ? null : new ArrayList<List<byte[][]>>();
   }
 
+  @Override
   public void handleResultRows(Query fromQuery, Field[] fields, List<byte[][]> tuples,
       ResultCursor cursor) {
     // If SELECT, then handleCommandStatus call would just be missing
@@ -63,9 +65,8 @@ public class BatchResultHandler extends ResultHandlerBase {
       try {
         // If SELECT, the resulting ResultSet is not valid
         // Thus it is up to handleCommandStatus to decide if resultSet is good enough
-        latestGeneratedKeysRs =
-            (PgResultSet) pgStatement.createResultSet(fromQuery, fields,
-                new ArrayList<byte[][]>(), cursor);
+        latestGeneratedKeysRs = (PgResultSet) pgStatement.createResultSet(fromQuery, fields,
+            new ArrayList<byte[][]>(), cursor);
       } catch (SQLException e) {
         handleError(e);
       }
@@ -73,7 +74,8 @@ public class BatchResultHandler extends ResultHandlerBase {
     latestGeneratedRows = tuples;
   }
 
-  public void handleCommandStatus(String status, int updateCount, long insertOID) {
+  @Override
+  public void handleCommandStatus(String status, long updateCount, long insertOID) {
     if (latestGeneratedRows != null) {
       // We have DML. Decrease resultIndex that was just increased in handleResultRows
       resultIndex--;
@@ -95,7 +97,7 @@ public class BatchResultHandler extends ResultHandlerBase {
     }
     latestGeneratedKeysRs = null;
 
-    updateCounts[resultIndex++] = updateCount;
+    longUpdateCounts[resultIndex++] = updateCount;
   }
 
   private boolean isAutoCommit() {
@@ -125,6 +127,7 @@ public class BatchResultHandler extends ResultHandlerBase {
     allGeneratedRows.clear();
   }
 
+  @Override
   public void handleWarning(SQLWarning warning) {
     pgStatement.addWarning(warning);
   }
@@ -132,7 +135,7 @@ public class BatchResultHandler extends ResultHandlerBase {
   @Override
   public void handleError(SQLException newError) {
     if (getException() == null) {
-      Arrays.fill(updateCounts, committedRows, updateCounts.length, Statement.EXECUTE_FAILED);
+      Arrays.fill(longUpdateCounts, committedRows, longUpdateCounts.length, Statement.EXECUTE_FAILED);
       if (allGeneratedRows != null) {
         allGeneratedRows.clear();
       }
@@ -142,11 +145,19 @@ public class BatchResultHandler extends ResultHandlerBase {
         queryString = queries[resultIndex].toString(parameterLists[resultIndex]);
       }
 
-      BatchUpdateException batchException = new BatchUpdateException(
+      BatchUpdateException batchException;
+      //#if mvn.project.property.postgresql.jdbc.spec >= "JDBC4.2"
+      batchException = new BatchUpdateException(
+          GT.tr("Batch entry {0} {1} was aborted: {2}  Call getNextException to see other errors in the batch.",
+              resultIndex, queryString, newError.getMessage()),
+          newError.getSQLState(), 0, uncompressLongUpdateCount(), newError);
+      //#else
+      batchException = new BatchUpdateException(
           GT.tr("Batch entry {0} {1} was aborted: {2}  Call getNextException to see other errors in the batch.",
               resultIndex, queryString, newError.getMessage()),
-          newError.getSQLState(), uncompressUpdateCount());
-      batchException.initCause(newError);
+          newError.getSQLState(), 0, uncompressUpdateCount(), newError);
+      //#endif
+
       super.handleError(batchException);
     }
     resultIndex++;
@@ -154,18 +165,30 @@ public class BatchResultHandler extends ResultHandlerBase {
     super.handleError(newError);
   }
 
+  @Override
   public void handleCompletion() throws SQLException {
     updateGeneratedKeys();
     SQLException batchException = getException();
     if (batchException != null) {
       if (isAutoCommit()) {
         // Re-create batch exception since rows after exception might indeed succeed.
-        BatchUpdateException newException = new BatchUpdateException(
+        BatchUpdateException newException;
+        //#if mvn.project.property.postgresql.jdbc.spec >= "JDBC4.2"
+        newException = new BatchUpdateException(
             batchException.getMessage(),
-            batchException.getSQLState(),
-            uncompressUpdateCount()
+            batchException.getSQLState(), 0,
+            uncompressLongUpdateCount(),
+            batchException.getCause()
         );
-        newException.initCause(batchException.getCause());
+        //#else
+        newException = new BatchUpdateException(
+            batchException.getMessage(),
+            batchException.getSQLState(), 0,
+            uncompressUpdateCount(),
+            batchException.getCause()
+        );
+        //#endif
+
         SQLException next = batchException.getNextException();
         if (next != null) {
           newException.setNextException(next);
@@ -181,8 +204,21 @@ public class BatchResultHandler extends ResultHandlerBase {
   }
 
   private int[] uncompressUpdateCount() {
+    long[] original = uncompressLongUpdateCount();
+    int[] copy = new int[original.length];
+    for (int i = 0; i < original.length; i++) {
+      copy[i] = original[i] > Integer.MAX_VALUE ? Statement.SUCCESS_NO_INFO : (int) original[i];
+    }
+    return copy;
+  }
+
+  public int[] getUpdateCount() {
+    return uncompressUpdateCount();
+  }
+
+  private long[] uncompressLongUpdateCount() {
     if (!(queries[0] instanceof BatchedQuery)) {
-      return updateCounts;
+      return longUpdateCounts;
     }
     int totalRows = 0;
     boolean hasRewrites = false;
@@ -192,7 +228,7 @@ public class BatchResultHandler extends ResultHandlerBase {
       hasRewrites |= batchSize > 1;
     }
     if (!hasRewrites) {
-      return updateCounts;
+      return longUpdateCounts;
     }
 
     /* In this situation there is a batch that has been rewritten. Substitute
@@ -200,12 +236,12 @@ public class BatchResultHandler extends ResultHandlerBase {
      * indicate successful completion for each row the driver client added
      * to the batch.
      */
-    int[] newUpdateCounts = new int[totalRows];
+    long[] newUpdateCounts = new long[totalRows];
     int offset = 0;
     for (int i = 0; i < queries.length; i++) {
       Query query = queries[i];
       int batchSize = query.getBatchSize();
-      int superBatchResult = updateCounts[i];
+      long superBatchResult = longUpdateCounts[i];
       if (batchSize == 1) {
         newUpdateCounts[offset++] = superBatchResult;
         continue;
@@ -221,7 +257,8 @@ public class BatchResultHandler extends ResultHandlerBase {
     return newUpdateCounts;
   }
 
-  public int[] getUpdateCount() {
-    return uncompressUpdateCount();
+  public long[] getLargeUpdateCount() {
+    return uncompressLongUpdateCount();
   }
+
 }
diff --git a/pgjdbc/src/main/java/org/postgresql/jdbc/PgDatabaseMetaData.java b/pgjdbc/src/main/java/org/postgresql/jdbc/PgDatabaseMetaData.java
index 56d13506..ef3ac750 100644
--- a/pgjdbc/src/main/java/org/postgresql/jdbc/PgDatabaseMetaData.java
+++ b/pgjdbc/src/main/java/org/postgresql/jdbc/PgDatabaseMetaData.java
@@ -9,11 +9,13 @@ import org.postgresql.core.BaseStatement;
 import org.postgresql.core.Field;
 import org.postgresql.core.Oid;
 import org.postgresql.core.ServerVersion;
+import org.postgresql.util.ByteConverter;
 import org.postgresql.util.GT;
 import org.postgresql.util.JdbcBlackHole;
 import org.postgresql.util.PSQLException;
 import org.postgresql.util.PSQLState;
 
+import java.math.BigInteger;
 import java.sql.Array;
 import java.sql.Connection;
 import java.sql.DatabaseMetaData;
@@ -24,6 +26,9 @@ import java.sql.Statement;
 import java.sql.Types;
 import java.util.ArrayList;
 import java.util.Arrays;
+
+import java.util.Collections;
+import java.util.Comparator;
 import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
@@ -2247,7 +2252,7 @@ public class PgDatabaseMetaData implements DatabaseMetaData {
               connection.encodeString(Integer.toString(java.sql.DatabaseMetaData.typeSearchable));
 
     while (rs.next()) {
-      byte[][] tuple = new byte[18][];
+      byte[][] tuple = new byte[19][];
       String typname = rs.getString(1);
       int typeOid = (int) rs.getLong(2);
 
@@ -2255,6 +2260,10 @@ public class PgDatabaseMetaData implements DatabaseMetaData {
       int sqlType = connection.getTypeInfo().getSQLType(typname);
       tuple[1] =
           connection.encodeString(Integer.toString(sqlType));
+
+      /* this is just for sorting below, the result set never sees this */
+      tuple[18] = BigInteger.valueOf(sqlType).toByteArray();
+
       tuple[2] = connection
           .encodeString(Integer.toString(connection.getTypeInfo().getMaximumPrecision(typeOid)));
 
@@ -2300,6 +2309,14 @@ public class PgDatabaseMetaData implements DatabaseMetaData {
     rs.close();
     stmt.close();
 
+    Collections.sort(v, new Comparator<byte[][]>() {
+      @Override
+      public int compare(byte[][] o1, byte[][] o2) {
+        int i1 = ByteConverter.bytesToInt(o1[18]);
+        int i2 = ByteConverter.bytesToInt(o2[18]);
+        return (i1 < i2) ? -1 : ((i1 == i2) ? 0 : 1);
+      }
+    });
     return ((BaseStatement) createMetaDataStatement()).createDriverResultSet(f, v);
   }
 
diff --git a/pgjdbc/src/main/java/org/postgresql/jdbc/PgPreparedStatement.java b/pgjdbc/src/main/java/org/postgresql/jdbc/PgPreparedStatement.java
index 8e66bc77..3c5ee626 100644
--- a/pgjdbc/src/main/java/org/postgresql/jdbc/PgPreparedStatement.java
+++ b/pgjdbc/src/main/java/org/postgresql/jdbc/PgPreparedStatement.java
@@ -122,9 +122,18 @@ class PgPreparedStatement extends PgStatement implements PreparedStatement {
   @Override
   public int executeUpdate() throws SQLException {
     executeWithFlags(QueryExecutor.QUERY_NO_RESULTS);
+    checkNoResultUpdate();
+    return getUpdateCount();
+  }
 
-    return getNoResultUpdateCount();
+  //#if mvn.project.property.postgresql.jdbc.spec >= "JDBC4.2"
+  @Override
+  public long executeLargeUpdate() throws SQLException {
+    executeWithFlags(QueryExecutor.QUERY_NO_RESULTS);
+    checkNoResultUpdate();
+    return getLargeUpdateCount();
   }
+  //#endif
 
   @Override
   public boolean execute(String sql) throws SQLException {
diff --git a/pgjdbc/src/main/java/org/postgresql/jdbc/PgResultSet.java b/pgjdbc/src/main/java/org/postgresql/jdbc/PgResultSet.java
index 107e9e64..9cc490a0 100644
--- a/pgjdbc/src/main/java/org/postgresql/jdbc/PgResultSet.java
+++ b/pgjdbc/src/main/java/org/postgresql/jdbc/PgResultSet.java
@@ -1744,17 +1744,20 @@ public class PgResultSet implements ResultSet, org.postgresql.PGRefCursorResultS
 
   public class CursorResultHandler extends ResultHandlerBase {
 
+    @Override
     public void handleResultRows(Query fromQuery, Field[] fields, List<byte[][]> tuples,
         ResultCursor cursor) {
       PgResultSet.this.rows = tuples;
       PgResultSet.this.cursor = cursor;
     }
 
-    public void handleCommandStatus(String status, int updateCount, long insertOID) {
+    @Override
+    public void handleCommandStatus(String status, long updateCount, long insertOID) {
       handleError(new PSQLException(GT.tr("Unexpected command status: {0}.", status),
           PSQLState.PROTOCOL_VIOLATION));
     }
 
+    @Override
     public void handleCompletion() throws SQLException {
       SQLWarning warning = getWarning();
       if (warning != null) {
diff --git a/pgjdbc/src/main/java/org/postgresql/jdbc/PgStatement.java b/pgjdbc/src/main/java/org/postgresql/jdbc/PgStatement.java
index 5dcf4c4f..3940e7a2 100644
--- a/pgjdbc/src/main/java/org/postgresql/jdbc/PgStatement.java
+++ b/pgjdbc/src/main/java/org/postgresql/jdbc/PgStatement.java
@@ -209,7 +209,7 @@ public class PgStatement implements Statement, BaseStatement {
     }
 
     @Override
-    public void handleCommandStatus(String status, int updateCount, long insertOID) {
+    public void handleCommandStatus(String status, long updateCount, long insertOID) {
       append(new ResultWrapper(updateCount, insertOID));
     }
 
@@ -244,10 +244,11 @@ public class PgStatement implements Statement, BaseStatement {
   @Override
   public int executeUpdate(String sql) throws SQLException {
     executeWithFlags(sql, QueryExecutor.QUERY_NO_RESULTS);
-    return getNoResultUpdateCount();
+    checkNoResultUpdate();
+    return getUpdateCount();
   }
 
-  protected int getNoResultUpdateCount() throws SQLException {
+  protected final void checkNoResultUpdate() throws SQLException {
     synchronized (this) {
       checkClosed();
       ResultWrapper iter = result;
@@ -255,12 +256,9 @@ public class PgStatement implements Statement, BaseStatement {
         if (iter.getResultSet() != null) {
           throw new PSQLException(GT.tr("A result was returned when none was expected."),
               PSQLState.TOO_MANY_RESULTS);
-
         }
         iter = iter.getNext();
       }
-
-      return getUpdateCount();
     }
   }
 
@@ -470,6 +468,7 @@ public class PgStatement implements Statement, BaseStatement {
 
   private volatile boolean isClosed = false;
 
+  @Override
   public int getUpdateCount() throws SQLException {
     synchronized (this) {
       checkClosed();
@@ -477,7 +476,8 @@ public class PgStatement implements Statement, BaseStatement {
         return -1;
       }
 
-      return result.getUpdateCount();
+      long count = result.getUpdateCount();
+      return count > Integer.MAX_VALUE ? Statement.SUCCESS_NO_INFO : (int) count;
     }
   }
 
@@ -739,26 +739,17 @@ public class PgStatement implements Statement, BaseStatement {
         wantsGeneratedKeysAlways);
   }
 
-  public int[] executeBatch() throws SQLException {
-    checkClosed();
-
-    closeForNextExecution();
-
-    if (batchStatements == null || batchStatements.isEmpty()) {
-      return new int[0];
-    }
-
+  private BatchResultHandler internalExecuteBatch() throws SQLException {
     // Construct query/parameter arrays.
     transformQueriesAndParameters();
     // Empty arrays should be passed to toArray
     // see http://shipilev.net/blog/2016/arrays-wisdom-ancients/
     Query[] queries = batchStatements.toArray(new Query[0]);
-    ParameterList[] parameterLists =
-        batchParameters.toArray(new ParameterList[0]);
+    ParameterList[] parameterLists = batchParameters.toArray(new ParameterList[0]);
     batchStatements.clear();
     batchParameters.clear();
 
-    int flags = 0;
+    int flags;
 
     // Force a Describe before any execution? We need to do this if we're going
     // to send anything dependent on the Describe results, e.g. binary parameters.
@@ -862,8 +853,18 @@ public class PgStatement implements Statement, BaseStatement {
         }
       }
     }
+    return handler;
+  }
+
+  public int[] executeBatch() throws SQLException {
+    checkClosed();
+    closeForNextExecution();
+
+    if (batchStatements == null || batchStatements.isEmpty()) {
+      return new int[0];
+    }
 
-    return handler.getUpdateCount();
+    return internalExecuteBatch().getUpdateCount();
   }
 
   public void cancel() throws SQLException {
@@ -1012,8 +1013,17 @@ public class PgStatement implements Statement, BaseStatement {
     return forceBinaryTransfers;
   }
 
+  //#if mvn.project.property.postgresql.jdbc.spec >= "JDBC4.2"
+  @Override
   public long getLargeUpdateCount() throws SQLException {
-    throw Driver.notImplemented(this.getClass(), "getLargeUpdateCount");
+    synchronized (this) {
+      checkClosed();
+      if (result == null || result.getResultSet() != null) {
+        return -1;
+      }
+
+      return result.getUpdateCount();
+    }
   }
 
   public void setLargeMaxRows(long max) throws SQLException {
@@ -1024,29 +1034,57 @@ public class PgStatement implements Statement, BaseStatement {
     throw Driver.notImplemented(this.getClass(), "getLargeMaxRows");
   }
 
+  @Override
   public long[] executeLargeBatch() throws SQLException {
-    throw Driver.notImplemented(this.getClass(), "executeLargeBatch");
+    checkClosed();
+    closeForNextExecution();
+
+    if (batchStatements == null || batchStatements.isEmpty()) {
+      return new long[0];
+    }
+
+    return internalExecuteBatch().getLargeUpdateCount();
   }
 
+  @Override
   public long executeLargeUpdate(String sql) throws SQLException {
-    throw Driver.notImplemented(this.getClass(), "executeLargeUpdate");
+    executeWithFlags(sql, QueryExecutor.QUERY_NO_RESULTS);
+    checkNoResultUpdate();
+    return getLargeUpdateCount();
   }
 
+  @Override
   public long executeLargeUpdate(String sql, int autoGeneratedKeys) throws SQLException {
-    throw Driver.notImplemented(this.getClass(), "executeLargeUpdate");
+    if (autoGeneratedKeys == Statement.NO_GENERATED_KEYS) {
+      return executeLargeUpdate(sql);
+    }
+
+    return executeLargeUpdate(sql, (String[]) null);
   }
 
+  @Override
   public long executeLargeUpdate(String sql, int[] columnIndexes) throws SQLException {
-    throw Driver.notImplemented(this.getClass(), "executeLargeUpdate");
+    if (columnIndexes == null || columnIndexes.length == 0) {
+      return executeLargeUpdate(sql);
+    }
+
+    throw new PSQLException(GT.tr("Returning autogenerated keys by column index is not supported."),
+        PSQLState.NOT_IMPLEMENTED);
   }
 
+  @Override
   public long executeLargeUpdate(String sql, String[] columnNames) throws SQLException {
-    throw Driver.notImplemented(this.getClass(), "executeLargeUpdate");
-  }
+    if (columnNames != null && columnNames.length == 0) {
+      return executeLargeUpdate(sql);
+    }
 
-  public long executeLargeUpdate() throws SQLException {
-    throw Driver.notImplemented(this.getClass(), "executeLargeUpdate");
+    wantsGeneratedKeysOnce = true;
+    if (!executeCachedSql(sql, 0, columnNames)) {
+      // no resultset returned. What's a pity!
+    }
+    return getLargeUpdateCount();
   }
+  //#endif
 
   public boolean isClosed() throws SQLException {
     return isClosed;
diff --git a/pgjdbc/src/main/java/org/postgresql/jdbc/ResultWrapper.java b/pgjdbc/src/main/java/org/postgresql/jdbc/ResultWrapper.java
index 3fb00345..df79ae78 100644
--- a/pgjdbc/src/main/java/org/postgresql/jdbc/ResultWrapper.java
+++ b/pgjdbc/src/main/java/org/postgresql/jdbc/ResultWrapper.java
@@ -21,7 +21,7 @@ public class ResultWrapper {
     this.insertOID = -1;
   }
 
-  public ResultWrapper(int updateCount, long insertOID) {
+  public ResultWrapper(long updateCount, long insertOID) {
     this.rs = null;
     this.updateCount = updateCount;
     this.insertOID = insertOID;
@@ -31,7 +31,7 @@ public class ResultWrapper {
     return rs;
   }
 
-  public int getUpdateCount() {
+  public long getUpdateCount() {
     return updateCount;
   }
 
@@ -53,7 +53,7 @@ public class ResultWrapper {
   }
 
   private final ResultSet rs;
-  private final int updateCount;
+  private final long updateCount;
   private final long insertOID;
   private ResultWrapper next;
 }
diff --git a/pgjdbc/src/main/java/org/postgresql/util/ByteConverter.java b/pgjdbc/src/main/java/org/postgresql/util/ByteConverter.java
index 5464236c..f1188977 100644
--- a/pgjdbc/src/main/java/org/postgresql/util/ByteConverter.java
+++ b/pgjdbc/src/main/java/org/postgresql/util/ByteConverter.java
@@ -16,6 +16,25 @@ public class ByteConverter {
     // prevent instantiation of static helper class
   }
 
+  /**
+   * Convert a variable length array of bytes to an integer
+   * @param bytes array of bytes that can be decoded as an integer
+   * @return integer
+   */
+  public static int bytesToInt(byte []bytes) {
+    if ( bytes.length == 1 ) {
+      return (int)bytes[0];
+    }
+    if ( bytes.length == 2 ) {
+      return int2(bytes, 0);
+    }
+    if ( bytes.length == 4 ) {
+      return int4(bytes, 0);
+    } else {
+      throw new IllegalArgumentException("Argument bytes is empty");
+    }
+  }
+
   /**
    * Parses a long value from the byte array.
    *


Test patch:
diff --git a/pgjdbc/src/test/java/org/postgresql/test/TestUtil.java b/pgjdbc/src/test/java/org/postgresql/test/TestUtil.java
index 211d01eb..b66c0fae 100644
--- a/pgjdbc/src/test/java/org/postgresql/test/TestUtil.java
+++ b/pgjdbc/src/test/java/org/postgresql/test/TestUtil.java
@@ -6,7 +6,9 @@
 package org.postgresql.test;
 
 import org.postgresql.PGProperty;
+import org.postgresql.core.BaseConnection;
 import org.postgresql.core.ServerVersion;
+import org.postgresql.core.TransactionState;
 import org.postgresql.core.Version;
 import org.postgresql.jdbc.PgConnection;
 
@@ -437,6 +439,26 @@ public class TestUtil {
     }
   }
 
+  /*
+   * Helper - creates a unlogged table for use by a test.
+   * Unlogged tables works from PostgreSQL 9.1+
+   */
+  public static void createUnloggedTable(Connection con, String table, String columns)
+      throws SQLException {
+    Statement st = con.createStatement();
+    try {
+      // Drop the table
+      dropTable(con, table);
+
+      String unlogged = haveMinimumServerVersion(con, ServerVersion.v9_1) ? "UNLOGGED" : "";
+
+      // Now create the table
+      st.executeUpdate("CREATE " + unlogged + " TABLE " + table + " (" + columns + ")");
+    } finally {
+      closeQuietly(st);
+    }
+  }
+
   /**
    * Helper creates an enum type.
    *
@@ -601,6 +623,11 @@ public class TestUtil {
     }
   }
 
+  public static void assertTransactionState(String message, Connection con, TransactionState expected) {
+    TransactionState actual = TestUtil.getTransactionState(con);
+    Assert.assertEquals(message, expected, actual);
+  }
+
   /*
    * Helper - generates INSERT SQL - very simple
    */
@@ -897,6 +924,73 @@ public class TestUtil {
     }
   }
 
+  /**
+   * Execute a SQL query with a given connection and return whether any rows were
+   * returned. No column data is fetched.
+   */
+  public static boolean executeQuery(Connection conn, String sql) throws SQLException {
+    Statement stmt = conn.createStatement();
+    ResultSet rs = stmt.executeQuery(sql);
+    boolean hasNext = rs.next();
+    rs.close();
+    stmt.close();
+    return hasNext;
+  }
+
+  /**
+   * Retrieve the backend process id for a given connection.
+   */
+  public static int getBackendPid(Connection conn) throws SQLException {
+    Statement stmt = conn.createStatement();
+    ResultSet rs = stmt.executeQuery("SELECT pg_backend_pid()");
+    rs.next();
+    int pid = rs.getInt(1);
+    rs.close();
+    stmt.close();
+    return pid;
+  }
+
+  /**
+   * Executed pg_terminate_backend(...) to terminate the server process for
+   * a given process id with the given connection.
+   */
+  public static boolean terminateBackend(Connection conn, int backendPid) throws SQLException {
+    PreparedStatement stmt = conn.prepareStatement("SELECT pg_terminate_backend(?)");
+    stmt.setInt(1, backendPid);
+    ResultSet rs = stmt.executeQuery();
+    rs.next();
+    boolean wasTerminated = rs.getBoolean(1);
+    rs.close();
+    stmt.close();
+    return wasTerminated;
+  }
+
+  /**
+   * Create a new connection using the default test credentials and use it to
+   * attempt to terminate the specified backend process.
+   */
+  public static boolean terminateBackend(int backendPid) throws SQLException {
+    Connection conn = TestUtil.openPrivilegedDB();
+    try {
+      return terminateBackend(conn, backendPid);
+    } finally {
+      conn.close();
+    }
+  }
+
+  /**
+   * Retrieve the given connection backend process id, then create a new connection
+   * using the default test credentials and attempt to terminate the process.
+   */
+  public static boolean terminateBackend(Connection conn) throws SQLException {
+    int pid = getBackendPid(conn);
+    return terminateBackend(pid);
+  }
+
+  public static TransactionState getTransactionState(Connection conn) {
+    return ((BaseConnection) conn).getTransactionState();
+  }
+
   private static void waitStopReplicationSlot(Connection connection, String slotName)
       throws InterruptedException, TimeoutException, SQLException {
     long startWaitTime = System.currentTimeMillis();
diff --git a/pgjdbc/src/test/java/org/postgresql/test/extensions/ExtensionsTestSuite.java b/pgjdbc/src/test/java/org/postgresql/test/extensions/ExtensionsTestSuite.java
index f0c5aed7..0da675e3 100644
--- a/pgjdbc/src/test/java/org/postgresql/test/extensions/ExtensionsTestSuite.java
+++ b/pgjdbc/src/test/java/org/postgresql/test/extensions/ExtensionsTestSuite.java
@@ -12,7 +12,9 @@ import org.junit.runners.Suite;
  * Executes all known tests for PostgreSQL extensions supported by JDBC driver
  */
 @RunWith(Suite.class)
-@Suite.SuiteClasses(HStoreTest.class)
+@Suite.SuiteClasses({
+    HStoreTest.class,
+})
 public class ExtensionsTestSuite {
 }
 
diff --git a/pgjdbc/src/test/java/org/postgresql/test/hostchooser/MultiHostTestSuite.java b/pgjdbc/src/test/java/org/postgresql/test/hostchooser/MultiHostTestSuite.java
index 984577e5..2f19cd40 100644
--- a/pgjdbc/src/test/java/org/postgresql/test/hostchooser/MultiHostTestSuite.java
+++ b/pgjdbc/src/test/java/org/postgresql/test/hostchooser/MultiHostTestSuite.java
@@ -12,6 +12,8 @@ import org.junit.runners.Suite;
  * Executes multi host tests (aka master/slave connectivity selection).
  */
 @RunWith(Suite.class)
-@Suite.SuiteClasses(MultiHostsConnectionTest.class)
+@Suite.SuiteClasses({
+    MultiHostsConnectionTest.class,
+})
 public class MultiHostTestSuite {
 }
diff --git a/pgjdbc/src/test/java/org/postgresql/test/jdbc2/CopyTest.java b/pgjdbc/src/test/java/org/postgresql/test/jdbc2/CopyTest.java
index 01f9e809..278e8102 100644
--- a/pgjdbc/src/test/java/org/postgresql/test/jdbc2/CopyTest.java
+++ b/pgjdbc/src/test/java/org/postgresql/test/jdbc2/CopyTest.java
@@ -6,6 +6,7 @@
 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.assertTrue;
 import static org.junit.Assert.fail;
@@ -31,7 +32,6 @@ import java.io.OutputStream;
 import java.io.PrintStream;
 import java.io.StringReader;
 import java.sql.Connection;
-import java.sql.PreparedStatement;
 import java.sql.ResultSet;
 import java.sql.SQLException;
 import java.sql.Statement;
@@ -135,6 +135,47 @@ public class CopyTest {
     assertEquals(dataRows, rowCount);
   }
 
+  @Test
+  public void testCopyInAsOutputStreamClosesAfterEndCopy() throws SQLException, IOException {
+    String sql = "COPY copytest FROM STDIN";
+    PGCopyOutputStream os = new PGCopyOutputStream((PGConnection) con, sql, 1000);
+    try {
+      for (String anOrigData : origData) {
+        byte[] buf = anOrigData.getBytes();
+        os.write(buf);
+      }
+      os.endCopy();
+    } finally {
+      os.close();
+    }
+    assertFalse(os.isActive());
+    int rowCount = getCount();
+    assertEquals(dataRows, rowCount);
+  }
+
+  @Test
+  public void testCopyInAsOutputStreamFailsOnFlushAfterEndCopy() throws SQLException, IOException {
+    String sql = "COPY copytest FROM STDIN";
+    PGCopyOutputStream os = new PGCopyOutputStream((PGConnection) con, sql, 1000);
+    try {
+      for (String anOrigData : origData) {
+        byte[] buf = anOrigData.getBytes();
+        os.write(buf);
+      }
+      os.endCopy();
+    } finally {
+      os.close();
+    }
+    try {
+      os.flush();
+      fail("should have failed flushing an inactive copy stream.");
+    } catch (IOException e) {
+      if (!e.toString().contains("This copy stream is closed.")) {
+        fail("has failed not due to checkClosed(): " + e);
+      }
+    }
+  }
+
   @Test
   public void testCopyInFromInputStream() throws SQLException, IOException {
     String sql = "COPY copytest FROM STDIN";
@@ -353,17 +394,14 @@ public class CopyTest {
     // on the Connection object fails to deadlock.
     con.setAutoCommit(false);
 
-    Statement stmt = con.createStatement();
-    ResultSet rs = stmt.executeQuery("select pg_backend_pid()");
-    rs.next();
-    int pid = rs.getInt(1);
-    rs.close();
-    stmt.close();
+    // We get the process id before the COPY as we cannot run other commands
+    // on the connection during the COPY operation.
+    int pid = TestUtil.getBackendPid(con);
 
     CopyManager manager = con.unwrap(PGConnection.class).getCopyAPI();
     CopyIn copyIn = manager.copyIn("COPY copytest FROM STDIN with " + copyParams);
     try {
-      killConnection(pid);
+      TestUtil.terminateBackend(pid);
       byte[] bunchOfNulls = ",,\n".getBytes();
       while (true) {
         copyIn.writeToCopy(bunchOfNulls, 0, bunchOfNulls.length);
@@ -420,23 +458,6 @@ public class CopyTest {
     }
   }
 
-  private void killConnection(int pid) throws SQLException {
-    Connection killerCon;
-    try {
-      killerCon = TestUtil.openPrivilegedDB();
-    } catch (Exception e) {
-      fail("Unable to open secondary connection to terminate copy");
-      return; // persuade Java killerCon will not be used uninitialized
-    }
-    try {
-      PreparedStatement stmt = killerCon.prepareStatement("select pg_terminate_backend(?)");
-      stmt.setInt(1, pid);
-      stmt.execute();
-    } finally {
-      killerCon.close();
-    }
-  }
-
   private void acceptIOCause(SQLException e) throws SQLException {
     if (!(e.getCause() instanceof IOException)) {
       throw e;
diff --git a/pgjdbc/src/test/java/org/postgresql/test/jdbc2/optional/BaseDataSourceFailoverUrlsTest.java b/pgjdbc/src/test/java/org/postgresql/test/jdbc2/optional/BaseDataSourceFailoverUrlsTest.java
new file mode 100644
index 00000000..4b0acd65
--- /dev/null
+++ b/pgjdbc/src/test/java/org/postgresql/test/jdbc2/optional/BaseDataSourceFailoverUrlsTest.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (c) 2004, PostgreSQL Global Development Group
+ * See the LICENSE file in the project root for more information.
+ */
+
+package org.postgresql.test.jdbc2.optional;
+
+import static org.junit.Assert.assertEquals;
+
+import org.postgresql.ds.common.BaseDataSource;
+
+import org.junit.Test;
+
+import java.io.IOException;
+import javax.naming.NamingException;
+
+/**
+* tests that failover urls survive the parse/rebuild roundtrip with and without specific ports
+*/
+public class BaseDataSourceFailoverUrlsTest {
+
+  private static final String DEFAULT_PORT = "5432";
+
+  @Test
+  public void testFullDefault() throws ClassNotFoundException, NamingException, IOException {
+    roundTripFromUrl("jdbc:postgresql://server/database", "jdbc:postgresql://server:" + DEFAULT_PORT + "/database");
+  }
+
+  @Test
+  public void testTwoNoPorts() throws ClassNotFoundException, NamingException, IOException {
+    roundTripFromUrl("jdbc:postgresql://server1,server2/database", "jdbc:postgresql://server1:" + DEFAULT_PORT + ",server2:" + DEFAULT_PORT + "/database");
+  }
+
+  @Test
+  public void testTwoWithPorts() throws ClassNotFoundException, NamingException, IOException {
+    roundTripFromUrl("jdbc:postgresql://server1:1234,server2:2345/database", "jdbc:postgresql://server1:1234,server2:2345/database");
+  }
+
+  @Test
+  public void testTwoFirstPort() throws ClassNotFoundException, NamingException, IOException {
+    roundTripFromUrl("jdbc:postgresql://server1,server2:2345/database", "jdbc:postgresql://server1:" + DEFAULT_PORT + ",server2:2345/database");
+  }
+
+  @Test
+  public void testTwoLastPort() throws ClassNotFoundException, NamingException, IOException {
+    roundTripFromUrl("jdbc:postgresql://server1:2345,server2/database", "jdbc:postgresql://server1:2345,server2:" + DEFAULT_PORT + "/database");
+  }
+
+  @Test
+  public void testNullPorts() {
+    BaseDataSource bds = newDS();
+    bds.setDatabaseName("database");
+    bds.setPortNumbers(null);
+    assertUrlWithoutParamsEquals("jdbc:postgresql://localhost/database", bds.getURL());
+    assertEquals(0, bds.getPortNumber());
+    assertEquals(0, bds.getPortNumbers()[0]);
+  }
+
+  @Test
+  public void testEmptyPorts() {
+    BaseDataSource bds = newDS();
+    bds.setDatabaseName("database");
+    bds.setPortNumbers(new int[0]);
+    assertUrlWithoutParamsEquals("jdbc:postgresql://localhost/database", bds.getURL());
+    assertEquals(0, bds.getPortNumber());
+    assertEquals(0, bds.getPortNumbers()[0]);
+  }
+
+  private BaseDataSource newDS() {
+    return new BaseDataSource() {
+      @Override
+      public String getDescription() {
+        return "BaseDataSourceFailoverUrlsTest-DS";
+      }
+    };
+  }
+
+  private void roundTripFromUrl(String in, String expected) throws NamingException, ClassNotFoundException, IOException {
+    BaseDataSource bds = newDS();
+
+    bds.setUrl(in);
+    assertUrlWithoutParamsEquals(expected, bds.getURL());
+
+    bds.setFromReference(bds.getReference());
+    assertUrlWithoutParamsEquals(expected, bds.getURL());
+
+    bds.initializeFrom(bds);
+    assertUrlWithoutParamsEquals(expected, bds.getURL());
+  }
+
+  private static String jdbcUrlStripParams(String in) {
+    return in.replaceAll("\\?.*$", "");
+  }
+
+  private static void assertUrlWithoutParamsEquals(String expected, String url) {
+    assertEquals(expected, jdbcUrlStripParams(url));
+  }
+}
diff --git a/pgjdbc/src/test/java/org/postgresql/test/jdbc2/optional/ConnectionPoolTest.java b/pgjdbc/src/test/java/org/postgresql/test/jdbc2/optional/ConnectionPoolTest.java
index bcfeade4..e5edcbe7 100644
--- a/pgjdbc/src/test/java/org/postgresql/test/jdbc2/optional/ConnectionPoolTest.java
+++ b/pgjdbc/src/test/java/org/postgresql/test/jdbc2/optional/ConnectionPoolTest.java
@@ -9,7 +9,6 @@ import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
-import org.postgresql.PGConnection;
 import org.postgresql.core.ServerVersion;
 import org.postgresql.ds.PGConnectionPoolDataSource;
 import org.postgresql.jdbc2.optional.ConnectionPool;
@@ -329,18 +328,9 @@ public class ConnectionPoolTest extends BaseDataSourceTest {
       Assume.assumeTrue("pg_terminate_backend requires PostgreSQL 8.4+",
           TestUtil.haveMinimumServerVersion(con, ServerVersion.v8_4));
 
-      int pid = ((PGConnection) con).getBackendPID();
-
-      Connection adminCon = TestUtil.openPrivilegedDB();
-      try {
-        Statement statement = adminCon.createStatement();
-        statement.executeQuery("SELECT pg_terminate_backend(" + pid + ")");
-      } finally {
-        TestUtil.closeDB(adminCon);
-      }
+      TestUtil.terminateBackend(con);
       try {
-        Statement statement = con.createStatement();
-        statement.executeQuery("SELECT 1");
+        TestUtil.executeQuery(con, "SELECT 1");
         fail("The connection should not be opened anymore. An exception was expected");
       } catch (SQLException e) {
         // this is expected as the connection has been forcibly closed from backend
diff --git a/pgjdbc/src/test/java/org/postgresql/test/jdbc2/optional/OptionalTestSuite.java b/pgjdbc/src/test/java/org/postgresql/test/jdbc2/optional/OptionalTestSuite.java
index 00c5d1a0..b9ca7240 100644
--- a/pgjdbc/src/test/java/org/postgresql/test/jdbc2/optional/OptionalTestSuite.java
+++ b/pgjdbc/src/test/java/org/postgresql/test/jdbc2/optional/OptionalTestSuite.java
@@ -15,12 +15,15 @@ import org.junit.runners.Suite;
  * @author Aaron Mulder (ammulder@chariotsolutions.com)
  */
 @RunWith(Suite.class)
-@Suite.SuiteClasses({SimpleDataSourceTest.class,
-        SimpleDataSourceWithUrlTest.class,
-        SimpleDataSourceWithSetURLTest.class,
+@Suite.SuiteClasses({
+        BaseDataSourceFailoverUrlsTest.class,
+        CaseOptimiserDataSourceTest.class,
         ConnectionPoolTest.class,
         PoolingDataSourceTest.class,
-        CaseOptimiserDataSourceTest.class})
+        SimpleDataSourceTest.class,
+        SimpleDataSourceWithSetURLTest.class,
+        SimpleDataSourceWithUrlTest.class,
+})
 public class OptionalTestSuite {
 
 }
diff --git a/pgjdbc/src/test/java/org/postgresql/test/jdbc4/DatabaseMetaDataTest.java b/pgjdbc/src/test/java/org/postgresql/test/jdbc4/DatabaseMetaDataTest.java
index 813a777f..a22f42ae 100644
--- a/pgjdbc/src/test/java/org/postgresql/test/jdbc4/DatabaseMetaDataTest.java
+++ b/pgjdbc/src/test/java/org/postgresql/test/jdbc4/DatabaseMetaDataTest.java
@@ -232,4 +232,17 @@ public class DatabaseMetaDataTest {
       stmt.execute("DROP FUNCTION getfunc_f3(int, varchar)");
     }
   }
+
+  @Test
+  public void testSortedDataTypes() throws SQLException {
+    // https://github.com/pgjdbc/pgjdbc/issues/716
+    DatabaseMetaData dbmd = conn.getMetaData();
+    ResultSet rs = dbmd.getTypeInfo();
+    int lastType = Integer.MIN_VALUE;
+    while (rs.next()) {
+      int type = rs.getInt("DATA_TYPE");
+      assertTrue(lastType <= type);
+      lastType = type;
+    }
+  }
 }
diff --git a/pgjdbc/src/test/java/org/postgresql/test/jdbc4/IsValidTest.java b/pgjdbc/src/test/java/org/postgresql/test/jdbc4/IsValidTest.java
index 0a4a6441..c68cc733 100644
--- a/pgjdbc/src/test/java/org/postgresql/test/jdbc4/IsValidTest.java
+++ b/pgjdbc/src/test/java/org/postgresql/test/jdbc4/IsValidTest.java
@@ -5,102 +5,62 @@
 
 package org.postgresql.test.jdbc4;
 
-import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
-import org.postgresql.core.BaseConnection;
 import org.postgresql.core.ServerVersion;
 import org.postgresql.core.TransactionState;
 import org.postgresql.test.TestUtil;
 import org.postgresql.test.jdbc2.BaseTest4;
 
+import org.junit.Assume;
 import org.junit.Test;
 
 import java.sql.Connection;
-import java.sql.ResultSet;
-import java.sql.Statement;
-import java.util.Properties;
+import java.sql.SQLException;
 
 public class IsValidTest extends BaseTest4 {
-
-  private TransactionState getTransactionState(Connection conn) {
-    return ((BaseConnection) conn).getTransactionState();
+  @Test
+  public void testIsValidShouldNotModifyTransactionStateOutsideTransaction() throws SQLException {
+    TransactionState initialTransactionState = TestUtil.getTransactionState(con);
+    assertTrue("Connection should be valid", con.isValid(0));
+    TestUtil.assertTransactionState("Transaction state should not be modified by non-transactional Connection.isValid(...)", con, initialTransactionState);
   }
 
   @Test
-  public void testIsValid() throws Exception {
-    try {
-      assertTrue(con.isValid(0));
-    } finally {
-      TestUtil.closeDB(con);
-    }
-    assertFalse(con.isValid(0));
+  public void testIsValidShouldNotModifyTransactionStateInEmptyTransaction() throws SQLException {
+    con.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
+    con.setAutoCommit(false);
+    TransactionState transactionState = TestUtil.getTransactionState(con);
+    assertTrue("Connection should be valid", con.isValid(0));
+    TestUtil.assertTransactionState("Transaction state should not be modified by Connection.isValid(...) within an empty transaction", con, transactionState);
   }
 
-  /**
-   * Test that the transaction state is left unchanged.
-   */
   @Test
-  public void testTransactionState() throws Exception {
-    TransactionState transactionState = getTransactionState(con);
-    con.isValid(0);
-    assertEquals("Transaction state has been changed", transactionState,
-        getTransactionState(con));
-
+  public void testIsValidShouldNotModifyTransactionStateInNonEmptyTransaction() throws SQLException {
     con.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
     con.setAutoCommit(false);
-    try {
-      transactionState = getTransactionState(con);
-      con.isValid(0);
-      assertEquals("Transaction state has been changed", transactionState,
-          getTransactionState(con));
-
-      Statement stmt = con.createStatement();
-      stmt.execute("SELECT 1");
-      transactionState = getTransactionState(con);
-      con.isValid(0);
-      assertEquals("Transaction state has been changed", transactionState,
-          getTransactionState(con));
-    } finally {
-      try {
-        con.setAutoCommit(true);
-      } catch (final Exception e) {
-      }
-    }
+    TestUtil.executeQuery(con, "SELECT 1");
+    TransactionState transactionState = TestUtil.getTransactionState(con);
+    assertTrue("Connection should be valid", con.isValid(0));
+    TestUtil.assertTransactionState("Transaction state should not be modified by Connection.isValid(...) within a non-empty transaction", con, transactionState);
   }
 
   @Test
-  public void testIsValidRemoteClose() throws Exception {
-
-    assumeMinimumServerVersion("Unable to use pg_terminate_backend before version 8.4", ServerVersion.v8_4);
-
-    Properties props = new Properties();
-    updateProperties(props);
-    Connection con2 = TestUtil.openPrivilegedDB();
-
-    try {
+  public void testIsValidRemoteClose() throws SQLException, InterruptedException {
+    Assume.assumeTrue("Unable to use pg_terminate_backend(...) before version 8.4", TestUtil.haveMinimumServerVersion(con, ServerVersion.v8_4));
 
-      assertTrue("First connection should be valid", con.isValid(0));
+    boolean wasTerminated = TestUtil.terminateBackend(con);
+    assertTrue("The backend should be terminated", wasTerminated);
 
-      String pid;
-      Statement s = con.createStatement();
-
-      try {
-        s.execute("select pg_backend_pid()");
-        ResultSet rs = s.getResultSet();
-        rs.next();
-        pid = rs.getString(1);
-      } finally {
-        TestUtil.closeQuietly(s);
+    // Keeps checking for up to 5-seconds that the connection is marked invalid
+    for (int i = 0;i < 500;i++) {
+      if (!con.isValid(0)) {
+        break;
       }
-
-      TestUtil.execute("select pg_terminate_backend(" + pid + ")", con2);
-
-      assertFalse("The first connection should now be invalid", con.isValid(0));
-
-    } finally {
-      TestUtil.closeQuietly(con2);
+      // Wait a bit to give the connection a chance to gracefully handle the termination
+      Thread.sleep(10);
     }
+    assertFalse("The terminated connection should not be valid", con.isValid(0));
   }
 }
diff --git a/pgjdbc/src/test/java/org/postgresql/test/jdbc42/Jdbc42TestSuite.java b/pgjdbc/src/test/java/org/postgresql/test/jdbc42/Jdbc42TestSuite.java
index 6b92e46f..3d444fd0 100644
--- a/pgjdbc/src/test/java/org/postgresql/test/jdbc42/Jdbc42TestSuite.java
+++ b/pgjdbc/src/test/java/org/postgresql/test/jdbc42/Jdbc42TestSuite.java
@@ -15,6 +15,7 @@ import org.junit.runners.Suite.SuiteClasses;
     GetObject310InfinityTests.class,
     GetObject310Test.class,
     Jdbc42CallableStatementTest.class,
+    LargeCountJdbc42Test.class,
     PreparedStatementTest.class,
     SetObject310Test.class,
     SimpleJdbc42Test.class,
diff --git a/pgjdbc/src/test/java/org/postgresql/test/jdbc42/LargeCountJdbc42Test.java b/pgjdbc/src/test/java/org/postgresql/test/jdbc42/LargeCountJdbc42Test.java
new file mode 100644
index 00000000..fef33625
--- /dev/null
+++ b/pgjdbc/src/test/java/org/postgresql/test/jdbc42/LargeCountJdbc42Test.java
@@ -0,0 +1,412 @@
+/*
+ * Copyright (c) 2018, PostgreSQL Global Development Group
+ * See the LICENSE file in the project root for more information.
+ */
+
+package org.postgresql.test.jdbc42;
+
+import org.postgresql.PGProperty;
+import org.postgresql.test.TestUtil;
+import org.postgresql.test.jdbc2.BaseTest4;
+import org.postgresql.util.PSQLState;
+
+import org.junit.Assert;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Properties;
+
+/**
+ * Test methods with small counts that return long and failure scenarios. This have two really big
+ * and slow test, they are ignored for CI but can be tested locally to check that it works.
+ */
+@RunWith(Parameterized.class)
+public class LargeCountJdbc42Test extends BaseTest4 {
+
+  private final boolean insertRewrite;
+
+  public LargeCountJdbc42Test(BinaryMode binaryMode, boolean insertRewrite) {
+    this.insertRewrite = insertRewrite;
+    setBinaryMode(binaryMode);
+  }
+
+  @Parameterized.Parameters(name = "binary = {0}, insertRewrite = {1}")
+  public static Iterable<Object[]> data() {
+    Collection<Object[]> ids = new ArrayList<>();
+    for (BinaryMode binaryMode : BinaryMode.values()) {
+      for (boolean insertRewrite : new boolean[]{false, true}) {
+        ids.add(new Object[]{binaryMode, insertRewrite});
+      }
+    }
+    return ids;
+  }
+
+  @Override
+  protected void updateProperties(Properties props) {
+    super.updateProperties(props);
+    PGProperty.REWRITE_BATCHED_INSERTS.set(props, insertRewrite);
+  }
+
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+    TestUtil.createUnloggedTable(con, "largetable", "a boolean");
+  }
+
+  @Override
+  public void tearDown() throws SQLException {
+    TestUtil.dropTable(con, "largetable");
+    TestUtil.closeDB(con);
+  }
+
+  // ********************* EXECUTE LARGE UPDATES *********************
+  //    FINEST: simple execute, handler=org.postgresql.jdbc.PgStatement$StatementResultHandler@38cccef, maxRows=0, fetchSize=0, flags=21
+  //    FINEST: FE=> Parse(stmt=null,query="insert into largetable select true from generate_series($1, $2)",oids={20,20})
+  //    FINEST: FE=> Bind(stmt=null,portal=null,$1=<1>,$2=<2147483757>)
+  //    FINEST: FE=> Describe(portal=null)
+  //    FINEST: FE=> Execute(portal=null,limit=1)
+  //    FINEST: FE=> Sync
+  //    FINEST: <=BE ParseComplete [null]
+  //    FINEST: <=BE BindComplete [unnamed]
+  //    FINEST: <=BE NoData
+  //    FINEST: <=BE CommandStatus(INSERT 0 2147483757)
+  //    FINEST: <=BE ReadyForQuery(I)
+  //    FINEST: simple execute, handler=org.postgresql.jdbc.PgStatement$StatementResultHandler@5679c6c6, maxRows=0, fetchSize=0, flags=21
+  //    FINEST: FE=> Parse(stmt=null,query="delete from largetable",oids={})
+  //    FINEST: FE=> Bind(stmt=null,portal=null)
+  //    FINEST: FE=> Describe(portal=null)
+  //    FINEST: FE=> Execute(portal=null,limit=1)
+  //    FINEST: FE=> Sync
+  //    FINEST: <=BE ParseComplete [null]
+  //    FINEST: <=BE BindComplete [unnamed]
+  //    FINEST: <=BE NoData
+  //    FINEST: <=BE CommandStatus(DELETE 2147483757)
+
+  /*
+   * Test PreparedStatement.executeLargeUpdate() and Statement.executeLargeUpdate(String sql)
+   */
+  @Ignore("This is the big and SLOW test")
+  @Test
+  public void testExecuteLargeUpdateBIG() throws Exception {
+    long expected = Integer.MAX_VALUE + 110L;
+    con.setAutoCommit(false);
+    // Test PreparedStatement.executeLargeUpdate()
+    try (PreparedStatement stmt = con.prepareStatement("insert into largetable "
+        + "select true from generate_series(?, ?)")) {
+      stmt.setLong(1, 1);
+      stmt.setLong(2, 2_147_483_757L); // Integer.MAX_VALUE + 110L
+      long count = stmt.executeLargeUpdate();
+      Assert.assertEquals("PreparedStatement 110 rows more than Integer.MAX_VALUE", expected, count);
+    }
+    // Test Statement.executeLargeUpdate(String sql)
+    try (Statement stmt = con.createStatement()) {
+      long count = stmt.executeLargeUpdate("delete from largetable");
+      Assert.assertEquals("Statement 110 rows more than Integer.MAX_VALUE", expected, count);
+    }
+    con.setAutoCommit(true);
+  }
+
+  /*
+   * Test Statement.executeLargeUpdate(String sql)
+   */
+  @Test
+  public void testExecuteLargeUpdateStatementSMALL() throws Exception {
+    try (Statement stmt = con.createStatement()) {
+      long count = stmt.executeLargeUpdate("insert into largetable "
+          + "select true from generate_series(1, 1010)");
+      long expected = 1010L;
+      Assert.assertEquals("Small long return 1010L", expected, count);
+    }
+  }
+
+  /*
+   * Test PreparedStatement.executeLargeUpdate();
+   */
+  @Test
+  public void testExecuteLargeUpdatePreparedStatementSMALL() throws Exception {
+    try (PreparedStatement stmt = con.prepareStatement("insert into largetable "
+        + "select true from generate_series(?, ?)")) {
+      stmt.setLong(1, 1);
+      stmt.setLong(2, 1010L);
+      long count = stmt.executeLargeUpdate();
+      long expected = 1010L;
+      Assert.assertEquals("Small long return 1010L", expected, count);
+    }
+  }
+
+  /*
+   * Test Statement.getLargeUpdateCount();
+   */
+  @Test
+  public void testGetLargeUpdateCountStatementSMALL() throws Exception {
+    try (Statement stmt = con.createStatement()) {
+      boolean isResult = stmt.execute("insert into largetable "
+          + "select true from generate_series(1, 1010)");
+      Assert.assertFalse("False if it is an update count or there are no results", isResult);
+      long count = stmt.getLargeUpdateCount();
+      long expected = 1010L;
+      Assert.assertEquals("Small long return 1010L", expected, count);
+    }
+  }
+
+  /*
+   * Test PreparedStatement.getLargeUpdateCount();
+   */
+  @Test
+  public void testGetLargeUpdateCountPreparedStatementSMALL() throws Exception {
+    try (PreparedStatement stmt = con.prepareStatement("insert into largetable "
+        + "select true from generate_series(?, ?)")) {
+      stmt.setInt(1, 1);
+      stmt.setInt(2, 1010);
+      boolean isResult = stmt.execute();
+      Assert.assertFalse("False if it is an update count or there are no results", isResult);
+      long count = stmt.getLargeUpdateCount();
+      long expected = 1010L;
+      Assert.assertEquals("Small long return 1010L", expected, count);
+    }
+  }
+
+  /*
+   * Test fail SELECT Statement.executeLargeUpdate(String sql)
+   */
+  @Test
+  public void testExecuteLargeUpdateStatementSELECT() throws Exception {
+    try (Statement stmt = con.createStatement()) {
+      long count = stmt.executeLargeUpdate("select true from generate_series(1, 5)");
+      Assert.fail("A result was returned when none was expected. Returned: " + count);
+    } catch (SQLException e) {
+      Assert.assertEquals(PSQLState.TOO_MANY_RESULTS.getState(), e.getSQLState());
+    }
+  }
+
+  /*
+   * Test fail SELECT PreparedStatement.executeLargeUpdate();
+   */
+  @Test
+  public void testExecuteLargeUpdatePreparedStatementSELECT() throws Exception {
+    try (PreparedStatement stmt = con.prepareStatement("select true from generate_series(?, ?)")) {
+      stmt.setLong(1, 1);
+      stmt.setLong(2, 5L);
+      long count = stmt.executeLargeUpdate();
+      Assert.fail("A result was returned when none was expected. Returned: " + count);
+    } catch (SQLException e) {
+      Assert.assertEquals(PSQLState.TOO_MANY_RESULTS.getState(), e.getSQLState());
+    }
+  }
+
+  /*
+   * Test Statement.getLargeUpdateCount();
+   */
+  @Test
+  public void testGetLargeUpdateCountStatementSELECT() throws Exception {
+    try (Statement stmt = con.createStatement()) {
+      boolean isResult = stmt.execute("select true from generate_series(1, 5)");
+      Assert.assertTrue("True since this is a SELECT", isResult);
+      long count = stmt.getLargeUpdateCount();
+      long expected = -1L;
+      Assert.assertEquals("-1 if the current result is a ResultSet object", expected, count);
+    }
+  }
+
+  /*
+   * Test PreparedStatement.getLargeUpdateCount();
+   */
+  @Test
+  public void testGetLargeUpdateCountPreparedStatementSELECT() throws Exception {
+    try (PreparedStatement stmt = con.prepareStatement("select true from generate_series(?, ?)")) {
+      stmt.setLong(1, 1);
+      stmt.setLong(2, 5L);
+      boolean isResult = stmt.execute();
+      Assert.assertTrue("True since this is a SELECT", isResult);
+      long count = stmt.getLargeUpdateCount();
+      long expected = -1L;
+      Assert.assertEquals("-1 if the current result is a ResultSet object", expected, count);
+    }
+  }
+
+  // ********************* BATCH LARGE UPDATES *********************
+  //    FINEST: batch execute 3 queries, handler=org.postgresql.jdbc.BatchResultHandler@3d04a311, maxRows=0, fetchSize=0, flags=21
+  //    FINEST: FE=> Parse(stmt=null,query="insert into largetable select true from generate_series($1, $2)",oids={23,23})
+  //    FINEST: FE=> Bind(stmt=null,portal=null,$1=<1>,$2=<200>)
+  //    FINEST: FE=> Describe(portal=null)
+  //    FINEST: FE=> Execute(portal=null,limit=1)
+  //    FINEST: FE=> Parse(stmt=null,query="insert into largetable select true from generate_series($1, $2)",oids={23,20})
+  //    FINEST: FE=> Bind(stmt=null,portal=null,$1=<1>,$2=<3000000000>)
+  //    FINEST: FE=> Describe(portal=null)
+  //    FINEST: FE=> Execute(portal=null,limit=1)
+  //    FINEST: FE=> Parse(stmt=null,query="insert into largetable select true from generate_series($1, $2)",oids={23,23})
+  //    FINEST: FE=> Bind(stmt=null,portal=null,$1=<1>,$2=<50>)
+  //    FINEST: FE=> Describe(portal=null)
+  //    FINEST: FE=> Execute(portal=null,limit=1)
+  //    FINEST: FE=> Sync
+  //    FINEST: <=BE ParseComplete [null]
+  //    FINEST: <=BE BindComplete [unnamed]
+  //    FINEST: <=BE NoData
+  //    FINEST: <=BE CommandStatus(INSERT 0 200)
+  //    FINEST: <=BE ParseComplete [null]
+  //    FINEST: <=BE BindComplete [unnamed]
+  //    FINEST: <=BE NoData
+  //    FINEST: <=BE CommandStatus(INSERT 0 3000000000)
+  //    FINEST: <=BE ParseComplete [null]
+  //    FINEST: <=BE BindComplete [unnamed]
+  //    FINEST: <=BE NoData
+  //    FINEST: <=BE CommandStatus(INSERT 0 50)
+
+  /*
+   * Test simple PreparedStatement.executeLargeBatch();
+   */
+  @Ignore("This is the big and SLOW test")
+  @Test
+  public void testExecuteLargeBatchStatementBIG() throws Exception {
+    con.setAutoCommit(false);
+    try (PreparedStatement stmt = con.prepareStatement("insert into largetable "
+        + "select true from generate_series(?, ?)")) {
+      stmt.setInt(1, 1);
+      stmt.setInt(2, 200);
+      stmt.addBatch(); // statement one
+      stmt.setInt(1, 1);
+      stmt.setLong(2, 3_000_000_000L);
+      stmt.addBatch(); // statement two
+      stmt.setInt(1, 1);
+      stmt.setInt(2, 50);
+      stmt.addBatch(); // statement three
+      long[] actual = stmt.executeLargeBatch();
+      Assert.assertArrayEquals("Large rows inserted via 3 batch", new long[]{200L, 3_000_000_000L, 50L}, actual);
+    }
+    con.setAutoCommit(true);
+  }
+
+  /*
+   * Test simple Statement.executeLargeBatch();
+   */
+  @Test
+  public void testExecuteLargeBatchStatementSMALL() throws Exception {
+    try (Statement stmt = con.createStatement()) {
+      stmt.addBatch("insert into largetable(a) select true"); // statement one
+      stmt.addBatch("insert into largetable select false"); // statement two
+      stmt.addBatch("insert into largetable(a) values(true)"); // statement three
+      stmt.addBatch("insert into largetable values(false)"); // statement four
+      long[] actual = stmt.executeLargeBatch();
+      Assert.assertArrayEquals("Rows inserted via 4 batch", new long[]{1L, 1L, 1L, 1L}, actual);
+    }
+  }
+
+  /*
+   * Test simple PreparedStatement.executeLargeBatch();
+   */
+  @Test
+  public void testExecuteLargePreparedStatementStatementSMALL() throws Exception {
+    try (PreparedStatement stmt = con.prepareStatement("insert into largetable "
+        + "select true from generate_series(?, ?)")) {
+      stmt.setInt(1, 1);
+      stmt.setInt(2, 200);
+      stmt.addBatch(); // statement one
+      stmt.setInt(1, 1);
+      stmt.setInt(2, 100);
+      stmt.addBatch(); // statement two
+      stmt.setInt(1, 1);
+      stmt.setInt(2, 50);
+      stmt.addBatch(); // statement three
+      stmt.addBatch(); // statement four, same parms as three
+      long[] actual = stmt.executeLargeBatch();
+      Assert.assertArrayEquals("Rows inserted via 4 batch", new long[]{200L, 100L, 50L, 50L}, actual);
+    }
+  }
+
+  /*
+   * Test loop PreparedStatement.executeLargeBatch();
+   */
+  @Test
+  public void testExecuteLargePreparedStatementStatementLoopSMALL() throws Exception {
+    long[] loop = {200, 100, 50, 300, 20, 60, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096};
+    try (PreparedStatement stmt = con.prepareStatement("insert into largetable "
+        + "select true from generate_series(?, ?)")) {
+      for (long i : loop) {
+        stmt.setInt(1, 1);
+        stmt.setLong(2, i);
+        stmt.addBatch();
+      }
+      long[] actual = stmt.executeLargeBatch();
+      Assert.assertArrayEquals("Rows inserted via batch", loop, actual);
+    }
+  }
+
+  /*
+   * Test loop PreparedStatement.executeLargeBatch();
+   */
+  @Test
+  public void testExecuteLargeBatchValuesInsertSMALL() throws Exception {
+    boolean[] loop = {true, false, true, false, false, false, true, true, true, true, false, true};
+    try (PreparedStatement stmt = con.prepareStatement("insert into largetable values(?)")) {
+      for (boolean i : loop) {
+        stmt.setBoolean(1, i);
+        stmt.addBatch();
+      }
+      long[] actual = stmt.executeLargeBatch();
+      Assert.assertEquals("Rows inserted via batch", loop.length, actual.length);
+      for (long i : actual) {
+        if (insertRewrite) {
+          Assert.assertEquals(Statement.SUCCESS_NO_INFO, i);
+        } else {
+          Assert.assertEquals(1, i);
+        }
+      }
+    }
+  }
+
+  /*
+   * Test null PreparedStatement.executeLargeBatch();
+   */
+  @Test
+  public void testNullExecuteLargeBatchStatement() throws Exception {
+    try (Statement stmt = con.createStatement()) {
+      long[] actual = stmt.executeLargeBatch();
+      Assert.assertArrayEquals("addBatch() not called batchStatements is null", new long[0], actual);
+    }
+  }
+
+  /*
+   * Test empty PreparedStatement.executeLargeBatch();
+   */
+  @Test
+  public void testEmptyExecuteLargeBatchStatement() throws Exception {
+    try (Statement stmt = con.createStatement()) {
+      stmt.addBatch("");
+      stmt.clearBatch();
+      long[] actual = stmt.executeLargeBatch();
+      Assert.assertArrayEquals("clearBatch() called, batchStatements.isEmpty()", new long[0], actual);
+    }
+  }
+
+  /*
+   * Test null PreparedStatement.executeLargeBatch();
+   */
+  @Test
+  public void testNullExecuteLargeBatchPreparedStatement() throws Exception {
+    try (PreparedStatement stmt = con.prepareStatement("")) {
+      long[] actual = stmt.executeLargeBatch();
+      Assert.assertArrayEquals("addBatch() not called batchStatements is null", new long[0], actual);
+    }
+  }
+
+  /*
+   * Test empty PreparedStatement.executeLargeBatch();
+   */
+  @Test
+  public void testEmptyExecuteLargeBatchPreparedStatement() throws Exception {
+    try (PreparedStatement stmt = con.prepareStatement("")) {
+      stmt.addBatch();
+      stmt.clearBatch();
+      long[] actual = stmt.executeLargeBatch();
+      Assert.assertArrayEquals("clearBatch() called, batchStatements.isEmpty()", new long[0], actual);
+    }
+  }
+
+}
diff --git a/pgjdbc/src/test/java/org/postgresql/test/jre8/core/Jre8TestSuite.java b/pgjdbc/src/test/java/org/postgresql/test/jre8/core/Jre8TestSuite.java
index 96cee652..6c971a0b 100644
--- a/pgjdbc/src/test/java/org/postgresql/test/jre8/core/Jre8TestSuite.java
+++ b/pgjdbc/src/test/java/org/postgresql/test/jre8/core/Jre8TestSuite.java
@@ -13,6 +13,8 @@ import org.junit.runners.Suite;
  *         Twitter: @codefinger
  */
 @RunWith(Suite.class)
-@Suite.SuiteClasses({SocksProxyTest.class})
+@Suite.SuiteClasses({
+    SocksProxyTest.class,
+})
 public class Jre8TestSuite {
 }
diff --git a/pgjdbc/src/test/java/org/postgresql/test/osgi/OsgiTestSuite.java b/pgjdbc/src/test/java/org/postgresql/test/osgi/OsgiTestSuite.java
index a7c909dc..7402247e 100644
--- a/pgjdbc/src/test/java/org/postgresql/test/osgi/OsgiTestSuite.java
+++ b/pgjdbc/src/test/java/org/postgresql/test/osgi/OsgiTestSuite.java
@@ -10,7 +10,9 @@ import org.junit.runners.Suite;
 import org.junit.runners.Suite.SuiteClasses;
 
 @RunWith(Suite.class)
-@SuiteClasses({PGDataSourceFactoryTest.class})
+@SuiteClasses({
+    PGDataSourceFactoryTest.class,
+})
 public class OsgiTestSuite {
 
 }
diff --git a/pgjdbc/src/test/java/org/postgresql/test/ssl/LazyKeyManagerTest.java b/pgjdbc/src/test/java/org/postgresql/test/ssl/LazyKeyManagerTest.java
new file mode 100644
index 00000000..1c1f1f0f
--- /dev/null
+++ b/pgjdbc/src/test/java/org/postgresql/test/ssl/LazyKeyManagerTest.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (c) 2019, PostgreSQL Global Development Group
+ * See the LICENSE file in the project root for more information.
+ */
+
+package org.postgresql.test.ssl;
+
+import org.postgresql.ssl.LazyKeyManager;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.io.File;
+import java.io.IOException;
+import java.security.PrivateKey;
+
+import javax.security.auth.callback.Callback;
+import javax.security.auth.callback.CallbackHandler;
+import javax.security.auth.callback.PasswordCallback;
+import javax.security.auth.callback.UnsupportedCallbackException;
+
+public class LazyKeyManagerTest {
+
+  @Test
+  public void testLoadKey() throws Exception {
+    String certdir = "../certdir/";
+    String path = new File("./").getAbsolutePath();
+    LazyKeyManager lazyKeyManager = new LazyKeyManager(certdir + "goodclient.crt",
+        certdir + "goodclient.pk8", new TestCallbackHandler("sslpwd"), true);
+    PrivateKey pk = lazyKeyManager.getPrivateKey("user");
+    Assert.assertNotNull(pk);
+  }
+
+  public static class TestCallbackHandler implements CallbackHandler {
+    char [] password;
+
+    public TestCallbackHandler(String password) {
+      if (password != null) {
+        this.password = password.toCharArray();
+      }
+    }
+
+    @Override
+    public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
+      for (Callback callback : callbacks) {
+        if (!(callback instanceof PasswordCallback)) {
+          throw new UnsupportedCallbackException(callback);
+        }
+        PasswordCallback pwdCallback = (PasswordCallback) callback;
+        if (password != null) {
+          pwdCallback.setPassword(password);
+          continue;
+        }
+        // It is used instead of cons.readPassword(prompt), because the prompt may contain '%'
+        // characters
+        //pwdCallback.setPassword(cons.readPassword("%s", pwdCallback.getPrompt()));
+      }
+    }
+  }
+}
diff --git a/pgjdbc/src/test/java/org/postgresql/test/ssl/SslTestSuite.java b/pgjdbc/src/test/java/org/postgresql/test/ssl/SslTestSuite.java
index 2dece06e..95e439c4 100644
--- a/pgjdbc/src/test/java/org/postgresql/test/ssl/SslTestSuite.java
+++ b/pgjdbc/src/test/java/org/postgresql/test/ssl/SslTestSuite.java
@@ -11,6 +11,7 @@ import org.junit.runners.Suite;
 @RunWith(Suite.class)
 @Suite.SuiteClasses({
     CommonNameVerifierTest.class,
+    LazyKeyManagerTest.class,
     LibPQFactoryHostNameTest.class,
     SslTest.class,
 })
diff --git a/pgjdbc/src/test/java/org/postgresql/test/xa/XATestSuite.java b/pgjdbc/src/test/java/org/postgresql/test/xa/XATestSuite.java
index 787ebc7d..d4d4cb7a 100644
--- a/pgjdbc/src/test/java/org/postgresql/test/xa/XATestSuite.java
+++ b/pgjdbc/src/test/java/org/postgresql/test/xa/XATestSuite.java
@@ -9,6 +9,8 @@ import org.junit.runner.RunWith;
 import org.junit.runners.Suite;
 
 @RunWith(Suite.class)
-@Suite.SuiteClasses(XADataSourceTest.class)
+@Suite.SuiteClasses({
+    XADataSourceTest.class,
+})
 public class XATestSuite {
 }