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

3.6 KiB
Raw Blame History

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

-- Контекст: что делает файл, зачем, порядок применения, 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 без комментария почему