measurr 0.3.0 → 0.5.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/README.md +175 -28
- package/dist/analytics.d.ts +47 -4
- package/dist/dataset.d.ts +50 -30
- package/dist/handler.d.ts +56 -0
- package/dist/handler.js +49 -0
- package/dist/index-g255jb1s.js +149 -0
- package/dist/index-g2mjqx4f.js +303 -0
- package/dist/{index-wrr67sxq.js → index-zgb1e89d.js} +160 -451
- package/dist/index.d.ts +1 -1
- package/dist/index.js +7 -5
- package/dist/period.d.ts +5 -0
- package/dist/shapes.d.ts +111 -0
- package/dist/shapes.js +134 -0
- package/dist/testing.d.ts +9 -4
- package/dist/testing.js +13 -3
- package/package.json +14 -2
- package/src/analytics.ts +337 -40
- package/src/dataset.ts +135 -60
- package/src/handler.ts +124 -0
- package/src/index.ts +3 -0
- package/src/period.ts +32 -0
- package/src/plan.ts +6 -0
- package/src/shapes.ts +295 -0
- package/src/testing.ts +31 -9
package/dist/handler.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AnalyticsError2
|
|
3
|
+
} from "./index-zgb1e89d.js";
|
|
4
|
+
|
|
5
|
+
// src/handler.ts
|
|
6
|
+
async function queryBatch(analytics, queries, ctx) {
|
|
7
|
+
const results = {};
|
|
8
|
+
const errors = {};
|
|
9
|
+
for await (const chunk of streamBatch(analytics, queries, ctx)) {
|
|
10
|
+
if ("error" in chunk)
|
|
11
|
+
errors[chunk.key] = chunk.error;
|
|
12
|
+
else
|
|
13
|
+
results[chunk.key] = chunk.result;
|
|
14
|
+
}
|
|
15
|
+
return { results, errors };
|
|
16
|
+
}
|
|
17
|
+
async function* streamBatch(analytics, queries, ctx) {
|
|
18
|
+
const pending = new Map(Object.entries(queries).map(([key, query]) => {
|
|
19
|
+
const settled = analytics.query(query, ctx).then((result) => ({ key, result }), (error) => ({ key, error: batchError(error) }));
|
|
20
|
+
return [key, settled];
|
|
21
|
+
}));
|
|
22
|
+
while (pending.size > 0) {
|
|
23
|
+
const chunk = await Promise.race(pending.values());
|
|
24
|
+
pending.delete(chunk.key);
|
|
25
|
+
yield chunk;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function batchError(error) {
|
|
29
|
+
return error instanceof AnalyticsError2 ? { code: error.code, message: error.message } : { code: "internal", message: "The query failed" };
|
|
30
|
+
}
|
|
31
|
+
function toHttpError(error) {
|
|
32
|
+
if (error.code === "forbidden") {
|
|
33
|
+
return { status: 403, code: "FORBIDDEN", message: error.message };
|
|
34
|
+
}
|
|
35
|
+
if (error.code === "missing_tenant") {
|
|
36
|
+
return { status: 401, code: "UNAUTHORIZED", message: error.message };
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
status: 400,
|
|
40
|
+
code: "BAD_REQUEST",
|
|
41
|
+
message: error.message,
|
|
42
|
+
data: { code: error.code, details: error.details }
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
export {
|
|
46
|
+
queryBatch,
|
|
47
|
+
streamBatch,
|
|
48
|
+
toHttpError
|
|
49
|
+
};
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// src/period.ts
|
|
2
|
+
var MAX_PERIOD_DAYS = 731;
|
|
3
|
+
var DEFAULT_PERIOD = {
|
|
4
|
+
last: { days: 30 }
|
|
5
|
+
};
|
|
6
|
+
var DAY_MS = 86400000;
|
|
7
|
+
var formatterCache = new Map;
|
|
8
|
+
function formatter(timezone) {
|
|
9
|
+
let cached = formatterCache.get(timezone);
|
|
10
|
+
if (!cached) {
|
|
11
|
+
cached = new Intl.DateTimeFormat("en-US", {
|
|
12
|
+
timeZone: timezone,
|
|
13
|
+
hourCycle: "h23",
|
|
14
|
+
year: "numeric",
|
|
15
|
+
month: "2-digit",
|
|
16
|
+
day: "2-digit",
|
|
17
|
+
hour: "2-digit",
|
|
18
|
+
minute: "2-digit",
|
|
19
|
+
second: "2-digit"
|
|
20
|
+
});
|
|
21
|
+
formatterCache.set(timezone, cached);
|
|
22
|
+
}
|
|
23
|
+
return cached;
|
|
24
|
+
}
|
|
25
|
+
function isValidTimezone(timezone) {
|
|
26
|
+
if (!/^[A-Za-z][A-Za-z0-9_+\-/]*$/.test(timezone))
|
|
27
|
+
return false;
|
|
28
|
+
try {
|
|
29
|
+
formatter(timezone);
|
|
30
|
+
return true;
|
|
31
|
+
} catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function localParts(instant, timezone) {
|
|
36
|
+
const parts = {};
|
|
37
|
+
for (const part of formatter(timezone).formatToParts(instant)) {
|
|
38
|
+
if (part.type !== "literal")
|
|
39
|
+
parts[part.type] = Number(part.value);
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
year: parts.year ?? 0,
|
|
43
|
+
month: parts.month ?? 1,
|
|
44
|
+
day: parts.day ?? 1,
|
|
45
|
+
hour: parts.hour ?? 0,
|
|
46
|
+
minute: parts.minute ?? 0,
|
|
47
|
+
second: parts.second ?? 0
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function offsetAt(instant, timezone) {
|
|
51
|
+
const p = localParts(instant, timezone);
|
|
52
|
+
const asUtc = Date.UTC(p.year, p.month - 1, p.day, p.hour, p.minute, p.second);
|
|
53
|
+
return asUtc - (instant.getTime() - instant.getUTCMilliseconds());
|
|
54
|
+
}
|
|
55
|
+
function localDate(instant, timezone) {
|
|
56
|
+
const p = localParts(instant, timezone);
|
|
57
|
+
return formatDate(Date.UTC(p.year, p.month - 1, p.day));
|
|
58
|
+
}
|
|
59
|
+
function startOfLocalDay(date, timezone) {
|
|
60
|
+
const wallClock = parseDate(date);
|
|
61
|
+
const firstGuess = wallClock - offsetAt(new Date(wallClock), timezone);
|
|
62
|
+
const corrected = wallClock - offsetAt(new Date(firstGuess), timezone);
|
|
63
|
+
return new Date(corrected);
|
|
64
|
+
}
|
|
65
|
+
function parseDate(date) {
|
|
66
|
+
const [year, month, day] = date.split("-").map(Number);
|
|
67
|
+
return Date.UTC(year ?? 0, (month ?? 1) - 1, day ?? 1);
|
|
68
|
+
}
|
|
69
|
+
function formatDate(utcMs) {
|
|
70
|
+
return new Date(utcMs).toISOString().slice(0, 10);
|
|
71
|
+
}
|
|
72
|
+
function addDays(date, days) {
|
|
73
|
+
return formatDate(parseDate(date) + days * DAY_MS);
|
|
74
|
+
}
|
|
75
|
+
function daysBetween(from, to) {
|
|
76
|
+
return Math.round((parseDate(to) - parseDate(from)) / DAY_MS);
|
|
77
|
+
}
|
|
78
|
+
function isoWeekday(date) {
|
|
79
|
+
const day = new Date(parseDate(date)).getUTCDay();
|
|
80
|
+
return day === 0 ? 7 : day;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
class PeriodError extends Error {
|
|
84
|
+
name = "PeriodError";
|
|
85
|
+
}
|
|
86
|
+
function resolvePeriod(input, timezone, now) {
|
|
87
|
+
const today = localDate(now, timezone);
|
|
88
|
+
const [from, to] = periodDays(input, today);
|
|
89
|
+
if (daysBetween(from, to) < 0) {
|
|
90
|
+
throw new PeriodError(`The period starts (${from}) after it ends (${to}).`);
|
|
91
|
+
}
|
|
92
|
+
return periodFromDays(from, to, timezone);
|
|
93
|
+
}
|
|
94
|
+
function periodDays(input, today) {
|
|
95
|
+
if ("last" in input)
|
|
96
|
+
return [addDays(today, -(input.last.days - 1)), today];
|
|
97
|
+
if ("from" in input)
|
|
98
|
+
return [input.from, input.to];
|
|
99
|
+
const monthStart = `${today.slice(0, 8)}01`;
|
|
100
|
+
const weekStart = addDays(today, -(isoWeekday(today) - 1));
|
|
101
|
+
switch (input.preset) {
|
|
102
|
+
case "today":
|
|
103
|
+
return [today, today];
|
|
104
|
+
case "yesterday":
|
|
105
|
+
return [addDays(today, -1), addDays(today, -1)];
|
|
106
|
+
case "thisWeek":
|
|
107
|
+
return [weekStart, today];
|
|
108
|
+
case "lastWeek":
|
|
109
|
+
return [addDays(weekStart, -7), addDays(weekStart, -1)];
|
|
110
|
+
case "thisMonth":
|
|
111
|
+
return [monthStart, today];
|
|
112
|
+
case "lastMonth": {
|
|
113
|
+
const lastMonthEnd = addDays(monthStart, -1);
|
|
114
|
+
return [`${lastMonthEnd.slice(0, 8)}01`, lastMonthEnd];
|
|
115
|
+
}
|
|
116
|
+
case "thisYear":
|
|
117
|
+
return [`${today.slice(0, 4)}-01-01`, today];
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function periodFromDays(from, to, timezone) {
|
|
121
|
+
const days = daysBetween(from, to) + 1;
|
|
122
|
+
if (days > MAX_PERIOD_DAYS) {
|
|
123
|
+
throw new PeriodError(`A period may cover at most ${MAX_PERIOD_DAYS} days (two years); ${from} to ${to} is ${days} days.`);
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
from,
|
|
127
|
+
to,
|
|
128
|
+
timezone,
|
|
129
|
+
start: startOfLocalDay(from, timezone),
|
|
130
|
+
end: startOfLocalDay(addDays(to, 1), timezone),
|
|
131
|
+
days
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
function previousPeriod(period) {
|
|
135
|
+
const to = addDays(period.from, -1);
|
|
136
|
+
const from = addDays(to, -(period.days - 1));
|
|
137
|
+
return periodFromDays(from, to, period.timezone);
|
|
138
|
+
}
|
|
139
|
+
function bucketStarts(from, to, unit) {
|
|
140
|
+
let start = unit === "day" ? from : unit === "week" ? addDays(from, -(isoWeekday(from) - 1)) : `${from.slice(0, 8)}01`;
|
|
141
|
+
const starts = [];
|
|
142
|
+
while (start <= to) {
|
|
143
|
+
starts.push(start);
|
|
144
|
+
start = unit === "day" ? addDays(start, 1) : unit === "week" ? addDays(start, 7) : formatDate(Date.UTC(Number(start.slice(0, 4)), Number(start.slice(5, 7)), 1));
|
|
145
|
+
}
|
|
146
|
+
return starts;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export { MAX_PERIOD_DAYS, DEFAULT_PERIOD, isValidTimezone, PeriodError, resolvePeriod, previousPeriod, bucketStarts };
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import {
|
|
2
|
+
rootOf,
|
|
3
|
+
directChildren
|
|
4
|
+
} from "./index-e6k30sfp.js";
|
|
5
|
+
import {
|
|
6
|
+
DEFAULT_PERIOD
|
|
7
|
+
} from "./index-g255jb1s.js";
|
|
8
|
+
|
|
9
|
+
// src/dataset.ts
|
|
10
|
+
function aggregate(result, fn, arg, options) {
|
|
11
|
+
return {
|
|
12
|
+
kind: "aggregate",
|
|
13
|
+
result,
|
|
14
|
+
label: options.label,
|
|
15
|
+
description: options.description,
|
|
16
|
+
access: options.access,
|
|
17
|
+
node: {
|
|
18
|
+
kind: "aggregate",
|
|
19
|
+
fn,
|
|
20
|
+
arg: arg?.node ?? null,
|
|
21
|
+
where: options.where?.node ?? null
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
var measure2 = {
|
|
26
|
+
count(options) {
|
|
27
|
+
return aggregate("count", "count", null, options);
|
|
28
|
+
},
|
|
29
|
+
countWhere(condition, options) {
|
|
30
|
+
return aggregate("count", "count", null, { ...options, where: condition });
|
|
31
|
+
},
|
|
32
|
+
countDistinct(expr, options) {
|
|
33
|
+
return aggregate("count", "countDistinct", expr, options);
|
|
34
|
+
},
|
|
35
|
+
sum(expr, options) {
|
|
36
|
+
return aggregate("number", "sum", expr, options);
|
|
37
|
+
},
|
|
38
|
+
avg(expr, options) {
|
|
39
|
+
return aggregate("number", "avg", expr, options);
|
|
40
|
+
},
|
|
41
|
+
median(expr, options) {
|
|
42
|
+
return aggregate("number", "median", expr, options);
|
|
43
|
+
},
|
|
44
|
+
share(options) {
|
|
45
|
+
return {
|
|
46
|
+
kind: "share",
|
|
47
|
+
result: "ratio",
|
|
48
|
+
label: options.label,
|
|
49
|
+
description: options.description,
|
|
50
|
+
access: options.access,
|
|
51
|
+
numerator: options.numerator.node,
|
|
52
|
+
denominator: options.denominator?.node ?? null
|
|
53
|
+
};
|
|
54
|
+
},
|
|
55
|
+
ratio(numerator, denominator, options) {
|
|
56
|
+
return {
|
|
57
|
+
kind: "ratio",
|
|
58
|
+
result: "ratio",
|
|
59
|
+
label: options.label,
|
|
60
|
+
description: options.description,
|
|
61
|
+
access: options.access,
|
|
62
|
+
numerator,
|
|
63
|
+
denominator
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
function base(options) {
|
|
68
|
+
return {
|
|
69
|
+
label: options.label,
|
|
70
|
+
description: options.description,
|
|
71
|
+
access: options.access,
|
|
72
|
+
groupable: options.filterOnly !== true,
|
|
73
|
+
filterable: options.groupOnly !== true
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function withAccess(_options, dimension) {
|
|
77
|
+
return dimension;
|
|
78
|
+
}
|
|
79
|
+
var dimension2 = {
|
|
80
|
+
enum(expr, values, options) {
|
|
81
|
+
for (const value of Object.values(values)) {
|
|
82
|
+
if (!(value in options.labels)) {
|
|
83
|
+
throw new Error(`${options.label}: the value ${value} has no label`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return withAccess(options, {
|
|
87
|
+
...base(options),
|
|
88
|
+
category: "closed",
|
|
89
|
+
kind: "enum",
|
|
90
|
+
expr: expr.node,
|
|
91
|
+
values: options.labels,
|
|
92
|
+
empty: options.empty
|
|
93
|
+
});
|
|
94
|
+
},
|
|
95
|
+
computed(options) {
|
|
96
|
+
return withAccess(options, {
|
|
97
|
+
...base(options),
|
|
98
|
+
category: "closed",
|
|
99
|
+
kind: "computed",
|
|
100
|
+
expr: options.expr.node,
|
|
101
|
+
values: options.values,
|
|
102
|
+
empty: options.empty
|
|
103
|
+
});
|
|
104
|
+
},
|
|
105
|
+
relation(options) {
|
|
106
|
+
return withAccess(options, {
|
|
107
|
+
...base(options),
|
|
108
|
+
category: "open",
|
|
109
|
+
kind: "relation",
|
|
110
|
+
key: options.key.node,
|
|
111
|
+
name: options.name.node,
|
|
112
|
+
empty: options.empty
|
|
113
|
+
});
|
|
114
|
+
},
|
|
115
|
+
manyToMany(options) {
|
|
116
|
+
return withAccess(options, {
|
|
117
|
+
...base(options),
|
|
118
|
+
category: "open",
|
|
119
|
+
kind: "manyToMany",
|
|
120
|
+
table: options.through.table.ref,
|
|
121
|
+
on: options.through.on.node,
|
|
122
|
+
key: options.through.key.node,
|
|
123
|
+
name: options.through.name.node,
|
|
124
|
+
empty: options.empty,
|
|
125
|
+
note: options.note
|
|
126
|
+
});
|
|
127
|
+
},
|
|
128
|
+
number(expr, options) {
|
|
129
|
+
return withAccess(options, {
|
|
130
|
+
...base(options),
|
|
131
|
+
category: "numeric",
|
|
132
|
+
kind: "number",
|
|
133
|
+
expr: expr.node,
|
|
134
|
+
labelFor: options.labelFor
|
|
135
|
+
});
|
|
136
|
+
},
|
|
137
|
+
timeBucket(time, unit, options) {
|
|
138
|
+
return withAccess(options, {
|
|
139
|
+
...base({
|
|
140
|
+
...options,
|
|
141
|
+
label: options?.label ?? unit[0]?.toUpperCase() + unit.slice(1)
|
|
142
|
+
}),
|
|
143
|
+
category: "time",
|
|
144
|
+
kind: "timeBucket",
|
|
145
|
+
time,
|
|
146
|
+
unit,
|
|
147
|
+
labelFor: options?.labelFor
|
|
148
|
+
});
|
|
149
|
+
},
|
|
150
|
+
timePart(time, part, options) {
|
|
151
|
+
return withAccess(options, {
|
|
152
|
+
...base({
|
|
153
|
+
...options,
|
|
154
|
+
label: options?.label ?? (part === "isodow" ? "Weekday" : "Hour")
|
|
155
|
+
}),
|
|
156
|
+
category: "numeric",
|
|
157
|
+
kind: "timePart",
|
|
158
|
+
time,
|
|
159
|
+
part,
|
|
160
|
+
labelFor: options?.labelFor
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
function defineDataset2(definition) {
|
|
165
|
+
const timeEntries = Object.entries(definition.time);
|
|
166
|
+
const firstTime = timeEntries[0];
|
|
167
|
+
if (!firstTime) {
|
|
168
|
+
throw new Error(`Dataset ${definition.key}: declare at least one time field`);
|
|
169
|
+
}
|
|
170
|
+
if (definition.tenantColumn === undefined) {
|
|
171
|
+
throw new Error(`Dataset ${definition.key}: declare tenantColumn, or null for a dataset without tenants`);
|
|
172
|
+
}
|
|
173
|
+
const time = Object.fromEntries(timeEntries.map(([key, field]) => [
|
|
174
|
+
key,
|
|
175
|
+
{ column: field.column.node, label: field.label }
|
|
176
|
+
]));
|
|
177
|
+
const dataset = {
|
|
178
|
+
key: definition.key,
|
|
179
|
+
label: definition.label,
|
|
180
|
+
description: definition.description,
|
|
181
|
+
from: definition.from.ref,
|
|
182
|
+
source: definition.source,
|
|
183
|
+
tenantColumn: definition.tenantColumn?.node ?? null,
|
|
184
|
+
scope: definition.scope?.node ?? null,
|
|
185
|
+
time,
|
|
186
|
+
defaultTime: definition.defaultTime ?? firstTime[0],
|
|
187
|
+
defaultPeriod: definition.defaultPeriod ?? DEFAULT_PERIOD,
|
|
188
|
+
measures: definition.measures,
|
|
189
|
+
dimensions: definition.dimensions
|
|
190
|
+
};
|
|
191
|
+
return validateDataset(dataset);
|
|
192
|
+
}
|
|
193
|
+
function validateDataset(dataset) {
|
|
194
|
+
const fail = (message) => {
|
|
195
|
+
throw new Error(`Dataset ${dataset.key}: ${message}`);
|
|
196
|
+
};
|
|
197
|
+
const check = (node, where, extraRoots = []) => {
|
|
198
|
+
visitScoped(node, [dataset.from, ...extraRoots], (inner, roots) => {
|
|
199
|
+
if (inner.kind === "column") {
|
|
200
|
+
const root = rootOf(inner.table);
|
|
201
|
+
if (!roots.includes(root)) {
|
|
202
|
+
fail(`${where} reads ${inner.table.name}.${inner.column}, which is not joined to ${dataset.from.name}`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
};
|
|
207
|
+
if (dataset.tenantColumn)
|
|
208
|
+
check(dataset.tenantColumn, "tenantColumn");
|
|
209
|
+
if (dataset.scope)
|
|
210
|
+
check(dataset.scope, "scope");
|
|
211
|
+
for (const [key, field] of Object.entries(dataset.time)) {
|
|
212
|
+
check(field.column, `time field ${key}`);
|
|
213
|
+
}
|
|
214
|
+
const measures = dataset.measures;
|
|
215
|
+
for (const [key, entry] of Object.entries(measures)) {
|
|
216
|
+
if (entry.kind === "aggregate")
|
|
217
|
+
check(entry.node, `measure ${key}`);
|
|
218
|
+
if (entry.kind === "share") {
|
|
219
|
+
check(entry.numerator, `measure ${key}`);
|
|
220
|
+
if (entry.denominator)
|
|
221
|
+
check(entry.denominator, `measure ${key}`);
|
|
222
|
+
}
|
|
223
|
+
if (entry.kind === "ratio")
|
|
224
|
+
checkRatio(measures, key, [], fail);
|
|
225
|
+
}
|
|
226
|
+
const dimensions = dataset.dimensions;
|
|
227
|
+
for (const [key, entry] of Object.entries(dimensions)) {
|
|
228
|
+
const where = `dimension ${key}`;
|
|
229
|
+
switch (entry.kind) {
|
|
230
|
+
case "enum":
|
|
231
|
+
case "number":
|
|
232
|
+
check(entry.expr, where);
|
|
233
|
+
break;
|
|
234
|
+
case "computed":
|
|
235
|
+
check(entry.expr, where);
|
|
236
|
+
for (const value of plainCaseValues(entry.expr)) {
|
|
237
|
+
if (!(value in entry.values)) {
|
|
238
|
+
fail(`${where} can produce "${value}", which is missing from its values`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
break;
|
|
242
|
+
case "relation":
|
|
243
|
+
check(entry.key, where);
|
|
244
|
+
check(entry.name, where);
|
|
245
|
+
break;
|
|
246
|
+
case "manyToMany":
|
|
247
|
+
if (entry.table.join) {
|
|
248
|
+
fail(`${where}: through.table must be a standalone table(), not a join`);
|
|
249
|
+
}
|
|
250
|
+
check(entry.on, where, [entry.table]);
|
|
251
|
+
check(entry.key, where, [entry.table]);
|
|
252
|
+
check(entry.name, where, [entry.table]);
|
|
253
|
+
break;
|
|
254
|
+
case "timeBucket":
|
|
255
|
+
case "timePart":
|
|
256
|
+
if (!(entry.time in dataset.time))
|
|
257
|
+
fail(`${where} names unknown time field ${entry.time}`);
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return dataset;
|
|
262
|
+
}
|
|
263
|
+
function checkRatio(measures, key, path, fail) {
|
|
264
|
+
if (path.includes(key))
|
|
265
|
+
fail(`ratio measures form a cycle: ${[...path, key].join(" -> ")}`);
|
|
266
|
+
const entry = measures[key];
|
|
267
|
+
if (!entry)
|
|
268
|
+
fail(`ratio measure ${path.at(-1)} names unknown measure ${key}`);
|
|
269
|
+
if (entry?.kind !== "ratio")
|
|
270
|
+
return;
|
|
271
|
+
checkRatio(measures, entry.numerator, [...path, key], fail);
|
|
272
|
+
checkRatio(measures, entry.denominator, [...path, key], fail);
|
|
273
|
+
}
|
|
274
|
+
function visitScoped(node, roots, visit, seenJoins = new Set) {
|
|
275
|
+
visit(node, roots);
|
|
276
|
+
if (node.kind === "exists") {
|
|
277
|
+
visitScoped(node.where, [...roots, node.table], visit, seenJoins);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
if (node.kind === "column" && node.table.join && !seenJoins.has(node.table)) {
|
|
281
|
+
seenJoins.add(node.table);
|
|
282
|
+
visitScoped(node.table.join.on, roots, visit, seenJoins);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
for (const child of directChildren(node)) {
|
|
286
|
+
visitScoped(child, roots, visit, seenJoins);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
function plainCaseValues(node) {
|
|
290
|
+
if (node.kind === "param" && typeof node.value === "string")
|
|
291
|
+
return [node.value];
|
|
292
|
+
if (node.kind === "case") {
|
|
293
|
+
return [
|
|
294
|
+
...node.branches.flatMap((branch) => plainCaseValues(branch.result)),
|
|
295
|
+
...node.otherwise ? plainCaseValues(node.otherwise) : []
|
|
296
|
+
];
|
|
297
|
+
}
|
|
298
|
+
if (node.kind === "coalesce")
|
|
299
|
+
return node.items.flatMap(plainCaseValues);
|
|
300
|
+
return [];
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export { measure2, dimension2, defineDataset2 };
|