ADR-0004: SQL layer (WitSQL) for pywitdb — путь к CASA
Статус: принято
Дата: 2026-06-06
Репозиторий: AI-Guiders/pywitdb
Движок: WitDatabase → OutWit.Database (WitSqlEngine)
Связано: ADR-0001, ADR-0002, ADR-0003
Потребитель (follow-up): casa-ontology-payload — agent_journal.db, agent_field.db
Решение (одной фразой)
SQL в pywitdb через WitSqlEngine (пакет OutWit.Database): сначала bridge (op: sql_exec / NDJSON rows), затем native C ABI v2; Python — тонкий фасад (connect / execute / fetchall), совместимый с паттернами sqlite3 в CASA. Миграция CASA — после стабильного SQL-контракта, не до.
Контекст
| Факт | Следствие |
|---|---|
| pywitdb v1 (ADR-0003) — только KV + txn | agent_journal.py (SQL append + GROUP BY + prune) на KV без боли не переезжает |
CASA field (agent_field.db) — bulk routes + chunk |
Теоретически KV, но цель — единый .witdb, не два движка |
| WitSQL ≠ SQLite | Нужна матрица совместимости, не слепой перенос DDL |
| Native win-x64 работает (ADR-0003) | SQL-native тяжелее (Engine + AOT trim), bridge — быстрый proof |
Primary driver миграции CASA: единый стек .witdb, шифрование, cross-lang с .NET — не обещание «быстрее sqlite» на journal workload. Performance — validate (бенч после контракта), not assume.
Архитектура слоёв
casa/domain/agent_journal.py (sqlite3 → pywitdb.sql)
↓
pywitdb.sql.Connection / Cursor ← Python фасад
↓
┌───────────────────┬───────────────────────┐
│ bridge │ native (ABI v2) │
│ op: sql_exec │ witdb_sql_exec(...) │
│ WitSqlEngine │ WitSqlEngine in AOT │
└───────────────────┴───────────────────────┘
↓
WitDatabase (Core) + WitSqlEngine (OutWit.Database)
Зависимость: OutWit.Database.Core (store) + OutWit.Database (SQL engine). Native project сегодня — только Core; SQL добавляет ссылку на Engine и расширяет trim roots.
Python API (v1 SQL)
Модуль: pywitdb.sql (или pywitdb._sql + re-export).
import pywitdb.sql as wsql
with wsql.connect("agent_journal.witdb", password=None, backend="auto") as conn:
conn.execute(_CREATE_SQL)
conn.execute(
"INSERT INTO skill_trace (at, tick, goal_id, skill, plan_step, outcome, payload) "
"VALUES (?, ?, ?, ?, ?, ?, ?)",
(at, tick, goal_id, skill, plan_step, outcome, payload_json),
)
rows = conn.execute(
"SELECT payload FROM skill_trace WHERE goal_id LIKE ? ORDER BY id DESC LIMIT ?",
(f"%{goal_id}%", limit),
).fetchall()
last_id = conn.last_insert_rowid()
| Метод | Семантика |
|---|---|
connect(path, password=..., backend="auto") |
open store + attach WitSqlEngine |
Connection.execute(sql, params=()) |
DDL/DML; returns Cursor |
Cursor.fetchall() / fetchone() |
rows as tuple or Row (mapping по имени колонки — v1.1) |
Connection.commit() / rollback() |
txn boundary (map на WitSqlEngine / Core txn) |
Connection.last_insert_rowid() |
LastInsertRowId после INSERT |
| context manager | close + dispose engine |
Параметры: позиционные ? в Python → @p0, @p1 или named @name в WitSqlEngine (зафиксировать в реализации; предпочтение ? для sqlite-порта CASA).
Non-goals SQL v1: DB-API 2.0 полнота, async cursor, SQLAlchemy dialect, multi-connection pool.
Bridge protocol (расширение ADR-0001)
После open на том же subprocess:
{"id": 10, "op": "sql_exec", "sql": "CREATE TABLE IF NOT EXISTS skill_trace (...)"}
{"id": 10, "ok": true, "rows_affected": 0}
{"id": 11, "op": "sql_query", "sql": "SELECT COUNT(*) AS c FROM skill_trace", "params": []}
{"id": 11, "ok": true, "columns": ["c"], "rows": [[42]]}
{"id": 12, "op": "sql_exec", "sql": "INSERT INTO ...", "params": ["2026-...", 1, "g1", ...]}
{"id": 12, "ok": true, "rows_affected": 1, "last_insert_rowid": 7}
op |
Назначение |
|---|---|
sql_exec |
DDL, DML без result set |
sql_query |
SELECT → columns + rows (JSON-safe types) |
sql_batch |
optional v1.1 — несколько stmt в одной txn |
Ошибки SQL: error.code = sql_error, message от движка.
Bridge csproj: + ProjectReference на OutWit.Database (Engine), не только Core.
Native C ABI (v2 — расширение ADR-0003)
Вариант A (принят): тот же WITDB_ABI_VERSION=1, новые exports (minor-compatible):
WitDbStatus witdb_sql_exec(
uintptr_t db,
const char* sql,
const char* params_json, // NULL or JSON array of values
int64_t* out_last_rowid,
int32_t* out_rows_affected);
WitDbStatus witdb_sql_query(
uintptr_t db,
const char* sql,
const char* params_json,
char** out_result_json, // {"columns":[...],"rows":[...]}
uint32_t* out_result_len);
out_result_json — caller frees via witdb_buffer_free. Вызовы через тот же CLR worker, что ADR-0003 (WitDbClrThread).
Вариант B (отклонён): bump WITDB_ABI_VERSION=2 только из-за SQL — избыточно, если KV exports не меняются.
Native csproj: + Engine reference; trimming.xml расширить под WitSqlEngine / parser.
CASA journal — матрица совместимости (acceptance)
Источник: casa/domain/agent_journal.py (_CREATE_SQL + запросы).
| Возможность SQLite в CASA | WitSQL | Действие |
|---|---|---|
CREATE TABLE IF NOT EXISTS |
✅ | как есть |
CREATE INDEX IF NOT EXISTS |
✅ | как есть |
INTEGER PRIMARY KEY AUTOINCREMENT |
⚠️ | явный AUTOINCREMENT поддерживается; в тестах движка чаще BIGINT (implicit autoincrement на Int64); gate — contract test |
TEXT, INTEGER |
✅ | TEXT, INT / INTEGER |
INSERT + lastrowid |
✅ | LastInsertRowId |
ORDER BY id DESC LIMIT ? |
✅ | параметризованный LIMIT |
GROUP BY + COUNT(*) |
✅ | проверить в contract test |
LIKE ? |
✅ | contract test |
DELETE ... WHERE id NOT IN (SELECT id ... ORDER BY ... LIMIT ?) |
✅ | SQLite-compat fix: nested subqueries use queryExpression (parser + engine test WitSqlEngineSqlitePruneTests) |
sqlite3.Row / dict by name |
⚠️ | Python Row mapping в фасаде |
Contract test (gate перед CASA ADR):
tests/contract/test_casa_journal_sql.py — поднять schema journal на witdb (bridge), прогнать representative ops из agent_journal.py (insert, prune, aggregate, query_skill_trace).
CASA field (фаза 2, после journal SQL)
agent_field.db можно:
- A) оставить на KV pywitdb (уже близко к модели keys), или
- B) перевести на SQL tables (
meta,routes,chunks) в том же.witdbфайле.
Решение — в отдельном ADR casa-ontology-payload после green journal contract.
Порядок реализации
| # | Задача | Репо |
|---|---|---|
| 1 | Bridge: sql_exec / sql_query + Engine ref |
pywitdb bridge/WitDbBridge |
| 2 | pywitdb.sql фасад на bridge |
pywitdb |
| 3 | Contract: CASA journal schema + ops | pywitdb tests/contract/ |
| 4 | Native: witdb_sql_* + trim roots |
WitDatabase OutWit.Database.Native |
| 5 | pywitdb.sql backend native |
pywitdb |
| 6 | Бенч journal vs sqlite (optional report) | pywitdb или casa |
| 7 | ADR CASA + миграция agent_journal.db → .witdb |
casa-ontology-payload |
Последствия
Положительные
- CASA journal переносится без переписывания на KV.
- Один
.witdb+ шифрование + тот же reopen (ADR-0002). - Bridge даёт SQL до готовности native AOT matrix.
Отрицательные / риски
- Engine в NativeAOT — больший артефакт и trim surface.
- Диалект: не все sqlite-конструкты; prune-subquery — точка риска.
- Два SQL path (bridge/native) — parity tests обязательны.
Non-goals
- SQLAlchemy dialect (ADR позже).
asyncioSQL (обёрткаto_thread— вне scope ADR-0004).- Замена sqlite в других проектах репо (IncomeCascade и т.д.).
Ссылки
- WitSQL engine:
witdatabase/Sources/Engine/OutWit.Database/ - CASA journal:
casa-ontology-payload/casa/domain/agent_journal.py - CASA field:
casa-ontology-payload/casa/domain/agent_field.py - Native worker: ADR-0003
WitDbClrThread