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