mirror of
https://github.com/django/django.git
synced 2025-10-31 09:41:08 +00:00
Fixed #20581 -- Added support for deferrable unique constraints.
This commit is contained in:
committed by
Mariusz Felisiak
parent
555e3a848e
commit
c226c6cb32
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user