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

  1. Inspect every revision's down_revision and resolve the loop so the chain is acyclic and rooted at a base.
  2. Use 'alembic history' to trace the chain and locate the offending edge.
  3. 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

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


AI-assisted analysis of sqlalchemy/alembic@44fb345033 (2026-08-04). Data as JSON: /data/errors/76c3243f372fb140.json. Report an issue: GitHub.