summaryrefslogtreecommitdiff
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
parent555e3a848e7ac13580371c7eafbc89195fee6ea9 (diff)
Fixed #20581 -- Added support for deferrable unique constraints.
-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
-rw-r--r--docs/ref/checks.txt2
-rw-r--r--docs/ref/models/constraints.txt34
-rw-r--r--docs/releases/3.1.txt3
-rw-r--r--tests/constraints/models.py22
-rw-r--r--tests/constraints/tests.py97
-rw-r--r--tests/invalid_models_tests/test_models.py44
-rw-r--r--tests/migrations/test_operations.py158
14 files changed, 457 insertions, 16 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
diff --git a/docs/ref/checks.txt b/docs/ref/checks.txt
index d9b56c3c58..daf651392f 100644
--- a/docs/ref/checks.txt
+++ b/docs/ref/checks.txt
@@ -354,6 +354,8 @@ Models
* **models.W036**: ``<database>`` does not support unique constraints with
conditions.
* **models.W037**: ``<database>`` does not support indexes with conditions.
+* **models.W038**: ``<database>`` does not support deferrable unique
+ constraints.
Security
--------
diff --git a/docs/ref/models/constraints.txt b/docs/ref/models/constraints.txt
index 9e3119f600..00e907a882 100644
--- a/docs/ref/models/constraints.txt
+++ b/docs/ref/models/constraints.txt
@@ -76,7 +76,7 @@ The name of the constraint.
``UniqueConstraint``
====================
-.. class:: UniqueConstraint(*, fields, name, condition=None)
+.. class:: UniqueConstraint(*, fields, name, condition=None, deferrable=None)
Creates a unique constraint in the database.
@@ -119,3 +119,35 @@ ensures that each user only has one draft.
These conditions have the same database restrictions as
:attr:`Index.condition`.
+
+``deferrable``
+--------------
+
+.. attribute:: UniqueConstraint.deferrable
+
+.. versionadded:: 3.1
+
+Set this parameter to create a deferrable unique constraint. Accepted values
+are ``Deferrable.DEFERRED`` or ``Deferrable.IMMEDIATE``. For example::
+
+ from django.db.models import Deferrable, UniqueConstraint
+
+ UniqueConstraint(
+ name='unique_order',
+ fields=['order'],
+ deferrable=Deferrable.DEFERRED,
+ )
+
+By default constraints are not deferred. A deferred constraint will not be
+enforced until the end of the transaction. An immediate constraint will be
+enforced immediately after every command.
+
+.. admonition:: MySQL, MariaDB, and SQLite.
+
+ Deferrable unique constraints are ignored on MySQL, MariaDB, and SQLite as
+ neither supports them.
+
+.. warning::
+
+ Deferred unique constraints may lead to a `performance penalty
+ <https://www.postgresql.org/docs/current/sql-createtable.html#id-1.9.3.85.9.4>`_.
diff --git a/docs/releases/3.1.txt b/docs/releases/3.1.txt
index 1de4f24684..ae482f0129 100644
--- a/docs/releases/3.1.txt
+++ b/docs/releases/3.1.txt
@@ -381,6 +381,9 @@ Models
<sqlite3.Connection.create_function>` on Python 3.8+. This allows using them
in check constraints and partial indexes.
+* The new :attr:`.UniqueConstraint.deferrable` attribute allows creating
+ deferrable unique constraints.
+
Pagination
~~~~~~~~~~
diff --git a/tests/constraints/models.py b/tests/constraints/models.py
index 98955498d4..3d091f6ccf 100644
--- a/tests/constraints/models.py
+++ b/tests/constraints/models.py
@@ -59,6 +59,28 @@ class UniqueConstraintConditionProduct(models.Model):
]
+class UniqueConstraintDeferrable(models.Model):
+ name = models.CharField(max_length=255)
+ shelf = models.CharField(max_length=31)
+
+ class Meta:
+ required_db_features = {
+ 'supports_deferrable_unique_constraints',
+ }
+ constraints = [
+ models.UniqueConstraint(
+ fields=['name'],
+ name='name_init_deferred_uniq',
+ deferrable=models.Deferrable.DEFERRED,
+ ),
+ models.UniqueConstraint(
+ fields=['shelf'],
+ name='sheld_init_immediate_uniq',
+ deferrable=models.Deferrable.IMMEDIATE,
+ ),
+ ]
+
+
class AbstractModel(models.Model):
age = models.IntegerField()
diff --git a/tests/constraints/tests.py b/tests/constraints/tests.py
index 85edb51aa7..8eb62a940d 100644
--- a/tests/constraints/tests.py
+++ b/tests/constraints/tests.py
@@ -3,11 +3,12 @@ from unittest import mock
from django.core.exceptions import ValidationError
from django.db import IntegrityError, connection, models
from django.db.models.constraints import BaseConstraint
+from django.db.transaction import atomic
from django.test import SimpleTestCase, TestCase, skipUnlessDBFeature
from .models import (
ChildModel, Product, UniqueConstraintConditionProduct,
- UniqueConstraintProduct,
+ UniqueConstraintDeferrable, UniqueConstraintProduct,
)
@@ -166,6 +167,20 @@ class UniqueConstraintTests(TestCase):
),
)
+ def test_eq_with_deferrable(self):
+ constraint_1 = models.UniqueConstraint(
+ fields=['foo', 'bar'],
+ name='unique',
+ deferrable=models.Deferrable.DEFERRED,
+ )
+ constraint_2 = models.UniqueConstraint(
+ fields=['foo', 'bar'],
+ name='unique',
+ deferrable=models.Deferrable.IMMEDIATE,
+ )
+ self.assertEqual(constraint_1, constraint_1)
+ self.assertNotEqual(constraint_1, constraint_2)
+
def test_repr(self):
fields = ['foo', 'bar']
name = 'unique_fields'
@@ -187,6 +202,18 @@ class UniqueConstraintTests(TestCase):
"condition=(AND: ('foo', F(bar)))>",
)
+ def test_repr_with_deferrable(self):
+ constraint = models.UniqueConstraint(
+ fields=['foo', 'bar'],
+ name='unique_fields',
+ deferrable=models.Deferrable.IMMEDIATE,
+ )
+ self.assertEqual(
+ repr(constraint),
+ "<UniqueConstraint: fields=('foo', 'bar') name='unique_fields' "
+ "deferrable=Deferrable.IMMEDIATE>",
+ )
+
def test_deconstruction(self):
fields = ['foo', 'bar']
name = 'unique_fields'
@@ -206,6 +233,23 @@ class UniqueConstraintTests(TestCase):
self.assertEqual(args, ())
self.assertEqual(kwargs, {'fields': tuple(fields), 'name': name, 'condition': condition})
+ def test_deconstruction_with_deferrable(self):
+ fields = ['foo']
+ name = 'unique_fields'
+ constraint = models.UniqueConstraint(
+ fields=fields,
+ name=name,
+ deferrable=models.Deferrable.DEFERRED,
+ )
+ path, args, kwargs = constraint.deconstruct()
+ self.assertEqual(path, 'django.db.models.UniqueConstraint')
+ self.assertEqual(args, ())
+ self.assertEqual(kwargs, {
+ 'fields': tuple(fields),
+ 'name': name,
+ 'deferrable': models.Deferrable.DEFERRED,
+ })
+
def test_database_constraint(self):
with self.assertRaises(IntegrityError):
UniqueConstraintProduct.objects.create(name=self.p1.name, color=self.p1.color)
@@ -238,3 +282,54 @@ class UniqueConstraintTests(TestCase):
def test_condition_must_be_q(self):
with self.assertRaisesMessage(ValueError, 'UniqueConstraint.condition must be a Q instance.'):
models.UniqueConstraint(name='uniq', fields=['name'], condition='invalid')
+
+ @skipUnlessDBFeature('supports_deferrable_unique_constraints')
+ def test_initially_deferred_database_constraint(self):
+ obj_1 = UniqueConstraintDeferrable.objects.create(name='p1', shelf='front')
+ obj_2 = UniqueConstraintDeferrable.objects.create(name='p2', shelf='back')
+
+ def swap():
+ obj_1.name, obj_2.name = obj_2.name, obj_1.name
+ obj_1.save()
+ obj_2.save()
+
+ swap()
+ # Behavior can be changed with SET CONSTRAINTS.
+ with self.assertRaises(IntegrityError):
+ with atomic(), connection.cursor() as cursor:
+ constraint_name = connection.ops.quote_name('name_init_deferred_uniq')
+ cursor.execute('SET CONSTRAINTS %s IMMEDIATE' % constraint_name)
+ swap()
+
+ @skipUnlessDBFeature('supports_deferrable_unique_constraints')
+ def test_initially_immediate_database_constraint(self):
+ obj_1 = UniqueConstraintDeferrable.objects.create(name='p1', shelf='front')
+ obj_2 = UniqueConstraintDeferrable.objects.create(name='p2', shelf='back')
+ obj_1.shelf, obj_2.shelf = obj_2.shelf, obj_1.shelf
+ with self.assertRaises(IntegrityError), atomic():
+ obj_1.save()
+ # Behavior can be changed with SET CONSTRAINTS.
+ with connection.cursor() as cursor:
+ constraint_name = connection.ops.quote_name('sheld_init_immediate_uniq')
+ cursor.execute('SET CONSTRAINTS %s DEFERRED' % constraint_name)
+ obj_1.save()
+ obj_2.save()
+
+ def test_deferrable_with_condition(self):
+ message = 'UniqueConstraint with conditions cannot be deferred.'
+ with self.assertRaisesMessage(ValueError, message):
+ models.UniqueConstraint(
+ fields=['name'],
+ name='name_without_color_unique',
+ condition=models.Q(color__isnull=True),
+ deferrable=models.Deferrable.DEFERRED,
+ )
+
+ def test_invalid_defer_argument(self):
+ message = 'UniqueConstraint.deferrable must be a Deferrable instance.'
+ with self.assertRaisesMessage(ValueError, message):
+ models.UniqueConstraint(
+ fields=['name'],
+ name='name_invalid',
+ deferrable='invalid',
+ )
diff --git a/tests/invalid_models_tests/test_models.py b/tests/invalid_models_tests/test_models.py
index f3f0bc8bd5..6bfdf2e736 100644
--- a/tests/invalid_models_tests/test_models.py
+++ b/tests/invalid_models_tests/test_models.py
@@ -1414,3 +1414,47 @@ class ConstraintsTests(TestCase):
]
self.assertEqual(Model.check(databases=self.databases), [])
+
+ def test_deferrable_unique_constraint(self):
+ class Model(models.Model):
+ age = models.IntegerField()
+
+ class Meta:
+ constraints = [
+ models.UniqueConstraint(
+ fields=['age'],
+ name='unique_age_deferrable',
+ deferrable=models.Deferrable.DEFERRED,
+ ),
+ ]
+
+ errors = Model.check(databases=self.databases)
+ expected = [] if connection.features.supports_deferrable_unique_constraints else [
+ 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=Model,
+ id='models.W038',
+ ),
+ ]
+ self.assertEqual(errors, expected)
+
+ def test_deferrable_unique_constraint_required_db_features(self):
+ class Model(models.Model):
+ age = models.IntegerField()
+
+ class Meta:
+ required_db_features = {'supports_deferrable_unique_constraints'}
+ constraints = [
+ models.UniqueConstraint(
+ fields=['age'],
+ name='unique_age_deferrable',
+ deferrable=models.Deferrable.IMMEDIATE,
+ ),
+ ]
+
+ self.assertEqual(Model.check(databases=self.databases), [])
diff --git a/tests/migrations/test_operations.py b/tests/migrations/test_operations.py
index 401cae6eae..e985535679 100644
--- a/tests/migrations/test_operations.py
+++ b/tests/migrations/test_operations.py
@@ -393,6 +393,60 @@ class OperationTests(OperationTestBase):
self.assertEqual(definition[1], [])
self.assertEqual(definition[2]['options']['constraints'], [partial_unique_constraint])
+ def test_create_model_with_deferred_unique_constraint(self):
+ deferred_unique_constraint = models.UniqueConstraint(
+ fields=['pink'],
+ name='deferrable_pink_constraint',
+ deferrable=models.Deferrable.DEFERRED,
+ )
+ operation = migrations.CreateModel(
+ 'Pony',
+ [
+ ('id', models.AutoField(primary_key=True)),
+ ('pink', models.IntegerField(default=3)),
+ ],
+ options={'constraints': [deferred_unique_constraint]},
+ )
+ project_state = ProjectState()
+ new_state = project_state.clone()
+ operation.state_forwards('test_crmo', new_state)
+ self.assertEqual(len(new_state.models['test_crmo', 'pony'].options['constraints']), 1)
+ self.assertTableNotExists('test_crmo_pony')
+ # Create table.
+ with connection.schema_editor() as editor:
+ operation.database_forwards('test_crmo', editor, project_state, new_state)
+ self.assertTableExists('test_crmo_pony')
+ Pony = new_state.apps.get_model('test_crmo', 'Pony')
+ Pony.objects.create(pink=1)
+ if connection.features.supports_deferrable_unique_constraints:
+ # Unique constraint is deferred.
+ with transaction.atomic():
+ obj = Pony.objects.create(pink=1)
+ obj.pink = 2
+ obj.save()
+ # Constraint behavior can be changed with SET CONSTRAINTS.
+ with self.assertRaises(IntegrityError):
+ with transaction.atomic(), connection.cursor() as cursor:
+ quoted_name = connection.ops.quote_name(deferred_unique_constraint.name)
+ cursor.execute('SET CONSTRAINTS %s IMMEDIATE' % quoted_name)
+ obj = Pony.objects.create(pink=1)
+ obj.pink = 3
+ obj.save()
+ else:
+ Pony.objects.create(pink=1)
+ # Reversal.
+ with connection.schema_editor() as editor:
+ operation.database_backwards('test_crmo', editor, new_state, project_state)
+ self.assertTableNotExists('test_crmo_pony')
+ # Deconstruction.
+ definition = operation.deconstruct()
+ self.assertEqual(definition[0], 'CreateModel')
+ self.assertEqual(definition[1], [])
+ self.assertEqual(
+ definition[2]['options']['constraints'],
+ [deferred_unique_constraint],
+ )
+
def test_create_model_managers(self):
"""
The managers on a model are set.
@@ -2046,6 +2100,110 @@ class OperationTests(OperationTestBase):
'name': 'test_constraint_pony_pink_for_weight_gt_5_uniq',
})
+ def test_add_deferred_unique_constraint(self):
+ app_label = 'test_adddeferred_uc'
+ project_state = self.set_up_test_model(app_label)
+ deferred_unique_constraint = models.UniqueConstraint(
+ fields=['pink'],
+ name='deferred_pink_constraint_add',
+ deferrable=models.Deferrable.DEFERRED,
+ )
+ operation = migrations.AddConstraint('Pony', deferred_unique_constraint)
+ self.assertEqual(
+ operation.describe(),
+ 'Create constraint deferred_pink_constraint_add on model Pony',
+ )
+ # Add constraint.
+ new_state = project_state.clone()
+ operation.state_forwards(app_label, new_state)
+ self.assertEqual(len(new_state.models[app_label, 'pony'].options['constraints']), 1)
+ Pony = new_state.apps.get_model(app_label, 'Pony')
+ self.assertEqual(len(Pony._meta.constraints), 1)
+ with connection.schema_editor() as editor:
+ operation.database_forwards(app_label, editor, project_state, new_state)
+ Pony.objects.create(pink=1, weight=4.0)
+ if connection.features.supports_deferrable_unique_constraints:
+ # Unique constraint is deferred.
+ with transaction.atomic():
+ obj = Pony.objects.create(pink=1, weight=4.0)
+ obj.pink = 2
+ obj.save()
+ # Constraint behavior can be changed with SET CONSTRAINTS.
+ with self.assertRaises(IntegrityError):
+ with transaction.atomic(), connection.cursor() as cursor:
+ quoted_name = connection.ops.quote_name(deferred_unique_constraint.name)
+ cursor.execute('SET CONSTRAINTS %s IMMEDIATE' % quoted_name)
+ obj = Pony.objects.create(pink=1, weight=4.0)
+ obj.pink = 3
+ obj.save()
+ else:
+ Pony.objects.create(pink=1, weight=4.0)
+ # Reversal.
+ with connection.schema_editor() as editor:
+ operation.database_backwards(app_label, editor, new_state, project_state)
+ # Constraint doesn't work.
+ Pony.objects.create(pink=1, weight=4.0)
+ # Deconstruction.
+ definition = operation.deconstruct()
+ self.assertEqual(definition[0], 'AddConstraint')
+ self.assertEqual(definition[1], [])
+ self.assertEqual(
+ definition[2],
+ {'model_name': 'Pony', 'constraint': deferred_unique_constraint},
+ )
+
+ def test_remove_deferred_unique_constraint(self):
+ app_label = 'test_removedeferred_uc'
+ deferred_unique_constraint = models.UniqueConstraint(
+ fields=['pink'],
+ name='deferred_pink_constraint_rm',
+ deferrable=models.Deferrable.DEFERRED,
+ )
+ project_state = self.set_up_test_model(app_label, constraints=[deferred_unique_constraint])
+ operation = migrations.RemoveConstraint('Pony', deferred_unique_constraint.name)
+ self.assertEqual(
+ operation.describe(),
+ 'Remove constraint deferred_pink_constraint_rm from model Pony',
+ )
+ # Remove constraint.
+ new_state = project_state.clone()
+ operation.state_forwards(app_label, new_state)
+ self.assertEqual(len(new_state.models[app_label, 'pony'].options['constraints']), 0)
+ Pony = new_state.apps.get_model(app_label, 'Pony')
+ self.assertEqual(len(Pony._meta.constraints), 0)
+ with connection.schema_editor() as editor:
+ operation.database_forwards(app_label, editor, project_state, new_state)
+ # Constraint doesn't work.
+ Pony.objects.create(pink=1, weight=4.0)
+ Pony.objects.create(pink=1, weight=4.0).delete()
+ # Reversal.
+ with connection.schema_editor() as editor:
+ operation.database_backwards(app_label, editor, new_state, project_state)
+ if connection.features.supports_deferrable_unique_constraints:
+ # Unique constraint is deferred.
+ with transaction.atomic():
+ obj = Pony.objects.create(pink=1, weight=4.0)
+ obj.pink = 2
+ obj.save()
+ # Constraint behavior can be changed with SET CONSTRAINTS.
+ with self.assertRaises(IntegrityError):
+ with transaction.atomic(), connection.cursor() as cursor:
+ quoted_name = connection.ops.quote_name(deferred_unique_constraint.name)
+ cursor.execute('SET CONSTRAINTS %s IMMEDIATE' % quoted_name)
+ obj = Pony.objects.create(pink=1, weight=4.0)
+ obj.pink = 3
+ obj.save()
+ else:
+ Pony.objects.create(pink=1, weight=4.0)
+ # Deconstruction.
+ definition = operation.deconstruct()
+ self.assertEqual(definition[0], 'RemoveConstraint')
+ self.assertEqual(definition[1], [])
+ self.assertEqual(definition[2], {
+ 'model_name': 'Pony',
+ 'name': 'deferred_pink_constraint_rm',
+ })
+
def test_alter_model_options(self):
"""
Tests the AlterModelOptions operation.