@pythia-software/query-table-core 0.4.2 → 0.6.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 +56 -0
- package/dist/index.d.ts +553 -249
- package/dist/index.js +1855 -410
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,3 +1,145 @@
|
|
|
1
|
+
// src/metricNormalize.ts
|
|
2
|
+
function normalizeMetricOptions(raw) {
|
|
3
|
+
const out = {}, diagnostics = [];
|
|
4
|
+
const record = (x) => !!x && typeof x === "object" && !Array.isArray(x);
|
|
5
|
+
const copy = (input, specs) => {
|
|
6
|
+
const result = {};
|
|
7
|
+
for (const [key, spec] of Object.entries(specs)) {
|
|
8
|
+
const v = input[key];
|
|
9
|
+
if (v === void 0) continue;
|
|
10
|
+
if (spec === "string" ? typeof v === "string" && v.length <= 1e4 : spec === "number" ? typeof v === "number" && Number.isFinite(v) : spec === "boolean" ? typeof v === "boolean" : spec === "integer" ? typeof v === "number" && Number.isSafeInteger(v) && v >= 0 && v <= 1e6 : typeof v === "string" && spec.includes(v))
|
|
11
|
+
result[key] = v;
|
|
12
|
+
else diagnostics.push(`Invalid metric ${key}.`);
|
|
13
|
+
}
|
|
14
|
+
return result;
|
|
15
|
+
};
|
|
16
|
+
Object.assign(
|
|
17
|
+
out,
|
|
18
|
+
copy(raw, {
|
|
19
|
+
expression: "string",
|
|
20
|
+
expressionY: "string",
|
|
21
|
+
scope: ["allMatching", "shownRows"],
|
|
22
|
+
groupLimit: "integer"
|
|
23
|
+
})
|
|
24
|
+
);
|
|
25
|
+
if (raw.display !== void 0) {
|
|
26
|
+
if (record(raw.display)) {
|
|
27
|
+
if (typeof raw.display.kind !== "string")
|
|
28
|
+
diagnostics.push("Invalid metric display kind.");
|
|
29
|
+
const d = copy(raw.display, {
|
|
30
|
+
kind: [
|
|
31
|
+
"auto",
|
|
32
|
+
"value",
|
|
33
|
+
"table",
|
|
34
|
+
"list",
|
|
35
|
+
"bar-horizontal",
|
|
36
|
+
"bar-vertical",
|
|
37
|
+
"line",
|
|
38
|
+
"pie",
|
|
39
|
+
"donut",
|
|
40
|
+
"scatter",
|
|
41
|
+
"box",
|
|
42
|
+
"histogram"
|
|
43
|
+
],
|
|
44
|
+
valueLabel: "string",
|
|
45
|
+
xLabel: "string",
|
|
46
|
+
yLabel: "string",
|
|
47
|
+
legendPosition: ["left", "right", "top", "bottom", "none"],
|
|
48
|
+
boxMean: "boolean"
|
|
49
|
+
});
|
|
50
|
+
for (const k of ["xScale", "yScale"]) {
|
|
51
|
+
if (record(raw.display[k]))
|
|
52
|
+
d[k] = copy(raw.display[k], {
|
|
53
|
+
mode: ["linear", "log"],
|
|
54
|
+
min: "number",
|
|
55
|
+
max: "number"
|
|
56
|
+
});
|
|
57
|
+
else if (raw.display[k] !== void 0)
|
|
58
|
+
diagnostics.push(`Invalid metric ${k}.`);
|
|
59
|
+
}
|
|
60
|
+
for (const k of ["format", "xFormat", "yFormat"])
|
|
61
|
+
if (record(raw.display[k]))
|
|
62
|
+
d[k] = copy(raw.display[k], {
|
|
63
|
+
kind: ["number", "percent", "duration", "date", "time", "datetime"],
|
|
64
|
+
decimals: "integer",
|
|
65
|
+
sourceUnit: [
|
|
66
|
+
"milliseconds",
|
|
67
|
+
"seconds",
|
|
68
|
+
"minutes",
|
|
69
|
+
"hours",
|
|
70
|
+
"days",
|
|
71
|
+
"iso",
|
|
72
|
+
"epochSeconds",
|
|
73
|
+
"epochMilliseconds",
|
|
74
|
+
"secondsOfDay",
|
|
75
|
+
"millisecondsOfDay"
|
|
76
|
+
],
|
|
77
|
+
style: ["human", "long", "clock", "custom", "iso"],
|
|
78
|
+
pattern: "string",
|
|
79
|
+
timeZone: "string"
|
|
80
|
+
});
|
|
81
|
+
if (record(raw.display.list))
|
|
82
|
+
d.list = copy(raw.display.list, {
|
|
83
|
+
showBars: "boolean",
|
|
84
|
+
useGroupColors: "boolean",
|
|
85
|
+
showValues: "boolean"
|
|
86
|
+
});
|
|
87
|
+
if (record(raw.display.pivot))
|
|
88
|
+
d.pivot = copy(raw.display.pivot, {
|
|
89
|
+
swap: "boolean",
|
|
90
|
+
rowDir: ["asc", "desc"],
|
|
91
|
+
columnDir: ["asc", "desc"]
|
|
92
|
+
});
|
|
93
|
+
out.display = { kind: "auto", ...d };
|
|
94
|
+
} else diagnostics.push("Invalid metric display.");
|
|
95
|
+
}
|
|
96
|
+
if (raw.distribution !== void 0) {
|
|
97
|
+
if (record(raw.distribution))
|
|
98
|
+
out.distribution = copy(raw.distribution, {
|
|
99
|
+
kind: ["box", "histogram"],
|
|
100
|
+
input: "string",
|
|
101
|
+
whiskers: ["minmax", "tukey"],
|
|
102
|
+
bins: "integer"
|
|
103
|
+
});
|
|
104
|
+
else diagnostics.push("Invalid metric distribution.");
|
|
105
|
+
}
|
|
106
|
+
if (record(raw.layout)) {
|
|
107
|
+
const l = copy(raw.layout, {
|
|
108
|
+
widthRem: "integer",
|
|
109
|
+
heightRem: "integer",
|
|
110
|
+
minWidthRem: "integer",
|
|
111
|
+
minHeightRem: "integer"
|
|
112
|
+
});
|
|
113
|
+
for (const k of ["widthRem", "heightRem", "minWidthRem", "minHeightRem"])
|
|
114
|
+
l[k] = Math.max(1, Math.min(1e3, Number(l[k] ?? 1)));
|
|
115
|
+
l.widthRem = Math.max(Number(l.widthRem), Number(l.minWidthRem));
|
|
116
|
+
l.heightRem = Math.max(Number(l.heightRem), Number(l.minHeightRem));
|
|
117
|
+
out.layout = l;
|
|
118
|
+
}
|
|
119
|
+
if (raw.sort !== void 0 && !Array.isArray(raw.sort))
|
|
120
|
+
diagnostics.push("Invalid metric sort.");
|
|
121
|
+
if (Array.isArray(raw.sort))
|
|
122
|
+
out.sort = raw.sort.slice(0, 20).flatMap((s) => {
|
|
123
|
+
if (!record(s) || typeof s.key !== "string" || !/^(value|y|count|samples|group(?:0|[1-9][0-9]?))$/.test(s.key) || !["asc", "desc"].includes(String(s.dir))) {
|
|
124
|
+
diagnostics.push("Invalid metric sort.");
|
|
125
|
+
return [];
|
|
126
|
+
}
|
|
127
|
+
return [
|
|
128
|
+
copy(s, {
|
|
129
|
+
key: [s.key],
|
|
130
|
+
dir: ["asc", "desc"],
|
|
131
|
+
nulls: ["first", "last"]
|
|
132
|
+
})
|
|
133
|
+
];
|
|
134
|
+
});
|
|
135
|
+
if (Array.isArray(raw.diagnostics))
|
|
136
|
+
diagnostics.push(
|
|
137
|
+
...raw.diagnostics.filter((s) => typeof s === "string" && s.length <= 1e3).slice(0, 20)
|
|
138
|
+
);
|
|
139
|
+
if (diagnostics.length) out.diagnostics = [...new Set(diagnostics)];
|
|
140
|
+
return out;
|
|
141
|
+
}
|
|
142
|
+
|
|
1
143
|
// src/computed.ts
|
|
2
144
|
var COMPUTED_PREFIX = "@computed/";
|
|
3
145
|
var computedFieldName = (id) => COMPUTED_PREFIX + id;
|
|
@@ -179,6 +321,9 @@ var FILTER_OPS = /* @__PURE__ */ new Set([
|
|
|
179
321
|
"ends_with",
|
|
180
322
|
"matches_regex",
|
|
181
323
|
"not_matches_regex",
|
|
324
|
+
"length_gt",
|
|
325
|
+
"length_lt",
|
|
326
|
+
"length_eq",
|
|
182
327
|
"includes",
|
|
183
328
|
"is_null",
|
|
184
329
|
"is_not_null"
|
|
@@ -206,7 +351,16 @@ function normalizeWhereClause(item) {
|
|
|
206
351
|
return clause;
|
|
207
352
|
}
|
|
208
353
|
function cloneWhereTerm(term) {
|
|
209
|
-
|
|
354
|
+
const copy = isOrGroup(term) ? { ...term, any: term.any.map((c) => ({ ...c })) } : { ...term };
|
|
355
|
+
if (term.setFilter) copy.setFilter = { ...term.setFilter, values: [...term.setFilter.values] };
|
|
356
|
+
return copy;
|
|
357
|
+
}
|
|
358
|
+
function normalizeSetFilter(item) {
|
|
359
|
+
if (!isRecord(item)) return void 0;
|
|
360
|
+
const id = boundedString(item.id, MAX_FIELD_NAME_LENGTH);
|
|
361
|
+
if (!id || !["any", "all", "none", "empty"].includes(String(item.mode)) || !Array.isArray(item.values)) return void 0;
|
|
362
|
+
if (item.values.length > MAX_WHERE_CLAUSES || !item.values.every((value) => typeof value === "string" && value.length <= MAX_FILTER_VALUE_LENGTH)) return void 0;
|
|
363
|
+
return { id, mode: item.mode, values: [...item.values] };
|
|
210
364
|
}
|
|
211
365
|
function normalizeQueryState(input, fallback = EMPTY_QUERY) {
|
|
212
366
|
const raw = isRecord(input) ? input : {};
|
|
@@ -230,6 +384,7 @@ function normalizeQueryState(input, fallback = EMPTY_QUERY) {
|
|
|
230
384
|
let literalBudget = MAX_WHERE_CLAUSES;
|
|
231
385
|
for (const item of raw.where) {
|
|
232
386
|
if (literalBudget <= 0) break;
|
|
387
|
+
const start = where.length;
|
|
233
388
|
if (isRecord(item) && Array.isArray(item.any)) {
|
|
234
389
|
const members = [];
|
|
235
390
|
for (const inner of item.any) {
|
|
@@ -249,6 +404,10 @@ function normalizeQueryState(input, fallback = EMPTY_QUERY) {
|
|
|
249
404
|
literalBudget--;
|
|
250
405
|
}
|
|
251
406
|
}
|
|
407
|
+
if (where.length > start && isRecord(item)) {
|
|
408
|
+
const metadata = normalizeSetFilter(item.setFilter);
|
|
409
|
+
if (metadata) where[start].setFilter = metadata;
|
|
410
|
+
}
|
|
252
411
|
}
|
|
253
412
|
} else {
|
|
254
413
|
where.push(...fallback.where.map(cloneWhereTerm));
|
|
@@ -258,7 +417,7 @@ function normalizeQueryState(input, fallback = EMPTY_QUERY) {
|
|
|
258
417
|
for (const item of raw.orderBy.slice(0, MAX_ORDER_BY_TERMS)) {
|
|
259
418
|
if (!isRecord(item)) continue;
|
|
260
419
|
const field = boundedString(item.field, MAX_FIELD_NAME_LENGTH);
|
|
261
|
-
if (!field ||
|
|
420
|
+
if (!field || item.dir !== "asc" && item.dir !== "desc") continue;
|
|
262
421
|
const term = { field, dir: item.dir };
|
|
263
422
|
if (item.nulls === "first" || item.nulls === "last") term.nulls = item.nulls;
|
|
264
423
|
if (isRecord(item.extract) && typeof item.extract.regex === "string" && item.extract.regex.length <= MAX_FILTER_VALUE_LENGTH) {
|
|
@@ -283,19 +442,26 @@ function normalizeQueryState(input, fallback = EMPTY_QUERY) {
|
|
|
283
442
|
for (const item of raw.aggregations.slice(0, MAX_AGGREGATIONS)) {
|
|
284
443
|
if (!isRecord(item)) continue;
|
|
285
444
|
const id = boundedString(item.id, MAX_FIELD_NAME_LENGTH);
|
|
286
|
-
if (!id
|
|
287
|
-
const
|
|
288
|
-
const
|
|
445
|
+
if (!id) continue;
|
|
446
|
+
const validOp = typeof item.op === "string" && AGG_OPS.has(item.op);
|
|
447
|
+
const rawGroups = Array.isArray(item.groupBy) ? item.groupBy : [];
|
|
448
|
+
const groupBy = rawGroups.slice(0, MAX_GROUP_BY_FIELDS).map((field2) => boundedString(field2, MAX_FIELD_NAME_LENGTH)).filter((field2) => field2 != null);
|
|
449
|
+
const aggregation = { id, op: validOp ? item.op : "count", groupBy };
|
|
289
450
|
const field = boundedString(item.field, MAX_FIELD_NAME_LENGTH);
|
|
290
451
|
const label = boundedString(item.label, MAX_LABEL_LENGTH);
|
|
291
|
-
if (field && isComputedField(field) || groupBy.some(isComputedField)) continue;
|
|
292
452
|
if (field) aggregation.field = field;
|
|
293
453
|
if (label) aggregation.label = label;
|
|
454
|
+
Object.assign(aggregation, normalizeMetricOptions(item));
|
|
455
|
+
if (!Array.isArray(item.groupBy) || rawGroups.length > MAX_GROUP_BY_FIELDS || groupBy.length !== rawGroups.length) aggregation.diagnostics = [...aggregation.diagnostics ?? [], "Invalid metric grouping fields or grouping budget exceeded."];
|
|
456
|
+
const unsupportedOp = boundedString(item.unsupportedOp ?? (!validOp ? item.op : void 0), MAX_FIELD_NAME_LENGTH);
|
|
457
|
+
if (unsupportedOp) aggregation.unsupportedOp = unsupportedOp;
|
|
458
|
+
if (item.field !== void 0 && !field) aggregation.diagnostics = [...aggregation.diagnostics ?? [], "Invalid metric measure field."];
|
|
459
|
+
if (!validOp && typeof item.expression !== "string") aggregation.diagnostics = [...aggregation.diagnostics ?? [], "Unsupported aggregate operation."];
|
|
294
460
|
aggregations.push(aggregation);
|
|
295
461
|
}
|
|
296
462
|
if (aggregations.length > 0) out.aggregations = aggregations;
|
|
297
463
|
} else if (fallback.aggregations?.length) {
|
|
298
|
-
out.aggregations =
|
|
464
|
+
out.aggregations = normalizeQueryState({ aggregations: fallback.aggregations }).aggregations;
|
|
299
465
|
}
|
|
300
466
|
return out;
|
|
301
467
|
}
|
|
@@ -325,7 +491,7 @@ function isPushdownFilter(f2) {
|
|
|
325
491
|
return isFilterable(f2) && (f2.filter?.pushdown ?? f2.source.kind === "backend");
|
|
326
492
|
}
|
|
327
493
|
function isSortable(f2) {
|
|
328
|
-
if (f2.source.kind === "derived" && f2.source.computedId) return
|
|
494
|
+
if (f2.source.kind === "derived" && f2.source.computedId) return f2.sort?.enabled === true;
|
|
329
495
|
return f2.sort?.enabled ?? f2.source.kind === "backend";
|
|
330
496
|
}
|
|
331
497
|
function isSelectable(f2) {
|
|
@@ -472,6 +638,34 @@ function normalizeAliases(rawAliases, rawAlias) {
|
|
|
472
638
|
return aliases;
|
|
473
639
|
}
|
|
474
640
|
|
|
641
|
+
// src/relativeTime.ts
|
|
642
|
+
var UNIT_MS = { ms: 1, s: 1e3, m: 6e4, h: 36e5, d: 864e5, w: 6048e5 };
|
|
643
|
+
var MAX_RELATIVE_TIME_MS = 864e13;
|
|
644
|
+
function parseRelativeDuration(value) {
|
|
645
|
+
const valid = /^[+-](?:[0-9]+(?:ms|s|m|h|d|w))+$/.exec(value);
|
|
646
|
+
if (!valid || valid[0] !== value) return null;
|
|
647
|
+
const body = value.slice(1);
|
|
648
|
+
const components = /([0-9]+)(ms|s|m|h|d|w)/g;
|
|
649
|
+
let total = 0;
|
|
650
|
+
for (const match of body.matchAll(components)) {
|
|
651
|
+
const amount = Number(match[1]);
|
|
652
|
+
const unit = UNIT_MS[match[2]];
|
|
653
|
+
if (!Number.isSafeInteger(amount) || amount > Math.floor((MAX_RELATIVE_TIME_MS - total) / unit)) return null;
|
|
654
|
+
total += amount * unit;
|
|
655
|
+
}
|
|
656
|
+
return value[0] === "-" && total !== 0 ? -total : total;
|
|
657
|
+
}
|
|
658
|
+
function evaluationTime(options) {
|
|
659
|
+
const now = options.now ?? Date.now();
|
|
660
|
+
if (!Number.isSafeInteger(now) || Math.abs(now) > MAX_RELATIVE_TIME_MS) throw new Error("Invalid query evaluation time");
|
|
661
|
+
return now;
|
|
662
|
+
}
|
|
663
|
+
function relativeTimestamp(now, offsetMs) {
|
|
664
|
+
const result = now + offsetMs;
|
|
665
|
+
if (!Number.isSafeInteger(result) || Math.abs(result) > MAX_RELATIVE_TIME_MS) throw new Error("Relative time exceeds datetime limits");
|
|
666
|
+
return result;
|
|
667
|
+
}
|
|
668
|
+
|
|
475
669
|
// src/ops.ts
|
|
476
670
|
var NULLITY = ["is_null", "is_not_null"];
|
|
477
671
|
var COMPLEMENT_OP = {
|
|
@@ -487,6 +681,12 @@ var COMPLEMENT_OP = {
|
|
|
487
681
|
not_matches_regex: "matches_regex"
|
|
488
682
|
};
|
|
489
683
|
function negateClause(clause, allowedOps) {
|
|
684
|
+
if (clause.value === "0" && !clause.negated) {
|
|
685
|
+
const zeroComplement = clause.op === "length_gt" ? "length_eq" : clause.op === "length_eq" ? "length_gt" : null;
|
|
686
|
+
if (zeroComplement && (!allowedOps || allowedOps.includes(zeroComplement))) {
|
|
687
|
+
return { field: clause.field, op: zeroComplement, value: "0" };
|
|
688
|
+
}
|
|
689
|
+
}
|
|
490
690
|
const complement = COMPLEMENT_OP[clause.op];
|
|
491
691
|
if (complement && (!allowedOps || allowedOps.includes(complement))) {
|
|
492
692
|
const next2 = { field: clause.field, op: complement, value: clause.value };
|
|
@@ -499,7 +699,7 @@ function negateClause(clause, allowedOps) {
|
|
|
499
699
|
function isNegativePredicate(clause) {
|
|
500
700
|
return Boolean(clause.negated) || clause.op === "!=" || clause.op === "not_matches_regex" || clause.op === "is_not_null";
|
|
501
701
|
}
|
|
502
|
-
var POSITIVE_OP_ORDER = ["=", ">", ">=", "contains", "starts_with", "ends_with", "matches_regex", "includes"];
|
|
702
|
+
var POSITIVE_OP_ORDER = ["=", ">", ">=", "contains", "starts_with", "ends_with", "length_gt", "length_lt", "length_eq", "matches_regex", "includes"];
|
|
503
703
|
function opPairsForField(field) {
|
|
504
704
|
const ops = opsForField(field);
|
|
505
705
|
const allow = new Set(ops);
|
|
@@ -523,6 +723,9 @@ var OPS_BY_TYPE = {
|
|
|
523
723
|
"ends_with",
|
|
524
724
|
"matches_regex",
|
|
525
725
|
"not_matches_regex",
|
|
726
|
+
"length_gt",
|
|
727
|
+
"length_lt",
|
|
728
|
+
"length_eq",
|
|
526
729
|
...NULLITY
|
|
527
730
|
],
|
|
528
731
|
enum: ["=", "!=", ...NULLITY],
|
|
@@ -538,7 +741,7 @@ function opsForField(field) {
|
|
|
538
741
|
function opAllowedForType(type, op) {
|
|
539
742
|
return OPS_BY_TYPE[type].includes(op);
|
|
540
743
|
}
|
|
541
|
-
function coerceValue(type, raw) {
|
|
744
|
+
function coerceValue(type, raw, now) {
|
|
542
745
|
switch (type) {
|
|
543
746
|
case "number": {
|
|
544
747
|
const n = Number(raw);
|
|
@@ -552,6 +755,11 @@ function coerceValue(type, raw) {
|
|
|
552
755
|
throw new Error(`not a bool: ${JSON.stringify(raw)}`);
|
|
553
756
|
}
|
|
554
757
|
case "datetime": {
|
|
758
|
+
if (raw.startsWith("+") || raw.startsWith("-")) {
|
|
759
|
+
const offset = parseRelativeDuration(raw);
|
|
760
|
+
if (offset === null) throw new Error(`not a signed duration: ${JSON.stringify(raw)}`);
|
|
761
|
+
return relativeTimestamp(now ?? Date.now(), offset);
|
|
762
|
+
}
|
|
555
763
|
const ms = Date.parse(raw);
|
|
556
764
|
if (Number.isNaN(ms)) throw new Error(`not a datetime: ${JSON.stringify(raw)}`);
|
|
557
765
|
return ms;
|
|
@@ -587,16 +795,63 @@ function aggOpAllowedForType(type, op) {
|
|
|
587
795
|
return AGG_OPS_BY_TYPE[type].includes(op);
|
|
588
796
|
}
|
|
589
797
|
function isMeasurable(field) {
|
|
798
|
+
if (field.source.kind === "derived" && field.source.computedId) return field.aggregate?.measure === true;
|
|
590
799
|
if (field.source.kind !== "backend") return false;
|
|
591
800
|
if (field.aggregate?.measure != null) return field.aggregate.measure;
|
|
592
801
|
return aggOpsForField(field).length > 0;
|
|
593
802
|
}
|
|
594
803
|
function isGroupable(field) {
|
|
804
|
+
if (field.source.kind === "derived" && field.source.computedId) return field.aggregate?.groupable === true;
|
|
595
805
|
if (field.source.kind !== "backend") return false;
|
|
596
806
|
if (field.aggregate?.groupable != null) return field.aggregate.groupable;
|
|
597
807
|
return field.type === "enum" || field.type === "text" || field.type === "bool";
|
|
598
808
|
}
|
|
599
809
|
|
|
810
|
+
// src/setFilter.ts
|
|
811
|
+
function createSetFilter(field, mode, values, id) {
|
|
812
|
+
const keys = [...new Set(values)];
|
|
813
|
+
if (mode !== "empty" && keys.length === 0) return [];
|
|
814
|
+
const includes = keys.map((value) => ({ field, op: "includes", value }));
|
|
815
|
+
let terms;
|
|
816
|
+
if (mode === "empty") terms = [{ field, op: "is_null", value: "" }];
|
|
817
|
+
else if (mode === "any") terms = includes.length === 1 ? includes : [{ any: includes }];
|
|
818
|
+
else if (mode === "all") terms = includes;
|
|
819
|
+
else terms = includes.map((clause) => ({ any: [{ field, op: "is_null", value: "" }, { ...clause, negated: true }] }));
|
|
820
|
+
return terms.map((term) => ({ ...term, setFilter: { id, mode, values: mode === "empty" ? [] : [...keys] } }));
|
|
821
|
+
}
|
|
822
|
+
function stripSetFilter(term) {
|
|
823
|
+
const strip = ({ setFilter: _metadata, ...clause }) => clause;
|
|
824
|
+
return isOrGroup(term) ? { any: term.any.map(strip) } : strip(term);
|
|
825
|
+
}
|
|
826
|
+
function readSetFilter(where, index) {
|
|
827
|
+
const term = where[index];
|
|
828
|
+
const metadata = term?.setFilter;
|
|
829
|
+
if (!term || !metadata || where[index - 1]?.setFilter?.id === metadata.id) return null;
|
|
830
|
+
const field = predicatesOf(term)[0]?.field;
|
|
831
|
+
if (!field) return null;
|
|
832
|
+
const expected = createSetFilter(field, metadata.mode, metadata.values, metadata.id);
|
|
833
|
+
if (expected.length === 0) return null;
|
|
834
|
+
for (let offset = 0; offset < expected.length; offset++) {
|
|
835
|
+
const actual = where[index + offset];
|
|
836
|
+
if (!actual || JSON.stringify(actual.setFilter) !== JSON.stringify(expected[offset].setFilter) || JSON.stringify(stripSetFilter(actual)) !== JSON.stringify(stripSetFilter(expected[offset]))) return null;
|
|
837
|
+
}
|
|
838
|
+
if (where[index + expected.length]?.setFilter?.id === metadata.id) return null;
|
|
839
|
+
return { field, metadata, count: expected.length };
|
|
840
|
+
}
|
|
841
|
+
function mapSetFilterValues(where, canonicalize) {
|
|
842
|
+
const result = [];
|
|
843
|
+
for (let index = 0; index < where.length; index++) {
|
|
844
|
+
const set = readSetFilter(where, index);
|
|
845
|
+
if (!set) {
|
|
846
|
+
result.push(where[index]);
|
|
847
|
+
continue;
|
|
848
|
+
}
|
|
849
|
+
result.push(...createSetFilter(set.field, set.metadata.mode, set.metadata.values.map((value) => canonicalize(set.field, value)), set.metadata.id));
|
|
850
|
+
index += set.count - 1;
|
|
851
|
+
}
|
|
852
|
+
return result;
|
|
853
|
+
}
|
|
854
|
+
|
|
600
855
|
// src/encode.ts
|
|
601
856
|
function toBase64Url(json) {
|
|
602
857
|
let b64;
|
|
@@ -667,7 +922,7 @@ function toServerQuery(q, schema) {
|
|
|
667
922
|
q = normalizeQueryState(q);
|
|
668
923
|
const byName = indexFields(schema);
|
|
669
924
|
const resolveField2 = (name) => resolveFieldName(schema, name) ?? name;
|
|
670
|
-
const where = q.where.filter((term) => termIsPushdown(term, byName, resolveField2));
|
|
925
|
+
const where = q.where.filter((term) => termIsPushdown(term, byName, resolveField2)).map(stripSetFilter);
|
|
671
926
|
const orderBy = [];
|
|
672
927
|
for (const term of q.orderBy) {
|
|
673
928
|
const field = resolveField2(term.field);
|
|
@@ -702,10 +957,12 @@ function termIsPushdown(term, byName, resolveField2) {
|
|
|
702
957
|
});
|
|
703
958
|
}
|
|
704
959
|
function toAggregationQuery(q, schema) {
|
|
960
|
+
const suppliedPredicateCount = q.where.reduce((n, t) => n + predicatesOf(t).length, 0);
|
|
705
961
|
q = normalizeQueryState(q);
|
|
962
|
+
if (q.aggregations?.some((a) => a.diagnostics?.length || a.expression !== void 0 || a.expressionY !== void 0 || a.distribution || a.scope === "shownRows" || a.sort?.length || a.groupLimit)) throw new Error("Metric definition requires the version 2 metric transport.");
|
|
706
963
|
const byName = indexFields(schema);
|
|
707
964
|
const resolveField2 = (name) => resolveFieldName(schema, name) ?? name;
|
|
708
|
-
const where = q.where.filter((term) => termIsPushdown(term, byName, resolveField2));
|
|
965
|
+
const where = q.where.filter((term) => termIsPushdown(term, byName, resolveField2)).map(stripSetFilter);
|
|
709
966
|
const isBackend = (name) => {
|
|
710
967
|
if (name == null) return true;
|
|
711
968
|
const field = resolveField2(name);
|
|
@@ -715,401 +972,10 @@ function toAggregationQuery(q, schema) {
|
|
|
715
972
|
const aggregations = (q.aggregations ?? []).filter(
|
|
716
973
|
(a) => isBackend(a.field) && a.groupBy.every((g) => isBackend(g))
|
|
717
974
|
);
|
|
718
|
-
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
function applyQuery(rows, q, schema) {
|
|
723
|
-
const byName = indexFields(schema);
|
|
724
|
-
const resolveField2 = (name) => resolveFieldName(schema, name) ?? name;
|
|
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)));
|
|
728
|
-
const total = out.length;
|
|
729
|
-
if (orderBy.length) {
|
|
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
|
-
);
|
|
738
|
-
out = [...out].sort((a, b) => {
|
|
739
|
-
for (const t of terms) {
|
|
740
|
-
const av = extractSortValue(readFieldValue(t.field, a), t.extract);
|
|
741
|
-
const bv = extractSortValue(readFieldValue(t.field, b), t.extract);
|
|
742
|
-
const aNull = av == null;
|
|
743
|
-
const bNull = bv == null;
|
|
744
|
-
if (aNull || bNull) {
|
|
745
|
-
if (aNull && bNull) continue;
|
|
746
|
-
return (aNull ? 1 : -1) * (t.nullsLast ? 1 : -1);
|
|
747
|
-
}
|
|
748
|
-
const cmp = compare(av, bv);
|
|
749
|
-
if (cmp !== 0) return t.dir === "asc" ? cmp : -cmp;
|
|
750
|
-
}
|
|
751
|
-
return 0;
|
|
752
|
-
});
|
|
753
|
-
}
|
|
754
|
-
const start = q.offset > 0 ? q.offset : 0;
|
|
755
|
-
const end = q.limit > 0 ? start + q.limit : out.length;
|
|
756
|
-
return { rows: out.slice(start, end), total };
|
|
757
|
-
}
|
|
758
|
-
function matchesClause(row, clause, schema) {
|
|
759
|
-
return matchesWith(indexFields(schema), row, prepareWhereClause(clause));
|
|
760
|
-
}
|
|
761
|
-
function applyAggregations(rows, q, schema) {
|
|
762
|
-
const byName = indexFields(schema);
|
|
763
|
-
const resolveField2 = (name) => resolveFieldName(schema, name) ?? name;
|
|
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) => ({
|
|
767
|
-
id: agg.id,
|
|
768
|
-
buckets: computeBuckets(filtered, agg, byName, resolveField2)
|
|
769
|
-
}));
|
|
770
|
-
return { metrics };
|
|
771
|
-
}
|
|
772
|
-
function computeBuckets(rows, agg, byName, resolveField2) {
|
|
773
|
-
const groupFields = agg.groupBy.map((n) => byName.get(resolveField2(n)));
|
|
774
|
-
const measure = agg.field ? byName.get(resolveField2(agg.field)) : void 0;
|
|
775
|
-
const groups = /* @__PURE__ */ new Map();
|
|
776
|
-
for (const row of rows) {
|
|
777
|
-
const keys = groupFields.map((f2) => {
|
|
778
|
-
if (!f2) return null;
|
|
779
|
-
const v = readFieldValue(f2, row);
|
|
780
|
-
return v == null || v === "" ? null : String(v);
|
|
781
|
-
});
|
|
782
|
-
const k = JSON.stringify(keys);
|
|
783
|
-
let g = groups.get(k);
|
|
784
|
-
if (!g) {
|
|
785
|
-
g = { keys, rows: [] };
|
|
786
|
-
groups.set(k, g);
|
|
787
|
-
}
|
|
788
|
-
g.rows.push(row);
|
|
789
|
-
}
|
|
790
|
-
const buckets = [...groups.values()].map((g) => ({
|
|
791
|
-
keys: g.keys,
|
|
792
|
-
count: g.rows.length,
|
|
793
|
-
value: aggValue(agg.op, measure, g.rows)
|
|
794
|
-
}));
|
|
795
|
-
return sortBuckets(buckets);
|
|
796
|
-
}
|
|
797
|
-
function aggValue(op, measure, rows) {
|
|
798
|
-
if (op === "count") {
|
|
799
|
-
if (!measure) return rows.length;
|
|
800
|
-
let n = 0;
|
|
801
|
-
for (const r of rows) if (readFieldValue(measure, r) != null) n++;
|
|
802
|
-
return n;
|
|
803
|
-
}
|
|
804
|
-
if (!measure) return null;
|
|
805
|
-
const values = rows.map((r) => readFieldValue(measure, r)).filter((v) => v != null);
|
|
806
|
-
switch (op) {
|
|
807
|
-
case "count_distinct":
|
|
808
|
-
return new Set(values.map((v) => String(v))).size;
|
|
809
|
-
case "sum":
|
|
810
|
-
case "avg": {
|
|
811
|
-
let sum = 0;
|
|
812
|
-
let n = 0;
|
|
813
|
-
for (const v of values) {
|
|
814
|
-
const num = typeof v === "number" ? v : Number(v);
|
|
815
|
-
if (Number.isFinite(num)) {
|
|
816
|
-
sum += num;
|
|
817
|
-
n++;
|
|
818
|
-
}
|
|
819
|
-
}
|
|
820
|
-
if (op === "avg") return n ? sum / n : null;
|
|
821
|
-
return n ? sum : null;
|
|
822
|
-
}
|
|
823
|
-
case "min":
|
|
824
|
-
case "max": {
|
|
825
|
-
let best;
|
|
826
|
-
for (const v of values) {
|
|
827
|
-
if (best === void 0) {
|
|
828
|
-
best = v;
|
|
829
|
-
continue;
|
|
830
|
-
}
|
|
831
|
-
const cmp = compare(v, best);
|
|
832
|
-
if (op === "min" ? cmp < 0 : cmp > 0) best = v;
|
|
833
|
-
}
|
|
834
|
-
return best ?? null;
|
|
835
|
-
}
|
|
836
|
-
default:
|
|
837
|
-
return null;
|
|
838
|
-
}
|
|
839
|
-
}
|
|
840
|
-
function sortBuckets(buckets) {
|
|
841
|
-
return [...buckets].sort((a, b) => {
|
|
842
|
-
const av = typeof a.value === "number" ? a.value : a.count;
|
|
843
|
-
const bv = typeof b.value === "number" ? b.value : b.count;
|
|
844
|
-
if (av !== bv) return bv - av;
|
|
845
|
-
return JSON.stringify(a.keys).localeCompare(JSON.stringify(b.keys));
|
|
846
|
-
});
|
|
847
|
-
}
|
|
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;
|
|
864
|
-
const field = byName.get(clause.field);
|
|
865
|
-
if (!field) return true;
|
|
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;
|
|
877
|
-
if (clause.op === "is_null") return Array.isArray(v) ? v.length === 0 : v == null || v === "";
|
|
878
|
-
if (clause.op === "is_not_null") return Array.isArray(v) ? v.length > 0 : v != null && v !== "";
|
|
879
|
-
const arrayText = (value) => field.filter?.arrayCaseSensitive ? String(value) : String(value).toLowerCase();
|
|
880
|
-
if (clause.op === "includes") {
|
|
881
|
-
const arr = Array.isArray(v) ? v : [];
|
|
882
|
-
const needle = arrayText(clause.value);
|
|
883
|
-
return arr.some((x) => arrayText(x) === needle);
|
|
884
|
-
}
|
|
885
|
-
if (clause.op === "matches_regex" || clause.op === "not_matches_regex") {
|
|
886
|
-
if (v == null || Array.isArray(v)) return false;
|
|
887
|
-
if (!regex) return false;
|
|
888
|
-
const matches = regex.test(String(v));
|
|
889
|
-
return clause.op === "matches_regex" ? matches : !matches;
|
|
890
|
-
}
|
|
891
|
-
if (Array.isArray(v)) {
|
|
892
|
-
const needle = arrayText(clause.value);
|
|
893
|
-
const hay = v.map(arrayText);
|
|
894
|
-
switch (clause.op) {
|
|
895
|
-
case "contains":
|
|
896
|
-
return hay.some((s) => s.includes(needle));
|
|
897
|
-
case "=":
|
|
898
|
-
return hay.includes(needle);
|
|
899
|
-
case "!=":
|
|
900
|
-
return !hay.includes(needle);
|
|
901
|
-
default:
|
|
902
|
-
return true;
|
|
903
|
-
}
|
|
904
|
-
}
|
|
905
|
-
switch (clause.op) {
|
|
906
|
-
case "=":
|
|
907
|
-
return String(v ?? "") === String(coerceSafe(field, clause.value));
|
|
908
|
-
case "!=":
|
|
909
|
-
return String(v ?? "") !== String(coerceSafe(field, clause.value));
|
|
910
|
-
case ">":
|
|
911
|
-
return compare(v, coerceSafe(field, clause.value)) > 0;
|
|
912
|
-
case ">=":
|
|
913
|
-
return compare(v, coerceSafe(field, clause.value)) >= 0;
|
|
914
|
-
case "<":
|
|
915
|
-
return compare(v, coerceSafe(field, clause.value)) < 0;
|
|
916
|
-
case "<=":
|
|
917
|
-
return compare(v, coerceSafe(field, clause.value)) <= 0;
|
|
918
|
-
case "contains":
|
|
919
|
-
return String(v ?? "").toLowerCase().includes(clause.value.toLowerCase());
|
|
920
|
-
case "starts_with":
|
|
921
|
-
return String(v ?? "").toLowerCase().startsWith(clause.value.toLowerCase());
|
|
922
|
-
case "ends_with":
|
|
923
|
-
return String(v ?? "").toLowerCase().endsWith(clause.value.toLowerCase());
|
|
924
|
-
default:
|
|
925
|
-
return true;
|
|
926
|
-
}
|
|
927
|
-
}
|
|
928
|
-
function compileRegex(pattern) {
|
|
929
|
-
if (pattern === void 0) return void 0;
|
|
930
|
-
try {
|
|
931
|
-
return new RegExp(pattern);
|
|
932
|
-
} catch {
|
|
933
|
-
return null;
|
|
934
|
-
}
|
|
935
|
-
}
|
|
936
|
-
function extractSortValue(value, regex) {
|
|
937
|
-
if (regex === void 0) return value;
|
|
938
|
-
if (regex === null || value == null) return null;
|
|
939
|
-
const match = regex.exec(String(value));
|
|
940
|
-
if (!match) return null;
|
|
941
|
-
return match.length > 1 ? match[1] ?? null : match[0];
|
|
942
|
-
}
|
|
943
|
-
function coerceSafe(field, raw) {
|
|
944
|
-
try {
|
|
945
|
-
return coerceValue(field.type, raw);
|
|
946
|
-
} catch {
|
|
947
|
-
return raw;
|
|
948
|
-
}
|
|
949
|
-
}
|
|
950
|
-
function compare(a, b) {
|
|
951
|
-
if (a == null && b == null) return 0;
|
|
952
|
-
if (a == null) return -1;
|
|
953
|
-
if (b == null) return 1;
|
|
954
|
-
if (typeof a === "number" && typeof b === "number") return a - b;
|
|
955
|
-
if (typeof a === "boolean" && typeof b === "boolean") return a === b ? 0 : a ? 1 : -1;
|
|
956
|
-
return String(a).localeCompare(String(b));
|
|
957
|
-
}
|
|
958
|
-
|
|
959
|
-
// src/adapters.ts
|
|
960
|
-
var LAST_PREFIX = "query-table:last:";
|
|
961
|
-
var SAVED_PREFIX = "query-table:saved:";
|
|
962
|
-
var DEFAULT_PREFIX = "query-table:default:";
|
|
963
|
-
var MAX_SAVED_QUERIES = 100;
|
|
964
|
-
var MAX_SAVED_NAME_LENGTH = 200;
|
|
965
|
-
function normalizeSavedQuery(value) {
|
|
966
|
-
if (value == null || typeof value !== "object" || Array.isArray(value)) return null;
|
|
967
|
-
const raw = value;
|
|
968
|
-
if (typeof raw.id !== "string" || !raw.id || raw.id.length > 256 || typeof raw.name !== "string" || !raw.name || raw.name.length > MAX_SAVED_NAME_LENGTH || typeof raw.savedAt !== "number" || !Number.isFinite(raw.savedAt) || raw.query == null || typeof raw.query !== "object") {
|
|
969
|
-
return null;
|
|
970
|
-
}
|
|
971
|
-
return { id: raw.id, name: raw.name, savedAt: raw.savedAt, query: normalizeQueryState(raw.query) };
|
|
972
|
-
}
|
|
973
|
-
function normalizeSavedQueries(value) {
|
|
974
|
-
if (!Array.isArray(value)) return [];
|
|
975
|
-
return value.slice(0, MAX_SAVED_QUERIES).map(normalizeSavedQuery).filter((item) => item != null);
|
|
976
|
-
}
|
|
977
|
-
function memoryStorageAdapter() {
|
|
978
|
-
const last = /* @__PURE__ */ new Map();
|
|
979
|
-
const saved = /* @__PURE__ */ new Map();
|
|
980
|
-
const defaults = /* @__PURE__ */ new Map();
|
|
981
|
-
return {
|
|
982
|
-
async loadLast(key) {
|
|
983
|
-
const query = last.get(key);
|
|
984
|
-
return query ? normalizeQueryState(query) : null;
|
|
985
|
-
},
|
|
986
|
-
async saveLast(key, query) {
|
|
987
|
-
last.set(key, normalizeQueryState(query));
|
|
988
|
-
},
|
|
989
|
-
async listSaved(key) {
|
|
990
|
-
return (saved.get(key) ?? []).map((item) => ({ ...item, query: normalizeQueryState(item.query) }));
|
|
991
|
-
},
|
|
992
|
-
async loadDefaultSaved(key) {
|
|
993
|
-
const id = defaults.get(key);
|
|
994
|
-
return id ? (saved.get(key) ?? []).find((item) => item.id === id) ?? null : null;
|
|
995
|
-
},
|
|
996
|
-
async setDefaultSaved(key, id) {
|
|
997
|
-
if (id == null) {
|
|
998
|
-
defaults.delete(key);
|
|
999
|
-
return;
|
|
1000
|
-
}
|
|
1001
|
-
if (!(saved.get(key) ?? []).some((item) => item.id === id)) throw new Error(`Saved query "${id}" does not exist.`);
|
|
1002
|
-
defaults.set(key, id);
|
|
1003
|
-
},
|
|
1004
|
-
async saveNamed(key, name, query, savedAt) {
|
|
1005
|
-
const normalizedName = name.trim();
|
|
1006
|
-
if (!normalizedName || normalizedName.length > MAX_SAVED_NAME_LENGTH) {
|
|
1007
|
-
throw new Error(`Saved query name must be between 1 and ${MAX_SAVED_NAME_LENGTH} characters.`);
|
|
1008
|
-
}
|
|
1009
|
-
const items = saved.get(key) ?? [];
|
|
1010
|
-
if (items.length >= MAX_SAVED_QUERIES) throw new Error(`At most ${MAX_SAVED_QUERIES} saved queries are allowed.`);
|
|
1011
|
-
if (items.some((item2) => item2.name === normalizedName)) throw new Error(`Saved query "${normalizedName}" already exists.`);
|
|
1012
|
-
const item = {
|
|
1013
|
-
id: `${savedAt}-${Math.round((savedAt * 9301 + 49297) % 233280)}`,
|
|
1014
|
-
name: normalizedName,
|
|
1015
|
-
savedAt,
|
|
1016
|
-
query: normalizeQueryState(query)
|
|
1017
|
-
};
|
|
1018
|
-
saved.set(key, [...items, item]);
|
|
1019
|
-
return item;
|
|
1020
|
-
},
|
|
1021
|
-
async deleteSaved(key, id) {
|
|
1022
|
-
saved.set(key, (saved.get(key) ?? []).filter((item) => item.id !== id));
|
|
1023
|
-
if (defaults.get(key) === id) defaults.delete(key);
|
|
1024
|
-
}
|
|
1025
|
-
};
|
|
1026
|
-
}
|
|
1027
|
-
function localStorageAdapter() {
|
|
1028
|
-
const ls = (() => {
|
|
1029
|
-
try {
|
|
1030
|
-
return typeof localStorage !== "undefined" ? localStorage : null;
|
|
1031
|
-
} catch {
|
|
1032
|
-
return null;
|
|
1033
|
-
}
|
|
1034
|
-
})();
|
|
1035
|
-
const readSaved = (key) => {
|
|
1036
|
-
if (!ls) return [];
|
|
1037
|
-
try {
|
|
1038
|
-
const raw = ls.getItem(SAVED_PREFIX + key);
|
|
1039
|
-
return normalizeSavedQueries(raw ? JSON.parse(raw) : []);
|
|
1040
|
-
} catch {
|
|
1041
|
-
return [];
|
|
1042
|
-
}
|
|
1043
|
-
};
|
|
1044
|
-
const writeSaved = (key, items) => {
|
|
1045
|
-
if (ls) ls.setItem(SAVED_PREFIX + key, JSON.stringify(items));
|
|
1046
|
-
};
|
|
1047
|
-
const readDefaultId = (key) => {
|
|
1048
|
-
if (!ls) return null;
|
|
1049
|
-
return ls.getItem(DEFAULT_PREFIX + key);
|
|
1050
|
-
};
|
|
1051
|
-
const writeDefaultId = (key, id) => {
|
|
1052
|
-
if (!ls) return;
|
|
1053
|
-
if (id) ls.setItem(DEFAULT_PREFIX + key, id);
|
|
1054
|
-
else ls.removeItem(DEFAULT_PREFIX + key);
|
|
1055
|
-
};
|
|
1056
|
-
return {
|
|
1057
|
-
async loadLast(key) {
|
|
1058
|
-
if (!ls) return null;
|
|
1059
|
-
try {
|
|
1060
|
-
const raw = ls.getItem(LAST_PREFIX + key);
|
|
1061
|
-
return raw ? normalizeQueryState(JSON.parse(raw)) : null;
|
|
1062
|
-
} catch {
|
|
1063
|
-
return null;
|
|
1064
|
-
}
|
|
1065
|
-
},
|
|
1066
|
-
async saveLast(key, query) {
|
|
1067
|
-
if (ls) ls.setItem(LAST_PREFIX + key, JSON.stringify(normalizeQueryState(query)));
|
|
1068
|
-
},
|
|
1069
|
-
async listSaved(key) {
|
|
1070
|
-
return readSaved(key).sort((a, b) => b.savedAt - a.savedAt);
|
|
1071
|
-
},
|
|
1072
|
-
async loadDefaultSaved(key) {
|
|
1073
|
-
const id = readDefaultId(key);
|
|
1074
|
-
if (!id) return null;
|
|
1075
|
-
const found = readSaved(key).find((q) => q.id === id);
|
|
1076
|
-
if (!found) writeDefaultId(key, null);
|
|
1077
|
-
return found ?? null;
|
|
1078
|
-
},
|
|
1079
|
-
async setDefaultSaved(key, id) {
|
|
1080
|
-
if (id && !readSaved(key).some((q) => q.id === id)) {
|
|
1081
|
-
throw new Error(`Saved query "${id}" does not exist.`);
|
|
1082
|
-
}
|
|
1083
|
-
writeDefaultId(key, id);
|
|
1084
|
-
},
|
|
1085
|
-
async saveNamed(key, name, query, savedAt) {
|
|
1086
|
-
const normalizedName = name.trim();
|
|
1087
|
-
if (!normalizedName || normalizedName.length > MAX_SAVED_NAME_LENGTH) {
|
|
1088
|
-
throw new Error(`Saved query name must be between 1 and ${MAX_SAVED_NAME_LENGTH} characters.`);
|
|
1089
|
-
}
|
|
1090
|
-
const items = readSaved(key);
|
|
1091
|
-
if (items.length >= MAX_SAVED_QUERIES) throw new Error(`At most ${MAX_SAVED_QUERIES} saved queries are allowed.`);
|
|
1092
|
-
if (items.some((q) => q.name === normalizedName)) {
|
|
1093
|
-
throw new Error(`Saved query "${normalizedName}" already exists.`);
|
|
1094
|
-
}
|
|
1095
|
-
const item = {
|
|
1096
|
-
id: `${savedAt}-${Math.round((savedAt * 9301 + 49297) % 233280)}`,
|
|
1097
|
-
name: normalizedName,
|
|
1098
|
-
savedAt,
|
|
1099
|
-
query: normalizeQueryState(query)
|
|
1100
|
-
};
|
|
1101
|
-
items.push(item);
|
|
1102
|
-
writeSaved(key, items);
|
|
1103
|
-
return item;
|
|
1104
|
-
},
|
|
1105
|
-
async deleteSaved(key, id) {
|
|
1106
|
-
writeSaved(
|
|
1107
|
-
key,
|
|
1108
|
-
readSaved(key).filter((q) => q.id !== id)
|
|
1109
|
-
);
|
|
1110
|
-
if (readDefaultId(key) === id) writeDefaultId(key, null);
|
|
1111
|
-
}
|
|
1112
|
-
};
|
|
975
|
+
const diagnostics = [];
|
|
976
|
+
for (const c of q.aggregations ?? []) if (!aggregations.includes(c)) diagnostics.push({ metricId: c.id, code: "unsupported_metric", message: "Metric requires unavailable backend fields.", from: 0, to: 1 });
|
|
977
|
+
if (where.length !== q.where.length || suppliedPredicateCount !== q.where.reduce((n, t) => n + predicatesOf(t).length, 0)) diagnostics.push({ code: "residual_filter", message: "All-matching metrics are unavailable with residual client-only filters.", from: 0, to: 1 });
|
|
978
|
+
return { where, aggregations, ...diagnostics.length ? { diagnostics } : {} };
|
|
1113
979
|
}
|
|
1114
980
|
|
|
1115
981
|
// src/formula.ts
|
|
@@ -1470,7 +1336,7 @@ var FormulaError = class extends Error {
|
|
|
1470
1336
|
this.to = to;
|
|
1471
1337
|
}
|
|
1472
1338
|
};
|
|
1473
|
-
function compileFormula(source, fields, resolve) {
|
|
1339
|
+
function compileFormula(source, fields, resolve, extension) {
|
|
1474
1340
|
if (!source.trim() || source.length > 1e4)
|
|
1475
1341
|
throw new FormulaError("Formula must contain 1\u201310,000 characters.");
|
|
1476
1342
|
const tokens = [];
|
|
@@ -1671,6 +1537,8 @@ function compileFormula(source, fields, resolve) {
|
|
|
1671
1537
|
n.valueType = t === "enum" ? "text" : t;
|
|
1672
1538
|
return n.valueType;
|
|
1673
1539
|
}
|
|
1540
|
+
const extended = extension?.(n, (arg) => check(arg, level + 1));
|
|
1541
|
+
if (extended !== void 0) return extended;
|
|
1674
1542
|
const ts = n.args.map((a) => check(a, level + 1));
|
|
1675
1543
|
const need = (index, t) => {
|
|
1676
1544
|
if (ts[index] !== void 0 && ts[index] !== "null" && ts[index] !== t)
|
|
@@ -1771,7 +1639,7 @@ function compileFormula(source, fields, resolve) {
|
|
|
1771
1639
|
const type = check(ast);
|
|
1772
1640
|
return { ast, dependencies: [...deps].sort(), type };
|
|
1773
1641
|
}
|
|
1774
|
-
function formulaRuntime(ast, inputs) {
|
|
1642
|
+
function formulaRuntime(ast, inputs, extension) {
|
|
1775
1643
|
const fail = (s) => {
|
|
1776
1644
|
throw new Error(s);
|
|
1777
1645
|
};
|
|
@@ -1825,6 +1693,11 @@ function formulaRuntime(ast, inputs) {
|
|
|
1825
1693
|
if (n.valueType === "textarray") return arr(v);
|
|
1826
1694
|
return v;
|
|
1827
1695
|
}
|
|
1696
|
+
const extended = extension?.(n);
|
|
1697
|
+
if (extended) {
|
|
1698
|
+
if (extended.error) fail(extended.error);
|
|
1699
|
+
return bound(extended.value);
|
|
1700
|
+
}
|
|
1828
1701
|
const name = n.name;
|
|
1829
1702
|
const args = n.args;
|
|
1830
1703
|
if (name === "IF") return run(run(args[0]) === true ? args[1] : args[2]);
|
|
@@ -2215,6 +2088,1552 @@ function formulaRuntime(ast, inputs) {
|
|
|
2215
2088
|
};
|
|
2216
2089
|
}
|
|
2217
2090
|
}
|
|
2091
|
+
|
|
2092
|
+
// src/execution.ts
|
|
2093
|
+
function toServerQueryV2(q, schema, execution) {
|
|
2094
|
+
const normalized = normalizeQueryState(q), legacy = toServerQuery(normalized, schema);
|
|
2095
|
+
const diagnostics = [];
|
|
2096
|
+
const expectedRevisions = /* @__PURE__ */ Object.create(null);
|
|
2097
|
+
const requireField = (name, operation) => {
|
|
2098
|
+
const cap = execution.fields[name];
|
|
2099
|
+
const revision = execution.resolvedRevisions[name] ?? execution.resolvedRevisions[name.slice("@computed/".length)];
|
|
2100
|
+
if (!cap?.[operation] || !revision) {
|
|
2101
|
+
diagnostics.push({
|
|
2102
|
+
code: "unsupported_computed",
|
|
2103
|
+
message: `Computed field ${name} is unavailable for ${operation}.`,
|
|
2104
|
+
from: 0,
|
|
2105
|
+
to: 1
|
|
2106
|
+
});
|
|
2107
|
+
return false;
|
|
2108
|
+
}
|
|
2109
|
+
return true;
|
|
2110
|
+
};
|
|
2111
|
+
for (const [key, value] of Object.entries(execution.resolvedRevisions)) {
|
|
2112
|
+
if (key.length > 256 || typeof value !== "string" || !value || value.length > 256)
|
|
2113
|
+
throw Error("Invalid computed revision envelope.");
|
|
2114
|
+
expectedRevisions[key] = value;
|
|
2115
|
+
}
|
|
2116
|
+
const select = [...legacy.select];
|
|
2117
|
+
for (const c of normalized.select.length ? normalized.select : selectedFields(schema, normalized).map((f2) => ({ field: f2.name })))
|
|
2118
|
+
if (isComputedField(c.field) && requireField(c.field, "select"))
|
|
2119
|
+
select.push(c.field);
|
|
2120
|
+
const orderBy = [];
|
|
2121
|
+
for (const term of normalized.orderBy.length ? normalized.orderBy : schema.defaultSort ?? []) {
|
|
2122
|
+
if (isComputedField(term.field)) {
|
|
2123
|
+
if (requireField(term.field, "sort")) {
|
|
2124
|
+
if (term.dir !== "asc" && term.dir !== "desc" || term.extract) {
|
|
2125
|
+
diagnostics.push({
|
|
2126
|
+
code: "unsupported_computed_sort",
|
|
2127
|
+
message: "Invalid computed sort or extraction.",
|
|
2128
|
+
from: 0,
|
|
2129
|
+
to: 1
|
|
2130
|
+
});
|
|
2131
|
+
continue;
|
|
2132
|
+
}
|
|
2133
|
+
orderBy.push({
|
|
2134
|
+
field: term.field,
|
|
2135
|
+
dir: term.dir,
|
|
2136
|
+
...term.nulls === "first" || term.nulls === "last" ? { nulls: term.nulls } : {}
|
|
2137
|
+
});
|
|
2138
|
+
}
|
|
2139
|
+
} else {
|
|
2140
|
+
const projected = toServerQuery(
|
|
2141
|
+
{ ...normalized, orderBy: [term] },
|
|
2142
|
+
schema
|
|
2143
|
+
).orderBy;
|
|
2144
|
+
if (!projected.length)
|
|
2145
|
+
diagnostics.push({
|
|
2146
|
+
code: "unsupported_sort",
|
|
2147
|
+
message: `Unavailable server sort: ${term.field}.`,
|
|
2148
|
+
from: 0,
|
|
2149
|
+
to: 1
|
|
2150
|
+
});
|
|
2151
|
+
orderBy.push(...projected);
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
if (legacy.where.length !== normalized.where.length || q.where.reduce((n, t) => n + predicatesOf(t).length, 0) !== normalized.where.reduce((n, t) => n + predicatesOf(t).length, 0))
|
|
2155
|
+
diagnostics.push({
|
|
2156
|
+
code: "residual_filter",
|
|
2157
|
+
message: "Global execution is unavailable with residual client-only filters.",
|
|
2158
|
+
from: 0,
|
|
2159
|
+
to: 1
|
|
2160
|
+
});
|
|
2161
|
+
return {
|
|
2162
|
+
...legacy,
|
|
2163
|
+
select: [...new Set(select)].sort(),
|
|
2164
|
+
orderBy,
|
|
2165
|
+
version: 2,
|
|
2166
|
+
profile: execution.profile,
|
|
2167
|
+
expectedRevisions,
|
|
2168
|
+
...execution.planToken ? { planToken: execution.planToken } : {},
|
|
2169
|
+
...execution.snapshot !== void 0 ? { snapshot: execution.snapshot } : {},
|
|
2170
|
+
diagnostics
|
|
2171
|
+
};
|
|
2172
|
+
}
|
|
2173
|
+
|
|
2174
|
+
// src/metricPrecision.ts
|
|
2175
|
+
function assertMetricNumber(value) {
|
|
2176
|
+
if (!Number.isFinite(value) || Number.isInteger(value) && !Number.isSafeInteger(value)) {
|
|
2177
|
+
throw Error("Aggregate exceeds finite safe numeric precision.");
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
function checkedMetricResult(result) {
|
|
2181
|
+
if (result.error || typeof result.value !== "number") return result;
|
|
2182
|
+
try {
|
|
2183
|
+
assertMetricNumber(result.value);
|
|
2184
|
+
return result;
|
|
2185
|
+
} catch (e) {
|
|
2186
|
+
return { value: null, error: e.message };
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
|
|
2190
|
+
// src/metricDistributions.ts
|
|
2191
|
+
function percentile(sorted, p) {
|
|
2192
|
+
if (!Number.isFinite(p) || p < 0 || p > 1)
|
|
2193
|
+
throw Error("Percentile must be between zero and one.");
|
|
2194
|
+
if (sorted.some((v, i) => !Number.isFinite(v) || i > 0 && v < sorted[i - 1]))
|
|
2195
|
+
throw Error("Percentile inputs must be sorted finite numbers.");
|
|
2196
|
+
sorted.forEach(assertMetricNumber);
|
|
2197
|
+
if (!sorted.length) return null;
|
|
2198
|
+
const position = (sorted.length - 1) * p, lower = Math.floor(position), fraction = position - lower;
|
|
2199
|
+
const a = sorted[lower], b = sorted[Math.min(lower + 1, sorted.length - 1)];
|
|
2200
|
+
const raw = (1 - fraction) * a + fraction * b;
|
|
2201
|
+
assertMetricNumber(raw);
|
|
2202
|
+
const value = Math.min(b, Math.max(a, raw));
|
|
2203
|
+
return value;
|
|
2204
|
+
}
|
|
2205
|
+
function boxSummary(values, whiskers = "minmax") {
|
|
2206
|
+
if (values.some((v) => !Number.isFinite(v)))
|
|
2207
|
+
throw Error("Distribution inputs must be finite numbers.");
|
|
2208
|
+
values.forEach(assertMetricNumber);
|
|
2209
|
+
const sorted = [...values].sort((a, b) => a - b), n = sorted.length;
|
|
2210
|
+
if (!n) return null;
|
|
2211
|
+
const min = sorted[0], max = sorted[n - 1], q1 = percentile(sorted, 0.25), median = percentile(sorted, 0.5), q3 = percentile(sorted, 0.75);
|
|
2212
|
+
const lowFence = q1 - 1.5 * (q3 - q1), highFence = q3 + 1.5 * (q3 - q1);
|
|
2213
|
+
const low = whiskers === "tukey" ? sorted.find((v) => v >= lowFence) : min;
|
|
2214
|
+
const high = whiskers === "tukey" ? [...sorted].reverse().find((v) => v <= highFence) : max;
|
|
2215
|
+
const outside = whiskers === "tukey" ? sorted.filter((v) => v < low || v > high) : [];
|
|
2216
|
+
const outliers = outside.length <= 20 ? outside : [...outside.slice(0, 10), ...outside.slice(-10)];
|
|
2217
|
+
const rawMean = sorted.reduce((sum, value) => sum + value / n, 0);
|
|
2218
|
+
assertMetricNumber(rawMean);
|
|
2219
|
+
const mean = Math.min(max, Math.max(min, rawMean));
|
|
2220
|
+
[min, q1, median, q3, max, mean, low, high].forEach(assertMetricNumber);
|
|
2221
|
+
return {
|
|
2222
|
+
n,
|
|
2223
|
+
min,
|
|
2224
|
+
q1,
|
|
2225
|
+
median,
|
|
2226
|
+
q3,
|
|
2227
|
+
max,
|
|
2228
|
+
mean,
|
|
2229
|
+
low,
|
|
2230
|
+
high,
|
|
2231
|
+
outliers,
|
|
2232
|
+
outlierCount: outside.length,
|
|
2233
|
+
whiskers,
|
|
2234
|
+
method: "exact-linear"
|
|
2235
|
+
};
|
|
2236
|
+
}
|
|
2237
|
+
function histogramEdges(values, bins) {
|
|
2238
|
+
if (!Number.isInteger(bins) || bins < 2 || bins > 30)
|
|
2239
|
+
throw Error("Choose 2\u201330 histogram bins.");
|
|
2240
|
+
if (values.some((v) => !Number.isFinite(v)))
|
|
2241
|
+
throw Error("Histogram inputs must be finite numbers.");
|
|
2242
|
+
values.forEach(assertMetricNumber);
|
|
2243
|
+
if (!values.length) return [];
|
|
2244
|
+
let min = Infinity, max = -Infinity;
|
|
2245
|
+
for (const value of values) {
|
|
2246
|
+
min = Math.min(min, value);
|
|
2247
|
+
max = Math.max(max, value);
|
|
2248
|
+
}
|
|
2249
|
+
if (min === max) return [min, max];
|
|
2250
|
+
if (!Number.isFinite(max - min))
|
|
2251
|
+
throw Error("Histogram range exceeds finite numeric precision.");
|
|
2252
|
+
const edges = Array.from(
|
|
2253
|
+
{ length: bins + 1 },
|
|
2254
|
+
(_, i) => min + (max - min) * (i / bins)
|
|
2255
|
+
);
|
|
2256
|
+
edges[0] = min;
|
|
2257
|
+
edges[bins] = max;
|
|
2258
|
+
edges.forEach(assertMetricNumber);
|
|
2259
|
+
if (edges.some((value, i) => i > 0 && value <= edges[i - 1]))
|
|
2260
|
+
throw Error(
|
|
2261
|
+
"Histogram bins are finer than numeric precision. Reduce the bin count."
|
|
2262
|
+
);
|
|
2263
|
+
return edges;
|
|
2264
|
+
}
|
|
2265
|
+
function histogramCounts(values, edges) {
|
|
2266
|
+
if (values.some((v) => !Number.isFinite(v)))
|
|
2267
|
+
throw Error("Histogram inputs must be finite numbers.");
|
|
2268
|
+
if (!edges.length) {
|
|
2269
|
+
if (values.length) throw Error("Histogram values require edges.");
|
|
2270
|
+
return [];
|
|
2271
|
+
}
|
|
2272
|
+
if (edges.length < 2 || edges.some(
|
|
2273
|
+
(v, i) => !Number.isFinite(v) || i > 0 && (v < edges[i - 1] || v === edges[i - 1] && edges.length !== 2)
|
|
2274
|
+
))
|
|
2275
|
+
throw Error("Histogram edges must be ordered finite boundaries.");
|
|
2276
|
+
values.forEach(assertMetricNumber);
|
|
2277
|
+
edges.forEach(assertMetricNumber);
|
|
2278
|
+
const counts = Array(edges.length - 1).fill(0);
|
|
2279
|
+
for (const value of values) {
|
|
2280
|
+
if (value < edges[0] || value > edges[edges.length - 1])
|
|
2281
|
+
throw Error("Value falls outside shared histogram edges.");
|
|
2282
|
+
let bin = 0;
|
|
2283
|
+
while (bin < counts.length - 1 && value >= edges[bin + 1]) bin++;
|
|
2284
|
+
counts[bin] = counts[bin] + 1;
|
|
2285
|
+
}
|
|
2286
|
+
return counts;
|
|
2287
|
+
}
|
|
2288
|
+
|
|
2289
|
+
// src/metrics.ts
|
|
2290
|
+
var aggregates = /* @__PURE__ */ new Set([
|
|
2291
|
+
"COUNT",
|
|
2292
|
+
"COUNT_DISTINCT",
|
|
2293
|
+
"SUM",
|
|
2294
|
+
"AVG",
|
|
2295
|
+
"MIN",
|
|
2296
|
+
"MAX"
|
|
2297
|
+
]);
|
|
2298
|
+
function compileMetric(expression, schema, resolve) {
|
|
2299
|
+
const fields = Array.isArray(schema) ? schema : schema.fields;
|
|
2300
|
+
const nodes = [];
|
|
2301
|
+
const aggregateTypes = [];
|
|
2302
|
+
let inAggregate = false;
|
|
2303
|
+
const plan = compileFormula(
|
|
2304
|
+
expression,
|
|
2305
|
+
resolve ? fields.filter(
|
|
2306
|
+
(f2) => f2.source.kind !== "derived" || !f2.source.computedId
|
|
2307
|
+
) : fields,
|
|
2308
|
+
resolve,
|
|
2309
|
+
(node, check) => {
|
|
2310
|
+
if (node.kind !== "call" || !aggregates.has(node.name)) return void 0;
|
|
2311
|
+
if (inAggregate)
|
|
2312
|
+
throw new FormulaError(
|
|
2313
|
+
"Nested aggregates are not allowed.",
|
|
2314
|
+
node.from,
|
|
2315
|
+
node.to
|
|
2316
|
+
);
|
|
2317
|
+
if (node.args.length > 1 || node.name !== "COUNT" && node.args.length !== 1)
|
|
2318
|
+
throw new FormulaError(
|
|
2319
|
+
`${node.name} requires ${node.name === "COUNT" ? "zero or one argument" : "one argument"}.`,
|
|
2320
|
+
node.from,
|
|
2321
|
+
node.to
|
|
2322
|
+
);
|
|
2323
|
+
inAggregate = true;
|
|
2324
|
+
let type;
|
|
2325
|
+
try {
|
|
2326
|
+
type = node.args[0] ? check(node.args[0]) : "number";
|
|
2327
|
+
} finally {
|
|
2328
|
+
inAggregate = false;
|
|
2329
|
+
}
|
|
2330
|
+
const visit = (n) => {
|
|
2331
|
+
if (n.kind === "field") {
|
|
2332
|
+
const field = fields.find((f2) => f2.name === n.name);
|
|
2333
|
+
if (!field)
|
|
2334
|
+
throw new FormulaError(
|
|
2335
|
+
`Unresolved dependency ${n.name}.`,
|
|
2336
|
+
n.from,
|
|
2337
|
+
n.to
|
|
2338
|
+
);
|
|
2339
|
+
if (!isMeasurable(field) || (field.aggregate?.ops !== void 0 || node.args[0] === n) && !aggOpsForField(field).includes(node.name.toLowerCase()))
|
|
2340
|
+
throw new FormulaError(
|
|
2341
|
+
`Field ${n.name} does not allow ${node.name}.`,
|
|
2342
|
+
n.from,
|
|
2343
|
+
n.to
|
|
2344
|
+
);
|
|
2345
|
+
} else if (n.kind === "call") {
|
|
2346
|
+
if (aggregates.has(n.name))
|
|
2347
|
+
throw new FormulaError(
|
|
2348
|
+
"Nested aggregates are not allowed.",
|
|
2349
|
+
n.from,
|
|
2350
|
+
n.to
|
|
2351
|
+
);
|
|
2352
|
+
n.args.forEach(visit);
|
|
2353
|
+
}
|
|
2354
|
+
};
|
|
2355
|
+
node.args.forEach(visit);
|
|
2356
|
+
if (["SUM", "AVG"].includes(node.name) && !["number", "null"].includes(type))
|
|
2357
|
+
throw new FormulaError(
|
|
2358
|
+
`${node.name} requires numeric input.`,
|
|
2359
|
+
node.from,
|
|
2360
|
+
node.to
|
|
2361
|
+
);
|
|
2362
|
+
if (["MIN", "MAX", "COUNT_DISTINCT"].includes(node.name) && (type === "textarray" || type === "bool" && node.name !== "COUNT_DISTINCT"))
|
|
2363
|
+
throw new FormulaError(
|
|
2364
|
+
`Unsupported ${node.name} input type.`,
|
|
2365
|
+
node.from,
|
|
2366
|
+
node.to
|
|
2367
|
+
);
|
|
2368
|
+
nodes.push(node);
|
|
2369
|
+
aggregateTypes.push(type);
|
|
2370
|
+
return ["COUNT", "COUNT_DISTINCT", "SUM", "AVG"].includes(node.name) ? "number" : type;
|
|
2371
|
+
}
|
|
2372
|
+
);
|
|
2373
|
+
const groupCheck = (node) => {
|
|
2374
|
+
if (node.kind === "field")
|
|
2375
|
+
throw new FormulaError(
|
|
2376
|
+
"Row fields must be inside an aggregate.",
|
|
2377
|
+
node.from,
|
|
2378
|
+
node.to
|
|
2379
|
+
);
|
|
2380
|
+
if (node.kind === "call" && !aggregates.has(node.name)) {
|
|
2381
|
+
if (node.name.startsWith("REGEX_"))
|
|
2382
|
+
throw new FormulaError(
|
|
2383
|
+
"Regex is supported only inside aggregate row expressions.",
|
|
2384
|
+
node.from,
|
|
2385
|
+
node.to
|
|
2386
|
+
);
|
|
2387
|
+
node.args.forEach(groupCheck);
|
|
2388
|
+
}
|
|
2389
|
+
};
|
|
2390
|
+
groupCheck(plan.ast);
|
|
2391
|
+
if (!nodes.length)
|
|
2392
|
+
throw new FormulaError("A metric must contain an aggregate.");
|
|
2393
|
+
if (plan.type === "textarray")
|
|
2394
|
+
throw new FormulaError("Metrics must return scalar values.");
|
|
2395
|
+
return { ...plan, aggregates: nodes, aggregateTypes };
|
|
2396
|
+
}
|
|
2397
|
+
function metricAggregatePlans(plan) {
|
|
2398
|
+
return plan.aggregates.map((node) => {
|
|
2399
|
+
if (!node.args[0]) return null;
|
|
2400
|
+
const dependencies = /* @__PURE__ */ new Set();
|
|
2401
|
+
const visit = (n) => {
|
|
2402
|
+
if (n.kind === "field") dependencies.add(n.name);
|
|
2403
|
+
else if (n.kind === "call") n.args.forEach(visit);
|
|
2404
|
+
};
|
|
2405
|
+
visit(node.args[0]);
|
|
2406
|
+
return {
|
|
2407
|
+
ast: node.args[0],
|
|
2408
|
+
dependencies: [...dependencies].sort(),
|
|
2409
|
+
type: plan.aggregateTypes[plan.aggregates.indexOf(node)]
|
|
2410
|
+
};
|
|
2411
|
+
});
|
|
2412
|
+
}
|
|
2413
|
+
function distinctAccumulator(options) {
|
|
2414
|
+
const values = /* @__PURE__ */ new Set();
|
|
2415
|
+
let bytes = 0;
|
|
2416
|
+
const cardinality = options.maxDistinctValues ?? 1e5;
|
|
2417
|
+
const byteLimit = options.maxDistinctBytes ?? 16e6;
|
|
2418
|
+
if (!Number.isSafeInteger(cardinality) || cardinality < 0 || !Number.isSafeInteger(byteLimit) || byteLimit < 0)
|
|
2419
|
+
throw Error("Invalid distinct budget.");
|
|
2420
|
+
return {
|
|
2421
|
+
add(value) {
|
|
2422
|
+
const v = scalar(value);
|
|
2423
|
+
if (typeof v === "number") assertMetricNumber(v);
|
|
2424
|
+
if (v === null || values.has(v)) return;
|
|
2425
|
+
const size = 64 + (typeof v === "string" ? v.length * 2 : 8);
|
|
2426
|
+
if (values.size >= cardinality || size > byteLimit - bytes)
|
|
2427
|
+
throw Error("Metric distinct retained-memory budget exceeded.");
|
|
2428
|
+
values.add(v);
|
|
2429
|
+
bytes += size;
|
|
2430
|
+
},
|
|
2431
|
+
result: () => ({ value: values.size })
|
|
2432
|
+
};
|
|
2433
|
+
}
|
|
2434
|
+
function reduceMetricValues(node, results, options = {}) {
|
|
2435
|
+
return reduceResults(node, results, options);
|
|
2436
|
+
}
|
|
2437
|
+
function reduceResults(node, results, options) {
|
|
2438
|
+
try {
|
|
2439
|
+
if (!aggregates.has(node.name)) throw Error("Expected an aggregate node.");
|
|
2440
|
+
const distinct = node.name === "COUNT_DISTINCT" ? distinctAccumulator(options) : void 0;
|
|
2441
|
+
const values = [];
|
|
2442
|
+
let count = 0, nonnull = 0, error;
|
|
2443
|
+
for (const r of results) {
|
|
2444
|
+
count++;
|
|
2445
|
+
if (r.error) {
|
|
2446
|
+
error ?? (error = r.error);
|
|
2447
|
+
continue;
|
|
2448
|
+
}
|
|
2449
|
+
if (r.value === null) continue;
|
|
2450
|
+
if (node.name !== "COUNT" && !distinct && typeof r.value === "number")
|
|
2451
|
+
assertMetricNumber(r.value);
|
|
2452
|
+
nonnull++;
|
|
2453
|
+
if (distinct) distinct.add(r.value);
|
|
2454
|
+
else if (node.name !== "COUNT") values.push(r.value);
|
|
2455
|
+
}
|
|
2456
|
+
if (error) return { value: null, error };
|
|
2457
|
+
if (distinct) return distinct.result();
|
|
2458
|
+
if (node.name === "COUNT")
|
|
2459
|
+
return { value: node.args.length ? nonnull : count };
|
|
2460
|
+
if (!values.length) return { value: null };
|
|
2461
|
+
if (node.name === "SUM" || node.name === "AVG") {
|
|
2462
|
+
let sum = 0;
|
|
2463
|
+
for (const v of values) {
|
|
2464
|
+
if (typeof v !== "number" || !Number.isFinite(v))
|
|
2465
|
+
throw Error("Expected a finite number.");
|
|
2466
|
+
sum += v / (node.name === "AVG" ? values.length : 1);
|
|
2467
|
+
assertMetricNumber(sum);
|
|
2468
|
+
}
|
|
2469
|
+
return { value: sum };
|
|
2470
|
+
}
|
|
2471
|
+
return {
|
|
2472
|
+
value: values.reduce(
|
|
2473
|
+
(a, b) => compareMetricValues(scalar(a), scalar(b)) * (node.name === "MIN" ? 1 : -1) <= 0 ? a : b
|
|
2474
|
+
)
|
|
2475
|
+
};
|
|
2476
|
+
} catch (e) {
|
|
2477
|
+
return { value: null, error: e instanceof Error ? e.message : String(e) };
|
|
2478
|
+
}
|
|
2479
|
+
}
|
|
2480
|
+
function evaluateMetricReductions(plan, reductions) {
|
|
2481
|
+
const key = (n) => `${n.from}:${n.to}:${n.kind === "call" ? n.name : n.kind}`;
|
|
2482
|
+
const byNode = new Map(
|
|
2483
|
+
plan.aggregates.map((n, i) => [key(n), reductions[i]])
|
|
2484
|
+
);
|
|
2485
|
+
return checkedMetricResult(
|
|
2486
|
+
formulaRuntime(
|
|
2487
|
+
plan.ast,
|
|
2488
|
+
{},
|
|
2489
|
+
(node) => byNode.has(key(node)) ? checkedMetricResult(
|
|
2490
|
+
byNode.get(key(node)) ?? {
|
|
2491
|
+
value: null,
|
|
2492
|
+
error: "Missing aggregate reduction."
|
|
2493
|
+
}
|
|
2494
|
+
) : void 0
|
|
2495
|
+
)
|
|
2496
|
+
);
|
|
2497
|
+
}
|
|
2498
|
+
function metricExpression(clause) {
|
|
2499
|
+
return clause.expression ?? `${clause.op.toUpperCase()}(${clause.field ? `[${clause.field.replace(/]/g, "]]")}]` : ""})`;
|
|
2500
|
+
}
|
|
2501
|
+
function hasRegex(plan) {
|
|
2502
|
+
const visit = (n) => n.kind === "call" && (n.name.startsWith("REGEX_") || n.args.some(visit));
|
|
2503
|
+
return visit(plan.ast);
|
|
2504
|
+
}
|
|
2505
|
+
function distributionActive(c) {
|
|
2506
|
+
return !!c.distribution && (!c.display || c.display.kind === "box" || c.display.kind === "histogram");
|
|
2507
|
+
}
|
|
2508
|
+
function metricPlans(c, schema, resolve) {
|
|
2509
|
+
if (distributionActive(c)) {
|
|
2510
|
+
const p = compileFormula(
|
|
2511
|
+
c.distribution.input,
|
|
2512
|
+
resolve ? schema.fields.filter(
|
|
2513
|
+
(f2) => f2.source.kind !== "derived" || !f2.source.computedId
|
|
2514
|
+
) : schema.fields,
|
|
2515
|
+
resolve
|
|
2516
|
+
);
|
|
2517
|
+
if (p.type !== "number" && p.type !== "null")
|
|
2518
|
+
throw new FormulaError("Distribution input must be numeric.");
|
|
2519
|
+
if (!["box", "histogram"].includes(c.distribution.kind))
|
|
2520
|
+
throw new FormulaError("Invalid distribution kind.");
|
|
2521
|
+
if (c.distribution.kind === "histogram" && (!Number.isInteger(c.distribution.bins ?? 10) || (c.distribution.bins ?? 10) < 2 || (c.distribution.bins ?? 10) > 30))
|
|
2522
|
+
throw new FormulaError("Choose 2\u201330 histogram bins.");
|
|
2523
|
+
for (const name of p.dependencies) {
|
|
2524
|
+
const field = schema.fields.find((f2) => f2.name === name);
|
|
2525
|
+
if (!isMeasurable(field))
|
|
2526
|
+
throw new FormulaError(`Field ${name} cannot be a distribution input.`);
|
|
2527
|
+
}
|
|
2528
|
+
if (c.scope !== "shownRows" && hasRegex(p))
|
|
2529
|
+
throw new FormulaError("Regex is unavailable for all-matching metrics.");
|
|
2530
|
+
return [p];
|
|
2531
|
+
}
|
|
2532
|
+
const out = [
|
|
2533
|
+
compileMetric(
|
|
2534
|
+
metricExpression(
|
|
2535
|
+
c.expression === void 0 && c.field ? { ...c, field: resolveFieldName(schema, c.field) ?? c.field } : c
|
|
2536
|
+
),
|
|
2537
|
+
schema,
|
|
2538
|
+
resolve
|
|
2539
|
+
)
|
|
2540
|
+
];
|
|
2541
|
+
if (c.display?.kind === "scatter") {
|
|
2542
|
+
out.push(compileMetric(c.expressionY ?? "", schema, resolve));
|
|
2543
|
+
if (out.some((p) => p.type !== "number" && p.type !== "null"))
|
|
2544
|
+
throw new FormulaError("Scatter expressions must be numeric.");
|
|
2545
|
+
}
|
|
2546
|
+
if (c.scope !== "shownRows" && out.some(hasRegex))
|
|
2547
|
+
throw new FormulaError("Regex is unavailable for all-matching metrics.");
|
|
2548
|
+
return out;
|
|
2549
|
+
}
|
|
2550
|
+
function validateMetric(c, schema, resolve) {
|
|
2551
|
+
try {
|
|
2552
|
+
if (c.diagnostics?.length) throw new FormulaError(c.diagnostics.join(" "));
|
|
2553
|
+
if (c.groupBy.length > 20)
|
|
2554
|
+
throw new FormulaError("At most 20 grouping fields are allowed.");
|
|
2555
|
+
for (const key of c.groupBy) {
|
|
2556
|
+
const f2 = schema.fields.find(
|
|
2557
|
+
(f3) => f3.name === (resolveFieldName(schema, key) ?? key)
|
|
2558
|
+
);
|
|
2559
|
+
if (!f2 || !isGroupable(f2))
|
|
2560
|
+
throw new FormulaError(`Field ${key} cannot group metrics.`);
|
|
2561
|
+
}
|
|
2562
|
+
if (c.groupLimit !== void 0 && (!Number.isInteger(c.groupLimit) || c.groupLimit < 1 || c.groupLimit > 1e4))
|
|
2563
|
+
throw new FormulaError("Group limit must be 1\u201310,000.");
|
|
2564
|
+
for (const s of c.sort ?? []) {
|
|
2565
|
+
if (s.key === "y" && c.display?.kind !== "scatter")
|
|
2566
|
+
throw new FormulaError("Y sorting requires a scatter metric.");
|
|
2567
|
+
if (s.key === "samples" && !distributionActive(c))
|
|
2568
|
+
throw new FormulaError(
|
|
2569
|
+
"Sample sorting requires a distribution metric."
|
|
2570
|
+
);
|
|
2571
|
+
if (s.key.startsWith("group") && Number(s.key.slice(5)) >= c.groupBy.length)
|
|
2572
|
+
throw new FormulaError("Sort refers to an unavailable grouping key.");
|
|
2573
|
+
}
|
|
2574
|
+
const compiled = metricPlans(c, schema, resolve);
|
|
2575
|
+
if (c.scope !== "shownRows" && compiled.some(hasRegex))
|
|
2576
|
+
throw new FormulaError("Regex is unavailable for all-matching metrics.");
|
|
2577
|
+
return [];
|
|
2578
|
+
} catch (e) {
|
|
2579
|
+
return [
|
|
2580
|
+
{
|
|
2581
|
+
message: e instanceof Error ? e.message : String(e),
|
|
2582
|
+
from: e instanceof FormulaError ? e.from : 0,
|
|
2583
|
+
to: e instanceof FormulaError ? e.to : 1
|
|
2584
|
+
}
|
|
2585
|
+
];
|
|
2586
|
+
}
|
|
2587
|
+
}
|
|
2588
|
+
function metricDependencies(c, schema, resolve) {
|
|
2589
|
+
return [
|
|
2590
|
+
.../* @__PURE__ */ new Set([
|
|
2591
|
+
...c.groupBy.map((name) => resolveFieldName(schema, name) ?? name),
|
|
2592
|
+
...metricPlans(c, schema, resolve).flatMap((p) => p.dependencies)
|
|
2593
|
+
])
|
|
2594
|
+
].sort();
|
|
2595
|
+
}
|
|
2596
|
+
function metricComputationKey(clauses) {
|
|
2597
|
+
return JSON.stringify(
|
|
2598
|
+
clauses.map((c) => ({
|
|
2599
|
+
id: c.id,
|
|
2600
|
+
diagnostics: c.diagnostics,
|
|
2601
|
+
unsupportedOp: c.unsupportedOp,
|
|
2602
|
+
expression: distributionActive(c) ? void 0 : metricExpression(c),
|
|
2603
|
+
expressionY: c.display?.kind === "scatter" ? c.expressionY : void 0,
|
|
2604
|
+
scope: c.scope ?? "allMatching",
|
|
2605
|
+
groupBy: c.groupBy,
|
|
2606
|
+
sort: c.sort,
|
|
2607
|
+
groupLimit: c.groupLimit,
|
|
2608
|
+
distribution: distributionActive(c) ? c.distribution : void 0
|
|
2609
|
+
})).sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0)
|
|
2610
|
+
);
|
|
2611
|
+
}
|
|
2612
|
+
function toMetricQuery(q, schema, execution) {
|
|
2613
|
+
const originalSchema = schema;
|
|
2614
|
+
const fields = new Map(schema.fields.map((f2) => [f2.name, f2]));
|
|
2615
|
+
for (const [name, prior] of fields)
|
|
2616
|
+
if (isComputedField(name))
|
|
2617
|
+
fields.set(name, {
|
|
2618
|
+
...prior,
|
|
2619
|
+
aggregate: { measure: false, groupable: false }
|
|
2620
|
+
});
|
|
2621
|
+
for (const [name, cap] of Object.entries(execution?.fields ?? {}))
|
|
2622
|
+
if (isComputedField(name)) {
|
|
2623
|
+
const prior = fields.get(name);
|
|
2624
|
+
fields.set(name, {
|
|
2625
|
+
...prior,
|
|
2626
|
+
name,
|
|
2627
|
+
label: prior?.label ?? name,
|
|
2628
|
+
type: cap.type,
|
|
2629
|
+
source: { kind: "backend" },
|
|
2630
|
+
filter: { enabled: false },
|
|
2631
|
+
sort: { enabled: cap.sort },
|
|
2632
|
+
aggregate: {
|
|
2633
|
+
...prior?.aggregate,
|
|
2634
|
+
measure: cap.measure,
|
|
2635
|
+
groupable: cap.group
|
|
2636
|
+
}
|
|
2637
|
+
});
|
|
2638
|
+
}
|
|
2639
|
+
schema = { ...schema, fields: [...fields.values()] };
|
|
2640
|
+
const raw = q;
|
|
2641
|
+
q = normalizeQueryState(q);
|
|
2642
|
+
const server = toServerQuery(q, schema);
|
|
2643
|
+
const metrics = (q.aggregations ?? []).map((c) => {
|
|
2644
|
+
const {
|
|
2645
|
+
label,
|
|
2646
|
+
layout,
|
|
2647
|
+
display,
|
|
2648
|
+
distribution,
|
|
2649
|
+
expression,
|
|
2650
|
+
expressionY,
|
|
2651
|
+
...rest
|
|
2652
|
+
} = c;
|
|
2653
|
+
return {
|
|
2654
|
+
...rest,
|
|
2655
|
+
...!distributionActive(c) && expression !== void 0 ? { expression } : {},
|
|
2656
|
+
...display?.kind === "scatter" ? {
|
|
2657
|
+
display: { kind: "scatter" },
|
|
2658
|
+
...expressionY !== void 0 ? { expressionY } : {}
|
|
2659
|
+
} : {},
|
|
2660
|
+
...distributionActive(c) && distribution ? { distribution } : {}
|
|
2661
|
+
};
|
|
2662
|
+
});
|
|
2663
|
+
const diagnostics = (q.aggregations ?? []).flatMap(
|
|
2664
|
+
(c) => validateMetric(c, schema).map((d) => ({
|
|
2665
|
+
...d,
|
|
2666
|
+
metricId: c.id,
|
|
2667
|
+
code: "invalid_metric"
|
|
2668
|
+
}))
|
|
2669
|
+
);
|
|
2670
|
+
try {
|
|
2671
|
+
assertMetricQueryState(raw, originalSchema);
|
|
2672
|
+
} catch (e) {
|
|
2673
|
+
diagnostics.push({
|
|
2674
|
+
message: String(e),
|
|
2675
|
+
from: 0,
|
|
2676
|
+
to: 1,
|
|
2677
|
+
metricId: "",
|
|
2678
|
+
code: "invalid_query"
|
|
2679
|
+
});
|
|
2680
|
+
}
|
|
2681
|
+
const ids = /* @__PURE__ */ new Set();
|
|
2682
|
+
for (const c of q.aggregations ?? []) {
|
|
2683
|
+
if (ids.has(c.id))
|
|
2684
|
+
diagnostics.push({
|
|
2685
|
+
message: "Duplicate metric ID.",
|
|
2686
|
+
from: 0,
|
|
2687
|
+
to: 1,
|
|
2688
|
+
metricId: c.id,
|
|
2689
|
+
code: "duplicate_metric"
|
|
2690
|
+
});
|
|
2691
|
+
ids.add(c.id);
|
|
2692
|
+
}
|
|
2693
|
+
if (server.where.length !== q.where.length || queryStateKey(raw.where) !== queryStateKey(q.where))
|
|
2694
|
+
diagnostics.push({
|
|
2695
|
+
message: "Metrics unavailable with residual client-only filters.",
|
|
2696
|
+
from: 0,
|
|
2697
|
+
to: 1,
|
|
2698
|
+
metricId: "",
|
|
2699
|
+
code: "residual_filter"
|
|
2700
|
+
});
|
|
2701
|
+
for (const c of q.aggregations ?? []) {
|
|
2702
|
+
try {
|
|
2703
|
+
for (const name of metricDependencies(c, schema))
|
|
2704
|
+
if (isComputedField(name)) {
|
|
2705
|
+
const revision = execution?.resolvedRevisions[name] ?? execution?.resolvedRevisions[name.slice("@computed/".length)];
|
|
2706
|
+
if (typeof revision !== "string" || !revision || revision.length > 256)
|
|
2707
|
+
diagnostics.push({
|
|
2708
|
+
metricId: c.id,
|
|
2709
|
+
code: "definition_revision",
|
|
2710
|
+
message: `Missing expected revision for ${name}.`,
|
|
2711
|
+
from: 0,
|
|
2712
|
+
to: 1
|
|
2713
|
+
});
|
|
2714
|
+
if (!execution?.fields[name])
|
|
2715
|
+
diagnostics.push({
|
|
2716
|
+
metricId: c.id,
|
|
2717
|
+
code: "unsupported_computed",
|
|
2718
|
+
message: `Missing server capability for ${name}.`,
|
|
2719
|
+
from: 0,
|
|
2720
|
+
to: 1
|
|
2721
|
+
});
|
|
2722
|
+
}
|
|
2723
|
+
} catch {
|
|
2724
|
+
}
|
|
2725
|
+
}
|
|
2726
|
+
const rowProjection = toServerQueryV2(
|
|
2727
|
+
{ ...q, select: [{ field: schema.idField }] },
|
|
2728
|
+
originalSchema,
|
|
2729
|
+
execution ?? {
|
|
2730
|
+
profile: "",
|
|
2731
|
+
planToken: "",
|
|
2732
|
+
fields: {},
|
|
2733
|
+
resolvedRevisions: {}
|
|
2734
|
+
}
|
|
2735
|
+
);
|
|
2736
|
+
if (q.aggregations?.some((c) => c.scope === "shownRows"))
|
|
2737
|
+
diagnostics.push(
|
|
2738
|
+
...rowProjection.diagnostics.map((d) => ({
|
|
2739
|
+
...d,
|
|
2740
|
+
metricId: "",
|
|
2741
|
+
code: d.code ?? "residual_order"
|
|
2742
|
+
}))
|
|
2743
|
+
);
|
|
2744
|
+
return {
|
|
2745
|
+
version: 2,
|
|
2746
|
+
where: server.where,
|
|
2747
|
+
orderBy: rowProjection.orderBy,
|
|
2748
|
+
limit: q.limit,
|
|
2749
|
+
offset: q.offset,
|
|
2750
|
+
metrics,
|
|
2751
|
+
diagnostics,
|
|
2752
|
+
...execution ? {
|
|
2753
|
+
profile: execution.profile,
|
|
2754
|
+
expectedRevisions: { ...execution.resolvedRevisions },
|
|
2755
|
+
planToken: execution.planToken,
|
|
2756
|
+
...execution.snapshot !== void 0 ? { snapshot: execution.snapshot } : {}
|
|
2757
|
+
} : {}
|
|
2758
|
+
};
|
|
2759
|
+
}
|
|
2760
|
+
function queryStateKey(value) {
|
|
2761
|
+
return JSON.stringify(
|
|
2762
|
+
value,
|
|
2763
|
+
(_key, item) => item && typeof item === "object" && !Array.isArray(item) ? Object.fromEntries(
|
|
2764
|
+
Object.entries(item).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)
|
|
2765
|
+
) : item
|
|
2766
|
+
);
|
|
2767
|
+
}
|
|
2768
|
+
function assertMetricQueryState(q, schema) {
|
|
2769
|
+
const normalized = normalizeQueryState(q);
|
|
2770
|
+
if ((q.aggregations?.length ?? 0) !== (normalized.aggregations?.length ?? 0))
|
|
2771
|
+
throw Error("Malformed metric definitions cannot be dropped.");
|
|
2772
|
+
const orderBy = q.orderBy.length ? q.orderBy : schema.defaultSort ?? [];
|
|
2773
|
+
const normalizedOrder = normalizeQueryState({ ...q, orderBy }).orderBy;
|
|
2774
|
+
if (queryStateKey(q.where) !== queryStateKey(normalized.where) || queryStateKey(orderBy) !== queryStateKey(normalizedOrder) || q.limit !== normalized.limit || q.offset !== normalized.offset)
|
|
2775
|
+
throw Error("Malformed metric query state cannot be executed exactly.");
|
|
2776
|
+
}
|
|
2777
|
+
function selectMetricShownRows(rows, q, schema, options = {}) {
|
|
2778
|
+
if (options.signal?.aborted) throw Error("Metric evaluation cancelled.");
|
|
2779
|
+
assertMetricQueryState(q, schema);
|
|
2780
|
+
if (options.shownRows) return options.shownRows;
|
|
2781
|
+
const orderBy = q.orderBy.length ? q.orderBy : schema.defaultSort ?? [];
|
|
2782
|
+
assertMetricPopulation(q, schema, orderBy);
|
|
2783
|
+
if (rows.length > (options.maxRows ?? 1e6))
|
|
2784
|
+
throw Error("Metric row budget exceeded.");
|
|
2785
|
+
if ((options.maxOperations ?? 1e7) <= 0)
|
|
2786
|
+
throw Error("Metric operation budget exceeded.");
|
|
2787
|
+
return applyQuery(
|
|
2788
|
+
rows,
|
|
2789
|
+
{
|
|
2790
|
+
...q,
|
|
2791
|
+
orderBy: orderBy.some((s) => s.field === schema.idField) ? orderBy : [...orderBy, { field: schema.idField, dir: "asc" }]
|
|
2792
|
+
},
|
|
2793
|
+
schema,
|
|
2794
|
+
options
|
|
2795
|
+
).rows;
|
|
2796
|
+
}
|
|
2797
|
+
function assertMetricPopulation(q, schema, orderBy) {
|
|
2798
|
+
const fields = indexFields(schema);
|
|
2799
|
+
if (q.where.some(
|
|
2800
|
+
(t) => predicatesOf(t).some(
|
|
2801
|
+
(p) => p.op === "matches_regex" || p.op === "not_matches_regex"
|
|
2802
|
+
)
|
|
2803
|
+
) || orderBy.some((s) => s.extract !== void 0))
|
|
2804
|
+
throw Error(
|
|
2805
|
+
"Query regex requires isolated execution; unavailable for metric population selection."
|
|
2806
|
+
);
|
|
2807
|
+
if (q.where.some(
|
|
2808
|
+
(t) => predicatesOf(t).some(
|
|
2809
|
+
(p) => isComputedField(p.field) || !fields.has(resolveFieldName(schema, p.field) ?? p.field)
|
|
2810
|
+
)
|
|
2811
|
+
))
|
|
2812
|
+
throw Error("Metric filter references an unavailable field.");
|
|
2813
|
+
if (orderBy.some(
|
|
2814
|
+
(s) => isComputedField(s.field) || !fields.has(resolveFieldName(schema, s.field) ?? s.field)
|
|
2815
|
+
))
|
|
2816
|
+
throw Error("Shown-row ordering requires an authoritative committed page.");
|
|
2817
|
+
}
|
|
2818
|
+
var scalar = (v) => {
|
|
2819
|
+
if (v instanceof Date) return v.toISOString();
|
|
2820
|
+
if (v == null) return null;
|
|
2821
|
+
if (typeof v === "string" || typeof v === "boolean" || typeof v === "number" && Number.isFinite(v))
|
|
2822
|
+
return v;
|
|
2823
|
+
throw Error("Metric keys and values must be finite scalars.");
|
|
2824
|
+
};
|
|
2825
|
+
function groupStringLength(value, limit) {
|
|
2826
|
+
let length = value.length + 2;
|
|
2827
|
+
if (length > limit)
|
|
2828
|
+
throw Error("Metric grouping key length budget exceeded.");
|
|
2829
|
+
for (let i = 0; i < value.length; i++) {
|
|
2830
|
+
const code = value.charCodeAt(i);
|
|
2831
|
+
if (code === 34 || code === 92 || code === 8 || code === 9 || code === 10 || code === 12 || code === 13)
|
|
2832
|
+
length++;
|
|
2833
|
+
else if (code < 32) length += 5;
|
|
2834
|
+
else if (code >= 55296 && code <= 56319 && i + 1 < value.length && value.charCodeAt(i + 1) >= 56320 && value.charCodeAt(i + 1) <= 57343)
|
|
2835
|
+
i++;
|
|
2836
|
+
else if (code >= 55296 && code <= 57343) length += 5;
|
|
2837
|
+
if (length > limit)
|
|
2838
|
+
throw Error("Metric grouping key length budget exceeded.");
|
|
2839
|
+
}
|
|
2840
|
+
return length;
|
|
2841
|
+
}
|
|
2842
|
+
function compareMetricValues(a, b) {
|
|
2843
|
+
if (a === b) return 0;
|
|
2844
|
+
if (a === null) return 1;
|
|
2845
|
+
if (b === null) return -1;
|
|
2846
|
+
if (typeof a !== typeof b) return typeof a < typeof b ? -1 : 1;
|
|
2847
|
+
if (typeof a === "string" && typeof b === "string") {
|
|
2848
|
+
const ac = Array.from(a), bc = Array.from(b);
|
|
2849
|
+
for (let i = 0; i < Math.min(ac.length, bc.length); i++) {
|
|
2850
|
+
const diff = ac[i].codePointAt(0) - bc[i].codePointAt(0);
|
|
2851
|
+
if (diff) return Math.sign(diff);
|
|
2852
|
+
}
|
|
2853
|
+
return Math.sign(ac.length - bc.length);
|
|
2854
|
+
}
|
|
2855
|
+
return a < b ? -1 : 1;
|
|
2856
|
+
}
|
|
2857
|
+
function evaluateMetrics(rows, q, schema, options = {}) {
|
|
2858
|
+
const now = evaluationTime(options), fields = indexFields(schema);
|
|
2859
|
+
let matching;
|
|
2860
|
+
let shown;
|
|
2861
|
+
return {
|
|
2862
|
+
metrics: (q.aggregations ?? []).map((c) => {
|
|
2863
|
+
let budget = options.maxOperations ?? 1e7;
|
|
2864
|
+
const tick = () => {
|
|
2865
|
+
if (--budget < 0) throw Error("Metric operation budget exceeded.");
|
|
2866
|
+
if (options.signal?.aborted)
|
|
2867
|
+
throw Error("Metric evaluation cancelled.");
|
|
2868
|
+
};
|
|
2869
|
+
try {
|
|
2870
|
+
if (options.signal?.aborted)
|
|
2871
|
+
throw Error("Metric evaluation cancelled.");
|
|
2872
|
+
assertMetricQueryState(q, schema);
|
|
2873
|
+
if ((q.aggregations?.length ?? 0) > 20)
|
|
2874
|
+
throw Error("At most 20 metrics are allowed.");
|
|
2875
|
+
const diagnostics = validateMetric(c, schema, options.resolveComputed);
|
|
2876
|
+
if (diagnostics.length)
|
|
2877
|
+
throw Error(diagnostics.map((d) => d.message).join(" "));
|
|
2878
|
+
const ps = metricPlans(c, schema, options.resolveComputed);
|
|
2879
|
+
if (ps.some(hasRegex) && !options.evaluateInput)
|
|
2880
|
+
throw Error(
|
|
2881
|
+
"Regex metrics require an isolated worker with a watchdog."
|
|
2882
|
+
);
|
|
2883
|
+
let inputRows;
|
|
2884
|
+
if (c.scope === "shownRows")
|
|
2885
|
+
inputRows = shown ?? (shown = selectMetricShownRows(rows, q, schema, options));
|
|
2886
|
+
else {
|
|
2887
|
+
if (!matching) {
|
|
2888
|
+
assertMetricPopulation(q, schema, []);
|
|
2889
|
+
if (rows.length > (options.maxRows ?? 1e6))
|
|
2890
|
+
throw Error("Metric row budget exceeded.");
|
|
2891
|
+
matching = applyQuery(
|
|
2892
|
+
rows,
|
|
2893
|
+
{ ...q, orderBy: [], offset: 0, limit: Number.MAX_SAFE_INTEGER },
|
|
2894
|
+
schema,
|
|
2895
|
+
{ now }
|
|
2896
|
+
).rows;
|
|
2897
|
+
}
|
|
2898
|
+
inputRows = matching;
|
|
2899
|
+
}
|
|
2900
|
+
if (inputRows.length > (options.maxRows ?? 1e6))
|
|
2901
|
+
throw Error("Metric row budget exceeded.");
|
|
2902
|
+
const groups = /* @__PURE__ */ new Map();
|
|
2903
|
+
const keyLimit = options.maxGroupKeyLength ?? 65536, byteLimit = options.maxGroupBytes ?? 16e6;
|
|
2904
|
+
if (![keyLimit, byteLimit].every((v) => Number.isSafeInteger(v) && v >= 0))
|
|
2905
|
+
throw Error("Invalid grouping budget.");
|
|
2906
|
+
let groupBytes = 0;
|
|
2907
|
+
const reserve = (bytes) => {
|
|
2908
|
+
if (bytes > byteLimit - groupBytes)
|
|
2909
|
+
throw Error("Metric grouping retained-memory budget exceeded.");
|
|
2910
|
+
};
|
|
2911
|
+
if (!c.groupBy.length) {
|
|
2912
|
+
if (keyLimit < 2)
|
|
2913
|
+
throw Error("Metric grouping key length budget exceeded.");
|
|
2914
|
+
reserve(256);
|
|
2915
|
+
groupBytes = 256;
|
|
2916
|
+
groups.set("[]", { keys: [], rows: [] });
|
|
2917
|
+
}
|
|
2918
|
+
for (const row of inputRows) {
|
|
2919
|
+
tick();
|
|
2920
|
+
if (!c.groupBy.length) {
|
|
2921
|
+
reserve(16);
|
|
2922
|
+
groupBytes += 16;
|
|
2923
|
+
groups.get("[]").rows.push(row);
|
|
2924
|
+
continue;
|
|
2925
|
+
}
|
|
2926
|
+
const keys = c.groupBy.map(
|
|
2927
|
+
(k) => scalar(
|
|
2928
|
+
readFieldValue(
|
|
2929
|
+
fields.get(resolveFieldName(schema, k) ?? k),
|
|
2930
|
+
row
|
|
2931
|
+
)
|
|
2932
|
+
)
|
|
2933
|
+
);
|
|
2934
|
+
let keyLength = 1, payloadBytes = 0;
|
|
2935
|
+
for (const value of keys) {
|
|
2936
|
+
keyLength += 1 + (typeof value === "string" ? groupStringLength(value, keyLimit - keyLength - 1) : value === null ? 4 : typeof value === "boolean" ? value ? 4 : 5 : String(value).length);
|
|
2937
|
+
payloadBytes += typeof value === "string" ? 2 * value.length : 8;
|
|
2938
|
+
if (keyLength > keyLimit)
|
|
2939
|
+
throw Error("Metric grouping key length budget exceeded.");
|
|
2940
|
+
}
|
|
2941
|
+
const retainedBytes = 256 + 16 * keys.length + payloadBytes + 2 * keyLength;
|
|
2942
|
+
reserve(retainedBytes + 2 * keyLength + 16);
|
|
2943
|
+
const key = JSON.stringify(keys);
|
|
2944
|
+
let g = groups.get(key);
|
|
2945
|
+
if (!g) {
|
|
2946
|
+
if (groups.size >= (options.maxGroups ?? 1e4))
|
|
2947
|
+
throw Error("Metric group budget exceeded.");
|
|
2948
|
+
g = { keys, rows: [] };
|
|
2949
|
+
groups.set(key, g);
|
|
2950
|
+
groupBytes += retainedBytes;
|
|
2951
|
+
}
|
|
2952
|
+
groupBytes += 16;
|
|
2953
|
+
g.rows.push(row);
|
|
2954
|
+
}
|
|
2955
|
+
const inputs = (row, p) => Object.fromEntries(
|
|
2956
|
+
p.dependencies.map((name) => [
|
|
2957
|
+
name,
|
|
2958
|
+
(() => {
|
|
2959
|
+
const value = readFieldValue(fields.get(name), row);
|
|
2960
|
+
return value instanceof Date ? value.toISOString() : value ?? null;
|
|
2961
|
+
})()
|
|
2962
|
+
])
|
|
2963
|
+
);
|
|
2964
|
+
const nodeCosts = /* @__PURE__ */ new Map();
|
|
2965
|
+
const cost = (n) => {
|
|
2966
|
+
const known = nodeCosts.get(n);
|
|
2967
|
+
if (known !== void 0) return known;
|
|
2968
|
+
const value = 1 + (n.kind === "call" ? n.args.reduce((s, a) => s + cost(a), 0) : 0);
|
|
2969
|
+
nodeCosts.set(n, value);
|
|
2970
|
+
return value;
|
|
2971
|
+
};
|
|
2972
|
+
const rowEval = (node, row, p) => {
|
|
2973
|
+
budget -= cost(node);
|
|
2974
|
+
tick();
|
|
2975
|
+
return options.evaluateInput && hasRegex({ ...p, ast: node }) ? options.evaluateInput(node, row, p) : formulaRuntime(node, inputs(row, p));
|
|
2976
|
+
};
|
|
2977
|
+
const reduce = (p, rs, inputErrors) => {
|
|
2978
|
+
const cache = /* @__PURE__ */ new Map();
|
|
2979
|
+
return checkedMetricResult(
|
|
2980
|
+
formulaRuntime(p.ast, {}, (node) => {
|
|
2981
|
+
if (!aggregates.has(node.name)) return void 0;
|
|
2982
|
+
if (cache.has(node)) return cache.get(node);
|
|
2983
|
+
let result;
|
|
2984
|
+
try {
|
|
2985
|
+
function* results() {
|
|
2986
|
+
for (let i = 0; i < rs.length; i++) {
|
|
2987
|
+
tick();
|
|
2988
|
+
const v = node.args[0] ? rowEval(node.args[0], rs[i], p) : { value: 1 };
|
|
2989
|
+
if (v.error) inputErrors.add(i);
|
|
2990
|
+
yield v;
|
|
2991
|
+
}
|
|
2992
|
+
}
|
|
2993
|
+
result = reduceResults(node, results(), options);
|
|
2994
|
+
} catch (e) {
|
|
2995
|
+
result = {
|
|
2996
|
+
value: null,
|
|
2997
|
+
error: e instanceof Error ? e.message : String(e)
|
|
2998
|
+
};
|
|
2999
|
+
}
|
|
3000
|
+
cache.set(node, result);
|
|
3001
|
+
return result;
|
|
3002
|
+
})
|
|
3003
|
+
);
|
|
3004
|
+
};
|
|
3005
|
+
const populations = /* @__PURE__ */ new Map();
|
|
3006
|
+
let buckets = [...groups.values()].map((g) => {
|
|
3007
|
+
const bucket = {
|
|
3008
|
+
keys: g.keys,
|
|
3009
|
+
count: g.rows.length,
|
|
3010
|
+
value: null
|
|
3011
|
+
};
|
|
3012
|
+
if (distributionActive(c)) {
|
|
3013
|
+
const values = [];
|
|
3014
|
+
bucket.nullCount = 0;
|
|
3015
|
+
bucket.inputErrorCount = 0;
|
|
3016
|
+
for (const row of g.rows) {
|
|
3017
|
+
const r = checkedMetricResult(rowEval(ps[0].ast, row, ps[0]));
|
|
3018
|
+
if (r.error) {
|
|
3019
|
+
bucket.inputErrorCount++;
|
|
3020
|
+
bucket.error = r.error;
|
|
3021
|
+
} else if (r.value === null) bucket.nullCount++;
|
|
3022
|
+
else values.push(Number(r.value));
|
|
3023
|
+
}
|
|
3024
|
+
populations.set(bucket, values);
|
|
3025
|
+
} else {
|
|
3026
|
+
const inputErrors = /* @__PURE__ */ new Set();
|
|
3027
|
+
const x = reduce(ps[0], g.rows, inputErrors);
|
|
3028
|
+
bucket.value = scalar(x.value);
|
|
3029
|
+
if (x.error) bucket.error = x.error;
|
|
3030
|
+
if (ps[1]) {
|
|
3031
|
+
const y = reduce(ps[1], g.rows, inputErrors);
|
|
3032
|
+
bucket.y = scalar(y.value);
|
|
3033
|
+
if (y.error) bucket.yError = y.error;
|
|
3034
|
+
}
|
|
3035
|
+
if (inputErrors.size) bucket.inputErrorCount = inputErrors.size;
|
|
3036
|
+
}
|
|
3037
|
+
return bucket;
|
|
3038
|
+
});
|
|
3039
|
+
if (distributionActive(c)) {
|
|
3040
|
+
const d = c.distribution;
|
|
3041
|
+
if (d.kind === "histogram" && buckets.some((b) => b.error))
|
|
3042
|
+
throw Error(
|
|
3043
|
+
"Histogram input errors prevent an exact shared extent."
|
|
3044
|
+
);
|
|
3045
|
+
const all = [];
|
|
3046
|
+
for (const values of populations.values())
|
|
3047
|
+
for (const v of values) all.push(v);
|
|
3048
|
+
const edges = d.kind === "histogram" ? histogramEdges(all, d.bins ?? 10) : [];
|
|
3049
|
+
for (const b of buckets) {
|
|
3050
|
+
const vs = populations.get(b);
|
|
3051
|
+
if (b.error) continue;
|
|
3052
|
+
if (d.kind === "box") {
|
|
3053
|
+
const summary = boxSummary(vs, d.whiskers);
|
|
3054
|
+
b.distribution = { kind: "box", summary };
|
|
3055
|
+
b.value = summary?.median ?? null;
|
|
3056
|
+
} else {
|
|
3057
|
+
b.distribution = {
|
|
3058
|
+
kind: "histogram",
|
|
3059
|
+
edges: [...edges],
|
|
3060
|
+
counts: histogramCounts(vs, edges),
|
|
3061
|
+
n: vs.length
|
|
3062
|
+
};
|
|
3063
|
+
b.value = vs.length;
|
|
3064
|
+
}
|
|
3065
|
+
}
|
|
3066
|
+
}
|
|
3067
|
+
const ordering = c.sort?.length ? c.sort : [{ key: "value", dir: "desc", nulls: "last" }];
|
|
3068
|
+
const target = (b, s) => s.key === "samples" ? b.count - (b.nullCount ?? 0) - (b.inputErrorCount ?? 0) : s.key.startsWith("group") ? b.keys[Number(s.key.slice(5))] ?? null : s.key === "count" ? b.count : s.key === "y" ? b.y ?? null : b.value;
|
|
3069
|
+
buckets.sort((a, b) => {
|
|
3070
|
+
if (!!(a.error || a.yError) !== !!(b.error || b.yError))
|
|
3071
|
+
return a.error || a.yError ? 1 : -1;
|
|
3072
|
+
for (const s of ordering) {
|
|
3073
|
+
const av = target(a, s), bv = target(b, s);
|
|
3074
|
+
const cmp = av === null || bv === null ? av === bv ? 0 : (av === null ? 1 : -1) * (s.nulls === "first" ? -1 : 1) : compareMetricValues(av, bv) * (s.dir === "asc" ? 1 : -1);
|
|
3075
|
+
if (cmp) return cmp;
|
|
3076
|
+
}
|
|
3077
|
+
for (let i = 0; i < a.keys.length; i++) {
|
|
3078
|
+
const cmp = compareMetricValues(a.keys[i], b.keys[i]);
|
|
3079
|
+
if (cmp) return cmp;
|
|
3080
|
+
}
|
|
3081
|
+
return 0;
|
|
3082
|
+
});
|
|
3083
|
+
tick();
|
|
3084
|
+
const groupCount = buckets.length;
|
|
3085
|
+
let other;
|
|
3086
|
+
if (c.groupLimit && buckets.length > c.groupLimit) {
|
|
3087
|
+
const omitted = buckets.slice(c.groupLimit), ast = ps[0].ast;
|
|
3088
|
+
if (!distributionActive(c) && ast.kind === "call" && ["SUM", "COUNT"].includes(ast.name) && omitted.every(
|
|
3089
|
+
(b) => !b.error && (typeof b.value === "number" || ast.name === "SUM" && b.value === null)
|
|
3090
|
+
)) {
|
|
3091
|
+
const value = omitted.reduce((s, b) => s + Number(b.value), 0);
|
|
3092
|
+
if (Number.isFinite(value) && (!Number.isInteger(value) || Number.isSafeInteger(value)))
|
|
3093
|
+
other = {
|
|
3094
|
+
keys: [],
|
|
3095
|
+
value,
|
|
3096
|
+
count: omitted.reduce((s, b) => s + b.count, 0)
|
|
3097
|
+
};
|
|
3098
|
+
}
|
|
3099
|
+
buckets = buckets.slice(0, c.groupLimit);
|
|
3100
|
+
}
|
|
3101
|
+
return {
|
|
3102
|
+
id: c.id,
|
|
3103
|
+
buckets,
|
|
3104
|
+
scope: c.scope ?? "allMatching",
|
|
3105
|
+
processedRows: inputRows.length,
|
|
3106
|
+
groupCount,
|
|
3107
|
+
coverage: "exact",
|
|
3108
|
+
...other ? { other } : {}
|
|
3109
|
+
};
|
|
3110
|
+
} catch (e) {
|
|
3111
|
+
return {
|
|
3112
|
+
id: c.id,
|
|
3113
|
+
buckets: [],
|
|
3114
|
+
error: e instanceof Error ? e.message : String(e)
|
|
3115
|
+
};
|
|
3116
|
+
}
|
|
3117
|
+
})
|
|
3118
|
+
};
|
|
3119
|
+
}
|
|
3120
|
+
|
|
3121
|
+
// src/apply.ts
|
|
3122
|
+
function applyQuery(rows, q, schema, options = {}) {
|
|
3123
|
+
const now = evaluationTime(options);
|
|
3124
|
+
const byName = indexFields(schema);
|
|
3125
|
+
const resolveField2 = (name) => resolveFieldName(schema, name) ?? name;
|
|
3126
|
+
const whereTerms = q.where.filter((term) => !predicatesOf(term).some((c) => isComputedField(c.field))).map((term) => prepareWhereTerm(term, resolveField2, now));
|
|
3127
|
+
prepareDatetimePredicates(whereTerms, byName);
|
|
3128
|
+
const orderBy = q.orderBy.filter((term) => !isComputedField(term.field)).map((term) => ({ ...term, field: resolveField2(term.field) }));
|
|
3129
|
+
let out = rows.filter(
|
|
3130
|
+
(row) => whereTerms.every((term) => matchesTerm(byName, row, term))
|
|
3131
|
+
);
|
|
3132
|
+
const total = out.length;
|
|
3133
|
+
if (orderBy.length) {
|
|
3134
|
+
const terms = orderBy.map((t) => ({
|
|
3135
|
+
field: byName.get(t.field),
|
|
3136
|
+
dir: t.dir,
|
|
3137
|
+
nullsLast: (t.nulls ?? "last") === "last",
|
|
3138
|
+
extract: compileRegex(t.extract?.regex)
|
|
3139
|
+
})).filter(
|
|
3140
|
+
(t) => t.field != null
|
|
3141
|
+
);
|
|
3142
|
+
out = [...out].sort((a, b) => {
|
|
3143
|
+
for (const t of terms) {
|
|
3144
|
+
const av = extractSortValue(readFieldValue(t.field, a), t.extract);
|
|
3145
|
+
const bv = extractSortValue(readFieldValue(t.field, b), t.extract);
|
|
3146
|
+
const aNull = av == null;
|
|
3147
|
+
const bNull = bv == null;
|
|
3148
|
+
if (aNull || bNull) {
|
|
3149
|
+
if (aNull && bNull) continue;
|
|
3150
|
+
return (aNull ? 1 : -1) * (t.nullsLast ? 1 : -1);
|
|
3151
|
+
}
|
|
3152
|
+
const cmp = compare(av, bv);
|
|
3153
|
+
if (cmp !== 0) return t.dir === "asc" ? cmp : -cmp;
|
|
3154
|
+
}
|
|
3155
|
+
return 0;
|
|
3156
|
+
});
|
|
3157
|
+
}
|
|
3158
|
+
const start = q.offset > 0 ? q.offset : 0;
|
|
3159
|
+
const end = q.limit > 0 ? start + q.limit : out.length;
|
|
3160
|
+
return { rows: out.slice(start, end), total };
|
|
3161
|
+
}
|
|
3162
|
+
function matchesClause(row, clause, schema, options = {}) {
|
|
3163
|
+
const field = resolveFieldName(schema, clause.field) ?? clause.field;
|
|
3164
|
+
const byName = indexFields(schema);
|
|
3165
|
+
const prepared = prepareWhereClause(
|
|
3166
|
+
{ ...clause, field },
|
|
3167
|
+
evaluationTime(options)
|
|
3168
|
+
);
|
|
3169
|
+
prepareDatetimePredicates([{ kind: "lit", predicate: prepared }], byName);
|
|
3170
|
+
return matchesWith(byName, row, prepared);
|
|
3171
|
+
}
|
|
3172
|
+
function applyAggregations(rows, q, schema, options = {}) {
|
|
3173
|
+
if (options.signal?.aborted || q.where.some(
|
|
3174
|
+
(t) => predicatesOf(t).some(
|
|
3175
|
+
(p) => p.op === "matches_regex" || p.op === "not_matches_regex"
|
|
3176
|
+
)
|
|
3177
|
+
))
|
|
3178
|
+
return evaluateMetrics(rows, q, schema, options);
|
|
3179
|
+
const advanced = (q.aggregations ?? []).filter(
|
|
3180
|
+
(a) => a.diagnostics?.length || a.expression !== void 0 || a.expressionY !== void 0 || a.scope !== void 0 || a.distribution || a.sort || a.groupLimit !== void 0
|
|
3181
|
+
);
|
|
3182
|
+
if (advanced.length) {
|
|
3183
|
+
if ((q.aggregations?.length ?? 0) > 20 || normalizeQueryState(q).aggregations?.length !== q.aggregations?.length)
|
|
3184
|
+
return evaluateMetrics(rows, q, schema, options);
|
|
3185
|
+
const legacy = (q.aggregations ?? []).filter((a) => !advanced.includes(a));
|
|
3186
|
+
const results = [
|
|
3187
|
+
...evaluateMetrics(
|
|
3188
|
+
rows,
|
|
3189
|
+
{ ...q, aggregations: advanced },
|
|
3190
|
+
schema,
|
|
3191
|
+
options
|
|
3192
|
+
).metrics,
|
|
3193
|
+
...legacy.length ? applyAggregations(
|
|
3194
|
+
rows,
|
|
3195
|
+
{ ...q, aggregations: legacy },
|
|
3196
|
+
schema,
|
|
3197
|
+
options
|
|
3198
|
+
).metrics : []
|
|
3199
|
+
];
|
|
3200
|
+
const byId = new Map(results.map((result) => [result.id, result]));
|
|
3201
|
+
return {
|
|
3202
|
+
metrics: (q.aggregations ?? []).flatMap((a) => byId.get(a.id) ?? [])
|
|
3203
|
+
};
|
|
3204
|
+
}
|
|
3205
|
+
const now = evaluationTime(options);
|
|
3206
|
+
const byName = indexFields(schema);
|
|
3207
|
+
const resolveField2 = (name) => resolveFieldName(schema, name) ?? name;
|
|
3208
|
+
const whereTerms = q.where.filter((term) => !predicatesOf(term).some((c) => isComputedField(c.field))).map((term) => prepareWhereTerm(term, resolveField2, now));
|
|
3209
|
+
prepareDatetimePredicates(whereTerms, byName);
|
|
3210
|
+
const filtered = rows.filter(
|
|
3211
|
+
(row) => whereTerms.every((term) => matchesTerm(byName, row, term))
|
|
3212
|
+
);
|
|
3213
|
+
const metrics = (q.aggregations ?? []).filter(
|
|
3214
|
+
(a) => !isComputedField(a.field ?? "") && !a.groupBy.some(isComputedField)
|
|
3215
|
+
).map((agg) => {
|
|
3216
|
+
const field = agg.field ? byName.get(resolveField2(agg.field)) : void 0;
|
|
3217
|
+
if (field && (!isMeasurable(field) || !aggOpsForField(field).includes(agg.op)))
|
|
3218
|
+
return {
|
|
3219
|
+
id: agg.id,
|
|
3220
|
+
buckets: [],
|
|
3221
|
+
error: `Field ${field.name} does not allow ${agg.op}.`
|
|
3222
|
+
};
|
|
3223
|
+
return {
|
|
3224
|
+
id: agg.id,
|
|
3225
|
+
buckets: computeBuckets(filtered, agg, byName, resolveField2)
|
|
3226
|
+
};
|
|
3227
|
+
});
|
|
3228
|
+
return { metrics };
|
|
3229
|
+
}
|
|
3230
|
+
function computeBuckets(rows, agg, byName, resolveField2) {
|
|
3231
|
+
const groupFields = agg.groupBy.map((n) => byName.get(resolveField2(n)));
|
|
3232
|
+
const measure = agg.field ? byName.get(resolveField2(agg.field)) : void 0;
|
|
3233
|
+
const groups = /* @__PURE__ */ new Map();
|
|
3234
|
+
if (!agg.groupBy.length) groups.set("[]", { keys: [], rows: [] });
|
|
3235
|
+
for (const row of rows) {
|
|
3236
|
+
const keys = groupFields.map((f2) => {
|
|
3237
|
+
if (!f2) return null;
|
|
3238
|
+
const v = readFieldValue(f2, row);
|
|
3239
|
+
return v == null || v === "" ? null : typeof v === "number" || typeof v === "boolean" || typeof v === "string" ? v : String(v);
|
|
3240
|
+
});
|
|
3241
|
+
const k = JSON.stringify(keys);
|
|
3242
|
+
let g = groups.get(k);
|
|
3243
|
+
if (!g) {
|
|
3244
|
+
g = { keys, rows: [] };
|
|
3245
|
+
groups.set(k, g);
|
|
3246
|
+
}
|
|
3247
|
+
g.rows.push(row);
|
|
3248
|
+
}
|
|
3249
|
+
const buckets = [...groups.values()].map((g) => ({
|
|
3250
|
+
keys: g.keys,
|
|
3251
|
+
count: g.rows.length,
|
|
3252
|
+
value: aggValue(agg.op, measure, g.rows)
|
|
3253
|
+
}));
|
|
3254
|
+
return sortBuckets(buckets);
|
|
3255
|
+
}
|
|
3256
|
+
function aggValue(op, measure, rows) {
|
|
3257
|
+
if (op === "count") {
|
|
3258
|
+
if (!measure) return rows.length;
|
|
3259
|
+
let n = 0;
|
|
3260
|
+
for (const r of rows) if (readFieldValue(measure, r) != null) n++;
|
|
3261
|
+
return n;
|
|
3262
|
+
}
|
|
3263
|
+
if (!measure) return null;
|
|
3264
|
+
const values = rows.map((r) => readFieldValue(measure, r)).filter((v) => v != null);
|
|
3265
|
+
switch (op) {
|
|
3266
|
+
case "count_distinct":
|
|
3267
|
+
return new Set(values.map((v) => String(v))).size;
|
|
3268
|
+
case "sum":
|
|
3269
|
+
case "avg": {
|
|
3270
|
+
let sum = 0;
|
|
3271
|
+
let n = 0;
|
|
3272
|
+
for (const v of values) {
|
|
3273
|
+
const num = typeof v === "number" ? v : Number(v);
|
|
3274
|
+
if (Number.isFinite(num)) {
|
|
3275
|
+
sum += num;
|
|
3276
|
+
n++;
|
|
3277
|
+
}
|
|
3278
|
+
}
|
|
3279
|
+
if (op === "avg") return n ? sum / n : null;
|
|
3280
|
+
return n ? sum : null;
|
|
3281
|
+
}
|
|
3282
|
+
case "min":
|
|
3283
|
+
case "max": {
|
|
3284
|
+
let best;
|
|
3285
|
+
for (const v of values) {
|
|
3286
|
+
if (best === void 0) {
|
|
3287
|
+
best = v;
|
|
3288
|
+
continue;
|
|
3289
|
+
}
|
|
3290
|
+
const cmp = compare(v, best);
|
|
3291
|
+
if (op === "min" ? cmp < 0 : cmp > 0) best = v;
|
|
3292
|
+
}
|
|
3293
|
+
return best ?? null;
|
|
3294
|
+
}
|
|
3295
|
+
default:
|
|
3296
|
+
return null;
|
|
3297
|
+
}
|
|
3298
|
+
}
|
|
3299
|
+
function sortBuckets(buckets) {
|
|
3300
|
+
return [...buckets].sort((a, b) => {
|
|
3301
|
+
const av = typeof a.value === "number" ? a.value : a.count;
|
|
3302
|
+
const bv = typeof b.value === "number" ? b.value : b.count;
|
|
3303
|
+
if (av !== bv) return bv - av;
|
|
3304
|
+
return JSON.stringify(a.keys).localeCompare(JSON.stringify(b.keys));
|
|
3305
|
+
});
|
|
3306
|
+
}
|
|
3307
|
+
function prepareWhereClause(clause, now) {
|
|
3308
|
+
const usesRegex = clause.op === "matches_regex" || clause.op === "not_matches_regex";
|
|
3309
|
+
return {
|
|
3310
|
+
clause,
|
|
3311
|
+
now,
|
|
3312
|
+
regex: usesRegex ? compileRegex(clause.value) : void 0
|
|
3313
|
+
};
|
|
3314
|
+
}
|
|
3315
|
+
function prepareWhereTerm(term, resolveField2, now) {
|
|
3316
|
+
if (isOrGroup(term)) {
|
|
3317
|
+
return {
|
|
3318
|
+
kind: "or",
|
|
3319
|
+
predicates: term.any.map(
|
|
3320
|
+
(c) => prepareWhereClause({ ...c, field: resolveField2(c.field) }, now)
|
|
3321
|
+
)
|
|
3322
|
+
};
|
|
3323
|
+
}
|
|
3324
|
+
return {
|
|
3325
|
+
kind: "lit",
|
|
3326
|
+
predicate: prepareWhereClause(
|
|
3327
|
+
{ ...term, field: resolveField2(term.field) },
|
|
3328
|
+
now
|
|
3329
|
+
)
|
|
3330
|
+
};
|
|
3331
|
+
}
|
|
3332
|
+
function prepareDatetimePredicates(terms, fields) {
|
|
3333
|
+
for (const term of terms)
|
|
3334
|
+
for (const p of term.kind === "lit" ? [term.predicate] : term.predicates) {
|
|
3335
|
+
const field = fields.get(p.clause.field);
|
|
3336
|
+
if (field?.type !== "datetime" || p.clause.value === "" || p.clause.op === "is_null" || p.clause.op === "is_not_null")
|
|
3337
|
+
continue;
|
|
3338
|
+
p.datetime = coerceValue("datetime", p.clause.value, p.now);
|
|
3339
|
+
}
|
|
3340
|
+
}
|
|
3341
|
+
function matchesTerm(byName, row, term) {
|
|
3342
|
+
if (term.kind === "lit") return matchesWith(byName, row, term.predicate);
|
|
3343
|
+
return term.predicates.some((p) => matchesWith(byName, row, p));
|
|
3344
|
+
}
|
|
3345
|
+
function matchesWith(byName, row, prepared) {
|
|
3346
|
+
const { clause } = prepared;
|
|
3347
|
+
const field = byName.get(clause.field);
|
|
3348
|
+
if (!field) return true;
|
|
3349
|
+
const v = readFieldValue(field, row);
|
|
3350
|
+
if (clause.value === "" && clause.op !== "=" && clause.op !== "!=" && clause.op !== "is_null" && clause.op !== "is_not_null") {
|
|
3351
|
+
return true;
|
|
3352
|
+
}
|
|
3353
|
+
if (clause.op === "length_gt" || clause.op === "length_lt" || clause.op === "length_eq") {
|
|
3354
|
+
if (!/^(0|[1-9]\d*)$/.test(clause.value) || !Number.isSafeInteger(Number(clause.value)))
|
|
3355
|
+
return false;
|
|
3356
|
+
}
|
|
3357
|
+
let base;
|
|
3358
|
+
if (prepared.datetime !== void 0) {
|
|
3359
|
+
const timestamp = v instanceof Date ? v.getTime() : typeof v === "number" ? v : typeof v === "string" ? Date.parse(v) : NaN;
|
|
3360
|
+
if (v === "" || !Number.isFinite(timestamp)) return false;
|
|
3361
|
+
base = matchesDatetime(timestamp, prepared.datetime, clause.op);
|
|
3362
|
+
} else {
|
|
3363
|
+
base = matchesBase(field, v, prepared);
|
|
3364
|
+
}
|
|
3365
|
+
if (!clause.negated) return base;
|
|
3366
|
+
const isNull = Array.isArray(v) ? v.length === 0 : v == null || v === "";
|
|
3367
|
+
return !isNull && !base;
|
|
3368
|
+
}
|
|
3369
|
+
function matchesDatetime(timestamp, rhs, op) {
|
|
3370
|
+
switch (op) {
|
|
3371
|
+
case "=":
|
|
3372
|
+
return timestamp === rhs;
|
|
3373
|
+
case "!=":
|
|
3374
|
+
return timestamp !== rhs;
|
|
3375
|
+
case ">":
|
|
3376
|
+
return timestamp > rhs;
|
|
3377
|
+
case ">=":
|
|
3378
|
+
return timestamp >= rhs;
|
|
3379
|
+
case "<":
|
|
3380
|
+
return timestamp < rhs;
|
|
3381
|
+
case "<=":
|
|
3382
|
+
return timestamp <= rhs;
|
|
3383
|
+
default:
|
|
3384
|
+
return false;
|
|
3385
|
+
}
|
|
3386
|
+
}
|
|
3387
|
+
function matchesBase(field, v, prepared) {
|
|
3388
|
+
const { clause, regex } = prepared;
|
|
3389
|
+
if (clause.op === "is_null")
|
|
3390
|
+
return Array.isArray(v) ? v.length === 0 : v == null || v === "";
|
|
3391
|
+
if (clause.op === "is_not_null")
|
|
3392
|
+
return Array.isArray(v) ? v.length > 0 : v != null && v !== "";
|
|
3393
|
+
const arrayText = (value) => field.filter?.arrayCaseSensitive ? String(value) : String(value).toLowerCase();
|
|
3394
|
+
if (clause.op === "includes") {
|
|
3395
|
+
const arr = Array.isArray(v) ? v : [];
|
|
3396
|
+
const needle = arrayText(clause.value);
|
|
3397
|
+
return arr.some((x) => arrayText(x) === needle);
|
|
3398
|
+
}
|
|
3399
|
+
if (clause.op === "matches_regex" || clause.op === "not_matches_regex") {
|
|
3400
|
+
if (v == null || Array.isArray(v)) return false;
|
|
3401
|
+
if (!regex) return false;
|
|
3402
|
+
const matches = regex.test(String(v));
|
|
3403
|
+
return clause.op === "matches_regex" ? matches : !matches;
|
|
3404
|
+
}
|
|
3405
|
+
if (clause.op === "length_gt" || clause.op === "length_lt" || clause.op === "length_eq") {
|
|
3406
|
+
if (typeof v !== "string" || !/^(0|[1-9]\d*)$/.test(clause.value))
|
|
3407
|
+
return false;
|
|
3408
|
+
const limit = Number(clause.value);
|
|
3409
|
+
if (!Number.isSafeInteger(limit)) return false;
|
|
3410
|
+
const length = Array.from(v).length;
|
|
3411
|
+
return clause.op === "length_gt" ? length > limit : clause.op === "length_lt" ? length < limit : length === limit;
|
|
3412
|
+
}
|
|
3413
|
+
if (Array.isArray(v)) {
|
|
3414
|
+
const needle = arrayText(clause.value);
|
|
3415
|
+
const hay = v.map(arrayText);
|
|
3416
|
+
switch (clause.op) {
|
|
3417
|
+
case "contains":
|
|
3418
|
+
return hay.some((s) => s.includes(needle));
|
|
3419
|
+
case "=":
|
|
3420
|
+
return hay.includes(needle);
|
|
3421
|
+
case "!=":
|
|
3422
|
+
return !hay.includes(needle);
|
|
3423
|
+
default:
|
|
3424
|
+
return true;
|
|
3425
|
+
}
|
|
3426
|
+
}
|
|
3427
|
+
switch (clause.op) {
|
|
3428
|
+
case "=":
|
|
3429
|
+
return String(v ?? "") === String(coerceSafe(field, clause.value));
|
|
3430
|
+
case "!=":
|
|
3431
|
+
return String(v ?? "") !== String(coerceSafe(field, clause.value));
|
|
3432
|
+
case ">":
|
|
3433
|
+
return compare(v, coerceSafe(field, clause.value)) > 0;
|
|
3434
|
+
case ">=":
|
|
3435
|
+
return compare(v, coerceSafe(field, clause.value)) >= 0;
|
|
3436
|
+
case "<":
|
|
3437
|
+
return compare(v, coerceSafe(field, clause.value)) < 0;
|
|
3438
|
+
case "<=":
|
|
3439
|
+
return compare(v, coerceSafe(field, clause.value)) <= 0;
|
|
3440
|
+
case "contains":
|
|
3441
|
+
return String(v ?? "").toLowerCase().includes(clause.value.toLowerCase());
|
|
3442
|
+
case "starts_with":
|
|
3443
|
+
return String(v ?? "").toLowerCase().startsWith(clause.value.toLowerCase());
|
|
3444
|
+
case "ends_with":
|
|
3445
|
+
return String(v ?? "").toLowerCase().endsWith(clause.value.toLowerCase());
|
|
3446
|
+
default:
|
|
3447
|
+
return true;
|
|
3448
|
+
}
|
|
3449
|
+
}
|
|
3450
|
+
function compileRegex(pattern) {
|
|
3451
|
+
if (pattern === void 0) return void 0;
|
|
3452
|
+
try {
|
|
3453
|
+
return new RegExp(pattern);
|
|
3454
|
+
} catch {
|
|
3455
|
+
return null;
|
|
3456
|
+
}
|
|
3457
|
+
}
|
|
3458
|
+
function extractSortValue(value, regex) {
|
|
3459
|
+
if (regex === void 0) return value;
|
|
3460
|
+
if (regex === null || value == null) return null;
|
|
3461
|
+
const match = regex.exec(String(value));
|
|
3462
|
+
if (!match) return null;
|
|
3463
|
+
return match.length > 1 ? match[1] ?? null : match[0];
|
|
3464
|
+
}
|
|
3465
|
+
function coerceSafe(field, raw) {
|
|
3466
|
+
try {
|
|
3467
|
+
return coerceValue(field.type, raw);
|
|
3468
|
+
} catch {
|
|
3469
|
+
return raw;
|
|
3470
|
+
}
|
|
3471
|
+
}
|
|
3472
|
+
function compare(a, b) {
|
|
3473
|
+
if (a == null && b == null) return 0;
|
|
3474
|
+
if (a == null) return -1;
|
|
3475
|
+
if (b == null) return 1;
|
|
3476
|
+
if (typeof a === "number" && typeof b === "number") return a - b;
|
|
3477
|
+
if (typeof a === "boolean" && typeof b === "boolean")
|
|
3478
|
+
return a === b ? 0 : a ? 1 : -1;
|
|
3479
|
+
return String(a).localeCompare(String(b));
|
|
3480
|
+
}
|
|
3481
|
+
|
|
3482
|
+
// src/adapters.ts
|
|
3483
|
+
var LAST_PREFIX = "query-table:last:";
|
|
3484
|
+
var SAVED_PREFIX = "query-table:saved:";
|
|
3485
|
+
var DEFAULT_PREFIX = "query-table:default:";
|
|
3486
|
+
var MAX_SAVED_QUERIES = 100;
|
|
3487
|
+
var MAX_SAVED_NAME_LENGTH = 200;
|
|
3488
|
+
function normalizeSavedQuery(value) {
|
|
3489
|
+
if (value == null || typeof value !== "object" || Array.isArray(value)) return null;
|
|
3490
|
+
const raw = value;
|
|
3491
|
+
if (typeof raw.id !== "string" || !raw.id || raw.id.length > 256 || typeof raw.name !== "string" || !raw.name || raw.name.length > MAX_SAVED_NAME_LENGTH || typeof raw.savedAt !== "number" || !Number.isFinite(raw.savedAt) || raw.query == null || typeof raw.query !== "object") {
|
|
3492
|
+
return null;
|
|
3493
|
+
}
|
|
3494
|
+
return { id: raw.id, name: raw.name, savedAt: raw.savedAt, query: normalizeQueryState(raw.query) };
|
|
3495
|
+
}
|
|
3496
|
+
function normalizeSavedQueries(value) {
|
|
3497
|
+
if (!Array.isArray(value)) return [];
|
|
3498
|
+
return value.slice(0, MAX_SAVED_QUERIES).map(normalizeSavedQuery).filter((item) => item != null);
|
|
3499
|
+
}
|
|
3500
|
+
function memoryStorageAdapter() {
|
|
3501
|
+
const last = /* @__PURE__ */ new Map();
|
|
3502
|
+
const saved = /* @__PURE__ */ new Map();
|
|
3503
|
+
const defaults = /* @__PURE__ */ new Map();
|
|
3504
|
+
return {
|
|
3505
|
+
async loadLast(key) {
|
|
3506
|
+
const query = last.get(key);
|
|
3507
|
+
return query ? normalizeQueryState(query) : null;
|
|
3508
|
+
},
|
|
3509
|
+
async saveLast(key, query) {
|
|
3510
|
+
last.set(key, normalizeQueryState(query));
|
|
3511
|
+
},
|
|
3512
|
+
async listSaved(key) {
|
|
3513
|
+
return (saved.get(key) ?? []).map((item) => ({ ...item, query: normalizeQueryState(item.query) }));
|
|
3514
|
+
},
|
|
3515
|
+
async loadDefaultSaved(key) {
|
|
3516
|
+
const id = defaults.get(key);
|
|
3517
|
+
return id ? (saved.get(key) ?? []).find((item) => item.id === id) ?? null : null;
|
|
3518
|
+
},
|
|
3519
|
+
async setDefaultSaved(key, id) {
|
|
3520
|
+
if (id == null) {
|
|
3521
|
+
defaults.delete(key);
|
|
3522
|
+
return;
|
|
3523
|
+
}
|
|
3524
|
+
if (!(saved.get(key) ?? []).some((item) => item.id === id)) throw new Error(`Saved query "${id}" does not exist.`);
|
|
3525
|
+
defaults.set(key, id);
|
|
3526
|
+
},
|
|
3527
|
+
async saveNamed(key, name, query, savedAt) {
|
|
3528
|
+
const normalizedName = name.trim();
|
|
3529
|
+
if (!normalizedName || normalizedName.length > MAX_SAVED_NAME_LENGTH) {
|
|
3530
|
+
throw new Error(`Saved query name must be between 1 and ${MAX_SAVED_NAME_LENGTH} characters.`);
|
|
3531
|
+
}
|
|
3532
|
+
const items = saved.get(key) ?? [];
|
|
3533
|
+
if (items.length >= MAX_SAVED_QUERIES) throw new Error(`At most ${MAX_SAVED_QUERIES} saved queries are allowed.`);
|
|
3534
|
+
if (items.some((item2) => item2.name === normalizedName)) throw new Error(`Saved query "${normalizedName}" already exists.`);
|
|
3535
|
+
const item = {
|
|
3536
|
+
id: `${savedAt}-${Math.round((savedAt * 9301 + 49297) % 233280)}`,
|
|
3537
|
+
name: normalizedName,
|
|
3538
|
+
savedAt,
|
|
3539
|
+
query: normalizeQueryState(query)
|
|
3540
|
+
};
|
|
3541
|
+
saved.set(key, [...items, item]);
|
|
3542
|
+
return item;
|
|
3543
|
+
},
|
|
3544
|
+
async deleteSaved(key, id) {
|
|
3545
|
+
saved.set(key, (saved.get(key) ?? []).filter((item) => item.id !== id));
|
|
3546
|
+
if (defaults.get(key) === id) defaults.delete(key);
|
|
3547
|
+
}
|
|
3548
|
+
};
|
|
3549
|
+
}
|
|
3550
|
+
function localStorageAdapter() {
|
|
3551
|
+
const ls = (() => {
|
|
3552
|
+
try {
|
|
3553
|
+
return typeof localStorage !== "undefined" ? localStorage : null;
|
|
3554
|
+
} catch {
|
|
3555
|
+
return null;
|
|
3556
|
+
}
|
|
3557
|
+
})();
|
|
3558
|
+
const readSaved = (key) => {
|
|
3559
|
+
if (!ls) return [];
|
|
3560
|
+
try {
|
|
3561
|
+
const raw = ls.getItem(SAVED_PREFIX + key);
|
|
3562
|
+
return normalizeSavedQueries(raw ? JSON.parse(raw) : []);
|
|
3563
|
+
} catch {
|
|
3564
|
+
return [];
|
|
3565
|
+
}
|
|
3566
|
+
};
|
|
3567
|
+
const writeSaved = (key, items) => {
|
|
3568
|
+
if (ls) ls.setItem(SAVED_PREFIX + key, JSON.stringify(items));
|
|
3569
|
+
};
|
|
3570
|
+
const readDefaultId = (key) => {
|
|
3571
|
+
if (!ls) return null;
|
|
3572
|
+
return ls.getItem(DEFAULT_PREFIX + key);
|
|
3573
|
+
};
|
|
3574
|
+
const writeDefaultId = (key, id) => {
|
|
3575
|
+
if (!ls) return;
|
|
3576
|
+
if (id) ls.setItem(DEFAULT_PREFIX + key, id);
|
|
3577
|
+
else ls.removeItem(DEFAULT_PREFIX + key);
|
|
3578
|
+
};
|
|
3579
|
+
return {
|
|
3580
|
+
async loadLast(key) {
|
|
3581
|
+
if (!ls) return null;
|
|
3582
|
+
try {
|
|
3583
|
+
const raw = ls.getItem(LAST_PREFIX + key);
|
|
3584
|
+
return raw ? normalizeQueryState(JSON.parse(raw)) : null;
|
|
3585
|
+
} catch {
|
|
3586
|
+
return null;
|
|
3587
|
+
}
|
|
3588
|
+
},
|
|
3589
|
+
async saveLast(key, query) {
|
|
3590
|
+
if (ls) ls.setItem(LAST_PREFIX + key, JSON.stringify(normalizeQueryState(query)));
|
|
3591
|
+
},
|
|
3592
|
+
async listSaved(key) {
|
|
3593
|
+
return readSaved(key).sort((a, b) => b.savedAt - a.savedAt);
|
|
3594
|
+
},
|
|
3595
|
+
async loadDefaultSaved(key) {
|
|
3596
|
+
const id = readDefaultId(key);
|
|
3597
|
+
if (!id) return null;
|
|
3598
|
+
const found = readSaved(key).find((q) => q.id === id);
|
|
3599
|
+
if (!found) writeDefaultId(key, null);
|
|
3600
|
+
return found ?? null;
|
|
3601
|
+
},
|
|
3602
|
+
async setDefaultSaved(key, id) {
|
|
3603
|
+
if (id && !readSaved(key).some((q) => q.id === id)) {
|
|
3604
|
+
throw new Error(`Saved query "${id}" does not exist.`);
|
|
3605
|
+
}
|
|
3606
|
+
writeDefaultId(key, id);
|
|
3607
|
+
},
|
|
3608
|
+
async saveNamed(key, name, query, savedAt) {
|
|
3609
|
+
const normalizedName = name.trim();
|
|
3610
|
+
if (!normalizedName || normalizedName.length > MAX_SAVED_NAME_LENGTH) {
|
|
3611
|
+
throw new Error(`Saved query name must be between 1 and ${MAX_SAVED_NAME_LENGTH} characters.`);
|
|
3612
|
+
}
|
|
3613
|
+
const items = readSaved(key);
|
|
3614
|
+
if (items.length >= MAX_SAVED_QUERIES) throw new Error(`At most ${MAX_SAVED_QUERIES} saved queries are allowed.`);
|
|
3615
|
+
if (items.some((q) => q.name === normalizedName)) {
|
|
3616
|
+
throw new Error(`Saved query "${normalizedName}" already exists.`);
|
|
3617
|
+
}
|
|
3618
|
+
const item = {
|
|
3619
|
+
id: `${savedAt}-${Math.round((savedAt * 9301 + 49297) % 233280)}`,
|
|
3620
|
+
name: normalizedName,
|
|
3621
|
+
savedAt,
|
|
3622
|
+
query: normalizeQueryState(query)
|
|
3623
|
+
};
|
|
3624
|
+
items.push(item);
|
|
3625
|
+
writeSaved(key, items);
|
|
3626
|
+
return item;
|
|
3627
|
+
},
|
|
3628
|
+
async deleteSaved(key, id) {
|
|
3629
|
+
writeSaved(
|
|
3630
|
+
key,
|
|
3631
|
+
readSaved(key).filter((q) => q.id !== id)
|
|
3632
|
+
);
|
|
3633
|
+
if (readDefaultId(key) === id) writeDefaultId(key, null);
|
|
3634
|
+
}
|
|
3635
|
+
};
|
|
3636
|
+
}
|
|
2218
3637
|
export {
|
|
2219
3638
|
AGG_OPS_BY_TYPE,
|
|
2220
3639
|
AGG_OPS_NEEDING_FIELD,
|
|
@@ -2228,6 +3647,7 @@ export {
|
|
|
2228
3647
|
MAX_QUERY_LIMIT,
|
|
2229
3648
|
MAX_QUERY_OFFSET,
|
|
2230
3649
|
MAX_QUERY_TOKEN_LENGTH,
|
|
3650
|
+
MAX_RELATIVE_TIME_MS,
|
|
2231
3651
|
MAX_SELECT_COLUMNS,
|
|
2232
3652
|
MAX_WHERE_CLAUSES,
|
|
2233
3653
|
NULLARY_OPS,
|
|
@@ -2237,14 +3657,23 @@ export {
|
|
|
2237
3657
|
aggOpsForField,
|
|
2238
3658
|
applyAggregations,
|
|
2239
3659
|
applyQuery,
|
|
3660
|
+
boxSummary,
|
|
2240
3661
|
coerceValue,
|
|
3662
|
+
compareMetricValues,
|
|
2241
3663
|
compileFormula,
|
|
3664
|
+
compileMetric,
|
|
2242
3665
|
computedFieldName,
|
|
3666
|
+
createSetFilter,
|
|
2243
3667
|
decodeQuery,
|
|
2244
3668
|
encodeQuery,
|
|
3669
|
+
evaluateMetricReductions,
|
|
3670
|
+
evaluateMetrics,
|
|
2245
3671
|
filterValues,
|
|
2246
3672
|
formulaRuntime,
|
|
2247
3673
|
groupPreview,
|
|
3674
|
+
hasRegex,
|
|
3675
|
+
histogramCounts,
|
|
3676
|
+
histogramEdges,
|
|
2248
3677
|
httpComputedColumnStore,
|
|
2249
3678
|
indexFields,
|
|
2250
3679
|
isComputedCellError,
|
|
@@ -2259,20 +3688,36 @@ export {
|
|
|
2259
3688
|
isSortable,
|
|
2260
3689
|
loadSchema,
|
|
2261
3690
|
localStorageAdapter,
|
|
3691
|
+
mapSetFilterValues,
|
|
2262
3692
|
matchesClause,
|
|
2263
3693
|
memoryComputedColumnStore,
|
|
2264
3694
|
memoryStorageAdapter,
|
|
3695
|
+
metricAggregatePlans,
|
|
3696
|
+
metricComputationKey,
|
|
3697
|
+
metricDependencies,
|
|
3698
|
+
metricExpression,
|
|
3699
|
+
metricPlans,
|
|
2265
3700
|
negateClause,
|
|
2266
3701
|
normalizeQueryState,
|
|
2267
3702
|
opAllowedForType,
|
|
2268
3703
|
opPairsForField,
|
|
2269
3704
|
opsForField,
|
|
3705
|
+
parseRelativeDuration,
|
|
3706
|
+
percentile,
|
|
2270
3707
|
predicatesOf,
|
|
2271
3708
|
queriesEqual,
|
|
2272
3709
|
readFieldValue,
|
|
3710
|
+
readSetFilter,
|
|
3711
|
+
reduceMetricValues,
|
|
3712
|
+
resolveFieldName,
|
|
3713
|
+
selectMetricShownRows,
|
|
2273
3714
|
selectedFields,
|
|
3715
|
+
stripSetFilter,
|
|
2274
3716
|
toAggregationQuery,
|
|
3717
|
+
toMetricQuery,
|
|
2275
3718
|
toServerQuery,
|
|
2276
|
-
|
|
3719
|
+
toServerQueryV2,
|
|
3720
|
+
validateComputedColumn,
|
|
3721
|
+
validateMetric
|
|
2277
3722
|
};
|
|
2278
3723
|
//# sourceMappingURL=index.js.map
|