Sstormforge942

sqlalchemy-v1-to-v2

SQLAlchemy 1.x to 2.0 migration (import moves, select() positional to generative, Column to mapped_column, session.query chains, DML generative, raw string execute, declarative_base)

transformationmigrationsqlalchemyormpythonv1v2database
Public
0 executions

Run locally

npx codemod sqlalchemy-v1-to-v2

sqlalchemy-v1-to-v2

Migrates SQLAlchemy 1.x codebases to the 2.0 API surface.

Covers the full migration checklist from the official SQLAlchemy 2.0 migration guide: import moves, query API, execution model, ORM mapping, and deprecated keyword removal.

What it does

Fully automated rewrites

1. select([...])select(...)

BeforeAfter
select([User.id, User.name])select(User.id, User.name)
select([column("id")])select(column("id"))

2. session.query() chains → session.execute(select())

BeforeAfter
session.query(User).filter(User.active).all()session.execute(select(User).where(User.active)).scalars().all()
session.query(User).get(42)session.get(User, 42)
session.query(User).count()session.execute(select(func.count()).select_from(User)).scalar()
session.query(User).filter_by(name="x").first()session.execute(select(User).filter_by(name="x")).scalars().first()
session.query(User).order_by(User.id).limit(10).all()session.execute(select(User).order_by(User.id).limit(10)).scalars().all()

Supported middle methods: .filter(), .filter_by(), .order_by(), .limit(), .offset(), .join(), .distinct().

Supported terminal methods: .all(), .first(), .one(), .one_or_none(), .scalar(), .get(), .count().

3. conn.execute("SQL")conn.execute(text("SQL"))

BeforeAfter
conn.execute("SELECT 1")conn.execute(text("SELECT 1"))
engine.execute("INSERT ...")engine.execute(text("INSERT ..."))

4. Column(...)mapped_column(...) in declarative classes

BeforeAfter
id = Column(Integer, primary_key=True)id = mapped_column(Integer, primary_key=True)
name = Column(String(50), nullable=False)name = mapped_column(String(50), nullable=False)

Only transforms Column inside class bodies inheriting from Base. Column inside Table() definitions is left unchanged.

5. DML inline where: table.update(cond)table.update().where(cond)

BeforeAfter
users.update(users.c.id == 1).values(name="x")users.update().where(users.c.id == 1).values(name="x")
users.delete(users.c.active == False)users.delete().where(users.c.active == False)

6. Import rewriting (ext.declarativeorm)

BeforeAfter
from sqlalchemy.ext.declarative import declarative_basefrom sqlalchemy.orm import declarative_base
from sqlalchemy.ext.declarative import declared_attrfrom sqlalchemy.orm import declared_attr
from sqlalchemy.ext.declarative import as_declarativefrom sqlalchemy.orm import as_declarative

Also manages adding new imports (text, select, func, mapped_column) and removing unused ones (Column) as needed by other transforms.

7. case([(cond, val), ...])case((cond, val), ...)

BeforeAfter
case([(User.role == "admin", 1), (User.role == "user", 2)], else_=0)case((User.role == "admin", 1), (User.role == "user", 2), else_=0)

Auto-fix + TODO flag

These transforms apply a code change and insert a # TODO(sqlalchemy-v2): comment for manual review.

8. MetaData(bind=engine) → remove bind=

BeforeAfter
meta = MetaData(bind=engine)# TODO(sqlalchemy-v2): ...<br>meta = MetaData()
meta = MetaData(bind=engine, schema="public")# TODO(sqlalchemy-v2): ...<br>meta = MetaData(schema="public")

Bound metadata is removed in 2.0. Connections must be passed explicitly.

9. Session(autocommit=True) → remove autocommit=

BeforeAfter
Session(autocommit=True)# TODO(sqlalchemy-v2): ...<br>Session()
sessionmaker(autocommit=True, bind=engine)# TODO(sqlalchemy-v2): ...<br>sessionmaker(bind=engine)

Autocommit mode is removed in 2.0. Use AUTOCOMMIT isolation level or explicit begin() blocks.

10. lazy="dynamic"lazy="write_only"

BeforeAfter
relationship("Address", lazy="dynamic")relationship("Address", lazy="write_only")

11. session.begin(subtransactions=True) → remove kwarg

BeforeAfter
session.begin(subtransactions=True)# TODO(sqlalchemy-v2): ...<br>session.begin()

Subtransactions are removed. Restructure to a single begin/commit block.

12. Table(autoload=True)Table(autoload_with=engine)

BeforeAfter
Table("users", meta, autoload=True)# TODO(sqlalchemy-v2): ...<br>Table("users", meta, autoload_with=engine)

autoload_with=engine is the required replacement. The engine placeholder must be replaced with the actual engine reference.

TODO flag only

These patterns require structural refactoring that cannot be safely automated. The codemod inserts a # TODO(sqlalchemy-v2): comment above each occurrence.

13. engine.execute() — removed

python

14. declarative_base() — superseded

python

15. mapper() — removed

python

16. from_self() / select_entity_from() — removed

python

17. String-based loader / join arguments

python

Covers: joinedload, subqueryload, selectinload, lazyload, immediateload, noload, raiseload, contains_eager, defaultload, with_parent, and .join() with string args.

18. execution_options(autocommit=True) — removed

python

Patterns intentionally left unchanged (safety)

PatternWhy it's skipped
session.query().join(User, User.id == Addr.user_id).join() with explicit ON clause — already v2-compatible
Column(...) inside Table(...) definitionsColumn is still valid in Core table definitions
session.query() chains with unsupported methods.group_by(), .having(), .union(), etc. — too complex to safely rewrite
registry.mapper()Method call on registry — not the standalone mapper()
Already-v2 code (select(User), mapped_column(...))No-op — safe to run repeatedly

Usage

From the registry

bash

Dry run (preview changes)

bash

Manual follow-up after running

  1. Search for # TODO(sqlalchemy-v2): — address each flagged location manually
  2. engine placeholder — replace autoload_with=engine with your actual engine variable
  3. DeclarativeBase — migrate from declarative_base() to class Base(DeclarativeBase): pass
  4. Session / connection lifecycle — adopt the with Session(engine) as session: / with engine.connect() as conn: context manager pattern
  5. Alembic migrations — these are excluded by default; update migration scripts separately if needed
  6. Type annotations — consider adding Mapped[T] type annotations to mapped columns
  7. Run your test suite — verify all queries produce the same results

Metrics

MetricDescription
select-list-to-positionalselect([...])select(...)
query-chain-to-selectsession.query() chains rewritten
execute-raw-stringRaw string execute() wrapped in text()
column-to-mapped-columnColumnmapped_column in classes
dml-inline-whereDML inline condition → .where()
import-rewriteext.declarative imports moved to orm
case-list-to-positionalcase([...])case(...)
metadata-bind-removalMetaData(bind=) kwarg removed
session-autocommit-removalSession(autocommit=) kwarg removed
lazy-dynamic-to-write-onlylazy="dynamic"lazy="write_only"
subtransactions-removalsubtransactions=True kwarg removed
autoload-to-autoload-withautoload=Trueautoload_with=engine
flag-engine-executeengine.execute() flagged
flag-declarative-basedeclarative_base() flagged
flag-mappermapper() flagged
flag-from-selffrom_self() / select_entity_from() flagged
flag-string-loaderString-based loader/join args flagged
flag-execution-optionsexecution_options(autocommit=True) flagged

License

MIT

Ready to contribute?

Build your own codemod and share it with the community.