Re: revisiting transaction isolation - Mailing list pgsql-jdbc

From Oliver Jowett
Subject Re: revisiting transaction isolation
Date
Msg-id 40FCE032.30201@opencloud.com
Whole thread Raw
In response to Re: revisiting transaction isolation  (Oliver Jowett <oliver@opencloud.com>)
Responses Re: revisiting transaction isolation
List pgsql-jdbc
Oliver Jowett wrote:

> A quick skim of AbstractJdbc2Connection turns up these methods as
> candidates for ignoring autocommit:
>
>  - getTransactionIsolation()
>  - getPGType (both versions) on a cache miss
>
> It seems reasonable to suppress BEGINs for both of those cases. I can
> put together a patch to do that. I'll leave the metadata queries alone
> for the moment.

And here is the patch.

I reworked execSQLQuery / execSQLUpdate to always ignore autocommit,
since all places they're called from want this behaviour. Also tweaked
them to detect unexpected cases (no resultset from query, resultset from
update).

-O
Index: org/postgresql/core/BaseConnection.java
===================================================================
RCS file: /usr/local/cvsroot/pgjdbc/pgjdbc/org/postgresql/core/BaseConnection.java,v
retrieving revision 1.8
diff -u -c -r1.8 BaseConnection.java
*** org/postgresql/core/BaseConnection.java    29 Jun 2004 06:43:24 -0000    1.8
--- org/postgresql/core/BaseConnection.java    20 Jul 2004 08:35:05 -0000
***************
*** 31,36 ****
--- 31,37 ----

      /**
       * Execute a SQL query that returns a single resultset.
+      * Never causes a new transaction to be started regardless of the autocommit setting.
       *
       * @param s the query to execute
       * @return the (non-null) returned resultset
***************
*** 40,45 ****
--- 41,47 ----

      /**
       * Execute a SQL query that does not return results.
+      * Never causes a new transaction to be started regardless of the autocommit setting.
       *
       * @param s the query to execute
       * @throws SQLException if something goes wrong.
Index: org/postgresql/core/BaseStatement.java
===================================================================
RCS file: /usr/local/cvsroot/pgjdbc/pgjdbc/org/postgresql/core/BaseStatement.java,v
retrieving revision 1.13
diff -u -c -r1.13 BaseStatement.java
*** org/postgresql/core/BaseStatement.java    16 Jul 2004 09:07:53 -0000    1.13
--- org/postgresql/core/BaseStatement.java    20 Jul 2004 08:35:05 -0000
***************
*** 53,56 ****
--- 53,65 ----
       * @throws SQLException if something goes wrong.
       */
      public boolean executeWithFlags(String p_sql, int flags) throws SQLException;
+
+     /**
+      * Execute a prepared query, passing additional query flags.
+      *
+      * @param flags additional {@link QueryExecutor} flags for execution; these
+      *  are bitwise-ORed into the default flags.
+      * @throws SQLException if something goes wrong.
+      */
+     public boolean executeWithFlags(int flags) throws SQLException;
  }
Index: org/postgresql/jdbc2/AbstractJdbc2Connection.java
===================================================================
RCS file: /usr/local/cvsroot/pgjdbc/pgjdbc/org/postgresql/jdbc2/AbstractJdbc2Connection.java,v
retrieving revision 1.13
diff -u -c -r1.13 AbstractJdbc2Connection.java
*** org/postgresql/jdbc2/AbstractJdbc2Connection.java    16 Jul 2004 09:07:57 -0000    1.13
--- org/postgresql/jdbc2/AbstractJdbc2Connection.java    20 Jul 2004 08:35:06 -0000
***************
*** 175,184 ****
      /**
       * Simple query execution.
       */
!     public ResultSet execSQLQuery(String s) throws SQLException
!     {
!         Statement stat = (Statement) createStatement();
!         boolean hasResultSet = stat.execute(s);

          // Transfer warnings to the connection, since the user never
          // has a chance to see the statement itself.
--- 175,189 ----
      /**
       * Simple query execution.
       */
!     public ResultSet execSQLQuery(String s) throws SQLException {
!         BaseStatement stat = (BaseStatement) createStatement();
!         boolean hasResultSet = stat.executeWithFlags(s, QueryExecutor.QUERY_SUPPRESS_BEGIN);
!
!         while (!hasResultSet && stat.getUpdateCount() != -1)
!             hasResultSet = stat.getMoreResults();
!
!         if (!hasResultSet)
!             throw new PSQLException("postgresql.stat.noresult", PSQLState.NO_DATA);

          // Transfer warnings to the connection, since the user never
          // has a chance to see the statement itself.
***************
*** 186,206 ****
          if (warnings != null)
              addWarning(warnings);

-         while (!hasResultSet && stat.getUpdateCount() != -1)
-             hasResultSet = stat.getMoreResults();
-
          return stat.getResultSet();
      }

!     public void execSQLUpdate(String s) throws SQLException
!     {
!         execSQLUpdate(s, 0);
!     }
!
!     private void execSQLUpdate(String s, int flags) throws SQLException
!     {
          BaseStatement stmt = (BaseStatement) createStatement();
!         stmt.executeWithFlags(s, QueryExecutor.QUERY_NO_METADATA | QueryExecutor.QUERY_NO_RESULTS | flags);

          // Transfer warnings to the connection, since the user never
          // has a chance to see the statement itself.
--- 191,203 ----
          if (warnings != null)
              addWarning(warnings);

          return stat.getResultSet();
      }

!     public void execSQLUpdate(String s) throws SQLException {
          BaseStatement stmt = (BaseStatement) createStatement();
!         if (stmt.executeWithFlags(s, QueryExecutor.QUERY_NO_METADATA | QueryExecutor.QUERY_NO_RESULTS |
QueryExecutor.QUERY_SUPPRESS_BEGIN))
!             throw new PSQLException("postgresql.stat.result");

          // Transfer warnings to the connection, since the user never
          // has a chance to see the statement itself.
***************
*** 526,532 ****

          if (haveMinimumServerVersion("7.4") && readOnly != this.readOnly) {
              String readOnlySql = "SET SESSION CHARACTERISTICS AS TRANSACTION " + (readOnly ? "READ ONLY" : "READ
WRITE");
!             execSQLUpdate(readOnlySql, QueryExecutor.QUERY_SUPPRESS_BEGIN); // No BEGIN regardles of our autocommit
setting
          }

          this.readOnly = readOnly;
--- 523,529 ----

          if (haveMinimumServerVersion("7.4") && readOnly != this.readOnly) {
              String readOnlySql = "SET SESSION CHARACTERISTICS AS TRANSACTION " + (readOnly ? "READ ONLY" : "READ
WRITE");
!             execSQLUpdate(readOnlySql); // nb: no BEGIN triggered.
          }

          this.readOnly = readOnly;
***************
*** 633,658 ****
      public int getTransactionIsolation() throws SQLException
      {
          String level = null;
-         Statement stmt = createStatement();

!         if (haveMinimumServerVersion("7.3")) {
!             ResultSet rs = stmt.executeQuery("SHOW TRANSACTION ISOLATION LEVEL");
              if (rs.next())
                  level = rs.getString(1);
!
!             // Transfer warnings to the connection, since the user never
!             // has a chance to see the statement itself.
!             SQLWarning warnings = stmt.getWarnings();
!             if (warnings != null)
!                 addWarning(warnings);
          } else {
!             stmt.executeUpdate("SHOW TRANSACTION ISOLATION LEVEL");
!             SQLWarning warning = stmt.getWarnings();
              if (warning != null)
                  level = warning.getMessage();
-         }

!         stmt.close();

          // XXX revisit: throw exception instead of silently eating the error in unkwon cases?
          if (level == null)
--- 630,661 ----
      public int getTransactionIsolation() throws SQLException
      {
          String level = null;

!         if (haveMinimumServerVersion("7.3")) {
!             // 7.3+ returns the level as a query result.
!             ResultSet rs = execSQLQuery("SHOW TRANSACTION ISOLATION LEVEL"); // nb: no BEGIN triggered
              if (rs.next())
                  level = rs.getString(1);
!             rs.close();
          } else {
!             // 7.2 returns the level as an INFO message. Ew.
!             // We juggle the warning chains a bit here.
!
!             // Swap out current warnings.
!             SQLWarning saveWarnings = getWarnings();
!             clearWarnings();
!
!             // Run the query any examine any resulting warnings.
!             execSQLUpdate("SHOW TRANSACTION ISOLATION LEVEL"); // nb: no BEGIN triggered
!             SQLWarning warning = getWarnings();
              if (warning != null)
                  level = warning.getMessage();

!             // Swap original warnings back.
!             clearWarnings();
!             if (saveWarnings != null)
!                 addWarning(saveWarnings);
!         }

          // XXX revisit: throw exception instead of silently eating the error in unkwon cases?
          if (level == null)
***************
*** 694,700 ****
              throw new PSQLException("postgresql.con.isolevel", PSQLState.TRANSACTION_STATE_INVALID, new
Integer(level));

          String isolationLevelSQL = "SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL " +
isolationLevelName;
!         execSQLUpdate(isolationLevelSQL, QueryExecutor.QUERY_SUPPRESS_BEGIN); // No BEGIN regardles of our autocommit
setting
      }

      protected String getIsolationLevelName(int level)
--- 697,703 ----
              throw new PSQLException("postgresql.con.isolevel", PSQLState.TRANSACTION_STATE_INVALID, new
Integer(level));

          String isolationLevelSQL = "SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL " +
isolationLevelName;
!         execSQLUpdate(isolationLevelSQL); // nb: no BEGIN triggered
      }

      protected String getIsolationLevelName(int level)
***************
*** 887,893 ****

              query.setString(1, typeName);

!             BaseResultSet result = (BaseResultSet)query.executeQuery();
              if (result.next()) {
                  oid = result.getInt(1);
                  oidTypeCache.put(new Integer(oid), typeName);
--- 890,899 ----

              query.setString(1, typeName);

!             if (! ((BaseStatement)query).executeWithFlags(QueryExecutor.QUERY_SUPPRESS_BEGIN) )
!                 throw new PSQLException("postgresql.stat.noresult", PSQLState.NO_DATA);
!
!             ResultSet result = query.getResultSet();
              if (result.next()) {
                  oid = result.getInt(1);
                  oidTypeCache.put(new Integer(oid), typeName);
***************
*** 926,932 ****

              query.setInt(1, oid);

!             ResultSet result = query.executeQuery();
              if (result.next()) {
                  typeName = result.getString(1);
                  typeOidCache.put(typeName, new Integer(oid));
--- 932,941 ----

              query.setInt(1, oid);

!             if (! ((BaseStatement)query).executeWithFlags(QueryExecutor.QUERY_SUPPRESS_BEGIN) )
!                 throw new PSQLException("postgresql.stat.noresult", PSQLState.NO_DATA);
!
!             ResultSet result = query.getResultSet();
              if (result.next()) {
                  typeName = result.getString(1);
                  typeOidCache.put(typeName, new Integer(oid));
Index: org/postgresql/jdbc2/AbstractJdbc2ResultSet.java
===================================================================
RCS file: /usr/local/cvsroot/pgjdbc/pgjdbc/org/postgresql/jdbc2/AbstractJdbc2ResultSet.java,v
retrieving revision 1.43
diff -u -c -r1.43 AbstractJdbc2ResultSet.java
*** org/postgresql/jdbc2/AbstractJdbc2ResultSet.java    16 Jul 2004 09:07:59 -0000    1.43
--- org/postgresql/jdbc2/AbstractJdbc2ResultSet.java    20 Jul 2004 08:35:06 -0000
***************
*** 158,163 ****
--- 158,171 ----
                      // Fetch all results.
                      String cursorName = getString(columnIndex);
                      String fetchSql = "FETCH ALL IN \"" + cursorName + "\"";
+
+                     // nb: no BEGIN triggered here. This is fine. If someone
+                     // committed, and the cursor was not holdable (closing the
+                     // cursor), we avoid starting a new xact and promptly causing
+                     // it to fail. If the cursor *was* holdable, we don't want a
+                     // new xact anyway since holdable cursor state isn't affected
+                     // by xact boundaries. If our caller didn't commit at all, or
+                     // autocommit was on, then we wouldn't issue a BEGIN anyway.
                      ResultSet rs = connection.execSQLQuery(fetchSql);
                      ((AbstractJdbc2ResultSet)rs).setRefCursor(cursorName);
                      return rs;
Index: org/postgresql/jdbc2/AbstractJdbc2Statement.java
===================================================================
RCS file: /usr/local/cvsroot/pgjdbc/pgjdbc/org/postgresql/jdbc2/AbstractJdbc2Statement.java,v
retrieving revision 1.27
diff -u -c -r1.27 AbstractJdbc2Statement.java
*** org/postgresql/jdbc2/AbstractJdbc2Statement.java    16 Jul 2004 09:08:00 -0000    1.27
--- org/postgresql/jdbc2/AbstractJdbc2Statement.java    20 Jul 2004 08:35:06 -0000
***************
*** 282,288 ****
          return executeWithFlags(0);
      }

!     private boolean executeWithFlags(int flags) throws SQLException
      {
          checkClosed();
          if (isFunction && !returnTypeSet)
--- 282,288 ----
          return executeWithFlags(0);
      }

!     public boolean executeWithFlags(int flags) throws SQLException
      {
          checkClosed();
          if (isFunction && !returnTypeSet)
Index: org/postgresql/test/jdbc2/ConnectionTest.java
===================================================================
RCS file: /usr/local/cvsroot/pgjdbc/pgjdbc/org/postgresql/test/jdbc2/ConnectionTest.java,v
retrieving revision 1.14
diff -u -c -r1.14 ConnectionTest.java
*** org/postgresql/test/jdbc2/ConnectionTest.java    10 Apr 2004 13:53:11 -0000    1.14
--- org/postgresql/test/jdbc2/ConnectionTest.java    20 Jul 2004 08:35:06 -0000
***************
*** 242,249 ****
              assertEquals(Connection.TRANSACTION_READ_COMMITTED,
                           con.getTransactionIsolation());

-
-             Statement stmt = con.createStatement();
              // Now run some tests with autocommit enabled.
              con.setAutoCommit(true);

--- 242,247 ----
***************
*** 274,280 ****
--- 272,297 ----
              con.setAutoCommit(false);
              assertEquals(Connection.TRANSACTION_READ_COMMITTED,
                           con.getTransactionIsolation());
+             con.commit();
+
+             // Test that getTransactionIsolation() does not actually start a new txn.
+             con.getTransactionIsolation(); // Shouldn't start a new transaction.
+             con.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE); // Should be ok -- we're not in a
transaction.
+             con.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED); // Should still be ok.
+
+             // Test that we can't change isolation mid-transaction
+             Statement stmt = con.createStatement();
+             stmt.executeQuery("SELECT 1");          // Start transaction.
+             stmt.close();
+
+             try {
+                 con.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);
+                 fail("Expected an exception when changing transaction isolation mid-transaction");
+             } catch (SQLException e) {
+                 // Ok.
+             }

+             con.rollback();
              TestUtil.closeDB(con);
      }


pgsql-jdbc by date:

Previous
From: Oliver Jowett
Date:
Subject: Re: patch: ensure testdbencoding test table is dropped
Next
From: Oliver Jowett
Date:
Subject: JDBC3 + HOLD_CURSORS_OVER_COMMIT