diff --git a/openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/DataProductResourceIT.java b/openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/DataProductResourceIT.java index 8e7af3c005a8..d12b2750f296 100644 --- a/openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/DataProductResourceIT.java +++ b/openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/DataProductResourceIT.java @@ -33,6 +33,7 @@ import org.openmetadata.schema.api.domains.CreateDomain.DomainType; import org.openmetadata.schema.api.domains.DataProductPortsView; import org.openmetadata.schema.api.services.CreateDatabaseService; +import org.openmetadata.schema.api.teams.CreateUser; import org.openmetadata.schema.entity.data.Dashboard; import org.openmetadata.schema.entity.data.Table; import org.openmetadata.schema.entity.data.Topic; @@ -41,6 +42,7 @@ import org.openmetadata.schema.entity.services.DashboardService; import org.openmetadata.schema.entity.services.DatabaseService; import org.openmetadata.schema.entity.services.MessagingService; +import org.openmetadata.schema.entity.teams.User; import org.openmetadata.schema.entity.type.Style; import org.openmetadata.schema.services.connections.database.MysqlConnection; import org.openmetadata.schema.services.connections.database.common.basicAuth; @@ -2873,4 +2875,122 @@ void test_deletingAssetRemovesItFromPorts(TestNamespace ns) throws Exception { ResultList> outputPorts = getOutputPorts(dataProduct.getId(), 10, 0); assertEquals(0, outputPorts.getPaging().getTotal()); } + + @Test + void softDeletedExpert_notReturnedInSingleGet(TestNamespace ns) { + OpenMetadataClient client = SdkClients.adminClient(); + Domain domain = getOrCreateDomain(ns); + + String userName = ns.shortPrefix("expert_user"); + User expert = + client + .users() + .create( + new CreateUser() + .withName(userName) + .withEmail(userName + "@test.openmetadata.org") + .withDescription("Expert user for soft-delete test")); + + CreateDataProduct create = + new CreateDataProduct() + .withName(ns.prefix("dp_softdel_expert")) + .withDescription("DataProduct for soft-delete expert test") + .withDomains(List.of(domain.getFullyQualifiedName())) + .withExperts(List.of(expert.getFullyQualifiedName())); + DataProduct dp = createEntity(create); + + client.users().delete(expert.getId().toString()); + + DataProduct byId = client.dataProducts().get(dp.getId().toString(), "experts"); + assertTrue( + byId.getExperts() == null || byId.getExperts().isEmpty(), + "Soft-deleted expert must not appear in single GET by ID"); + + DataProduct byName = client.dataProducts().getByName(dp.getFullyQualifiedName(), "experts"); + assertTrue( + byName.getExperts() == null || byName.getExperts().isEmpty(), + "Soft-deleted expert must not appear in single GET by name"); + } + + @Test + void softDeletedExpert_notReturnedInListEndpoint(TestNamespace ns) { + OpenMetadataClient client = SdkClients.adminClient(); + Domain domain = getOrCreateDomain(ns); + + String userName = ns.shortPrefix("expert_list_user"); + User expert = + client + .users() + .create( + new CreateUser() + .withName(userName) + .withEmail(userName + "@test.openmetadata.org") + .withDescription("Expert user for bulk soft-delete test")); + + CreateDataProduct create = + new CreateDataProduct() + .withName(ns.prefix("dp_softdel_expert_list")) + .withDescription("DataProduct for soft-delete expert list test") + .withDomains(List.of(domain.getFullyQualifiedName())) + .withExperts(List.of(expert.getFullyQualifiedName())); + DataProduct dp = createEntity(create); + + client.users().delete(expert.getId().toString()); + + ListParams params = + new ListParams() + .setFields("experts") + .withDomain(domain.getFullyQualifiedName()) + .withLimit(100); + ListResponse list = client.dataProducts().list(params); + DataProduct listed = + list.getData().stream() + .filter(p -> p.getId().equals(dp.getId())) + .findFirst() + .orElseThrow(() -> new AssertionError("DataProduct not found in list")); + assertTrue( + listed.getExperts() == null || listed.getExperts().isEmpty(), + "Soft-deleted expert must not appear in list endpoint"); + } + + @Test + void softDeletedOwner_notReturnedInListEndpoint(TestNamespace ns) { + OpenMetadataClient client = SdkClients.adminClient(); + Domain domain = getOrCreateDomain(ns); + + String userName = ns.shortPrefix("owner_list_user"); + User owner = + client + .users() + .create( + new CreateUser() + .withName(userName) + .withEmail(userName + "@test.openmetadata.org") + .withDescription("Owner user for soft-delete list test")); + + CreateDataProduct create = + new CreateDataProduct() + .withName(ns.prefix("dp_softdel_owner_list")) + .withDescription("DataProduct for soft-delete owner list test") + .withDomains(List.of(domain.getFullyQualifiedName())) + .withOwners(List.of(owner.getEntityReference())); + DataProduct dp = createEntity(create); + + client.users().delete(owner.getId().toString()); + + ListParams params = + new ListParams() + .setFields("owners") + .withDomain(domain.getFullyQualifiedName()) + .withLimit(100); + ListResponse list = client.dataProducts().list(params); + DataProduct listed = + list.getData().stream() + .filter(p -> p.getId().equals(dp.getId())) + .findFirst() + .orElseThrow(() -> new AssertionError("DataProduct not found in list")); + assertTrue( + listed.getOwners() == null || listed.getOwners().isEmpty(), + "Soft-deleted owner must not appear in list endpoint"); + } } diff --git a/openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/DomainResourceIT.java b/openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/DomainResourceIT.java index c36b0bcbd661..793bb80e1ad2 100644 --- a/openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/DomainResourceIT.java +++ b/openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/DomainResourceIT.java @@ -29,14 +29,20 @@ import org.junit.jupiter.api.parallel.ExecutionMode; import org.openmetadata.it.util.SdkClients; import org.openmetadata.it.util.TestNamespace; +import org.openmetadata.schema.api.VoteRequest; import org.openmetadata.schema.api.domains.CreateDomain; import org.openmetadata.schema.api.domains.CreateDomain.DomainType; +import org.openmetadata.schema.api.teams.CreateUser; import org.openmetadata.schema.entity.domains.Domain; +import org.openmetadata.schema.entity.teams.User; +import org.openmetadata.schema.type.ChangeEvent; import org.openmetadata.schema.type.EntityHistory; import org.openmetadata.schema.type.EntityReference; +import org.openmetadata.schema.type.Votes; import org.openmetadata.sdk.client.OpenMetadataClient; import org.openmetadata.sdk.models.ListParams; import org.openmetadata.sdk.models.ListResponse; +import org.openmetadata.sdk.network.HttpMethod; /** * Integration tests for Domain entity operations. @@ -1153,4 +1159,204 @@ void test_renameDomainDoesNotAffectSimilarPrefixDomains(TestNamespace ns) throws // Verify old child FQN no longer works assertThrows(Exception.class, () -> getEntityByName(oldChildFqn)); } + + @Test + void softDeletedExpert_notReturnedInSingleGet(TestNamespace ns) { + OpenMetadataClient client = SdkClients.adminClient(); + + String userName = ns.shortPrefix("domain_expert"); + User expert = + client + .users() + .create( + new CreateUser() + .withName(userName) + .withEmail(userName + "@test.openmetadata.org") + .withDescription("Expert user for domain soft-delete test")); + + CreateDomain create = + new CreateDomain() + .withName(ns.prefix("domain_softdel")) + .withDomainType(DomainType.AGGREGATE) + .withExperts(List.of(expert.getFullyQualifiedName())) + .withDescription("Domain for soft-delete expert test"); + Domain domain = createEntity(create); + + client.users().delete(expert.getId().toString()); + + Domain byId = client.domains().get(domain.getId().toString(), "experts"); + assertTrue( + byId.getExperts() == null || byId.getExperts().isEmpty(), + "Soft-deleted expert must not appear in single GET by ID"); + + Domain byName = client.domains().getByName(domain.getFullyQualifiedName(), "experts"); + assertTrue( + byName.getExperts() == null || byName.getExperts().isEmpty(), + "Soft-deleted expert must not appear in single GET by name"); + } + + @Test + void softDeletedExpert_notReturnedInListEndpoint(TestNamespace ns) { + OpenMetadataClient client = SdkClients.adminClient(); + + String userName = ns.shortPrefix("domain_expert_list"); + User expert = + client + .users() + .create( + new CreateUser() + .withName(userName) + .withEmail(userName + "@test.openmetadata.org") + .withDescription("Expert user for domain list soft-delete test")); + + CreateDomain create = + new CreateDomain() + .withName(ns.prefix("domain_softdel_list")) + .withDomainType(DomainType.AGGREGATE) + .withExperts(List.of(expert.getFullyQualifiedName())) + .withDescription("Domain for soft-delete expert list test"); + Domain domain = createEntity(create); + + client.users().delete(expert.getId().toString()); + + Domain listed = null; + ListParams params = new ListParams().setFields("experts").withLimit(100); + while (listed == null) { + ListResponse page = listEntities(params); + listed = + page.getData().stream() + .filter(d -> d.getId().equals(domain.getId())) + .findFirst() + .orElse(null); + String after = page.getPaging() != null ? page.getPaging().getAfter() : null; + if (listed != null || after == null) break; + params = new ListParams().setFields("experts").withLimit(100).setAfter(after); + } + assertNotNull(listed, "Domain not found in list"); + assertTrue( + listed.getExperts() == null || listed.getExperts().isEmpty(), + "Soft-deleted expert must not appear in list endpoint"); + } + + @Test + void softDeletedExpert_notReturnedInListWithIncludeAll(TestNamespace ns) { + OpenMetadataClient client = SdkClients.adminClient(); + + String userName = ns.shortPrefix("domain_expert_all"); + User expert = + client + .users() + .create( + new CreateUser() + .withName(userName) + .withEmail(userName + "@test.openmetadata.org") + .withDescription("Expert user for domain include-all soft-delete test")); + + CreateDomain create = + new CreateDomain() + .withName(ns.prefix("domain_softdel_all")) + .withDomainType(DomainType.AGGREGATE) + .withExperts(List.of(expert.getFullyQualifiedName())) + .withDescription("Domain for include-all soft-delete expert test"); + Domain domain = createEntity(create); + + client.users().delete(expert.getId().toString()); + + Domain listed = null; + ListParams params = + new ListParams().setFields("experts").withLimit(100).addFilter("include", "all"); + while (listed == null) { + ListResponse page = listEntities(params); + listed = + page.getData().stream() + .filter(d -> d.getId().equals(domain.getId())) + .findFirst() + .orElse(null); + String after = page.getPaging() != null ? page.getPaging().getAfter() : null; + if (listed != null || after == null) break; + params = + new ListParams() + .setFields("experts") + .withLimit(100) + .addFilter("include", "all") + .setAfter(after); + } + assertNotNull(listed, "Domain not found in list with include=all"); + assertTrue( + listed.getExperts() == null || listed.getExperts().isEmpty(), + "Soft-deleted expert must not appear even when include=all (applies to top-level only)"); + } + + @Test + void softDeletedFollower_notReturnedInListEndpoint(TestNamespace ns) { + OpenMetadataClient client = SdkClients.adminClient(); + + String userName = ns.shortPrefix("follower_list"); + User follower = + client + .users() + .create( + new CreateUser().withName(userName).withEmail(userName + "@test.openmetadata.org")); + + Domain domain = createEntity(createRequest(ns.prefix("dom_follower"), ns)); + + client + .getHttpClient() + .execute( + HttpMethod.PUT, + "/v1/domains/" + domain.getId() + "/followers", + follower.getId(), + ChangeEvent.class); + + client.users().delete(follower.getId().toString()); + + ListParams params = new ListParams().setFields("followers").withLimit(1000000); + ListResponse list = listEntities(params); + Domain listed = + list.getData().stream() + .filter(d -> d.getId().equals(domain.getId())) + .findFirst() + .orElseThrow(() -> new AssertionError("Domain not found in list")); + assertTrue( + listed.getFollowers() == null || listed.getFollowers().isEmpty(), + "Soft-deleted follower must not appear in list endpoint"); + } + + @Test + void softDeletedVoter_notReturnedInListEndpoint(TestNamespace ns) { + String userName = ns.shortPrefix("voter_list"); + String userEmail = userName + "@test.openmetadata.org"; + + OpenMetadataClient adminClient = SdkClients.adminClient(); + User voter = + adminClient.users().create(new CreateUser().withName(userName).withEmail(userEmail)); + + Domain domain = createEntity(createRequest(ns.prefix("dom_voter"), ns)); + + OpenMetadataClient voterClient = SdkClients.createClient(userEmail, userEmail, new String[] {}); + voterClient + .getHttpClient() + .execute( + HttpMethod.PUT, + "/v1/domains/" + domain.getId() + "/vote", + new VoteRequest().withUpdatedVoteType(VoteRequest.VoteType.VOTED_UP), + ChangeEvent.class); + + adminClient.users().delete(voter.getId().toString()); + + ListParams params = new ListParams().setFields("votes").withLimit(1000000); + ListResponse list = listEntities(params); + Domain listed = + list.getData().stream() + .filter(d -> d.getId().equals(domain.getId())) + .findFirst() + .orElseThrow(() -> new AssertionError("Domain not found in list")); + Votes votes = listed.getVotes(); + boolean voterInUpVotes = + votes != null + && votes.getUpVoters() != null + && votes.getUpVoters().stream() + .anyMatch(ref -> ref != null && voter.getId().equals(ref.getId())); + assertFalse(voterInUpVotes, "Soft-deleted voter must not appear in list endpoint votes"); + } } diff --git a/openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/GlossaryTermResourceIT.java b/openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/GlossaryTermResourceIT.java index f338d6fc9d1a..8830a27c8550 100644 --- a/openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/GlossaryTermResourceIT.java +++ b/openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/GlossaryTermResourceIT.java @@ -28,6 +28,7 @@ import org.openmetadata.schema.api.data.CreateTable; import org.openmetadata.schema.api.data.TermReference; import org.openmetadata.schema.api.feed.CreateThread; +import org.openmetadata.schema.api.teams.CreateUser; import org.openmetadata.schema.entity.data.DatabaseSchema; import org.openmetadata.schema.entity.data.Glossary; import org.openmetadata.schema.entity.data.GlossaryTerm; @@ -3197,4 +3198,45 @@ private String getTermAssetsByName(OpenMetadataClient client, String fqn) { null, optionsBuilder.build()); } + + @Test + void softDeletedReviewer_notReturnedInListEndpoint(TestNamespace ns) { + OpenMetadataClient client = SdkClients.adminClient(); + Glossary glossary = getOrCreateGlossary(ns); + + String userName = ns.shortPrefix("reviewer_list"); + User reviewer = + client + .users() + .create( + new CreateUser() + .withName(userName) + .withEmail(userName + "@test.openmetadata.org") + .withDescription("Reviewer user for glossary soft-delete list test")); + + CreateGlossaryTerm create = + new CreateGlossaryTerm() + .withName(ns.prefix("term_softdel_reviewer")) + .withGlossary(glossary.getFullyQualifiedName()) + .withDescription("Term for soft-delete reviewer list test") + .withReviewers(List.of(reviewer.getEntityReference())); + GlossaryTerm term = createEntity(create); + + client.users().delete(reviewer.getId().toString()); + + ListParams params = + new ListParams() + .setFields("reviewers") + .withLimit(100) + .addFilter("glossary", glossary.getId().toString()); + ListResponse list = listEntities(params); + GlossaryTerm listed = + list.getData().stream() + .filter(t -> t.getId().equals(term.getId())) + .findFirst() + .orElseThrow(() -> new AssertionError("GlossaryTerm not found in list")); + assertTrue( + listed.getReviewers() == null || listed.getReviewers().isEmpty(), + "Soft-deleted reviewer must not appear in list endpoint"); + } } diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/Entity.java b/openmetadata-service/src/main/java/org/openmetadata/service/Entity.java index 497661f4b989..5fe2a534efc3 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/Entity.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/Entity.java @@ -491,7 +491,7 @@ public static EntityReference getEntityReferenceById( // For regular entities, use the standard repository EntityRepository repository = getEntityRepository(entityType); - include = repository.supportsSoftDelete ? Include.ALL : include; + include = repository.supportsSoftDelete ? include : Include.ALL; return repository.getReference(id, include); } @@ -512,7 +512,7 @@ public static List getEntityReferencesByIds( // For regular entities, use the standard repository EntityRepository repository = getEntityRepository(entityType); - include = repository.supportsSoftDelete ? Include.ALL : include; + include = repository.supportsSoftDelete ? include : Include.ALL; return repository.getReferences(ids, include); } @@ -525,48 +525,6 @@ public static EntityReference getEntityReferenceByName( return repository.getReferenceByName(fqn, include); } - /** - * Get entity reference by ID, respecting the include parameter for soft-delete filtering. Unlike - * {@link #getEntityReferenceById}, this method does NOT override the include parameter to ALL for - * repositories that support soft delete. - */ - public static EntityReference getEntityReferenceByIdRespectingInclude( - @NonNull String entityType, @NonNull UUID id, Include include) { - if (ENTITY_TS_REPOSITORY_MAP.containsKey(entityType)) { - return new EntityReference() - .withId(id) - .withType(entityType) - .withFullyQualifiedName(entityType + "." + id); - } - EntityRepository repository = getEntityRepository(entityType); - // If repository doesn't support soft delete, use ALL since there's no deleted column - include = repository.supportsSoftDelete ? include : Include.ALL; - return repository.getReference(id, include); - } - - /** - * Get entity references by IDs, respecting the include parameter for soft-delete filtering. - * Unlike {@link #getEntityReferencesByIds}, this method does NOT override the include parameter - * to ALL for repositories that support soft delete. - */ - public static List getEntityReferencesByIdsRespectingInclude( - @NonNull String entityType, @NonNull List ids, Include include) { - if (ENTITY_TS_REPOSITORY_MAP.containsKey(entityType)) { - return ids.stream() - .map( - id -> - new EntityReference() - .withId(id) - .withType(entityType) - .withFullyQualifiedName(entityType + "." + id)) - .collect(Collectors.toList()); - } - EntityRepository repository = getEntityRepository(entityType); - // If repository doesn't support soft delete, use ALL since there's no deleted column - include = repository.supportsSoftDelete ? include : Include.ALL; - return repository.getReferences(ids, include); - } - public static List getOwners(@NonNull EntityReference reference) { EntityRepository repository = getEntityRepository(reference.getType()); diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/DataProductRepository.java b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/DataProductRepository.java index b5f815d18c45..1d0d26db6d64 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/DataProductRepository.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/DataProductRepository.java @@ -38,6 +38,7 @@ import java.util.Map; import java.util.Set; import java.util.UUID; +import java.util.function.Function; import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; import org.jdbi.v3.sqlobject.transaction.Transaction; @@ -967,24 +968,28 @@ private Map> batchFetchExperts(List dat return expertsMap; } - // Initialize empty lists for all data products for (DataProduct dataProduct : dataProducts) { expertsMap.put(dataProduct.getId(), new ArrayList<>()); } - // Single batch query to get all expert relationships List records = daoCollection .relationshipDAO() .findToBatch( entityListToStrings(dataProducts), Relationship.EXPERT.ordinal(), Entity.USER); - // Group experts by data product ID + List expertIds = + records.stream().map(r -> UUID.fromString(r.getToId())).distinct().toList(); + Map expertRefsById = + Entity.getEntityReferencesByIds(Entity.USER, expertIds, Include.NON_DELETED).stream() + .collect(Collectors.toMap(EntityReference::getId, Function.identity(), (a, b) -> a)); + for (CollectionDAO.EntityRelationshipObject record : records) { UUID dataProductId = UUID.fromString(record.getFromId()); - EntityReference expertRef = - Entity.getEntityReferenceById( - Entity.USER, UUID.fromString(record.getToId()), NON_DELETED); + EntityReference expertRef = expertRefsById.get(UUID.fromString(record.getToId())); + if (expertRef == null) { + continue; + } expertsMap.get(dataProductId).add(expertRef); } diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/DomainRepository.java b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/DomainRepository.java index 615e0ebc717e..244eea25bc42 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/DomainRepository.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/DomainRepository.java @@ -27,6 +27,7 @@ import java.util.List; import java.util.Map; import java.util.UUID; +import java.util.function.Function; import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; import org.jdbi.v3.sqlobject.transaction.Transaction; @@ -580,22 +581,26 @@ private Map> batchFetchExperts(List domains) return expertsMap; } - // Initialize empty lists for all domains domains.forEach(domain -> expertsMap.put(domain.getId(), new ArrayList<>())); - // Single batch query to get all expert relationships var records = daoCollection .relationshipDAO() .findToBatch(entityListToStrings(domains), Relationship.EXPERT.ordinal(), Entity.USER); - // Group experts by domain ID + List expertIds = + records.stream().map(r -> UUID.fromString(r.getToId())).distinct().toList(); + Map expertRefsById = + Entity.getEntityReferencesByIds(Entity.USER, expertIds, Include.NON_DELETED).stream() + .collect(Collectors.toMap(EntityReference::getId, Function.identity(), (a, b) -> a)); + records.forEach( record -> { var domainId = UUID.fromString(record.getFromId()); - var expertRef = - getEntityReferenceById(Entity.USER, UUID.fromString(record.getToId()), NON_DELETED); - expertsMap.get(domainId).add(expertRef); + var expertRef = expertRefsById.get(UUID.fromString(record.getToId())); + if (expertRef != null) { + expertsMap.get(domainId).add(expertRef); + } }); return expertsMap; diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/EntityRelationshipRepository.java b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/EntityRelationshipRepository.java index b24110afadbe..16acb2b27f4b 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/EntityRelationshipRepository.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/EntityRelationshipRepository.java @@ -78,8 +78,7 @@ public List getEntityReferences( queryCount++; try { - List typeRefs = - Entity.getEntityReferencesByIdsRespectingInclude(entityType, ids, include); + List typeRefs = Entity.getEntityReferencesByIds(entityType, ids, include); refs.addAll(typeRefs); } catch (Exception e) { // Fallback for partial failures - fetch individually to handle deleted entities gracefully @@ -89,7 +88,7 @@ public List getEntityReferences( e.getMessage()); for (UUID id : ids) { try { - refs.add(Entity.getEntityReferenceByIdRespectingInclude(entityType, id, include)); + refs.add(Entity.getEntityReferenceById(entityType, id, include)); } catch (EntityNotFoundException ex) { // Skip deleted or missing entities skippedCount++; diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/EntityRepository.java b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/EntityRepository.java index 70d0e5a5a7b1..a38781138f2b 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/EntityRepository.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/EntityRepository.java @@ -8815,7 +8815,8 @@ private Map> resolveRelationshipEntityReferen Map> refsByType = new HashMap<>(); for (Entry> entry : idsByType.entrySet()) { List refs = - Entity.getEntityReferencesByIds(entry.getKey(), new ArrayList<>(entry.getValue()), ALL); + Entity.getEntityReferencesByIds( + entry.getKey(), new ArrayList<>(entry.getValue()), NON_DELETED); refsByType.put( entry.getKey(), refs.stream() @@ -9028,7 +9029,7 @@ private Map> batchFetchOwners(List entities) { ownerIdsByType.forEach( (entityType, ownerIds) -> { var ownerRefs = - Entity.getEntityReferencesByIds(entityType, new ArrayList<>(ownerIds), ALL); + Entity.getEntityReferencesByIds(entityType, new ArrayList<>(ownerIds), NON_DELETED); var refMap = ownerRefs.stream() .collect(Collectors.toMap(EntityReference::getId, ref -> ref, (a, b) -> a)); @@ -9073,7 +9074,7 @@ private Map> batchFetchFollowers(List entities) { .collect(Collectors.toList()); Map followerRefs = - Entity.getEntityReferencesByIds(USER, followerIds, ALL).stream() + Entity.getEntityReferencesByIds(USER, followerIds, NON_DELETED).stream() .collect(Collectors.toMap(EntityReference::getId, Function.identity())); records.forEach( @@ -9081,7 +9082,9 @@ record -> { UUID entityId = UUID.fromString(record.getToId()); UUID followerId = UUID.fromString(record.getFromId()); EntityReference followerRef = followerRefs.get(followerId); - followersMap.computeIfAbsent(entityId, k -> new ArrayList<>()).add(followerRef); + if (followerRef != null) { + followersMap.computeIfAbsent(entityId, k -> new ArrayList<>()).add(followerRef); + } }); return followersMap; @@ -9117,7 +9120,8 @@ private Map batchFetchVotes(List entities) { upVoterIds.values().forEach(allUserIds::addAll); downVoterIds.values().forEach(allUserIds::addAll); Map userRefs = - Entity.getEntityReferencesByIds(Entity.USER, new ArrayList<>(allUserIds), ALL).stream() + Entity.getEntityReferencesByIds(Entity.USER, new ArrayList<>(allUserIds), NON_DELETED) + .stream() .collect(Collectors.toMap(EntityReference::getId, Function.identity())); for (T entity : entities) { @@ -9313,7 +9317,8 @@ private Map> batchFetchReviewers(List entities) { reviewerIdsByType.forEach( (entityType, reviewerIds) -> { var reviewerRefs = - Entity.getEntityReferencesByIds(entityType, new ArrayList<>(reviewerIds), ALL); + Entity.getEntityReferencesByIds( + entityType, new ArrayList<>(reviewerIds), NON_DELETED); var refMap = reviewerRefs.stream() .collect(Collectors.toMap(EntityReference::getId, ref -> ref, (a, b) -> a)); @@ -9388,23 +9393,23 @@ private Map> batchFetchExperts(List entities) { // Cache UUID conversions to avoid repeated parsing Map uuidCache = new HashMap<>(); - // Collect all unique expert user IDs (with .distinct() to avoid duplicate fetches) + // findToBatch returns fromId=entity, toId=user — collect user IDs from toId List expertIds = records.stream() - .map(record -> uuidCache.computeIfAbsent(record.getFromId(), UUID::fromString)) + .map(record -> uuidCache.computeIfAbsent(record.getToId(), UUID::fromString)) .distinct() .collect(Collectors.toList()); - // Batch fetch all expert references + // Batch fetch all expert references, filtering out soft-deleted users Map expertRefs = - Entity.getEntityReferencesByIds(USER, expertIds, ALL).stream() + Entity.getEntityReferencesByIds(USER, expertIds, NON_DELETED).stream() .collect(Collectors.toMap(EntityReference::getId, Function.identity(), (a, b) -> a)); - // Group experts by entity (reuse cached UUIDs) + // Group experts by entity records.forEach( record -> { - UUID entityId = uuidCache.computeIfAbsent(record.getToId(), UUID::fromString); - UUID expertId = uuidCache.get(record.getFromId()); // Already cached above + UUID entityId = uuidCache.computeIfAbsent(record.getFromId(), UUID::fromString); + UUID expertId = uuidCache.get(record.getToId()); // Already cached above EntityReference expertRef = expertRefs.get(expertId); if (expertRef != null) { expertsMap.computeIfAbsent(entityId, k -> new ArrayList<>()).add(expertRef); diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/EntityResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/EntityResource.java index d620e4b0e3c4..08114377ef0e 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/EntityResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/EntityResource.java @@ -309,7 +309,8 @@ public T getInternal( String includeRelations) { Fields fields = getFields(fieldsParam); OperationContext operationContext = new OperationContext(entityType, getViewOperations(fields)); - RelationIncludes relationIncludes = new RelationIncludes(include, includeRelations); + Include resolvedInclude = include != null ? include : Include.NON_DELETED; + RelationIncludes relationIncludes = new RelationIncludes(resolvedInclude, includeRelations); return getInternal( uriInfo, securityContext, @@ -412,7 +413,8 @@ public T getByNameInternal( String includeRelations) { Fields fields = getFields(fieldsParam); OperationContext operationContext = new OperationContext(entityType, getViewOperations(fields)); - RelationIncludes relationIncludes = new RelationIncludes(include, includeRelations); + Include resolvedInclude = include != null ? include : Include.NON_DELETED; + RelationIncludes relationIncludes = new RelationIncludes(resolvedInclude, includeRelations); return getByNameInternal( uriInfo, securityContext,