Skip to content

Commit 560d178

Browse files
authored
docs: add fastsqla-session agent skill (#34)
1 parent 5d0a602 commit 560d178

1 file changed

Lines changed: 152 additions & 0 deletions

File tree

skills/fastsqla-session/SKILL.md

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
---
2+
name: fastsqla-session
3+
description: >
4+
Manages async SQLAlchemy sessions in FastSQLA endpoints and background tasks.
5+
Covers the Session dependency (auto-commit/rollback lifecycle), flush vs commit
6+
rules, IntegrityError handling, and the open_session() context manager. Use when
7+
writing FastAPI endpoints or background tasks that interact with a database through
8+
FastSQLA.
9+
---
10+
11+
# FastSQLA Session Management
12+
13+
FastSQLA provides two ways to get an async SQLAlchemy session:
14+
15+
1. **`Session`** — A FastAPI dependency for endpoints.
16+
2. **`open_session()`** — An async context manager for non-endpoint code.
17+
18+
Both follow the same lifecycle: auto-commit on success, auto-rollback on exception, always close.
19+
20+
---
21+
22+
## Session Dependency
23+
24+
`Session` is a FastAPI dependency. Type-annotate an endpoint parameter as `Session` and FastAPI injects an async session automatically.
25+
26+
```python
27+
from fastsqla import Session, Item
28+
29+
@app.get("/users/{user_id}", response_model=Item[UserModel])
30+
async def get_user(session: Session, user_id: int):
31+
user = await session.get(User, user_id)
32+
return {"data": user}
33+
```
34+
35+
### Lifecycle
36+
37+
| Phase | What happens |
38+
|-----------|------------------------------------------------------|
39+
| Success | Session is **committed** automatically |
40+
| Exception | Session is **rolled back** automatically |
41+
| Always | Session is **closed**, connection returned to pool |
42+
43+
You do not need to manage any of this yourself.
44+
45+
---
46+
47+
## Critical: flush() vs commit()
48+
49+
**NEVER call `session.commit()` inside an endpoint.** FastSQLA commits automatically when the endpoint returns without error. Calling `commit()` manually breaks the transactional guarantee — if an error occurs after your manual commit, the already-committed changes cannot be rolled back.
50+
51+
Use `session.flush()` when you need server-generated data (e.g., auto-increment IDs) before the response is returned. Flushing sends pending changes to the database **within the current transaction** without finalizing it.
52+
53+
### CORRECT — use flush()
54+
55+
```python
56+
from fastsqla import Session, Item
57+
58+
@app.post("/heroes", response_model=Item[HeroItem])
59+
async def create_hero(session: Session, new_hero: HeroBase):
60+
hero = Hero(**new_hero.model_dump())
61+
session.add(hero)
62+
await session.flush() # hero.id is now populated
63+
return {"data": hero}
64+
# FastSQLA auto-commits here
65+
```
66+
67+
### INCORRECT — do not call commit()
68+
69+
```python
70+
from fastsqla import Session, Item
71+
72+
@app.post("/heroes", response_model=Item[HeroItem])
73+
async def create_hero(session: Session, new_hero: HeroBase):
74+
hero = Hero(**new_hero.model_dump())
75+
session.add(hero)
76+
await session.commit() # WRONG: breaks auto-commit lifecycle
77+
return {"data": hero}
78+
```
79+
80+
If you call `commit()` and a later step raises an exception, the committed data **cannot** be rolled back. Let FastSQLA handle the commit.
81+
82+
---
83+
84+
## IntegrityError Handling
85+
86+
When a `flush()` triggers a constraint violation (unique, foreign key, etc.), SQLAlchemy raises `IntegrityError`. The session is **invalidated** after this — you cannot continue using it for further queries.
87+
88+
The correct pattern is to catch `IntegrityError` after `flush()` and re-raise it as an `HTTPException`. The raised exception triggers FastSQLA's automatic rollback.
89+
90+
```python
91+
from http import HTTPStatus
92+
93+
from sqlalchemy.exc import IntegrityError
94+
from fastapi import HTTPException
95+
from fastsqla import Session, Item
96+
97+
@app.post("/heroes", response_model=Item[HeroItem])
98+
async def create_hero(session: Session, new_hero: HeroBase):
99+
hero = Hero(**new_hero.model_dump())
100+
session.add(hero)
101+
try:
102+
await session.flush()
103+
except IntegrityError:
104+
raise HTTPException(
105+
status_code=HTTPStatus.CONFLICT, detail="Hero already exists"
106+
)
107+
return {"data": hero}
108+
```
109+
110+
### Rules for IntegrityError
111+
112+
- **Always re-raise as an exception.** Do not catch and silently ignore — the session is broken after an `IntegrityError` and cannot be used for further operations.
113+
- **Use `flush()`, not `commit()`**, so the error is caught within the transaction.
114+
- The `HTTPException` propagates up, triggering the automatic rollback, which is the correct behavior.
115+
116+
---
117+
118+
## open_session()
119+
120+
For code that runs **outside FastAPI endpoints** (background tasks, CLI scripts, scheduled jobs), use `open_session()`:
121+
122+
```python
123+
from fastsqla import open_session
124+
125+
async def sync_external_data():
126+
async with open_session() as session:
127+
result = await session.execute(select(Hero))
128+
heroes = result.scalars().all()
129+
for hero in heroes:
130+
hero.synced = True
131+
# auto-commit on successful exit
132+
```
133+
134+
### Lifecycle
135+
136+
`open_session()` follows the same pattern as the `Session` dependency:
137+
138+
- **Context body succeeds** — session is committed, then closed.
139+
- **Context body raises** — session is rolled back, then closed. The exception is re-raised.
140+
- **Commit itself fails** — session is rolled back, then closed. The commit exception is re-raised.
141+
142+
The third case is important: if everything in your `async with` block succeeds but the `commit()` call at exit fails (e.g., a deferred constraint violation), `open_session()` rolls back and re-raises the commit exception. You do not get a silent failure.
143+
144+
---
145+
146+
## Summary Rules
147+
148+
1. **Use `Session` for endpoints** — type-annotate a parameter and FastAPI injects it. Never instantiate sessions manually in endpoint code.
149+
2. **Never call `session.commit()` in endpoints** — FastSQLA auto-commits on success. Use `session.flush()` to get server-generated values.
150+
3. **Catch `IntegrityError` after `flush()` and re-raise as `HTTPException`** — the session is broken after an integrity error; do not attempt further operations on it.
151+
4. **Use `open_session()` outside endpoints** — background tasks, scripts, and other non-request code should use this async context manager.
152+
5. **Trust the lifecycle** — success commits, exceptions roll back, sessions always close. Do not add manual commit/rollback/close calls.

0 commit comments

Comments
 (0)