Skip to content

Commit

Permalink
Add isType, isNotType and TYPES for type predicate expressions
Browse files Browse the repository at this point in the history
  • Loading branch information
angrykoala committed Jan 18, 2024
1 parent 3889edd commit f97c229
Show file tree
Hide file tree
Showing 6 changed files with 379 additions and 1 deletion.
18 changes: 18 additions & 0 deletions .changeset/purple-queens-warn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
"@neo4j/cypher-builder": minor
---

Add support for type predicate expressions with the functions `Cypher.isType` and `Cypher.isNotType`:

```ts
const variable = new Cypher.Variable();
const unwindClause = new Cypher.Unwind([new Cypher.Literal([42, true, "abc", null]), variable]).return(
variable,
Cypher.isType(variable, Cypher.TYPE.INTEGER)
);
```

```cypher
UNWIND [42, true, \\"abc\\", NULL] AS var0
RETURN var0, var0 IS :: INTEGER
```
1 change: 1 addition & 0 deletions src/Cypher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export { NamedVariable, Variable } from "./references/Variable";

// Expressions
export { Case } from "./expressions/Case";
export { CypherTypes as TYPE, isNotType, isType } from "./expressions/IsType";
export { Count } from "./expressions/subquery/Count";
export { Exists } from "./expressions/subquery/Exists";

Expand Down
191 changes: 191 additions & 0 deletions src/expressions/IsType.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
/*
* 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("IsType", () => {
test("UNWIND return isType", () => {
const variable = new Cypher.Variable();
const unwindClause = new Cypher.Unwind([new Cypher.Literal([42, true, "abc", null]), variable]).return(
variable,
Cypher.isType(variable, Cypher.TYPE.INTEGER)
);

const { cypher, params } = unwindClause.build();

expect(cypher).toMatchInlineSnapshot(`
"UNWIND [42, true, \\"abc\\", NULL] AS var0
RETURN var0, var0 IS :: INTEGER"
`);
expect(params).toMatchInlineSnapshot(`{}`);
});

test.each([
Cypher.TYPE.ANY,
Cypher.TYPE.BOOLEAN,
Cypher.TYPE.DATE,
Cypher.TYPE.DURATION,
Cypher.TYPE.FLOAT,
Cypher.TYPE.INTEGER,
Cypher.TYPE.LOCAL_DATETIME,
Cypher.TYPE.LOCAL_TIME,
Cypher.TYPE.MAP,
Cypher.TYPE.NODE,
Cypher.TYPE.NOTHING,
Cypher.TYPE.NULL,
Cypher.TYPE.PATH,
Cypher.TYPE.POINT,
Cypher.TYPE.PROPERTY_VALUE,
Cypher.TYPE.RELATIONSHIP,
Cypher.TYPE.STRING,
Cypher.TYPE.ZONED_DATETIME,
Cypher.TYPE.ZONED_TIME,
] as const)("isType '%s'", (type) => {
const movie = new Cypher.Node({ labels: ["Movie"] });
const matchClause = new Cypher.Match(movie).where(Cypher.isType(movie.property("title"), type)).return(movie);

const { cypher } = matchClause.build();

expect(cypher).toEqual(`MATCH (this0:Movie)
WHERE this0.title IS :: ${type}
RETURN this0`);
});

test("isType 'List<STRING>'", () => {
const movie = new Cypher.Node({ labels: ["Movie"] });
const matchClause = new Cypher.Match(movie)
.where(Cypher.isType(movie.property("title"), Cypher.TYPE.list(Cypher.TYPE.STRING)))
.return(movie);

const { cypher } = matchClause.build();

expect(cypher).toMatchInlineSnapshot(`
"MATCH (this0:Movie)
WHERE this0.title IS :: LIST<STRING>
RETURN this0"
`);
});

test("isType 'List<List<STRING>>'", () => {
const movie = new Cypher.Node({ labels: ["Movie"] });
const matchClause = new Cypher.Match(movie)
.where(Cypher.isType(movie.property("title"), Cypher.TYPE.list(Cypher.TYPE.list(Cypher.TYPE.STRING))))
.return(movie);

const { cypher } = matchClause.build();

expect(cypher).toMatchInlineSnapshot(`
"MATCH (this0:Movie)
WHERE this0.title IS :: LIST<LIST<STRING>>
RETURN this0"
`);
});

test("isType with union type", () => {
const movie = new Cypher.Node({ labels: ["Movie"] });
const matchClause = new Cypher.Match(movie)
.where(Cypher.isType(movie.property("title"), Cypher.TYPE.list(Cypher.TYPE.STRING), Cypher.TYPE.STRING))
.return(movie);

const { cypher } = matchClause.build();

expect(cypher).toMatchInlineSnapshot(`
"MATCH (this0:Movie)
WHERE this0.title IS :: LIST<STRING> | STRING
RETURN this0"
`);
});

test("isType in NOT", () => {
const movie = new Cypher.Node({ labels: ["Movie"] });
const matchClause = new Cypher.Match(movie)
.where(Cypher.not(Cypher.isType(movie.property("title"), Cypher.TYPE.STRING)))
.return(movie);

const { cypher } = matchClause.build();

expect(cypher).toMatchInlineSnapshot(`
"MATCH (this0:Movie)
WHERE NOT (this0.title IS :: STRING)
RETURN this0"
`);
});

test("isNotType", () => {
const movie = new Cypher.Node({ labels: ["Movie"] });
const matchClause = new Cypher.Match(movie)
.where(Cypher.isNotType(movie.property("title"), Cypher.TYPE.STRING))
.return(movie);

const { cypher } = matchClause.build();

expect(cypher).toMatchInlineSnapshot(`
"MATCH (this0:Movie)
WHERE this0.title IS NOT :: STRING
RETURN this0"
`);
});

describe("notNull", () => {
test("isType.notNull", () => {
const movie = new Cypher.Node({ labels: ["Movie"] });
const matchClause = new Cypher.Match(movie)
.where(Cypher.isType(movie.property("title"), Cypher.TYPE.STRING).notNull())
.return(movie);

const { cypher } = matchClause.build();

expect(cypher).toMatchInlineSnapshot(`
"MATCH (this0:Movie)
WHERE this0.title IS :: STRING NOT NULL
RETURN this0"
`);
});

test("isNotType.notNull", () => {
const movie = new Cypher.Node({ labels: ["Movie"] });
const matchClause = new Cypher.Match(movie)
.where(Cypher.isNotType(movie.property("title"), Cypher.TYPE.STRING).notNull())
.return(movie);

const { cypher } = matchClause.build();

expect(cypher).toMatchInlineSnapshot(`
"MATCH (this0:Movie)
WHERE this0.title IS NOT :: STRING NOT NULL
RETURN this0"
`);
});

test("isType.notNull with union type", () => {
const movie = new Cypher.Node({ labels: ["Movie"] });
const matchClause = new Cypher.Match(movie)
.where(Cypher.isType(movie.property("title"), Cypher.TYPE.STRING, Cypher.TYPE.BOOLEAN).notNull())
.return(movie);

const { cypher } = matchClause.build();

expect(cypher).toMatchInlineSnapshot(`
"MATCH (this0:Movie)
WHERE this0.title IS :: STRING NOT NULL | BOOLEAN NOT NULL
RETURN this0"
`);
});
});
});
146 changes: 146 additions & 0 deletions src/expressions/IsType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
/*
* 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 Cypher from "..";
import { CypherASTNode } from "../CypherASTNode";
import type { ValueOf } from "../utils/type-helpers";

const BaseTypes = {
ANY: "ANY",
BOOLEAN: "BOOLEAN",
DATE: "DATE",
DURATION: "DURATION",
FLOAT: "FLOAT",
INTEGER: "INTEGER",
LOCAL_DATETIME: "LOCAL DATETIME",
LOCAL_TIME: "LOCAL_TIME",
MAP: "MAP",
NODE: "NODE",
NOTHING: "NOTHING",
NULL: "NULL",
PATH: "PATH",
POINT: "POINT",
PROPERTY_VALUE: "PROPERTY VALUE",
RELATIONSHIP: "RELATIONSHIP",
STRING: "STRING",
ZONED_DATETIME: "ZONED DATETIME",
ZONED_TIME: "ZONED TIME",
} as const;

/**
* Generates a cypher LIST<...> type
* @example
* ```cypher
* LIST<STRING>
* ```
*/
function list(type: Type): ListType {
return new ListType(type);
}

/**
* Types supported by Neo4j
* @see [Cypher Documentation](https://neo4j.com/docs/cypher-manual/current/values-and-types/property-structural-constructed/#types-synonyms)
*/
export const CypherTypes = {
...BaseTypes,
list,
} as const;

/**
* Type predicate expression
* @see [Cypher Documentation](https://neo4j.com/docs/cypher-manual/current/values-and-types/type-predicate/)
* @example
* ```cypher
* val IS :: INTEGER
* ```
*/
export function isType(expr: Cypher.Expr, ...type: Type[]): IsType {
return new IsType(expr, type);
}

/**
* Type predicate expression with NOT
* @see [Cypher Documentation](https://neo4j.com/docs/cypher-manual/current/values-and-types/type-predicate/#type-predicate-not)
* @example
* ```cypher
* val IS NOT :: INTEGER
* ```
*/
export function isNotType(expr: Cypher.Expr, ...type: Type[]): IsType {
return new IsType(expr, type, true);
}

class ListType {
private type: Type;

constructor(type: Type) {
this.type = type;
}

public getCypher(env: Cypher.Environment): string {
const typeStr = compileType(this.type, env);

return `LIST<${typeStr}>`;
}
}

export class IsType extends CypherASTNode {
private expr: Cypher.Expr;
private type: Type[];
private not: boolean;
private _notNull: boolean = false;

public constructor(expr: Cypher.Expr, type: Type[], not = false) {
super();
this.expr = expr;
this.type = type;
this.not = not;
}

public notNull(): this {
this._notNull = true;
return this;
}

public getCypher(env: Cypher.Environment): string {
const exprCypher = env.compile(this.expr);
const isStr = this.not ? "IS NOT" : "IS";

// Note that all types must be nullable or non nullable
const notNullStr = this._notNull ? " NOT NULL" : "";
const typesStr = this.type.map((type) => {
const typeStr = compileType(type, env);

return `${typeStr}${notNullStr}`;
});

return `${exprCypher} ${isStr} :: ${Array.from(typesStr).join(" | ")}`;
}
}

type Type = ValueOf<typeof BaseTypes> | ListType;

function compileType(type: Type, env: Cypher.Environment): string {
if (type instanceof ListType) {
return env.compile(type);
} else {
return type;
}
}
4 changes: 3 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import type { Raw, RawCypher } from ".";
import type { CypherEnvironment } from "./Environment";
import type { Case } from "./expressions/Case";
import type { HasLabel } from "./expressions/HasLabel";
import type { IsType } from "./expressions/IsType";
import type { CypherFunction } from "./expressions/functions/CypherFunctions";
import type { PredicateFunction } from "./expressions/functions/predicate";
import type { ListComprehension } from "./expressions/list/ListComprehension";
Expand Down Expand Up @@ -69,7 +70,8 @@ export type Predicate =
| PredicateFunction
| Literal<boolean>
| Case
| HasLabel;
| HasLabel
| IsType;

export type CypherResult = {
cypher: string;
Expand Down
Loading

0 comments on commit f97c229

Please sign in to comment.