Skip to content

ISSUE #20212 - TestCase DP Propagation + Search Index Propagation Refactor & Issue#26901

Open
TeddyCr wants to merge 13 commits intoopen-metadata:mainfrom
TeddyCr:ISSUE-20212
Open

ISSUE #20212 - TestCase DP Propagation + Search Index Propagation Refactor & Issue#26901
TeddyCr wants to merge 13 commits intoopen-metadata:mainfrom
TeddyCr:ISSUE-20212

Conversation

@TeddyCr
Copy link
Copy Markdown
Collaborator

@TeddyCr TeddyCr commented Apr 1, 2026

Overview

Fixes #20212

The SearchRepository maintained its own hardcoded list of inheritable fields and entity-type if/else chains to decide when and how to propagate field changes to child search indices. This duplicated knowledge that belongs in EntityRepository, causing drift — for example, tags were inherited at the DB layer but never propagated to Elasticsearch, and displayName propagation 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 EntityRepository subclass via getSearchPropagationDescriptors(), and makes SearchRepository a generic propagation engine that dispatches on PropagationType. It also introduces TAG_LABEL_LIST as a new propagation type to fix the gap where tags (which use TagLabel/DERIVED, not EntityReference/inherited) were silently dropped during propagation.

The most important changes are in:

  • SearchRepository.java — the core propagation engine refactor (appendAddScript, appendDeleteScript, appendUpdateScript now dispatch on PropagationType instead of hardcoded field-name if/else chains, plus three new Painless script generators for tag propagation)
  • PropagationDescriptor.java — new TAG_LABEL_LIST enum value
  • EntityRepository.java — dynamic nestPath computation for displayName (entityType + ".displayName" instead of null)

Summary

  • Introduce TAG_LABEL_LIST propagation type in PropagationDescriptor to correctly handle tag propagation — tags use TagLabel with labelType=DERIVED, not EntityReference with inherited=true
  • Fix displayName nested field propagation by computing nestPath dynamically in the base EntityRepository (entityType + ".displayName") instead of null, which produced broken Painless scripts (ctx._source.null)
  • Move "which fields propagate and how" declarations from SearchRepository's hardcoded lists into each EntityRepository subclass via getSearchPropagationDescriptors(), making SearchRepository a generic propagation engine
  • Add dedicated Painless script generators for tag propagation with dedup-by-tagFQN and sort semantics
  • Add dataProducts propagation descriptor to TableRepository
  • Add comprehensive unit tests (15 new tests in SearchRepositoryBehaviorTest) covering all PropagationType branches across add/update/delete operations
  • Add 10 integration tests across TableResourceIT, TestCaseResourceIT, GlossaryTermResourceIT, and TagResourceIT validating search index propagation end-to-end for owners, domains, displayName, tags, reviewers, and dataProducts

Changes by file

Core propagation engine

File Change
.../search/PropagationDescriptor.java Added TAG_LABEL_LIST to PropagationType enum — represents List<TagLabel> fields using labelType=DERIVED instead of EntityReference/inherited
.../search/SearchRepository.java Added TAG_LABEL_LIST case to appendAddScript(), appendDeleteScript(), appendUpdateScript() — deserializes TagLabel, sets labelType=DERIVED, delegates to tag-specific Painless generators
.../search/SearchRepository.java Added generateAddTagLabelListScript() — appends non-duplicate tags by tagFQN, sorts result
.../search/SearchRepository.java Added generateDeleteTagLabelListScript() — removes tags by tagFQN match
.../search/SearchRepository.java Added generateUpdateTagLabelListScript() — deletes old + adds new, deduplicates, sorts
.../search/SearchRepository.java Removed dead "tags" -> List.of() case from resolveEntityReferenceList()

Repository descriptors

File Change
.../jdbi3/EntityRepository.java Changed displayName descriptor nestPath from null to entityType + ".displayName" — dynamic per entity type (e.g., table.displayName, database.displayName)
.../jdbi3/TableRepository.java Changed tags descriptor from ENTITY_REFERENCE_LIST to TAG_LABEL_LIST
.../jdbi3/TableRepository.java Added dataProducts as ENTITY_REFERENCE_LIST descriptor
.../jdbi3/GlossaryTermRepository.java Changed tags descriptor from ENTITY_REFERENCE_LIST to TAG_LABEL_LIST
.../jdbi3/ServiceEntityRepository.java No changes — already correctly overrides displayName with nestPath = "service.displayName"
.../jdbi3/TagRepository.java No changes — uses SIMPLE_VALUE for name and certification
.../jdbi3/TestCaseRepository.java No changes — inherits base (owners, domains, followers, disabled, testSuites, displayName)

Unit tests

File Change
.../search/SearchRepositoryBehaviorTest.java Refactored mock setup: replaced static baseDescriptors with buildDescriptorsFor(entityType) helper computing correct displayName nestPath per entity type
.../search/SearchRepositoryBehaviorTest.java Added DATABASE_SERVICE mock with nestPath = "service.displayName"
.../search/SearchRepositoryBehaviorTest.java Updated tags descriptors from ENTITY_REFERENCE_LIST to TAG_LABEL_LIST
.../search/SearchRepositoryBehaviorTest.java +7 tests: TAG_LABEL_LIST add/delete/update (verifies DERIVED, data keys, script content), ignored for Domain, empty list edge case
.../search/SearchRepositoryBehaviorTest.java +4 tests: dataProducts add/delete/update on Table, null dataProducts → empty list
.../search/SearchRepositoryBehaviorTest.java +4 tests: RAW_REPLACE add/delete, NESTED_FIELD delete, ENTITY_REFERENCE_LIST update (owners)

Integration tests

File Change
.../it/tests/TableResourceIT.java Added findSourceById() helper for search index JSON navigation
.../it/tests/TableResourceIT.java +4 tests: owner add, owner remove, domain add, displayName update → Table search index
.../it/tests/TestCaseResourceIT.java +2 tests: owner, domain propagation from Table → TestCase search index
.../it/tests/TestCaseResourceIT.java +1 test (@Disabled): dataProduct propagation from Table → TestCase search index (pending domain validation rule)
.../it/tests/GlossaryTermResourceIT.java +2 tests: owner, reviewer propagation from Glossary → GlossaryTerm search index
.../it/tests/TagResourceIT.java +1 test: owner propagation from Classification → Tag search index

Documentation

File Change
PROPAGATION_REFACTOR_TASKS.md Added TAG_LABEL_LIST to type semantics table and enum definition
PROPAGATION_REFACTOR_TASKS.md Updated code snippets for TableRepository, GlossaryTermRepository, resolveEntityReferenceList
PROPAGATION_REFACTOR_TASKS.md Updated Task 7 nuance section to reflect tag-aware Painless scripts

Test plan

  • SearchRepositoryBehaviorTest — 92 tests pass (15 new for TAG_LABEL_LIST, ENTITY_REFERENCE_LIST dataProducts, RAW_REPLACE add/delete, NESTED_FIELD delete, 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 @Disabled pending domain validation rule handling
  • GlossaryTermResourceIT — owners, reviewers propagation from Glossary → GlossaryTerm search index (2 new)
  • TagResourceIT — owner propagation from Classification → Tag search index (1 new)

Type of change:

  • Bug fix
  • Improvement
  • New feature
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation

Checklist:

  • I have read the CONTRIBUTING document.
  • My PR title is Fixes <issue-number>: <short explanation>
  • I have commented on my code, particularly in hard-to-understand areas.
  • For JSON Schema changes: I updated the migration scripts or explained why it is not needed.

Copilot AI review requested due to automatic review settings April 1, 2026 01:25
@github-actions github-actions bot added Ingestion safe to test Add this label to run secure Github workflows on PRs labels Apr 1, 2026
Comment on lines 1469 to +1483
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)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Bug: requiresPropagation regression: Page parent & upstreamEntityRelationship no longer detected

The old requiresPropagation had special handling for:

  1. upstreamEntityRelationship field changes (used in lineage) — always triggered propagation
  2. Page entity parent field 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

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 1, 2026

⚠️ TypeScript Types Need Update

The generated TypeScript types are out of sync with the JSON schema changes.

Since this is a pull request from a forked repository, the types cannot be automatically committed.
Please generate and commit the types manually:

cd openmetadata-ui/src/main/resources/ui
./json2ts-generate-all.sh -l true
git add src/generated/
git commit -m "Update generated TypeScript types"
git push

After pushing the changes, this check will pass automatically.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 SearchRepository propagation script generation to dispatch by PropagationType, adding dedicated tag-label propagation scripts.
  • Adds dataProducts to 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

Comment on lines +171 to +209
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);
}
}
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Copilot AI review requested due to automatic review settings April 1, 2026 01:38
@TeddyCr TeddyCr requested a review from a team as a code owner April 1, 2026 01:38
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 18 out of 19 changed files in this pull request and generated 3 comments.

Comment on lines 1469 to +1484
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()));
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need a PR on Collate side

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 1, 2026

Jest test Coverage

UI tests summary

Lines Statements Branches Functions
Coverage: 64%
64.69% (58906/91054) 44.22% (30873/69812) 47.5% (9333/19648)

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 1, 2026

🟡 Playwright Results — all passed (25 flaky)

✅ 3442 passed · ❌ 0 failed · 🟡 25 flaky · ⏭️ 223 skipped

Shard Passed Failed Flaky Skipped
🟡 Shard 1 453 0 2 2
🟡 Shard 2 614 0 5 32
🟡 Shard 3 615 0 5 27
🟡 Shard 4 618 0 6 47
🟡 Shard 5 584 0 3 67
🟡 Shard 6 558 0 4 48
🟡 25 flaky test(s) (passed on retry)
  • Flow/Navbar.spec.ts › Search Term - Glossary (shard 1, 1 retry)
  • Pages/UserCreationWithPersona.spec.ts › Create user with persona and verify on profile (shard 1, 1 retry)
  • Features/BulkEditEntity.spec.ts › Glossary (shard 2, 1 retry)
  • Features/DataQuality/BundleSuiteBulkOperations.spec.ts › Bulk selection operations (shard 2, 1 retry)
  • Features/DomainTierCertificationVoting.spec.ts › DataProduct - UpVote and DownVote (shard 2, 1 retry)
  • Features/ExploreQuickFilters.spec.ts › search dropdown should work properly for quick filters (shard 2, 1 retry)
  • Features/Glossary/GlossaryP3Tests.spec.ts › should handle multiple rapid API calls (shard 2, 1 retry)
  • Features/Permissions/GlossaryPermissions.spec.ts › Team-based permissions work correctly (shard 3, 1 retry)
  • Features/RestoreEntityInheritedFields.spec.ts › Validate restore with Inherited domain and data products assigned (shard 3, 1 retry)
  • Features/TestSuiteMultiPipeline.spec.ts › TestSuite multi pipeline support (shard 3, 1 retry)
  • Flow/ExploreDiscovery.spec.ts › Should display deleted assets when showDeleted is checked and deleted is not present in queryFilter (shard 3, 1 retry)
  • Flow/PersonaFlow.spec.ts › Set default persona for team should work properly (shard 3, 1 retry)
  • Pages/Customproperties-part2.spec.ts › entityReferenceList shows item count, scrollable list, no expand toggle (shard 4, 1 retry)
  • Pages/DataContracts.spec.ts › Create Data Contract and validate for Topic (shard 4, 1 retry)
  • Pages/Domains.spec.ts › Domain announcement create, edit & delete (shard 4, 1 retry)
  • Pages/Domains.spec.ts › Rename domain with data products attached at domain and subdomain levels (shard 4, 1 retry)
  • Pages/Domains.spec.ts › Rename domain with assets (tables, topics, dashboards) preserves associations (shard 4, 1 retry)
  • Pages/Domains.spec.ts › Comprehensive domain rename with ALL relationships preserved (shard 4, 1 retry)
  • Pages/EntityDataConsumer.spec.ts › Tier Add, Update and Remove (shard 5, 1 retry)
  • Pages/EntityDataConsumer.spec.ts › Follow & Un-follow entity (shard 5, 1 retry)
  • Pages/EntityDataSteward.spec.ts › Glossary Term Add, Update and Remove (shard 5, 1 retry)
  • Pages/Glossary.spec.ts › Add and Remove Assets (shard 6, 1 retry)
  • Pages/InputOutputPorts.spec.ts › Lineage section collapse/expand (shard 6, 1 retry)
  • Pages/Users.spec.ts › Permissions for table details page for Data Consumer (shard 6, 1 retry)
  • VersionPages/EntityVersionPages.spec.ts › Directory (shard 6, 1 retry)

📦 Download artifacts

How to debug locally
# Download playwright-test-results-<shard> artifact and unzip
npx playwright show-trace path/to/trace.zip    # view trace

Comment on lines +1939 to +1941
data.put(
"entityBeforeUpdate",
JsonUtils.readValue(field.getOldValue().toString(), EntityReference.class));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ 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

Copilot AI review requested due to automatic review settings April 1, 2026 20:41
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 19 out of 20 changed files in this pull request and generated 3 comments.

Field repoMapField = Entity.class.getDeclaredField("ENTITY_REPOSITORY_MAP");
repoMapField.setAccessible(true);
Map<String, Object> repoMap = (Map<String, Object>) repoMapField.get(null);
repoMap.clear();
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
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);
}

Copilot uses AI. Check for mistakes.
@gitar-bot
Copy link
Copy Markdown

gitar-bot bot commented Apr 1, 2026

Code Review ⚠️ Changes requested 1 resolved / 6 findings

Refactors TestCase DP and Search Index propagation but introduces multiple regressions: requiresPropagation no longer detects Page parent and upstreamEntityRelationship, rowKey="name" produces undefined keys with dynamic columns, Pandas row sampler can exceed sample size limits, and appendUpdateScript lacks null checks on getOldValue(). Resolve these issues before proceeding.

⚠️ Bug: requiresPropagation regression: Page parent & upstreamEntityRelationship no longer detected

📄 openmetadata-service/src/main/java/org/openmetadata/service/search/SearchRepository.java:1469-1483 📄 openmetadata-service/src/main/java/org/openmetadata/service/search/SearchRepository.java:1094-1101 📄 openmetadata-service/src/main/java/org/openmetadata/service/search/SearchRepository.java:1675-1689

The old requiresPropagation had special handling for:

  1. upstreamEntityRelationship field changes (used in lineage) — always triggered propagation
  2. Page entity parent field 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;
⚠️ Bug: rowKey="name" produces undefined keys for arbitrary column sets

The FailedTestCaseSampleData component uses rowKey="name" on the Ant Design <Table>, but the data rows are built dynamically from whatever columns the API returns (e.g., customer_id, first_name, email). There is no guarantee a column named "name" exists. When it doesn't, every row gets undefined as its React key, which causes duplicate-key warnings and can lead to incorrect row rendering/state management (e.g., wrong rows highlighted, stale DOM on re-render).

Suggested fix
Use the row index as the key instead:

  rowKey={(_, index) => String(index)}
⚠️ Edge Case: Pandas row sampler can exceed FAILED_ROW_SAMPLE_SIZE across chunks

In PandasFailedRowSamplerMixin._get_failed_rows_sample(), each chunk is sliced to FAILED_ROW_SAMPLE_SIZE (50) independently, but the accumulated rows list is only checked after extending. If chunk 1 yields 30 matching rows and chunk 2 yields 30 matching rows, the result will contain 60 rows — exceeding the 50-row limit. The existing tests don't catch this because they only test single-chunk or small multi-chunk scenarios.

Suggested fix
Slice relative to remaining capacity:

  remaining = FAILED_ROW_SAMPLE_SIZE - len(rows)
  rows.extend(chunk_rows[:remaining])
  if len(rows) >= FAILED_ROW_SAMPLE_SIZE:
      break
⚠️ Edge Case: appendUpdateScript: getOldValue() may NPE without null check

📄 openmetadata-service/src/main/java/org/openmetadata/service/search/SearchRepository.java:1939-1941

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);
}
💡 Edge Case: Empty filter string causes ValueError in df.query("")

Several pandas validators' filter() methods (e.g., columnValueLengthsToBeBetween, columnValuesToBeBetween) return " or ".join(filters). When both bounds are None or infinite, filters is empty and the join returns "". This empty string is then passed to DataFrame.query(""), which raises a ValueError. The exception is caught by FailedSampleValidatorMixin and logged, so it won't crash the workflow, but the failed row sample will silently be missing. Consider returning None or a tautological expression when no filters apply.

Suggested fix
Return None when no filter conditions apply:

  def filter(self):
      ...
      if not filters:
          return None
      return " or ".join(filters)
✅ 1 resolved
Bug: appendDeleteScript uses getNewValue() for ENTITY_REFERENCE deletion → NPE

📄 openmetadata-service/src/main/java/org/openmetadata/service/search/SearchRepository.java:1873-1882 📄 openmetadata-service/src/main/java/org/openmetadata/service/search/SearchRepository.java:1847-1861 📄 openmetadata-service/src/main/java/org/openmetadata/service/search/SearchRepository.java:1830-1834 📄 openmetadata-service/src/main/java/org/openmetadata/service/search/SearchRepository.java:1943-1947
In appendDeleteScript, the ENTITY_REFERENCE case reads field.getNewValue().toString() (line 1875). For a field deletion, newValue is typically null — the value being removed is in oldValue. This will throw a NullPointerException at runtime when a single-valued entity reference field is deleted. Compare with the TAG_LABEL_LIST case on line 1868 which correctly reads field.getOldValue().

🤖 Prompt for agents
Code Review: Refactors TestCase DP and Search Index propagation but introduces multiple regressions: requiresPropagation no longer detects Page parent and upstreamEntityRelationship, rowKey="name" produces undefined keys with dynamic columns, Pandas row sampler can exceed sample size limits, and appendUpdateScript lacks null checks on getOldValue(). Resolve these issues before proceeding.

1. ⚠️ Bug: requiresPropagation regression: Page parent & upstreamEntityRelationship no longer detected
   Files: openmetadata-service/src/main/java/org/openmetadata/service/search/SearchRepository.java:1469-1483, openmetadata-service/src/main/java/org/openmetadata/service/search/SearchRepository.java:1094-1101, openmetadata-service/src/main/java/org/openmetadata/service/search/SearchRepository.java:1675-1689

   The old `requiresPropagation` had special handling for:
   1. `upstreamEntityRelationship` field changes (used in lineage) — always triggered propagation
   2. `Page` entity `parent` field 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;

2. ⚠️ Bug: rowKey="name" produces undefined keys for arbitrary column sets

   The `FailedTestCaseSampleData` component uses `rowKey="name"` on the Ant Design `<Table>`, but the data rows are built dynamically from whatever columns the API returns (e.g., `customer_id`, `first_name`, `email`). There is no guarantee a column named `"name"` exists. When it doesn't, every row gets `undefined` as its React key, which causes duplicate-key warnings and can lead to incorrect row rendering/state management (e.g., wrong rows highlighted, stale DOM on re-render).

   Suggested fix:
   Use the row index as the key instead:
   
     rowKey={(_, index) => String(index)}

3. ⚠️ Edge Case: Pandas row sampler can exceed FAILED_ROW_SAMPLE_SIZE across chunks

   In `PandasFailedRowSamplerMixin._get_failed_rows_sample()`, each chunk is sliced to `FAILED_ROW_SAMPLE_SIZE` (50) independently, but the accumulated `rows` list is only checked *after* extending. If chunk 1 yields 30 matching rows and chunk 2 yields 30 matching rows, the result will contain 60 rows — exceeding the 50-row limit. The existing tests don't catch this because they only test single-chunk or small multi-chunk scenarios.

   Suggested fix:
   Slice relative to remaining capacity:
   
     remaining = FAILED_ROW_SAMPLE_SIZE - len(rows)
     rows.extend(chunk_rows[:remaining])
     if len(rows) >= FAILED_ROW_SAMPLE_SIZE:
         break

4. 💡 Edge Case: Empty filter string causes ValueError in df.query("")

   Several pandas validators' `filter()` methods (e.g., `columnValueLengthsToBeBetween`, `columnValuesToBeBetween`) return `" or ".join(filters)`. When both bounds are `None` or infinite, `filters` is empty and the join returns `""`. This empty string is then passed to `DataFrame.query("")`, which raises a `ValueError`. The exception is caught by `FailedSampleValidatorMixin` and logged, so it won't crash the workflow, but the failed row sample will silently be missing. Consider returning `None` or a tautological expression when no filters apply.

   Suggested fix:
   Return None when no filter conditions apply:
   
     def filter(self):
         ...
         if not filters:
             return None
         return " or ".join(filters)

5. ⚠️ Edge Case: appendUpdateScript: getOldValue() may NPE without null check
   Files: openmetadata-service/src/main/java/org/openmetadata/service/search/SearchRepository.java:1939-1941

   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);
   }

Options

Display: compact → Showing less information.

Comment with these commands to change:

Compact
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Gitar

@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud bot commented Apr 1, 2026

@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud bot commented Apr 1, 2026

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Ingestion safe to test Add this label to run secure Github workflows on PRs

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support for Aggregated Metrics at Data Product Level

2 participants