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

  1. Create the op with the column info: DropColumnOp.from_column_and_tablename(schema, 't', column_obj) so _reverse is populated.
  2. When reversing a list of ops, ensure each DropColumnOp originated from from_column_and_tablename() or carried an AddColumnOp as _reverse.
  3. 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

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


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)