Files
alpenwerk-hr/db/migrations/20260928140000_hay_grade.sql
Andrei Laas 779beb4478 Hay-Grade statt Verwendungsgruppe A--F
A--F stammte aus der Spezifikation, nicht vom Kunden: sechs erfundene Stufen
mit erfundenen Beschreibungen. Der Kunde bewertet nach Hay und hat die Liste
geschickt -- 13 Stufen plus einen Generic Grade.

Dieselbe Spalte fuellt im Cornerstone-Extrakt die Grade ID. Solange dort A--F
steht, ist die Datei in jeder Zeile falsch, ohne dass es beim Erzeugen
auffaellt: das Zielsystem kennt diese Kennungen nicht.

Der Typ heisst weiter paygrade_type, ist aber jetzt eine Domain ueber text mit
CHECK statt eines Aufzaehlungstyps. Ein Enum laesst sich nicht umschreiben --
Werte entfernen geht gar nicht, und add value darf im selben Vorgang, der den
neuen Wert schreibt, nicht benutzt werden. Wichtiger: die rund zehn
SQL-Funktionen, die den Wert nach paygrade_type umwandeln, bleiben unveraendert
gueltig. Jede von ihnen neu zu erzeugen hiesse, zehnmal die Gelegenheit zu
haben, aus einer veralteten Vorlage zu kopieren.

Zwei Funktionen muessen doch angefasst werden, beide per Punktaenderung an der
laufenden Definition statt per Kopie aus einer Datei: der Vorgabewert B in
hire_employee und die Beschriftung im Protokoll von promote_employee.

Der Bestand bekommt den Generic Grade. Aus A--F liesse sich kein Hay-Grade
ableiten: andere Einteilung, andere Anzahl. Geraten saehe im Extrakt genauso
aus wie erhoben.
2026-09-28 11:13:41 +02:00

248 lines
10 KiB
SQL
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

-- Hay-Grade statt Verwendungsgruppe A–F
--
-- ═══ 1. Warum die Liste wechselt ═══════════════════════════════════
--
-- A–F stammte aus der Spezifikation, nicht vom Kunden: sechs erfundene
-- Stufen mit erfundenen Beschreibungen („B – Qualifiziert"). Der Kunde
-- bewertet nach Hay und hat die Liste geschickt — 13 Stufen plus einen
-- „Generic Grade" für alles, was keine trägt.
--
-- Die Spalte füllt im Cornerstone-Extrakt die „Grade ID". Solange dort A–F
-- steht, ist die Datei in jeder Zeile falsch, ohne dass es beim Erzeugen
-- auffällt: das Zielsystem kennt diese Kennungen nicht.
--
-- ═══ 2. Warum eine Domain und kein Aufzählungstyp ══════════════════
--
-- `paygrade_type` war ein Enum. Ein Enum lässt sich nicht umschreiben:
-- Werte entfernen geht gar nicht, und `alter type … add value` darf im
-- selben Vorgang, der den neuen Wert schreibt, nicht benutzt werden — die
-- Umstellung bräuchte also zwei Migrationen und liesse dazwischen eine
-- Datenbank mit beiden Listen zurück.
--
-- Der Typ **heisst** deshalb weiter `paygrade_type`, ist aber jetzt eine
-- Domain über text mit CHECK. Das ist die Form, die dieses Projekt für
-- kundenseitige Listen ohnehin schon führt (Abwesenheitsart, Personenkreis,
-- Mitarbeiterart) — und, wichtiger: die rund zehn SQL-Funktionen, die
-- `(payload->>'paygrade')::paygrade_type` schreiben, bleiben unverändert
-- gültig. Jede von ihnen neu zu erzeugen hiesse, zehnmal die Gelegenheit zu
-- haben, aus einer veralteten Vorlage zu kopieren; genau daran hat dieses
-- Projekt schon dreimal Verhalten verloren.
--
-- Eine einzige Stelle muss doch angefasst werden: `hire_employee` setzt als
-- Vorgabe das Literal 'B', und das besteht den CHECK nicht mehr. Sie wird
-- unten **nicht** aus einer Datei kopiert, sondern aus der laufenden
-- Definition gelesen und an genau dieser Stelle geändert.
-- ═══ 3. Hängt der Typ noch woanders? ══════════════════════════════
--
-- Die Dateien sagen: nur employees.paygrade. Die Datenbank ist die Quelle,
-- nicht die Dateien — also gefragt statt angenommen. Hinge eine zweite
-- Spalte daran, scheiterte der DROP weiter unten ohnehin, aber mit einer
-- Meldung, die nicht sagt, was los ist.
do $$
declare
v_spalten text;
begin
select string_agg(c.relname || '.' || a.attname, ', ')
into v_spalten
from pg_attribute a
join pg_class c on c.oid = a.attrelid
join pg_namespace n on n.oid = c.relnamespace
where a.atttypid = 'paygrade_type'::regtype
and a.attnum > 0
and not a.attisdropped
and n.nspname = 'public'
and not (c.relname = 'employees' and a.attname = 'paygrade');
if v_spalten is not null then
raise exception 'paygrade_type wird noch benutzt von: %. Umstellung abgebrochen.', v_spalten;
end if;
end
$$;
-- ═══ 4. Offene datierte Beförderungen ═════════════════════════════
--
-- Eine Beförderung mit Datum in der Zukunft liegt als pending-Zeile mit
-- `new_paygrade` im Nutzdatensatz. Steht dort A–F, scheiterte der Nachtlauf
-- Wochen später an der Umwandlung — um drei Uhr früh und ohne jemanden, dem
-- die Meldung zugestellt würde. Lieber jetzt und laut.
do $$
declare
v_anzahl int;
begin
select count(*) into v_anzahl
from pending_org_changes
where status = 'pending'
and payload ? 'new_paygrade'
and payload->>'new_paygrade' not in
('-','HG09','HG10','HG11','HG12','HG13','HG14','HG15','HG16','HG17','HG18','HG19','HG19P','HG20');
if v_anzahl > 0 then
raise exception
'Es warten % geplante Beförderungen auf eine Verwendungsgruppe, die es nicht mehr gibt. Erst entscheiden, dann umstellen.',
v_anzahl;
end if;
end
$$;
-- ═══ 5. Der Typwechsel ════════════════════════════════════════════
alter table employees alter column paygrade drop default;
alter table employees alter column paygrade type text using paygrade::text;
drop type paygrade_type;
create domain paygrade_type as text
constraint chk_hay_grade check (
value in ('-','HG09','HG10','HG11','HG12','HG13','HG14','HG15','HG16','HG17','HG18','HG19','HG19P','HG20')
);
-- Der Bestand bekommt „Generic Grade". Aus A–F liesse sich kein Hay-Grade
-- ableiten: andere Einteilung, andere Anzahl. Geraten sähe im Extrakt
-- genauso aus wie erhoben — und wäre nicht mehr davon zu unterscheiden.
update employees
set paygrade = '-'
where paygrade not in ('-','HG09','HG10','HG11','HG12','HG13','HG14','HG15','HG16','HG17','HG18','HG19','HG19P','HG20');
alter table employees alter column paygrade type paygrade_type using paygrade::paygrade_type;
alter table employees alter column paygrade set default '-';
-- ═══ 6. hire_employee: Vorgabewert 'B' → '-' ══════════════════════
--
-- Aus der laufenden Definition gelesen, nicht aus einer Migrationsdatei
-- kopiert: ein Teil der Funktionen dieses Projekts ist nachträglich
-- dynamisch gepatcht worden, die Summe der Dateien ist also nicht das, was
-- in der Datenbank steht. Der Anker muss genau einmal vorkommen — käme er
-- keinmal vor, hätte jemand die Vorgabe schon geändert und wir überschrieben
-- etwas Unbekanntes; käme er zweimal vor, träfe die Ersetzung eine Stelle,
-- die hier niemand angeschaut hat.
do $migration$
declare
v_alt constant text := $anker$coalesce((payload->>'paygrade')::paygrade_type, 'B')$anker$;
v_neu constant text := $anker$coalesce((payload->>'paygrade')::paygrade_type, '-')$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 Vorgabewert kommt % mal vor, erwartet genau einmal.', v_anzahl;
end if;
execute replace(v_def, v_alt, v_neu);
end
$migration$;
-- ═══ 6b. promote_employee: die Beschriftung im Protokoll ══════════
--
-- Die Funktion schreibt den Grund einer Beförderung als Klartext nach
-- audit_log und employee_history: „…, neue Verwendungsgruppe: C". Diese
-- Zeilen liest der Kunde. Bliebe das Wort stehen, hiesse ein Feld in der
-- Maske anders als im Protokoll darüber — dieselbe Änderung unter zwei
-- Namen. Bestehende Einträge bleiben, wie sie sind: Historie wird
-- fortgeschrieben, nicht umgeschrieben.
do $migration$
declare
v_alt constant text := $anker$, neue Verwendungsgruppe: $anker$;
v_neu constant text := $anker$, neuer Hay-Grade: $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 'Die Beschriftung kommt % mal vor, erwartet genau einmal.', v_anzahl;
end if;
execute replace(v_def, v_alt, v_neu);
end
$migration$;
-- ═══ 7. Selbstprüfung ═════════════════════════════════════════════
--
-- Geprüft wird nicht nur das Neue, sondern auch das, was bei früheren
-- Neuerzeugungen schon einmal still verschwunden ist: Rechteprüfung,
-- search_path und die zuletzt hinzugekommenen Felder. `hire_employee` ist
-- gerade neu erzeugt worden — wenn dabei etwas abhanden gekommen wäre, wäre
-- es hier zu sehen und nirgends sonst.
do $$
declare
v_def text;
v_werte int;
begin
select count(*) into v_werte
from pg_constraint
where conname = 'chk_hay_grade'
and contypid = 'paygrade_type'::regtype;
if v_werte <> 1 then
raise exception 'Die Bedingung chk_hay_grade fehlt an paygrade_type.';
end if;
if exists (select 1 from employees where paygrade::text = 'B') then
raise exception 'Es stehen noch Verwendungsgruppen in employees.paygrade.';
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 = 'hire_employee' and p.prokind = 'f';
if v_def not like '%paygrade_type, ''-''%' then
raise exception 'hire_employee traegt den neuen Vorgabewert nicht.';
end if;
if v_def like '%paygrade_type, ''B''%' then
raise exception 'hire_employee traegt noch den alten Vorgabewert.';
end if;
if v_def not like '%require_hr_admin()%' then
raise exception 'hire_employee hat die Rechtepruefung verloren.';
end if;
if v_def not like '%search_path%' then
raise exception 'hire_employee hat den search_path verloren.';
end if;
if v_def not like '%cornerstone_id%' then
raise exception 'hire_employee hat die Cornerstone-ID verloren.';
end if;
if v_def not like '%company_email%' then
raise exception 'hire_employee hat die Firmen-E-Mail 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 '%neuer Hay-Grade%' then
raise exception 'promote_employee traegt die neue Beschriftung nicht.';
end if;
if v_def not like '%require_hr_admin()%' then
raise exception 'promote_employee hat die Rechtepruefung verloren.';
end if;
-- Die Zielplanstelle kam erst mit 20260924100000 dazu und ist genau die Art
-- Verhalten, die bei einer Neuerzeugung schon einmal still verschwunden ist.
if v_def not like '%target_position_id%' then
raise exception 'promote_employee hat die Zielplanstelle verloren.';
end if;
raise notice 'Hay-Grade steht, hire_employee und promote_employee vollstaendig.';
end
$$;