diff options
| author | siddus <dcsid10@gmail.com> | 2026-05-27 18:35:28 -0400 |
|---|---|---|
| committer | Jacob Walls <jacobtylerwalls@gmail.com> | 2026-06-11 10:07:02 -0400 |
| commit | c6f81b383251dc9fd0918b4ba040c993444b931e (patch) | |
| tree | 95713e49084752818ed0f261064f121940b8ef9a | |
| parent | e8cff2921ba169d806a15de18304f431f12700f4 (diff) | |
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.
| -rw-r--r-- | django/db/backends/base/schema.py | 16 | ||||
| -rw-r--r-- | tests/migrations/test_operations.py | 38 |
2 files changed, 53 insertions, 1 deletions
diff --git a/django/db/backends/base/schema.py b/django/db/backends/base/schema.py index af30f7f7fc..a99218da92 100644 --- a/django/db/backends/base/schema.py +++ b/django/db/backends/base/schema.py @@ -156,6 +156,9 @@ class BaseDatabaseSchemaEditor: 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 @@ -700,6 +703,14 @@ class BaseDatabaseSchemaEditor: "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): @@ -2022,9 +2033,12 @@ class BaseDatabaseSchemaEditor: ) 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 49bda86d1f..7189cef400 100644 --- a/tests/migrations/test_operations.py +++ b/tests/migrations/test_operations.py @@ -938,6 +938,44 @@ class OperationTests(OperationTestBase): "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) + # Forwards: only the old table exists, so the renamed table can't be + # introspected. The rename is collected and the self-referential FK is + # handled (rather than silently skipped) using the constraint + # introspected from the still-existing old table. + 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) + self.assertIn( + connection.ops.quote_name("test_rmwsrfcs_horserider"), collected_sql + ) + self.assertIn(connection.ops.quote_name("friend_id"), collected_sql) + # Backwards: apply the rename for real so the renamed table exists, + # then collect the reverse SQL. The same redirection must happen, this + # time back to the "horserider" table. + with connection.schema_editor() as editor: + operation.database_forwards( + "test_rmwsrfcs", editor, project_state, new_state + ) + with connection.schema_editor(collect_sql=True) as editor: + operation.database_backwards( + "test_rmwsrfcs", editor, new_state, project_state + ) + collected_sql = "\n".join(editor.collected_sql) + self.assertIn(connection.ops.quote_name("test_rmwsrfcs_rider"), collected_sql) + self.assertIn(connection.ops.quote_name("friend_id"), collected_sql) + def test_rename_model_with_superclass_fk(self): """ Tests the RenameModel operation on a model which has a superclass that |
