@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/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 || isComputedField(field) || item.dir !== "asc" && item.dir !== "desc") continue;
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 || typeof item.op !== "string" || !AGG_OPS.has(item.op) || !Array.isArray(item.groupBy)) continue;
304
- const groupBy = item.groupBy.slice(0, MAX_GROUP_BY_FIELDS).map((field2) => boundedString(field2, MAX_FIELD_NAME_LENGTH)).filter((field2) => field2 != null);
305
- const aggregation = { id, op: item.op, groupBy };
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 = fallback.aggregations.map((aggregation) => ({ ...aggregation, groupBy: [...aggregation.groupBy] }));
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 false;
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
- return { where, aggregations };
790
- }
791
-
792
- // src/apply.ts
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
- validateComputedColumn
3719
+ toServerQueryV2,
3720
+ validateComputedColumn,
3721
+ validateMetric
2362
3722
  };
2363
3723
  //# sourceMappingURL=index.js.map