From ef4beafa2ce58a81a56ffd48a20a6153126b611a Mon Sep 17 00:00:00 2001 From: Hasan Ramezani Date: Sat, 28 Sep 2019 02:46:18 +0200 Subject: [PATCH] Refs #28816 -- Prevented silencing data loss when decreasing CharField.max_length for ArrayField.base_field on PostgreSQL. --- django/db/backends/postgresql/schema.py | 18 ++++++++-- tests/schema/tests.py | 48 +++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/django/db/backends/postgresql/schema.py b/django/db/backends/postgresql/schema.py index 6a65d12415..cf90bb8f81 100644 --- a/django/db/backends/postgresql/schema.py +++ b/django/db/backends/postgresql/schema.py @@ -47,6 +47,13 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor): field.db_type(self.connection), ) + def _field_base_data_types(self, field): + # Yield base data types for array fields. + if field.base_field.get_internal_type() == 'ArrayField': + yield from self._field_base_data_types(field.base_field) + else: + yield self._field_data_type(field.base_field) + def _create_like_index_sql(self, model, field): """ Return the statement to create an index with varchar operator pattern @@ -72,8 +79,15 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor): 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 self._field_data_type(old_field) != self._field_data_type(new_field): - self.sql_alter_column_type += ' USING %(column)s::%(type)s' + using_sql = ' USING %(column)s::%(type)s' + new_internal_type = new_field.get_internal_type() + old_internal_type = old_field.get_internal_type() + if new_internal_type == 'ArrayField' and new_internal_type == old_internal_type: + # Compare base data types for array fields. + if list(self._field_base_data_types(old_field)) != list(self._field_base_data_types(new_field)): + self.sql_alter_column_type += using_sql + elif self._field_data_type(old_field) != self._field_data_type(new_field): + self.sql_alter_column_type += using_sql # Make ALTER TYPE with SERIAL make sense. table = strip_quotes(model._meta.db_table) serial_fields_map = {'bigserial': 'bigint', 'serial': 'integer', 'smallserial': 'smallint'} diff --git a/tests/schema/tests.py b/tests/schema/tests.py index 08a77000b2..9a2195d1d5 100644 --- a/tests/schema/tests.py +++ b/tests/schema/tests.py @@ -895,6 +895,54 @@ class SchemaTests(TransactionTestCase): with connection.schema_editor() as editor: editor.alter_field(Foo, old_field, new_field, strict=True) + @isolate_apps('schema') + @unittest.skipUnless(connection.vendor == 'postgresql', 'PostgreSQL specific') + def test_alter_array_field_decrease_base_field_length(self): + from django.contrib.postgres.fields import ArrayField + + class ArrayModel(Model): + field = ArrayField(CharField(max_length=16)) + + class Meta: + app_label = 'schema' + + with connection.schema_editor() as editor: + editor.create_model(ArrayModel) + self.isolated_local_models = [ArrayModel] + ArrayModel.objects.create(field=['x' * 16]) + old_field = ArrayModel._meta.get_field('field') + new_field = ArrayField(CharField(max_length=15)) + new_field.set_attributes_from_name('field') + new_field.model = ArrayModel + with connection.schema_editor() as editor: + msg = 'value too long for type character varying(15)' + 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') + def test_alter_array_field_decrease_nested_base_field_length(self): + from django.contrib.postgres.fields import ArrayField + + class ArrayModel(Model): + field = ArrayField(ArrayField(CharField(max_length=16))) + + class Meta: + app_label = 'schema' + + with connection.schema_editor() as editor: + editor.create_model(ArrayModel) + self.isolated_local_models = [ArrayModel] + ArrayModel.objects.create(field=[['x' * 16]]) + old_field = ArrayModel._meta.get_field('field') + new_field = ArrayField(ArrayField(CharField(max_length=15))) + new_field.set_attributes_from_name('field') + new_field.model = ArrayModel + with connection.schema_editor() as editor: + msg = 'value too long for type character varying(15)' + with self.assertRaisesMessage(DataError, msg): + editor.alter_field(ArrayModel, old_field, new_field, strict=True) + def test_alter_textfield_to_null(self): """ #24307 - Should skip an alter statement on databases with