-- 128_supply_layers_dev_group_key.sql
-- #970 (REOPENED, CRITICAL) — Layer-3 future-supply collapse: добавляем dev_group_name в зерно
-- и в логический UNIQUE-ключ supply_layers, чтобы dev_group'ы района перестали схлопываться.
--
-- БАГ (ground-truth, измерено на prod):
--   Layer-3 (future supply) в supply_layers.py (_L3_FUTURE_SQL) считается per
--   (district_name, dev_group_name) — GROUP BY district_name, dev_group_name — но строки
--   эмитятся с complex_id=NULL и БЕЗ dev_group_name (колонки нет с м.125). Логический ключ
--   uq_supply_layers_logical (м.125) = (layer, district_name, complex_id, obj_class, source,
--   snapshot_date) NULLS NOT DISTINCT — dev_group_name в нём НЕТ. Итог: ВСЕ dev_group'ы района
--   дают ОДИН и тот же ключ (complex_id всегда NULL для L3) → UPSERT воркера
--   (supply_layers_refresh.py, ON CONFLICT) перезатирает их друг другом → массовая потеря.
--   Измерено (Академический, latest snapshot): должно быть 13 808 юнитов в 15 dev_group'ах /
--   54 объектах, выживает 1 строка / 607 юнитов (~95.6% потерь, недетерминированно per-run).
--
-- ЧИНИМ (схема): dev_group_name становится частью L3-grain и частью логического ключа.
--   1. ADD COLUMN dev_group_name TEXT (nullable — у L1/L2 dev_group'а НЕТ, его ставит только L3).
--   2. Перестроить uq_supply_layers_logical, ДОБАВив dev_group_name:
--        (layer, district_name, complex_id, obj_class, dev_group_name, source, snapshot_date)
--      по-прежнему UNIQUE NULLS NOT DISTINCT (PG15+, как м.125).
--
-- Почему NULLS NOT DISTINCT сохраняется (КРИТИЧНО — корректность дедупа обоих случаев):
--   В обычном UNIQUE Postgres считает NULL DISTINCT → (а) region-wide / L1/L2 строки
--   (district_name/complex_id/obj_class/dev_group_name IS NULL) дублировались бы, (б) ON CONFLICT
--   их НЕ матчил бы (NULL <> NULL). NULLS NOT DISTINCT сравнивает NULL'ы как РАВНЫЕ. Следствия:
--     • L1/L2 строки: dev_group_name=NULL у ВСЕХ из них → новая колонка для них «прозрачна»,
--       они продолжают дедуплиться по ОСТАЛЬНОЙ части ключа ровно как раньше (NULL=NULL по
--       dev_group_name не ломает их уникальность). Семантика L1/L2 НЕ меняется.
--     • L3 строки: теперь несут РАЗНЫЕ непустые dev_group_name → дают РАЗНЫЕ ключи → больше
--       НЕ схлопываются; разные dev_group'ы района сосуществуют (15 строк вместо 1).
--   Прецеденты NULLS NOT DISTINCT в репо: м.125 (исходный ключ), м.110 (uq_infra_dedupe).
--
-- Зависимости / окружение:
--   • supply_layers (м.125) — ALTER существующей таблицы (ADD COLUMN + rebuild constraint).
--   • PostgreSQL 16 (postgis/postgis:16-3.4) — нужен для UNIQUE NULLS NOT DISTINCT (PG15+).
--   • Unique-constraint сам по себе даёт индекс — отдельный supporting-индекс под этот ключ
--     НЕ нужен (м.125 его и не заводила; read-path индексы idx_supply_layers_* остаются как есть).
--
-- Idempotent (acceptance — ре-applied чисто ДВАЖДЫ): ADD COLUMN IF NOT EXISTS + guarded DROP/ADD
--   констрейнта через DO-блок с проверкой pg_constraint (мирроринг м.125 / м.40). Re-apply
--   конвергентен. BEGIN/COMMIT — атомарно. НИКАКИХ DROP TABLE/TRUNCATE, никаких INSERT/строк.
--
-- Deploy order (КРИТИЧНО): эта SQL-миграция применяется ПЕРВОЙ (auto-apply через deploy.yml /
--   _schema_migrations tracking, ровно один раз по NN). ЗАТЕМ — отдельным backend-изменением
--   (НЕ в этом PR) воркер backend/app/workers/tasks/supply_layers_refresh.py:
--     • _UPSERT_SQL INSERT column-list + VALUES должны ДОБАВИТЬ dev_group_name
--       (CAST(:dev_group_name AS text)),
--     • ON CONFLICT (...) должен стать
--       (layer, district_name, complex_id, obj_class, dev_group_name, source, snapshot_date) —
--       ровно под новый ключ,
--     • а supply_layers.py L3-emit (SupplyLayerRow, layer=3) должен ПОПУЛИРОВАТЬ dev_group_name
--       (сейчас он только пишет dev_group в method-строку, в саму строку — нет).
--   Никогда наоборот: воркер с новым ON CONFLICT упадёт без этой колонки/ключа.
--   Существующие схлопнутые данные САМО-залечиваются на следующем supply_layers_refresh —
--   воркер перезапишет район строками по ИСПРАВЛЕННОМУ ключу (15 dev_group'ов вместо 1).

BEGIN;

-- ── 1. Новая колонка dev_group_name (L3-grain) ─────────────────────────────────
-- Nullable: L1/L2 dev_group'а не имеют (ставит только L3 = бакет future-supply по группе
-- застройщика). ADD COLUMN IF NOT EXISTS → re-apply пропускает добавление (safe).
ALTER TABLE supply_layers
    ADD COLUMN IF NOT EXISTS dev_group_name TEXT;

COMMENT ON COLUMN supply_layers.dev_group_name IS
    'L3-grain (#970): бакет future-supply по группе застройщика (dev_group_name, м.113). '
    'Часть логического ключа uq_supply_layers_logical для L3 (domrf_multiphase). NULL для '
    'L1/L2 (у них dev_group''а нет). Без этой колонки в ключе все dev_group''ы района '
    'схлопывались в один NULL-key и UPSERT их перезатирал (~95.6% потерь L3).';

-- ── 2. Перестройка логического UNIQUE-ключа: ДОБАВляем dev_group_name ───────────
-- Цель ON CONFLICT воркера (#950 Step5 / #970 fix). NULLS NOT DISTINCT сохраняется (PG15+):
-- L1/L2 (dev_group_name=NULL) дедуплятся по остальной части ключа как раньше; L3 с разными
-- непустыми dev_group_name дают разные ключи и больше не схлопываются. Guarded DROP+ADD через
-- pg_constraint → idempotent re-apply (констрейнт нельзя добавить дважды; имя то же —
-- uq_supply_layers_logical, чтобы ON CONFLICT таргетил его по column-list). Паттерн зеркалит
-- м.125 (constraint-add guard) + м.40 (FK guard). DROP с тем же именем безопасен: на «чистой»
-- м.125-таблице он снимает старый 6-колоночный ключ, затем ставит новый 7-колоночный; на
-- повторном прогоне старого имени уже нет → DROP no-op (IF EXISTS), ADD пропускается guard'ом.
DO $$
BEGIN
    -- Снять старый ключ (м.125: без dev_group_name), если он есть. IF EXISTS → re-apply safe.
    ALTER TABLE supply_layers
        DROP CONSTRAINT IF EXISTS uq_supply_layers_logical;

    -- Поставить новый ключ (с dev_group_name) только если его ещё нет → re-apply конвергентен.
    IF NOT EXISTS (
        SELECT 1 FROM pg_constraint WHERE conname = 'uq_supply_layers_logical'
    ) THEN
        ALTER TABLE supply_layers
            ADD CONSTRAINT uq_supply_layers_logical
            UNIQUE NULLS NOT DISTINCT
                (layer, district_name, complex_id, obj_class,
                 dev_group_name, source, snapshot_date);
    END IF;
END
$$;

COMMIT;
