Skip to content

fix(data-products): reject non-data-asset entities as ports + drop bad tableColumn port rows#30186

Open
sonika-shah wants to merge 3 commits into
open-metadata:mainfrom
sonika-shah:fix-data-product-ports-non-asset-guard
Open

fix(data-products): reject non-data-asset entities as ports + drop bad tableColumn port rows#30186
sonika-shah wants to merge 3 commits into
open-metadata:mainfrom
sonika-shah:fix-data-product-ports-non-asset-guard

Conversation

@sonika-shah

@sonika-shah sonika-shah commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator

Problem

Opening a Data Product detail page can fail with a 500 and the toast:

Entity repository for tableColumn not found. Is the ENTITY_TYPE_MAP initialized?

The failing request is GET /api/v1/dataProducts/name/{fqn}/portsView.

Root cause

A data product port is a relationship (INPUT_PORT/OUTPUT_PORT) to some entity, and
getPaginatedPorts loads each port with Entity.getEntities, which resolves a repository
via Entity.getEntityRepository. That throws EntityNotFoundException for a type with no
registered repository — including tableColumn, a search-index-only pseudo type
(Entity.TABLE_COLUMN) that is deliberately not a first-class entity. So a single port
relationship pointing at a column makes the whole portsView response 500.

A port is meant to reference a data asset, and the add-ports operation did not enforce this.

Fix

  1. Validate on add. Adding a port now rejects any reference that is not a real entity
    supporting the dataProducts field, reported as a per-row failure (consistent with the
    existing opposite-port and ownership checks in the same loop):

    Entity.hasEntityRepository(type)
        && !Entity.isTimeSeriesEntity(type)
        && Entity.entityHasField(type, FIELD_DATA_PRODUCTS)

    No hardcoded type list, so new asset types are covered automatically.

  2. Migration to clean existing data. A 2.0.0 postDataMigrationSQLScript deletes any
    existing dataProduct -> tableColumn input/output port relationships (relation 23/24) so
    already-affected instances stop 500-ing. The read path (getPaginatedPorts) is left
    unchanged — no per-request filtering.

Port eligibility == data product asset eligibility (intentional)

The eligibility check above is exactly the rule that governs what can be added as a data
product asset (validateAssetDataProductAssignment requires the entity to carry a
dataProducts field, and output ports already require the target to be a data product
asset). So a type is a valid port iff it is a valid data product asset — ports do not
introduce a separate, narrower "dataset-only" rule. Concretely this means dataset entities
(table, topic, dashboard, container, mlmodel, …) and every other data-product-taggable entity
(e.g. glossaryTerm, tag, testCase) are accepted, while non-assets (user, team,
domain, query, …) and the non-entity tableColumn are rejected — the same set the assets
API accepts. Keeping ports and assets on one shared rule avoids drift and needs no maintained
whitelist.

Tests

Adds DataProductResourceIT#test_addPort_rejectsNonDataAssetEntity: a user (a real entity
that is not a data asset) is rejected when added as an input port, and the ports view still
loads with zero ports. Fails without change (1).

Greptile Summary

This PR fixes a 500 error on the portsView endpoint caused by tableColumn port relationships that reference a pseudo-type with no registered entity repository. The fix has two parts: a write-time validation that rejects non-data-asset entities when adding ports, and a 2.0.0 SQL migration to remove existing corrupt rows.

  • Validation: isPortEligibleType checks that a port target has an entity repository, is not a time-series entity, and has a dataProducts schema field — exactly the same rule used by the assets API — and issues a per-row failure in the bulk response instead of persisting the relationship.
  • Migration: Deletes dataProduct → tableColumn INPUT_PORT (relation 23) and OUTPUT_PORT (relation 24) rows from entity_relationship in both MySQL and Postgres scripts; ordinals are correct per the entityRelationship.json enum definition.
  • Tests: Two new integration tests verify that a user (real entity, not a data asset) and a tableColumn (pseudo-type) are each rejected as a port with a per-row failure, and that the ports view loads cleanly afterward.

Confidence Score: 5/5

Safe to merge — the write-side guard, migration, and tests are all consistent and correct.

The validation logic is minimal and well-scoped: it rejects ineligible types before any DB write, uses the same eligibility rule as the existing assets API, and the read path is left completely unchanged. The SQL migration deletes only the known problematic relationship rows using verified ordinals. Two integration tests cover both the real-entity-non-asset and pseudo-type cases.

No files require special attention.

Important Files Changed

Filename Overview
openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/DataProductRepository.java Adds isPortEligibleType helper and a guard at the top of the add branch in executeBulkPortsOperation; read path (getPaginatedPorts) is left unchanged, relying on the migration to remove bad rows.
bootstrap/sql/migrations/native/2.0.0/mysql/postDataMigrationSQLScript.sql Appends a DELETE targeting dataProduct → tableColumn INPUT_PORT/OUTPUT_PORT rows; relation ordinals 23 and 24 match the schema enum definition.
bootstrap/sql/migrations/native/2.0.0/postgres/postDataMigrationSQLScript.sql Identical migration to the MySQL script; syntax is Postgres-compatible and correct.
openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/DataProductResourceIT.java Adds two integration tests and a private addInputPortsWithResult helper; tests correctly assert per-row failure status and zero ports in the subsequent view response.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Client
    participant DataProductRepository
    participant EntityUtil
    participant DB

    Client->>DataProductRepository: "PUT /inputPorts/add {assets}"
    DataProductRepository->>EntityUtil: populateEntityReferences(assets)
    Note over EntityUtil: Updates resolvable refs in-place. Returns filtered copy (discarded). Original assets list unchanged.
    EntityUtil-->>DataProductRepository: (return value discarded)

    loop for each ref in assets
        alt not isPortEligibleType
            DataProductRepository-->>DataProductRepository: per-row failure added
        else eligible type in opposite port
            DataProductRepository-->>DataProductRepository: per-row failure added
        else eligible
            DataProductRepository->>DB: addRelationship INPUT_PORT
            DataProductRepository-->>DataProductRepository: per-row success
        end
    end

    DataProductRepository-->>Client: BulkOperationResult
Loading
%%{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 Client
    participant DataProductRepository
    participant EntityUtil
    participant DB

    Client->>DataProductRepository: "PUT /inputPorts/add {assets}"
    DataProductRepository->>EntityUtil: populateEntityReferences(assets)
    Note over EntityUtil: Updates resolvable refs in-place. Returns filtered copy (discarded). Original assets list unchanged.
    EntityUtil-->>DataProductRepository: (return value discarded)

    loop for each ref in assets
        alt not isPortEligibleType
            DataProductRepository-->>DataProductRepository: per-row failure added
        else eligible type in opposite port
            DataProductRepository-->>DataProductRepository: per-row failure added
        else eligible
            DataProductRepository->>DB: addRelationship INPUT_PORT
            DataProductRepository-->>DataProductRepository: per-row success
        end
    end

    DataProductRepository-->>Client: BulkOperationResult
Loading

Reviews (5): Last reviewed commit: "test(data-products): assert tableColumn ..." | Re-trigger Greptile

Copilot AI review requested due to automatic review settings July 17, 2026 13:48
@github-actions

Copy link
Copy Markdown
Contributor

❌ PR checklist incomplete

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

  • No GitHub issue is linked. Link an issue in the Development section of the PR (or add Fixes #12345 to the description). For a same-org cross-repo issue, add Fixes open-metadata/<repo>#123 to the description.

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.

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 added backend safe to test Add this label to run secure Github workflows on PRs labels Jul 17, 2026
@github-actions

github-actions Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

🟡 Playwright Results — all passed (34 flaky)

✅ 4537 passed · ❌ 0 failed · 🟡 34 flaky · ⏭️ 95 skipped

Shard Passed Failed Flaky Skipped
🟡 Shard 1 435 0 5 16
✅ Shard 2 11 0 0 0
🟡 Shard 3 820 0 12 8
🟡 Shard 4 820 0 3 18
🟡 Shard 5 840 0 1 5
🟡 Shard 6 786 0 2 46
🟡 Shard 7 825 0 11 2
🟡 34 flaky test(s) (passed on retry)
  • Flow/TestConnectionModal.spec.ts › modal opens with gate card and capability checks sections (shard 1, 2 retries)
  • Flow/TestConnectionModal.spec.ts › success state shows Done button and hides Edit Connection and Retry Test (shard 1, 1 retry)
  • Pages/Lineage/LineageRightPanel.spec.ts › Verify custom properties tab IS visible for supported type: metric (shard 1, 1 retry)
  • Pages/SearchSettings.spec.ts › Latest preview config wins when a superseded request resolves late (shard 1, 1 retry)
  • Flow/SearchRBAC.spec.ts › a fully denied user sees neither asset type when browsing (shard 1, 1 retry)
  • Features/BulkEditEntity.spec.ts › Glossary Term (Nested) (shard 3, 2 retries)
  • Features/BulkEditOperationBadges.spec.ts › Database service bulk edit search filters rows and clear restores them (shard 3, 1 retry)
  • Features/BulkImportWithDotInName.spec.ts › Service name with multiple dots (shard 3, 1 retry)
  • Features/ContextCenterArchive.spec.ts › archive page lazy-loads more rows on scroll within its own scroll container (shard 3, 1 retry)
  • Features/ContextCenterArticles.spec.ts › Article listing search filters, clears, and shows empty state (shard 3, 1 retry)
  • Features/ContextCenterArticles.spec.ts › Article list cards, recently viewed widget, and pagination work (shard 3, 1 retry)
  • Features/ContextCenterArticles.spec.ts › description: switching articles does not bleed unsaved content into next article (shard 3, 1 retry)
  • Features/ContextCenterMemories.spec.ts › editing visibility from Shared to Private saves and updates the badge (shard 3, 1 retry)
  • Features/ContextCenterMemories.spec.ts › adding a linked asset in edit mode shows entity badge on the row (shard 3, 1 retry)
  • Features/ContextCenterPermission.spec.ts › user with editAll permission can see restore action but not delete action on an archived document, and can restore it (shard 3, 1 retry)
  • Features/Glossary/GlossaryAdvancedOperations.spec.ts › should remove domain from glossary (shard 3, 1 retry)
  • Features/Glossary/GlossaryAdvancedOperations.spec.ts › should handle term with very long name (shard 3, 1 retry)
  • Features/IncidentManager.spec.ts › Complete Incident lifecycle with table owner (shard 4, 1 retry)
  • Features/MetricBulkImportExportEdit.spec.ts › MetricListPage unchecking header checkbox clears the selection bar (shard 4, 1 retry)
  • Features/Table.spec.ts › should persist page size (shard 4, 1 retry)
  • Flow/ExploreDiscovery.spec.ts › Should display domain and owner of deleted asset in suggestions when showDeleted is on (shard 5, 1 retry)
  • Pages/ExplorePageRightPanel_KnowledgeCenter.spec.ts › Should remove user owner for knowledgeCenter (shard 6, 1 retry)
  • Pages/ExplorePageRightPanel.spec.ts › Should perform CRUD and Removal operations for dashboardDataModel (shard 6, 1 retry)
  • Pages/ExplorePageRightPanel.spec.ts › Should allow Data Consumer to edit tier for dashboard (shard 7, 1 retry)
  • Pages/GlossaryImportExport.spec.ts › Glossary Bulk Import Export (shard 7, 1 retry)
  • Pages/GlossaryImportExport.spec.ts › Import partial success - some terms pass, some fail (shard 7, 1 retry)
  • Pages/InputOutputPorts.spec.ts › Output ports section collapse/expand (shard 7, 1 retry)
  • Pages/Lineage/DataAssetLineage.spec.ts › Column lineage for dashboard -> table (shard 7, 1 retry)
  • Pages/Lineage/LineageFilters.spec.ts › Verify Impact Analysis service filter selection (shard 7, 1 retry)
  • Pages/Lineage/LineageFilters.spec.ts › Verify lineage schema filter selection (shard 7, 1 retry)
  • ... and 4 more

📦 Download artifacts

How to debug locally
# Download playwright-test-results-<shard> artifact and unzip
npx playwright show-trace path/to/trace.zip    # view trace

Copilot AI review requested due to automatic review settings July 19, 2026 11:31
@sonika-shah
sonika-shah force-pushed the fix-data-product-ports-non-asset-guard branch from c32f0f4 to 5cabbfb Compare July 19, 2026 11:31

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.

@gitar-bot

gitar-bot Bot commented Jul 19, 2026

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

Hardens Data Product ports by validating entity eligibility during writes and skipping unregistered types during reads, resolving the 500 error for invalid port relationships.

✅ 1 resolved
Edge Case: isPortEligibleType can throw for time-series entity types

📄 openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/DataProductRepository.java:629-643
hasEntityRepository returns true for time-series types (it also checks ENTITY_TS_REPOSITORY_MAP), but entityHasField resolves via Entity.getEntityRepository, which only consults ENTITY_REPOSITORY_MAP and throws EntityNotFoundException for TS-only types. So if a time-series entity reference is added as a port, isPortEligibleType throws instead of returning a clean per-row failure, re-introducing the 500 this PR aims to eliminate. The read-path guard in getPaginatedPorts has the same gap: for a TS type hasEntityRepository is true, so it proceeds to Entity.getEntities, which can throw. The primary tableColumn case is unaffected (it is in neither map). Guard the eligibility/read check against types not in the standard entity repository map, or catch the lookup failure.

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

…d tableColumn port rows

A data product port must reference a data asset (a dataset such as a table,
topic, or dashboard). Adding a port did not validate this, and a historical
input/output port relationship pointing at a column (entity type "tableColumn",
a search-only pseudo type with no repository) makes
GET /dataProducts/{..}/portsView 500 with "Entity repository for tableColumn
not found".

- Add-ports now rejects any reference that is not a real entity supporting the
  dataProducts field, reported as a per-row failure (consistent with the
  existing opposite-port and ownership checks). Eligibility is derived from the
  entity's own capabilities (repository + dataProducts field, excluding
  time-series types), so new asset types are covered automatically with no list
  to maintain.
- Adds a 2.0.0 data migration that deletes existing dataProduct -> tableColumn
  input/output port relationships so already-affected instances stop 500-ing.

Adds an integration test asserting a non-data-asset entity is rejected as a
port.
@sonika-shah
sonika-shah force-pushed the fix-data-product-ports-non-asset-guard branch from 5cabbfb to e31393b Compare July 19, 2026 12:15
Copilot AI review requested due to automatic review settings July 19, 2026 12:15
@sonika-shah sonika-shah changed the title fix(data-products): guard ports against non-data-asset entities and harden portsView fix(data-products): reject non-data-asset entities as ports + drop bad tableColumn port rows Jul 19, 2026
…s-non-asset-guard

# Conflicts:
#	bootstrap/sql/migrations/native/2.0.0/mysql/postDataMigrationSQLScript.sql
#	bootstrap/sql/migrations/native/2.0.0/postgres/postDataMigrationSQLScript.sql
Copilot AI review requested due to automatic review settings July 19, 2026 18:42
@sonika-shah
sonika-shah enabled auto-merge (squash) July 19, 2026 18:42

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

This comment has been minimized.

…y dropped

populateEntityReferences works on a copy and executeBulkPortsOperation discards
its return, so an unresolvable ref (tableColumn) is NOT stripped from the assets
list — it reaches the in-loop eligibility check and is reported as a per-row
failure. This test pins that behavior.
Copilot AI review requested due to automatic review settings July 19, 2026 19:09

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

This comment has been minimized.

@gitar-bot

gitar-bot Bot commented Jul 19, 2026

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

Prevents 500 errors in Data Product views by implementing write-time validation for port entities and migrating historical invalid 'tableColumn' relationships. No open issues remain following the implementation of the eligibility guard and verification of idempotent migration scripts.

✅ 2 resolved
Edge Case: isPortEligibleType can throw for time-series entity types

📄 openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/DataProductRepository.java:629-643
hasEntityRepository returns true for time-series types (it also checks ENTITY_TS_REPOSITORY_MAP), but entityHasField resolves via Entity.getEntityRepository, which only consults ENTITY_REPOSITORY_MAP and throws EntityNotFoundException for TS-only types. So if a time-series entity reference is added as a port, isPortEligibleType throws instead of returning a clean per-row failure, re-introducing the 500 this PR aims to eliminate. The read-path guard in getPaginatedPorts has the same gap: for a TS type hasEntityRepository is true, so it proceeds to Entity.getEntities, which can throw. The primary tableColumn case is unaffected (it is in neither map). Guard the eligibility/read check against types not in the standard entity repository map, or catch the lookup failure.

Bug: Port eligibility check moved after populateEntityReferences drops bad refs

📄 openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/DataProductRepository.java:426-427 📄 openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/DataProductRepository.java:457-470
The eligibility check (isPortEligibleType) was moved from the raw request (previous collectPortEligibleAssets) into the loop that runs after EntityUtil.populateEntityReferences(assets). That method resolves each ref via Entity.getEntityReferenceById, catches EntityNotFoundException, and then does mutableList.removeIf(ref -> !populatedRefs.contains(ref)) — so a reference to a type with no repository (exactly the tableColumn case this PR targets) is silently removed before the loop and is never reported as a per-row failure. This reintroduces the silent-omission behavior the deleted comment explicitly warned against: the caller gets a success with the row neither passed nor failed, instead of the intended PARTIAL_SUCCESS with an error message. Restore validation against the raw request list before populateEntityReferences, then continue processing only the resolved+eligible refs.

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

@sonarqubecloud

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.

2 participants