major
#29603
Physisches Löschen gelöschter Objekte aus versionierten Tabellen (Purge): Abschlussmenge über Pflichtreferenzen und Inhalte, optionale Verweise werden geleert
Die Persistenzschicht versioniert standardmäßig: Das Löschen eines Objekts schließt nur dessen aktuelle Zeile (REV_MAX wird auf die Commit-Revision minus eins gesetzt, siehe VersionedDBAccess.outdate()), löscht aber nie eine Zeile physisch. Alle Versionen eines gelöschten Objekts (Objekttabelle und FLEX_DATA) bleiben dauerhaft in der Datenbank. Neben dem globalen Kompaktieren der Historie (#27490) wird eine gezielte Operation benötigt, die eine Menge logisch gelöschter Objekte unabhängig von ihrem Alter vollständig aus der Datenbank entfernt (Anwendungsfall: Löschpflicht für Personen und ihre Daten).
Verbesserung
Operation purge(S) für eine Menge S` von Objektidentitäten `(Branch, Typ, ID). Das Ergebnis ist der Datenbestand, der entstanden wäre, wenn die Objekte nie existiert hätten: In jedem Branch und zu jeder Revision sind dieselben Zeilen sichtbar wie zuvor, abzüglich der Zeilen der gelöschten Objekte; jeder Verweis auf eines dieser Objekte liest sich als leerer Verweis.
Abschlussmenge
Eine Referenz ist entweder verpflichtend (mandatory, insbesondere source und dest von Assoziationstabellen) oder optional. Eine Link-Zeile ist keine eigenständige Information, sondern die Speicherform einer Referenz des Modells (so wie FLEX_DATA die Speicherform von Attributwerten ist); ein Link ohne Ende ist sinnlos. Daraus ergibt sich die Abschlussmenge H(S) als kleinste Obermenge von S, die abgeschlossen ist unter
- Eingehenden verpflichtenden Referenzen: Jedes Objekt, das in irgendeiner seiner Versionen eine verpflichtende Referenz auf ein Objekt aus H` hält, gehört zu `H. Damit gehören alle Links, die ein Objekt aus H` berühren, mit allen Versionen zu `H, transitiv.
- Ausgehenden Containment-Referenzen: Jedes Objekt, das aus einer Zeile von H` über eine Container-Referenz (`is-container) erreicht wird und in derselben Revision gelöscht wurde, in der diese Zeile endet, gehört zu H. Das sind genau die Inhalte, die die Löschkaskade zusammen mit ihrem Container gelöscht hat; anderweitig gelöschte oder fortbestehende Inhalte bleiben unberührt.
Die Operation löscht alle Zeilen aller Objekte aus H` in allen Versionen aus ihren Objekttabellen und aus `FLEX_DATA. Jede verbleibende Zeile (aktuell oder historisch, eines fortbestehenden oder eines gelöschten Objekts), deren optionale Referenz auf ein Objekt aus H` zeigt, erhält in den Referenzspalten die Null-Darstellung (wie sie #27490 für ins Leere zeigende Pins schreibt; der Resolver liefert dafür `null). Referenzen mit history-type="current", "historic" und "mixed" werden gleich behandelt. REVISION, REVISION_XREF und BRANCH bleiben unverändert (leere Revisionen: #27489).
Vorbedingung
Jedes Objekt aus H` ist gelöscht, d.h. hat keine Zeile mit `REV_MAX = Long.MAX_VALUE. Die Operation verändert nie den aktuellen Datenstand; das ist Sache einer Transaktion. Zieht die Abschlussmenge ein lebendes Objekt hinzu (etwa ein fortbestehendes Objekt, dessen alte Version eine verpflichtende Referenz auf ein Objekt aus S` hält), bricht die Operation ab, ohne etwas zu ändern; der Bericht nennt die lebenden Objekte und die Referenz, über die sie in `H gelangt sind. Die Anwendung löscht sie in einer gewöhnlichen Transaktion und wiederholt die Operation. Optionale Verweise lebender Objekte, auch ein stabilisierter Verweis, der zum Wiederfinden des gelöschten Objekts dient, blockieren nicht; sie werden geleert. Ein Objekt aus H ohne jede Zeile ist bereits entfernt und blockiert nicht.
Ein Trockenlauf (analyze) berechnet H, prüft die Vorbedingung und zählt die zu löschenden Zeilen und die zu leerenden Referenzen je Tabelle und Spalte, ohne etwas zu ändern; seine Zahlen sind die des echten Laufs auf demselben Datenstand.
Verweisprüfung
Geprüft werden alle Referenzspalten aller Item-Tabellen (Attribut-Referenzen und die source/`dest`-Spalten von Assoziationstabellen). Eine Referenz besteht aus bis zu vier Spalten (_ID, _REV, _BRC, _TYPE, siehe AbstractMOReference). Fehlende Spalten werden aus dem Kontext aufgelöst: Ohne _BRC-Spalte ist der Ziel-Branch der BRANCH der verweisenden Zeile; ohne _TYPE-Spalte ist der Zieltyp der konfigurierte monomorphe Zieltyp. Eine branch-lokale Referenz mit _BRC-Spalte speichert für einen aktuellen Wert den Marker DUMMY_BRANCH_VALUE statt eines Branches; auch dann gilt der BRANCH der Zeile. Der Branch eines Verweises ist ein Sicht-Branch; die Zeilen eines Objekts liegen in seinem Daten-Branch (BRANCH_SWITCH). Ein Verweis trifft eine Identität, wenn ID und Typ übereinstimmen und der Daten-Branch des Zieltyps zum Sicht-Branch des Verweises der Daten-Branch der Identität ist.
FLEX_DATA speichert für referenzartige Werte nur eine TLID ohne Typ und Branch; das ist kein auflösbarer Verweis und wird nicht umgeschrieben.
Ausführung
Wie bei #27490: aktives Wartungsfenster, im Cluster nur ein aktiver Knoten, anschließend Neustart des Moduls KnowledgeBaseFactory (aktuelle Zeilen können umgeschrieben werden, historische Items sind gecacht). Bereitstellung als Java-API und als TL-Script-Funktionen; die Administrationsaktion ist der Aufruf der TL-Script-Funktion aus der Skript-Konsole des Administrationsbereichs.
Lösung
Gemeinsame Infrastruktur mit #27490
ItemTables.Table beschreibt alle Referenzen einer Tabelle (ItemTables.Reference: Spalten je ReferencePart, Verpflichtung, Containment, History-Typ, monomorpher Zieltyp) und liefert die SQL-Ausdrücke für den Branch einer Zeile und den Sicht-Branch eines Referenzwerts; die gepinnten Referenzen sind daraus abgeleitet. NullReference hält die Null-Darstellung einer Referenz (Spalten und Werte) an einer Stelle; HistoryCompaction schreibt damit ihre geleerten Pins.
Korrektur der Kompaktierung (#27490): Ein ins Leere zeigender Pin einer verpflichtenden Referenz kann nicht geleert werden; die Zeile wird stattdessen gelöscht (auch wenn sie den aktuellen Stand hält) und im Bericht gesondert gezählt (getRowsDeletedForDanglingPins). Die kanonischen Enden source/`dest` einer Assoziation sind in TL 8 stets current (ein Override mit anderem History-Typ wird abgewiesen) und können daher nie ins Leere pinnen; die Regel greift bei jeder anderen verpflichtenden gepinnten Referenz, etwa einer Link-Tabelle mit eigener Pflichtreferenz.
SQL-Kern `DeletedObjectPurge`
Klasse in com.top_logic.knowledge.service.db2 nach dem Vorbild von HistoryCompaction: ConnectionPool, Typ-Repository, SQL-Dialekt; alle Statements über SQLFactory; Ausführung auf einer Schreibverbindung ohne Knowledge-Base-Transaktion. analyze(seeds, log) und purge(seeds, log) laufen über dieselbe Implementierung mit Trockenlauf-Schalter:
- Abschlussmenge: Arbeitsliste über Identitäten bis zum Fixpunkt; je Item-Tabelle und verpflichtender Referenz SELECT BRANCH, IDENTIFIER, R_ID ... WHERE R_ID IN (...) [AND R_TYPE = ?] [AND <Sicht-Branch> IN (...)] (Mengen in Blöcken bis getMaxSetSize(), gruppiert nach Zieltyp und Daten-Branch); je Container-Referenz die Ziele der Zeilen eines Mitglieds mit deren REV_MAX, aufgenommen, wenn die letzte Revision des Ziels gleich ist und nicht CURRENT_REV. Die Sicht-Branches je Typ und Daten-Branch werden einmal aus BRANCH und BRANCH_SWITCH gelesen.
- Vorbedingung: je Identität keine Zeile mit REV_MAX = CURRENT_REV; ein blockierter Lauf ändert nichts und liefert die Blocker mit Herkunft.
- Lauf: Löschen aller Zeilen der Abschlussmenge je Tabelle und FLEX_DATA in Blöcken von Objekten (setDeleteChunkSize, Vorgabe 1000) mit Commit je Block (#28053); danach UPDATE ... SET <NullReference> WHERE <Referenz trifft Identität> je Tabelle und optionaler Referenz mit demselben Prädikat, das der Trockenlauf zählt. Jeder Schritt ist idempotent; ein zweiter Lauf ist leer.
- Report: Abschlussmenge mit Origin (Startobjekt, verpflichtende Referenz, Inhalt; jeweils mit Referenz und Verursacher), gelöschte Zeilen je Tabelle, geleerte Werte je Tabelle und Referenz, Blocker, isBlocked(), isEmpty(), isDryRun().
Ausführung und TL-Script
PersistencyMaintenance (com.top_logic.knowledge.service.maintenance) bündelt, was jede die Historie umschreibende Wartungsoperation braucht: Vorbedingungsprüfung (Wartungsfenster, einziger aktiver Cluster-Knoten), Zugriff auf die Standard-Knowledge-Base und den verzögerten Neustart der Persistenzschicht; HistoryCompactionOperation delegiert daran, die zugehörigen Meldungen sind operationsneutral formuliert. DeletedObjectPurgeOperation (com.top_logic.knowledge.service.purge) ist die statische Fassade des Purge: analyze ohne Vorbedingungen, purge mit Vorbedingungen, purgeAndRestart mit Neustart nur, wenn tatsächlich etwas entfernt wurde.
TL-Script (PurgeFunctions, @ScriptPrefix("purge"), TLScriptFunctions-Mechanismus ohne Registrierung): purgeAnalyze(objects) und purgeDeleted(objects). Argument ist ein Objekt oder eine Liste von Objekten: Fachobjekte beliebiger Revision, Knowledge-Items, ObjectKey`s oder deren Textform, wie `objectKey(obj) sie liefert. Ergebnis ist eine Struktur mit dryRun, blocked, empty, hull und blockers (Einträge mit key in derselben Textform, origin = SEED / REFERENCE / CONTENT, reference und cause als key des Verursachers), erasedRows (Tabelle → Anzahl) und clearedReferences (Tabelle → Referenz → Anzahl). Die Schlüssel eines Berichts sind wieder gültige Argumente: purgeDeleted($report['hull'].map(m -> $m['key'])) entfernt, was die Analyse gezählt hat. purgeDeleted verlangt die Vorbedingungen, verändert bei Blockade nichts und stößt sonst den Neustart an, der alle Sitzungen beendet. Die Funktionen sind aus der Skript-Konsole des Administrationsbereichs aufrufbar; eine eigene Wartungsseite gibt es nicht.
Skriptfunktionen nur für Administratoren
Beide Purge-Funktionen dürfen in einem interaktiv eingegebenen Skript (Skript-Konsole, Suche) nur von einem Administrator aufgerufen werden: Die Analyse verrät zu einem gelöschten Objekt, was mit ihm zusammenhängt, und für gelöschte Objekte gibt es keine Leserechte, über die sich das filtern ließe. Dafür gibt es jetzt einen allgemeinen Mechanismus: Die Annotation @AdminOnly an einer Methode einer TLScriptFunctions-Klasse oder an der Klasse selbst (dann für alle ihre Funktionen) lässt TLScriptMethod bei jedem Aufruf EvalContext.checkAdmin(name) prüfen: In einem interaktiven Kontext (EvalContext.isInteractive()) scheitert der Aufruf eines Benutzers, der kein Administrator ist, mit PERMISSION_DENIED__NAME; ein Skript aus der Anwendungskonfiguration wird nicht interaktiv ausgewertet und darf die Funktion für jeden Benutzer aufrufen. Eine so markierte Funktion wird nie zur Übersetzungszeit ausgewertet. Die bisher einzige Stelle mit dieser Regel, appConfig (GetAppConfig), verwendet dieselbe Prüfung.
Objektschlüssel in TL-Script
objectId(obj) (#29530, ObjectFunctions) liefert nur den tabellenlokalen Bezeichner für URL-Segmente; zum Wiederfinden braucht $id.objectResolve(typeOrTable) (#29639) den Typ oder die Tabelle, und ein gelöschtes Objekt lässt sich damit nicht benennen. Neu in ObjectFunctions: objectKey(obj) liefert den vollständigen Schlüssel in der Textform des ObjectKey (Tabelle:ID, dazu #Branch außerhalb des Trunks und @Revision für eine historische Sicht) – auch für ein historisches oder ein über die Historie gelesenes gelöschtes Objekt, null für ein transientes. objectResolveKey(text) findet das Objekt ohne weitere Angaben wieder, bei angegebener Revision das historische; für einen unbekannten Schlüssel, ein aktuell gelöschtes Objekt oder einen Text, der kein Schlüssel ist, null. Wie objectResolve findet objectResolveKey nur ein Objekt, das der aktuelle Benutzer lesen darf (ModelAccessRights.isReadAllowed, über einen @UsesSecurity-Parameter nach #29639); für jedes andere liefert es null, sodass das Ergebnis nicht verrät, ob ein nicht lesbares Objekt existiert. Ohne Zugriffsprüfung ausgewertet (UpdateSecurityVisitor, QueryExecutor.disableSecurity()) findet es jedes Objekt. Der Purge-Bericht verwendet dieselbe Textform; die Purge-Funktionen lesen den Schlüssel direkt und unterliegen dieser Prüfung nicht.
Tests
TestDeletedObjectPurge (AbstractDBKnowledgeBaseTest, mit Branches, eigener Testtyp PurgeOwner mit verpflichtender polymorpher branch-globaler Referenz): Abschlussmenge (Links transitiv, verpflichtender Referrer, gleichzeitig gelöschte Inhalte, nicht aber früher entfernte), Blocker mit Referenz (lebender Referrer mit historischer Pflichtreferenz, lebendes Startobjekt), Zeilen aller Revisionen und FLEX_DATA, Referenz aus einem Branch auf ein Trunk-Objekt, Analyse ohne Seiteneffekt; Lauf: alle Zeilen der Abschlussmenge weg, optionale Referenzen in historischen und Branch-Zeilen geleert und null lesbar, aktueller Stand und Historie unbeteiligter Objekte unverändert, Trockenlauf gleich echtem Lauf, zweiter Lauf leer, blockierter Lauf ändert nichts. TestHistoryCompaction: ins Leere zeigender Pin einer Pflichtreferenz löscht die Zeile, gesondert gezählt, Trockenlauf gleich. TestPurgeFunctions: Analyse gelöschter, lebender, historischer und mehrerer Objekte, Analyse über den von objectKey gelieferten Schlüssel und über die Schlüssel eines Berichts, Ablehnung fremder Werte, purgeDeleted ohne Wartungsfenster. TestObjectFunctions (die Tests von #29639 erweitert): objectKey/`objectResolveKey` für aktuelle, historische, gelöschte und transiente Objekte, null für Texte, die kein Schlüssel sind, kein Ergebnis für ein aktuelles oder historisches Objekt ohne Leserecht, Ergebnis bei abgeschalteter Zugriffsprüfung. TestAdminOnly: an der Methode und an der Klasse markierte Funktionen scheitern interaktiv für einen Benutzer ohne Administratorrecht (auch in einem Lambda), gelingen interaktiv für root und nicht-interaktiv für jeden Benutzer, eine unmarkierte Funktion gelingt interaktiv für jeden; keine Auswertung zur Übersetzungszeit. Die Testobjekte werden nach jedem Test gelöscht, damit die übrigen Tests der Suite (TestSearchExpression) sie nicht sehen. Manuell in der Demo-Anwendung über die Skript-Konsole: Anlegen, Analyse (blockiert), Löschen, Analyse, Fehler ohne Wartungsfenster, Purge im Wartungsfenster mit Neustart, leere Analyse danach.
Grenzen
- Lokal wurde nur H2 ausgeführt; die Statements (CASE ... IN, UPDATE ... WHERE ... IN, Batch-Delete) sind auf Oracle/PostgreSQL/MySQL/MSSQL nicht gelaufen.
- Andere Cluster-Knoten werden nicht neu gestartet; die Operation verweigert den Lauf, solange sie aktiv sind.