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