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 django#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 Diff line number Diff line change
@@ -1,8 +1,11 @@
from django.conf import settings
from django.core import signals
from django.core.exceptions import ImproperlyConfigured
from django.db.utils import (ConnectionHandler, ConnectionRouter,
load_backend, DEFAULT_DB_ALIAS, DatabaseError, IntegrityError)
from django.db.utils import (DEFAULT_DB_ALIAS,
DataError, OperationalError, IntegrityError, InternalError,
ProgrammingError, NotSupportedError, DatabaseError,
InterfaceError, Error,
load_backend, ConnectionHandler, ConnectionRouter)

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

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

def get_connection_params(self):
raise NotImplementedError

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

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

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

def _rollback(self):
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):
"""
Expand Down Expand Up @@ -333,8 +345,9 @@ def check_constraints(self, table_names=None):

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

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

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.OperationalError as e:
# Map some error codes to IntegrityError, since they seem to be
# misclassified and Django would prefer the more logical place.
if e[0] in self.codes_for_integrityerror:
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])
except Database.DatabaseError as e:
six.reraise(utils.DatabaseError, utils.DatabaseError(*tuple(e.args)), sys.exc_info()[2])
raise

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

Database = Database

def __init__(self, *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 Diff line number Diff line change
Expand Up @@ -501,6 +501,8 @@ class DatabaseWrapper(BaseDatabaseWrapper):
'iendswith': "LIKEC UPPER(%s) ESCAPE '\\'",
})

Database = Database

def __init__(self, *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:
try:
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:
# cx_Oracle 5.0.4 raises a cx_Oracle.DatabaseError exception
# with the following attributes and values:
Expand All @@ -620,7 +618,7 @@ def _commit(self):
if hasattr(x, 'code') and hasattr(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.DatabaseError, utils.DatabaseError(*tuple(e.args)), sys.exc_info()[2])
raise

@cached_property
def oracle_version(self):
Expand Down Expand Up @@ -760,13 +758,11 @@ def execute(self, query, params=None):
self._guess_input_sizes([params])
try:
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:
# 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):
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):
# cx_Oracle doesn't support iterators, convert them to lists
Expand All @@ -789,13 +785,11 @@ def executemany(self, query, params=None):
try:
return self.cursor.executemany(query,
[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:
# 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):
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):
row = self.cursor.fetchone()
Expand Down
45 changes: 3 additions & 42 deletions django/db/backends/postgresql_psycopg2/base.py
Original file line number 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")
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):
needs_datetime_string_cast = False
can_return_id_from_insert = True
Expand Down Expand Up @@ -106,6 +72,8 @@ class DatabaseWrapper(BaseDatabaseWrapper):
'iendswith': 'LIKE UPPER(%s)',
}

Database = Database

def __init__(self, *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):
cursor = self.connection.cursor()
cursor.tzinfo_factory = utc_tzinfo_factory if settings.USE_TZ else None
return CursorWrapper(cursor)
return cursor

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
not self.features.uses_autocommit):
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 Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
import decimal
import warnings
import re
import sys

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

Database = Database

def __init__(self, *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=()):
query = self.convert_query(query)
try:
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])
return Database.Cursor.execute(self, query, params)

def executemany(self, query, param_list):
query = self.convert_query(query)
try:
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])
return Database.Cursor.executemany(self, query, param_list)

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):
if dt is None:
Expand Down
13 changes: 10 additions & 3 deletions django/db/backends/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,12 @@ def __init__(self, cursor, db):
def __getattr__(self, attr):
if attr in ('execute', 'executemany', 'callproc'):
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):
return iter(self.cursor)
Expand All @@ -34,7 +39,8 @@ def execute(self, sql, params=()):
self.db.set_dirty()
start = time()
try:
return self.cursor.execute(sql, params)
with self.db.wrap_database_errors():
return self.cursor.execute(sql, params)
finally:
stop = time()
duration = stop - start
Expand All @@ -51,7 +57,8 @@ def executemany(self, sql, param_list):
self.db.set_dirty()
start = time()
try:
return self.cursor.executemany(sql, param_list)
with self.db.wrap_database_errors():
return self.cursor.executemany(sql, param_list)
finally:
stop = time()
duration = stop - start
Expand Down
Loading

0 comments on commit 59a3520

Please sign in to comment.