Skip to content

Writing data

NamiDB Cypher supports the standard write clauses from openCypher 9 / GQL: CREATE, MERGE, SET, REMOVE, DELETE, DETACH DELETE. Every write statement is durably committed before the call returns (WAL append + manifest CAS). There are no explicit BEGIN / COMMIT in v0; the unit of atomicity is the statement.

See RFC-009 for the execution model.

CREATE

Creates nodes and relationships. Properties can be literals or $param.

CREATE (a:Person {name: 'Alice', age: 30})
CREATE (a:Person {name: 'Alice'})-[:KNOWS {since: 2020}]->(b:Person {name: 'Bob'})

CREATE after a MATCH chains both bindings into the new pattern:

MATCH (a:Person {name: 'Alice'}), (b:Person {name: 'Bob'})
CREATE (a)-[:KNOWS {since: 2020}]->(b)

A write-only query (no MATCH before) starts from an implicit single driver row — the same pattern used by UNWIND.

MERGE

Upsert. If the pattern matches, the existing nodes / relationships are returned; if it doesn’t, the pattern is created.

MERGE (p:Person {name: 'Alice'})
ON CREATE SET p.firstSeen = $now, p.lastSeen = $now
ON MATCH SET p.lastSeen = $now

In v0, MERGE accepts a single pattern part (no multi-part patterns). Multi-label MERGE (a:A:B {...}) is rejected at parse time with E007_MergeMultiLabel.

Concurrency: MERGE relies on the single-writer-per-namespace invariant for serialization. With one writer per namespace, two concurrent MERGEs against the same key are linearised by the writer mutex.

SET

Mutate properties or add labels.

Property assignment:

MATCH (p:Person {name: 'Alice'})
SET p.age = 31

Replace the whole property map:

MATCH (p:Person {name: 'Alice'})
SET p = {name: 'Alice', age: 31, country: 'MX'}

Merge a partial map (only the listed keys change):

MATCH (p:Person {name: 'Alice'})
SET p += {age: 31, country: 'MX'}

Add labels:

MATCH (p:Person {name: 'Alice'})
SET p:Employee:Manager

REMOVE

The inverse of SET. Removes properties or labels.

MATCH (p:Person {name: 'Alice'})
REMOVE p.country
MATCH (p:Person {name: 'Alice'})
REMOVE p:Manager

DELETE and DETACH DELETE

DELETE tombstones a node or relationship. A bare DELETE against a node that still has edges fails with an explicit ExecError::Mutation message suggesting DETACH DELETE:

MATCH (p:Person {name: 'Alice'})
DELETE p -- fails if Alice has incident edges

DETACH DELETE enumerates the incident edges across every edge type in the manifest schema, tombstones them, then tombstones the node:

MATCH (p:Person {name: 'Alice'})
DETACH DELETE p -- removes Alice and every edge touching her

Deleting an edge directly works without DETACH:

MATCH (a:Person {name: 'Alice'})-[r:KNOWS]->(b:Person {name: 'Bob'})
DELETE r

UNWIND for bulk writes

UNWIND expands a parameter list into rows, which CREATE then materialises. This is the idiomatic Cypher path for bulk inserts:

UNWIND $people AS p
CREATE (:Person {name: p.name, age: p.age})
client.cypher(
"UNWIND $people AS p CREATE (:Person {name: p.name, age: p.age})",
params={"people": [
{"name": "Alice", "age": 30},
{"name": "Bob", "age": 25},
{"name": "Carol", "age": 42},
]},
)

For high-volume ingest, the Python staging API (client.merge_nodes / client.merge_edges) is faster — it batches mutations behind one tokio-mutex round trip per call. See Python library.

Auto-commit and the write outcome

Each write statement runs through execute_write, which:

  1. Walks the plan top to bottom, calling upsert_node / upsert_edge / tombstone_* against the WriterSession per row.
  2. Calls writer.commit_batch().await automatically before returning.

The return value carries a counter summary:

WriteOutcome {
rows: ...,
nodes_created: u64,
edges_created: u64,
nodes_deleted: u64,
edges_deleted: u64,
properties_set: u64,
}

Counters increment per operation, not per net change of state (so SET p.x = p.x counts as one properties_set). The counters are exposed on the HTTP envelope as write_outcome — see the HTTP API.

Read-your-own-writes (not yet)

Inside a single statement, writes are not visible to reads that come after them in the same plan tree. Example:

CREATE (a:Person {name: 'Ada'})
MATCH (p:Person) RETURN p.name

The MATCH runs against the snapshot pinned before the CREATE. Workaround: split into two statements. Cypher writes auto-commit, so the second statement (or the next Client.cypher call) sees the new node.

RFC-009 documents why and traces the path to a future transactional model.

What’s next