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 as ValueError by DropColumnOp.reverse() (ops.py:2308-2313) when _reverse is None. To reverse a column drop, Alembic must re-add the original column, which requires the original Column object captured at construction time via from_column_and_tablename(). A DropColumnOp built without that reverse reference cannot produce an AddColumnOp.
Solutions
- Create the op with the column info: DropColumnOp.from_column_and_tablename(schema, 't', column_obj) so _reverse is populated.
- When reversing a list of ops, ensure each DropColumnOp originated from from_column_and_tablename() or carried an AddColumnOp as _reverse.
- Guard with `if op._reverse is not None:` before calling .reverse() in generic loop code.
Example fix
// before
op = ops.DropColumnOp('users', 'email')
downgrade = op.reverse() # raises
// after
op = ops.DropColumnOp.from_column_and_tablename(
None, 'users', sa.Column('email', sa.String(50))
)
downgrade = op.reverse() # returns AddColumnOp Defensive patterns
Strategy: validation
Validate before calling
# Build DropColumnOp with the original column so .reverse() works.
from alembic.operations.ops import DropColumnOp
import sqlalchemy as sa
def reversible_drop(schema, table, col):
return DropColumnOp.from_column_and_tablename(schema, table, col)
op = reversible_drop(None, 'users', sa.Column('email', sa.String(50)))
downgrade = op.reverse() # safe, returns AddColumnOp Type guard
from alembic.operations.ops import DropColumnOp
def is_reversible(op: DropColumnOp) -> bool:
return getattr(op, '_reverse', None) is not None Prevention
- Create DropColumnOp via from_column_and_tablename() so the original column is captured.
- In autogenerate rewriters, only call .reverse() when _reverse is set.
- Keep the original Column object available when building reversible ops.
When it happens
Trigger: Constructing ops.DropColumnOp('t', 'c') directly (no _reverse) and then calling .reverse() on it — e.g. when building a downgrade from an upgrade op list programmatically. from_column_and_tablename(schema, tname, col) sets _reverse, but the bare constructor does not.
Common situations: Custom autogenerate renderers or rewriters that drop columns by constructing the op manually; calling .reverse() during downgrade generation before the original column metadata was attached.
Related errors
- constraint cannot be produced; original constraint is not…
- Can't drop table in batch mode
- Connection, url, or dialect_name is required.
- Constraint must have a name
- No context has been configured yet.
AI-assisted analysis of sqlalchemy/alembic@5551b5d35f (2026-08-11).
Data as JSON: /api/errors/b6643d8f53aa7bec.
Report an issue: GitHub.
Appendix: 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 5551b5d35f)