{"id":"1bc2ea98f4fb5f5e","repo":"sqlalchemy/alembic","slug":"can-only-return-single-object-for-upgradeops-trave","errorCode":null,"errorMessage":"Can only return single object for UpgradeOps traverse","messagePattern":"Can only return single object for UpgradeOps traverse","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"alembic/autogenerate/rewriter.py","lineNumber":168,"sourceCode":"        revision: _GetRevArg,\n        directives: list[MigrationScript],\n    ) -> None:\n        self.process_revision_directives(context, revision, directives)\n        for process_revision_directives in self._chained:\n            process_revision_directives(context, revision, directives)\n\n    @_traverse.dispatch_for(ops.MigrationScript)\n    def _traverse_script(\n        self,\n        context: MigrationContext,\n        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(","sourceCodeStart":150,"sourceCodeEnd":186,"githubUrl":"https://github.com/sqlalchemy/alembic/blob/44fb3450330204b222ff05135e1fbbbdb28c44db/alembic/autogenerate/rewriter.py#L150-L186","documentation":"The Rewriter._traverse_script walks a MigrationScript's upgrade_ops_list; for each UpgradeOps it calls the user's process_revision_directives hook and expects exactly one replacement UpgradeOps object back. Returning zero or multiple ops would break the in-place rewrite contract (the script must still have a single upgrade ops tree), so the length check raises.","triggerScenarios":"Passing a process_revision_directives callable to command.revision / RevisionContext that returns a list of more than one (or zero) UpgradeOps in place of the single directive it received. Typically happens when a custom hook appends or replaces directives[0].upgrade_ops with a list rather than a single UpgradeOps instance.","commonSituations":"Custom process_revision_directives that splits work into multiple upgrade ops trees; a hook that mistakenly returns [op1, op2] instead of op1 with op2 nested as an op inside it.","solutions":["Have the hook mutate the single UpgradeOps it received (append ops to its .ops list) rather than returning a new list.","Return exactly one UpgradeOps instance; nest additional operations inside it.","If multiple upgrade paths are genuinely needed, restructure so they are separate MigrationScript entries, not multiple UpgradeOps in one script."],"exampleFix":"// before\ndef process_revision(ctx, revision, directives):\n    upgrade_ops = directives[0].upgrade_ops\n    upgrade_ops.ops = [op1, op2]  # OK\n    return [upgrade_ops, extra_ops]   # wrong: returns 2\n// after\ndef process_revision(ctx, revision, directives):\n    upgrade_ops = directives[0].upgrade_ops\n    upgrade_ops.ops.append(extra_op)\n    # return nothing; mutation is enough","handlingStrategy":"validation","validationCode":"# inside process_revision_directives\nupgrade_ops = directives[0].upgrade_ops\nassert isinstance(upgrade_ops, UpgradeOps)\nupgrade_ops.ops.append(new_op)  # mutate in place, do not return a list","typeGuard":"def is_single_upgrade_ops(obj) -> bool:\n    from alembic.operations.ops import UpgradeOps\n    return isinstance(obj, UpgradeOps)","tryCatchPattern":null,"preventionTips":["Mutate directives[0].upgrade_ops.ops in place; do not replace the UpgradeOps with a list.","Never return a list from process_revision_directives; either return None or a single object.","Add a smoke test that runs autogenerate with your hook."],"tags":["autogenerate","rewriter","process-revision-directives"],"analyzedSha":"44fb3450330204b222ff05135e1fbbbdb28c44db","analyzedAt":"2026-08-04T19:57:10.248Z","schemaVersion":2}