sqlalchemy/alembic · critical · CycleDetected
Cycle is detected in revisions
Error message
Cycle is detected in revisions (%s)
What it means
Raised as CycleDetected (RevisionError subclass, revision.py:89-97) by _detect_cycles (revision.py:272-273) when, after building the revision map, there are no heads or no bases reachable through the down_revision chain. With no head and no base, the down_revision graph cannot be a DAG, so the entire revision set is reported as cyclic. This blocks Alembic from computing head/base for upgrades/downgrades.
Solutions
- Inspect every revision file's down_revision and ensure each points to a strictly earlier revision with no loop.
- Use `alembic history` to list revisions and find the cycle; correct the down_revision of the offending file.
- For branches, use merge revisions correctly and avoid pointing two parents at each other.
Example fix
// before # revision abc: down_revision = 'def' # revision def: down_revision = 'abc' # cycle // after # revision abc: down_revision = 'def' # revision def: down_revision = None # def is the base
Defensive patterns
Strategy: validation
Validate before calling
# Walk the revision DAG before running migrations and reject cycles.
from alembic.script import ScriptDirectory
def detect_down_revision_cycle(config):
script_dir = ScriptDirectory.from_config(config)
revs = {r.revision: r for r in script_dir.walk_revisions()}
for rev in revs.values():
seen, cur = set(), rev.revision
while cur is not None and cur in revs:
if cur in seen:
raise ValueError(f'Cycle in down_revision chain at {cur}')
seen.add(cur)
cur = revs[cur].down_revision
import alembic.command # constructing the map itself will raise CycleDetected too Type guard
def is_acyclic_down_revision(revs: dict) -> bool:
color = {}
def dfs(r):
if color.get(r) == 'gray':
return False
if color.get(r) == 'black':
return True
color[r] = 'gray'
parent = revs[r].down_revision
if parent in revs and not dfs(parent):
return False
color[r] = 'black'
return True
return all(dfs(r) for r in revs) Try / catch
from alembic.script.revision import CycleDetected
try:
alembic.command.upgrade(config, 'head')
except CycleDetected as e:
print('Revision cycle detected; offending revisions:', e.revisions) Prevention
- Never hand-edit down_revision to point at a later revision.
- Run `alembic history` after authoring each revision to confirm the chain.
- Generate revisions with `alembic revision` so down_revision is wired automatically.
When it happens
Trigger: A revision whose down_revision points to a descendant, forming a loop in the parent chain (e.g. rev A -> down B, rev B -> down A); or a self-referential down_revision. The traversal finds no terminus, heads/bases come back empty, and CycleDetected is raised with the full revision list.
Common situations: Hand-editing a revision's revision/down_revision identifiers; merge revisions wired with incorrect parents; copy-pasting a revision file and forgetting to change down_revision, creating a mutual reference.
Related errors
- Dependency cycle is detected in revisions
- Self-loop is detected in revisions
- A plugin named is already registered
- Can not set dispatch function for object
- Can't change down_revision on a refresh operation.
AI-assisted analysis of sqlalchemy/alembic@5551b5d35f (2026-08-11).
Data as JSON: /api/errors/76c3243f372fb140.
Report an issue: GitHub.
Appendix: 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 5551b5d35f)