diff --git a/docs/documentation/head/connect.md b/docs/documentation/head/connect.md index 98b31f8c20..cfb5e3b542 100644 --- a/docs/documentation/head/connect.md +++ b/docs/documentation/head/connect.md @@ -220,6 +220,15 @@ Connection conn = DriverManager.getConnection(url); The default is 'false' +* **raiseExceptionOnSilentRollback** = boolean + + since 42.2.11 + + Certain database versions perform a silent rollback instead of commit in case the transaction was in a failed state. + See https://www.postgresql.org/message-id/b9fb50dc-0f6e-15fb-6555-8ddb86f4aa71%40postgresfriends.org + + The default is 'true' + * **binaryTransfer** = boolean Use binary format for sending and receiving data if possible. diff --git a/pgjdbc/src/main/java/org/postgresql/PGProperty.java b/pgjdbc/src/main/java/org/postgresql/PGProperty.java index bac39d10f6..6335a095a2 100644 --- a/pgjdbc/src/main/java/org/postgresql/PGProperty.java +++ b/pgjdbc/src/main/java/org/postgresql/PGProperty.java @@ -662,6 +662,14 @@ public enum PGProperty { "false", "Use SPNEGO in SSPI authentication requests"), + /** + * Certain database versions perform a silent rollback instead of commit in case the transaction was in a failed state. + */ + RAISE_EXCEPTION_ON_SILENT_ROLLBACK( + "raiseExceptionOnSilentRollback", + "true", + "Certain database versions perform a silent rollback instead of commit in case the transaction was in a failed state"), + ; private final String name; diff --git a/pgjdbc/src/main/java/org/postgresql/core/QueryExecutor.java b/pgjdbc/src/main/java/org/postgresql/core/QueryExecutor.java index 6df042d7c4..f8b82c869e 100644 --- a/pgjdbc/src/main/java/org/postgresql/core/QueryExecutor.java +++ b/pgjdbc/src/main/java/org/postgresql/core/QueryExecutor.java @@ -444,6 +444,10 @@ Object createQueryKey(String sql, boolean escapeProcessing, boolean isParameteri boolean willHealOnRetry(SQLException e); + boolean isRaiseExceptionOnSilentRollback(); + + void setRaiseExceptionOnSilentRollback(boolean raiseExceptionOnSilentRollback); + /** * By default, the connection resets statement cache in case deallocate all/discard all * message is observed. diff --git a/pgjdbc/src/main/java/org/postgresql/core/QueryExecutorBase.java b/pgjdbc/src/main/java/org/postgresql/core/QueryExecutorBase.java index 82d636c4fd..3bc8d6a366 100644 --- a/pgjdbc/src/main/java/org/postgresql/core/QueryExecutorBase.java +++ b/pgjdbc/src/main/java/org/postgresql/core/QueryExecutorBase.java @@ -48,6 +48,7 @@ public abstract class QueryExecutorBase implements QueryExecutor { private AutoSave autoSave; private boolean flushCacheOnDeallocate = true; protected final boolean logServerErrorDetail; + private boolean raiseExceptionOnSilentRollback; // default value for server versions that don't report standard_conforming_strings private boolean standardConformingStrings = false; @@ -408,6 +409,16 @@ public void setFlushCacheOnDeallocate(boolean flushCacheOnDeallocate) { this.flushCacheOnDeallocate = flushCacheOnDeallocate; } + @Override + public boolean isRaiseExceptionOnSilentRollback() { + return raiseExceptionOnSilentRollback; + } + + @Override + public void setRaiseExceptionOnSilentRollback(boolean raiseExceptionOnSilentRollback) { + this.raiseExceptionOnSilentRollback = raiseExceptionOnSilentRollback; + } + protected boolean hasNotifications() { return notifications.size() > 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 ab06a88ba7..f1ffeaef28 100644 --- a/pgjdbc/src/main/java/org/postgresql/core/v3/QueryExecutorImpl.java +++ b/pgjdbc/src/main/java/org/postgresql/core/v3/QueryExecutorImpl.java @@ -67,6 +67,7 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.regex.Pattern; /** * QueryExecutor implementation for the V3 protocol. @@ -75,6 +76,23 @@ public class QueryExecutorImpl extends QueryExecutorBase { private static final Logger LOGGER = Logger.getLogger(QueryExecutorImpl.class.getName()); private static final String COPY_ERROR_MESSAGE = "COPY commands are only supported using the CopyManager API."; + private static final Pattern ROLLBACK_PATTERN = Pattern.compile("\\brollback\\b", Pattern.CASE_INSENSITIVE); + private static final Pattern COMMIT_PATTERN = Pattern.compile("\\bcommit\\b", Pattern.CASE_INSENSITIVE); + private static final Pattern PREPARE_PATTERN = Pattern.compile("\\bprepare ++transaction\\b", Pattern.CASE_INSENSITIVE); + + private static boolean looksLikeCommit(String sql) { + if ("COMMIT".equalsIgnoreCase(sql)) { + return true; + } + if ("ROLLBACK".equalsIgnoreCase(sql)) { + return false; + } + return COMMIT_PATTERN.matcher(sql).find() && !ROLLBACK_PATTERN.matcher(sql).find(); + } + + private static boolean looksLikePrepare(String sql) { + return sql.startsWith("PREPARE TRANSACTION") || PREPARE_PATTERN.matcher(sql).find(); + } /** * TimeZone of the current connection (TimeZone backend parameter). @@ -355,7 +373,6 @@ private boolean sendAutomaticSavepoint(Query query, int flags) throws IOExceptio if (((flags & QueryExecutor.QUERY_SUPPRESS_BEGIN) == 0 || getTransactionState() == TransactionState.OPEN) && query != restoreToAutoSave - && !query.getNativeSql().equalsIgnoreCase("COMMIT") && 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 savepoint before such query @@ -2166,8 +2183,36 @@ protected void processResults(ResultHandler handler, int flags) throws IOExcepti SimpleQuery currentQuery = executeData.query; Portal currentPortal = executeData.portal; + String nativeSql = currentQuery.getNativeQuery().nativeSql; + // Certain backend versions (e.g. 12.2, 11.7, 10.12, 9.6.17, 9.5.21, etc) + // silently rollback the transaction in the response to COMMIT statement + // in case the transaction has failed. + // See discussion in pgsql-hackers: https://www.postgresql.org/message-id/b9fb50dc-0f6e-15fb-6555-8ddb86f4aa71%40postgresfriends.org + if (isRaiseExceptionOnSilentRollback() + && handler.getException() == null + && status.startsWith("ROLLBACK")) { + String message = null; + if (looksLikeCommit(nativeSql)) { + if (transactionFailCause == null) { + message = GT.tr("The database returned ROLLBACK, so the transaction cannot be committed. Transaction failure is not known (check server logs?)"); + } else { + message = GT.tr("The database returned ROLLBACK, so the transaction cannot be committed. Transaction failure cause is <<{0}>>", transactionFailCause.getMessage()); + } + } else if (looksLikePrepare(nativeSql)) { + if (transactionFailCause == null) { + message = GT.tr("The database returned ROLLBACK, so the transaction cannot be prepared. Transaction failure is not known (check server logs?)"); + } else { + message = GT.tr("The database returned ROLLBACK, so the transaction cannot be prepared. Transaction failure cause is <<{0}>>", transactionFailCause.getMessage()); + } + } + if (message != null) { + handler.handleError( + new PSQLException( + message, PSQLState.IN_FAILED_SQL_TRANSACTION, transactionFailCause)); + } + } + if (status.startsWith("SET")) { - String nativeSql = currentQuery.getNativeQuery().nativeSql; // Scan only the first 1024 characters to // avoid big overhead for long queries. if (nativeSql.lastIndexOf("search_path", 1024) != -1 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 f6e0fa76f1..6b8e5e8bbe 100644 --- a/pgjdbc/src/main/java/org/postgresql/ds/common/BaseDataSource.java +++ b/pgjdbc/src/main/java/org/postgresql/ds/common/BaseDataSource.java @@ -1461,6 +1461,22 @@ public void setAutosave(AutoSave autoSave) { PGProperty.AUTOSAVE.set(properties, autoSave.value()); } + /** + * @return connection configuration regarding throwing exception from commit if database rolls back the transaction + * @see PGProperty#RAISE_EXCEPTION_ON_SILENT_ROLLBACK + */ + public boolean isRaiseExceptionOnSilentRollback() { + return PGProperty.RAISE_EXCEPTION_ON_SILENT_ROLLBACK.getBoolean(properties); + } + + /** + * @param raiseExceptionOnSilentRollback if the database should throw exception if commit silently rolls back + * @see PGProperty#RAISE_EXCEPTION_ON_SILENT_ROLLBACK + */ + public void setRaiseExceptionOnSilentRollback(boolean raiseExceptionOnSilentRollback) { + PGProperty.RAISE_EXCEPTION_ON_SILENT_ROLLBACK.set(properties, raiseExceptionOnSilentRollback); + } + /** * see PGProperty#CLEANUP_SAVEPOINTS * diff --git a/pgjdbc/src/main/java/org/postgresql/jdbc/PgConnection.java b/pgjdbc/src/main/java/org/postgresql/jdbc/PgConnection.java index 72e9639480..785f3e5283 100644 --- a/pgjdbc/src/main/java/org/postgresql/jdbc/PgConnection.java +++ b/pgjdbc/src/main/java/org/postgresql/jdbc/PgConnection.java @@ -247,6 +247,10 @@ public PgConnection(HostSpec[] hostSpecs, LOGGER.log(Level.FINEST, " integer date/time = {0}", queryExecutor.getIntegerDateTimes()); } + queryExecutor.setRaiseExceptionOnSilentRollback( + PGProperty.RAISE_EXCEPTION_ON_SILENT_ROLLBACK.getBoolean(info) + ); + // // String -> text or unknown? // diff --git a/pgjdbc/src/main/java/org/postgresql/xa/PGXAConnection.java b/pgjdbc/src/main/java/org/postgresql/xa/PGXAConnection.java index a746f77988..9e4b178769 100644 --- a/pgjdbc/src/main/java/org/postgresql/xa/PGXAConnection.java +++ b/pgjdbc/src/main/java/org/postgresql/xa/PGXAConnection.java @@ -647,6 +647,9 @@ private int mapSQLStateToXAErrorCode(SQLException sqlException) { if (isPostgreSQLIntegrityConstraintViolation(sqlException)) { return XAException.XA_RBINTEGRITY; } + if (PSQLState.IN_FAILED_SQL_TRANSACTION.getState().equals(sqlException.getSQLState())) { + return XAException.XA_RBOTHER; + } return XAException.XAER_RMFAIL; } diff --git a/pgjdbc/src/test/java/org/postgresql/test/jdbc2/AutoRollbackTestSuite.java b/pgjdbc/src/test/java/org/postgresql/test/jdbc2/AutoRollbackTestSuite.java index 212d4e9654..767bf98be2 100644 --- a/pgjdbc/src/test/java/org/postgresql/test/jdbc2/AutoRollbackTestSuite.java +++ b/pgjdbc/src/test/java/org/postgresql/test/jdbc2/AutoRollbackTestSuite.java @@ -162,6 +162,9 @@ public void setUp() throws Exception { public void tearDown() throws SQLException { try { con.setAutoCommit(true); + } catch (Exception ignored) { + } + try { TestUtil.dropTable(con, "rollbacktest"); } catch (Exception e) { e.printStackTrace(); @@ -292,15 +295,46 @@ public void run() throws SQLException { switch (continueMode) { case COMMIT: try { + TransactionState transactionState = pgConnection.getTransactionState(); doCommit(); - // No assert here: commit should always succeed with exception of well known failure cases in catch + Assert.assertNotEquals( + ".commit() should throw exception since the transaction was in failed state", + TransactionState.FAILED, transactionState); + Assert.assertEquals("Transaction should be IDLE after .commit()", + TransactionState.IDLE, pgConnection.getTransactionState()); + // Commit might fail in case the transaction is in aborted state } catch (SQLException e) { + if (!PSQLState.INVALID_SQL_STATEMENT_NAME.getState().equals(e.getSQLState())) { + // In preferQueryMode=extendedCacheEverything mode it might be that "commit" + // statement is using server-prepared mode, and it might result in + // ERROR: prepared statement "S_4" does not exist + try { + Assert.assertEquals("Transaction should be IDLE after .commit()", + TransactionState.IDLE, pgConnection.getTransactionState()); + } catch (AssertionError ae) { + ae.initCause(e); + throw ae; + } + } + if (PSQLState.IN_FAILED_SQL_TRANSACTION.getState().equals(e.getSQLState()) + && (!flushCacheOnDeallocate && DEALLOCATES.contains(failMode) + || autoCommit == AutoCommit.NO)) { + // We have two cases here: + // 1) autocommit=false, so transaction was in progress, so it is expected that commit + // fails with IN_FAILED_SQL_TRANSACTION + // 2) commit might fail due to <> + // However, if flushCacheOnDeallocate is false, then autosave can't really work, so + // it is expected that commit fails + // Commit terminates the transaction, so "autosave" can't really "rollback" + return; + } 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()); + // Commit terminates the transaction, so "autosave" can't really "rollback" return; } throw e; diff --git a/pgjdbc/src/test/java/org/postgresql/test/jdbc2/BatchExecuteTest.java b/pgjdbc/src/test/java/org/postgresql/test/jdbc2/BatchExecuteTest.java index a1f0b9c033..8180385374 100644 --- a/pgjdbc/src/test/java/org/postgresql/test/jdbc2/BatchExecuteTest.java +++ b/pgjdbc/src/test/java/org/postgresql/test/jdbc2/BatchExecuteTest.java @@ -8,6 +8,7 @@ import org.postgresql.PGProperty; import org.postgresql.PGStatement; import org.postgresql.test.TestUtil; +import org.postgresql.util.PSQLState; import org.junit.Assert; import org.junit.Test; @@ -238,13 +239,13 @@ public void testSelectInBatch() throws Exception { } @Test - public void testSelectInBatchThrowsAutoCommit() throws Exception { + public void testSelectInBatchThrowsAutoCommit() throws Throwable { con.setAutoCommit(true); testSelectInBatchThrows(); } @Test - public void testSelectInBatchThrows() throws Exception { + public void testSelectInBatchThrows() throws Throwable { Statement stmt = con.createStatement(); int oldValue = getCol1Value(); @@ -261,7 +262,13 @@ public void testSelectInBatchThrows() throws Exception { } if (!con.getAutoCommit()) { - con.commit(); + try { + con.commit(); + } catch (SQLException e) { + if (!PSQLState.IN_FAILED_SQL_TRANSACTION.getState().equals(e.getSQLState())) { + throw e; + } + } } int newValue = getCol1Value(); diff --git a/pgjdbc/src/test/java/org/postgresql/test/jdbc2/BatchFailureTest.java b/pgjdbc/src/test/java/org/postgresql/test/jdbc2/BatchFailureTest.java index d2ef4e70d6..5d8b6923e4 100644 --- a/pgjdbc/src/test/java/org/postgresql/test/jdbc2/BatchFailureTest.java +++ b/pgjdbc/src/test/java/org/postgresql/test/jdbc2/BatchFailureTest.java @@ -7,6 +7,7 @@ import org.postgresql.PGProperty; import org.postgresql.test.TestUtil; +import org.postgresql.util.PSQLState; import org.junit.Assert; import org.junit.Test; @@ -242,7 +243,14 @@ public void run() throws SQLException { } if (!con.getAutoCommit()) { - con.commit(); + try { + // Commit might fail if the transaction is in aborted state + con.commit(); + } catch (SQLException e) { + if (!PSQLState.IN_FAILED_SQL_TRANSACTION.getState().equals(e.getSQLState())) { + throw e; + } + } } int finalCount = getBatchUpdCount(); diff --git a/pgjdbc/src/test/java/org/postgresql/test/jdbc2/Jdbc2TestSuite.java b/pgjdbc/src/test/java/org/postgresql/test/jdbc2/Jdbc2TestSuite.java index 653512f820..95701392a2 100644 --- a/pgjdbc/src/test/java/org/postgresql/test/jdbc2/Jdbc2TestSuite.java +++ b/pgjdbc/src/test/java/org/postgresql/test/jdbc2/Jdbc2TestSuite.java @@ -118,6 +118,7 @@ TimeTest.class, TimezoneCachingTest.class, TimezoneTest.class, + TransactionTest.class, TypeCacheDLLStressTest.class, UpdateableResultTest.class, UpsertTest.class, diff --git a/pgjdbc/src/test/java/org/postgresql/test/jdbc2/MiscTest.java b/pgjdbc/src/test/java/org/postgresql/test/jdbc2/MiscTest.java index 990e1bf3bf..79e1ddc236 100644 --- a/pgjdbc/src/test/java/org/postgresql/test/jdbc2/MiscTest.java +++ b/pgjdbc/src/test/java/org/postgresql/test/jdbc2/MiscTest.java @@ -9,7 +9,9 @@ import static org.junit.Assert.fail; import org.postgresql.test.TestUtil; +import org.postgresql.util.PSQLState; +import org.junit.Assert; import org.junit.Ignore; import org.junit.Test; @@ -88,7 +90,15 @@ public void testError() throws Exception { oos.close(); } - con.commit(); + try { + con.commit(); + } catch (SQLException e) { + Assert.assertEquals( + "Transaction is in failed state, so .commit() should throw", + PSQLState.IN_FAILED_SQL_TRANSACTION.getState(), + e.getSQLState() + ); + } con.close(); } diff --git a/pgjdbc/src/test/java/org/postgresql/test/jdbc2/TransactionTest.java b/pgjdbc/src/test/java/org/postgresql/test/jdbc2/TransactionTest.java new file mode 100644 index 0000000000..f31b08481a --- /dev/null +++ b/pgjdbc/src/test/java/org/postgresql/test/jdbc2/TransactionTest.java @@ -0,0 +1,142 @@ +/* + * Copyright (c) 2020, PostgreSQL Global Development Group + * See the LICENSE file in the project root for more information. + */ + +package org.postgresql.test.jdbc2; + +import org.postgresql.test.TestUtil; +import org.postgresql.util.PSQLState; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.sql.SQLException; +import java.sql.Statement; +import java.util.concurrent.Callable; + +public class TransactionTest extends BaseTest4 { + @Before + public void setUp() throws Exception { + super.setUp(); + TestUtil.createTempTable(con, "transaction_test_table", "i int"); + con.setAutoCommit(false); + } + + @After + public void tearDown() throws SQLException { + con.setAutoCommit(true); + TestUtil.dropTable(con, "transaction_test_table"); + super.tearDown(); + } + + private void setupTransaction() throws SQLException { + Statement st = con.createStatement(); + st.execute("insert into transaction_test_table(i) values(42)"); + try { + st.execute("invalid sql to abort transaction"); + } catch (SQLException ignored) { + } + TestUtil.closeQuietly(st); + } + + private void assertFails(Callable action) throws Exception { + String msg = "commit should fail since the transaction is in invalid state"; + try { + action.call(); + Assert.fail(msg); + } catch (SQLException e) { + try { + Assert.assertEquals( + msg, + PSQLState.IN_FAILED_SQL_TRANSACTION.getState(), + e.getSQLState() + ); + } catch (Throwable t) { + t.initCause(e); + throw t; + } + } + } + + @Test + public void commitShouldFailIfTransactionWasInvalidState() throws Exception { + setupTransaction(); + assertFails(new Callable() { + @Override + public Void call() throws Exception { + con.commit(); + return null; + } + }); + } + + @Test + public void execute_commit_statement_ShouldFailIfTransactionWasInvalidState() throws Exception { + setupTransaction(); + assertFails(new Callable() { + @Override + public Void call() throws Exception { + con.createStatement().execute("commit"); + return null; + } + }); + } + + @Test + public void execute_CoMMiT_statement_ShouldFailIfTransactionWasInvalidState() throws Exception { + setupTransaction(); + assertFails(new Callable() { + @Override + public Void call() throws Exception { + con.createStatement().execute("CoMMiT"); + return null; + } + }); + } + + @Test + public void execute_CoMMiT_with_comments_statement_ShouldFailIfTransactionWasInvalidState() throws Exception { + setupTransaction(); + assertFails(new Callable() { + @Override + public Void call() throws Exception { + con.createStatement().execute("/* Dear database, please save the data if possible, thanks */CoMMiT"); + return null; + } + }); + } + + @Test + public void execute_rollback_with_comments_statement_should_not_fail() throws Exception { + setupTransaction(); + // Note: there's rollback, so exception should not be there + con.createStatement().execute("/*adsf,commit*/rollBack/*CoMMiT,commit*/"); + } + + @Test + public void execute_rollback_statement_shouldsucceed() throws Exception { + setupTransaction(); + con.createStatement().execute("rollback"); + } + + @Test + public void rolback_shouldsucceed() throws Exception { + setupTransaction(); + con.rollback(); + } + + @Test + public void switch_to_autocommit_should_fail_because_it_does_implicit_commit() throws Exception { + setupTransaction(); + assertFails(new Callable() { + @Override + public Void call() throws Exception { + con.setAutoCommit(true); + return null; + } + }); + } +} diff --git a/pgjdbc/src/test/java/org/postgresql/test/jdbc3/Jdbc3SavepointTest.java b/pgjdbc/src/test/java/org/postgresql/test/jdbc3/Jdbc3SavepointTest.java index 46306e0375..4f5af5a16c 100644 --- a/pgjdbc/src/test/java/org/postgresql/test/jdbc3/Jdbc3SavepointTest.java +++ b/pgjdbc/src/test/java/org/postgresql/test/jdbc3/Jdbc3SavepointTest.java @@ -9,8 +9,10 @@ import static org.junit.Assert.fail; import org.postgresql.test.TestUtil; +import org.postgresql.util.PSQLState; import org.junit.After; +import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -34,7 +36,15 @@ public void setUp() throws Exception { @After public void tearDown() throws SQLException { - conn.setAutoCommit(true); + try { + conn.setAutoCommit(true); + } catch (SQLException e) { + Assert.assertEquals( + "Unexpected exception from setAutoCommit(true)", + PSQLState.IN_FAILED_SQL_TRANSACTION.getState(), + e.getSQLState() + ); + } TestUtil.dropTable(conn, "savepointtable"); TestUtil.closeDB(conn); } diff --git a/pgjdbc/src/test/java/org/postgresql/test/xa/XAPrepareFailureTest.java b/pgjdbc/src/test/java/org/postgresql/test/xa/XAPrepareFailureTest.java new file mode 100644 index 0000000000..d6d11af2fe --- /dev/null +++ b/pgjdbc/src/test/java/org/postgresql/test/xa/XAPrepareFailureTest.java @@ -0,0 +1,142 @@ +/* + * Copyright (c) 2020, PostgreSQL Global Development Group + * See the LICENSE file in the project root for more information. + */ + +package org.postgresql.test.xa; + +import static org.junit.Assert.assertTrue; +import static org.junit.Assume.assumeTrue; + +import org.postgresql.test.TestUtil; +import org.postgresql.test.jdbc2.BaseTest4; +import org.postgresql.test.jdbc2.optional.BaseDataSourceTest; +import org.postgresql.xa.PGXADataSource; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.concurrent.Callable; + +import javax.sql.XAConnection; +import javax.sql.XADataSource; +import javax.transaction.xa.XAException; +import javax.transaction.xa.XAResource; + +public class XAPrepareFailureTest extends BaseTest4 { + private XADataSource xaDs; + + private XAConnection xaconn; + private XAResource xaRes; + private Connection conn; + XADataSourceTest.CustomXid xid = new XADataSourceTest.CustomXid(7); + + public XAPrepareFailureTest() { + xaDs = new PGXADataSource(); + BaseDataSourceTest.setupDataSource((PGXADataSource) xaDs); + } + + @Before + public void setUp() throws Exception { + super.setUp(); + assumeTrue(isPreparedTransactionEnabled(con)); + + TestUtil.createTable(con, "xatransaction_test_table", "i int"); + + clearAllPrepared(); + xaconn = xaDs.getXAConnection(); + xaRes = xaconn.getXAResource(); + conn = xaconn.getConnection(); + } + + @After + public void tearDown() throws SQLException { + TestUtil.dropTable(con, "xatransaction_test_table"); + clearAllPrepared(); + super.tearDown(); + } + + private static boolean isPreparedTransactionEnabled(Connection connection) throws SQLException { + Statement stmt = connection.createStatement(); + ResultSet rs = stmt.executeQuery("SHOW max_prepared_transactions"); + rs.next(); + int mpt = rs.getInt(1); + rs.close(); + stmt.close(); + return mpt > 0; + } + + private void clearAllPrepared() throws SQLException { + Statement st = con.createStatement(); + try { + ResultSet rs = st.executeQuery( + "SELECT x.gid, x.owner = current_user " + + "FROM pg_prepared_xacts x " + + "WHERE x.database = current_database()"); + + Statement st2 = con.createStatement(); + while (rs.next()) { + // TODO: This should really use org.junit.Assume once we move to JUnit 4 + assertTrue("Only prepared xacts owned by current user may be present in db", + rs.getBoolean(2)); + st2.executeUpdate("ROLLBACK PREPARED '" + rs.getString(1) + "'"); + } + TestUtil.closeQuietly(st2); + } finally { + TestUtil.closeQuietly(st); + } + } + + private XADataSourceTest.CustomXid setupTransaction() throws SQLException, XAException { + xaRes.start(xid, XAResource.TMNOFLAGS); + Statement st = conn.createStatement(); + st.execute("insert into xatransaction_test_table(i) values(42)"); + try { + st.execute("invalid sql to abort transaction"); + } catch (SQLException ignored) { + } + TestUtil.closeQuietly(st); + return xid; + } + + private void assertFails(Callable action) throws Exception { + String msg = "prepare transaction should fail since the transaction is in invalid state"; + try { + action.call(); + Assert.fail(msg); + } catch (XAException e) { + try { + Assert.assertEquals(msg, XAException.XA_RBOTHER, e.errorCode); + } catch (Throwable t) { + t.initCause(e); + throw t; + } + } + } + + @Test + public void prepareShouldFailIfTransactionWasInvalidState() throws Exception { + setupTransaction(); + assertFails(new Callable() { + @Override + public Void call() throws Exception { + xaRes.end(xid, XAResource.TMSUCCESS); + xaRes.prepare(xid); + return null; + } + }); + } + + @Test + public void execute_rollback_statement_shouldsucceed() throws Exception { + setupTransaction(); + xaRes.end(xid, XAResource.TMSUCCESS); + xaRes.rollback(xid); + } +}