sqlacache 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. sqlacache-0.1.0/.github/workflows/ci.yml +26 -0
  2. sqlacache-0.1.0/.github/workflows/integration.yml +18 -0
  3. sqlacache-0.1.0/.github/workflows/release.yml +21 -0
  4. sqlacache-0.1.0/.gitignore +49 -0
  5. sqlacache-0.1.0/.pre-commit-config.yaml +32 -0
  6. sqlacache-0.1.0/.prek/README +2 -0
  7. sqlacache-0.1.0/CHANGELOG.md +13 -0
  8. sqlacache-0.1.0/CONTRIBUTING.md +49 -0
  9. sqlacache-0.1.0/LICENSE +21 -0
  10. sqlacache-0.1.0/Makefile +37 -0
  11. sqlacache-0.1.0/PKG-INFO +298 -0
  12. sqlacache-0.1.0/README.md +254 -0
  13. sqlacache-0.1.0/docker-compose.yml +14 -0
  14. sqlacache-0.1.0/openspec/changes/implement-mvp/.openspec.yaml +2 -0
  15. sqlacache-0.1.0/openspec/changes/implement-mvp/design.md +116 -0
  16. sqlacache-0.1.0/openspec/changes/implement-mvp/proposal.md +42 -0
  17. sqlacache-0.1.0/openspec/changes/implement-mvp/specs/1-project-infrastructure.md +321 -0
  18. sqlacache-0.1.0/openspec/changes/implement-mvp/specs/2-cache-configuration.md +212 -0
  19. sqlacache-0.1.0/openspec/changes/implement-mvp/specs/3-query-interception.md +181 -0
  20. sqlacache-0.1.0/openspec/changes/implement-mvp/specs/4-cache-key-generation.md +190 -0
  21. sqlacache-0.1.0/openspec/changes/implement-mvp/specs/5-row-level-invalidation.md +294 -0
  22. sqlacache-0.1.0/openspec/changes/implement-mvp/specs/6-cashews-transport.md +289 -0
  23. sqlacache-0.1.0/openspec/changes/implement-mvp/specs/7-cross-process-invalidation.md +315 -0
  24. sqlacache-0.1.0/openspec/changes/implement-mvp/tasks.md +158 -0
  25. sqlacache-0.1.0/openspec/config.yaml +20 -0
  26. sqlacache-0.1.0/pyproject.toml +100 -0
  27. sqlacache-0.1.0/sqlacache-architecture.md +1277 -0
  28. sqlacache-0.1.0/src/sqlacache/__init__.py +14 -0
  29. sqlacache-0.1.0/src/sqlacache/config.py +175 -0
  30. sqlacache-0.1.0/src/sqlacache/contrib/__init__.py +1 -0
  31. sqlacache-0.1.0/src/sqlacache/contrib/fastapi.py +1 -0
  32. sqlacache-0.1.0/src/sqlacache/contrib/prometheus.py +1 -0
  33. sqlacache-0.1.0/src/sqlacache/exceptions.py +13 -0
  34. sqlacache-0.1.0/src/sqlacache/interceptor.py +128 -0
  35. sqlacache-0.1.0/src/sqlacache/invalidation.py +45 -0
  36. sqlacache-0.1.0/src/sqlacache/manager.py +348 -0
  37. sqlacache-0.1.0/src/sqlacache/pubsub/__init__.py +3 -0
  38. sqlacache-0.1.0/src/sqlacache/pubsub/redis.py +89 -0
  39. sqlacache-0.1.0/src/sqlacache/py.typed +0 -0
  40. sqlacache-0.1.0/src/sqlacache/serializers/__init__.py +5 -0
  41. sqlacache-0.1.0/src/sqlacache/serializers/json.py +24 -0
  42. sqlacache-0.1.0/src/sqlacache/transport/__init__.py +45 -0
  43. sqlacache-0.1.0/src/sqlacache/transport/cashews.py +136 -0
  44. sqlacache-0.1.0/src/sqlacache/utils/__init__.py +1 -0
  45. sqlacache-0.1.0/src/sqlacache/utils/key_generation.py +40 -0
  46. sqlacache-0.1.0/src/sqlacache/utils/query_analysis.py +108 -0
  47. sqlacache-0.1.0/src/sqlacache/utils/sync_wrapper.py +16 -0
  48. sqlacache-0.1.0/tests/__init__.py +1 -0
  49. sqlacache-0.1.0/tests/conftest.py +102 -0
  50. sqlacache-0.1.0/tests/integration/__init__.py +1 -0
  51. sqlacache-0.1.0/tests/integration/conftest.py +50 -0
  52. sqlacache-0.1.0/tests/integration/test_cross_process_invalidation.py +25 -0
  53. sqlacache-0.1.0/tests/pubsub/__init__.py +1 -0
  54. sqlacache-0.1.0/tests/pubsub/test_redis.py +44 -0
  55. sqlacache-0.1.0/tests/test_config.py +26 -0
  56. sqlacache-0.1.0/tests/test_interceptor.py +67 -0
  57. sqlacache-0.1.0/tests/test_invalidation.py +40 -0
  58. sqlacache-0.1.0/tests/test_key_generation.py +26 -0
  59. sqlacache-0.1.0/tests/test_manager.py +138 -0
  60. sqlacache-0.1.0/tests/test_query_analysis.py +35 -0
  61. sqlacache-0.1.0/tests/transport/__init__.py +1 -0
  62. sqlacache-0.1.0/tests/transport/conftest.py +38 -0
  63. sqlacache-0.1.0/tests/transport/test_cashews.py +102 -0
  64. sqlacache-0.1.0/uv.lock +1694 -0
@@ -0,0 +1,26 @@
1
+ name: CI
2
+
3
+ on:
4
+ pull_request:
5
+ push:
6
+ branches: ["main"]
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ fail-fast: false
13
+ matrix:
14
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
15
+
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+ - uses: astral-sh/setup-uv@v5
19
+ with:
20
+ python-version: ${{ matrix.python-version }}
21
+ - run: uv sync --extra redis --group dev
22
+ - run: uv run ruff check src/ tests/
23
+ - run: uv run ruff format --check src/ tests/
24
+ - run: uv run mypy src/
25
+ - run: uv run ty src/ || true
26
+ - run: uv run pytest -m "not integration"
@@ -0,0 +1,18 @@
1
+ name: Integration
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ schedule:
6
+ - cron: "0 6 * * 1"
7
+
8
+ jobs:
9
+ integration:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ - uses: astral-sh/setup-uv@v5
14
+ with:
15
+ python-version: "3.12"
16
+ - run: docker compose up -d
17
+ - run: uv sync --extra redis --group dev
18
+ - run: uv run pytest -m integration
@@ -0,0 +1,21 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ jobs:
9
+ publish:
10
+ runs-on: ubuntu-latest
11
+ permissions:
12
+ id-token: write
13
+ contents: read
14
+
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ - uses: astral-sh/setup-uv@v5
18
+ with:
19
+ python-version: "3.10"
20
+ - run: uv build
21
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,49 @@
1
+ # Python
2
+ .venv/
3
+ __pycache__/
4
+ *.py[cod]
5
+ *.egg-info/
6
+ dist/
7
+ build/
8
+
9
+ # Type checkers / linters
10
+ .mypy_cache/
11
+ .ruff_cache/
12
+ .ty_cache/
13
+
14
+ # Test / coverage
15
+ .pytest_cache/
16
+ .coverage
17
+ htmlcov/
18
+
19
+ # Package managers
20
+ .uv-cache/
21
+ uv.lock.bak
22
+
23
+ # OS
24
+ .DS_Store
25
+ Thumbs.db
26
+
27
+ # IDE
28
+ .idea/
29
+ .vscode/
30
+
31
+ # AI assistant tool directories (not project source)
32
+ .claude/
33
+ CLAUDE.md
34
+ .codex/
35
+ .gemini/
36
+ .opencode/
37
+ .github/prompts/
38
+ .github/skills/
39
+
40
+ # prek hook runner logs and scratch
41
+ .prek/prek.log
42
+ .prek/scratch/
43
+
44
+ # Scratch / research notes not part of the project
45
+ research.md
46
+ landscape-research.md
47
+
48
+ # Examples (not part of the library itself)
49
+ examples/
@@ -0,0 +1,32 @@
1
+ repos:
2
+ - repo: https://github.com/pre-commit/pre-commit-hooks
3
+ rev: v5.0.0
4
+ hooks:
5
+ - id: trailing-whitespace
6
+ - id: end-of-file-fixer
7
+ - id: check-yaml
8
+ - id: check-toml
9
+ - id: check-added-large-files
10
+
11
+ - repo: local
12
+ hooks:
13
+ - id: ruff-check
14
+ name: ruff check
15
+ entry: uv run ruff check --fix src/ tests/
16
+ language: system
17
+ pass_filenames: false
18
+ - id: ruff-format
19
+ name: ruff format
20
+ entry: uv run ruff format src/ tests/
21
+ language: system
22
+ pass_filenames: false
23
+ - id: mypy
24
+ name: mypy
25
+ entry: uv run mypy src/
26
+ language: system
27
+ pass_filenames: false
28
+ - id: ty
29
+ name: ty
30
+ entry: uv run ty check src/
31
+ language: system
32
+ pass_filenames: false
@@ -0,0 +1,2 @@
1
+ This directory is maintained by the prek project.
2
+ Learn more: https://github.com/j178/prek
@@ -0,0 +1,13 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0
4
+
5
+ - Initial alpha MVP release.
6
+ - Added package scaffolding, linting, typing, tests, CI, and build metadata.
7
+ - Added validated configuration and cache manager APIs via `configure(...)`.
8
+ - Added `cashews` transport integration for `mem://` and `redis://` backends.
9
+ - Added automatic async ORM caching for configured reads, including `AsyncSession.get(...)`.
10
+ - Added row-level invalidation for ORM inserts, updates, deletes, and bulk mutations.
11
+ - Added Redis pub/sub support for cross-process invalidation.
12
+ - Added unit tests, Redis integration coverage, and an external wheel-install smoke test workflow.
13
+ - Sync session support remains deferred.
@@ -0,0 +1,49 @@
1
+ # Contributing
2
+
3
+ ## Environment
4
+
5
+ ```bash
6
+ uv sync --extra redis --group dev
7
+ prek install
8
+ ```
9
+
10
+ ## Common Commands
11
+
12
+ ```bash
13
+ make lint
14
+ make format
15
+ make typecheck
16
+ make test
17
+ ```
18
+
19
+ ## Current Scope
20
+
21
+ - The implemented runtime is async SQLAlchemy
22
+ - Redis integration is supported and tested
23
+ - Sync session support is not implemented yet
24
+ - The architecture document includes planned work beyond the current MVP
25
+
26
+ ## Release Smoke Test
27
+
28
+ To verify the built package outside the repo, run:
29
+
30
+ ```bash
31
+ .venv/bin/python -m build
32
+ .venv/bin/python -m twine check dist/*
33
+ python -m venv /tmp/sqlacache-release-venv
34
+ UV_CACHE_DIR=.uv-cache uv pip install --python /tmp/sqlacache-release-venv/bin/python dist/*.whl
35
+ UV_CACHE_DIR=.uv-cache uv pip install --python /tmp/sqlacache-release-venv/bin/python redis aiosqlite
36
+ ```
37
+
38
+ Then run a standalone script from `/tmp` that imports `sqlacache` from that external environment and validates:
39
+
40
+ - automatic `session.get(...)` caching
41
+ - Redis keys appearing after cache population
42
+ - invalidation after an update
43
+ - recache with fresh data after invalidation
44
+
45
+ ## Workflow
46
+
47
+ - Keep changes focused and aligned with the active OpenSpec change.
48
+ - Run the relevant local checks before opening a pull request.
49
+ - CI should pass before merging.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Hamidreza Samsami
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,37 @@
1
+ .PHONY: help install lint format typecheck test testcov integration clean
2
+
3
+ help:
4
+ @printf "Available targets:\n"
5
+ @printf " install Install project dependencies with uv\n"
6
+ @printf " lint Run ruff checks\n"
7
+ @printf " format Format code with ruff\n"
8
+ @printf " typecheck Run mypy and ty\n"
9
+ @printf " test Run unit tests\n"
10
+ @printf " testcov Run unit tests with coverage\n"
11
+ @printf " integration Run integration tests\n"
12
+ @printf " clean Remove caches, build artifacts, and virtualenv\n"
13
+
14
+ install:
15
+ uv sync --extra redis --group dev
16
+
17
+ lint:
18
+ uv run ruff check src/ tests/
19
+
20
+ format:
21
+ uv run ruff format src/ tests/
22
+
23
+ typecheck:
24
+ uv run mypy src/
25
+ uv run ty check src/ || true
26
+
27
+ test:
28
+ uv run pytest -x -m "not integration"
29
+
30
+ testcov:
31
+ uv run pytest --cov=sqlacache --cov-report=term-missing -m "not integration"
32
+
33
+ integration:
34
+ uv run pytest -m integration
35
+
36
+ clean:
37
+ rm -rf .venv .mypy_cache .pytest_cache .ruff_cache dist build src/*.egg-info .coverage htmlcov
@@ -0,0 +1,298 @@
1
+ Metadata-Version: 2.4
2
+ Name: sqlacache
3
+ Version: 0.1.0
4
+ Summary: Django-cacheops-style declarative caching with automatic row-level invalidation for SQLAlchemy
5
+ Project-URL: Homepage, https://github.com/persix/sqlacache
6
+ Project-URL: Repository, https://github.com/persix/sqlacache
7
+ Project-URL: Documentation, https://github.com/persix/sqlacache/blob/main/README.md
8
+ Project-URL: Issues, https://github.com/persix/sqlacache/issues
9
+ Author-email: Hamidreza Samsami <hamidreza.samsami@gmail.com>
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: asyncio,cache,orm,redis,sqlalchemy
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Framework :: AsyncIO
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python
19
+ Classifier: Programming Language :: Python :: 3
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Programming Language :: Python :: Implementation :: CPython
25
+ Classifier: Topic :: Database
26
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
27
+ Requires-Python: >=3.10
28
+ Requires-Dist: cashews<8.0,>=7.0
29
+ Requires-Dist: sqlalchemy>=1.4
30
+ Provides-Extra: all
31
+ Requires-Dist: asyncpg>=0.30.0; extra == 'all'
32
+ Requires-Dist: cashews[dill,diskcache,redis,speedup]<8.0,>=7.0; extra == 'all'
33
+ Provides-Extra: dill
34
+ Requires-Dist: cashews[dill]<8.0,>=7.0; extra == 'dill'
35
+ Provides-Extra: diskcache
36
+ Requires-Dist: cashews[diskcache]<8.0,>=7.0; extra == 'diskcache'
37
+ Provides-Extra: postgresql
38
+ Requires-Dist: asyncpg>=0.30.0; extra == 'postgresql'
39
+ Provides-Extra: redis
40
+ Requires-Dist: cashews[redis]<8.0,>=7.0; extra == 'redis'
41
+ Provides-Extra: speedup
42
+ Requires-Dist: cashews[speedup]<8.0,>=7.0; extra == 'speedup'
43
+ Description-Content-Type: text/markdown
44
+
45
+ # sqlacache
46
+
47
+ A slick library that adds automatic queryset caching and row-level invalidation to SQLAlchemy async sessions.
48
+
49
+ [![Tests](https://github.com/hr-samsami/sqlacache/actions/workflows/ci.yml/badge.svg)](https://github.com/hr-samsami/sqlacache/actions/workflows/ci.yml)
50
+ [![PyPI version](https://img.shields.io/pypi/v/sqlacache)](https://pypi.org/project/sqlacache/)
51
+ [![Python](https://img.shields.io/pypi/pyversions/sqlacache)](https://pypi.org/project/sqlacache/)
52
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
53
+
54
+ ---
55
+
56
+ `session.get(User, 42)` is cached. `session.commit()` invalidates it. That's the whole idea.
57
+
58
+ Built on top of [`cashews`](https://github.com/Krukov/cashews) for storage and tag-based dependency tracking.
59
+
60
+ - Zero changes to your session usage — reads are intercepted automatically
61
+ - Row-level invalidation — only the rows that changed are evicted, not the whole table
62
+ - Cross-process invalidation via Redis pub/sub — all workers stay in sync
63
+ - Declarative config — map models to ops and TTLs in one place, with wildcard fallback
64
+ - Two backends: `redis://` for production, `mem://` for dev and testing
65
+
66
+ ---
67
+
68
+ ## Requirements
69
+
70
+ - Python 3.10+
71
+ - SQLAlchemy >= 1.4
72
+ - cashews >= 7.0
73
+ - Redis (for production; not needed for `mem://`)
74
+
75
+ ---
76
+
77
+ ## Installation
78
+
79
+ ```bash
80
+ pip install sqlacache
81
+ pip install "sqlacache[redis]" # with Redis support
82
+ ```
83
+
84
+ Using `uv`:
85
+
86
+ ```bash
87
+ uv add "sqlacache[redis]"
88
+ ```
89
+
90
+ ---
91
+
92
+ ## Setup
93
+
94
+ Call `configure()` once at startup — alongside your engine setup.
95
+
96
+ ```python
97
+ from sqlalchemy.ext.asyncio import create_async_engine
98
+ from sqlacache import configure
99
+
100
+ engine = create_async_engine("postgresql+asyncpg://user:pass@localhost/mydb")
101
+
102
+ sqlacache = configure(
103
+ backend="redis://localhost:6379/1",
104
+ models={
105
+ "app.models.User": {"ops": {"get", "fetch"}, "timeout": 900},
106
+ "app.models.Product": {"ops": "all", "timeout": 3600},
107
+ "*": {"timeout": 3600},
108
+ },
109
+ )
110
+ await sqlacache.bind(engine)
111
+ ```
112
+
113
+ That's all the wiring needed. From here, your sessions work as normal.
114
+
115
+ ---
116
+
117
+ ## Usage
118
+
119
+ ### Reads are cached automatically
120
+
121
+ ```python
122
+ async with AsyncSession(engine) as session:
123
+ user = await session.get(User, 42) # cache miss → fetches from DB, stores result
124
+ user = await session.get(User, 42) # cache hit → returned instantly
125
+
126
+ result = await session.execute(select(User).where(User.active == True))
127
+ users = result.scalars().all() # multi-row fetch, also cached
128
+ ```
129
+
130
+ ### Writes invalidate the cache automatically
131
+
132
+ ```python
133
+ async with AsyncSession(engine) as session:
134
+ user = await session.get(User, 42)
135
+ user.name = "Alice"
136
+ await session.commit() # evicts all cached reads that touched User id=42
137
+ ```
138
+
139
+ No decorators. No manual cache keys. No changes to how you write queries.
140
+
141
+ ---
142
+
143
+ ## FastAPI Example
144
+
145
+ ```python
146
+ from contextlib import asynccontextmanager
147
+ from fastapi import FastAPI, Depends
148
+ from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
149
+ from sqlacache import configure
150
+
151
+ engine = create_async_engine("postgresql+asyncpg://user:pass@localhost/mydb")
152
+ session_maker = async_sessionmaker(engine, expire_on_commit=False)
153
+
154
+ sqlacache = configure(
155
+ backend="redis://localhost:6379/1",
156
+ models={"app.models.User": {"ops": "all", "timeout": 300}},
157
+ )
158
+
159
+
160
+ @asynccontextmanager
161
+ async def lifespan(app: FastAPI):
162
+ await sqlacache.bind(engine)
163
+ yield
164
+ await sqlacache.disconnect()
165
+
166
+
167
+ app = FastAPI(lifespan=lifespan)
168
+
169
+
170
+ async def get_session():
171
+ async with session_maker() as session:
172
+ yield session
173
+
174
+
175
+ @app.get("/users/{user_id}")
176
+ async def get_user(user_id: int, session: AsyncSession = Depends(get_session)):
177
+ return await session.get(User, user_id) # cached after the first request
178
+ ```
179
+
180
+ ---
181
+
182
+ ## Configuration Reference
183
+
184
+ ### `configure()`
185
+
186
+ ```python
187
+ sqlacache = configure(
188
+ backend="redis://localhost:6379/1", # or "mem://" for in-memory
189
+ models={...},
190
+ prefix="sqlacache", # key prefix (default: "sqlacache")
191
+ default_timeout=3600, # TTL when not specified per model (default: 3600)
192
+ serializer="sqlalchemy", # cashews serializer (default: "sqlalchemy")
193
+ )
194
+ await sqlacache.bind(engine)
195
+ ```
196
+
197
+ ### Model config
198
+
199
+ ```python
200
+ models={
201
+ "app.models.User": {
202
+ "ops": {"get", "fetch"}, # operations to cache
203
+ "timeout": 900, # TTL in seconds
204
+ },
205
+ "app.models.Product": {"ops": "all", "timeout": 3600},
206
+ "*": {"timeout": 3600}, # wildcard fallback
207
+ }
208
+ ```
209
+
210
+ **Ops:**
211
+
212
+ | Op | What it covers |
213
+ | --- | --- |
214
+ | `"get"` | `session.get(Model, pk)` |
215
+ | `"fetch"` | `session.execute(select(Model))` |
216
+ | `"count"` | `select(func.count())` queries |
217
+ | `"exists"` | `select(exists(...))` queries |
218
+ | `"all"` | All four above |
219
+
220
+ ### Backends
221
+
222
+ | Backend | URL | Notes |
223
+ | --- | --- | --- |
224
+ | Redis | `redis://host:port/db` | Production; enables cross-process invalidation |
225
+ | In-memory | `mem://` | Dev and testing; no infrastructure needed |
226
+
227
+ ---
228
+
229
+ ## Manual Control
230
+
231
+ You rarely need these — sqlacache handles everything through the session automatically. They're available for edge cases:
232
+
233
+ ```python
234
+ # Cache a statement explicitly
235
+ result = await sqlacache.execute(session, select(User).where(User.active == True), timeout=300)
236
+
237
+ # Invalidate specific rows
238
+ await sqlacache.invalidate(User, pks=[42, 99])
239
+
240
+ # Invalidate all cached reads for a model
241
+ await sqlacache.invalidate(User)
242
+
243
+ # Flush everything
244
+ await sqlacache.invalidate_all()
245
+ ```
246
+
247
+ ---
248
+
249
+ ## How It Works
250
+
251
+ ```text
252
+ session.get(User, 42)
253
+
254
+ ├── cache HIT → return immediately
255
+ └── cache MISS → execute SQL → tag result as "users:42" → return
256
+
257
+ session.commit() [User id=42 changed]
258
+
259
+ └── after_flush event → delete tag "users:42" → all reads that touched that row are evicted
260
+ └── (Redis) → pub/sub → other workers evict their copies too
261
+ ```
262
+
263
+ - Cache keys are a hash of the compiled SQL + bound parameters + a per-table version counter.
264
+ - Tags (`"{tablename}:{pk}"`) let cashews atomically evict all keys that depended on a row.
265
+ - Bulk `UPDATE ... WHERE ...` bumps a table-level version so all cached queries for that model go stale.
266
+
267
+ ---
268
+
269
+ ## Caveats
270
+
271
+ - **Async only.** Sync `Session` is not supported yet (planned for v0.2.0).
272
+ - **Bulk writes use table-level invalidation.** `session.execute(update(Model).where(...))` evicts all cached reads for that model, not just the affected rows.
273
+ - **Eager-loaded relationships are not tracked.** If a related row changes, queries that joined or selectin-loaded it won't be invalidated.
274
+ - **Raw SQL is not intercepted.** `text(...)` and `engine.execute()` bypass sqlacache entirely.
275
+
276
+ ---
277
+
278
+ ## Development
279
+
280
+ ```bash
281
+ git clone https://github.com/hr-samsami/sqlacache
282
+ cd sqlacache
283
+ uv sync --extra redis --group dev
284
+
285
+ make test # unit tests (no infrastructure needed)
286
+ make lint # ruff
287
+ make format # ruff format
288
+ make typecheck # mypy
289
+
290
+ docker-compose up -d
291
+ make integration # Redis + Postgres integration tests
292
+ ```
293
+
294
+ ---
295
+
296
+ ## License
297
+
298
+ [MIT](LICENSE)