gendesign/.claude/rules/sql.md
Light1YT 07106d744a
Some checks failed
Deploy / changes (push) Successful in 5s
Deploy / build-frontend (push) Has been skipped
Deploy / build-worker (push) Successful in 33s
Deploy / build-backend (push) Successful in 37s
Deploy / deploy (push) Has been cancelled
chore(mv): regression test + sql rule for weighted AVG (#295) (#626)
2026-05-28 13:49:54 +00:00

87 lines
3.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
paths: data/sql/**/*.sql
---
# SQL conventions — PostgreSQL 16 / PostGIS 3.4
## File naming
`NN_topic.sql` где NN — следующий sequential номер. Проверка: `ls data/sql/ | sort | tail -5`.
## Structure
```sql
-- Контекст: что делает файл, зачем, порядок применения, 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:
```sql
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 без комментария почему