From 26db86f1200b31a2eeec0d212453a8f07b8c15dd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jens=20Pryce-=C3=85klundh?=
<112686610+JPryce-Aklundh@users.noreply.github.com>
Date: Tue, 15 Apr 2025 09:20:31 +0200
Subject: [PATCH 1/8] New WITH page (#1238)
---
modules/ROOT/images/with_clause.svg | 1 +
modules/ROOT/pages/clauses/with.adoc | 586 ++++++++++++++++++++-------
2 files changed, 436 insertions(+), 151 deletions(-)
create mode 100644 modules/ROOT/images/with_clause.svg
diff --git a/modules/ROOT/images/with_clause.svg b/modules/ROOT/images/with_clause.svg
new file mode 100644
index 000000000..f77938445
--- /dev/null
+++ b/modules/ROOT/images/with_clause.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/modules/ROOT/pages/clauses/with.adoc b/modules/ROOT/pages/clauses/with.adoc
index c2939fa21..36ac8b6c3 100644
--- a/modules/ROOT/pages/clauses/with.adoc
+++ b/modules/ROOT/pages/clauses/with.adoc
@@ -1,244 +1,528 @@
-:description: The `WITH` clause allows query parts to be chained together, piping the results from one to be used as starting points or criteria in the next.
+:description: Information about Cypher's `WITH` clause, which allows query parts to be chained together, piping the results from one part to be used as the starting point of the next.
+:table-caption!:
-[[query-with]]
= WITH
-The `WITH` clause allows query parts to be chained together, piping the results from one to be used as starting points or criteria in the next.
+The `WITH` clause serves multiple purposes in Cypher:
-[NOTE]
-====
-It is important to note that `WITH` affects variables in scope.
-Any variables not included in the `WITH` clause are not carried over to the rest of the query.
-The wildcard `*` can be used to include all variables that are currently in scope.
-====
+* xref:clauses/with.adoc#create-new-variables[Create new variables]
+* xref:clauses/with.adoc#variable-scope[Control variables in scope]
+* xref:clauses/with.adoc#bind-values-to-variables[Bind the results of expressions to new variables]
+* xref:clauses/with.adoc#aggregations[Perform aggregations]
+* xref:clauses/with.adoc#remove-duplicate-values[Remove duplicate values]
+* xref:clauses/with.adoc#ordering-and-pagination[Order and paginate results]
+* xref:clauses/with.adoc#filter-results[Filter results]
-Using `WITH`, you can manipulate the output before it is passed on to the following query parts.
-Manipulations can be done to the shape and/or number of entries in the result set.
+[[example-graph]]
+== Example graph
+A graph with the following schema is used for the examples below:
-One common usage of `WITH` is to limit the number of entries passed on to other `MATCH` clauses.
-By combining `ORDER BY` and `LIMIT`, it is possible to get the top X entries by some criteria and then bring in additional data from the graph.
+image::with_clause.svg[width="600",role="middle"]
-`WITH` can also be used to introduce new variables containing the results of expressions for use in the following query parts (see xref::clauses/with.adoc#with-introduce-variables[Introducing variables for expressions]).
-For convenience, the wildcard `*` expands to all variables that are currently in scope and carries them over to the next query part (see xref::clauses/with.adoc#with-wildcard[Using the wildcard to carry over variables]).
+To recreate the graph, run the following query against an empty Neo4j database.
-Another use is to filter on aggregated values.
-`WITH` is used to introduce aggregates which can then be used in predicates in `WHERE`.
-These aggregate expressions create new bindings in the results.
+[source, cypher, role=test-setup]
+----
+CREATE (techCorp:Supplier {name: 'TechCorp', email: 'contact@techcorp.com'}),
+ (foodies:Supplier {name: 'Foodies Inc.', email: 'info@foodies.com'}),
+
+ (laptop:Product {name: 'Laptop', price: 1000}),
+ (phone:Product {name: 'Phone', price: 500}),
+ (headphones:Product {name: 'Headphones', price: 250}),
+ (chocolate:Product {name: 'Chocolate', price: 5}),
+ (coffee:Product {name: 'Coffee', price: 10}),
+
+ (amir:Customer {firstName: 'Amir', lastName: 'Rahman', email: 'amir.rahman@example.com', discount: 0.1}),
+ (keisha:Customer {firstName: 'Keisha', lastName: 'Nguyen', email: 'keisha.nguyen@example.com', discount: 0.2}),
+ (mateo:Customer {firstName: 'Mateo', lastName: 'Ortega', email: 'mateo.ortega@example.com', discount: 0.05}),
+ (hannah:Customer {firstName: 'Hannah', lastName: 'Connor', email: 'hannah.connor@example.com', discount: 0.15}),
+ (leila:Customer {firstName: 'Leila', lastName: 'Haddad', email: 'leila.haddad@example.com', discount: 0.1}),
+ (niko:Customer {firstName: 'Niko', lastName: 'Petrov', email: 'niko.petrov@example.com', discount: 0.25}),
+ (yusuf:Customer {firstName: 'Yusuf', lastName: 'Abdi', email: 'yusuf.abdi@example.com', discount: 0.1}),
+
+ (amir)-[:BUYS {date: date('2024-10-09')}]->(laptop),
+ (amir)-[:BUYS {date: date('2025-01-10')}]->(chocolate),
+ (keisha)-[:BUYS {date: date('2023-07-09')}]->(headphones),
+ (mateo)-[:BUYS {date: date('2025-03-05')}]->(chocolate),
+ (mateo)-[:BUYS {date: date('2025-03-05')}]->(coffee),
+ (mateo)-[:BUYS {date: date('2024-04-11')}]->(laptop),
+ (hannah)-[:BUYS {date: date('2023-12-11')}]->(coffee),
+ (hannah)-[:BUYS {date: date('2024-06-02')}]->(headphones),
+ (leila)-[:BUYS {date: date('2023-05-17')}]->(laptop),
+ (niko)-[:BUYS {date: date('2025-02-27')}]->(phone),
+ (niko)-[:BUYS {date: date('2024-08-23')}]->(headphones),
+ (niko)-[:BUYS {date: date('2024-12-24')}]->(coffee),
+ (yusuf)-[:BUYS {date: date('2024-12-24')}]->(chocolate),
+ (yusuf)-[:BUYS {date: date('2025-01-02')}]->(laptop),
+
+ (techCorp)-[:SUPPLIES]->(laptop),
+ (techCorp)-[:SUPPLIES]->(phone),
+ (techCorp)-[:SUPPLIES]->(headphones),
+ (foodies)-[:SUPPLIES]->(chocolate),
+ (foodies)-[:SUPPLIES]->(coffee)
+----
+// end::clauses_with_variables[]
-`WITH` is also used to separate reading from updating of the graph.
-Every part of a query must be either read-only or write-only.
-When going from a writing part to a reading part, the switch must be done with a `WITH` clause.
+[[create-new-variables]]
+== Create new variables
-image:graph_with_clause.svg[]
+`WITH` can be used in combination with the `AS` keyword to bind new variables which can then be passed to subsequent clauses.
-////
-[source, cypher, role=test-setup]
+.Create a new variable
+[source, cypher]
----
-CREATE
- (a {name: 'Anders'}),
- (b {name: 'Bossman'}),
- (c {name: 'Caesar'}),
- (d {name: 'David'}),
- (e {name: 'George'}),
- (a)-[:KNOWS]->(b),
- (a)-[:BLOCKS]->(c),
- (d)-[:KNOWS]->(a),
- (b)-[:KNOWS]->(e),
- (c)-[:KNOWS]->(e),
- (b)-[:BLOCKS]->(d)
+WITH [1, 2, 3] AS list
+RETURN list
----
-////
+.Result
+[role="queryresult",options="header,footer",cols="1*(:Product {name: 'Chocolate'})
+WITH c AS customer
+RETURN customer.firstName AS chocolateCustomer
----
-// end::clauses_with_variables[]
-
-This query returns the name of persons connected to *'George'* whose name starts with a `C`, regardless of capitalization.
+// end::clauses_with_wildcard[]
.Result
[role="queryresult",options="header,footer",cols="1*(otherPerson)
-WITH *, type(r) AS connectionType
-RETURN person.name, otherPerson.name, connectionType
+MATCH (c:Customer)-[:BUYS]->(p:Product {name: 'Chocolate'})
+WITH c.name AS chocolateCustomers
+RETURN chocolateCustomers,
+ p.price AS chocolatePrice
----
-// end::clauses_with_wildcard[]
-This query returns the names of all related persons and the type of relationship between them.
+.Error message
+[source, error]
+----
+Variable `p` not defined
+----
+
+.Retain all variables with `WITH *`
+[source, cypher]
+----
+MATCH (supplier:Supplier)-[r]->(product:Product)
+WITH *
+RETURN s.name AS company,
+ type(r) AS relType,
+ product.name AS product
+----
.Result
[role="queryresult",options="header,footer",cols="3* Import variables].
+
+[[bind-values-to-variables]]
+== Bind values to variables
+
+`WITH` can be used to assign the values of expressions to variables.
+In the below query, the value of the xref:expressions/string-operators.adoc[`STRING` concatenation] expression is bound to a new variable `customerFullName`, and the value from the expression `chocolate.price * (1 - customer.discount)` is bound to `chocolateNetPrice`, both of which are then available in the `RETURN` clause.
+
+.Bind values to variables
+[source, cypher]
----
-MATCH (david {name: 'David'})--(otherPerson)-->()
-WITH otherPerson, count(*) AS foaf
-WHERE foaf > 1
-RETURN otherPerson.name
+MATCH (customer:Customer)-[:BUYS]->(chocolate:Product {name: 'Chocolate'})
+WITH customer.firstName || ' ' || customer.lastName AS customerFullName,
+ chocolate.price * (1 - customer.discount) AS chocolateNetPrice
+RETURN customerFullName,
+ chocolateNetPrice
----
-The name of the person connected to *'David'* with the at least more than one outgoing relationship will be returned by the query.
+.Result
+[role="queryresult",options="header,footer",cols="2*= 500 AS isExpensive
+WITH p, isExpensive, NOT isExpensive AS isAffordable
+WITH p, isExpensive, isAffordable,
+ CASE
+ WHEN isExpensive THEN 'High-end'
+ ELSE 'Budget'
+ END AS discountCategory
+RETURN p.name AS product,
+ p.price AS price,
+ isAffordable,
+ discountCategory
+ORDER BY price
+----
.Result
-[role="queryresult",options="header,footer",cols="1*(p:Product)
+WITH c.firstName AS customer,
+ sum(p.price) AS totalSpent,
+ collect(p.name) AS productsBought
+RETURN customer,
+ totalSpent,
+ productsBought
+ORDER BY totalSpent DESC
----
-A list of the names of people in reverse order, limited to 3, is returned in a list.
-
.Result
-[role="queryresult",options="header,footer",cols="1* 2
-RETURN x
+MATCH (c:Customer)-[:BUYS]->(p:Product)
+WITH c,
+ sum(p.price) AS totalSpent
+ ORDER BY totalSpent DESC
+RETURN c.firstName AS customer, totalSpent
----
-The limit is first applied, reducing the rows to the first 5 items in the list. The filter is then applied, reducing the final result as seen below:
-
.Result
-[role="queryresult",options="header,footer",cols="1*(p:Product)
+WITH c,
+ sum(p.price) AS totalSpent
+ ORDER BY totalSpent DESC
+ LIMIT 3
+SET c.topSpender = true
+RETURN c.firstName AS customer,
+ totalSpent,
+ c.topSpender AS topSpender
+----
+
+[role="queryresult",options="header,footer", cols="3*(p:Product)
+WITH c,
+ sum(p.price) AS totalSpent
+ ORDER BY totalSpent DESC
+ SKIP 3
+SET c.topSpender = false
+RETURN c.firstName AS customer,
+ totalSpent,
+ c.topSpender AS topSpender
+----
+
+[role="queryresult",options="header,footer", cols="3*(p:Product)
+WITH p
+ ORDER BY p.price DESC
+ LIMIT 1
+MATCH (p)<-[:BUYS]-(c:Customer)
+RETURN p.name AS product
+ p.price AS price,
+ collect(c.firstName) AS customers
+----
+
+
+[role="queryresult",options="header,footer", cols="3* 2
-WITH x
-LIMIT 5
+ WHERE x > 2
RETURN x
----
-This time the filter is applied first, reducing the rows to consist of the list `[3, 4, 5, 6]`.
-Then the limit is applied.
-As the limit is larger than the total number of remaining rows, all rows are returned.
-
-.Result
-[role="queryresult",options="header,footer",cols="1*(p:Product)<-[:BUYS]-(c:Customer)
+WITH s,
+ sum(p.price) AS totalSales,
+ count(DISTINCT c) AS uniqueCustomers
+ WHERE totalSales > 1000
+RETURN s.name AS supplier,
+ totalSales,
+ uniqueCustomers
+----
+
+[role="queryresult",options="header,footer", cols="3*
Date: Tue, 15 Apr 2025 10:02:33 +0200
Subject: [PATCH 2/8] Cypher 5 changes
---
modules/ROOT/pages/clauses/with.adoc | 68 ++++++++++++++--------------
1 file changed, 33 insertions(+), 35 deletions(-)
diff --git a/modules/ROOT/pages/clauses/with.adoc b/modules/ROOT/pages/clauses/with.adoc
index 36ac8b6c3..cd8746723 100644
--- a/modules/ROOT/pages/clauses/with.adoc
+++ b/modules/ROOT/pages/clauses/with.adoc
@@ -12,6 +12,7 @@ The `WITH` clause serves multiple purposes in Cypher:
* xref:clauses/with.adoc#remove-duplicate-values[Remove duplicate values]
* xref:clauses/with.adoc#ordering-and-pagination[Order and paginate results]
* xref:clauses/with.adoc#filter-results[Filter results]
+* xref:clauses/with.adoc#combine-write-and-read-clauses[Combine write and read clauses]
[[example-graph]]
== Example graph
@@ -61,7 +62,6 @@ CREATE (techCorp:Supplier {name: 'TechCorp', email: 'contact@techcorp.com'}),
(foodies)-[:SUPPLIES]->(chocolate),
(foodies)-[:SUPPLIES]->(coffee)
----
-// end::clauses_with_variables[]
[[create-new-variables]]
== Create new variables
@@ -95,7 +95,6 @@ MATCH (c:Customer)-[:BUYS]->(:Product {name: 'Chocolate'})
WITH c AS customer
RETURN customer.firstName AS chocolateCustomer
----
-// end::clauses_with_wildcard[]
.Result
[role="queryresult",options="header,footer",cols="1*(coffee),
+ (yusuf)-[:BUYS {date: date('2025-04-15')}]->(headphones)
+WITH yusuf
+MATCH (yusuf)-[r:BUYS]->(p:Product)
+RETURN collect(p.name) AS yusufPurchases,
+ r.date AS date
+ORDER BY date DESC
+----
+
+[role="queryresult",options="header,footer", cols="2*
Date: Tue, 15 Apr 2025 10:13:53 +0200
Subject: [PATCH 3/8] query fix
---
modules/ROOT/pages/clauses/with.adoc | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/modules/ROOT/pages/clauses/with.adoc b/modules/ROOT/pages/clauses/with.adoc
index cd8746723..7bebece28 100644
--- a/modules/ROOT/pages/clauses/with.adoc
+++ b/modules/ROOT/pages/clauses/with.adoc
@@ -141,7 +141,7 @@ Variable `p` not defined
----
MATCH (supplier:Supplier)-[r]->(product:Product)
WITH *
-RETURN s.name AS company,
+RETURN supplier.name AS company,
type(r) AS relType,
product.name AS product
----
From e662f491bee7093ce4851fc91ccba5a16d6181c4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jens=20Pryce-=C3=85klundh?=
<112686610+JPryce-Aklundh@users.noreply.github.com>
Date: Tue, 15 Apr 2025 10:16:06 +0200
Subject: [PATCH 4/8] remove link
---
modules/ROOT/pages/clauses/with.adoc | 1 -
1 file changed, 1 deletion(-)
diff --git a/modules/ROOT/pages/clauses/with.adoc b/modules/ROOT/pages/clauses/with.adoc
index 7bebece28..3dbbc3840 100644
--- a/modules/ROOT/pages/clauses/with.adoc
+++ b/modules/ROOT/pages/clauses/with.adoc
@@ -444,7 +444,6 @@ RETURN p.name AS product
`WITH` can be followed by the xref:clauses/where.adoc[`WHERE`] subclause to filter results.
Similar to the subclauses used for xref:clauses/with.adoc#ordering-pagination[ordering and pagination], `WHERE` should be understood as part of the result manipulation performed by `WITH` -- not as a standalone clause -- before the results are passed on to subsequent clauses.
-For more information, see xref:clauses/where.adoc#where-and-with[Using `WHERE` after `WITH`].
.Filter using `WITH` and `WHERE`
[source, cypher]
From ff318993c4fec55099a11317cc724f68f839b8dc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jens=20Pryce-=C3=85klundh?=
<112686610+JPryce-Aklundh@users.noreply.github.com>
Date: Tue, 15 Apr 2025 10:24:00 +0200
Subject: [PATCH 5/8] spelling
---
modules/ROOT/pages/clauses/with.adoc | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/modules/ROOT/pages/clauses/with.adoc b/modules/ROOT/pages/clauses/with.adoc
index 3dbbc3840..8e4378101 100644
--- a/modules/ROOT/pages/clauses/with.adoc
+++ b/modules/ROOT/pages/clauses/with.adoc
@@ -323,7 +323,7 @@ ORDER BY discountRates
== Ordering and pagination
`WITH` can order and paginate results if used together with the xref:clauses/order-by.adoc[`ORDER BY`], xref:clauses/limit.adoc[`LIMIT`], and xref:clauses/skip.adoc[`SKIP`] subclauses.
-If so, these subclauses be understood as part of the result manipulation performed by `WITH` -- not as a standalone clause -- before results are passed on to subsequent clauses.
+If so, these subclauses should be understood as part of the result manipulation performed by `WITH` -- not as standalone clauses -- before results are passed on to subsequent clauses.
In the below query, the results are ordered in a descending order by which `Customer` has spent the most using `ORDER BY` before they are passed on to the final `RETURN` clause.
From 37ecadc0c3067c1a13aef7495c0f121f7ce3d0e9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jens=20Pryce-=C3=85klundh?=
<112686610+JPryce-Aklundh@users.noreply.github.com>
Date: Tue, 15 Apr 2025 10:27:51 +0200
Subject: [PATCH 6/8] query fix
---
modules/ROOT/pages/clauses/with.adoc | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/modules/ROOT/pages/clauses/with.adoc b/modules/ROOT/pages/clauses/with.adoc
index 8e4378101..e1dcc5c4d 100644
--- a/modules/ROOT/pages/clauses/with.adoc
+++ b/modules/ROOT/pages/clauses/with.adoc
@@ -246,7 +246,7 @@ ORDER BY price
| "Chocolate" | 5 | TRUE | 'Budget'
| "Coffee" | 10 | TRUE | 'Budget'
| "Headphones" | 250 | TRUE | 'Budget'
-| "Phone" | 600 | FALSE | 'High-end'
+| "Phone" | 500 | FALSE | 'High-end'
| "Laptop" | 1000 | FALSE | 'High-end'
4+d|Rows: 5
From 9d46a1a0bc9ae15c2ac5dbcbafed1ceada1f1359 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jens=20Pryce-=C3=85klundh?=
<112686610+JPryce-Aklundh@users.noreply.github.com>
Date: Tue, 15 Apr 2025 10:31:57 +0200
Subject: [PATCH 7/8] another query fix
---
modules/ROOT/pages/clauses/with.adoc | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/modules/ROOT/pages/clauses/with.adoc b/modules/ROOT/pages/clauses/with.adoc
index e1dcc5c4d..066d91b93 100644
--- a/modules/ROOT/pages/clauses/with.adoc
+++ b/modules/ROOT/pages/clauses/with.adoc
@@ -423,7 +423,7 @@ WITH p
ORDER BY p.price DESC
LIMIT 1
MATCH (p)<-[:BUYS]-(c:Customer)
-RETURN p.name AS product
+RETURN p.name AS product,
p.price AS price,
collect(c.firstName) AS customers
----
From 6bd23a192892fc32e818d7333f8348bbaef5fe62 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jens=20Pryce-=C3=85klundh?=
<112686610+JPryce-Aklundh@users.noreply.github.com>
Date: Tue, 15 Apr 2025 11:08:15 +0200
Subject: [PATCH 8/8] cheat sheet tags
---
modules/ROOT/pages/clauses/with.adoc | 25 +++++++++++++++++++++++++
1 file changed, 25 insertions(+)
diff --git a/modules/ROOT/pages/clauses/with.adoc b/modules/ROOT/pages/clauses/with.adoc
index 066d91b93..33cf54228 100644
--- a/modules/ROOT/pages/clauses/with.adoc
+++ b/modules/ROOT/pages/clauses/with.adoc
@@ -89,12 +89,15 @@ In the below example, the `WITH` clause binds all matched `Customer` nodes to a
The bound nodes are `MAP` values which can then be referenced from the new variable.
.Create a new variable bound to matched nodes
+// tag::clauses_with_new_variable[]
[source, cypher]
----
MATCH (c:Customer)-[:BUYS]->(:Product {name: 'Chocolate'})
WITH c AS customer
RETURN customer.firstName AS chocolateCustomer
----
+// end::clauses_with_new_variable[]
+
.Result
[role="queryresult",options="header,footer",cols="1*(product:Product)
@@ -145,6 +149,7 @@ RETURN supplier.name AS company,
type(r) AS relType,
product.name AS product
----
+// end::clauses_with_all_variables[]
.Result
[role="queryresult",options="header,footer",cols="3*(chocolate:Product {name: 'Chocolate'})
@@ -205,6 +214,8 @@ WITH customer.firstName || ' ' || customer.lastName AS customerFullName,
RETURN customerFullName,
chocolateNetPrice
----
+// end::clauses_with_bind_values[]
+
.Result
[role="queryresult",options="header,footer",cols="2*(p:Product)
@@ -272,6 +286,8 @@ RETURN customer,
productsBought
ORDER BY totalSpent DESC
----
+// end::clauses_with_aggregations[]
+
.Result
[role="queryresult",options="header,footer", cols="3*(p:Product)
@@ -369,6 +388,8 @@ RETURN c.firstName AS customer,
totalSpent,
c.topSpender AS topSpender
----
+// end::clauses_with_ordering_pagination[]
+
[role="queryresult",options="header,footer", cols="3*(p:Product)<-[:BUYS]-(c:Customer)
@@ -481,6 +503,7 @@ RETURN s.name AS supplier,
totalSales,
uniqueCustomers
----
+// end::clauses_with_filtering[]
[role="queryresult",options="header,footer", cols="3*