Skip to content

Commit

Permalink
Refactored database exceptions wrapping.
Browse files Browse the repository at this point in the history
Squashed commit of the following:

commit 2181d833ed1a2e422494738dcef311164c4e097e
Author: Aymeric Augustin <aymeric.augustin@m4x.org>
Date:   Wed Feb 27 14:28:39 2013 +0100

    Fixed #15901 -- Wrapped all PEP-249 exceptions.

commit 5476a5d93c19aa2f928c497d39ce6e33f52694e2
Author: Aymeric Augustin <aymeric.augustin@m4x.org>
Date:   Tue Feb 26 17:26:52 2013 +0100

    Added PEP 3134 exception chaining.

    Thanks Jacob Kaplan-Moss for the suggestion.

commit 9365fad0a650328002fb424457d675a273c95802
Author: Aymeric Augustin <aymeric.augustin@m4x.org>
Date:   Tue Feb 26 17:13:49 2013 +0100

    Improved API for wrapping database errors.

    Thanks Alex Gaynor for the proposal.

commit 1b463b765f2826f73a8d9266795cd5da4f8d5e9e
Author: Aymeric Augustin <aymeric.augustin@m4x.org>
Date:   Tue Feb 26 15:00:39 2013 +0100

    Removed redundant exception wrapping.

    This is now taken care of by the cursor wrapper.

commit 524bc7345a724bf526bdd2dd1bcf5ede67d6bb5c
Author: Aymeric Augustin <aymeric.augustin@m4x.org>
Date:   Tue Feb 26 14:55:10 2013 +0100

    Wrapped database exceptions in the base backend.

    This covers the most common PEP-249 APIs:
    - Connection APIs: close(), commit(), rollback(), cursor()
    - Cursor APIs: callproc(), close(), execute(), executemany(),
      fetchone(), fetchmany(), fetchall(), nextset().

    Fixed #19920.

commit a66746bb5f0839f35543222787fce3b6a0d0a3ea
Author: Aymeric Augustin <aymeric.augustin@m4x.org>
Date:   Tue Feb 26 14:53:34 2013 +0100

    Added a wrap_database_exception context manager and decorator.

    It re-throws backend-specific exceptions using Django's common wrappers.
  • Loading branch information
aaugustin committed Feb 27, 2013
1 parent 50328f0 commit 59a3520
Show file tree
Hide file tree
Showing 10 changed files with 147 additions and 100 deletions.
7 changes: 5 additions & 2 deletions django/db/__init__.py
Original file line number Original file line Diff line number Diff line change
@@ -1,8 +1,11 @@
from django.conf import settings from django.conf import settings
from django.core import signals from django.core import signals
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.db.utils import (ConnectionHandler, ConnectionRouter, from django.db.utils import (DEFAULT_DB_ALIAS,
load_backend, DEFAULT_DB_ALIAS, DatabaseError, IntegrityError) DataError, OperationalError, IntegrityError, InternalError,
ProgrammingError, NotSupportedError, DatabaseError,
InterfaceError, Error,
load_backend, ConnectionHandler, ConnectionRouter)


__all__ = ('backend', 'connection', 'connections', 'router', 'DatabaseError', __all__ = ('backend', 'connection', 'connections', 'router', 'DatabaseError',
'IntegrityError', 'DEFAULT_DB_ALIAS') 'IntegrityError', 'DEFAULT_DB_ALIAS')
Expand Down
33 changes: 23 additions & 10 deletions django/db/backends/__init__.py
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from django.db.backends.signals import connection_created from django.db.backends.signals import connection_created
from django.db.backends import util from django.db.backends import util
from django.db.transaction import TransactionManagementError from django.db.transaction import TransactionManagementError
from django.db.utils import DatabaseErrorWrapper
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.importlib import import_module from django.utils.importlib import import_module
from django.utils import six from django.utils import six
Expand Down Expand Up @@ -57,6 +58,9 @@ def __ne__(self, other):
def __hash__(self): def __hash__(self):
return hash(self.alias) return hash(self.alias)


def wrap_database_errors(self):
return DatabaseErrorWrapper(self.Database)

def get_connection_params(self): def get_connection_params(self):
raise NotImplementedError raise NotImplementedError


Expand All @@ -70,20 +74,28 @@ def create_cursor(self):
raise NotImplementedError raise NotImplementedError


def _cursor(self): def _cursor(self):
if self.connection is None: with self.wrap_database_errors():
conn_params = self.get_connection_params() if self.connection is None:
self.connection = self.get_new_connection(conn_params) conn_params = self.get_connection_params()
self.init_connection_state() self.connection = self.get_new_connection(conn_params)
connection_created.send(sender=self.__class__, connection=self) self.init_connection_state()
return self.create_cursor() connection_created.send(sender=self.__class__, connection=self)
return self.create_cursor()


def _commit(self): def _commit(self):
if self.connection is not None: if self.connection is not None:
return self.connection.commit() with self.wrap_database_errors():
return self.connection.commit()


def _rollback(self): def _rollback(self):
if self.connection is not None: if self.connection is not None:
return self.connection.rollback() with self.wrap_database_errors():
return self.connection.rollback()

def _close(self):
if self.connection is not None:
with self.wrap_database_errors():
return self.connection.close()


def _enter_transaction_management(self, managed): def _enter_transaction_management(self, managed):
""" """
Expand Down Expand Up @@ -333,8 +345,9 @@ def check_constraints(self, table_names=None):


def close(self): def close(self):
self.validate_thread_sharing() self.validate_thread_sharing()
if self.connection is not None: try:
self.connection.close() self._close()
finally:
self.connection = None self.connection = None
self.set_clean() self.set_clean()


Expand Down
14 changes: 4 additions & 10 deletions django/db/backends/mysql/base.py
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -116,30 +116,22 @@ def __init__(self, cursor):
def execute(self, query, args=None): def execute(self, query, args=None):
try: try:
return self.cursor.execute(query, args) return self.cursor.execute(query, args)
except Database.IntegrityError as e:
six.reraise(utils.IntegrityError, utils.IntegrityError(*tuple(e.args)), sys.exc_info()[2])
except Database.OperationalError as e: except Database.OperationalError as e:
# Map some error codes to IntegrityError, since they seem to be # Map some error codes to IntegrityError, since they seem to be
# misclassified and Django would prefer the more logical place. # misclassified and Django would prefer the more logical place.
if e[0] in self.codes_for_integrityerror: if e[0] in self.codes_for_integrityerror:
six.reraise(utils.IntegrityError, utils.IntegrityError(*tuple(e.args)), sys.exc_info()[2]) six.reraise(utils.IntegrityError, utils.IntegrityError(*tuple(e.args)), sys.exc_info()[2])
six.reraise(utils.DatabaseError, utils.DatabaseError(*tuple(e.args)), sys.exc_info()[2]) raise
except Database.DatabaseError as e:
six.reraise(utils.DatabaseError, utils.DatabaseError(*tuple(e.args)), sys.exc_info()[2])


def executemany(self, query, args): def executemany(self, query, args):
try: try:
return self.cursor.executemany(query, args) return self.cursor.executemany(query, args)
except Database.IntegrityError as e:
six.reraise(utils.IntegrityError, utils.IntegrityError(*tuple(e.args)), sys.exc_info()[2])
except Database.OperationalError as e: except Database.OperationalError as e:
# Map some error codes to IntegrityError, since they seem to be # Map some error codes to IntegrityError, since they seem to be
# misclassified and Django would prefer the more logical place. # misclassified and Django would prefer the more logical place.
if e[0] in self.codes_for_integrityerror: if e[0] in self.codes_for_integrityerror:
six.reraise(utils.IntegrityError, utils.IntegrityError(*tuple(e.args)), sys.exc_info()[2]) six.reraise(utils.IntegrityError, utils.IntegrityError(*tuple(e.args)), sys.exc_info()[2])
six.reraise(utils.DatabaseError, utils.DatabaseError(*tuple(e.args)), sys.exc_info()[2]) raise
except Database.DatabaseError as e:
six.reraise(utils.DatabaseError, utils.DatabaseError(*tuple(e.args)), sys.exc_info()[2])


def __getattr__(self, attr): def __getattr__(self, attr):
if attr in self.__dict__: if attr in self.__dict__:
Expand Down Expand Up @@ -391,6 +383,8 @@ class DatabaseWrapper(BaseDatabaseWrapper):
'iendswith': 'LIKE %s', 'iendswith': 'LIKE %s',
} }


Database = Database

def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(DatabaseWrapper, self).__init__(*args, **kwargs) super(DatabaseWrapper, self).__init__(*args, **kwargs)


Expand Down
16 changes: 5 additions & 11 deletions django/db/backends/oracle/base.py
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -501,6 +501,8 @@ class DatabaseWrapper(BaseDatabaseWrapper):
'iendswith': "LIKEC UPPER(%s) ESCAPE '\\'", 'iendswith': "LIKEC UPPER(%s) ESCAPE '\\'",
}) })


Database = Database

def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(DatabaseWrapper, self).__init__(*args, **kwargs) super(DatabaseWrapper, self).__init__(*args, **kwargs)


Expand Down Expand Up @@ -604,10 +606,6 @@ def _commit(self):
if self.connection is not None: if self.connection is not None:
try: try:
return self.connection.commit() return self.connection.commit()
except Database.IntegrityError as e:
# In case cx_Oracle implements (now or in a future version)
# raising this specific exception
six.reraise(utils.IntegrityError, utils.IntegrityError(*tuple(e.args)), sys.exc_info()[2])
except Database.DatabaseError as e: except Database.DatabaseError as e:
# cx_Oracle 5.0.4 raises a cx_Oracle.DatabaseError exception # cx_Oracle 5.0.4 raises a cx_Oracle.DatabaseError exception
# with the following attributes and values: # with the following attributes and values:
Expand All @@ -620,7 +618,7 @@ def _commit(self):
if hasattr(x, 'code') and hasattr(x, 'message') \ if hasattr(x, 'code') and hasattr(x, 'message') \
and x.code == 2091 and 'ORA-02291' in x.message: and x.code == 2091 and 'ORA-02291' in x.message:
six.reraise(utils.IntegrityError, utils.IntegrityError(*tuple(e.args)), sys.exc_info()[2]) six.reraise(utils.IntegrityError, utils.IntegrityError(*tuple(e.args)), sys.exc_info()[2])
six.reraise(utils.DatabaseError, utils.DatabaseError(*tuple(e.args)), sys.exc_info()[2]) raise


@cached_property @cached_property
def oracle_version(self): def oracle_version(self):
Expand Down Expand Up @@ -760,13 +758,11 @@ def execute(self, query, params=None):
self._guess_input_sizes([params]) self._guess_input_sizes([params])
try: try:
return self.cursor.execute(query, self._param_generator(params)) return self.cursor.execute(query, self._param_generator(params))
except Database.IntegrityError as e:
six.reraise(utils.IntegrityError, utils.IntegrityError(*tuple(e.args)), sys.exc_info()[2])
except Database.DatabaseError as e: except Database.DatabaseError as e:
# cx_Oracle <= 4.4.0 wrongly raises a DatabaseError for ORA-01400. # cx_Oracle <= 4.4.0 wrongly raises a DatabaseError for ORA-01400.
if hasattr(e.args[0], 'code') and e.args[0].code == 1400 and not isinstance(e, IntegrityError): if hasattr(e.args[0], 'code') and e.args[0].code == 1400 and not isinstance(e, IntegrityError):
six.reraise(utils.IntegrityError, utils.IntegrityError(*tuple(e.args)), sys.exc_info()[2]) six.reraise(utils.IntegrityError, utils.IntegrityError(*tuple(e.args)), sys.exc_info()[2])
six.reraise(utils.DatabaseError, utils.DatabaseError(*tuple(e.args)), sys.exc_info()[2]) raise


def executemany(self, query, params=None): def executemany(self, query, params=None):
# cx_Oracle doesn't support iterators, convert them to lists # cx_Oracle doesn't support iterators, convert them to lists
Expand All @@ -789,13 +785,11 @@ def executemany(self, query, params=None):
try: try:
return self.cursor.executemany(query, return self.cursor.executemany(query,
[self._param_generator(p) for p in formatted]) [self._param_generator(p) for p in formatted])
except Database.IntegrityError as e:
six.reraise(utils.IntegrityError, utils.IntegrityError(*tuple(e.args)), sys.exc_info()[2])
except Database.DatabaseError as e: except Database.DatabaseError as e:
# cx_Oracle <= 4.4.0 wrongly raises a DatabaseError for ORA-01400. # cx_Oracle <= 4.4.0 wrongly raises a DatabaseError for ORA-01400.
if hasattr(e.args[0], 'code') and e.args[0].code == 1400 and not isinstance(e, IntegrityError): if hasattr(e.args[0], 'code') and e.args[0].code == 1400 and not isinstance(e, IntegrityError):
six.reraise(utils.IntegrityError, utils.IntegrityError(*tuple(e.args)), sys.exc_info()[2]) six.reraise(utils.IntegrityError, utils.IntegrityError(*tuple(e.args)), sys.exc_info()[2])
six.reraise(utils.DatabaseError, utils.DatabaseError(*tuple(e.args)), sys.exc_info()[2]) raise


def fetchone(self): def fetchone(self):
row = self.cursor.fetchone() row = self.cursor.fetchone()
Expand Down
45 changes: 3 additions & 42 deletions django/db/backends/postgresql_psycopg2/base.py
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -40,40 +40,6 @@ def utc_tzinfo_factory(offset):
raise AssertionError("database connection isn't set to UTC") raise AssertionError("database connection isn't set to UTC")
return utc return utc


class CursorWrapper(object):
"""
A thin wrapper around psycopg2's normal cursor class so that we can catch
particular exception instances and reraise them with the right types.
"""

def __init__(self, cursor):
self.cursor = cursor

def execute(self, query, args=None):
try:
return self.cursor.execute(query, args)
except Database.IntegrityError as e:
six.reraise(utils.IntegrityError, utils.IntegrityError(*tuple(e.args)), sys.exc_info()[2])
except Database.DatabaseError as e:
six.reraise(utils.DatabaseError, utils.DatabaseError(*tuple(e.args)), sys.exc_info()[2])

def executemany(self, query, args):
try:
return self.cursor.executemany(query, args)
except Database.IntegrityError as e:
six.reraise(utils.IntegrityError, utils.IntegrityError(*tuple(e.args)), sys.exc_info()[2])
except Database.DatabaseError as e:
six.reraise(utils.DatabaseError, utils.DatabaseError(*tuple(e.args)), sys.exc_info()[2])

def __getattr__(self, attr):
if attr in self.__dict__:
return self.__dict__[attr]
else:
return getattr(self.cursor, attr)

def __iter__(self):
return iter(self.cursor)

class DatabaseFeatures(BaseDatabaseFeatures): class DatabaseFeatures(BaseDatabaseFeatures):
needs_datetime_string_cast = False needs_datetime_string_cast = False
can_return_id_from_insert = True can_return_id_from_insert = True
Expand Down Expand Up @@ -106,6 +72,8 @@ class DatabaseWrapper(BaseDatabaseWrapper):
'iendswith': 'LIKE UPPER(%s)', 'iendswith': 'LIKE UPPER(%s)',
} }


Database = Database

def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(DatabaseWrapper, self).__init__(*args, **kwargs) super(DatabaseWrapper, self).__init__(*args, **kwargs)


Expand Down Expand Up @@ -207,7 +175,7 @@ def init_connection_state(self):
def create_cursor(self): def create_cursor(self):
cursor = self.connection.cursor() cursor = self.connection.cursor()
cursor.tzinfo_factory = utc_tzinfo_factory if settings.USE_TZ else None cursor.tzinfo_factory = utc_tzinfo_factory if settings.USE_TZ else None
return CursorWrapper(cursor) return cursor


def _enter_transaction_management(self, managed): def _enter_transaction_management(self, managed):
""" """
Expand Down Expand Up @@ -245,10 +213,3 @@ def set_dirty(self):
if ((self.transaction_state and self.transaction_state[-1]) or if ((self.transaction_state and self.transaction_state[-1]) or
not self.features.uses_autocommit): not self.features.uses_autocommit):
super(DatabaseWrapper, self).set_dirty() super(DatabaseWrapper, self).set_dirty()

def _commit(self):
if self.connection is not None:
try:
return self.connection.commit()
except Database.IntegrityError as e:
six.reraise(utils.IntegrityError, utils.IntegrityError(*tuple(e.args)), sys.exc_info()[2])
19 changes: 5 additions & 14 deletions django/db/backends/sqlite3/base.py
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
import decimal import decimal
import warnings import warnings
import re import re
import sys


from django.db import utils from django.db import utils
from django.db.backends import * from django.db.backends import *
Expand Down Expand Up @@ -291,6 +290,8 @@ class DatabaseWrapper(BaseDatabaseWrapper):
'iendswith': "LIKE %s ESCAPE '\\'", 'iendswith': "LIKE %s ESCAPE '\\'",
} }


Database = Database

def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(DatabaseWrapper, self).__init__(*args, **kwargs) super(DatabaseWrapper, self).__init__(*args, **kwargs)


Expand Down Expand Up @@ -398,24 +399,14 @@ class SQLiteCursorWrapper(Database.Cursor):
""" """
def execute(self, query, params=()): def execute(self, query, params=()):
query = self.convert_query(query) query = self.convert_query(query)
try: return Database.Cursor.execute(self, query, params)
return Database.Cursor.execute(self, query, params)
except Database.IntegrityError as e:
six.reraise(utils.IntegrityError, utils.IntegrityError(*tuple(e.args)), sys.exc_info()[2])
except Database.DatabaseError as e:
six.reraise(utils.DatabaseError, utils.DatabaseError(*tuple(e.args)), sys.exc_info()[2])


def executemany(self, query, param_list): def executemany(self, query, param_list):
query = self.convert_query(query) query = self.convert_query(query)
try: return Database.Cursor.executemany(self, query, param_list)
return Database.Cursor.executemany(self, query, param_list)
except Database.IntegrityError as e:
six.reraise(utils.IntegrityError, utils.IntegrityError(*tuple(e.args)), sys.exc_info()[2])
except Database.DatabaseError as e:
six.reraise(utils.DatabaseError, utils.DatabaseError(*tuple(e.args)), sys.exc_info()[2])


def convert_query(self, query): def convert_query(self, query):
return FORMAT_QMARK_REGEX.sub('?', query).replace('%%','%') return FORMAT_QMARK_REGEX.sub('?', query).replace('%%', '%')


def _sqlite_date_extract(lookup_type, dt): def _sqlite_date_extract(lookup_type, dt):
if dt is None: if dt is None:
Expand Down
13 changes: 10 additions & 3 deletions django/db/backends/util.py
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -22,7 +22,12 @@ def __init__(self, cursor, db):
def __getattr__(self, attr): def __getattr__(self, attr):
if attr in ('execute', 'executemany', 'callproc'): if attr in ('execute', 'executemany', 'callproc'):
self.db.set_dirty() self.db.set_dirty()
return getattr(self.cursor, attr) cursor_attr = getattr(self.cursor, attr)
if attr in ('callproc', 'close', 'execute', 'executemany',
'fetchone', 'fetchmany', 'fetchall', 'nextset'):
return self.db.wrap_database_errors()(cursor_attr)
else:
return cursor_attr


def __iter__(self): def __iter__(self):
return iter(self.cursor) return iter(self.cursor)
Expand All @@ -34,7 +39,8 @@ def execute(self, sql, params=()):
self.db.set_dirty() self.db.set_dirty()
start = time() start = time()
try: try:
return self.cursor.execute(sql, params) with self.db.wrap_database_errors():
return self.cursor.execute(sql, params)
finally: finally:
stop = time() stop = time()
duration = stop - start duration = stop - start
Expand All @@ -51,7 +57,8 @@ def executemany(self, sql, param_list):
self.db.set_dirty() self.db.set_dirty()
start = time() start = time()
try: try:
return self.cursor.executemany(sql, param_list) with self.db.wrap_database_errors():
return self.cursor.executemany(sql, param_list)
finally: finally:
stop = time() stop = time()
duration = stop - start duration = stop - start
Expand Down
Loading

0 comments on commit 59a3520

Please sign in to comment.