Skip to content

Support parent-scoped asset counts and assets listing for glossary terms#26801

Open
sonika-shah wants to merge 2 commits intomainfrom
fix-3241
Open

Support parent-scoped asset counts and assets listing for glossary terms#26801
sonika-shah wants to merge 2 commits intomainfrom
fix-3241

Conversation

@sonika-shah
Copy link
Copy Markdown
Collaborator

@sonika-shah sonika-shah commented Mar 26, 2026

Describe your changes:

Fixes #3241

  • Added optional parent query parameter to three glossary term asset endpoints: GET /assets/counts, GET /{id}/assets, and GET /name/{fqn}/assets
  • When parent is provided, results are scoped to only glossary terms whose FQN is a child of the given parent, using FullyQualifiedName.isParent() instead of hardcoded string logic
  • Enables efficient retrieval of aggregated asset counts for a specific glossary or glossary term hierarchy without fetching all terms globally

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 March 26, 2026 12:05
@sonika-shah sonika-shah added safe to test Add this label to run secure Github workflows on PRs backend labels Mar 26, 2026
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

Adds an optional parent scope filter to glossary-term asset count and asset listing APIs, enabling callers (e.g., UI) to request results limited to a specific glossary or glossary-term subtree.

Changes:

  • Add parent query parameter to /v1/glossaryTerms/assets/counts, /v1/glossaryTerms/{id}/assets, and /v1/glossaryTerms/name/{fqn}/assets.
  • Extend GlossaryTermRepository methods to accept parent and apply parent-scope filtering.
  • Add integration tests covering the new parent query parameter on these endpoints.

Reviewed changes

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

File Description
openmetadata-service/src/main/java/org/openmetadata/service/resources/glossary/GlossaryTermResource.java Exposes the new parent query parameter on glossary term asset count/listing endpoints.
openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/GlossaryTermRepository.java Implements parent-scope filtering for asset counts and asset listing queries.
openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/GlossaryTermResourceIT.java Adds integration tests intended to validate parent-filtered asset counts and asset listing behavior.

Comment on lines 211 to +218
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());
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.
Comment on lines +879 to +880
"Filter by parent glossary or glossary term FQN. "
+ "When provided, only returns assets for children whose FQN starts with this value.")
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.
Comment on lines +927 to +928
"Filter by parent glossary or glossary term FQN. "
+ "When provided, only returns assets for children whose FQN starts with this value.")
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.
Comment on lines +2618 to +2627
// 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);
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.
Comment on lines +2665 to +2679
// 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);
}
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.
Comment on lines +2706 to +2715
// 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);
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.
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 26, 2026

OpenMetadata Service New-Code Coverage

FAIL. Required changed-line coverage: 90.00% overall and per touched production file.

  • Overall executable changed lines: 2/28 covered (7.14%)
  • Missed executable changed lines: 26
  • Non-executable changed lines ignored by JaCoCo: 35
  • Changed production files: 4

Files below threshold:

  • openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/GlossaryTermRepository.java: 0/13 covered (0.00%), uncovered lines 177, 178, 179, 182, 183, 184, 190, 200, 212, 213, 214, 215, +1 more
  • openmetadata-service/src/main/java/org/openmetadata/service/search/InheritedFieldEntitySearch.java: 0/9 covered (0.00%), uncovered lines 249, 250, 251, 252, 253, 254, 255, 256, 257
  • openmetadata-service/src/main/java/org/openmetadata/service/resources/glossary/GlossaryTermResource.java: 0/3 covered (0.00%), uncovered lines 447, 883, 931
  • openmetadata-service/src/main/java/org/openmetadata/service/search/QueryFilterBuilder.java: 2/3 covered (66.67%), uncovered lines 139
File Covered Missed Executable Non-exec Coverage Uncovered lines
openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/GlossaryTermRepository.java 0 13 13 7 0.00% 177, 178, 179, 182, 183, 184, 190, 200, 212, 213, 214, 215, +1 more
openmetadata-service/src/main/java/org/openmetadata/service/search/InheritedFieldEntitySearch.java 0 9 9 4 0.00% 249, 250, 251, 252, 253, 254, 255, 256, 257
openmetadata-service/src/main/java/org/openmetadata/service/resources/glossary/GlossaryTermResource.java 0 3 3 22 0.00% 447, 883, 931
openmetadata-service/src/main/java/org/openmetadata/service/search/QueryFilterBuilder.java 2 1 3 2 66.67% 139

Only changed executable lines under openmetadata-service/src/main/java are counted. Test files, comments, imports, and non-executable lines are excluded.

Comment on lines +178 to +184
boolean useParentScope =
parent != null && !parent.isEmpty() && FullyQualifiedName.isParent(fqn, parent);

InheritedFieldQuery query =
InheritedFieldQuery.forGlossaryTerm(term.getFullyQualifiedName(), offset, limit);
useParentScope
? InheritedFieldQuery.forGlossaryTermChildren(parent, offset, limit)
: InheritedFieldQuery.forGlossaryTerm(fqn, offset, limit);
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: Parent filter silently ignored when term is not under parent

The old code had a guard clause: if parent is provided but the term's FQN is not a child of parent, return an empty result list. This guard was removed in the refactor. Now when parent is provided but FullyQualifiedName.isParent(fqn, parent) is false, useParentScope is false and the code falls through to forGlossaryTerm(fqn, ...), returning the term's assets as if no parent filter was specified at all.

This is a behavioral regression — callers that pass a parent parameter expect it to act as a scope filter, but now an unrelated term's assets are returned instead of an empty list.

Suggested fix:

String fqn = term.getFullyQualifiedName();
boolean parentProvided = parent != null && !parent.isEmpty();
boolean isChild = parentProvided && FullyQualifiedName.isParent(fqn, parent);

if (parentProvided && !isChild) {
  return new ResultList<>(new ArrayList<>(), null, null, 0);
}

InheritedFieldQuery query =
    isChild
        ? InheritedFieldQuery.forGlossaryTermChildren(parent, offset, limit)
        : InheritedFieldQuery.forGlossaryTerm(fqn, offset, limit);

Was this helpful? React with 👍 / 👎 | Reply gitar fix to apply this suggestion

@gitar-bot
Copy link
Copy Markdown

gitar-bot bot commented Mar 26, 2026

Code Review ⚠️ Changes requested 0 resolved / 2 findings

Adds parent-scoped asset counts and assets listing for glossary terms, but getAllGlossaryTermsWithAssetsCount loads all terms into memory before filtering by parent, and getGlossaryTermAssets fetches from the database before checking parent scope—both causing unnecessary resource consumption and silently ignoring parent filters when terms fall outside the specified parent scope.

⚠️ Performance: getAllGlossaryTermsWithAssetsCount loads all terms before filtering

📄 openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/GlossaryTermRepository.java:211-218 📄 openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/GlossaryTermRepository.java:170-176

In getAllGlossaryTermsWithAssetsCount, listAll() at line 212 fetches every glossary term in the system into memory, and only then applies the parent filter at line 214-218. When a parent is provided, this loads potentially thousands of terms only to discard most of them. Since each surviving term also triggers an individual search query (line 224-235), the combination of full table scan + N search calls can be expensive.

Consider pushing the parent filter into the listAll query (e.g., via a ListFilter that filters by FQN prefix) so only matching terms are loaded, reducing both memory usage and the number of search queries.

⚠️ Bug: Parent filter silently ignored when term is not under parent

📄 openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/GlossaryTermRepository.java:178-184

The old code had a guard clause: if parent is provided but the term's FQN is not a child of parent, return an empty result list. This guard was removed in the refactor. Now when parent is provided but FullyQualifiedName.isParent(fqn, parent) is false, useParentScope is false and the code falls through to forGlossaryTerm(fqn, ...), returning the term's assets as if no parent filter was specified at all.

This is a behavioral regression — callers that pass a parent parameter expect it to act as a scope filter, but now an unrelated term's assets are returned instead of an empty list.

Suggested fix
String fqn = term.getFullyQualifiedName();
boolean parentProvided = parent != null && !parent.isEmpty();
boolean isChild = parentProvided && FullyQualifiedName.isParent(fqn, parent);

if (parentProvided && !isChild) {
  return new ResultList<>(new ArrayList<>(), null, null, 0);
}

InheritedFieldQuery query =
    isChild
        ? InheritedFieldQuery.forGlossaryTermChildren(parent, offset, limit)
        : InheritedFieldQuery.forGlossaryTerm(fqn, offset, limit);
🤖 Prompt for agents
Code Review: Adds parent-scoped asset counts and assets listing for glossary terms, but `getAllGlossaryTermsWithAssetsCount` loads all terms into memory before filtering by parent, and `getGlossaryTermAssets` fetches from the database before checking parent scope—both causing unnecessary resource consumption and silently ignoring parent filters when terms fall outside the specified parent scope.

1. ⚠️ Performance: getAllGlossaryTermsWithAssetsCount loads all terms before filtering
   Files: openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/GlossaryTermRepository.java:211-218, openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/GlossaryTermRepository.java:170-176

   In `getAllGlossaryTermsWithAssetsCount`, `listAll()` at line 212 fetches every glossary term in the system into memory, and only then applies the `parent` filter at line 214-218. When a `parent` is provided, this loads potentially thousands of terms only to discard most of them. Since each surviving term also triggers an individual search query (line 224-235), the combination of full table scan + N search calls can be expensive.
   
   Consider pushing the parent filter into the `listAll` query (e.g., via a `ListFilter` that filters by FQN prefix) so only matching terms are loaded, reducing both memory usage and the number of search queries.

2. ⚠️ Bug: Parent filter silently ignored when term is not under parent
   Files: openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/GlossaryTermRepository.java:178-184

   The old code had a guard clause: if `parent` is provided but the term's FQN is not a child of `parent`, return an empty result list. This guard was removed in the refactor. Now when `parent` is provided but `FullyQualifiedName.isParent(fqn, parent)` is false, `useParentScope` is false and the code falls through to `forGlossaryTerm(fqn, ...)`, returning the term's assets as if no `parent` filter was specified at all.
   
   This is a behavioral regression — callers that pass a `parent` parameter expect it to act as a scope filter, but now an unrelated term's assets are returned instead of an empty list.

   Suggested fix:
   String fqn = term.getFullyQualifiedName();
   boolean parentProvided = parent != null && !parent.isEmpty();
   boolean isChild = parentProvided && FullyQualifiedName.isParent(fqn, parent);
   
   if (parentProvided && !isChild) {
     return new ResultList<>(new ArrayList<>(), null, null, 0);
   }
   
   InheritedFieldQuery query =
       isChild
           ? InheritedFieldQuery.forGlossaryTermChildren(parent, offset, limit)
           : InheritedFieldQuery.forGlossaryTerm(fqn, offset, limit);

Options

Auto-apply is off → Gitar will not commit updates to this branch.
Display: compact → Showing less information.

Comment with these commands to change:

Auto-apply Compact
gitar auto-apply:on         
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Gitar

@sonarqubecloud
Copy link
Copy Markdown

@github-actions
Copy link
Copy Markdown
Contributor

🟡 Playwright Results — all passed (17 flaky)

✅ 3397 passed · ❌ 0 failed · 🟡 17 flaky · ⏭️ 217 skipped

Shard Passed Failed Flaky Skipped
🟡 Shard 1 452 0 3 2
🟡 Shard 2 601 0 2 32
🟡 Shard 3 606 0 2 28
🟡 Shard 4 599 0 4 47
🟡 Shard 5 586 0 1 67
🟡 Shard 6 553 0 5 41
🟡 17 flaky test(s) (passed on retry)
  • Features/CustomizeDetailPage.spec.ts › API Endpoint - customization should work (shard 1, 1 retry)
  • Features/Pagination.spec.ts › should test Database Schemas complete flow with search (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/BulkImport.spec.ts › Table (shard 2, 1 retry)
  • Features/Permissions/GlossaryPermissions.spec.ts › Team-based permissions work correctly (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)
  • Pages/Customproperties-part2.spec.ts › entityReferenceList shows item count, scrollable list, no expand toggle (shard 4, 1 retry)
  • Pages/DataContractsSemanticRules.spec.ts › Validate Description Rule Is_Not_Set (shard 4, 1 retry)
  • Pages/Domains.spec.ts › Follow/unfollow subdomain and create nested sub domain (shard 4, 1 retry)
  • Pages/Entity.spec.ts › Tag Add, Update and Remove (shard 4, 1 retry)
  • Pages/EntityDataConsumer.spec.ts › Tier Add, Update and Remove (shard 5, 1 retry)
  • Pages/InputOutputPorts.spec.ts › Input port button visible, output port button hidden when no assets (shard 6, 1 retry)
  • Pages/ODCSImportExport.spec.ts › Multi-object ODCS contract - object selector shows all schema objects (shard 6, 1 retry)
  • Pages/ServiceEntity.spec.ts › Inactive Announcement create & delete (shard 6, 1 retry)
  • Pages/UserDetails.spec.ts › Admin user can get all the roles hierarchy and edit roles (shard 6, 1 retry)
  • Pages/Users.spec.ts › Permissions for table details page for Data Consumer (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

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

Labels

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

2 participants