-- --------------------------------------------------------------------------- -- HANDGESCHRIEBEN. Prisma erzeugt hier zusaetzlich Loeschbefehle fuer die -- zusammengesetzten Fremdschluessel (Mandantengrenze), die Trigramm-Indizes -- (Archivsuche), die partiellen eindeutigen Indizes und alle Trigger: es kennt -- sie nicht, weil sie sich im Schema nicht ausdruecken lassen. Diese -- Loeschbefehle sind bewusst gestrichen. -- -- tests/datenbankregeln.test.ts prueft, dass alle diese Objekte vorhanden sind – -- ein versehentlich stehengelassener Loeschbefehl faellt dort sofort auf. -- --------------------------------------------------------------------------- -- Bedingte Abschnitte und Schritte (#039). ALTER TABLE "VorlagenAbschnitt" ADD COLUMN "bedingung" JSONB; ALTER TABLE "VorlagenSchritt" ADD COLUMN "bedingung" JSONB; /* * Die **Form** der Bedingung, in der Datenbank. * * Das vollstaendige Schema steht in `src/lib/bedingungen.ts` und wird beim * Schreiben angewandt. Hier steht, was auch dann gelten muss, wenn jemand an * der Service-Schicht vorbei schreibt (CLAUDE.md) — und das ist mehr als * Formsache: eine Bedingung mit unbekannter `art` wuerde beim Lesen als „keine * Bedingung“ gelten, der Abschnitt waere also sichtbar. Das ist die harmlose * Richtung, aber es waere **stillschweigend** falsch, und niemand koennte sagen, * warum ein Abschnitt, der eine Bedingung hat, immer erscheint. * * Geprueft wird deshalb: `art` ist einer der beiden Werte, und die Felder, die * diese Art braucht, sind da. Nicht geprueft wird der Inhalt von `zustaende` — * ein Array-Element einzeln zu pruefen ginge nur mit einer Unterabfrage, und * die gehoert nicht in einen CHECK. */ ALTER TABLE "VorlagenAbschnitt" ADD CONSTRAINT "VorlagenAbschnitt_bedingung_form" CHECK ( "bedingung" IS NULL OR ( "bedingung" ->> 'art' = 'variable' AND "bedingung" ->> 'schluessel' IS NOT NULL AND "bedingung" ->> 'operator' IN ('ist', 'istNicht', 'gesetzt', 'leer') AND ( "bedingung" ->> 'operator' IN ('gesetzt', 'leer') OR btrim(COALESCE("bedingung" ->> 'wert', '')) <> '' ) ) OR ( "bedingung" ->> 'art' = 'schritt' AND "bedingung" ->> 'schrittId' IS NOT NULL AND jsonb_typeof("bedingung" -> 'zustaende') = 'array' AND jsonb_array_length("bedingung" -> 'zustaende') > 0 ) ); ALTER TABLE "VorlagenSchritt" ADD CONSTRAINT "VorlagenSchritt_bedingung_form" CHECK ( "bedingung" IS NULL OR ( "bedingung" ->> 'art' = 'variable' AND "bedingung" ->> 'schluessel' IS NOT NULL AND "bedingung" ->> 'operator' IN ('ist', 'istNicht', 'gesetzt', 'leer') AND ( "bedingung" ->> 'operator' IN ('gesetzt', 'leer') OR btrim(COALESCE("bedingung" ->> 'wert', '')) <> '' ) ) OR ( "bedingung" ->> 'art' = 'schritt' AND "bedingung" ->> 'schrittId' IS NOT NULL AND jsonb_typeof("bedingung" -> 'zustaende') = 'array' AND jsonb_array_length("bedingung" -> 'zustaende') > 0 ) ); /* * **Eine Schritt-Bedingung zeigt auf einen Schritt derselben Version.** * * Das ist die eigentliche Regel, und ein CHECK kann sie nicht halten: sie * verlangt einen Blick in zwei andere Tabellen. Ein Fremdschluessel auf * `VorlagenSchritt` ginge auch nicht — der Wert steckt in einem Json, und * „derselbe Version“ steht dort nicht. * * Ohne diese Pruefung zeigt eine Bedingung nach dem Veroeffentlichen ins Leere: * „Bearbeiten“ erzeugt eine **tiefe Kopie** mit neuen Kennungen (CLAUDE.md), und * ein Verweis auf die alte Kennung waere dann nicht mehr entscheidbar. Die * Auswertung zeigt in diesem Fall an, der Abschnitt erscheint also immer — und * niemand koennte sagen, warum die Bedingung nicht wirkt. Genau die Art Fehler, * die eine gruene Suite nicht sieht. * * Geprueft wird beim Schreiben der **Bedingung**, nicht beim Loeschen des * Ziels: einen Schritt zu loeschen, auf den eine Bedingung zeigt, ist nur im * Entwurf moeglich, und dort faengt es der Dienst mit einer besseren Meldung. */ CREATE OR REPLACE FUNCTION laufzettel_bedingung_gleiche_version() RETURNS TRIGGER AS $$ DECLARE eigene_version TEXT; ziel_version TEXT; ziel_id TEXT; BEGIN IF NEW."bedingung" IS NULL OR NEW."bedingung" ->> 'art' <> 'schritt' THEN RETURN NEW; END IF; ziel_id := NEW."bedingung" ->> 'schrittId'; -- Die eigene Version: am Abschnitt steht sie direkt, am Schritt eine Ebene hoeher. IF TG_TABLE_NAME = 'VorlagenAbschnitt' THEN eigene_version := NEW."versionId"; ELSE SELECT "versionId" INTO eigene_version FROM "VorlagenAbschnitt" WHERE id = NEW."abschnittId"; END IF; SELECT a."versionId" INTO ziel_version FROM "VorlagenSchritt" s JOIN "VorlagenAbschnitt" a ON a.id = s."abschnittId" WHERE s.id = ziel_id; IF ziel_version IS NULL THEN RAISE EXCEPTION 'Die Bedingung zeigt auf den Schritt %, den es nicht gibt.', ziel_id USING ERRCODE = 'integrity_constraint_violation'; END IF; IF ziel_version <> eigene_version THEN RAISE EXCEPTION 'Die Bedingung zeigt auf einen Schritt einer anderen Version. Nach dem Veroeffentlichen waere sie nicht mehr entscheidbar.' USING ERRCODE = 'integrity_constraint_violation'; END IF; RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER "VorlagenAbschnitt_bedingung_gleiche_version" BEFORE INSERT OR UPDATE ON "VorlagenAbschnitt" FOR EACH ROW EXECUTE FUNCTION laufzettel_bedingung_gleiche_version(); CREATE TRIGGER "VorlagenSchritt_bedingung_gleiche_version" BEFORE INSERT OR UPDATE ON "VorlagenSchritt" FOR EACH ROW EXECUTE FUNCTION laufzettel_bedingung_gleiche_version(); /* * Gegenprobe: die neuen Spalten sind ueberall leer, und die CHECKs lassen die * bestehenden Zeilen zu. `ALTER TABLE ... ADD CONSTRAINT` prueft das von sich * aus — aber eine Migration, die ihre eigene Annahme nicht nachrechnet, ist * genau die Art Zusage, die dieses Projekt teuer bezahlt hat. */ DO $$ DECLARE mit_bedingung INTEGER; BEGIN SELECT count(*) INTO mit_bedingung FROM "VorlagenAbschnitt" WHERE "bedingung" IS NOT NULL; IF mit_bedingung > 0 THEN RAISE EXCEPTION 'Unerwartet % Abschnitte mit Bedingung vor der Migration.', mit_bedingung; END IF; SELECT count(*) INTO mit_bedingung FROM "VorlagenSchritt" WHERE "bedingung" IS NOT NULL; IF mit_bedingung > 0 THEN RAISE EXCEPTION 'Unerwartet % Schritte mit Bedingung vor der Migration.', mit_bedingung; END IF; END $$;