Zwei Fehler aus dem ersten Lauf des Rauchtests, beide von mir. Der Anker fuer transfer_employee stammte aus 20260727120200 und fand nichts: die Funktion ist seither dynamisch gepatcht worden, die Belegungsabfrage ist zweizeilig und beruecksichtigt den Stichtag. Dieselbe Falle wie immer, nur diesmal nicht im Rumpf einer Funktion, sondern im Anker auf sie -- fuer hire_employee und rehire_employee stimmten die Anker, weil sie aus der zuletzt erzeugten Fassung kamen. Die Migration war nie angewendet (sie bricht als Ganzes ab), deshalb die Datei korrigiert statt eine neue geschrieben. Und der Rauchtest hat sich an einer Stelle selbst durchgewunken: das `raise` fuer den Fehlschlag stand innerhalb des Blocks mit exception-Zweig, wurde also vom eigenen Handler gefangen -- und weil seine Meldung das Wort "vorgemerkte" enthielt, bestand die Pruefung auf die erwartete Fehlermeldung. Der Schritt galt als bestanden, obwohl die Migration gar nicht angewendet war. Jetzt merkt sich der Block nur, ob es ging, und ausgewertet wird danach.
251 lines
11 KiB
PL/PgSQL
251 lines
11 KiB
PL/PgSQL
-- Eine Planstelle, auf die jemand vorgemerkt ist, ist nicht frei
|
|
--
|
|
-- Gemeldet im Test vom 29.09. (F.24): eine Person wurde auf eine Planstelle
|
|
-- eingestellt, auf die bereits eine Versetzung zum 20.10. vorgemerkt war. Die
|
|
-- Planstellenliste führte die Stelle als unbesetzt, der Assistent bot sie an,
|
|
-- und das Organigramm zum 20.10. zeigte sie zweifach besetzt.
|
|
--
|
|
-- ═══ Warum das schlimmer ist, als es aussieht ═════════════════════
|
|
--
|
|
-- Eine vorgemerkte Versetzung steht in pending_org_changes und noch nicht in
|
|
-- position_assignments. „Besetzt?" wird aber allein an position_assignments
|
|
-- gefragt — die Stelle ist zum Zeitpunkt der Buchung tatsächlich frei.
|
|
--
|
|
-- Auffallen würde es erst in der Nacht zum 20.10.: dann legt der Nachtlauf die
|
|
-- Besetzung der vorgemerkten Person an und läuft in
|
|
-- `position_assignments_one_holder`, den Teilindex, der genau eine laufende
|
|
-- Besetzung je Planstelle zulässt. Der Nachtlauf arbeitet in **einer**
|
|
-- Transaktion über alle fälligen Vorgänge; eine einzige solche Buchung bringt
|
|
-- ihn also vollständig zum Stehen — auch für alle anderen. Der Fehler entsteht
|
|
-- heute und schlägt Wochen später bei jemand anderem zu.
|
|
--
|
|
-- ═══ Was hier steht ═══════════════════════════════════════════════
|
|
--
|
|
-- Eine Auskunft, die pending_org_changes mitliest, und ihr Aufruf in den vier
|
|
-- Funktionen, die jemanden auf eine Planstelle setzen: hire_employee,
|
|
-- rehire_employee, transfer_employee, promote_employee.
|
|
--
|
|
-- Abgewiesen wird mit Datum — „ab dem 20.10.2026 vorgemerkt" sagt der Person
|
|
-- vor dem Bildschirm, was zu tun ist. Die eigene Vormerkung zählt nicht als
|
|
-- Hindernis: wer dieselbe Versetzung noch einmal erfasst, korrigiert sie.
|
|
|
|
create or replace function planstelle_vorgemerkt(p_position_id uuid, p_ab date, p_ausser uuid default null)
|
|
returns date language sql stable
|
|
set search_path = public, pg_temp
|
|
as $$
|
|
select min(o.effective_date)
|
|
from pending_org_changes o
|
|
where o.status = 'pending'
|
|
-- Nur die beiden Vorgänge, die eine Planstelle belegen. Eine
|
|
-- Vertragsänderung oder eine Karenz trägt keine Zielplanstelle.
|
|
and o.change_type in ('transfer', 'promotion')
|
|
and o.effective_date >= p_ab
|
|
and (p_ausser is null or o.employee_id <> p_ausser)
|
|
and nullif(o.payload->>'target_position_id', '')::uuid = p_position_id;
|
|
$$;
|
|
|
|
comment on function planstelle_vorgemerkt(uuid, date, uuid) is
|
|
'Ab wann eine Planstelle durch eine vorgemerkte Versetzung oder Beförderung belegt ist, oder null. Die Vormerkung der übergebenen Person zählt nicht mit.';
|
|
|
|
grant execute on function planstelle_vorgemerkt(uuid, date, uuid) to alpenwerk_app;
|
|
|
|
|
|
-- ═══ hire_employee ════════════════════════════════════════════════
|
|
do $migration$
|
|
declare
|
|
v_alt constant text := $anker$and (pa.valid_to is null or pa.valid_to > v_entry);$anker$;
|
|
v_neu constant text := $anker$and (pa.valid_to is null or pa.valid_to > v_entry);
|
|
|
|
declare
|
|
v_vorgemerkt date := planstelle_vorgemerkt(v_position_id, v_entry, null);
|
|
begin
|
|
if v_vorgemerkt is not null then
|
|
raise exception 'Für diese Planstelle ist ab dem % bereits eine Versetzung vorgemerkt.',
|
|
to_char(v_vorgemerkt, 'DD.MM.YYYY');
|
|
end if;
|
|
end;
|
|
$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 = 'hire_employee' and p.prokind = 'f';
|
|
if v_def is null then raise exception 'hire_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 hire_employee kommt % mal vor, erwartet einmal.', v_anzahl;
|
|
end if;
|
|
|
|
execute replace(v_def, v_alt, v_neu);
|
|
end
|
|
$migration$;
|
|
|
|
-- ═══ rehire_employee ══════════════════════════════════════════════
|
|
do $migration$
|
|
declare
|
|
v_alt constant text := $anker$and (pa.valid_to is null or pa.valid_to > v_date);$anker$;
|
|
v_neu constant text := $anker$and (pa.valid_to is null or pa.valid_to > v_date);
|
|
|
|
declare
|
|
v_vorgemerkt date := planstelle_vorgemerkt(v_position_id, v_date, v_employee_id);
|
|
begin
|
|
if v_vorgemerkt is not null then
|
|
raise exception 'Für diese Planstelle ist ab dem % bereits eine Versetzung vorgemerkt.',
|
|
to_char(v_vorgemerkt, 'DD.MM.YYYY');
|
|
end if;
|
|
end;
|
|
$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 = 'rehire_employee' and p.prokind = 'f';
|
|
if v_def is null then raise exception 'rehire_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 rehire_employee kommt % mal vor, erwartet einmal.', v_anzahl;
|
|
end if;
|
|
|
|
execute replace(v_def, v_alt, v_neu);
|
|
end
|
|
$migration$;
|
|
|
|
-- ═══ transfer_employee ════════════════════════════════════════════
|
|
--
|
|
-- Der Anker steht auf der zweiten Zeile der Belegungsabfrage. Die erste
|
|
-- Fassung dieser Migration zielte auf
|
|
--
|
|
-- where pa.position_id = v_target_position and pa.valid_to is null;
|
|
--
|
|
-- also auf den Wortlaut aus 20260727120200 — und fand nichts. Die Funktion ist
|
|
-- seither dynamisch gepatcht worden: die Abfrage ist zweizeilig und
|
|
-- berücksichtigt den Stichtag. Dieselbe Falle wie immer, nur diesmal nicht im
|
|
-- Rumpf einer Funktion, sondern im Anker auf sie.
|
|
do $migration$
|
|
declare
|
|
v_alt constant text := $anker$and (pa.valid_to is null or pa.valid_to > v_effective);$anker$;
|
|
v_neu constant text := $anker$and (pa.valid_to is null or pa.valid_to > v_effective);
|
|
|
|
declare
|
|
v_vorgemerkt date := planstelle_vorgemerkt(v_target_position, v_effective, v_employee_id);
|
|
begin
|
|
if v_vorgemerkt is not null then
|
|
raise exception 'Für diese Planstelle ist ab dem % bereits eine Versetzung vorgemerkt.',
|
|
to_char(v_vorgemerkt, 'DD.MM.YYYY');
|
|
end if;
|
|
end;
|
|
$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$;
|
|
|
|
-- ═══ promote_employee ═════════════════════════════════════════════
|
|
--
|
|
-- Ohne Zielplanstelle ist v_position null; die Auskunft liefert dann nichts,
|
|
-- und der Zweig läuft durch. Eine Beförderung auf derselben Stelle braucht
|
|
-- deshalb keine Sonderbehandlung.
|
|
do $migration$
|
|
declare
|
|
v_alt constant text := $anker$where pa.position_id = v_position and pa.valid_to is null;$anker$;
|
|
v_neu constant text := $anker$where pa.position_id = v_position and pa.valid_to is null;
|
|
|
|
declare
|
|
v_vorgemerkt date := planstelle_vorgemerkt(v_position, v_effective_date, v_employee_id);
|
|
begin
|
|
if v_vorgemerkt is not null then
|
|
raise exception 'Für diese Planstelle ist ab dem % bereits eine Versetzung vorgemerkt.',
|
|
to_char(v_vorgemerkt, 'DD.MM.YYYY');
|
|
end if;
|
|
end;
|
|
$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$;
|
|
|
|
|
|
-- ═══ Selbstprüfung ════════════════════════════════════════════════
|
|
do $$
|
|
declare
|
|
v_name text;
|
|
v_def text;
|
|
begin
|
|
foreach v_name in array array['hire_employee', 'rehire_employee', 'transfer_employee', 'promote_employee'] loop
|
|
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 = v_name and p.prokind = 'f';
|
|
|
|
if v_def not like '%planstelle_vorgemerkt(%' then
|
|
raise exception '% fragt die vorgemerkten Planstellen nicht ab.', v_name;
|
|
end if;
|
|
if v_def not like '%require_hr_admin()%' then
|
|
raise exception '% prueft die Rechte nicht.', v_name;
|
|
end if;
|
|
if v_def not like '%search_path%' then
|
|
raise exception '% hat keinen festen search_path.', v_name;
|
|
end if;
|
|
end loop;
|
|
|
|
-- Was an diesen Funktionen schon einmal verlorenging. Die Statusumwandlung
|
|
-- im Wiedereintritt ist zweimal verschwunden; sie wird hier mitgeprüft,
|
|
-- weil diese Migration die Funktion erneut erzeugt.
|
|
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 = 'rehire_employee' and p.prokind = 'f';
|
|
if v_def not like '%end)::employment_status%' then
|
|
raise exception 'rehire_employee wandelt den Status nicht um.';
|
|
end if;
|
|
if v_def not like '%cornerstone_id%' then
|
|
raise exception 'rehire_employee hat die Cornerstone-ID 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 = '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 mehr ab.';
|
|
end if;
|
|
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 = '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 mehr ab.';
|
|
end if;
|
|
|
|
raise notice 'Vorgemerkte Planstellen: alle vier Buchungswege pruefen sie.';
|
|
end
|
|
$$;
|