Repository navigation
Fixes #35984: Store ontology concept bindings as rows and serve a paged native data graph - #35996
Conversation
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.
✅ PR checks passedThe linked issue has a description and all required Shipping project fields set. Thanks! |
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.
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.
…d per-operation revisions
…tatus and release change summary
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.
Main moved to daec19a before PR3 landed. The merge had no conflicts.
Main moved to aeb3cfc before PR4 landed. The merge had no conflicts.
Main moved to 5f76085 before PR10 landed. It changes three workflow task files that this PR does not touch. The merge had no conflicts.
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
✅ Performance: Bindings page read can scan every row of a concept on one GET
✅ Quality: Data graph limits are judgment calls missing from the ADR amendment
Closed: Performance: Data graph loads and authorizes every binding row of each concept
VerificationRules No rules evaluated Functional validation Not enabledAuto-approval Not enabledFor more verification details, comment OptionsDisplay: compact → Showing compact verification results. Comment with these commands to change the behavior for this request:
Was this helpful? React with 👍 / 👎 | Powered by Gitar — free for open source |
|
|
…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>



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 withmain's AI-mode restyle. Every later head comes from merges that keep PR12's patch-id7aaee8552f9, so the verdict carries to this head. After PR4 landed,a3c3e0fb320merged PR10's landing update758d0214ed7. PR10 then landed onmainas the squashb1941043ab4. This head mergesmain's5f760856698(#36560), which changes three workflow task files that PR12 does not touch. It then records PR10's squash with anoursmerge, which leaves the tree unchanged. Neither merge had a conflict. No minor issue is declared.Test evidence. The gates of the merge at
77de9395387ran on its staged trees. Package andspotlesspassed, 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 ofOntologyEntityDAOTest.reviewLocksSeeEditsAndArchivalCommittedWhileWaiting, whose rows share one database on the local engines.OntologyConceptBindingMigrationTestskips 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. Ata3c3e0fb320, where PR4's content meets PR12's inOntologyGraphView.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
32f98026b44were still running when this block was written. This PR now targetsmain. Ata3c3e0fb320, its only failing check wasmaven-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-elasticsearchand then-Ppostgres-opensearch, and confirm aTests run:line.NativeOntologyDataGraphPageIT,NativeOntologyDataGraphIT,NativeOntologyDataGraphContextIT,NativeOntologyDataGraphBindingScanIT,NativeOntologyDataGraphEdgeScanIT,OntologyConceptBindingsIT,OntologyConceptBindingCleanupIT,OntologyResourceITandGlossaryCsvRelationTypesIT, then the PR10 list.TopicResourceITandTopicServiceHydrationIT, then the migration ITsContinuousMigrationIT,OpenMetadataOperationsITandSeedDataPresenceIT, andConversationSchemaMigrationITandMetricMigrationITin the global-state lane.main's changes select.OntologyEntityDAOTest.reviewLocksSeeEditsAndArchivalCommittedWhileWaiting, and the container-backed classes once under Testcontainers onmysql:8.0andpostgres:15:OntologyConceptStatusStorageTest,OntologyDerivedStatusStorageTest,OntologyEntityDAOTest,OntologyReleaseComparisonStorageTest,OntologyReleaseDAOTest,OntologyReleaseReviewListTest,OntologyWorkingStateTest,OntologyHierarchyStorageTestandOntologyPropertyInverseStorageTest, plusOntologyPrefixLineageDaoTestandOntologyConceptBindingMigrationTest.FERNET_KEY=no_encryption_at_rest. A topology continuation returns 200 before the restart and 400Invalid native graph topology cursorafter it.OntologyNativeOperations.spec.tsagainst a live stack. It drives classic mode.AI mode.
main's Ontology Studio AI-mode restyle #36433 now gates new screens on AI mode. Paths are underopenmetadata-ui/src/main/resources/ui/src/components/governance/ontology/OntologyConceptInspector/. AI mode lacks two PR12 guards, which render only in classic branches. InOntologyConceptAssets.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. InOntologyColumnBindingEditor.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-metadatato 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:
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 isaa7ef4419f7; 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 anaftercursor 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}/dataGraphranks 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
Unit tests
At production commit
6e7d3d762d8, a fresh root run passed 289 raw unit cases without failures, errors, or skips. At test-only commitaa7ef4419f7, 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.
GlossaryTermRepositoryis 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
2fa529727d2passed 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 privateom-pr12-network. Receipts areswarm/pr12/owner/api-proof.jsonandprivate-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:
make harness-checkexits 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.ba6d06c88bdand this branch; the differential adds and removes zero diagnostics. No overall TypeScript pass is claimed.

Confidence 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.
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]Reviews (21) · Last reviewed commit: "Merge native relations fixes into bindin..." · Reviewed by Greptile