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

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
@@ -451,16 +451,29 @@ var METRIC_OPTION_SPECS = {
451
451
  expressionY: "string",
452
452
  scope: ["allMatching", "shownRows"],
453
453
  groupLimit: "integer",
454
- minCount: "positiveInteger",
454
+ minCount: "minCount",
455
455
  share: ["total", "group0"]
456
456
  },
457
+ minCount: {
458
+ min: "positiveInteger",
459
+ distinct: "string",
460
+ sum: "string"
461
+ },
457
462
  window: {
458
463
  reduce: ["total", "mean"],
459
464
  partial: ["omit", "include"]
460
465
  },
461
466
  dateGrouping: {
462
467
  field: "string",
463
- interval: ["hour", "day", "week", "month", "year"],
468
+ interval: [
469
+ "hour",
470
+ "day",
471
+ "week",
472
+ "month",
473
+ "year",
474
+ "hourOfDay",
475
+ "dayOfWeek"
476
+ ],
464
477
  timeZone: "string",
465
478
  includeEmpty: "boolean",
466
479
  range: ["query", "observed", "custom"],
@@ -468,6 +481,10 @@ var METRIC_OPTION_SPECS = {
468
481
  end: "string",
469
482
  partialIntervals: ["include", "exclude"]
470
483
  },
484
+ occurrence: {
485
+ interval: ["hour", "day"],
486
+ includeEmpty: "boolean"
487
+ },
471
488
  display: {
472
489
  kind: [
473
490
  "auto",
@@ -548,6 +565,14 @@ function normalizeMetricOptions(raw) {
548
565
  for (const [key, spec] of Object.entries(specs)) {
549
566
  const v = input[key];
550
567
  if (v === void 0) continue;
568
+ if (spec === "minCount") {
569
+ const m = record2(v) ? copy(v, METRIC_OPTION_SPECS.minCount) : v;
570
+ const named = (x) => x === void 0 || typeof x === "string" && !!x && x.length <= 256;
571
+ if (record2(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)
572
+ result[key] = m;
573
+ else diagnostics.push(`Invalid metric ${key}.`);
574
+ continue;
575
+ }
551
576
  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
577
  result[key] = v;
553
578
  else diagnostics.push(`Invalid metric ${key}.`);
@@ -573,6 +598,14 @@ function normalizeMetricOptions(raw) {
573
598
  const d = copy(raw.dateGrouping, METRIC_OPTION_SPECS.dateGrouping);
574
599
  if (typeof d.field !== "string" || !d.field || d.field.length > 256 || !d.interval || typeof d.includeEmpty !== "boolean" || !d.range)
575
600
  diagnostics.push("Invalid metric date grouping.");
601
+ const occurrence = raw.dateGrouping.occurrence;
602
+ if (record2(occurrence)) {
603
+ const o = copy(occurrence, METRIC_OPTION_SPECS.occurrence);
604
+ if (!o.interval || typeof o.includeEmpty !== "boolean")
605
+ diagnostics.push("Invalid metric date grouping occurrence.");
606
+ d.occurrence = o;
607
+ } else if (occurrence !== void 0)
608
+ diagnostics.push("Invalid metric date grouping occurrence.");
576
609
  out.dateGrouping = d;
577
610
  } else diagnostics.push("Invalid metric date grouping.");
578
611
  }
@@ -634,14 +667,472 @@ function normalizeMetricOptions(raw) {
634
667
  return out;
635
668
  }
636
669
 
670
+ // src/relativeTime.ts
671
+ var UNIT_MS = { ms: 1, s: 1e3, m: 6e4, h: 36e5, d: 864e5, w: 6048e5 };
672
+ var MAX_RELATIVE_TIME_MS = 864e13;
673
+ function parseRelativeDuration(value) {
674
+ const valid = /^[+-](?:[0-9]+(?:ms|s|m|h|d|w))+$/.exec(value);
675
+ if (!valid || valid[0] !== value) return null;
676
+ const body = value.slice(1);
677
+ const components = /([0-9]+)(ms|s|m|h|d|w)/g;
678
+ let total = 0;
679
+ for (const match of body.matchAll(components)) {
680
+ const amount = Number(match[1]);
681
+ const unit = UNIT_MS[match[2]];
682
+ if (!Number.isSafeInteger(amount) || amount > Math.floor((MAX_RELATIVE_TIME_MS - total) / unit)) return null;
683
+ total += amount * unit;
684
+ }
685
+ return value[0] === "-" && total !== 0 ? -total : total;
686
+ }
687
+ function evaluationTime(options2) {
688
+ const now = options2.now ?? Date.now();
689
+ if (!Number.isSafeInteger(now) || Math.abs(now) > MAX_RELATIVE_TIME_MS) throw new Error("Invalid query evaluation time");
690
+ return now;
691
+ }
692
+ function relativeTimestamp(now, offsetMs) {
693
+ const result = now + offsetMs;
694
+ if (!Number.isSafeInteger(result) || Math.abs(result) > MAX_RELATIVE_TIME_MS) throw new Error("Relative time exceeds datetime limits");
695
+ return result;
696
+ }
697
+
698
+ // src/ops.ts
699
+ var NULLITY = ["is_null", "is_not_null"];
700
+ var COMPLEMENT_OP = {
701
+ "=": "!=",
702
+ "!=": "=",
703
+ ">": "<=",
704
+ "<=": ">",
705
+ ">=": "<",
706
+ "<": ">=",
707
+ is_null: "is_not_null",
708
+ is_not_null: "is_null",
709
+ matches_regex: "not_matches_regex",
710
+ not_matches_regex: "matches_regex"
711
+ };
712
+ function negateClause(clause, allowedOps) {
713
+ if (clause.value === "0" && !clause.negated) {
714
+ const zeroComplement = clause.op === "length_gt" ? "length_eq" : clause.op === "length_eq" ? "length_gt" : null;
715
+ if (zeroComplement && (!allowedOps || allowedOps.includes(zeroComplement))) {
716
+ return { field: clause.field, op: zeroComplement, value: "0" };
717
+ }
718
+ }
719
+ const complement = COMPLEMENT_OP[clause.op];
720
+ if (complement && (!allowedOps || allowedOps.includes(complement))) {
721
+ const next2 = { field: clause.field, op: complement, value: clause.value };
722
+ return next2;
723
+ }
724
+ const next = { field: clause.field, op: clause.op, value: clause.value };
725
+ if (!clause.negated) next.negated = true;
726
+ return next;
727
+ }
728
+ function isNegativePredicate(clause) {
729
+ return Boolean(clause.negated) || clause.op === "!=" || clause.op === "not_matches_regex" || clause.op === "is_not_null";
730
+ }
731
+ var POSITIVE_OP_ORDER = ["=", ">", ">=", "contains", "starts_with", "ends_with", "length_gt", "length_lt", "length_eq", "matches_regex", "includes"];
732
+ function opPairsForField(field) {
733
+ const ops = opsForField(field);
734
+ const allow = new Set(ops);
735
+ const pairs = [];
736
+ for (const op of POSITIVE_OP_ORDER) {
737
+ if (!allow.has(op)) continue;
738
+ const neg = negateClause({ field: "", op, value: "" }, ops);
739
+ pairs.push({ keep: { op, negated: false }, exclude: { op: neg.op, negated: Boolean(neg.negated) } });
740
+ }
741
+ if (allow.has("is_null") || allow.has("is_not_null")) {
742
+ pairs.push({ keep: { op: "is_null", negated: false }, exclude: { op: "is_not_null", negated: false } });
743
+ }
744
+ return pairs;
745
+ }
746
+ var OPS_BY_TYPE = {
747
+ text: [
748
+ "=",
749
+ "!=",
750
+ "contains",
751
+ "starts_with",
752
+ "ends_with",
753
+ "matches_regex",
754
+ "not_matches_regex",
755
+ "length_gt",
756
+ "length_lt",
757
+ "length_eq",
758
+ ...NULLITY
759
+ ],
760
+ enum: ["=", "!=", ...NULLITY],
761
+ number: ["=", "!=", ">", ">=", "<", "<=", ...NULLITY],
762
+ datetime: ["=", "!=", ">", ">=", "<", "<=", ...NULLITY],
763
+ bool: ["=", "!=", ...NULLITY],
764
+ textarray: ["includes", ...NULLITY]
765
+ };
766
+ var NULLARY_OPS = new Set(NULLITY);
767
+ function opsForField(field) {
768
+ return field.filter?.ops ?? OPS_BY_TYPE[field.type];
769
+ }
770
+ function opAllowedForType(type, op) {
771
+ return OPS_BY_TYPE[type].includes(op);
772
+ }
773
+ function coerceValue(type, raw, now) {
774
+ switch (type) {
775
+ case "number": {
776
+ const n = Number(raw);
777
+ if (raw.trim() === "" || Number.isNaN(n)) throw new Error(`not a number: ${JSON.stringify(raw)}`);
778
+ return n;
779
+ }
780
+ case "bool": {
781
+ const v = raw.toLowerCase();
782
+ if (v === "true" || v === "1" || v === "t") return true;
783
+ if (v === "false" || v === "0" || v === "f") return false;
784
+ throw new Error(`not a bool: ${JSON.stringify(raw)}`);
785
+ }
786
+ case "datetime": {
787
+ if (raw.startsWith("+") || raw.startsWith("-")) {
788
+ const offset = parseRelativeDuration(raw);
789
+ if (offset === null) throw new Error(`not a signed duration: ${JSON.stringify(raw)}`);
790
+ return relativeTimestamp(now ?? Date.now(), offset);
791
+ }
792
+ const ms = Date.parse(raw);
793
+ if (Number.isNaN(ms)) throw new Error(`not a datetime: ${JSON.stringify(raw)}`);
794
+ return ms;
795
+ }
796
+ default:
797
+ return raw;
798
+ }
799
+ }
800
+
801
+ // src/metricDates.ts
802
+ import { Temporal } from "temporal-polyfill";
803
+ var METRIC_DATE_INTERVALS = [
804
+ "hour",
805
+ "day",
806
+ "week",
807
+ "month",
808
+ "year",
809
+ "hourOfDay",
810
+ "dayOfWeek"
811
+ ];
812
+ var METRIC_CYCLE_INTERVALS = ["hourOfDay", "dayOfWeek"];
813
+ function isMetricCycle(interval) {
814
+ return interval === "hourOfDay" || interval === "dayOfWeek";
815
+ }
816
+ function metricCycleSlots(interval) {
817
+ return interval === "hourOfDay" ? Array.from({ length: 24 }, (_, i) => i) : [1, 2, 3, 4, 5, 6, 7];
818
+ }
819
+ function metricCalendarUnit(interval) {
820
+ return interval === "hourOfDay" ? "hour" : interval === "dayOfWeek" ? "day" : interval;
821
+ }
822
+ function metricCycleSlot(time, interval, timeZone = "UTC") {
823
+ if (timeZone === "UTC") {
824
+ const date = new Date(time);
825
+ return interval === "hourOfDay" ? date.getUTCHours() : (date.getUTCDay() + 6) % 7 + 1;
826
+ }
827
+ const zoned = Temporal.Instant.fromEpochMilliseconds(time).toZonedDateTimeISO(timeZone);
828
+ return interval === "hourOfDay" ? zoned.hour : zoned.dayOfWeek;
829
+ }
830
+ function metricDateTime(value, timeZone = "UTC") {
831
+ 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(
832
+ value
833
+ ))
834
+ throw Error(
835
+ "Date grouping requires ISO dates or timestamps with a timezone."
836
+ );
837
+ const time = Date.parse(value), month = Number(value.slice(5, 7)), day = Number(value.slice(8, 10));
838
+ const calendar = /* @__PURE__ */ new Date(0);
839
+ calendar.setUTCFullYear(Number(value.slice(0, 4)), month, 0);
840
+ if (!Number.isFinite(time) || month < 1 || month > 12 || day < 1 || day > calendar.getUTCDate())
841
+ throw Error("Invalid date grouping value.");
842
+ return value.length === 10 && timeZone !== "UTC" ? Temporal.PlainDate.from(value).toZonedDateTime(timeZone).epochMilliseconds : time;
843
+ }
844
+ function metricOffsetTransition(start, end, timeZone) {
845
+ if (end <= start) return void 0;
846
+ const offset = (time) => Temporal.Instant.fromEpochMilliseconds(time).toZonedDateTimeISO(timeZone).offsetNanoseconds;
847
+ const before = offset(start);
848
+ if (before === offset(end)) return void 0;
849
+ while (end - start > 1) {
850
+ const middle = Math.floor((start + end) / 2);
851
+ if (offset(middle) === before) start = middle;
852
+ else end = middle;
853
+ }
854
+ return end;
855
+ }
856
+ function metricDateFloor(time, cycleOrInterval, timeZone = "UTC") {
857
+ const interval = metricCalendarUnit(cycleOrInterval);
858
+ if (timeZone !== "UTC") {
859
+ const zoned = Temporal.Instant.fromEpochMilliseconds(time).toZonedDateTimeISO(timeZone);
860
+ if (interval === "hour") {
861
+ const floor = time - zoned.minute * 6e4 - zoned.second * 1e3 - zoned.millisecond;
862
+ return metricOffsetTransition(floor, time, timeZone) ?? floor;
863
+ }
864
+ let date2 = zoned.toPlainDate();
865
+ if (interval === "week") date2 = date2.subtract({ days: date2.dayOfWeek - 1 });
866
+ if (interval === "month" || interval === "year")
867
+ date2 = date2.with({ day: 1 });
868
+ if (interval === "year") date2 = date2.with({ month: 1 });
869
+ return date2.toZonedDateTime(timeZone).epochMilliseconds;
870
+ }
871
+ const date = new Date(time);
872
+ date.setUTCMinutes(0, 0, 0);
873
+ if (interval !== "hour") date.setUTCHours(0);
874
+ if (interval === "week")
875
+ date.setUTCDate(date.getUTCDate() - (date.getUTCDay() + 6) % 7);
876
+ if (interval === "month" || interval === "year") date.setUTCDate(1);
877
+ if (interval === "year") date.setUTCMonth(0);
878
+ return date.getTime();
879
+ }
880
+ function metricDateNext(time, cycleOrInterval, timeZone = "UTC") {
881
+ const interval = metricCalendarUnit(cycleOrInterval);
882
+ if (timeZone !== "UTC") {
883
+ const zoned = Temporal.Instant.fromEpochMilliseconds(time).toZonedDateTimeISO(timeZone);
884
+ if (interval === "hour") {
885
+ const target = zoned.toPlainDateTime().with({
886
+ minute: 0,
887
+ second: 0,
888
+ millisecond: 0,
889
+ microsecond: 0,
890
+ nanosecond: 0
891
+ }).add({ hours: 1 });
892
+ let next = target.toZonedDateTime(timeZone).epochMilliseconds;
893
+ if (next <= time)
894
+ next = target.toZonedDateTime(timeZone, {
895
+ disambiguation: "later"
896
+ }).epochMilliseconds;
897
+ return metricOffsetTransition(time, next, timeZone) ?? next;
898
+ }
899
+ let date2 = zoned.toPlainDate();
900
+ if (interval === "month") date2 = date2.with({ day: 1 }).add({ months: 1 });
901
+ else if (interval === "year")
902
+ date2 = date2.with({ month: 1, day: 1 }).add({ years: 1 });
903
+ else if (interval === "week")
904
+ date2 = date2.subtract({ days: date2.dayOfWeek - 1 }).add({ weeks: 1 });
905
+ else date2 = date2.add({ days: 1 });
906
+ return date2.toZonedDateTime(timeZone).epochMilliseconds;
907
+ }
908
+ const date = new Date(time);
909
+ if (interval === "hour") date.setUTCHours(date.getUTCHours() + 1);
910
+ else if (interval === "month") date.setUTCMonth(date.getUTCMonth() + 1);
911
+ else if (interval === "year") date.setUTCFullYear(date.getUTCFullYear() + 1);
912
+ else date.setUTCDate(date.getUTCDate() + (interval === "week" ? 7 : 1));
913
+ return date.getTime();
914
+ }
915
+ function metricCustomEndTime(value, timeZone = "UTC") {
916
+ const time = metricDateTime(value, timeZone);
917
+ return value.length === 10 ? metricDateNext(time, "day", timeZone) - 1 : time;
918
+ }
919
+ function isGroupingTimeZone(value) {
920
+ if (typeof value !== "string" || !value || value.length > 80 || /^[+-]/.test(value))
921
+ return false;
922
+ try {
923
+ new Intl.DateTimeFormat("en-US", { timeZone: value });
924
+ return true;
925
+ } catch {
926
+ return false;
927
+ }
928
+ }
929
+ function validateDateGrouping(config) {
930
+ 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))
931
+ throw Error("Invalid date grouping definition.");
932
+ if (config.timeZone !== void 0 && !isGroupingTimeZone(config.timeZone))
933
+ throw Error(
934
+ "Use a valid IANA grouping timezone, such as America/Denver or UTC."
935
+ );
936
+ if (config.occurrence !== void 0) {
937
+ const o = config.occurrence;
938
+ if (!o || typeof o !== "object" || Array.isArray(o) || !["hour", "day"].includes(o.interval) || typeof o.includeEmpty !== "boolean")
939
+ throw Error("Invalid date grouping occurrence.");
940
+ if (!isMetricCycle(config.interval))
941
+ throw Error(
942
+ "Occurrences summarise an hour-of-day or day-of-week slot; calendar intervals are already one occurrence each."
943
+ );
944
+ if (config.interval === "hourOfDay" && o.interval !== "hour")
945
+ throw Error("An hour-of-day slot occurs once per hour: use hourly occurrences.");
946
+ if (config.partialIntervals === "include")
947
+ throw Error(
948
+ "Occurrences are always whole: a partial day or hour would bias the typical value. Leave out partialIntervals."
949
+ );
950
+ }
951
+ if (config.range === "custom" && (isMetricCycle(config.interval) ? config.occurrence?.includeEmpty : config.includeEmpty)) {
952
+ const start = metricDateTime(config.start ?? "", config.timeZone), end = metricCustomEndTime(config.end ?? "", config.timeZone);
953
+ if (end < start)
954
+ throw Error("The end date must be on or after the start date.");
955
+ }
956
+ }
957
+ function metricDateRange(config, query, observed, now, canonical) {
958
+ let min = Infinity, max = -Infinity;
959
+ for (const time of observed) {
960
+ min = Math.min(min, time);
961
+ max = Math.max(max, time);
962
+ }
963
+ if (config.range === "observed")
964
+ return observed.length ? [min, max] : void 0;
965
+ if (config.range === "custom") {
966
+ const start2 = metricDateFloor(
967
+ metricDateTime(config.start ?? "", config.timeZone),
968
+ config.interval,
969
+ config.timeZone
970
+ );
971
+ const end2 = metricDateFloor(
972
+ metricCustomEndTime(config.end ?? "", config.timeZone),
973
+ config.interval,
974
+ config.timeZone
975
+ );
976
+ if (observed.length && (start2 > min || end2 < max))
977
+ throw Error(
978
+ "Custom date bounds must include all observed buckets. Narrow the query filters to exclude data."
979
+ );
980
+ return [start2, end2];
981
+ }
982
+ let start = -Infinity, end = Infinity;
983
+ for (const term of query.where) {
984
+ if (isWhereGroup(term) || term.negated || canonical(term.field) !== canonical(config.field))
985
+ continue;
986
+ if (!["=", ">", ">=", "<", "<="].includes(term.op)) continue;
987
+ const time = Number(coerceValue("datetime", term.value, now));
988
+ if (term.op === ">" || term.op === ">=" || term.op === "=")
989
+ start = Math.max(start, time + (term.op === ">" ? 1 : 0));
990
+ if (term.op === "<" || term.op === "<=" || term.op === "=")
991
+ end = Math.min(end, time - (term.op === "<" ? 1 : 0));
992
+ }
993
+ if (!Number.isFinite(start) || !Number.isFinite(end)) {
994
+ const missing = [
995
+ ...Number.isFinite(start) ? [] : ["a lower bound (>= or >)"],
996
+ ...Number.isFinite(end) ? [] : ["an upper bound (<= or <)"]
997
+ ].join(" and ");
998
+ throw Error(
999
+ `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.`
1000
+ );
1001
+ }
1002
+ if (end < start) return void 0;
1003
+ return [
1004
+ metricDateFloor(start, config.interval, config.timeZone),
1005
+ metricDateFloor(end, config.interval, config.timeZone)
1006
+ ];
1007
+ }
1008
+
1009
+ // src/groupKeys.ts
1010
+ var GROUP_KEY_INTERVALS = ["hour", "day", "week", "month", "year"];
1011
+ function isCalendarGroupKey(key) {
1012
+ return typeof key === "object" && key !== null;
1013
+ }
1014
+ function groupKeyField(key) {
1015
+ return isCalendarGroupKey(key) ? key.field : key;
1016
+ }
1017
+ function groupKeyFields(keys) {
1018
+ return keys.map(groupKeyField);
1019
+ }
1020
+ function calendarGroupKey(keys, field) {
1021
+ return keys.find((key) => isCalendarGroupKey(key) && key.field === field);
1022
+ }
1023
+ function calendarKeyReader(key) {
1024
+ let start = Infinity, end = -Infinity, iso = "";
1025
+ return (value) => {
1026
+ if (value == null) return null;
1027
+ const input = value instanceof Date ? value.toISOString() : value;
1028
+ if (typeof input !== "string")
1029
+ throw Error(`Calendar group key ${key.field} needs ISO dates or timestamps with a timezone.`);
1030
+ const time = metricDateTime(input, key.timeZone);
1031
+ if (time < start || time >= end) {
1032
+ start = metricDateFloor(time, key.interval, key.timeZone);
1033
+ end = metricDateNext(start, key.interval, key.timeZone);
1034
+ iso = new Date(start).toISOString();
1035
+ }
1036
+ return iso;
1037
+ };
1038
+ }
1039
+ function datetimeValue(value) {
1040
+ const time = value instanceof Date ? value.getTime() : typeof value === "number" ? value : typeof value === "string" && value !== "" ? Date.parse(value) : NaN;
1041
+ return Number.isFinite(time) ? new Date(time).toISOString() : null;
1042
+ }
1043
+ function keyClause(field, value) {
1044
+ if (!field || field.type === "textarray" || !isFilterable(field)) return null;
1045
+ if (value == null) return { field: field.name, op: "is_null", value: "" };
1046
+ if (field.type === "datetime") {
1047
+ const iso = datetimeValue(value);
1048
+ return iso === null ? null : { field: field.name, op: "=", value: iso };
1049
+ }
1050
+ if (typeof value === "number" ? !Number.isFinite(value) : typeof value !== "string" && typeof value !== "boolean")
1051
+ return null;
1052
+ return { field: field.name, op: "=", value: String(value) };
1053
+ }
1054
+ function calendarKeyClauses(field, key, value) {
1055
+ if (!field || field.type !== "datetime" || !isFilterable(field)) return null;
1056
+ if (value == null) return [{ field: field.name, op: "is_null", value: "" }];
1057
+ const iso = datetimeValue(value);
1058
+ if (iso === null) return null;
1059
+ const start = Date.parse(iso), end = metricDateNext(start, key.interval, key.timeZone);
1060
+ if (!Number.isFinite(end) || end <= start) return null;
1061
+ return [
1062
+ { field: field.name, op: ">=", value: iso },
1063
+ { field: field.name, op: "<", value: new Date(end).toISOString() }
1064
+ ];
1065
+ }
1066
+ function groupKeyTerms(keys, group, schema) {
1067
+ const terms = [];
1068
+ for (const key of keys) {
1069
+ const name = groupKeyField(key);
1070
+ const field = resolveField(schema, name);
1071
+ if (isCalendarGroupKey(key)) {
1072
+ const range = calendarKeyClauses(field, key, group[name]);
1073
+ if (!range) return null;
1074
+ terms.push(...range);
1075
+ continue;
1076
+ }
1077
+ const term = keyClause(field, group[name]);
1078
+ if (!term) return null;
1079
+ terms.push(term);
1080
+ }
1081
+ return terms;
1082
+ }
1083
+
637
1084
  // src/query.ts
1085
+ function isWhereConjunction(branch) {
1086
+ return branch != null && Array.isArray(branch.all);
1087
+ }
638
1088
  function isOrGroup(term) {
1089
+ const any = term.any;
1090
+ return Array.isArray(any) && !any.some((member) => isWhereConjunction(member));
1091
+ }
1092
+ function isOrOfAndsGroup(term) {
1093
+ const any = term.any;
1094
+ return Array.isArray(any) && any.some(isWhereConjunction);
1095
+ }
1096
+ function isWhereGroup(term) {
639
1097
  return Array.isArray(term.any);
640
1098
  }
641
1099
  function predicatesOf(term) {
642
- return isOrGroup(term) ? term.any : [term];
1100
+ if (!isWhereGroup(term)) return [term];
1101
+ return term.any.flatMap((member) => isWhereConjunction(member) ? member.all : [member]);
1102
+ }
1103
+ function branchesOf(term) {
1104
+ if (!isWhereGroup(term)) return [[term]];
1105
+ return term.any.map((member) => isWhereConjunction(member) ? member.all : [member]);
643
1106
  }
644
1107
  var ITEM_DETAILS_COLUMN = "__qt_item_details__";
1108
+ var MAX_HOST_QUERY_PARAMS = 64;
1109
+ var MAX_HOST_QUERY_VALUES = 64;
1110
+ var MAX_HOST_QUERY_VALUE_LENGTH = 4e3;
1111
+ var MAX_HOST_QUERY_STATE_BYTES = 32 * 1024;
1112
+ var HOST_QUERY_PARAM_NAME = /^[A-Za-z_][A-Za-z0-9_.-]{0,127}$/;
1113
+ function parseHostQueryState(input) {
1114
+ const fail = (why) => {
1115
+ throw Error(`Invalid host query state: ${why}`);
1116
+ };
1117
+ if (!isRecord(input)) return fail("expected an object.");
1118
+ if (Object.keys(input).some((key) => key !== "version" && key !== "params")) fail("unexpected properties.");
1119
+ const version = input.version;
1120
+ if (typeof version !== "number" || !Number.isSafeInteger(version) || version < 1) return fail("unsupported version.");
1121
+ if (!isRecord(input.params)) return fail("params must be an object.");
1122
+ const names = Object.keys(input.params).sort();
1123
+ if (names.length > MAX_HOST_QUERY_PARAMS) fail("too many parameters.");
1124
+ const params = {};
1125
+ for (const name of names) {
1126
+ const values = input.params[name];
1127
+ if (!HOST_QUERY_PARAM_NAME.test(name)) fail("invalid parameter name.");
1128
+ if (!Array.isArray(values) || values.length > MAX_HOST_QUERY_VALUES || values.some((value) => typeof value !== "string" || value.length > MAX_HOST_QUERY_VALUE_LENGTH))
1129
+ fail(`parameter ${name} must be a list of strings.`);
1130
+ params[name] = [...values];
1131
+ }
1132
+ const state = { version, params };
1133
+ if (new TextEncoder().encode(JSON.stringify(state)).length > MAX_HOST_QUERY_STATE_BYTES) fail("payload too large.");
1134
+ return state;
1135
+ }
645
1136
  var EMPTY_QUERY = {
646
1137
  select: [],
647
1138
  where: [],
@@ -704,7 +1195,7 @@ function normalizeWhereClause(item) {
704
1195
  return clause;
705
1196
  }
706
1197
  function cloneWhereTerm(term) {
707
- const copy = isOrGroup(term) ? { ...term, any: term.any.map((c) => ({ ...c })) } : { ...term };
1198
+ const copy = isWhereGroup(term) ? { ...term, any: term.any.map((c) => isWhereConjunction(c) ? { all: c.all.map((m) => ({ ...m })) } : { ...c }) } : { ...term };
708
1199
  if (term.setFilter) copy.setFilter = { ...term.setFilter, values: [...term.setFilter.values] };
709
1200
  return copy;
710
1201
  }
@@ -715,6 +1206,13 @@ function normalizeSetFilter(item) {
715
1206
  if (item.values.length > MAX_WHERE_CLAUSES || !item.values.every((value) => typeof value === "string" && value.length <= MAX_FILTER_VALUE_LENGTH)) return void 0;
716
1207
  return { id, mode: item.mode, values: [...item.values] };
717
1208
  }
1209
+ function suppliedPredicateCount(term) {
1210
+ if (!isRecord(term) || !Array.isArray(term.any)) return 1;
1211
+ let count = 0;
1212
+ for (const member of term.any)
1213
+ count += isRecord(member) && Array.isArray(member.all) ? Math.max(1, member.all.length) : 1;
1214
+ return count;
1215
+ }
718
1216
  function normalizeWhereTerms(input) {
719
1217
  const where = [];
720
1218
  let literalBudget = MAX_WHERE_CLAUSES;
@@ -725,14 +1223,31 @@ function normalizeWhereTerms(input) {
725
1223
  const members = [];
726
1224
  for (const inner of item.any) {
727
1225
  if (literalBudget <= 0) break;
1226
+ if (isRecord(inner) && Array.isArray(inner.all)) {
1227
+ const all = [];
1228
+ for (const literal of inner.all) {
1229
+ if (literalBudget <= 0) break;
1230
+ const clause2 = normalizeWhereClause(literal);
1231
+ if (clause2) {
1232
+ all.push(clause2);
1233
+ literalBudget--;
1234
+ }
1235
+ }
1236
+ if (all.length === 1) members.push(all[0]);
1237
+ else if (all.length > 1) members.push({ all });
1238
+ continue;
1239
+ }
728
1240
  const clause = normalizeWhereClause(inner);
729
1241
  if (clause) {
730
1242
  members.push(clause);
731
1243
  literalBudget--;
732
1244
  }
733
1245
  }
734
- if (members.length === 1) where.push(members[0]);
735
- else if (members.length > 1) where.push({ any: members });
1246
+ if (members.length === 1) {
1247
+ const only = members[0];
1248
+ if (isWhereConjunction(only)) where.push(...only.all);
1249
+ else where.push(only);
1250
+ } else if (members.length > 1) where.push({ any: members });
736
1251
  } else {
737
1252
  const clause = normalizeWhereClause(item);
738
1253
  if (clause) {
@@ -751,7 +1266,7 @@ function normalizeMetricWhere(value) {
751
1266
  if (value === void 0) return { invalid: false };
752
1267
  if (!Array.isArray(value)) return { invalid: true };
753
1268
  const where = normalizeWhereTerms(value);
754
- const supplied = value.reduce((count, term) => count + (isRecord(term) && Array.isArray(term.any) ? term.any.length : 1), 0);
1269
+ const supplied = value.reduce((count, term) => count + suppliedPredicateCount(term), 0);
755
1270
  const kept = where.reduce((count, term) => count + predicatesOf(term).length, 0);
756
1271
  return { ...where.length ? { where } : {}, invalid: supplied !== kept };
757
1272
  }
@@ -808,6 +1323,13 @@ function normalizeQuery(input, fallback, links) {
808
1323
  offset: boundedInteger(raw.offset, fallback.offset, 0, MAX_QUERY_OFFSET)
809
1324
  };
810
1325
  if (raw.fitToScreen === true) out.fitToScreen = true;
1326
+ if (links && raw.hostState !== void 0) {
1327
+ try {
1328
+ out.hostState = parseHostQueryState(raw.hostState);
1329
+ } catch {
1330
+ out.hostState = { version: 0, params: {} };
1331
+ }
1332
+ }
811
1333
  if (raw.grouping != null) out.grouping = normalizeGroupingState(raw.grouping, links);
812
1334
  if (Array.isArray(raw.aggregations)) {
813
1335
  const aggregations = [];
@@ -879,8 +1401,20 @@ function normalizeGroupingState(value, links) {
879
1401
  const fail = (message) => {
880
1402
  diagnostics.push(`Invalid grouping: ${message}`);
881
1403
  };
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)
1404
+ const keys = [];
1405
+ for (const [index, key] of (Array.isArray(raw.keys) ? raw.keys.slice(0, MAX_GROUP_BY_FIELDS) : []).entries()) {
1406
+ if (boundedString(key, MAX_FIELD_NAME_LENGTH)) {
1407
+ keys.push(key);
1408
+ continue;
1409
+ }
1410
+ if (!isRecord(key)) continue;
1411
+ const field = boundedString(key.field, MAX_FIELD_NAME_LENGTH), zone = key.timeZone;
1412
+ if (field && GROUP_KEY_INTERVALS.includes(key.interval) && (zone === void 0 || typeof zone === "string" && zone.length > 0 && zone.length <= 80 && !/^[+-]/.test(zone)))
1413
+ keys.push({ field, interval: key.interval, ...zone !== void 0 ? { timeZone: zone } : {} });
1414
+ 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.`);
1415
+ }
1416
+ const keyNames = groupKeyFields(keys);
1417
+ if (!Array.isArray(raw.keys) || !keys.length || keys.length !== raw.keys.length || new Set(keyNames).size !== keys.length)
884
1418
  fail(`keys must list 1\u2013${MAX_GROUP_BY_FIELDS} distinct field names.`);
885
1419
  const summaries = [];
886
1420
  if (!Array.isArray(raw.summaries) || raw.summaries.length > 64) fail("summaries must be a list of at most 64 columns.");
@@ -897,7 +1431,7 @@ function normalizeGroupingState(value, links) {
897
1431
  }
898
1432
  const column = { id: item.id, op: item.op };
899
1433
  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.`);
1434
+ 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
1435
  summaryIds.add(column.id);
902
1436
  if (item.op === "link") {
903
1437
  const link = normalizeLinkSpec(item.link);
@@ -946,13 +1480,13 @@ function normalizeGroupingState(value, links) {
946
1480
  }
947
1481
  if (!isRecord(raw.source)) fail("source must be the record query that is grouped.");
948
1482
  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 : {};
1483
+ const { grouping: _nested, hostState: _host, ...source } = isRecord(raw.source) ? raw.source : {};
950
1484
  if (Array.isArray(raw.diagnostics))
951
1485
  for (const message of raw.diagnostics.slice(0, 20))
952
1486
  diagnostics.push(typeof message === "string" && message ? message.slice(0, MAX_LABEL_LENGTH) : "Invalid grouping definition.");
953
1487
  const normalizedSource = normalizeQueryState(source);
954
1488
  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);
1489
+ const supplied = source.where.reduce((count, term) => count + suppliedPredicateCount(term), 0);
956
1490
  if (supplied !== normalizedSource.where.reduce((count, term) => count + predicatesOf(term).length, 0)) fail("source filters contain malformed predicates or exceed the predicate budget.");
957
1491
  }
958
1492
  return { keys, summaries, representative, source: normalizedSource, ...diagnostics.length ? { diagnostics: [...new Set(diagnostics)] } : {} };
@@ -997,7 +1531,7 @@ function normalizeLinkSpec(value) {
997
1531
  }
998
1532
  function keptEveryTerm(supplied, kept) {
999
1533
  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);
1534
+ const count = supplied.reduce((total, term) => total + Math.max(1, suppliedPredicateCount(term)), 0);
1001
1535
  return count === kept.reduce((total, term) => total + predicatesOf(term).length, 0);
1002
1536
  }
1003
1537
  function normalizeLinkPatch(value) {
@@ -1024,6 +1558,7 @@ function queriesEqual(a, b) {
1024
1558
  if (!sameArray(a.aggregations ?? [], b.aggregations ?? [])) return false;
1025
1559
  if (JSON.stringify(a.itemDetails) !== JSON.stringify(b.itemDetails)) return false;
1026
1560
  if (JSON.stringify(a.grouping) !== JSON.stringify(b.grouping)) return false;
1561
+ if (JSON.stringify(a.hostState) !== JSON.stringify(b.hostState)) return false;
1027
1562
  return true;
1028
1563
  }
1029
1564
  function sameArray(a, b) {
@@ -1034,137 +1569,6 @@ function sameArray(a, b) {
1034
1569
  return true;
1035
1570
  }
1036
1571
 
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];
1136
- }
1137
- function opAllowedForType(type, op) {
1138
- return OPS_BY_TYPE[type].includes(op);
1139
- }
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;
1165
- }
1166
- }
1167
-
1168
1572
  // src/agg.ts
1169
1573
  var AGG_OPS_BY_TYPE = {
1170
1574
  number: ["count", "count_distinct", "sum", "avg", "min", "max"],
@@ -1231,7 +1635,8 @@ function createSetFilter(field, mode, values, id, target = "array") {
1231
1635
  }
1232
1636
  function stripSetFilter(term) {
1233
1637
  const strip = ({ setFilter: _metadata, ...clause }) => clause;
1234
- return isOrGroup(term) ? { any: term.any.map(strip) } : strip(term);
1638
+ if (!isWhereGroup(term)) return strip(term);
1639
+ return { any: term.any.map((member) => isWhereConjunction(member) ? { all: member.all.map(strip) } : strip(member)) };
1235
1640
  }
1236
1641
  function readSetFilter(where, index) {
1237
1642
  const term = where[index];
@@ -1296,6 +1701,7 @@ function compactQuery(q) {
1296
1701
  if (q.itemDetails) c.d = q.itemDetails;
1297
1702
  if (q.fitToScreen) c.fs = true;
1298
1703
  if (q.grouping) c.b = q.grouping;
1704
+ if (q.hostState) c.h = q.hostState;
1299
1705
  return c;
1300
1706
  }
1301
1707
  var MAX_QUERY_STATE_BYTES = 4 * 1024 * 1024;
@@ -1422,7 +1828,7 @@ function decodeToken(token, strict) {
1422
1828
  const c = versioned ? unpackQuery(raw) : raw;
1423
1829
  if (strict && c.w) {
1424
1830
  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);
1831
+ const count = c.w.reduce((n, term) => n + suppliedPredicateCount(term), 0);
1426
1832
  const normalized = normalizeQueryState({ where: c.w });
1427
1833
  const normalizedCount = normalized.where.reduce((n, term) => n + predicatesOf(term).length, 0);
1428
1834
  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 +1843,16 @@ function decodeToken(token, strict) {
1437
1843
  aggregations: c.g,
1438
1844
  itemDetails: c.d,
1439
1845
  fitToScreen: c.fs,
1440
- grouping: c.b
1846
+ grouping: c.b,
1847
+ hostState: c.h
1441
1848
  });
1849
+ if (strict && c.h !== void 0) {
1850
+ try {
1851
+ parseHostQueryState(c.h);
1852
+ } catch {
1853
+ throw new QueryDecodeError("invalid", "Invalid host state in query link.");
1854
+ }
1855
+ }
1442
1856
  if (strict && c.g && out.aggregations?.length !== c.g.length) throw new QueryDecodeError("invalid", "Invalid query metrics.");
1443
1857
  return out;
1444
1858
  } catch (error) {
@@ -1505,7 +1919,7 @@ function termIsPushdown(term, byName, resolveField2) {
1505
1919
  }
1506
1920
  function toAggregationQuery(q, schema) {
1507
1921
  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);
1922
+ const suppliedCount = q.where.reduce((n, t) => n + predicatesOf(t).length, 0);
1509
1923
  q = normalizeQueryState(q);
1510
1924
  if (q.aggregations?.some((a) => a.dateGrouping)) throw new Error("Date grouping currently requires local metric execution.");
1511
1925
  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 +1937,7 @@ function toAggregationQuery(q, schema) {
1523
1937
  );
1524
1938
  const diagnostics = [];
1525
1939
  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 });
1940
+ 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
1941
  return { where, aggregations, ...diagnostics.length ? { diagnostics } : {} };
1528
1942
  }
1529
1943
 
@@ -3121,176 +3535,6 @@ function formulaRuntime(ast, inputs, extension) {
3121
3535
  }
3122
3536
  }
3123
3537
 
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
- );
3234
- }
3235
- }
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
- );
3286
- }
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
- }
3293
-
3294
3538
  // src/metricPrecision.ts
3295
3539
  function assertMetricNumber(value) {
3296
3540
  if (!Number.isFinite(value) || Number.isInteger(value) && !Number.isSafeInteger(value)) {
@@ -3307,11 +3551,110 @@ function checkedMetricResult(result) {
3307
3551
  }
3308
3552
  }
3309
3553
 
3554
+ // src/metricDistributions.ts
3555
+ function percentile(sorted, p) {
3556
+ if (!Number.isFinite(p) || p < 0 || p > 1)
3557
+ throw Error("Percentile must be between zero and one.");
3558
+ if (sorted.some((v, i) => !Number.isFinite(v) || i > 0 && v < sorted[i - 1]))
3559
+ throw Error("Percentile inputs must be sorted finite numbers.");
3560
+ sorted.forEach(assertMetricNumber);
3561
+ if (!sorted.length) return null;
3562
+ const position = (sorted.length - 1) * p, lower = Math.floor(position), fraction = position - lower;
3563
+ const a = sorted[lower], b = sorted[Math.min(lower + 1, sorted.length - 1)];
3564
+ const raw = (1 - fraction) * a + fraction * b;
3565
+ assertMetricNumber(raw);
3566
+ const value = Math.min(b, Math.max(a, raw));
3567
+ return value;
3568
+ }
3569
+ function boxSummary(values, whiskers = "minmax") {
3570
+ if (values.some((v) => !Number.isFinite(v)))
3571
+ throw Error("Distribution inputs must be finite numbers.");
3572
+ values.forEach(assertMetricNumber);
3573
+ const sorted = [...values].sort((a, b) => a - b), n = sorted.length;
3574
+ if (!n) return null;
3575
+ const min = sorted[0], max = sorted[n - 1], q1 = percentile(sorted, 0.25), median = percentile(sorted, 0.5), q3 = percentile(sorted, 0.75);
3576
+ const lowFence = q1 - 1.5 * (q3 - q1), highFence = q3 + 1.5 * (q3 - q1);
3577
+ const low = whiskers === "tukey" ? sorted.find((v) => v >= lowFence) : min;
3578
+ const high = whiskers === "tukey" ? [...sorted].reverse().find((v) => v <= highFence) : max;
3579
+ const outside = whiskers === "tukey" ? sorted.filter((v) => v < low || v > high) : [];
3580
+ const outliers = outside.length <= 20 ? outside : [...outside.slice(0, 10), ...outside.slice(-10)];
3581
+ const rawMean = sorted.reduce((sum, value) => sum + value / n, 0);
3582
+ assertMetricNumber(rawMean);
3583
+ const mean = Math.min(max, Math.max(min, rawMean));
3584
+ [min, q1, median, q3, max, mean, low, high].forEach(assertMetricNumber);
3585
+ return {
3586
+ n,
3587
+ min,
3588
+ q1,
3589
+ median,
3590
+ q3,
3591
+ max,
3592
+ mean,
3593
+ low,
3594
+ high,
3595
+ outliers,
3596
+ outlierCount: outside.length,
3597
+ whiskers,
3598
+ method: "exact-linear"
3599
+ };
3600
+ }
3601
+ function histogramEdges(values, bins) {
3602
+ if (!Number.isInteger(bins) || bins < 2 || bins > 30)
3603
+ throw Error("Choose 2\u201330 histogram bins.");
3604
+ if (values.some((v) => !Number.isFinite(v)))
3605
+ throw Error("Histogram inputs must be finite numbers.");
3606
+ values.forEach(assertMetricNumber);
3607
+ if (!values.length) return [];
3608
+ let min = Infinity, max = -Infinity;
3609
+ for (const value of values) {
3610
+ min = Math.min(min, value);
3611
+ max = Math.max(max, value);
3612
+ }
3613
+ if (min === max) return [min, max];
3614
+ if (!Number.isFinite(max - min))
3615
+ throw Error("Histogram range exceeds finite numeric precision.");
3616
+ const edges = Array.from(
3617
+ { length: bins + 1 },
3618
+ (_, i) => min + (max - min) * (i / bins)
3619
+ );
3620
+ edges[0] = min;
3621
+ edges[bins] = max;
3622
+ edges.forEach(assertMetricNumber);
3623
+ if (edges.some((value, i) => i > 0 && value <= edges[i - 1]))
3624
+ throw Error(
3625
+ "Histogram bins are finer than numeric precision. Reduce the bin count."
3626
+ );
3627
+ return edges;
3628
+ }
3629
+ function histogramCounts(values, edges) {
3630
+ if (values.some((v) => !Number.isFinite(v)))
3631
+ throw Error("Histogram inputs must be finite numbers.");
3632
+ if (!edges.length) {
3633
+ if (values.length) throw Error("Histogram values require edges.");
3634
+ return [];
3635
+ }
3636
+ if (edges.length < 2 || edges.some(
3637
+ (v, i) => !Number.isFinite(v) || i > 0 && (v < edges[i - 1] || v === edges[i - 1] && edges.length !== 2)
3638
+ ))
3639
+ throw Error("Histogram edges must be ordered finite boundaries.");
3640
+ values.forEach(assertMetricNumber);
3641
+ edges.forEach(assertMetricNumber);
3642
+ const counts = Array(edges.length - 1).fill(0);
3643
+ for (const value of values) {
3644
+ if (value < edges[0] || value > edges[edges.length - 1])
3645
+ throw Error("Value falls outside shared histogram edges.");
3646
+ let bin = 0;
3647
+ while (bin < counts.length - 1 && value >= edges[bin + 1]) bin++;
3648
+ counts[bin] = counts[bin] + 1;
3649
+ }
3650
+ return counts;
3651
+ }
3652
+
3310
3653
  // src/metricSeries.ts
3311
3654
  var MAX_METRIC_WINDOW_PERIODS = 1e3;
3312
3655
  var MERGEABLE = ["SUM", "COUNT", "MIN", "MAX", "AVG"];
3313
3656
  function hasMetricSeries(c) {
3314
- return c.window !== void 0 || c.share !== void 0 || c.dateGrouping?.partialIntervals !== void 0;
3657
+ return c.window !== void 0 || c.share !== void 0 || c.dateGrouping?.partialIntervals !== void 0 || c.dateGrouping?.occurrence !== void 0;
3315
3658
  }
3316
3659
  var soleAggregate = (plan) => plan.ast.kind === "call" && plan.aggregates.includes(plan.ast) ? plan.ast.name : void 0;
3317
3660
  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 +3667,7 @@ function validateMetricSeries(c, plan) {
3324
3667
  );
3325
3668
  const metric = plan, sole = soleAggregate(metric);
3326
3669
  if (window) {
3327
- if (!c.dateGrouping)
3670
+ if (!c.dateGrouping || isMetricCycle(c.dateGrouping.interval))
3328
3671
  throw new FormulaError(
3329
3672
  "A window needs a calendar interval on the first grouping field."
3330
3673
  );
@@ -3374,12 +3717,12 @@ function validateMetricSeries(c, plan) {
3374
3717
  `A window or share runs one metric per aggregate; this one needs more than ${MAX_AGGREGATIONS}.`
3375
3718
  );
3376
3719
  }
3377
- function wholeIntervalSpan(c, query, schema, now) {
3378
- const { field, interval, timeZone } = c.dateGrouping;
3720
+ function metricDateBounds(c, query, schema, now) {
3721
+ const { field } = c.dateGrouping;
3379
3722
  const canonical = (name) => resolveFieldName(schema, name) ?? name;
3380
3723
  let lower = -Infinity, upper = Infinity;
3381
3724
  for (const term of [...query.where, ...c.where ?? []]) {
3382
- if (isOrGroup(term) || term.negated || canonical(term.field) !== canonical(field) || !["=", ">", ">=", "<", "<="].includes(term.op))
3725
+ if (isWhereGroup(term) || term.negated || canonical(term.field) !== canonical(field) || !["=", ">", ">=", "<", "<="].includes(term.op))
3383
3726
  continue;
3384
3727
  if (term.value === "") continue;
3385
3728
  let time;
@@ -3394,6 +3737,11 @@ function wholeIntervalSpan(c, query, schema, now) {
3394
3737
  if (term.op === "<" || term.op === "<=" || term.op === "=")
3395
3738
  upper = Math.min(upper, time + (term.op === "<" ? 0 : 1));
3396
3739
  }
3740
+ return { lower, upper };
3741
+ }
3742
+ function wholeIntervalSpan(c, query, schema, now) {
3743
+ const { interval, timeZone } = c.dateGrouping;
3744
+ const { lower, upper } = metricDateBounds(c, query, schema, now);
3397
3745
  const floor = (time) => metricDateFloor(time, interval, timeZone);
3398
3746
  const end = floor(Math.min(upper, now));
3399
3747
  if (!Number.isFinite(lower)) return { end };
@@ -3431,45 +3779,88 @@ function planMetricSeries(query, schema, options2 = {}) {
3431
3779
  function prepare(c) {
3432
3780
  const issue = validateMetric(c, schema, options2.resolveComputed)[0];
3433
3781
  if (issue) return { kind: "error", clause: c, error: issue.message };
3782
+ function whole(config) {
3783
+ let where2 = c.where ?? [], dateGrouping2 = config;
3784
+ const span = wholeIntervalSpan(
3785
+ { ...c, dateGrouping: config },
3786
+ query,
3787
+ schema,
3788
+ now
3789
+ );
3790
+ if (span.start !== void 0 && span.end <= span.start) {
3791
+ if (!isMetricCycle(config.interval)) return void 0;
3792
+ span.end = span.start;
3793
+ }
3794
+ const field = resolveFieldName(schema, config.field) ?? config.field;
3795
+ const iso = (time) => new Date(time).toISOString();
3796
+ where2 = [
3797
+ ...where2,
3798
+ ...span.start !== void 0 ? [{ field, op: ">=", value: iso(span.start) }] : [],
3799
+ { field, op: "<", value: iso(span.end) }
3800
+ ];
3801
+ if (config.includeEmpty && config.range !== "observed" && !isMetricCycle(config.interval)) {
3802
+ const floor = (time) => metricDateFloor(time, config.interval, config.timeZone);
3803
+ let from = span.start, through = span.end - 1;
3804
+ if (config.range === "custom") {
3805
+ const start = floor(
3806
+ metricDateTime(config.start ?? "", config.timeZone)
3807
+ );
3808
+ from = Math.max(from ?? start, start);
3809
+ through = Math.min(
3810
+ through,
3811
+ metricCustomEndTime(config.end ?? "", config.timeZone)
3812
+ );
3813
+ }
3814
+ if (from !== void 0) {
3815
+ if (through < from) return void 0;
3816
+ dateGrouping2 = {
3817
+ ...config,
3818
+ range: "custom",
3819
+ start: iso(from),
3820
+ end: iso(through)
3821
+ };
3822
+ }
3823
+ }
3824
+ return { where: where2, dateGrouping: dateGrouping2 };
3825
+ }
3434
3826
  let where = c.where ?? [];
3435
3827
  let dateGrouping;
3436
3828
  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
- }
3829
+ const { partialIntervals, occurrence, ...config } = c.dateGrouping;
3830
+ const narrowed = occurrence ? whole({
3831
+ ...config,
3832
+ interval: occurrence.interval,
3833
+ includeEmpty: occurrence.includeEmpty
3834
+ }) : partialIntervals === "exclude" ? whole(config) : { where, dateGrouping: config };
3835
+ if (!narrowed) return { kind: "empty", clause: c };
3836
+ ({ where, dateGrouping } = narrowed);
3837
+ if (occurrence) {
3838
+ const {
3839
+ dateGrouping: _date,
3840
+ where: _where,
3841
+ scope: _scope,
3842
+ distribution: _distribution,
3843
+ display: _display,
3844
+ sort: _sort,
3845
+ groupLimit: _limit,
3846
+ minCount: _min,
3847
+ label: _label,
3848
+ layout: _layout,
3849
+ ...rest
3850
+ } = c;
3851
+ return {
3852
+ kind: "occurrence",
3853
+ clause: c,
3854
+ parts: [
3855
+ {
3856
+ ...rest,
3857
+ id: id(),
3858
+ ...c.scope ? { scope: c.scope } : {},
3859
+ ...where.length ? { where } : {},
3860
+ dateGrouping
3861
+ }
3862
+ ]
3863
+ };
3473
3864
  }
3474
3865
  }
3475
3866
  const scoped = (groupBy) => ({
@@ -3517,16 +3908,33 @@ function planMetricSeries(query, schema, options2 = {}) {
3517
3908
  ...scoped(groupBy)
3518
3909
  })
3519
3910
  );
3911
+ const all = [
3912
+ ...parts(c.groupBy),
3913
+ ...c.share === "group0" ? parts(c.groupBy.slice(0, 1)) : c.share === "total" ? parts([]) : []
3914
+ ];
3915
+ const measure = metricMinCountMeasure(c);
3916
+ const minPart = measure ? all.length : void 0;
3917
+ if (measure)
3918
+ all.push({
3919
+ id: id(),
3920
+ op: "count",
3921
+ expression: metricExpression({
3922
+ id: "",
3923
+ groupBy: [],
3924
+ op: measure.op,
3925
+ field: resolveFieldName(schema, measure.field) ?? measure.field
3926
+ }),
3927
+ groupBy: c.groupBy,
3928
+ ...scoped(c.groupBy)
3929
+ });
3520
3930
  return {
3521
3931
  kind: "series",
3522
3932
  clause: c,
3523
3933
  plan,
3524
- parts: [
3525
- ...parts(c.groupBy),
3526
- ...c.share === "group0" ? parts(c.groupBy.slice(0, 1)) : c.share === "total" ? parts([]) : []
3527
- ],
3934
+ parts: all,
3528
3935
  ...reductions ? { reductions } : {},
3529
- size: texts.length
3936
+ size: texts.length,
3937
+ ...minPart !== void 0 ? { minPart } : {}
3530
3938
  };
3531
3939
  }
3532
3940
  const queries = [];
@@ -3548,14 +3956,16 @@ function planMetricSeries(query, schema, options2 = {}) {
3548
3956
  const c = item.clause;
3549
3957
  if (item.kind === "error")
3550
3958
  return { id: c.id, buckets: [], error: item.error };
3551
- if (item.kind === "empty")
3552
- return {
3959
+ if (item.kind === "empty") {
3960
+ const none = {
3553
3961
  id: c.id,
3554
3962
  buckets: [],
3555
3963
  scope: c.scope ?? "allMatching",
3556
3964
  groupCount: 0,
3557
3965
  coverage: "exact"
3558
3966
  };
3967
+ return c.dateGrouping?.occurrence ? combineOccurrences(c, none) : none;
3968
+ }
3559
3969
  const entries = item.parts.map(
3560
3970
  (part) => byId.get(part.id) ?? {
3561
3971
  id: part.id,
@@ -3565,6 +3975,8 @@ function planMetricSeries(query, schema, options2 = {}) {
3565
3975
  );
3566
3976
  if (item.kind === "plain") return { ...entries[0], id: c.id };
3567
3977
  try {
3978
+ if (item.kind === "occurrence")
3979
+ return combineOccurrences(c, entries[0]);
3568
3980
  return combineSeries(item, entries);
3569
3981
  } catch (e) {
3570
3982
  return {
@@ -3617,7 +4029,7 @@ function combineSeries(item, entries) {
3617
4029
  "A window or share needs every group, but the engine returned only some of them."
3618
4030
  );
3619
4031
  }
3620
- const numerator = entries.slice(0, item.size), denominator = c.share ? entries.slice(item.size) : [];
4032
+ 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
4033
  const dateOf = (keys) => keys[0] === null || keys[0] === void 0 ? void 0 : keys[0];
3622
4034
  const times = /* @__PURE__ */ new Map();
3623
4035
  if (window)
@@ -3727,10 +4139,32 @@ function combineSeries(item, entries) {
3727
4139
  ...own.intervals !== void 0 ? { intervals: own.intervals } : {}
3728
4140
  };
3729
4141
  });
4142
+ const thresholdTracks = threshold ? tracks(threshold) : void 0, thresholdBuckets = threshold ? exact(threshold) : void 0;
4143
+ const measureOf = (keys) => {
4144
+ const at = positionOf(keys);
4145
+ let members;
4146
+ if (!window || at === void 0)
4147
+ members = [thresholdBuckets.get(JSON.stringify(keys))];
4148
+ else {
4149
+ const from = Math.max(0, at - periods + 1), track = thresholdTracks.get(JSON.stringify(keys.slice(1)));
4150
+ members = Array.from(
4151
+ { length: at - from + 1 },
4152
+ (_, i) => track?.[from + i]
4153
+ );
4154
+ }
4155
+ const sum = new Accumulator("SUM");
4156
+ for (const bucket of members) sum.add(bucket);
4157
+ return sum.result();
4158
+ };
3730
4159
  let hiddenGroups;
3731
- if (c.minCount !== void 0) {
4160
+ const min = metricMinCount(c);
4161
+ if (min !== void 0) {
3732
4162
  const kept = buckets.filter(
3733
- (b) => b.count === 0 || b.count >= c.minCount
4163
+ (b) => metricMinCountKeeps(
4164
+ min,
4165
+ b.count,
4166
+ threshold ? measureOf(b.keys) : void 0
4167
+ )
3734
4168
  );
3735
4169
  hiddenGroups = buckets.length - kept.length;
3736
4170
  buckets = kept;
@@ -3749,31 +4183,87 @@ function combineSeries(item, entries) {
3749
4183
  ...hiddenGroups !== void 0 ? { hiddenGroups } : {}
3750
4184
  };
3751
4185
  }
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);
4186
+ function combineOccurrences(c, entry) {
4187
+ const config = c.dateGrouping, cycle = config.interval;
4188
+ if (!isMetricCycle(cycle)) throw Error("Invalid date grouping occurrence.");
4189
+ if (entry.error) throw Error(entry.error);
4190
+ if (entry.coverage === "partial" || (entry.groupCount ?? entry.buckets.length) > entry.buckets.length)
4191
+ throw Error(
4192
+ "Occurrences need every interval, but the engine returned only some of them."
4193
+ );
4194
+ const slots = /* @__PURE__ */ new Map();
4195
+ const slotOf = (keys) => {
4196
+ const key = JSON.stringify(keys);
4197
+ let slot = slots.get(key);
4198
+ if (!slot) slots.set(key, slot = { keys, count: 0, values: [] });
4199
+ return slot;
4200
+ };
4201
+ for (const bucket of entry.buckets) {
4202
+ if (bucket.keys[0] === null || bucket.keys[0] === void 0) continue;
4203
+ const slot = slotOf([
4204
+ metricCycleSlot(metricDateTime(bucket.keys[0]), cycle, config.timeZone),
4205
+ ...bucket.keys.slice(1)
4206
+ ]);
4207
+ slot.count += bucket.count;
4208
+ if (slot.error) continue;
4209
+ if (bucket.error) slot.error = bucket.error;
4210
+ else if (typeof bucket.value === "number") slot.values.push(bucket.value);
4211
+ else if (bucket.value !== null)
4212
+ slot.error = "Occurrence values must be numeric.";
4213
+ }
4214
+ if (config.includeEmpty) {
4215
+ const tails = /* @__PURE__ */ new Map();
4216
+ for (const slot of slots.values())
4217
+ tails.set(JSON.stringify(slot.keys.slice(1)), slot.keys.slice(1));
4218
+ if (c.groupBy.length === 1 && !tails.size) tails.set("[]", []);
4219
+ for (const number of metricCycleSlots(cycle))
4220
+ for (const tail of tails.values()) slotOf([number, ...tail]);
4221
+ }
4222
+ const box = occurrenceDistribution(c);
4223
+ let buckets = [...slots.values()].map((slot) => {
4224
+ const bucket = {
4225
+ keys: slot.keys,
4226
+ count: slot.count,
4227
+ value: null,
4228
+ occurrences: slot.values.length
4229
+ };
4230
+ if (slot.error) return { ...bucket, error: slot.error };
4231
+ try {
4232
+ if (box) {
4233
+ const summary = boxSummary(slot.values, c.distribution.whiskers);
4234
+ bucket.distribution = { kind: "box", summary };
4235
+ bucket.value = summary?.median ?? null;
4236
+ } else if (slot.values.length) {
4237
+ const mean = slot.values.reduce((sum, value) => sum + value, 0) / slot.values.length;
4238
+ assertMetricNumber(mean);
4239
+ bucket.value = mean;
4240
+ }
4241
+ } catch (e) {
4242
+ return { ...bucket, value: null, error: e.message };
4243
+ }
4244
+ return bucket;
4245
+ });
4246
+ let hiddenGroups;
4247
+ const min = metricMinCount(c);
4248
+ if (min !== void 0) {
4249
+ const kept = buckets.filter(
4250
+ (b) => b.count === 0 || b.count >= min
4251
+ );
4252
+ hiddenGroups = buckets.length - kept.length;
4253
+ buckets = kept;
3775
4254
  }
3776
- return terms;
4255
+ sortMetricBuckets(buckets, c);
4256
+ const groupCount = buckets.length;
4257
+ if (c.groupLimit) buckets = buckets.slice(0, c.groupLimit);
4258
+ return {
4259
+ id: c.id,
4260
+ buckets,
4261
+ scope: entry.scope ?? c.scope ?? "allMatching",
4262
+ ...entry.processedRows !== void 0 ? { processedRows: entry.processedRows } : {},
4263
+ groupCount,
4264
+ coverage: "exact",
4265
+ ...hiddenGroups !== void 0 ? { hiddenGroups } : {}
4266
+ };
3777
4267
  }
3778
4268
 
3779
4269
  // src/grouping.ts
@@ -3793,29 +4283,37 @@ function withoutGrouping(query) {
3793
4283
  return result;
3794
4284
  }
3795
4285
  function exitGrouping(query) {
3796
- return query.grouping ? query.grouping.source : query;
4286
+ if (!query.grouping) return query;
4287
+ if (!query.hostState && !query.grouping.source.hostState) return query.grouping.source;
4288
+ const { hostState: _source, ...source } = query.grouping.source;
4289
+ return query.hostState ? { ...source, hostState: query.hostState } : source;
3797
4290
  }
3798
4291
  function withGroupingKeys(query, keys) {
3799
4292
  const before = query.grouping;
3800
4293
  if (!keys.length) return exitGrouping(query);
3801
- if (!before)
4294
+ const names = groupKeyFields(keys);
4295
+ if (!before) {
4296
+ const { hostState, ...source } = query;
3802
4297
  return {
3803
- select: keys.map((field) => ({ field })),
4298
+ select: names.map((field) => ({ field })),
3804
4299
  where: [],
3805
4300
  orderBy: [],
3806
4301
  limit: query.limit,
3807
4302
  offset: 0,
3808
- grouping: { keys, summaries: [], representative: null, source: query }
4303
+ grouping: { keys, summaries: [], representative: null, source },
4304
+ ...hostState ? { hostState } : {}
3809
4305
  };
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 }));
4306
+ }
4307
+ const previous = groupKeyFields(before.keys);
4308
+ const removed = new Set(previous.filter((key) => !names.includes(key)));
4309
+ const columns = query.select.length ? query.select : previous.map((field) => ({ field }));
3812
4310
  return {
3813
4311
  ...query,
3814
4312
  grouping: { ...before, keys },
3815
4313
  offset: 0,
3816
4314
  aggregations: [],
3817
4315
  select: [
3818
- ...keys.filter((key) => !before.keys.includes(key)).map((field) => ({ field })),
4316
+ ...names.filter((key) => !previous.includes(key)).map((field) => ({ field })),
3819
4317
  ...columns.filter((c) => !removed.has(c.field))
3820
4318
  ],
3821
4319
  where: query.where.filter(
@@ -3905,14 +4403,15 @@ function createGroupingModel(inputSchema) {
3905
4403
  if (link.to.kind === "template")
3906
4404
  for (const name of templateFields(link.to.template)) names.add(name);
3907
4405
  else if (link.to.keyFilters !== false)
3908
- for (const key of keys) names.add(key);
4406
+ for (const key of keys) names.add(groupKeyField(key));
3909
4407
  return [...names];
3910
4408
  }
3911
4409
  function linkProblem(column, keys, summaries) {
3912
4410
  const link = column.link;
3913
4411
  if (column.op !== "link") return null;
3914
4412
  if (!link) return "Invalid link definition.";
3915
- const readable = (name) => keys.includes(name) || summaries.some((other) => other.id === name && other.op !== "link");
4413
+ const names = groupKeyFields(keys);
4414
+ const readable = (name) => names.includes(name) || summaries.some((other) => other.id === name && other.op !== "link");
3916
4415
  if (typeof link.label === "object" && !readable(link.label.column))
3917
4416
  return `Unknown link label column: ${link.label.column}.`;
3918
4417
  if (link.to.kind === "template") {
@@ -3920,7 +4419,7 @@ function createGroupingModel(inputSchema) {
3920
4419
  return urlTemplateProblem(link.to.template, [], readable);
3921
4420
  }
3922
4421
  if (link.to.keyFilters !== false)
3923
- for (const key of keys) {
4422
+ for (const key of names) {
3924
4423
  const field = sourceField(key);
3925
4424
  if (field?.type === "textarray")
3926
4425
  return `Key filters cannot match the textarray key ${field.label}.`;
@@ -3969,11 +4468,12 @@ function createGroupingModel(inputSchema) {
3969
4468
  aggregate: { measure: false, groupable: false }
3970
4469
  },
3971
4470
  ...keys.flatMap((key) => {
3972
- const field = sourceField(key);
4471
+ const field = sourceField(groupKeyField(key));
3973
4472
  if (!field) return [];
3974
4473
  return [{
3975
- name: key,
3976
- label: field.label,
4474
+ // A calendar key's column holds each interval's start instant.
4475
+ name: field.name,
4476
+ label: isCalendarGroupKey(key) ? `${field.label} (${key.interval})` : field.label,
3977
4477
  type: field.type,
3978
4478
  source: { kind: "backend" },
3979
4479
  ...field.select ? { select: { ...field.select } } : {},
@@ -4054,7 +4554,7 @@ function createGroupingModel(inputSchema) {
4054
4554
  name: `${inputSchema.name}:groups`,
4055
4555
  idField: GROUP_ID_FIELD,
4056
4556
  fields,
4057
- defaultSelect: keys.filter((key) => sourceField(key)).map((field) => ({ field })),
4557
+ defaultSelect: groupKeyFields(keys).filter((key) => sourceField(key)).map((field) => ({ field })),
4058
4558
  defaultLimit: 25
4059
4559
  };
4060
4560
  return schema;
@@ -4066,13 +4566,8 @@ function createGroupingModel(inputSchema) {
4066
4566
  return value;
4067
4567
  throw new Error("Group keys must be finite scalar values.");
4068
4568
  }
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
- ]);
4569
+ function groupId(values) {
4570
+ const tuple = [...values].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([key, value]) => [key, keyValue(value)]);
4076
4571
  if (tuple.some(
4077
4572
  ([key, value]) => String(key).length + String(value).length > 1e4
4078
4573
  ))
@@ -4141,11 +4636,21 @@ function createGroupingModel(inputSchema) {
4141
4636
  throw new Error("Grouping row budget exceeded.");
4142
4637
  let bytes = rows.length * 16;
4143
4638
  let operations = 0;
4639
+ const readers = keys.map((key) => {
4640
+ const name = groupKeyField(key);
4641
+ const field = schema.fields.find((f2) => f2.name === name);
4642
+ const calendar = isCalendarGroupKey(key) ? calendarKeyReader(key) : null;
4643
+ return [
4644
+ name,
4645
+ (row) => calendar ? calendar(readFieldValue(field, row)) : readFieldValue(field, row)
4646
+ ];
4647
+ });
4144
4648
  const groups = /* @__PURE__ */ new Map();
4145
4649
  for (const row of rows) {
4146
- const id = groupId(row, keys, schema);
4650
+ const values = readers.map(([, read]) => read(row));
4651
+ const id = groupId(readers.map(([name], i) => [name, values[i]]));
4147
4652
  const bucket = groups.get(id);
4148
- if (bucket) bucket.push(row);
4653
+ if (bucket) bucket.rows.push(row);
4149
4654
  else {
4150
4655
  if (groups.size >= MAX_RESULT_GROUPS)
4151
4656
  throw new Error(
@@ -4154,7 +4659,7 @@ function createGroupingModel(inputSchema) {
4154
4659
  bytes += id.length * 2 + 256;
4155
4660
  if (bytes > 16e6)
4156
4661
  throw new Error("Grouping retained-memory budget exceeded.");
4157
- groups.set(id, [row]);
4662
+ groups.set(id, { keys: values, rows: [row] });
4158
4663
  }
4159
4664
  }
4160
4665
  const formulas = new Map(
@@ -4181,15 +4686,13 @@ function createGroupingModel(inputSchema) {
4181
4686
  compiled.plan?.aggregates.map(nodeCost) ?? []
4182
4687
  ])
4183
4688
  );
4184
- return [...groups].map(([id, members]) => {
4689
+ return [...groups].map(([id, { keys: values, rows: members }]) => {
4185
4690
  const output = Object.assign(/* @__PURE__ */ Object.create(null), {
4186
4691
  [GROUP_ID_FIELD]: id
4187
4692
  });
4188
- for (const key of keys)
4189
- output[key] = readFieldValue(
4190
- schema.fields.find((f2) => f2.name === key),
4191
- members[0]
4192
- );
4693
+ readers.forEach(([name], i) => {
4694
+ output[name] = values[i];
4695
+ });
4193
4696
  const chosen = rule && summaries.some((column) => column.op === "row") ? representative(members, rule, schema) : null;
4194
4697
  for (const column of summaries) {
4195
4698
  if (column.op === "link") continue;
@@ -4209,7 +4712,7 @@ function createGroupingModel(inputSchema) {
4209
4712
  throw new Error("Grouping operation budget exceeded.");
4210
4713
  const rowPlan = rowPlans[i];
4211
4714
  if (!rowPlan) return { value: 1 };
4212
- const values = Object.fromEntries(
4715
+ const values2 = Object.fromEntries(
4213
4716
  rowPlan.dependencies.map((name) => {
4214
4717
  const value = readFieldValue(
4215
4718
  schema.fields.find((f2) => f2.name === name),
@@ -4221,7 +4724,7 @@ function createGroupingModel(inputSchema) {
4221
4724
  ];
4222
4725
  })
4223
4726
  );
4224
- return formulaRuntime(rowPlan.ast, values);
4727
+ return formulaRuntime(rowPlan.ast, values2);
4225
4728
  })
4226
4729
  )
4227
4730
  );
@@ -4290,10 +4793,33 @@ function createGroupingModel(inputSchema) {
4290
4793
  function validate(definition) {
4291
4794
  if (definition.diagnostics?.length)
4292
4795
  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)
4796
+ const names = definition.keys.map(
4797
+ (key) => typeof key === "string" || isCalendarGroupKey(key) ? groupKeyField(key) : ""
4798
+ );
4799
+ if (!definition.keys.length || definition.keys.length > MAX_GROUP_BY_FIELDS || new Set(names).size !== names.length || names.some(
4800
+ (key) => typeof key !== "string" || key === GROUP_ID_FIELD || !groupableFields.some((f2) => f2.name === key)
4295
4801
  ))
4296
4802
  throw new Error("Choose valid group keys from the source schema.");
4803
+ for (const key of definition.keys) {
4804
+ if (!isCalendarGroupKey(key)) continue;
4805
+ const field = sourceField(key.field);
4806
+ if (isMetricCycle(key.interval))
4807
+ throw new Error(
4808
+ `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.`
4809
+ );
4810
+ if (!GROUP_KEY_INTERVALS.includes(key.interval))
4811
+ throw new Error(
4812
+ `Group key ${field.label}: choose an hour, day, week, month or year interval.`
4813
+ );
4814
+ if (field.type !== "datetime")
4815
+ throw new Error(
4816
+ `Group key ${field.label}: a calendar interval needs a datetime field.`
4817
+ );
4818
+ if (key.timeZone !== void 0 && !isGroupingTimeZone(key.timeZone))
4819
+ throw new Error(
4820
+ `Group key ${field.label}: use a valid IANA timezone, such as America/Denver or UTC.`
4821
+ );
4822
+ }
4297
4823
  if (definition.summaries.length > MAX_SUMMARY_COLUMNS)
4298
4824
  throw new Error("Too many aggregate columns.");
4299
4825
  const ids = /* @__PURE__ */ new Set([
@@ -4449,105 +4975,6 @@ function toServerQueryV2(q, schema, execution) {
4449
4975
  };
4450
4976
  }
4451
4977
 
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
4978
  // src/metrics.ts
4552
4979
  var aggregates = /* @__PURE__ */ new Set([
4553
4980
  "COUNT",
@@ -4801,8 +5228,11 @@ function hasRegex(plan) {
4801
5228
  function distributionActive(c) {
4802
5229
  return !!c.distribution && (!c.display || c.display.kind === "box" || c.display.kind === "histogram");
4803
5230
  }
5231
+ function occurrenceDistribution(c) {
5232
+ return distributionActive(c) && !!c.dateGrouping?.occurrence;
5233
+ }
4804
5234
  function metricPlans(c, schema, resolve) {
4805
- if (distributionActive(c)) {
5235
+ if (distributionActive(c) && !occurrenceDistribution(c)) {
4806
5236
  const p = compileFormula(
4807
5237
  c.distribution.input,
4808
5238
  resolve ? schema.fields.filter(
@@ -4862,21 +5292,28 @@ function validateMetric(c, schema, resolve) {
4862
5292
  throw new FormulaError(
4863
5293
  "Date grouping must use the first grouping field, with a datetime type."
4864
5294
  );
4865
- if (distributionActive(c))
5295
+ const { occurrence } = c.dateGrouping;
5296
+ if (occurrence) {
5297
+ if (distributionActive(c) && c.distribution.kind !== "box")
5298
+ throw new FormulaError(
5299
+ "Occurrences summarise as a mean or a box plot, not a histogram."
5300
+ );
5301
+ if (c.display?.kind === "scatter")
5302
+ throw new FormulaError(
5303
+ "Occurrences summarise single-value metrics, not scatters."
5304
+ );
5305
+ if (c.window || c.share !== void 0)
5306
+ throw new FormulaError(
5307
+ "Windows and shares do not apply to occurrences of a slot."
5308
+ );
5309
+ } else if (distributionActive(c))
4866
5310
  throw new FormulaError(
4867
5311
  "Date grouping currently supports scalar metrics, not distributions."
4868
5312
  );
4869
5313
  }
4870
5314
  if (c.groupLimit !== void 0 && (!Number.isInteger(c.groupLimit) || c.groupLimit < 1 || c.groupLimit > 1e4))
4871
5315
  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
- }
5316
+ if (c.minCount !== void 0) validateMinCount(c, schema);
4880
5317
  validateMetricWhere(c.where, schema);
4881
5318
  for (const s of c.sort ?? []) {
4882
5319
  if (s.key === "y" && c.display?.kind !== "scatter")
@@ -4903,6 +5340,81 @@ function validateMetric(c, schema, resolve) {
4903
5340
  ];
4904
5341
  }
4905
5342
  }
5343
+ function metricMinCount(c) {
5344
+ const m = c.minCount;
5345
+ return m === void 0 ? void 0 : typeof m === "number" ? m : m?.min;
5346
+ }
5347
+ function metricMinCountMeasure(c) {
5348
+ const m = c.minCount;
5349
+ if (m === void 0 || m === null || typeof m !== "object") return void 0;
5350
+ return m.distinct !== void 0 ? { op: "count_distinct", field: m.distinct } : { op: "sum", field: m.sum };
5351
+ }
5352
+ function metricMinCountText(c, label = (field) => field) {
5353
+ const min = metricMinCount(c), measure = metricMinCountMeasure(c);
5354
+ if (min === void 0) return void 0;
5355
+ const n = min.toLocaleString();
5356
+ if (!measure) return `${n} ${min === 1 ? "row" : "rows"}`;
5357
+ return measure.op === "sum" ? `${n} total ${label(measure.field)}` : `${n} distinct ${label(measure.field)} ${min === 1 ? "value" : "values"}`;
5358
+ }
5359
+ function metricMinCountPlan(c, schema, resolve) {
5360
+ const measure = metricMinCountMeasure(c);
5361
+ return measure ? compileMetric(
5362
+ metricExpression({
5363
+ id: "",
5364
+ groupBy: [],
5365
+ op: measure.op,
5366
+ field: resolveFieldName(schema, measure.field) ?? measure.field
5367
+ }),
5368
+ schema,
5369
+ resolve
5370
+ ) : void 0;
5371
+ }
5372
+ function metricMinCountKeeps(min, count, measure) {
5373
+ if (count === 0) return true;
5374
+ if (!measure) return count >= min;
5375
+ return measure.error !== void 0 || Number(measure.value ?? 0) >= min;
5376
+ }
5377
+ function validateMinCount(c, schema) {
5378
+ const m = c.minCount;
5379
+ const measured = m !== null && typeof m === "object";
5380
+ const min = measured ? m.min : m;
5381
+ const label = measured ? "Minimum per group" : "Minimum rows per group";
5382
+ if (typeof min !== "number" || !Number.isInteger(min) || min < 1 || min > 1e6)
5383
+ throw new FormulaError(`${label} must be 1\u20131,000,000.`);
5384
+ if (measured) {
5385
+ const o = m;
5386
+ if (Object.keys(o).some((k) => !["min", "distinct", "sum"].includes(k)) || o.distinct === void 0 === (o.sum === void 0))
5387
+ throw new FormulaError(
5388
+ "A minimum per group needs exactly one of distinct and sum."
5389
+ );
5390
+ const name = o.distinct ?? o.sum;
5391
+ const op = o.distinct !== void 0 ? "count_distinct" : "sum";
5392
+ const resolved = typeof name === "string" ? resolveFieldName(schema, name) ?? name : "";
5393
+ const field = schema.fields.find((f2) => f2.name === resolved);
5394
+ if (op === "sum" && field && field.type !== "number")
5395
+ throw new FormulaError(
5396
+ `A summed minimum requires a numeric field, not ${resolved}.`
5397
+ );
5398
+ if (!field || isComputedField(resolved) || !isMeasurable(field) || !aggOpsForField(field).includes(op))
5399
+ throw new FormulaError(
5400
+ 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.`
5401
+ );
5402
+ if (distributionActive(c))
5403
+ throw new FormulaError(
5404
+ "A distinct or summed minimum currently supports scalar metrics, not distributions."
5405
+ );
5406
+ if (c.window && op === "count_distinct")
5407
+ throw new FormulaError(
5408
+ "A distinct minimum cannot be combined across a window's intervals."
5409
+ );
5410
+ if (c.dateGrouping?.occurrence)
5411
+ throw new FormulaError(
5412
+ "A distinct or summed minimum does not apply to occurrences of a slot; use a minimum of rows."
5413
+ );
5414
+ }
5415
+ if (!c.groupBy.length)
5416
+ throw new FormulaError(`${label} requires at least one grouping field.`);
5417
+ }
4906
5418
  function validateMetricWhere(where, schema) {
4907
5419
  if (where === void 0) return;
4908
5420
  if (!Array.isArray(where)) throw new FormulaError("Invalid metric filter.");
@@ -4930,13 +5442,23 @@ function metricPopulationError(clause, support) {
4930
5442
  return "This server does not support metric filters.";
4931
5443
  if (clause.minCount !== void 0 && !support.minCount)
4932
5444
  return "This server does not support a minimum rows per group.";
5445
+ if (metricMinCountMeasure(clause) && !support.minCountMeasures)
5446
+ return "This server does not support a minimum of distinct values or a sum per group.";
4933
5447
  return void 0;
4934
5448
  }
4935
5449
  function metricDependencies(c, schema, resolve) {
4936
5450
  return [
4937
5451
  .../* @__PURE__ */ new Set([
4938
5452
  ...c.groupBy.map((name) => resolveFieldName(schema, name) ?? name),
4939
- ...metricPlans(c, schema, resolve).flatMap((p) => p.dependencies)
5453
+ ...metricPlans(c, schema, resolve).flatMap((p) => p.dependencies),
5454
+ // An invalid minimum fails validation, not dependency discovery.
5455
+ ...(() => {
5456
+ try {
5457
+ return metricMinCountPlan(c, schema, resolve)?.dependencies;
5458
+ } catch {
5459
+ return void 0;
5460
+ }
5461
+ })() ?? []
4940
5462
  ])
4941
5463
  ].sort();
4942
5464
  }
@@ -4946,7 +5468,7 @@ function metricComputationKey(clauses) {
4946
5468
  id: c.id,
4947
5469
  diagnostics: c.diagnostics,
4948
5470
  unsupportedOp: c.unsupportedOp,
4949
- expression: distributionActive(c) ? void 0 : metricExpression(c),
5471
+ expression: distributionActive(c) && !occurrenceDistribution(c) ? void 0 : metricExpression(c),
4950
5472
  expressionY: c.display?.kind === "scatter" ? c.expressionY : void 0,
4951
5473
  scope: c.scope ?? "allMatching",
4952
5474
  groupBy: c.groupBy,
@@ -5046,7 +5568,7 @@ function toMetricQuery(q, schema, execution) {
5046
5568
  code: "unexpanded_series",
5047
5569
  from: 0,
5048
5570
  to: 1,
5049
- message: "Windows, shares and whole intervals are computed from plain metrics; expand the query with planMetricSeries first."
5571
+ message: "Windows, shares, whole intervals and occurrences are computed from plain metrics; expand the query with planMetricSeries first."
5050
5572
  });
5051
5573
  for (const c of q.aggregations ?? [])
5052
5574
  if (c.dateGrouping)
@@ -5395,7 +5917,7 @@ function evaluateMetrics(rows, q, schema, options2 = {}) {
5395
5917
  config.interval,
5396
5918
  config.timeZone
5397
5919
  );
5398
- dateKey = new Date(dateStart).toISOString();
5920
+ dateKey = isMetricCycle(config.interval) ? metricCycleSlot(dateStart, config.interval, config.timeZone) : new Date(dateStart).toISOString();
5399
5921
  }
5400
5922
  keys[0] = dateKey;
5401
5923
  }
@@ -5429,7 +5951,8 @@ function evaluateMetrics(rows, q, schema, options2 = {}) {
5429
5951
  tick();
5430
5952
  reserve(8);
5431
5953
  groupBytes += 8;
5432
- observed.push(metricDateTime(group.keys[0]));
5954
+ if (!isMetricCycle(config.interval))
5955
+ observed.push(metricDateTime(group.keys[0]));
5433
5956
  const tuple = group.keys.slice(1);
5434
5957
  const key = JSON.stringify(tuple);
5435
5958
  if (!series2.has(key)) {
@@ -5444,7 +5967,24 @@ function evaluateMetrics(rows, q, schema, options2 = {}) {
5444
5967
  groupBytes += 132;
5445
5968
  series2.set("[]", []);
5446
5969
  }
5447
- const range = metricDateRange(
5970
+ const slots = isMetricCycle(config.interval) ? metricCycleSlots(config.interval) : void 0;
5971
+ for (const slot of slots ?? [])
5972
+ for (const tuple of series2.values()) {
5973
+ tick();
5974
+ const keys = [slot, ...tuple];
5975
+ const key = JSON.stringify(keys);
5976
+ if (key.length > keyLimit)
5977
+ throw Error("Metric grouping key length budget exceeded.");
5978
+ if (!groups.has(key)) {
5979
+ if (groups.size >= (options2.maxGroups ?? 1e4))
5980
+ throw Error("Metric group budget exceeded.");
5981
+ const bytes = 256 + 16 * keys.length + 4 * key.length;
5982
+ reserve(bytes);
5983
+ groupBytes += bytes;
5984
+ groups.set(key, { keys, rows: [] });
5985
+ }
5986
+ }
5987
+ const range = slots ? void 0 : metricDateRange(
5448
5988
  config,
5449
5989
  q,
5450
5990
  observed,
@@ -5539,12 +6079,16 @@ function evaluateMetrics(rows, q, schema, options2 = {}) {
5539
6079
  );
5540
6080
  };
5541
6081
  const populations = /* @__PURE__ */ new Map();
6082
+ const minPlan = metricMinCountPlan(c, schema, options2.resolveComputed);
6083
+ const measures = /* @__PURE__ */ new Map();
5542
6084
  let buckets = [...groups.values()].map((g) => {
5543
6085
  const bucket = {
5544
6086
  keys: g.keys,
5545
6087
  count: g.rows.length,
5546
6088
  value: null
5547
6089
  };
6090
+ if (minPlan && g.rows.length)
6091
+ measures.set(bucket, reduce(minPlan, g.rows, /* @__PURE__ */ new Set()));
5548
6092
  if (distributionActive(c)) {
5549
6093
  const values = [];
5550
6094
  bucket.nullCount = 0;
@@ -5607,9 +6151,10 @@ function evaluateMetrics(rows, q, schema, options2 = {}) {
5607
6151
  }
5608
6152
  }
5609
6153
  let hiddenGroups;
5610
- if (c.minCount !== void 0) {
6154
+ const min = metricMinCount(c);
6155
+ if (min !== void 0) {
5611
6156
  const kept = buckets.filter(
5612
- (b) => b.count === 0 || b.count >= c.minCount
6157
+ (b) => metricMinCountKeeps(min, b.count, minPlan ? measures.get(b) : void 0)
5613
6158
  );
5614
6159
  hiddenGroups = buckets.length - kept.length;
5615
6160
  buckets = kept;
@@ -5710,7 +6255,7 @@ function matchesClause(row, clause, schema, options2 = {}) {
5710
6255
  { ...clause, field },
5711
6256
  evaluationTime(options2)
5712
6257
  );
5713
- prepareDatetimePredicates([{ kind: "lit", predicate: prepared }], byName);
6258
+ prepareDatetimePredicates([{ branches: [[prepared]] }], byName);
5714
6259
  return matchesWith(byName, row, prepared);
5715
6260
  }
5716
6261
  function applyAggregations(rows, q, schema, options2 = {}) {
@@ -5858,25 +6403,15 @@ function prepareWhereClause(clause, now) {
5858
6403
  };
5859
6404
  }
5860
6405
  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
6406
  return {
5870
- kind: "lit",
5871
- predicate: prepareWhereClause(
5872
- { ...term, field: resolveField2(term.field) },
5873
- now
6407
+ branches: branchesOf(term).map(
6408
+ (branch) => branch.map((c) => prepareWhereClause({ ...c, field: resolveField2(c.field) }, now))
5874
6409
  )
5875
6410
  };
5876
6411
  }
5877
6412
  function prepareDatetimePredicates(terms, fields) {
5878
6413
  for (const term of terms)
5879
- for (const p of term.kind === "lit" ? [term.predicate] : term.predicates) {
6414
+ for (const p of term.branches.flat()) {
5880
6415
  const field = fields.get(p.clause.field);
5881
6416
  if (field?.type !== "datetime" || p.clause.value === "" || p.clause.op === "is_null" || p.clause.op === "is_not_null")
5882
6417
  continue;
@@ -5884,8 +6419,7 @@ function prepareDatetimePredicates(terms, fields) {
5884
6419
  }
5885
6420
  }
5886
6421
  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));
6422
+ return term.branches.some((branch) => branch.every((p) => matchesWith(byName, row, p)));
5889
6423
  }
5890
6424
  function matchesWith(byName, row, prepared) {
5891
6425
  const { clause } = prepared;
@@ -6053,6 +6587,7 @@ function memoryStorageAdapter() {
6053
6587
  const saved = /* @__PURE__ */ new Map();
6054
6588
  const defaults = /* @__PURE__ */ new Map();
6055
6589
  return {
6590
+ storesHostState: true,
6056
6591
  async loadLast(key) {
6057
6592
  const query = last.get(key);
6058
6593
  return query ? normalizeQueryState(query) : null;
@@ -6143,6 +6678,7 @@ function localStorageAdapter() {
6143
6678
  else ls.removeItem(DEFAULT_PREFIX + key);
6144
6679
  };
6145
6680
  return {
6681
+ storesHostState: true,
6146
6682
  async loadLast(key) {
6147
6683
  if (!ls) return null;
6148
6684
  try {
@@ -6325,7 +6861,7 @@ function sourceDependencies(query, schema, resolveComputed) {
6325
6861
  if (query.grouping) {
6326
6862
  const model = createGroupingModel(schema);
6327
6863
  model.validate(query.grouping);
6328
- for (const key of query.grouping.keys) fields.add(key);
6864
+ for (const key of groupKeyFields(query.grouping.keys)) fields.add(key);
6329
6865
  for (const summary of query.grouping.summaries) {
6330
6866
  if (summary.op === "link") continue;
6331
6867
  if (summary.op === "row") fields.add(summary.field);
@@ -6372,8 +6908,9 @@ function clauseKey(clause) {
6372
6908
  ]);
6373
6909
  }
6374
6910
  function whereTermKey(term) {
6375
- if (!isOrGroup(term)) return clauseKey(term);
6376
- return `any:${JSON.stringify(term.any.map(clauseKey).sort())}`;
6911
+ if (!isWhereGroup(term)) return clauseKey(term);
6912
+ const branchKey = (member) => isWhereConjunction(member) ? `all:${JSON.stringify(member.all.map(clauseKey).sort())}` : clauseKey(member);
6913
+ return `any:${JSON.stringify(term.any.map(branchKey).sort())}`;
6377
6914
  }
6378
6915
  function appendWhereTerms(where, terms) {
6379
6916
  const present = new Set(where.map(whereTermKey));
@@ -6391,7 +6928,7 @@ function filterUnits(where) {
6391
6928
  for (const term of where) {
6392
6929
  const last = units[units.length - 1];
6393
6930
  const previous = last?.[last.length - 1];
6394
- if (last && previous && !isOrGroup(term) && !isOrGroup(previous) && term.op === "!=" && previous.op === "!=" && term.field === previous.field)
6931
+ if (last && previous && !isWhereGroup(term) && !isWhereGroup(previous) && term.op === "!=" && previous.op === "!=" && term.field === previous.field)
6395
6932
  last.push(term);
6396
6933
  else units.push([term]);
6397
6934
  }
@@ -6629,6 +7166,8 @@ function toQueryRequestV3(query, baseSchema, capabilities, execution, options2 =
6629
7166
  let resultSchema = sourceSchema;
6630
7167
  if (q.grouping) {
6631
7168
  if (!capabilities.groupedRows) diagnostics.push("This server does not support grouped rows.");
7169
+ else if (q.grouping.keys.some(isCalendarGroupKey) && !capabilities.calendarGroupKeys)
7170
+ diagnostics.push("This server does not support grouping keys by calendar interval.");
6632
7171
  try {
6633
7172
  const model = createGroupingModel(sourceSchema);
6634
7173
  model.validate(q.grouping);
@@ -6656,6 +7195,8 @@ function toQueryRequestV3(query, baseSchema, capabilities, execution, options2 =
6656
7195
  for (const metric of metrics) {
6657
7196
  diagnostics.push(...validateMetric(metric, resultSchema).map((issue) => issue.message));
6658
7197
  if (metric.dateGrouping && !capabilities.calendarGrouping) diagnostics.push("This server does not support calendar grouping.");
7198
+ else if (isMetricCycle(metric.dateGrouping?.interval) && !capabilities.calendarCycles)
7199
+ diagnostics.push("This server does not support hour-of-day or day-of-week grouping.");
6659
7200
  }
6660
7201
  const revisions = /* @__PURE__ */ Object.create(null);
6661
7202
  for (const [id, revision] of Object.entries(execution?.resolvedRevisions ?? {})) {
@@ -6714,6 +7255,13 @@ function inspectBucketQuery(query, bucket, clause, schema) {
6714
7255
  for (const [index, name] of clause.groupBy.entries()) {
6715
7256
  const field = resolveField(fields, name);
6716
7257
  const key = bucket.keys[index] ?? null;
7258
+ if (clause.dateGrouping && index === 0 && key !== null && isMetricCycle(clause.dateGrouping.interval)) {
7259
+ if (!field || field.type !== "datetime" || !isFilterable(field)) return null;
7260
+ const slot = cycleSlotTerms(query, clause, fields, field.name, key);
7261
+ if (!slot) return null;
7262
+ terms.push(...slot);
7263
+ continue;
7264
+ }
6717
7265
  if (clause.dateGrouping && index === 0 && key !== null) {
6718
7266
  if (!field || field.type !== "datetime" || !isFilterable(field)) return null;
6719
7267
  let start;
@@ -6760,7 +7308,54 @@ function inspectBucketQuery(query, bucket, clause, schema) {
6760
7308
  { field: field.name, op: inclusiveMax ? "<=" : "<", value: String(max) }
6761
7309
  );
6762
7310
  }
6763
- return { ...query, where: appendWhereTerms(query.where, terms), offset: 0 };
7311
+ const where = appendWhereTerms(query.where, terms);
7312
+ if (isMetricCycle(clause.dateGrouping?.interval) && where.reduce((n, term) => n + predicatesOf(term).length, 0) > MAX_WHERE_CLAUSES) return null;
7313
+ return { ...query, where, offset: 0 };
7314
+ }
7315
+ function cycleSlotTerms(query, clause, schema, field, key) {
7316
+ const config = clause.dateGrouping, cycle = config.interval;
7317
+ if (!isMetricCycle(cycle) || typeof key !== "number" || !metricCycleSlots(cycle).includes(key)) return null;
7318
+ const now = Date.now();
7319
+ let { lower, upper } = metricDateBounds(clause, query, schema, now);
7320
+ const whole = config.occurrence ? { ...clause, dateGrouping: { ...config, interval: config.occurrence.interval } } : config.partialIntervals === "exclude" ? clause : void 0;
7321
+ if (whole) {
7322
+ const span = wholeIntervalSpan(whole, query, schema, now);
7323
+ lower = span.start ?? -Infinity;
7324
+ upper = span.end;
7325
+ }
7326
+ if (!Number.isFinite(lower) || !Number.isFinite(upper)) return null;
7327
+ const budget = Math.floor(MAX_WHERE_CLAUSES / 2);
7328
+ const ranges = [];
7329
+ const unit = metricCalendarUnit(cycle);
7330
+ for (let start = metricDateFloor(lower, unit, config.timeZone); start < upper; ) {
7331
+ const next = metricDateNext(start, unit, config.timeZone);
7332
+ if (!Number.isFinite(next) || next <= start) return null;
7333
+ if (metricCycleSlot(start, cycle, config.timeZone) === key) {
7334
+ const from = Math.max(start, lower), to = Math.min(next, upper);
7335
+ const last = ranges[ranges.length - 1];
7336
+ if (from < to) {
7337
+ if (last && last[1] === from) last[1] = to;
7338
+ else if (ranges.push([from, to]) > budget) return null;
7339
+ }
7340
+ }
7341
+ start = next;
7342
+ }
7343
+ const iso = (time) => new Date(time).toISOString();
7344
+ if (!ranges.length)
7345
+ return [
7346
+ { field, op: ">=", value: iso(lower) },
7347
+ { field, op: "<", value: iso(lower) }
7348
+ ];
7349
+ const terms = [
7350
+ { field, op: ">=", value: iso(ranges[0][0]) },
7351
+ { field, op: "<", value: iso(ranges[ranges.length - 1][1]) }
7352
+ ];
7353
+ for (let i = 0; i + 1 < ranges.length; i++)
7354
+ terms.push({ any: [
7355
+ { field, op: "<", value: iso(ranges[i][1]) },
7356
+ { field, op: ">=", value: iso(ranges[i + 1][0]) }
7357
+ ] });
7358
+ return terms;
6764
7359
  }
6765
7360
  function groupSourceQuery(query, group, schema) {
6766
7361
  if (!query.grouping) return null;
@@ -6777,10 +7372,15 @@ export {
6777
7372
  FORMULA_FUNCTIONS,
6778
7373
  FormulaError,
6779
7374
  GROUP_ID_FIELD,
7375
+ GROUP_KEY_INTERVALS,
6780
7376
  GROUP_LINK_RENDER,
6781
7377
  ITEM_DETAILS_COLUMN,
6782
7378
  MAX_AGGREGATIONS,
6783
7379
  MAX_GROUP_BY_FIELDS,
7380
+ MAX_HOST_QUERY_PARAMS,
7381
+ MAX_HOST_QUERY_STATE_BYTES,
7382
+ MAX_HOST_QUERY_VALUES,
7383
+ MAX_HOST_QUERY_VALUE_LENGTH,
6784
7384
  MAX_METRIC_WINDOW_PERIODS,
6785
7385
  MAX_ORDER_BY_TERMS,
6786
7386
  MAX_PROPERTIES,
@@ -6794,6 +7394,7 @@ export {
6794
7394
  MAX_SELECT_COLUMNS,
6795
7395
  MAX_SUMMARY_COLUMNS,
6796
7396
  MAX_WHERE_CLAUSES,
7397
+ METRIC_CYCLE_INTERVALS,
6797
7398
  METRIC_DATE_INTERVALS,
6798
7399
  METRIC_FUNCTIONS,
6799
7400
  NULLARY_OPS,
@@ -6808,6 +7409,8 @@ export {
6808
7409
  applyPatch,
6809
7410
  applyQuery,
6810
7411
  boxSummary,
7412
+ branchesOf,
7413
+ calendarGroupKey,
6811
7414
  coerceValue,
6812
7415
  compareMetricValues,
6813
7416
  compileFormula,
@@ -6826,6 +7429,9 @@ export {
6826
7429
  exitGrouping,
6827
7430
  filterValues,
6828
7431
  formulaRuntime,
7432
+ groupKeyField,
7433
+ groupKeyFields,
7434
+ groupKeyTerms,
6829
7435
  groupLinkQuery,
6830
7436
  groupPreview,
6831
7437
  groupSourceQuery,
@@ -6837,18 +7443,24 @@ export {
6837
7443
  httpQuerySnapshotStore,
6838
7444
  indexFields,
6839
7445
  inspectBucketQuery,
7446
+ isCalendarGroupKey,
6840
7447
  isComputedCellError,
6841
7448
  isComputedField,
6842
7449
  isFilterable,
6843
7450
  isGroupLinkField,
6844
7451
  isGroupLinkValue,
6845
7452
  isGroupable,
7453
+ isGroupingTimeZone,
6846
7454
  isMeasurable,
7455
+ isMetricCycle,
6847
7456
  isNegativePredicate,
6848
7457
  isOrGroup,
7458
+ isOrOfAndsGroup,
6849
7459
  isPushdownFilter,
6850
7460
  isSelectable,
6851
7461
  isSortable,
7462
+ isWhereConjunction,
7463
+ isWhereGroup,
6852
7464
  itemPageFields,
6853
7465
  jsonPropertyViewStorage,
6854
7466
  loadSchema,
@@ -6863,14 +7475,23 @@ export {
6863
7475
  memoryQuerySnapshotCache,
6864
7476
  memoryStorageAdapter,
6865
7477
  metricAggregatePlans,
7478
+ metricCalendarUnit,
6866
7479
  metricComputationKey,
6867
7480
  metricCustomEndTime,
7481
+ metricCycleSlot,
7482
+ metricCycleSlots,
7483
+ metricDateBounds,
6868
7484
  metricDateFloor,
6869
7485
  metricDateNext,
6870
7486
  metricDateRange,
6871
7487
  metricDateTime,
6872
7488
  metricDependencies,
6873
7489
  metricExpression,
7490
+ metricMinCount,
7491
+ metricMinCountKeeps,
7492
+ metricMinCountMeasure,
7493
+ metricMinCountPlan,
7494
+ metricMinCountText,
6874
7495
  metricPlans,
6875
7496
  metricPopulationError,
6876
7497
  negateClause,
@@ -6878,9 +7499,11 @@ export {
6878
7499
  normalizePropertyView,
6879
7500
  normalizeQueryState,
6880
7501
  objectReferenceKey,
7502
+ occurrenceDistribution,
6881
7503
  opAllowedForType,
6882
7504
  opPairsForField,
6883
7505
  opsForField,
7506
+ parseHostQueryState,
6884
7507
  parseRelativeDuration,
6885
7508
  percentile,
6886
7509
  planMetricSeries,