Commit 21e21c7b authored by Patryk Zawadzki's avatar Patryk Zawadzki Committed by Tim Graham
Browse files

Fixed #23844 -- Used topological sort for migration operation dependency resolution.

This removes the concept of equality between operations to guarantee
compatilibity with Python 3.

Python 3 requires equality to result in identical object hashes. It's
impossible to implement a unique hash that preserves equality as
operations such as field creation depend on being able to accept
arbitrary dicts that cannot be hashed reliably.

Thanks Klaas van Schelven for the original patch in
13d613f8.
parent 53908c1f
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -378,6 +378,7 @@ answer newbie questions, and generally made Django that much better:
    Kevin McConnell <kevin.mcconnell@gmail.com>
    Kieran Holland <http://www.kieranholland.com>
    kilian <kilian.cavalotti@lip6.fr>
    Klaas van Schelven <klaas@vanschelven.com>
    knox <christobzr@gmail.com>
    konrad@gwu.edu
    Kowito Charoenratchatabhan <kowito@felspar.com>
+14 −22
Original line number Diff line number Diff line
@@ -14,6 +14,8 @@ from django.db.migrations.questioner import MigrationQuestioner
from django.db.migrations.optimizer import MigrationOptimizer
from django.db.migrations.operations.models import AlterModelOptions

from .topological_sort import stable_topological_sort


class MigrationAutodetector(object):
    """
@@ -191,28 +193,18 @@ class MigrationAutodetector(object):
        # isn't bad, but we need to pull a few things around so FKs work nicely
        # inside the same app
        for app_label, ops in sorted(self.generated_operations.items()):
            for i in range(10000):
                found = False
                for i, op in enumerate(ops):

            # construct a dependency graph for intra-app dependencies
            dependency_graph = {op: set() for op in ops}
            for op in ops:
                for dep in op._auto_deps:
                    if dep[0] == app_label:
                            # Alright, there's a dependency on the same app.
                            for j, op2 in enumerate(ops):
                                if j > i and self.check_dependency(op2, dep):
                                    # shift the operation from position i after
                                    # the operation at position j
                                    ops = ops[:i] + ops[i + 1:j + 1] + [op] + ops[j + 1:]
                                    found = True
                                    break
                        if found:
                            break
                    if found:
                        break
                if not found:
                    break
            else:
                raise ValueError("Infinite loop caught in operation dependency resolution")
            self.generated_operations[app_label] = ops
                        for op2 in ops:
                            if self.check_dependency(op2, dep):
                                dependency_graph[op].add(op2)

            # we use a stable sort for deterministic tests & general behavior
            self.generated_operations[app_label] = stable_topological_sort(ops, dependency_graph)

        # Now, we need to chop the lists of operations up into migrations with
        # dependencies on each other.
+0 −6
Original line number Diff line number Diff line
@@ -116,9 +116,3 @@ class Operation(object):
            ", ".join(map(repr, self._constructor_args[0])),
            ",".join(" %s=%r" % x for x in self._constructor_args[1].items()),
        )

    def __eq__(self, other):
        return (self.__class__ == other.__class__) and (self.deconstruct() == other.deconstruct())

    def __ne__(self, other):
        return not (self == other)
+34 −16
Original line number Diff line number Diff line
@@ -16,6 +16,16 @@ class AddField(Operation):
        self.field = field
        self.preserve_default = preserve_default

    def deconstruct(self):
        kwargs = {}
        if self.preserve_default is not True:
            kwargs['preserve_default'] = self.preserve_default
        return (
            self.__class__.__name__,
            [self.model_name, self.name, self.field],
            kwargs
        )

    def state_forwards(self, app_label, state):
        # If preserve default is off, don't use the default for future state
        if not self.preserve_default:
@@ -47,14 +57,6 @@ class AddField(Operation):
    def describe(self):
        return "Add field %s to %s" % (self.name, self.model_name)

    def __eq__(self, other):
        return (
            (self.__class__ == other.__class__) and
            (self.name == other.name) and
            (self.model_name.lower() == other.model_name.lower()) and
            (self.field.deconstruct()[1:] == other.field.deconstruct()[1:])
        )

    def references_model(self, name, app_label=None):
        return name.lower() == self.model_name.lower()

@@ -71,6 +73,13 @@ class RemoveField(Operation):
        self.model_name = model_name
        self.name = name

    def deconstruct(self):
        return (
            self.__class__.__name__,
            [self.model_name, self.name],
            {}
        )

    def state_forwards(self, app_label, state):
        new_fields = []
        for name, instance in state.models[app_label, self.model_name.lower()].fields:
@@ -110,6 +119,16 @@ class AlterField(Operation):
        self.field = field
        self.preserve_default = preserve_default

    def deconstruct(self):
        kwargs = {}
        if self.preserve_default is not True:
            kwargs['preserve_default'] = self.preserve_default
        return (
            self.__class__.__name__,
            [self.model_name, self.name, self.field],
            kwargs
        )

    def state_forwards(self, app_label, state):
        if not self.preserve_default:
            field = self.field.clone()
@@ -146,14 +165,6 @@ class AlterField(Operation):
    def describe(self):
        return "Alter field %s on %s" % (self.name, self.model_name)

    def __eq__(self, other):
        return (
            (self.__class__ == other.__class__) and
            (self.name == other.name) and
            (self.model_name.lower() == other.model_name.lower()) and
            (self.field.deconstruct()[1:] == other.field.deconstruct()[1:])
        )

    def references_model(self, name, app_label=None):
        return name.lower() == self.model_name.lower()

@@ -171,6 +182,13 @@ class RenameField(Operation):
        self.old_name = old_name
        self.new_name = new_name

    def deconstruct(self):
        return (
            self.__class__.__name__,
            [self.model_name, self.old_name, self.new_name],
            {}
        )

    def state_forwards(self, app_label, state):
        # Rename the field
        state.models[app_label, self.model_name.lower()].fields = [
+61 −9
Original line number Diff line number Diff line
@@ -20,6 +20,18 @@ class CreateModel(Operation):
        self.options = options or {}
        self.bases = bases or (models.Model,)

    def deconstruct(self):
        kwargs = {}
        if self.options:
            kwargs['options'] = self.options
        if self.bases and self.bases != (models.Model,):
            kwargs['bases'] = self.bases
        return (
            self.__class__.__name__,
            [self.name, self.fields],
            kwargs
        )

    def state_forwards(self, app_label, state):
        state.models[app_label, self.name.lower()] = ModelState(
            app_label,
@@ -61,15 +73,6 @@ class CreateModel(Operation):
                return True
        return False

    def __eq__(self, other):
        return (
            (self.__class__ == other.__class__) and
            (self.name == other.name) and
            (self.options == other.options) and
            (self.bases == other.bases) and
            ([(k, f.deconstruct()[1:]) for k, f in self.fields] == [(k, f.deconstruct()[1:]) for k, f in other.fields])
        )


class DeleteModel(Operation):
    """
@@ -79,6 +82,13 @@ class DeleteModel(Operation):
    def __init__(self, name):
        self.name = name

    def deconstruct(self):
        return (
            self.__class__.__name__,
            [self.name],
            {}
        )

    def state_forwards(self, app_label, state):
        del state.models[app_label, self.name.lower()]

@@ -110,6 +120,13 @@ class RenameModel(Operation):
        self.old_name = old_name
        self.new_name = new_name

    def deconstruct(self):
        return (
            self.__class__.__name__,
            [self.old_name, self.new_name],
            {}
        )

    def state_forwards(self, app_label, state):
        # Get all of the related objects we need to repoint
        apps = state.render(skip_cache=True)
@@ -196,6 +213,13 @@ class AlterModelTable(Operation):
        self.name = name
        self.table = table

    def deconstruct(self):
        return (
            self.__class__.__name__,
            [self.name, self.table],
            {}
        )

    def state_forwards(self, app_label, state):
        state.models[app_label, self.name.lower()].options["db_table"] = self.table

@@ -241,6 +265,13 @@ class AlterUniqueTogether(Operation):
        unique_together = normalize_together(unique_together)
        self.unique_together = set(tuple(cons) for cons in unique_together)

    def deconstruct(self):
        return (
            self.__class__.__name__,
            [self.name, self.unique_together],
            {}
        )

    def state_forwards(self, app_label, state):
        model_state = state.models[app_label, self.name.lower()]
        model_state.options[self.option_name] = self.unique_together
@@ -279,6 +310,13 @@ class AlterIndexTogether(Operation):
        index_together = normalize_together(index_together)
        self.index_together = set(tuple(cons) for cons in index_together)

    def deconstruct(self):
        return (
            self.__class__.__name__,
            [self.name, self.index_together],
            {}
        )

    def state_forwards(self, app_label, state):
        model_state = state.models[app_label, self.name.lower()]
        model_state.options[self.option_name] = self.index_together
@@ -314,6 +352,13 @@ class AlterOrderWithRespectTo(Operation):
        self.name = name
        self.order_with_respect_to = order_with_respect_to

    def deconstruct(self):
        return (
            self.__class__.__name__,
            [self.name, self.order_with_respect_to],
            {}
        )

    def state_forwards(self, app_label, state):
        model_state = state.models[app_label, self.name.lower()]
        model_state.options['order_with_respect_to'] = self.order_with_respect_to
@@ -366,6 +411,13 @@ class AlterModelOptions(Operation):
        self.name = name
        self.options = options

    def deconstruct(self):
        return (
            self.__class__.__name__,
            [self.name, self.options],
            {}
        )

    def state_forwards(self, app_label, state):
        model_state = state.models[app_label, self.name.lower()]
        model_state.options = dict(model_state.options)
Loading