gendesign/data/sql/189_land_reservation_nulls_not_distinct.sql
bot-backend 0792e34172
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
fix(ptica): land_reservation перестаёт копить дубли — 91% таблицы были копиями (#2464)
ВНИМАНИЕ: миграция 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>
2026-08-20 14:45:56 +05:00

52 lines
3.3 KiB
PL/PgSQL
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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