flowquery 1.0.20 → 1.0.21

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 (151) hide show
  1. package/.github/workflows/release.yml +2 -2
  2. package/.husky/pre-commit +26 -0
  3. package/dist/flowquery.min.js +1 -1
  4. package/dist/graph/data.d.ts +5 -4
  5. package/dist/graph/data.d.ts.map +1 -1
  6. package/dist/graph/data.js +35 -19
  7. package/dist/graph/data.js.map +1 -1
  8. package/dist/graph/node.d.ts +2 -0
  9. package/dist/graph/node.d.ts.map +1 -1
  10. package/dist/graph/node.js +23 -0
  11. package/dist/graph/node.js.map +1 -1
  12. package/dist/graph/node_data.js +1 -1
  13. package/dist/graph/node_data.js.map +1 -1
  14. package/dist/graph/relationship.d.ts +6 -1
  15. package/dist/graph/relationship.d.ts.map +1 -1
  16. package/dist/graph/relationship.js +38 -7
  17. package/dist/graph/relationship.js.map +1 -1
  18. package/dist/graph/relationship_data.d.ts +2 -0
  19. package/dist/graph/relationship_data.d.ts.map +1 -1
  20. package/dist/graph/relationship_data.js +8 -1
  21. package/dist/graph/relationship_data.js.map +1 -1
  22. package/dist/graph/relationship_match_collector.js +2 -2
  23. package/dist/graph/relationship_match_collector.js.map +1 -1
  24. package/dist/graph/relationship_reference.d.ts.map +1 -1
  25. package/dist/graph/relationship_reference.js +2 -1
  26. package/dist/graph/relationship_reference.js.map +1 -1
  27. package/dist/index.d.ts +7 -0
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +4 -4
  30. package/dist/index.js.map +1 -1
  31. package/dist/parsing/parser.d.ts +1 -0
  32. package/dist/parsing/parser.d.ts.map +1 -1
  33. package/dist/parsing/parser.js +47 -0
  34. package/dist/parsing/parser.js.map +1 -1
  35. package/docs/flowquery.min.js +1 -1
  36. package/flowquery-py/notebooks/TestFlowQuery.ipynb +1 -1
  37. package/flowquery-py/pyproject.toml +45 -2
  38. package/flowquery-py/src/__init__.py +5 -5
  39. package/flowquery-py/src/compute/runner.py +14 -10
  40. package/flowquery-py/src/extensibility.py +8 -8
  41. package/flowquery-py/src/graph/__init__.py +7 -7
  42. package/flowquery-py/src/graph/data.py +36 -19
  43. package/flowquery-py/src/graph/database.py +10 -20
  44. package/flowquery-py/src/graph/node.py +50 -19
  45. package/flowquery-py/src/graph/node_data.py +1 -1
  46. package/flowquery-py/src/graph/node_reference.py +10 -11
  47. package/flowquery-py/src/graph/pattern.py +23 -36
  48. package/flowquery-py/src/graph/pattern_expression.py +13 -11
  49. package/flowquery-py/src/graph/patterns.py +2 -2
  50. package/flowquery-py/src/graph/physical_node.py +4 -3
  51. package/flowquery-py/src/graph/physical_relationship.py +5 -5
  52. package/flowquery-py/src/graph/relationship.py +56 -15
  53. package/flowquery-py/src/graph/relationship_data.py +7 -2
  54. package/flowquery-py/src/graph/relationship_match_collector.py +15 -10
  55. package/flowquery-py/src/graph/relationship_reference.py +4 -4
  56. package/flowquery-py/src/io/command_line.py +13 -14
  57. package/flowquery-py/src/parsing/__init__.py +2 -2
  58. package/flowquery-py/src/parsing/alias_option.py +1 -1
  59. package/flowquery-py/src/parsing/ast_node.py +21 -20
  60. package/flowquery-py/src/parsing/base_parser.py +7 -7
  61. package/flowquery-py/src/parsing/components/__init__.py +3 -3
  62. package/flowquery-py/src/parsing/components/from_.py +3 -1
  63. package/flowquery-py/src/parsing/components/headers.py +2 -2
  64. package/flowquery-py/src/parsing/components/null.py +2 -2
  65. package/flowquery-py/src/parsing/context.py +7 -7
  66. package/flowquery-py/src/parsing/data_structures/associative_array.py +7 -7
  67. package/flowquery-py/src/parsing/data_structures/json_array.py +3 -3
  68. package/flowquery-py/src/parsing/data_structures/key_value_pair.py +4 -4
  69. package/flowquery-py/src/parsing/data_structures/lookup.py +2 -2
  70. package/flowquery-py/src/parsing/data_structures/range_lookup.py +2 -2
  71. package/flowquery-py/src/parsing/expressions/__init__.py +16 -16
  72. package/flowquery-py/src/parsing/expressions/expression.py +16 -13
  73. package/flowquery-py/src/parsing/expressions/expression_map.py +9 -9
  74. package/flowquery-py/src/parsing/expressions/f_string.py +3 -3
  75. package/flowquery-py/src/parsing/expressions/identifier.py +4 -3
  76. package/flowquery-py/src/parsing/expressions/number.py +3 -3
  77. package/flowquery-py/src/parsing/expressions/operator.py +16 -16
  78. package/flowquery-py/src/parsing/expressions/reference.py +3 -3
  79. package/flowquery-py/src/parsing/expressions/string.py +2 -2
  80. package/flowquery-py/src/parsing/functions/__init__.py +17 -17
  81. package/flowquery-py/src/parsing/functions/aggregate_function.py +8 -8
  82. package/flowquery-py/src/parsing/functions/async_function.py +12 -9
  83. package/flowquery-py/src/parsing/functions/avg.py +4 -4
  84. package/flowquery-py/src/parsing/functions/collect.py +6 -6
  85. package/flowquery-py/src/parsing/functions/function.py +6 -6
  86. package/flowquery-py/src/parsing/functions/function_factory.py +31 -34
  87. package/flowquery-py/src/parsing/functions/function_metadata.py +10 -11
  88. package/flowquery-py/src/parsing/functions/functions.py +14 -6
  89. package/flowquery-py/src/parsing/functions/join.py +3 -3
  90. package/flowquery-py/src/parsing/functions/keys.py +3 -3
  91. package/flowquery-py/src/parsing/functions/predicate_function.py +8 -7
  92. package/flowquery-py/src/parsing/functions/predicate_sum.py +12 -7
  93. package/flowquery-py/src/parsing/functions/rand.py +2 -2
  94. package/flowquery-py/src/parsing/functions/range_.py +9 -4
  95. package/flowquery-py/src/parsing/functions/replace.py +2 -2
  96. package/flowquery-py/src/parsing/functions/round_.py +2 -2
  97. package/flowquery-py/src/parsing/functions/size.py +2 -2
  98. package/flowquery-py/src/parsing/functions/split.py +9 -4
  99. package/flowquery-py/src/parsing/functions/stringify.py +3 -3
  100. package/flowquery-py/src/parsing/functions/sum.py +4 -4
  101. package/flowquery-py/src/parsing/functions/to_json.py +2 -2
  102. package/flowquery-py/src/parsing/functions/type_.py +3 -3
  103. package/flowquery-py/src/parsing/functions/value_holder.py +1 -1
  104. package/flowquery-py/src/parsing/logic/__init__.py +2 -2
  105. package/flowquery-py/src/parsing/logic/case.py +0 -1
  106. package/flowquery-py/src/parsing/logic/when.py +3 -1
  107. package/flowquery-py/src/parsing/operations/__init__.py +10 -10
  108. package/flowquery-py/src/parsing/operations/aggregated_return.py +3 -5
  109. package/flowquery-py/src/parsing/operations/aggregated_with.py +4 -4
  110. package/flowquery-py/src/parsing/operations/call.py +6 -7
  111. package/flowquery-py/src/parsing/operations/create_node.py +5 -4
  112. package/flowquery-py/src/parsing/operations/create_relationship.py +5 -4
  113. package/flowquery-py/src/parsing/operations/group_by.py +18 -16
  114. package/flowquery-py/src/parsing/operations/load.py +21 -19
  115. package/flowquery-py/src/parsing/operations/match.py +8 -7
  116. package/flowquery-py/src/parsing/operations/operation.py +3 -3
  117. package/flowquery-py/src/parsing/operations/projection.py +6 -6
  118. package/flowquery-py/src/parsing/operations/return_op.py +9 -5
  119. package/flowquery-py/src/parsing/operations/unwind.py +3 -2
  120. package/flowquery-py/src/parsing/operations/where.py +9 -7
  121. package/flowquery-py/src/parsing/operations/with_op.py +2 -2
  122. package/flowquery-py/src/parsing/parser.py +104 -57
  123. package/flowquery-py/src/parsing/token_to_node.py +2 -2
  124. package/flowquery-py/src/tokenization/__init__.py +4 -4
  125. package/flowquery-py/src/tokenization/keyword.py +1 -1
  126. package/flowquery-py/src/tokenization/operator.py +1 -1
  127. package/flowquery-py/src/tokenization/string_walker.py +4 -4
  128. package/flowquery-py/src/tokenization/symbol.py +1 -1
  129. package/flowquery-py/src/tokenization/token.py +11 -11
  130. package/flowquery-py/src/tokenization/token_mapper.py +10 -9
  131. package/flowquery-py/src/tokenization/token_type.py +1 -1
  132. package/flowquery-py/src/tokenization/tokenizer.py +19 -19
  133. package/flowquery-py/src/tokenization/trie.py +18 -17
  134. package/flowquery-py/src/utils/__init__.py +1 -1
  135. package/flowquery-py/src/utils/object_utils.py +3 -3
  136. package/flowquery-py/src/utils/string_utils.py +12 -12
  137. package/flowquery-py/tests/compute/test_runner.py +205 -1
  138. package/flowquery-py/tests/parsing/test_parser.py +41 -0
  139. package/flowquery-vscode/flowQueryEngine/flowquery.min.js +1 -1
  140. package/package.json +1 -1
  141. package/src/graph/data.ts +35 -19
  142. package/src/graph/node.ts +23 -0
  143. package/src/graph/node_data.ts +1 -1
  144. package/src/graph/relationship.ts +37 -5
  145. package/src/graph/relationship_data.ts +8 -1
  146. package/src/graph/relationship_match_collector.ts +1 -1
  147. package/src/graph/relationship_reference.ts +2 -1
  148. package/src/index.ts +1 -0
  149. package/src/parsing/parser.ts +47 -0
  150. package/tests/compute/runner.test.ts +178 -0
  151. package/tests/parsing/parser.test.ts +32 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flowquery",
3
- "version": "1.0.20",
3
+ "version": "1.0.21",
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",
package/src/graph/data.ts CHANGED
@@ -27,13 +27,19 @@ class IndexEntry {
27
27
  }
28
28
 
29
29
  class Layer {
30
- private _index: Map<string, IndexEntry> = new Map();
30
+ private _indexes: Map<string, Map<string, IndexEntry>> = new Map();
31
31
  private _current: number = -1;
32
- constructor(index: Map<string, IndexEntry>) {
33
- this._index = index;
32
+ constructor(indexes: Map<string, Map<string, IndexEntry>>) {
33
+ this._indexes = indexes;
34
34
  }
35
- public get index(): Map<string, IndexEntry> {
36
- return this._index;
35
+ public index(name: string): Map<string, IndexEntry> {
36
+ if (!this._indexes.has(name)) {
37
+ this._indexes.set(name, new Map());
38
+ }
39
+ return this._indexes.get(name)!;
40
+ }
41
+ public get indexes(): Map<string, Map<string, IndexEntry>> {
42
+ return this._indexes;
37
43
  }
38
44
  public get current(): number {
39
45
  return this._current;
@@ -52,33 +58,41 @@ class Data {
52
58
  this._layers.set(0, new Layer(new Map()));
53
59
  }
54
60
  protected _buildIndex(key: string, level: number = 0): void {
55
- this.layer(level).index.clear();
56
- this._records.forEach((record, idx) => {
61
+ const idx = this.layer(level).index(key);
62
+ idx.clear();
63
+ this._records.forEach((record, i) => {
57
64
  if (record.hasOwnProperty(key)) {
58
- if (!this.layer(level).index.has(record[key])) {
59
- this.layer(level).index.set(record[key], new IndexEntry());
65
+ if (!idx.has(record[key])) {
66
+ idx.set(record[key], new IndexEntry());
60
67
  }
61
- this.layer(level).index.get(record[key])!.add(idx);
68
+ idx.get(record[key])!.add(i);
62
69
  }
63
70
  });
64
71
  }
65
72
  public layer(level: number = 0): Layer {
66
73
  if (!this._layers.has(level)) {
67
74
  const first = this._layers.get(0)!;
68
- const cloned = new Map<string, IndexEntry>();
69
- for (const [key, entry] of first.index) {
70
- cloned.set(key, entry.clone());
75
+ const clonedIndexes = new Map<string, Map<string, IndexEntry>>();
76
+ for (const [name, indexMap] of first.indexes) {
77
+ const clonedMap = new Map<string, IndexEntry>();
78
+ for (const [key, entry] of indexMap) {
79
+ clonedMap.set(key, entry.clone());
80
+ }
81
+ clonedIndexes.set(name, clonedMap);
71
82
  }
72
- this._layers.set(level, new Layer(cloned));
83
+ this._layers.set(level, new Layer(clonedIndexes));
73
84
  }
74
85
  return this._layers.get(level)!;
75
86
  }
76
- protected _find(key: string, level: number = 0): boolean {
77
- if (!this.layer(level).index.has(key)) {
87
+ protected _find(key: string, level: number = 0, indexName?: string): boolean {
88
+ const idx = indexName
89
+ ? this.layer(level).index(indexName)
90
+ : this.layer(level).indexes.values().next().value;
91
+ if (!idx || !idx.has(key)) {
78
92
  this.layer(level).current = this._records.length; // Move to end
79
93
  return false;
80
94
  } else {
81
- const entry = this.layer(level).index.get(key)!;
95
+ const entry = idx.get(key)!;
82
96
  const more = entry.next();
83
97
  if (!more) {
84
98
  this.layer(level).current = this._records.length; // Move to end
@@ -91,8 +105,10 @@ class Data {
91
105
  public reset(): void {
92
106
  for (const layer of this._layers.values()) {
93
107
  layer.current = -1;
94
- for (const entry of layer.index.values()) {
95
- entry.reset();
108
+ for (const indexMap of layer.indexes.values()) {
109
+ for (const entry of indexMap.values()) {
110
+ entry.reset();
111
+ }
96
112
  }
97
113
  }
98
114
  }
package/src/graph/node.ts CHANGED
@@ -41,6 +41,23 @@ class Node extends ASTNode {
41
41
  public get properties(): Map<string, Expression> {
42
42
  return this._properties;
43
43
  }
44
+ public set properties(properties: Map<string, Expression>) {
45
+ this._properties = properties;
46
+ }
47
+ private _matchesProperties(hop: number = 0): boolean {
48
+ const data: NodeData = this._data!;
49
+ for (const [key, expression] of this._properties) {
50
+ const record: NodeRecord = data.current(hop)!;
51
+ if (record === null) {
52
+ throw new Error("No current node data available");
53
+ }
54
+ if (!(key in record)) {
55
+ throw new Error("Node does not have property");
56
+ }
57
+ return record[key] === expression.value();
58
+ }
59
+ return true;
60
+ }
44
61
  public setProperty(key: string, value: Expression): void {
45
62
  this._properties.set(key, value);
46
63
  }
@@ -72,6 +89,9 @@ class Node extends ASTNode {
72
89
  this._data?.reset();
73
90
  while (this._data?.next()) {
74
91
  this.setValue(this._data?.current()!);
92
+ if (!this._matchesProperties()) {
93
+ continue;
94
+ }
75
95
  await this._outgoing?.find(this._value!.id);
76
96
  await this.runTodoNext();
77
97
  }
@@ -80,6 +100,9 @@ class Node extends ASTNode {
80
100
  this._data?.reset();
81
101
  while (this._data?.find(id, hop)) {
82
102
  this.setValue(this._data?.current(hop) as NodeRecord);
103
+ if (!this._matchesProperties(hop)) {
104
+ continue;
105
+ }
83
106
  this._incoming?.setEndNode(this);
84
107
  await this._outgoing?.find(this._value!.id, hop);
85
108
  await this.runTodoNext();
@@ -8,7 +8,7 @@ class NodeData extends Data {
8
8
  super._buildIndex("id");
9
9
  }
10
10
  public find(id: string, hop: number = 0): boolean {
11
- return super._find(id, hop);
11
+ return super._find(id, hop, "id");
12
12
  }
13
13
  public current(hop: number = 0): NodeRecord | null {
14
14
  return super.current(hop) as NodeRecord | null;
@@ -18,6 +18,7 @@ class Relationship extends ASTNode {
18
18
 
19
19
  protected _source: Node | null = null;
20
20
  protected _target: Node | null = null;
21
+ protected _direction: "left" | "right" = "right";
21
22
 
22
23
  private _data: RelationshipData | null = null;
23
24
 
@@ -38,8 +39,25 @@ class Relationship extends ASTNode {
38
39
  public get type(): string | null {
39
40
  return this._type;
40
41
  }
41
- public get properties(): Record<string, any> {
42
- return this._data?.properties() || {};
42
+ public get properties(): Map<string, Expression> {
43
+ return this._properties;
44
+ }
45
+ public set properties(properties: Map<string, Expression>) {
46
+ this._properties = properties;
47
+ }
48
+ private _matchesProperties(hop: number = 0): boolean {
49
+ const data: RelationshipData = this._data!;
50
+ for (const [key, expression] of this._properties) {
51
+ const record: RelationshipRecord = data.current(hop)!;
52
+ if (record === null) {
53
+ throw new Error("No current relationship data available");
54
+ }
55
+ if (!(key in record)) {
56
+ throw new Error("Relationship does not have property");
57
+ }
58
+ return record[key] === expression.value();
59
+ }
60
+ return true;
43
61
  }
44
62
  public setProperty(key: string, value: Expression): void {
45
63
  this._properties.set(key, value);
@@ -69,6 +87,12 @@ class Relationship extends ASTNode {
69
87
  public get target(): Node | null {
70
88
  return this._target;
71
89
  }
90
+ public set direction(direction: "left" | "right") {
91
+ this._direction = direction;
92
+ }
93
+ public get direction(): "left" | "right" {
94
+ return this._direction;
95
+ }
72
96
  public value(): RelationshipMatchRecord | RelationshipMatchRecord[] | null {
73
97
  return this._value;
74
98
  }
@@ -87,6 +111,7 @@ class Relationship extends ASTNode {
87
111
  public async find(left_id: string, hop: number = 0): Promise<void> {
88
112
  // Save original source node
89
113
  const original = this._source;
114
+ const isLeft = this._direction === "left";
90
115
  if (hop > 0) {
91
116
  // For hops greater than 0, the source becomes the target of the previous hop
92
117
  this._source = this._target;
@@ -102,16 +127,23 @@ class Relationship extends ASTNode {
102
127
  await this._target.find(left_id, hop);
103
128
  }
104
129
  }
105
- while (this._data?.find(left_id, hop)) {
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)) {
106
135
  const data: RelationshipRecord = this._data?.current(hop) as RelationshipRecord;
107
136
  if (hop >= this.hops!.min) {
108
137
  this.setValue(this);
109
- await this._target?.find(data.right_id, hop);
138
+ if (!this._matchesProperties(hop)) {
139
+ continue;
140
+ }
141
+ await this._target?.find(data[followId], hop);
110
142
  if (this._matches.isCircular()) {
111
143
  throw new Error("Circular relationship detected");
112
144
  }
113
145
  if (hop + 1 < this.hops!.max) {
114
- await this.find(data.right_id, hop + 1);
146
+ await this.find(data[followId], hop + 1);
115
147
  }
116
148
  this._matches.pop();
117
149
  }
@@ -6,9 +6,13 @@ class RelationshipData extends Data {
6
6
  constructor(records: RelationshipRecord[] = []) {
7
7
  super(records);
8
8
  super._buildIndex("left_id");
9
+ super._buildIndex("right_id");
9
10
  }
10
11
  public find(left_id: string, hop: number = 0): boolean {
11
- return super._find(left_id, hop);
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");
12
16
  }
13
17
  /*
14
18
  ** Get the properties of the current relationship record
@@ -22,6 +26,9 @@ class RelationshipData extends Data {
22
26
  }
23
27
  return null;
24
28
  }
29
+ public current(hop: number = 0): RelationshipRecord | null {
30
+ return super.current(hop) as RelationshipRecord | null;
31
+ }
25
32
  }
26
33
 
27
34
  export default RelationshipData;
@@ -16,7 +16,7 @@ class RelationshipMatchCollector {
16
16
  type: relationship.type!,
17
17
  startNode: relationship.source?.value() || {},
18
18
  endNode: null,
19
- properties: relationship.properties,
19
+ properties: relationship.getData()?.properties() as Record<string, any>,
20
20
  };
21
21
  this._matches.push(match);
22
22
  this._nodeIds.push(match.startNode.id);
@@ -17,7 +17,8 @@ class RelationshipReference extends Relationship {
17
17
  const data: RelationshipRecord = this._reference!.getData()?.current(
18
18
  hop
19
19
  ) as RelationshipRecord;
20
- await this._target?.find(data.right_id, hop);
20
+ const followId = this._direction === "left" ? "left_id" : "right_id";
21
+ await this._target?.find(data[followId], hop);
21
22
  }
22
23
  }
23
24
 
package/src/index.ts CHANGED
@@ -5,6 +5,7 @@
5
5
  *
6
6
  * @packageDocumentation
7
7
  */
8
+
8
9
  import CommandLine from "./io/command_line";
9
10
 
10
11
  const commandLine = new CommandLine();
@@ -431,6 +431,7 @@ class Parser extends BaseParser {
431
431
  }
432
432
  this.skipWhitespaceAndComments();
433
433
  let node = new Node();
434
+ node.properties = new Map(this.parseProperties());
434
435
  node.label = label!;
435
436
  if (label !== null && identifier !== null) {
436
437
  node.identifier = identifier;
@@ -449,6 +450,47 @@ class Parser extends BaseParser {
449
450
  return node;
450
451
  }
451
452
 
453
+ private *parseProperties(): Iterable<[string, Expression]> {
454
+ let parts: number = 0;
455
+ while (true) {
456
+ this.skipWhitespaceAndComments();
457
+ if (!this.token.isOpeningBrace() && parts == 0) {
458
+ return;
459
+ } else if (!this.token.isOpeningBrace() && parts > 0) {
460
+ throw new Error("Expected opening brace");
461
+ }
462
+ this.setNextToken();
463
+ this.skipWhitespaceAndComments();
464
+ if (!this.token.isIdentifier()) {
465
+ throw new Error("Expected identifier");
466
+ }
467
+ const key: string = this.token.value!;
468
+ this.setNextToken();
469
+ this.skipWhitespaceAndComments();
470
+ if (!this.token.isColon()) {
471
+ throw new Error("Expected colon");
472
+ }
473
+ this.setNextToken();
474
+ this.skipWhitespaceAndComments();
475
+ const expression: Expression | null = this.parseExpression();
476
+ if (expression === null) {
477
+ throw new Error("Expected expression");
478
+ }
479
+ this.skipWhitespaceAndComments();
480
+ if (!this.token.isClosingBrace()) {
481
+ throw new Error("Expected closing brace");
482
+ }
483
+ this.setNextToken();
484
+ yield [key, expression];
485
+ this.skipWhitespaceAndComments();
486
+ if (!this.token.isComma()) {
487
+ break;
488
+ }
489
+ this.setNextToken();
490
+ parts++;
491
+ }
492
+ }
493
+
452
494
  private *parsePatterns(): IterableIterator<Pattern> {
453
495
  while (true) {
454
496
  let identifier: string | null = null;
@@ -538,7 +580,9 @@ class Parser extends BaseParser {
538
580
  }
539
581
 
540
582
  private parseRelationship(): Relationship | null {
583
+ let direction: "left" | "right" = "right";
541
584
  if (this.token.isLessThan() && this.peek()?.isSubtract()) {
585
+ direction = "left";
542
586
  this.setNextToken();
543
587
  this.setNextToken();
544
588
  } else if (this.token.isSubtract()) {
@@ -565,6 +609,7 @@ class Parser extends BaseParser {
565
609
  const type: string = this.token.value || "";
566
610
  this.setNextToken();
567
611
  const hops: Hops | null = this.parseRelationshipHops();
612
+ const properties: Map<string, Expression> = new Map(this.parseProperties());
568
613
  if (!this.token.isClosingBracket()) {
569
614
  throw new Error("Expected closing bracket for relationship definition");
570
615
  }
@@ -577,6 +622,8 @@ class Parser extends BaseParser {
577
622
  this.setNextToken();
578
623
  }
579
624
  let relationship = new Relationship();
625
+ relationship.direction = direction;
626
+ relationship.properties = properties;
580
627
  if (type !== null && variable !== null) {
581
628
  relationship.identifier = variable;
582
629
  this.variables.set(variable, relationship);
@@ -1282,3 +1282,181 @@ test("Test equality comparison", async () => {
1282
1282
  }
1283
1283
  }
1284
1284
  });
1285
+
1286
+ test("Test match with constraints", async () => {
1287
+ await new Runner(`
1288
+ CREATE VIRTUAL (:Employee) AS {
1289
+ unwind [
1290
+ {id: 1, name: 'Employee 1'},
1291
+ {id: 2, name: 'Employee 2'},
1292
+ {id: 3, name: 'Employee 3'},
1293
+ {id: 4, name: 'Employee 4'}
1294
+ ] as record
1295
+ RETURN record.id as id, record.name as name
1296
+ }
1297
+ `).run();
1298
+ const match = new Runner(`
1299
+ match (e:Employee{name:'Employee 1'})
1300
+ return e.name as name
1301
+ `);
1302
+ await match.run();
1303
+ const results = match.results;
1304
+ expect(results.length).toBe(1);
1305
+ expect(results[0].name).toBe("Employee 1");
1306
+ });
1307
+
1308
+ test("Test match with leftward relationship direction", async () => {
1309
+ await new Runner(`
1310
+ CREATE VIRTUAL (:Person) AS {
1311
+ unwind [
1312
+ {id: 1, name: 'Person 1'},
1313
+ {id: 2, name: 'Person 2'},
1314
+ {id: 3, name: 'Person 3'}
1315
+ ] as record
1316
+ RETURN record.id as id, record.name as name
1317
+ }
1318
+ `).run();
1319
+ await new Runner(`
1320
+ CREATE VIRTUAL (:Person)-[:REPORTS_TO]-(:Person) AS {
1321
+ unwind [
1322
+ {left_id: 2, right_id: 1},
1323
+ {left_id: 3, right_id: 1}
1324
+ ] as record
1325
+ RETURN record.left_id as left_id, record.right_id as right_id
1326
+ }
1327
+ `).run();
1328
+ // Rightward: left_id -> right_id (2->1, 3->1)
1329
+ const rightMatch = new Runner(`
1330
+ MATCH (a:Person)-[:REPORTS_TO]->(b:Person)
1331
+ RETURN a.name AS employee, b.name AS manager
1332
+ `);
1333
+ await rightMatch.run();
1334
+ const rightResults = rightMatch.results;
1335
+ expect(rightResults.length).toBe(2);
1336
+ expect(rightResults[0]).toEqual({ employee: "Person 2", manager: "Person 1" });
1337
+ expect(rightResults[1]).toEqual({ employee: "Person 3", manager: "Person 1" });
1338
+
1339
+ // Leftward: right_id -> left_id (1->2, 1->3) — reverse traversal
1340
+ const leftMatch = new Runner(`
1341
+ MATCH (m:Person)<-[:REPORTS_TO]-(e:Person)
1342
+ RETURN m.name AS manager, e.name AS employee
1343
+ `);
1344
+ await leftMatch.run();
1345
+ const leftResults = leftMatch.results;
1346
+ expect(leftResults.length).toBe(2);
1347
+ expect(leftResults[0]).toEqual({ manager: "Person 1", employee: "Person 2" });
1348
+ expect(leftResults[1]).toEqual({ manager: "Person 1", employee: "Person 3" });
1349
+ });
1350
+
1351
+ test("Test match with leftward direction produces same results as rightward with swapped data", async () => {
1352
+ await new Runner(`
1353
+ CREATE VIRTUAL (:City) AS {
1354
+ unwind [
1355
+ {id: 1, name: 'New York'},
1356
+ {id: 2, name: 'Boston'},
1357
+ {id: 3, name: 'Chicago'}
1358
+ ] as record
1359
+ RETURN record.id as id, record.name as name
1360
+ }
1361
+ `).run();
1362
+ await new Runner(`
1363
+ CREATE VIRTUAL (:City)-[:ROUTE]-(:City) AS {
1364
+ unwind [
1365
+ {left_id: 1, right_id: 2},
1366
+ {left_id: 1, right_id: 3}
1367
+ ] as record
1368
+ RETURN record.left_id as left_id, record.right_id as right_id
1369
+ }
1370
+ `).run();
1371
+ // Leftward from destination: find where right_id matches, follow left_id
1372
+ const match = new Runner(`
1373
+ MATCH (dest:City)<-[:ROUTE]-(origin:City)
1374
+ RETURN dest.name AS destination, origin.name AS origin
1375
+ `);
1376
+ await match.run();
1377
+ const results = match.results;
1378
+ expect(results.length).toBe(2);
1379
+ expect(results[0]).toEqual({ destination: "Boston", origin: "New York" });
1380
+ expect(results[1]).toEqual({ destination: "Chicago", origin: "New York" });
1381
+ });
1382
+
1383
+ test("Test match with leftward variable-length relationships", async () => {
1384
+ await new Runner(`
1385
+ CREATE VIRTUAL (:Person) AS {
1386
+ unwind [
1387
+ {id: 1, name: 'Person 1'},
1388
+ {id: 2, name: 'Person 2'},
1389
+ {id: 3, name: 'Person 3'}
1390
+ ] as record
1391
+ RETURN record.id as id, record.name as name
1392
+ }
1393
+ `).run();
1394
+ await new Runner(`
1395
+ CREATE VIRTUAL (:Person)-[:MANAGES]-(:Person) AS {
1396
+ unwind [
1397
+ {left_id: 1, right_id: 2},
1398
+ {left_id: 2, right_id: 3}
1399
+ ] as record
1400
+ RETURN record.left_id as left_id, record.right_id as right_id
1401
+ }
1402
+ `).run();
1403
+ // Leftward variable-length: traverse from right_id to left_id
1404
+ // Person 3 can reach Person 2 (1 hop) and Person 1 (2 hops)
1405
+ const match = new Runner(`
1406
+ MATCH (a:Person)<-[:MANAGES*]-(b:Person)
1407
+ RETURN a.name AS name1, b.name AS name2
1408
+ `);
1409
+ await match.run();
1410
+ const results = match.results;
1411
+ // Zero-hop results for all 3 persons + multi-hop results
1412
+ // Leftward indexes on right_id. find(id) looks up right_id=id, follows left_id.
1413
+ // right_id=1: no records → Person 1 zero-hop only
1414
+ // right_id=2: record {left_id:1, right_id:2} → Person 2 → Person 1, then recurse find(1) → no more
1415
+ // right_id=3: record {left_id:2, right_id:3} → Person 3 → Person 2, then recurse find(2) → Person 1
1416
+ expect(results.length).toBe(6);
1417
+ // Person 1: zero-hop
1418
+ expect(results[0]).toEqual({ name1: "Person 1", name2: "Person 1" });
1419
+ // Person 2: zero-hop, then reaches Person 1
1420
+ expect(results[1]).toEqual({ name1: "Person 2", name2: "Person 2" });
1421
+ expect(results[2]).toEqual({ name1: "Person 2", name2: "Person 1" });
1422
+ // Person 3: zero-hop, then reaches Person 2, then Person 1
1423
+ expect(results[3]).toEqual({ name1: "Person 3", name2: "Person 3" });
1424
+ expect(results[4]).toEqual({ name1: "Person 3", name2: "Person 2" });
1425
+ expect(results[5]).toEqual({ name1: "Person 3", name2: "Person 1" });
1426
+ });
1427
+
1428
+ test("Test match with leftward double graph pattern", async () => {
1429
+ await new Runner(`
1430
+ CREATE VIRTUAL (:Person) AS {
1431
+ unwind [
1432
+ {id: 1, name: 'Person 1'},
1433
+ {id: 2, name: 'Person 2'},
1434
+ {id: 3, name: 'Person 3'},
1435
+ {id: 4, name: 'Person 4'}
1436
+ ] as record
1437
+ RETURN record.id as id, record.name as name
1438
+ }
1439
+ `).run();
1440
+ await new Runner(`
1441
+ CREATE VIRTUAL (:Person)-[:KNOWS]-(:Person) AS {
1442
+ unwind [
1443
+ {left_id: 1, right_id: 2},
1444
+ {left_id: 2, right_id: 3},
1445
+ {left_id: 3, right_id: 4}
1446
+ ] as record
1447
+ RETURN record.left_id as left_id, record.right_id as right_id
1448
+ }
1449
+ `).run();
1450
+ // Leftward chain: (c)<-[:KNOWS]-(b)<-[:KNOWS]-(a)
1451
+ // First rel: find right_id=c, follow left_id to b
1452
+ // Second rel: find right_id=b, follow left_id to a
1453
+ const match = new Runner(`
1454
+ MATCH (c:Person)<-[:KNOWS]-(b:Person)<-[:KNOWS]-(a:Person)
1455
+ RETURN a.name AS name1, b.name AS name2, c.name AS name3
1456
+ `);
1457
+ await match.run();
1458
+ const results = match.results;
1459
+ expect(results.length).toBe(2);
1460
+ expect(results[0]).toEqual({ name1: "Person 1", name2: "Person 2", name3: "Person 3" });
1461
+ expect(results[1]).toEqual({ name1: "Person 2", name2: "Person 3", name3: "Person 4" });
1462
+ });
@@ -760,3 +760,35 @@ test("Test check pattern expression without NodeReference", () => {
760
760
  parser.parse("MATCH (a:Person) WHERE (:Person)-[:KNOWS]->(:Person) RETURN a");
761
761
  }).toThrow("PatternExpression must contain at least one NodeReference");
762
762
  });
763
+
764
+ test("Test node with properties", () => {
765
+ const parser = new Parser();
766
+ const ast = parser.parse("MATCH (a:Person{value: 'hello'}) return a");
767
+ // prettier-ignore
768
+ expect(ast.print()).toBe(
769
+ "ASTNode\n" +
770
+ "- Match\n" +
771
+ "- Return\n" +
772
+ "-- Expression (a)\n" +
773
+ "--- Reference (a)"
774
+ );
775
+ const match: Match = ast.firstChild() as Match;
776
+ const node: Node = match.patterns[0].chain[0] as Node;
777
+ expect(node.properties.get("value")?.value()).toBe("hello");
778
+ });
779
+
780
+ test("Test relationship with properties", () => {
781
+ const parser = new Parser();
782
+ const ast = parser.parse("MATCH (:Person)-[r:LIKES{since: 2022}]->(:Food) return a");
783
+ // prettier-ignore
784
+ expect(ast.print()).toBe(
785
+ "ASTNode\n" +
786
+ "- Match\n" +
787
+ "- Return\n" +
788
+ "-- Expression (a)\n" +
789
+ "--- Reference (a)"
790
+ );
791
+ const match: Match = ast.firstChild() as Match;
792
+ const relationship: Relationship = match.patterns[0].chain[1] as Relationship;
793
+ expect(relationship.properties.get("since")?.value()).toBe(2022);
794
+ });