sqlalchemy/alembic · critical · CycleDetected
Cycle is detected in revisions (%s)
Error message
Cycle is detected in revisions (%s)
What it means
Raised as CycleDetected in _detect_cycles() when, after wiring down_revision links, there are no reachable heads or no bases despite revisions existing (rev_map non-empty). This indicates the revision graph is disconnected in a way consistent with a cycle: a revision points to a down_revision that ultimately loops back, leaving no well-defined head or base. The message lists the revisions in the affected map.
Source
Thrown at alembic/script/revision.py:273
self._real_heads = tuple(rev.revision for rev in _real_heads)
self.bases = tuple(rev.revision for rev in bases)
self._real_bases = tuple(rev.revision for rev in _real_bases)
self._add_branches(has_branch_labels, revision_map)
return revision_map
def _detect_cycles(
self,
rev_map: _InterimRevisionMapType,
heads: set[Revision],
bases: tuple[Revision, ...],
_real_heads: set[Revision],
_real_bases: tuple[Revision, ...],
) -> None:
if not rev_map:
return
if not heads or not bases:
raise CycleDetected(list(rev_map))
total_space = {
rev.revision
for rev in self._iterate_related_revisions(
lambda r: r._versioned_down_revisions,
heads,
map_=cast(_RevisionMapType, rev_map),
)
}.intersection(
rev.revision
for rev in self._iterate_related_revisions(
lambda r: r.nextrev,
bases,
map_=cast(_RevisionMapType, rev_map),
)
)
deleted_revs = set(rev_map.keys()) - total_space
if deleted_revs:
raise CycleDetected(sorted(deleted_revs))View on GitHub (pinned to 44fb345033)
Solutions
- Inspect every revision's down_revision and resolve the loop so the chain is acyclic and rooted at a base.
- Use 'alembic history' to trace the chain and locate the offending edge.
- Re-generate the conflicting revision with a correct --head / down_revision.
Example fix
// before # revision abc: down_revision = 'def' # revision def: down_revision = 'abc' # cycle // after # revision abc: down_revision = None (or a real ancestor) # revision def: down_revision = 'abc'
Defensive patterns
Strategy: validation
Validate before calling
def revisions_are_acyclic(revisions):
# revisions: dict id -> down_revision(s)
WHITE, GRAY, BLACK = 0, 1, 2
color = {r: WHITE for r in revisions}
def visit(node):
color[node] = GRAY
for parent in (revisions[node] or ()):
if isinstance(parent, tuple):
for p in parent:
if color.get(p) == GRAY:
return True
if color.get(p) == WHITE and visit(p):
return True
else:
if color.get(parent) == GRAY:
return True
if color.get(parent) == WHITE and visit(parent):
return True
color[node] = BLACK
return False
return not any(color[r] == WHITE and visit(r) for r in revisions) Try / catch
try:
script_dir.get_revision(head)
except Exception as e:
if 'Cycle is detected' in str(e):
# run 'alembic history', fix down_revision edges, then retry
...
else:
raise Prevention
- Never hand-edit down_revision to a value that loops back.
- Run 'alembic history' after creating revisions to validate the chain.
- Use 'alembic revision' to generate correct down_revisions.
When it happens
Trigger: A migration file whose down_revision points to another revision whose chain leads back to it; two revisions that reference each other as down_revision/down_revision tuple members; a down_revision that was renamed but not everywhere, creating a closed loop; merge revisions forming a loop.
Common situations: Manual editing of revision files that introduces a self/mutual down_revision reference; rebasing or cherry-picking migrations that corrupt the parent chain; hand-written down_revision values that typo into another revision's id forming a loop.
Related errors
- Dependency cycle is detected in revisions (%s)
- Branch name '%s' in revision %s already used by revision %s
- No such constraint: '%s'
- No context has been configured yet.
- Connection, url, or dialect_name is required.
AI-assisted analysis of sqlalchemy/alembic@44fb345033 (2026-08-04).
Data as JSON: /data/errors/76c3243f372fb140.json.
Report an issue: GitHub.