Ticket #3615: allow_forward_refs_in_fixtures_v8.diff
File allow_forward_refs_in_fixtures_v8.diff, 25.7 KB (added by , 13 years ago) |
---|
-
django/core/management/commands/loaddata.py
diff --git a/django/core/management/commands/loaddata.py b/django/core/management/commands/loaddata.py index 34f3543..0aa07f4 100644
a b 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 … … class Command(BaseCommand): 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 -
django/db/backends/__init__.py
diff --git a/django/db/backends/__init__.py b/django/db/backends/__init__.py index 1c3bc7e..e4f40a8 100644
a b try: 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 … … class BaseDatabaseWrapper(local): 237 238 """ 238 239 if self.savepoint_state: 239 240 self._savepoint_commit(sid) 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 252 def disable_constraint_checking(self): 253 """ 254 Backends can implement as needed to temporarily disable foreign key constraint 255 checking. 256 """ 257 pass 258 259 def enable_constraint_checking(self): 260 """ 261 Backends can implement as needed to re-enable foreign key constraint checking. 262 """ 263 pass 264 265 def check_constraints(self, table_names=None): 266 """ 267 Backends can override this method if they can apply constraint checking (e.g. via "SET CONSTRAINTS 268 ALL IMMEDIATE"). Should raise an IntegrityError if any invalid foreign key references are encountered. 269 """ 270 pass 240 271 241 272 def close(self): 242 273 if self.connection is not None: … … class BaseDatabaseIntrospection(object): 869 900 870 901 return sequence_list 871 902 903 def get_key_columns(self, cursor, table_name): 904 """ 905 Backends can override this to return a list of (column_name, referenced_table_name, 906 referenced_column_name) for all key columns in given table. 907 """ 908 raise NotImplementedError 909 910 def get_primary_key_column(self, cursor, table_name): 911 """ 912 Backends can override this to return the column name of the primary key for the given table. 913 """ 914 raise NotImplementedError 915 872 916 class BaseDatabaseClient(object): 873 917 """ 874 918 This class encapsulates all backend-specific methods for opening a -
django/db/backends/dummy/base.py
diff --git a/django/db/backends/dummy/base.py b/django/db/backends/dummy/base.py index 7de48c8..746f26b 100644
a b 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 = {} -
django/db/backends/mysql/base.py
diff --git a/django/db/backends/mysql/base.py b/django/db/backends/mysql/base.py index 6d02aa7..7cf2b6d 100644
a b if (version < (1,2,1) or (version[:3] == (1, 2, 1) and 25 25 from MySQLdb.converters import conversions 26 26 from MySQLdb.constants import FIELD_TYPE, CLIENT 27 27 28 from django.db import utils 28 from django.db import utils, IntegrityError 29 29 from django.db.backends import * 30 30 from django.db.backends.signals import connection_created 31 31 from django.db.backends.mysql.client import DatabaseClient … … class DatabaseWrapper(BaseDatabaseWrapper): 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): 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 for table_name in table_names: 381 primary_key_column_name = self.introspection.get_primary_key_column(cursor, table_name) 382 if not primary_key_column_name: 383 continue 384 key_columns = self.introspection.get_key_columns(cursor, table_name) 385 for column_name, referenced_table_name, referenced_column_name in key_columns: 386 cursor.execute(""" 387 SELECT REFERRING.`%s`, REFERRING.`%s` FROM `%s` as REFERRING 388 LEFT JOIN `%s` as REFERRED 389 ON (REFERRING.`%s` = REFERRED.`%s`) 390 WHERE REFERRING.`%s` IS NOT NULL 391 AND REFERRED.`%s` IS NULL""" 392 % (primary_key_column_name, column_name, table_name, referenced_table_name, 393 column_name, referenced_column_name, column_name, referenced_column_name)) 394 for bad_row in cursor.fetchall(): 395 raise IntegrityError("The row in table '%s' with primary key '%s' has an invalid \ 396 foreign key: %s.%s contains a value '%s' that does not have a corresponding value in %s.%s." 397 % (table_name, bad_row[0], table_name, column_name, bad_row[1], 398 referenced_table_name, referenced_column_name)) -
django/db/backends/mysql/introspection.py
diff --git a/django/db/backends/mysql/introspection.py b/django/db/backends/mysql/introspection.py index 9e1518b..4612221 100644
a b class DatabaseIntrospection(BaseDatabaseIntrospection): 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 … … class DatabaseIntrospection(BaseDatabaseIntrospection): 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. … … class DatabaseIntrospection(BaseDatabaseIntrospection): 74 85 if match == None: 75 86 break 76 87 pos = match.end() 77 constraints.append(match.groups()) 78 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 88 key_columns.append(match.groups()) 89 return key_columns 90 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 """ -
django/db/backends/oracle/base.py
diff --git a/django/db/backends/oracle/base.py b/django/db/backends/oracle/base.py index 930b1bb..3cadb66 100644
a b class DatabaseWrapper(BaseDatabaseWrapper): 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 -
django/db/backends/postgresql_psycopg2/base.py
diff --git a/django/db/backends/postgresql_psycopg2/base.py b/django/db/backends/postgresql_psycopg2/base.py index 6ed59a6..4136d4f 100644
a b class DatabaseWrapper(BaseDatabaseWrapper): 105 105 self.introspection = DatabaseIntrospection(self) 106 106 self.validation = BaseDatabaseValidation(self) 107 107 self._pg_version = None 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') 108 116 109 117 def _get_pg_version(self): 110 118 if self._pg_version is None: -
django/db/backends/sqlite3/base.py
diff --git a/django/db/backends/sqlite3/base.py b/django/db/backends/sqlite3/base.py index 79c5ede..c92d72c 100644
a b class DatabaseWrapper(BaseDatabaseWrapper): 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): 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 for table_name in table_names: 223 primary_key_column_name = self.introspection.get_primary_key_column(cursor, table_name) 224 if not primary_key_column_name: 225 continue 226 key_columns = self.introspection.get_key_columns(cursor, table_name) 227 for column_name, referenced_table_name, referenced_column_name in key_columns: 228 cursor.execute(""" 229 SELECT REFERRING.`%s`, REFERRING.`%s` FROM `%s` as REFERRING 230 LEFT JOIN `%s` as REFERRED 231 ON (REFERRING.`%s` = REFERRED.`%s`) 232 WHERE REFERRING.`%s` IS NOT NULL 233 AND REFERRED.`%s` IS NULL""" 234 % (primary_key_column_name, column_name, table_name, referenced_table_name, 235 column_name, referenced_column_name, column_name, referenced_column_name)) 236 for bad_row in cursor.fetchall(): 237 raise IntegrityError("The row in table '%s' with primary key '%s' has an invalid \ 238 foreign key: %s.%s contains a value '%s' that does not have a corresponding value in %s.%s." 239 % (table_name, bad_row[0], table_name, column_name, bad_row[1], 240 referenced_table_name, referenced_column_name)) 241 209 242 def close(self): 210 243 # If database is in memory, closing the connection destroys the 211 244 # database. To prevent accidental data loss, ignore close requests on -
django/db/backends/sqlite3/introspection.py
diff --git a/django/db/backends/sqlite3/introspection.py b/django/db/backends/sqlite3/introspection.py index 5ee7b64..27de913 100644
a b class DatabaseIntrospection(BaseDatabaseIntrospection): 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, … … class DatabaseIntrospection(BaseDatabaseIntrospection): 127 156 name = info[0][2] # seqno, cid, name 128 157 indexes[name]['unique'] = True 129 158 return indexes 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 130 174 131 175 def _table_info(self, cursor, name): 132 176 cursor.execute('PRAGMA table_info(%s)' % self.connection.ops.quote_name(name)) -
docs/ref/databases.txt
diff --git a/docs/ref/databases.txt b/docs/ref/databases.txt index 2f55b9c..5a2042a 100644
a b 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 -
tests/modeltests/serializers/tests.py
diff --git a/tests/modeltests/serializers/tests.py b/tests/modeltests/serializers/tests.py index 4a7e0a2..def0254 100644
a b 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 … … from xml.dom import minidom 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 … … class SerializersTransactionTestBase(object): 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 -
new file tests/regressiontests/fixtures_regress/fixtures/forward_ref.json
diff --git a/tests/regressiontests/fixtures_regress/fixtures/forward_ref.json b/tests/regressiontests/fixtures_regress/fixtures/forward_ref.json new file mode 100644 index 0000000..237b076
- + 1 [ 2 { 3 "pk": 1, 4 "model": "fixtures_regress.book", 5 "fields": { 6 "name": "Cryptonomicon", 7 "author": 4 8 } 9 }, 10 { 11 "pk": "4", 12 "model": "fixtures_regress.person", 13 "fields": { 14 "name": "Neal Stephenson" 15 } 16 } 17 ] 18 No newline at end of file -
new file tests/regressiontests/fixtures_regress/fixtures/forward_ref_bad_data.json
diff --git a/tests/regressiontests/fixtures_regress/fixtures/forward_ref_bad_data.json b/tests/regressiontests/fixtures_regress/fixtures/forward_ref_bad_data.json new file mode 100644 index 0000000..3a3fb64
- + 1 [ 2 { 3 "pk": 1, 4 "model": "fixtures_regress.book", 5 "fields": { 6 "name": "Cryptonomicon", 7 "author": 3 8 } 9 }, 10 { 11 "pk": "4", 12 "model": "fixtures_regress.person", 13 "fields": { 14 "name": "Neal Stephenson" 15 } 16 } 17 ] 18 No newline at end of file -
tests/regressiontests/fixtures_regress/tests.py
diff --git a/tests/regressiontests/fixtures_regress/tests.py b/tests/regressiontests/fixtures_regress/tests.py index a565ec9..344bc04 100644
a b class TestFixtures(TestCase): 361 361 """[{"pk": %d, "model": "fixtures_regress.widget", "fields": {"name": "grommet"}}]""" 362 362 % widget.pk 363 363 ) 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 ) 364 393 365 394 366 395 class NaturalKeyFixtureTests(TestCase): -
tests/regressiontests/introspection/tests.py
diff --git a/tests/regressiontests/introspection/tests.py b/tests/regressiontests/introspection/tests.py index 4f5fb09..a7c5bf2 100644
a b class IntrospectionTests(TestCase): 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) -
tests/regressiontests/serializers_regress/tests.py
diff --git a/tests/regressiontests/serializers_regress/tests.py b/tests/regressiontests/serializers_regress/tests.py index cd2ce3c..bb6f598 100644
a b test case that is capable of testing the capabilities of 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 … … 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: