Repository: pgjdbc/pgjdbc
Pull request #451 (2015-12-09 20:41:09)

fix: try re-execute statement if backend responds with "prepared statement is no longer valid"
Bug report: http://stackoverflow.com/questions/34180932/error-cached-plan-must-not-change-result-type-when-mixing-ddl-with-select-via
When using "deallocate all" or "alter table", cached prepared statements might become invalid, thus we need to reexecute query in those cases

It might require some logging, however I'm not sure what is the best way to log that.

Summary (technical details):
1) By default pgjdbc watches for `DEALLOCATE ALL`, and `DISCARD ALL` statements, and invalidates prepared statement cache if those are detected.
That does not mean "deallocate all" should be used often. It just means deallocate all might be suitable for maintenance-like procedures.
Note: `deallocate all` invalidates only a single connection.
2) If either `prepared statement XXX does not exist` or `cached statement must not change result type` observed, pgjdbc would invalidate prepared statement cache (see `deallocateEpoch++;`). In general those errors should not happen, however if observed it means something had changed behind the scenes.
3) By default pgjdbc would retry `prepared statement XXX does not exist` and `cached statement must not change result type` errors in `autocommit=true` (non-transactional) mode.
4) New connection property: `autosave`
 * `conservative` -- rollback to savepoint only in case of "prepared statement does not exist" and
       "cached plan must not change result type". Then the driver would re-execute the statement ant it would pass through
 * `never` (default) -- never set automatic safepoint. Note: in this mode statements might still fail with "cached plan must not change result type"
       in autoCommit=FALSE mode
 * `always` -- always rollback to "before statement execution" state in case of failure. This mode prevents "current transaction aborted" errors.
       It is similar to psql's ON_ERROR_ROLLBACK.

For `autocommit=true` the savepoint is not used.
In `conservative` mode, savepoint is used only for queries that return something (see `org.postgresql.core.v3.QueryExecutorImpl#sendAutomaticSavepoint`)

lukaseder: Thanks @vlsi for submitting this PR. I have reported the original issue on Stack Overflow as a means to document the current behaviour. When googling, I've found a few incidents of this type, and none of them was resolved or understood.

This all has to do with the JDBC driver defaulting to a "prepare threshold" of 5, which is why the sixth DDL statement seems to cause cursors to expire.

I believe that this kind of issue shouldn't be dealt with in the client application, specifically because the client closes each prepared statement, it is no longer the responsibility of the client to handle these stale resources.
alexdupre: +1

It's very useful when you have live database migrations and multiple nodes connecting to the same database that cannot be all upgraded/restarted at the same time. To be able to use prepared statements and avoid downtime is an important feature.

I'm not sure that parsing the textual error message is the best way to do it, there aren't specific error codes identifying such errors?
vladimirfx: We are heavily affected by this issue. We try to solve it on connection pool level (Hikari) by closing poisoned connection but fix it on driver level seems to be more proper way.
But proposed fix is invalid for cases with non English locale on PG server, as in our case.
vlsi: @vladimirfx Can you please clarify which error are you going into? "Prepared statement does not exist" or "cached statement cannot change result type"?
The first one has its own error code, but the latter uses just "not implemented" error code.
codecov-io: ## [Current coverage](https://codecov.io/gh/pgjdbc/pgjdbc/pull/451?src=pr) is 58.21% (diff: 85.71%)
> Merging [#451](https://codecov.io/gh/pgjdbc/pgjdbc/pull/451?src=pr) into [master](https://codecov.io/gh/pgjdbc/pgjdbc/branch/master?src=pr) will increase coverage by **0.02%**


```diff
@@             master       #451   diff @@
==========================================
  Files           148        148          
  Lines         15632      15645    +13   
  Methods           0          0          
  Messages          0          0          
  Branches       3095       3100     +5   
==========================================
+ Hits           9096       9108    +12   
+ Misses         5307       5306     -1   
- Partials       1229       1231     +2   
```

> Powered by [Codecov](https://codecov.io?src=pr). Last update [88fbbc5...b6e901d](https://codecov.io/gh/pgjdbc/pgjdbc/compare/88fbbc59132090ad7602377014f7d55fde3aa487...b6e901da9c602bf24c41f8503863a961bc4d4053?src=pr)
vlsi: If no objections, I will merge this in
vladimirfx: We hit by both but most of time "cached statement cannot change result type" . My coment is about first patch where only error text is analyzed. Current impl seems more robust.
vlsi: Can you clarify what kind of setup you are using? I'm inclined to log warnings in case the above conditions are met.
Is the application using "set search_path"? Is it using "deallocate all"? 
vladimirfx: Postgresql 9.3.13 , locale ru_RU.UTF-8 , pgjdbc 9.4.1208 , JDK 1.8 .
2 middleware OSGI containers with couple of modules each. When applying rolling upgrade with minor (backward compatible) schema changes modules that not upgraded stop working with errors like "cached plan must not change result type" (in rus: "в кешированном плане не должен изменяться тип результата"). 
When such situation detected DBA execute DEALLOCATE ALL but without success, because new type of errors arise: "prepared statement ... does not exist" (rus: "подготовленный оператор не существует").
We already doing connection invalidation on pool level but it is not robust solution because of dependency on localized string and not optimal solution because of failing transaction until all connections in pool will be invalidated.
We are waiting for release with this pool request merged. Thanks!
vlsi: >When such situation detected DBA execute DEALLOCATE ALL but without success

@vladimirfx , Can you elaborate on that?
AFAIK, `deallocate all` should be done through pgjdbc connection. How does DBA do that?
vlsi: Unfortunately, using SAVEPOINTS and DML causes extra xid allocation, so I do not see good solution for "cached plan cannot change result type" in transactional mode (see https://github.com/pgjdbc/pgjdbc/pull/477#issuecomment-239644770)

deallocate/discard can be solved without savepoints (the driver would just notice deallocate responses and invalidate the cache).
davecramer: I don't think this is a good reason to abandon AutoSave feature. the psqlODBC driver has it. Also I didn't realize that any select that does NOT use a transaction uses up an XID as well. As I think most people don't use transactions they are consuming XID's at a considerable rate without complaining.
vlsi: Some field test is required to check how autosave=conservative impacts xid allocation rate in real applications.
davecramer: Well unfortunately the only way we are going to get field tests is by enabling the feature and getting reports back. I think we put the feature in. Do not make it the default. If people want the feature they can enable it.
vlsi: > I think we put the feature in. Do not make it the default.

Ok good. I was having the same in mind.


Just brainstorming for other possible solutions: we can try LISTEN/NOTIFY to cooperate pgjdbc with maintenance procedures.
For instance:
1) Before DB maintenance (e.g. alter table, etc), DBA issues `NOTIFY 'pgjdbc', 'please, use unnamed statements only'`.
2) As far as I understand, every backend would receive that NOTIFY message, and pgjdbc would eventually consume it. There's a glitch that "newly established connections would not know the maintenance is in progress".
3) As pgjdbc consumes that message, it will go into "unnamed statements" mode.
4) DB upgrade is done. Note: since no long-lived statements are in use, the probability of "cached plan must not change result types" kind of errors should be minimal
5) As the maintenance completes, DBA issues `NOTIFY 'pgjdbc', 'ok, you might use named statements again'`.




iqbalmd: @davecramer 
How to get field tests is by enabling the feature and getting reports back??
davecramer: @iqbalmd release it into the wild and see who uses it...

Patch:
diff --git a/pgjdbc/src/main/java/org/postgresql/PGConnection.java b/pgjdbc/src/main/java/org/postgresql/PGConnection.java
index 8099a3ca..95c4763e 100644
--- a/pgjdbc/src/main/java/org/postgresql/PGConnection.java
+++ b/pgjdbc/src/main/java/org/postgresql/PGConnection.java
@@ -10,6 +10,7 @@ package org.postgresql;
 
 import org.postgresql.copy.CopyManager;
 import org.postgresql.fastpath.Fastpath;
+import org.postgresql.jdbc.AutoSave;
 import org.postgresql.jdbc.PreferQueryMode;
 import org.postgresql.largeobject.LargeObjectManager;
 import org.postgresql.util.PGobject;
@@ -178,4 +179,20 @@ public interface PGConnection {
    * @return true if the connection is configured to use "simple 'Q' execute" commands only
    */
   PreferQueryMode getPreferQueryMode();
+
+
+  /**
+   * Connection configuration regarding automatic per-query savepoints.
+   *
+   * @see PGProperty#AUTOSAVE
+   * @return connection configuration regarding automatic per-query savepoints
+   */
+  AutoSave getAutosave();
+
+  /**
+   * Configures if connection should use automatic savepoints.
+   * @see PGProperty#AUTOSAVE
+   * @param autoSave connection configuration regarding automatic per-query savepoints
+   */
+  void setAutosave(AutoSave autoSave);
 }
diff --git a/pgjdbc/src/main/java/org/postgresql/PGProperty.java b/pgjdbc/src/main/java/org/postgresql/PGProperty.java
index afeedc13..3bccc0ed 100644
--- a/pgjdbc/src/main/java/org/postgresql/PGProperty.java
+++ b/pgjdbc/src/main/java/org/postgresql/PGProperty.java
@@ -369,6 +369,19 @@ public enum PGProperty {
           + "extendedCacheEveryting means use extended protocol and try cache every statement (including Statement.execute(String sql)) in a query cache.", false,
       "extended", "extendedForPrepared", "extendedCacheEveryting", "simple"),
 
+  /**
+   * Specifies what the driver should do if a query fails. In {@code autosave=always} mode, JDBC driver sets a safepoint before each query,
+   * and rolls back to that safepoint in case of failure. In {@code autosave=never} mode (default), no safepoint dance is made ever.
+   * In {@code autosave=conservative} mode, safepoint is set for each query, however the rollback is done only for rare cases
+   * like 'cached statement cannot change return type' or 'statement XXX is not valid' so JDBC driver rollsback and retries
+   */
+  AUTOSAVE("autosave", "never",
+      "Specifies what the driver should do if a query fails. In autosave=always mode, JDBC driver sets a safepoint before each query, "
+          + "and rolls back to that safepoint in case of failure. In autosave=never mode (default), no safepoint dance is made ever. "
+          + "In autosave=conservative mode, safepoint is set for each query, however the rollback is done only for rare cases"
+          + " like 'cached statement cannot change return type' or 'statement XXX is not valid' so JDBC driver rollsback and retries", false,
+      "always", "never", "conservative"),
+
   /**
    * Configure optimization to enable batch insert re-writing.
    */
diff --git a/pgjdbc/src/main/java/org/postgresql/core/BaseConnection.java b/pgjdbc/src/main/java/org/postgresql/core/BaseConnection.java
index 20755735..09ea6586 100644
--- a/pgjdbc/src/main/java/org/postgresql/core/BaseConnection.java
+++ b/pgjdbc/src/main/java/org/postgresql/core/BaseConnection.java
@@ -244,4 +244,13 @@ public interface BaseConnection extends PGConnection, Connection {
   CachedQuery createQuery(String sql, boolean escapeProcessing, boolean isParameterized,
       String... columnNames)
       throws SQLException;
+
+  /**
+   * By default, the connection resets statement cache in case deallocate all/discard all
+   * message is observed.
+   * This API allows to disable that feature for testing purposes.
+   *
+   * @param flushCacheOnDeallocate true if statement cache should be reset when "deallocate/discard" message observed
+   */
+  void setFlushCacheOnDeallocate(boolean flushCacheOnDeallocate);
 }
diff --git a/pgjdbc/src/main/java/org/postgresql/core/QueryExecutor.java b/pgjdbc/src/main/java/org/postgresql/core/QueryExecutor.java
index b050af7f..9fbc14b8 100644
--- a/pgjdbc/src/main/java/org/postgresql/core/QueryExecutor.java
+++ b/pgjdbc/src/main/java/org/postgresql/core/QueryExecutor.java
@@ -12,6 +12,7 @@ package org.postgresql.core;
 import org.postgresql.PGNotification;
 import org.postgresql.copy.CopyOperation;
 import org.postgresql.core.v3.TypeTransferModeRegistry;
+import org.postgresql.jdbc.AutoSave;
 import org.postgresql.jdbc.BatchResultHandler;
 import org.postgresql.jdbc.PreferQueryMode;
 import org.postgresql.util.HostSpec;
@@ -408,4 +409,19 @@ public interface QueryExecutor extends TypeTransferModeRegistry {
   boolean isColumnSanitiserDisabled();
 
   PreferQueryMode getPreferQueryMode();
+
+  AutoSave getAutoSave();
+
+  void setAutoSave(AutoSave autoSave);
+
+  boolean willHealOnRetry(SQLException e);
+
+  /**
+   * By default, the connection resets statement cache in case deallocate all/discard all
+   * message is observed.
+   * This API allows to disable that feature for testing purposes.
+   *
+   * @param flushCacheOnDeallocate true if statement cache should be reset when "deallocate/discard" message observed
+   */
+  void setFlushCacheOnDeallocate(boolean flushCacheOnDeallocate);
 }
diff --git a/pgjdbc/src/main/java/org/postgresql/core/QueryExecutorBase.java b/pgjdbc/src/main/java/org/postgresql/core/QueryExecutorBase.java
index e95ad0b1..686a970b 100644
--- a/pgjdbc/src/main/java/org/postgresql/core/QueryExecutorBase.java
+++ b/pgjdbc/src/main/java/org/postgresql/core/QueryExecutorBase.java
@@ -2,9 +2,13 @@ package org.postgresql.core;
 
 import org.postgresql.PGNotification;
 import org.postgresql.PGProperty;
+import org.postgresql.jdbc.AutoSave;
 import org.postgresql.jdbc.PreferQueryMode;
 import org.postgresql.util.HostSpec;
 import org.postgresql.util.LruCache;
+import org.postgresql.util.PSQLException;
+import org.postgresql.util.PSQLState;
+import org.postgresql.util.ServerErrorMessage;
 
 import java.io.IOException;
 import java.sql.SQLException;
@@ -28,6 +32,8 @@ public abstract class QueryExecutorBase implements QueryExecutor {
   private final boolean reWriteBatchedInserts;
   private final boolean columnSanitiserDisabled;
   private final PreferQueryMode preferQueryMode;
+  private AutoSave autoSave;
+  private boolean flushCacheOnDeallocate = true;
 
   // default value for server versions that don't report standard_conforming_strings
   private boolean standardConformingStrings = false;
@@ -49,6 +55,7 @@ public abstract class QueryExecutorBase implements QueryExecutor {
     this.columnSanitiserDisabled = PGProperty.DISABLE_COLUMN_SANITISER.getBoolean(info);
     String preferMode = PGProperty.PREFER_QUERY_MODE.get(info);
     this.preferQueryMode = PreferQueryMode.of(preferMode);
+    this.autoSave = AutoSave.of(PGProperty.AUTOSAVE.get(info));
     this.cachedQueryCreateAction = new CachedQueryCreateAction(this);
     statementCache = new LruCache<Object, CachedQuery>(
         Math.max(0, PGProperty.PREPARED_STATEMENT_CACHE_QUERIES.getInt(info)),
@@ -317,4 +324,55 @@ public abstract class QueryExecutorBase implements QueryExecutor {
   public PreferQueryMode getPreferQueryMode() {
     return preferQueryMode;
   }
+
+  public AutoSave getAutoSave() {
+    return autoSave;
+  }
+
+  public void setAutoSave(AutoSave autoSave) {
+    this.autoSave = autoSave;
+  }
+
+  protected boolean willHealViaReparse(SQLException e) {
+    // "prepared statement \"S_2\" does not exist"
+    if (PSQLState.INVALID_SQL_STATEMENT_NAME.getState().equals(e.getSQLState())) {
+      return true;
+    }
+    if (!PSQLState.NOT_IMPLEMENTED.getState().equals(e.getSQLState())) {
+      return false;
+    }
+
+    if (!(e instanceof PSQLException)) {
+      return false;
+    }
+
+    PSQLException pe = (PSQLException) e;
+
+    ServerErrorMessage serverErrorMessage = pe.getServerErrorMessage();
+    if (serverErrorMessage == null) {
+      return false;
+    }
+    // "cached plan must not change result type"
+    String routine = pe.getServerErrorMessage().getRoutine();
+    return "RevalidateCachedQuery".equals(routine) // 9.2+
+        || "RevalidateCachedPlan".equals(routine); // <= 9.1
+  }
+
+  @Override
+  public boolean willHealOnRetry(SQLException e) {
+    if (autoSave == AutoSave.NEVER && getTransactionState() == TransactionState.FAILED) {
+      // If autorollback is not activated, then every statement will fail with
+      // 'transaction is aborted', etc, etc
+      return false;
+    }
+    return willHealViaReparse(e);
+  }
+
+  public boolean isFlushCacheOnDeallocate() {
+    return flushCacheOnDeallocate;
+  }
+
+  public void setFlushCacheOnDeallocate(boolean flushCacheOnDeallocate) {
+    this.flushCacheOnDeallocate = flushCacheOnDeallocate;
+  }
 }
diff --git a/pgjdbc/src/main/java/org/postgresql/core/ResultHandler.java b/pgjdbc/src/main/java/org/postgresql/core/ResultHandler.java
index 34b391fc..b6f29c82 100644
--- a/pgjdbc/src/main/java/org/postgresql/core/ResultHandler.java
+++ b/pgjdbc/src/main/java/org/postgresql/core/ResultHandler.java
@@ -83,4 +83,16 @@ public interface ResultHandler {
    * statements are executed successfully and which are not.
    */
   void secureProgress();
+
+  /**
+   * Returns the first encountered exception. The rest are chained via {@link SQLException#setNextException(SQLException)}
+   * @return the first encountered exception
+   */
+  SQLException getException();
+
+  /**
+   * Returns the first encountered warning. The rest are chained via {@link SQLException#setNextException(SQLException)}
+   * @return the first encountered warning
+   */
+  SQLWarning getWarning();
 }
diff --git a/pgjdbc/src/main/java/org/postgresql/core/ResultHandlerBase.java b/pgjdbc/src/main/java/org/postgresql/core/ResultHandlerBase.java
new file mode 100644
index 00000000..2b87d0d4
--- /dev/null
+++ b/pgjdbc/src/main/java/org/postgresql/core/ResultHandlerBase.java
@@ -0,0 +1,78 @@
+/*-------------------------------------------------------------------------
+*
+* Copyright (c) 2016-2016, PostgreSQL Global Development Group
+*
+*
+*-------------------------------------------------------------------------
+*/
+
+package org.postgresql.core;
+
+import java.sql.SQLException;
+import java.sql.SQLWarning;
+import java.util.List;
+
+/**
+ * Empty implementation of {@link ResultHandler} interface.
+ * {@link SQLException#setNextException(SQLException)} has {@code O(N)} complexity,
+ * so this class tracks the last exception object to speedup {@code setNextException}.
+ */
+public class ResultHandlerBase implements ResultHandler {
+  // Last exception is tracked to avoid O(N) SQLException#setNextException just in case there
+  // will be lots of exceptions (e.g. all batch rows fail with constraint violation or so)
+  private SQLException firstException;
+  private SQLException lastException;
+
+  private SQLWarning firstWarning;
+  private SQLWarning lastWarning;
+
+  @Override
+  public void handleResultRows(Query fromQuery, Field[] fields, List<byte[][]> tuples,
+      ResultCursor cursor) {
+  }
+
+  @Override
+  public void handleCommandStatus(String status, int updateCount, long insertOID) {
+  }
+
+  @Override
+  public void secureProgress() {
+  }
+
+  @Override
+  public void handleWarning(SQLWarning warning) {
+    if (firstWarning == null) {
+      firstWarning = lastWarning = warning;
+      return;
+    }
+    lastWarning.setNextException(warning);
+    lastWarning = warning;
+  }
+
+  @Override
+  public void handleError(SQLException error) {
+    if (firstException == null) {
+      firstException = lastException = error;
+      return;
+    }
+    lastException.setNextException(error);
+    lastException = error;
+  }
+
+  @Override
+  public void handleCompletion() throws SQLException {
+    if (firstException != null) {
+      throw firstException;
+    }
+  }
+
+  @Override
+  public SQLException getException() {
+    return firstException;
+  }
+
+  @Override
+  public SQLWarning getWarning() {
+    return firstWarning;
+  }
+}
diff --git a/pgjdbc/src/main/java/org/postgresql/core/ResultHandlerDelegate.java b/pgjdbc/src/main/java/org/postgresql/core/ResultHandlerDelegate.java
index fe74960c..8c4ff03a 100644
--- a/pgjdbc/src/main/java/org/postgresql/core/ResultHandlerDelegate.java
+++ b/pgjdbc/src/main/java/org/postgresql/core/ResultHandlerDelegate.java
@@ -59,4 +59,20 @@ public class ResultHandlerDelegate implements ResultHandler {
       delegate.secureProgress();
     }
   }
+
+  @Override
+  public SQLException getException() {
+    if (delegate != null) {
+      return delegate.getException();
+    }
+    return null;
+  }
+
+  @Override
+  public SQLWarning getWarning() {
+    if (delegate != null) {
+      return delegate.getWarning();
+    }
+    return null;
+  }
 }
diff --git a/pgjdbc/src/main/java/org/postgresql/core/SetupQueryRunner.java b/pgjdbc/src/main/java/org/postgresql/core/SetupQueryRunner.java
index 3e611d6c..584b04fc 100644
--- a/pgjdbc/src/main/java/org/postgresql/core/SetupQueryRunner.java
+++ b/pgjdbc/src/main/java/org/postgresql/core/SetupQueryRunner.java
@@ -23,13 +23,9 @@ import java.util.List;
  */
 public class SetupQueryRunner {
 
-  private static class SimpleResultHandler implements ResultHandler {
-    private SQLException error;
+  private static class SimpleResultHandler extends ResultHandlerBase {
     private List<byte[][]> tuples;
 
-    SimpleResultHandler() {
-    }
-
     List<byte[][]> getResults() {
       return tuples;
     }
@@ -39,31 +35,10 @@ public class SetupQueryRunner {
       this.tuples = tuples;
     }
 
-    public void handleCommandStatus(String status, int updateCount, long insertOID) {
-    }
-
     public void handleWarning(SQLWarning warning) {
       // We ignore warnings. We assume we know what we're
       // doing in the setup queries.
     }
-
-    public void handleError(SQLException newError) {
-      if (error == null) {
-        error = newError;
-      } else {
-        error.setNextException(newError);
-      }
-    }
-
-    public void handleCompletion() throws SQLException {
-      if (error != null) {
-        throw error;
-      }
-    }
-
-    @Override
-    public void secureProgress() {
-    }
   }
 
   public static byte[][] run(QueryExecutor executor, String queryString,
diff --git a/pgjdbc/src/main/java/org/postgresql/core/SqlCommand.java b/pgjdbc/src/main/java/org/postgresql/core/SqlCommand.java
index bf1d3b7f..59ac4b08 100644
--- a/pgjdbc/src/main/java/org/postgresql/core/SqlCommand.java
+++ b/pgjdbc/src/main/java/org/postgresql/core/SqlCommand.java
@@ -18,6 +18,7 @@ import static org.postgresql.core.SqlCommandType.INSERT;
  *
  */
 public class SqlCommand {
+  public static final SqlCommand BLANK = SqlCommand.createStatementTypeInfo(SqlCommandType.BLANK);
 
   public boolean isBatchedReWriteCompatible() {
     return valuesBraceOpenPosition >= 0;
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 620f0bf5..3abc2bde 100644
--- a/pgjdbc/src/main/java/org/postgresql/core/v3/QueryExecutorImpl.java
+++ b/pgjdbc/src/main/java/org/postgresql/core/v3/QueryExecutorImpl.java
@@ -26,11 +26,13 @@ import org.postgresql.core.QueryExecutor;
 import org.postgresql.core.QueryExecutorBase;
 import org.postgresql.core.ResultCursor;
 import org.postgresql.core.ResultHandler;
+import org.postgresql.core.ResultHandlerBase;
 import org.postgresql.core.ResultHandlerDelegate;
 import org.postgresql.core.SqlCommand;
 import org.postgresql.core.SqlCommandType;
 import org.postgresql.core.TransactionState;
 import org.postgresql.core.Utils;
+import org.postgresql.jdbc.AutoSave;
 import org.postgresql.jdbc.BatchResultHandler;
 import org.postgresql.jdbc.TimestampUtils;
 import org.postgresql.util.GT;
@@ -91,6 +93,8 @@ public class QueryExecutorImpl extends QueryExecutorBase {
    */
   private final SimpleQuery sync = (SimpleQuery) createQuery("SYNC", false, true).query;
 
+  private short deallocateEpoch;
+
   public QueryExecutorImpl(PGStream pgStream, String user, String database,
       int cancelSignalTimeout, Properties info, Logger logger) throws SQLException, IOException {
     super(logger, pgStream, user, database, cancelSignalTimeout, info);
@@ -259,12 +263,13 @@ public class QueryExecutorImpl extends QueryExecutorBase {
       ((V3ParameterList) parameters).checkAllParametersSet();
     }
 
+    boolean autosave = false;
     try {
       try {
         handler = sendQueryPreamble(handler, flags);
-        ErrorTrackingResultHandler trackingHandler = new ErrorTrackingResultHandler(handler);
+        autosave = sendAutomaticSavepoint(query, flags);
         sendQuery(query, (V3ParameterList) parameters, maxRows, fetchSize, flags,
-            trackingHandler, null);
+            handler, null);
         if ((flags & QueryExecutor.QUERY_EXECUTE_AS_SIMPLE) != 0) {
           // Sync message is not required for 'Q' execution as 'Q' ends with ReadyForQuery message
           // on its own
@@ -302,7 +307,47 @@ public class QueryExecutorImpl extends QueryExecutorBase {
               PSQLState.CONNECTION_FAILURE, e));
     }
 
-    handler.handleCompletion();
+    try {
+      handler.handleCompletion();
+    } catch (SQLException e) {
+      rollbackIfRequired(autosave, e);
+    }
+  }
+
+  private boolean sendAutomaticSavepoint(Query query, int flags) throws IOException {
+    if (((flags & QueryExecutor.QUERY_SUPPRESS_BEGIN) == 0
+        || getTransactionState() == TransactionState.OPEN)
+        && query != restoreToAutoSave
+        && getAutoSave() != AutoSave.NEVER
+        // If query has no resulting fields, it cannot fail with 'cached plan must not change result type'
+        // thus no need to set a safepoint before such query
+        && (getAutoSave() == AutoSave.ALWAYS
+        // If CompositeQuery is observed, just assume it might fail and set the savepoint
+        || !(query instanceof SimpleQuery)
+        || ((SimpleQuery) query).getFields() != null)) {
+      sendOneQuery(autoSaveQuery, SimpleQuery.NO_PARAMETERS, 1, 0,
+          updateQueryMode(QUERY_NO_RESULTS | QUERY_NO_METADATA)
+              // PostgreSQL does not support bind, exec, simple, sync message flow,
+              // so we force autosavepoint to use simple if the main query is using simple
+              | (flags & QueryExecutor.QUERY_EXECUTE_AS_SIMPLE));
+      return true;
+    }
+    return false;
+  }
+
+  private void rollbackIfRequired(boolean autosave, SQLException e) throws SQLException {
+    if (autosave
+        && getTransactionState() == TransactionState.FAILED
+        && (getAutoSave() == AutoSave.ALWAYS || willHealOnRetry(e))) {
+      try {
+        execute(restoreToAutoSave, SimpleQuery.NO_PARAMETERS, new ResultHandlerDelegate(null),
+            1, 0, updateQueryMode(QUERY_NO_RESULTS | QUERY_NO_METADATA));
+      } catch (SQLException e2) {
+        // That's O(N), sorry
+        e.setNextException(e2);
+      }
+    }
+    throw e;
   }
 
   // Deadlock avoidance:
@@ -355,24 +400,6 @@ public class QueryExecutorImpl extends QueryExecutorBase {
   private static final int MAX_BUFFERED_RECV_BYTES = 64000;
   private static final int NODATA_QUERY_RESPONSE_SIZE_BYTES = 250;
 
-  // Helper handler that tracks error status.
-  private static class ErrorTrackingResultHandler extends ResultHandlerDelegate {
-    private boolean sawError = false;
-
-    ErrorTrackingResultHandler(ResultHandler delegateHandler) {
-      super(delegateHandler);
-    }
-
-    public void handleError(SQLException error) {
-      sawError = true;
-      super.handleError(error);
-    }
-
-    boolean hasErrors() {
-      return sawError;
-    }
-  }
-
   public synchronized void execute(Query[] queries, ParameterList[] parameterLists,
       BatchResultHandler batchHandler, int maxRows, int fetchSize, int flags) throws SQLException {
     waitOnLock();
@@ -393,10 +420,11 @@ public class QueryExecutorImpl extends QueryExecutorBase {
       }
     }
 
+    boolean autosave = false;
     ResultHandler handler = batchHandler;
     try {
       handler = sendQueryPreamble(batchHandler, flags);
-      ErrorTrackingResultHandler trackingHandler = new ErrorTrackingResultHandler(handler);
+      autosave = sendAutomaticSavepoint(queries[0], flags);
       estimatedReceiveBufferBytes = 0;
 
       for (int i = 0; i < queries.length; ++i) {
@@ -406,14 +434,14 @@ public class QueryExecutorImpl extends QueryExecutorBase {
           parameters = SimpleQuery.NO_PARAMETERS;
         }
 
-        sendQuery(query, parameters, maxRows, fetchSize, flags, trackingHandler, batchHandler);
+        sendQuery(query, parameters, maxRows, fetchSize, flags, handler, batchHandler);
 
-        if (trackingHandler.hasErrors()) {
+        if (handler.getException() != null) {
           break;
         }
       }
 
-      if (!trackingHandler.hasErrors()) {
+      if (handler.getException() == null) {
         if ((flags & QueryExecutor.QUERY_EXECUTE_AS_SIMPLE) != 0) {
           // Sync message is not required for 'Q' execution as 'Q' ends with ReadyForQuery message
           // on its own
@@ -430,7 +458,11 @@ public class QueryExecutorImpl extends QueryExecutorBase {
               PSQLState.CONNECTION_FAILURE, e));
     }
 
-    handler.handleCompletion();
+    try {
+      handler.handleCompletion();
+    } catch (SQLException e) {
+      rollbackIfRequired(autosave, e);
+    }
   }
 
   private ResultHandler sendQueryPreamble(final ResultHandler delegateHandler, int flags)
@@ -508,13 +540,8 @@ public class QueryExecutorImpl extends QueryExecutorBase {
         logger.debug("Issuing BEGIN before fastpath or copy call.");
       }
 
-      ResultHandler handler = new ResultHandlerDelegate(null) {
+      ResultHandler handler = new ResultHandlerBase() {
         private boolean sawBegin = false;
-        private SQLException sqle = null;
-
-        public void handleResultRows(Query fromQuery, Field[] fields, List<byte[][]> tuples,
-            ResultCursor cursor) {
-        }
 
         public void handleCommandStatus(String status, int updateCount, long insertOID) {
           if (!sawBegin) {
@@ -537,20 +564,6 @@ public class QueryExecutorImpl extends QueryExecutorBase {
           // them errors.
           handleError(warning);
         }
-
-        public void handleError(SQLException error) {
-          if (sqle == null) {
-            sqle = error;
-          } else {
-            sqle.setNextException(error);
-          }
-        }
-
-        public void handleCompletion() throws SQLException {
-          if (sqle != null) {
-            throw sqle;
-          }
-        }
       };
 
       try {
@@ -1204,7 +1217,7 @@ public class QueryExecutorImpl extends QueryExecutorBase {
    * See the comments above MAX_BUFFERED_RECV_BYTES's declaration for details.
    */
   private void flushIfDeadlockRisk(Query query, boolean disallowBatching,
-      ErrorTrackingResultHandler trackingHandler,
+      ResultHandler resultHandler,
       BatchResultHandler batchHandler,
       final int flags) throws IOException {
     // Assume all statements need at least this much reply buffer space,
@@ -1240,7 +1253,7 @@ public class QueryExecutorImpl extends QueryExecutorBase {
     if (disallowBatching || estimatedReceiveBufferBytes >= MAX_BUFFERED_RECV_BYTES) {
       logger.debug("Forcing Sync, receive buffer full or batching disallowed");
       sendSync();
-      processResults(trackingHandler, flags);
+      processResults(resultHandler, flags);
       estimatedReceiveBufferBytes = 0;
       if (batchHandler != null) {
         batchHandler.secureProgress();
@@ -1253,7 +1266,7 @@ public class QueryExecutorImpl extends QueryExecutorBase {
    * Send a query to the backend.
    */
   private void sendQuery(Query query, V3ParameterList parameters, int maxRows, int fetchSize,
-      int flags, ErrorTrackingResultHandler trackingHandler,
+      int flags, ResultHandler resultHandler,
       BatchResultHandler batchHandler) throws IOException, SQLException {
     // Now the query itself.
     Query[] subqueries = query.getSubqueries();
@@ -1265,20 +1278,20 @@ public class QueryExecutorImpl extends QueryExecutorBase {
     boolean disallowBatching = (flags & QueryExecutor.QUERY_DISALLOW_BATCHING) != 0;
 
     if (subqueries == null) {
-      flushIfDeadlockRisk(query, disallowBatching, trackingHandler, batchHandler, flags);
+      flushIfDeadlockRisk(query, disallowBatching, resultHandler, batchHandler, flags);
 
       // If we saw errors, don't send anything more.
-      if (!trackingHandler.hasErrors()) {
+      if (resultHandler.getException() == null) {
         sendOneQuery((SimpleQuery) query, (SimpleParameterList) parameters, maxRows, fetchSize,
             flags);
       }
     } else {
       for (int i = 0; i < subqueries.length; ++i) {
         final Query subquery = subqueries[i];
-        flushIfDeadlockRisk(subquery, disallowBatching, trackingHandler, batchHandler, flags);
+        flushIfDeadlockRisk(subquery, disallowBatching, resultHandler, batchHandler, flags);
 
         // If we saw errors, don't send anything more.
-        if (trackingHandler.hasErrors()) {
+        if (resultHandler.getException() != null) {
           break;
         }
 
@@ -1317,7 +1330,7 @@ public class QueryExecutorImpl extends QueryExecutorBase {
       throws IOException {
     // Already parsed, or we have a Parse pending and the types are right?
     int[] typeOIDs = params.getTypeOIDs();
-    if (query.isPreparedFor(typeOIDs)) {
+    if (query.isPreparedFor(typeOIDs, deallocateEpoch)) {
       return;
     }
 
@@ -1339,7 +1352,7 @@ public class QueryExecutorImpl extends QueryExecutorBase {
       // NB: Must clone the OID array, as it's a direct reference to
       // the SimpleParameterList's internal array that might be modified
       // under us.
-      query.setStatementName(statementName);
+      query.setStatementName(statementName, deallocateEpoch);
       query.setStatementTypes(typeOIDs.clone());
       registerParsedQuery(query, statementName);
     }
@@ -2017,6 +2030,10 @@ public class QueryExecutorImpl extends QueryExecutorBase {
         case 'C': // Command Status (end of Execute)
           // Handle status.
           String status = receiveCommandStatus();
+          if (isFlushCacheOnDeallocate()
+              && (status.startsWith("DEALLOCATE ALL") || status.startsWith("DISCARD ALL"))) {
+            deallocateEpoch++;
+          }
 
           doneAfterRowDescNoData = false;
 
@@ -2030,6 +2047,12 @@ public class QueryExecutorImpl extends QueryExecutorBase {
           } else {
             // For simple 'Q' queries, executeQueue is cleared via ReadyForQuery message
           }
+
+          if (currentQuery == autoSaveQuery) {
+            // ignore "SAVEPOINT" status from autosave query
+            break;
+          }
+
           Field[] fields = currentQuery.getFields();
           if (fields != null && !noResults && tuples == null) {
             tuples = new ArrayList<byte[][]>();
@@ -2110,7 +2133,16 @@ public class QueryExecutorImpl extends QueryExecutorBase {
           // Error Response (response to pretty much everything; backend then skips until Sync)
           SQLException error = receiveErrorResponse();
           handler.handleError(error);
-
+          if (willHealViaReparse(error)) {
+            // prepared statement ... is not valid kind of error
+            // Technically speaking, the error is unexpected, thus we invalidate other
+            // server-prepared statements just in case.
+            deallocateEpoch++;
+            if (logger.logDebug()) {
+              logger.debug(" FE: received " + error.getSQLState() + ", will invalidate statements. "
+                  + "deallocateEpoch is now " + deallocateEpoch);
+            }
+          }
           // keep processing
           break;
 
@@ -2657,13 +2689,22 @@ public class QueryExecutorImpl extends QueryExecutorBase {
 
   private final SimpleQuery beginTransactionQuery =
       new SimpleQuery(
-          new NativeQuery("BEGIN", new int[0], false,
-              SqlCommand.createStatementTypeInfo(SqlCommandType.BLANK)
-          ), null, false);
+          new NativeQuery("BEGIN", new int[0], false, SqlCommand.BLANK),
+          null, false);
 
   private final SimpleQuery EMPTY_QUERY =
       new SimpleQuery(
           new NativeQuery("", new int[0], false,
               SqlCommand.createStatementTypeInfo(SqlCommandType.BLANK)
           ), null, false);
+
+  private final SimpleQuery autoSaveQuery =
+      new SimpleQuery(
+          new NativeQuery("SAVEPOINT PGJDBC_AUTOSAVE", new int[0], false, SqlCommand.BLANK),
+          null, false);
+
+  private final SimpleQuery restoreToAutoSave =
+      new SimpleQuery(
+          new NativeQuery("ROLLBACK TO SAVEPOINT PGJDBC_AUTOSAVE", new int[0], false, SqlCommand.BLANK),
+          null, false);
 }
diff --git a/pgjdbc/src/main/java/org/postgresql/core/v3/SimpleQuery.java b/pgjdbc/src/main/java/org/postgresql/core/v3/SimpleQuery.java
index 85a48f61..11a44a53 100644
--- a/pgjdbc/src/main/java/org/postgresql/core/v3/SimpleQuery.java
+++ b/pgjdbc/src/main/java/org/postgresql/core/v3/SimpleQuery.java
@@ -110,10 +110,11 @@ class SimpleQuery implements Query {
     return nativeQuery.nativeSql;
   }
 
-  void setStatementName(String statementName) {
+  void setStatementName(String statementName, short deallocateEpoch) {
     assert statementName != null : "statement name should not be null";
     this.statementName = statementName;
     this.encodedStatementName = Utils.encodeUTF8(statementName);
+    this.deallocateEpoch = deallocateEpoch;
   }
 
   void setStatementTypes(int[] paramTypes) {
@@ -128,10 +129,13 @@ class SimpleQuery implements Query {
     return statementName;
   }
 
-  boolean isPreparedFor(int[] paramTypes) {
+  boolean isPreparedFor(int[] paramTypes, short deallocateEpoch) {
     if (statementName == null) {
       return false; // Not prepared.
     }
+    if (this.deallocateEpoch != deallocateEpoch) {
+      return false;
+    }
 
     assert preparedTypes == null || paramTypes.length == preparedTypes.length
         : String.format("paramTypes:%1$d preparedTypes:%2$d", paramTypes.length,
@@ -311,6 +315,7 @@ class SimpleQuery implements Query {
   private final boolean sanitiserDisabled;
   private PhantomReference<?> cleanupRef;
   private int[] preparedTypes;
+  private short deallocateEpoch;
 
   private Integer cachedMaxResultRowSize;
 
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 af765e39..776fd055 100644
--- a/pgjdbc/src/main/java/org/postgresql/ds/common/BaseDataSource.java
+++ b/pgjdbc/src/main/java/org/postgresql/ds/common/BaseDataSource.java
@@ -9,6 +9,7 @@
 package org.postgresql.ds.common;
 
 import org.postgresql.PGProperty;
+import org.postgresql.jdbc.AutoSave;
 import org.postgresql.jdbc.PreferQueryMode;
 import org.postgresql.util.GT;
 import org.postgresql.util.PSQLException;
@@ -1245,6 +1246,22 @@ public abstract class BaseDataSource implements Referenceable {
     PGProperty.PREFER_QUERY_MODE.set(properties, preferQueryMode.value());
   }
 
+  /**
+   * @see PGProperty#AUTOSAVE
+   * @return connection configuration regarding automatic per-query savepoints
+   */
+  public AutoSave getAutosave() {
+    return AutoSave.of(PGProperty.AUTOSAVE.get(properties));
+  }
+
+  /**
+   * @see PGProperty#AUTOSAVE
+   * @param autoSave connection configuration regarding automatic per-query savepoints
+   */
+  public void setAutosave(AutoSave autoSave) {
+    PGProperty.AUTOSAVE.set(properties, autoSave.value());
+  }
+
   /**
    * @see PGProperty#REWRITE_BATCHED_INSERTS
    * @return boolean indicating property is enabled or not.
diff --git a/pgjdbc/src/main/java/org/postgresql/jdbc/AutoSave.java b/pgjdbc/src/main/java/org/postgresql/jdbc/AutoSave.java
new file mode 100644
index 00000000..e45dbb67
--- /dev/null
+++ b/pgjdbc/src/main/java/org/postgresql/jdbc/AutoSave.java
@@ -0,0 +1,21 @@
+package org.postgresql.jdbc;
+
+public enum AutoSave {
+  NEVER,
+  ALWAYS,
+  CONSERVATIVE;
+
+  private final String value;
+
+  AutoSave() {
+    value = this.name().toLowerCase();
+  }
+
+  public String value() {
+    return value;
+  }
+
+  public static AutoSave of(String value) {
+    return valueOf(value.toUpperCase());
+  }
+}
diff --git a/pgjdbc/src/main/java/org/postgresql/jdbc/BatchResultHandler.java b/pgjdbc/src/main/java/org/postgresql/jdbc/BatchResultHandler.java
index df31d06d..634e243c 100644
--- a/pgjdbc/src/main/java/org/postgresql/jdbc/BatchResultHandler.java
+++ b/pgjdbc/src/main/java/org/postgresql/jdbc/BatchResultHandler.java
@@ -1,10 +1,18 @@
+/*-------------------------------------------------------------------------
+*
+* Copyright (c) 2016-2016, PostgreSQL Global Development Group
+*
+*
+*-------------------------------------------------------------------------
+*/
+
 package org.postgresql.jdbc;
 
 import org.postgresql.core.Field;
 import org.postgresql.core.ParameterList;
 import org.postgresql.core.Query;
 import org.postgresql.core.ResultCursor;
-import org.postgresql.core.ResultHandler;
+import org.postgresql.core.ResultHandlerBase;
 import org.postgresql.core.v3.BatchedQuery;
 import org.postgresql.util.GT;
 import org.postgresql.util.PSQLException;
@@ -22,12 +30,8 @@ import java.util.List;
 /**
  * Internal class, it is not a part of public API.
  */
-public class BatchResultHandler implements ResultHandler {
+public class BatchResultHandler extends ResultHandlerBase {
   private PgStatement pgStatement;
-  private BatchUpdateException batchException = null;
-  // Last exception is tracked to avoid O(N) SQLException#setNextException just in case there
-  // will be lots of exceptions (e.g. all batch rows fail with constraint violation or so)
-  private SQLException lastException;
   private int resultIndex = 0;
 
   private final Query[] queries;
@@ -78,7 +82,7 @@ public class BatchResultHandler implements ResultHandler {
       resultIndex--;
       // If exception thrown, no need to collect generated keys
       // Note: some generated keys might be secured in generatedKeys
-      if (updateCount > 0 && (batchException == null || isAutoCommit())) {
+      if (updateCount > 0 && (getException() == null || isAutoCommit())) {
         allGeneratedRows.add(latestGeneratedRows);
         if (generatedKeys == null) {
           generatedKeys = latestGeneratedKeysRs;
@@ -129,7 +133,7 @@ public class BatchResultHandler implements ResultHandler {
   }
 
   public void handleError(SQLException newError) {
-    if (batchException == null) {
+    if (getException() == null) {
       Arrays.fill(updateCounts, committedRows, updateCounts.length, Statement.EXECUTE_FAILED);
       if (allGeneratedRows != null) {
         allGeneratedRows.clear();
@@ -140,20 +144,19 @@ public class BatchResultHandler implements ResultHandler {
         queryString = queries[resultIndex].toString(parameterLists[resultIndex]);
       }
 
-      batchException = new BatchUpdateException(
+      super.handleError(new BatchUpdateException(
           GT.tr("Batch entry {0} {1} was aborted: {2}  Call getNextException to see the cause.",
               new Object[]{resultIndex, queryString, newError.getMessage()}),
-          newError.getSQLState(), uncompressUpdateCount());
-      lastException = batchException;
+          newError.getSQLState(), uncompressUpdateCount()));
     }
     resultIndex++;
 
-    lastException.setNextException(newError);
-    lastException = newError;
+    super.handleError(newError);
   }
 
   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.
diff --git a/pgjdbc/src/main/java/org/postgresql/jdbc/CallableBatchResultHandler.java b/pgjdbc/src/main/java/org/postgresql/jdbc/CallableBatchResultHandler.java
index f8942168..3e49405b 100644
--- a/pgjdbc/src/main/java/org/postgresql/jdbc/CallableBatchResultHandler.java
+++ b/pgjdbc/src/main/java/org/postgresql/jdbc/CallableBatchResultHandler.java
@@ -1,3 +1,11 @@
+/*-------------------------------------------------------------------------
+*
+* Copyright (c) 2016-2016, PostgreSQL Global Development Group
+*
+*
+*-------------------------------------------------------------------------
+*/
+
 package org.postgresql.jdbc;
 
 import org.postgresql.core.Field;
diff --git a/pgjdbc/src/main/java/org/postgresql/jdbc/PgConnection.java b/pgjdbc/src/main/java/org/postgresql/jdbc/PgConnection.java
index 2d7d600a..ffa4c6bf 100644
--- a/pgjdbc/src/main/java/org/postgresql/jdbc/PgConnection.java
+++ b/pgjdbc/src/main/java/org/postgresql/jdbc/PgConnection.java
@@ -17,14 +17,12 @@ import org.postgresql.core.BaseStatement;
 import org.postgresql.core.CachedQuery;
 import org.postgresql.core.ConnectionFactory;
 import org.postgresql.core.Encoding;
-import org.postgresql.core.Field;
 import org.postgresql.core.Logger;
 import org.postgresql.core.Oid;
 import org.postgresql.core.Provider;
 import org.postgresql.core.Query;
 import org.postgresql.core.QueryExecutor;
-import org.postgresql.core.ResultCursor;
-import org.postgresql.core.ResultHandler;
+import org.postgresql.core.ResultHandlerBase;
 import org.postgresql.core.ServerVersion;
 import org.postgresql.core.SqlCommand;
 import org.postgresql.core.TransactionState;
@@ -64,7 +62,6 @@ import java.sql.Types;
 import java.util.Enumeration;
 import java.util.HashMap;
 import java.util.HashSet;
-import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.NoSuchElementException;
@@ -141,6 +138,8 @@ public class PgConnection implements BaseConnection {
   // Only instantiated if a task is actually scheduled.
   private volatile Timer cancelTimer = null;
 
+  private PreparedStatement checkConnectionQuery;
+
   private final LruCache<FieldMetadata.Key, FieldMetadata> fieldMetadataCache;
 
   final CachedQuery borrowQuery(String sql) throws SQLException {
@@ -166,6 +165,11 @@ public class PgConnection implements BaseConnection {
     queryExecutor.releaseQuery(cachedQuery);
   }
 
+  @Override
+  public void setFlushCacheOnDeallocate(boolean flushCacheOnDeallocate) {
+    queryExecutor.setFlushCacheOnDeallocate(flushCacheOnDeallocate);
+  }
+
   //
   // Ctor.
   //
@@ -769,7 +773,17 @@ public class PgConnection implements BaseConnection {
       flags |= QueryExecutor.QUERY_ONESHOT;
     }
 
-    getQueryExecutor().execute(query, null, new TransactionCommandHandler(), 0, 0, flags);
+    try {
+      getQueryExecutor().execute(query, null, new TransactionCommandHandler(), 0, 0, flags);
+    } catch (SQLException e) {
+      // Don't retry composite queries as it might get partially executed
+      if (query.getSubqueries() != null || !queryExecutor.willHealOnRetry(e)) {
+        throw e;
+      }
+      query.close();
+      // retry
+      getQueryExecutor().execute(query, null, new TransactionCommandHandler(), 0, 0, flags);
+    }
   }
 
   public void commit() throws SQLException {
@@ -1047,36 +1061,13 @@ public class PgConnection implements BaseConnection {
   /**
    * Handler for transaction queries
    */
-  private class TransactionCommandHandler implements ResultHandler {
-    private SQLException error;
-
-    public void handleResultRows(Query fromQuery, Field[] fields, List<byte[][]> tuples,
-        ResultCursor cursor) {
-    }
-
-    public void handleCommandStatus(String status, int updateCount, long insertOID) {
-    }
-
-    public void handleWarning(SQLWarning warning) {
-      PgConnection.this.addWarning(warning);
-    }
-
-    public void handleError(SQLException newError) {
-      if (error == null) {
-        error = newError;
-      } else {
-        error.setNextException(newError);
-      }
-    }
-
+  private class TransactionCommandHandler extends ResultHandlerBase {
     public void handleCompletion() throws SQLException {
-      if (error != null) {
-        throw error;
+      SQLWarning warning = getWarning();
+      if (warning != null) {
+        PgConnection.this.addWarning(warning);
       }
-    }
-
-    @Override
-    public void secureProgress() {
+      super.handleCompletion();
     }
   }
 
@@ -1156,6 +1147,16 @@ public class PgConnection implements BaseConnection {
     return queryExecutor.getPreferQueryMode();
   }
 
+  @Override
+  public AutoSave getAutosave() {
+    return queryExecutor.getAutoSave();
+  }
+
+  @Override
+  public void setAutosave(AutoSave autoSave) {
+    queryExecutor.setAutoSave(autoSave);
+  }
+
   protected void abort() {
     queryExecutor.abort();
   }
@@ -1336,33 +1337,28 @@ public class PgConnection implements BaseConnection {
   }
 
   public boolean isValid(int timeout) throws SQLException {
-    if (isClosed()) {
-      return false;
-    }
     if (timeout < 0) {
       throw new PSQLException(GT.tr("Invalid timeout ({0}<0).", timeout),
           PSQLState.INVALID_PARAMETER_VALUE);
     }
-    boolean valid = false;
-    Statement stmt = null;
+    if (isClosed()) {
+      return false;
+    }
     try {
-      if (!isClosed()) {
-        stmt = createStatement();
-        stmt.setQueryTimeout(timeout);
-        stmt.executeUpdate("");
-        valid = true;
+      if (checkConnectionQuery == null) {
+        checkConnectionQuery = prepareStatement("");
       }
+      checkConnectionQuery.setQueryTimeout(timeout);
+      checkConnectionQuery.executeUpdate();
+      return true;
     } catch (SQLException e) {
-      getLogger().log(GT.tr("Validating connection."), e);
-    } finally {
-      if (stmt != null) {
-        try {
-          stmt.close();
-        } catch (Exception ex) {
-        }
+      if (PSQLState.IN_FAILED_SQL_TRANSACTION.getState().equals(e.getSQLState())) {
+        // "current transaction aborted", assume the connection is up and running
+        return true;
       }
+      getLogger().log(GT.tr("Validating connection."), e);
     }
-    return valid;
+    return false;
   }
 
   public void setClientInfo(String name, String value) throws SQLClientInfoException {
diff --git a/pgjdbc/src/main/java/org/postgresql/jdbc/PgResultSet.java b/pgjdbc/src/main/java/org/postgresql/jdbc/PgResultSet.java
index 916bac61..fe4fab66 100644
--- a/pgjdbc/src/main/java/org/postgresql/jdbc/PgResultSet.java
+++ b/pgjdbc/src/main/java/org/postgresql/jdbc/PgResultSet.java
@@ -16,7 +16,7 @@ import org.postgresql.core.Field;
 import org.postgresql.core.Oid;
 import org.postgresql.core.Query;
 import org.postgresql.core.ResultCursor;
-import org.postgresql.core.ResultHandler;
+import org.postgresql.core.ResultHandlerBase;
 import org.postgresql.core.ServerVersion;
 import org.postgresql.core.TypeInfo;
 import org.postgresql.core.Utils;
@@ -1751,8 +1751,7 @@ public class PgResultSet implements ResultSet, org.postgresql.PGRefCursorResultS
     }
   }
 
-  public class CursorResultHandler implements ResultHandler {
-    private SQLException error;
+  public class CursorResultHandler extends ResultHandlerBase {
 
     public void handleResultRows(Query fromQuery, Field[] fields, List<byte[][]> tuples,
         ResultCursor cursor) {
@@ -1765,31 +1764,15 @@ public class PgResultSet implements ResultSet, org.postgresql.PGRefCursorResultS
           PSQLState.PROTOCOL_VIOLATION));
     }
 
-    public void handleWarning(SQLWarning warning) {
-      PgResultSet.this.addWarning(warning);
-    }
-
-    public void handleError(SQLException newError) {
-      if (error == null) {
-        error = newError;
-      } else {
-        error.setNextException(newError);
-      }
-    }
-
     public void handleCompletion() throws SQLException {
-      if (error != null) {
-        throw error;
+      SQLWarning warning = getWarning();
+      if (warning != null) {
+        PgResultSet.this.addWarning(warning);
       }
-    }
-
-    @Override
-    public void secureProgress() {
+      super.handleCompletion();
     }
   }
 
-  ;
-
 
   public BaseStatement getPGStatement() {
     return statement;
diff --git a/pgjdbc/src/main/java/org/postgresql/jdbc/PgStatement.java b/pgjdbc/src/main/java/org/postgresql/jdbc/PgStatement.java
index 2876db7a..8014a679 100644
--- a/pgjdbc/src/main/java/org/postgresql/jdbc/PgStatement.java
+++ b/pgjdbc/src/main/java/org/postgresql/jdbc/PgStatement.java
@@ -18,7 +18,7 @@ import org.postgresql.core.ParameterList;
 import org.postgresql.core.Query;
 import org.postgresql.core.QueryExecutor;
 import org.postgresql.core.ResultCursor;
-import org.postgresql.core.ResultHandler;
+import org.postgresql.core.ResultHandlerBase;
 import org.postgresql.core.SqlCommand;
 import org.postgresql.util.GT;
 import org.postgresql.util.PSQLException;
@@ -190,9 +190,9 @@ public class PgStatement implements Statement, BaseStatement {
   /**
    * ResultHandler implementations for updates, queries, and either-or.
    */
-  public class StatementResultHandler implements ResultHandler {
-    private SQLException error;
+  public class StatementResultHandler extends ResultHandlerBase {
     private ResultWrapper results;
+    private ResultWrapper lastResult;
 
     ResultWrapper getResults() {
       return results;
@@ -200,12 +200,13 @@ public class PgStatement implements Statement, BaseStatement {
 
     private void append(ResultWrapper newResult) {
       if (results == null) {
-        results = newResult;
+        lastResult = results = newResult;
       } else {
-        results.append(newResult);
+        lastResult.append(newResult);
       }
     }
 
+    @Override
     public void handleResultRows(Query fromQuery, Field[] fields, List<byte[][]> tuples,
         ResultCursor cursor) {
       try {
@@ -216,30 +217,18 @@ public class PgStatement implements Statement, BaseStatement {
       }
     }
 
+    @Override
     public void handleCommandStatus(String status, int updateCount, long insertOID) {
       append(new ResultWrapper(updateCount, insertOID));
     }
 
-    public void handleWarning(SQLWarning warning) {
-      PgStatement.this.addWarning(warning);
-    }
-
-    public void handleError(SQLException newError) {
-      if (error == null) {
-        error = newError;
-      } else {
-        error.setNextException(newError);
-      }
-    }
-
+    @Override
     public void handleCompletion() throws SQLException {
-      if (error != null) {
-        throw error;
+      SQLWarning warning = getWarning();
+      if (warning != null) {
+        PgStatement.this.addWarning(warning);
       }
-    }
-
-    @Override
-    public void secureProgress() {
+      super.handleCompletion();
     }
   }
 
@@ -363,7 +352,23 @@ public class PgStatement implements Statement, BaseStatement {
     return false;
   }
 
-  protected void execute(CachedQuery cachedQuery, ParameterList queryParameters, int flags)
+  protected final void execute(CachedQuery cachedQuery, ParameterList queryParameters, int flags)
+      throws SQLException {
+    try {
+      executeInternal(cachedQuery, queryParameters, flags);
+    } catch (SQLException e) {
+      // Don't retry composite queries as it might get partially executed
+      if (cachedQuery.query.getSubqueries() != null
+          || !connection.getQueryExecutor().willHealOnRetry(e)) {
+        throw e;
+      }
+      cachedQuery.query.close();
+      // Execute the query one more time
+      executeInternal(cachedQuery, queryParameters, flags);
+    }
+  }
+
+  private void executeInternal(CachedQuery cachedQuery, ParameterList queryParameters, int flags)
       throws SQLException {
     closeForNextExecution();
 
diff --git a/pgjdbc/src/main/java/org/postgresql/util/PSQLState.java b/pgjdbc/src/main/java/org/postgresql/util/PSQLState.java
index 3b402292..a0a98645 100644
--- a/pgjdbc/src/main/java/org/postgresql/util/PSQLState.java
+++ b/pgjdbc/src/main/java/org/postgresql/util/PSQLState.java
@@ -83,6 +83,7 @@ public class PSQLState implements java.io.Serializable {
   public final static PSQLState NUMERIC_VALUE_OUT_OF_RANGE = new PSQLState("22003");
   public final static PSQLState BAD_DATETIME_FORMAT = new PSQLState("22007");
   public final static PSQLState DATETIME_OVERFLOW = new PSQLState("22008");
+  public final static PSQLState DIVISION_BY_ZERO = new PSQLState("22012");
   public final static PSQLState MOST_SPECIFIC_TYPE_DOES_NOT_MATCH = new PSQLState("2200G");
   public final static PSQLState INVALID_PARAMETER_VALUE = new PSQLState("22023");
 
@@ -91,6 +92,9 @@ public class PSQLState implements java.io.Serializable {
   public final static PSQLState TRANSACTION_STATE_INVALID = new PSQLState("25000");
   public final static PSQLState ACTIVE_SQL_TRANSACTION = new PSQLState("25001");
   public final static PSQLState NO_ACTIVE_SQL_TRANSACTION = new PSQLState("25P01");
+  public final static PSQLState IN_FAILED_SQL_TRANSACTION = new PSQLState("25P02");
+
+  public final static PSQLState INVALID_SQL_STATEMENT_NAME = new PSQLState("26000");
   public final static PSQLState INVALID_AUTHORIZATION_SPECIFICATION = new PSQLState("28000");
 
   public final static PSQLState STATEMENT_NOT_ALLOWED_IN_FUNCTION_CALL = new PSQLState("2F003");
diff --git a/ubenchmark/src/main/java/org/postgresql/benchmark/statement/ParseStatement.java b/ubenchmark/src/main/java/org/postgresql/benchmark/statement/ParseStatement.java
index dfa804e7..d0caedfb 100644
--- a/ubenchmark/src/main/java/org/postgresql/benchmark/statement/ParseStatement.java
+++ b/ubenchmark/src/main/java/org/postgresql/benchmark/statement/ParseStatement.java
@@ -50,6 +50,9 @@ public class ParseStatement {
 
   private Connection connection;
 
+  @Param({"conservative"})
+  private String autoSave;
+
   private String sql;
 
   private int cntr;
@@ -57,8 +60,16 @@ public class ParseStatement {
   @Setup(Level.Trial)
   public void setUp() throws SQLException {
     Properties props = ConnectionUtil.getProperties();
+    props.put("autosave", autoSave);
 
     connection = DriverManager.getConnection(ConnectionUtil.getURL(), props);
+
+    // Start transaction
+    Statement st = connection.createStatement();
+    connection.setAutoCommit(false);
+    st.execute("BEGIN");
+    st.close();
+
     StringBuilder sb = new StringBuilder();
     sb.append("SELECT ");
     for (int i = 0; i < bindCount; i++) {


Test patch:
diff --git a/pgjdbc/src/test/java/org/postgresql/test/jdbc2/AutoRollbackTestSuite.java b/pgjdbc/src/test/java/org/postgresql/test/jdbc2/AutoRollbackTestSuite.java
new file mode 100644
index 00000000..4a3ea6e5
--- /dev/null
+++ b/pgjdbc/src/test/java/org/postgresql/test/jdbc2/AutoRollbackTestSuite.java
@@ -0,0 +1,339 @@
+package org.postgresql.test.jdbc2;
+
+import org.postgresql.PGConnection;
+import org.postgresql.PGProperty;
+import org.postgresql.core.BaseConnection;
+import org.postgresql.core.ResultHandler;
+import org.postgresql.core.TransactionState;
+import org.postgresql.jdbc.AutoSave;
+import org.postgresql.jdbc.PgConnection;
+import org.postgresql.jdbc.PreferQueryMode;
+import org.postgresql.test.TestUtil;
+import org.postgresql.util.PSQLState;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.EnumSet;
+import java.util.Properties;
+
+@RunWith(Parameterized.class)
+public class AutoRollbackTestSuite extends BaseTest4 {
+
+  private enum FailMode {
+    /**
+     * Executes "select 1/0" and causes transaction failure (if autocommit=no).
+     * Mitigation: "autosave=always" or "autocommit=true"
+     */
+    SELECT,
+    /**
+     * Executes "alter table rollbacktest", thus it breaks a prepared select over that table
+     * Mitigation: "autosave in (always, conservative)"
+     */
+    ALTER,
+    /**
+     * Executes DEALLOCATE ALL
+     * Mitigation:
+     *  1) QueryExecutor tracks "DEALLOCATE ALL" responses ({@see org.postgresql.core.QueryExecutor#setFlushCacheOnDeallocate(boolean)}
+     *  2) QueryExecutor tracks "prepared statement name is invalid" and unprepares relevant statements ({@link org.postgresql.core.v3.QueryExecutorImpl#processResults(ResultHandler, int)}
+     *  3) "autosave in (always, conservative)"
+     *  4) Non-transactional cases are healed by retry (when no transaction present, just retry is possible)
+     */
+    DEALLOCATE,
+    /**
+     * Executes DISCARD ALL
+     * Mitigation: the same as for {@link #DEALLOCATE}
+     */
+    DISCARD,
+    /**
+     * Executes "insert ... select 1/0" in a batch statement, thus causing the transaction to fail.
+     */
+    INSERT_BATCH,
+  }
+
+  private final static EnumSet<FailMode> DEALLOCATES =
+      EnumSet.of(FailMode.DEALLOCATE, FailMode.DISCARD);
+
+  private final static EnumSet<FailMode> TRANS_KILLERS =
+      EnumSet.of(FailMode.SELECT, FailMode.INSERT_BATCH);
+
+  private enum ContinueMode {
+    COMMIT,
+    IS_VALID,
+    SELECT,
+  }
+
+  private final AutoSave autoSave;
+  private final AutoCommit autoCommit;
+  private final FailMode failMode;
+  private final ContinueMode continueMode;
+  private final boolean flushCacheOnDeallocate;
+
+  public AutoRollbackTestSuite(AutoSave autoSave, AutoCommit autoCommit,
+      FailMode failMode, ContinueMode continueMode, boolean flushCacheOnDeallocate) {
+    this.autoSave = autoSave;
+    this.autoCommit = autoCommit;
+    this.failMode = failMode;
+    this.continueMode = continueMode;
+    this.flushCacheOnDeallocate = flushCacheOnDeallocate;
+  }
+
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+    TestUtil.createTable(con, "rollbacktest", "a int, str text");
+    con.setAutoCommit(autoCommit == AutoCommit.YES);
+    BaseConnection baseConnection = con.unwrap(BaseConnection.class);
+    baseConnection.setFlushCacheOnDeallocate(flushCacheOnDeallocate);
+  }
+
+  @Override
+  public void tearDown() throws SQLException {
+    try {
+      con.setAutoCommit(true);
+      TestUtil.dropTable(con, "rollbacktest");
+    } catch (Exception e) {
+      e.printStackTrace();
+    }
+    super.tearDown();
+  }
+
+  @Override
+  protected void updateProperties(Properties props) {
+    super.updateProperties(props);
+    PGProperty.AUTOSAVE.set(props, autoSave.value());
+    PGProperty.PREPARE_THRESHOLD.set(props, 1);
+  }
+
+
+  @Parameterized.Parameters(name = "{index}: autorollback(autoSave={0}, autoCommit={1}, failMode={2}, continueMode={3}, flushOnDeallocate={4})")
+  public static Iterable<Object[]> data() {
+    Collection<Object[]> ids = new ArrayList<Object[]>();
+    boolean[] booleans = new boolean[] {true, false};
+    for (AutoSave autoSave : AutoSave.values()) {
+      for (AutoCommit autoCommit : AutoCommit.values()) {
+        for (FailMode failMode : FailMode.values()) {
+          // ERROR: DISCARD ALL cannot run inside a transaction block
+          if (failMode == FailMode.DISCARD && autoCommit == AutoCommit.NO) {
+            continue;
+          }
+          for (ContinueMode continueMode : ContinueMode.values()) {
+            if (failMode == FailMode.ALTER && continueMode != ContinueMode.SELECT) {
+              continue;
+            }
+            for (boolean flushCacheOnDeallocate : booleans) {
+              if (!(flushCacheOnDeallocate || DEALLOCATES.contains(failMode))) {
+                continue;
+              }
+
+              ids.add(new Object[]{autoSave, autoCommit, failMode, continueMode, flushCacheOnDeallocate});
+            }
+          }
+        }
+      }
+    }
+    return ids;
+  }
+
+
+  @Test
+  public void run() throws SQLException {
+    if (continueMode == ContinueMode.IS_VALID) {
+      // make "isValid" a server-prepared statement
+      con.isValid(4);
+    } else if (continueMode == ContinueMode.COMMIT) {
+      doCommit();
+    } else if (continueMode == ContinueMode.SELECT) {
+      assertRows("rollbacktest", 0);
+    }
+
+    Statement statement = con.createStatement();
+    statement.executeUpdate("insert into rollbacktest(a, str) values (0, 'test')");
+
+    PreparedStatement ps = con.prepareStatement("select * from rollbacktest");
+    // Server-prepare the statement
+    ps.executeQuery().close();
+
+    switch (failMode) {
+      case SELECT:
+        try {
+          statement.execute("select 1/0");
+          Assert.fail("select 1/0 should fail");
+        } catch (SQLException e) {
+          Assert.assertEquals("division by zero expected",
+              PSQLState.DIVISION_BY_ZERO.getState(), e.getSQLState());
+        }
+        break;
+      case DEALLOCATE:
+        statement.executeUpdate("DEALLOCATE ALL");
+        break;
+      case DISCARD:
+        statement.executeUpdate("DISCARD ALL");
+        break;
+      case ALTER:
+        statement.executeUpdate("alter table rollbacktest add q int");
+        break;
+      case INSERT_BATCH:
+        try {
+          statement.addBatch("insert into rollbacktest(a, str) values (1/0, 'test')");
+          statement.executeBatch();
+          Assert.fail("select 1/0 should fail");
+        } catch (SQLException e) {
+          Assert.assertEquals("division by zero expected",
+              PSQLState.DIVISION_BY_ZERO.getState(), e.getSQLState());
+        }
+        break;
+      default:
+        Assert.fail("Fail mode " + failMode + " is not implemented");
+    }
+
+    PgConnection pgConnection = con.unwrap(PgConnection.class);
+    if (autoSave == AutoSave.ALWAYS) {
+      Assert.assertNotEquals("In AutoSave.ALWAYS, transaction should not fail",
+          TransactionState.FAILED, pgConnection.getTransactionState());
+    }
+    if (autoCommit == AutoCommit.NO) {
+      Assert.assertNotEquals("AutoCommit == NO, thus transaction should be active (open or failed)",
+          TransactionState.IDLE, pgConnection.getTransactionState());
+    }
+    statement.close();
+
+    switch (continueMode) {
+      case COMMIT:
+        try {
+          doCommit();
+          // No assert here: commit should always succeed with exception of well known failure cases in catch
+        } catch (SQLException e) {
+          if (!flushCacheOnDeallocate && DEALLOCATES.contains(failMode)
+              && autoSave == AutoSave.NEVER) {
+            Assert.assertEquals(
+                "flushCacheOnDeallocate is disabled, thus " + failMode + " should cause 'prepared statement \"...\" does not exist'"
+                    + " error message is " + e.getMessage(),
+                PSQLState.INVALID_SQL_STATEMENT_NAME.getState(), e.getSQLState());
+            return;
+          }
+          throw e;
+        }
+        return;
+      case IS_VALID:
+        if (!flushCacheOnDeallocate && autoSave == AutoSave.NEVER
+            && DEALLOCATES.contains(failMode) && autoCommit == AutoCommit.NO
+            && con.unwrap(PGConnection.class).getPreferQueryMode() != PreferQueryMode.SIMPLE) {
+          Assert.assertFalse("Connection.isValid should return false since failMode=" + failMode
+              + ", flushCacheOnDeallocate=false, and autosave=NEVER",
+              con.isValid(4));
+        } else {
+          Assert.assertTrue("Connection.isValid should return true unless the connection is closed",
+              con.isValid(4));
+        }
+        return;
+      default:
+        break;
+    }
+
+    try {
+      // Try execute server-prepared statement again
+      ps.executeQuery().close();
+      executeSqlSuccess();
+    } catch (SQLException e) {
+      if (autoSave != AutoSave.ALWAYS && TRANS_KILLERS.contains(failMode) && autoCommit == AutoCommit.NO) {
+        Assert.assertEquals(
+            "AutoSave==" + autoSave + ", thus statements should fail with 'current transaction is aborted...', "
+                + " error message is " + e.getMessage(),
+            PSQLState.IN_FAILED_SQL_TRANSACTION.getState(), e.getSQLState());
+        return;
+      }
+
+      if (autoSave == AutoSave.NEVER && autoCommit == AutoCommit.NO) {
+        if (DEALLOCATES.contains(failMode) && !flushCacheOnDeallocate) {
+          Assert.assertEquals(
+              "flushCacheOnDeallocate is disabled, thus " + failMode + " should cause 'prepared statement \"...\" does not exist'"
+                  + " error message is " + e.getMessage(),
+              PSQLState.INVALID_SQL_STATEMENT_NAME.getState(), e.getSQLState());
+        } else if (failMode == FailMode.ALTER) {
+          Assert.assertEquals(
+              "AutoSave==NEVER, autocommit=NO, thus ALTER TABLE causes SELECT * to fail with "
+                  + "'cached plan must not change result type', "
+                  + " error message is " + e.getMessage(),
+              PSQLState.NOT_IMPLEMENTED.getState(), e.getSQLState());
+        } else {
+          throw e;
+        }
+      } else {
+        throw e;
+      }
+    }
+
+
+    try {
+      assertRows("rollbacktest", 1);
+      executeSqlSuccess();
+    } catch (SQLException e) {
+      if (autoSave == AutoSave.NEVER && autoCommit == AutoCommit.NO) {
+        if (DEALLOCATES.contains(failMode) && !flushCacheOnDeallocate
+            || failMode == FailMode.ALTER) {
+          // The above statement failed with "prepared statement does not exist", thus subsequent one should fail with
+          // transaction aborted.
+          Assert.assertEquals(
+              "AutoSave==NEVER, thus statements should fail with 'current transaction is aborted...', "
+                  + " error message is " + e.getMessage(),
+              PSQLState.IN_FAILED_SQL_TRANSACTION.getState(), e.getSQLState());
+        }
+      } else {
+        throw e;
+      }
+    }
+  }
+
+  private void executeSqlSuccess() throws SQLException {
+    if (autoCommit == AutoCommit.YES) {
+      // in autocommit everything should just work
+    } else if (TRANS_KILLERS.contains(failMode)) {
+      if (autoSave != AutoSave.ALWAYS) {
+        Assert.fail(
+            "autosave= " + autoSave + " != ALWAYS, thus the transaction should be killed");
+      }
+    } else if (DEALLOCATES.contains(failMode)) {
+      if (autoSave == AutoSave.NEVER && !flushCacheOnDeallocate
+          && con.unwrap(PGConnection.class).getPreferQueryMode() != PreferQueryMode.SIMPLE) {
+        Assert.fail("flushCacheOnDeallocate == false, thus DEALLOCATE ALL should kill the transaction");
+      }
+    } else if (failMode == FailMode.ALTER) {
+      if (autoSave == AutoSave.NEVER
+          && con.unwrap(PGConnection.class).getPreferQueryMode() != PreferQueryMode.SIMPLE) {
+        Assert.fail("autosave=NEVER, thus the transaction should be killed");
+      }
+    } else {
+      Assert.fail("It is not specified why the test should pass, thus marking a failure");
+    }
+  }
+
+  private void assertRows(String tableName, int nrows) throws SQLException {
+    Statement st = con.createStatement();
+    ResultSet rs = st.executeQuery("select count(*) from " + tableName);
+    rs.next();
+    Assert.assertEquals("Table " + tableName, nrows, rs.getInt(1));
+  }
+
+  private void doCommit() throws SQLException {
+    // Such a dance is required since "commit" checks "current transaction state",
+    // so we need some pending changes, so "commit" query would be sent to the database
+    if (con.getAutoCommit()) {
+      con.setAutoCommit(false);
+      Statement st = con.createStatement();
+      st.executeUpdate(
+          "insert into rollbacktest(a, str) values (42, '" + System.currentTimeMillis() + "')");
+      st.close();
+    }
+    con.commit();
+    con.setAutoCommit(autoCommit == AutoCommit.YES);
+  }
+}
diff --git a/pgjdbc/src/test/java/org/postgresql/test/jdbc2/PreparedStatementTest.java b/pgjdbc/src/test/java/org/postgresql/test/jdbc2/PreparedStatementTest.java
index d6350d88..d571bce7 100644
--- a/pgjdbc/src/test/java/org/postgresql/test/jdbc2/PreparedStatementTest.java
+++ b/pgjdbc/src/test/java/org/postgresql/test/jdbc2/PreparedStatementTest.java
@@ -60,6 +60,7 @@ public class PreparedStatementTest extends BaseTest4 {
     TestUtil.createTable(con, "streamtable", "bin bytea, str text");
     TestUtil.createTable(con, "texttable", "ch char(3), te text, vc varchar(3)");
     TestUtil.createTable(con, "intervaltable", "i interval");
+    TestUtil.createTable(con, "inttable", "a int");
   }
 
   @Override
@@ -67,6 +68,7 @@ public class PreparedStatementTest extends BaseTest4 {
     TestUtil.dropTable(con, "streamtable");
     TestUtil.dropTable(con, "texttable");
     TestUtil.dropTable(con, "intervaltable");
+    TestUtil.dropTable(con, "inttable");
     super.tearDown();
   }