{"record":{"id":"cf693b67d0b5d223","repo":"sqlalchemy/alembic","slug":"can-only-return-single-object-for-downgradeops-tra","errorCode":null,"errorMessage":"Can only return single object for DowngradeOps traverse","messagePattern":"Can only return single object for DowngradeOps traverse","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"alembic/autogenerate/rewriter.py","lineNumber":179,"sourceCode":"        revision: _GetRevArg,\n        directive: MigrationScript,\n    ) -> None:\n        upgrade_ops_list: list[UpgradeOps] = []\n        for upgrade_ops in directive.upgrade_ops_list:\n            ret = self._traverse_for(context, revision, upgrade_ops)\n            if len(ret) != 1:\n                raise ValueError(\n                    \"Can only return single object for UpgradeOps traverse\"\n                )\n            upgrade_ops_list.append(ret[0])\n\n        directive.upgrade_ops = upgrade_ops_list\n\n        downgrade_ops_list: list[DowngradeOps] = []\n        for downgrade_ops in directive.downgrade_ops_list:\n            ret = self._traverse_for(context, revision, downgrade_ops)\n            if len(ret) != 1:\n                raise ValueError(\n                    \"Can only return single object for DowngradeOps traverse\"\n                )\n            downgrade_ops_list.append(ret[0])\n        directive.downgrade_ops = downgrade_ops_list\n\n    @_traverse.dispatch_for(ops.OpContainer)\n    def _traverse_op_container(\n        self,\n        context: MigrationContext,\n        revision: _GetRevArg,\n        directive: OpContainer,\n    ) -> None:\n        self._traverse_list(context, revision, directive.ops)\n\n    @_traverse.dispatch_for(ops.MigrateOperation)\n    def _traverse_any_directive(\n        self,\n        context: MigrationContext,","sourceCodeStart":161,"sourceCodeEnd":197,"githubUrl":"https://github.com/sqlalchemy/alembic/blob/5551b5d35f985c99cb8f1af2b3c526b050e4c059/alembic/autogenerate/rewriter.py#L161-L197","documentation":"The rewriter raises ValueError when a custom rewrite registered for a DowngradeOps directive returns zero or more than one directive during traversal. As with UpgradeOps, the traversal contract requires each DowngradeOps container to map to exactly one DowngradeOps to preserve the migration script structure.","triggerScenarios":"Registering @writer.rewrites(DowngradeOps) and returning a list of multiple DowngradeOps objects, None, or an empty list.","commonSituations":"Custom downgrade rewriting that attempts to fork a downgrade into multiple branches; a rewrite that returns [] when it has 'nothing to do'.","solutions":["Ensure your @writer.rewrites handler for DowngradeOps always returns exactly one DowngradeOps.","Mutate op.ops in place to add or remove downgrade steps, then return the same op.","Return an empty DowngradeOps(ops=[]) if you genuinely want a no-op downgrade, never None or a multi-element list."],"exampleFix":"# before\n@writer.rewrites(DowngradeOps)\ndef rewrite_down(context, revision, op):\n    if not op.ops:\n        return None  # error\n    return op\n\n# after\n@writer.rewrites(DowngradeOps)\ndef rewrite_down(context, revision, op):\n    return op  # always exactly one, even if ops is empty","handlingStrategy":"validation","validationCode":"from alembic.operations import ops\n\ndef safe_rewrite_downgrade(handler):\n    def wrapper(context, revision, op):\n        result = handler(context, revision, op)\n        assert isinstance(result, ops.DowngradeOps), 'must return one DowngradeOps'\n        return result\n    return wrapper","typeGuard":"from alembic.operations import ops\n\ndef is_single_downgrade_ops(value) -> bool:\n    return isinstance(value, ops.DowngradeOps)","tryCatchPattern":null,"preventionTips":["Always return exactly one DowngradeOps from @writer.rewrites(DowngradeOps) handlers.","Never return None; return DowngradeOps(ops=[]) for a no-op downgrade.","Unit-test the rewrite against empty and non-empty op lists."],"tags":["rewriter","autogenerate","downgrade-ops","directive"],"backgroundTag":null,"analyzedSha":"5551b5d35f985c99cb8f1af2b3c526b050e4c059","analyzedAt":"2026-08-11T01:38:46.612Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}