Skip to content

Fixes #26824: filter column bulk operations metadata status on the aggregate row - #32905

Merged
sonika-shah merged 4 commits into
mainfrom
fix/26824-column-grid-metadata-status
Sep 9, 2026
Merged

Fixes #26824: filter column bulk operations metadata status on the aggregate row#32905
sonika-shah merged 4 commits into
mainfrom
fix/26824-column-grid-metadata-status

Conversation

@sonika-shah

@sonika-shah sonika-shah commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator

Describe your changes:

Fixes #26824

In Column Bulk Operations, the "Has / Missing Metadata" filter (metadataStatus) did not actually hide rows of other statuses, and the page counts were wrong. Picking "Complete" or "Incomplete" still showed Missing/Inconsistent rows, and the number of items per page drifted from page to page.

Root cause was that the filter ran in the wrong place. metadataStatus (MISSING/INCOMPLETE/COMPLETE) was pushed down as a per-document search query, while INCONSISTENT/hasConflicts/hasMissingMetadata were applied after the aggregator had already paginated and computed the totals. But the status shown for a row is an aggregate over all of a column's occurrences (INCONSISTENT when they disagree), so a per-document filter re-grouped into rows whose status differed from the request, and the post-pagination filter shrank the page while the total stayed unfiltered.

This moves every status/row-level filter to run on the fully-grouped items, before pagination, and derives the totals from the filtered set — so a status filter returns only rows of that status and the page count and per-page size stay correct.

As a bonus, the removed per-document query is the wildcard/exists query on flat-object columns.description/columns.tags that crashed ES/OS with search_phase_execution_exception ... all shards failed and had ColumnGridResourceIT disabled. With it gone, the IT is re-enabled.


Type of change:

  • Bug fix

High-level design:

The column-grid aggregator already runs three paths (composite browse, terms-agg name pattern, tag/glossary source read) introduced in #27216, each ending in ColumnMetadataGrouper.groupColumns.

A filter that depends on a row's aggregate status can't be a per-document push-down or a per-page post-filter — it has to run on the grouped items. So:

  • Added shared, pure helpers on the ColumnAggregator interface: hasRowLevelFilter, matchesRowFilters, and paginateFilteredItems (filter grouped items by aggregate status / hasConflicts / hasMissingMetadata, then paginate in memory with totals computed from the filtered set). These are unit-tested with no ES/OS needed.
  • When a row-level filter is active, both aggregators read _source for the scoped entities in one scan per field-path group — the same mechanism the tag/glossary filter already uses — group every column, then filter + paginate the items. The _source is restricted to the column tree and entity-identity fields, so the per-entity payload stays small (it drops the heavy entity-level derived fields like columnNames/columnNamesFuzzy). When no such filter is active, the existing composite/pattern paths are untouched.
  • Removed the per-document metadataStatus query and its now-dead helpers from both aggregators, and removed the post-pagination filter block from ColumnRepository.
  • ColumnResource now documents INCONSISTENT as a supported metadataStatus value (it is now handled uniformly with the others).

Scale note: the scan is bounded to 10K entities per field-path group (the same bound the tag path uses, logged at WARN when exceeded) and only runs when a status/row-level filter is active. Because it reads every occurrence (not a top_hits sample), the aggregate status can't be misclassified by under-sampling.


Tests:

Use cases covered

  • Selecting metadataStatus=COMPLETE returns only rows whose aggregate status is COMPLETE (no Missing/Incomplete/Inconsistent leak) — the reported bug.
  • Same for MISSING, INCOMPLETE, and INCONSISTENT.
  • With a status filter and size=2 over 3 matching + 1 non-matching column, totalUniqueColumns is 3 (not 4), page 1 has 2 rows with a cursor, page 2 has the remaining 1 — the pagination half of the bug.

Unit tests

  • Added to ColumnAggregatorTest: matchesRowFilters (status match, case-insensitivity, INCONSISTENT as first-class, null/blank passes all, hasConflicts) and paginateFilteredItems (filtered totals; multi-page cursor consistency).

Backend integration tests

  • Re-enabled ColumnGridResourceIT (removed the @Disabled that the crashing query forced) and strengthened its status tests to assert every returned row carries the requested status; added test_getColumnGrid_metadataStatusPaginationCountsAreConsistent. Fixed createTableWithFullMetadata to add a tag so the "Complete" fixture is genuinely COMPLETE (it previously had a description but no tags → INCOMPLETE, which the old assert-not-null test never caught).

Playwright (UI) tests

  • Not applicable (no UI changes).

Manual testing performed

Backend-only change. Validated the pure filter/pagination logic with the new ColumnAggregatorTest unit tests locally; the re-enabled ES/OS ColumnGridResourceIT runs against testcontainers in CI.

Additionally ran a full end-to-end filter test on a fresh local stack built from this branch, on both search engines (MySQL+Elasticsearch and Postgres+OpenSearch), seeded with ~13k unique columns across tables and dashboard data models (including a 12k-column wide table) with mixed statuses/tags. Every filter and combination (metadataStatus × columnNamePattern × scope, hasConflicts, hasMissingMetadata, entityTypes, pagination) returned correct results with correct totals and no crashes attributable to this change — 80/80 checks on ES, 33/33 on OS. See the test-summary comment on this PR for details.


UI screen recording / screenshots:

Not applicable.


Checklist:

  • I have read the CONTRIBUTING document.
  • My PR title is Fixes <issue-number>: <short explanation>
  • My PR is linked to a GitHub issue via Fixes #<issue-number> above.
  • I have commented on my code, particularly in hard-to-understand areas.
  • For JSON Schema changes: not needed (no schema changes).
  • For UI changes: not applicable.
  • I have added tests (unit + integration) and listed them above.
  • I have added a test that covers the exact scenario we are fixing.

…, not per document

The "Has / Missing Metadata" filter in Column Bulk Operations did not hide
rows of other statuses, and page counts drifted per page.

metadataStatus (MISSING/INCOMPLETE/COMPLETE) was pushed down as a per-document
search query, while INCONSISTENT/hasConflicts/hasMissingMetadata were applied
after the aggregator had already paginated and computed totals. But a row's
status is an aggregate over all of a column's occurrences (INCONSISTENT when
they disagree), so a per-document filter re-grouped into rows whose status
differed from the request, and the post-pagination filter shrank the page while
the total stayed unfiltered.

Move every row-level filter (metadataStatus, hasConflicts, hasMissingMetadata)
onto the fully-grouped items, before pagination, with totals derived from the
filtered set. Shared pure helpers (hasRowLevelFilter, matchesRowFilters,
paginateFilteredItems) live on the ColumnAggregator interface; both aggregators
route through a materialized name-enumeration path when such a filter is active,
and the untouched composite/pattern paths still serve unfiltered browsing.

Removing the per-document status query also removes the wildcard/exists query on
flat-object columns.description/columns.tags that crashed ES/OS with
search_phase_execution_exception and had ColumnGridResourceIT disabled; the IT
is re-enabled and its status assertions strengthened to check every returned
row carries the requested status, plus a status+pagination consistency test.
@sonika-shah
sonika-shah requested a review from a team as a code owner September 7, 2026 20:53
Copilot AI lite review requested due to automatic review settings September 7, 2026 20:53

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

❌ PR checklist incomplete

This PR cannot be merged until the following are addressed on its linked issue:

The fields live on the linked issue in the Shipping project (open the issue → right sidebar → Projects). After you set them, re-run this check (or push a commit) — issue/project changes do not re-trigger it automatically.

Maintainers can bypass this check by adding the skip-pr-checks label.

@github-actions github-actions Bot added backend safe to test Add this label to run secure Github workflows on PRs labels Sep 7, 2026
@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

✅ Playwright Results — workflow succeeded

Validated commit 58a4bcbbb1891765cfa0823d4029707840371d06 in Playwright run 34207739427, attempt 1.

✅ 4476 passed · ❌ 0 failed · 🟡 7 flaky · ⏭️ 1 skipped · 🧰 0 lifecycle flaky

Performance

Blocking targets: ✅ met · Optimization targets: 🟡 in progress

Shard-job maxima below are not the full workflow wall time; the linked run includes build, fixture, planning, and reporting.

🕒 Full workflow signal wall (to summary) 56m 12s

⏱️ Max setup 4m 56s · max shard execution 22m 15s · max shard-job elapsed before upload 25m 11s · reporting 20s

🌐 216.72 requests/attempt · 2.31 app boots/UI scenario · 34.46% common-shard skew

Optimization targets still in progress:

  • Common shard skew was 34.46% (convergence target: at most 15%).
  • Browser traffic was 216.72 requests per attempt (convergence target: fewer than 200).
  • Application boot ratio was 2.31 per UI scenario (10923 boots / 4732 scenarios; convergence target: at most 1).
Shard Passed Failed Flaky Skipped Lifecycle failed Lifecycle flaky
✅ Shard advanced-search-01 130 0 0 0 0 0
🟡 Shard chromium-01 173 0 1 0 0 0
✅ Shard chromium-02 167 0 0 0 0 0
✅ Shard chromium-03 157 0 0 0 0 0
✅ Shard chromium-04 186 0 0 0 0 0
✅ Shard chromium-05 192 0 0 0 0 0
✅ Shard chromium-06 152 0 0 0 0 0
🟡 Shard chromium-07 174 0 1 0 0 0
🟡 Shard chromium-08 166 0 1 0 0 0
✅ Shard chromium-09 140 0 0 0 0 0
✅ Shard chromium-10 226 0 0 0 0 0
✅ Shard chromium-11 201 0 0 0 0 0
✅ Shard chromium-12 211 0 0 0 0 0
🟡 Shard chromium-13 183 0 1 1 0 0
✅ Shard chromium-14 167 0 0 0 0 0
🟡 Shard chromium-15 213 0 2 0 0 0
✅ Shard chromium-16 164 0 0 0 0 0
✅ Shard chromium-17 160 0 0 0 0 0
✅ Shard chromium-18 216 0 0 0 0 0
🟡 Shard chromium-19 144 0 1 0 0 0
✅ Shard chromium-20 180 0 0 0 0 0
✅ Shard chromium-21 187 0 0 0 0 0
✅ Shard chromium-22 167 0 0 0 0 0
✅ Shard data-asset-rules-01 65 0 0 0 0 0
✅ Shard domain-isolation-01 16 0 0 0 0 0
✅ Shard global-state-01 34 0 0 0 0 0
✅ Shard import-export-01 80 0 0 0 0 0
✅ Shard import-export-02 70 0 0 0 0 0
✅ Shard ingestion-01 39 0 0 0 0 0
✅ Shard ingestion-02 47 0 0 0 0 0
✅ Shard reindex-01 28 0 0 0 0 0
✅ Shard search-01 12 0 0 0 0 0
✅ Shard search-rbac-01 29 0 0 0 0 0
🟡 7 flaky test(s) (passed on retry)
  • Features/ColumnBulkOperations.spec.tsshould expand STRUCT column to show nested fields (shard chromium-01, 1 retry)
  • Pages/TasksUIFlow.spec.tsCreate and reject tag task for Dashboard via UI (shard chromium-07, 1 retry)
  • Features/DataQuality/TestLibrary.spec.tsshould create, edit, and delete a test definition (shard chromium-08, 1 retry)
  • Features/DataQuality/TableLevelTests.spec.tsCustom SQL Query (shard chromium-13, 1 retry)
  • Pages/GlossaryTermRelationSettings.spec.tscreates a custom relation type via the drawer (shard chromium-15, 1 retry)
  • Pages/InputOutputPorts.spec.tsAdd single input port (shard chromium-15, 1 retry)
  • Flow/CustomizeWidgets.spec.tsKPI Widget (shard chromium-19, 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

…ource scan

Replace the row-filter path's names-agg + per-name top_hits fan-out (~1+N
queries/page) with a single _source scan per field-path group — the same
mechanism the tag/glossary filter already uses — restricting _source to the
column tree and entity-identity fields.

This cuts queries per page from ~1+N to ~1 per field-path group, reuses the
already-verified in-memory pagination, and reads every occurrence of a column
instead of a 100-doc top_hits sample, so the aggregate status can no longer be
misclassified (e.g. COMPLETE vs INCONSISTENT) by under-sampling. Restricting
_source to columns + identity fields keeps the per-entity payload small,
dropping the heavy entity-level derived fields (columnNames/columnNamesFuzzy).

extractMatchingColumnsFromHit gains an includeAllColumns flag (the tag path
passes false; the status scan passes true); the now-unused name-enumeration
constants are removed.
Copilot AI review requested due to automatic review settings September 8, 2026 03:23

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@sonika-shah

Copy link
Copy Markdown
Collaborator Author

End-to-end filter test at scale — PR #32905 (Fixes #26824)

Ran a fresh local stack built from this branch (verified the server jar contains our methods: applyColumnNamePattern, fetchColumnsFromSource, paginateFilteredItems, aggregateColumnsWithRowFilters), on both search engines, and drove GET /api/v1/columns/grid through every filter and combination.

Seed (fresh instance, both entity types)

  • Service column_bulk_test_service: 2 databases × 2 schemas × 10 tables + one deliberately-pathological 12,000-column wide table → ~13,100 unique column names / ~100k occurrences, mixed states (missing / incomplete / complete / inconsistent), 5% tagged (PII.Sensitive), nested STRUCTs.
  • Dashboard service column_bulk_dashboards: 12 dashboard data models (~833 unique columns), names overlapping the tables → exercises cross-entity-type dedup.

Elasticsearch (MySQL + ES) — 80 / 80 checks pass

  • metadataStatus partition (the Incorrect filter effect on Column Bulk Operations for Completeness #26824 core): every returned row carries the requested status (0 mismatches over MISSING 6701 / INCOMPLETE 4476 / COMPLETE 221 / INCONSISTENT 1707 rows); the four sets are pairwise disjoint and sum to the unique count (13,105 ≈ cardinality 13,179).
  • hasConflicts total == INCONSISTENT (1707), all hasVariations=true. hasMissingMetadata 200.
  • 48 combination checks (metadataStatus × columnNamePattern × {service, database} scope): every row matches BOTH predicates and totalUniqueColumns == returned rows.
  • Pagination (size 25/50/200 on a filtered set): pages == ceil(total/size), no duplicates across pages, count == total.
  • entityTypes table / dashboardDataModel / both — cross-type dedup holds; status filter correct across both.
  • Edge: nonexistent pattern → empty/total 0; invalid metadataStatus value → no 500.

OpenSearch (Postgres + OS) — 33 / 33 correctness checks pass

  • Identical results to ES for every filter and combination (COMPLETE 221, INCONSISTENT 1707, INCOMPLETE 4476, MISSING 6701; partition 13,105). Full exact pagination of the small sets (COMPLETE, INCONSISTENT) — correct totals, no dupes.

Two scale findings (neither a regression from this PR)

  1. Pre-existing top_hits paths blow up on the wide table. The unfiltered browse and columnNamePattern-only paths (unchanged by this PR) use a top_hits (sample_docs) sub-agg that pulls full _source; at page size ≥ ~100 over the 12k-column table they trip ES's request circuit breaker (Data too large … [sample_docs] … 615MB). Our new status-scan path uses _source field-filtering and does not use top_hits, so it stays healthy — it returned 200 at size=200 where the browse path 500s.
  2. Re-materialize-per-page cost on a memory-constrained node (the gitar-bot comment already noted as a follow-up). Our scan re-reads the scoped entities' _source on every page; sustained deep-paging of the largest filtered set (MISSING, 6701 rows / 68 pages), each page re-scanning the 12k-column doc, saturates the dev OpenSearch node's ~1GB heap → transient node-level [parent] breaker (recovers on GC). Individual queries and full paging of normal-sized sets are correct throughout. ES (same run, same load) tolerated it. Mitigation = scope-keyed cache of the grouped scan (bounded), tracked as a follow-up.

Verdict

The #26824 fix behaves correctly at ~13k-column scale on both engines, across every filter and combination, with correct pagination and no crashes attributable to this change. The only stress point is a pre-existing/environmental memory bound on a pathological single 12k-column table under sustained deep paging — not a correctness defect and not introduced by this PR; the caching follow-up would remove it.

@sonarqubecloud

sonarqubecloud Bot commented Sep 8, 2026

Copy link
Copy Markdown

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.

Incorrect filter effect on Column Bulk Operations for Completeness

3 participants