diff --git a/django/contrib/postgres/fields/citext.py b/django/contrib/postgres/fields/citext.py index 42660001ae6..46f6d3d1c26 100644 --- a/django/contrib/postgres/fields/citext.py +++ b/django/contrib/postgres/fields/citext.py @@ -4,6 +4,10 @@ __all__ = ['CICharField', 'CIEmailField', 'CIText', 'CITextField'] class CIText: + + def get_internal_type(self): + return 'CI' + super().get_internal_type() + def db_type(self, connection): return 'citext' diff --git a/django/db/backends/postgresql/operations.py b/django/db/backends/postgresql/operations.py index 268fa99b51f..c4a61f7070a 100644 --- a/django/db/backends/postgresql/operations.py +++ b/django/db/backends/postgresql/operations.py @@ -77,6 +77,8 @@ class DatabaseOperations(BaseDatabaseOperations): 'istartswith', 'endswith', 'iendswith', 'regex', 'iregex'): if internal_type in ('IPAddressField', 'GenericIPAddressField'): lookup = "HOST(%s)" + elif internal_type in ('CICharField', 'CIEmailField', 'CITextField'): + lookup = '%s::citext' else: lookup = "%s::text" diff --git a/django/db/backends/postgresql/schema.py b/django/db/backends/postgresql/schema.py index 20cea3f2495..18388cc5237 100644 --- a/django/db/backends/postgresql/schema.py +++ b/django/db/backends/postgresql/schema.py @@ -107,11 +107,12 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor): def _alter_field(self, model, old_field, new_field, old_type, new_type, old_db_params, new_db_params, strict=False): - # Drop indexes on varchar/text columns that are changing to a different - # type. + # Drop indexes on varchar/text/citext columns that are changing to a + # different type. if (old_field.db_index or old_field.unique) and ( (old_type.startswith('varchar') and not new_type.startswith('varchar')) or - (old_type.startswith('text') and not new_type.startswith('text')) + (old_type.startswith('text') and not new_type.startswith('text')) or + (old_type.startswith('citext') and not new_type.startswith('citext')) ): index_name = self._create_index_name(model._meta.db_table, [old_field.column], suffix='_like') self.execute(self._delete_constraint_sql(self.sql_delete_index, model, index_name)) diff --git a/docs/releases/1.11.8.txt b/docs/releases/1.11.8.txt index 6f744a68866..959731bbd5c 100644 --- a/docs/releases/1.11.8.txt +++ b/docs/releases/1.11.8.txt @@ -24,3 +24,6 @@ Bugfixes * Fixed crash on SQLite and MySQL when ordering by a filtered subquery that uses ``nulls_first`` or ``nulls_last`` (:ticket:`28848`). + +* Made query lookups for ``CICharField``, ``CIEmailField``, and ``CITextField`` + use a ``citext`` cast (:ticket:`28702`). diff --git a/tests/backends/postgresql/tests.py b/tests/backends/postgresql/tests.py index caa8b87fb94..c4810c09606 100644 --- a/tests/backends/postgresql/tests.py +++ b/tests/backends/postgresql/tests.py @@ -138,6 +138,10 @@ class Tests(TestCase): for lookup in lookups: with self.subTest(lookup=lookup): self.assertIn('::text', do.lookup_cast(lookup)) + for lookup in lookups: + for field_type in ('CICharField', 'CIEmailField', 'CITextField'): + with self.subTest(lookup=lookup, field_type=field_type): + self.assertIn('::citext', do.lookup_cast(lookup, internal_type=field_type)) def test_correct_extraction_psycopg2_version(self): from django.db.backends.postgresql.base import psycopg2_version diff --git a/tests/postgres_tests/test_citext.py b/tests/postgres_tests/test_citext.py index 0a7012b0720..350f34ba33c 100644 --- a/tests/postgres_tests/test_citext.py +++ b/tests/postgres_tests/test_citext.py @@ -12,6 +12,7 @@ from .models import CITestModel @modify_settings(INSTALLED_APPS={'append': 'django.contrib.postgres'}) class CITextTestCase(PostgreSQLTestCase): + case_sensitive_lookups = ('contains', 'startswith', 'endswith', 'regex') @classmethod def setUpTestData(cls): @@ -42,3 +43,21 @@ class CITextTestCase(PostgreSQLTestCase): instance = CITestModel.objects.get() self.assertEqual(instance.array_field, self.john.array_field) self.assertTrue(CITestModel.objects.filter(array_field__contains=['joe']).exists()) + + def test_lookups_name_char(self): + for lookup in self.case_sensitive_lookups: + with self.subTest(lookup=lookup): + query = {'name__{}'.format(lookup): 'john'} + self.assertSequenceEqual(CITestModel.objects.filter(**query), [self.john]) + + def test_lookups_description_text(self): + for lookup, string in zip(self.case_sensitive_lookups, ('average', 'average joe', 'john', 'Joe.named')): + with self.subTest(lookup=lookup, string=string): + query = {'description__{}'.format(lookup): string} + self.assertSequenceEqual(CITestModel.objects.filter(**query), [self.john]) + + def test_lookups_email(self): + for lookup, string in zip(self.case_sensitive_lookups, ('john', 'john', 'john.com', 'john.com')): + with self.subTest(lookup=lookup, string=string): + query = {'email__{}'.format(lookup): string} + self.assertSequenceEqual(CITestModel.objects.filter(**query), [self.john])