示例#1
0
    def walk_down(self, from_node: Migration, unapplied_only=True, _node_counters=None):
        """
        Walks down over migrations graph. Iterates in order as migrations
        should be applied.

        We're used modified DFS (depth-first search) algorithm to traverse
        the graph. Migrations are built into directed graph (digraph)
        counted from one root node to the last ones. Commonly DFS tries to
        walk to the maximum depth firstly.

        But there are some problems:
        * Graph can have directed cycles. This happens when some
          migration has several dependencies. Therefore we'll walk
          over such migration several times
        * Another problem arises from the first one. Typically we must walk
          over all dependencies before a dependent migration will be
          touched. DFS will process only one dependency before get to
          a dependent migration

        In order to manage it we use counter for each migration
        (node in digraph) initially equal to its parents count.
        Every time the algorithm gets to node it decrements this counter.
        If counter > 0 after that then don't touch this node and
        break traversing on this depth and go up. If counter == 0 then
        continue traversing.
        :param from_node: current node in graph
        :param unapplied_only: if True then return only unapplied migrations
         or return all migrations otherwise
        :param _node_counters:
        :raises MigrationGraphError: if graph has a closed cycle
        :return: Migration objects generator
        """
        # FIXME: may yield nodes not related to target migration if branchy graph
        # FIXME: if migration was applied after its dependencies unapplied then it is an error
        # FIXME: should have stable migrations order
        if _node_counters is None:
            _node_counters = {}
        if from_node is None:
            return ()
        _node_counters.setdefault(from_node.name, len(self._parents[from_node.name]) or 1)
        _node_counters[from_node.name] -= 1

        if _node_counters[from_node.name] > 0:
            # Stop on this depth if not all parents has been viewed
            return

        if _node_counters[from_node.name] < 0:
            # A node was already returned and we're reached it again
            # This means there is a closed cycle
            raise MigrationGraphError(f'Found closed cycle in migration graph, '
                                      f'{from_node.name!r} is repeated twice')

        if not (from_node.applied and unapplied_only):
            yield from_node

        for child in self._children[from_node.name]:
            yield from self.walk_down(child, unapplied_only, _node_counters)
示例#2
0
    def verify(self):
        """
        Verify migrations graph to be satisfied to consistency rules
        Graph must not have loops, disconnections.
        Also it should have single initial migration and (for a while)
        single last migration.
        :raises MigrationGraphError: if problem in graph was found
        :return:
        """
        # FIXME: This function is not used anywhere
        initials = []
        last_children = []

        for name, obj in self._migrations.items():
            if not self._parents[name]:
                initials.append(name)
            if not self._children[name]:
                last_children.append(name)
            if len(obj.dependencies) > len(self._parents[name]):
                diff = set(obj.dependencies) - {x.name for x in self._parents[name]}
                raise MigrationGraphError(f'Unknown dependencies in migration {name!r}: {diff}')
            if name in (x.name for x in self._children[name]):
                raise MigrationGraphError(f'Found migration which dependent on itself: {name!r}')

        if len(initials) == len(last_children) and len(initials) > 1:
            raise MigrationGraphError(f'Migrations graph is disconnected, history segments '
                                 f'started on: {initials!r}, ended on: {last_children!r}')
        if len(initials) > 1:
            raise MigrationGraphError(f'Several initial migrations found: {initials!r}')

        if len(last_children) > 1:
            raise MigrationGraphError(f'Several last migrations found: {last_children!r}')

        if not initials or not last_children:
            raise MigrationGraphError(f'No initial or last children found')
示例#3
0
    def upgrade(self,
                migration_name: str,
                graph: Optional[MigrationsGraph] = None):
        """
        Upgrade db to the given migration
        :param migration_name: target migration name
        :param graph: Optional. Migrations graph. If omitted, then it
         will be loaded
        :return:
        """
        if graph is None:
            log.debug('Loading migration files...')
            graph = self.build_graph()
        log.debug('Loading schema from database...')
        left_schema = self.load_db_schema()

        if migration_name not in graph.migrations:
            raise MigrationGraphError(f'Migration {migration_name} not found')

        db = self.db
        for migration in graph.walk_down(graph.initial, unapplied_only=True):
            log.info('Upgrading %s...', migration.name)
            for idx, action_object in enumerate(migration.get_actions(),
                                                start=1):
                log.debug('> [%d] %s', idx, str(action_object))
                if not action_object.dummy_action and not runtime_flags.schema_only:
                    action_object.prepare(db, left_schema, migration.policy)
                    action_object.run_forward()
                    action_object.cleanup()

                try:
                    left_schema = patch(
                        action_object.to_schema_patch(left_schema),
                        left_schema)
                except (TypeError, ValueError, KeyError) as e:
                    raise ActionError(
                        f"Unable to apply schema patch of {action_object!r}. More likely that the "
                        f"schema is corrupted. You can use schema repair tools to fix this issue"
                    ) from e

            graph.migrations[migration.name].applied = True

            if not runtime_flags.dry_run:
                log.debug('Writing db schema and migrations graph...')
                self.write_db_schema(left_schema)
                self.write_db_migrations_graph(graph)

            if migration.name == migration_name:
                break  # We've reached the target migration

        self._verify_schema(left_schema)
示例#4
0
    def migrate(self, migration_name: str = None):
        """
        Migrate db in order to reach a given migration. This process
        may require either upgrading or downgrading
        :param migration_name: target migration name
        :return:
        """
        log.debug('Loading migration files...')
        graph = self.build_graph()
        if not graph.last:
            raise MigrationGraphError('No migrations found')

        if migration_name is None:
            migration_name = graph.last.name

        if migration_name not in graph.migrations:
            raise MigrationGraphError(f'Migration {migration_name} not found')

        migration = graph.migrations[migration_name]
        if migration.applied:
            self.downgrade(migration_name, graph)
        else:
            self.upgrade(migration_name, graph)
示例#5
0
    def walk_up(self,
                from_node: Migration,
                applied_only=True,
                _node_counters=None):
        """
        Walks up over migrations graph. Iterates in order as migrations
        should be reverted.

        We're using modified DFS (depth-first search) algorithm which in
        reversed order (see `walk_down`). Instead of looking at node
        parents count we're consider children count in order to return
        all dependent nodes before dependency.

        Because of the migrations graph may have many orphan child nodes
        they all should be passed as parameter
        :param from_node:  last children node we are starting for
        :param applied_only: if True then return only applied migrations,
         return all migrations otherwise
        :param _node_counters:
        :raises MigrationGraphError: if graph has a closed cycle
        :return: Migration objects generator
        """
        # FIXME: may yield nodes not related to reverting if branchy graph
        #        if migration was unapplied before its dependencies applied then it is an error
        if _node_counters is None:
            _node_counters = {}
        if from_node is None:
            return ()
        _node_counters.setdefault(from_node.name,
                                  len(self._children[from_node.name]) or 1)
        _node_counters[from_node.name] -= 1

        if _node_counters[from_node.name] > 0:
            # Stop in this depth if not all children has been viewed
            return

        if _node_counters[from_node.name] < 0:
            # A node was already returned and we're reached it again
            # This means there is a closed cycle
            raise MigrationGraphError(
                f'Found closed cycle in migration graph, '
                f'{from_node.name!r} is repeated twice')

        if from_node.applied or not applied_only:
            yield from_node

        for child in self._parents[from_node.name]:
            yield from self.walk_up(child, applied_only, _node_counters)
示例#6
0
    def build_graph(self) -> MigrationsGraph:
        """Build migrations graph with all migration modules"""
        graph = MigrationsGraph()
        for m in self.load_migrations(Path(self.migration_dir)):
            graph.add(m)

        applied = []
        for migration_name in self.get_db_migration_names():
            if migration_name not in graph.migrations:
                raise MigrationGraphError(
                    f'Migration {migration_name} was applied, but its python module not found. '
                    f'You can use schema repair to fix this issue')
            graph.migrations[migration_name].applied = True
            applied.append(migration_name)

        log.debug('> Applied migrations: %s', applied)
        if graph.last:
            log.debug('> Last migration is: %s', graph.last.name)
        else:
            log.debug('> Last migration is: %s', 'None')

        return graph
示例#7
0
    def downgrade(self,
                  migration_name: str,
                  graph: Optional[MigrationsGraph] = None):
        """
        Downgrade db to the given migration
        :param migration_name: target migration name
        :param graph: Optional. Migrations graph. If omitted, then it
         will be loaded
        :return:
        """
        if graph is None:
            log.debug('Loading migration files...')
            graph = self.build_graph()
        log.debug('Loading schema from database...')
        left_schema = self.load_db_schema()

        if migration_name not in graph.migrations:
            raise MigrationGraphError(f'Migration {migration_name} not found')

        log.debug('Precalculating schema diffs...')
        # Collect schema diffs across all migrations
        migration_diffs = {}  # {migration_name: [action1_diff, ...]}
        temp_left_schema = Schema()
        for migration in graph.walk_down(graph.initial, unapplied_only=False):
            migration_diffs[migration.name] = []
            for action in migration.get_actions():
                forward_patch = action.to_schema_patch(temp_left_schema)
                migration_diffs[migration.name].append(forward_patch)

                try:
                    temp_left_schema = patch(forward_patch, temp_left_schema)
                except (TypeError, ValueError, KeyError) as e:
                    raise ActionError(
                        f"Unable to apply schema patch of {action!r}. More likely that the "
                        f"schema is corrupted. You can use schema repair tools to fix this issue"
                    ) from e

        db = self.db
        for migration in graph.walk_up(graph.last, applied_only=True):
            if migration.name == migration_name:
                break  # We've reached the target migration

            log.info('Downgrading %s...', migration.name)

            action_diffs = zip(migration.get_actions(),
                               migration_diffs[migration.name],
                               range(1,
                                     len(migration.get_actions()) + 1))
            for action_object, action_diff, idx in reversed(
                    list(action_diffs)):
                log.debug('> [%d] %s', idx, str(action_object))

                try:
                    left_schema = patch(list(swap(action_diff)), left_schema)
                except (TypeError, ValueError, KeyError) as e:
                    raise ActionError(
                        f"Unable to apply schema patch of {action_object!r}. More likely that the "
                        f"schema is corrupted. You can use schema repair tools to fix this issue"
                    ) from e

                if not action_object.dummy_action and not runtime_flags.schema_only:
                    action_object.prepare(db, left_schema, migration.policy)
                    action_object.run_backward()
                    action_object.cleanup()

            graph.migrations[migration.name].applied = False

            if not runtime_flags.dry_run:
                log.debug('Writing db schema and migrations graph...')
                self.write_db_schema(left_schema)
                self.write_db_migrations_graph(graph)

        self._verify_schema(left_schema)