diff --git a/docs.json b/docs.json index 9b64f3b..d797c5a 100644 --- a/docs.json +++ b/docs.json @@ -67,6 +67,7 @@ "query/matching", "query/paths", "query/aggregation", + "query/composition", "query/subqueries", "query/functions", "query/writing", diff --git a/query/aggregation.mdx b/query/aggregation.mdx index ac770c8..f702ead 100644 --- a/query/aggregation.mdx +++ b/query/aggregation.mdx @@ -1,7 +1,7 @@ --- title: "Results, Aggregation and Lists" sidebarTitle: "Results and Aggregation" -description: "Shape results with WITH, DISTINCT, ORDER BY, SKIP, LIMIT and UNION, chain queries with NEXT and WHEN, then aggregate, unwind and transform lists." +description: "Shape results with WITH, DISTINCT, ORDER BY, SKIP, LIMIT and UNION, then aggregate, unwind and transform lists." --- These examples use the [sample graph](/query/matching) from the matching page. @@ -45,7 +45,7 @@ RETURN n.name, n.age ORDER BY n.age DESC SKIP 1 LIMIT 2 ``` -- You can sort by several keys, by aliases and by expressions. Ascending is the default. +- You can sort by several keys, by aliases and by expressions. Ascending is the default, and `ASCENDING` / `DESCENDING` are synonyms of `ASC` / `DESC`. - `SKIP` and `LIMIT` combine for pagination: `SKIP 10 LIMIT 10` returns the 11th to 20th rows. - `SKIP` and `LIMIT` take constant expressions. - `RETURN DISTINCT ... ORDER BY` can only sort by returned columns. @@ -97,48 +97,7 @@ RETURN c.name AS name `UNION` removes duplicates and `UNION ALL` keeps them. All branches must return the same column names, and each column must keep one value type across the branches. -## NEXT - -`NEXT` chains queries in one request: the columns returned by the query before it become the incoming rows of the query after it. - -```cypher -MATCH (n:Person) -RETURN n.name AS nm, n.age AS age -NEXT -RETURN nm -ORDER BY nm LIMIT 2 - -MATCH (n:Person) -RETURN n.city AS city, count(*) AS people -NEXT -RETURN max(people) AS biggestCity -``` - -- Every expression in a `RETURN` followed by `NEXT` needs an alias (`AS`). -- A later query sees what an earlier one wrote: `CREATE (x:Tag {v: 1}) RETURN x NEXT MATCH (t:Tag) RETURN count(t)`. - -## Conditional queries with WHEN - -`WHEN THEN [WHEN ... THEN ...] [ELSE ]` runs the first branch whose condition holds. Branches return the same columns, and they can also write. - -```cypher -WHEN true THEN RETURN 1 AS x ELSE RETURN 2 AS x - -MATCH (n:Person) -RETURN n.name AS nm, n.age AS age -NEXT -WHEN age > 30 THEN RETURN nm, 'senior' AS band -ELSE RETURN nm, 'junior' AS band - -MATCH (n:Person) -CALL (n) { - WHEN n.age > 30 THEN RETURN 'senior' AS band - ELSE RETURN 'junior' AS band -} -RETURN n.name, band -``` - -A `WHEN` can start a query, follow `NEXT`, or form the body of a [`CALL` subquery](/query/subqueries#call-subqueries), where it runs once per row. Several `WHEN` queries combine with `UNION`: `{ WHEN c1 THEN RETURN 1 AS x } UNION { WHEN c2 THEN RETURN 2 AS x }`. +To run several queries in one request, or choose which query runs, see [NEXT and WHEN](/query/composition). ## Aggregation @@ -176,6 +135,16 @@ ORDER BY c DESC MATCH (n:Person) RETURN stDev(n.age) AS sd, percentileCont(n.age, 0.5) AS median, percentileDisc(n.age, 0.9) AS p90 + +MATCH (n:Person) +RETURN stDev(n.age) AS sample, stDevP(n.age) AS population + +MATCH (n:Person) +WITH n, 0.25 AS q +RETURN percentileCont(n.age, q) AS firstQuartile + +UNWIND [[1, 2], [1, 3], [0, 9]] AS x +RETURN min(x) AS lo, max(x) AS hi ``` | Aggregate | Notes | @@ -246,7 +215,7 @@ RETURN size([1, 2]), head([1, 2]), last([1, 2]), tail([1, 2]), reverse([1, 2, 3] MATCH (n:Person {name: 'Alice'}) RETURN n.tags[0], size(n.tags) -RETURN [1, 2] + [3], [1, 2] + 3, 'a' || 'b' +RETURN [1, 2] + [3], [1, 2] + 3, 0 + [1, 2], 'a' || 'b' ``` - Indexing and slicing work on literals, list properties and function results (`labels(n)[0]`). Negative indexes count from the end, and an out-of-range index returns `null`. diff --git a/query/cheatsheet.mdx b/query/cheatsheet.mdx index b44b4c3..1ce141e 100644 --- a/query/cheatsheet.mdx +++ b/query/cheatsheet.mdx @@ -190,7 +190,7 @@ RETURN [n IN nodes(p) | n.name], length(p), reduce(t = 0, r IN relationships(p) ## WITH, ORDER BY, SKIP, LIMIT, UNION, NEXT, WHEN -[Results and aggregation](/query/aggregation) +[Results and aggregation](/query/aggregation), [NEXT and WHEN](/query/composition) ```cypher MATCH (n:Person) @@ -221,9 +221,20 @@ RETURN n.name AS nm, n.age AS age NEXT WHEN age > 30 THEN RETURN nm, 'senior' AS band ELSE RETURN nm, 'junior' AS band + +MATCH (n:Person) +WHERE n.age > 30 +RETURN n AS p +NEXT +MATCH (p)-[:KNOWS]->(f) +RETURN p.name, f.name + +WHEN COUNT { MATCH (n:Person) } > 3 THEN RETURN 'big' AS size +ELSE RETURN 'small' AS size ``` -- Expressions in `WITH`, and in a `RETURN` followed by `NEXT`, must be aliased. +- Expressions in `WITH`, and in a `RETURN` followed by `NEXT`, must be aliased. Only the returned columns carry over a `NEXT`. +- `WHEN` branches return the same columns. Without `ELSE`, a conditional with no matching branch returns no rows. - Nulls sort last ascending and first descending. ## Aggregation @@ -293,6 +304,16 @@ RETURN p.name, COUNT { (p)-[:KNOWS]->() } AS outDegree MATCH (p:Person) RETURN p.name, COLLECT { MATCH (p)-[:KNOWS]->(f) RETURN f.name } AS friends +MATCH (c:Company) +RETURN c.name, COLLECT { MATCH (p:Person)-[:WORKS_AT]->(c) RETURN p.name ORDER BY p.age DESC LIMIT 1 } AS oldest + +MATCH (p:Person) +CALL (*) { + MATCH (p)-[:KNOWS]->(f) + RETURN count(f) AS deg +} +RETURN p.name, deg + MATCH (p:Person) CALL (p) { MATCH (p)-[:KNOWS]->(f) diff --git a/query/composition.mdx b/query/composition.mdx new file mode 100644 index 0000000..bf1a84a --- /dev/null +++ b/query/composition.mdx @@ -0,0 +1,218 @@ +--- +title: "Chaining and Conditional Queries" +sidebarTitle: "NEXT and WHEN" +description: "Chain queries in one request with NEXT, and choose which query runs with WHEN ... THEN ... ELSE." +--- + +These examples use the [sample graph](/query/matching) from the matching page. + +## NEXT + +`NEXT` runs the query before it, then hands the columns it returns to the query after it, as that query's incoming rows. Use it to run several steps in one request, or to start a new stage from a smaller set of rows. + +```cypher +MATCH (n:Person) +RETURN n.name AS nm, n.age AS age +NEXT +RETURN nm +ORDER BY nm LIMIT 2 + +MATCH (n:Person) +RETURN n.city AS city, count(*) AS people +NEXT +RETURN max(people) AS biggestCity +``` + +### Passing nodes on + +A returned node is still a node in the next step, so you can match from it: + +```cypher +MATCH (n:Person) +WHERE n.age > 30 +RETURN n AS p +NEXT +MATCH (p)-[:KNOWS]->(f) +RETURN p.name, f.name +``` + +### Longer chains + +A chain can have as many steps as you need, and each step is a full query: + +```cypher +MATCH (n:Person) +RETURN n.name AS nm, n.age AS age +NEXT +WITH nm, age +WHERE age > 28 +RETURN nm, age +NEXT +RETURN count(*) AS over28 +``` + +### After a UNION + +A step can be a `UNION`, and the next step reads the combined rows: + +```cypher +MATCH (p:Person) +RETURN p.name AS name +UNION +MATCH (c:Company) +RETURN c.name AS name +NEXT +RETURN count(*) AS total +``` + +A whole chain in braces can also be one side of a `UNION`: + +```cypher +{ MATCH (p:Person) RETURN p.age AS v NEXT RETURN max(v) AS v } +UNION +{ MATCH (c:Company) RETURN c.founded AS v NEXT RETURN max(v) AS v } +``` + +### Inside subqueries + +`NEXT` works inside [`CALL`, `EXISTS` and `COUNT`](/query/subqueries) subqueries, where the chain runs once for each outer row: + +```cypher +MATCH (c:Company) +CALL (c) { + MATCH (p:Person)-[:WORKS_AT]->(c) + RETURN p.age AS age + NEXT + RETURN avg(age) AS avgAge +} +RETURN c.name, avgAge + +MATCH (p:Person) +RETURN p.name, COUNT { + MATCH (p)-[:KNOWS]->(f) + RETURN f AS friend + NEXT + MATCH (friend)-[:KNOWS]->(fof) + RETURN fof +} AS friendsOfFriends +``` + +### Writes + +Inside a [change](/query/writing), a later step sees what an earlier step wrote: + +```cypher +CREATE (t:Tag {name: 'new'}) +RETURN t +NEXT +MATCH (x:Tag) +RETURN count(x) AS tags +``` + +### Rules + +- **Aliases.** Every expression in a `RETURN` followed by `NEXT` needs an alias. Without one: `Expression in a RETURN followed by NEXT must be aliased (use AS)`. +- **Scope.** Only the returned columns carry over. `MATCH (n:Person) RETURN n.name AS nm NEXT RETURN n.age` reports `Variable 'n' not found`, because the first step did not return `n`. +- `NEXT` is not yet available inside a `COLLECT { }` body. + +## WHEN + +`WHEN THEN ` runs a query when the condition holds. Add more `WHEN ... THEN ...` branches and an `ELSE ` as needed: the first branch whose condition holds is the one that runs. + +```cypher +WHEN true THEN RETURN 1 AS x ELSE RETURN 2 AS x +``` + +### Conditions on the data + +Conditions can be subqueries over the graph: + +```cypher +WHEN EXISTS { MATCH (:Company {name: 'Acme'}) } THEN RETURN 'has Acme' AS result +ELSE RETURN 'no Acme' AS result + +WHEN COUNT { MATCH (n:Person) } > 3 THEN RETURN 'big' AS size +ELSE RETURN 'small' AS size +``` + +### For each row, after NEXT + +After `NEXT`, the conditions are checked for each incoming row, and each row takes the first branch that matches: + +```cypher +MATCH (n:Person) +RETURN n.name AS nm, n.age AS age +NEXT +WHEN age > 35 THEN RETURN nm, 'over 35' AS band +WHEN age > 29 THEN RETURN nm, '30 to 35' AS band +ELSE RETURN nm, 'under 30' AS band +``` + +### For each row, in a CALL subquery + +As the body of a [`CALL` subquery](/query/subqueries#call-subqueries), a conditional runs once per outer row, and a branch can match more data: + +```cypher +MATCH (n:Person) +CALL (n) { + WHEN n.city = 'London' THEN + MATCH (n)-[:WORKS_AT]->(c) + RETURN c.name AS employer + ELSE RETURN 'elsewhere' AS employer +} +RETURN n.name, employer +``` + +### Without ELSE + +When no branch matches and there is no `ELSE`, the conditional returns no rows, so `WHEN false THEN RETURN 1 AS x` returns nothing. In a `CALL`, rows that match no branch are dropped. Use `OPTIONAL CALL` to keep them with `null` values: + +```cypher +MATCH (n:Person) +OPTIONAL CALL (n) { + WHEN n.age >= 40 THEN RETURN 'senior' AS band +} +RETURN n.name, band +``` + +### Several steps in a branch + +Put a `NEXT` chain in braces: + +```cypher +WHEN true THEN { + MATCH (n:Person) + RETURN n.city AS city, count(*) AS people + NEXT + RETURN max(people) AS biggest +} +ELSE RETURN 0 AS biggest +``` + +### Combining with UNION + +Conditionals in braces combine with `UNION`. Each one names its columns with `AS`: + +```cypher +{ WHEN true THEN RETURN 'a' AS x } +UNION +{ WHEN false THEN RETURN 'b' AS x ELSE RETURN 'c' AS x } +``` + +### Writes + +Branches can write, inside a change. See [Conditional writes](/query/writing#conditional-writes): + +```cypher +MATCH (n:Person) +CALL (n) { + WHEN n.age > 30 THEN CREATE (:Senior {name: n.name}) +} +RETURN count(*) +``` + +### Rules + +- **Same columns.** All branches return the same column names. Otherwise: `All branches of a conditional query must return the same column names`. +- **Where it goes.** A `WHEN` starts a query, follows `NEXT`, forms a `CALL` body, or is a braced side of a `UNION`. To branch on the results of a `MATCH`, return them and continue after `NEXT`, as above. +- **Not yet available:** `UNION` inside a branch, and `WHEN` inside an `EXISTS`, `COUNT` or `COLLECT` body. diff --git a/query/cypher_subset.mdx b/query/cypher_subset.mdx index 4171773..8786cdc 100644 --- a/query/cypher_subset.mdx +++ b/query/cypher_subset.mdx @@ -13,7 +13,7 @@ If you know Cypher from Neo4j, you can write the queries you are used to. This p | Area | Supported | | ---- | --------- | | Reading | `MATCH`, `OPTIONAL MATCH`, `WHERE`, `WITH`, `RETURN [DISTINCT]`, `ORDER BY`, `SKIP`, `LIMIT`, `UNWIND`, `UNION [ALL]`, `LOAD CSV` | -| Composing queries | `NEXT` to chain queries, `WHEN ... THEN ... ELSE` conditional queries | +| Composing queries | [`NEXT`](/query/composition#next) to chain queries, [`WHEN ... THEN ... ELSE`](/query/composition#when) conditional queries | | Patterns | Multiple labels, label expressions (`:A\|B`, `:!A`, `:A&B`), edge type alternatives (`[:A\|B]`), inline `WHERE`, variable-length paths (`*1..3`, `->+`, `{2,4}`), quantified path patterns, named paths, pattern predicates | | Expressions | Arithmetic, string and list concatenation, `CASE`, list and pattern comprehensions, `reduce`, `all` / `any` / `none` / `single`, list slicing, maps and map access | | Functions | String (`substring`, `split`, `replace`, `left`, `right`, `normalize`, ...), math, conversion, `keys`, `properties` | @@ -74,11 +74,22 @@ df = client.query("MATCH (n:Person) RETURN n.name, n.age") # a pandas DataFram ```python client.query("MATCH (n:$label) RETURN n.name", {"label": "Person"}) client.query("MATCH (n:$labels) RETURN n.name", {"labels": ["Person", "Employee"]}) # every label in the list +client.query("MATCH (n:Person|$other) RETURN n.name", {"other": "Company"}) # inside a label expression +client.query("MATCH (n) WHERE n:$label RETURN n.name", {"label": "Company"}) # in a label test client.query("MATCH (a)-[r:$type]->(b) RETURN a.name, b.name", {"type": "WORKS_AT"}) -client.query("CREATE (n:$label {name: 'Zoe'})", {"label": "Person"}) # inside a change +client.query("MATCH (n:$0) RETURN n.name", {"0": "Company"}) # numbered parameter + +# inside a change +client.query("CREATE (n:$label {name: 'Zoe'})", {"label": "Person"}) +client.query("MERGE (t:$label {name: 'x'})", {"label": "Tag"}) +client.query("MATCH (a:Person {name: 'Alice'}), (b:Person {name: 'Bob'}) MERGE (a)-[:$type]->(b)", {"type": "FOLLOWS"}) ``` -Put values directly in the query text for now: value parameters, such as `WHERE n.name = $name`, are not yet supported. In the [interactive shell](/commands#interactive-shell), set parameters with `:param`. +- A parameter holds a string, or a list of strings to require several labels. +- A parameter used in the query but missing from the dictionary is reported when the query runs: `Parameter $label is not defined`. +- Put values directly in the query text for now: value parameters, such as `WHERE n.name = $name`, are reported as `Not implemented: Parameters`. + +In the [interactive shell](/commands#interactive-shell), set parameters with `:param`. Over [HTTP](/commands#http-api), send them in the `parameters` field of the request. ## Coming from Neo4j @@ -86,7 +97,7 @@ TuringDB follows openCypher closely, so the queries you know carry over. These a - **`RETURN n` returns the node's internal ID**, an integer. Return the properties you need with `RETURN n.name, n.age`, or all of them with `RETURN properties(n)`. - **Values go in the query text.** [Parameters](#query-parameters) name labels and edge types. Put literal values directly in the text and escape quotes in your application. -- **One statement per request.** Send each statement as its own request (a single trailing `;` is fine), or chain steps in one request with [`NEXT`](/query/aggregation#next). +- **One statement per request.** Send each statement as its own request (a single trailing `;` is fine), or chain steps in one request with [`NEXT`](/query/composition#next). - **Labels are set at creation.** Every node has at least one label and every edge exactly one edge type, chosen when it is created. - **Property types are global.** A property name has a single value type across the whole graph: if `age` is an Integer, it is an Integer on every node. - **Comparing Doubles.** Integers and Doubles compare by value (`n.age = 30.0` matches 30). Match one Double against another with a range (`n.score > 1.99 AND n.score < 2.01`). diff --git a/query/functions.mdx b/query/functions.mdx index 8aa11f9..7d5a6b6 100644 --- a/query/functions.mdx +++ b/query/functions.mdx @@ -54,6 +54,43 @@ RETURN keys(n), properties(n) RETURN toIntegerOrNull('x'), toString(datetime('2024-03-15T10:30:00Z')) ``` +### String functions + +Positions are 0-based, and `substring` without a length runs to the end of the string: + +```cypher +RETURN substring('hello', 1, 3) AS s, substring('hello', 2) AS rest + +RETURN left('hello', 2) AS l, right('hello', 3) AS r + +RETURN split('a.b.c', '.') AS parts, replace('banana', 'a', 'o') AS swapped + +RETURN char_length('héllo') AS chars, size('héllo') AS alsoChars + +MATCH (n:Person) +RETURN toUpper(left(n.name, 1)) + toLower(substring(n.name, 1)) AS display +``` + +`normalize` converts a string to a Unicode normal form (`NFC` by default, or `NFD`, `NFKC`, `NFKD`), and `IS NORMALIZED` tests for one: + +```cypher +RETURN normalize('fi', NFKC) AS plain, 'café' IS NFC NORMALIZED AS composed +``` + +### Conversions + +The strict conversions return `null` for a string they cannot read, and reject other values they cannot convert. The `OrNull` forms return `null` instead of rejecting: + +```cypher +RETURN toInteger('12') AS i, toInteger('x') AS bad, toInteger(true) AS fromBool, toBoolean(1) AS b + +RETURN toIntegerOrNull('12') AS a, toFloatOrNull('2.5') AS c, toBooleanOrNull('yes') AS d, toStringOrNull(3.5) AS e + +RETURN toString(duration({days: 2, hours: 3})) AS iso +``` + +`toString` writes a `Duration` in ISO 8601 form, so the last query returns `PT51H`. + In v3, `type(e)` returns an edge's type as a string. It replaces `edgeType()` from earlier versions. @@ -142,7 +179,8 @@ MATCH (n:Person {name: 'Alice'}) RETURN properties(n).name, keys(n) ``` -- `m.key` reads a key written in the query, and `m[k]` a key held in a variable. A map stored as a property reads the same way: `n.meta.level`. +- `m.key` reads a key written in the query, and `m[k]` a key held in a variable. A map stored as a property reads the same way: `n.meta.level`, `keys(n.meta)`. +- Keys can be any word, keywords included: `WITH {count: 3, order: 'x'} AS m RETURN m.count, m.order`. - Write a fixed key as `m.key`: the bracket form with a literal (`m['key']`) is reported with that hint. - Map projection (`n {.name, .age}`) is not yet supported. Build the map you need (`{name: n.name, age: n.age}`) or use `properties(n)`. - Values read from a map can hold any type. To store one as a property, convert it first, for example `toString(row.name)` or `toInteger(row.pop)` (see [Bulk writes](/query/writing#bulk-writes)). diff --git a/query/matching.mdx b/query/matching.mdx index da71321..6a70641 100644 --- a/query/matching.mdx +++ b/query/matching.mdx @@ -92,9 +92,20 @@ RETURN n.name MATCH (a)-[r:!KNOWS]->(b) RETURN type(r), count(*) + +MATCH (p:Person) +OPTIONAL MATCH (p)-[:WORKS_AT]->(c:Company|Interest) +RETURN p.name, c.name + +MATCH (n) +WHERE NOT n:Person|Company +RETURN n.name ``` -The same expressions work as tests in `WHERE`: `WHERE n:Person|Company`. A label or an edge type can also come from a [query parameter](/query/cypher_subset#query-parameters): `MATCH (n:$label)`. +- Label expressions work in `MATCH` and `OPTIONAL MATCH` patterns and as tests in `WHERE` (`WHERE n:Person|Company`). +- For edges, `[:KNOWS|WORKS_AT]` also works on variable-length hops: `-[:KNOWS|LIKES]->+`. +- A label or an edge type can come from a [query parameter](/query/cypher_subset#query-parameters), on its own or inside an expression: `MATCH (n:Person|$other)`. +- In `CREATE` and `MERGE`, labels are joined with `:` or `&` only, since a new node needs definite labels. ### Filtering inside the pattern diff --git a/query/paths.mdx b/query/paths.mdx index dcddc35..66c7670 100644 --- a/query/paths.mdx +++ b/query/paths.mdx @@ -41,7 +41,7 @@ MATCH (a {name: 'Alice'})-[e:KNOWS]->+(b) RETURN b.name, [r IN e | r.since] AS sinces ``` -- **Edge type alternatives** (`[:KNOWS|WORKS_AT]`) work on variable-length hops. +- **Edge type alternatives** (`[:KNOWS|WORKS_AT]`) work on variable-length hops, and so does either direction written with both arrow heads: `(a)<-[:KNOWS]->{1,2}(b)`. - **A predicate or property map applies to every hop.** `-[e:KNOWS WHERE e.since > 2012]->+` only follows edges that satisfy it, and `-[:KNOWS*1..3 {since: 2010}]->` requires every edge on the path to have that property value. - **Trail semantics.** Each edge is used at most once within a path, while nodes may repeat. Each distinct path is its own row, so use `DISTINCT` or an aggregate to collapse them. - **One quantifier per edge.** Choose either `[*1..3]` or `{1,3}` for an edge. @@ -69,7 +69,19 @@ MATCH (a:Person {name: 'Alice'}) ((x)-[r:KNOWS]->(y)){1,3} (b) RETURN b.name, [n IN y | n.name] AS via, size(r) AS hops ``` -The second query follows `KNOWS` edges only while ages strictly increase, and the third repeats a two-hop sub-pattern. The inner variables (`x`, `y`, `r`) are **group variables**: each becomes a list with one element per repetition, which you can use in comprehensions, `size()` and `UNWIND`. +The second query follows `KNOWS` edges only while ages strictly increase, and the third repeats a two-hop sub-pattern. The inner variables (`x`, `y`, `r`) are **group variables**: each becomes a list with one element per repetition, which you can use in comprehensions, `size()`, `UNWIND` and sort keys: + +```cypher +MATCH (a:Person {name: 'Alice'}) ((x)-[r:KNOWS]->(y)){1,3} (b:Person {name: 'Dave'}) +UNWIND r AS e +RETURN e.since + +MATCH (a:Person {name: 'Alice'}) ((x)-[r:KNOWS]->(y)){1,3} (b) +RETURN b.name +ORDER BY size(r) DESC LIMIT 1 +``` + +The `WHERE` inside the parentheses can test the edge and both nodes of each repetition: `((x)-[r:KNOWS]->(y) WHERE r.since >= 2010 AND y.age > 25){2}`. ## Named paths diff --git a/query/subqueries.mdx b/query/subqueries.mdx index 9fb95d5..e83a9fb 100644 --- a/query/subqueries.mdx +++ b/query/subqueries.mdx @@ -28,8 +28,20 @@ RETURN p.name MATCH (p:Person) RETURN p.name, EXISTS { (p)-[:LIKES]->() } AS likesSomething + +MATCH (p:Person) +WHERE EXISTS { + MATCH (p)-[:LIKES]->(x) + RETURN x + UNION + MATCH (p)-[:WORKS_AT]->(x:Company {name: 'Globex'}) + RETURN x +} +RETURN p.name ``` +The full form can hold a `UNION`, as in the last example, or a [`NEXT`](/query/composition#next) chain. + ## COUNT `COUNT { ... }` returns the number of matches for each row: @@ -45,21 +57,88 @@ WHERE COUNT { RETURN x } >= 2 RETURN p.name + +MATCH (p:Person) +RETURN p.name, COUNT { + MATCH (p)-[:KNOWS]->(x) + RETURN x + UNION + MATCH (p)-[:LIKES]->(x) + RETURN x +} AS links ``` +Like `EXISTS`, the body of `COUNT` can hold a `UNION` or a [`NEXT`](/query/composition#next) chain. + ## COLLECT -`COLLECT { ... }` returns the single column of its body as a list, for each row: +`COLLECT { ... }` runs a query for each row and returns its single column as a list. The list is empty when the query finds nothing: ```cypher MATCH (p:Person) RETURN p.name, COLLECT { MATCH (p)-[:KNOWS]->(f) RETURN f.name } AS friends +``` + +The body is a full query, so it can filter, deduplicate, sort and limit: + +```cypher +MATCH (p:Person) +RETURN p.name, COLLECT { + MATCH (p)-[:KNOWS]->(f) + WHERE f.age > 30 + RETURN f.name +} AS olderFriends + +MATCH (c:Company) +RETURN c.name, COLLECT { + MATCH (p:Person)-[:WORKS_AT]->(c) + RETURN DISTINCT p.city +} AS cities + +MATCH (c:Company) +RETURN c.name, COLLECT { + MATCH (p:Person)-[:WORKS_AT]->(c) + RETURN p.name + ORDER BY p.age DESC LIMIT 1 +} AS oldestEmployee +``` +It can also combine queries with `UNION`, or collect nodes to use later: + +```cypher +MATCH (p:Person) +RETURN p.name, COLLECT { + MATCH (p)-[:KNOWS]->(f) + RETURN f.name AS link + UNION + MATCH (p)-[:WORKS_AT]->(c) + RETURN c.name AS link +} AS links + +MATCH (p:Person {name: 'Alice'}) +WITH COLLECT { MATCH (p)-[:KNOWS]->+(f) RETURN f } AS reachable +RETURN [x IN reachable | x.name] AS names +``` + +A `COLLECT` is an ordinary list value, so it works anywhere a list does: in `WHERE`, `ORDER BY`, `UNWIND` or `SET`. + +```cypher MATCH (p:Person) WHERE size(COLLECT { MATCH (p)-[:KNOWS]-(f) RETURN f.name }) >= 2 RETURN p.name + +MATCH (p:Person {name: 'Bob'}) +UNWIND COLLECT { MATCH (p)-[:KNOWS]-(f) RETURN f.name } AS friend +RETURN friend + +MATCH (p:Person {name: 'Alice'}) +SET p.friendNames = COLLECT { MATCH (p)-[:KNOWS]->(f) RETURN f.name } ``` +- **One column.** The body returns exactly one column. Two columns are reported: `A COLLECT subquery must return exactly one column, not 2`. To collect several values per match, use a [pattern comprehension](/query/aggregation#list-comprehensions) or `collect()` in a `CALL` body. +- **A full query.** The body is `MATCH ... RETURN ...`, not a bare pattern. +- **Not yet available:** collecting map values, and a `NEXT` chain or `WHEN` inside the body. + ## CALL subqueries A `CALL { ... }` subquery runs once per incoming row. It is the way to compute a per-row top-k or a per-row aggregate. Import the outer variables either in the `CALL (p)` header or with a `WITH p` as the first clause of the body: @@ -106,10 +185,29 @@ RETURN nm ORDER BY nm ``` -- **Importing variables.** `CALL (p)` imports the listed variables, and `CALL (*)` imports every variable in scope. +- **Importing variables.** `CALL (p)` imports the listed variables, and `CALL (*)` imports every variable in scope, including values computed in an earlier `WITH`. - **Dropped rows.** A non-optional `CALL` drops the outer rows for which its body returns nothing. Use `OPTIONAL CALL` to keep them with `null` values. - **Returning an imported variable.** Give it an alias in the `CALL` body: `RETURN p AS me`. -- A `CALL` body can contain `UNION`, aggregation, `ORDER BY` and `LIMIT`, or be a [conditional query](/query/aggregation#conditional-queries-with-when). Write-only subqueries also work inside a [change](/query/writing). +- A `CALL` body can contain `UNION`, aggregation, `ORDER BY`, `LIMIT` and a [`NEXT`](/query/composition#next) chain, or be a [conditional query](/query/composition#when). +- **Writing.** Inside a [change](/query/writing), a `CALL` body can write and return what it wrote, and the branches of a `UNION` can mix `CREATE` and `MERGE`: + +```cypher +UNWIND ['x', 'y', 'x'] AS k +CALL (k) { + MERGE (t:Tag {name: k}) + RETURN t AS tag +} +RETURN tag.name + +CALL { + CREATE (a:Tag {name: 'p'}) + RETURN a AS t + UNION + MERGE (b:Tag {name: 'q'}) + RETURN b AS t +} +RETURN t.name +``` `CALL { } IN TRANSACTIONS`, `OPTIONAL CALL` on a procedure, and `WHEN` inside an `EXISTS`, `COUNT` or `COLLECT` body are not yet available. diff --git a/query/writing.mdx b/query/writing.mdx index 7bf7926..b973828 100644 --- a/query/writing.mdx +++ b/query/writing.mdx @@ -232,7 +232,7 @@ DELETE e, n ## FOREACH -`FOREACH` runs write clauses once for each element of a list, without producing rows: +`FOREACH` runs write clauses once for each element of a list. It does not change the rows of the query: ```cypher FOREACH (i IN [1, 2, 3] | CREATE (:Num {v: i})) @@ -241,16 +241,40 @@ FOREACH (nm IN ['x', 'y', 'x'] | MERGE (:Tag {name: nm})) MATCH (n:Person) WITH collect(n) AS people -FOREACH (p IN people | SET p.reviewed = true) +FOREACH (p IN people | SET p.reviewed = true, p.nextAge = p.age + 1) FOREACH (i IN [1, 2] | FOREACH (j IN [10, 20] | CREATE (:Cell {v: i * j}))) ``` -The second example creates two `Tag` nodes, since `MERGE` deduplicates. `FOREACH` blocks nest. +The second example creates two `Tag` nodes, since `MERGE` deduplicates, and the last shows that `FOREACH` blocks nest. + +The clauses inside can use the variables of the row, so `FOREACH` can hang new nodes off a matched one, or run several clauses per element: + +```cypher +MATCH (a:Person {name: 'Alice'}) +FOREACH (i IN range(1, 3) | CREATE (a)-[:OWNS]->(:Item {n: i})) + +MATCH (n:Person) +FOREACH (t IN n.tags | + MERGE (tag:TagNode {name: toString(t)}) + CREATE (n)-[:TAGGED]->(tag) +) +``` + +Elements of a list property such as `n.tags` can hold any type, so convert them as you store them (`toString(t)`), as with [map and mixed rows](#small-volumes). + +**Writing only when a condition holds.** Loop over a list that is empty when the condition fails: + +```cypher +MATCH (n:Person) +FOREACH (x IN CASE WHEN n.age > 35 THEN [1] ELSE [] END | SET n.senior = true) +``` + +A `RETURN` can follow, since `FOREACH` keeps the rows: `MATCH (n:Person) FOREACH (x IN [1] | SET n.seen = true) RETURN count(*)`. ### Conditional writes -The branches of a [conditional query](/query/aggregation#conditional-queries-with-when) can write: +The branches of a [conditional query](/query/composition#when) can write: ```cypher MATCH (n:Person) @@ -304,7 +328,7 @@ UNWIND [['Brest', 'FR-29'], ['Rennes', 'FR-35']] AS row CREATE (:City {name: row[0], code: row[1]}) ``` -Rows can also be maps, or lists of mixed types. Their values can hold any type, so convert each one as you store it: +Rows can also be maps, or lists of mixed types. Their values, like the elements of a list property, can hold any type, so convert each one as you store it: ```cypher UNWIND [{name: 'Brest', pop: 139000}, {name: 'Rennes', pop: 220000}] AS row