sqlalchemy/alembic · error · RevisionError

Not a valid downgrade target from current heads

Error message

Not a valid downgrade target from current heads

What it means

Raised by RevisionMap._collect_downgrade_revisions when a branch label is used to filter downgrade roots and the intersection of candidate roots with the branch's ancestors is empty. This means the branch label you supplied does not correspond to any revision lineage reachable from the current heads during a downgrade. The error prevents Alembic from silently rolling back nothing or the wrong branch.

Solutions

  1. Run 'alembic heads' and 'alembic history --verbose' to inspect the actual branch labels and lineage of current heads.
  2. Verify the branch label is declared via branch_labels= in the target revision's migration script, not just used as a CLI argument.
  3. Drop the branch qualifier and downgrade by revision id directly: alembic downgrade <revision_id> instead of <branch>@<revision>.
  4. If branches were merged, ensure the merge revision carries the expected branch_labels so the ancestor walk succeeds.

Example fix

# before
alembic downgrade stalebranch@dead123

# after
alembic history --verbose  # confirm which labels exist
alembic downgrade dead123   # downgrade by raw revision id
Defensive patterns

Strategy: validation

Validate before calling

from alembic.script.revision import RevisionMap

def validate_downgrade_target(script_dir, branch_label, target_revision):
    rev_map = script_dir.revision_map
    heads = rev_map.get_revisions(rev_map.heads)
    branch_rev = rev_map._resolve_branch(branch_label) if branch_label else None
    ancestors = {
        rev.revision for rev in rev_map._get_ancestor_nodes(
            [branch_rev], include_dependencies=False
        )
    } if branch_rev else None
    if branch_label and ancestors is not None:
        target = rev_map.get_revision(target_revision)
        roots = {rev.revision for rev in rev_map.get_revisions(target.nextrev)}
        if not roots.intersection(ancestors):
            raise ValueError(f"Branch '{branch_label}' has no valid downgrade path")
    return True

Try / catch

from alembic.script.revision import RevisionError

try:
    command.downgrade(config, f"{branch_label}@{target}")
except RevisionError as e:
    if "Not a valid downgrade target" in str(e):
        # fall back to downgrade without branch qualifier
        command.downgrade(config, target)
    else:
        raise

Prevention

When it happens

Trigger: Calling a downgrade with a branch-qualified target (e.g. downgrade('mybranch@abc123')) where 'mybranch' resolves to a revision whose ancestors share no overlap with the direct children of the downgrade target. Occurs specifically when len(roots) > 1 (multiple branches) AND branch_label filtering removes every root (len(roots) == 0 after intersection at revision.py:1379).

Common situations: Using branch labels in a multi-head migration tree where branches were merged or relabeled. Specifying a branch label that was applied to a different lineage than the one currently at head. Renaming or removing branch labels after merge operations. Confusing a revision id used as a branch label with an actual branch_labels declaration.

Related errors


AI-assisted analysis of sqlalchemy/alembic@5551b5d35f (2026-08-11). Data as JSON: /api/errors/b6dcfd3caf38154c. Report an issue: GitHub.

Appendix: source

Thrown at alembic/script/revision.py:1380

            ancestors = {
                rev.revision
                for rev in self._get_ancestor_nodes(
                    [self._resolve_branch(branch_label)],
                    include_dependencies=False,
                )
            }
            # Intersection gives the root revisions we are trying to
            # rollback with the downgrade.
            roots = [
                is_revision(rev)
                for rev in self.get_revisions(
                    {rev.revision for rev in roots}.intersection(ancestors)
                )
            ]

            # Ensure we didn't throw everything away when filtering branches.
            if len(roots) == 0:
                raise RevisionError(
                    "Not a valid downgrade target from current heads"
                )

        heads = self.get_revisions(upper)

        # Aim is to drop :branch_revision; to do so we also need to drop its
        # descendents and anything dependent on it.
        downgrade_revisions = set(
            self._get_descendant_nodes(
                roots,
                include_dependencies=True,
                omit_immediate_dependencies=False,
            )
        )
        active_revisions = set(
            self._get_ancestor_nodes(heads, include_dependencies=True)
        )

View on GitHub (pinned to 5551b5d35f)