summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorSimon Charette <charette.s@gmail.com>2023-07-07 19:43:51 -0400
committerMariusz Felisiak <felisiak.mariusz@gmail.com>2023-07-19 21:42:27 +0200
commit595a2abb58e04caa4d55fb2589bb80fb2a8fdfa1 (patch)
treef08cf5a71a9d637818d896a7b31b2d7860e336ca /tests
parent98cfb90182a8baa806fc4e09e294b6cfc5d09eff (diff)
Fixed #34701 -- Added support for NULLS [NOT] DISTINCT on PostgreSQL 15+.
Diffstat (limited to 'tests')
-rw-r--r--tests/constraints/tests.py58
-rw-r--r--tests/invalid_models_tests/test_models.py46
-rw-r--r--tests/schema/tests.py37
-rw-r--r--tests/validation/models.py14
-rw-r--r--tests/validation/test_constraints.py23
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)