-
Notifications
You must be signed in to change notification settings - Fork 2.1k
fix: server-side column tag filtering across all pages (#25063) #27334
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -132,6 +132,7 @@ | |||||||||||||||||||||||||||||||||
| import org.openmetadata.service.util.EntityUtil.RelationIncludes; | ||||||||||||||||||||||||||||||||||
| import org.openmetadata.service.util.FullyQualifiedName; | ||||||||||||||||||||||||||||||||||
| import org.openmetadata.service.util.RestUtil; | ||||||||||||||||||||||||||||||||||
| import org.openmetadata.service.util.TableUtil; | ||||||||||||||||||||||||||||||||||
| import org.openmetadata.service.util.ValidatorUtil; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| @Slf4j | ||||||||||||||||||||||||||||||||||
|
|
@@ -2785,6 +2786,32 @@ public ResultList<Column> getTableColumns( | |||||||||||||||||||||||||||||||||
| List<EntityReference> piiOwners, | ||||||||||||||||||||||||||||||||||
| Authorizer authorizer, | ||||||||||||||||||||||||||||||||||
| SecurityContext securityContext) { | ||||||||||||||||||||||||||||||||||
| return getTableColumns( | ||||||||||||||||||||||||||||||||||
| tableId, | ||||||||||||||||||||||||||||||||||
| limit, | ||||||||||||||||||||||||||||||||||
| offset, | ||||||||||||||||||||||||||||||||||
| fieldsParam, | ||||||||||||||||||||||||||||||||||
| include, | ||||||||||||||||||||||||||||||||||
| sortBy, | ||||||||||||||||||||||||||||||||||
| sortOrder, | ||||||||||||||||||||||||||||||||||
| null, | ||||||||||||||||||||||||||||||||||
| piiOwners, | ||||||||||||||||||||||||||||||||||
| authorizer, | ||||||||||||||||||||||||||||||||||
| securityContext); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| public ResultList<Column> getTableColumns( | ||||||||||||||||||||||||||||||||||
| UUID tableId, | ||||||||||||||||||||||||||||||||||
| int limit, | ||||||||||||||||||||||||||||||||||
| int offset, | ||||||||||||||||||||||||||||||||||
| String fieldsParam, | ||||||||||||||||||||||||||||||||||
| Include include, | ||||||||||||||||||||||||||||||||||
| String sortBy, | ||||||||||||||||||||||||||||||||||
| String sortOrder, | ||||||||||||||||||||||||||||||||||
| String tag, | ||||||||||||||||||||||||||||||||||
| List<EntityReference> piiOwners, | ||||||||||||||||||||||||||||||||||
| Authorizer authorizer, | ||||||||||||||||||||||||||||||||||
| SecurityContext securityContext) { | ||||||||||||||||||||||||||||||||||
| Table table = find(tableId, include); | ||||||||||||||||||||||||||||||||||
| return getTableColumnsInternal( | ||||||||||||||||||||||||||||||||||
| table, | ||||||||||||||||||||||||||||||||||
|
|
@@ -2794,6 +2821,7 @@ public ResultList<Column> getTableColumns( | |||||||||||||||||||||||||||||||||
| include, | ||||||||||||||||||||||||||||||||||
| sortBy, | ||||||||||||||||||||||||||||||||||
| sortOrder, | ||||||||||||||||||||||||||||||||||
| tag, | ||||||||||||||||||||||||||||||||||
| piiOwners, | ||||||||||||||||||||||||||||||||||
| authorizer, | ||||||||||||||||||||||||||||||||||
| securityContext); | ||||||||||||||||||||||||||||||||||
|
|
@@ -2858,6 +2886,32 @@ public ResultList<Column> getTableColumnsByFQN( | |||||||||||||||||||||||||||||||||
| List<EntityReference> piiOwners, | ||||||||||||||||||||||||||||||||||
| Authorizer authorizer, | ||||||||||||||||||||||||||||||||||
| SecurityContext securityContext) { | ||||||||||||||||||||||||||||||||||
| return getTableColumnsByFQN( | ||||||||||||||||||||||||||||||||||
| fqn, | ||||||||||||||||||||||||||||||||||
| limit, | ||||||||||||||||||||||||||||||||||
| offset, | ||||||||||||||||||||||||||||||||||
| fieldsParam, | ||||||||||||||||||||||||||||||||||
| include, | ||||||||||||||||||||||||||||||||||
| sortBy, | ||||||||||||||||||||||||||||||||||
| sortOrder, | ||||||||||||||||||||||||||||||||||
| null, | ||||||||||||||||||||||||||||||||||
| piiOwners, | ||||||||||||||||||||||||||||||||||
| authorizer, | ||||||||||||||||||||||||||||||||||
| securityContext); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| public ResultList<Column> getTableColumnsByFQN( | ||||||||||||||||||||||||||||||||||
| String fqn, | ||||||||||||||||||||||||||||||||||
| int limit, | ||||||||||||||||||||||||||||||||||
| int offset, | ||||||||||||||||||||||||||||||||||
| String fieldsParam, | ||||||||||||||||||||||||||||||||||
| Include include, | ||||||||||||||||||||||||||||||||||
| String sortBy, | ||||||||||||||||||||||||||||||||||
| String sortOrder, | ||||||||||||||||||||||||||||||||||
| String tag, | ||||||||||||||||||||||||||||||||||
| List<EntityReference> piiOwners, | ||||||||||||||||||||||||||||||||||
| Authorizer authorizer, | ||||||||||||||||||||||||||||||||||
| SecurityContext securityContext) { | ||||||||||||||||||||||||||||||||||
| Table table = findByName(fqn, include); | ||||||||||||||||||||||||||||||||||
| return getTableColumnsInternal( | ||||||||||||||||||||||||||||||||||
| table, | ||||||||||||||||||||||||||||||||||
|
|
@@ -2867,6 +2921,7 @@ public ResultList<Column> getTableColumnsByFQN( | |||||||||||||||||||||||||||||||||
| include, | ||||||||||||||||||||||||||||||||||
| sortBy, | ||||||||||||||||||||||||||||||||||
| sortOrder, | ||||||||||||||||||||||||||||||||||
| tag, | ||||||||||||||||||||||||||||||||||
| piiOwners, | ||||||||||||||||||||||||||||||||||
| authorizer, | ||||||||||||||||||||||||||||||||||
| securityContext); | ||||||||||||||||||||||||||||||||||
|
|
@@ -2880,6 +2935,7 @@ private ResultList<Column> getTableColumnsInternal( | |||||||||||||||||||||||||||||||||
| Include include, | ||||||||||||||||||||||||||||||||||
| String sortBy, | ||||||||||||||||||||||||||||||||||
| String sortOrder, | ||||||||||||||||||||||||||||||||||
| String tag, | ||||||||||||||||||||||||||||||||||
| List<EntityReference> piiOwners, | ||||||||||||||||||||||||||||||||||
| Authorizer authorizer, | ||||||||||||||||||||||||||||||||||
| SecurityContext securityContext) { | ||||||||||||||||||||||||||||||||||
|
|
@@ -2889,8 +2945,26 @@ private ResultList<Column> getTableColumnsInternal( | |||||||||||||||||||||||||||||||||
| return new ResultList<>(new ArrayList<>(), "0", String.valueOf(offset + limit), 0); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| boolean hasTagFilter = tag != null && !tag.trim().isEmpty(); | ||||||||||||||||||||||||||||||||||
| boolean needsTags = fieldsParam != null && fieldsParam.contains("tags"); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // When filtering by tag, populate tags on ALL columns BEFORE filtering | ||||||||||||||||||||||||||||||||||
| if (hasTagFilter && needsTags) { | ||||||||||||||||||||||||||||||||||
| populateEntityFieldTags(entityType, allColumns, table.getFullyQualifiedName(), true); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+2948
to
+2954
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🚨 Bug: Tag filter silently returns empty when
|
||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| List<Column> workingColumns = new ArrayList<>(allColumns); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // Apply tag filter if provided | ||||||||||||||||||||||||||||||||||
| if (hasTagFilter) { | ||||||||||||||||||||||||||||||||||
| Set<String> tagFQNs = Set.of(tag.split(",")); | ||||||||||||||||||||||||||||||||||
| workingColumns = | ||||||||||||||||||||||||||||||||||
| workingColumns.stream() | ||||||||||||||||||||||||||||||||||
| .filter(column -> columnMatchesAnyTag(column, tagFQNs)) | ||||||||||||||||||||||||||||||||||
| .collect(Collectors.toCollection(ArrayList::new)); | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+2960
to
+2964
|
||||||||||||||||||||||||||||||||||
| Set<String> tagFQNs = Set.of(tag.split(",")); | |
| workingColumns = | |
| workingColumns.stream() | |
| .filter(column -> columnMatchesAnyTag(column, tagFQNs)) | |
| .collect(Collectors.toCollection(ArrayList::new)); | |
| Set<String> tagFQNs = | |
| Arrays.stream(tag.split(",")) | |
| .map(String::trim) | |
| .filter(Predicate.not(String::isBlank)) | |
| .collect(Collectors.toSet()); | |
| if (!tagFQNs.isEmpty()) { | |
| workingColumns = | |
| workingColumns.stream() | |
| .filter(column -> columnMatchesAnyTag(column, tagFQNs)) | |
| .collect(Collectors.toCollection(ArrayList::new)); | |
| } |
Copilot
AI
Apr 14, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Set.of(tag.split(",")) will throw IllegalArgumentException if the query param contains duplicate tag FQNs (e.g., tag=a,b,a), and it also preserves leading/trailing whitespace (e.g., "PII.Sensitive, Tier.Tier1"). Parse the CSV by trimming each entry, dropping blanks, and collecting via Collectors.toSet() (or similar) so duplicates/whitespace don’t cause 500s.
Copilot
AI
Apr 14, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as the non-search endpoint: tag filtering is gated on needsTags, so tag queries can silently return no results when fields doesn’t include tags (or *). The server-side filter should populate tags whenever tag is provided, independent of the requested return fields.
| // When filtering by tag, populate tags on ALL columns BEFORE filtering | |
| if (hasTagFilter && needsTags) { | |
| // Populate tags when they are needed either for server-side filtering or for the response. | |
| if (hasTagFilter || needsTags) { |
Copilot
AI
Apr 14, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hasTagFilter is currently gated by needsTags before calling populateEntityFieldTags(...). If a client uses tag=... but does not request tags (or requests fields=*), columnMatchesAnyTag() will evaluate against null/unpopulated tags and can filter out everything. Tag filtering should work regardless of which fields are requested; consider always populating tags when tag is present (and optionally stripping tags from the response when not requested).
| // When filtering by tag, populate tags on ALL columns BEFORE filtering | |
| if (hasTagFilter && needsTags) { | |
| // Populate tags before filtering or when tags are explicitly requested in the response | |
| if (hasTagFilter || needsTags) { |
Copilot
AI
Apr 14, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same parsing issue here: Set.of(tag.split(",")) can throw on duplicates and won’t trim whitespace. Use a trim + blank-filter + toSet() approach so user-supplied query params can’t trigger 500s.
Copilot
AI
Apr 14, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Set.of(tag.split(",")) is also used here and has the same failure mode: duplicate values will throw at runtime, and whitespace/empty segments won’t be normalized. Parse with trimming + blank filtering + Collectors.toSet() for robustness.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hasTagFiltercurrently only triggerspopulateEntityFieldTags(...)whenneedsTagsis true (i.e., when the caller requestedtags). If a client calls the endpoint withtag=...but withoutfieldsincludingtags(or withfields=*), filtering will run againstcolumn.getTags()that were never populated and can incorrectly return 0 columns (and may also break PII masking whenprofileis requested). Consider always populating tags whentagis provided (for filtering purposes), and iftagswere not requested, strip them from the response after filtering/pagination to preserve the fields contract.