@pythia-software/query-table-core 0.9.5 → 0.9.6
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 +76 -3
- package/dist/index.js +840 -325
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -445,6 +445,101 @@ function renderItemPageTemplate(template, schema, row, id, query) {
|
|
|
445
445
|
}
|
|
446
446
|
|
|
447
447
|
// src/metricNormalize.ts
|
|
448
|
+
var METRIC_OPTION_SPECS = {
|
|
449
|
+
top: {
|
|
450
|
+
expression: "string",
|
|
451
|
+
expressionY: "string",
|
|
452
|
+
scope: ["allMatching", "shownRows"],
|
|
453
|
+
groupLimit: "integer",
|
|
454
|
+
minCount: "positiveInteger",
|
|
455
|
+
share: ["total", "group0"]
|
|
456
|
+
},
|
|
457
|
+
window: {
|
|
458
|
+
reduce: ["total", "mean"],
|
|
459
|
+
partial: ["omit", "include"]
|
|
460
|
+
},
|
|
461
|
+
dateGrouping: {
|
|
462
|
+
field: "string",
|
|
463
|
+
interval: ["hour", "day", "week", "month", "year"],
|
|
464
|
+
timeZone: "string",
|
|
465
|
+
includeEmpty: "boolean",
|
|
466
|
+
range: ["query", "observed", "custom"],
|
|
467
|
+
start: "string",
|
|
468
|
+
end: "string",
|
|
469
|
+
partialIntervals: ["include", "exclude"]
|
|
470
|
+
},
|
|
471
|
+
display: {
|
|
472
|
+
kind: [
|
|
473
|
+
"auto",
|
|
474
|
+
"value",
|
|
475
|
+
"table",
|
|
476
|
+
"list",
|
|
477
|
+
"bar-horizontal",
|
|
478
|
+
"bar-vertical",
|
|
479
|
+
"line",
|
|
480
|
+
"pie",
|
|
481
|
+
"donut",
|
|
482
|
+
"scatter",
|
|
483
|
+
"box",
|
|
484
|
+
"histogram"
|
|
485
|
+
],
|
|
486
|
+
valueLabel: "string",
|
|
487
|
+
xLabel: "string",
|
|
488
|
+
yLabel: "string",
|
|
489
|
+
legendPosition: ["left", "right", "top", "bottom", "none"],
|
|
490
|
+
maxGroups: "positiveInteger",
|
|
491
|
+
showOther: "boolean",
|
|
492
|
+
boxMean: "boolean"
|
|
493
|
+
},
|
|
494
|
+
scale: {
|
|
495
|
+
mode: ["linear", "log"],
|
|
496
|
+
min: "number",
|
|
497
|
+
max: "number"
|
|
498
|
+
},
|
|
499
|
+
format: {
|
|
500
|
+
kind: ["number", "percent", "duration", "date", "time", "datetime"],
|
|
501
|
+
decimals: "integer",
|
|
502
|
+
sourceUnit: [
|
|
503
|
+
"milliseconds",
|
|
504
|
+
"seconds",
|
|
505
|
+
"minutes",
|
|
506
|
+
"hours",
|
|
507
|
+
"days",
|
|
508
|
+
"iso",
|
|
509
|
+
"epochSeconds",
|
|
510
|
+
"epochMilliseconds",
|
|
511
|
+
"secondsOfDay",
|
|
512
|
+
"millisecondsOfDay"
|
|
513
|
+
],
|
|
514
|
+
style: ["human", "long", "clock", "custom", "iso"],
|
|
515
|
+
pattern: "string",
|
|
516
|
+
timeZone: "string"
|
|
517
|
+
},
|
|
518
|
+
list: {
|
|
519
|
+
showBars: "boolean",
|
|
520
|
+
useGroupColors: "boolean",
|
|
521
|
+
showValues: "boolean",
|
|
522
|
+
barPosition: ["inline", "below"],
|
|
523
|
+
maxLabelLines: "positiveInteger"
|
|
524
|
+
},
|
|
525
|
+
pivot: {
|
|
526
|
+
swap: "boolean",
|
|
527
|
+
rowDir: ["asc", "desc"],
|
|
528
|
+
columnDir: ["asc", "desc"]
|
|
529
|
+
},
|
|
530
|
+
distribution: {
|
|
531
|
+
kind: ["box", "histogram"],
|
|
532
|
+
input: "string",
|
|
533
|
+
whiskers: ["minmax", "tukey"],
|
|
534
|
+
bins: "integer"
|
|
535
|
+
},
|
|
536
|
+
layout: {
|
|
537
|
+
widthRem: "integer",
|
|
538
|
+
heightRem: "integer",
|
|
539
|
+
minWidthRem: "integer",
|
|
540
|
+
minHeightRem: "integer"
|
|
541
|
+
}
|
|
542
|
+
};
|
|
448
543
|
function normalizeMetricOptions(raw) {
|
|
449
544
|
const out = {}, diagnostics = [];
|
|
450
545
|
const record2 = (x) => !!x && typeof x === "object" && !Array.isArray(x);
|
|
@@ -461,25 +556,21 @@ function normalizeMetricOptions(raw) {
|
|
|
461
556
|
};
|
|
462
557
|
Object.assign(
|
|
463
558
|
out,
|
|
464
|
-
copy(raw,
|
|
465
|
-
expression: "string",
|
|
466
|
-
expressionY: "string",
|
|
467
|
-
scope: ["allMatching", "shownRows"],
|
|
468
|
-
groupLimit: "integer",
|
|
469
|
-
minCount: "positiveInteger"
|
|
470
|
-
})
|
|
559
|
+
copy(raw, METRIC_OPTION_SPECS.top)
|
|
471
560
|
);
|
|
561
|
+
if (raw.window !== void 0) {
|
|
562
|
+
if (record2(raw.window)) {
|
|
563
|
+
const w = copy(raw.window, METRIC_OPTION_SPECS.window);
|
|
564
|
+
const periods = raw.window.periods;
|
|
565
|
+
if (periods === "all" || Number.isSafeInteger(periods) && periods >= 0 && periods <= 1e6)
|
|
566
|
+
w.periods = periods;
|
|
567
|
+
else diagnostics.push("Invalid metric window.");
|
|
568
|
+
out.window = w;
|
|
569
|
+
} else diagnostics.push("Invalid metric window.");
|
|
570
|
+
}
|
|
472
571
|
if (raw.dateGrouping !== void 0) {
|
|
473
572
|
if (record2(raw.dateGrouping)) {
|
|
474
|
-
const d = copy(raw.dateGrouping,
|
|
475
|
-
field: "string",
|
|
476
|
-
interval: ["hour", "day", "week", "month", "year"],
|
|
477
|
-
timeZone: "string",
|
|
478
|
-
includeEmpty: "boolean",
|
|
479
|
-
range: ["query", "observed", "custom"],
|
|
480
|
-
start: "string",
|
|
481
|
-
end: "string"
|
|
482
|
-
});
|
|
573
|
+
const d = copy(raw.dateGrouping, METRIC_OPTION_SPECS.dateGrouping);
|
|
483
574
|
if (typeof d.field !== "string" || !d.field || d.field.length > 256 || !d.interval || typeof d.includeEmpty !== "boolean" || !d.range)
|
|
484
575
|
diagnostics.push("Invalid metric date grouping.");
|
|
485
576
|
out.dateGrouping = d;
|
|
@@ -489,94 +580,30 @@ function normalizeMetricOptions(raw) {
|
|
|
489
580
|
if (record2(raw.display)) {
|
|
490
581
|
if (typeof raw.display.kind !== "string")
|
|
491
582
|
diagnostics.push("Invalid metric display kind.");
|
|
492
|
-
const d = copy(raw.display,
|
|
493
|
-
kind: [
|
|
494
|
-
"auto",
|
|
495
|
-
"value",
|
|
496
|
-
"table",
|
|
497
|
-
"list",
|
|
498
|
-
"bar-horizontal",
|
|
499
|
-
"bar-vertical",
|
|
500
|
-
"line",
|
|
501
|
-
"pie",
|
|
502
|
-
"donut",
|
|
503
|
-
"scatter",
|
|
504
|
-
"box",
|
|
505
|
-
"histogram"
|
|
506
|
-
],
|
|
507
|
-
valueLabel: "string",
|
|
508
|
-
xLabel: "string",
|
|
509
|
-
yLabel: "string",
|
|
510
|
-
legendPosition: ["left", "right", "top", "bottom", "none"],
|
|
511
|
-
maxGroups: "positiveInteger",
|
|
512
|
-
showOther: "boolean",
|
|
513
|
-
boxMean: "boolean"
|
|
514
|
-
});
|
|
583
|
+
const d = copy(raw.display, METRIC_OPTION_SPECS.display);
|
|
515
584
|
for (const k of ["xScale", "yScale"]) {
|
|
516
585
|
if (record2(raw.display[k]))
|
|
517
|
-
d[k] = copy(raw.display[k],
|
|
518
|
-
mode: ["linear", "log"],
|
|
519
|
-
min: "number",
|
|
520
|
-
max: "number"
|
|
521
|
-
});
|
|
586
|
+
d[k] = copy(raw.display[k], METRIC_OPTION_SPECS.scale);
|
|
522
587
|
else if (raw.display[k] !== void 0)
|
|
523
588
|
diagnostics.push(`Invalid metric ${k}.`);
|
|
524
589
|
}
|
|
525
590
|
for (const k of ["format", "xFormat", "yFormat"])
|
|
526
591
|
if (record2(raw.display[k]))
|
|
527
|
-
d[k] = copy(raw.display[k],
|
|
528
|
-
kind: ["number", "percent", "duration", "date", "time", "datetime"],
|
|
529
|
-
decimals: "integer",
|
|
530
|
-
sourceUnit: [
|
|
531
|
-
"milliseconds",
|
|
532
|
-
"seconds",
|
|
533
|
-
"minutes",
|
|
534
|
-
"hours",
|
|
535
|
-
"days",
|
|
536
|
-
"iso",
|
|
537
|
-
"epochSeconds",
|
|
538
|
-
"epochMilliseconds",
|
|
539
|
-
"secondsOfDay",
|
|
540
|
-
"millisecondsOfDay"
|
|
541
|
-
],
|
|
542
|
-
style: ["human", "long", "clock", "custom", "iso"],
|
|
543
|
-
pattern: "string",
|
|
544
|
-
timeZone: "string"
|
|
545
|
-
});
|
|
592
|
+
d[k] = copy(raw.display[k], METRIC_OPTION_SPECS.format);
|
|
546
593
|
if (record2(raw.display.list))
|
|
547
|
-
d.list = copy(raw.display.list,
|
|
548
|
-
showBars: "boolean",
|
|
549
|
-
useGroupColors: "boolean",
|
|
550
|
-
showValues: "boolean",
|
|
551
|
-
barPosition: ["inline", "below"],
|
|
552
|
-
maxLabelLines: "positiveInteger"
|
|
553
|
-
});
|
|
594
|
+
d.list = copy(raw.display.list, METRIC_OPTION_SPECS.list);
|
|
554
595
|
if (record2(raw.display.pivot))
|
|
555
|
-
d.pivot = copy(raw.display.pivot,
|
|
556
|
-
swap: "boolean",
|
|
557
|
-
rowDir: ["asc", "desc"],
|
|
558
|
-
columnDir: ["asc", "desc"]
|
|
559
|
-
});
|
|
596
|
+
d.pivot = copy(raw.display.pivot, METRIC_OPTION_SPECS.pivot);
|
|
560
597
|
out.display = { kind: "auto", ...d };
|
|
561
598
|
} else diagnostics.push("Invalid metric display.");
|
|
562
599
|
}
|
|
563
600
|
if (raw.distribution !== void 0) {
|
|
564
601
|
if (record2(raw.distribution))
|
|
565
|
-
out.distribution = copy(raw.distribution,
|
|
566
|
-
kind: ["box", "histogram"],
|
|
567
|
-
input: "string",
|
|
568
|
-
whiskers: ["minmax", "tukey"],
|
|
569
|
-
bins: "integer"
|
|
570
|
-
});
|
|
602
|
+
out.distribution = copy(raw.distribution, METRIC_OPTION_SPECS.distribution);
|
|
571
603
|
else diagnostics.push("Invalid metric distribution.");
|
|
572
604
|
}
|
|
573
605
|
if (record2(raw.layout)) {
|
|
574
|
-
const l = copy(raw.layout,
|
|
575
|
-
widthRem: "integer",
|
|
576
|
-
heightRem: "integer",
|
|
577
|
-
minWidthRem: "integer",
|
|
578
|
-
minHeightRem: "integer"
|
|
579
|
-
});
|
|
606
|
+
const l = copy(raw.layout, METRIC_OPTION_SPECS.layout);
|
|
580
607
|
for (const k of ["widthRem", "heightRem", "minWidthRem", "minHeightRem"])
|
|
581
608
|
l[k] = Math.max(1, Math.min(1e3, Number(l[k] ?? 1)));
|
|
582
609
|
l.widthRem = Math.max(Number(l.widthRem), Number(l.minWidthRem));
|
|
@@ -684,7 +711,7 @@ function cloneWhereTerm(term) {
|
|
|
684
711
|
function normalizeSetFilter(item) {
|
|
685
712
|
if (!isRecord(item)) return void 0;
|
|
686
713
|
const id = boundedString(item.id, MAX_FIELD_NAME_LENGTH);
|
|
687
|
-
if (!id || !["any", "all", "none", "empty"].includes(
|
|
714
|
+
if (!id || typeof item.mode !== "string" || !["any", "all", "none", "empty"].includes(item.mode) || !Array.isArray(item.values)) return void 0;
|
|
688
715
|
if (item.values.length > MAX_WHERE_CLAUSES || !item.values.every((value) => typeof value === "string" && value.length <= MAX_FILTER_VALUE_LENGTH)) return void 0;
|
|
689
716
|
return { id, mode: item.mode, values: [...item.values] };
|
|
690
717
|
}
|
|
@@ -1251,7 +1278,7 @@ function toBase64Url(json) {
|
|
|
1251
1278
|
}
|
|
1252
1279
|
function fromBase64Url(s) {
|
|
1253
1280
|
const b64 = s.replace(/-/g, "+").replace(/_/g, "/") + "===".slice((s.length + 3) % 4);
|
|
1254
|
-
if (typeof Buffer !== "undefined") return Buffer.from(b64, "base64").toString("utf8");
|
|
1281
|
+
if (typeof Buffer !== "undefined") return Buffer.from(b64, "base64").toString("utf8").replace(/^\uFEFF/, "");
|
|
1255
1282
|
const bin = atob(b64);
|
|
1256
1283
|
const bytes = Uint8Array.from(bin, (c) => c.charCodeAt(0));
|
|
1257
1284
|
return new TextDecoder().decode(bytes);
|
|
@@ -3066,32 +3093,661 @@ function formulaRuntime(ast, inputs, extension) {
|
|
|
3066
3093
|
out = Math.trunc((date(c).getTime() - date(b).getTime()) / ms);
|
|
3067
3094
|
break;
|
|
3068
3095
|
}
|
|
3069
|
-
case "FORMAT_DATE": {
|
|
3070
|
-
const d = date(a), pad = (n2) => String(n2).padStart(2, "0"), parts2 = {
|
|
3071
|
-
YYYY: String(d.getUTCFullYear()).padStart(4, "0"),
|
|
3072
|
-
MM: pad(d.getUTCMonth() + 1),
|
|
3073
|
-
DD: pad(d.getUTCDate()),
|
|
3074
|
-
HH: pad(d.getUTCHours()),
|
|
3075
|
-
mm: pad(d.getUTCMinutes()),
|
|
3076
|
-
ss: pad(d.getUTCSeconds())
|
|
3077
|
-
};
|
|
3078
|
-
out = str(b).replace(/YYYY|MM|DD|HH|mm|ss/g, (t) => parts2[t]);
|
|
3079
|
-
break;
|
|
3096
|
+
case "FORMAT_DATE": {
|
|
3097
|
+
const d = date(a), pad = (n2) => String(n2).padStart(2, "0"), parts2 = {
|
|
3098
|
+
YYYY: String(d.getUTCFullYear()).padStart(4, "0"),
|
|
3099
|
+
MM: pad(d.getUTCMonth() + 1),
|
|
3100
|
+
DD: pad(d.getUTCDate()),
|
|
3101
|
+
HH: pad(d.getUTCHours()),
|
|
3102
|
+
mm: pad(d.getUTCMinutes()),
|
|
3103
|
+
ss: pad(d.getUTCSeconds())
|
|
3104
|
+
};
|
|
3105
|
+
out = str(b).replace(/YYYY|MM|DD|HH|mm|ss/g, (t) => parts2[t]);
|
|
3106
|
+
break;
|
|
3107
|
+
}
|
|
3108
|
+
default:
|
|
3109
|
+
return fail(`Unknown operation: ${name}`);
|
|
3110
|
+
}
|
|
3111
|
+
return bound(out);
|
|
3112
|
+
}
|
|
3113
|
+
try {
|
|
3114
|
+
const value = run(ast);
|
|
3115
|
+
return inspection ? { value, regex: inspection } : { value };
|
|
3116
|
+
} catch (e) {
|
|
3117
|
+
return {
|
|
3118
|
+
value: null,
|
|
3119
|
+
error: e instanceof Error ? e.message : "Formula evaluation failed."
|
|
3120
|
+
};
|
|
3121
|
+
}
|
|
3122
|
+
}
|
|
3123
|
+
|
|
3124
|
+
// src/metricDates.ts
|
|
3125
|
+
import { Temporal } from "temporal-polyfill";
|
|
3126
|
+
var METRIC_DATE_INTERVALS = [
|
|
3127
|
+
"hour",
|
|
3128
|
+
"day",
|
|
3129
|
+
"week",
|
|
3130
|
+
"month",
|
|
3131
|
+
"year"
|
|
3132
|
+
];
|
|
3133
|
+
function metricDateTime(value, timeZone = "UTC") {
|
|
3134
|
+
if (typeof value !== "string" || !/^\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?(?:Z|[+-]\d{2}:\d{2}))?$/.test(
|
|
3135
|
+
value
|
|
3136
|
+
))
|
|
3137
|
+
throw Error(
|
|
3138
|
+
"Date grouping requires ISO dates or timestamps with a timezone."
|
|
3139
|
+
);
|
|
3140
|
+
const time = Date.parse(value), month = Number(value.slice(5, 7)), day = Number(value.slice(8, 10));
|
|
3141
|
+
const calendar = /* @__PURE__ */ new Date(0);
|
|
3142
|
+
calendar.setUTCFullYear(Number(value.slice(0, 4)), month, 0);
|
|
3143
|
+
if (!Number.isFinite(time) || month < 1 || month > 12 || day < 1 || day > calendar.getUTCDate())
|
|
3144
|
+
throw Error("Invalid date grouping value.");
|
|
3145
|
+
return value.length === 10 && timeZone !== "UTC" ? Temporal.PlainDate.from(value).toZonedDateTime(timeZone).epochMilliseconds : time;
|
|
3146
|
+
}
|
|
3147
|
+
function metricOffsetTransition(start, end, timeZone) {
|
|
3148
|
+
if (end <= start) return void 0;
|
|
3149
|
+
const offset = (time) => Temporal.Instant.fromEpochMilliseconds(time).toZonedDateTimeISO(timeZone).offsetNanoseconds;
|
|
3150
|
+
const before = offset(start);
|
|
3151
|
+
if (before === offset(end)) return void 0;
|
|
3152
|
+
while (end - start > 1) {
|
|
3153
|
+
const middle = Math.floor((start + end) / 2);
|
|
3154
|
+
if (offset(middle) === before) start = middle;
|
|
3155
|
+
else end = middle;
|
|
3156
|
+
}
|
|
3157
|
+
return end;
|
|
3158
|
+
}
|
|
3159
|
+
function metricDateFloor(time, interval, timeZone = "UTC") {
|
|
3160
|
+
if (timeZone !== "UTC") {
|
|
3161
|
+
const zoned = Temporal.Instant.fromEpochMilliseconds(time).toZonedDateTimeISO(timeZone);
|
|
3162
|
+
if (interval === "hour") {
|
|
3163
|
+
const floor = time - zoned.minute * 6e4 - zoned.second * 1e3 - zoned.millisecond;
|
|
3164
|
+
return metricOffsetTransition(floor, time, timeZone) ?? floor;
|
|
3165
|
+
}
|
|
3166
|
+
let date2 = zoned.toPlainDate();
|
|
3167
|
+
if (interval === "week") date2 = date2.subtract({ days: date2.dayOfWeek - 1 });
|
|
3168
|
+
if (interval === "month" || interval === "year")
|
|
3169
|
+
date2 = date2.with({ day: 1 });
|
|
3170
|
+
if (interval === "year") date2 = date2.with({ month: 1 });
|
|
3171
|
+
return date2.toZonedDateTime(timeZone).epochMilliseconds;
|
|
3172
|
+
}
|
|
3173
|
+
const date = new Date(time);
|
|
3174
|
+
date.setUTCMinutes(0, 0, 0);
|
|
3175
|
+
if (interval !== "hour") date.setUTCHours(0);
|
|
3176
|
+
if (interval === "week")
|
|
3177
|
+
date.setUTCDate(date.getUTCDate() - (date.getUTCDay() + 6) % 7);
|
|
3178
|
+
if (interval === "month" || interval === "year") date.setUTCDate(1);
|
|
3179
|
+
if (interval === "year") date.setUTCMonth(0);
|
|
3180
|
+
return date.getTime();
|
|
3181
|
+
}
|
|
3182
|
+
function metricDateNext(time, interval, timeZone = "UTC") {
|
|
3183
|
+
if (timeZone !== "UTC") {
|
|
3184
|
+
const zoned = Temporal.Instant.fromEpochMilliseconds(time).toZonedDateTimeISO(timeZone);
|
|
3185
|
+
if (interval === "hour") {
|
|
3186
|
+
const target = zoned.toPlainDateTime().with({
|
|
3187
|
+
minute: 0,
|
|
3188
|
+
second: 0,
|
|
3189
|
+
millisecond: 0,
|
|
3190
|
+
microsecond: 0,
|
|
3191
|
+
nanosecond: 0
|
|
3192
|
+
}).add({ hours: 1 });
|
|
3193
|
+
let next = target.toZonedDateTime(timeZone).epochMilliseconds;
|
|
3194
|
+
if (next <= time)
|
|
3195
|
+
next = target.toZonedDateTime(timeZone, {
|
|
3196
|
+
disambiguation: "later"
|
|
3197
|
+
}).epochMilliseconds;
|
|
3198
|
+
return metricOffsetTransition(time, next, timeZone) ?? next;
|
|
3199
|
+
}
|
|
3200
|
+
let date2 = zoned.toPlainDate();
|
|
3201
|
+
if (interval === "month") date2 = date2.with({ day: 1 }).add({ months: 1 });
|
|
3202
|
+
else if (interval === "year")
|
|
3203
|
+
date2 = date2.with({ month: 1, day: 1 }).add({ years: 1 });
|
|
3204
|
+
else if (interval === "week")
|
|
3205
|
+
date2 = date2.subtract({ days: date2.dayOfWeek - 1 }).add({ weeks: 1 });
|
|
3206
|
+
else date2 = date2.add({ days: 1 });
|
|
3207
|
+
return date2.toZonedDateTime(timeZone).epochMilliseconds;
|
|
3208
|
+
}
|
|
3209
|
+
const date = new Date(time);
|
|
3210
|
+
if (interval === "hour") date.setUTCHours(date.getUTCHours() + 1);
|
|
3211
|
+
else if (interval === "month") date.setUTCMonth(date.getUTCMonth() + 1);
|
|
3212
|
+
else if (interval === "year") date.setUTCFullYear(date.getUTCFullYear() + 1);
|
|
3213
|
+
else date.setUTCDate(date.getUTCDate() + (interval === "week" ? 7 : 1));
|
|
3214
|
+
return date.getTime();
|
|
3215
|
+
}
|
|
3216
|
+
function metricCustomEndTime(value, timeZone = "UTC") {
|
|
3217
|
+
const time = metricDateTime(value, timeZone);
|
|
3218
|
+
return value.length === 10 ? metricDateNext(time, "day", timeZone) - 1 : time;
|
|
3219
|
+
}
|
|
3220
|
+
function validateDateGrouping(config) {
|
|
3221
|
+
if (typeof config.field !== "string" || !config.field || config.field.length > 256 || !METRIC_DATE_INTERVALS.includes(config.interval) || typeof config.includeEmpty !== "boolean" || !["query", "observed", "custom"].includes(config.range) || config.partialIntervals !== void 0 && !["include", "exclude"].includes(config.partialIntervals))
|
|
3222
|
+
throw Error("Invalid date grouping definition.");
|
|
3223
|
+
if (config.timeZone !== void 0) {
|
|
3224
|
+
if (typeof config.timeZone !== "string" || !config.timeZone || config.timeZone.length > 80 || /^[+-]/.test(config.timeZone))
|
|
3225
|
+
throw Error(
|
|
3226
|
+
"Use a valid IANA grouping timezone, such as America/Denver or UTC."
|
|
3227
|
+
);
|
|
3228
|
+
try {
|
|
3229
|
+
new Intl.DateTimeFormat("en-US", { timeZone: config.timeZone });
|
|
3230
|
+
} catch {
|
|
3231
|
+
throw Error(
|
|
3232
|
+
"Use a valid IANA grouping timezone, such as America/Denver or UTC."
|
|
3233
|
+
);
|
|
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
|
+
// src/metricPrecision.ts
|
|
3295
|
+
function assertMetricNumber(value) {
|
|
3296
|
+
if (!Number.isFinite(value) || Number.isInteger(value) && !Number.isSafeInteger(value)) {
|
|
3297
|
+
throw Error("Aggregate exceeds finite safe numeric precision.");
|
|
3298
|
+
}
|
|
3299
|
+
}
|
|
3300
|
+
function checkedMetricResult(result) {
|
|
3301
|
+
if (result.error || typeof result.value !== "number") return result;
|
|
3302
|
+
try {
|
|
3303
|
+
assertMetricNumber(result.value);
|
|
3304
|
+
return result;
|
|
3305
|
+
} catch (e) {
|
|
3306
|
+
return { value: null, error: e.message };
|
|
3307
|
+
}
|
|
3308
|
+
}
|
|
3309
|
+
|
|
3310
|
+
// src/metricSeries.ts
|
|
3311
|
+
var MAX_METRIC_WINDOW_PERIODS = 1e3;
|
|
3312
|
+
var MERGEABLE = ["SUM", "COUNT", "MIN", "MAX", "AVG"];
|
|
3313
|
+
function hasMetricSeries(c) {
|
|
3314
|
+
return c.window !== void 0 || c.share !== void 0 || c.dateGrouping?.partialIntervals !== void 0;
|
|
3315
|
+
}
|
|
3316
|
+
var soleAggregate = (plan) => plan.ast.kind === "call" && plan.aggregates.includes(plan.ast) ? plan.ast.name : void 0;
|
|
3317
|
+
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);
|
|
3318
|
+
function validateMetricSeries(c, plan) {
|
|
3319
|
+
const { window, share } = c;
|
|
3320
|
+
if (!window && share === void 0) return;
|
|
3321
|
+
if (!("aggregates" in plan) || c.display?.kind === "scatter")
|
|
3322
|
+
throw new FormulaError(
|
|
3323
|
+
"Windows and shares apply to single-value metrics, not distributions or scatters."
|
|
3324
|
+
);
|
|
3325
|
+
const metric = plan, sole = soleAggregate(metric);
|
|
3326
|
+
if (window) {
|
|
3327
|
+
if (!c.dateGrouping)
|
|
3328
|
+
throw new FormulaError(
|
|
3329
|
+
"A window needs a calendar interval on the first grouping field."
|
|
3330
|
+
);
|
|
3331
|
+
if (!c.dateGrouping.includeEmpty)
|
|
3332
|
+
throw new FormulaError(
|
|
3333
|
+
"A window spans every interval, including empty ones: include empty dates."
|
|
3334
|
+
);
|
|
3335
|
+
const { periods, reduce = "total", partial = "omit" } = window;
|
|
3336
|
+
if (periods !== "all" && !(Number.isInteger(periods) && periods >= 2 && periods <= MAX_METRIC_WINDOW_PERIODS))
|
|
3337
|
+
throw new FormulaError(
|
|
3338
|
+
"A window covers 2\u20131,000 intervals, or all of them."
|
|
3339
|
+
);
|
|
3340
|
+
if (!["total", "mean"].includes(reduce) || !["omit", "include"].includes(partial))
|
|
3341
|
+
throw new FormulaError("Invalid metric window.");
|
|
3342
|
+
if (reduce === "mean") {
|
|
3343
|
+
if (!sole || !["SUM", "COUNT", "COUNT_DISTINCT"].includes(sole))
|
|
3344
|
+
throw new FormulaError(
|
|
3345
|
+
"A mean per interval needs a single SUM, COUNT or COUNT_DISTINCT; it would weight each interval's ratio or average equally. A window total pools them instead."
|
|
3346
|
+
);
|
|
3347
|
+
} else
|
|
3348
|
+
for (const node of metric.aggregates)
|
|
3349
|
+
if (!MERGEABLE.includes(node.name))
|
|
3350
|
+
throw new FormulaError(
|
|
3351
|
+
node.name === "COUNT_DISTINCT" ? "A window total cannot combine COUNT_DISTINCT across intervals, because one value can occur in several. Use a mean per interval." : `A window total cannot combine ${node.name} across intervals.`,
|
|
3352
|
+
node.from,
|
|
3353
|
+
node.to
|
|
3354
|
+
);
|
|
3355
|
+
}
|
|
3356
|
+
if (share !== void 0) {
|
|
3357
|
+
if (share !== "total" && share !== "group0")
|
|
3358
|
+
throw new FormulaError("Invalid metric share.");
|
|
3359
|
+
if (sole !== "SUM" && sole !== "COUNT")
|
|
3360
|
+
throw new FormulaError(
|
|
3361
|
+
"A share divides additive totals: use a single SUM or COUNT. Distinct counts, averages and ratios do not add up across groups."
|
|
3362
|
+
);
|
|
3363
|
+
if (share === "group0" ? c.groupBy.length < 2 : !c.groupBy.length)
|
|
3364
|
+
throw new FormulaError(
|
|
3365
|
+
share === "group0" ? "A share within the first grouping needs a second grouping field." : "A share of the total needs a grouping field."
|
|
3366
|
+
);
|
|
3367
|
+
if (share === "total" && window)
|
|
3368
|
+
throw new FormulaError(
|
|
3369
|
+
"A windowed share divides by each interval's own total: share within the first grouping."
|
|
3370
|
+
);
|
|
3371
|
+
}
|
|
3372
|
+
if (partBudget(c, metric) > MAX_AGGREGATIONS)
|
|
3373
|
+
throw new FormulaError(
|
|
3374
|
+
`A window or share runs one metric per aggregate; this one needs more than ${MAX_AGGREGATIONS}.`
|
|
3375
|
+
);
|
|
3376
|
+
}
|
|
3377
|
+
function wholeIntervalSpan(c, query, schema, now) {
|
|
3378
|
+
const { field, interval, timeZone } = c.dateGrouping;
|
|
3379
|
+
const canonical = (name) => resolveFieldName(schema, name) ?? name;
|
|
3380
|
+
let lower = -Infinity, upper = Infinity;
|
|
3381
|
+
for (const term of [...query.where, ...c.where ?? []]) {
|
|
3382
|
+
if (isOrGroup(term) || term.negated || canonical(term.field) !== canonical(field) || !["=", ">", ">=", "<", "<="].includes(term.op))
|
|
3383
|
+
continue;
|
|
3384
|
+
if (term.value === "") continue;
|
|
3385
|
+
let time;
|
|
3386
|
+
try {
|
|
3387
|
+
time = Number(coerceValue("datetime", term.value, now));
|
|
3388
|
+
} catch {
|
|
3389
|
+
continue;
|
|
3390
|
+
}
|
|
3391
|
+
if (!Number.isFinite(time)) continue;
|
|
3392
|
+
if (term.op === ">" || term.op === ">=" || term.op === "=")
|
|
3393
|
+
lower = Math.max(lower, time + (term.op === ">" ? 1 : 0));
|
|
3394
|
+
if (term.op === "<" || term.op === "<=" || term.op === "=")
|
|
3395
|
+
upper = Math.min(upper, time + (term.op === "<" ? 0 : 1));
|
|
3396
|
+
}
|
|
3397
|
+
const floor = (time) => metricDateFloor(time, interval, timeZone);
|
|
3398
|
+
const end = floor(Math.min(upper, now));
|
|
3399
|
+
if (!Number.isFinite(lower)) return { end };
|
|
3400
|
+
const first = floor(lower);
|
|
3401
|
+
return {
|
|
3402
|
+
start: first === lower ? lower : metricDateNext(first, interval, timeZone),
|
|
3403
|
+
end
|
|
3404
|
+
};
|
|
3405
|
+
}
|
|
3406
|
+
function planMetricSeries(query, schema, options2 = {}) {
|
|
3407
|
+
const clauses = query.aggregations ?? [];
|
|
3408
|
+
if (!clauses.some(hasMetricSeries)) return void 0;
|
|
3409
|
+
const now = options2.now ?? Date.now();
|
|
3410
|
+
const taken = new Set(clauses.map((c) => c.id));
|
|
3411
|
+
let next = 0;
|
|
3412
|
+
const id = () => {
|
|
3413
|
+
let value;
|
|
3414
|
+
do
|
|
3415
|
+
value = `qt-series-${next++}`;
|
|
3416
|
+
while (taken.has(value));
|
|
3417
|
+
return value;
|
|
3418
|
+
};
|
|
3419
|
+
const prepared = clauses.map((c) => {
|
|
3420
|
+
if (!hasMetricSeries(c)) return { kind: "plain", clause: c, parts: [c] };
|
|
3421
|
+
try {
|
|
3422
|
+
return prepare(c);
|
|
3423
|
+
} catch (e) {
|
|
3424
|
+
return {
|
|
3425
|
+
kind: "error",
|
|
3426
|
+
clause: c,
|
|
3427
|
+
error: e instanceof Error ? e.message : String(e)
|
|
3428
|
+
};
|
|
3429
|
+
}
|
|
3430
|
+
});
|
|
3431
|
+
function prepare(c) {
|
|
3432
|
+
const issue = validateMetric(c, schema, options2.resolveComputed)[0];
|
|
3433
|
+
if (issue) return { kind: "error", clause: c, error: issue.message };
|
|
3434
|
+
let where = c.where ?? [];
|
|
3435
|
+
let dateGrouping;
|
|
3436
|
+
if (c.dateGrouping) {
|
|
3437
|
+
const { partialIntervals, ...config } = c.dateGrouping;
|
|
3438
|
+
dateGrouping = config;
|
|
3439
|
+
if (partialIntervals === "exclude") {
|
|
3440
|
+
const span = wholeIntervalSpan(c, query, schema, now);
|
|
3441
|
+
if (span.start !== void 0 && span.end <= span.start)
|
|
3442
|
+
return { kind: "empty", clause: c };
|
|
3443
|
+
const field = resolveFieldName(schema, config.field) ?? config.field;
|
|
3444
|
+
const iso = (time) => new Date(time).toISOString();
|
|
3445
|
+
where = [
|
|
3446
|
+
...where,
|
|
3447
|
+
...span.start !== void 0 ? [{ field, op: ">=", value: iso(span.start) }] : [],
|
|
3448
|
+
{ field, op: "<", value: iso(span.end) }
|
|
3449
|
+
];
|
|
3450
|
+
if (config.includeEmpty && config.range !== "observed") {
|
|
3451
|
+
const floor = (time) => metricDateFloor(time, config.interval, config.timeZone);
|
|
3452
|
+
let from = span.start, through = span.end - 1;
|
|
3453
|
+
if (config.range === "custom") {
|
|
3454
|
+
const start = floor(
|
|
3455
|
+
metricDateTime(config.start ?? "", config.timeZone)
|
|
3456
|
+
);
|
|
3457
|
+
from = Math.max(from ?? start, start);
|
|
3458
|
+
through = Math.min(
|
|
3459
|
+
through,
|
|
3460
|
+
metricCustomEndTime(config.end ?? "", config.timeZone)
|
|
3461
|
+
);
|
|
3462
|
+
}
|
|
3463
|
+
if (from !== void 0) {
|
|
3464
|
+
if (through < from) return { kind: "empty", clause: c };
|
|
3465
|
+
dateGrouping = {
|
|
3466
|
+
...config,
|
|
3467
|
+
range: "custom",
|
|
3468
|
+
start: iso(from),
|
|
3469
|
+
end: iso(through)
|
|
3470
|
+
};
|
|
3471
|
+
}
|
|
3472
|
+
}
|
|
3473
|
+
}
|
|
3474
|
+
}
|
|
3475
|
+
const scoped = (groupBy) => ({
|
|
3476
|
+
...c.scope ? { scope: c.scope } : {},
|
|
3477
|
+
...where.length ? { where } : {},
|
|
3478
|
+
...dateGrouping && groupBy.length ? { dateGrouping } : {}
|
|
3479
|
+
});
|
|
3480
|
+
if (!c.window && c.share === void 0) {
|
|
3481
|
+
const {
|
|
3482
|
+
dateGrouping: _date,
|
|
3483
|
+
where: _where,
|
|
3484
|
+
scope: _scope,
|
|
3485
|
+
...rest
|
|
3486
|
+
} = c;
|
|
3487
|
+
return {
|
|
3488
|
+
kind: "plain",
|
|
3489
|
+
clause: c,
|
|
3490
|
+
parts: [{ ...rest, id: id(), ...scoped(c.groupBy) }]
|
|
3491
|
+
};
|
|
3492
|
+
}
|
|
3493
|
+
const source = metricExpression(
|
|
3494
|
+
c.expression === void 0 && c.field ? { ...c, field: resolveFieldName(schema, c.field) ?? c.field } : c
|
|
3495
|
+
);
|
|
3496
|
+
const plan = compileMetric(source, schema, options2.resolveComputed);
|
|
3497
|
+
const texts = [];
|
|
3498
|
+
const at = (text) => {
|
|
3499
|
+
const index = texts.indexOf(text);
|
|
3500
|
+
return index < 0 ? texts.push(text) - 1 : index;
|
|
3501
|
+
};
|
|
3502
|
+
let reductions;
|
|
3503
|
+
if (c.window && (c.window.reduce ?? "total") === "total")
|
|
3504
|
+
reductions = plan.aggregates.map((node) => {
|
|
3505
|
+
const text = source.slice(node.from, node.to);
|
|
3506
|
+
if (node.name !== "AVG") return { name: node.name, part: at(text) };
|
|
3507
|
+
const input = text.slice(text.indexOf("(") + 1, text.lastIndexOf(")"));
|
|
3508
|
+
return { name: "AVG", sum: at(`SUM(${input})`), count: at(`COUNT(${input})`) };
|
|
3509
|
+
});
|
|
3510
|
+
else at(source);
|
|
3511
|
+
const parts = (groupBy) => texts.map(
|
|
3512
|
+
(text) => ({
|
|
3513
|
+
id: id(),
|
|
3514
|
+
// The whole legacy measure keeps its operation, so v1 servers run it.
|
|
3515
|
+
...text === source && c.expression === void 0 ? { op: c.op, ...c.field ? { field: c.field } : {} } : { op: "count", expression: text },
|
|
3516
|
+
groupBy,
|
|
3517
|
+
...scoped(groupBy)
|
|
3518
|
+
})
|
|
3519
|
+
);
|
|
3520
|
+
return {
|
|
3521
|
+
kind: "series",
|
|
3522
|
+
clause: c,
|
|
3523
|
+
plan,
|
|
3524
|
+
parts: [
|
|
3525
|
+
...parts(c.groupBy),
|
|
3526
|
+
...c.share === "group0" ? parts(c.groupBy.slice(0, 1)) : c.share === "total" ? parts([]) : []
|
|
3527
|
+
],
|
|
3528
|
+
...reductions ? { reductions } : {},
|
|
3529
|
+
size: texts.length
|
|
3530
|
+
};
|
|
3531
|
+
}
|
|
3532
|
+
const queries = [];
|
|
3533
|
+
let batch = [];
|
|
3534
|
+
for (const item of prepared) {
|
|
3535
|
+
if (item.kind === "error" || item.kind === "empty") continue;
|
|
3536
|
+
if (batch.length + item.parts.length > MAX_AGGREGATIONS) {
|
|
3537
|
+
queries.push({ ...query, aggregations: batch });
|
|
3538
|
+
batch = [];
|
|
3539
|
+
}
|
|
3540
|
+
batch.push(...item.parts);
|
|
3541
|
+
}
|
|
3542
|
+
if (batch.length) queries.push({ ...query, aggregations: batch });
|
|
3543
|
+
return {
|
|
3544
|
+
queries,
|
|
3545
|
+
combine(results) {
|
|
3546
|
+
const byId = new Map(results.map((r) => [r.id, r]));
|
|
3547
|
+
return prepared.map((item) => {
|
|
3548
|
+
const c = item.clause;
|
|
3549
|
+
if (item.kind === "error")
|
|
3550
|
+
return { id: c.id, buckets: [], error: item.error };
|
|
3551
|
+
if (item.kind === "empty")
|
|
3552
|
+
return {
|
|
3553
|
+
id: c.id,
|
|
3554
|
+
buckets: [],
|
|
3555
|
+
scope: c.scope ?? "allMatching",
|
|
3556
|
+
groupCount: 0,
|
|
3557
|
+
coverage: "exact"
|
|
3558
|
+
};
|
|
3559
|
+
const entries = item.parts.map(
|
|
3560
|
+
(part) => byId.get(part.id) ?? {
|
|
3561
|
+
id: part.id,
|
|
3562
|
+
buckets: [],
|
|
3563
|
+
error: "Server omitted this metric."
|
|
3564
|
+
}
|
|
3565
|
+
);
|
|
3566
|
+
if (item.kind === "plain") return { ...entries[0], id: c.id };
|
|
3567
|
+
try {
|
|
3568
|
+
return combineSeries(item, entries);
|
|
3569
|
+
} catch (e) {
|
|
3570
|
+
return {
|
|
3571
|
+
id: c.id,
|
|
3572
|
+
buckets: [],
|
|
3573
|
+
error: e instanceof Error ? e.message : String(e)
|
|
3574
|
+
};
|
|
3575
|
+
}
|
|
3576
|
+
});
|
|
3577
|
+
}
|
|
3578
|
+
};
|
|
3579
|
+
}
|
|
3580
|
+
var Accumulator = class {
|
|
3581
|
+
constructor(name) {
|
|
3582
|
+
this.name = name;
|
|
3583
|
+
this.total = 0;
|
|
3584
|
+
this.seen = false;
|
|
3585
|
+
this.best = null;
|
|
3586
|
+
}
|
|
3587
|
+
add(bucket) {
|
|
3588
|
+
if (!bucket || this.error) return;
|
|
3589
|
+
if (bucket.error) {
|
|
3590
|
+
this.error = bucket.error;
|
|
3591
|
+
return;
|
|
3592
|
+
}
|
|
3593
|
+
const value = bucket.value;
|
|
3594
|
+
if (value === null) return;
|
|
3595
|
+
if (this.name === "SUM" || this.name === "COUNT") {
|
|
3596
|
+
if (typeof value !== "number") {
|
|
3597
|
+
this.error = `${this.name} returned a non-numeric value.`;
|
|
3598
|
+
return;
|
|
3599
|
+
}
|
|
3600
|
+
this.total += value;
|
|
3601
|
+
this.seen = true;
|
|
3602
|
+
} else if (this.best === null || compareMetricValues(value, this.best) * (this.name === "MIN" ? 1 : -1) < 0)
|
|
3603
|
+
this.best = value;
|
|
3604
|
+
}
|
|
3605
|
+
result() {
|
|
3606
|
+
if (this.error) return { value: null, error: this.error };
|
|
3607
|
+
if (this.name === "MIN" || this.name === "MAX") return { value: this.best };
|
|
3608
|
+
return { value: this.name === "COUNT" || this.seen ? this.total : null };
|
|
3609
|
+
}
|
|
3610
|
+
};
|
|
3611
|
+
function combineSeries(item, entries) {
|
|
3612
|
+
const c = item.clause, window = c.window;
|
|
3613
|
+
for (const entry of entries) {
|
|
3614
|
+
if (entry.error) throw Error(entry.error);
|
|
3615
|
+
if (entry.coverage === "partial" || (entry.groupCount ?? entry.buckets.length) > entry.buckets.length)
|
|
3616
|
+
throw Error(
|
|
3617
|
+
"A window or share needs every group, but the engine returned only some of them."
|
|
3618
|
+
);
|
|
3619
|
+
}
|
|
3620
|
+
const numerator = entries.slice(0, item.size), denominator = c.share ? entries.slice(item.size) : [];
|
|
3621
|
+
const dateOf = (keys) => keys[0] === null || keys[0] === void 0 ? void 0 : keys[0];
|
|
3622
|
+
const times = /* @__PURE__ */ new Map();
|
|
3623
|
+
if (window)
|
|
3624
|
+
for (const entry of entries)
|
|
3625
|
+
for (const bucket of entry.buckets) {
|
|
3626
|
+
const date = dateOf(bucket.keys);
|
|
3627
|
+
if (date !== void 0 && !times.has(date))
|
|
3628
|
+
times.set(date, metricDateTime(date));
|
|
3080
3629
|
}
|
|
3081
|
-
|
|
3082
|
-
|
|
3630
|
+
const order = [...times.entries()].sort((a, b) => a[1] - b[1]);
|
|
3631
|
+
const position = new Map(order.map(([date], index) => [date, index]));
|
|
3632
|
+
const positionOf = (keys) => {
|
|
3633
|
+
const date = dateOf(keys);
|
|
3634
|
+
return date === void 0 ? void 0 : position.get(date);
|
|
3635
|
+
};
|
|
3636
|
+
const tracks = (part) => {
|
|
3637
|
+
const byTail = /* @__PURE__ */ new Map();
|
|
3638
|
+
for (const bucket of part.buckets) {
|
|
3639
|
+
const at = positionOf(bucket.keys);
|
|
3640
|
+
if (at === void 0) continue;
|
|
3641
|
+
const tail = JSON.stringify(bucket.keys.slice(1));
|
|
3642
|
+
const track = byTail.get(tail) ?? Array(order.length);
|
|
3643
|
+
track[at] = bucket;
|
|
3644
|
+
byTail.set(tail, track);
|
|
3645
|
+
}
|
|
3646
|
+
return byTail;
|
|
3647
|
+
};
|
|
3648
|
+
const exact = (part) => new Map(
|
|
3649
|
+
part.buckets.map((bucket) => [JSON.stringify(bucket.keys), bucket])
|
|
3650
|
+
);
|
|
3651
|
+
const numeratorTracks = numerator.map(tracks), denominatorTracks = denominator.map(tracks);
|
|
3652
|
+
const numeratorBuckets = numerator.map(exact), denominatorBuckets = denominator.map(exact);
|
|
3653
|
+
const checked = (result, count) => {
|
|
3654
|
+
if (!result.error && typeof result.value === "number")
|
|
3655
|
+
try {
|
|
3656
|
+
assertMetricNumber(result.value);
|
|
3657
|
+
} catch (e) {
|
|
3658
|
+
result = { value: null, error: e.message };
|
|
3659
|
+
}
|
|
3660
|
+
return result.error ? { value: null, count, error: result.error } : { value: result.value, count };
|
|
3661
|
+
};
|
|
3662
|
+
const evaluate = (members, mean) => {
|
|
3663
|
+
const own = members[0], count = own.reduce((n, bucket) => n + (bucket?.count ?? 0), 0);
|
|
3664
|
+
if (item.reductions) {
|
|
3665
|
+
const reduce = (name, part) => {
|
|
3666
|
+
const accumulator = new Accumulator(name);
|
|
3667
|
+
for (const bucket of members[part]) accumulator.add(bucket);
|
|
3668
|
+
return accumulator.result();
|
|
3669
|
+
};
|
|
3670
|
+
const reductions = item.reductions.map((r) => {
|
|
3671
|
+
if (r.name !== "AVG") return reduce(r.name, r.part);
|
|
3672
|
+
const sum = reduce("SUM", r.sum), rows = reduce("COUNT", r.count);
|
|
3673
|
+
if (sum.error || rows.error)
|
|
3674
|
+
return { value: null, error: sum.error ?? rows.error };
|
|
3675
|
+
return {
|
|
3676
|
+
value: sum.value === null || !rows.value ? null : Number(sum.value) / Number(rows.value)
|
|
3677
|
+
};
|
|
3678
|
+
});
|
|
3679
|
+
return checked(evaluateMetricReductions(item.plan, reductions), count);
|
|
3680
|
+
}
|
|
3681
|
+
if (!mean)
|
|
3682
|
+
return own[0]?.error ? { value: null, count, error: own[0].error } : checked({ value: own[0]?.value ?? null }, count);
|
|
3683
|
+
let total = 0;
|
|
3684
|
+
for (const bucket of own) {
|
|
3685
|
+
if (bucket?.error) return { value: null, count, error: bucket.error };
|
|
3686
|
+
if (bucket?.value === null || bucket?.value === void 0) continue;
|
|
3687
|
+
if (typeof bucket.value !== "number")
|
|
3688
|
+
return { value: null, count, error: "Expected a numeric metric value." };
|
|
3689
|
+
total += bucket.value;
|
|
3690
|
+
}
|
|
3691
|
+
return checked({ value: total / own.length }, count);
|
|
3692
|
+
};
|
|
3693
|
+
const periods = window?.periods === "all" ? Infinity : window?.periods ?? 1;
|
|
3694
|
+
const point = (keys, tracksOf, bucketsOf) => {
|
|
3695
|
+
const at = positionOf(keys);
|
|
3696
|
+
if (!window || at === void 0)
|
|
3697
|
+
return evaluate(
|
|
3698
|
+
bucketsOf.map((parts) => [parts.get(JSON.stringify(keys))]),
|
|
3699
|
+
false
|
|
3700
|
+
);
|
|
3701
|
+
const from = Math.max(0, at - periods + 1), intervals = at - from + 1, tail = JSON.stringify(keys.slice(1));
|
|
3702
|
+
const members = tracksOf.map((byTail) => {
|
|
3703
|
+
const track = byTail.get(tail);
|
|
3704
|
+
return Array.from({ length: intervals }, (_, i) => track?.[from + i]);
|
|
3705
|
+
});
|
|
3706
|
+
const result = evaluate(members, window.reduce === "mean");
|
|
3707
|
+
return intervals < periods && periods !== Infinity && (window.partial ?? "omit") === "omit" ? { value: null, count: result.count, intervals } : { ...result, intervals };
|
|
3708
|
+
};
|
|
3709
|
+
const keyed = /* @__PURE__ */ new Map();
|
|
3710
|
+
for (const entry of numerator)
|
|
3711
|
+
for (const bucket of entry.buckets)
|
|
3712
|
+
keyed.set(JSON.stringify(bucket.keys), bucket.keys);
|
|
3713
|
+
let buckets = [...keyed.values()].map((keys) => {
|
|
3714
|
+
const own = point(keys, numeratorTracks, numeratorBuckets);
|
|
3715
|
+
let { value } = own, error = own.error;
|
|
3716
|
+
if (c.share && !error) {
|
|
3717
|
+
const total = c.share === "group0" ? keys.slice(0, 1) : [];
|
|
3718
|
+
const whole = point(total, denominatorTracks, denominatorBuckets);
|
|
3719
|
+
error = whole.error;
|
|
3720
|
+
value = error || typeof value !== "number" || typeof whole.value !== "number" || whole.value === 0 ? null : value / whole.value;
|
|
3083
3721
|
}
|
|
3084
|
-
return bound(out);
|
|
3085
|
-
}
|
|
3086
|
-
try {
|
|
3087
|
-
const value = run(ast);
|
|
3088
|
-
return inspection ? { value, regex: inspection } : { value };
|
|
3089
|
-
} catch (e) {
|
|
3090
3722
|
return {
|
|
3091
|
-
|
|
3092
|
-
|
|
3723
|
+
keys,
|
|
3724
|
+
value: error ? null : value,
|
|
3725
|
+
count: own.count,
|
|
3726
|
+
...error ? { error } : {},
|
|
3727
|
+
...own.intervals !== void 0 ? { intervals: own.intervals } : {}
|
|
3093
3728
|
};
|
|
3729
|
+
});
|
|
3730
|
+
let hiddenGroups;
|
|
3731
|
+
if (c.minCount !== void 0) {
|
|
3732
|
+
const kept = buckets.filter(
|
|
3733
|
+
(b) => b.count === 0 || b.count >= c.minCount
|
|
3734
|
+
);
|
|
3735
|
+
hiddenGroups = buckets.length - kept.length;
|
|
3736
|
+
buckets = kept;
|
|
3094
3737
|
}
|
|
3738
|
+
sortMetricBuckets(buckets, c);
|
|
3739
|
+
const groupCount = buckets.length;
|
|
3740
|
+
if (c.groupLimit) buckets = buckets.slice(0, c.groupLimit);
|
|
3741
|
+
const first = entries[0];
|
|
3742
|
+
return {
|
|
3743
|
+
id: c.id,
|
|
3744
|
+
buckets,
|
|
3745
|
+
scope: first.scope ?? c.scope ?? "allMatching",
|
|
3746
|
+
...first.processedRows !== void 0 ? { processedRows: first.processedRows } : {},
|
|
3747
|
+
groupCount,
|
|
3748
|
+
coverage: "exact",
|
|
3749
|
+
...hiddenGroups !== void 0 ? { hiddenGroups } : {}
|
|
3750
|
+
};
|
|
3095
3751
|
}
|
|
3096
3752
|
|
|
3097
3753
|
// src/groupKeys.ts
|
|
@@ -3793,22 +4449,6 @@ function toServerQueryV2(q, schema, execution) {
|
|
|
3793
4449
|
};
|
|
3794
4450
|
}
|
|
3795
4451
|
|
|
3796
|
-
// src/metricPrecision.ts
|
|
3797
|
-
function assertMetricNumber(value) {
|
|
3798
|
-
if (!Number.isFinite(value) || Number.isInteger(value) && !Number.isSafeInteger(value)) {
|
|
3799
|
-
throw Error("Aggregate exceeds finite safe numeric precision.");
|
|
3800
|
-
}
|
|
3801
|
-
}
|
|
3802
|
-
function checkedMetricResult(result) {
|
|
3803
|
-
if (result.error || typeof result.value !== "number") return result;
|
|
3804
|
-
try {
|
|
3805
|
-
assertMetricNumber(result.value);
|
|
3806
|
-
return result;
|
|
3807
|
-
} catch (e) {
|
|
3808
|
-
return { value: null, error: e.message };
|
|
3809
|
-
}
|
|
3810
|
-
}
|
|
3811
|
-
|
|
3812
4452
|
// src/metricDistributions.ts
|
|
3813
4453
|
function percentile(sorted, p) {
|
|
3814
4454
|
if (!Number.isFinite(p) || p < 0 || p > 1)
|
|
@@ -3908,176 +4548,6 @@ function histogramCounts(values, edges) {
|
|
|
3908
4548
|
return counts;
|
|
3909
4549
|
}
|
|
3910
4550
|
|
|
3911
|
-
// src/metricDates.ts
|
|
3912
|
-
import { Temporal } from "temporal-polyfill";
|
|
3913
|
-
var METRIC_DATE_INTERVALS = [
|
|
3914
|
-
"hour",
|
|
3915
|
-
"day",
|
|
3916
|
-
"week",
|
|
3917
|
-
"month",
|
|
3918
|
-
"year"
|
|
3919
|
-
];
|
|
3920
|
-
function metricDateTime(value, timeZone = "UTC") {
|
|
3921
|
-
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(
|
|
3922
|
-
value
|
|
3923
|
-
))
|
|
3924
|
-
throw Error(
|
|
3925
|
-
"Date grouping requires ISO dates or timestamps with a timezone."
|
|
3926
|
-
);
|
|
3927
|
-
const time = Date.parse(value), month = Number(value.slice(5, 7)), day = Number(value.slice(8, 10));
|
|
3928
|
-
const calendar = /* @__PURE__ */ new Date(0);
|
|
3929
|
-
calendar.setUTCFullYear(Number(value.slice(0, 4)), month, 0);
|
|
3930
|
-
if (!Number.isFinite(time) || month < 1 || month > 12 || day < 1 || day > calendar.getUTCDate())
|
|
3931
|
-
throw Error("Invalid date grouping value.");
|
|
3932
|
-
return value.length === 10 && timeZone !== "UTC" ? Temporal.PlainDate.from(value).toZonedDateTime(timeZone).epochMilliseconds : time;
|
|
3933
|
-
}
|
|
3934
|
-
function metricOffsetTransition(start, end, timeZone) {
|
|
3935
|
-
if (end <= start) return void 0;
|
|
3936
|
-
const offset = (time) => Temporal.Instant.fromEpochMilliseconds(time).toZonedDateTimeISO(timeZone).offsetNanoseconds;
|
|
3937
|
-
const before = offset(start);
|
|
3938
|
-
if (before === offset(end)) return void 0;
|
|
3939
|
-
while (end - start > 1) {
|
|
3940
|
-
const middle = Math.floor((start + end) / 2);
|
|
3941
|
-
if (offset(middle) === before) start = middle;
|
|
3942
|
-
else end = middle;
|
|
3943
|
-
}
|
|
3944
|
-
return end;
|
|
3945
|
-
}
|
|
3946
|
-
function metricDateFloor(time, interval, timeZone = "UTC") {
|
|
3947
|
-
if (timeZone !== "UTC") {
|
|
3948
|
-
const zoned = Temporal.Instant.fromEpochMilliseconds(time).toZonedDateTimeISO(timeZone);
|
|
3949
|
-
if (interval === "hour") {
|
|
3950
|
-
const floor = time - zoned.minute * 6e4 - zoned.second * 1e3 - zoned.millisecond;
|
|
3951
|
-
return metricOffsetTransition(floor, time, timeZone) ?? floor;
|
|
3952
|
-
}
|
|
3953
|
-
let date2 = zoned.toPlainDate();
|
|
3954
|
-
if (interval === "week") date2 = date2.subtract({ days: date2.dayOfWeek - 1 });
|
|
3955
|
-
if (interval === "month" || interval === "year")
|
|
3956
|
-
date2 = date2.with({ day: 1 });
|
|
3957
|
-
if (interval === "year") date2 = date2.with({ month: 1 });
|
|
3958
|
-
return date2.toZonedDateTime(timeZone).epochMilliseconds;
|
|
3959
|
-
}
|
|
3960
|
-
const date = new Date(time);
|
|
3961
|
-
date.setUTCMinutes(0, 0, 0);
|
|
3962
|
-
if (interval !== "hour") date.setUTCHours(0);
|
|
3963
|
-
if (interval === "week")
|
|
3964
|
-
date.setUTCDate(date.getUTCDate() - (date.getUTCDay() + 6) % 7);
|
|
3965
|
-
if (interval === "month" || interval === "year") date.setUTCDate(1);
|
|
3966
|
-
if (interval === "year") date.setUTCMonth(0);
|
|
3967
|
-
return date.getTime();
|
|
3968
|
-
}
|
|
3969
|
-
function metricDateNext(time, interval, timeZone = "UTC") {
|
|
3970
|
-
if (timeZone !== "UTC") {
|
|
3971
|
-
const zoned = Temporal.Instant.fromEpochMilliseconds(time).toZonedDateTimeISO(timeZone);
|
|
3972
|
-
if (interval === "hour") {
|
|
3973
|
-
const target = zoned.toPlainDateTime().with({
|
|
3974
|
-
minute: 0,
|
|
3975
|
-
second: 0,
|
|
3976
|
-
millisecond: 0,
|
|
3977
|
-
microsecond: 0,
|
|
3978
|
-
nanosecond: 0
|
|
3979
|
-
}).add({ hours: 1 });
|
|
3980
|
-
let next = target.toZonedDateTime(timeZone).epochMilliseconds;
|
|
3981
|
-
if (next <= time)
|
|
3982
|
-
next = target.toZonedDateTime(timeZone, {
|
|
3983
|
-
disambiguation: "later"
|
|
3984
|
-
}).epochMilliseconds;
|
|
3985
|
-
return metricOffsetTransition(time, next, timeZone) ?? next;
|
|
3986
|
-
}
|
|
3987
|
-
let date2 = zoned.toPlainDate();
|
|
3988
|
-
if (interval === "month") date2 = date2.with({ day: 1 }).add({ months: 1 });
|
|
3989
|
-
else if (interval === "year")
|
|
3990
|
-
date2 = date2.with({ month: 1, day: 1 }).add({ years: 1 });
|
|
3991
|
-
else if (interval === "week")
|
|
3992
|
-
date2 = date2.subtract({ days: date2.dayOfWeek - 1 }).add({ weeks: 1 });
|
|
3993
|
-
else date2 = date2.add({ days: 1 });
|
|
3994
|
-
return date2.toZonedDateTime(timeZone).epochMilliseconds;
|
|
3995
|
-
}
|
|
3996
|
-
const date = new Date(time);
|
|
3997
|
-
if (interval === "hour") date.setUTCHours(date.getUTCHours() + 1);
|
|
3998
|
-
else if (interval === "month") date.setUTCMonth(date.getUTCMonth() + 1);
|
|
3999
|
-
else if (interval === "year") date.setUTCFullYear(date.getUTCFullYear() + 1);
|
|
4000
|
-
else date.setUTCDate(date.getUTCDate() + (interval === "week" ? 7 : 1));
|
|
4001
|
-
return date.getTime();
|
|
4002
|
-
}
|
|
4003
|
-
function customEndTime(value, timeZone = "UTC") {
|
|
4004
|
-
const time = metricDateTime(value, timeZone);
|
|
4005
|
-
return value.length === 10 ? metricDateNext(time, "day", timeZone) - 1 : time;
|
|
4006
|
-
}
|
|
4007
|
-
function validateDateGrouping(config) {
|
|
4008
|
-
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))
|
|
4009
|
-
throw Error("Invalid date grouping definition.");
|
|
4010
|
-
if (config.timeZone !== void 0) {
|
|
4011
|
-
if (typeof config.timeZone !== "string" || !config.timeZone || config.timeZone.length > 80 || /^[+-]/.test(config.timeZone))
|
|
4012
|
-
throw Error(
|
|
4013
|
-
"Use a valid IANA grouping timezone, such as America/Denver or UTC."
|
|
4014
|
-
);
|
|
4015
|
-
try {
|
|
4016
|
-
new Intl.DateTimeFormat("en-US", { timeZone: config.timeZone });
|
|
4017
|
-
} catch {
|
|
4018
|
-
throw Error(
|
|
4019
|
-
"Use a valid IANA grouping timezone, such as America/Denver or UTC."
|
|
4020
|
-
);
|
|
4021
|
-
}
|
|
4022
|
-
}
|
|
4023
|
-
if (config.includeEmpty && config.range === "custom") {
|
|
4024
|
-
const start = metricDateTime(config.start ?? "", config.timeZone), end = customEndTime(config.end ?? "", config.timeZone);
|
|
4025
|
-
if (end < start)
|
|
4026
|
-
throw Error("The end date must be on or after the start date.");
|
|
4027
|
-
}
|
|
4028
|
-
}
|
|
4029
|
-
function metricDateRange(config, query, observed, now, canonical) {
|
|
4030
|
-
let min = Infinity, max = -Infinity;
|
|
4031
|
-
for (const time of observed) {
|
|
4032
|
-
min = Math.min(min, time);
|
|
4033
|
-
max = Math.max(max, time);
|
|
4034
|
-
}
|
|
4035
|
-
if (config.range === "observed")
|
|
4036
|
-
return observed.length ? [min, max] : void 0;
|
|
4037
|
-
if (config.range === "custom") {
|
|
4038
|
-
const start2 = metricDateFloor(
|
|
4039
|
-
metricDateTime(config.start ?? "", config.timeZone),
|
|
4040
|
-
config.interval,
|
|
4041
|
-
config.timeZone
|
|
4042
|
-
);
|
|
4043
|
-
const end2 = metricDateFloor(
|
|
4044
|
-
customEndTime(config.end ?? "", config.timeZone),
|
|
4045
|
-
config.interval,
|
|
4046
|
-
config.timeZone
|
|
4047
|
-
);
|
|
4048
|
-
if (observed.length && (start2 > min || end2 < max))
|
|
4049
|
-
throw Error(
|
|
4050
|
-
"Custom date bounds must include all observed buckets. Narrow the query filters to exclude data."
|
|
4051
|
-
);
|
|
4052
|
-
return [start2, end2];
|
|
4053
|
-
}
|
|
4054
|
-
let start = -Infinity, end = Infinity;
|
|
4055
|
-
for (const term of query.where) {
|
|
4056
|
-
if (isOrGroup(term) || term.negated || canonical(term.field) !== canonical(config.field))
|
|
4057
|
-
continue;
|
|
4058
|
-
if (!["=", ">", ">=", "<", "<="].includes(term.op)) continue;
|
|
4059
|
-
const time = Number(coerceValue("datetime", term.value, now));
|
|
4060
|
-
if (term.op === ">" || term.op === ">=" || term.op === "=")
|
|
4061
|
-
start = Math.max(start, time + (term.op === ">" ? 1 : 0));
|
|
4062
|
-
if (term.op === "<" || term.op === "<=" || term.op === "=")
|
|
4063
|
-
end = Math.min(end, time - (term.op === "<" ? 1 : 0));
|
|
4064
|
-
}
|
|
4065
|
-
if (!Number.isFinite(start) || !Number.isFinite(end)) {
|
|
4066
|
-
const missing = [
|
|
4067
|
-
...Number.isFinite(start) ? [] : ["a lower bound (>= or >)"],
|
|
4068
|
-
...Number.isFinite(end) ? [] : ["an upper bound (<= or <)"]
|
|
4069
|
-
].join(" and ");
|
|
4070
|
-
throw Error(
|
|
4071
|
-
`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.`
|
|
4072
|
-
);
|
|
4073
|
-
}
|
|
4074
|
-
if (end < start) return void 0;
|
|
4075
|
-
return [
|
|
4076
|
-
metricDateFloor(start, config.interval, config.timeZone),
|
|
4077
|
-
metricDateFloor(end, config.interval, config.timeZone)
|
|
4078
|
-
];
|
|
4079
|
-
}
|
|
4080
|
-
|
|
4081
4551
|
// src/metrics.ts
|
|
4082
4552
|
var aggregates = /* @__PURE__ */ new Set([
|
|
4083
4553
|
"COUNT",
|
|
@@ -4421,6 +4891,7 @@ function validateMetric(c, schema, resolve) {
|
|
|
4421
4891
|
const compiled = metricPlans(c, schema, resolve);
|
|
4422
4892
|
if (c.scope !== "shownRows" && compiled.some(hasRegex))
|
|
4423
4893
|
throw new FormulaError("Regex is unavailable for all-matching metrics.");
|
|
4894
|
+
validateMetricSeries(c, compiled[0]);
|
|
4424
4895
|
return [];
|
|
4425
4896
|
} catch (e) {
|
|
4426
4897
|
return [
|
|
@@ -4484,6 +4955,8 @@ function metricComputationKey(clauses) {
|
|
|
4484
4955
|
groupLimit: c.groupLimit,
|
|
4485
4956
|
where: c.where,
|
|
4486
4957
|
minCount: c.minCount,
|
|
4958
|
+
window: c.window,
|
|
4959
|
+
share: c.share,
|
|
4487
4960
|
distribution: distributionActive(c) ? c.distribution : void 0
|
|
4488
4961
|
})).sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0)
|
|
4489
4962
|
);
|
|
@@ -4535,6 +5008,8 @@ function toMetricQuery(q, schema, execution) {
|
|
|
4535
5008
|
expressionY,
|
|
4536
5009
|
dateGrouping,
|
|
4537
5010
|
where,
|
|
5011
|
+
window: _window,
|
|
5012
|
+
share: _share,
|
|
4538
5013
|
...rest
|
|
4539
5014
|
} = c;
|
|
4540
5015
|
return {
|
|
@@ -4564,6 +5039,15 @@ function toMetricQuery(q, schema, execution) {
|
|
|
4564
5039
|
to: 1,
|
|
4565
5040
|
message: "Metric filters must use server-filterable fields."
|
|
4566
5041
|
});
|
|
5042
|
+
for (const c of q.aggregations ?? [])
|
|
5043
|
+
if (hasMetricSeries(c))
|
|
5044
|
+
diagnostics.push({
|
|
5045
|
+
metricId: c.id,
|
|
5046
|
+
code: "unexpanded_series",
|
|
5047
|
+
from: 0,
|
|
5048
|
+
to: 1,
|
|
5049
|
+
message: "Windows, shares and whole intervals are computed from plain metrics; expand the query with planMetricSeries first."
|
|
5050
|
+
});
|
|
4567
5051
|
for (const c of q.aggregations ?? [])
|
|
4568
5052
|
if (c.dateGrouping)
|
|
4569
5053
|
diagnostics.push({
|
|
@@ -4759,6 +5243,24 @@ function compareMetricValues(a, b) {
|
|
|
4759
5243
|
}
|
|
4760
5244
|
return a < b ? -1 : 1;
|
|
4761
5245
|
}
|
|
5246
|
+
function sortMetricBuckets(buckets, c) {
|
|
5247
|
+
const ordering = c.sort?.length ? c.sort : c.dateGrouping ? [{ key: "group0", dir: "asc", nulls: "last" }] : [{ key: "value", dir: "desc", nulls: "last" }];
|
|
5248
|
+
const target = (b, s) => s.key === "samples" ? b.count - (b.nullCount ?? 0) - (b.inputErrorCount ?? 0) : s.key.startsWith("group") ? b.keys[Number(s.key.slice(5))] ?? null : s.key === "count" ? b.count : s.key === "y" ? b.y ?? null : b.value;
|
|
5249
|
+
return buckets.sort((a, b) => {
|
|
5250
|
+
if (!!(a.error || a.yError) !== !!(b.error || b.yError))
|
|
5251
|
+
return a.error || a.yError ? 1 : -1;
|
|
5252
|
+
for (const s of ordering) {
|
|
5253
|
+
const av = target(a, s), bv = target(b, s);
|
|
5254
|
+
const cmp = av === null || bv === null ? av === bv ? 0 : (av === null ? 1 : -1) * (s.nulls === "first" ? -1 : 1) : compareMetricValues(av, bv) * (s.dir === "asc" ? 1 : -1);
|
|
5255
|
+
if (cmp) return cmp;
|
|
5256
|
+
}
|
|
5257
|
+
for (let i = 0; i < a.keys.length; i++) {
|
|
5258
|
+
const cmp = compareMetricValues(a.keys[i], b.keys[i]);
|
|
5259
|
+
if (cmp) return cmp;
|
|
5260
|
+
}
|
|
5261
|
+
return 0;
|
|
5262
|
+
});
|
|
5263
|
+
}
|
|
4762
5264
|
function evaluateMetrics(rows, q, schema, options2 = {}) {
|
|
4763
5265
|
if (q.grouping) {
|
|
4764
5266
|
try {
|
|
@@ -4775,6 +5277,18 @@ function evaluateMetrics(rows, q, schema, options2 = {}) {
|
|
|
4775
5277
|
}
|
|
4776
5278
|
}
|
|
4777
5279
|
const now = evaluationTime(options2), fields = indexFields(schema);
|
|
5280
|
+
const series = planMetricSeries(q, schema, {
|
|
5281
|
+
now,
|
|
5282
|
+
...options2.resolveComputed ? { resolveComputed: options2.resolveComputed } : {}
|
|
5283
|
+
});
|
|
5284
|
+
if (series)
|
|
5285
|
+
return {
|
|
5286
|
+
metrics: series.combine(
|
|
5287
|
+
series.queries.flatMap(
|
|
5288
|
+
(part) => evaluateMetrics(rows, part, schema, { ...options2, now }).metrics
|
|
5289
|
+
)
|
|
5290
|
+
)
|
|
5291
|
+
};
|
|
4778
5292
|
let matching;
|
|
4779
5293
|
let shown;
|
|
4780
5294
|
return {
|
|
@@ -4909,7 +5423,7 @@ function evaluateMetrics(rows, q, schema, options2 = {}) {
|
|
|
4909
5423
|
if (c.dateGrouping?.includeEmpty) {
|
|
4910
5424
|
const config = c.dateGrouping;
|
|
4911
5425
|
const observed = [];
|
|
4912
|
-
const
|
|
5426
|
+
const series2 = /* @__PURE__ */ new Map();
|
|
4913
5427
|
for (const group of groups.values())
|
|
4914
5428
|
if (group.keys[0] !== null) {
|
|
4915
5429
|
tick();
|
|
@@ -4918,17 +5432,17 @@ function evaluateMetrics(rows, q, schema, options2 = {}) {
|
|
|
4918
5432
|
observed.push(metricDateTime(group.keys[0]));
|
|
4919
5433
|
const tuple = group.keys.slice(1);
|
|
4920
5434
|
const key = JSON.stringify(tuple);
|
|
4921
|
-
if (!
|
|
5435
|
+
if (!series2.has(key)) {
|
|
4922
5436
|
const bytes = 128 + 16 * tuple.length + 2 * key.length;
|
|
4923
5437
|
reserve(bytes);
|
|
4924
5438
|
groupBytes += bytes;
|
|
4925
|
-
|
|
5439
|
+
series2.set(key, tuple);
|
|
4926
5440
|
}
|
|
4927
5441
|
}
|
|
4928
|
-
if (c.groupBy.length === 1 && !
|
|
5442
|
+
if (c.groupBy.length === 1 && !series2.size) {
|
|
4929
5443
|
reserve(132);
|
|
4930
5444
|
groupBytes += 132;
|
|
4931
|
-
|
|
5445
|
+
series2.set("[]", []);
|
|
4932
5446
|
}
|
|
4933
5447
|
const range = metricDateRange(
|
|
4934
5448
|
config,
|
|
@@ -4940,7 +5454,7 @@ function evaluateMetrics(rows, q, schema, options2 = {}) {
|
|
|
4940
5454
|
if (range)
|
|
4941
5455
|
for (let time = range[0]; time <= range[1]; ) {
|
|
4942
5456
|
tick();
|
|
4943
|
-
for (const tuple of
|
|
5457
|
+
for (const tuple of series2.values()) {
|
|
4944
5458
|
tick();
|
|
4945
5459
|
const keys = [
|
|
4946
5460
|
new Date(time).toISOString(),
|
|
@@ -5100,22 +5614,7 @@ function evaluateMetrics(rows, q, schema, options2 = {}) {
|
|
|
5100
5614
|
hiddenGroups = buckets.length - kept.length;
|
|
5101
5615
|
buckets = kept;
|
|
5102
5616
|
}
|
|
5103
|
-
|
|
5104
|
-
const target = (b, s) => s.key === "samples" ? b.count - (b.nullCount ?? 0) - (b.inputErrorCount ?? 0) : s.key.startsWith("group") ? b.keys[Number(s.key.slice(5))] ?? null : s.key === "count" ? b.count : s.key === "y" ? b.y ?? null : b.value;
|
|
5105
|
-
buckets.sort((a, b) => {
|
|
5106
|
-
if (!!(a.error || a.yError) !== !!(b.error || b.yError))
|
|
5107
|
-
return a.error || a.yError ? 1 : -1;
|
|
5108
|
-
for (const s of ordering) {
|
|
5109
|
-
const av = target(a, s), bv = target(b, s);
|
|
5110
|
-
const cmp = av === null || bv === null ? av === bv ? 0 : (av === null ? 1 : -1) * (s.nulls === "first" ? -1 : 1) : compareMetricValues(av, bv) * (s.dir === "asc" ? 1 : -1);
|
|
5111
|
-
if (cmp) return cmp;
|
|
5112
|
-
}
|
|
5113
|
-
for (let i = 0; i < a.keys.length; i++) {
|
|
5114
|
-
const cmp = compareMetricValues(a.keys[i], b.keys[i]);
|
|
5115
|
-
if (cmp) return cmp;
|
|
5116
|
-
}
|
|
5117
|
-
return 0;
|
|
5118
|
-
});
|
|
5617
|
+
sortMetricBuckets(buckets, c);
|
|
5119
5618
|
tick();
|
|
5120
5619
|
const groupCount = buckets.length;
|
|
5121
5620
|
let other;
|
|
@@ -5223,7 +5722,7 @@ function applyAggregations(rows, q, schema, options2 = {}) {
|
|
|
5223
5722
|
))
|
|
5224
5723
|
return evaluateMetrics(rows, q, schema, options2);
|
|
5225
5724
|
const advanced = (q.aggregations ?? []).filter(
|
|
5226
|
-
(a) => a.dateGrouping || a.diagnostics?.length || a.expression !== void 0 || a.expressionY !== void 0 || a.scope !== void 0 || a.distribution || a.sort || a.groupLimit !== void 0 || a.where !== void 0 || a.minCount !== void 0
|
|
5725
|
+
(a) => a.dateGrouping || a.diagnostics?.length || a.expression !== void 0 || a.expressionY !== void 0 || a.scope !== void 0 || a.distribution || a.sort || a.groupLimit !== void 0 || a.where !== void 0 || a.minCount !== void 0 || hasMetricSeries(a)
|
|
5227
5726
|
);
|
|
5228
5727
|
if (advanced.length) {
|
|
5229
5728
|
if ((q.aggregations?.length ?? 0) > 20 || normalizeQueryState(q).aggregations?.length !== q.aggregations?.length)
|
|
@@ -6149,7 +6648,7 @@ function toQueryRequestV3(query, baseSchema, capabilities, execution, options2 =
|
|
|
6149
6648
|
if (predicate.field.startsWith("@computed/") && !capabilities.computedSourceFilters) diagnostics.push("This server does not support computed source predicates.");
|
|
6150
6649
|
}
|
|
6151
6650
|
const metricQuery = toMetricQuery(withoutGrouping(q), resultSchema, execution);
|
|
6152
|
-
diagnostics.push(...metricQuery.diagnostics.filter((issue) => issue.code === "residual_metric_filter").map((issue) => issue.message));
|
|
6651
|
+
diagnostics.push(...metricQuery.diagnostics.filter((issue) => issue.code === "residual_metric_filter" || issue.code === "unexpanded_series").map((issue) => issue.message));
|
|
6153
6652
|
const metrics = metricQuery.metrics.map((metric) => {
|
|
6154
6653
|
const dateGrouping = q.aggregations?.find((clause) => clause.id === metric.id)?.dateGrouping;
|
|
6155
6654
|
return dateGrouping ? { ...metric, dateGrouping } : metric;
|
|
@@ -6223,12 +6722,21 @@ function inspectBucketQuery(query, bucket, clause, schema) {
|
|
|
6223
6722
|
} catch {
|
|
6224
6723
|
return null;
|
|
6225
6724
|
}
|
|
6226
|
-
const { interval, timeZone } = clause.dateGrouping;
|
|
6725
|
+
const { interval, timeZone, partialIntervals } = clause.dateGrouping;
|
|
6227
6726
|
const next = metricDateNext(start, interval, timeZone);
|
|
6228
|
-
|
|
6229
|
-
|
|
6230
|
-
|
|
6231
|
-
)
|
|
6727
|
+
let from = start;
|
|
6728
|
+
const periods = clause.window?.periods;
|
|
6729
|
+
if (periods === "all") from = void 0;
|
|
6730
|
+
else if (periods !== void 0) {
|
|
6731
|
+
if (!Number.isInteger(periods) || periods < 2 || periods > MAX_METRIC_WINDOW_PERIODS) return null;
|
|
6732
|
+
for (let i = 1; i < periods; i++) from = metricDateFloor(from - 1, interval, timeZone);
|
|
6733
|
+
}
|
|
6734
|
+
if (partialIntervals === "exclude") {
|
|
6735
|
+
const whole = wholeIntervalSpan(clause, query, fields, Date.now()).start;
|
|
6736
|
+
if (whole !== void 0) from = Math.max(from ?? whole, whole);
|
|
6737
|
+
}
|
|
6738
|
+
if (from !== void 0) terms.push({ field: field.name, op: ">=", value: new Date(from).toISOString() });
|
|
6739
|
+
terms.push({ field: field.name, op: "<", value: new Date(next).toISOString() });
|
|
6232
6740
|
continue;
|
|
6233
6741
|
}
|
|
6234
6742
|
const term = keyClause(field, key);
|
|
@@ -6273,6 +6781,7 @@ export {
|
|
|
6273
6781
|
ITEM_DETAILS_COLUMN,
|
|
6274
6782
|
MAX_AGGREGATIONS,
|
|
6275
6783
|
MAX_GROUP_BY_FIELDS,
|
|
6784
|
+
MAX_METRIC_WINDOW_PERIODS,
|
|
6276
6785
|
MAX_ORDER_BY_TERMS,
|
|
6277
6786
|
MAX_PROPERTIES,
|
|
6278
6787
|
MAX_QUERY_LIMIT,
|
|
@@ -6320,6 +6829,7 @@ export {
|
|
|
6320
6829
|
groupLinkQuery,
|
|
6321
6830
|
groupPreview,
|
|
6322
6831
|
groupSourceQuery,
|
|
6832
|
+
hasMetricSeries,
|
|
6323
6833
|
hasRegex,
|
|
6324
6834
|
histogramCounts,
|
|
6325
6835
|
histogramEdges,
|
|
@@ -6354,6 +6864,7 @@ export {
|
|
|
6354
6864
|
memoryStorageAdapter,
|
|
6355
6865
|
metricAggregatePlans,
|
|
6356
6866
|
metricComputationKey,
|
|
6867
|
+
metricCustomEndTime,
|
|
6357
6868
|
metricDateFloor,
|
|
6358
6869
|
metricDateNext,
|
|
6359
6870
|
metricDateRange,
|
|
@@ -6372,6 +6883,7 @@ export {
|
|
|
6372
6883
|
opsForField,
|
|
6373
6884
|
parseRelativeDuration,
|
|
6374
6885
|
percentile,
|
|
6886
|
+
planMetricSeries,
|
|
6375
6887
|
predicatesOf,
|
|
6376
6888
|
prepareGrouping,
|
|
6377
6889
|
queriesEqual,
|
|
@@ -6387,6 +6899,7 @@ export {
|
|
|
6387
6899
|
selectedFields,
|
|
6388
6900
|
setFilterModes,
|
|
6389
6901
|
setFilterTarget,
|
|
6902
|
+
sortMetricBuckets,
|
|
6390
6903
|
sourceDependencies,
|
|
6391
6904
|
sourceExecutionSchema,
|
|
6392
6905
|
stripSetFilter,
|
|
@@ -6399,8 +6912,10 @@ export {
|
|
|
6399
6912
|
validateDateGrouping,
|
|
6400
6913
|
validateItemPageTemplate,
|
|
6401
6914
|
validateMetric,
|
|
6915
|
+
validateMetricSeries,
|
|
6402
6916
|
validateQueryResultV3,
|
|
6403
6917
|
validateQuerySnapshot,
|
|
6918
|
+
wholeIntervalSpan,
|
|
6404
6919
|
withGroupingKeys,
|
|
6405
6920
|
withoutGrouping,
|
|
6406
6921
|
withoutLinkSummaries
|