fix(db): default cad_parcels.source in trigger (on-demand ingest 2nd NOT NULL gap)
Some checks failed
Deploy / build-worker (push) Successful in 39s
Deploy / deploy (push) Has been cancelled
Deploy / changes (push) Successful in 6s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 36s

After #129 fixed quarter_cad_number, the on-demand НСПД ingest of a real
not-pre-loaded parcel hit the NEXT cad_parcels NOT NULL column: source
(NotNullViolation) → geometry transaction rolled back → POST /analyze still
stuck. Repro 66:41:0610029:83 (job 57: quarter derived by #129, source null).

Migration 130: extend the #129 BEFORE INSERT/UPDATE trigger function to also
default source→'nspd' when NULL (column DEFAULT can't help — INSERT binds
explicit NULL; trigger overrides it). Explicit sources ("search"/"wms_grid_walk")
respected — only NULL is filled. cad_parcels NOT NULL surface now fully covered
(cad_num/quarter[#129]/category_id[literal]/source[#130]/fetched_at+updated_at[NOW]).
Dry-run-verified on prod: null quarter+source → 66:41:0610029 + nspd, INSERT ok.
This commit is contained in:
Light1YT 2026-06-04 12:49:46 +05:00
parent 45d5a338ea
commit 51966b38c8

View file

@ -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;