sqlalchemy/alembic · critical · DependencyCycleDetected

Dependency cycle is detected in revisions (%s)

Error message

Dependency cycle is detected in revisions (%s)

What it means

Raised as DependencyCycleDetected (subclass of CycleDetected) in _detect_cycles() when, after wiring depends_on links, there are no real heads or no real bases (the dependency subgraph has no endpoints). This signals a cycle purely among the depends_on dependencies declared on revisions, independent of the down_revision lineage. The message lists the revisions involved.

Source

Thrown at alembic/script/revision.py:294

            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))

        if not _real_heads or not _real_bases:
            raise DependencyCycleDetected(list(rev_map))
        total_space = {
            rev.revision
            for rev in self._iterate_related_revisions(
                lambda r: r._all_down_revisions,
                _real_heads,
                map_=cast(_RevisionMapType, rev_map),
            )
        }.intersection(
            rev.revision
            for rev in self._iterate_related_revisions(
                lambda r: r._all_nextrev,
                _real_bases,
                map_=cast(_RevisionMapType, rev_map),
            )
        )
        deleted_revs = set(rev_map.keys()) - total_space
        if deleted_revs:
            raise DependencyCycleDetected(sorted(deleted_revs))

View on GitHub (pinned to 44fb345033)

Solutions

  1. Audit the depends_on attributes of the listed revisions and break the cycle.
  2. Use 'alembic history' and inspect each revision's depends_on to find the loop edge.
  3. Re-declare depends_on as a strict DAG (no reciprocal dependencies).

Example fix

// before
# revision A: depends_on='B'
# revision B: depends_on='A'  # cycle

// after
# revision A: depends_on='B'
# revision B: depends_on=None
Defensive patterns

Strategy: validation

Validate before calling

def depends_on_acyclic(dep_map):
    # dep_map: dict revision_id -> tuple of depends_on ids
    WHITE, GRAY, BLACK = 0, 1, 2
    color = {r: WHITE for r in dep_map}
    def visit(node):
        color[node] = GRAY
        for dep in (dep_map.get(node) or ()):
            if color.get(dep) == GRAY:
                return True
            if color.get(dep) == WHITE and visit(dep):
                return True
        color[node] = BLACK
        return False
    return not any(color[r] == WHITE and visit(r) for r in dep_map)

Try / catch

try:
    script_dir.get_revision('heads')
except Exception as e:
    if 'Dependency cycle' in str(e):
        # audit depends_on edges on listed revisions
        ...
    else:
        raise

Prevention

When it happens

Trigger: Two revisions where revision A lists revision B in depends_on and B lists A in depends_on; a longer depends_on loop (A->B->C->A); a revision depending on itself via depends_on.

Common situations: Manually setting depends_on on revisions to enforce ordering and accidentally forming a loop; cross-branch dependencies declared symmetrically; generated depends_on values that reference each other.

Related errors


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