Skip to content

Commit 109f39a

Browse files
knyghtyfelixxm
authored andcommitted
[4.2.x] Fixed #34932 -- Restored varchar_pattern_ops/text_pattern_ops index creation when deterministic collaction is set.
Regression in f3f9d03 (4.2) and 8ed25d6 (5.0). Backport of 34b4117 from main.
1 parent 6161299 commit 109f39a

File tree

4 files changed

+121
-1
lines changed

4 files changed

+121
-1
lines changed

django/db/backends/postgresql/features.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
7272
supports_covering_indexes = True
7373
can_rename_index = True
7474
test_collations = {
75+
"deterministic": "C",
7576
"non_default": "sv-x-icu",
7677
"swedish_ci": "sv-x-icu",
7778
}

django/db/backends/postgresql/schema.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,8 @@ def _create_like_index_sql(self, model, field):
100100
return None
101101
# Non-deterministic collations on Postgresql don't support indexes
102102
# for operator classes varchar_pattern_ops/text_pattern_ops.
103-
if getattr(field, "db_collation", None):
103+
collation_name = getattr(field, "db_collation", None)
104+
if collation_name and not self._is_collation_deterministic(collation_name):
104105
return None
105106
if db_type.startswith("varchar"):
106107
return self._create_index_sql(
@@ -372,3 +373,16 @@ def _create_index_sql(
372373
include=include,
373374
expressions=expressions,
374375
)
376+
377+
def _is_collation_deterministic(self, collation_name):
378+
with self.connection.cursor() as cursor:
379+
cursor.execute(
380+
"""
381+
SELECT collisdeterministic
382+
FROM pg_collation
383+
WHERE collname = %s
384+
""",
385+
[collation_name],
386+
)
387+
row = cursor.fetchone()
388+
return row[0] if row else None

docs/releases/4.2.7.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,7 @@ Bugfixes
1313
* Fixed a regression in Django 4.2 that caused a crash of
1414
``QuerySet.aggregate()`` with aggregates referencing expressions containing
1515
subqueries (:ticket:`34798`).
16+
17+
* Restored, following a regression in Django 4.2, creating
18+
``varchar/text_pattern_ops`` indexes on ``CharField`` and ``TextField`` with
19+
deterministic collations on PostgreSQL (:ticket:`34932`).

tests/schema/tests.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,18 @@ def get_constraints_for_column(self, model, column_name):
227227
constraints_for_column.append(name)
228228
return sorted(constraints_for_column)
229229

230+
def get_constraint_opclasses(self, constraint_name):
231+
with connection.cursor() as cursor:
232+
sql = """
233+
SELECT opcname
234+
FROM pg_opclass AS oc
235+
JOIN pg_index as i on oc.oid = ANY(i.indclass)
236+
JOIN pg_class as c on c.oid = i.indexrelid
237+
WHERE c.relname = %s
238+
"""
239+
cursor.execute(sql, [constraint_name])
240+
return [row[0] for row in cursor.fetchall()]
241+
230242
def check_added_field_default(
231243
self,
232244
schema_editor,
@@ -1378,6 +1390,95 @@ class Meta:
13781390
)
13791391
self.assertIn("field", self.get_uniques(CiCharModel._meta.db_table))
13801392

1393+
@isolate_apps("schema")
1394+
@unittest.skipUnless(connection.vendor == "postgresql", "PostgreSQL specific")
1395+
@skipUnlessDBFeature("supports_collation_on_charfield")
1396+
def test_unique_with_deterministic_collation_charfield(self):
1397+
deterministic_collation = connection.features.test_collations.get(
1398+
"deterministic"
1399+
)
1400+
if not deterministic_collation:
1401+
self.skipTest("This backend does not support deterministic collations.")
1402+
1403+
class CharModel(Model):
1404+
field = CharField(db_collation=deterministic_collation, unique=True)
1405+
1406+
class Meta:
1407+
app_label = "schema"
1408+
1409+
# Create the table.
1410+
with connection.schema_editor() as editor:
1411+
editor.create_model(CharModel)
1412+
self.isolated_local_models = [CharModel]
1413+
constraints = self.get_constraints_for_column(
1414+
CharModel, CharModel._meta.get_field("field").column
1415+
)
1416+
self.assertIn("schema_charmodel_field_8b338dea_like", constraints)
1417+
self.assertIn(
1418+
"varchar_pattern_ops",
1419+
self.get_constraint_opclasses("schema_charmodel_field_8b338dea_like"),
1420+
)
1421+
self.assertEqual(
1422+
self.get_column_collation(CharModel._meta.db_table, "field"),
1423+
deterministic_collation,
1424+
)
1425+
self.assertIn("field", self.get_uniques(CharModel._meta.db_table))
1426+
1427+
@isolate_apps("schema")
1428+
@unittest.skipUnless(connection.vendor == "postgresql", "PostgreSQL specific")
1429+
@skipUnlessDBFeature("supports_collation_on_charfield")
1430+
def test_relation_to_deterministic_collation_charfield(self):
1431+
deterministic_collation = connection.features.test_collations.get(
1432+
"deterministic"
1433+
)
1434+
if not deterministic_collation:
1435+
self.skipTest("This backend does not support deterministic collations.")
1436+
1437+
class CharModel(Model):
1438+
field = CharField(db_collation=deterministic_collation, unique=True)
1439+
1440+
class Meta:
1441+
app_label = "schema"
1442+
1443+
class RelationModel(Model):
1444+
field = OneToOneField(CharModel, CASCADE, to_field="field")
1445+
1446+
class Meta:
1447+
app_label = "schema"
1448+
1449+
# Create the table.
1450+
with connection.schema_editor() as editor:
1451+
editor.create_model(CharModel)
1452+
editor.create_model(RelationModel)
1453+
self.isolated_local_models = [CharModel, RelationModel]
1454+
constraints = self.get_constraints_for_column(
1455+
CharModel, CharModel._meta.get_field("field").column
1456+
)
1457+
self.assertIn("schema_charmodel_field_8b338dea_like", constraints)
1458+
self.assertIn(
1459+
"varchar_pattern_ops",
1460+
self.get_constraint_opclasses("schema_charmodel_field_8b338dea_like"),
1461+
)
1462+
rel_constraints = self.get_constraints_for_column(
1463+
RelationModel, RelationModel._meta.get_field("field").column
1464+
)
1465+
self.assertIn("schema_relationmodel_field_id_395fbb08_like", rel_constraints)
1466+
self.assertIn(
1467+
"varchar_pattern_ops",
1468+
self.get_constraint_opclasses(
1469+
"schema_relationmodel_field_id_395fbb08_like"
1470+
),
1471+
)
1472+
self.assertEqual(
1473+
self.get_column_collation(RelationModel._meta.db_table, "field_id"),
1474+
deterministic_collation,
1475+
)
1476+
self.assertEqual(
1477+
self.get_column_collation(CharModel._meta.db_table, "field"),
1478+
deterministic_collation,
1479+
)
1480+
self.assertIn("field_id", self.get_uniques(RelationModel._meta.db_table))
1481+
13811482
def test_alter_textfield_to_null(self):
13821483
"""
13831484
#24307 - Should skip an alter statement on databases with

0 commit comments

Comments
 (0)