Skip to content

Commit

Permalink
Merge pull request #301 from neo4j/208-collect-subqueries
Browse files Browse the repository at this point in the history
Add Collect subqueries
  • Loading branch information
angrykoala committed Feb 19, 2024
2 parents d9fae06 + b50af62 commit 5df1d34
Show file tree
Hide file tree
Showing 8 changed files with 188 additions and 27 deletions.
27 changes: 27 additions & 0 deletions .changeset/purple-crews-pretend.md
Original file line number Diff line number Diff line change
@@ -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
```
3 changes: 3 additions & 0 deletions src/Cypher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down
80 changes: 80 additions & 0 deletions src/expressions/subquery/Collect.test.ts
Original file line number Diff line number Diff line change
@@ -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(`{}`);
});
});
37 changes: 37 additions & 0 deletions src/expressions/subquery/Collect.ts
Original file line number Diff line number Diff line change
@@ -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}`;
}
}
16 changes: 3 additions & 13 deletions src/expressions/subquery/Count.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`;
}
Expand Down
16 changes: 3 additions & 13 deletions src/expressions/subquery/Exists.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`;
}
Expand Down
32 changes: 32 additions & 0 deletions src/expressions/subquery/Subquery.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
4 changes: 3 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -57,7 +58,8 @@ export type Expr =
| MapProjection // NOTE this cannot be set as a property in a node
| ListExpr
| ListIndex
| Case<ComparisonOp>;
| Case<ComparisonOp>
| Collect;

/** Represents a predicate statement (i.e returns a boolean). Note that Raw is only added for compatibility */
export type Predicate =
Expand Down

0 comments on commit 5df1d34

Please sign in to comment.