Skip to content

fix(bpp): zakaz nakładających się okresów autor-jednostka (EXCLUDE + btree_gist) (1.7)#655

Merged
mpasternak merged 1 commit into
devfrom
fix/autor-jednostka-interval
Jul 24, 2026
Merged

fix(bpp): zakaz nakładających się okresów autor-jednostka (EXCLUDE + btree_gist) (1.7)#655
mpasternak merged 1 commit into
devfrom
fix/autor-jednostka-interval

Conversation

@mpasternak

Copy link
Copy Markdown
Member

Kontynuacja #642 (zmergowane 2026-07-20). Gałąź przebazowana na dev — diff
zawiera wyłącznie zmiany 1.7 (0473/0474 bazują na 0471/0472 już w dev).

Problem (poz. 1.7 audytu)

Autor.dodaj_jednostke (autor.py:343-355) robi check-then-create z predykatem
przedziałowym (rozpoczal_prace__lte / zakonczyl_prace__gte). Nie da się
tego wyrazić zwykłym unique constraintem — dwa współbieżne wywołania produkują
nakładające się okresy zatrudnienia tego samego autora w tej samej jednostce.
1.3 (#642) pokryło tylko wariant rozpoczal_prace IS NULL; wariant przedziałowy
został tutaj.

Zmiana

ExclusionConstraint (btree_gist — już w baseline od 0056) blokująca
nakładające się okresy dla tej samej pary (autor, jednostka):

  • = na autor, = na jednostka, && (overlap) na daterange('[]'),
  • condition=Q(rozpoczal_prace__isnull=False)rozłączna z partial-unique
    0472 (IS NULL): predykaty IS NULL / IS NOT NULL dzielą wiersze idealnie,
    bez pokrycia i bez luki.

Granice '[]' (inkluzywne, zgodne z __lte/__gte): PostgreSQL normalizuje do
[), więc przyleganie (koniec D + start D+1) PRZECHODZI (legalna ciągła
historia), a wspólny skrajny dzień ([A,D]+[D,B]) jest BLOKOWANY. Otwarty
koniec (zakonczyl_prace NULL) → [start, ), objęty.

  • migracja 0473guard (RunPython): wykrywa istniejące nakładania tym
    SAMYM SQL-em co constraint i odmawia (raise z listą kolizji), NIE
    deduplikuje. Uzasadnienie: unique_together (autor, jednostka, rozpoczal_prace)
    czyni identyczny duplikat niemożliwym wśród NIE-NULL, więc każde nakładanie to
    „realnie różne okresy", których nie wolno auto-scalać (decyzja dziedzinowa).
    Na dzisiejszych danych no-op.
  • migracja 0474 — sam AddConstraint (zależność 0473, rozdział od guarda —
    deferred trigger events).
  • obsługa IntegrityError na ścieżce przedziałowej dodaj_jednostke (wchłania
    pokrywający okres z wyścigu, spójnie z 1.3, bez dublowania post-checku NULL).

Weryfikacja danych produkcyjnych

Na czerwcowym dumpie (85832 wierszy, 4426 z datą): ZERO nakładań, 0 par z >1
datowanym okresem. ALTER TABLE ... ADD CONSTRAINT EXCLUDE przeszedł ręcznie na
prawdziwych danych. Guard 0473 jest więc dziś no-op, ale chroni przyszłe bazy.

Weryfikacja testowa

  • 20+ testów: nakładające się (w tym wspólny skrajny dzień, otwarty koniec) →
    IntegrityError; przylegające/rozłączne → PRZECHODZĄ (realny ORM, count==2,
    nie atrapy). Guard: odmawia na nakładaniach, przepuszcza rozłączne.
  • makemigrations bpp --check → No changes detected. Review PASS + APPROVED.

Znany Minor (follow-up, opcjonalny): handler wyścigu nie dopasuje pokrywającego
okresu OTWARTEGO (zakonczyl_prace NULL) — ale ta sama luka jest w istniejącym
czy_juz_istnieje (spójne z 1.3), skrajnie wąski wyścig.

Poz. 1.7 audytu.

🤖 Generated with Claude Code

https://claude.ai/code/session_019GmViAsaif9MXeuDMA5NHX

Datowane okresy zatrudnienia tego samego autora w tej samej jednostce
tworzone metoda check-then-create moglyby sie czesciowo pokrywac — zaden
warunek unikalnosci tego nie wykrywal. Dodano:

- ExclusionConstraint (EXCLUDE + btree_gist) na daterange '[]' powiazania,
  rozlaczny z partial-unique z 0472 (warunek rozpoczal_prace IS NOT NULL);
- migracje 0473: GUARD (RunPython) odmawiajacy z czytelna lista kolizji,
  gdy w bazie sa juz nakladajace sie okresy (dedup/guard i DDL osobno) —
  na dzisiejszych danych no-op;
- 0474: sam AddConstraint, zalezny od 0473;
- obsluge IntegrityError na sciezce przedzialowej dodaj_jednostke:
  pokrywajacy okres z wyscigu jest wchlaniany (return), a nie re-raise;
- testy: nakladanie/wspolny skrajny dzien -> IntegrityError; przylegajace
  (koniec+1=start), rozlaczne i otwarty koniec -> przechodza; guard.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@mpasternak
mpasternak merged commit 32684ce into dev Jul 24, 2026
18 of 22 checks passed
@mpasternak
mpasternak deleted the fix/autor-jednostka-interval branch July 24, 2026 15:04
mpasternak added a commit that referenced this pull request Jul 24, 2026
)

ExclusionConstraint `bpp_autor_jednostka_okresy_bez_nakladan` (#655) jest
IMMEDIATE, a `defragmentuj` rozszerzał i ZAPISYWAŁ ocalały rekord ZANIM
skasował wchłaniany (kasowanie było odroczone na koniec pętli) — przez
moment istniały dwa nakładające się okresy tej samej pary (autor, jednostka)
→ IntegrityError na `save()`. Trafiało też w pipeline importu (utworzenie
nowego, przylegającego okresu → scalenie).

- `defragmentuj` + `_merge_with_open_end`: kasuj wchłaniany rekord PRZED
  rozszerzeniem/domknięciem ocalałego (obiekt żyje w pamięci, więc jego
  daty czytamy po `delete()`).
- test_kiedykolwiek_zwiazani_bez_duplikatow: dane modelowały dwa OTWARTE,
  nakładające się okresy [2010,∞)+[2015,∞) (teraz nielegalne na CREATE) →
  pierwszy zamknięty 2010–2014; intencja (wiele powiązań = jeden wiersz)
  zachowana.
- test_defragmentuj blok 2: [2012,∞)+[2014,2015] (nakładające, to czego #655
  zakazuje) → legalny wariant fragmentacji: sąsiadujące przedziały zamknięte
  scalane do [2012,2015] + asercja count()==1.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01156k3YZrfnErZAzQGbNibt
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant