Skip to content

Fixes #24294: support re-parenting a Container via PATCH#28201

Merged
harshach merged 4 commits into
mainfrom
harshach/issue-24294
May 18, 2026
Merged

Fixes #24294: support re-parenting a Container via PATCH#28201
harshach merged 4 commits into
mainfrom
harshach/issue-24294

Conversation

@harshach
Copy link
Copy Markdown
Collaborator

@harshach harshach commented May 17, 2026

Describe your changes:

Fixes #24294

Allow the PATCH API to update a Container's parent so users can re-parent containers (and their entire subtree) without delete+recreate, preserving followers, likes, ownership, tags, and other metadata.

Type of change:

  • New feature

High-level design:

ContainerRepository.restorePatchAttributes no longer wipes parent, CONTAINER_PATCH_FIELDS now includes it, and a new ContainerUpdater.updateParent runs the same FQN-cascade flow GlossaryTermRepository.updateNameAndParent uses — invalidate descendant caches, bulk-rewrite descendant FQNs, re-apply tags, rename tag_usage targets for classification + glossary sources, rewrite entity-links, rewrite policy conditions, swap the CONTAINS relationship edge, and reindex the search documents via updateByFqnPrefix. ContainerDAO.updateFqn is overridden because the generic MySQL JSON_REPLACE only rewrites $.fullyQualifiedName and silently leaves dataModel.columns[*].fullyQualifiedName pointing at the old parent. setFullyQualifiedName no longer falls back to the relationship table when parent is null in-memory — that fallback was silently restoring the cleared parent on every "promote to top-level" PATCH. Validation is extracted to a package-private static helper validateContainerParent and rejects cross-StorageService parents (HTTP 400), self-parent, and descendant-as-parent cycles. Java fluent SDK gains Containers.create().under(...) (3 overloads) plus FluentContainer.withParent(...)/withParentFqn/withoutParent; Python fluent SDK gains Containers.set_parent and Containers.clear_parent following the existing Tables.add_tag PATCH idiom — parent was already in ALLOWED_COMMON_PATCH_FIELDS. Scoped to same-StorageService moves; cross-service is explicitly rejected.

Tests:

Use cases covered

  • Move a container under a new parent in the same StorageService; followers/tags/owners/description survive the move
  • Moving a container cascades the FQN to every descendant container and to nested dataModel.columns[*].fullyQualifiedName
  • Set parent to null to promote a container to top-level under its service; set a parent on a previously top-level container
  • Reject cycles (self-parent, descendant-as-parent), cross-service parents, and non-existent parents
  • Re-parent emits a proper changeDescription with parent recorded and bumps the entity version

Unit tests

  • openmetadata-service/src/test/java/org/openmetadata/service/jdbi3/ContainerRepositoryParentValidationTest.java — 6 tests, all pass

Backend integration tests

  • openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/ContainerResourceIT.java — 11 new @Test methods; full class is 253/253 passing (17 pre-existing skipped)

Ingestion integration tests

  • Not applicable for backend cascade; 2 new Python unit tests added to ingestion/tests/unit/sdk/test_container_entity.py for the fluent SDK (14/14 pass)

Playwright (UI) tests

  • Not applicable (no UI changes)

Manual testing performed

  • mvn -DskipTests clean install (full build)
  • mvn test -pl openmetadata-integration-tests -Dtest=ContainerResourceIT against the spawned MySQL/OpenSearch test harness — 253/253 passing
  • mvn spotless:check and make py_format_check — both clean

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


Summary by Gitar

  • Performance and logic:
    • Optimized setFields to resolve parent only when requested, reducing unnecessary database round-trips for standard GET requests.
  • Data integrity:
    • Updated updateEntityLinks to process all descendants from renamedContainers instead of just direct children, ensuring consistent FQN updates for deeply nested feed threads.

This will update automatically on new commits.

Allow the PATCH API to update a Container's `parent`, cascading the FQN
change to every descendant container, nested column FQN, tag-usage row,
entity-link, policy condition, and search-index document — same shape as
GlossaryTerm re-parenting. Scoped to same-StorageService moves; cross-service
parents are rejected with HTTP 400. Adds parent-aware fluent SDK methods in
Java (`Containers.under(...)`, `FluentContainer.withParent(...)`/`withoutParent()`)
and Python (`Containers.set_parent`, `Containers.clear_parent`), unit tests
for the validation logic, 11 integration tests covering the cascade and
rejection paths, and 9 + 2 SDK tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@harshach harshach requested a review from a team as a code owner May 17, 2026 17:19
Copilot AI review requested due to automatic review settings May 17, 2026 17:19
@github-actions github-actions Bot added backend safe to test Add this label to run secure Github workflows on PRs labels May 17, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

The single-transaction cascade in updateParent locks every descendant row
in storage_container_entity, rewrites their JSON, renames their tag_usage
rows, and issues an Elasticsearch updateByQuery — all while holding
row locks that block any concurrent write on the subtree. At ~10k+
descendants this becomes a multi-minute outage on the cluster.

Add an indexed COUNT(*) preflight in ContainerUpdater.updateParent that
short-circuits with HTTP 400 when the moved subtree exceeds
openmetadata.container.maxReparentDescendants (default 10000), pointing
the operator at the system property if they have measured the impact
and accept it. Run BEFORE invalidateCacheForRenameCascade so a rejected
request pays no cache-eviction cost.

Tests: 5 new unit tests in ContainerRepositoryParentValidationTest cover
under/at/over-limit and the system-property override; 2 new IT methods
in ContainerResourceIT exercise the end-to-end reject path with a
test-scoped low threshold and confirm at-limit moves still succeed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 17, 2026

🟡 Playwright Results — all passed (10 flaky)

✅ 4111 passed · ❌ 0 failed · 🟡 10 flaky · ⏭️ 86 skipped

Shard Passed Failed Flaky Skipped
✅ Shard 1 299 0 0 4
🟡 Shard 2 772 0 2 8
🟡 Shard 3 782 0 2 7
🟡 Shard 4 814 0 2 18
🟡 Shard 5 708 0 1 41
🟡 Shard 6 736 0 3 8
🟡 10 flaky test(s) (passed on retry)
  • Features/ActivityAPI.spec.ts › creates an activity event when tags are added (shard 2, 1 retry)
  • Features/KnowledgeCenter.spec.ts › Article mentions in description should working for Knowledge Center (shard 2, 1 retry)
  • Features/OntologyExplorer.spec.ts › should repopulate data-node-positions after fit-view (shard 3, 1 retry)
  • Features/RestoreEntityInheritedFields.spec.ts › Validate restore with Inherited domain and data products assigned (shard 3, 1 retry)
  • Pages/CustomProperties.spec.ts › Hyperlink (shard 4, 1 retry)
  • Pages/Entity.spec.ts › Tier Add, Update and Remove (shard 4, 1 retry)
  • Pages/EntityDataConsumer.spec.ts › Tier Add, Update and Remove (shard 5, 1 retry)
  • Pages/Glossary.spec.ts › Drag and Drop Glossary Term (shard 6, 1 retry)
  • Pages/Lineage/LineageFilters.spec.ts › Verify lineage schema filter selection (shard 6, 1 retry)
  • Pages/ServiceEntity.spec.ts › Announcement create, edit & delete (shard 6, 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

…race (#24294)

Four fixes from the gitar-bot review:

1. Performance — validateContainerParent short-circuits when the proposed
   parent id matches the original. Skips an unnecessary Entity.getEntity
   round-trip on every container PATCH/PUT that doesn't touch the parent
   field (description edits, tag adds, etc.).

2. Cycle detection — second-line ID-based ancestor-chain walk added in
   ContainerUpdater.validateAncestorChainCycle. Uses
   relationshipDAO.findFrom (direct DB) so a descendant with a briefly
   stale FQN can't bypass the FQN-prefix check. Visited-set bounded.

3. IT race condition — drop System.setProperty in
   patch_containerParent_rejectsOversizedSubtree_400 and
   patch_containerParent_allowsMoveAtConfiguredLimit_200. Add a
   package-accessible test-override field (set/clear via public static
   methods) plus @ResourceLock(MAX_REPARENT_DESCENDANTS_TEST_LOCK) to
   serialize any test that mutates the override, even though the class
   runs methods concurrently.

4. SQL build comment — document why ContainerDAO.updateFqn interpolates
   values via String.format (mirrors EntityDAO pattern, FQN values are
   server-computed, escapeApostrophe handles the only SQL metacharacter
   that can appear in a validated entity name).

Tests: ContainerRepositoryParentValidationTest extended to 12 cases
(adds parent-unchanged short-circuit assertion + override-priority
coverage). Full ContainerResourceIT still 255/255.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 18, 2026 02:02
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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 9 out of 9 changed files in this pull request and generated 2 comments.

Comment on lines +104 to +108
// Always resolve parent — re-parenting via PATCH (#24294) requires the loaded entity to
// carry the current parent so the JSON Patch document can target an existing
// `/parent` member. Mirrors `GlossaryTermRepository.setFields`. `clearFields` below still
// strips parent from the response payload when the caller did not request it.
container.setParent(getContainerParent(container));
Comment on lines +1019 to +1036
/**
* Rewrite feed entity-links and field-relationships when a container's FQN changes (parent
* move). Mirrors {@code GlossaryTermRepository.updateEntityLinks}. Descendant container
* entity-links are also updated by walking children via {@link Relationship#CONTAINS}.
*/
private void updateEntityLinks(String oldFqn, String newFqn, Container updated) {
daoCollection.fieldRelationshipDAO().renameByToFQN(oldFqn, newFqn);

EntityLink newAbout = new EntityLink(CONTAINER, newFqn);
feedRepository.updateLegacyThreadsAbout(newAbout.getLinkString(), updated.getId().toString());

List<EntityReference> children =
findTo(updated.getId(), CONTAINER, Relationship.CONTAINS, CONTAINER);
for (EntityReference child : children) {
EntityLink childAbout = new EntityLink(CONTAINER, child.getFullyQualifiedName());
feedRepository.updateLegacyThreadsAbout(childAbout.getLinkString(), child.getId().toString());
}
}
Two findings from copilot-pull-request-reviewer:

1. setFields perf regression — restored the conditional
   fields.contains(FIELD_PARENT) ? getContainerParent(c) : c.getParent()
   guard. The unconditional load forced an extra entity_relationship
   lookup on every container GET, which is a measurable regression on
   the hot path. The PATCH flow still loads parent because
   CONTAINER_PATCH_FIELDS includes parent (so fields.contains is true
   there). Full ContainerResourceIT still 255/255.

2. updateEntityLinks shallow walk — previously only iterated direct
   children, leaving deep descendants' (grandchildren+) legacy feed
   thread entityLinks pointing at the old FQN and breaking activity-
   feed navigation after a multi-level move. Now takes the
   renamedContainers snapshot captured by invalidateCacheForRenameCascade
   and rewrites each descendant's entityLink by swapping the FQN
   prefix (oldFqn → newFqn) — consistent with the same prefix-substitution
   ContainerDAO.updateFqn applies to the JSON/fqnHash.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@gitar-bot
Copy link
Copy Markdown

gitar-bot Bot commented May 18, 2026

Code Review ✅ Approved 4 resolved / 4 findings

Enables Container re-parenting via PATCH by cascading FQN updates, metadata migration, and relationship rewrites. Addresses race conditions, optimizes performance, and resolves cycle detection issues.

✅ 4 resolved
Performance: validateParent fires DB query on every update even when parent unchanged

📄 openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/ContainerRepository.java:1056-1062 📄 openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/ContainerRepository.java:1089 📄 openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/ContainerRepository.java:1156-1158
validateParent() is called unconditionally at the top of entitySpecificUpdate (line 1089). The static validateContainerParent method only short-circuits when newParent == null — if the container already has a parent and it hasn't changed, Entity.getEntity(CONTAINER, newParent.getId(), "service", NON_DELETED) still fires, adding a needless DB round-trip on every PUT/PATCH of a container that has a parent.

This affects all container updates (description changes, tag additions, etc.), not just re-parenting operations.

Bug: Cycle detection uses FQN prefix check, misses non-FQN-based cycles

📄 openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/ContainerRepository.java:1072-1077
The cycle detection in validateContainerParent (line 1074) uses FullyQualifiedName.isParent(parentFqn, origFqn) which checks if parentFqn starts with origFqn + ".". This works for detecting descendants by FQN prefix. However, if a descendant's FQN has already been modified (e.g., in a concurrent operation or due to a stale entity cache), the check could miss a cycle.

More critically, the validation runs before updateParent in entitySpecificUpdate, using the updated object which carries the new parent reference. But the updated.getParent().getId() is resolved to get resolvedParent — if that resolved parent's FQN doesn't yet reflect a concurrent in-progress move, the cycle check could pass incorrectly. This is a narrow race window but worth noting for correctness in concurrent environments.

Bug: System.setProperty in concurrent tests causes race condition

📄 openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/ContainerResourceIT.java:3087-3088 📄 openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/ContainerResourceIT.java:3131
The integration tests patch_containerParent_rejectsOversizedSubtree_400 and patch_containerParent_allowsMoveAtConfiguredLimit_200 use System.setProperty("openmetadata.container.maxReparentDescendants", "2") to override the subtree limit. Since the class is annotated @Execution(ExecutionMode.CONCURRENT), other tests performing re-parent PATCHes that run in parallel will see this artificially low limit and may fail spuriously. System.setProperty is JVM-global and not thread-safe for this purpose.

Consider injecting the limit via a test-scoped mechanism (e.g., a package-private setter/resettable AtomicInteger in ContainerRepository, or use @ResourceLock from JUnit 5 to serialize these specific tests).

Security: updateFqn builds SQL via String.format with user-derived FQN values

📄 openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/CollectionDAO.java:998-1012
The ContainerDAO.updateFqn override (lines 999-1029) interpolates oldPrefix/newPrefix into raw SQL via String.format, relying solely on escapeApostrophe (which only doubles single quotes). While FQN values are server-controlled and validated elsewhere, and this follows the pre-existing EntityDAO.updateFqn pattern, the approach is fragile — any future path that allows special characters in container names could expose SQL injection. Consider adding a comment acknowledging this limitation, or using parameterized queries if the JDBI framework supports it for this dynamic pattern.

Options

Display: compact → Showing less information.

Comment with these commands to change:

Compact
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Gitar

@sonarqubecloud
Copy link
Copy Markdown

@harshach harshach merged commit 5c53151 into main May 18, 2026
53 checks passed
@harshach harshach deleted the harshach/issue-24294 branch May 18, 2026 14:01
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.

Support updating the parent of a container via PATCH API

2 participants