flowquery 1.0.30 → 1.0.32

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 (41) hide show
  1. package/dist/flowquery.min.js +1 -1
  2. package/dist/graph/node_reference.d.ts +3 -2
  3. package/dist/graph/node_reference.d.ts.map +1 -1
  4. package/dist/graph/node_reference.js.map +1 -1
  5. package/dist/graph/relationship.d.ts +2 -1
  6. package/dist/graph/relationship.d.ts.map +1 -1
  7. package/dist/graph/relationship.js +15 -15
  8. package/dist/graph/relationship.js.map +1 -1
  9. package/dist/graph/relationship_data.d.ts +1 -2
  10. package/dist/graph/relationship_data.d.ts.map +1 -1
  11. package/dist/graph/relationship_data.js +2 -5
  12. package/dist/graph/relationship_data.js.map +1 -1
  13. package/dist/graph/relationship_match_collector.d.ts +2 -2
  14. package/dist/graph/relationship_match_collector.d.ts.map +1 -1
  15. package/dist/graph/relationship_match_collector.js +6 -7
  16. package/dist/graph/relationship_match_collector.js.map +1 -1
  17. package/dist/parsing/operations/group_by.d.ts.map +1 -1
  18. package/dist/parsing/operations/group_by.js +8 -4
  19. package/dist/parsing/operations/group_by.js.map +1 -1
  20. package/dist/parsing/parser.d.ts.map +1 -1
  21. package/dist/parsing/parser.js +2 -1
  22. package/dist/parsing/parser.js.map +1 -1
  23. package/docs/flowquery.min.js +1 -1
  24. package/flowquery-py/pyproject.toml +1 -1
  25. package/flowquery-py/src/graph/node_reference.py +5 -4
  26. package/flowquery-py/src/graph/relationship.py +20 -22
  27. package/flowquery-py/src/graph/relationship_data.py +4 -7
  28. package/flowquery-py/src/graph/relationship_match_collector.py +5 -7
  29. package/flowquery-py/src/parsing/operations/group_by.py +16 -2
  30. package/flowquery-py/src/parsing/parser.py +1 -1
  31. package/flowquery-py/tests/compute/test_runner.py +61 -5
  32. package/flowquery-vscode/flowQueryEngine/flowquery.min.js +1 -1
  33. package/package.json +1 -1
  34. package/src/graph/node_reference.ts +4 -3
  35. package/src/graph/relationship.ts +15 -15
  36. package/src/graph/relationship_data.ts +2 -5
  37. package/src/graph/relationship_match_collector.ts +6 -7
  38. package/src/parsing/operations/group_by.ts +27 -19
  39. package/src/parsing/parser.ts +4 -1
  40. package/tests/compute/runner.test.ts +123 -4
  41. package/tests/parsing/parser.test.ts +2 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flowquery",
3
- "version": "1.0.30",
3
+ "version": "1.0.32",
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",
@@ -1,8 +1,9 @@
1
+ import ASTNode from "../parsing/ast_node";
1
2
  import Node from "./node";
2
3
 
3
4
  class NodeReference extends Node {
4
- private _reference: Node | null = null;
5
- constructor(base: Node, reference: Node) {
5
+ private _reference: ASTNode | null = null;
6
+ constructor(base: Node, reference: ASTNode) {
6
7
  super();
7
8
  this._identifier = base.identifier;
8
9
  this._label = base.label;
@@ -11,7 +12,7 @@ class NodeReference extends Node {
11
12
  this._incoming = base.incoming;
12
13
  this._reference = reference;
13
14
  }
14
- public get reference(): Node | null {
15
+ public get reference(): ASTNode | null {
15
16
  return this._reference;
16
17
  }
17
18
  public async next(): Promise<void> {
@@ -71,8 +71,8 @@ class Relationship extends ASTNode {
71
71
  public get hops(): Hops | null {
72
72
  return this._hops;
73
73
  }
74
- public setValue(relationship: Relationship): void {
75
- const match: RelationshipMatchRecord = this._matches.push(relationship);
74
+ public setValue(relationship: Relationship, traversalId: string = ""): void {
75
+ const match: RelationshipMatchRecord = this._matches.push(relationship, traversalId);
76
76
  this._value = this._matches.value();
77
77
  }
78
78
  public set source(node: Node | null) {
@@ -108,10 +108,12 @@ class Relationship extends ASTNode {
108
108
  public setEndNode(node: Node): void {
109
109
  this._matches.endNode = node;
110
110
  }
111
+ public _left_id_or_right_id(): string {
112
+ return this._direction === "left" ? "left_id" : "right_id";
113
+ }
111
114
  public async find(left_id: string, hop: number = 0): Promise<void> {
112
115
  // Save original source node
113
116
  const original = this._source;
114
- const isLeft = this._direction === "left";
115
117
  if (hop > 0) {
116
118
  // For hops greater than 0, the source becomes the target of the previous hop
117
119
  this._source = this._target;
@@ -127,28 +129,26 @@ class Relationship extends ASTNode {
127
129
  await this._target.find(left_id, hop);
128
130
  }
129
131
  }
130
- const findMatch = isLeft
131
- ? (id: string, h: number) => this._data!.findReverse(id, h)
132
- : (id: string, h: number) => this._data!.find(id, h);
133
- const followId = isLeft ? "left_id" : "right_id";
134
- while (findMatch(left_id, hop)) {
132
+ while (this._data!.find(left_id, hop, this._direction)) {
135
133
  const data: RelationshipRecord = this._data?.current(hop) as RelationshipRecord;
134
+ const id = data[this._left_id_or_right_id()];
136
135
  if (hop + 1 >= this.hops!.min) {
137
- this.setValue(this);
136
+ this.setValue(this, left_id);
138
137
  if (!this._matchesProperties(hop)) {
139
138
  continue;
140
139
  }
141
- await this._target?.find(data[followId], hop);
142
- if (this._matches.isCircular()) {
143
- throw new Error("Circular relationship detected");
144
- }
140
+ await this._target?.find(id, hop);
145
141
  if (hop + 1 < this.hops!.max) {
146
- await this.find(data[followId], hop + 1);
142
+ if (this._matches.isCircular(id)) {
143
+ this._matches.pop();
144
+ continue;
145
+ }
146
+ await this.find(id, hop + 1);
147
147
  }
148
148
  this._matches.pop();
149
149
  } else {
150
150
  // Below minimum hops: traverse the edge without yielding a match
151
- await this.find(data[followId], hop + 1);
151
+ await this.find(id, hop + 1);
152
152
  }
153
153
  }
154
154
  // Restore original source node
@@ -8,11 +8,8 @@ class RelationshipData extends Data {
8
8
  super._buildIndex("left_id");
9
9
  super._buildIndex("right_id");
10
10
  }
11
- public find(left_id: string, hop: number = 0): boolean {
12
- return super._find(left_id, hop, "left_id");
13
- }
14
- public findReverse(right_id: string, hop: number = 0): boolean {
15
- return super._find(right_id, hop, "right_id");
11
+ public find(id: string, hop: number = 0, direction: "left" | "right" = "right"): boolean {
12
+ return super._find(id, hop, direction === "left" ? "right_id" : "left_id");
16
13
  }
17
14
  /*
18
15
  ** Get the properties of the current relationship record
@@ -11,7 +11,7 @@ class RelationshipMatchCollector {
11
11
  private _matches: RelationshipMatchRecord[] = [];
12
12
  private _nodeIds: Array<string> = [];
13
13
 
14
- public push(relationship: Relationship): RelationshipMatchRecord {
14
+ public push(relationship: Relationship, traversalId: string): RelationshipMatchRecord {
15
15
  const match: RelationshipMatchRecord = {
16
16
  type: relationship.type!,
17
17
  startNode: relationship.source?.value() || {},
@@ -19,7 +19,7 @@ class RelationshipMatchCollector {
19
19
  properties: relationship.getData()?.properties() as Record<string, any>,
20
20
  };
21
21
  this._matches.push(match);
22
- this._nodeIds.push(match.startNode.id);
22
+ this._nodeIds.push(traversalId);
23
23
  return match;
24
24
  }
25
25
  public set endNode(node: any) {
@@ -46,12 +46,11 @@ class RelationshipMatchCollector {
46
46
  return this._matches;
47
47
  }
48
48
  /*
49
- ** Checks if the collected relationships form a circular pattern
50
- ** meaning the same node id occur more than 2 times in the collected matches
49
+ ** Checks if traversing to the given node id would form a cycle
50
+ ** in the current traversal path
51
51
  */
52
- public isCircular(): boolean {
53
- const seen = new Set(this._nodeIds);
54
- return seen.size < this._nodeIds.length;
52
+ public isCircular(nextId: string): boolean {
53
+ return this._nodeIds.includes(nextId);
55
54
  }
56
55
  }
57
56
 
@@ -50,10 +50,10 @@ class GroupBy extends Projection {
50
50
  }
51
51
  private map() {
52
52
  let node: Node = this.current;
53
- for(const mapper of this.mappers) {
53
+ for (const mapper of this.mappers) {
54
54
  const value: any = mapper.value();
55
55
  let child: Node | undefined = node.children.get(value);
56
- if(child === undefined) {
56
+ if (child === undefined) {
57
57
  child = new Node(value);
58
58
  node.children.set(value, child);
59
59
  }
@@ -62,8 +62,8 @@ class GroupBy extends Projection {
62
62
  this.current = node;
63
63
  }
64
64
  private reduce() {
65
- if(this.current.elements === null) {
66
- this.current.elements = this.reducers.map(reducer => reducer.element());
65
+ if (this.current.elements === null) {
66
+ this.current.elements = this.reducers.map((reducer) => reducer.element());
67
67
  }
68
68
  const elements: AggregationElement[] = this.current.elements;
69
69
  this.reducers.forEach((reducer, index) => {
@@ -71,29 +71,37 @@ class GroupBy extends Projection {
71
71
  });
72
72
  }
73
73
  private get mappers(): Expression[] {
74
- if(this._mappers === null) {
74
+ if (this._mappers === null) {
75
75
  this._mappers = [...this._generate_mappers()];
76
76
  }
77
77
  return this._mappers;
78
78
  }
79
79
  private *_generate_mappers(): Generator<Expression> {
80
- for(const [expression, _] of this.expressions()) {
81
- if(expression.mappable()) {
80
+ for (const [expression, _] of this.expressions()) {
81
+ if (expression.mappable()) {
82
82
  yield expression;
83
83
  }
84
84
  }
85
85
  }
86
86
  private get reducers(): AggregateFunction[] {
87
- if(this._reducers === null) {
88
- this._reducers = this.children.map((child) => {
89
- return (child as Expression).reducers();
90
- }).flat();
87
+ if (this._reducers === null) {
88
+ this._reducers = this.children
89
+ .map((child) => {
90
+ return (child as Expression).reducers();
91
+ })
92
+ .flat();
91
93
  }
92
94
  return this._reducers;
93
95
  }
94
- public *generate_results(mapperIndex: number = 0, node: Node = this.root): Generator<Record<string, any>> {
95
- if(node.children.size > 0) {
96
- for(const child of node.children.values()) {
96
+ public *generate_results(
97
+ mapperIndex: number = 0,
98
+ node: Node = this.root
99
+ ): Generator<Record<string, any>> {
100
+ if (mapperIndex === 0 && node.children.size === 0 && this.mappers.length > 0) {
101
+ return;
102
+ }
103
+ if (node.children.size > 0) {
104
+ for (const child of node.children.values()) {
97
105
  this.mappers[mapperIndex].overridden = child.value;
98
106
  yield* this.generate_results(mapperIndex + 1, child);
99
107
  }
@@ -102,10 +110,10 @@ class GroupBy extends Projection {
102
110
  this.reducers[reducerIndex].overridden = element.value;
103
111
  });
104
112
  const record: Record<string, any> = {};
105
- for(const [expression, alias] of this.expressions()) {
113
+ for (const [expression, alias] of this.expressions()) {
106
114
  record[alias] = expression.value();
107
115
  }
108
- if(this.where) {
116
+ if (this.where) {
109
117
  yield record;
110
118
  }
111
119
  }
@@ -114,11 +122,11 @@ class GroupBy extends Projection {
114
122
  this._where = where;
115
123
  }
116
124
  public get where(): boolean {
117
- if(this._where === null) {
125
+ if (this._where === null) {
118
126
  return true;
119
127
  }
120
128
  return this._where.value();
121
129
  }
122
- };
130
+ }
123
131
 
124
- export default GroupBy;
132
+ export default GroupBy;
@@ -467,7 +467,10 @@ class Parser extends BaseParser {
467
467
  reference = inner;
468
468
  }
469
469
  }
470
- if (reference === undefined || !(reference instanceof Node)) {
470
+ if (
471
+ reference === undefined ||
472
+ (!(reference instanceof Node) && !(reference instanceof Unwind))
473
+ ) {
471
474
  throw new Error(`Undefined node reference: ${identifier}`);
472
475
  }
473
476
  node = new NodeReference(node, reference);
@@ -334,6 +334,20 @@ test("Test aggregated with and return", async () => {
334
334
  expect(results[1]).toEqual({ i: 2, sum: 12 });
335
335
  });
336
336
 
337
+ test("Test aggregated with on empty result set", async () => {
338
+ const runner = new Runner(
339
+ `
340
+ unwind [] as i
341
+ unwind [1, 2] as j
342
+ with i, count(j) as cnt
343
+ return i, cnt
344
+ `
345
+ );
346
+ await runner.run();
347
+ const results = runner.results;
348
+ expect(results.length).toBe(0);
349
+ });
350
+
337
351
  test("Test aggregated with using collect and return", async () => {
338
352
  const runner = new Runner(
339
353
  `
@@ -1138,7 +1152,7 @@ test("Test circular graph pattern", async () => {
1138
1152
  expect(results[0].pattern[4].id).toBe(1);
1139
1153
  });
1140
1154
 
1141
- test("Test circular graph pattern with variable length should throw error", async () => {
1155
+ test("Test circular graph pattern with variable length should not revisit nodes", async () => {
1142
1156
  await new Runner(`
1143
1157
  CREATE VIRTUAL (:Person) AS {
1144
1158
  unwind [
@@ -1161,9 +1175,10 @@ test("Test circular graph pattern with variable length should throw error", asyn
1161
1175
  MATCH p=(:Person)-[:KNOWS*]-(:Person)
1162
1176
  RETURN p AS pattern
1163
1177
  `);
1164
- await expect(async () => {
1165
- await match.run();
1166
- }).rejects.toThrow("Circular relationship detected");
1178
+ await match.run();
1179
+ const results = match.results;
1180
+ // Circular graph 1↔2: cycles are skipped, only acyclic paths are returned
1181
+ expect(results.length).toBe(6);
1167
1182
  });
1168
1183
 
1169
1184
  test("Test multi-hop match with min hops constraint *1..", async () => {
@@ -2064,3 +2079,107 @@ test("Test WHERE with CONTAINS combined with AND", async () => {
2064
2079
  expect(results.length).toBe(1);
2065
2080
  expect(results[0].fruit).toBe("pineapple");
2066
2081
  });
2082
+
2083
+ test("Test collected nodes and re-matching", async () => {
2084
+ await new Runner(`
2085
+ CREATE VIRTUAL (:Person) AS {
2086
+ unwind [
2087
+ {id: 1, name: 'Person 1'},
2088
+ {id: 2, name: 'Person 2'},
2089
+ {id: 3, name: 'Person 3'},
2090
+ {id: 4, name: 'Person 4'}
2091
+ ] as record
2092
+ RETURN record.id as id, record.name as name
2093
+ }
2094
+ `).run();
2095
+ await new Runner(`
2096
+ CREATE VIRTUAL (:Person)-[:KNOWS]-(:Person) AS {
2097
+ unwind [
2098
+ {left_id: 1, right_id: 2},
2099
+ {left_id: 2, right_id: 3},
2100
+ {left_id: 3, right_id: 4}
2101
+ ] as record
2102
+ RETURN record.left_id as left_id, record.right_id as right_id
2103
+ }
2104
+ `).run();
2105
+ const match = new Runner(`
2106
+ MATCH (a:Person)-[:KNOWS*0..3]->(b:Person)
2107
+ WITH collect(a) AS persons, b
2108
+ UNWIND persons AS p
2109
+ match (p)-[:KNOWS]->(:Person)
2110
+ return p.name AS name
2111
+ `);
2112
+ await match.run();
2113
+ const results = match.results;
2114
+ expect(results.length).toBe(9);
2115
+ const names = results.map((r: any) => r.name);
2116
+ expect(names).toContain("Person 1");
2117
+ expect(names).toContain("Person 2");
2118
+ expect(names).toContain("Person 3");
2119
+ });
2120
+
2121
+ test("Test collected patterns and unwind", async () => {
2122
+ await new Runner(`
2123
+ CREATE VIRTUAL (:Person) AS {
2124
+ unwind [
2125
+ {id: 1, name: 'Person 1'},
2126
+ {id: 2, name: 'Person 2'},
2127
+ {id: 3, name: 'Person 3'},
2128
+ {id: 4, name: 'Person 4'}
2129
+ ] as record
2130
+ RETURN record.id as id, record.name as name
2131
+ }
2132
+ `).run();
2133
+ await new Runner(`
2134
+ CREATE VIRTUAL (:Person)-[:KNOWS]-(:Person) AS {
2135
+ unwind [
2136
+ {left_id: 1, right_id: 2},
2137
+ {left_id: 2, right_id: 3},
2138
+ {left_id: 3, right_id: 4}
2139
+ ] as record
2140
+ RETURN record.left_id as left_id, record.right_id as right_id
2141
+ }
2142
+ `).run();
2143
+ const match = new Runner(`
2144
+ MATCH p=(a:Person)-[:KNOWS*0..3]->(b:Person)
2145
+ WITH collect(p) AS patterns
2146
+ UNWIND patterns AS pattern
2147
+ RETURN pattern
2148
+ `);
2149
+ await match.run();
2150
+ const results = match.results;
2151
+ expect(results.length).toBe(10);
2152
+ // Index 0: Person 1 zero-hop - pattern = [node1] (single node)
2153
+ expect(results[0].pattern.length).toBe(1);
2154
+ expect(results[0].pattern[0].id).toBe(1);
2155
+
2156
+ // Index 1: Person 1 -> Person 2 (1-hop)
2157
+ expect(results[1].pattern.length).toBe(3);
2158
+
2159
+ // Index 2: Person 1 -> Person 2 -> Person 3 (2-hop)
2160
+ expect(results[2].pattern.length).toBe(5);
2161
+
2162
+ // Index 3: Person 1 -> Person 2 -> Person 3 -> Person 4 (3-hop)
2163
+ expect(results[3].pattern.length).toBe(7);
2164
+
2165
+ // Index 4: Person 2 zero-hop - pattern = [node2]
2166
+ expect(results[4].pattern.length).toBe(1);
2167
+ expect(results[4].pattern[0].id).toBe(2);
2168
+
2169
+ // Index 5: Person 2 -> Person 3 (1-hop)
2170
+ expect(results[5].pattern.length).toBe(3);
2171
+
2172
+ // Index 6: Person 2 -> Person 3 -> Person 4 (2-hop)
2173
+ expect(results[6].pattern.length).toBe(5);
2174
+
2175
+ // Index 7: Person 3 zero-hop - pattern = [node3]
2176
+ expect(results[7].pattern.length).toBe(1);
2177
+ expect(results[7].pattern[0].id).toBe(3);
2178
+
2179
+ // Index 8: Person 3 -> Person 4 (1-hop)
2180
+ expect(results[8].pattern.length).toBe(3);
2181
+
2182
+ // Index 9: Person 4 zero-hop - pattern = [node4]
2183
+ expect(results[9].pattern.length).toBe(1);
2184
+ expect(results[9].pattern[0].id).toBe(4);
2185
+ });
@@ -839,7 +839,7 @@ test("Parse statement with graph pattern in where clause", () => {
839
839
  const source = pattern.chain[0] as NodeReference;
840
840
  const relationship = pattern.chain[1] as Relationship;
841
841
  const target = pattern.chain[2] as Node;
842
- expect(source.reference?.identifier).toBe("a");
842
+ expect((source.reference as Node)?.identifier).toBe("a");
843
843
  expect(relationship.type).toBe("KNOWS");
844
844
  expect(target.label).toBe("Person");
845
845
  });
@@ -909,7 +909,7 @@ test("Test node reference with label creates NodeReference instead of new node",
909
909
  expect(firstNode.identifier).toBe("n");
910
910
  expect(firstNode.label).toBe("Person");
911
911
  expect(secondNode).toBeInstanceOf(NodeReference);
912
- expect(secondNode.reference!.identifier).toBe("n");
912
+ expect((secondNode.reference! as Node).identifier).toBe("n");
913
913
  expect(secondNode.label).toBe("Person");
914
914
  });
915
915