Async Flows¶
pyrsql itself does not perform database I/O. It builds SQLAlchemy statements
and criteria objects. Because of that, the same compiled Select objects work
for both:
- synchronous
Session - asynchronous
AsyncSession
The async work happens in your application when you execute the statement.
What async support means in pyrsql¶
pyrsql is async-compatible, not async-only:
- the core query/sort/page APIs are synchronous object transformations
- the FastAPI adapter parses request data synchronously
- async execution happens when your app runs the generated SQLAlchemy
statements through
AsyncSession
This keeps the same query pipeline usable in both sync and async applications.
SQLAlchemy async session¶
from sqlalchemy.ext.asyncio import (
AsyncSession,
async_sessionmaker,
create_async_engine,
)
engine = create_async_engine(
"sqlite+aiosqlite:///./app.db",
)
SessionFactory = async_sessionmaker(
engine,
expire_on_commit=False,
)
async def get_async_session():
async with SessionFactory() as session:
yield session
FastAPI async route¶
from typing import Annotated, Any
from fastapi import Depends, FastAPI
from sqlalchemy.ext.asyncio import AsyncSession
from pyrsql.integrations.fastapi import FastAPISQLAlchemyIntegration
app = FastAPI()
integration = FastAPISQLAlchemyIntegration()
@app.get("/users")
async def list_users(
stmt: Annotated[Any, Depends(integration.select_dependency(User))],
session: Annotated[AsyncSession, Depends(get_async_session)],
):
result = await session.scalars(stmt)
return result.all()
The dependency is still synchronous from pyrsql's perspective because it only
parses request data and builds one SQLAlchemy Select.
Count and pagination¶
@app.get("/users/count")
async def count_users(
stmt: Annotated[Any, Depends(integration.count_select_dependency(User))],
session: Annotated[AsyncSession, Depends(get_async_session)],
):
return {"count": await session.scalar(stmt)}
@app.get("/users/paginated")
async def paginated_users(
bundle: Annotated[
Any,
Depends(integration.paginated_select_dependency(User)),
],
session: Annotated[AsyncSession, Depends(get_async_session)],
):
items = (await session.scalars(bundle.statement)).all()
total = await session.scalar(bundle.count_statement)
return {"items": items, "total": total}
Direct async execution without FastAPI¶
from sqlalchemy import select
import pyrsql
query = pyrsql.parse("name==demo")
stmt = query.apply(select(User), User, orm=sqlalchemy_orm)
async with SessionFactory() as session:
result = await session.scalars(stmt)
users = result.all()
Constraints and guidance¶
- Do not share one
AsyncSessionacross concurrent tasks. pyrsqlcan safely reuse immutable options/configuration objects across async requests.- The integration layer only builds statements; it does not manage session lifetime.
- Parse and page-validation failures still become structured
HTTP 400responses in FastAPI. - Semantic and backend integration failures still become structured
HTTP 422responses in FastAPI.
Free-threaded note¶
Async support and free-threaded support are separate concerns:
- async support is about how your application executes the generated SQLAlchemy statements
- free-threaded support is about protecting shared mutable caches used by the integration and SQLAlchemy helper layers
You can use async routes without a free-threaded Python build, and you can benefit from free-threaded-safe cache handling in synchronous applications too.
Test coverage¶
The test suite includes async coverage at three levels:
- FastAPI adapter behavior on
async defroutes - SQLAlchemy pipeline execution with
AsyncSession - FastAPI + SQLAlchemy end-to-end async integration