Chapter 06 · Graph Writes: CREATE, MERGE, SET, REMOVE, DELETE, and Idempotent Mutation
SET and REMOVE Properties/Labels, Map Replacement vs Mutation, and Schema Evolution
Evolve graph state without accidental data loss: distinguish scalar updates, full replacement, patches, removals, labels, and migration contracts before changing shared entities.
Learning outcomes
AtlasMart's catalog payload now contains only fields changed by a source system. A full property replacement would erase fields maintained by other teams, while a patch can intentionally add, replace, or remove selected values. This lesson makes those semantics explicit before schema evolution begins.
Distinguish scalar SET, full-map replacement
with =, and patch mutation with
+=.
Explain how null in a patch removes a property
and how REMOVE expresses explicit removal.
Add and remove labels without confusing classification with integrity enforcement.
Protect stable business keys from accidental map replacement and verify schema drift explicitly.
Design additive/backfill/deprecate/remove migrations that can be rolled forward or reconciled safely.
The mandatory lab continues the accepted course baseline:
Neo4j Community 2026.07.1, database
neo4j, explicit CYPHER 25 on
version-sensitive examples, authentication enabled, local Bolt
at bolt://localhost:7687, no mandatory APOC/GDS
plugin, and the AtlasMart Customer/Product/Order model from
Chapters 01–05. Neo4j 5.26.30 remains the current
LTS comparison line. Optional concurrency examples use Neo4j
Python Driver 6.3.x; the mandatory mutation logic
itself is plain Community Cypher.
This chapter changes graph state. Every destructive example
targets dedicated IDs prefixed P-WRITE-,
O-WRITE-, or O-DELETE-, and every
cleanup query repeats those predicates. Never broaden them to
MATCH (n) DETACH DELETE n in a database
containing unrelated work. This generation environment does
not run Neo4j or Docker, so expected results are stated as
deterministic invariants, not fabricated captured output.
Re-establish the isolated Chapter 06 write fixture
The fixture deliberately reuses AtlasMart's stable business
identifiers and Community-supported uniqueness constraints. It
creates or normalizes one customer, one dedicated product, one
dedicated order, and their PLACED/CONTAINS
relationships. Because every identity is deterministic and the
write pattern uses MERGE, rerunning the fixture
should leave the target counts unchanged.
CYPHER 25CREATE CONSTRAINT customer_id IF NOT EXISTS FOR (c:Customer) REQUIRE c.customerId IS UNIQUE;CREATE CONSTRAINT product_id IF NOT EXISTS FOR (p:Product) REQUIRE p.productId IS UNIQUE;CREATE CONSTRAINT order_id IF NOT EXISTS FOR (o:Order) REQUIRE o.orderId IS UNIQUE;MERGE (c:Customer {customerId:'C-1001'}) ON CREATE SET c.name='Ava Chen', c.region='eu', c.createdAt=datetime('2026-09-09T00:00:00Z') ON MATCH SET c.lastSeenAt=datetime('2026-09-09T00:00:00Z');MERGE (p:Product {productId:'P-WRITE-1001'}) ON CREATE SET p.name='Atlas Travel Camera', p.price=149.90, p.status='active', p.createdAt=datetime('2026-09-09T00:00:00Z') ON MATCH SET p.name='Atlas Travel Camera', p.price=149.90, p.status='active';MERGE (o:Order {orderId:'O-WRITE-2001'}) ON CREATE SET o.status='pending', o.source='chapter06', o.createdAt=datetime('2026-09-09T00:00:00Z') ON MATCH SET o.source='chapter06';MERGE (c)-[:PLACED]->(o)MERGE (o)-[line:CONTAINS]->(p) ON CREATE SET line.quantity=1, line.unitPrice=149.90 ON MATCH SET line.quantity=1, line.unitPrice=149.90;
CYPHER 25MATCH (p:Product {productId:'P-WRITE-1001'})OPTIONAL MATCH (c:Customer {customerId:'C-1001'})-[placed:PLACED]->(o:Order {orderId:'O-WRITE-2001'})OPTIONAL MATCH (o)-[line:CONTAINS]->(p)RETURN count(DISTINCT p) AS products, count(DISTINCT o) AS orders, count(DISTINCT placed) AS placedRelationships, count(DISTINCT line) AS containsRelationships;
The deterministic target for the dedicated slice is
1 / 1 / 1 / 1. Those counts do not imply the entire
database contains only one Product or Order; previous chapters
intentionally contain additional AtlasMart data.
1. SET one property when one property changes
CYPHER 25MATCH (p:Product {productId:'P-WRITE-1001'})SET p.price=154.90, p.updatedAt=datetime()RETURN p.productId, p.price, p.updatedAt;
Neo4j takes a write lock on the entity being mutated. The operation is transactional: either the containing transaction commits the new properties or the previous committed state remains.
2. SET entity = map replaces the complete property set
CYPHER 25MATCH (p:Product {productId:'P-WRITE-1001'})SET p = {name:'Atlas Travel Camera', price:159.90}RETURN p.productId, p.name, p.price, p.status;
This removes every property not present in the map—including
productId, status, and timestamps. A
Community uniqueness constraint on productId does
not require the property to exist, so the replacement can erase
the business key without violating uniqueness. That is why
constraint knowledge and mutation semantics must be combined.
If you want to observe the effect, first create a dedicated
throwaway Product such as P-WRITE-MAPTEST, run
the replacement there, verify the missing key, then delete
that dedicated node.
3. SET entity += map is patch semantics
CYPHER 25MATCH (p:Product {productId:'P-WRITE-1001'})SET p += { name:'Atlas Travel Camera Mk II', price:159.90, legacySku:null}RETURN p.productId, p.name, p.price, p.status, p.legacySku;
With +=, properties absent from the map remain
unchanged; properties present are added or replaced; a key whose
patch value is null is removed. An empty patch map
has no effect. This is usually closer to PATCH-like application
semantics, but the API contract still must decide which keys the
caller is allowed to mutate.
| Operation | Properties not mentioned | Mentioned non-null key | Mentioned null key |
|---|---|---|---|
SET n = map |
Removed | Added/replaced | Not stored |
SET n += map |
Preserved | Added/replaced | Removed |
SET n.k = value |
Preserved | Specific key changed | Setting null removes that key |
REMOVE n.k |
Preserved | Specific key removed | N/A |
4. REMOVE makes deprecation intent explicit
CYPHER 25MATCH (p:Product {productId:'P-WRITE-1001'})SET p:CatalogV2, p.legacySku='LEGACY-1001'RETURN labels(p), p.legacySku;MATCH (p:Product {productId:'P-WRITE-1001'})REMOVE p.legacySkuREMOVE p:CatalogV2RETURN labels(p), p.legacySku;
Labels classify nodes; adding :CatalogV2 does not
validate all required fields. Conversely, removing a label can
change which indexes/constraints/query patterns apply. Treat
label changes as schema evolution with query and index impact,
not cosmetic tags.
5. Schema evolution should be staged, not surprise replacement
For a new catalog field such as currency, a safer
rollout is usually: make readers tolerate absence; start
dual-writing the new property; backfill bounded batches; verify
missing/invalid counts; switch readers; stop writing the old
property; then remove the deprecated field. In Community
Edition, some integrity checks remain application/validation
queries rather than Enterprise-only existence/type constraints.
CYPHER 25MATCH (p:Product)RETURN count(p) AS products, count(p.currency) AS withCurrency, count(CASE WHEN p.currency IS NULL THEN 1 END) AS missingCurrency;
Do not set a migration “complete” flag merely because the write script exited successfully. Verify committed graph state and sample the consumer queries that depend on the new representation.
6. Deliberately wrong: accept an unrestricted map from the API
CYPHER 25MATCH (p:Product {productId:$productId})SET p += $bodyRETURN p;
If $body can contain productId,
lifecycle timestamps, tenant ownership, or security-sensitive
flags, a caller can modify fields outside the endpoint contract.
The repair is to construct an allowlisted patch map in
application code or map only explicitly permitted fields in
Cypher.
CYPHER 25MATCH (p:Product {productId:$productId})SET p.name=$name, p.price=$price, p.updatedAt=datetime()RETURN p.productId, p.name, p.price;
Lab: prove replacement vs mutation on a throwaway node
CYPHER 25MERGE (p:Product {productId:'P-WRITE-MAPTEST'})SET p.name='Map Test', p.price=10.0, p.status='active', p.owner='catalog';
Record properties(p). Then run
SET p += {price:11.0} and prove
status/owner remain. Next run
SET p = {productId:'P-WRITE-MAPTEST',
name:'Replacement'}
and prove price/status/owner disappear. Finally restore the
fixture or delete only the map-test node.
CYPHER 25MATCH (p:Product {productId:'P-WRITE-MAPTEST'})DETACH DELETE p;
Production judgment
Patch semantics reduce accidental data loss but do not solve authorization, concurrency, or schema-contract problems. Decide whether updates are last-write-wins, compare-and-set/versioned, or serialized by another invariant. Measure write contention on hot entities. For migrations, retain rollback/reconciliation data until both old and new readers are proven. Never use a broad full-map replacement as a shortcut for “sync the object” unless the source is explicitly authoritative for every property on that graph entity.
Check your understanding
- What is the key difference between SET n = map and SET n += map?
- What happens when a += patch contains a key whose value is null?
- Why might deleting productId not violate a Community uniqueness constraint?
- Why are labels part of schema evolution?
- Why is passing an unrestricted API body directly to SET += risky?
Review the answers
1. = replaces the entire property set; += mutates only mentioned keys and preserves others.
2. That property is removed from the entity.
3. Uniqueness constrains duplicate values that exist; it does not itself require every node to have the property.
4. Queries, indexes, constraints, and semantics can depend on labels, so adding/removing them changes more than presentation.
5. It lets the caller modify protected or unintended properties unless the update surface is allowlisted.
Summary and next step
Property mutation is precise only when the source of truth and allowed update surface are explicit. Next, the chapter handles the irreversible side of state change: deleting relationships and nodes without turning a targeted cleanup into a graph-wide cascade.
Authoritative references
- Current Neo4j versions — Release/LTS snapshot used to pin the course baseline.
- Cypher Manual — CREATE — CREATE semantics for nodes, relationships, parameters, and dynamic labels/types.
- Cypher Manual — MERGE — Match-or-create semantics, ON CREATE/ON MATCH, constraints, and concurrent relationship merges.
- Cypher Manual — SET — Property/label updates and map replacement versus mutation.
- Cypher Manual — REMOVE — Property and label removal semantics.
- Cypher Manual — DELETE — DELETE, NODETACH DELETE, DETACH DELETE, and large-delete guidance.
- Operations Manual — transactional behavior — ACID, read-committed isolation, locking, transaction logs, and deadlock behavior.
- Working with null — Null as absence and property-removal semantics.
- Create constraints — Uniqueness versus existence/key/type contracts and edition boundaries.