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.

Intermediate120–150 minutesSET/REMOVE schema-evolution labNeo4j 2026.07.1 Community · Cypher 25Last reviewed: September 2026

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.

01

Distinguish scalar SET, full-map replacement with =, and patch mutation with +=.

02

Explain how null in a patch removes a property and how REMOVE expresses explicit removal.

03

Add and remove labels without confusing classification with integrity enforcement.

04

Protect stable business keys from accidental map replacement and verify schema drift explicitly.

05

Design additive/backfill/deprecate/remove migrations that can be rolled forward or reconciled safely.

Chapter 06 baseline · reviewed 9 September 2026

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.

Safety and evidence rule

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 · idempotent Chapter 06 fixture
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 · acceptance counts for only the dedicated slice
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 · explicit scalar mutation
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

Dangerous on a business entity · full replacement
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.

Do not run the full-replacement example against the shared fixture unless you immediately restore it.

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

Preferred for partial catalog updates · 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 · property and label cleanup
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 · verify migration completeness before removal
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

Risky · caller can overwrite protected fields
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.

Safer · explicit allowlist
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 · create dedicated map-semantics target
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 · targeted cleanup
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

  1. What is the key difference between SET n = map and SET n += map?
  2. What happens when a += patch contains a key whose value is null?
  3. Why might deleting productId not violate a Community uniqueness constraint?
  4. Why are labels part of schema evolution?
  5. 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

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only Ethereum or ERC-20 compatible assets to this address.