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
Verification
| Check | Result | What we found |
|---|---|---|
| Listinglisting.yaml matches the schema and declares every file in the folder. | Pass | Schema valid; 8 files declared, none missing or extra
|
| 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
|
| 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
|
| 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
|
| 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
|
| 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
|
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/.
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
| File | Goes to |
|---|---|
AGENTS.md | AGENTS.md |
CLAUDE.md | CLAUDE.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.
Codex CLI reads AGENTS.md and skills from .agents/skills/.
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 .agents/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 .agents/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 .agents/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 .agents/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
| File | Goes to |
|---|---|
AGENTS.md | AGENTS.md |
skills/sqlalchemy-2-guard/SKILL.md | .agents/skills/sqlalchemy-2-guard/SKILL.md |
skills/sqlalchemy-2-guard/scripts/sqlalchemy2_guard.py | .agents/skills/sqlalchemy-2-guard/scripts/sqlalchemy2_guard.py |
skills/sqlalchemy-2-guard/references/migration-table.md | .agents/skills/sqlalchemy-2-guard/references/migration-table.md |
skills/sqlalchemy-2-guard/references/sqlalchemy1-sample.py.txt | .agents/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 Codex CLI reads.
Cursor reads AGENTS.md + .cursor/rules/*.mdc and skills from .agents/skills/.
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 .cursor/rules/sqlalchemy-2-style.mdc https://setupbay.com/raw/sqlalchemy-2-style-guard/1.0.0/.cursor/rules/sqlalchemy-2-style.mdc curl -fsSL --create-dirs -o .agents/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 .agents/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 .agents/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 .agents/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
| File | Goes to |
|---|---|
AGENTS.md | AGENTS.md |
.cursor/rules/sqlalchemy-2-style.mdc | .cursor/rules/sqlalchemy-2-style.mdc |
skills/sqlalchemy-2-guard/SKILL.md | .agents/skills/sqlalchemy-2-guard/SKILL.md |
skills/sqlalchemy-2-guard/scripts/sqlalchemy2_guard.py | .agents/skills/sqlalchemy-2-guard/scripts/sqlalchemy2_guard.py |
skills/sqlalchemy-2-guard/references/migration-table.md | .agents/skills/sqlalchemy-2-guard/references/migration-table.md |
skills/sqlalchemy-2-guard/references/sqlalchemy1-sample.py.txt | .agents/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 Cursor reads.
Gemini CLI reads GEMINI.md (imports AGENTS.md with @./AGENTS.md) and skills from .agents/skills/.
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 GEMINI.md https://setupbay.com/raw/sqlalchemy-2-style-guard/1.0.0/GEMINI.md curl -fsSL --create-dirs -o .agents/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 .agents/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 .agents/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 .agents/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
| File | Goes to |
|---|---|
AGENTS.md | AGENTS.md |
GEMINI.md | GEMINI.md |
skills/sqlalchemy-2-guard/SKILL.md | .agents/skills/sqlalchemy-2-guard/SKILL.md |
skills/sqlalchemy-2-guard/scripts/sqlalchemy2_guard.py | .agents/skills/sqlalchemy-2-guard/scripts/sqlalchemy2_guard.py |
skills/sqlalchemy-2-guard/references/migration-table.md | .agents/skills/sqlalchemy-2-guard/references/migration-table.md |
skills/sqlalchemy-2-guard/references/sqlalchemy1-sample.py.txt | .agents/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 Gemini CLI reads.
Antigravity CLI reads AGENTS.md (read natively; GEMINI.md is also read) and skills from .agents/skills/.
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 .agents/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 .agents/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 .agents/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 .agents/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
| File | Goes to |
|---|---|
AGENTS.md | AGENTS.md |
skills/sqlalchemy-2-guard/SKILL.md | .agents/skills/sqlalchemy-2-guard/SKILL.md |
skills/sqlalchemy-2-guard/scripts/sqlalchemy2_guard.py | .agents/skills/sqlalchemy-2-guard/scripts/sqlalchemy2_guard.py |
skills/sqlalchemy-2-guard/references/migration-table.md | .agents/skills/sqlalchemy-2-guard/references/migration-table.md |
skills/sqlalchemy-2-guard/references/sqlalchemy1-sample.py.txt | .agents/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 Antigravity CLI reads.
GitHub Copilot reads AGENTS.md and skills from .agents/skills/.
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 .agents/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 .agents/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 .agents/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 .agents/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
| File | Goes to |
|---|---|
AGENTS.md | AGENTS.md |
skills/sqlalchemy-2-guard/SKILL.md | .agents/skills/sqlalchemy-2-guard/SKILL.md |
skills/sqlalchemy-2-guard/scripts/sqlalchemy2_guard.py | .agents/skills/sqlalchemy-2-guard/scripts/sqlalchemy2_guard.py |
skills/sqlalchemy-2-guard/references/migration-table.md | .agents/skills/sqlalchemy-2-guard/references/migration-table.md |
skills/sqlalchemy-2-guard/references/sqlalchemy1-sample.py.txt | .agents/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 GitHub Copilot 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
# 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
# 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
# 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
---
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
---
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
#!/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
# 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
# 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
Credits
Public work that inspired this loadout. The wording here is our own; follow the links for the originals.
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.