diff --git a/.changeset/purple-crews-pretend.md b/.changeset/purple-crews-pretend.md new file mode 100644 index 00000000..327e9f91 --- /dev/null +++ b/.changeset/purple-crews-pretend.md @@ -0,0 +1,27 @@ +--- +"@neo4j/cypher-builder": minor +--- + +Add support for Collect subqueries: + +```js +const dog = new Cypher.Node({ labels: ["Dog"] }); +const person = new Cypher.Node({ labels: ["Person"] }); + +const subquery = new Cypher.Match( + new Cypher.Pattern(person).related(new Cypher.Relationship({ type: "HAS_DOG" })).to(dog) +).return(dog.property("name")); + +const match = new Cypher.Match(person) + .where(Cypher.in(new Cypher.Literal("Ozzy"), new Cypher.Collect(subquery))) + .return(person); +``` + +```cypher +MATCH (this0:Person) +WHERE "Ozzy" IN COLLECT { + MATCH (this0:Person)-[this1:HAS_DOG]->(this2:Dog) + RETURN this2.name +} +RETURN this0 +``` diff --git a/src/Cypher.ts b/src/Cypher.ts index 2160c90f..99705d5a 100644 --- a/src/Cypher.ts +++ b/src/Cypher.ts @@ -48,6 +48,9 @@ export { NamedVariable, Variable } from "./references/Variable"; // Expressions export { Case } from "./expressions/Case"; export { CypherTypes as TYPE, isNotType, isType } from "./expressions/IsType"; + +// Subquery Expressions +export { Collect } from "./expressions/subquery/Collect"; export { Count } from "./expressions/subquery/Count"; export { Exists } from "./expressions/subquery/Exists"; diff --git a/src/expressions/subquery/Collect.test.ts b/src/expressions/subquery/Collect.test.ts new file mode 100644 index 00000000..0d080199 --- /dev/null +++ b/src/expressions/subquery/Collect.test.ts @@ -0,0 +1,80 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Cypher from "../.."; + +describe("Collect Subquery", () => { + test("Collect expression with subclause", () => { + const dog = new Cypher.Node({ labels: ["Dog"] }); + const person = new Cypher.Node({ labels: ["Person"] }); + + const subquery = new Cypher.Match( + new Cypher.Pattern(person).related(new Cypher.Relationship({ type: "HAS_DOG" })).to(dog) + ).return(dog.property("name")); + + const match = new Cypher.Match(person) + .where(Cypher.in(new Cypher.Literal("Ozzy"), new Cypher.Collect(subquery))) + .return(person); + + const queryResult = match.build(); + + expect(queryResult.cypher).toMatchInlineSnapshot(` +"MATCH (this0:Person) +WHERE \\"Ozzy\\" IN COLLECT { + MATCH (this0:Person)-[this1:HAS_DOG]->(this2:Dog) + RETURN this2.name +} +RETURN this0" +`); + + expect(queryResult.params).toMatchInlineSnapshot(`{}`); + }); + + test("Return collect subquery with an union", () => { + const dog = new Cypher.Node({ labels: ["Dog"] }); + const cat = new Cypher.Node({ labels: ["Cat"] }); + const person = new Cypher.Node({ labels: ["Person"] }); + + const matchDog = new Cypher.Match( + new Cypher.Pattern(person).related(new Cypher.Relationship({ type: "HAS_DOG" })).to(dog) + ).return([dog.property("name"), "petName"]); + const matchCat = new Cypher.Match( + new Cypher.Pattern(person).related(new Cypher.Relationship({ type: "HAS_CAT" })).to(cat) + ).return([cat.property("name"), "petName"]); + + const subquery = new Cypher.Union(matchDog, matchCat); + + const match = new Cypher.Match(person).return(person, [new Cypher.Collect(subquery), "petNames"]); + + const queryResult = match.build(); + + expect(queryResult.cypher).toMatchInlineSnapshot(` +"MATCH (this0:Person) +RETURN this0, COLLECT { + MATCH (this0:Person)-[this1:HAS_DOG]->(this2:Dog) + RETURN this2.name AS petName + UNION + MATCH (this0:Person)-[this3:HAS_CAT]->(this4:Cat) + RETURN this4.name AS petName +} AS petNames" +`); + + expect(queryResult.params).toMatchInlineSnapshot(`{}`); + }); +}); diff --git a/src/expressions/subquery/Collect.ts b/src/expressions/subquery/Collect.ts new file mode 100644 index 00000000..d19e755c --- /dev/null +++ b/src/expressions/subquery/Collect.ts @@ -0,0 +1,37 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { CypherEnvironment } from "../../Environment"; +import { padBlock } from "../../utils/pad-block"; +import { Subquery } from "./Subquery"; + +/** + * @see [Cypher Documentation](https://neo4j.com/docs/cypher-manual/current/subqueries/collect/) + * @group Other + */ +export class Collect extends Subquery { + /** + * @internal + */ + public getCypher(env: CypherEnvironment): string { + const subQueryStr = this.subquery.getCypher(env); + const paddedSubQuery = padBlock(subQueryStr); + return `COLLECT {\n${paddedSubQuery}\n}`; + } +} diff --git a/src/expressions/subquery/Count.ts b/src/expressions/subquery/Count.ts index 5912125e..3411058f 100644 --- a/src/expressions/subquery/Count.ts +++ b/src/expressions/subquery/Count.ts @@ -18,29 +18,19 @@ */ import type { CypherEnvironment } from "../../Environment"; -import type { Clause } from "../../clauses/Clause"; -import { CypherASTNode } from "../../CypherASTNode"; import { padBlock } from "../../utils/pad-block"; +import { Subquery } from "./Subquery"; /** COUNT subquery expression * @see [Cypher Documentation](https://neo4j.com/docs/cypher-manual/current/syntax/expressions/#count-subqueries) * @group Other */ -export class Count extends CypherASTNode { - private subQuery: CypherASTNode; - - constructor(subQuery: Clause) { - super(); - const rootQuery = subQuery.getRoot(); - this.addChildren(rootQuery); - this.subQuery = rootQuery; - } - +export class Count extends Subquery { /** * @internal */ public getCypher(env: CypherEnvironment): string { - const subQueryStr = this.subQuery.getCypher(env); + const subQueryStr = this.subquery.getCypher(env); const paddedSubQuery = padBlock(subQueryStr); return `COUNT {\n${paddedSubQuery}\n}`; } diff --git a/src/expressions/subquery/Exists.ts b/src/expressions/subquery/Exists.ts index eae7077a..3e214ced 100644 --- a/src/expressions/subquery/Exists.ts +++ b/src/expressions/subquery/Exists.ts @@ -18,29 +18,19 @@ */ import type { CypherEnvironment } from "../../Environment"; -import type { Clause } from "../../clauses/Clause"; -import { CypherASTNode } from "../../CypherASTNode"; import { padBlock } from "../../utils/pad-block"; +import { Subquery } from "./Subquery"; /** * @see [Cypher Documentation](https://neo4j.com/docs/cypher-manual/current/syntax/expressions/#existential-subqueries) * @group Other */ -export class Exists extends CypherASTNode { - private subQuery: CypherASTNode; - - constructor(subQuery: Clause) { - super(); - const rootQuery = subQuery.getRoot(); - this.addChildren(rootQuery); - this.subQuery = rootQuery; - } - +export class Exists extends Subquery { /** * @internal */ public getCypher(env: CypherEnvironment): string { - const subQueryStr = this.subQuery.getCypher(env); + const subQueryStr = this.subquery.getCypher(env); const paddedSubQuery = padBlock(subQueryStr); return `EXISTS {\n${paddedSubQuery}\n}`; } diff --git a/src/expressions/subquery/Subquery.ts b/src/expressions/subquery/Subquery.ts new file mode 100644 index 00000000..a7826a1d --- /dev/null +++ b/src/expressions/subquery/Subquery.ts @@ -0,0 +1,32 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { CypherASTNode } from "../../CypherASTNode"; +import type { Clause } from "../../clauses/Clause"; + +export abstract class Subquery extends CypherASTNode { + protected subquery: CypherASTNode; + + constructor(subquery: Clause) { + super(); + const rootQuery = subquery.getRoot(); + this.addChildren(rootQuery); + this.subquery = rootQuery; + } +} diff --git a/src/types.ts b/src/types.ts index 28b52f04..816e4878 100644 --- a/src/types.ts +++ b/src/types.ts @@ -33,6 +33,7 @@ import type { MapProjection } from "./expressions/map/MapProjection"; import type { BooleanOp } from "./expressions/operations/boolean"; import type { ComparisonOp } from "./expressions/operations/comparison"; import type { MathOp } from "./expressions/operations/math"; +import type { Collect } from "./expressions/subquery/Collect"; import type { Count } from "./expressions/subquery/Count"; import type { Exists } from "./expressions/subquery/Exists"; import type { Literal } from "./references/Literal"; @@ -57,7 +58,8 @@ export type Expr = | MapProjection // NOTE this cannot be set as a property in a node | ListExpr | ListIndex - | Case; + | Case + | Collect; /** Represents a predicate statement (i.e returns a boolean). Note that Raw is only added for compatibility */ export type Predicate =