ablative docs
↑↓ move↵ openesc close
Stack Notebook ablative.com.au

Relations and the graph

Declare relations, write and delete edges, and walk two directed hops through patients, wards and nurses.

Open .md

Before you begin

Use the running service and the patients records from Write your first records. p0 is Ada in ward 4B. Every command below uses that service’s configuration. A protected store requires the writer’s rights on the branch and affected resources.

Create the other records

haematite write --config service.json --branch main collection.put '{"collection":"wards","schema":null}'
haematite write --config service.json --branch main collection.put '{"collection":"nurses","schema":null}'
haematite write --config service.json --branch main document.put '{"collection":"wards","document_id":"w0","document":{"code":"4B","name":"Ward 4B","beds":20}}'
haematite write --config service.json --branch main document.put '{"collection":"nurses","document_id":"n0","document":{"name":"Dee"}}'

Declare the relations

haematite write --config service.json --branch main relation.put '{"relation":"admitted_to","from_collection":"patients","to_collection":"wards","cardinality":"many_to_many"}'
haematite write --config service.json --branch main relation.put '{"relation":"staffed_by","from_collection":"nurses","to_collection":"wards","cardinality":"many_to_many"}'

relation.put requires relation, from_collection, to_collection and cardinality. Both collections must exist. A declaration is versioned on the branch. Replacing it checks all existing edges against the proposed collections and cardinality before writing it.

one_to_one permits one partner at each end. one_to_many permits several destinations from a source, but each destination has only one source. many_to_many permits several partners at both ends. Repeating an existing pair is not another partner.

collection_missing names a missing endpoint collection. relation_collection means an edge uses the wrong declared collection. relation_cardinality means an existing edge or another row in the batch conflicts with the declaration.

Write edges

haematite write --config service.json --branch main edge.put '{"relation":"admitted_to","from":{"collection":"patients","document_id":"p0"},"to":{"collection":"wards","document_id":"w0"},"properties":{"current":true}}'
haematite write --config service.json --branch main edges.put_many '{"edges":[{"relation":"staffed_by","from":{"collection":"nurses","document_id":"n0"},"to":{"collection":"wards","document_id":"w0"},"properties":{}}]}'

edge.put requires relation, from, to and properties. Each endpoint has collection and document_id. Both records must exist at the selected branch version. Properties must be a JSON object. document_missing identifies an absent endpoint; edge_properties rejects another property type.

The edge identity is its relation and both endpoints. Putting that identity again replaces the properties. Outgoing and incoming indexes are changed together. Deleting either record removes its incident edges.

edges.put_many takes a nonempty edges array of the same shape. It requires a declaration for every relation, rejects repeated identities as import_duplicate and validates cardinality against both stored edges and earlier batch rows. A row refusal includes a path such as /edges/1/to. The whole mutation is refused before applying any row. The batch is bounded by the service import limit and frame size.

The current write client does not include relation_missing, relation_cardinality or relation_collection in its known-refusal list. In the scratch run, those named refusals therefore produced exit 3 and an outcome-unknown diagnostic, rather than exit 2. Keep the request id and reconcile its receipt as instructed. A named server refusal and the client’s exit classification are separate evidence.

For compatibility, a single edge.put can still write an undeclared relation and applies declaration constraints only when one exists. The batch command refuses an undeclared relation as relation_missing. Declare the relation before using either command so both use the same contract.

Walk the graph

haematite query --config service.json --branch main 'match (c:patients)-[:admitted_to]->(w:wards)<-[:staffed_by]-(n:nurses)'

The first hop follows admitted_to from a patient to a ward. The second follows staffed_by backwards from that ward to a nurse. The result’s value is the three bound records.

{"c":{"name":"Ada","ward":"4B","admitted":20260912},"w":{"code":"4B","name":"Ward 4B","beds":20},"n":{"name":"Dee"}}

This exact query returned Ada, Ward 4B and Dee in the scratch store. The full row also has its origin and version. It is two edge hops across three records.

A match can start with one labelled node or a fixed chain of at most eight nodes and seven directed hops. Bindings are distinct lowercase names using letters, digits and underscores, beginning with a lowercase letter or underscore. Each node names a collection. Use -[:relation]-> for an outgoing hop and <-[:relation]- for an incoming hop. Variable-length paths and an undirected relation are not this grammar.

The match runs at the query’s captured selection. Later pages retain that version. Ordinary filters, sorting and bounds can follow a whole-record match. An invalid pattern or binding is refused by name, with its query span.

Read edges directly

haematite call --config service.json edge.out '{"selection":{"branch":"main"},"node":{"collection":"patients","document_id":"p0"},"limit":20}'

edge.out reads outgoing edges for a node; edge.in reads incoming ones. Supply relation to select one kind. Both answer a captured selection, edges and next_after. Continue with that captured version and the returned cursor. graph.export pages all edges at a selection; it does not export record bodies or constitute a backup.

Studio’s Graph area uses these versioned records and bounded graph reads. A screen’s drawing limit does not delete or hide an edge in storage. Read its completion and refusal state before treating the drawing as the whole graph.

haematite write --config service.json --branch main edge.delete '{"relation":"admitted_to","from":{"collection":"patients","document_id":"p0"},"to":{"collection":"wards","document_id":"w0"}}'

edge.delete identifies the same relation and endpoints and takes no properties. It removes both edge indexes. Removing an absent identity is permitted. relation.delete removes the declaration only; it does not remove existing edges under that name.

The MCP tools link_records and unlink_records provide the single-edge operations with required request_id, branch and expected guards. Relation declarations and batch imports go through the service change kinds. Use the same expected version and request identity rules as every other mutation.