summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Bayer <mike_mp@zzzcomputing.com>2011-07-01 16:52:11 -0400
committerMike Bayer <mike_mp@zzzcomputing.com>2011-07-01 16:52:11 -0400
commite936a7b359a205e0476b932a1f175f5da7289e06 (patch)
tree161e369330c3446b6bbd122fc2a4cdbadda00317
parent409a95adf44f577a204114469ff414bebefca293 (diff)
downloadsqlalchemy-e936a7b359a205e0476b932a1f175f5da7289e06.tar.gz
- 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]
-rw-r--r--CHANGES15
-rw-r--r--lib/sqlalchemy/dialects/oracle/cx_oracle.py7
-rw-r--r--lib/sqlalchemy/dialects/sqlite/pysqlite.py3
-rw-r--r--lib/sqlalchemy/engine/strategies.py3
-rw-r--r--test/engine/test_parseconnect.py55
5 files changed, 65 insertions, 18 deletions
diff --git a/CHANGES b/CHANGES
index 4c2243f9f..44ee35d78 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 4f0cdffcf..29d9e2ab0 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 07df64712..c8739f242 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 250824431..528bc3175 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 3b3e09a7a..1c1ab6aad 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'