enhancement
major
minor
defect
critical
major
minor
minor
#30142
CommaSeparatedStringSet: Non-deterministic serialization creates duplicate key attributes in the schema baseline
== Problem
During migration, duplicate <key-attributes> entries may occur in the saved schema baseline, resulting in the following error:
Multiple entries with key: interface com.top_logic.basic.db.schema.setup.config.KeyAttributes Invalid indexed property entry for property 'configuration-interface'
The saved schema XML then contains, for example: {{{#!xml <key-attributes attributes="dest,source"/> <key-attributes attributes="source,dest"/> }}}
== Cause
The duplication results from the interaction of two issues:
=== 1. Non-deterministic serialization in CommaSeparatedStringSet
KeyAttributes.getAttributes() returns a Set<String> formatted using CommaSeparatedStringSet. This class internally uses HashSet, whose iteration order is not deterministic:
- CommaSeparatedStringSet.getValueNonEmpty() calls StringServices.toSet(), which returns a HashSet
- CommaSeparatedStringSet.getSpecificationNonNull() calls StringServices.join(), which iterates over the HashSet
- Result: {source, dest} can be serialized as "source,dest" or "dest,source," depending on the JVM run
=== 2. Unsafe "Copy-and-Append" Pattern in XSLT Migrations
Several XSLT migrations add <key-attributes> without checking whether an entry already exists:
Ticket_27205_KeyAttributes_for_Association.transform.xslt (tl module): {{{#!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> }}}
This template copies all existing children (including any existing <key-attributes attributes="dest,source"/>) and appends a new <key-attributes attributes="source,dest"/>.
Same unsafe pattern in:
- Ticket_27205 (tl-element) for hasWrapperAttValueBaseAssociation
- Ticket_28604 (tl-element) for MetaElement_generalizations, hasHistoricValue
- Ticket_28604 (tl-model-wysiwyg) for HTMLAttributeStorage
In contrast, the same Ticket_28604 uses the correct pattern for I18NAttributeStorage, which replaces the existing entry instead of duplicating it: {{{#!xml <xsl:template match=".../annotations/key-attributes">
<key-attributes attributes="object, metaAttribute"/>
</xsl:template> }}}
=== Specific Process
- The schema baseline is stored in DBProperties. Due to HashSet nondeterminism, it contains, for example, attributes="dest,source".
- XSLT migration (Ticket_27205/28604) runs via ` XsltTableDefinitionBaselineProcessor ` on the raw XML. The XSLT copies all children and appends a new `<key-attributes> ` element → duplicate.
- A subsequent processor calls MigrationContext.getPersistentSchema() → TypedConfiguration.fromString(). The @Key(CONFIGURATION_INTERFACE_NAME) collection in MetaObjectConfig.getAnnotations() rejects the duplicate.
Specifically affected is, for example, RemoveApplicationTypesProcessor in Ticket_29019_Removed_log_entries.
== Proposed Solution
=== Deterministic Serialization in CommaSeparatedStringSet
In CommaSeparatedStringSet.getSpecificationNonNull(), sort the set before the join:
{{{#!java @Override protected String getSpecificationNonNull(Set<String> configValue) {
List<String> sorted = new ArrayList<>(configValue);
Collections.sort(sorted);
return StringServices.join(sorted, ",");
} }}}
This makes serialization deterministic without changing deserialization or the internal data structure. Prevents future duplicates.
=== Retrospectively Secure XSLT Migrations
Optional: An additional framework migration that removes duplicate <key-attributes> from the stored baseline to clean up existing affected databases.
Solution
- The cause of the error is the copy-and-append pattern used in XSLT migrations: The duplicate is detected via the configuration interface, meaning it occurs regardless of the order of the attributes. All XSLT migrations that append a <key-attributes> no longer copy an existing entry but replace it: tl Ticket_27205, tl-element Ticket_27205 (previously created a second <annotations> ), Ticket_28604, and Ticket_29088; tl-model-search Ticket_28710; tl-model-wysiwyg Ticket_28604. For a database that has not yet performed the migration, the result remains unchanged.
- Two templates never matched the stored baseline because they expect an <attributes> container that the baseline does not write (@DefaultContainer): tl-doc Ticket_28604 (key-attributes of tl.doc.Page) and tl-element Ticket_29088 ( tType attribute of PersBoundComp). Both now match the actual form. Migrated databases are not affected by this because StoreTypeConfiguration overwrites the baseline with the configured schema at the end of each migration run; the discrepancy existed only for subsequent processors in the same run.
- CommaSeparatedStringSet is serialized in sorted order; the stored baseline is therefore stable.
- No cleanup migration: MigrationService checks the log before committing (commitConnection → checkErrors()), so a migration that failed due to a duplicate was never committed; no database contains a corrupted baseline.
- Tests: AbstractBaselineTransformTest (tl-core tests) applies a migration XSLT to a baseline in its stored form; for each module, TestBaselineTransforms checks the modified XSLTs (with and without an existing entry).