@pythia-software/query-table-core 0.4.2 → 0.6.0

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