Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -525,9 +525,6 @@ private void closeStatementResources() throws SQLException {
this.currentUpdateCount = -1;
this.currentJobIdIndex = -1;
if (this.connection != null) {
if (this.connection.isTransactionStarted()) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please add this logic to the connection.closeImpl()?

I think when connection is closed, it needs to cancel any pending transaction otherwise they will be eventually cancelled by some timeout.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.
Not sure about calling rollbackImpl() as-is, since it starts a new transaction after the rollback, which seems unnecessary during close().
On the other hand, connection.close() may fail before the connection is fully closed, so keeping the existing rollback behavior might actually be the safer option.

this.connection.rollback();
}
this.connection.removeStatement(this);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,20 @@ public void testCancelWithJoblessQuery() throws SQLException, InterruptedExcepti
verify(bigquery, Mockito.never()).cancel(any(JobId.class));
}

@Test
public void testCancelDoesNotRollbackTransaction() throws SQLException {
doReturn(true).when(bigQueryConnection).isTransactionStarted();
BigQueryStatement statementSpy = Mockito.spy(bigQueryStatement);
statementSpy.jobIds.add(jobId);

statementSpy.cancel();

// Cancel should call bigquery.cancel() but not rollback the transaction
verify(bigquery).cancel(eq(jobId));
verify(bigQueryConnection, Mockito.never()).rollback();
verify(bigQueryConnection).removeStatement(statementSpy);
}

@ParameterizedTest
@ValueSource(booleans = {true, false})
public void testGetStatementType(boolean isReadOnlyTokenUsed) throws Exception {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2324,6 +2324,104 @@ public void testConnectionWithMultipleTransactionCommits() throws SQLException {
connection.close();
}

@Test
public void testPreparedStatementCloseDoesNotRollbackTransaction() throws SQLException {
String TRANSACTION_TABLE = "JDBC_PS_CLOSE_TABLE" + randomNumber;
String createTransactionTable =
String.format(
"CREATE OR REPLACE TABLE %s.%s (`id` INTEGER, `name` STRING, `age` INTEGER);",
DATASET, TRANSACTION_TABLE);
String insertQuery =
String.format("INSERT INTO %s.%s (id, name, age) VALUES (?, ?, ?);", DATASET, TRANSACTION_TABLE);
String selectQuery =
String.format("SELECT id, name, age FROM %s.%s ORDER BY id;", DATASET, TRANSACTION_TABLE);

bigQueryStatement.execute(createTransactionTable);

Connection connection = DriverManager.getConnection(session_enabled_connection_uri);
connection.setAutoCommit(false);

PreparedStatement ps1 = connection.prepareStatement(insertQuery);
PreparedStatement ps2 = connection.prepareStatement(insertQuery);
try {
ps1.setInt(1, 1);
ps1.setString(2, "DwightShrute");
ps1.setInt(3, 10);
assertEquals(1, ps1.executeUpdate());

ps2.setInt(1, 2);
ps2.setString(2, "MichaelScott");
ps2.setInt(3, 20);
assertEquals(1, ps2.executeUpdate());

ps1.close();
connection.commit();

ResultSet resultSet = bigQueryStatement.executeQuery(selectQuery);
int rowCount = 0;
while (resultSet.next()) {
rowCount++;
assertEquals(rowCount, resultSet.getInt(1));
}
assertEquals(2, rowCount);
} finally {
try {
ps2.close();
} finally {
bigQueryStatement.execute(
String.format("DROP TABLE IF EXISTS %s.%s", DATASET, TRANSACTION_TABLE));
connection.close();
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The connection, prepared statements, and result set are not safely closed if an exception occurs during test execution. If connection.prepareStatement or any other statement throws an exception, the database connection and statements will be leaked. Using try-with-resources ensures that all closeable resources are properly released in all execution paths.

    try (Connection connection = DriverManager.getConnection(session_enabled_connection_uri)) {
      connection.setAutoCommit(false);
      try (PreparedStatement ps1 = connection.prepareStatement(insertQuery);
           PreparedStatement ps2 = connection.prepareStatement(insertQuery)) {
        ps1.setInt(1, 1);
        ps1.setString(2, "DwightShrute");
        ps1.setInt(3, 10);
        assertEquals(1, ps1.executeUpdate());

        ps2.setInt(1, 2);
        ps2.setString(2, "MichaelScott");
        ps2.setInt(3, 20);
        assertEquals(1, ps2.executeUpdate());

        ps1.close();
        connection.commit();

        try (ResultSet resultSet = bigQueryStatement.executeQuery(selectQuery)) {
          int rowCount = 0;
          while (resultSet.next()) {
            rowCount++;
            assertEquals(rowCount, resultSet.getInt(1));
          }
          assertEquals(2, rowCount);
        }
      }
    } finally {
      bigQueryStatement.execute(
          String.format("DROP TABLE IF EXISTS %s.%s", DATASET, TRANSACTION_TABLE));
    }
References
  1. When managing a collection of closeable resources (e.g., scopes), ensure they are closed in the reverse order of their creation (LIFO). The implementation must be exception-safe to prevent resource leaks, meaning all opened resources should be closed even if exceptions occur during their creation or closing.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

}

@Test
public void testClosingUnusedPreparedStatementDoesNotRollbackPreviousExecute()
throws SQLException {
String TRANSACTION_TABLE = "JDBC_PS_UNUSED_CLOSE_TABLE" + randomNumber;
String createTransactionTable =
String.format(
"CREATE OR REPLACE TABLE %s.%s (`id` INTEGER, `name` STRING, `age` INTEGER);",
DATASET, TRANSACTION_TABLE);
String insertQuery =
String.format("INSERT INTO %s.%s (id, name, age) VALUES (?, ?, ?);", DATASET, TRANSACTION_TABLE);
String selectQuery =
String.format("SELECT id, name, age FROM %s.%s ORDER BY id;", DATASET, TRANSACTION_TABLE);

bigQueryStatement.execute(createTransactionTable);

Connection connection = DriverManager.getConnection(session_enabled_connection_uri);
connection.setAutoCommit(false);

PreparedStatement ps1 = connection.prepareStatement(insertQuery);
PreparedStatement ps2 = connection.prepareStatement(insertQuery);
try {

ps2.setInt(1, 1);
ps2.setString(2, "MichaelScott");
ps2.setInt(3, 20);
assertEquals(1, ps2.executeUpdate());

ps1.close();
connection.commit();

ResultSet resultSet = bigQueryStatement.executeQuery(selectQuery);
assertTrue(resultSet.next());
assertEquals(1, resultSet.getInt(1));
assertEquals("MichaelScott", resultSet.getString(2));
assertEquals(20, resultSet.getInt(3));
assertFalse(resultSet.next());
} finally {
try {
ps2.close();
} finally {
bigQueryStatement.execute(
String.format("DROP TABLE IF EXISTS %s.%s", DATASET, TRANSACTION_TABLE));
connection.close();
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Similarly, the connection, prepared statements, and result set in this test are not safely closed in case of exceptions, leading to potential resource leaks. Refactoring to use try-with-resources ensures robust resource management.

    try (Connection connection = DriverManager.getConnection(session_enabled_connection_uri)) {
      connection.setAutoCommit(false);
      try (PreparedStatement ps1 = connection.prepareStatement(insertQuery);
           PreparedStatement ps2 = connection.prepareStatement(insertQuery)) {

        ps2.setInt(1, 1);
        ps2.setString(2, "MichaelScott");
        ps2.setInt(3, 20);
        assertEquals(1, ps2.executeUpdate());

        ps1.close();
        connection.commit();

        try (ResultSet resultSet = bigQueryStatement.executeQuery(selectQuery)) {
          assertTrue(resultSet.next());
          assertEquals(1, resultSet.getInt(1));
          assertEquals("MichaelScott", resultSet.getString(2));
          assertEquals(20, resultSet.getInt(3));
          assertFalse(resultSet.next());
        }
      }
    } finally {
      bigQueryStatement.execute(
          String.format("DROP TABLE IF EXISTS %s.%s", DATASET, TRANSACTION_TABLE));
    }
References
  1. When managing a collection of closeable resources (e.g., scopes), ensure they are closed in the reverse order of their creation (LIFO). The implementation must be exception-safe to prevent resource leaks, meaning all opened resources should be closed even if exceptions occur during their creation or closing.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please fix; I think using try-with-resources would address the issue

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

}

// Private Helper functions
private String getSessionId() throws InterruptedException {
QueryJobConfiguration stubJobConfig =
Expand Down
Loading