|
| 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 sqlalchemy.exc import IntegrityError |
| 92 | +from fastapi import HTTPException |
| 93 | +from fastsqla import Session, Item |
| 94 | + |
| 95 | +@app.post("/heroes", response_model=Item[HeroItem]) |
| 96 | +async def create_hero(session: Session, new_hero: HeroBase): |
| 97 | + hero = Hero(**new_hero.model_dump()) |
| 98 | + session.add(hero) |
| 99 | + try: |
| 100 | + await session.flush() |
| 101 | + except IntegrityError: |
| 102 | + raise HTTPException(status_code=409, detail="Hero already exists") |
| 103 | + return {"data": hero} |
| 104 | +``` |
| 105 | + |
| 106 | +### Rules for IntegrityError |
| 107 | + |
| 108 | +- **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. |
| 109 | +- **Use `flush()`, not `commit()`**, so the error is caught within the transaction. |
| 110 | +- The `HTTPException` propagates up, triggering the automatic rollback, which is the correct behavior. |
| 111 | + |
| 112 | +--- |
| 113 | + |
| 114 | +## open_session() |
| 115 | + |
| 116 | +For code that runs **outside FastAPI endpoints** (background tasks, CLI scripts, scheduled jobs), use `open_session()`: |
| 117 | + |
| 118 | +```python |
| 119 | +from fastsqla import open_session |
| 120 | + |
| 121 | +async def sync_external_data(): |
| 122 | + async with open_session() as session: |
| 123 | + result = await session.execute(select(Hero)) |
| 124 | + heroes = result.scalars().all() |
| 125 | + for hero in heroes: |
| 126 | + hero.synced = True |
| 127 | + # auto-commit on successful exit |
| 128 | +``` |
| 129 | + |
| 130 | +### Lifecycle |
| 131 | + |
| 132 | +`open_session()` follows the same pattern as the `Session` dependency: |
| 133 | + |
| 134 | +- **Context body succeeds** — session is committed, then closed. |
| 135 | +- **Context body raises** — session is rolled back, then closed. The exception is re-raised. |
| 136 | +- **Commit itself fails** — session is rolled back, then closed. The commit exception is re-raised. |
| 137 | + |
| 138 | +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. |
| 139 | + |
| 140 | +--- |
| 141 | + |
| 142 | +## Summary Rules |
| 143 | + |
| 144 | +1. **Use `Session` for endpoints** — type-annotate a parameter and FastAPI injects it. Never instantiate sessions manually in endpoint code. |
| 145 | +2. **Never call `session.commit()` in endpoints** — FastSQLA auto-commits on success. Use `session.flush()` to get server-generated values. |
| 146 | +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. |
| 147 | +4. **Use `open_session()` outside endpoints** — background tasks, scripts, and other non-request code should use this async context manager. |
| 148 | +5. **Trust the lifecycle** — success commits, exceptions roll back, sessions always close. Do not add manual commit/rollback/close calls. |
0 commit comments