]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- Added support for the case of the misbehaving DBAPI that has
authorMike Bayer <mike_mp@zzzcomputing.com>
Fri, 15 May 2015 16:35:21 +0000 (12:35 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Fri, 15 May 2015 16:38:28 +0000 (12:38 -0400)
pep-249 exception names linked to exception classes of an entirely
different name, preventing SQLAlchemy's own exception wrapping from
wrapping the error appropriately.
The SQLAlchemy dialect in use needs to implement a new
accessor :attr:`.DefaultDialect.dbapi_exception_translation_map`
to support this feature; this is implemented now for the py-postgresql
dialect.
fixes #3421

doc/build/changelog/changelog_10.rst
lib/sqlalchemy/dialects/postgresql/pypostgresql.py
lib/sqlalchemy/engine/base.py
lib/sqlalchemy/engine/default.py
lib/sqlalchemy/engine/interfaces.py
lib/sqlalchemy/exc.py
lib/sqlalchemy/testing/suite/__init__.py
lib/sqlalchemy/testing/suite/test_dialect.py [new file with mode: 0644]
test/base/test_except.py

index 0a012962f82b8124ab9ab1e7aafb883a4484fb3d..f4091a98847d2f2c8efc3eb51c5c0224d5d132c2 100644 (file)
 .. changelog::
     :version: 1.0.5
 
+    .. change::
+        :tags: bug, engine
+        :tickets: 3421
+
+        Added support for the case of the misbehaving DBAPI that has
+        pep-249 exception names linked to exception classes of an entirely
+        different name, preventing SQLAlchemy's own exception wrapping from
+        wrapping the error appropriately.
+        The SQLAlchemy dialect in use needs to implement a new
+        accessor :attr:`.DefaultDialect.dbapi_exception_translation_map`
+        to support this feature; this is implemented now for the py-postgresql
+        dialect.
+
     .. change::
         :tags: bug, orm
         :tickets: 3420
index 00c67d1707311078a8e3fd87660eb3170d53072c..db6d5e16ca0638f4e10c9291a9f367ce77190a05 100644 (file)
@@ -65,6 +65,23 @@ class PGDialect_pypostgresql(PGDialect):
         from postgresql.driver import dbapi20
         return dbapi20
 
+    _DBAPI_ERROR_NAMES = [
+        "Error",
+        "InterfaceError", "DatabaseError", "DataError",
+        "OperationalError", "IntegrityError", "InternalError",
+        "ProgrammingError", "NotSupportedError"
+    ]
+
+    @util.memoized_property
+    def dbapi_exception_translation_map(self):
+        if self.dbapi is None:
+            return {}
+
+        return dict(
+            (getattr(self.dbapi, name).__name__, name)
+            for name in self._DBAPI_ERROR_NAMES
+        )
+
     def create_connect_args(self, url):
         opts = url.translate_connect_args(username='user')
         if 'port' in opts:
index af310c4506c7c5ff0c40eb7c6fc94de326972887..7ebe39bbf8f7e5e6a8124f49269b157943539afd 100644 (file)
@@ -1261,7 +1261,8 @@ class Connection(Connectable):
                 exc.DBAPIError.instance(statement,
                                         parameters,
                                         e,
-                                        self.dialect.dbapi.Error),
+                                        self.dialect.dbapi.Error,
+                                        dialect=self.dialect),
                 exc_info
             )
         self._reentrant_error = True
@@ -1277,7 +1278,8 @@ class Connection(Connectable):
                     parameters,
                     e,
                     self.dialect.dbapi.Error,
-                    connection_invalidated=self._is_disconnect)
+                    connection_invalidated=self._is_disconnect,
+                    dialect=self.dialect)
             else:
                 sqlalchemy_exception = None
 
index 763e85f82123e5a7201c5d5a84e5d1f33b96196e..9330a602c16f9228ec08cf61270768fd3327ef87 100644 (file)
@@ -157,6 +157,15 @@ class DefaultDialect(interfaces.Dialect):
 
     reflection_options = ()
 
+    dbapi_exception_translation_map = util.immutabledict()
+    """mapping used in the extremely unusual case that a DBAPI's
+    published exceptions don't actually have the __name__ that they
+    are linked towards.
+
+    .. versionadded:: 1.0.5
+
+    """
+
     def __init__(self, convert_unicode=False,
                  encoding='utf-8', paramstyle=None, dbapi=None,
                  implicit_returning=None,
index 2dd1921626aff43d28b4a10cd04727d99fc224b1..73a8b4635172e9e4a9a8c390696dcd9b6a5d97b1 100644 (file)
@@ -150,6 +150,16 @@ class Dialect(object):
       This will prevent types.Boolean from generating a CHECK
       constraint when that type is used.
 
+    dbapi_exception_translation_map
+       A dictionary of names that will contain as values the names of
+       pep-249 exceptions ("IntegrityError", "OperationalError", etc)
+       keyed to alternate class names, to support the case where a
+       DBAPI has exception classes that aren't named as they are
+       referred to (e.g. IntegrityError = MyException).   In the vast
+       majority of cases this dictionary is empty.
+
+       .. versionadded:: 1.0.5
+
     """
 
     _has_events = False
index 9b27436b3064ac6c543f869601707032078e84ff..3a4f346e0adee5602e235b2c9e8da2e027abfdbf 100644 (file)
@@ -13,8 +13,6 @@ raised as a result of DBAPI exceptions are all subclasses of
 
 """
 
-import traceback
-
 
 class SQLAlchemyError(Exception):
     """Generic error class."""
@@ -278,7 +276,8 @@ class DBAPIError(StatementError):
     @classmethod
     def instance(cls, statement, params,
                  orig, dbapi_base_err,
-                 connection_invalidated=False):
+                 connection_invalidated=False,
+                 dialect=None):
         # Don't ever wrap these, just return them directly as if
         # DBAPIError didn't exist.
         if (isinstance(orig, BaseException) and
@@ -300,6 +299,9 @@ class DBAPIError(StatementError):
             glob = globals()
             for super_ in orig.__class__.__mro__:
                 name = super_.__name__
+                if dialect:
+                    name = dialect.dbapi_exception_translation_map.get(
+                        name, name)
                 if name in glob and issubclass(glob[name], DBAPIError):
                     cls = glob[name]
                     break
index 780aa40aa71b31053d73ffd8f03bab78a23b5e17..9eeffd4cb0e6d92b0e01dfc2b8e33188729f7d64 100644 (file)
@@ -1,4 +1,5 @@
 
+from sqlalchemy.testing.suite.test_dialect import *
 from sqlalchemy.testing.suite.test_ddl import *
 from sqlalchemy.testing.suite.test_insert import *
 from sqlalchemy.testing.suite.test_sequence import *
diff --git a/lib/sqlalchemy/testing/suite/test_dialect.py b/lib/sqlalchemy/testing/suite/test_dialect.py
new file mode 100644 (file)
index 0000000..5ad5694
--- /dev/null
@@ -0,0 +1,39 @@
+from .. import fixtures, config
+from sqlalchemy import exc
+from sqlalchemy import Integer, String
+from .. import assert_raises
+from ..schema import Table, Column
+
+
+class ExceptionTest(fixtures.TablesTest):
+    """Test basic exception wrapping.
+
+    DBAPIs vary a lot in exception behavior so to actually anticipate
+    specific exceptions from real round trips, we need to be conservative.
+
+    """
+    run_deletes = 'each'
+
+    __backend__ = True
+
+    @classmethod
+    def define_tables(cls, metadata):
+        Table('manual_pk', metadata,
+              Column('id', Integer, primary_key=True, autoincrement=False),
+              Column('data', String(50))
+              )
+
+    def test_integrity_error(self):
+
+        with config.db.begin() as conn:
+            conn.execute(
+                self.tables.manual_pk.insert(),
+                {'id': 1, 'data': 'd1'}
+            )
+
+            assert_raises(
+                exc.IntegrityError,
+                conn.execute,
+                self.tables.manual_pk.insert(),
+                {'id': 1, 'data': 'd1'}
+            )
index 918e7a0423d37c8406f931236bd3c87ecbd945bc..9e8dd47603ccedc967c4f24b76e1ccc40c488278 100644 (file)
@@ -4,6 +4,7 @@
 from sqlalchemy import exc as sa_exceptions
 from sqlalchemy.testing import fixtures
 from sqlalchemy.testing import eq_
+from sqlalchemy.engine import default
 
 
 class Error(Exception):
@@ -28,8 +29,28 @@ class OutOfSpec(DatabaseError):
     pass
 
 
+# exception with a totally different name...
+class WrongNameError(DatabaseError):
+    pass
+
+# but they're going to call it their "IntegrityError"
+IntegrityError = WrongNameError
+
+
+# and they're going to subclass it!
+class SpecificIntegrityError(WrongNameError):
+    pass
+
+
 class WrapTest(fixtures.TestBase):
 
+    def _translating_dialect_fixture(self):
+        d = default.DefaultDialect()
+        d.dbapi_exception_translation_map = {
+            "WrongNameError": "IntegrityError"
+        }
+        return d
+
     def test_db_error_normal(self):
         try:
             raise sa_exceptions.DBAPIError.instance(
@@ -160,6 +181,42 @@ class WrapTest(fixtures.TestBase):
         except sa_exceptions.ArgumentError:
             self.assert_(False)
 
+        dialect = self._translating_dialect_fixture()
+        try:
+            raise sa_exceptions.DBAPIError.instance(
+                '', [],
+                sa_exceptions.ArgumentError(), DatabaseError,
+                dialect=dialect)
+        except sa_exceptions.DBAPIError as e:
+            self.assert_(e.__class__ is sa_exceptions.DBAPIError)
+        except sa_exceptions.ArgumentError:
+            self.assert_(False)
+
+    def test_db_error_dbapi_uses_wrong_names(self):
+        dialect = self._translating_dialect_fixture()
+
+        try:
+            raise sa_exceptions.DBAPIError.instance(
+                '', [], IntegrityError(),
+                DatabaseError, dialect=dialect)
+        except sa_exceptions.DBAPIError as e:
+            self.assert_(e.__class__ is sa_exceptions.IntegrityError)
+
+        try:
+            raise sa_exceptions.DBAPIError.instance(
+                '', [], SpecificIntegrityError(),
+                DatabaseError, dialect=dialect)
+        except sa_exceptions.DBAPIError as e:
+            self.assert_(e.__class__ is sa_exceptions.IntegrityError)
+
+        try:
+            raise sa_exceptions.DBAPIError.instance(
+                '', [], SpecificIntegrityError(),
+                DatabaseError)
+        except sa_exceptions.DBAPIError as e:
+            # doesn't work without a dialect
+            self.assert_(e.__class__ is not sa_exceptions.IntegrityError)
+
     def test_db_error_keyboard_interrupt(self):
         try:
             raise sa_exceptions.DBAPIError.instance(