Commits

Andrew Godwin  committed 2be4eed Merge

Merged in charettes/south/delete-recursive-foreign-key (pull request #50)

  • Participants
  • Parent commits 90388aa, 1ec9bcf

Comments (0)

Files changed (2)

File south/db/generic.py

 
     @cached_property
     def has_ddl_transactions(self):
-        "Tests the database using feature detection to see if it has DDL transactional support"
+        """
+        Tests the database using feature detection to see if it has
+        transactional DDL support.
+        """
         self._possibly_initialise()
         connection = self._get_connection()
         if hasattr(connection.features, "confirm") and not connection.features._confirmed:
 
     @invalidate_table_constraints
     def delete_foreign_key(self, table_name, column):
-        "Drop a foreign key constraint"
+        """
+        Drop a foreign key constraint
+        """
         if self.dry_run:
             if self.debug:
                 print '   - no dry run output for delete_foreign_key() due to dynamic DDL, sorry'
     drop_foreign_key = alias('delete_foreign_key')
 
     def _find_foreign_constraints(self, table_name, column_name=None):
-        return list(self._constraints_affecting_columns(
-                    table_name, [column_name], "FOREIGN KEY"))
+        constraints = self._constraints_affecting_columns(
+                            table_name, [column_name], "FOREIGN KEY")
+
+        primary_key_columns = self._find_primary_key_columns(table_name)
+
+        if len(primary_key_columns) > 1:
+            # Composite primary keys cannot be referenced by a foreign key
+            return list(constraints)
+        else:
+            primary_key_columns.add(column_name)
+            recursive_constraints = set(self._constraints_affecting_columns(
+                                table_name, primary_key_columns, "FOREIGN KEY"))
+            return list(recursive_constraints.union(constraints))
 
     def _digest(self, *args):
         """
             "columns": ", ".join(map(self.quote_name, columns)),
         })
 
+    def _find_primary_key_columns(self, table_name):
+        """
+        Find all columns of the primary key of the specified table
+        """
+        db_name = self._get_setting('NAME')
+        
+        primary_key_columns = set()
+        for col, constraints in self.lookup_constraint(db_name, table_name):
+            for kind, cname in constraints:
+                if kind == 'PRIMARY KEY':
+                    primary_key_columns.add(col.lower())
+                    
+        return primary_key_columns
+
     def start_transaction(self):
         """
         Makes sure the following commands are inside a transaction.

File south/tests/db.py

 
 from south.db import db, generic
 from django.db import connection, models
+from django.utils.unittest.case import skipUnless
 
 # Create a list of error classes from the various database libraries
 errors = []
             ('UNIQUE', models.ForeignKey(Test)),
         ])
         db.execute_deferred_sql()
+        
+    @skipUnless(db.supports_foreign_keys, 'Foreign keys can only be deleted on '
+                                          'engines that support them.')
+    def test_recursive_foreign_key_delete(self):
+        """
+        Test that recursive foreign keys are deleted correctly (see #1065)
+        """
+        Test = db.mock_model(model_name='Test', db_table='test_rec_fk_del',
+                             db_tablespace='', pk_field_name='id',
+                             pk_field_type=models.AutoField, pk_field_args=[])
+        db.create_table('test_rec_fk_del', [
+            ('id', models.AutoField(primary_key=True, auto_created=True)),
+            ('fk', models.ForeignKey(Test)),
+        ])
+        db.execute_deferred_sql()
+        db.delete_foreign_key('test_rec_fk_del', 'fk_id')
     
     def test_rename(self):
         """