Fixes #25063: Add server-side tag filtering for table columns via ColumnResource API#27331
Conversation
…lumns via ColumnResource API
|
Hi there 👋 Thanks for your contribution! The OpenMetadata team will review the PR shortly! Once it has been labeled as Let us know if you need any help! |
...adata-ui/src/main/resources/ui/src/components/Database/SchemaTable/SchemaTable.component.tsx
Show resolved
Hide resolved
...adata-ui/src/main/resources/ui/src/components/Database/SchemaTable/SchemaTable.component.tsx
Show resolved
Hide resolved
openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/TableRepository.java
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Pull request overview
This PR aims to fix Issue #25063 by moving column tag filtering from client-side (current-page only) to server-side filtering so tag filters work across all table columns before pagination.
Changes:
- Added a
tagsquery parameter to the backend table column search endpoints and implemented tag-based filtering inTableRepository.searchTableColumnsInternal. - Updated UI table column fetch/search requests to pass a
tagsparam and introduced abuildTagsParamhelper. - Added unit tests for tag-param construction and (partial) API integration behavior.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| openmetadata-ui/src/main/resources/ui/src/rest/tableAPI.ts | Adds tags to table column request parameter typings. |
| openmetadata-ui/src/main/resources/ui/src/components/Database/SchemaTable/SchemaTable.component.tsx | Adds buildTagsParam and attempts to send tags to backend; updates hook dependencies. |
| openmetadata-ui/src/main/resources/ui/src/components/Database/SchemaTable/SchemaTable.test.tsx | Adds tests for tag param building and API-call expectations. |
| openmetadata-service/src/main/java/org/openmetadata/service/resources/databases/TableResource.java | Adds tags query param to /columns/search endpoints. |
| openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/TableRepository.java | Implements server-side tag filtering inside column search logic. |
| const tagFilter = useMemo(() => { | ||
| const tags = getAllTags(tableColumns); | ||
|
|
||
| return groupBy(uniqBy(tags, 'value'), (tag) => tag.source) as Record< | ||
| TagSource, | ||
| TagFilterOptions[] | ||
| >; | ||
| }, [tableColumns]); |
There was a problem hiding this comment.
tagFilter is derived from tableColumns via getAllTags(...), i.e., it's the set of available tag filter options for the currently loaded page. Using it to build the tags query param will send all tags from the current page (and will change as pagination changes), rather than the user’s selected tag filters. This breaks the intended behavior and can also cause repeated refetching because tagFilter changes whenever tableColumns changes. Introduce separate state for the active/selected tag filters (e.g., from AntD Table onChange filters or a dedicated filter state), keep tagFilter only for options, and build the server tags param from the selected values instead.
There was a problem hiding this comment.
Fixed — Removed duplicate tags?: string from
SearchTableColumnsParams since it already inherits from
GetTableColumnsParams.
| useEffect(() => { | ||
| if (searchText) { | ||
| searchTableColumns(searchText, currentPage, sortBy, sortOrder); | ||
| } | ||
| }, [searchText, currentPage, searchTableColumns, sortBy, sortOrder]); | ||
| }, [searchText, currentPage, searchTableColumns, sortBy, sortOrder, tagFilter]); | ||
|
|
||
| useEffect(() => { | ||
| if (searchText) { | ||
| return; | ||
| } | ||
| fetchTableColumns(currentPage, sortBy, sortOrder); | ||
| }, [ | ||
| tableFqn, | ||
| pageSize, | ||
| currentPage, | ||
| searchText, | ||
| fetchTableColumns, | ||
| sortBy, | ||
| sortOrder, | ||
| tagFilter, | ||
| ]); |
There was a problem hiding this comment.
Including tagFilter in these effects’ dependency arrays will likely create a fetch loop: fetchTableColumns/searchTableColumns update tableColumns → tagFilter recomputes (depends on tableColumns) → effect reruns → triggers another fetch, etc. The dependency for refetching should be the selected tag filters (a stable piece of state), not the derived tag option list computed from fetched data.
There was a problem hiding this comment.
Fixed — fetchTableColumns now calls searchTableColumnsByFQN
with q: '' when tag filters are active, ensuring server-side
tag filtering applies in both search and non-search scenarios.
| include?: Include; | ||
| sortBy?: 'name' | 'ordinalPosition'; | ||
| sortOrder?: 'asc' | 'desc'; | ||
| tags?: string; |
There was a problem hiding this comment.
Adding tags?: string to GetTableColumnsParams implies the /tables/.../columns endpoints support tag filtering, but TableResource#getTableColumns/getTableColumnsByFQN currently do not accept a tags query param. This can mislead callers and silently do nothing at runtime. Either implement tags on the non-search column list endpoints, or restrict tags to SearchTableColumnsParams only and ensure the UI calls the /columns/search endpoints when tag filters are active.
| tags?: string; |
There was a problem hiding this comment.
Fixed — Removed unused TagFilterOptions and TagSource imports.
buildTagsParam now accepts Record<string, string[]> directly,
eliminating the need for these type imports in tests.
| if (tags != null && !tags.trim().isEmpty()) { | ||
| String[] tagFQNs = tags.split(","); | ||
| matchingColumns = | ||
| new ArrayList<>( | ||
| matchingColumns.stream() | ||
| .filter( | ||
| column -> | ||
| column.getTags() != null | ||
| && column.getTags().stream() | ||
| .anyMatch( | ||
| tag -> | ||
| Arrays.stream(tagFQNs) | ||
| .anyMatch( | ||
| tagFQN -> tag.getTagFQN().equals(tagFQN.trim())))) | ||
| .toList()); |
There was a problem hiding this comment.
Current tag filtering iterates Arrays.stream(tagFQNs) for every tag on every column, which is avoidable overhead for wide tables. Consider normalizing the incoming tags once (trim, drop empties) into a Set<String> and then checking membership (set.contains(tag.getTagFQN())) during the stream filter. This keeps the same OR semantics but reduces per-column work.
| String sortOrder, | ||
| @Parameter( | ||
| description = "Comma-separated list of tag FQNs to filter columns by", | ||
| schema = @Schema(type = "string")) | ||
| @QueryParam("tags") | ||
| String tags) { |
There was a problem hiding this comment.
tags is only wired into the /columns/search endpoints here, but the UI’s initial (non-search) load uses /tables/.../columns. Without adding tags to the non-search /columns endpoints (or updating the UI to call /columns/search when tag filters are active), server-side tag filtering won’t apply in the common no-search-text case.
| const response = await getTableColumnsByFQN(tableFqn, { | ||
| limit: pageSize, | ||
| offset: offset, | ||
| fields: 'tags,customMetrics,extension', | ||
| sortBy: sortByParam, | ||
| sortOrder: sortOrderParam, | ||
| ...(tagsParam ? { tags: tagsParam } : {}), | ||
| }); |
There was a problem hiding this comment.
getTableColumnsByFQN calls /tables/name/{fqn}/columns, but the backend getTableColumnsByFQN endpoint in TableResource does not accept a tags query parameter (only the /columns/search endpoints do). Passing tags here will be ignored server-side, so tag filtering won’t work when there’s no search query. Either add tags support to the non-search /columns endpoints (and their repository methods), or switch the UI to call the /columns/search endpoint when tag filters are active (even if q is empty).
| const response = await getTableColumnsByFQN(tableFqn, { | |
| limit: pageSize, | |
| offset: offset, | |
| fields: 'tags,customMetrics,extension', | |
| sortBy: sortByParam, | |
| sortOrder: sortOrderParam, | |
| ...(tagsParam ? { tags: tagsParam } : {}), | |
| }); | |
| const response = tagsParam | |
| ? await searchTableColumnsByFQN(tableFqn, { | |
| q: '', | |
| limit: pageSize, | |
| offset: offset, | |
| fields: 'tags,customMetrics,extension', | |
| sortBy: sortByParam, | |
| sortOrder: sortOrderParam, | |
| tags: tagsParam, | |
| }) | |
| : await getTableColumnsByFQN(tableFqn, { | |
| limit: pageSize, | |
| offset: offset, | |
| fields: 'tags,customMetrics,extension', | |
| sortBy: sortByParam, | |
| sortOrder: sortOrderParam, | |
| }); |
| import { getTableColumnsByFQN, searchTableColumnsByFQN } from '../../../rest/tableAPI'; | ||
| import { DEFAULT_ENTITY_PERMISSION } from '../../../utils/PermissionsUtils'; | ||
| import SchemaTable from './SchemaTable.component'; | ||
| import SchemaTable, { buildTagsParam } from './SchemaTable.component'; | ||
| import { TagFilterOptions } from 'Models'; | ||
| import { TagSource } from '../../../generated/type/schema'; | ||
| import { getAllTags, searchTagInData } from '../../../utils/TableTags/TableTags.utils'; | ||
|
|
There was a problem hiding this comment.
The new test suite imports getAllTags/searchTagInData but never uses them, which will trip lint rules like no-unused-vars in CI. Remove the unused imports (or use them if needed for assertions). Also consider moving the Models import up with other external imports to match the repo’s organized-imports expectations.
| beforeEach(() => { | ||
| jest.clearAllMocks(); | ||
|
|
||
| // Reset core mocks back to default implementation for TEST 1 integration | ||
| (getTableColumnsByFQN as jest.Mock).mockResolvedValue({ | ||
| data: mockColumns, | ||
| paging: { total: mockColumns.length }, | ||
| }); | ||
| (searchTableColumnsByFQN as jest.Mock).mockResolvedValue({ | ||
| data: mockColumns, | ||
| paging: { total: mockColumns.length }, | ||
| }); | ||
| }); | ||
|
|
||
| it('TEST 1 — No tags param sent when no filter active', async () => { | ||
|
|
||
| await act(async () => { | ||
| render(<SchemaTable />, { wrapper: MemoryRouter }); | ||
| }); | ||
|
|
||
| expect(getTableColumnsByFQN).toHaveBeenCalledWith( | ||
| MOCK_TABLE.fullyQualifiedName, | ||
| expect.not.objectContaining({ tags: expect.anything() }) | ||
| ); | ||
| }); | ||
|
|
||
| it('TEST 2 — Classification filter', () => { | ||
| expect(buildTagsParam({ | ||
| Classification: [{ value: 'PII.Sensitive' }], | ||
| Glossary: [] | ||
| } as Record<TagSource, TagFilterOptions[]>)).toBe('PII.Sensitive'); | ||
| }); | ||
|
|
||
| it('TEST 3 — Glossary filter', () => { | ||
| expect(buildTagsParam({ | ||
| Classification: [], | ||
| Glossary: [{ value: 'Glossary.Term1' }] | ||
| } as Record<TagSource, TagFilterOptions[]>)).toBe('Glossary.Term1'); | ||
| }); | ||
|
|
||
| it('TEST 4 — Multiple tags comma-separated', () => { | ||
| expect(buildTagsParam({ | ||
| Classification: [{ value: 'PII.Sensitive' }], | ||
| Glossary: [{ value: 'Glossary.Term1' }] | ||
| } as Record<TagSource, TagFilterOptions[]>)).toBe('PII.Sensitive,Glossary.Term1'); | ||
| }); | ||
|
|
||
| it('TEST 5 — Empty filter returns undefined', () => { | ||
| expect(buildTagsParam({ | ||
| Classification: [], | ||
| Glossary: [] | ||
| } as Record<TagSource, TagFilterOptions[]>)).toBeUndefined(); |
There was a problem hiding this comment.
TEST 1 — No tags param sent when no filter active currently passes because getAllTags is mocked with no return value, so tagFilter is empty and buildTagsParam(tagFilter) becomes undefined. This doesn’t exercise the real-world case where columns have tags but the user has not selected any filters (which is the critical scenario for Issue #25063). Update the test to simulate columns that contain tags and verify that the API call only includes tags when the user actually applies a tag filter (once the component tracks selected filters separately).
| beforeEach(() => { | |
| jest.clearAllMocks(); | |
| // Reset core mocks back to default implementation for TEST 1 integration | |
| (getTableColumnsByFQN as jest.Mock).mockResolvedValue({ | |
| data: mockColumns, | |
| paging: { total: mockColumns.length }, | |
| }); | |
| (searchTableColumnsByFQN as jest.Mock).mockResolvedValue({ | |
| data: mockColumns, | |
| paging: { total: mockColumns.length }, | |
| }); | |
| }); | |
| it('TEST 1 — No tags param sent when no filter active', async () => { | |
| await act(async () => { | |
| render(<SchemaTable />, { wrapper: MemoryRouter }); | |
| }); | |
| expect(getTableColumnsByFQN).toHaveBeenCalledWith( | |
| MOCK_TABLE.fullyQualifiedName, | |
| expect.not.objectContaining({ tags: expect.anything() }) | |
| ); | |
| }); | |
| it('TEST 2 — Classification filter', () => { | |
| expect(buildTagsParam({ | |
| Classification: [{ value: 'PII.Sensitive' }], | |
| Glossary: [] | |
| } as Record<TagSource, TagFilterOptions[]>)).toBe('PII.Sensitive'); | |
| }); | |
| it('TEST 3 — Glossary filter', () => { | |
| expect(buildTagsParam({ | |
| Classification: [], | |
| Glossary: [{ value: 'Glossary.Term1' }] | |
| } as Record<TagSource, TagFilterOptions[]>)).toBe('Glossary.Term1'); | |
| }); | |
| it('TEST 4 — Multiple tags comma-separated', () => { | |
| expect(buildTagsParam({ | |
| Classification: [{ value: 'PII.Sensitive' }], | |
| Glossary: [{ value: 'Glossary.Term1' }] | |
| } as Record<TagSource, TagFilterOptions[]>)).toBe('PII.Sensitive,Glossary.Term1'); | |
| }); | |
| it('TEST 5 — Empty filter returns undefined', () => { | |
| expect(buildTagsParam({ | |
| Classification: [], | |
| Glossary: [] | |
| } as Record<TagSource, TagFilterOptions[]>)).toBeUndefined(); | |
| const taggedColumns = [ | |
| { | |
| ...mockColumns[0], | |
| tags: [ | |
| { | |
| tagFQN: 'PII.Sensitive', | |
| source: TagSource.Classification, | |
| }, | |
| ], | |
| }, | |
| ...mockColumns.slice(1), | |
| ] as Column[]; | |
| beforeEach(() => { | |
| jest.clearAllMocks(); | |
| // Reset core mocks back to a realistic integration scenario where | |
| // columns already contain tags, but no tag filter has been selected. | |
| (getTableColumnsByFQN as jest.Mock).mockResolvedValue({ | |
| data: taggedColumns, | |
| paging: { total: taggedColumns.length }, | |
| }); | |
| (searchTableColumnsByFQN as jest.Mock).mockResolvedValue({ | |
| data: taggedColumns, | |
| paging: { total: taggedColumns.length }, | |
| }); | |
| (getAllTags as jest.Mock).mockReturnValue({ | |
| Classification: [{ value: 'PII.Sensitive', label: 'PII.Sensitive' }], | |
| Glossary: [], | |
| } as Record<TagSource, TagFilterOptions[]>); | |
| (searchTagInData as jest.Mock).mockReturnValue(taggedColumns); | |
| }); | |
| it('TEST 1 — No tags param sent when no filter active', async () => { | |
| await act(async () => { | |
| render(<SchemaTable />, { wrapper: MemoryRouter }); | |
| }); | |
| expect(getAllTags).toHaveBeenCalled(); | |
| expect(getTableColumnsByFQN).toHaveBeenCalled(); | |
| const [, requestParams] = (getTableColumnsByFQN as jest.Mock).mock.calls[0]; | |
| expect(requestParams).not.toHaveProperty('tags'); | |
| }); | |
| it('TEST 2 — Classification filter', () => { | |
| expect( | |
| buildTagsParam({ | |
| Classification: [{ value: 'PII.Sensitive' }], | |
| Glossary: [], | |
| } as Record<TagSource, TagFilterOptions[]>) | |
| ).toBe('PII.Sensitive'); | |
| }); | |
| it('TEST 3 — Glossary filter', () => { | |
| expect( | |
| buildTagsParam({ | |
| Classification: [], | |
| Glossary: [{ value: 'Glossary.Term1' }], | |
| } as Record<TagSource, TagFilterOptions[]>) | |
| ).toBe('Glossary.Term1'); | |
| }); | |
| it('TEST 4 — Multiple tags comma-separated', () => { | |
| expect( | |
| buildTagsParam({ | |
| Classification: [{ value: 'PII.Sensitive' }], | |
| Glossary: [{ value: 'Glossary.Term1' }], | |
| } as Record<TagSource, TagFilterOptions[]>) | |
| ).toBe('PII.Sensitive,Glossary.Term1'); | |
| }); | |
| it('TEST 5 — Empty filter returns undefined', () => { | |
| expect( | |
| buildTagsParam({ | |
| Classification: [], | |
| Glossary: [], | |
| } as Record<TagSource, TagFilterOptions[]>) | |
| ).toBeUndefined(); |
|
|
||
| export interface SearchTableColumnsParams extends GetTableColumnsParams { | ||
| q?: string; // Search query | ||
| tags?: string; |
There was a problem hiding this comment.
SearchTableColumnsParams already extends GetTableColumnsParams, which now includes tags?: string. Re-declaring tags?: string here is redundant and can lead to confusion about which params are supported. Remove the duplicate tags property from SearchTableColumnsParams (or, if only search should support tags, keep it only on the search params and not on GetTableColumnsParams).
| tags?: string; |
… O(n) tag lookup, API routing
|
Hi there 👋 Thanks for your contribution! The OpenMetadata team will review the PR shortly! Once it has been labeled as Let us know if you need any help! |
...adata-ui/src/main/resources/ui/src/components/Database/SchemaTable/SchemaTable.component.tsx
Show resolved
Hide resolved
|
Hi @harshach @PubChimps 👋 All review comments from Gitar Bot and GitHub Copilot have been ✅ Infinite render loop fixed — separate activeTagFilter state Could you please add the safe-to-test label so CI can run?
|
|
Hi there 👋 Thanks for your contribution! The OpenMetadata team will review the PR shortly! Once it has been labeled as Let us know if you need any help! |
| export type GetTableColumnsParams = { | ||
| limit?: number; | ||
| offset?: number; | ||
| fields?: string; | ||
| include?: Include; | ||
| sortBy?: 'name' | 'ordinalPosition'; | ||
| sortOrder?: 'asc' | 'desc'; | ||
| tags?: string; | ||
| }; |
There was a problem hiding this comment.
tags was added to GetTableColumnsParams, but the non-search /tables/{id}/columns and /tables/name/{fqn}/columns endpoints don’t accept a tags query param (they’ll ignore it). This type change can mislead callers into thinking tag filtering works on the list endpoints. Either (a) implement tags on the list endpoints too, or (b) move tags to SearchTableColumnsParams only (or a separate type) so only /columns/search advertises support.
| const tagsParam = buildTagsParam(activeTagFilter); | ||
|
|
||
| if (tagsParam) { | ||
| const response = await searchTableColumnsByFQN(tableFqn, { | ||
| q: '', | ||
| limit: pageSize, | ||
| offset: offset, | ||
| fields: 'tags,customMetrics,extension', | ||
| sortBy: sortByParam, | ||
| sortOrder: sortOrderParam, | ||
| tags: tagsParam, | ||
| }); | ||
| setTableColumns(pruneEmptyChildren(response.data) || []); | ||
| handlePagingChange(response.paging); | ||
| } else { | ||
| const response = await getTableColumnsByFQN(tableFqn, { | ||
| limit: pageSize, | ||
| offset: offset, | ||
| fields: 'tags,customMetrics,extension', | ||
| sortBy: sortByParam, | ||
| sortOrder: sortOrderParam, | ||
| }); | ||
| setTableColumns(pruneEmptyChildren(response.data) || []); | ||
| handlePagingChange(response.paging); | ||
| } |
There was a problem hiding this comment.
When tag filters are active, fetchTableColumns switches to searchTableColumnsByFQN(q: ''). The backend search implementation flattens nested columns before filtering/pagination, whereas the normal /columns endpoint paginates top-level columns and preserves the nested structure. This can regress the schema view for nested columns (loss of expand/collapse, different paging totals/offset semantics, and different “parent matches when child has tag” behavior). Consider adding tags filtering to the regular /columns endpoints (so you can keep using getTableColumnsByFQN with tags) or updating the backend search endpoint to preserve hierarchy when q is empty.
| onChange={(_pagination, filters) => { | ||
| setActiveTagFilter({ | ||
| Classification: (filters[TABLE_COLUMNS_KEYS.TAGS] as string[]) || [], | ||
| Glossary: (filters[TABLE_COLUMNS_KEYS.GLOSSARY] as string[]) || [], | ||
| }); | ||
| handlePageChange(1); | ||
| }} |
There was a problem hiding this comment.
The table columns still define onFilter: searchTagInData for both Tags and Glossary columns, so Ant Design will apply client-side filtering in addition to the server-side tags param. This becomes inconsistent when both filters are set: the server returns results using OR semantics across all tag FQNs, but the table will apply AND across the two columns’ filters, and paging totals from the server can become incorrect (pages can appear empty). To make the behavior consistent, either disable client-side filtering for these columns when using server filtering (e.g., remove/neutralize onFilter) or change the backend/UI semantics so both sides apply the same AND/OR logic.
| if (tags != null && !tags.trim().isEmpty()) { | ||
| Set<String> tagFQNSet = Arrays.stream(tags.split(",")) | ||
| .map(String::trim) | ||
| .filter(s -> !s.isEmpty()) | ||
| .collect(Collectors.toSet()); | ||
| matchingColumns = new ArrayList<>( | ||
| matchingColumns.stream() | ||
| .filter(column -> column.getTags() != null | ||
| && column.getTags().stream() | ||
| .anyMatch(tag -> tagFQNSet.contains( | ||
| tag.getTagFQN()))) | ||
| .toList()); |
There was a problem hiding this comment.
searchTableColumnsInternal flattens nested columns and then applies tag filtering only on column.getTags(). This means (1) a parent column with no direct tags but with a tagged child won’t match (UI’s previous recursive filtering matched parents via children), and (2) results returned to the UI are a flat list, losing hierarchy. If the schema table is expected to preserve nesting during tag filtering, consider filtering on the hierarchical structure (recursively) and returning the original tree, or move tag filtering to the non-search columns endpoint which already preserves nesting.
|
Hi there 👋 Thanks for your contribution! The OpenMetadata team will review the PR shortly! Once it has been labeled as Let us know if you need any help! |
Code Review ✅ Approved 4 resolved / 4 findingsAdds server-side tag filtering for table columns via ColumnResource API, resolving the infinite render loop, inconsistent parameter passing, inefficient O(n×m) filtering, and pagination reset issues. No remaining issues found. ✅ 4 resolved✅ Bug: Infinite render loop: tagFilter → fetch → setColumns → tagFilter
✅ Quality: Inconsistent tags param passing between fetch and search
✅ Performance: O(n×m) tag filtering with stream-in-stream in Java
✅ Bug: Pagination not reset to page 1 when tag filter changes
OptionsDisplay: compact → Showing less information. Comment with these commands to change:
Was this helpful? React with 👍 / 👎 | Gitar |

Description:
Fixes #25063
Problem:
Column tag filters in the table schema view only filtered columns
visible on the current page (max 50 columns). Tables with 80+ columns
across multiple pages showed incorrect filter results — columns on
other pages with matching tags were completely invisible to the filter.
Root Cause:
Tag filtering was applied client-side against
tableColumnsstatewhich only holds the current paginated slice. The filter had no
awareness of columns on other pages.
Solution:
Implemented server-side tag filtering at the ColumnResource API level
as suggested by @harshach. The filter is now delegated to the backend
which applies it across all columns before paginating the response.
Backend changes (
TableResource.java,TableRepository.java):tagsquery parameter (comma-separated tag FQNs) to bothsearchTableColumnsByFQNandsearchTableColumnsByIdendpointssearchTableColumnsInternalusing OR logic — applied after name/description filtering and before
pagination
tags parameter is provided
Frontend changes (
SchemaTable.component.tsx,tableAPI.ts):tags?: stringtoGetTableColumnsParamsandSearchTableColumnsParamsinterfacesbuildTagsParam()utility function to build comma-separatedtag string from active Classification and Glossary filters
fetchTableColumnsandsearchTableColumnsnow pass the tagsparameter to the backend API call
tagFilteradded touseCallbackdependency arrays to ensurerefetch on filter change
Tests (
SchemaTable.test.tsx):Tag Filtering Server-Side Integration (Issue #25063)multiple tags comma-separated, and empty filter returns undefined
buildTagsParampure function directly for reliabilityTest Results:
Screenshot 1 — Tests starting:

Screenshot 2 — Test results (21/21 passed):

Note on full-stack testing:
Local full-stack execution was not possible on this development machine
(8GB RAM) as OpenMetadata requires ~8GB for backend + Elasticsearch +
MySQL simultaneously. The fix has been validated through unit tests
covering the core tag parameter building and API integration logic.
Backend logic correctness is verified through code review of the
searchTableColumnsInternalfiltering implementation.Type of change:
Checklist:
Fixes #25063: Add server-side tag filtering for table columns via ColumnResource API