async sqlmodel
This commit is contained in:
@@ -62,6 +62,9 @@ Use these concepts as the planning backbone:
|
|||||||
7. Observability and resilience:
|
7. Observability and resilience:
|
||||||
Add pool/connection settings, logging, timeout, and health checks as first-class plan items.
|
Add pool/connection settings, logging, timeout, and health checks as first-class plan items.
|
||||||
See the [observability reference](references/observability.md).
|
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
|
### Concept Reference Map
|
||||||
|
|
||||||
@@ -74,6 +77,7 @@ Use these concepts as the planning backbone:
|
|||||||
| Dependency injection | [Session management reference](references/session.md) |
|
| Dependency injection | [Session management reference](references/session.md) |
|
||||||
| Implicit I/O control in ORM | [Implicit I/O reference](references/implicit_io.md) |
|
| Implicit I/O control in ORM | [Implicit I/O reference](references/implicit_io.md) |
|
||||||
| Observability and resilience | [Observability reference](references/observability.md) |
|
| Observability and resilience | [Observability reference](references/observability.md) |
|
||||||
|
| SQLModel adoption where appropriate | [SQLModel integration reference](references/sqlmodel.md) |
|
||||||
|
|
||||||
## Decision Points
|
## 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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| Concurrency | Background jobs/tasks use DB | No background DB use |
|
||||||
| Transaction style | Explicit context-managed transactions | Implicit/autobegin side effects |
|
| 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.
|
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
|
### Step 2: Plan Engine Modernization
|
||||||
|
|
||||||
Plan engine creation and pool behavior.
|
Plan engine creation and pool behavior.
|
||||||
@@ -198,6 +214,7 @@ Create modernization quality gates.
|
|||||||
- Concurrency tests confirming one-session-per-task behavior.
|
- Concurrency tests confirming one-session-per-task behavior.
|
||||||
- Lifespan tests verifying cleanup calls and ordering.
|
- Lifespan tests verifying cleanup calls and ordering.
|
||||||
- Health/readiness tests including DB connectivity checks.
|
- 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.
|
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.
|
- Explicit context-manager patterns for resource ownership.
|
||||||
- AsyncExitStack composition strategy.
|
- AsyncExitStack composition strategy.
|
||||||
- Transaction policy and exception behavior.
|
- Transaction policy and exception behavior.
|
||||||
|
- SQLModel adoption branch (use/adopt/defer) with rationale.
|
||||||
- Concrete tests and rollout checkpoints.
|
- Concrete tests and rollout checkpoints.
|
||||||
- A documented advisory backlog for non-critical implicit I/O improvements.
|
- 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.
|
- Implicit commit/rollback behavior with unclear ownership.
|
||||||
- Global mutable session state.
|
- Global mutable session state.
|
||||||
- Lifespan cleanup that depends on implicit garbage collection.
|
- Lifespan cleanup that depends on implicit garbage collection.
|
||||||
|
- Forcing SQLModel rewrites across the entire codebase in one phase without module-level rollout.
|
||||||
|
|
||||||
## Output Contract
|
## Output Contract
|
||||||
|
|
||||||
|
|||||||
@@ -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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ async def get_db_session(
|
|||||||
session_factory: async_sessionmaker[AsyncSession] = Depends(get_session_factory),
|
session_factory: async_sessionmaker[AsyncSession] = Depends(get_session_factory),
|
||||||
) -> AsyncIterator[AsyncSession]:
|
) -> AsyncIterator[AsyncSession]:
|
||||||
async with session_factory() as session:
|
async with session_factory() as session:
|
||||||
yield session
|
yield session
|
||||||
```
|
```
|
||||||
|
|
||||||
Route usage:
|
Route usage:
|
||||||
@@ -72,8 +72,8 @@ router = APIRouter()
|
|||||||
@router.post("/items")
|
@router.post("/items")
|
||||||
async def create_item(session: AsyncSession = Depends(get_db_session)) -> dict:
|
async def create_item(session: AsyncSession = Depends(get_db_session)) -> dict:
|
||||||
async with session.begin():
|
async with session.begin():
|
||||||
# write operations here
|
# write operations here
|
||||||
...
|
...
|
||||||
return {"status": "ok"}
|
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.
|
- `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.
|
- `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
|
## Concurrency Rules
|
||||||
|
|||||||
@@ -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.
|
||||||
Reference in New Issue
Block a user