@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.d.ts +260 -34
- package/dist/index.js +1189 -566
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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: "
|
|
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: [
|
|
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
|
-
|
|
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 =
|
|
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)
|
|
735
|
-
|
|
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 + (
|
|
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 =
|
|
883
|
-
|
|
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 (
|
|
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 + (
|
|
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 +
|
|
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
|
-
|
|
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 +
|
|
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
|
|
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 ||
|
|
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
|
|
3378
|
-
const { field
|
|
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 (
|
|
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
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
...
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4160
|
+
const min = metricMinCount(c);
|
|
4161
|
+
if (min !== void 0) {
|
|
3732
4162
|
const kept = buckets.filter(
|
|
3733
|
-
(b) =>
|
|
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
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
const
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4294
|
+
const names = groupKeyFields(keys);
|
|
4295
|
+
if (!before) {
|
|
4296
|
+
const { hostState, ...source } = query;
|
|
3802
4297
|
return {
|
|
3803
|
-
select:
|
|
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
|
|
4303
|
+
grouping: { keys, summaries: [], representative: null, source },
|
|
4304
|
+
...hostState ? { hostState } : {}
|
|
3809
4305
|
};
|
|
3810
|
-
|
|
3811
|
-
const
|
|
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
|
-
...
|
|
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
|
|
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
|
|
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
|
-
|
|
3976
|
-
|
|
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(
|
|
4070
|
-
const tuple = [...
|
|
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
|
|
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
|
-
|
|
4189
|
-
output[
|
|
4190
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
4294
|
-
(key) => 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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
6154
|
+
const min = metricMinCount(c);
|
|
6155
|
+
if (min !== void 0) {
|
|
5611
6156
|
const kept = buckets.filter(
|
|
5612
|
-
(b) => b.count
|
|
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([{
|
|
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
|
-
|
|
5871
|
-
|
|
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.
|
|
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
|
-
|
|
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 (!
|
|
6376
|
-
|
|
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 && !
|
|
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
|
-
|
|
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,
|