From: Mike Bayer Date: Sun, 12 Feb 2012 23:07:41 +0000 (-0500) Subject: - [feature] Added support for the "isolation_level" X-Git-Tag: rel_0_7_6~57 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=d101c597f57bfa7a72636b8e76a14d5c9f82bfd4;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git - [feature] Added support for the "isolation_level" parameter to all MySQL dialects. Thanks to mu_mind for the patch here. [ticket:2394] - add documentation examples for mysql, postgresql - pep8ing --- diff --git a/CHANGES b/CHANGES index 90398f8f37..49488c601b 100644 --- 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) diff --git a/lib/sqlalchemy/dialects/mysql/base.py b/lib/sqlalchemy/dialects/mysql/base.py index 6aa250d2d8..d0dd28e70c 100644 --- a/lib/sqlalchemy/dialects/mysql/base.py +++ b/lib/sqlalchemy/dialects/mysql/base.py @@ -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 `` 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.""" diff --git a/lib/sqlalchemy/dialects/postgresql/base.py b/lib/sqlalchemy/dialects/postgresql/base.py index 69c11d80fa..c4c2bbdb4e 100644 --- a/lib/sqlalchemy/dialects/postgresql/base.py +++ b/lib/sqlalchemy/dialects/postgresql/base.py @@ -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 `` 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 ----------------------------------------- diff --git a/lib/sqlalchemy/dialects/postgresql/psycopg2.py b/lib/sqlalchemy/dialects/postgresql/psycopg2.py index c66180f985..47c9e22326 100644 --- a/lib/sqlalchemy/dialects/postgresql/psycopg2.py +++ b/lib/sqlalchemy/dialects/postgresql/psycopg2.py @@ -97,6 +97,8 @@ Transactions The psycopg2 dialect fully supports SAVEPOINT and two-phase commit operations. +.. _psycopg2_isolation: + Transaction Isolation Level --------------------------- diff --git a/test/engine/test_transaction.py b/test/engine/test_transaction.py index 2d6861f0d2..04a3e642cf 100644 --- a/test/engine/test_transaction.py +++ b/test/engine/test_transaction.py @@ -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} ) diff --git a/test/lib/requires.py b/test/lib/requires.py index 89b9a317b0..ce1cdb6f54 100644 --- a/test/lib/requires.py +++ b/test/lib/requires.py @@ -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') )