@pythia-software/query-table-core 0.6.0 → 0.7.1

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.js CHANGED
@@ -1,13 +1,438 @@
1
+ // src/cellDisplay.ts
2
+ var calendarOptions = {
3
+ style: ["human", "iso", "custom"],
4
+ pattern: "string",
5
+ timeZone: "string",
6
+ locale: "string"
7
+ };
8
+ var numberOptions = { decimals: [0, 20], locale: "string" };
9
+ var calendarUnits = ["iso", "epochSeconds", "epochMilliseconds"];
10
+ var options = {
11
+ json: { encoding: ["auto", "value", "json", "escaped"], maxDecodeDepth: [1, 8], previewLength: [20, 500], expandDepth: [0, 8] },
12
+ date: { ...calendarOptions, sourceUnit: calendarUnits },
13
+ datetime: { ...calendarOptions, sourceUnit: calendarUnits },
14
+ time: { ...calendarOptions, sourceUnit: [...calendarUnits, "secondsOfDay", "millisecondsOfDay"] },
15
+ duration: { sourceUnit: ["milliseconds", "seconds", "minutes", "hours", "days"], style: ["human", "long", "clock", "custom"], pattern: "string", decimals: [0, 20] },
16
+ number: numberOptions,
17
+ percent: numberOptions,
18
+ chip: { labels: "labels", tones: "tones" },
19
+ text: { truncate: "boolean" }
20
+ };
21
+ function parseCellDisplay(raw, context = "cell display") {
22
+ const fail = (reason) => {
23
+ throw Error(`${context}: ${reason}`);
24
+ };
25
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return fail("expected an object");
26
+ const record2 = raw;
27
+ if (typeof record2.kind !== "string" || !Object.prototype.hasOwnProperty.call(options, record2.kind)) return fail("unsupported kind");
28
+ const allowed = options[record2.kind];
29
+ for (const [key, value] of Object.entries(record2)) {
30
+ if (key === "kind") continue;
31
+ const rule = Object.prototype.hasOwnProperty.call(allowed, key) ? allowed[key] : void 0;
32
+ if (!rule) return fail(`unsupported option ${key}`);
33
+ if (Array.isArray(rule)) {
34
+ if (typeof rule[0] === "number") {
35
+ if (typeof value !== "number" || !Number.isInteger(value) || value < Number(rule[0]) || value > Number(rule[1])) return fail(`${key} must be an integer from ${rule[0]} to ${rule[1]}`);
36
+ } else if (!rule.includes(value)) return fail(`unsupported ${key}`);
37
+ } else if (rule === "labels" || rule === "tones") {
38
+ if (!value || typeof value !== "object" || Array.isArray(value) || !Object.values(value).every((item) => typeof item === "string" && (rule === "labels" || ["neutral", "success", "warning", "danger"].includes(item)))) return fail(`invalid ${key} map`);
39
+ } else if (typeof value !== rule) return fail(`${key} must be ${rule}`);
40
+ if (key === "pattern" && value.length > 160) return fail("pattern must be at most 160 characters");
41
+ }
42
+ return {
43
+ ...record2,
44
+ ...record2.labels ? { labels: { ...record2.labels } } : {},
45
+ ...record2.tones ? { tones: { ...record2.tones } } : {}
46
+ };
47
+ }
48
+
49
+ // src/properties.ts
50
+ var MAX_PROPERTIES = 200;
51
+ var string = (value, max = 256) => typeof value === "string" && value.length > 0 && value.length <= max;
52
+ var record = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
53
+ function normalizePropertyFormat(input) {
54
+ if (!record(input)) return;
55
+ const kind = input.kind;
56
+ if (kind === "json" || kind === "chip") {
57
+ try {
58
+ return parseCellDisplay(input);
59
+ } catch {
60
+ return;
61
+ }
62
+ }
63
+ if (input.decimals !== void 0 && !(typeof input.decimals === "number" && Number.isInteger(input.decimals) && input.decimals >= 0 && input.decimals <= 20))
64
+ return;
65
+ if (["text", "code", "boolean", "chips"].includes(String(kind)))
66
+ return { kind };
67
+ if (kind === "relative") {
68
+ if (input.sourceUnit !== void 0 && !["iso", "epochSeconds", "epochMilliseconds"].includes(
69
+ String(input.sourceUnit)
70
+ ))
71
+ return;
72
+ return {
73
+ kind,
74
+ ...input.sourceUnit ? { sourceUnit: input.sourceUnit } : {}
75
+ };
76
+ }
77
+ if (kind === "currency") {
78
+ if (typeof input.currency !== "string" || !/^[A-Z]{3}$/.test(input.currency))
79
+ return;
80
+ return {
81
+ kind,
82
+ currency: input.currency,
83
+ ...typeof input.decimals === "number" && Number.isInteger(input.decimals) && input.decimals >= 0 && input.decimals <= 20 ? { decimals: input.decimals } : {}
84
+ };
85
+ }
86
+ if (!["number", "percent", "duration", "date", "time", "datetime"].includes(
87
+ String(kind)
88
+ ))
89
+ return;
90
+ if (input.sourceUnit !== void 0 && ![
91
+ "milliseconds",
92
+ "seconds",
93
+ "minutes",
94
+ "hours",
95
+ "days",
96
+ "iso",
97
+ "epochSeconds",
98
+ "epochMilliseconds",
99
+ "secondsOfDay",
100
+ "millisecondsOfDay"
101
+ ].includes(String(input.sourceUnit)))
102
+ return;
103
+ if (input.style !== void 0 && !["human", "long", "clock", "custom", "iso"].includes(String(input.style)))
104
+ return;
105
+ if (input.pattern !== void 0 && !string(input.pattern, 1e3)) return;
106
+ if (input.timeZone !== void 0 && !string(input.timeZone)) return;
107
+ const out = { kind };
108
+ if (typeof input.decimals === "number" && Number.isInteger(input.decimals) && input.decimals >= 0 && input.decimals <= 20)
109
+ out.decimals = input.decimals;
110
+ if ([
111
+ "milliseconds",
112
+ "seconds",
113
+ "minutes",
114
+ "hours",
115
+ "days",
116
+ "iso",
117
+ "epochSeconds",
118
+ "epochMilliseconds",
119
+ "secondsOfDay",
120
+ "millisecondsOfDay"
121
+ ].includes(String(input.sourceUnit)))
122
+ out.sourceUnit = input.sourceUnit;
123
+ if (["human", "long", "clock", "custom", "iso"].includes(String(input.style)))
124
+ out.style = input.style;
125
+ if (string(input.pattern, 1e3)) out.pattern = input.pattern;
126
+ if (string(input.timeZone)) out.timeZone = input.timeZone;
127
+ return out;
128
+ }
129
+ function normalizePropertyView(input) {
130
+ if (!record(input) || input.version !== 1 || !Array.isArray(input.properties))
131
+ throw new Error(
132
+ "Unsupported or invalid property view (expected version 1)."
133
+ );
134
+ if (input.properties.length > MAX_PROPERTIES)
135
+ throw new Error(`At most ${MAX_PROPERTIES} properties are allowed.`);
136
+ const ids = /* @__PURE__ */ new Set();
137
+ const properties = input.properties.map((item) => {
138
+ if (!record(item) || !string(item.id) || !string(item.field) || ids.has(item.id))
139
+ throw new Error("Property fields and unique instance IDs are required.");
140
+ ids.add(item.id);
141
+ const property = { id: item.id, field: item.field };
142
+ if (string(item.label, 1e3)) property.label = item.label;
143
+ if (string(item.render)) property.render = item.render;
144
+ if (item.format !== void 0) {
145
+ const format = normalizePropertyFormat(item.format);
146
+ if (!format) throw new Error(`Invalid format for property ${item.id}.`);
147
+ property.format = format;
148
+ }
149
+ return property;
150
+ });
151
+ const labelWidth = typeof input.labelWidth === "number" && Number.isFinite(input.labelWidth) ? Math.max(15, Math.min(75, Math.round(input.labelWidth))) : void 0;
152
+ return { version: 1, properties, ...labelWidth !== void 0 ? { labelWidth } : {} };
153
+ }
154
+ function createPropertyView(schema, names) {
155
+ const enabled = schema.fields.filter((f2) => f2.property?.enabled !== false);
156
+ const hasPropertyDefaults = enabled.some(
157
+ (f2) => f2.property?.default !== void 0
158
+ );
159
+ const defaults = hasPropertyDefaults ? enabled.filter((f2) => f2.property?.default).map((f2) => f2.name) : schema.defaultSelect?.map((column) => column.field) ?? enabled.filter((f2) => f2.select?.default).map((f2) => f2.name);
160
+ const fields = names ?? (hasPropertyDefaults || defaults.length ? defaults.filter((name) => enabled.some((field) => field.name === name)) : enabled.map((f2) => f2.name));
161
+ return normalizePropertyView({
162
+ version: 1,
163
+ properties: fields.map((field, i) => ({ id: `property-${i}`, field }))
164
+ });
165
+ }
166
+ function objectReferenceKey(reference) {
167
+ return JSON.stringify([
168
+ reference.objectType,
169
+ Object.entries(reference.key).sort(([a], [b]) => a.localeCompare(b))
170
+ ]);
171
+ }
172
+ function readObjectReference(field, fields, row) {
173
+ const relationship = field.relationship;
174
+ if (!relationship) return null;
175
+ const key = /* @__PURE__ */ Object.create(null);
176
+ for (const [target, local] of Object.entries(relationship.key)) {
177
+ const source = fields.find((f2) => f2.name === local);
178
+ if (!source) return null;
179
+ const value = readFieldValue(source, row);
180
+ if (!(typeof value === "string" && value !== "" || typeof value === "number" && Number.isFinite(value)))
181
+ return null;
182
+ key[target] = value;
183
+ }
184
+ return Object.keys(key).length ? { objectType: relationship.objectType, key } : null;
185
+ }
186
+ function discoverFieldRelationships(fields, keys, types) {
187
+ return fields.map((field) => {
188
+ if (field.relationship || !field.origin) return field;
189
+ const candidates = [];
190
+ for (const fk of keys) {
191
+ if (fk.sourceTable !== field.origin.table || !fk.sourceColumns.includes(field.origin.column) || fk.sourceColumns.length === 0 || fk.sourceColumns.length !== fk.targetColumns.length)
192
+ continue;
193
+ const targets = types.filter(
194
+ (type) => type.table === fk.targetTable && type.keyFields.length === fk.targetColumns.length && type.keyFields.every((k) => fk.targetColumns.includes(k))
195
+ );
196
+ if (targets.length !== 1) continue;
197
+ const key = /* @__PURE__ */ Object.create(null);
198
+ let complete = true;
199
+ fk.sourceColumns.forEach((column, i) => {
200
+ const local = fields.filter(
201
+ (f2) => f2.origin?.table === fk.sourceTable && f2.origin.column === column
202
+ );
203
+ if (local.length !== 1) complete = false;
204
+ else key[fk.targetColumns[i]] = local[0].name;
205
+ });
206
+ if (complete) candidates.push({ objectType: targets[0].name, key });
207
+ }
208
+ return candidates.length === 1 ? { ...field, relationship: candidates[0] } : field;
209
+ });
210
+ }
211
+
212
+ // src/schema.ts
213
+ function isFilterable(f2) {
214
+ if (f2.source.kind === "derived" && f2.source.computedId) return false;
215
+ return f2.filter?.enabled ?? f2.source.kind === "backend";
216
+ }
217
+ function isPushdownFilter(f2) {
218
+ return isFilterable(f2) && (f2.filter?.pushdown ?? f2.source.kind === "backend");
219
+ }
220
+ function isSortable(f2) {
221
+ if (f2.source.kind === "derived" && f2.source.computedId) return f2.sort?.enabled === true;
222
+ return f2.sort?.enabled ?? f2.source.kind === "backend";
223
+ }
224
+ function isSelectable(f2) {
225
+ return f2.select?.enabled ?? true;
226
+ }
227
+ function filterValues(f2) {
228
+ return f2.filter?.values ?? { source: "autocomplete" };
229
+ }
230
+ function indexFields(schema) {
231
+ return new Map(schema.fields.map((f2) => [f2.name, f2]));
232
+ }
233
+ function selectedFields(schema, q) {
234
+ const order = q.select.length > 0 ? q.select.map((c) => c.field) : (schema.defaultSelect ?? schema.fields.filter((f2) => f2.select?.default).map((f2) => ({ field: f2.name }))).map(
235
+ (c) => c.field
236
+ );
237
+ const seen = /* @__PURE__ */ new Set();
238
+ const out = [];
239
+ for (const name of order) {
240
+ if (seen.has(name)) continue;
241
+ const f2 = resolveField(schema, name);
242
+ if (!f2 || !isSelectable(f2)) continue;
243
+ seen.add(name);
244
+ seen.add(f2.name);
245
+ out.push(f2);
246
+ }
247
+ return out;
248
+ }
249
+ function resolveField(schema, field) {
250
+ const token = field.trim();
251
+ if (!token) return void 0;
252
+ const byName = indexFields(schema);
253
+ const byExactName = byName.get(token);
254
+ if (byExactName) return byExactName;
255
+ const needle = token.toLowerCase();
256
+ for (const f2 of schema.fields) {
257
+ if (f2.name.toLowerCase() === needle) return f2;
258
+ if (aliasMatches(f2, needle)) return f2;
259
+ }
260
+ return void 0;
261
+ }
262
+ function resolveFieldName(schema, field) {
263
+ return resolveField(schema, field)?.name;
264
+ }
265
+ function aliasTermsFor(field) {
266
+ const out = [];
267
+ const seen = /* @__PURE__ */ new Set();
268
+ const add = (raw) => {
269
+ if (typeof raw !== "string") return;
270
+ const value = raw.trim();
271
+ if (!value) return;
272
+ const key = value.toLowerCase();
273
+ if (seen.has(key)) return;
274
+ seen.add(key);
275
+ out.push(value);
276
+ };
277
+ add(field.alias);
278
+ if (field.aliases) {
279
+ for (const alias of field.aliases) add(alias);
280
+ }
281
+ return out;
282
+ }
283
+ function aliasMatches(field, needle) {
284
+ for (const alias of aliasTermsFor(field)) {
285
+ if (alias.toLowerCase() === needle) return true;
286
+ }
287
+ return false;
288
+ }
289
+ function readFieldValue(field, row) {
290
+ if (row == null) return null;
291
+ if (field.source.kind === "derived" && field.source.accessor) {
292
+ return field.source.accessor(row);
293
+ }
294
+ const path = field.source.kind === "backend" && field.source.path || field.name;
295
+ const r = row;
296
+ if (path in r) return r[path] ?? null;
297
+ let cur = row;
298
+ for (const part of path.split(".")) {
299
+ if (cur == null || typeof cur !== "object") return null;
300
+ cur = cur[part];
301
+ }
302
+ return cur ?? null;
303
+ }
304
+ function loadSchema(doc) {
305
+ const d = doc;
306
+ if (!d || typeof d !== "object") throw new Error("schema: document is not an object");
307
+ if (typeof d.name !== "string") throw new Error("schema: missing `name`");
308
+ if (typeof d.idField !== "string") throw new Error("schema: missing `idField`");
309
+ if (!Array.isArray(d.fields) || d.fields.length === 0) throw new Error("schema: `fields` must be a non-empty array");
310
+ const fields = d.fields.map((raw, i) => projectField(raw, i));
311
+ return {
312
+ name: d.name,
313
+ idField: d.idField,
314
+ fields,
315
+ defaultSort: d.defaultSort,
316
+ defaultSelect: d.defaultSelect,
317
+ defaultLimit: d.defaultLimit
318
+ };
319
+ }
320
+ function projectField(raw, i) {
321
+ if (!raw || typeof raw.name !== "string") throw new Error(`schema: field[${i}] missing \`name\``);
322
+ if (typeof raw.label !== "string") throw new Error(`schema: field ${raw.name} missing \`label\``);
323
+ if (typeof raw.type !== "string") throw new Error(`schema: field ${raw.name} missing \`type\``);
324
+ if (raw.property !== void 0) {
325
+ const property = raw.property;
326
+ if (!property || typeof property !== "object" || Array.isArray(property) || property.enabled !== void 0 && typeof property.enabled !== "boolean" || property.default !== void 0 && typeof property.default !== "boolean" || property.render !== void 0 && (typeof property.render !== "string" || !property.render)) {
327
+ throw new Error(`schema: field ${raw.name} has invalid property metadata`);
328
+ }
329
+ if (property.format !== void 0 && !normalizePropertyFormat(property.format)) {
330
+ throw new Error(`schema: field ${raw.name} has an invalid property format`);
331
+ }
332
+ }
333
+ if (raw.origin !== void 0 && (!raw.origin || typeof raw.origin.table !== "string" || !raw.origin.table || typeof raw.origin.column !== "string" || !raw.origin.column)) {
334
+ throw new Error(`schema: field ${raw.name} has an invalid origin`);
335
+ }
336
+ if (raw.relationship !== void 0 && (!raw.relationship || typeof raw.relationship.objectType !== "string" || !raw.relationship.objectType || !raw.relationship.key || typeof raw.relationship.key !== "object" || Array.isArray(raw.relationship.key) || Object.keys(raw.relationship.key).length === 0 || Object.entries(raw.relationship.key).some(([key, value]) => !key || typeof value !== "string" || !value))) {
337
+ throw new Error(`schema: field ${raw.name} has an invalid relationship`);
338
+ }
339
+ const rs = raw.source ?? (raw.bindings ? "backend" : "derived");
340
+ let source;
341
+ if (typeof rs === "string") {
342
+ source = rs === "derived" ? { kind: "derived" } : { kind: "backend" };
343
+ } else if (rs.kind === "derived") {
344
+ source = { kind: "derived" };
345
+ } else {
346
+ source = { kind: "backend", path: rs.path, synthetic: rs.synthetic };
347
+ }
348
+ return {
349
+ name: raw.name,
350
+ label: raw.label,
351
+ type: raw.type,
352
+ source,
353
+ filter: raw.filter,
354
+ sort: raw.sort,
355
+ select: raw.select,
356
+ aggregate: raw.aggregate,
357
+ group: raw.group,
358
+ alias: raw.alias,
359
+ aliases: normalizeAliases(raw.aliases, raw.alias),
360
+ ...raw.display === void 0 ? {} : { display: parseCellDisplay(raw.display, `schema: field ${raw.name} display`) },
361
+ render: raw.render,
362
+ // string key from JSON; consumer may override with a fn
363
+ property: raw.property ? { ...raw.property, format: raw.property.format === void 0 ? void 0 : normalizePropertyFormat(raw.property.format) } : void 0,
364
+ origin: raw.origin,
365
+ relationship: raw.relationship
366
+ };
367
+ }
368
+ function normalizeAliases(rawAliases, rawAlias) {
369
+ const aliases = [];
370
+ const seen = /* @__PURE__ */ new Set();
371
+ const add = (raw) => {
372
+ if (typeof raw !== "string") return;
373
+ const value = raw.trim();
374
+ if (!value) return;
375
+ const key = value.toLowerCase();
376
+ if (seen.has(key)) return;
377
+ seen.add(key);
378
+ aliases.push(value);
379
+ };
380
+ add(rawAlias);
381
+ if (Array.isArray(rawAliases)) {
382
+ for (const item of rawAliases) add(item);
383
+ }
384
+ return aliases;
385
+ }
386
+
387
+ // src/itemPage.ts
388
+ function itemPageFields(template) {
389
+ return [...new Set(Array.from(template.matchAll(/\{field:([^{}]+)\}/g), (match) => match[1]))];
390
+ }
391
+ function validateItemPageTemplate(template, schema) {
392
+ if (template.length > 8e3) return "Use a URL template of at most 8,000 characters.";
393
+ if (!template.trim()) return null;
394
+ const remainder = template.replace(/\{(id|query|field:[^{}]+)\}/g, "value");
395
+ if (/[{}]/.test(remainder)) return "Use {id}, {query}, or {field:field_name} placeholders.";
396
+ for (const name of itemPageFields(template)) {
397
+ if (!schema.fields.some((field) => field.name === name)) return `Unknown URL field: ${name}.`;
398
+ }
399
+ try {
400
+ if (!["http:", "https:"].includes(new URL(remainder, "https://query-table.invalid/").protocol))
401
+ return "Use an HTTP, HTTPS, or relative page URL.";
402
+ } catch {
403
+ return "Enter a valid page URL.";
404
+ }
405
+ return null;
406
+ }
407
+ function renderItemPageTemplate(template, schema, row, id, query) {
408
+ if (!template.trim() || validateItemPageTemplate(template, schema)) return null;
409
+ let missing = false;
410
+ const href = template.replace(/\{(id|query|field:[^{}]+)\}/g, (_match, token) => {
411
+ const value = token === "id" ? id : token === "query" ? query : readFieldValue(schema.fields.find((field) => field.name === token.slice(6)), row);
412
+ if (value == null || !["string", "number", "boolean"].includes(typeof value)) {
413
+ missing = true;
414
+ return "";
415
+ }
416
+ return encodeURIComponent(String(value));
417
+ });
418
+ try {
419
+ if (missing || !["http:", "https:"].includes(new URL(href, "https://query-table.invalid/").protocol)) return null;
420
+ } catch {
421
+ return null;
422
+ }
423
+ return href;
424
+ }
425
+
1
426
  // src/metricNormalize.ts
2
427
  function normalizeMetricOptions(raw) {
3
428
  const out = {}, diagnostics = [];
4
- const record = (x) => !!x && typeof x === "object" && !Array.isArray(x);
429
+ const record2 = (x) => !!x && typeof x === "object" && !Array.isArray(x);
5
430
  const copy = (input, specs) => {
6
431
  const result = {};
7
432
  for (const [key, spec] of Object.entries(specs)) {
8
433
  const v = input[key];
9
434
  if (v === void 0) continue;
10
- if (spec === "string" ? typeof v === "string" && v.length <= 1e4 : spec === "number" ? typeof v === "number" && Number.isFinite(v) : spec === "boolean" ? typeof v === "boolean" : spec === "integer" ? typeof v === "number" && Number.isSafeInteger(v) && v >= 0 && v <= 1e6 : typeof v === "string" && spec.includes(v))
435
+ 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))
11
436
  result[key] = v;
12
437
  else diagnostics.push(`Invalid metric ${key}.`);
13
438
  }
@@ -22,8 +447,24 @@ function normalizeMetricOptions(raw) {
22
447
  groupLimit: "integer"
23
448
  })
24
449
  );
450
+ if (raw.dateGrouping !== void 0) {
451
+ if (record2(raw.dateGrouping)) {
452
+ const d = copy(raw.dateGrouping, {
453
+ field: "string",
454
+ interval: ["hour", "day", "week", "month", "year"],
455
+ timeZone: "string",
456
+ includeEmpty: "boolean",
457
+ range: ["query", "observed", "custom"],
458
+ start: "string",
459
+ end: "string"
460
+ });
461
+ if (typeof d.field !== "string" || !d.field || d.field.length > 256 || !d.interval || typeof d.includeEmpty !== "boolean" || !d.range)
462
+ diagnostics.push("Invalid metric date grouping.");
463
+ out.dateGrouping = d;
464
+ } else diagnostics.push("Invalid metric date grouping.");
465
+ }
25
466
  if (raw.display !== void 0) {
26
- if (record(raw.display)) {
467
+ if (record2(raw.display)) {
27
468
  if (typeof raw.display.kind !== "string")
28
469
  diagnostics.push("Invalid metric display kind.");
29
470
  const d = copy(raw.display, {
@@ -45,10 +486,12 @@ function normalizeMetricOptions(raw) {
45
486
  xLabel: "string",
46
487
  yLabel: "string",
47
488
  legendPosition: ["left", "right", "top", "bottom", "none"],
489
+ maxGroups: "positiveInteger",
490
+ showOther: "boolean",
48
491
  boxMean: "boolean"
49
492
  });
50
493
  for (const k of ["xScale", "yScale"]) {
51
- if (record(raw.display[k]))
494
+ if (record2(raw.display[k]))
52
495
  d[k] = copy(raw.display[k], {
53
496
  mode: ["linear", "log"],
54
497
  min: "number",
@@ -58,228 +501,88 @@ function normalizeMetricOptions(raw) {
58
501
  diagnostics.push(`Invalid metric ${k}.`);
59
502
  }
60
503
  for (const k of ["format", "xFormat", "yFormat"])
61
- if (record(raw.display[k]))
504
+ if (record2(raw.display[k]))
62
505
  d[k] = copy(raw.display[k], {
63
506
  kind: ["number", "percent", "duration", "date", "time", "datetime"],
64
507
  decimals: "integer",
65
- sourceUnit: [
66
- "milliseconds",
67
- "seconds",
68
- "minutes",
69
- "hours",
70
- "days",
71
- "iso",
72
- "epochSeconds",
73
- "epochMilliseconds",
74
- "secondsOfDay",
75
- "millisecondsOfDay"
76
- ],
77
- style: ["human", "long", "clock", "custom", "iso"],
78
- pattern: "string",
79
- timeZone: "string"
80
- });
81
- if (record(raw.display.list))
82
- d.list = copy(raw.display.list, {
83
- showBars: "boolean",
84
- useGroupColors: "boolean",
85
- showValues: "boolean"
86
- });
87
- if (record(raw.display.pivot))
88
- d.pivot = copy(raw.display.pivot, {
89
- swap: "boolean",
90
- rowDir: ["asc", "desc"],
91
- columnDir: ["asc", "desc"]
92
- });
93
- out.display = { kind: "auto", ...d };
94
- } else diagnostics.push("Invalid metric display.");
95
- }
96
- if (raw.distribution !== void 0) {
97
- if (record(raw.distribution))
98
- out.distribution = copy(raw.distribution, {
99
- kind: ["box", "histogram"],
100
- input: "string",
101
- whiskers: ["minmax", "tukey"],
102
- bins: "integer"
103
- });
104
- else diagnostics.push("Invalid metric distribution.");
105
- }
106
- if (record(raw.layout)) {
107
- const l = copy(raw.layout, {
108
- widthRem: "integer",
109
- heightRem: "integer",
110
- minWidthRem: "integer",
111
- minHeightRem: "integer"
112
- });
113
- for (const k of ["widthRem", "heightRem", "minWidthRem", "minHeightRem"])
114
- l[k] = Math.max(1, Math.min(1e3, Number(l[k] ?? 1)));
115
- l.widthRem = Math.max(Number(l.widthRem), Number(l.minWidthRem));
116
- l.heightRem = Math.max(Number(l.heightRem), Number(l.minHeightRem));
117
- out.layout = l;
118
- }
119
- if (raw.sort !== void 0 && !Array.isArray(raw.sort))
120
- diagnostics.push("Invalid metric sort.");
121
- if (Array.isArray(raw.sort))
122
- out.sort = raw.sort.slice(0, 20).flatMap((s) => {
123
- if (!record(s) || typeof s.key !== "string" || !/^(value|y|count|samples|group(?:0|[1-9][0-9]?))$/.test(s.key) || !["asc", "desc"].includes(String(s.dir))) {
124
- diagnostics.push("Invalid metric sort.");
125
- return [];
126
- }
127
- return [
128
- copy(s, {
129
- key: [s.key],
130
- dir: ["asc", "desc"],
131
- nulls: ["first", "last"]
132
- })
133
- ];
134
- });
135
- if (Array.isArray(raw.diagnostics))
136
- diagnostics.push(
137
- ...raw.diagnostics.filter((s) => typeof s === "string" && s.length <= 1e3).slice(0, 20)
138
- );
139
- if (diagnostics.length) out.diagnostics = [...new Set(diagnostics)];
140
- return out;
141
- }
142
-
143
- // src/computed.ts
144
- var COMPUTED_PREFIX = "@computed/";
145
- var computedFieldName = (id) => COMPUTED_PREFIX + id;
146
- var isComputedField = (name) => name.trim().toLowerCase().startsWith(COMPUTED_PREFIX);
147
- function validateComputedColumn(input) {
148
- const c = input;
149
- if (!c || typeof c.id !== "string" || !/^[a-zA-Z0-9_-]{1,128}$/.test(c.id) || typeof c.label !== "string" || !c.label.trim() || c.label.length > 200 || typeof c.revision !== "string" || !c.revision || c.revision.length > 256 || c.expression?.language !== "qt-expr" || c.expression.version !== 1 || typeof c.expression.source !== "string" || !c.expression.source.trim() || c.expression.source.length > 1e4) {
150
- throw new Error(
151
- "Invalid computed column definition or unsupported formula version."
152
- );
153
- }
154
- return {
155
- id: c.id,
156
- label: c.label,
157
- revision: c.revision,
158
- expression: { ...c.expression }
159
- };
160
- }
161
- function memoryComputedColumnStore(seed = {}) {
162
- const data = new Map(
163
- Object.entries(seed).map(([k, v]) => [
164
- k,
165
- new Map(v.map((c) => [c.id, validateComputedColumn(c)]))
166
- ])
167
- );
168
- const seedRevisions = new Set(
169
- [...data.values()].flatMap(
170
- (entries) => [...entries.values()].map((c) => c.revision)
171
- )
172
- );
173
- let revisionSequence = 0n;
174
- function freshRevision() {
175
- let revision;
176
- do {
177
- revision = `memory:${++revisionSequence}`;
178
- } while (seedRevisions.has(revision));
179
- return revision;
180
- }
181
- const listeners = /* @__PURE__ */ new Map();
182
- return {
183
- async list(dataset) {
184
- return [...data.get(dataset)?.values() ?? []].map(
185
- validateComputedColumn
186
- );
187
- },
188
- async save(dataset, column, expectedRevision) {
189
- const entries = data.get(dataset) ?? /* @__PURE__ */ new Map();
190
- const current = entries.get(column.id);
191
- if ((current?.revision ?? null) !== expectedRevision)
192
- throw new Error(
193
- "This definition changed. Reload it before saving again."
194
- );
195
- const next = validateComputedColumn({
196
- ...column,
197
- revision: freshRevision()
198
- });
199
- entries.set(next.id, next);
200
- data.set(dataset, entries);
201
- listeners.get(dataset)?.forEach((fn) => fn());
202
- return validateComputedColumn(next);
203
- },
204
- subscribe(dataset, listener) {
205
- const set = listeners.get(dataset) ?? /* @__PURE__ */ new Set();
206
- set.add(listener);
207
- listeners.set(dataset, set);
208
- return () => {
209
- set.delete(listener);
210
- };
211
- }
212
- };
213
- }
214
- function httpComputedColumnStore(base, fetcher = fetch) {
215
- const listeners = /* @__PURE__ */ new Map();
216
- const url = (dataset) => `${base}${base.includes("?") ? "&" : "?"}dataset=${encodeURIComponent(dataset)}`;
217
- async function read(response) {
218
- if (response.status === 409)
219
- throw new Error(
220
- "This definition changed. Reload it before saving again."
221
- );
222
- if (!response.ok)
223
- throw new Error(
224
- `Computed column store request failed (${response.status}).`
225
- );
226
- return response.json();
227
- }
228
- return {
229
- async list(dataset, signal) {
230
- const raw = await read(
231
- await fetcher(url(dataset), { ...signal ? { signal } : {} })
232
- );
233
- if (!Array.isArray(raw))
234
- throw new Error("Invalid computed column catalogue.");
235
- return raw.map(validateComputedColumn);
236
- },
237
- async save(dataset, column, expectedRevision) {
238
- const result = validateComputedColumn(
239
- await read(
240
- await fetcher(url(dataset), {
241
- method: "PUT",
242
- headers: { "Content-Type": "application/json" },
243
- body: JSON.stringify({ column, expectedRevision })
244
- })
245
- )
246
- );
247
- listeners.get(dataset)?.forEach((fn) => fn());
248
- return result;
249
- },
250
- subscribe(dataset, listener) {
251
- const set = listeners.get(dataset) ?? /* @__PURE__ */ new Set();
252
- set.add(listener);
253
- listeners.set(dataset, set);
254
- return () => {
255
- set.delete(listener);
256
- };
257
- }
258
- };
259
- }
260
- function isComputedCellError(value) {
261
- return value !== null && typeof value === "object" && "computedError" in value && typeof value.computedError === "string";
262
- }
263
- function groupPreview(dependencies, inputs, results, total) {
264
- const groups = /* @__PURE__ */ new Map();
265
- let nulls = 0, errors = 0;
266
- inputs.forEach((row, i) => {
267
- const values = dependencies.map((name) => row[name] ?? null), result = results[i] ?? { value: null, error: "No result." };
268
- const key = JSON.stringify([values, result.value, result.error ?? null]);
269
- const group = groups.get(key);
270
- if (group) group.count++;
271
- else groups.set(key, { inputs: values, result, count: 1 });
272
- if (result.error) errors++;
273
- else if (result.value === null) nulls++;
274
- });
275
- return {
276
- dependencies,
277
- groups: [...groups.values()].sort((a, b) => b.count - a.count),
278
- processed: inputs.length,
279
- total,
280
- nulls,
281
- errors
282
- };
508
+ sourceUnit: [
509
+ "milliseconds",
510
+ "seconds",
511
+ "minutes",
512
+ "hours",
513
+ "days",
514
+ "iso",
515
+ "epochSeconds",
516
+ "epochMilliseconds",
517
+ "secondsOfDay",
518
+ "millisecondsOfDay"
519
+ ],
520
+ style: ["human", "long", "clock", "custom", "iso"],
521
+ pattern: "string",
522
+ timeZone: "string"
523
+ });
524
+ if (record2(raw.display.list))
525
+ d.list = copy(raw.display.list, {
526
+ showBars: "boolean",
527
+ useGroupColors: "boolean",
528
+ showValues: "boolean",
529
+ barPosition: ["inline", "below"],
530
+ maxLabelLines: "positiveInteger"
531
+ });
532
+ if (record2(raw.display.pivot))
533
+ d.pivot = copy(raw.display.pivot, {
534
+ swap: "boolean",
535
+ rowDir: ["asc", "desc"],
536
+ columnDir: ["asc", "desc"]
537
+ });
538
+ out.display = { kind: "auto", ...d };
539
+ } else diagnostics.push("Invalid metric display.");
540
+ }
541
+ if (raw.distribution !== void 0) {
542
+ if (record2(raw.distribution))
543
+ out.distribution = copy(raw.distribution, {
544
+ kind: ["box", "histogram"],
545
+ input: "string",
546
+ whiskers: ["minmax", "tukey"],
547
+ bins: "integer"
548
+ });
549
+ else diagnostics.push("Invalid metric distribution.");
550
+ }
551
+ if (record2(raw.layout)) {
552
+ const l = copy(raw.layout, {
553
+ widthRem: "integer",
554
+ heightRem: "integer",
555
+ minWidthRem: "integer",
556
+ minHeightRem: "integer"
557
+ });
558
+ for (const k of ["widthRem", "heightRem", "minWidthRem", "minHeightRem"])
559
+ l[k] = Math.max(1, Math.min(1e3, Number(l[k] ?? 1)));
560
+ l.widthRem = Math.max(Number(l.widthRem), Number(l.minWidthRem));
561
+ l.heightRem = Math.max(Number(l.heightRem), Number(l.minHeightRem));
562
+ out.layout = l;
563
+ }
564
+ if (raw.sort !== void 0 && !Array.isArray(raw.sort))
565
+ diagnostics.push("Invalid metric sort.");
566
+ if (Array.isArray(raw.sort))
567
+ out.sort = raw.sort.slice(0, 20).flatMap((s) => {
568
+ if (!record2(s) || typeof s.key !== "string" || !/^(value|y|count|samples|group(?:0|[1-9][0-9]?))$/.test(s.key) || !["asc", "desc"].includes(String(s.dir))) {
569
+ diagnostics.push("Invalid metric sort.");
570
+ return [];
571
+ }
572
+ return [
573
+ copy(s, {
574
+ key: [s.key],
575
+ dir: ["asc", "desc"],
576
+ nulls: ["first", "last"]
577
+ })
578
+ ];
579
+ });
580
+ if (Array.isArray(raw.diagnostics))
581
+ diagnostics.push(
582
+ ...raw.diagnostics.filter((s) => typeof s === "string" && s.length <= 1e3).slice(0, 20)
583
+ );
584
+ if (diagnostics.length) out.diagnostics = [...new Set(diagnostics)];
585
+ return out;
283
586
  }
284
587
 
285
588
  // src/query.ts
@@ -289,6 +592,7 @@ function isOrGroup(term) {
289
592
  function predicatesOf(term) {
290
593
  return isOrGroup(term) ? term.any : [term];
291
594
  }
595
+ var ITEM_DETAILS_COLUMN = "__qt_item_details__";
292
596
  var EMPTY_QUERY = {
293
597
  select: [],
294
598
  where: [],
@@ -343,7 +647,7 @@ function normalizeWhereClause(item) {
343
647
  if (!isRecord(item)) return null;
344
648
  const field = boundedString(item.field, MAX_FIELD_NAME_LENGTH);
345
649
  const value = typeof item.value === "string" && item.value.length <= MAX_FILTER_VALUE_LENGTH ? item.value : null;
346
- if (!field || isComputedField(field) || typeof item.op !== "string" || !FILTER_OPS.has(item.op) || value == null) {
650
+ if (!field || typeof item.op !== "string" || !FILTER_OPS.has(item.op) || value == null) {
347
651
  return null;
348
652
  }
349
653
  const clause = { field, op: item.op, value };
@@ -437,6 +741,8 @@ function normalizeQueryState(input, fallback = EMPTY_QUERY) {
437
741
  limit: boundedInteger(raw.limit, fallback.limit, 1, MAX_QUERY_LIMIT),
438
742
  offset: boundedInteger(raw.offset, fallback.offset, 0, MAX_QUERY_OFFSET)
439
743
  };
744
+ if (raw.fitToScreen === true) out.fitToScreen = true;
745
+ if (raw.grouping != null) out.grouping = normalizeGroupingState(raw.grouping);
440
746
  if (Array.isArray(raw.aggregations)) {
441
747
  const aggregations = [];
442
748
  for (const item of raw.aggregations.slice(0, MAX_AGGREGATIONS)) {
@@ -463,179 +769,113 @@ function normalizeQueryState(input, fallback = EMPTY_QUERY) {
463
769
  } else if (fallback.aggregations?.length) {
464
770
  out.aggregations = normalizeQueryState({ aggregations: fallback.aggregations }).aggregations;
465
771
  }
466
- return out;
467
- }
468
- function queriesEqual(a, b) {
469
- if (a === b) return true;
470
- if (a.limit !== b.limit || a.offset !== b.offset) return false;
471
- if (!sameArray(a.where, b.where)) return false;
472
- if (!sameArray(a.orderBy, b.orderBy)) return false;
473
- if (!sameArray(a.select, b.select)) return false;
474
- if (!sameArray(a.aggregations ?? [], b.aggregations ?? [])) return false;
475
- return true;
476
- }
477
- function sameArray(a, b) {
478
- if (a.length !== b.length) return false;
479
- for (let i = 0; i < a.length; i++) {
480
- if (JSON.stringify(a[i]) !== JSON.stringify(b[i])) return false;
481
- }
482
- return true;
483
- }
484
-
485
- // src/schema.ts
486
- function isFilterable(f2) {
487
- if (f2.source.kind === "derived" && f2.source.computedId) return false;
488
- return f2.filter?.enabled ?? f2.source.kind === "backend";
489
- }
490
- function isPushdownFilter(f2) {
491
- return isFilterable(f2) && (f2.filter?.pushdown ?? f2.source.kind === "backend");
492
- }
493
- function isSortable(f2) {
494
- if (f2.source.kind === "derived" && f2.source.computedId) return f2.sort?.enabled === true;
495
- return f2.sort?.enabled ?? f2.source.kind === "backend";
496
- }
497
- function isSelectable(f2) {
498
- return f2.select?.enabled ?? true;
499
- }
500
- function filterValues(f2) {
501
- return f2.filter?.values ?? { source: "autocomplete" };
502
- }
503
- function indexFields(schema) {
504
- return new Map(schema.fields.map((f2) => [f2.name, f2]));
505
- }
506
- function selectedFields(schema, q) {
507
- const order = q.select.length > 0 ? q.select.map((c) => c.field) : (schema.defaultSelect ?? schema.fields.filter((f2) => f2.select?.default).map((f2) => ({ field: f2.name }))).map(
508
- (c) => c.field
509
- );
510
- const seen = /* @__PURE__ */ new Set();
511
- const out = [];
512
- for (const name of order) {
513
- if (seen.has(name)) continue;
514
- const f2 = resolveField(schema, name);
515
- if (!f2 || !isSelectable(f2)) continue;
516
- seen.add(name);
517
- seen.add(f2.name);
518
- out.push(f2);
772
+ if (isRecord(raw.itemDetails) && Array.isArray(raw.itemDetails.modes)) {
773
+ const modes = [...new Set(raw.itemDetails.modes.filter(
774
+ (mode) => typeof mode === "string" && ["hover", "modal", "expand", "page"].includes(mode)
775
+ ))];
776
+ const details = { modes };
777
+ if (isRecord(raw.itemDetails.page) && typeof raw.itemDetails.page.urlTemplate === "string" && raw.itemDetails.page.urlTemplate.length <= 8e3) {
778
+ details.page = {
779
+ urlTemplate: raw.itemDetails.page.urlTemplate,
780
+ target: raw.itemDetails.page.target === "_self" ? "_self" : "_blank"
781
+ };
782
+ }
783
+ if (raw.itemDetails.view !== void 0) {
784
+ try {
785
+ details.view = normalizePropertyView(raw.itemDetails.view);
786
+ } catch {
787
+ }
788
+ }
789
+ if (isRecord(raw.itemDetails.views)) {
790
+ const views = {};
791
+ for (const mode of ["hover", "modal", "expand"]) {
792
+ try {
793
+ if (raw.itemDetails.views[mode] !== void 0) views[mode] = normalizePropertyView(raw.itemDetails.views[mode]);
794
+ } catch {
795
+ }
796
+ }
797
+ if (Object.keys(views).length) details.views = views;
798
+ }
799
+ const active = raw.itemDetails.active;
800
+ 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))) {
801
+ details.active = { id: active.id, mode: active.mode };
802
+ }
803
+ out.itemDetails = details;
519
804
  }
520
805
  return out;
521
806
  }
522
- function resolveField(schema, field) {
523
- const token = field.trim();
524
- if (!token) return void 0;
525
- const byName = indexFields(schema);
526
- const byExactName = byName.get(token);
527
- if (byExactName) return byExactName;
528
- const needle = token.toLowerCase();
529
- for (const f2 of schema.fields) {
530
- if (f2.name.toLowerCase() === needle) return f2;
531
- if (aliasMatches(f2, needle)) return f2;
532
- }
533
- return void 0;
534
- }
535
- function resolveFieldName(schema, field) {
536
- return resolveField(schema, field)?.name;
537
- }
538
- function aliasTermsFor(field) {
539
- const out = [];
540
- const seen = /* @__PURE__ */ new Set();
541
- const add = (raw) => {
542
- if (typeof raw !== "string") return;
543
- const value = raw.trim();
544
- if (!value) return;
545
- const key = value.toLowerCase();
546
- if (seen.has(key)) return;
547
- seen.add(key);
548
- out.push(value);
807
+ function normalizeGroupingState(value) {
808
+ const raw = isRecord(value) ? value : {};
809
+ const diagnostics = [];
810
+ const fail = () => {
811
+ diagnostics.push("Invalid grouping definition.");
549
812
  };
550
- add(field.alias);
551
- if (field.aliases) {
552
- for (const alias of field.aliases) add(alias);
553
- }
554
- return out;
555
- }
556
- function aliasMatches(field, needle) {
557
- for (const alias of aliasTermsFor(field)) {
558
- if (alias.toLowerCase() === needle) return true;
559
- }
560
- return false;
561
- }
562
- function readFieldValue(field, row) {
563
- if (row == null) return null;
564
- if (field.source.kind === "derived" && field.source.accessor) {
565
- return field.source.accessor(row);
813
+ const keys = Array.isArray(raw.keys) ? raw.keys.slice(0, MAX_GROUP_BY_FIELDS).filter((key) => !!boundedString(key, MAX_FIELD_NAME_LENGTH)) : [];
814
+ if (!Array.isArray(raw.keys) || !keys.length || keys.length !== raw.keys.length || new Set(keys).size !== keys.length) fail();
815
+ const summaries = [];
816
+ if (!Array.isArray(raw.summaries) || raw.summaries.length > 64) fail();
817
+ for (const item of Array.isArray(raw.summaries) ? raw.summaries.slice(0, 64) : []) {
818
+ if (!isRecord(item) || !boundedString(item.id, MAX_FIELD_NAME_LENGTH) || typeof item.op !== "string" || !AGG_OPS.has(item.op) && item.op !== "row" && item.op !== "formula") {
819
+ fail();
820
+ continue;
821
+ }
822
+ const column = { id: item.id, op: item.op };
823
+ if (item.field !== void 0) {
824
+ if (!boundedString(item.field, MAX_FIELD_NAME_LENGTH)) fail();
825
+ else column.field = item.field;
826
+ }
827
+ if (item.op === "formula") {
828
+ if (typeof item.expression !== "string" || item.expression.length > 1e4) fail();
829
+ else column.expression = item.expression;
830
+ }
831
+ if (item.label !== void 0) {
832
+ if (typeof item.label !== "string" || item.label.length > MAX_LABEL_LENGTH) fail();
833
+ else column.label = item.label;
834
+ }
835
+ summaries.push(column);
566
836
  }
567
- const path = field.source.kind === "backend" && field.source.path || field.name;
568
- const r = row;
569
- if (path in r) return r[path] ?? null;
570
- let cur = row;
571
- for (const part of path.split(".")) {
572
- if (cur == null || typeof cur !== "object") return null;
573
- cur = cur[part];
837
+ let representative = null;
838
+ if (raw.representative != null) {
839
+ const ordering = isRecord(raw.representative) ? raw.representative.orderBy : void 0;
840
+ const orderBy = [];
841
+ if (!Array.isArray(ordering) || !ordering.length || ordering.length > 8) fail();
842
+ for (const term of Array.isArray(ordering) ? ordering.slice(0, 8) : []) {
843
+ if (!isRecord(term) || !boundedString(term.field, MAX_FIELD_NAME_LENGTH) || !["asc", "desc"].includes(String(term.dir)) || term.nulls !== void 0 && !["first", "last"].includes(String(term.nulls))) {
844
+ fail();
845
+ continue;
846
+ }
847
+ orderBy.push({ field: term.field, dir: term.dir, nulls: term.nulls === "first" ? "first" : "last" });
848
+ }
849
+ representative = { orderBy };
574
850
  }
575
- return cur ?? null;
576
- }
577
- function loadSchema(doc) {
578
- const d = doc;
579
- if (!d || typeof d !== "object") throw new Error("schema: document is not an object");
580
- if (typeof d.name !== "string") throw new Error("schema: missing `name`");
581
- if (typeof d.idField !== "string") throw new Error("schema: missing `idField`");
582
- if (!Array.isArray(d.fields) || d.fields.length === 0) throw new Error("schema: `fields` must be a non-empty array");
583
- const fields = d.fields.map((raw, i) => projectField(raw, i));
584
- return {
585
- name: d.name,
586
- idField: d.idField,
587
- fields,
588
- defaultSort: d.defaultSort,
589
- defaultSelect: d.defaultSelect,
590
- defaultLimit: d.defaultLimit
591
- };
592
- }
593
- function projectField(raw, i) {
594
- if (!raw || typeof raw.name !== "string") throw new Error(`schema: field[${i}] missing \`name\``);
595
- if (typeof raw.label !== "string") throw new Error(`schema: field ${raw.name} missing \`label\``);
596
- if (typeof raw.type !== "string") throw new Error(`schema: field ${raw.name} missing \`type\``);
597
- const rs = raw.source ?? (raw.bindings ? "backend" : "derived");
598
- let source;
599
- if (typeof rs === "string") {
600
- source = rs === "derived" ? { kind: "derived" } : { kind: "backend" };
601
- } else if (rs.kind === "derived") {
602
- source = { kind: "derived" };
603
- } else {
604
- source = { kind: "backend", path: rs.path, synthetic: rs.synthetic };
851
+ if (!isRecord(raw.source) || raw.source.grouping != null) fail();
852
+ const { grouping: _nested, ...source } = isRecord(raw.source) ? raw.source : {};
853
+ if (Array.isArray(raw.diagnostics) && raw.diagnostics.length) fail();
854
+ const normalizedSource = normalizeQueryState(source);
855
+ if (Array.isArray(source.where)) {
856
+ const supplied = source.where.reduce((count, term) => count + (isRecord(term) && Array.isArray(term.any) ? term.any.length : 1), 0);
857
+ if (supplied !== normalizedSource.where.reduce((count, term) => count + predicatesOf(term).length, 0)) fail();
605
858
  }
606
- return {
607
- name: raw.name,
608
- label: raw.label,
609
- type: raw.type,
610
- source,
611
- filter: raw.filter,
612
- sort: raw.sort,
613
- select: raw.select,
614
- aggregate: raw.aggregate,
615
- group: raw.group,
616
- alias: raw.alias,
617
- aliases: normalizeAliases(raw.aliases, raw.alias),
618
- render: raw.render
619
- // string key from JSON; consumer may override with a fn
620
- };
859
+ return { keys, summaries, representative, source: normalizedSource, ...diagnostics.length ? { diagnostics: [...new Set(diagnostics)] } : {} };
621
860
  }
622
- function normalizeAliases(rawAliases, rawAlias) {
623
- const aliases = [];
624
- const seen = /* @__PURE__ */ new Set();
625
- const add = (raw) => {
626
- if (typeof raw !== "string") return;
627
- const value = raw.trim();
628
- if (!value) return;
629
- const key = value.toLowerCase();
630
- if (seen.has(key)) return;
631
- seen.add(key);
632
- aliases.push(value);
633
- };
634
- add(rawAlias);
635
- if (Array.isArray(rawAliases)) {
636
- for (const item of rawAliases) add(item);
861
+ function queriesEqual(a, b) {
862
+ if (a === b) return true;
863
+ if (a.limit !== b.limit || a.offset !== b.offset) return false;
864
+ if (a.fitToScreen === true !== (b.fitToScreen === true)) return false;
865
+ if (!sameArray(a.where, b.where)) return false;
866
+ if (!sameArray(a.orderBy, b.orderBy)) return false;
867
+ if (!sameArray(a.select, b.select)) return false;
868
+ if (!sameArray(a.aggregations ?? [], b.aggregations ?? [])) return false;
869
+ if (JSON.stringify(a.itemDetails) !== JSON.stringify(b.itemDetails)) return false;
870
+ if (JSON.stringify(a.grouping) !== JSON.stringify(b.grouping)) return false;
871
+ return true;
872
+ }
873
+ function sameArray(a, b) {
874
+ if (a.length !== b.length) return false;
875
+ for (let i = 0; i < a.length; i++) {
876
+ if (JSON.stringify(a[i]) !== JSON.stringify(b[i])) return false;
637
877
  }
638
- return aliases;
878
+ return true;
639
879
  }
640
880
 
641
881
  // src/relativeTime.ts
@@ -655,8 +895,8 @@ function parseRelativeDuration(value) {
655
895
  }
656
896
  return value[0] === "-" && total !== 0 ? -total : total;
657
897
  }
658
- function evaluationTime(options) {
659
- const now = options.now ?? Date.now();
898
+ function evaluationTime(options2) {
899
+ const now = options2.now ?? Date.now();
660
900
  if (!Number.isSafeInteger(now) || Math.abs(now) > MAX_RELATIVE_TIME_MS) throw new Error("Invalid query evaluation time");
661
901
  return now;
662
902
  }
@@ -807,6 +1047,9 @@ function isGroupable(field) {
807
1047
  return field.type === "enum" || field.type === "text" || field.type === "bool";
808
1048
  }
809
1049
 
1050
+ // src/encode.ts
1051
+ import { deflateSync, Inflate } from "fflate";
1052
+
810
1053
  // src/setFilter.ts
811
1054
  function createSetFilter(field, mode, values, id) {
812
1055
  const keys = [...new Set(values)];
@@ -872,7 +1115,7 @@ function fromBase64Url(s) {
872
1115
  const bytes = Uint8Array.from(bin, (c) => c.charCodeAt(0));
873
1116
  return new TextDecoder().decode(bytes);
874
1117
  }
875
- function encodeQuery(q) {
1118
+ function compactQuery(q) {
876
1119
  q = normalizeQueryState(q);
877
1120
  const c = {};
878
1121
  if (q.select.length)
@@ -882,25 +1125,157 @@ function encodeQuery(q) {
882
1125
  c.l = q.limit;
883
1126
  if (q.offset) c.f = q.offset;
884
1127
  if (q.aggregations?.length) c.g = q.aggregations;
885
- if (Object.keys(c).length === 0) return "";
886
- return toBase64Url(JSON.stringify(c));
1128
+ if (q.itemDetails) c.d = q.itemDetails;
1129
+ if (q.fitToScreen) c.fs = true;
1130
+ if (q.grouping) c.b = q.grouping;
1131
+ return c;
1132
+ }
1133
+ var MAX_QUERY_STATE_BYTES = 4 * 1024 * 1024;
1134
+ var QueryDecodeError = class extends Error {
1135
+ constructor(code, message) {
1136
+ super(message);
1137
+ this.code = code;
1138
+ this.name = "QueryDecodeError";
1139
+ }
1140
+ };
1141
+ function packQuery(c) {
1142
+ const packed = { ...c };
1143
+ if (c.l === EMPTY_QUERY.limit) delete packed.l;
1144
+ if (c.w) {
1145
+ const terms = [];
1146
+ for (let i = 0; i < c.w.length; i++) {
1147
+ const set = readSetFilter(c.w, i);
1148
+ if (set) {
1149
+ terms.push(["set", set.field, set.metadata.mode, set.metadata.values, set.metadata.id]);
1150
+ i += set.count - 1;
1151
+ } else terms.push(c.w[i]);
1152
+ }
1153
+ packed.w = terms;
1154
+ }
1155
+ if (c.d?.views) {
1156
+ const pool = [];
1157
+ const refs = {};
1158
+ for (const [mode, view] of Object.entries(c.d.views)) {
1159
+ let index = pool.findIndex((v) => JSON.stringify(v) === JSON.stringify(view));
1160
+ if (index < 0) {
1161
+ index = pool.length;
1162
+ pool.push(view);
1163
+ }
1164
+ refs[mode] = index;
1165
+ }
1166
+ packed.d = { ...c.d, views: { pool, refs } };
1167
+ }
1168
+ return packed;
1169
+ }
1170
+ function unpackQuery(c) {
1171
+ if (Array.isArray(c.w)) c.w = c.w.flatMap((term) => {
1172
+ if (!Array.isArray(term)) return [term];
1173
+ if (term.length !== 5 || term[0] !== "set" || typeof term[1] !== "string" || typeof term[2] !== "string" || !["any", "all", "none", "empty"].includes(term[2]) || !Array.isArray(term[3]) || term[3].length > 100 || !term[3].every((v) => typeof v === "string" && v.length <= 1e4) || typeof term[4] !== "string")
1174
+ throw new QueryDecodeError("invalid", "Invalid set filter in query link.");
1175
+ return createSetFilter(term[1], term[2], term[3], term[4]);
1176
+ });
1177
+ const details = c.d;
1178
+ const views = details && typeof details === "object" && "views" in details ? details.views : void 0;
1179
+ if (details && typeof details === "object" && views && typeof views === "object" && "pool" in views) {
1180
+ const pool = views.pool;
1181
+ const refs = "refs" in views ? views.refs : void 0;
1182
+ if (!Array.isArray(pool) || pool.length > 3 || !refs || typeof refs !== "object" || Array.isArray(refs))
1183
+ throw new QueryDecodeError("invalid", "Invalid property views in query link.");
1184
+ c.d = { ...details, views: Object.fromEntries(Object.entries(refs).map(([mode, index]) => {
1185
+ if (!["hover", "modal", "expand"].includes(mode) || !Number.isInteger(index) || index < 0 || index >= pool.length)
1186
+ throw new QueryDecodeError("invalid", "Invalid property view reference.");
1187
+ return [mode, pool[index]];
1188
+ })) };
1189
+ }
1190
+ return c;
1191
+ }
1192
+ function encodeQuery(q) {
1193
+ const json = JSON.stringify(packQuery(compactQuery(q)));
1194
+ const bytes = new TextEncoder().encode(json);
1195
+ if (bytes.length > MAX_QUERY_STATE_BYTES) throw new QueryDecodeError("too_large", "Query state exceeds the storage limit.");
1196
+ const plain = "2j." + toBase64Url(json);
1197
+ const compressed = "2d." + bytesToBase64(deflateSync(bytes));
1198
+ const token = compressed.length < plain.length ? compressed : plain;
1199
+ if (token.length > MAX_QUERY_TOKEN_LENGTH) throw new QueryDecodeError("too_large", "Encoded query state exceeds the storage limit.");
1200
+ return token;
1201
+ }
1202
+ function bytesToBase64(bytes) {
1203
+ if (typeof Buffer !== "undefined") return Buffer.from(bytes).toString("base64").replace(/=+$/, "").replace(/\+/g, "-").replace(/\//g, "_");
1204
+ let bin = "";
1205
+ for (const byte of bytes) bin += String.fromCharCode(byte);
1206
+ return btoa(bin).replace(/=+$/, "").replace(/\+/g, "-").replace(/\//g, "_");
1207
+ }
1208
+ function inflateToken(token) {
1209
+ const bin = atob(token.replace(/-/g, "+").replace(/_/g, "/"));
1210
+ const input = Uint8Array.from(bin, (c) => c.charCodeAt(0));
1211
+ const chunks = [];
1212
+ let size = 0;
1213
+ let complete = false;
1214
+ const stream = new Inflate((chunk, final) => {
1215
+ size += chunk.length;
1216
+ if (size > MAX_QUERY_STATE_BYTES) throw new QueryDecodeError("too_large", "Expanded query state exceeds the storage limit.");
1217
+ chunks.push(chunk);
1218
+ complete = final;
1219
+ });
1220
+ for (let i = 0; i < input.length; i += 256) stream.push(input.subarray(i, i + 256), i + 256 >= input.length);
1221
+ if (!complete) throw new QueryDecodeError("invalid", "Incomplete compressed query link.");
1222
+ const bytes = new Uint8Array(size);
1223
+ let offset = 0;
1224
+ for (const chunk of chunks) {
1225
+ bytes.set(chunk, offset);
1226
+ offset += chunk.length;
1227
+ }
1228
+ return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
887
1229
  }
888
1230
  function decodeQuery(token) {
1231
+ try {
1232
+ return decodeToken(token, false);
1233
+ } catch {
1234
+ return { ...EMPTY_QUERY };
1235
+ }
1236
+ }
1237
+ function decodeQueryStrict(token) {
1238
+ return decodeToken(token, true);
1239
+ }
1240
+ function decodeToken(token, strict) {
889
1241
  if (!token) return { ...EMPTY_QUERY };
890
- if (token.length > MAX_QUERY_TOKEN_LENGTH) return { ...EMPTY_QUERY };
1242
+ if (token.length > MAX_QUERY_TOKEN_LENGTH) throw new QueryDecodeError("too_large", "Query link exceeds the storage limit.");
891
1243
  try {
892
- const c = JSON.parse(fromBase64Url(token));
1244
+ const versioned = token.includes(".");
1245
+ if (versioned && !token.startsWith("2j.") && !token.startsWith("2d."))
1246
+ throw new QueryDecodeError("unsupported_version", "Unsupported query link version.");
1247
+ const payload = versioned ? token.slice(3) : token;
1248
+ if (!/^[A-Za-z0-9_-]+$/.test(payload) || payload.length % 4 === 1) throw new QueryDecodeError("invalid", "Invalid query link encoding.");
1249
+ const json = token.startsWith("2d.") ? inflateToken(payload) : fromBase64Url(payload);
1250
+ if (new TextEncoder().encode(json).length > MAX_QUERY_STATE_BYTES) throw new QueryDecodeError("too_large", "Query state exceeds the storage limit.");
1251
+ const raw = JSON.parse(json);
1252
+ if (!raw || typeof raw !== "object" || Array.isArray(raw) || ["w", "g"].some((k) => raw[k] !== void 0 && !Array.isArray(raw[k])))
1253
+ throw new QueryDecodeError("invalid", "Invalid query link state.");
1254
+ const c = versioned ? unpackQuery(raw) : raw;
1255
+ if (strict && c.w) {
1256
+ if (c.w.some((term) => !term || typeof term !== "object")) throw new QueryDecodeError("invalid", "Invalid query predicates.");
1257
+ const count = c.w.reduce((n, term) => n + predicatesOf(term).length, 0);
1258
+ const normalized = normalizeQueryState({ where: c.w });
1259
+ const normalizedCount = normalized.where.reduce((n, term) => n + predicatesOf(term).length, 0);
1260
+ 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")))
1261
+ throw new QueryDecodeError("invalid", "Invalid query predicates or predicate budget exceeded.");
1262
+ }
893
1263
  const out = normalizeQueryState({
894
1264
  select: normalizeSelect(c.s ?? c.c),
895
1265
  where: Array.isArray(c.w) ? c.w : [],
896
1266
  orderBy: normalizeOrderBy(c.o),
897
1267
  limit: c.l,
898
1268
  offset: c.f,
899
- aggregations: c.g
1269
+ aggregations: c.g,
1270
+ itemDetails: c.d,
1271
+ fitToScreen: c.fs,
1272
+ grouping: c.b
900
1273
  });
1274
+ if (strict && c.g && out.aggregations?.length !== c.g.length) throw new QueryDecodeError("invalid", "Invalid query metrics.");
901
1275
  return out;
902
- } catch {
903
- return { ...EMPTY_QUERY };
1276
+ } catch (error) {
1277
+ if (error instanceof QueryDecodeError) throw error;
1278
+ throw new QueryDecodeError("invalid", "Invalid or incomplete query link.");
904
1279
  }
905
1280
  }
906
1281
  function normalizeSelect(s) {
@@ -919,6 +1294,7 @@ function normalizeOrderBy(o) {
919
1294
  return [o];
920
1295
  }
921
1296
  function toServerQuery(q, schema) {
1297
+ if (q.grouping) throw new Error("This transport does not support grouped queries. Enable the v3 query executor.");
922
1298
  q = normalizeQueryState(q);
923
1299
  const byName = indexFields(schema);
924
1300
  const resolveField2 = (name) => resolveFieldName(schema, name) ?? name;
@@ -957,8 +1333,10 @@ function termIsPushdown(term, byName, resolveField2) {
957
1333
  });
958
1334
  }
959
1335
  function toAggregationQuery(q, schema) {
1336
+ if (q.grouping) throw new Error("Metrics over grouped rows require grouped-query execution.");
960
1337
  const suppliedPredicateCount = q.where.reduce((n, t) => n + predicatesOf(t).length, 0);
961
1338
  q = normalizeQueryState(q);
1339
+ if (q.aggregations?.some((a) => a.dateGrouping)) throw new Error("Date grouping currently requires local metric execution.");
962
1340
  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)) throw new Error("Metric definition requires the version 2 metric transport.");
963
1341
  const byName = indexFields(schema);
964
1342
  const resolveField2 = (name) => resolveFieldName(schema, name) ?? name;
@@ -969,13 +1347,453 @@ function toAggregationQuery(q, schema) {
969
1347
  const f2 = byName.get(field);
970
1348
  return f2 != null && f2.source.kind === "backend";
971
1349
  };
972
- const aggregations = (q.aggregations ?? []).filter(
973
- (a) => isBackend(a.field) && a.groupBy.every((g) => isBackend(g))
974
- );
975
- const diagnostics = [];
976
- 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 });
977
- if (where.length !== q.where.length || suppliedPredicateCount !== 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 });
978
- return { where, aggregations, ...diagnostics.length ? { diagnostics } : {} };
1350
+ const aggregations = (q.aggregations ?? []).filter(
1351
+ (a) => isBackend(a.field) && a.groupBy.every((g) => isBackend(g))
1352
+ );
1353
+ const diagnostics = [];
1354
+ 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 });
1355
+ if (where.length !== q.where.length || suppliedPredicateCount !== 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 });
1356
+ return { where, aggregations, ...diagnostics.length ? { diagnostics } : {} };
1357
+ }
1358
+
1359
+ // src/queryLinks.ts
1360
+ var codePattern = /^[A-Za-z0-9_-]{22}$/;
1361
+ function validateQuerySnapshot(input) {
1362
+ const s = input;
1363
+ if (!s || s.version !== 1 || !codePattern.test(s.code) || typeof s.token !== "string" || s.token.length > MAX_QUERY_TOKEN_LENGTH || !Number.isFinite(s.createdAt)) throw Error("Invalid query snapshot.");
1364
+ decodeQueryStrict(s.token);
1365
+ return { version: 1, code: s.code, token: s.token, createdAt: s.createdAt };
1366
+ }
1367
+ function newSnapshot(token) {
1368
+ const bytes = crypto.getRandomValues(new Uint8Array(16));
1369
+ let bin = "";
1370
+ for (const b of bytes) bin += String.fromCharCode(b);
1371
+ return { version: 1, code: btoa(bin).replace(/=+$/, "").replace(/\+/g, "-").replace(/\//g, "_"), token, createdAt: Date.now() };
1372
+ }
1373
+ function localQuerySnapshotCache(options2 = {}) {
1374
+ const storageKey = options2.storageKey ?? "query-table:link-cache:v1";
1375
+ const maxEntries = options2.maxEntries ?? 100;
1376
+ const maxBytes = options2.maxBytes ?? 2 * 1024 * 1024;
1377
+ if (!Number.isInteger(maxEntries) || maxEntries < 1 || !Number.isFinite(maxBytes) || maxBytes < 1) throw Error("Invalid query link cache limits.");
1378
+ const storage = () => options2.storage ?? globalThis.localStorage;
1379
+ function read() {
1380
+ const text = storage().getItem(storageKey);
1381
+ if (!text) return [];
1382
+ let raw;
1383
+ try {
1384
+ raw = JSON.parse(text);
1385
+ } catch {
1386
+ return [];
1387
+ }
1388
+ if (!Array.isArray(raw)) return [];
1389
+ const entries = raw.filter((e) => e && typeof e.key === "string" && e.snapshot && e.snapshot.version === 1 && codePattern.test(e.snapshot.code) && typeof e.snapshot.token === "string" && e.snapshot.token.length <= MAX_QUERY_TOKEN_LENGTH && Number.isFinite(e.snapshot.createdAt)).slice(-maxEntries);
1390
+ while (entries.length && JSON.stringify(entries).length * 2 > maxBytes) entries.shift();
1391
+ return entries;
1392
+ }
1393
+ return {
1394
+ isDurable(key, code) {
1395
+ return read().some((e) => e.key === key && e.snapshot.code === code && e.durable);
1396
+ },
1397
+ markDurable(key, code) {
1398
+ const entries = read();
1399
+ const entry = entries.find((e) => e.key === key && e.snapshot.code === code);
1400
+ if (entry) {
1401
+ entry.durable = true;
1402
+ let text = JSON.stringify(entries);
1403
+ while (text.length * 2 > maxBytes && entries.length > 1) {
1404
+ entries.splice(entries[0] === entry ? 1 : 0, 1);
1405
+ text = JSON.stringify(entries);
1406
+ }
1407
+ if (text.length * 2 <= maxBytes) storage().setItem(storageKey, text);
1408
+ }
1409
+ },
1410
+ get(key, code) {
1411
+ return read().find((e) => e.key === key && e.snapshot.code === code)?.snapshot ?? null;
1412
+ },
1413
+ find(key, token) {
1414
+ return read().find((e) => e.key === key && e.snapshot.token === token)?.snapshot ?? null;
1415
+ },
1416
+ put(key, snapshot) {
1417
+ const entries = read();
1418
+ const old = entries.find((e) => e.key === key && e.snapshot.code === snapshot.code);
1419
+ if (old) {
1420
+ if (old.snapshot.token !== snapshot.token) throw Error("A query link code cannot be overwritten.");
1421
+ const text2 = JSON.stringify(entries);
1422
+ if (storage().getItem(storageKey) !== text2) storage().setItem(storageKey, text2);
1423
+ return;
1424
+ }
1425
+ entries.push({ key, snapshot: validateQuerySnapshot(snapshot) });
1426
+ while (entries.length > maxEntries) entries.shift();
1427
+ let text = JSON.stringify(entries);
1428
+ while (text.length * 2 > maxBytes && entries.length > 1) {
1429
+ entries.shift();
1430
+ text = JSON.stringify(entries);
1431
+ }
1432
+ if (text.length * 2 > maxBytes) throw Error("This query is too large for the local link cache.");
1433
+ storage().setItem(storageKey, text);
1434
+ }
1435
+ };
1436
+ }
1437
+ function memoryQuerySnapshotCache() {
1438
+ let text = null;
1439
+ return localQuerySnapshotCache({ storage: { getItem: () => text, setItem: (_key, value) => {
1440
+ text = value;
1441
+ } } });
1442
+ }
1443
+ var QuerySnapshotHttpError = class extends Error {
1444
+ constructor(status, code, message, detail, serverCode) {
1445
+ super(message);
1446
+ this.status = status;
1447
+ this.code = code;
1448
+ this.detail = detail;
1449
+ this.serverCode = serverCode;
1450
+ this.name = "QuerySnapshotHttpError";
1451
+ }
1452
+ };
1453
+ async function snapshotHttpError(response) {
1454
+ let detail;
1455
+ let serverCode;
1456
+ const type = response.headers.get("content-type")?.split(";")[0]?.trim().toLowerCase();
1457
+ if (response.body && (type === "text/plain" || type === "application/json")) {
1458
+ const reader = response.body.getReader();
1459
+ try {
1460
+ const chunks = [];
1461
+ let size = 0;
1462
+ let complete = false;
1463
+ while (size <= 4096) {
1464
+ const next = await reader.read();
1465
+ if (next.done) {
1466
+ complete = true;
1467
+ break;
1468
+ }
1469
+ size += next.value.byteLength;
1470
+ if (size <= 4096) chunks.push(next.value);
1471
+ }
1472
+ if (complete) {
1473
+ const bytes = new Uint8Array(size);
1474
+ let offset = 0;
1475
+ for (const chunk of chunks) {
1476
+ bytes.set(chunk, offset);
1477
+ offset += chunk.length;
1478
+ }
1479
+ const text = new TextDecoder().decode(bytes);
1480
+ let reason = text;
1481
+ if (type === "application/json") {
1482
+ const body = JSON.parse(text);
1483
+ reason = body?.message ?? body?.error?.message ?? body?.error;
1484
+ const code2 = body?.code ?? body?.error?.code;
1485
+ if (typeof code2 === "string" && /^[A-Za-z0-9_.-]{1,64}$/.test(code2)) serverCode = code2;
1486
+ }
1487
+ if (typeof reason === "string" && !/[<>]/.test(reason)) {
1488
+ const safe = reason.replace(/[\u0000-\u001f\u007f]/g, " ").trim();
1489
+ if (safe) detail = safe.slice(0, 512);
1490
+ }
1491
+ }
1492
+ } catch {
1493
+ } finally {
1494
+ try {
1495
+ await reader.cancel();
1496
+ } catch {
1497
+ }
1498
+ reader.releaseLock();
1499
+ }
1500
+ }
1501
+ const status = response.status;
1502
+ const code = status === 507 ? "quota" : status === 401 || status === 403 ? "forbidden" : status === 409 ? "conflict" : status === 404 ? "unavailable" : status >= 500 ? "storage" : "http";
1503
+ const messages = {
1504
+ quota: "Query snapshot storage is full. Ask your host to increase its quota, then retry saving. Existing links still work.",
1505
+ forbidden: "You do not have permission to save or open this query link. Sign in or ask your host for access.",
1506
+ conflict: "This query link code already identifies different state. Ask your host to check its snapshot store.",
1507
+ unavailable: "This query link is unavailable. Check its address or ask its author for a new link.",
1508
+ storage: "Query snapshot storage is temporarily unavailable. Retry later; your previous saved query is unchanged.",
1509
+ http: `Query snapshot request failed (${status}). Check the request or retry later.`
1510
+ };
1511
+ const message = messages[code] + (code !== "http" ? ` (HTTP ${status})` : "") + (detail && (status < 500 || status === 507) ? ` ${detail}` : "");
1512
+ return new QuerySnapshotHttpError(status, code, message, detail, serverCode);
1513
+ }
1514
+ function httpQuerySnapshotStore(base, fetcher = fetch) {
1515
+ const url = (key, code) => `${base}${base.includes("?") ? "&" : "?"}dataset=${encodeURIComponent(key)}&code=${encodeURIComponent(code)}`;
1516
+ async function responseSnapshot(response) {
1517
+ if (!response.ok) throw await snapshotHttpError(response);
1518
+ return validateQuerySnapshot(await response.json());
1519
+ }
1520
+ return {
1521
+ async save(key, snapshot, signal) {
1522
+ const result = await responseSnapshot(await fetcher(url(key, snapshot.code), {
1523
+ method: "PUT",
1524
+ headers: { "Content-Type": "application/json" },
1525
+ body: JSON.stringify(snapshot),
1526
+ ...signal ? { signal } : {}
1527
+ }));
1528
+ if (result.code !== snapshot.code || result.token !== snapshot.token) throw Error("The snapshot store changed this immutable query link.");
1529
+ return result;
1530
+ },
1531
+ async resolve(key, code, signal) {
1532
+ const response = await fetcher(url(key, code), signal ? { signal } : {});
1533
+ if (response.status === 404) return null;
1534
+ const result = await responseSnapshot(response);
1535
+ if (result.code !== code) throw Error("The snapshot store returned a different query link.");
1536
+ return result;
1537
+ }
1538
+ };
1539
+ }
1540
+ function createQueryLinkService(key, store, options2 = {}) {
1541
+ const budget = options2.maxUrlBytes ?? 2e3;
1542
+ if (!Number.isInteger(budget) || budget < 1) throw Error("Invalid query URL budget.");
1543
+ const cache = options2.cache ?? localQuerySnapshotCache();
1544
+ const promoted = /* @__PURE__ */ new Map();
1545
+ const attempts = /* @__PURE__ */ new Map();
1546
+ const known = /* @__PURE__ */ new Map();
1547
+ function remember(snapshot2) {
1548
+ known.set(snapshot2.code, snapshot2);
1549
+ while (known.size > 100) known.delete(known.keys().next().value);
1550
+ try {
1551
+ cache.put(key, snapshot2);
1552
+ cache.markDurable?.(key, snapshot2.code);
1553
+ } catch {
1554
+ }
1555
+ }
1556
+ function link(base, parameter, value, kind) {
1557
+ const url = new URL(base);
1558
+ url.searchParams.set(parameter, value);
1559
+ const bytes = new TextEncoder().encode(url.toString()).length;
1560
+ if (bytes > budget) throw Error("The query link exceeds the URL budget, including the page address.");
1561
+ return { url: url.toString(), kind, bytes, ...kind !== "inline" ? { snapshotCode: value.slice(1) } : {} };
1562
+ }
1563
+ function cached(token) {
1564
+ try {
1565
+ return cache.find(key, token);
1566
+ } catch {
1567
+ return null;
1568
+ }
1569
+ }
1570
+ async function snapshot(query) {
1571
+ if (!store) return null;
1572
+ const token = encodeQuery(query);
1573
+ const existing = promoted.get(token);
1574
+ if (existing) return existing;
1575
+ const local = attempts.get(token) ?? cached(token) ?? newSnapshot(token);
1576
+ attempts.set(token, local);
1577
+ while (attempts.size > 100) attempts.delete(attempts.keys().next().value);
1578
+ try {
1579
+ cache.put(key, local);
1580
+ } catch {
1581
+ }
1582
+ const pending = store.save(key, local).then((result) => {
1583
+ const valid = validateQuerySnapshot(result);
1584
+ if (valid.code !== local.code || valid.token !== token) throw Error("The snapshot store changed this immutable query link.");
1585
+ remember(valid);
1586
+ return valid;
1587
+ });
1588
+ promoted.set(token, pending);
1589
+ while (promoted.size > 100) promoted.delete(promoted.keys().next().value);
1590
+ try {
1591
+ return await pending;
1592
+ } catch (error) {
1593
+ promoted.delete(token);
1594
+ throw error;
1595
+ }
1596
+ }
1597
+ return {
1598
+ local(query, baseUrl, parameter = "q") {
1599
+ const token = encodeQuery(query);
1600
+ const current = new URL(baseUrl).searchParams.get(parameter);
1601
+ if (current?.startsWith("@") && known.get(current.slice(1))?.token === token)
1602
+ return link(baseUrl, parameter, current, "durable");
1603
+ if (current?.startsWith("@")) {
1604
+ try {
1605
+ const code = current.slice(1);
1606
+ if (cache.isDurable?.(key, code) && cache.get(key, code)?.token === token)
1607
+ return link(baseUrl, parameter, current, "durable");
1608
+ } catch {
1609
+ }
1610
+ }
1611
+ const url = new URL(baseUrl);
1612
+ url.searchParams.set(parameter, token);
1613
+ if (new TextEncoder().encode(url.toString()).length <= budget) return link(baseUrl, parameter, token, "inline");
1614
+ const local = cached(token) ?? newSnapshot(token);
1615
+ const result = link(baseUrl, parameter, "@" + local.code, "local");
1616
+ cache.put(key, local);
1617
+ return result;
1618
+ },
1619
+ async durable(query, baseUrl, parameter = "q") {
1620
+ const saved = await snapshot(query);
1621
+ if (saved) return link(baseUrl, parameter, "@" + saved.code, "durable");
1622
+ const token = encodeQuery(query);
1623
+ return link(baseUrl, parameter, token, "inline");
1624
+ },
1625
+ snapshot,
1626
+ async resolve(baseUrl, parameter = "q", signal) {
1627
+ const values = new URL(baseUrl).searchParams.getAll(parameter);
1628
+ if (!values.length || values.length === 1 && values[0] === "") return null;
1629
+ if (values.length !== 1) throw Error("Invalid or conflicting query link parameters.");
1630
+ const value = values[0];
1631
+ if (!value.startsWith("@")) return decodeQueryStrict(value);
1632
+ const code = value.slice(1);
1633
+ if (!codePattern.test(code)) throw Error("Invalid query link code.");
1634
+ let local = null;
1635
+ try {
1636
+ local = cache.get(key, code);
1637
+ if (local) local = validateQuerySnapshot(local);
1638
+ } catch {
1639
+ local = null;
1640
+ }
1641
+ const result = local ?? (store ? await store.resolve(key, code, signal) : null);
1642
+ if (!result) throw Error("This query link is unavailable. Its local cache entry may have expired; use a saved or shared link for permanent access.");
1643
+ const valid = validateQuerySnapshot(result);
1644
+ if (valid.code !== code) throw Error("Invalid query snapshot code.");
1645
+ if (!local) remember(valid);
1646
+ else {
1647
+ try {
1648
+ if (cache.isDurable?.(key, code)) remember(valid);
1649
+ } catch {
1650
+ }
1651
+ }
1652
+ return decodeQueryStrict(valid.token);
1653
+ }
1654
+ };
1655
+ }
1656
+
1657
+ // src/computed.ts
1658
+ var COMPUTED_PREFIX = "@computed/";
1659
+ var computedFieldName = (id) => COMPUTED_PREFIX + id;
1660
+ var isComputedField = (name) => name.trim().toLowerCase().startsWith(COMPUTED_PREFIX);
1661
+ function validateComputedColumn(input) {
1662
+ const c = input;
1663
+ if (!c || typeof c.id !== "string" || !/^[a-zA-Z0-9_-]{1,128}$/.test(c.id) || typeof c.label !== "string" || !c.label.trim() || c.label.length > 200 || typeof c.revision !== "string" || !c.revision || c.revision.length > 256 || c.expression?.language !== "qt-expr" || c.expression.version !== 1 || typeof c.expression.source !== "string" || !c.expression.source.trim() || c.expression.source.length > 1e4) {
1664
+ throw new Error(
1665
+ "Invalid computed column definition or unsupported formula version."
1666
+ );
1667
+ }
1668
+ return {
1669
+ id: c.id,
1670
+ label: c.label,
1671
+ revision: c.revision,
1672
+ expression: { ...c.expression }
1673
+ };
1674
+ }
1675
+ function memoryComputedColumnStore(seed = {}) {
1676
+ const data = new Map(
1677
+ Object.entries(seed).map(([k, v]) => [
1678
+ k,
1679
+ new Map(v.map((c) => [c.id, validateComputedColumn(c)]))
1680
+ ])
1681
+ );
1682
+ const seedRevisions = new Set(
1683
+ [...data.values()].flatMap(
1684
+ (entries) => [...entries.values()].map((c) => c.revision)
1685
+ )
1686
+ );
1687
+ let revisionSequence = 0n;
1688
+ function freshRevision() {
1689
+ let revision;
1690
+ do {
1691
+ revision = `memory:${++revisionSequence}`;
1692
+ } while (seedRevisions.has(revision));
1693
+ return revision;
1694
+ }
1695
+ const listeners = /* @__PURE__ */ new Map();
1696
+ return {
1697
+ async list(dataset) {
1698
+ return [...data.get(dataset)?.values() ?? []].map(
1699
+ validateComputedColumn
1700
+ );
1701
+ },
1702
+ async save(dataset, column, expectedRevision) {
1703
+ const entries = data.get(dataset) ?? /* @__PURE__ */ new Map();
1704
+ const current = entries.get(column.id);
1705
+ if ((current?.revision ?? null) !== expectedRevision)
1706
+ throw new Error(
1707
+ "This definition changed. Reload it before saving again."
1708
+ );
1709
+ const next = validateComputedColumn({
1710
+ ...column,
1711
+ revision: freshRevision()
1712
+ });
1713
+ entries.set(next.id, next);
1714
+ data.set(dataset, entries);
1715
+ listeners.get(dataset)?.forEach((fn) => fn());
1716
+ return validateComputedColumn(next);
1717
+ },
1718
+ subscribe(dataset, listener) {
1719
+ const set = listeners.get(dataset) ?? /* @__PURE__ */ new Set();
1720
+ set.add(listener);
1721
+ listeners.set(dataset, set);
1722
+ return () => {
1723
+ set.delete(listener);
1724
+ };
1725
+ }
1726
+ };
1727
+ }
1728
+ function httpComputedColumnStore(base, fetcher = fetch) {
1729
+ const listeners = /* @__PURE__ */ new Map();
1730
+ const url = (dataset) => `${base}${base.includes("?") ? "&" : "?"}dataset=${encodeURIComponent(dataset)}`;
1731
+ async function read(response) {
1732
+ if (response.status === 409)
1733
+ throw new Error(
1734
+ "This definition changed. Reload it before saving again."
1735
+ );
1736
+ if (!response.ok)
1737
+ throw new Error(
1738
+ `Computed column store request failed (${response.status}).`
1739
+ );
1740
+ return response.json();
1741
+ }
1742
+ return {
1743
+ async list(dataset, signal) {
1744
+ const raw = await read(
1745
+ await fetcher(url(dataset), { ...signal ? { signal } : {} })
1746
+ );
1747
+ if (!Array.isArray(raw))
1748
+ throw new Error("Invalid computed column catalogue.");
1749
+ return raw.map(validateComputedColumn);
1750
+ },
1751
+ async save(dataset, column, expectedRevision) {
1752
+ const result = validateComputedColumn(
1753
+ await read(
1754
+ await fetcher(url(dataset), {
1755
+ method: "PUT",
1756
+ headers: { "Content-Type": "application/json" },
1757
+ body: JSON.stringify({ column, expectedRevision })
1758
+ })
1759
+ )
1760
+ );
1761
+ listeners.get(dataset)?.forEach((fn) => fn());
1762
+ return result;
1763
+ },
1764
+ subscribe(dataset, listener) {
1765
+ const set = listeners.get(dataset) ?? /* @__PURE__ */ new Set();
1766
+ set.add(listener);
1767
+ listeners.set(dataset, set);
1768
+ return () => {
1769
+ set.delete(listener);
1770
+ };
1771
+ }
1772
+ };
1773
+ }
1774
+ function isComputedCellError(value) {
1775
+ return value !== null && typeof value === "object" && "computedError" in value && typeof value.computedError === "string";
1776
+ }
1777
+ function groupPreview(dependencies, inputs, results, total) {
1778
+ const groups = /* @__PURE__ */ new Map();
1779
+ let nulls = 0, errors = 0;
1780
+ inputs.forEach((row, i) => {
1781
+ const values = dependencies.map((name) => row[name] ?? null), result = results[i] ?? { value: null, error: "No result." };
1782
+ const key = JSON.stringify([values, result.value, result.error ?? null]);
1783
+ const group = groups.get(key);
1784
+ if (group) group.count++;
1785
+ else groups.set(key, { inputs: values, result, count: 1 });
1786
+ if (result.error) errors++;
1787
+ else if (result.value === null) nulls++;
1788
+ });
1789
+ return {
1790
+ dependencies,
1791
+ groups: [...groups.values()].sort((a, b) => b.count - a.count),
1792
+ processed: inputs.length,
1793
+ total,
1794
+ nulls,
1795
+ errors
1796
+ };
979
1797
  }
980
1798
 
981
1799
  // src/formula.ts
@@ -2089,6 +2907,455 @@ function formulaRuntime(ast, inputs, extension) {
2089
2907
  }
2090
2908
  }
2091
2909
 
2910
+ // src/grouping.ts
2911
+ var GROUP_ID_FIELD = "__qt_group_id__";
2912
+ var MAX_REPRESENTATIVE_SORTS = 8;
2913
+ var MAX_SUMMARY_COLUMNS = 64;
2914
+ var MAX_RESULT_GROUPS = 1e4;
2915
+ function withoutGrouping(query) {
2916
+ const { grouping: _grouping, ...result } = query;
2917
+ return result;
2918
+ }
2919
+ var OP_LABEL = {
2920
+ count: "COUNT",
2921
+ count_distinct: "DISTINCT",
2922
+ sum: "SUM",
2923
+ avg: "AVG",
2924
+ min: "MIN",
2925
+ max: "MAX",
2926
+ row: "ROW",
2927
+ formula: "\u0192x"
2928
+ };
2929
+ function createGroupingModel(inputSchema) {
2930
+ const sourceField = (name) => inputSchema.fields.find((f2) => f2.name === name);
2931
+ const groupableFields = inputSchema.fields.filter(
2932
+ (field) => isGroupable(field) && isSelectable(field)
2933
+ );
2934
+ const summaryFields = inputSchema.fields.filter(
2935
+ (f2) => f2.type !== "textarray" && isSelectable(f2)
2936
+ );
2937
+ const representativeFields = inputSchema.fields.filter(
2938
+ (field) => isSortable(field) && field.type !== "textarray"
2939
+ );
2940
+ function representativeLabel(rule) {
2941
+ return rule.orderBy.map(
2942
+ (term) => `${sourceField(term.field)?.label ?? term.field} ${term.dir === "asc" ? "\u2191" : "\u2193"}${term.nulls === "first" ? " (empty first)" : ""}`
2943
+ ).join(", ");
2944
+ }
2945
+ function allowedOps(field) {
2946
+ if (!field) return ["count"];
2947
+ const f2 = sourceField(field);
2948
+ return f2 && isSelectable(f2) ? [...isMeasurable(f2) ? aggOpsForField(f2) : [], "row"] : [];
2949
+ }
2950
+ function summaryExpression(column) {
2951
+ if (column.op === "formula") return column.expression ?? "";
2952
+ const field = column.field ? `[${column.field.replace(/\]/g, "]]")}]` : "";
2953
+ return `${column.op === "row" ? "COUNT" : column.op.toUpperCase()}(${field})`;
2954
+ }
2955
+ function summaryProblem(column) {
2956
+ if (column.op !== "formula") return null;
2957
+ try {
2958
+ const plan = compileMetric(summaryExpression(column), inputSchema);
2959
+ if (plan.dependencies.some((name) => !isSelectable(sourceField(name))))
2960
+ throw new Error("Aggregate fields must be selectable.");
2961
+ if (hasRegex(plan))
2962
+ throw new Error(
2963
+ "Regex aggregate formulas require isolated execution and are not supported in grouped columns."
2964
+ );
2965
+ return null;
2966
+ } catch (error) {
2967
+ return error instanceof Error ? error.message : String(error);
2968
+ }
2969
+ }
2970
+ function summaryLabel(column) {
2971
+ if (column.op === "formula")
2972
+ return column.label?.trim() || "Custom aggregate";
2973
+ if (!column.field) return "Row count";
2974
+ const label = sourceField(column.field)?.label ?? column.field;
2975
+ const prefix = {
2976
+ count: "Count of",
2977
+ count_distinct: "Distinct",
2978
+ sum: "Total",
2979
+ avg: "Average",
2980
+ min: "Min",
2981
+ max: "Max",
2982
+ row: "Representative \xB7",
2983
+ formula: ""
2984
+ };
2985
+ return `${prefix[column.op]} ${label}`;
2986
+ }
2987
+ function resultSchema(keys, summaries) {
2988
+ const fields = [
2989
+ {
2990
+ name: GROUP_ID_FIELD,
2991
+ label: "Group identity",
2992
+ type: "text",
2993
+ source: { kind: "backend" },
2994
+ select: { enabled: false },
2995
+ filter: { enabled: false },
2996
+ sort: { enabled: false },
2997
+ aggregate: { measure: false, groupable: false }
2998
+ },
2999
+ ...keys.flatMap((key) => {
3000
+ const field = sourceField(key);
3001
+ if (!field) return [];
3002
+ return [{
3003
+ name: key,
3004
+ label: field.label,
3005
+ type: field.type,
3006
+ source: { kind: "backend" },
3007
+ ...field.select ? { select: { ...field.select } } : {},
3008
+ ...field.filter ? { filter: { ...field.filter } } : {},
3009
+ ...field.aggregate ? { aggregate: { ...field.aggregate } } : {},
3010
+ ...field.sort?.enabled !== void 0 ? { sort: { enabled: field.sort.enabled } } : {},
3011
+ ...typeof field.render === "string" ? { render: field.render } : {},
3012
+ ...field.display ? { display: field.display } : {},
3013
+ group: "Group keys"
3014
+ }];
3015
+ })
3016
+ ];
3017
+ for (const column of summaries) {
3018
+ if (column.op === "formula") {
3019
+ let type = "number";
3020
+ try {
3021
+ const compiled = compileMetric(
3022
+ summaryExpression(column),
3023
+ inputSchema
3024
+ );
3025
+ type = compiled.type === "null" ? "number" : compiled.type;
3026
+ } catch {
3027
+ }
3028
+ fields.push({
3029
+ name: column.id,
3030
+ label: summaryLabel(column),
3031
+ type,
3032
+ source: { kind: "backend" },
3033
+ group: "Aggregates",
3034
+ select: { align: type === "number" ? "right" : "left" }
3035
+ });
3036
+ continue;
3037
+ }
3038
+ const input = column.field ? sourceField(column.field) : void 0;
3039
+ const counting = column.op === "count" || column.op === "count_distinct";
3040
+ const numeric = counting || column.op === "sum" || column.op === "avg";
3041
+ const example = column.op === "row";
3042
+ fields.push({
3043
+ name: column.id,
3044
+ label: summaryLabel(column),
3045
+ type: numeric ? "number" : input?.type ?? "number",
3046
+ source: { kind: "backend" },
3047
+ select: {
3048
+ align: numeric || input?.type === "number" ? "right" : "left"
3049
+ },
3050
+ group: example ? "Representative row" : "Summaries",
3051
+ ...!numeric && input?.aggregate?.groupable !== void 0 ? { aggregate: { groupable: input.aggregate.groupable } } : {},
3052
+ ...typeof input?.render === "string" && !counting ? { render: input.render } : {},
3053
+ ...input?.display && !counting ? { display: input.display } : {}
3054
+ });
3055
+ }
3056
+ return {
3057
+ name: `${inputSchema.name}:groups`,
3058
+ idField: GROUP_ID_FIELD,
3059
+ fields,
3060
+ defaultSelect: keys.filter((key) => sourceField(key)).map((field) => ({ field })),
3061
+ defaultLimit: 25
3062
+ };
3063
+ }
3064
+ function keyValue(value) {
3065
+ if (value == null) return null;
3066
+ if (value instanceof Date) return value.toISOString();
3067
+ if (typeof value === "string" || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value))
3068
+ return value;
3069
+ throw new Error("Group keys must be finite scalar values.");
3070
+ }
3071
+ function groupId(row, keys, schema) {
3072
+ const tuple = [...keys].sort().map((key) => [
3073
+ key,
3074
+ keyValue(
3075
+ readFieldValue(schema.fields.find((f2) => f2.name === key), row)
3076
+ )
3077
+ ]);
3078
+ if (tuple.some(
3079
+ ([key, value]) => String(key).length + String(value).length > 1e4
3080
+ ))
3081
+ throw new Error("Grouping key length budget exceeded.");
3082
+ const id = JSON.stringify(tuple);
3083
+ if (id.length > 65536)
3084
+ throw new Error("Grouping key length budget exceeded.");
3085
+ return id;
3086
+ }
3087
+ function compare2(a, b) {
3088
+ if (a === b) return 0;
3089
+ if (a == null) return -1;
3090
+ if (b == null) return 1;
3091
+ return a < b ? -1 : 1;
3092
+ }
3093
+ function representative(rows, rule, schema) {
3094
+ const identityField = schema.fields.find(
3095
+ (field) => field.name === schema.idField
3096
+ );
3097
+ const identities = /* @__PURE__ */ new Set();
3098
+ for (const row of rows) {
3099
+ const id = identityField ? readFieldValue(identityField, row) : row[schema.idField];
3100
+ if (!(typeof id === "string" || typeof id === "number" && Number.isFinite(id)) || identities.has(id))
3101
+ throw new Error(
3102
+ "Representative rows require unique string or finite numeric source IDs."
3103
+ );
3104
+ identities.add(id);
3105
+ }
3106
+ const order = rule.orderBy.some((term) => term.field === schema.idField) ? rule.orderBy : [
3107
+ ...rule.orderBy,
3108
+ { field: schema.idField, dir: "desc", nulls: "last" }
3109
+ ];
3110
+ const terms = order.map((term) => ({
3111
+ ...term,
3112
+ def: schema.fields.find((field) => field.name === term.field)
3113
+ }));
3114
+ return rows.reduce((best, row) => {
3115
+ for (const term of terms) {
3116
+ const value = (record2) => {
3117
+ const raw = term.def ? readFieldValue(term.def, record2) : record2[schema.idField];
3118
+ if (raw == null || term.def?.type !== "datetime") return raw;
3119
+ const instant = Date.parse(String(raw));
3120
+ return Number.isFinite(instant) ? instant : null;
3121
+ };
3122
+ const a = value(row), b = value(best);
3123
+ if (a == null || b == null) {
3124
+ if (a == null && b == null) continue;
3125
+ return a == null === (term.nulls === "first") ? row : best;
3126
+ }
3127
+ const rank = compare2(a, b);
3128
+ if (rank !== 0)
3129
+ return (term.dir === "asc" ? rank < 0 : rank > 0) ? row : best;
3130
+ }
3131
+ return best;
3132
+ });
3133
+ }
3134
+ function groupRows(rows, schema, keys, summaries, rule = null) {
3135
+ if (!rule && summaries.some((column) => column.op === "row"))
3136
+ throw new Error(
3137
+ "Configure representative-row ordering before selecting row values."
3138
+ );
3139
+ if (keys.length === 0) return [];
3140
+ if (keys.length > MAX_GROUP_BY_FIELDS || summaries.length > MAX_SUMMARY_COLUMNS)
3141
+ throw new Error("Grouping definition exceeds column limits.");
3142
+ if (rows.length > 1e6)
3143
+ throw new Error("Grouping row budget exceeded.");
3144
+ let bytes = rows.length * 16;
3145
+ let operations = 0;
3146
+ const groups = /* @__PURE__ */ new Map();
3147
+ for (const row of rows) {
3148
+ const id = groupId(row, keys, schema);
3149
+ const bucket = groups.get(id);
3150
+ if (bucket) bucket.push(row);
3151
+ else {
3152
+ if (groups.size >= MAX_RESULT_GROUPS)
3153
+ throw new Error(
3154
+ `Grouping exceeds ${MAX_RESULT_GROUPS.toLocaleString()} groups.`
3155
+ );
3156
+ bytes += id.length * 2 + 256;
3157
+ if (bytes > 16e6)
3158
+ throw new Error("Grouping retained-memory budget exceeded.");
3159
+ groups.set(id, [row]);
3160
+ }
3161
+ }
3162
+ const formulas = new Map(
3163
+ summaries.filter((c) => c.op !== "row").map((column) => {
3164
+ try {
3165
+ const problem = summaryProblem(column);
3166
+ if (problem) throw new Error(problem);
3167
+ return [
3168
+ column.id,
3169
+ { plan: compileMetric(summaryExpression(column), schema) }
3170
+ ];
3171
+ } catch (error) {
3172
+ return [
3173
+ column.id,
3174
+ { error: error instanceof Error ? error.message : String(error) }
3175
+ ];
3176
+ }
3177
+ })
3178
+ );
3179
+ const nodeCost = (node) => 1 + (node.kind === "call" ? node.args.reduce((cost, child) => cost + nodeCost(child), 0) : 0);
3180
+ const costs = new Map(
3181
+ [...formulas].map(([id, compiled]) => [
3182
+ id,
3183
+ compiled.plan?.aggregates.map(nodeCost) ?? []
3184
+ ])
3185
+ );
3186
+ return [...groups].map(([id, members]) => {
3187
+ const output = Object.assign(/* @__PURE__ */ Object.create(null), {
3188
+ [GROUP_ID_FIELD]: id
3189
+ });
3190
+ for (const key of keys)
3191
+ output[key] = readFieldValue(
3192
+ schema.fields.find((f2) => f2.name === key),
3193
+ members[0]
3194
+ );
3195
+ const chosen = rule && summaries.some((column) => column.op === "row") ? representative(members, rule, schema) : null;
3196
+ for (const column of summaries) {
3197
+ if (column.op !== "row") {
3198
+ const compiled = formulas.get(column.id);
3199
+ if (!compiled.plan) {
3200
+ output[column.id] = { computedError: compiled.error };
3201
+ continue;
3202
+ }
3203
+ const plan = compiled.plan;
3204
+ const rowPlans = metricAggregatePlans(plan);
3205
+ const reductions = plan.aggregates.map(
3206
+ (node, i) => reduceMetricValues(
3207
+ node,
3208
+ members.map((row) => {
3209
+ if ((operations += costs.get(column.id)?.[i] ?? 1) > 1e7)
3210
+ throw new Error("Grouping operation budget exceeded.");
3211
+ const rowPlan = rowPlans[i];
3212
+ if (!rowPlan) return { value: 1 };
3213
+ const values = Object.fromEntries(
3214
+ rowPlan.dependencies.map((name) => {
3215
+ const value = readFieldValue(
3216
+ schema.fields.find((f2) => f2.name === name),
3217
+ row
3218
+ );
3219
+ return [
3220
+ name,
3221
+ value instanceof Date ? value.toISOString() : value ?? null
3222
+ ];
3223
+ })
3224
+ );
3225
+ return formulaRuntime(rowPlan.ast, values);
3226
+ })
3227
+ )
3228
+ );
3229
+ const result = evaluateMetricReductions(plan, reductions);
3230
+ output[column.id] = result.error ? { computedError: result.error } : result.value;
3231
+ continue;
3232
+ }
3233
+ const field = schema.fields.find((f2) => f2.name === column.field);
3234
+ if (column.op === "row") {
3235
+ output[column.id] = field && chosen ? readFieldValue(field, chosen) : null;
3236
+ continue;
3237
+ }
3238
+ }
3239
+ return output;
3240
+ });
3241
+ }
3242
+ function filteredInput(rows, query, schema, options2 = {}) {
3243
+ return selectMetricShownRows(
3244
+ rows,
3245
+ {
3246
+ ...withoutGrouping(query),
3247
+ aggregations: [],
3248
+ orderBy: [],
3249
+ offset: 0,
3250
+ limit: Math.max(1, rows.length)
3251
+ },
3252
+ { ...schema, defaultSort: [] },
3253
+ options2
3254
+ );
3255
+ }
3256
+ function reconcileRepresentative(query, summaries, schema, remove) {
3257
+ const affected = new Set(
3258
+ summaries.filter((s) => s.op === "row").map((s) => s.id)
3259
+ );
3260
+ if (!affected.size) return query;
3261
+ let grew = true;
3262
+ while (grew) {
3263
+ grew = false;
3264
+ for (const f2 of schema.fields) {
3265
+ if (!affected.has(f2.name) && f2.source.kind === "derived" && f2.source.dependencies?.some((name) => affected.has(name))) {
3266
+ affected.add(f2.name);
3267
+ grew = true;
3268
+ }
3269
+ }
3270
+ }
3271
+ return {
3272
+ ...query,
3273
+ offset: 0,
3274
+ select: remove ? query.select.filter((c) => !affected.has(c.field)) : query.select,
3275
+ where: query.where.filter(
3276
+ (term) => !predicatesOf(term).some((p) => affected.has(p.field))
3277
+ ),
3278
+ orderBy: query.orderBy.filter((term) => !affected.has(term.field)),
3279
+ aggregations: (query.aggregations ?? []).filter((metric) => {
3280
+ if (metric.groupBy.some((name) => affected.has(name))) return false;
3281
+ try {
3282
+ return !metricPlans(metric, schema).some(
3283
+ (plan) => plan.dependencies.some((name) => affected.has(name))
3284
+ );
3285
+ } catch {
3286
+ return false;
3287
+ }
3288
+ })
3289
+ };
3290
+ }
3291
+ function validate(definition) {
3292
+ if (definition.diagnostics?.length)
3293
+ throw new Error(definition.diagnostics.join(" "));
3294
+ if (!definition.keys.length || definition.keys.length > MAX_GROUP_BY_FIELDS || new Set(definition.keys).size !== definition.keys.length || definition.keys.some(
3295
+ (key) => key === GROUP_ID_FIELD || !groupableFields.some((f2) => f2.name === key)
3296
+ ))
3297
+ throw new Error("Choose valid group keys from the source schema.");
3298
+ if (definition.summaries.length > MAX_SUMMARY_COLUMNS)
3299
+ throw new Error("Too many aggregate columns.");
3300
+ const ids = /* @__PURE__ */ new Set([
3301
+ GROUP_ID_FIELD,
3302
+ ...inputSchema.fields.map((field) => field.name)
3303
+ ]);
3304
+ for (const column of definition.summaries) {
3305
+ if (!column.id || column.id.length > 256 || ids.has(column.id) || isComputedField(column.id) || ["__proto__", "constructor", "prototype"].includes(column.id))
3306
+ throw new Error(
3307
+ "Aggregate column IDs must be unique and distinct from source fields."
3308
+ );
3309
+ ids.add(column.id);
3310
+ if (column.op === "formula") {
3311
+ const error = summaryProblem(column);
3312
+ if (error) throw new Error(error);
3313
+ } else if (!allowedOps(column.field).includes(column.op) || column.op === "row" && (!column.field || !definition.representative))
3314
+ throw new Error("Invalid aggregate or representative column.");
3315
+ }
3316
+ const rule = definition.representative;
3317
+ if (rule && (!rule.orderBy.length || rule.orderBy.length > MAX_REPRESENTATIVE_SORTS || new Set(rule.orderBy.map((t) => t.field)).size !== rule.orderBy.length || rule.orderBy.some(
3318
+ (t) => !representativeFields.some((f2) => f2.name === t.field) || !["asc", "desc"].includes(t.dir) || !["first", "last"].includes(t.nulls)
3319
+ )))
3320
+ throw new Error("Choose valid representative-row ordering.");
3321
+ }
3322
+ return {
3323
+ inputSchema,
3324
+ sourceField,
3325
+ groupableFields,
3326
+ summaryFields,
3327
+ representativeFields,
3328
+ representativeLabel,
3329
+ allowedOps,
3330
+ summaryExpression,
3331
+ summaryProblem,
3332
+ summaryLabel,
3333
+ resultSchema,
3334
+ groupRows,
3335
+ filteredInput,
3336
+ reconcileRepresentative,
3337
+ validate
3338
+ };
3339
+ }
3340
+ function prepareGrouping(rows, schema, grouping, options2 = {}) {
3341
+ const model = createGroupingModel(schema);
3342
+ if (grouping.source.grouping)
3343
+ throw new Error("Only one grouping stage is supported.");
3344
+ model.validate(grouping);
3345
+ const inputs = model.filteredInput(rows, grouping.source, schema, options2);
3346
+ return {
3347
+ inputs,
3348
+ rows: model.groupRows(
3349
+ inputs,
3350
+ schema,
3351
+ grouping.keys,
3352
+ grouping.summaries,
3353
+ grouping.representative
3354
+ ),
3355
+ schema: model.resultSchema(grouping.keys, grouping.summaries)
3356
+ };
3357
+ }
3358
+
2092
3359
  // src/execution.ts
2093
3360
  function toServerQueryV2(q, schema, execution) {
2094
3361
  const normalized = normalizeQueryState(q), legacy = toServerQuery(normalized, schema);
@@ -2115,7 +3382,7 @@ function toServerQueryV2(q, schema, execution) {
2115
3382
  }
2116
3383
  const select = [...legacy.select];
2117
3384
  for (const c of normalized.select.length ? normalized.select : selectedFields(schema, normalized).map((f2) => ({ field: f2.name })))
2118
- if (isComputedField(c.field) && requireField(c.field, "select"))
3385
+ if (isComputedField(c.field) && execution.fields[c.field]?.select && requireField(c.field, "select"))
2119
3386
  select.push(c.field);
2120
3387
  const orderBy = [];
2121
3388
  for (const term of normalized.orderBy.length ? normalized.orderBy : schema.defaultSort ?? []) {
@@ -2286,6 +3553,171 @@ function histogramCounts(values, edges) {
2286
3553
  return counts;
2287
3554
  }
2288
3555
 
3556
+ // src/metricDates.ts
3557
+ import { Temporal } from "temporal-polyfill";
3558
+ var METRIC_DATE_INTERVALS = [
3559
+ "hour",
3560
+ "day",
3561
+ "week",
3562
+ "month",
3563
+ "year"
3564
+ ];
3565
+ function metricDateTime(value, timeZone = "UTC") {
3566
+ 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(
3567
+ value
3568
+ ))
3569
+ throw Error(
3570
+ "Date grouping requires ISO dates or timestamps with a timezone."
3571
+ );
3572
+ const time = Date.parse(value), month = Number(value.slice(5, 7)), day = Number(value.slice(8, 10));
3573
+ const calendar = /* @__PURE__ */ new Date(0);
3574
+ calendar.setUTCFullYear(Number(value.slice(0, 4)), month, 0);
3575
+ if (!Number.isFinite(time) || month < 1 || month > 12 || day < 1 || day > calendar.getUTCDate())
3576
+ throw Error("Invalid date grouping value.");
3577
+ return value.length === 10 && timeZone !== "UTC" ? Temporal.PlainDate.from(value).toZonedDateTime(timeZone).epochMilliseconds : time;
3578
+ }
3579
+ function metricOffsetTransition(start, end, timeZone) {
3580
+ if (end <= start) return void 0;
3581
+ const offset = (time) => Temporal.Instant.fromEpochMilliseconds(time).toZonedDateTimeISO(timeZone).offsetNanoseconds;
3582
+ const before = offset(start);
3583
+ if (before === offset(end)) return void 0;
3584
+ while (end - start > 1) {
3585
+ const middle = Math.floor((start + end) / 2);
3586
+ if (offset(middle) === before) start = middle;
3587
+ else end = middle;
3588
+ }
3589
+ return end;
3590
+ }
3591
+ function metricDateFloor(time, interval, timeZone = "UTC") {
3592
+ if (timeZone !== "UTC") {
3593
+ const zoned = Temporal.Instant.fromEpochMilliseconds(time).toZonedDateTimeISO(timeZone);
3594
+ if (interval === "hour") {
3595
+ const floor = time - zoned.minute * 6e4 - zoned.second * 1e3 - zoned.millisecond;
3596
+ return metricOffsetTransition(floor, time, timeZone) ?? floor;
3597
+ }
3598
+ let date2 = zoned.toPlainDate();
3599
+ if (interval === "week") date2 = date2.subtract({ days: date2.dayOfWeek - 1 });
3600
+ if (interval === "month" || interval === "year")
3601
+ date2 = date2.with({ day: 1 });
3602
+ if (interval === "year") date2 = date2.with({ month: 1 });
3603
+ return date2.toZonedDateTime(timeZone).epochMilliseconds;
3604
+ }
3605
+ const date = new Date(time);
3606
+ date.setUTCMinutes(0, 0, 0);
3607
+ if (interval !== "hour") date.setUTCHours(0);
3608
+ if (interval === "week")
3609
+ date.setUTCDate(date.getUTCDate() - (date.getUTCDay() + 6) % 7);
3610
+ if (interval === "month" || interval === "year") date.setUTCDate(1);
3611
+ if (interval === "year") date.setUTCMonth(0);
3612
+ return date.getTime();
3613
+ }
3614
+ function metricDateNext(time, interval, timeZone = "UTC") {
3615
+ if (timeZone !== "UTC") {
3616
+ const zoned = Temporal.Instant.fromEpochMilliseconds(time).toZonedDateTimeISO(timeZone);
3617
+ if (interval === "hour") {
3618
+ const target = zoned.toPlainDateTime().with({
3619
+ minute: 0,
3620
+ second: 0,
3621
+ millisecond: 0,
3622
+ microsecond: 0,
3623
+ nanosecond: 0
3624
+ }).add({ hours: 1 });
3625
+ let next = target.toZonedDateTime(timeZone).epochMilliseconds;
3626
+ if (next <= time)
3627
+ next = target.toZonedDateTime(timeZone, {
3628
+ disambiguation: "later"
3629
+ }).epochMilliseconds;
3630
+ return metricOffsetTransition(time, next, timeZone) ?? next;
3631
+ }
3632
+ let date2 = zoned.toPlainDate();
3633
+ if (interval === "month") date2 = date2.with({ day: 1 }).add({ months: 1 });
3634
+ else if (interval === "year")
3635
+ date2 = date2.with({ month: 1, day: 1 }).add({ years: 1 });
3636
+ else if (interval === "week")
3637
+ date2 = date2.subtract({ days: date2.dayOfWeek - 1 }).add({ weeks: 1 });
3638
+ else date2 = date2.add({ days: 1 });
3639
+ return date2.toZonedDateTime(timeZone).epochMilliseconds;
3640
+ }
3641
+ const date = new Date(time);
3642
+ if (interval === "hour") date.setUTCHours(date.getUTCHours() + 1);
3643
+ else if (interval === "month") date.setUTCMonth(date.getUTCMonth() + 1);
3644
+ else if (interval === "year") date.setUTCFullYear(date.getUTCFullYear() + 1);
3645
+ else date.setUTCDate(date.getUTCDate() + (interval === "week" ? 7 : 1));
3646
+ return date.getTime();
3647
+ }
3648
+ function customEndTime(value, timeZone = "UTC") {
3649
+ const time = metricDateTime(value, timeZone);
3650
+ return value.length === 10 ? metricDateNext(time, "day", timeZone) - 1 : time;
3651
+ }
3652
+ function validateDateGrouping(config) {
3653
+ 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))
3654
+ throw Error("Invalid date grouping definition.");
3655
+ if (config.timeZone !== void 0) {
3656
+ if (typeof config.timeZone !== "string" || !config.timeZone || config.timeZone.length > 80 || /^[+-]/.test(config.timeZone))
3657
+ throw Error(
3658
+ "Use a valid IANA grouping timezone, such as America/Denver or UTC."
3659
+ );
3660
+ try {
3661
+ new Intl.DateTimeFormat("en-US", { timeZone: config.timeZone });
3662
+ } catch {
3663
+ throw Error(
3664
+ "Use a valid IANA grouping timezone, such as America/Denver or UTC."
3665
+ );
3666
+ }
3667
+ }
3668
+ if (config.includeEmpty && config.range === "custom") {
3669
+ const start = metricDateTime(config.start ?? "", config.timeZone), end = customEndTime(config.end ?? "", config.timeZone);
3670
+ if (end < start)
3671
+ throw Error("The end date must be on or after the start date.");
3672
+ }
3673
+ }
3674
+ function metricDateRange(config, query, observed, now, canonical) {
3675
+ let min = Infinity, max = -Infinity;
3676
+ for (const time of observed) {
3677
+ min = Math.min(min, time);
3678
+ max = Math.max(max, time);
3679
+ }
3680
+ if (config.range === "observed")
3681
+ return observed.length ? [min, max] : void 0;
3682
+ if (config.range === "custom") {
3683
+ const start2 = metricDateFloor(
3684
+ metricDateTime(config.start ?? "", config.timeZone),
3685
+ config.interval,
3686
+ config.timeZone
3687
+ );
3688
+ const end2 = metricDateFloor(
3689
+ customEndTime(config.end ?? "", config.timeZone),
3690
+ config.interval,
3691
+ config.timeZone
3692
+ );
3693
+ if (observed.length && (start2 > min || end2 < max))
3694
+ throw Error(
3695
+ "Custom date bounds must include all observed buckets. Narrow the query filters to exclude data."
3696
+ );
3697
+ return [start2, end2];
3698
+ }
3699
+ let start = -Infinity, end = Infinity;
3700
+ for (const term of query.where) {
3701
+ if (isOrGroup(term) || term.negated || canonical(term.field) !== canonical(config.field))
3702
+ continue;
3703
+ if (!["=", ">", ">=", "<", "<="].includes(term.op)) continue;
3704
+ const time = Number(coerceValue("datetime", term.value, now));
3705
+ if (term.op === ">" || term.op === ">=" || term.op === "=")
3706
+ start = Math.max(start, time + (term.op === ">" ? 1 : 0));
3707
+ if (term.op === "<" || term.op === "<=" || term.op === "=")
3708
+ end = Math.min(end, time - (term.op === "<" ? 1 : 0));
3709
+ }
3710
+ if (!Number.isFinite(start) || !Number.isFinite(end))
3711
+ throw Error(
3712
+ "Include empty dates needs a bounded date filter. Choose observed dates or a custom range."
3713
+ );
3714
+ if (end < start) return void 0;
3715
+ return [
3716
+ metricDateFloor(start, config.interval, config.timeZone),
3717
+ metricDateFloor(end, config.interval, config.timeZone)
3718
+ ];
3719
+ }
3720
+
2289
3721
  // src/metrics.ts
2290
3722
  var aggregates = /* @__PURE__ */ new Set([
2291
3723
  "COUNT",
@@ -2293,8 +3725,29 @@ var aggregates = /* @__PURE__ */ new Set([
2293
3725
  "SUM",
2294
3726
  "AVG",
2295
3727
  "MIN",
2296
- "MAX"
3728
+ "MAX",
3729
+ "MEDIAN"
2297
3730
  ]);
3731
+ var METRIC_FUNCTIONS = [
3732
+ ["COUNT", "Count rows, or non-empty values."],
3733
+ ["COUNT_DISTINCT", "Count distinct non-empty values."],
3734
+ ["SUM", "Total numeric values in each group."],
3735
+ ["AVG", "Average numeric values in each group."],
3736
+ ["MIN", "Smallest non-empty value in each group."],
3737
+ ["MAX", "Largest non-empty value in each group."],
3738
+ [
3739
+ "MEDIAN",
3740
+ "Middle numeric value; averages the middle pair. Ignores empty values."
3741
+ ]
3742
+ ].map(([name, description]) => ({
3743
+ name,
3744
+ description,
3745
+ signature: `${name}(row expression${name === "COUNT" ? "?" : ""})`,
3746
+ min: name === "COUNT" ? 0 : 1,
3747
+ max: 1,
3748
+ result: name === "MIN" || name === "MAX" ? "branch" : "number",
3749
+ args: ["SUM", "AVG", "MEDIAN"].includes(name) ? ["number"] : "any"
3750
+ }));
2298
3751
  function compileMetric(expression, schema, resolve) {
2299
3752
  const fields = Array.isArray(schema) ? schema : schema.fields;
2300
3753
  const nodes = [];
@@ -2336,7 +3789,7 @@ function compileMetric(expression, schema, resolve) {
2336
3789
  n.from,
2337
3790
  n.to
2338
3791
  );
2339
- if (!isMeasurable(field) || (field.aggregate?.ops !== void 0 || node.args[0] === n) && !aggOpsForField(field).includes(node.name.toLowerCase()))
3792
+ if (!isMeasurable(field) || (field.aggregate?.ops !== void 0 || node.args[0] === n && node.name !== "MEDIAN") && !aggOpsForField(field).includes(node.name.toLowerCase()))
2340
3793
  throw new FormulaError(
2341
3794
  `Field ${n.name} does not allow ${node.name}.`,
2342
3795
  n.from,
@@ -2353,7 +3806,7 @@ function compileMetric(expression, schema, resolve) {
2353
3806
  }
2354
3807
  };
2355
3808
  node.args.forEach(visit);
2356
- if (["SUM", "AVG"].includes(node.name) && !["number", "null"].includes(type))
3809
+ if (["SUM", "AVG", "MEDIAN"].includes(node.name) && !["number", "null"].includes(type))
2357
3810
  throw new FormulaError(
2358
3811
  `${node.name} requires numeric input.`,
2359
3812
  node.from,
@@ -2367,7 +3820,9 @@ function compileMetric(expression, schema, resolve) {
2367
3820
  );
2368
3821
  nodes.push(node);
2369
3822
  aggregateTypes.push(type);
2370
- return ["COUNT", "COUNT_DISTINCT", "SUM", "AVG"].includes(node.name) ? "number" : type;
3823
+ return ["COUNT", "COUNT_DISTINCT", "SUM", "AVG", "MEDIAN"].includes(
3824
+ node.name
3825
+ ) ? "number" : type;
2371
3826
  }
2372
3827
  );
2373
3828
  const groupCheck = (node) => {
@@ -2410,11 +3865,11 @@ function metricAggregatePlans(plan) {
2410
3865
  };
2411
3866
  });
2412
3867
  }
2413
- function distinctAccumulator(options) {
3868
+ function distinctAccumulator(options2) {
2414
3869
  const values = /* @__PURE__ */ new Set();
2415
3870
  let bytes = 0;
2416
- const cardinality = options.maxDistinctValues ?? 1e5;
2417
- const byteLimit = options.maxDistinctBytes ?? 16e6;
3871
+ const cardinality = options2.maxDistinctValues ?? 1e5;
3872
+ const byteLimit = options2.maxDistinctBytes ?? 16e6;
2418
3873
  if (!Number.isSafeInteger(cardinality) || cardinality < 0 || !Number.isSafeInteger(byteLimit) || byteLimit < 0)
2419
3874
  throw Error("Invalid distinct budget.");
2420
3875
  return {
@@ -2431,13 +3886,13 @@ function distinctAccumulator(options) {
2431
3886
  result: () => ({ value: values.size })
2432
3887
  };
2433
3888
  }
2434
- function reduceMetricValues(node, results, options = {}) {
2435
- return reduceResults(node, results, options);
3889
+ function reduceMetricValues(node, results, options2 = {}) {
3890
+ return reduceResults(node, results, options2);
2436
3891
  }
2437
- function reduceResults(node, results, options) {
3892
+ function reduceResults(node, results, options2) {
2438
3893
  try {
2439
3894
  if (!aggregates.has(node.name)) throw Error("Expected an aggregate node.");
2440
- const distinct = node.name === "COUNT_DISTINCT" ? distinctAccumulator(options) : void 0;
3895
+ const distinct = node.name === "COUNT_DISTINCT" ? distinctAccumulator(options2) : void 0;
2441
3896
  const values = [];
2442
3897
  let count = 0, nonnull = 0, error;
2443
3898
  for (const r of results) {
@@ -2458,6 +3913,17 @@ function reduceResults(node, results, options) {
2458
3913
  if (node.name === "COUNT")
2459
3914
  return { value: node.args.length ? nonnull : count };
2460
3915
  if (!values.length) return { value: null };
3916
+ if (node.name === "MEDIAN") {
3917
+ const sorted = values.map((value2) => {
3918
+ if (typeof value2 !== "number" || !Number.isFinite(value2))
3919
+ throw Error("Expected a finite number.");
3920
+ return value2;
3921
+ }).sort((a, b) => a - b);
3922
+ const mid = Math.floor(sorted.length / 2);
3923
+ const value = sorted.length % 2 ? sorted[mid] : sorted[mid - 1] / 2 + sorted[mid] / 2;
3924
+ assertMetricNumber(value);
3925
+ return { value };
3926
+ }
2461
3927
  if (node.name === "SUM" || node.name === "AVG") {
2462
3928
  let sum = 0;
2463
3929
  for (const v of values) {
@@ -2559,6 +4025,18 @@ function validateMetric(c, schema, resolve) {
2559
4025
  if (!f2 || !isGroupable(f2))
2560
4026
  throw new FormulaError(`Field ${key} cannot group metrics.`);
2561
4027
  }
4028
+ if (c.dateGrouping) {
4029
+ validateDateGrouping(c.dateGrouping);
4030
+ const field = resolveFieldName(schema, c.dateGrouping.field) ?? c.dateGrouping.field;
4031
+ if (field !== (resolveFieldName(schema, c.groupBy[0] ?? "") ?? c.groupBy[0]) || schema.fields.find((f2) => f2.name === field)?.type !== "datetime")
4032
+ throw new FormulaError(
4033
+ "Date grouping must use the first grouping field, with a datetime type."
4034
+ );
4035
+ if (distributionActive(c))
4036
+ throw new FormulaError(
4037
+ "Date grouping currently supports scalar metrics, not distributions."
4038
+ );
4039
+ }
2562
4040
  if (c.groupLimit !== void 0 && (!Number.isInteger(c.groupLimit) || c.groupLimit < 1 || c.groupLimit > 1e4))
2563
4041
  throw new FormulaError("Group limit must be 1\u201310,000.");
2564
4042
  for (const s of c.sort ?? []) {
@@ -2603,6 +4081,7 @@ function metricComputationKey(clauses) {
2603
4081
  expressionY: c.display?.kind === "scatter" ? c.expressionY : void 0,
2604
4082
  scope: c.scope ?? "allMatching",
2605
4083
  groupBy: c.groupBy,
4084
+ dateGrouping: c.dateGrouping,
2606
4085
  sort: c.sort,
2607
4086
  groupLimit: c.groupLimit,
2608
4087
  distribution: distributionActive(c) ? c.distribution : void 0
@@ -2648,6 +4127,7 @@ function toMetricQuery(q, schema, execution) {
2648
4127
  distribution,
2649
4128
  expression,
2650
4129
  expressionY,
4130
+ dateGrouping,
2651
4131
  ...rest
2652
4132
  } = c;
2653
4133
  return {
@@ -2667,6 +4147,15 @@ function toMetricQuery(q, schema, execution) {
2667
4147
  code: "invalid_metric"
2668
4148
  }))
2669
4149
  );
4150
+ for (const c of q.aggregations ?? [])
4151
+ if (c.dateGrouping)
4152
+ diagnostics.push({
4153
+ metricId: c.id,
4154
+ code: "unsupported_date_grouping",
4155
+ from: 0,
4156
+ to: 1,
4157
+ message: "Date grouping currently requires local metric execution."
4158
+ });
2670
4159
  try {
2671
4160
  assertMetricQueryState(raw, originalSchema);
2672
4161
  } catch (e) {
@@ -2774,24 +4263,23 @@ function assertMetricQueryState(q, schema) {
2774
4263
  if (queryStateKey(q.where) !== queryStateKey(normalized.where) || queryStateKey(orderBy) !== queryStateKey(normalizedOrder) || q.limit !== normalized.limit || q.offset !== normalized.offset)
2775
4264
  throw Error("Malformed metric query state cannot be executed exactly.");
2776
4265
  }
2777
- function selectMetricShownRows(rows, q, schema, options = {}) {
2778
- if (options.signal?.aborted) throw Error("Metric evaluation cancelled.");
4266
+ function selectMetricShownRows(rows, q, schema, options2 = {}) {
4267
+ if (options2.signal?.aborted) throw Error("Metric evaluation cancelled.");
2779
4268
  assertMetricQueryState(q, schema);
2780
- if (options.shownRows) return options.shownRows;
4269
+ if (options2.shownRows) return options2.shownRows;
2781
4270
  const orderBy = q.orderBy.length ? q.orderBy : schema.defaultSort ?? [];
2782
4271
  assertMetricPopulation(q, schema, orderBy);
2783
- if (rows.length > (options.maxRows ?? 1e6))
4272
+ if (rows.length > (options2.maxRows ?? 1e6))
2784
4273
  throw Error("Metric row budget exceeded.");
2785
- if ((options.maxOperations ?? 1e7) <= 0)
4274
+ if ((options2.maxOperations ?? 1e7) <= 0)
2786
4275
  throw Error("Metric operation budget exceeded.");
2787
4276
  return applyQuery(
2788
4277
  rows,
2789
- {
2790
- ...q,
2791
- orderBy: orderBy.some((s) => s.field === schema.idField) ? orderBy : [...orderBy, { field: schema.idField, dir: "asc" }]
2792
- },
4278
+ // Local tables use a stable sort: ties retain input order. An extra ID
4279
+ // sort here would choose a different page than the one the table shows.
4280
+ { ...q, orderBy },
2793
4281
  schema,
2794
- options
4282
+ options2
2795
4283
  ).rows;
2796
4284
  }
2797
4285
  function assertMetricPopulation(q, schema, orderBy) {
@@ -2806,12 +4294,12 @@ function assertMetricPopulation(q, schema, orderBy) {
2806
4294
  );
2807
4295
  if (q.where.some(
2808
4296
  (t) => predicatesOf(t).some(
2809
- (p) => isComputedField(p.field) || !fields.has(resolveFieldName(schema, p.field) ?? p.field)
4297
+ (p) => isComputedField(p.field) && !computedInputAllowed(schema, p.field, "filter") || !fields.has(resolveFieldName(schema, p.field) ?? p.field)
2810
4298
  )
2811
4299
  ))
2812
4300
  throw Error("Metric filter references an unavailable field.");
2813
4301
  if (orderBy.some(
2814
- (s) => isComputedField(s.field) || !fields.has(resolveFieldName(schema, s.field) ?? s.field)
4302
+ (s) => isComputedField(s.field) && !computedInputAllowed(schema, s.field, "sort") || !fields.has(resolveFieldName(schema, s.field) ?? s.field)
2815
4303
  ))
2816
4304
  throw Error("Shown-row ordering requires an authoritative committed page.");
2817
4305
  }
@@ -2854,39 +4342,53 @@ function compareMetricValues(a, b) {
2854
4342
  }
2855
4343
  return a < b ? -1 : 1;
2856
4344
  }
2857
- function evaluateMetrics(rows, q, schema, options = {}) {
2858
- const now = evaluationTime(options), fields = indexFields(schema);
4345
+ function evaluateMetrics(rows, q, schema, options2 = {}) {
4346
+ if (q.grouping) {
4347
+ try {
4348
+ if (options2.signal?.aborted) throw new Error("Metric evaluation cancelled.");
4349
+ if (rows.length > (options2.maxRows ?? 1e6)) throw new Error("Metric row budget exceeded.");
4350
+ const now2 = evaluationTime(options2);
4351
+ const prepared = prepareGrouping(rows, schema, q.grouping, { now: now2 });
4352
+ return evaluateMetrics(prepared.rows, withoutGrouping(q), prepared.schema, {
4353
+ ...options2,
4354
+ now: now2
4355
+ });
4356
+ } catch (error) {
4357
+ return { metrics: (q.aggregations ?? []).map((metric) => ({ id: metric.id, buckets: [], error: error instanceof Error ? error.message : String(error) })) };
4358
+ }
4359
+ }
4360
+ const now = evaluationTime(options2), fields = indexFields(schema);
2859
4361
  let matching;
2860
4362
  let shown;
2861
4363
  return {
2862
4364
  metrics: (q.aggregations ?? []).map((c) => {
2863
- let budget = options.maxOperations ?? 1e7;
4365
+ let budget = options2.maxOperations ?? 1e7;
2864
4366
  const tick = () => {
2865
4367
  if (--budget < 0) throw Error("Metric operation budget exceeded.");
2866
- if (options.signal?.aborted)
4368
+ if (options2.signal?.aborted)
2867
4369
  throw Error("Metric evaluation cancelled.");
2868
4370
  };
2869
4371
  try {
2870
- if (options.signal?.aborted)
4372
+ if (options2.signal?.aborted)
2871
4373
  throw Error("Metric evaluation cancelled.");
2872
4374
  assertMetricQueryState(q, schema);
2873
4375
  if ((q.aggregations?.length ?? 0) > 20)
2874
4376
  throw Error("At most 20 metrics are allowed.");
2875
- const diagnostics = validateMetric(c, schema, options.resolveComputed);
4377
+ const diagnostics = validateMetric(c, schema, options2.resolveComputed);
2876
4378
  if (diagnostics.length)
2877
4379
  throw Error(diagnostics.map((d) => d.message).join(" "));
2878
- const ps = metricPlans(c, schema, options.resolveComputed);
2879
- if (ps.some(hasRegex) && !options.evaluateInput)
4380
+ const ps = metricPlans(c, schema, options2.resolveComputed);
4381
+ if (ps.some(hasRegex) && !options2.evaluateInput)
2880
4382
  throw Error(
2881
4383
  "Regex metrics require an isolated worker with a watchdog."
2882
4384
  );
2883
4385
  let inputRows;
2884
4386
  if (c.scope === "shownRows")
2885
- inputRows = shown ?? (shown = selectMetricShownRows(rows, q, schema, options));
4387
+ inputRows = shown ?? (shown = selectMetricShownRows(rows, q, schema, options2));
2886
4388
  else {
2887
4389
  if (!matching) {
2888
4390
  assertMetricPopulation(q, schema, []);
2889
- if (rows.length > (options.maxRows ?? 1e6))
4391
+ if (rows.length > (options2.maxRows ?? 1e6))
2890
4392
  throw Error("Metric row budget exceeded.");
2891
4393
  matching = applyQuery(
2892
4394
  rows,
@@ -2897,10 +4399,10 @@ function evaluateMetrics(rows, q, schema, options = {}) {
2897
4399
  }
2898
4400
  inputRows = matching;
2899
4401
  }
2900
- if (inputRows.length > (options.maxRows ?? 1e6))
4402
+ if (inputRows.length > (options2.maxRows ?? 1e6))
2901
4403
  throw Error("Metric row budget exceeded.");
2902
4404
  const groups = /* @__PURE__ */ new Map();
2903
- const keyLimit = options.maxGroupKeyLength ?? 65536, byteLimit = options.maxGroupBytes ?? 16e6;
4405
+ const keyLimit = options2.maxGroupKeyLength ?? 65536, byteLimit = options2.maxGroupBytes ?? 16e6;
2904
4406
  if (![keyLimit, byteLimit].every((v) => Number.isSafeInteger(v) && v >= 0))
2905
4407
  throw Error("Invalid grouping budget.");
2906
4408
  let groupBytes = 0;
@@ -2915,6 +4417,8 @@ function evaluateMetrics(rows, q, schema, options = {}) {
2915
4417
  groupBytes = 256;
2916
4418
  groups.set("[]", { keys: [], rows: [] });
2917
4419
  }
4420
+ let dateStart = Infinity, dateEnd = -Infinity, dateKey = "";
4421
+ let dateInput, dateTime = NaN;
2918
4422
  for (const row of inputRows) {
2919
4423
  tick();
2920
4424
  if (!c.groupBy.length) {
@@ -2931,6 +4435,27 @@ function evaluateMetrics(rows, q, schema, options = {}) {
2931
4435
  )
2932
4436
  )
2933
4437
  );
4438
+ if (c.dateGrouping && keys[0] !== null) {
4439
+ const config = c.dateGrouping;
4440
+ if (keys[0] !== dateInput) {
4441
+ dateTime = metricDateTime(keys[0], config.timeZone);
4442
+ dateInput = keys[0];
4443
+ }
4444
+ if (dateTime < dateStart || dateTime >= dateEnd) {
4445
+ dateStart = metricDateFloor(
4446
+ dateTime,
4447
+ config.interval,
4448
+ config.timeZone
4449
+ );
4450
+ dateEnd = metricDateNext(
4451
+ dateStart,
4452
+ config.interval,
4453
+ config.timeZone
4454
+ );
4455
+ dateKey = new Date(dateStart).toISOString();
4456
+ }
4457
+ keys[0] = dateKey;
4458
+ }
2934
4459
  let keyLength = 1, payloadBytes = 0;
2935
4460
  for (const value of keys) {
2936
4461
  keyLength += 1 + (typeof value === "string" ? groupStringLength(value, keyLimit - keyLength - 1) : value === null ? 4 : typeof value === "boolean" ? value ? 4 : 5 : String(value).length);
@@ -2943,7 +4468,7 @@ function evaluateMetrics(rows, q, schema, options = {}) {
2943
4468
  const key = JSON.stringify(keys);
2944
4469
  let g = groups.get(key);
2945
4470
  if (!g) {
2946
- if (groups.size >= (options.maxGroups ?? 1e4))
4471
+ if (groups.size >= (options2.maxGroups ?? 1e4))
2947
4472
  throw Error("Metric group budget exceeded.");
2948
4473
  g = { keys, rows: [] };
2949
4474
  groups.set(key, g);
@@ -2952,6 +4477,68 @@ function evaluateMetrics(rows, q, schema, options = {}) {
2952
4477
  groupBytes += 16;
2953
4478
  g.rows.push(row);
2954
4479
  }
4480
+ if (c.dateGrouping?.includeEmpty) {
4481
+ const config = c.dateGrouping;
4482
+ const observed = [];
4483
+ const series = /* @__PURE__ */ new Map();
4484
+ for (const group of groups.values())
4485
+ if (group.keys[0] !== null) {
4486
+ tick();
4487
+ reserve(8);
4488
+ groupBytes += 8;
4489
+ observed.push(metricDateTime(group.keys[0]));
4490
+ const tuple = group.keys.slice(1);
4491
+ const key = JSON.stringify(tuple);
4492
+ if (!series.has(key)) {
4493
+ const bytes = 128 + 16 * tuple.length + 2 * key.length;
4494
+ reserve(bytes);
4495
+ groupBytes += bytes;
4496
+ series.set(key, tuple);
4497
+ }
4498
+ }
4499
+ if (c.groupBy.length === 1 && !series.size) {
4500
+ reserve(132);
4501
+ groupBytes += 132;
4502
+ series.set("[]", []);
4503
+ }
4504
+ const range = metricDateRange(
4505
+ config,
4506
+ q,
4507
+ observed,
4508
+ now,
4509
+ (name) => resolveFieldName(schema, name) ?? name
4510
+ );
4511
+ if (range)
4512
+ for (let time = range[0]; time <= range[1]; ) {
4513
+ tick();
4514
+ for (const tuple of series.values()) {
4515
+ tick();
4516
+ const keys = [
4517
+ new Date(time).toISOString(),
4518
+ ...tuple
4519
+ ];
4520
+ const key = JSON.stringify(keys);
4521
+ if (key.length > keyLimit)
4522
+ throw Error("Metric grouping key length budget exceeded.");
4523
+ if (!groups.has(key)) {
4524
+ if (groups.size >= (options2.maxGroups ?? 1e4))
4525
+ throw Error("Metric group budget exceeded.");
4526
+ const bytes = 256 + 16 * keys.length + 4 * key.length;
4527
+ reserve(bytes);
4528
+ groupBytes += bytes;
4529
+ groups.set(key, { keys, rows: [] });
4530
+ }
4531
+ }
4532
+ const next = metricDateNext(
4533
+ time,
4534
+ config.interval,
4535
+ config.timeZone
4536
+ );
4537
+ if (!Number.isFinite(next) || next <= time)
4538
+ throw Error("Date grouping exceeds calendar limits.");
4539
+ time = next;
4540
+ }
4541
+ }
2955
4542
  const inputs = (row, p) => Object.fromEntries(
2956
4543
  p.dependencies.map((name) => [
2957
4544
  name,
@@ -2972,7 +4559,13 @@ function evaluateMetrics(rows, q, schema, options = {}) {
2972
4559
  const rowEval = (node, row, p) => {
2973
4560
  budget -= cost(node);
2974
4561
  tick();
2975
- return options.evaluateInput && hasRegex({ ...p, ast: node }) ? options.evaluateInput(node, row, p) : formulaRuntime(node, inputs(row, p));
4562
+ if (node.kind === "field") {
4563
+ const value = readFieldValue(fields.get(node.name), row);
4564
+ return formulaRuntime(node, {
4565
+ [node.name]: value instanceof Date ? value.toISOString() : value ?? null
4566
+ });
4567
+ }
4568
+ return options2.evaluateInput && hasRegex({ ...p, ast: node }) ? options2.evaluateInput(node, row, p) : formulaRuntime(node, inputs(row, p));
2976
4569
  };
2977
4570
  const reduce = (p, rs, inputErrors) => {
2978
4571
  const cache = /* @__PURE__ */ new Map();
@@ -2990,7 +4583,7 @@ function evaluateMetrics(rows, q, schema, options = {}) {
2990
4583
  yield v;
2991
4584
  }
2992
4585
  }
2993
- result = reduceResults(node, results(), options);
4586
+ result = reduceResults(node, results(), options2);
2994
4587
  } catch (e) {
2995
4588
  result = {
2996
4589
  value: null,
@@ -3023,6 +4616,12 @@ function evaluateMetrics(rows, q, schema, options = {}) {
3023
4616
  }
3024
4617
  populations.set(bucket, values);
3025
4618
  } else {
4619
+ if (c.dateGrouping && !g.rows.length) {
4620
+ const empty2 = (p) => p.ast.kind === "call" && ["COUNT", "COUNT_DISTINCT", "SUM"].includes(p.ast.name) ? 0 : null;
4621
+ bucket.value = empty2(ps[0]);
4622
+ if (ps[1]) bucket.y = empty2(ps[1]);
4623
+ return bucket;
4624
+ }
3026
4625
  const inputErrors = /* @__PURE__ */ new Set();
3027
4626
  const x = reduce(ps[0], g.rows, inputErrors);
3028
4627
  bucket.value = scalar(x.value);
@@ -3064,7 +4663,7 @@ function evaluateMetrics(rows, q, schema, options = {}) {
3064
4663
  }
3065
4664
  }
3066
4665
  }
3067
- const ordering = c.sort?.length ? c.sort : [{ key: "value", dir: "desc", nulls: "last" }];
4666
+ const ordering = c.sort?.length ? c.sort : c.dateGrouping ? [{ key: "group0", dir: "asc", nulls: "last" }] : [{ key: "value", dir: "desc", nulls: "last" }];
3068
4667
  const target = (b, s) => s.key === "samples" ? b.count - (b.nullCount ?? 0) - (b.inputErrorCount ?? 0) : s.key.startsWith("group") ? b.keys[Number(s.key.slice(5))] ?? null : s.key === "count" ? b.count : s.key === "y" ? b.y ?? null : b.value;
3069
4668
  buckets.sort((a, b) => {
3070
4669
  if (!!(a.error || a.yError) !== !!(b.error || b.yError))
@@ -3119,13 +4718,17 @@ function evaluateMetrics(rows, q, schema, options = {}) {
3119
4718
  }
3120
4719
 
3121
4720
  // src/apply.ts
3122
- function applyQuery(rows, q, schema, options = {}) {
3123
- const now = evaluationTime(options);
4721
+ function applyQuery(rows, q, schema, options2 = {}) {
4722
+ const now = evaluationTime(options2);
4723
+ if (q.grouping) {
4724
+ const grouped = prepareGrouping(rows, schema, q.grouping, { now });
4725
+ return applyQuery(grouped.rows, withoutGrouping(q), grouped.schema, { now });
4726
+ }
3124
4727
  const byName = indexFields(schema);
3125
4728
  const resolveField2 = (name) => resolveFieldName(schema, name) ?? name;
3126
- const whereTerms = q.where.filter((term) => !predicatesOf(term).some((c) => isComputedField(c.field))).map((term) => prepareWhereTerm(term, resolveField2, now));
4729
+ const whereTerms = q.where.filter((term) => !predicatesOf(term).some((c) => isComputedField(c.field) && !computedInputAllowed(schema, c.field, "filter"))).map((term) => prepareWhereTerm(term, resolveField2, now));
3127
4730
  prepareDatetimePredicates(whereTerms, byName);
3128
- const orderBy = q.orderBy.filter((term) => !isComputedField(term.field)).map((term) => ({ ...term, field: resolveField2(term.field) }));
4731
+ const orderBy = q.orderBy.filter((term) => !isComputedField(term.field) || computedInputAllowed(schema, term.field, "sort")).map((term) => ({ ...term, field: resolveField2(term.field) }));
3129
4732
  let out = rows.filter(
3130
4733
  (row) => whereTerms.every((term) => matchesTerm(byName, row, term))
3131
4734
  );
@@ -3159,42 +4762,47 @@ function applyQuery(rows, q, schema, options = {}) {
3159
4762
  const end = q.limit > 0 ? start + q.limit : out.length;
3160
4763
  return { rows: out.slice(start, end), total };
3161
4764
  }
3162
- function matchesClause(row, clause, schema, options = {}) {
4765
+ function computedInputAllowed(schema, name, operation) {
4766
+ const field = schema.fields.find((field2) => field2.name === name);
4767
+ return !!field && (field.source.kind === "backend" && field[operation]?.enabled !== false || field[operation]?.enabled === true);
4768
+ }
4769
+ function matchesClause(row, clause, schema, options2 = {}) {
3163
4770
  const field = resolveFieldName(schema, clause.field) ?? clause.field;
3164
4771
  const byName = indexFields(schema);
3165
4772
  const prepared = prepareWhereClause(
3166
4773
  { ...clause, field },
3167
- evaluationTime(options)
4774
+ evaluationTime(options2)
3168
4775
  );
3169
4776
  prepareDatetimePredicates([{ kind: "lit", predicate: prepared }], byName);
3170
4777
  return matchesWith(byName, row, prepared);
3171
4778
  }
3172
- function applyAggregations(rows, q, schema, options = {}) {
3173
- if (options.signal?.aborted || q.where.some(
4779
+ function applyAggregations(rows, q, schema, options2 = {}) {
4780
+ if (q.grouping) return evaluateMetrics(rows, q, schema, options2);
4781
+ if (options2.signal?.aborted || q.where.some(
3174
4782
  (t) => predicatesOf(t).some(
3175
4783
  (p) => p.op === "matches_regex" || p.op === "not_matches_regex"
3176
4784
  )
3177
4785
  ))
3178
- return evaluateMetrics(rows, q, schema, options);
4786
+ return evaluateMetrics(rows, q, schema, options2);
3179
4787
  const advanced = (q.aggregations ?? []).filter(
3180
- (a) => a.diagnostics?.length || a.expression !== void 0 || a.expressionY !== void 0 || a.scope !== void 0 || a.distribution || a.sort || a.groupLimit !== void 0
4788
+ (a) => a.dateGrouping || a.diagnostics?.length || a.expression !== void 0 || a.expressionY !== void 0 || a.scope !== void 0 || a.distribution || a.sort || a.groupLimit !== void 0
3181
4789
  );
3182
4790
  if (advanced.length) {
3183
4791
  if ((q.aggregations?.length ?? 0) > 20 || normalizeQueryState(q).aggregations?.length !== q.aggregations?.length)
3184
- return evaluateMetrics(rows, q, schema, options);
4792
+ return evaluateMetrics(rows, q, schema, options2);
3185
4793
  const legacy = (q.aggregations ?? []).filter((a) => !advanced.includes(a));
3186
4794
  const results = [
3187
4795
  ...evaluateMetrics(
3188
4796
  rows,
3189
4797
  { ...q, aggregations: advanced },
3190
4798
  schema,
3191
- options
4799
+ options2
3192
4800
  ).metrics,
3193
4801
  ...legacy.length ? applyAggregations(
3194
4802
  rows,
3195
4803
  { ...q, aggregations: legacy },
3196
4804
  schema,
3197
- options
4805
+ options2
3198
4806
  ).metrics : []
3199
4807
  ];
3200
4808
  const byId = new Map(results.map((result) => [result.id, result]));
@@ -3202,7 +4810,7 @@ function applyAggregations(rows, q, schema, options = {}) {
3202
4810
  metrics: (q.aggregations ?? []).flatMap((a) => byId.get(a.id) ?? [])
3203
4811
  };
3204
4812
  }
3205
- const now = evaluationTime(options);
4813
+ const now = evaluationTime(options2);
3206
4814
  const byName = indexFields(schema);
3207
4815
  const resolveField2 = (name) => resolveFieldName(schema, name) ?? name;
3208
4816
  const whereTerms = q.where.filter((term) => !predicatesOf(term).some((c) => isComputedField(c.field))).map((term) => prepareWhereTerm(term, resolveField2, now));
@@ -3491,7 +5099,13 @@ function normalizeSavedQuery(value) {
3491
5099
  if (typeof raw.id !== "string" || !raw.id || raw.id.length > 256 || typeof raw.name !== "string" || !raw.name || raw.name.length > MAX_SAVED_NAME_LENGTH || typeof raw.savedAt !== "number" || !Number.isFinite(raw.savedAt) || raw.query == null || typeof raw.query !== "object") {
3492
5100
  return null;
3493
5101
  }
3494
- return { id: raw.id, name: raw.name, savedAt: raw.savedAt, query: normalizeQueryState(raw.query) };
5102
+ return {
5103
+ id: raw.id,
5104
+ name: raw.name,
5105
+ savedAt: raw.savedAt,
5106
+ query: normalizeQueryState(raw.query),
5107
+ ...typeof raw.snapshotCode === "string" ? { snapshotCode: raw.snapshotCode } : {}
5108
+ };
3495
5109
  }
3496
5110
  function normalizeSavedQueries(value) {
3497
5111
  if (!Array.isArray(value)) return [];
@@ -3524,7 +5138,7 @@ function memoryStorageAdapter() {
3524
5138
  if (!(saved.get(key) ?? []).some((item) => item.id === id)) throw new Error(`Saved query "${id}" does not exist.`);
3525
5139
  defaults.set(key, id);
3526
5140
  },
3527
- async saveNamed(key, name, query, savedAt) {
5141
+ async saveNamed(key, name, query, savedAt, snapshotCode) {
3528
5142
  const normalizedName = name.trim();
3529
5143
  if (!normalizedName || normalizedName.length > MAX_SAVED_NAME_LENGTH) {
3530
5144
  throw new Error(`Saved query name must be between 1 and ${MAX_SAVED_NAME_LENGTH} characters.`);
@@ -3533,6 +5147,7 @@ function memoryStorageAdapter() {
3533
5147
  if (items.length >= MAX_SAVED_QUERIES) throw new Error(`At most ${MAX_SAVED_QUERIES} saved queries are allowed.`);
3534
5148
  if (items.some((item2) => item2.name === normalizedName)) throw new Error(`Saved query "${normalizedName}" already exists.`);
3535
5149
  const item = {
5150
+ ...snapshotCode ? { snapshotCode } : {},
3536
5151
  id: `${savedAt}-${Math.round((savedAt * 9301 + 49297) % 233280)}`,
3537
5152
  name: normalizedName,
3538
5153
  savedAt,
@@ -3541,6 +5156,20 @@ function memoryStorageAdapter() {
3541
5156
  saved.set(key, [...items, item]);
3542
5157
  return item;
3543
5158
  },
5159
+ async updateNamed(key, id, query, savedAt, snapshotCode) {
5160
+ const items = saved.get(key) ?? [];
5161
+ const original = items.find((item2) => item2.id === id);
5162
+ if (!original) throw new Error(`Saved query "${id}" does not exist.`);
5163
+ const item = {
5164
+ id,
5165
+ name: original.name,
5166
+ savedAt,
5167
+ query: normalizeQueryState(query),
5168
+ ...snapshotCode ? { snapshotCode } : {}
5169
+ };
5170
+ saved.set(key, items.map((entry) => entry.id === id ? item : entry));
5171
+ return item;
5172
+ },
3544
5173
  async deleteSaved(key, id) {
3545
5174
  saved.set(key, (saved.get(key) ?? []).filter((item) => item.id !== id));
3546
5175
  if (defaults.get(key) === id) defaults.delete(key);
@@ -3605,7 +5234,7 @@ function localStorageAdapter() {
3605
5234
  }
3606
5235
  writeDefaultId(key, id);
3607
5236
  },
3608
- async saveNamed(key, name, query, savedAt) {
5237
+ async saveNamed(key, name, query, savedAt, snapshotCode) {
3609
5238
  const normalizedName = name.trim();
3610
5239
  if (!normalizedName || normalizedName.length > MAX_SAVED_NAME_LENGTH) {
3611
5240
  throw new Error(`Saved query name must be between 1 and ${MAX_SAVED_NAME_LENGTH} characters.`);
@@ -3616,6 +5245,7 @@ function localStorageAdapter() {
3616
5245
  throw new Error(`Saved query "${normalizedName}" already exists.`);
3617
5246
  }
3618
5247
  const item = {
5248
+ ...snapshotCode ? { snapshotCode } : {},
3619
5249
  id: `${savedAt}-${Math.round((savedAt * 9301 + 49297) % 233280)}`,
3620
5250
  name: normalizedName,
3621
5251
  savedAt,
@@ -3625,6 +5255,20 @@ function localStorageAdapter() {
3625
5255
  writeSaved(key, items);
3626
5256
  return item;
3627
5257
  },
5258
+ async updateNamed(key, id, query, savedAt, snapshotCode) {
5259
+ const items = readSaved(key);
5260
+ const original = items.find((item2) => item2.id === id);
5261
+ if (!original) throw new Error(`Saved query "${id}" does not exist.`);
5262
+ const item = {
5263
+ id,
5264
+ name: original.name,
5265
+ savedAt,
5266
+ query: normalizeQueryState(query),
5267
+ ...snapshotCode ? { snapshotCode } : {}
5268
+ };
5269
+ writeSaved(key, items.map((entry) => entry.id === id ? item : entry));
5270
+ return item;
5271
+ },
3628
5272
  async deleteSaved(key, id) {
3629
5273
  writeSaved(
3630
5274
  key,
@@ -3634,6 +5278,248 @@ function localStorageAdapter() {
3634
5278
  }
3635
5279
  };
3636
5280
  }
5281
+
5282
+ // src/propertyStorage.ts
5283
+ var empty = () => ({ last: null, saved: [], defaultId: null });
5284
+ var clone = (saved) => ({
5285
+ ...saved,
5286
+ view: normalizePropertyView(saved.view)
5287
+ });
5288
+ function jsonPropertyViewStorage(read, write) {
5289
+ const keyFor = (type) => `query-table:properties:${type}`;
5290
+ function load(type) {
5291
+ const value = read(keyFor(type));
5292
+ if (value === null) return empty();
5293
+ const raw = JSON.parse(value);
5294
+ if (!raw || !Array.isArray(raw.saved) || raw.saved.length > 100 || !(raw.defaultId === null || typeof raw.defaultId === "string"))
5295
+ throw new Error("Invalid saved property views.");
5296
+ const saved = raw.saved.map((item) => {
5297
+ if (!item || typeof item.id !== "string" || !item.id || typeof item.name !== "string" || !item.name.trim() || item.name.length > 200 || !Number.isFinite(item.savedAt))
5298
+ throw new Error("Invalid saved property view.");
5299
+ return clone(item);
5300
+ });
5301
+ return {
5302
+ last: raw.last === null ? null : normalizePropertyView(raw.last),
5303
+ saved,
5304
+ defaultId: raw.defaultId
5305
+ };
5306
+ }
5307
+ const store = (type, value) => write(keyFor(type), JSON.stringify(value));
5308
+ return {
5309
+ async loadLast(type) {
5310
+ return load(type).last;
5311
+ },
5312
+ async saveLast(type, view) {
5313
+ const data = load(type);
5314
+ data.last = normalizePropertyView(view);
5315
+ store(type, data);
5316
+ },
5317
+ async listSaved(type) {
5318
+ return load(type).saved;
5319
+ },
5320
+ async saveNamed(type, name, view, savedAt) {
5321
+ name = name.trim();
5322
+ if (!name || name.length > 200 || !Number.isFinite(savedAt))
5323
+ throw new Error(
5324
+ "A view name (1\u2013200 characters) and valid save time are required."
5325
+ );
5326
+ const data = load(type);
5327
+ if (data.saved.some((v) => v.name === name))
5328
+ throw new Error(`A property view named \u201C${name}\u201D already exists.`);
5329
+ if (data.saved.length >= 100)
5330
+ throw new Error("At most 100 saved property views are allowed.");
5331
+ let suffix = 0;
5332
+ while (data.saved.some((v) => v.id === `${savedAt}-${suffix}`)) suffix++;
5333
+ const saved = {
5334
+ id: `${savedAt}-${suffix}`,
5335
+ name,
5336
+ savedAt,
5337
+ view: normalizePropertyView(view)
5338
+ };
5339
+ data.saved.push(saved);
5340
+ store(type, data);
5341
+ return clone(saved);
5342
+ },
5343
+ async deleteSaved(type, id) {
5344
+ const data = load(type);
5345
+ data.saved = data.saved.filter((v) => v.id !== id);
5346
+ if (data.defaultId === id) data.defaultId = null;
5347
+ store(type, data);
5348
+ },
5349
+ async loadDefaultSaved(type) {
5350
+ const data = load(type);
5351
+ return data.saved.find((v) => v.id === data.defaultId) ?? null;
5352
+ },
5353
+ async setDefaultSaved(type, id) {
5354
+ const data = load(type);
5355
+ if (id !== null && !data.saved.some((v) => v.id === id))
5356
+ throw new Error("Unknown property view.");
5357
+ data.defaultId = id;
5358
+ store(type, data);
5359
+ }
5360
+ };
5361
+ }
5362
+ function memoryPropertyViewStorage() {
5363
+ const values = /* @__PURE__ */ new Map();
5364
+ return jsonPropertyViewStorage(
5365
+ (key) => values.get(key) ?? null,
5366
+ (key, value) => {
5367
+ values.set(key, value);
5368
+ }
5369
+ );
5370
+ }
5371
+ function localPropertyViewStorage() {
5372
+ return jsonPropertyViewStorage(
5373
+ (key) => localStorage.getItem(key),
5374
+ (key, value) => localStorage.setItem(key, value)
5375
+ );
5376
+ }
5377
+
5378
+ // src/source.ts
5379
+ function sourceDependencies(query, schema, resolveComputed) {
5380
+ const fields = /* @__PURE__ */ new Set([schema.idField]);
5381
+ const addSort = (name) => {
5382
+ fields.add(name);
5383
+ const sortField = schema.fields.find((field) => field.name === name)?.sort?.field;
5384
+ if (sortField) fields.add(sortField);
5385
+ };
5386
+ const source = query.grouping?.source ?? query;
5387
+ for (const term of source.where) for (const predicate of predicatesOf(term)) fields.add(predicate.field);
5388
+ if (query.grouping) {
5389
+ const model = createGroupingModel(schema);
5390
+ model.validate(query.grouping);
5391
+ for (const key of query.grouping.keys) fields.add(key);
5392
+ for (const summary of query.grouping.summaries) {
5393
+ if (summary.op === "row") fields.add(summary.field);
5394
+ else for (const dependency of compileMetric(model.summaryExpression(summary), schema, resolveComputed).dependencies) fields.add(dependency);
5395
+ }
5396
+ for (const term of query.grouping.representative?.orderBy ?? []) addSort(term.field);
5397
+ } else {
5398
+ for (const column of source.select) if (column.field !== ITEM_DETAILS_COLUMN) fields.add(column.field);
5399
+ for (const term of source.orderBy) addSort(term.field);
5400
+ for (const metric of source.aggregations ?? []) for (const dependency of metricDependencies(metric, schema, resolveComputed)) fields.add(dependency);
5401
+ }
5402
+ const base = /* @__PURE__ */ new Set();
5403
+ const visiting = /* @__PURE__ */ new Set();
5404
+ const visit = (name) => {
5405
+ if (visiting.has(name)) throw Error("Source dependency cycle.");
5406
+ const field = schema.fields.find((field2) => field2.name === name);
5407
+ if (!field && name === schema.idField) {
5408
+ base.add(name);
5409
+ return;
5410
+ }
5411
+ const dependencies = field?.source.kind === "derived" ? field.source.dependencies : resolveComputed?.(name)?.dependencies;
5412
+ if (!field && !dependencies) throw Error(`Unavailable source dependency: ${name}.`);
5413
+ if (dependencies?.length) {
5414
+ visiting.add(name);
5415
+ for (const dependency of dependencies) visit(dependency);
5416
+ visiting.delete(name);
5417
+ } else base.add(name);
5418
+ };
5419
+ for (const name of fields) visit(name);
5420
+ const paths = Object.fromEntries([...base].map((name) => {
5421
+ const field = schema.fields.find((field2) => field2.name === name);
5422
+ return [name, field?.source.kind === "backend" ? field.source.path ?? name : name];
5423
+ }));
5424
+ return { fields: [...fields].sort(), baseFields: [...base].sort(), identity: schema.idField, paths };
5425
+ }
5426
+
5427
+ // src/queryV3.ts
5428
+ function sourceExecutionSchema(schema, execution, catalogue = []) {
5429
+ const fields = new Map(schema.fields.map((field) => [field.name, field]));
5430
+ for (const [name, capability] of Object.entries(execution?.fields ?? {})) {
5431
+ const definition = catalogue.find((definition2) => name === `@computed/${definition2.id}`);
5432
+ const revision = execution?.resolvedRevisions[name] ?? execution?.resolvedRevisions[name.slice(10)];
5433
+ const current = !!revision && (!definition || definition.revision === revision);
5434
+ fields.set(name, {
5435
+ ...fields.get(name),
5436
+ name,
5437
+ label: definition?.label ?? fields.get(name)?.label ?? name,
5438
+ type: capability.type,
5439
+ source: { kind: "backend" },
5440
+ select: { enabled: current && capability.select },
5441
+ filter: { enabled: current && capability.filter === true },
5442
+ sort: { enabled: current && capability.sort },
5443
+ aggregate: { measure: current && capability.measure, groupable: current && capability.group }
5444
+ });
5445
+ }
5446
+ return { ...schema, fields: [...fields.values()] };
5447
+ }
5448
+ function toQueryRequestV3(query, baseSchema, capabilities, execution, options2 = {}) {
5449
+ const now = evaluationTime(options2), q = normalizeQueryState(query);
5450
+ const sourceSchema = sourceExecutionSchema(baseSchema, execution);
5451
+ const source = q.grouping?.source ?? withoutGrouping(q);
5452
+ const diagnostics = [];
5453
+ if ((query.aggregations?.length ?? 0) !== (q.aggregations?.length ?? 0)) diagnostics.push("The query exceeds its metric budget or contains malformed metric state.");
5454
+ if (capabilities.version !== 3 || execution && execution.profile !== capabilities.profile) diagnostics.push("The negotiated query profile is unavailable.");
5455
+ let resultSchema = sourceSchema;
5456
+ if (q.grouping) {
5457
+ if (!capabilities.groupedRows) diagnostics.push("This server does not support grouped rows.");
5458
+ try {
5459
+ const model = createGroupingModel(sourceSchema);
5460
+ model.validate(q.grouping);
5461
+ resultSchema = model.resultSchema(q.grouping.keys, q.grouping.summaries);
5462
+ } catch (error) {
5463
+ diagnostics.push(error instanceof Error ? error.message : String(error));
5464
+ }
5465
+ }
5466
+ const sourceRequest = toServerQuery(withoutGrouping(source), sourceSchema);
5467
+ for (const term of source.where) for (const predicate of predicatesOf(term)) if (["matches_regex", "not_matches_regex"].includes(predicate.op) && !capabilities.regexSourceFilters)
5468
+ diagnostics.push("This server profile does not support isolated regex source filtering.");
5469
+ const result = toServerQuery(withoutGrouping(q), resultSchema);
5470
+ if (sourceRequest.where.length !== source.where.length || result.where.length !== q.where.length) diagnostics.push("The query contains unsupported source or result predicates.");
5471
+ if (result.orderBy.length !== q.orderBy.length) diagnostics.push("The query contains unsupported result ordering.");
5472
+ if (q.grouping) {
5473
+ for (const term of q.grouping.source.where) for (const predicate of predicatesOf(term))
5474
+ if (predicate.field.startsWith("@computed/") && !capabilities.computedSourceFilters) diagnostics.push("This server does not support computed source predicates.");
5475
+ }
5476
+ const metrics = toMetricQuery(withoutGrouping(q), resultSchema, execution).metrics.map((metric) => {
5477
+ const dateGrouping = q.aggregations?.find((clause) => clause.id === metric.id)?.dateGrouping;
5478
+ return dateGrouping ? { ...metric, dateGrouping } : metric;
5479
+ });
5480
+ for (const metric of metrics) {
5481
+ diagnostics.push(...validateMetric(metric, resultSchema).map((issue) => issue.message));
5482
+ if (metric.dateGrouping && !capabilities.calendarGrouping) diagnostics.push("This server does not support calendar grouping.");
5483
+ }
5484
+ const revisions = /* @__PURE__ */ Object.create(null);
5485
+ for (const [id, revision] of Object.entries(execution?.resolvedRevisions ?? {})) {
5486
+ if (!revision || id.length > 256 || revision.length > 256) diagnostics.push("Invalid computed revision envelope.");
5487
+ else revisions[id.startsWith("@computed/") ? id.slice(10) : id] = revision;
5488
+ }
5489
+ return {
5490
+ version: 3,
5491
+ profile: capabilities.profile,
5492
+ now,
5493
+ expectedRevisions: revisions,
5494
+ ...execution?.planToken ? { planToken: execution.planToken } : {},
5495
+ ...execution?.snapshot !== void 0 ? { snapshot: execution.snapshot } : {},
5496
+ source: sourceRequest,
5497
+ result,
5498
+ ...q.grouping ? { grouping: {
5499
+ keys: q.grouping.keys,
5500
+ summaries: q.grouping.summaries,
5501
+ representative: q.grouping.representative,
5502
+ ...q.grouping.diagnostics ? { diagnostics: q.grouping.diagnostics } : {}
5503
+ } } : {},
5504
+ rows: options2.rows !== false,
5505
+ metrics,
5506
+ diagnostics: [...new Set(diagnostics)]
5507
+ };
5508
+ }
5509
+ function validateQueryResultV3(result, request, schema) {
5510
+ if (!result || result.version !== 3 || result.now !== request.now || !Number.isSafeInteger(result.total) || result.total < 0 || !Number.isSafeInteger(result.sourceTotal) || result.sourceTotal < 0 || !Array.isArray(result.rows) || !Array.isArray(result.metrics) || result.execution?.profile !== request.profile || (result.execution.planToken ?? "") !== (request.planToken ?? "") || (result.execution.snapshot ?? "") !== (request.snapshot ?? "") || result.schema?.idField !== schema.idField)
5511
+ throw Error("Invalid or mismatched query execution response.");
5512
+ const expected = new Map(schema.fields.map((field) => [field.name, field.type]));
5513
+ if (!Array.isArray(result.schema.fields) || result.schema.fields.some((field) => expected.get(field.name) !== field.type) || request.rows && request.result.select.some((name) => !result.schema.fields.some((field) => field.name === name))) throw Error("The server returned a different projected schema.");
5514
+ for (const [id, revision] of Object.entries(request.expectedRevisions)) if (result.execution.resolvedRevisions[id] !== revision)
5515
+ throw Error("Computed definitions changed. Refresh the server execution handshake.");
5516
+ const seen = /* @__PURE__ */ new Set();
5517
+ for (const row of result.rows) {
5518
+ const id = row[schema.idField];
5519
+ if (typeof id !== "string" && typeof id !== "number" || seen.has(id)) throw Error("The server returned invalid or duplicate group identities.");
5520
+ seen.add(id);
5521
+ }
5522
+ }
3637
5523
  export {
3638
5524
  AGG_OPS_BY_TYPE,
3639
5525
  AGG_OPS_NEEDING_FIELD,
@@ -3641,17 +5527,29 @@ export {
3641
5527
  EMPTY_QUERY,
3642
5528
  FORMULA_FUNCTIONS,
3643
5529
  FormulaError,
5530
+ GROUP_ID_FIELD,
5531
+ ITEM_DETAILS_COLUMN,
3644
5532
  MAX_AGGREGATIONS,
3645
5533
  MAX_GROUP_BY_FIELDS,
3646
5534
  MAX_ORDER_BY_TERMS,
5535
+ MAX_PROPERTIES,
3647
5536
  MAX_QUERY_LIMIT,
3648
5537
  MAX_QUERY_OFFSET,
5538
+ MAX_QUERY_STATE_BYTES,
3649
5539
  MAX_QUERY_TOKEN_LENGTH,
3650
5540
  MAX_RELATIVE_TIME_MS,
5541
+ MAX_REPRESENTATIVE_SORTS,
5542
+ MAX_RESULT_GROUPS,
3651
5543
  MAX_SELECT_COLUMNS,
5544
+ MAX_SUMMARY_COLUMNS,
3652
5545
  MAX_WHERE_CLAUSES,
5546
+ METRIC_DATE_INTERVALS,
5547
+ METRIC_FUNCTIONS,
3653
5548
  NULLARY_OPS,
3654
5549
  OPS_BY_TYPE,
5550
+ OP_LABEL,
5551
+ QueryDecodeError,
5552
+ QuerySnapshotHttpError,
3655
5553
  aggOpAllowedForType,
3656
5554
  aggOpNeedsField,
3657
5555
  aggOpsForField,
@@ -3663,8 +5561,13 @@ export {
3663
5561
  compileFormula,
3664
5562
  compileMetric,
3665
5563
  computedFieldName,
5564
+ createGroupingModel,
5565
+ createPropertyView,
5566
+ createQueryLinkService,
3666
5567
  createSetFilter,
3667
5568
  decodeQuery,
5569
+ decodeQueryStrict,
5570
+ discoverFieldRelationships,
3668
5571
  encodeQuery,
3669
5572
  evaluateMetricReductions,
3670
5573
  evaluateMetrics,
@@ -3675,6 +5578,7 @@ export {
3675
5578
  histogramCounts,
3676
5579
  histogramEdges,
3677
5580
  httpComputedColumnStore,
5581
+ httpQuerySnapshotStore,
3678
5582
  indexFields,
3679
5583
  isComputedCellError,
3680
5584
  isComputedField,
@@ -3686,38 +5590,62 @@ export {
3686
5590
  isPushdownFilter,
3687
5591
  isSelectable,
3688
5592
  isSortable,
5593
+ itemPageFields,
5594
+ jsonPropertyViewStorage,
3689
5595
  loadSchema,
5596
+ localPropertyViewStorage,
5597
+ localQuerySnapshotCache,
3690
5598
  localStorageAdapter,
3691
5599
  mapSetFilterValues,
3692
5600
  matchesClause,
3693
5601
  memoryComputedColumnStore,
5602
+ memoryPropertyViewStorage,
5603
+ memoryQuerySnapshotCache,
3694
5604
  memoryStorageAdapter,
3695
5605
  metricAggregatePlans,
3696
5606
  metricComputationKey,
5607
+ metricDateFloor,
5608
+ metricDateNext,
5609
+ metricDateRange,
5610
+ metricDateTime,
3697
5611
  metricDependencies,
3698
5612
  metricExpression,
3699
5613
  metricPlans,
3700
5614
  negateClause,
5615
+ normalizePropertyFormat,
5616
+ normalizePropertyView,
3701
5617
  normalizeQueryState,
5618
+ objectReferenceKey,
3702
5619
  opAllowedForType,
3703
5620
  opPairsForField,
3704
5621
  opsForField,
3705
5622
  parseRelativeDuration,
3706
5623
  percentile,
3707
5624
  predicatesOf,
5625
+ prepareGrouping,
3708
5626
  queriesEqual,
3709
5627
  readFieldValue,
5628
+ readObjectReference,
3710
5629
  readSetFilter,
3711
5630
  reduceMetricValues,
5631
+ renderItemPageTemplate,
3712
5632
  resolveFieldName,
3713
5633
  selectMetricShownRows,
3714
5634
  selectedFields,
5635
+ sourceDependencies,
5636
+ sourceExecutionSchema,
3715
5637
  stripSetFilter,
3716
5638
  toAggregationQuery,
3717
5639
  toMetricQuery,
5640
+ toQueryRequestV3,
3718
5641
  toServerQuery,
3719
5642
  toServerQueryV2,
3720
5643
  validateComputedColumn,
3721
- validateMetric
5644
+ validateDateGrouping,
5645
+ validateItemPageTemplate,
5646
+ validateMetric,
5647
+ validateQueryResultV3,
5648
+ validateQuerySnapshot,
5649
+ withoutGrouping
3722
5650
  };
3723
5651
  //# sourceMappingURL=index.js.map