pytest-alembic test_model_definitions_match_ddl fails on raw SQL migration with functional unique index
pytest-alembic's built-in test_model_definitions_match_ddl started failing after adding a table whose migration was written in raw SQL with a functional unique index: CREATE UNIQUE INDEX uq_invite_email ON invite (parent_id, lower(email)). The failure says the models are out of sync and shows the autogenerated diff: op.drop_index(op.f('uq_invite_email'), table_name='invite'). The SQLAlchemy declarative model declared all columns correctly; only the expression index had no model-side declaration, since mapped_column can't express lower(email) and the columns aren't bound names at class-body time in the declarative-dataclass style. Expected alembic autogenerate to ignore indexes it can't introspect; instead it proposes dropping them.
Alembic autogenerate compares reflected DDL against Base.metadata. A functional/expression index created only in raw migration SQL exists in the database but not in the metadata, so autogenerate emits drop_index for it — and pytest-alembic's test_model_definitions_match_ddl treats any non-empty autogenerated revision as failure.
Two fixes:
- Declare the expression index on the model so metadata matches the DDL:
from sqlalchemy import Index, text
class Invite(Base):
__tablename__ = "invite"
__table_args__ = (
Index("uq_invite_email", "parent_id", text("lower(email)"), unique=True),
)String column names work positionally, and text() carries the expression. Caveat: expression comparison in autogenerate is version-sensitive (older alembic can still flag a diff between text("lower(email)") and the reflected expression), so verify the drift test passes on your alembic version.
- If the expression only exists to case-normalize, drop the expression index entirely: use a plain
UniqueConstraint("parent_id", "email")plus lowercasing at the single write path. This is fully representable in metadata, immune to expression-comparison quirks, and equivalent as long as writes are normalized (document that invariant at the write path).
Either way, the invariant to remember: with a model-vs-DDL drift test in CI, every index/constraint written in raw migration SQL must have an exact metadata-side counterpart, including functional indexes.