flowquery 1.0.43 → 1.0.45

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 (94) hide show
  1. package/dist/flowquery.min.js +1 -1
  2. package/dist/parsing/functions/join.d.ts.map +1 -1
  3. package/dist/parsing/functions/join.js +6 -3
  4. package/dist/parsing/functions/join.js.map +1 -1
  5. package/dist/parsing/functions/keys.d.ts.map +1 -1
  6. package/dist/parsing/functions/keys.js +3 -5
  7. package/dist/parsing/functions/keys.js.map +1 -1
  8. package/dist/parsing/functions/range.d.ts.map +1 -1
  9. package/dist/parsing/functions/range.js +11 -3
  10. package/dist/parsing/functions/range.js.map +1 -1
  11. package/dist/parsing/functions/replace.d.ts.map +1 -1
  12. package/dist/parsing/functions/replace.js +8 -3
  13. package/dist/parsing/functions/replace.js.map +1 -1
  14. package/dist/parsing/functions/round.d.ts.map +1 -1
  15. package/dist/parsing/functions/round.js +5 -4
  16. package/dist/parsing/functions/round.js.map +1 -1
  17. package/dist/parsing/functions/size.d.ts.map +1 -1
  18. package/dist/parsing/functions/size.js +5 -4
  19. package/dist/parsing/functions/size.js.map +1 -1
  20. package/dist/parsing/functions/split.d.ts.map +1 -1
  21. package/dist/parsing/functions/split.js +12 -4
  22. package/dist/parsing/functions/split.js.map +1 -1
  23. package/dist/parsing/functions/string_distance.d.ts.map +1 -1
  24. package/dist/parsing/functions/string_distance.js +3 -0
  25. package/dist/parsing/functions/string_distance.js.map +1 -1
  26. package/dist/parsing/functions/stringify.d.ts.map +1 -1
  27. package/dist/parsing/functions/stringify.js +7 -6
  28. package/dist/parsing/functions/stringify.js.map +1 -1
  29. package/dist/parsing/functions/substring.d.ts.map +1 -1
  30. package/dist/parsing/functions/substring.js +3 -0
  31. package/dist/parsing/functions/substring.js.map +1 -1
  32. package/dist/parsing/functions/to_json.d.ts.map +1 -1
  33. package/dist/parsing/functions/to_json.js +5 -4
  34. package/dist/parsing/functions/to_json.js.map +1 -1
  35. package/dist/parsing/functions/to_lower.d.ts.map +1 -1
  36. package/dist/parsing/functions/to_lower.js +3 -0
  37. package/dist/parsing/functions/to_lower.js.map +1 -1
  38. package/dist/parsing/functions/to_string.js +1 -1
  39. package/dist/parsing/functions/to_string.js.map +1 -1
  40. package/dist/parsing/functions/trim.d.ts.map +1 -1
  41. package/dist/parsing/functions/trim.js +3 -0
  42. package/dist/parsing/functions/trim.js.map +1 -1
  43. package/dist/parsing/operations/order_by.d.ts +22 -2
  44. package/dist/parsing/operations/order_by.d.ts.map +1 -1
  45. package/dist/parsing/operations/order_by.js +54 -6
  46. package/dist/parsing/operations/order_by.js.map +1 -1
  47. package/dist/parsing/operations/return.d.ts.map +1 -1
  48. package/dist/parsing/operations/return.js +4 -0
  49. package/dist/parsing/operations/return.js.map +1 -1
  50. package/dist/parsing/parser.d.ts.map +1 -1
  51. package/dist/parsing/parser.js +4 -5
  52. package/dist/parsing/parser.js.map +1 -1
  53. package/docs/flowquery.min.js +1 -1
  54. package/flowquery-py/pyproject.toml +1 -1
  55. package/flowquery-py/src/parsing/functions/join.py +2 -0
  56. package/flowquery-py/src/parsing/functions/keys.py +1 -1
  57. package/flowquery-py/src/parsing/functions/range_.py +2 -0
  58. package/flowquery-py/src/parsing/functions/replace.py +2 -0
  59. package/flowquery-py/src/parsing/functions/round_.py +2 -0
  60. package/flowquery-py/src/parsing/functions/size.py +2 -0
  61. package/flowquery-py/src/parsing/functions/split.py +2 -0
  62. package/flowquery-py/src/parsing/functions/string_distance.py +5 -1
  63. package/flowquery-py/src/parsing/functions/stringify.py +2 -0
  64. package/flowquery-py/src/parsing/functions/substring.py +2 -0
  65. package/flowquery-py/src/parsing/functions/to_json.py +2 -0
  66. package/flowquery-py/src/parsing/functions/to_lower.py +2 -0
  67. package/flowquery-py/src/parsing/functions/to_string.py +1 -1
  68. package/flowquery-py/src/parsing/functions/trim.py +2 -0
  69. package/flowquery-py/src/parsing/operations/order_by.py +55 -13
  70. package/flowquery-py/src/parsing/operations/return_op.py +3 -0
  71. package/flowquery-py/src/parsing/parser.py +4 -5
  72. package/flowquery-py/tests/compute/test_runner.py +255 -0
  73. package/flowquery-py/tests/parsing/test_parser.py +63 -0
  74. package/flowquery-vscode/flowQueryEngine/flowquery.min.js +1 -1
  75. package/package.json +1 -1
  76. package/src/parsing/functions/join.ts +8 -5
  77. package/src/parsing/functions/keys.ts +4 -6
  78. package/src/parsing/functions/range.ts +12 -4
  79. package/src/parsing/functions/replace.ts +11 -4
  80. package/src/parsing/functions/round.ts +6 -5
  81. package/src/parsing/functions/size.ts +6 -5
  82. package/src/parsing/functions/split.ts +14 -6
  83. package/src/parsing/functions/string_distance.ts +3 -0
  84. package/src/parsing/functions/stringify.ts +9 -8
  85. package/src/parsing/functions/substring.ts +3 -0
  86. package/src/parsing/functions/to_json.ts +6 -5
  87. package/src/parsing/functions/to_lower.ts +3 -0
  88. package/src/parsing/functions/to_string.ts +1 -1
  89. package/src/parsing/functions/trim.ts +3 -0
  90. package/src/parsing/operations/order_by.ts +58 -7
  91. package/src/parsing/operations/return.ts +4 -0
  92. package/src/parsing/parser.ts +4 -5
  93. package/tests/compute/runner.test.ts +234 -0
  94. package/tests/parsing/parser.test.ts +56 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flowquery",
3
- "version": "1.0.43",
3
+ "version": "1.0.45",
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",
@@ -1,6 +1,6 @@
1
1
  import ASTNode from "../ast_node";
2
- import Function from "./function";
3
2
  import String from "../expressions/string";
3
+ import Function from "./function";
4
4
  import { FunctionDef } from "./function_metadata";
5
5
 
6
6
  @FunctionDef({
@@ -8,10 +8,10 @@ import { FunctionDef } from "./function_metadata";
8
8
  category: "scalar",
9
9
  parameters: [
10
10
  { name: "array", description: "Array of values to join", type: "array" },
11
- { name: "delimiter", description: "Delimiter to join with", type: "string" }
11
+ { name: "delimiter", description: "Delimiter to join with", type: "string" },
12
12
  ],
13
13
  output: { description: "Joined string", type: "string", example: "a,b,c" },
14
- examples: ["WITH ['a', 'b', 'c'] AS arr RETURN join(arr, ',')"]
14
+ examples: ["WITH ['a', 'b', 'c'] AS arr RETURN join(arr, ',')"],
15
15
  })
16
16
  class Join extends Function {
17
17
  constructor() {
@@ -19,7 +19,7 @@ class Join extends Function {
19
19
  this._expectedParameterCount = 2;
20
20
  }
21
21
  public set parameters(nodes: ASTNode[]) {
22
- if(nodes.length === 1) {
22
+ if (nodes.length === 1) {
23
23
  nodes.push(new String(""));
24
24
  }
25
25
  super.parameters = nodes;
@@ -27,6 +27,9 @@ class Join extends Function {
27
27
  public value(): any {
28
28
  const array = this.getChildren()[0].value();
29
29
  const delimiter = this.getChildren()[1].value();
30
+ if (array === null || array === undefined) {
31
+ return null;
32
+ }
30
33
  if (!Array.isArray(array) || typeof delimiter !== "string") {
31
34
  throw new Error("Invalid arguments for join function");
32
35
  }
@@ -34,4 +37,4 @@ class Join extends Function {
34
37
  }
35
38
  }
36
39
 
37
- export default Join;
40
+ export default Join;
@@ -4,11 +4,9 @@ import { FunctionDef } from "./function_metadata";
4
4
  @FunctionDef({
5
5
  description: "Returns the keys of an object (associative array) as an array",
6
6
  category: "scalar",
7
- parameters: [
8
- { name: "object", description: "Object to extract keys from", type: "object" }
9
- ],
7
+ parameters: [{ name: "object", description: "Object to extract keys from", type: "object" }],
10
8
  output: { description: "Array of keys", type: "array", example: "['name', 'age']" },
11
- examples: ["WITH { name: 'Alice', age: 30 } AS obj RETURN keys(obj)"]
9
+ examples: ["WITH { name: 'Alice', age: 30 } AS obj RETURN keys(obj)"],
12
10
  })
13
11
  class Keys extends Function {
14
12
  constructor() {
@@ -19,7 +17,7 @@ class Keys extends Function {
19
17
  public value(): any {
20
18
  const obj = this.getChildren()[0].value();
21
19
  if (obj === null || obj === undefined) {
22
- return [];
20
+ return null;
23
21
  }
24
22
  if (typeof obj !== "object" || Array.isArray(obj)) {
25
23
  throw new Error("keys() expects an object, not an array or primitive");
@@ -28,4 +26,4 @@ class Keys extends Function {
28
26
  }
29
27
  }
30
28
 
31
- export default Keys;
29
+ export default Keys;
@@ -6,10 +6,15 @@ import { FunctionDef } from "./function_metadata";
6
6
  category: "scalar",
7
7
  parameters: [
8
8
  { name: "start", description: "Starting number (inclusive)", type: "number" },
9
- { name: "end", description: "Ending number (inclusive)", type: "number" }
9
+ { name: "end", description: "Ending number (inclusive)", type: "number" },
10
10
  ],
11
- output: { description: "Array of integers from start to end", type: "array", items: { type: "number" }, example: [1, 2, 3, 4, 5] },
12
- examples: ["WITH range(1, 5) AS nums RETURN nums"]
11
+ output: {
12
+ description: "Array of integers from start to end",
13
+ type: "array",
14
+ items: { type: "number" },
15
+ example: [1, 2, 3, 4, 5],
16
+ },
17
+ examples: ["WITH range(1, 5) AS nums RETURN nums"],
13
18
  })
14
19
  class Range extends Function {
15
20
  constructor() {
@@ -19,6 +24,9 @@ class Range extends Function {
19
24
  public value(): any {
20
25
  const start = this.getChildren()[0].value();
21
26
  const end = this.getChildren()[1].value();
27
+ if (start === null || start === undefined || end === null || end === undefined) {
28
+ return null;
29
+ }
22
30
  if (typeof start !== "number" || typeof end !== "number") {
23
31
  throw new Error("Invalid arguments for range function");
24
32
  }
@@ -26,4 +34,4 @@ class Range extends Function {
26
34
  }
27
35
  }
28
36
 
29
- export default Range;
37
+ export default Range;
@@ -7,10 +7,10 @@ import { FunctionDef } from "./function_metadata";
7
7
  parameters: [
8
8
  { name: "text", description: "Source string", type: "string" },
9
9
  { name: "pattern", description: "Pattern to find", type: "string" },
10
- { name: "replacement", description: "Replacement string", type: "string" }
10
+ { name: "replacement", description: "Replacement string", type: "string" },
11
11
  ],
12
12
  output: { description: "String with replacements", type: "string", example: "hello world" },
13
- examples: ["WITH 'hello there' AS s RETURN replace(s, 'there', 'world')"]
13
+ examples: ["WITH 'hello there' AS s RETURN replace(s, 'there', 'world')"],
14
14
  })
15
15
  class Replace extends Function {
16
16
  constructor() {
@@ -21,11 +21,18 @@ class Replace extends Function {
21
21
  const str = this.getChildren()[0].value();
22
22
  const search = this.getChildren()[1].value();
23
23
  const replacement = this.getChildren()[2].value();
24
- if (typeof str !== "string" || typeof search !== "string" || typeof replacement !== "string") {
24
+ if (str === null || str === undefined) {
25
+ return null;
26
+ }
27
+ if (
28
+ typeof str !== "string" ||
29
+ typeof search !== "string" ||
30
+ typeof replacement !== "string"
31
+ ) {
25
32
  throw new Error("Invalid arguments for replace function");
26
33
  }
27
34
  return str.replace(new RegExp(search, "g"), replacement);
28
35
  }
29
36
  }
30
37
 
31
- export default Replace;
38
+ export default Replace;
@@ -4,11 +4,9 @@ import { FunctionDef } from "./function_metadata";
4
4
  @FunctionDef({
5
5
  description: "Rounds a number to the nearest integer",
6
6
  category: "scalar",
7
- parameters: [
8
- { name: "value", description: "Number to round", type: "number" }
9
- ],
7
+ parameters: [{ name: "value", description: "Number to round", type: "number" }],
10
8
  output: { description: "Rounded integer", type: "number", example: 4 },
11
- examples: ["WITH 3.7 AS n RETURN round(n)"]
9
+ examples: ["WITH 3.7 AS n RETURN round(n)"],
12
10
  })
13
11
  class Round extends Function {
14
12
  constructor() {
@@ -17,6 +15,9 @@ class Round extends Function {
17
15
  }
18
16
  public value(): any {
19
17
  const value = this.getChildren()[0].value();
18
+ if (value === null || value === undefined) {
19
+ return null;
20
+ }
20
21
  if (typeof value !== "number") {
21
22
  throw new Error("Invalid argument for round function");
22
23
  }
@@ -24,4 +25,4 @@ class Round extends Function {
24
25
  }
25
26
  }
26
27
 
27
- export default Round;
28
+ export default Round;
@@ -4,11 +4,9 @@ import { FunctionDef } from "./function_metadata";
4
4
  @FunctionDef({
5
5
  description: "Returns the length of an array or string",
6
6
  category: "scalar",
7
- parameters: [
8
- { name: "value", description: "Array or string to measure", type: "array" }
9
- ],
7
+ parameters: [{ name: "value", description: "Array or string to measure", type: "array" }],
10
8
  output: { description: "Length of the input", type: "number", example: 3 },
11
- examples: ["WITH [1, 2, 3] AS arr RETURN size(arr)"]
9
+ examples: ["WITH [1, 2, 3] AS arr RETURN size(arr)"],
12
10
  })
13
11
  class Size extends Function {
14
12
  constructor() {
@@ -17,6 +15,9 @@ class Size extends Function {
17
15
  }
18
16
  public value(): any {
19
17
  const arr = this.getChildren()[0].value();
18
+ if (arr === null || arr === undefined) {
19
+ return null;
20
+ }
20
21
  if (!Array.isArray(arr)) {
21
22
  throw new Error("Invalid argument for size function");
22
23
  }
@@ -24,4 +25,4 @@ class Size extends Function {
24
25
  }
25
26
  }
26
27
 
27
- export default Size;
28
+ export default Size;
@@ -1,6 +1,6 @@
1
1
  import ASTNode from "../ast_node";
2
- import Function from "./function";
3
2
  import String from "../expressions/string";
3
+ import Function from "./function";
4
4
  import { FunctionDef } from "./function_metadata";
5
5
 
6
6
  @FunctionDef({
@@ -8,10 +8,15 @@ import { FunctionDef } from "./function_metadata";
8
8
  category: "scalar",
9
9
  parameters: [
10
10
  { name: "text", description: "String to split", type: "string" },
11
- { name: "delimiter", description: "Delimiter to split by", type: "string" }
11
+ { name: "delimiter", description: "Delimiter to split by", type: "string" },
12
12
  ],
13
- output: { description: "Array of string parts", type: "array", items: { type: "string" }, example: ["a", "b", "c"] },
14
- examples: ["WITH 'a,b,c' AS s RETURN split(s, ',')"]
13
+ output: {
14
+ description: "Array of string parts",
15
+ type: "array",
16
+ items: { type: "string" },
17
+ example: ["a", "b", "c"],
18
+ },
19
+ examples: ["WITH 'a,b,c' AS s RETURN split(s, ',')"],
15
20
  })
16
21
  class Split extends Function {
17
22
  constructor() {
@@ -19,7 +24,7 @@ class Split extends Function {
19
24
  this._expectedParameterCount = 2;
20
25
  }
21
26
  public set parameters(nodes: ASTNode[]) {
22
- if(nodes.length === 1) {
27
+ if (nodes.length === 1) {
23
28
  nodes.push(new String(""));
24
29
  }
25
30
  super.parameters = nodes;
@@ -27,6 +32,9 @@ class Split extends Function {
27
32
  public value(): any {
28
33
  const str = this.getChildren()[0].value();
29
34
  const delimiter = this.getChildren()[1].value();
35
+ if (str === null || str === undefined) {
36
+ return null;
37
+ }
30
38
  if (typeof str !== "string" || typeof delimiter !== "string") {
31
39
  throw new Error("Invalid arguments for split function");
32
40
  }
@@ -34,4 +42,4 @@ class Split extends Function {
34
42
  }
35
43
  }
36
44
 
37
- export default Split;
45
+ export default Split;
@@ -68,6 +68,9 @@ class StringDistance extends Function {
68
68
  public value(): any {
69
69
  const str1 = this.getChildren()[0].value();
70
70
  const str2 = this.getChildren()[1].value();
71
+ if (str1 === null || str1 === undefined || str2 === null || str2 === undefined) {
72
+ return null;
73
+ }
71
74
  if (typeof str1 !== "string" || typeof str2 !== "string") {
72
75
  throw new Error(
73
76
  "Invalid arguments for string_distance function: both arguments must be strings"
@@ -1,16 +1,14 @@
1
1
  import ASTNode from "../ast_node";
2
- import Function from "./function";
3
2
  import Number from "../expressions/number";
3
+ import Function from "./function";
4
4
  import { FunctionDef } from "./function_metadata";
5
5
 
6
6
  @FunctionDef({
7
7
  description: "Converts a value to its JSON string representation",
8
8
  category: "scalar",
9
- parameters: [
10
- { name: "value", description: "Value to stringify", type: "any" }
11
- ],
12
- output: { description: "JSON string", type: "string", example: "{\"a\":1}" },
13
- examples: ["WITH {a: 1} AS obj RETURN stringify(obj)"]
9
+ parameters: [{ name: "value", description: "Value to stringify", type: "any" }],
10
+ output: { description: "JSON string", type: "string", example: '{"a":1}' },
11
+ examples: ["WITH {a: 1} AS obj RETURN stringify(obj)"],
14
12
  })
15
13
  class Stringify extends Function {
16
14
  constructor() {
@@ -18,7 +16,7 @@ class Stringify extends Function {
18
16
  this._expectedParameterCount = 2;
19
17
  }
20
18
  public set parameters(nodes: ASTNode[]) {
21
- if(nodes.length === 1) {
19
+ if (nodes.length === 1) {
22
20
  nodes.push(new Number("3")); // Default to 3 if only one parameter is provided
23
21
  }
24
22
  super.parameters = nodes;
@@ -26,6 +24,9 @@ class Stringify extends Function {
26
24
  public value(): any {
27
25
  const value = this.getChildren()[0].value();
28
26
  const indent = parseInt(this.getChildren()[1].value());
27
+ if (value === null || value === undefined) {
28
+ return null;
29
+ }
29
30
  if (typeof value !== "object") {
30
31
  throw new Error("Invalid argument for stringify function");
31
32
  }
@@ -33,4 +34,4 @@ class Stringify extends Function {
33
34
  }
34
35
  }
35
36
 
36
- export default Stringify;
37
+ export default Stringify;
@@ -37,6 +37,9 @@ class Substring extends Function {
37
37
  const original = children[0].value();
38
38
  const start = children[1].value();
39
39
 
40
+ if (original === null || original === undefined) {
41
+ return null;
42
+ }
40
43
  if (typeof original !== "string") {
41
44
  throw new Error(
42
45
  "Invalid argument for substring function: expected a string as the first argument"
@@ -4,11 +4,9 @@ import { FunctionDef } from "./function_metadata";
4
4
  @FunctionDef({
5
5
  description: "Parses a JSON string into an object",
6
6
  category: "scalar",
7
- parameters: [
8
- { name: "text", description: "JSON string to parse", type: "string" }
9
- ],
7
+ parameters: [{ name: "text", description: "JSON string to parse", type: "string" }],
10
8
  output: { description: "Parsed object or array", type: "object", example: { a: 1 } },
11
- examples: ["WITH '{\"a\": 1}' AS s RETURN tojson(s)"]
9
+ examples: ["WITH '{\"a\": 1}' AS s RETURN tojson(s)"],
12
10
  })
13
11
  class ToJson extends Function {
14
12
  constructor() {
@@ -17,6 +15,9 @@ class ToJson extends Function {
17
15
  }
18
16
  public value(): any {
19
17
  const str = this.getChildren()[0].value();
18
+ if (str === null || str === undefined) {
19
+ return null;
20
+ }
20
21
  if (typeof str !== "string") {
21
22
  throw new Error("Invalid arguments for tojson function");
22
23
  }
@@ -24,4 +25,4 @@ class ToJson extends Function {
24
25
  }
25
26
  }
26
27
 
27
- export default ToJson;
28
+ export default ToJson;
@@ -15,6 +15,9 @@ class ToLower extends Function {
15
15
  }
16
16
  public value(): any {
17
17
  const val = this.getChildren()[0].value();
18
+ if (val === null || val === undefined) {
19
+ return null;
20
+ }
18
21
  if (typeof val !== "string") {
19
22
  throw new Error("Invalid argument for toLower function: expected a string");
20
23
  }
@@ -20,7 +20,7 @@ class ToString extends Function {
20
20
  public value(): any {
21
21
  const val = this.getChildren()[0].value();
22
22
  if (val === null || val === undefined) {
23
- return String(val);
23
+ return null;
24
24
  }
25
25
  if (typeof val === "object") {
26
26
  return JSON.stringify(val);
@@ -15,6 +15,9 @@ class Trim extends Function {
15
15
  }
16
16
  public value(): any {
17
17
  const val = this.getChildren()[0].value();
18
+ if (val === null || val === undefined) {
19
+ return null;
20
+ }
18
21
  if (typeof val !== "string") {
19
22
  throw new Error("Invalid argument for trim function: expected a string");
20
23
  }
@@ -1,8 +1,11 @@
1
+ import Expression from "../expressions/expression";
2
+ import Reference from "../expressions/reference";
1
3
  import Operation from "./operation";
2
4
 
3
5
  export interface SortField {
4
- field: string;
5
6
  direction: "asc" | "desc";
7
+ /** The parsed expression to evaluate for this sort field. */
8
+ expression: Expression;
6
9
  }
7
10
 
8
11
  /**
@@ -11,14 +14,23 @@ export interface SortField {
11
14
  * Can be attached to a RETURN operation (sorting its results),
12
15
  * or used as a standalone accumulating operation after a non-aggregate WITH.
13
16
  *
17
+ * Supports both simple field references and arbitrary expressions:
14
18
  * @example
15
19
  * ```
16
20
  * RETURN x ORDER BY x DESC
21
+ * RETURN x ORDER BY toLower(x.name) ASC
22
+ * RETURN x ORDER BY string_distance(toLower(x.name), toLower('Thomas')) ASC
17
23
  * ```
18
24
  */
19
25
  class OrderBy extends Operation {
20
26
  private _fields: SortField[];
21
27
  private _results: Record<string, any>[] = [];
28
+ /**
29
+ * Parallel array of pre-computed sort-key tuples, one entry per
30
+ * accumulated result row. Each inner array has one value per sort
31
+ * field, in the same order as `_fields`.
32
+ */
33
+ private _sortKeys: any[][] = [];
22
34
 
23
35
  constructor(fields: SortField[]) {
24
36
  super();
@@ -30,13 +42,50 @@ class OrderBy extends Operation {
30
42
  }
31
43
 
32
44
  /**
33
- * Sorts an array of records according to the sort fields.
45
+ * Evaluates every sort-field expression against the current runtime
46
+ * context and stores the resulting values. Must be called once per
47
+ * accumulated row (from `Return.run()`).
48
+ */
49
+ public captureSortKeys(): void {
50
+ this._sortKeys.push(this._fields.map((f) => f.expression.value()));
51
+ }
52
+
53
+ /**
54
+ * Sorts an array of records using the pre-computed sort keys captured
55
+ * during accumulation. When no keys have been captured (e.g.
56
+ * aggregated returns), falls back to looking up simple reference
57
+ * identifiers in each record.
34
58
  */
35
59
  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];
60
+ const useKeys = this._sortKeys.length === records.length;
61
+ // Build an index array so we can sort records and keys together.
62
+ const indices = records.map((_, i) => i);
63
+ const keys = this._sortKeys;
64
+
65
+ // Pre-compute fallback field names for when sort keys aren't
66
+ // available (aggregated returns). Simple references like `x`
67
+ // map to the column name; complex expressions have no fallback.
68
+ const fallbackFields: (string | null)[] = this._fields.map((f) => {
69
+ const root = f.expression.firstChild();
70
+ if (root instanceof Reference && f.expression.childCount() === 1) {
71
+ return (root as Reference).identifier;
72
+ }
73
+ return null;
74
+ });
75
+
76
+ indices.sort((ai, bi) => {
77
+ for (let f = 0; f < this._fields.length; f++) {
78
+ let aVal: any;
79
+ let bVal: any;
80
+ if (useKeys) {
81
+ aVal = keys[ai][f];
82
+ bVal = keys[bi][f];
83
+ } else if (fallbackFields[f] !== null) {
84
+ aVal = records[ai][fallbackFields[f]!];
85
+ bVal = records[bi][fallbackFields[f]!];
86
+ } else {
87
+ continue;
88
+ }
40
89
  let cmp = 0;
41
90
  if (aVal == null && bVal == null) cmp = 0;
42
91
  else if (aVal == null) cmp = -1;
@@ -44,11 +93,12 @@ class OrderBy extends Operation {
44
93
  else if (aVal < bVal) cmp = -1;
45
94
  else if (aVal > bVal) cmp = 1;
46
95
  if (cmp !== 0) {
47
- return direction === "desc" ? -cmp : cmp;
96
+ return this._fields[f].direction === "desc" ? -cmp : cmp;
48
97
  }
49
98
  }
50
99
  return 0;
51
100
  });
101
+ return indices.map((i) => records[i]);
52
102
  }
53
103
 
54
104
  /**
@@ -64,6 +114,7 @@ class OrderBy extends Operation {
64
114
 
65
115
  public async initialize(): Promise<void> {
66
116
  this._results = [];
117
+ this._sortKeys = [];
67
118
  await this.next?.initialize();
68
119
  }
69
120
 
@@ -49,6 +49,10 @@ class Return extends Projection {
49
49
  const value: any = typeof raw === "object" && raw !== null ? structuredClone(raw) : raw;
50
50
  record.set(alias, value);
51
51
  }
52
+ // Capture sort-key values while expression bindings are still live.
53
+ if (this._orderBy !== null) {
54
+ this._orderBy.captureSortKeys();
55
+ }
52
56
  this._results.push(Object.fromEntries(record));
53
57
  if (this._orderBy === null && this._limit !== null) {
54
58
  this._limit.increment();
@@ -876,11 +876,10 @@ class Parser extends BaseParser {
876
876
  this.expectAndSkipWhitespaceAndComments();
877
877
  const fields: SortField[] = [];
878
878
  while (true) {
879
- if (!this.token.isIdentifierOrKeyword()) {
880
- throw new Error("Expected field name in ORDER BY");
879
+ const expression: Expression | null = this.parseExpression();
880
+ if (expression === null) {
881
+ throw new Error("Expected expression in ORDER BY");
881
882
  }
882
- const field = this.token.value!;
883
- this.setNextToken();
884
883
  this.skipWhitespaceAndComments();
885
884
  let direction: "asc" | "desc" = "asc";
886
885
  if (this.token.isAsc()) {
@@ -892,7 +891,7 @@ class Parser extends BaseParser {
892
891
  this.setNextToken();
893
892
  this.skipWhitespaceAndComments();
894
893
  }
895
- fields.push({ field, direction });
894
+ fields.push({ direction, expression });
896
895
  if (this.token.isComma()) {
897
896
  this.setNextToken();
898
897
  this.skipWhitespaceAndComments();