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.
- package/.gitattributes +3 -0
- package/.github/workflows/python-publish.yml +56 -4
- package/.github/workflows/release.yml +26 -19
- package/.husky/pre-commit +26 -0
- package/README.md +37 -32
- 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 +38 -20
- 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/pattern.d.ts.map +1 -1
- package/dist/graph/pattern.js +11 -4
- package/dist/graph/pattern.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 +43 -5
- 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 +1 -1
- package/dist/index.js +1 -1
- package/dist/parsing/parser.d.ts +6 -0
- package/dist/parsing/parser.d.ts.map +1 -1
- package/dist/parsing/parser.js +139 -72
- package/dist/parsing/parser.js.map +1 -1
- package/docs/flowquery.min.js +1 -1
- package/flowquery-py/misc/data/test.json +10 -0
- package/flowquery-py/misc/data/users.json +242 -0
- package/flowquery-py/notebooks/TestFlowQuery.ipynb +440 -0
- package/flowquery-py/pyproject.toml +48 -2
- package/flowquery-py/src/__init__.py +7 -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 +38 -20
- 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 +27 -37
- 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 +62 -14
- 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 +178 -114
- 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 +214 -7
- 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 +38 -20
- package/src/graph/node.ts +23 -0
- package/src/graph/node_data.ts +1 -1
- package/src/graph/pattern.ts +13 -4
- package/src/graph/relationship.ts +45 -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 +5 -5
- package/src/parsing/parser.ts +139 -71
- package/tests/compute/runner.test.ts +249 -79
- 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
|
|
@@ -89,9 +103,13 @@ class Data {
|
|
|
89
103
|
}
|
|
90
104
|
}
|
|
91
105
|
public reset(): void {
|
|
92
|
-
this.
|
|
93
|
-
|
|
94
|
-
|
|
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();
|
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;
|
package/src/graph/pattern.ts
CHANGED
|
@@ -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 (
|
|
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
|
|
81
|
+
let j = 0;
|
|
73
82
|
for (const match of element.matches) {
|
|
74
83
|
yield match;
|
|
75
|
-
if (
|
|
84
|
+
if (j < element.matches.length - 1) {
|
|
76
85
|
yield match.endNode;
|
|
77
86
|
}
|
|
78
|
-
|
|
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():
|
|
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,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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
*
|
|
5
|
-
*
|
|
3
|
+
*
|
|
4
|
+
* Main entry point for the FlowQuery command-line interface.
|
|
5
|
+
*
|
|
6
6
|
* @packageDocumentation
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import CommandLine from
|
|
9
|
+
import CommandLine from "./io/command_line";
|
|
10
10
|
|
|
11
11
|
const commandLine = new CommandLine();
|
|
12
|
-
commandLine.loop();
|
|
12
|
+
commandLine.loop();
|
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);
|
|
@@ -610,10 +657,11 @@ class Parser extends BaseParser {
|
|
|
610
657
|
}
|
|
611
658
|
this.setNextToken();
|
|
612
659
|
if (!this.token.isNumber()) {
|
|
613
|
-
|
|
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.
|
|
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 {
|