-- 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 $$;