Skip to content

Commit 55b90ab

Browse files
JuliaEdomclaude
andcommitted
docs: record the Postgres run and what it exposed
Methodology §14: how the data was loaded (official BIRD dump, not the pandas loader, and why), the two-engine table, the two bugs a live Postgres exposed, and a copy-pasteable reproduction. README's Postgres bullet no longer promises more than was measured. BACKLOG moves the PG data-migration item out of "gated" into "done" and keeps the honest remainder (sustained-load host, Space cleanup, external pen-test). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent d998589 commit 55b90ab

3 files changed

Lines changed: 72 additions & 2 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ The UI reads `MISTRAL_API_KEY` from `.env`; copy `.env.example` first.
9494
- **Hard budget: $0 external cost.** Primary: Mistral La Plateforme (`codestral-latest` SQL + `mistral-large-latest` NL + `mistral-embed`). Voting layers cycled через free-tier Groq (llama-3.3-70b / qwen3-32b / gpt-oss-20b — TPM/TPD-bounded) + OpenRouter free models (nemotron-3-super-120b — 50/day account-wide) + Sonnet 4.6 via GraceKelly Perplexity bridge (Chrome-gated). См. `docs/v11_saturation_evidence.md` для actual reach × rescues × why-stopped per провайдер.
9595
- **ChromaDB** — 2 коллекции: `schema_chunks` + `fewshot_qsql`
9696
- **SQLite** (read-only) — target БД по умолчанию: Chinook + 11 BIRD Mini-Dev slices локально (вся eval на SQLite); на live Space — 9 (три самых больших > 100 MB/файл, см. DEPLOY.md).
97-
- **Postgres 16** — второй backend с genuinely read-only транзакциями (execution-option, не роль-плацебо); запускается локально через `docker-compose.yml` (profile `postgres`), данные грузятся `scripts/load_postgres.py` (`codebase_community` = StackExchange-mini). Read-only enforcement проверяется на живом PG16 в CI. На HF Space не тянется (SQLite-only деплой).
97+
- **Postgres 16** — второй backend с genuinely read-only транзакциями (execution-option, не роль-плацебо); запускается локально через `docker-compose.yml` (profile `postgres`). **Прогнан, а не только заявлен:** `codebase_community` (StackExchange-mini, 741 646 строк) залит из официального BIRD PG-дампа (`scripts/extract_pg_dump_slice.py`), и тот же пайплайн на BIRD-овском **Postgres-gold** даёт **49.0% EA (24/49)** против 44.9% на SQLite для тех же вопросов, validity 100% — движок переносим. Живой прогон нашёл два бага, невидимых на SQLite (`%` в `LIKE` ронял psycopg-путь; `Decimal` из `numeric` занижал скоринг) — оба починены, см. `docs/03_eval_methodology.md` §14. Read-only enforcement проверяется на живом PG16 в CI. На HF Space не тянется (SQLite-only деплой).
9898
- **sqlglot** — AST guard, dialect translation
9999
- **FastAPI + Pydantic v2** — API (X-API-Key + безусловный token-bucket rate-limit)
100100
- **Streamlit** — UI

‎docs/03_eval_methodology.md‎

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,3 +409,65 @@ uv run python scripts/eval_baseline.py --config E --n 200 --bird-rescue-hints
409409
94.0% выше human-expert baseline (BIRD paper, 92.96%) на +1.04pp — но это **hint-assisted слой**, кодирующий ответы к конкретным вопросам теста, а не провайдер-уровневая победа. Поэтому он выключен в продукте по умолчанию: живое демо и API отдают уровень 1 (57.5%). `GET /eval/latest` отдаёт именно уровень 1 — число, которого достигает сам API. Разделение на слои показывает и инженерию (воспроизводимые 57.5%), и научную честность (какой слой что даёт).
410410

411411
На Arcwise-corrected gold (Jin et al., CIDR/VLDB 2026) — 74.37% (148/199): noise-floor после исправления annotation-ошибок BIRD.
412+
413+
---
414+
415+
## 14. Postgres: тот же пайплайн на другом движке (2026-07-11)
416+
417+
До этой даты «Postgres 16 как второй backend» был заявлен, но **ни разу не прогнан**: read-only-защиту чинили в CI на пустой пробной таблице, а живых данных и живого прогона не было. Заявление проверено — и проверка нашла два дефекта, которые на SQLite не проявляются в принципе.
418+
419+
### Как загружены данные
420+
421+
BIRD Mini-Dev официально поставляет Postgres-версию: `MINIDEV_postgresql/BIRD_dev.sql` (955 МБ `pg_dump`, все 11 БД **в одну схему `public`**) и `mini_dev_postgresql.json` — gold SQL, переписанный под PG (**312 из 500** запросов отличаются от SQLite-версии: `STRFTIME('%Y', x)` → `TO_CHAR(CAST(x AS TIMESTAMP), 'YYYY')` и т.п.).
422+
423+
Заливать дамп целиком нельзя: 75 посторонних таблиц (Formula-1, карточные игры, больничные лаборатории) попали бы в schema-retrieval и подменили задачу. `scripts/extract_pg_dump_slice.py` вырезает одну БД по именам таблиц (они глобально уникальны между 11 БД — свойство закреплено тестом) и валидирует срез: все `CREATE TABLE`, все `COPY`, ни одного FK наружу.
424+
425+
**Почему не `scripts/load_postgres.py` (SQLite → pandas → PG).** BIRD-овский PG-gold написан под схему их дампа: идентификаторы свёрнуты в нижний регистр (`displayname`, `posthistory`), даты — настоящий `timestamptz`. Pandas-загрузка воспроизводит CamelCase из SQLite, и gold вида `SELECT DisplayName FROM users` (в PG это `displayname`) просто не нашёл бы колонку. Официальный дамп — то, что делает прогон честным BIRD-раном, а не похожим на него. `load_postgres.py` остаётся правильным инструментом для Chinook и произвольных SQLite-срезов, где PG-gold не существует.
426+
427+
`codebase_community` (StackExchange-mini): 8 таблиц, **741 646 строк**, 385 МБ в PG. Сверено с SQLite построчно — совпадает по всем таблицам.
428+
429+
### Результат (config E, codestral, hints off — тот же продуктовый пайплайн)
430+
431+
Одни и те же 49 вопросов `codebase_community`, два движка, свой gold и свой schema-индекс у каждого:
432+
433+
| Движок | EA | simple | moderate | challenging | Validity |
434+
|---|---:|---:|---:|---:|---:|
435+
| SQLite | 44.9% (22/49) | 76.2% | 21.7% | 20.0% | 100% |
436+
| **Postgres 16** | **49.0% (24/49)** | 66.7% | 34.8% | 40.0% | 100% |
437+
438+
Согласие движков — **88%** (18 вопросов оба решают верно, 25 оба неверно; 4 только SQLite, 2 только Postgres). Разница в 2 вопроса на n=49 — **шум, а не превосходство**: доверительный интервал здесь ±14 pp. Читать это следует так: **пайплайн переносим — на Postgres он не деградирует**, генерируя PG-диалект (`TO_CHAR`, `::`, `ILIKE`) под PG-схему. Валидность 100% означает, что каждый сгенерированный запрос исполнился на живом сервере.
439+
440+
Артефакт: `eval/baselines/postgres_codebase_community_n49.json` (под тем же consistency-гейтом, что и остальные baseline'ы).
441+
442+
### Два бага, которые нашёл только живой Postgres
443+
444+
Оба сидели в продуктовом коде и оба невидимы на SQLite:
445+
446+
1. **`%` в SQL убивал запрос.** `execute_readonly` исполнял SQL через `exec_driver_sql`, который передаёт драйверу (пустой) набор параметров. psycopg говорит на `pyformat`, поэтому начинал искать плейсхолдеры и падал на `LIKE '%variance%'`: *only '%s', '%b', '%t' are allowed as placeholders, got '%v'*. Ломался самый частый шаблон text-to-SQL (LIKE-фильтр) и оператор `%` (modulo) — **и BIRD-овский gold тоже**, так что вопрос нельзя было даже оценить (qids 586/587). Починено: исполнение через DBAPI-курсор вообще без параметров — тогда ни `%` (psycopg), ни `:__` (SQLAlchemy `text()`, BIRD qids 959/989/990) не интерпретируются. Драйверные исключения заворачиваются обратно в `sqlalchemy.exc`, чтобы классификация ошибок не поехала.
447+
448+
2. **Скоринг занижал Postgres.** psycopg возвращает `numeric` (любые `AVG`, `SUM`, деление) как `Decimal`, SQLite — как `float`. `_hashable` квантовал к сетке допуска только `float`, а `Decimal` пропускал как есть → верный ответ `Decimal('1.3070996799810359')` не попадал в ту же корзину, что gold `1.307099679981036`, и получал miss. Докстринг обещал «Decimal → float» — строки в коде не было. Из-за этого PG-прогон показывал 40.8% вместо 49.0%: **четыре балла EA съедал баг компаратора, а не модель.**
449+
450+
Заметность обоих багов на SQLite — нулевая: sqlite3 не парсит `%` и не возвращает `Decimal`. Это и есть аргумент в пользу того, чтобы прогонять заявленный backend, а не только объявлять его.
451+
452+
### Воспроизвести
453+
454+
```bash
455+
# 1. срез одной БД из официального PG-дампа BIRD
456+
python scripts/extract_pg_dump_slice.py --db codebase_community --out .tmp/codebase_community_pg.sql
457+
458+
# 2. Postgres 16 (docker-compose profile postgres → localhost:5433) + загрузка
459+
docker compose --profile postgres up -d
460+
psql "postgresql://postgres:postgres@localhost:5433/nl_sql_demo" -v ON_ERROR_STOP=1 -f .tmp/codebase_community_pg.sql
461+
psql "postgresql://postgres:postgres@localhost:5433/nl_sql_demo" -c "GRANT SELECT ON ALL TABLES IN SCHEMA public TO nl_sql_ro"
462+
463+
# 3. schema-индекс по ЖИВОЙ PG-схеме (отдельный persist — иначе перезапишет SQLite-чанки того же db_id)
464+
python scripts/build_index.py --db bird_codebase_community --persist .tmp/chroma_pg \
465+
--pg-dsn "postgresql+psycopg://nl_sql_ro:nl_sql_ro_pwd@localhost:5433/nl_sql_demo"
466+
467+
# 4. eval на PG-gold (пайплайн промптит postgresql, gold берётся из mini_dev_postgresql.json)
468+
python scripts/eval_baseline.py --config E --dialect postgresql --db bird_codebase_community --n 49 \
469+
--persist .tmp/chroma_pg \
470+
--pg-dsn "postgresql+psycopg://nl_sql_ro:nl_sql_ro_pwd@localhost:5433/nl_sql_demo"
471+
```
472+
473+
`--dialect postgresql` без Postgres-бэкенда для выбранных БД падает с ошибкой: PG-gold, исполненный на SQLite-движке, дал бы не громкую ошибку, а тихо заниженный EA.

‎docs/BACKLOG.md‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,17 @@ Docs / hygiene / CI
2828
- ✅ CI: mypy over `src app scripts`; coverage floor (`--cov-fail-under=85`, currently 91%); aligned on Python 3.13.
2929
- ✅ `docs/SESSION_HANDOFF.md` un-tracked (kitchen); eval trace moved into methodology §13.
3030

31+
## Done (2026-07-11, second pass — the Postgres claim, actually run)
32+
33+
- ✅ **PG data load + PG-backed eval.** `codebase_community` (StackExchange-mini, 741 646 rows, 385 MB) loaded into a live Postgres 16 from BIRD's own PG dump via `scripts/extract_pg_dump_slice.py` (the dump ships all eleven databases in one `public` schema; the slice keeps one). Row counts verified against SQLite table by table. The product pipeline then ran on BIRD's **Postgres gold**: **49.0% EA (24/49)** vs 44.9% on SQLite for the same questions, 100% validity, 88% agreement between engines — the engine ports. Artefact: `eval/baselines/postgres_codebase_community_n49.json`; method: `docs/03_eval_methodology.md` §14.
34+
- ✅ **Two product bugs that only a live Postgres could expose** (both invisible on SQLite):
35+
- `execute_readonly` ran statements through `exec_driver_sql`, which hands the driver an empty parameter set — psycopg (`pyformat`) then parsed `%` as a placeholder and killed every `LIKE '%x%'` and every `%` modulo, *including BIRD's gold* (qids 586/587 couldn't even be scored). Now executed on a DBAPI cursor with no parameters, so neither `%` (psycopg) nor `:__` (SQLAlchemy `text()`) is interpreted; driver exceptions are re-wrapped into `sqlalchemy.exc` so error classification is unchanged.
36+
- `_normalise_cell` never converted `Decimal` → `float` (its docstring claimed it did), so Postgres `numeric` results (`AVG`, `SUM`, any division) skipped the float-tolerance grid and correct answers scored misses. Cost 4 EA points on the PG run (40.8% → 49.0%).
37+
- ✅ SQLite headline unaffected by both fixes: re-measured n=200 → still 57.5%.
38+
3139
## Gated (needs a decision or an external host)
3240

33-
- ⛔ **Sustained-load / PG data migration.** The 460 MB `codebase_community` (StackExchange) load + a PG-backed eval need a real Postgres host; not runnable on the Windows dev box. Loader + CI read-only proof are in place; the full data load is the remaining step.
41+
- ⛔ **Sustained-load benchmark.** The data-migration half is done (above); a throughput/latency run under sustained load still wants a host that isn't the Windows dev box.
3442
- ⛔ **Space cleanup + git history.** `SESSION_HANDOFF.md` is un-tracked now, so future deploys won't republish it; removing it from the live Space and any git-history rewrite are force/deploy actions — owner's call.
3543
- ⛔ **External pen-test of the public API.** Self-attestation is not a substitute; needs an outside pass.
3644

0 commit comments

Comments
 (0)