#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:  1
Easy pickings:  0               |                    UI/UX:  0
--------------------------------+--------------------------------------
Comment (by Vishy Algo):

 Replying to [comment:6 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(
 > }}}


 The current proposal to introduce a cascade keyword argument in
 delete_model lacks a mechanism to prevent syntax errors on backends that
 do not support DDL-level cascading.

 As seen in the migration test failures, SQLite chokes on the CASCADE
 keyword. Rather than forcing every caller to check the backend vendor, we
 should introduce a new feature flag: supports_table_drop_cascade. This
 allows BaseDatabaseSchemaEditor to stay backend-agnostic by conditionally
 selecting the SQL template based on both the user's intent (cascade=True)
 and the backend’s capability.

 This approach ensures the API is extensible and backward-compatible
 without breaking SQLite or third-party backends.
-- 
Ticket URL: <https://code.djangoproject.com/ticket/36779#comment:8>
Django <https://code.djangoproject.com/>
The Web framework for perfectionists with deadlines.

-- 
You received this message because you are subscribed to the Google Groups 
"Django updates" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
To view this discussion visit 
https://groups.google.com/d/msgid/django-updates/0107019b4dedce19-f1725247-68c1-44c8-9588-f5d83c3d1559-000000%40eu-central-1.amazonses.com.

Reply via email to