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.
Files changed (45) hide show
  1. package/dist/flowquery.min.js +1 -1
  2. package/dist/graph/database.d.ts +2 -0
  3. package/dist/graph/database.d.ts.map +1 -1
  4. package/dist/graph/database.js +12 -0
  5. package/dist/graph/database.js.map +1 -1
  6. package/dist/parsing/functions/function_factory.d.ts +1 -0
  7. package/dist/parsing/functions/function_factory.d.ts.map +1 -1
  8. package/dist/parsing/functions/function_factory.js +1 -0
  9. package/dist/parsing/functions/function_factory.js.map +1 -1
  10. package/dist/parsing/functions/substring.d.ts +9 -0
  11. package/dist/parsing/functions/substring.d.ts.map +1 -0
  12. package/dist/parsing/functions/substring.js +62 -0
  13. package/dist/parsing/functions/substring.js.map +1 -0
  14. package/dist/parsing/operations/delete_node.d.ts +11 -0
  15. package/dist/parsing/operations/delete_node.d.ts.map +1 -0
  16. package/dist/parsing/operations/delete_node.js +46 -0
  17. package/dist/parsing/operations/delete_node.js.map +1 -0
  18. package/dist/parsing/operations/delete_relationship.d.ts +11 -0
  19. package/dist/parsing/operations/delete_relationship.d.ts.map +1 -0
  20. package/dist/parsing/operations/delete_relationship.js +46 -0
  21. package/dist/parsing/operations/delete_relationship.js.map +1 -0
  22. package/dist/parsing/parser.d.ts +8 -0
  23. package/dist/parsing/parser.d.ts.map +1 -1
  24. package/dist/parsing/parser.js +105 -31
  25. package/dist/parsing/parser.js.map +1 -1
  26. package/docs/flowquery.min.js +1 -1
  27. package/flowquery-py/pyproject.toml +1 -1
  28. package/flowquery-py/src/graph/database.py +12 -0
  29. package/flowquery-py/src/parsing/functions/__init__.py +2 -0
  30. package/flowquery-py/src/parsing/functions/substring.py +74 -0
  31. package/flowquery-py/src/parsing/operations/__init__.py +4 -0
  32. package/flowquery-py/src/parsing/operations/delete_node.py +29 -0
  33. package/flowquery-py/src/parsing/operations/delete_relationship.py +29 -0
  34. package/flowquery-py/src/parsing/parser.py +75 -10
  35. package/flowquery-py/tests/compute/test_runner.py +226 -1
  36. package/flowquery-vscode/flowQueryEngine/flowquery.min.js +1 -1
  37. package/package.json +1 -1
  38. package/src/graph/database.ts +12 -0
  39. package/src/parsing/functions/function_factory.ts +1 -0
  40. package/src/parsing/functions/substring.ts +65 -0
  41. package/src/parsing/operations/delete_node.ts +33 -0
  42. package/src/parsing/operations/delete_relationship.ts +32 -0
  43. package/src/parsing/parser.ts +110 -33
  44. package/tests/compute/runner.test.ts +194 -0
  45. package/tests/parsing/parser.test.ts +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flowquery",
3
- "version": "1.0.39",
3
+ "version": "1.0.41",
4
4
  "description": "A declarative query language for data processing pipelines.",
5
5
  "main": "dist/index.node.js",
6
6
  "types": "dist/index.node.d.ts",
@@ -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
  }
@@ -39,6 +39,7 @@ import "./size";
39
39
  import "./split";
40
40
  import "./string_distance";
41
41
  import "./stringify";
42
+ import "./substring";
42
43
  // Import built-in functions to ensure their @FunctionDef decorators run
43
44
  import "./sum";
44
45
  import "./tail";
@@ -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;
@@ -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("Last statement must be a RETURN, WHERE, CALL, or CREATE statement");
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 = Array.from(this.parseExpressions(AliasOption.REQUIRED));
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 = Array.from(this.parseExpressions(AliasOption.OPTIONAL));
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 = Array.from(this.parseExpressions(AliasOption.OPTIONAL));
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
- private *parseExpressions(
846
- alias_option: AliasOption = AliasOption.NOT_ALLOWED
847
- ): IterableIterator<Expression> {
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 !== null) {
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 = Array.from(this.parseExpressions(AliasOption.NOT_ALLOWED));
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 = Array.from(this.parseExpressions(AliasOption.NOT_ALLOWED));
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 CREATE statement"
524
+ "Last statement must be a RETURN, WHERE, CALL, CREATE, or DELETE statement"
525
525
  );
526
526
  });
527
527