flowquery 1.0.18 → 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 (158) hide show
  1. package/.gitattributes +3 -0
  2. package/.github/workflows/python-publish.yml +56 -4
  3. package/.github/workflows/release.yml +26 -19
  4. package/.husky/pre-commit +26 -0
  5. package/README.md +37 -32
  6. package/dist/flowquery.min.js +1 -1
  7. package/dist/graph/data.d.ts +5 -4
  8. package/dist/graph/data.d.ts.map +1 -1
  9. package/dist/graph/data.js +38 -20
  10. package/dist/graph/data.js.map +1 -1
  11. package/dist/graph/node.d.ts +2 -0
  12. package/dist/graph/node.d.ts.map +1 -1
  13. package/dist/graph/node.js +23 -0
  14. package/dist/graph/node.js.map +1 -1
  15. package/dist/graph/node_data.js +1 -1
  16. package/dist/graph/node_data.js.map +1 -1
  17. package/dist/graph/pattern.d.ts.map +1 -1
  18. package/dist/graph/pattern.js +11 -4
  19. package/dist/graph/pattern.js.map +1 -1
  20. package/dist/graph/relationship.d.ts +6 -1
  21. package/dist/graph/relationship.d.ts.map +1 -1
  22. package/dist/graph/relationship.js +43 -5
  23. package/dist/graph/relationship.js.map +1 -1
  24. package/dist/graph/relationship_data.d.ts +2 -0
  25. package/dist/graph/relationship_data.d.ts.map +1 -1
  26. package/dist/graph/relationship_data.js +8 -1
  27. package/dist/graph/relationship_data.js.map +1 -1
  28. package/dist/graph/relationship_match_collector.js +2 -2
  29. package/dist/graph/relationship_match_collector.js.map +1 -1
  30. package/dist/graph/relationship_reference.d.ts.map +1 -1
  31. package/dist/graph/relationship_reference.js +2 -1
  32. package/dist/graph/relationship_reference.js.map +1 -1
  33. package/dist/index.d.ts +1 -1
  34. package/dist/index.js +1 -1
  35. package/dist/parsing/parser.d.ts +6 -0
  36. package/dist/parsing/parser.d.ts.map +1 -1
  37. package/dist/parsing/parser.js +139 -72
  38. package/dist/parsing/parser.js.map +1 -1
  39. package/docs/flowquery.min.js +1 -1
  40. package/flowquery-py/misc/data/test.json +10 -0
  41. package/flowquery-py/misc/data/users.json +242 -0
  42. package/flowquery-py/notebooks/TestFlowQuery.ipynb +440 -0
  43. package/flowquery-py/pyproject.toml +48 -2
  44. package/flowquery-py/src/__init__.py +7 -5
  45. package/flowquery-py/src/compute/runner.py +14 -10
  46. package/flowquery-py/src/extensibility.py +8 -8
  47. package/flowquery-py/src/graph/__init__.py +7 -7
  48. package/flowquery-py/src/graph/data.py +38 -20
  49. package/flowquery-py/src/graph/database.py +10 -20
  50. package/flowquery-py/src/graph/node.py +50 -19
  51. package/flowquery-py/src/graph/node_data.py +1 -1
  52. package/flowquery-py/src/graph/node_reference.py +10 -11
  53. package/flowquery-py/src/graph/pattern.py +27 -37
  54. package/flowquery-py/src/graph/pattern_expression.py +13 -11
  55. package/flowquery-py/src/graph/patterns.py +2 -2
  56. package/flowquery-py/src/graph/physical_node.py +4 -3
  57. package/flowquery-py/src/graph/physical_relationship.py +5 -5
  58. package/flowquery-py/src/graph/relationship.py +62 -14
  59. package/flowquery-py/src/graph/relationship_data.py +7 -2
  60. package/flowquery-py/src/graph/relationship_match_collector.py +15 -10
  61. package/flowquery-py/src/graph/relationship_reference.py +4 -4
  62. package/flowquery-py/src/io/command_line.py +13 -14
  63. package/flowquery-py/src/parsing/__init__.py +2 -2
  64. package/flowquery-py/src/parsing/alias_option.py +1 -1
  65. package/flowquery-py/src/parsing/ast_node.py +21 -20
  66. package/flowquery-py/src/parsing/base_parser.py +7 -7
  67. package/flowquery-py/src/parsing/components/__init__.py +3 -3
  68. package/flowquery-py/src/parsing/components/from_.py +3 -1
  69. package/flowquery-py/src/parsing/components/headers.py +2 -2
  70. package/flowquery-py/src/parsing/components/null.py +2 -2
  71. package/flowquery-py/src/parsing/context.py +7 -7
  72. package/flowquery-py/src/parsing/data_structures/associative_array.py +7 -7
  73. package/flowquery-py/src/parsing/data_structures/json_array.py +3 -3
  74. package/flowquery-py/src/parsing/data_structures/key_value_pair.py +4 -4
  75. package/flowquery-py/src/parsing/data_structures/lookup.py +2 -2
  76. package/flowquery-py/src/parsing/data_structures/range_lookup.py +2 -2
  77. package/flowquery-py/src/parsing/expressions/__init__.py +16 -16
  78. package/flowquery-py/src/parsing/expressions/expression.py +16 -13
  79. package/flowquery-py/src/parsing/expressions/expression_map.py +9 -9
  80. package/flowquery-py/src/parsing/expressions/f_string.py +3 -3
  81. package/flowquery-py/src/parsing/expressions/identifier.py +4 -3
  82. package/flowquery-py/src/parsing/expressions/number.py +3 -3
  83. package/flowquery-py/src/parsing/expressions/operator.py +16 -16
  84. package/flowquery-py/src/parsing/expressions/reference.py +3 -3
  85. package/flowquery-py/src/parsing/expressions/string.py +2 -2
  86. package/flowquery-py/src/parsing/functions/__init__.py +17 -17
  87. package/flowquery-py/src/parsing/functions/aggregate_function.py +8 -8
  88. package/flowquery-py/src/parsing/functions/async_function.py +12 -9
  89. package/flowquery-py/src/parsing/functions/avg.py +4 -4
  90. package/flowquery-py/src/parsing/functions/collect.py +6 -6
  91. package/flowquery-py/src/parsing/functions/function.py +6 -6
  92. package/flowquery-py/src/parsing/functions/function_factory.py +31 -34
  93. package/flowquery-py/src/parsing/functions/function_metadata.py +10 -11
  94. package/flowquery-py/src/parsing/functions/functions.py +14 -6
  95. package/flowquery-py/src/parsing/functions/join.py +3 -3
  96. package/flowquery-py/src/parsing/functions/keys.py +3 -3
  97. package/flowquery-py/src/parsing/functions/predicate_function.py +8 -7
  98. package/flowquery-py/src/parsing/functions/predicate_sum.py +12 -7
  99. package/flowquery-py/src/parsing/functions/rand.py +2 -2
  100. package/flowquery-py/src/parsing/functions/range_.py +9 -4
  101. package/flowquery-py/src/parsing/functions/replace.py +2 -2
  102. package/flowquery-py/src/parsing/functions/round_.py +2 -2
  103. package/flowquery-py/src/parsing/functions/size.py +2 -2
  104. package/flowquery-py/src/parsing/functions/split.py +9 -4
  105. package/flowquery-py/src/parsing/functions/stringify.py +3 -3
  106. package/flowquery-py/src/parsing/functions/sum.py +4 -4
  107. package/flowquery-py/src/parsing/functions/to_json.py +2 -2
  108. package/flowquery-py/src/parsing/functions/type_.py +3 -3
  109. package/flowquery-py/src/parsing/functions/value_holder.py +1 -1
  110. package/flowquery-py/src/parsing/logic/__init__.py +2 -2
  111. package/flowquery-py/src/parsing/logic/case.py +0 -1
  112. package/flowquery-py/src/parsing/logic/when.py +3 -1
  113. package/flowquery-py/src/parsing/operations/__init__.py +10 -10
  114. package/flowquery-py/src/parsing/operations/aggregated_return.py +3 -5
  115. package/flowquery-py/src/parsing/operations/aggregated_with.py +4 -4
  116. package/flowquery-py/src/parsing/operations/call.py +6 -7
  117. package/flowquery-py/src/parsing/operations/create_node.py +5 -4
  118. package/flowquery-py/src/parsing/operations/create_relationship.py +5 -4
  119. package/flowquery-py/src/parsing/operations/group_by.py +18 -16
  120. package/flowquery-py/src/parsing/operations/load.py +21 -19
  121. package/flowquery-py/src/parsing/operations/match.py +8 -7
  122. package/flowquery-py/src/parsing/operations/operation.py +3 -3
  123. package/flowquery-py/src/parsing/operations/projection.py +6 -6
  124. package/flowquery-py/src/parsing/operations/return_op.py +9 -5
  125. package/flowquery-py/src/parsing/operations/unwind.py +3 -2
  126. package/flowquery-py/src/parsing/operations/where.py +9 -7
  127. package/flowquery-py/src/parsing/operations/with_op.py +2 -2
  128. package/flowquery-py/src/parsing/parser.py +178 -114
  129. package/flowquery-py/src/parsing/token_to_node.py +2 -2
  130. package/flowquery-py/src/tokenization/__init__.py +4 -4
  131. package/flowquery-py/src/tokenization/keyword.py +1 -1
  132. package/flowquery-py/src/tokenization/operator.py +1 -1
  133. package/flowquery-py/src/tokenization/string_walker.py +4 -4
  134. package/flowquery-py/src/tokenization/symbol.py +1 -1
  135. package/flowquery-py/src/tokenization/token.py +11 -11
  136. package/flowquery-py/src/tokenization/token_mapper.py +10 -9
  137. package/flowquery-py/src/tokenization/token_type.py +1 -1
  138. package/flowquery-py/src/tokenization/tokenizer.py +19 -19
  139. package/flowquery-py/src/tokenization/trie.py +18 -17
  140. package/flowquery-py/src/utils/__init__.py +1 -1
  141. package/flowquery-py/src/utils/object_utils.py +3 -3
  142. package/flowquery-py/src/utils/string_utils.py +12 -12
  143. package/flowquery-py/tests/compute/test_runner.py +214 -7
  144. package/flowquery-py/tests/parsing/test_parser.py +41 -0
  145. package/flowquery-vscode/flowQueryEngine/flowquery.min.js +1 -1
  146. package/package.json +1 -1
  147. package/src/graph/data.ts +38 -20
  148. package/src/graph/node.ts +23 -0
  149. package/src/graph/node_data.ts +1 -1
  150. package/src/graph/pattern.ts +13 -4
  151. package/src/graph/relationship.ts +45 -5
  152. package/src/graph/relationship_data.ts +8 -1
  153. package/src/graph/relationship_match_collector.ts +1 -1
  154. package/src/graph/relationship_reference.ts +2 -1
  155. package/src/index.ts +5 -5
  156. package/src/parsing/parser.ts +139 -71
  157. package/tests/compute/runner.test.ts +249 -79
  158. 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.18",
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
@@ -89,9 +103,13 @@ class Data {
89
103
  }
90
104
  }
91
105
  public reset(): void {
92
- this.layer(0).current = -1;
93
- for (const entry of this.layer(0).index.values()) {
94
- entry.reset();
106
+ for (const layer of this._layers.values()) {
107
+ layer.current = -1;
108
+ for (const indexMap of layer.indexes.values()) {
109
+ for (const entry of indexMap.values()) {
110
+ entry.reset();
111
+ }
112
+ }
95
113
  }
96
114
  }
97
115
  public next(level: number = 0): boolean {
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;
@@ -65,17 +65,26 @@ class Pattern extends ASTNode {
65
65
  return Array.from(this.values());
66
66
  }
67
67
  public *values(): Generator<any> {
68
- for (const element of this._chain) {
68
+ for (let i = 0; i < this._chain.length; i++) {
69
+ const element = this._chain[i];
69
70
  if (element instanceof Node) {
71
+ // Skip node if previous element was a zero-hop relationship (no matches)
72
+ if (
73
+ i > 0 &&
74
+ this._chain[i - 1] instanceof Relationship &&
75
+ (this._chain[i - 1] as Relationship).matches.length === 0
76
+ ) {
77
+ continue;
78
+ }
70
79
  yield element.value();
71
80
  } else if (element instanceof Relationship) {
72
- let i = 0;
81
+ let j = 0;
73
82
  for (const match of element.matches) {
74
83
  yield match;
75
- if (i < element.matches.length - 1) {
84
+ if (j < element.matches.length - 1) {
76
85
  yield match.endNode;
77
86
  }
78
- i++;
87
+ j++;
79
88
  }
80
89
  }
81
90
  }
@@ -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,23 +111,39 @@ 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;
93
118
  }
94
119
  if (hop === 0) {
95
120
  this._data?.reset();
121
+
122
+ // Handle zero-hop case: when min is 0 on a variable-length relationship,
123
+ // match source node as target (no traversal)
124
+ if (this.hops?.multi() && this.hops.min === 0 && this._target) {
125
+ // For zero-hop, target finds the same node as source (left_id)
126
+ // No relationship match is pushed since no edge is traversed
127
+ await this._target.find(left_id, hop);
128
+ }
96
129
  }
97
- 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)) {
98
135
  const data: RelationshipRecord = this._data?.current(hop) as RelationshipRecord;
99
136
  if (hop >= this.hops!.min) {
100
137
  this.setValue(this);
101
- await this._target?.find(data.right_id, hop);
138
+ if (!this._matchesProperties(hop)) {
139
+ continue;
140
+ }
141
+ await this._target?.find(data[followId], hop);
102
142
  if (this._matches.isCircular()) {
103
143
  throw new Error("Circular relationship detected");
104
144
  }
105
145
  if (hop + 1 < this.hops!.max) {
106
- await this.find(data.right_id, hop + 1);
146
+ await this.find(data[followId], hop + 1);
107
147
  }
108
148
  this._matches.pop();
109
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
@@ -1,12 +1,12 @@
1
1
  /**
2
2
  * FlowQuery - A declarative query language for data processing pipelines.
3
- *
4
- * This is the main entry point for the FlowQuery command-line interface.
5
- *
3
+ *
4
+ * Main entry point for the FlowQuery command-line interface.
5
+ *
6
6
  * @packageDocumentation
7
7
  */
8
8
 
9
- import CommandLine from './io/command_line';
9
+ import CommandLine from "./io/command_line";
10
10
 
11
11
  const commandLine = new CommandLine();
12
- commandLine.loop();
12
+ commandLine.loop();
@@ -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);
@@ -610,10 +657,11 @@ class Parser extends BaseParser {
610
657
  }
611
658
  this.setNextToken();
612
659
  if (!this.token.isNumber()) {
613
- throw new Error("Expected number for relationship hops");
660
+ hops.max = Number.MAX_SAFE_INTEGER;
661
+ } else {
662
+ hops.max = parseInt(this.token.value || "0");
663
+ this.setNextToken();
614
664
  }
615
- hops.max = parseInt(this.token.value || "0");
616
- this.setNextToken();
617
665
  }
618
666
  } else {
619
667
  hops.min = 0;
@@ -691,77 +739,97 @@ class Parser extends BaseParser {
691
739
  }
692
740
  }
693
741
 
742
+ /**
743
+ * Parse a single operand (without operators).
744
+ * @returns True if an operand was parsed, false otherwise.
745
+ */
746
+ private parseOperand(expression: Expression): boolean {
747
+ this.skipWhitespaceAndComments();
748
+ if (this.token.isIdentifier() && !this.peek()?.isLeftParenthesis()) {
749
+ const identifier: string = this.token.value || "";
750
+ const reference = new Reference(identifier, this.variables.get(identifier));
751
+ this.setNextToken();
752
+ const lookup = this.parseLookup(reference);
753
+ expression.addNode(lookup);
754
+ return true;
755
+ } else if (this.token.isIdentifier() && this.peek()?.isLeftParenthesis()) {
756
+ const func = this.parsePredicateFunction() || this.parseFunction();
757
+ if (func !== null) {
758
+ const lookup = this.parseLookup(func);
759
+ expression.addNode(lookup);
760
+ return true;
761
+ }
762
+ } else if (
763
+ this.token.isLeftParenthesis() &&
764
+ (this.peek()?.isIdentifier() ||
765
+ this.peek()?.isColon() ||
766
+ this.peek()?.isRightParenthesis())
767
+ ) {
768
+ // Possible graph pattern expression
769
+ const pattern = this.parsePatternExpression();
770
+ if (pattern !== null) {
771
+ expression.addNode(pattern);
772
+ return true;
773
+ }
774
+ } else if (this.token.isOperand()) {
775
+ expression.addNode(this.token.node);
776
+ this.setNextToken();
777
+ return true;
778
+ } else if (this.token.isFString()) {
779
+ const f_string = this.parseFString();
780
+ if (f_string === null) {
781
+ throw new Error("Expected f-string");
782
+ }
783
+ expression.addNode(f_string);
784
+ return true;
785
+ } else if (this.token.isLeftParenthesis()) {
786
+ this.setNextToken();
787
+ const sub = this.parseExpression();
788
+ if (sub === null) {
789
+ throw new Error("Expected expression");
790
+ }
791
+ if (!this.token.isRightParenthesis()) {
792
+ throw new Error("Expected right parenthesis");
793
+ }
794
+ this.setNextToken();
795
+ const lookup = this.parseLookup(sub);
796
+ expression.addNode(lookup);
797
+ return true;
798
+ } else if (this.token.isOpeningBrace() || this.token.isOpeningBracket()) {
799
+ const json = this.parseJSON();
800
+ if (json === null) {
801
+ throw new Error("Expected JSON object");
802
+ }
803
+ const lookup = this.parseLookup(json);
804
+ expression.addNode(lookup);
805
+ return true;
806
+ } else if (this.token.isCase()) {
807
+ const _case = this.parseCase();
808
+ if (_case === null) {
809
+ throw new Error("Expected CASE statement");
810
+ }
811
+ expression.addNode(_case);
812
+ return true;
813
+ } else if (this.token.isNot()) {
814
+ const not = new Not();
815
+ this.setNextToken();
816
+ // NOT should only bind to the next operand, not the entire expression
817
+ const tempExpr = new Expression();
818
+ if (!this.parseOperand(tempExpr)) {
819
+ throw new Error("Expected expression after NOT");
820
+ }
821
+ tempExpr.finish();
822
+ not.addChild(tempExpr);
823
+ expression.addNode(not);
824
+ return true;
825
+ }
826
+ return false;
827
+ }
828
+
694
829
  private parseExpression(): Expression | null {
695
830
  const expression = new Expression();
696
831
  while (true) {
697
- this.skipWhitespaceAndComments();
698
- if (this.token.isIdentifier() && !this.peek()?.isLeftParenthesis()) {
699
- const identifier: string = this.token.value || "";
700
- const reference = new Reference(identifier, this.variables.get(identifier));
701
- this.setNextToken();
702
- const lookup = this.parseLookup(reference);
703
- expression.addNode(lookup);
704
- } else if (this.token.isIdentifier() && this.peek()?.isLeftParenthesis()) {
705
- const func = this.parsePredicateFunction() || this.parseFunction();
706
- if (func !== null) {
707
- const lookup = this.parseLookup(func);
708
- expression.addNode(lookup);
709
- }
710
- } else if (
711
- this.token.isLeftParenthesis() &&
712
- (this.peek()?.isIdentifier() ||
713
- this.peek()?.isColon() ||
714
- this.peek()?.isRightParenthesis())
715
- ) {
716
- // Possible graph pattern expression
717
- const pattern = this.parsePatternExpression();
718
- if (pattern !== null) {
719
- expression.addNode(pattern);
720
- }
721
- } else if (this.token.isOperand()) {
722
- expression.addNode(this.token.node);
723
- this.setNextToken();
724
- } else if (this.token.isFString()) {
725
- const f_string = this.parseFString();
726
- if (f_string === null) {
727
- throw new Error("Expected f-string");
728
- }
729
- expression.addNode(f_string);
730
- } else if (this.token.isLeftParenthesis()) {
731
- this.setNextToken();
732
- const sub = this.parseExpression();
733
- if (sub === null) {
734
- throw new Error("Expected expression");
735
- }
736
- if (!this.token.isRightParenthesis()) {
737
- throw new Error("Expected right parenthesis");
738
- }
739
- this.setNextToken();
740
- const lookup = this.parseLookup(sub);
741
- expression.addNode(lookup);
742
- } else if (this.token.isOpeningBrace() || this.token.isOpeningBracket()) {
743
- const json = this.parseJSON();
744
- if (json === null) {
745
- throw new Error("Expected JSON object");
746
- }
747
- const lookup = this.parseLookup(json);
748
- expression.addNode(lookup);
749
- } else if (this.token.isCase()) {
750
- const _case = this.parseCase();
751
- if (_case === null) {
752
- throw new Error("Expected CASE statement");
753
- }
754
- expression.addNode(_case);
755
- } else if (this.token.isNot()) {
756
- const not = new Not();
757
- this.setNextToken();
758
- const sub = this.parseExpression();
759
- if (sub === null) {
760
- throw new Error("Expected expression");
761
- }
762
- not.addChild(sub);
763
- expression.addNode(not);
764
- } else {
832
+ if (!this.parseOperand(expression)) {
765
833
  if (expression.nodesAdded()) {
766
834
  throw new Error("Expected operand or left parenthesis");
767
835
  } else {