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.
248 lines
10 KiB
SQL
248 lines
10 KiB
SQL
-- 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
|
||
$$;
|