Browse Source

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).
Tom Carrick 1 năm trước cách đây
mục cha
commit
34b411762b

+ 1 - 0
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",
     }

+ 17 - 3
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

+ 4 - 0
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`).

+ 101 - 0
tests/schema/tests.py

@@ -223,6 +223,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,
@@ -1402,6 +1414,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(
@@ -1438,6 +1484,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