mirror of https://github.com/django/django.git
Fixed #33881 -- Added support for database collations to ArrayField(Char/TextFields).
This commit is contained in:
parent
89e695a69b
commit
ab1955a05e
|
@ -25,6 +25,7 @@ class ArrayField(CheckFieldDefaultMixin, Field):
|
||||||
|
|
||||||
def __init__(self, base_field, size=None, **kwargs):
|
def __init__(self, base_field, size=None, **kwargs):
|
||||||
self.base_field = base_field
|
self.base_field = base_field
|
||||||
|
self.db_collation = getattr(self.base_field, "db_collation", None)
|
||||||
self.size = size
|
self.size = size
|
||||||
if self.size:
|
if self.size:
|
||||||
self.default_validators = [
|
self.default_validators = [
|
||||||
|
@ -97,6 +98,11 @@ class ArrayField(CheckFieldDefaultMixin, Field):
|
||||||
size = self.size or ""
|
size = self.size or ""
|
||||||
return "%s[%s]" % (self.base_field.cast_db_type(connection), size)
|
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):
|
def get_placeholder(self, value, compiler, connection):
|
||||||
return "%s::{}".format(self.db_type(connection))
|
return "%s::{}".format(self.db_type(connection))
|
||||||
|
|
||||||
|
|
|
@ -950,7 +950,7 @@ class BaseDatabaseSchemaEditor:
|
||||||
if old_collation != new_collation:
|
if old_collation != new_collation:
|
||||||
# Collation change handles also a type change.
|
# Collation change handles also a type change.
|
||||||
fragment = self._alter_column_collation_sql(
|
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)
|
actions.append(fragment)
|
||||||
# Type change?
|
# Type change?
|
||||||
|
@ -1079,6 +1079,7 @@ class BaseDatabaseSchemaEditor:
|
||||||
new_rel.field,
|
new_rel.field,
|
||||||
rel_type,
|
rel_type,
|
||||||
rel_collation,
|
rel_collation,
|
||||||
|
old_rel.field,
|
||||||
)
|
)
|
||||||
other_actions = []
|
other_actions = []
|
||||||
else:
|
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 (
|
return (
|
||||||
self.sql_alter_column_collate
|
self.sql_alter_column_collate
|
||||||
% {
|
% {
|
||||||
|
|
|
@ -242,9 +242,11 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
|
||||||
)
|
)
|
||||||
return cursor.fetchone()[0]
|
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:
|
if new_collation is None:
|
||||||
new_collation = self._get_default_collation(model._meta.db_table)
|
new_collation = self._get_default_collation(model._meta.db_table)
|
||||||
return super()._alter_column_collation_sql(
|
return super()._alter_column_collation_sql(
|
||||||
model, new_field, new_type, new_collation
|
model, new_field, new_type, new_collation, old_field
|
||||||
)
|
)
|
||||||
|
|
|
@ -112,9 +112,7 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _alter_column_type_sql(self, model, old_field, new_field, new_type):
|
def _using_sql(self, new_field, old_field):
|
||||||
self.sql_alter_column_type = "ALTER COLUMN %(column)s TYPE %(type)s"
|
|
||||||
# Cast when data type changed.
|
|
||||||
using_sql = " USING %(column)s::%(type)s"
|
using_sql = " USING %(column)s::%(type)s"
|
||||||
new_internal_type = new_field.get_internal_type()
|
new_internal_type = new_field.get_internal_type()
|
||||||
old_internal_type = old_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(
|
if list(self._field_base_data_types(old_field)) != list(
|
||||||
self._field_base_data_types(new_field)
|
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):
|
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
|
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.
|
# Make ALTER TYPE with IDENTITY make sense.
|
||||||
table = strip_quotes(model._meta.db_table)
|
table = strip_quotes(model._meta.db_table)
|
||||||
auto_field_types = {
|
auto_field_types = {
|
||||||
|
@ -186,6 +193,25 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
|
||||||
else:
|
else:
|
||||||
return super()._alter_column_type_sql(model, old_field, new_field, new_type)
|
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(
|
def _alter_field(
|
||||||
self,
|
self,
|
||||||
model,
|
model,
|
||||||
|
|
|
@ -1236,6 +1236,55 @@ class SchemaTests(TransactionTestCase):
|
||||||
with self.assertRaisesMessage(DataError, msg):
|
with self.assertRaisesMessage(DataError, msg):
|
||||||
editor.alter_field(ArrayModel, old_field, new_field, strict=True)
|
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):
|
def test_alter_textfield_to_null(self):
|
||||||
"""
|
"""
|
||||||
#24307 - Should skip an alter statement on databases with
|
#24307 - Should skip an alter statement on databases with
|
||||||
|
|
Loading…
Reference in New Issue