fix(search): clear orphaned column docs on recursive service hard delete#29549
Conversation
PR #29322 made recursive service hard delete fast by setting descendantsCoveredByAncestorCascade=true on the database-subtree repos, which skips the per-table deleteFromSearch dispatch and relies on the root service's deleteOrUpdateChildren cascade (delete-by-query on service.id against the service childAliases). Those childAliases do not include tableColumn, and column docs live in a separate flat column_search_index pruned only by the per-table deleteTableColumns. So once the per-table dispatch is skipped, every descendant column doc orphans in search (a 20k-table service leaves 100k stale column docs). Complete the hard-delete search cascade: deleteEntityIndex now also clears the column index for the database-subtree ancestor types (databaseService / database / databaseSchema) by the service.id / database.id / databaseSchema.id field the column docs carry, mirroring the existing per-table deleteTableColumns case. The shared childAliases-driven soft-delete / certification / propagation cascades are intentionally left unchanged. Tests: - SearchRepositoryBehaviorTest: deleteEntityIndex issues the column delete-by-query with the correct parent field for each of the three ancestor types. - DatabaseServiceResourceIT: the recursive-hard-delete regression now also guards the column_search_index doc (searchable before, gone after). - ServiceDeleteSearchCleanupScaleIT (new, @tag("scale")): seeds a service with N tables (5 columns each), recursively hard-deletes it, and asserts both the table and column docs scoped to service.id drop to zero. Verified clearing 100k column docs (20k tables) in ~7s. Fixes #29548 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
❌ PR checklist incompleteThis 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 |
Review feedback: deleteDescendantColumns let the IOException propagate silently to the generic outer catch in deleteEntityIndex, so a column-cleanup failure was indistinguishable from a primary-document delete failure in the logs. Wrap the delete-by-query in a try/catch that logs the entityType + FQN and rethrows (preserving the retry path), matching the sibling deleteTableColumns. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
🔴 Playwright Results — 3 failure(s), 51 flaky✅ 4422 passed · ❌ 3 failed · 🟡 51 flaky · ⏭️ 38 skipped
Genuine Failures (failed on all attempts)❌
|
|
Code Review ✅ ApprovedClears orphaned column documents in the search index during recursive hard deletes of database-related services by introducing a targeted descendant cleanup. The fix is verified through new unit, integration, and scale tests. OptionsDisplay: compact → Showing less information. Comment with these commands to change:
Was this helpful? React with 👍 / 👎 | Gitar |
…ardDelete IT DatabaseServiceResourceIT.recursiveHardDelete_serviceSubtree_leavesNoOrphansAndSearchClean fails fleet-wide (every PR, including unrelated ones) on the "column_search_index doc should be present in search before delete" precondition. It passes in isolation and passed on its introducing PR (#29549, 3 engines), but flakes under the full concurrent IT suite: no backend indexing code changed after #29549. Root cause: #29549 added deleteDescendantColumns, a delete-by-query against the shared column_search_index that now fires on every databaseService/database/databaseSchema hard delete (SearchRepository.deleteEntityIndex). Under the concurrent suite hundreds of these run together, contending on that one index and delaying visibility of a freshly indexed column doc past the 60s precondition timeout. Drop the column_search_index SearchDoc from this concurrent base-test subtree, keeping the table_search_index and relationship-orphan assertions. Column-cleanup correctness stays covered by SearchRepositoryBehaviorTest (unit) and ServiceDeleteSearchCleanupScaleIT (@tag scale), neither of which runs under this contention.
…ardDelete IT DatabaseServiceResourceIT.recursiveHardDelete_serviceSubtree_leavesNoOrphansAndSearchClean fails fleet-wide (every PR, including unrelated ones) on the "column_search_index doc should be present in search before delete" precondition. It passes in isolation and passed on its introducing PR (#29549, 3 engines), but flakes under the full concurrent IT suite; no backend indexing code changed after #29549. Root cause: #29549 added deleteDescendantColumns, a delete-by-query against the shared column_search_index that now fires on every databaseService/database/databaseSchema hard delete (SearchRepository.deleteEntityIndex). Under the concurrent suite hundreds of these run together, contending on that one index and delaying visibility of a freshly indexed column doc past the 60s precondition timeout. Drop the column_search_index SearchDoc from this concurrent base-test subtree, keeping the table_search_index and relationship-orphan assertions. Column-cleanup correctness stays covered by SearchRepositoryBehaviorTest (unit) and ServiceDeleteSearchCleanupScaleIT (@tag scale), neither of which runs under this contention.
…ardDelete IT (#29626) DatabaseServiceResourceIT.recursiveHardDelete_serviceSubtree_leavesNoOrphansAndSearchClean fails fleet-wide (every PR, including unrelated ones) on the "column_search_index doc should be present in search before delete" precondition. It passes in isolation and passed on its introducing PR (#29549, 3 engines), but flakes under the full concurrent IT suite; no backend indexing code changed after #29549. Root cause: #29549 added deleteDescendantColumns, a delete-by-query against the shared column_search_index that now fires on every databaseService/database/databaseSchema hard delete (SearchRepository.deleteEntityIndex). Under the concurrent suite hundreds of these run together, contending on that one index and delaying visibility of a freshly indexed column doc past the 60s precondition timeout. Drop the column_search_index SearchDoc from this concurrent base-test subtree, keeping the table_search_index and relationship-orphan assertions. Column-cleanup correctness stays covered by SearchRepositoryBehaviorTest (unit) and ServiceDeleteSearchCleanupScaleIT (@tag scale), neither of which runs under this contention.



Problem
After #29322 (perf: bound memory + skip ancestor-covered work in bulk recursive hard delete), a recursive hard delete of a
databaseService/database/databaseSchemaleaves the descendant column docs (column_search_index/tableColumn) orphaned in the search index. The entities are removed from the DB, but their column docs linger and reappear in column-level search / Explore until a full reindex.Root cause
#29322 set
descendantsCoveredByAncestorCascade = trueon the database-subtree repos, which makes the bulk hard-delete path skip the per-tabledeleteFromSearchdispatch and rely on the root service's singledeleteFromSearch→SearchRepository.deleteOrUpdateChildrencascade (adelete-by-queryonservice.id).That cascade targets the service's
childAliases—[database, databaseSchema, storedProcedure, table, testSuite, …]— which does not includetableColumn. Column docs live in a separate flatcolumn_search_indexand were only ever pruned by a table's own per-entity delete (deleteEntityIndex(table)→deleteTableColumns, keyed bytable.id). Once the per-table dispatch is skipped, nothing clears them.It slipped through because the #29322 benchmark and IT only counted
table_search_indexdocs, nevercolumn_search_index.Change
SearchRepository.deleteEntityIndexnow also clears the flatcolumn_search_indexfor the database-subtree ancestor types (databaseService/database/databaseSchema), keyed by theservice.id/database.id/databaseSchema.idfield the column docs carry (verified againstColumnSearchIndex+ the column index mapping). This mirrors the existing per-tabledeleteTableColumnsspecial case.The shared
childAliases-driven cascades (soft-delete, certification, inherited-field propagation) are intentionally left unchanged — the fix is scoped to the hard-delete search path that regressed, not the global alias config.Tests
SearchRepositoryBehaviorTest.deleteEntityIndexRemovesDescendantColumnsForDatabaseSubtreeAncestors: assertsdeleteEntityIndexissues the column delete-by-query with the correct parent field for each of the three ancestor types.DatabaseServiceResourceIT: the recursive-hard-delete regression now also guards thecolumn_search_indexdoc (searchable before, gone after) — fails without the fix.@Tag("scale")) —ServiceDeleteSearchCleanupScaleIT: seeds a service with N tables (5 columns each), recursively hard-deletes it, and asserts both the table and column docs scoped toservice.iddrop to zero. Runs in the nightlyscale-itprofile.Scale results (real Testcontainers ES)
The delete-by-query is one server-side op, so it scales independently of doc count — demonstrated clearing 100k column docs cleanly.
Fixes #29548
🤖 Generated with Claude Code
Greptile Summary
This PR fixes orphaned column docs in
column_search_indexafter a recursive hard-delete of adatabaseService,database, ordatabaseSchema. PR #29322 introduced an ancestor-cascade optimisation that skips per-table search dispatch, but the resultingdeleteOrUpdateChildrencascade only targetedchildAliases(which excludedtableColumn), leaving column docs stranded until a full reindex.SearchRepository.deleteDescendantColumnsis wired into theelsebranch ofdeleteEntityIndex. It issues adeleteEntityByFieldsoncolumn_search_indexkeyed byservice.id,database.id, ordatabaseSchema.idas appropriate, mirroring the existingdeleteTableColumnspattern for table-level deletes.DatabaseServiceResourceITregression guard is extended to also verify the column doc disappears; a new@scale-tagged test seeds up to 100k tables (5 columns each) and asserts both indexes empty to zero after recursive hard delete.Confidence Score: 5/5
Safe to merge — the change is a narrowly scoped, additive fix that only runs an extra delete-by-query for three specific entity types during hard delete.
The fix is well-contained:
deleteDescendantColumnsis a no-op for all entity types except DATABASE_SERVICE, DATABASE, and DATABASE_SCHEMA, so no existing code paths are altered. The parent-field names (service.id, database.id, databaseSchema.id) are confirmed against the column index mapping. Error handling mirrors the establisheddeleteTableColumnspattern. The unit test covers all three ancestor types, the integration test extends the regression guard to the column index, and the scale test demonstrates the cleanup holds at 100k column docs.No files require special attention.
Important Files Changed
Sequence Diagram
%%{init: {'theme': 'neutral'}}%% sequenceDiagram participant Caller participant SearchRepository participant SearchClient participant ES as Elasticsearch Caller->>SearchRepository: deleteEntityIndex(service/database/schema) SearchRepository->>SearchClient: deleteEntity(primary index, entityId) SearchClient->>ES: DELETE primary doc SearchRepository->>SearchRepository: deleteOrUpdateChildren(entity, indexMapping) SearchRepository->>SearchClient: deleteEntityByFields(childAliases) note over SearchClient,ES: tableColumn NOT in childAliases alt "entityType == TABLE" SearchRepository->>SearchRepository: deleteTableColumns(table) SearchRepository->>SearchClient: deleteEntityByFields(column_search_index, table.id) SearchClient->>ES: DELETE column docs by table.id else "entityType == DATABASE_SERVICE / DATABASE / DATABASE_SCHEMA" SearchRepository->>SearchRepository: deleteDescendantColumns NEW SearchRepository->>SearchClient: deleteEntityByFields(column_search_index, parent_field) SearchClient->>ES: DELETE column docs by service.id or database.id or databaseSchema.id end%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%% sequenceDiagram participant Caller participant SearchRepository participant SearchClient participant ES as Elasticsearch Caller->>SearchRepository: deleteEntityIndex(service/database/schema) SearchRepository->>SearchClient: deleteEntity(primary index, entityId) SearchClient->>ES: DELETE primary doc SearchRepository->>SearchRepository: deleteOrUpdateChildren(entity, indexMapping) SearchRepository->>SearchClient: deleteEntityByFields(childAliases) note over SearchClient,ES: tableColumn NOT in childAliases alt "entityType == TABLE" SearchRepository->>SearchRepository: deleteTableColumns(table) SearchRepository->>SearchClient: deleteEntityByFields(column_search_index, table.id) SearchClient->>ES: DELETE column docs by table.id else "entityType == DATABASE_SERVICE / DATABASE / DATABASE_SCHEMA" SearchRepository->>SearchRepository: deleteDescendantColumns NEW SearchRepository->>SearchClient: deleteEntityByFields(column_search_index, parent_field) SearchClient->>ES: DELETE column docs by service.id or database.id or databaseSchema.id endReviews (4): Last reviewed commit: "Merge branch 'main' into search-cleanup-..." | Re-trigger Greptile