Repository: neo4j/cypher-builder
Pull request #508: 503 set with mutation operator
Author: login: angrykoala
Created: 2025-03-04T14:22:11Z | Merged: 2025-03-05T10:20:12Z

Add support for `+=` operator on `SET`

```js
const movie = new Cypher.Node();
const clause = new Cypher.Match(new Cypher.Pattern(movie)).set([
    movie,
    "+=",
    new Cypher.Map({
        title: new Cypher.Param("The Matrix"),
        year: new Cypher.Param(1999),
    }),
]);
```

```cypher
MATCH (this0)
SET
    this0 += { title: $param0, year: $param1 }
```


Diff:
diff --git a/.changeset/kind-bushes-prove.md b/.changeset/kind-bushes-prove.md
new file mode 100644
index 00000000..f52c7fba
--- /dev/null
+++ b/.changeset/kind-bushes-prove.md
@@ -0,0 +1,23 @@
+---
+"@neo4j/cypher-builder": minor
+---
+
+Add support for `+=` operator on `SET`
+
+```js
+const movie = new Cypher.Node();
+const clause = new Cypher.Match(new Cypher.Pattern(movie)).set([
+    movie,
+    "+=",
+    new Cypher.Map({
+        title: new Cypher.Param("The Matrix"),
+        year: new Cypher.Param(1999),
+    }),
+]);
+```
+
+```cypher
+MATCH (this0)
+SET
+    this0 += { title: $param0, year: $param1 }
+```
diff --git a/docs/modules/ROOT/pages/clauses/set.adoc b/docs/modules/ROOT/pages/clauses/set.adoc
index fc5f907a..c23ef6cc 100644
--- a/docs/modules/ROOT/pages/clauses/set.adoc
+++ b/docs/modules/ROOT/pages/clauses/set.adoc
@@ -97,6 +97,34 @@ SET
     this1 = this0
 ----
 
+==== Using operator `+=`
+
+When passing a map to update a node properties with `SET`, it is possible to use the operator `+=` to add new properties, instead of replacing them:
+
+[source, javascript]
+----
+const personNode = new Cypher.Node();
+
+const matchQuery = new Cypher.Match(new Cypher.Pattern(personNode, { labels: ["Person"] })).set([
+    personNode, "+=", new Cypher.Map({
+        name: new Cypher.Param("Keanu"),
+        year: new Cypher.Param(1999)
+    })
+]);
+----
+
+This generates the cypher:
+
+
+[source, cypher]
+----
+CREATE (this0:Movie)
+SET
+    this0 += { title: $param0, year: $param1 }
+----
+
+
+
 == Set with labels
 
 Set can be used to update the labels of a node or the type of a relationship. More information on xref:../../how-to/update-labels.adoc#_add_labels[How to update labels].
diff --git a/src/clauses/sub-clauses/Set.test.ts b/src/clauses/sub-clauses/Set.test.ts
index b550d709..bd7ec288 100644
--- a/src/clauses/sub-clauses/Set.test.ts
+++ b/src/clauses/sub-clauses/Set.test.ts
@@ -151,6 +151,48 @@ SET
 MATCH (this1:Actor)
 SET
     this0 = this1"
+`);
+        expect(queryResult.params).toMatchInlineSnapshot(`{}`);
+    });
+
+    test("Operator += with a map", () => {
+        const movie = new Cypher.Node();
+        const clause = new Cypher.Match(new Cypher.Pattern(movie)).set([
+            movie,
+            "+=",
+            new Cypher.Map({
+                title: new Cypher.Param("The Matrix"),
+                year: new Cypher.Param(1999),
+            }),
+        ]);
+
+        const queryResult = clause.build();
+        expect(queryResult.cypher).toMatchInlineSnapshot(`
+"MATCH (this0)
+SET
+    this0 += { title: $param0, year: $param1 }"
+`);
+        expect(queryResult.params).toMatchInlineSnapshot(`
+{
+  "param0": "The Matrix",
+  "param1": 1999,
+}
+`);
+    });
+
+    test("Operator += with a variable", () => {
+        const movie = new Cypher.Node();
+        const actor = new Cypher.Node();
+        const clause = new Cypher.Match(new Cypher.Pattern(movie, { labels: ["Movie"] }))
+            .match(new Cypher.Pattern(actor, { labels: ["Actor"] }))
+            .set([movie, "+=", actor]);
+
+        const queryResult = clause.build();
+        expect(queryResult.cypher).toMatchInlineSnapshot(`
+"MATCH (this0:Movie)
+MATCH (this1:Actor)
+SET
+    this0 += this1"
 `);
         expect(queryResult.params).toMatchInlineSnapshot(`{}`);
     });
diff --git a/src/clauses/sub-clauses/Set.ts b/src/clauses/sub-clauses/Set.ts
index c00aad2b..75a7dec5 100644
--- a/src/clauses/sub-clauses/Set.ts
+++ b/src/clauses/sub-clauses/Set.ts
@@ -27,7 +27,11 @@ import type { PropertyRef } from "../../references/PropertyRef";
 import type { Expr } from "../../types";
 import { padBlock } from "../../utils/pad-block";
 
-export type SetParam = [PropertyRef, Exclude<Expr, MapExpr | MapProjection>] | [Variable, MapExpr | Variable] | Label;
+export type SetParam =
+    | [PropertyRef, Exclude<Expr, MapExpr | MapProjection>]
+    | [Variable, MapExpr | Variable]
+    | [Variable, "+=", MapExpr | Variable]
+    | Label;
 
 export class SetClause extends CypherASTNode {
     protected params: SetParam[];
@@ -56,6 +60,9 @@ export class SetClause extends CypherASTNode {
     private composeParam(env: CypherEnvironment, setParam: SetParam): string {
         if (setParam instanceof Label) {
             return setParam.getCypher(env);
+        } else if (setParam.length === 3) {
+            const [ref, operator, param] = setParam;
+            return `${ref.getCypher(env)} ${operator} ${param.getCypher(env)}`;
         } else {
             const [ref, param] = setParam;
             return `${ref.getCypher(env)} = ${param.getCypher(env)}`;


Reviews:
user: login: MacondoExpress
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T17:59:35Z
user: login: MacondoExpress
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T18:00:41Z
user: login: angrykoala
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-05T10:17:08Z

Review comments:
diff_hunk: @@ -0,0 +1,23 @@
+---
+"@neo4j/cypher-builder": patch
path: .changeset/kind-bushes-prove.md
user: login: MacondoExpress
body: Should it be `minor` ?
created_at: 2025-03-04T18:00:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,23 @@
+---
+"@neo4j/cypher-builder": patch
path: .changeset/kind-bushes-prove.md
user: login: angrykoala
body: yeah, I agree

```suggestion
"@neo4j/cypher-builder": minor
```
created_at: 2025-03-05T10:17:08Z
author_association: MEMBER

PR comments:
user: login: changeset-bot[bot]
created_at: 2025-03-04T14:22:14Z
body: ###  🦋  Changeset detected

Latest commit: d8a571d7f3f855435ef8385a3047f0472505974a

**The changes in this PR will be included in the next version bump.**

<details><summary>This PR includes changesets to release 1 package</summary>

  | Name                  | Type  |
| --------------------- | ----- |
| @neo4j/cypher-builder | Minor |

</details>

Not sure what this means? [Click here  to learn what changesets are](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add another changeset to this PR](https://github.com/neo4j/cypher-builder/new/503-set-with-mutation-operator?filename=.changeset/many-dancers-draw.md&value=---%0A%22%40neo4j%2Fcypher-builder%22%3A%20patch%0A---%0A%0A503%20set%20with%20mutation%20operator%0A)


author_association: NONE
user: login: sonarqubecloud[bot]
created_at: 2025-03-05T10:20:12Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=neo4j_cypher-builder&pullRequest=508) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=neo4j_cypher-builder&pullRequest=508&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=neo4j_cypher-builder&pullRequest=508&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=neo4j_cypher-builder&pullRequest=508&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [100.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=neo4j_cypher-builder&pullRequest=508&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=neo4j_cypher-builder&pullRequest=508&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=neo4j_cypher-builder&pullRequest=508)


author_association: NONE