Fixed #33881 -- Added support for database collations to ArrayField(Char/TextFields).

This commit is contained in:
Mariusz Felisiak 2022-08-02 11:44:26 +02:00 committed by GitHub
parent 89e695a69b
commit ab1955a05e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 94 additions and 8 deletions

View File

@ -25,6 +25,7 @@ class ArrayField(CheckFieldDefaultMixin, Field):
def __init__(self, base_field, size=None, **kwargs):
self.base_field = base_field
self.db_collation = getattr(self.base_field, "db_collation", None)
self.size = size
if self.size:
self.default_validators = [
@ -97,6 +98,11 @@ class ArrayField(CheckFieldDefaultMixin, Field):
size = self.size or ""
return "%s[%s]" % (self.base_field.cast_db_type(connection), size)
def db_parameters(self, connection):
db_params = super().db_parameters(connection)
db_params["collation"] = self.db_collation
return db_params
def get_placeholder(self, value, compiler, connection):
return "%s::{}".format(self.db_type(connection))

View File

@ -950,7 +950,7 @@ class BaseDatabaseSchemaEditor:
if old_collation != new_collation:
# Collation change handles also a type change.
fragment = self._alter_column_collation_sql(
model, new_field, new_type, new_collation
model, new_field, new_type, new_collation, old_field
)
actions.append(fragment)
# Type change?
@ -1079,6 +1079,7 @@ class BaseDatabaseSchemaEditor:
new_rel.field,
rel_type,
rel_collation,
old_rel.field,
)
other_actions = []
else:
@ -1226,7 +1227,9 @@ class BaseDatabaseSchemaEditor:
[],
)
def _alter_column_collation_sql(self, model, new_field, new_type, new_collation):
def _alter_column_collation_sql(
self, model, new_field, new_type, new_collation, old_field
):
return (
self.sql_alter_column_collate
% {

View File

@ -242,9 +242,11 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
)
return cursor.fetchone()[0]
def _alter_column_collation_sql(self, model, new_field, new_type, new_collation):
def _alter_column_collation_sql(
self, model, new_field, new_type, new_collation, old_field
):
if new_collation is None:
new_collation = self._get_default_collation(model._meta.db_table)
return super()._alter_column_collation_sql(
model, new_field, new_type, new_collation
model, new_field, new_type, new_collation, old_field
)

View File

@ -112,9 +112,7 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
)
return None
def _alter_column_type_sql(self, model, old_field, new_field, new_type):
self.sql_alter_column_type = "ALTER COLUMN %(column)s TYPE %(type)s"
# Cast when data type changed.
def _using_sql(self, new_field, old_field):
using_sql = " USING %(column)s::%(type)s"
new_internal_type = new_field.get_internal_type()
old_internal_type = old_field.get_internal_type()
@ -123,9 +121,18 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
if list(self._field_base_data_types(old_field)) != list(
self._field_base_data_types(new_field)
):
self.sql_alter_column_type += using_sql
return using_sql
elif self._field_data_type(old_field) != self._field_data_type(new_field):
return using_sql
return ""
def _alter_column_type_sql(self, model, old_field, new_field, new_type):
self.sql_alter_column_type = "ALTER COLUMN %(column)s TYPE %(type)s"
# Cast when data type changed.
if using_sql := self._using_sql(new_field, old_field):
self.sql_alter_column_type += using_sql
new_internal_type = new_field.get_internal_type()
old_internal_type = old_field.get_internal_type()
# Make ALTER TYPE with IDENTITY make sense.
table = strip_quotes(model._meta.db_table)
auto_field_types = {
@ -186,6 +193,25 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
else:
return super()._alter_column_type_sql(model, old_field, new_field, new_type)
def _alter_column_collation_sql(
self, model, new_field, new_type, new_collation, old_field
):
sql = self.sql_alter_column_collate
# Cast when data type changed.
if using_sql := self._using_sql(new_field, old_field):
sql += using_sql
return (
sql
% {
"column": self.quote_name(new_field.column),
"type": new_type,
"collation": " " + self._collate_sql(new_collation)
if new_collation
else "",
},
[],
)
def _alter_field(
self,
model,

View File

@ -1236,6 +1236,55 @@ class SchemaTests(TransactionTestCase):
with self.assertRaisesMessage(DataError, msg):
editor.alter_field(ArrayModel, old_field, new_field, strict=True)
@isolate_apps("schema")
@unittest.skipUnless(connection.vendor == "postgresql", "PostgreSQL specific")
@skipUnlessDBFeature(
"supports_collation_on_charfield",
"supports_non_deterministic_collations",
)
def test_db_collation_arrayfield(self):
from django.contrib.postgres.fields import ArrayField
ci_collation = "case_insensitive"
cs_collation = "en-x-icu"
def drop_collation():
with connection.cursor() as cursor:
cursor.execute(f"DROP COLLATION IF EXISTS {ci_collation}")
with connection.cursor() as cursor:
cursor.execute(
f"CREATE COLLATION IF NOT EXISTS {ci_collation} (provider = icu, "
f"locale = 'und-u-ks-level2', deterministic = false)"
)
self.addCleanup(drop_collation)
class ArrayModel(Model):
field = ArrayField(CharField(max_length=16, db_collation=ci_collation))
class Meta:
app_label = "schema"
# Create the table.
with connection.schema_editor() as editor:
editor.create_model(ArrayModel)
self.isolated_local_models = [ArrayModel]
self.assertEqual(
self.get_column_collation(ArrayModel._meta.db_table, "field"),
ci_collation,
)
# Alter collation.
old_field = ArrayModel._meta.get_field("field")
new_field_cs = ArrayField(CharField(max_length=16, db_collation=cs_collation))
new_field_cs.set_attributes_from_name("field")
new_field_cs.model = ArrayField
with connection.schema_editor() as editor:
editor.alter_field(ArrayModel, old_field, new_field_cs, strict=True)
self.assertEqual(
self.get_column_collation(ArrayModel._meta.db_table, "field"),
cs_collation,
)
def test_alter_textfield_to_null(self):
"""
#24307 - Should skip an alter statement on databases with