sqlalchemy/alembic · error · Exception

Can't change down_revision on a refresh operation.

Error message

Can't change down_revision on a refresh operation.

What it means

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.

Source

Thrown at alembic/testing/env.py:331

def write_script(
    scriptdir, rev_id, content, encoding="ascii", sourceless=False
):
    old = scriptdir.revision_map.get_revision(rev_id)
    path = old.path

    content = textwrap.dedent(content)
    if encoding:
        content = content.encode(encoding)
    with open(path, "wb") as fp:
        fp.write(content)
    pyc_path = util.pyc_file_from_path(path)
    if pyc_path:
        os.unlink(pyc_path)
    script = Script._from_path(scriptdir, path)
    old = scriptdir.revision_map.get_revision(script.revision)
    if old.down_revision != script.down_revision:
        raise Exception("Can't change down_revision on a refresh operation.")
    scriptdir.revision_map.add_revision(script, _replace=True)

    if sourceless:
        make_sourceless(
            path, "pep3147" if sourceless == "pep3147_everything" else "simple"
        )


def make_sourceless(path, style):
    import py_compile

    py_compile.compile(path)

    if style == "simple":
        pyc_path = util.pyc_file_from_path(path)
        suffix = importlib.machinery.BYTECODE_SUFFIXES[0]
        filepath, ext = os.path.splitext(path)
        simple_pyc_path = filepath + suffix

View on GitHub (pinned to 5551b5d35f)

Solutions

  1. Ensure the new content's down_revision matches the original script's down_revision exactly.
  2. 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.
  3. Diff the content string against the original script to spot the down_revision mismatch.
  4. Extract the down_revision from the original script programmatically and inject it into the new content.

Example fix

# before
write_script(scriptdir, 'abc123', dedent('''
    revision = 'abc123'
    down_revision = 'wrong_id'  # differs from original
    def upgrade(): pass
'''))

# after
original = scriptdir.revision_map.get_revision('abc123')
write_script(scriptdir, 'abc123', dedent(f'''
    revision = 'abc123'
    down_revision = '{original.down_revision}'
    def upgrade(): pass
'''))
Defensive patterns

Strategy: validation

Validate before calling

def validate_write_script(scriptdir, rev_id, new_content):
    old = scriptdir.revision_map.get_revision(rev_id)
    # parse down_revision from new_content and compare
    import ast
    tree = ast.parse(new_content)
    new_down = None
    for node in ast.walk(tree):
        if isinstance(node, ast.Assign):
            for t in node.targets:
                if isinstance(t, ast.Name) and t.id == 'down_revision':
                    new_down = ast.literal_eval(node.value)
    if old.down_revision != new_down:
        raise ValueError(f"down_revision mismatch: {old.down_revision} != {new_down}")
    return True

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of sqlalchemy/alembic@5551b5d35f (2026-08-11). Data as JSON: /api/errors/0917efe27d98012a. Report an issue: GitHub.