sqlalchemy — Cursor rule: SQLAlchemy 2.x for Python
A Cursor rule that keeps Python database code on SQLAlchemy 2.x: typed models, context-managed sessions, select() instead of the legacy Query.
.cursor/rules/sqlalchemy.mdcWhat sqlalchemy does
Drop .cursor/rules/sqlalchemy.mdc into your project and Cursor applies it to every **/*.py file. The rule pairs bad and good examples: models built on DeclarativeBase with Mapped annotations, mixins for fields like created_at, sessions opened as with Session(engine) as session:, and explicit transaction blocks via session.begin().
Querying gets its own section. It pushes select() with session.execute() or session.scalar(), since session.query() is deprecated in 2.x. For relationships it points at selectinload or joinedload so a loop over users and their addresses doesn't fire a query per row.
The rule assumes you're already on the 2-style API. On a codebase still using declarative_base() and Column, the examples fight your existing models, so decide on the migration before pasting it in. The text cuts off in the section on updating objects, so deletes and bulk operations aren't covered.
What it helps with
- move models from
declarative_base()toDeclarativeBaseandMapped - stop leaking sessions by wrapping them in a context manager
- replace
session.query()withselect() - kill N+1 queries when walking relationships
- share timestamp and soft-delete fields through a mixin
Example prompts for the agent
- Rewrite the models in
models.pyusing SQLAlchemy 2.xMappedandDeclarativeBase - Check this query — if it uses
session.query(), convert it toselect() - Add
selectinloadfor theaddressesrelationship so we don't hit N+1
How to install sqlalchemy
Manually: put the files in place
.cursor/rules/sqlalchemy.mdcFrontmatter: description, globs, alwaysApply
Manually: put the files in place
.claude/rules/sqlalchemy.mdThe paths frontmatter field limits the files: move globs from the source into paths
AGENTS.md
Codex reads AGENTS.md: paste the rule text there
Manually: put the files in place
.github/instructions/sqlalchemy.instructions.mdThe applyTo field sets which files it applies to: move globs from the source into applyTo
Manually: put the files in place
.devin/rules/sqlalchemy.mdManually: put the files in place
.clinerules/sqlalchemy.mdStep by step, with commands for each platform: Cursor rules: .mdc files, four modes and what replaced .cursorrules
Questions about sqlalchemy
How do I install the sqlalchemy rule in Cursor?
Create .cursor/rules/sqlalchemy.mdc and paste the rule contents into it. Cursor then applies it to files matching the **/*.py glob.
What does this rule do?
It sets SQLAlchemy 2.x conventions for Python code: typed models, sessions as context managers, queries through select(), and eager loading for relationships.
Does it work with an older SQLAlchemy codebase?
It only describes the 2-style API, so on code using declarative_base() and Column the examples will clash with what you have. Settle the migration question first.
Source text
sqlalchemy.mdc · CC0-1.0
---
description: This guide enforces modern SQLAlchemy 2.x best practices for Python applications, ensuring type-safe, performant, and maintainable database interactions.
globs: **/*.py
---
# SQLAlchemy Best Practices (2.x Style)
This document outlines the definitive best practices for using SQLAlchemy 2.x in our projects. We exclusively adopt the "2-style" API, leveraging its fully typed, declarative features for robust and maintainable code.
## 1. Code Organization and Data Modeling
Always define your ORM models using the modern `DeclarativeBase` and `Mapped` annotations. Keep model definitions in a dedicated `models.py` or `orm_models/` directory.
### 1.1 Declarative Models with Type Annotations
Use `DeclarativeBase` as your base class and `Mapped` for all ORM-mapped attributes. This enables static type checking and clear schema definition.
❌ BAD: Legacy `declarative_base()` function and untyped `Column`.
```python
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, Integer, String
Base = declarative_base()
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True)
name = Column(String) # No type hint
```
✅ GOOD: Modern `DeclarativeBase` and `Mapped` for explicit typing.
```python
from typing import List, Optional
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
from sqlalchemy import String, ForeignKey, func, DateTime
class Base(DeclarativeBase):
pass
class User(Base):
__tablename__ = "user_account"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(30))
fullname: Mapped[Optional[str]]
created_at: Mapped[DateTime] = mapped_column(DateTime, insert_default=func.now())
addresses: Mapped[List["Address"]] = relationship(back_populates="user", cascade="all, delete-orphan")
class Address(Base):
__tablename__ = "address"
id: Mapped[int] = mapped_column(primary_key=True)
email_address: Mapped[str]
user_id: Mapped[int] = mapped_column(ForeignKey("user_account.id"))
user: Mapped["User"] = relationship(back_populates="addresses")
```
### 1.2 Mixins for Common Fields
Implement common fields like timestamps, soft-delete flags, or version counters using mixins.
```python
from datetime import datetime
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy import DateTime, func
class TimestampMixin:
created_at: Mapped[datetime] = mapped_column(DateTime, insert_default=func.now())
updated_at: Mapped[datetime] = mapped_column(DateTime, insert_default=func.now(), onupdate=func.now())
class SoftDeleteMixin:
deleted_at: Mapped[Optional[datetime]] = mapped_column(DateTime, default=None)
class User(Base, TimestampMixin, SoftDeleteMixin):
__tablename__ = "user_account"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str]
# ... other fields
```
## 2. Session Management
The `Session` is your unit of work. Always use it as a context manager to ensure proper transaction handling and resource cleanup.
### 2.1 Context Manager for Sessions
Wrap all database operations within a `with Session(engine) as session:` block. This guarantees `commit()` on success and `rollback()` on error, followed by `close()`.
❌ BAD: Manual session handling, prone to leaks and uncommitted transactions.
```python
session = Session(engine)
try:
user = User(name="Alice")
session.add(user)
session.commit()
except Exception:
session.rollback()
finally:
session.close()
```
✅ GOOD: `Session` as a context manager.
```python
from sqlalchemy.orm import sessionmaker
from sqlalchemy import create_engine
engine = create_engine("sqlite:///./test.db")
Session = sessionmaker(autocommit=False, autoflush=False, bind=engine)
def create_user(name: str):
with Session() as session:
user = User(name=name)
session.add(user)
session.commit() # Flushes changes and commits transaction
session.refresh(user) # Refresh to get auto-generated IDs
return user
```
### 2.2 Explicit Transaction Blocks
For complex operations requiring multiple steps within a single transaction, use `session.begin()`.
```python
def transfer_funds(from_account_id: int, to_account_id: int, amount: float):
with Session() as session:
with session.begin(): # Explicit transaction block
from_account = session.get(Account, from_account_id)
to_account = session.get(Account, to_account_id)
if not from_account or not to_account:
raise ValueError("Account not found")
if from_account.balance < amount:
raise ValueError("Insufficient funds")
from_account.balance -= amount
to_account.balance += amount
# Transaction commits here if no exceptions, or rolls back if an error occurs
print(f"Transferred {amount} from {from_account_id} to {to_account_id}")
```
## 3. Querying and Optimization
Always use the `select()` construct for queries and prioritize eager loading for relationships to avoid N+1 problems.
### 3.1 Use `select()` for All Queries
The `Query` object is deprecated in 2.x. Use `select()` for all ORM and Core queries.
❌ BAD: Using the legacy `session.query()` API.
```python
users = session.query(User).filter(User.name == "Alice").all()
```
✅ GOOD: Using `select()` with `session.execute()`.
```python
from sqlalchemy import select
def get_user_by_name(session: Session, name: str) -> Optional[User]:
stmt = select(User).where(User.name == name)
return session.scalar(stmt) # Use scalar for single result, one_or_none for one, all for list
```
### 3.2 Eager Loading Relationships
Prevent N+1 query issues by eagerly loading related objects using `selectinload` or `joinedload`. `selectinload` is generally preferred for collections.
❌ BAD: Lazy loading in a loop, leading to N+1 queries.
```python
users = session.scalars(select(User)).all()
for user in users:
print(f"{user.name} has {len(user.addresses)} addresses") # Each access triggers a new query
```
✅ GOOD: Eager loading with `selectinload`.
```python
from sqlalchemy.orm import selectinload
def get_users_with_addresses(session: Session) -> List[User]:
stmt = select(User).options(selectinload(User.addresses))
return session.scalars(stmt).all()
# Now, accessing user.addresses won't trigger additional queries
users = get_users_with_addresses(session)
for user in users:
print(f"{user.name} has {len(user.addresses)} addresses")
```
## 4. Data Manipulation
The ORM Unit of Work pattern handles inserts, updates, and deletes efficiently.
### 4.1 Adding and Updating Objects
Add new objects with `session.add()`. Modifications to existing objects are tracked automatically.
```python
def update_user_name(session: Session, user_id: int, new_name: str):
user = session.get(User, user_id) # Efficiently get by primary key
if user:
user.name = new_name # Change is tracked
session.commit()
return user
def add_new_user_with_address(session: Session, name: str, email: str):
user = User(name=name, fullname=name)
address = Address(email_address=email, user=user) # Relationship automatically links
session.add(user) # Adding user also adds address due to cascade
session.commit()
return user
```
### 4.2 Deleting Objects
Delete objects using `session.delete()`.
```python
def delete_user(session: Session, user_id: int):
user = session.get(User, user_id)
if user:
session.delete(user)
session.commit()
print(f"User {user_id} deleted.")
else:
print(f"User {user_id} not found.")
```
### 4.3 Bulk Operations (Use Sparingly)
Only use bulk operations (`session.bulk_insert_mappings`, `session.execute(insert/update/delete)`) when performance is paramount and ORM event handling (e.g., `onupdate` hooks) is not required.
```python
from sqlalchemy import insert
def bulk_insert_users(session: Session, user_data: List[dict]):
# This bypasses ORM object creation and event hooks.
# Use when you have raw data and need speed.
session.execute(insert(User), user_data)
session.commit()
# Example user_data: [{"name": "Charlie", "fullname": "Charlie Brown"}, ...]
```
## 5. Migrations and Type Checking
Integrate Alembic for schema migrations and ensure static type checking with Mypy or Ruff.
### 5.1 Alembic for Schema Evolution
Always use Alembic to manage database schema changes. Never manually alter the database schema.
```bash
# Initialize Alembic (once per project)
alembic init -t async migrations
# Generate a new migration script
alembic revision --autogenerate -m "Add new_column to user_account"
# Apply migrations
alembic upgrade head
```
### 5.2 Static Type Checking
Leverage Python's `typing` and SQLAlchemy's `Mapped` for comprehensive type checking with tools like Mypy or Ruff. This catches schema mismatches and API misuses early.
```python
# mypy.ini or pyproject.toml configuration for mypy
[mypy]
plugins = sqlalchemy.ext.mypy.plugin
```
© sanjeed5 · CC0-1.0 · sanjeed5/awesome-cursor-rules-mdc