Opened 10 years ago
Closed 10 years ago
#23979 closed Bug (worksforme)
Multi-db test fails to run because of fixture containing M2M field (dumped with --natural-foreign)
Reported by: | Edwin | Owned by: | nobody |
---|---|---|---|
Component: | Core (Serialization) | Version: | 1.7 |
Severity: | Normal | Keywords: | multi-db, m2m, fixture, natural key |
Cc: | Triage Stage: | Unreviewed | |
Has patch: | no | Needs documentation: | no |
Needs tests: | no | Patch needs improvement: | no |
Easy pickings: | no | UI/UX: | no |
Description (last modified by )
I noticed this issue in Django 1.5.10 but I've managed to replicate it in Django 1.7.1 as well.
I have a multi-db configuration but when I run a test case that uses a fixture containing many-to-many field (dumped using --natural-foreign option), it throws an error because it's trying to query a table in the secondary DB that only exists in the default DB when installing the fixture.
Looking at this code, it looks like Django tries install the fixture for all configured DB:
Line 899 (django/test/testcases.py):
for db_name in self._databases_names(include_mirrors=False): if self.fixtures: try: call_command('loaddata', *self.fixtures, **{ 'verbosity': 0, 'commit': False, 'database': db_name, 'skip_checks': True, }) except Exception: self._fixture_teardown() raise
...which is fine, but then here during serialization, it's making a query using the requested db without checking with "allow_migrate" first:
Line 112: django/core/serializers/python.py
# Handle M2M relations if field.rel and isinstance(field.rel, models.ManyToManyRel): if hasattr(field.rel.to._default_manager, 'get_by_natural_key'): def m2m_convert(value): if hasattr(value, '__iter__') and not isinstance(value, six.text_type): return field.rel.to._default_manager.db_manager(db).get_by_natural_key(*value).pk else: return smart_text(field.rel.to._meta.pk.to_python(value)) else: m2m_convert = lambda v: smart_text(field.rel.to._meta.pk.to_python(v)) m2m_data[field.name] = [m2m_convert(pk) for pk in field_value]
The line field.rel.to._default_manager.db_manager(db).get_by_natural_key(*value).pk
doesn't check if the related model actually exists (or allowed) in db
, causing a DeserializationError when fixture is being installed during unit test. In my example, I have a model that has a Many-to-Many reference to "Permission" table. I serialize this model with "dumpdata" command using "--natural-foreign" option.
However, when I run the test, the error I got was:
(django1.7)$ ./manage.py test Creating test database for alias 'default'... Creating test database for alias 'misc'... E ====================================================================== ERROR: test_something (book.tests.MyTest) ---------------------------------------------------------------------- Traceback (most recent call last): File "/home/ejaury/venvs/django1.7/local/lib/python2.7/site-packages/django/test/testcases.py", line 182, in __call__ self._pre_setup() File "/home/ejaury/venvs/django1.7/local/lib/python2.7/site-packages/django/test/testcases.py", line 754, in _pre_setup self._fixture_setup() File "/home/ejaury/venvs/django1.7/local/lib/python2.7/site-packages/django/test/testcases.py", line 907, in _fixture_setup 'skip_checks': True, File "/home/ejaury/venvs/django1.7/local/lib/python2.7/site-packages/django/core/management/__init__.py", line 115, in call_command return klass.execute(*args, **defaults) File "/home/ejaury/venvs/django1.7/local/lib/python2.7/site-packages/django/core/management/base.py", line 338, in execute output = self.handle(*args, **options) File "/home/ejaury/venvs/django1.7/local/lib/python2.7/site-packages/django/core/management/commands/loaddata.py", line 61, in handle self.loaddata(fixture_labels) File "/home/ejaury/venvs/django1.7/local/lib/python2.7/site-packages/django/core/management/commands/loaddata.py", line 91, in loaddata self.load_label(fixture_label) File "/home/ejaury/venvs/django1.7/local/lib/python2.7/site-packages/django/core/management/commands/loaddata.py", line 142, in load_label for obj in objects: File "/home/ejaury/venvs/django1.7/local/lib/python2.7/site-packages/django/core/serializers/json.py", line 81, in Deserializer six.reraise(DeserializationError, DeserializationError(e), sys.exc_info()[2]) File "/home/ejaury/venvs/django1.7/local/lib/python2.7/site-packages/django/core/serializers/json.py", line 75, in Deserializer for obj in PythonDeserializer(objects, **options): File "/home/ejaury/venvs/django1.7/local/lib/python2.7/site-packages/django/core/serializers/python.py", line 122, in Deserializer m2m_data[field.name] = [m2m_convert(pk) for pk in field_value] File "/home/ejaury/venvs/django1.7/local/lib/python2.7/site-packages/django/core/serializers/python.py", line 117, in m2m_convert return field.rel.to._default_manager.db_manager(db).get_by_natural_key(*value).pk File "/home/ejaury/venvs/django1.7/local/lib/python2.7/site-packages/django/contrib/auth/models.py", line 35, in get_by_natural_key model), File "/home/ejaury/venvs/django1.7/local/lib/python2.7/site-packages/django/db/models/manager.py", line 92, in manager_method return getattr(self.get_queryset(), name)(*args, **kwargs) File "/home/ejaury/venvs/django1.7/local/lib/python2.7/site-packages/django/db/models/query.py", line 351, in get num = len(clone) File "/home/ejaury/venvs/django1.7/local/lib/python2.7/site-packages/django/db/models/query.py", line 122, in __len__ self._fetch_all() File "/home/ejaury/venvs/django1.7/local/lib/python2.7/site-packages/django/db/models/query.py", line 966, in _fetch_all self._result_cache = list(self.iterator()) File "/home/ejaury/venvs/django1.7/local/lib/python2.7/site-packages/django/db/models/query.py", line 265, in iterator for row in compiler.results_iter(): File "/home/ejaury/venvs/django1.7/local/lib/python2.7/site-packages/django/db/models/sql/compiler.py", line 700, in results_iter for rows in self.execute_sql(MULTI): File "/home/ejaury/venvs/django1.7/local/lib/python2.7/site-packages/django/db/models/sql/compiler.py", line 786, in execute_sql cursor.execute(sql, params) File "/home/ejaury/venvs/django1.7/local/lib/python2.7/site-packages/django/db/backends/utils.py", line 65, in execute return self.cursor.execute(sql, params) File "/home/ejaury/venvs/django1.7/local/lib/python2.7/site-packages/django/db/utils.py", line 94, in __exit__ six.reraise(dj_exc_type, dj_exc_value, traceback) File "/home/ejaury/venvs/django1.7/local/lib/python2.7/site-packages/django/db/backends/utils.py", line 65, in execute return self.cursor.execute(sql, params) File "/home/ejaury/venvs/django1.7/local/lib/python2.7/site-packages/django/db/backends/sqlite3/base.py", line 485, in execute return Database.Cursor.execute(self, query, params) DeserializationError: Problem installing fixture '/scratch/django/1.7/mybook/book/fixtures/permset_test.json': no such table: auth_permission
Here's my setup (note that I have simplified this example from my actual code, but it's still reproducible with this simplified code):
router.py
class MyRouter(object): def db_for_read(self, model, **hints): return None def db_for_write(self, model, **hints): return None def allow_relation(self, obj1, obj2, **hints): return True def allow_migrate(self, db, model): return (db == 'default')
database settings
DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), }, 'misc': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': os.path.join(BASE_DIR, 'db_misc.sqlite3'), }, } DATABASE_ROUTERS = ['router.MyRouter']
models.py
from django.db import models from django.contrib.auth.models import Permission class PermSet(models.Model): name = models.CharField(max_length=100) permissions = models.ManyToManyField(Permission) class AdminProfile(models.Model): name = models.CharField(max_length=100) perm_sets = models.ManyToManyField(PermSet)
tests.py
from django.test import TestCase from book.models import PermSet class MyTest(TestCase): fixtures = ['permset_test.json'] multi_db = True def test_something(self): pass
This is my fixture:
permset_test.json
[ { "fields": { "name": "test", "permissions": [ [ "change_logentry", "admin", "logentry" ], [ "delete_logentry", "admin", "logentry" ] ] }, "model": "book.permset", "pk": 1 } ]
...which I dumped using this command:
(django1.7)$ ./manage.py dumpdata book.PermSet --indent=4 --natural-foreign > permset_test.json
Change History (7)
comment:1 by , 10 years ago
Description: | modified (diff) |
---|
comment:2 by , 10 years ago
comment:3 by , 10 years ago
I have tried forcing db_for_write()
and db_for_read()
to return 'default' database, but that doesn't work either. If you look at that the particular code I highlighted above, self._databases_names(include_mirrors=False)
returns all databases in my case, given that my test case is configured with multi_db = True
. Regardless of db_for_read
and db_for_write
, fixture is installed for both databases.
comment:5 by , 10 years ago
Changing it to permset_test.default.json
doesn't work. It's still trying to install the fixture in the secondary db.
comment:6 by , 10 years ago
The issue seems to be fixed if you rename the fixtures file to permset_test.default.json
but keep fixtures = ['permset_test.json']
in the TestCase
. self.assertEqual(PermSet.objects.count(), 1)
passes in the test. Does this resolve your issue?
What happens if you add
(db == 'default')
todb_for_write()
anddb_for_read()
as well?