gendesign/data/sql/184_gas_grs_outlet_points.sql
bot-backend 07f9f7c943
All checks were successful
CI / changes (pull_request) Successful in 9s
CI Trade-In / changes (pull_request) Successful in 10s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 1m51s
CI / backend-tests (pull_request) Successful in 14m12s
feat(db): gas_grs_outlet_points — точки выхода ГРС форма 6 (#2119 B2 PR-1/4)
Миграция 184: адресные точки выхода газораспределительной сети (форма 6
приказа ФАС №960, раскрытие АО «Екатеринбурггаз», ~1037 строк/мес).

- grs_name_norm нормализуется как в gas_grs_capacity (181) → JOIN «точка
  выхода → свободная мощность ГРС» без FK (источники разные: ГТЕ vs ЕКгаз).
- Размерность млн. м³/МЕСЯЦ (НЕ тыс. м³/ч как в 181) — отражено в COMMENT.
- «0.000*» = требуется гидрорасчёт: free_capacity_mln_m3 NULL +
  free_capacity_needs_calc TRUE (не путать с настоящим 0).
- is_forecast разводит факт («за <месяц>») и прогноз («на <месяц>») одного
  period_month; логический UNIQUE(grs_name_norm, outlet_name, period_month,
  is_forecast) — все 4 колонки NOT NULL, обычный UNIQUE (без NULLS NOT DISTINCT).
- Индексы: GIST(geom), btree(grs_name_norm), btree(period_month DESC).
- geom NULL до PR-3 (геокод DaData) и навсегда у ~81% строк без адреса.

Идемпотентно (IF NOT EXISTS), BEGIN/COMMIT. Провалидировано dry-run в
ephemeral postgis/postgis:16-3.4 (prod-образ): двойной прогон,
fact+forecast coexist, ON CONFLICT upsert, отклонение дубля.

PR-план #2119 B2: PR-1 schema (этот) · PR-2 лоадер · PR-3 геокод · PR-4 §3.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 06:59:48 +05:00

121 lines
9.8 KiB
PL/PgSQL
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.

-- 184_gas_grs_outlet_points.sql
-- Forgejo #2119 Phase B2 — адресные точки выхода газораспределительной сети
-- (форма 6 приказа ФАС №960) для ПТИЦА §3. PR-1 из 4 (Schema).
--
-- Одна таблица gas_grs_outlet_points: построчная выгрузка «точек выхода» ГРС из
-- раскрытия АО «Екатеринбурггаз» (ГРО, газораспределительная организация):
-- https://ekgas.ru/ — apex-домен, форма 6 (техвозможность подключения по
-- точкам выхода) приказа ФАС России №960 «О порядке раскрытия информации…».
-- ~1037 строк/месяц, помесячные срезы.
--
-- Источник ГЕО-БЛОКИРУЕТ non-RU IP → загрузчик РАБОТАЕТ НА ПРОДЕ (Celery / manual
-- docker exec) — как gas_grs_capacity (181, ГТЕ) и rosseti-/vodokanal-лоадеры (180).
-- Код не должен предполагать локальный сетевой доступ.
--
-- Связь с B1: grs_name_norm нормализуется ТЕМ ЖЕ правилом, что и в
-- gas_grs_capacity (181): lower, ё→е, пробелы/дефисы схлопнуты, префиксы «ГРС»/«АРП»
-- СОХРАНЯЮТСЯ. Это даёт JOIN «точка выхода → свободная мощность ГРС» без FK
-- (источники разные — ГТЕ vs Екатеринбурггаз, — жёсткий FK был бы ложным).
--
-- ВНИМАНИЕ, РАЗМЕРНОСТЬ: объёмы формы 6 — млн. м³/МЕСЯЦ (не тыс. м³/ЧАС, как
-- free_capacity_th_m3_h в gas_grs_capacity!). Не смешивать при агрегации в §3.
--
-- «0.000*» в столбце свободной мощности источника = НЕ ноль, а «требует
-- гидравлического расчёта» → free_capacity_mln_m3 = NULL, флаг
-- free_capacity_needs_calc = TRUE (см. семантику ниже).
--
-- address_extracted покрывает лишь ~19% строк (у ~81% «точки выхода» адреса в
-- тексте нет) → geom останется NULL у большинства даже после PR-3 (геокод DaData).
--
-- PR-план #2119 B2:
-- PR-1 (этот) — schema.
-- PR-2 — лоадер (forward-fill grs_name по block-marker «Точка входа», парс
-- «за/на <месяц> <год>» → period_month + is_forecast, разбор «0.000*»,
-- извлечение address_extracted, UPSERT ON CONFLICT).
-- PR-3 — геокод address_extracted через DaData → geom (ключи DaData на проде).
-- PR-4 — интеграция в отчёт §3 (свободная мощность точек выхода рядом с ГРС B1).
--
-- Порядок применения: миграция ПЕРЕД деплоем кода (schema-first, sql.md).
-- Таблица НОВАЯ → deploy.yml гоняет всё в одной транзакции; CREATE INDEX
-- CONCURRENTLY здесь не нужен (и запрещён внутри транзакции).
BEGIN;
-- ── gas_grs_outlet_points ─────────────────────────────────────────────────────
-- geom NULL до PR-3 (геокод) и у ~81% строк без адреса — навсегда. raw jsonb —
-- сырые ячейки строки формы 6 целиком (неизвестные/будущие колонки не теряются).
CREATE TABLE IF NOT EXISTS gas_grs_outlet_points (
id BIGSERIAL PRIMARY KEY,
grs_name TEXT NOT NULL, -- «Точка входа» (block-marker, forward-fill в лоадере)
grs_name_norm TEXT NOT NULL, -- нормализация как в gas_grs_capacity → матч на B1-ГРС
outlet_name TEXT NOT NULL, -- «Точка выхода» — полный свободный текст (юрлицо/объект)
consumer_type TEXT, -- котельная / ТЭЦ / площадка / промплощадка
consumption_group TEXT, -- «5 - 0,1-1млн.куб.м» и т.п.
volume_requested_mln_m3 NUMERIC, -- заявки, млн. м³/мес
volume_approved_mln_m3 NUMERIC, -- удовлетворено, млн. м³/мес
free_capacity_mln_m3 NUMERIC, -- свободная мощность, млн. м³/мес; NULL если «0.000*»
free_capacity_needs_calc BOOLEAN NOT NULL DEFAULT FALSE, -- маркер «0.000*» (нужен гидрорасчёт)
address_extracted TEXT, -- адресный хвост для DaData; NULL если адреса нет (~81%)
geom GEOMETRY(POINT, 4326), -- NULL до PR-3 геокода (и навсегда у строк без адреса)
period_month DATE NOT NULL, -- 2026-05-01 из «за май 2026» (первое число месяца)
is_forecast BOOLEAN NOT NULL DEFAULT FALSE, -- «на <месяц>»=TRUE (прогноз), «за»=FALSE (факт)
asof DATE, -- дата актуальности данных
source_url TEXT, -- URL страницы/файла раскрытия
raw JSONB, -- сырая строка формы 6 целиком
fetched_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
-- Логический dedup-ключ. Все 4 колонки NOT NULL → обычный UNIQUE достаточен
-- (NULLS NOT DISTINCT не нужен: NULL в ключе невозможен по CHECK NOT NULL).
-- Тот же (grs_name_norm, outlet_name) может встречаться в двух строках одного
-- period_month — факт («за») и прогноз («на»); is_forecast разводит их.
UNIQUE (grs_name_norm, outlet_name, period_month, is_forecast)
);
CREATE INDEX IF NOT EXISTS idx_gas_grs_outlet_points_name_norm
ON gas_grs_outlet_points (grs_name_norm);
CREATE INDEX IF NOT EXISTS idx_gas_grs_outlet_points_period
ON gas_grs_outlet_points (period_month DESC);
CREATE INDEX IF NOT EXISTS gist_gas_grs_outlet_points_geom
ON gas_grs_outlet_points USING GIST (geom);
COMMENT ON TABLE gas_grs_outlet_points IS
'Адресные точки выхода газораспределительной сети (форма 6 приказа ФАС №960), '
'раскрытие АО «Екатеринбурггаз» (ekgas.ru), ~1037 строк/мес, помесячные срезы. '
'ВНИМАНИЕ: объёмы в млн. м³/МЕСЯЦ (не тыс. м³/ч как в gas_grs_capacity!). '
'grs_name_norm матчит gas_grs_capacity (181, ГТЕ) без FK. geom NULL до PR-3 '
'геокода и навсегда у ~81% строк без адреса. Refresh: monthly. Forgejo #2119 B2.';
COMMENT ON COLUMN gas_grs_outlet_points.grs_name_norm IS
'Нормализованное имя ГРС/точки входа для матча на gas_grs_capacity (181): lower, '
'ё→е, пробелы/дефисы схлопнуты, префиксы «ГРС»/«АРП» СОХРАНЯЮТСЯ. Заполняется '
'forward-fill по block-marker «Точка входа» в лоадере (PR-2).';
COMMENT ON COLUMN gas_grs_outlet_points.outlet_name IS
'«Точка выхода» — полный свободный текст (юрлицо / объект / промплощадка). '
'Часть логического UNIQUE-ключа; адресный хвост из него → address_extracted.';
COMMENT ON COLUMN gas_grs_outlet_points.free_capacity_mln_m3 IS
'Свободная (резервная) мощность точки выхода, млн. м³/МЕСЯЦ. NULL, когда в '
'источнике «0.000*» — это НЕ ноль, а «требуется гидравлический расчёт» '
'(free_capacity_needs_calc=TRUE). Реальный 0 (без звёздочки) хранится как 0.';
COMMENT ON COLUMN gas_grs_outlet_points.free_capacity_needs_calc IS
'TRUE = в источнике «0.000*»: свободная мощность не определена, нужен '
'гидравлический расчёт (free_capacity_mln_m3 при этом NULL). FALSE = значение '
'известно (в т.ч. настоящий 0).';
COMMENT ON COLUMN gas_grs_outlet_points.address_extracted IS
'Адресный хвост, вытащенный из outlet_name для геокода DaData (PR-3). NULL, '
'когда адреса в тексте нет (~81% строк) → geom у них останется NULL.';
COMMENT ON COLUMN gas_grs_outlet_points.period_month IS
'Первое число месяца отчётного среза (2026-05-01 из «за/на май 2026»). Часть '
'логического UNIQUE-ключа: помесячные срезы не перезатирают друг друга.';
COMMENT ON COLUMN gas_grs_outlet_points.is_forecast IS
'TRUE = прогноз («на <месяц> <год>»), FALSE = факт («за <месяц> <год>»). '
'Разводит факт и прогноз одного period_month в UNIQUE-ключе.';
COMMIT;