{"record":{"id":"0917efe27d98012a","repo":"sqlalchemy/alembic","slug":"can-t-change-down-revision-on-a-refresh-operation","errorCode":null,"errorMessage":"Can't change down_revision on a refresh operation.","messagePattern":"Can't change down_revision on a refresh operation\\.","errorType":"exception","errorClass":"Exception","httpStatus":null,"severity":"error","filePath":"alembic/testing/env.py","lineNumber":331,"sourceCode":"\ndef write_script(\n    scriptdir, rev_id, content, encoding=\"ascii\", sourceless=False\n):\n    old = scriptdir.revision_map.get_revision(rev_id)\n    path = old.path\n\n    content = textwrap.dedent(content)\n    if encoding:\n        content = content.encode(encoding)\n    with open(path, \"wb\") as fp:\n        fp.write(content)\n    pyc_path = util.pyc_file_from_path(path)\n    if pyc_path:\n        os.unlink(pyc_path)\n    script = Script._from_path(scriptdir, path)\n    old = scriptdir.revision_map.get_revision(script.revision)\n    if old.down_revision != script.down_revision:\n        raise Exception(\"Can't change down_revision on a refresh operation.\")\n    scriptdir.revision_map.add_revision(script, _replace=True)\n\n    if sourceless:\n        make_sourceless(\n            path, \"pep3147\" if sourceless == \"pep3147_everything\" else \"simple\"\n        )\n\n\ndef make_sourceless(path, style):\n    import py_compile\n\n    py_compile.compile(path)\n\n    if style == \"simple\":\n        pyc_path = util.pyc_file_from_path(path)\n        suffix = importlib.machinery.BYTECODE_SUFFIXES[0]\n        filepath, ext = os.path.splitext(path)\n        simple_pyc_path = filepath + suffix","sourceCodeStart":313,"sourceCodeEnd":349,"githubUrl":"https://github.com/sqlalchemy/alembic/blob/5551b5d35f985c99cb8f1af2b3c526b050e4c059/alembic/testing/env.py#L313-L349","documentation":"Raised by the testing helper write_script in alembic/testing/env.py when refreshing a migration script's content changes its down_revision. The write_script function is designed to update only the body of an existing script while preserving its position in the revision graph. Changing down_revision during a refresh would corrupt the graph topology silently, so it's guarded with a hard exception.","triggerScenarios":"Calling write_script(scriptdir, rev_id, content) where the new content defines a different down_revision than the original script at rev_id (env.py:330-331). This is a test-suite-only helper, not part of the public API.","commonSituations":"Writing Alembic's own test suite and using write_script to modify a migration body but accidentally changing the down_revision line. Copy-pasting test migration content from another revision without updating down_revision to match the original. Refactoring tests that rewrite migration scripts.","solutions":["Ensure the new content's down_revision matches the original script's down_revision exactly.","If you need to test a different down_revision, create a new script with _from_path or use the proper Script construction, not write_script.","Diff the content string against the original script to spot the down_revision mismatch.","Extract the down_revision from the original script programmatically and inject it into the new content."],"exampleFix":"# before\nwrite_script(scriptdir, 'abc123', dedent('''\n    revision = 'abc123'\n    down_revision = 'wrong_id'  # differs from original\n    def upgrade(): pass\n'''))\n\n# after\noriginal = scriptdir.revision_map.get_revision('abc123')\nwrite_script(scriptdir, 'abc123', dedent(f'''\n    revision = 'abc123'\n    down_revision = '{original.down_revision}'\n    def upgrade(): pass\n'''))","handlingStrategy":"validation","validationCode":"def validate_write_script(scriptdir, rev_id, new_content):\n    old = scriptdir.revision_map.get_revision(rev_id)\n    # parse down_revision from new_content and compare\n    import ast\n    tree = ast.parse(new_content)\n    new_down = None\n    for node in ast.walk(tree):\n        if isinstance(node, ast.Assign):\n            for t in node.targets:\n                if isinstance(t, ast.Name) and t.id == 'down_revision':\n                    new_down = ast.literal_eval(node.value)\n    if old.down_revision != new_down:\n        raise ValueError(f\"down_revision mismatch: {old.down_revision} != {new_down}\")\n    return True","typeGuard":null,"tryCatchPattern":null,"preventionTips":["When using write_script in tests, always extract the original down_revision and template it into the new content.","Treat write_script as body-only refresh; never change revision graph fields.","Add an assertion in test helpers that checks down_revision equality before calling write_script."],"tags":["alembic","testing","test-helper","migration-scripts","validation"],"backgroundTag":null,"analyzedSha":"5551b5d35f985c99cb8f1af2b3c526b050e4c059","analyzedAt":"2026-08-11T01:38:46.612Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}