#36779: DeleteModel can lead to missing FK constraints
--------------------------------+--------------------------------------
Reporter: Jamie Cockburn | Owner: Vishy Algo
Type: Bug | Status: assigned
Component: Migrations | Version: 6.0
Severity: Normal | Resolution:
Keywords: | Triage Stage: Accepted
Has patch: 1 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
--------------------------------+--------------------------------------
Comment (by Simon Charette):
Well I'm not sure how I missed it in my initial analysis (maybe something
got cached in my `django-docker-box` setup) but it appears that removing
the `CASCADE` from `DROM TABLE` actually breaks a ton of tests so there
might be more to it than I initially thought.
Most of the failures seem to point at an innaproprite use of
`delete_model` in test teardown though and not at an actual problem in
migrations (which explicitly drop foreign keys first).
The following patch might be a more realistic approach to the problem
{{{#!diff
diff --git a/django/db/backends/base/schema.py
b/django/db/backends/base/schema.py
index 1f27d6a0d4..6982d094bf 100644
--- a/django/db/backends/base/schema.py
+++ b/django/db/backends/base/schema.py
@@ -86,7 +86,8 @@ class BaseDatabaseSchemaEditor:
sql_create_table = "CREATE TABLE %(table)s (%(definition)s)"
sql_rename_table = "ALTER TABLE %(old_table)s RENAME TO
%(new_table)s"
sql_retablespace_table = "ALTER TABLE %(table)s SET TABLESPACE
%(new_tablespace)s"
- sql_delete_table = "DROP TABLE %(table)s CASCADE"
+ sql_delete_table = "DROP TABLE %(table)s"
+ sql_delete_table_cascade = "DROP TABLE %(table)s CASCADE"
sql_create_column = "ALTER TABLE %(table)s ADD COLUMN %(column)s
%(definition)s"
sql_alter_column = "ALTER TABLE %(table)s %(changes)s"
@@ -538,7 +539,7 @@ class BaseDatabaseSchemaEditor:
if field.remote_field.through._meta.auto_created:
self.create_model(field.remote_field.through)
- def delete_model(self, model):
+ def delete_model(self, model, *, cascade=False):
"""Delete a model from the database."""
# Handle auto-created intermediary models
for field in model._meta.local_many_to_many:
@@ -546,8 +547,9 @@ class BaseDatabaseSchemaEditor:
self.delete_model(field.remote_field.through)
# Delete the table
+ delete_sql = self.sql_delete_table_cascade if cascade else
self.sql_delete_table
self.execute(
- self.sql_delete_table
+ delete_sql
% {
"table": self.quote_name(model._meta.db_table),
}
diff --git a/django/db/backends/oracle/schema.py
b/django/db/backends/oracle/schema.py
index 13fa7220ce..281aa1db7b 100644
--- a/django/db/backends/oracle/schema.py
+++ b/django/db/backends/oracle/schema.py
@@ -23,7 +23,7 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
"CONSTRAINT %(name)s REFERENCES
%(to_table)s(%(to_column)s)%(on_delete_db)"
"s%(deferrable)s"
)
- sql_delete_table = "DROP TABLE %(table)s CASCADE CONSTRAINTS"
+ sql_delete_table_cascade = "DROP TABLE %(table)s CASCADE CONSTRAINTS"
sql_create_index = "CREATE INDEX %(name)s ON %(table)s
(%(columns)s)%(extra)s"
def quote_value(self, value):
@@ -47,9 +47,9 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
self._drop_identity(model._meta.db_table, field.column)
super().remove_field(model, field)
- def delete_model(self, model):
+ def delete_model(self, model, *, cascade=False):
# Run superclass action
- super().delete_model(model)
+ super().delete_model(model, cascade=cascade)
# Clean up manually created sequence.
self.execute(
"""
diff --git a/django/db/backends/sqlite3/schema.py
b/django/db/backends/sqlite3/schema.py
index ee6163c253..6313b9be43 100644
--- a/django/db/backends/sqlite3/schema.py
+++ b/django/db/backends/sqlite3/schema.py
@@ -10,7 +10,8 @@ from django.db.models import CompositePrimaryKey,
UniqueConstraint
class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
- sql_delete_table = "DROP TABLE %(table)s"
+ # SQLite doesn't support DROP TABLE CASCADE.
+ sql_delete_table_cascade = "DROP TABLE %(table)s"
sql_create_fk = None
sql_create_inline_fk = (
"REFERENCES %(to_table)s (%(to_column)s)%(on_delete_db)s
DEFERRABLE INITIALLY "
@@ -278,9 +279,9 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
if restore_pk_field:
restore_pk_field.primary_key = True
- def delete_model(self, model, handle_autom2m=True):
+ def delete_model(self, model, handle_autom2m=True, *, cascade=False):
if handle_autom2m:
- super().delete_model(model)
+ super().delete_model(model, cascade=cascade)
else:
# Delete the table (and only that)
self.execute(
diff --git a/tests/schema/tests.py b/tests/schema/tests.py
index ab8b07e9d3..2aaafc93e3 100644
--- a/tests/schema/tests.py
+++ b/tests/schema/tests.py
@@ -159,7 +159,7 @@ class SchemaTests(TransactionTestCase):
if self.isolated_local_models:
with connection.schema_editor() as editor:
for model in self.isolated_local_models:
- editor.delete_model(model)
+ editor.delete_model(model, cascade=True)
def delete_tables(self):
"Deletes all model tables for our models for a clean test
environment"
@@ -174,7 +174,7 @@ class SchemaTests(TransactionTestCase):
if connection.features.ignores_table_name_case:
tbl = tbl.lower()
if tbl in table_names:
- editor.delete_model(model)
+ editor.delete_model(model, cascade=True)
table_names.remove(tbl)
connection.enable_constraint_checking()
@@ -1542,7 +1542,7 @@ class SchemaTests(TransactionTestCase):
def drop_collation():
with connection.cursor() as cursor:
- cursor.execute(f"DROP COLLATION IF EXISTS
{ci_collation}")
+ cursor.execute(f"DROP COLLATION IF EXISTS {ci_collation}
CASCADE")
with connection.cursor() as cursor:
cursor.execute(
}}}
--
Ticket URL: <
https://code.djangoproject.com/ticket/36779#comment:6>