diff options
| author | Ian Foote <python@ian.feete.org> | 2018-08-27 03:25:06 +0100 |
|---|---|---|
| committer | Mariusz Felisiak <felisiak.mariusz@gmail.com> | 2020-04-30 10:43:50 +0200 |
| commit | c226c6cb3209122b6732fd501e2994c408dc258e (patch) | |
| tree | 3a72f1403e4a061c9d4dbf0d7cd88639457f9df2 /django | |
| parent | 555e3a848e7ac13580371c7eafbc89195fee6ea9 (diff) | |
Fixed #20581 -- Added support for deferrable unique constraints.
Diffstat (limited to 'django')
| -rw-r--r-- | django/db/backends/base/features.py | 2 | ||||
| -rw-r--r-- | django/db/backends/base/schema.py | 38 | ||||
| -rw-r--r-- | django/db/backends/oracle/base.py | 10 | ||||
| -rw-r--r-- | django/db/backends/oracle/features.py | 1 | ||||
| -rw-r--r-- | django/db/backends/postgresql/features.py | 1 | ||||
| -rw-r--r-- | django/db/models/base.py | 19 | ||||
| -rw-r--r-- | django/db/models/constraints.py | 42 |
7 files changed, 99 insertions, 14 deletions
diff --git a/django/db/backends/base/features.py b/django/db/backends/base/features.py index c2b92ecdee..a8f55f966c 100644 --- a/django/db/backends/base/features.py +++ b/django/db/backends/base/features.py @@ -20,6 +20,8 @@ class BaseDatabaseFeatures: # Does the backend allow inserting duplicate rows when a unique_together # constraint exists and some fields are nullable but not all of them? supports_partially_nullable_unique_constraints = True + # Does the backend support initially deferrable unique constraints? + supports_deferrable_unique_constraints = False can_use_chunked_reads = True can_return_columns_from_insert = False diff --git a/django/db/backends/base/schema.py b/django/db/backends/base/schema.py index 104f569a07..2b2ad9cdb4 100644 --- a/django/db/backends/base/schema.py +++ b/django/db/backends/base/schema.py @@ -5,7 +5,7 @@ from django.db.backends.ddl_references import ( Columns, ForeignKeyName, IndexName, Statement, Table, ) from django.db.backends.utils import names_digest, split_identifier -from django.db.models import Index +from django.db.models import Deferrable, Index from django.db.transaction import TransactionManagementError, atomic from django.utils import timezone @@ -65,7 +65,7 @@ class BaseDatabaseSchemaEditor: sql_rename_column = "ALTER TABLE %(table)s RENAME COLUMN %(old_column)s TO %(new_column)s" sql_update_with_default = "UPDATE %(table)s SET %(column)s = %(default)s WHERE %(column)s IS NULL" - sql_unique_constraint = "UNIQUE (%(columns)s)" + sql_unique_constraint = "UNIQUE (%(columns)s)%(deferrable)s" sql_check_constraint = "CHECK (%(check)s)" sql_delete_constraint = "ALTER TABLE %(table)s DROP CONSTRAINT %(name)s" sql_constraint = "CONSTRAINT %(name)s %(constraint)s" @@ -73,7 +73,7 @@ class BaseDatabaseSchemaEditor: sql_create_check = "ALTER TABLE %(table)s ADD CONSTRAINT %(name)s CHECK (%(check)s)" sql_delete_check = sql_delete_constraint - sql_create_unique = "ALTER TABLE %(table)s ADD CONSTRAINT %(name)s UNIQUE (%(columns)s)" + sql_create_unique = "ALTER TABLE %(table)s ADD CONSTRAINT %(name)s UNIQUE (%(columns)s)%(deferrable)s" sql_delete_unique = sql_delete_constraint sql_create_fk = ( @@ -1075,7 +1075,20 @@ class BaseDatabaseSchemaEditor: def _delete_fk_sql(self, model, name): return self._delete_constraint_sql(self.sql_delete_fk, model, name) - def _unique_sql(self, model, fields, name, condition=None): + def _deferrable_constraint_sql(self, deferrable): + if deferrable is None: + return '' + if deferrable == Deferrable.DEFERRED: + return ' DEFERRABLE INITIALLY DEFERRED' + if deferrable == Deferrable.IMMEDIATE: + return ' DEFERRABLE INITIALLY IMMEDIATE' + + def _unique_sql(self, model, fields, name, condition=None, deferrable=None): + if ( + deferrable and + not self.connection.features.supports_deferrable_unique_constraints + ): + return None if condition: # Databases support conditional unique constraints via a unique # index. @@ -1085,13 +1098,20 @@ class BaseDatabaseSchemaEditor: return None constraint = self.sql_unique_constraint % { 'columns': ', '.join(map(self.quote_name, fields)), + 'deferrable': self._deferrable_constraint_sql(deferrable), } return self.sql_constraint % { 'name': self.quote_name(name), 'constraint': constraint, } - def _create_unique_sql(self, model, columns, name=None, condition=None): + def _create_unique_sql(self, model, columns, name=None, condition=None, deferrable=None): + if ( + deferrable and + not self.connection.features.supports_deferrable_unique_constraints + ): + return None + def create_unique_name(*args, **kwargs): return self.quote_name(self._create_index_name(*args, **kwargs)) @@ -1113,9 +1133,15 @@ class BaseDatabaseSchemaEditor: name=name, columns=columns, condition=self._index_condition_sql(condition), + deferrable=self._deferrable_constraint_sql(deferrable), ) - def _delete_unique_sql(self, model, name, condition=None): + def _delete_unique_sql(self, model, name, condition=None, deferrable=None): + if ( + deferrable and + not self.connection.features.supports_deferrable_unique_constraints + ): + return None if condition: return ( self._delete_constraint_sql(self.sql_delete_index, model, name) diff --git a/django/db/backends/oracle/base.py b/django/db/backends/oracle/base.py index 8285aacdc1..e9ec2bac51 100644 --- a/django/db/backends/oracle/base.py +++ b/django/db/backends/oracle/base.py @@ -71,9 +71,17 @@ def wrap_oracle_errors(): # message = 'ORA-02091: transaction rolled back # 'ORA-02291: integrity constraint (TEST_DJANGOTEST.SYS # _C00102056) violated - parent key not found' + # or: + # 'ORA-00001: unique constraint (DJANGOTEST.DEFERRABLE_ + # PINK_CONSTRAINT) violated # Convert that case to Django's IntegrityError exception. x = e.args[0] - if hasattr(x, 'code') and hasattr(x, 'message') and x.code == 2091 and 'ORA-02291' in x.message: + if ( + hasattr(x, 'code') and + hasattr(x, 'message') and + x.code == 2091 and + ('ORA-02291' in x.message or 'ORA-00001' in x.message) + ): raise IntegrityError(*tuple(e.args)) raise diff --git a/django/db/backends/oracle/features.py b/django/db/backends/oracle/features.py index 24b281281d..3782874512 100644 --- a/django/db/backends/oracle/features.py +++ b/django/db/backends/oracle/features.py @@ -17,6 +17,7 @@ class DatabaseFeatures(BaseDatabaseFeatures): has_native_duration_field = True can_defer_constraint_checks = True supports_partially_nullable_unique_constraints = False + supports_deferrable_unique_constraints = True truncates_names = True supports_tablespaces = True supports_sequence_reset = False diff --git a/django/db/backends/postgresql/features.py b/django/db/backends/postgresql/features.py index 533c8dc282..3b4199fa78 100644 --- a/django/db/backends/postgresql/features.py +++ b/django/db/backends/postgresql/features.py @@ -56,6 +56,7 @@ class DatabaseFeatures(BaseDatabaseFeatures): supports_aggregate_filter_clause = True supported_explain_formats = {'JSON', 'TEXT', 'XML', 'YAML'} validates_explain_options = False # A query will error on invalid options. + supports_deferrable_unique_constraints = True @cached_property def is_postgresql_9_6(self): diff --git a/django/db/models/base.py b/django/db/models/base.py index 0f8a8c50f3..6c9e9d3707 100644 --- a/django/db/models/base.py +++ b/django/db/models/base.py @@ -1904,6 +1904,25 @@ class Model(metaclass=ModelBase): id='models.W036', ) ) + if not ( + connection.features.supports_deferrable_unique_constraints or + 'supports_deferrable_unique_constraints' in cls._meta.required_db_features + ) and any( + isinstance(constraint, UniqueConstraint) and constraint.deferrable is not None + for constraint in cls._meta.constraints + ): + errors.append( + checks.Warning( + '%s does not support deferrable unique constraints.' + % connection.display_name, + hint=( + "A constraint won't be created. Silence this " + "warning if you don't care about it." + ), + obj=cls, + id='models.W038', + ) + ) return errors diff --git a/django/db/models/constraints.py b/django/db/models/constraints.py index 98912a6467..64bd60484e 100644 --- a/django/db/models/constraints.py +++ b/django/db/models/constraints.py @@ -1,7 +1,9 @@ +from enum import Enum + from django.db.models.query_utils import Q from django.db.models.sql.query import Query -__all__ = ['CheckConstraint', 'UniqueConstraint'] +__all__ = ['CheckConstraint', 'Deferrable', 'UniqueConstraint'] class BaseConstraint: @@ -69,14 +71,28 @@ class CheckConstraint(BaseConstraint): return path, args, kwargs +class Deferrable(Enum): + DEFERRED = 'deferred' + IMMEDIATE = 'immediate' + + class UniqueConstraint(BaseConstraint): - def __init__(self, *, fields, name, condition=None): + def __init__(self, *, fields, name, condition=None, deferrable=None): if not fields: raise ValueError('At least one field is required to define a unique constraint.') if not isinstance(condition, (type(None), Q)): raise ValueError('UniqueConstraint.condition must be a Q instance.') + if condition and deferrable: + raise ValueError( + 'UniqueConstraint with conditions cannot be deferred.' + ) + if not isinstance(deferrable, (type(None), Deferrable)): + raise ValueError( + 'UniqueConstraint.deferrable must be a Deferrable instance.' + ) self.fields = tuple(fields) self.condition = condition + self.deferrable = deferrable super().__init__(name) def _get_condition_sql(self, model, schema_editor): @@ -91,21 +107,30 @@ class UniqueConstraint(BaseConstraint): def constraint_sql(self, model, schema_editor): fields = [model._meta.get_field(field_name).column for field_name in self.fields] condition = self._get_condition_sql(model, schema_editor) - return schema_editor._unique_sql(model, fields, self.name, condition=condition) + return schema_editor._unique_sql( + model, fields, self.name, condition=condition, + deferrable=self.deferrable, + ) def create_sql(self, model, schema_editor): fields = [model._meta.get_field(field_name).column for field_name in self.fields] condition = self._get_condition_sql(model, schema_editor) - return schema_editor._create_unique_sql(model, fields, self.name, condition=condition) + return schema_editor._create_unique_sql( + model, fields, self.name, condition=condition, + deferrable=self.deferrable, + ) def remove_sql(self, model, schema_editor): condition = self._get_condition_sql(model, schema_editor) - return schema_editor._delete_unique_sql(model, self.name, condition=condition) + return schema_editor._delete_unique_sql( + model, self.name, condition=condition, deferrable=self.deferrable, + ) def __repr__(self): - return '<%s: fields=%r name=%r%s>' % ( + return '<%s: fields=%r name=%r%s%s>' % ( self.__class__.__name__, self.fields, self.name, '' if self.condition is None else ' condition=%s' % self.condition, + '' if self.deferrable is None else ' deferrable=%s' % self.deferrable, ) def __eq__(self, other): @@ -113,7 +138,8 @@ class UniqueConstraint(BaseConstraint): return ( self.name == other.name and self.fields == other.fields and - self.condition == other.condition + self.condition == other.condition and + self.deferrable == other.deferrable ) return super().__eq__(other) @@ -122,4 +148,6 @@ class UniqueConstraint(BaseConstraint): kwargs['fields'] = self.fields if self.condition: kwargs['condition'] = self.condition + if self.deferrable: + kwargs['deferrable'] = self.deferrable return path, args, kwargs |
