ISSUE #20212 - TestCase DP Propagation + Search Index Propagation Refactor & Issue#26901
ISSUE #20212 - TestCase DP Propagation + Search Index Propagation Refactor & Issue#26901TeddyCr wants to merge 13 commits intoopen-metadata:mainfrom
Conversation
openmetadata-service/src/main/java/org/openmetadata/service/search/SearchRepository.java
Outdated
Show resolved
Hide resolved
openmetadata-service/src/main/java/org/openmetadata/service/search/SearchRepository.java
Show resolved
Hide resolved
| private boolean requiresPropagation( | ||
| ChangeDescription changeDescription, String entityType, EntityInterface entity) { | ||
| if (changeDescription == null) { | ||
| return false; | ||
| } | ||
| if (changeDescription == null) return false; | ||
|
|
||
| // Check if any inheritable fields have changed (owners, domains, etc.) | ||
| boolean hasInheritableChanges = | ||
| changeDescription.getFieldsAdded().stream() | ||
| .anyMatch(field -> inheritableFields.contains(field.getName())) | ||
| || changeDescription.getFieldsUpdated().stream() | ||
| .anyMatch(field -> inheritableFields.contains(field.getName())) | ||
| || changeDescription.getFieldsDeleted().stream() | ||
| .anyMatch(field -> inheritableFields.contains(field.getName())); | ||
|
|
||
| // Tags need special handling - they only propagate in specific scenarios: | ||
| // 1. From glossary terms to entities | ||
| // 2. When a tag entity itself is updated (to all entities using it) | ||
| // 3. NOT from table to columns | ||
| boolean hasTagChanges = false; | ||
| boolean nameChanged = | ||
| changeDescription.getFieldsUpdated().stream() | ||
| .anyMatch(field -> field.getName().equals(FIELD_NAME)); | ||
| if (entityType.equalsIgnoreCase(Entity.GLOSSARY_TERM) | ||
| || entityType.equalsIgnoreCase(Entity.TAG)) { | ||
| hasTagChanges = | ||
| changeDescription.getFieldsAdded().stream() | ||
| .anyMatch(field -> propagateFields.contains(field.getName())) | ||
| || changeDescription.getFieldsUpdated().stream() | ||
| .anyMatch(field -> propagateFields.contains(field.getName())) | ||
| || changeDescription.getFieldsDeleted().stream() | ||
| .anyMatch(field -> propagateFields.contains(field.getName())); | ||
| } | ||
|
|
||
| // Check for glossary term specific changes | ||
| if (entityType.equalsIgnoreCase(Entity.GLOSSARY_TERM)) { | ||
| hasInheritableChanges = | ||
| hasInheritableChanges | ||
| || hasTagChanges | ||
| || nameChanged | ||
| || changeDescription.getFieldsAdded().stream() | ||
| .anyMatch(field -> field.getName().equals(Entity.FIELD_TAGS)) | ||
| || changeDescription.getFieldsDeleted().stream() | ||
| .anyMatch(field -> field.getName().equals(Entity.FIELD_TAGS)); | ||
| } | ||
|
|
||
| // Check for certification tag changes + tag specific changes | ||
| if (entityType.equalsIgnoreCase(Entity.TAG)) { | ||
| Tag tag = (Tag) entity; | ||
| hasInheritableChanges = hasInheritableChanges || nameChanged; | ||
| if (tag != null && tag.getCertification() != null) { | ||
| hasInheritableChanges = | ||
| hasInheritableChanges | ||
| || changeDescription.getFieldsUpdated().stream() | ||
| .anyMatch(field -> field.getName().equals("certification")); | ||
| } | ||
| } | ||
|
|
||
| // Check for relationship changes that need propagation | ||
| if (changeDescription.getFieldsAdded().stream() | ||
| .anyMatch(field -> field.getName().equals("upstreamEntityRelationship")) | ||
| || changeDescription.getFieldsUpdated().stream() | ||
| .anyMatch(field -> field.getName().equals("upstreamEntityRelationship"))) { | ||
| hasInheritableChanges = true; | ||
| } | ||
|
|
||
| // Page entities have special FQN propagation when parent changes | ||
| if (entityType.equalsIgnoreCase(Entity.PAGE)) { | ||
| boolean parentChanged = | ||
| changeDescription.getFieldsAdded().stream() | ||
| .anyMatch(field -> field.getName().contains("parent")) | ||
| || changeDescription.getFieldsUpdated().stream() | ||
| .anyMatch(field -> field.getName().contains("parent")); | ||
| hasInheritableChanges = hasInheritableChanges || parentChanged; | ||
| } | ||
|
|
||
| boolean propagationRequired = hasInheritableChanges || hasTagChanges; | ||
|
|
||
| if (propagationRequired) { | ||
| LOG.debug( | ||
| "Propagation required for entity {} of type {} - changes detected in inheritable fields or special propagation needed", | ||
| entity.getId(), | ||
| entityType); | ||
| } | ||
| EntityRepository<? extends EntityInterface> repository = Entity.getEntityRepository(entityType); | ||
| Set<String> propagatedFields = | ||
| repository.getSearchPropagationDescriptors().stream() | ||
| .map(PropagationDescriptor::fieldName) | ||
| .collect(Collectors.toSet()); | ||
|
|
||
| return propagationRequired; | ||
| return Stream.of( | ||
| changeDescription.getFieldsAdded(), | ||
| changeDescription.getFieldsUpdated(), | ||
| changeDescription.getFieldsDeleted()) | ||
| .flatMap(List::stream) |
There was a problem hiding this comment.
⚠️ Bug: requiresPropagation regression: Page parent & upstreamEntityRelationship no longer detected
The old requiresPropagation had special handling for:
upstreamEntityRelationshipfield changes (used in lineage) — always triggered propagationPageentityparentfield changes — triggered FQN propagation to subpages
The new descriptor-based implementation (lines 1469-1485) only checks fields listed in getSearchPropagationDescriptors(). Neither upstreamEntityRelationship nor parent appear in any repository's descriptors. Since requiresPropagation gates all propagation paths at line 1094 — including propagateToRelatedEntities() (line 1101) which still contains the Page/lineage logic — those propagations will never execute.
Suggested fix:
Either add `parent` and `upstreamEntityRelationship` to the relevant entity repositories' propagation descriptors, or keep `requiresPropagation` as a general gate and add the special-case checks back:
// After the descriptor-based check:
boolean descriptorMatch = Stream.of(...).anyMatch(...);
boolean hasPageParentChange = Entity.PAGE.equalsIgnoreCase(entityType)
&& allChanges.anyMatch(f -> f.getName().contains("parent"));
boolean hasLineageChange = allChanges.anyMatch(
f -> "upstreamEntityRelationship".equals(f.getName()));
return descriptorMatch || hasPageParentChange || hasLineageChange;
Was this helpful? React with 👍 / 👎 | Reply gitar fix to apply this suggestion
|
There was a problem hiding this comment.
Pull request overview
Refactors search-index propagation so SearchRepository becomes a generic propagation engine driven by per-entity EntityRepository#getSearchPropagationDescriptors(), and adds dataProducts inheritance + search mapping/coverage for TestCase (plus tag propagation fixes via a new TAG_LABEL_LIST type).
Changes:
- Introduces
PropagationDescriptor/PropagationType(incl.TAG_LABEL_LIST) and moves propagation field configuration into repositories. - Refactors
SearchRepositorypropagation script generation to dispatch byPropagationType, adding dedicated tag-label propagation scripts. - Adds
dataProductsto TestCase schema + ES mapping and wires inheritance/loading in repositories and resources, with new unit/integration coverage.
Reviewed changes
Copilot reviewed 18 out of 18 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| openmetadata-spec/src/main/resources/json/schema/tests/testCase.json | Adds dataProducts to TestCase schema |
| openmetadata-spec/src/main/resources/elasticsearch/en/test_case_index_mapping.json | Adds dataProducts mapping to TestCase search index |
| openmetadata-service/src/test/java/org/openmetadata/service/search/SearchRepositoryBehaviorTest.java | Updates/expands unit tests for new propagation behavior/types |
| openmetadata-service/src/main/java/org/openmetadata/service/search/SearchRepository.java | Refactors propagation engine and adds tag propagation scripts |
| openmetadata-service/src/main/java/org/openmetadata/service/search/SearchClient.java | Updates follower-removal script param naming (removedFollowers) |
| openmetadata-service/src/main/java/org/openmetadata/service/search/PropagationDescriptor.java | Adds new propagation descriptor record + enum |
| openmetadata-service/src/main/java/org/openmetadata/service/resources/dqtests/TestCaseResource.java | Exposes dataProducts in TestCase FIELDS |
| openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/TestCaseRepository.java | Loads/inherits dataProducts from linked table and includes in export |
| openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/TagRepository.java | Adds tag propagation descriptors for search propagation |
| openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/TableRepository.java | Adds tag/dataProducts propagation descriptors |
| openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/ServiceEntityRepository.java | Overrides displayName propagation nestPath to service.displayName |
| openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/GlossaryTermRepository.java | Adds reviewers/tags propagation descriptors |
| openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/EntityRepository.java | Adds default search propagation descriptors + inheritDataProducts |
| openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/TestCaseResourceIT.java | Adds E2E tests for propagation into TestCase search docs |
| openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/TagResourceIT.java | Adds E2E test for owner propagation into Tag search docs |
| openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/TableResourceIT.java | Adds E2E tests + helper for propagation into Table search docs |
| openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/GlossaryTermResourceIT.java | Adds E2E tests for propagation into GlossaryTerm search docs |
| common/src/main/java/org/openmetadata/common/utils/CommonUtil.java | Adds CapitalizeFirst utility for script-param key names |
openmetadata-service/src/main/java/org/openmetadata/service/search/SearchRepository.java
Outdated
Show resolved
Hide resolved
openmetadata-service/src/main/java/org/openmetadata/service/search/SearchRepository.java
Outdated
Show resolved
Hide resolved
openmetadata-service/src/main/java/org/openmetadata/service/search/SearchRepository.java
Show resolved
Hide resolved
...data-service/src/test/java/org/openmetadata/service/search/SearchRepositoryBehaviorTest.java
Outdated
Show resolved
Hide resolved
| private void registerMockEntityRepositories() { | ||
| try { | ||
| Field repoMapField = Entity.class.getDeclaredField("ENTITY_REPOSITORY_MAP"); | ||
| repoMapField.setAccessible(true); | ||
| Map<String, Object> repoMap = (Map<String, Object>) repoMapField.get(null); | ||
|
|
||
| for (String entityType : | ||
| List.of( | ||
| Entity.TABLE, | ||
| Entity.GLOSSARY_TERM, | ||
| Entity.TAG, | ||
| Entity.PAGE, | ||
| Entity.DOMAIN, | ||
| Entity.DATABASE_SERVICE, | ||
| Entity.TEST_SUITE, | ||
| Entity.GLOSSARY, | ||
| Entity.CLASSIFICATION, | ||
| Entity.QUERY)) { | ||
| List<PropagationDescriptor> descriptors = buildDescriptorsFor(entityType); | ||
| EntityRepository<?> mockRepo = mock(EntityRepository.class); | ||
| doReturn(descriptors).when(mockRepo).getSearchPropagationDescriptors(); | ||
| repoMap.put(entityType, mockRepo); | ||
| } | ||
| } catch (Exception e) { | ||
| throw new RuntimeException("Failed to register mock entity repositories", e); | ||
| } | ||
| } | ||
|
|
||
| @SuppressWarnings("unchecked") | ||
| private void clearMockEntityRepositories() { | ||
| try { | ||
| Field repoMapField = Entity.class.getDeclaredField("ENTITY_REPOSITORY_MAP"); | ||
| repoMapField.setAccessible(true); | ||
| Map<String, Object> repoMap = (Map<String, Object>) repoMapField.get(null); | ||
| repoMap.clear(); | ||
| } catch (Exception e) { | ||
| throw new RuntimeException("Failed to clear mock entity repositories", e); | ||
| } | ||
| } |
There was a problem hiding this comment.
This test mutates the private static Entity.ENTITY_REPOSITORY_MAP via reflection and then calls repoMap.clear() in @AfterEach. This can leak global state across tests (especially if other tests rely on repositories registered in Entity). Prefer snapshotting the original map entries and restoring them in tearDown (or providing a dedicated test hook) instead of clearing the entire map.
openmetadata-spec/src/main/resources/json/schema/tests/testCase.json
Outdated
Show resolved
Hide resolved
common/src/main/java/org/openmetadata/common/utils/CommonUtil.java
Outdated
Show resolved
Hide resolved
openmetadata-service/src/main/java/org/openmetadata/service/search/SearchRepository.java
Show resolved
Hide resolved
| private boolean requiresPropagation( | ||
| ChangeDescription changeDescription, String entityType, EntityInterface entity) { | ||
| if (changeDescription == null) { | ||
| return false; | ||
| } | ||
| if (changeDescription == null) return false; | ||
|
|
||
| // Check if any inheritable fields have changed (owners, domains, etc.) | ||
| boolean hasInheritableChanges = | ||
| changeDescription.getFieldsAdded().stream() | ||
| .anyMatch(field -> inheritableFields.contains(field.getName())) | ||
| || changeDescription.getFieldsUpdated().stream() | ||
| .anyMatch(field -> inheritableFields.contains(field.getName())) | ||
| || changeDescription.getFieldsDeleted().stream() | ||
| .anyMatch(field -> inheritableFields.contains(field.getName())); | ||
|
|
||
| // Tags need special handling - they only propagate in specific scenarios: | ||
| // 1. From glossary terms to entities | ||
| // 2. When a tag entity itself is updated (to all entities using it) | ||
| // 3. NOT from table to columns | ||
| boolean hasTagChanges = false; | ||
| boolean nameChanged = | ||
| changeDescription.getFieldsUpdated().stream() | ||
| .anyMatch(field -> field.getName().equals(FIELD_NAME)); | ||
| if (entityType.equalsIgnoreCase(Entity.GLOSSARY_TERM) | ||
| || entityType.equalsIgnoreCase(Entity.TAG)) { | ||
| hasTagChanges = | ||
| changeDescription.getFieldsAdded().stream() | ||
| .anyMatch(field -> propagateFields.contains(field.getName())) | ||
| || changeDescription.getFieldsUpdated().stream() | ||
| .anyMatch(field -> propagateFields.contains(field.getName())) | ||
| || changeDescription.getFieldsDeleted().stream() | ||
| .anyMatch(field -> propagateFields.contains(field.getName())); | ||
| } | ||
|
|
||
| // Check for glossary term specific changes | ||
| if (entityType.equalsIgnoreCase(Entity.GLOSSARY_TERM)) { | ||
| hasInheritableChanges = | ||
| hasInheritableChanges | ||
| || hasTagChanges | ||
| || nameChanged | ||
| || changeDescription.getFieldsAdded().stream() | ||
| .anyMatch(field -> field.getName().equals(Entity.FIELD_TAGS)) | ||
| || changeDescription.getFieldsDeleted().stream() | ||
| .anyMatch(field -> field.getName().equals(Entity.FIELD_TAGS)); | ||
| } | ||
|
|
||
| // Check for certification tag changes + tag specific changes | ||
| if (entityType.equalsIgnoreCase(Entity.TAG)) { | ||
| Tag tag = (Tag) entity; | ||
| hasInheritableChanges = hasInheritableChanges || nameChanged; | ||
| if (tag != null && tag.getCertification() != null) { | ||
| hasInheritableChanges = | ||
| hasInheritableChanges | ||
| || changeDescription.getFieldsUpdated().stream() | ||
| .anyMatch(field -> field.getName().equals("certification")); | ||
| } | ||
| } | ||
|
|
||
| // Check for relationship changes that need propagation | ||
| if (changeDescription.getFieldsAdded().stream() | ||
| .anyMatch(field -> field.getName().equals("upstreamEntityRelationship")) | ||
| || changeDescription.getFieldsUpdated().stream() | ||
| .anyMatch(field -> field.getName().equals("upstreamEntityRelationship"))) { | ||
| hasInheritableChanges = true; | ||
| } | ||
|
|
||
| // Page entities have special FQN propagation when parent changes | ||
| if (entityType.equalsIgnoreCase(Entity.PAGE)) { | ||
| boolean parentChanged = | ||
| changeDescription.getFieldsAdded().stream() | ||
| .anyMatch(field -> field.getName().contains("parent")) | ||
| || changeDescription.getFieldsUpdated().stream() | ||
| .anyMatch(field -> field.getName().contains("parent")); | ||
| hasInheritableChanges = hasInheritableChanges || parentChanged; | ||
| } | ||
|
|
||
| boolean propagationRequired = hasInheritableChanges || hasTagChanges; | ||
|
|
||
| if (propagationRequired) { | ||
| LOG.debug( | ||
| "Propagation required for entity {} of type {} - changes detected in inheritable fields or special propagation needed", | ||
| entity.getId(), | ||
| entityType); | ||
| } | ||
| EntityRepository<? extends EntityInterface> repository = Entity.getEntityRepository(entityType); | ||
| Set<String> propagatedFields = | ||
| repository.getSearchPropagationDescriptors().stream() | ||
| .map(PropagationDescriptor::fieldName) | ||
| .collect(Collectors.toSet()); | ||
|
|
||
| return propagationRequired; | ||
| return Stream.of( | ||
| changeDescription.getFieldsAdded(), | ||
| changeDescription.getFieldsUpdated(), | ||
| changeDescription.getFieldsDeleted()) | ||
| .flatMap(List::stream) | ||
| .anyMatch(field -> propagatedFields.contains(field.getName())); |
There was a problem hiding this comment.
requiresPropagation(...) now gates all propagation (inherited fields, glossary/certification tags, and propagateToRelatedEntities). For Entity.PAGE, the parent-change handling lives in propagateToRelatedEntities (parent removal + FQN prefix updates), but requiresPropagation will return false unless PageRepository.getSearchPropagationDescriptors() includes a matching parent descriptor (it currently doesn't in this PR). This will prevent page parent changes from triggering the related-entity propagation logic. Consider either adding a parent descriptor for pages, or separating the gating so related-entity propagation runs when its own trigger fields change.
There was a problem hiding this comment.
Need a PR on Collate side
openmetadata-service/src/main/java/org/openmetadata/service/search/SearchRepository.java
Outdated
Show resolved
Hide resolved
🟡 Playwright Results — all passed (25 flaky)✅ 3442 passed · ❌ 0 failed · 🟡 25 flaky · ⏭️ 223 skipped
🟡 25 flaky test(s) (passed on retry)
How to debug locally# Download playwright-test-results-<shard> artifact and unzip
npx playwright show-trace path/to/trace.zip # view trace |
| data.put( | ||
| "entityBeforeUpdate", | ||
| JsonUtils.readValue(field.getOldValue().toString(), EntityReference.class)); |
There was a problem hiding this comment.
⚠️ Edge Case: appendUpdateScript: getOldValue() may NPE without null check
In appendUpdateScript, the ENTITY_REFERENCE case at line 1941 calls field.getOldValue().toString() without a null guard. The sibling appendDeleteScript method (line 1875) correctly checks if (!nullOrEmpty(oldValue)) before dereferencing. If a propagation update is triggered for a field that had no previous value (e.g., first-time domain assignment that goes through the update path rather than add), this will throw a NullPointerException.
The Painless script itself already handles the ctx._source.field == null case, so the intent is to support first-time writes through the update path — but the Java side will crash before the script is ever sent to Elasticsearch.
Suggested fix:
case ENTITY_REFERENCE -> {
EntityReference ref =
JsonUtils.readValue(field.getNewValue().toString(), EntityReference.class);
script.append(...);
Object oldVal = field.getOldValue();
if (!nullOrEmpty(oldVal)) {
data.put("entityBeforeUpdate",
JsonUtils.readValue(oldVal.toString(), EntityReference.class));
}
data.put(field.getName(), ref);
}
Was this helpful? React with 👍 / 👎 | Reply gitar fix to apply this suggestion
| Field repoMapField = Entity.class.getDeclaredField("ENTITY_REPOSITORY_MAP"); | ||
| repoMapField.setAccessible(true); | ||
| Map<String, Object> repoMap = (Map<String, Object>) repoMapField.get(null); | ||
| repoMap.clear(); |
There was a problem hiding this comment.
clearMockEntityRepositories() calls repoMap.clear(), which wipes the global Entity.ENTITY_REPOSITORY_MAP for the entire JVM. That can break any other unit tests that run after this class (repositories will no longer be registered). Instead, snapshot the original map contents in setUp() and restore them in tearDown() (or remove only the keys you added).
| repoMap.clear(); | |
| // Remove only the entries that were added by registerMockEntityRepositories() | |
| for (String entityType : | |
| List.of( | |
| Entity.TABLE, | |
| Entity.GLOSSARY_TERM, | |
| Entity.TAG, | |
| Entity.PAGE, | |
| Entity.DOMAIN, | |
| Entity.DATABASE_SERVICE, | |
| Entity.TEST_SUITE, | |
| Entity.GLOSSARY, | |
| Entity.CLASSIFICATION, | |
| Entity.QUERY)) { | |
| repoMap.remove(entityType); | |
| } |
openmetadata-service/src/main/java/org/openmetadata/service/search/SearchRepository.java
Show resolved
Hide resolved
openmetadata-service/src/main/java/org/openmetadata/service/search/PropagationDescriptor.java
Show resolved
Hide resolved
Code Review
|
| Compact |
|
Was this helpful? React with 👍 / 👎 | Gitar
|
|



Overview
Fixes #20212
The
SearchRepositorymaintained its own hardcoded list of inheritable fields and entity-typeif/elsechains to decide when and how to propagate field changes to child search indices. This duplicated knowledge that belongs inEntityRepository, causing drift — for example, tags were inherited at the DB layer but never propagated to Elasticsearch, anddisplayNamepropagation fired for all entities even though only service entities needed a nested path.This PR moves the "which fields propagate and how" declaration into each
EntityRepositorysubclass viagetSearchPropagationDescriptors(), and makesSearchRepositorya generic propagation engine that dispatches onPropagationType. It also introducesTAG_LABEL_LISTas a new propagation type to fix the gap where tags (which useTagLabel/DERIVED, notEntityReference/inherited) were silently dropped during propagation.The most important changes are in:
SearchRepository.java— the core propagation engine refactor (appendAddScript,appendDeleteScript,appendUpdateScriptnow dispatch onPropagationTypeinstead of hardcoded field-name if/else chains, plus three new Painless script generators for tag propagation)PropagationDescriptor.java— newTAG_LABEL_LISTenum valueEntityRepository.java— dynamicnestPathcomputation fordisplayName(entityType + ".displayName"instead ofnull)Summary
TAG_LABEL_LISTpropagation type inPropagationDescriptorto correctly handle tag propagation — tags useTagLabelwithlabelType=DERIVED, notEntityReferencewithinherited=truedisplayNamenested field propagation by computingnestPathdynamically in the baseEntityRepository(entityType + ".displayName") instead ofnull, which produced broken Painless scripts (ctx._source.null)SearchRepository's hardcoded lists into eachEntityRepositorysubclass viagetSearchPropagationDescriptors(), makingSearchRepositorya generic propagation enginetagFQNand sort semanticsdataProductspropagation descriptor toTableRepositorySearchRepositoryBehaviorTest) covering allPropagationTypebranches across add/update/delete operationsTableResourceIT,TestCaseResourceIT,GlossaryTermResourceIT, andTagResourceITvalidating search index propagation end-to-end for owners, domains, displayName, tags, reviewers, and dataProductsChanges by file
Core propagation engine
.../search/PropagationDescriptor.javaTAG_LABEL_LISTtoPropagationTypeenum — representsList<TagLabel>fields usinglabelType=DERIVEDinstead ofEntityReference/inherited.../search/SearchRepository.javaTAG_LABEL_LISTcase toappendAddScript(),appendDeleteScript(),appendUpdateScript()— deserializesTagLabel, setslabelType=DERIVED, delegates to tag-specific Painless generators.../search/SearchRepository.javagenerateAddTagLabelListScript()— appends non-duplicate tags bytagFQN, sorts result.../search/SearchRepository.javagenerateDeleteTagLabelListScript()— removes tags bytagFQNmatch.../search/SearchRepository.javagenerateUpdateTagLabelListScript()— deletes old + adds new, deduplicates, sorts.../search/SearchRepository.java"tags" -> List.of()case fromresolveEntityReferenceList()Repository descriptors
.../jdbi3/EntityRepository.javadisplayNamedescriptornestPathfromnulltoentityType + ".displayName"— dynamic per entity type (e.g.,table.displayName,database.displayName).../jdbi3/TableRepository.javatagsdescriptor fromENTITY_REFERENCE_LISTtoTAG_LABEL_LIST.../jdbi3/TableRepository.javadataProductsasENTITY_REFERENCE_LISTdescriptor.../jdbi3/GlossaryTermRepository.javatagsdescriptor fromENTITY_REFERENCE_LISTtoTAG_LABEL_LIST.../jdbi3/ServiceEntityRepository.javadisplayNamewithnestPath = "service.displayName".../jdbi3/TagRepository.javaSIMPLE_VALUEfornameandcertification.../jdbi3/TestCaseRepository.javaUnit tests
.../search/SearchRepositoryBehaviorTest.javabaseDescriptorswithbuildDescriptorsFor(entityType)helper computing correctdisplayNamenestPath per entity type.../search/SearchRepositoryBehaviorTest.javaDATABASE_SERVICEmock withnestPath = "service.displayName".../search/SearchRepositoryBehaviorTest.javatagsdescriptors fromENTITY_REFERENCE_LISTtoTAG_LABEL_LIST.../search/SearchRepositoryBehaviorTest.javaTAG_LABEL_LISTadd/delete/update (verifiesDERIVED, data keys, script content), ignored for Domain, empty list edge case.../search/SearchRepositoryBehaviorTest.javadataProductsadd/delete/update on Table, null dataProducts → empty list.../search/SearchRepositoryBehaviorTest.javaRAW_REPLACEadd/delete,NESTED_FIELDdelete,ENTITY_REFERENCE_LISTupdate (owners)Integration tests
.../it/tests/TableResourceIT.javafindSourceById()helper for search index JSON navigation.../it/tests/TableResourceIT.java.../it/tests/TestCaseResourceIT.java.../it/tests/TestCaseResourceIT.java@Disabled): dataProduct propagation from Table → TestCase search index (pending domain validation rule).../it/tests/GlossaryTermResourceIT.java.../it/tests/TagResourceIT.javaDocumentation
PROPAGATION_REFACTOR_TASKS.mdTAG_LABEL_LISTto type semantics table and enum definitionPROPAGATION_REFACTOR_TASKS.mdTableRepository,GlossaryTermRepository,resolveEntityReferenceListPROPAGATION_REFACTOR_TASKS.mdTest plan
SearchRepositoryBehaviorTest— 92 tests pass (15 new forTAG_LABEL_LIST,ENTITY_REFERENCE_LISTdataProducts,RAW_REPLACEadd/delete,NESTED_FIELDdelete, update path coverage)TableResourceIT— owners add/remove, domain, displayName propagation to table search index (4 new)TestCaseResourceIT— owners, domains propagation from Table → TestCase search index (2 new); dataProducts propagation test added but@Disabledpending domain validation rule handlingGlossaryTermResourceIT— owners, reviewers propagation from Glossary → GlossaryTerm search index (2 new)TagResourceIT— owner propagation from Classification → Tag search index (1 new)Type of change:
Checklist:
Fixes <issue-number>: <short explanation>