fix(bpp): zakaz nakładających się okresów autor-jednostka (EXCLUDE + btree_gist) (1.7)#655
Merged
Merged
Conversation
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
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem (poz. 1.7 audytu)
Autor.dodaj_jednostke(autor.py:343-355) robi check-then-create z predykatemprzedział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łowyzostał tutaj.
Zmiana
ExclusionConstraint(btree_gist — już w baseline od0056) blokującanakładające się okresy dla tej samej pary (autor, jednostka):
=na autor,=na jednostka,&&(overlap) nadaterange('[]'),condition=Q(rozpoczal_prace__isnull=False)— rozłączna z partial-unique0472(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łahistoria), a wspólny skrajny dzień ([A,D]+[D,B]) jest BLOKOWANY. Otwarty
koniec (
zakonczyl_praceNULL) →[start, ), objęty.0473— guard (RunPython): wykrywa istniejące nakładania tymSAMYM 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.
0474— samAddConstraint(zależność0473, rozdział od guarda —deferred trigger events).
IntegrityErrorna ścieżce przedziałowejdodaj_jednostke(wchłaniapokrywają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 EXCLUDEprzeszedł ręcznie naprawdziwych danych. Guard
0473jest więc dziś no-op, ale chroni przyszłe bazy.Weryfikacja testowa
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_praceNULL) — ale ta sama luka jest w istniejącymczy_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