flowquery 1.0.20 → 1.0.22
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/database.d.ts +1 -0
- package/dist/graph/database.d.ts.map +1 -1
- package/dist/graph/database.js +39 -0
- package/dist/graph/database.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/functions/function_factory.d.ts +1 -0
- package/dist/parsing/functions/function_factory.d.ts.map +1 -1
- package/dist/parsing/functions/function_factory.js +1 -0
- package/dist/parsing/functions/function_factory.js.map +1 -1
- package/dist/parsing/functions/schema.d.ts +17 -0
- package/dist/parsing/functions/schema.d.ts.map +1 -0
- package/dist/parsing/functions/schema.js +62 -0
- package/dist/parsing/functions/schema.js.map +1 -0
- 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 +34 -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 +19 -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/schema.py +36 -0
- 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 +250 -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/database.ts +30 -0
- 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/functions/function_factory.ts +1 -0
- package/src/parsing/functions/schema.ts +36 -0
- package/src/parsing/parser.ts +47 -0
- package/tests/compute/runner.test.ts +218 -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/database.ts
CHANGED
|
@@ -39,6 +39,36 @@ class Database {
|
|
|
39
39
|
public getRelationship(relationship: Relationship): PhysicalRelationship | null {
|
|
40
40
|
return Database.relationships.get(relationship.type!) || null;
|
|
41
41
|
}
|
|
42
|
+
public async schema(): Promise<Record<string, any>[]> {
|
|
43
|
+
const result: Record<string, any>[] = [];
|
|
44
|
+
|
|
45
|
+
for (const [label, physical] of Database.nodes) {
|
|
46
|
+
const records = await physical.data();
|
|
47
|
+
const entry: Record<string, any> = { kind: "node", label };
|
|
48
|
+
if (records.length > 0) {
|
|
49
|
+
const { id, ...sample } = records[0];
|
|
50
|
+
if (Object.keys(sample).length > 0) {
|
|
51
|
+
entry.sample = sample;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
result.push(entry);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
for (const [type, physical] of Database.relationships) {
|
|
58
|
+
const records = await physical.data();
|
|
59
|
+
const entry: Record<string, any> = { kind: "relationship", type };
|
|
60
|
+
if (records.length > 0) {
|
|
61
|
+
const { left_id, right_id, ...sample } = records[0];
|
|
62
|
+
if (Object.keys(sample).length > 0) {
|
|
63
|
+
entry.sample = sample;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
result.push(entry);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
|
|
42
72
|
public async getData(element: Node | Relationship): Promise<NodeData | RelationshipData> {
|
|
43
73
|
if (element instanceof Node) {
|
|
44
74
|
const node = this.getNode(element);
|
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
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import Database from "../../graph/database";
|
|
2
|
+
import AsyncFunction from "./async_function";
|
|
3
|
+
import { FunctionDef } from "./function_metadata";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Built-in function that returns the graph schema of the database.
|
|
7
|
+
*
|
|
8
|
+
* Lists all nodes and relationships with their labels/types and a sample
|
|
9
|
+
* of their data (excluding id from nodes, left_id and right_id from relationships).
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```
|
|
13
|
+
* LOAD FROM schema() AS s RETURN s
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
@FunctionDef({
|
|
17
|
+
description:
|
|
18
|
+
"Returns the graph schema listing all nodes and relationships with a sample of their data.",
|
|
19
|
+
category: "async",
|
|
20
|
+
parameters: [],
|
|
21
|
+
output: {
|
|
22
|
+
description: "Schema entry with kind, label/type, and optional sample data",
|
|
23
|
+
type: "object",
|
|
24
|
+
},
|
|
25
|
+
examples: ["LOAD FROM schema() AS s RETURN s"],
|
|
26
|
+
})
|
|
27
|
+
class Schema extends AsyncFunction {
|
|
28
|
+
public async *generate(): AsyncGenerator<any> {
|
|
29
|
+
const entries = await Database.getInstance().schema();
|
|
30
|
+
for (const entry of entries) {
|
|
31
|
+
yield entry;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export default Schema;
|
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,221 @@ 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
|
+
});
|
|
1463
|
+
|
|
1464
|
+
test("Test schema() returns nodes and relationships with sample data", async () => {
|
|
1465
|
+
await new Runner(`
|
|
1466
|
+
CREATE VIRTUAL (:Animal) AS {
|
|
1467
|
+
UNWIND [
|
|
1468
|
+
{id: 1, species: 'Cat', legs: 4},
|
|
1469
|
+
{id: 2, species: 'Dog', legs: 4}
|
|
1470
|
+
] AS record
|
|
1471
|
+
RETURN record.id AS id, record.species AS species, record.legs AS legs
|
|
1472
|
+
}
|
|
1473
|
+
`).run();
|
|
1474
|
+
await new Runner(`
|
|
1475
|
+
CREATE VIRTUAL (:Animal)-[:CHASES]-(:Animal) AS {
|
|
1476
|
+
UNWIND [
|
|
1477
|
+
{left_id: 2, right_id: 1, speed: 'fast'}
|
|
1478
|
+
] AS record
|
|
1479
|
+
RETURN record.left_id AS left_id, record.right_id AS right_id, record.speed AS speed
|
|
1480
|
+
}
|
|
1481
|
+
`).run();
|
|
1482
|
+
|
|
1483
|
+
const runner = new Runner(
|
|
1484
|
+
"CALL schema() YIELD kind, label, type, sample RETURN kind, label, type, sample"
|
|
1485
|
+
);
|
|
1486
|
+
await runner.run();
|
|
1487
|
+
const results = runner.results;
|
|
1488
|
+
|
|
1489
|
+
const animal = results.find((r: any) => r.kind === "node" && r.label === "Animal");
|
|
1490
|
+
expect(animal).toBeDefined();
|
|
1491
|
+
expect(animal.sample).toBeDefined();
|
|
1492
|
+
expect(animal.sample).not.toHaveProperty("id");
|
|
1493
|
+
expect(animal.sample).toHaveProperty("species");
|
|
1494
|
+
expect(animal.sample).toHaveProperty("legs");
|
|
1495
|
+
|
|
1496
|
+
const chases = results.find((r: any) => r.kind === "relationship" && r.type === "CHASES");
|
|
1497
|
+
expect(chases).toBeDefined();
|
|
1498
|
+
expect(chases.sample).toBeDefined();
|
|
1499
|
+
expect(chases.sample).not.toHaveProperty("left_id");
|
|
1500
|
+
expect(chases.sample).not.toHaveProperty("right_id");
|
|
1501
|
+
expect(chases.sample).toHaveProperty("speed");
|
|
1502
|
+
});
|