@pythia-software/query-table-core 0.5.0 → 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 +51 -0
- package/dist/index.d.ts +561 -281
- package/dist/index.js +1776 -416
- 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;
|
|
@@ -275,7 +417,7 @@ function normalizeQueryState(input, fallback = EMPTY_QUERY) {
|
|
|
275
417
|
for (const item of raw.orderBy.slice(0, MAX_ORDER_BY_TERMS)) {
|
|
276
418
|
if (!isRecord(item)) continue;
|
|
277
419
|
const field = boundedString(item.field, MAX_FIELD_NAME_LENGTH);
|
|
278
|
-
if (!field ||
|
|
420
|
+
if (!field || item.dir !== "asc" && item.dir !== "desc") continue;
|
|
279
421
|
const term = { field, dir: item.dir };
|
|
280
422
|
if (item.nulls === "first" || item.nulls === "last") term.nulls = item.nulls;
|
|
281
423
|
if (isRecord(item.extract) && typeof item.extract.regex === "string" && item.extract.regex.length <= MAX_FILTER_VALUE_LENGTH) {
|
|
@@ -300,19 +442,26 @@ function normalizeQueryState(input, fallback = EMPTY_QUERY) {
|
|
|
300
442
|
for (const item of raw.aggregations.slice(0, MAX_AGGREGATIONS)) {
|
|
301
443
|
if (!isRecord(item)) continue;
|
|
302
444
|
const id = boundedString(item.id, MAX_FIELD_NAME_LENGTH);
|
|
303
|
-
if (!id
|
|
304
|
-
const
|
|
305
|
-
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 };
|
|
306
450
|
const field = boundedString(item.field, MAX_FIELD_NAME_LENGTH);
|
|
307
451
|
const label = boundedString(item.label, MAX_LABEL_LENGTH);
|
|
308
|
-
if (field && isComputedField(field) || groupBy.some(isComputedField)) continue;
|
|
309
452
|
if (field) aggregation.field = field;
|
|
310
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."];
|
|
311
460
|
aggregations.push(aggregation);
|
|
312
461
|
}
|
|
313
462
|
if (aggregations.length > 0) out.aggregations = aggregations;
|
|
314
463
|
} else if (fallback.aggregations?.length) {
|
|
315
|
-
out.aggregations =
|
|
464
|
+
out.aggregations = normalizeQueryState({ aggregations: fallback.aggregations }).aggregations;
|
|
316
465
|
}
|
|
317
466
|
return out;
|
|
318
467
|
}
|
|
@@ -342,7 +491,7 @@ function isPushdownFilter(f2) {
|
|
|
342
491
|
return isFilterable(f2) && (f2.filter?.pushdown ?? f2.source.kind === "backend");
|
|
343
492
|
}
|
|
344
493
|
function isSortable(f2) {
|
|
345
|
-
if (f2.source.kind === "derived" && f2.source.computedId) return
|
|
494
|
+
if (f2.source.kind === "derived" && f2.source.computedId) return f2.sort?.enabled === true;
|
|
346
495
|
return f2.sort?.enabled ?? f2.source.kind === "backend";
|
|
347
496
|
}
|
|
348
497
|
function isSelectable(f2) {
|
|
@@ -489,6 +638,34 @@ function normalizeAliases(rawAliases, rawAlias) {
|
|
|
489
638
|
return aliases;
|
|
490
639
|
}
|
|
491
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
|
+
|
|
492
669
|
// src/ops.ts
|
|
493
670
|
var NULLITY = ["is_null", "is_not_null"];
|
|
494
671
|
var COMPLEMENT_OP = {
|
|
@@ -564,7 +741,7 @@ function opsForField(field) {
|
|
|
564
741
|
function opAllowedForType(type, op) {
|
|
565
742
|
return OPS_BY_TYPE[type].includes(op);
|
|
566
743
|
}
|
|
567
|
-
function coerceValue(type, raw) {
|
|
744
|
+
function coerceValue(type, raw, now) {
|
|
568
745
|
switch (type) {
|
|
569
746
|
case "number": {
|
|
570
747
|
const n = Number(raw);
|
|
@@ -578,6 +755,11 @@ function coerceValue(type, raw) {
|
|
|
578
755
|
throw new Error(`not a bool: ${JSON.stringify(raw)}`);
|
|
579
756
|
}
|
|
580
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
|
+
}
|
|
581
763
|
const ms = Date.parse(raw);
|
|
582
764
|
if (Number.isNaN(ms)) throw new Error(`not a datetime: ${JSON.stringify(raw)}`);
|
|
583
765
|
return ms;
|
|
@@ -613,11 +795,13 @@ function aggOpAllowedForType(type, op) {
|
|
|
613
795
|
return AGG_OPS_BY_TYPE[type].includes(op);
|
|
614
796
|
}
|
|
615
797
|
function isMeasurable(field) {
|
|
798
|
+
if (field.source.kind === "derived" && field.source.computedId) return field.aggregate?.measure === true;
|
|
616
799
|
if (field.source.kind !== "backend") return false;
|
|
617
800
|
if (field.aggregate?.measure != null) return field.aggregate.measure;
|
|
618
801
|
return aggOpsForField(field).length > 0;
|
|
619
802
|
}
|
|
620
803
|
function isGroupable(field) {
|
|
804
|
+
if (field.source.kind === "derived" && field.source.computedId) return field.aggregate?.groupable === true;
|
|
621
805
|
if (field.source.kind !== "backend") return false;
|
|
622
806
|
if (field.aggregate?.groupable != null) return field.aggregate.groupable;
|
|
623
807
|
return field.type === "enum" || field.type === "text" || field.type === "bool";
|
|
@@ -773,7 +957,9 @@ function termIsPushdown(term, byName, resolveField2) {
|
|
|
773
957
|
});
|
|
774
958
|
}
|
|
775
959
|
function toAggregationQuery(q, schema) {
|
|
960
|
+
const suppliedPredicateCount = q.where.reduce((n, t) => n + predicatesOf(t).length, 0);
|
|
776
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.");
|
|
777
963
|
const byName = indexFields(schema);
|
|
778
964
|
const resolveField2 = (name) => resolveFieldName(schema, name) ?? name;
|
|
779
965
|
const where = q.where.filter((term) => termIsPushdown(term, byName, resolveField2)).map(stripSetFilter);
|
|
@@ -786,411 +972,10 @@ function toAggregationQuery(q, schema) {
|
|
|
786
972
|
const aggregations = (q.aggregations ?? []).filter(
|
|
787
973
|
(a) => isBackend(a.field) && a.groupBy.every((g) => isBackend(g))
|
|
788
974
|
);
|
|
789
|
-
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
function applyQuery(rows, q, schema) {
|
|
794
|
-
const byName = indexFields(schema);
|
|
795
|
-
const resolveField2 = (name) => resolveFieldName(schema, name) ?? name;
|
|
796
|
-
const whereTerms = q.where.filter((term) => !predicatesOf(term).some((c) => isComputedField(c.field))).map((term) => prepareWhereTerm(term, resolveField2));
|
|
797
|
-
const orderBy = q.orderBy.filter((term) => !isComputedField(term.field)).map((term) => ({ ...term, field: resolveField2(term.field) }));
|
|
798
|
-
let out = rows.filter((row) => whereTerms.every((term) => matchesTerm(byName, row, term)));
|
|
799
|
-
const total = out.length;
|
|
800
|
-
if (orderBy.length) {
|
|
801
|
-
const terms = orderBy.map((t) => ({
|
|
802
|
-
field: byName.get(t.field),
|
|
803
|
-
dir: t.dir,
|
|
804
|
-
nullsLast: (t.nulls ?? "last") === "last",
|
|
805
|
-
extract: compileRegex(t.extract?.regex)
|
|
806
|
-
})).filter(
|
|
807
|
-
(t) => t.field != null
|
|
808
|
-
);
|
|
809
|
-
out = [...out].sort((a, b) => {
|
|
810
|
-
for (const t of terms) {
|
|
811
|
-
const av = extractSortValue(readFieldValue(t.field, a), t.extract);
|
|
812
|
-
const bv = extractSortValue(readFieldValue(t.field, b), t.extract);
|
|
813
|
-
const aNull = av == null;
|
|
814
|
-
const bNull = bv == null;
|
|
815
|
-
if (aNull || bNull) {
|
|
816
|
-
if (aNull && bNull) continue;
|
|
817
|
-
return (aNull ? 1 : -1) * (t.nullsLast ? 1 : -1);
|
|
818
|
-
}
|
|
819
|
-
const cmp = compare(av, bv);
|
|
820
|
-
if (cmp !== 0) return t.dir === "asc" ? cmp : -cmp;
|
|
821
|
-
}
|
|
822
|
-
return 0;
|
|
823
|
-
});
|
|
824
|
-
}
|
|
825
|
-
const start = q.offset > 0 ? q.offset : 0;
|
|
826
|
-
const end = q.limit > 0 ? start + q.limit : out.length;
|
|
827
|
-
return { rows: out.slice(start, end), total };
|
|
828
|
-
}
|
|
829
|
-
function matchesClause(row, clause, schema) {
|
|
830
|
-
return matchesWith(indexFields(schema), row, prepareWhereClause(clause));
|
|
831
|
-
}
|
|
832
|
-
function applyAggregations(rows, q, schema) {
|
|
833
|
-
const byName = indexFields(schema);
|
|
834
|
-
const resolveField2 = (name) => resolveFieldName(schema, name) ?? name;
|
|
835
|
-
const whereTerms = q.where.filter((term) => !predicatesOf(term).some((c) => isComputedField(c.field))).map((term) => prepareWhereTerm(term, resolveField2));
|
|
836
|
-
const filtered = rows.filter((row) => whereTerms.every((term) => matchesTerm(byName, row, term)));
|
|
837
|
-
const metrics = (q.aggregations ?? []).filter((a) => !isComputedField(a.field ?? "") && !a.groupBy.some(isComputedField)).map((agg) => ({
|
|
838
|
-
id: agg.id,
|
|
839
|
-
buckets: computeBuckets(filtered, agg, byName, resolveField2)
|
|
840
|
-
}));
|
|
841
|
-
return { metrics };
|
|
842
|
-
}
|
|
843
|
-
function computeBuckets(rows, agg, byName, resolveField2) {
|
|
844
|
-
const groupFields = agg.groupBy.map((n) => byName.get(resolveField2(n)));
|
|
845
|
-
const measure = agg.field ? byName.get(resolveField2(agg.field)) : void 0;
|
|
846
|
-
const groups = /* @__PURE__ */ new Map();
|
|
847
|
-
for (const row of rows) {
|
|
848
|
-
const keys = groupFields.map((f2) => {
|
|
849
|
-
if (!f2) return null;
|
|
850
|
-
const v = readFieldValue(f2, row);
|
|
851
|
-
return v == null || v === "" ? null : String(v);
|
|
852
|
-
});
|
|
853
|
-
const k = JSON.stringify(keys);
|
|
854
|
-
let g = groups.get(k);
|
|
855
|
-
if (!g) {
|
|
856
|
-
g = { keys, rows: [] };
|
|
857
|
-
groups.set(k, g);
|
|
858
|
-
}
|
|
859
|
-
g.rows.push(row);
|
|
860
|
-
}
|
|
861
|
-
const buckets = [...groups.values()].map((g) => ({
|
|
862
|
-
keys: g.keys,
|
|
863
|
-
count: g.rows.length,
|
|
864
|
-
value: aggValue(agg.op, measure, g.rows)
|
|
865
|
-
}));
|
|
866
|
-
return sortBuckets(buckets);
|
|
867
|
-
}
|
|
868
|
-
function aggValue(op, measure, rows) {
|
|
869
|
-
if (op === "count") {
|
|
870
|
-
if (!measure) return rows.length;
|
|
871
|
-
let n = 0;
|
|
872
|
-
for (const r of rows) if (readFieldValue(measure, r) != null) n++;
|
|
873
|
-
return n;
|
|
874
|
-
}
|
|
875
|
-
if (!measure) return null;
|
|
876
|
-
const values = rows.map((r) => readFieldValue(measure, r)).filter((v) => v != null);
|
|
877
|
-
switch (op) {
|
|
878
|
-
case "count_distinct":
|
|
879
|
-
return new Set(values.map((v) => String(v))).size;
|
|
880
|
-
case "sum":
|
|
881
|
-
case "avg": {
|
|
882
|
-
let sum = 0;
|
|
883
|
-
let n = 0;
|
|
884
|
-
for (const v of values) {
|
|
885
|
-
const num = typeof v === "number" ? v : Number(v);
|
|
886
|
-
if (Number.isFinite(num)) {
|
|
887
|
-
sum += num;
|
|
888
|
-
n++;
|
|
889
|
-
}
|
|
890
|
-
}
|
|
891
|
-
if (op === "avg") return n ? sum / n : null;
|
|
892
|
-
return n ? sum : null;
|
|
893
|
-
}
|
|
894
|
-
case "min":
|
|
895
|
-
case "max": {
|
|
896
|
-
let best;
|
|
897
|
-
for (const v of values) {
|
|
898
|
-
if (best === void 0) {
|
|
899
|
-
best = v;
|
|
900
|
-
continue;
|
|
901
|
-
}
|
|
902
|
-
const cmp = compare(v, best);
|
|
903
|
-
if (op === "min" ? cmp < 0 : cmp > 0) best = v;
|
|
904
|
-
}
|
|
905
|
-
return best ?? null;
|
|
906
|
-
}
|
|
907
|
-
default:
|
|
908
|
-
return null;
|
|
909
|
-
}
|
|
910
|
-
}
|
|
911
|
-
function sortBuckets(buckets) {
|
|
912
|
-
return [...buckets].sort((a, b) => {
|
|
913
|
-
const av = typeof a.value === "number" ? a.value : a.count;
|
|
914
|
-
const bv = typeof b.value === "number" ? b.value : b.count;
|
|
915
|
-
if (av !== bv) return bv - av;
|
|
916
|
-
return JSON.stringify(a.keys).localeCompare(JSON.stringify(b.keys));
|
|
917
|
-
});
|
|
918
|
-
}
|
|
919
|
-
function prepareWhereClause(clause) {
|
|
920
|
-
const usesRegex = clause.op === "matches_regex" || clause.op === "not_matches_regex";
|
|
921
|
-
return { clause, regex: usesRegex ? compileRegex(clause.value) : void 0 };
|
|
922
|
-
}
|
|
923
|
-
function prepareWhereTerm(term, resolveField2) {
|
|
924
|
-
if (isOrGroup(term)) {
|
|
925
|
-
return { kind: "or", predicates: term.any.map((c) => prepareWhereClause({ ...c, field: resolveField2(c.field) })) };
|
|
926
|
-
}
|
|
927
|
-
return { kind: "lit", predicate: prepareWhereClause({ ...term, field: resolveField2(term.field) }) };
|
|
928
|
-
}
|
|
929
|
-
function matchesTerm(byName, row, term) {
|
|
930
|
-
if (term.kind === "lit") return matchesWith(byName, row, term.predicate);
|
|
931
|
-
return term.predicates.some((p) => matchesWith(byName, row, p));
|
|
932
|
-
}
|
|
933
|
-
function matchesWith(byName, row, prepared) {
|
|
934
|
-
const { clause } = prepared;
|
|
935
|
-
const field = byName.get(clause.field);
|
|
936
|
-
if (!field) return true;
|
|
937
|
-
const v = readFieldValue(field, row);
|
|
938
|
-
if (clause.value === "" && clause.op !== "=" && clause.op !== "!=" && clause.op !== "is_null" && clause.op !== "is_not_null") {
|
|
939
|
-
return true;
|
|
940
|
-
}
|
|
941
|
-
if (clause.op === "length_gt" || clause.op === "length_lt" || clause.op === "length_eq") {
|
|
942
|
-
if (!/^(0|[1-9]\d*)$/.test(clause.value) || !Number.isSafeInteger(Number(clause.value))) return false;
|
|
943
|
-
}
|
|
944
|
-
const base = matchesBase(field, v, prepared);
|
|
945
|
-
if (!clause.negated) return base;
|
|
946
|
-
const isNull = Array.isArray(v) ? v.length === 0 : v == null || v === "";
|
|
947
|
-
return !isNull && !base;
|
|
948
|
-
}
|
|
949
|
-
function matchesBase(field, v, prepared) {
|
|
950
|
-
const { clause, regex } = prepared;
|
|
951
|
-
if (clause.op === "is_null") return Array.isArray(v) ? v.length === 0 : v == null || v === "";
|
|
952
|
-
if (clause.op === "is_not_null") return Array.isArray(v) ? v.length > 0 : v != null && v !== "";
|
|
953
|
-
const arrayText = (value) => field.filter?.arrayCaseSensitive ? String(value) : String(value).toLowerCase();
|
|
954
|
-
if (clause.op === "includes") {
|
|
955
|
-
const arr = Array.isArray(v) ? v : [];
|
|
956
|
-
const needle = arrayText(clause.value);
|
|
957
|
-
return arr.some((x) => arrayText(x) === needle);
|
|
958
|
-
}
|
|
959
|
-
if (clause.op === "matches_regex" || clause.op === "not_matches_regex") {
|
|
960
|
-
if (v == null || Array.isArray(v)) return false;
|
|
961
|
-
if (!regex) return false;
|
|
962
|
-
const matches = regex.test(String(v));
|
|
963
|
-
return clause.op === "matches_regex" ? matches : !matches;
|
|
964
|
-
}
|
|
965
|
-
if (clause.op === "length_gt" || clause.op === "length_lt" || clause.op === "length_eq") {
|
|
966
|
-
if (typeof v !== "string" || !/^(0|[1-9]\d*)$/.test(clause.value)) return false;
|
|
967
|
-
const limit = Number(clause.value);
|
|
968
|
-
if (!Number.isSafeInteger(limit)) return false;
|
|
969
|
-
const length = Array.from(v).length;
|
|
970
|
-
return clause.op === "length_gt" ? length > limit : clause.op === "length_lt" ? length < limit : length === limit;
|
|
971
|
-
}
|
|
972
|
-
if (Array.isArray(v)) {
|
|
973
|
-
const needle = arrayText(clause.value);
|
|
974
|
-
const hay = v.map(arrayText);
|
|
975
|
-
switch (clause.op) {
|
|
976
|
-
case "contains":
|
|
977
|
-
return hay.some((s) => s.includes(needle));
|
|
978
|
-
case "=":
|
|
979
|
-
return hay.includes(needle);
|
|
980
|
-
case "!=":
|
|
981
|
-
return !hay.includes(needle);
|
|
982
|
-
default:
|
|
983
|
-
return true;
|
|
984
|
-
}
|
|
985
|
-
}
|
|
986
|
-
switch (clause.op) {
|
|
987
|
-
case "=":
|
|
988
|
-
return String(v ?? "") === String(coerceSafe(field, clause.value));
|
|
989
|
-
case "!=":
|
|
990
|
-
return String(v ?? "") !== String(coerceSafe(field, clause.value));
|
|
991
|
-
case ">":
|
|
992
|
-
return compare(v, coerceSafe(field, clause.value)) > 0;
|
|
993
|
-
case ">=":
|
|
994
|
-
return compare(v, coerceSafe(field, clause.value)) >= 0;
|
|
995
|
-
case "<":
|
|
996
|
-
return compare(v, coerceSafe(field, clause.value)) < 0;
|
|
997
|
-
case "<=":
|
|
998
|
-
return compare(v, coerceSafe(field, clause.value)) <= 0;
|
|
999
|
-
case "contains":
|
|
1000
|
-
return String(v ?? "").toLowerCase().includes(clause.value.toLowerCase());
|
|
1001
|
-
case "starts_with":
|
|
1002
|
-
return String(v ?? "").toLowerCase().startsWith(clause.value.toLowerCase());
|
|
1003
|
-
case "ends_with":
|
|
1004
|
-
return String(v ?? "").toLowerCase().endsWith(clause.value.toLowerCase());
|
|
1005
|
-
default:
|
|
1006
|
-
return true;
|
|
1007
|
-
}
|
|
1008
|
-
}
|
|
1009
|
-
function compileRegex(pattern) {
|
|
1010
|
-
if (pattern === void 0) return void 0;
|
|
1011
|
-
try {
|
|
1012
|
-
return new RegExp(pattern);
|
|
1013
|
-
} catch {
|
|
1014
|
-
return null;
|
|
1015
|
-
}
|
|
1016
|
-
}
|
|
1017
|
-
function extractSortValue(value, regex) {
|
|
1018
|
-
if (regex === void 0) return value;
|
|
1019
|
-
if (regex === null || value == null) return null;
|
|
1020
|
-
const match = regex.exec(String(value));
|
|
1021
|
-
if (!match) return null;
|
|
1022
|
-
return match.length > 1 ? match[1] ?? null : match[0];
|
|
1023
|
-
}
|
|
1024
|
-
function coerceSafe(field, raw) {
|
|
1025
|
-
try {
|
|
1026
|
-
return coerceValue(field.type, raw);
|
|
1027
|
-
} catch {
|
|
1028
|
-
return raw;
|
|
1029
|
-
}
|
|
1030
|
-
}
|
|
1031
|
-
function compare(a, b) {
|
|
1032
|
-
if (a == null && b == null) return 0;
|
|
1033
|
-
if (a == null) return -1;
|
|
1034
|
-
if (b == null) return 1;
|
|
1035
|
-
if (typeof a === "number" && typeof b === "number") return a - b;
|
|
1036
|
-
if (typeof a === "boolean" && typeof b === "boolean") return a === b ? 0 : a ? 1 : -1;
|
|
1037
|
-
return String(a).localeCompare(String(b));
|
|
1038
|
-
}
|
|
1039
|
-
|
|
1040
|
-
// src/adapters.ts
|
|
1041
|
-
var LAST_PREFIX = "query-table:last:";
|
|
1042
|
-
var SAVED_PREFIX = "query-table:saved:";
|
|
1043
|
-
var DEFAULT_PREFIX = "query-table:default:";
|
|
1044
|
-
var MAX_SAVED_QUERIES = 100;
|
|
1045
|
-
var MAX_SAVED_NAME_LENGTH = 200;
|
|
1046
|
-
function normalizeSavedQuery(value) {
|
|
1047
|
-
if (value == null || typeof value !== "object" || Array.isArray(value)) return null;
|
|
1048
|
-
const raw = value;
|
|
1049
|
-
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") {
|
|
1050
|
-
return null;
|
|
1051
|
-
}
|
|
1052
|
-
return { id: raw.id, name: raw.name, savedAt: raw.savedAt, query: normalizeQueryState(raw.query) };
|
|
1053
|
-
}
|
|
1054
|
-
function normalizeSavedQueries(value) {
|
|
1055
|
-
if (!Array.isArray(value)) return [];
|
|
1056
|
-
return value.slice(0, MAX_SAVED_QUERIES).map(normalizeSavedQuery).filter((item) => item != null);
|
|
1057
|
-
}
|
|
1058
|
-
function memoryStorageAdapter() {
|
|
1059
|
-
const last = /* @__PURE__ */ new Map();
|
|
1060
|
-
const saved = /* @__PURE__ */ new Map();
|
|
1061
|
-
const defaults = /* @__PURE__ */ new Map();
|
|
1062
|
-
return {
|
|
1063
|
-
async loadLast(key) {
|
|
1064
|
-
const query = last.get(key);
|
|
1065
|
-
return query ? normalizeQueryState(query) : null;
|
|
1066
|
-
},
|
|
1067
|
-
async saveLast(key, query) {
|
|
1068
|
-
last.set(key, normalizeQueryState(query));
|
|
1069
|
-
},
|
|
1070
|
-
async listSaved(key) {
|
|
1071
|
-
return (saved.get(key) ?? []).map((item) => ({ ...item, query: normalizeQueryState(item.query) }));
|
|
1072
|
-
},
|
|
1073
|
-
async loadDefaultSaved(key) {
|
|
1074
|
-
const id = defaults.get(key);
|
|
1075
|
-
return id ? (saved.get(key) ?? []).find((item) => item.id === id) ?? null : null;
|
|
1076
|
-
},
|
|
1077
|
-
async setDefaultSaved(key, id) {
|
|
1078
|
-
if (id == null) {
|
|
1079
|
-
defaults.delete(key);
|
|
1080
|
-
return;
|
|
1081
|
-
}
|
|
1082
|
-
if (!(saved.get(key) ?? []).some((item) => item.id === id)) throw new Error(`Saved query "${id}" does not exist.`);
|
|
1083
|
-
defaults.set(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 = saved.get(key) ?? [];
|
|
1091
|
-
if (items.length >= MAX_SAVED_QUERIES) throw new Error(`At most ${MAX_SAVED_QUERIES} saved queries are allowed.`);
|
|
1092
|
-
if (items.some((item2) => item2.name === normalizedName)) throw new Error(`Saved query "${normalizedName}" already exists.`);
|
|
1093
|
-
const item = {
|
|
1094
|
-
id: `${savedAt}-${Math.round((savedAt * 9301 + 49297) % 233280)}`,
|
|
1095
|
-
name: normalizedName,
|
|
1096
|
-
savedAt,
|
|
1097
|
-
query: normalizeQueryState(query)
|
|
1098
|
-
};
|
|
1099
|
-
saved.set(key, [...items, item]);
|
|
1100
|
-
return item;
|
|
1101
|
-
},
|
|
1102
|
-
async deleteSaved(key, id) {
|
|
1103
|
-
saved.set(key, (saved.get(key) ?? []).filter((item) => item.id !== id));
|
|
1104
|
-
if (defaults.get(key) === id) defaults.delete(key);
|
|
1105
|
-
}
|
|
1106
|
-
};
|
|
1107
|
-
}
|
|
1108
|
-
function localStorageAdapter() {
|
|
1109
|
-
const ls = (() => {
|
|
1110
|
-
try {
|
|
1111
|
-
return typeof localStorage !== "undefined" ? localStorage : null;
|
|
1112
|
-
} catch {
|
|
1113
|
-
return null;
|
|
1114
|
-
}
|
|
1115
|
-
})();
|
|
1116
|
-
const readSaved = (key) => {
|
|
1117
|
-
if (!ls) return [];
|
|
1118
|
-
try {
|
|
1119
|
-
const raw = ls.getItem(SAVED_PREFIX + key);
|
|
1120
|
-
return normalizeSavedQueries(raw ? JSON.parse(raw) : []);
|
|
1121
|
-
} catch {
|
|
1122
|
-
return [];
|
|
1123
|
-
}
|
|
1124
|
-
};
|
|
1125
|
-
const writeSaved = (key, items) => {
|
|
1126
|
-
if (ls) ls.setItem(SAVED_PREFIX + key, JSON.stringify(items));
|
|
1127
|
-
};
|
|
1128
|
-
const readDefaultId = (key) => {
|
|
1129
|
-
if (!ls) return null;
|
|
1130
|
-
return ls.getItem(DEFAULT_PREFIX + key);
|
|
1131
|
-
};
|
|
1132
|
-
const writeDefaultId = (key, id) => {
|
|
1133
|
-
if (!ls) return;
|
|
1134
|
-
if (id) ls.setItem(DEFAULT_PREFIX + key, id);
|
|
1135
|
-
else ls.removeItem(DEFAULT_PREFIX + key);
|
|
1136
|
-
};
|
|
1137
|
-
return {
|
|
1138
|
-
async loadLast(key) {
|
|
1139
|
-
if (!ls) return null;
|
|
1140
|
-
try {
|
|
1141
|
-
const raw = ls.getItem(LAST_PREFIX + key);
|
|
1142
|
-
return raw ? normalizeQueryState(JSON.parse(raw)) : null;
|
|
1143
|
-
} catch {
|
|
1144
|
-
return null;
|
|
1145
|
-
}
|
|
1146
|
-
},
|
|
1147
|
-
async saveLast(key, query) {
|
|
1148
|
-
if (ls) ls.setItem(LAST_PREFIX + key, JSON.stringify(normalizeQueryState(query)));
|
|
1149
|
-
},
|
|
1150
|
-
async listSaved(key) {
|
|
1151
|
-
return readSaved(key).sort((a, b) => b.savedAt - a.savedAt);
|
|
1152
|
-
},
|
|
1153
|
-
async loadDefaultSaved(key) {
|
|
1154
|
-
const id = readDefaultId(key);
|
|
1155
|
-
if (!id) return null;
|
|
1156
|
-
const found = readSaved(key).find((q) => q.id === id);
|
|
1157
|
-
if (!found) writeDefaultId(key, null);
|
|
1158
|
-
return found ?? null;
|
|
1159
|
-
},
|
|
1160
|
-
async setDefaultSaved(key, id) {
|
|
1161
|
-
if (id && !readSaved(key).some((q) => q.id === id)) {
|
|
1162
|
-
throw new Error(`Saved query "${id}" does not exist.`);
|
|
1163
|
-
}
|
|
1164
|
-
writeDefaultId(key, id);
|
|
1165
|
-
},
|
|
1166
|
-
async saveNamed(key, name, query, savedAt) {
|
|
1167
|
-
const normalizedName = name.trim();
|
|
1168
|
-
if (!normalizedName || normalizedName.length > MAX_SAVED_NAME_LENGTH) {
|
|
1169
|
-
throw new Error(`Saved query name must be between 1 and ${MAX_SAVED_NAME_LENGTH} characters.`);
|
|
1170
|
-
}
|
|
1171
|
-
const items = readSaved(key);
|
|
1172
|
-
if (items.length >= MAX_SAVED_QUERIES) throw new Error(`At most ${MAX_SAVED_QUERIES} saved queries are allowed.`);
|
|
1173
|
-
if (items.some((q) => q.name === normalizedName)) {
|
|
1174
|
-
throw new Error(`Saved query "${normalizedName}" already exists.`);
|
|
1175
|
-
}
|
|
1176
|
-
const item = {
|
|
1177
|
-
id: `${savedAt}-${Math.round((savedAt * 9301 + 49297) % 233280)}`,
|
|
1178
|
-
name: normalizedName,
|
|
1179
|
-
savedAt,
|
|
1180
|
-
query: normalizeQueryState(query)
|
|
1181
|
-
};
|
|
1182
|
-
items.push(item);
|
|
1183
|
-
writeSaved(key, items);
|
|
1184
|
-
return item;
|
|
1185
|
-
},
|
|
1186
|
-
async deleteSaved(key, id) {
|
|
1187
|
-
writeSaved(
|
|
1188
|
-
key,
|
|
1189
|
-
readSaved(key).filter((q) => q.id !== id)
|
|
1190
|
-
);
|
|
1191
|
-
if (readDefaultId(key) === id) writeDefaultId(key, null);
|
|
1192
|
-
}
|
|
1193
|
-
};
|
|
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 } : {} };
|
|
1194
979
|
}
|
|
1195
980
|
|
|
1196
981
|
// src/formula.ts
|
|
@@ -1551,7 +1336,7 @@ var FormulaError = class extends Error {
|
|
|
1551
1336
|
this.to = to;
|
|
1552
1337
|
}
|
|
1553
1338
|
};
|
|
1554
|
-
function compileFormula(source, fields, resolve) {
|
|
1339
|
+
function compileFormula(source, fields, resolve, extension) {
|
|
1555
1340
|
if (!source.trim() || source.length > 1e4)
|
|
1556
1341
|
throw new FormulaError("Formula must contain 1\u201310,000 characters.");
|
|
1557
1342
|
const tokens = [];
|
|
@@ -1752,6 +1537,8 @@ function compileFormula(source, fields, resolve) {
|
|
|
1752
1537
|
n.valueType = t === "enum" ? "text" : t;
|
|
1753
1538
|
return n.valueType;
|
|
1754
1539
|
}
|
|
1540
|
+
const extended = extension?.(n, (arg) => check(arg, level + 1));
|
|
1541
|
+
if (extended !== void 0) return extended;
|
|
1755
1542
|
const ts = n.args.map((a) => check(a, level + 1));
|
|
1756
1543
|
const need = (index, t) => {
|
|
1757
1544
|
if (ts[index] !== void 0 && ts[index] !== "null" && ts[index] !== t)
|
|
@@ -1852,7 +1639,7 @@ function compileFormula(source, fields, resolve) {
|
|
|
1852
1639
|
const type = check(ast);
|
|
1853
1640
|
return { ast, dependencies: [...deps].sort(), type };
|
|
1854
1641
|
}
|
|
1855
|
-
function formulaRuntime(ast, inputs) {
|
|
1642
|
+
function formulaRuntime(ast, inputs, extension) {
|
|
1856
1643
|
const fail = (s) => {
|
|
1857
1644
|
throw new Error(s);
|
|
1858
1645
|
};
|
|
@@ -1906,6 +1693,11 @@ function formulaRuntime(ast, inputs) {
|
|
|
1906
1693
|
if (n.valueType === "textarray") return arr(v);
|
|
1907
1694
|
return v;
|
|
1908
1695
|
}
|
|
1696
|
+
const extended = extension?.(n);
|
|
1697
|
+
if (extended) {
|
|
1698
|
+
if (extended.error) fail(extended.error);
|
|
1699
|
+
return bound(extended.value);
|
|
1700
|
+
}
|
|
1909
1701
|
const name = n.name;
|
|
1910
1702
|
const args = n.args;
|
|
1911
1703
|
if (name === "IF") return run(run(args[0]) === true ? args[1] : args[2]);
|
|
@@ -2296,6 +2088,1552 @@ function formulaRuntime(ast, inputs) {
|
|
|
2296
2088
|
};
|
|
2297
2089
|
}
|
|
2298
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
|
+
}
|
|
2299
3637
|
export {
|
|
2300
3638
|
AGG_OPS_BY_TYPE,
|
|
2301
3639
|
AGG_OPS_NEEDING_FIELD,
|
|
@@ -2309,6 +3647,7 @@ export {
|
|
|
2309
3647
|
MAX_QUERY_LIMIT,
|
|
2310
3648
|
MAX_QUERY_OFFSET,
|
|
2311
3649
|
MAX_QUERY_TOKEN_LENGTH,
|
|
3650
|
+
MAX_RELATIVE_TIME_MS,
|
|
2312
3651
|
MAX_SELECT_COLUMNS,
|
|
2313
3652
|
MAX_WHERE_CLAUSES,
|
|
2314
3653
|
NULLARY_OPS,
|
|
@@ -2318,15 +3657,23 @@ export {
|
|
|
2318
3657
|
aggOpsForField,
|
|
2319
3658
|
applyAggregations,
|
|
2320
3659
|
applyQuery,
|
|
3660
|
+
boxSummary,
|
|
2321
3661
|
coerceValue,
|
|
3662
|
+
compareMetricValues,
|
|
2322
3663
|
compileFormula,
|
|
3664
|
+
compileMetric,
|
|
2323
3665
|
computedFieldName,
|
|
2324
3666
|
createSetFilter,
|
|
2325
3667
|
decodeQuery,
|
|
2326
3668
|
encodeQuery,
|
|
3669
|
+
evaluateMetricReductions,
|
|
3670
|
+
evaluateMetrics,
|
|
2327
3671
|
filterValues,
|
|
2328
3672
|
formulaRuntime,
|
|
2329
3673
|
groupPreview,
|
|
3674
|
+
hasRegex,
|
|
3675
|
+
histogramCounts,
|
|
3676
|
+
histogramEdges,
|
|
2330
3677
|
httpComputedColumnStore,
|
|
2331
3678
|
indexFields,
|
|
2332
3679
|
isComputedCellError,
|
|
@@ -2345,19 +3692,32 @@ export {
|
|
|
2345
3692
|
matchesClause,
|
|
2346
3693
|
memoryComputedColumnStore,
|
|
2347
3694
|
memoryStorageAdapter,
|
|
3695
|
+
metricAggregatePlans,
|
|
3696
|
+
metricComputationKey,
|
|
3697
|
+
metricDependencies,
|
|
3698
|
+
metricExpression,
|
|
3699
|
+
metricPlans,
|
|
2348
3700
|
negateClause,
|
|
2349
3701
|
normalizeQueryState,
|
|
2350
3702
|
opAllowedForType,
|
|
2351
3703
|
opPairsForField,
|
|
2352
3704
|
opsForField,
|
|
3705
|
+
parseRelativeDuration,
|
|
3706
|
+
percentile,
|
|
2353
3707
|
predicatesOf,
|
|
2354
3708
|
queriesEqual,
|
|
2355
3709
|
readFieldValue,
|
|
2356
3710
|
readSetFilter,
|
|
3711
|
+
reduceMetricValues,
|
|
3712
|
+
resolveFieldName,
|
|
3713
|
+
selectMetricShownRows,
|
|
2357
3714
|
selectedFields,
|
|
2358
3715
|
stripSetFilter,
|
|
2359
3716
|
toAggregationQuery,
|
|
3717
|
+
toMetricQuery,
|
|
2360
3718
|
toServerQuery,
|
|
2361
|
-
|
|
3719
|
+
toServerQueryV2,
|
|
3720
|
+
validateComputedColumn,
|
|
3721
|
+
validateMetric
|
|
2362
3722
|
};
|
|
2363
3723
|
//# sourceMappingURL=index.js.map
|