Skip to content

Commit

Permalink
Use a switch statement to compare transaction isolation levels (#2998)
Browse files Browse the repository at this point in the history
* Use a switch statement 
* fix: remove defaultTransactionIsolation caching from PgDatabaseMetaData, refactor tests

---------

Co-authored-by: Vladimir Sitnikov <sitnikov.vladimir@gmail.com>
  • Loading branch information
davecramer and vlsi committed Nov 17, 2023
1 parent 3ced0bc commit 65f158e
Show file tree
Hide file tree
Showing 3 changed files with 171 additions and 67 deletions.
59 changes: 27 additions & 32 deletions pgjdbc/src/main/java/org/postgresql/jdbc/PgDatabaseMetaData.java
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ public PgDatabaseMetaData(PgConnection conn) {

private int nameDataLength = 0; // length for name datatype
private int indexMaxKeys = 0; // maximum number of keys in an index.
private int defaultTransactionIsolation = 0;

protected int getMaxIndexKeys() throws SQLException {
if (indexMaxKeys == 0) {
Expand Down Expand Up @@ -959,40 +958,36 @@ public int getMaxUserNameLength() throws SQLException {
}

public int getDefaultTransactionIsolation() throws SQLException {
if (defaultTransactionIsolation == 0) {
String sql;
sql = "SELECT setting FROM pg_catalog.pg_settings WHERE name='default_transaction_isolation'";

try (Statement stmt = connection.createStatement()) {
try (ResultSet rs = stmt.executeQuery(sql)) {
String level = null;
if (rs.next()) {
level = rs.getString(1);
}
if (level == null) {
throw new PSQLException(
GT.tr(
"Unable to determine a value for DefaultTransactionIsolation due to missing "
+ "system catalog data."),
PSQLState.UNEXPECTED_ERROR);
}
String sql =
"SELECT setting FROM pg_catalog.pg_settings WHERE name='default_transaction_isolation'";

level = level.toUpperCase(Locale.US);
if (level.equals("READ COMMITTED")) {
defaultTransactionIsolation = Connection.TRANSACTION_READ_COMMITTED;
} else if (level.equals("READ UNCOMMITTED")) {
defaultTransactionIsolation = Connection.TRANSACTION_READ_UNCOMMITTED;
} else if (level.equals("REPEATABLE READ")) {
defaultTransactionIsolation = Connection.TRANSACTION_REPEATABLE_READ;
} else if (level.equals("SERIALIZABLE")) {
defaultTransactionIsolation = Connection.TRANSACTION_SERIALIZABLE;
} else {
defaultTransactionIsolation = Connection.TRANSACTION_READ_COMMITTED; // Best guess.
}
}
try (Statement stmt = connection.createStatement();
ResultSet rs = stmt.executeQuery(sql)) {
String level = null;
if (rs.next()) {
level = rs.getString(1);
}
if (level == null) {
throw new PSQLException(
GT.tr(
"Unable to determine a value for DefaultTransactionIsolation due to missing "
+ " entry in pg_catalog.pg_settings WHERE name='default_transaction_isolation'."),
PSQLState.UNEXPECTED_ERROR);
}
// PostgreSQL returns the value in lower case, so using "toLowerCase" here would be
// slightly more efficient.
switch (level.toLowerCase(Locale.ROOT)) {
case "read committed":
default: // Best guess.
return Connection.TRANSACTION_READ_COMMITTED;
case "read uncommitted":
return Connection.TRANSACTION_READ_UNCOMMITTED;
case "repeatable read":
return Connection.TRANSACTION_REPEATABLE_READ;
case "serializable":
return Connection.TRANSACTION_SERIALIZABLE;
}
}
return defaultTransactionIsolation;
}

public boolean supportsTransactions() throws SQLException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,41 +147,6 @@ public void testIdentifiers() throws SQLException {

}

@Test
public void testDefaultTransactionIsolation() throws SQLException {
DatabaseMetaData dbmd = con.getMetaData();
assertNotNull(dbmd);

int transactionIsolation = dbmd.getDefaultTransactionIsolation();
assertEquals(Connection.TRANSACTION_READ_COMMITTED, transactionIsolation);

String [] isolationLevels = {"\"read committed\"","\"read uncommitted\"", "\"repeatable read\"","serializable"};
try {
for (int i = 0; i < isolationLevels.length; i++) {
con.createStatement().execute("alter database test set default_transaction_isolation to " + isolationLevels[i]);
try (Connection con1 = TestUtil.openDB()) {
transactionIsolation = con1.getMetaData().getDefaultTransactionIsolation();
switch (i) {
case 0:
assertEquals(Connection.TRANSACTION_READ_COMMITTED, transactionIsolation);
break;
case 1:
assertEquals(Connection.TRANSACTION_READ_UNCOMMITTED, transactionIsolation);
break;
case 2:
assertEquals(Connection.TRANSACTION_REPEATABLE_READ, transactionIsolation);
break;
case 3:
assertEquals(Connection.TRANSACTION_SERIALIZABLE, transactionIsolation);
break;
}
}
}
} finally {
con.createStatement().execute("alter database test set default_transaction_isolation to DEFAULT");
}
}

@Test
public void testTables() throws SQLException {
DatabaseMetaData dbmd = con.getMetaData();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
/*
* Copyright (c) 2023, 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.test.TestUtil;

import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.function.Supplier;

public class DatabaseMetaDataTransactionIsolationTest {
static Connection con;

@BeforeAll
static void setup() throws SQLException {
con = TestUtil.openDB();
}

@AfterAll
static void teardown() throws SQLException {
TestUtil.closeDB(con);
}

@BeforeEach
void resetTransactionIsolation() throws SQLException {
// Restore to defaults
con.setAutoCommit(true);
try (Statement st = con.createStatement()) {
st.execute("alter database test set default_transaction_isolation to DEFAULT");
}
}

@Test
void connectionTransactionIsolation() throws SQLException {
// We use a new connection to avoid any side effects from other tests as we need to test
// the default transaction isolation level.
try (Connection con = TestUtil.openDB()) {
assertIsolationEquals(
"read committed",
con.getTransactionIsolation(),
() -> "Default connection transaction isolation in PostgreSQL is read committed");
}
}

@Test
void metadataDefaultTransactionIsolation() throws SQLException {
assertIsolationEquals(
"read committed",
getDefaultTransactionIsolation(),
() -> "Default database transaction isolation in PostgreSQL is read committed");
}

@ParameterizedTest
@ValueSource(strings = {"read committed", "read uncommitted", "repeatable read", "serializable"})
void alterDatabaseDefaultTransactionIsolation(String isolationLevel) throws SQLException {
try (Statement st = con.createStatement();) {
st.execute(
"alter database test set default_transaction_isolation to '" + isolationLevel + "'");
}

assertIsolationEquals(
isolationLevel,
getDefaultTransactionIsolation(),
() -> "Default transaction isolation should be " + isolationLevel);
}

/**
* PostgreSQL does not seem to update the value in
* pg_catalog.pg_settings WHERE name='default_transaction_isolation'
* when changing default_transaction_isolation, so we reconnect to get the new value.
*/
static int getDefaultTransactionIsolation() throws SQLException {
try (Connection con = TestUtil.openDB()) {
return con.getMetaData().getDefaultTransactionIsolation();
}
}

@ParameterizedTest
@ValueSource(strings = {"read committed", "read uncommitted", "repeatable read", "serializable"})
void alterConnectionTransactionIsolation(String isolationLevel) throws SQLException {
con.setAutoCommit(false);
try (Statement st = con.createStatement();) {
st.execute("set transaction ISOLATION LEVEL " + isolationLevel);
}

assertIsolationEquals(
isolationLevel,
con.getTransactionIsolation(),
() -> "Connection transaction isolation should be " + isolationLevel);
}

@ParameterizedTest
@ValueSource(ints = {
Connection.TRANSACTION_SERIALIZABLE,
Connection.TRANSACTION_REPEATABLE_READ,
Connection.TRANSACTION_READ_COMMITTED,
Connection.TRANSACTION_READ_UNCOMMITTED})
void setConnectionTransactionIsolation(int isolationLevel) throws SQLException {
con.setAutoCommit(false);
con.setTransactionIsolation(isolationLevel);

assertIsolationEquals(
mapJdbcIsolationToPg(isolationLevel),
con.getTransactionIsolation(),
() -> "Connection transaction isolation should be " + isolationLevel);
}

private static void assertIsolationEquals(String expected, int actual, Supplier<String> message) {
assertEquals(
expected,
mapJdbcIsolationToPg(actual),
message);
}

private static String mapJdbcIsolationToPg(int isolationLevel) {
switch (isolationLevel) {
case Connection.TRANSACTION_READ_COMMITTED:
return "read committed";
case Connection.TRANSACTION_READ_UNCOMMITTED:
return "read uncommitted";
case Connection.TRANSACTION_REPEATABLE_READ:
return "repeatable read";
case Connection.TRANSACTION_SERIALIZABLE:
return "serializable";
case Connection.TRANSACTION_NONE:
return "none";
default:
return "Unknown isolation level " + isolationLevel;
}
}
}

0 comments on commit 65f158e

Please sign in to comment.