-- ============================================================================= -- Phase 8 — Mandantentrennung, Prüfbedingungen und der Vektorindex -- -- Drei Dinge, die nicht im Anwendungscode stehen dürfen: -- -- 1. Row Level Security für die sechs neuen Tabellen. Wie überall: ohne -- gesetztes app.current_organisation liefert die Anwendungsrolle nichts. -- 2. Prüfbedingungen für das, was fachlich unmöglich ist — ein negativer -- Betrag auf einer Quittung, eine Entscheidung ohne Entscheider. -- 3. Der Vektorindex für die semantische Suche. Ohne ihn wäre jede Frage ans -- Wiki ein vollständiger Tabellendurchlauf über alle Abschnitte. -- ============================================================================= -- ----------------------------------------------------------------------------- -- Row Level Security -- ----------------------------------------------------------------------------- DO $$ DECLARE tabelle text; BEGIN FOREACH tabelle IN ARRAY ARRAY[ 'ki_vorgang', 'wikiseite', 'wikiabschnitt', 'wikiversion', 'diktat', 'belegentwurf' ] LOOP EXECUTE format('ALTER TABLE %I ENABLE ROW LEVEL SECURITY', tabelle); EXECUTE format('ALTER TABLE %I FORCE ROW LEVEL SECURITY', tabelle); EXECUTE format('DROP POLICY IF EXISTS %I ON %I', tabelle || '_mandant', tabelle); EXECUTE format( 'CREATE POLICY %I ON %I FOR ALL ' 'USING ("organisationId" = aktuelle_organisation()) ' 'WITH CHECK ("organisationId" = aktuelle_organisation())', tabelle || '_mandant', tabelle ); END LOOP; END $$; -- ----------------------------------------------------------------------------- -- Prüfbedingungen -- ----------------------------------------------------------------------------- -- Eine Entscheidung braucht einen Zeitpunkt und umgekehrt. Ein Vorgang, der -- als übernommen gilt, ohne dass jemand ihn übernommen hat, wäre genau die -- Lücke, die diese Tabelle schließen soll. ALTER TABLE "ki_vorgang" ADD CONSTRAINT "ki_vorgang_entscheidung_vollstaendig" CHECK ( ("stand" IN ('ENTWURF', 'FEHLGESCHLAGEN') AND "entschiedenAm" IS NULL) OR ("stand" IN ('UEBERNOMMEN', 'VERWORFEN') AND "entschiedenAm" IS NOT NULL) ); -- Ein fehlgeschlagener Vorgang hat einen Grund. Sonst steht dort "ging nicht" -- und niemand weiß, woran es lag. ALTER TABLE "ki_vorgang" ADD CONSTRAINT "ki_vorgang_fehler_begruendet" CHECK ( "stand" <> 'FEHLGESCHLAGEN' OR "fehler" IS NOT NULL ); ALTER TABLE "ki_vorgang" ADD CONSTRAINT "ki_vorgang_dauer_plausibel" CHECK ( "dauerMs" IS NULL OR "dauerMs" >= 0 ); -- Eine Quittung über einen negativen Betrag gibt es nicht. Eine Gutschrift ist -- ein eigener Vorgang und kommt mit der Kasse in Phase 9. ALTER TABLE "belegentwurf" ADD CONSTRAINT "belegentwurf_betrag_positiv" CHECK ( "betrag" IS NULL OR "betrag" >= 0 ); ALTER TABLE "belegentwurf" ADD CONSTRAINT "belegentwurf_steuersatz_plausibel" CHECK ( "steuersatz" IS NULL OR ("steuersatz" >= 0 AND "steuersatz" <= 100) ); ALTER TABLE "diktat" ADD CONSTRAINT "diktat_dauer_plausibel" CHECK ( "dauerSekunden" IS NULL OR "dauerSekunden" >= 0 ); ALTER TABLE "wikiversion" ADD CONSTRAINT "wikiversion_nummer_positiv" CHECK ("nummer" > 0); -- ----------------------------------------------------------------------------- -- Suche -- ----------------------------------------------------------------------------- -- Der Vektorindex. -- -- HNSW und nicht IVFFlat: IVFFlat muss auf einem gefüllten Bestand trainiert -- werden und liefert auf einer frisch angelegten Tabelle schlechte Treffer. -- HNSW ist ab der ersten Zeile brauchbar — und ein Wiki wächst Seite für -- Seite, es gibt nie den Moment, an dem man den Index einmal aufbaut. -- -- Kosinusabstand, weil die Einbettungen normalisiert sind und es dann dasselbe -- misst wie das Skalarprodukt, aber unabhängig von der Länge des Vektors. CREATE INDEX IF NOT EXISTS "wikiabschnitt_einbettung_idx" ON "wikiabschnitt" USING hnsw ("einbettung" vector_cosine_ops); -- Volltext daneben. Die semantische Suche findet, was gemeint ist; die -- Wortsuche findet, was dasteht. Wer "FwDV 7" eingibt, will die Seite mit -- genau dieser Zeichenfolge — kein Modell der Welt ist da besser als LIKE. CREATE INDEX IF NOT EXISTS "wikiseite_suche_titel" ON "wikiseite" USING gin ("titel" gin_trgm_ops); CREATE INDEX IF NOT EXISTS "wikiseite_suche_inhalt" ON "wikiseite" USING gin ("inhalt" gin_trgm_ops); -- Nur die veröffentlichten Seiten werden durchsucht und beantwortet. CREATE INDEX IF NOT EXISTS "wikiseite_veroeffentlicht" ON "wikiseite" ("organisationId", "art") WHERE "veroeffentlicht" = true AND "geloeschtAm" IS NULL; -- Offene Vorschläge — die Liste, die jemand abarbeitet. CREATE INDEX IF NOT EXISTS "ki_vorgang_offen" ON "ki_vorgang" ("organisationId", "erstelltAm" DESC) WHERE "stand" = 'ENTWURF'; -- ----------------------------------------------------------------------------- -- Rechte -- ----------------------------------------------------------------------------- DO $$ BEGIN IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'florian_app') THEN EXECUTE 'GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO florian_app'; EXECUTE 'GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO florian_app'; EXECUTE 'REVOKE INSERT, UPDATE, DELETE ON "berechtigung" FROM florian_app'; EXECUTE 'REVOKE UPDATE, DELETE ON "audit_log" FROM florian_app'; EXECUTE 'REVOKE UPDATE, DELETE ON "kiosk_vorgang" FROM florian_app'; EXECUTE 'REVOKE UPDATE, DELETE ON "druckmessung" FROM florian_app'; EXECUTE 'REVOKE UPDATE, DELETE ON "atemschutz_ereignis" FROM florian_app'; EXECUTE 'REVOKE DELETE ON "benachrichtigung" FROM florian_app'; -- Ein KI-Vorgang lässt sich nicht löschen und nicht umschreiben, was -- die Sache selbst betrifft. Geändert werden darf nur die -- Entscheidung — deshalb kein REVOKE UPDATE, sondern der Trigger -- darunter, der die Ausgabe festhält. EXECUTE 'REVOKE DELETE ON "ki_vorgang" FROM florian_app'; -- Eine einmal weggeschriebene Fassung einer Wiki-Seite bleibt. Eine -- Dienstvorschrift, deren Vorgeschichte sich bereinigen lässt, ist -- als Nachweis wertlos. EXECUTE 'REVOKE UPDATE, DELETE ON "wikiversion" FROM florian_app'; END IF; END $$; -- ----------------------------------------------------------------------------- -- Was ein Modell erzeugt hat, bleibt, wie es war -- ----------------------------------------------------------------------------- -- Der Trigger schützt genau das, worauf es ankommt: die Ausgabe des Modells -- und die Angabe, welches Modell sie erzeugt hat. Wer einen Entwurf -- nachbessern will, tut das im Zieldokument — nicht im Vorgang, der belegen -- soll, was die Maschine wirklich geliefert hat. -- -- Die Entscheidung (stand, entschiedenVonId, entschiedenAm) darf sich ändern, -- aber nur einmal: von ENTWURF weg, nicht wieder zurück. CREATE OR REPLACE FUNCTION ki_vorgang_unveraenderlich() RETURNS TRIGGER AS $$ BEGIN IF NEW."ausgabe" IS DISTINCT FROM OLD."ausgabe" OR NEW."eingabe" IS DISTINCT FROM OLD."eingabe" OR NEW."modell" IS DISTINCT FROM OLD."modell" OR NEW."art" IS DISTINCT FROM OLD."art" THEN RAISE EXCEPTION 'Ein KI-Vorgang haelt fest, was das Modell geliefert hat. Eingabe, ' 'Ausgabe, Modell und Art lassen sich nicht nachtraeglich aendern.'; END IF; IF OLD."stand" <> 'ENTWURF' AND NEW."stand" <> OLD."stand" THEN RAISE EXCEPTION 'Ueber diesen Vorschlag wurde bereits entschieden (%). Eine zweite ' 'Entscheidung waere nicht nachvollziehbar.', OLD."stand"; END IF; RETURN NEW; END; $$ LANGUAGE plpgsql; DROP TRIGGER IF EXISTS ki_vorgang_unveraenderlich_trigger ON "ki_vorgang"; CREATE TRIGGER ki_vorgang_unveraenderlich_trigger BEFORE UPDATE ON "ki_vorgang" FOR EACH ROW EXECUTE FUNCTION ki_vorgang_unveraenderlich();