Complete worked example
python
from sqlalchemy import create_engine, MetaData, Table, Column, Integer, CheckConstraint, inspect, select, update
from alembic.migration import MigrationContext
from alembic.operations import Operations
def inspect_example(scores):
engine = create_engine("sqlite+pysqlite://", connect_args={"autocommit": False})
try:
metadata = MetaData()
roles = Table("roles", metadata, Column("id", Integer, primary_key=True),
Column("score", Integer, nullable=True),
CheckConstraint("score >= 0", name="ck_roles_score"))
metadata.create_all(engine)
with engine.begin() as connection:
if scores:
connection.execute(roles.insert(), [{"id": i, "score": s} for i, s in enumerate(scores, 1)])
connection.execute(update(roles).where(roles.c.score.is_(None)).values(score=0))
operation = Operations(MigrationContext.configure(connection))
with operation.batch_alter_table("roles", recreate="always") as batch:
batch.alter_column("score", existing_type=Integer(), nullable=False)
inspector = inspect(connection)
score_column = next(c for c in inspector.get_columns("roles") if c["name"] == "score")
return {"scores": list(connection.scalars(select(roles.c.score).order_by(roles.c.id))),
"nullable": score_column["nullable"],
"checks": sorted(c["name"] for c in inspector.get_check_constraints("roles")),
"indexes": sorted(i["name"] for i in inspector.get_indexes("roles"))}
finally:
engine.dispose()- Reasoning
- The program inserts supplied scores, backfills NULL to zero and enters batch_alter_table with explicit recreation. alter_column changes the score’s nullability while retaining its integer type. The final report reads actual stored scores, the inspected nullable flag, named CHECK constraints and indexes. For [5, None, 2], the expected scores are [5, 0, 2], nullable is False and ck_roles_score still exists. These observations are independent: preserving values alone would not prove that the new column rule or named constraint survived, and a correct schema alone would not prove that rows were copied correctly.