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**: ```` does not support unique constraints with conditions. * **models.W037**: ```` does not support indexes with conditions. +* **models.W038**: ```` 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 + `_. 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 ` 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), + "", + ) + 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.