diff options
| author | Simon Charette <charette.s@gmail.com> | 2023-07-07 19:43:51 -0400 |
|---|---|---|
| committer | Mariusz Felisiak <felisiak.mariusz@gmail.com> | 2023-07-19 21:42:27 +0200 |
| commit | 595a2abb58e04caa4d55fb2589bb80fb2a8fdfa1 (patch) | |
| tree | f08cf5a71a9d637818d896a7b31b2d7860e336ca /tests | |
| parent | 98cfb90182a8baa806fc4e09e294b6cfc5d09eff (diff) | |
Fixed #34701 -- Added support for NULLS [NOT] DISTINCT on PostgreSQL 15+.
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/constraints/tests.py | 58 | ||||
| -rw-r--r-- | tests/invalid_models_tests/test_models.py | 46 | ||||
| -rw-r--r-- | tests/schema/tests.py | 37 | ||||
| -rw-r--r-- | tests/validation/models.py | 14 | ||||
| -rw-r--r-- | tests/validation/test_constraints.py | 23 |
5 files changed, 178 insertions, 0 deletions
diff --git a/tests/constraints/tests.py b/tests/constraints/tests.py index 1ed669a629..5fde168af4 100644 --- a/tests/constraints/tests.py +++ b/tests/constraints/tests.py @@ -503,6 +503,27 @@ class UniqueConstraintTests(TestCase): self.assertEqual(constraint, mock.ANY) self.assertNotEqual(constraint, another_constraint) + def test_eq_with_nulls_distinct(self): + constraint_1 = models.UniqueConstraint( + Lower("title"), + nulls_distinct=False, + name="book_func_nulls_distinct_uq", + ) + constraint_2 = models.UniqueConstraint( + Lower("title"), + nulls_distinct=True, + name="book_func_nulls_distinct_uq", + ) + constraint_3 = models.UniqueConstraint( + Lower("title"), + name="book_func_nulls_distinct_uq", + ) + self.assertEqual(constraint_1, constraint_1) + self.assertEqual(constraint_1, mock.ANY) + self.assertNotEqual(constraint_1, constraint_2) + self.assertNotEqual(constraint_1, constraint_3) + self.assertNotEqual(constraint_2, constraint_3) + def test_repr(self): fields = ["foo", "bar"] name = "unique_fields" @@ -560,6 +581,18 @@ class UniqueConstraintTests(TestCase): "opclasses=['text_pattern_ops', 'varchar_pattern_ops']>", ) + def test_repr_with_nulls_distinct(self): + constraint = models.UniqueConstraint( + fields=["foo", "bar"], + name="nulls_distinct_fields", + nulls_distinct=False, + ) + self.assertEqual( + repr(constraint), + "<UniqueConstraint: fields=('foo', 'bar') name='nulls_distinct_fields' " + "nulls_distinct=False>", + ) + def test_repr_with_expressions(self): constraint = models.UniqueConstraint( Lower("title"), @@ -679,6 +712,24 @@ class UniqueConstraintTests(TestCase): }, ) + def test_deconstruction_with_nulls_distinct(self): + fields = ["foo", "bar"] + name = "unique_fields" + constraint = models.UniqueConstraint( + fields=fields, name=name, nulls_distinct=True + ) + path, args, kwargs = constraint.deconstruct() + self.assertEqual(path, "django.db.models.UniqueConstraint") + self.assertEqual(args, ()) + self.assertEqual( + kwargs, + { + "fields": tuple(fields), + "name": name, + "nulls_distinct": True, + }, + ) + def test_deconstruction_with_expressions(self): name = "unique_fields" constraint = models.UniqueConstraint(Lower("title"), name=name) @@ -1029,6 +1080,13 @@ class UniqueConstraintTests(TestCase): opclasses="jsonb_path_ops", ) + def test_invalid_nulls_distinct_argument(self): + msg = "UniqueConstraint.nulls_distinct must be a bool." + with self.assertRaisesMessage(ValueError, msg): + models.UniqueConstraint( + name="uniq_opclasses", fields=["field"], nulls_distinct="NULLS DISTINCT" + ) + def test_opclasses_and_fields_same_length(self): msg = ( "UniqueConstraint.fields and UniqueConstraint.opclasses must have " diff --git a/tests/invalid_models_tests/test_models.py b/tests/invalid_models_tests/test_models.py index 9e2a37b79a..dc52f58c44 100644 --- a/tests/invalid_models_tests/test_models.py +++ b/tests/invalid_models_tests/test_models.py @@ -2753,6 +2753,52 @@ class ConstraintsTests(TestCase): self.assertEqual(Model.check(databases=self.databases), []) + def test_unique_constraint_nulls_distinct(self): + class Model(models.Model): + name = models.CharField(max_length=10) + + class Meta: + constraints = [ + models.UniqueConstraint( + fields=["name"], + name="name_uq_distinct_null", + nulls_distinct=True, + ), + ] + + warn = Warning( + f"{connection.display_name} does not support unique constraints with nulls " + "distinct.", + hint=( + "A constraint won't be created. Silence this warning if you don't care " + "about it." + ), + obj=Model, + id="models.W047", + ) + expected = ( + [] + if connection.features.supports_nulls_distinct_unique_constraints + else [warn] + ) + self.assertEqual(Model.check(databases=self.databases), expected) + + def test_unique_constraint_nulls_distinct_required_db_features(self): + class Model(models.Model): + name = models.CharField(max_length=10) + + class Meta: + constraints = [ + models.UniqueConstraint( + fields=["name"], + name="name_uq_distinct_null", + nulls_distinct=True, + ), + ] + required_db_features = {"supports_nulls_distinct_unique_constraints"} + + self.assertEqual(Model.check(databases=self.databases), []) + @skipUnlessDBFeature("supports_expression_indexes") def test_func_unique_constraint_expression_custom_lookup(self): class Model(models.Model): diff --git a/tests/schema/tests.py b/tests/schema/tests.py index 688a9f1fcf..5c20155387 100644 --- a/tests/schema/tests.py +++ b/tests/schema/tests.py @@ -3318,6 +3318,43 @@ class SchemaTests(TransactionTestCase): with self.assertRaises(DatabaseError): editor.add_constraint(Author, constraint) + @skipUnlessDBFeature("supports_nulls_distinct_unique_constraints") + def test_unique_constraint_nulls_distinct(self): + with connection.schema_editor() as editor: + editor.create_model(Author) + nulls_distinct = UniqueConstraint( + F("height"), name="distinct_height", nulls_distinct=True + ) + nulls_not_distinct = UniqueConstraint( + F("weight"), name="not_distinct_weight", nulls_distinct=False + ) + with connection.schema_editor() as editor: + editor.add_constraint(Author, nulls_distinct) + editor.add_constraint(Author, nulls_not_distinct) + Author.objects.create(name="", height=None, weight=None) + Author.objects.create(name="", height=None, weight=1) + with self.assertRaises(IntegrityError): + Author.objects.create(name="", height=1, weight=None) + with connection.schema_editor() as editor: + editor.remove_constraint(Author, nulls_distinct) + editor.remove_constraint(Author, nulls_not_distinct) + constraints = self.get_constraints(Author._meta.db_table) + self.assertNotIn(nulls_distinct.name, constraints) + self.assertNotIn(nulls_not_distinct.name, constraints) + + @skipIfDBFeature("supports_nulls_distinct_unique_constraints") + def test_unique_constraint_nulls_distinct_unsupported(self): + # UniqueConstraint is ignored on databases that don't support + # NULLS [NOT] DISTINCT. + with connection.schema_editor() as editor: + editor.create_model(Author) + constraint = UniqueConstraint( + F("name"), name="func_name_uq", nulls_distinct=True + ) + with connection.schema_editor() as editor, self.assertNumQueries(0): + self.assertIsNone(editor.add_constraint(Author, constraint)) + self.assertIsNone(editor.remove_constraint(Author, constraint)) + @ignore_warnings(category=RemovedInDjango51Warning) def test_index_together(self): """ diff --git a/tests/validation/models.py b/tests/validation/models.py index 8919a69310..612a8dd63a 100644 --- a/tests/validation/models.py +++ b/tests/validation/models.py @@ -217,3 +217,17 @@ class UniqueConstraintConditionProduct(models.Model): condition=models.Q(color__isnull=True), ), ] + + +class UniqueConstraintNullsDistinctProduct(models.Model): + name = models.CharField(max_length=255, blank=True, null=True) + + class Meta: + required_db_features = {"supports_nulls_distinct_unique_constraints"} + constraints = [ + models.UniqueConstraint( + fields=["name"], + name="name_nulls_not_distinct_uniq", + nulls_distinct=False, + ), + ] diff --git a/tests/validation/test_constraints.py b/tests/validation/test_constraints.py index 0b1ee6518e..eea2d0c533 100644 --- a/tests/validation/test_constraints.py +++ b/tests/validation/test_constraints.py @@ -6,6 +6,7 @@ from .models import ( ChildUniqueConstraintProduct, Product, UniqueConstraintConditionProduct, + UniqueConstraintNullsDistinctProduct, UniqueConstraintProduct, ) @@ -93,3 +94,25 @@ class PerformConstraintChecksTest(TestCase): UniqueConstraintConditionProduct.objects.create(name="product") product = UniqueConstraintConditionProduct(name="product") product.full_clean(validate_constraints=False) + + @skipUnlessDBFeature("supports_nulls_distinct_unique_constraints") + def test_full_clean_with_nulls_distinct_unique_constraints(self): + UniqueConstraintNullsDistinctProduct.objects.create(name=None) + product = UniqueConstraintNullsDistinctProduct(name=None) + with self.assertRaises(ValidationError) as cm: + product.full_clean() + self.assertEqual( + cm.exception.message_dict, + { + "name": [ + "Unique constraint nulls distinct product with this Name " + "already exists." + ] + }, + ) + + @skipUnlessDBFeature("supports_nulls_distinct_unique_constraints") + def test_full_clean_with_nulls_distinct_unique_constraints_disabled(self): + UniqueConstraintNullsDistinctProduct.objects.create(name=None) + product = UniqueConstraintNullsDistinctProduct(name=None) + product.full_clean(validate_constraints=False) |
