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.
- package/.github/workflows/release.yml +2 -2
- package/.husky/pre-commit +26 -0
- package/dist/flowquery.min.js +1 -1
- package/dist/graph/data.d.ts +5 -4
- package/dist/graph/data.d.ts.map +1 -1
- package/dist/graph/data.js +35 -19
- package/dist/graph/data.js.map +1 -1
- package/dist/graph/node.d.ts +2 -0
- package/dist/graph/node.d.ts.map +1 -1
- package/dist/graph/node.js +23 -0
- package/dist/graph/node.js.map +1 -1
- package/dist/graph/node_data.js +1 -1
- package/dist/graph/node_data.js.map +1 -1
- package/dist/graph/relationship.d.ts +6 -1
- package/dist/graph/relationship.d.ts.map +1 -1
- package/dist/graph/relationship.js +38 -7
- package/dist/graph/relationship.js.map +1 -1
- package/dist/graph/relationship_data.d.ts +2 -0
- package/dist/graph/relationship_data.d.ts.map +1 -1
- package/dist/graph/relationship_data.js +8 -1
- package/dist/graph/relationship_data.js.map +1 -1
- package/dist/graph/relationship_match_collector.js +2 -2
- package/dist/graph/relationship_match_collector.js.map +1 -1
- package/dist/graph/relationship_reference.d.ts.map +1 -1
- package/dist/graph/relationship_reference.js +2 -1
- package/dist/graph/relationship_reference.js.map +1 -1
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- package/dist/parsing/parser.d.ts +1 -0
- package/dist/parsing/parser.d.ts.map +1 -1
- package/dist/parsing/parser.js +47 -0
- package/dist/parsing/parser.js.map +1 -1
- package/docs/flowquery.min.js +1 -1
- package/flowquery-py/notebooks/TestFlowQuery.ipynb +1 -1
- package/flowquery-py/pyproject.toml +45 -2
- package/flowquery-py/src/__init__.py +5 -5
- package/flowquery-py/src/compute/runner.py +14 -10
- package/flowquery-py/src/extensibility.py +8 -8
- package/flowquery-py/src/graph/__init__.py +7 -7
- package/flowquery-py/src/graph/data.py +36 -19
- package/flowquery-py/src/graph/database.py +10 -20
- package/flowquery-py/src/graph/node.py +50 -19
- package/flowquery-py/src/graph/node_data.py +1 -1
- package/flowquery-py/src/graph/node_reference.py +10 -11
- package/flowquery-py/src/graph/pattern.py +23 -36
- package/flowquery-py/src/graph/pattern_expression.py +13 -11
- package/flowquery-py/src/graph/patterns.py +2 -2
- package/flowquery-py/src/graph/physical_node.py +4 -3
- package/flowquery-py/src/graph/physical_relationship.py +5 -5
- package/flowquery-py/src/graph/relationship.py +56 -15
- package/flowquery-py/src/graph/relationship_data.py +7 -2
- package/flowquery-py/src/graph/relationship_match_collector.py +15 -10
- package/flowquery-py/src/graph/relationship_reference.py +4 -4
- package/flowquery-py/src/io/command_line.py +13 -14
- package/flowquery-py/src/parsing/__init__.py +2 -2
- package/flowquery-py/src/parsing/alias_option.py +1 -1
- package/flowquery-py/src/parsing/ast_node.py +21 -20
- package/flowquery-py/src/parsing/base_parser.py +7 -7
- package/flowquery-py/src/parsing/components/__init__.py +3 -3
- package/flowquery-py/src/parsing/components/from_.py +3 -1
- package/flowquery-py/src/parsing/components/headers.py +2 -2
- package/flowquery-py/src/parsing/components/null.py +2 -2
- package/flowquery-py/src/parsing/context.py +7 -7
- package/flowquery-py/src/parsing/data_structures/associative_array.py +7 -7
- package/flowquery-py/src/parsing/data_structures/json_array.py +3 -3
- package/flowquery-py/src/parsing/data_structures/key_value_pair.py +4 -4
- package/flowquery-py/src/parsing/data_structures/lookup.py +2 -2
- package/flowquery-py/src/parsing/data_structures/range_lookup.py +2 -2
- package/flowquery-py/src/parsing/expressions/__init__.py +16 -16
- package/flowquery-py/src/parsing/expressions/expression.py +16 -13
- package/flowquery-py/src/parsing/expressions/expression_map.py +9 -9
- package/flowquery-py/src/parsing/expressions/f_string.py +3 -3
- package/flowquery-py/src/parsing/expressions/identifier.py +4 -3
- package/flowquery-py/src/parsing/expressions/number.py +3 -3
- package/flowquery-py/src/parsing/expressions/operator.py +16 -16
- package/flowquery-py/src/parsing/expressions/reference.py +3 -3
- package/flowquery-py/src/parsing/expressions/string.py +2 -2
- package/flowquery-py/src/parsing/functions/__init__.py +17 -17
- package/flowquery-py/src/parsing/functions/aggregate_function.py +8 -8
- package/flowquery-py/src/parsing/functions/async_function.py +12 -9
- package/flowquery-py/src/parsing/functions/avg.py +4 -4
- package/flowquery-py/src/parsing/functions/collect.py +6 -6
- package/flowquery-py/src/parsing/functions/function.py +6 -6
- package/flowquery-py/src/parsing/functions/function_factory.py +31 -34
- package/flowquery-py/src/parsing/functions/function_metadata.py +10 -11
- package/flowquery-py/src/parsing/functions/functions.py +14 -6
- package/flowquery-py/src/parsing/functions/join.py +3 -3
- package/flowquery-py/src/parsing/functions/keys.py +3 -3
- package/flowquery-py/src/parsing/functions/predicate_function.py +8 -7
- package/flowquery-py/src/parsing/functions/predicate_sum.py +12 -7
- package/flowquery-py/src/parsing/functions/rand.py +2 -2
- package/flowquery-py/src/parsing/functions/range_.py +9 -4
- package/flowquery-py/src/parsing/functions/replace.py +2 -2
- package/flowquery-py/src/parsing/functions/round_.py +2 -2
- package/flowquery-py/src/parsing/functions/size.py +2 -2
- package/flowquery-py/src/parsing/functions/split.py +9 -4
- package/flowquery-py/src/parsing/functions/stringify.py +3 -3
- package/flowquery-py/src/parsing/functions/sum.py +4 -4
- package/flowquery-py/src/parsing/functions/to_json.py +2 -2
- package/flowquery-py/src/parsing/functions/type_.py +3 -3
- package/flowquery-py/src/parsing/functions/value_holder.py +1 -1
- package/flowquery-py/src/parsing/logic/__init__.py +2 -2
- package/flowquery-py/src/parsing/logic/case.py +0 -1
- package/flowquery-py/src/parsing/logic/when.py +3 -1
- package/flowquery-py/src/parsing/operations/__init__.py +10 -10
- package/flowquery-py/src/parsing/operations/aggregated_return.py +3 -5
- package/flowquery-py/src/parsing/operations/aggregated_with.py +4 -4
- package/flowquery-py/src/parsing/operations/call.py +6 -7
- package/flowquery-py/src/parsing/operations/create_node.py +5 -4
- package/flowquery-py/src/parsing/operations/create_relationship.py +5 -4
- package/flowquery-py/src/parsing/operations/group_by.py +18 -16
- package/flowquery-py/src/parsing/operations/load.py +21 -19
- package/flowquery-py/src/parsing/operations/match.py +8 -7
- package/flowquery-py/src/parsing/operations/operation.py +3 -3
- package/flowquery-py/src/parsing/operations/projection.py +6 -6
- package/flowquery-py/src/parsing/operations/return_op.py +9 -5
- package/flowquery-py/src/parsing/operations/unwind.py +3 -2
- package/flowquery-py/src/parsing/operations/where.py +9 -7
- package/flowquery-py/src/parsing/operations/with_op.py +2 -2
- package/flowquery-py/src/parsing/parser.py +104 -57
- package/flowquery-py/src/parsing/token_to_node.py +2 -2
- package/flowquery-py/src/tokenization/__init__.py +4 -4
- package/flowquery-py/src/tokenization/keyword.py +1 -1
- package/flowquery-py/src/tokenization/operator.py +1 -1
- package/flowquery-py/src/tokenization/string_walker.py +4 -4
- package/flowquery-py/src/tokenization/symbol.py +1 -1
- package/flowquery-py/src/tokenization/token.py +11 -11
- package/flowquery-py/src/tokenization/token_mapper.py +10 -9
- package/flowquery-py/src/tokenization/token_type.py +1 -1
- package/flowquery-py/src/tokenization/tokenizer.py +19 -19
- package/flowquery-py/src/tokenization/trie.py +18 -17
- package/flowquery-py/src/utils/__init__.py +1 -1
- package/flowquery-py/src/utils/object_utils.py +3 -3
- package/flowquery-py/src/utils/string_utils.py +12 -12
- package/flowquery-py/tests/compute/test_runner.py +205 -1
- package/flowquery-py/tests/parsing/test_parser.py +41 -0
- package/flowquery-vscode/flowQueryEngine/flowquery.min.js +1 -1
- package/package.json +1 -1
- package/src/graph/data.ts +35 -19
- package/src/graph/node.ts +23 -0
- package/src/graph/node_data.ts +1 -1
- package/src/graph/relationship.ts +37 -5
- package/src/graph/relationship_data.ts +8 -1
- package/src/graph/relationship_match_collector.ts +1 -1
- package/src/graph/relationship_reference.ts +2 -1
- package/src/index.ts +1 -0
- package/src/parsing/parser.ts +47 -0
- package/tests/compute/runner.test.ts +178 -0
- package/tests/parsing/parser.test.ts +32 -0
package/package.json
CHANGED
package/src/graph/data.ts
CHANGED
|
@@ -27,13 +27,19 @@ class IndexEntry {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
class Layer {
|
|
30
|
-
private
|
|
30
|
+
private _indexes: Map<string, Map<string, IndexEntry>> = new Map();
|
|
31
31
|
private _current: number = -1;
|
|
32
|
-
constructor(
|
|
33
|
-
this.
|
|
32
|
+
constructor(indexes: Map<string, Map<string, IndexEntry>>) {
|
|
33
|
+
this._indexes = indexes;
|
|
34
34
|
}
|
|
35
|
-
public
|
|
36
|
-
|
|
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
|
|
56
|
-
|
|
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 (!
|
|
59
|
-
|
|
65
|
+
if (!idx.has(record[key])) {
|
|
66
|
+
idx.set(record[key], new IndexEntry());
|
|
60
67
|
}
|
|
61
|
-
|
|
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
|
|
69
|
-
for (const [
|
|
70
|
-
|
|
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(
|
|
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
|
-
|
|
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 =
|
|
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
|
|
95
|
-
entry.
|
|
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();
|
package/src/graph/node_data.ts
CHANGED
|
@@ -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():
|
|
42
|
-
return this.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
package/src/parsing/parser.ts
CHANGED
|
@@ -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
|
+
});
|