Automatic query caching and row-level cache invalidation for SQLAlchemy
Project description
sqlacache
A slick library that adds automatic queryset caching and row-level invalidation to SQLAlchemy async sessions.
session.get(User, 42) is cached. session.commit() invalidates it. That's the whole idea.
Built on top of cashews for storage and tag-based dependency tracking.
- Zero changes to your session usage — reads are intercepted automatically
- Row-level invalidation — only the rows that changed are evicted, not the whole table
- Cross-process invalidation via Redis pub/sub — all workers stay in sync
- Declarative config — map models to ops and TTLs in one place, with wildcard fallback
- Two backends:
redis://for production,mem://for dev and testing
Requirements
- Python 3.10+
- SQLAlchemy >= 1.4
- cashews >= 7.0
- Redis (for production; not needed for
mem://)
Installation
pip install sqlacache
pip install "sqlacache[redis]" # with Redis support
Using uv:
uv add "sqlacache[redis]"
Setup
Call configure() once at startup — alongside your engine setup.
from sqlalchemy.ext.asyncio import create_async_engine
from sqlacache import configure
engine = create_async_engine("postgresql+asyncpg://user:pass@localhost/mydb")
sqlacache = configure(
backend="redis://localhost:6379/1",
models={
"app.models.User": {"ops": {"get", "fetch"}, "timeout": 900},
"app.models.Product": {"ops": "all", "timeout": 3600},
"*": {"timeout": 3600},
},
)
await sqlacache.bind(engine)
That's all the wiring needed. From here, your sessions work as normal.
Usage
Reads are cached automatically
async with AsyncSession(engine) as session:
user = await session.get(User, 42) # cache miss → fetches from DB, stores result
user = await session.get(User, 42) # cache hit → returned instantly
result = await session.execute(select(User).where(User.active == True))
users = result.scalars().all() # multi-row fetch, also cached
Writes invalidate the cache automatically
async with AsyncSession(engine) as session:
user = await session.get(User, 42)
user.name = "Alice"
await session.commit() # evicts all cached reads that touched User id=42
No decorators. No manual cache keys. No changes to how you write queries.
FastAPI Example
from contextlib import asynccontextmanager
from fastapi import FastAPI, Depends
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from sqlacache import configure
engine = create_async_engine("postgresql+asyncpg://user:pass@localhost/mydb")
session_maker = async_sessionmaker(engine, expire_on_commit=False)
sqlacache = configure(
backend="redis://localhost:6379/1",
models={"app.models.User": {"ops": "all", "timeout": 300}},
)
@asynccontextmanager
async def lifespan(app: FastAPI):
await sqlacache.bind(engine)
yield
await sqlacache.disconnect()
app = FastAPI(lifespan=lifespan)
async def get_session():
async with session_maker() as session:
yield session
@app.get("/users/{user_id}")
async def get_user(user_id: int, session: AsyncSession = Depends(get_session)):
return await session.get(User, user_id) # cached after the first request
Configuration Reference
configure()
sqlacache = configure(
backend="redis://localhost:6379/1", # or "mem://" for in-memory
models={...},
prefix="sqlacache", # key prefix (default: "sqlacache")
default_timeout=3600, # TTL when not specified per model (default: 3600)
serializer="sqlalchemy", # cashews serializer (default: "sqlalchemy")
)
await sqlacache.bind(engine)
Model config
models={
"app.models.User": {
"ops": {"get", "fetch"}, # operations to cache
"timeout": 900, # TTL in seconds
},
"app.models.Product": {"ops": "all", "timeout": 3600},
"*": {"timeout": 3600}, # wildcard fallback
}
Ops:
| Op | What it covers |
|---|---|
"get" |
session.get(Model, pk) |
"fetch" |
session.execute(select(Model)) |
"count" |
select(func.count()) queries |
"exists" |
select(exists(...)) queries |
"all" |
All four above |
Backends
| Backend | URL | Notes |
|---|---|---|
| Redis | redis://host:port/db |
Production; enables cross-process invalidation |
| In-memory | mem:// |
Dev and testing; no infrastructure needed |
Manual Control
You rarely need these — sqlacache handles everything through the session automatically. They're available for edge cases:
# Cache a statement explicitly
result = await sqlacache.execute(session, select(User).where(User.active == True), timeout=300)
# Invalidate specific rows
await sqlacache.invalidate(User, pks=[42, 99])
# Invalidate all cached reads for a model
await sqlacache.invalidate(User)
# Flush everything
await sqlacache.invalidate_all()
How It Works
session.get(User, 42)
│
├── cache HIT → return immediately
└── cache MISS → execute SQL → tag result as "users:42" → return
session.commit() [User id=42 changed]
│
└── after_flush event → delete tag "users:42" → all reads that touched that row are evicted
└── (Redis) → pub/sub → other workers evict their copies too
- Cache keys are a hash of the compiled SQL + bound parameters + a per-table version counter.
- Tags (
"{tablename}:{pk}") let cashews atomically evict all keys that depended on a row. - Bulk
UPDATE ... WHERE ...bumps a table-level version so all cached queries for that model go stale.
Caveats
- Async only. Sync
Sessionis not supported yet (planned for v0.2.0). - Bulk writes use table-level invalidation.
session.execute(update(Model).where(...))evicts all cached reads for that model, not just the affected rows. - Eager-loaded relationships are not tracked. If a related row changes, queries that joined or selectin-loaded it won't be invalidated.
- Raw SQL is not intercepted.
text(...)andengine.execute()bypass sqlacache entirely.
Development
git clone https://github.com/hr-samsami/sqlacache
cd sqlacache
uv sync --extra redis --group dev
make test # unit tests (no infrastructure needed)
make lint # ruff
make format # ruff format
make typecheck # mypy
docker-compose up -d
make integration # Redis + Postgres integration tests
License
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file sqlacache-0.1.1.tar.gz.
File metadata
- Download URL: sqlacache-0.1.1.tar.gz
- Upload date:
- Size: 167.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
884cf9d861b8d865bb0177c6b61a899b146c80b3313be90c079f800183eef1d9
|
|
| MD5 |
07f53398d502cf4f478c5064af852ea7
|
|
| BLAKE2b-256 |
79b66287231f2fc7fc165a1db855ebcc964fbeb90f6027b831e6a3d034ca6535
|
Provenance
The following attestation bundles were made for sqlacache-0.1.1.tar.gz:
Publisher:
release.yml on hr-samsami/sqlacache
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
sqlacache-0.1.1.tar.gz -
Subject digest:
884cf9d861b8d865bb0177c6b61a899b146c80b3313be90c079f800183eef1d9 - Sigstore transparency entry: 1266258157
- Sigstore integration time:
-
Permalink:
hr-samsami/sqlacache@6408cbe0d27ea000001829508759ee6430f266c3 -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/hr-samsami
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@6408cbe0d27ea000001829508759ee6430f266c3 -
Trigger Event:
push
-
Statement type:
File details
Details for the file sqlacache-0.1.1-py3-none-any.whl.
File metadata
- Download URL: sqlacache-0.1.1-py3-none-any.whl
- Upload date:
- Size: 20.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bc8573283b3b82fcd20deeb7e44fda0c90d8f5c2ddc24df2891fc3e5d631231b
|
|
| MD5 |
c77b922088fbd97ba1876c2c2a7a39ad
|
|
| BLAKE2b-256 |
b333fa90d3ed051ce29d2905b54d3605188e1ad7ec24c7e17e18f57d1a8ee1e0
|
Provenance
The following attestation bundles were made for sqlacache-0.1.1-py3-none-any.whl:
Publisher:
release.yml on hr-samsami/sqlacache
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
sqlacache-0.1.1-py3-none-any.whl -
Subject digest:
bc8573283b3b82fcd20deeb7e44fda0c90d8f5c2ddc24df2891fc3e5d631231b - Sigstore transparency entry: 1266258353
- Sigstore integration time:
-
Permalink:
hr-samsami/sqlacache@6408cbe0d27ea000001829508759ee6430f266c3 -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/hr-samsami
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@6408cbe0d27ea000001829508759ee6430f266c3 -
Trigger Event:
push
-
Statement type: