-- 130_cad_parcels_default_source.sql -- FIX (prod, продолжение #129) — on-demand НСПД-ингест cad_parcels падает на ВТОРОЙ NOT NULL -- колонке `source` (после того как #129 закрыл quarter_cad_number). -- -- БАГ (ground-truth, измерено на prod после деплоя #129): -- cad_parcels NOT NULL колонки: cad_num, quarter_cad_number, category_id, source, -- fetched_at, updated_at. #129-триггер достроил quarter_cad_number, но on-demand-ингест -- (bulk_harvest.upsert_parcel / parcels-fetch воркер для не-предзагруженных участков) -- доезжает до INSERT с source=NULL → -- psycopg.errors.NotNullViolation: null value in column "source" of relation "cad_parcels" -- → строка падает → geometry-транзакция роллбэчится → cad_parcels_geom тоже не сохраняется -- → POST /analyze для участка навсегда залипает «Геометрия загружается из НСПД…». -- Воспроизведено на РЕАЛЬНОМ участке 66:41:0610029:83 (job 57: упавшая строка несёт -- quarter_cad_number=66:41:0610029 (достроен #129) + валидный geom, null только в source). -- -- Все три code-вызывающих upsert_features передают непустой source ("search", -- "search_per_cat", "wms_grid_walk"), т.е. в локальном коде source не None — но в проде -- до INSERT доезжает NULL (как и с quarter_cad_number, путь ускользает: вероятно отдельная -- ветка/стейл-фронт grid-walk). Вместо охоты за каждым код-путём — гарантируем на уровне БД, -- как для quarter_cad_number: source — метаданные происхождения, для on-demand НСПД-фетча -- осмысленный дефолт 'nspd'. -- -- ПОЧЕМУ ТРИГГЕР (а не DEFAULT): колоночный DEFAULT срабатывает только когда колонка ОПУЩЕНА -- из INSERT; здесь INSERT биндит source ЯВНЫМ NULL → DEFAULT не применился бы. BEFORE-триггер -- же переопределяет явный NULL. Расширяем СУЩЕСТВУЮЩУЮ функцию #129 (CREATE OR REPLACE) — сам -- триггер cad_parcels_derive_quarter_cad (BEFORE INSERT OR UPDATE, #129) уже навешен, меняем -- только тело функции. Один инвариант-страж для всех NOT NULL метаданных, выводимых/дефолтимых. -- -- ПОВЕДЕНИЕ (дополнение к #129): -- • quarter_cad_number IS NULL + cad_num ≥3 сегмента → достроить (как #129). -- • source IS NULL → 'nspd' (осмысленный дефолт происхождения для on-demand НСПД-ингеста; -- не маскирует данные — это просто метка источника, явный source от bulk-загрузчиков -- ("search"/"wms_grid_walk"/...) уважается, перетираем ТОЛЬКО NULL). -- -- Idempotent: CREATE OR REPLACE FUNCTION (переопределяет тело на месте). Триггер не трогаем -- (#129 его создал). BEGIN/COMMIT. Никаких DROP TABLE/TRUNCATE/строк. -- Deploy order: чистая СХЕМА, без зависимости от кода. Эффект мгновенный со следующего ингеста: -- on-demand cad_parcels INSERT больше не падает на NOT NULL (quarter+source достраиваются) → -- geometry персистится → реальный новый участок анализируется. BEGIN; CREATE OR REPLACE FUNCTION cad_parcels_derive_quarter_cad() RETURNS TRIGGER AS $$ BEGIN -- #129: достроить quarter_cad_number из cad_num (первые 3 colon-сегмента), если NULL. IF NEW.quarter_cad_number IS NULL AND NEW.cad_num IS NOT NULL AND array_length(string_to_array(NEW.cad_num, ':'), 1) >= 3 THEN NEW.quarter_cad_number := array_to_string((string_to_array(NEW.cad_num, ':'))[1:3], ':'); END IF; -- #130: source NOT NULL — on-demand-ингест доезжает с NULL. Дефолт 'nspd' (метка -- источника). Перетираем ТОЛЬКО NULL → явные source ("search"/"wms_grid_walk"/...) целы. IF NEW.source IS NULL THEN NEW.source := 'nspd'; END IF; RETURN NEW; END; $$ LANGUAGE plpgsql; COMMENT ON FUNCTION cad_parcels_derive_quarter_cad() IS 'BEFORE INSERT OR UPDATE-триггер cad_parcels (#129+#130): достраивает NOT NULL ' 'quarter_cad_number из cad_num когда NULL, и дефолтит NOT NULL source→''nspd'' когда NULL. ' 'Чинит NotNullViolation во ВСЕХ путях записи on-demand НСПД-ингеста (репро 66:41:0610029:83). ' 'Явные значения уважаются — перетираются только NULL. <3-сегм cad_num → quarter остаётся ' 'NULL (NOT NULL отклонит реальный мусор).'; COMMIT;