Commits

Mike Bayer committed 6bfddcf

- [feature] Added new connection event
dbapi_error(). Is called for all DBAPI-level
errors passing the original DBAPI exception
before SQLAlchemy modifies the state
of the cursor.

Comments (0)

Files changed (4)

     as an Index could be a placeholder for just an 
     index of a certain name.
 
+  - [feature] Added new connection event
+    dbapi_error(). Is called for all DBAPI-level
+    errors passing the original DBAPI exception
+    before SQLAlchemy modifies the state 
+    of the cursor.
+
 - mssql
   - [feature] Added interim create_engine flag
     supports_unicode_binds to PyODBC dialect,

lib/sqlalchemy/engine/base.py

                 (statement is not None and context is None)
 
             if should_wrap and context:
+                if self._has_events:
+                    self.engine.dispatch.dbapi_error(self, 
+                                                    cursor, 
+                                                    statement, 
+                                                    parameters, 
+                                                    context, 
+                                                    e)
                 context.handle_dbapi_exception(e)
 
             is_disconnect = isinstance(e, self.dialect.dbapi.Error) and \
                                 self.dialect.is_disconnect(e, self.__connection, cursor)
+
+
             if is_disconnect:
                 self.invalidate(e)
                 self.engine.dispose()

lib/sqlalchemy/events.py

                         parameters, context, executemany):
         """Intercept low-level cursor execute() events."""
 
+    def dbapi_error(self, conn, cursor, statement, parameters, 
+                        context, exception):
+        """Intercept a raw DBAPI error.
+        
+        This event is called with the DBAPI exception instance 
+        received from the DBAPI itself, *before* SQLAlchemy wraps the 
+        exception with it's own exception wrappers, and before any
+        other operations are performed on the DBAPI cursor; the
+        existing transaction remains in effect as well as any state
+        on the cursor.
+        
+        The use case here is to inject low-level exception handling
+        into an :class:`.Engine`, typically for logging and
+        debugging purposes.   In general, user code should **not** modify
+        any state or throw any exceptions here as this will
+        interfere with SQLAlchemy's cleanup and error handling
+        routines.
+        
+        Subsequent to this hook, SQLAlchemy may attempt any
+        number of operations on the connection/cursor, including
+        closing the cursor, rolling back of the transaction in the 
+        case of connectionless execution, and disposing of the entire
+        connection pool if a "disconnect" was detected.   The
+        exception is then wrapped in a SQLAlchemy DBAPI exception
+        wrapper and re-thrown.
+        
+        New in 0.7.7.
+
+        """
+
     def begin(self, conn):
         """Intercept begin() events."""
 

test/engine/test_execute.py

         e1.execute(select([1]).compile(dialect=e1.dialect))
         e1._execute_compiled(select([1]).compile(dialect=e1.dialect), [], {})
 
+    def test_exception_event(self):
+        engine = engines.testing_engine()
+        canary = []
+
+        @event.listens_for(engine, 'dbapi_error')
+        def err(conn, cursor, stmt, parameters, context, exception):
+            canary.append((stmt, parameters, exception))
+
+        conn = engine.connect()
+        try:
+            conn.execute("SELECT FOO FROM I_DONT_EXIST")
+            assert False
+        except tsa.exc.DBAPIError, e:
+            assert canary[0][2] is e.orig
+            assert canary[0][0] == "SELECT FOO FROM I_DONT_EXIST"
+
+
     @testing.fails_on('firebird', 'Data type unknown')
     def test_execute_events(self):