flowquery 1.0.37 → 1.0.39
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/parsing/expressions/operator.js +4 -4
- package/dist/parsing/expressions/operator.js.map +1 -1
- package/dist/parsing/operations/aggregated_return.d.ts.map +1 -1
- package/dist/parsing/operations/aggregated_return.js +6 -2
- package/dist/parsing/operations/aggregated_return.js.map +1 -1
- package/dist/parsing/operations/group_by.d.ts.map +1 -1
- package/dist/parsing/operations/group_by.js +3 -2
- package/dist/parsing/operations/group_by.js.map +1 -1
- package/dist/parsing/operations/limit.d.ts +3 -0
- package/dist/parsing/operations/limit.d.ts.map +1 -1
- package/dist/parsing/operations/limit.js +9 -0
- package/dist/parsing/operations/limit.js.map +1 -1
- package/dist/parsing/operations/order_by.d.ts +35 -0
- package/dist/parsing/operations/order_by.d.ts.map +1 -0
- package/dist/parsing/operations/order_by.js +87 -0
- package/dist/parsing/operations/order_by.js.map +1 -0
- package/dist/parsing/operations/return.d.ts +6 -0
- package/dist/parsing/operations/return.d.ts.map +1 -1
- package/dist/parsing/operations/return.js +24 -1
- package/dist/parsing/operations/return.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 +67 -10
- package/dist/parsing/parser.js.map +1 -1
- package/dist/tokenization/token.d.ts +8 -0
- package/dist/tokenization/token.d.ts.map +1 -1
- package/dist/tokenization/token.js +24 -0
- package/dist/tokenization/token.js.map +1 -1
- package/docs/flowquery.min.js +1 -1
- package/flowquery-py/pyproject.toml +1 -1
- package/flowquery-py/src/parsing/expressions/operator.py +4 -4
- package/flowquery-py/src/parsing/operations/__init__.py +3 -0
- package/flowquery-py/src/parsing/operations/aggregated_return.py +4 -1
- package/flowquery-py/src/parsing/operations/limit.py +11 -0
- package/flowquery-py/src/parsing/operations/order_by.py +72 -0
- package/flowquery-py/src/parsing/operations/return_op.py +32 -1
- package/flowquery-py/src/parsing/parser.py +57 -9
- package/flowquery-py/src/tokenization/token.py +28 -0
- package/flowquery-py/tests/compute/test_runner.py +238 -1
- package/flowquery-vscode/flowQueryEngine/flowquery.min.js +1 -1
- package/package.json +1 -1
- package/src/parsing/expressions/operator.ts +4 -4
- package/src/parsing/operations/aggregated_return.ts +9 -5
- package/src/parsing/operations/group_by.ts +4 -2
- package/src/parsing/operations/limit.ts +10 -1
- package/src/parsing/operations/order_by.ts +75 -0
- package/src/parsing/operations/return.ts +26 -1
- package/src/parsing/parser.ts +64 -10
- package/src/tokenization/token.ts +32 -0
- package/tests/compute/runner.test.ts +211 -0
package/package.json
CHANGED
|
@@ -197,7 +197,7 @@ class Not extends Operator {
|
|
|
197
197
|
|
|
198
198
|
class Is extends Operator {
|
|
199
199
|
constructor() {
|
|
200
|
-
super(
|
|
200
|
+
super(0, true);
|
|
201
201
|
}
|
|
202
202
|
public value(): number {
|
|
203
203
|
return this.lhs.value() == this.rhs.value() ? 1 : 0;
|
|
@@ -206,7 +206,7 @@ class Is extends Operator {
|
|
|
206
206
|
|
|
207
207
|
class IsNot extends Operator {
|
|
208
208
|
constructor() {
|
|
209
|
-
super(
|
|
209
|
+
super(0, true);
|
|
210
210
|
}
|
|
211
211
|
public value(): number {
|
|
212
212
|
return this.lhs.value() != this.rhs.value() ? 1 : 0;
|
|
@@ -215,7 +215,7 @@ class IsNot extends Operator {
|
|
|
215
215
|
|
|
216
216
|
class In extends Operator {
|
|
217
217
|
constructor() {
|
|
218
|
-
super(
|
|
218
|
+
super(0, true);
|
|
219
219
|
}
|
|
220
220
|
public value(): number {
|
|
221
221
|
const list = this.rhs.value();
|
|
@@ -228,7 +228,7 @@ class In extends Operator {
|
|
|
228
228
|
|
|
229
229
|
class NotIn extends Operator {
|
|
230
230
|
constructor() {
|
|
231
|
-
super(
|
|
231
|
+
super(0, true);
|
|
232
232
|
}
|
|
233
233
|
public value(): number {
|
|
234
234
|
const list = this.rhs.value();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import Return from "./return";
|
|
2
|
-
import GroupBy from "./group_by";
|
|
3
1
|
import Expression from "../expressions/expression";
|
|
2
|
+
import GroupBy from "./group_by";
|
|
3
|
+
import Return from "./return";
|
|
4
4
|
|
|
5
5
|
class AggregatedReturn extends Return {
|
|
6
6
|
private _group_by: GroupBy = new GroupBy(this.children as Expression[]);
|
|
@@ -8,11 +8,15 @@ class AggregatedReturn extends Return {
|
|
|
8
8
|
await this._group_by.run();
|
|
9
9
|
}
|
|
10
10
|
public get results(): Record<string, any>[] {
|
|
11
|
-
if(this._where !== null) {
|
|
11
|
+
if (this._where !== null) {
|
|
12
12
|
this._group_by.where = this._where;
|
|
13
13
|
}
|
|
14
|
-
|
|
14
|
+
const results = Array.from(this._group_by.generate_results());
|
|
15
|
+
if (this._orderBy !== null) {
|
|
16
|
+
return this._orderBy.sort(results);
|
|
17
|
+
}
|
|
18
|
+
return results;
|
|
15
19
|
}
|
|
16
20
|
}
|
|
17
21
|
|
|
18
|
-
export default AggregatedReturn;
|
|
22
|
+
export default AggregatedReturn;
|
|
@@ -52,10 +52,12 @@ class GroupBy extends Projection {
|
|
|
52
52
|
let node: Node = this.current;
|
|
53
53
|
for (const mapper of this.mappers) {
|
|
54
54
|
const value: any = mapper.value();
|
|
55
|
-
|
|
55
|
+
const key: string =
|
|
56
|
+
typeof value === "object" && value !== null ? JSON.stringify(value) : String(value);
|
|
57
|
+
let child: Node | undefined = node.children.get(key);
|
|
56
58
|
if (child === undefined) {
|
|
57
59
|
child = new Node(value);
|
|
58
|
-
node.children.set(
|
|
60
|
+
node.children.set(key, child);
|
|
59
61
|
}
|
|
60
62
|
node = child;
|
|
61
63
|
}
|
|
@@ -7,6 +7,15 @@ class Limit extends Operation {
|
|
|
7
7
|
super();
|
|
8
8
|
this.limit = limit;
|
|
9
9
|
}
|
|
10
|
+
public get isLimitReached(): boolean {
|
|
11
|
+
return this.count >= this.limit;
|
|
12
|
+
}
|
|
13
|
+
public get limitValue(): number {
|
|
14
|
+
return this.limit;
|
|
15
|
+
}
|
|
16
|
+
public increment(): void {
|
|
17
|
+
this.count++;
|
|
18
|
+
}
|
|
10
19
|
public async run(): Promise<void> {
|
|
11
20
|
if (this.count >= this.limit) {
|
|
12
21
|
return;
|
|
@@ -19,4 +28,4 @@ class Limit extends Operation {
|
|
|
19
28
|
}
|
|
20
29
|
}
|
|
21
30
|
|
|
22
|
-
export default Limit;
|
|
31
|
+
export default Limit;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import Operation from "./operation";
|
|
2
|
+
|
|
3
|
+
export interface SortField {
|
|
4
|
+
field: string;
|
|
5
|
+
direction: "asc" | "desc";
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Represents an ORDER BY operation that sorts results.
|
|
10
|
+
*
|
|
11
|
+
* Can be attached to a RETURN operation (sorting its results),
|
|
12
|
+
* or used as a standalone accumulating operation after a non-aggregate WITH.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```
|
|
16
|
+
* RETURN x ORDER BY x DESC
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
class OrderBy extends Operation {
|
|
20
|
+
private _fields: SortField[];
|
|
21
|
+
private _results: Record<string, any>[] = [];
|
|
22
|
+
|
|
23
|
+
constructor(fields: SortField[]) {
|
|
24
|
+
super();
|
|
25
|
+
this._fields = fields;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
public get fields(): SortField[] {
|
|
29
|
+
return this._fields;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Sorts an array of records according to the sort fields.
|
|
34
|
+
*/
|
|
35
|
+
public sort(records: Record<string, any>[]): Record<string, any>[] {
|
|
36
|
+
return records.sort((a, b) => {
|
|
37
|
+
for (const { field, direction } of this._fields) {
|
|
38
|
+
const aVal = a[field];
|
|
39
|
+
const bVal = b[field];
|
|
40
|
+
let cmp = 0;
|
|
41
|
+
if (aVal == null && bVal == null) cmp = 0;
|
|
42
|
+
else if (aVal == null) cmp = -1;
|
|
43
|
+
else if (bVal == null) cmp = 1;
|
|
44
|
+
else if (aVal < bVal) cmp = -1;
|
|
45
|
+
else if (aVal > bVal) cmp = 1;
|
|
46
|
+
if (cmp !== 0) {
|
|
47
|
+
return direction === "desc" ? -cmp : cmp;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return 0;
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* When used as a standalone operation (after non-aggregate WITH),
|
|
56
|
+
* accumulates records to sort later.
|
|
57
|
+
*/
|
|
58
|
+
public async run(): Promise<void> {
|
|
59
|
+
const record: Record<string, any> = {};
|
|
60
|
+
// Collect current variable values from the context
|
|
61
|
+
// This gets called per-row, and then finish() sorts and emits
|
|
62
|
+
await this.next?.run();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
public async initialize(): Promise<void> {
|
|
66
|
+
this._results = [];
|
|
67
|
+
await this.next?.initialize();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
public get results(): Record<string, any>[] {
|
|
71
|
+
return this._results;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export default OrderBy;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import Limit from "./limit";
|
|
2
|
+
import OrderBy from "./order_by";
|
|
1
3
|
import Projection from "./projection";
|
|
2
4
|
import Where from "./where";
|
|
3
5
|
|
|
@@ -15,6 +17,8 @@ import Where from "./where";
|
|
|
15
17
|
class Return extends Projection {
|
|
16
18
|
protected _where: Where | null = null;
|
|
17
19
|
protected _results: Record<string, any>[] = [];
|
|
20
|
+
private _limit: Limit | null = null;
|
|
21
|
+
protected _orderBy: OrderBy | null = null;
|
|
18
22
|
public set where(where: Where) {
|
|
19
23
|
this._where = where;
|
|
20
24
|
}
|
|
@@ -24,10 +28,21 @@ class Return extends Projection {
|
|
|
24
28
|
}
|
|
25
29
|
return this._where.value();
|
|
26
30
|
}
|
|
31
|
+
public set limit(limit: Limit) {
|
|
32
|
+
this._limit = limit;
|
|
33
|
+
}
|
|
34
|
+
public set orderBy(orderBy: OrderBy) {
|
|
35
|
+
this._orderBy = orderBy;
|
|
36
|
+
}
|
|
27
37
|
public async run(): Promise<void> {
|
|
28
38
|
if (!this.where) {
|
|
29
39
|
return;
|
|
30
40
|
}
|
|
41
|
+
// When ORDER BY is present, skip limit during accumulation;
|
|
42
|
+
// limit will be applied after sorting in get results()
|
|
43
|
+
if (this._orderBy === null && this._limit !== null && this._limit.isLimitReached) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
31
46
|
const record: Map<string, any> = new Map();
|
|
32
47
|
for (const [expression, alias] of this.expressions()) {
|
|
33
48
|
const raw = expression.value();
|
|
@@ -35,12 +50,22 @@ class Return extends Projection {
|
|
|
35
50
|
record.set(alias, value);
|
|
36
51
|
}
|
|
37
52
|
this._results.push(Object.fromEntries(record));
|
|
53
|
+
if (this._orderBy === null && this._limit !== null) {
|
|
54
|
+
this._limit.increment();
|
|
55
|
+
}
|
|
38
56
|
}
|
|
39
57
|
public async initialize(): Promise<void> {
|
|
40
58
|
this._results = [];
|
|
41
59
|
}
|
|
42
60
|
public get results(): Record<string, any>[] {
|
|
43
|
-
|
|
61
|
+
let results = this._results;
|
|
62
|
+
if (this._orderBy !== null) {
|
|
63
|
+
results = this._orderBy.sort(results);
|
|
64
|
+
}
|
|
65
|
+
if (this._orderBy !== null && this._limit !== null) {
|
|
66
|
+
results = results.slice(0, this._limit.limitValue);
|
|
67
|
+
}
|
|
68
|
+
return results;
|
|
44
69
|
}
|
|
45
70
|
}
|
|
46
71
|
|
package/src/parsing/parser.ts
CHANGED
|
@@ -57,6 +57,7 @@ import Limit from "./operations/limit";
|
|
|
57
57
|
import Load from "./operations/load";
|
|
58
58
|
import Match from "./operations/match";
|
|
59
59
|
import Operation from "./operations/operation";
|
|
60
|
+
import OrderBy, { SortField } from "./operations/order_by";
|
|
60
61
|
import Return from "./operations/return";
|
|
61
62
|
import Union from "./operations/union";
|
|
62
63
|
import UnionAll from "./operations/union_all";
|
|
@@ -112,6 +113,9 @@ class Parser extends BaseParser {
|
|
|
112
113
|
if (this.token.isUnion()) {
|
|
113
114
|
break;
|
|
114
115
|
}
|
|
116
|
+
if (this.token.isEOF()) {
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
115
119
|
operation = this.parseOperation();
|
|
116
120
|
if (operation === null && !isSubQuery) {
|
|
117
121
|
throw new Error("Expected one of WITH, UNWIND, RETURN, LOAD, OR CALL");
|
|
@@ -140,10 +144,23 @@ class Parser extends BaseParser {
|
|
|
140
144
|
operation = where;
|
|
141
145
|
}
|
|
142
146
|
}
|
|
147
|
+
const orderBy = this.parseOrderBy();
|
|
148
|
+
if (orderBy !== null) {
|
|
149
|
+
if (operation instanceof Return) {
|
|
150
|
+
(operation as Return).orderBy = orderBy;
|
|
151
|
+
} else {
|
|
152
|
+
operation!.addSibling(orderBy);
|
|
153
|
+
operation = orderBy;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
143
156
|
const limit = this.parseLimit();
|
|
144
157
|
if (limit !== null) {
|
|
145
|
-
operation
|
|
146
|
-
|
|
158
|
+
if (operation instanceof Return) {
|
|
159
|
+
(operation as Return).limit = limit;
|
|
160
|
+
} else {
|
|
161
|
+
operation!.addSibling(limit);
|
|
162
|
+
operation = limit;
|
|
163
|
+
}
|
|
147
164
|
}
|
|
148
165
|
previous = operation;
|
|
149
166
|
}
|
|
@@ -494,16 +511,11 @@ class Parser extends BaseParser {
|
|
|
494
511
|
node.label = label!;
|
|
495
512
|
if (identifier !== null && this._state.variables.has(identifier)) {
|
|
496
513
|
let reference = this._state.variables.get(identifier);
|
|
497
|
-
// Resolve through Expression -> Reference -> Node (e.g., after WITH)
|
|
498
|
-
if (reference instanceof Expression && reference.firstChild() instanceof Reference) {
|
|
499
|
-
const inner = (reference.firstChild() as Reference).referred;
|
|
500
|
-
if (inner instanceof Node) {
|
|
501
|
-
reference = inner;
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
514
|
if (
|
|
505
515
|
reference === undefined ||
|
|
506
|
-
(!(reference instanceof Node) &&
|
|
516
|
+
(!(reference instanceof Node) &&
|
|
517
|
+
!(reference instanceof Unwind) &&
|
|
518
|
+
!(reference instanceof Expression))
|
|
507
519
|
) {
|
|
508
520
|
throw new Error(`Undefined node reference: ${identifier}`);
|
|
509
521
|
}
|
|
@@ -788,6 +800,48 @@ class Parser extends BaseParser {
|
|
|
788
800
|
return limit;
|
|
789
801
|
}
|
|
790
802
|
|
|
803
|
+
private parseOrderBy(): OrderBy | null {
|
|
804
|
+
this.skipWhitespaceAndComments();
|
|
805
|
+
if (!this.token.isOrder()) {
|
|
806
|
+
return null;
|
|
807
|
+
}
|
|
808
|
+
this.expectPreviousTokenToBeWhitespaceOrComment();
|
|
809
|
+
this.setNextToken();
|
|
810
|
+
this.expectAndSkipWhitespaceAndComments();
|
|
811
|
+
if (!this.token.isByKeyword()) {
|
|
812
|
+
throw new Error("Expected BY after ORDER");
|
|
813
|
+
}
|
|
814
|
+
this.setNextToken();
|
|
815
|
+
this.expectAndSkipWhitespaceAndComments();
|
|
816
|
+
const fields: SortField[] = [];
|
|
817
|
+
while (true) {
|
|
818
|
+
if (!this.token.isIdentifierOrKeyword()) {
|
|
819
|
+
throw new Error("Expected field name in ORDER BY");
|
|
820
|
+
}
|
|
821
|
+
const field = this.token.value!;
|
|
822
|
+
this.setNextToken();
|
|
823
|
+
this.skipWhitespaceAndComments();
|
|
824
|
+
let direction: "asc" | "desc" = "asc";
|
|
825
|
+
if (this.token.isAsc()) {
|
|
826
|
+
direction = "asc";
|
|
827
|
+
this.setNextToken();
|
|
828
|
+
this.skipWhitespaceAndComments();
|
|
829
|
+
} else if (this.token.isDesc()) {
|
|
830
|
+
direction = "desc";
|
|
831
|
+
this.setNextToken();
|
|
832
|
+
this.skipWhitespaceAndComments();
|
|
833
|
+
}
|
|
834
|
+
fields.push({ field, direction });
|
|
835
|
+
if (this.token.isComma()) {
|
|
836
|
+
this.setNextToken();
|
|
837
|
+
this.skipWhitespaceAndComments();
|
|
838
|
+
} else {
|
|
839
|
+
break;
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
return new OrderBy(fields);
|
|
843
|
+
}
|
|
844
|
+
|
|
791
845
|
private *parseExpressions(
|
|
792
846
|
alias_option: AliasOption = AliasOption.NOT_ALLOWED
|
|
793
847
|
): IterableIterator<Expression> {
|
|
@@ -675,6 +675,38 @@ class Token {
|
|
|
675
675
|
return this._type === TokenType.KEYWORD && this._value === Keyword.DISTINCT;
|
|
676
676
|
}
|
|
677
677
|
|
|
678
|
+
public static get ORDER(): Token {
|
|
679
|
+
return new Token(TokenType.KEYWORD, Keyword.ORDER);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
public isOrder(): boolean {
|
|
683
|
+
return this._type === TokenType.KEYWORD && this._value === Keyword.ORDER;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
public static get BY(): Token {
|
|
687
|
+
return new Token(TokenType.KEYWORD, Keyword.BY);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
public isByKeyword(): boolean {
|
|
691
|
+
return this._type === TokenType.KEYWORD && this._value === Keyword.BY;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
public static get ASC(): Token {
|
|
695
|
+
return new Token(TokenType.KEYWORD, Keyword.ASC);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
public isAsc(): boolean {
|
|
699
|
+
return this._type === TokenType.KEYWORD && this._value === Keyword.ASC;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
public static get DESC(): Token {
|
|
703
|
+
return new Token(TokenType.KEYWORD, Keyword.DESC);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
public isDesc(): boolean {
|
|
707
|
+
return this._type === TokenType.KEYWORD && this._value === Keyword.DESC;
|
|
708
|
+
}
|
|
709
|
+
|
|
678
710
|
public static get LIMIT(): Token {
|
|
679
711
|
return new Token(TokenType.KEYWORD, Keyword.LIMIT);
|
|
680
712
|
}
|
|
@@ -842,6 +842,19 @@ test("Test limit", async () => {
|
|
|
842
842
|
expect(results.length).toBe(50);
|
|
843
843
|
});
|
|
844
844
|
|
|
845
|
+
test("Test limit as last operation", async () => {
|
|
846
|
+
const runner = new Runner(
|
|
847
|
+
`
|
|
848
|
+
unwind range(1, 10) as i
|
|
849
|
+
return i
|
|
850
|
+
limit 5
|
|
851
|
+
`
|
|
852
|
+
);
|
|
853
|
+
await runner.run();
|
|
854
|
+
const results = runner.results;
|
|
855
|
+
expect(results.length).toBe(5);
|
|
856
|
+
});
|
|
857
|
+
|
|
845
858
|
test("Test range lookup", async () => {
|
|
846
859
|
const runner = new Runner(
|
|
847
860
|
`
|
|
@@ -1327,6 +1340,60 @@ test("Test match with referenced to previous variable", async () => {
|
|
|
1327
1340
|
expect(results[1]).toEqual({ name1: "Person 2", name2: "Person 3", name3: "Person 4" });
|
|
1328
1341
|
});
|
|
1329
1342
|
|
|
1343
|
+
test("Test match with aggregated with and subsequent match", async () => {
|
|
1344
|
+
await new Runner(`
|
|
1345
|
+
CREATE VIRTUAL (:User) AS {
|
|
1346
|
+
unwind [
|
|
1347
|
+
{id: 1, name: 'Alice'},
|
|
1348
|
+
{id: 2, name: 'Bob'},
|
|
1349
|
+
{id: 3, name: 'Carol'}
|
|
1350
|
+
] as record
|
|
1351
|
+
RETURN record.id as id, record.name as name
|
|
1352
|
+
}
|
|
1353
|
+
`).run();
|
|
1354
|
+
await new Runner(`
|
|
1355
|
+
CREATE VIRTUAL (:User)-[:KNOWS]-(:User) AS {
|
|
1356
|
+
unwind [
|
|
1357
|
+
{left_id: 1, right_id: 2},
|
|
1358
|
+
{left_id: 1, right_id: 3}
|
|
1359
|
+
] as record
|
|
1360
|
+
RETURN record.left_id as left_id, record.right_id as right_id
|
|
1361
|
+
}
|
|
1362
|
+
`).run();
|
|
1363
|
+
await new Runner(`
|
|
1364
|
+
CREATE VIRTUAL (:Project) AS {
|
|
1365
|
+
unwind [
|
|
1366
|
+
{id: 1, name: 'Project A'},
|
|
1367
|
+
{id: 2, name: 'Project B'}
|
|
1368
|
+
] as record
|
|
1369
|
+
RETURN record.id as id, record.name as name
|
|
1370
|
+
}
|
|
1371
|
+
`).run();
|
|
1372
|
+
await new Runner(`
|
|
1373
|
+
CREATE VIRTUAL (:User)-[:WORKS_ON]-(:Project) AS {
|
|
1374
|
+
unwind [
|
|
1375
|
+
{left_id: 1, right_id: 1},
|
|
1376
|
+
{left_id: 1, right_id: 2}
|
|
1377
|
+
] as record
|
|
1378
|
+
RETURN record.left_id as left_id, record.right_id as right_id
|
|
1379
|
+
}
|
|
1380
|
+
`).run();
|
|
1381
|
+
const match = new Runner(`
|
|
1382
|
+
MATCH (u:User)-[:KNOWS]->(s:User)
|
|
1383
|
+
WITH u, count(s) as acquaintances
|
|
1384
|
+
MATCH (u)-[:WORKS_ON]->(p:Project)
|
|
1385
|
+
RETURN u.name as name, acquaintances, collect(p.name) as projects
|
|
1386
|
+
`);
|
|
1387
|
+
await match.run();
|
|
1388
|
+
const results = match.results;
|
|
1389
|
+
expect(results.length).toBe(1);
|
|
1390
|
+
expect(results[0]).toEqual({
|
|
1391
|
+
name: "Alice",
|
|
1392
|
+
acquaintances: 2,
|
|
1393
|
+
projects: ["Project A", "Project B"],
|
|
1394
|
+
});
|
|
1395
|
+
});
|
|
1396
|
+
|
|
1330
1397
|
test("Test match and return full node", async () => {
|
|
1331
1398
|
await new Runner(`
|
|
1332
1399
|
CREATE VIRTUAL (:Person) AS {
|
|
@@ -2481,6 +2548,54 @@ test("Test WHERE with IN combined with AND", async () => {
|
|
|
2481
2548
|
expect(results.map((r: any) => r.n)).toEqual([10, 15, 20]);
|
|
2482
2549
|
});
|
|
2483
2550
|
|
|
2551
|
+
test("Test WHERE with AND before IN", async () => {
|
|
2552
|
+
const runner = new Runner(`
|
|
2553
|
+
unwind ['expert', 'intermediate', 'beginner'] as proficiency
|
|
2554
|
+
with proficiency where 1=1 and proficiency in ['expert']
|
|
2555
|
+
return proficiency
|
|
2556
|
+
`);
|
|
2557
|
+
await runner.run();
|
|
2558
|
+
const results = runner.results;
|
|
2559
|
+
expect(results.length).toBe(1);
|
|
2560
|
+
expect(results[0]).toEqual({ proficiency: "expert" });
|
|
2561
|
+
});
|
|
2562
|
+
|
|
2563
|
+
test("Test WHERE with AND before NOT IN", async () => {
|
|
2564
|
+
const runner = new Runner(`
|
|
2565
|
+
unwind ['expert', 'intermediate', 'beginner'] as proficiency
|
|
2566
|
+
with proficiency where 1=1 and proficiency not in ['expert']
|
|
2567
|
+
return proficiency
|
|
2568
|
+
`);
|
|
2569
|
+
await runner.run();
|
|
2570
|
+
const results = runner.results;
|
|
2571
|
+
expect(results.length).toBe(2);
|
|
2572
|
+
expect(results.map((r: any) => r.proficiency)).toEqual(["intermediate", "beginner"]);
|
|
2573
|
+
});
|
|
2574
|
+
|
|
2575
|
+
test("Test WHERE with OR before IN", async () => {
|
|
2576
|
+
const runner = new Runner(`
|
|
2577
|
+
unwind range(1, 10) as n
|
|
2578
|
+
with n where 1=0 or n in [3, 7]
|
|
2579
|
+
return n
|
|
2580
|
+
`);
|
|
2581
|
+
await runner.run();
|
|
2582
|
+
const results = runner.results;
|
|
2583
|
+
expect(results.length).toBe(2);
|
|
2584
|
+
expect(results.map((r: any) => r.n)).toEqual([3, 7]);
|
|
2585
|
+
});
|
|
2586
|
+
|
|
2587
|
+
test("Test IN as return expression with AND in WHERE", async () => {
|
|
2588
|
+
const runner = new Runner(`
|
|
2589
|
+
unwind ['expert', 'intermediate', 'beginner'] as proficiency
|
|
2590
|
+
with proficiency where 1=1 and proficiency in ['expert']
|
|
2591
|
+
return proficiency, proficiency in ['expert'] as isExpert
|
|
2592
|
+
`);
|
|
2593
|
+
await runner.run();
|
|
2594
|
+
const results = runner.results;
|
|
2595
|
+
expect(results.length).toBe(1);
|
|
2596
|
+
expect(results[0]).toEqual({ proficiency: "expert", isExpert: 1 });
|
|
2597
|
+
});
|
|
2598
|
+
|
|
2484
2599
|
test("Test WHERE with CONTAINS", async () => {
|
|
2485
2600
|
const runner = new Runner(`
|
|
2486
2601
|
unwind ['apple', 'banana', 'grape', 'pineapple'] as fruit
|
|
@@ -3692,3 +3807,99 @@ test("Test duration() with time only", async () => {
|
|
|
3692
3807
|
expect(d.totalSeconds).toBe(9000);
|
|
3693
3808
|
expect(d.formatted).toBe("PT2H30M");
|
|
3694
3809
|
});
|
|
3810
|
+
|
|
3811
|
+
// ORDER BY tests
|
|
3812
|
+
|
|
3813
|
+
test("Test order by ascending", async () => {
|
|
3814
|
+
const runner = new Runner("unwind [3, 1, 2] as x return x order by x");
|
|
3815
|
+
await runner.run();
|
|
3816
|
+
const results = runner.results;
|
|
3817
|
+
expect(results.length).toBe(3);
|
|
3818
|
+
expect(results[0]).toEqual({ x: 1 });
|
|
3819
|
+
expect(results[1]).toEqual({ x: 2 });
|
|
3820
|
+
expect(results[2]).toEqual({ x: 3 });
|
|
3821
|
+
});
|
|
3822
|
+
|
|
3823
|
+
test("Test order by descending", async () => {
|
|
3824
|
+
const runner = new Runner("unwind [3, 1, 2] as x return x order by x desc");
|
|
3825
|
+
await runner.run();
|
|
3826
|
+
const results = runner.results;
|
|
3827
|
+
expect(results.length).toBe(3);
|
|
3828
|
+
expect(results[0]).toEqual({ x: 3 });
|
|
3829
|
+
expect(results[1]).toEqual({ x: 2 });
|
|
3830
|
+
expect(results[2]).toEqual({ x: 1 });
|
|
3831
|
+
});
|
|
3832
|
+
|
|
3833
|
+
test("Test order by ascending explicit", async () => {
|
|
3834
|
+
const runner = new Runner("unwind [3, 1, 2] as x return x order by x asc");
|
|
3835
|
+
await runner.run();
|
|
3836
|
+
const results = runner.results;
|
|
3837
|
+
expect(results.length).toBe(3);
|
|
3838
|
+
expect(results[0]).toEqual({ x: 1 });
|
|
3839
|
+
expect(results[1]).toEqual({ x: 2 });
|
|
3840
|
+
expect(results[2]).toEqual({ x: 3 });
|
|
3841
|
+
});
|
|
3842
|
+
|
|
3843
|
+
test("Test order by with multiple fields", async () => {
|
|
3844
|
+
const runner = new Runner(`
|
|
3845
|
+
unwind [{name: 'Alice', age: 30}, {name: 'Bob', age: 25}, {name: 'Alice', age: 25}] as person
|
|
3846
|
+
return person.name as name, person.age as age
|
|
3847
|
+
order by name asc, age asc
|
|
3848
|
+
`);
|
|
3849
|
+
await runner.run();
|
|
3850
|
+
const results = runner.results;
|
|
3851
|
+
expect(results.length).toBe(3);
|
|
3852
|
+
expect(results[0]).toEqual({ name: "Alice", age: 25 });
|
|
3853
|
+
expect(results[1]).toEqual({ name: "Alice", age: 30 });
|
|
3854
|
+
expect(results[2]).toEqual({ name: "Bob", age: 25 });
|
|
3855
|
+
});
|
|
3856
|
+
|
|
3857
|
+
test("Test order by with strings", async () => {
|
|
3858
|
+
const runner = new Runner(
|
|
3859
|
+
"unwind ['banana', 'apple', 'cherry'] as fruit return fruit order by fruit"
|
|
3860
|
+
);
|
|
3861
|
+
await runner.run();
|
|
3862
|
+
const results = runner.results;
|
|
3863
|
+
expect(results.length).toBe(3);
|
|
3864
|
+
expect(results[0]).toEqual({ fruit: "apple" });
|
|
3865
|
+
expect(results[1]).toEqual({ fruit: "banana" });
|
|
3866
|
+
expect(results[2]).toEqual({ fruit: "cherry" });
|
|
3867
|
+
});
|
|
3868
|
+
|
|
3869
|
+
test("Test order by with aggregated return", async () => {
|
|
3870
|
+
const runner = new Runner(`
|
|
3871
|
+
unwind [1, 1, 2, 2, 3, 3] as x
|
|
3872
|
+
return x, count(x) as cnt
|
|
3873
|
+
order by x desc
|
|
3874
|
+
`);
|
|
3875
|
+
await runner.run();
|
|
3876
|
+
const results = runner.results;
|
|
3877
|
+
expect(results.length).toBe(3);
|
|
3878
|
+
expect(results[0]).toEqual({ x: 3, cnt: 2 });
|
|
3879
|
+
expect(results[1]).toEqual({ x: 2, cnt: 2 });
|
|
3880
|
+
expect(results[2]).toEqual({ x: 1, cnt: 2 });
|
|
3881
|
+
});
|
|
3882
|
+
|
|
3883
|
+
test("Test order by with limit", async () => {
|
|
3884
|
+
const runner = new Runner("unwind [3, 1, 4, 1, 5, 9, 2, 6] as x return x order by x limit 3");
|
|
3885
|
+
await runner.run();
|
|
3886
|
+
const results = runner.results;
|
|
3887
|
+
expect(results.length).toBe(3);
|
|
3888
|
+
expect(results[0]).toEqual({ x: 1 });
|
|
3889
|
+
expect(results[1]).toEqual({ x: 1 });
|
|
3890
|
+
expect(results[2]).toEqual({ x: 2 });
|
|
3891
|
+
});
|
|
3892
|
+
|
|
3893
|
+
test("Test order by with where", async () => {
|
|
3894
|
+
const runner = new Runner(
|
|
3895
|
+
"unwind [3, 1, 4, 1, 5, 9, 2, 6] as x return x where x > 2 order by x desc"
|
|
3896
|
+
);
|
|
3897
|
+
await runner.run();
|
|
3898
|
+
const results = runner.results;
|
|
3899
|
+
expect(results.length).toBe(5);
|
|
3900
|
+
expect(results[0]).toEqual({ x: 9 });
|
|
3901
|
+
expect(results[1]).toEqual({ x: 6 });
|
|
3902
|
+
expect(results[2]).toEqual({ x: 5 });
|
|
3903
|
+
expect(results[3]).toEqual({ x: 4 });
|
|
3904
|
+
expect(results[4]).toEqual({ x: 3 });
|
|
3905
|
+
});
|