Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

sqla-1to2-codemod

A conservative command-line codemod that upgrades legacy SQLAlchemy 1.x code to modern 2.0 style — it rewrites the patterns that are provably safe and flags the rest with actionable guidance, so you never end up with silently-broken code.

By default it prints a unified diff (a dry run) and changes nothing. Every finding is tagged with a stable rule id and a one-line explanation, and unrewritable patterns are reported instead of guessed at.

Before → after

Given this legacy module:

from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()

def get_user(session, user_id):
    return session.query(User).get(user_id)

def all_users(session):
    return session.query(User).all()

sqla-1to2-codemod app.py --write produces:

from sqlalchemy.orm import declarative_base
from sqlalchemy import select

Base = declarative_base()

def get_user(session, user_id):
    return session.get(User, user_id)

def all_users(session):
    return session.execute(select(User)).scalars().all()

Rewrites preserve your formatting and comments — the tool is built on LibCST, a concrete syntax tree, not a regex pass.

Install

The tool is distributed from GitHub (it is not on PyPI):

pip install "git+https://github.com/async-workflows/sqla-1to2-codemod.git"

It requires Python 3.10+ and installs LibCST as its only runtime dependency.

Usage

Scan a path (dry run — prints a diff of proposed changes and a grouped summary of flags, exit code 0):

sqla-1to2-codemod path/to/project/
Flag Effect
(none) Dry run: print a unified diff plus a grouped flag summary, exit 0.
--write Apply the safe rewrites in place.
--check Exit non-zero if any legacy pattern is found (for CI gates).
--rules SA001,SA003 Run only the listed rules.
--json Emit a machine-readable report: a list of {file, line, rule_id, summary, fixable}.
--explain SA004 Print a rule's full explanation and documentation link, then exit.
--list-rules List every available rule and exit.
--version Print the version and exit.

You can pass multiple files or directories; directories are searched recursively for .py files.

Rules

Rule Pattern Action
SA001 from sqlalchemy.ext.declarative import declarative_base Auto-fixfrom sqlalchemy.orm import declarative_base
SA002 from sqlalchemy.ext.declarative import declared_attr Auto-fixfrom sqlalchemy.orm import declared_attr
SA003 session.query(Model).get(pk) Auto-fixsession.get(Model, pk); Model.query.get(pk) is flagged
SA004 session.query(...) Auto-fix the simple .all() shape → session.execute(select(...)).scalars().all(); richer chains are flagged
SA005 Query.filter(...) / filter_by(...) Flag — migrate to select().where(...)
SA006 engine.execute(...) / conn.execute("raw sql") Flag — use a connection + text() in an explicit transaction
SA007 x = Column(...) in a model Flag — adopt x: Mapped[...] = mapped_column(...)

Run sqla-1to2-codemod --explain SA00X for the full rationale and a link to the relevant migration guide.

Continuous integration

Use --check to fail a build when legacy patterns remain. It exits non-zero as soon as any rule matches, so it doubles as a ratchet once a module is migrated:

# .github/workflows/ci.yml
- name: Check for legacy SQLAlchemy 1.x patterns
  run: |
    pip install "git+https://github.com/async-workflows/sqla-1to2-codemod.git"
    sqla-1to2-codemod src/ --check

Prefer machine-readable output? sqla-1to2-codemod src/ --json emits one JSON object per finding for your own dashboards or review bots.

Limitations and safety

  • Rewrites are deliberately narrow. Only shapes that are provably behaviour- preserving are changed (import relocations, Query.get(), and the single-entity session.query(Model).all()). Everything else is flagged so a human keeps control.
  • SA004 only rewrites the trivial .all() case. Any filters, ordering, joins, column selections, or terminal methods such as .first() / .one() are reported, not auto-migrated.
  • SA007 never rewrites. Turning Column(Integer) into Mapped[int] would flip a column from nullable to NOT NULL, since 2.0 infers nullability from the annotation, so the codemod recommends the change but leaves the exact annotation to you.
  • Heuristics can over-flag. SA006 keys off receiver names (engine, conn, connection) and string-literal arguments, so a non-SQLAlchemy cursor.execute("...") may be flagged. Review flags before acting on them.
  • Run it in dry-run mode first, review the diff, then re-run with --write. Rewrites are idempotent — running twice makes no further changes.

Further reading

Deeper background on the migration these rules automate:

License

MIT © 2026 async-workflows

About

AST-based codemod that upgrades legacy SQLAlchemy 1.x code to 2.0 style — Query→select(), relocated imports, session.get(), with dry-run diffs and a per-change explanation for every rewrite.

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages