gendesign/data/sql/99c_power_supply_centers_dedup.sql
bot-backend b35f444d50
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 11s
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 2m22s
CI / backend-tests (pull_request) Successful in 17m46s
fix(rosseti): стабильный external_id ЦП вместо сессионного fid GeoServer
feature['id'] в WFS-ответе — сессионный fid, новый на каждый GetFeature.
ON CONFLICT (source, external_id) не срабатывал ни разу, каждый weekly-прогон
дописывал полный комплект ~488 фич: 4880 строк на 481 ЦП. Задуманный sha1-фолбэк
по атрибутам был мёртв — fid присутствует всегда.

Ключ теперь считается ТОЛЬКО по стабильным атрибутам (нормализованное имя, класс
напряжения, координаты в 1e-5 градуса), fid игнорируется. Координата квантуется
в целое, а не форматируется как float: ключ обязан совпадать байт-в-байт с
бэкфиллом в SQL. sha256 вместо sha1 — встроен в PG16, pgcrypto не нужен.

Починка разбора старые строки не убирает (ключи не совпадут, ON CONFLICT ничего
не перезапишет) → data/sql/99c_power_supply_centers_dedup.sql: пересчёт ключа
существующих строк + схлопывание копий (победитель — свежайший snapshot,
NULLS LAST явно), с печатью чисел до/после и идемпотентностью.

Refs #3322
2026-09-02 14:43:58 +05:00

125 lines
6.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.

-- 99c_power_supply_centers_dedup.sql
-- Issue #3322 — power_supply_centers раздут ×10: 4880 строк на 481 уникальный ЦП.
--
-- Причина. rosseti_wfs_loader брал external_id из feature['id'] WFS-ответа, а
-- GeoServer отдаёт СЕССИОННЫЙ fid (новый на каждый GetFeature) → ON CONFLICT
-- (source, external_id) не срабатывал ни разу, каждый weekly-прогон добавлял
-- полный набор ~488 фич заново. Починка разбора сама старые строки не убирает
-- (ON CONFLICT ничего не перезапишет, ключи не совпадут) → нужен этот бэкфилл.
--
-- Что делает файл:
-- (а) пересчитывает external_id по НОВОЙ формуле (см. ниже) для всех строк
-- source='rosseti_wfs';
-- (б) схлопывает копии: победитель группы — свежайший снапшот
-- (fetched_at DESC NULLS LAST, id DESC — DESC в PG это NULLS FIRST,
-- поэтому NULLS LAST задан ЯВНО);
-- (в) печатает числа: строк до / после, удалено, переключено на новый ключ.
-- Ожидание после прогона — ~481-488 строк (столько ЦП отдаёт источник).
-- (г) идемпотентен: на повторном прогоне ключи уже совпадают → 0 удалений,
-- 0 обновлений, «до» = «после».
--
-- ФОРМУЛА КЛЮЧА (дублирует rosseti_wfs_loader._stable_external_id — менять только
-- синхронно, иначе следующий weekly-прогон вставит второй комплект строк):
-- seed = sc_name_norm || '|' || voltage_class || '|' || lon_e5 || '|' || lat_e5
-- external_id = 'h:' || left(hex(sha256(utf8(seed))), 16)
-- где lon_e5/lat_e5 — координата в единицах 1e-5 градуса (~1 м), округление
-- round() = half-away-from-zero (в питоне ровно то же: floor(|v|*1e5+0.5) со знаком);
-- пустая строка, если geom отсутствует. Целые, а не форматированный float —
-- текстовое представление double в питоне и в PG различается.
-- sha256, а не sha1: sha256 встроен в PG16, sha1 потребовал бы pgcrypto.
--
-- Байт-в-байт совпадение с питоном держится на том, что SQL НИЧЕГО не нормализует
-- сам: sc_name_norm и voltage_class — уже готовые колонки, их записал тот же
-- normalize_sc_name / parse_voltage_class. Если normalize_sc_name когда-нибудь
-- изменится, старые sc_name_norm разъедутся с новыми ключами — тогда нужен
-- повторный прогон логики этого файла (он идемпотентен, ре-apply безопасен).
--
-- Порядок: миграция ПЕРЕД деплоем кода (schema-first) — новый код после неё
-- попадает ON CONFLICT-ом в уже схлопнутые строки.
--
-- Naming: deploy.yml применяет файлы по `ls -1 data/sql/*.sql | sort`;
-- '99c_' идёт после '99b_grant_quarter_price_index_fdw.sql' ('b' < 'c').
BEGIN;
SET LOCAL lock_timeout = '5s';
DO $$
DECLARE
rows_before bigint;
names_before bigint;
rows_after bigint;
names_after bigint;
deleted bigint;
rekeyed bigint;
BEGIN
SELECT count(*), count(DISTINCT sc_name_norm)
INTO rows_before, names_before
FROM power_supply_centers
WHERE source = 'rosseti_wfs';
CREATE TEMP TABLE psc_new_key ON COMMIT DROP AS
SELECT
id,
fetched_at,
'h:' || substring(
encode(
sha256(convert_to(
sc_name_norm
|| '|' || coalesce(voltage_class, '')
|| '|' || CASE WHEN geom IS NULL THEN ''
ELSE round(ST_X(geom)::numeric * 100000)::text END
|| '|' || CASE WHEN geom IS NULL THEN ''
ELSE round(ST_Y(geom)::numeric * 100000)::text END,
'UTF8'
)),
'hex'
) FROM 1 FOR 16
) AS new_key
FROM power_supply_centers
WHERE source = 'rosseti_wfs';
-- (б) схлопывание: оставляем свежайший снапшот каждой группы.
-- Резервы (reserve_mva и пр.) не теряются: rosseti/eesk-лоадеры пишут их
-- UPDATE-ом по sc_name_norm, т.е. во ВСЕ копии сразу, победитель их несёт.
WITH ranked AS (
SELECT
id,
row_number() OVER (
PARTITION BY new_key
ORDER BY fetched_at DESC NULLS LAST, id DESC
) AS rn
FROM psc_new_key
)
DELETE FROM power_supply_centers p
USING ranked r
WHERE p.id = r.id
AND r.rn > 1;
GET DIAGNOSTICS deleted = ROW_COUNT;
-- (а) пересчёт ключа у выживших. После DELETE каждый new_key принадлежит
-- ровно одной строке → UNIQUE (source, external_id) не нарушается.
-- IS DISTINCT FROM даёт идемпотентность: второй прогон обновит 0 строк.
UPDATE power_supply_centers p
SET external_id = k.new_key
FROM psc_new_key k
WHERE p.id = k.id
AND p.external_id IS DISTINCT FROM k.new_key;
GET DIAGNOSTICS rekeyed = ROW_COUNT;
SELECT count(*), count(DISTINCT sc_name_norm)
INTO rows_after, names_after
FROM power_supply_centers
WHERE source = 'rosseti_wfs';
RAISE NOTICE '#3322 power_supply_centers: было % строк / % имён -> стало % строк / % имён (удалено %, переключено на стабильный ключ %)',
rows_before, names_before, rows_after, names_after, deleted, rekeyed;
-- 700 — потолок здравого смысла: источник отдаёт ~488 ЦП по области.
-- Превышение = формула ключа не схлопнула дубли, смотреть глазами.
IF rows_after > 700 THEN
RAISE WARNING '#3322: после дедупа осталось % строк (ожидалось ~481-488) — проверить формулу ключа', rows_after;
END IF;
END $$;
COMMIT;