flowquery 1.0.39 → 1.0.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/flowquery.min.js +1 -1
- package/dist/graph/database.d.ts +2 -0
- package/dist/graph/database.d.ts.map +1 -1
- package/dist/graph/database.js +12 -0
- package/dist/graph/database.js.map +1 -1
- package/dist/parsing/functions/function_factory.d.ts +1 -0
- package/dist/parsing/functions/function_factory.d.ts.map +1 -1
- package/dist/parsing/functions/function_factory.js +1 -0
- package/dist/parsing/functions/function_factory.js.map +1 -1
- package/dist/parsing/functions/substring.d.ts +9 -0
- package/dist/parsing/functions/substring.d.ts.map +1 -0
- package/dist/parsing/functions/substring.js +62 -0
- package/dist/parsing/functions/substring.js.map +1 -0
- package/dist/parsing/operations/delete_node.d.ts +11 -0
- package/dist/parsing/operations/delete_node.d.ts.map +1 -0
- package/dist/parsing/operations/delete_node.js +46 -0
- package/dist/parsing/operations/delete_node.js.map +1 -0
- package/dist/parsing/operations/delete_relationship.d.ts +11 -0
- package/dist/parsing/operations/delete_relationship.d.ts.map +1 -0
- package/dist/parsing/operations/delete_relationship.js +46 -0
- package/dist/parsing/operations/delete_relationship.js.map +1 -0
- package/dist/parsing/parser.d.ts +8 -0
- package/dist/parsing/parser.d.ts.map +1 -1
- package/dist/parsing/parser.js +105 -31
- package/dist/parsing/parser.js.map +1 -1
- package/docs/flowquery.min.js +1 -1
- package/flowquery-py/pyproject.toml +1 -1
- package/flowquery-py/src/graph/database.py +12 -0
- package/flowquery-py/src/parsing/functions/__init__.py +2 -0
- package/flowquery-py/src/parsing/functions/substring.py +74 -0
- package/flowquery-py/src/parsing/operations/__init__.py +4 -0
- package/flowquery-py/src/parsing/operations/delete_node.py +29 -0
- package/flowquery-py/src/parsing/operations/delete_relationship.py +29 -0
- package/flowquery-py/src/parsing/parser.py +75 -10
- package/flowquery-py/tests/compute/test_runner.py +226 -1
- package/flowquery-vscode/flowQueryEngine/flowquery.min.js +1 -1
- package/package.json +1 -1
- package/src/graph/database.ts +12 -0
- package/src/parsing/functions/function_factory.ts +1 -0
- package/src/parsing/functions/substring.ts +65 -0
- package/src/parsing/operations/delete_node.ts +33 -0
- package/src/parsing/operations/delete_relationship.ts +32 -0
- package/src/parsing/parser.ts +110 -33
- package/tests/compute/runner.test.ts +194 -0
- package/tests/parsing/parser.test.ts +1 -1
package/package.json
CHANGED
package/src/graph/database.ts
CHANGED
|
@@ -25,6 +25,12 @@ class Database {
|
|
|
25
25
|
physical.statement = statement;
|
|
26
26
|
Database.nodes.set(node.label, physical);
|
|
27
27
|
}
|
|
28
|
+
public removeNode(node: Node): void {
|
|
29
|
+
if (node.label === null) {
|
|
30
|
+
throw new Error("Node label is null");
|
|
31
|
+
}
|
|
32
|
+
Database.nodes.delete(node.label);
|
|
33
|
+
}
|
|
28
34
|
public getNode(node: Node): PhysicalNode | null {
|
|
29
35
|
return Database.nodes.get(node.label!) || null;
|
|
30
36
|
}
|
|
@@ -38,6 +44,12 @@ class Database {
|
|
|
38
44
|
physical.target = relationship.target;
|
|
39
45
|
Database.relationships.set(relationship.type, physical);
|
|
40
46
|
}
|
|
47
|
+
public removeRelationship(relationship: Relationship): void {
|
|
48
|
+
if (relationship.type === null) {
|
|
49
|
+
throw new Error("Relationship type is null");
|
|
50
|
+
}
|
|
51
|
+
Database.relationships.delete(relationship.type);
|
|
52
|
+
}
|
|
41
53
|
public getRelationship(relationship: Relationship): PhysicalRelationship | null {
|
|
42
54
|
return Database.relationships.get(relationship.type!) || null;
|
|
43
55
|
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import ASTNode from "../ast_node";
|
|
2
|
+
import Function from "./function";
|
|
3
|
+
import { FunctionDef } from "./function_metadata";
|
|
4
|
+
|
|
5
|
+
@FunctionDef({
|
|
6
|
+
description:
|
|
7
|
+
"Returns a substring of a string, starting at a 0-based index with an optional length",
|
|
8
|
+
category: "scalar",
|
|
9
|
+
parameters: [
|
|
10
|
+
{ name: "original", description: "The original string", type: "string" },
|
|
11
|
+
{ name: "start", description: "The 0-based start index", type: "integer" },
|
|
12
|
+
{
|
|
13
|
+
name: "length",
|
|
14
|
+
description: "The length of the substring (optional, defaults to remainder of string)",
|
|
15
|
+
type: "integer",
|
|
16
|
+
},
|
|
17
|
+
],
|
|
18
|
+
output: { description: "The substring", type: "string", example: "llo" },
|
|
19
|
+
examples: ["RETURN substring('hello', 1, 3)", "RETURN substring('hello', 2)"],
|
|
20
|
+
})
|
|
21
|
+
class Substring extends Function {
|
|
22
|
+
constructor() {
|
|
23
|
+
super("substring");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
public set parameters(nodes: ASTNode[]) {
|
|
27
|
+
if (nodes.length < 2 || nodes.length > 3) {
|
|
28
|
+
throw new Error(
|
|
29
|
+
`Function substring expected 2 or 3 parameters, but got ${nodes.length}`
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
this.children = nodes;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
public value(): any {
|
|
36
|
+
const children = this.getChildren();
|
|
37
|
+
const original = children[0].value();
|
|
38
|
+
const start = children[1].value();
|
|
39
|
+
|
|
40
|
+
if (typeof original !== "string") {
|
|
41
|
+
throw new Error(
|
|
42
|
+
"Invalid argument for substring function: expected a string as the first argument"
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
if (typeof start !== "number" || !Number.isInteger(start)) {
|
|
46
|
+
throw new Error(
|
|
47
|
+
"Invalid argument for substring function: expected an integer as the second argument"
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (children.length === 3) {
|
|
52
|
+
const length = children[2].value();
|
|
53
|
+
if (typeof length !== "number" || !Number.isInteger(length)) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
"Invalid argument for substring function: expected an integer as the third argument"
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
return original.substring(start, start + length);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return original.substring(start);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export default Substring;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import Database from "../../graph/database";
|
|
2
|
+
import Node from "../../graph/node";
|
|
3
|
+
import Operation from "./operation";
|
|
4
|
+
|
|
5
|
+
class DeleteNode extends Operation {
|
|
6
|
+
private _node: Node | null = null;
|
|
7
|
+
constructor(node: Node) {
|
|
8
|
+
super();
|
|
9
|
+
this._node = node;
|
|
10
|
+
}
|
|
11
|
+
public get node(): Node | null {
|
|
12
|
+
return this._node;
|
|
13
|
+
}
|
|
14
|
+
public run(): Promise<void> {
|
|
15
|
+
return new Promise(async (resolve, reject) => {
|
|
16
|
+
try {
|
|
17
|
+
if (this._node === null) {
|
|
18
|
+
throw new Error("Node is null");
|
|
19
|
+
}
|
|
20
|
+
const db: Database = Database.getInstance();
|
|
21
|
+
db.removeNode(this._node);
|
|
22
|
+
resolve();
|
|
23
|
+
} catch (error) {
|
|
24
|
+
reject(error);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
public get results(): Record<string, any>[] {
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export default DeleteNode;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import Database from "../../graph/database";
|
|
2
|
+
import Relationship from "../../graph/relationship";
|
|
3
|
+
import Operation from "./operation";
|
|
4
|
+
|
|
5
|
+
class DeleteRelationship extends Operation {
|
|
6
|
+
private _relationship: Relationship | null = null;
|
|
7
|
+
constructor(relationship: Relationship) {
|
|
8
|
+
super();
|
|
9
|
+
this._relationship = relationship;
|
|
10
|
+
}
|
|
11
|
+
public get relationship(): Relationship | null {
|
|
12
|
+
return this._relationship;
|
|
13
|
+
}
|
|
14
|
+
public run(): Promise<void> {
|
|
15
|
+
return new Promise(async (resolve, reject) => {
|
|
16
|
+
try {
|
|
17
|
+
if (this._relationship === null) {
|
|
18
|
+
throw new Error("Relationship is null");
|
|
19
|
+
}
|
|
20
|
+
const db = Database.getInstance();
|
|
21
|
+
db.removeRelationship(this._relationship);
|
|
22
|
+
resolve();
|
|
23
|
+
} catch (error) {
|
|
24
|
+
reject(error);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
public get results(): Record<string, any>[] {
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export default DeleteRelationship;
|
package/src/parsing/parser.ts
CHANGED
|
@@ -53,6 +53,8 @@ import AggregatedWith from "./operations/aggregated_with";
|
|
|
53
53
|
import Call from "./operations/call";
|
|
54
54
|
import CreateNode from "./operations/create_node";
|
|
55
55
|
import CreateRelationship from "./operations/create_relationship";
|
|
56
|
+
import DeleteNode from "./operations/delete_node";
|
|
57
|
+
import DeleteRelationship from "./operations/delete_relationship";
|
|
56
58
|
import Limit from "./operations/limit";
|
|
57
59
|
import Load from "./operations/load";
|
|
58
60
|
import Match from "./operations/match";
|
|
@@ -186,9 +188,13 @@ class Parser extends BaseParser {
|
|
|
186
188
|
!(operation instanceof Return) &&
|
|
187
189
|
!(operation instanceof Call) &&
|
|
188
190
|
!(operation instanceof CreateNode) &&
|
|
189
|
-
!(operation instanceof CreateRelationship)
|
|
191
|
+
!(operation instanceof CreateRelationship) &&
|
|
192
|
+
!(operation instanceof DeleteNode) &&
|
|
193
|
+
!(operation instanceof DeleteRelationship)
|
|
190
194
|
) {
|
|
191
|
-
throw new Error(
|
|
195
|
+
throw new Error(
|
|
196
|
+
"Last statement must be a RETURN, WHERE, CALL, CREATE, or DELETE statement"
|
|
197
|
+
);
|
|
192
198
|
}
|
|
193
199
|
return root;
|
|
194
200
|
}
|
|
@@ -201,6 +207,7 @@ class Parser extends BaseParser {
|
|
|
201
207
|
this.parseLoad() ||
|
|
202
208
|
this.parseCall() ||
|
|
203
209
|
this.parseCreate() ||
|
|
210
|
+
this.parseDelete() ||
|
|
204
211
|
this.parseMatch()
|
|
205
212
|
);
|
|
206
213
|
}
|
|
@@ -217,7 +224,7 @@ class Parser extends BaseParser {
|
|
|
217
224
|
this.setNextToken();
|
|
218
225
|
this.expectAndSkipWhitespaceAndComments();
|
|
219
226
|
}
|
|
220
|
-
const expressions =
|
|
227
|
+
const expressions = this.parseExpressions(AliasOption.REQUIRED);
|
|
221
228
|
if (expressions.length === 0) {
|
|
222
229
|
throw new Error("Expected expression");
|
|
223
230
|
}
|
|
@@ -272,7 +279,7 @@ class Parser extends BaseParser {
|
|
|
272
279
|
this.setNextToken();
|
|
273
280
|
this.expectAndSkipWhitespaceAndComments();
|
|
274
281
|
}
|
|
275
|
-
const expressions =
|
|
282
|
+
const expressions = this.parseExpressions(AliasOption.OPTIONAL);
|
|
276
283
|
if (expressions.length === 0) {
|
|
277
284
|
throw new Error("Expected expression");
|
|
278
285
|
}
|
|
@@ -386,7 +393,7 @@ class Parser extends BaseParser {
|
|
|
386
393
|
this.expectPreviousTokenToBeWhitespaceOrComment();
|
|
387
394
|
this.setNextToken();
|
|
388
395
|
this.expectAndSkipWhitespaceAndComments();
|
|
389
|
-
const expressions =
|
|
396
|
+
const expressions = this.parseExpressions(AliasOption.OPTIONAL);
|
|
390
397
|
if (expressions.length === 0) {
|
|
391
398
|
throw new Error("Expected at least one expression");
|
|
392
399
|
}
|
|
@@ -459,6 +466,60 @@ class Parser extends BaseParser {
|
|
|
459
466
|
return create;
|
|
460
467
|
}
|
|
461
468
|
|
|
469
|
+
private parseDelete(): DeleteNode | DeleteRelationship | null {
|
|
470
|
+
if (!this.token.isDelete()) {
|
|
471
|
+
return null;
|
|
472
|
+
}
|
|
473
|
+
this.setNextToken();
|
|
474
|
+
this.expectAndSkipWhitespaceAndComments();
|
|
475
|
+
if (!this.token.isVirtual()) {
|
|
476
|
+
throw new Error("Expected VIRTUAL");
|
|
477
|
+
}
|
|
478
|
+
this.setNextToken();
|
|
479
|
+
this.expectAndSkipWhitespaceAndComments();
|
|
480
|
+
const node: Node | null = this.parseNode();
|
|
481
|
+
if (node === null) {
|
|
482
|
+
throw new Error("Expected node definition");
|
|
483
|
+
}
|
|
484
|
+
let relationship: Relationship | null = null;
|
|
485
|
+
if (this.token.isSubtract() && this.peek()?.isOpeningBracket()) {
|
|
486
|
+
this.setNextToken();
|
|
487
|
+
this.setNextToken();
|
|
488
|
+
if (!this.token.isColon()) {
|
|
489
|
+
throw new Error("Expected ':' for relationship type");
|
|
490
|
+
}
|
|
491
|
+
this.setNextToken();
|
|
492
|
+
if (!this.token.isIdentifierOrKeyword()) {
|
|
493
|
+
throw new Error("Expected relationship type identifier");
|
|
494
|
+
}
|
|
495
|
+
const type: string = this.token.value || "";
|
|
496
|
+
this.setNextToken();
|
|
497
|
+
if (!this.token.isClosingBracket()) {
|
|
498
|
+
throw new Error("Expected closing bracket for relationship definition");
|
|
499
|
+
}
|
|
500
|
+
this.setNextToken();
|
|
501
|
+
if (!this.token.isSubtract()) {
|
|
502
|
+
throw new Error("Expected '-' for relationship definition");
|
|
503
|
+
}
|
|
504
|
+
this.setNextToken();
|
|
505
|
+
const target: Node | null = this.parseNode();
|
|
506
|
+
if (target === null) {
|
|
507
|
+
throw new Error("Expected target node definition");
|
|
508
|
+
}
|
|
509
|
+
relationship = new Relationship();
|
|
510
|
+
relationship.type = type;
|
|
511
|
+
relationship.source = node;
|
|
512
|
+
relationship.target = target;
|
|
513
|
+
}
|
|
514
|
+
let result: DeleteNode | DeleteRelationship;
|
|
515
|
+
if (relationship !== null) {
|
|
516
|
+
result = new DeleteRelationship(relationship);
|
|
517
|
+
} else {
|
|
518
|
+
result = new DeleteNode(node);
|
|
519
|
+
}
|
|
520
|
+
return result;
|
|
521
|
+
}
|
|
522
|
+
|
|
462
523
|
private parseMatch(): Match | null {
|
|
463
524
|
let optional = false;
|
|
464
525
|
if (this.token.isOptional()) {
|
|
@@ -842,36 +903,52 @@ class Parser extends BaseParser {
|
|
|
842
903
|
return new OrderBy(fields);
|
|
843
904
|
}
|
|
844
905
|
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
906
|
+
/**
|
|
907
|
+
* Parses a comma-separated list of expressions with deferred variable
|
|
908
|
+
* registration. Aliases set by earlier expressions in the same clause
|
|
909
|
+
* won't shadow variables needed by later expressions
|
|
910
|
+
* (e.g. `RETURN a.x AS a, a.y AS b`).
|
|
911
|
+
*/
|
|
912
|
+
private parseExpressions(alias_option: AliasOption = AliasOption.NOT_ALLOWED): Expression[] {
|
|
913
|
+
const parsed = Array.from(this._parseExpressions(alias_option));
|
|
914
|
+
for (const [expression, variableName] of parsed) {
|
|
915
|
+
if (variableName !== null) {
|
|
916
|
+
this._state.variables.set(variableName, expression);
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
return parsed.map(([expression]) => expression);
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
private *_parseExpressions(
|
|
923
|
+
alias_option: AliasOption
|
|
924
|
+
): IterableIterator<[Expression, string | null]> {
|
|
848
925
|
while (true) {
|
|
849
926
|
const expression: Expression | null = this.parseExpression();
|
|
850
|
-
if (expression
|
|
851
|
-
const alias = this.parseAlias();
|
|
852
|
-
if (expression.firstChild() instanceof Reference && alias === null) {
|
|
853
|
-
const reference: Reference = expression.firstChild() as Reference;
|
|
854
|
-
expression.setAlias(reference.identifier);
|
|
855
|
-
this._state.variables.set(reference.identifier, expression);
|
|
856
|
-
} else if (
|
|
857
|
-
alias_option === AliasOption.REQUIRED &&
|
|
858
|
-
alias === null &&
|
|
859
|
-
!(expression.firstChild() instanceof Reference)
|
|
860
|
-
) {
|
|
861
|
-
throw new Error("Alias required");
|
|
862
|
-
} else if (alias_option === AliasOption.NOT_ALLOWED && alias !== null) {
|
|
863
|
-
throw new Error("Alias not allowed");
|
|
864
|
-
} else if (
|
|
865
|
-
[AliasOption.OPTIONAL, AliasOption.REQUIRED].includes(alias_option) &&
|
|
866
|
-
alias !== null
|
|
867
|
-
) {
|
|
868
|
-
expression.setAlias(alias.getAlias());
|
|
869
|
-
this._state.variables.set(alias.getAlias(), expression);
|
|
870
|
-
}
|
|
871
|
-
yield expression;
|
|
872
|
-
} else {
|
|
927
|
+
if (expression === null) {
|
|
873
928
|
break;
|
|
874
929
|
}
|
|
930
|
+
let variableName: string | null = null;
|
|
931
|
+
const alias = this.parseAlias();
|
|
932
|
+
if (expression.firstChild() instanceof Reference && alias === null) {
|
|
933
|
+
const reference: Reference = expression.firstChild() as Reference;
|
|
934
|
+
expression.setAlias(reference.identifier);
|
|
935
|
+
variableName = reference.identifier;
|
|
936
|
+
} else if (
|
|
937
|
+
alias_option === AliasOption.REQUIRED &&
|
|
938
|
+
alias === null &&
|
|
939
|
+
!(expression.firstChild() instanceof Reference)
|
|
940
|
+
) {
|
|
941
|
+
throw new Error("Alias required");
|
|
942
|
+
} else if (alias_option === AliasOption.NOT_ALLOWED && alias !== null) {
|
|
943
|
+
throw new Error("Alias not allowed");
|
|
944
|
+
} else if (
|
|
945
|
+
[AliasOption.OPTIONAL, AliasOption.REQUIRED].includes(alias_option) &&
|
|
946
|
+
alias !== null
|
|
947
|
+
) {
|
|
948
|
+
expression.setAlias(alias.getAlias());
|
|
949
|
+
variableName = alias.getAlias();
|
|
950
|
+
}
|
|
951
|
+
yield [expression, variableName];
|
|
875
952
|
this.skipWhitespaceAndComments();
|
|
876
953
|
if (!this.token.isComma()) {
|
|
877
954
|
break;
|
|
@@ -1299,7 +1376,7 @@ class Parser extends BaseParser {
|
|
|
1299
1376
|
this.setNextToken();
|
|
1300
1377
|
this.expectAndSkipWhitespaceAndComments();
|
|
1301
1378
|
}
|
|
1302
|
-
func.parameters =
|
|
1379
|
+
func.parameters = this.parseExpressions(AliasOption.NOT_ALLOWED);
|
|
1303
1380
|
this.skipWhitespaceAndComments();
|
|
1304
1381
|
if (!this.token.isRightParenthesis()) {
|
|
1305
1382
|
throw new Error("Expected right parenthesis");
|
|
@@ -1333,7 +1410,7 @@ class Parser extends BaseParser {
|
|
|
1333
1410
|
this.setNextToken(); // skip function name
|
|
1334
1411
|
this.setNextToken(); // skip left parenthesis
|
|
1335
1412
|
this.skipWhitespaceAndComments();
|
|
1336
|
-
asyncFunc.parameters =
|
|
1413
|
+
asyncFunc.parameters = this.parseExpressions(AliasOption.NOT_ALLOWED);
|
|
1337
1414
|
this.skipWhitespaceAndComments();
|
|
1338
1415
|
if (!this.token.isRightParenthesis()) {
|
|
1339
1416
|
throw new Error("Expected right parenthesis");
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import Runner from "../../src/compute/runner";
|
|
2
2
|
import Database from "../../src/graph/database";
|
|
3
3
|
import Node from "../../src/graph/node";
|
|
4
|
+
import Relationship from "../../src/graph/relationship";
|
|
4
5
|
import AsyncFunction from "../../src/parsing/functions/async_function";
|
|
5
6
|
import { FunctionDef } from "../../src/parsing/functions/function_metadata";
|
|
6
7
|
|
|
@@ -725,6 +726,38 @@ test("Test trim function with empty string", async () => {
|
|
|
725
726
|
expect(results[0]).toEqual({ result: "" });
|
|
726
727
|
});
|
|
727
728
|
|
|
729
|
+
test("Test substring function with start and length", async () => {
|
|
730
|
+
const runner = new Runner('RETURN substring("hello", 1, 3) as result');
|
|
731
|
+
await runner.run();
|
|
732
|
+
const results = runner.results;
|
|
733
|
+
expect(results.length).toBe(1);
|
|
734
|
+
expect(results[0]).toEqual({ result: "ell" });
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
test("Test substring function with start only", async () => {
|
|
738
|
+
const runner = new Runner('RETURN substring("hello", 2) as result');
|
|
739
|
+
await runner.run();
|
|
740
|
+
const results = runner.results;
|
|
741
|
+
expect(results.length).toBe(1);
|
|
742
|
+
expect(results[0]).toEqual({ result: "llo" });
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
test("Test substring function with zero start", async () => {
|
|
746
|
+
const runner = new Runner('RETURN substring("hello", 0, 5) as result');
|
|
747
|
+
await runner.run();
|
|
748
|
+
const results = runner.results;
|
|
749
|
+
expect(results.length).toBe(1);
|
|
750
|
+
expect(results[0]).toEqual({ result: "hello" });
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
test("Test substring function with zero length", async () => {
|
|
754
|
+
const runner = new Runner('RETURN substring("hello", 1, 0) as result');
|
|
755
|
+
await runner.run();
|
|
756
|
+
const results = runner.results;
|
|
757
|
+
expect(results.length).toBe(1);
|
|
758
|
+
expect(results[0]).toEqual({ result: "" });
|
|
759
|
+
});
|
|
760
|
+
|
|
728
761
|
test("Test associative array with key which is keyword", async () => {
|
|
729
762
|
const runner = new Runner("RETURN {return: 1} as aa");
|
|
730
763
|
await runner.run();
|
|
@@ -3903,3 +3936,164 @@ test("Test order by with where", async () => {
|
|
|
3903
3936
|
expect(results[3]).toEqual({ x: 4 });
|
|
3904
3937
|
expect(results[4]).toEqual({ x: 3 });
|
|
3905
3938
|
});
|
|
3939
|
+
|
|
3940
|
+
test("Test delete virtual node operation", async () => {
|
|
3941
|
+
const db = Database.getInstance();
|
|
3942
|
+
// Create a virtual node first
|
|
3943
|
+
const create = new Runner(`
|
|
3944
|
+
CREATE VIRTUAL (:DeleteTestPerson) AS {
|
|
3945
|
+
unwind [
|
|
3946
|
+
{id: 1, name: 'Person 1'},
|
|
3947
|
+
{id: 2, name: 'Person 2'}
|
|
3948
|
+
] as record
|
|
3949
|
+
RETURN record.id as id, record.name as name
|
|
3950
|
+
}
|
|
3951
|
+
`);
|
|
3952
|
+
await create.run();
|
|
3953
|
+
expect(db.getNode(new Node(null, "DeleteTestPerson"))).not.toBeNull();
|
|
3954
|
+
|
|
3955
|
+
// Delete the virtual node
|
|
3956
|
+
const del = new Runner("DELETE VIRTUAL (:DeleteTestPerson)");
|
|
3957
|
+
await del.run();
|
|
3958
|
+
expect(del.results.length).toBe(0);
|
|
3959
|
+
expect(db.getNode(new Node(null, "DeleteTestPerson"))).toBeNull();
|
|
3960
|
+
});
|
|
3961
|
+
|
|
3962
|
+
test("Test delete virtual node then match throws", async () => {
|
|
3963
|
+
// Create a virtual node
|
|
3964
|
+
const create = new Runner(`
|
|
3965
|
+
CREATE VIRTUAL (:DeleteMatchPerson) AS {
|
|
3966
|
+
unwind [{id: 1, name: 'Alice'}] as record
|
|
3967
|
+
RETURN record.id as id, record.name as name
|
|
3968
|
+
}
|
|
3969
|
+
`);
|
|
3970
|
+
await create.run();
|
|
3971
|
+
|
|
3972
|
+
// Verify it can be matched
|
|
3973
|
+
const match1 = new Runner("MATCH (n:DeleteMatchPerson) RETURN n");
|
|
3974
|
+
await match1.run();
|
|
3975
|
+
expect(match1.results.length).toBe(1);
|
|
3976
|
+
|
|
3977
|
+
// Delete the virtual node
|
|
3978
|
+
const del = new Runner("DELETE VIRTUAL (:DeleteMatchPerson)");
|
|
3979
|
+
await del.run();
|
|
3980
|
+
|
|
3981
|
+
// Matching should now throw since the node is gone
|
|
3982
|
+
const match2 = new Runner("MATCH (n:DeleteMatchPerson) RETURN n");
|
|
3983
|
+
await expect(match2.run()).rejects.toThrow();
|
|
3984
|
+
});
|
|
3985
|
+
|
|
3986
|
+
test("Test delete virtual relationship operation", async () => {
|
|
3987
|
+
const db = Database.getInstance();
|
|
3988
|
+
// Create virtual nodes and relationship
|
|
3989
|
+
await new Runner(`
|
|
3990
|
+
CREATE VIRTUAL (:DelRelUser) AS {
|
|
3991
|
+
unwind [
|
|
3992
|
+
{id: 1, name: 'Alice'},
|
|
3993
|
+
{id: 2, name: 'Bob'}
|
|
3994
|
+
] as record
|
|
3995
|
+
RETURN record.id as id, record.name as name
|
|
3996
|
+
}
|
|
3997
|
+
`).run();
|
|
3998
|
+
|
|
3999
|
+
await new Runner(`
|
|
4000
|
+
CREATE VIRTUAL (:DelRelUser)-[:DEL_KNOWS]-(:DelRelUser) AS {
|
|
4001
|
+
unwind [
|
|
4002
|
+
{left_id: 1, right_id: 2}
|
|
4003
|
+
] as record
|
|
4004
|
+
RETURN record.left_id as left_id, record.right_id as right_id
|
|
4005
|
+
}
|
|
4006
|
+
`).run();
|
|
4007
|
+
|
|
4008
|
+
// Verify relationship exists
|
|
4009
|
+
const rel = new Relationship();
|
|
4010
|
+
rel.type = "DEL_KNOWS";
|
|
4011
|
+
expect(db.getRelationship(rel)).not.toBeNull();
|
|
4012
|
+
|
|
4013
|
+
// Delete the virtual relationship
|
|
4014
|
+
const del = new Runner("DELETE VIRTUAL (:DelRelUser)-[:DEL_KNOWS]-(:DelRelUser)");
|
|
4015
|
+
await del.run();
|
|
4016
|
+
expect(del.results.length).toBe(0);
|
|
4017
|
+
expect(db.getRelationship(rel)).toBeNull();
|
|
4018
|
+
});
|
|
4019
|
+
|
|
4020
|
+
test("Test delete virtual node leaves other nodes intact", async () => {
|
|
4021
|
+
const db = Database.getInstance();
|
|
4022
|
+
// Create two virtual node types
|
|
4023
|
+
await new Runner(`
|
|
4024
|
+
CREATE VIRTUAL (:KeepNode) AS {
|
|
4025
|
+
unwind [{id: 1, name: 'Keep'}] as record
|
|
4026
|
+
RETURN record.id as id, record.name as name
|
|
4027
|
+
}
|
|
4028
|
+
`).run();
|
|
4029
|
+
|
|
4030
|
+
await new Runner(`
|
|
4031
|
+
CREATE VIRTUAL (:RemoveNode) AS {
|
|
4032
|
+
unwind [{id: 2, name: 'Remove'}] as record
|
|
4033
|
+
RETURN record.id as id, record.name as name
|
|
4034
|
+
}
|
|
4035
|
+
`).run();
|
|
4036
|
+
|
|
4037
|
+
expect(db.getNode(new Node(null, "KeepNode"))).not.toBeNull();
|
|
4038
|
+
expect(db.getNode(new Node(null, "RemoveNode"))).not.toBeNull();
|
|
4039
|
+
|
|
4040
|
+
// Delete only one
|
|
4041
|
+
await new Runner("DELETE VIRTUAL (:RemoveNode)").run();
|
|
4042
|
+
|
|
4043
|
+
// The other should still exist
|
|
4044
|
+
expect(db.getNode(new Node(null, "KeepNode"))).not.toBeNull();
|
|
4045
|
+
expect(db.getNode(new Node(null, "RemoveNode"))).toBeNull();
|
|
4046
|
+
|
|
4047
|
+
// The remaining node can still be matched
|
|
4048
|
+
const match = new Runner("MATCH (n:KeepNode) RETURN n");
|
|
4049
|
+
await match.run();
|
|
4050
|
+
expect(match.results.length).toBe(1);
|
|
4051
|
+
expect(match.results[0].n.name).toBe("Keep");
|
|
4052
|
+
});
|
|
4053
|
+
|
|
4054
|
+
test("Test RETURN alias shadowing graph variable in same RETURN clause", async () => {
|
|
4055
|
+
// Create User nodes with displayName, jobTitle, and department
|
|
4056
|
+
await new Runner(`
|
|
4057
|
+
CREATE VIRTUAL (:MentorUser) AS {
|
|
4058
|
+
UNWIND [
|
|
4059
|
+
{id: 1, displayName: 'Alice Smith', jobTitle: 'Senior Engineer', department: 'Engineering'},
|
|
4060
|
+
{id: 2, displayName: 'Bob Jones', jobTitle: 'Staff Engineer', department: 'Engineering'},
|
|
4061
|
+
{id: 3, displayName: 'Chloe Dubois', jobTitle: 'Junior Engineer', department: 'Engineering'}
|
|
4062
|
+
] AS record
|
|
4063
|
+
RETURN record.id AS id, record.displayName AS displayName, record.jobTitle AS jobTitle, record.department AS department
|
|
4064
|
+
}
|
|
4065
|
+
`).run();
|
|
4066
|
+
|
|
4067
|
+
// Create MENTORS relationships
|
|
4068
|
+
await new Runner(`
|
|
4069
|
+
CREATE VIRTUAL (:MentorUser)-[:MENTORS]-(:MentorUser) AS {
|
|
4070
|
+
UNWIND [
|
|
4071
|
+
{left_id: 1, right_id: 3},
|
|
4072
|
+
{left_id: 2, right_id: 3}
|
|
4073
|
+
] AS record
|
|
4074
|
+
RETURN record.left_id AS left_id, record.right_id AS right_id
|
|
4075
|
+
}
|
|
4076
|
+
`).run();
|
|
4077
|
+
|
|
4078
|
+
// This query aliases mentor.displayName AS mentor, which shadows the graph variable "mentor".
|
|
4079
|
+
// Subsequent expressions mentor.jobTitle and mentor.department should still reference the graph node.
|
|
4080
|
+
const runner = new Runner(`
|
|
4081
|
+
MATCH (mentor:MentorUser)-[:MENTORS]->(mentee:MentorUser)
|
|
4082
|
+
WHERE mentee.displayName = "Chloe Dubois"
|
|
4083
|
+
RETURN mentor.displayName AS mentor, mentor.jobTitle AS mentorJobTitle, mentor.department AS mentorDepartment
|
|
4084
|
+
`);
|
|
4085
|
+
await runner.run();
|
|
4086
|
+
const results = runner.results;
|
|
4087
|
+
|
|
4088
|
+
expect(results.length).toBe(2);
|
|
4089
|
+
expect(results[0]).toEqual({
|
|
4090
|
+
mentor: "Alice Smith",
|
|
4091
|
+
mentorJobTitle: "Senior Engineer",
|
|
4092
|
+
mentorDepartment: "Engineering",
|
|
4093
|
+
});
|
|
4094
|
+
expect(results[1]).toEqual({
|
|
4095
|
+
mentor: "Bob Jones",
|
|
4096
|
+
mentorJobTitle: "Staff Engineer",
|
|
4097
|
+
mentorDepartment: "Engineering",
|
|
4098
|
+
});
|
|
4099
|
+
});
|
|
@@ -521,7 +521,7 @@ test("Test non-well formed statements", () => {
|
|
|
521
521
|
"Only one RETURN statement is allowed"
|
|
522
522
|
);
|
|
523
523
|
expect(() => new Parser().parse("return 1 with 1 as n")).toThrow(
|
|
524
|
-
"Last statement must be a RETURN, WHERE, CALL, or
|
|
524
|
+
"Last statement must be a RETURN, WHERE, CALL, CREATE, or DELETE statement"
|
|
525
525
|
);
|
|
526
526
|
});
|
|
527
527
|
|