Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
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 @@ -2570,4 +2570,232 @@ void test_glossaryTermEntityStatusFiltering(TestNamespace ns) {
assertEquals(
2, ourMultiStatusTerms.size(), "Both terms should be returned with multi-status filter");
}

// ===================================================================
// ASSET ENDPOINT TESTS WITH PARENT FILTER
// ===================================================================

@Test
void get_assetsCountsWithParentFilter(TestNamespace ns) {
OpenMetadataClient client = SdkClients.adminClient();

// Create two glossaries with terms
CreateGlossary createGlossary1 =
new CreateGlossary()
.withName(ns.prefix("asset_count_glossary1"))
.withDescription("Glossary 1 for asset count test");
Glossary glossary1 = client.glossaries().create(createGlossary1);

CreateGlossary createGlossary2 =
new CreateGlossary()
.withName(ns.prefix("asset_count_glossary2"))
.withDescription("Glossary 2 for asset count test");
Glossary glossary2 = client.glossaries().create(createGlossary2);

// Create terms in glossary1
CreateGlossaryTerm termReq1 =
new CreateGlossaryTerm()
.withName(ns.prefix("count_term1"))
.withGlossary(glossary1.getFullyQualifiedName())
.withDescription("Term 1 in glossary 1");
GlossaryTerm term1 = createEntity(termReq1);

CreateGlossaryTerm termReq2 =
new CreateGlossaryTerm()
.withName(ns.prefix("count_term2"))
.withGlossary(glossary1.getFullyQualifiedName())
.withDescription("Term 2 in glossary 1");
GlossaryTerm term2 = createEntity(termReq2);

// Create term in glossary2
CreateGlossaryTerm termReq3 =
new CreateGlossaryTerm()
.withName(ns.prefix("count_term3"))
.withGlossary(glossary2.getFullyQualifiedName())
.withDescription("Term in glossary 2");
GlossaryTerm term3 = createEntity(termReq3);

// Get counts with parent=glossary1 — should only include glossary1's terms
String countsWithParent = getAssetCounts(client, glossary1.getFullyQualifiedName());
assertNotNull(countsWithParent);
assertFalse(
countsWithParent.contains(term3.getFullyQualifiedName()),
"Should not contain terms from glossary2 when filtering by glossary1");

// Get counts without parent — should include all terms
String countsWithoutParent = getAssetCounts(client, null);
assertNotNull(countsWithoutParent);
Comment on lines +2618 to +2627
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

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

These tests don’t strongly validate the new parent filtering behavior. For example, countsWithParent is only asserted to not contain term3, which would also pass if the endpoint returned {} or an empty body; and term1/term2 are created but never asserted. Consider parsing the response JSON into a map and asserting that (a) term1 and term2 keys are present, (b) term3 is absent, and (c) the filtered response differs from the unfiltered response in the expected way.

Copilot uses AI. Check for mistakes.
}

@Test
void get_assetsCountsWithNoParent(TestNamespace ns) {
OpenMetadataClient client = SdkClients.adminClient();

// Calling without parent should succeed and return a response
String counts = getAssetCounts(client, null);
assertNotNull(counts);
}

@Test
void get_termAssetsById_withParentFilter(TestNamespace ns) {
OpenMetadataClient client = SdkClients.adminClient();

CreateGlossary createGlossary =
new CreateGlossary()
.withName(ns.prefix("id_assets_glossary"))
.withDescription("Glossary for assets by id test");
Glossary glossary = client.glossaries().create(createGlossary);

// Create parent term and child term
CreateGlossaryTerm parentReq =
new CreateGlossaryTerm()
.withName(ns.prefix("id_parent"))
.withGlossary(glossary.getFullyQualifiedName())
.withDescription("Parent term");
GlossaryTerm parentTerm = createEntity(parentReq);

CreateGlossaryTerm childReq =
new CreateGlossaryTerm()
.withName(ns.prefix("id_child"))
.withGlossary(glossary.getFullyQualifiedName())
.withParent(parentTerm.getFullyQualifiedName())
.withDescription("Child term");
GlossaryTerm childTerm = createEntity(childReq);

// Query child term assets with parent filter matching its parent — should succeed
String result =
getTermAssetsById(client, childTerm.getId().toString(), parentTerm.getFullyQualifiedName());
assertNotNull(result);

// Query parent term assets with parent filter = glossary — should succeed
String parentResult =
getTermAssetsById(client, parentTerm.getId().toString(), glossary.getFullyQualifiedName());
assertNotNull(parentResult);

// Query child term assets with a non-matching parent — should return empty
String emptyResult =
getTermAssetsById(client, childTerm.getId().toString(), "nonexistent.glossary");
assertNotNull(emptyResult);
}
Comment on lines +2665 to +2679
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

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

get_termAssetsById_withParentFilter doesn’t currently assert the parent-filter behavior. No assets are tagged with the terms, so both the matching-parent and non-matching-parent calls can return empty results and still pass (the test only checks assertNotNull). To actually cover the new logic, create at least one real asset (e.g., a table) tagged with childTerm, then parse the paging response and assert the asset is returned when parent matches and that the result total/data is empty when parent does not match.

Copilot uses AI. Check for mistakes.

@Test
void get_termAssetsByName_withParentFilter(TestNamespace ns) {
OpenMetadataClient client = SdkClients.adminClient();

CreateGlossary createGlossary =
new CreateGlossary()
.withName(ns.prefix("name_assets_glossary"))
.withDescription("Glossary for assets by name test");
Glossary glossary = client.glossaries().create(createGlossary);

CreateGlossaryTerm parentReq =
new CreateGlossaryTerm()
.withName(ns.prefix("name_parent"))
.withGlossary(glossary.getFullyQualifiedName())
.withDescription("Parent term");
GlossaryTerm parentTerm = createEntity(parentReq);

CreateGlossaryTerm childReq =
new CreateGlossaryTerm()
.withName(ns.prefix("name_child"))
.withGlossary(glossary.getFullyQualifiedName())
.withParent(parentTerm.getFullyQualifiedName())
.withDescription("Child term");
GlossaryTerm childTerm = createEntity(childReq);

// Query child term assets by name with parent filter matching — should succeed
String result =
getTermAssetsByName(
client, childTerm.getFullyQualifiedName(), parentTerm.getFullyQualifiedName());
assertNotNull(result);

// Query with non-matching parent — should return empty
String emptyResult =
getTermAssetsByName(client, childTerm.getFullyQualifiedName(), "nonexistent.glossary");
assertNotNull(emptyResult);
Comment on lines +2706 to +2715
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

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

get_termAssetsByName_withParentFilter has the same issue as the ID-based test: it doesn’t tag any assets and only asserts non-null responses, so it won’t fail if the parent filter is ignored. Add a tagged asset for childTerm and assert (by parsing the paging response) that results are returned only when parent matches and are empty when it doesn’t.

Copilot uses AI. Check for mistakes.
}

@Test
void get_termAssetsById_withoutParentFilter(TestNamespace ns) {
OpenMetadataClient client = SdkClients.adminClient();

CreateGlossary createGlossary =
new CreateGlossary()
.withName(ns.prefix("noparent_assets_glossary"))
.withDescription("Glossary for assets without parent test");
Glossary glossary = client.glossaries().create(createGlossary);

CreateGlossaryTerm termReq =
new CreateGlossaryTerm()
.withName(ns.prefix("noparent_term"))
.withGlossary(glossary.getFullyQualifiedName())
.withDescription("Term for no-parent test");
GlossaryTerm term = createEntity(termReq);

// Without parent param — should return all assets for the term
String result = getTermAssetsById(client, term.getId().toString(), null);
assertNotNull(result);
}

@Test
void get_termAssetsByName_withoutParentFilter(TestNamespace ns) {
OpenMetadataClient client = SdkClients.adminClient();

CreateGlossary createGlossary =
new CreateGlossary()
.withName(ns.prefix("noparent_name_glossary"))
.withDescription("Glossary for name assets without parent test");
Glossary glossary = client.glossaries().create(createGlossary);

CreateGlossaryTerm termReq =
new CreateGlossaryTerm()
.withName(ns.prefix("noparent_name_term"))
.withGlossary(glossary.getFullyQualifiedName())
.withDescription("Term for no-parent name test");
GlossaryTerm term = createEntity(termReq);

// Without parent param — should return all assets for the term
String result = getTermAssetsByName(client, term.getFullyQualifiedName(), null);
assertNotNull(result);
}

private String getAssetCounts(OpenMetadataClient client, String parent) {
RequestOptions.Builder optionsBuilder = RequestOptions.builder();
if (parent != null) {
optionsBuilder.queryParam("parent", parent);
}
return client
.getHttpClient()
.executeForString(
HttpMethod.GET, "/v1/glossaryTerms/assets/counts", null, optionsBuilder.build());
}

private String getTermAssetsById(OpenMetadataClient client, String id, String parent) {
RequestOptions.Builder optionsBuilder = RequestOptions.builder();
optionsBuilder.queryParam("limit", "10");
optionsBuilder.queryParam("offset", "0");
if (parent != null) {
optionsBuilder.queryParam("parent", parent);
}
return client
.getHttpClient()
.executeForString(
HttpMethod.GET, "/v1/glossaryTerms/" + id + "/assets", null, optionsBuilder.build());
}

private String getTermAssetsByName(OpenMetadataClient client, String fqn, String parent) {
RequestOptions.Builder optionsBuilder = RequestOptions.builder();
optionsBuilder.queryParam("limit", "10");
optionsBuilder.queryParam("offset", "0");
if (parent != null) {
optionsBuilder.queryParam("parent", parent);
}
return client
.getHttpClient()
.executeForString(
HttpMethod.GET,
"/v1/glossaryTerms/name/" + fqn + "/assets",
null,
optionsBuilder.build());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -166,9 +166,15 @@ public GlossaryTermRepository() {
}

public ResultList<EntityReference> getGlossaryTermAssets(
UUID glossaryTermId, int limit, int offset) {
UUID glossaryTermId, int limit, int offset, String parent) {
GlossaryTerm term = get(null, glossaryTermId, getFields("id,fullyQualifiedName"));

if (parent != null
&& !parent.isEmpty()
&& !FullyQualifiedName.isParent(term.getFullyQualifiedName(), parent)) {
return new ResultList<>(new ArrayList<>(), null, null, 0);
}

if (inheritedFieldEntitySearch == null) {
LOG.warn("Search is unavailable for glossary term assets. Returning empty list.");
return new ResultList<>(new ArrayList<>(), null, null, 0);
Expand All @@ -191,19 +197,27 @@ public ResultList<EntityReference> getGlossaryTermAssets(
}

public ResultList<EntityReference> getGlossaryTermAssetsByName(
String termName, int limit, int offset) {
String termName, int limit, int offset, String parent) {
GlossaryTerm term = getByName(null, termName, getFields("id,fullyQualifiedName"));
return getGlossaryTermAssets(term.getId(), limit, offset);
return getGlossaryTermAssets(term.getId(), limit, offset, parent);
}

public Map<String, Integer> getAllGlossaryTermsWithAssetsCount() {
public Map<String, Integer> getAllGlossaryTermsWithAssetsCount(String parent) {
if (inheritedFieldEntitySearch == null) {
LOG.warn("Search unavailable for glossary term asset counts");
return new HashMap<>();
}

List<GlossaryTerm> allGlossaryTerms =
listAll(getFields("fullyQualifiedName"), new ListFilter(null));

if (parent != null && !parent.isEmpty()) {
allGlossaryTerms =
allGlossaryTerms.stream()
.filter(term -> FullyQualifiedName.isParent(term.getFullyQualifiedName(), parent))
.collect(Collectors.toList());
Comment on lines 209 to +216
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

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

getAllGlossaryTermsWithAssetsCount(parent) still loads all glossary terms via listAll(...) and then filters in-memory when parent is provided. For large catalogs this can be unnecessarily expensive and defeats the purpose of a scoped query. Consider fetching only the nested terms directly (e.g., via daoCollection.glossaryTermDAO().getNestedTerms(parent) / an FQN-prefix DAO query) when parent is non-empty, and then run the count loop on that smaller set.

Suggested change
List<GlossaryTerm> allGlossaryTerms =
listAll(getFields("fullyQualifiedName"), new ListFilter(null));
if (parent != null && !parent.isEmpty()) {
allGlossaryTerms =
allGlossaryTerms.stream()
.filter(term -> FullyQualifiedName.isParent(term.getFullyQualifiedName(), parent))
.collect(Collectors.toList());
List<GlossaryTerm> allGlossaryTerms;
if (parent != null && !parent.isEmpty()) {
// Fetch only the nested terms under the given parent to avoid loading all glossary terms
allGlossaryTerms = daoCollection.glossaryTermDAO().getNestedTerms(parent);
} else {
allGlossaryTerms = listAll(getFields("fullyQualifiedName"), new ListFilter(null));

Copilot uses AI. Check for mistakes.
}

Map<String, Integer> glossaryTermAssetCounts = new HashMap<>();

for (GlossaryTerm term : allGlossaryTerms) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -433,11 +433,18 @@ public Response getRelationTypeUsageCounts(
content = @Content(mediaType = "application/json"))
})
public Response getAllGlossaryTermsWithAssetsCount(
@Context UriInfo uriInfo, @Context SecurityContext securityContext) {
@Context UriInfo uriInfo,
@Context SecurityContext securityContext,
@Parameter(
description =
"Filter by parent glossary or glossary term FQN. "
+ "When provided, only returns asset counts for children whose FQN starts with this value.")
@QueryParam("parent")
String parent) {
OperationContext operationContext =
new OperationContext(entityType, MetadataOperation.VIEW_ALL);
authorizer.authorize(securityContext, operationContext, getResourceContext());
java.util.Map<String, Integer> result = repository.getAllGlossaryTermsWithAssetsCount();
java.util.Map<String, Integer> result = repository.getAllGlossaryTermsWithAssetsCount(parent);
return Response.ok(result).build();
}

Expand Down Expand Up @@ -866,8 +873,14 @@ public Response listGlossaryTermAssets(
@DefaultValue("0")
@Min(0)
@QueryParam("offset")
int offset) {
return Response.ok(repository.getGlossaryTermAssets(id, limit, offset)).build();
int offset,
@Parameter(
description =
"Filter by parent glossary or glossary term FQN. "
+ "When provided, only returns assets for children whose FQN starts with this value.")
Comment on lines +879 to +880
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

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

The parent query param description here says it "returns assets for children whose FQN starts with this value", but this endpoint returns assets for the requested term ({id}) and only uses parent as an additional scope filter. The current wording is misleading in the generated OpenAPI docs; please update it to describe the actual behavior (e.g., only return results if the term is within the provided parent scope).

Suggested change
"Filter by parent glossary or glossary term FQN. "
+ "When provided, only returns assets for children whose FQN starts with this value.")
"Optional scope filter by parent glossary or glossary term FQN. "
+ "When provided, assets are returned only if the requested glossary term's FQN is within this parent hierarchy (for example, its FQN starts with the given value).")

Copilot uses AI. Check for mistakes.
@QueryParam("parent")
String parent) {
return Response.ok(repository.getGlossaryTermAssets(id, limit, offset, parent)).build();
}

@GET
Expand Down Expand Up @@ -908,8 +921,14 @@ public Response listGlossaryTermAssetsByName(
@DefaultValue("0")
@Min(0)
@QueryParam("offset")
int offset) {
return Response.ok(repository.getGlossaryTermAssetsByName(fqn, limit, offset)).build();
int offset,
@Parameter(
description =
"Filter by parent glossary or glossary term FQN. "
+ "When provided, only returns assets for children whose FQN starts with this value.")
Comment on lines +927 to +928
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

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

The parent query param description here says it "returns assets for children whose FQN starts with this value", but this endpoint returns assets for the requested term ({fqn}) and only uses parent as an additional scope filter. Please adjust the description to match the actual behavior so the OpenAPI docs are accurate.

Suggested change
"Filter by parent glossary or glossary term FQN. "
+ "When provided, only returns assets for children whose FQN starts with this value.")
"Optional scope filter by parent glossary or glossary term FQN. "
+ "Assets are always returned for the requested term `{fqn}`; when `parent` is provided, "
+ "only assets associated to that term within the specified parent hierarchy "
+ "(children whose FQN starts with this value) are included.")

Copilot uses AI. Check for mistakes.
@QueryParam("parent")
String parent) {
return Response.ok(repository.getGlossaryTermAssetsByName(fqn, limit, offset, parent)).build();
}

@PUT
Expand Down
Loading