diff --git a/modules/ROOT/content-nav.adoc b/modules/ROOT/content-nav.adoc index 1a27ec07d..1a09a10a2 100644 --- a/modules/ROOT/content-nav.adoc +++ b/modules/ROOT/content-nav.adoc @@ -18,6 +18,7 @@ ** xref:clauses/filter.adoc[] ** xref:clauses/finish.adoc[] ** xref:clauses/foreach.adoc[] +** xref:clauses/let.adoc[] ** xref:clauses/limit.adoc[] ** xref:clauses/load-csv.adoc[] ** xref:clauses/match.adoc[] diff --git a/modules/ROOT/images/let_clause.svg b/modules/ROOT/images/let_clause.svg new file mode 100644 index 000000000..f77938445 --- /dev/null +++ b/modules/ROOT/images/let_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/appendix/gql-conformance/supported-optional.adoc b/modules/ROOT/pages/appendix/gql-conformance/supported-optional.adoc index 164229a74..0f930c4ab 100644 --- a/modules/ROOT/pages/appendix/gql-conformance/supported-optional.adoc +++ b/modules/ROOT/pages/appendix/gql-conformance/supported-optional.adoc @@ -178,6 +178,11 @@ For example, GQL’s graph reference values `CURRENT_GRAPH` and `CURRENT_PROPERT | xref:clauses/filter.adoc[`FILTER`] | +| GQ09 +| `LET` statement +| xref:clauses/let.adoc[`LET`] +| + | GQ13 | `ORDER BY` and page statement: `LIMIT` | xref:clauses/limit.adoc[`LIMIT`], xref:clauses/order-by.adoc[`ORDER BY`] diff --git a/modules/ROOT/pages/clauses/index.adoc b/modules/ROOT/pages/clauses/index.adoc index 07192fe4f..fce6b31ee 100644 --- a/modules/ROOT/pages/clauses/index.adoc +++ b/modules/ROOT/pages/clauses/index.adoc @@ -17,6 +17,11 @@ This set is refined and augmented by subsequent parts of the query. |=== | Clause | Description + +m| xref::clauses/filter.adoc[FILTER] +| Adds filters to queries. +label:new[Introduced in Neo4j 2025.04] + m| xref::clauses/match.adoc[MATCH] | Specify the patterns to search for in the database. @@ -35,20 +40,21 @@ The returned expressions may all be aliased using `AS`. |=== | Clause | Description +m| xref::clauses/finish.adoc[FINISH] +| Defines a query to have no result. + +m| xref::clauses/let.adoc[LET] +| Binds values to variables. +label:new[Introduced in Neo4j 2025.04] + m| xref::clauses/return.adoc[RETURN ... [AS]] | Defines what to include in the query result set. -m| xref::clauses/with.adoc[WITH ... [AS]] -| Allows query parts to be chained together, piping the results from one to be used as starting points or criteria in the next. - m| xref::clauses/unwind.adoc[UNWIND ... [AS]] | Expands a list into a sequence of rows. -m| xref::clauses/filter.adoc[FILTER] -| Adds filters to queries. - -m| xref::clauses/finish.adoc[FINISH] -| Defines a query to have no result. +m| xref::clauses/with.adoc[WITH ... [AS]] +| Allows query parts to be chained together, piping the results from one to be used as starting points or criteria in the next. |=== diff --git a/modules/ROOT/pages/clauses/let.adoc b/modules/ROOT/pages/clauses/let.adoc new file mode 100644 index 000000000..01a6ad455 --- /dev/null +++ b/modules/ROOT/pages/clauses/let.adoc @@ -0,0 +1,394 @@ += LET +:description: Information about Cypher's `LET` clause. +:table-caption!: +:page-role: new-2025.04 + +`LET` binds expressions to variables. +For queries involving several chained expressions, it can be a more succinct and readable alternative to xref:clauses/with.adoc[`WITH`]. +Unlike `WITH`, `LET` does not drop variables from the scope of subsequent clauses. +Nor can it be used for aggregations or in combination with `DISTINCT`; it can only be used to bind new variables. + +[[example-graph]] +== Example graph + +A graph with the following schema is used for the examples below: + +image::let_clause.svg[width="600",role="middle"] + +To recreate the graph, run the following query against an empty Neo4j database. + +[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) +---- + +[[assigning-expressions-to-variables]] +== Bind values to variables + +`LET` is used to bind variables to the results of expressions. + +.Syntax +[source, syntax] +---- +LET variable = expression, variable = expression +---- + +.Using `LET` to bind a variable +[source, cypher] +---- +MATCH (c:Customer) +LET fullName = c.firstName + ' ' + c.lastName +RETURN fullName +---- + +.Result +[role="queryresult",options="header,footer", cols="1*(p:Product) +LET supplier = s.name, product = p.name +RETURN supplier, product +---- + +.Result +[role="queryresult",options="header,footer", cols="2* = ` is a substitute for `WITH *, AS `, not `WITH AS ` (which would drop any variables present in the preceding clause not referenced in ``). + +.Variables in scope: comparing `LET` and `WITH` +===== + +Any variable not explicitly referenced by `WITH` (or carried over by `WITH *`) is dropped from the scope of subsequent clauses. + +.Not allowed -- Referencing a variable dropped by `WITH` +[source, cypher, role=test-fail] +---- +MATCH (s:Supplier)-[:SUPPLIES]->(p:Product) +WITH s.name AS supplier +RETURN supplier, p.name AS product +---- + +.Error message +[source, error] +---- +Variable `p` not defined +---- + +`LET`, however, cannot regulate which variables are in scope. +Replacing `WITH` with `LET` in the above query would, therefore, return results. + +.`LET` does not drop variables +[source, cypher] +---- +MATCH (s:Supplier)-[:SUPPLIES]->(p:Product) +LET supplier = s.name +RETURN supplier, p.name AS product +---- + +.Result +[role="queryresult",options="header,footer", cols="2*= 500 +LET isAffordable = NOT isExpensive +LET discountCategory = CASE + WHEN isExpensive THEN 'High-end' + ELSE 'Budget' +END +RETURN p.name AS product, p.price AS price, isAffordable, discountCategory +ORDER BY price +---- + +.Result +[role="queryresult",options="header,footer", cols="4*= 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 +---- + +===== + +[[aggregations-distinct]] +=== Aggregations and `DISTINCT` + +Unlike `WITH`, `LET` cannot perform aggregations or be combined with `DISTINCT`. +For example, in the following query, `WITH` could not be replaced by `LET`: + +.Combining `WITH DISTINCT` and aggregations on expressions +[source, source] +---- +MATCH (c:Customer)-[:BUYS]->(p:Product) +WITH DISTINCT c, sum(p.price) AS totalSpent +RETURN c.firstName AS customer, totalSpent +---- + +.Result +[role="queryresult",options="header,footer", cols="2*(p:Product) +WITH DISTINCT c, sum(p.price) AS totalSpent +LET fullName = c.firstName + ' ' + c.lastName +RETURN fullName, totalSpent +---- + +.Result +[role="queryresult",options="header,footer", cols="2*(p:Product)<--(s:Supplier) +LET fullname = c.firstName + ' ' + c.lastName, + effectivePrice = p.price * (1 - c.discount) +LET message = fullname + " bought " + p.name + " for $" + effectivePrice + " after a " + (c.discount * 100) + "% discount" +RETURN b.date AS date, message, s.email AS toSupplier +ORDER BY date +---- + +.Result +[role="queryresult",options="header,footer", cols="1m, 2m, 1m"] +|=== +| date | message | toSupplier + +| 2023-05-17 | "Leila Haddad bought Laptop for $900.0 after a 10.0% discount" | "contact@techcorp.com" +| 2023-07-09 | "Keisha Nguyen bought Headphones for $200.0 after a 20.0% discount" | "contact@techcorp.com" +| 2023-12-11 | "Hannah Connor bought Coffee for $8.5 after a 15.0% discount" | "info@foodies.com" +| 2024-04-11 | "Mateo Ortega bought Laptop for $950.0 after a 5.0% discount" | "contact@techcorp.com" +| 2024-06-02 | "Hannah Connor bought Headphones for $212.5 after a 15.0% discount" | "contact@techcorp.com" +| 2024-08-23 | "Niko Petrov bought Headphones for $187.5 after a 25.0% discount" | "contact@techcorp.com" +| 2024-10-09 | "Amir Rahman bought Laptop for $900.0 after a 10.0% discount" | "contact@techcorp.com" +| 2024-12-24 | "Yusuf Abdi bought Chocolate for $4.5 after a 10.0% discount" | "info@foodies.com" +| 2024-12-24 | "Niko Petrov bought Coffee for $7.5 after a 25.0% discount" | "info@foodies.com" +| 2025-01-02 | "Yusuf Abdi bought Laptop for $900.0 after a 10.0% discount" | "contact@techcorp.com" +| 2025-01-10 | "Amir Rahman bought Chocolate for $4.5 after a 10.0% discount" | "info@foodies.com" +| 2025-02-27 | "Niko Petrov bought Phone for $375.0 after a 25.0% discount" | "contact@techcorp.com" +| 2025-03-05 | "Mateo Ortega bought Chocolate for $4.75 after a 5.0% discount" | "info@foodies.com" +| 2025-03-05 | "Mateo Ortega bought Coffee for $9.5 after a 5.0% discount" | "info@foodies.com" + +3+d| Rows: 14 +|=== + +===== + +.Scenario 2: Supplier gift card distribution based on customer spending +===== + +The example calculates the `customerRevenue` for each `Customer` after applying their `discount` on each `Product` they bought. Customers are then categorized into three groups based on their total spending: Category `A` for those who spent more than `850`, Category `B` for those who spent more than `350` but less than or equal to `850`, and Category `C` for those who spent `350` or less. +Category `C` customers are excluded from the results using the xref:clauses/filter.adoc[`FILTER`] clause, leaving only Category `A` and `B` customers eligible for a gift card. +The `amount` in the gift card is assigned based on the category, with with Category `A` receiving `20` and Category `B` receiving `10`. +The details of the gift card are then sent to the `email` of the relevant customers. + +This example highlights how `LET` can be used to succinctly chain expressions, and also that it cannot be used to perform aggregations. + +.Calculate customer gift card distribution based on spending +[source, cypher] +---- +MATCH (customer:Customer)-[bought:BUYS]->(product:Product) +LET effectivePrice = product.price * (1 - customer.discount) +WITH customer, bought, sum(effectivePrice) AS customerRevenue +LET category = CASE + WHEN customerRevenue > 850 THEN 'A' + WHEN customerRevenue > 350 THEN 'B' + ELSE 'C' + END +FILTER category <> 'C' +LET amount = CASE + WHEN category = 'A' THEN 20 + WHEN category = 'B' THEN 10 +END +LET message = { + type: 'giftcard', + addressee: customer.firstName + ' ' + customer.lastName, + amount: amount, + year: bought.date.year +} +RETURN message, customer.email AS toCustomer, customerRevenue +ORDER BY amount +---- + +.Result +[role="queryresult",options="header,footer", cols="3m, 2m, 1m"] +|=== +| message | toCustomer | customerRevenue + +| {amount: 10, addressee: "Niko Petrov", type: "giftcard", year: 2025} | "niko.petrov@example.com" | 375.0 +| {amount: 20, addressee: "Amir Rahman", type: "giftcard", year: 2024} | "amir.rahman@example.com" | 900.0 +| {amount: 20, addressee: "Mateo Ortega", type: "giftcard", year: 2024} | "mateo.ortega@example.com" | 950.0 +| {amount: 20, addressee: "Leila Haddad", type: "giftcard", year: 2023} | "leila.haddad@example.com" | 900.0 +| {amount: 20, addressee: "Yusuf Abdi", type: "giftcard", year: 2025} | "yusuf.abdi@example.com" | 900.0 + +3+d|Rows: 5 +|=== +===== \ No newline at end of file diff --git a/modules/ROOT/pages/deprecations-additions-removals-compatibility.adoc b/modules/ROOT/pages/deprecations-additions-removals-compatibility.adoc index 9d2a4e1af..63b2f457e 100644 --- a/modules/ROOT/pages/deprecations-additions-removals-compatibility.adoc +++ b/modules/ROOT/pages/deprecations-additions-removals-compatibility.adoc @@ -209,6 +209,19 @@ a| label:functionality[] label:new[] +[source, cypher, role="noheader"] +---- +MATCH (s:Supplier)-[:SUPPLIES]->(p:Product) +LET supplier = s.name +RETURN supplier, p.name AS product +---- + +| New xref:clauses/let.adoc[`LET`] clause used to bind values to variables. + +a| +label:functionality[] +label:new[] + [source, cypher, role="noheader"] ---- UNWIND [1, 2, 3, 4, 5, 6] AS x diff --git a/modules/ROOT/pages/syntax/keywords.adoc b/modules/ROOT/pages/syntax/keywords.adoc index dc71b20a2..792c0160a 100644 --- a/modules/ROOT/pages/syntax/keywords.adoc +++ b/modules/ROOT/pages/syntax/keywords.adoc @@ -215,6 +215,7 @@ Note that with future functionality, Cypher may be extended with additional keyw * `LABEL` * `LABELS` * `LEADING` +* `LET` * `LIMITROWS` * `LIST` * `LOAD`