diff --git a/data/sql/130_cad_parcels_default_source.sql b/data/sql/130_cad_parcels_default_source.sql new file mode 100644 index 00000000..5d293b18 --- /dev/null +++ b/data/sql/130_cad_parcels_default_source.sql @@ -0,0 +1,71 @@ +-- 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;