enhancement
major
minor
defect
critical
major
minor
minor
#30142
CommaSeparatedStringSet: Nicht-deterministische Serialisierung erzeugt doppelte key-attributes in Schema-Baseline
== Problem
Bei der Migration kann es zu doppelten <key-attributes>-Einträgen in der gespeicherten Schema-Baseline kommen, was zum Fehler führt:
Multiple entries with key: interface com.top_logic.basic.db.schema.setup.config.KeyAttributes Invalid indexed property entry for property 'configuration-interface'
Im gespeicherten Schema-XML finden sich dann z.B.: {{{#!xml <key-attributes attributes="dest,source"/> <key-attributes attributes="source,dest"/> }}}
== Ursache
Die Dopplung entsteht durch das Zusammenspiel zweier Probleme:
=== 1. Nicht-deterministische Serialisierung in CommaSeparatedStringSet
KeyAttributes.getAttributes() ist ein Set<String>, formatiert mit CommaSeparatedStringSet. Diese Klasse nutzt intern HashSet, dessen Iterationsreihenfolge nicht deterministisch ist:
- CommaSeparatedStringSet.getValueNonEmpty() ruft StringServices.toSet() auf, das ein HashSet zurückgibt
- CommaSeparatedStringSet.getSpecificationNonNull() ruft StringServices.join() auf, das über das HashSet iteriert
- Ergebnis: {source, dest} kann als "source,dest" oder "dest,source" serialisiert werden, je nach JVM-Lauf
=== 2. Unsicheres "Copy-and-Append"-Pattern in XSLT-Migrationen
Mehrere XSLT-Migrationen fügen <key-attributes> hinzu, ohne zu prüfen ob bereits ein Eintrag existiert:
Ticket_27205_KeyAttributes_for_Association.transform.xslt (Modul tl): {{{#!xml <xsl:template match="/config/meta-objects/association[@object_name='Association']/annotations">
<xsl:copy>
<xsl:apply-templates select="@* | node()"/>
<key-attributes attributes="source,dest"/>
</xsl:copy>
</xsl:template> }}}
Dieses Template kopiert alle bestehenden Kinder (inkl. eines evtl. schon vorhandenen <key-attributes attributes="dest,source"/>) und hängt ein neues <key-attributes attributes="source,dest"/> an.
Gleiches unsicheres Pattern in:
- Ticket_27205 (tl-element) für hasWrapperAttValueBaseAssociation
- Ticket_28604 (tl-element) für MetaElement_generalizations, hasHistoricValue
- Ticket_28604 (tl-model-wysiwyg) für HTMLAttributeStorage
Im Gegensatz dazu nutzt dasselbe Ticket_28604 für I18NAttributeStorage das korrekte Pattern, das den bestehenden Eintrag ersetzt statt zu duplizieren: {{{#!xml <xsl:template match=".../annotations/key-attributes">
<key-attributes attributes="object, metaAttribute"/>
</xsl:template> }}}
=== Konkreter Ablauf
- Schema-Baseline wird in DBProperties gespeichert. Wegen HashSet-Nichtdeterminismus steht dort z.B. attributes="dest,source".
- XSLT-Migration (Ticket_27205/28604) läuft via XsltTableDefinitionBaselineProcessor auf dem rohen XML. Das XSLT kopiert alle Kinder und hängt ein neues <key-attributes> an → Duplikat.
- Ein späterer Processor ruft MigrationContext.getPersistentSchema() → TypedConfiguration.fromString() auf. Die @Key(CONFIGURATION_INTERFACE_NAME)-Collection in MetaObjectConfig.getAnnotations() lehnt das Duplikat ab.
Konkret betroffen ist z.B. RemoveApplicationTypesProcessor in Ticket_29019_Removed_log_entries.
== Lösungsvorschlag
=== Deterministische Serialisierung in CommaSeparatedStringSet
In CommaSeparatedStringSet.getSpecificationNonNull() das Set vor dem Join sortieren:
{{{#!java @Override protected String getSpecificationNonNull(Set<String> configValue) {
List<String> sorted = new ArrayList<>(configValue);
Collections.sort(sorted);
return StringServices.join(sorted, ",");
} }}}
Dies macht die Serialisierung deterministisch, ohne die Deserialisierung oder die interne Datenstruktur zu ändern. Verhindert zukünftige Dopplungen.
=== XSLT-Migrationen nachträglich absichern
Optional: Eine zusätzliche Framework-Migration, die doppelte <key-attributes> aus der gespeicherten Baseline entfernt, um bestehende betroffene Datenbanken zu bereinigen.
Lösung
- Ursache des Fehlers ist das Copy-and-Append-Muster der XSLT-Migrationen: Das Duplikat wird über das Konfigurationsinterface erkannt, entsteht also unabhängig von der Reihenfolge der Attribute. Alle XSLT-Migrationen, die ein <key-attributes> anhängen, kopieren einen schon vorhandenen Eintrag nicht mehr mit, sondern ersetzen ihn: tl Ticket_27205, tl-element Ticket_27205 (legte zuvor ein zweites <annotations> an), Ticket_28604 und Ticket_29088, tl-model-search Ticket_28710, tl-model-wysiwyg Ticket_28604. Für eine Datenbank, die die Migration noch nicht ausgeführt hat, ist das Ergebnis unverändert.
- Zwei Templates trafen die gespeicherte Baseline nie, weil sie einen <attributes>-Container erwarten, den die Baseline nicht schreibt (@DefaultContainer): tl-doc Ticket_28604 (key-attributes von tl.doc.Page) und tl-element Ticket_29088 (Attribut tType von PersBoundComp). Beide treffen jetzt die tatsächliche Form. Migrierte Datenbanken sind davon nicht betroffen, weil StoreTypeConfiguration die Baseline am Ende jedes Migrationslaufs mit dem konfigurierten Schema überschreibt; die Lücke bestand nur für nachfolgende Prozessoren desselben Laufs.
- CommaSeparatedStringSet serialisiert sortiert; die gespeicherte Baseline ist damit stabil.
- Keine Bereinigungs-Migration: MigrationService prüft das Protokoll vor dem Commit (commitConnection → checkErrors()), eine an dem Duplikat gescheiterte Migration wurde also nie festgeschrieben; keine Datenbank enthält eine kaputte Baseline.
- Tests: AbstractBaselineTransformTest (tl-core Tests) wendet ein Migrations-XSLT auf eine Baseline in der gespeicherten Form an; je Modul prüft TestBaselineTransforms die geänderten XSLTs (mit und ohne vorhandenen Eintrag).