Versetzung und Befoerderung am Tag, an dem die Besetzung begann
Some checks failed
CI / Lint, Typen, Tests, Build (push) Successful in 11m24s
CI / Migrationen auf leerer Datenbank (push) Has been cancelled

Wer heute schon einmal versetzt wurde, liess sich heute nicht noch einmal
versetzen oder befoerdern: die laufende Besetzung beginnt dann heute, und der
Code schliesst sie auf denselben Tag. chk_assignment_range verlangt aber
valid_to > valid_from -- ein Intervall von null Tagen ist verboten, und der
Vorgang brach mit der rohen Datenbankmeldung ab.

Das Verbot ist richtig, und der Fall ist im Haus schon einmal entschieden
worden: terminate_employee loescht die Besetzung beim Grund "No Show", statt
sie auf [Eintritt, Eintritt) zu schliessen -- niemand hatte sie je inne.
Dieselbe Begruendung gilt hier. Die zweite Versetzung am selben Tag
abzuweisen waere genau der haeufigste Fall gewesen: jemand hat die falsche
Planstelle erwischt und will es sofort richtigstellen. Beide Ereignisse
bleiben in der Historie stehen.

Vier Stellen mit demselben Paar aus Schliessen und Einfuegen: transfer_employee,
promote_employee und im Nachtlauf die Zweige transfer und promotion. Alle vier
aus der laufenden Definition gelesen und an einem Anker ergaenzt; die
Selbstpruefung zaehlt die beiden Stellen im Nachtlauf, statt nur ihr
Vorhandensein zu pruefen -- der Zweig transfer ist dort schon einmal spurlos
verschwunden.
This commit is contained in:
2026-09-29 18:57:56 +02:00
parent be9ca4758f
commit 990ebdbc84

View File

@@ -0,0 +1,177 @@
-- Versetzung und Beförderung am selben Tag, an dem die Besetzung begann
--
-- Gemeldet beim Nachtest am 29.09.: eine Beförderung auf eine andere
-- Planstelle scheiterte mit
--
-- new row for relation "position_assignments" violates check constraint
-- "chk_assignment_range"
--
-- Die Person war am selben Tag schon einmal versetzt worden. Ihre laufende
-- Besetzung begann also heute, und der Code schliesst sie auf denselben Tag:
--
-- update position_assignments set valid_to = <wirksam ab> where valid_to is null;
-- insert into position_assignments (…) values (…, <wirksam ab>);
--
-- `chk_assignment_range` verlangt `valid_to > valid_from`; ein Intervall von
-- null Tagen ist verboten. Das ist richtig so — und der Fall ist im Haus auch
-- schon einmal gelöst worden: terminate_employee **löscht** die Besetzung beim
-- Grund „No Show", statt sie auf [Eintritt, Eintritt) zu schliessen, mit der
-- Begründung, dass niemand sie je innehatte. Dieselbe Begründung gilt hier:
-- eine Besetzung, die am Tag ihres Beginns wieder endet, hat nicht
-- stattgefunden.
--
-- Die Alternative — die zweite Versetzung am selben Tag abzuweisen — wäre
-- genau der häufigste Fall: jemand hat die falsche Planstelle erwischt und
-- will es sofort richtigstellen. Die Ereignisse in employee_history bleiben
-- beide stehen; sichtbar ist also weiterhin, dass zweimal verschoben wurde.
--
-- Betroffen sind vier Stellen mit demselben Paar aus Schliessen und Einfügen:
-- transfer_employee, promote_employee und im Nachtlauf die Zweige `transfer`
-- und `promotion`. Alle vier werden aus der laufenden Definition gelesen und
-- an einem Anker ergänzt.
-- ═══ 1. transfer_employee ═════════════════════════════════════════
do $migration$
declare
v_alt constant text := $anker$update position_assignments set valid_to = v_effective$anker$;
v_neu constant text := $anker$delete from position_assignments
where employee_id = v_employee_id and valid_to is null and valid_from >= v_effective;
update position_assignments set valid_to = v_effective$anker$;
v_def text;
v_anzahl int;
begin
select pg_get_functiondef(p.oid) into v_def
from pg_proc p join pg_namespace n on n.oid = p.pronamespace
where n.nspname = 'public' and p.proname = 'transfer_employee' and p.prokind = 'f';
if v_def is null then
raise exception 'transfer_employee ist nicht vorhanden.';
end if;
v_anzahl := (length(v_def) - length(replace(v_def, v_alt, ''))) / length(v_alt);
if v_anzahl <> 1 then
raise exception 'Der Anker in transfer_employee kommt % mal vor, erwartet einmal.', v_anzahl;
end if;
execute replace(v_def, v_alt, v_neu);
end
$migration$;
-- ═══ 2. promote_employee ══════════════════════════════════════════
do $migration$
declare
v_alt constant text := $anker$update position_assignments set valid_to = v_effective_date$anker$;
v_neu constant text := $anker$delete from position_assignments
where employee_id = v_employee_id and valid_to is null and valid_from >= v_effective_date;
update position_assignments set valid_to = v_effective_date$anker$;
v_def text;
v_anzahl int;
begin
select pg_get_functiondef(p.oid) into v_def
from pg_proc p join pg_namespace n on n.oid = p.pronamespace
where n.nspname = 'public' and p.proname = 'promote_employee' and p.prokind = 'f';
if v_def is null then
raise exception 'promote_employee ist nicht vorhanden.';
end if;
v_anzahl := (length(v_def) - length(replace(v_def, v_alt, ''))) / length(v_alt);
if v_anzahl <> 1 then
raise exception 'Der Anker in promote_employee kommt % mal vor, erwartet einmal.', v_anzahl;
end if;
execute replace(v_def, v_alt, v_neu);
end
$migration$;
-- ═══ 3. Der Nachtlauf, beide Zweige ═══════════════════════════════
--
-- Hier steht dasselbe zweimal — bei `transfer` und bei `promotion` mit
-- Zielplanstelle. Beide werden ersetzt, und die Zahl wird geprüft: der Zweig
-- `transfer` ist in diesem Projekt schon einmal spurlos verschwunden.
do $migration$
declare
v_alt constant text := $anker$update position_assignments set valid_to = v_rec.effective_date$anker$;
v_neu constant text := $anker$delete from position_assignments
where employee_id = v_rec.employee_id and valid_to is null and valid_from >= v_rec.effective_date;
update position_assignments set valid_to = v_rec.effective_date$anker$;
v_def text;
v_anzahl int;
begin
select pg_get_functiondef(p.oid) into v_def
from pg_proc p join pg_namespace n on n.oid = p.pronamespace
where n.nspname = 'public' and p.proname = 'apply_due_pending_changes' and p.prokind = 'f';
if v_def is null then
raise exception 'apply_due_pending_changes ist nicht vorhanden.';
end if;
v_anzahl := (length(v_def) - length(replace(v_def, v_alt, ''))) / length(v_alt);
if v_anzahl <> 2 then
raise exception 'Der Anker im Nachtlauf kommt % mal vor, erwartet zweimal.', v_anzahl;
end if;
execute replace(v_def, v_alt, v_neu);
end
$migration$;
-- ═══ 4. Selbstprüfung ═════════════════════════════════════════════
do $$
declare
v_def text;
v_anzahl int;
begin
select pg_get_functiondef(p.oid) into v_def
from pg_proc p join pg_namespace n on n.oid = p.pronamespace
where n.nspname = 'public' and p.proname = 'transfer_employee' and p.prokind = 'f';
if v_def not like '%valid_from >= v_effective%' then
raise exception 'transfer_employee raeumt die Besetzung vom selben Tag nicht ab.';
end if;
if v_def not like '%require_hr_admin()%' then
raise exception 'transfer_employee prueft die Rechte nicht.';
end if;
select pg_get_functiondef(p.oid) into v_def
from pg_proc p join pg_namespace n on n.oid = p.pronamespace
where n.nspname = 'public' and p.proname = 'promote_employee' and p.prokind = 'f';
if v_def not like '%valid_from >= v_effective_date%' then
raise exception 'promote_employee raeumt die Besetzung vom selben Tag nicht ab.';
end if;
if v_def not like '%require_hr_admin()%' then
raise exception 'promote_employee prueft die Rechte nicht.';
end if;
-- Was an dieser Funktion schon einmal verlorenging.
if v_def not like '%target_position_id%' then
raise exception 'promote_employee hat die Zielplanstelle verloren.';
end if;
select pg_get_functiondef(p.oid) into v_def
from pg_proc p join pg_namespace n on n.oid = p.pronamespace
where n.nspname = 'public' and p.proname = 'apply_due_pending_changes' and p.prokind = 'f';
v_anzahl := (length(v_def) - length(replace(v_def, 'valid_from >= v_rec.effective_date', '')))
/ length('valid_from >= v_rec.effective_date');
if v_anzahl <> 2 then
raise exception 'Der Nachtlauf raeumt an % von 2 Stellen ab.', v_anzahl;
end if;
v_anzahl := (length(v_def) - length(replace(v_def, 'insert into position_assignments', '')))
/ length('insert into position_assignments');
if v_anzahl <> 2 then
raise exception 'Der Nachtlauf haengt die Besetzung % mal um, erwartet zweimal.', v_anzahl;
end if;
if v_def not like '%v_rec.payload->''role''%' then
raise exception 'Der Nachtlauf hat die Gruppe role verloren.';
end if;
if not exists (
select 1 from pg_proc p join pg_namespace n on n.oid = p.pronamespace
where n.nspname = 'public' and p.proname = 'apply_due_pending_changes' and p.prosecdef
) then
raise exception 'Der Nachtlauf ist nicht mehr SECURITY DEFINER.';
end if;
raise notice 'Besetzung am selben Tag: alle vier Stellen abgeraeumt.';
end
$$;