diff --git a/django/contrib/postgres/fields/array.py b/django/contrib/postgres/fields/array.py index 46c3e07b21..3edc72ac94 100644 --- a/django/contrib/postgres/fields/array.py +++ b/django/contrib/postgres/fields/array.py @@ -68,18 +68,35 @@ class ArrayField(CheckFieldDefaultMixin, Field): ) else: # Remove the field name checks as they are not needed here. - base_errors = self.base_field.check() - if base_errors: - messages = "\n ".join( - "%s (%s)" % (error.msg, error.id) for error in base_errors + base_checks = self.base_field.check() + if base_checks: + error_messages = "\n ".join( + "%s (%s)" % (base_check.msg, base_check.id) + for base_check in base_checks + if isinstance(base_check, checks.Error) ) - errors.append( - checks.Error( - "Base field for array has errors:\n %s" % messages, - obj=self, - id="postgres.E001", + if error_messages: + errors.append( + checks.Error( + "Base field for array has errors:\n %s" % error_messages, + obj=self, + id="postgres.E001", + ) ) + warning_messages = "\n ".join( + "%s (%s)" % (base_check.msg, base_check.id) + for base_check in base_checks + if isinstance(base_check, checks.Warning) ) + if warning_messages: + errors.append( + checks.Warning( + "Base field for array has warnings:\n %s" + % warning_messages, + obj=self, + id="postgres.W004", + ) + ) return errors def set_attributes_from_name(self, name): diff --git a/django/contrib/postgres/fields/citext.py b/django/contrib/postgres/fields/citext.py index 2b943614d2..2dac2577d1 100644 --- a/django/contrib/postgres/fields/citext.py +++ b/django/contrib/postgres/fields/citext.py @@ -1,9 +1,22 @@ +import warnings + from django.db.models import CharField, EmailField, TextField +from django.test.utils import ignore_warnings +from django.utils.deprecation import RemovedInDjango51Warning __all__ = ["CICharField", "CIEmailField", "CIText", "CITextField"] +# RemovedInDjango51Warning. class CIText: + def __init__(self, *args, **kwargs): + warnings.warn( + "django.contrib.postgres.fields.CIText mixin is deprecated.", + RemovedInDjango51Warning, + stacklevel=2, + ) + super().__init__(*args, **kwargs) + def get_internal_type(self): return "CI" + super().get_internal_type() @@ -12,12 +25,54 @@ class CIText: class CICharField(CIText, CharField): - pass + system_check_deprecated_details = { + "msg": ( + "django.contrib.postgres.fields.CICharField is deprecated. Support for it " + "(except in historical migrations) will be removed in Django 5.1." + ), + "hint": ( + 'Use CharField(db_collation="…") with a case-insensitive non-deterministic ' + "collation instead." + ), + "id": "fields.W905", + } + + def __init__(self, *args, **kwargs): + with ignore_warnings(category=RemovedInDjango51Warning): + super().__init__(*args, **kwargs) class CIEmailField(CIText, EmailField): - pass + system_check_deprecated_details = { + "msg": ( + "django.contrib.postgres.fields.CIEmailField is deprecated. Support for it " + "(except in historical migrations) will be removed in Django 5.1." + ), + "hint": ( + 'Use EmailField(db_collation="…") with a case-insensitive ' + "non-deterministic collation instead." + ), + "id": "fields.W906", + } + + def __init__(self, *args, **kwargs): + with ignore_warnings(category=RemovedInDjango51Warning): + super().__init__(*args, **kwargs) class CITextField(CIText, TextField): - pass + system_check_deprecated_details = { + "msg": ( + "django.contrib.postgres.fields.CITextField is deprecated. Support for it " + "(except in historical migrations) will be removed in Django 5.1." + ), + "hint": ( + 'Use TextField(db_collation="…") with a case-insensitive non-deterministic ' + "collation instead." + ), + "id": "fields.W907", + } + + def __init__(self, *args, **kwargs): + with ignore_warnings(category=RemovedInDjango51Warning): + super().__init__(*args, **kwargs) diff --git a/django/db/backends/postgresql/operations.py b/django/db/backends/postgresql/operations.py index e8eb06c9e2..2303703ebc 100644 --- a/django/db/backends/postgresql/operations.py +++ b/django/db/backends/postgresql/operations.py @@ -142,6 +142,7 @@ class DatabaseOperations(BaseDatabaseOperations): ): if internal_type in ("IPAddressField", "GenericIPAddressField"): lookup = "HOST(%s)" + # RemovedInDjango51Warning. elif internal_type in ("CICharField", "CIEmailField", "CITextField"): lookup = "%s::citext" else: diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index a12834c431..ff10c9de5a 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -25,6 +25,13 @@ details on these changes. ``django.contrib.auth.hashers.UnsaltedSHA1PasswordHasher``, and ``django.contrib.auth.hashers.UnsaltedMD5PasswordHasher`` will be removed. +* The model ``django.contrib.postgres.fields.CICharField``, + ``django.contrib.postgres.fields.CIEmailField``, and + ``django.contrib.postgres.fields.CITextField`` will be removed. Stub fields + will remain for compatibility with historical migrations. + +* The ``django.contrib.postgres.fields.CIText`` mixin will be removed. + .. _deprecation-removed-in-5.0: 5.0 diff --git a/docs/ref/checks.txt b/docs/ref/checks.txt index 92278db659..aa8d5b6d62 100644 --- a/docs/ref/checks.txt +++ b/docs/ref/checks.txt @@ -223,6 +223,15 @@ Model fields 4.0. *This check appeared in Django 3.1 and 3.2*. * **fields.E904**: ``django.contrib.postgres.fields.JSONField`` is removed except for support in historical migrations. +* **fields.W905**: ``django.contrib.postgres.fields.CICharField`` is + deprecated. Support for it (except in historical migrations) will be removed + in Django 5.1. +* **fields.W906**: ``django.contrib.postgres.fields.CIEmailField`` is + deprecated. Support for it (except in historical migrations) will be removed + in Django 5.1. +* **fields.W907**: ``django.contrib.postgres.fields.CITextField`` is + deprecated. Support for it (except in historical migrations) will be removed + in Django 5.1. File fields ~~~~~~~~~~~ @@ -851,6 +860,7 @@ fields: * **postgres.E003**: ```` default should be a callable instead of an instance so that it's not shared between all field instances. *This check was changed to* ``fields.E010`` *in Django 3.1*. +* **postgres.W004**: Base field for array has warnings: ... ``sites`` --------- diff --git a/docs/ref/contrib/postgres/fields.txt b/docs/ref/contrib/postgres/fields.txt index 25ca442b6f..820f6de956 100644 --- a/docs/ref/contrib/postgres/fields.txt +++ b/docs/ref/contrib/postgres/fields.txt @@ -259,6 +259,8 @@ transform do not change. For example:: .. class:: CIText(**options) + .. deprecated:: 4.2 + A mixin to create case-insensitive text fields backed by the citext_ type. Read about `the performance considerations`_ prior to using it. @@ -274,9 +276,29 @@ transform do not change. For example:: Several fields that use the mixin are provided: .. class:: CICharField(**options) + + .. deprecated:: 4.2 + + ``CICharField`` is deprecated in favor of + ``CharField(db_collation="…")`` with a case-insensitive + non-deterministic collation. + .. class:: CIEmailField(**options) + + .. deprecated:: 4.2 + + ``CIEmailField`` is deprecated in favor of + ``EmailField(db_collation="…")`` with a case-insensitive + non-deterministic collation. + .. class:: CITextField(**options) + .. deprecated:: 4.2 + + ``CITextField`` is deprecated in favor of + ``TextField(db_collation="…")`` with a case-insensitive + non-deterministic collation. + These fields subclass :class:`~django.db.models.CharField`, :class:`~django.db.models.EmailField`, and :class:`~django.db.models.TextField`, respectively. diff --git a/docs/releases/1.11.txt b/docs/releases/1.11.txt index 4716307526..5da81cd739 100644 --- a/docs/releases/1.11.txt +++ b/docs/releases/1.11.txt @@ -215,11 +215,11 @@ Minor features parameter to specify a custom class to encode data types not supported by the standard encoder. -* The new :class:`~django.contrib.postgres.fields.CIText` mixin and +* The new ``CIText`` mixin and :class:`~django.contrib.postgres.operations.CITextExtension` migration operation allow using PostgreSQL's ``citext`` extension for case-insensitive - lookups. Three fields are provided: :class:`.CICharField`, - :class:`.CIEmailField`, and :class:`.CITextField`. + lookups. Three fields are provided: ``CICharField``, ``CIEmailField``, and + ``CITextField``. * The new :class:`~django.contrib.postgres.aggregates.JSONBAgg` allows aggregating values as a JSON array. diff --git a/docs/releases/4.2.txt b/docs/releases/4.2.txt index fefba040e2..e3553e737b 100644 --- a/docs/releases/4.2.txt +++ b/docs/releases/4.2.txt @@ -351,3 +351,17 @@ Miscellaneous * ``django.contrib.auth.hashers.SHA1PasswordHasher``, ``django.contrib.auth.hashers.UnsaltedSHA1PasswordHasher``, and ``django.contrib.auth.hashers.UnsaltedMD5PasswordHasher`` are deprecated. + +* ``django.contrib.postgres.fields.CICharField`` is deprecated in favor of + ``CharField(db_collation="…")`` with a case-insensitive non-deterministic + collation. + +* ``django.contrib.postgres.fields.CIEmailField`` is deprecated in favor of + ``EmailField(db_collation="…")`` with a case-insensitive non-deterministic + collation. + +* ``django.contrib.postgres.fields.CITextField`` is deprecated in favor of + ``TextField(db_collation="…")`` with a case-insensitive non-deterministic + collation. + +* ``django.contrib.postgres.fields.CIText`` mixin is deprecated. diff --git a/tests/backends/postgresql/tests.py b/tests/backends/postgresql/tests.py index 63d7064aac..e28acef2c0 100644 --- a/tests/backends/postgresql/tests.py +++ b/tests/backends/postgresql/tests.py @@ -283,6 +283,7 @@ class Tests(TestCase): for lookup in lookups: with self.subTest(lookup=lookup): self.assertIn("::text", do.lookup_cast(lookup)) + # RemovedInDjango51Warning. for lookup in lookups: for field_type in ("CICharField", "CIEmailField", "CITextField"): with self.subTest(lookup=lookup, field_type=field_type): diff --git a/tests/invalid_models_tests/test_deprecated_fields.py b/tests/invalid_models_tests/test_deprecated_fields.py index 3f3953496c..7b1e3fb7ac 100644 --- a/tests/invalid_models_tests/test_deprecated_fields.py +++ b/tests/invalid_models_tests/test_deprecated_fields.py @@ -85,3 +85,65 @@ class DeprecatedFieldsTests(SimpleTestCase): ), ], ) + + @skipUnless(connection.vendor == "postgresql", "PostgreSQL specific SQL") + def test_postgres_ci_fields_deprecated(self): + from django.contrib.postgres.fields import ( + ArrayField, + CICharField, + CIEmailField, + CITextField, + ) + + class PostgresCIFieldsModel(models.Model): + ci_char = CICharField(max_length=255) + ci_email = CIEmailField() + ci_text = CITextField() + array_ci_text = ArrayField(CITextField()) + + self.assertEqual( + PostgresCIFieldsModel.check(), + [ + checks.Warning( + "django.contrib.postgres.fields.CICharField is deprecated. Support " + "for it (except in historical migrations) will be removed in " + "Django 5.1.", + hint=( + 'Use CharField(db_collation="…") with a case-insensitive ' + "non-deterministic collation instead." + ), + obj=PostgresCIFieldsModel._meta.get_field("ci_char"), + id="fields.W905", + ), + checks.Warning( + "django.contrib.postgres.fields.CIEmailField is deprecated. " + "Support for it (except in historical migrations) will be removed " + "in Django 5.1.", + hint=( + 'Use EmailField(db_collation="…") with a case-insensitive ' + "non-deterministic collation instead." + ), + obj=PostgresCIFieldsModel._meta.get_field("ci_email"), + id="fields.W906", + ), + checks.Warning( + "django.contrib.postgres.fields.CITextField is deprecated. Support " + "for it (except in historical migrations) will be removed in " + "Django 5.1.", + hint=( + 'Use TextField(db_collation="…") with a case-insensitive ' + "non-deterministic collation instead." + ), + obj=PostgresCIFieldsModel._meta.get_field("ci_text"), + id="fields.W907", + ), + checks.Warning( + "Base field for array has warnings:\n" + " django.contrib.postgres.fields.CITextField is deprecated. " + "Support for it (except in historical migrations) will be removed " + "in Django 5.1. (fields.W907)", + obj=PostgresCIFieldsModel._meta.get_field("array_ci_text"), + id="postgres.W004", + ), + ], + ) diff --git a/tests/postgres_tests/fields.py b/tests/postgres_tests/fields.py index 2d62e26a92..1565b5ed43 100644 --- a/tests/postgres_tests/fields.py +++ b/tests/postgres_tests/fields.py @@ -7,12 +7,12 @@ import enum from django.db import models try: + from django.contrib.postgres.fields import CICharField # RemovedInDjango51Warning. + from django.contrib.postgres.fields import CIEmailField # RemovedInDjango51Warning. + from django.contrib.postgres.fields import CITextField # RemovedInDjango51Warning. from django.contrib.postgres.fields import ( ArrayField, BigIntegerRangeField, - CICharField, - CIEmailField, - CITextField, DateRangeField, DateTimeRangeField, DecimalRangeField, @@ -47,9 +47,9 @@ except ImportError: ArrayField = DummyArrayField BigIntegerRangeField = models.Field - CICharField = models.Field - CIEmailField = models.Field - CITextField = models.Field + CICharField = models.Field # RemovedInDjango51Warning. + CIEmailField = models.Field # RemovedInDjango51Warning. + CITextField = models.Field # RemovedInDjango51Warning. DateRangeField = models.Field DateTimeRangeField = DummyContinuousRangeField DecimalRangeField = DummyContinuousRangeField diff --git a/tests/postgres_tests/migrations/0002_create_test_models.py b/tests/postgres_tests/migrations/0002_create_test_models.py index 1b05435494..a1d9a942d0 100644 --- a/tests/postgres_tests/migrations/0002_create_test_models.py +++ b/tests/postgres_tests/migrations/0002_create_test_models.py @@ -291,6 +291,7 @@ class Migration(migrations.Migration): options=None, bases=None, ), + # RemovedInDjango51Warning. migrations.CreateModel( name="CITestModel", fields=[ diff --git a/tests/postgres_tests/models.py b/tests/postgres_tests/models.py index 8f27838ad5..05f2732fb8 100644 --- a/tests/postgres_tests/models.py +++ b/tests/postgres_tests/models.py @@ -119,6 +119,7 @@ class Character(models.Model): name = models.CharField(max_length=255) +# RemovedInDjango51Warning. class CITestModel(PostgreSQLModel): name = CICharField(primary_key=True, max_length=255) email = CIEmailField() diff --git a/tests/postgres_tests/test_citext.py b/tests/postgres_tests/test_citext.py index f1c13184b6..314f2e40d3 100644 --- a/tests/postgres_tests/test_citext.py +++ b/tests/postgres_tests/test_citext.py @@ -1,3 +1,4 @@ +# RemovedInDjango51Warning. """ The citext PostgreSQL extension supports indexing of case-insensitive text strings and thus eliminates the need for operations such as iexact and other @@ -5,6 +6,7 @@ modifiers to enforce use of an index. """ from django.db import IntegrityError from django.test.utils import modify_settings +from django.utils.deprecation import RemovedInDjango51Warning from . import PostgreSQLTestCase from .models import CITestModel @@ -82,3 +84,10 @@ class CITextTestCase(PostgreSQLTestCase): self.assertSequenceEqual( CITestModel.objects.filter(**query), [self.john] ) + + def test_citext_deprecated(self): + from django.contrib.postgres.fields import CIText + + msg = "django.contrib.postgres.fields.CIText mixin is deprecated." + with self.assertRaisesMessage(RemovedInDjango51Warning, msg): + CIText() diff --git a/tests/runtests.py b/tests/runtests.py index 3bbaf5ada7..b678988391 100755 --- a/tests/runtests.py +++ b/tests/runtests.py @@ -244,6 +244,13 @@ def setup_collect_tests(start_at, start_after, test_labels=None): settings.LOGGING = log_config settings.SILENCED_SYSTEM_CHECKS = [ "fields.W342", # ForeignKey(unique=True) -> OneToOneField + # django.contrib.postgres.fields.CICharField deprecated. + "fields.W905", + "postgres.W004", + # django.contrib.postgres.fields.CIEmailField deprecated. + "fields.W906", + # django.contrib.postgres.fields.CITextField deprecated. + "fields.W907", ] # RemovedInDjango50Warning