From 9cb2cba6fc7f71ed768aed8cbc32e7447ecb0be8 Mon Sep 17 00:00:00 2001 From: Clifford Gama Date: Mon, 2 Mar 2026 09:45:40 +0200 Subject: [PATCH 1/3] Fixed typo in django/db/backends/mysql/schema.py. --- django/db/backends/mysql/schema.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django/db/backends/mysql/schema.py b/django/db/backends/mysql/schema.py index 9eba216256ed..5f12acbf692e 100644 --- a/django/db/backends/mysql/schema.py +++ b/django/db/backends/mysql/schema.py @@ -226,7 +226,7 @@ def _rename_field_sql(self, table, old_field, new_field, new_type): return super()._rename_field_sql(table, old_field, new_field, new_type) def _alter_column_comment_sql(self, model, new_field, new_type, new_db_comment): - # Comment is alter when altering the column type. + # Comment is altered when altering the column type. return "", [] def _comment_sql(self, comment): From 64edef37e7b419dd584307d84650a192fb47dc4c Mon Sep 17 00:00:00 2001 From: Clifford Gama Date: Sun, 1 Mar 2026 21:04:31 +0200 Subject: [PATCH 2/3] Fixed #36947 -- Avoided altering column type when only db_comment changes. Backends that allow altering column comments without altering the column type should set supports_independent_comment_alteration=True. Thanks Alex Fischer for the report. Co-authored-by: Adam Johnson --- django/db/backends/base/features.py | 2 + django/db/backends/base/schema.py | 10 +++++ django/db/backends/oracle/features.py | 1 + django/db/backends/postgresql/features.py | 1 + tests/schema/tests.py | 46 ++++++++++++++++++++++- 5 files changed, 58 insertions(+), 2 deletions(-) diff --git a/django/db/backends/base/features.py b/django/db/backends/base/features.py index 9f00dcfc3537..044a095ed97e 100644 --- a/django/db/backends/base/features.py +++ b/django/db/backends/base/features.py @@ -377,6 +377,8 @@ class BaseDatabaseFeatures: supports_comments = False # Does the backend support column comments in ADD COLUMN statements? supports_comments_inline = False + # Does the backend support changing column comments without altering type? + supports_independent_comment_alteration = False # Does the backend support stored generated columns? supports_stored_generated_columns = False diff --git a/django/db/backends/base/schema.py b/django/db/backends/base/schema.py index 9857eea57107..bd0dd7890acb 100644 --- a/django/db/backends/base/schema.py +++ b/django/db/backends/base/schema.py @@ -1151,6 +1151,7 @@ def _alter_field( or ( self.connection.features.supports_comments and old_field.db_comment != new_field.db_comment + and not self.connection.features.supports_independent_comment_alteration ) ): fragment, other_actions = self._alter_column_type_sql( @@ -1158,6 +1159,15 @@ def _alter_field( ) actions.append(fragment) post_actions.extend(other_actions) + elif ( + self.connection.features.supports_comments + and old_field.db_comment != new_field.db_comment + and self.connection.features.supports_independent_comment_alteration + ): + sql, params = self._alter_column_comment_sql( + model, new_field, new_type, new_field.db_comment + ) + post_actions.append((sql, params)) if new_field.has_db_default(): if ( diff --git a/django/db/backends/oracle/features.py b/django/db/backends/oracle/features.py index 3ca7894b2f63..99322c357277 100644 --- a/django/db/backends/oracle/features.py +++ b/django/db/backends/oracle/features.py @@ -31,6 +31,7 @@ class DatabaseFeatures(BaseDatabaseFeatures): supports_deferrable_unique_constraints = True truncates_names = True supports_comments = True + supports_independent_comment_alteration = True supports_tablespaces = True supports_sequence_reset = False can_introspect_materialized_views = True diff --git a/django/db/backends/postgresql/features.py b/django/db/backends/postgresql/features.py index fa23aea34579..fb12c798dda3 100644 --- a/django/db/backends/postgresql/features.py +++ b/django/db/backends/postgresql/features.py @@ -25,6 +25,7 @@ class DatabaseFeatures(BaseDatabaseFeatures): has_select_for_no_key_update = True can_release_savepoints = True supports_comments = True + supports_independent_comment_alteration = True supports_tablespaces = True supports_transactions = True can_introspect_materialized_views = True diff --git a/tests/schema/tests.py b/tests/schema/tests.py index 62c85634a785..1844d332529c 100644 --- a/tests/schema/tests.py +++ b/tests/schema/tests.py @@ -1111,6 +1111,41 @@ class Meta: "generated", self.get_indexes(GeneratedFieldIndexedModel._meta.db_table) ) + @isolate_apps("schema") + @skipUnlessDBFeature( + "supports_stored_generated_columns", + "supports_independent_comment_alteration", + ) + def test_alter_generated_field_base_field_comment(self): + class GenFieldModelComment(Model): + name = CharField(max_length=100) + name_lower = GeneratedField( + expression=Lower("name"), db_persist=True, output_field=CharField() + ) + + class Meta: + app_label = "schema" + + with connection.schema_editor() as editor: + editor.create_model(GenFieldModelComment) + + old_field = GenFieldModelComment._meta.get_field("name") + new_field = CharField(max_length=100, db_comment="Super useful comment") + new_field.set_attributes_from_name("name") + new_field.model = GenFieldModelComment + with ( + connection.schema_editor() as editor, + CaptureQueriesContext(connection) as ctx, + ): + editor.alter_field(GenFieldModelComment, old_field, new_field, strict=True) + + self.assertEqual(len(ctx), 1) + self.assertIn("COMMENT ON COLUMN", ctx.captured_queries[0]["sql"]) + self.assertEqual( + self.get_column_comment(GenFieldModelComment._meta.db_table, "name"), + "Super useful comment", + ) + @isolate_apps("schema") def test_add_auto_field(self): class AddAutoFieldModel(Model): @@ -5103,12 +5138,19 @@ def test_add_db_comment_and_default_charfield(self): def test_alter_db_comment(self): with connection.schema_editor() as editor: editor.create_model(Author) - # Add comment. old_field = Author._meta.get_field("name") new_field = CharField(max_length=255, db_comment="Custom comment") new_field.set_attributes_from_name("name") - with connection.schema_editor() as editor: + with ( + connection.schema_editor() as editor, + CaptureQueriesContext(connection) as ctx, + ): editor.alter_field(Author, old_field, new_field, strict=True) + self.assertEqual(len(ctx), 1) + if connection.features.supports_independent_comment_alteration: + self.assertIn("COMMENT ON COLUMN", ctx.captured_queries[0]["sql"]) + else: + self.assertIn("ALTER TABLE", ctx.captured_queries[0]["sql"]) self.assertEqual( self.get_column_comment(Author._meta.db_table, "name"), "Custom comment", From 8cbdd4a814397f81adf0129288f32b615bd1f94f Mon Sep 17 00:00:00 2001 From: Yassin Bahri <131185064+yassinbahri@users.noreply.github.com> Date: Wed, 19 Aug 2026 13:54:21 +0200 Subject: [PATCH 3/3] Fixed #37286 -- Preserved unique constraints when removing db_index. Thank you Clifford Gama for the review. --- AUTHORS | 1 + django/db/backends/base/schema.py | 5 ++++- tests/schema/tests.py | 36 +++++++++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 1 deletion(-) diff --git a/AUTHORS b/AUTHORS index e752d00671b3..438e77092894 100644 --- a/AUTHORS +++ b/AUTHORS @@ -1136,6 +1136,7 @@ answer newbie questions, and generally made Django that much better: Yann Fouillat Yann Malet Yash Jhunjhunwala + Yassin Bahri Yasushi Masuda ye7cakf02@sneakemail.com ymasuda@ethercube.com diff --git a/django/db/backends/base/schema.py b/django/db/backends/base/schema.py index bd0dd7890acb..353515315c70 100644 --- a/django/db/backends/base/schema.py +++ b/django/db/backends/base/schema.py @@ -1084,6 +1084,9 @@ def _alter_field( and (not new_field.db_index or new_field.unique) ): # Find the index for this field + meta_constraint_names = { + constraint.name for constraint in model._meta.constraints + } meta_index_names = {index.name for index in model._meta.indexes} # Retrieve only BTREE indexes since this is what's created with # db_index=True. @@ -1092,7 +1095,7 @@ def _alter_field( [old_field.column], index=True, type_=Index.suffix, - exclude=meta_index_names, + exclude=meta_constraint_names | meta_index_names, ) for index_name in index_names: # The only way to check if an index was created with diff --git a/tests/schema/tests.py b/tests/schema/tests.py index 1844d332529c..c5b94ccc281a 100644 --- a/tests/schema/tests.py +++ b/tests/schema/tests.py @@ -4148,6 +4148,42 @@ def test_remove_db_index_doesnt_remove_custom_indexes(self): finally: AuthorWithIndexedName._meta.indexes = [] + @skipUnlessDBFeature("allows_multiple_constraints_on_same_fields") + def test_remove_db_index_doesnt_remove_unique_constraints(self): + with connection.schema_editor() as editor: + editor.create_model(AuthorCharFieldWithIndex) + constraint = UniqueConstraint( + fields=["char_field"], name="author_char_field_uniq" + ) + try: + AuthorCharFieldWithIndex._meta.constraints = [constraint] + with connection.schema_editor() as editor: + editor.add_constraint(AuthorCharFieldWithIndex, constraint) + db_index_name = editor._create_index_name( + table_name=AuthorCharFieldWithIndex._meta.db_table, + column_names=("char_field",), + ) + old_constraints = self.get_constraints( + AuthorCharFieldWithIndex._meta.db_table + ) + self.assertIn(constraint.name, old_constraints) + self.assertIn(db_index_name, old_constraints) + + old_field = AuthorCharFieldWithIndex._meta.get_field("char_field") + new_field = CharField(max_length=31) + new_field.set_attributes_from_name("char_field") + with connection.schema_editor() as editor: + editor.alter_field( + AuthorCharFieldWithIndex, old_field, new_field, strict=True + ) + new_constraints = self.get_constraints( + AuthorCharFieldWithIndex._meta.db_table + ) + self.assertNotIn(db_index_name, new_constraints) + self.assertIn(constraint.name, new_constraints) + finally: + AuthorCharFieldWithIndex._meta.constraints = [] + def test_order_index(self): """ Indexes defined with ordering (ASC/DESC) defined on column