test(parcels): integration EXPLAIN-gate для analyze hot-path SQL (#1198) #1200

Merged
bot-backend merged 1 commit from fix/analyze-integration-test into main 2026-06-13 04:53:04 +00:00
Collaborator

Summary

Closes #1198. Integration EXPLAIN-gate для SQL-уровневых регрессий в analyze hot-path. Расширение существующего tests/integration/ phantom-column gate.

Зачем (incident #1195#1197)

PR #1195 (Phase B #1130) ввёл CTE alias overlaps (PG keyword для time-period operator (s1,e1) OVERLAPS (s2,e2)). Парсер видел overlaps AS ( и ждал OVERLAPS operator → syntax error at or near "overlaps". Каждый POST /analyze падал ~10 часов до hotfix PR #1197 (фронт получал пустой neighbors_summary).

Mock-БД не поймала. tests/api/v1/test_*.py патчат db через MagicMockdb.execute() не парсит SQL, никакого SQL-уровня не видно. Это второй такой случай — первый — recurring psycopg v3 :x::type antipattern (см. .claude/rules/backend.md, vault Bug_SQLAlchemy_DoubleColon_Cast).

Что в PR

1. Refactor parcels.py

  • _NEIGHBORS_SUMMARY_SQL вынесен из inline text("...") внутри функции в module-level constant (паттерн уже устоявшийся: best_layouts._PARCEL_CENTROID_SQL, ird_overlay_lookup._IRD_OVERLAP_SQL, krt_lookup._PLANNING_KRT_SQL). Делает его доступным для импорта в тестах.
  • Заодно — конвертирован на canonical CAST(:wkt AS text) / CAST(:our_cad AS text) per .claude/rules/backend.md. Семантика идентична.

2. Integration test (tests/integration/test_analyze_parcels_sql.py, 169 строк)

EXPLAIN-тесты (runtime, маркер integration, skip без TEST_DATABASE_URL):

  • test_explain_neighbors_summary — EXPLAIN над _NEIGHBORS_SUMMARY_SQL против реальной prod-PG schema. Был бы поймал #1195.
  • test_explain_ird_overlap — то же для _IRD_OVERLAP_SQL.

Compile-time guards (всегда работают, без БД):

  • test_neighbors_cte_aliases_not_pg_keywords — CTE-alias не должен быть PG-keyword (overlaps/user/current_date/select/where).
  • test_no_bind_double_colon_cast (parametrized над обоими SQL) — запрет :bind::type antipattern.

Запуск EXPLAIN-тестов

# Без TEST_DATABASE_URL — EXPLAIN тесты skip, compile-time guards run:
uv run pytest tests/integration/test_analyze_parcels_sql.py -v
# → 3 passed, 2 skipped

# С SSH-туннелем (`ssh -N gendesign` → localhost:15432):
export TEST_DATABASE_URL="postgresql+psycopg://USER:PASS@localhost:15432/DB"
uv run pytest tests/integration/test_analyze_parcels_sql.py -v -m integration
# → 5 passed

Что НЕ в scope этого PR

  • CI job для EXPLAIN-тестов с docker-side postgis (или decrypted SSH-туннель) — devops-engineer material, отдельный PR.
  • Покрытие остальных ~10 SQL в parcels.py (competitors, polygon_suitability и пр.) — расширение по тому же паттерну, отдельный follow-up.

Test plan

  • pytest tests/integration/test_analyze_parcels_sql.py → 3 passed, 2 skipped.
  • pytest tests/api/v1/ -k analyze → 26 passed (refactor backward-compatible).
  • ruff check + ruff format → clean.

Refs #1198

## Summary Closes #1198. Integration EXPLAIN-gate для SQL-уровневых регрессий в analyze hot-path. Расширение существующего `tests/integration/` phantom-column gate. ## Зачем (incident #1195 → #1197) PR #1195 (Phase B #1130) ввёл CTE alias `overlaps` (PG keyword для time-period operator `(s1,e1) OVERLAPS (s2,e2)`). Парсер видел `overlaps AS (` и ждал OVERLAPS operator → `syntax error at or near "overlaps"`. **Каждый POST /analyze падал ~10 часов** до hotfix PR #1197 (фронт получал пустой `neighbors_summary`). **Mock-БД не поймала.** `tests/api/v1/test_*.py` патчат `db` через `MagicMock` → `db.execute()` не парсит SQL, никакого SQL-уровня не видно. Это **второй такой случай** — первый — recurring psycopg v3 `:x::type` antipattern (см. `.claude/rules/backend.md`, vault `Bug_SQLAlchemy_DoubleColon_Cast`). ## Что в PR ### 1. Refactor `parcels.py` - `_NEIGHBORS_SUMMARY_SQL` вынесен из inline `text("...")` внутри функции в **module-level constant** (паттерн уже устоявшийся: `best_layouts._PARCEL_CENTROID_SQL`, `ird_overlay_lookup._IRD_OVERLAP_SQL`, `krt_lookup._PLANNING_KRT_SQL`). Делает его доступным для импорта в тестах. - Заодно — конвертирован на canonical `CAST(:wkt AS text)` / `CAST(:our_cad AS text)` per `.claude/rules/backend.md`. Семантика идентична. ### 2. Integration test (`tests/integration/test_analyze_parcels_sql.py`, 169 строк) **EXPLAIN-тесты** (runtime, маркер `integration`, skip без `TEST_DATABASE_URL`): - `test_explain_neighbors_summary` — EXPLAIN над `_NEIGHBORS_SUMMARY_SQL` против реальной prod-PG schema. **Был бы поймал #1195.** - `test_explain_ird_overlap` — то же для `_IRD_OVERLAP_SQL`. **Compile-time guards** (всегда работают, без БД): - `test_neighbors_cte_aliases_not_pg_keywords` — CTE-alias не должен быть PG-keyword (`overlaps`/`user`/`current_date`/`select`/`where`). - `test_no_bind_double_colon_cast` (parametrized над обоими SQL) — запрет `:bind::type` antipattern. ### Запуск EXPLAIN-тестов ```bash # Без TEST_DATABASE_URL — EXPLAIN тесты skip, compile-time guards run: uv run pytest tests/integration/test_analyze_parcels_sql.py -v # → 3 passed, 2 skipped # С SSH-туннелем (`ssh -N gendesign` → localhost:15432): export TEST_DATABASE_URL="postgresql+psycopg://USER:PASS@localhost:15432/DB" uv run pytest tests/integration/test_analyze_parcels_sql.py -v -m integration # → 5 passed ``` ## Что НЕ в scope этого PR - **CI job для EXPLAIN-тестов с docker-side postgis** (или decrypted SSH-туннель) — devops-engineer material, отдельный PR. - **Покрытие остальных ~10 SQL в parcels.py** (competitors, polygon_suitability и пр.) — расширение по тому же паттерну, отдельный follow-up. ## Test plan - [x] `pytest tests/integration/test_analyze_parcels_sql.py` → 3 passed, 2 skipped. - [x] `pytest tests/api/v1/ -k analyze` → 26 passed (refactor backward-compatible). - [x] `ruff check` + `ruff format` → clean. Refs #1198
bot-backend added 1 commit 2026-06-13 04:44:29 +00:00
test(parcels): integration EXPLAIN-gate для analyze hot-path SQL (#1198)
All checks were successful
CI / changes (push) Successful in 7s
CI / frontend-tests (push) Has been skipped
CI / changes (pull_request) Successful in 6s
CI / frontend-tests (pull_request) Has been skipped
CI / backend-tests (push) Successful in 6m24s
Deploy / changes (push) Successful in 6s
Deploy / build-backend (push) Successful in 1m46s
Deploy / build-worker (push) Successful in 2m55s
CI / backend-tests (pull_request) Successful in 6m20s
Deploy / build-frontend (push) Has been skipped
Deploy / deploy (push) Successful in 1m10s
c8000d1089
PR #1195 ввёл CTE alias `overlaps` (PG keyword для time-period operator)
→ каждый POST /analyze падал с syntax error ~10ч до hotfix PR #1197. Mock-БД
в tests/api/v1/test_*.py не поймала: MagicMock на db.execute() не парсит SQL.

Расширяет существующий phantom-column gate (tests/integration/) на
_neighbors_summary и parcel_ird_overlaps SQL:

1. _NEIGHBORS_SUMMARY_SQL вынесен из inline-text в module-level constant
   в parcels.py (паттерн как в best_layouts/ird_overlay_lookup/krt_lookup).
   Заодно сконвертирован на canonical CAST(:wkt AS text) — backend.md rule.
2. EXPLAIN-тесты через phantom_check_session (SSH-туннель к prod-PG) —
   skip без TEST_DATABASE_URL, run в спец CI job.
3. Compile-time guards (всегда работают, без БД):
   - CTE-alias не должен быть PG-keyword (overlaps/user/current_date/select/where)
   - SQL не должен содержать :bind::type (psycopg v3 antipattern)

Поймал бы:
- #1195 (CTE alias overlaps) — оба EXPLAIN-тест + compile-time keyword guard
- :type recurring class — compile-time parametrized guard
- Phantom column / typo PostGIS function — EXPLAIN parses + plans

Refactor backward-compatible: _neighbors_summary использует ту же константу,
behavior идентичен. 26 unit-тестов parcels analyze продолжают зелёные.

Refs #1198
bot-backend merged commit c8000d1089 into main 2026-06-13 04:53:04 +00:00
bot-backend deleted branch fix/analyze-integration-test 2026-06-13 04:53:04 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#1200
No description provided.