fix(ptica): land_reservation перестаёт копить дубли — 91% таблицы были копиями (#2464) #2966
No reviewers
Labels
No labels
Fable 5 ревью
GG-форсайт
admin
analytics
auth
automation
bug
business
chore
ci
compliance
data
data-moat
docs
duplicate
dx
enhancement
feedback/max
generative
needs-discussion
needs-human
observability
pause-bots
performance
priority/p0
priority/p1
priority/p2
priority/p3
scope/backend
scope/db
scope/devops
scope/frontend
scope/qa
scrapers
security
site-finder
stage/1
stage/2
status/blocked
status/done
status/needs-analysis
status/needs-fix
status/qa
status/ready
status/review
status/wip
tech-debt
tradein
ux
week ревью 1
wontfix
ИРД
вторичка
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: lekss361/gendesign#2966
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "fix/2464-land-reservation-dedup"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Пункт эпика #2464:
izyatie_ocr_ingest.py:101. Оказался не про документацию.Дефект
_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)
act_number IS NULL(cad_num, doc_url)с дублямиЧто удаляется
Копии сверх первой (минимальный
id) в каждой группе. Это порождение бага, а не пользовательские данные: таблица — кэш OCR-разбора PDF с сайта, пересобираемый прогоном таски.Проверено, что ключ подходит:
cad_numнет более одногоdoc_url(max = 1) →NULLS NOT DISTINCTпо(cad_num, act_number)не схлопнет разные документы одного участка;reservation_kindиact_dateна группу.Прецеденты
NULLS NOT DISTINCTв репо: м.110, м.125, м.140, м.158. Prod = PG 16.4.Документация приведена к реальности
Docstring обещал python-дедуп по
(cad_num, doc_url)перед UPSERT и «двухшаговый UPSERT ниже». Ни того, ни другого в коде не было.Комментарий у варианта B был честнее — он признавал проблему, но оценивал её как «rare, data audit OK» и откладывал уникальный индекс «database-expert'у». Оценка не подтвердилась: 91 % таблицы. Отложенный там вариант этой миграцией и реализован.
Тест — репетиция, а не описание
На временной копии, засеянной как прод (11 копий одной записи + соседний участок), исполняются реальные выражения из файла миграции: 12 строк → 2, повторная вставка стала no-op.
Плюс фальсификация: со старым констрейнтом дубль обязан появиться — без неё зелёный тест неотличим от «оно и так работало». Плюс два контроля: записи с номером акта дедуплицировались и раньше; разные участки не схлопываются.
Первая версия репетиции упала на
ADD CONSTRAINT: я разбивал миграцию по;и молча выбрасывал куски, начинающиеся с комментария, — вместе сDELETE. Это ровно то, что репетиция и должна ловить; разбор исправлен.Прогоны
Шесть nodeid в
skip_allowlist.txt— нужен Postgres, в CI идут.7d1db9edceto0792e34172Путь восстановления — до мержа, не после
Миграция удаляет 270 строк на проде, поэтому фиксирую здесь, чем это откатывается.
Ежедневные дампы БД на VPS (
ops/backup.sh, cron 03:30 UTC):Дамп за 20.08 сделан в 03:41, миграция применится позже — то есть в нём таблица в состоянии «до». Восстановление одной таблицы из такого дампа обычной выборкой по
land_reservation, полный откат БД не требуется.Дополнительно: таблица — кэш OCR-разбора PDF с сайта екатеринбург.рф, пересобираемый прогоном таски
ingest_izyatie_ocr. То есть даже без дампа данные восстановимы источником.Что удаляется, ещё раз коротко
270 строк-копий из 297; каждая — точная копия соседа по группе
(cad_num, act_number IS NULL). Первая строка группы (минимальныйid) сохраняется. Проверено, что ни у одногоcad_numнет более одногоdoc_url, то есть ключ не схлопывает разные документы.Прод-проверка после деплоя
Миграция применилась. Сверка со снимком «до», снятым перед мержем:
cad_numUNIQUE (cad_num, act_number)UNIQUE NULLS NOT DISTINCT (cad_num, act_number)Число уникальных
cad_numне изменилось — удалены ровно копии, ни одной настоящей записи не потеряно. Миграция отмечена в_schema_migrations.Механизм проверен на живой таблице
Не по коду, а поведением — вставка существующей записи тем же
ON CONFLICT DO NOTHING, что в таске, внутри транзакции с откатом:INSERT 0 0— это и есть доказательство: до миграции такая же вставка добавляла бы строку (NULL != NULL, конфликт не наступал). Теперь недельный прогон таски перестаёт копить дубли.Первая попытка пробы была составлена неполно (не хватило
basis_act, NOT NULL) — транзакция откатилась чисто, данные не пострадали; проба переделана.