Skip to content

fix(glossary): include cardinality on default term relation types - #28376

Merged
sonika-shah merged 6 commits into
open-metadata:mainfrom
sonika-shah:fix/glossary-term-relation-cardinality-default
Sep 18, 2026
Merged

sonika-shah merged 6 commits into
open-metadata:mainfrom
sonika-shah:fix/glossary-term-relation-cardinality-default

Conversation

@sonika-shah

@sonika-shah sonika-shah commented May 22, 2026

Copy link
Copy Markdown
Collaborator

Summary

Fixes collate#4212: GET /v1/system/settings/glossaryTermRelationSettings returned the system-seeded relation types with cardinality: null. The UI fell back to MANY_TO_MANY and, on the next PUT, persisted that fallback onto the system relations.

Root cause

SettingsCache.createRelationType seeded each system default without a cardinality, so between server boot and the first settings PUT the defaults shipped with cardinality == null. The PUT path runs GlossaryTermRelationSettingsUtil.normalize(...), but GET had no equivalent, so the null leaked to the client.

Fix

Seed and backfill every system-defined relation with an explicit cardinality so GET always returns a non-null value and the UI never has to guess. All ten system relations are set to MANY_TO_MANY with no sourceMax/targetMax bounds — unbounded, exactly matching the behavior installs have today. This intentionally adds no cardinality enforcement, so existing terms that already have (for example) multiple broader parents keep working and there is no upgrade risk.

On hierarchy semantics: conceptually broader is MANY_TO_ONE (a term has one broader parent; a parent has many children) and narrower is its ONE_TO_MANY inverse. We deliberately do not encode those as enforced cardinalities here — turning on a sourceMax=1 cap in a patch release would start rejecting existing terms that have multiple parents on their next edit. Enforcing single-parent hierarchy is a separate, opt-in change that needs a data-cleanup migration, and is out of scope for this fix.

  1. SettingsCache.java — every system default is now created with MANY_TO_MANY and null bounds.

  2. 2.0.3 data migration (migration/utils/v203/MigrationUtil + mysql|postgres/v203/Migration) — backfills MANY_TO_MANY on system-defined relations whose cardinality is still null on existing installs, deriving bounds through the canonical GlossaryTermRelationSettingsUtil.normalize() (single source of truth, so enum and bounds cannot disagree). Custom, user-defined relations are left untouched.

  3. GlossaryTermRelationSettingsIT.test_systemRelationTypesAreUnboundedManyToMany — asserts the exact cardinality (MANY_TO_MANY) and null sourceMax/targetMax for all ten system relations, so a wrong or bounded value fails the test.

Test plan

  • Backend build: mvn clean install -pl openmetadata-service,openmetadata-integration-tests -am -DskipTests — BUILD SUCCESS.
  • CI green: all Integration Test Lanes (parallel, multi-node, global-state, retry-queue, postgres-es-redis) and maven-collate-ci pass, including the new IT.
  • Validated on an isolated local stack (fresh MySQL 8 + Elasticsearch 9.3.0):
    • Fresh install — all 10 system relations seed as MANY_TO_MANY, sourceMax=null, targetMax=null.
    • Upgrade path — simulated an old install (nulled the cardinalities), ran the v203 migration, and confirmed all 10 were backfilled to MANY_TO_MANY with null bounds.

Copilot AI lite review requested due to automatic review settings May 22, 2026 13:10
@github-actions github-actions Bot added backend safe to test Add this label to run secure Github workflows on PRs labels May 22, 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

Fixes an inconsistency in the System Settings API where system-seeded glossary term relation types were returned without a cardinality field (while custom relation types included it). The change ensures defaults always include cardinality and backfills it on reads for existing installations seeded before this fix.

Changes:

  • Seed default glossary term relation types with an explicit RelationCardinality (currently MANY_TO_MANY) during initial settings creation.
  • Backfill missing cardinality on GET /v1/system/settings/glossaryTermRelationSettings without mutating sourceMax/targetMax.
  • Extend the integration test to assert cardinality is present and non-null for all returned relation types.

Reviewed changes

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

File Description
openmetadata-service/src/main/java/org/openmetadata/service/resources/system/SystemResource.java Backfills missing cardinality in glossary term relation settings on read via getSettingByName.
openmetadata-service/src/main/java/org/openmetadata/service/resources/settings/SettingsCache.java Ensures system-seeded default relation types are created with cardinality populated.
openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/GlossaryTermRelationSettingsIT.java Adds assertions that cardinality exists and is non-null in the settings GET response.

The system-seeded glossary term relation types (relatedTo, synonym, broader,
narrower, partOf, hasPart, etc.) were created without a cardinality value, so
the GET /v1/system/settings/glossaryTermRelationSettings response omitted the
field for defaults while returning it for custom relations.

The UI fell back to MANY_TO_MANY for every default and, on the next PUT, wrote
that fallback back into the system relations - overriding any intended value.

Set the expected cardinality on each default in SettingsCache and add a 2.0.2
data migration that backfills the field for installs that were already seeded
without it. broader/narrower follow strict SKOS semantics (ONE_TO_MANY /
MANY_TO_ONE - a term has at most one broader parent); all other system
relations stay MANY_TO_MANY.
@sonika-shah
sonika-shah force-pushed the fix/glossary-term-relation-cardinality-default branch from 7446c7e to 60f2219 Compare May 22, 2026 13:27
@sonarqubecloud

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown
Contributor

🟡 Playwright Results — all passed (14 flaky)

✅ 4241 passed · ❌ 0 failed · 🟡 14 flaky · ⏭️ 87 skipped

Shard Passed Failed Flaky Skipped
✅ Shard 1 299 0 0 4
🟡 Shard 2 799 0 4 8
🟡 Shard 3 797 0 3 8
🟡 Shard 4 839 0 2 12
🟡 Shard 5 718 0 1 47
🟡 Shard 6 789 0 4 8
🟡 14 flaky test(s) (passed on retry)
  • Features/DataQuality/ColumnLevelTests.spec.ts › Column Values To Be Not Null (shard 2, 1 retry)
  • Features/DomainTierCertificationVoting.spec.ts › DataProduct - Certification assign, update, and remove (shard 2, 1 retry)
  • Features/Glossary/GlossaryP3Tests.spec.ts › should handle multiple rapid API calls (shard 2, 1 retry)
  • Features/Glossary/MUIGlossaryMutualExclusivity.spec.ts › MUI-ME-T01: Apply single ME glossary term and save Data Product (shard 2, 1 retry)
  • Features/OntologyExplorerE2E.spec.ts › toggling edge labels off and back on leaves the graph and cardinality map intact (shard 3, 2 retries)
  • Features/RTL.spec.ts › Verify Following widget functionality (shard 3, 1 retry)
  • Flow/IngestionBot.spec.ts › Ingestion bot should be able to access domain specific domain (shard 3, 1 retry)
  • Flow/PersonaFlow.spec.ts › Set default persona for team should work properly (shard 4, 1 retry)
  • Pages/CustomProperties.spec.ts › Enum (shard 4, 1 retry)
  • Pages/ExplorePageRightPanel_KnowledgeCenter.spec.ts › Should remove user owner for knowledgeCenter (shard 5, 1 retry)
  • Pages/GlossaryImportExport.spec.ts › Glossary CSV import preserves typed relations (shard 6, 1 retry)
  • Pages/Lineage/DataAssetLineage.spec.ts › Column lineage for dashboard -> container (shard 6, 1 retry)
  • Pages/Lineage/LineageFilters.spec.ts › Verify Impact Analysis service filter selection (shard 6, 1 retry)
  • Pages/ODCSImportExport.spec.ts › Multi-object ODCS contract - object selector shows all schema objects (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

@github-actions

Copy link
Copy Markdown
Contributor

This PR has had no activity for 30 days and will be closed in 7 days if no further activity occurs.
Feel free to reopen it if you'd like to continue working on it.

@github-actions github-actions Bot added the Stale label Jun 22, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Closing due to inactivity. Reopen anytime to continue.

@github-actions github-actions Bot closed this Jun 30, 2026
@sonika-shah sonika-shah reopened this Sep 16, 2026
@sonika-shah
sonika-shah requested a review from a team as a code owner September 16, 2026 06:02
The glossary cardinality backfill bound the settings payload as a string
into openmetadata_settings.json, which is jsonb on Postgres - Postgres
refuses the implicit varchar->jsonb cast and the whole migration (and
therefore TestSuiteBootstrap / every IT in the lane) aborted. Select the
DB-specific UPDATE the way PiiRecognizerMigrationUtil / v1126 do: plain
:json for MySQL, :json::jsonb for Postgres.
Copilot AI review requested due to automatic review settings September 17, 2026 12:11

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 safe to test Add this label to run secure Github workflows on PRs and removed safe to test Add this label to run secure Github workflows on PRs labels Sep 17, 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.

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

…MANY

Resolves the enum/bounds inversion flagged in review by not imposing any
per-relation cardinality on the system defaults - the original bug was only
that GET returned a null cardinality, not that hierarchy needed enforcing.

- SettingsCache: broader/narrower now MANY_TO_MANY with null bounds, matching
  every other system default. GET returns a non-null cardinality and the UI no
  longer falls back to (and persists) a guessed value - with zero new
  enforcement, so existing terms with multiple broader parents keep working.
- v203 migration: backfill null cardinality on system-defined relations to
  MANY_TO_MANY and derive bounds via the canonical normalize(), deleting the
  hand-written (inverted) bounds switch and per-relation map.
- IT: assert exact cardinality (MANY_TO_MANY) and null sourceMax/targetMax for
  all 10 system relations, so an inverted or bounded value fails the test.

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 Sep 17, 2026

Copy link
Copy Markdown
Code Review ✅ Approved 4 closed / 4 findings

Adds explicit cardinality metadata to system-defined glossary relation types and introduces a 2.0.3 migration to backfill missing cardinalities on existing installations. Resolved cardinality bounds inversions for broader/narrower relations, applied backfill to the list endpoint, and improved code quality with final fields and immutable collections.

✅ 4 closed
Bug: backfillMissingCardinality not applied in list endpoint

📄 openmetadata-service/src/main/java/org/openmetadata/service/resources/system/SystemResource.java:243-252 📄 openmetadata-service/src/main/java/org/openmetadata/service/resources/system/SystemResource.java:193-207
The backfillMissingCardinality logic is only applied in getSettingByName (line 249), but the list method (line 178) can also return GLOSSARY_TERM_RELATION_SETTINGS without backfilling cardinality. Any client hitting GET /v1/system/settings (the list endpoint) will still see cardinality: null for legacy-seeded relation types, defeating the purpose of the read-time backfill.

Consider applying the same backfill in the list method when iterating over results, or extracting a shared post-processing step.

Quality: MigrationUtil.handle field should be final

📄 openmetadata-service/src/main/java/org/openmetadata/service/migration/utils/v202/MigrationUtil.java:20
The handle field is set once in the constructor and never reassigned. Per project guidelines, non-changing fields should be declared final.

Quality: Use Map.of() for immutable SYSTEM_DEFAULT_CARDINALITIES

📄 openmetadata-service/src/main/java/org/openmetadata/service/migration/utils/v202/MigrationUtil.java:75-88
The systemDefaultCardinalities() method builds a mutable HashMap for what is effectively a constant lookup table. Per project guidelines (use immutable collections, Java 21 features), this should use Map.of() which is both shorter and guarantees immutability.

Bug: Migration inverts ONE_TO_MANY/MANY_TO_ONE cardinality bounds

📄 openmetadata-service/src/main/java/org/openmetadata/service/migration/utils/v203/MigrationUtil.java:90-104
applyCardinalityBounds maps ONE_TO_MANY to sourceMax=1/targetMax=null and MANY_TO_ONE to sourceMax=null/targetMax=1, but the authoritative GlossaryTermRelationSettingsUtil.normalize() (used on every settings PUT) maps them the opposite way: ONE_TO_MANY -> sourceMax=null/targetMax=1 and MANY_TO_ONE -> sourceMax=1/targetMax=null. So the migration writes broader/narrower rows whose cardinality enum and source/target bounds disagree. On the first PUT the normalizer will flip the bounds, silently changing which side is constrained and breaking the intended "one broader parent" semantics the PR sets out to fix. Reuse normalize() (or match its switch) so bounds are derived from the single source of truth.

Review coverage

Rules No rules evaluated

Functional validation Not enabled · Set up

Auto-approval Not enabled · Set up

Options

Display: compact → Counting what did not apply, without listing it.

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

Compact
gitar display:verbose         

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

@sonarqubecloud

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown
Contributor

🤖 Backport to 2.0 is under way. It is built from openmetadata-collate, which holds the credentials needed to open a PR here, and it will comment again with the result — nothing is needed from you unless it reports otherwise.

@github-actions

Copy link
Copy Markdown
Contributor

🤖 Backport to 1.13 is under way. It is built from openmetadata-collate, which holds the credentials needed to open a PR here, and it will comment again with the result — nothing is needed from you unless it reports otherwise.

@gh-bot-collate

Copy link
Copy Markdown

🤖 Backport to 2.0 opened as #33554 (clean cherry-pick). It merges itself once the checks pass.

@gh-bot-collate

Copy link
Copy Markdown

🤖 Backport to 1.13 failed. The conflict could not be resolved, so this change is not on that branch. Resolver run.

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 To release Will cherry-pick this PR into the release branch

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants