fix(db): dedupe domrf_kn_documents + UNIQUE NULLS NOT DISTINCT (#1206) #1263

Merged
bot-backend merged 1 commit from fix/domrf-documents-unique-nulls-not-distinct into main 2026-06-13 06:17:13 +00:00
Collaborator

Summary

Closes #1206 (P2 data-integrity, prod). domrf_kn_documents копила дубли при каждом sweep: UNIQUE без NULLS NOT DISTINCT, doc_num nullable → ON CONFLICT-арбитр не матчился для NULL.

Bug

  • data/sql/112_22i_domrf_documents.sql:18 объявил UNIQUE (obj_id, doc_type, doc_num, file_url) БЕЗ NULLS NOT DISTINCT.
  • doc_num nullable, extract_documents:125-134 реально передаёт doc_num=None (документированный частый вход).
  • PG: NULL != NULL в уникальном индексе → для docNum=null арбитр ON CONFLICT в scrapers/documents.py:59 НИКОГДА не матчится → каждый ежедневный sweep domrf_kn.py:1903 молча вставляет новую строку-дубль вместо UPDATE.
  • Таблица растёт ×N прогонов; дубли уходят в analytics_queries.py:1068.

Прецедент того же класса: м.110 (POI), м.125 (supply_layers).

Fix (Миграция 140, SQL-only)

  1. Дедуп существующих NULL-дублей: ROW_NUMBER() OVER (PARTITION BY obj_id, doc_type, doc_num, file_url ORDER BY scraped_at DESC, id DESC) → DELETE WHERE rn > 1 AND doc_num IS NULL. Winner = самая свежая (caller бампит scraped_at на каждом UPDATE).
  2. DROP старого autonamed constraint'а IF EXISTS (re-apply safe).
  3. ADD uq_domrf_kn_documents_logical UNIQUE NULLS NOT DISTINCT (...) именованный + guard по pg_constraint.conname (паттерн м.125 → idempotent).

Caller scrapers/documents.py НЕ меняется — после миграции ON CONFLICT-арбитр начнёт матчить NULL-doc_num строки автоматически. Никакой data-coercion (doc_num="") не нужно.

Deploy

  • Auto-apply через deploy.yml + _schema_migrations tracking.
  • Backend re-deploy не требуется (caller не меняется).

Closes #1206

## Summary Closes #1206 (P2 data-integrity, prod). `domrf_kn_documents` копила дубли при каждом sweep: UNIQUE без `NULLS NOT DISTINCT`, `doc_num` nullable → ON CONFLICT-арбитр не матчился для NULL. ## Bug - `data/sql/112_22i_domrf_documents.sql:18` объявил `UNIQUE (obj_id, doc_type, doc_num, file_url)` БЕЗ `NULLS NOT DISTINCT`. - `doc_num` nullable, `extract_documents:125-134` реально передаёт `doc_num=None` (документированный частый вход). - PG: `NULL != NULL` в уникальном индексе → для `docNum=null` арбитр `ON CONFLICT` в `scrapers/documents.py:59` **НИКОГДА не матчится** → каждый ежедневный sweep `domrf_kn.py:1903` молча вставляет новую строку-дубль вместо UPDATE. - Таблица растёт ×N прогонов; дубли уходят в `analytics_queries.py:1068`. Прецедент того же класса: м.110 (POI), м.125 (supply_layers). ## Fix (Миграция 140, SQL-only) 1. **Дедуп существующих NULL-дублей**: `ROW_NUMBER() OVER (PARTITION BY obj_id, doc_type, doc_num, file_url ORDER BY scraped_at DESC, id DESC)` → DELETE `WHERE rn > 1 AND doc_num IS NULL`. Winner = самая свежая (caller бампит `scraped_at` на каждом UPDATE). 2. **DROP** старого autonamed constraint'а `IF EXISTS` (re-apply safe). 3. **ADD** `uq_domrf_kn_documents_logical UNIQUE NULLS NOT DISTINCT (...)` именованный + guard по `pg_constraint.conname` (паттерн м.125 → idempotent). Caller `scrapers/documents.py` НЕ меняется — после миграции ON CONFLICT-арбитр начнёт матчить NULL-doc_num строки автоматически. Никакой data-coercion (`doc_num=""`) не нужно. ## Deploy - Auto-apply через `deploy.yml` + `_schema_migrations` tracking. - Backend re-deploy не требуется (caller не меняется). Closes #1206
bot-backend added 1 commit 2026-06-13 06:05:44 +00:00
fix(db): dedupe domrf_kn_documents + UNIQUE NULLS NOT DISTINCT (#1206)
All checks were successful
CI / changes (push) Successful in 6s
CI / changes (pull_request) Successful in 5s
CI / frontend-tests (push) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / backend-tests (push) Successful in 6m33s
CI / backend-tests (pull_request) Successful in 6m31s
8caa41dcb1
UNIQUE (obj_id, doc_type, doc_num, file_url) в м.112 без NULLS NOT
DISTINCT, doc_num nullable. В PG NULL != NULL в уникальном индексе →
для документов с docNum=null (документированный частый вход:
extract_documents:125-134 → doc_num=None) ON CONFLICT-арбитр в
scrapers/documents.py:59 НИКОГДА не матчился → каждый ежедневный sweep
domrf_kn.py:1903 молча вставлял новую строку-дубль вместо UPDATE.
Таблица росла ×N прогонов; дубли уходили в analytics_queries.py:1068.

Миграция 140:
1. Дедуп существующих NULL-дублей: ROW_NUMBER() PARTITION BY все 4 ключа
   ORDER BY scraped_at DESC, id DESC → оставляем самую свежую, удаляем
   остальные WHERE rn>1 AND doc_num IS NULL.
2. DROP старого autonamed constraint'а IF EXISTS.
3. ADD uq_domrf_kn_documents_logical UNIQUE NULLS NOT DISTINCT (...) с
   именованным guard по pg_constraint.conname (паттерн м.125 → idempotent).

Caller scrapers/documents.py НЕ меняется — после миграции ON CONFLICT-
арбитр начнёт матчить NULL-doc_num строки автоматически. Прецедент того
же класса: м.110 (uq_infra_dedupe), м.125 (uq_supply_layers_logical).

Closes #1206
bot-backend merged commit b85147a3a0 into main 2026-06-13 06:17:13 +00:00
bot-backend deleted branch fix/domrf-documents-unique-nulls-not-distinct 2026-06-13 06:17:14 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#1263
No description provided.