summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorsiddus <dcsid10@gmail.com>2026-05-27 18:35:28 -0400
committerJacob Walls <jacobtylerwalls@gmail.com>2026-06-11 10:07:02 -0400
commitc6f81b383251dc9fd0918b4ba040c993444b931e (patch)
tree95713e49084752818ed0f261064f121940b8ef9a
parente8cff2921ba169d806a15de18304f431f12700f4 (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.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