Stop offering positions that are already spoken for, and let hiring work again
Two reports, four defects, all of them in the way of ordinary use.
A position with a signed starter is not vacant. loadOpenPositions asked "is
anyone on it today?", so three positions whose new holders begin in
September and October were listed as open, labelled "vacant for 2 days".
That same list feeds the hire wizard, so it invited filling a position a
second time — discovered at the partial unique index, after the second
interview. Vacancy now means no assignment that still stands, including one
that has not started. An assignment that ended still frees the position.
Hiring was broken three times over, each fault hidden behind the previous
one:
1. hire_employee cast to ::weekday[], a type that no longer exists — it
was replaced by text plus a CHECK constraint and the function was never
updated. apply_due_pending_changes had the same problem with
::relationship_type, which would have broken the nightly run.
PL/pgSQL resolves types in embedded statements at execution time, so
both functions were created without complaint and failed only in use.
2. Fourteen functions called auth.uid(). The application connects as a
role with no rights on the auth schema, so every write — hire,
transfer, promote, exit, notes, positions — failed with "permission
denied for schema auth". They now use app_current_user_id(), which is
where #23 was heading anyway. Its own fallback also caught only
"function missing" and now catches the privilege error too, so a call
without session context returns null instead of raising.
3. The audit line built a name as `payload->>'a' || ' ' || payload->>'b'`.
`||` binds tighter than `->>`, so Postgres reads
`payload ->> ('a' || ' ' || payload) ->> 'b'`. The ACL failure above
had aborted analysis before the parser ever reached it.
And the wizard collected an email, showed it in the summary, and dropped it:
the server action's signature had no such field. employees.email is NOT
NULL, so every hire that got past the three faults above would have failed
there. It is now passed through and required in step one, rather than
refused by the database at the end of step four.
Verified against the live database, each rolled back: a hire now creates the
employee, the assignment, the history entry and an audit line reading "Probe
Einstellung"; open positions drop from 13 to 10, and the three that
disappear are exactly the ones with a starter.
Migrations rewrite the affected functions in place rather than restating
them — retyping 165 lines of working PL/pgSQL to change two words is the
larger risk. Each one asserts the result afterwards.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
-- Klammern in hire_employee.
|
||||
--
|
||||
-- Der Protokolleintrag baute den Namen so:
|
||||
--
|
||||
-- payload->>'first_name' || ' ' || payload->>'last_name'
|
||||
--
|
||||
-- In PostgreSQL bindet `||` **stärker** als `->>`. Gelesen wird also
|
||||
--
|
||||
-- payload ->> ('first_name' || ' ' || payload) ->> 'last_name'
|
||||
--
|
||||
-- und das endet in „operator does not exist: text ->> unknown". Jede
|
||||
-- Neueinstellung scheiterte daran.
|
||||
--
|
||||
-- Warum es niemandem auffiel: davor stand in derselben Anweisung ein Aufruf
|
||||
-- von auth.uid(). Die Rechteprüfung auf das Schema `auth` schlug schon
|
||||
-- während der Analyse fehl, und der Parser kam nie bis zu diesem Ausdruck.
|
||||
-- Ein Fehler hat den anderen verdeckt — beide mussten weg, damit eine
|
||||
-- Einstellung durchläuft.
|
||||
|
||||
do $$
|
||||
declare
|
||||
v_def text;
|
||||
v_alt constant text := 'payload->>''first_name'' || '' '' || payload->>''last_name''';
|
||||
v_neu constant text := '(payload->>''first_name'') || '' '' || (payload->>''last_name'')';
|
||||
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'
|
||||
limit 1;
|
||||
|
||||
if v_def is null then
|
||||
raise exception 'hire_employee() nicht gefunden.';
|
||||
end if;
|
||||
|
||||
if position(v_alt in v_def) = 0 then
|
||||
raise notice 'hire_employee(): bereits geklammert.';
|
||||
else
|
||||
execute replace(v_def, v_alt, v_neu);
|
||||
raise notice 'hire_employee(): geklammert.';
|
||||
end if;
|
||||
end;
|
||||
$$;
|
||||
|
||||
-- ═══ Gegenprobe ══════════════════════════════════════════════════
|
||||
-- Der Ausdruck selbst, in beiden Lesarten. Ohne Klammern wirft er; mit
|
||||
-- Klammern kommt der Name heraus. Das hält die Regel fest, damit sie beim
|
||||
-- nächsten Mal nicht neu entdeckt werden muss.
|
||||
do $$
|
||||
declare
|
||||
v_payload jsonb := '{"first_name": "Anna", "last_name": "Berger"}'::jsonb;
|
||||
v_name text;
|
||||
begin
|
||||
v_name := (v_payload->>'first_name') || ' ' || (v_payload->>'last_name');
|
||||
if v_name <> 'Anna Berger' then
|
||||
raise exception 'Geklammert ergibt „%" statt „Anna Berger".', v_name;
|
||||
end if;
|
||||
|
||||
begin
|
||||
execute $probe$ select ('{"a":"x"}'::jsonb)->>'a' || ' ' || ('{"a":"x"}'::jsonb)->>'a' $probe$;
|
||||
raise exception 'Ungeklammert wirft nicht mehr — die Vorrangregel hat sich geändert, die Prüfung ist wertlos geworden.';
|
||||
exception
|
||||
when undefined_function then null; -- erwartet: text ->> unknown
|
||||
end;
|
||||
end;
|
||||
$$;
|
||||
Reference in New Issue
Block a user