flowquery 1.0.38 → 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.
Files changed (47) hide show
  1. package/dist/flowquery.min.js +1 -1
  2. package/dist/parsing/expressions/operator.js +4 -4
  3. package/dist/parsing/expressions/operator.js.map +1 -1
  4. package/dist/parsing/operations/aggregated_return.d.ts.map +1 -1
  5. package/dist/parsing/operations/aggregated_return.js +6 -2
  6. package/dist/parsing/operations/aggregated_return.js.map +1 -1
  7. package/dist/parsing/operations/limit.d.ts +1 -0
  8. package/dist/parsing/operations/limit.d.ts.map +1 -1
  9. package/dist/parsing/operations/limit.js +3 -0
  10. package/dist/parsing/operations/limit.js.map +1 -1
  11. package/dist/parsing/operations/order_by.d.ts +35 -0
  12. package/dist/parsing/operations/order_by.d.ts.map +1 -0
  13. package/dist/parsing/operations/order_by.js +87 -0
  14. package/dist/parsing/operations/order_by.js.map +1 -0
  15. package/dist/parsing/operations/return.d.ts +3 -0
  16. package/dist/parsing/operations/return.d.ts.map +1 -1
  17. package/dist/parsing/operations/return.js +16 -3
  18. package/dist/parsing/operations/return.js.map +1 -1
  19. package/dist/parsing/parser.d.ts +1 -0
  20. package/dist/parsing/parser.d.ts.map +1 -1
  21. package/dist/parsing/parser.js +54 -0
  22. package/dist/parsing/parser.js.map +1 -1
  23. package/dist/tokenization/token.d.ts +8 -0
  24. package/dist/tokenization/token.d.ts.map +1 -1
  25. package/dist/tokenization/token.js +24 -0
  26. package/dist/tokenization/token.js.map +1 -1
  27. package/docs/flowquery.min.js +1 -1
  28. package/flowquery-py/pyproject.toml +1 -1
  29. package/flowquery-py/src/parsing/expressions/operator.py +4 -4
  30. package/flowquery-py/src/parsing/operations/__init__.py +3 -0
  31. package/flowquery-py/src/parsing/operations/aggregated_return.py +4 -1
  32. package/flowquery-py/src/parsing/operations/limit.py +4 -0
  33. package/flowquery-py/src/parsing/operations/order_by.py +72 -0
  34. package/flowquery-py/src/parsing/operations/return_op.py +20 -3
  35. package/flowquery-py/src/parsing/parser.py +44 -0
  36. package/flowquery-py/src/tokenization/token.py +28 -0
  37. package/flowquery-py/tests/compute/test_runner.py +159 -1
  38. package/flowquery-vscode/flowQueryEngine/flowquery.min.js +1 -1
  39. package/package.json +1 -1
  40. package/src/parsing/expressions/operator.ts +4 -4
  41. package/src/parsing/operations/aggregated_return.ts +9 -5
  42. package/src/parsing/operations/limit.ts +3 -0
  43. package/src/parsing/operations/order_by.ts +75 -0
  44. package/src/parsing/operations/return.ts +17 -3
  45. package/src/parsing/parser.ts +52 -0
  46. package/src/tokenization/token.ts +32 -0
  47. package/tests/compute/runner.test.ts +144 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flowquery",
3
- "version": "1.0.38",
3
+ "version": "1.0.39",
4
4
  "description": "A declarative query language for data processing pipelines.",
5
5
  "main": "dist/index.node.js",
6
6
  "types": "dist/index.node.d.ts",
@@ -197,7 +197,7 @@ class Not extends Operator {
197
197
 
198
198
  class Is extends Operator {
199
199
  constructor() {
200
- super(-1, true);
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(-1, true);
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(-1, true);
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(-1, true);
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
- return Array.from(this._group_by.generate_results());
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;
@@ -10,6 +10,9 @@ class Limit extends Operation {
10
10
  public get isLimitReached(): boolean {
11
11
  return this.count >= this.limit;
12
12
  }
13
+ public get limitValue(): number {
14
+ return this.limit;
15
+ }
13
16
  public increment(): void {
14
17
  this.count++;
15
18
  }
@@ -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,4 +1,5 @@
1
1
  import Limit from "./limit";
2
+ import OrderBy from "./order_by";
2
3
  import Projection from "./projection";
3
4
  import Where from "./where";
4
5
 
@@ -17,6 +18,7 @@ class Return extends Projection {
17
18
  protected _where: Where | null = null;
18
19
  protected _results: Record<string, any>[] = [];
19
20
  private _limit: Limit | null = null;
21
+ protected _orderBy: OrderBy | null = null;
20
22
  public set where(where: Where) {
21
23
  this._where = where;
22
24
  }
@@ -29,11 +31,16 @@ class Return extends Projection {
29
31
  public set limit(limit: Limit) {
30
32
  this._limit = limit;
31
33
  }
34
+ public set orderBy(orderBy: OrderBy) {
35
+ this._orderBy = orderBy;
36
+ }
32
37
  public async run(): Promise<void> {
33
38
  if (!this.where) {
34
39
  return;
35
40
  }
36
- if (this._limit !== null && this._limit.isLimitReached) {
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) {
37
44
  return;
38
45
  }
39
46
  const record: Map<string, any> = new Map();
@@ -43,7 +50,7 @@ class Return extends Projection {
43
50
  record.set(alias, value);
44
51
  }
45
52
  this._results.push(Object.fromEntries(record));
46
- if (this._limit !== null) {
53
+ if (this._orderBy === null && this._limit !== null) {
47
54
  this._limit.increment();
48
55
  }
49
56
  }
@@ -51,7 +58,14 @@ class Return extends Projection {
51
58
  this._results = [];
52
59
  }
53
60
  public get results(): Record<string, any>[] {
54
- return this._results;
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;
55
69
  }
56
70
  }
57
71
 
@@ -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";
@@ -143,6 +144,15 @@ class Parser extends BaseParser {
143
144
  operation = where;
144
145
  }
145
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
+ }
146
156
  const limit = this.parseLimit();
147
157
  if (limit !== null) {
148
158
  if (operation instanceof Return) {
@@ -790,6 +800,48 @@ class Parser extends BaseParser {
790
800
  return limit;
791
801
  }
792
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
+
793
845
  private *parseExpressions(
794
846
  alias_option: AliasOption = AliasOption.NOT_ALLOWED
795
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
  }
@@ -2548,6 +2548,54 @@ test("Test WHERE with IN combined with AND", async () => {
2548
2548
  expect(results.map((r: any) => r.n)).toEqual([10, 15, 20]);
2549
2549
  });
2550
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
+
2551
2599
  test("Test WHERE with CONTAINS", async () => {
2552
2600
  const runner = new Runner(`
2553
2601
  unwind ['apple', 'banana', 'grape', 'pineapple'] as fruit
@@ -3759,3 +3807,99 @@ test("Test duration() with time only", async () => {
3759
3807
  expect(d.totalSeconds).toBe(9000);
3760
3808
  expect(d.formatted).toBe("PT2H30M");
3761
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
+ });