@pythia-software/query-table-react 0.9.9 → 1.0.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
@@ -339,12 +339,15 @@ import {
339
339
  createGroupingModel,
340
340
  prepareGrouping,
341
341
  withoutGrouping as withoutGrouping2,
342
- sourceExecutionSchema as sourceExecutionSchema2,
343
- toQueryRequestV3 as toQueryRequestV32,
344
- validateQueryResultV3 as validateQueryResultV32,
345
- metricPopulationError as metricPopulationError2
342
+ sourceExecutionSchema as sourceExecutionSchema3,
343
+ toQueryRequestV3 as toQueryRequestV33,
344
+ validateQueryResultV3 as validateQueryResultV33,
345
+ fetchDistinctValuesV3,
346
+ fetchFieldStatsV3,
347
+ fetchRowV3,
348
+ isPushdownFilter
346
349
  } from "@pythia-software/query-table-core";
347
- import { ITEM_DETAILS_COLUMN, itemPageFields } from "@pythia-software/query-table-core";
350
+ import { ITEM_DETAILS_COLUMN, itemPageFields, QueryError as QueryError2 } from "@pythia-software/query-table-core";
348
351
 
349
352
  // src/useItemDetails.ts
350
353
  import { useCallback as useCallback3, useEffect as useEffect3, useRef as useRef3, useState as useState3 } from "react";
@@ -577,6 +580,10 @@ function useDetailProperties(mode, schema, config, query, setQuery, enabled, rea
577
580
  }, [enabled, queryViewKey, localViewKey, properties.loading, properties.setView, setQuery]);
578
581
  return properties;
579
582
  }
583
+ function projectedFields(schema, row) {
584
+ const keys = new Set(Object.keys(row));
585
+ return schema.fields.filter((field) => keys.has(field.name) || keys.has((field.source.kind === "backend" ? field.source.path ?? field.name : field.name).split(".")[0])).map((field) => field.name);
586
+ }
580
587
  function useItemDetails({
581
588
  schema,
582
589
  query,
@@ -585,7 +592,7 @@ function useItemDetails({
585
592
  rows,
586
593
  clientRows,
587
594
  rowId,
588
- transport,
595
+ fetchRow,
589
596
  refreshKey,
590
597
  links,
591
598
  queryReady = true,
@@ -621,7 +628,9 @@ function useItemDetails({
621
628
  const hasLookup = Boolean(lookupRow);
622
629
  const hasCustomLoad = Boolean(customLoad);
623
630
  const sourceType = config?.objectType ?? detailSchema.name;
624
- const fallbackRows = !customLoad && !clientRows && !transport?.fetchRow ? rows : void 0;
631
+ const fallbackRows = !customLoad && !clientRows && !fetchRow ? rows : void 0;
632
+ const detailSchemaRef = useRef3(detailSchema);
633
+ detailSchemaRef.current = detailSchema;
625
634
  const loadRow = useCallback3(async (id, signal) => {
626
635
  if (lookupRef.current) return lookupRef.current(id) ?? null;
627
636
  if (customLoadRef.current) return customLoadRef.current(id, signal);
@@ -629,13 +638,13 @@ function useItemDetails({
629
638
  const row2 = clientRows.find((row3) => rowId(row3) === id);
630
639
  return row2 ? { row: row2 } : null;
631
640
  }
632
- if (transport?.fetchRow) {
633
- const row2 = await transport.fetchRow(id, signal);
634
- return row2 ? { row: row2 } : null;
641
+ if (fetchRow) {
642
+ const row2 = await fetchRow(id, signal);
643
+ return row2 ? { row: row2, loadedFields: projectedFields(detailSchemaRef.current, row2) } : null;
635
644
  }
636
645
  const row = rows.find((row2) => rowId(row2) === id);
637
- return row ? { row, loadedFields: Object.keys(row) } : null;
638
- }, [hasLookup, hasCustomLoad, sourceType, clientRows, transport?.fetchRow, fallbackRows, rowId]);
646
+ return row ? { row, loadedFields: projectedFields(detailSchemaRef.current, row) } : null;
647
+ }, [hasLookup, hasCustomLoad, sourceType, clientRows, fetchRow, fallbackRows, rowId]);
639
648
  const [retryKey, setRetryKey] = useState3(0);
640
649
  const [result, setResult] = useState3({ source: void 0, epoch: "", id: void 0, record: null, loading: false, error: null });
641
650
  const activeId = enabled && !lookupRow ? active?.id : void 0;
@@ -768,116 +777,53 @@ function useItemDetails({
768
777
  }
769
778
 
770
779
  // src/computedExecution.ts
771
- import {
772
- predicatesOf as predicatesOf2,
773
- isComputedField,
774
- readFieldValue
775
- } from "@pythia-software/query-table-core";
776
780
  function executionRevision(execution, id) {
777
781
  return execution?.resolvedRevisions[id] ?? execution?.resolvedRevisions[`@computed/${id}`];
778
782
  }
779
- function validateComputedResponse(request, result, schema) {
780
- const execution = result.execution;
781
- if (result.version !== 2 || !execution || execution.profile !== request.profile || request.planToken && execution.planToken !== request.planToken || request.snapshot !== void 0 && execution.snapshot !== request.snapshot)
782
- throw Error(
783
- "Server computed execution identity changed. Refresh the handshake."
784
- );
785
- const executed = /* @__PURE__ */ new Set([
786
- ...request.select.filter(isComputedField),
787
- ...request.orderBy.map((term) => term.field).filter(isComputedField),
788
- ...request.where.flatMap((term) => predicatesOf2(term).map((predicate) => predicate.field)).filter(isComputedField)
789
- ]);
790
- for (const [name, revision] of Object.entries(request.expectedRevisions)) {
791
- const id = name.replace(/^@computed\//, "");
792
- if (!executed.has(`@computed/${id}`)) continue;
793
- if (executionRevision(execution, id) !== revision)
794
- throw Error(
795
- "Server computed definition revision changed. Refresh the handshake."
796
- );
797
- }
798
- const idField = schema.fields.find((f) => f.name === schema.idField);
799
- if (!idField)
800
- throw Error("Stable row identity is required for computed values.");
801
- const ids = new Set(result.rows.map((row) => readFieldValue(idField, row)));
802
- if (ids.has(null) || ids.has(void 0) || ids.size !== result.rows.length)
803
- throw Error(
804
- "Computed response contains missing or duplicate row identities."
805
- );
806
- const sidecars = new Map(
807
- result.computed.map((item) => [item.id, item.values])
808
- );
809
- if (sidecars.size !== result.computed.length || [...sidecars.keys()].some((id) => !ids.has(id)))
810
- throw Error("Computed sidecars do not match returned row identities.");
811
- for (const field of request.select.filter(isComputedField)) {
812
- const id = field.slice("@computed/".length);
813
- for (const rowId of ids) {
814
- const value = sidecars.get(rowId)?.[id];
815
- if (!value || value.value !== null && !["string", "number", "boolean"].includes(typeof value.value) || typeof value.value === "number" && !Number.isFinite(value.value))
816
- throw Error(`Missing or invalid server computed result: ${field}.`);
817
- }
818
- }
819
- }
820
783
 
821
784
  // src/previewRows.ts
822
785
  import {
823
- predicatesOf as predicatesOf3,
786
+ predicatesOf as predicatesOf2,
824
787
  sourceExecutionSchema,
825
788
  toQueryRequestV3,
826
- toServerQuery,
827
- toServerQueryV2,
828
789
  validateQueryResultV3
829
790
  } from "@pythia-software/query-table-core";
830
791
  async function fetchPreviewRows(transport, query, schema, execution, definitions, now, signal, baseSchema = schema) {
831
792
  if (signal.aborted) throw new DOMException("Aborted", "AbortError");
832
793
  const sourceSchema = sourceExecutionSchema(baseSchema, execution, definitions);
833
- let result;
834
- const computedFilter = query.where.some((term) => predicatesOf3(term).some((predicate) => predicate.field.startsWith("@computed/")));
835
- if (transport.fetchRowsV2 && execution && !query.grouping && !computedFilter) {
836
- const request = toServerQueryV2(query, sourceSchema, execution);
837
- if (request.diagnostics.length)
838
- throw Error(request.diagnostics.map((issue) => issue.message).join(" "));
839
- for (const definition of definitions) {
840
- if (executionRevision(execution, definition.id) !== definition.revision && (request.select.includes(`@computed/${definition.id}`) || request.orderBy.some((term) => term.field === `@computed/${definition.id}`) || request.where.some((term) => predicatesOf3(term).some((predicate) => predicate.field === `@computed/${definition.id}`))))
841
- throw Error("Computed definitions changed. Refresh the server execution handshake.");
842
- }
843
- const response = await transport.fetchRowsV2(request, signal);
844
- validateComputedResponse(request, response, sourceSchema);
845
- result = response;
846
- } else if (transport.fetchQueryV3 && transport.queryCapabilities?.version === 3) {
847
- const request = toQueryRequestV3(
848
- { ...query, aggregations: [] },
849
- sourceSchema,
850
- transport.queryCapabilities,
851
- execution,
852
- { now }
853
- );
854
- if (request.diagnostics.length) throw Error(request.diagnostics.join(" "));
855
- const response = await transport.fetchQueryV3(request, signal);
856
- validateQueryResultV3(response, request, query.grouping ? schema : sourceSchema);
857
- result = { rows: response.rows, total: response.total };
858
- } else if (transport.fetchRows && !query.grouping && !(computedFilter && transport.fetchRowsV2 && execution)) {
859
- result = await transport.fetchRows(toServerQuery(query, schema), signal);
860
- } else {
861
- throw Error("Row execution requires fetchRows, negotiated fetchRowsV2, or negotiated fetchQueryV3.");
862
- }
794
+ const request = toQueryRequestV3(
795
+ { ...query, aggregations: [] },
796
+ sourceSchema,
797
+ transport.queryCapabilities,
798
+ execution,
799
+ { now }
800
+ );
801
+ const executed = /* @__PURE__ */ new Set([
802
+ ...request.result.select,
803
+ ...request.result.orderBy.map((term) => term.field),
804
+ ...[...request.source.where, ...request.result.where].flatMap((term) => predicatesOf2(term).map((predicate) => predicate.field))
805
+ ]);
806
+ for (const definition of definitions)
807
+ if (executed.has(`@computed/${definition.id}`) && executionRevision(execution, definition.id) !== definition.revision)
808
+ throw Error("Computed definitions changed. Refresh the server execution handshake.");
809
+ const response = await transport.fetchQueryV3(request, signal);
863
810
  if (signal.aborted) throw new DOMException("Aborted", "AbortError");
864
- if (!Number.isSafeInteger(result.total) || result.total < 0 || !Array.isArray(result.rows) || result.rows.length > query.limit)
865
- throw Error("Invalid preview row response.");
866
- return result;
811
+ validateQueryResultV3(response, request, query.grouping ? schema : sourceSchema);
812
+ if (response.rows.length > query.limit) throw Error("Invalid preview row response.");
813
+ return { rows: response.rows, total: response.total };
867
814
  }
868
815
 
869
816
  // src/useComputedColumns.ts
870
817
  import { useCallback as useCallback4, useEffect as useEffect4, useMemo as useMemo3, useRef as useRef4, useState as useState4 } from "react";
871
818
  import {
872
- predicatesOf as predicatesOf4,
873
819
  isGroupLinkField,
874
820
  applyQuery,
875
821
  compileFormula,
876
822
  computedFieldName,
877
823
  groupPreview,
878
- isComputedField as isComputedField2,
824
+ isComputedField,
879
825
  memoryComputedColumnStore,
880
- readFieldValue as readFieldValue2,
826
+ readFieldValue,
881
827
  validateComputedColumn,
882
828
  isSelectable,
883
829
  hasRegex
@@ -1036,7 +982,7 @@ function useComputedColumns(options) {
1036
982
  const stack = new Set(editingId ? [editingId] : []);
1037
983
  let expanded = 0;
1038
984
  const resolve = (name) => {
1039
- if (!isComputedField2(name)) return void 0;
985
+ if (!isComputedField(name)) return void 0;
1040
986
  const id = name.slice("@computed/".length), def = definitions.find((d) => d.id === id);
1041
987
  if (!def) throw new Error(`Computed column unavailable: ${id}`);
1042
988
  if (stack.has(id))
@@ -1094,7 +1040,7 @@ function useComputedColumns(options) {
1094
1040
  const values = /* @__PURE__ */ Object.create(null);
1095
1041
  for (const name of plan.dependencies) {
1096
1042
  const field = inputFields.find((f) => f.name === name);
1097
- const raw = readFieldValue2(field, row);
1043
+ const raw = readFieldValue(field, row);
1098
1044
  values[name] = raw instanceof Date ? raw.toISOString() : raw;
1099
1045
  }
1100
1046
  size += JSON.stringify(values).length;
@@ -1108,14 +1054,14 @@ function useComputedColumns(options) {
1108
1054
  [inputFields]
1109
1055
  );
1110
1056
  const activeSelect = query.select.length ? query.select : schema.defaultSelect ?? [];
1111
- const activeIds = activeSelect.filter((c) => isComputedField2(c.field)).map((c) => c.field.slice("@computed/".length)).sort().join(",");
1057
+ const activeIds = activeSelect.filter((c) => isComputedField(c.field)).map((c) => c.field.slice("@computed/".length)).sort().join(",");
1112
1058
  const [evaluated, setEvaluated] = useState4(null);
1113
1059
  useEffect4(() => {
1114
1060
  const ac = new AbortController();
1115
1061
  const values = /* @__PURE__ */ new Map();
1116
1062
  void (async () => {
1117
1063
  for (const id of activeIds.split(",").filter(Boolean)) {
1118
- if (options.execution?.fields[computedFieldName(id)]?.select && (transport?.fetchRowsV2 || transport?.fetchQueryV3))
1064
+ if (options.execution?.fields[computedFieldName(id)]?.select && transport)
1119
1065
  continue;
1120
1066
  const entry = plans.get(id);
1121
1067
  if (!entry?.plan) continue;
@@ -1167,7 +1113,7 @@ function useComputedColumns(options) {
1167
1113
  dependencies: entry?.plan ? backendDependencies(entry.plan) : [],
1168
1114
  accessor: (row) => {
1169
1115
  if (entry?.error) return { computedError: entry.error };
1170
- if (options.execution?.fields[computedFieldName(def.id)]?.select && (transport?.fetchRowsV2 || transport?.fetchQueryV3)) {
1116
+ if (options.execution?.fields[computedFieldName(def.id)]?.select && transport) {
1171
1117
  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)
1172
1118
  return {
1173
1119
  computedError: "Definition changed or values are pending. Refresh the server handshake."
@@ -1175,7 +1121,7 @@ function useComputedColumns(options) {
1175
1121
  const idField = schema.fields.find(
1176
1122
  (f) => f.name === schema.idField
1177
1123
  );
1178
- const id = idField ? readFieldValue2(idField, row) : null;
1124
+ const id = idField ? readFieldValue(idField, row) : null;
1179
1125
  const server = id === null ? void 0 : options.serverComputed?.get(id)?.[def.id];
1180
1126
  return server ? server.error ? { computedError: server.error.code } : server.value : { computedError: "Server computed result unavailable." };
1181
1127
  }
@@ -1185,16 +1131,16 @@ function useComputedColumns(options) {
1185
1131
  },
1186
1132
  filter: { enabled: options.execution?.fields[computedFieldName(def.id)]?.filter === true && executionRevision(options.execution, def.id) === def.revision && !!entry?.plan && !hasRegex(entry.plan) },
1187
1133
  sort: {
1188
- enabled: !!(transport?.fetchRowsV2 || transport?.fetchQueryV3) && !!options.execution?.fields[computedFieldName(def.id)]?.sort && executionRevision(options.execution, def.id) === def.revision && !!entry?.plan && !hasRegex(entry.plan)
1134
+ enabled: !!transport && !!options.execution?.fields[computedFieldName(def.id)]?.sort && executionRevision(options.execution, def.id) === def.revision && !!entry?.plan && !hasRegex(entry.plan)
1189
1135
  },
1190
1136
  aggregate: {
1191
1137
  measure: !!entry?.plan,
1192
- groupable: (!!metricTransport?.fetchMetrics && metricTransport.metricCapabilities?.version === 2 || !!metricTransport?.fetchQueryV3) && !!metricExecution?.fields[computedFieldName(def.id)]?.group && executionRevision(metricExecution, def.id) === def.revision && !!entry?.plan && !hasRegex(entry.plan)
1138
+ groupable: !!metricTransport && !!metricExecution?.fields[computedFieldName(def.id)]?.group && executionRevision(metricExecution, def.id) === def.revision && !!entry?.plan && !hasRegex(entry.plan)
1193
1139
  }
1194
1140
  };
1195
1141
  });
1196
1142
  for (const col of activeSelect)
1197
- if (isComputedField2(col.field) && !fields.some((f) => f.name === col.field))
1143
+ if (isComputedField(col.field) && !fields.some((f) => f.name === col.field))
1198
1144
  fields.push({
1199
1145
  name: col.field,
1200
1146
  label: col.field.slice(10),
@@ -1271,9 +1217,7 @@ function useComputedColumns(options) {
1271
1217
  offset: 0
1272
1218
  };
1273
1219
  const now = options.now ?? Date.now();
1274
- const computedFilter = request.where.some((term) => predicatesOf4(term).some((predicate) => isComputedField2(predicate.field)));
1275
- const v3 = !(transport.fetchRowsV2 && options.execution && !request.grouping && !computedFilter) && transport.fetchQueryV3 && transport.queryCapabilities?.version === 3;
1276
- const pageSize = Math.min(500, v3 ? transport.queryCapabilities.maxRows : 500);
1220
+ const pageSize = Math.min(500, transport.queryCapabilities.maxRows);
1277
1221
  if (!Number.isInteger(pageSize) || pageSize < 1) throw Error("Invalid negotiated preview row limit.");
1278
1222
  while (batch.length < count) {
1279
1223
  const res = await fetchPreviewRows(
@@ -1361,41 +1305,34 @@ import {
1361
1305
  memoryStorageAdapter,
1362
1306
  normalizeQueryState as normalizeQueryState3,
1363
1307
  parseHostQueryState,
1364
- readFieldValue as readFieldValue4,
1308
+ readFieldValue as readFieldValue3,
1365
1309
  selectedFields as selectedFields2,
1366
- toServerQuery as toServerQuery2,
1310
+ toServerQuery,
1367
1311
  queriesEqual as queriesEqual2,
1368
1312
  isWhereConjunction as isWhereConjunction2,
1369
1313
  isWhereGroup,
1370
- predicatesOf as predicatesOf6,
1314
+ predicatesOf as predicatesOf4,
1315
+ opsForField as opsForField2,
1371
1316
  metricPlans as metricPlans2,
1372
1317
  validateMetric as validateMetric2,
1373
1318
  metricDependencies as metricDependencies2,
1374
- isComputedField as isComputedField4,
1319
+ isComputedField as isComputedField3,
1375
1320
  computedFieldName as computedFieldName2,
1376
- toServerQueryV2 as toServerQueryV22,
1377
1321
  groupKeyField,
1378
1322
  groupKeyFields as groupKeyFields2
1379
1323
  } from "@pythia-software/query-table-core";
1380
1324
 
1381
1325
  // src/useRequestActivity.ts
1382
1326
  import { useCallback as useCallback5, useEffect as useEffect5, useMemo as useMemo4, useRef as useRef5, useState as useState5 } from "react";
1383
- var methods = /* @__PURE__ */ new Set([
1384
- "fetchRows",
1385
- "fetchRowsV2",
1386
- "fetchQueryV3",
1387
- "fetchMetrics",
1388
- "fetchAggregations",
1389
- "fetchRow",
1390
- "fetchDistinctValues",
1391
- "fetchFieldStats"
1392
- ]);
1327
+ function requestKind(request) {
1328
+ return request?.distinct ? "DistinctValues" : request?.rows ? "Rows" : "Metrics";
1329
+ }
1393
1330
  function observeTransport(transport, publish, nextId) {
1394
1331
  const wrappers = /* @__PURE__ */ new Map();
1395
1332
  return new Proxy(Object.create(transport), {
1396
1333
  get(_target, key) {
1397
1334
  const method = Reflect.get(transport, key, transport);
1398
- if (!methods.has(String(key)) || typeof method !== "function")
1335
+ if (key !== "fetchQueryV3" || typeof method !== "function")
1399
1336
  return method;
1400
1337
  if (!wrappers.has(key))
1401
1338
  wrappers.set(key, async (...args) => {
@@ -1403,7 +1340,7 @@ function observeTransport(transport, publish, nextId) {
1403
1340
  const start = performance.now();
1404
1341
  const entry = {
1405
1342
  id: nextId(),
1406
- kind: String(key).replace(/^fetch/, ""),
1343
+ kind: requestKind(args[0]),
1407
1344
  startedAt: Date.now(),
1408
1345
  status: "pending"
1409
1346
  };
@@ -1484,6 +1421,7 @@ import {
1484
1421
 
1485
1422
  // src/metricExecution.ts
1486
1423
  import {
1424
+ QueryError,
1487
1425
  applyAggregations,
1488
1426
  normalizeQueryState as normalizeQueryState2,
1489
1427
  evaluateMetrics,
@@ -1493,25 +1431,77 @@ import {
1493
1431
  metricPopulationError,
1494
1432
  hasMetricSeries,
1495
1433
  planMetricSeries,
1496
- predicatesOf as predicatesOf5,
1497
- toAggregationQuery,
1498
- toMetricQuery,
1434
+ predicatesOf as predicatesOf3,
1435
+ sourceExecutionSchema as sourceExecutionSchema2,
1436
+ toQueryRequestV3 as toQueryRequestV32,
1437
+ validateQueryResultV3 as validateQueryResultV32,
1499
1438
  validateMetric,
1500
1439
  selectMetricShownRows,
1501
- readFieldValue as readFieldValue3,
1440
+ readFieldValue as readFieldValue2,
1502
1441
  resolveFieldName,
1503
- isComputedField as isComputedField3
1442
+ isComputedField as isComputedField2
1504
1443
  } from "@pythia-software/query-table-core";
1505
1444
  var abort = (signal) => {
1506
1445
  if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
1507
1446
  };
1508
1447
  function usesLocalMetricPage(clause, context) {
1509
- return !context.executeV3 && clause.scope === "shownRows" && (!context.transport?.fetchMetrics || context.transport.metricCapabilities?.version !== 2 || context.transport.metricCapabilities.shownRows !== true || !!clause.dateGrouping || metricPlans(clause, context.schema, context.resolveComputed).some(hasRegex2));
1448
+ if (clause.scope !== "shownRows") return false;
1449
+ if (!context.executeV3 && !context.transport) return true;
1450
+ if (context.transport?.queryCapabilities.regexAggregateExpressions) return false;
1451
+ try {
1452
+ return metricPlans(clause, context.schema, context.resolveComputed).some(hasRegex2);
1453
+ } catch {
1454
+ return false;
1455
+ }
1456
+ }
1457
+ async function executeV3Metrics(transport, query, baseSchema, resultSchema, execution, now, signal) {
1458
+ const capabilities = transport.queryCapabilities;
1459
+ const refused = /* @__PURE__ */ new Map();
1460
+ for (const clause of query.aggregations ?? []) {
1461
+ if (clause.unsupportedOp) {
1462
+ refused.set(clause.id, new QueryError("invalid_metric", `Metric ${clause.id} uses an unsupported operation: ${clause.unsupportedOp}.`, { metricId: clause.id }));
1463
+ continue;
1464
+ }
1465
+ const issue = metricPopulationError(clause, { filters: capabilities.metricFilters, minCount: capabilities.metricMinCount, minCountMeasures: capabilities.metricMinCountMeasures });
1466
+ if (issue) refused.set(clause.id, new QueryError("capability_unsupported", issue, { metricId: clause.id }));
1467
+ }
1468
+ let runnable = (query.aggregations ?? []).filter((clause) => !refused.has(clause.id));
1469
+ const refusal = (q) => {
1470
+ try {
1471
+ toQueryRequestV32(q, baseSchema, capabilities, execution, { now, rows: false });
1472
+ } catch (error) {
1473
+ if (error instanceof QueryError) return error;
1474
+ throw error;
1475
+ }
1476
+ };
1477
+ const unpaged = { ...query, orderBy: [], offset: 0 };
1478
+ const shown = runnable.filter((clause) => clause.scope === "shownRows");
1479
+ const page = shown.length ? refusal({ ...query, aggregations: [] }) : void 0;
1480
+ if (page && !refusal({ ...unpaged, aggregations: [] })) {
1481
+ for (const clause of shown) refused.set(clause.id, page);
1482
+ runnable = runnable.filter((clause) => clause.scope !== "shownRows");
1483
+ }
1484
+ let metrics = [];
1485
+ if (runnable.length) {
1486
+ const paged = runnable.some((clause) => clause.scope === "shownRows");
1487
+ const request = toQueryRequestV32({ ...paged ? query : unpaged, aggregations: runnable }, baseSchema, capabilities, execution, { now, rows: false });
1488
+ const result = await transport.fetchQueryV3(request, signal);
1489
+ validateQueryResultV32(result, request, resultSchema);
1490
+ metrics = result.metrics;
1491
+ }
1492
+ if (!refused.size) return { metrics };
1493
+ const byId = new Map(metrics.map((metric) => [metric.id, metric]));
1494
+ return { metrics: (query.aggregations ?? []).flatMap((clause) => {
1495
+ const issue = refused.get(clause.id);
1496
+ if (issue) return [{ id: clause.id, buckets: [], error: issue.message, errorCode: issue.code }];
1497
+ const metric = byId.get(clause.id);
1498
+ return metric ? [metric] : [];
1499
+ }) };
1510
1500
  }
1511
1501
  async function localMetrics(rows, query, context, signal) {
1512
1502
  const originalClauses = query.aggregations ?? [];
1513
1503
  const advanced = (query.aggregations ?? []).filter(
1514
- (c) => context.strictLocalMetrics || c.dateGrouping || 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 || c.where !== void 0 || c.minCount !== void 0 || hasMetricSeries(c)
1504
+ (c) => context.strictLocalMetrics || c.dateGrouping || isComputedField2(c.field ?? "") || c.groupBy.some(isComputedField2) || c.diagnostics?.length || c.expression !== void 0 || c.expressionY !== void 0 || c.scope !== void 0 || c.distribution || c.sort || c.groupLimit !== void 0 || c.where !== void 0 || c.minCount !== void 0 || hasMetricSeries(c)
1515
1505
  );
1516
1506
  if (advanced.length && (originalClauses.length > 20 || normalizeQueryState2(query).aggregations?.length !== originalClauses.length))
1517
1507
  return evaluateMetrics(rows, query, context.schema, {
@@ -1545,7 +1535,7 @@ async function localMetrics(rows, query, context, signal) {
1545
1535
  context.resolveComputed
1546
1536
  );
1547
1537
  if (diagnostics.length)
1548
- throw Error(diagnostics.map((d) => d.message).join(" "));
1538
+ throw new QueryError("invalid_metric", diagnostics.map((d) => d.message).join(" "), { metricId: clause.id });
1549
1539
  }
1550
1540
  const requiresPage = (query.aggregations ?? []).some(
1551
1541
  (clause) => clause.scope === "shownRows"
@@ -1573,7 +1563,7 @@ async function localMetrics(rows, query, context, signal) {
1573
1563
  for (const name of plan.dependencies) {
1574
1564
  const field = context.schema.fields.find((f) => f.name === name);
1575
1565
  if (!field) throw Error(`Unavailable metric input: ${name}`);
1576
- const raw = readFieldValue3(field, row);
1566
+ const raw = readFieldValue2(field, row);
1577
1567
  values2[name] = raw instanceof Date ? raw.toISOString() : raw ?? null;
1578
1568
  }
1579
1569
  bytes += JSON.stringify(values2).length;
@@ -1631,14 +1621,33 @@ async function executeMetrics(query, context, signal) {
1631
1621
  );
1632
1622
  return { metrics: series.combine(parts.flatMap((part) => part.metrics)) };
1633
1623
  }
1634
- if (context.executeV3) return context.executeV3(query, signal);
1624
+ if (context.executeV3 || context.transport) {
1625
+ const page = clauses.filter((c) => usesLocalMetricPage(c, context));
1626
+ const remote = (q) => context.executeV3 ? context.executeV3(q, signal) : executeV3Metrics(
1627
+ context.transport,
1628
+ q,
1629
+ context.schema,
1630
+ sourceExecutionSchema2(context.schema, context.execution),
1631
+ context.execution,
1632
+ context.now ?? Date.now(),
1633
+ signal
1634
+ );
1635
+ if (!page.length) return remote(query);
1636
+ const { executeV3: _executeV3, transport: _transport, ...local } = context;
1637
+ const rest = clauses.filter((c) => !page.includes(c));
1638
+ const [own, served] = await Promise.all([
1639
+ executeMetrics({ ...query, aggregations: page }, local, signal),
1640
+ rest.length ? remote({ ...query, aggregations: rest }) : { metrics: [] }
1641
+ ]);
1642
+ const byId2 = new Map([...own.metrics, ...served.metrics].map((metric) => [metric.id, metric]));
1643
+ return { metrics: clauses.flatMap((c) => byId2.get(c.id) ?? []) };
1644
+ }
1635
1645
  if (context.rowsReady === false && clauses.some((c) => usesLocalMetricPage(c, context)))
1636
1646
  throw Error("Wait for the current table page before previewing shown-row metrics.");
1637
- const supportsV2 = !!context.transport?.fetchMetrics && context.transport.metricCapabilities?.version === 2;
1638
- if (context.clientRows && !supportsV2 && !context.transport?.fetchAggregations)
1647
+ if (context.clientRows)
1639
1648
  return localMetrics(context.clientRows, query, context, signal);
1640
1649
  const shown = clauses.filter((c) => usesLocalMetricPage(c, context));
1641
- let matching = clauses.filter((c) => !shown.includes(c));
1650
+ const matching = clauses.filter((c) => !shown.includes(c));
1642
1651
  const results = [];
1643
1652
  if (shown.length) {
1644
1653
  if (!context.rowsReady || !context.shownRows)
@@ -1649,7 +1658,7 @@ async function executeMetrics(query, context, signal) {
1649
1658
  shown.flatMap((clause) => [
1650
1659
  ...clause.groupBy,
1651
1660
  ...(clause.where ?? []).flatMap(
1652
- (term) => predicatesOf5(term).map((predicate) => predicate.field)
1661
+ (term) => predicatesOf3(term).map((predicate) => predicate.field)
1653
1662
  ),
1654
1663
  // Includes a measured minimum's field, which the page must also hold.
1655
1664
  ...metricDependencies(clause, context.schema, context.resolveComputed)
@@ -1681,103 +1690,10 @@ async function executeMetrics(query, context, signal) {
1681
1690
  ...(await localMetrics(context.shownRows, pageQuery, context, signal)).metrics
1682
1691
  );
1683
1692
  }
1684
- const unsupported = (clause, error) => results.push({ id: clause.id, buckets: [], error });
1685
- const calendar = matching.filter((c) => c.dateGrouping);
1686
- for (const clause of calendar)
1687
- unsupported(
1688
- clause,
1689
- "This server does not support calendar grouping. Enable the v3 query executor."
1693
+ if (matching.length)
1694
+ throw Error(
1695
+ "Remote all-matching metrics require a metric-capable server transport."
1690
1696
  );
1691
- matching = matching.filter((c) => !c.dateGrouping);
1692
- {
1693
- const transport = context.transport;
1694
- if (transport?.fetchMetrics || transport?.fetchAggregations) {
1695
- const support = transport.fetchMetrics && transport.metricCapabilities?.version === 2 ? transport.metricCapabilities : {};
1696
- matching = matching.filter((c) => {
1697
- const error = metricPopulationError(c, support);
1698
- if (error) unsupported(c, error);
1699
- return !error;
1700
- });
1701
- }
1702
- }
1703
- if (matching.length) {
1704
- const transport = context.transport;
1705
- if (transport?.fetchMetrics && transport.metricCapabilities?.version === 2) {
1706
- const capabilities = transport.metricCapabilities;
1707
- const request = toMetricQuery(
1708
- { ...query, aggregations: matching },
1709
- context.schema,
1710
- context.execution
1711
- );
1712
- if (request.diagnostics.length)
1713
- throw Error(request.diagnostics.map((d) => d.message).join(" "));
1714
- if (request.profile && capabilities.profile && request.profile !== capabilities.profile)
1715
- throw Error(
1716
- "Metric transport and computed execution profiles do not match. Refresh the handshake."
1717
- );
1718
- const remoteFields = new Map(
1719
- context.schema.fields.map((field) => [field.name, field])
1720
- );
1721
- for (const [name, capability] of Object.entries(
1722
- context.execution?.fields ?? {}
1723
- ))
1724
- if (isComputedField3(name))
1725
- remoteFields.set(name, {
1726
- name,
1727
- label: name,
1728
- type: capability.type,
1729
- source: { kind: "backend" }
1730
- });
1731
- const remoteSchema = {
1732
- ...context.schema,
1733
- fields: [...remoteFields.values()]
1734
- };
1735
- const wire = new Map(request.metrics.map((m) => [m.id, m]));
1736
- const limitation = (clause) => {
1737
- if (clause.scope === "shownRows" && !capabilities.shownRows)
1738
- return "This server does not support shown-row metrics.";
1739
- if (!capabilities.computedFields && [
1740
- ...clause.groupBy,
1741
- ...metricPlans(clause, remoteSchema).flatMap(
1742
- (plan) => plan.dependencies
1743
- )
1744
- ].some(isComputedField3))
1745
- return "This metric server does not support computed fields.";
1746
- const sent = wire.get(clause.id);
1747
- if (!capabilities.expressions && sent && !sent.distribution && (sent.expression !== void 0 || sent.expressionY !== void 0))
1748
- return "This server does not support composed metric expressions.";
1749
- if (!capabilities.distributions && clause.distribution && (!clause.display || ["box", "histogram"].includes(clause.display.kind)))
1750
- return "This server does not support distributions.";
1751
- return void 0;
1752
- };
1753
- const rejected = /* @__PURE__ */ new Set();
1754
- for (const clause of matching) {
1755
- const error = limitation(clause);
1756
- if (error) {
1757
- rejected.add(clause.id);
1758
- unsupported(clause, error);
1759
- }
1760
- }
1761
- const metrics = request.metrics.filter((m) => !rejected.has(m.id));
1762
- if (metrics.length)
1763
- results.push(
1764
- ...(await transport.fetchMetrics({ ...request, metrics }, signal)).metrics
1765
- );
1766
- } else if (transport?.fetchAggregations) {
1767
- const request = toAggregationQuery(
1768
- { ...query, aggregations: matching },
1769
- context.schema
1770
- );
1771
- if (request.diagnostics?.length)
1772
- throw Error(request.diagnostics.map((d) => d.message).join(" "));
1773
- results.push(
1774
- ...(await transport.fetchAggregations(request, signal)).metrics
1775
- );
1776
- } else
1777
- throw Error(
1778
- "Remote all-matching metrics require a metric-capable server transport."
1779
- );
1780
- }
1781
1697
  abort(signal);
1782
1698
  const byId = new Map(results.map((r) => [r.id, r]));
1783
1699
  return {
@@ -1785,7 +1701,8 @@ async function executeMetrics(query, context, signal) {
1785
1701
  (c) => byId.get(c.id) ?? {
1786
1702
  id: c.id,
1787
1703
  buckets: [],
1788
- error: "Server omitted this metric."
1704
+ error: "Server omitted this metric.",
1705
+ errorCode: "internal"
1789
1706
  }
1790
1707
  )
1791
1708
  };
@@ -1823,7 +1740,7 @@ function useMetricBatch(query, clauses, context, debounceMs, nonce, validateQuer
1823
1740
  context.queryContextKey,
1824
1741
  page ? context.shownFields : null
1825
1742
  ]);
1826
- const localDataset = !context.executeV3 && !(context.transport?.fetchMetrics && context.transport.metricCapabilities?.version === 2) && !context.transport?.fetchAggregations;
1743
+ const localDataset = !context.executeV3 && !context.transport;
1827
1744
  const pageReady = context.rowsReady ?? (localDataset && context.clientRows !== void 0);
1828
1745
  const validationToken = useMemo5(() => ({}), [
1829
1746
  key,
@@ -2056,12 +1973,10 @@ function useSelect(query, setQuery, schema) {
2056
1973
  return schema.fields.filter((f) => !shown.has(f.name) && (f.select?.enabled ?? true));
2057
1974
  }, [schema, fields]);
2058
1975
  const writeSelect = useCallback8(
2059
- (mutate, clearFitToScreen = false) => {
1976
+ (mutate) => {
2060
1977
  setQuery((prev) => {
2061
1978
  const current = prev.select.length ? prev.select : visible;
2062
- const next = { ...prev, select: mutate([...current]) };
2063
- if (clearFitToScreen) delete next.fitToScreen;
2064
- return next;
1979
+ return { ...prev, select: mutate([...current]) };
2065
1980
  });
2066
1981
  },
2067
1982
  [setQuery, visible]
@@ -2079,7 +1994,7 @@ function useSelect(query, setQuery, schema) {
2079
1994
  [writeSelect]
2080
1995
  );
2081
1996
  const setWidth = useCallback8(
2082
- (field, width) => writeSelect((c) => c.map((x) => x.field === field ? { ...x, width: Math.round(width) } : x), true),
1997
+ (field, width) => writeSelect((c) => c.map((x) => x.field === field ? { ...x, width: Math.round(width) } : x)),
2083
1998
  [writeSelect]
2084
1999
  );
2085
2000
  const reset = useCallback8(() => setQuery((prev) => ({ ...prev, select: [] })), [setQuery]);
@@ -2372,7 +2287,7 @@ function cloneQueryState(q) {
2372
2287
  limit: q.limit,
2373
2288
  offset: q.offset
2374
2289
  };
2375
- if (q.fitToScreen) clone.fitToScreen = true;
2290
+ if (q.fit) clone.fit = q.fit;
2376
2291
  if (q.hostState) clone.hostState = { version: q.hostState.version, params: Object.fromEntries(Object.entries(q.hostState.params).map(([name, values]) => [name, [...values]])) };
2377
2292
  if (q.aggregations)
2378
2293
  clone.aggregations = JSON.parse(
@@ -2401,7 +2316,7 @@ function asDistinctValue(value) {
2401
2316
  }
2402
2317
  function fieldHasNullInRows(field, rows) {
2403
2318
  for (const row of rows) {
2404
- if (isNullLike(readFieldValue4(field, row))) return true;
2319
+ if (isNullLike(readFieldValue3(field, row))) return true;
2405
2320
  }
2406
2321
  return false;
2407
2322
  }
@@ -2411,7 +2326,7 @@ function distinctValuesFromRows(field, rows, search) {
2411
2326
  const seen = /* @__PURE__ */ new Set();
2412
2327
  let hasMore = false;
2413
2328
  rowsLoop: for (const row of rows) {
2414
- const value = readFieldValue4(field, row);
2329
+ const value = readFieldValue3(field, row);
2415
2330
  const candidates = field.type === "textarray" && Array.isArray(value) ? value : [value];
2416
2331
  for (const candidate of candidates) {
2417
2332
  const raw = asDistinctValue(candidate);
@@ -2465,7 +2380,6 @@ function writeHostParameters(url, host, state) {
2465
2380
  for (const value of state?.params[name] ?? []) next.searchParams.append(name, value);
2466
2381
  return next.toString();
2467
2382
  }
2468
- var HOST_REQUEST_METHODS = /* @__PURE__ */ new Set(["fetchRows", "fetchRowsV2", "fetchQueryV3", "fetchMetrics", "fetchAggregations", "fetchDistinctValues"]);
2469
2383
  function withHostTransport(transport, hostStateKey) {
2470
2384
  if (!transport || !hostStateKey) return transport;
2471
2385
  const hostState = JSON.parse(hostStateKey);
@@ -2473,7 +2387,7 @@ function withHostTransport(transport, hostStateKey) {
2473
2387
  return new Proxy(Object.create(transport), {
2474
2388
  get(_target, key) {
2475
2389
  const method = Reflect.get(transport, key, transport);
2476
- if (!HOST_REQUEST_METHODS.has(String(key)) || typeof method !== "function") return method;
2390
+ if (key !== "fetchQueryV3" || typeof method !== "function") return method;
2477
2391
  if (!wrappers.has(key))
2478
2392
  wrappers.set(key, (request, signal) => method.call(transport, { ...request, hostState }, signal));
2479
2393
  return wrappers.get(key);
@@ -2571,10 +2485,10 @@ function useQueryTable(opts) {
2571
2485
  const metadataTransport = useMemo9(() => withHostTransport(activityTransport, hostStateKey), [activityTransport, hostStateKey]);
2572
2486
  const metricTransport = useMemo9(() => withHostTransport(activityMetricTransport, hostStateKey), [activityMetricTransport, hostStateKey]);
2573
2487
  const transport = metadataTransport;
2574
- const sourceCatalogue = useMemo9(() => sourceExecutionSchema2(sourceSchema, opts.computedExecution), [sourceSchema, opts.computedExecution]);
2488
+ const sourceCatalogue = useMemo9(() => sourceExecutionSchema3(sourceSchema, opts.computedExecution), [sourceSchema, opts.computedExecution]);
2575
2489
  const sourceRows = providedRows;
2576
2490
  const groupingModel = useMemo9(() => createGroupingModel(sourceCatalogue), [sourceCatalogue]);
2577
- const remoteGrouping = !!transport?.fetchQueryV3 && transport.queryCapabilities?.version === 3 && transport.queryCapabilities.groupedRows;
2491
+ const remoteGrouping = !!transport && transport.queryCapabilities.version === 3 && transport.queryCapabilities.groupedRows;
2578
2492
  const groupingSupported = (!transport && sourceRows !== void 0 || remoteGrouping) && opts.grouping !== false;
2579
2493
  const groupKey = JSON.stringify(query.grouping ?? null);
2580
2494
  const groupingNow = useMemo9(
@@ -2775,10 +2689,10 @@ function useQueryTable(opts) {
2775
2689
  ...clientRows ? { clientRows } : {}
2776
2690
  });
2777
2691
  const resolveComputed = useCallback12(
2778
- (name) => isComputedField4(name) ? computed.compile(`[${name.replace(/]/g, "]] ".trim())}]`) : void 0,
2692
+ (name) => isComputedField3(name) ? computed.compile(`[${name.replace(/]/g, "]] ".trim())}]`) : void 0,
2779
2693
  [computed.compile]
2780
2694
  );
2781
- const metricCompileSchema = useMemo9(() => opts.metricExecution ? sourceExecutionSchema2(displaySchema, opts.metricExecution, computed.definitions) : displaySchema, [displaySchema, opts.metricExecution, computed.definitions]);
2695
+ const metricCompileSchema = useMemo9(() => opts.metricExecution ? sourceExecutionSchema3(displaySchema, opts.metricExecution, computed.definitions) : displaySchema, [displaySchema, opts.metricExecution, computed.definitions]);
2782
2696
  const metricCompile = useCallback12(
2783
2697
  (clause) => {
2784
2698
  const diagnostics = validateMetric2(
@@ -2787,7 +2701,7 @@ function useQueryTable(opts) {
2787
2701
  resolveComputed
2788
2702
  );
2789
2703
  if (diagnostics.length)
2790
- throw new Error(diagnostics.map((d) => d.message).join(" "));
2704
+ throw new QueryError2("invalid_metric", diagnostics.map((d) => d.message).join(" "), { metricId: clause.id });
2791
2705
  return metricPlans2(clause, metricCompileSchema, resolveComputed);
2792
2706
  },
2793
2707
  [metricCompileSchema, resolveComputed]
@@ -2799,7 +2713,6 @@ function useQueryTable(opts) {
2799
2713
  const definitionKey = JSON.stringify(computedRevisions);
2800
2714
  const queryKey = useMemo9(() => JSON.stringify(query), [query]);
2801
2715
  const serverRowQuery = useMemo9(() => {
2802
- const execution2 = opts.computedExecution;
2803
2716
  const detailConfig = typeof opts.itemDetails === "object" ? opts.itemDetails : void 0;
2804
2717
  const page = query.itemDetails?.page ?? detailConfig?.page;
2805
2718
  const pageEnabled = Boolean(opts.itemDetails) && (query.itemDetails?.modes ?? initialDetailModes(detailConfig)).includes("page") && (!detailConfig?.modes || detailConfig.modes.includes("page"));
@@ -2808,20 +2721,7 @@ function useQueryTable(opts) {
2808
2721
  ...query.select.length ? query.select : defaults.select,
2809
2722
  ...pageFields.filter((name) => displaySchema.fields.some((field) => field.name === name)).map((field) => ({ field }))
2810
2723
  ] } : executionQuery;
2811
- const v2Request = execution2 && transport?.fetchRowsV2 ? toServerQueryV22(requestQuery, displaySchema, execution2) : void 0;
2812
- const request = v2Request ?? toServerQuery2(requestQuery, displaySchema);
2813
- if (v2Request)
2814
- for (const definition of computed.definitions) {
2815
- if ((execution2?.fields[computedFieldName2(definition.id)]?.select || v2Request.orderBy.some(
2816
- (term) => term.field === computedFieldName2(definition.id)
2817
- )) && executionRevision(execution2, definition.id) !== void 0 && executionRevision(execution2, definition.id) !== definition.revision)
2818
- v2Request.diagnostics.push({
2819
- from: 0,
2820
- to: 1,
2821
- code: "definition_changed",
2822
- message: "Computed definitions changed. Refresh the server execution handshake."
2823
- });
2824
- }
2724
+ const request = toServerQuery(requestQuery, displaySchema);
2825
2725
  for (const clause of query.aggregations ?? [])
2826
2726
  if (clause.scope === "shownRows") {
2827
2727
  try {
@@ -2844,16 +2744,14 @@ function useQueryTable(opts) {
2844
2744
  }, [
2845
2745
  queryKey,
2846
2746
  displaySchema,
2847
- transport,
2848
- computedRevisions,
2849
2747
  schema,
2850
2748
  resolveComputed,
2851
- opts.computedExecution,
2852
2749
  opts.itemDetails,
2853
2750
  defaults
2854
2751
  ]);
2752
+ const serverDefinitionKey = JSON.stringify(computed.definitions.flatMap((definition) => executionRevision(opts.computedExecution, definition.id) !== void 0 ? [[definition.id, definition.revision]] : []));
2855
2753
  const rowQueryKey = useMemo9(
2856
- () => transport ? `server:${JSON.stringify(serverRowQuery)}:${transport.fetchQueryV3 ? JSON.stringify([
2754
+ () => transport ? `server:${JSON.stringify(serverRowQuery)}:${serverDefinitionKey}:${JSON.stringify([
2857
2755
  groupKey,
2858
2756
  groupingNow,
2859
2757
  query.select.map((column) => column.field),
@@ -2861,10 +2759,11 @@ function useQueryTable(opts) {
2861
2759
  query.orderBy,
2862
2760
  query.limit,
2863
2761
  query.offset
2864
- ]) : ""}` : `client:${JSON.stringify([query.where, query.orderBy, query.limit, query.offset])}`,
2762
+ ])}` : `client:${JSON.stringify([query.where, query.orderBy, query.limit, query.offset])}`,
2865
2763
  [
2866
2764
  transport,
2867
2765
  serverRowQuery,
2766
+ serverDefinitionKey,
2868
2767
  groupKey,
2869
2768
  groupingNow,
2870
2769
  query.select,
@@ -2893,67 +2792,37 @@ function useQueryTable(opts) {
2893
2792
  try {
2894
2793
  validateExecution(query);
2895
2794
  if (transport) {
2896
- if (transport.fetchQueryV3 && transport.queryCapabilities?.version === 3 && (query.grouping || query.select.some((column) => isComputedField4(column.field)) || query.orderBy.some((term) => isComputedField4(term.field)) || query.where.some((term) => predicatesOf6(term).some((predicate) => isComputedField4(predicate.field))) || !transport.fetchRows && !(transport.fetchRowsV2 && opts.computedExecution))) {
2897
- const request = toQueryRequestV32(
2898
- { ...query, aggregations: [], select: [
2899
- ...query.select.filter((column) => isComputedField4(column.field)),
2900
- ...serverRowQuery.select.map((field) => ({ field }))
2901
- ] },
2902
- sourceSchema,
2903
- transport.queryCapabilities,
2904
- opts.computedExecution,
2905
- { now: groupingNow ?? (opts.evaluationNow ?? Date.now)() }
2906
- );
2907
- if (request.diagnostics.length) throw Error(request.diagnostics.join(" "));
2908
- const result = await transport.fetchQueryV3(request, ac.signal);
2909
- validateQueryResultV32(result, request, query.grouping ? schema : sourceCatalogue);
2910
- if (!cancelled) {
2911
- setCommittedFields(request.result.select);
2912
- setServerExecution(result.execution);
2913
- setServerComputed(new Map(result.rows.map((row) => [
2914
- row[schema.idField],
2915
- Object.fromEntries(Object.entries(row).filter(([name]) => isComputedField4(name)).map(([name, value]) => [name.slice(10), value && typeof value === "object" && "computedError" in value ? { value: null, error: { code: String(value.computedError) } } : { value }]))
2916
- ])));
2917
- setRows(result.rows);
2918
- setTotal(result.total);
2919
- setCommittedPage(pageContext);
2920
- setCommittedSource({ schema, transport, clientRows });
2921
- }
2922
- return;
2795
+ const request = toQueryRequestV33(
2796
+ { ...query, aggregations: [], select: [
2797
+ ...query.select.filter((column) => isComputedField3(column.field)),
2798
+ ...serverRowQuery.select.map((field) => ({ field }))
2799
+ ] },
2800
+ sourceSchema,
2801
+ transport.queryCapabilities,
2802
+ opts.computedExecution,
2803
+ { now: groupingNow ?? (opts.evaluationNow ?? Date.now)() }
2804
+ );
2805
+ const executed = /* @__PURE__ */ new Set([
2806
+ ...request.result.select,
2807
+ ...request.result.orderBy.map((term) => term.field),
2808
+ ...[...request.source.where, ...request.result.where].flatMap((term) => predicatesOf4(term).map((predicate) => predicate.field))
2809
+ ]);
2810
+ for (const definition of computed.definitions) {
2811
+ const revision = executionRevision(opts.computedExecution, definition.id);
2812
+ if (executed.has(computedFieldName2(definition.id)) && revision !== void 0 && revision !== definition.revision)
2813
+ throw Error("Computed definitions changed. Refresh the server execution handshake.");
2923
2814
  }
2924
- const useV2 = "version" in serverRowQuery && transport.fetchRowsV2;
2925
- if (!useV2 && !transport.fetchRows)
2926
- throw Error("Row execution requires fetchRows, negotiated fetchRowsV2, or negotiated fetchQueryV3.");
2927
- if ("diagnostics" in serverRowQuery && Array.isArray(serverRowQuery.diagnostics) && serverRowQuery.diagnostics.length)
2928
- throw Error(
2929
- serverRowQuery.diagnostics.map((d) => d.message).join(" ")
2930
- );
2931
- if (!useV2 && query.orderBy.some((s) => isComputedField4(s.field)))
2932
- throw Error(
2933
- "Global computed sorting requires a server execution handshake and fetchRowsV2."
2934
- );
2935
- const res = useV2 ? await transport.fetchRowsV2(
2936
- serverRowQuery,
2937
- ac.signal
2938
- ) : await transport.fetchRows(serverRowQuery, ac.signal);
2939
- if (useV2)
2940
- validateComputedResponse(
2941
- serverRowQuery,
2942
- res,
2943
- schema
2944
- );
2815
+ const result = await transport.fetchQueryV3(request, ac.signal);
2816
+ validateQueryResultV33(result, request, query.grouping ? schema : sourceCatalogue);
2945
2817
  if (!cancelled) {
2946
- setCommittedFields(serverRowQuery.select);
2947
- setServerExecution(
2948
- useV2 ? res.execution : void 0
2949
- );
2950
- if ("computed" in res && Array.isArray(res.computed))
2951
- setServerComputed(
2952
- new Map(res.computed.map((c) => [c.id, c.values]))
2953
- );
2954
- else setServerComputed(/* @__PURE__ */ new Map());
2955
- setRows(res.rows);
2956
- setTotal(res.total);
2818
+ setCommittedFields(request.result.select);
2819
+ setServerExecution(result.execution);
2820
+ setServerComputed(new Map(result.rows.map((row) => [
2821
+ row[schema.idField],
2822
+ Object.fromEntries(Object.entries(row).filter(([name]) => isComputedField3(name)).map(([name, value]) => [name.slice(10), value && typeof value === "object" && "computedError" in value ? { value: null, error: { code: String(value.computedError) } } : { value }]))
2823
+ ])));
2824
+ setRows(result.rows);
2825
+ setTotal(result.total);
2957
2826
  setCommittedPage(pageContext);
2958
2827
  setCommittedSource({ schema, transport, clientRows });
2959
2828
  }
@@ -3016,7 +2885,7 @@ function useQueryTable(opts) {
3016
2885
  );
3017
2886
  const rowId = useCallback12(
3018
2887
  (row) => (() => {
3019
- const value = idField ? readFieldValue4(idField, row) : row?.[schema.idField];
2888
+ const value = idField ? readFieldValue3(idField, row) : row?.[schema.idField];
3020
2889
  return typeof value === "string" || typeof value === "number" && Number.isFinite(value) ? value : null;
3021
2890
  })(),
3022
2891
  [idField, schema.idField]
@@ -3075,55 +2944,33 @@ function useQueryTable(opts) {
3075
2944
  const metricExecution = opts.metricExecution ?? (opts.computedExecution ? pageReady && serverExecution ? serverExecution : opts.computedExecution : void 0);
3076
2945
  const v3MetricExecution = opts.metricExecution ?? opts.computedExecution;
3077
2946
  const metricSourceCatalogue = useMemo9(
3078
- () => sourceExecutionSchema2(sourceSchema, v3MetricExecution),
2947
+ () => sourceExecutionSchema3(sourceSchema, v3MetricExecution),
3079
2948
  [sourceSchema, v3MetricExecution]
3080
2949
  );
3081
- const executeV3Metrics = useCallback12(async (metricQuery, signal) => {
3082
- if (!metricTransport?.fetchQueryV3 || metricTransport.queryCapabilities?.version !== 3) throw Error("The v3 query executor is unavailable.");
3083
- const capabilities = metricTransport.queryCapabilities;
3084
- const refused = /* @__PURE__ */ new Map();
3085
- for (const clause of metricQuery.aggregations ?? []) {
3086
- const issue = metricPopulationError2(clause, { filters: capabilities.metricFilters, minCount: capabilities.metricMinCount, minCountMeasures: capabilities.metricMinCountMeasures });
3087
- if (issue) refused.set(clause.id, issue);
3088
- }
3089
- const runnable = (metricQuery.aggregations ?? []).filter((clause) => !refused.has(clause.id));
3090
- let metrics = [];
3091
- if (runnable.length) {
3092
- const scoped = { ...metricQuery, aggregations: runnable };
3093
- const request = toQueryRequestV32(
3094
- query.grouping ? { ...scoped, grouping: query.grouping } : scoped,
3095
- sourceSchema,
3096
- capabilities,
3097
- v3MetricExecution,
3098
- { now: groupingNow ?? (opts.evaluationNow ?? Date.now)(), rows: false }
3099
- );
3100
- if (request.diagnostics.length) throw Error(request.diagnostics.join(" "));
3101
- const result = await metricTransport.fetchQueryV3(request, signal);
3102
- validateQueryResultV32(result, request, query.grouping ? schema : metricSourceCatalogue);
3103
- metrics = result.metrics;
3104
- }
3105
- if (!refused.size) return { metrics };
3106
- const byId = new Map(metrics.map((metric) => [metric.id, metric]));
3107
- return { metrics: (metricQuery.aggregations ?? []).flatMap((clause) => {
3108
- const issue = refused.get(clause.id);
3109
- if (issue) return [{ id: clause.id, buckets: [], error: issue }];
3110
- const metric = byId.get(clause.id);
3111
- return metric ? [metric] : [];
3112
- }) };
2950
+ const executeV3Metrics2 = useCallback12(async (metricQuery, signal) => {
2951
+ if (!metricTransport) throw Error("The v3 query executor is unavailable.");
2952
+ return executeV3Metrics(
2953
+ metricTransport,
2954
+ query.grouping ? { ...metricQuery, grouping: query.grouping } : metricQuery,
2955
+ sourceSchema,
2956
+ query.grouping ? schema : metricSourceCatalogue,
2957
+ v3MetricExecution,
2958
+ groupingNow ?? (opts.evaluationNow ?? Date.now)(),
2959
+ signal
2960
+ );
3113
2961
  }, [metricTransport, sourceSchema, metricSourceCatalogue, schema, v3MetricExecution, groupKey, groupingNow, opts.evaluationNow]);
3114
2962
  const previewGrouping = useCallback12(async (draft, signal) => {
3115
2963
  if (!active || !storageReady || restoration.loading || restoration.error) throw Error("Wait for query restoration before previewing groups.");
3116
- if (!transport?.fetchQueryV3 || !transport.queryCapabilities?.groupedRows || !draft.grouping) throw Error("The server group preview is unavailable.");
3117
- const request = toQueryRequestV32(
2964
+ if (!transport?.queryCapabilities.groupedRows || !draft.grouping) throw Error("The server group preview is unavailable.");
2965
+ const request = toQueryRequestV33(
3118
2966
  { ...draft, aggregations: [] },
3119
2967
  sourceSchema,
3120
2968
  transport.queryCapabilities,
3121
2969
  opts.computedExecution,
3122
2970
  { now: groupingNow ?? (opts.evaluationNow ?? Date.now)() }
3123
2971
  );
3124
- if (request.diagnostics.length) throw Error(request.diagnostics.join(" "));
3125
2972
  const result = await transport.fetchQueryV3(request, signal);
3126
- validateQueryResultV32(result, request, groupingModel.resultSchema(draft.grouping.keys, draft.grouping.summaries));
2973
+ validateQueryResultV33(result, request, groupingModel.resultSchema(draft.grouping.keys, draft.grouping.summaries));
3127
2974
  return { rows: result.rows, total: result.total, sourceTotal: result.sourceTotal };
3128
2975
  }, [transport, sourceSchema, opts.computedExecution, groupingModel, groupingNow, opts.evaluationNow, active, storageReady, restoration.loading, restoration.error]);
3129
2976
  const aggState = useAggregations(
@@ -3135,7 +2982,7 @@ function useQueryTable(opts) {
3135
2982
  nonce,
3136
2983
  validateExecution,
3137
2984
  {
3138
- ...metricTransport?.fetchQueryV3 && metricTransport.queryCapabilities?.version === 3 && (query.grouping || query.aggregations?.some((metric) => metric.dateGrouping) || query.where.some((term) => predicatesOf6(term).some((predicate) => isComputedField4(predicate.field)))) ? { executeV3: executeV3Metrics, queryContextKey: groupKey } : {},
2985
+ ...metricTransport ? { executeV3: executeV3Metrics2, queryContextKey: groupKey } : {},
3139
2986
  ...groupingNow !== void 0 ? { now: groupingNow, strictLocalMetrics: true } : {},
3140
2987
  shownRows: rows,
3141
2988
  rowsReady: metricRowsReady,
@@ -3279,9 +3126,24 @@ function useQueryTable(opts) {
3279
3126
  () => setQuery((q) => ({ ...q, offset: Math.max(0, q.offset - q.limit) })),
3280
3127
  [setQuery]
3281
3128
  );
3129
+ const auxiliaryRef = useRef9({ evaluationNow: opts.evaluationNow, execution: opts.computedExecution });
3130
+ auxiliaryRef.current = { evaluationNow: opts.evaluationNow, execution: opts.computedExecution };
3131
+ const auxiliaryOptions = useCallback12(() => {
3132
+ const { evaluationNow, execution: execution2 } = auxiliaryRef.current;
3133
+ return { now: (evaluationNow ?? Date.now)(), ...execution2 ? { execution: execution2 } : {} };
3134
+ }, []);
3135
+ const remoteDistinct = useCallback12(async (field, search, signal) => {
3136
+ const definition = sourceCatalogue.fields.find((f) => f.name === field);
3137
+ if (!metadataTransport || !definition || !isPushdownFilter(definition)) return { values: [], hasMore: false };
3138
+ return fetchDistinctValuesV3(metadataTransport, sourceCatalogue, field, search.slice(0, 2e3), { ...auxiliaryOptions(), limit: AUTOCOMPLETE_LIMIT }, signal);
3139
+ }, [metadataTransport, sourceCatalogue, auxiliaryOptions]);
3140
+ const remoteFieldStats = useCallback12(
3141
+ async (fields, signal) => metadataTransport && opts.remoteFieldStats ? fetchFieldStatsV3(metadataTransport, sourceCatalogue, fields, auxiliaryOptions(), signal) : {},
3142
+ [metadataTransport, sourceCatalogue, auxiliaryOptions, opts.remoteFieldStats]
3143
+ );
3282
3144
  const filterValues = useCallback12(
3283
- async (field, search) => {
3284
- if (!metadataTransport?.fetchDistinctValues || query.grouping && !query.grouping.keys.includes(field)) {
3145
+ async (field, search, signal) => {
3146
+ if (!metadataTransport || query.grouping && !query.grouping.keys.includes(field)) {
3285
3147
  const f = byName.get(field);
3286
3148
  if (!f || !clientRows) return { values: [], hasMore: false };
3287
3149
  const fieldHasNull = fieldHasNullInRows(f, clientRows);
@@ -3292,35 +3154,39 @@ function useQueryTable(opts) {
3292
3154
  hasNull: fieldHasNull
3293
3155
  };
3294
3156
  }
3295
- return metadataTransport.fetchDistinctValues({ field, search });
3157
+ return remoteDistinct(field, search, signal);
3296
3158
  },
3297
- [metadataTransport, byName, clientRows, groupKey]
3159
+ [metadataTransport, remoteDistinct, byName, clientRows, groupKey]
3298
3160
  );
3299
3161
  const fieldStats = useCallback12(
3300
3162
  async (fields, signal) => {
3301
3163
  const requested = [...new Set(fields)];
3302
- if (metadataTransport?.fetchFieldStats && !query.grouping) {
3303
- const backendFields = requested.filter(
3304
- (name) => byName.get(name)?.source.kind === "backend"
3305
- );
3306
- return backendFields.length ? metadataTransport.fetchFieldStats(backendFields, signal) : {};
3307
- }
3164
+ if (metadataTransport && !query.grouping)
3165
+ return remoteFieldStats(requested.filter((name) => byName.get(name)?.source.kind === "backend"), signal);
3308
3166
  if (!clientRows) return {};
3309
- const result = {};
3167
+ const result = [];
3310
3168
  for (const name of requested) {
3311
3169
  if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
3312
3170
  const field = byName.get(name);
3313
3171
  if (!field) continue;
3314
3172
  const distinct = /* @__PURE__ */ new Set();
3315
3173
  for (const row of clientRows) {
3316
- const value = readFieldValue4(field, row);
3174
+ const value = readFieldValue3(field, row);
3317
3175
  if (value !== null) distinct.add(JSON.stringify(value));
3318
3176
  }
3319
- result[name] = { distinct: distinct.size };
3177
+ result.push([name, { distinct: distinct.size }]);
3320
3178
  }
3321
- return result;
3179
+ return Object.fromEntries(result);
3322
3180
  },
3323
- [metadataTransport, byName, clientRows, groupKey]
3181
+ [metadataTransport, remoteFieldStats, byName, clientRows, groupKey]
3182
+ );
3183
+ const identityLookup = useMemo9(() => {
3184
+ const identity = sourceCatalogue.fields.find((field) => field.name === sourceCatalogue.idField);
3185
+ return !identity || isPushdownFilter(identity) && opsForField2(identity).includes("=");
3186
+ }, [sourceCatalogue]);
3187
+ const fetchRow = useCallback12(
3188
+ async (id, signal, select2) => fetchRowV3(metadataTransport, sourceCatalogue, id, { ...auxiliaryOptions(), ...select2?.length ? { select: select2 } : {} }, signal),
3189
+ [metadataTransport, sourceCatalogue, auxiliaryOptions]
3324
3190
  );
3325
3191
  const refresh = useCallback12(() => {
3326
3192
  setNonce((n) => n + 1);
@@ -3385,17 +3251,17 @@ function useQueryTable(opts) {
3385
3251
  );
3386
3252
  const refreshRow = useCallback12(
3387
3253
  async (id) => {
3388
- if (opts.computedExecution && transport?.fetchRowsV2) {
3254
+ if (!transport) return;
3255
+ if (!identityLookup || query.grouping || committedFields.some(isComputedField3) || query.orderBy.some((term) => isComputedField3(term.field))) {
3389
3256
  refresh();
3390
3257
  return;
3391
3258
  }
3392
- if (!transport?.fetchRow) return;
3393
- const updated = await transport.fetchRow(id);
3259
+ const updated = await fetchRow(id, void 0, committedFields);
3394
3260
  setRows(
3395
3261
  (prev) => prev.map((r) => rowId(r) === id ? updated ?? r : r)
3396
3262
  );
3397
3263
  },
3398
- [transport, rowId, opts.computedExecution, refresh]
3264
+ [transport, fetchRow, rowId, groupKey, committedFields, query.orderBy, refresh, identityLookup]
3399
3265
  );
3400
3266
  const visibleFields = useMemo9(
3401
3267
  () => selectedFields2(columnSchema, query),
@@ -3494,9 +3360,23 @@ function useQueryTable(opts) {
3494
3360
  }, []);
3495
3361
  const sourceRowId = useCallback12((row) => {
3496
3362
  const field = sourceSchema.fields.find((f) => f.name === sourceSchema.idField);
3497
- const value = field ? readFieldValue4(field, row) : row[sourceSchema.idField];
3363
+ const value = field ? readFieldValue3(field, row) : row[sourceSchema.idField];
3498
3364
  return typeof value === "string" || typeof value === "number" && Number.isFinite(value) ? value : null;
3499
3365
  }, [sourceSchema]);
3366
+ const detailCatalogue = typeof opts.itemDetails === "object" ? opts.itemDetails.schema : void 0;
3367
+ const lookupCatalogue = useMemo9(
3368
+ () => !detailCatalogue ? sourceCatalogue : {
3369
+ ...sourceCatalogue,
3370
+ fields: [...sourceCatalogue.fields, ...detailCatalogue.fields.filter((field) => !sourceCatalogue.fields.some((f) => f.name === field.name))]
3371
+ },
3372
+ [sourceCatalogue, detailCatalogue]
3373
+ );
3374
+ const lookupCatalogueRef = useRef9(lookupCatalogue);
3375
+ lookupCatalogueRef.current = lookupCatalogue;
3376
+ const fetchItem = useCallback12(
3377
+ async (id, signal) => fetchRowV3(metadataTransport, lookupCatalogueRef.current, id, auxiliaryOptions(), signal),
3378
+ [metadataTransport, auxiliaryOptions]
3379
+ );
3500
3380
  const itemDetails = useItemDetails({
3501
3381
  schema: query.grouping ? sourceSchema : displaySchema,
3502
3382
  query: sourceQuery,
@@ -3504,8 +3384,9 @@ function useQueryTable(opts) {
3504
3384
  options: opts.itemDetails,
3505
3385
  rows: query.grouping ? sourceRows ?? [] : rows,
3506
3386
  clientRows: sourceRows,
3387
+ // Without an identity lookup, item details read the rows on the page.
3507
3388
  rowId: sourceRowId,
3508
- transport: metadataTransport,
3389
+ fetchRow: metadataTransport && identityLookup ? fetchItem : void 0,
3509
3390
  refreshKey: nonce,
3510
3391
  links,
3511
3392
  queryParameter: parameter,
@@ -3546,7 +3427,7 @@ function useQueryTable(opts) {
3546
3427
  rows,
3547
3428
  clientRows: void 0,
3548
3429
  rowId,
3549
- transport: void 0,
3430
+ fetchRow: void 0,
3550
3431
  refreshKey: nonce,
3551
3432
  links,
3552
3433
  queryParameter: parameter,
@@ -3618,25 +3499,20 @@ function useQueryTable(opts) {
3618
3499
  visibleFields,
3619
3500
  rowId
3620
3501
  };
3621
- const sourceFilterValues = useCallback12(async (name, search) => {
3622
- if (metadataTransport?.fetchDistinctValues) return metadataTransport.fetchDistinctValues({ field: name, search });
3502
+ const sourceFilterValues = useCallback12(async (name, search, signal) => {
3503
+ if (metadataTransport) return remoteDistinct(name, search, signal);
3623
3504
  const field = groupingModel.sourceField(name);
3624
3505
  return field && sourceRows ? { ...distinctValuesFromRows(field, sourceRows, search), hasNull: fieldHasNullInRows(field, sourceRows) } : { values: [], hasMore: false };
3625
- }, [metadataTransport, groupingModel, sourceRows]);
3506
+ }, [metadataTransport, remoteDistinct, groupingModel, sourceRows]);
3626
3507
  const sourceFieldStats = useCallback12(async (names, signal) => {
3627
3508
  if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
3628
3509
  const requested = [...new Set(names)];
3629
- if (metadataTransport?.fetchFieldStats) {
3630
- const backendFields = requested.filter(
3631
- (name) => sourceCatalogue.fields.find((field) => field.name === name)?.source.kind === "backend"
3632
- );
3633
- return backendFields.length ? metadataTransport.fetchFieldStats(backendFields, signal) : {};
3634
- }
3510
+ if (metadataTransport) return remoteFieldStats(requested, signal);
3635
3511
  return Object.fromEntries(requested.flatMap((name) => {
3636
3512
  const field = groupingModel.sourceField(name);
3637
- return field && sourceRows ? [[name, { distinct: new Set(sourceRows.map((row) => JSON.stringify(readFieldValue4(field, row)))).size }]] : [];
3513
+ return field && sourceRows ? [[name, { distinct: new Set(sourceRows.map((row) => JSON.stringify(readFieldValue3(field, row)))).size }]] : [];
3638
3514
  }));
3639
- }, [metadataTransport, groupingModel, sourceRows, sourceCatalogue]);
3515
+ }, [metadataTransport, remoteFieldStats, groupingModel, sourceRows]);
3640
3516
  const sourceApi = {
3641
3517
  ...sourceFilters,
3642
3518
  loading,