diff --git a/data/sql/184_gas_grs_outlet_points.sql b/data/sql/184_gas_grs_outlet_points.sql new file mode 100644 index 00000000..0a3e9a8e --- /dev/null +++ b/data/sql/184_gas_grs_outlet_points.sql @@ -0,0 +1,121 @@ +-- 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;