diff --git a/django/db/backends/postgresql/features.py b/django/db/backends/postgresql/features.py index 0a0ade4ced..1254586d03 100644 --- a/django/db/backends/postgresql/features.py +++ b/django/db/backends/postgresql/features.py @@ -74,6 +74,7 @@ class DatabaseFeatures(BaseDatabaseFeatures): supports_virtual_generated_columns = False 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 40fef6660e..5dc93a27d0 100644 --- a/django/db/backends/postgresql/schema.py +++ b/django/db/backends/postgresql/schema.py @@ -98,9 +98,10 @@ 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) or ( - field.is_relation and getattr(field.target_field, "db_collation", None) - ): + collation_name = getattr(field, "db_collation", None) + if not collation_name and field.is_relation: + collation_name = getattr(field.target_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 166fe7048a..87cabe4cad 100644 --- a/tests/schema/tests.py +++ b/tests/schema/tests.py @@ -229,6 +229,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, @@ -1408,6 +1420,40 @@ 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( @@ -1444,6 +1490,61 @@ class SchemaTests(TransactionTestCase): ) self.assertIn("field_id", self.get_uniques(RelationModel._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