@pythia-software/query-table-core 0.2.0 → 0.3.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 +8 -0
- package/dist/index.d.ts +194 -11
- package/dist/index.js +1483 -65
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,4 +1,152 @@
|
|
|
1
|
+
// src/computed.ts
|
|
2
|
+
var COMPUTED_PREFIX = "@computed/";
|
|
3
|
+
var computedFieldName = (id) => COMPUTED_PREFIX + id;
|
|
4
|
+
var isComputedField = (name) => name.trim().toLowerCase().startsWith(COMPUTED_PREFIX);
|
|
5
|
+
function validateComputedColumn(input) {
|
|
6
|
+
const c = input;
|
|
7
|
+
if (!c || typeof c.id !== "string" || !/^[a-zA-Z0-9_-]{1,128}$/.test(c.id) || typeof c.label !== "string" || !c.label.trim() || c.label.length > 200 || typeof c.revision !== "string" || !c.revision || c.revision.length > 256 || c.expression?.language !== "qt-expr" || c.expression.version !== 1 || typeof c.expression.source !== "string" || !c.expression.source.trim() || c.expression.source.length > 1e4) {
|
|
8
|
+
throw new Error(
|
|
9
|
+
"Invalid computed column definition or unsupported formula version."
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
return {
|
|
13
|
+
id: c.id,
|
|
14
|
+
label: c.label,
|
|
15
|
+
revision: c.revision,
|
|
16
|
+
expression: { ...c.expression }
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
function memoryComputedColumnStore(seed = {}) {
|
|
20
|
+
const data = new Map(
|
|
21
|
+
Object.entries(seed).map(([k, v]) => [
|
|
22
|
+
k,
|
|
23
|
+
new Map(v.map((c) => [c.id, validateComputedColumn(c)]))
|
|
24
|
+
])
|
|
25
|
+
);
|
|
26
|
+
const seedRevisions = new Set(
|
|
27
|
+
[...data.values()].flatMap(
|
|
28
|
+
(entries) => [...entries.values()].map((c) => c.revision)
|
|
29
|
+
)
|
|
30
|
+
);
|
|
31
|
+
let revisionSequence = 0n;
|
|
32
|
+
function freshRevision() {
|
|
33
|
+
let revision;
|
|
34
|
+
do {
|
|
35
|
+
revision = `memory:${++revisionSequence}`;
|
|
36
|
+
} while (seedRevisions.has(revision));
|
|
37
|
+
return revision;
|
|
38
|
+
}
|
|
39
|
+
const listeners = /* @__PURE__ */ new Map();
|
|
40
|
+
return {
|
|
41
|
+
async list(dataset) {
|
|
42
|
+
return [...data.get(dataset)?.values() ?? []].map(
|
|
43
|
+
validateComputedColumn
|
|
44
|
+
);
|
|
45
|
+
},
|
|
46
|
+
async save(dataset, column, expectedRevision) {
|
|
47
|
+
const entries = data.get(dataset) ?? /* @__PURE__ */ new Map();
|
|
48
|
+
const current = entries.get(column.id);
|
|
49
|
+
if ((current?.revision ?? null) !== expectedRevision)
|
|
50
|
+
throw new Error(
|
|
51
|
+
"This definition changed. Reload it before saving again."
|
|
52
|
+
);
|
|
53
|
+
const next = validateComputedColumn({
|
|
54
|
+
...column,
|
|
55
|
+
revision: freshRevision()
|
|
56
|
+
});
|
|
57
|
+
entries.set(next.id, next);
|
|
58
|
+
data.set(dataset, entries);
|
|
59
|
+
listeners.get(dataset)?.forEach((fn) => fn());
|
|
60
|
+
return validateComputedColumn(next);
|
|
61
|
+
},
|
|
62
|
+
subscribe(dataset, listener) {
|
|
63
|
+
const set = listeners.get(dataset) ?? /* @__PURE__ */ new Set();
|
|
64
|
+
set.add(listener);
|
|
65
|
+
listeners.set(dataset, set);
|
|
66
|
+
return () => {
|
|
67
|
+
set.delete(listener);
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function httpComputedColumnStore(base, fetcher = fetch) {
|
|
73
|
+
const listeners = /* @__PURE__ */ new Map();
|
|
74
|
+
const url = (dataset) => `${base}${base.includes("?") ? "&" : "?"}dataset=${encodeURIComponent(dataset)}`;
|
|
75
|
+
async function read(response) {
|
|
76
|
+
if (response.status === 409)
|
|
77
|
+
throw new Error(
|
|
78
|
+
"This definition changed. Reload it before saving again."
|
|
79
|
+
);
|
|
80
|
+
if (!response.ok)
|
|
81
|
+
throw new Error(
|
|
82
|
+
`Computed column store request failed (${response.status}).`
|
|
83
|
+
);
|
|
84
|
+
return response.json();
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
async list(dataset, signal) {
|
|
88
|
+
const raw = await read(
|
|
89
|
+
await fetcher(url(dataset), { ...signal ? { signal } : {} })
|
|
90
|
+
);
|
|
91
|
+
if (!Array.isArray(raw))
|
|
92
|
+
throw new Error("Invalid computed column catalogue.");
|
|
93
|
+
return raw.map(validateComputedColumn);
|
|
94
|
+
},
|
|
95
|
+
async save(dataset, column, expectedRevision) {
|
|
96
|
+
const result = validateComputedColumn(
|
|
97
|
+
await read(
|
|
98
|
+
await fetcher(url(dataset), {
|
|
99
|
+
method: "PUT",
|
|
100
|
+
headers: { "Content-Type": "application/json" },
|
|
101
|
+
body: JSON.stringify({ column, expectedRevision })
|
|
102
|
+
})
|
|
103
|
+
)
|
|
104
|
+
);
|
|
105
|
+
listeners.get(dataset)?.forEach((fn) => fn());
|
|
106
|
+
return result;
|
|
107
|
+
},
|
|
108
|
+
subscribe(dataset, listener) {
|
|
109
|
+
const set = listeners.get(dataset) ?? /* @__PURE__ */ new Set();
|
|
110
|
+
set.add(listener);
|
|
111
|
+
listeners.set(dataset, set);
|
|
112
|
+
return () => {
|
|
113
|
+
set.delete(listener);
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function isComputedCellError(value) {
|
|
119
|
+
return value !== null && typeof value === "object" && "computedError" in value && typeof value.computedError === "string";
|
|
120
|
+
}
|
|
121
|
+
function groupPreview(dependencies, inputs, results, total) {
|
|
122
|
+
const groups = /* @__PURE__ */ new Map();
|
|
123
|
+
let nulls = 0, errors = 0;
|
|
124
|
+
inputs.forEach((row, i) => {
|
|
125
|
+
const values = dependencies.map((name) => row[name] ?? null), result = results[i] ?? { value: null, error: "No result." };
|
|
126
|
+
const key = JSON.stringify([values, result.value, result.error ?? null]);
|
|
127
|
+
const group = groups.get(key);
|
|
128
|
+
if (group) group.count++;
|
|
129
|
+
else groups.set(key, { inputs: values, result, count: 1 });
|
|
130
|
+
if (result.error) errors++;
|
|
131
|
+
else if (result.value === null) nulls++;
|
|
132
|
+
});
|
|
133
|
+
return {
|
|
134
|
+
dependencies,
|
|
135
|
+
groups: [...groups.values()].sort((a, b) => b.count - a.count),
|
|
136
|
+
processed: inputs.length,
|
|
137
|
+
total,
|
|
138
|
+
nulls,
|
|
139
|
+
errors
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
1
143
|
// src/query.ts
|
|
144
|
+
function isOrGroup(term) {
|
|
145
|
+
return Array.isArray(term.any);
|
|
146
|
+
}
|
|
147
|
+
function predicatesOf(term) {
|
|
148
|
+
return isOrGroup(term) ? term.any : [term];
|
|
149
|
+
}
|
|
2
150
|
var EMPTY_QUERY = {
|
|
3
151
|
select: [],
|
|
4
152
|
where: [],
|
|
@@ -29,6 +177,8 @@ var FILTER_OPS = /* @__PURE__ */ new Set([
|
|
|
29
177
|
"contains",
|
|
30
178
|
"starts_with",
|
|
31
179
|
"ends_with",
|
|
180
|
+
"matches_regex",
|
|
181
|
+
"not_matches_regex",
|
|
32
182
|
"includes",
|
|
33
183
|
"is_null",
|
|
34
184
|
"is_not_null"
|
|
@@ -44,6 +194,20 @@ function boundedInteger(value, fallback, min, max) {
|
|
|
44
194
|
if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
|
|
45
195
|
return Math.max(min, Math.min(max, Math.round(value)));
|
|
46
196
|
}
|
|
197
|
+
function normalizeWhereClause(item) {
|
|
198
|
+
if (!isRecord(item)) return null;
|
|
199
|
+
const field = boundedString(item.field, MAX_FIELD_NAME_LENGTH);
|
|
200
|
+
const value = typeof item.value === "string" && item.value.length <= MAX_FILTER_VALUE_LENGTH ? item.value : null;
|
|
201
|
+
if (!field || isComputedField(field) || typeof item.op !== "string" || !FILTER_OPS.has(item.op) || value == null) {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
const clause = { field, op: item.op, value };
|
|
205
|
+
if (item.negated === true) clause.negated = true;
|
|
206
|
+
return clause;
|
|
207
|
+
}
|
|
208
|
+
function cloneWhereTerm(term) {
|
|
209
|
+
return isOrGroup(term) ? { any: term.any.map((c) => ({ ...c })) } : { ...term };
|
|
210
|
+
}
|
|
47
211
|
function normalizeQueryState(input, fallback = EMPTY_QUERY) {
|
|
48
212
|
const raw = isRecord(input) ? input : {};
|
|
49
213
|
const select = [];
|
|
@@ -63,28 +227,49 @@ function normalizeQueryState(input, fallback = EMPTY_QUERY) {
|
|
|
63
227
|
}
|
|
64
228
|
const where = [];
|
|
65
229
|
if (Array.isArray(raw.where)) {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
230
|
+
let literalBudget = MAX_WHERE_CLAUSES;
|
|
231
|
+
for (const item of raw.where) {
|
|
232
|
+
if (literalBudget <= 0) break;
|
|
233
|
+
if (isRecord(item) && Array.isArray(item.any)) {
|
|
234
|
+
const members = [];
|
|
235
|
+
for (const inner of item.any) {
|
|
236
|
+
if (literalBudget <= 0) break;
|
|
237
|
+
const clause = normalizeWhereClause(inner);
|
|
238
|
+
if (clause) {
|
|
239
|
+
members.push(clause);
|
|
240
|
+
literalBudget--;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (members.length === 1) where.push(members[0]);
|
|
244
|
+
else if (members.length > 1) where.push({ any: members });
|
|
245
|
+
} else {
|
|
246
|
+
const clause = normalizeWhereClause(item);
|
|
247
|
+
if (clause) {
|
|
248
|
+
where.push(clause);
|
|
249
|
+
literalBudget--;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
72
252
|
}
|
|
73
253
|
} else {
|
|
74
|
-
where.push(...fallback.where.map(
|
|
254
|
+
where.push(...fallback.where.map(cloneWhereTerm));
|
|
75
255
|
}
|
|
76
256
|
const orderBy = [];
|
|
77
257
|
if (Array.isArray(raw.orderBy)) {
|
|
78
258
|
for (const item of raw.orderBy.slice(0, MAX_ORDER_BY_TERMS)) {
|
|
79
259
|
if (!isRecord(item)) continue;
|
|
80
260
|
const field = boundedString(item.field, MAX_FIELD_NAME_LENGTH);
|
|
81
|
-
if (!field || item.dir !== "asc" && item.dir !== "desc") continue;
|
|
261
|
+
if (!field || isComputedField(field) || item.dir !== "asc" && item.dir !== "desc") continue;
|
|
82
262
|
const term = { field, dir: item.dir };
|
|
83
263
|
if (item.nulls === "first" || item.nulls === "last") term.nulls = item.nulls;
|
|
264
|
+
if (isRecord(item.extract) && typeof item.extract.regex === "string" && item.extract.regex.length <= MAX_FILTER_VALUE_LENGTH) {
|
|
265
|
+
term.extract = { regex: item.extract.regex };
|
|
266
|
+
}
|
|
84
267
|
orderBy.push(term);
|
|
85
268
|
}
|
|
86
269
|
} else {
|
|
87
|
-
orderBy.push(
|
|
270
|
+
orderBy.push(
|
|
271
|
+
...fallback.orderBy.map((term) => term.extract ? { ...term, extract: { ...term.extract } } : { ...term })
|
|
272
|
+
);
|
|
88
273
|
}
|
|
89
274
|
const out = {
|
|
90
275
|
select,
|
|
@@ -103,6 +288,7 @@ function normalizeQueryState(input, fallback = EMPTY_QUERY) {
|
|
|
103
288
|
const aggregation = { id, op: item.op, groupBy };
|
|
104
289
|
const field = boundedString(item.field, MAX_FIELD_NAME_LENGTH);
|
|
105
290
|
const label = boundedString(item.label, MAX_LABEL_LENGTH);
|
|
291
|
+
if (field && isComputedField(field) || groupBy.some(isComputedField)) continue;
|
|
106
292
|
if (field) aggregation.field = field;
|
|
107
293
|
if (label) aggregation.label = label;
|
|
108
294
|
aggregations.push(aggregation);
|
|
@@ -131,37 +317,39 @@ function sameArray(a, b) {
|
|
|
131
317
|
}
|
|
132
318
|
|
|
133
319
|
// src/schema.ts
|
|
134
|
-
function isFilterable(
|
|
135
|
-
|
|
320
|
+
function isFilterable(f2) {
|
|
321
|
+
if (f2.source.kind === "derived" && f2.source.computedId) return false;
|
|
322
|
+
return f2.filter?.enabled ?? f2.source.kind === "backend";
|
|
136
323
|
}
|
|
137
|
-
function isPushdownFilter(
|
|
138
|
-
return isFilterable(
|
|
324
|
+
function isPushdownFilter(f2) {
|
|
325
|
+
return isFilterable(f2) && (f2.filter?.pushdown ?? f2.source.kind === "backend");
|
|
139
326
|
}
|
|
140
|
-
function isSortable(
|
|
141
|
-
|
|
327
|
+
function isSortable(f2) {
|
|
328
|
+
if (f2.source.kind === "derived" && f2.source.computedId) return false;
|
|
329
|
+
return f2.sort?.enabled ?? f2.source.kind === "backend";
|
|
142
330
|
}
|
|
143
|
-
function isSelectable(
|
|
144
|
-
return
|
|
331
|
+
function isSelectable(f2) {
|
|
332
|
+
return f2.select?.enabled ?? true;
|
|
145
333
|
}
|
|
146
|
-
function filterValues(
|
|
147
|
-
return
|
|
334
|
+
function filterValues(f2) {
|
|
335
|
+
return f2.filter?.values ?? { source: "autocomplete" };
|
|
148
336
|
}
|
|
149
337
|
function indexFields(schema) {
|
|
150
|
-
return new Map(schema.fields.map((
|
|
338
|
+
return new Map(schema.fields.map((f2) => [f2.name, f2]));
|
|
151
339
|
}
|
|
152
340
|
function selectedFields(schema, q) {
|
|
153
|
-
const order = q.select.length > 0 ? q.select.map((c) => c.field) : (schema.defaultSelect ?? schema.fields.filter((
|
|
341
|
+
const order = q.select.length > 0 ? q.select.map((c) => c.field) : (schema.defaultSelect ?? schema.fields.filter((f2) => f2.select?.default).map((f2) => ({ field: f2.name }))).map(
|
|
154
342
|
(c) => c.field
|
|
155
343
|
);
|
|
156
344
|
const seen = /* @__PURE__ */ new Set();
|
|
157
345
|
const out = [];
|
|
158
346
|
for (const name of order) {
|
|
159
347
|
if (seen.has(name)) continue;
|
|
160
|
-
const
|
|
161
|
-
if (!
|
|
348
|
+
const f2 = resolveField(schema, name);
|
|
349
|
+
if (!f2 || !isSelectable(f2)) continue;
|
|
162
350
|
seen.add(name);
|
|
163
|
-
seen.add(
|
|
164
|
-
out.push(
|
|
351
|
+
seen.add(f2.name);
|
|
352
|
+
out.push(f2);
|
|
165
353
|
}
|
|
166
354
|
return out;
|
|
167
355
|
}
|
|
@@ -172,9 +360,9 @@ function resolveField(schema, field) {
|
|
|
172
360
|
const byExactName = byName.get(token);
|
|
173
361
|
if (byExactName) return byExactName;
|
|
174
362
|
const needle = token.toLowerCase();
|
|
175
|
-
for (const
|
|
176
|
-
if (
|
|
177
|
-
if (aliasMatches(
|
|
363
|
+
for (const f2 of schema.fields) {
|
|
364
|
+
if (f2.name.toLowerCase() === needle) return f2;
|
|
365
|
+
if (aliasMatches(f2, needle)) return f2;
|
|
178
366
|
}
|
|
179
367
|
return void 0;
|
|
180
368
|
}
|
|
@@ -286,8 +474,57 @@ function normalizeAliases(rawAliases, rawAlias) {
|
|
|
286
474
|
|
|
287
475
|
// src/ops.ts
|
|
288
476
|
var NULLITY = ["is_null", "is_not_null"];
|
|
477
|
+
var COMPLEMENT_OP = {
|
|
478
|
+
"=": "!=",
|
|
479
|
+
"!=": "=",
|
|
480
|
+
">": "<=",
|
|
481
|
+
"<=": ">",
|
|
482
|
+
">=": "<",
|
|
483
|
+
"<": ">=",
|
|
484
|
+
is_null: "is_not_null",
|
|
485
|
+
is_not_null: "is_null",
|
|
486
|
+
matches_regex: "not_matches_regex",
|
|
487
|
+
not_matches_regex: "matches_regex"
|
|
488
|
+
};
|
|
489
|
+
function negateClause(clause, allowedOps) {
|
|
490
|
+
const complement = COMPLEMENT_OP[clause.op];
|
|
491
|
+
if (complement && (!allowedOps || allowedOps.includes(complement))) {
|
|
492
|
+
const next2 = { field: clause.field, op: complement, value: clause.value };
|
|
493
|
+
return next2;
|
|
494
|
+
}
|
|
495
|
+
const next = { field: clause.field, op: clause.op, value: clause.value };
|
|
496
|
+
if (!clause.negated) next.negated = true;
|
|
497
|
+
return next;
|
|
498
|
+
}
|
|
499
|
+
function isNegativePredicate(clause) {
|
|
500
|
+
return Boolean(clause.negated) || clause.op === "!=" || clause.op === "not_matches_regex" || clause.op === "is_not_null";
|
|
501
|
+
}
|
|
502
|
+
var POSITIVE_OP_ORDER = ["=", ">", ">=", "contains", "starts_with", "ends_with", "matches_regex", "includes"];
|
|
503
|
+
function opPairsForField(field) {
|
|
504
|
+
const ops = opsForField(field);
|
|
505
|
+
const allow = new Set(ops);
|
|
506
|
+
const pairs = [];
|
|
507
|
+
for (const op of POSITIVE_OP_ORDER) {
|
|
508
|
+
if (!allow.has(op)) continue;
|
|
509
|
+
const neg = negateClause({ field: "", op, value: "" }, ops);
|
|
510
|
+
pairs.push({ keep: { op, negated: false }, exclude: { op: neg.op, negated: Boolean(neg.negated) } });
|
|
511
|
+
}
|
|
512
|
+
if (allow.has("is_null") || allow.has("is_not_null")) {
|
|
513
|
+
pairs.push({ keep: { op: "is_null", negated: false }, exclude: { op: "is_not_null", negated: false } });
|
|
514
|
+
}
|
|
515
|
+
return pairs;
|
|
516
|
+
}
|
|
289
517
|
var OPS_BY_TYPE = {
|
|
290
|
-
text: [
|
|
518
|
+
text: [
|
|
519
|
+
"=",
|
|
520
|
+
"!=",
|
|
521
|
+
"contains",
|
|
522
|
+
"starts_with",
|
|
523
|
+
"ends_with",
|
|
524
|
+
"matches_regex",
|
|
525
|
+
"not_matches_regex",
|
|
526
|
+
...NULLITY
|
|
527
|
+
],
|
|
291
528
|
enum: ["=", "!=", ...NULLITY],
|
|
292
529
|
number: ["=", "!=", ">", ">=", "<", "<=", ...NULLITY],
|
|
293
530
|
datetime: ["=", "!=", ">", ">=", "<", "<=", ...NULLITY],
|
|
@@ -430,42 +667,50 @@ function toServerQuery(q, schema) {
|
|
|
430
667
|
q = normalizeQueryState(q);
|
|
431
668
|
const byName = indexFields(schema);
|
|
432
669
|
const resolveField2 = (name) => resolveFieldName(schema, name) ?? name;
|
|
433
|
-
const where = q.where.filter((
|
|
434
|
-
const field = resolveField2(cl.field);
|
|
435
|
-
const f = byName.get(field);
|
|
436
|
-
return f != null && isPushdownFilter(f);
|
|
437
|
-
});
|
|
670
|
+
const where = q.where.filter((term) => termIsPushdown(term, byName, resolveField2));
|
|
438
671
|
const orderBy = [];
|
|
439
672
|
for (const term of q.orderBy) {
|
|
440
673
|
const field = resolveField2(term.field);
|
|
441
|
-
const
|
|
442
|
-
if (!
|
|
443
|
-
orderBy.push({ ...term, field:
|
|
674
|
+
const f2 = byName.get(field);
|
|
675
|
+
if (!f2 || !isSortable(f2) || f2.source.kind !== "backend") continue;
|
|
676
|
+
orderBy.push({ ...term, field: f2.sort?.field ?? f2.name });
|
|
444
677
|
}
|
|
445
678
|
const select = new Set(
|
|
446
|
-
selectedFields(schema, q).filter((
|
|
679
|
+
selectedFields(schema, q).filter((f2) => f2.source.kind === "backend").map((f2) => f2.name)
|
|
447
680
|
);
|
|
681
|
+
for (const field of selectedFields(schema, q)) {
|
|
682
|
+
if (field.source.kind === "derived") {
|
|
683
|
+
for (const name of field.source.dependencies ?? []) {
|
|
684
|
+
if (byName.get(name)?.source.kind === "backend") select.add(name);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}
|
|
448
688
|
select.add(schema.idField);
|
|
449
|
-
for (const
|
|
450
|
-
const
|
|
451
|
-
|
|
452
|
-
|
|
689
|
+
for (const term of q.where) {
|
|
690
|
+
for (const cl of predicatesOf(term)) {
|
|
691
|
+
const field = resolveField2(cl.field);
|
|
692
|
+
const f2 = byName.get(field);
|
|
693
|
+
if (f2 && isFilterable(f2) && !isPushdownFilter(f2) && f2.source.kind === "backend") select.add(field);
|
|
694
|
+
}
|
|
453
695
|
}
|
|
454
|
-
return { select: [...select], where, orderBy, limit: q.limit, offset: q.offset };
|
|
696
|
+
return { select: [...select].sort(), where, orderBy, limit: q.limit, offset: q.offset };
|
|
697
|
+
}
|
|
698
|
+
function termIsPushdown(term, byName, resolveField2) {
|
|
699
|
+
return predicatesOf(term).every((cl) => {
|
|
700
|
+
const f2 = byName.get(resolveField2(cl.field));
|
|
701
|
+
return f2 != null && isPushdownFilter(f2);
|
|
702
|
+
});
|
|
455
703
|
}
|
|
456
704
|
function toAggregationQuery(q, schema) {
|
|
457
705
|
q = normalizeQueryState(q);
|
|
458
706
|
const byName = indexFields(schema);
|
|
459
707
|
const resolveField2 = (name) => resolveFieldName(schema, name) ?? name;
|
|
460
|
-
const where = q.where.filter((
|
|
461
|
-
const f = byName.get(resolveField2(cl.field));
|
|
462
|
-
return f != null && isPushdownFilter(f);
|
|
463
|
-
});
|
|
708
|
+
const where = q.where.filter((term) => termIsPushdown(term, byName, resolveField2));
|
|
464
709
|
const isBackend = (name) => {
|
|
465
710
|
if (name == null) return true;
|
|
466
711
|
const field = resolveField2(name);
|
|
467
|
-
const
|
|
468
|
-
return
|
|
712
|
+
const f2 = byName.get(field);
|
|
713
|
+
return f2 != null && f2.source.kind === "backend";
|
|
469
714
|
};
|
|
470
715
|
const aggregations = (q.aggregations ?? []).filter(
|
|
471
716
|
(a) => isBackend(a.field) && a.groupBy.every((g) => isBackend(g))
|
|
@@ -477,16 +722,23 @@ function toAggregationQuery(q, schema) {
|
|
|
477
722
|
function applyQuery(rows, q, schema) {
|
|
478
723
|
const byName = indexFields(schema);
|
|
479
724
|
const resolveField2 = (name) => resolveFieldName(schema, name) ?? name;
|
|
480
|
-
const
|
|
481
|
-
const orderBy = q.orderBy.map((term) => ({ ...term, field: resolveField2(term.field) }));
|
|
482
|
-
let out = rows.filter((row) =>
|
|
725
|
+
const whereTerms = q.where.filter((term) => !predicatesOf(term).some((c) => isComputedField(c.field))).map((term) => prepareWhereTerm(term, resolveField2));
|
|
726
|
+
const orderBy = q.orderBy.filter((term) => !isComputedField(term.field)).map((term) => ({ ...term, field: resolveField2(term.field) }));
|
|
727
|
+
let out = rows.filter((row) => whereTerms.every((term) => matchesTerm(byName, row, term)));
|
|
483
728
|
const total = out.length;
|
|
484
729
|
if (orderBy.length) {
|
|
485
|
-
const terms = orderBy.map((t) => ({
|
|
730
|
+
const terms = orderBy.map((t) => ({
|
|
731
|
+
field: byName.get(t.field),
|
|
732
|
+
dir: t.dir,
|
|
733
|
+
nullsLast: (t.nulls ?? "last") === "last",
|
|
734
|
+
extract: compileRegex(t.extract?.regex)
|
|
735
|
+
})).filter(
|
|
736
|
+
(t) => t.field != null
|
|
737
|
+
);
|
|
486
738
|
out = [...out].sort((a, b) => {
|
|
487
739
|
for (const t of terms) {
|
|
488
|
-
const av = readFieldValue(t.field, a);
|
|
489
|
-
const bv = readFieldValue(t.field, b);
|
|
740
|
+
const av = extractSortValue(readFieldValue(t.field, a), t.extract);
|
|
741
|
+
const bv = extractSortValue(readFieldValue(t.field, b), t.extract);
|
|
490
742
|
const aNull = av == null;
|
|
491
743
|
const bNull = bv == null;
|
|
492
744
|
if (aNull || bNull) {
|
|
@@ -504,14 +756,14 @@ function applyQuery(rows, q, schema) {
|
|
|
504
756
|
return { rows: out.slice(start, end), total };
|
|
505
757
|
}
|
|
506
758
|
function matchesClause(row, clause, schema) {
|
|
507
|
-
return matchesWith(indexFields(schema), row, clause);
|
|
759
|
+
return matchesWith(indexFields(schema), row, prepareWhereClause(clause));
|
|
508
760
|
}
|
|
509
761
|
function applyAggregations(rows, q, schema) {
|
|
510
762
|
const byName = indexFields(schema);
|
|
511
763
|
const resolveField2 = (name) => resolveFieldName(schema, name) ?? name;
|
|
512
|
-
const
|
|
513
|
-
const filtered = rows.filter((row) =>
|
|
514
|
-
const metrics = (q.aggregations ?? []).map((agg) => ({
|
|
764
|
+
const whereTerms = q.where.filter((term) => !predicatesOf(term).some((c) => isComputedField(c.field))).map((term) => prepareWhereTerm(term, resolveField2));
|
|
765
|
+
const filtered = rows.filter((row) => whereTerms.every((term) => matchesTerm(byName, row, term)));
|
|
766
|
+
const metrics = (q.aggregations ?? []).filter((a) => !isComputedField(a.field ?? "") && !a.groupBy.some(isComputedField)).map((agg) => ({
|
|
515
767
|
id: agg.id,
|
|
516
768
|
buckets: computeBuckets(filtered, agg, byName, resolveField2)
|
|
517
769
|
}));
|
|
@@ -522,9 +774,9 @@ function computeBuckets(rows, agg, byName, resolveField2) {
|
|
|
522
774
|
const measure = agg.field ? byName.get(resolveField2(agg.field)) : void 0;
|
|
523
775
|
const groups = /* @__PURE__ */ new Map();
|
|
524
776
|
for (const row of rows) {
|
|
525
|
-
const keys = groupFields.map((
|
|
526
|
-
if (!
|
|
527
|
-
const v = readFieldValue(
|
|
777
|
+
const keys = groupFields.map((f2) => {
|
|
778
|
+
if (!f2) return null;
|
|
779
|
+
const v = readFieldValue(f2, row);
|
|
528
780
|
return v == null || v === "" ? null : String(v);
|
|
529
781
|
});
|
|
530
782
|
const k = JSON.stringify(keys);
|
|
@@ -593,18 +845,48 @@ function sortBuckets(buckets) {
|
|
|
593
845
|
return JSON.stringify(a.keys).localeCompare(JSON.stringify(b.keys));
|
|
594
846
|
});
|
|
595
847
|
}
|
|
596
|
-
function
|
|
848
|
+
function prepareWhereClause(clause) {
|
|
849
|
+
const usesRegex = clause.op === "matches_regex" || clause.op === "not_matches_regex";
|
|
850
|
+
return { clause, regex: usesRegex ? compileRegex(clause.value) : void 0 };
|
|
851
|
+
}
|
|
852
|
+
function prepareWhereTerm(term, resolveField2) {
|
|
853
|
+
if (isOrGroup(term)) {
|
|
854
|
+
return { kind: "or", predicates: term.any.map((c) => prepareWhereClause({ ...c, field: resolveField2(c.field) })) };
|
|
855
|
+
}
|
|
856
|
+
return { kind: "lit", predicate: prepareWhereClause({ ...term, field: resolveField2(term.field) }) };
|
|
857
|
+
}
|
|
858
|
+
function matchesTerm(byName, row, term) {
|
|
859
|
+
if (term.kind === "lit") return matchesWith(byName, row, term.predicate);
|
|
860
|
+
return term.predicates.some((p) => matchesWith(byName, row, p));
|
|
861
|
+
}
|
|
862
|
+
function matchesWith(byName, row, prepared) {
|
|
863
|
+
const { clause } = prepared;
|
|
597
864
|
const field = byName.get(clause.field);
|
|
598
865
|
if (!field) return true;
|
|
599
866
|
const v = readFieldValue(field, row);
|
|
867
|
+
if (clause.value === "" && clause.op !== "=" && clause.op !== "!=" && clause.op !== "is_null" && clause.op !== "is_not_null") {
|
|
868
|
+
return true;
|
|
869
|
+
}
|
|
870
|
+
const base = matchesBase(field, v, prepared);
|
|
871
|
+
if (!clause.negated) return base;
|
|
872
|
+
const isNull = Array.isArray(v) ? v.length === 0 : v == null || v === "";
|
|
873
|
+
return !isNull && !base;
|
|
874
|
+
}
|
|
875
|
+
function matchesBase(field, v, prepared) {
|
|
876
|
+
const { clause, regex } = prepared;
|
|
600
877
|
if (clause.op === "is_null") return Array.isArray(v) ? v.length === 0 : v == null || v === "";
|
|
601
878
|
if (clause.op === "is_not_null") return Array.isArray(v) ? v.length > 0 : v != null && v !== "";
|
|
602
|
-
if (clause.value === "" && clause.op !== "=" && clause.op !== "!=") return true;
|
|
603
879
|
if (clause.op === "includes") {
|
|
604
880
|
const arr = Array.isArray(v) ? v : [];
|
|
605
881
|
const needle = clause.value.toLowerCase();
|
|
606
882
|
return arr.some((x) => String(x).toLowerCase() === needle);
|
|
607
883
|
}
|
|
884
|
+
if (clause.op === "matches_regex" || clause.op === "not_matches_regex") {
|
|
885
|
+
if (v == null || Array.isArray(v)) return false;
|
|
886
|
+
if (!regex) return false;
|
|
887
|
+
const matches = regex.test(String(v));
|
|
888
|
+
return clause.op === "matches_regex" ? matches : !matches;
|
|
889
|
+
}
|
|
608
890
|
if (Array.isArray(v)) {
|
|
609
891
|
const needle = clause.value.toLowerCase();
|
|
610
892
|
const hay = v.map((x) => String(x).toLowerCase());
|
|
@@ -642,6 +924,21 @@ function matchesWith(byName, row, clause) {
|
|
|
642
924
|
return true;
|
|
643
925
|
}
|
|
644
926
|
}
|
|
927
|
+
function compileRegex(pattern) {
|
|
928
|
+
if (pattern === void 0) return void 0;
|
|
929
|
+
try {
|
|
930
|
+
return new RegExp(pattern);
|
|
931
|
+
} catch {
|
|
932
|
+
return null;
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
function extractSortValue(value, regex) {
|
|
936
|
+
if (regex === void 0) return value;
|
|
937
|
+
if (regex === null || value == null) return null;
|
|
938
|
+
const match = regex.exec(String(value));
|
|
939
|
+
if (!match) return null;
|
|
940
|
+
return match.length > 1 ? match[1] ?? null : match[0];
|
|
941
|
+
}
|
|
645
942
|
function coerceSafe(field, raw) {
|
|
646
943
|
try {
|
|
647
944
|
return coerceValue(field.type, raw);
|
|
@@ -813,10 +1110,1117 @@ function localStorageAdapter() {
|
|
|
813
1110
|
}
|
|
814
1111
|
};
|
|
815
1112
|
}
|
|
1113
|
+
|
|
1114
|
+
// src/formula.ts
|
|
1115
|
+
var f = (name, signature, description, min, max, result, args) => ({
|
|
1116
|
+
name,
|
|
1117
|
+
signature,
|
|
1118
|
+
description,
|
|
1119
|
+
min,
|
|
1120
|
+
max,
|
|
1121
|
+
result,
|
|
1122
|
+
args
|
|
1123
|
+
});
|
|
1124
|
+
var FORMULA_FUNCTIONS = [
|
|
1125
|
+
...["LEFT", "RIGHT"].map(
|
|
1126
|
+
(n) => f(
|
|
1127
|
+
n,
|
|
1128
|
+
`${n}(text, count)`,
|
|
1129
|
+
"Take Unicode code points from the end indicated.",
|
|
1130
|
+
2,
|
|
1131
|
+
2,
|
|
1132
|
+
"text",
|
|
1133
|
+
["text", "number"]
|
|
1134
|
+
)
|
|
1135
|
+
),
|
|
1136
|
+
f(
|
|
1137
|
+
"SUBSTRING",
|
|
1138
|
+
"SUBSTRING(text, start, length)",
|
|
1139
|
+
"One-based start; length is optional.",
|
|
1140
|
+
2,
|
|
1141
|
+
3,
|
|
1142
|
+
"text",
|
|
1143
|
+
["text", "number", "number"]
|
|
1144
|
+
),
|
|
1145
|
+
f("LENGTH", "LENGTH(text)", "Count Unicode code points.", 1, 1, "number", [
|
|
1146
|
+
"text"
|
|
1147
|
+
]),
|
|
1148
|
+
...["LOWER", "UPPER", "TRIM", "LTRIM", "RTRIM"].map(
|
|
1149
|
+
(n) => f(
|
|
1150
|
+
n,
|
|
1151
|
+
`${n}(text)`,
|
|
1152
|
+
"Change case or remove surrounding whitespace.",
|
|
1153
|
+
1,
|
|
1154
|
+
1,
|
|
1155
|
+
"text",
|
|
1156
|
+
["text"]
|
|
1157
|
+
)
|
|
1158
|
+
),
|
|
1159
|
+
f(
|
|
1160
|
+
"REPLACE",
|
|
1161
|
+
"REPLACE(text, search, replacement)",
|
|
1162
|
+
"Replace all literal occurrences.",
|
|
1163
|
+
3,
|
|
1164
|
+
3,
|
|
1165
|
+
"text",
|
|
1166
|
+
["text", "text", "text"]
|
|
1167
|
+
),
|
|
1168
|
+
...["LPAD", "RPAD"].map(
|
|
1169
|
+
(n) => f(
|
|
1170
|
+
n,
|
|
1171
|
+
`${n}(text, length, padding)`,
|
|
1172
|
+
"Pad to a code-point length; default padding is a space.",
|
|
1173
|
+
2,
|
|
1174
|
+
3,
|
|
1175
|
+
"text",
|
|
1176
|
+
["text", "number", "text"]
|
|
1177
|
+
)
|
|
1178
|
+
),
|
|
1179
|
+
f(
|
|
1180
|
+
"SPLIT_PART",
|
|
1181
|
+
"SPLIT_PART(text, separator, index)",
|
|
1182
|
+
"One-based part; missing part returns null.",
|
|
1183
|
+
3,
|
|
1184
|
+
3,
|
|
1185
|
+
"text",
|
|
1186
|
+
["text", "text", "number"]
|
|
1187
|
+
),
|
|
1188
|
+
f(
|
|
1189
|
+
"CONCAT",
|
|
1190
|
+
"CONCAT(text, ...)",
|
|
1191
|
+
"Combine text; null propagates.",
|
|
1192
|
+
1,
|
|
1193
|
+
50,
|
|
1194
|
+
"text",
|
|
1195
|
+
["text"]
|
|
1196
|
+
),
|
|
1197
|
+
f(
|
|
1198
|
+
"CONCAT_WS",
|
|
1199
|
+
"CONCAT_WS(separator, text, ...)",
|
|
1200
|
+
"Combine text, skipping null arguments.",
|
|
1201
|
+
2,
|
|
1202
|
+
50,
|
|
1203
|
+
"text",
|
|
1204
|
+
["text"]
|
|
1205
|
+
),
|
|
1206
|
+
...["CONTAINS", "STARTS_WITH", "ENDS_WITH"].map(
|
|
1207
|
+
(n) => f(
|
|
1208
|
+
n,
|
|
1209
|
+
`${n}(text, search, ignoreCase)`,
|
|
1210
|
+
"Case-sensitive unless the optional third argument is true.",
|
|
1211
|
+
2,
|
|
1212
|
+
3,
|
|
1213
|
+
"bool",
|
|
1214
|
+
["text", "text", "bool"]
|
|
1215
|
+
)
|
|
1216
|
+
),
|
|
1217
|
+
f(
|
|
1218
|
+
"REGEX_TEST",
|
|
1219
|
+
"REGEX_TEST(text, pattern, flags)",
|
|
1220
|
+
"Test a JavaScript Unicode regex; flags: i, m, s.",
|
|
1221
|
+
2,
|
|
1222
|
+
3,
|
|
1223
|
+
"bool",
|
|
1224
|
+
["text", "text", "text"]
|
|
1225
|
+
),
|
|
1226
|
+
f(
|
|
1227
|
+
"REGEX_EXTRACT",
|
|
1228
|
+
"REGEX_EXTRACT(text, pattern, group, flags)",
|
|
1229
|
+
"Extract a capture (default 0: whole match); no match returns null.",
|
|
1230
|
+
2,
|
|
1231
|
+
4,
|
|
1232
|
+
"text",
|
|
1233
|
+
["text", "text", "number", "text"]
|
|
1234
|
+
),
|
|
1235
|
+
f(
|
|
1236
|
+
"REGEX_REPLACE",
|
|
1237
|
+
"REGEX_REPLACE(text, pattern, replacement, flags)",
|
|
1238
|
+
"Replace all matches; supports $1 capture references.",
|
|
1239
|
+
3,
|
|
1240
|
+
4,
|
|
1241
|
+
"text",
|
|
1242
|
+
["text", "text", "text", "text"]
|
|
1243
|
+
),
|
|
1244
|
+
...["ABS", "FLOOR", "CEIL", "TRUNC", "SQRT"].map(
|
|
1245
|
+
(n) => f(n, `${n}(number)`, "Numeric transformation.", 1, 1, "number", ["number"])
|
|
1246
|
+
),
|
|
1247
|
+
f(
|
|
1248
|
+
"ROUND",
|
|
1249
|
+
"ROUND(number, digits)",
|
|
1250
|
+
"Round to decimal places (default 0, range -15 to 15).",
|
|
1251
|
+
1,
|
|
1252
|
+
2,
|
|
1253
|
+
"number",
|
|
1254
|
+
["number"]
|
|
1255
|
+
),
|
|
1256
|
+
f(
|
|
1257
|
+
"POWER",
|
|
1258
|
+
"POWER(base, exponent)",
|
|
1259
|
+
"Raise a number to a power.",
|
|
1260
|
+
2,
|
|
1261
|
+
2,
|
|
1262
|
+
"number",
|
|
1263
|
+
["number"]
|
|
1264
|
+
),
|
|
1265
|
+
f(
|
|
1266
|
+
"CLAMP",
|
|
1267
|
+
"CLAMP(number, lower, upper)",
|
|
1268
|
+
"Constrain a number to an inclusive range.",
|
|
1269
|
+
3,
|
|
1270
|
+
3,
|
|
1271
|
+
"number",
|
|
1272
|
+
["number"]
|
|
1273
|
+
),
|
|
1274
|
+
...["LEAST", "GREATEST"].map(
|
|
1275
|
+
(n) => f(
|
|
1276
|
+
n,
|
|
1277
|
+
`${n}(number, ...)`,
|
|
1278
|
+
"Compare numbers within this row.",
|
|
1279
|
+
1,
|
|
1280
|
+
50,
|
|
1281
|
+
"number",
|
|
1282
|
+
["number"]
|
|
1283
|
+
)
|
|
1284
|
+
),
|
|
1285
|
+
f(
|
|
1286
|
+
"IF",
|
|
1287
|
+
"IF(condition, then, otherwise)",
|
|
1288
|
+
"Evaluate only the chosen branch; null condition uses otherwise.",
|
|
1289
|
+
3,
|
|
1290
|
+
3,
|
|
1291
|
+
"branch",
|
|
1292
|
+
"any"
|
|
1293
|
+
),
|
|
1294
|
+
f(
|
|
1295
|
+
"IFS",
|
|
1296
|
+
"IFS(condition, value, ..., fallback)",
|
|
1297
|
+
"First true condition wins; final fallback is required.",
|
|
1298
|
+
3,
|
|
1299
|
+
49,
|
|
1300
|
+
"branch",
|
|
1301
|
+
"any"
|
|
1302
|
+
),
|
|
1303
|
+
f(
|
|
1304
|
+
"SWITCH",
|
|
1305
|
+
"SWITCH(value, match, result, ..., fallback)",
|
|
1306
|
+
"Match a value to a result; final fallback is required.",
|
|
1307
|
+
4,
|
|
1308
|
+
50,
|
|
1309
|
+
"branch",
|
|
1310
|
+
"any"
|
|
1311
|
+
),
|
|
1312
|
+
f(
|
|
1313
|
+
"COALESCE",
|
|
1314
|
+
"COALESCE(value, ...)",
|
|
1315
|
+
"First non-null value, evaluated lazily.",
|
|
1316
|
+
1,
|
|
1317
|
+
50,
|
|
1318
|
+
"branch",
|
|
1319
|
+
"any"
|
|
1320
|
+
),
|
|
1321
|
+
f(
|
|
1322
|
+
"NULLIF",
|
|
1323
|
+
"NULLIF(value, other)",
|
|
1324
|
+
"Return null if the values are equal.",
|
|
1325
|
+
2,
|
|
1326
|
+
2,
|
|
1327
|
+
"branch",
|
|
1328
|
+
"any"
|
|
1329
|
+
),
|
|
1330
|
+
...["IS_NULL", "IS_EMPTY"].map(
|
|
1331
|
+
(n) => f(
|
|
1332
|
+
n,
|
|
1333
|
+
`${n}(value)`,
|
|
1334
|
+
"Test null, or null/empty text/empty array.",
|
|
1335
|
+
1,
|
|
1336
|
+
1,
|
|
1337
|
+
"bool",
|
|
1338
|
+
"any"
|
|
1339
|
+
)
|
|
1340
|
+
),
|
|
1341
|
+
f(
|
|
1342
|
+
"IFERROR",
|
|
1343
|
+
"IFERROR(value, fallback)",
|
|
1344
|
+
"Use fallback only when evaluating value fails.",
|
|
1345
|
+
2,
|
|
1346
|
+
2,
|
|
1347
|
+
"branch",
|
|
1348
|
+
"any"
|
|
1349
|
+
),
|
|
1350
|
+
...[
|
|
1351
|
+
["TO_TEXT", "text"],
|
|
1352
|
+
["TO_NUMBER", "number"],
|
|
1353
|
+
["TO_BOOLEAN", "bool"],
|
|
1354
|
+
["TO_DATETIME", "datetime"]
|
|
1355
|
+
].map(
|
|
1356
|
+
([n, t]) => f(
|
|
1357
|
+
n,
|
|
1358
|
+
`${n}(value)`,
|
|
1359
|
+
"Explicit conversion; invalid input produces a cell error.",
|
|
1360
|
+
1,
|
|
1361
|
+
1,
|
|
1362
|
+
t,
|
|
1363
|
+
"any"
|
|
1364
|
+
)
|
|
1365
|
+
),
|
|
1366
|
+
f(
|
|
1367
|
+
"SPLIT",
|
|
1368
|
+
"SPLIT(text, separator)",
|
|
1369
|
+
"Split text into an array.",
|
|
1370
|
+
2,
|
|
1371
|
+
2,
|
|
1372
|
+
"textarray",
|
|
1373
|
+
["text", "text"]
|
|
1374
|
+
),
|
|
1375
|
+
f("JOIN", "JOIN(array, separator)", "Join an array of text.", 2, 2, "text", [
|
|
1376
|
+
"textarray",
|
|
1377
|
+
"text"
|
|
1378
|
+
]),
|
|
1379
|
+
f(
|
|
1380
|
+
"ARRAY_LENGTH",
|
|
1381
|
+
"ARRAY_LENGTH(array)",
|
|
1382
|
+
"Count array elements.",
|
|
1383
|
+
1,
|
|
1384
|
+
1,
|
|
1385
|
+
"number",
|
|
1386
|
+
["textarray"]
|
|
1387
|
+
),
|
|
1388
|
+
f(
|
|
1389
|
+
"ARRAY_CONTAINS",
|
|
1390
|
+
"ARRAY_CONTAINS(array, text)",
|
|
1391
|
+
"Case-sensitive array membership.",
|
|
1392
|
+
2,
|
|
1393
|
+
2,
|
|
1394
|
+
"bool",
|
|
1395
|
+
["textarray", "text"]
|
|
1396
|
+
),
|
|
1397
|
+
f(
|
|
1398
|
+
"ARRAY_GET",
|
|
1399
|
+
"ARRAY_GET(array, index)",
|
|
1400
|
+
"One-based element; out of range returns null.",
|
|
1401
|
+
2,
|
|
1402
|
+
2,
|
|
1403
|
+
"text",
|
|
1404
|
+
["textarray", "number"]
|
|
1405
|
+
),
|
|
1406
|
+
...["ARRAY_UNIQUE", "ARRAY_SORT"].map(
|
|
1407
|
+
(n) => f(
|
|
1408
|
+
n,
|
|
1409
|
+
`${n}(array)`,
|
|
1410
|
+
"Deduplicate or sort text elements by code-point order.",
|
|
1411
|
+
1,
|
|
1412
|
+
1,
|
|
1413
|
+
"textarray",
|
|
1414
|
+
["textarray"]
|
|
1415
|
+
)
|
|
1416
|
+
),
|
|
1417
|
+
...["YEAR", "MONTH", "DAY", "HOUR", "WEEKDAY"].map(
|
|
1418
|
+
(n) => f(
|
|
1419
|
+
n,
|
|
1420
|
+
`${n}(datetime)`,
|
|
1421
|
+
"UTC date component; weekday is Monday=1 through Sunday=7.",
|
|
1422
|
+
1,
|
|
1423
|
+
1,
|
|
1424
|
+
"number",
|
|
1425
|
+
["datetime"]
|
|
1426
|
+
)
|
|
1427
|
+
),
|
|
1428
|
+
f(
|
|
1429
|
+
"DATE_TRUNC",
|
|
1430
|
+
"DATE_TRUNC(unit, datetime)",
|
|
1431
|
+
"UTC truncation: year, month, week (Monday), day, hour, minute, second.",
|
|
1432
|
+
2,
|
|
1433
|
+
2,
|
|
1434
|
+
"datetime",
|
|
1435
|
+
["text", "datetime"]
|
|
1436
|
+
),
|
|
1437
|
+
f(
|
|
1438
|
+
"DATE_ADD",
|
|
1439
|
+
"DATE_ADD(unit, amount, datetime)",
|
|
1440
|
+
"UTC calendar addition; month/year clamp to the last day.",
|
|
1441
|
+
3,
|
|
1442
|
+
3,
|
|
1443
|
+
"datetime",
|
|
1444
|
+
["text", "number", "datetime"]
|
|
1445
|
+
),
|
|
1446
|
+
f(
|
|
1447
|
+
"DATE_DIFF",
|
|
1448
|
+
"DATE_DIFF(unit, start, end)",
|
|
1449
|
+
"Elapsed whole units: week, day, hour, minute, second, millisecond.",
|
|
1450
|
+
3,
|
|
1451
|
+
3,
|
|
1452
|
+
"number",
|
|
1453
|
+
["text", "datetime", "datetime"]
|
|
1454
|
+
),
|
|
1455
|
+
f(
|
|
1456
|
+
"FORMAT_DATE",
|
|
1457
|
+
"FORMAT_DATE(datetime, pattern)",
|
|
1458
|
+
"UTC tokens: YYYY MM DD HH mm ss; other text is literal.",
|
|
1459
|
+
2,
|
|
1460
|
+
2,
|
|
1461
|
+
"text",
|
|
1462
|
+
["datetime", "text"]
|
|
1463
|
+
)
|
|
1464
|
+
];
|
|
1465
|
+
var FormulaError = class extends Error {
|
|
1466
|
+
constructor(message, from = 0, to = from + 1) {
|
|
1467
|
+
super(message);
|
|
1468
|
+
this.from = from;
|
|
1469
|
+
this.to = to;
|
|
1470
|
+
}
|
|
1471
|
+
};
|
|
1472
|
+
function compileFormula(source, fields, resolve) {
|
|
1473
|
+
if (!source.trim() || source.length > 1e4)
|
|
1474
|
+
throw new FormulaError("Formula must contain 1\u201310,000 characters.");
|
|
1475
|
+
const tokens = [];
|
|
1476
|
+
let pos = 0;
|
|
1477
|
+
while (pos < source.length) {
|
|
1478
|
+
if (/\s/.test(source[pos])) {
|
|
1479
|
+
pos++;
|
|
1480
|
+
continue;
|
|
1481
|
+
}
|
|
1482
|
+
const from = pos;
|
|
1483
|
+
let match;
|
|
1484
|
+
if (source[pos] === "[") {
|
|
1485
|
+
pos++;
|
|
1486
|
+
let text = "";
|
|
1487
|
+
let closed = false;
|
|
1488
|
+
while (pos < source.length) {
|
|
1489
|
+
if (source[pos] === "]") {
|
|
1490
|
+
if (source[pos + 1] === "]") {
|
|
1491
|
+
text += "]";
|
|
1492
|
+
pos += 2;
|
|
1493
|
+
} else {
|
|
1494
|
+
pos++;
|
|
1495
|
+
closed = true;
|
|
1496
|
+
break;
|
|
1497
|
+
}
|
|
1498
|
+
} else text += source[pos++];
|
|
1499
|
+
}
|
|
1500
|
+
if (!closed)
|
|
1501
|
+
throw new FormulaError("Unclosed field reference.", from, pos);
|
|
1502
|
+
tokens.push({ kind: "field", text, from, to: pos });
|
|
1503
|
+
} else if (source[pos] === '"') {
|
|
1504
|
+
pos++;
|
|
1505
|
+
while (pos < source.length && source[pos] !== '"') {
|
|
1506
|
+
if (source[pos] === "\\") pos++;
|
|
1507
|
+
pos++;
|
|
1508
|
+
}
|
|
1509
|
+
pos++;
|
|
1510
|
+
try {
|
|
1511
|
+
tokens.push({
|
|
1512
|
+
kind: "string",
|
|
1513
|
+
text: JSON.parse(source.slice(from, pos)),
|
|
1514
|
+
from,
|
|
1515
|
+
to: pos
|
|
1516
|
+
});
|
|
1517
|
+
} catch {
|
|
1518
|
+
throw new FormulaError("Invalid JSON string literal.", from, pos);
|
|
1519
|
+
}
|
|
1520
|
+
} else if (match = source.slice(pos).match(/^(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?/)) {
|
|
1521
|
+
pos += match[0].length;
|
|
1522
|
+
tokens.push({ kind: "number", text: match[0], from, to: pos });
|
|
1523
|
+
} else if (match = source.slice(pos).match(/^[a-zA-Z_][a-zA-Z_0-9]*/)) {
|
|
1524
|
+
pos += match[0].length;
|
|
1525
|
+
tokens.push({
|
|
1526
|
+
kind: "word",
|
|
1527
|
+
text: match[0].toUpperCase(),
|
|
1528
|
+
from,
|
|
1529
|
+
to: pos
|
|
1530
|
+
});
|
|
1531
|
+
} else if (match = source.slice(pos).match(/^(>=|<=|!=|<>|[=<>+*/%(),-])/)) {
|
|
1532
|
+
pos += match[0].length;
|
|
1533
|
+
tokens.push({ kind: "op", text: match[0], from, to: pos });
|
|
1534
|
+
} else throw new FormulaError(`Unexpected character ${source[pos]}.`, from);
|
|
1535
|
+
if (tokens.length > 2e3)
|
|
1536
|
+
throw new FormulaError("Formula is too complex.", from);
|
|
1537
|
+
}
|
|
1538
|
+
tokens.push({ kind: "end", text: "", from: pos, to: pos });
|
|
1539
|
+
let i = 0, depth = 0;
|
|
1540
|
+
const peek = () => tokens[i];
|
|
1541
|
+
const take = () => tokens[i++];
|
|
1542
|
+
const expect = (s) => {
|
|
1543
|
+
if (peek().text !== s)
|
|
1544
|
+
throw new FormulaError(`Expected ${s}.`, peek().from, peek().to);
|
|
1545
|
+
return take();
|
|
1546
|
+
};
|
|
1547
|
+
const call = (name, args, from, to) => ({ kind: "call", name, args, from, to });
|
|
1548
|
+
const priority = {
|
|
1549
|
+
OR: 1,
|
|
1550
|
+
AND: 2,
|
|
1551
|
+
"=": 3,
|
|
1552
|
+
"!=": 3,
|
|
1553
|
+
"<>": 3,
|
|
1554
|
+
"<": 3,
|
|
1555
|
+
">": 3,
|
|
1556
|
+
"<=": 3,
|
|
1557
|
+
">=": 3,
|
|
1558
|
+
IN: 3,
|
|
1559
|
+
BETWEEN: 3,
|
|
1560
|
+
"+": 4,
|
|
1561
|
+
"-": 4,
|
|
1562
|
+
"*": 5,
|
|
1563
|
+
"/": 5,
|
|
1564
|
+
"%": 5
|
|
1565
|
+
};
|
|
1566
|
+
function expression(min = 0) {
|
|
1567
|
+
if (++depth > 50)
|
|
1568
|
+
throw new FormulaError("Maximum nesting depth is 50.", peek().from);
|
|
1569
|
+
const t = take();
|
|
1570
|
+
let node;
|
|
1571
|
+
if (t.kind === "number") {
|
|
1572
|
+
if (!Number.isFinite(Number(t.text)))
|
|
1573
|
+
throw new FormulaError("Number must be finite.", t.from, t.to);
|
|
1574
|
+
node = { kind: "literal", value: Number(t.text), from: t.from, to: t.to };
|
|
1575
|
+
} else if (t.kind === "string")
|
|
1576
|
+
node = { kind: "literal", value: t.text, from: t.from, to: t.to };
|
|
1577
|
+
else if (t.kind === "field")
|
|
1578
|
+
node = { kind: "field", name: t.text, from: t.from, to: t.to };
|
|
1579
|
+
else if (["TRUE", "FALSE", "NULL"].includes(t.text))
|
|
1580
|
+
node = {
|
|
1581
|
+
kind: "literal",
|
|
1582
|
+
value: t.text === "NULL" ? null : t.text === "TRUE",
|
|
1583
|
+
from: t.from,
|
|
1584
|
+
to: t.to
|
|
1585
|
+
};
|
|
1586
|
+
else if (t.text === "(") {
|
|
1587
|
+
node = expression();
|
|
1588
|
+
expect(")");
|
|
1589
|
+
} else if (["NOT", "-", "+"].includes(t.text)) {
|
|
1590
|
+
const arg = expression(t.text === "NOT" ? 3 : 6);
|
|
1591
|
+
node = call(`unary:${t.text}`, [arg], t.from, arg.to);
|
|
1592
|
+
} else if (t.kind === "word") {
|
|
1593
|
+
expect("(");
|
|
1594
|
+
const args = [];
|
|
1595
|
+
if (peek().text !== ")") {
|
|
1596
|
+
do {
|
|
1597
|
+
args.push(expression());
|
|
1598
|
+
if (peek().text !== ",") break;
|
|
1599
|
+
take();
|
|
1600
|
+
} while (true);
|
|
1601
|
+
}
|
|
1602
|
+
node = call(t.text, args, t.from, expect(")").to);
|
|
1603
|
+
} else
|
|
1604
|
+
throw new FormulaError(
|
|
1605
|
+
"Expected a value, field, or function.",
|
|
1606
|
+
t.from,
|
|
1607
|
+
t.to
|
|
1608
|
+
);
|
|
1609
|
+
while (peek().kind !== "end" && (priority[peek().text] ?? -1) >= min) {
|
|
1610
|
+
const op = take();
|
|
1611
|
+
const p = priority[op.text];
|
|
1612
|
+
if (op.text === "IN") {
|
|
1613
|
+
expect("(");
|
|
1614
|
+
const args = [node];
|
|
1615
|
+
do {
|
|
1616
|
+
args.push(expression());
|
|
1617
|
+
if (peek().text !== ",") break;
|
|
1618
|
+
take();
|
|
1619
|
+
} while (true);
|
|
1620
|
+
node = call("op:IN", args, node.from, expect(")").to);
|
|
1621
|
+
} else if (op.text === "BETWEEN") {
|
|
1622
|
+
const lo = expression(p + 1);
|
|
1623
|
+
expect("AND");
|
|
1624
|
+
const hi = expression(p + 1);
|
|
1625
|
+
node = call("op:BETWEEN", [node, lo, hi], node.from, hi.to);
|
|
1626
|
+
} else {
|
|
1627
|
+
const right = expression(p + 1);
|
|
1628
|
+
node = call(`op:${op.text}`, [node, right], node.from, right.to);
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
depth--;
|
|
1632
|
+
return node;
|
|
1633
|
+
}
|
|
1634
|
+
const ast = expression();
|
|
1635
|
+
if (peek().kind !== "end")
|
|
1636
|
+
throw new FormulaError("Unexpected token.", peek().from, peek().to);
|
|
1637
|
+
const deps = /* @__PURE__ */ new Set();
|
|
1638
|
+
const byName = new Map(fields.map((v) => [v.name, v.type]));
|
|
1639
|
+
const same = (ts, n) => {
|
|
1640
|
+
const real = [...new Set(ts.filter((t) => t !== "null"))];
|
|
1641
|
+
if (real.length > 1)
|
|
1642
|
+
throw new FormulaError(
|
|
1643
|
+
"Values must have compatible types; use an explicit conversion.",
|
|
1644
|
+
n.from,
|
|
1645
|
+
n.to
|
|
1646
|
+
);
|
|
1647
|
+
return real[0] ?? "null";
|
|
1648
|
+
};
|
|
1649
|
+
function check(n, level = 0) {
|
|
1650
|
+
if (level > 50)
|
|
1651
|
+
throw new FormulaError("Maximum nesting depth is 50.", n.from, n.to);
|
|
1652
|
+
if (n.kind === "literal")
|
|
1653
|
+
return n.value === null ? "null" : typeof n.value === "boolean" ? "bool" : typeof n.value === "number" ? "number" : "text";
|
|
1654
|
+
if (n.kind === "field") {
|
|
1655
|
+
const t = byName.get(n.name);
|
|
1656
|
+
if (!t) {
|
|
1657
|
+
const nested = resolve?.(n.name);
|
|
1658
|
+
if (nested) {
|
|
1659
|
+
Object.assign(n, nested.ast);
|
|
1660
|
+
nested.dependencies.forEach((d) => deps.add(d));
|
|
1661
|
+
return nested.type;
|
|
1662
|
+
}
|
|
1663
|
+
throw new FormulaError(
|
|
1664
|
+
`Unknown or unavailable field: ${n.name}`,
|
|
1665
|
+
n.from,
|
|
1666
|
+
n.to
|
|
1667
|
+
);
|
|
1668
|
+
}
|
|
1669
|
+
deps.add(n.name);
|
|
1670
|
+
n.valueType = t === "enum" ? "text" : t;
|
|
1671
|
+
return n.valueType;
|
|
1672
|
+
}
|
|
1673
|
+
const ts = n.args.map((a) => check(a, level + 1));
|
|
1674
|
+
const need = (index, t) => {
|
|
1675
|
+
if (ts[index] !== void 0 && ts[index] !== "null" && ts[index] !== t)
|
|
1676
|
+
throw new FormulaError(
|
|
1677
|
+
`Expected ${t}, received ${ts[index]}.`,
|
|
1678
|
+
n.args[index].from,
|
|
1679
|
+
n.args[index].to
|
|
1680
|
+
);
|
|
1681
|
+
};
|
|
1682
|
+
if (n.name.startsWith("op:") || n.name.startsWith("unary:")) {
|
|
1683
|
+
const op = n.name.split(":")[1];
|
|
1684
|
+
if (["AND", "OR", "NOT"].includes(op)) {
|
|
1685
|
+
ts.forEach((_, j) => need(j, "bool"));
|
|
1686
|
+
return "bool";
|
|
1687
|
+
}
|
|
1688
|
+
if (["+", "-", "*", "/", "%"].includes(op)) {
|
|
1689
|
+
ts.forEach((_, j) => need(j, "number"));
|
|
1690
|
+
return "number";
|
|
1691
|
+
}
|
|
1692
|
+
same(ts, n);
|
|
1693
|
+
return "bool";
|
|
1694
|
+
}
|
|
1695
|
+
const spec = FORMULA_FUNCTIONS.find((v) => v.name === n.name);
|
|
1696
|
+
if (!spec)
|
|
1697
|
+
throw new FormulaError(`Unknown function: ${n.name}`, n.from, n.to);
|
|
1698
|
+
if (ts.length < spec.min || ts.length > spec.max)
|
|
1699
|
+
throw new FormulaError(`Use ${spec.signature}.`, n.from, n.to);
|
|
1700
|
+
if (spec.args !== "any") {
|
|
1701
|
+
const args = spec.args;
|
|
1702
|
+
ts.forEach((_, j) => need(j, args[Math.min(j, args.length - 1)]));
|
|
1703
|
+
}
|
|
1704
|
+
if (n.name === "IF") {
|
|
1705
|
+
need(0, "bool");
|
|
1706
|
+
return same(ts.slice(1), n);
|
|
1707
|
+
}
|
|
1708
|
+
if (n.name === "IFS") {
|
|
1709
|
+
if (ts.length % 2 !== 1)
|
|
1710
|
+
throw new FormulaError(
|
|
1711
|
+
"IFS requires condition/value pairs and a fallback.",
|
|
1712
|
+
n.from,
|
|
1713
|
+
n.to
|
|
1714
|
+
);
|
|
1715
|
+
const branches = [];
|
|
1716
|
+
ts.forEach((t, j) => {
|
|
1717
|
+
if (j === ts.length - 1 || j % 2 === 1) branches.push(t);
|
|
1718
|
+
else need(j, "bool");
|
|
1719
|
+
});
|
|
1720
|
+
return same(branches, n);
|
|
1721
|
+
}
|
|
1722
|
+
if (n.name === "SWITCH") {
|
|
1723
|
+
if (ts.length % 2 !== 0)
|
|
1724
|
+
throw new FormulaError(
|
|
1725
|
+
"SWITCH requires match/result pairs and a fallback.",
|
|
1726
|
+
n.from,
|
|
1727
|
+
n.to
|
|
1728
|
+
);
|
|
1729
|
+
const branches = [];
|
|
1730
|
+
for (let j = 1; j < ts.length - 1; j += 2) {
|
|
1731
|
+
same([ts[0], ts[j]], n);
|
|
1732
|
+
branches.push(ts[j + 1]);
|
|
1733
|
+
}
|
|
1734
|
+
branches.push(ts[ts.length - 1]);
|
|
1735
|
+
return same(branches, n);
|
|
1736
|
+
}
|
|
1737
|
+
if (n.name.startsWith("REGEX_")) {
|
|
1738
|
+
const pattern = n.args[1];
|
|
1739
|
+
const flags = n.args[n.name === "REGEX_TEST" ? 2 : 3];
|
|
1740
|
+
if (pattern?.kind === "literal" && typeof pattern.value === "string") {
|
|
1741
|
+
if (pattern.value.length > 2e3)
|
|
1742
|
+
throw new FormulaError(
|
|
1743
|
+
"Regex pattern exceeds 2,000 characters.",
|
|
1744
|
+
pattern.from,
|
|
1745
|
+
pattern.to
|
|
1746
|
+
);
|
|
1747
|
+
if (!flags || flags.kind === "literal" && typeof flags.value === "string") {
|
|
1748
|
+
const value = flags?.value ?? "";
|
|
1749
|
+
if (!/^(?!.*(.).*\1)[ims]*$/.test(String(value)))
|
|
1750
|
+
throw new FormulaError(
|
|
1751
|
+
"Regex flags must be unique i, m, or s.",
|
|
1752
|
+
n.from,
|
|
1753
|
+
n.to
|
|
1754
|
+
);
|
|
1755
|
+
try {
|
|
1756
|
+
new RegExp(pattern.value, `${value}u`);
|
|
1757
|
+
} catch {
|
|
1758
|
+
throw new FormulaError(
|
|
1759
|
+
"Invalid regex pattern.",
|
|
1760
|
+
pattern.from,
|
|
1761
|
+
pattern.to
|
|
1762
|
+
);
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
if (spec.result === "branch") return same(ts, n);
|
|
1768
|
+
return spec.result;
|
|
1769
|
+
}
|
|
1770
|
+
const type = check(ast);
|
|
1771
|
+
return { ast, dependencies: [...deps].sort(), type };
|
|
1772
|
+
}
|
|
1773
|
+
function formulaRuntime(ast, inputs) {
|
|
1774
|
+
const fail = (s) => {
|
|
1775
|
+
throw new Error(s);
|
|
1776
|
+
};
|
|
1777
|
+
const num = (v) => typeof v === "number" && Number.isFinite(v) ? v : fail("Expected a finite number.");
|
|
1778
|
+
const str = (v) => typeof v === "string" ? v : fail("Expected text.");
|
|
1779
|
+
const arr = (v) => Array.isArray(v) && v.every((x) => typeof x === "string") ? v : fail("Expected a text array.");
|
|
1780
|
+
const integer = (v, min = 0, max = 1e5) => {
|
|
1781
|
+
const n = num(v);
|
|
1782
|
+
return Number.isInteger(n) && n >= min && n <= max ? n : fail(`Expected an integer between ${min} and ${max}.`);
|
|
1783
|
+
};
|
|
1784
|
+
const equal = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
1785
|
+
const date = (v) => {
|
|
1786
|
+
const s = str(v);
|
|
1787
|
+
if (!/^\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?(?:Z|[+-]\d{2}:\d{2}))?$/.test(
|
|
1788
|
+
s
|
|
1789
|
+
))
|
|
1790
|
+
fail("Expected an ISO date or timestamp with timezone.");
|
|
1791
|
+
const d = new Date(s);
|
|
1792
|
+
const day = Number(s.slice(8, 10)), month = Number(s.slice(5, 7));
|
|
1793
|
+
if (!Number.isFinite(d.getTime()) || month < 1 || month > 12 || day < 1 || day > new Date(Date.UTC(Number(s.slice(0, 4)), month, 0)).getUTCDate())
|
|
1794
|
+
fail("Invalid date.");
|
|
1795
|
+
return d;
|
|
1796
|
+
};
|
|
1797
|
+
const bound = (v) => {
|
|
1798
|
+
if (v !== null && typeof v !== "number" && typeof v !== "string" && typeof v !== "boolean" && !Array.isArray(v))
|
|
1799
|
+
fail("Unsupported input value type.");
|
|
1800
|
+
if (Array.isArray(v) && !v.every((x) => typeof x === "string"))
|
|
1801
|
+
fail("Expected a text array.");
|
|
1802
|
+
if (typeof v === "number" && !Number.isFinite(v))
|
|
1803
|
+
fail("Result is not finite.");
|
|
1804
|
+
if (typeof v === "string" && v.length > 1e5)
|
|
1805
|
+
fail("Text result exceeds 100,000 characters.");
|
|
1806
|
+
if (Array.isArray(v) && (v.length > 1e4 || v.reduce((s, x) => s + x.length, 0) > 1e5))
|
|
1807
|
+
fail("Array result is too large.");
|
|
1808
|
+
return v;
|
|
1809
|
+
};
|
|
1810
|
+
let inspection;
|
|
1811
|
+
let budget = 5e3;
|
|
1812
|
+
function run(n) {
|
|
1813
|
+
if (--budget < 0) fail("Formula operation limit exceeded.");
|
|
1814
|
+
if (n.kind === "literal") return n.value;
|
|
1815
|
+
if (n.kind === "field") {
|
|
1816
|
+
if (!Object.prototype.hasOwnProperty.call(inputs, n.name)) return null;
|
|
1817
|
+
const v = bound(inputs[n.name] ?? null);
|
|
1818
|
+
if (v === null) return null;
|
|
1819
|
+
if (n.valueType === "number") return num(v);
|
|
1820
|
+
if (n.valueType === "bool" && typeof v !== "boolean")
|
|
1821
|
+
fail("Expected a boolean.");
|
|
1822
|
+
if (n.valueType === "datetime") return date(v).toISOString();
|
|
1823
|
+
if (n.valueType === "text") return str(v);
|
|
1824
|
+
if (n.valueType === "textarray") return arr(v);
|
|
1825
|
+
return v;
|
|
1826
|
+
}
|
|
1827
|
+
const name = n.name;
|
|
1828
|
+
const args = n.args;
|
|
1829
|
+
if (name === "IF") return run(run(args[0]) === true ? args[1] : args[2]);
|
|
1830
|
+
if (name === "IFS") {
|
|
1831
|
+
for (let j = 0; j < args.length - 1; j += 2)
|
|
1832
|
+
if (run(args[j]) === true) return run(args[j + 1]);
|
|
1833
|
+
return run(args[args.length - 1]);
|
|
1834
|
+
}
|
|
1835
|
+
if (name === "SWITCH") {
|
|
1836
|
+
const v = run(args[0]);
|
|
1837
|
+
for (let j = 1; j < args.length - 1; j += 2)
|
|
1838
|
+
if (equal(v, run(args[j]))) return run(args[j + 1]);
|
|
1839
|
+
return run(args[args.length - 1]);
|
|
1840
|
+
}
|
|
1841
|
+
if (name === "COALESCE") {
|
|
1842
|
+
for (const arg of args) {
|
|
1843
|
+
const v = run(arg);
|
|
1844
|
+
if (v !== null) return v;
|
|
1845
|
+
}
|
|
1846
|
+
return null;
|
|
1847
|
+
}
|
|
1848
|
+
if (name === "IFERROR") {
|
|
1849
|
+
try {
|
|
1850
|
+
return run(args[0]);
|
|
1851
|
+
} catch {
|
|
1852
|
+
return run(args[1]);
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
if (name === "op:AND" || name === "op:OR") {
|
|
1856
|
+
const a2 = run(args[0]);
|
|
1857
|
+
if (name === "op:AND" && a2 === false) return false;
|
|
1858
|
+
if (name === "op:OR" && a2 === true) return true;
|
|
1859
|
+
const b2 = run(args[1]);
|
|
1860
|
+
return name === "op:AND" ? b2 === false ? false : a2 === null || b2 === null ? null : true : b2 === true ? true : a2 === null || b2 === null ? null : false;
|
|
1861
|
+
}
|
|
1862
|
+
const vs = args.map(run);
|
|
1863
|
+
const a = vs[0] ?? null, b = vs[1] ?? null, c = vs[2] ?? null;
|
|
1864
|
+
if (name === "IS_NULL") return a === null;
|
|
1865
|
+
if (name === "IS_EMPTY")
|
|
1866
|
+
return a === null || a === "" || Array.isArray(a) && a.length === 0;
|
|
1867
|
+
if (name === "NULLIF") return equal(a, b) ? null : a;
|
|
1868
|
+
if (name === "CONCAT_WS")
|
|
1869
|
+
return a === null ? null : bound(
|
|
1870
|
+
vs.slice(1).filter((v) => v !== null).map(str).join(str(a))
|
|
1871
|
+
);
|
|
1872
|
+
if (name === "op:IN") {
|
|
1873
|
+
if (a === null) return null;
|
|
1874
|
+
if (vs.slice(1).some((v) => v !== null && equal(a, v))) return true;
|
|
1875
|
+
return vs.slice(1).includes(null) ? null : false;
|
|
1876
|
+
}
|
|
1877
|
+
if (vs.some((v) => v === null)) return null;
|
|
1878
|
+
let out;
|
|
1879
|
+
switch (name) {
|
|
1880
|
+
case "unary:NOT":
|
|
1881
|
+
out = !a;
|
|
1882
|
+
break;
|
|
1883
|
+
case "unary:-":
|
|
1884
|
+
out = -num(a);
|
|
1885
|
+
break;
|
|
1886
|
+
case "unary:+":
|
|
1887
|
+
out = num(a);
|
|
1888
|
+
break;
|
|
1889
|
+
case "op:+":
|
|
1890
|
+
out = num(a) + num(b);
|
|
1891
|
+
break;
|
|
1892
|
+
case "op:-":
|
|
1893
|
+
out = num(a) - num(b);
|
|
1894
|
+
break;
|
|
1895
|
+
case "op:*":
|
|
1896
|
+
out = num(a) * num(b);
|
|
1897
|
+
break;
|
|
1898
|
+
case "op:/":
|
|
1899
|
+
case "op:%":
|
|
1900
|
+
if (num(b) === 0) fail("Division by zero.");
|
|
1901
|
+
out = name === "op:/" ? num(a) / num(b) : num(a) % num(b);
|
|
1902
|
+
break;
|
|
1903
|
+
case "op:=":
|
|
1904
|
+
out = equal(a, b);
|
|
1905
|
+
break;
|
|
1906
|
+
case "op:!=":
|
|
1907
|
+
case "op:<>":
|
|
1908
|
+
out = !equal(a, b);
|
|
1909
|
+
break;
|
|
1910
|
+
case "op:<":
|
|
1911
|
+
out = a < b;
|
|
1912
|
+
break;
|
|
1913
|
+
case "op:>":
|
|
1914
|
+
out = a > b;
|
|
1915
|
+
break;
|
|
1916
|
+
case "op:<=":
|
|
1917
|
+
out = a <= b;
|
|
1918
|
+
break;
|
|
1919
|
+
case "op:>=":
|
|
1920
|
+
out = a >= b;
|
|
1921
|
+
break;
|
|
1922
|
+
case "op:BETWEEN":
|
|
1923
|
+
out = a >= b && a <= c;
|
|
1924
|
+
break;
|
|
1925
|
+
case "LEFT":
|
|
1926
|
+
out = Array.from(str(a)).slice(0, integer(b)).join("");
|
|
1927
|
+
break;
|
|
1928
|
+
case "RIGHT": {
|
|
1929
|
+
const n2 = integer(b);
|
|
1930
|
+
out = n2 ? Array.from(str(a)).slice(-n2).join("") : "";
|
|
1931
|
+
break;
|
|
1932
|
+
}
|
|
1933
|
+
case "SUBSTRING": {
|
|
1934
|
+
const start = integer(b, 1) - 1;
|
|
1935
|
+
out = Array.from(str(a)).slice(start, vs.length > 2 ? start + integer(c) : void 0).join("");
|
|
1936
|
+
break;
|
|
1937
|
+
}
|
|
1938
|
+
case "LENGTH":
|
|
1939
|
+
out = Array.from(str(a)).length;
|
|
1940
|
+
break;
|
|
1941
|
+
case "LOWER":
|
|
1942
|
+
out = str(a).toLowerCase();
|
|
1943
|
+
break;
|
|
1944
|
+
case "UPPER":
|
|
1945
|
+
out = str(a).toUpperCase();
|
|
1946
|
+
break;
|
|
1947
|
+
case "TRIM":
|
|
1948
|
+
out = str(a).trim();
|
|
1949
|
+
break;
|
|
1950
|
+
case "LTRIM":
|
|
1951
|
+
out = str(a).trimStart();
|
|
1952
|
+
break;
|
|
1953
|
+
case "RTRIM":
|
|
1954
|
+
out = str(a).trimEnd();
|
|
1955
|
+
break;
|
|
1956
|
+
case "REPLACE":
|
|
1957
|
+
if (b === "") fail("Search text must not be empty.");
|
|
1958
|
+
const parts = str(a).split(str(b));
|
|
1959
|
+
if (parts.length * str(c).length + str(a).length > 2e5)
|
|
1960
|
+
fail("Replacement result is too large.");
|
|
1961
|
+
out = parts.join(str(c));
|
|
1962
|
+
break;
|
|
1963
|
+
case "LPAD":
|
|
1964
|
+
case "RPAD": {
|
|
1965
|
+
const chars = Array.from(str(a)), len = integer(b), pad = Array.from(vs.length > 2 ? str(c) : " ");
|
|
1966
|
+
if (!pad.length) fail("Padding must not be empty.");
|
|
1967
|
+
const fill = Array.from(
|
|
1968
|
+
{ length: Math.max(0, len - chars.length) },
|
|
1969
|
+
(_, i) => pad[i % pad.length]
|
|
1970
|
+
).join("");
|
|
1971
|
+
out = (name === "LPAD" ? fill : "") + chars.slice(0, len).join("") + (name === "RPAD" ? fill : "");
|
|
1972
|
+
break;
|
|
1973
|
+
}
|
|
1974
|
+
case "SPLIT_PART":
|
|
1975
|
+
out = str(a).split(str(b))[integer(c, 1) - 1] ?? null;
|
|
1976
|
+
break;
|
|
1977
|
+
case "CONCAT":
|
|
1978
|
+
out = vs.map(str).join("");
|
|
1979
|
+
break;
|
|
1980
|
+
case "CONTAINS":
|
|
1981
|
+
case "STARTS_WITH":
|
|
1982
|
+
case "ENDS_WITH": {
|
|
1983
|
+
const s = c === true ? str(a).toLowerCase() : str(a), t = c === true ? str(b).toLowerCase() : str(b);
|
|
1984
|
+
out = name === "CONTAINS" ? s.includes(t) : name === "STARTS_WITH" ? s.startsWith(t) : s.endsWith(t);
|
|
1985
|
+
break;
|
|
1986
|
+
}
|
|
1987
|
+
case "REGEX_TEST":
|
|
1988
|
+
case "REGEX_EXTRACT":
|
|
1989
|
+
case "REGEX_REPLACE": {
|
|
1990
|
+
const flags = vs[name === "REGEX_TEST" ? 2 : 3] ?? "";
|
|
1991
|
+
if (!/^(?!.*(.).*\1)[ims]*$/.test(str(flags)))
|
|
1992
|
+
fail("Regex flags must be unique i, m, or s.");
|
|
1993
|
+
if (str(b).length > 2e3)
|
|
1994
|
+
fail("Regex pattern exceeds 2,000 characters.");
|
|
1995
|
+
const re = new RegExp(
|
|
1996
|
+
str(b),
|
|
1997
|
+
`${flags}u${name === "REGEX_REPLACE" ? "g" : ""}`
|
|
1998
|
+
);
|
|
1999
|
+
if (name === "REGEX_TEST") out = re.test(str(a));
|
|
2000
|
+
else if (name === "REGEX_REPLACE") {
|
|
2001
|
+
let produced = 0;
|
|
2002
|
+
out = str(a).replace(re, (...captures) => {
|
|
2003
|
+
const replacement = str(c).replace(
|
|
2004
|
+
/\$(\$|[0-9]{1,2}|&|`|')/g,
|
|
2005
|
+
(_token, key) => {
|
|
2006
|
+
if (key === "$") return "$";
|
|
2007
|
+
if (key === "&") return String(captures[0]);
|
|
2008
|
+
if (key === "`" || key === "'")
|
|
2009
|
+
return fail(
|
|
2010
|
+
"Regex replacement supports $$, $&, and numbered captures only."
|
|
2011
|
+
);
|
|
2012
|
+
const extra = typeof captures[captures.length - 1] === "object" ? 3 : 2;
|
|
2013
|
+
const i = Number(key);
|
|
2014
|
+
return i > 0 && i < captures.length - extra ? String(captures[i] ?? "") : fail("Invalid replacement capture.");
|
|
2015
|
+
}
|
|
2016
|
+
);
|
|
2017
|
+
produced += replacement.length;
|
|
2018
|
+
if (produced > 1e5) fail("Replacement result is too large.");
|
|
2019
|
+
return replacement;
|
|
2020
|
+
});
|
|
2021
|
+
} else {
|
|
2022
|
+
const match = re.exec(str(a));
|
|
2023
|
+
const group = vs.length > 2 ? integer(c, 0, 100) : 0;
|
|
2024
|
+
out = match?.[group] ?? null;
|
|
2025
|
+
if (match)
|
|
2026
|
+
inspection = {
|
|
2027
|
+
input: str(a),
|
|
2028
|
+
start: match.index,
|
|
2029
|
+
end: match.index + match[0].length,
|
|
2030
|
+
groups: Array.from(match, (v) => v ?? null)
|
|
2031
|
+
};
|
|
2032
|
+
}
|
|
2033
|
+
break;
|
|
2034
|
+
}
|
|
2035
|
+
case "ABS":
|
|
2036
|
+
out = Math.abs(num(a));
|
|
2037
|
+
break;
|
|
2038
|
+
case "FLOOR":
|
|
2039
|
+
out = Math.floor(num(a));
|
|
2040
|
+
break;
|
|
2041
|
+
case "CEIL":
|
|
2042
|
+
out = Math.ceil(num(a));
|
|
2043
|
+
break;
|
|
2044
|
+
case "TRUNC":
|
|
2045
|
+
out = Math.trunc(num(a));
|
|
2046
|
+
break;
|
|
2047
|
+
case "SQRT":
|
|
2048
|
+
out = Math.sqrt(num(a));
|
|
2049
|
+
break;
|
|
2050
|
+
case "ROUND": {
|
|
2051
|
+
const scale = 10 ** (vs.length > 1 ? integer(b, -15, 15) : 0);
|
|
2052
|
+
out = Math.round(num(a) * scale) / scale;
|
|
2053
|
+
break;
|
|
2054
|
+
}
|
|
2055
|
+
case "POWER":
|
|
2056
|
+
out = Math.pow(num(a), num(b));
|
|
2057
|
+
break;
|
|
2058
|
+
case "CLAMP":
|
|
2059
|
+
if (num(b) > num(c)) fail("Lower bound exceeds upper bound.");
|
|
2060
|
+
out = Math.max(num(b), Math.min(num(c), num(a)));
|
|
2061
|
+
break;
|
|
2062
|
+
case "LEAST":
|
|
2063
|
+
out = Math.min(...vs.map(num));
|
|
2064
|
+
break;
|
|
2065
|
+
case "GREATEST":
|
|
2066
|
+
out = Math.max(...vs.map(num));
|
|
2067
|
+
break;
|
|
2068
|
+
case "TO_TEXT":
|
|
2069
|
+
out = Array.isArray(a) ? JSON.stringify(a) : String(a);
|
|
2070
|
+
break;
|
|
2071
|
+
case "TO_NUMBER":
|
|
2072
|
+
if (typeof a !== "number" && (typeof a !== "string" || !/^[-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/.test(a.trim())))
|
|
2073
|
+
fail("Cannot convert to number.");
|
|
2074
|
+
out = Number(a);
|
|
2075
|
+
break;
|
|
2076
|
+
case "TO_BOOLEAN":
|
|
2077
|
+
if (a === true || a === false) out = a;
|
|
2078
|
+
else if (a === "true" || a === 1) out = true;
|
|
2079
|
+
else if (a === "false" || a === 0) out = false;
|
|
2080
|
+
else return fail("Cannot convert to boolean.");
|
|
2081
|
+
break;
|
|
2082
|
+
case "TO_DATETIME":
|
|
2083
|
+
out = date(a).toISOString();
|
|
2084
|
+
break;
|
|
2085
|
+
case "SPLIT":
|
|
2086
|
+
out = str(a).split(str(b));
|
|
2087
|
+
break;
|
|
2088
|
+
case "JOIN":
|
|
2089
|
+
out = arr(a).join(str(b));
|
|
2090
|
+
break;
|
|
2091
|
+
case "ARRAY_LENGTH":
|
|
2092
|
+
out = arr(a).length;
|
|
2093
|
+
break;
|
|
2094
|
+
case "ARRAY_CONTAINS":
|
|
2095
|
+
out = arr(a).includes(str(b));
|
|
2096
|
+
break;
|
|
2097
|
+
case "ARRAY_GET":
|
|
2098
|
+
out = arr(a)[integer(b, 1) - 1] ?? null;
|
|
2099
|
+
break;
|
|
2100
|
+
case "ARRAY_UNIQUE":
|
|
2101
|
+
out = [...new Set(arr(a))];
|
|
2102
|
+
break;
|
|
2103
|
+
case "ARRAY_SORT":
|
|
2104
|
+
out = [...arr(a)].sort((left, right) => {
|
|
2105
|
+
const l = Array.from(left), r = Array.from(right);
|
|
2106
|
+
for (let i = 0; i < Math.min(l.length, r.length); i++) {
|
|
2107
|
+
const delta = l[i].codePointAt(0) - r[i].codePointAt(0);
|
|
2108
|
+
if (delta) return delta;
|
|
2109
|
+
}
|
|
2110
|
+
return l.length - r.length;
|
|
2111
|
+
});
|
|
2112
|
+
break;
|
|
2113
|
+
case "YEAR":
|
|
2114
|
+
out = date(a).getUTCFullYear();
|
|
2115
|
+
break;
|
|
2116
|
+
case "MONTH":
|
|
2117
|
+
out = date(a).getUTCMonth() + 1;
|
|
2118
|
+
break;
|
|
2119
|
+
case "DAY":
|
|
2120
|
+
out = date(a).getUTCDate();
|
|
2121
|
+
break;
|
|
2122
|
+
case "HOUR":
|
|
2123
|
+
out = date(a).getUTCHours();
|
|
2124
|
+
break;
|
|
2125
|
+
case "WEEKDAY":
|
|
2126
|
+
out = (date(a).getUTCDay() + 6) % 7 + 1;
|
|
2127
|
+
break;
|
|
2128
|
+
case "DATE_TRUNC": {
|
|
2129
|
+
const d = date(b), unit = str(a).toLowerCase();
|
|
2130
|
+
if (![
|
|
2131
|
+
"year",
|
|
2132
|
+
"month",
|
|
2133
|
+
"week",
|
|
2134
|
+
"day",
|
|
2135
|
+
"hour",
|
|
2136
|
+
"minute",
|
|
2137
|
+
"second"
|
|
2138
|
+
].includes(unit))
|
|
2139
|
+
fail("Unsupported date unit.");
|
|
2140
|
+
d.setUTCMilliseconds(0);
|
|
2141
|
+
if (unit !== "second") d.setUTCSeconds(0);
|
|
2142
|
+
if (!["second", "minute"].includes(unit)) d.setUTCMinutes(0);
|
|
2143
|
+
if (["year", "month", "week", "day"].includes(unit)) d.setUTCHours(0);
|
|
2144
|
+
if (unit === "week")
|
|
2145
|
+
d.setUTCDate(d.getUTCDate() - (d.getUTCDay() + 6) % 7);
|
|
2146
|
+
if (unit === "year" || unit === "month") d.setUTCDate(1);
|
|
2147
|
+
if (unit === "year") d.setUTCMonth(0);
|
|
2148
|
+
out = d.toISOString();
|
|
2149
|
+
break;
|
|
2150
|
+
}
|
|
2151
|
+
case "DATE_ADD": {
|
|
2152
|
+
const d = date(c), unit = str(a).toLowerCase(), amount = integer(b, -1e5, 1e5);
|
|
2153
|
+
if (unit === "year" || unit === "month") {
|
|
2154
|
+
const day = d.getUTCDate();
|
|
2155
|
+
d.setUTCDate(1);
|
|
2156
|
+
d.setUTCMonth(d.getUTCMonth() + amount * (unit === "year" ? 12 : 1));
|
|
2157
|
+
const last = new Date(d.getTime());
|
|
2158
|
+
last.setUTCMonth(last.getUTCMonth() + 1, 0);
|
|
2159
|
+
d.setUTCDate(Math.min(day, last.getUTCDate()));
|
|
2160
|
+
} else {
|
|
2161
|
+
const units = {
|
|
2162
|
+
week: 6048e5,
|
|
2163
|
+
day: 864e5,
|
|
2164
|
+
hour: 36e5,
|
|
2165
|
+
minute: 6e4,
|
|
2166
|
+
second: 1e3,
|
|
2167
|
+
millisecond: 1
|
|
2168
|
+
};
|
|
2169
|
+
const ms = units[unit];
|
|
2170
|
+
if (!ms) fail("Unsupported date unit.");
|
|
2171
|
+
d.setTime(d.getTime() + amount * ms);
|
|
2172
|
+
}
|
|
2173
|
+
out = d.toISOString();
|
|
2174
|
+
break;
|
|
2175
|
+
}
|
|
2176
|
+
case "DATE_DIFF": {
|
|
2177
|
+
const units = {
|
|
2178
|
+
week: 6048e5,
|
|
2179
|
+
day: 864e5,
|
|
2180
|
+
hour: 36e5,
|
|
2181
|
+
minute: 6e4,
|
|
2182
|
+
second: 1e3,
|
|
2183
|
+
millisecond: 1
|
|
2184
|
+
};
|
|
2185
|
+
const ms = units[str(a).toLowerCase()];
|
|
2186
|
+
if (!ms) fail("Unsupported elapsed date unit.");
|
|
2187
|
+
out = Math.trunc((date(c).getTime() - date(b).getTime()) / ms);
|
|
2188
|
+
break;
|
|
2189
|
+
}
|
|
2190
|
+
case "FORMAT_DATE": {
|
|
2191
|
+
const d = date(a), pad = (n2) => String(n2).padStart(2, "0"), parts2 = {
|
|
2192
|
+
YYYY: String(d.getUTCFullYear()).padStart(4, "0"),
|
|
2193
|
+
MM: pad(d.getUTCMonth() + 1),
|
|
2194
|
+
DD: pad(d.getUTCDate()),
|
|
2195
|
+
HH: pad(d.getUTCHours()),
|
|
2196
|
+
mm: pad(d.getUTCMinutes()),
|
|
2197
|
+
ss: pad(d.getUTCSeconds())
|
|
2198
|
+
};
|
|
2199
|
+
out = str(b).replace(/YYYY|MM|DD|HH|mm|ss/g, (t) => parts2[t]);
|
|
2200
|
+
break;
|
|
2201
|
+
}
|
|
2202
|
+
default:
|
|
2203
|
+
return fail(`Unknown operation: ${name}`);
|
|
2204
|
+
}
|
|
2205
|
+
return bound(out);
|
|
2206
|
+
}
|
|
2207
|
+
try {
|
|
2208
|
+
const value = run(ast);
|
|
2209
|
+
return inspection ? { value, regex: inspection } : { value };
|
|
2210
|
+
} catch (e) {
|
|
2211
|
+
return {
|
|
2212
|
+
value: null,
|
|
2213
|
+
error: e instanceof Error ? e.message : "Formula evaluation failed."
|
|
2214
|
+
};
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
816
2217
|
export {
|
|
817
2218
|
AGG_OPS_BY_TYPE,
|
|
818
2219
|
AGG_OPS_NEEDING_FIELD,
|
|
2220
|
+
COMPUTED_PREFIX,
|
|
819
2221
|
EMPTY_QUERY,
|
|
2222
|
+
FORMULA_FUNCTIONS,
|
|
2223
|
+
FormulaError,
|
|
820
2224
|
MAX_AGGREGATIONS,
|
|
821
2225
|
MAX_GROUP_BY_FIELDS,
|
|
822
2226
|
MAX_ORDER_BY_TERMS,
|
|
@@ -833,27 +2237,41 @@ export {
|
|
|
833
2237
|
applyAggregations,
|
|
834
2238
|
applyQuery,
|
|
835
2239
|
coerceValue,
|
|
2240
|
+
compileFormula,
|
|
2241
|
+
computedFieldName,
|
|
836
2242
|
decodeQuery,
|
|
837
2243
|
encodeQuery,
|
|
838
2244
|
filterValues,
|
|
2245
|
+
formulaRuntime,
|
|
2246
|
+
groupPreview,
|
|
2247
|
+
httpComputedColumnStore,
|
|
839
2248
|
indexFields,
|
|
2249
|
+
isComputedCellError,
|
|
2250
|
+
isComputedField,
|
|
840
2251
|
isFilterable,
|
|
841
2252
|
isGroupable,
|
|
842
2253
|
isMeasurable,
|
|
2254
|
+
isNegativePredicate,
|
|
2255
|
+
isOrGroup,
|
|
843
2256
|
isPushdownFilter,
|
|
844
2257
|
isSelectable,
|
|
845
2258
|
isSortable,
|
|
846
2259
|
loadSchema,
|
|
847
2260
|
localStorageAdapter,
|
|
848
2261
|
matchesClause,
|
|
2262
|
+
memoryComputedColumnStore,
|
|
849
2263
|
memoryStorageAdapter,
|
|
2264
|
+
negateClause,
|
|
850
2265
|
normalizeQueryState,
|
|
851
2266
|
opAllowedForType,
|
|
2267
|
+
opPairsForField,
|
|
852
2268
|
opsForField,
|
|
2269
|
+
predicatesOf,
|
|
853
2270
|
queriesEqual,
|
|
854
2271
|
readFieldValue,
|
|
855
2272
|
selectedFields,
|
|
856
2273
|
toAggregationQuery,
|
|
857
|
-
toServerQuery
|
|
2274
|
+
toServerQuery,
|
|
2275
|
+
validateComputedColumn
|
|
858
2276
|
};
|
|
859
2277
|
//# sourceMappingURL=index.js.map
|