diff --git a/django/db/backends/base/schema.py b/django/db/backends/base/schema.py index 9d6952df21..a140a6dc61 100644 --- a/django/db/backends/base/schema.py +++ b/django/db/backends/base/schema.py @@ -1376,22 +1376,9 @@ class BaseDatabaseSchemaEditor: # - changing only a field name # - changing an attribute that doesn't affect the schema # - adding only a db_column and the column name is not changed - non_database_attrs = [ - "blank", - "db_column", - "editable", - "error_messages", - "help_text", - "limit_choices_to", - # Database-level options are not supported, see #21961. - "on_delete", - "related_name", - "related_query_name", - "validators", - "verbose_name", - ] - for attr in non_database_attrs: + for attr in old_field.non_db_attrs: old_kwargs.pop(attr, None) + for attr in new_field.non_db_attrs: new_kwargs.pop(attr, None) return self.quote_name(old_field.column) != self.quote_name( new_field.column diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py index 72208efd04..5c5a5e0cfe 100644 --- a/django/db/models/fields/__init__.py +++ b/django/db/models/fields/__init__.py @@ -140,6 +140,24 @@ class Field(RegisterLookupMixin): system_check_deprecated_details = None system_check_removed_details = None + # Attributes that don't affect a column definition. + # These attributes are ignored when altering the field. + non_db_attrs = ( + "blank", + "choices", + "db_column", + "editable", + "error_messages", + "help_text", + "limit_choices_to", + # Database-level options are not supported, see #21961. + "on_delete", + "related_name", + "related_query_name", + "validators", + "verbose_name", + ) + # Field flags hidden = False diff --git a/docs/howto/custom-model-fields.txt b/docs/howto/custom-model-fields.txt index 2dedf05a11..c4621c8500 100644 --- a/docs/howto/custom-model-fields.txt +++ b/docs/howto/custom-model-fields.txt @@ -314,6 +314,26 @@ reconstructing the field:: new_instance = MyField(*args, **kwargs) self.assertEqual(my_field_instance.some_attribute, new_instance.some_attribute) +.. _custom-field-non_db_attrs: + +Field attributes not affecting database column definition +--------------------------------------------------------- + +.. versionadded:: 4.1 + +You can override ``Field.non_db_attrs`` to customize attributes of a field that +don't affect a column definition. It's used during model migrations to detect +no-op ``AlterField`` operations. + +For example:: + + class CommaSepField(models.Field): + + @property + def non_db_attrs(self): + return super().non_db_attrs + ("separator",) + + Changing a custom field's base class ------------------------------------ diff --git a/docs/releases/4.1.txt b/docs/releases/4.1.txt index 4413fdfc9a..d83da638fc 100644 --- a/docs/releases/4.1.txt +++ b/docs/releases/4.1.txt @@ -288,6 +288,10 @@ Models on MariaDB and MySQL. For databases that do not support ``XOR``, the query will be converted to an equivalent using ``AND``, ``OR``, and ``NOT``. +* The new :ref:`Field.non_db_attrs ` attribute + allows customizing attributes of fields that don't affect a column + definition. + Requests and Responses ~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests/schema/tests.py b/tests/schema/tests.py index d9e59d32dc..fcc090aaf2 100644 --- a/tests/schema/tests.py +++ b/tests/schema/tests.py @@ -3961,6 +3961,20 @@ class SchemaTests(TransactionTestCase): with connection.schema_editor() as editor, self.assertNumQueries(0): editor.alter_field(Book, new_field, old_field, strict=True) + def test_alter_field_choices_noop(self): + with connection.schema_editor() as editor: + editor.create_model(Author) + old_field = Author._meta.get_field("name") + new_field = CharField( + choices=(("Jane", "Jane"), ("Joe", "Joe")), + max_length=255, + ) + new_field.set_attributes_from_name("name") + with connection.schema_editor() as editor, self.assertNumQueries(0): + editor.alter_field(Author, old_field, new_field, strict=True) + with connection.schema_editor() as editor, self.assertNumQueries(0): + editor.alter_field(Author, new_field, old_field, strict=True) + def test_add_textfield_unhashable_default(self): # Create the table with connection.schema_editor() as editor: