sqlalchemy/alembic · error · ValueError
operation is not reversible; original column is not present
Error message
operation is not reversible; original column is not present
What it means
Raised by DropColumnOp.reverse() when self._reverse is None. reverse() needs the original AddColumnOp (captured by from_column_and_tablename) to rebuild an add-column operation for downgrade. A DropColumnOp built directly from just (table, column_name) carries no column definition and cannot be inverted, so reversal is refused.
Source
Thrown at alembic/operations/ops.py:2310
super().__init__(table_name, schema=schema)
self.column_name = column_name
self.kw = kw
self.if_exists = if_exists
self._reverse = _reverse
def to_diff_tuple(
self,
) -> tuple[str, str | None, str, Column[Any]]:
return (
"remove_column",
self.schema,
self.table_name,
self.to_column(),
)
def reverse(self) -> AddColumnOp:
if self._reverse is None:
raise ValueError(
"operation is not reversible; "
"original column is not present"
)
op = AddColumnOp.from_column_and_tablename(
self.schema, self.table_name, self._reverse.column
)
op.if_not_exists = self.if_exists
return op
@classmethod
def from_column_and_tablename(
cls,
schema: str | None,
tname: str,
col: Column[Any],
) -> DropColumnOp:
return cls(View on GitHub (pinned to 44fb345033)
Solutions
- Build the op via DropColumnOp.from_column_and_tablename(schema, table, column) so _reverse is attached.
- Use op.drop_column('t','c') for forward execution (reverse() is only needed when generating downgrades), and provide the column metadata when reversibility is required.
- In custom rewriter hooks, attach the AddColumnOp reverse before relying on .reverse().
Example fix
// before
op_obj = DropColumnOp('user', 'email')
op_obj.reverse() # raises ValueError
// after
from sqlalchemy import Column, String
op_obj = DropColumnOp.from_column_and_tablename(
None, 'user', Column('email', String(50))
)
op_obj.reverse() # ok, produces AddColumnOp Defensive patterns
Strategy: validation
Validate before calling
from alembic.operations.ops import DropColumnOp
from sqlalchemy import Column
def build_reversible_drop_col(schema, table, col: Column):
return DropColumnOp.from_column_and_tablename(schema, table, col) Type guard
from alembic.operations.ops import DropColumnOp
def drop_col_is_reversible(op_obj: DropColumnOp) -> bool:
return getattr(op_obj, '_reverse', None) is not None Try / catch
try:
op_obj.reverse()
except ValueError as e:
if 'operation is not reversible' in str(e):
op_obj = DropColumnOp.from_column_and_tablename(schema, table, column)
else:
raise Prevention
- Use from_column_and_tablename() so drop-column ops carry reverse info.
- Provide full Column metadata when downgrade generation is required.
- Don't construct DropColumnOp from a bare name in reversible contexts.
When it happens
Trigger: Constructing DropColumnOp('t', 'c') directly and calling .reverse() on it; the autogenerate reverser attempting to invert a manually-created drop-column op; a process_revision_directives hook building drop ops without column metadata.
Common situations: Custom migration tooling that creates DropColumnOp from a name string rather than from a Column object; downgrade generation hitting an op that lacks reverse info.
Related errors
- constraint cannot be produced; original constraint is not pr
- This MigrationScript instance has a multiple-entry list for
- This MigrationScript instance has a multiple-entry list for
- 'type' can be one of %s
- Can't drop table in batch mode
AI-assisted analysis of sqlalchemy/alembic@44fb345033 (2026-08-04).
Data as JSON: /data/errors/b6643d8f53aa7bec.json.
Report an issue: GitHub.