diff options
| -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 |
