gendesign/.claude/rules/sql.md
bot-backend b73161a133
All checks were successful
CI / frontend-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 6s
CI / backend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
chore(claude-config): secret-read + psycopg2 hooks, tradein.md rule, tradein-mvp globs
- scripts/claude-hooks/check-secret-read.py — PreToolUse(Bash) guard: блокирует чтение/передачу
  .env*/.pgpass/ssh-key (cat/Get-Content/curl @file/<redirect), закрывает Bash(*) обход Read(.env)
  deny. Allow: .env.example/templates, ls/stat. Tested 12/12.
- scripts/claude-hooks/check-sql-pitfalls.py — PostToolUse(Edit|Write|MultiEdit): блокирует
  import psycopg2 в backend + tradein-mvp/backend .py (psycopg v3). Cast-trap НЕ чекаем (FP на
  comment/test-mentions). Tested 7/7.
- scripts/claude-hooks/session-preflight.py — SessionStart: статус FORGEJO/OBSIDIAN token + db-tunnel.
- .claude/rules/tradein.md — НОВОЕ: tradein-mvp gotchas (2 БД, :8000=gendesign, scheduler в
  tradein-scraper, strict SQL auto-apply + NN-collisions, rapid-merge trap).
- backend.md/sql.md/frontend.md: paths globs расширены на tradein-mvp/**.

Wiring (settings.json) — local/gitignored, отдельно. Audit 2026-06-27 P0+P1.
2026-06-27 13:02:30 +03:00

3.6 KiB
Raw Blame History

paths
data/sql/**/*.sql
tradein-mvp/backend/data/sql/**/*.sql

SQL conventions — PostgreSQL 16 / PostGIS 3.4

File naming

NN_topic.sql где NN — следующий sequential номер. Проверка: ls data/sql/ | sort | tail -5.

Structure

-- Контекст: что делает файл, зачем, порядок применения, dependencies.
BEGIN;

-- DDL здесь (idempotent)

COMMIT;

Idempotency (обязательно)

  • CREATE TABLE IF NOT EXISTS
  • ALTER TABLE ... ADD COLUMN IF NOT EXISTS
  • ALTER TABLE ... DROP COLUMN IF EXISTS
  • CREATE OR REPLACE VIEW
  • CREATE INDEX IF NOT EXISTS
  • ON CONFLICT DO NOTHING для seed data
  • DROP INDEX IF EXISTS перед CREATE INDEX если меняется тип column

Auto-apply на prod

data/sql/NN_*.sql применяются автоматически через deploy.yml (tracking через _schema_migrations). Каждый файл — ровно один раз, по NN order. Idempotency критична.

Если auto-apply падает → deploy exit 1, containers не обновляются. Diagnose: GHA log → "Apply DB migrations". Fix: psql -f file.sql через SSH tunnel + INSERT INTO _schema_migrations (filename) VALUES ('NN_xxx.sql') ON CONFLICT DO NOTHING.

Migration order

  1. SQL migration first (схема) — deploy first
  2. Backend code matching new schema — deploy second
  3. Никогда наоборот — deployed code напорется на несовместимую схему

psycopg v3 CAST trap

В Python через psycopg v3 / SQLAlchemy text(...):

  • :param::type — игнорируется парсером
  • CAST(:param AS type) — canonical

В чистом SQL (.sql файл, без bind params) — обычный ::type работает.

Reference: vault Pattern_CAST_AS_Type.

VIEW dependencies

Перед ALTER COLUMN type column'а:

  1. Найти зависимые view: \d+ table_name или pg_get_viewdef('view_name'::regclass, true)
  2. DROP VIEW <dependent>;
  3. ALTER COLUMN ...;
  4. CREATE OR REPLACE VIEW <dependent> AS SELECT ...;

Reference: 93_cad_parcels_geom_multipolygon.sql (Polygon → MultiPolygon migration).

Агрегация по pre-aggregated строкам (обязательно weighted AVG)

Если источник содержит строки вида «одна строка = один период (месяц) + уже посчитанный avg_value + count» (например objective_corpus_room_month), то наивный AVG(avg_value) неверен: строки с нулевыми сделками занижают результат в 2-10x.

Правильная формула — count-weighted AVG:

SUM(avg_value * cnt) / NULLIF(SUM(cnt), 0)
  • NULLIF(..., 0) обязателен — предотвращает division by zero при all-zero периодах и возвращает NULL вместо фейкового 0.
  • Без весов: AVG() равноправно учитывает «пустые» месяцы → занижение.

Reference: fix #295 (100_fix_mv_layout_velocity_weighted_avg.sql), тест backend/tests/sql/test_mv_layout_velocity_weighted_avg.py.

Запреты

  • DROP TABLE / TRUNCATE без явного approval пользователя
  • Не-idempotent файлы (без IF EXISTS) — на случай ре-apply
  • Миграции без BEGIN...COMMIT обёртки
  • Hardcoded даты / IDs без комментария почему