flowquery 1.0.39 → 1.0.40

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 +1 -0
  23. package/dist/parsing/parser.d.ts.map +1 -1
  24. package/dist/parsing/parser.js +62 -2
  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 +54 -3
  35. package/flowquery-py/tests/compute/test_runner.py +171 -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 +63 -2
  44. package/tests/compute/runner.test.ts +147 -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.40",
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
  }
@@ -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()) {
@@ -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,117 @@ 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
+ });
@@ -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