{"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":"validation","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/44fb3450330204b222ff05135e1fbbbdb28c44db/alembic/autogenerate/rewriter.py#L161-L197","documentation":"Symmetric to the UpgradeOps case: when the rewriter walks downgrade_ops_list, each DowngradeOps must yield exactly one replacement DowngradeOps. Returning a list of length != 1 breaks the migration script's single-downgrade-tree contract.","triggerScenarios":"A process_revision_directives hook returns zero or more than one DowngradeOps when replacing directives[0].downgrade_ops.","commonSituations":"Custom hooks that try to produce multiple downgrade branches or forget to wrap a single DowngradeOps; copy-paste from an upgrade hook that mutated a different structure.","solutions":["Mutate the existing DowngradeOps.ops list in place rather than replacing the DowngradeOps with a list.","Return exactly one DowngradeOps; nest extra ops inside it.","If multiple downgrade paths are needed, model them as separate MigrationScripts."],"exampleFix":"// before\ndef process_revision(ctx, revision, directives):\n    directives[0].downgrade_ops = [down_ops_a, down_ops_b]\n// after\ndef process_revision(ctx, revision, directives):\n    directives[0].downgrade_ops.ops.extend([op_a, op_b])","handlingStrategy":"validation","validationCode":"downgrade_ops = directives[0].downgrade_ops\nassert isinstance(downgrade_ops, DowngradeOps)\ndowngrade_ops.ops.append(new_op)  # mutate, don't return a list","typeGuard":"def is_single_downgrade_ops(obj) -> bool:\n    from alembic.operations.ops import DowngradeOps\n    return isinstance(obj, DowngradeOps)","tryCatchPattern":null,"preventionTips":["Mirror the upgrade hook's discipline on the downgrade side.","Return None from process_revision_directives and rely on in-place mutation.","Test autogenerate produces a single downgrade block."],"tags":["autogenerate","rewriter","process-revision-directives"],"analyzedSha":"44fb3450330204b222ff05135e1fbbbdb28c44db","analyzedAt":"2026-08-04T19:57:10.248Z","schemaVersion":2}