Skip to content

Fixes #30117: keep soft-deleted owners in PATCH base to fix index crash - #30520

Open
mohityadav766 wants to merge 7 commits into
mainfrom
investigate-issue-30117
Open

Fixes #30117: keep soft-deleted owners in PATCH base to fix index crash#30520
mohityadav766 wants to merge 7 commits into
mainfrom
investigate-issue-30117

Conversation

@mohityadav766

@mohityadav766 mohityadav766 commented Jul 27, 2026

Copy link
Copy Markdown
Member

Describe your changes:

Fixes #30117

Editing owners failed with array item index is out of range when an owner user was soft-deleted: detail pages fetch with include=all (so the soft-deleted owner is present) and compute a positional JSON Patch, but the server loaded the PATCH base with NON_DELETED, dropping that owner and shrinking the array so the positional op went out of range — and the dangling ownership could never be removed. I fixed this at the single shared chokepoint EntityRepository.patch().

Type of change:

  • Bug fix

High-level design:

  • Patch base now resolves owner refs with Include.ALL (PATCH_ORIGINAL_INCLUDES), while the entity itself still loads NON_DELETED — so the positional patch stays in range and the ID-based updateOwners diff removes the dangling OWNS row.
  • getValidatedOwnersForPatch: because the base now surfaces the soft-deleted owner, getValidatedOwners (which runs on every PATCH) would re-validate it against NON_DELETED and 404 on any edit (e.g. a description change). The new method retains owners already assigned in the base (a soft-deleted user is still a valid, existing assignment) and validates only newly added owners against NON_DELETED, so a deleted user still can't be freshly assigned.
  • Scoped to owners only; experts/reviewers/followers are validated against NON_DELETED on update and share the latent issue — left as the issue's explicit follow-up.

Tests:

Use cases covered

  • Removing a soft-deleted owner (sorting last) no longer crashes and cleans up the dangling ownership.
  • Removing a soft-deleted owner (sorting in the middle) removes the correct owner (the old positional mismatch silently removed a different, active owner).
  • Editing a non-owner field (description) while a soft-deleted owner is present succeeds and preserves that owner (the 404 regression above).
  • All owner-supporting entities are covered generically.

Backend integration tests

  • Added in openmetadata-integration-tests/.
  • BaseEntityIT.patch_removeSoftDeletedOwner_removedWithoutCrash — generic; runs for every owner-supporting entity.
  • TestSuiteResourceIT: patch_removeSoftDeletedOwnerSortingLast_removesOwnerWithoutCrash, patch_removeSoftDeletedOwnerSortingInMiddle_removesCorrectOwner, patch_nonOwnerField_withSoftDeletedOwnerPresent_succeeds.

Ingestion integration tests

  • Not applicable (no ingestion changes).

Playwright (UI) tests

  • Not applicable (no UI changes).

Manual testing performed

Ran against the embedded app with real MySQL + Elasticsearch.

  1. RED (without fix): sort-last → An array item index is out of range. Index: 2, Size: 2 (the exact reported error); sort-middle → wrong owner removed; description edit → 404 user instance ... not found.
  2. GREEN (with fix): all pass. Generic test verified across TestSuite, Topic, GlossaryTerm (inherited owners), Pipeline; Domain skips (its custom inheritance owner-path filters soft-deleted owners, so the crash never applies). Existing owner tests (patch_entityUpdateOwner_200, patch_entityChangeOwner_200) still pass across 4 entities — no regression.

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.
  • I have added tests (unit / integration / Playwright as applicable) and listed them above.

🤖 Generated with Claude Code

Greptile Summary

This PR changes shared entity PATCH processing to preserve soft-deleted owners in the patch base.

  • Resolves owner relations with Include.ALL while keeping the entity lookup itself limited to non-deleted entities.
  • Retains existing owner references during PATCH and validates only newly assigned owners.
  • Adds generic and TestSuite integration coverage for removing soft-deleted owners and editing unrelated fields.

Confidence Score: 3/5

The PR is not yet safe to merge because clients using the default non-deleted representation can still submit positional owner patches against a different server-side array.

The server now always applies owner operations to an Include.ALL base, while ordinary entity reads default to NON_DELETED relations; when a soft-deleted owner changes array positions, a client-generated numeric owner operation can affect a different owner.

Files Needing Attention: openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/EntityRepository.java

Important Files Changed

Filename Overview
openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/EntityRepository.java Changes the shared PATCH base and owner-validation behavior to retain existing soft-deleted owner references.
openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/BaseEntityIT.java Adds generic integration coverage for removing a soft-deleted owner while preserving active owners.
openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/TestSuiteResourceIT.java Adds TestSuite scenarios for positional owner removal and unrelated edits with soft-deleted owners.

Reviews (7): Last reviewed commit: "Merge branch 'main' into investigate-iss..." | Re-trigger Greptile

Context used:

Editing owners failed with "array item index out of range" when an owner
user was soft-deleted. Detail pages fetch with include=all (soft-deleted
owner present) and compute a positional JSON Patch, but the server loaded
the PATCH base with NON_DELETED, which dropped that owner and shrank the
array so the positional op referenced an index past the end. The dangling
ownership also could not be removed.

Both patch() entry points now load the original with a RelationIncludes
that keeps the entity at NON_DELETED but resolves owners/experts/reviewers/
followers with ALL, so the positional patch stays in range and the ID-based
updateOwners diff removes the dangling OWNS row. Adds two integration tests
reproducing the reported crash (deleted owner sorting last) and the silent
wrong-owner removal (deleted owner in the middle).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@mohityadav766
mohityadav766 requested a review from a team as a code owner July 27, 2026 11:46
Copilot AI review requested due to automatic review settings July 27, 2026 11:46
@github-actions github-actions Bot added backend safe to test Add this label to run secure Github workflows on PRs labels Jul 27, 2026

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.

Pull request overview

This PR addresses a backend PATCH failure triggered when an entity has soft-deleted user relationships (owners/experts/reviewers/followers) and the client computes a positional JSON Patch from an include=all payload.

Changes:

  • Update EntityRepository.patch() to load the PATCH “original” with relation-specific includes so soft-deleted user references are present when applying positional JSON Patch ops.
  • Add integration tests to reproduce and verify removal of soft-deleted owners (both “sorts last” crash case and “sorts middle” wrong-index removal case).

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/EntityRepository.java Changes PATCH base-load include behavior for specific user relation fields to prevent positional JSON Patch index errors when soft-deleted user refs exist.
openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/TestSuiteResourceIT.java Adds integration coverage for PATCH removal of soft-deleted owners in two ordering scenarios.
Comments suppressed due to low confidence (1)

openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/EntityRepository.java:4338

  • Same concern as the by-id PATCH path: loading the base with PATCH_ORIGINAL_INCLUDES for every PATCH can cause PATCHes that don't touch owners/experts/reviewers/followers to fail when one of those relations points at a soft-deleted user, because later validation resolves owners with Include.NON_DELETED and throws.

Gate PATCH_ORIGINAL_INCLUDES behind whether the patch actually modifies one of those fields; otherwise keep NON_DELETED behavior to avoid new failures.

    // Get only the fields relevant to this patch operation
    T original;
    try (var ignored = phase("patchLoadOriginalByName")) {
      original = getByName(null, fqn, patchFields, PATCH_ORIGINAL_INCLUDES, false);

Comment on lines 4274 to +4276
T original;
try (var ignored = phase("patchLoadOriginal")) {
original = get(null, id, patchFields, NON_DELETED, false);
original = get(null, id, patchFields, PATCH_ORIGINAL_INCLUDES, false);
Copilot AI review requested due to automatic review settings July 27, 2026 11:52

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.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (1)

openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/EntityRepository.java:445

  • Changing PATCH to load the original with owners/experts/reviewers/followers = Include.ALL fixes clients that diff against an include=all representation, but it can break clients that build a positional JSON Patch from a NON_DELETED owner array (they will now apply indices against a longer server-side base and can remove the wrong owner).

This is not just theoretical: the UI’s updateEntityField uses fast-json-patch positional diffs, and some entity GETs explicitly request includeRelations: 'owners:non-deleted,…' (e.g. openmetadata-ui/src/main/resources/ui/src/rest/tableAPI.ts:65-77). In a scenario where a soft-deleted owner sorts before an active owner, a client removing /owners/0 from its filtered array would cause the server to remove the soft-deleted owner instead, leaving the intended active owner unchanged.

Consider a contract-level fix: e.g. allow PATCH to accept an includeRelations/relationIncludes parameter (or header) so the client can declare which representation it diffed against, or switch the UI to a non-positional update for these arrays (whole-array replace / ID-based operations).

  // A PATCH's base ("original") must resolve user references the same way the client did when it
  // computed the diff. Detail pages fetch with include=all, so a soft-deleted owner (or expert,
  // reviewer, follower) is in the client's base. Loading the server base with NON_DELETED drops it
  // and shrinks the array, so a positional JSON-Patch op goes out of range ("array item index out
  // of range") and the dangling relationship can never be removed. Resolve these relations with ALL
  // for the patch base; the entity itself still loads NON_DELETED so a soft-deleted entity is not
  // patched through this path. See issue #30117.
  private static final RelationIncludes PATCH_ORIGINAL_INCLUDES =
      new RelationIncludes(
          NON_DELETED,
          Map.of(
              FIELD_OWNERS, ALL,
              FIELD_EXPERTS, ALL,
              FIELD_REVIEWERS, ALL,
              FIELD_FOLLOWERS, ALL));

Comment on lines +392 to +400
// ===================================================================
// SOFT-DELETED OWNER PATCH TESTS (issue #30117)
//
// A detail page loads the entity with include=all, so a soft-deleted owner is present in the
// client's owner array and in the positional JSON Patch it computes. The server used to load the
// PATCH base with NON_DELETED, which dropped that owner and shrank the array — a positional op
// then referenced an index past the end ("array item index out of range") and the dangling
// ownership could never be removed. These tests reproduce that exact client behavior.
// ===================================================================
Copilot AI review requested due to automatic review settings July 27, 2026 12:32

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.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

Copilot AI review requested due to automatic review settings July 28, 2026 12:41

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.

Pull request overview

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

Comments suppressed due to low confidence (1)

openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/EntityRepository.java:443

  • The new block comment for PATCH_ORIGINAL_INCLUDES has accidental line breaks (standalone "a", "entity", "latent") that make it hard to read and slightly misleading (experts/reviewers/followers validation doesn’t prevent the positional JSON Patch crash; it’s just out of scope). Consider rewriting the comment as a coherent paragraph while keeping the same intent.
  // A PATCH's base ("original") must resolve OWNER references the same way the client diffed
  // against. Detail pages fetch with include=all, so a soft-deleted owner is in the client's base;
  // a
  // NON_DELETED server base drops it and shrinks the array, so a positional JSON-Patch op goes out
  // of range ("array item index out of range") and the dangling ownership can never be removed.

Copilot AI review requested due to automatic review settings July 28, 2026 13:37
@gitar-bot

gitar-bot Bot commented Jul 28, 2026

Copy link
Copy Markdown
Code Review ✅ Approved 2 resolved / 2 findings

Retains soft-deleted owners in the PATCH base and updates validation logic to prevent index crashes and positional mismatches, addressing the PATCH base=ALL misaligns positional patches and Retained PATCH owners bypass validateOwners findings.

✅ 2 resolved
Edge Case: PATCH base=ALL misaligns positional patches from NON_DELETED clients

📄 openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/EntityRepository.java:438-445 📄 openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/EntityRepository.java:4276 📄 openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/EntityRepository.java:4338
Resolving the PATCH base owners/experts/reviewers/followers with Include.ALL aligns the server base with detail-page clients (include=all), but inverts the alignment for any caller that computed its positional JSON-Patch against a NON_DELETED view (SDK/API consumers or internal code that builds a patch via JsonUtils.getJsonPatch from a NON_DELETED-loaded entity). When soft-deleted owners exist, such a client's array is shorter than the new ALL base, so a positional remove/replace op can now hit the wrong index or go out of range in the opposite direction. This only manifests when soft-deleted related entities are present and is the tradeoff the PR acknowledges as deferred, so impact is low — but callers that diff against NON_DELETED should be confirmed to use include=all, or the patch should be applied against the base the client actually used (e.g. via ETag/If-Match to reject stale bases).

Edge Case: Retained PATCH owners bypass validateOwners, risking sort NPE

📄 openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/EntityRepository.java:7869-7883
In getValidatedOwnersForPatch, owners already present in originalOwners are copied straight from the client's patch input into result without going through validateOwners, then the combined list is sorted via Comparator.comparing(EntityReference::getName). Unlike getValidatedOwners (which repopulates name/type/fqn from the DB before sorting), a client that sends a minimal owner reference (id+type only, no name) for an already-assigned owner will produce a null name, and the sort will throw a NullPointerException (HTTP 500). It also means retained owners are returned with whatever metadata the client supplied rather than authoritative DB values. Consider repopulating retained owners' fields (e.g. resolve each retained id via getEntityReferenceById with Include.ALL) or at minimum sorting with a null-safe comparator.

Options

Display: compact → Showing less information.

Comment with these commands to change the behavior for this request:

Compact
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Gitar | Powered by Gitar — free for open source

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.

Pull request overview

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

Comments suppressed due to low confidence (2)

openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/EntityRepository.java:443

  • The new PATCH_ORIGINAL_INCLUDES block comment has broken wrapping (standalone "a", "entity", "latent" lines), which makes it harder to read and looks like an accidental formatter artifact. Please reflow it into complete sentences on each line.
  // A PATCH's base ("original") must resolve OWNER references the same way the client diffed
  // against. Detail pages fetch with include=all, so a soft-deleted owner is in the client's base;
  // a
  // NON_DELETED server base drops it and shrinks the array, so a positional JSON-Patch op goes out
  // of range ("array item index out of range") and the dangling ownership can never be removed.

openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/BaseEntityIT.java:868

  • This test uses Assumptions.assumeTrue(...) to verify conditions that should be deterministic (owners were just assigned and fetched with include=all). If this ever fails it indicates a real regression and should fail the test rather than silently skip it.
    JsonNode ownersBefore = ownersWithIncludeAll(entityPath);
    int doomedIndex = indexOfOwner(ownersBefore, doomedOwner.getId());
    Assumptions.assumeTrue(
        indexOfOwner(ownersBefore, activeOwner.getId()) >= 0 && doomedIndex >= 0,
        "both explicit owners must be assigned and the soft-deleted one surfaced by include=all");

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.

Editing owners fails with "array item index out of range" when an owner user is soft-deleted

3 participants