summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--django/db/backends/base/schema.py16
-rw-r--r--tests/migrations/test_operations.py38
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