{"id":"76c3243f372fb140","repo":"sqlalchemy/alembic","slug":"cycle-is-detected-in-revisions-s","errorCode":null,"errorMessage":"Cycle is detected in revisions (%s)","messagePattern":"Cycle is detected in revisions \\((.+?)\\)","errorType":"exception","errorClass":"CycleDetected","httpStatus":null,"severity":"critical","filePath":"alembic/script/revision.py","lineNumber":273,"sourceCode":"        self._real_heads = tuple(rev.revision for rev in _real_heads)\n        self.bases = tuple(rev.revision for rev in bases)\n        self._real_bases = tuple(rev.revision for rev in _real_bases)\n\n        self._add_branches(has_branch_labels, revision_map)\n        return revision_map\n\n    def _detect_cycles(\n        self,\n        rev_map: _InterimRevisionMapType,\n        heads: set[Revision],\n        bases: tuple[Revision, ...],\n        _real_heads: set[Revision],\n        _real_bases: tuple[Revision, ...],\n    ) -> None:\n        if not rev_map:\n            return\n        if not heads or not bases:\n            raise CycleDetected(list(rev_map))\n        total_space = {\n            rev.revision\n            for rev in self._iterate_related_revisions(\n                lambda r: r._versioned_down_revisions,\n                heads,\n                map_=cast(_RevisionMapType, rev_map),\n            )\n        }.intersection(\n            rev.revision\n            for rev in self._iterate_related_revisions(\n                lambda r: r.nextrev,\n                bases,\n                map_=cast(_RevisionMapType, rev_map),\n            )\n        )\n        deleted_revs = set(rev_map.keys()) - total_space\n        if deleted_revs:\n            raise CycleDetected(sorted(deleted_revs))","sourceCodeStart":255,"sourceCodeEnd":291,"githubUrl":"https://github.com/sqlalchemy/alembic/blob/44fb3450330204b222ff05135e1fbbbdb28c44db/alembic/script/revision.py#L255-L291","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\n# revision abc: down_revision = 'def'\n# revision def: down_revision = 'abc'  # cycle\n\n// after\n# revision abc: down_revision = None  (or a real ancestor)\n# revision def: down_revision = 'abc'","handlingStrategy":"validation","validationCode":"def revisions_are_acyclic(revisions):\n    # revisions: dict id -> down_revision(s)\n    WHITE, GRAY, BLACK = 0, 1, 2\n    color = {r: WHITE for r in revisions}\n    def visit(node):\n        color[node] = GRAY\n        for parent in (revisions[node] or ()):\n            if isinstance(parent, tuple):\n                for p in parent:\n                    if color.get(p) == GRAY:\n                        return True\n                    if color.get(p) == WHITE and visit(p):\n                        return True\n            else:\n                if color.get(parent) == GRAY:\n                    return True\n                if color.get(parent) == WHITE and visit(parent):\n                    return True\n        color[node] = BLACK\n        return False\n    return not any(color[r] == WHITE and visit(r) for r in revisions)","typeGuard":null,"tryCatchPattern":"try:\n    script_dir.get_revision(head)\nexcept Exception as e:\n    if 'Cycle is detected' in str(e):\n        # run 'alembic history', fix down_revision edges, then retry\n        ...\n    else:\n        raise","preventionTips":["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."],"tags":["alembic","revisions","migration-graph","data-integrity","configuration"],"analyzedSha":"44fb3450330204b222ff05135e1fbbbdb28c44db","analyzedAt":"2026-08-04T19:57:10.248Z","schemaVersion":2}