{"record":{"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/5551b5d35f985c99cb8f1af2b3c526b050e4c059/alembic/script/revision.py#L255-L291","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\n# revision abc: down_revision = 'def'\n# revision def: down_revision = 'abc'  # cycle\n// after\n# revision abc: down_revision = 'def'\n# revision def: down_revision = None  # def is the base","handlingStrategy":"validation","validationCode":"# Walk the revision DAG before running migrations and reject cycles.\nfrom alembic.script import ScriptDirectory\n\ndef detect_down_revision_cycle(config):\n    script_dir = ScriptDirectory.from_config(config)\n    revs = {r.revision: r for r in script_dir.walk_revisions()}\n    for rev in revs.values():\n        seen, cur = set(), rev.revision\n        while cur is not None and cur in revs:\n            if cur in seen:\n                raise ValueError(f'Cycle in down_revision chain at {cur}')\n            seen.add(cur)\n            cur = revs[cur].down_revision\n\nimport alembic.command  # constructing the map itself will raise CycleDetected too","typeGuard":"def is_acyclic_down_revision(revs: dict) -> bool:\n    color = {}\n    def dfs(r):\n        if color.get(r) == 'gray':\n            return False\n        if color.get(r) == 'black':\n            return True\n        color[r] = 'gray'\n        parent = revs[r].down_revision\n        if parent in revs and not dfs(parent):\n            return False\n        color[r] = 'black'\n        return True\n    return all(dfs(r) for r in revs)","tryCatchPattern":"from alembic.script.revision import CycleDetected\n\ntry:\n    alembic.command.upgrade(config, 'head')\nexcept CycleDetected as e:\n    print('Revision cycle detected; offending revisions:', e.revisions)","preventionTips":["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."],"tags":["alembic","revisions","cycle","migration-graph"],"backgroundTag":null,"analyzedSha":"5551b5d35f985c99cb8f1af2b3c526b050e4c059","analyzedAt":"2026-08-11T01:38:46.612Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}