summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMariusz Felisiak <felisiak.mariusz@gmail.com>2022-08-02 11:44:26 +0200
committerGitHub <noreply@github.com>2022-08-02 11:44:26 +0200
commitab1955a05e369337c3eaad1a2d34de346932179d (patch)
tree01854839ca59cb667cc8eb6ffada5c3ae284913f
parent89e695a69b16b8c0e720169b3ca4852cfd0c485f (diff)
Fixed #33881 -- Added support for database collations to ArrayField(Char/TextFields).
-rw-r--r--django/contrib/postgres/fields/array.py6
-rw-r--r--django/db/backends/base/schema.py7
-rw-r--r--django/db/backends/oracle/schema.py6
-rw-r--r--django/db/backends/postgresql/schema.py34
-rw-r--r--tests/schema/tests.py49
5 files changed, 94 insertions, 8 deletions
diff --git a/django/contrib/postgres/fields/array.py b/django/contrib/postgres/fields/array.py
index 7269198674..46c3e07b21 100644
--- a/django/contrib/postgres/fields/array.py
+++ b/django/contrib/postgres/fields/array.py
@@ -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))
diff --git a/django/db/backends/base/schema.py b/django/db/backends/base/schema.py
index 54f13be798..5e111aed73 100644
--- a/django/db/backends/base/schema.py
+++ b/django/db/backends/base/schema.py
@@ -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
% {
diff --git a/django/db/backends/oracle/schema.py b/django/db/backends/oracle/schema.py
index f2a5099110..1bf12293e8 100644
--- a/django/db/backends/oracle/schema.py
+++ b/django/db/backends/oracle/schema.py
@@ -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
)
diff --git a/django/db/backends/postgresql/schema.py b/django/db/backends/postgresql/schema.py
index a22ae0094f..3d4ddeeb6d 100644
--- a/django/db/backends/postgresql/schema.py
+++ b/django/db/backends/postgresql/schema.py
@@ -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,
diff --git a/tests/schema/tests.py b/tests/schema/tests.py
index 2868f59501..0702b54135 100644
--- a/tests/schema/tests.py
+++ b/tests/schema/tests.py
@@ -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