summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTom Carrick <tom@carrick.eu>2023-10-27 15:46:05 +0200
committerMariusz Felisiak <felisiak.mariusz@gmail.com>2023-10-30 07:37:40 +0100
commit34b411762b50883d768d7b67e0a158ec39da8b09 (patch)
treea8bdec327436eb7be76b1219e61fdcdfc1735b1a
parent46df3ab244e1688bd186f0bfbfea6a354097a910 (diff)
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).
-rw-r--r--django/db/backends/postgresql/features.py1
-rw-r--r--django/db/backends/postgresql/schema.py20
-rw-r--r--docs/releases/4.2.7.txt4
-rw-r--r--tests/schema/tests.py101
4 files changed, 123 insertions, 3 deletions
diff --git a/django/db/backends/postgresql/features.py b/django/db/backends/postgresql/features.py
index f2c3800307..0fe5d950a5 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 68b6442794..20a00e29f7 100644
--- a/tests/schema/tests.py
+++ b/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,
@@ -1404,6 +1416,40 @@ class SchemaTests(TransactionTestCase):
@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",
"supports_non_deterministic_collations",
@@ -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