Skip to content

ADR 0056: Shared identified relationship types

Date: 2026-10-26. Status: Accepted on 2026-10-26, under the owner's decision D3 (entity-level interop after the other should rows), which needs this feature and additional labels (ADR 0055). GR-62.

Context

An identified relationship (ADR 0016) is an EF entity stored as a relationship with an application key. Until now a relationship type could have only one such owner across all endpoint types: - GraphRAG gives each ontology class its own mention type (MENTIONED_IN_PERSON, MENTIONED_IN_PLACE), and the wave 0 spike pinned the refusal with a guard test (ADR 0052, "What the provider doesn't do yet", item 1). - neo4j-graphrag links every entity to its chunk with one type, FROM_CHUNK, whatever the entity's label. GraphBridge couldn't write that shape.

Associations (endpoint-pair relationships) already share a type between different source and target node types.

Decision

1. Owners told apart by their endpoints

Several identified-edge entity types may own one relationship type when: - their endpoints differ: no two owners have the same source node type and target node type. A pair in the other direction is a different pair; - they share the key property: every owner stores its key under the same property name, which the type's one uniqueness constraint covers. Keys are therefore unique across all owners of the type. The key's CLR type may differ between owners, and no property-type constraint is involved. Keys of different CLR types can still be equal in storage, such as an int and a long key, or a Guid key and the same text as a string key; the type's one constraint then refuses the second, so nothing is written twice.

Refused when the model is built: - a second owner with the same endpoint types, because a read couldn't tell their relationships apart; - owners whose key property names differ; - as before, a type shared between an identified relationship and an association, or used by a native reference.

2. What already holds

Every statement of one relationship names both endpoints or checks the key type-wide, so nothing else changes for them: - Reads: MATCH (s:Source)-[r:TYPE]->(t:Target), with each endpoint's labels (and additional labels). Each owner reads only its own relationships. - Duplicate guard on reads: a relationship's key is counted across the whole type, whoever owns it; two relationships with one key are reported as corruption. Since owners share the key property and its constraint, that stays correct. - Keyed updates and deletes: the relationship is found by type and key across the type, then its endpoints must be the owner's. A key that belongs to another owner's relationship fails the endpoint check and is reported, never changed. - Upserts: the preflight already refuses a key that exists on a relationship between other endpoints. - Constraint check: each owner requires the type's uniqueness constraint on the key; one constraint satisfies all of them.

This assumes a node carries one mapped type's label, as GraphBridge writes them. A node another tool gave two mapped labels could make one relationship readable by two owners; validation can't see data, so this stays the writer's responsibility.

2a. Paths over a shared type (added after review, 26 October 2026)

A bounded or undirected Fluent path constrains only its two ends, so its hops could cross another mapping's relationships and node types: with owners from A to A and from A to B, (a:A)-[:T*1..2]-(a2:A) passes through a B node. Such a path is refused with GBFC203 when its relationship type has more than one mapping. This also closes the same gap for associations, which could already share a type.

3. Schema comparison

The manifest lists one relationship entry per owner. Comparison groups them by type: - a shared type is reported as SharedRelationshipType (warning), and type-wide property and index proposals are suppressed, as for shared associations; - the key's uniqueness constraint is checked once per type and key, and proposed when missing, because it is the same for every owner.

Consequences

  • Tooling: the mapping generator still refuses a relationship type shared by identified relationships. Models that share a type are written by hand.

  • GraphRAG can map every class's mentions to one type, the second half of entity-level interop. Choosing that layout is GraphRAG's work (GR-16); its default stays one type per class.

  • Existing models keep their Cypher. One thing they did is refused now (section 2a): a bounded or undirected Fluent path over an association type that several mappings share, which GBFC203 refuses because its hops could cross from one mapping to another.
  • With a shared type, a relationship's key must be unique across all owners. GraphRAG's mention IDs hash the chunk, the offset and the thing, so they are.

Not adopted

  • Owners told apart by a type property (a discriminator on the relationship). It would add a property every other tool must write, and neo4j-graphrag doesn't.
  • Keys unique per owner only. Two constraints on one type and property can't express it, and the type-wide duplicate guard would then misreport valid data.

Verification

  • Offline: two owners with different endpoints build; the same endpoints, or another key name, are refused; schema comparison checks and proposes the key constraint once.
  • Offline: owners in opposite directions build and read their own pair; a bounded or undirected path over a shared type is refused with GBFC203.
  • Live: two owners of one type, written, read, updated, deleted and upserted, each touching only its own relationships, with additional labels on the endpoints. A key the other owner holds is refused three ways: by the upsert's preflight, by the uniqueness constraint when SaveChanges adds it (DbUpdateException), and by the endpoint check when a keyed update comes through the wrong owner (DbUpdateConcurrencyException, the relationship unchanged).