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
- Run 'alembic heads' and 'alembic history --verbose' to inspect the actual branch labels and lineage of current heads.
- Verify the branch label is declared via branch_labels= in the target revision's migration script, not just used as a CLI argument.
- Drop the branch qualifier and downgrade by revision id directly: alembic downgrade <revision_id> instead of <branch>@<revision>.
- 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
- Always run 'alembic heads' and 'alembic history --verbose' before downgrade operations involving branch labels.
- Prefer raw revision ids over branch-qualified targets unless branches are essential to your workflow.
- Document branch_labels assignments in your team's migration guide to avoid confusion.
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
- Revision is not an ancestor of revision
- Character(s) ' ' not allowed in revision identifier
- Dependency self-loop is detected in revisions
- Self-loop is detected in revisions
- A plugin named is already registered
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)