Skip to content
Back to skills

Sqlalchemy

ASecurity

Use when working with SQLAlchemy 2.0 in Python - declarative models, queries, sessions, async engines, Alembic migrations, relationship loading strategies, N+1 detection, pool exhaustion, or 2.0 migration

  • 21 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 2, 2026
databasespythongobashsqlawstestingdebuggingapidatabaseperformance

Works with

  • api

Security analysis

A96/100
  • mediumInstalls packages at runtime which could introduce malicious dependencies

Pro scans all 7 files and shows the line behind each finding

Scanned October 2, 2026

npx -y skills add CodeAtCode/oss-ai-skills --skill sqlalchemy --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Sqlalchemy?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Sqlalchemy
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/codeatcode-sqlalchemy/badge)](https://www.skillsdirectory.com/skills/codeatcode-sqlalchemy)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: sqlalchemy
description: "Use when working with SQLAlchemy 2.0 in Python - declarative models, queries, sessions, async engines, Alembic migrations, relationship loading strategies, N+1 detection, pool exhaustion, or 2.0 migration"
metadata:
  author: mte90
  version: "3.0.0"
  tags:
    - python
    - orm
    - database
    - sql
    - alembic
    - async
---

# SQLAlchemy

Python SQL toolkit and ORM. See [official docs](https://docs.sqlalchemy.org/en/20/) for full API reference.

## Installation

```bash
pip install sqlalchemy[asyncio] alembic
pip install asyncpg  # PostgreSQL async driver
pip install psycopg2-binary  # PostgreSQL sync driver
```

## Engine and Pool Configuration

```python
from sqlalchemy import create_engine

# Basic engine
engine = create_engine("postgresql://user:pass@localhost/db", echo=True)

# Production pool settings
engine = create_engine(
    "postgresql://user:pass@localhost/db",
    pool_size=10,           # Base connection pool
    max_overflow=20,        # Max extra connections under load
    pool_timeout=30,        # Wait time for connection
    pool_recycle=3600,      # Recycle connections after 1 hour
    pool_pre_ping=True,     # Check connection health (critical for pool exhaustion fix)
    echo_pool=True,         # Log pool events for debugging
)
```

### Async Engine

```python
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker

async_engine = create_async_engine(
    "postgresql+asyncpg://user:pass@localhost/db",
    echo=True,
)

AsyncSessionLocal = sessionmaker(
    async_engine,
    class_=AsyncSession,
    expire_on_commit=False,
)
```

### Serverless (NullPool)

```python
from sqlalchemy.pool import NullPool

# AWS Lambda, Vercel - no persistent connections
engine = create_engine(DATABASE_URL, poolclass=NullPool)
```

## Declarative Models

```python
from sqlalchemy import Column, Integer, String, DateTime, Boolean, ForeignKey
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
from sqlalchemy.sql import func
from typing import Optional, List

class Base(DeclarativeBase):
    pass

class User(Base):
    __tablename__ = "users"
    
    id: Mapped[int] = mapped_column(primary_key=True)
    username: Mapped[str] = mapped_column(String(50), unique=True, nullable=False)
    email: Mapped[str] = mapped_column(String(255), unique=True)
    created_at: Mapped[DateTime] = mapped_column(DateTime(timezone=True), server_default=func.now())
    
    # Relationship (see Relationship Loading Guide below)
    articles: Mapped[List["Article"]] = relationship(back_populates="author")
    
    def __repr__(self):
        return f"<User(id={self.id}, username='{self.username}')>"

class Article(Base):
    __tablename__ = "articles"
    
    id: Mapped[int] = mapped_column(primary_key=True)
    title: Mapped[str] = mapped_column(String(200))
    author_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
    
    author: Mapped["User"] = relationship(back_populates="articles")
```

### Column Types Reference

```python
from sqlalchemy import BigInteger, Text, Numeric, JSON, Enum, LargeBinary

class Product(Base):
    __tablename__ = "products"
    
    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str] = mapped_column(String(100))
    description: Mapped[str] = mapped_column(Text)
    price: Mapped[Decimal] = mapped_column(Numeric(10, 2))
    metadata: Mapped[dict] = mapped_column(JSON)
    status: Mapped[str] = mapped_column(Enum("draft", "published", name="product_status"))
```

## Sessions

### Session Lifecycle (Critical)

**One session per request/task, never shared.** Sessions are not thread-safe.

```python
# ✅ GOOD: One session per request
def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

# ✅ GOOD: One AsyncSession per async task
async def process_user(user_id: int):
    async with AsyncSessionLocal() as session:
        result = await session.execute(select(User).where(User.id == user_id))
        return result.scalar_one_or_none()

# ❌ BAD: Module-level shared session (race conditions)
db = SessionLocal()  # Never do this!
```

### Transaction Management

```python
# Implicit transaction (default)
with SessionLocal() as session:
    session.add(user)
    session.commit()

# Explicit transaction (recommended)
with SessionLocal() as session:
    with session.begin():  # Auto-commit/rollback
        session.add(user)

# Nested transaction (SAVEPOINT)
with SessionLocal() as session:
    with session.begin():
        session.add(user)
        with session.begin_nested():
            session.add(related_object)
            # Rolls back to savepoint on exception
```

### expire_on_commit=False Trade-off

```python
SessionLocal = sessionmaker(engine, expire_on_commit=False)

# ✅ Can access attributes after commit
with SessionLocal() as session:
    user = User(name="test")
    session.add(user)
    session.commit()
    print(user.name)  # Works

# Trade-off: Convenient but may return stale data if DB changed externally
```

## Queries (2.0 Style)

```python
from sqlalchemy import select, update, delete

# Select all
stmt = select(User)
users = session.execute(stmt).scalars().all()

# Select with filter
stmt = select(User).where(User.is_active == True)
users = session.execute(stmt).scalars().all()

# Get by primary key
user = session.get(User, 1)  # Returns None if not found

# Get one
stmt = select(User).where(User.username == "john")
user = session.execute(stmt).scalar_one_or_none()

# Update
stmt = update(User).where(User.id == 1).values(email="new@example.com")
session.execute(stmt)
session.commit()

# Delete
stmt = delete(User).where(User.is_active == False)
session.execute(stmt)
session.commit()
```

See `references/queries.md` for joins, aggregations, CTEs, window functions, and bulk operations.

## Relationship Loading Decision Guide

**The single most important performance decision.** Wrong choices cause N+1 queries or row explosion.

### Strategy Comparison

| Strategy | Query Count | Row Duplication | When to Use |
|----------|-------------|-----------------|-------------|
| `lazy="select"` (default) | N+1 if iterated | No | **Avoid in production** - one extra query per relation |
| `joinedload` | 1 query | **Yes** - duplicates parent columns | Scalar relations only (one-to-one, many-to-one) |
| `selectinload` | 2-3 queries | No | **Default recommendation** - efficient for collections |
| `raiseload` | Raises error | No | Tests/debugging - catch lazy loads |
| `noload` | 0 queries | No | Optional relations you never need |

### Why `joinedload` on Collections Explodes

```python
# Author has 3 articles, each with 2 tags
# joinedload creates: 1 × 3 × 2 = 6 rows (Cartesian product)

stmt = select(Author).options(
    joinedload(Author.articles).joinedload(Article.tags)
)
authors = session.execute(stmt).unique().scalars().all()
# Memory: O(parents × children × grandchildren) - BAD!
```

**Fix**: Use `selectinload` for collections:

```python
stmt = select(Author).options(
    selectinload(Author.articles).selectinload(Article.tags)
)
# Query 1: SELECT authors
# Query 2: SELECT articles WHERE author_id IN (...)
# Query 3: SELECT tags WHERE article_id IN (...)
```

### Decision Table

```
Do you need the relation?
├─ No → noload or don't include
├─ Yes, always, scalar relation → joinedload
├─ Yes, always, collection → selectinload (default recommendation)
├─ Sometimes → lazy="select" or explicit load per endpoint
└─ In tests → raiseload to catch bugs
```

### The "Load Only What You Serialize" Rule

Never eager-load relations you won't return. Loading full object graphs wastes memory.

```python
# Bad: Load entire graph
stmt = select(User).options(
    joinedload(User.articles).joinedload(Article.comments)
)

# Good: Load only what you return
stmt = select(User.id, User.username)  # No relations

# Or: Load specific relation only
stmt = select(User).options(selectinload(User.articles))
# Don't cascade to Article.comments unless needed
```

### raiseload as Bug Detector

```python
from sqlalchemy.orm import raiseload

# Catch ANY lazy load in tests
stmt = select(User).options(raiseload("*"))
# Test fails if code accesses user.articles without eager loading
```

### noload for Optional Relations

```python
class Order(Base):
    __tablename__ = "orders"
    
    id: Mapped[int] = mapped_column(primary_key=True)
    user_id: Mapped[Optional[int]] = mapped_column(ForeignKey("users.id"))
    
    # Rarely needed - most orders don't have extended details
    shipping_address: Mapped[Optional["ShippingAddress"]] = relationship(
        lazy="selectin",  # Load only when explicitly accessed
    )
```

### Before/After: Query Count

**Before (N+1):**

```python
authors = session.execute(select(Author)).scalars().all()
for author in authors:  # 100 authors
    print(len(author.articles))  # 100 more queries!
# Total: 101 queries
```

**After (selectinload):**

```python
stmt = select(Author).options(selectinload(Author.articles))
authors = session.execute(stmt).scalars().all()
for author in authors:
    print(len(author.articles))  # Already loaded
# Total: 2 queries
```

See `references/relationships.md` for full session lifecycle rules and async patterns.

## 2.0 Migration Checklist

| Old (1.x) | New (2.0) |
|-----------|-----------|
| `session.query(User).get(1)` | `session.get(User, 1)` |
| `session.query(User).filter(...)` | `session.execute(select(User).where(...))` |
| `Query` object | `select()` + `session.execute()` |
| Implicit autocommit | Explicit `session.commit()` required |
| `engine.execute("SELECT ...")` | Removed - use `session.execute(text("SELECT ..."))` |
| `text("SELECT ...")` without binds | `text("SELECT ...").bindparams(...)` or `:param` in string |
| `MetaData()` | `MetaData(naming_convention={...})` for constraints |

```python
# Before (1.x)
user = session.query(User).filter(User.id == 1).first()

# After (2.0)
user = session.get(User, 1)
# OR
user = session.execute(select(User).where(User.id == 1)).scalar_one_or_none()
```

## Common Issues

See `references/common-issues.md` for detailed detection and fixes:

| Issue | Symptom | Detection |
|-------|---------|-----------|
| **N+1 queries** | Slow list endpoints | `echo=True` shows repeated queries; `assert_num_queries(2)` in tests |
| **MissingGreenletError** | "greenlet_spawn has not been called" | Lazy load outside async context |
| **StaleDataError** | "Could not refresh identity map" | Lost updates under concurrency |
| **DetachedInstanceError** | "Parent instance is not bound to a Session" | Access after commit/expire |
| **Pool exhaustion** | "QueuePool limit reached" | `echo_pool=True` logs; too many open connections |
| **Missing tables** | "relation does not exist" | Migration not run; check with `inspect(engine).get_table_names()` |

See `references/common-issues.md` for:
- **N+1 detection** via `echo=True`, query counting, `assert_num_queries`
- **MissingGreenletError** - lazy load outside async context fix
- **StaleDataError** - optimistic/pessimistic locking patterns
- **DetachedInstanceError** - access within session, eager load, `expire_on_commit=False`
- **Pool exhaustion** - `pool_size`, `max_overflow`, `pool_pre_ping`, `NullPool` for serverless

## Deep Dives

Load these reference files on demand for detailed coverage:

- **`references/queries.md`** - Joins, aggregations, CTEs, window functions, bulk operations, PostgreSQL optimization
- **`references/relationships.md`** - Full relationship loading decision guide, session lifecycle rules, async patterns
- **`references/common-issues.md`** - Real failure modes with detection and fixes (N+1, MissingGreenlet, StaleDataError, DetachedInstanceError, pool exhaustion)
- **`references/migrations.md`** - Alembic setup, migration creation, testing strategies

Files in this skill

  • SKILL.md11.7 KB
  • references/async.md2.6 KB
  • references/common-issues.md8.7 KB
  • references/debugging.md2 KB
  • references/migrations.md6.1 KB
  • references/queries.md10.6 KB
  • references/relationships.md7.7 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…