From 109f39a38bf62e350171d8e342788160af95d21a Mon Sep 17 00:00:00 2001 From: Tom Carrick Date: Fri, 27 Oct 2023 15:46:05 +0200 Subject: [PATCH] [4.2.x] Fixed #34932 -- Restored varchar_pattern_ops/text_pattern_ops index creation when deterministic collaction is set. Regression in f3f9d03edf17ccfa17263c7efa0b1350d1ac9278 (4.2) and 8ed25d65ea7546fafd808086fa07e7e5bb5428fc (5.0). Backport of 34b411762b50883d768d7b67e0a158ec39da8b09 from main. --- django/db/backends/postgresql/features.py | 1 + django/db/backends/postgresql/schema.py | 16 +++- docs/releases/4.2.7.txt | 4 + tests/schema/tests.py | 101 ++++++++++++++++++++++ 4 files changed, 121 insertions(+), 1 deletion(-) diff --git a/django/db/backends/postgresql/features.py b/django/db/backends/postgresql/features.py index 732b30b0a4..fbbc2d1fd1 100644 --- a/django/db/backends/postgresql/features.py +++ b/django/db/backends/postgresql/features.py @@ -72,6 +72,7 @@ class DatabaseFeatures(BaseDatabaseFeatures): supports_covering_indexes = True can_rename_index = True test_collations = { + "deterministic": "C", "non_default": "sv-x-icu", "swedish_ci": "sv-x-icu", } diff --git a/django/db/backends/postgresql/schema.py b/django/db/backends/postgresql/schema.py index 19f8b225e2..aa9780e433 100644 --- a/django/db/backends/postgresql/schema.py +++ b/django/db/backends/postgresql/schema.py @@ -100,7 +100,8 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor): return None # Non-deterministic collations on Postgresql don't support indexes # for operator classes varchar_pattern_ops/text_pattern_ops. - if getattr(field, "db_collation", None): + collation_name = getattr(field, "db_collation", None) + if collation_name and not self._is_collation_deterministic(collation_name): return None if db_type.startswith("varchar"): return self._create_index_sql( @@ -372,3 +373,16 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor): include=include, expressions=expressions, ) + + def _is_collation_deterministic(self, collation_name): + with self.connection.cursor() as cursor: + cursor.execute( + """ + SELECT collisdeterministic + FROM pg_collation + WHERE collname = %s + """, + [collation_name], + ) + row = cursor.fetchone() + return row[0] if row else None diff --git a/docs/releases/4.2.7.txt b/docs/releases/4.2.7.txt index 0e8e41058e..0b1228c9f6 100644 --- a/docs/releases/4.2.7.txt +++ b/docs/releases/4.2.7.txt @@ -13,3 +13,7 @@ Bugfixes * Fixed a regression in Django 4.2 that caused a crash of ``QuerySet.aggregate()`` with aggregates referencing expressions containing subqueries (:ticket:`34798`). + +* Restored, following a regression in Django 4.2, creating + ``varchar/text_pattern_ops`` indexes on ``CharField`` and ``TextField`` with + deterministic collations on PostgreSQL (:ticket:`34932`). diff --git a/tests/schema/tests.py b/tests/schema/tests.py index bd4ca38dff..9bb71de0be 100644 --- a/tests/schema/tests.py +++ b/tests/schema/tests.py @@ -227,6 +227,18 @@ class SchemaTests(TransactionTestCase): constraints_for_column.append(name) return sorted(constraints_for_column) + def get_constraint_opclasses(self, constraint_name): + with connection.cursor() as cursor: + sql = """ + SELECT opcname + FROM pg_opclass AS oc + JOIN pg_index as i on oc.oid = ANY(i.indclass) + JOIN pg_class as c on c.oid = i.indexrelid + WHERE c.relname = %s + """ + cursor.execute(sql, [constraint_name]) + return [row[0] for row in cursor.fetchall()] + def check_added_field_default( self, schema_editor, @@ -1378,6 +1390,95 @@ class SchemaTests(TransactionTestCase): ) self.assertIn("field", self.get_uniques(CiCharModel._meta.db_table)) + @isolate_apps("schema") + @unittest.skipUnless(connection.vendor == "postgresql", "PostgreSQL specific") + @skipUnlessDBFeature("supports_collation_on_charfield") + def test_unique_with_deterministic_collation_charfield(self): + deterministic_collation = connection.features.test_collations.get( + "deterministic" + ) + if not deterministic_collation: + self.skipTest("This backend does not support deterministic collations.") + + class CharModel(Model): + field = CharField(db_collation=deterministic_collation, unique=True) + + class Meta: + app_label = "schema" + + # Create the table. + with connection.schema_editor() as editor: + editor.create_model(CharModel) + self.isolated_local_models = [CharModel] + constraints = self.get_constraints_for_column( + CharModel, CharModel._meta.get_field("field").column + ) + self.assertIn("schema_charmodel_field_8b338dea_like", constraints) + self.assertIn( + "varchar_pattern_ops", + self.get_constraint_opclasses("schema_charmodel_field_8b338dea_like"), + ) + self.assertEqual( + self.get_column_collation(CharModel._meta.db_table, "field"), + deterministic_collation, + ) + self.assertIn("field", self.get_uniques(CharModel._meta.db_table)) + + @isolate_apps("schema") + @unittest.skipUnless(connection.vendor == "postgresql", "PostgreSQL specific") + @skipUnlessDBFeature("supports_collation_on_charfield") + def test_relation_to_deterministic_collation_charfield(self): + deterministic_collation = connection.features.test_collations.get( + "deterministic" + ) + if not deterministic_collation: + self.skipTest("This backend does not support deterministic collations.") + + class CharModel(Model): + field = CharField(db_collation=deterministic_collation, unique=True) + + class Meta: + app_label = "schema" + + class RelationModel(Model): + field = OneToOneField(CharModel, CASCADE, to_field="field") + + class Meta: + app_label = "schema" + + # Create the table. + with connection.schema_editor() as editor: + editor.create_model(CharModel) + editor.create_model(RelationModel) + self.isolated_local_models = [CharModel, RelationModel] + constraints = self.get_constraints_for_column( + CharModel, CharModel._meta.get_field("field").column + ) + self.assertIn("schema_charmodel_field_8b338dea_like", constraints) + self.assertIn( + "varchar_pattern_ops", + self.get_constraint_opclasses("schema_charmodel_field_8b338dea_like"), + ) + rel_constraints = self.get_constraints_for_column( + RelationModel, RelationModel._meta.get_field("field").column + ) + self.assertIn("schema_relationmodel_field_id_395fbb08_like", rel_constraints) + self.assertIn( + "varchar_pattern_ops", + self.get_constraint_opclasses( + "schema_relationmodel_field_id_395fbb08_like" + ), + ) + self.assertEqual( + self.get_column_collation(RelationModel._meta.db_table, "field_id"), + deterministic_collation, + ) + self.assertEqual( + self.get_column_collation(CharModel._meta.db_table, "field"), + deterministic_collation, + ) + self.assertIn("field_id", self.get_uniques(RelationModel._meta.db_table)) + def test_alter_textfield_to_null(self): """ #24307 - Should skip an alter statement on databases with