Skip to content
Web apps · Data

SQLAlchemy 2 guard against 1.x and legacy Query code

Stop agents writing engine.execute, select([...]), row["name"] and session.query into SQLAlchemy 2 projects, and catch writes that are never committed.

Verified install Not agent-tested v1.0.0 MIT updated sqlalchemypythonpytest
Equips
  • 2 2 Rules
  • 1 1 Skills
  • – 0 MCP

SQLAlchemy 2.0 has been out since January 2023, but agents still write 1.x code: engine.execute("..."), plain SQL strings, select([...]), row["name"], MetaData(bind=), autocommit=True, and untyped Column models behind declarative_base(). The removed APIs fail at once. session.query() keeps running as the legacy Query API, so it spreads. The costly change is silent: 2.x has no autocommit, so an insert inside engine.connect() with no commit is rolled back when the block ends, and nothing reports it.

This loadout pairs a short Cursor rule and AGENTS.md (a never/always table) with a skill that checks the work. A standard-library Python scanner reports removed 1.x APIs and legacy Query calls as errors, and untyped columns, backref, lazy="dynamic" and writes inside connect() as warnings. Then the tests run with SADeprecationWarning raised as an error and read every write back in a new session. The verifier runs both against a 2.0-style app on the pinned SQLAlchemy 2.1.6 and a 1.x sample, and shows SQLAlchemy refusing the sample's removed calls and rolling back its uncommitted write.

What's in it

  • Never/always table for 2.0-style SQLAlchemy in a .py-scoped Cursor rule
  • Scanner flags removed 1.x APIs and legacy session.query as errors
  • Warns on writes inside engine.connect() that are never committed
  • Typed Mapped models, back_populates and WriteOnlyMapped instead of legacy forms
  • Tests run with SADeprecationWarning as an error and read writes back

You'll need

  • Tools: python3, uv
Level 1 · automated

Verification

Verified install

Checked at commit 783d9eb · 9 passed, 0 failed, 1 not applicable

CheckResultWhat we found
Listinglisting.yaml matches the schema and declares every file in the folder. Pass
Schema valid; 8 files declared, none missing or extra
  • tested.status: untested (agent tests are separate from Level 1)
Files parseJSON parses; SKILL.md and .mdc front matter is valid YAML with the fields each agent needs; code fences are closed. Pass All 8 files parse; front matter valid
AdaptersCLAUDE.md imports @AGENTS.md, GEMINI.md imports @./AGENTS.md, and both match the generator. Pass
CLAUDE.md and GEMINI.md import AGENTS.md and are up to date
  • CLAUDE.md: matches the generator
  • GEMINI.md: matches the generator
Install pathsEvery file lands where each agent documents it, with no collisions, and the laid-out project is re-discovered correctly. Pass
Correct paths for all 6 agents; layouts re-discovered
  • Claude Code: 6 files, 1 skill in .claude/skills/
  • Codex CLI: 5 files, 1 skill in .agents/skills/
  • Cursor: 6 files, 1 skill in .agents/skills/, 1 rule
  • Gemini CLI: 6 files, 1 skill in .agents/skills/
  • Antigravity CLI: 5 files, 1 skill in .agents/skills/
  • GitHub Copilot: 5 files, 1 skill in .agents/skills/
MCP configmcp.json is structurally valid, converts to each agent's format, declares no literal secrets, and referenced packages exist on npm/PyPI. Not applicable No MCP servers in this loadout
Safety scanThe intake scanner finds no credentials, risky shell, hidden Unicode, instructions hidden in HTML comments or after long blank padding, or prompt-injection and covert wording (such as telling the agent to keep something from you), and the listing's network flag is honest. Pass
Clean: no credentials, risky shell, hidden text or covert instructions
  • Scanner intake-precheck/2: verdict clean; network no (listing says yes)
ReferencesLinks and file paths between the loadout's files resolve, including for each agent's installed layout. Pass 2 references between files resolve for every agent
Size limitsFile count and sizes are within the submission limits; SKILL.md bodies stay under 500 lines. Pass
8 files, 15.7 KB total; largest skills/sqlalchemy-2-guard/scripts/sqlalchemy2_guard.py 6.1 KB
  • Limits: 30 files, 32 KB per file, 128 KB total, 500 lines per skill or rule
LicenceA recognised SPDX licence, with the licence text available. Pass MIT, by SetupBay; text in the repository LICENSE
Starter buildA minimal fixture project with the loadout applied installs and builds or type-checks, and the loadout's own code examples compile. Pass
sqlalchemy2_guard.py passes a 2.0-style data layer and flags every 1.x line in the skill's sample: 10 removed or legacy calls as errors, 7 warnings including a write inside engine.connect(). The pinned SQLAlchemy 2.1.6 runs the app's tests with SADeprecationWarning as an error, refuses the sample's
  • Applied 12 loadout files at every agent's paths
  • ✓ scanner: the 2.0-style app and tests are clean: bash -c python3 .agents/skills/sqlalchemy-2-guard/scripts/sqlalchemy2_guard.py app tests; echo "exit=$?"
  • ✓ scanner: the 1.x sample is flagged line by line: bash -c mkdir -p /tmp/sqlalchemy1-sample && cp .claude/skills/sqlalchemy-2-guard/references/sqlalchemy1-sample.py.txt /tmp/sqlalchemy1-sample/models1.py && python3 .claude/skills/sqlalchemy-2-guard/scripts/sqlalchemy2_guard.py /tmp/sqlalchemy1-sample; echo "exit=$?"
  • ✓ pytest on SQLAlchemy 2.1.6 with SADeprecationWarning as an error: uv run --python 3.12 pytest -q -p no:cacheprovider -W error::sqlalchemy.exc.SADeprecationWarning
  • ✓ SQLAlchemy 2.1.6 on the sample's 1.x calls: uv run --python 3.12 python legacy_probe.py
  • Covers: The scanner's rules for removed 1.x APIs, the legacy Query API and 1.x model style, against a real 1.x module
  • Covers: No false positives on typed DeclarativeBase models with back_populates, lazy='raise', selectinload, session.get, text() with bound parameters and engine.begin()
  • Covers: SQLAlchemy 2.1.6 as the second check: removed calls are refused, Query.get() warns LegacyAPIWarning (an error in the tests), and an uncommitted write in connect() is rolled back
  • Covers: Not covered: Postgres-specific behaviour and Alembic migrations (the fixture uses in-memory SQLite)

What this means: a script checked the files themselves: they parse, they install where each agent looks, any MCP config is sound, the safety scan is clean, references between files resolve, and, where a free and repeatable fixture exists, a small starter project builds with the loadout applied. No AI agent was run. Per-agent results from agent runs are separate and stay untested until an agent actually runs.

Install

Choose your agent. Run the commands from your project root, or download a zip already laid out in the right folders.

Claude Code reads CLAUDE.md (imports AGENTS.md with @AGENTS.md) and skills from .claude/skills/.

Download zip
curl -fsSL --create-dirs -o AGENTS.md https://setupbay.com/raw/sqlalchemy-2-style-guard/1.0.0/AGENTS.md
curl -fsSL --create-dirs -o CLAUDE.md https://setupbay.com/raw/sqlalchemy-2-style-guard/1.0.0/CLAUDE.md
curl -fsSL --create-dirs -o .claude/skills/sqlalchemy-2-guard/SKILL.md https://setupbay.com/raw/sqlalchemy-2-style-guard/1.0.0/skills/sqlalchemy-2-guard/SKILL.md
curl -fsSL --create-dirs -o .claude/skills/sqlalchemy-2-guard/scripts/sqlalchemy2_guard.py https://setupbay.com/raw/sqlalchemy-2-style-guard/1.0.0/skills/sqlalchemy-2-guard/scripts/sqlalchemy2_guard.py
curl -fsSL --create-dirs -o .claude/skills/sqlalchemy-2-guard/references/migration-table.md https://setupbay.com/raw/sqlalchemy-2-style-guard/1.0.0/skills/sqlalchemy-2-guard/references/migration-table.md
curl -fsSL --create-dirs -o .claude/skills/sqlalchemy-2-guard/references/sqlalchemy1-sample.py.txt https://setupbay.com/raw/sqlalchemy-2-style-guard/1.0.0/skills/sqlalchemy-2-guard/references/sqlalchemy1-sample.py.txt
FileGoes to
AGENTS.mdAGENTS.md
CLAUDE.mdCLAUDE.md
skills/sqlalchemy-2-guard/SKILL.md.claude/skills/sqlalchemy-2-guard/SKILL.md
skills/sqlalchemy-2-guard/scripts/sqlalchemy2_guard.py.claude/skills/sqlalchemy-2-guard/scripts/sqlalchemy2_guard.py
skills/sqlalchemy-2-guard/references/migration-table.md.claude/skills/sqlalchemy-2-guard/references/migration-table.md
skills/sqlalchemy-2-guard/references/sqlalchemy1-sample.py.txt.claude/skills/sqlalchemy-2-guard/references/sqlalchemy1-sample.py.txt

Already have an AGENTS.md or CLAUDE.md? The commands above overwrite it — download the file instead and merge the sections you want. Which file wins when several are present: what Claude Code reads.

Or as a Claude Code plugin

Adds the skill to Claude Code for all your projects, without copying files. Plugins can't carry project rules, so add AGENTS.md and CLAUDE.md with the commands above. About the plugin marketplace.

/plugin marketplace add https://setupbay.com/marketplace.json
/plugin install sqlalchemy-2-style-guard@setupbay

Files

Rules AGENTS.md SQLAlchemy 2 version facts, rules and the done checklist. 33 lines
Download Raw
# AGENTS.md: SQLAlchemy 2 project (1.x and legacy Query guard)

This project uses **SQLAlchemy 2.x** (`sqlalchemy>=2.1,<3`) in **2.0 style**: typed `Mapped[]` models,
`select()` statements and explicit transactions. Most SQLAlchemy code you have seen is 1.x. Some of it was removed,
some still runs as the "legacy" Query API, and one change loses writes without an error.

## Version facts
- Check the pin first: `uv pip show sqlalchemy`.
- Removed in 2.0: `engine.execute()`, plain SQL strings in `execute()` (use `text()`), `select([...])`,
  `MetaData(bind=)`, `autocommit=True`, and string keys on rows (`row["name"]`).
- No autocommit: `with engine.connect() as conn:` rolls back anything you didn't `conn.commit()`. Use
  `with engine.begin() as conn:` for writes. Sessions work the same way: `with Session(engine) as s, s.begin():`.
- `session.query(...)` still runs but is legacy. New code uses `session.scalars(select(...))` / `session.execute(...)`.
- If you're not sure of an API, check the 2.0 migration guide (https://docs.sqlalchemy.org/en/21/changelog/migration_20.html)
  instead of copying 1.x answers.

## Rules
- Models: `class Base(DeclarativeBase): pass`, then `name: Mapped[str] = mapped_column(String(80))`.
  `Mapped[str | None]` for nullable columns.
- Relationships: `relationship(back_populates=...)` on both sides. Use `lazy="raise"` or `selectinload` when N+1
  queries matter. Use `WriteOnlyMapped` for big collections, not `lazy="dynamic"`.
- Queries: `select(User).where(User.name == name)`, then `session.scalars(stmt).all()` or `.one_or_none()`.
  Use `session.get(User, pk)` for primary keys.
- Raw SQL only through `text("... :name")` with bound parameters. Never use f-strings.
- Writes happen inside `engine.begin()` or `session.begin()`, or are followed by an explicit commit.
- Don't upgrade or downgrade SQLAlchemy without asking the human.

## Before you say "done"
1. Run the `sqlalchemy-2-guard` scanner on the files you touched. It must report `0 error(s)`. Read every `warn`.
2. Run the tests with SQLAlchemy's own warnings as errors:
   `uv run pytest -q -W error::sqlalchemy.exc.SADeprecationWarning`.
3. For every write path you touched, say which test shows the row is really there afterwards.
Adapter CLAUDE.md Imports AGENTS.md for Claude Code. 11 lines
Download Raw
# CLAUDE.md

@AGENTS.md

## Claude Code notes

- Skills from this loadout live in `.claude/skills/`: `sqlalchemy-2-guard`. Use them when their description matches the task.
- Follow AGENTS.md above; where this file and AGENTS.md disagree, AGENTS.md wins.

<!-- From SetupBay "SQLAlchemy 2 guard against 1.x and legacy Query code" v1.0.0 — https://setupbay.com/loadouts/sqlalchemy-2-style-guard/ -->
Adapter GEMINI.md Imports AGENTS.md for Gemini CLI. 11 lines
Download Raw
# GEMINI.md

@./AGENTS.md

## Gemini CLI notes

- Skills from this loadout live in `.agents/skills/`: `sqlalchemy-2-guard`. Use them when their description matches the task.
- Follow AGENTS.md above; where this file and AGENTS.md disagree, AGENTS.md wins.

<!-- From SetupBay "SQLAlchemy 2 guard against 1.x and legacy Query code" v1.0.0 — https://setupbay.com/loadouts/sqlalchemy-2-style-guard/ -->
Cursor rule .cursor/rules/sqlalchemy-2-style.mdc Never/always table for Python files. 23 lines
Download Raw
---
description: SQLAlchemy 2.0 style only; no removed 1.x APIs or legacy Query code
globs: "**/*.py"
alwaysApply: false
---

This project uses SQLAlchemy 2.x in 2.0 style. There is no autocommit.

| Never (1.x / legacy) | Always (2.0 style) |
|---|---|
| `engine.execute("select ...")` | `with engine.begin() as conn: conn.execute(text("..."), params)` |
| `conn.execute("raw sql")` | `conn.execute(text("... :id"), {"id": 1})` |
| `select([User.id])` | `select(User.id)` |
| `session.query(User).filter_by(name=n).all()` | `session.scalars(select(User).where(User.name == n)).all()` |
| `session.query(User).get(1)` | `session.get(User, 1)` |
| `row["name"]` | `row.name` or `row._mapping["name"]` |
| `declarative_base()`, `id = Column(Integer, ...)` | `class Base(DeclarativeBase)`, `id: Mapped[int] = mapped_column(primary_key=True)` |
| `relationship(backref="x")`, `lazy="dynamic"` | `back_populates=` both sides, `WriteOnlyMapped` |
| `MetaData(bind=engine)`, `autocommit=True` | pass the engine; `with Session(engine) as s, s.begin():` |
| writes inside `engine.connect()` with no commit | `engine.begin()` |

- After editing, run the `sqlalchemy-2-guard` skill's scanner and the tests.
Skill skills/sqlalchemy-2-guard/SKILL.md Scan for 1.x and legacy SQLAlchemy code, then run the tests with deprecations as errors. 41 lines
Download Raw
---
name: sqlalchemy-2-guard
description: Check Python for SQLAlchemy 1.x APIs removed in 2.0, legacy session.query code, untyped models and writes that are never committed, before saying a database change is done. Use after writing or editing SQLAlchemy models, queries or transactions, when SQLAlchemy raises ObjectNotExecutableError or ArgumentError, or when porting 1.x snippets.
---

# SQLAlchemy 2 guard

SQLAlchemy 2.0 came out in January 2023, and agents still write 1.x: `engine.execute("...")`, `select([...])`,
`row["name"]`, `session.query()`, `declarative_base()` with untyped `Column`s. The removed APIs fail at once.
`session.query()` keeps working as legacy code. The dangerous change is quiet: there is no autocommit, so a write
done inside `engine.connect()` without `commit()` is rolled back when the block ends, and no error is raised.

## 1. Confirm the version
- `uv run python -c "import sqlalchemy; print(sqlalchemy.__version__)"` should print `2.x`. If it prints `1.4`,
  ask before migrating. 1.4 with `SQLALCHEMY_WARN_20=1` is the stepping stone.

## 2. Scan
```bash
python3 .agents/skills/sqlalchemy-2-guard/scripts/sqlalchemy2_guard.py app tests
```
(Use the path the skill was installed to: `.claude/skills/...` for Claude Code, `.agents/skills/...` for the others.)

- Every `error` line is removed 1.x code or the legacy Query API. Fix it with the replacement the line gives.
  `references/migration-table.md` has before/after code.
- `connect-then-write` warnings: if the block writes, switch to `engine.begin()` or call `conn.commit()`. If it only
  reads, add `# guard: ok`. That comment silences warnings, never errors.
- `untyped-column` and `backref` warnings: convert the model to `Mapped[]` / `mapped_column` and `back_populates`.
  These conversions don't change the schema. Run `alembic check` (or compare `CREATE TABLE` output) to confirm.

## 3. Let SQLAlchemy check it
```bash
uv run pytest -q -W error::sqlalchemy.exc.SADeprecationWarning
```
- Legacy calls such as `Query.get()` warn `LegacyAPIWarning`, a subclass of `SADeprecationWarning`, so they fail here.
- Every write test should read the row back in a **new** session or connection. If it reads it back in the same
  one, an uncommitted write still looks fine.

## 4. Report
Say how many 1.x calls you replaced, which write paths you checked for commits and with which test, and that the
scanner and the tests were clean.
Skill file skills/sqlalchemy-2-guard/scripts/sqlalchemy2_guard.py Read-only scanner for removed and legacy SQLAlchemy APIs and uncommitted writes. 144 lines
Download Raw
#!/usr/bin/env python3
"""Find SQLAlchemy 1.x code (and the legacy Query API) in a project on SQLAlchemy 2.x.

Usage: python3 sqlalchemy2_guard.py [PATH ...]        (default: the current folder)
       python3 sqlalchemy2_guard.py --list            (print every rule)

A "guard: ok" comment on a line silences its warnings (never its errors).
Prints one line per finding:  path:line: level [rule] what is wrong -> what to use
Exit 0 when nothing at "error" level is found, 1 when something is, 2 on bad usage.
Standard library only. Read-only: it never edits a file and makes no network calls.
"""
import fnmatch
import os
import re
import sys

# (rule id, level, regex, message, file patterns). Patterns run against each line with comments removed.
RULES = [
    ('engine-execute', 'error', '\\b(engine|eng|db_engine|self\\.engine)\\.execute\\(',
     'Engine.execute() was removed in 2.0 -> with engine.begin() as conn: conn.execute(text(...))', []),
    ('select-list', 'error', '\\bselect\\(\\s*\\[',
     'select([a, b]) was removed -> select(a, b)', []),
    ('string-sql', 'error', '\\.execute\\(\\s*[frbu]*[\\"\']',
     'executing a plain SQL string was removed -> conn.execute(text("..."), {"name": value})', []),
    ('metadata-bind', 'error', '\\bMetaData\\([^)]*\\bbind\\s*=',
     'MetaData(bind=...) was removed -> pass the engine or connection explicitly', []),
    ('autocommit', 'error', '\\bautocommit\\s*=\\s*True',
     'Session/sessionmaker(autocommit=True) was removed -> with Session(engine) as s, s.begin(): ...', []),
    ('row-string-key', 'error', '\\b(row|r|result_row|record)\\[\\s*[\\"\']',
     "rows are tuples: row['name'] fails -> row.name or row._mapping['name']", []),
    ('ext-declarative', 'error', 'sqlalchemy\\.ext\\.declarative\\s+import\\s+[^#]*declarative_base',
     'sqlalchemy.ext.declarative.declarative_base moved -> class Base(DeclarativeBase): pass', []),
    ('query-get', 'error', '\\.query\\([^)]*\\)\\.get\\(',
     'Query.get() is legacy -> session.get(Model, pk)', []),
    ('legacy-query', 'error', '\\b(session|db|s|sess|self\\.session|db\\.session)\\.query\\(',
     'session.query() is the legacy Query API (still runs) -> session.scalars(select(Model).where(...))', []),
    ('orm-relation', 'error', '\\borm\\.relation\\(|\\brelation\\(',
     'relation() -> relationship()', []),
    ('declarative-base-fn', 'warn', '\\bdeclarative_base\\(\\s*\\)',
     'declarative_base() works but 2.0 style is class Base(DeclarativeBase): pass, with Mapped[] annotations', []),
    ('untyped-column', 'warn', '^\\s+\\w+\\s*=\\s*(sa\\.)?Column\\(',
     'Column() in a mapped class -> name: Mapped[str] = mapped_column(...) so types reach the type checker', []),
    ('backref', 'warn', '\\bbackref\\s*=',
     'backref= -> relationship(back_populates=...) on both sides, so both attributes are visible', []),
    ('future-flag', 'warn', '\\bfuture\\s*=\\s*True',
     'future=True does nothing on 2.x; remove it', []),
    ('dynamic-loader', 'warn', 'lazy\\s*=\\s*[\\"\']dynamic[\\"\']',
     "lazy='dynamic' is legacy -> WriteOnlyMapped[...] with lazy='write_only'", []),
    ('connect-then-write', 'warn', 'with\\s+\\w+\\.connect\\(\\)\\s+as\\s+\\w+\\s*:',
     'engine.connect() does not commit: writes are rolled back unless you call conn.commit() -> use engine.begin() for writes', []),
]
FILES = ['*.py']
COMMENT = '#'
SKIP_DIRS = {".git", ".venv", "venv", "node_modules", "__pycache__", ".agents", ".claude", ".cursor", "dist", "build", "target", ".godot"}
COMPILED = [(rid, lvl, re.compile(rx), msg, globs) for rid, lvl, rx, msg, globs in RULES]


def strip_comment(line):
    """Drop a trailing comment, leaving string literals alone."""
    q = None
    i = 0
    while i < len(line):
        c = line[i]
        if q:
            if c == "\\":
                i += 2
                continue
            if c == q:
                q = None
        elif c in "\"'`":
            q = c
        elif line.startswith(COMMENT, i):
            return line[:i]
        i += 1
    return line


def wanted(name):
    return any(fnmatch.fnmatch(name, g) for g in FILES)


def walk(paths):
    for p in paths:
        if os.path.isfile(p):
            yield p
            continue
        for root, dirs, files in os.walk(p):
            dirs[:] = sorted(d for d in dirs if d not in SKIP_DIRS)
            for f in sorted(files):
                if wanted(f):
                    yield os.path.join(root, f)


def scan(path):
    out = []
    name = os.path.basename(path)
    try:
        with open(path, encoding="utf-8", errors="replace") as fh:
            lines = fh.read().splitlines()
    except OSError as e:
        return [(path, 0, "error", "unreadable", str(e))]
    for n, raw in enumerate(lines, 1):
        code = strip_comment(raw)
        if not code.strip():
            continue
        accepted = "guard: ok" in raw  # a person accepted the warnings on this line
        for rid, lvl, rx, msg, globs in COMPILED:
            if accepted and lvl == "warn":
                continue
            if globs and not any(fnmatch.fnmatch(name, g) for g in globs):
                continue
            if rx.search(code):
                out.append((path, n, lvl, rid, msg))
    return out


def main(argv):
    if "--list" in argv:
        for rid, lvl, _, msg, globs in RULES:
            print(f"{lvl:5} [{rid}] {msg}" + (f"  (only {', '.join(globs)})" if globs else ""))
        return 0
    if any(a.startswith("-") for a in argv):
        print(__doc__)
        return 2
    paths = argv or ["."]
    for p in paths:
        if not os.path.exists(p):
            print(f"no such path: {p}", file=sys.stderr)
            return 2
    files = list(walk(paths))
    findings = []
    for f in files:
        findings.extend(scan(f))
    for path, n, lvl, rid, msg in findings:
        print(f"{os.path.relpath(path)}:{n}: {lvl} [{rid}] {msg}")
    errors = sum(1 for x in findings if x[2] == "error")
    warns = sum(1 for x in findings if x[2] == "warn")
    print(f"sqlalchemy2_guard: {len(files)} file(s), {errors} error(s), {warns} warning(s)")
    return 1 if errors else 0


if __name__ == "__main__":
    sys.exit(main(sys.argv[1:]))
Skill file skills/sqlalchemy-2-guard/references/migration-table.md SQLAlchemy 1.x to 2.0 style table, including transactions. 38 lines
Download Raw
# SQLAlchemy 1.x → 2.0 style table

From the SQLAlchemy 2.0 migration guide (https://docs.sqlalchemy.org/en/21/changelog/migration_20.html) and the
ORM querying guide. Checked against SQLAlchemy 2.1.6.

## Removed in 2.0 (raises)

| 1.x | 2.x | Error on 2.1.6 |
|---|---|---|
| `engine.execute("select 1")` | `with engine.connect() as c: c.execute(text("select 1"))` | `AttributeError` |
| `conn.execute("select 1")` | `conn.execute(text("select 1"))` | `ObjectNotExecutableError` |
| `select([User.id, User.name])` | `select(User.id, User.name)` | `ArgumentError` |
| `MetaData(bind=engine)` | `MetaData()`; pass the engine to `create_all(engine)` | `TypeError` |
| `Session(engine, autocommit=True)` | `with Session(engine) as s, s.begin(): ...` | `ArgumentError` |
| `row["name"]` | `row.name`, `row._mapping["name"]` | `TypeError` |

## Legacy (still runs, warns or is discouraged)

| 1.x | 2.0 style |
|---|---|
| `session.query(User).all()` | `session.scalars(select(User)).all()` |
| `session.query(User).filter_by(name=n).first()` | `session.scalars(select(User).filter_by(name=n)).first()` |
| `session.query(User).get(1)` (`LegacyAPIWarning`) | `session.get(User, 1)` |
| `session.query(func.count(User.id)).scalar()` | `session.scalar(select(func.count(User.id)))` |
| `from sqlalchemy.ext.declarative import declarative_base` (`MovedIn20Warning`) | `class Base(DeclarativeBase): pass` |
| `id = Column(Integer, primary_key=True)` | `id: Mapped[int] = mapped_column(primary_key=True)` |
| `name = Column(String, nullable=True)` | `name: Mapped[str \| None] = mapped_column(String(80))` |
| `relationship("Post", backref="author")` | `posts: Mapped[list["Post"]] = relationship(back_populates="author")` on both sides |
| `lazy="dynamic"` | `posts: WriteOnlyMapped["Post"] = relationship()` |
| `create_engine(url, future=True)` | `create_engine(url)` |

## Transactions: writes that vanish

| Code | What happens on 2.x | Fix |
|---|---|---|
| `with engine.connect() as c: c.execute(insert_stmt)` | rolled back at the end of the block | `with engine.begin() as c:` or `c.commit()` |
| `s = Session(engine); s.add(u)` and no commit | discarded when the session closes | `with Session(engine) as s, s.begin(): s.add(u)` |
Skill file skills/sqlalchemy-2-guard/references/sqlalchemy1-sample.py.txt A SQLAlchemy 1.x module for checking the guard. 23 lines
Download Raw
# A SQLAlchemy 1.3/1.4 module, used to check the guard. Each line is removed, legacy or silently different in 2.x.
from sqlalchemy import Column, Integer, MetaData, String, create_engine, select
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship, sessionmaker
engine = create_engine("sqlite:///app.db", future=True)
meta = MetaData(bind=engine)
Base = declarative_base()
class User(Base):
    __tablename__ = "users"
    id = Column(Integer, primary_key=True)
    name = Column(String)
    posts = relationship("Post", backref="author", lazy="dynamic")
Session = sessionmaker(bind=engine, autocommit=True)
session = Session()
admins = session.query(User).filter_by(name="admin").all()
one = session.query(User).get(1)
rows = engine.execute("select id, name from users")
for row in rows:
    print(row["name"])
ids = select([User.id])
with engine.connect() as conn:
    conn.execute(text("update users set name = 'x'"))

All files: source zip · Licence MIT · By SetupBay · First listed

Installing and checks

How do I install SQLAlchemy 2 guard against 1.x and legacy Query code in Claude Code?

From your project root, put AGENTS.md, CLAUDE.md, skills/sqlalchemy-2-guard/SKILL.md at .claude/skills/sqlalchemy-2-guard/SKILL.md, skills/sqlalchemy-2-guard/scripts/sqlalchemy2_guard.py at .claude/skills/sqlalchemy-2-guard/scripts/sqlalchemy2_guard.py, skills/sqlalchemy-2-guard/references/migration-table.md at .claude/skills/sqlalchemy-2-guard/references/migration-table.md, skills/sqlalchemy-2-guard/references/sqlalchemy1-sample.py.txt at .claude/skills/sqlalchemy-2-guard/references/sqlalchemy1-sample.py.txt. The "Claude Code" tab on this page has the curl commands, or download https://setupbay.com/download/sqlalchemy-2-style-guard/claude-code.zip, which is already laid out. Do not overwrite an existing AGENTS.md or CLAUDE.md; merge it.

How do I install SQLAlchemy 2 guard against 1.x and legacy Query code in Codex CLI?

From your project root, put AGENTS.md, skills/sqlalchemy-2-guard/SKILL.md at .agents/skills/sqlalchemy-2-guard/SKILL.md, skills/sqlalchemy-2-guard/scripts/sqlalchemy2_guard.py at .agents/skills/sqlalchemy-2-guard/scripts/sqlalchemy2_guard.py, skills/sqlalchemy-2-guard/references/migration-table.md at .agents/skills/sqlalchemy-2-guard/references/migration-table.md, skills/sqlalchemy-2-guard/references/sqlalchemy1-sample.py.txt at .agents/skills/sqlalchemy-2-guard/references/sqlalchemy1-sample.py.txt. The "Codex CLI" tab on this page has the curl commands, or download https://setupbay.com/download/sqlalchemy-2-style-guard/codex.zip, which is already laid out. Do not overwrite an existing AGENTS.md or CLAUDE.md; merge it.

How do I install SQLAlchemy 2 guard against 1.x and legacy Query code in Cursor?

From your project root, put AGENTS.md, .cursor/rules/sqlalchemy-2-style.mdc, skills/sqlalchemy-2-guard/SKILL.md at .agents/skills/sqlalchemy-2-guard/SKILL.md, skills/sqlalchemy-2-guard/scripts/sqlalchemy2_guard.py at .agents/skills/sqlalchemy-2-guard/scripts/sqlalchemy2_guard.py, skills/sqlalchemy-2-guard/references/migration-table.md at .agents/skills/sqlalchemy-2-guard/references/migration-table.md, skills/sqlalchemy-2-guard/references/sqlalchemy1-sample.py.txt at .agents/skills/sqlalchemy-2-guard/references/sqlalchemy1-sample.py.txt. The "Cursor" tab on this page has the curl commands, or download https://setupbay.com/download/sqlalchemy-2-style-guard/cursor.zip, which is already laid out. Do not overwrite an existing AGENTS.md or CLAUDE.md; merge it.

How do I install SQLAlchemy 2 guard against 1.x and legacy Query code in Gemini CLI?

From your project root, put AGENTS.md, GEMINI.md, skills/sqlalchemy-2-guard/SKILL.md at .agents/skills/sqlalchemy-2-guard/SKILL.md, skills/sqlalchemy-2-guard/scripts/sqlalchemy2_guard.py at .agents/skills/sqlalchemy-2-guard/scripts/sqlalchemy2_guard.py, skills/sqlalchemy-2-guard/references/migration-table.md at .agents/skills/sqlalchemy-2-guard/references/migration-table.md, skills/sqlalchemy-2-guard/references/sqlalchemy1-sample.py.txt at .agents/skills/sqlalchemy-2-guard/references/sqlalchemy1-sample.py.txt. The "Gemini CLI" tab on this page has the curl commands, or download https://setupbay.com/download/sqlalchemy-2-style-guard/gemini-cli.zip, which is already laid out. Do not overwrite an existing AGENTS.md or CLAUDE.md; merge it.

How do I install SQLAlchemy 2 guard against 1.x and legacy Query code in Antigravity CLI?

From your project root, put AGENTS.md, skills/sqlalchemy-2-guard/SKILL.md at .agents/skills/sqlalchemy-2-guard/SKILL.md, skills/sqlalchemy-2-guard/scripts/sqlalchemy2_guard.py at .agents/skills/sqlalchemy-2-guard/scripts/sqlalchemy2_guard.py, skills/sqlalchemy-2-guard/references/migration-table.md at .agents/skills/sqlalchemy-2-guard/references/migration-table.md, skills/sqlalchemy-2-guard/references/sqlalchemy1-sample.py.txt at .agents/skills/sqlalchemy-2-guard/references/sqlalchemy1-sample.py.txt. The "Antigravity CLI" tab on this page has the curl commands, or download https://setupbay.com/download/sqlalchemy-2-style-guard/antigravity-cli.zip, which is already laid out. Do not overwrite an existing AGENTS.md or CLAUDE.md; merge it.

How do I install SQLAlchemy 2 guard against 1.x and legacy Query code in GitHub Copilot?

From your project root, put AGENTS.md, skills/sqlalchemy-2-guard/SKILL.md at .agents/skills/sqlalchemy-2-guard/SKILL.md, skills/sqlalchemy-2-guard/scripts/sqlalchemy2_guard.py at .agents/skills/sqlalchemy-2-guard/scripts/sqlalchemy2_guard.py, skills/sqlalchemy-2-guard/references/migration-table.md at .agents/skills/sqlalchemy-2-guard/references/migration-table.md, skills/sqlalchemy-2-guard/references/sqlalchemy1-sample.py.txt at .agents/skills/sqlalchemy-2-guard/references/sqlalchemy1-sample.py.txt. The "GitHub Copilot" tab on this page has the curl commands, or download https://setupbay.com/download/sqlalchemy-2-style-guard/copilot.zip, which is already laid out. Do not overwrite an existing AGENTS.md or CLAUDE.md; merge it.

What has been checked in SQLAlchemy 2 guard against 1.x and legacy Query code?

It passed the Level 1 automated install checks on 2026-10-29: the files parse, install to each agent's documented paths, and pass the safety scan. No AI agent has run it yet, so no agent pass or fail is claimed.

More for Claude Code, Codex, Cursor, Gemini CLI, Antigravity CLI and GitHub Copilot.