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 @@ +BOUGHTdate:DATESUPPLIESCustomerfirstName:STRINGlastName:STRINGemail:STRINGdiscount:FLOATProductname:STRINGprice:INTEGERSuppliername:STRINGemail:STRING \ 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*