Opened 5 years ago

Closed 5 years ago

#29897 closed Bug (fixed)

Initial migration fails when referencing a custom user model from a sequence of at least two concrete ancestor models

Reported by: Steven Ganz Owned by: nobody
Component: Migrations Version: 2.1
Severity: Normal Keywords: "custom user model" "abstract model" "foreign key"
Cc: Triage Stage: Accepted
Has patch: yes Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description (last modified by Steven Ganz)

The following model fails on initial migration.

from django.db import models
from django.contrib.auth.models import (AbstractBaseUser)

class A(models.Model):
    createdByUser = models.ForeignKey('User', related_name='creations+', verbose_name='creatorUser', blank=True, null=True, editable=False, on_delete=models.PROTECT)

class B(A):
    pass

class User(AbstractBaseUser, B):
    email = models.EmailField('email address', max_length=256, unique=True, db_index=True)
    USERNAME_FIELD = 'email'

    @property
    def username(self):
         return self.email

settings.py contains:

AUTH_USER_MODEL = 'core.User'

Output of running the migrations:

  Applying core.0001_initial...Traceback (most recent call last):
  File "./manage.py", line 13, in <module>
    execute_from_command_line(sys.argv)
  File ".../lib/python3.6/site-packages/django/core/management/__init__.py", line 364, in execute_from_command_line
    utility.execute()
  File ".../lib/python3.6/site-packages/django/core/management/__init__.py", line 356, in execute
    self.fetch_command(subcommand).run_from_argv(self.argv)
  File ".../lib/python3.6/site-packages/django/core/management/base.py", line 283, in run_from_argv
    self.execute(*args, **cmd_options)
  File ".../lib/python3.6/site-packages/django/core/management/base.py", line 330, in execute
    output = self.handle(*args, **options)
  File ".../lib/python3.6/site-packages/django/core/management/commands/migrate.py", line 204, in handle
    fake_initial=fake_initial,
  File ".../lib/python3.6/site-packages/django/db/migrations/executor.py", line 115, in migrate
    state = self._migrate_all_forwards(state, plan, full_plan, fake=fake, fake_initial=fake_initial)
  File ".../lib/python3.6/site-packages/django/db/migrations/executor.py", line 145, in _migrate_all_forwards
    state = self.apply_migration(state, migration, fake=fake, fake_initial=fake_initial)
  File ".../lib/python3.6/site-packages/django/db/migrations/executor.py", line 244, in apply_migration
    state = migration.apply(state, schema_editor)
  File ".../lib/python3.6/site-packages/django/db/migrations/migration.py", line 129, in apply
    operation.database_forwards(self.app_label, schema_editor, old_state, project_state)
  File ".../lib/python3.6/site-packages/django/db/migrations/operations/fields.py", line 88, in database_forwards
    field,
  File ".../lib/python3.6/site-packages/django/db/backends/base/schema.py", line 431, in add_field
    definition, params = self.column_sql(model, field, include_default=True)
  File ".../lib/python3.6/site-packages/django/db/backends/base/schema.py", line 160, in column_sql
    db_params = field.db_parameters(connection=self.connection)
  File ".../lib/python3.6/site-packages/django/db/models/fields/related.py", line 994, in db_parameters
    return {"type": self.db_type(connection), "check": self.db_check(connection)}
  File ".../lib/python3.6/site-packages/django/db/models/fields/related.py", line 991, in db_type
    return self.target_field.rel_db_type(connection=connection)
  File ".../lib/python3.6/site-packages/django/db/models/fields/related.py", line 909, in target_field
    return self.foreign_related_fields[0]
  File ".../lib/python3.6/site-packages/django/db/models/fields/related.py", line 653, in foreign_related_fields
    return tuple(rhs_field for lhs_field, rhs_field in self.related_fields if rhs_field)
  File ".../lib/python3.6/site-packages/django/db/models/fields/related.py", line 640, in related_fields
    self._related_fields = self.resolve_related_fields()
  File ".../lib/python3.6/site-packages/django/db/models/fields/related.py", line 625, in resolve_related_fields
    raise ValueError('Related model %r cannot be resolved' % self.remote_field.model)
ValueError: Related model 'core.User' cannot be resolved

Notably, this works fine with any of the following modifications:

  • model B is removed, closing the chain
  • createdByUser is moved to B or removed altogether
  • either A or B is made abstract
  • the referenced model is not the custom user model

This is not just a matter of preference for concrete classes, as only a concrete class can be referenced through a foreign key.

Change History (8)

comment:1 by Simon Charette, 5 years ago

Hello there,

Did you reproduce against Django 2.1?

Could you provide the content of your initial migration as well?

comment:2 by Simon Charette, 5 years ago

Severity: Release blockerNormal
Triage Stage: UnreviewedAccepted

From my local testing this has been broken since at least Django 1.11 but the makemigrations's behavior changed in the current master as the optimizer got more aggressive and assumes operation to be appropriately ordered which is not the case here.

Migrations generated on Django 1.11, 2.0, and 2.1

Migrations for 'core':
  core/migrations/0001_initial.py
    - Create model A
    - Create model B
    - Add field createdByUser to a
    - Create model User

Migration generated on the current master

Migrations for 'core':
  core/migrations/0001_initial.py
    - Create model A
    - Create model B
    - Create model User

An interesting thing here is that everything works fine on the current master if AUTH_USER_MODEL is not set to core.User. Given the optimizer is completely swappable model agnostic the issue most probably lies in the auto-detector swappable model dependency resolution.

To make it clear the expected operation's order here is the same as if the model wasn't swappable

Migrations for 'core':
  core/migrations/0001_initial.py
    - Create model A
    - Create model B
    - Create model User
    - Add field createdByUser to a

Downgrading to non-release blocker as this is not a regression and can be easily worked around by either generating the migration without AUTH_USER_MODEL pointed at core.User (and then adjusting it) or by manually altering the migration to move the AddField(ForeignKey(User)) operation after the CreateModel(User) one.

comment:3 by Steven Ganz, 5 years ago

Description: modified (diff)

I accidentally reset your changes to several fields -- please check that I've restored them appropriately.

This was for an initial migration. Like you, I reproduced it on 1.11 & 2.1.

Your first work-around didn't work for me (perhaps because my actual models are much more complex), but the second (temporarily updating the custom user model) does. Thanks, and let me know if you need any more information.

comment:4 by Simon Charette, 5 years ago

Has patch: set

comment:5 by Simon Charette, 5 years ago

Steven, could you confirm https://github.com/django/django/pull/10577 fixes your issue.

comment:6 by Steven Ganz, 5 years ago

Yes, that at least gets the migrations to run. Thanks for the quick turn-around!

comment:7 by Tim Graham <timograham@…>, 5 years ago

In 82353ef:

Refs #29897 -- Moved autodetector swappable dependency resolution to a method.

comment:8 by Tim Graham <timograham@…>, 5 years ago

Resolution: fixed
Status: newclosed

In d8e03fde:

Fixed #29897 -- Fixed autodetector's swappable MTI dependency resolution.

Thanks Steven Ganz for the detailed report.

Note: See TracTickets for help on using tickets.
Back to Top