summaryrefslogtreecommitdiff
path: root/django
diff options
context:
space:
mode:
authorIan Foote <python@ian.feete.org>2018-08-27 03:25:06 +0100
committerMariusz Felisiak <felisiak.mariusz@gmail.com>2020-04-30 10:43:50 +0200
commitc226c6cb3209122b6732fd501e2994c408dc258e (patch)
tree3a72f1403e4a061c9d4dbf0d7cd88639457f9df2 /django
parent555e3a848e7ac13580371c7eafbc89195fee6ea9 (diff)
Fixed #20581 -- Added support for deferrable unique constraints.
Diffstat (limited to 'django')
-rw-r--r--django/db/backends/base/features.py2
-rw-r--r--django/db/backends/base/schema.py38
-rw-r--r--django/db/backends/oracle/base.py10
-rw-r--r--django/db/backends/oracle/features.py1
-rw-r--r--django/db/backends/postgresql/features.py1
-rw-r--r--django/db/models/base.py19
-rw-r--r--django/db/models/constraints.py42
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