From 11ee9746a0530ec38f523fb4de44950d9b783877 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Fri, 29 Jan 2010 15:45:55 +0000 Subject: [PATCH] Fixed #12702 -- Introduced a common implementation of DatabaseError and IntegrityError, so that database backends can (re)raise common error classes. Thanks for Waldemar Kornewald for the report. git-svn-id: http://code.djangoproject.com/svn/django/trunk@12352 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/db/__init__.py | 5 +-- django/db/backends/mysql/base.py | 14 ++++++- django/db/backends/oracle/base.py | 21 ++++++---- django/db/backends/postgresql/base.py | 19 +++++++-- .../db/backends/postgresql_psycopg2/base.py | 39 ++++++++++++++++++- django/db/backends/sqlite3/base.py | 24 +++++++++--- django/db/utils.py | 13 +++++++ 7 files changed, 113 insertions(+), 22 deletions(-) diff --git a/django/db/__init__.py b/django/db/__init__.py index 69996e6ad5..4bae04ab9a 100644 --- a/django/db/__init__.py +++ b/django/db/__init__.py @@ -1,7 +1,8 @@ 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 +from django.db.utils import ConnectionHandler, ConnectionRouter, load_backend, DEFAULT_DB_ALIAS, \ + DatabaseError, IntegrityError from django.utils.functional import curry __all__ = ('backend', 'connection', 'connections', 'router', 'DatabaseError', @@ -73,8 +74,6 @@ router = ConnectionRouter(settings.DATABASE_ROUTERS) # connections['default'] instead. connection = connections[DEFAULT_DB_ALIAS] backend = load_backend(connection.settings_dict['ENGINE']) -DatabaseError = backend.DatabaseError -IntegrityError = backend.IntegrityError # Register an event that closes the database connection # when a Django request is finished. diff --git a/django/db/backends/mysql/base.py b/django/db/backends/mysql/base.py index 5c6abce8aa..2a5acad0c0 100644 --- a/django/db/backends/mysql/base.py +++ b/django/db/backends/mysql/base.py @@ -5,6 +5,7 @@ Requires MySQLdb: http://sourceforge.net/projects/mysql-python """ import re +import sys try: import MySQLdb as Database @@ -24,6 +25,7 @@ if (version < (1,2,1) or (version[:3] == (1, 2, 1) and from MySQLdb.converters import conversions from MySQLdb.constants import FIELD_TYPE, FLAG, CLIENT +from django.db import utils from django.db.backends import * from django.db.backends.signals import connection_created from django.db.backends.mysql.client import DatabaseClient @@ -82,22 +84,30 @@ class CursorWrapper(object): def execute(self, query, args=None): try: return self.cursor.execute(query, args) + except Database.IntegrityError, e: + raise utils.IntegrityError, utils.IntegrityError(*tuple(e)), sys.exc_info()[2] except Database.OperationalError, 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: - raise Database.IntegrityError(tuple(e)) + raise utils.IntegrityError, utils.IntegrityError(*tuple(e)), sys.exc_info()[2] raise + except Database.DatabaseError, e: + raise utils.DatabaseError, utils.DatabaseError(*tuple(e)), sys.exc_info()[2] def executemany(self, query, args): try: return self.cursor.executemany(query, args) + except Database.IntegrityError, e: + raise utils.IntegrityError, utils.IntegrityError(*tuple(e)), sys.exc_info()[2] except Database.OperationalError, 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: - raise Database.IntegrityError(tuple(e)) + raise utils.IntegrityError, utils.IntegrityError(*tuple(e)), sys.exc_info()[2] raise + except Database.DatabaseError, e: + raise utils.DatabaseError, utils.DatabaseError(*tuple(e)), sys.exc_info()[2] def __getattr__(self, attr): if attr in self.__dict__: diff --git a/django/db/backends/oracle/base.py b/django/db/backends/oracle/base.py index bbbf3c4c55..f87a482609 100644 --- a/django/db/backends/oracle/base.py +++ b/django/db/backends/oracle/base.py @@ -4,8 +4,10 @@ Oracle database backend for Django. Requires cx_Oracle: http://cx-oracle.sourceforge.net/ """ -import os + import datetime +import os +import sys import time try: from decimal import Decimal @@ -24,6 +26,7 @@ except ImportError, e: from django.core.exceptions import ImproperlyConfigured raise ImproperlyConfigured("Error loading cx_Oracle module: %s" % e) +from django.db import utils from django.db.backends import * from django.db.backends.signals import connection_created from django.db.backends.oracle.client import DatabaseClient @@ -480,11 +483,13 @@ class FormatStylePlaceholderCursor(object): self._guess_input_sizes([params]) try: return self.cursor.execute(query, self._param_generator(params)) - except DatabaseError, e: + except Database.IntegrityError, e: + raise utils.IntegrityError, utils.IntegrityError(*tuple(e)), sys.exc_info()[2] + except Database.DatabaseError, e: # cx_Oracle <= 4.4.0 wrongly raises a DatabaseError for ORA-01400. if e.args[0].code == 1400 and not isinstance(e, IntegrityError): - e = IntegrityError(e.args[0]) - raise e + raise utils.IntegrityError, utils.IntegrityError(*tuple(e)), sys.exc_info()[2] + raise utils.DatabaseError, utils.DatabaseError(*tuple(e)), sys.exc_info()[2] def executemany(self, query, params=None): try: @@ -504,11 +509,13 @@ class FormatStylePlaceholderCursor(object): try: return self.cursor.executemany(query, [self._param_generator(p) for p in formatted]) - except DatabaseError, e: + except Database.IntegrityError, e: + raise utils.IntegrityError, utils.IntegrityError(*tuple(e)), sys.exc_info()[2] + except Database.DatabaseError, e: # cx_Oracle <= 4.4.0 wrongly raises a DatabaseError for ORA-01400. if e.args[0].code == 1400 and not isinstance(e, IntegrityError): - e = IntegrityError(e.args[0]) - raise e + raise utils.IntegrityError, utils.IntegrityError(*tuple(e)), sys.exc_info()[2] + raise utils.DatabaseError, utils.DatabaseError(*tuple(e)), sys.exc_info()[2] def fetchone(self): row = self.cursor.fetchone() diff --git a/django/db/backends/postgresql/base.py b/django/db/backends/postgresql/base.py index 119df9f171..faf5dd20db 100644 --- a/django/db/backends/postgresql/base.py +++ b/django/db/backends/postgresql/base.py @@ -4,6 +4,9 @@ PostgreSQL database backend for Django. Requires psycopg 1: http://initd.org/projects/psycopg1 """ +import sys + +from django.db import utils from django.db.backends import * from django.db.backends.signals import connection_created from django.db.backends.postgresql.client import DatabaseClient @@ -50,11 +53,21 @@ class UnicodeCursorWrapper(object): return tuple([smart_str(p, self.charset, True) for p in params]) def execute(self, sql, params=()): - return self.cursor.execute(smart_str(sql, self.charset), self.format_params(params)) + try: + return self.cursor.execute(query, args) + except Database.IntegrityError, e: + raise utils.IntegrityError, utils.IntegrityError(*tuple(e)), sys.exc_info()[2] + except Database.DatabaseError, e: + raise utils.DatabaseError, utils.DatabaseError(*tuple(e)), sys.exc_info()[2] def executemany(self, sql, param_list): - new_param_list = [self.format_params(params) for params in param_list] - return self.cursor.executemany(sql, new_param_list) + try: + new_param_list = [self.format_params(params) for params in param_list] + return self.cursor.executemany(sql, new_param_list) + except Database.IntegrityError, e: + raise utils.IntegrityError, utils.IntegrityError(*tuple(e)), sys.exc_info()[2] + except Database.DatabaseError, e: + raise utils.DatabaseError, utils.DatabaseError(*tuple(e)), sys.exc_info()[2] def __getattr__(self, attr): if attr in self.__dict__: diff --git a/django/db/backends/postgresql_psycopg2/base.py b/django/db/backends/postgresql_psycopg2/base.py index fcd84cd290..b6823f2303 100644 --- a/django/db/backends/postgresql_psycopg2/base.py +++ b/django/db/backends/postgresql_psycopg2/base.py @@ -4,6 +4,9 @@ PostgreSQL database backend for Django. Requires psycopg 2: http://initd.org/projects/psycopg2 """ +import sys + +from django.db import utils from django.db.backends import * from django.db.backends.signals import connection_created from django.db.backends.postgresql.operations import DatabaseOperations as PostgresqlDatabaseOperations @@ -27,6 +30,40 @@ psycopg2.extensions.register_type(psycopg2.extensions.UNICODE) psycopg2.extensions.register_adapter(SafeString, psycopg2.extensions.QuotedString) psycopg2.extensions.register_adapter(SafeUnicode, psycopg2.extensions.QuotedString) +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, e: + raise utils.IntegrityError, utils.IntegrityError(*tuple(e)), sys.exc_info()[2] + except Database.DatabaseError, e: + raise utils.DatabaseError, utils.DatabaseError(*tuple(e)), sys.exc_info()[2] + + def executemany(self, query, args): + try: + return self.cursor.executemany(query, args) + except Database.IntegrityError, e: + raise utils.IntegrityError, utils.IntegrityError(*tuple(e)), sys.exc_info()[2] + except Database.DatabaseError, e: + raise utils.DatabaseError, utils.DatabaseError(*tuple(e)), 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 = False @@ -118,7 +155,7 @@ class DatabaseWrapper(BaseDatabaseWrapper): # versions that support it, but, right now, that's hard to # do without breaking other things (#10509). self.features.can_return_id_from_insert = True - return cursor + return CursorWrapper(cursor) def _enter_transaction_management(self, managed): """ diff --git a/django/db/backends/sqlite3/base.py b/django/db/backends/sqlite3/base.py index 000b55f686..a9b1aa3f8b 100644 --- a/django/db/backends/sqlite3/base.py +++ b/django/db/backends/sqlite3/base.py @@ -7,6 +7,9 @@ Python 2.5 and later can use a pysqlite2 module or the sqlite3 module in the standard library. """ +import sys + +from django.db import utils from django.db.backends import * from django.db.backends.signals import connection_created from django.db.backends.sqlite3.client import DatabaseClient @@ -185,16 +188,25 @@ class SQLiteCursorWrapper(Database.Cursor): you'll need to use "%%s". """ def execute(self, query, params=()): - query = self.convert_query(query, len(params)) - return Database.Cursor.execute(self, query, params) + try: + query = self.convert_query(query, len(params)) + return Database.Cursor.execute(self, query, params) + except Database.IntegrityError, e: + raise utils.IntegrityError, utils.IntegrityError(*tuple(e)), sys.exc_info()[2] + except Database.DatabaseError, e: + raise utils.DatabaseError, utils.DatabaseError(*tuple(e)), sys.exc_info()[2] def executemany(self, query, param_list): try: - query = self.convert_query(query, len(param_list[0])) - return Database.Cursor.executemany(self, query, param_list) + query = self.convert_query(query, len(param_list[0])) + return Database.Cursor.executemany(self, query, param_list) except (IndexError,TypeError): - # No parameter list provided - return None + # No parameter list provided + return None + except Database.IntegrityError, e: + raise utils.IntegrityError, utils.IntegrityError(*tuple(e)), sys.exc_info()[2] + except Database.DatabaseError, e: + raise utils.DatabaseError, utils.DatabaseError(*tuple(e)), sys.exc_info()[2] def convert_query(self, query, num_params): return query % tuple("?" * num_params) diff --git a/django/db/utils.py b/django/db/utils.py index fe5d7944aa..00b3568cb0 100644 --- a/django/db/utils.py +++ b/django/db/utils.py @@ -7,6 +7,16 @@ from django.utils.importlib import import_module DEFAULT_DB_ALIAS = 'default' +# Define some exceptions that mirror the PEP249 interface. +# We will rethrow any backend-specific errors using these +# common wrappers +class DatabaseError(Exception): + pass + +class IntegrityError(DatabaseError): + pass + + def load_backend(backend_name): try: module = import_module('.base', 'django.db.backends.%s' % backend_name) @@ -40,9 +50,11 @@ def load_backend(backend_name): else: raise # If there's some other error, this must be an error in Django itself. + class ConnectionDoesNotExist(Exception): pass + class ConnectionHandler(object): def __init__(self, databases): self.databases = databases @@ -87,6 +99,7 @@ class ConnectionHandler(object): def all(self): return [self[alias] for alias in self] + class ConnectionRouter(object): def __init__(self, routers): self.routers = []