From 650859b6596acf5ed6a39e9bef190774bb8e4b2a Mon Sep 17 00:00:00 2001 From: siddus Date: Wed, 27 May 2026 18:35:28 -0400 Subject: [PATCH] Fixed #33185 -- Fixed sqlmigrate crash for RenameModel with a self-referential foreign key. When collecting SQL (e.g. for sqlmigrate), a RenameModel operation's table rename is not executed, so the subsequent field alteration introspected the renamed table before it existed. On MySQL this raised "Table doesn't exist", and on PostgreSQL the missing introspection silently omitted the self-referential foreign key's drop and recreate. The schema editor now records table renames while collecting SQL and redirects constraint-name introspection to the still-existing old table name, which carries the same constraints. Applying migrations is unaffected. --- django/db/backends/base/schema.py | 16 +++++++++++++++- tests/migrations/test_operations.py | 24 ++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/django/db/backends/base/schema.py b/django/db/backends/base/schema.py index c465668120d5..b93221b1204c 100644 --- a/django/db/backends/base/schema.py +++ b/django/db/backends/base/schema.py @@ -153,6 +153,9 @@ def __init__(self, connection, collect_sql=False, atomic=True): self.collect_sql = collect_sql if self.collect_sql: self.collected_sql = [] + # Tables renamed while collecting SQL don't exist under their new + # name in the database, so introspection must target the old name. + self.collected_table_renames = {} self.atomic_migration = self.connection.features.can_rollback_ddl and atomic # State-managing methods @@ -697,6 +700,14 @@ def alter_db_table(self, model, old_db_table, new_db_table): "new_table": self.quote_name(new_db_table), } ) + if self.collect_sql: + # The rename isn't executed, so later introspection of the new + # table name must be redirected to the still-existing old one, + # following any earlier rename of the same table in this batch. + existing_table = self.collected_table_renames.pop( + old_db_table, old_db_table + ) + self.collected_table_renames[new_db_table] = existing_table # Rename all references to the old table name. for sql in self.deferred_sql: if isinstance(sql, Statement): @@ -2019,9 +2030,12 @@ def _constraint_names( ) for name in column_names ] + table_name = model._meta.db_table + if self.collect_sql: + table_name = self.collected_table_renames.get(table_name, table_name) with self.connection.cursor() as cursor: constraints = self.connection.introspection.get_constraints( - cursor, model._meta.db_table + cursor, table_name ) result = [] for name, infodict in constraints.items(): diff --git a/tests/migrations/test_operations.py b/tests/migrations/test_operations.py index 85295d8e5099..7b045d1ba5be 100644 --- a/tests/migrations/test_operations.py +++ b/tests/migrations/test_operations.py @@ -938,6 +938,30 @@ def test_rename_model_with_self_referential_fk(self): "test_rmwsrf_rider", ["friend_id"], ("test_rmwsrf_horserider", "id") ) + def test_rename_model_with_self_referential_fk_collect_sql(self): + """ + Collecting SQL (e.g. sqlmigrate) for a RenameModel operation on a model + with a self-referential foreign key doesn't introspect the renamed + table, which doesn't exist yet (#33185). + """ + project_state = self.set_up_test_model("test_rmwsrfcs", related_model=True) + operation = migrations.RenameModel("Rider", "HorseRider") + new_state = project_state.clone() + operation.state_forwards("test_rmwsrfcs", new_state) + with connection.schema_editor(collect_sql=True) as editor: + operation.database_forwards( + "test_rmwsrfcs", editor, project_state, new_state + ) + collected_sql = "\n".join(editor.collected_sql) + # The table is renamed without crashing on introspection of the + # not-yet-renamed table, and the self-referential FK is handled + # (rather than silently skipped) using the constraint introspected + # from the still-existing old table. + self.assertIn( + connection.ops.quote_name("test_rmwsrfcs_horserider"), collected_sql + ) + self.assertIn("friend_id", collected_sql) + def test_rename_model_with_superclass_fk(self): """ Tests the RenameModel operation on a model which has a superclass that