Some checks failed
CI Trade-In / changes (pull_request) Successful in 15s
CI / changes (pull_request) Failing after 14s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
ВНИМАНИЕ: миграция 189 УДАЛЯЕТ строки на проде (см. «Что удаляется»). _UPSERT_NO_ACT_SQL заканчивается ON CONFLICT DO NOTHING, а единственный подходящий констрейнт — uq_land_reservation_cad_act UNIQUE (cad_num, act_number) с обычной NULL-семантикой. В Postgres NULL != NULL, поэтому у записей БЕЗ номера акта конфликт не наступает никогда: DO NOTHING не срабатывает, каждый недельный прогон вставляет копию. То есть ON CONFLICT здесь был декорацией. Замер прода 20.08.2026: строк всего 297 из них act_number IS NULL 297 (все) групп (cad_num, doc_url) с дублями 27 максимум копий одной записи 11 лишних строк 270 (91% таблицы) Что удаляется: копии сверх первой (минимальный id) в каждой группе. Это порождение бага, а не пользовательские данные: таблица — кэш OCR-разбора PDF с сайта, пересобираемый прогоном таски. Проверено, что ключ подходит: ни у одного cad_num нет более одного doc_url (max = 1), дубли внутри групп — точные копии. Прецеденты NULLS NOT DISTINCT в репо: м.110, м.125, м.140, м.158. Prod = PG16.4. Документация приведена к реальности. Docstring обещал python-дедуп по (cad_num, doc_url) и «двухшаговый UPSERT ниже» — ни того, ни другого в коде не было. Комментарий у варианта B был честнее, но его оценка «rare, data audit OK» не подтвердилась: 91% таблицы. Отложенный там вариант (уникальный индекс на NULL) и реализован этой миграцией. Тест репетирует миграцию на ВРЕМЕННОЙ копии, засеянной как прод (11 копий одной записи + соседний участок): исполняет РЕАЛЬНЫЕ выражения из файла, проверяет 12 строк → 2 и что повторная вставка стала no-op. Плюс фальсификация: со СТАРЫМ констрейнтом дубль обязан появиться — без неё зелёный тест неотличим от «оно и так работало». Плюс два контроля: записи С номером акта дедуплицировались и раньше, разные участки не схлопываются. Первая версия репетиции упала на ADD CONSTRAINT — я разбивал миграцию по «;» и молча выбрасывал куски, начинающиеся с комментария, вместе с DELETE. Это ровно то, что репетиция и должна ловить; разбор исправлен. Прогоны: tests/sql (живой Postgres) 6 passed rc=0; -k "izyatie or reservation" 76 passed rc=0. Шесть nodeid в skip_allowlist.txt — нужен Postgres, в CI идут. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
52 lines
3.3 KiB
PL/PgSQL
52 lines
3.3 KiB
PL/PgSQL
-- 189_land_reservation_nulls_not_distinct.sql
|
||
-- #2464 — дедуп land_reservation и UNIQUE NULLS NOT DISTINCT на живой таблице.
|
||
--
|
||
-- БАГ. `_UPSERT_NO_ACT_SQL` (workers/tasks/izyatie_ocr_ingest.py) заканчивается
|
||
-- `ON CONFLICT DO NOTHING`, а единственный подходящий констрейнт —
|
||
-- `uq_land_reservation_cad_act UNIQUE (cad_num, act_number)` с обычной NULL-семантикой.
|
||
-- В Postgres NULL != NULL, поэтому у записей БЕЗ номера акта конфликт не наступает
|
||
-- никогда: `ON CONFLICT DO NOTHING` не срабатывает, и каждый недельный прогон
|
||
-- вставляет копию. Docstring таски при этом обещает per-batch дедуп и двухшаговый
|
||
-- upsert по (cad_num, doc_url) — ни того, ни другого в коде нет.
|
||
--
|
||
-- ЗАМЕР ПРОДА 2026-08-20 (до правки):
|
||
-- строк всего 297
|
||
-- из них с act_number IS NULL 297 (то есть все)
|
||
-- групп (cad_num, doc_url) с дублями 27
|
||
-- максимум копий в группе 11
|
||
-- лишних строк 270 (91% таблицы)
|
||
--
|
||
-- Проверено, что ключ подходит: ни у одного cad_num нет более одного doc_url
|
||
-- (max = 1), то есть NULLS NOT DISTINCT по (cad_num, act_number) НЕ схлопнет
|
||
-- разные документы одного участка. Дубли внутри групп — точные копии: по одному
|
||
-- различному значению reservation_kind и act_date на группу.
|
||
--
|
||
-- ЧТО УДАЛЯЕТСЯ. Строки-копии сверх первой (по возрастанию id) в каждой группе
|
||
-- (cad_num, act_number) среди act_number IS NULL. Это порождение бага, а не
|
||
-- пользовательские данные; таблица — кэш OCR-разбора PDF с сайта, пересобираемый
|
||
-- прогоном таски. Первая строка группы (минимальный id) сохраняется целиком.
|
||
--
|
||
-- ПОЧЕМУ ОТДЕЛЬНОЙ МИГРАЦИЕЙ, а не правкой CREATE TABLE: та же причина, что в
|
||
-- м.158 — исходный файл уже в _schema_migrations и на деплое пропускается.
|
||
-- Прецеденты NULLS NOT DISTINCT в репо: м.110, м.125, м.140, м.158. Prod = PG16.4.
|
||
-- Apply after: 188_regrant_quarter_price_index_fdw.sql
|
||
|
||
BEGIN;
|
||
|
||
-- 1) Дедуп: оставляем строку с минимальным id в каждой группе.
|
||
DELETE FROM land_reservation a
|
||
USING land_reservation b
|
||
WHERE a.act_number IS NULL
|
||
AND b.act_number IS NULL
|
||
AND a.cad_num = b.cad_num
|
||
AND a.id > b.id;
|
||
|
||
-- 2) Пересоздаём констрейнт с NULL-семантикой, при которой ON CONFLICT матчит.
|
||
ALTER TABLE land_reservation
|
||
DROP CONSTRAINT IF EXISTS uq_land_reservation_cad_act;
|
||
|
||
ALTER TABLE land_reservation
|
||
ADD CONSTRAINT uq_land_reservation_cad_act
|
||
UNIQUE NULLS NOT DISTINCT (cad_num, act_number);
|
||
|
||
COMMIT;
|