@pythia-software/query-table-react 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,3 +1,47 @@
1
+ // src/computedExecution.ts
2
+ import {
3
+ isComputedField,
4
+ readFieldValue
5
+ } from "@pythia-software/query-table-core";
6
+ function executionRevision(execution, id) {
7
+ return execution?.resolvedRevisions[id] ?? execution?.resolvedRevisions[`@computed/${id}`];
8
+ }
9
+ function validateComputedResponse(request, result, schema) {
10
+ const execution = result.execution;
11
+ if (result.version !== 2 || !execution || execution.profile !== request.profile || request.planToken && execution.planToken !== request.planToken || request.snapshot !== void 0 && execution.snapshot !== request.snapshot)
12
+ throw Error(
13
+ "Server computed execution identity changed. Refresh the handshake."
14
+ );
15
+ for (const [name, revision] of Object.entries(request.expectedRevisions)) {
16
+ const id = name.replace(/^@computed\//, "");
17
+ if (executionRevision(execution, id) !== revision)
18
+ throw Error(
19
+ "Server computed definition revision changed. Refresh the handshake."
20
+ );
21
+ }
22
+ const idField = schema.fields.find((f) => f.name === schema.idField);
23
+ if (!idField)
24
+ throw Error("Stable row identity is required for computed values.");
25
+ const ids = new Set(result.rows.map((row) => readFieldValue(idField, row)));
26
+ if (ids.has(null) || ids.has(void 0) || ids.size !== result.rows.length)
27
+ throw Error(
28
+ "Computed response contains missing or duplicate row identities."
29
+ );
30
+ const sidecars = new Map(
31
+ result.computed.map((item) => [item.id, item.values])
32
+ );
33
+ if (sidecars.size !== result.computed.length || [...sidecars.keys()].some((id) => !ids.has(id)))
34
+ throw Error("Computed sidecars do not match returned row identities.");
35
+ for (const field of request.select.filter(isComputedField)) {
36
+ const id = field.slice("@computed/".length);
37
+ for (const rowId of ids) {
38
+ const value = sidecars.get(rowId)?.[id];
39
+ if (!value || value.value !== null && !["string", "number", "boolean"].includes(typeof value.value) || typeof value.value === "number" && !Number.isFinite(value.value))
40
+ throw Error(`Missing or invalid server computed result: ${field}.`);
41
+ }
42
+ }
43
+ }
44
+
1
45
  // src/useComputedColumns.ts
2
46
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
3
47
  import {
@@ -5,12 +49,13 @@ import {
5
49
  compileFormula,
6
50
  computedFieldName,
7
51
  groupPreview,
8
- isComputedField,
52
+ isComputedField as isComputedField2,
9
53
  memoryComputedColumnStore,
10
- readFieldValue,
54
+ readFieldValue as readFieldValue2,
11
55
  toServerQuery,
12
56
  validateComputedColumn,
13
- isSelectable
57
+ isSelectable,
58
+ hasRegex
14
59
  } from "@pythia-software/query-table-core";
15
60
 
16
61
  // src/formulaWorker.ts
@@ -51,9 +96,9 @@ function evaluateFormulaRows(ast, inputs, signal, factory = createFormulaWorker)
51
96
  const cleanup = () => {
52
97
  clearTimeout(timer);
53
98
  worker.terminate();
54
- signal?.removeEventListener("abort", abort);
99
+ signal?.removeEventListener("abort", abort2);
55
100
  };
56
- const abort = () => {
101
+ const abort2 = () => {
57
102
  cleanup();
58
103
  reject(new DOMException("Aborted", "AbortError"));
59
104
  };
@@ -82,7 +127,7 @@ function evaluateFormulaRows(ast, inputs, signal, factory = createFormulaWorker)
82
127
  reject(e);
83
128
  }
84
129
  };
85
- signal?.addEventListener("abort", abort, { once: true });
130
+ signal?.addEventListener("abort", abort2, { once: true });
86
131
  worker.onerror = () => {
87
132
  cleanup();
88
133
  reject(new Error("Formula worker failed."));
@@ -164,7 +209,7 @@ function useComputedColumns(options) {
164
209
  const stack = new Set(editingId ? [editingId] : []);
165
210
  let expanded = 0;
166
211
  const resolve = (name) => {
167
- if (!isComputedField(name)) return void 0;
212
+ if (!isComputedField2(name)) return void 0;
168
213
  const id = name.slice("@computed/".length), def = definitions.find((d) => d.id === id);
169
214
  if (!def) throw new Error(`Computed column unavailable: ${id}`);
170
215
  if (stack.has(id))
@@ -222,7 +267,7 @@ function useComputedColumns(options) {
222
267
  const values = /* @__PURE__ */ Object.create(null);
223
268
  for (const name of plan.dependencies) {
224
269
  const field = inputFields.find((f) => f.name === name);
225
- const raw = readFieldValue(field, row);
270
+ const raw = readFieldValue2(field, row);
226
271
  values[name] = raw instanceof Date ? raw.toISOString() : raw;
227
272
  }
228
273
  size += JSON.stringify(values).length;
@@ -236,12 +281,13 @@ function useComputedColumns(options) {
236
281
  [inputFields]
237
282
  );
238
283
  const activeSelect = query.select.length ? query.select : schema.defaultSelect ?? [];
239
- const activeIds = activeSelect.filter((c) => isComputedField(c.field)).map((c) => c.field.slice("@computed/".length)).sort().join(",");
284
+ const activeIds = activeSelect.filter((c) => isComputedField2(c.field)).map((c) => c.field.slice("@computed/".length)).sort().join(",");
240
285
  const [evaluated, setEvaluated] = useState(null);
241
286
  useEffect(() => {
242
287
  const ac = new AbortController();
243
288
  const values = /* @__PURE__ */ new Map();
244
289
  void (async () => {
290
+ if (options.execution && transport?.fetchRowsV2) return;
245
291
  for (const id of activeIds.split(",").filter(Boolean)) {
246
292
  const entry = plans.get(id);
247
293
  if (!entry?.plan) continue;
@@ -269,7 +315,15 @@ function useComputedColumns(options) {
269
315
  if (!ac.signal.aborted) setEvaluated({ rows, plans, values });
270
316
  })();
271
317
  return () => ac.abort();
272
- }, [activeIds, rows, plans, inputsFor, workerFactory]);
318
+ }, [
319
+ activeIds,
320
+ rows,
321
+ plans,
322
+ inputsFor,
323
+ workerFactory,
324
+ options.execution,
325
+ transport
326
+ ]);
273
327
  const displaySchema = useMemo(() => {
274
328
  const indices = new Map(rows.map((row, i) => [row, i]));
275
329
  const fields = definitions.map((def) => {
@@ -285,17 +339,34 @@ function useComputedColumns(options) {
285
339
  dependencies: entry?.plan ? backendDependencies(entry.plan) : [],
286
340
  accessor: (row) => {
287
341
  if (entry?.error) return { computedError: entry.error };
342
+ if (options.execution && transport?.fetchRowsV2) {
343
+ if (options.serverExecution?.profile !== options.execution.profile || options.serverExecution?.planToken !== options.execution.planToken || options.serverExecution?.snapshot !== options.execution.snapshot || executionRevision(options.execution, def.id) !== def.revision || executionRevision(options.serverExecution, def.id) !== def.revision)
344
+ return {
345
+ computedError: "Definition changed or values are pending. Refresh the server handshake."
346
+ };
347
+ const idField = schema.fields.find(
348
+ (f) => f.name === schema.idField
349
+ );
350
+ const id = idField ? readFieldValue2(idField, row) : null;
351
+ const server = id === null ? void 0 : options.serverComputed?.get(id)?.[def.id];
352
+ return server ? server.error ? { computedError: server.error.code } : server.value : { computedError: "Server computed result unavailable." };
353
+ }
288
354
  const result = evaluated?.rows === rows && evaluated.plans === plans ? evaluated.values.get(def.id)?.[indices.get(row) ?? -1] : void 0;
289
355
  return result?.error ? { computedError: result.error } : result ? result.value : { computedError: "Calculating\u2026" };
290
356
  }
291
357
  },
292
358
  filter: { enabled: false },
293
- sort: { enabled: false },
294
- aggregate: { measure: false, groupable: false }
359
+ sort: {
360
+ enabled: !!transport?.fetchRowsV2 && !!options.execution?.fields[computedFieldName(def.id)]?.sort && executionRevision(options.execution, def.id) === def.revision && !!entry?.plan && !hasRegex(entry.plan)
361
+ },
362
+ aggregate: {
363
+ measure: !!entry?.plan,
364
+ groupable: !!transport?.fetchMetrics && transport.metricCapabilities?.version === 2 && !!options.execution?.fields[computedFieldName(def.id)]?.group && executionRevision(options.execution, def.id) === def.revision && !!entry?.plan && !hasRegex(entry.plan)
365
+ }
295
366
  };
296
367
  });
297
368
  for (const col of activeSelect)
298
- if (isComputedField(col.field) && !fields.some((f) => f.name === col.field))
369
+ if (isComputedField2(col.field) && !fields.some((f) => f.name === col.field))
299
370
  fields.push({
300
371
  name: col.field,
301
372
  label: col.field.slice(10),
@@ -321,7 +392,11 @@ function useComputedColumns(options) {
321
392
  rows,
322
393
  query.select,
323
394
  schema.defaultSelect,
324
- loading
395
+ loading,
396
+ transport,
397
+ options.execution,
398
+ options.serverComputed,
399
+ options.serverExecution
325
400
  ]);
326
401
  const save = useCallback(
327
402
  async (column, expectedRevision) => {
@@ -422,85 +497,531 @@ function useComputedColumns(options) {
422
497
  }
423
498
 
424
499
  // src/useQueryTable.ts
425
- import { useCallback as useCallback6, useEffect as useEffect4, useMemo as useMemo6, useRef as useRef3, useState as useState6 } from "react";
500
+ import { useCallback as useCallback8, useEffect as useEffect5, useMemo as useMemo7, useRef as useRef5, useState as useState7 } from "react";
426
501
  import {
427
502
  EMPTY_QUERY,
428
503
  applyQuery as applyQuery2,
429
504
  decodeQuery,
430
505
  encodeQuery,
431
506
  memoryStorageAdapter,
432
- normalizeQueryState,
433
- readFieldValue as readFieldValue2,
507
+ normalizeQueryState as normalizeQueryState2,
508
+ readFieldValue as readFieldValue4,
434
509
  selectedFields as selectedFields2,
435
510
  toServerQuery as toServerQuery2,
436
511
  queriesEqual,
437
512
  isOrGroup,
438
513
  predicatesOf,
439
514
  negateClause,
440
- opsForField
515
+ opsForField,
516
+ metricPlans as metricPlans3,
517
+ validateMetric as validateMetric2,
518
+ metricDependencies,
519
+ isComputedField as isComputedField4,
520
+ toServerQueryV2
441
521
  } from "@pythia-software/query-table-core";
442
522
 
443
- // src/useAggregations.ts
444
- import { useEffect as useEffect2, useMemo as useMemo2, useState as useState2 } from "react";
445
- import { applyAggregations, toAggregationQuery } from "@pythia-software/query-table-core";
446
- var EMPTY_RESULT = { metrics: [] };
447
- function useAggregations(query, schema, transport, clientRows, debounceMs, nonce, validateQuery) {
448
- const [results, setResults] = useState2(null);
449
- const [loading, setLoading] = useState2(false);
450
- const [error, setError] = useState2(null);
451
- const key = useMemo2(
452
- () => JSON.stringify({ where: query.where, aggregations: query.aggregations ?? [] }),
453
- [query.where, query.aggregations]
454
- );
455
- const hasAggregations = (query.aggregations?.length ?? 0) > 0;
523
+ // src/useRequestActivity.ts
524
+ import { useCallback as useCallback2, useEffect as useEffect2, useMemo as useMemo2, useRef as useRef2, useState as useState2 } from "react";
525
+ var methods = /* @__PURE__ */ new Set([
526
+ "fetchRows",
527
+ "fetchRowsV2",
528
+ "fetchMetrics",
529
+ "fetchAggregations",
530
+ "fetchRow",
531
+ "fetchDistinctValues",
532
+ "fetchFieldStats"
533
+ ]);
534
+ function observeTransport(transport, publish, nextId) {
535
+ const wrappers = /* @__PURE__ */ new Map();
536
+ return new Proxy(Object.create(transport), {
537
+ get(_target, key) {
538
+ const method = Reflect.get(transport, key, transport);
539
+ if (!methods.has(String(key)) || typeof method !== "function")
540
+ return method;
541
+ if (!wrappers.has(key))
542
+ wrappers.set(key, async (...args) => {
543
+ const signal = args[1];
544
+ const start = performance.now();
545
+ const entry = {
546
+ id: nextId(),
547
+ kind: String(key).replace(/^fetch/, ""),
548
+ startedAt: Date.now(),
549
+ status: "pending"
550
+ };
551
+ publish(entry);
552
+ let done = false;
553
+ const finish = (status, error) => {
554
+ if (done) return;
555
+ done = true;
556
+ publish({
557
+ ...entry,
558
+ status,
559
+ durationMs: Math.max(0, performance.now() - start),
560
+ ...error !== void 0 ? {
561
+ error: error instanceof Error ? error.message : String(error)
562
+ } : {}
563
+ });
564
+ };
565
+ const abort2 = () => finish("aborted");
566
+ signal?.addEventListener("abort", abort2, { once: true });
567
+ if (signal?.aborted) abort2();
568
+ try {
569
+ const result = await method.apply(transport, args);
570
+ finish(signal?.aborted ? "aborted" : "success");
571
+ return result;
572
+ } catch (error) {
573
+ finish(
574
+ signal?.aborted || error instanceof Error && error.name === "AbortError" ? "aborted" : "error",
575
+ error
576
+ );
577
+ throw error;
578
+ } finally {
579
+ signal?.removeEventListener("abort", abort2);
580
+ }
581
+ });
582
+ return wrappers.get(key);
583
+ }
584
+ });
585
+ }
586
+ function useRequestActivity(source) {
587
+ const [entries, setEntries] = useState2([]);
588
+ const sequence = useRef2(0), mounted = useRef2(true);
456
589
  useEffect2(() => {
457
- if (!hasAggregations) {
590
+ mounted.current = true;
591
+ return () => {
592
+ mounted.current = false;
593
+ };
594
+ }, []);
595
+ const publish = useCallback2((entry) => {
596
+ if (mounted.current)
597
+ setEntries(
598
+ (previous) => [entry, ...previous.filter((e) => e.id !== entry.id)].sort((a, b) => b.id - a.id).slice(0, 100)
599
+ );
600
+ }, []);
601
+ const transport = useMemo2(
602
+ () => source ? observeTransport(source, publish, () => ++sequence.current) : void 0,
603
+ [source, publish]
604
+ );
605
+ const clear = useCallback2(
606
+ () => setEntries((previous) => previous.filter((e) => e.status === "pending")),
607
+ []
608
+ );
609
+ const activity = useMemo2(
610
+ () => ({ entries, clear }),
611
+ [entries, clear]
612
+ );
613
+ return { transport, activity };
614
+ }
615
+
616
+ // src/useAggregations.ts
617
+ import { useCallback as useCallback3, useEffect as useEffect3, useMemo as useMemo3, useRef as useRef3, useState as useState3 } from "react";
618
+ import {
619
+ metricComputationKey,
620
+ metricPlans as metricPlans2,
621
+ hasRegex as hasRegex3
622
+ } from "@pythia-software/query-table-core";
623
+
624
+ // src/metricExecution.ts
625
+ import {
626
+ applyAggregations,
627
+ normalizeQueryState,
628
+ evaluateMetrics,
629
+ metricPlans,
630
+ hasRegex as hasRegex2,
631
+ toAggregationQuery,
632
+ toMetricQuery,
633
+ validateMetric,
634
+ selectMetricShownRows,
635
+ readFieldValue as readFieldValue3,
636
+ resolveFieldName,
637
+ isComputedField as isComputedField3
638
+ } from "@pythia-software/query-table-core";
639
+ var abort = (signal) => {
640
+ if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
641
+ };
642
+ async function localMetrics(rows, query, context, signal) {
643
+ const originalClauses = query.aggregations ?? [];
644
+ const advanced = (query.aggregations ?? []).filter(
645
+ (c) => isComputedField3(c.field ?? "") || c.groupBy.some(isComputedField3) || c.diagnostics?.length || c.expression !== void 0 || c.expressionY !== void 0 || c.scope !== void 0 || c.distribution || c.sort || c.groupLimit !== void 0
646
+ );
647
+ if (advanced.length && (originalClauses.length > 20 || normalizeQueryState(query).aggregations?.length !== originalClauses.length))
648
+ return evaluateMetrics(rows, query, context.schema, {
649
+ ...signal ? { signal } : {},
650
+ ...context.resolveComputed ? { resolveComputed: context.resolveComputed } : {}
651
+ });
652
+ const legacy = (query.aggregations ?? []).filter(
653
+ (c) => !advanced.includes(c)
654
+ );
655
+ const compatible = applyAggregations(
656
+ rows,
657
+ { ...query, aggregations: legacy },
658
+ context.schema,
659
+ {
660
+ ...signal ? { signal } : {}
661
+ }
662
+ );
663
+ if (!advanced.length) return compatible;
664
+ query = { ...query, aggregations: advanced };
665
+ const options = {
666
+ ...context.resolveComputed ? { resolveComputed: context.resolveComputed } : {},
667
+ ...signal ? { signal } : {}
668
+ };
669
+ for (const clause of query.aggregations ?? []) {
670
+ const diagnostics = validateMetric(
671
+ clause,
672
+ context.schema,
673
+ context.resolveComputed
674
+ );
675
+ if (diagnostics.length)
676
+ throw Error(diagnostics.map((d) => d.message).join(" "));
677
+ }
678
+ const requiresPage = (query.aggregations ?? []).some(
679
+ (clause) => clause.scope === "shownRows"
680
+ );
681
+ const page = requiresPage ? selectMetricShownRows(rows, query, context.schema, {
682
+ ...options,
683
+ ...context.rowsReady && context.shownRows ? { shownRows: context.shownRows } : {}
684
+ }) : [];
685
+ const caches = /* @__PURE__ */ new Map();
686
+ for (const clause of query.aggregations ?? []) {
687
+ for (const plan of metricPlans(
688
+ clause,
689
+ context.schema,
690
+ context.resolveComputed
691
+ )) {
692
+ if (!hasRegex2(plan)) continue;
693
+ const nodes = "aggregates" in plan ? plan.aggregates.flatMap((a) => a.args) : [plan.ast];
694
+ for (const node of nodes) {
695
+ if (!hasRegex2({ ...plan, ast: node })) continue;
696
+ const key = JSON.stringify(node);
697
+ if (caches.has(key)) continue;
698
+ let bytes = 0;
699
+ const inputs = page.map((row) => {
700
+ const values2 = /* @__PURE__ */ Object.create(null);
701
+ for (const name of plan.dependencies) {
702
+ const field = context.schema.fields.find((f) => f.name === name);
703
+ if (!field) throw Error(`Unavailable metric input: ${name}`);
704
+ const raw = readFieldValue3(field, row);
705
+ values2[name] = raw instanceof Date ? raw.toISOString() : raw ?? null;
706
+ }
707
+ bytes += JSON.stringify(values2).length;
708
+ if (bytes > 16e6)
709
+ throw Error(
710
+ "Metric worker input exceeds 16 MB. Reduce the shown rows."
711
+ );
712
+ return values2;
713
+ });
714
+ const values = await evaluateFormulaRows(
715
+ node,
716
+ inputs,
717
+ signal,
718
+ context.workerFactory
719
+ );
720
+ caches.set(
721
+ key,
722
+ new Map(page.map((row, i) => [row, values[i]]))
723
+ );
724
+ }
725
+ }
726
+ }
727
+ abort(signal);
728
+ const typed = evaluateMetrics(rows, query, context.schema, {
729
+ ...options,
730
+ shownRows: page,
731
+ ...caches.size ? {
732
+ evaluateInput: (node, row, plan) => {
733
+ const cached = caches.get(JSON.stringify(node));
734
+ if (cached)
735
+ return cached.get(row) ?? {
736
+ value: null,
737
+ error: "Metric worker result missing."
738
+ };
739
+ throw Error("Metric regex input was not evaluated in a worker.");
740
+ }
741
+ } : {}
742
+ });
743
+ const byId = new Map(
744
+ [...compatible.metrics, ...typed.metrics].map((m) => [m.id, m])
745
+ );
746
+ return { metrics: originalClauses.flatMap((c) => byId.get(c.id) ?? []) };
747
+ }
748
+ async function executeMetrics(query, context, signal) {
749
+ abort(signal);
750
+ const clauses = query.aggregations ?? [];
751
+ if (!clauses.length) return { metrics: [] };
752
+ const supportsV2 = !!context.transport?.fetchMetrics && context.transport.metricCapabilities?.version === 2;
753
+ if (context.clientRows && !supportsV2 && !context.transport?.fetchAggregations)
754
+ return localMetrics(context.clientRows, query, context, signal);
755
+ const needsWorker = (c) => metricPlans(c, context.schema, context.resolveComputed).some(hasRegex2);
756
+ const shown = clauses.filter(
757
+ (c) => c.scope === "shownRows" && (!supportsV2 || needsWorker(c))
758
+ );
759
+ const matching = clauses.filter(
760
+ (c) => c.scope !== "shownRows" || supportsV2 && !needsWorker(c)
761
+ );
762
+ const results = [];
763
+ if (shown.length) {
764
+ if (!context.rowsReady || !context.shownRows)
765
+ throw Error(
766
+ "Wait for the current table page before previewing shown-row metrics."
767
+ );
768
+ const names = new Set(
769
+ shown.flatMap((clause) => [
770
+ ...clause.groupBy,
771
+ ...metricPlans(clause, context.schema, context.resolveComputed).flatMap(
772
+ (p) => p.dependencies
773
+ )
774
+ ])
775
+ );
776
+ for (const name of names) {
777
+ const canonical = resolveFieldName(context.schema, name) ?? name;
778
+ const field = context.schema.fields.find((f) => f.name === canonical);
779
+ const dependencies = field?.source.kind === "derived" ? field.source.dependencies ?? [] : [canonical];
780
+ const loaded = context.shownFields ? dependencies.every((dep) => context.shownFields.includes(dep)) : field && context.shownRows.every(
781
+ (row) => field.source.kind === "derived" || loadedPath(
782
+ row,
783
+ field.source.kind === "backend" ? field.source.path ?? field.name : field.name
784
+ )
785
+ );
786
+ if (!field || !loaded)
787
+ throw Error(
788
+ `Shown-page input ${name} is not loaded. Add it to the selected columns and refresh, or use a snapshot-capable metric transport.`
789
+ );
790
+ }
791
+ const pageQuery = {
792
+ ...query,
793
+ where: [],
794
+ orderBy: [],
795
+ limit: Math.max(1, context.shownRows.length),
796
+ offset: 0,
797
+ aggregations: shown
798
+ };
799
+ results.push(
800
+ ...(await localMetrics(context.shownRows, pageQuery, context, signal)).metrics
801
+ );
802
+ }
803
+ if (matching.length) {
804
+ const transport = context.transport;
805
+ const scoped = { ...query, aggregations: matching };
806
+ if (transport?.fetchMetrics && transport.metricCapabilities?.version === 2) {
807
+ const request = toMetricQuery(scoped, context.schema, context.execution);
808
+ if (request.diagnostics.length)
809
+ throw Error(request.diagnostics.map((d) => d.message).join(" "));
810
+ if (matching.some((c) => c.scope === "shownRows") && !transport.metricCapabilities.shownRows)
811
+ throw Error("This server does not support shown-row metrics.");
812
+ if (request.profile && transport.metricCapabilities.profile && request.profile !== transport.metricCapabilities.profile)
813
+ throw Error(
814
+ "Metric transport and computed execution profiles do not match. Refresh the handshake."
815
+ );
816
+ const remoteFields = new Map(
817
+ context.schema.fields.map((field) => [field.name, field])
818
+ );
819
+ for (const [name, capability] of Object.entries(
820
+ context.execution?.fields ?? {}
821
+ ))
822
+ if (isComputedField3(name))
823
+ remoteFields.set(name, {
824
+ name,
825
+ label: name,
826
+ type: capability.type,
827
+ source: { kind: "backend" }
828
+ });
829
+ const remoteSchema = {
830
+ ...context.schema,
831
+ fields: [...remoteFields.values()]
832
+ };
833
+ if (matching.some(
834
+ (clause) => [
835
+ ...clause.groupBy,
836
+ ...metricPlans(clause, remoteSchema).flatMap(
837
+ (plan) => plan.dependencies
838
+ )
839
+ ].some(isComputedField3)
840
+ ) && !transport.metricCapabilities.computedFields)
841
+ throw Error("This metric server does not support computed fields.");
842
+ if (request.metrics.some(
843
+ (c) => !c.distribution && (c.expression !== void 0 || c.expressionY !== void 0)
844
+ ) && !transport.metricCapabilities.expressions)
845
+ throw Error(
846
+ "This server does not support composed metric expressions."
847
+ );
848
+ if (matching.some(
849
+ (c) => c.distribution && (!c.display || ["box", "histogram"].includes(c.display.kind))
850
+ ) && !transport.metricCapabilities.distributions)
851
+ throw Error("This server does not support distributions.");
852
+ results.push(...(await transport.fetchMetrics(request, signal)).metrics);
853
+ } else if (transport?.fetchAggregations) {
854
+ const request = toAggregationQuery(scoped, context.schema);
855
+ if (request.diagnostics?.length)
856
+ throw Error(request.diagnostics.map((d) => d.message).join(" "));
857
+ results.push(
858
+ ...(await transport.fetchAggregations(request, signal)).metrics
859
+ );
860
+ } else
861
+ throw Error(
862
+ "All-matching metrics require a complete client dataset or a metric-capable server transport."
863
+ );
864
+ }
865
+ abort(signal);
866
+ const byId = new Map(results.map((r) => [r.id, r]));
867
+ return {
868
+ metrics: clauses.map(
869
+ (c) => byId.get(c.id) ?? {
870
+ id: c.id,
871
+ buckets: [],
872
+ error: "Server omitted this metric."
873
+ }
874
+ )
875
+ };
876
+ }
877
+ function loadedPath(row, path) {
878
+ if (!row || typeof row !== "object") return false;
879
+ if (path in row) return true;
880
+ let value = row;
881
+ for (const part of path.split(".")) {
882
+ if (value === null) return true;
883
+ if (!value || typeof value !== "object" || !(part in value)) return false;
884
+ value = value[part];
885
+ }
886
+ return true;
887
+ }
888
+
889
+ // src/useAggregations.ts
890
+ function useAggregations(query, schema, transport, clientRows, debounceMs, nonce, validateQuery, options = {}) {
891
+ const [results, setResults] = useState3(null);
892
+ const [loading, setLoading] = useState3(false);
893
+ const [error, setError] = useState3(null);
894
+ const latest = useRef3({
895
+ query,
896
+ schema,
897
+ transport,
898
+ clientRows,
899
+ options,
900
+ validateQuery
901
+ });
902
+ latest.current = {
903
+ query,
904
+ schema,
905
+ transport,
906
+ clientRows,
907
+ options,
908
+ validateQuery
909
+ };
910
+ const preview = useCallback3(
911
+ async (clauses, signal) => {
912
+ const current = latest.current;
913
+ const draft = { ...current.query, aggregations: clauses };
914
+ current.validateQuery?.(draft);
915
+ return executeMetrics(
916
+ draft,
917
+ {
918
+ schema: current.schema,
919
+ transport: current.transport,
920
+ clientRows: current.clientRows,
921
+ ...current.options
922
+ },
923
+ signal
924
+ );
925
+ },
926
+ []
927
+ );
928
+ const shown = (query.aggregations ?? []).some((c) => c.scope === "shownRows");
929
+ const localPage = shown && (!transport?.fetchMetrics || transport.metricCapabilities?.version !== 2 || (query.aggregations ?? []).some((clause) => {
930
+ if (clause.scope !== "shownRows") return false;
931
+ try {
932
+ return metricPlans2(clause, schema, options.resolveComputed).some(
933
+ hasRegex3
934
+ );
935
+ } catch {
936
+ return true;
937
+ }
938
+ }));
939
+ const key = useMemo3(
940
+ () => JSON.stringify([
941
+ query.where,
942
+ metricComputationKey(query.aggregations ?? []),
943
+ shown ? [query.orderBy, query.limit, query.offset] : null
944
+ ]),
945
+ [
946
+ query.where,
947
+ query.aggregations,
948
+ query.orderBy,
949
+ query.limit,
950
+ query.offset,
951
+ shown
952
+ ]
953
+ );
954
+ const dependencyKey = JSON.stringify([
955
+ options.revisions ?? {},
956
+ options.execution ?? null
957
+ ]);
958
+ useEffect3(() => {
959
+ if (!latest.current.query.aggregations?.length) {
458
960
  setResults(null);
459
- setError(null);
460
961
  setLoading(false);
962
+ setError(null);
461
963
  return;
462
964
  }
463
965
  const ac = new AbortController();
464
- let cancelled = false;
465
- const run = async () => {
466
- setLoading(true);
966
+ setLoading(true);
967
+ setResults(null);
968
+ if (localPage && transport && !options.rowsReady) {
969
+ try {
970
+ latest.current.validateQuery?.(latest.current.query);
971
+ setError(options.rowsError ?? null);
972
+ if (options.rowsError) setLoading(false);
973
+ } catch (e) {
974
+ setError(e instanceof Error ? e : Error(String(e)));
975
+ setLoading(false);
976
+ }
977
+ return;
978
+ }
979
+ const timer = setTimeout(() => {
467
980
  setError(null);
468
981
  try {
469
- validateQuery?.(query);
470
- let res;
471
- if (transport?.fetchAggregations) {
472
- res = await transport.fetchAggregations(toAggregationQuery(query, schema), ac.signal);
473
- } else if (clientRows) {
474
- res = applyAggregations(clientRows, query, schema);
475
- } else {
476
- res = EMPTY_RESULT;
477
- }
478
- if (!cancelled) setResults(res);
982
+ latest.current.validateQuery?.(latest.current.query);
479
983
  } catch (e) {
480
- if (!cancelled && !ac.signal.aborted) {
481
- setError(e);
482
- setResults(null);
483
- }
484
- } finally {
485
- if (!cancelled) setLoading(false);
984
+ setError(e instanceof Error ? e : Error(String(e)));
985
+ setLoading(false);
986
+ return;
486
987
  }
487
- };
488
- const t = setTimeout(run, debounceMs);
988
+ void preview(latest.current.query.aggregations ?? [], ac.signal).then((result) => {
989
+ if (!ac.signal.aborted) setResults(result);
990
+ }).catch((e) => {
991
+ if (!ac.signal.aborted)
992
+ setError(e instanceof Error ? e : Error(String(e)));
993
+ }).finally(() => {
994
+ if (!ac.signal.aborted) setLoading(false);
995
+ });
996
+ }, debounceMs);
489
997
  return () => {
490
- cancelled = true;
491
- clearTimeout(t);
492
998
  ac.abort();
999
+ clearTimeout(timer);
493
1000
  };
494
- }, [key, hasAggregations, transport, clientRows, schema, debounceMs, nonce, validateQuery]);
495
- return { results, loading, error };
1001
+ }, [
1002
+ key,
1003
+ schema,
1004
+ transport,
1005
+ clientRows,
1006
+ debounceMs,
1007
+ nonce,
1008
+ validateQuery,
1009
+ dependencyKey,
1010
+ localPage,
1011
+ localPage ? options.shownRows : void 0,
1012
+ localPage ? options.rowsReady : void 0,
1013
+ localPage ? options.rowsError : void 0,
1014
+ preview
1015
+ ]);
1016
+ return { results, loading, error, preview };
496
1017
  }
497
1018
 
498
1019
  // src/useSelection.ts
499
- import { useCallback as useCallback2, useMemo as useMemo3, useRef as useRef2, useState as useState3 } from "react";
1020
+ import { useCallback as useCallback4, useMemo as useMemo4, useRef as useRef4, useState as useState4 } from "react";
500
1021
  function useSelection(displayedIds) {
501
- const [selected, setSelected] = useState3(() => /* @__PURE__ */ new Set());
502
- const anchor = useRef2(null);
503
- const toggle = useCallback2(
1022
+ const [selected, setSelected] = useState4(() => /* @__PURE__ */ new Set());
1023
+ const anchor = useRef4(null);
1024
+ const toggle = useCallback4(
504
1025
  (id, shiftKey = false) => {
505
1026
  setSelected((prev) => {
506
1027
  const next = new Set(prev);
@@ -528,7 +1049,7 @@ function useSelection(displayedIds) {
528
1049
  },
529
1050
  [displayedIds]
530
1051
  );
531
- const setPage = useCallback2((ids, select) => {
1052
+ const setPage = useCallback4((ids, select) => {
532
1053
  setSelected((prev) => {
533
1054
  const next = new Set(prev);
534
1055
  for (const id of ids) {
@@ -538,12 +1059,12 @@ function useSelection(displayedIds) {
538
1059
  return next;
539
1060
  });
540
1061
  }, []);
541
- const replace = useCallback2((ids) => {
1062
+ const replace = useCallback4((ids) => {
542
1063
  const next = new Set(ids);
543
1064
  anchor.current = null;
544
1065
  setSelected(next);
545
1066
  }, []);
546
- const retain = useCallback2((ids) => {
1067
+ const retain = useCallback4((ids) => {
547
1068
  const allowed = new Set(ids);
548
1069
  anchor.current = null;
549
1070
  setSelected((prev) => {
@@ -552,7 +1073,7 @@ function useSelection(displayedIds) {
552
1073
  return next;
553
1074
  });
554
1075
  }, []);
555
- const pageState = useCallback2(
1076
+ const pageState = useCallback4(
556
1077
  (pageIds) => {
557
1078
  if (pageIds.length === 0) return "none";
558
1079
  let n = 0;
@@ -561,11 +1082,11 @@ function useSelection(displayedIds) {
561
1082
  },
562
1083
  [selected]
563
1084
  );
564
- const clear = useCallback2(() => {
1085
+ const clear = useCallback4(() => {
565
1086
  anchor.current = null;
566
1087
  setSelected(/* @__PURE__ */ new Set());
567
1088
  }, []);
568
- return useMemo3(
1089
+ return useMemo4(
569
1090
  () => ({
570
1091
  selected,
571
1092
  isSelected: (id) => selected.has(id),
@@ -582,16 +1103,16 @@ function useSelection(displayedIds) {
582
1103
  }
583
1104
 
584
1105
  // src/useSelect.ts
585
- import { useCallback as useCallback3, useMemo as useMemo4 } from "react";
1106
+ import { useCallback as useCallback5, useMemo as useMemo5 } from "react";
586
1107
  import { selectedFields } from "@pythia-software/query-table-core";
587
1108
  function useSelect(query, setQuery, schema) {
588
- const fields = useMemo4(() => selectedFields(schema, query), [schema, query]);
589
- const visible = useMemo4(() => fields.map((f) => columnFor(query.select, f.name)), [fields, query.select]);
590
- const hidden = useMemo4(() => {
1109
+ const fields = useMemo5(() => selectedFields(schema, query), [schema, query]);
1110
+ const visible = useMemo5(() => fields.map((f) => columnFor(query.select, f.name)), [fields, query.select]);
1111
+ const hidden = useMemo5(() => {
591
1112
  const shown = new Set(fields.map((f) => f.name));
592
1113
  return schema.fields.filter((f) => !shown.has(f.name) && (f.select?.enabled ?? true));
593
1114
  }, [schema, fields]);
594
- const writeSelect = useCallback3(
1115
+ const writeSelect = useCallback5(
595
1116
  (mutate) => {
596
1117
  setQuery((prev) => {
597
1118
  const current = prev.select.length ? prev.select : visible;
@@ -600,9 +1121,9 @@ function useSelect(query, setQuery, schema) {
600
1121
  },
601
1122
  [setQuery, visible]
602
1123
  );
603
- const show = useCallback3((field) => writeSelect((c) => c.some((x) => x.field === field) ? c : [...c, { field }]), [writeSelect]);
604
- const hide = useCallback3((field) => writeSelect((c) => c.filter((x) => x.field !== field)), [writeSelect]);
605
- const move = useCallback3(
1124
+ const show = useCallback5((field) => writeSelect((c) => c.some((x) => x.field === field) ? c : [...c, { field }]), [writeSelect]);
1125
+ const hide = useCallback5((field) => writeSelect((c) => c.filter((x) => x.field !== field)), [writeSelect]);
1126
+ const move = useCallback5(
606
1127
  (field, toIndex) => writeSelect((c) => {
607
1128
  const from = c.findIndex((x) => x.field === field);
608
1129
  if (from === -1) return c;
@@ -612,11 +1133,11 @@ function useSelect(query, setQuery, schema) {
612
1133
  }),
613
1134
  [writeSelect]
614
1135
  );
615
- const setWidth = useCallback3(
1136
+ const setWidth = useCallback5(
616
1137
  (field, width) => writeSelect((c) => c.map((x) => x.field === field ? { ...x, width: Math.round(width) } : x)),
617
1138
  [writeSelect]
618
1139
  );
619
- const reset = useCallback3(() => setQuery((prev) => ({ ...prev, select: [] })), [setQuery]);
1140
+ const reset = useCallback5(() => setQuery((prev) => ({ ...prev, select: [] })), [setQuery]);
620
1141
  return { visible, fields, hidden, show, hide, move, setWidth, reset };
621
1142
  }
622
1143
  function columnFor(select, name) {
@@ -625,50 +1146,81 @@ function columnFor(select, name) {
625
1146
  }
626
1147
 
627
1148
  // src/useColumnDrag.ts
628
- import { useCallback as useCallback4, useMemo as useMemo5, useState as useState4 } from "react";
1149
+ import { useCallback as useCallback6, useMemo as useMemo6, useState as useState5 } from "react";
629
1150
  function useColumnDrag() {
630
- const [state, setState] = useState4(null);
631
- const start = useCallback4((field, index) => setState({ source: field, overIndex: index }), []);
632
- const over = useCallback4(
633
- (index) => (
1151
+ const [state, setState] = useState5(null);
1152
+ const [dropRevision, setDropRevision] = useState5(0);
1153
+ const start = useCallback6(
1154
+ (field, index, order) => setState({
1155
+ source: field,
1156
+ overIndex: index,
1157
+ order: order?.slice(),
1158
+ moved: false
1159
+ }),
1160
+ []
1161
+ );
1162
+ const over = useCallback6(
1163
+ (index, order) => (
634
1164
  // Keep the SAME object reference when unchanged so React can bail out of the
635
1165
  // re-render — onDragOver fires continuously while the cursor moves.
636
- setState((s) => s && s.overIndex !== index ? { source: s.source, overIndex: index } : s)
1166
+ setState((s) => {
1167
+ if (!s) return s;
1168
+ if (s.moved && s.overIndex === index && (!order && !s.order || order && s.order && order.length === s.order.length && order.every((name, i) => name === s.order[i])))
1169
+ return s;
1170
+ return {
1171
+ source: s.source,
1172
+ overIndex: index,
1173
+ order: order?.slice(),
1174
+ moved: true
1175
+ };
1176
+ })
637
1177
  ),
638
1178
  []
639
1179
  );
640
- const end = useCallback4(() => setState(null), []);
641
- const preview = useCallback4(
1180
+ const end = useCallback6((committed = false) => {
1181
+ if (committed) setDropRevision((revision) => revision + 1);
1182
+ setState(null);
1183
+ }, []);
1184
+ const preview = useCallback6(
642
1185
  (order) => {
643
- if (!state) return order;
1186
+ if (!state || state.order && !state.moved) return order;
644
1187
  const without = order.filter((n) => n !== state.source);
645
1188
  if (without.length === order.length) return order;
646
- const at = Math.max(0, Math.min(state.overIndex, without.length));
1189
+ let at = Math.max(0, Math.min(state.overIndex, without.length));
1190
+ if (state.order) {
1191
+ const reference = state.order.filter((name) => name !== state.source);
1192
+ const slot = Math.max(0, Math.min(state.overIndex, reference.length));
1193
+ const following = reference.slice(slot).find((name) => without.includes(name));
1194
+ const preceding = reference.slice(0, slot).reverse().find((name) => without.includes(name));
1195
+ if (following != null) at = without.indexOf(following);
1196
+ else if (preceding != null) at = without.indexOf(preceding) + 1;
1197
+ }
647
1198
  return [...without.slice(0, at), state.source, ...without.slice(at)];
648
1199
  },
649
1200
  [state]
650
1201
  );
651
- return useMemo5(
1202
+ return useMemo6(
652
1203
  () => ({
653
1204
  source: state?.source ?? null,
654
1205
  overIndex: state?.overIndex ?? null,
655
1206
  active: state != null,
1207
+ dropRevision,
656
1208
  start,
657
1209
  over,
658
1210
  end,
659
1211
  preview
660
1212
  }),
661
- [state, start, over, end, preview]
1213
+ [state, dropRevision, start, over, end, preview]
662
1214
  );
663
1215
  }
664
1216
 
665
1217
  // src/useSavedQueries.ts
666
- import { useCallback as useCallback5, useEffect as useEffect3, useState as useState5 } from "react";
1218
+ import { useCallback as useCallback7, useEffect as useEffect4, useState as useState6 } from "react";
667
1219
  function useSavedQueries(key, currentQuery, applyQueryState, storage, now) {
668
- const [items, setItems] = useState5([]);
669
- const [defaultId, setDefaultId] = useState5(null);
670
- const [loading, setLoading] = useState5(true);
671
- const refresh = useCallback5(async () => {
1220
+ const [items, setItems] = useState6([]);
1221
+ const [defaultId, setDefaultId] = useState6(null);
1222
+ const [loading, setLoading] = useState6(true);
1223
+ const refresh = useCallback7(async () => {
672
1224
  setLoading(true);
673
1225
  try {
674
1226
  const [savedItems, defaultSaved] = await Promise.all([
@@ -681,10 +1233,10 @@ function useSavedQueries(key, currentQuery, applyQueryState, storage, now) {
681
1233
  setLoading(false);
682
1234
  }
683
1235
  }, [storage, key]);
684
- useEffect3(() => {
1236
+ useEffect4(() => {
685
1237
  void refresh();
686
1238
  }, [refresh]);
687
- const save = useCallback5(
1239
+ const save = useCallback7(
688
1240
  async (name) => {
689
1241
  const saved = await storage.saveNamed(key, name, currentQuery, now());
690
1242
  await refresh();
@@ -692,25 +1244,25 @@ function useSavedQueries(key, currentQuery, applyQueryState, storage, now) {
692
1244
  },
693
1245
  [storage, key, currentQuery, now, refresh]
694
1246
  );
695
- const load = useCallback5(
1247
+ const load = useCallback7(
696
1248
  (id) => {
697
1249
  const found = items.find((q) => q.id === id);
698
1250
  if (found) applyQueryState(found.query);
699
1251
  },
700
1252
  [items, applyQueryState]
701
1253
  );
702
- const setDefault = useCallback5(
1254
+ const setDefault = useCallback7(
703
1255
  async (id) => {
704
1256
  await storage.setDefaultSaved?.(key, id);
705
1257
  await refresh();
706
1258
  },
707
1259
  [storage, key, refresh]
708
1260
  );
709
- const clearDefault = useCallback5(async () => {
1261
+ const clearDefault = useCallback7(async () => {
710
1262
  await storage.setDefaultSaved?.(key, null);
711
1263
  await refresh();
712
1264
  }, [storage, key, refresh]);
713
- const remove = useCallback5(
1265
+ const remove = useCallback7(
714
1266
  async (id) => {
715
1267
  if (id === defaultId) await storage.setDefaultSaved?.(key, null);
716
1268
  await storage.deleteSaved(key, id);
@@ -725,7 +1277,7 @@ function useSavedQueries(key, currentQuery, applyQueryState, storage, now) {
725
1277
  var AUTOCOMPLETE_LIMIT = 50;
726
1278
  var MAX_AUTO_REFRESH_POLLS = 1e3;
727
1279
  function defaultsFor(schema) {
728
- return normalizeQueryState({
1280
+ return normalizeQueryState2({
729
1281
  select: schema.defaultSelect ?? [],
730
1282
  where: [],
731
1283
  orderBy: schema.defaultSort ?? [],
@@ -735,30 +1287,31 @@ function defaultsFor(schema) {
735
1287
  }
736
1288
  function cloneWhereTerm(term) {
737
1289
  const copy = isOrGroup(term) ? { ...term, any: term.any.map((c) => ({ ...c })) } : { ...term };
738
- if (term.setFilter) copy.setFilter = { ...term.setFilter, values: [...term.setFilter.values] };
1290
+ if (term.setFilter)
1291
+ copy.setFilter = { ...term.setFilter, values: [...term.setFilter.values] };
739
1292
  return copy;
740
1293
  }
741
1294
  function cloneQueryState(q) {
742
1295
  const clone = {
743
1296
  select: q.select.map((column) => ({ ...column })),
744
1297
  where: q.where.map(cloneWhereTerm),
745
- orderBy: q.orderBy.map((term) => term.extract ? { ...term, extract: { ...term.extract } } : { ...term }),
1298
+ orderBy: q.orderBy.map(
1299
+ (term) => term.extract ? { ...term, extract: { ...term.extract } } : { ...term }
1300
+ ),
746
1301
  limit: q.limit,
747
1302
  offset: q.offset
748
1303
  };
749
- if (q.aggregations) clone.aggregations = q.aggregations.map((a) => ({ ...a, groupBy: [...a.groupBy] }));
1304
+ if (q.aggregations)
1305
+ clone.aggregations = JSON.parse(
1306
+ JSON.stringify(q.aggregations)
1307
+ );
750
1308
  return clone;
751
1309
  }
752
1310
  function mergeAggregation(a, patch) {
753
- const next = {
754
- id: a.id,
755
- op: patch.op ?? a.op,
756
- groupBy: patch.groupBy ?? a.groupBy
757
- };
758
- const field = "field" in patch ? patch.field : a.field;
759
- if (field) next.field = field;
760
- const label = "label" in patch ? patch.label : a.label;
761
- if (label) next.label = label;
1311
+ const next = { ...a, ...patch };
1312
+ for (const key of Object.keys(next)) {
1313
+ if (next[key] === void 0) delete next[key];
1314
+ }
762
1315
  return next;
763
1316
  }
764
1317
  function isNullLike(value) {
@@ -773,7 +1326,7 @@ function asDistinctValue(value) {
773
1326
  }
774
1327
  function fieldHasNullInRows(field, rows) {
775
1328
  for (const row of rows) {
776
- if (isNullLike(readFieldValue2(field, row))) return true;
1329
+ if (isNullLike(readFieldValue4(field, row))) return true;
777
1330
  }
778
1331
  return false;
779
1332
  }
@@ -782,73 +1335,110 @@ function distinctValuesFromRows(field, rows, search) {
782
1335
  const values = [];
783
1336
  const seen = /* @__PURE__ */ new Set();
784
1337
  let hasMore = false;
785
- for (const row of rows) {
786
- const raw = asDistinctValue(readFieldValue2(field, row));
787
- if (raw == null || seen.has(raw)) continue;
788
- if (target && !raw.toLowerCase().includes(target)) continue;
789
- if (values.length < AUTOCOMPLETE_LIMIT) {
790
- seen.add(raw);
791
- values.push(raw);
792
- continue;
1338
+ rowsLoop: for (const row of rows) {
1339
+ const value = readFieldValue4(field, row);
1340
+ const candidates = field.type === "textarray" && Array.isArray(value) ? value : [value];
1341
+ for (const candidate of candidates) {
1342
+ const raw = asDistinctValue(candidate);
1343
+ if (raw == null || seen.has(raw)) continue;
1344
+ if (target && !raw.toLowerCase().includes(target)) continue;
1345
+ if (values.length < AUTOCOMPLETE_LIMIT) {
1346
+ seen.add(raw);
1347
+ values.push(raw);
1348
+ continue;
1349
+ }
1350
+ hasMore = true;
1351
+ break rowsLoop;
793
1352
  }
794
- hasMore = true;
795
- break;
796
1353
  }
797
1354
  return { values: values.sort((a, b) => a.localeCompare(b)), hasMore };
798
1355
  }
799
1356
  function resolveInitial(opts) {
800
1357
  const defaults = defaultsFor(opts.schema);
801
- if (opts.initialQuery) return { q: normalizeQueryState(opts.initialQuery, defaults), fromUrl: false };
1358
+ if (opts.initialQuery)
1359
+ return {
1360
+ q: normalizeQueryState2(opts.initialQuery, defaults),
1361
+ fromUrl: false
1362
+ };
802
1363
  if (opts.syncUrl === true && typeof window !== "undefined") {
803
1364
  const token = new URLSearchParams(window.location.search).get("q");
804
- if (token) return { q: normalizeQueryState(decodeQuery(token), defaults), fromUrl: true };
1365
+ if (token)
1366
+ return {
1367
+ q: normalizeQueryState2(decodeQuery(token), defaults),
1368
+ fromUrl: true
1369
+ };
805
1370
  }
806
1371
  return { q: defaults, fromUrl: false };
807
1372
  }
808
1373
  function useQueryTable(opts) {
809
- const { schema, transport, clientRows, initialQuery, syncUrl = false, debounceMs = 200 } = opts;
810
- const storage = useMemo6(() => opts.storage ?? memoryStorageAdapter(), [opts.storage]);
1374
+ const {
1375
+ schema,
1376
+ transport: sourceTransport,
1377
+ clientRows,
1378
+ initialQuery,
1379
+ syncUrl = false,
1380
+ debounceMs = 200
1381
+ } = opts;
1382
+ const { transport, activity: requestActivity } = useRequestActivity(sourceTransport);
1383
+ const storage = useMemo7(
1384
+ () => opts.storage ?? memoryStorageAdapter(),
1385
+ [opts.storage]
1386
+ );
811
1387
  const now = opts.now ?? Date.now;
812
- const nowRef = useRef3(now);
813
- const onRefreshRef = useRef3(opts.onRefresh);
814
- const defaults = useMemo6(() => defaultsFor(schema), [schema]);
815
- const byName = useMemo6(() => new Map(schema.fields.map((f) => [f.name, f])), [schema]);
816
- const initRef = useRef3(resolveInitial(opts));
817
- const aggIdRef = useRef3(0);
818
- const [storedQuery, setQueryState] = useState6(initRef.current.q);
819
- const query = useMemo6(() => opts.canonicalizeQuery?.(storedQuery) ?? storedQuery, [storedQuery, opts.canonicalizeQuery]);
820
- const undoStack = useRef3([cloneQueryState(initRef.current.q)]);
821
- const redoStack = useRef3([]);
822
- const [rows, setRows] = useState6([]);
823
- const [total, setTotal] = useState6(null);
824
- const [loading, setLoading] = useState6(false);
825
- const [error, setError] = useState6(null);
826
- const [nonce, setNonce] = useState6(0);
827
- const [autoRefreshStatus, setAutoRefreshStatus] = useState6(null);
828
- useEffect4(() => {
1388
+ const nowRef = useRef5(now);
1389
+ const onRefreshRef = useRef5(opts.onRefresh);
1390
+ const defaults = useMemo7(() => defaultsFor(schema), [schema]);
1391
+ const byName = useMemo7(
1392
+ () => new Map(schema.fields.map((f) => [f.name, f])),
1393
+ [schema]
1394
+ );
1395
+ const initRef = useRef5(resolveInitial(opts));
1396
+ const aggIdRef = useRef5(0);
1397
+ const [storedQuery, setQueryState] = useState7(initRef.current.q);
1398
+ const query = useMemo7(
1399
+ () => opts.canonicalizeQuery?.(storedQuery) ?? storedQuery,
1400
+ [storedQuery, opts.canonicalizeQuery]
1401
+ );
1402
+ const undoStack = useRef5([cloneQueryState(initRef.current.q)]);
1403
+ const redoStack = useRef5([]);
1404
+ const [rows, setRows] = useState7([]);
1405
+ const [serverComputed, setServerComputed] = useState7(/* @__PURE__ */ new Map());
1406
+ const [committedFields, setCommittedFields] = useState7([]);
1407
+ const [serverExecution, setServerExecution] = useState7();
1408
+ const [total, setTotal] = useState7(null);
1409
+ const [loading, setLoading] = useState7(false);
1410
+ const [error, setError] = useState7(null);
1411
+ const [nonce, setNonce] = useState7(0);
1412
+ const [committedPage, setCommittedPage] = useState7(null);
1413
+ const [committedSource, setCommittedSource] = useState7(null);
1414
+ const [autoRefreshStatus, setAutoRefreshStatus] = useState7(null);
1415
+ useEffect5(() => {
829
1416
  nowRef.current = now;
830
1417
  }, [now]);
831
- useEffect4(() => {
1418
+ useEffect5(() => {
832
1419
  onRefreshRef.current = opts.onRefresh;
833
1420
  }, [opts.onRefresh]);
834
- const setQuery = useCallback6((next) => {
835
- setQueryState((prev) => {
836
- const candidate = typeof next === "function" ? next(prev) : next;
837
- const nextQuery = normalizeQueryState(candidate, defaults);
838
- if (queriesEqual(prev, nextQuery)) return prev;
839
- undoStack.current.push(cloneQueryState(prev));
840
- redoStack.current = [];
841
- return cloneQueryState(nextQuery);
842
- });
843
- }, [defaults]);
844
- useEffect4(() => {
1421
+ const setQuery = useCallback8(
1422
+ (next) => {
1423
+ setQueryState((prev) => {
1424
+ const candidate = typeof next === "function" ? next(prev) : next;
1425
+ const nextQuery = normalizeQueryState2(candidate, defaults);
1426
+ if (queriesEqual(prev, nextQuery)) return prev;
1427
+ undoStack.current.push(cloneQueryState(prev));
1428
+ redoStack.current = [];
1429
+ return cloneQueryState(nextQuery);
1430
+ });
1431
+ },
1432
+ [defaults]
1433
+ );
1434
+ useEffect5(() => {
845
1435
  if (initRef.current.fromUrl || initialQuery) return;
846
1436
  let cancelled = false;
847
1437
  void (async () => {
848
1438
  const defaultSaved = await storage.loadDefaultSaved?.(schema.name);
849
1439
  const restored = defaultSaved?.query ?? await storage.loadLast(schema.name);
850
1440
  if (!cancelled && restored) {
851
- const normalized = normalizeQueryState(restored, defaults);
1441
+ const normalized = normalizeQueryState2(restored, defaults);
852
1442
  undoStack.current = [cloneQueryState(normalized)];
853
1443
  redoStack.current = [];
854
1444
  setQueryState(normalized);
@@ -862,18 +1452,93 @@ function useQueryTable(opts) {
862
1452
  schema,
863
1453
  query,
864
1454
  rows,
1455
+ serverComputed,
1456
+ ...serverExecution ? { serverExecution } : {},
1457
+ ...opts.computedExecution ? { execution: opts.computedExecution } : {},
865
1458
  ...opts.computedColumnStore ? { store: opts.computedColumnStore } : {},
866
1459
  ...opts.formulaWorkerFactory ? { workerFactory: opts.formulaWorkerFactory } : {},
867
1460
  ...transport ? { transport } : {},
868
1461
  ...clientRows ? { clientRows } : {}
869
1462
  });
870
- const queryKey = useMemo6(() => encodeQuery(query), [query]);
871
- const serverRowQuery = useMemo6(() => toServerQuery2(query, displaySchema), [queryKey, displaySchema]);
872
- const rowQueryKey = useMemo6(
1463
+ const resolveComputed = useCallback8(
1464
+ (name) => isComputedField4(name) ? computed.compile(`[${name.replace(/]/g, "]] ".trim())}]`) : void 0,
1465
+ [computed.compile]
1466
+ );
1467
+ const metricCompile = useCallback8(
1468
+ (clause) => {
1469
+ const diagnostics = validateMetric2(
1470
+ clause,
1471
+ displaySchema,
1472
+ resolveComputed
1473
+ );
1474
+ if (diagnostics.length)
1475
+ throw new Error(diagnostics.map((d) => d.message).join(" "));
1476
+ return metricPlans3(clause, displaySchema, resolveComputed);
1477
+ },
1478
+ [displaySchema, resolveComputed]
1479
+ );
1480
+ const computedRevisions = useMemo7(
1481
+ () => Object.fromEntries(computed.definitions.map((d) => [d.id, d.revision])),
1482
+ [computed.definitions]
1483
+ );
1484
+ const definitionKey = JSON.stringify(computedRevisions);
1485
+ const queryKey = useMemo7(() => encodeQuery(query), [query]);
1486
+ const serverRowQuery = useMemo7(() => {
1487
+ const execution = opts.computedExecution;
1488
+ const v2Request = execution && transport?.fetchRowsV2 ? toServerQueryV2(query, displaySchema, execution) : void 0;
1489
+ const request = v2Request ?? toServerQuery2(query, displaySchema);
1490
+ if (v2Request)
1491
+ for (const definition of computed.definitions) {
1492
+ if (executionRevision(execution, definition.id) !== void 0 && executionRevision(execution, definition.id) !== definition.revision)
1493
+ v2Request.diagnostics.push({
1494
+ from: 0,
1495
+ to: 1,
1496
+ code: "definition_changed",
1497
+ message: "Computed definitions changed. Refresh the server execution handshake."
1498
+ });
1499
+ }
1500
+ for (const clause of query.aggregations ?? [])
1501
+ if (clause.scope === "shownRows") {
1502
+ try {
1503
+ for (const name of metricDependencies(
1504
+ clause,
1505
+ schema,
1506
+ resolveComputed
1507
+ )) {
1508
+ const field = schema.fields.find((f) => f.name === name);
1509
+ if (field?.source.kind === "backend") request.select.push(name);
1510
+ else
1511
+ for (const dep of field?.source.kind === "derived" ? field.source.dependencies ?? [] : [])
1512
+ request.select.push(dep);
1513
+ }
1514
+ } catch {
1515
+ }
1516
+ }
1517
+ request.select = [...new Set(request.select)].sort();
1518
+ return request;
1519
+ }, [
1520
+ queryKey,
1521
+ displaySchema,
1522
+ transport,
1523
+ computedRevisions,
1524
+ schema,
1525
+ resolveComputed,
1526
+ opts.computedExecution
1527
+ ]);
1528
+ const rowQueryKey = useMemo7(
873
1529
  () => transport ? `server:${JSON.stringify(serverRowQuery)}` : `client:${JSON.stringify([query.where, query.orderBy, query.limit, query.offset])}`,
874
- [transport, serverRowQuery, query.where, query.orderBy, query.limit, query.offset]
1530
+ [
1531
+ transport,
1532
+ serverRowQuery,
1533
+ query.where,
1534
+ query.orderBy,
1535
+ query.limit,
1536
+ query.offset
1537
+ ]
875
1538
  );
876
- useEffect4(() => {
1539
+ const executionKey = JSON.stringify(opts.computedExecution ?? null);
1540
+ const pageContext = `${schema.name}:${rowQueryKey}:${nonce}:${executionKey}`;
1541
+ useEffect5(() => {
877
1542
  const ac = new AbortController();
878
1543
  let cancelled = false;
879
1544
  const run = async () => {
@@ -882,16 +1547,47 @@ function useQueryTable(opts) {
882
1547
  try {
883
1548
  opts.validateQuery?.(query);
884
1549
  if (transport) {
885
- const res = await transport.fetchRows(serverRowQuery, ac.signal);
1550
+ const useV2 = "version" in serverRowQuery && transport.fetchRowsV2;
1551
+ if ("diagnostics" in serverRowQuery && Array.isArray(serverRowQuery.diagnostics) && serverRowQuery.diagnostics.length)
1552
+ throw Error(
1553
+ serverRowQuery.diagnostics.map((d) => d.message).join(" ")
1554
+ );
1555
+ if (!useV2 && query.orderBy.some((s) => isComputedField4(s.field)))
1556
+ throw Error(
1557
+ "Global computed sorting requires a server execution handshake and fetchRowsV2."
1558
+ );
1559
+ const res = useV2 ? await transport.fetchRowsV2(
1560
+ serverRowQuery,
1561
+ ac.signal
1562
+ ) : await transport.fetchRows(serverRowQuery, ac.signal);
1563
+ if (useV2)
1564
+ validateComputedResponse(
1565
+ serverRowQuery,
1566
+ res,
1567
+ schema
1568
+ );
886
1569
  if (!cancelled) {
1570
+ setCommittedFields(serverRowQuery.select);
1571
+ setServerExecution(
1572
+ useV2 ? res.execution : void 0
1573
+ );
1574
+ if ("computed" in res && Array.isArray(res.computed))
1575
+ setServerComputed(
1576
+ new Map(res.computed.map((c) => [c.id, c.values]))
1577
+ );
1578
+ else setServerComputed(/* @__PURE__ */ new Map());
887
1579
  setRows(res.rows);
888
1580
  setTotal(res.total);
1581
+ setCommittedPage(pageContext);
1582
+ setCommittedSource({ schema, transport, clientRows });
889
1583
  }
890
1584
  } else {
891
1585
  const res = applyQuery2(clientRows ?? [], query, schema);
892
1586
  if (!cancelled) {
893
1587
  setRows(res.rows);
894
1588
  setTotal(res.total);
1589
+ setCommittedPage(pageContext);
1590
+ setCommittedSource({ schema, transport, clientRows });
895
1591
  }
896
1592
  }
897
1593
  } catch (e) {
@@ -899,6 +1595,7 @@ function useQueryTable(opts) {
899
1595
  setError(e);
900
1596
  setRows([]);
901
1597
  setTotal(null);
1598
+ setCommittedPage(null);
902
1599
  }
903
1600
  } finally {
904
1601
  if (!cancelled) setLoading(false);
@@ -910,8 +1607,17 @@ function useQueryTable(opts) {
910
1607
  clearTimeout(t);
911
1608
  ac.abort();
912
1609
  };
913
- }, [rowQueryKey, transport, clientRows, schema, debounceMs, nonce, opts.validateQuery]);
914
- useEffect4(() => {
1610
+ }, [
1611
+ rowQueryKey,
1612
+ transport,
1613
+ clientRows,
1614
+ schema,
1615
+ debounceMs,
1616
+ nonce,
1617
+ opts.validateQuery,
1618
+ executionKey
1619
+ ]);
1620
+ useEffect5(() => {
915
1621
  if (!syncUrl || typeof window === "undefined") return;
916
1622
  const url = new URL(window.location.href);
917
1623
  const token = encodeQuery(query);
@@ -919,16 +1625,22 @@ function useQueryTable(opts) {
919
1625
  else url.searchParams.delete("q");
920
1626
  window.history.replaceState(null, "", url.toString());
921
1627
  }, [queryKey, syncUrl, query]);
922
- useEffect4(() => {
1628
+ useEffect5(() => {
923
1629
  const t = setTimeout(() => void storage.saveLast(schema.name, query), 400);
924
1630
  return () => clearTimeout(t);
925
1631
  }, [queryKey, storage, schema.name, query]);
926
- const idField = useMemo6(() => schema.fields.find((f) => f.name === schema.idField), [schema]);
927
- const rowId = useCallback6(
928
- (row) => idField ? readFieldValue2(idField, row) : null,
1632
+ const idField = useMemo7(
1633
+ () => schema.fields.find((f) => f.name === schema.idField),
1634
+ [schema]
1635
+ );
1636
+ const rowId = useCallback8(
1637
+ (row) => idField ? readFieldValue4(idField, row) : null,
929
1638
  [idField]
930
1639
  );
931
- const displayedIds = useMemo6(() => rows.map(rowId).filter((id) => id != null), [rows, rowId]);
1640
+ const displayedIds = useMemo7(
1641
+ () => rows.map(rowId).filter((id) => id != null),
1642
+ [rows, rowId]
1643
+ );
932
1644
  const selection = useSelection(displayedIds);
933
1645
  const select = useSelect(query, setQuery, displaySchema);
934
1646
  const columnDrag = useColumnDrag();
@@ -939,10 +1651,54 @@ function useQueryTable(opts) {
939
1651
  storage,
940
1652
  now
941
1653
  );
942
- const aggState = useAggregations(query, schema, transport, clientRows, debounceMs, nonce, opts.validateQuery);
1654
+ const pageReady = committedPage === pageContext && committedSource?.schema === schema && committedSource?.transport === transport && committedSource?.clientRows === clientRows && !error;
1655
+ const dataToken = useRef5(0);
1656
+ const dataVersion = useMemo7(
1657
+ () => ++dataToken.current,
1658
+ [schema, transport, clientRows, nonce, definitionKey, executionKey]
1659
+ );
1660
+ const aggState = useAggregations(
1661
+ query,
1662
+ schema,
1663
+ transport,
1664
+ clientRows,
1665
+ debounceMs,
1666
+ nonce,
1667
+ opts.validateQuery,
1668
+ {
1669
+ shownRows: rows,
1670
+ rowsReady: pageReady,
1671
+ rowsError: error,
1672
+ ...transport ? { shownFields: committedFields } : {},
1673
+ resolveComputed,
1674
+ revisions: computedRevisions,
1675
+ ...opts.computedExecution ? {
1676
+ execution: pageReady && serverExecution ? serverExecution : opts.computedExecution
1677
+ } : {},
1678
+ ...opts.formulaWorkerFactory ? { workerFactory: opts.formulaWorkerFactory } : {}
1679
+ }
1680
+ );
1681
+ const queryRef = useRef5(query);
1682
+ queryRef.current = query;
1683
+ const replaceAggregations = useCallback8(
1684
+ (clauses, expected) => {
1685
+ if (expected && JSON.stringify(queryRef.current.aggregations ?? []) !== JSON.stringify(expected))
1686
+ throw new Error(
1687
+ "Metrics changed while editing. Reopen the editor to use the latest configuration."
1688
+ );
1689
+ for (const clause of clauses) metricCompile(clause);
1690
+ setQuery((q) => ({
1691
+ ...q,
1692
+ aggregations: JSON.parse(
1693
+ JSON.stringify(clauses)
1694
+ )
1695
+ }));
1696
+ },
1697
+ [setQuery, metricCompile]
1698
+ );
943
1699
  const canUndo = undoStack.current.length > 0;
944
1700
  const canRedo = redoStack.current.length > 0;
945
- const undo = useCallback6(() => {
1701
+ const undo = useCallback8(() => {
946
1702
  setQueryState((prev) => {
947
1703
  const previous = undoStack.current.pop();
948
1704
  if (!previous) return prev;
@@ -950,7 +1706,7 @@ function useQueryTable(opts) {
950
1706
  return cloneQueryState(previous);
951
1707
  });
952
1708
  }, []);
953
- const redo = useCallback6(() => {
1709
+ const redo = useCallback8(() => {
954
1710
  setQueryState((prev) => {
955
1711
  const next = redoStack.current.pop();
956
1712
  if (!next) return prev;
@@ -958,30 +1714,50 @@ function useQueryTable(opts) {
958
1714
  return cloneQueryState(next);
959
1715
  });
960
1716
  }, []);
961
- const patch = useCallback6((p) => setQuery((prev) => ({ ...prev, ...p })), [setQuery]);
962
- const addFilter = useCallback6((clause) => setQuery((q) => ({ ...q, offset: 0, where: [...q.where, clause] })), [setQuery]);
963
- const updateFilter = useCallback6(
964
- (index, clause) => setQuery((q) => ({ ...q, offset: 0, where: q.where.map((t, i) => i === index ? clause : t) })),
1717
+ const patch = useCallback8(
1718
+ (p) => setQuery((prev) => ({ ...prev, ...p })),
965
1719
  [setQuery]
966
1720
  );
967
- const removeFilter = useCallback6(
968
- (index) => setQuery((q) => ({ ...q, offset: 0, where: q.where.filter((_, i) => i !== index) })),
1721
+ const addFilter = useCallback8(
1722
+ (clause) => setQuery((q) => ({ ...q, offset: 0, where: [...q.where, clause] })),
969
1723
  [setQuery]
970
1724
  );
971
- const clearFilters = useCallback6(() => setQuery((q) => ({ ...q, offset: 0, where: [] })), [setQuery]);
972
- const updatePredicate = useCallback6(
1725
+ const updateFilter = useCallback8(
1726
+ (index, clause) => setQuery((q) => ({
1727
+ ...q,
1728
+ offset: 0,
1729
+ where: q.where.map((t, i) => i === index ? clause : t)
1730
+ })),
1731
+ [setQuery]
1732
+ );
1733
+ const removeFilter = useCallback8(
1734
+ (index) => setQuery((q) => ({
1735
+ ...q,
1736
+ offset: 0,
1737
+ where: q.where.filter((_, i) => i !== index)
1738
+ })),
1739
+ [setQuery]
1740
+ );
1741
+ const clearFilters = useCallback8(
1742
+ () => setQuery((q) => ({ ...q, offset: 0, where: [] })),
1743
+ [setQuery]
1744
+ );
1745
+ const updatePredicate = useCallback8(
973
1746
  (termIndex, predIndex, clause) => setQuery((q) => ({
974
1747
  ...q,
975
1748
  offset: 0,
976
1749
  where: q.where.map((term, i) => {
977
1750
  if (i !== termIndex) return term;
978
- if (isOrGroup(term)) return { any: term.any.map((c, j) => j === predIndex ? clause : c) };
1751
+ if (isOrGroup(term))
1752
+ return {
1753
+ any: term.any.map((c, j) => j === predIndex ? clause : c)
1754
+ };
979
1755
  return clause;
980
1756
  })
981
1757
  })),
982
1758
  [setQuery]
983
1759
  );
984
- const removePredicate = useCallback6(
1760
+ const removePredicate = useCallback8(
985
1761
  (termIndex, predIndex) => setQuery((q) => {
986
1762
  const where = [];
987
1763
  q.where.forEach((term, i) => {
@@ -1000,26 +1776,29 @@ function useQueryTable(opts) {
1000
1776
  }),
1001
1777
  [setQuery]
1002
1778
  );
1003
- const negateOne = useCallback6(
1779
+ const negateOne = useCallback8(
1004
1780
  (clause) => {
1005
1781
  const field = byName.get(clause.field);
1006
1782
  return negateClause(clause, field ? opsForField(field) : void 0);
1007
1783
  },
1008
1784
  [byName]
1009
1785
  );
1010
- const negatePredicate = useCallback6(
1786
+ const negatePredicate = useCallback8(
1011
1787
  (termIndex, predIndex) => setQuery((q) => ({
1012
1788
  ...q,
1013
1789
  offset: 0,
1014
1790
  where: q.where.map((term, i) => {
1015
1791
  if (i !== termIndex) return term;
1016
- if (isOrGroup(term)) return { any: term.any.map((c, j) => j === predIndex ? negateOne(c) : c) };
1792
+ if (isOrGroup(term))
1793
+ return {
1794
+ any: term.any.map((c, j) => j === predIndex ? negateOne(c) : c)
1795
+ };
1017
1796
  return negateOne(term);
1018
1797
  })
1019
1798
  })),
1020
1799
  [setQuery, negateOne]
1021
1800
  );
1022
- const reorderFilters = useCallback6(
1801
+ const reorderFilters = useCallback8(
1023
1802
  (fromIndex, toIndex) => setQuery((q) => {
1024
1803
  if (fromIndex < 0 || fromIndex >= q.where.length) return q;
1025
1804
  const where = [...q.where];
@@ -1029,13 +1808,15 @@ function useQueryTable(opts) {
1029
1808
  }),
1030
1809
  [setQuery]
1031
1810
  );
1032
- const mergeFilters = useCallback6(
1811
+ const mergeFilters = useCallback8(
1033
1812
  (sourceIndex, targetIndex) => setQuery((q) => {
1034
1813
  if (sourceIndex === targetIndex) return q;
1035
1814
  const source = q.where[sourceIndex];
1036
1815
  const target = q.where[targetIndex];
1037
1816
  if (!source || !target) return q;
1038
- const merged = { any: [...predicatesOf(target), ...predicatesOf(source)] };
1817
+ const merged = {
1818
+ any: [...predicatesOf(target), ...predicatesOf(source)]
1819
+ };
1039
1820
  const where = [];
1040
1821
  q.where.forEach((term, i) => {
1041
1822
  if (i === sourceIndex) return;
@@ -1045,24 +1826,37 @@ function useQueryTable(opts) {
1045
1826
  }),
1046
1827
  [setQuery]
1047
1828
  );
1048
- const resetAll = useCallback6(() => setQuery(() => cloneQueryState(defaults)), [setQuery, defaults]);
1049
- const addAggregation = useCallback6(
1829
+ const resetAll = useCallback8(
1830
+ () => setQuery(() => cloneQueryState(defaults)),
1831
+ [setQuery, defaults]
1832
+ );
1833
+ const addAggregation = useCallback8(
1050
1834
  (partial) => {
1051
1835
  const id = `a${nowRef.current()}-${aggIdRef.current++}`;
1052
- const clause = { id, op: "count", groupBy: [], ...partial };
1053
- setQuery((q) => ({ ...q, aggregations: [...q.aggregations ?? [], clause] }));
1836
+ const clause = {
1837
+ id,
1838
+ op: "count",
1839
+ groupBy: [],
1840
+ ...partial
1841
+ };
1842
+ setQuery((q) => ({
1843
+ ...q,
1844
+ aggregations: [...q.aggregations ?? [], clause]
1845
+ }));
1054
1846
  return id;
1055
1847
  },
1056
1848
  [setQuery]
1057
1849
  );
1058
- const updateAggregation = useCallback6(
1850
+ const updateAggregation = useCallback8(
1059
1851
  (id, patch2) => setQuery((q) => ({
1060
1852
  ...q,
1061
- aggregations: (q.aggregations ?? []).map((a) => a.id === id ? mergeAggregation(a, patch2) : a)
1853
+ aggregations: (q.aggregations ?? []).map(
1854
+ (a) => a.id === id ? mergeAggregation(a, patch2) : a
1855
+ )
1062
1856
  })),
1063
1857
  [setQuery]
1064
1858
  );
1065
- const moveAggregation = useCallback6(
1859
+ const moveAggregation = useCallback8(
1066
1860
  (id, toIndex) => setQuery((q) => {
1067
1861
  const current = q.aggregations ?? [];
1068
1862
  const from = current.findIndex((a) => a.id === id);
@@ -1074,24 +1868,43 @@ function useQueryTable(opts) {
1074
1868
  }),
1075
1869
  [setQuery]
1076
1870
  );
1077
- const removeAggregation = useCallback6(
1078
- (id) => setQuery((q) => ({ ...q, aggregations: (q.aggregations ?? []).filter((a) => a.id !== id) })),
1871
+ const removeAggregation = useCallback8(
1872
+ (id) => setQuery((q) => ({
1873
+ ...q,
1874
+ aggregations: (q.aggregations ?? []).filter((a) => a.id !== id)
1875
+ })),
1079
1876
  [setQuery]
1080
1877
  );
1081
- const clearAggregations = useCallback6(
1878
+ const clearAggregations = useCallback8(
1082
1879
  () => setQuery((q) => ({ ...q, aggregations: [] })),
1083
1880
  [setQuery]
1084
1881
  );
1085
- const setSort = useCallback6((orderBy) => setQuery((q) => ({ ...q, offset: 0, orderBy })), [setQuery]);
1086
- const toggleSort = useCallback6(
1087
- (field, additive = false) => setQuery((q) => ({ ...q, offset: 0, orderBy: nextOrderBy(q.orderBy, field, additive) })),
1882
+ const setSort = useCallback8(
1883
+ (orderBy) => setQuery((q) => ({ ...q, offset: 0, orderBy })),
1088
1884
  [setQuery]
1089
1885
  );
1090
- const setLimit = useCallback6((limit) => patch({ limit, offset: 0 }), [patch]);
1091
- const setOffset = useCallback6((offset) => patch({ offset }), [patch]);
1092
- const nextPage = useCallback6(() => setQuery((q) => ({ ...q, offset: q.offset + q.limit })), [setQuery]);
1093
- const prevPage = useCallback6(() => setQuery((q) => ({ ...q, offset: Math.max(0, q.offset - q.limit) })), [setQuery]);
1094
- const filterValues = useCallback6(
1886
+ const toggleSort = useCallback8(
1887
+ (field, additive = false) => setQuery((q) => ({
1888
+ ...q,
1889
+ offset: 0,
1890
+ orderBy: nextOrderBy(q.orderBy, field, additive)
1891
+ })),
1892
+ [setQuery]
1893
+ );
1894
+ const setLimit = useCallback8(
1895
+ (limit) => patch({ limit, offset: 0 }),
1896
+ [patch]
1897
+ );
1898
+ const setOffset = useCallback8((offset) => patch({ offset }), [patch]);
1899
+ const nextPage = useCallback8(
1900
+ () => setQuery((q) => ({ ...q, offset: q.offset + q.limit })),
1901
+ [setQuery]
1902
+ );
1903
+ const prevPage = useCallback8(
1904
+ () => setQuery((q) => ({ ...q, offset: Math.max(0, q.offset - q.limit) })),
1905
+ [setQuery]
1906
+ );
1907
+ const filterValues = useCallback8(
1095
1908
  async (field, search) => {
1096
1909
  if (!transport?.fetchDistinctValues) {
1097
1910
  const f = byName.get(field);
@@ -1108,7 +1921,7 @@ function useQueryTable(opts) {
1108
1921
  },
1109
1922
  [transport, byName, clientRows]
1110
1923
  );
1111
- const fieldStats = useCallback6(
1924
+ const fieldStats = useCallback8(
1112
1925
  async (fields, signal) => {
1113
1926
  const requested = [...new Set(fields)];
1114
1927
  if (transport?.fetchFieldStats) {
@@ -1125,7 +1938,7 @@ function useQueryTable(opts) {
1125
1938
  if (!field) continue;
1126
1939
  const distinct = /* @__PURE__ */ new Set();
1127
1940
  for (const row of clientRows) {
1128
- const value = readFieldValue2(field, row);
1941
+ const value = readFieldValue4(field, row);
1129
1942
  if (value !== null) distinct.add(JSON.stringify(value));
1130
1943
  }
1131
1944
  result[name] = { distinct: distinct.size };
@@ -1134,12 +1947,12 @@ function useQueryTable(opts) {
1134
1947
  },
1135
1948
  [transport, byName, clientRows]
1136
1949
  );
1137
- const refresh = useCallback6(() => {
1950
+ const refresh = useCallback8(() => {
1138
1951
  setNonce((n) => n + 1);
1139
1952
  onRefreshRef.current?.();
1140
1953
  }, []);
1141
- const stopAutoRefresh = useCallback6(() => setAutoRefreshStatus(null), []);
1142
- const startAutoRefresh = useCallback6(
1954
+ const stopAutoRefresh = useCallback8(() => setAutoRefreshStatus(null), []);
1955
+ const startAutoRefresh = useCallback8(
1143
1956
  ({ frequencyMs, turnOffAfterMs }) => {
1144
1957
  const pollCount = frequencyMs > 0 && turnOffAfterMs > 0 ? Math.floor(turnOffAfterMs / frequencyMs) : 0;
1145
1958
  if (pollCount < 1 || pollCount > MAX_AUTO_REFRESH_POLLS) {
@@ -1159,11 +1972,13 @@ function useQueryTable(opts) {
1159
1972
  );
1160
1973
  const autoRefreshFrequencyMs = autoRefreshStatus?.frequencyMs ?? null;
1161
1974
  const autoRefreshStopsAt = autoRefreshStatus?.stopsAt ?? null;
1162
- useEffect4(() => {
1975
+ useEffect5(() => {
1163
1976
  if (autoRefreshFrequencyMs == null || autoRefreshStopsAt == null) return;
1164
1977
  const tick = () => {
1165
1978
  if (nowRef.current() >= autoRefreshStopsAt) {
1166
- setAutoRefreshStatus((current) => current?.stopsAt === autoRefreshStopsAt ? null : current);
1979
+ setAutoRefreshStatus(
1980
+ (current) => current?.stopsAt === autoRefreshStopsAt ? null : current
1981
+ );
1167
1982
  return;
1168
1983
  }
1169
1984
  refresh();
@@ -1175,7 +1990,9 @@ function useQueryTable(opts) {
1175
1990
  };
1176
1991
  const interval = setInterval(tick, autoRefreshFrequencyMs);
1177
1992
  const timeout = setTimeout(
1178
- () => setAutoRefreshStatus((current) => current?.stopsAt === autoRefreshStopsAt ? null : current),
1993
+ () => setAutoRefreshStatus(
1994
+ (current) => current?.stopsAt === autoRefreshStopsAt ? null : current
1995
+ ),
1179
1996
  Math.max(0, autoRefreshStopsAt - nowRef.current())
1180
1997
  );
1181
1998
  return () => {
@@ -1183,7 +2000,7 @@ function useQueryTable(opts) {
1183
2000
  clearTimeout(timeout);
1184
2001
  };
1185
2002
  }, [autoRefreshFrequencyMs, autoRefreshStopsAt, refresh]);
1186
- const autoRefresh = useMemo6(
2003
+ const autoRefresh = useMemo7(
1187
2004
  () => ({
1188
2005
  status: autoRefreshStatus,
1189
2006
  start: startAutoRefresh,
@@ -1191,16 +2008,25 @@ function useQueryTable(opts) {
1191
2008
  }),
1192
2009
  [autoRefreshStatus, startAutoRefresh, stopAutoRefresh]
1193
2010
  );
1194
- const refreshRow = useCallback6(
2011
+ const refreshRow = useCallback8(
1195
2012
  async (id) => {
2013
+ if (opts.computedExecution && transport?.fetchRowsV2) {
2014
+ refresh();
2015
+ return;
2016
+ }
1196
2017
  if (!transport?.fetchRow) return;
1197
2018
  const updated = await transport.fetchRow(id);
1198
- setRows((prev) => prev.map((r) => rowId(r) === id ? updated ?? r : r));
2019
+ setRows(
2020
+ (prev) => prev.map((r) => rowId(r) === id ? updated ?? r : r)
2021
+ );
1199
2022
  },
1200
- [transport, rowId]
2023
+ [transport, rowId, opts.computedExecution, refresh]
2024
+ );
2025
+ const visibleFields = useMemo7(
2026
+ () => selectedFields2(displaySchema, query),
2027
+ [displaySchema, query]
1201
2028
  );
1202
- const visibleFields = useMemo6(() => selectedFields2(displaySchema, query), [displaySchema, query]);
1203
- const aggregations = useMemo6(
2029
+ const aggregations = useMemo7(
1204
2030
  () => ({
1205
2031
  clauses: query.aggregations ?? [],
1206
2032
  results: aggState.results,
@@ -1210,7 +2036,11 @@ function useQueryTable(opts) {
1210
2036
  update: updateAggregation,
1211
2037
  move: moveAggregation,
1212
2038
  remove: removeAggregation,
1213
- clear: clearAggregations
2039
+ clear: clearAggregations,
2040
+ preview: aggState.preview,
2041
+ replace: replaceAggregations,
2042
+ compile: metricCompile,
2043
+ contextKey: `${dataVersion}:${pageContext}:${pageReady}:${JSON.stringify(query.where)}`
1214
2044
  }),
1215
2045
  [
1216
2046
  query.aggregations,
@@ -1221,7 +2051,14 @@ function useQueryTable(opts) {
1221
2051
  updateAggregation,
1222
2052
  moveAggregation,
1223
2053
  removeAggregation,
1224
- clearAggregations
2054
+ clearAggregations,
2055
+ aggState.preview,
2056
+ replaceAggregations,
2057
+ metricCompile,
2058
+ pageContext,
2059
+ pageReady,
2060
+ dataVersion,
2061
+ query.where
1225
2062
  ]
1226
2063
  );
1227
2064
  return {
@@ -1232,6 +2069,7 @@ function useQueryTable(opts) {
1232
2069
  total,
1233
2070
  loading,
1234
2071
  error,
2072
+ requestActivity,
1235
2073
  canUndo,
1236
2074
  canRedo,
1237
2075
  undo,