rowpipe 1.0.0
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/README.md +401 -0
- package/benchmarks/memory-bench.ts +109 -0
- package/dist/analytics/reduce.d.ts +33 -0
- package/dist/analytics/reduce.d.ts.map +1 -0
- package/dist/analytics/reduce.js +318 -0
- package/dist/analytics/reduce.js.map +1 -0
- package/dist/analytics/schema-inference.d.ts +31 -0
- package/dist/analytics/schema-inference.d.ts.map +1 -0
- package/dist/analytics/schema-inference.js +166 -0
- package/dist/analytics/schema-inference.js.map +1 -0
- package/dist/analytics/semantic-types.d.ts +6 -0
- package/dist/analytics/semantic-types.d.ts.map +1 -0
- package/dist/analytics/semantic-types.js +40 -0
- package/dist/analytics/semantic-types.js.map +1 -0
- package/dist/analytics/stats.d.ts +102 -0
- package/dist/analytics/stats.d.ts.map +1 -0
- package/dist/analytics/stats.js +307 -0
- package/dist/analytics/stats.js.map +1 -0
- package/dist/analytics/validator.d.ts +32 -0
- package/dist/analytics/validator.d.ts.map +1 -0
- package/dist/analytics/validator.js +96 -0
- package/dist/analytics/validator.js.map +1 -0
- package/dist/cli/commands/cast.d.ts +13 -0
- package/dist/cli/commands/cast.d.ts.map +1 -0
- package/dist/cli/commands/cast.js +39 -0
- package/dist/cli/commands/cast.js.map +1 -0
- package/dist/cli/commands/convert.d.ts +15 -0
- package/dist/cli/commands/convert.d.ts.map +1 -0
- package/dist/cli/commands/convert.js +89 -0
- package/dist/cli/commands/convert.js.map +1 -0
- package/dist/cli/commands/filter.d.ts +11 -0
- package/dist/cli/commands/filter.d.ts.map +1 -0
- package/dist/cli/commands/filter.js +38 -0
- package/dist/cli/commands/filter.js.map +1 -0
- package/dist/cli/commands/inspect.d.ts +10 -0
- package/dist/cli/commands/inspect.d.ts.map +1 -0
- package/dist/cli/commands/inspect.js +134 -0
- package/dist/cli/commands/inspect.js.map +1 -0
- package/dist/cli/commands/map.d.ts +11 -0
- package/dist/cli/commands/map.d.ts.map +1 -0
- package/dist/cli/commands/map.js +43 -0
- package/dist/cli/commands/map.js.map +1 -0
- package/dist/cli/commands/reduce.d.ts +12 -0
- package/dist/cli/commands/reduce.d.ts.map +1 -0
- package/dist/cli/commands/reduce.js +49 -0
- package/dist/cli/commands/reduce.js.map +1 -0
- package/dist/cli/commands/rename.d.ts +11 -0
- package/dist/cli/commands/rename.d.ts.map +1 -0
- package/dist/cli/commands/rename.js +39 -0
- package/dist/cli/commands/rename.js.map +1 -0
- package/dist/cli/commands/sample.d.ts +13 -0
- package/dist/cli/commands/sample.d.ts.map +1 -0
- package/dist/cli/commands/sample.js +40 -0
- package/dist/cli/commands/sample.js.map +1 -0
- package/dist/cli/commands/schema.d.ts +13 -0
- package/dist/cli/commands/schema.d.ts.map +1 -0
- package/dist/cli/commands/schema.js +57 -0
- package/dist/cli/commands/schema.js.map +1 -0
- package/dist/cli/commands/select.d.ts +11 -0
- package/dist/cli/commands/select.d.ts.map +1 -0
- package/dist/cli/commands/select.js +39 -0
- package/dist/cli/commands/select.js.map +1 -0
- package/dist/cli/commands/stats.d.ts +15 -0
- package/dist/cli/commands/stats.d.ts.map +1 -0
- package/dist/cli/commands/stats.js +75 -0
- package/dist/cli/commands/stats.js.map +1 -0
- package/dist/cli/commands/validate.d.ts +12 -0
- package/dist/cli/commands/validate.d.ts.map +1 -0
- package/dist/cli/commands/validate.js +86 -0
- package/dist/cli/commands/validate.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +262 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/core/batch.d.ts +15 -0
- package/dist/core/batch.d.ts.map +1 -0
- package/dist/core/batch.js +67 -0
- package/dist/core/batch.js.map +1 -0
- package/dist/core/errors.d.ts +35 -0
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/core/errors.js +55 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/pipeline.d.ts +40 -0
- package/dist/core/pipeline.d.ts.map +1 -0
- package/dist/core/pipeline.js +120 -0
- package/dist/core/pipeline.js.map +1 -0
- package/dist/core/types.d.ts +73 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +2 -0
- package/dist/core/types.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +28 -0
- package/dist/index.js.map +1 -0
- package/dist/readers/csv.d.ts +24 -0
- package/dist/readers/csv.d.ts.map +1 -0
- package/dist/readers/csv.js +251 -0
- package/dist/readers/csv.js.map +1 -0
- package/dist/readers/index.d.ts +17 -0
- package/dist/readers/index.d.ts.map +1 -0
- package/dist/readers/index.js +74 -0
- package/dist/readers/index.js.map +1 -0
- package/dist/readers/json.d.ts +20 -0
- package/dist/readers/json.d.ts.map +1 -0
- package/dist/readers/json.js +219 -0
- package/dist/readers/json.js.map +1 -0
- package/dist/readers/jsonl.d.ts +14 -0
- package/dist/readers/jsonl.d.ts.map +1 -0
- package/dist/readers/jsonl.js +95 -0
- package/dist/readers/jsonl.js.map +1 -0
- package/dist/readers/xlsx.d.ts +18 -0
- package/dist/readers/xlsx.d.ts.map +1 -0
- package/dist/readers/xlsx.js +138 -0
- package/dist/readers/xlsx.js.map +1 -0
- package/dist/transforms/cast.d.ts +21 -0
- package/dist/transforms/cast.d.ts.map +1 -0
- package/dist/transforms/cast.js +182 -0
- package/dist/transforms/cast.js.map +1 -0
- package/dist/transforms/expression.d.ts +67 -0
- package/dist/transforms/expression.d.ts.map +1 -0
- package/dist/transforms/expression.js +943 -0
- package/dist/transforms/expression.js.map +1 -0
- package/dist/transforms/filter.d.ts +6 -0
- package/dist/transforms/filter.d.ts.map +1 -0
- package/dist/transforms/filter.js +31 -0
- package/dist/transforms/filter.js.map +1 -0
- package/dist/transforms/map.d.ts +12 -0
- package/dist/transforms/map.d.ts.map +1 -0
- package/dist/transforms/map.js +76 -0
- package/dist/transforms/map.js.map +1 -0
- package/dist/transforms/rename.d.ts +10 -0
- package/dist/transforms/rename.d.ts.map +1 -0
- package/dist/transforms/rename.js +39 -0
- package/dist/transforms/rename.js.map +1 -0
- package/dist/transforms/sample.d.ts +10 -0
- package/dist/transforms/sample.d.ts.map +1 -0
- package/dist/transforms/sample.js +47 -0
- package/dist/transforms/sample.js.map +1 -0
- package/dist/transforms/select.d.ts +6 -0
- package/dist/transforms/select.d.ts.map +1 -0
- package/dist/transforms/select.js +63 -0
- package/dist/transforms/select.js.map +1 -0
- package/dist/utils/compression.d.ts +14 -0
- package/dist/utils/compression.d.ts.map +1 -0
- package/dist/utils/compression.js +38 -0
- package/dist/utils/compression.js.map +1 -0
- package/dist/utils/formatting.d.ts +6 -0
- package/dist/utils/formatting.d.ts.map +1 -0
- package/dist/utils/formatting.js +56 -0
- package/dist/utils/formatting.js.map +1 -0
- package/dist/utils/progress.d.ts +13 -0
- package/dist/utils/progress.d.ts.map +1 -0
- package/dist/utils/progress.js +27 -0
- package/dist/utils/progress.js.map +1 -0
- package/dist/writers/csv.d.ts +18 -0
- package/dist/writers/csv.d.ts.map +1 -0
- package/dist/writers/csv.js +84 -0
- package/dist/writers/csv.js.map +1 -0
- package/dist/writers/index.d.ts +17 -0
- package/dist/writers/index.d.ts.map +1 -0
- package/dist/writers/index.js +73 -0
- package/dist/writers/index.js.map +1 -0
- package/dist/writers/json.d.ts +12 -0
- package/dist/writers/json.d.ts.map +1 -0
- package/dist/writers/json.js +42 -0
- package/dist/writers/json.js.map +1 -0
- package/dist/writers/jsonl.d.ts +12 -0
- package/dist/writers/jsonl.d.ts.map +1 -0
- package/dist/writers/jsonl.js +41 -0
- package/dist/writers/jsonl.js.map +1 -0
- package/dist/writers/xlsx.d.ts +13 -0
- package/dist/writers/xlsx.d.ts.map +1 -0
- package/dist/writers/xlsx.js +65 -0
- package/dist/writers/xlsx.js.map +1 -0
- package/package.json +44 -0
- package/src/analytics/reduce.ts +388 -0
- package/src/analytics/schema-inference.ts +217 -0
- package/src/analytics/semantic-types.ts +36 -0
- package/src/analytics/stats.ts +383 -0
- package/src/analytics/validator.ts +134 -0
- package/src/cli/commands/cast.ts +60 -0
- package/src/cli/commands/convert.ts +128 -0
- package/src/cli/commands/filter.ts +58 -0
- package/src/cli/commands/inspect.ts +193 -0
- package/src/cli/commands/map.ts +66 -0
- package/src/cli/commands/reduce.ts +74 -0
- package/src/cli/commands/rename.ts +59 -0
- package/src/cli/commands/sample.ts +61 -0
- package/src/cli/commands/schema.ts +86 -0
- package/src/cli/commands/select.ts +59 -0
- package/src/cli/commands/stats.ts +100 -0
- package/src/cli/commands/validate.ts +123 -0
- package/src/cli/index.ts +285 -0
- package/src/core/batch.ts +81 -0
- package/src/core/errors.ts +81 -0
- package/src/core/pipeline.ts +150 -0
- package/src/core/types.ts +110 -0
- package/src/index.ts +52 -0
- package/src/readers/csv.ts +302 -0
- package/src/readers/index.ts +88 -0
- package/src/readers/json.ts +256 -0
- package/src/readers/jsonl.ts +126 -0
- package/src/readers/xlsx.ts +182 -0
- package/src/transforms/cast.ts +212 -0
- package/src/transforms/expression.ts +1114 -0
- package/src/transforms/filter.ts +38 -0
- package/src/transforms/map.ts +99 -0
- package/src/transforms/rename.ts +47 -0
- package/src/transforms/sample.ts +57 -0
- package/src/transforms/select.ts +64 -0
- package/src/utils/compression.ts +43 -0
- package/src/utils/formatting.ts +75 -0
- package/src/utils/progress.ts +37 -0
- package/src/writers/csv.ts +106 -0
- package/src/writers/index.ts +87 -0
- package/src/writers/json.ts +52 -0
- package/src/writers/jsonl.ts +49 -0
- package/src/writers/xlsx.ts +87 -0
- package/tests/analytics.test.ts +119 -0
- package/tests/cli-integration.test.ts +187 -0
- package/tests/map-reduce.test.ts +223 -0
- package/tests/readers-writers.test.ts +189 -0
- package/tests/transforms.test.ts +199 -0
- package/tsconfig.json +21 -0
- package/vitest.config.ts +9 -0
|
@@ -0,0 +1,1114 @@
|
|
|
1
|
+
import { InvalidArgumentError } from "../core/errors.js";
|
|
2
|
+
import type { Row } from "../core/types.js";
|
|
3
|
+
import { detectSemanticType } from "../analytics/semantic-types.js";
|
|
4
|
+
|
|
5
|
+
// Token Types
|
|
6
|
+
type TokenType =
|
|
7
|
+
| "NUMBER"
|
|
8
|
+
| "STRING"
|
|
9
|
+
| "BOOLEAN"
|
|
10
|
+
| "NULL"
|
|
11
|
+
| "IDENTIFIER"
|
|
12
|
+
| "OP"
|
|
13
|
+
| "PIPE"
|
|
14
|
+
| "LPAREN"
|
|
15
|
+
| "RPAREN"
|
|
16
|
+
| "COMMA"
|
|
17
|
+
| "EOF";
|
|
18
|
+
|
|
19
|
+
interface Token {
|
|
20
|
+
type: TokenType;
|
|
21
|
+
value: string | number | boolean | null;
|
|
22
|
+
pos: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Tokenizer
|
|
26
|
+
export class Lexer {
|
|
27
|
+
private input: string;
|
|
28
|
+
private cursor = 0;
|
|
29
|
+
|
|
30
|
+
constructor(input: string) {
|
|
31
|
+
this.input = input;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
private isWhitespace(c: string): boolean {
|
|
35
|
+
return c === " " || c === "\t" || c === "\n" || c === "\r";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private isDigit(c: string): boolean {
|
|
39
|
+
return c >= "0" && c <= "9";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private isAlpha(c: string): boolean {
|
|
43
|
+
return (
|
|
44
|
+
(c >= "a" && c <= "z") ||
|
|
45
|
+
(c >= "A" && c <= "Z") ||
|
|
46
|
+
c === "_" ||
|
|
47
|
+
c === "$" ||
|
|
48
|
+
c === "."
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
tokenize(): Token[] {
|
|
53
|
+
const tokens: Token[] = [];
|
|
54
|
+
|
|
55
|
+
while (this.cursor < this.input.length) {
|
|
56
|
+
const char = this.input[this.cursor]!;
|
|
57
|
+
|
|
58
|
+
if (this.isWhitespace(char)) {
|
|
59
|
+
this.cursor++;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const pos = this.cursor;
|
|
64
|
+
|
|
65
|
+
// Parentheses & comma
|
|
66
|
+
if (char === "(") {
|
|
67
|
+
tokens.push({ type: "LPAREN", value: "(", pos });
|
|
68
|
+
this.cursor++;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (char === ")") {
|
|
72
|
+
tokens.push({ type: "RPAREN", value: ")", pos });
|
|
73
|
+
this.cursor++;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (char === ",") {
|
|
77
|
+
tokens.push({ type: "COMMA", value: ",", pos });
|
|
78
|
+
this.cursor++;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Pipe operator | vs ||
|
|
83
|
+
if (char === "|") {
|
|
84
|
+
if (this.cursor + 1 < this.input.length && this.input[this.cursor + 1] === "|") {
|
|
85
|
+
tokens.push({ type: "OP", value: "||", pos });
|
|
86
|
+
this.cursor += 2;
|
|
87
|
+
continue;
|
|
88
|
+
} else {
|
|
89
|
+
tokens.push({ type: "PIPE", value: "|", pos });
|
|
90
|
+
this.cursor++;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Strings (single or double quoted)
|
|
96
|
+
if (char === '"' || char === "'") {
|
|
97
|
+
const quoteChar = char;
|
|
98
|
+
let strVal = "";
|
|
99
|
+
this.cursor++;
|
|
100
|
+
while (this.cursor < this.input.length) {
|
|
101
|
+
const c = this.input[this.cursor]!;
|
|
102
|
+
if (c === "\\") {
|
|
103
|
+
this.cursor++;
|
|
104
|
+
if (this.cursor < this.input.length) {
|
|
105
|
+
strVal += this.input[this.cursor]!;
|
|
106
|
+
this.cursor++;
|
|
107
|
+
}
|
|
108
|
+
} else if (c === quoteChar) {
|
|
109
|
+
this.cursor++;
|
|
110
|
+
break;
|
|
111
|
+
} else {
|
|
112
|
+
strVal += c;
|
|
113
|
+
this.cursor++;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
tokens.push({ type: "STRING", value: strVal, pos });
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Numbers
|
|
121
|
+
if (this.isDigit(char) || (char === "-" && this.isDigit(this.input[this.cursor + 1] || ""))) {
|
|
122
|
+
let numStr = char;
|
|
123
|
+
this.cursor++;
|
|
124
|
+
let hasDot = false;
|
|
125
|
+
while (this.cursor < this.input.length) {
|
|
126
|
+
const c = this.input[this.cursor]!;
|
|
127
|
+
if (this.isDigit(c)) {
|
|
128
|
+
numStr += c;
|
|
129
|
+
this.cursor++;
|
|
130
|
+
} else if (c === "." && !hasDot) {
|
|
131
|
+
hasDot = true;
|
|
132
|
+
numStr += c;
|
|
133
|
+
this.cursor++;
|
|
134
|
+
} else {
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
tokens.push({ type: "NUMBER", value: Number.parseFloat(numStr), pos });
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Multi-character operators: ==, !=, >=, <=, &&
|
|
143
|
+
const twoChar = this.input.slice(this.cursor, this.cursor + 2);
|
|
144
|
+
if (["==", "!=", ">=", "<=", "&&"].includes(twoChar)) {
|
|
145
|
+
tokens.push({ type: "OP", value: twoChar, pos });
|
|
146
|
+
this.cursor += 2;
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Single character operators: >, <, !, +, -, *, /, %
|
|
151
|
+
if ([">", "<", "!", "+", "-", "*", "/", "%"].includes(char)) {
|
|
152
|
+
tokens.push({ type: "OP", value: char, pos });
|
|
153
|
+
this.cursor++;
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Identifiers / Keywords / Column Names (may be backtick quoted e.g. `user name`)
|
|
158
|
+
if (char === "`") {
|
|
159
|
+
let identVal = "";
|
|
160
|
+
this.cursor++;
|
|
161
|
+
while (this.cursor < this.input.length && this.input[this.cursor] !== "`") {
|
|
162
|
+
identVal += this.input[this.cursor]!;
|
|
163
|
+
this.cursor++;
|
|
164
|
+
}
|
|
165
|
+
if (this.cursor < this.input.length && this.input[this.cursor] === "`") {
|
|
166
|
+
this.cursor++;
|
|
167
|
+
}
|
|
168
|
+
tokens.push({ type: "IDENTIFIER", value: identVal, pos });
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (this.isAlpha(char)) {
|
|
173
|
+
let ident = "";
|
|
174
|
+
while (this.cursor < this.input.length && (this.isAlpha(this.input[this.cursor]!) || this.isDigit(this.input[this.cursor]!))) {
|
|
175
|
+
ident += this.input[this.cursor]!;
|
|
176
|
+
this.cursor++;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (ident === "true") {
|
|
180
|
+
tokens.push({ type: "BOOLEAN", value: true, pos });
|
|
181
|
+
} else if (ident === "false") {
|
|
182
|
+
tokens.push({ type: "BOOLEAN", value: false, pos });
|
|
183
|
+
} else if (ident === "null" || ident === "NULL") {
|
|
184
|
+
tokens.push({ type: "NULL", value: null, pos });
|
|
185
|
+
} else if (ident === "and" || ident === "AND") {
|
|
186
|
+
tokens.push({ type: "OP", value: "&&", pos });
|
|
187
|
+
} else if (ident === "or" || ident === "OR") {
|
|
188
|
+
tokens.push({ type: "OP", value: "||", pos });
|
|
189
|
+
} else if (ident === "not" || ident === "NOT") {
|
|
190
|
+
tokens.push({ type: "OP", value: "!", pos });
|
|
191
|
+
} else {
|
|
192
|
+
tokens.push({ type: "IDENTIFIER", value: ident, pos });
|
|
193
|
+
}
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
throw new InvalidArgumentError(
|
|
198
|
+
`Unexpected character "${char}" in filter expression at position ${pos}`
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
tokens.push({ type: "EOF", value: null, pos: this.cursor });
|
|
203
|
+
return tokens;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// AST Nodes
|
|
208
|
+
export type ASTNode =
|
|
209
|
+
| { type: "Literal"; value: unknown }
|
|
210
|
+
| { type: "Identifier"; name: string }
|
|
211
|
+
| { type: "UnaryOp"; operator: string; argument: ASTNode }
|
|
212
|
+
| { type: "BinaryOp"; operator: string; left: ASTNode; right: ASTNode }
|
|
213
|
+
| { type: "CallExpr"; callee: string; args: ASTNode[] };
|
|
214
|
+
|
|
215
|
+
export class Parser {
|
|
216
|
+
private tokens: Token[];
|
|
217
|
+
private current = 0;
|
|
218
|
+
|
|
219
|
+
constructor(tokens: Token[]) {
|
|
220
|
+
this.tokens = tokens;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
private peek(): Token {
|
|
224
|
+
return this.tokens[this.current] || { type: "EOF", value: null, pos: -1 };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
private advance(): Token {
|
|
228
|
+
const t = this.peek();
|
|
229
|
+
this.current++;
|
|
230
|
+
return t;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
private match(...expectedValues: string[]): boolean {
|
|
234
|
+
const t = this.peek();
|
|
235
|
+
if (expectedValues.includes(String(t.value))) {
|
|
236
|
+
this.advance();
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
parse(): ASTNode {
|
|
243
|
+
const expr = this.parseLogicalOr();
|
|
244
|
+
if (this.peek().type !== "EOF") {
|
|
245
|
+
throw new InvalidArgumentError(
|
|
246
|
+
`Unexpected token "${String(this.peek().value)}" at position ${this.peek().pos}`
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
return expr;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
private parseLogicalOr(): ASTNode {
|
|
253
|
+
let left = this.parseLogicalAnd();
|
|
254
|
+
|
|
255
|
+
while (this.match("||")) {
|
|
256
|
+
const right = this.parseLogicalAnd();
|
|
257
|
+
left = { type: "BinaryOp", operator: "||", left, right };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return left;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
private parseLogicalAnd(): ASTNode {
|
|
264
|
+
let left = this.parseEquality();
|
|
265
|
+
|
|
266
|
+
while (this.match("&&")) {
|
|
267
|
+
const right = this.parseEquality();
|
|
268
|
+
left = { type: "BinaryOp", operator: "&&", left, right };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return left;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
private parseEquality(): ASTNode {
|
|
275
|
+
let left = this.parseRelational();
|
|
276
|
+
|
|
277
|
+
while (this.peek().value === "==" || this.peek().value === "!=") {
|
|
278
|
+
const op = String(this.advance().value);
|
|
279
|
+
const right = this.parseRelational();
|
|
280
|
+
left = { type: "BinaryOp", operator: op, left, right };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return left;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
private parseRelational(): ASTNode {
|
|
287
|
+
let left = this.parseAdditive();
|
|
288
|
+
|
|
289
|
+
while (
|
|
290
|
+
this.peek().value === ">" ||
|
|
291
|
+
this.peek().value === ">=" ||
|
|
292
|
+
this.peek().value === "<" ||
|
|
293
|
+
this.peek().value === "<="
|
|
294
|
+
) {
|
|
295
|
+
const op = String(this.advance().value);
|
|
296
|
+
const right = this.parseAdditive();
|
|
297
|
+
left = { type: "BinaryOp", operator: op, left, right };
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return left;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
private parseAdditive(): ASTNode {
|
|
304
|
+
let left = this.parseMultiplicative();
|
|
305
|
+
|
|
306
|
+
while (this.peek().value === "+" || this.peek().value === "-") {
|
|
307
|
+
const op = String(this.advance().value);
|
|
308
|
+
const right = this.parseMultiplicative();
|
|
309
|
+
left = { type: "BinaryOp", operator: op, left, right };
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return left;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
private parseMultiplicative(): ASTNode {
|
|
316
|
+
let left = this.parsePipe();
|
|
317
|
+
|
|
318
|
+
while (
|
|
319
|
+
this.peek().value === "*" ||
|
|
320
|
+
this.peek().value === "/" ||
|
|
321
|
+
this.peek().value === "%"
|
|
322
|
+
) {
|
|
323
|
+
const op = String(this.advance().value);
|
|
324
|
+
const right = this.parsePipe();
|
|
325
|
+
left = { type: "BinaryOp", operator: op, left, right };
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return left;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Parses pipe expressions e.g. email | trim | lower | startsWith("admin")
|
|
333
|
+
*/
|
|
334
|
+
private parsePipe(): ASTNode {
|
|
335
|
+
let left = this.parseUnary();
|
|
336
|
+
|
|
337
|
+
while (this.peek().type === "PIPE") {
|
|
338
|
+
this.advance(); // consume '|'
|
|
339
|
+
|
|
340
|
+
const token = this.peek();
|
|
341
|
+
if (token.type !== "IDENTIFIER") {
|
|
342
|
+
throw new InvalidArgumentError(
|
|
343
|
+
`Expected function or identifier after '|' at position ${token.pos}`
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const fnName = String(this.advance().value);
|
|
348
|
+
const args: ASTNode[] = [left]; // left becomes first argument
|
|
349
|
+
|
|
350
|
+
if (this.peek().type === "LPAREN") {
|
|
351
|
+
this.advance(); // consume '('
|
|
352
|
+
if (this.peek().type !== "RPAREN") {
|
|
353
|
+
args.push(this.parseLogicalOr());
|
|
354
|
+
while (this.peek().type === "COMMA") {
|
|
355
|
+
this.advance(); // consume ','
|
|
356
|
+
args.push(this.parseLogicalOr());
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (this.peek().type !== "RPAREN") {
|
|
361
|
+
throw new InvalidArgumentError(
|
|
362
|
+
`Expected closing ')' at position ${this.peek().pos}`
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
this.advance(); // consume ')'
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
left = { type: "CallExpr", callee: fnName, args };
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return left;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
private parseUnary(): ASTNode {
|
|
375
|
+
if (this.peek().value === "!" || this.peek().value === "-") {
|
|
376
|
+
const op = String(this.advance().value);
|
|
377
|
+
const argument = this.parseUnary();
|
|
378
|
+
return { type: "UnaryOp", operator: op, argument };
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return this.parsePrimary();
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
private parsePrimary(): ASTNode {
|
|
385
|
+
const token = this.peek();
|
|
386
|
+
|
|
387
|
+
if (token.type === "NUMBER" || token.type === "STRING" || token.type === "BOOLEAN" || token.type === "NULL") {
|
|
388
|
+
this.advance();
|
|
389
|
+
return { type: "Literal", value: token.value };
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (token.type === "IDENTIFIER") {
|
|
393
|
+
const name = String(this.advance().value);
|
|
394
|
+
|
|
395
|
+
// Check if this is a function call: contains(name, "Ahmet")
|
|
396
|
+
if (this.peek().type === "LPAREN") {
|
|
397
|
+
this.advance(); // consume '('
|
|
398
|
+
const args: ASTNode[] = [];
|
|
399
|
+
|
|
400
|
+
if (this.peek().type !== "RPAREN") {
|
|
401
|
+
args.push(this.parseLogicalOr());
|
|
402
|
+
while (this.peek().type === "COMMA") {
|
|
403
|
+
this.advance(); // consume ','
|
|
404
|
+
args.push(this.parseLogicalOr());
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (this.peek().type !== "RPAREN") {
|
|
409
|
+
throw new InvalidArgumentError(
|
|
410
|
+
`Expected closing ')' after function arguments at position ${this.peek().pos}`
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
this.advance(); // consume ')'
|
|
414
|
+
|
|
415
|
+
return { type: "CallExpr", callee: name, args };
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return { type: "Identifier", name };
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (token.type === "LPAREN") {
|
|
422
|
+
this.advance(); // consume '('
|
|
423
|
+
const expr = this.parseLogicalOr();
|
|
424
|
+
if (this.peek().type !== "RPAREN") {
|
|
425
|
+
throw new InvalidArgumentError(
|
|
426
|
+
`Expected closing ')' at position ${this.peek().pos}`
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
this.advance(); // consume ')'
|
|
430
|
+
return expr;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
throw new InvalidArgumentError(
|
|
434
|
+
`Unexpected token "${String(token.value)}" at position ${token.pos}`
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Safely accesses a nested path in an object or parsed JSON (e.g. user.address.city).
|
|
441
|
+
*/
|
|
442
|
+
function getNestedValue(obj: unknown, path: string): unknown {
|
|
443
|
+
if (obj === null || obj === undefined) return null;
|
|
444
|
+
let current: unknown = obj;
|
|
445
|
+
|
|
446
|
+
if (typeof current === "string") {
|
|
447
|
+
try {
|
|
448
|
+
current = JSON.parse(current);
|
|
449
|
+
} catch {
|
|
450
|
+
return null;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const parts = path.split(".");
|
|
455
|
+
for (const part of parts) {
|
|
456
|
+
if (current === null || current === undefined || typeof current !== "object") {
|
|
457
|
+
return null;
|
|
458
|
+
}
|
|
459
|
+
current = (current as Record<string, unknown>)[part];
|
|
460
|
+
}
|
|
461
|
+
return current;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function resolveIdentifier(name: string, parts: string[] | null, row: Row): unknown {
|
|
465
|
+
if (!parts) {
|
|
466
|
+
const val = row[name];
|
|
467
|
+
return val === undefined || val === null || val === "" ? null : val;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
if (name in row && row[name] !== undefined) {
|
|
471
|
+
const val = row[name];
|
|
472
|
+
return val === null || val === "" ? null : val;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
for (let i = 1; i <= parts.length; i++) {
|
|
476
|
+
const rootKey = parts.slice(0, i).join(".");
|
|
477
|
+
if (rootKey in row && row[rootKey] !== undefined) {
|
|
478
|
+
const restPath = parts.slice(i).join(".");
|
|
479
|
+
if (restPath === "") {
|
|
480
|
+
const val = row[rootKey];
|
|
481
|
+
return val === null || val === "" ? null : val;
|
|
482
|
+
}
|
|
483
|
+
const nested = getNestedValue(row[rootKey], restPath);
|
|
484
|
+
if (nested !== undefined && nested !== null && nested !== "") {
|
|
485
|
+
return nested;
|
|
486
|
+
}
|
|
487
|
+
return null;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const firstPart = parts[0];
|
|
492
|
+
if (firstPart === "payload" || firstPart === "row" || firstPart === "data") {
|
|
493
|
+
const restPath = parts.slice(1).join(".");
|
|
494
|
+
const nested = getNestedValue(row, restPath);
|
|
495
|
+
if (nested !== undefined && nested !== null && nested !== "") {
|
|
496
|
+
return nested;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
return null;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
type CompiledNodeFn = (row: Row) => unknown;
|
|
504
|
+
|
|
505
|
+
function compileCallFunction(callee: string, argFns: CompiledNodeFn[]): CompiledNodeFn {
|
|
506
|
+
const fnName = callee.toLowerCase();
|
|
507
|
+
|
|
508
|
+
switch (fnName) {
|
|
509
|
+
// --- 1. Set & Range ---
|
|
510
|
+
case "in":
|
|
511
|
+
return (row) => {
|
|
512
|
+
const val = argFns[0]!(row);
|
|
513
|
+
for (let i = 1; i < argFns.length; i++) {
|
|
514
|
+
const t = argFns[i]!(row);
|
|
515
|
+
if (String(t) === String(val) || (typeof t === "number" && Number(t) === Number(val))) return true;
|
|
516
|
+
}
|
|
517
|
+
return false;
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
case "notin":
|
|
521
|
+
return (row) => {
|
|
522
|
+
const val = argFns[0]!(row);
|
|
523
|
+
for (let i = 1; i < argFns.length; i++) {
|
|
524
|
+
const t = argFns[i]!(row);
|
|
525
|
+
if (String(t) === String(val) || (typeof t === "number" && Number(t) === Number(val))) return false;
|
|
526
|
+
}
|
|
527
|
+
return true;
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
case "between": {
|
|
531
|
+
const [valFn, minFn, maxFn] = argFns;
|
|
532
|
+
return (row) => {
|
|
533
|
+
const val = Number(valFn!(row));
|
|
534
|
+
const min = Number(minFn!(row));
|
|
535
|
+
const max = Number(maxFn!(row));
|
|
536
|
+
if (Number.isNaN(val) || Number.isNaN(min) || Number.isNaN(max)) return false;
|
|
537
|
+
return val >= min && val <= max;
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// --- 2. Date & Time ---
|
|
542
|
+
case "year": {
|
|
543
|
+
const [fn] = argFns;
|
|
544
|
+
return (row) => {
|
|
545
|
+
const d = new Date(String(fn!(row) ?? ""));
|
|
546
|
+
return Number.isNaN(d.getTime()) ? null : d.getUTCFullYear();
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
case "month": {
|
|
551
|
+
const [fn] = argFns;
|
|
552
|
+
return (row) => {
|
|
553
|
+
const d = new Date(String(fn!(row) ?? ""));
|
|
554
|
+
return Number.isNaN(d.getTime()) ? null : d.getUTCMonth() + 1;
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
case "day": {
|
|
559
|
+
const [fn] = argFns;
|
|
560
|
+
return (row) => {
|
|
561
|
+
const d = new Date(String(fn!(row) ?? ""));
|
|
562
|
+
return Number.isNaN(d.getTime()) ? null : d.getUTCDate();
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
case "hour": {
|
|
567
|
+
const [fn] = argFns;
|
|
568
|
+
return (row) => {
|
|
569
|
+
const d = new Date(String(fn!(row) ?? ""));
|
|
570
|
+
return Number.isNaN(d.getTime()) ? null : d.getUTCHours();
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
case "minute": {
|
|
575
|
+
const [fn] = argFns;
|
|
576
|
+
return (row) => {
|
|
577
|
+
const d = new Date(String(fn!(row) ?? ""));
|
|
578
|
+
return Number.isNaN(d.getTime()) ? null : d.getUTCMinutes();
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
case "dayofweek": {
|
|
583
|
+
const [fn] = argFns;
|
|
584
|
+
return (row) => {
|
|
585
|
+
const d = new Date(String(fn!(row) ?? ""));
|
|
586
|
+
return Number.isNaN(d.getTime()) ? null : d.getUTCDay();
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
case "datediff": {
|
|
591
|
+
const [d1Fn, d2Fn, unitFn] = argFns;
|
|
592
|
+
return (row) => {
|
|
593
|
+
const d1 = new Date(String(d1Fn!(row) ?? ""));
|
|
594
|
+
const d2 = new Date(String(d2Fn!(row) ?? ""));
|
|
595
|
+
if (Number.isNaN(d1.getTime()) || Number.isNaN(d2.getTime())) return null;
|
|
596
|
+
|
|
597
|
+
const diffMs = d1.getTime() - d2.getTime();
|
|
598
|
+
const unit = String(unitFn ? unitFn(row) ?? "days" : "days").toLowerCase();
|
|
599
|
+
|
|
600
|
+
if (unit.startsWith("hour")) return Math.round(diffMs / (1000 * 60 * 60));
|
|
601
|
+
if (unit.startsWith("minute")) return Math.round(diffMs / (1000 * 60));
|
|
602
|
+
if (unit.startsWith("second")) return Math.round(diffMs / 1000);
|
|
603
|
+
if (unit.startsWith("year")) return Math.round(diffMs / (1000 * 60 * 60 * 24 * 365.25));
|
|
604
|
+
return Math.round(diffMs / (1000 * 60 * 60 * 24));
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
case "ispast": {
|
|
609
|
+
const [fn] = argFns;
|
|
610
|
+
return (row) => {
|
|
611
|
+
const d = new Date(String(fn!(row) ?? ""));
|
|
612
|
+
return Number.isNaN(d.getTime()) ? false : d.getTime() < Date.now();
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
case "isfuture": {
|
|
617
|
+
const [fn] = argFns;
|
|
618
|
+
return (row) => {
|
|
619
|
+
const d = new Date(String(fn!(row) ?? ""));
|
|
620
|
+
return Number.isNaN(d.getTime()) ? false : d.getTime() > Date.now();
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
case "istoday": {
|
|
625
|
+
const [fn] = argFns;
|
|
626
|
+
return (row) => {
|
|
627
|
+
const d = new Date(String(fn!(row) ?? ""));
|
|
628
|
+
if (Number.isNaN(d.getTime())) return false;
|
|
629
|
+
const today = new Date();
|
|
630
|
+
return (
|
|
631
|
+
d.getUTCFullYear() === today.getUTCFullYear() &&
|
|
632
|
+
d.getUTCMonth() === today.getUTCMonth() &&
|
|
633
|
+
d.getUTCDate() === today.getUTCDate()
|
|
634
|
+
);
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// --- 3. String & Text ---
|
|
639
|
+
case "concat":
|
|
640
|
+
return (row) => argFns.map((f) => {
|
|
641
|
+
const a = f(row);
|
|
642
|
+
return a === null || a === undefined ? "" : String(a);
|
|
643
|
+
}).join("");
|
|
644
|
+
|
|
645
|
+
case "substr":
|
|
646
|
+
case "substring": {
|
|
647
|
+
const [sFn, startFn, lenFn] = argFns;
|
|
648
|
+
return (row) => {
|
|
649
|
+
const str = String(sFn!(row) ?? "");
|
|
650
|
+
const start = Number(startFn ? startFn(row) ?? 0 : 0);
|
|
651
|
+
const length = lenFn ? Number(lenFn(row)) : undefined;
|
|
652
|
+
return length !== undefined ? str.slice(start, start + length) : str.slice(start);
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
case "replace": {
|
|
657
|
+
const [sFn, searchFn, repFn] = argFns;
|
|
658
|
+
return (row) => {
|
|
659
|
+
const str = String(sFn!(row) ?? "");
|
|
660
|
+
const search = String(searchFn!(row) ?? "");
|
|
661
|
+
const replacement = String(repFn!(row) ?? "");
|
|
662
|
+
return str.replaceAll(search, replacement);
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
case "splitindex": {
|
|
667
|
+
const [sFn, delimFn, idxFn] = argFns;
|
|
668
|
+
return (row) => {
|
|
669
|
+
const str = String(sFn!(row) ?? "");
|
|
670
|
+
const delimiter = String(delimFn ? delimFn(row) ?? "," : ",");
|
|
671
|
+
const index = Number(idxFn ? idxFn(row) ?? 0 : 0);
|
|
672
|
+
const parts = str.split(delimiter);
|
|
673
|
+
return parts[index] ?? "";
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
case "indexof": {
|
|
678
|
+
const [sFn, searchFn] = argFns;
|
|
679
|
+
return (row) => {
|
|
680
|
+
const str = String(sFn!(row) ?? "");
|
|
681
|
+
const search = String(searchFn!(row) ?? "");
|
|
682
|
+
return str.indexOf(search);
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
case "padleft": {
|
|
687
|
+
const [sFn, lenFn, charFn] = argFns;
|
|
688
|
+
return (row) => {
|
|
689
|
+
const str = String(sFn!(row) ?? "");
|
|
690
|
+
const len = Number(lenFn!(row) ?? 0);
|
|
691
|
+
const char = String(charFn ? charFn(row) ?? " " : " ");
|
|
692
|
+
return str.padStart(len, char);
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
case "padright": {
|
|
697
|
+
const [sFn, lenFn, charFn] = argFns;
|
|
698
|
+
return (row) => {
|
|
699
|
+
const str = String(sFn!(row) ?? "");
|
|
700
|
+
const len = Number(lenFn!(row) ?? 0);
|
|
701
|
+
const char = String(charFn ? charFn(row) ?? " " : " ");
|
|
702
|
+
return str.padEnd(len, char);
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
case "contains": {
|
|
707
|
+
const [sFn, tFn] = argFns;
|
|
708
|
+
return (row) => {
|
|
709
|
+
const str = String(sFn!(row) ?? "").toLowerCase();
|
|
710
|
+
const target = String(tFn!(row) ?? "").toLowerCase();
|
|
711
|
+
return str.includes(target);
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
case "startswith": {
|
|
716
|
+
const [sFn, pFn] = argFns;
|
|
717
|
+
return (row) => {
|
|
718
|
+
const str = String(sFn!(row) ?? "");
|
|
719
|
+
const prefix = String(pFn!(row) ?? "");
|
|
720
|
+
return str.startsWith(prefix);
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
case "endswith": {
|
|
725
|
+
const [sFn, pFn] = argFns;
|
|
726
|
+
return (row) => {
|
|
727
|
+
const str = String(sFn!(row) ?? "");
|
|
728
|
+
const suffix = String(pFn!(row) ?? "");
|
|
729
|
+
return str.endsWith(suffix);
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
case "lower": {
|
|
734
|
+
const [fn] = argFns;
|
|
735
|
+
return (row) => String(fn!(row) ?? "").toLowerCase();
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
case "upper": {
|
|
739
|
+
const [fn] = argFns;
|
|
740
|
+
return (row) => String(fn!(row) ?? "").toUpperCase();
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
case "trim": {
|
|
744
|
+
const [fn] = argFns;
|
|
745
|
+
return (row) => String(fn!(row) ?? "").trim();
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
case "length": {
|
|
749
|
+
const [fn] = argFns;
|
|
750
|
+
return (row) => String(fn!(row) ?? "").length;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
case "matches": {
|
|
754
|
+
const [sFn, pFn] = argFns;
|
|
755
|
+
return (row) => {
|
|
756
|
+
const str = String(sFn!(row) ?? "");
|
|
757
|
+
const pattern = String(pFn!(row) ?? "");
|
|
758
|
+
try {
|
|
759
|
+
return new RegExp(pattern).test(str);
|
|
760
|
+
} catch {
|
|
761
|
+
return false;
|
|
762
|
+
}
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// --- 4. Math & Numeric ---
|
|
767
|
+
case "abs": {
|
|
768
|
+
const [fn] = argFns;
|
|
769
|
+
return (row) => Math.abs(Number(fn!(row) ?? 0));
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
case "round": {
|
|
773
|
+
const [valFn, decFn] = argFns;
|
|
774
|
+
return (row) => {
|
|
775
|
+
const num = Number(valFn!(row) ?? 0);
|
|
776
|
+
const decimals = decFn ? Number(decFn(row) ?? 0) : 0;
|
|
777
|
+
const factor = 10 ** decimals;
|
|
778
|
+
return Math.round(num * factor) / factor;
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
case "ceil": {
|
|
783
|
+
const [fn] = argFns;
|
|
784
|
+
return (row) => Math.ceil(Number(fn!(row) ?? 0));
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
case "floor": {
|
|
788
|
+
const [fn] = argFns;
|
|
789
|
+
return (row) => Math.floor(Number(fn!(row) ?? 0));
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
case "clamp": {
|
|
793
|
+
const [valFn, minFn, maxFn] = argFns;
|
|
794
|
+
return (row) => {
|
|
795
|
+
const num = Number(valFn!(row) ?? 0);
|
|
796
|
+
const min = minFn ? Number(minFn(row) ?? Number.NEGATIVE_INFINITY) : Number.NEGATIVE_INFINITY;
|
|
797
|
+
const max = maxFn ? Number(maxFn(row) ?? Number.POSITIVE_INFINITY) : Number.POSITIVE_INFINITY;
|
|
798
|
+
return Math.min(Math.max(num, min), max);
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
case "min":
|
|
803
|
+
return (row) => Math.min(...argFns.map((f) => Number(f(row) ?? 0)));
|
|
804
|
+
|
|
805
|
+
case "max":
|
|
806
|
+
return (row) => Math.max(...argFns.map((f) => Number(f(row) ?? 0)));
|
|
807
|
+
|
|
808
|
+
case "sqrt": {
|
|
809
|
+
const [fn] = argFns;
|
|
810
|
+
return (row) => Math.sqrt(Number(fn!(row) ?? 0));
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
case "pow": {
|
|
814
|
+
const [bFn, eFn] = argFns;
|
|
815
|
+
return (row) => Number(bFn!(row) ?? 0) ** Number(eFn ? eFn(row) ?? 1 : 1);
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
case "log": {
|
|
819
|
+
const [fn] = argFns;
|
|
820
|
+
return (row) => Math.log(Number(fn!(row) ?? 1));
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// --- 5. Control Flow & Nulls ---
|
|
824
|
+
case "if":
|
|
825
|
+
case "iif": {
|
|
826
|
+
const [condFn, thenFn, elseFn] = argFns;
|
|
827
|
+
return (row) => Boolean(condFn!(row)) ? thenFn!(row) : (elseFn ? elseFn(row) : null);
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
case "isnull": {
|
|
831
|
+
const [fn] = argFns;
|
|
832
|
+
return (row) => {
|
|
833
|
+
const val = fn!(row);
|
|
834
|
+
return val === null || val === undefined || val === "";
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
case "isnotnull": {
|
|
839
|
+
const [fn] = argFns;
|
|
840
|
+
return (row) => {
|
|
841
|
+
const val = fn!(row);
|
|
842
|
+
return val !== null && val !== undefined && val !== "";
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
case "coalesce":
|
|
847
|
+
return (row) => {
|
|
848
|
+
for (const f of argFns) {
|
|
849
|
+
const val = f(row);
|
|
850
|
+
if (val !== null && val !== undefined && val !== "") return val;
|
|
851
|
+
}
|
|
852
|
+
return null;
|
|
853
|
+
};
|
|
854
|
+
|
|
855
|
+
case "nullif": {
|
|
856
|
+
const [aFn, bFn] = argFns;
|
|
857
|
+
return (row) => {
|
|
858
|
+
const a = aFn!(row);
|
|
859
|
+
const b = bFn!(row);
|
|
860
|
+
return a === b ? null : a;
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
case "nvl": {
|
|
865
|
+
const [aFn, bFn] = argFns;
|
|
866
|
+
return (row) => {
|
|
867
|
+
const a = aFn!(row);
|
|
868
|
+
return a === null || a === undefined || a === "" ? bFn!(row) : a;
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// --- 6. Type Inspection & Conversion ---
|
|
873
|
+
case "isnumber": {
|
|
874
|
+
const [fn] = argFns;
|
|
875
|
+
return (row) => {
|
|
876
|
+
const val = fn!(row);
|
|
877
|
+
if (val === null || val === undefined || val === "") return false;
|
|
878
|
+
return !Number.isNaN(Number(val));
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
case "isemail": {
|
|
883
|
+
const [fn] = argFns;
|
|
884
|
+
return (row) => detectSemanticType(fn!(row)) === "email";
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
case "isurl": {
|
|
888
|
+
const [fn] = argFns;
|
|
889
|
+
return (row) => detectSemanticType(fn!(row)) === "url";
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
case "isuuid": {
|
|
893
|
+
const [fn] = argFns;
|
|
894
|
+
return (row) => detectSemanticType(fn!(row)) === "uuid";
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
case "isdate": {
|
|
898
|
+
const [fn] = argFns;
|
|
899
|
+
return (row) => {
|
|
900
|
+
const d = new Date(String(fn!(row) ?? ""));
|
|
901
|
+
return !Number.isNaN(d.getTime());
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
case "toint": {
|
|
906
|
+
const [fn] = argFns;
|
|
907
|
+
return (row) => {
|
|
908
|
+
const num = Number.parseInt(String(fn!(row) ?? ""), 10);
|
|
909
|
+
return Number.isNaN(num) ? null : num;
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
case "tofloat": {
|
|
914
|
+
const [fn] = argFns;
|
|
915
|
+
return (row) => {
|
|
916
|
+
const num = Number.parseFloat(String(fn!(row) ?? ""));
|
|
917
|
+
return Number.isNaN(num) ? null : num;
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
case "tostring": {
|
|
922
|
+
const [fn] = argFns;
|
|
923
|
+
return (row) => {
|
|
924
|
+
const val = fn!(row);
|
|
925
|
+
return val !== null && val !== undefined ? String(val) : "";
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
case "tobool": {
|
|
930
|
+
const [fn] = argFns;
|
|
931
|
+
return (row) => {
|
|
932
|
+
const s = String(fn!(row) ?? "").toLowerCase();
|
|
933
|
+
return ["true", "1", "yes", "t", "y"].includes(s);
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
// --- 7. JSON & Nested Access ---
|
|
938
|
+
case "jsonget": {
|
|
939
|
+
const [objFn, pathFn] = argFns;
|
|
940
|
+
return (row) => getNestedValue(objFn!(row), String(pathFn!(row) ?? ""));
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
default:
|
|
944
|
+
throw new InvalidArgumentError(`Unknown function "${callee}" in filter expression`);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
/**
|
|
949
|
+
* Compiles an AST node into a high-performance closure with zero runtime AST traversals.
|
|
950
|
+
*/
|
|
951
|
+
function compileASTNode(node: ASTNode): CompiledNodeFn {
|
|
952
|
+
switch (node.type) {
|
|
953
|
+
case "Literal": {
|
|
954
|
+
const val = node.value;
|
|
955
|
+
return () => val;
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
case "Identifier": {
|
|
959
|
+
const name = node.name;
|
|
960
|
+
const parts = name.includes(".") ? name.split(".") : null;
|
|
961
|
+
return (row: Row) => resolveIdentifier(name, parts, row);
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
case "UnaryOp": {
|
|
965
|
+
const argFn = compileASTNode(node.argument);
|
|
966
|
+
if (node.operator === "!") {
|
|
967
|
+
return (row: Row) => !argFn(row);
|
|
968
|
+
}
|
|
969
|
+
if (node.operator === "-") {
|
|
970
|
+
return (row: Row) => -Number(argFn(row));
|
|
971
|
+
}
|
|
972
|
+
return () => false;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
case "BinaryOp": {
|
|
976
|
+
const leftFn = compileASTNode(node.left);
|
|
977
|
+
const rightFn = compileASTNode(node.right);
|
|
978
|
+
|
|
979
|
+
switch (node.operator) {
|
|
980
|
+
case "&&":
|
|
981
|
+
return (row) => Boolean(leftFn(row)) && Boolean(rightFn(row));
|
|
982
|
+
|
|
983
|
+
case "||":
|
|
984
|
+
return (row) => Boolean(leftFn(row)) || Boolean(rightFn(row));
|
|
985
|
+
|
|
986
|
+
case "==":
|
|
987
|
+
return (row) => {
|
|
988
|
+
const leftVal = leftFn(row);
|
|
989
|
+
const rightVal = rightFn(row);
|
|
990
|
+
if (leftVal === rightVal) return true;
|
|
991
|
+
if (leftVal === null || rightVal === null) return leftVal === rightVal;
|
|
992
|
+
if (typeof leftVal === "number" || typeof rightVal === "number") {
|
|
993
|
+
return Number(leftVal) === Number(rightVal);
|
|
994
|
+
}
|
|
995
|
+
return String(leftVal) === String(rightVal);
|
|
996
|
+
};
|
|
997
|
+
|
|
998
|
+
case "!=":
|
|
999
|
+
return (row) => {
|
|
1000
|
+
const leftVal = leftFn(row);
|
|
1001
|
+
const rightVal = rightFn(row);
|
|
1002
|
+
if (leftVal === rightVal) return false;
|
|
1003
|
+
if (leftVal === null || rightVal === null) return leftVal !== rightVal;
|
|
1004
|
+
if (typeof leftVal === "number" || typeof rightVal === "number") {
|
|
1005
|
+
return Number(leftVal) !== Number(rightVal);
|
|
1006
|
+
}
|
|
1007
|
+
return String(leftVal) !== String(rightVal);
|
|
1008
|
+
};
|
|
1009
|
+
|
|
1010
|
+
case ">":
|
|
1011
|
+
return (row) => {
|
|
1012
|
+
const leftVal = leftFn(row);
|
|
1013
|
+
if (leftVal === null) return false;
|
|
1014
|
+
const rightVal = rightFn(row);
|
|
1015
|
+
if (rightVal === null) return false;
|
|
1016
|
+
return Number(leftVal) > Number(rightVal);
|
|
1017
|
+
};
|
|
1018
|
+
|
|
1019
|
+
case ">=":
|
|
1020
|
+
return (row) => {
|
|
1021
|
+
const leftVal = leftFn(row);
|
|
1022
|
+
if (leftVal === null) return false;
|
|
1023
|
+
const rightVal = rightFn(row);
|
|
1024
|
+
if (rightVal === null) return false;
|
|
1025
|
+
return Number(leftVal) >= Number(rightVal);
|
|
1026
|
+
};
|
|
1027
|
+
|
|
1028
|
+
case "<":
|
|
1029
|
+
return (row) => {
|
|
1030
|
+
const leftVal = leftFn(row);
|
|
1031
|
+
if (leftVal === null) return false;
|
|
1032
|
+
const rightVal = rightFn(row);
|
|
1033
|
+
if (rightVal === null) return false;
|
|
1034
|
+
return Number(leftVal) < Number(rightVal);
|
|
1035
|
+
};
|
|
1036
|
+
|
|
1037
|
+
case "<=":
|
|
1038
|
+
return (row) => {
|
|
1039
|
+
const leftVal = leftFn(row);
|
|
1040
|
+
if (leftVal === null) return false;
|
|
1041
|
+
const rightVal = rightFn(row);
|
|
1042
|
+
if (rightVal === null) return false;
|
|
1043
|
+
return Number(leftVal) <= Number(rightVal);
|
|
1044
|
+
};
|
|
1045
|
+
|
|
1046
|
+
case "+":
|
|
1047
|
+
return (row) => {
|
|
1048
|
+
const leftVal = leftFn(row);
|
|
1049
|
+
const rightVal = rightFn(row);
|
|
1050
|
+
if (typeof leftVal === "string" || typeof rightVal === "string") {
|
|
1051
|
+
return String(leftVal ?? "") + String(rightVal ?? "");
|
|
1052
|
+
}
|
|
1053
|
+
return Number(leftVal ?? 0) + Number(rightVal ?? 0);
|
|
1054
|
+
};
|
|
1055
|
+
|
|
1056
|
+
case "-":
|
|
1057
|
+
return (row) => Number(leftFn(row) ?? 0) - Number(rightFn(row) ?? 0);
|
|
1058
|
+
|
|
1059
|
+
case "*":
|
|
1060
|
+
return (row) => Number(leftFn(row) ?? 0) * Number(rightFn(row) ?? 0);
|
|
1061
|
+
|
|
1062
|
+
case "/":
|
|
1063
|
+
return (row) => Number(leftFn(row) ?? 0) / Number(rightFn(row) ?? 0);
|
|
1064
|
+
|
|
1065
|
+
case "%":
|
|
1066
|
+
return (row) => Number(leftFn(row) ?? 0) % Number(rightFn(row) ?? 0);
|
|
1067
|
+
|
|
1068
|
+
default:
|
|
1069
|
+
return () => false;
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
case "CallExpr": {
|
|
1074
|
+
const argFns = node.args.map(compileASTNode);
|
|
1075
|
+
return compileCallFunction(node.callee, argFns);
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
/**
|
|
1081
|
+
* Compiles a filter expression string into an optimized predicate function.
|
|
1082
|
+
*/
|
|
1083
|
+
export function compileExpression(expressionStr: string): (row: Row) => boolean {
|
|
1084
|
+
if (!expressionStr || expressionStr.trim().length === 0) {
|
|
1085
|
+
return () => true;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
const lexer = new Lexer(expressionStr);
|
|
1089
|
+
const tokens = lexer.tokenize();
|
|
1090
|
+
const parser = new Parser(tokens);
|
|
1091
|
+
const ast = parser.parse();
|
|
1092
|
+
const compiled = compileASTNode(ast);
|
|
1093
|
+
|
|
1094
|
+
return function (row: Row): boolean {
|
|
1095
|
+
return Boolean(compiled(row));
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
/**
|
|
1100
|
+
* Compiles an expression string into an optimized value evaluator returning computed result (number, string, etc.).
|
|
1101
|
+
*/
|
|
1102
|
+
export function compileValueExpression(expressionStr: string): (row: Row) => unknown {
|
|
1103
|
+
if (!expressionStr || expressionStr.trim().length === 0) {
|
|
1104
|
+
return () => null;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
const lexer = new Lexer(expressionStr);
|
|
1108
|
+
const tokens = lexer.tokenize();
|
|
1109
|
+
const parser = new Parser(tokens);
|
|
1110
|
+
const ast = parser.parse();
|
|
1111
|
+
|
|
1112
|
+
return compileASTNode(ast);
|
|
1113
|
+
}
|
|
1114
|
+
|