]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- [feature] Added support for the "isolation_level"
authorMike Bayer <mike_mp@zzzcomputing.com>
Sun, 12 Feb 2012 23:07:41 +0000 (18:07 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Sun, 12 Feb 2012 23:07:41 +0000 (18:07 -0500)
parameter to all MySQL dialects.  Thanks
to mu_mind for the patch here. [ticket:2394]
- add documentation examples for mysql, postgresql
- pep8ing

CHANGES
lib/sqlalchemy/dialects/mysql/base.py
lib/sqlalchemy/dialects/postgresql/base.py
lib/sqlalchemy/dialects/postgresql/psycopg2.py
test/engine/test_transaction.py
test/lib/requires.py

diff --git a/CHANGES b/CHANGES
index 90398f8f37a857660c05746bffe441213279a9fa..49488c601b649accd9c59c218a69c1d40bbe7c4d 100644 (file)
--- a/CHANGES
+++ b/CHANGES
@@ -81,9 +81,14 @@ CHANGES
     commit or rollback transaction with errors
     on engine.begin().
 
+- mysql
+  - [feature] Added support for the "isolation_level"
+    parameter to all MySQL dialects.  Thanks
+    to mu_mind for the patch here. [ticket:2394]
+
 - oracle
 
-  - Added missing compilation support for 
+  - [bug] Added missing compilation support for 
     LONG [ticket:2401]
 
 0.7.5 (January 28, 2012)
index 6aa250d2d8bb9027d900153dcb95f8b0b5c2b006..d0dd28e70ca945ac0b1af6238b6c92aa88a2c82a 100644 (file)
@@ -84,6 +84,23 @@ all lower case both within SQLAlchemy as well as on the MySQL
 database itself, especially if database reflection features are
 to be used.
 
+Transaction Isolation Level
+---------------------------
+
+:func:`.create_engine` accepts an ``isolation_level`` 
+parameter which results in the command ``SET SESSION 
+TRANSACTION ISOLATION LEVEL <level>`` being invoked for 
+every new connection. Valid values for this parameter are
+``READ COMMITTED``, ``READ UNCOMMITTED``, 
+``REPEATABLE READ``, and ``SERIALIZABLE``::
+
+    engine = create_engine(
+                    "mysql://scott:tiger@localhost/test", 
+                    isolation_level="READ UNCOMMITTED"
+                )
+
+(new in 0.7.6)
+
 Keys
 ----
 
@@ -1768,8 +1785,40 @@ class MySQLDialect(default.DefaultDialect):
     _backslash_escapes = True
     _server_ansiquotes = False
 
-    def __init__(self, use_ansiquotes=None, **kwargs):
+    def __init__(self, use_ansiquotes=None, isolation_level=None, **kwargs):
         default.DefaultDialect.__init__(self, **kwargs)
+        self.isolation_level = isolation_level
+
+    def on_connect(self):
+        if self.isolation_level is not None:
+            def connect(conn):
+                self.set_isolation_level(conn, self.isolation_level)
+            return connect
+        else:
+            return None
+
+    _isolation_lookup = set(['SERIALIZABLE', 
+                'READ UNCOMMITTED', 'READ COMMITTED', 'REPEATABLE READ'])
+
+    def set_isolation_level(self, connection, level):
+        level = level.replace('_', ' ')
+        if level not in self._isolation_lookup:
+            raise exc.ArgumentError(
+                "Invalid value '%s' for isolation_level. "
+                "Valid isolation levels for %s are %s" % 
+                (level, self.name, ", ".join(self._isolation_lookup))
+                )
+        cursor = connection.cursor()
+        cursor.execute("SET SESSION TRANSACTION ISOLATION LEVEL %s" % level)
+        cursor.execute("COMMIT")
+        cursor.close()
+
+    def get_isolation_level(self, connection):
+        cursor = connection.cursor()
+        cursor.execute('SELECT @@tx_isolation')
+        val = cursor.fetchone()[0]
+        cursor.close()
+        return val.upper().replace("-", " ")
 
     def do_commit(self, connection):
         """Execute a COMMIT."""
index 69c11d80faef014089931e0c6c1eb9ef8bbfe788..c4c2bbdb4e56cae4faf033978197ee278aa28f08 100644 (file)
@@ -47,9 +47,18 @@ Transaction Isolation Level
 :func:`.create_engine` accepts an ``isolation_level`` parameter which results
 in the command ``SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL
 <level>`` being invoked for every new connection. Valid values for this
-parameter are ``READ_COMMITTED``, ``READ_UNCOMMITTED``, ``REPEATABLE_READ``,
-and ``SERIALIZABLE``.  Note that the psycopg2 dialect does *not* use this
-technique and uses psycopg2-specific APIs (see that dialect for details).
+parameter are ``READ COMMITTED``, ``READ UNCOMMITTED``, ``REPEATABLE READ``,
+and ``SERIALIZABLE``::
+
+    engine = create_engine(
+                    "postgresql+pg8000://scott:tiger@localhost/test", 
+                    isolation_level="READ UNCOMMITTED"
+                )
+
+When using the psycopg2 dialect, a psycopg2-specific method of setting
+transaction isolation level is used, but the API of ``isolation_level``
+remains the same - see :ref:`psycopg2_isolation`.
+
 
 Remote / Cross-Schema Table Introspection
 -----------------------------------------
index c66180f98510a3b005a0b41d35363217cc09925c..47c9e2232661005540e8140fd9e035d2a55ddba3 100644 (file)
@@ -97,6 +97,8 @@ Transactions
 
 The psycopg2 dialect fully supports SAVEPOINT and two-phase commit operations.
 
+.. _psycopg2_isolation:
+
 Transaction Isolation Level
 ---------------------------
 
index 2d6861f0d2138253e04638e15dc00b69deb1a72a..04a3e642cf74bb086bca4bc1d596aa83c2bc531a 100644 (file)
@@ -1148,6 +1148,8 @@ class IsolationLevelTest(fixtures.TestBase):
             return 'SERIALIZABLE'
         elif testing.against('postgresql'):
             return 'READ COMMITTED'
+        elif testing.against('mysql'):
+            return "REPEATABLE READ"
         else:
             assert False, "default isolation level not known"
 
@@ -1156,20 +1158,24 @@ class IsolationLevelTest(fixtures.TestBase):
             return 'READ UNCOMMITTED'
         elif testing.against('postgresql'):
             return 'SERIALIZABLE'
+        elif testing.against('mysql'):
+            return "SERIALIZABLE"
         else:
             assert False, "non default isolation level not known"
 
     def test_engine_param_stays(self):
 
         eng = testing_engine()
-        isolation_level = eng.dialect.get_isolation_level(eng.connect().connection)
+        isolation_level = eng.dialect.get_isolation_level(
+                                eng.connect().connection)
         level = self._non_default_isolation_level()
 
         ne_(isolation_level, level)
 
         eng = testing_engine(options=dict(isolation_level=level))
         eq_(
-            eng.dialect.get_isolation_level(eng.connect().connection),
+            eng.dialect.get_isolation_level(
+                                eng.connect().connection),
             level
         )
 
@@ -1190,33 +1196,48 @@ class IsolationLevelTest(fixtures.TestBase):
 
     def test_default_level(self):
         eng = testing_engine(options=dict())
-        isolation_level = eng.dialect.get_isolation_level(eng.connect().connection)
+        isolation_level = eng.dialect.get_isolation_level(
+                                        eng.connect().connection)
         eq_(isolation_level, self._default_isolation_level())
 
     def test_reset_level(self):
         eng = testing_engine(options=dict())
         conn = eng.connect()
-        eq_(eng.dialect.get_isolation_level(conn.connection), self._default_isolation_level())
+        eq_(
+            eng.dialect.get_isolation_level(conn.connection), 
+            self._default_isolation_level()
+        )
 
-        eng.dialect.set_isolation_level(conn.connection, self._non_default_isolation_level())
-        eq_(eng.dialect.get_isolation_level(conn.connection), self._non_default_isolation_level())
+        eng.dialect.set_isolation_level(
+                conn.connection, self._non_default_isolation_level()
+            )
+        eq_(
+            eng.dialect.get_isolation_level(conn.connection), 
+            self._non_default_isolation_level()
+        )
 
         eng.dialect.reset_isolation_level(conn.connection)
-        eq_(eng.dialect.get_isolation_level(conn.connection), self._default_isolation_level())
+        eq_(
+            eng.dialect.get_isolation_level(conn.connection), 
+            self._default_isolation_level()
+        )
 
         conn.close()
 
     def test_reset_level_with_setting(self):
-        eng = testing_engine(options=dict(isolation_level=self._non_default_isolation_level()))
+        eng = testing_engine(options=dict(
+                            isolation_level=
+                                self._non_default_isolation_level()))
         conn = eng.connect()
-        eq_(eng.dialect.get_isolation_level(conn.connection), self._non_default_isolation_level())
-
-        eng.dialect.set_isolation_level(conn.connection, self._default_isolation_level())
-        eq_(eng.dialect.get_isolation_level(conn.connection), self._default_isolation_level())
-
+        eq_(eng.dialect.get_isolation_level(conn.connection),
+            self._non_default_isolation_level())
+        eng.dialect.set_isolation_level(conn.connection,
+                self._default_isolation_level())
+        eq_(eng.dialect.get_isolation_level(conn.connection),
+            self._default_isolation_level())
         eng.dialect.reset_isolation_level(conn.connection)
-        eq_(eng.dialect.get_isolation_level(conn.connection), self._non_default_isolation_level())
-
+        eq_(eng.dialect.get_isolation_level(conn.connection),
+            self._non_default_isolation_level())
         conn.close()
 
     def test_invalid_level(self):
@@ -1225,27 +1246,41 @@ class IsolationLevelTest(fixtures.TestBase):
             exc.ArgumentError, 
                 "Invalid value '%s' for isolation_level. "
                 "Valid isolation levels for %s are %s" % 
-                ("FOO", eng.dialect.name, ", ".join(eng.dialect._isolation_lookup)),
+                ("FOO", eng.dialect.name, 
+                ", ".join(eng.dialect._isolation_lookup)),
             eng.connect)
 
     def test_per_connection(self):
         from sqlalchemy.pool import QueuePool
-        eng = testing_engine(options=dict(poolclass=QueuePool, pool_size=2, max_overflow=0))
+        eng = testing_engine(options=dict(
+                                poolclass=QueuePool, 
+                                pool_size=2, max_overflow=0))
 
         c1 = eng.connect()
-        c1 = c1.execution_options(isolation_level=self._non_default_isolation_level())
-
+        c1 = c1.execution_options(
+                    isolation_level=self._non_default_isolation_level()
+                )
         c2 = eng.connect()
-        eq_(eng.dialect.get_isolation_level(c1.connection), self._non_default_isolation_level())
-        eq_(eng.dialect.get_isolation_level(c2.connection), self._default_isolation_level())
-
+        eq_(
+            eng.dialect.get_isolation_level(c1.connection),
+            self._non_default_isolation_level()
+        )
+        eq_(
+            eng.dialect.get_isolation_level(c2.connection),
+            self._default_isolation_level()
+        )
         c1.close()
         c2.close()
         c3 = eng.connect()
-        eq_(eng.dialect.get_isolation_level(c3.connection), self._default_isolation_level())
-
+        eq_(
+            eng.dialect.get_isolation_level(c3.connection),
+            self._default_isolation_level()
+        )
         c4 = eng.connect()
-        eq_(eng.dialect.get_isolation_level(c4.connection), self._default_isolation_level())
+        eq_(
+            eng.dialect.get_isolation_level(c4.connection),
+            self._default_isolation_level()
+        )
 
         c3.close()
         c4.close()
@@ -1257,7 +1292,8 @@ class IsolationLevelTest(fixtures.TestBase):
             r"on Connection.execution_options\(\), or "
             r"per-engine using the isolation_level "
             r"argument to create_engine\(\).",
-            select([1]).execution_options, isolation_level=self._non_default_isolation_level()
+            select([1]).execution_options, 
+                    isolation_level=self._non_default_isolation_level()
         )
 
 
@@ -1269,5 +1305,7 @@ class IsolationLevelTest(fixtures.TestBase):
             r"To set engine-wide isolation level, "
             r"use the isolation_level argument to create_engine\(\).",
             create_engine,
-            testing.db.url, execution_options={'isolation_level':self._non_default_isolation_level}
+            testing.db.url, 
+                execution_options={'isolation_level':
+                            self._non_default_isolation_level}
         )
index 89b9a317b05a530d55a9c8328ff276a3ef1e31a6..ce1cdb6f54965a9694d707596b2903324dd775a1 100644 (file)
@@ -107,7 +107,7 @@ def updateable_autoincrement_pks(fn):
 def isolation_level(fn):
     return _chain_decorators_on(
         fn,
-        only_on(('postgresql', 'sqlite'), "DBAPI has no isolation level support"),
+        only_on(('postgresql', 'sqlite', 'mysql'), "DBAPI has no isolation level support"),
         fails_on('postgresql+pypostgresql',
                       'pypostgresql bombs on multiple isolation level calls')
     )