@pythia-software/query-table-core 0.9.6 → 0.9.8

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
@@ -23,10 +23,10 @@ function parseCellDisplay(raw, context = "cell display") {
23
23
  throw Error(`${context}: ${reason}`);
24
24
  };
25
25
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) return fail("expected an object");
26
- const record2 = raw;
27
- if (typeof record2.kind !== "string" || !Object.prototype.hasOwnProperty.call(options, record2.kind)) return fail("unsupported kind");
28
- const allowed = options[record2.kind];
29
- for (const [key, value] of Object.entries(record2)) {
26
+ const record3 = raw;
27
+ if (typeof record3.kind !== "string" || !Object.prototype.hasOwnProperty.call(options, record3.kind)) return fail("unsupported kind");
28
+ const allowed = options[record3.kind];
29
+ for (const [key, value] of Object.entries(record3)) {
30
30
  if (key === "kind") continue;
31
31
  const rule = Object.prototype.hasOwnProperty.call(allowed, key) ? allowed[key] : void 0;
32
32
  if (!rule) return fail(`unsupported option ${key}`);
@@ -40,13 +40,16 @@ function parseCellDisplay(raw, context = "cell display") {
40
40
  if (key === "pattern" && value.length > 160) return fail("pattern must be at most 160 characters");
41
41
  }
42
42
  return {
43
- ...record2,
44
- ...record2.labels ? { labels: { ...record2.labels } } : {},
45
- ...record2.tones ? { tones: { ...record2.tones } } : {}
43
+ ...record3,
44
+ ...record3.labels ? { labels: { ...record3.labels } } : {},
45
+ ...record3.tones ? { tones: { ...record3.tones } } : {}
46
46
  };
47
47
  }
48
48
 
49
49
  // src/properties.ts
50
+ function isPropertyFieldLoaded(field, loadedFields) {
51
+ return !loadedFields || loadedFields.includes(field.name) || field.source.kind === "derived" && field.source.dependencies !== void 0 && field.source.dependencies.every((name) => loadedFields.includes(name));
52
+ }
50
53
  var MAX_PROPERTIES = 200;
51
54
  var string = (value, max = 256) => typeof value === "string" && value.length > 0 && value.length <= max;
52
55
  var record = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
@@ -444,6 +447,71 @@ function renderItemPageTemplate(template, schema, row, id, query) {
444
447
  return fillUrlTemplate(template, ITEM_PAGE_TOKENS, (token) => token === "id" ? id : token === "query" ? query : readFieldValue(schema.fields.find((field) => field.name === token.slice(6)), row));
445
448
  }
446
449
 
450
+ // src/jsonInspection.ts
451
+ var MAX_JSON_INSPECTIONS = 200;
452
+ var MAX_JSON_PATHS = 5e3;
453
+ var record2 = (x) => x !== null && typeof x === "object" && !Array.isArray(x);
454
+ var text = (x, max) => typeof x === "string" && x.length > 0 && x.length <= max;
455
+ function isJsonPath(path) {
456
+ return text(path, 1e4) && /^\$(?:\[(?:0|[1-9]\d*|"(?:[^"\\\x00-\x1f]|\\(?:["\\/bfnrt]|u[\da-fA-F]{4}))*")\])*$/.test(path);
457
+ }
458
+ function isJsonBucketId(path) {
459
+ if (!text(path, 10032)) return false;
460
+ const index = path.lastIndexOf(":bucket:");
461
+ return index > 0 && isJsonPath(path.slice(0, index)) && /^(?:0|[1-9]\d*)$/.test(path.slice(index + 8)) && Number.isSafeInteger(Number(path.slice(index + 8))) && Number(path.slice(index + 8)) >= 20 && Number(path.slice(index + 8)) % 20 === 0;
462
+ }
463
+ function jsonInspectionScopeKey(scope) {
464
+ return JSON.stringify([scope.dataset, scope.rowId, scope.field]);
465
+ }
466
+ function parseJsonInspectionState(input) {
467
+ if (!record2(input) || input.version !== 1 || typeof input.open !== "boolean" || typeof input.raw !== "boolean" || typeof input.search !== "string" || input.search.length > 1e4 || input.searchMode !== "filter" && input.searchMode !== "find" || typeof input.visibleEntryLimit !== "number" || !Number.isInteger(input.visibleEntryLimit) || input.visibleEntryLimit < 1 || input.visibleEntryLimit > 1e4)
468
+ throw Error("Invalid or unsupported JSON inspection state.");
469
+ const paths = (value, valid) => {
470
+ if (!Array.isArray(value) || value.length > MAX_JSON_PATHS || !value.every(valid) || new Set(value).size !== value.length)
471
+ throw Error("Invalid JSON inspection paths.");
472
+ return value.slice();
473
+ };
474
+ const state = {
475
+ version: 1,
476
+ open: input.open,
477
+ search: input.search,
478
+ searchMode: input.searchMode,
479
+ raw: input.raw,
480
+ collapsedPaths: paths(input.collapsedPaths, isJsonPath),
481
+ expandedBuckets: paths(input.expandedBuckets, isJsonBucketId),
482
+ visibleEntryLimit: input.visibleEntryLimit,
483
+ searchOverrides: []
484
+ };
485
+ if (input.selectedMatch !== void 0) {
486
+ const match = input.selectedMatch;
487
+ if (!record2(match) || !isJsonPath(match.path) || match.part !== "key" && match.part !== "value" && match.part !== "path" || typeof match.start !== "number" || !Number.isSafeInteger(match.start) || match.start < 0 || match.start > 1048576)
488
+ throw Error("Invalid JSON match identity.");
489
+ state.selectedMatch = { path: match.path, part: match.part, start: match.start };
490
+ }
491
+ if (!Array.isArray(input.searchOverrides) || input.searchOverrides.length > MAX_JSON_PATHS) throw Error("Invalid JSON search expansion overrides.");
492
+ const seen = /* @__PURE__ */ new Set();
493
+ for (const entry of input.searchOverrides) {
494
+ if (!record2(entry) || !(isJsonPath(entry.path) || isJsonBucketId(entry.path)) || typeof entry.expanded !== "boolean" || seen.has(entry.path))
495
+ throw Error("Invalid JSON search expansion override.");
496
+ seen.add(entry.path);
497
+ state.searchOverrides.push({ path: entry.path, expanded: entry.expanded });
498
+ }
499
+ return state;
500
+ }
501
+ function parseJsonInspections(input) {
502
+ if (!Array.isArray(input) || input.length > MAX_JSON_INSPECTIONS) throw Error("Invalid JSON inspection scopes.");
503
+ const seen = /* @__PURE__ */ new Set();
504
+ return input.map((entry) => {
505
+ if (!record2(entry) || !text(entry.dataset, 256) || !text(entry.field, 256) || !(text(entry.rowId, 1e3) || typeof entry.rowId === "number" && Number.isFinite(entry.rowId)))
506
+ throw Error("Invalid JSON inspection scope.");
507
+ const scope = { dataset: entry.dataset, rowId: entry.rowId, field: entry.field };
508
+ const key = jsonInspectionScopeKey(scope);
509
+ if (seen.has(key)) throw Error("Duplicate JSON inspection scope.");
510
+ seen.add(key);
511
+ return { ...scope, state: parseJsonInspectionState(entry.state) };
512
+ });
513
+ }
514
+
447
515
  // src/metricNormalize.ts
448
516
  var METRIC_OPTION_SPECS = {
449
517
  top: {
@@ -451,16 +519,29 @@ var METRIC_OPTION_SPECS = {
451
519
  expressionY: "string",
452
520
  scope: ["allMatching", "shownRows"],
453
521
  groupLimit: "integer",
454
- minCount: "positiveInteger",
522
+ minCount: "minCount",
455
523
  share: ["total", "group0"]
456
524
  },
525
+ minCount: {
526
+ min: "positiveInteger",
527
+ distinct: "string",
528
+ sum: "string"
529
+ },
457
530
  window: {
458
531
  reduce: ["total", "mean"],
459
532
  partial: ["omit", "include"]
460
533
  },
461
534
  dateGrouping: {
462
535
  field: "string",
463
- interval: ["hour", "day", "week", "month", "year"],
536
+ interval: [
537
+ "hour",
538
+ "day",
539
+ "week",
540
+ "month",
541
+ "year",
542
+ "hourOfDay",
543
+ "dayOfWeek"
544
+ ],
464
545
  timeZone: "string",
465
546
  includeEmpty: "boolean",
466
547
  range: ["query", "observed", "custom"],
@@ -468,6 +549,10 @@ var METRIC_OPTION_SPECS = {
468
549
  end: "string",
469
550
  partialIntervals: ["include", "exclude"]
470
551
  },
552
+ occurrence: {
553
+ interval: ["hour", "day"],
554
+ includeEmpty: "boolean"
555
+ },
471
556
  display: {
472
557
  kind: [
473
558
  "auto",
@@ -542,12 +627,20 @@ var METRIC_OPTION_SPECS = {
542
627
  };
543
628
  function normalizeMetricOptions(raw) {
544
629
  const out = {}, diagnostics = [];
545
- const record2 = (x) => !!x && typeof x === "object" && !Array.isArray(x);
630
+ const record3 = (x) => !!x && typeof x === "object" && !Array.isArray(x);
546
631
  const copy = (input, specs) => {
547
632
  const result = {};
548
633
  for (const [key, spec] of Object.entries(specs)) {
549
634
  const v = input[key];
550
635
  if (v === void 0) continue;
636
+ if (spec === "minCount") {
637
+ const m = record3(v) ? copy(v, METRIC_OPTION_SPECS.minCount) : v;
638
+ const named = (x) => x === void 0 || typeof x === "string" && !!x && x.length <= 256;
639
+ if (record3(m) ? m.min !== void 0 && m.distinct === void 0 !== (m.sum === void 0) && named(m.distinct) && named(m.sum) : typeof m === "number" && Number.isSafeInteger(m) && m >= 1 && m <= 1e6)
640
+ result[key] = m;
641
+ else diagnostics.push(`Invalid metric ${key}.`);
642
+ continue;
643
+ }
551
644
  if (spec === "string" ? typeof v === "string" && v.length <= 1e4 : spec === "number" ? typeof v === "number" && Number.isFinite(v) : spec === "boolean" ? typeof v === "boolean" : spec === "integer" || spec === "positiveInteger" ? typeof v === "number" && Number.isSafeInteger(v) && v >= (spec === "positiveInteger" ? 1 : 0) && v <= 1e6 : typeof v === "string" && spec.includes(v))
552
645
  result[key] = v;
553
646
  else diagnostics.push(`Invalid metric ${key}.`);
@@ -559,7 +652,7 @@ function normalizeMetricOptions(raw) {
559
652
  copy(raw, METRIC_OPTION_SPECS.top)
560
653
  );
561
654
  if (raw.window !== void 0) {
562
- if (record2(raw.window)) {
655
+ if (record3(raw.window)) {
563
656
  const w = copy(raw.window, METRIC_OPTION_SPECS.window);
564
657
  const periods = raw.window.periods;
565
658
  if (periods === "all" || Number.isSafeInteger(periods) && periods >= 0 && periods <= 1e6)
@@ -569,40 +662,48 @@ function normalizeMetricOptions(raw) {
569
662
  } else diagnostics.push("Invalid metric window.");
570
663
  }
571
664
  if (raw.dateGrouping !== void 0) {
572
- if (record2(raw.dateGrouping)) {
665
+ if (record3(raw.dateGrouping)) {
573
666
  const d = copy(raw.dateGrouping, METRIC_OPTION_SPECS.dateGrouping);
574
667
  if (typeof d.field !== "string" || !d.field || d.field.length > 256 || !d.interval || typeof d.includeEmpty !== "boolean" || !d.range)
575
668
  diagnostics.push("Invalid metric date grouping.");
669
+ const occurrence = raw.dateGrouping.occurrence;
670
+ if (record3(occurrence)) {
671
+ const o = copy(occurrence, METRIC_OPTION_SPECS.occurrence);
672
+ if (!o.interval || typeof o.includeEmpty !== "boolean")
673
+ diagnostics.push("Invalid metric date grouping occurrence.");
674
+ d.occurrence = o;
675
+ } else if (occurrence !== void 0)
676
+ diagnostics.push("Invalid metric date grouping occurrence.");
576
677
  out.dateGrouping = d;
577
678
  } else diagnostics.push("Invalid metric date grouping.");
578
679
  }
579
680
  if (raw.display !== void 0) {
580
- if (record2(raw.display)) {
681
+ if (record3(raw.display)) {
581
682
  if (typeof raw.display.kind !== "string")
582
683
  diagnostics.push("Invalid metric display kind.");
583
684
  const d = copy(raw.display, METRIC_OPTION_SPECS.display);
584
685
  for (const k of ["xScale", "yScale"]) {
585
- if (record2(raw.display[k]))
686
+ if (record3(raw.display[k]))
586
687
  d[k] = copy(raw.display[k], METRIC_OPTION_SPECS.scale);
587
688
  else if (raw.display[k] !== void 0)
588
689
  diagnostics.push(`Invalid metric ${k}.`);
589
690
  }
590
691
  for (const k of ["format", "xFormat", "yFormat"])
591
- if (record2(raw.display[k]))
692
+ if (record3(raw.display[k]))
592
693
  d[k] = copy(raw.display[k], METRIC_OPTION_SPECS.format);
593
- if (record2(raw.display.list))
694
+ if (record3(raw.display.list))
594
695
  d.list = copy(raw.display.list, METRIC_OPTION_SPECS.list);
595
- if (record2(raw.display.pivot))
696
+ if (record3(raw.display.pivot))
596
697
  d.pivot = copy(raw.display.pivot, METRIC_OPTION_SPECS.pivot);
597
698
  out.display = { kind: "auto", ...d };
598
699
  } else diagnostics.push("Invalid metric display.");
599
700
  }
600
701
  if (raw.distribution !== void 0) {
601
- if (record2(raw.distribution))
702
+ if (record3(raw.distribution))
602
703
  out.distribution = copy(raw.distribution, METRIC_OPTION_SPECS.distribution);
603
704
  else diagnostics.push("Invalid metric distribution.");
604
705
  }
605
- if (record2(raw.layout)) {
706
+ if (record3(raw.layout)) {
606
707
  const l = copy(raw.layout, METRIC_OPTION_SPECS.layout);
607
708
  for (const k of ["widthRem", "heightRem", "minWidthRem", "minHeightRem"])
608
709
  l[k] = Math.max(1, Math.min(1e3, Number(l[k] ?? 1)));
@@ -614,7 +715,7 @@ function normalizeMetricOptions(raw) {
614
715
  diagnostics.push("Invalid metric sort.");
615
716
  if (Array.isArray(raw.sort))
616
717
  out.sort = raw.sort.slice(0, 20).flatMap((s) => {
617
- if (!record2(s) || typeof s.key !== "string" || !/^(value|y|count|samples|group(?:0|[1-9][0-9]?))$/.test(s.key) || !["asc", "desc"].includes(String(s.dir))) {
718
+ if (!record3(s) || typeof s.key !== "string" || !/^(value|y|count|samples|group(?:0|[1-9][0-9]?))$/.test(s.key) || !["asc", "desc"].includes(String(s.dir))) {
618
719
  diagnostics.push("Invalid metric sort.");
619
720
  return [];
620
721
  }
@@ -634,138 +735,620 @@ function normalizeMetricOptions(raw) {
634
735
  return out;
635
736
  }
636
737
 
637
- // src/query.ts
638
- function isOrGroup(term) {
639
- return Array.isArray(term.any);
738
+ // src/relativeTime.ts
739
+ var UNIT_MS = { ms: 1, s: 1e3, m: 6e4, h: 36e5, d: 864e5, w: 6048e5 };
740
+ var MAX_RELATIVE_TIME_MS = 864e13;
741
+ function parseRelativeDuration(value) {
742
+ const valid = /^[+-](?:[0-9]+(?:ms|s|m|h|d|w))+$/.exec(value);
743
+ if (!valid || valid[0] !== value) return null;
744
+ const body = value.slice(1);
745
+ const components = /([0-9]+)(ms|s|m|h|d|w)/g;
746
+ let total = 0;
747
+ for (const match of body.matchAll(components)) {
748
+ const amount = Number(match[1]);
749
+ const unit = UNIT_MS[match[2]];
750
+ if (!Number.isSafeInteger(amount) || amount > Math.floor((MAX_RELATIVE_TIME_MS - total) / unit)) return null;
751
+ total += amount * unit;
752
+ }
753
+ return value[0] === "-" && total !== 0 ? -total : total;
640
754
  }
641
- function predicatesOf(term) {
642
- return isOrGroup(term) ? term.any : [term];
755
+ function evaluationTime(options2) {
756
+ const now = options2.now ?? Date.now();
757
+ if (!Number.isSafeInteger(now) || Math.abs(now) > MAX_RELATIVE_TIME_MS) throw new Error("Invalid query evaluation time");
758
+ return now;
643
759
  }
644
- var ITEM_DETAILS_COLUMN = "__qt_item_details__";
645
- var EMPTY_QUERY = {
646
- select: [],
647
- where: [],
648
- orderBy: [],
649
- limit: 100,
650
- offset: 0
651
- };
652
- var MAX_QUERY_LIMIT = Number.MAX_SAFE_INTEGER;
653
- var MAX_QUERY_OFFSET = 1e6;
654
- var MAX_SELECT_COLUMNS = 200;
655
- var MAX_WHERE_CLAUSES = 100;
656
- var MAX_ORDER_BY_TERMS = 20;
657
- var MAX_AGGREGATIONS = 20;
658
- var MAX_GROUP_BY_FIELDS = 20;
659
- var MAX_QUERY_TOKEN_LENGTH = 2 * 1024 * 1024;
660
- var MAX_FIELD_NAME_LENGTH = 256;
661
- var MAX_FILTER_VALUE_LENGTH = 1e4;
662
- var MAX_LABEL_LENGTH = 1e3;
663
- var MIN_COLUMN_WIDTH = 24;
664
- var MAX_COLUMN_WIDTH = 2e3;
665
- var FILTER_OPS = /* @__PURE__ */ new Set([
666
- "=",
667
- "!=",
668
- ">",
669
- ">=",
670
- "<",
671
- "<=",
672
- "contains",
673
- "starts_with",
674
- "ends_with",
675
- "matches_regex",
676
- "not_matches_regex",
677
- "length_gt",
678
- "length_lt",
679
- "length_eq",
680
- "includes",
681
- "is_null",
682
- "is_not_null"
683
- ]);
684
- var AGG_OPS = /* @__PURE__ */ new Set(["count", "count_distinct", "sum", "avg", "min", "max"]);
685
- function isRecord(value) {
686
- return value != null && typeof value === "object" && !Array.isArray(value);
760
+ function relativeTimestamp(now, offsetMs) {
761
+ const result = now + offsetMs;
762
+ if (!Number.isSafeInteger(result) || Math.abs(result) > MAX_RELATIVE_TIME_MS) throw new Error("Relative time exceeds datetime limits");
763
+ return result;
687
764
  }
688
- function boundedString(value, maxLength) {
689
- return typeof value === "string" && value.length > 0 && value.length <= maxLength ? value : null;
765
+
766
+ // src/ops.ts
767
+ var NULLITY = ["is_null", "is_not_null"];
768
+ var COMPLEMENT_OP = {
769
+ "=": "!=",
770
+ "!=": "=",
771
+ ">": "<=",
772
+ "<=": ">",
773
+ ">=": "<",
774
+ "<": ">=",
775
+ is_null: "is_not_null",
776
+ is_not_null: "is_null",
777
+ matches_regex: "not_matches_regex",
778
+ not_matches_regex: "matches_regex"
779
+ };
780
+ function negateClause(clause, allowedOps) {
781
+ if (clause.value === "0" && !clause.negated) {
782
+ const zeroComplement = clause.op === "length_gt" ? "length_eq" : clause.op === "length_eq" ? "length_gt" : null;
783
+ if (zeroComplement && (!allowedOps || allowedOps.includes(zeroComplement))) {
784
+ return { field: clause.field, op: zeroComplement, value: "0" };
785
+ }
786
+ }
787
+ const complement = COMPLEMENT_OP[clause.op];
788
+ if (complement && (!allowedOps || allowedOps.includes(complement))) {
789
+ const next2 = { field: clause.field, op: complement, value: clause.value };
790
+ return next2;
791
+ }
792
+ const next = { field: clause.field, op: clause.op, value: clause.value };
793
+ if (!clause.negated) next.negated = true;
794
+ return next;
690
795
  }
691
- function boundedInteger(value, fallback, min, max) {
692
- if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
693
- return Math.max(min, Math.min(max, Math.round(value)));
796
+ function isNegativePredicate(clause) {
797
+ return Boolean(clause.negated) || clause.op === "!=" || clause.op === "not_matches_regex" || clause.op === "is_not_null";
694
798
  }
695
- function normalizeWhereClause(item) {
696
- if (!isRecord(item)) return null;
697
- const field = boundedString(item.field, MAX_FIELD_NAME_LENGTH);
698
- const value = typeof item.value === "string" && item.value.length <= MAX_FILTER_VALUE_LENGTH ? item.value : null;
699
- if (!field || typeof item.op !== "string" || !FILTER_OPS.has(item.op) || value == null) {
700
- return null;
799
+ var POSITIVE_OP_ORDER = ["=", ">", ">=", "contains", "starts_with", "ends_with", "length_gt", "length_lt", "length_eq", "matches_regex", "includes"];
800
+ function opPairsForField(field) {
801
+ const ops = opsForField(field);
802
+ const allow = new Set(ops);
803
+ const pairs = [];
804
+ for (const op of POSITIVE_OP_ORDER) {
805
+ if (!allow.has(op)) continue;
806
+ const neg = negateClause({ field: "", op, value: "" }, ops);
807
+ pairs.push({ keep: { op, negated: false }, exclude: { op: neg.op, negated: Boolean(neg.negated) } });
701
808
  }
702
- const clause = { field, op: item.op, value };
703
- if (item.negated === true) clause.negated = true;
704
- return clause;
809
+ if (allow.has("is_null") || allow.has("is_not_null")) {
810
+ pairs.push({ keep: { op: "is_null", negated: false }, exclude: { op: "is_not_null", negated: false } });
811
+ }
812
+ return pairs;
705
813
  }
706
- function cloneWhereTerm(term) {
707
- const copy = isOrGroup(term) ? { ...term, any: term.any.map((c) => ({ ...c })) } : { ...term };
708
- if (term.setFilter) copy.setFilter = { ...term.setFilter, values: [...term.setFilter.values] };
709
- return copy;
814
+ var OPS_BY_TYPE = {
815
+ text: [
816
+ "=",
817
+ "!=",
818
+ "contains",
819
+ "starts_with",
820
+ "ends_with",
821
+ "matches_regex",
822
+ "not_matches_regex",
823
+ "length_gt",
824
+ "length_lt",
825
+ "length_eq",
826
+ ...NULLITY
827
+ ],
828
+ enum: ["=", "!=", ...NULLITY],
829
+ number: ["=", "!=", ">", ">=", "<", "<=", ...NULLITY],
830
+ datetime: ["=", "!=", ">", ">=", "<", "<=", ...NULLITY],
831
+ bool: ["=", "!=", ...NULLITY],
832
+ textarray: ["includes", ...NULLITY]
833
+ };
834
+ var NULLARY_OPS = new Set(NULLITY);
835
+ function opsForField(field) {
836
+ return field.filter?.ops ?? OPS_BY_TYPE[field.type];
710
837
  }
711
- function normalizeSetFilter(item) {
712
- if (!isRecord(item)) return void 0;
713
- const id = boundedString(item.id, MAX_FIELD_NAME_LENGTH);
714
- if (!id || typeof item.mode !== "string" || !["any", "all", "none", "empty"].includes(item.mode) || !Array.isArray(item.values)) return void 0;
715
- if (item.values.length > MAX_WHERE_CLAUSES || !item.values.every((value) => typeof value === "string" && value.length <= MAX_FILTER_VALUE_LENGTH)) return void 0;
716
- return { id, mode: item.mode, values: [...item.values] };
838
+ function opAllowedForType(type, op) {
839
+ return OPS_BY_TYPE[type].includes(op);
717
840
  }
718
- function normalizeWhereTerms(input) {
719
- const where = [];
720
- let literalBudget = MAX_WHERE_CLAUSES;
721
- for (const item of input) {
722
- if (literalBudget <= 0) break;
723
- const start = where.length;
724
- if (isRecord(item) && Array.isArray(item.any)) {
725
- const members = [];
726
- for (const inner of item.any) {
727
- if (literalBudget <= 0) break;
728
- const clause = normalizeWhereClause(inner);
729
- if (clause) {
730
- members.push(clause);
731
- literalBudget--;
732
- }
733
- }
734
- if (members.length === 1) where.push(members[0]);
735
- else if (members.length > 1) where.push({ any: members });
736
- } else {
737
- const clause = normalizeWhereClause(item);
738
- if (clause) {
739
- where.push(clause);
740
- literalBudget--;
741
- }
841
+ function coerceValue(type, raw, now) {
842
+ switch (type) {
843
+ case "number": {
844
+ const n = Number(raw);
845
+ if (raw.trim() === "" || Number.isNaN(n)) throw new Error(`not a number: ${JSON.stringify(raw)}`);
846
+ return n;
742
847
  }
743
- if (where.length > start && isRecord(item)) {
744
- const metadata = normalizeSetFilter(item.setFilter);
745
- if (metadata) where[start].setFilter = metadata;
848
+ case "bool": {
849
+ const v = raw.toLowerCase();
850
+ if (v === "true" || v === "1" || v === "t") return true;
851
+ if (v === "false" || v === "0" || v === "f") return false;
852
+ throw new Error(`not a bool: ${JSON.stringify(raw)}`);
853
+ }
854
+ case "datetime": {
855
+ if (raw.startsWith("+") || raw.startsWith("-")) {
856
+ const offset = parseRelativeDuration(raw);
857
+ if (offset === null) throw new Error(`not a signed duration: ${JSON.stringify(raw)}`);
858
+ return relativeTimestamp(now ?? Date.now(), offset);
859
+ }
860
+ const ms = Date.parse(raw);
861
+ if (Number.isNaN(ms)) throw new Error(`not a datetime: ${JSON.stringify(raw)}`);
862
+ return ms;
746
863
  }
864
+ default:
865
+ return raw;
747
866
  }
748
- return where;
749
867
  }
750
- function normalizeMetricWhere(value) {
751
- if (value === void 0) return { invalid: false };
752
- if (!Array.isArray(value)) return { invalid: true };
753
- const where = normalizeWhereTerms(value);
754
- const supplied = value.reduce((count, term) => count + (isRecord(term) && Array.isArray(term.any) ? term.any.length : 1), 0);
755
- const kept = where.reduce((count, term) => count + predicatesOf(term).length, 0);
756
- return { ...where.length ? { where } : {}, invalid: supplied !== kept };
868
+
869
+ // src/metricDates.ts
870
+ import { Temporal } from "temporal-polyfill";
871
+ var METRIC_DATE_INTERVALS = [
872
+ "hour",
873
+ "day",
874
+ "week",
875
+ "month",
876
+ "year",
877
+ "hourOfDay",
878
+ "dayOfWeek"
879
+ ];
880
+ var METRIC_CYCLE_INTERVALS = ["hourOfDay", "dayOfWeek"];
881
+ function isMetricCycle(interval) {
882
+ return interval === "hourOfDay" || interval === "dayOfWeek";
757
883
  }
758
- function normalizeQueryState(input, fallback = EMPTY_QUERY) {
759
- return normalizeQuery(input, fallback, true);
884
+ function metricCycleSlots(interval) {
885
+ return interval === "hourOfDay" ? Array.from({ length: 24 }, (_, i) => i) : [1, 2, 3, 4, 5, 6, 7];
760
886
  }
761
- function normalizeQuery(input, fallback, links) {
762
- const raw = isRecord(input) ? input : {};
763
- const select = [];
764
- if (Array.isArray(raw.select)) {
765
- for (const item of raw.select.slice(0, MAX_SELECT_COLUMNS)) {
766
- if (!isRecord(item)) continue;
767
- const field = boundedString(item.field, MAX_FIELD_NAME_LENGTH);
768
- if (!field) continue;
887
+ function metricCalendarUnit(interval) {
888
+ return interval === "hourOfDay" ? "hour" : interval === "dayOfWeek" ? "day" : interval;
889
+ }
890
+ function metricCycleSlot(time, interval, timeZone = "UTC") {
891
+ if (timeZone === "UTC") {
892
+ const date = new Date(time);
893
+ return interval === "hourOfDay" ? date.getUTCHours() : (date.getUTCDay() + 6) % 7 + 1;
894
+ }
895
+ const zoned = Temporal.Instant.fromEpochMilliseconds(time).toZonedDateTimeISO(timeZone);
896
+ return interval === "hourOfDay" ? zoned.hour : zoned.dayOfWeek;
897
+ }
898
+ function metricDateTime(value, timeZone = "UTC") {
899
+ if (typeof value !== "string" || !/^\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?(?:Z|[+-]\d{2}:\d{2}))?$/.test(
900
+ value
901
+ ))
902
+ throw Error(
903
+ "Date grouping requires ISO dates or timestamps with a timezone."
904
+ );
905
+ const time = Date.parse(value), month = Number(value.slice(5, 7)), day = Number(value.slice(8, 10));
906
+ const calendar = /* @__PURE__ */ new Date(0);
907
+ calendar.setUTCFullYear(Number(value.slice(0, 4)), month, 0);
908
+ if (!Number.isFinite(time) || month < 1 || month > 12 || day < 1 || day > calendar.getUTCDate())
909
+ throw Error("Invalid date grouping value.");
910
+ return value.length === 10 && timeZone !== "UTC" ? Temporal.PlainDate.from(value).toZonedDateTime(timeZone).epochMilliseconds : time;
911
+ }
912
+ function metricOffsetTransition(start, end, timeZone) {
913
+ if (end <= start) return void 0;
914
+ const offset = (time) => Temporal.Instant.fromEpochMilliseconds(time).toZonedDateTimeISO(timeZone).offsetNanoseconds;
915
+ const before = offset(start);
916
+ if (before === offset(end)) return void 0;
917
+ while (end - start > 1) {
918
+ const middle = Math.floor((start + end) / 2);
919
+ if (offset(middle) === before) start = middle;
920
+ else end = middle;
921
+ }
922
+ return end;
923
+ }
924
+ function metricDateFloor(time, cycleOrInterval, timeZone = "UTC") {
925
+ const interval = metricCalendarUnit(cycleOrInterval);
926
+ if (timeZone !== "UTC") {
927
+ const zoned = Temporal.Instant.fromEpochMilliseconds(time).toZonedDateTimeISO(timeZone);
928
+ if (interval === "hour") {
929
+ const floor = time - zoned.minute * 6e4 - zoned.second * 1e3 - zoned.millisecond;
930
+ return metricOffsetTransition(floor, time, timeZone) ?? floor;
931
+ }
932
+ let date2 = zoned.toPlainDate();
933
+ if (interval === "week") date2 = date2.subtract({ days: date2.dayOfWeek - 1 });
934
+ if (interval === "month" || interval === "year")
935
+ date2 = date2.with({ day: 1 });
936
+ if (interval === "year") date2 = date2.with({ month: 1 });
937
+ return date2.toZonedDateTime(timeZone).epochMilliseconds;
938
+ }
939
+ const date = new Date(time);
940
+ date.setUTCMinutes(0, 0, 0);
941
+ if (interval !== "hour") date.setUTCHours(0);
942
+ if (interval === "week")
943
+ date.setUTCDate(date.getUTCDate() - (date.getUTCDay() + 6) % 7);
944
+ if (interval === "month" || interval === "year") date.setUTCDate(1);
945
+ if (interval === "year") date.setUTCMonth(0);
946
+ return date.getTime();
947
+ }
948
+ function metricDateNext(time, cycleOrInterval, timeZone = "UTC") {
949
+ const interval = metricCalendarUnit(cycleOrInterval);
950
+ if (timeZone !== "UTC") {
951
+ const zoned = Temporal.Instant.fromEpochMilliseconds(time).toZonedDateTimeISO(timeZone);
952
+ if (interval === "hour") {
953
+ const target = zoned.toPlainDateTime().with({
954
+ minute: 0,
955
+ second: 0,
956
+ millisecond: 0,
957
+ microsecond: 0,
958
+ nanosecond: 0
959
+ }).add({ hours: 1 });
960
+ let next = target.toZonedDateTime(timeZone).epochMilliseconds;
961
+ if (next <= time)
962
+ next = target.toZonedDateTime(timeZone, {
963
+ disambiguation: "later"
964
+ }).epochMilliseconds;
965
+ return metricOffsetTransition(time, next, timeZone) ?? next;
966
+ }
967
+ let date2 = zoned.toPlainDate();
968
+ if (interval === "month") date2 = date2.with({ day: 1 }).add({ months: 1 });
969
+ else if (interval === "year")
970
+ date2 = date2.with({ month: 1, day: 1 }).add({ years: 1 });
971
+ else if (interval === "week")
972
+ date2 = date2.subtract({ days: date2.dayOfWeek - 1 }).add({ weeks: 1 });
973
+ else date2 = date2.add({ days: 1 });
974
+ return date2.toZonedDateTime(timeZone).epochMilliseconds;
975
+ }
976
+ const date = new Date(time);
977
+ if (interval === "hour") date.setUTCHours(date.getUTCHours() + 1);
978
+ else if (interval === "month") date.setUTCMonth(date.getUTCMonth() + 1);
979
+ else if (interval === "year") date.setUTCFullYear(date.getUTCFullYear() + 1);
980
+ else date.setUTCDate(date.getUTCDate() + (interval === "week" ? 7 : 1));
981
+ return date.getTime();
982
+ }
983
+ function metricCustomEndTime(value, timeZone = "UTC") {
984
+ const time = metricDateTime(value, timeZone);
985
+ return value.length === 10 ? metricDateNext(time, "day", timeZone) - 1 : time;
986
+ }
987
+ function isGroupingTimeZone(value) {
988
+ if (typeof value !== "string" || !value || value.length > 80 || /^[+-]/.test(value))
989
+ return false;
990
+ try {
991
+ new Intl.DateTimeFormat("en-US", { timeZone: value });
992
+ return true;
993
+ } catch {
994
+ return false;
995
+ }
996
+ }
997
+ function validateDateGrouping(config) {
998
+ if (typeof config.field !== "string" || !config.field || config.field.length > 256 || !METRIC_DATE_INTERVALS.includes(config.interval) || typeof config.includeEmpty !== "boolean" || !["query", "observed", "custom"].includes(config.range) || config.partialIntervals !== void 0 && !["include", "exclude"].includes(config.partialIntervals))
999
+ throw Error("Invalid date grouping definition.");
1000
+ if (config.timeZone !== void 0 && !isGroupingTimeZone(config.timeZone))
1001
+ throw Error(
1002
+ "Use a valid IANA grouping timezone, such as America/Denver or UTC."
1003
+ );
1004
+ if (config.occurrence !== void 0) {
1005
+ const o = config.occurrence;
1006
+ if (!o || typeof o !== "object" || Array.isArray(o) || !["hour", "day"].includes(o.interval) || typeof o.includeEmpty !== "boolean")
1007
+ throw Error("Invalid date grouping occurrence.");
1008
+ if (!isMetricCycle(config.interval))
1009
+ throw Error(
1010
+ "Occurrences summarise an hour-of-day or day-of-week slot; calendar intervals are already one occurrence each."
1011
+ );
1012
+ if (config.interval === "hourOfDay" && o.interval !== "hour")
1013
+ throw Error("An hour-of-day slot occurs once per hour: use hourly occurrences.");
1014
+ if (config.partialIntervals === "include")
1015
+ throw Error(
1016
+ "Occurrences are always whole: a partial day or hour would bias the typical value. Leave out partialIntervals."
1017
+ );
1018
+ }
1019
+ if (config.range === "custom" && (isMetricCycle(config.interval) ? config.occurrence?.includeEmpty : config.includeEmpty)) {
1020
+ const start = metricDateTime(config.start ?? "", config.timeZone), end = metricCustomEndTime(config.end ?? "", config.timeZone);
1021
+ if (end < start)
1022
+ throw Error("The end date must be on or after the start date.");
1023
+ }
1024
+ }
1025
+ function metricDateRange(config, query, observed, now, canonical) {
1026
+ let min = Infinity, max = -Infinity;
1027
+ for (const time of observed) {
1028
+ min = Math.min(min, time);
1029
+ max = Math.max(max, time);
1030
+ }
1031
+ if (config.range === "observed")
1032
+ return observed.length ? [min, max] : void 0;
1033
+ if (config.range === "custom") {
1034
+ const start2 = metricDateFloor(
1035
+ metricDateTime(config.start ?? "", config.timeZone),
1036
+ config.interval,
1037
+ config.timeZone
1038
+ );
1039
+ const end2 = metricDateFloor(
1040
+ metricCustomEndTime(config.end ?? "", config.timeZone),
1041
+ config.interval,
1042
+ config.timeZone
1043
+ );
1044
+ if (observed.length && (start2 > min || end2 < max))
1045
+ throw Error(
1046
+ "Custom date bounds must include all observed buckets. Narrow the query filters to exclude data."
1047
+ );
1048
+ return [start2, end2];
1049
+ }
1050
+ let start = -Infinity, end = Infinity;
1051
+ for (const term of query.where) {
1052
+ if (isWhereGroup(term) || term.negated || canonical(term.field) !== canonical(config.field))
1053
+ continue;
1054
+ if (!["=", ">", ">=", "<", "<="].includes(term.op)) continue;
1055
+ const time = Number(coerceValue("datetime", term.value, now));
1056
+ if (term.op === ">" || term.op === ">=" || term.op === "=")
1057
+ start = Math.max(start, time + (term.op === ">" ? 1 : 0));
1058
+ if (term.op === "<" || term.op === "<=" || term.op === "=")
1059
+ end = Math.min(end, time - (term.op === "<" ? 1 : 0));
1060
+ }
1061
+ if (!Number.isFinite(start) || !Number.isFinite(end)) {
1062
+ const missing = [
1063
+ ...Number.isFinite(start) ? [] : ["a lower bound (>= or >)"],
1064
+ ...Number.isFinite(end) ? [] : ["an upper bound (<= or <)"]
1065
+ ].join(" and ");
1066
+ throw Error(
1067
+ `Include empty dates with range "query" needs a bounded date filter on ${config.field}: add ${missing} as top-level filters (bounds inside OR groups do not count). Otherwise use range "observed" to fill from the first to the last observed date, or range "custom" with start and end.`
1068
+ );
1069
+ }
1070
+ if (end < start) return void 0;
1071
+ return [
1072
+ metricDateFloor(start, config.interval, config.timeZone),
1073
+ metricDateFloor(end, config.interval, config.timeZone)
1074
+ ];
1075
+ }
1076
+
1077
+ // src/groupKeys.ts
1078
+ var GROUP_KEY_INTERVALS = ["hour", "day", "week", "month", "year"];
1079
+ function isCalendarGroupKey(key) {
1080
+ return typeof key === "object" && key !== null;
1081
+ }
1082
+ function groupKeyField(key) {
1083
+ return isCalendarGroupKey(key) ? key.field : key;
1084
+ }
1085
+ function groupKeyFields(keys) {
1086
+ return keys.map(groupKeyField);
1087
+ }
1088
+ function calendarGroupKey(keys, field) {
1089
+ return keys.find((key) => isCalendarGroupKey(key) && key.field === field);
1090
+ }
1091
+ function calendarKeyReader(key) {
1092
+ let start = Infinity, end = -Infinity, iso = "";
1093
+ return (value) => {
1094
+ if (value == null) return null;
1095
+ const input = value instanceof Date ? value.toISOString() : value;
1096
+ if (typeof input !== "string")
1097
+ throw Error(`Calendar group key ${key.field} needs ISO dates or timestamps with a timezone.`);
1098
+ const time = metricDateTime(input, key.timeZone);
1099
+ if (time < start || time >= end) {
1100
+ start = metricDateFloor(time, key.interval, key.timeZone);
1101
+ end = metricDateNext(start, key.interval, key.timeZone);
1102
+ iso = new Date(start).toISOString();
1103
+ }
1104
+ return iso;
1105
+ };
1106
+ }
1107
+ function datetimeValue(value) {
1108
+ const time = value instanceof Date ? value.getTime() : typeof value === "number" ? value : typeof value === "string" && value !== "" ? Date.parse(value) : NaN;
1109
+ return Number.isFinite(time) ? new Date(time).toISOString() : null;
1110
+ }
1111
+ function keyClause(field, value) {
1112
+ if (!field || field.type === "textarray" || !isFilterable(field)) return null;
1113
+ if (value == null) return { field: field.name, op: "is_null", value: "" };
1114
+ if (field.type === "datetime") {
1115
+ const iso = datetimeValue(value);
1116
+ return iso === null ? null : { field: field.name, op: "=", value: iso };
1117
+ }
1118
+ if (typeof value === "number" ? !Number.isFinite(value) : typeof value !== "string" && typeof value !== "boolean")
1119
+ return null;
1120
+ return { field: field.name, op: "=", value: String(value) };
1121
+ }
1122
+ function calendarKeyClauses(field, key, value) {
1123
+ if (!field || field.type !== "datetime" || !isFilterable(field)) return null;
1124
+ if (value == null) return [{ field: field.name, op: "is_null", value: "" }];
1125
+ const iso = datetimeValue(value);
1126
+ if (iso === null) return null;
1127
+ const start = Date.parse(iso), end = metricDateNext(start, key.interval, key.timeZone);
1128
+ if (!Number.isFinite(end) || end <= start) return null;
1129
+ return [
1130
+ { field: field.name, op: ">=", value: iso },
1131
+ { field: field.name, op: "<", value: new Date(end).toISOString() }
1132
+ ];
1133
+ }
1134
+ function groupKeyTerms(keys, group, schema) {
1135
+ const terms = [];
1136
+ for (const key of keys) {
1137
+ const name = groupKeyField(key);
1138
+ const field = resolveField(schema, name);
1139
+ if (isCalendarGroupKey(key)) {
1140
+ const range = calendarKeyClauses(field, key, group[name]);
1141
+ if (!range) return null;
1142
+ terms.push(...range);
1143
+ continue;
1144
+ }
1145
+ const term = keyClause(field, group[name]);
1146
+ if (!term) return null;
1147
+ terms.push(term);
1148
+ }
1149
+ return terms;
1150
+ }
1151
+
1152
+ // src/query.ts
1153
+ function isWhereConjunction(branch) {
1154
+ return branch != null && Array.isArray(branch.all);
1155
+ }
1156
+ function isOrGroup(term) {
1157
+ const any = term.any;
1158
+ return Array.isArray(any) && !any.some((member) => isWhereConjunction(member));
1159
+ }
1160
+ function isOrOfAndsGroup(term) {
1161
+ const any = term.any;
1162
+ return Array.isArray(any) && any.some(isWhereConjunction);
1163
+ }
1164
+ function isWhereGroup(term) {
1165
+ return Array.isArray(term.any);
1166
+ }
1167
+ function predicatesOf(term) {
1168
+ if (!isWhereGroup(term)) return [term];
1169
+ return term.any.flatMap((member) => isWhereConjunction(member) ? member.all : [member]);
1170
+ }
1171
+ function branchesOf(term) {
1172
+ if (!isWhereGroup(term)) return [[term]];
1173
+ return term.any.map((member) => isWhereConjunction(member) ? member.all : [member]);
1174
+ }
1175
+ var ITEM_DETAILS_COLUMN = "__qt_item_details__";
1176
+ var MAX_HOST_QUERY_PARAMS = 64;
1177
+ var MAX_HOST_QUERY_VALUES = 64;
1178
+ var MAX_HOST_QUERY_VALUE_LENGTH = 4e3;
1179
+ var MAX_HOST_QUERY_STATE_BYTES = 32 * 1024;
1180
+ var HOST_QUERY_PARAM_NAME = /^[A-Za-z_][A-Za-z0-9_.-]{0,127}$/;
1181
+ function parseHostQueryState(input) {
1182
+ const fail = (why) => {
1183
+ throw Error(`Invalid host query state: ${why}`);
1184
+ };
1185
+ if (!isRecord(input)) return fail("expected an object.");
1186
+ if (Object.keys(input).some((key) => key !== "version" && key !== "params")) fail("unexpected properties.");
1187
+ const version = input.version;
1188
+ if (typeof version !== "number" || !Number.isSafeInteger(version) || version < 1) return fail("unsupported version.");
1189
+ if (!isRecord(input.params)) return fail("params must be an object.");
1190
+ const names = Object.keys(input.params).sort();
1191
+ if (names.length > MAX_HOST_QUERY_PARAMS) fail("too many parameters.");
1192
+ const params = {};
1193
+ for (const name of names) {
1194
+ const values = input.params[name];
1195
+ if (!HOST_QUERY_PARAM_NAME.test(name)) fail("invalid parameter name.");
1196
+ if (!Array.isArray(values) || values.length > MAX_HOST_QUERY_VALUES || values.some((value) => typeof value !== "string" || value.length > MAX_HOST_QUERY_VALUE_LENGTH))
1197
+ fail(`parameter ${name} must be a list of strings.`);
1198
+ params[name] = [...values];
1199
+ }
1200
+ const state = { version, params };
1201
+ if (new TextEncoder().encode(JSON.stringify(state)).length > MAX_HOST_QUERY_STATE_BYTES) fail("payload too large.");
1202
+ return state;
1203
+ }
1204
+ var EMPTY_QUERY = {
1205
+ select: [],
1206
+ where: [],
1207
+ orderBy: [],
1208
+ limit: 100,
1209
+ offset: 0
1210
+ };
1211
+ var MAX_QUERY_LIMIT = Number.MAX_SAFE_INTEGER;
1212
+ var MAX_QUERY_OFFSET = 1e6;
1213
+ var MAX_SELECT_COLUMNS = 200;
1214
+ var MAX_WHERE_CLAUSES = 100;
1215
+ var MAX_ORDER_BY_TERMS = 20;
1216
+ var MAX_AGGREGATIONS = 20;
1217
+ var MAX_GROUP_BY_FIELDS = 20;
1218
+ var MAX_QUERY_TOKEN_LENGTH = 2 * 1024 * 1024;
1219
+ var MAX_FIELD_NAME_LENGTH = 256;
1220
+ var MAX_FILTER_VALUE_LENGTH = 1e4;
1221
+ var MAX_LABEL_LENGTH = 1e3;
1222
+ var MIN_COLUMN_WIDTH = 24;
1223
+ var MAX_COLUMN_WIDTH = 2e3;
1224
+ var FILTER_OPS = /* @__PURE__ */ new Set([
1225
+ "=",
1226
+ "!=",
1227
+ ">",
1228
+ ">=",
1229
+ "<",
1230
+ "<=",
1231
+ "contains",
1232
+ "starts_with",
1233
+ "ends_with",
1234
+ "matches_regex",
1235
+ "not_matches_regex",
1236
+ "length_gt",
1237
+ "length_lt",
1238
+ "length_eq",
1239
+ "includes",
1240
+ "is_null",
1241
+ "is_not_null"
1242
+ ]);
1243
+ var AGG_OPS = /* @__PURE__ */ new Set(["count", "count_distinct", "sum", "avg", "min", "max"]);
1244
+ function isRecord(value) {
1245
+ return value != null && typeof value === "object" && !Array.isArray(value);
1246
+ }
1247
+ function boundedString(value, maxLength) {
1248
+ return typeof value === "string" && value.length > 0 && value.length <= maxLength ? value : null;
1249
+ }
1250
+ function boundedInteger(value, fallback, min, max) {
1251
+ if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
1252
+ return Math.max(min, Math.min(max, Math.round(value)));
1253
+ }
1254
+ function normalizeWhereClause(item) {
1255
+ if (!isRecord(item)) return null;
1256
+ const field = boundedString(item.field, MAX_FIELD_NAME_LENGTH);
1257
+ const value = typeof item.value === "string" && item.value.length <= MAX_FILTER_VALUE_LENGTH ? item.value : null;
1258
+ if (!field || typeof item.op !== "string" || !FILTER_OPS.has(item.op) || value == null) {
1259
+ return null;
1260
+ }
1261
+ const clause = { field, op: item.op, value };
1262
+ if (item.negated === true) clause.negated = true;
1263
+ return clause;
1264
+ }
1265
+ function cloneWhereTerm(term) {
1266
+ const copy = isWhereGroup(term) ? { ...term, any: term.any.map((c) => isWhereConjunction(c) ? { all: c.all.map((m) => ({ ...m })) } : { ...c }) } : { ...term };
1267
+ if (term.setFilter) copy.setFilter = { ...term.setFilter, values: [...term.setFilter.values] };
1268
+ return copy;
1269
+ }
1270
+ function normalizeSetFilter(item) {
1271
+ if (!isRecord(item)) return void 0;
1272
+ const id = boundedString(item.id, MAX_FIELD_NAME_LENGTH);
1273
+ if (!id || typeof item.mode !== "string" || !["any", "all", "none", "empty"].includes(item.mode) || !Array.isArray(item.values)) return void 0;
1274
+ if (item.values.length > MAX_WHERE_CLAUSES || !item.values.every((value) => typeof value === "string" && value.length <= MAX_FILTER_VALUE_LENGTH)) return void 0;
1275
+ return { id, mode: item.mode, values: [...item.values] };
1276
+ }
1277
+ function suppliedPredicateCount(term) {
1278
+ if (!isRecord(term) || !Array.isArray(term.any)) return 1;
1279
+ let count = 0;
1280
+ for (const member of term.any)
1281
+ count += isRecord(member) && Array.isArray(member.all) ? Math.max(1, member.all.length) : 1;
1282
+ return count;
1283
+ }
1284
+ function normalizeWhereTerms(input) {
1285
+ const where = [];
1286
+ let literalBudget = MAX_WHERE_CLAUSES;
1287
+ for (const item of input) {
1288
+ if (literalBudget <= 0) break;
1289
+ const start = where.length;
1290
+ if (isRecord(item) && Array.isArray(item.any)) {
1291
+ const members = [];
1292
+ for (const inner of item.any) {
1293
+ if (literalBudget <= 0) break;
1294
+ if (isRecord(inner) && Array.isArray(inner.all)) {
1295
+ const all = [];
1296
+ for (const literal of inner.all) {
1297
+ if (literalBudget <= 0) break;
1298
+ const clause2 = normalizeWhereClause(literal);
1299
+ if (clause2) {
1300
+ all.push(clause2);
1301
+ literalBudget--;
1302
+ }
1303
+ }
1304
+ if (all.length === 1) members.push(all[0]);
1305
+ else if (all.length > 1) members.push({ all });
1306
+ continue;
1307
+ }
1308
+ const clause = normalizeWhereClause(inner);
1309
+ if (clause) {
1310
+ members.push(clause);
1311
+ literalBudget--;
1312
+ }
1313
+ }
1314
+ if (members.length === 1) {
1315
+ const only = members[0];
1316
+ if (isWhereConjunction(only)) where.push(...only.all);
1317
+ else where.push(only);
1318
+ } else if (members.length > 1) where.push({ any: members });
1319
+ } else {
1320
+ const clause = normalizeWhereClause(item);
1321
+ if (clause) {
1322
+ where.push(clause);
1323
+ literalBudget--;
1324
+ }
1325
+ }
1326
+ if (where.length > start && isRecord(item)) {
1327
+ const metadata = normalizeSetFilter(item.setFilter);
1328
+ if (metadata) where[start].setFilter = metadata;
1329
+ }
1330
+ }
1331
+ return where;
1332
+ }
1333
+ function normalizeMetricWhere(value) {
1334
+ if (value === void 0) return { invalid: false };
1335
+ if (!Array.isArray(value)) return { invalid: true };
1336
+ const where = normalizeWhereTerms(value);
1337
+ const supplied = value.reduce((count, term) => count + suppliedPredicateCount(term), 0);
1338
+ const kept = where.reduce((count, term) => count + predicatesOf(term).length, 0);
1339
+ return { ...where.length ? { where } : {}, invalid: supplied !== kept };
1340
+ }
1341
+ function normalizeQueryState(input, fallback = EMPTY_QUERY) {
1342
+ return normalizeQuery(input, fallback, true);
1343
+ }
1344
+ function normalizeQuery(input, fallback, links) {
1345
+ const raw = isRecord(input) ? input : {};
1346
+ const select = [];
1347
+ if (Array.isArray(raw.select)) {
1348
+ for (const item of raw.select.slice(0, MAX_SELECT_COLUMNS)) {
1349
+ if (!isRecord(item)) continue;
1350
+ const field = boundedString(item.field, MAX_FIELD_NAME_LENGTH);
1351
+ if (!field) continue;
769
1352
  const column = { field };
770
1353
  if (typeof item.width === "number" && Number.isFinite(item.width)) {
771
1354
  column.width = boundedInteger(item.width, MIN_COLUMN_WIDTH, MIN_COLUMN_WIDTH, MAX_COLUMN_WIDTH);
@@ -808,6 +1391,13 @@ function normalizeQuery(input, fallback, links) {
808
1391
  offset: boundedInteger(raw.offset, fallback.offset, 0, MAX_QUERY_OFFSET)
809
1392
  };
810
1393
  if (raw.fitToScreen === true) out.fitToScreen = true;
1394
+ if (links && raw.hostState !== void 0) {
1395
+ try {
1396
+ out.hostState = parseHostQueryState(raw.hostState);
1397
+ } catch {
1398
+ out.hostState = { version: 0, params: {} };
1399
+ }
1400
+ }
811
1401
  if (raw.grouping != null) out.grouping = normalizeGroupingState(raw.grouping, links);
812
1402
  if (Array.isArray(raw.aggregations)) {
813
1403
  const aggregations = [];
@@ -865,6 +1455,7 @@ function normalizeQuery(input, fallback, links) {
865
1455
  }
866
1456
  if (Object.keys(views).length) details.views = views;
867
1457
  }
1458
+ if (raw.itemDetails.jsonInspections !== void 0) details.jsonInspections = parseJsonInspections(raw.itemDetails.jsonInspections);
868
1459
  const active = raw.itemDetails.active;
869
1460
  if (isRecord(active) && (active.mode === "modal" || active.mode === "expand") && modes.includes(active.mode) && (typeof active.id === "string" && active.id.length > 0 && active.id.length <= 1e3 || typeof active.id === "number" && Number.isFinite(active.id))) {
870
1461
  details.active = { id: active.id, mode: active.mode };
@@ -879,8 +1470,20 @@ function normalizeGroupingState(value, links) {
879
1470
  const fail = (message) => {
880
1471
  diagnostics.push(`Invalid grouping: ${message}`);
881
1472
  };
882
- const keys = Array.isArray(raw.keys) ? raw.keys.slice(0, MAX_GROUP_BY_FIELDS).filter((key) => !!boundedString(key, MAX_FIELD_NAME_LENGTH)) : [];
883
- if (!Array.isArray(raw.keys) || !keys.length || keys.length !== raw.keys.length || new Set(keys).size !== keys.length)
1473
+ const keys = [];
1474
+ for (const [index, key] of (Array.isArray(raw.keys) ? raw.keys.slice(0, MAX_GROUP_BY_FIELDS) : []).entries()) {
1475
+ if (boundedString(key, MAX_FIELD_NAME_LENGTH)) {
1476
+ keys.push(key);
1477
+ continue;
1478
+ }
1479
+ if (!isRecord(key)) continue;
1480
+ const field = boundedString(key.field, MAX_FIELD_NAME_LENGTH), zone = key.timeZone;
1481
+ if (field && GROUP_KEY_INTERVALS.includes(key.interval) && (zone === void 0 || typeof zone === "string" && zone.length > 0 && zone.length <= 80 && !/^[+-]/.test(zone)))
1482
+ keys.push({ field, interval: key.interval, ...zone !== void 0 ? { timeZone: zone } : {} });
1483
+ else fail(`key ${index + 1} must be a field name or a calendar key {field, interval, timeZone} with interval hour, day, week, month or year; hour-of-day and day-of-week are metric intervals.`);
1484
+ }
1485
+ const keyNames = groupKeyFields(keys);
1486
+ if (!Array.isArray(raw.keys) || !keys.length || keys.length !== raw.keys.length || new Set(keyNames).size !== keys.length)
884
1487
  fail(`keys must list 1\u2013${MAX_GROUP_BY_FIELDS} distinct field names.`);
885
1488
  const summaries = [];
886
1489
  if (!Array.isArray(raw.summaries) || raw.summaries.length > 64) fail("summaries must be a list of at most 64 columns.");
@@ -897,7 +1500,7 @@ function normalizeGroupingState(value, links) {
897
1500
  }
898
1501
  const column = { id: item.id, op: item.op };
899
1502
  if (summaryIds.has(column.id)) fail(`${name} uses the same id as another summary column.`);
900
- else if (keys.includes(column.id)) fail(`${name} uses the same id as group key "${column.id}"; summary ids must differ from source field names.`);
1503
+ else if (keyNames.includes(column.id)) fail(`${name} uses the same id as group key "${column.id}"; summary ids must differ from source field names.`);
901
1504
  summaryIds.add(column.id);
902
1505
  if (item.op === "link") {
903
1506
  const link = normalizeLinkSpec(item.link);
@@ -946,13 +1549,13 @@ function normalizeGroupingState(value, links) {
946
1549
  }
947
1550
  if (!isRecord(raw.source)) fail("source must be the record query that is grouped.");
948
1551
  else if (raw.source.grouping != null) fail("only one grouping stage is supported; the source query cannot itself be grouped.");
949
- const { grouping: _nested, ...source } = isRecord(raw.source) ? raw.source : {};
1552
+ const { grouping: _nested, hostState: _host, ...source } = isRecord(raw.source) ? raw.source : {};
950
1553
  if (Array.isArray(raw.diagnostics))
951
1554
  for (const message of raw.diagnostics.slice(0, 20))
952
1555
  diagnostics.push(typeof message === "string" && message ? message.slice(0, MAX_LABEL_LENGTH) : "Invalid grouping definition.");
953
1556
  const normalizedSource = normalizeQueryState(source);
954
1557
  if (Array.isArray(source.where)) {
955
- const supplied = source.where.reduce((count, term) => count + (isRecord(term) && Array.isArray(term.any) ? term.any.length : 1), 0);
1558
+ const supplied = source.where.reduce((count, term) => count + suppliedPredicateCount(term), 0);
956
1559
  if (supplied !== normalizedSource.where.reduce((count, term) => count + predicatesOf(term).length, 0)) fail("source filters contain malformed predicates or exceed the predicate budget.");
957
1560
  }
958
1561
  return { keys, summaries, representative, source: normalizedSource, ...diagnostics.length ? { diagnostics: [...new Set(diagnostics)] } : {} };
@@ -990,179 +1593,49 @@ function normalizeLinkSpec(value) {
990
1593
  if (!isRecord(to.params)) return void 0;
991
1594
  const entries = Object.entries(to.params);
992
1595
  if (entries.length > MAX_LINK_PARAMS || entries.some(([key, item]) => !boundedString(key, MAX_FIELD_NAME_LENGTH) || typeof item !== "string" || item.length > MAX_FILTER_VALUE_LENGTH))
993
- return void 0;
994
- destination.params = Object.fromEntries(entries);
995
- }
996
- return { label, to: destination };
997
- }
998
- function keptEveryTerm(supplied, kept) {
999
- if (!Array.isArray(supplied)) return false;
1000
- const count = supplied.reduce((total, term) => total + (isRecord(term) && Array.isArray(term.any) ? Math.max(1, term.any.length) : 1), 0);
1001
- return count === kept.reduce((total, term) => total + predicatesOf(term).length, 0);
1002
- }
1003
- function normalizeLinkPatch(value) {
1004
- if (!isRecord(value) || !boundedString(value.id, MAX_FIELD_NAME_LENGTH) || !Array.isArray(value.owns) || !isRecord(value.query)) return void 0;
1005
- if (value.owns.length > QUERY_FACETS.size || value.owns.some((facet) => typeof facet !== "string" || !QUERY_FACETS.has(facet))) return void 0;
1006
- const raw = value.query;
1007
- const normalized = normalizeQuery(raw, EMPTY_QUERY, false);
1008
- if (normalized.grouping?.diagnostics) return void 0;
1009
- if (raw.where !== void 0 && !keptEveryTerm(raw.where, normalized.where)) return void 0;
1010
- if (raw.aggregations !== void 0 && (!Array.isArray(raw.aggregations) || raw.aggregations.length !== (normalized.aggregations?.length ?? 0) || normalized.aggregations?.some((clause) => clause.diagnostics?.length))) return void 0;
1011
- const query = {};
1012
- for (const key of PATCH_QUERY_KEYS) {
1013
- if (raw[key] !== void 0 && normalized[key] !== void 0) query[key] = normalized[key];
1014
- }
1015
- return { id: value.id, owns: [...new Set(value.owns)], query };
1016
- }
1017
- function queriesEqual(a, b) {
1018
- if (a === b) return true;
1019
- if (a.limit !== b.limit || a.offset !== b.offset) return false;
1020
- if (a.fitToScreen === true !== (b.fitToScreen === true)) return false;
1021
- if (!sameArray(a.where, b.where)) return false;
1022
- if (!sameArray(a.orderBy, b.orderBy)) return false;
1023
- if (!sameArray(a.select, b.select)) return false;
1024
- if (!sameArray(a.aggregations ?? [], b.aggregations ?? [])) return false;
1025
- if (JSON.stringify(a.itemDetails) !== JSON.stringify(b.itemDetails)) return false;
1026
- if (JSON.stringify(a.grouping) !== JSON.stringify(b.grouping)) return false;
1027
- return true;
1028
- }
1029
- function sameArray(a, b) {
1030
- if (a.length !== b.length) return false;
1031
- for (let i = 0; i < a.length; i++) {
1032
- if (JSON.stringify(a[i]) !== JSON.stringify(b[i])) return false;
1033
- }
1034
- return true;
1035
- }
1036
-
1037
- // src/relativeTime.ts
1038
- var UNIT_MS = { ms: 1, s: 1e3, m: 6e4, h: 36e5, d: 864e5, w: 6048e5 };
1039
- var MAX_RELATIVE_TIME_MS = 864e13;
1040
- function parseRelativeDuration(value) {
1041
- const valid = /^[+-](?:[0-9]+(?:ms|s|m|h|d|w))+$/.exec(value);
1042
- if (!valid || valid[0] !== value) return null;
1043
- const body = value.slice(1);
1044
- const components = /([0-9]+)(ms|s|m|h|d|w)/g;
1045
- let total = 0;
1046
- for (const match of body.matchAll(components)) {
1047
- const amount = Number(match[1]);
1048
- const unit = UNIT_MS[match[2]];
1049
- if (!Number.isSafeInteger(amount) || amount > Math.floor((MAX_RELATIVE_TIME_MS - total) / unit)) return null;
1050
- total += amount * unit;
1051
- }
1052
- return value[0] === "-" && total !== 0 ? -total : total;
1053
- }
1054
- function evaluationTime(options2) {
1055
- const now = options2.now ?? Date.now();
1056
- if (!Number.isSafeInteger(now) || Math.abs(now) > MAX_RELATIVE_TIME_MS) throw new Error("Invalid query evaluation time");
1057
- return now;
1058
- }
1059
- function relativeTimestamp(now, offsetMs) {
1060
- const result = now + offsetMs;
1061
- if (!Number.isSafeInteger(result) || Math.abs(result) > MAX_RELATIVE_TIME_MS) throw new Error("Relative time exceeds datetime limits");
1062
- return result;
1063
- }
1064
-
1065
- // src/ops.ts
1066
- var NULLITY = ["is_null", "is_not_null"];
1067
- var COMPLEMENT_OP = {
1068
- "=": "!=",
1069
- "!=": "=",
1070
- ">": "<=",
1071
- "<=": ">",
1072
- ">=": "<",
1073
- "<": ">=",
1074
- is_null: "is_not_null",
1075
- is_not_null: "is_null",
1076
- matches_regex: "not_matches_regex",
1077
- not_matches_regex: "matches_regex"
1078
- };
1079
- function negateClause(clause, allowedOps) {
1080
- if (clause.value === "0" && !clause.negated) {
1081
- const zeroComplement = clause.op === "length_gt" ? "length_eq" : clause.op === "length_eq" ? "length_gt" : null;
1082
- if (zeroComplement && (!allowedOps || allowedOps.includes(zeroComplement))) {
1083
- return { field: clause.field, op: zeroComplement, value: "0" };
1084
- }
1085
- }
1086
- const complement = COMPLEMENT_OP[clause.op];
1087
- if (complement && (!allowedOps || allowedOps.includes(complement))) {
1088
- const next2 = { field: clause.field, op: complement, value: clause.value };
1089
- return next2;
1090
- }
1091
- const next = { field: clause.field, op: clause.op, value: clause.value };
1092
- if (!clause.negated) next.negated = true;
1093
- return next;
1094
- }
1095
- function isNegativePredicate(clause) {
1096
- return Boolean(clause.negated) || clause.op === "!=" || clause.op === "not_matches_regex" || clause.op === "is_not_null";
1097
- }
1098
- var POSITIVE_OP_ORDER = ["=", ">", ">=", "contains", "starts_with", "ends_with", "length_gt", "length_lt", "length_eq", "matches_regex", "includes"];
1099
- function opPairsForField(field) {
1100
- const ops = opsForField(field);
1101
- const allow = new Set(ops);
1102
- const pairs = [];
1103
- for (const op of POSITIVE_OP_ORDER) {
1104
- if (!allow.has(op)) continue;
1105
- const neg = negateClause({ field: "", op, value: "" }, ops);
1106
- pairs.push({ keep: { op, negated: false }, exclude: { op: neg.op, negated: Boolean(neg.negated) } });
1107
- }
1108
- if (allow.has("is_null") || allow.has("is_not_null")) {
1109
- pairs.push({ keep: { op: "is_null", negated: false }, exclude: { op: "is_not_null", negated: false } });
1110
- }
1111
- return pairs;
1112
- }
1113
- var OPS_BY_TYPE = {
1114
- text: [
1115
- "=",
1116
- "!=",
1117
- "contains",
1118
- "starts_with",
1119
- "ends_with",
1120
- "matches_regex",
1121
- "not_matches_regex",
1122
- "length_gt",
1123
- "length_lt",
1124
- "length_eq",
1125
- ...NULLITY
1126
- ],
1127
- enum: ["=", "!=", ...NULLITY],
1128
- number: ["=", "!=", ">", ">=", "<", "<=", ...NULLITY],
1129
- datetime: ["=", "!=", ">", ">=", "<", "<=", ...NULLITY],
1130
- bool: ["=", "!=", ...NULLITY],
1131
- textarray: ["includes", ...NULLITY]
1132
- };
1133
- var NULLARY_OPS = new Set(NULLITY);
1134
- function opsForField(field) {
1135
- return field.filter?.ops ?? OPS_BY_TYPE[field.type];
1596
+ return void 0;
1597
+ destination.params = Object.fromEntries(entries);
1598
+ }
1599
+ return { label, to: destination };
1136
1600
  }
1137
- function opAllowedForType(type, op) {
1138
- return OPS_BY_TYPE[type].includes(op);
1601
+ function keptEveryTerm(supplied, kept) {
1602
+ if (!Array.isArray(supplied)) return false;
1603
+ const count = supplied.reduce((total, term) => total + Math.max(1, suppliedPredicateCount(term)), 0);
1604
+ return count === kept.reduce((total, term) => total + predicatesOf(term).length, 0);
1139
1605
  }
1140
- function coerceValue(type, raw, now) {
1141
- switch (type) {
1142
- case "number": {
1143
- const n = Number(raw);
1144
- if (raw.trim() === "" || Number.isNaN(n)) throw new Error(`not a number: ${JSON.stringify(raw)}`);
1145
- return n;
1146
- }
1147
- case "bool": {
1148
- const v = raw.toLowerCase();
1149
- if (v === "true" || v === "1" || v === "t") return true;
1150
- if (v === "false" || v === "0" || v === "f") return false;
1151
- throw new Error(`not a bool: ${JSON.stringify(raw)}`);
1152
- }
1153
- case "datetime": {
1154
- if (raw.startsWith("+") || raw.startsWith("-")) {
1155
- const offset = parseRelativeDuration(raw);
1156
- if (offset === null) throw new Error(`not a signed duration: ${JSON.stringify(raw)}`);
1157
- return relativeTimestamp(now ?? Date.now(), offset);
1158
- }
1159
- const ms = Date.parse(raw);
1160
- if (Number.isNaN(ms)) throw new Error(`not a datetime: ${JSON.stringify(raw)}`);
1161
- return ms;
1162
- }
1163
- default:
1164
- return raw;
1606
+ function normalizeLinkPatch(value) {
1607
+ if (!isRecord(value) || !boundedString(value.id, MAX_FIELD_NAME_LENGTH) || !Array.isArray(value.owns) || !isRecord(value.query)) return void 0;
1608
+ if (value.owns.length > QUERY_FACETS.size || value.owns.some((facet) => typeof facet !== "string" || !QUERY_FACETS.has(facet))) return void 0;
1609
+ const raw = value.query;
1610
+ const normalized = normalizeQuery(raw, EMPTY_QUERY, false);
1611
+ if (normalized.grouping?.diagnostics) return void 0;
1612
+ if (raw.where !== void 0 && !keptEveryTerm(raw.where, normalized.where)) return void 0;
1613
+ if (raw.aggregations !== void 0 && (!Array.isArray(raw.aggregations) || raw.aggregations.length !== (normalized.aggregations?.length ?? 0) || normalized.aggregations?.some((clause) => clause.diagnostics?.length))) return void 0;
1614
+ const query = {};
1615
+ for (const key of PATCH_QUERY_KEYS) {
1616
+ if (raw[key] !== void 0 && normalized[key] !== void 0) query[key] = normalized[key];
1617
+ }
1618
+ return { id: value.id, owns: [...new Set(value.owns)], query };
1619
+ }
1620
+ function queriesEqual(a, b) {
1621
+ if (a === b) return true;
1622
+ if (a.limit !== b.limit || a.offset !== b.offset) return false;
1623
+ if (a.fitToScreen === true !== (b.fitToScreen === true)) return false;
1624
+ if (!sameArray(a.where, b.where)) return false;
1625
+ if (!sameArray(a.orderBy, b.orderBy)) return false;
1626
+ if (!sameArray(a.select, b.select)) return false;
1627
+ if (!sameArray(a.aggregations ?? [], b.aggregations ?? [])) return false;
1628
+ if (JSON.stringify(a.itemDetails) !== JSON.stringify(b.itemDetails)) return false;
1629
+ if (JSON.stringify(a.grouping) !== JSON.stringify(b.grouping)) return false;
1630
+ if (JSON.stringify(a.hostState) !== JSON.stringify(b.hostState)) return false;
1631
+ return true;
1632
+ }
1633
+ function sameArray(a, b) {
1634
+ if (a.length !== b.length) return false;
1635
+ for (let i = 0; i < a.length; i++) {
1636
+ if (JSON.stringify(a[i]) !== JSON.stringify(b[i])) return false;
1165
1637
  }
1638
+ return true;
1166
1639
  }
1167
1640
 
1168
1641
  // src/agg.ts
@@ -1231,7 +1704,8 @@ function createSetFilter(field, mode, values, id, target = "array") {
1231
1704
  }
1232
1705
  function stripSetFilter(term) {
1233
1706
  const strip = ({ setFilter: _metadata, ...clause }) => clause;
1234
- return isOrGroup(term) ? { any: term.any.map(strip) } : strip(term);
1707
+ if (!isWhereGroup(term)) return strip(term);
1708
+ return { any: term.any.map((member) => isWhereConjunction(member) ? { all: member.all.map(strip) } : strip(member)) };
1235
1709
  }
1236
1710
  function readSetFilter(where, index) {
1237
1711
  const term = where[index];
@@ -1296,6 +1770,7 @@ function compactQuery(q) {
1296
1770
  if (q.itemDetails) c.d = q.itemDetails;
1297
1771
  if (q.fitToScreen) c.fs = true;
1298
1772
  if (q.grouping) c.b = q.grouping;
1773
+ if (q.hostState) c.h = q.hostState;
1299
1774
  return c;
1300
1775
  }
1301
1776
  var MAX_QUERY_STATE_BYTES = 4 * 1024 * 1024;
@@ -1422,7 +1897,7 @@ function decodeToken(token, strict) {
1422
1897
  const c = versioned ? unpackQuery(raw) : raw;
1423
1898
  if (strict && c.w) {
1424
1899
  if (c.w.some((term) => !term || typeof term !== "object")) throw new QueryDecodeError("invalid", "Invalid query predicates.");
1425
- const count = c.w.reduce((n, term) => n + predicatesOf(term).length, 0);
1900
+ const count = c.w.reduce((n, term) => n + suppliedPredicateCount(term), 0);
1426
1901
  const normalized = normalizeQueryState({ where: c.w });
1427
1902
  const normalizedCount = normalized.where.reduce((n, term) => n + predicatesOf(term).length, 0);
1428
1903
  if (count > 100 || count !== normalizedCount || c.w.some((term) => predicatesOf(term).some((clause) => !clause || typeof clause !== "object" || clause.negated !== void 0 && typeof clause.negated !== "boolean")))
@@ -1437,8 +1912,16 @@ function decodeToken(token, strict) {
1437
1912
  aggregations: c.g,
1438
1913
  itemDetails: c.d,
1439
1914
  fitToScreen: c.fs,
1440
- grouping: c.b
1915
+ grouping: c.b,
1916
+ hostState: c.h
1441
1917
  });
1918
+ if (strict && c.h !== void 0) {
1919
+ try {
1920
+ parseHostQueryState(c.h);
1921
+ } catch {
1922
+ throw new QueryDecodeError("invalid", "Invalid host state in query link.");
1923
+ }
1924
+ }
1442
1925
  if (strict && c.g && out.aggregations?.length !== c.g.length) throw new QueryDecodeError("invalid", "Invalid query metrics.");
1443
1926
  return out;
1444
1927
  } catch (error) {
@@ -1505,7 +1988,7 @@ function termIsPushdown(term, byName, resolveField2) {
1505
1988
  }
1506
1989
  function toAggregationQuery(q, schema) {
1507
1990
  if (q.grouping) throw new Error("Metrics over grouped rows require grouped-query execution.");
1508
- const suppliedPredicateCount = q.where.reduce((n, t) => n + predicatesOf(t).length, 0);
1991
+ const suppliedCount = q.where.reduce((n, t) => n + predicatesOf(t).length, 0);
1509
1992
  q = normalizeQueryState(q);
1510
1993
  if (q.aggregations?.some((a) => a.dateGrouping)) throw new Error("Date grouping currently requires local metric execution.");
1511
1994
  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 || a.where?.length || a.minCount !== void 0)) throw new Error("Metric definition requires the version 2 metric transport.");
@@ -1523,7 +2006,7 @@ function toAggregationQuery(q, schema) {
1523
2006
  );
1524
2007
  const diagnostics = [];
1525
2008
  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 });
1526
- 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 });
2009
+ if (where.length !== q.where.length || suppliedCount !== 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 });
1527
2010
  return { where, aggregations, ...diagnostics.length ? { diagnostics } : {} };
1528
2011
  }
1529
2012
 
@@ -1548,11 +2031,11 @@ function localQuerySnapshotCache(options2 = {}) {
1548
2031
  if (!Number.isInteger(maxEntries) || maxEntries < 1 || !Number.isFinite(maxBytes) || maxBytes < 1) throw Error("Invalid query link cache limits.");
1549
2032
  const storage = () => options2.storage ?? globalThis.localStorage;
1550
2033
  function read() {
1551
- const text = storage().getItem(storageKey);
1552
- if (!text) return [];
2034
+ const text2 = storage().getItem(storageKey);
2035
+ if (!text2) return [];
1553
2036
  let raw;
1554
2037
  try {
1555
- raw = JSON.parse(text);
2038
+ raw = JSON.parse(text2);
1556
2039
  } catch {
1557
2040
  return [];
1558
2041
  }
@@ -1570,12 +2053,12 @@ function localQuerySnapshotCache(options2 = {}) {
1570
2053
  const entry = entries.find((e) => e.key === key && e.snapshot.code === code);
1571
2054
  if (entry) {
1572
2055
  entry.durable = true;
1573
- let text = JSON.stringify(entries);
1574
- while (text.length * 2 > maxBytes && entries.length > 1) {
2056
+ let text2 = JSON.stringify(entries);
2057
+ while (text2.length * 2 > maxBytes && entries.length > 1) {
1575
2058
  entries.splice(entries[0] === entry ? 1 : 0, 1);
1576
- text = JSON.stringify(entries);
2059
+ text2 = JSON.stringify(entries);
1577
2060
  }
1578
- if (text.length * 2 <= maxBytes) storage().setItem(storageKey, text);
2061
+ if (text2.length * 2 <= maxBytes) storage().setItem(storageKey, text2);
1579
2062
  }
1580
2063
  },
1581
2064
  get(key, code) {
@@ -1589,26 +2072,26 @@ function localQuerySnapshotCache(options2 = {}) {
1589
2072
  const old = entries.find((e) => e.key === key && e.snapshot.code === snapshot.code);
1590
2073
  if (old) {
1591
2074
  if (old.snapshot.token !== snapshot.token) throw Error("A query link code cannot be overwritten.");
1592
- const text2 = JSON.stringify(entries);
1593
- if (storage().getItem(storageKey) !== text2) storage().setItem(storageKey, text2);
2075
+ const text3 = JSON.stringify(entries);
2076
+ if (storage().getItem(storageKey) !== text3) storage().setItem(storageKey, text3);
1594
2077
  return;
1595
2078
  }
1596
2079
  entries.push({ key, snapshot: validateQuerySnapshot(snapshot) });
1597
2080
  while (entries.length > maxEntries) entries.shift();
1598
- let text = JSON.stringify(entries);
1599
- while (text.length * 2 > maxBytes && entries.length > 1) {
2081
+ let text2 = JSON.stringify(entries);
2082
+ while (text2.length * 2 > maxBytes && entries.length > 1) {
1600
2083
  entries.shift();
1601
- text = JSON.stringify(entries);
2084
+ text2 = JSON.stringify(entries);
1602
2085
  }
1603
- if (text.length * 2 > maxBytes) throw Error("This query is too large for the local link cache.");
1604
- storage().setItem(storageKey, text);
2086
+ if (text2.length * 2 > maxBytes) throw Error("This query is too large for the local link cache.");
2087
+ storage().setItem(storageKey, text2);
1605
2088
  }
1606
2089
  };
1607
2090
  }
1608
2091
  function memoryQuerySnapshotCache() {
1609
- let text = null;
1610
- return localQuerySnapshotCache({ storage: { getItem: () => text, setItem: (_key, value) => {
1611
- text = value;
2092
+ let text2 = null;
2093
+ return localQuerySnapshotCache({ storage: { getItem: () => text2, setItem: (_key, value) => {
2094
+ text2 = value;
1612
2095
  } } });
1613
2096
  }
1614
2097
  var QuerySnapshotHttpError = class extends Error {
@@ -1647,10 +2130,10 @@ async function snapshotHttpError(response) {
1647
2130
  bytes.set(chunk, offset);
1648
2131
  offset += chunk.length;
1649
2132
  }
1650
- const text = new TextDecoder().decode(bytes);
1651
- let reason = text;
2133
+ const text2 = new TextDecoder().decode(bytes);
2134
+ let reason = text2;
1652
2135
  if (type === "application/json") {
1653
- const body = JSON.parse(text);
2136
+ const body = JSON.parse(text2);
1654
2137
  reason = body?.message ?? body?.error?.message ?? body?.error;
1655
2138
  const code2 = body?.code ?? body?.error?.code;
1656
2139
  if (typeof code2 === "string" && /^[A-Za-z0-9_.-]{1,64}$/.test(code2)) serverCode = code2;
@@ -2348,23 +2831,23 @@ function compileFormula(source, fields, resolve, extension) {
2348
2831
  let match;
2349
2832
  if (source[pos] === "[") {
2350
2833
  pos++;
2351
- let text = "";
2834
+ let text2 = "";
2352
2835
  let closed = false;
2353
2836
  while (pos < source.length) {
2354
2837
  if (source[pos] === "]") {
2355
2838
  if (source[pos + 1] === "]") {
2356
- text += "]";
2839
+ text2 += "]";
2357
2840
  pos += 2;
2358
2841
  } else {
2359
2842
  pos++;
2360
2843
  closed = true;
2361
2844
  break;
2362
2845
  }
2363
- } else text += source[pos++];
2846
+ } else text2 += source[pos++];
2364
2847
  }
2365
2848
  if (!closed)
2366
2849
  throw new FormulaError("Unclosed field reference.", from, pos);
2367
- tokens.push({ kind: "field", text, from, to: pos });
2850
+ tokens.push({ kind: "field", text: text2, from, to: pos });
2368
2851
  } else if (source[pos] === '"') {
2369
2852
  pos++;
2370
2853
  while (pos < source.length && source[pos] !== '"') {
@@ -2384,9 +2867,9 @@ function compileFormula(source, fields, resolve, extension) {
2384
2867
  }
2385
2868
  } else if (source[pos] === "'") {
2386
2869
  const end = source.indexOf("'", pos + 1);
2387
- const text = end < 0 ? "" : source.slice(pos + 1, end);
2870
+ const text2 = end < 0 ? "" : source.slice(pos + 1, end);
2388
2871
  throw new FormulaError(
2389
- end < 0 || text.length > 40 || /["\\\n]/.test(text) ? 'Text literals use double quotes ("text"), not single quotes.' : `Text literals use double quotes: write "${text}" instead of '${text}'.`,
2872
+ end < 0 || text2.length > 40 || /["\\\n]/.test(text2) ? 'Text literals use double quotes ("text"), not single quotes.' : `Text literals use double quotes: write "${text2}" instead of '${text2}'.`,
2390
2873
  from,
2391
2874
  end < 0 ? pos + 1 : end + 1
2392
2875
  );
@@ -3036,259 +3519,89 @@ function formulaRuntime(ast, inputs, extension) {
3036
3519
  if (![
3037
3520
  "year",
3038
3521
  "month",
3039
- "week",
3040
- "day",
3041
- "hour",
3042
- "minute",
3043
- "second"
3044
- ].includes(unit))
3045
- fail("Unsupported date unit.");
3046
- d.setUTCMilliseconds(0);
3047
- if (unit !== "second") d.setUTCSeconds(0);
3048
- if (!["second", "minute"].includes(unit)) d.setUTCMinutes(0);
3049
- if (["year", "month", "week", "day"].includes(unit)) d.setUTCHours(0);
3050
- if (unit === "week")
3051
- d.setUTCDate(d.getUTCDate() - (d.getUTCDay() + 6) % 7);
3052
- if (unit === "year" || unit === "month") d.setUTCDate(1);
3053
- if (unit === "year") d.setUTCMonth(0);
3054
- out = d.toISOString();
3055
- break;
3056
- }
3057
- case "DATE_ADD": {
3058
- const d = date(c), unit = str(a).toLowerCase(), amount = integer(b, -1e5, 1e5);
3059
- if (unit === "year" || unit === "month") {
3060
- const day = d.getUTCDate();
3061
- d.setUTCDate(1);
3062
- d.setUTCMonth(d.getUTCMonth() + amount * (unit === "year" ? 12 : 1));
3063
- const last = new Date(d.getTime());
3064
- last.setUTCMonth(last.getUTCMonth() + 1, 0);
3065
- d.setUTCDate(Math.min(day, last.getUTCDate()));
3066
- } else {
3067
- const units = {
3068
- week: 6048e5,
3069
- day: 864e5,
3070
- hour: 36e5,
3071
- minute: 6e4,
3072
- second: 1e3,
3073
- millisecond: 1
3074
- };
3075
- const ms = units[unit];
3076
- if (!ms) fail("Unsupported date unit.");
3077
- d.setTime(d.getTime() + amount * ms);
3078
- }
3079
- out = d.toISOString();
3080
- break;
3081
- }
3082
- case "DATE_DIFF": {
3083
- const units = {
3084
- week: 6048e5,
3085
- day: 864e5,
3086
- hour: 36e5,
3087
- minute: 6e4,
3088
- second: 1e3,
3089
- millisecond: 1
3090
- };
3091
- const ms = units[str(a).toLowerCase()];
3092
- if (!ms) fail("Unsupported elapsed date unit.");
3093
- out = Math.trunc((date(c).getTime() - date(b).getTime()) / ms);
3094
- break;
3095
- }
3096
- case "FORMAT_DATE": {
3097
- const d = date(a), pad = (n2) => String(n2).padStart(2, "0"), parts2 = {
3098
- YYYY: String(d.getUTCFullYear()).padStart(4, "0"),
3099
- MM: pad(d.getUTCMonth() + 1),
3100
- DD: pad(d.getUTCDate()),
3101
- HH: pad(d.getUTCHours()),
3102
- mm: pad(d.getUTCMinutes()),
3103
- ss: pad(d.getUTCSeconds())
3104
- };
3105
- out = str(b).replace(/YYYY|MM|DD|HH|mm|ss/g, (t) => parts2[t]);
3106
- break;
3107
- }
3108
- default:
3109
- return fail(`Unknown operation: ${name}`);
3110
- }
3111
- return bound(out);
3112
- }
3113
- try {
3114
- const value = run(ast);
3115
- return inspection ? { value, regex: inspection } : { value };
3116
- } catch (e) {
3117
- return {
3118
- value: null,
3119
- error: e instanceof Error ? e.message : "Formula evaluation failed."
3120
- };
3121
- }
3122
- }
3123
-
3124
- // src/metricDates.ts
3125
- import { Temporal } from "temporal-polyfill";
3126
- var METRIC_DATE_INTERVALS = [
3127
- "hour",
3128
- "day",
3129
- "week",
3130
- "month",
3131
- "year"
3132
- ];
3133
- function metricDateTime(value, timeZone = "UTC") {
3134
- if (typeof value !== "string" || !/^\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?(?:Z|[+-]\d{2}:\d{2}))?$/.test(
3135
- value
3136
- ))
3137
- throw Error(
3138
- "Date grouping requires ISO dates or timestamps with a timezone."
3139
- );
3140
- const time = Date.parse(value), month = Number(value.slice(5, 7)), day = Number(value.slice(8, 10));
3141
- const calendar = /* @__PURE__ */ new Date(0);
3142
- calendar.setUTCFullYear(Number(value.slice(0, 4)), month, 0);
3143
- if (!Number.isFinite(time) || month < 1 || month > 12 || day < 1 || day > calendar.getUTCDate())
3144
- throw Error("Invalid date grouping value.");
3145
- return value.length === 10 && timeZone !== "UTC" ? Temporal.PlainDate.from(value).toZonedDateTime(timeZone).epochMilliseconds : time;
3146
- }
3147
- function metricOffsetTransition(start, end, timeZone) {
3148
- if (end <= start) return void 0;
3149
- const offset = (time) => Temporal.Instant.fromEpochMilliseconds(time).toZonedDateTimeISO(timeZone).offsetNanoseconds;
3150
- const before = offset(start);
3151
- if (before === offset(end)) return void 0;
3152
- while (end - start > 1) {
3153
- const middle = Math.floor((start + end) / 2);
3154
- if (offset(middle) === before) start = middle;
3155
- else end = middle;
3156
- }
3157
- return end;
3158
- }
3159
- function metricDateFloor(time, interval, timeZone = "UTC") {
3160
- if (timeZone !== "UTC") {
3161
- const zoned = Temporal.Instant.fromEpochMilliseconds(time).toZonedDateTimeISO(timeZone);
3162
- if (interval === "hour") {
3163
- const floor = time - zoned.minute * 6e4 - zoned.second * 1e3 - zoned.millisecond;
3164
- return metricOffsetTransition(floor, time, timeZone) ?? floor;
3165
- }
3166
- let date2 = zoned.toPlainDate();
3167
- if (interval === "week") date2 = date2.subtract({ days: date2.dayOfWeek - 1 });
3168
- if (interval === "month" || interval === "year")
3169
- date2 = date2.with({ day: 1 });
3170
- if (interval === "year") date2 = date2.with({ month: 1 });
3171
- return date2.toZonedDateTime(timeZone).epochMilliseconds;
3172
- }
3173
- const date = new Date(time);
3174
- date.setUTCMinutes(0, 0, 0);
3175
- if (interval !== "hour") date.setUTCHours(0);
3176
- if (interval === "week")
3177
- date.setUTCDate(date.getUTCDate() - (date.getUTCDay() + 6) % 7);
3178
- if (interval === "month" || interval === "year") date.setUTCDate(1);
3179
- if (interval === "year") date.setUTCMonth(0);
3180
- return date.getTime();
3181
- }
3182
- function metricDateNext(time, interval, timeZone = "UTC") {
3183
- if (timeZone !== "UTC") {
3184
- const zoned = Temporal.Instant.fromEpochMilliseconds(time).toZonedDateTimeISO(timeZone);
3185
- if (interval === "hour") {
3186
- const target = zoned.toPlainDateTime().with({
3187
- minute: 0,
3188
- second: 0,
3189
- millisecond: 0,
3190
- microsecond: 0,
3191
- nanosecond: 0
3192
- }).add({ hours: 1 });
3193
- let next = target.toZonedDateTime(timeZone).epochMilliseconds;
3194
- if (next <= time)
3195
- next = target.toZonedDateTime(timeZone, {
3196
- disambiguation: "later"
3197
- }).epochMilliseconds;
3198
- return metricOffsetTransition(time, next, timeZone) ?? next;
3199
- }
3200
- let date2 = zoned.toPlainDate();
3201
- if (interval === "month") date2 = date2.with({ day: 1 }).add({ months: 1 });
3202
- else if (interval === "year")
3203
- date2 = date2.with({ month: 1, day: 1 }).add({ years: 1 });
3204
- else if (interval === "week")
3205
- date2 = date2.subtract({ days: date2.dayOfWeek - 1 }).add({ weeks: 1 });
3206
- else date2 = date2.add({ days: 1 });
3207
- return date2.toZonedDateTime(timeZone).epochMilliseconds;
3208
- }
3209
- const date = new Date(time);
3210
- if (interval === "hour") date.setUTCHours(date.getUTCHours() + 1);
3211
- else if (interval === "month") date.setUTCMonth(date.getUTCMonth() + 1);
3212
- else if (interval === "year") date.setUTCFullYear(date.getUTCFullYear() + 1);
3213
- else date.setUTCDate(date.getUTCDate() + (interval === "week" ? 7 : 1));
3214
- return date.getTime();
3215
- }
3216
- function metricCustomEndTime(value, timeZone = "UTC") {
3217
- const time = metricDateTime(value, timeZone);
3218
- return value.length === 10 ? metricDateNext(time, "day", timeZone) - 1 : time;
3219
- }
3220
- function validateDateGrouping(config) {
3221
- if (typeof config.field !== "string" || !config.field || config.field.length > 256 || !METRIC_DATE_INTERVALS.includes(config.interval) || typeof config.includeEmpty !== "boolean" || !["query", "observed", "custom"].includes(config.range) || config.partialIntervals !== void 0 && !["include", "exclude"].includes(config.partialIntervals))
3222
- throw Error("Invalid date grouping definition.");
3223
- if (config.timeZone !== void 0) {
3224
- if (typeof config.timeZone !== "string" || !config.timeZone || config.timeZone.length > 80 || /^[+-]/.test(config.timeZone))
3225
- throw Error(
3226
- "Use a valid IANA grouping timezone, such as America/Denver or UTC."
3227
- );
3228
- try {
3229
- new Intl.DateTimeFormat("en-US", { timeZone: config.timeZone });
3230
- } catch {
3231
- throw Error(
3232
- "Use a valid IANA grouping timezone, such as America/Denver or UTC."
3233
- );
3522
+ "week",
3523
+ "day",
3524
+ "hour",
3525
+ "minute",
3526
+ "second"
3527
+ ].includes(unit))
3528
+ fail("Unsupported date unit.");
3529
+ d.setUTCMilliseconds(0);
3530
+ if (unit !== "second") d.setUTCSeconds(0);
3531
+ if (!["second", "minute"].includes(unit)) d.setUTCMinutes(0);
3532
+ if (["year", "month", "week", "day"].includes(unit)) d.setUTCHours(0);
3533
+ if (unit === "week")
3534
+ d.setUTCDate(d.getUTCDate() - (d.getUTCDay() + 6) % 7);
3535
+ if (unit === "year" || unit === "month") d.setUTCDate(1);
3536
+ if (unit === "year") d.setUTCMonth(0);
3537
+ out = d.toISOString();
3538
+ break;
3539
+ }
3540
+ case "DATE_ADD": {
3541
+ const d = date(c), unit = str(a).toLowerCase(), amount = integer(b, -1e5, 1e5);
3542
+ if (unit === "year" || unit === "month") {
3543
+ const day = d.getUTCDate();
3544
+ d.setUTCDate(1);
3545
+ d.setUTCMonth(d.getUTCMonth() + amount * (unit === "year" ? 12 : 1));
3546
+ const last = new Date(d.getTime());
3547
+ last.setUTCMonth(last.getUTCMonth() + 1, 0);
3548
+ d.setUTCDate(Math.min(day, last.getUTCDate()));
3549
+ } else {
3550
+ const units = {
3551
+ week: 6048e5,
3552
+ day: 864e5,
3553
+ hour: 36e5,
3554
+ minute: 6e4,
3555
+ second: 1e3,
3556
+ millisecond: 1
3557
+ };
3558
+ const ms = units[unit];
3559
+ if (!ms) fail("Unsupported date unit.");
3560
+ d.setTime(d.getTime() + amount * ms);
3561
+ }
3562
+ out = d.toISOString();
3563
+ break;
3564
+ }
3565
+ case "DATE_DIFF": {
3566
+ const units = {
3567
+ week: 6048e5,
3568
+ day: 864e5,
3569
+ hour: 36e5,
3570
+ minute: 6e4,
3571
+ second: 1e3,
3572
+ millisecond: 1
3573
+ };
3574
+ const ms = units[str(a).toLowerCase()];
3575
+ if (!ms) fail("Unsupported elapsed date unit.");
3576
+ out = Math.trunc((date(c).getTime() - date(b).getTime()) / ms);
3577
+ break;
3578
+ }
3579
+ case "FORMAT_DATE": {
3580
+ const d = date(a), pad = (n2) => String(n2).padStart(2, "0"), parts2 = {
3581
+ YYYY: String(d.getUTCFullYear()).padStart(4, "0"),
3582
+ MM: pad(d.getUTCMonth() + 1),
3583
+ DD: pad(d.getUTCDate()),
3584
+ HH: pad(d.getUTCHours()),
3585
+ mm: pad(d.getUTCMinutes()),
3586
+ ss: pad(d.getUTCSeconds())
3587
+ };
3588
+ out = str(b).replace(/YYYY|MM|DD|HH|mm|ss/g, (t) => parts2[t]);
3589
+ break;
3590
+ }
3591
+ default:
3592
+ return fail(`Unknown operation: ${name}`);
3234
3593
  }
3594
+ return bound(out);
3235
3595
  }
3236
- if (config.includeEmpty && config.range === "custom") {
3237
- const start = metricDateTime(config.start ?? "", config.timeZone), end = metricCustomEndTime(config.end ?? "", config.timeZone);
3238
- if (end < start)
3239
- throw Error("The end date must be on or after the start date.");
3240
- }
3241
- }
3242
- function metricDateRange(config, query, observed, now, canonical) {
3243
- let min = Infinity, max = -Infinity;
3244
- for (const time of observed) {
3245
- min = Math.min(min, time);
3246
- max = Math.max(max, time);
3247
- }
3248
- if (config.range === "observed")
3249
- return observed.length ? [min, max] : void 0;
3250
- if (config.range === "custom") {
3251
- const start2 = metricDateFloor(
3252
- metricDateTime(config.start ?? "", config.timeZone),
3253
- config.interval,
3254
- config.timeZone
3255
- );
3256
- const end2 = metricDateFloor(
3257
- metricCustomEndTime(config.end ?? "", config.timeZone),
3258
- config.interval,
3259
- config.timeZone
3260
- );
3261
- if (observed.length && (start2 > min || end2 < max))
3262
- throw Error(
3263
- "Custom date bounds must include all observed buckets. Narrow the query filters to exclude data."
3264
- );
3265
- return [start2, end2];
3266
- }
3267
- let start = -Infinity, end = Infinity;
3268
- for (const term of query.where) {
3269
- if (isOrGroup(term) || term.negated || canonical(term.field) !== canonical(config.field))
3270
- continue;
3271
- if (!["=", ">", ">=", "<", "<="].includes(term.op)) continue;
3272
- const time = Number(coerceValue("datetime", term.value, now));
3273
- if (term.op === ">" || term.op === ">=" || term.op === "=")
3274
- start = Math.max(start, time + (term.op === ">" ? 1 : 0));
3275
- if (term.op === "<" || term.op === "<=" || term.op === "=")
3276
- end = Math.min(end, time - (term.op === "<" ? 1 : 0));
3277
- }
3278
- if (!Number.isFinite(start) || !Number.isFinite(end)) {
3279
- const missing = [
3280
- ...Number.isFinite(start) ? [] : ["a lower bound (>= or >)"],
3281
- ...Number.isFinite(end) ? [] : ["an upper bound (<= or <)"]
3282
- ].join(" and ");
3283
- throw Error(
3284
- `Include empty dates with range "query" needs a bounded date filter on ${config.field}: add ${missing} as top-level filters (bounds inside OR groups do not count). Otherwise use range "observed" to fill from the first to the last observed date, or range "custom" with start and end.`
3285
- );
3596
+ try {
3597
+ const value = run(ast);
3598
+ return inspection ? { value, regex: inspection } : { value };
3599
+ } catch (e) {
3600
+ return {
3601
+ value: null,
3602
+ error: e instanceof Error ? e.message : "Formula evaluation failed."
3603
+ };
3286
3604
  }
3287
- if (end < start) return void 0;
3288
- return [
3289
- metricDateFloor(start, config.interval, config.timeZone),
3290
- metricDateFloor(end, config.interval, config.timeZone)
3291
- ];
3292
3605
  }
3293
3606
 
3294
3607
  // src/metricPrecision.ts
@@ -3307,11 +3620,110 @@ function checkedMetricResult(result) {
3307
3620
  }
3308
3621
  }
3309
3622
 
3623
+ // src/metricDistributions.ts
3624
+ function percentile(sorted, p) {
3625
+ if (!Number.isFinite(p) || p < 0 || p > 1)
3626
+ throw Error("Percentile must be between zero and one.");
3627
+ if (sorted.some((v, i) => !Number.isFinite(v) || i > 0 && v < sorted[i - 1]))
3628
+ throw Error("Percentile inputs must be sorted finite numbers.");
3629
+ sorted.forEach(assertMetricNumber);
3630
+ if (!sorted.length) return null;
3631
+ const position = (sorted.length - 1) * p, lower = Math.floor(position), fraction = position - lower;
3632
+ const a = sorted[lower], b = sorted[Math.min(lower + 1, sorted.length - 1)];
3633
+ const raw = (1 - fraction) * a + fraction * b;
3634
+ assertMetricNumber(raw);
3635
+ const value = Math.min(b, Math.max(a, raw));
3636
+ return value;
3637
+ }
3638
+ function boxSummary(values, whiskers = "minmax") {
3639
+ if (values.some((v) => !Number.isFinite(v)))
3640
+ throw Error("Distribution inputs must be finite numbers.");
3641
+ values.forEach(assertMetricNumber);
3642
+ const sorted = [...values].sort((a, b) => a - b), n = sorted.length;
3643
+ if (!n) return null;
3644
+ const min = sorted[0], max = sorted[n - 1], q1 = percentile(sorted, 0.25), median = percentile(sorted, 0.5), q3 = percentile(sorted, 0.75);
3645
+ const lowFence = q1 - 1.5 * (q3 - q1), highFence = q3 + 1.5 * (q3 - q1);
3646
+ const low = whiskers === "tukey" ? sorted.find((v) => v >= lowFence) : min;
3647
+ const high = whiskers === "tukey" ? [...sorted].reverse().find((v) => v <= highFence) : max;
3648
+ const outside = whiskers === "tukey" ? sorted.filter((v) => v < low || v > high) : [];
3649
+ const outliers = outside.length <= 20 ? outside : [...outside.slice(0, 10), ...outside.slice(-10)];
3650
+ const rawMean = sorted.reduce((sum, value) => sum + value / n, 0);
3651
+ assertMetricNumber(rawMean);
3652
+ const mean = Math.min(max, Math.max(min, rawMean));
3653
+ [min, q1, median, q3, max, mean, low, high].forEach(assertMetricNumber);
3654
+ return {
3655
+ n,
3656
+ min,
3657
+ q1,
3658
+ median,
3659
+ q3,
3660
+ max,
3661
+ mean,
3662
+ low,
3663
+ high,
3664
+ outliers,
3665
+ outlierCount: outside.length,
3666
+ whiskers,
3667
+ method: "exact-linear"
3668
+ };
3669
+ }
3670
+ function histogramEdges(values, bins) {
3671
+ if (!Number.isInteger(bins) || bins < 2 || bins > 30)
3672
+ throw Error("Choose 2\u201330 histogram bins.");
3673
+ if (values.some((v) => !Number.isFinite(v)))
3674
+ throw Error("Histogram inputs must be finite numbers.");
3675
+ values.forEach(assertMetricNumber);
3676
+ if (!values.length) return [];
3677
+ let min = Infinity, max = -Infinity;
3678
+ for (const value of values) {
3679
+ min = Math.min(min, value);
3680
+ max = Math.max(max, value);
3681
+ }
3682
+ if (min === max) return [min, max];
3683
+ if (!Number.isFinite(max - min))
3684
+ throw Error("Histogram range exceeds finite numeric precision.");
3685
+ const edges = Array.from(
3686
+ { length: bins + 1 },
3687
+ (_, i) => min + (max - min) * (i / bins)
3688
+ );
3689
+ edges[0] = min;
3690
+ edges[bins] = max;
3691
+ edges.forEach(assertMetricNumber);
3692
+ if (edges.some((value, i) => i > 0 && value <= edges[i - 1]))
3693
+ throw Error(
3694
+ "Histogram bins are finer than numeric precision. Reduce the bin count."
3695
+ );
3696
+ return edges;
3697
+ }
3698
+ function histogramCounts(values, edges) {
3699
+ if (values.some((v) => !Number.isFinite(v)))
3700
+ throw Error("Histogram inputs must be finite numbers.");
3701
+ if (!edges.length) {
3702
+ if (values.length) throw Error("Histogram values require edges.");
3703
+ return [];
3704
+ }
3705
+ if (edges.length < 2 || edges.some(
3706
+ (v, i) => !Number.isFinite(v) || i > 0 && (v < edges[i - 1] || v === edges[i - 1] && edges.length !== 2)
3707
+ ))
3708
+ throw Error("Histogram edges must be ordered finite boundaries.");
3709
+ values.forEach(assertMetricNumber);
3710
+ edges.forEach(assertMetricNumber);
3711
+ const counts = Array(edges.length - 1).fill(0);
3712
+ for (const value of values) {
3713
+ if (value < edges[0] || value > edges[edges.length - 1])
3714
+ throw Error("Value falls outside shared histogram edges.");
3715
+ let bin = 0;
3716
+ while (bin < counts.length - 1 && value >= edges[bin + 1]) bin++;
3717
+ counts[bin] = counts[bin] + 1;
3718
+ }
3719
+ return counts;
3720
+ }
3721
+
3310
3722
  // src/metricSeries.ts
3311
3723
  var MAX_METRIC_WINDOW_PERIODS = 1e3;
3312
3724
  var MERGEABLE = ["SUM", "COUNT", "MIN", "MAX", "AVG"];
3313
3725
  function hasMetricSeries(c) {
3314
- return c.window !== void 0 || c.share !== void 0 || c.dateGrouping?.partialIntervals !== void 0;
3726
+ return c.window !== void 0 || c.share !== void 0 || c.dateGrouping?.partialIntervals !== void 0 || c.dateGrouping?.occurrence !== void 0;
3315
3727
  }
3316
3728
  var soleAggregate = (plan) => plan.ast.kind === "call" && plan.aggregates.includes(plan.ast) ? plan.ast.name : void 0;
3317
3729
  var partBudget = (c, plan) => (c.window && (c.window.reduce ?? "total") === "total" ? plan.aggregates.reduce((n, node) => n + (node.name === "AVG" ? 2 : 1), 0) : 1) * (c.share ? 2 : 1);
@@ -3324,7 +3736,7 @@ function validateMetricSeries(c, plan) {
3324
3736
  );
3325
3737
  const metric = plan, sole = soleAggregate(metric);
3326
3738
  if (window) {
3327
- if (!c.dateGrouping)
3739
+ if (!c.dateGrouping || isMetricCycle(c.dateGrouping.interval))
3328
3740
  throw new FormulaError(
3329
3741
  "A window needs a calendar interval on the first grouping field."
3330
3742
  );
@@ -3374,12 +3786,12 @@ function validateMetricSeries(c, plan) {
3374
3786
  `A window or share runs one metric per aggregate; this one needs more than ${MAX_AGGREGATIONS}.`
3375
3787
  );
3376
3788
  }
3377
- function wholeIntervalSpan(c, query, schema, now) {
3378
- const { field, interval, timeZone } = c.dateGrouping;
3789
+ function metricDateBounds(c, query, schema, now) {
3790
+ const { field } = c.dateGrouping;
3379
3791
  const canonical = (name) => resolveFieldName(schema, name) ?? name;
3380
3792
  let lower = -Infinity, upper = Infinity;
3381
3793
  for (const term of [...query.where, ...c.where ?? []]) {
3382
- if (isOrGroup(term) || term.negated || canonical(term.field) !== canonical(field) || !["=", ">", ">=", "<", "<="].includes(term.op))
3794
+ if (isWhereGroup(term) || term.negated || canonical(term.field) !== canonical(field) || !["=", ">", ">=", "<", "<="].includes(term.op))
3383
3795
  continue;
3384
3796
  if (term.value === "") continue;
3385
3797
  let time;
@@ -3394,6 +3806,11 @@ function wholeIntervalSpan(c, query, schema, now) {
3394
3806
  if (term.op === "<" || term.op === "<=" || term.op === "=")
3395
3807
  upper = Math.min(upper, time + (term.op === "<" ? 0 : 1));
3396
3808
  }
3809
+ return { lower, upper };
3810
+ }
3811
+ function wholeIntervalSpan(c, query, schema, now) {
3812
+ const { interval, timeZone } = c.dateGrouping;
3813
+ const { lower, upper } = metricDateBounds(c, query, schema, now);
3397
3814
  const floor = (time) => metricDateFloor(time, interval, timeZone);
3398
3815
  const end = floor(Math.min(upper, now));
3399
3816
  if (!Number.isFinite(lower)) return { end };
@@ -3431,45 +3848,88 @@ function planMetricSeries(query, schema, options2 = {}) {
3431
3848
  function prepare(c) {
3432
3849
  const issue = validateMetric(c, schema, options2.resolveComputed)[0];
3433
3850
  if (issue) return { kind: "error", clause: c, error: issue.message };
3851
+ function whole(config) {
3852
+ let where2 = c.where ?? [], dateGrouping2 = config;
3853
+ const span = wholeIntervalSpan(
3854
+ { ...c, dateGrouping: config },
3855
+ query,
3856
+ schema,
3857
+ now
3858
+ );
3859
+ if (span.start !== void 0 && span.end <= span.start) {
3860
+ if (!isMetricCycle(config.interval)) return void 0;
3861
+ span.end = span.start;
3862
+ }
3863
+ const field = resolveFieldName(schema, config.field) ?? config.field;
3864
+ const iso = (time) => new Date(time).toISOString();
3865
+ where2 = [
3866
+ ...where2,
3867
+ ...span.start !== void 0 ? [{ field, op: ">=", value: iso(span.start) }] : [],
3868
+ { field, op: "<", value: iso(span.end) }
3869
+ ];
3870
+ if (config.includeEmpty && config.range !== "observed" && !isMetricCycle(config.interval)) {
3871
+ const floor = (time) => metricDateFloor(time, config.interval, config.timeZone);
3872
+ let from = span.start, through = span.end - 1;
3873
+ if (config.range === "custom") {
3874
+ const start = floor(
3875
+ metricDateTime(config.start ?? "", config.timeZone)
3876
+ );
3877
+ from = Math.max(from ?? start, start);
3878
+ through = Math.min(
3879
+ through,
3880
+ metricCustomEndTime(config.end ?? "", config.timeZone)
3881
+ );
3882
+ }
3883
+ if (from !== void 0) {
3884
+ if (through < from) return void 0;
3885
+ dateGrouping2 = {
3886
+ ...config,
3887
+ range: "custom",
3888
+ start: iso(from),
3889
+ end: iso(through)
3890
+ };
3891
+ }
3892
+ }
3893
+ return { where: where2, dateGrouping: dateGrouping2 };
3894
+ }
3434
3895
  let where = c.where ?? [];
3435
3896
  let dateGrouping;
3436
3897
  if (c.dateGrouping) {
3437
- const { partialIntervals, ...config } = c.dateGrouping;
3438
- dateGrouping = config;
3439
- if (partialIntervals === "exclude") {
3440
- const span = wholeIntervalSpan(c, query, schema, now);
3441
- if (span.start !== void 0 && span.end <= span.start)
3442
- return { kind: "empty", clause: c };
3443
- const field = resolveFieldName(schema, config.field) ?? config.field;
3444
- const iso = (time) => new Date(time).toISOString();
3445
- where = [
3446
- ...where,
3447
- ...span.start !== void 0 ? [{ field, op: ">=", value: iso(span.start) }] : [],
3448
- { field, op: "<", value: iso(span.end) }
3449
- ];
3450
- if (config.includeEmpty && config.range !== "observed") {
3451
- const floor = (time) => metricDateFloor(time, config.interval, config.timeZone);
3452
- let from = span.start, through = span.end - 1;
3453
- if (config.range === "custom") {
3454
- const start = floor(
3455
- metricDateTime(config.start ?? "", config.timeZone)
3456
- );
3457
- from = Math.max(from ?? start, start);
3458
- through = Math.min(
3459
- through,
3460
- metricCustomEndTime(config.end ?? "", config.timeZone)
3461
- );
3462
- }
3463
- if (from !== void 0) {
3464
- if (through < from) return { kind: "empty", clause: c };
3465
- dateGrouping = {
3466
- ...config,
3467
- range: "custom",
3468
- start: iso(from),
3469
- end: iso(through)
3470
- };
3471
- }
3472
- }
3898
+ const { partialIntervals, occurrence, ...config } = c.dateGrouping;
3899
+ const narrowed = occurrence ? whole({
3900
+ ...config,
3901
+ interval: occurrence.interval,
3902
+ includeEmpty: occurrence.includeEmpty
3903
+ }) : partialIntervals === "exclude" ? whole(config) : { where, dateGrouping: config };
3904
+ if (!narrowed) return { kind: "empty", clause: c };
3905
+ ({ where, dateGrouping } = narrowed);
3906
+ if (occurrence) {
3907
+ const {
3908
+ dateGrouping: _date,
3909
+ where: _where,
3910
+ scope: _scope,
3911
+ distribution: _distribution,
3912
+ display: _display,
3913
+ sort: _sort,
3914
+ groupLimit: _limit,
3915
+ minCount: _min,
3916
+ label: _label,
3917
+ layout: _layout,
3918
+ ...rest
3919
+ } = c;
3920
+ return {
3921
+ kind: "occurrence",
3922
+ clause: c,
3923
+ parts: [
3924
+ {
3925
+ ...rest,
3926
+ id: id(),
3927
+ ...c.scope ? { scope: c.scope } : {},
3928
+ ...where.length ? { where } : {},
3929
+ dateGrouping
3930
+ }
3931
+ ]
3932
+ };
3473
3933
  }
3474
3934
  }
3475
3935
  const scoped = (groupBy) => ({
@@ -3495,38 +3955,55 @@ function planMetricSeries(query, schema, options2 = {}) {
3495
3955
  );
3496
3956
  const plan = compileMetric(source, schema, options2.resolveComputed);
3497
3957
  const texts = [];
3498
- const at = (text) => {
3499
- const index = texts.indexOf(text);
3500
- return index < 0 ? texts.push(text) - 1 : index;
3958
+ const at = (text2) => {
3959
+ const index = texts.indexOf(text2);
3960
+ return index < 0 ? texts.push(text2) - 1 : index;
3501
3961
  };
3502
3962
  let reductions;
3503
3963
  if (c.window && (c.window.reduce ?? "total") === "total")
3504
3964
  reductions = plan.aggregates.map((node) => {
3505
- const text = source.slice(node.from, node.to);
3506
- if (node.name !== "AVG") return { name: node.name, part: at(text) };
3507
- const input = text.slice(text.indexOf("(") + 1, text.lastIndexOf(")"));
3965
+ const text2 = source.slice(node.from, node.to);
3966
+ if (node.name !== "AVG") return { name: node.name, part: at(text2) };
3967
+ const input = text2.slice(text2.indexOf("(") + 1, text2.lastIndexOf(")"));
3508
3968
  return { name: "AVG", sum: at(`SUM(${input})`), count: at(`COUNT(${input})`) };
3509
3969
  });
3510
3970
  else at(source);
3511
3971
  const parts = (groupBy) => texts.map(
3512
- (text) => ({
3972
+ (text2) => ({
3513
3973
  id: id(),
3514
3974
  // The whole legacy measure keeps its operation, so v1 servers run it.
3515
- ...text === source && c.expression === void 0 ? { op: c.op, ...c.field ? { field: c.field } : {} } : { op: "count", expression: text },
3975
+ ...text2 === source && c.expression === void 0 ? { op: c.op, ...c.field ? { field: c.field } : {} } : { op: "count", expression: text2 },
3516
3976
  groupBy,
3517
3977
  ...scoped(groupBy)
3518
3978
  })
3519
3979
  );
3980
+ const all = [
3981
+ ...parts(c.groupBy),
3982
+ ...c.share === "group0" ? parts(c.groupBy.slice(0, 1)) : c.share === "total" ? parts([]) : []
3983
+ ];
3984
+ const measure = metricMinCountMeasure(c);
3985
+ const minPart = measure ? all.length : void 0;
3986
+ if (measure)
3987
+ all.push({
3988
+ id: id(),
3989
+ op: "count",
3990
+ expression: metricExpression({
3991
+ id: "",
3992
+ groupBy: [],
3993
+ op: measure.op,
3994
+ field: resolveFieldName(schema, measure.field) ?? measure.field
3995
+ }),
3996
+ groupBy: c.groupBy,
3997
+ ...scoped(c.groupBy)
3998
+ });
3520
3999
  return {
3521
4000
  kind: "series",
3522
4001
  clause: c,
3523
4002
  plan,
3524
- parts: [
3525
- ...parts(c.groupBy),
3526
- ...c.share === "group0" ? parts(c.groupBy.slice(0, 1)) : c.share === "total" ? parts([]) : []
3527
- ],
4003
+ parts: all,
3528
4004
  ...reductions ? { reductions } : {},
3529
- size: texts.length
4005
+ size: texts.length,
4006
+ ...minPart !== void 0 ? { minPart } : {}
3530
4007
  };
3531
4008
  }
3532
4009
  const queries = [];
@@ -3548,14 +4025,16 @@ function planMetricSeries(query, schema, options2 = {}) {
3548
4025
  const c = item.clause;
3549
4026
  if (item.kind === "error")
3550
4027
  return { id: c.id, buckets: [], error: item.error };
3551
- if (item.kind === "empty")
3552
- return {
4028
+ if (item.kind === "empty") {
4029
+ const none = {
3553
4030
  id: c.id,
3554
4031
  buckets: [],
3555
4032
  scope: c.scope ?? "allMatching",
3556
4033
  groupCount: 0,
3557
4034
  coverage: "exact"
3558
4035
  };
4036
+ return c.dateGrouping?.occurrence ? combineOccurrences(c, none) : none;
4037
+ }
3559
4038
  const entries = item.parts.map(
3560
4039
  (part) => byId.get(part.id) ?? {
3561
4040
  id: part.id,
@@ -3565,6 +4044,8 @@ function planMetricSeries(query, schema, options2 = {}) {
3565
4044
  );
3566
4045
  if (item.kind === "plain") return { ...entries[0], id: c.id };
3567
4046
  try {
4047
+ if (item.kind === "occurrence")
4048
+ return combineOccurrences(c, entries[0]);
3568
4049
  return combineSeries(item, entries);
3569
4050
  } catch (e) {
3570
4051
  return {
@@ -3617,7 +4098,7 @@ function combineSeries(item, entries) {
3617
4098
  "A window or share needs every group, but the engine returned only some of them."
3618
4099
  );
3619
4100
  }
3620
- const numerator = entries.slice(0, item.size), denominator = c.share ? entries.slice(item.size) : [];
4101
+ const numerator = entries.slice(0, item.size), denominator = c.share ? entries.slice(item.size, item.minPart) : [], threshold = item.minPart !== void 0 ? entries[item.minPart] : void 0;
3621
4102
  const dateOf = (keys) => keys[0] === null || keys[0] === void 0 ? void 0 : keys[0];
3622
4103
  const times = /* @__PURE__ */ new Map();
3623
4104
  if (window)
@@ -3727,10 +4208,32 @@ function combineSeries(item, entries) {
3727
4208
  ...own.intervals !== void 0 ? { intervals: own.intervals } : {}
3728
4209
  };
3729
4210
  });
4211
+ const thresholdTracks = threshold ? tracks(threshold) : void 0, thresholdBuckets = threshold ? exact(threshold) : void 0;
4212
+ const measureOf = (keys) => {
4213
+ const at = positionOf(keys);
4214
+ let members;
4215
+ if (!window || at === void 0)
4216
+ members = [thresholdBuckets.get(JSON.stringify(keys))];
4217
+ else {
4218
+ const from = Math.max(0, at - periods + 1), track = thresholdTracks.get(JSON.stringify(keys.slice(1)));
4219
+ members = Array.from(
4220
+ { length: at - from + 1 },
4221
+ (_, i) => track?.[from + i]
4222
+ );
4223
+ }
4224
+ const sum = new Accumulator("SUM");
4225
+ for (const bucket of members) sum.add(bucket);
4226
+ return sum.result();
4227
+ };
3730
4228
  let hiddenGroups;
3731
- if (c.minCount !== void 0) {
4229
+ const min = metricMinCount(c);
4230
+ if (min !== void 0) {
3732
4231
  const kept = buckets.filter(
3733
- (b) => b.count === 0 || b.count >= c.minCount
4232
+ (b) => metricMinCountKeeps(
4233
+ min,
4234
+ b.count,
4235
+ threshold ? measureOf(b.keys) : void 0
4236
+ )
3734
4237
  );
3735
4238
  hiddenGroups = buckets.length - kept.length;
3736
4239
  buckets = kept;
@@ -3749,31 +4252,87 @@ function combineSeries(item, entries) {
3749
4252
  ...hiddenGroups !== void 0 ? { hiddenGroups } : {}
3750
4253
  };
3751
4254
  }
3752
-
3753
- // src/groupKeys.ts
3754
- function datetimeValue(value) {
3755
- const time = value instanceof Date ? value.getTime() : typeof value === "number" ? value : typeof value === "string" && value !== "" ? Date.parse(value) : NaN;
3756
- return Number.isFinite(time) ? new Date(time).toISOString() : null;
3757
- }
3758
- function keyClause(field, value) {
3759
- if (!field || field.type === "textarray" || !isFilterable(field)) return null;
3760
- if (value == null) return { field: field.name, op: "is_null", value: "" };
3761
- if (field.type === "datetime") {
3762
- const iso = datetimeValue(value);
3763
- return iso === null ? null : { field: field.name, op: "=", value: iso };
3764
- }
3765
- if (typeof value === "number" ? !Number.isFinite(value) : typeof value !== "string" && typeof value !== "boolean")
3766
- return null;
3767
- return { field: field.name, op: "=", value: String(value) };
3768
- }
3769
- function groupKeyTerms(keys, group, schema) {
3770
- const terms = [];
3771
- for (const key of keys) {
3772
- const term = keyClause(resolveField(schema, key), group[key]);
3773
- if (!term) return null;
3774
- terms.push(term);
4255
+ function combineOccurrences(c, entry) {
4256
+ const config = c.dateGrouping, cycle = config.interval;
4257
+ if (!isMetricCycle(cycle)) throw Error("Invalid date grouping occurrence.");
4258
+ if (entry.error) throw Error(entry.error);
4259
+ if (entry.coverage === "partial" || (entry.groupCount ?? entry.buckets.length) > entry.buckets.length)
4260
+ throw Error(
4261
+ "Occurrences need every interval, but the engine returned only some of them."
4262
+ );
4263
+ const slots = /* @__PURE__ */ new Map();
4264
+ const slotOf = (keys) => {
4265
+ const key = JSON.stringify(keys);
4266
+ let slot = slots.get(key);
4267
+ if (!slot) slots.set(key, slot = { keys, count: 0, values: [] });
4268
+ return slot;
4269
+ };
4270
+ for (const bucket of entry.buckets) {
4271
+ if (bucket.keys[0] === null || bucket.keys[0] === void 0) continue;
4272
+ const slot = slotOf([
4273
+ metricCycleSlot(metricDateTime(bucket.keys[0]), cycle, config.timeZone),
4274
+ ...bucket.keys.slice(1)
4275
+ ]);
4276
+ slot.count += bucket.count;
4277
+ if (slot.error) continue;
4278
+ if (bucket.error) slot.error = bucket.error;
4279
+ else if (typeof bucket.value === "number") slot.values.push(bucket.value);
4280
+ else if (bucket.value !== null)
4281
+ slot.error = "Occurrence values must be numeric.";
4282
+ }
4283
+ if (config.includeEmpty) {
4284
+ const tails = /* @__PURE__ */ new Map();
4285
+ for (const slot of slots.values())
4286
+ tails.set(JSON.stringify(slot.keys.slice(1)), slot.keys.slice(1));
4287
+ if (c.groupBy.length === 1 && !tails.size) tails.set("[]", []);
4288
+ for (const number of metricCycleSlots(cycle))
4289
+ for (const tail of tails.values()) slotOf([number, ...tail]);
4290
+ }
4291
+ const box = occurrenceDistribution(c);
4292
+ let buckets = [...slots.values()].map((slot) => {
4293
+ const bucket = {
4294
+ keys: slot.keys,
4295
+ count: slot.count,
4296
+ value: null,
4297
+ occurrences: slot.values.length
4298
+ };
4299
+ if (slot.error) return { ...bucket, error: slot.error };
4300
+ try {
4301
+ if (box) {
4302
+ const summary = boxSummary(slot.values, c.distribution.whiskers);
4303
+ bucket.distribution = { kind: "box", summary };
4304
+ bucket.value = summary?.median ?? null;
4305
+ } else if (slot.values.length) {
4306
+ const mean = slot.values.reduce((sum, value) => sum + value, 0) / slot.values.length;
4307
+ assertMetricNumber(mean);
4308
+ bucket.value = mean;
4309
+ }
4310
+ } catch (e) {
4311
+ return { ...bucket, value: null, error: e.message };
4312
+ }
4313
+ return bucket;
4314
+ });
4315
+ let hiddenGroups;
4316
+ const min = metricMinCount(c);
4317
+ if (min !== void 0) {
4318
+ const kept = buckets.filter(
4319
+ (b) => b.count === 0 || b.count >= min
4320
+ );
4321
+ hiddenGroups = buckets.length - kept.length;
4322
+ buckets = kept;
3775
4323
  }
3776
- return terms;
4324
+ sortMetricBuckets(buckets, c);
4325
+ const groupCount = buckets.length;
4326
+ if (c.groupLimit) buckets = buckets.slice(0, c.groupLimit);
4327
+ return {
4328
+ id: c.id,
4329
+ buckets,
4330
+ scope: entry.scope ?? c.scope ?? "allMatching",
4331
+ ...entry.processedRows !== void 0 ? { processedRows: entry.processedRows } : {},
4332
+ groupCount,
4333
+ coverage: "exact",
4334
+ ...hiddenGroups !== void 0 ? { hiddenGroups } : {}
4335
+ };
3777
4336
  }
3778
4337
 
3779
4338
  // src/grouping.ts
@@ -3793,29 +4352,37 @@ function withoutGrouping(query) {
3793
4352
  return result;
3794
4353
  }
3795
4354
  function exitGrouping(query) {
3796
- return query.grouping ? query.grouping.source : query;
4355
+ if (!query.grouping) return query;
4356
+ if (!query.hostState && !query.grouping.source.hostState) return query.grouping.source;
4357
+ const { hostState: _source, ...source } = query.grouping.source;
4358
+ return query.hostState ? { ...source, hostState: query.hostState } : source;
3797
4359
  }
3798
4360
  function withGroupingKeys(query, keys) {
3799
4361
  const before = query.grouping;
3800
4362
  if (!keys.length) return exitGrouping(query);
3801
- if (!before)
4363
+ const names = groupKeyFields(keys);
4364
+ if (!before) {
4365
+ const { hostState, ...source } = query;
3802
4366
  return {
3803
- select: keys.map((field) => ({ field })),
4367
+ select: names.map((field) => ({ field })),
3804
4368
  where: [],
3805
4369
  orderBy: [],
3806
4370
  limit: query.limit,
3807
4371
  offset: 0,
3808
- grouping: { keys, summaries: [], representative: null, source: query }
4372
+ grouping: { keys, summaries: [], representative: null, source },
4373
+ ...hostState ? { hostState } : {}
3809
4374
  };
3810
- const removed = new Set(before.keys.filter((key) => !keys.includes(key)));
3811
- const columns = query.select.length ? query.select : before.keys.map((field) => ({ field }));
4375
+ }
4376
+ const previous = groupKeyFields(before.keys);
4377
+ const removed = new Set(previous.filter((key) => !names.includes(key)));
4378
+ const columns = query.select.length ? query.select : previous.map((field) => ({ field }));
3812
4379
  return {
3813
4380
  ...query,
3814
4381
  grouping: { ...before, keys },
3815
4382
  offset: 0,
3816
4383
  aggregations: [],
3817
4384
  select: [
3818
- ...keys.filter((key) => !before.keys.includes(key)).map((field) => ({ field })),
4385
+ ...names.filter((key) => !previous.includes(key)).map((field) => ({ field })),
3819
4386
  ...columns.filter((c) => !removed.has(c.field))
3820
4387
  ],
3821
4388
  where: query.where.filter(
@@ -3905,14 +4472,15 @@ function createGroupingModel(inputSchema) {
3905
4472
  if (link.to.kind === "template")
3906
4473
  for (const name of templateFields(link.to.template)) names.add(name);
3907
4474
  else if (link.to.keyFilters !== false)
3908
- for (const key of keys) names.add(key);
4475
+ for (const key of keys) names.add(groupKeyField(key));
3909
4476
  return [...names];
3910
4477
  }
3911
4478
  function linkProblem(column, keys, summaries) {
3912
4479
  const link = column.link;
3913
4480
  if (column.op !== "link") return null;
3914
4481
  if (!link) return "Invalid link definition.";
3915
- const readable = (name) => keys.includes(name) || summaries.some((other) => other.id === name && other.op !== "link");
4482
+ const names = groupKeyFields(keys);
4483
+ const readable = (name) => names.includes(name) || summaries.some((other) => other.id === name && other.op !== "link");
3916
4484
  if (typeof link.label === "object" && !readable(link.label.column))
3917
4485
  return `Unknown link label column: ${link.label.column}.`;
3918
4486
  if (link.to.kind === "template") {
@@ -3920,7 +4488,7 @@ function createGroupingModel(inputSchema) {
3920
4488
  return urlTemplateProblem(link.to.template, [], readable);
3921
4489
  }
3922
4490
  if (link.to.keyFilters !== false)
3923
- for (const key of keys) {
4491
+ for (const key of names) {
3924
4492
  const field = sourceField(key);
3925
4493
  if (field?.type === "textarray")
3926
4494
  return `Key filters cannot match the textarray key ${field.label}.`;
@@ -3969,11 +4537,12 @@ function createGroupingModel(inputSchema) {
3969
4537
  aggregate: { measure: false, groupable: false }
3970
4538
  },
3971
4539
  ...keys.flatMap((key) => {
3972
- const field = sourceField(key);
4540
+ const field = sourceField(groupKeyField(key));
3973
4541
  if (!field) return [];
3974
4542
  return [{
3975
- name: key,
3976
- label: field.label,
4543
+ // A calendar key's column holds each interval's start instant.
4544
+ name: field.name,
4545
+ label: isCalendarGroupKey(key) ? `${field.label} (${key.interval})` : field.label,
3977
4546
  type: field.type,
3978
4547
  source: { kind: "backend" },
3979
4548
  ...field.select ? { select: { ...field.select } } : {},
@@ -4054,7 +4623,7 @@ function createGroupingModel(inputSchema) {
4054
4623
  name: `${inputSchema.name}:groups`,
4055
4624
  idField: GROUP_ID_FIELD,
4056
4625
  fields,
4057
- defaultSelect: keys.filter((key) => sourceField(key)).map((field) => ({ field })),
4626
+ defaultSelect: groupKeyFields(keys).filter((key) => sourceField(key)).map((field) => ({ field })),
4058
4627
  defaultLimit: 25
4059
4628
  };
4060
4629
  return schema;
@@ -4066,13 +4635,8 @@ function createGroupingModel(inputSchema) {
4066
4635
  return value;
4067
4636
  throw new Error("Group keys must be finite scalar values.");
4068
4637
  }
4069
- function groupId(row, keys, schema) {
4070
- const tuple = [...keys].sort().map((key) => [
4071
- key,
4072
- keyValue(
4073
- readFieldValue(schema.fields.find((f2) => f2.name === key), row)
4074
- )
4075
- ]);
4638
+ function groupId(values) {
4639
+ const tuple = [...values].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([key, value]) => [key, keyValue(value)]);
4076
4640
  if (tuple.some(
4077
4641
  ([key, value]) => String(key).length + String(value).length > 1e4
4078
4642
  ))
@@ -4111,8 +4675,8 @@ function createGroupingModel(inputSchema) {
4111
4675
  }));
4112
4676
  return rows.reduce((best, row) => {
4113
4677
  for (const term of terms) {
4114
- const value = (record2) => {
4115
- const raw = term.def ? readFieldValue(term.def, record2) : record2[schema.idField];
4678
+ const value = (record3) => {
4679
+ const raw = term.def ? readFieldValue(term.def, record3) : record3[schema.idField];
4116
4680
  if (raw == null || term.def?.type !== "datetime") return raw;
4117
4681
  const instant = Date.parse(String(raw));
4118
4682
  return Number.isFinite(instant) ? instant : null;
@@ -4141,11 +4705,21 @@ function createGroupingModel(inputSchema) {
4141
4705
  throw new Error("Grouping row budget exceeded.");
4142
4706
  let bytes = rows.length * 16;
4143
4707
  let operations = 0;
4708
+ const readers = keys.map((key) => {
4709
+ const name = groupKeyField(key);
4710
+ const field = schema.fields.find((f2) => f2.name === name);
4711
+ const calendar = isCalendarGroupKey(key) ? calendarKeyReader(key) : null;
4712
+ return [
4713
+ name,
4714
+ (row) => calendar ? calendar(readFieldValue(field, row)) : readFieldValue(field, row)
4715
+ ];
4716
+ });
4144
4717
  const groups = /* @__PURE__ */ new Map();
4145
4718
  for (const row of rows) {
4146
- const id = groupId(row, keys, schema);
4719
+ const values = readers.map(([, read]) => read(row));
4720
+ const id = groupId(readers.map(([name], i) => [name, values[i]]));
4147
4721
  const bucket = groups.get(id);
4148
- if (bucket) bucket.push(row);
4722
+ if (bucket) bucket.rows.push(row);
4149
4723
  else {
4150
4724
  if (groups.size >= MAX_RESULT_GROUPS)
4151
4725
  throw new Error(
@@ -4154,7 +4728,7 @@ function createGroupingModel(inputSchema) {
4154
4728
  bytes += id.length * 2 + 256;
4155
4729
  if (bytes > 16e6)
4156
4730
  throw new Error("Grouping retained-memory budget exceeded.");
4157
- groups.set(id, [row]);
4731
+ groups.set(id, { keys: values, rows: [row] });
4158
4732
  }
4159
4733
  }
4160
4734
  const formulas = new Map(
@@ -4181,15 +4755,13 @@ function createGroupingModel(inputSchema) {
4181
4755
  compiled.plan?.aggregates.map(nodeCost) ?? []
4182
4756
  ])
4183
4757
  );
4184
- return [...groups].map(([id, members]) => {
4758
+ return [...groups].map(([id, { keys: values, rows: members }]) => {
4185
4759
  const output = Object.assign(/* @__PURE__ */ Object.create(null), {
4186
4760
  [GROUP_ID_FIELD]: id
4187
4761
  });
4188
- for (const key of keys)
4189
- output[key] = readFieldValue(
4190
- schema.fields.find((f2) => f2.name === key),
4191
- members[0]
4192
- );
4762
+ readers.forEach(([name], i) => {
4763
+ output[name] = values[i];
4764
+ });
4193
4765
  const chosen = rule && summaries.some((column) => column.op === "row") ? representative(members, rule, schema) : null;
4194
4766
  for (const column of summaries) {
4195
4767
  if (column.op === "link") continue;
@@ -4209,7 +4781,7 @@ function createGroupingModel(inputSchema) {
4209
4781
  throw new Error("Grouping operation budget exceeded.");
4210
4782
  const rowPlan = rowPlans[i];
4211
4783
  if (!rowPlan) return { value: 1 };
4212
- const values = Object.fromEntries(
4784
+ const values2 = Object.fromEntries(
4213
4785
  rowPlan.dependencies.map((name) => {
4214
4786
  const value = readFieldValue(
4215
4787
  schema.fields.find((f2) => f2.name === name),
@@ -4221,7 +4793,7 @@ function createGroupingModel(inputSchema) {
4221
4793
  ];
4222
4794
  })
4223
4795
  );
4224
- return formulaRuntime(rowPlan.ast, values);
4796
+ return formulaRuntime(rowPlan.ast, values2);
4225
4797
  })
4226
4798
  )
4227
4799
  );
@@ -4290,10 +4862,33 @@ function createGroupingModel(inputSchema) {
4290
4862
  function validate(definition) {
4291
4863
  if (definition.diagnostics?.length)
4292
4864
  throw new Error(definition.diagnostics.join(" "));
4293
- if (!definition.keys.length || definition.keys.length > MAX_GROUP_BY_FIELDS || new Set(definition.keys).size !== definition.keys.length || definition.keys.some(
4294
- (key) => key === GROUP_ID_FIELD || !groupableFields.some((f2) => f2.name === key)
4865
+ const names = definition.keys.map(
4866
+ (key) => typeof key === "string" || isCalendarGroupKey(key) ? groupKeyField(key) : ""
4867
+ );
4868
+ if (!definition.keys.length || definition.keys.length > MAX_GROUP_BY_FIELDS || new Set(names).size !== names.length || names.some(
4869
+ (key) => typeof key !== "string" || key === GROUP_ID_FIELD || !groupableFields.some((f2) => f2.name === key)
4295
4870
  ))
4296
4871
  throw new Error("Choose valid group keys from the source schema.");
4872
+ for (const key of definition.keys) {
4873
+ if (!isCalendarGroupKey(key)) continue;
4874
+ const field = sourceField(key.field);
4875
+ if (isMetricCycle(key.interval))
4876
+ throw new Error(
4877
+ `Group key ${field.label}: hour-of-day and day-of-week are metric intervals (dateGrouping); a group key takes hour, day, week, month or year.`
4878
+ );
4879
+ if (!GROUP_KEY_INTERVALS.includes(key.interval))
4880
+ throw new Error(
4881
+ `Group key ${field.label}: choose an hour, day, week, month or year interval.`
4882
+ );
4883
+ if (field.type !== "datetime")
4884
+ throw new Error(
4885
+ `Group key ${field.label}: a calendar interval needs a datetime field.`
4886
+ );
4887
+ if (key.timeZone !== void 0 && !isGroupingTimeZone(key.timeZone))
4888
+ throw new Error(
4889
+ `Group key ${field.label}: use a valid IANA timezone, such as America/Denver or UTC.`
4890
+ );
4891
+ }
4297
4892
  if (definition.summaries.length > MAX_SUMMARY_COLUMNS)
4298
4893
  throw new Error("Too many aggregate columns.");
4299
4894
  const ids = /* @__PURE__ */ new Set([
@@ -4449,105 +5044,6 @@ function toServerQueryV2(q, schema, execution) {
4449
5044
  };
4450
5045
  }
4451
5046
 
4452
- // src/metricDistributions.ts
4453
- function percentile(sorted, p) {
4454
- if (!Number.isFinite(p) || p < 0 || p > 1)
4455
- throw Error("Percentile must be between zero and one.");
4456
- if (sorted.some((v, i) => !Number.isFinite(v) || i > 0 && v < sorted[i - 1]))
4457
- throw Error("Percentile inputs must be sorted finite numbers.");
4458
- sorted.forEach(assertMetricNumber);
4459
- if (!sorted.length) return null;
4460
- const position = (sorted.length - 1) * p, lower = Math.floor(position), fraction = position - lower;
4461
- const a = sorted[lower], b = sorted[Math.min(lower + 1, sorted.length - 1)];
4462
- const raw = (1 - fraction) * a + fraction * b;
4463
- assertMetricNumber(raw);
4464
- const value = Math.min(b, Math.max(a, raw));
4465
- return value;
4466
- }
4467
- function boxSummary(values, whiskers = "minmax") {
4468
- if (values.some((v) => !Number.isFinite(v)))
4469
- throw Error("Distribution inputs must be finite numbers.");
4470
- values.forEach(assertMetricNumber);
4471
- const sorted = [...values].sort((a, b) => a - b), n = sorted.length;
4472
- if (!n) return null;
4473
- const min = sorted[0], max = sorted[n - 1], q1 = percentile(sorted, 0.25), median = percentile(sorted, 0.5), q3 = percentile(sorted, 0.75);
4474
- const lowFence = q1 - 1.5 * (q3 - q1), highFence = q3 + 1.5 * (q3 - q1);
4475
- const low = whiskers === "tukey" ? sorted.find((v) => v >= lowFence) : min;
4476
- const high = whiskers === "tukey" ? [...sorted].reverse().find((v) => v <= highFence) : max;
4477
- const outside = whiskers === "tukey" ? sorted.filter((v) => v < low || v > high) : [];
4478
- const outliers = outside.length <= 20 ? outside : [...outside.slice(0, 10), ...outside.slice(-10)];
4479
- const rawMean = sorted.reduce((sum, value) => sum + value / n, 0);
4480
- assertMetricNumber(rawMean);
4481
- const mean = Math.min(max, Math.max(min, rawMean));
4482
- [min, q1, median, q3, max, mean, low, high].forEach(assertMetricNumber);
4483
- return {
4484
- n,
4485
- min,
4486
- q1,
4487
- median,
4488
- q3,
4489
- max,
4490
- mean,
4491
- low,
4492
- high,
4493
- outliers,
4494
- outlierCount: outside.length,
4495
- whiskers,
4496
- method: "exact-linear"
4497
- };
4498
- }
4499
- function histogramEdges(values, bins) {
4500
- if (!Number.isInteger(bins) || bins < 2 || bins > 30)
4501
- throw Error("Choose 2\u201330 histogram bins.");
4502
- if (values.some((v) => !Number.isFinite(v)))
4503
- throw Error("Histogram inputs must be finite numbers.");
4504
- values.forEach(assertMetricNumber);
4505
- if (!values.length) return [];
4506
- let min = Infinity, max = -Infinity;
4507
- for (const value of values) {
4508
- min = Math.min(min, value);
4509
- max = Math.max(max, value);
4510
- }
4511
- if (min === max) return [min, max];
4512
- if (!Number.isFinite(max - min))
4513
- throw Error("Histogram range exceeds finite numeric precision.");
4514
- const edges = Array.from(
4515
- { length: bins + 1 },
4516
- (_, i) => min + (max - min) * (i / bins)
4517
- );
4518
- edges[0] = min;
4519
- edges[bins] = max;
4520
- edges.forEach(assertMetricNumber);
4521
- if (edges.some((value, i) => i > 0 && value <= edges[i - 1]))
4522
- throw Error(
4523
- "Histogram bins are finer than numeric precision. Reduce the bin count."
4524
- );
4525
- return edges;
4526
- }
4527
- function histogramCounts(values, edges) {
4528
- if (values.some((v) => !Number.isFinite(v)))
4529
- throw Error("Histogram inputs must be finite numbers.");
4530
- if (!edges.length) {
4531
- if (values.length) throw Error("Histogram values require edges.");
4532
- return [];
4533
- }
4534
- if (edges.length < 2 || edges.some(
4535
- (v, i) => !Number.isFinite(v) || i > 0 && (v < edges[i - 1] || v === edges[i - 1] && edges.length !== 2)
4536
- ))
4537
- throw Error("Histogram edges must be ordered finite boundaries.");
4538
- values.forEach(assertMetricNumber);
4539
- edges.forEach(assertMetricNumber);
4540
- const counts = Array(edges.length - 1).fill(0);
4541
- for (const value of values) {
4542
- if (value < edges[0] || value > edges[edges.length - 1])
4543
- throw Error("Value falls outside shared histogram edges.");
4544
- let bin = 0;
4545
- while (bin < counts.length - 1 && value >= edges[bin + 1]) bin++;
4546
- counts[bin] = counts[bin] + 1;
4547
- }
4548
- return counts;
4549
- }
4550
-
4551
5047
  // src/metrics.ts
4552
5048
  var aggregates = /* @__PURE__ */ new Set([
4553
5049
  "COUNT",
@@ -4801,8 +5297,11 @@ function hasRegex(plan) {
4801
5297
  function distributionActive(c) {
4802
5298
  return !!c.distribution && (!c.display || c.display.kind === "box" || c.display.kind === "histogram");
4803
5299
  }
5300
+ function occurrenceDistribution(c) {
5301
+ return distributionActive(c) && !!c.dateGrouping?.occurrence;
5302
+ }
4804
5303
  function metricPlans(c, schema, resolve) {
4805
- if (distributionActive(c)) {
5304
+ if (distributionActive(c) && !occurrenceDistribution(c)) {
4806
5305
  const p = compileFormula(
4807
5306
  c.distribution.input,
4808
5307
  resolve ? schema.fields.filter(
@@ -4862,21 +5361,28 @@ function validateMetric(c, schema, resolve) {
4862
5361
  throw new FormulaError(
4863
5362
  "Date grouping must use the first grouping field, with a datetime type."
4864
5363
  );
4865
- if (distributionActive(c))
5364
+ const { occurrence } = c.dateGrouping;
5365
+ if (occurrence) {
5366
+ if (distributionActive(c) && c.distribution.kind !== "box")
5367
+ throw new FormulaError(
5368
+ "Occurrences summarise as a mean or a box plot, not a histogram."
5369
+ );
5370
+ if (c.display?.kind === "scatter")
5371
+ throw new FormulaError(
5372
+ "Occurrences summarise single-value metrics, not scatters."
5373
+ );
5374
+ if (c.window || c.share !== void 0)
5375
+ throw new FormulaError(
5376
+ "Windows and shares do not apply to occurrences of a slot."
5377
+ );
5378
+ } else if (distributionActive(c))
4866
5379
  throw new FormulaError(
4867
5380
  "Date grouping currently supports scalar metrics, not distributions."
4868
5381
  );
4869
5382
  }
4870
5383
  if (c.groupLimit !== void 0 && (!Number.isInteger(c.groupLimit) || c.groupLimit < 1 || c.groupLimit > 1e4))
4871
5384
  throw new FormulaError("Group limit must be 1\u201310,000.");
4872
- if (c.minCount !== void 0) {
4873
- if (!Number.isInteger(c.minCount) || c.minCount < 1 || c.minCount > 1e6)
4874
- throw new FormulaError("Minimum rows per group must be 1\u20131,000,000.");
4875
- if (!c.groupBy.length)
4876
- throw new FormulaError(
4877
- "Minimum rows per group requires at least one grouping field."
4878
- );
4879
- }
5385
+ if (c.minCount !== void 0) validateMinCount(c, schema);
4880
5386
  validateMetricWhere(c.where, schema);
4881
5387
  for (const s of c.sort ?? []) {
4882
5388
  if (s.key === "y" && c.display?.kind !== "scatter")
@@ -4903,6 +5409,81 @@ function validateMetric(c, schema, resolve) {
4903
5409
  ];
4904
5410
  }
4905
5411
  }
5412
+ function metricMinCount(c) {
5413
+ const m = c.minCount;
5414
+ return m === void 0 ? void 0 : typeof m === "number" ? m : m?.min;
5415
+ }
5416
+ function metricMinCountMeasure(c) {
5417
+ const m = c.minCount;
5418
+ if (m === void 0 || m === null || typeof m !== "object") return void 0;
5419
+ return m.distinct !== void 0 ? { op: "count_distinct", field: m.distinct } : { op: "sum", field: m.sum };
5420
+ }
5421
+ function metricMinCountText(c, label = (field) => field) {
5422
+ const min = metricMinCount(c), measure = metricMinCountMeasure(c);
5423
+ if (min === void 0) return void 0;
5424
+ const n = min.toLocaleString();
5425
+ if (!measure) return `${n} ${min === 1 ? "row" : "rows"}`;
5426
+ return measure.op === "sum" ? `${n} total ${label(measure.field)}` : `${n} distinct ${label(measure.field)} ${min === 1 ? "value" : "values"}`;
5427
+ }
5428
+ function metricMinCountPlan(c, schema, resolve) {
5429
+ const measure = metricMinCountMeasure(c);
5430
+ return measure ? compileMetric(
5431
+ metricExpression({
5432
+ id: "",
5433
+ groupBy: [],
5434
+ op: measure.op,
5435
+ field: resolveFieldName(schema, measure.field) ?? measure.field
5436
+ }),
5437
+ schema,
5438
+ resolve
5439
+ ) : void 0;
5440
+ }
5441
+ function metricMinCountKeeps(min, count, measure) {
5442
+ if (count === 0) return true;
5443
+ if (!measure) return count >= min;
5444
+ return measure.error !== void 0 || Number(measure.value ?? 0) >= min;
5445
+ }
5446
+ function validateMinCount(c, schema) {
5447
+ const m = c.minCount;
5448
+ const measured = m !== null && typeof m === "object";
5449
+ const min = measured ? m.min : m;
5450
+ const label = measured ? "Minimum per group" : "Minimum rows per group";
5451
+ if (typeof min !== "number" || !Number.isInteger(min) || min < 1 || min > 1e6)
5452
+ throw new FormulaError(`${label} must be 1\u20131,000,000.`);
5453
+ if (measured) {
5454
+ const o = m;
5455
+ if (Object.keys(o).some((k) => !["min", "distinct", "sum"].includes(k)) || o.distinct === void 0 === (o.sum === void 0))
5456
+ throw new FormulaError(
5457
+ "A minimum per group needs exactly one of distinct and sum."
5458
+ );
5459
+ const name = o.distinct ?? o.sum;
5460
+ const op = o.distinct !== void 0 ? "count_distinct" : "sum";
5461
+ const resolved = typeof name === "string" ? resolveFieldName(schema, name) ?? name : "";
5462
+ const field = schema.fields.find((f2) => f2.name === resolved);
5463
+ if (op === "sum" && field && field.type !== "number")
5464
+ throw new FormulaError(
5465
+ `A summed minimum requires a numeric field, not ${resolved}.`
5466
+ );
5467
+ if (!field || isComputedField(resolved) || !isMeasurable(field) || !aggOpsForField(field).includes(op))
5468
+ throw new FormulaError(
5469
+ op === "sum" ? `Field ${String(name)} cannot be summed for a minimum per group.` : `Field ${String(name)} cannot count distinct values for a minimum per group.`
5470
+ );
5471
+ if (distributionActive(c))
5472
+ throw new FormulaError(
5473
+ "A distinct or summed minimum currently supports scalar metrics, not distributions."
5474
+ );
5475
+ if (c.window && op === "count_distinct")
5476
+ throw new FormulaError(
5477
+ "A distinct minimum cannot be combined across a window's intervals."
5478
+ );
5479
+ if (c.dateGrouping?.occurrence)
5480
+ throw new FormulaError(
5481
+ "A distinct or summed minimum does not apply to occurrences of a slot; use a minimum of rows."
5482
+ );
5483
+ }
5484
+ if (!c.groupBy.length)
5485
+ throw new FormulaError(`${label} requires at least one grouping field.`);
5486
+ }
4906
5487
  function validateMetricWhere(where, schema) {
4907
5488
  if (where === void 0) return;
4908
5489
  if (!Array.isArray(where)) throw new FormulaError("Invalid metric filter.");
@@ -4930,13 +5511,23 @@ function metricPopulationError(clause, support) {
4930
5511
  return "This server does not support metric filters.";
4931
5512
  if (clause.minCount !== void 0 && !support.minCount)
4932
5513
  return "This server does not support a minimum rows per group.";
5514
+ if (metricMinCountMeasure(clause) && !support.minCountMeasures)
5515
+ return "This server does not support a minimum of distinct values or a sum per group.";
4933
5516
  return void 0;
4934
5517
  }
4935
5518
  function metricDependencies(c, schema, resolve) {
4936
5519
  return [
4937
5520
  .../* @__PURE__ */ new Set([
4938
5521
  ...c.groupBy.map((name) => resolveFieldName(schema, name) ?? name),
4939
- ...metricPlans(c, schema, resolve).flatMap((p) => p.dependencies)
5522
+ ...metricPlans(c, schema, resolve).flatMap((p) => p.dependencies),
5523
+ // An invalid minimum fails validation, not dependency discovery.
5524
+ ...(() => {
5525
+ try {
5526
+ return metricMinCountPlan(c, schema, resolve)?.dependencies;
5527
+ } catch {
5528
+ return void 0;
5529
+ }
5530
+ })() ?? []
4940
5531
  ])
4941
5532
  ].sort();
4942
5533
  }
@@ -4946,7 +5537,7 @@ function metricComputationKey(clauses) {
4946
5537
  id: c.id,
4947
5538
  diagnostics: c.diagnostics,
4948
5539
  unsupportedOp: c.unsupportedOp,
4949
- expression: distributionActive(c) ? void 0 : metricExpression(c),
5540
+ expression: distributionActive(c) && !occurrenceDistribution(c) ? void 0 : metricExpression(c),
4950
5541
  expressionY: c.display?.kind === "scatter" ? c.expressionY : void 0,
4951
5542
  scope: c.scope ?? "allMatching",
4952
5543
  groupBy: c.groupBy,
@@ -5046,7 +5637,7 @@ function toMetricQuery(q, schema, execution) {
5046
5637
  code: "unexpanded_series",
5047
5638
  from: 0,
5048
5639
  to: 1,
5049
- message: "Windows, shares and whole intervals are computed from plain metrics; expand the query with planMetricSeries first."
5640
+ message: "Windows, shares, whole intervals and occurrences are computed from plain metrics; expand the query with planMetricSeries first."
5050
5641
  });
5051
5642
  for (const c of q.aggregations ?? [])
5052
5643
  if (c.dateGrouping)
@@ -5395,7 +5986,7 @@ function evaluateMetrics(rows, q, schema, options2 = {}) {
5395
5986
  config.interval,
5396
5987
  config.timeZone
5397
5988
  );
5398
- dateKey = new Date(dateStart).toISOString();
5989
+ dateKey = isMetricCycle(config.interval) ? metricCycleSlot(dateStart, config.interval, config.timeZone) : new Date(dateStart).toISOString();
5399
5990
  }
5400
5991
  keys[0] = dateKey;
5401
5992
  }
@@ -5429,7 +6020,8 @@ function evaluateMetrics(rows, q, schema, options2 = {}) {
5429
6020
  tick();
5430
6021
  reserve(8);
5431
6022
  groupBytes += 8;
5432
- observed.push(metricDateTime(group.keys[0]));
6023
+ if (!isMetricCycle(config.interval))
6024
+ observed.push(metricDateTime(group.keys[0]));
5433
6025
  const tuple = group.keys.slice(1);
5434
6026
  const key = JSON.stringify(tuple);
5435
6027
  if (!series2.has(key)) {
@@ -5444,7 +6036,24 @@ function evaluateMetrics(rows, q, schema, options2 = {}) {
5444
6036
  groupBytes += 132;
5445
6037
  series2.set("[]", []);
5446
6038
  }
5447
- const range = metricDateRange(
6039
+ const slots = isMetricCycle(config.interval) ? metricCycleSlots(config.interval) : void 0;
6040
+ for (const slot of slots ?? [])
6041
+ for (const tuple of series2.values()) {
6042
+ tick();
6043
+ const keys = [slot, ...tuple];
6044
+ const key = JSON.stringify(keys);
6045
+ if (key.length > keyLimit)
6046
+ throw Error("Metric grouping key length budget exceeded.");
6047
+ if (!groups.has(key)) {
6048
+ if (groups.size >= (options2.maxGroups ?? 1e4))
6049
+ throw Error("Metric group budget exceeded.");
6050
+ const bytes = 256 + 16 * keys.length + 4 * key.length;
6051
+ reserve(bytes);
6052
+ groupBytes += bytes;
6053
+ groups.set(key, { keys, rows: [] });
6054
+ }
6055
+ }
6056
+ const range = slots ? void 0 : metricDateRange(
5448
6057
  config,
5449
6058
  q,
5450
6059
  observed,
@@ -5539,12 +6148,16 @@ function evaluateMetrics(rows, q, schema, options2 = {}) {
5539
6148
  );
5540
6149
  };
5541
6150
  const populations = /* @__PURE__ */ new Map();
6151
+ const minPlan = metricMinCountPlan(c, schema, options2.resolveComputed);
6152
+ const measures = /* @__PURE__ */ new Map();
5542
6153
  let buckets = [...groups.values()].map((g) => {
5543
6154
  const bucket = {
5544
6155
  keys: g.keys,
5545
6156
  count: g.rows.length,
5546
6157
  value: null
5547
6158
  };
6159
+ if (minPlan && g.rows.length)
6160
+ measures.set(bucket, reduce(minPlan, g.rows, /* @__PURE__ */ new Set()));
5548
6161
  if (distributionActive(c)) {
5549
6162
  const values = [];
5550
6163
  bucket.nullCount = 0;
@@ -5607,9 +6220,10 @@ function evaluateMetrics(rows, q, schema, options2 = {}) {
5607
6220
  }
5608
6221
  }
5609
6222
  let hiddenGroups;
5610
- if (c.minCount !== void 0) {
6223
+ const min = metricMinCount(c);
6224
+ if (min !== void 0) {
5611
6225
  const kept = buckets.filter(
5612
- (b) => b.count === 0 || b.count >= c.minCount
6226
+ (b) => metricMinCountKeeps(min, b.count, minPlan ? measures.get(b) : void 0)
5613
6227
  );
5614
6228
  hiddenGroups = buckets.length - kept.length;
5615
6229
  buckets = kept;
@@ -5710,7 +6324,7 @@ function matchesClause(row, clause, schema, options2 = {}) {
5710
6324
  { ...clause, field },
5711
6325
  evaluationTime(options2)
5712
6326
  );
5713
- prepareDatetimePredicates([{ kind: "lit", predicate: prepared }], byName);
6327
+ prepareDatetimePredicates([{ branches: [[prepared]] }], byName);
5714
6328
  return matchesWith(byName, row, prepared);
5715
6329
  }
5716
6330
  function applyAggregations(rows, q, schema, options2 = {}) {
@@ -5858,25 +6472,15 @@ function prepareWhereClause(clause, now) {
5858
6472
  };
5859
6473
  }
5860
6474
  function prepareWhereTerm(term, resolveField2, now) {
5861
- if (isOrGroup(term)) {
5862
- return {
5863
- kind: "or",
5864
- predicates: term.any.map(
5865
- (c) => prepareWhereClause({ ...c, field: resolveField2(c.field) }, now)
5866
- )
5867
- };
5868
- }
5869
6475
  return {
5870
- kind: "lit",
5871
- predicate: prepareWhereClause(
5872
- { ...term, field: resolveField2(term.field) },
5873
- now
6476
+ branches: branchesOf(term).map(
6477
+ (branch) => branch.map((c) => prepareWhereClause({ ...c, field: resolveField2(c.field) }, now))
5874
6478
  )
5875
6479
  };
5876
6480
  }
5877
6481
  function prepareDatetimePredicates(terms, fields) {
5878
6482
  for (const term of terms)
5879
- for (const p of term.kind === "lit" ? [term.predicate] : term.predicates) {
6483
+ for (const p of term.branches.flat()) {
5880
6484
  const field = fields.get(p.clause.field);
5881
6485
  if (field?.type !== "datetime" || p.clause.value === "" || p.clause.op === "is_null" || p.clause.op === "is_not_null")
5882
6486
  continue;
@@ -5884,8 +6488,7 @@ function prepareDatetimePredicates(terms, fields) {
5884
6488
  }
5885
6489
  }
5886
6490
  function matchesTerm(byName, row, term) {
5887
- if (term.kind === "lit") return matchesWith(byName, row, term.predicate);
5888
- return term.predicates.some((p) => matchesWith(byName, row, p));
6491
+ return term.branches.some((branch) => branch.every((p) => matchesWith(byName, row, p)));
5889
6492
  }
5890
6493
  function matchesWith(byName, row, prepared) {
5891
6494
  const { clause } = prepared;
@@ -6053,6 +6656,7 @@ function memoryStorageAdapter() {
6053
6656
  const saved = /* @__PURE__ */ new Map();
6054
6657
  const defaults = /* @__PURE__ */ new Map();
6055
6658
  return {
6659
+ storesHostState: true,
6056
6660
  async loadLast(key) {
6057
6661
  const query = last.get(key);
6058
6662
  return query ? normalizeQueryState(query) : null;
@@ -6143,6 +6747,7 @@ function localStorageAdapter() {
6143
6747
  else ls.removeItem(DEFAULT_PREFIX + key);
6144
6748
  };
6145
6749
  return {
6750
+ storesHostState: true,
6146
6751
  async loadLast(key) {
6147
6752
  if (!ls) return null;
6148
6753
  try {
@@ -6325,7 +6930,7 @@ function sourceDependencies(query, schema, resolveComputed) {
6325
6930
  if (query.grouping) {
6326
6931
  const model = createGroupingModel(schema);
6327
6932
  model.validate(query.grouping);
6328
- for (const key of query.grouping.keys) fields.add(key);
6933
+ for (const key of groupKeyFields(query.grouping.keys)) fields.add(key);
6329
6934
  for (const summary of query.grouping.summaries) {
6330
6935
  if (summary.op === "link") continue;
6331
6936
  if (summary.op === "row") fields.add(summary.field);
@@ -6372,8 +6977,9 @@ function clauseKey(clause) {
6372
6977
  ]);
6373
6978
  }
6374
6979
  function whereTermKey(term) {
6375
- if (!isOrGroup(term)) return clauseKey(term);
6376
- return `any:${JSON.stringify(term.any.map(clauseKey).sort())}`;
6980
+ if (!isWhereGroup(term)) return clauseKey(term);
6981
+ const branchKey = (member) => isWhereConjunction(member) ? `all:${JSON.stringify(member.all.map(clauseKey).sort())}` : clauseKey(member);
6982
+ return `any:${JSON.stringify(term.any.map(branchKey).sort())}`;
6377
6983
  }
6378
6984
  function appendWhereTerms(where, terms) {
6379
6985
  const present = new Set(where.map(whereTermKey));
@@ -6391,7 +6997,7 @@ function filterUnits(where) {
6391
6997
  for (const term of where) {
6392
6998
  const last = units[units.length - 1];
6393
6999
  const previous = last?.[last.length - 1];
6394
- if (last && previous && !isOrGroup(term) && !isOrGroup(previous) && term.op === "!=" && previous.op === "!=" && term.field === previous.field)
7000
+ if (last && previous && !isWhereGroup(term) && !isWhereGroup(previous) && term.op === "!=" && previous.op === "!=" && term.field === previous.field)
6395
7001
  last.push(term);
6396
7002
  else units.push([term]);
6397
7003
  }
@@ -6629,6 +7235,8 @@ function toQueryRequestV3(query, baseSchema, capabilities, execution, options2 =
6629
7235
  let resultSchema = sourceSchema;
6630
7236
  if (q.grouping) {
6631
7237
  if (!capabilities.groupedRows) diagnostics.push("This server does not support grouped rows.");
7238
+ else if (q.grouping.keys.some(isCalendarGroupKey) && !capabilities.calendarGroupKeys)
7239
+ diagnostics.push("This server does not support grouping keys by calendar interval.");
6632
7240
  try {
6633
7241
  const model = createGroupingModel(sourceSchema);
6634
7242
  model.validate(q.grouping);
@@ -6656,6 +7264,8 @@ function toQueryRequestV3(query, baseSchema, capabilities, execution, options2 =
6656
7264
  for (const metric of metrics) {
6657
7265
  diagnostics.push(...validateMetric(metric, resultSchema).map((issue) => issue.message));
6658
7266
  if (metric.dateGrouping && !capabilities.calendarGrouping) diagnostics.push("This server does not support calendar grouping.");
7267
+ else if (isMetricCycle(metric.dateGrouping?.interval) && !capabilities.calendarCycles)
7268
+ diagnostics.push("This server does not support hour-of-day or day-of-week grouping.");
6659
7269
  }
6660
7270
  const revisions = /* @__PURE__ */ Object.create(null);
6661
7271
  for (const [id, revision] of Object.entries(execution?.resolvedRevisions ?? {})) {
@@ -6714,6 +7324,13 @@ function inspectBucketQuery(query, bucket, clause, schema) {
6714
7324
  for (const [index, name] of clause.groupBy.entries()) {
6715
7325
  const field = resolveField(fields, name);
6716
7326
  const key = bucket.keys[index] ?? null;
7327
+ if (clause.dateGrouping && index === 0 && key !== null && isMetricCycle(clause.dateGrouping.interval)) {
7328
+ if (!field || field.type !== "datetime" || !isFilterable(field)) return null;
7329
+ const slot = cycleSlotTerms(query, clause, fields, field.name, key);
7330
+ if (!slot) return null;
7331
+ terms.push(...slot);
7332
+ continue;
7333
+ }
6717
7334
  if (clause.dateGrouping && index === 0 && key !== null) {
6718
7335
  if (!field || field.type !== "datetime" || !isFilterable(field)) return null;
6719
7336
  let start;
@@ -6760,7 +7377,54 @@ function inspectBucketQuery(query, bucket, clause, schema) {
6760
7377
  { field: field.name, op: inclusiveMax ? "<=" : "<", value: String(max) }
6761
7378
  );
6762
7379
  }
6763
- return { ...query, where: appendWhereTerms(query.where, terms), offset: 0 };
7380
+ const where = appendWhereTerms(query.where, terms);
7381
+ if (isMetricCycle(clause.dateGrouping?.interval) && where.reduce((n, term) => n + predicatesOf(term).length, 0) > MAX_WHERE_CLAUSES) return null;
7382
+ return { ...query, where, offset: 0 };
7383
+ }
7384
+ function cycleSlotTerms(query, clause, schema, field, key) {
7385
+ const config = clause.dateGrouping, cycle = config.interval;
7386
+ if (!isMetricCycle(cycle) || typeof key !== "number" || !metricCycleSlots(cycle).includes(key)) return null;
7387
+ const now = Date.now();
7388
+ let { lower, upper } = metricDateBounds(clause, query, schema, now);
7389
+ const whole = config.occurrence ? { ...clause, dateGrouping: { ...config, interval: config.occurrence.interval } } : config.partialIntervals === "exclude" ? clause : void 0;
7390
+ if (whole) {
7391
+ const span = wholeIntervalSpan(whole, query, schema, now);
7392
+ lower = span.start ?? -Infinity;
7393
+ upper = span.end;
7394
+ }
7395
+ if (!Number.isFinite(lower) || !Number.isFinite(upper)) return null;
7396
+ const budget = Math.floor(MAX_WHERE_CLAUSES / 2);
7397
+ const ranges = [];
7398
+ const unit = metricCalendarUnit(cycle);
7399
+ for (let start = metricDateFloor(lower, unit, config.timeZone); start < upper; ) {
7400
+ const next = metricDateNext(start, unit, config.timeZone);
7401
+ if (!Number.isFinite(next) || next <= start) return null;
7402
+ if (metricCycleSlot(start, cycle, config.timeZone) === key) {
7403
+ const from = Math.max(start, lower), to = Math.min(next, upper);
7404
+ const last = ranges[ranges.length - 1];
7405
+ if (from < to) {
7406
+ if (last && last[1] === from) last[1] = to;
7407
+ else if (ranges.push([from, to]) > budget) return null;
7408
+ }
7409
+ }
7410
+ start = next;
7411
+ }
7412
+ const iso = (time) => new Date(time).toISOString();
7413
+ if (!ranges.length)
7414
+ return [
7415
+ { field, op: ">=", value: iso(lower) },
7416
+ { field, op: "<", value: iso(lower) }
7417
+ ];
7418
+ const terms = [
7419
+ { field, op: ">=", value: iso(ranges[0][0]) },
7420
+ { field, op: "<", value: iso(ranges[ranges.length - 1][1]) }
7421
+ ];
7422
+ for (let i = 0; i + 1 < ranges.length; i++)
7423
+ terms.push({ any: [
7424
+ { field, op: "<", value: iso(ranges[i][1]) },
7425
+ { field, op: ">=", value: iso(ranges[i + 1][0]) }
7426
+ ] });
7427
+ return terms;
6764
7428
  }
6765
7429
  function groupSourceQuery(query, group, schema) {
6766
7430
  if (!query.grouping) return null;
@@ -6769,6 +7433,119 @@ function groupSourceQuery(query, group, schema) {
6769
7433
  const source = exitGrouping(query);
6770
7434
  return { ...source, where: appendWhereTerms(source.where, terms), offset: 0 };
6771
7435
  }
7436
+
7437
+ // src/attention.ts
7438
+ function validTarget(target) {
7439
+ return typeof target.dataset === "string" && target.dataset.length > 0 && target.dataset.length <= 256 && (target.rowId === void 0 || typeof target.rowId === "string" && target.rowId.length > 0 && target.rowId.length <= 1e3 || typeof target.rowId === "number" && Number.isFinite(target.rowId)) && (target.field === void 0 || target.rowId !== void 0 && typeof target.field === "string" && target.field.length > 0 && target.field.length <= 256) && (target.path === void 0 || target.field !== void 0 && isJsonPath(target.path));
7440
+ }
7441
+ function attentionTargetKey(target) {
7442
+ return JSON.stringify([target.dataset, target.rowId ?? null, target.field ?? null, target.path ?? null]);
7443
+ }
7444
+ var cancelled = () => new Error("Attention navigation cancelled.");
7445
+ function abortable(promise, signal) {
7446
+ return new Promise((resolve, reject) => {
7447
+ const abort = () => {
7448
+ cleanup();
7449
+ reject(cancelled());
7450
+ };
7451
+ const cleanup = () => signal.removeEventListener("abort", abort);
7452
+ if (signal.aborted) {
7453
+ reject(cancelled());
7454
+ return;
7455
+ }
7456
+ signal.addEventListener("abort", abort, { once: true });
7457
+ promise.then((value) => {
7458
+ cleanup();
7459
+ resolve(value);
7460
+ }, (error) => {
7461
+ cleanup();
7462
+ reject(error);
7463
+ });
7464
+ });
7465
+ }
7466
+ function createAttentionRegistry() {
7467
+ const providers = /* @__PURE__ */ new Map();
7468
+ const listeners = /* @__PURE__ */ new Set();
7469
+ let current;
7470
+ const lookup = (target) => providers.get(attentionTargetKey(target))?.slice(-1)[0];
7471
+ return {
7472
+ cancel() {
7473
+ current?.abort();
7474
+ current = void 0;
7475
+ },
7476
+ register(target, provider) {
7477
+ if (!validTarget(target)) throw Error("Invalid semantic attention target.");
7478
+ const key = attentionTargetKey(target);
7479
+ const entries = providers.get(key) ?? [];
7480
+ providers.set(key, [...entries, provider]);
7481
+ listeners.forEach((listener) => listener());
7482
+ return () => {
7483
+ const remaining = providers.get(key)?.filter((entry) => entry !== provider) ?? [];
7484
+ if (remaining.length) providers.set(key, remaining);
7485
+ else providers.delete(key);
7486
+ };
7487
+ },
7488
+ whenRegistered(target, signal, except) {
7489
+ return new Promise((resolve, reject) => {
7490
+ const cleanup = () => {
7491
+ listeners.delete(check);
7492
+ signal.removeEventListener("abort", abort);
7493
+ };
7494
+ const abort = () => {
7495
+ cleanup();
7496
+ reject(cancelled());
7497
+ };
7498
+ const check = () => {
7499
+ const provider = lookup(target);
7500
+ if (provider && provider !== except) {
7501
+ cleanup();
7502
+ resolve(provider);
7503
+ }
7504
+ };
7505
+ if (signal.aborted) {
7506
+ reject(cancelled());
7507
+ return;
7508
+ }
7509
+ listeners.add(check);
7510
+ signal.addEventListener("abort", abort, { once: true });
7511
+ check();
7512
+ });
7513
+ },
7514
+ async resolve(target, options2) {
7515
+ current?.abort();
7516
+ const controller = new AbortController();
7517
+ current = controller;
7518
+ const signal = controller.signal;
7519
+ const abort = () => controller.abort();
7520
+ options2?.signal?.addEventListener("abort", abort, { once: true });
7521
+ if (options2?.signal?.aborted) controller.abort();
7522
+ const result = (status) => ({ status, target });
7523
+ try {
7524
+ if (signal.aborted) return result("cancelled");
7525
+ if (!validTarget(target)) return result("unavailable");
7526
+ const candidates = [
7527
+ target,
7528
+ { dataset: target.dataset, ...target.rowId !== void 0 ? { rowId: target.rowId } : {}, ...target.field !== void 0 ? { field: target.field } : {} },
7529
+ { dataset: target.dataset, ...target.rowId !== void 0 ? { rowId: target.rowId } : {} },
7530
+ { dataset: target.dataset }
7531
+ ];
7532
+ const provider = candidates.map(lookup).find(Boolean);
7533
+ if (!provider) return result("unavailable");
7534
+ if (provider.ready) await abortable(provider.ready(signal), signal);
7535
+ if (provider.materialize && !await abortable(Promise.resolve(provider.materialize(target, signal)), signal)) return result("unavailable");
7536
+ if (signal.aborted) return result("cancelled");
7537
+ const revealed = await abortable(Promise.resolve(provider.reveal(target, signal)), signal);
7538
+ return result(signal.aborted ? "cancelled" : revealed ? "revealed" : "unavailable");
7539
+ } catch (error) {
7540
+ if (signal.aborted) return result("cancelled");
7541
+ throw error;
7542
+ } finally {
7543
+ options2?.signal?.removeEventListener("abort", abort);
7544
+ if (current === controller) current = void 0;
7545
+ }
7546
+ }
7547
+ };
7548
+ }
6772
7549
  export {
6773
7550
  AGG_OPS_BY_TYPE,
6774
7551
  AGG_OPS_NEEDING_FIELD,
@@ -6777,10 +7554,17 @@ export {
6777
7554
  FORMULA_FUNCTIONS,
6778
7555
  FormulaError,
6779
7556
  GROUP_ID_FIELD,
7557
+ GROUP_KEY_INTERVALS,
6780
7558
  GROUP_LINK_RENDER,
6781
7559
  ITEM_DETAILS_COLUMN,
6782
7560
  MAX_AGGREGATIONS,
6783
7561
  MAX_GROUP_BY_FIELDS,
7562
+ MAX_HOST_QUERY_PARAMS,
7563
+ MAX_HOST_QUERY_STATE_BYTES,
7564
+ MAX_HOST_QUERY_VALUES,
7565
+ MAX_HOST_QUERY_VALUE_LENGTH,
7566
+ MAX_JSON_INSPECTIONS,
7567
+ MAX_JSON_PATHS,
6784
7568
  MAX_METRIC_WINDOW_PERIODS,
6785
7569
  MAX_ORDER_BY_TERMS,
6786
7570
  MAX_PROPERTIES,
@@ -6794,6 +7578,7 @@ export {
6794
7578
  MAX_SELECT_COLUMNS,
6795
7579
  MAX_SUMMARY_COLUMNS,
6796
7580
  MAX_WHERE_CLAUSES,
7581
+ METRIC_CYCLE_INTERVALS,
6797
7582
  METRIC_DATE_INTERVALS,
6798
7583
  METRIC_FUNCTIONS,
6799
7584
  NULLARY_OPS,
@@ -6807,12 +7592,16 @@ export {
6807
7592
  applyAggregations,
6808
7593
  applyPatch,
6809
7594
  applyQuery,
7595
+ attentionTargetKey,
6810
7596
  boxSummary,
7597
+ branchesOf,
7598
+ calendarGroupKey,
6811
7599
  coerceValue,
6812
7600
  compareMetricValues,
6813
7601
  compileFormula,
6814
7602
  compileMetric,
6815
7603
  computedFieldName,
7604
+ createAttentionRegistry,
6816
7605
  createGroupingModel,
6817
7606
  createPropertyView,
6818
7607
  createQueryLinkService,
@@ -6826,6 +7615,9 @@ export {
6826
7615
  exitGrouping,
6827
7616
  filterValues,
6828
7617
  formulaRuntime,
7618
+ groupKeyField,
7619
+ groupKeyFields,
7620
+ groupKeyTerms,
6829
7621
  groupLinkQuery,
6830
7622
  groupPreview,
6831
7623
  groupSourceQuery,
@@ -6837,19 +7629,29 @@ export {
6837
7629
  httpQuerySnapshotStore,
6838
7630
  indexFields,
6839
7631
  inspectBucketQuery,
7632
+ isCalendarGroupKey,
6840
7633
  isComputedCellError,
6841
7634
  isComputedField,
6842
7635
  isFilterable,
6843
7636
  isGroupLinkField,
6844
7637
  isGroupLinkValue,
6845
7638
  isGroupable,
7639
+ isGroupingTimeZone,
7640
+ isJsonBucketId,
7641
+ isJsonPath,
6846
7642
  isMeasurable,
7643
+ isMetricCycle,
6847
7644
  isNegativePredicate,
6848
7645
  isOrGroup,
7646
+ isOrOfAndsGroup,
7647
+ isPropertyFieldLoaded,
6849
7648
  isPushdownFilter,
6850
7649
  isSelectable,
6851
7650
  isSortable,
7651
+ isWhereConjunction,
7652
+ isWhereGroup,
6852
7653
  itemPageFields,
7654
+ jsonInspectionScopeKey,
6853
7655
  jsonPropertyViewStorage,
6854
7656
  loadSchema,
6855
7657
  localPropertyViewStorage,
@@ -6863,14 +7665,23 @@ export {
6863
7665
  memoryQuerySnapshotCache,
6864
7666
  memoryStorageAdapter,
6865
7667
  metricAggregatePlans,
7668
+ metricCalendarUnit,
6866
7669
  metricComputationKey,
6867
7670
  metricCustomEndTime,
7671
+ metricCycleSlot,
7672
+ metricCycleSlots,
7673
+ metricDateBounds,
6868
7674
  metricDateFloor,
6869
7675
  metricDateNext,
6870
7676
  metricDateRange,
6871
7677
  metricDateTime,
6872
7678
  metricDependencies,
6873
7679
  metricExpression,
7680
+ metricMinCount,
7681
+ metricMinCountKeeps,
7682
+ metricMinCountMeasure,
7683
+ metricMinCountPlan,
7684
+ metricMinCountText,
6874
7685
  metricPlans,
6875
7686
  metricPopulationError,
6876
7687
  negateClause,
@@ -6878,9 +7689,13 @@ export {
6878
7689
  normalizePropertyView,
6879
7690
  normalizeQueryState,
6880
7691
  objectReferenceKey,
7692
+ occurrenceDistribution,
6881
7693
  opAllowedForType,
6882
7694
  opPairsForField,
6883
7695
  opsForField,
7696
+ parseHostQueryState,
7697
+ parseJsonInspectionState,
7698
+ parseJsonInspections,
6884
7699
  parseRelativeDuration,
6885
7700
  percentile,
6886
7701
  planMetricSeries,