postgresql-jdbc.git / summary / log / commit / refs

commit    85dc995308cfda69a9c2bd8f707ff99bc067f922
Author:   Vladimir Sitnikov <sitnikov.vladimir@gmail.com>
Date:     Tue Mar 24 06:55:52 2026 +0000

    test: add tests for QueryExecutor#getTransactionState
    
    Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
    Co-Authored-By: GPT 5.4 <codex@openai.com>


.github/workflows/main.yml | 5 + .../test/jdbc2/TransactionStateTest.java | 255 +++++++++++++++++++++ 2 files changed, 260 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 97c9fc15..aa7dad0c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -375,6 +375,11 @@ jobs: name="pg-server-logs-${{ matrix.name }}" # Remove characters illegal in GitHub artifact names: " : < > | * ? \r \n \ / name="${name//[\":<>|*?\\\/]/}" + # Shorten to 50 characters and add run id for uniqueness + if [ ${#name} -gt 50 ]; then + suffix="-${{ github.run_id }}" + name="${name:0:$((50 - ${#suffix}))}${suffix}" + fi echo "name=$name" >> "$GITHUB_OUTPUT" - name: Upload PostgreSQL server logs if: ${{ failure() }} diff --git a/pgjdbc/src/test/java/org/postgresql/test/jdbc2/TransactionStateTest.java b/pgjdbc/src/test/java/org/postgresql/test/jdbc2/TransactionStateTest.java new file mode 100644 index 00000000..25779d4f --- /dev/null +++ b/pgjdbc/src/test/java/org/postgresql/test/jdbc2/TransactionStateTest.java @@ -0,0 +1,255 @@ +/* + * Copyright (c) 2026, PostgreSQL Global Development Group + * See the LICENSE file in the project root for more information. + */ + +package org.postgresql.test.jdbc2; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.postgresql.core.BaseConnection; +import org.postgresql.core.TransactionState; +import org.postgresql.jdbc.AutoSave; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; + +import java.sql.SQLException; +import java.sql.Savepoint; +import java.sql.Statement; +import java.util.stream.Stream; + +/** + * Tests for {@link TransactionState} transitions as reported by + * {@link org.postgresql.core.QueryExecutor#getTransactionState()}. + */ +class TransactionStateTest extends BaseTest4 { + + private TransactionState getTransactionState() throws SQLException { + return con.unwrap(BaseConnection.class).getTransactionState(); + } + + private boolean isAutoSaveAlways() throws SQLException { + return con.unwrap(BaseConnection.class).getQueryExecutor().getAutoSave() == AutoSave.ALWAYS; + } + + /** + * Returns the expected state after a query error inside a transaction. + * With {@code autosave=always} the driver automatically rolls back to a savepoint, + * so the transaction stays OPEN instead of entering FAILED. + */ + private TransactionState expectedStateAfterError() throws SQLException { + return isAutoSaveAlways() ? TransactionState.OPEN : TransactionState.FAILED; + } + + @Test + void initialStateIsIdle() throws Exception { + assertEquals(TransactionState.IDLE, getTransactionState(), + "a fresh connection should have IDLE transaction state"); + } + + @Test + void idleBeforeFirstQueryWithAutoCommitFalse() throws Exception { + con.setAutoCommit(false); + // BEGIN is deferred until the first query, so state should still be IDLE + assertEquals(TransactionState.IDLE, getTransactionState(), + "setAutoCommit(false) should not change transaction state before first query"); + } + + @Test + void openAfterQueryWithAutoCommitFalse() throws Exception { + con.setAutoCommit(false); + try (Statement stmt = con.createStatement()) { + stmt.execute("SELECT 1"); + } + assertEquals(TransactionState.OPEN, getTransactionState(), + "transaction state should be OPEN after executing a query with autoCommit=false"); + } + + static Stream<String> startTransactionCommands() { + return Stream.of( + "BEGIN", + "START TRANSACTION", + "START TRANSACTION READ ONLY" + ); + } + + @ParameterizedTest + @MethodSource("startTransactionCommands") + void openAfterSqlBegin(String sql) throws Exception { + try (Statement stmt = con.createStatement()) { + stmt.execute(sql); + } + assertEquals(TransactionState.OPEN, getTransactionState(), + () -> "transaction state should be OPEN after " + sql); + } + + @ParameterizedTest + @ValueSource(strings = {"COMMIT", "ROLLBACK"}) + void idleAfterSqlEndTransaction(String sql) throws Exception { + try (Statement stmt = con.createStatement()) { + stmt.execute("BEGIN"); + stmt.execute(sql); + } + assertEquals(TransactionState.IDLE, getTransactionState(), + () -> "transaction state should be IDLE after " + sql); + } + + @Test + void idleAfterJdbcCommit() throws Exception { + con.setAutoCommit(false); + try (Statement stmt = con.createStatement()) { + stmt.execute("SELECT 1"); + } + con.commit(); + assertEquals(TransactionState.IDLE, getTransactionState(), + "transaction state should be IDLE after Connection.commit()"); + } + + @Test + void idleAfterJdbcRollback() throws Exception { + con.setAutoCommit(false); + try (Statement stmt = con.createStatement()) { + stmt.execute("SELECT 1"); + } + con.rollback(); + assertEquals(TransactionState.IDLE, getTransactionState(), + "transaction state should be IDLE after Connection.rollback()"); + } + + @Test + void idleAfterSetAutoCommitTrue() throws Exception { + con.setAutoCommit(false); + try (Statement stmt = con.createStatement()) { + stmt.execute("SELECT 1"); + } + assertEquals(TransactionState.OPEN, getTransactionState()); + con.setAutoCommit(true); + assertEquals(TransactionState.IDLE, getTransactionState(), + "transaction state should be IDLE after setAutoCommit(true)"); + } + + @Test + void failedAfterErrorInTransaction() throws Exception { + con.setAutoCommit(false); + try (Statement stmt = con.createStatement()) { + stmt.execute("SELECT 1"); + try { + stmt.execute("SELECT * FROM nonexistent_table_xyz_12345"); + } catch (SQLException expected) { + // expected + } + } + assertEquals(expectedStateAfterError(), getTransactionState(), + "transaction state after an error in a transaction"); + } + + @Test + void idleAfterRollbackFromFailed() throws Exception { + con.setAutoCommit(false); + try (Statement stmt = con.createStatement()) { + stmt.execute("SELECT 1"); + try { + stmt.execute("SELECT * FROM nonexistent_table_xyz_12345"); + } catch (SQLException expected) { + // expected + } + } + assertEquals(expectedStateAfterError(), getTransactionState()); + con.rollback(); + assertEquals(TransactionState.IDLE, getTransactionState(), + "transaction state should be IDLE after rollback"); + } + + @Test + void idleAfterErrorWithAutoCommitTrue() throws Exception { + // With autoCommit=true each statement runs in its own implicit transaction, + // so a failed statement should leave the connection IDLE (ReadyForQuery 'I'), + // not FAILED + try (Statement stmt = con.createStatement()) { + try { + stmt.execute("SELECT * FROM nonexistent_table_xyz_12345"); + } catch (SQLException expected) { + // expected + } + } + assertEquals(TransactionState.IDLE, getTransactionState(), + "error with autoCommit=true should leave state IDLE, not FAILED"); + } + + @Test + void idleAfterCommitFromFailed() throws Exception { + // PostgreSQL treats COMMIT in an aborted transaction as a transaction end + con.setAutoCommit(false); + try (Statement stmt = con.createStatement()) { + stmt.execute("SELECT 1"); + try { + stmt.execute("SELECT * FROM nonexistent_table_xyz_12345"); + } catch (SQLException expected) { + // expected + } + } + assertEquals(expectedStateAfterError(), getTransactionState()); + con.commit(); + assertEquals(TransactionState.IDLE, getTransactionState(), + "transaction state should be IDLE after commit()"); + } + + @Test + void openAfterRollbackToSavepointFromFailed() throws Exception { + con.setAutoCommit(false); + try (Statement stmt = con.createStatement()) { + stmt.execute("SELECT 1"); + Savepoint sp = con.setSavepoint("sp1"); + try { + stmt.execute("SELECT * FROM nonexistent_table_xyz_12345"); + } catch (SQLException expected) { + // expected + } + assertEquals(expectedStateAfterError(), getTransactionState()); + con.rollback(sp); + } + assertEquals(TransactionState.OPEN, getTransactionState(), + "ROLLBACK TO SAVEPOINT should move back to OPEN"); + } + + @Test + void idleAfterCommitWhileIdleWithAutoCommitFalse() throws Exception { + con.setAutoCommit(false); + // No queries executed, so transaction state is IDLE (BEGIN not sent yet) + assertEquals(TransactionState.IDLE, getTransactionState()); + con.commit(); + assertEquals(TransactionState.IDLE, getTransactionState(), + "commit() while IDLE with autoCommit=false should keep IDLE"); + } + + @Test + void idleAfterRollbackWhileIdleWithAutoCommitFalse() throws Exception { + con.setAutoCommit(false); + assertEquals(TransactionState.IDLE, getTransactionState()); + con.rollback(); + assertEquals(TransactionState.IDLE, getTransactionState(), + "rollback() while IDLE with autoCommit=false should keep IDLE"); + } + + @Test + void idleAfterSetAutoCommitTrueFromFailed() throws Exception { + // setAutoCommit(true) routes through commit() via PgConnection.setAutoCommit, + // which is a different path than calling commit() directly on a FAILED transaction + con.setAutoCommit(false); + try (Statement stmt = con.createStatement()) { + stmt.execute("SELECT 1"); + try { + stmt.execute("SELECT * FROM nonexistent_table_xyz_12345"); + } catch (SQLException expected) { + // expected + } + } + assertEquals(expectedStateAfterError(), getTransactionState()); + con.setAutoCommit(true); + assertEquals(TransactionState.IDLE, getTransactionState(), + "transaction state should be IDLE after setAutoCommit(true)"); + } +} [parent: b2f05fd54a21]