@uptimizr/db 2.0.1 → 2.1.0
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/AGENTS.md +228 -3
- package/README.md +13 -0
- package/dist/duckdb/migrations.d.ts.map +1 -1
- package/dist/duckdb/migrations.js +169 -0
- package/dist/duckdb/migrations.js.map +1 -1
- package/dist/duckdb/panelSpecs.d.ts +29 -0
- package/dist/duckdb/panelSpecs.d.ts.map +1 -0
- package/dist/duckdb/panelSpecs.js +104 -0
- package/dist/duckdb/panelSpecs.js.map +1 -0
- package/dist/duckdb/projectMetadata.d.ts +43 -0
- package/dist/duckdb/projectMetadata.d.ts.map +1 -0
- package/dist/duckdb/projectMetadata.js +222 -0
- package/dist/duckdb/projectMetadata.js.map +1 -0
- package/dist/duckdb/subscriptions.d.ts +45 -0
- package/dist/duckdb/subscriptions.d.ts.map +1 -0
- package/dist/duckdb/subscriptions.js +202 -0
- package/dist/duckdb/subscriptions.js.map +1 -0
- package/dist/index.d.ts +18 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +48 -3
- package/dist/index.js.map +1 -1
- package/dist/insights/anomalies.d.ts +213 -0
- package/dist/insights/anomalies.d.ts.map +1 -0
- package/dist/insights/anomalies.js +340 -0
- package/dist/insights/anomalies.js.map +1 -0
- package/dist/insights/baseline.d.ts +51 -0
- package/dist/insights/baseline.d.ts.map +1 -0
- package/dist/insights/baseline.js +48 -0
- package/dist/insights/baseline.js.map +1 -0
- package/dist/insights/buckets.d.ts +114 -0
- package/dist/insights/buckets.d.ts.map +1 -0
- package/dist/insights/buckets.js +220 -0
- package/dist/insights/buckets.js.map +1 -0
- package/dist/insights/changepoint.d.ts +130 -0
- package/dist/insights/changepoint.d.ts.map +1 -0
- package/dist/insights/changepoint.js +209 -0
- package/dist/insights/changepoint.js.map +1 -0
- package/dist/insights/evaluate.d.ts +63 -0
- package/dist/insights/evaluate.d.ts.map +1 -0
- package/dist/insights/evaluate.js +158 -0
- package/dist/insights/evaluate.js.map +1 -0
- package/dist/insights/health.d.ts +219 -0
- package/dist/insights/health.d.ts.map +1 -0
- package/dist/insights/health.js +349 -0
- package/dist/insights/health.js.map +1 -0
- package/dist/insights/index.d.ts +45 -0
- package/dist/insights/index.d.ts.map +1 -0
- package/dist/insights/index.js +40 -0
- package/dist/insights/index.js.map +1 -0
- package/dist/insights/measures.d.ts +222 -0
- package/dist/insights/measures.d.ts.map +1 -0
- package/dist/insights/measures.js +431 -0
- package/dist/insights/measures.js.map +1 -0
- package/dist/insights/movers.d.ts +123 -0
- package/dist/insights/movers.d.ts.map +1 -0
- package/dist/insights/movers.js +166 -0
- package/dist/insights/movers.js.map +1 -0
- package/dist/insights/significance.d.ts +221 -0
- package/dist/insights/significance.d.ts.map +1 -0
- package/dist/insights/significance.js +531 -0
- package/dist/insights/significance.js.map +1 -0
- package/dist/insights/stats.d.ts +104 -0
- package/dist/insights/stats.d.ts.map +1 -0
- package/dist/insights/stats.js +181 -0
- package/dist/insights/stats.js.map +1 -0
- package/dist/insights/windows.d.ts +95 -0
- package/dist/insights/windows.d.ts.map +1 -0
- package/dist/insights/windows.js +97 -0
- package/dist/insights/windows.js.map +1 -0
- package/dist/metadata.d.ts +178 -1
- package/dist/metadata.d.ts.map +1 -1
- package/dist/metadata.js +80 -0
- package/dist/metadata.js.map +1 -1
- package/dist/narrative/build.d.ts +78 -0
- package/dist/narrative/build.d.ts.map +1 -0
- package/dist/narrative/build.js +437 -0
- package/dist/narrative/build.js.map +1 -0
- package/dist/narrative/index.d.ts +14 -0
- package/dist/narrative/index.d.ts.map +1 -0
- package/dist/narrative/index.js +13 -0
- package/dist/narrative/index.js.map +1 -0
- package/dist/narrative/text.d.ts +25 -0
- package/dist/narrative/text.d.ts.map +1 -0
- package/dist/narrative/text.js +41 -0
- package/dist/narrative/text.js.map +1 -0
- package/dist/parity/cases.d.ts +3 -0
- package/dist/parity/cases.d.ts.map +1 -1
- package/dist/parity/cases.js +387 -7
- package/dist/parity/cases.js.map +1 -1
- package/dist/parity/compare.d.ts +19 -0
- package/dist/parity/compare.d.ts.map +1 -1
- package/dist/parity/compare.js +19 -0
- package/dist/parity/compare.js.map +1 -1
- package/dist/parity/fixtures.d.ts.map +1 -1
- package/dist/parity/fixtures.js +25 -0
- package/dist/parity/fixtures.js.map +1 -1
- package/dist/query/aggregations.d.ts +46 -1
- package/dist/query/aggregations.d.ts.map +1 -1
- package/dist/query/aggregations.js +86 -0
- package/dist/query/aggregations.js.map +1 -1
- package/dist/query/clickhouseDialect.d.ts.map +1 -1
- package/dist/query/clickhouseDialect.js +5 -1
- package/dist/query/clickhouseDialect.js.map +1 -1
- package/dist/query/customEventVocabulary.d.ts +46 -0
- package/dist/query/customEventVocabulary.d.ts.map +1 -0
- package/dist/query/customEventVocabulary.js +127 -0
- package/dist/query/customEventVocabulary.js.map +1 -0
- package/dist/query/dsl/builders.d.ts +38 -0
- package/dist/query/dsl/builders.d.ts.map +1 -0
- package/dist/query/dsl/builders.js +35 -0
- package/dist/query/dsl/builders.js.map +1 -0
- package/dist/query/dsl/compare.d.ts +167 -0
- package/dist/query/dsl/compare.d.ts.map +1 -0
- package/dist/query/dsl/compare.js +358 -0
- package/dist/query/dsl/compare.js.map +1 -0
- package/dist/query/dsl/compile.d.ts +89 -0
- package/dist/query/dsl/compile.d.ts.map +1 -0
- package/dist/query/dsl/compile.js +174 -0
- package/dist/query/dsl/compile.js.map +1 -0
- package/dist/query/dsl/explain.d.ts +115 -0
- package/dist/query/dsl/explain.d.ts.map +1 -0
- package/dist/query/dsl/explain.js +143 -0
- package/dist/query/dsl/explain.js.map +1 -0
- package/dist/query/dsl/generic.d.ts +127 -0
- package/dist/query/dsl/generic.d.ts.map +1 -0
- package/dist/query/dsl/generic.js +323 -0
- package/dist/query/dsl/generic.js.map +1 -0
- package/dist/query/dsl/index.d.ts +30 -0
- package/dist/query/dsl/index.d.ts.map +1 -0
- package/dist/query/dsl/index.js +23 -0
- package/dist/query/dsl/index.js.map +1 -0
- package/dist/query/dsl/order.d.ts +41 -0
- package/dist/query/dsl/order.d.ts.map +1 -0
- package/dist/query/dsl/order.js +55 -0
- package/dist/query/dsl/order.js.map +1 -0
- package/dist/query/dsl/significance.d.ts +130 -0
- package/dist/query/dsl/significance.d.ts.map +1 -0
- package/dist/query/dsl/significance.js +224 -0
- package/dist/query/dsl/significance.js.map +1 -0
- package/dist/query/index.d.ts +2 -0
- package/dist/query/index.d.ts.map +1 -1
- package/dist/query/index.js +8 -0
- package/dist/query/index.js.map +1 -1
- package/dist/query/summary/index.d.ts +4 -2
- package/dist/query/summary/index.d.ts.map +1 -1
- package/dist/query/summary/index.js +2 -1
- package/dist/query/summary/index.js.map +1 -1
- package/dist/query/summary/labels.d.ts +101 -0
- package/dist/query/summary/labels.d.ts.map +1 -0
- package/dist/query/summary/labels.js +261 -0
- package/dist/query/summary/labels.js.map +1 -0
- package/dist/query/summary/reading.d.ts.map +1 -1
- package/dist/query/summary/reading.js +21 -2
- package/dist/query/summary/reading.js.map +1 -1
- package/dist/query/summary/schema.d.ts +250 -20
- package/dist/query/summary/schema.d.ts.map +1 -1
- package/dist/query/summary/schema.js +135 -123
- package/dist/query/summary/schema.js.map +1 -1
- package/dist/query/summary/summarize.d.ts.map +1 -1
- package/dist/query/summary/summarize.js +91 -5
- package/dist/query/summary/summarize.js.map +1 -1
- package/dist/query/summary/types.d.ts +77 -2
- package/dist/query/summary/types.d.ts.map +1 -1
- package/dist/query/types.d.ts +47 -1
- package/dist/query/types.d.ts.map +1 -1
- package/dist/subscriptions.d.ts +208 -0
- package/dist/subscriptions.d.ts.map +1 -0
- package/dist/subscriptions.js +153 -0
- package/dist/subscriptions.js.map +1 -0
- package/llms.txt +9 -0
- package/package.json +8 -8
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **The delegated query compiler** (ADR 0051 §3, design sketch §C.2, tier 1).
|
|
3
|
+
*
|
|
4
|
+
* A validated `queryV1` document becomes a `QuerySpec` by way of the metric's
|
|
5
|
+
* *existing* aggregation builder: the registry says which `build*` computes the
|
|
6
|
+
* metric, `FILTER_TARGETS` says which option field each filter drives, and the
|
|
7
|
+
* builder renders the SQL it has always rendered, through the same `Dialect`.
|
|
8
|
+
*
|
|
9
|
+
* That is the point of the delegated tier. Every registry metric is reachable
|
|
10
|
+
* through the DSL on day one at exactly the power its canned endpoint has — no
|
|
11
|
+
* second SQL path, no "DSL-only" subset of metrics, and nothing new for the
|
|
12
|
+
* cross-engine parity harness to cover, because the specs it compiles *are* the
|
|
13
|
+
* specs the parity cases already pin (`src/__tests__/queryDsl.test.ts` asserts
|
|
14
|
+
* that equality builder by builder, on all four dialects).
|
|
15
|
+
*
|
|
16
|
+
* It also means the DSL inherits the two properties that matter most and cannot
|
|
17
|
+
* be bolted on afterwards: every value reaches SQL as a bound parameter (the
|
|
18
|
+
* builders' `ParamBag`), and the returned spec carries its `metric`, so the
|
|
19
|
+
* store edge coerces the rows (`coerceRows`, ADR 0051 §2) without knowing a DSL
|
|
20
|
+
* exists.
|
|
21
|
+
*
|
|
22
|
+
* What this module does **not** do: validate. A query must already have passed
|
|
23
|
+
* `queryV1Schema` (shape) and `validateQuery` (vocabulary) — by the time it gets
|
|
24
|
+
* here, the metric exists, has a builder, accepts every filter named, and asks
|
|
25
|
+
* for a grain it can produce. The two `throw`s below are guards against a caller
|
|
26
|
+
* skipping that, not a client-facing error path.
|
|
27
|
+
*/
|
|
28
|
+
import { FILTER_TARGETS, getMetric, queryTier, } from "@uptimizr/metrics";
|
|
29
|
+
import { builderFor } from "./builders.js";
|
|
30
|
+
import { compileGenericGroupBy } from "./generic.js";
|
|
31
|
+
/**
|
|
32
|
+
* The dashboard's camera-mode toggle → the `scene.cameraType` the events carry
|
|
33
|
+
* (ADR 0026). The same mapping the canned routes apply; it lives here too so a
|
|
34
|
+
* DSL query and a `?cameraMode=` request produce the identical spec.
|
|
35
|
+
*/
|
|
36
|
+
export function cameraTypeForMode(mode) {
|
|
37
|
+
if (mode === "first-person")
|
|
38
|
+
return "free";
|
|
39
|
+
if (mode === "viewer")
|
|
40
|
+
return "arc-rotate";
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Assign `value` to `field` on the option bag. Almost every `FILTER_TARGETS`
|
|
45
|
+
* field is a plain property name; the three `center[i]` entries address one
|
|
46
|
+
* element of a tuple option (the reference point `camera_distance` measures
|
|
47
|
+
* from), which is the only indexed form the registry uses.
|
|
48
|
+
*/
|
|
49
|
+
function assign(options, field, value) {
|
|
50
|
+
const indexed = /^([A-Za-z0-9_]+)\[(\d+)\]$/.exec(field);
|
|
51
|
+
if (!indexed) {
|
|
52
|
+
options[field] = value;
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const [, name, index] = indexed;
|
|
56
|
+
const tuple = Array.isArray(options[name]) ? options[name] : [];
|
|
57
|
+
tuple[Number(index)] = value;
|
|
58
|
+
options[name] = tuple;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Turn a validated query into the option bag its builder takes.
|
|
62
|
+
*
|
|
63
|
+
* The mapping is the registry's, not this file's: for each filter the query
|
|
64
|
+
* carries, `FILTER_TARGETS[filter].field` names the option field it drives. Only
|
|
65
|
+
* two values are transformed on the way — the camera-mode toggle becomes the
|
|
66
|
+
* stored `cameraType`, and a region id becomes the bounds the caller resolved.
|
|
67
|
+
*/
|
|
68
|
+
export function toBuilderOptions(query, resolved = {}, tier = "delegated") {
|
|
69
|
+
const options = {
|
|
70
|
+
since: query.range.since,
|
|
71
|
+
until: query.range.until,
|
|
72
|
+
};
|
|
73
|
+
for (const [key, value] of Object.entries(query.filters ?? {})) {
|
|
74
|
+
if (value === undefined)
|
|
75
|
+
continue;
|
|
76
|
+
// The two generic-tier filters are not builder options at all: no `build*`
|
|
77
|
+
// takes an event predicate or a device attribute. They ride in the bag under
|
|
78
|
+
// their own names and are read by the generic builder alone.
|
|
79
|
+
if (key === "event" || key === "device") {
|
|
80
|
+
if (tier === "generic")
|
|
81
|
+
options[key] = value;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const target = FILTER_TARGETS[key];
|
|
85
|
+
if (!target)
|
|
86
|
+
continue;
|
|
87
|
+
if (key === "cameraMode") {
|
|
88
|
+
assign(options, target.field, cameraTypeForMode(value));
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (key === "region") {
|
|
92
|
+
const region = typeof value === "string" ? resolved.region : value;
|
|
93
|
+
if (region == null) {
|
|
94
|
+
throw new Error("filters.region names a registered region id, which must be resolved to bounds before compiling");
|
|
95
|
+
}
|
|
96
|
+
assign(options, target.field, region);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
assign(options, target.field, value);
|
|
100
|
+
}
|
|
101
|
+
// A `segment` is a set of equality filters with a name. Where the metric
|
|
102
|
+
// already has a filter for the dimension, it *is* that filter — so it lands on
|
|
103
|
+
// the same option field and a delegated query never has to know the difference.
|
|
104
|
+
// Anything else is a dimension only the generic builder can hold fixed.
|
|
105
|
+
const segment = {};
|
|
106
|
+
for (const [dimension, value] of Object.entries(query.segment ?? {})) {
|
|
107
|
+
const target = FILTER_TARGETS[dimension];
|
|
108
|
+
if (target != null) {
|
|
109
|
+
if (dimension === "cameraMode") {
|
|
110
|
+
assign(options, target.field, cameraTypeForMode(value));
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
assign(options, target.field, value);
|
|
114
|
+
}
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
segment[dimension] = value;
|
|
118
|
+
}
|
|
119
|
+
if (query.limit != null)
|
|
120
|
+
options.limit = query.limit;
|
|
121
|
+
if (resolved.cellSize != null && options.cellSize == null)
|
|
122
|
+
options.cellSize = resolved.cellSize;
|
|
123
|
+
if (tier === "generic") {
|
|
124
|
+
// The generic builder reads its grain, its order and its segment from the
|
|
125
|
+
// same bag; `compileMetric` dispatches on `tier`, which is the one field
|
|
126
|
+
// that is about compilation rather than about the query.
|
|
127
|
+
options.tier = "generic";
|
|
128
|
+
options.dimensions = query.dimensions == null ? undefined : [...query.dimensions];
|
|
129
|
+
if (query.order != null)
|
|
130
|
+
options.order = { ...query.order };
|
|
131
|
+
if (Object.keys(segment).length > 0)
|
|
132
|
+
options.segment = segment;
|
|
133
|
+
}
|
|
134
|
+
return options;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Render the `QuerySpec` for one metric and an already-assembled option bag.
|
|
138
|
+
*
|
|
139
|
+
* This is the seam the stores use: a store's `runMetric` is exactly
|
|
140
|
+
* `run<Engine>Query(compileMetric(metric, projectId, options, <engine>Dialect))`,
|
|
141
|
+
* so the DSL reaches every engine through the one code path aggregations already
|
|
142
|
+
* take — and therefore through the same parity harness and the same numeric
|
|
143
|
+
* coercion.
|
|
144
|
+
*/
|
|
145
|
+
export function compileMetric(metric, projectId, options, dialect) {
|
|
146
|
+
const definition = getMetric(metric);
|
|
147
|
+
if (!definition?.builder) {
|
|
148
|
+
throw new Error(`metric '${metric}' has no aggregation builder and cannot be compiled`);
|
|
149
|
+
}
|
|
150
|
+
// `tier` is the one field of the bag that describes the *compilation* rather
|
|
151
|
+
// than the query, and it is set only by `toBuilderOptions` after
|
|
152
|
+
// `validateQuery` decided the metric can answer at another grain. A store
|
|
153
|
+
// calling `runMetric` with a plain option bag therefore keeps the delegated
|
|
154
|
+
// behaviour it has always had.
|
|
155
|
+
if (options.tier === "generic") {
|
|
156
|
+
return compileGenericGroupBy(definition, projectId, options, dialect);
|
|
157
|
+
}
|
|
158
|
+
return builderFor(definition.builder)(projectId, { ...options }, dialect);
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Compile a validated query end to end: filters → option bag → the metric's
|
|
162
|
+
* builder (or the generic one) → a `QuerySpec`.
|
|
163
|
+
*
|
|
164
|
+
* The tier is derived here rather than passed in, so a caller that has a
|
|
165
|
+
* validated query cannot compile it onto the wrong compiler; the collector,
|
|
166
|
+
* which already has the tier from `validateQuery`, may pass it to save the
|
|
167
|
+
* lookup.
|
|
168
|
+
*/
|
|
169
|
+
export function compileQuery(projectId, query, dialect, resolved = {}, tier) {
|
|
170
|
+
const definition = getMetric(query.metric);
|
|
171
|
+
const effective = tier ?? (definition == null ? "delegated" : queryTier(definition, query));
|
|
172
|
+
return compileMetric(query.metric, projectId, toBuilderOptions(query, resolved, effective), dialect);
|
|
173
|
+
}
|
|
174
|
+
//# sourceMappingURL=compile.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compile.js","sourceRoot":"","sources":["../../../src/query/dsl/compile.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **`explain`: can I trust this?** (ADR 0051 §3, design sketch §C.2).
|
|
3
|
+
*
|
|
4
|
+
* The second thing an agent cannot do today. A result that came back empty
|
|
5
|
+
* because the capture channel is switched off looks exactly like a result that
|
|
6
|
+
* came back empty because nothing happened — and a model will report the second
|
|
7
|
+
* either way. `explain: true` answers the request with the **plan** instead of
|
|
8
|
+
* the rows: which tier would run, the SQL that tier renders for this store's
|
|
9
|
+
* dialect, the parameters it binds, how much data is behind the window, and
|
|
10
|
+
* every reason the answer might not mean what it looks like.
|
|
11
|
+
*
|
|
12
|
+
* ## Why the SQL can be shown at all
|
|
13
|
+
*
|
|
14
|
+
* Because it contains nothing to redact. The DSL's whole design is that no
|
|
15
|
+
* caller-supplied value ever reaches the SQL text: a scene id, a mesh name, an
|
|
16
|
+
* event predicate and a row cap are all bound parameters, and
|
|
17
|
+
* `src/__tests__/queryDsl.test.ts` proves it by pushing a hostile string through
|
|
18
|
+
* every filter and asserting it appears only in `query_params`. So the rendered
|
|
19
|
+
* plan is the *shape* of the query with placeholders where the values are, and
|
|
20
|
+
* {@link explainSpec} lists the parameters by **name and logical type only** —
|
|
21
|
+
* never their values, which may be a session id or a mesh name the caller is not
|
|
22
|
+
* entitled to see echoed into a shared transcript.
|
|
23
|
+
*
|
|
24
|
+
* That is also what makes `explain` worth having: a reader can check for
|
|
25
|
+
* themselves that the filter they asked for is in the `WHERE`, and that nothing
|
|
26
|
+
* else is.
|
|
27
|
+
*
|
|
28
|
+
* ## `rowsScanned`
|
|
29
|
+
*
|
|
30
|
+
* No supported engine reports rows scanned without either a second pass or a
|
|
31
|
+
* dialect-specific `EXPLAIN ANALYZE`, and paying for either on a read endpoint
|
|
32
|
+
* would be a poor trade. What the collector *can* answer cheaply is the honest
|
|
33
|
+
* version of the same question — how many events of the metric's own capture
|
|
34
|
+
* channels exist in the window — from the `event_counts` pass it already runs to
|
|
35
|
+
* detect a disabled channel. `null` where the metric declares no channels (a
|
|
36
|
+
* derived rollup).
|
|
37
|
+
*/
|
|
38
|
+
import type { EventType } from "@uptimizr/schema";
|
|
39
|
+
import type { MetricDefinition, MetricId, QueryTier } from "@uptimizr/metrics";
|
|
40
|
+
import type { QuerySpec } from "../types.js";
|
|
41
|
+
import type { SampleSize } from "../summary/types.js";
|
|
42
|
+
/** The logical type of a bound parameter, as the plan reports it. */
|
|
43
|
+
export type ExplainParamType = "string" | "number" | "timestamp" | "boolean" | "json";
|
|
44
|
+
/** One bound parameter, named and typed — never valued. */
|
|
45
|
+
export interface ExplainParam {
|
|
46
|
+
name: string;
|
|
47
|
+
type: ExplainParamType;
|
|
48
|
+
}
|
|
49
|
+
/** The plan `explain: true` answers with. */
|
|
50
|
+
export interface QueryPlan {
|
|
51
|
+
metric: MetricId;
|
|
52
|
+
/** Which compiler would run it (design sketch §C.2). */
|
|
53
|
+
tier: QueryTier;
|
|
54
|
+
/** The store's engine, so the SQL below is readable in context. */
|
|
55
|
+
dialect: string;
|
|
56
|
+
/** The rendered SQL, parameters left as placeholders. */
|
|
57
|
+
sql: string;
|
|
58
|
+
/** Every parameter the SQL binds, by name and logical type. */
|
|
59
|
+
params: readonly ExplainParam[];
|
|
60
|
+
/**
|
|
61
|
+
* Events of the metric's own capture channels in the window — the cheap,
|
|
62
|
+
* honest stand-in for "rows scanned". `null` when the metric declares no
|
|
63
|
+
* channels, or when the collector could not count them.
|
|
64
|
+
*/
|
|
65
|
+
rowsScanned: number | null;
|
|
66
|
+
/** How much data is behind the window, on the metric's own terms. */
|
|
67
|
+
sampleSize: SampleSize;
|
|
68
|
+
/** Every reason the answer might not mean what it looks like. */
|
|
69
|
+
warnings: readonly string[];
|
|
70
|
+
}
|
|
71
|
+
/** Everything the collector knows that the plan should say. */
|
|
72
|
+
export interface PlanContext {
|
|
73
|
+
tier: QueryTier;
|
|
74
|
+
/** The store's dialect name (`duckdb`, `clickhouse`, …). */
|
|
75
|
+
dialect: string;
|
|
76
|
+
/** Per-event-type counts over the query's range, for the channel warnings. */
|
|
77
|
+
channelCounts?: Readonly<Record<string, number>>;
|
|
78
|
+
/** How much data is behind the result. */
|
|
79
|
+
sampleSize?: SampleSize;
|
|
80
|
+
/** The row cap in force, and the rows the query produced, for truncation. */
|
|
81
|
+
limit?: number;
|
|
82
|
+
rows?: number;
|
|
83
|
+
/** Whether the project has the scene geometry a spatial label would need. */
|
|
84
|
+
spatial?: {
|
|
85
|
+
proxy: boolean;
|
|
86
|
+
regions: number;
|
|
87
|
+
};
|
|
88
|
+
/** Extra warnings the route knows and this module cannot. */
|
|
89
|
+
extra?: readonly string[];
|
|
90
|
+
}
|
|
91
|
+
/** The SQL and the parameter list of a compiled spec, with values withheld. */
|
|
92
|
+
export declare function explainSpec(spec: QuerySpec): {
|
|
93
|
+
sql: string;
|
|
94
|
+
params: ExplainParam[];
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* The capture channels that produced nothing in this window.
|
|
98
|
+
*
|
|
99
|
+
* A channel with a zero count is the single most common reason an Uptimizr
|
|
100
|
+
* answer is empty and wrong: capture is a per-project dial (ADR 0012), and a
|
|
101
|
+
* project that never turned on gaze raycasting has no attention data at all —
|
|
102
|
+
* which is a different statement from "nobody looked at anything".
|
|
103
|
+
*/
|
|
104
|
+
export declare function silentChannels(metric: MetricDefinition, counts: Readonly<Record<string, number>> | undefined): readonly EventType[];
|
|
105
|
+
/** Events of the metric's own channels in the window; `null` when unknowable. */
|
|
106
|
+
export declare function channelRows(metric: MetricDefinition, counts: Readonly<Record<string, number>> | undefined): number | null;
|
|
107
|
+
/**
|
|
108
|
+
* Every reason this result might not mean what it looks like, in the order a
|
|
109
|
+
* reader should weigh them: no data at all, then too little data, then data that
|
|
110
|
+
* has been cut off, then a presentational limit.
|
|
111
|
+
*/
|
|
112
|
+
export declare function planWarnings(metric: MetricDefinition, ctx: PlanContext, sampleSize: SampleSize): string[];
|
|
113
|
+
/** Assemble the plan `explain: true` answers with. */
|
|
114
|
+
export declare function explainQuery(metric: MetricDefinition, spec: QuerySpec, ctx: PlanContext): QueryPlan;
|
|
115
|
+
//# sourceMappingURL=explain.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"explain.d.ts","sourceRoot":"","sources":["../../../src/query/dsl/explain.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,KAAK,EAAE,gBAAgB,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAC/E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAEtD,qEAAqE;AACrE,MAAM,MAAM,gBAAgB,GAAG,QAAQ,GAAG,QAAQ,GAAG,WAAW,GAAG,SAAS,GAAG,MAAM,CAAC;AAEtF,2DAA2D;AAC3D,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,gBAAgB,CAAC;CACxB;AAED,6CAA6C;AAC7C,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,QAAQ,CAAC;IACjB,wDAAwD;IACxD,IAAI,EAAE,SAAS,CAAC;IAChB,mEAAmE;IACnE,OAAO,EAAE,MAAM,CAAC;IAChB,yDAAyD;IACzD,GAAG,EAAE,MAAM,CAAC;IACZ,+DAA+D;IAC/D,MAAM,EAAE,SAAS,YAAY,EAAE,CAAC;IAChC;;;;OAIG;IACH,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,qEAAqE;IACrE,UAAU,EAAE,UAAU,CAAC;IACvB,iEAAiE;IACjE,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;CAC7B;AAED,+DAA+D;AAC/D,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,SAAS,CAAC;IAChB,4DAA4D;IAC5D,OAAO,EAAE,MAAM,CAAC;IAChB,8EAA8E;IAC9E,aAAa,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACjD,0CAA0C;IAC1C,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,6EAA6E;IAC7E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,6EAA6E;IAC7E,OAAO,CAAC,EAAE;QAAE,KAAK,EAAE,OAAO,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IAC9C,6DAA6D;IAC7D,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC3B;AA2BD,+EAA+E;AAC/E,wBAAgB,WAAW,CAAC,IAAI,EAAE,SAAS,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,YAAY,EAAE,CAAA;CAAE,CAOpF;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAC5B,MAAM,EAAE,gBAAgB,EACxB,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GAAG,SAAS,GACnD,SAAS,SAAS,EAAE,CAGtB;AAED,iFAAiF;AACjF,wBAAgB,WAAW,CACzB,MAAM,EAAE,gBAAgB,EACxB,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GAAG,SAAS,GACnD,MAAM,GAAG,IAAI,CAGf;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAC1B,MAAM,EAAE,gBAAgB,EACxB,GAAG,EAAE,WAAW,EAChB,UAAU,EAAE,UAAU,GACrB,MAAM,EAAE,CA6CV;AAED,sDAAsD;AACtD,wBAAgB,YAAY,CAC1B,MAAM,EAAE,gBAAgB,EACxB,IAAI,EAAE,SAAS,EACf,GAAG,EAAE,WAAW,GACf,SAAS,CAaX"}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **`explain`: can I trust this?** (ADR 0051 §3, design sketch §C.2).
|
|
3
|
+
*
|
|
4
|
+
* The second thing an agent cannot do today. A result that came back empty
|
|
5
|
+
* because the capture channel is switched off looks exactly like a result that
|
|
6
|
+
* came back empty because nothing happened — and a model will report the second
|
|
7
|
+
* either way. `explain: true` answers the request with the **plan** instead of
|
|
8
|
+
* the rows: which tier would run, the SQL that tier renders for this store's
|
|
9
|
+
* dialect, the parameters it binds, how much data is behind the window, and
|
|
10
|
+
* every reason the answer might not mean what it looks like.
|
|
11
|
+
*
|
|
12
|
+
* ## Why the SQL can be shown at all
|
|
13
|
+
*
|
|
14
|
+
* Because it contains nothing to redact. The DSL's whole design is that no
|
|
15
|
+
* caller-supplied value ever reaches the SQL text: a scene id, a mesh name, an
|
|
16
|
+
* event predicate and a row cap are all bound parameters, and
|
|
17
|
+
* `src/__tests__/queryDsl.test.ts` proves it by pushing a hostile string through
|
|
18
|
+
* every filter and asserting it appears only in `query_params`. So the rendered
|
|
19
|
+
* plan is the *shape* of the query with placeholders where the values are, and
|
|
20
|
+
* {@link explainSpec} lists the parameters by **name and logical type only** —
|
|
21
|
+
* never their values, which may be a session id or a mesh name the caller is not
|
|
22
|
+
* entitled to see echoed into a shared transcript.
|
|
23
|
+
*
|
|
24
|
+
* That is also what makes `explain` worth having: a reader can check for
|
|
25
|
+
* themselves that the filter they asked for is in the `WHERE`, and that nothing
|
|
26
|
+
* else is.
|
|
27
|
+
*
|
|
28
|
+
* ## `rowsScanned`
|
|
29
|
+
*
|
|
30
|
+
* No supported engine reports rows scanned without either a second pass or a
|
|
31
|
+
* dialect-specific `EXPLAIN ANALYZE`, and paying for either on a read endpoint
|
|
32
|
+
* would be a poor trade. What the collector *can* answer cheaply is the honest
|
|
33
|
+
* version of the same question — how many events of the metric's own capture
|
|
34
|
+
* channels exist in the window — from the `event_counts` pass it already runs to
|
|
35
|
+
* detect a disabled channel. `null` where the metric declares no channels (a
|
|
36
|
+
* derived rollup).
|
|
37
|
+
*/
|
|
38
|
+
/** Collapse the rendered SQL to something a human can read in a transcript. */
|
|
39
|
+
function tidy(sql) {
|
|
40
|
+
return sql
|
|
41
|
+
.split("\n")
|
|
42
|
+
.map((line) => line.trimEnd())
|
|
43
|
+
.filter((line) => line.trim().length > 0)
|
|
44
|
+
.map((line) => line.replace(/^ {6}/, ""))
|
|
45
|
+
.join("\n")
|
|
46
|
+
.trim();
|
|
47
|
+
}
|
|
48
|
+
/** The logical type of a bound value, without ever reporting the value. */
|
|
49
|
+
function typeOf(value) {
|
|
50
|
+
if (value instanceof Date)
|
|
51
|
+
return "timestamp";
|
|
52
|
+
if (typeof value === "number")
|
|
53
|
+
return "number";
|
|
54
|
+
if (typeof value === "boolean")
|
|
55
|
+
return "boolean";
|
|
56
|
+
if (typeof value === "string") {
|
|
57
|
+
// Every dialect binds a timestamp as either a `Date` or an ISO-8601 /
|
|
58
|
+
// naive-UTC string; `2024-06-16T10:00:00.000` is not a value anyone supplied
|
|
59
|
+
// as text, so reporting it as a timestamp is more useful than "string".
|
|
60
|
+
return /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}/.test(value) ? "timestamp" : "string";
|
|
61
|
+
}
|
|
62
|
+
return "json";
|
|
63
|
+
}
|
|
64
|
+
/** The SQL and the parameter list of a compiled spec, with values withheld. */
|
|
65
|
+
export function explainSpec(spec) {
|
|
66
|
+
return {
|
|
67
|
+
sql: tidy(spec.query),
|
|
68
|
+
params: Object.entries(spec.query_params)
|
|
69
|
+
.map(([name, value]) => ({ name, type: typeOf(value) }))
|
|
70
|
+
.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0)),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The capture channels that produced nothing in this window.
|
|
75
|
+
*
|
|
76
|
+
* A channel with a zero count is the single most common reason an Uptimizr
|
|
77
|
+
* answer is empty and wrong: capture is a per-project dial (ADR 0012), and a
|
|
78
|
+
* project that never turned on gaze raycasting has no attention data at all —
|
|
79
|
+
* which is a different statement from "nobody looked at anything".
|
|
80
|
+
*/
|
|
81
|
+
export function silentChannels(metric, counts) {
|
|
82
|
+
if (counts == null)
|
|
83
|
+
return [];
|
|
84
|
+
return metric.sourceChannels.filter((channel) => (counts[channel] ?? 0) === 0);
|
|
85
|
+
}
|
|
86
|
+
/** Events of the metric's own channels in the window; `null` when unknowable. */
|
|
87
|
+
export function channelRows(metric, counts) {
|
|
88
|
+
if (counts == null || metric.sourceChannels.length === 0)
|
|
89
|
+
return null;
|
|
90
|
+
return metric.sourceChannels.reduce((sum, channel) => sum + (counts[channel] ?? 0), 0);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Every reason this result might not mean what it looks like, in the order a
|
|
94
|
+
* reader should weigh them: no data at all, then too little data, then data that
|
|
95
|
+
* has been cut off, then a presentational limit.
|
|
96
|
+
*/
|
|
97
|
+
export function planWarnings(metric, ctx, sampleSize) {
|
|
98
|
+
const warnings = [];
|
|
99
|
+
const silent = silentChannels(metric, ctx.channelCounts);
|
|
100
|
+
if (silent.length > 0) {
|
|
101
|
+
warnings.push(`No \`${silent.join("`, `")}\` events exist in this project over the selected range, and ` +
|
|
102
|
+
`"${metric.id}" is computed from ${metric.sourceChannels.map((c) => `\`${c}\``).join(", ")}. ` +
|
|
103
|
+
"That is almost always a capture channel switched off (ADR 0012), not an absence of " +
|
|
104
|
+
"behaviour — check the project's capture options before reporting a zero.");
|
|
105
|
+
}
|
|
106
|
+
const minSample = metric.comparable?.minSample;
|
|
107
|
+
const observed = sampleSize.events ?? sampleSize.sessions;
|
|
108
|
+
if (minSample != null && observed != null && observed < minSample) {
|
|
109
|
+
warnings.push(`Only ${observed} events of this metric's capture channels exist in the selected window; ` +
|
|
110
|
+
`"${metric.id}" declares ${minSample} as the minimum at which a change is worth ` +
|
|
111
|
+
"reporting. Treat the numbers as directional.");
|
|
112
|
+
}
|
|
113
|
+
if ((metric.grain === "voxel" || metric.grain === "bin") &&
|
|
114
|
+
ctx.spatial != null &&
|
|
115
|
+
!ctx.spatial.proxy &&
|
|
116
|
+
ctx.spatial.regions === 0) {
|
|
117
|
+
warnings.push("This project has no scene proxy and no registered regions, so spatial results can only be " +
|
|
118
|
+
"reported as grid indices — there is nothing to name a hotspot after. Upload a scene " +
|
|
119
|
+
"proxy (`PUT /api/v1/scenes/:sceneId/proxy`) or register regions to get labels.");
|
|
120
|
+
}
|
|
121
|
+
if (ctx.limit != null && ctx.rows != null && ctx.rows >= ctx.limit) {
|
|
122
|
+
warnings.push(`The result hit its ${ctx.limit}-row cap, so totals and shares describe the returned rows ` +
|
|
123
|
+
"only. Narrow the query with a filter rather than raising the cap.");
|
|
124
|
+
}
|
|
125
|
+
warnings.push(...(ctx.extra ?? []));
|
|
126
|
+
return warnings;
|
|
127
|
+
}
|
|
128
|
+
/** Assemble the plan `explain: true` answers with. */
|
|
129
|
+
export function explainQuery(metric, spec, ctx) {
|
|
130
|
+
const sampleSize = ctx.sampleSize ?? { sessions: null, events: null };
|
|
131
|
+
const { sql, params } = explainSpec(spec);
|
|
132
|
+
return {
|
|
133
|
+
metric: metric.id,
|
|
134
|
+
tier: ctx.tier,
|
|
135
|
+
dialect: ctx.dialect,
|
|
136
|
+
sql,
|
|
137
|
+
params,
|
|
138
|
+
rowsScanned: channelRows(metric, ctx.channelCounts),
|
|
139
|
+
sampleSize,
|
|
140
|
+
warnings: planWarnings(metric, ctx, sampleSize),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
//# sourceMappingURL=explain.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"explain.js","sourceRoot":"","sources":["../../../src/query/dsl/explain.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **The generic group-by compiler** (ADR 0051 §3, design sketch §C.2, tier 2).
|
|
3
|
+
*
|
|
4
|
+
* The delegated tier (`compile.ts`) can run any metric, but only at the grain
|
|
5
|
+
* its builder renders: `top_meshes` is one row per mesh and nothing else. That
|
|
6
|
+
* is right for a spatial binning or a percentile — those measures *are* their
|
|
7
|
+
* grain — but wrong for a count. "How many mesh interactions per input source"
|
|
8
|
+
* and "…per scene" are the same question about the same events, and the only
|
|
9
|
+
* reason they needed two endpoints is that somebody wrote two `GROUP BY`s.
|
|
10
|
+
*
|
|
11
|
+
* So a metric that declares `genericGroupBy` in the registry (a portable
|
|
12
|
+
* `count` / `count(DISTINCT session_id)` / `sum` / `avg` / `max` over promoted
|
|
13
|
+
* columns) gets one shared, dialect-authored builder that renders
|
|
14
|
+
*
|
|
15
|
+
* ```sql
|
|
16
|
+
* SELECT <dimensions>, <measures>
|
|
17
|
+
* FROM events [LEFT JOIN session_attrs …]
|
|
18
|
+
* WHERE project_id = ? AND <event types> AND <scope> AND <range> AND <filters>
|
|
19
|
+
* GROUP BY <dimensions>
|
|
20
|
+
* ORDER BY <measure> DESC, <dimensions>
|
|
21
|
+
* LIMIT ?
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* for **any** subset of the dimensions that metric declares.
|
|
25
|
+
*
|
|
26
|
+
* ## Why this is still only one SQL path
|
|
27
|
+
*
|
|
28
|
+
* Everything variable in the SQL above comes from **registry data**, never from
|
|
29
|
+
* the request: the event types and the scope predicate are the metric's
|
|
30
|
+
* `genericGroupBy`, the measures are its declared columns, and each dimension's
|
|
31
|
+
* expression is chosen from a closed table keyed by `DimensionId`. Every value a
|
|
32
|
+
* caller supplies — a scene id, a mesh name, an event predicate — reaches the
|
|
33
|
+
* SQL as a bound parameter through the same {@link ParamBag} the canned
|
|
34
|
+
* aggregations use. There is no string interpolation of caller input anywhere in
|
|
35
|
+
* this file, which is what makes a grammar with a `GROUP BY` in it still a
|
|
36
|
+
* closed grammar.
|
|
37
|
+
*
|
|
38
|
+
* ## Portability
|
|
39
|
+
*
|
|
40
|
+
* The promoted dimensions (`scene_id`, `session_id`, `mesh`, `name`, `source`,
|
|
41
|
+
* `event_type`) are plain columns on every engine. The session attributes
|
|
42
|
+
* (`cameraMode`, `device.*`) live in the `session_start` payload, so they are
|
|
43
|
+
* read through `Dialect.jsonText` in one CTE and `LEFT JOIN`ed on `session_id`
|
|
44
|
+
* — the same shape `buildPerfByDevice` has always used. `GROUP BY` repeats the
|
|
45
|
+
* expressions rather than naming the output aliases, because T-SQL rejects the
|
|
46
|
+
* latter (the mssql dialect rewrites it, but not relying on the rewrite is
|
|
47
|
+
* cheaper than relying on it).
|
|
48
|
+
*
|
|
49
|
+
* ## Why it is not called `build*`
|
|
50
|
+
*
|
|
51
|
+
* Every `build*` export of this package is a dialect-agnostic aggregation with
|
|
52
|
+
* the signature `(projectId, opts, dialect)`, and the optional stores' suites
|
|
53
|
+
* enumerate them by that prefix to run each one against DuckDB. This one takes a
|
|
54
|
+
* `MetricDefinition` first, because the metric *is* its configuration — so it is
|
|
55
|
+
* a `compile*`, next to `compileMetric` and `compileQuery`, and the prefix keeps
|
|
56
|
+
* meaning what it has always meant.
|
|
57
|
+
*
|
|
58
|
+
* The one session attribute deliberately left out is `device.isMobile`: it is a
|
|
59
|
+
* boolean, and a group-by on it would key rows by `"true"` on one engine and `1`
|
|
60
|
+
* on another. `GENERIC_DIMENSIONS` in the registry says so, and `validateQuery`
|
|
61
|
+
* refuses it by name.
|
|
62
|
+
*/
|
|
63
|
+
import { type DimensionId, type MetricDefinition } from "@uptimizr/metrics";
|
|
64
|
+
import { type Dialect } from "../dialect.js";
|
|
65
|
+
import type { QuerySpec } from "../types.js";
|
|
66
|
+
/**
|
|
67
|
+
* A funnel-step predicate (ADR 0038) as the generic tier takes it. Structurally
|
|
68
|
+
* the schema's `FunnelStep`, restated as a local shape so this module keeps no
|
|
69
|
+
* dependency on `@uptimizr/schema`.
|
|
70
|
+
*/
|
|
71
|
+
export interface GenericEventPredicate {
|
|
72
|
+
type: string;
|
|
73
|
+
name?: string;
|
|
74
|
+
mesh?: string;
|
|
75
|
+
}
|
|
76
|
+
/** Device attributes a generic query can be scoped to. */
|
|
77
|
+
export interface GenericDeviceFilter {
|
|
78
|
+
os?: string;
|
|
79
|
+
browser?: string;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* The option bag the generic builder takes. Assembled by
|
|
83
|
+
* {@link import("./compile.js").toBuilderOptions} from a validated query; every
|
|
84
|
+
* field is already registry-checked by the time it gets here.
|
|
85
|
+
*/
|
|
86
|
+
export interface GenericQueryOptions {
|
|
87
|
+
/** Inclusive lower bound (epoch ms). */
|
|
88
|
+
since?: number;
|
|
89
|
+
/** Exclusive upper bound (epoch ms). */
|
|
90
|
+
until?: number;
|
|
91
|
+
/** The grain to group by. Empty means "one row for the whole range". */
|
|
92
|
+
dimensions?: readonly DimensionId[];
|
|
93
|
+
scene?: string;
|
|
94
|
+
session?: string;
|
|
95
|
+
source?: string;
|
|
96
|
+
mesh?: string;
|
|
97
|
+
/** The stored `scene.cameraType`, already mapped from the DSL's `cameraMode`. */
|
|
98
|
+
cameraType?: string;
|
|
99
|
+
/** Restrict to sessions whose `session_start` declares these attributes. */
|
|
100
|
+
device?: GenericDeviceFilter;
|
|
101
|
+
/** Restrict to sessions in which at least one matching event occurred. */
|
|
102
|
+
event?: GenericEventPredicate;
|
|
103
|
+
/** Dimensions held fixed at a value, keyed by `DimensionId`. */
|
|
104
|
+
segment?: Readonly<Record<string, string>>;
|
|
105
|
+
/** Result order; defaults to the first measure, descending. */
|
|
106
|
+
order?: {
|
|
107
|
+
by: string;
|
|
108
|
+
dir: "asc" | "desc";
|
|
109
|
+
};
|
|
110
|
+
/** Row cap. Always applied — a generic group-by is unbounded without one. */
|
|
111
|
+
limit?: number;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Compile a generic group-by query for one metric.
|
|
115
|
+
*
|
|
116
|
+
* `metric` must declare `genericGroupBy` — the caller (`compileMetric`) has
|
|
117
|
+
* already checked that, and `validateQuery` before it, so the throw below guards
|
|
118
|
+
* a programming error rather than a client one.
|
|
119
|
+
*/
|
|
120
|
+
export declare function compileGenericGroupBy(metric: MetricDefinition, projectId: string, opts: GenericQueryOptions, d: Dialect): QuerySpec;
|
|
121
|
+
/**
|
|
122
|
+
* The columns a generic group-by result carries, in order: the grouping
|
|
123
|
+
* dimensions, then the metric's measures. Used by the explain plan and by the
|
|
124
|
+
* tests that assert a generic row is self-describing.
|
|
125
|
+
*/
|
|
126
|
+
export declare function genericResultColumns(metric: MetricDefinition, dimensions: readonly DimensionId[]): readonly string[];
|
|
127
|
+
//# sourceMappingURL=generic.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generic.d.ts","sourceRoot":"","sources":["../../../src/query/dsl/generic.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6DG;AAEH,OAAO,EAEL,KAAK,WAAW,EAEhB,KAAK,gBAAgB,EAEtB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAA2C,KAAK,OAAO,EAAE,MAAM,eAAe,CAAC;AACtF,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAE7C;;;;GAIG;AACH,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,0DAA0D;AAC1D,MAAM,WAAW,mBAAmB;IAClC,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;GAIG;AACH,MAAM,WAAW,mBAAmB;IAClC,wCAAwC;IACxC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,wCAAwC;IACxC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,wEAAwE;IACxE,UAAU,CAAC,EAAE,SAAS,WAAW,EAAE,CAAC;IACpC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,iFAAiF;IACjF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,4EAA4E;IAC5E,MAAM,CAAC,EAAE,mBAAmB,CAAC;IAC7B,0EAA0E;IAC1E,KAAK,CAAC,EAAE,qBAAqB,CAAC;IAC9B,gEAAgE;IAChE,OAAO,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAC3C,+DAA+D;IAC/D,KAAK,CAAC,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,KAAK,GAAG,MAAM,CAAA;KAAE,CAAC;IAC5C,6EAA6E;IAC7E,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AA4OD;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,gBAAgB,EACxB,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,mBAAmB,EACzB,CAAC,EAAE,OAAO,GACT,SAAS,CA+DX;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,gBAAgB,EACxB,UAAU,EAAE,SAAS,WAAW,EAAE,GACjC,SAAS,MAAM,EAAE,CAKnB"}
|