From: Mike Bayer Date: Fri, 1 Jul 2011 20:52:11 +0000 (-0400) Subject: - Failures on connect which raise dbapi.Error X-Git-Tag: rel_0_7_2~38 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=e936a7b359a205e0476b932a1f175f5da7289e06;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git - Failures on connect which raise dbapi.Error will forward the error to dialect.is_disconnect() and set the "connection_invalidated" flag if the dialect knows this to be a potentially "retryable" condition. Only Oracle ORA-01033 implemented for now. [ticket:2201] - Added ORA-01033 to disconnect codes, which can be caught during a connection event. [ticket:2201] --- diff --git a/CHANGES b/CHANGES index 4c2243f9fb..44ee35d786 100644 --- a/CHANGES +++ b/CHANGES @@ -66,8 +66,15 @@ CHANGES occur in the context of a statement execution. - - StatementException wrapping will display the - original exception class in the message. + - StatementException wrapping will display the + original exception class in the message. + + - Failures on connect which raise dbapi.Error + will forward the error to dialect.is_disconnect() + and set the "connection_invalidated" flag if + the dialect knows this to be a potentially + "retryable" condition. Only Oracle ORA-01033 + implemented for now. [ticket:2201] - mssql - Adjusted the pyodbc dialect such that bound @@ -83,6 +90,10 @@ CHANGES cx_oracle _Error.code to get at the code, [ticket:2200]. Also in 0.6.9. + - Added ORA-01033 to disconnect codes, which + can be caught during a connection + event. [ticket:2201] + -ext - Fixed bug in the mutable extension whereby if the same type were used twice in one diff --git a/lib/sqlalchemy/dialects/oracle/cx_oracle.py b/lib/sqlalchemy/dialects/oracle/cx_oracle.py index 4f0cdffcfc..29d9e2ab0a 100644 --- a/lib/sqlalchemy/dialects/oracle/cx_oracle.py +++ b/lib/sqlalchemy/dialects/oracle/cx_oracle.py @@ -689,11 +689,14 @@ class OracleDialect_cx_oracle(OracleDialect): error, = e.args if isinstance(e, self.dbapi.InterfaceError): return "not connected" in str(e) - else: + elif hasattr(error, 'code'): # ORA-00028: your session has been killed # ORA-03114: not connected to ORACLE # ORA-03113: end-of-file on communication channel - return error.code in (28, 3114, 3113) + # ORA-01033: ORACLE initialization or shutdown in progress + return error.code in (28, 3114, 3113, 1033) + else: + return False def create_xid(self): """create a two-phase transaction ID. diff --git a/lib/sqlalchemy/dialects/sqlite/pysqlite.py b/lib/sqlalchemy/dialects/sqlite/pysqlite.py index 07df64712e..c8739f242d 100644 --- a/lib/sqlalchemy/dialects/sqlite/pysqlite.py +++ b/lib/sqlalchemy/dialects/sqlite/pysqlite.py @@ -289,6 +289,7 @@ class SQLiteDialect_pysqlite(SQLiteDialect): return ([filename], opts) def is_disconnect(self, e, connection, cursor): - return isinstance(e, self.dbapi.ProgrammingError) and "Cannot operate on a closed database." in str(e) + return isinstance(e, self.dbapi.ProgrammingError) and \ + "Cannot operate on a closed database." in str(e) dialect = SQLiteDialect_pysqlite diff --git a/lib/sqlalchemy/engine/strategies.py b/lib/sqlalchemy/engine/strategies.py index 2508244318..528bc31757 100644 --- a/lib/sqlalchemy/engine/strategies.py +++ b/lib/sqlalchemy/engine/strategies.py @@ -85,7 +85,8 @@ class DefaultEngineStrategy(EngineStrategy): # Py2K import sys raise exc.DBAPIError.instance( - None, None, e, dialect.dbapi.Error), \ + None, None, e, dialect.dbapi.Error, + connection_invalidated=dialect.is_disconnect(e, None, None)), \ None, sys.exc_info()[2] # end Py2K diff --git a/test/engine/test_parseconnect.py b/test/engine/test_parseconnect.py index 3b3e09a7ad..1c1ab6aad1 100644 --- a/test/engine/test_parseconnect.py +++ b/test/engine/test_parseconnect.py @@ -5,7 +5,7 @@ import sqlalchemy.engine.url as url from sqlalchemy import create_engine, engine_from_config, exc from sqlalchemy.engine import _coerce_config import sqlalchemy as tsa -from test.lib import fixtures +from test.lib import fixtures, testing class ParseConnectTest(fixtures.TestBase): def test_rfc1738(self): @@ -175,7 +175,7 @@ pool_timeout=10 module=dbapi, _initialize=False) assert e.pool._recycle == 472 - def test_badargs(self): + def test_bad_args(self): assert_raises(exc.ArgumentError, create_engine, 'foobar://', module=mock_dbapi) @@ -201,19 +201,49 @@ pool_timeout=10 assert_raises(TypeError, create_engine, 'mysql+mysqldb://', use_unicode=True, module=mock_dbapi) + @testing.requires.sqlite + def test_wraps_connect_in_dbapi(self): # sqlite uses SingletonThreadPool which doesnt have max_overflow assert_raises(TypeError, create_engine, 'sqlite://', max_overflow=5, module=mock_sqlite_dbapi) + e = create_engine('sqlite://', connect_args={'use_unicode' + : True}, convert_unicode=True) try: - e = create_engine('sqlite://', connect_args={'use_unicode' - : True}, convert_unicode=True) - except ImportError: - # no sqlite - pass - else: - # raises DBAPIerror due to use_unicode not a sqlite arg - assert_raises(tsa.exc.DBAPIError, e.connect) + e.connect() + except tsa.exc.DBAPIError, de: + assert not de.connection_invalidated + + def test_ensure_dialect_does_is_disconnect_no_conn(self): + """test that is_disconnect() doesn't choke if no connection, cursor given.""" + dialect = testing.db.dialect + dbapi = dialect.dbapi + assert not dialect.is_disconnect(dbapi.OperationalError("test"), None, None) + + @testing.requires.sqlite + def test_invalidate_on_connect(self): + """test that is_disconnect() is called during connect. + + interpretation of connection failures are not supported by + every backend. + + """ + # pretend pysqlite throws the + # "Cannot operate on a closed database." error + # on connect. IRL we'd be getting Oracle's "shutdown in progress" + + import sqlite3 + class ThrowOnConnect(MockDBAPI): + dbapi = sqlite3 + Error = sqlite3.Error + ProgrammingError = sqlite3.ProgrammingError + def connect(self, *args, **kw): + raise sqlite3.ProgrammingError("Cannot operate on a closed database.") + try: + create_engine('sqlite://', module=ThrowOnConnect()).connect() + assert False + except tsa.exc.DBAPIError, de: + assert de.connection_invalidated def test_urlattr(self): """test the url attribute on ``Engine``.""" @@ -264,6 +294,9 @@ pool_timeout=10 ) class MockDBAPI(object): + version_info = sqlite_version_info = 99, 9, 9 + sqlite_version = '99.9.9' + def __init__(self, **kwargs): self.kwargs = kwargs self.paramstyle = 'named' @@ -293,5 +326,3 @@ class MockCursor(object): mock_dbapi = MockDBAPI() mock_sqlite_dbapi = msd = MockDBAPI() -msd.version_info = msd.sqlite_version_info = 99, 9, 9 -msd.sqlite_version = '99.9.9'