Skip to content
Open
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
89122f6
Fix #27107: soft-deleted users still appear in experts/reviewers acro…
yan-3005 Apr 7, 2026
f56057c
Thread Include through bulk relationship resolution instead of hardco…
yan-3005 Apr 7, 2026
3264fb3
Fix inherited experts/owners including soft-deleted users from parent…
yan-3005 Apr 7, 2026
841775b
Fix soft-deleted users leaking into owners/experts/reviewers across a…
yan-3005 Apr 7, 2026
d4743a1
Fix test user email validation in soft-delete integration tests
yan-3005 Apr 7, 2026
05b41cb
Fix GlossaryTerm list test to filter by glossary ID
yan-3005 Apr 7, 2026
1a1888a
Fix fromId/toId swap in EntityRepository.batchFetchExperts
yan-3005 Apr 7, 2026
0c45006
Add merge function to Collectors.toMap in batchFetchExperts
yan-3005 Apr 7, 2026
639c950
Handle soft-deleted parent in setInheritedFields for DataProduct and …
yan-3005 Apr 7, 2026
97f328a
Remove dead setFieldsInBulk(Include) overload and collapse Include fr…
yan-3005 Apr 7, 2026
d9aa379
Paginate domain list to find test domain regardless of total count
yan-3005 Apr 7, 2026
00e7896
Merge branch 'main' into ram/fix-27107-soft-deleted-users-in-relations
yan-3005 Apr 7, 2026
37a014a
Revert WorksheetRepository Include change — out of scope for #27107
yan-3005 Apr 7, 2026
47def38
Merge branch 'main' into ram/fix-27107-soft-deleted-users-in-relations
yan-3005 Apr 8, 2026
4af35c3
Address PR review: add limit to DataProduct list test; align TagRepos…
yan-3005 Apr 8, 2026
b3d7482
Revert TagRepository Include changes — out of scope for #27107
yan-3005 Apr 8, 2026
d5446b1
Remove unnecessary changes and compilation error
yan-3005 Apr 8, 2026
736b773
Thread Include parameter through bulk relationship resolution path
yan-3005 Apr 8, 2026
007976e
Fix subclass bypass: use ThreadLocal to carry Include through virtual…
yan-3005 Apr 8, 2026
21c8e64
Merge branch 'main' into ram/fix-27107-soft-deleted-users-in-relations
yan-3005 Apr 8, 2026
f4a875e
Merge branch 'main' into ram/fix-27107-soft-deleted-users-in-relations
yan-3005 Apr 8, 2026
5e77478
Merge branch 'main' into ram/fix-27107-soft-deleted-users-in-relations
yan-3005 Apr 9, 2026
8b443b1
Fix null Include in setFieldsInBulk — default to NON_DELETED
yan-3005 Apr 9, 2026
85911a3
Merge branch 'main' into ram/fix-27107-soft-deleted-users-in-relations
yan-3005 Apr 12, 2026
6807212
Merge remote-tracking branch 'origin/main' into ram/fix-27107-soft-de…
yan-3005 Apr 17, 2026
70a4b67
Fix #27107: fix inverted ternary root cause; harden bulk-list NON_DEL…
yan-3005 Apr 20, 2026
344bc44
Merge branch 'main' into ram/fix-27107-soft-deleted-users-in-relations
yan-3005 Apr 20, 2026
7217773
Fix Copilot review: null-guard in batchFetchFollowers, remove dead 3-…
yan-3005 Apr 20, 2026
135be1c
fix: bump list limit to 1000000 in follower/voter IT tests to handle …
yan-3005 Apr 20, 2026
607743b
fix: remove unused Include param from listInternal and serializeJsons
yan-3005 Apr 20, 2026
9e31dbc
Merge branch 'main' into ram/fix-27107-soft-deleted-users-in-relations
yan-3005 Apr 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -2873,4 +2875,81 @@ void test_deletingAssetRemovesItFromPorts(TestNamespace ns) throws Exception {
ResultList<Map<String, Object>> 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<DataProduct> 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");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@
import org.openmetadata.it.util.TestNamespace;
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.EntityHistory;
import org.openmetadata.schema.type.EntityReference;
import org.openmetadata.sdk.client.OpenMetadataClient;
Expand Down Expand Up @@ -1153,4 +1155,82 @@ 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<Domain> 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);
}
Comment on lines +1222 to +1234
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

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

This paging loop has no hard stop / cursor-repeat protection. If paging ever returns a non-null after that doesn’t advance (or cycles), this test can hang indefinitely in CI. Consider adding a max-iterations bound and/or tracking previously seen after cursors to fail fast with a clear assertion.

Copilot uses AI. Check for mistakes.
assertNotNull(listed, "Domain not found in list");
assertTrue(
listed.getExperts() == null || listed.getExperts().isEmpty(),
"Soft-deleted expert must not appear in list endpoint");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -3147,4 +3148,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<GlossaryTerm> 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");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -967,24 +968,30 @@ private Map<UUID, List<EntityReference>> batchFetchExperts(List<DataProduct> 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<CollectionDAO.EntityRelationshipObject> records =
daoCollection
.relationshipDAO()
.findToBatch(
entityListToStrings(dataProducts), Relationship.EXPERT.ordinal(), Entity.USER);

// Group experts by data product ID
List<UUID> expertIds =
records.stream().map(r -> UUID.fromString(r.getToId())).distinct().toList();
Map<UUID, EntityReference> expertRefsById =
Entity.getEntityReferencesByIdsRespectingInclude(
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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -580,22 +581,28 @@ private Map<UUID, List<EntityReference>> batchFetchExperts(List<Domain> 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<UUID> expertIds =
records.stream().map(r -> UUID.fromString(r.getToId())).distinct().toList();
Map<UUID, EntityReference> expertRefsById =
Entity.getEntityReferencesByIdsRespectingInclude(
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;
Expand Down
Loading
Loading