Add initial SQLmem package structure with SQL parser, cache manager, column registry, and tests
This commit is contained in:
+202
@@ -0,0 +1,202 @@
|
||||
# SQLmem — Project Specification
|
||||
|
||||
## Cíl
|
||||
|
||||
Python modul fungující jako **transparentní cache vrstva mezi SQLAlchemy a databází**. Aplikace volá SQLAlchemy stejně jako dosud — SQLmem sedí mezi nimi, zachytává SELECT dotazy a vrací výsledky z in-memory SQLite cache. Zápisy (INSERT/UPDATE/DELETE) jsou přepouštěny přímo do databáze bez zásahu.
|
||||
|
||||
---
|
||||
|
||||
## Architektura
|
||||
|
||||
```
|
||||
Aplikace (SQLAlchemy)
|
||||
│
|
||||
▼
|
||||
[ SQLmem Proxy ]
|
||||
┌───────────────────────────────┐
|
||||
│ SQL Parser │ → rozezná SELECT vs. zápis
|
||||
│ Cache Manager (SQLite RAM) │ → drží data v paměti
|
||||
│ Query Executor │ → cache hit / miss logika
|
||||
└───────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
Databáze (přes původní SQLAlchemy engine)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API
|
||||
|
||||
Modul implementuje **SQLAlchemy event hooks / custom engine wrapper** — aplikace nemusí měnit způsob volání ani formát vrácených dat.
|
||||
|
||||
```python
|
||||
from sqlmem import CachingEngine
|
||||
from sqlalchemy import create_engine
|
||||
|
||||
base_engine = create_engine("postgresql://...")
|
||||
engine = CachingEngine(base_engine)
|
||||
|
||||
# Dál se pracuje stejně jako s běžným SQLAlchemy enginem:
|
||||
with engine.connect() as conn:
|
||||
result = conn.execute(text("SELECT * FROM users WHERE status = 'active'"))
|
||||
```
|
||||
|
||||
`CachingEngine` vrací objekty kompatibilní s `CursorResult` — `fetchall()`, `fetchone()`, `keys()` atd. fungují stejně.
|
||||
|
||||
---
|
||||
|
||||
## Cache backend
|
||||
|
||||
- **SQLite in-memory** jako primární úložiště — veškeré dotazy běží v RAM.
|
||||
- **Persistence na disk** (`cache.db`) ve třech situacích:
|
||||
- **Při startu**: pokud soubor existuje, načte se do paměti (`ATTACH` + kopie).
|
||||
- **Periodicky každou hodinu**: snapshot in-memory SQLite se zapíše na disk (backup API).
|
||||
- **Při vypnutí**: finální zápis na disk před ukončením (signal handler + context manager).
|
||||
- Celé tabulky se při cache miss načtou z databáze a drží v paměti.
|
||||
|
||||
---
|
||||
|
||||
## Komponenty
|
||||
|
||||
### 1. SQL Parser
|
||||
- Detekuje typ dotazu (SELECT / zápis).
|
||||
- Extrahuje názvy tabulek z FROM a JOIN klauzulí.
|
||||
- Extrahuje seznam požadovaných sloupců.
|
||||
- Detekuje `SELECT *` (wildcard) a JOIN — vyhodí `UnsupportedQueryError`.
|
||||
- Rozhoduje, zda je dotaz obsloužitelný z cache.
|
||||
|
||||
### 2. Column Registry
|
||||
|
||||
Modul se **za běhu učí**, jaké sloupce z každé tabulky aplikace potřebuje — nevyžaduje žádnou předem danou konfiguraci.
|
||||
|
||||
**Logika při každém příchozím dotazu:**
|
||||
|
||||
1. Parser detekuje `SELECT *` nebo JOIN → vyhodí `UnsupportedQueryError` (není implementováno).
|
||||
2. Parser extrahuje `(tabulka, sloupce)` z dotazu.
|
||||
3. Registry provede **union** nově požadovaných sloupců s již známými.
|
||||
4. Cache Manager zkontroluje, zda cache pro danou tabulku obsahuje všechny potřebné sloupce:
|
||||
- **Ano** → dotaz jde přímo do SQLite RAM (cache hit).
|
||||
- **Ne** → re-fetch tabulky z DB s rozšířenou sadou sloupců → přepíše cache → dotaz do SQLite RAM.
|
||||
|
||||
**Příklad akumulace sloupců:**
|
||||
|
||||
```
|
||||
Dotaz 1: SELECT A, B FROM T3 → Registry: T3 = {A, B} → fetch T3(A,B) z DB
|
||||
Dotaz 2: SELECT A, D FROM T3 → Registry: T3 = {A, B, D} → re-fetch T3(A,B,D) z DB
|
||||
Dotaz 3: SELECT B FROM T3 → cache hit, žádný DB dotaz
|
||||
Dotaz 4: SELECT * FROM T3 → UnsupportedQueryError (wildcard není podporován)
|
||||
Dotaz 5: SELECT A FROM T3 JOIN T4 ... → UnsupportedQueryError (JOIN není podporován)
|
||||
```
|
||||
|
||||
**Metadata tabulka `_sqlmem_columns`** (uložena v SQLite):
|
||||
|
||||
```sql
|
||||
CREATE TABLE _sqlmem_columns (
|
||||
table_name TEXT NOT NULL,
|
||||
column_name TEXT NOT NULL,
|
||||
PRIMARY KEY (table_name, column_name)
|
||||
);
|
||||
```
|
||||
|
||||
- Při startu se načte z `cache.db` — Registry ví, co bylo kešováno v minulé session.
|
||||
- Při každém rozšíření sady sloupců se záznam aktualizuje.
|
||||
|
||||
---
|
||||
|
||||
### 3. Cache Manager
|
||||
- Drží in-memory SQLite instanci.
|
||||
- Sleduje, které tabulky jsou již načteny.
|
||||
- Podporuje TTL (Time-to-Live) pro automatické vypršení cache.
|
||||
- Umožňuje manuální invalidaci konkrétní tabulky.
|
||||
- **Persistence**:
|
||||
- Při inicializaci načte `cache.db` ze disku (pokud existuje) do paměti.
|
||||
- Spustí background vlákno, které každou hodinu provede `sqlite3` backup API (`conn.backup(disk_conn)`).
|
||||
- Zaregistruje `atexit` handler a `SIGTERM` handler pro finální zápis při vypnutí.
|
||||
- **Metadata tabulky** (`_sqlmem_meta`) uložená přímo v SQLite cache:
|
||||
|
||||
```sql
|
||||
CREATE TABLE _sqlmem_meta (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL
|
||||
);
|
||||
-- Záznamy:
|
||||
-- app_version verze sqlmem balíčku (např. "0.1.0")
|
||||
-- schema_version verze schématu cache.db (integer, pro migrace)
|
||||
-- created_at ISO timestamp prvního vytvoření cache.db
|
||||
|
||||
CREATE TABLE _sqlmem_tables (
|
||||
table_name TEXT PRIMARY KEY,
|
||||
last_refresh_at TEXT NOT NULL, -- ISO 8601 UTC timestamp
|
||||
row_count INTEGER
|
||||
);
|
||||
```
|
||||
|
||||
- `last_refresh_at` se aktualizuje pokaždé, když se tabulka znovu načte z databáze.
|
||||
- Při načtení `cache.db` ze disku se zkontroluje `schema_version` — pokud nesedí, cache se zahodí a načte znovu z DB.
|
||||
|
||||
### 3. Query Executor
|
||||
- **Cache hit**: Spustí dotaz přímo v in-memory SQLite, vrátí výsledek.
|
||||
- **Cache miss**: Načte potřebné tabulky z databáze → uloží do cache → spustí dotaz.
|
||||
- **Zápisy (INSERT / UPDATE / DELETE)**: Vyhodí výjimku `ReadOnlyError` a dotaz zablokuje. Modul je striktně read-only.
|
||||
|
||||
---
|
||||
|
||||
## Rozsah MVP
|
||||
|
||||
- [ ] `CachingEngine` wrapper kompatibilní se SQLAlchemy
|
||||
- [ ] Načtení `cache.db` při startu do in-memory SQLite
|
||||
- [ ] Periodický zápis na disk každou hodinu (background vlákno)
|
||||
- [ ] Zápis na disk při vypnutí (`atexit` + `SIGTERM`)
|
||||
- [ ] Parser pro detekci tabulek, sloupců a typu dotazu
|
||||
- [ ] Column Registry s akumulační logikou (union sloupců, wildcard detekce)
|
||||
- [ ] Re-fetch cache při rozšíření sady sloupců
|
||||
- [ ] Cache Manager s SQLite in-memory backendem
|
||||
- [ ] Cache hit/miss logika v Query Executoru
|
||||
- [ ] TTL podpora na úrovni tabulky
|
||||
- [ ] Manuální invalidace cache (`engine.cache.invalidate("tabulka")`)
|
||||
- [ ] Testy pokrývající cache hit, cache miss a blokování zápisů (`ReadOnlyError`)
|
||||
|
||||
---
|
||||
|
||||
## Co modul NEŘEŠÍ (mimo scope)
|
||||
|
||||
- INSERT/UPDATE/DELETE — tyto operace jsou **zakázány** a vyhodí `ReadOnlyError`
|
||||
- Redis nebo jiný distribuovaný backend
|
||||
- Transakční konzistence
|
||||
|
||||
---
|
||||
|
||||
## Logování
|
||||
|
||||
Řízeno přes `loguru`. Úroveň se nastavuje v `.env`:
|
||||
|
||||
```env
|
||||
SQLMEM_DEBUG=true # DEBUG level — podrobný výpis každého dotazu, cache operace, backup
|
||||
# false (výchozí) — INFO a výše
|
||||
```
|
||||
|
||||
| Level | Kdy |
|
||||
|---|---|
|
||||
| `DEBUG` | Každý příchozí dotaz, extrahované tabulky/sloupce, cache hit/miss/re-fetch, backup start/konec |
|
||||
| `INFO` | Start/stop modulu, načtení cache.db, periodický backup, refresh tabulky |
|
||||
| `WARNING` | Re-fetch tabulky kvůli novým sloupcům, blížící se TTL expirace |
|
||||
| `ERROR` | `ReadOnlyError`, `UnsupportedQueryError`, selhání připojení k DB, selhání zápisu cache.db |
|
||||
|
||||
---
|
||||
|
||||
## TODO — budoucí funkce
|
||||
|
||||
- **Podpora `SELECT *` (wildcard)**: Načte celou tabulku do cache, označí ji jako `full` — další dotazy na libovolný sloupec jsou vždy cache hit bez re-fetch.
|
||||
- **Podpora JOIN**: Parser extrahuje sloupce z každé joinované tabulky zvlášť, Column Registry je sleduje nezávisle. Cache Manager zajistí, že všechny potřebné tabulky jsou v paměti před spuštěním dotazu.
|
||||
|
||||
---
|
||||
|
||||
## Technologie
|
||||
|
||||
| Vrstva | Knihovna |
|
||||
|---|---|
|
||||
| SQL parsing | `sqlglot` |
|
||||
| Cache úložiště | `sqlite3` (stdlib) |
|
||||
| Integrace | SQLAlchemy events / engine wrapper |
|
||||
| Logování | `loguru`, `python-dotenv` |
|
||||
| Testování | `pytest` |
|
||||
Reference in New Issue
Block a user