diff --git a/docs/skills/fastapi-async-sqlalchemy-modernization/SKILL.md b/docs/skills/fastapi-async-sqlalchemy-modernization/SKILL.md index 400438e..8689ce5 100644 --- a/docs/skills/fastapi-async-sqlalchemy-modernization/SKILL.md +++ b/docs/skills/fastapi-async-sqlalchemy-modernization/SKILL.md @@ -62,6 +62,9 @@ Use these concepts as the planning backbone: 7. Observability and resilience: Add pool/connection settings, logging, timeout, and health checks as first-class plan items. See the [observability reference](references/observability.md). +8. SQLModel adoption where appropriate: + Prefer SQLModel for typed ORM models and API-facing data models when it reduces duplication, while preserving SQLAlchemy async lifecycle patterns. + See the [SQLModel integration reference](references/sqlmodel.md). ### Concept Reference Map @@ -74,6 +77,7 @@ Use these concepts as the planning backbone: | Dependency injection | [Session management reference](references/session.md) | | Implicit I/O control in ORM | [Implicit I/O reference](references/implicit_io.md) | | Observability and resilience | [Observability reference](references/observability.md) | +| SQLModel adoption where appropriate | [SQLModel integration reference](references/sqlmodel.md) | ## Decision Points @@ -85,6 +89,7 @@ Use these branching decisions before proposing migration steps. | ORM usage | Already ORM 2.x style (`select`, `session.execute`) | Legacy Query API: add compatibility stage and refactor incrementally | | Session scope | Request-scoped already | Global/shared sessions found: prioritize session-scope fix first | | Lifespan | Existing FastAPI lifespan hook | No lifespan hook: introduce lifespan before broader DB changes | +| Model layer | Existing SQLModel models fit roadmap | SQLAlchemy-only models: evaluate SQLModel adoption by bounded module | | Concurrency | Background jobs/tasks use DB | No background DB use | | Transaction style | Explicit context-managed transactions | Implicit/autobegin side effects | @@ -115,6 +120,17 @@ Define one canonical model to migrate toward. Completion check: architecture diagram can explain where engine/session are created, used, and closed. +### Step 1.5: Decide SQLModel Adoption Scope + +Decide where SQLModel should be introduced during modernization. + +- Prefer SQLModel when it reduces duplicated schema definitions between ORM entities and API data models. +- Keep SQLAlchemy async engine/session lifecycle as the runtime foundation. +- Use bounded adoption first (one module or feature area), then expand after validation. +- If project is already heavily SQLAlchemy-only and stable, document rationale for staying SQLAlchemy-only. + +Completion check: plan includes an explicit SQLModel branch with target modules and non-goals. + ### Step 2: Plan Engine Modernization Plan engine creation and pool behavior. @@ -198,6 +214,7 @@ Create modernization quality gates. - Concurrency tests confirming one-session-per-task behavior. - Lifespan tests verifying cleanup calls and ordering. - Health/readiness tests including DB connectivity checks. +- If SQLModel is adopted, model-validation tests cover SQLModel table models and API models at module boundaries. Completion check: all quality gates pass under the target async configuration. @@ -222,6 +239,7 @@ A plan is complete only when it includes: - Explicit context-manager patterns for resource ownership. - AsyncExitStack composition strategy. - Transaction policy and exception behavior. +- SQLModel adoption branch (use/adopt/defer) with rationale. - Concrete tests and rollout checkpoints. - A documented advisory backlog for non-critical implicit I/O improvements. @@ -232,6 +250,7 @@ A plan is complete only when it includes: - Implicit commit/rollback behavior with unclear ownership. - Global mutable session state. - Lifespan cleanup that depends on implicit garbage collection. +- Forcing SQLModel rewrites across the entire codebase in one phase without module-level rollout. ## Output Contract diff --git a/docs/skills/fastapi-async-sqlalchemy-modernization/references/index.md b/docs/skills/fastapi-async-sqlalchemy-modernization/references/index.md index 34d919f..313de3b 100644 --- a/docs/skills/fastapi-async-sqlalchemy-modernization/references/index.md +++ b/docs/skills/fastapi-async-sqlalchemy-modernization/references/index.md @@ -13,6 +13,7 @@ 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 | --- diff --git a/docs/skills/fastapi-async-sqlalchemy-modernization/references/session.md b/docs/skills/fastapi-async-sqlalchemy-modernization/references/session.md index c40ba2b..0579c6d 100644 --- a/docs/skills/fastapi-async-sqlalchemy-modernization/references/session.md +++ b/docs/skills/fastapi-async-sqlalchemy-modernization/references/session.md @@ -57,7 +57,7 @@ async def get_db_session( session_factory: async_sessionmaker[AsyncSession] = Depends(get_session_factory), ) -> AsyncIterator[AsyncSession]: async with session_factory() as session: - yield session + yield session ``` Route usage: @@ -72,8 +72,8 @@ router = APIRouter() @router.post("/items") async def create_item(session: AsyncSession = Depends(get_db_session)) -> dict: async with session.begin(): - # write operations here - ... + # write operations here + ... return {"status": "ok"} ``` @@ -98,6 +98,12 @@ Notes: - `expire_on_commit=False` is commonly preferred in asyncio applications to reduce accidental post-commit reload behavior. - `AsyncSession.refresh()` is preferred over broad expiration patterns when state refresh is needed. +## 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. + --- ## Concurrency Rules diff --git a/docs/skills/fastapi-async-sqlalchemy-modernization/references/sqlmodel.md b/docs/skills/fastapi-async-sqlalchemy-modernization/references/sqlmodel.md new file mode 100644 index 0000000..fff215b --- /dev/null +++ b/docs/skills/fastapi-async-sqlalchemy-modernization/references/sqlmodel.md @@ -0,0 +1,123 @@ +# SQLModel Adoption and Boundaries + +!!! info "Primary sources" + - [SQLModel documentation](https://sqlmodel.tiangolo.com/) + - [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 + - Applies to: api-runtime, workers, tests + - Last reviewed: 2026-06-26 + +--- + +## Purpose + +Define when and how to use SQLModel in an async FastAPI + SQLAlchemy modernization effort. + +The goal is pragmatic adoption: use SQLModel where it reduces model duplication and improves typing ergonomics, without disrupting established async engine/session lifecycle rules. + +--- + +## 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. + +--- + +## Rules + +- 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. + +--- + +## 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 + +Use distinct models for persistence and external contracts. + +```python +from sqlmodel import Field, SQLModel + + +class UserBase(SQLModel): + email: str + display_name: str + + +class User(UserBase, table=True): + id: int | None = Field(default=None, primary_key=True) + + +class UserCreate(UserBase): + pass + + +class UserRead(UserBase): + id: int +``` + +### Pattern C: Keep async lifecycle unchanged + +```python +from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine + +engine = create_async_engine(settings.database_url, pool_pre_ping=True) +session_factory = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False) +``` + +--- + +## Interoperability Notes + +- SQLModel is designed as a thin layer over SQLAlchemy and Pydantic, so mixed codebases are expected during migration. +- Prefer one query style per module to reduce cognitive overhead. +- Keep loader strategies explicit in async paths to avoid implicit I/O surprises. + +--- + +## 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. +- 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. +- Session/transaction ownership remains consistent across both model styles. +- New model modules use explicit API boundary models where needed. + +--- + +## Testing Checks + +- Module-level tests verify CRUD semantics for adopted SQLModel models. +- API tests verify response/request model behavior for SQLModel-based endpoints. +- Regression tests confirm unchanged modules continue to function during phased rollout. + +--- + +## Migration Notes + +- 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. \ No newline at end of file