improving async fastapi sqlmodel skill

This commit is contained in:
John Lancaster
2026-07-26 17:57:55 -05:00
parent b6393f1222
commit 4818e86a1e
13 changed files with 197 additions and 291 deletions
@@ -66,13 +66,13 @@ roles = await user.awaitable_attrs.roles
## Practical Enforcement Model
Use phased enforcement:
Require explicit I/O behavior on every async ORM path:
1. High-traffic and latency-sensitive routes: enforce explicit eager loading.
2. Background tasks and less critical paths: track and progressively tighten.
3. Add review checks to prevent newly introduced implicit-load hotspots.
1. Define loader options for relationships and deferred columns needed by the operation.
2. Use `refresh()` or awaitable attributes only when the additional query is deliberate and visible.
3. Add review checks that reject unplanned lazy-load paths.
This keeps modernization pragmatic while reducing hidden I/O over time.
This keeps event-loop behavior predictable and makes query boundaries reviewable from the code.
---
@@ -99,9 +99,3 @@ This keeps modernization pragmatic while reducing hidden I/O over time.
- Tests verify expected data is present without hidden secondary query surprises.
- Regression tests exist for routes previously affected by implicit-load failures.
---
## Migration Notes
- Start advisory: target high-risk paths first.
- As coverage improves, elevate selected rules to mandatory in code review policy.
@@ -1,6 +1,6 @@
# FastAPI Async SQLAlchemy References Index
Purpose: concept registry for modernization guidance used by this skill.
Purpose: concept registry for the principles, mechanics, and implementation guidance used by this skill.
---
@@ -13,13 +13,13 @@ Purpose: concept registry for modernization guidance used by this skill.
| Transaction boundaries | [transactions.md](transactions.md) | adopted | mandatory | platform/backend | 2026-06-17 |
| Implicit ORM I/O under asyncio | [implicit_io.md](implicit_io.md) | adopted | advisory | platform/backend | 2026-06-17 |
| Observability and resilience | [observability.md](observability.md) | adopted | mandatory | platform/backend | 2026-06-17 |
| SQLModel adoption and boundaries | [sqlmodel.md](sqlmodel.md) | adopted | advisory | platform/backend | 2026-06-26 |
| SQLModel modeling and async boundaries | [sqlmodel.md](sqlmodel.md) | adopted | mandatory | platform/backend | 2026-07-26 |
---
## How to Use This Folder
- `SKILL.md` defines the planning workflow and migration procedure.
- `SKILL.md` defines the explanatory workflow and shared mental model.
- Each concept doc defines policy-level guidance for one concern.
- Use the template in [template.md](template.md) for new concept docs.
- Keep references source-linked and implementation snippets minimal.
@@ -30,4 +30,4 @@ Purpose: concept registry for modernization guidance used by this skill.
- If a PR changes database lifecycle/session/ORM loading behavior, update the relevant concept file.
- Keep `Status`, `Decision Level`, and `Last Reviewed` current.
- Use `advisory` only when incremental rollout is intended; use `mandatory` for required runtime policy.
- Use `advisory` for recommendations that depend on application context; use `mandatory` for required runtime policy.
@@ -105,10 +105,3 @@ Readiness checks should be lightweight and bounded (timeouts), not heavy diagnos
- Readiness endpoint test covers healthy and unhealthy DB states.
- Integration test simulates disconnect/reconnect behavior.
- Load/concurrency tests validate pool behavior under stress.
---
## Migration Notes
- Start with resilient defaults (`pool_pre_ping`) and simple health policy.
- Add deeper metrics/event hooks incrementally once baseline reliability is in place.
@@ -100,9 +100,9 @@ Notes:
## SQLModel Alignment
- If using SQLModel, keep the same session ownership model: one `async_sessionmaker`, one `AsyncSession` per request/unit-of-work.
- SQLModel does not replace SQLAlchemy async lifecycle primitives; it complements model declaration and typed data handling.
- During migration, avoid mixed ad hoc patterns where some handlers create SQLAlchemy sessions directly while others use SQLModel-specific wrappers.
- Use SQLModel as the default model and statement layer while keeping the same session ownership model: one `async_sessionmaker`, one `AsyncSession` per request/unit-of-work.
- SQLModel does not replace SQLAlchemy async lifecycle primitives; it provides model declaration, validation, and typing ergonomics on top of them.
- Do not mix ad hoc session construction with the canonical async dependency.
---
@@ -139,9 +139,3 @@ Notes:
- Parallel-task tests verify no shared AsyncSession instances.
- Lifespan tests confirm session factory is initialized and teardown-safe.
---
## Migration Notes
- If current code uses global/shared sessions, fix scope first before refactoring query style.
- If legacy sync patterns are present, keep session boundary rules stable while migrating incrementally.
@@ -1,53 +1,50 @@
# SQLModel Adoption and Boundaries
# SQLModel-First Modeling and Async Boundaries
!!! info "Primary sources"
- [SQLModel documentation](https://sqlmodel.tiangolo.com/)
- [SQLModel features](https://sqlmodel.tiangolo.com/features/)
- [SQLModel advanced guide](https://sqlmodel.tiangolo.com/advanced/)
- [SQLModel FastAPI session dependency tutorial](https://sqlmodel.tiangolo.com/tutorial/fastapi/session-with-dependency/)
- [SQLModel release notes](https://sqlmodel.tiangolo.com/release-notes/)
- [SQLAlchemy asyncio extension](https://docs.sqlalchemy.org/en/21/orm/extensions/asyncio.html)
??? abstract "Decision metadata"
- Status: adopted
- Decision level: advisory
- Decision level: mandatory
- Applies to: api-runtime, workers, tests
- Last reviewed: 2026-06-26
- Last reviewed: 2026-07-26
---
## Purpose
Define when and how to use SQLModel in an async FastAPI + SQLAlchemy modernization effort.
Define SQLModel as the primary model layer for async FastAPI applications and explain how it composes with SQLAlchemy's async runtime.
The goal is pragmatic adoption: use SQLModel where it reduces model duplication and improves typing ergonomics, without disrupting established async engine/session lifecycle rules.
SQLModel is designed for FastAPI, built on Pydantic and SQLAlchemy, and intended to minimize duplication while preserving the capabilities of both. Async engine, session, transaction, and loading behavior still follow SQLAlchemy's asyncio contract.
---
## Scope and Non-Goals
- In scope: model-layer decisions, integration boundaries, phased adoption strategy.
- Out of scope: full framework rewrites and all-at-once model migration.
- In scope: table models, API data models, SQLAlchemy interoperability, async session usage, and exception criteria.
- Out of scope: replacing SQLAlchemy's async runtime primitives or claiming that synchronous tutorial examples are async patterns.
---
## Rules
- Default to SQLModel for new table models and API data models.
- Keep SQLAlchemy async primitives as the runtime base: `create_async_engine`, `async_sessionmaker`, and `AsyncSession`.
- Prefer SQLModel for new domain modules where table models and API schemas would otherwise be duplicated.
- Migrate by bounded module or feature area; do not force whole-repo conversion in one phase.
- Keep transaction and session ownership policies identical whether models are SQLAlchemy Declarative or SQLModel.
- Document explicit reasons when SQLModel is deferred for a module.
- Use SQLModel inheritance to share validated fields while keeping table, create, update, and public contracts distinct where their semantics differ.
- Use SQLAlchemy declarative models only for a concrete unsupported mapping or third-party constraint; document the reason.
- Use SQLAlchemy relationship loading options explicitly on async paths.
---
## Recommended Patterns
### Pattern A: Bounded module adoption
- Choose one feature slice (for example, billing, projects, or auth profile data).
- Introduce SQLModel models for that slice only.
- Keep unchanged modules on existing SQLAlchemy models until a dedicated migration phase.
### Pattern B: Data model split for API boundaries
### Pattern A: Data model split for API boundaries
Use distinct models for persistence and external contracts.
@@ -72,20 +69,29 @@ class UserRead(UserBase):
id: int
```
### Pattern C: Keep async lifecycle unchanged
### Pattern B: Keep SQLModel models with the async runtime
```python
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from sqlmodel import select
engine = create_async_engine(settings.database_url, pool_pre_ping=True)
session_factory = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
async with session_factory() as session:
users = (await session.scalars(select(User))).all()
```
`sqlmodel.select()` keeps SQLModel's typing-oriented statement construction, while `AsyncSession.scalars()` and the surrounding lifecycle come from SQLAlchemy.
---
## Interoperability Notes
- SQLModel is designed as a thin layer over SQLAlchemy and Pydantic, so mixed codebases are expected during migration.
- A SQLModel table model is a SQLAlchemy model and can participate in SQLAlchemy relationships, statements, loader options, and sessions.
- A SQLModel model is also a Pydantic model; non-table models are useful for request and response contracts.
- SQLModel's official FastAPI dependency tutorial currently uses synchronous `Session`; translate the ownership pattern, not the concrete session type, for async applications.
- SQLModel's advanced guide still lists dedicated async documentation as future work, so use SQLAlchemy's asyncio documentation as the authority for runtime mechanics.
- Prefer one query style per module to reduce cognitive overhead.
- Keep loader strategies explicit in async paths to avoid implicit I/O surprises.
@@ -93,31 +99,29 @@ session_factory = async_sessionmaker(engine, class_=AsyncSession, expire_on_comm
## Anti-Patterns
- Treating SQLModel adoption as equivalent to async-session modernization.
- Rewriting all models at once without rollback checkpoints.
- Introducing SQLModel in handlers while keeping old global/shared session patterns.
- Treating SQLModel as an alternative to SQLAlchemy rather than a layer built on it.
- Copying a synchronous `Session` example into an async request path.
- Constructing sessions in handlers instead of using the application session factory.
- Mixing multiple query/session idioms within the same module without clear conventions.
---
## Operational Checks
- Modernized module documents whether it is SQLModel-first or SQLAlchemy-only.
- New model modules are SQLModel-first; exceptions state the unsupported need or constraint.
- Session/transaction ownership remains consistent across both model styles.
- New model modules use explicit API boundary models where needed.
- Table, create, update, and public models share fields intentionally without exposing persistence-only data.
---
## Testing Checks
- Module-level tests verify CRUD semantics for adopted SQLModel models.
- Module-level tests verify CRUD semantics for SQLModel models through `AsyncSession`.
- API tests verify response/request model behavior for SQLModel-based endpoints.
- Regression tests confirm unchanged modules continue to function during phased rollout.
- Relationship tests verify async loader strategies do not depend on implicit I/O.
---
## Migration Notes
## Version Checks
- Start with low-risk bounded domains.
- Expand only after validation of session lifecycle, transaction behavior, and endpoint correctness.
- Maintain a tracked backlog of deferred modules with rationale and planned phase.
- Verify installed SQLModel, SQLAlchemy, and Pydantic versions together when using newly added typing or ORM features.
@@ -57,8 +57,3 @@ Describe what this concept governs and why it exists.
- Test 1
- Test 2
---
## Migration Notes
- Staged rollout notes and compatibility caveats.
@@ -102,10 +102,3 @@ Use nested transactions only when partial failure semantics are explicitly requi
- Failure path test verifies rollback behavior.
- Tests cover concurrency-sensitive write flows.
- Savepoint usage (if present) has dedicated behavior tests.
---
## Migration Notes
- First stabilize session scope, then normalize transaction ownership.
- Replace ad hoc commit patterns incrementally with bounded write units.