diff --git a/django/db/migrations/state.py b/django/db/migrations/state.py index 9150705ffc..0be5ed41a2 100644 --- a/django/db/migrations/state.py +++ b/django/db/migrations/state.py @@ -312,12 +312,23 @@ class ModelState(object): # Sanity-check that fields is NOT a dict. It must be ordered. if isinstance(self.fields, dict): raise ValueError("ModelState.fields cannot be a dict - it must be a list of 2-tuples.") - # Sanity-check that fields are NOT already bound to a model. for name, field in fields: + # Sanity-check that fields are NOT already bound to a model. if hasattr(field, 'model'): raise ValueError( 'ModelState.fields cannot be bound to a model - "%s" is.' % name ) + # Sanity-check that relation fields are NOT referring to a model class. + if field.is_relation and hasattr(field.related_model, '_meta'): + raise ValueError( + 'ModelState.fields cannot refer to a model class - "%s.to" does. ' + 'Use a string reference instead.' % name + ) + if field.many_to_many and hasattr(field.remote_field.through, '_meta'): + raise ValueError( + 'ModelState.fields cannot refer to a model class - "%s.through" does. ' + 'Use a string reference instead.' % name + ) @cached_property def name_lower(self): @@ -502,10 +513,10 @@ class ModelState(object): return self.__class__( app_label=self.app_label, name=self.name, - fields=list(self.construct_fields()), + fields=list(self.fields), options=dict(self.options), bases=self.bases, - managers=list(self.construct_managers()), + managers=list(self.managers), ) def render(self, apps): diff --git a/docs/ref/migration-operations.txt b/docs/ref/migration-operations.txt index a1d5d5c454..991cc6f33b 100644 --- a/docs/ref/migration-operations.txt +++ b/docs/ref/migration-operations.txt @@ -402,7 +402,8 @@ You can take this template and work from it, though we suggest looking at the built-in Django operations in ``django.db.migrations.operations`` - they're easy to read and cover a lot of the example usage of semi-internal aspects of the migration framework like ``ProjectState`` and the patterns used to get -historical models. +historical models, as well as ``ModelState`` and the patterns used to mutate +historical models in ``state_forwards()``. Some things to note: @@ -421,6 +422,16 @@ Some things to note: operations; this is part of the autodetection code and does not matter for custom operations. +.. warning:: + + For performance reasons, the :class:`~django.db.models.Field` instances in + ``ModelState.fields`` are reused across migrations. You must never change + the attributes on these instances. If you need to mutate a field in + ``state_forwards()``, you must remove the old instance from + ``ModelState.fields`` and add a new instance in its place. The same is true + for the :class:`~django.db.models.Manager` instances in + ``ModelState.managers``. + As a simple example, let's make an operation that loads PostgreSQL extensions (which contain some of PostgreSQL's more exciting features). It's simple enough; there's no model state changes, and all it does is run one command:: diff --git a/tests/migrations/test_state.py b/tests/migrations/test_state.py index 551ca11459..6ff9ba003c 100644 --- a/tests/migrations/test_state.py +++ b/tests/migrations/test_state.py @@ -10,6 +10,7 @@ from django.test import SimpleTestCase, TestCase, override_settings from .models import ( FoodManager, FoodQuerySet, ModelWithCustomBase, NoMigrationFoodManager, + UnicodeModel, ) @@ -695,6 +696,21 @@ class ModelStateTests(TestCase): 'ModelState.fields cannot be bound to a model - "field" is.'): ModelState('app', 'Model', [('field', field)]) + def test_sanity_check_to(self): + field = models.ForeignKey(UnicodeModel) + with self.assertRaisesMessage(ValueError, + 'ModelState.fields cannot refer to a model class - "field.to" does. ' + 'Use a string reference instead.'): + ModelState('app', 'Model', [('field', field)]) + + def test_sanity_check_through(self): + field = models.ManyToManyField('UnicodeModel') + field.remote_field.through = UnicodeModel + with self.assertRaisesMessage(ValueError, + 'ModelState.fields cannot refer to a model class - "field.through" does. ' + 'Use a string reference instead.'): + ModelState('app', 'Model', [('field', field)]) + def test_fields_immutability(self): """ Tests that rendering a model state doesn't alter its internal fields.