flowquery 1.0.29 → 1.0.31
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/dist/flowquery.min.js +1 -1
- package/dist/graph/node_reference.d.ts +3 -2
- package/dist/graph/node_reference.d.ts.map +1 -1
- package/dist/graph/node_reference.js.map +1 -1
- package/dist/graph/relationship.d.ts +2 -1
- package/dist/graph/relationship.d.ts.map +1 -1
- package/dist/graph/relationship.js +15 -15
- package/dist/graph/relationship.js.map +1 -1
- package/dist/graph/relationship_data.d.ts +1 -2
- package/dist/graph/relationship_data.d.ts.map +1 -1
- package/dist/graph/relationship_data.js +2 -5
- package/dist/graph/relationship_data.js.map +1 -1
- package/dist/graph/relationship_match_collector.d.ts +2 -2
- package/dist/graph/relationship_match_collector.d.ts.map +1 -1
- package/dist/graph/relationship_match_collector.js +6 -7
- package/dist/graph/relationship_match_collector.js.map +1 -1
- package/dist/parsing/parser.d.ts +12 -0
- package/dist/parsing/parser.d.ts.map +1 -1
- package/dist/parsing/parser.js +35 -6
- package/dist/parsing/parser.js.map +1 -1
- package/docs/flowquery.min.js +1 -1
- package/flowquery-py/pyproject.toml +1 -1
- package/flowquery-py/src/graph/node_reference.py +5 -4
- package/flowquery-py/src/graph/relationship.py +20 -22
- package/flowquery-py/src/graph/relationship_data.py +4 -7
- package/flowquery-py/src/graph/relationship_match_collector.py +5 -7
- package/flowquery-py/src/parsing/operations/group_by.py +13 -2
- package/flowquery-py/src/parsing/parser.py +30 -7
- package/flowquery-py/tests/compute/test_runner.py +46 -5
- package/flowquery-py/tests/parsing/test_parser.py +58 -0
- package/flowquery-vscode/flowQueryEngine/flowquery.min.js +1 -1
- package/package.json +1 -1
- package/src/graph/node_reference.ts +4 -3
- package/src/graph/relationship.ts +15 -15
- package/src/graph/relationship_data.ts +2 -5
- package/src/graph/relationship_match_collector.ts +6 -7
- package/src/parsing/parser.ts +41 -7
- package/tests/compute/runner.test.ts +109 -4
- package/tests/parsing/parser.test.ts +60 -2
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import ASTNode from "../parsing/ast_node";
|
|
1
2
|
import Node from "./node";
|
|
2
3
|
|
|
3
4
|
class NodeReference extends Node {
|
|
4
|
-
private _reference:
|
|
5
|
-
constructor(base: Node, reference:
|
|
5
|
+
private _reference: ASTNode | null = null;
|
|
6
|
+
constructor(base: Node, reference: ASTNode) {
|
|
6
7
|
super();
|
|
7
8
|
this._identifier = base.identifier;
|
|
8
9
|
this._label = base.label;
|
|
@@ -11,7 +12,7 @@ class NodeReference extends Node {
|
|
|
11
12
|
this._incoming = base.incoming;
|
|
12
13
|
this._reference = reference;
|
|
13
14
|
}
|
|
14
|
-
public get reference():
|
|
15
|
+
public get reference(): ASTNode | null {
|
|
15
16
|
return this._reference;
|
|
16
17
|
}
|
|
17
18
|
public async next(): Promise<void> {
|
|
@@ -71,8 +71,8 @@ class Relationship extends ASTNode {
|
|
|
71
71
|
public get hops(): Hops | null {
|
|
72
72
|
return this._hops;
|
|
73
73
|
}
|
|
74
|
-
public setValue(relationship: Relationship): void {
|
|
75
|
-
const match: RelationshipMatchRecord = this._matches.push(relationship);
|
|
74
|
+
public setValue(relationship: Relationship, traversalId: string = ""): void {
|
|
75
|
+
const match: RelationshipMatchRecord = this._matches.push(relationship, traversalId);
|
|
76
76
|
this._value = this._matches.value();
|
|
77
77
|
}
|
|
78
78
|
public set source(node: Node | null) {
|
|
@@ -108,10 +108,12 @@ class Relationship extends ASTNode {
|
|
|
108
108
|
public setEndNode(node: Node): void {
|
|
109
109
|
this._matches.endNode = node;
|
|
110
110
|
}
|
|
111
|
+
public _left_id_or_right_id(): string {
|
|
112
|
+
return this._direction === "left" ? "left_id" : "right_id";
|
|
113
|
+
}
|
|
111
114
|
public async find(left_id: string, hop: number = 0): Promise<void> {
|
|
112
115
|
// Save original source node
|
|
113
116
|
const original = this._source;
|
|
114
|
-
const isLeft = this._direction === "left";
|
|
115
117
|
if (hop > 0) {
|
|
116
118
|
// For hops greater than 0, the source becomes the target of the previous hop
|
|
117
119
|
this._source = this._target;
|
|
@@ -127,28 +129,26 @@ class Relationship extends ASTNode {
|
|
|
127
129
|
await this._target.find(left_id, hop);
|
|
128
130
|
}
|
|
129
131
|
}
|
|
130
|
-
|
|
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)) {
|
|
132
|
+
while (this._data!.find(left_id, hop, this._direction)) {
|
|
135
133
|
const data: RelationshipRecord = this._data?.current(hop) as RelationshipRecord;
|
|
134
|
+
const id = data[this._left_id_or_right_id()];
|
|
136
135
|
if (hop + 1 >= this.hops!.min) {
|
|
137
|
-
this.setValue(this);
|
|
136
|
+
this.setValue(this, left_id);
|
|
138
137
|
if (!this._matchesProperties(hop)) {
|
|
139
138
|
continue;
|
|
140
139
|
}
|
|
141
|
-
await this._target?.find(
|
|
142
|
-
if (this._matches.isCircular()) {
|
|
143
|
-
throw new Error("Circular relationship detected");
|
|
144
|
-
}
|
|
140
|
+
await this._target?.find(id, hop);
|
|
145
141
|
if (hop + 1 < this.hops!.max) {
|
|
146
|
-
|
|
142
|
+
if (this._matches.isCircular(id)) {
|
|
143
|
+
this._matches.pop();
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
await this.find(id, hop + 1);
|
|
147
147
|
}
|
|
148
148
|
this._matches.pop();
|
|
149
149
|
} else {
|
|
150
150
|
// Below minimum hops: traverse the edge without yielding a match
|
|
151
|
-
await this.find(
|
|
151
|
+
await this.find(id, hop + 1);
|
|
152
152
|
}
|
|
153
153
|
}
|
|
154
154
|
// Restore original source node
|
|
@@ -8,11 +8,8 @@ class RelationshipData extends Data {
|
|
|
8
8
|
super._buildIndex("left_id");
|
|
9
9
|
super._buildIndex("right_id");
|
|
10
10
|
}
|
|
11
|
-
public find(
|
|
12
|
-
return super._find(
|
|
13
|
-
}
|
|
14
|
-
public findReverse(right_id: string, hop: number = 0): boolean {
|
|
15
|
-
return super._find(right_id, hop, "right_id");
|
|
11
|
+
public find(id: string, hop: number = 0, direction: "left" | "right" = "right"): boolean {
|
|
12
|
+
return super._find(id, hop, direction === "left" ? "right_id" : "left_id");
|
|
16
13
|
}
|
|
17
14
|
/*
|
|
18
15
|
** Get the properties of the current relationship record
|
|
@@ -11,7 +11,7 @@ class RelationshipMatchCollector {
|
|
|
11
11
|
private _matches: RelationshipMatchRecord[] = [];
|
|
12
12
|
private _nodeIds: Array<string> = [];
|
|
13
13
|
|
|
14
|
-
public push(relationship: Relationship): RelationshipMatchRecord {
|
|
14
|
+
public push(relationship: Relationship, traversalId: string): RelationshipMatchRecord {
|
|
15
15
|
const match: RelationshipMatchRecord = {
|
|
16
16
|
type: relationship.type!,
|
|
17
17
|
startNode: relationship.source?.value() || {},
|
|
@@ -19,7 +19,7 @@ class RelationshipMatchCollector {
|
|
|
19
19
|
properties: relationship.getData()?.properties() as Record<string, any>,
|
|
20
20
|
};
|
|
21
21
|
this._matches.push(match);
|
|
22
|
-
this._nodeIds.push(
|
|
22
|
+
this._nodeIds.push(traversalId);
|
|
23
23
|
return match;
|
|
24
24
|
}
|
|
25
25
|
public set endNode(node: any) {
|
|
@@ -46,12 +46,11 @@ class RelationshipMatchCollector {
|
|
|
46
46
|
return this._matches;
|
|
47
47
|
}
|
|
48
48
|
/*
|
|
49
|
-
** Checks if the
|
|
50
|
-
**
|
|
49
|
+
** Checks if traversing to the given node id would form a cycle
|
|
50
|
+
** in the current traversal path
|
|
51
51
|
*/
|
|
52
|
-
public isCircular(): boolean {
|
|
53
|
-
|
|
54
|
-
return seen.size < this._nodeIds.length;
|
|
52
|
+
public isCircular(nextId: string): boolean {
|
|
53
|
+
return this._nodeIds.includes(nextId);
|
|
55
54
|
}
|
|
56
55
|
}
|
|
57
56
|
|
package/src/parsing/parser.ts
CHANGED
|
@@ -467,7 +467,10 @@ class Parser extends BaseParser {
|
|
|
467
467
|
reference = inner;
|
|
468
468
|
}
|
|
469
469
|
}
|
|
470
|
-
if (
|
|
470
|
+
if (
|
|
471
|
+
reference === undefined ||
|
|
472
|
+
(!(reference instanceof Node) && !(reference instanceof Unwind))
|
|
473
|
+
) {
|
|
471
474
|
throw new Error(`Undefined node reference: ${identifier}`);
|
|
472
475
|
}
|
|
473
476
|
node = new NodeReference(node, reference);
|
|
@@ -798,12 +801,7 @@ class Parser extends BaseParser {
|
|
|
798
801
|
expression.addNode(lookup);
|
|
799
802
|
return true;
|
|
800
803
|
}
|
|
801
|
-
} else if (
|
|
802
|
-
this.token.isLeftParenthesis() &&
|
|
803
|
-
(this.peek()?.isIdentifierOrKeyword() ||
|
|
804
|
-
this.peek()?.isColon() ||
|
|
805
|
-
this.peek()?.isRightParenthesis())
|
|
806
|
-
) {
|
|
804
|
+
} else if (this.token.isLeftParenthesis() && this.looksLikeNodePattern()) {
|
|
807
805
|
// Possible graph pattern expression
|
|
808
806
|
const pattern = this.parsePatternExpression();
|
|
809
807
|
if (pattern !== null) {
|
|
@@ -865,6 +863,42 @@ class Parser extends BaseParser {
|
|
|
865
863
|
return false;
|
|
866
864
|
}
|
|
867
865
|
|
|
866
|
+
/**
|
|
867
|
+
* Peeks ahead from a left parenthesis to determine whether the
|
|
868
|
+
* upcoming tokens form a graph-node pattern (e.g. (n:Label), (n),
|
|
869
|
+
* (:Label), ()) rather than a parenthesised expression (e.g.
|
|
870
|
+
* (variable.property), (a + b)).
|
|
871
|
+
*
|
|
872
|
+
* The heuristic is:
|
|
873
|
+
* • ( followed by `:` or `)` → node pattern
|
|
874
|
+
* • ( identifier, then `:` or `{` or `)` → node pattern
|
|
875
|
+
* • anything else → parenthesised expression
|
|
876
|
+
*/
|
|
877
|
+
private looksLikeNodePattern(): boolean {
|
|
878
|
+
const savedIndex = this.tokenIndex;
|
|
879
|
+
this.setNextToken(); // skip '('
|
|
880
|
+
this.skipWhitespaceAndComments();
|
|
881
|
+
|
|
882
|
+
if (this.token.isColon() || this.token.isRightParenthesis()) {
|
|
883
|
+
this.tokenIndex = savedIndex;
|
|
884
|
+
return true;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
if (this.token.isIdentifierOrKeyword()) {
|
|
888
|
+
this.setNextToken(); // skip identifier
|
|
889
|
+
this.skipWhitespaceAndComments();
|
|
890
|
+
const result =
|
|
891
|
+
this.token.isColon() ||
|
|
892
|
+
this.token.isOpeningBrace() ||
|
|
893
|
+
this.token.isRightParenthesis();
|
|
894
|
+
this.tokenIndex = savedIndex;
|
|
895
|
+
return result;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
this.tokenIndex = savedIndex;
|
|
899
|
+
return false;
|
|
900
|
+
}
|
|
901
|
+
|
|
868
902
|
private parseExpression(): Expression | null {
|
|
869
903
|
const expression = new Expression();
|
|
870
904
|
while (true) {
|
|
@@ -1138,7 +1138,7 @@ test("Test circular graph pattern", async () => {
|
|
|
1138
1138
|
expect(results[0].pattern[4].id).toBe(1);
|
|
1139
1139
|
});
|
|
1140
1140
|
|
|
1141
|
-
test("Test circular graph pattern with variable length should
|
|
1141
|
+
test("Test circular graph pattern with variable length should not revisit nodes", async () => {
|
|
1142
1142
|
await new Runner(`
|
|
1143
1143
|
CREATE VIRTUAL (:Person) AS {
|
|
1144
1144
|
unwind [
|
|
@@ -1161,9 +1161,10 @@ test("Test circular graph pattern with variable length should throw error", asyn
|
|
|
1161
1161
|
MATCH p=(:Person)-[:KNOWS*]-(:Person)
|
|
1162
1162
|
RETURN p AS pattern
|
|
1163
1163
|
`);
|
|
1164
|
-
await
|
|
1165
|
-
|
|
1166
|
-
|
|
1164
|
+
await match.run();
|
|
1165
|
+
const results = match.results;
|
|
1166
|
+
// Circular graph 1↔2: cycles are skipped, only acyclic paths are returned
|
|
1167
|
+
expect(results.length).toBe(6);
|
|
1167
1168
|
});
|
|
1168
1169
|
|
|
1169
1170
|
test("Test multi-hop match with min hops constraint *1..", async () => {
|
|
@@ -2064,3 +2065,107 @@ test("Test WHERE with CONTAINS combined with AND", async () => {
|
|
|
2064
2065
|
expect(results.length).toBe(1);
|
|
2065
2066
|
expect(results[0].fruit).toBe("pineapple");
|
|
2066
2067
|
});
|
|
2068
|
+
|
|
2069
|
+
test("Test collected nodes and re-matching", async () => {
|
|
2070
|
+
await new Runner(`
|
|
2071
|
+
CREATE VIRTUAL (:Person) AS {
|
|
2072
|
+
unwind [
|
|
2073
|
+
{id: 1, name: 'Person 1'},
|
|
2074
|
+
{id: 2, name: 'Person 2'},
|
|
2075
|
+
{id: 3, name: 'Person 3'},
|
|
2076
|
+
{id: 4, name: 'Person 4'}
|
|
2077
|
+
] as record
|
|
2078
|
+
RETURN record.id as id, record.name as name
|
|
2079
|
+
}
|
|
2080
|
+
`).run();
|
|
2081
|
+
await new Runner(`
|
|
2082
|
+
CREATE VIRTUAL (:Person)-[:KNOWS]-(:Person) AS {
|
|
2083
|
+
unwind [
|
|
2084
|
+
{left_id: 1, right_id: 2},
|
|
2085
|
+
{left_id: 2, right_id: 3},
|
|
2086
|
+
{left_id: 3, right_id: 4}
|
|
2087
|
+
] as record
|
|
2088
|
+
RETURN record.left_id as left_id, record.right_id as right_id
|
|
2089
|
+
}
|
|
2090
|
+
`).run();
|
|
2091
|
+
const match = new Runner(`
|
|
2092
|
+
MATCH (a:Person)-[:KNOWS*0..3]->(b:Person)
|
|
2093
|
+
WITH collect(a) AS persons, b
|
|
2094
|
+
UNWIND persons AS p
|
|
2095
|
+
match (p)-[:KNOWS]->(:Person)
|
|
2096
|
+
return p.name AS name
|
|
2097
|
+
`);
|
|
2098
|
+
await match.run();
|
|
2099
|
+
const results = match.results;
|
|
2100
|
+
expect(results.length).toBe(9);
|
|
2101
|
+
const names = results.map((r: any) => r.name);
|
|
2102
|
+
expect(names).toContain("Person 1");
|
|
2103
|
+
expect(names).toContain("Person 2");
|
|
2104
|
+
expect(names).toContain("Person 3");
|
|
2105
|
+
});
|
|
2106
|
+
|
|
2107
|
+
test("Test collected patterns and unwind", async () => {
|
|
2108
|
+
await new Runner(`
|
|
2109
|
+
CREATE VIRTUAL (:Person) AS {
|
|
2110
|
+
unwind [
|
|
2111
|
+
{id: 1, name: 'Person 1'},
|
|
2112
|
+
{id: 2, name: 'Person 2'},
|
|
2113
|
+
{id: 3, name: 'Person 3'},
|
|
2114
|
+
{id: 4, name: 'Person 4'}
|
|
2115
|
+
] as record
|
|
2116
|
+
RETURN record.id as id, record.name as name
|
|
2117
|
+
}
|
|
2118
|
+
`).run();
|
|
2119
|
+
await new Runner(`
|
|
2120
|
+
CREATE VIRTUAL (:Person)-[:KNOWS]-(:Person) AS {
|
|
2121
|
+
unwind [
|
|
2122
|
+
{left_id: 1, right_id: 2},
|
|
2123
|
+
{left_id: 2, right_id: 3},
|
|
2124
|
+
{left_id: 3, right_id: 4}
|
|
2125
|
+
] as record
|
|
2126
|
+
RETURN record.left_id as left_id, record.right_id as right_id
|
|
2127
|
+
}
|
|
2128
|
+
`).run();
|
|
2129
|
+
const match = new Runner(`
|
|
2130
|
+
MATCH p=(a:Person)-[:KNOWS*0..3]->(b:Person)
|
|
2131
|
+
WITH collect(p) AS patterns
|
|
2132
|
+
UNWIND patterns AS pattern
|
|
2133
|
+
RETURN pattern
|
|
2134
|
+
`);
|
|
2135
|
+
await match.run();
|
|
2136
|
+
const results = match.results;
|
|
2137
|
+
expect(results.length).toBe(10);
|
|
2138
|
+
// Index 0: Person 1 zero-hop - pattern = [node1] (single node)
|
|
2139
|
+
expect(results[0].pattern.length).toBe(1);
|
|
2140
|
+
expect(results[0].pattern[0].id).toBe(1);
|
|
2141
|
+
|
|
2142
|
+
// Index 1: Person 1 -> Person 2 (1-hop)
|
|
2143
|
+
expect(results[1].pattern.length).toBe(3);
|
|
2144
|
+
|
|
2145
|
+
// Index 2: Person 1 -> Person 2 -> Person 3 (2-hop)
|
|
2146
|
+
expect(results[2].pattern.length).toBe(5);
|
|
2147
|
+
|
|
2148
|
+
// Index 3: Person 1 -> Person 2 -> Person 3 -> Person 4 (3-hop)
|
|
2149
|
+
expect(results[3].pattern.length).toBe(7);
|
|
2150
|
+
|
|
2151
|
+
// Index 4: Person 2 zero-hop - pattern = [node2]
|
|
2152
|
+
expect(results[4].pattern.length).toBe(1);
|
|
2153
|
+
expect(results[4].pattern[0].id).toBe(2);
|
|
2154
|
+
|
|
2155
|
+
// Index 5: Person 2 -> Person 3 (1-hop)
|
|
2156
|
+
expect(results[5].pattern.length).toBe(3);
|
|
2157
|
+
|
|
2158
|
+
// Index 6: Person 2 -> Person 3 -> Person 4 (2-hop)
|
|
2159
|
+
expect(results[6].pattern.length).toBe(5);
|
|
2160
|
+
|
|
2161
|
+
// Index 7: Person 3 zero-hop - pattern = [node3]
|
|
2162
|
+
expect(results[7].pattern.length).toBe(1);
|
|
2163
|
+
expect(results[7].pattern[0].id).toBe(3);
|
|
2164
|
+
|
|
2165
|
+
// Index 8: Person 3 -> Person 4 (1-hop)
|
|
2166
|
+
expect(results[8].pattern.length).toBe(3);
|
|
2167
|
+
|
|
2168
|
+
// Index 9: Person 4 zero-hop - pattern = [node4]
|
|
2169
|
+
expect(results[9].pattern.length).toBe(1);
|
|
2170
|
+
expect(results[9].pattern[0].id).toBe(4);
|
|
2171
|
+
});
|
|
@@ -839,7 +839,7 @@ test("Parse statement with graph pattern in where clause", () => {
|
|
|
839
839
|
const source = pattern.chain[0] as NodeReference;
|
|
840
840
|
const relationship = pattern.chain[1] as Relationship;
|
|
841
841
|
const target = pattern.chain[2] as Node;
|
|
842
|
-
expect(source.reference?.identifier).toBe("a");
|
|
842
|
+
expect((source.reference as Node)?.identifier).toBe("a");
|
|
843
843
|
expect(relationship.type).toBe("KNOWS");
|
|
844
844
|
expect(target.label).toBe("Person");
|
|
845
845
|
});
|
|
@@ -909,7 +909,7 @@ test("Test node reference with label creates NodeReference instead of new node",
|
|
|
909
909
|
expect(firstNode.identifier).toBe("n");
|
|
910
910
|
expect(firstNode.label).toBe("Person");
|
|
911
911
|
expect(secondNode).toBeInstanceOf(NodeReference);
|
|
912
|
-
expect(secondNode.reference
|
|
912
|
+
expect((secondNode.reference! as Node).identifier).toBe("n");
|
|
913
913
|
expect(secondNode.label).toBe("Person");
|
|
914
914
|
});
|
|
915
915
|
|
|
@@ -1098,3 +1098,61 @@ test("Test WHERE with NOT ENDS WITH", () => {
|
|
|
1098
1098
|
"--- Reference (s)"
|
|
1099
1099
|
);
|
|
1100
1100
|
});
|
|
1101
|
+
|
|
1102
|
+
test("Test parenthesized expression with addition", () => {
|
|
1103
|
+
const parser = new Parser();
|
|
1104
|
+
const ast = parser.parse("WITH 1 AS n RETURN (n + 2)");
|
|
1105
|
+
// prettier-ignore
|
|
1106
|
+
expect(ast.print()).toBe(
|
|
1107
|
+
"ASTNode\n" +
|
|
1108
|
+
"- With\n" +
|
|
1109
|
+
"-- Expression (n)\n" +
|
|
1110
|
+
"--- Number (1)\n" +
|
|
1111
|
+
"- Return\n" +
|
|
1112
|
+
"-- Expression\n" +
|
|
1113
|
+
"--- Expression\n" +
|
|
1114
|
+
"---- Add\n" +
|
|
1115
|
+
"----- Reference (n)\n" +
|
|
1116
|
+
"----- Number (2)"
|
|
1117
|
+
);
|
|
1118
|
+
});
|
|
1119
|
+
|
|
1120
|
+
test("Test parenthesized expression with property access", () => {
|
|
1121
|
+
const parser = new Parser();
|
|
1122
|
+
const ast = parser.parse("WITH {a: 1} AS obj RETURN (obj.a)");
|
|
1123
|
+
// prettier-ignore
|
|
1124
|
+
expect(ast.print()).toBe(
|
|
1125
|
+
"ASTNode\n" +
|
|
1126
|
+
"- With\n" +
|
|
1127
|
+
"-- Expression (obj)\n" +
|
|
1128
|
+
"--- AssociativeArray\n" +
|
|
1129
|
+
"---- KeyValuePair\n" +
|
|
1130
|
+
"----- String (a)\n" +
|
|
1131
|
+
"----- Expression\n" +
|
|
1132
|
+
"------ Number (1)\n" +
|
|
1133
|
+
"- Return\n" +
|
|
1134
|
+
"-- Expression\n" +
|
|
1135
|
+
"--- Expression\n" +
|
|
1136
|
+
"---- Lookup\n" +
|
|
1137
|
+
"----- Identifier (a)\n" +
|
|
1138
|
+
"----- Reference (obj)"
|
|
1139
|
+
);
|
|
1140
|
+
});
|
|
1141
|
+
|
|
1142
|
+
test("Test parenthesized expression with multiplication", () => {
|
|
1143
|
+
const parser = new Parser();
|
|
1144
|
+
const ast = parser.parse("WITH 5 AS x RETURN (x * 3)");
|
|
1145
|
+
// prettier-ignore
|
|
1146
|
+
expect(ast.print()).toBe(
|
|
1147
|
+
"ASTNode\n" +
|
|
1148
|
+
"- With\n" +
|
|
1149
|
+
"-- Expression (x)\n" +
|
|
1150
|
+
"--- Number (5)\n" +
|
|
1151
|
+
"- Return\n" +
|
|
1152
|
+
"-- Expression\n" +
|
|
1153
|
+
"--- Expression\n" +
|
|
1154
|
+
"---- Multiply\n" +
|
|
1155
|
+
"----- Reference (x)\n" +
|
|
1156
|
+
"----- Number (3)"
|
|
1157
|
+
);
|
|
1158
|
+
});
|