Opened 9 months ago

Closed 9 months ago

#34820 closed Bug (fixed)

Migrations crashes when changing ForeignObject properties.

Reported by: puc_dong Owned by: puc_dong
Component: Database layer (models, ORM) Version: 4.2
Severity: Normal Keywords:
Cc: Triage Stage: Ready for checkin
Has patch: yes Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description (last modified by puc_dong)

Execute migrate to report an error

If the models.ForeignObject attribute is used, changing null or blank to generate a new migration record will result in an error message when executing the migrate again

File "/Users/donghao/test/test_migations/manage.py", line 22, in <module>
    execute_from_command_line(sys.argv)
  File "/Users/donghao/.virtualenvs/simpleui/lib/python3.9/site-packages/django/core/management/__init__.py", line 442, in execute_from_command_line
    utility.execute()
  File "/Users/donghao/.virtualenvs/simpleui/lib/python3.9/site-packages/django/core/management/__init__.py", line 436, in execute
    self.fetch_command(subcommand).run_from_argv(self.argv)
  File "/Users/donghao/.virtualenvs/simpleui/lib/python3.9/site-packages/django/core/management/base.py", line 412, in run_from_argv
    self.execute(*args, **cmd_options)
  File "/Users/donghao/.virtualenvs/simpleui/lib/python3.9/site-packages/django/core/management/base.py", line 458, in execute
    output = self.handle(*args, **options)
  File "/Users/donghao/.virtualenvs/simpleui/lib/python3.9/site-packages/django/core/management/base.py", line 106, in wrapper
    res = handle_func(*args, **kwargs)
  File "/Users/donghao/.virtualenvs/simpleui/lib/python3.9/site-packages/django/core/management/commands/migrate.py", line 356, in handle
    post_migrate_state = executor.migrate(
  File "/Users/donghao/.virtualenvs/simpleui/lib/python3.9/site-packages/django/db/migrations/executor.py", line 135, in migrate
    state = self._migrate_all_forwards(
  File "/Users/donghao/.virtualenvs/simpleui/lib/python3.9/site-packages/django/db/migrations/executor.py", line 167, in _migrate_all_forwards
    state = self.apply_migration(
  File "/Users/donghao/.virtualenvs/simpleui/lib/python3.9/site-packages/django/db/migrations/executor.py", line 252, in apply_migration
    state = migration.apply(state, schema_editor)
  File "/Users/donghao/.virtualenvs/simpleui/lib/python3.9/site-packages/django/db/migrations/migration.py", line 132, in apply
    operation.database_forwards(
  File "/Users/donghao/.virtualenvs/simpleui/lib/python3.9/site-packages/django/db/migrations/operations/fields.py", line 235, in database_forwards
    schema_editor.alter_field(from_model, from_field, to_field)
  File "/Users/donghao/.virtualenvs/simpleui/lib/python3.9/site-packages/django/db/backends/base/schema.py", line 785, in alter_field
    if not self._field_should_be_altered(old_field, new_field):
  File "/Users/donghao/.virtualenvs/simpleui/lib/python3.9/site-packages/django/db/backends/base/schema.py", line 1530, in _field_should_be_altered
    return self.quote_name(old_field.column) != self.quote_name(
  File "/Users/donghao/.virtualenvs/simpleui/lib/python3.9/site-packages/django/db/backends/base/schema.py", line 204, in quote_name
    return self.connection.ops.quote_name(name)
  File "/Users/donghao/.virtualenvs/simpleui/lib/python3.9/site-packages/django/db/backends/mysql/operations.py", line 184, in quote_name
    if name.startswith("`") and name.endswith("`"):
AttributeError: 'NoneType' object has no attribute 'startswith'

models:

# app1:
class Model1(models.Model):
    name = models.CharField(max_length=50)

# app2:
class Model2(models.Model):
    name = models.CharField(max_length=100, db_index=True)
    clu_id = models.BigIntegerField()
    scenes = models.ForeignObject(Model1,
                                  from_fields=['clu_id'],
                                  to_fields=['id'],
                                  null=True,
                                  related_name='model2_config',
                                  on_delete=models.DO_NOTHING)

Successfully executed makemigrations&migrate for the first time. When I add blank=True to the scenes field of Model2, and then execute makemigrations and migrate to report an error
All migration files:
app1.0001_initial.py

class Migration(migrations.Migration):

    initial = True

    dependencies = [
    ]

    operations = [
        migrations.CreateModel(
            name='Model1',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('name', models.CharField(max_length=50)),
            ],
        ),
    ]

app2.0001_initial.py

class Migration(migrations.Migration):

    initial = True

    dependencies = [
        ('app_1', '0001_initial'),
    ]

    operations = [
        migrations.CreateModel(
            name='Model2',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('name', models.CharField(db_index=True, max_length=100)),
                ('clu_id', models.BigIntegerField()),
                ('scenes', models.ForeignObject(from_fields=['clu_id'], null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='model2_config', to='app_1.model1', to_fields=['id'])),
            ],
        ),
    ]

app2.0002_alter_model2_scenes.py

class Migration(migrations.Migration):

    dependencies = [
        ('app_1', '0001_initial'),
        ('app_2', '0001_initial'),
    ]

    operations = [
        migrations.AlterField(
            model_name='model2',
            name='scenes',
            field=models.ForeignObject(blank=True, from_fields=('clu_id',), null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='model2_config', to='app_1.model1', to_fields=('id',)),
        ),
    ]

FIX: https://github.com/django/django/pull/17235

Change History (17)

comment:1 by puc_dong, 9 months ago

Description: modified (diff)

comment:2 by puc_dong, 9 months ago

Description: modified (diff)

comment:3 by Mariusz Felisiak, 9 months ago

Component: Database layer (models, ORM)Migrations
Needs tests: set
Resolution: needsinfo
Status: newclosed

I'm not sure how to you reach a field without a column in migrations. I don't think you've explained the issue in enough detail to confirm a bug in Django, can you provide a sample project or test case that reproduces the issue?

comment:4 by puc_dong, 9 months ago

Component: MigrationsDatabase layer (models, ORM)
Description: modified (diff)
Needs tests: unset

comment:5 by puc_dong, 9 months ago

Resolution: needsinfo
Status: closednew

in reply to:  3 comment:6 by puc_dong, 9 months ago

Replying to Mariusz Felisiak:

I'm not sure how to you reach a field without a column in migrations. I don't think you've explained the issue in enough detail to confirm a bug in Django, can you provide a sample project or test case that reproduces the issue?

I submitted my case code and process

comment:7 by puc_dong, 9 months ago

(simpleui) ☁  test_migations  python manage.py makemigrations 
System check identified some issues:

WARNINGS:
app_1.Model1: (models.W042) Auto-created primary key used when not defining a primary key type, by default 'django.db.models.AutoField'.
        HINT: Configure the DEFAULT_AUTO_FIELD setting or the App1Config.default_auto_field attribute to point to a subclass of AutoField, e.g. 'django.db.models.BigAutoField'.
app_2.Model2: (models.W042) Auto-created primary key used when not defining a primary key type, by default 'django.db.models.AutoField'.
        HINT: Configure the DEFAULT_AUTO_FIELD setting or the App2Config.default_auto_field attribute to point to a subclass of AutoField, e.g. 'django.db.models.BigAutoField'.
Migrations for 'app_1':
  app_1/migrations/0001_initial.py
    - Create model Model1
Migrations for 'app_2':
  app_2/migrations/0001_initial.py
    - Create model Model2
(simpleui) ☁  test_migations  python manage.py migrate        
System check identified some issues:

WARNINGS:
app_1.Model1: (models.W042) Auto-created primary key used when not defining a primary key type, by default 'django.db.models.AutoField'.
        HINT: Configure the DEFAULT_AUTO_FIELD setting or the App1Config.default_auto_field attribute to point to a subclass of AutoField, e.g. 'django.db.models.BigAutoField'.
app_2.Model2: (models.W042) Auto-created primary key used when not defining a primary key type, by default 'django.db.models.AutoField'.
        HINT: Configure the DEFAULT_AUTO_FIELD setting or the App2Config.default_auto_field attribute to point to a subclass of AutoField, e.g. 'django.db.models.BigAutoField'.
Operations to perform:
  Apply all migrations: admin, app_1, app_2, auth, contenttypes, sessions
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying admin.0003_logentry_add_action_flag_choices... OK
  Applying app_1.0001_initial... OK
  Applying app_2.0001_initial... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying auth.0009_alter_user_last_name_max_length... OK
  Applying auth.0010_alter_group_name_max_length... OK
  Applying auth.0011_update_proxy_permissions... OK
  Applying auth.0012_alter_user_first_name_max_length... OK
  Applying sessions.0001_initial... OK

**Add blank=True to the scenes field of Model2**

(simpleui) ☁  test_migations  python manage.py makemigrations 
System check identified some issues:

WARNINGS:
app_1.Model1: (models.W042) Auto-created primary key used when not defining a primary key type, by default 'django.db.models.AutoField'.
        HINT: Configure the DEFAULT_AUTO_FIELD setting or the App1Config.default_auto_field attribute to point to a subclass of AutoField, e.g. 'django.db.models.BigAutoField'.
app_2.Model2: (models.W042) Auto-created primary key used when not defining a primary key type, by default 'django.db.models.AutoField'.
        HINT: Configure the DEFAULT_AUTO_FIELD setting or the App2Config.default_auto_field attribute to point to a subclass of AutoField, e.g. 'django.db.models.BigAutoField'.
Migrations for 'app_2':
  app_2/migrations/0002_alter_model2_scenes.py
    - Alter field scenes on model2

(simpleui) ☁  test_migations  python manage.py migrate        
System check identified some issues:

WARNINGS:
app_1.Model1: (models.W042) Auto-created primary key used when not defining a primary key type, by default 'django.db.models.AutoField'.
        HINT: Configure the DEFAULT_AUTO_FIELD setting or the App1Config.default_auto_field attribute to point to a subclass of AutoField, e.g. 'django.db.models.BigAutoField'.
app_2.Model2: (models.W042) Auto-created primary key used when not defining a primary key type, by default 'django.db.models.AutoField'.
        HINT: Configure the DEFAULT_AUTO_FIELD setting or the App2Config.default_auto_field attribute to point to a subclass of AutoField, e.g. 'django.db.models.BigAutoField'.
Operations to perform:
  Apply all migrations: admin, app_1, app_2, auth, contenttypes, sessions
Running migrations:
  Applying app_2.0002_alter_model2_scenes...Traceback (most recent call last):
  File "/Users/donghao/test/test_migations/manage.py", line 22, in <module>
    execute_from_command_line(sys.argv)
  File "/Users/donghao/.virtualenvs/simpleui/lib/python3.9/site-packages/django/core/management/__init__.py", line 442, in execute_from_command_line
    utility.execute()
  File "/Users/donghao/.virtualenvs/simpleui/lib/python3.9/site-packages/django/core/management/__init__.py", line 436, in execute
    self.fetch_command(subcommand).run_from_argv(self.argv)
  File "/Users/donghao/.virtualenvs/simpleui/lib/python3.9/site-packages/django/core/management/base.py", line 412, in run_from_argv
    self.execute(*args, **cmd_options)
  File "/Users/donghao/.virtualenvs/simpleui/lib/python3.9/site-packages/django/core/management/base.py", line 458, in execute
    output = self.handle(*args, **options)
  File "/Users/donghao/.virtualenvs/simpleui/lib/python3.9/site-packages/django/core/management/base.py", line 106, in wrapper
    res = handle_func(*args, **kwargs)
  File "/Users/donghao/.virtualenvs/simpleui/lib/python3.9/site-packages/django/core/management/commands/migrate.py", line 356, in handle
    post_migrate_state = executor.migrate(
  File "/Users/donghao/.virtualenvs/simpleui/lib/python3.9/site-packages/django/db/migrations/executor.py", line 135, in migrate
    state = self._migrate_all_forwards(
  File "/Users/donghao/.virtualenvs/simpleui/lib/python3.9/site-packages/django/db/migrations/executor.py", line 167, in _migrate_all_forwards
    state = self.apply_migration(
  File "/Users/donghao/.virtualenvs/simpleui/lib/python3.9/site-packages/django/db/migrations/executor.py", line 252, in apply_migration
    state = migration.apply(state, schema_editor)
  File "/Users/donghao/.virtualenvs/simpleui/lib/python3.9/site-packages/django/db/migrations/migration.py", line 132, in apply
    operation.database_forwards(
  File "/Users/donghao/.virtualenvs/simpleui/lib/python3.9/site-packages/django/db/migrations/operations/fields.py", line 235, in database_forwards
    schema_editor.alter_field(from_model, from_field, to_field)
  File "/Users/donghao/.virtualenvs/simpleui/lib/python3.9/site-packages/django/db/backends/base/schema.py", line 785, in alter_field
    if not self._field_should_be_altered(old_field, new_field):
  File "/Users/donghao/.virtualenvs/simpleui/lib/python3.9/site-packages/django/db/backends/base/schema.py", line 1520, in _field_should_be_altered
    return self.quote_name(old_field.column) != self.quote_name(
  File "/Users/donghao/.virtualenvs/simpleui/lib/python3.9/site-packages/django/db/backends/base/schema.py", line 204, in quote_name
    return self.connection.ops.quote_name(name)
  File "/Users/donghao/.virtualenvs/simpleui/lib/python3.9/site-packages/django/db/backends/mysql/operations.py", line 184, in quote_name
    if name.startswith("`") and name.endswith("`"):
AttributeError: 'NoneType' object has no attribute 'startswith'

comment:8 by Mariusz Felisiak, 9 months ago

Resolution: invalid
Status: newclosed

ForeignObject is an abstraction level, not a proper model field. You should use models.ForeignKey. If you're having trouble understanding how Django works, see TicketClosingReasons/UseSupportChannels for ways to get help.

in reply to:  8 ; comment:9 by puc_dong, 9 months ago

Replying to Mariusz Felisiak:

ForeignObject is an abstraction level, not a proper model field. You should use models.ForeignKey. If you're having trouble understanding how Django works, see TicketClosingReasons/UseSupportChannels for ways to get help.

We do not need to create a foreign key, we have used ForeignObject and currently only need to perform queries. Should this field not be generated in the migration record for the first time?

in reply to:  9 comment:10 by Mariusz Felisiak, 9 months ago

Replying to puc_dong:

Replying to Mariusz Felisiak:

ForeignObject is an abstraction level, not a proper model field. You should use models.ForeignKey. If you're having trouble understanding how Django works, see TicketClosingReasons/UseSupportChannels for ways to get help.

We do not need to create a foreign key, we have used ForeignObject and currently only need to perform queries. Should this field not be generated in the migration record for the first time?

It seems that you have a niche use case. Trac is not a support channel, you can try to discuss this with folks on the Django Forum.

comment:11 by Mariusz Felisiak, 9 months ago

Needs tests: set
Patch needs improvement: set
Resolution: invalid
Status: closednew
Summary: Change the properties of ForeignObject object, such as blank, null, and execute migrate to report an errorMigrations crashes when changing ForeignObject properties.
Triage Stage: UnreviewedAccepted

After reconsideration I think we should fix this (check out similar report #25128).

comment:12 by Mariusz Felisiak, 9 months ago

Has patch: unset
Needs tests: unset
Patch needs improvement: unset

comment:13 by Mariusz Felisiak, 9 months ago

A regression test:

  • tests/migrations/test_operations.py

    diff --git a/tests/migrations/test_operations.py b/tests/migrations/test_operations.py
    index d4fc3e855a..6366ccd667 100644
    a b class OperationTests(OperationTestBase):  
    23062306                operation.database_forwards(app_label, editor, new_state, project_state)
    23072307        self.assertColumnExists(rider_table, "pony_id")
    23082308
     2309    def test_alter_field_foreignobject_noop(self):
     2310        app_label = "test_alflfo_noop"
     2311        project_state = self.set_up_test_model(app_label)
     2312        project_state = self.apply_operations(
     2313            app_label,
     2314            project_state,
     2315            [
     2316                migrations.CreateModel(
     2317                    "Rider",
     2318                    fields=[
     2319                        ("pony_id", models.IntegerField()),
     2320                        (
     2321                            "pony",
     2322                            models.ForeignObject(
     2323                                f"{app_label}.Pony",
     2324                                models.CASCADE,
     2325                                from_fields=("pony_id",),
     2326                                to_fields=("id",),
     2327                            ),
     2328                        ),
     2329                    ],
     2330                ),
     2331            ],
     2332        )
     2333        operation = migrations.AlterField(
     2334            "Rider",
     2335            "pony",
     2336            models.ForeignObject(
     2337                f"{app_label}.Pony",
     2338                models.CASCADE,
     2339                from_fields=("pony_id",),
     2340                to_fields=("id",),
     2341                null=True,
     2342            ),
     2343        )
     2344        new_state = project_state.clone()
     2345        operation.state_forwards(app_label, new_state)
     2346        with self.assertNumQueries(0), connection.schema_editor() as editor:
     2347            operation.database_forwards(app_label, editor, project_state, new_state)
     2348
    23092349    @skipUnlessDBFeature("supports_comments")
    23102350    def test_alter_model_table_comment(self):
    23112351        app_label = "test_almotaco"

in reply to:  13 comment:14 by puc_dong, 9 months ago

Replying to Mariusz Felisiak:

A regression test:

  • tests/migrations/test_operations.py

    diff --git a/tests/migrations/test_operations.py b/tests/migrations/test_operations.py
    index d4fc3e855a..6366ccd667 100644
    a b class OperationTests(OperationTestBase):  
    23062306                operation.database_forwards(app_label, editor, new_state, project_state)
    23072307        self.assertColumnExists(rider_table, "pony_id")
    23082308
     2309    def test_alter_field_foreignobject_noop(self):
     2310        app_label = "test_alflfo_noop"
     2311        project_state = self.set_up_test_model(app_label)
     2312        project_state = self.apply_operations(
     2313            app_label,
     2314            project_state,
     2315            [
     2316                migrations.CreateModel(
     2317                    "Rider",
     2318                    fields=[
     2319                        ("pony_id", models.IntegerField()),
     2320                        (
     2321                            "pony",
     2322                            models.ForeignObject(
     2323                                f"{app_label}.Pony",
     2324                                models.CASCADE,
     2325                                from_fields=("pony_id",),
     2326                                to_fields=("id",),
     2327                            ),
     2328                        ),
     2329                    ],
     2330                ),
     2331            ],
     2332        )
     2333        operation = migrations.AlterField(
     2334            "Rider",
     2335            "pony",
     2336            models.ForeignObject(
     2337                f"{app_label}.Pony",
     2338                models.CASCADE,
     2339                from_fields=("pony_id",),
     2340                to_fields=("id",),
     2341                null=True,
     2342            ),
     2343        )
     2344        new_state = project_state.clone()
     2345        operation.state_forwards(app_label, new_state)
     2346        with self.assertNumQueries(0), connection.schema_editor() as editor:
     2347            operation.database_forwards(app_label, editor, project_state, new_state)
     2348
    23092349    @skipUnlessDBFeature("supports_comments")
    23102350    def test_alter_model_table_comment(self):
    23112351        app_label = "test_almotaco"

I realized that there will be a default exclude behavior, and there are 6 queries that will be executed in database setup, so I modified the use case, and the 6 queries were not written directly.
This is my modification:https://github.com/django/django/pull/17240

comment:15 by Jacob Walls, 9 months ago

Has patch: set

comment:16 by Mariusz Felisiak, 9 months ago

Owner: changed from nobody to puc_dong
Status: newassigned
Triage Stage: AcceptedReady for checkin

comment:17 by Mariusz Felisiak <felisiak.mariusz@…>, 9 months ago

Resolution: fixed
Status: assignedclosed

In 71820c9f:

Fixed #34820 -- Fixed migrations crash when changing a ForeignObject field.

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