Skip to content

Fixes #35984: Store ontology concept bindings as rows and serve a paged native data graph - #35996

Merged
harshach merged 172 commits into
mainfrom
harshach/ontology-studio/12-binding-rows
Oct 30, 2026
Merged

harshach merged 172 commits into
mainfrom
harshach/ontology-studio/12-binding-rows

Conversation

@harshach

@harshach harshach commented Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #35984

Concept bindings use database rows and paged reads, so the editor preserves bindings beyond the first page. The native data graph returns ranked, permission-filtered concept and asset data with bounded scans. This branch includes the reviewed paging, Topic hydration, editor recovery and graph changes.

The published stack targets release 2.1.0. Each linked Shipping issue is In Progress with Product, P1 and Governance metadata.

PR1 #35990, PR2 #35994, PR3 #35997, PR4 #35995 and PR10 #35991 are merged. Two chains continue from main:

This PR targets main. Its child is PR15 #35989. PR9, PR11, PR13, PR14 and PR16 are not published yet.

Current assessment for 32f98026b44, recorded 2026-10-30.

Verdict. PASS. A scoped independent review passed the merge at 77de9395387, which resolved six conflicts with main's AI-mode restyle. Every later head comes from merges that keep PR12's patch-id 7aaee8552f9, so the verdict carries to this head. After PR4 landed, a3c3e0fb320 merged PR10's landing update 758d0214ed7. PR10 then landed on main as the squash b1941043ab4. This head merges main's 5f760856698 (#36560), which changes three workflow task files that PR12 does not touch. It then records PR10's squash with an ours merge, which leaves the tree unchanged. Neither merge had a conflict. No minor issue is declared.

Test evidence. The gates of the merge at 77de9395387 ran on its staged trees. Package and spotless passed, and 674 unit tests in 51 classes passed. The database classes passed 46 cases per engine on local MySQL 9.3 and PostgreSQL 14, apart from two invocations of OntologyEntityDAOTest.reviewLocksSeeEditsAndArchivalCommittedWhileWaiting, whose rows share one database on the local engines. OntologyConceptBindingMigrationTest skips its 3 cases without Docker. TypeScript reported 0 errors, Jest passed 189 tests in 13 suites, and ESLint, Prettier and the i18n check were clean. At a3c3e0fb320, where PR4's content meets PR12's in OntologyGraphView.test.tsx, Jest passes 41 of 41 in the two graph view suites. The Java impact map check passed on each push.

CI. Checks at 32f98026b44 were still running when this block was written. This PR now targets main. At a3c3e0fb320, its only failing check was maven-collate-ci, which failed with HTTP 422 because the Collate repository has no branch named after the old base.

Still owed. None of these has run yet. Run each IT on -Pmysql-elasticsearch and then -Ppostgres-opensearch, and confirm a Tests run: line.

  • This PR's own ITs NativeOntologyDataGraphPageIT, NativeOntologyDataGraphIT, NativeOntologyDataGraphContextIT, NativeOntologyDataGraphBindingScanIT, NativeOntologyDataGraphEdgeScanIT, OntologyConceptBindingsIT, OntologyConceptBindingCleanupIT, OntologyResourceIT and GlossaryCsvRelationTypesIT, then the PR10 list.
  • TopicResourceIT and TopicServiceHydrationIT, then the migration ITs ContinuousMigrationIT, OpenMetadataOperationsIT and SeedDataPresenceIT, and ConversationSchemaMigrationIT and MetricMigrationIT in the global-state lane.
  • Every merge-queue IT that main's changes select.
  • The two invocations of OntologyEntityDAOTest.reviewLocksSeeEditsAndArchivalCommittedWhileWaiting, and the container-backed classes once under Testcontainers on mysql:8.0 and postgres:15: OntologyConceptStatusStorageTest, OntologyDerivedStatusStorageTest, OntologyEntityDAOTest, OntologyReleaseComparisonStorageTest, OntologyReleaseDAOTest, OntologyReleaseReviewListTest, OntologyWorkingStateTest, OntologyHierarchyStorageTest and OntologyPropertyInverseStorageTest, plus OntologyPrefixLineageDaoTest and OntologyConceptBindingMigrationTest.
  • A live restart check with FERNET_KEY=no_encryption_at_rest. A topology continuation returns 200 before the restart and 400 Invalid native graph topology cursor after it.
  • Playwright OntologyNativeOperations.spec.ts against a live stack. It drives classic mode.
  • Changed-class coverage, which needs the ITs above.
  • The plan's live and performance lanes for this PR.

AI mode. main's Ontology Studio AI-mode restyle #36433 now gates new screens on AI mode. Paths are under openmetadata-ui/src/main/resources/ui/src/components/governance/ontology/OntologyConceptInspector/. AI mode lacks two PR12 guards, which render only in classic branches. In OntologyConceptAssets.tsx, the Add data assets button stays disabled while the bindings cannot be edited. AI mode's section-header add has no such state, but the picker it opens keeps the guard on Save. In OntologyColumnBindingEditor.tsx, the AI Save stays enabled without an editable snapshot, and pressing it writes nothing and shows nothing. Every write carries the snapshot's revision, which the server checks.

Not verified yet. This PR is verified only when its unit, live and performance boxes in the plan are all checked. The operator lands the stack.

Earlier description and verification records

These records preserve the earlier design notes, test results and attachments. Earlier statements about the current head or readiness apply to their recorded revisions. The integration assessment above supersedes those statements.

Describe your changes:

Fixes #35984

This amends ADR 2026-10-19-native-concept-bindings-remain-operational-metadata to v2. Native concept bindings now use one database row per concept and asset, removing the 100-asset ceiling while retaining revision checks and the existing per-asset and column limits. A native ontology data graph returns ranked concept clusters, property edges, and observed lineage.

Type of change:

  • Improvement

High-level design:

The MySQL and PostgreSQL 2.1 migration creates ontology_concept_binding, keyed by (conceptId, assetId) with an asset lookup index. An idempotent data migration copies legacy extension documents into rows. The extension retains the revision; writes and pruning remain under the ontology lock. Retrying an identical binding is a no-op. Current head is aa7ef4419f7; verification provenance below distinguishes the earlier backend artifact from the final cleanup.

GET /ontologyConcepts/{id}/bindings?limit=&after= orders by asset ID and resolves service types in batches. Each request examines at most ten times its requested page size. A reader hidden from most assets can receive an empty page with an after cursor and must continue until that cursor is absent. The existing editor adapter collects all read and mutation pages, retaining the initial revision so concurrent changes still conflict. Selecting a previously bound asset beyond page one therefore preserves its column assignments. The new graph UI remains in PR13.

GET /ontologies/{id}/dataGraph ranks bound concepts by stored binding count and name. Each cluster reports the caller's visible live asset count and up to four assets, with a binding cursor for the rest. Hidden concepts and clusters with no visible assets are omitted. Concept paging totals describe stored bound concepts. Property labels/references respect property visibility; lineage joins only returned assets. Graph queries batch lookups by entity type.

Tests:

Use cases covered

  • More than 100 bindings page without duplicates or gaps, with service types and unchanged revision semantics.
  • Hidden/deleted assets, hidden concepts, hidden property labels, and denied ontology access.
  • Ranked clusters, cluster-to-bindings cursors, assertion/restriction edges, and observed lineage.
  • Real MySQL/PostgreSQL legacy copy and retry behavior, SQL migration ordering, and measured graph query counts.

Unit tests

At production commit 6e7d3d762d8, a fresh root run passed 289 raw unit cases without failures, errors, or skips. At test-only commit aa7ef4419f7, all 31 planner-selected service classes passed 283 raw cases without failures, errors, or skips. The fresh XML contains every selected class. Scoped Spotless and commit hooks passed without changing the frozen test sources.

Backend integration tests

The current production artifact passed 704 raw HTTP cases with 94 skips on each required engine. Separate 178-case HTTP and 287-case import/search lanes also passed; import/search had 13 skips. After the test-only change, four exact cases passed without failures, errors, or skips on each required engine. They cover descendant CSV export, import dry run and persisted relations, ontology graph assets and lineage, and impact reassignment. Strict whole-class line coverage exceeds 90% for all 12 changed executable top-level classes on both engines. GlossaryTermRepository is lowest at 1263/1403 lines (90.02%). Both four-case runs matched frozen test, production source, compiled class, and service JAR hashes. Earlier failed graph diagnostics are preserved.

The full integration workflows passed at production-identical 6e7d3d762d8:

The same workflows are running at current head aa7ef4419f7:

LOCAL PASS: 31 planner-selected service classes, 283 raw cases, and zero failures, errors, or skips at aa7ef4419f7. Four focused IT cases passed on each required engine against byte-identical production artifacts. All 12 changed executable top-level classes exceed 90% whole-class line coverage. Full current-head integration workflows are running above.

Ingestion integration tests

Not applicable: no ingestion changes.

Playwright (UI) tests

Five focused Jest suites passed 48/48. After correcting the test mock types, the two directly edited suites passed 32/32 again. A counterfactual without pagination reached Save and failed because the stored column assignments became empty, then the adapter was restored. The existing NativeOperations Chromium test passed 1/1 against the current UI and private packaged backend, covering link, nested-column assignment, reload, unlink, and unchanged semantic state. This change adds no new graph view.

Manual testing performed

The isolated packaged server at 2fa529727d2 passed five API proof checks. It stored 150 bound tables across Snowflake, BigQuery, and MySQL, returned pages of 40/40/40/30, reported graph counts of 150 and 2 with four initial assets and the remaining cluster cursor, returned the property and lineage edges, rejected a stale write and kept an identical retry at revision 150, and exposed only the visible Kafka topic to a reader denied table access. All four containers were inspected on the private om-pr12-network. Receipts are swarm/pr12/owner/api-proof.json and private-network.txt.

UI screen recording:

No new UI view. This PR preserves existing editor behavior through its REST adapter; the graph UI is a separate change.

Checklist:

  • PR title and issue link follow the repository convention.
  • JSON Schemas, generated TypeScript, and both database migrations are included.
  • Unit and API integration tests cover the changed behavior.
  • ADR v2 records paging, storage, ranking, and authorization behavior.
  • Branch impact-map check and Spotless pass. make harness-check exits zero with 12 unrelated/general warnings. UI organize-imports, ESLint (zero errors), Prettier, license headers, i18n sync, and app-docs generation pass without unrelated changes.
  • All 12 changed executable top-level classes exceed 90% whole-class line coverage on both required engines. Root independent review, current-head full integration CI, UI and latency gates, and the final verdict remain open. Full application TypeScript still fails with the same 562 diagnostics at base ba6d06c88bd and this branch; the differential adds and removes zero diagnostics. No overall TypeScript pass is claimed.

RetriggerView in GreptileConfidence Score: 5/5

The reviewed changes appear safe to merge; no new blocking issue was established.

Summary

Moves concept bindings into database rows and adds paged native graph reads.

  • Ontology bindings are stored per asset and read page by page.
  • Ontology graph requests return ranked, visible results in bounded pages.
  • Topic responses load their service references in batches.
  • Glossary CSV tests cover relation types during import and export.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  Editor[Binding editor] --> Read[Collect every binding page]
  Read --> Gate[Allow edits from a complete read]
  Gate --> Write[Write with expected revision]
  Write --> Lock[Lock ontology]
  Lock --> Rows[(Binding rows and revision)]
  Write --> Refresh[Read every page again]
  Refresh --> Gate
  Graph[Graph request] --> Checks[Check concepts and permissions]
  Checks --> Scan[Read bounded topology pages]
  Scan --> Cursor[Encrypt continuation cursor]
Loading

Reviews (21) · Last reviewed commit: "Merge native relations fixes into bindin..." · Reviewed by Greptile

First unit of #35983. The enum carries the Any or All reviewer rule that
later commits put on the ontology, freeze into the review target, and
turn into the review task's approval threshold.
…and CSV

Make the preview request's format optional, add CSV and the uploaded file
name, and add the preview response that wraps the change set with the
detected format, the source label, and per-kind counts.
One row per concept and bound asset, keyed by concept id and asset id, with
the asset type, the asset FQN hash and the bound column paths, indexed by
asset id. The parity test now checks the table exists in both dialects.

Part of #35984.
…ionship types

Decision record for the native relation picker: registry types become native
object properties matched by exact predicate IRI, inverses stay reciprocal on
write, and the create option is gated on the relationshipType permission.
@harshach
harshach requested a review from a team as a code owner October 22, 2026 15:12
@harshach harshach added safe to test Add this label to run secure Github workflows on PRs backend labels Oct 22, 2026
@harshach
harshach requested a review from sonika-shah October 22, 2026 15:12
@github-actions

github-actions Bot commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

✅ PR checks passed

The linked issue has a description and all required Shipping project fields set. Thanks!

harshach and others added 7 commits October 22, 2026 08:16
Adds the fields to the concept and property schemas, the create
requests, the release payload allowlist, and the RDF catalog vocabulary.
Adds the DEPRECATED change type, the REPLACED_BY reference role, and the
concept-grouped rows of the working diff. Regenerates the TypeScript
types.
…ts into rows

OntologyConceptBindingDAO reads and writes ontology_concept_binding rows,
ranks the bound concepts of an ontology, and probes which bound assets are
live together with their service type. The v210 data migration copies every
ontology.conceptBindings document into rows, skips assets whose entity row is
gone, and leaves rows that already exist alone, so a re-run changes nothing
and never undoes an edit made through the row store. The documents stay
because they still carry each concept's revision.

Part of #35984.
The request, the summary, and the detection response now reference one
format definition with its own Java type, NativeOntologyImportFormat. A
reference to the request's property made jsonschema2pojo generate a
second enum per schema. The preview response wraps a summary and the
change set; the summary carries the detected format, the source label,
and eight counts.

The RDF reader returns a NativeOntologyDocument: the source's definitions
sorted by IRI and axiom signature, plus the ontology header when the
source declares exactly one. Axiom signatures move with it.
The IT states the contract before the implementation: setting, creating with,
repointing, clearing, deleting and restoring an object property's inverse keeps
both sides reciprocal; a one-directional import normalizes on apply and is
reused by exact IRI; the release export carries owl:inverseOf both ways.
Concepts and properties now carry deprecation and replacement, and
concepts carry abstract and a definition source:

- A replacement must name another definition of the same kind in the
  same ontology. It is indexed as a REPLACED_BY reference, so deleting
  a replacement target is refused like any other dangling reference.
- An enumeration needs at least two allowed values. One-value
  enumerations stored before this rule stay editable until their values
  change.
- Release projection writes om:abstract, dct:source, owl:deprecated,
  and dct:isReplacedBy, and native import reads them back.
- Release and working diffs classify a member whose deprecated flag
  turned true as DEPRECATED.
- GET /ontologies/{id}/working/diff?groupBy=concept returns the working
  changes grouped by the concept they define, with parent labels and a
  property count, paged by group under its own revision-bound cursor.
Bindings are read and written as ontology_concept_binding rows. The
extension document keeps only the per-concept revision, compared under
the ontology lock as before. GET /ontologyConcepts/{id}/bindings takes
limit (1..100) and after, returns serviceType per asset from one batched
lookup per page, and returns an after cursor only when a next page
exists. PUT and DELETE return the first page. The per-asset (100) and
per-concept (500) column limits stay; the 100-asset limit is gone.
…sion

IndependentOntologyClassModelIT covers the field round trips, the
enumeration minimum and its legacy carve-out, replacedBy integrity, the
DEPRECATED diff row, and grouped diff paging with the 412 cursor rule.
NativeOntologyMemoryIT adds a class-create change set and its rollback.
Existing ITs that stored one-value enumerations now use two values.
@github-actions

github-actions Bot commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Jest test Coverage

UI tests summary

Lines Statements Branches Functions
Coverage: 80%
80.29% (133227/165917) 66.98% (47297/70610) 70.04% (26793/38251)

Adds the fields to the concept and property schemas, the create
requests, the release payload allowlist, and the RDF catalog vocabulary.
Adds the DEPRECATED change type, the REPLACED_BY reference role, and the
concept-grouped rows of the working diff. Regenerates the TypeScript
types.
Concepts and properties now carry deprecation and replacement, and
concepts carry abstract and a definition source:

- A replacement must name another definition of the same kind in the
  same ontology. It is indexed as a REPLACED_BY reference, so deleting
  a replacement target is refused like any other dangling reference.
- An enumeration needs at least two allowed values. One-value
  enumerations stored before this rule stay editable until their values
  change.
- Release projection writes om:abstract, dct:source, owl:deprecated,
  and dct:isReplacedBy, and native import reads them back.
- Release and working diffs classify a member whose deprecated flag
  turned true as DEPRECATED.
- GET /ontologies/{id}/working/diff?groupBy=concept returns the working
  changes grouped by the concept they define, with parent labels and a
  property count, paged by group under its own revision-bound cursor.
…sion

IndependentOntologyClassModelIT covers the field round trips, the
enumeration minimum and its legacy carve-out, replacedBy integrity, the
DEPRECATED diff row, and grouped diff paging with the 412 cursor rule.
NativeOntologyMemoryIT adds a class-create change set and its rollback.
Existing ITs that stored one-value enumerations now use two values.
PR2 landed on main as 8c01d4c, a squash of ae01b93 with the same tree.
This merge records the squash so that main is an ancestor of this branch. The
tree does not change.
A new case puts another author's unlinked "approves" in the registry and
creates "approved by" with the inverse "approves". The view registers
"approves2" and links only that type back, so it never claims a type it
did not register.

The case where the back link failed now also checks that the retry
writes the relation once and that only the first failure is reported.
After the back link of "approved by" fails, the author retries with the
label "signed off by" and the same inverse "approves". The new case
expects the retry to link "approves" back to "approvedBy" and to give
"signedOffBy" its own inverse, "approves2", so no type is the inverse of
two primaries. It fails until the view finishes the earlier pair before
it plans the retry.
When the back link fails, the primary names the inverse but the inverse
names nothing. A retry under a new label reused that inverse for the new
primary, so the inverse ended up named by two primaries.

Before it plans a retry, the view now links the inverse it registered
back to the type that already names it, whatever the retry is called,
and then plans the draft with no reuse, so the new primary gets its own
inverse. The inverse is reused only while nothing names it, after a
failed primary write. Only the inverse this view registered is patched.
Two cases in OntologyGraphView.test.tsx pin behavior the view already has.
A retry whose back-link repair fails keeps the remembered inverse, so a
later retry links it back. A retry drawn from the other end, under the
inverse's own label, still links the earlier pair back.
After the back link of "approved by" fails, the author draws the
relation from the other end and types "approves". The case already
checks that the retry links the earlier pair back. It now also checks
the relation the retry writes: the property "approvedBy" first, then
"approves" naming it as its inverse, and one axiom on "approves". That
is what a first draw of "approves" writes. It fails until the view
looks the label up in the registry as the repair left it, because the
retry still writes "approves" with no inverse.
When the back link of "approved by" fails and the author retries by
drawing "approves", the view links the pair back in the registry. It
then looked the label up in the list it loaded before that repair,
where "approves" names no inverse. So the retry wrote the property
"approves" with no inverse and never declared "approvedBy". A first
draw of "approves" writes both properties, linked.

finishUnlinkedPair now returns the registry with the patched inverse in
place of the stale entry, and the view looks the label up and plans the
draft in that list. The retry now writes "approvedBy", then "approves"
naming it, then one axiom, as a first draw does. The view still patches
only the inverse it registered, keeps it when the repair fails, and
reports the failed write once.
PR3's landed head 9021493 brings main through 8c01d4c and records PR2's
squash merge. The merge had no conflicts.
Main moved to daec19a before PR3 landed. The merge had no conflicts.
PR3 landed on main as 5575a2d, a squash of 9021493 merged with daec19a.
This merge records the squash so that main is an ancestor of this branch. The
tree does not change.
PR4's landed head 8f4748f brings main through 5575a2d and records PR3's
squash merge. The merge had no conflicts.
Main moved to aeb3cfc before PR4 landed. The merge had no conflicts.
PR4 landed on main as 92d4e43, a squash of 8f4748f merged with aeb3cfc.
This merge records the squash so that main is an ancestor of this branch. The
tree does not change.
PR10's landing update 758d021 brings PR4's squash merge 92d4e43 and main
through aeb3cfc, with no change to this PR's own lines. The merge had no
conflicts.
Base automatically changed from harshach/ontology-studio/10-import-detection to main October 30, 2026 00:01
Main moved to 5f76085 before PR10 landed. It changes three workflow task
files that this PR does not touch. The merge had no conflicts.
PR10 landed on main as b194104, a squash of 758d021 merged with 5f76085.
This merge records the squash so that main is an ancestor of this branch. The
tree does not change.
@gitar-bot

gitar-bot Bot commented Oct 30, 2026

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

🔴 High risk · Moving ontology bindings into rows and adding paged graph reads could lose associations or reveal assets if migration or permission filtering fails.

Moves ontology concept bindings into database rows with paged reads and adds a permission-filtered native data graph endpoint. Resolved the missing ADR amendment reference, unbounded per-concept binding scans on GET, and undocumented data graph limits. One wont_fix item remains: the data graph loads and authorizes every binding row of each concept, accepted without a fix in this PR. No open findings.

✅ 4 closed
✅ Quality: New binding table cites an ADR amendment v2 that does not exist

📄 bootstrap/sql/migrations/native/2.1.0/mysql/schemaChanges.sql:1103-1113 📄 bootstrap/sql/migrations/native/2.1.0/postgres/schemaChanges.sql:865-875
Both migrations cite ADR:2026-10-19-native-concept-bindings-remain-operational-metadata, amendment v2. That record is still Accepted at v1 2026-10-19 (initial), and it says "Store bindings under the concept UUID in the ontology.conceptBindings entity extension". It also sets the 100-asset cap. This commit adds a separate row store ontology_concept_binding(conceptId, assetId, …), which goes against the storage rule in that record, and the citation points to text the tree does not contain. The PR description says the amendment will come later, but if this unit merges by itself, the schema change goes in without its record.

The amendment would say: "Store native concept bindings as one row per concept and asset in ontology_concept_binding, keyed by (conceptId, assetId). The per-concept revision stays in the ontology.conceptBindings extension and is compared under the ontology lock. The 100-asset-per-concept cap is removed." Add it to the ADR as revision v2 in this PR, or remove the citation until the amendment lands.

✅ Performance: Bindings page read can scan every row of a concept on one GET

📄 openmetadata-service/src/main/java/org/openmetadata/service/ontology/OntologyConceptBindingService.java:216-230
page() keeps fetching limit + 1 rows, and for each batch runs a live-asset query, a bulk entity load (tables also load their columns) and a per-entity authorization, until it finds limit + 1 visible assets or runs out of rows. Hidden and deleted rows don't count toward the page, and the read path never prunes deleted rows. Now that the 100-asset cap is gone, a reader who can see few or none of a concept's assets makes one GET walk the concept's whole binding set, which is O(N) entity hydration per request. A fix is to cap how many rows one request may scan (for example MAX_SCAN = 10 * limit). When the cap is reached, return the visible assets found so far with an after cursor at the last scanned row, so the caller resumes from there instead of the server walking the rest.

✅ Quality: Data graph limits are judgment calls missing from the ADR amendment

📄 openmetadata-service/src/main/java/org/openmetadata/service/resources/ontology/OntologyResource.java:347-352 📄 docs/decisions/2026-10-19-native-concept-bindings-remain-operational-metadata.md:34
The new endpoint sets fixed bounds: limit ≤ 12 (default 12), offset ≤ 48, assetsPerConcept ≤ 4, and edgeLimit/lineageEdgeLimit ≤ 500 (default 100). Edge candidate scans also use ontologyRelationCandidateLimit. The v2 amendment records the bindings limit of 100 and the ten-times scan budget, but none of these dataGraph bounds. With offset ≤ 48 and limit ≤ 12, a reader can never page past the first 60 bound concepts, even though paging.total reports more. That is a product rule clients must agree with. Add to the amendment a sentence like: "GET /ontologies/{id}/dataGraph returns at most 12 concepts per page with offset ≤ 48 (the first 60 ranked concepts), at most 4 preview assets per concept, and at most 500 concept and lineage edges." Otherwise lift the offset cap.

Closed: Performance: Data graph loads and authorizes every binding row of each concept

📄 openmetadata-service/src/main/java/org/openmetadata/service/ontology/NativeOntologyDataGraphService.java:109-123 📄 openmetadata-service/src/main/java/org/openmetadata/service/ontology/NativeOntologyDataGraphService.java:149-161 📄 openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/OntologyConceptBindingDAO.java:129-135
clusters() calls store.rows(visibleConcepts.keySet()). That runs listForConcepts, which has no LIMIT, so every row of up to 12 concepts is loaded. All of those rows then go through liveAssets and viewableIds, just to compute assetCount and keep the first 4 assets. This PR removes the 100-asset cap, so one popular concept with tens of thousands of bindings makes every dataGraph request read and policy-check all of them. The bindings endpoint has a scan budget for this case; the data graph has none. Fix: give each concept a bounded scan, like page() in the bindings service, taking up to assetsPerConcept+1 visible rows within a budget. Report assetCount from the stored count, or mark it as a lower bound, instead of a full visibility pass.

Verification

Rules No rules evaluated

Functional validation Not enabled

Set up

Auto-approval Not enabled

Set up

For more verification details, comment gitar display:verbose.

Options

Display: compact → Showing compact verification results.

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

@sonarqubecloud

Copy link
Copy Markdown

@harshach
harshach merged commit 6bcc283 into main Oct 30, 2026
76 checks passed
@harshach
harshach deleted the harshach/ontology-studio/12-binding-rows branch October 30, 2026 00:44
manerow pushed a commit that referenced this pull request Oct 30, 2026
…ed native data graph (#35996)

* WIP: add OntologyApprovalRule schema for release review rules

First unit of #35983. The enum carries the Any or All reviewer rule that
later commits put on the ontology, freeze into the review target, and
turn into the review task's approval threshold.

* WIP: native ontology import preview contract with detection, counts, and CSV

Make the preview request's format optional, add CSV and the uploaded file
name, and add the preview response that wraps the change set with the
detected format, the source label, and per-kind counts.

* WIP: record that ontology definition status is derived from releases

* Add the ontology_concept_binding table to the 2.1.0 native migrations

One row per concept and bound asset, keyed by concept id and asset id, with
the asset type, the asset FQN hash and the bound column paths, indexed by
asset id. The parity test now checks the table exists in both dialects.

Part of #35984.

* Fixes #35987: Record that native relations instantiate registry relationship types

Decision record for the native relation picker: registry types become native
object properties matched by exact predicate IRI, inverses stay reciprocal on
write, and the create option is gated on the relationshipType permission.

* Update generated TypeScript types and dereferenced schemas

* WIP: add abstract, source, and deprecation fields to native classes

Adds the fields to the concept and property schemas, the create
requests, the release payload allowlist, and the RDF catalog vocabulary.
Adds the DEPRECATED change type, the REPLACED_BY reference role, and the
concept-grouped rows of the working diff. Regenerates the TypeScript
types.

* Update generated TypeScript types and dereferenced schemas

* Add the concept binding row DAO and the 2.1.0 copy of binding documents into rows

OntologyConceptBindingDAO reads and writes ontology_concept_binding rows,
ranks the bound concepts of an ontology, and probes which bound assets are
live together with their service type. The v210 data migration copies every
ontology.conceptBindings document into rows, skips assets whose entity row is
gone, and leaves rows that already exist alone, so a re-run changes nothing
and never undoes an edit made through the row store. The documents stay
because they still carry each concept's revision.

Part of #35984.

* Scaffold the native import document and one shared import format enum

The request, the summary, and the detection response now reference one
format definition with its own Java type, NativeOntologyImportFormat. A
reference to the request's property made jsonschema2pojo generate a
second enum per schema. The preview response wraps a summary and the
change set; the summary carries the detected format, the source label,
and eight counts.

The RDF reader returns a NativeOntologyDocument: the source's definitions
sorted by IRI and axiom signature, plus the ontology header when the
source declares exactly one. Axiom signatures move with it.

* Add IndependentOntologyInverseIT for reciprocal native inverses

The IT states the contract before the implementation: setting, creating with,
repointing, clearing, deleting and restoring an object property's inverse keeps
both sides reciprocal; a one-directional import normalizes on apply and is
reused by exact IRI; the release export carries owl:inverseOf both ways.

* Validate, project, and diff the class model fields

Concepts and properties now carry deprecation and replacement, and
concepts carry abstract and a definition source:

- A replacement must name another definition of the same kind in the
  same ontology. It is indexed as a REPLACED_BY reference, so deleting
  a replacement target is refused like any other dangling reference.
- An enumeration needs at least two allowed values. One-value
  enumerations stored before this rule stay editable until their values
  change.
- Release projection writes om:abstract, dct:source, owl:deprecated,
  and dct:isReplacedBy, and native import reads them back.
- Release and working diffs classify a member whose deprecated flag
  turned true as DEPRECATED.
- GET /ontologies/{id}/working/diff?groupBy=concept returns the working
  changes grouped by the concept they define, with parent labels and a
  property count, paged by group under its own revision-bound cursor.

* Page concept bindings from rows and drop the 100-asset cap

Bindings are read and written as ontology_concept_binding rows. The
extension document keeps only the per-concept revision, compared under
the ontology lock as before. GET /ontologyConcepts/{id}/bindings takes
limit (1..100) and after, returns serviceType per asset from one batched
lookup per page, and returns an after cursor only when a next page
exists. PUT and DELETE return the first page. The per-asset (100) and
per-concept (500) column limits stay; the 100-asset limit is gone.

* Cover the class model fields in integration tests and record the decision

IndependentOntologyClassModelIT covers the field round trips, the
enumeration minimum and its legacy carve-out, replacedBy integrity, the
DEPRECATED diff row, and grouped diff paging with the 412 cursor rule.
NativeOntologyMemoryIT adds a class-create change set and its rollback.
Existing ITs that stored one-value enumerations now use two values.

* WIP: add abstract, source, and deprecation fields to native classes

Adds the fields to the concept and property schemas, the create
requests, the release payload allowlist, and the RDF catalog vocabulary.
Adds the DEPRECATED change type, the REPLACED_BY reference role, and the
concept-grouped rows of the working diff. Regenerates the TypeScript
types.

* Validate, project, and diff the class model fields

Concepts and properties now carry deprecation and replacement, and
concepts carry abstract and a definition source:

- A replacement must name another definition of the same kind in the
  same ontology. It is indexed as a REPLACED_BY reference, so deleting
  a replacement target is refused like any other dangling reference.
- An enumeration needs at least two allowed values. One-value
  enumerations stored before this rule stay editable until their values
  change.
- Release projection writes om:abstract, dct:source, owl:deprecated,
  and dct:isReplacedBy, and native import reads them back.
- Release and working diffs classify a member whose deprecated flag
  turned true as DEPRECATED.
- GET /ontologies/{id}/working/diff?groupBy=concept returns the working
  changes grouped by the concept they define, with parent labels and a
  property count, paged by group under its own revision-bound cursor.

* Cover the class model fields in integration tests and record the decision

IndependentOntologyClassModelIT covers the field round trips, the
enumeration minimum and its legacy carve-out, replacedBy integrity, the
DEPRECATED diff row, and grouped diff paging with the 412 cursor rule.
NativeOntologyMemoryIT adds a class-create change set and its rollback.
Existing ITs that stored one-value enumerations now use two values.

* Fix class model integration test assertions for wrapped SDK errors and per-operation revisions

* Fixes #35981: Derive concept lifecycle status, ontology publication status and release change summary

* WIP: record that ontology definition status is derived from releases

* Fixes #35981: Derive concept lifecycle status, ontology publication status and release change summary

* WIP: add approvalRule, reviewVotes and the release review read schemas

* Keep native inverses reciprocal on write

OntologyWorkingState runs registered finalizers at the outermost
mutation's exit, before validation. OntologyPropertyRepository registers
one that settles inverse links over the affected neighbourhood: the side
that states a link decides it, the latest statement wins, and a partner
no longer pointed back at is released. Partners are written through the
updater so they version, index and validate like any edit. The RDF
importer reads owl:inverseOf as symmetric, so a one-directional file
describes both sides and a re-import changes nothing.
IndependentOntologyAxiomIT drops its indirect inverse variant, which
relied on one-sided inverse chains.

* Decide ontology release reviews by the ontology's approval rule

An ontology carries an approvalRule, AnyReviewer by default or
AllReviewers. prepareReview freezes the rule into the task's review
target and leaves the requester's and the author's own user entries out
of the assignees. The rule reaches the workflow as the start variable
approvalMode=allAssignees, which CreateTask turns into the task's
approvalThreshold, so the shared counters stay unchanged.

Each counted vote is recorded on the task in reviewVotes with its
comment; the deciding vote is recorded in the decision transaction with
any voter whose record was lost. Reviewers and the approval rule are
locked with 409 while a release of the ontology is in review.

GET /v1/ontologyReleases/{id}/review and
GET /v1/ontologies/reviews?view=assigned|created|pending read a review
from the task's recorded votes.

Amends ADR:2026-10-14-ontology-reviews-bind-workflow-tasks-to-revisions
as v13.

* Record votes only for ontology reviews and test the review read model

Only an ontology review reconciles the deciding vote, so recording
counted votes on any other multi-approval task would always leave its
last vote out and add version bumps to glossary approvals. The waiting
branch now records a vote only for an ontology review.

OntologyReleaseReviewReaderTest reads reviews through the reader with
literal tasks and votes. OntologyReleaseRepository.page and offset are
static so the cursor math has a test with literal rows.

* Validate the net change of each ontology definition

A restore relinks or drops its inverse through the reciprocity finalizer,
which writes the same row a second time inside one mutation. Validation
kept the first row per id, so it still saw the dangling inverse and
refused the restore. OntologyWorkingState now folds a mutation's changes
to one per definition before finalizers and validation see them.

A restored property whose partner was hard-deleted meanwhile drops the
link before its updater runs, so the restore response matches the stored
row, and IndependentOntologyInverseIT covers it on both profiles.

* Detect import formats, count dry-run changes, and read native CSV

* Offer registry relation types with their inverses in the native picker

The native relation picker lists the relationship-type registry in two
groups, Core and Custom, with each type's inverse beside it, plus the
ontology's own object properties that no registry predicate backs.
Picking a type writes the missing property pair with the registry
predicates as IRIs, linked through inverse, then the restriction axiom.
An admin can create a custom type with its inverse from the picker; the
dialog writes both registry types and links them before instantiating.
The concept inspector labels incoming relations with the inverse. The
Playwright spec covers the groups, Part Of, a custom type, and a steward
who sees the types but no create option.

* chore(playwright): auto-refresh impact-map.generated.json

* Skip the reference delete for a definition that does not exist yet

* Keep the review task's own reviewers and declare approvalRule in the RDF ontology

The release inherits its reviewers from the ontology, and the entity
updater drops inherited references from a reviewer list, so the task's
REVIEWS rows vanished on the workflow's first stage update.
OntologyReview.eligibleReviewers now returns plain copies.

A counted vote whose record loses every optimistic-locking retry answers
200 with a warning, because the vote already counted in Flowable and the
decision records it.

openmetadata.ttl declares om:approvalRule, which the ontology projection
writes. The ITs unwrap the SDK's wrapped 409 and fetch the task with its
reviewers.

* Import files with detection, a dry run, and CSV in the import panel

The panel detects the format on the server, shows counts from a dry run,
and imports into the chosen ontology or a new one through the library
installation flow. Documents the native CSV and amends the working-draft
import decision.

* Fixes #35981: Cover property and ontology edits in the unpublished-changes count, drop restating comments

* Match registry inverses by id and link reused property pairs

The server fills only `id` on a relationship type's `inverse` reference,
so the catalog and the picker now resolve a type's inverse by `id`, with
`fullyQualifiedName` as the fallback, instead of by a `name` that is
never set. Custom types with an inverse show both labels again.

When both properties of a pair are already live but the one the type
names does not point at the type's partner, the plan patches that
property's `inverse` instead of leaving the pair unlinked. The spec
covers a second Part Of that reuses the pair and a Subclass Of that
writes only the hierarchy axiom.

* Serve a native ontology data graph from paged binding rows

* Fix review vote durability and authorized pagination

* Preserve off-page column bindings in the existing ontology editor

* Test the documented native ontology CSV example

* Use ordinary record equality for migrated binding rows

* Keep restored inverses and load every registry page

* Attribute inverse cleanup to the deleting user

* fix: persist workflow votes with terminal task decisions

* Guard ontology import file selection and enforce CSV limits

* Fixes #35981: Clarify the derived publication status contract

* Fixes #35987: Map native ontology changes to browser coverage

* Fix ontology import browser fixture expectations

* test: assert ontology review approval rule round trips

* Fix inverse deletion actor test email length

* test(ontology): cover property validation and grouped diff cursors

* test(ontology): reject release headers as captured definitions

* test(ontology): verify import detection prefers content

* test(ontology): cover glossary CSV and asset graph behavior

* Use the registered fixture actor for inverse rollback tests

* Fix PR10 review findings: CSV property kind checks, trailing JSON tokens, Turtle blank-node detection, rejected replacement file

* Recheck review setup under the lock, list reviews by caller, vote per stage

Starting a review now rechecks the frozen approval rule and the chosen
reviewers against the ontology under the review lock and refuses with
409 when either changed since the request was prepared, so the lock has
no window before the release is In Review.

The review list starts from the caller's own open approval tasks, which
the database narrows to the caller as reviewer, assignee or requester,
instead of scanning every release in review and loading its task.

A reviewer's vote is recorded once per workflow stage, so a task that
moves through several approval stages keeps every stage's vote.

Regenerate the API reference so it lists the two review endpoints.

* Cover relationship type reuse after the picker's list went stale

The 409 fallback in OntologyGraphView adopted whichever registry type
held a planned name. A reused primary lost the inverse it already had,
because the inverse was resolved against only the two types in hand,
and a reused inverse had its own partner overwritten by the back-link
patch. These tests pin the behavior the picker should have and fail on
the current code.

* Reload the registry before registering a relationship type

The picker planned names against a list up to 30 s old and recovered
from a 409 by fetching the existing type by name. That treated a name
collision as the same type: a reused primary dropped the inverse the
registry already held, and a reused inverse was re-pointed at the new
primary, orphaning its partner for every glossary and ontology that
uses it.

The picker now reloads the registry after the dialog. A type registered
since the list was cached is reused as it is, with its real inverse.
Otherwise names are planned apart from everything the registry holds,
and the back link is written only to the inverse this flow just
created. A 409 after that is a race and surfaces as an error instead of
being papered over.

* test(ontology): split the native data graph ITs by behavior

NativeOntologyDataGraphIT had grown to 1306 lines. It is now four classes,
one per behavior: NativeOntologyDataGraphIT (ranking, offsets, partial
readers, statement budget), NativeOntologyDataGraphEdgeScanIT (axiom scan
boundaries), NativeOntologyDataGraphBindingScanIT (binding scan boundaries)
and TopicServiceHydrationIT (topic hydration). The REST helpers live in
OntologyBindingFixtures; the raw-SQL seeding and the SQL statement observer
live in NativeOntologyGraphScanProbe. OntologyConceptBindingsIT takes
wideTableAsset from the fixtures. The SQL-counting classes keep @isolated,
and the impact-map globs *Ontology*IT and Topic*IT still select every one.

GlossaryCsvRelationTypesIT reads import results into an ImportResult record,
builds its term requests through one helper and imports URLEncoder instead
of spelling out the package. OntologyResourceIT keeps the asset graph
fixture in a TaggedGraph record and asserts each page through a helper.
Parameters and unchanged locals are final.

* test(ontology): pin paged binding cleanup and lineage between returned assets

OntologyConceptBindingCleanupIT asserts that a successful binding write
reads the concept's rows 500 at a time while it removes rows whose assets
are deleted or missing, and that the capacity check counts the surviving
column assignments on every page.

NativeOntologyDataGraphIT.aLineageEdgeBetweenPreviewAssetsSurvivesEarlierEdgesToUnboundAssets
asserts that a returned asset's edges to assets outside the page cannot
consume the lineage edge limit.

On the current service code the cleanup loads every row of the concept in
one statement, and the lineage query limits candidate edges before it
filters their targets. The next commit changes both.

* fix(ontology): page the binding cleanup and select lineage between returned assets

A successful binding write pruned dead rows by loading every row of the
concept in one statement. prune now walks the rows with
OntologyConceptBindingStore.page in SCAN_PAGE_SIZE (500) keyset pages,
deletes the dead rows of each page and sums the surviving column
assignments for the capacity check. OntologyConceptBindingStore.rows and
OntologyConceptBindingDAO.list are gone, so nothing reads a concept's rows
unbounded. ADR 2026-10-19 v4 records the paged cleanup.

The data graph took its lineage edges from a query that limited candidate
edges before filtering their targets, so a returned asset's edges to assets
outside the page could hide an edge between two returned assets.
OntologyDAO.listLineageEdgesBetween filters both endpoints before the
limit; the glossary query is unchanged.

Review-rule changes in the same files: OntologyConceptBindingStore takes
its asset-table resolver in the constructor, with registeredAssetTables()
as the one registry lookup, and NativeOntologyDataGraphService takes the
store; OntologyResource binds the data graph query values through a
@BeanParam DataGraphQuery; the migration copy and the graph service are
split into methods that do one job each; final on unchanged locals and
parameters, in the unit tests too.

* Fixes #35985: Pair CSV inverse properties the way the RDF reader does

A CSV that names an inverse on one property and leaves it empty on the
partner made the second preview clear the partner's inverse, and applying
it removed the pair. The CSV reader now links the partner back through the
same rule the RDF importer uses, shared on NativeOntologyDocument.

* Test ontology review votes through TaskWorkflowHandler.resolveTask

A non-final vote leaves the task open and records one vote with its comment.
A repeat voter is recorded once, and a voter keeps one vote in each approval stage.

* Split PR12 topic tests into focused helpers and mark unchanged locals final

* Cover relationship type creation for users who cannot edit registry types

A user whose policy grants Create but not EditAll on relationshipType
could register a primary type and its inverse and then fail on the
back-link patch, which the server authorizes as EditAll. The new tests
expect the dialog to disable its inverse field for that user and the
flow to write a single type, plain or symmetric, with no patch. The
first test fails on the current component.

* Offer a registry inverse only to users who can edit relationship types

The inverse's back link is a PATCH of /inverse on the inverse type,
which the server authorizes as EditAll on relationshipType, and the
creator holds no owner right on a type it just made. The create dialog
now disables the inverse field for a user without that right and shows
the standard permission message under it, so a create-only user
registers one type, plain or symmetric, and never leaves a half-linked
pair. The decision record gains a v2 amendment with the rule.

* Refuse unusable property lists in the import preview

A dry run counted a one-value enumeration, duplicate enum values, and a
one-item property chain as importable, and the draft then failed to apply.
The repository and the preview now read one rule set, so the preview names
the CSV row and column, or the RDF property, before Import is offered.

* Regenerate the API reference for the import endpoints

* Serve the paged native data graph topology

Add POST /v1/ontologies/{id}/dataGraph/pages. One request returns a
selected cluster page or binding page together with bounded concept and
lineage edges over the loaded concept prefixes, and a cursor to continue
the topology.

The loaded context holds at most 60 concepts. A cluster page asks only
for the concepts the context has room for and omits clusters already
loaded, so the context is never over-filled and a client that reaches the
limit sees contextFull instead of a 400. A forged high-water mark is not
checked against the binding rows, so the endpoint cannot be used to learn
whether a hidden asset is bound. A missing or hidden loaded concept and a
cursor axiom that no longer qualifies return 412.

The prefix lineage query materializes the loaded assets and probes
entity_relationship by source asset. A JOIN_ORDER hint keeps MySQL from
driving the join from a range scan of the relationship table.

Amends the native concept bindings decision record.

* Split the native graph pager into focused parts

Move topology cursor encoding into NativeOntologyTopologyCursorCodec
and the lineage stream into NativeOntologyPrefixLineage. Split the
pager's long methods into named steps and mark unchanged parameters and
locals final.

OntologyResource now builds the pager, its binding service and its
codec. The graph service no longer builds the pager or exposes page(),
and it receives the entity reference lookup through its constructor
instead of calling Entity.getEntityReferencesByIds.

Behavior is unchanged. The pager and service unit tests keep their
assertions; the cursor round trip test moves next to the codec.

* Test the topology cursor contract

Add tests for the topology cursor that fail until the codec seals it:

- an issued cursor's Base64 decoding is not JSON and shows no axiom,
  asset or relation type, including when the last scanned lineage pair
  is one the reader cannot view;
- a tampered byte, a token sealed with another key, the unsigned
  format and a cursor for another context are refused with the same
  400 as random bytes, before the pager reads anything;
- a codec with no key issues no cursor and accepts none;
- a cursor offset past a changed visible axiom returns 412, and the
  same cursor skips an axiom the reader cannot view.

Pin the next offset of a clamped cluster page: 55 loaded concepts and
offset 48 load at most 5 more, so the next offset is 53.

The codec now receives the server's Fernet keys from OntologyResource.
The next commit uses them.

* Make topology cursors opaque

Seal topology.after with Fernet, using the key that every server
shares. OntologyResource passes the configured keys to
NativeOntologyTopologyCursorCodec. The first key seals and any key
opens, so a key rotation keeps open cursors. A cursor shows no concept,
axiom, asset or relation type. Cursors do not expire.

The pager opens a topology cursor before it reads anything. A token
that fails authentication, is malformed, uses the unsigned format or
was issued for another context gets the same 400.

A server without a Fernet key issues no cursor. A page that needs one
fails with an error that names fernetConfiguration.fernetKey, and every
token is refused.

A cursor offset past a visible axiom's targets now returns 412: only a
server-issued cursor reaches that check, so the axiom changed after the
cursor was issued. The check no longer runs for an axiom the reader
cannot view.

Fernet.getCachedKeys loses @VisibleForTesting because the resource now
reads it. The decision record's cursor sentences describe the sealed
cursor.

* Assert the PostgreSQL lineage probe by its index condition

The PostgreSQL plan check now looks for an index condition on fromId
that takes each loaded asset, instead of a plan node name. It accepts
an index, bitmap or index-only scan, and it fails when PostgreSQL reads
entity_relationship without that condition. PostgreSQL drops the
trivial loaded subquery scan, so the condition names the binding alias
source.

Against the earlier EXISTS form of the query, the MySQL check fails
because MySQL drives the plan from entity_relationship by a primary-key
range. PostgreSQL 14 plans that form as a semi join that probes by
fromId, so its check passes there.

Group the relationship values that relate() writes into a record, so
the helper takes two parameters.

* Split the paged data graph ITs and cover the sealed cursor

NativeOntologyDataGraphPageIT keeps the topology tests.
NativeOntologyDataGraphContextIT takes the tests of the loaded context
and cluster selection. Both classes are under 500 lines, and their long
test methods are split into named setup and check helpers. Shared
helpers move to NativeOntologyPageFixtures, including edge and
sortedTables, which now take an imported Table.

New cases:
- A reader who cannot view an axiom gets the same 400 for random bytes,
  for an unsigned cursor that names that axiom or a random id, and for
  a token sealed with another key.
- An issued cursor whose last scanned lineage pair the reader cannot
  view carries neither table id nor the relation type, and its Base64
  decoding is not JSON. It continues the page, and a tampered copy gets
  the same 400 as random bytes.
- A cluster page clamped to the five ranks left in the context, holding
  a concept whose only asset the reader cannot view, advertises the
  offset after those five ranks.

OntologyConceptBindingsIT splits the 150-asset paging test into setup
and check helpers.

* Use a core Card for the concept asset list

The asset list took its background, border and radius from classes on
a Box, which is layout only. A Card now supplies the surface, and the
list keeps only the separators between its rows.

* Name the paged graph tests in the binding record's guard

The decision record's Guard line now lists
NativeOntologyDataGraphPageIT, NativeOntologyDataGraphContextIT,
NativeOntologyDataGraphPagerRequestTest,
NativeOntologyTopologyCursorCodecTest and OntologyPrefixLineageDaoTest.
They fail when the paged graph contract that the record states is
broken.

* Move the deleted lineage endpoint check into a helper

The prefix lineage test now reads as setup, the admin page, the reader
page and the deleted endpoint, each in one step.

* Drop comments that restate the paged graph code

Remove nine doc comments that repeat a method name, a method body or
a signature in the paged data graph tests, the page fixtures and
NativeOntologyDataGraphService.ReferenceLookup.

The loadInPagesOfEleven comment also gave a reason that does not
hold. Pages of twelve are not clamped before 60 concepts either.
Eleven is used because five pages load exactly 55 concepts.

Comments that give a reason, an ordering the code depends on or
database planner output stay.

* Derive the topology cursor key from each server key

The topology cursor codec now seals and opens cursors with keys derived
from the server's Fernet keys: HMAC-SHA256 over each key's 32 bytes and
the label native-ontology-topology-cursor/v1, in the same order. A value
that a server key seals for another purpose no longer opens as a cursor,
and a rotated key list still opens cursors sealed under an older key.

OntologyResource builds the codec through forServerKeys. The codec's
constructor is package-private, so code outside the package cannot build
a codec from the server keys themselves.

* Seal topology cursors with a process key when none is configured

Without a configured Fernet key, the topology cursor codec seals cursors
with one key that the server generates at start and holds for the life
of the process. A page whose topology needs a continuation now returns
its first page and a cursor that the next request accepts, instead of
failing. A cursor sent to another server or after a restart gets the
same 400 as any other invalid cursor.

* Record the derived cursor key and the process key in the binding record

The combined graph amendment now says that each cursor key is derived
from one Fernet key, that a server without a configured key seals
cursors with a key it generates at start, and that a 412 can follow a
change to a hidden axiom at the sealed scan position. The Guard line
names the new unit tests.

* Name the keyless refusal test for what it checks

The test seals a cursor with a random key and checks that a codec
without a server key refuses it with the same message as any other
invalid cursor. It builds no second process key, so the test name and
the decision record's Guard line now say what it checks.

* Keep release and member ID binds within one parameter budget in definition status queries

The status resolver and the unpublished-change counter chunked only the member IDs and bound every release ID into each chunk. A large page could pass the database bind-parameter limit and fail the list request. Both lists now share one budget and every release and member pair is still queried once.

* Test that restoring an ontology keeps inverse links between its properties

* Keep inverse links between properties that a restore brings back together

* Test that relation choices follow the rights their writes need

A user who can create axioms but not properties is offered only the
relations an axiom completes, and a reused pair is offered from the side
whose inverse the user can edit. Submitting a choice the user cannot
complete shows the permission message and writes nothing.

* Offer only the relations whose writes the user may make

The native relation picker offered every registry type to any user who
could create axioms. Picking a type can also declare the properties the
ontology lacks, which needs create rights on properties, or point a
reused property at its partner, which needs edit rights on that
property. A user without those rights picked the type, and the server
refused the first write.

The picker now offers a type only when the user holds every right its
planned writes need, and it reads edit rights per property. Submitting
a type the user cannot complete shows the permission message before any
write. Hierarchy relations and pairs that the ontology already holds
linked need only the axiom right, so they stay available.

Planning every type on each model refresh made the planner quadratic in
the number of properties, because each plan built a set of all property
names and scanned the properties by IRI. The planner now indexes the
properties by IRI once per model and builds the name set only when it
declares a property. With 100 registry types and 10,000 properties,
planning the whole catalog takes about 41 ms instead of about 1.9 s, and
the plans match the previous planner's.

The decision record gains a v3 amendment that states the rule.

* Test that restoring an ontology drops a link its partner no longer names back

A property deleted on its own keeps its inverse, while the server clears
the partner's side. The new case gives the partner another inverse,
deletes and restores the ontology, and expects the partner's current
pair back with the older claim cleared. It runs every order of the three
properties' ids, because a write applies its changes in id order.

* Drop a restored inverse that its returning partner no longer names

Restoring an ontology also brings back the properties that were deleted
on their own. Such a property keeps the inverse it held, but the server
cleared its partner's side when it was deleted, and the partner may have
taken another inverse since. The restore kept that older claim whenever
the partner came back in the same write, so the pair that survived
depended on the order of the properties' ids.

A link to a partner that the same restore brings back now survives only
when the partner's stored inverse names the property. Pairs that were
linked on both sides come back as they were, and the older claim is
cleared, whatever the id order. A property whose partner was left
without an inverse comes back unlinked. A single restore still relinks a
live partner and drops a partner that stays deleted.

* Test that restoring an ontology drops claims that compete for a live partner

A property can be restored on its own inside a deleted ontology. When
two properties that were deleted earlier still name it as their inverse,
restoring the ontology brings both claims back at once, and both pass
the check that the partner is live. The claim that reconciles last
wins, so the pair that survives depends on the order of the properties'
ids.

The new test runs every order of three properties' ids on both engines
and expects all three properties to come back unlinked. Two more tests
cover the claims an ontology restore keeps. A lone claim on a live
partner relinks it. Two claims on a vocabulary property both survive,
because the server never writes the side of a vocabulary property.

* Drop restored claims that compete for one live partner

Restoring an ontology brings back every property deleted in it,
including properties that were deleted on their own earlier, and each
keeps the inverse it held. A partner they name can already be live,
because a property can be restored on its own inside a deleted
ontology. When two of the restored properties named the same live
partner, both claims passed the check that the partner is live, and
the claim that reconciled last won. The pair that survived depended on
the order of the properties' ids.

A restore states its links all at once, so none of these claims is the
latest. When two or more properties that one restore brings back name
the same live partner, the restore now clears every one of those
claims and leaves the partner as it was. A lone claim on a live
partner still relinks it. A claim on a partner that returns in the same
restore is still kept only when that partner names the property back.
Claims on a vocabulary property that the ontology does not declare
never compete, because no write changes that property, so a round trip
keeps them.

The decision record states the rule beside its restore exception, and
its Guard names the test that covers it.

* Reword the restore rule in the record and split the restore test setup

* Test each ontology review view over stored tasks

List the assigned, pending and created views through listReviews on
MySQL and PostgreSQL, over approval tasks stored the way TaskRepository
stores them: about, the requester and the reviewers live in relationship
rows, not in the task JSON. The test database gains user_entity so the
task's people resolve.

Every view returns no reviews on the current head.

* List ontology reviews with the release each task is about

TaskRepository keeps a task's about out of the stored JSON and clears it
unless the read asks for it. The review list read tasks without about, so
it matched no release and returned no rows in any view.

* Test that the review list never reads the caller's glossary approvals

A steward who reviews glossary approvals and one release review lists
pending reviews, and a requester who created them lists created reviews.
Each gets the one release, and no statement the list runs names a
glossary approval task.

* List only the approval tasks that are about an ontology release

The review list read every open approval task the caller was on,
glossary and data product approvals included, and hydrated each one.
The task filter now also requires a task about an ontology release, and
the list reads the releases of the listed tasks in one query instead of
one query per task.

* Name the tasks in the glossary approval list test

* Test that an oversized replacement file clears the parsed preview

* Test that import preview and reconcile refuse what apply refuses

* Check imported properties with the rules apply uses

The import preview, summary and reconcile now run every rule that
OntologyPropertyRepository.validateDefinition applies to a property on
its own, through one shared OntologyPropertyRules validator. A CSV
violation names its row and column; an RDF violation names the
property. The CSV reader's own copy of the kind rules is removed.

* Assert which rule refuses apply in the list rules IT

* Document the CSV list rules and one-sided inverses

* Keep the review list test from registering a glossary repository

Surefire runs every service test class in one JVM, and a repository
the test registers stays bound to its database after the class ends.
The fixed list never reads a glossary term, so the test now names the
term's type on its reference instead of registering the repository.

* Test that the review list drops a superseded review task

A release can be in review under a new task while the task it was in
review under before is still open. The list must show the release once,
under the current task. No test covered the check in reviewedRelease
that drops the older task, so every test passed without it.

* Test that the review list reads its releases in one statement

The list loads the releases of all listed tasks in one batched read.
The list test now asserts that exactly one statement reads release rows
and that it covers both releases, so a read per task fails it. Resolving
each task's about reads only the names of its release, so the assertion
does not count that lookup.

* Order the AI-mode import and space the graph helpers

* Test the AI-mode import panel's size limit and layout

The AI case now fails if the classic layout renders in AI mode. A new case checks that AI mode rejects an oversized file before detection.

* Test that a retried relation type finishes the pair a failed write left

The type dialog registers the inverse, then the primary, then patches the
inverse to name the primary. These cases fail one of the later writes once
and retry the same draft through the dialog. A retry after a failed primary
write must reuse the inverse already registered, and a retry after a failed
back link must patch the inverse to name the primary. Either way the
registry ends with one reciprocal pair and no suffixed duplicate.

* Finish a relation type pair on retry after a registry write fails

The type dialog registers the inverse, then the primary, then patches the
inverse to name the primary. When the primary write failed, a retry
registered a second inverse with a numeric suffix and left the first one
unlinked. When the back link failed, a retry found the primary and returned
it, so the pair stayed one-way.

The view now remembers the inverse it registered until the back link is
written. On a retry, the planner names the draft's inverse after that type
and reuses it, or, when the primary is already registered and names it, the
view patches the back link. No other type is patched, and the error from
the failed write is still shown.

A rollback would need the Delete right on relationship types, which the
dialog does not check, and the registry's soft delete keeps the name taken,
so the retry's create would conflict.

* Test that a retry reuses no other inverse and draws the relation once

A new case puts another author's unlinked "approves" in the registry and
creates "approved by" with the inverse "approves". The view registers
"approves2" and links only that type back, so it never claims a type it
did not register.

The case where the back link failed now also checks that the retry
writes the relation once and that only the first failure is reported.

* Test that a relabelled retry finishes the earlier pair first

After the back link of "approved by" fails, the author retries with the
label "signed off by" and the same inverse "approves". The new case
expects the retry to link "approves" back to "approvedBy" and to give
"signedOffBy" its own inverse, "approves2", so no type is the inverse of
two primaries. It fails until the view finishes the earlier pair before
it plans the retry.

* Finish the earlier pair before planning a relabelled retry

When the back link fails, the primary names the inverse but the inverse
names nothing. A retry under a new label reused that inverse for the new
primary, so the inverse ended up named by two primaries.

Before it plans a retry, the view now links the inverse it registered
back to the type that already names it, whatever the retry is called,
and then plans the draft with no reuse, so the new primary gets its own
inverse. The inverse is reused only while nothing names it, after a
failed primary write. Only the inverse this view registered is patched.

* Test that a failed repair keeps the inverse and a reverse retry links it

Two cases in OntologyGraphView.test.tsx pin behavior the view already has.
A retry whose back-link repair fails keeps the remembered inverse, so a
later retry links it back. A retry drawn from the other end, under the
inverse's own label, still links the earlier pair back.

* Check the property writes when a retry is drawn as the inverse

After the back link of "approved by" fails, the author draws the
relation from the other end and types "approves". The case already
checks that the retry links the earlier pair back. It now also checks
the relation the retry writes: the property "approvedBy" first, then
"approves" naming it as its inverse, and one axiom on "approves". That
is what a first draw of "approves" writes. It fails until the view
looks the label up in the registry as the repair left it, because the
retry still writes "approves" with no inverse.

* Look up the retry label in the repaired registry

When the back link of "approved by" fails and the author retries by
drawing "approves", the view links the pair back in the registry. It
then looked the label up in the list it loaded before that repair,
where "approves" names no inverse. So the retry wrote the property
"approves" with no inverse and never declared "approvedBy". A first
draw of "approves" writes both properties, linked.

finishUnlinkedPair now returns the registry with the patched inverse in
place of the stale entry, and the view looks the label up and plans the
draft in that list. The retry now writes "approvedBy", then "approves"
naming it, then one axiom, as a first draw does. The view still patches
only the inverse it registered, keeps it when the repair fails, and
reports the failed write once.

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
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 UI UI specific issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Store ontology concept bindings as rows and serve a paged native data graph

1 participant