Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Fixed #3615: Added support for loading fixtures with forward referenc…

…es on database backends (such as MySQL/InnoDB) that do not support deferred constraint checking. Many thanks to jsdalton for coming up with a clever solution to this long-standing issue, and to jacob, ramiro, graham_king, and russellm for review/testing. (Apologies if I missed anyone else who helped here.)

git-svn-id: http://code.djangoproject.com/svn/django/trunk@16590 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit be87f0b0ec992e6f3e72735d8e95c654da969f6d 1 parent e3c8934
Karen Tracey authored August 07, 2011
24  django/core/management/commands/loaddata.py
... ...
@@ -1,3 +1,7 @@
  1
+# This is necessary in Python 2.5 to enable the with statement, in 2.6
  2
+# and up it is no longer necessary.
  3
+from __future__ import with_statement
  4
+
1 5
 import sys
2 6
 import os
3 7
 import gzip
@@ -166,12 +170,20 @@ def read(self):
166 170
                                     (format, fixture_name, humanize(fixture_dir)))
167 171
                             try:
168 172
                                 objects = serializers.deserialize(format, fixture, using=using)
169  
-                                for obj in objects:
170  
-                                    objects_in_fixture += 1
171  
-                                    if router.allow_syncdb(using, obj.object.__class__):
172  
-                                        loaded_objects_in_fixture += 1
173  
-                                        models.add(obj.object.__class__)
174  
-                                        obj.save(using=using)
  173
+
  174
+                                with connection.constraint_checks_disabled():
  175
+                                    for obj in objects:
  176
+                                        objects_in_fixture += 1
  177
+                                        if router.allow_syncdb(using, obj.object.__class__):
  178
+                                            loaded_objects_in_fixture += 1
  179
+                                            models.add(obj.object.__class__)
  180
+                                            obj.save(using=using)
  181
+
  182
+                                # Since we disabled constraint checks, we must manually check for
  183
+                                # any invalid keys that might have been added
  184
+                                table_names = [model._meta.db_table for model in models]
  185
+                                connection.check_constraints(table_names=table_names)
  186
+
175 187
                                 loaded_object_count += loaded_objects_in_fixture
176 188
                                 fixture_object_count += objects_in_fixture
177 189
                                 label_found = True
43  django/db/backends/__init__.py
@@ -3,6 +3,7 @@
3 3
 except ImportError:
4 4
     import dummy_thread as thread
5 5
 from threading import local
  6
+from contextlib import contextmanager
6 7
 
7 8
 from django.conf import settings
8 9
 from django.db import DEFAULT_DB_ALIAS
@@ -238,6 +239,35 @@ def savepoint_commit(self, sid):
238 239
         if self.savepoint_state:
239 240
             self._savepoint_commit(sid)
240 241
 
  242
+    @contextmanager
  243
+    def constraint_checks_disabled(self):
  244
+        disabled = self.disable_constraint_checking()
  245
+        try:
  246
+            yield
  247
+        finally:
  248
+            if disabled:
  249
+                self.enable_constraint_checking()
  250
+
  251
+    def disable_constraint_checking(self):
  252
+        """
  253
+        Backends can implement as needed to temporarily disable foreign key constraint
  254
+        checking.
  255
+        """
  256
+        pass
  257
+
  258
+    def enable_constraint_checking(self):
  259
+        """
  260
+        Backends can implement as needed to re-enable foreign key constraint checking.
  261
+        """
  262
+        pass
  263
+
  264
+    def check_constraints(self, table_names=None):
  265
+        """
  266
+        Backends can override this method if they can apply constraint checking (e.g. via "SET CONSTRAINTS
  267
+        ALL IMMEDIATE"). Should raise an IntegrityError if any invalid foreign key references are encountered.
  268
+        """
  269
+        pass
  270
+
241 271
     def close(self):
242 272
         if self.connection is not None:
243 273
             self.connection.close()
@@ -869,6 +899,19 @@ def sequence_list(self):
869 899
 
870 900
         return sequence_list
871 901
 
  902
+    def get_key_columns(self, cursor, table_name):
  903
+        """
  904
+        Backends can override this to return a list of (column_name, referenced_table_name,
  905
+        referenced_column_name) for all key columns in given table.
  906
+        """
  907
+        raise NotImplementedError
  908
+
  909
+    def get_primary_key_column(self, cursor, table_name):
  910
+        """
  911
+        Backends can override this to return the column name of the primary key for the given table.
  912
+        """
  913
+        raise NotImplementedError
  914
+
872 915
 class BaseDatabaseClient(object):
873 916
     """
874 917
     This class encapsulates all backend-specific methods for opening a
1  django/db/backends/dummy/base.py
@@ -34,6 +34,7 @@ class DatabaseIntrospection(BaseDatabaseIntrospection):
34 34
     get_table_description = complain
35 35
     get_relations = complain
36 36
     get_indexes = complain
  37
+    get_key_columns = complain
37 38
 
38 39
 class DatabaseWrapper(BaseDatabaseWrapper):
39 40
     operators = {}
49  django/db/backends/mysql/base.py
@@ -349,3 +349,52 @@ def get_server_version(self):
349 349
                 raise Exception('Unable to determine MySQL version from version string %r' % self.connection.get_server_info())
350 350
             self.server_version = tuple([int(x) for x in m.groups()])
351 351
         return self.server_version
  352
+
  353
+    def disable_constraint_checking(self):
  354
+        """
  355
+        Disables foreign key checks, primarily for use in adding rows with forward references. Always returns True,
  356
+        to indicate constraint checks need to be re-enabled.
  357
+        """
  358
+        self.cursor().execute('SET foreign_key_checks=0')
  359
+        return True
  360
+
  361
+    def enable_constraint_checking(self):
  362
+        """
  363
+        Re-enable foreign key checks after they have been disabled.
  364
+        """
  365
+        self.cursor().execute('SET foreign_key_checks=1')
  366
+
  367
+    def check_constraints(self, table_names=None):
  368
+        """
  369
+        Checks each table name in table-names for rows with invalid foreign key references. This method is
  370
+        intended to be used in conjunction with `disable_constraint_checking()` and `enable_constraint_checking()`, to
  371
+        determine if rows with invalid references were entered while constraint checks were off.
  372
+
  373
+        Raises an IntegrityError on the first invalid foreign key reference encountered (if any) and provides
  374
+        detailed information about the invalid reference in the error message.
  375
+
  376
+        Backends can override this method if they can more directly apply constraint checking (e.g. via "SET CONSTRAINTS
  377
+        ALL IMMEDIATE")
  378
+        """
  379
+        cursor = self.cursor()
  380
+        if table_names is None:
  381
+            table_names = self.introspection.get_table_list(cursor)
  382
+        for table_name in table_names:
  383
+            primary_key_column_name = self.introspection.get_primary_key_column(cursor, table_name)
  384
+            if not primary_key_column_name:
  385
+                continue
  386
+            key_columns = self.introspection.get_key_columns(cursor, table_name)
  387
+            for column_name, referenced_table_name, referenced_column_name in key_columns:
  388
+                cursor.execute("""
  389
+                    SELECT REFERRING.`%s`, REFERRING.`%s` FROM `%s` as REFERRING
  390
+                    LEFT JOIN `%s` as REFERRED
  391
+                    ON (REFERRING.`%s` = REFERRED.`%s`)
  392
+                    WHERE REFERRING.`%s` IS NOT NULL AND REFERRED.`%s` IS NULL"""
  393
+                    % (primary_key_column_name, column_name, table_name, referenced_table_name,
  394
+                    column_name, referenced_column_name, column_name, referenced_column_name))
  395
+                for bad_row in cursor.fetchall():
  396
+                    raise utils.IntegrityError("The row in table '%s' with primary key '%s' has an invalid "
  397
+                        "foreign key: %s.%s contains a value '%s' that does not have a corresponding value in %s.%s."
  398
+                        % (table_name, bad_row[0],
  399
+                        table_name, column_name, bad_row[1],
  400
+                        referenced_table_name, referenced_column_name))
34  django/db/backends/mysql/introspection.py
@@ -51,10 +51,21 @@ def get_relations(self, cursor, table_name):
51 51
         representing all relationships to the given table. Indexes are 0-based.
52 52
         """
53 53
         my_field_dict = self._name_to_index(cursor, table_name)
54  
-        constraints = []
  54
+        constraints = self.get_key_columns(cursor, table_name)
55 55
         relations = {}
  56
+        for my_fieldname, other_table, other_field in constraints:
  57
+            other_field_index = self._name_to_index(cursor, other_table)[other_field]
  58
+            my_field_index = my_field_dict[my_fieldname]
  59
+            relations[my_field_index] = (other_field_index, other_table)
  60
+        return relations
  61
+
  62
+    def get_key_columns(self, cursor, table_name):
  63
+        """
  64
+        Returns a list of (column_name, referenced_table_name, referenced_column_name) for all
  65
+        key columns in given table.
  66
+        """
  67
+        key_columns = []
56 68
         try:
57  
-            # This should work for MySQL 5.0.
58 69
             cursor.execute("""
59 70
                 SELECT column_name, referenced_table_name, referenced_column_name
60 71
                 FROM information_schema.key_column_usage
@@ -62,7 +73,7 @@ def get_relations(self, cursor, table_name):
62 73
                     AND table_schema = DATABASE()
63 74
                     AND referenced_table_name IS NOT NULL
64 75
                     AND referenced_column_name IS NOT NULL""", [table_name])
65  
-            constraints.extend(cursor.fetchall())
  76
+            key_columns.extend(cursor.fetchall())
66 77
         except (ProgrammingError, OperationalError):
67 78
             # Fall back to "SHOW CREATE TABLE", for previous MySQL versions.
68 79
             # Go through all constraints and save the equal matches.
@@ -74,14 +85,17 @@ def get_relations(self, cursor, table_name):
74 85
                     if match == None:
75 86
                         break
76 87
                     pos = match.end()
77  
-                    constraints.append(match.groups())
  88
+                    key_columns.append(match.groups())
  89
+        return key_columns
78 90
 
79  
-        for my_fieldname, other_table, other_field in constraints:
80  
-            other_field_index = self._name_to_index(cursor, other_table)[other_field]
81  
-            my_field_index = my_field_dict[my_fieldname]
82  
-            relations[my_field_index] = (other_field_index, other_table)
83  
-
84  
-        return relations
  91
+    def get_primary_key_column(self, cursor, table_name):
  92
+        """
  93
+        Returns the name of the primary key column for the given table
  94
+        """
  95
+        for column in self.get_indexes(cursor, table_name).iteritems():
  96
+            if column[1]['primary_key']:
  97
+                return column[0]
  98
+        return None
85 99
 
86 100
     def get_indexes(self, cursor, table_name):
87 101
         """
8  django/db/backends/oracle/base.py
@@ -428,6 +428,14 @@ def __init__(self, *args, **kwargs):
428 428
         self.introspection = DatabaseIntrospection(self)
429 429
         self.validation = BaseDatabaseValidation(self)
430 430
 
  431
+    def check_constraints(self, table_names=None):
  432
+        """
  433
+        To check constraints, we set constraints to immediate. Then, when, we're done we must ensure they
  434
+        are returned to deferred.
  435
+        """
  436
+        self.cursor().execute('SET CONSTRAINTS ALL IMMEDIATE')
  437
+        self.cursor().execute('SET CONSTRAINTS ALL DEFERRED')
  438
+
431 439
     def _valid_connection(self):
432 440
         return self.connection is not None
433 441
 
8  django/db/backends/postgresql_psycopg2/base.py
@@ -106,6 +106,14 @@ def __init__(self, *args, **kwargs):
106 106
         self.validation = BaseDatabaseValidation(self)
107 107
         self._pg_version = None
108 108
 
  109
+    def check_constraints(self, table_names=None):
  110
+        """
  111
+        To check constraints, we set constraints to immediate. Then, when, we're done we must ensure they
  112
+        are returned to deferred.
  113
+        """
  114
+        self.cursor().execute('SET CONSTRAINTS ALL IMMEDIATE')
  115
+        self.cursor().execute('SET CONSTRAINTS ALL DEFERRED')
  116
+
109 117
     def _get_pg_version(self):
110 118
         if self._pg_version is None:
111 119
             self._pg_version = get_version(self.connection)
34  django/db/backends/sqlite3/base.py
@@ -206,6 +206,40 @@ def _cursor(self):
206 206
             connection_created.send(sender=self.__class__, connection=self)
207 207
         return self.connection.cursor(factory=SQLiteCursorWrapper)
208 208
 
  209
+    def check_constraints(self, table_names=None):
  210
+        """
  211
+        Checks each table name in table-names for rows with invalid foreign key references. This method is
  212
+        intended to be used in conjunction with `disable_constraint_checking()` and `enable_constraint_checking()`, to
  213
+        determine if rows with invalid references were entered while constraint checks were off.
  214
+
  215
+        Raises an IntegrityError on the first invalid foreign key reference encountered (if any) and provides
  216
+        detailed information about the invalid reference in the error message.
  217
+
  218
+        Backends can override this method if they can more directly apply constraint checking (e.g. via "SET CONSTRAINTS
  219
+        ALL IMMEDIATE")
  220
+        """
  221
+        cursor = self.cursor()
  222
+        if table_names is None:
  223
+            table_names = self.introspection.get_table_list(cursor)
  224
+        for table_name in table_names:
  225
+            primary_key_column_name = self.introspection.get_primary_key_column(cursor, table_name)
  226
+            if not primary_key_column_name:
  227
+                continue
  228
+            key_columns = self.introspection.get_key_columns(cursor, table_name)
  229
+            for column_name, referenced_table_name, referenced_column_name in key_columns:
  230
+                cursor.execute("""
  231
+                    SELECT REFERRING.`%s`, REFERRING.`%s` FROM `%s` as REFERRING
  232
+                    LEFT JOIN `%s` as REFERRED
  233
+                    ON (REFERRING.`%s` = REFERRED.`%s`)
  234
+                    WHERE REFERRING.`%s` IS NOT NULL AND REFERRED.`%s` IS NULL"""
  235
+                    % (primary_key_column_name, column_name, table_name, referenced_table_name,
  236
+                    column_name, referenced_column_name, column_name, referenced_column_name))
  237
+                for bad_row in cursor.fetchall():
  238
+                    raise utils.IntegrityError("The row in table '%s' with primary key '%s' has an invalid "
  239
+                        "foreign key: %s.%s contains a value '%s' that does not have a corresponding value in %s.%s."
  240
+                        % (table_name, bad_row[0], table_name, column_name, bad_row[1],
  241
+                        referenced_table_name, referenced_column_name))
  242
+
209 243
     def close(self):
210 244
         # If database is in memory, closing the connection destroys the
211 245
         # database. To prevent accidental data loss, ignore close requests on
44  django/db/backends/sqlite3/introspection.py
@@ -103,6 +103,35 @@ def get_relations(self, cursor, table_name):
103 103
 
104 104
         return relations
105 105
 
  106
+    def get_key_columns(self, cursor, table_name):
  107
+        """
  108
+        Returns a list of (column_name, referenced_table_name, referenced_column_name) for all
  109
+        key columns in given table.
  110
+        """
  111
+        key_columns = []
  112
+
  113
+        # Schema for this table
  114
+        cursor.execute("SELECT sql FROM sqlite_master WHERE tbl_name = %s AND type = %s", [table_name, "table"])
  115
+        results = cursor.fetchone()[0].strip()
  116
+        results = results[results.index('(')+1:results.rindex(')')]
  117
+
  118
+        # Walk through and look for references to other tables. SQLite doesn't
  119
+        # really have enforced references, but since it echoes out the SQL used
  120
+        # to create the table we can look for REFERENCES statements used there.
  121
+        for field_index, field_desc in enumerate(results.split(',')):
  122
+            field_desc = field_desc.strip()
  123
+            if field_desc.startswith("UNIQUE"):
  124
+                continue
  125
+
  126
+            m = re.search('"(.*)".*references (.*) \(["|](.*)["|]\)', field_desc, re.I)
  127
+            if not m:
  128
+                continue
  129
+
  130
+            # This will append (column_name, referenced_table_name, referenced_column_name) to key_columns
  131
+            key_columns.append(tuple([s.strip('"') for s in m.groups()]))
  132
+
  133
+        return key_columns
  134
+
106 135
     def get_indexes(self, cursor, table_name):
107 136
         """
108 137
         Returns a dictionary of fieldname -> infodict for the given table,
@@ -128,6 +157,21 @@ def get_indexes(self, cursor, table_name):
128 157
             indexes[name]['unique'] = True
129 158
         return indexes
130 159
 
  160
+    def get_primary_key_column(self, cursor, table_name):
  161
+        """
  162
+        Get the column name of the primary key for the given table.
  163
+        """
  164
+        # Don't use PRAGMA because that causes issues with some transactions
  165
+        cursor.execute("SELECT sql FROM sqlite_master WHERE tbl_name = %s AND type = %s", [table_name, "table"])
  166
+        results = cursor.fetchone()[0].strip()
  167
+        results = results[results.index('(')+1:results.rindex(')')]
  168
+        for field_desc in results.split(','):
  169
+            field_desc = field_desc.strip()
  170
+            m = re.search('"(.*)".*PRIMARY KEY$', field_desc)
  171
+            if m:
  172
+                return m.groups()[0]
  173
+        return None
  174
+
131 175
     def _table_info(self, cursor, name):
132 176
         cursor.execute('PRAGMA table_info(%s)' % self.connection.ops.quote_name(name))
133 177
         # cid, name, type, notnull, dflt_value, pk
12  docs/ref/databases.txt
@@ -142,6 +142,18 @@ currently the only engine that supports full-text indexing and searching.
142 142
 The InnoDB_ engine is fully transactional and supports foreign key references
143 143
 and is probably the best choice at this point in time.
144 144
 
  145
+.. versionchanged:: 1.4
  146
+
  147
+In previous versions of Django, fixtures with forward references (i.e.
  148
+relations to rows that have not yet been inserted into the database) would fail
  149
+to load when using the InnoDB storage engine. This was due to the fact that InnoDB
  150
+deviates from the SQL standard by checking foreign key constraints immediately
  151
+instead of deferring the check until the transaction is committed. This
  152
+problem has been resolved in Django 1.4. Fixture data is now loaded with foreign key
  153
+checks turned off; foreign key checks are then re-enabled when the data has
  154
+finished loading, at which point the entire table is checked for invalid foreign
  155
+key references and an `IntegrityError` is raised if any are found.
  156
+
145 157
 .. _storage engines: http://dev.mysql.com/doc/refman/5.5/en/storage-engines.html
146 158
 .. _MyISAM: http://dev.mysql.com/doc/refman/5.5/en/myisam-storage-engine.html
147 159
 .. _InnoDB: http://dev.mysql.com/doc/refman/5.5/en/innodb.html
3  docs/releases/1.4.txt
@@ -235,6 +235,9 @@ Django 1.4 also includes several smaller improvements worth noting:
235 235
   to delete all files at the destination before copying or linking the static
236 236
   files.
237 237
 
  238
+* It is now possible to load fixtures containing forward references when using
  239
+  MySQL with the InnoDB database engine.
  240
+
238 241
 .. _backwards-incompatible-changes-1.4:
239 242
 
240 243
 Backwards incompatible changes in 1.4
11  tests/modeltests/serializers/tests.py
... ...
@@ -1,3 +1,7 @@
  1
+# This is necessary in Python 2.5 to enable the with statement, in 2.6
  2
+# and up it is no longer necessary.
  3
+from __future__ import with_statement
  4
+
1 5
 # -*- coding: utf-8 -*-
2 6
 from datetime import datetime
3 7
 from StringIO import StringIO
@@ -5,7 +9,7 @@
5 9
 
6 10
 from django.conf import settings
7 11
 from django.core import serializers
8  
-from django.db import transaction
  12
+from django.db import transaction, connection
9 13
 from django.test import TestCase, TransactionTestCase, Approximate
10 14
 from django.utils import simplejson, unittest
11 15
 
@@ -252,8 +256,9 @@ def test_forward_refs(self):
252 256
         transaction.enter_transaction_management()
253 257
         transaction.managed(True)
254 258
         objs = serializers.deserialize(self.serializer_name, self.fwd_ref_str)
255  
-        for obj in objs:
256  
-            obj.save()
  259
+        with connection.constraint_checks_disabled():
  260
+            for obj in objs:
  261
+                obj.save()
257 262
         transaction.commit()
258 263
         transaction.leave_transaction_management()
259 264
 
64  tests/regressiontests/backends/tests.py
... ...
@@ -1,10 +1,11 @@
1 1
 # -*- coding: utf-8 -*-
2 2
 # Unit and doctests for specific database backends.
  3
+from __future__ import with_statement
3 4
 import datetime
4 5
 
5 6
 from django.conf import settings
6 7
 from django.core.management.color import no_style
7  
-from django.db import backend, connection, connections, DEFAULT_DB_ALIAS, IntegrityError
  8
+from django.db import backend, connection, connections, DEFAULT_DB_ALIAS, IntegrityError, transaction
8 9
 from django.db.backends.signals import connection_created
9 10
 from django.db.backends.postgresql_psycopg2 import version as pg_version
10 11
 from django.test import TestCase, skipUnlessDBFeature, TransactionTestCase
@@ -328,7 +329,8 @@ def test_integrity_checks_on_creation(self):
328 329
         try:
329 330
             a.save()
330 331
         except IntegrityError:
331  
-            pass
  332
+            return
  333
+        self.skipTest("This backend does not support integrity checks.")
332 334
 
333 335
     def test_integrity_checks_on_update(self):
334 336
         """
@@ -343,4 +345,60 @@ def test_integrity_checks_on_update(self):
343 345
         try:
344 346
             a.save()
345 347
         except IntegrityError:
346  
-            pass
  348
+            return
  349
+        self.skipTest("This backend does not support integrity checks.")
  350
+
  351
+    def test_disable_constraint_checks_manually(self):
  352
+        """
  353
+        When constraint checks are disabled, should be able to write bad data without IntegrityErrors.
  354
+        """
  355
+        with transaction.commit_manually():
  356
+            # Create an Article.
  357
+            models.Article.objects.create(headline="Test article", pub_date=datetime.datetime(2010, 9, 4), reporter=self.r)
  358
+            # Retrive it from the DB
  359
+            a = models.Article.objects.get(headline="Test article")
  360
+            a.reporter_id = 30
  361
+            try:
  362
+                connection.disable_constraint_checking()
  363
+                a.save()
  364
+                connection.enable_constraint_checking()
  365
+            except IntegrityError:
  366
+                self.fail("IntegrityError should not have occurred.")
  367
+            finally:
  368
+                transaction.rollback()
  369
+
  370
+    def test_disable_constraint_checks_context_manager(self):
  371
+        """
  372
+        When constraint checks are disabled (using context manager), should be able to write bad data without IntegrityErrors.
  373
+        """
  374
+        with transaction.commit_manually():
  375
+            # Create an Article.
  376
+            models.Article.objects.create(headline="Test article", pub_date=datetime.datetime(2010, 9, 4), reporter=self.r)
  377
+            # Retrive it from the DB
  378
+            a = models.Article.objects.get(headline="Test article")
  379
+            a.reporter_id = 30
  380
+            try:
  381
+                with connection.constraint_checks_disabled():
  382
+                    a.save()
  383
+            except IntegrityError:
  384
+                self.fail("IntegrityError should not have occurred.")
  385
+            finally:
  386
+                transaction.rollback()
  387
+
  388
+    def test_check_constraints(self):
  389
+        """
  390
+        Constraint checks should raise an IntegrityError when bad data is in the DB.
  391
+        """
  392
+        with transaction.commit_manually():
  393
+            # Create an Article.
  394
+            models.Article.objects.create(headline="Test article", pub_date=datetime.datetime(2010, 9, 4), reporter=self.r)
  395
+            # Retrive it from the DB
  396
+            a = models.Article.objects.get(headline="Test article")
  397
+            a.reporter_id = 30
  398
+            try:
  399
+                with connection.constraint_checks_disabled():
  400
+                    a.save()
  401
+                    with self.assertRaises(IntegrityError):
  402
+                        connection.check_constraints()
  403
+            finally:
  404
+                transaction.rollback()
29  tests/regressiontests/fixtures_regress/tests.py
@@ -362,6 +362,35 @@ def test_proxy_model_included(self):
362 362
             % widget.pk
363 363
             )
364 364
 
  365
+    def test_loaddata_works_when_fixture_has_forward_refs(self):
  366
+        """
  367
+        Regression for #3615 - Forward references cause fixtures not to load in MySQL (InnoDB)
  368
+        """
  369
+        management.call_command(
  370
+            'loaddata',
  371
+            'forward_ref.json',
  372
+            verbosity=0,
  373
+            commit=False
  374
+        )
  375
+        self.assertEqual(Book.objects.all()[0].id, 1)
  376
+        self.assertEqual(Person.objects.all()[0].id, 4)
  377
+
  378
+    def test_loaddata_raises_error_when_fixture_has_invalid_foreign_key(self):
  379
+        """
  380
+        Regression for #3615 - Ensure data with nonexistent child key references raises error
  381
+        """
  382
+        stderr = StringIO()
  383
+        management.call_command(
  384
+            'loaddata',
  385
+            'forward_ref_bad_data.json',
  386
+            verbosity=0,
  387
+            commit=False,
  388
+            stderr=stderr,
  389
+        )
  390
+        self.assertTrue(
  391
+            stderr.getvalue().startswith('Problem installing fixture')
  392
+        )
  393
+
365 394
 
366 395
 class NaturalKeyFixtureTests(TestCase):
367 396
     def assertRaisesMessage(self, exc, msg, func, *args, **kwargs):
10  tests/regressiontests/introspection/tests.py
@@ -95,6 +95,16 @@ def test_get_relations(self):
95 95
             # That's {field_index: (field_index_other_table, other_table)}
96 96
             self.assertEqual(relations, {3: (0, Reporter._meta.db_table)})
97 97
 
  98
+    def test_get_key_columns(self):
  99
+        cursor = connection.cursor()
  100
+        key_columns = connection.introspection.get_key_columns(cursor, Article._meta.db_table)
  101
+        self.assertEqual(key_columns, [(u'reporter_id', Reporter._meta.db_table, u'id')])
  102
+
  103
+    def test_get_primary_key_column(self):
  104
+        cursor = connection.cursor()
  105
+        primary_key_column = connection.introspection.get_primary_key_column(cursor, Article._meta.db_table)
  106
+        self.assertEqual(primary_key_column, u'id')
  107
+
98 108
     def test_get_indexes(self):
99 109
         cursor = connection.cursor()
100 110
         indexes = connection.introspection.get_indexes(cursor, Article._meta.db_table)
5  tests/regressiontests/serializers_regress/tests.py
@@ -6,6 +6,8 @@
6 6
 the serializers. This includes all valid data values, plus
7 7
 forward, backwards and self references.
8 8
 """
  9
+# This is necessary in Python 2.5 to enable the with statement, in 2.6
  10
+# and up it is no longer necessary.
9 11
 from __future__ import with_statement
10 12
 
11 13
 import datetime
@@ -382,7 +384,8 @@ def serializerTest(format, self):
382 384
     objects = []
383 385
     instance_count = {}
384 386
     for (func, pk, klass, datum) in test_data:
385  
-        objects.extend(func[0](pk, klass, datum))
  387
+        with connection.constraint_checks_disabled():
  388
+            objects.extend(func[0](pk, klass, datum))
386 389
 
387 390
     # Get a count of the number of objects created for each class
388 391
     for klass in instance_count:

0 notes on commit be87f0b

Please sign in to comment.
Something went wrong with that request. Please try again.