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 + suffixView on GitHub (pinned to 5551b5d35f)
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.
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 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.
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
- 'type' can be one of %s
- Invalid plugin expression {name!r}
- Character(s) '%s' not allowed in revision identifier '%s'
- Self-loop is detected in revisions (%s)
- Dependency self-loop is detected in revisions (%s)
AI-assisted analysis of sqlalchemy/alembic@5551b5d35f (2026-08-11).
Data as JSON: /api/errors/0917efe27d98012a.
Report an issue: GitHub.