Opened 3 months ago

Last modified 2 days ago

#35813 assigned Bug

Makemigrations not properly tracking changes to unmanaged models

Reported by: Hanny G Owned by: Hanny G
Component: Migrations Version: 5.1
Severity: Normal Keywords: makemigrations, unmanaged
Cc: Hanny G, Simon Charette, Imran Ahmed Khan 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 Hanny G)

I believe there is a bug with the makemigrations command.

The Issue/Bug: Changes to unmanaged models are not being recognized by makemigrations. If those same changes are applied to a managed model, makemigrations recognizes the changes.

The makemigrations command tracks the Creation of an unmanaged model, the Deletion of an unmanaged model, but does not track changes to the attributes/fields of an unmanaged model (see example below for more depth).

Concerns/Discussion: If you create an unmanaged model, make the initial migration and then have to change a field on it, makemigrations will not detect the change you made. At that point, your migrations (the 'historical reference') are out of sync with the current reality of the model.

I have an if this is unclear.
For this example let's assume we have two models: UnmanagedThing and ManagedThing (unmanaged and managed, respectively)

class UnmanagedThing(models.Model):
    """
    This model represents an unmanaged Thing.
    """

    id = models.IntegerField(
        db_column="thingID", primary_key=True
    )
    thing_number = models.CharField(max_length=16, db_column="thingNumber")
    thing_name = models.CharField(max_length=50, db_column="thingName")
    grade_level = models.CharField(max_length=2, db_column="gradeLevel")

    class Meta:
        """Meta options for the UnmanagedThing model."""

        verbose_name = "Unmanaged Thing"
        verbose_name_plural = "Unmanaged Things"
        managed = False
        db_table = "some_table_name"

    def __str__(self) -> str:
        """Return the course name."""
        return f"{self.thing_name}"

class ManagedThing(models.Model):
    """
    This model represents an unmanaged Thing.
    """

    id = models.IntegerField(
        db_column="thingID", primary_key=True
    )
    thing_number = models.CharField(max_length=16, db_column="thingNumber")
    thing_name = models.CharField(max_length=50, db_column="thingName")
    grade_level = models.CharField(max_length=2, db_column="gradeLevel")

    class Meta:
        """Meta options for the UnmanagedThing model."""

        verbose_name = "Unmanaged Thing"
        verbose_name_plural = "Unmanaged Things"

    def __str__(self) -> str:
        """Return the course name."""
        return f"{self.thing_name}"

If I run makemigrations I get the expected 0001... migration file and I see both models created with their respective fields/options.

If I change thing_name to be an IntegerField on the managed model and run makemigrations, I see a 0002... migration file created with the expected 'AlterField' inside of it.

If I change thing_name to be an IntegerField on the unmanaged model and run makemigrations, I see "No changes detected" and nothing is created. However, now there is a disconnect between my model and what is recorded in migrations.

Everything else works similarly between the two; if I delete either one, it's recorded in a migration file. If I change the unmanaged model to 'managed', it creates a migration file for that (and subsequently begins tracking changes as expected with makemigrations)

Per the django docs: " You should think of migrations as a version control system for your database schema. makemigrations is responsible for packaging up your model changes into individual migration files - analogous to commits"; it's not behaving that way currently, so I believe this to be a valid bug worth fixing if the docs

Per the django forums: "This lack of proper tracking might explain surface bugs like: https://code.djangoproject.com/ticket/27319"

Additional Suggestions: I think an option to exclude specific models or just exclude all unmanaged models would be something worth adding in the event we did not want to track historical changes to an unmanaged model or have it recorded in migrations.

Change History (8)

comment:1 by Hanny G, 3 months ago

Description: modified (diff)

comment:2 by Hanny G, 3 months ago

Has patch: set
Owner: set to Hanny G
Status: newassigned

comment:3 by Sarah Boyce, 3 months ago

Cc: Simon Charette added
Triage Stage: UnreviewedAccepted

comment:4 by Imran Ahmed Khan, 3 months ago

The purpose of Django's makemigrations command is to identify changes in managed models, or models in which Django has authority over the database table structure.
Makemigrations does not completely monitor unmanaged models (where managed = False in the model's Meta class).
In particular, the makemigrations command does not detect changes to the fields of unmanaged models, even though the creation or deletion of an unmanaged model is tracked.
A mismatch between your code and the database structure may result from this behavior, especially if the model is altered after the initial transfer.

Consider you have two models in Django: one is unmanaged and the other is managed.

class UnmanagedThing(models.Model):
    id = models.IntegerField(db_column="thingID", primary_key=True)
    thing_number = models.CharField(max_length=16, db_column="thingNumber")
    thing_name = models.CharField(max_length=50, db_column="thingName")
    grade_level = models.CharField(max_length=2, db_column="gradeLevel")

    class Meta:
        managed = False  # This tells Django not to manage the table's structure.
        db_table = "some_table_name"


class ManagedThing(models.Model):
    id = models.IntegerField(db_column="thingID", primary_key=True)
    thing_number = models.CharField(max_length=16, db_column="thingNumber")
    thing_name = models.CharField(max_length=50, db_column="thingName")
    grade_level = models.CharField(max_length=2, db_column="gradeLevel")

    class Meta:
        managed = True  # This tells Django to manage the table's structure.

Anticipated Conduct: First Migration: Django creates a migration file that records the construction of the managed and unmanaged models when you execute makemigrations for the first time.
Modifications to the Managed Model: Django recognizes when you change the thing_name in the managed model from a CharField to an IntegerField and generates a migration that takes into account the field change.
Changes in the Unmanaged Model: Django does not detect any changes when makemigrations are executed if you change thing_name in the unmanaged model from a CharField to an IntegerField.

Last edited 2 days ago by Tim Graham (previous) (diff)

comment:5 by Imran Ahmed Khan, 3 months ago

Cc: Imran Ahmed Khan added

pls check code

comment:6 by Simon Charette, 2 months ago

The purpose of Django's makemigrations command is to identify changes in managed models, or models in which Django has authority over the database table structure.

I think you're mistaken, the migration framework has always been tracking changes to unmanaged models for two reasons

  1. Support managed models referencing un-managed ones
  2. Support data migrations involving un-managed models

The report here relates to 2. which I think it reasonable to assume that if the framework tracks additions and removal it should also do the same with alterations.

Last edited 2 months ago by Simon Charette (previous) (diff)

comment:7 by Hanny G, 2 days ago

Just curious - what are the remaining steps for this ticket to be looked at and the PR to be accepted?

comment:8 by Simon Charette, 2 days ago

The patch is in the review queue so it will eventually be looked at. The upcoming release of 5.2 has kept everyone busy which explains why no one had a chance to look at your patch yet.

One thing that might be worth doing in the mean time is to add a release note mentioning that makemigrations will now appropriately track any change to managed models which might cause new changes to be detected on upgrade.

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