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

  1. Build the op via DropColumnOp.from_column_and_tablename(schema, table, column) so _reverse is attached.
  2. 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.
  3. 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

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


AI-assisted analysis of sqlalchemy/alembic@44fb345033 (2026-08-04). Data as JSON: /data/errors/b6643d8f53aa7bec.json. Report an issue: GitHub.