@mastra/clickhouse 1.25.0 → 1.25.1-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { TupleParam, createClient } from "@clickhouse/client";
2
2
  import { ErrorCategory, ErrorDomain, MastraError } from "@mastra/core/error";
3
3
  import * as coreStorage from "@mastra/core/storage";
4
- import { BRANCH_SPAN_TYPES, EntityType, METRIC_DISTINCT_COLUMNS, MastraCompositeStore, MemoryStorage, ObservabilityStorage, SCORERS_SCHEMA, SPAN_SCHEMA, ScoresStorage, TABLE_AGENT_VERSIONS, TABLE_DATASETS, TABLE_DATASET_ITEMS, TABLE_DATASET_VERSIONS, TABLE_EXPERIMENTS, TABLE_EXPERIMENT_RESULTS, TABLE_FAVORITES, TABLE_HARNESS_SESSIONS, TABLE_KNOWLEDGE_ACTIVITY, TABLE_KNOWLEDGE_MENTIONS, TABLE_KNOWLEDGE_NODES, TABLE_KNOWLEDGE_RECORDS, TABLE_KNOWLEDGE_SEMANTIC_OUTBOX, TABLE_MCP_CLIENTS, TABLE_MCP_CLIENT_VERSIONS, TABLE_MCP_SERVERS, TABLE_MCP_SERVER_VERSIONS, TABLE_MESSAGES, TABLE_NOTIFICATIONS, TABLE_PROMPT_BLOCKS, TABLE_PROMPT_BLOCK_VERSIONS, TABLE_RESOURCES, TABLE_SCHEDULES, TABLE_SCHEDULE_TRIGGERS, TABLE_SCHEMAS, TABLE_SCORERS, TABLE_SCORER_DEFINITIONS, TABLE_SCORER_DEFINITION_VERSIONS, TABLE_SKILLS, TABLE_SKILL_BLOBS, TABLE_SKILL_VERSIONS, TABLE_SPANS, TABLE_THREADS, TABLE_THREAD_STATE, TABLE_TOOL_PROVIDER_CONNECTIONS, TABLE_TRACES, TABLE_WORKFLOW_DEFINITIONS, TABLE_WORKFLOW_SNAPSHOT, TABLE_WORKSPACES, TABLE_WORKSPACE_VERSIONS, TraceStatus, WorkflowsStorage, buildInputPreview, calculatePagination, computeTraceStatus, createStorageErrorId, getDefaultValue, getSqlType, listBranchesArgsSchema, listFeedbackArgsSchema, listLogsArgsSchema, listMetricsArgsSchema, listScoresArgsSchema, listTracesArgsSchema, normalizePerPage, safelyParseJSON, toTraceSpans, transformScoreRow, validateStorageMetadataFilter } from "@mastra/core/storage";
4
+ import { BRANCH_SPAN_TYPES, EntityType, METRIC_DISTINCT_COLUMNS, MastraCompositeStore, MemoryStorage, ObservabilityStorage, SCORERS_SCHEMA, SPAN_SCHEMA, ScoresStorage, TABLE_AGENT_VERSIONS, TABLE_DATASETS, TABLE_DATASET_ITEMS, TABLE_DATASET_VERSIONS, TABLE_EXPERIMENTS, TABLE_EXPERIMENT_RESULTS, TABLE_FAVORITES, TABLE_HARNESS_SESSIONS, TABLE_KNOWLEDGE_ACTIVITY, TABLE_KNOWLEDGE_MENTIONS, TABLE_KNOWLEDGE_NODES, TABLE_KNOWLEDGE_RECORDS, TABLE_KNOWLEDGE_SEMANTIC_OUTBOX, TABLE_MCP_CLIENTS, TABLE_MCP_CLIENT_VERSIONS, TABLE_MCP_SERVERS, TABLE_MCP_SERVER_VERSIONS, TABLE_MESSAGES, TABLE_NOTIFICATIONS, TABLE_PROMPT_BLOCKS, TABLE_PROMPT_BLOCK_VERSIONS, TABLE_RESOURCES, TABLE_SCHEDULES, TABLE_SCHEDULE_TRIGGERS, TABLE_SCHEMAS, TABLE_SCORERS, TABLE_SCORER_DEFINITIONS, TABLE_SCORER_DEFINITION_VERSIONS, TABLE_SKILLS, TABLE_SKILL_BLOBS, TABLE_SKILL_VERSIONS, TABLE_SPANS, TABLE_THREADS, TABLE_THREAD_STATE, TABLE_TOOL_PROVIDER_CONNECTIONS, TABLE_TRACES, TABLE_WORKFLOW_DEFINITIONS, TABLE_WORKFLOW_SNAPSHOT, TABLE_WORKSPACES, TABLE_WORKSPACE_VERSIONS, TraceStatus, WorkflowsStorage, calculatePagination, computeTraceStatus, createStorageErrorId, getDefaultValue, getSqlType, listBranchesArgsSchema, listFeedbackArgsSchema, listLogsArgsSchema, listMetricsArgsSchema, listScoresArgsSchema, listTracesArgsSchema, normalizePerPage, safelyParseJSON, toTraceSpans, transformScoreRow, validateStorageMetadataFilter } from "@mastra/core/storage";
5
5
  import { MessageList } from "@mastra/core/agent";
6
6
  import { MastraBase } from "@mastra/core/base";
7
7
  import { parseFieldKey } from "@mastra/core/utils";
@@ -942,12 +942,23 @@ var MemoryStorageClickhouse = class extends MemoryStorage {
942
942
  query_params: { messageIds }
943
943
  });
944
944
  if (threadIds.length > 0) {
945
- const now = (/* @__PURE__ */ new Date()).toISOString().replace("Z", "");
946
- await this.client.command({
947
- query: `ALTER TABLE ${TABLE_THREADS} UPDATE updatedAt = {now:DateTime64(3)} WHERE id IN {threadIds:Array(String)}`,
948
- query_params: {
949
- now,
950
- threadIds
945
+ const now = Date.now();
946
+ const existingThreads = await this.getLatestThreads(threadIds);
947
+ if (existingThreads.size > 0) await this.client.insert({
948
+ table: TABLE_THREADS,
949
+ format: "JSONEachRow",
950
+ values: Array.from(existingThreads.values()).map((existingThread) => ({
951
+ id: existingThread.id,
952
+ resourceId: existingThread.resourceId,
953
+ title: existingThread.title,
954
+ metadata: serializeMetadata(existingThread.metadata),
955
+ createdAt: existingThread.createdAt,
956
+ updatedAt: new Date(Math.max(now, new Date(existingThread.updatedAt).getTime() + 1)).toISOString().replace("Z", "")
957
+ })),
958
+ clickhouse_settings: {
959
+ date_time_input_format: "best_effort",
960
+ use_client_time_zone: 1,
961
+ output_format_json_quote_64bit_integers: 0
951
962
  }
952
963
  });
953
964
  }
@@ -2722,7 +2733,14 @@ CREATE TABLE IF NOT EXISTS ${TABLE_SPAN_EVENTS} (
2722
2733
  output Nullable(String),
2723
2734
  error Nullable(String),
2724
2735
  metadataRaw Nullable(String),
2725
- requestContext Nullable(String)
2736
+ requestContext Nullable(String),
2737
+
2738
+ -- Previews of \`input\` / \`output\` / \`error\` in core's \`buildPreview\` format, written
2739
+ -- on every span ('' when there is nothing to preview). NULL on rows written before
2740
+ -- the columns existed; readers build those from the payload columns.
2741
+ inputPreview Nullable(String),
2742
+ outputPreview Nullable(String),
2743
+ errorPreview Nullable(String)
2726
2744
  )
2727
2745
  ENGINE = ReplacingMergeTree
2728
2746
  PARTITION BY toDate(endedAt)
@@ -2790,6 +2808,12 @@ CREATE TABLE IF NOT EXISTS ${TABLE_TRACE_ROOTS} (
2790
2808
  metadataRaw Nullable(String),
2791
2809
  requestContext Nullable(String),
2792
2810
 
2811
+ -- Previews of \`input\` / \`output\` / \`error\` ('' when there is nothing to preview).
2812
+ -- NULL on rows written before the columns existed.
2813
+ inputPreview Nullable(String),
2814
+ outputPreview Nullable(String),
2815
+ errorPreview Nullable(String),
2816
+
2793
2817
  -- traceId is second in the sort key, behind startedAt, so point lookups by
2794
2818
  -- traceId (getRootSpan, trace deletes, trace-query root dedupe) need a skip
2795
2819
  -- index to avoid scanning every granule.
@@ -2868,6 +2892,9 @@ CREATE TABLE IF NOT EXISTS ${TABLE_TRACE_BRANCHES} (
2868
2892
  error Nullable(String),
2869
2893
  metadataRaw Nullable(String),
2870
2894
  requestContext Nullable(String),
2895
+ inputPreview Nullable(String),
2896
+ outputPreview Nullable(String),
2897
+ errorPreview Nullable(String),
2871
2898
 
2872
2899
  INDEX idx_traceId traceId TYPE bloom_filter(0.01) GRANULARITY 1
2873
2900
  )
@@ -3599,7 +3626,26 @@ const addBloomIndex = (table, name, column, granularity = 2) => ({
3599
3626
  name,
3600
3627
  sql: `ALTER TABLE ${table} ADD INDEX IF NOT EXISTS ${name} ${column} TYPE bloom_filter(0.01) GRANULARITY ${granularity}`
3601
3628
  });
3629
+ const LOG_SKIP_INDEX_COLUMNS = [
3630
+ "traceId",
3631
+ "threadId",
3632
+ "resourceId",
3633
+ "userId",
3634
+ "organizationId",
3635
+ "experimentId",
3636
+ "runId",
3637
+ "sessionId",
3638
+ "requestId"
3639
+ ];
3602
3640
  const ALL_MIGRATIONS = [
3641
+ ...[
3642
+ "inputPreview",
3643
+ "outputPreview",
3644
+ "errorPreview"
3645
+ ].flatMap((column) => [addColumn(TABLE_TRACE_ROOTS, column, "Nullable(String)"), addColumn(TABLE_TRACE_BRANCHES, column, "Nullable(String)")]),
3646
+ addColumn(TABLE_SPAN_EVENTS, "inputPreview", "Nullable(String)"),
3647
+ addColumn(TABLE_SPAN_EVENTS, "outputPreview", "Nullable(String)"),
3648
+ addColumn(TABLE_SPAN_EVENTS, "errorPreview", "Nullable(String)"),
3603
3649
  addColumn(TABLE_SPAN_EVENTS, "entityVersionId", "Nullable(String)"),
3604
3650
  addColumn(TABLE_SPAN_EVENTS, "parentEntityVersionId", "Nullable(String)"),
3605
3651
  addColumn(TABLE_SPAN_EVENTS, "rootEntityVersionId", "Nullable(String)"),
@@ -3636,17 +3682,7 @@ const ALL_MIGRATIONS = [
3636
3682
  addBloomIndex(TABLE_METRIC_EVENTS, "idx_runId", "runId"),
3637
3683
  addBloomIndex(TABLE_METRIC_EVENTS, "idx_sessionId", "sessionId"),
3638
3684
  addBloomIndex(TABLE_METRIC_EVENTS, "idx_requestId", "requestId"),
3639
- ...[
3640
- "traceId",
3641
- "threadId",
3642
- "resourceId",
3643
- "userId",
3644
- "organizationId",
3645
- "experimentId",
3646
- "runId",
3647
- "sessionId",
3648
- "requestId"
3649
- ].map((column) => addBloomIndex(TABLE_LOG_EVENTS, `idx_${column}`, column)),
3685
+ ...LOG_SKIP_INDEX_COLUMNS.map((column) => addBloomIndex(TABLE_LOG_EVENTS, `idx_${column}`, column)),
3650
3686
  addBloomIndex(TABLE_DELETION_REQUESTS, "idx_predicateValues", "predicateValues")
3651
3687
  ];
3652
3688
  const ALL_TABLE_NAMES = [
@@ -3756,6 +3792,78 @@ function parseTtlExpression(expr) {
3756
3792
  };
3757
3793
  }
3758
3794
  //#endregion
3795
+ //#region src/storage/domains/observability/v-next/preview-compat.ts
3796
+ const generator = coreStorage.buildStoredPreview;
3797
+ const canGeneratePreviews = typeof generator === "function";
3798
+ function needsPreview(value) {
3799
+ return canGeneratePreviews && value == null;
3800
+ }
3801
+ function storedPreview(preview, value, serialized) {
3802
+ return canGeneratePreviews ? generator(preview, value, serialized) : preview ?? null;
3803
+ }
3804
+ function readPreview(preview, payload, jsonEncoded = false) {
3805
+ const supplied = preview;
3806
+ if (!canGeneratePreviews) return supplied ?? null;
3807
+ if (supplied != null) return supplied.trim() || null;
3808
+ let value = payload;
3809
+ if (needsPreview(preview) && jsonEncoded && typeof payload === "string") try {
3810
+ value = JSON.parse(payload);
3811
+ } catch {
3812
+ value = null;
3813
+ }
3814
+ return storedPreview(supplied, value, jsonEncoded && typeof payload === "string" ? payload : void 0) || null;
3815
+ }
3816
+ /** Older response builders may shorten previews or turn empty strings into null. */
3817
+ function buildSpanQueryResponse(...args) {
3818
+ if (canGeneratePreviews) return coreStorage.buildSpanQueryResponse(...args);
3819
+ const [plan, selected, payloads, metrics] = args;
3820
+ const response = coreStorage.buildSpanQueryResponse(plan, selected, [], metrics);
3821
+ const byIdentity = new Map(payloads.map((payload) => [coreStorage.spanQueryIdentityKey(payload), payload]));
3822
+ for (const span of response.spans) {
3823
+ const payload = byIdentity.get(coreStorage.spanQueryIdentityKey(span));
3824
+ span.inputPreview = payload?.inputPreview ?? null;
3825
+ span.outputPreview = payload?.outputPreview ?? null;
3826
+ span.errorPreview = payload?.errorPreview ?? null;
3827
+ }
3828
+ return response;
3829
+ }
3830
+ //#endregion
3831
+ //#region src/storage/domains/observability/v-next/previews.ts
3832
+ /**
3833
+ * Stored span previews (`inputPreview` / `outputPreview` / `errorPreview`).
3834
+ *
3835
+ * The writer stores each preview in core's `buildPreview` format, `''` when there is nothing to
3836
+ * preview. A NULL preview means the row was written before the columns existed; readers fetch the
3837
+ * payload columns for just those rows and build the same preview from them.
3838
+ */
3839
+ const PREVIEW_COLUMNS = [
3840
+ "inputPreview",
3841
+ "outputPreview",
3842
+ "errorPreview"
3843
+ ];
3844
+ /** Payload column each preview is built from. */
3845
+ const PREVIEW_PAYLOAD_COLUMNS = {
3846
+ inputPreview: "input",
3847
+ outputPreview: "output",
3848
+ errorPreview: "error"
3849
+ };
3850
+ /** Whether a row was written before previews were stored, so it needs its payload columns. */
3851
+ function needsPreviewPayloads(row) {
3852
+ return PREVIEW_COLUMNS.some((column) => needsPreview(row[column]));
3853
+ }
3854
+ /**
3855
+ * The previews a list returns for a row: the stored ones (`''` becomes `null`), or for a row written
3856
+ * before they were stored, built from the payload columns on `payloads` (default: the row itself).
3857
+ */
3858
+ function resolveStoredPreviews(row, payloads = row) {
3859
+ const previews = {};
3860
+ for (const column of PREVIEW_COLUMNS) {
3861
+ const stored = row[column];
3862
+ previews[column] = readPreview(stored, payloads?.[PREVIEW_PAYLOAD_COLUMNS[column]], true);
3863
+ }
3864
+ return previews;
3865
+ }
3866
+ //#endregion
3759
3867
  //#region src/storage/domains/observability/v-next/review-status.ts
3760
3868
  const FEEDBACK_REVIEW_STATUSES = ["needs-review", "reviewed"];
3761
3869
  function isFeedbackReviewStatus(value) {
@@ -3962,7 +4070,7 @@ function rowToLightSpanRecord(row) {
3962
4070
  endedAt
3963
4071
  }),
3964
4072
  metadata: parseJson(row.metadataRaw) ?? void 0,
3965
- inputPreview: buildInputPreview(row.input ?? null),
4073
+ ...resolveStoredPreviews(row),
3966
4074
  createdAt: startedAt,
3967
4075
  updatedAt: null
3968
4076
  };
@@ -3970,6 +4078,9 @@ function rowToLightSpanRecord(row) {
3970
4078
  function spanRecordToRow(span) {
3971
4079
  const endedAt = span.isEvent ? span.startedAt : span.endedAt ?? span.startedAt;
3972
4080
  const metadata = span.metadata ?? null;
4081
+ const input = jsonEncode(span.input);
4082
+ const output = jsonEncode(span.output);
4083
+ const error = jsonEncode(span.error);
3973
4084
  return {
3974
4085
  dedupeKey: buildDedupeKey(span.traceId, span.spanId),
3975
4086
  traceId: span.traceId,
@@ -4009,10 +4120,13 @@ function spanRecordToRow(span) {
4009
4120
  scope: jsonEncode(span.scope),
4010
4121
  attributes: jsonEncode(span.attributes),
4011
4122
  links: jsonEncode(span.links),
4012
- input: jsonEncode(span.input),
4013
- output: jsonEncode(span.output),
4014
- error: jsonEncode(span.error),
4015
- requestContext: jsonEncode(span.requestContext)
4123
+ input,
4124
+ output,
4125
+ error,
4126
+ requestContext: jsonEncode(span.requestContext),
4127
+ inputPreview: storedPreview(span.inputPreview, span.input, input),
4128
+ outputPreview: storedPreview(span.outputPreview, span.output, output),
4129
+ errorPreview: storedPreview(span.errorPreview, span.error, error)
4016
4130
  };
4017
4131
  }
4018
4132
  function rowToLogRecord(row) {
@@ -6927,11 +7041,15 @@ const FEEDBACK_FIELDS = {
6927
7041
  const TRACE_SELECT = `
6928
7042
  r.traceId AS traceId,
6929
7043
  r.spanId AS rootSpanId,
7044
+ r.dedupeKey AS dedupeKey,
7045
+ r.organizationId AS organizationId,
6930
7046
  r.name AS name,
6931
7047
  r.entityId AS entityId,
6932
7048
  r.parentSpanId AS parentSpanId,
6933
7049
  r.metadataRaw AS metadata,
6934
- r.input AS input,
7050
+ r.inputPreview AS inputPreview,
7051
+ r.outputPreview AS outputPreview,
7052
+ r.errorPreview AS errorPreview,
6935
7053
  r.threadId AS threadId,
6936
7054
  r.resourceId AS resourceId,
6937
7055
  r.startedAt AS startedAt,
@@ -6940,22 +7058,6 @@ const TRACE_SELECT = `
6940
7058
  r.entityType AS entityType,
6941
7059
  r.environment AS environment,
6942
7060
  ${TRACE_STATUS_SQL} AS status`;
6943
- /** Candidate columns carried through the page-mode window sort (no payload blobs). */
6944
- const TRACE_PAGE_COLUMNS = [
6945
- "traceId",
6946
- "rootSpanId",
6947
- "name",
6948
- "entityId",
6949
- "parentSpanId",
6950
- "threadId",
6951
- "resourceId",
6952
- "startedAt",
6953
- "endedAt",
6954
- "entityName",
6955
- "entityType",
6956
- "environment",
6957
- "status"
6958
- ];
6959
7061
  var ParameterBuilder = class {
6960
7062
  params = {};
6961
7063
  #next = 1;
@@ -7079,18 +7181,38 @@ function compileTenantScope(scope, parameters) {
7079
7181
  if (scope.resourceId !== void 0) sql += `\n AND resourceId = ${parameters.add(scope.resourceId, "String")}`;
7080
7182
  return sql;
7081
7183
  }
7082
- function compileClickHouseTraceScope(selection, relationCollections, parameters, scope, seedConjuncts = []) {
7184
+ function compileClickHouseTraceScope(selection, relationCollections, parameters, scope, seedConjuncts = [], options = {}) {
7083
7185
  const from = parameters.add(selection.timeRange.from, "DateTime64(3, 'UTC')");
7084
7186
  const to = parameters.add(selection.timeRange.to, "DateTime64(3, 'UTC')");
7085
7187
  const tenant = compileTenantScope(scope, parameters);
7086
- const ctes = [`current_roots AS (
7188
+ const seedFilter = seedConjuncts.map((conjunct) => `\n AND (${compilePredicate(conjunct, parameters)})`).join("");
7189
+ const bound = options.delta ? `\n AND (traceId IN (SELECT traceId FROM delta_candidates))` : "";
7190
+ const windowTraces = (more) => `
7191
+ SELECT traceId
7192
+ FROM ${TABLE_TRACE_ROOTS} w
7193
+ WHERE startedAt >= ${from}
7194
+ AND startedAt < ${to}${tenant}${more}
7195
+ `;
7196
+ const delta = options.delta ? [`delta_candidates AS (
7197
+ SELECT traceId, max(cursorId) AS latestCursorId
7198
+ FROM ${TABLE_TRACE_ROOTS_DELTA}
7199
+ WHERE ${options.delta}
7200
+ AND traceId IN (${windowTraces("")})
7201
+ GROUP BY traceId
7202
+ )`] : [];
7203
+ const ctes = options.oneRootPerTrace ? [...delta, `root_scope AS (
7204
+ SELECT *
7205
+ FROM ${TABLE_TRACE_ROOTS} r
7206
+ WHERE startedAt >= ${from}
7207
+ AND startedAt < ${to}${tenant}${seedFilter}${bound}
7208
+ )`] : [`current_roots AS (
7087
7209
  SELECT *
7088
7210
  FROM ${TABLE_TRACE_ROOTS}
7089
7211
  WHERE traceId IN (
7090
7212
  SELECT traceId
7091
7213
  FROM ${TABLE_TRACE_ROOTS} r
7092
7214
  WHERE startedAt >= ${from}
7093
- AND startedAt < ${to}${tenant}${seedConjuncts.map((conjunct) => `\n AND (${compilePredicate(conjunct, parameters)})`).join("")}
7215
+ AND startedAt < ${to}${tenant}${seedFilter}
7094
7216
  )${tenant}
7095
7217
  ORDER BY traceId, dedupeKey
7096
7218
  LIMIT 1 BY traceId
@@ -7124,9 +7246,9 @@ function compileClickHouseTraceScope(selection, relationCollections, parameters,
7124
7246
  organizationId
7125
7247
  FROM ${TABLE_SPAN_EVENTS}
7126
7248
  WHERE isNotNull(traceId)
7127
- AND traceId IN (SELECT traceId FROM root_scope)${tenant}
7249
+ AND traceId IN (${options.spanExistence && options.oneRootPerTrace ? windowTraces(bound) : "SELECT traceId FROM root_scope"})${tenant}${options.spanExistence ? "" : `
7128
7250
  ORDER BY dedupeKey
7129
- LIMIT 1 BY dedupeKey
7251
+ LIMIT 1 BY dedupeKey`}
7130
7252
  )`);
7131
7253
  if (relationCollections.has("scores")) ctes.push(`current_scores AS (
7132
7254
  SELECT
@@ -7194,9 +7316,9 @@ function parseDeltaWatermark(value) {
7194
7316
  * selection's time range and tenant scope that match its `where` predicate. Trace queries and
7195
7317
  * trace aggregates both select from this CTE so they always see the same population.
7196
7318
  */
7197
- function compileClickHouseTraceCandidates(selection, columns, parameters) {
7319
+ function compileClickHouseTraceCandidates(selection, columns, parameters, options = {}) {
7198
7320
  const rootConjuncts = selection.where ? (selection.where.type === "boolean" && selection.where.operator === "and" ? selection.where.args : [selection.where]).filter((conjunct) => collectRelationCollections(conjunct).size === 0) : [];
7199
- const ctes = compileClickHouseTraceScope(selection, collectRelationCollections(selection.where), parameters, selection.scope, rootConjuncts);
7321
+ const ctes = compileClickHouseTraceScope(selection, collectRelationCollections(selection.where), parameters, selection.scope, rootConjuncts, options);
7200
7322
  const predicate = selection.where ? compilePredicate(selection.where, parameters) : "1";
7201
7323
  ctes.push(`candidates AS (
7202
7324
  SELECT ${columns}
@@ -7205,94 +7327,191 @@ function compileClickHouseTraceCandidates(selection, columns, parameters) {
7205
7327
  )`);
7206
7328
  return ctes;
7207
7329
  }
7330
+ /**
7331
+ * `queryTraces()` statements read one root per trace and only test spans for existence
7332
+ * (TraceScopeOptions). The list statements carry narrow columns; `metadata` and the previews are fetched
7333
+ * for the returned rows afterwards (compileClickHouseTraceRootPayloads).
7334
+ */
7335
+ const QUERY_TRACES_SCOPE = {
7336
+ oneRootPerTrace: true,
7337
+ spanExistence: true
7338
+ };
7339
+ const TRACE_LIST_SELECT = TRACE_SELECT.split("\n").filter((line) => !/ AS (metadata|inputPreview|outputPreview|errorPreview),$/.test(line)).join("\n");
7340
+ function traceOrder(plan) {
7341
+ return {
7342
+ field: resolveOrderField(plan.orderBy.field),
7343
+ direction: plan.orderBy.direction === "asc" ? "ASC" : "DESC"
7344
+ };
7345
+ }
7346
+ /**
7347
+ * Keyset cursor on a trace's sort value. Applied after each trace is reduced to one sort value:
7348
+ * filtering root rows first would let a trace with several roots (or unmerged copies with
7349
+ * different `endedAt`) come back on a later page under a root that sorts after the cursor.
7350
+ */
7351
+ function keysetCursorCondition(plan, order, parameters) {
7352
+ if (plan.paginationMode !== "keyset" || !plan.cursor) return "";
7353
+ const comparison = order.direction === "ASC" ? ">" : "<";
7354
+ const sortValue = parameters.add(plan.cursor.sortValue, "DateTime64(3, 'UTC')");
7355
+ return `(sortValue ${comparison} ${sortValue} OR (sortValue = ${sortValue} AND traceId > ${parameters.add(plan.cursor.traceId, "String")}))`;
7356
+ }
7357
+ /**
7358
+ * First statement of keyset and page mode: the page's traces (after the keyset cursor, `limit + 1`
7359
+ * for keyset mode, `perPage` at the page offset for page mode) as root keys, and the
7360
+ * number of matching traces on every row. Per trace, the root row that sorts first gives the sort
7361
+ * value and the key, so the key always names a row with that sort value. No blob columns are read.
7362
+ */
7363
+ function compileClickHouseTraceQueryKeys(plan) {
7364
+ if (plan.paginationMode === "delta") throw new Error("Delta trace queries have no keys statement");
7365
+ const parameters = new ParameterBuilder();
7366
+ const order = traceOrder(plan);
7367
+ const ctes = compileClickHouseTraceCandidates(plan, "r.traceId AS traceId, r.dedupeKey AS dedupeKey, r.startedAt AS startedAt, r.endedAt AS endedAt", parameters, QUERY_TRACES_SCOPE);
7368
+ const cursor = keysetCursorCondition(plan, order, parameters);
7369
+ const pick = order.direction === "DESC" ? "max" : "min";
7370
+ const limit = plan.paginationMode === "page" ? `LIMIT ${parameters.add(plan.perPage, "UInt64")} OFFSET ${parameters.add(plan.page * plan.perPage, "UInt64")}` : `LIMIT ${parameters.add(plan.limit + 1, "UInt64")}`;
7371
+ return {
7372
+ query: `WITH ${ctes.join(",\n")},
7373
+ per_trace AS (
7374
+ SELECT traceId, ${pick}(${order.field}) AS sortValue,
7375
+ ${pick === "max" ? "argMax" : "argMin"}((dedupeKey, startedAt), ${order.field}) AS root
7376
+ FROM candidates
7377
+ GROUP BY traceId${cursor ? `\n HAVING ${cursor}` : ""}
7378
+ )
7379
+ SELECT traceId, root.1 AS dedupeKey, sortValue, root.2 AS rootStartedAt, count() OVER () AS __total
7380
+ FROM per_trace
7381
+ ORDER BY sortValue ${order.direction}, traceId ASC
7382
+ ${limit}`,
7383
+ query_params: parameters.params,
7384
+ sharedSnapshot: true
7385
+ };
7386
+ }
7387
+ /**
7388
+ * Second statement of keyset and page mode: the rows of the keyed roots, with `metadata`
7389
+ * and the stored previews, read by sort-key tuple. Unmerged copies of a root key are redeliveries of one
7390
+ * span; the relation-free `where` conjuncts are re-applied so a copy that doesn't match isn't
7391
+ * returned, and the copy that sorts first is kept, as in the keys statement.
7392
+ */
7393
+ function compileClickHouseTraceRowsByKey(plan, keys) {
7394
+ const parameters = new ParameterBuilder();
7395
+ const order = traceOrder(plan);
7396
+ const ts = (value) => parameters.add(value, "DateTime64(3, 'UTC')");
7397
+ const byEnd = order.field === "endedAt";
7398
+ const tuples = keys.map((key) => {
7399
+ const started = ts(byEnd ? key.rootStartedAt : key.sortValue);
7400
+ const id = `${parameters.add(key.traceId, "String")}, ${parameters.add(key.dedupeKey, "String")}`;
7401
+ return byEnd ? `(${started}, ${id}, ${ts(key.sortValue)})` : `(${started}, ${id})`;
7402
+ });
7403
+ const predicate = (plan.where ? (plan.where.type === "boolean" && plan.where.operator === "and" ? plan.where.args : [plan.where]).filter((conjunct) => collectRelationCollections(conjunct).size === 0) : []).map((conjunct) => `\n AND (${compilePredicate(conjunct, parameters)})`).join("");
7404
+ return {
7405
+ query: `SELECT ${TRACE_SELECT}
7406
+ FROM ${TABLE_TRACE_ROOTS} r
7407
+ WHERE (r.startedAt, r.traceId, r.dedupeKey${byEnd ? ", r.endedAt" : ""}) IN (${tuples.join(", ")})${compileTenantScope(plan.scope, parameters)}${predicate}
7408
+ ORDER BY ${order.field} ${order.direction}, traceId ASC
7409
+ LIMIT 1 BY traceId`,
7410
+ query_params: parameters.params
7411
+ };
7412
+ }
7413
+ /** Number of matching traces, for numbered pages past the last one (the keys statement returns no rows). */
7414
+ function compileClickHouseTraceQueryTotal(plan) {
7415
+ const parameters = new ParameterBuilder();
7416
+ return {
7417
+ query: `WITH ${compileClickHouseTraceCandidates(plan, "r.traceId AS traceId", parameters, QUERY_TRACES_SCOPE).join(",\n")}
7418
+ SELECT uniqExact(traceId) AS traces
7419
+ FROM candidates`,
7420
+ query_params: parameters.params,
7421
+ sharedSnapshot: true
7422
+ };
7423
+ }
7424
+ /**
7425
+ * The list statement of groups and delta mode (rows without `metadata` and the previews). Keyset
7426
+ * and page mode list with compileClickHouseTraceQueryKeys and compileClickHouseTraceRowsByKey.
7427
+ * A trace with several candidate rows (several roots, or unmerged copies of one) is listed in delta
7428
+ * mode under the row that sorts first in the list order, as in the keys statement; ties go to the
7429
+ * lowest dedupeKey, then the latest endedAt, so every poll picks the same row.
7430
+ */
7208
7431
  function compileClickHouseTraceQuery(plan, deltaHead) {
7209
7432
  const parameters = new ParameterBuilder();
7210
- const candidates = `WITH ${compileClickHouseTraceCandidates(plan, TRACE_SELECT, parameters).join(",\n")}`;
7211
- if (plan.result === "groups") return {
7212
- query: `${candidates}
7433
+ if (plan.result === "groups") {
7434
+ const ctes = compileClickHouseTraceCandidates(plan, TRACE_LIST_SELECT, parameters, QUERY_TRACES_SCOPE);
7435
+ const pageCondition = plan.cursor ? `AND threadId > ${parameters.add(plan.cursor.threadId, "String")}` : "";
7436
+ const limit = parameters.add(plan.limit + 1, "UInt64");
7437
+ return {
7438
+ query: `WITH ${ctes.join(",\n")}
7213
7439
  SELECT threadId
7214
7440
  FROM candidates
7215
- WHERE isNotNull(threadId) ${plan.cursor ? `AND threadId > ${parameters.add(plan.cursor.threadId, "String")}` : ""}
7441
+ WHERE isNotNull(threadId) ${pageCondition}
7216
7442
  GROUP BY threadId
7217
7443
  ORDER BY threadId ASC
7218
- LIMIT ${parameters.add(plan.limit + 1, "UInt64")}`,
7219
- query_params: parameters.params
7220
- };
7221
- if (plan.paginationMode === "delta") {
7222
- const watermark = coreStorage.getTraceQueryDeltaWatermark(plan, "clickhouse");
7223
- const after = watermark ? parseDeltaWatermark(watermark) : {
7224
- cursorId: "0",
7225
- traceId: ""
7226
- };
7227
- const lowerCursor = parameters.add(after.cursorId, "UInt64");
7228
- return {
7229
- query: `${candidates}, delta_candidates AS (
7230
- SELECT traceId, max(cursorId) AS latestCursorId
7231
- FROM ${TABLE_TRACE_ROOTS_DELTA}
7232
- WHERE cursorId >= ${lowerCursor} AND tuple(cursorId, traceId) > ${`tuple(${lowerCursor}, ${parameters.add(after.traceId, "String")})`} ${deltaHead ? `AND tuple(cursorId, traceId) <= tuple(${parameters.add(deltaHead.cursorId, "UInt64")}, ${parameters.add(deltaHead.traceId, "String")})` : ""}
7233
- GROUP BY traceId
7234
- )
7235
- SELECT c.*, toString(d.latestCursorId) AS __delta_cursor
7236
- FROM candidates c
7237
- INNER JOIN delta_candidates d ON c.traceId = d.traceId
7238
- ORDER BY d.latestCursorId ASC, c.traceId ASC
7239
- LIMIT ${parameters.add(plan.limit + 1, "UInt64")}`,
7240
- query_params: parameters.params,
7241
- sharedSnapshot: true
7242
- };
7243
- }
7244
- const orderField = resolveOrderField(plan.orderBy.field);
7245
- const direction = plan.orderBy.direction === "asc" ? "ASC" : "DESC";
7246
- if (plan.paginationMode === "page") {
7247
- const onPage = `__row_position > ${parameters.add(plan.page * plan.perPage, "UInt64")} AND __row_position <= ${parameters.add((plan.page + 1) * plan.perPage, "UInt64")}`;
7248
- return {
7249
- query: `${candidates},
7250
- page_rows AS (
7251
- SELECT
7252
- ${TRACE_PAGE_COLUMNS.join(",\n ")},
7253
- row_number() OVER (ORDER BY ${orderField} ${direction}, traceId ASC) AS __row_position,
7254
- count() OVER () AS total
7255
- FROM candidates
7256
- )
7257
- SELECT *, if(${onPage}, 0, 1) AS __metadata
7258
- FROM page_rows
7259
- WHERE (${onPage}) OR __row_position = 1
7260
- ORDER BY __metadata ASC, __row_position ASC`,
7444
+ LIMIT ${limit}`,
7261
7445
  query_params: parameters.params,
7262
7446
  sharedSnapshot: true
7263
7447
  };
7264
7448
  }
7265
- let pageCondition = "";
7266
- if (plan.cursor) {
7267
- const comparison = plan.orderBy.direction === "asc" ? ">" : "<";
7268
- const sortValue = parameters.add(plan.cursor.sortValue, "DateTime64(3, 'UTC')");
7269
- pageCondition = `WHERE (${orderField} ${comparison} ${sortValue} OR (${orderField} = ${sortValue} AND traceId > ${parameters.add(plan.cursor.traceId, "String")}))`;
7270
- }
7449
+ if (plan.paginationMode !== "delta") throw new Error("Keyset and page trace queries use compileClickHouseTraceQueryKeys and compileClickHouseTraceRowsByKey");
7450
+ const order = traceOrder(plan);
7451
+ const watermark = coreStorage.getTraceQueryDeltaWatermark(plan, "clickhouse");
7452
+ const after = watermark ? parseDeltaWatermark(watermark) : {
7453
+ cursorId: "0",
7454
+ traceId: ""
7455
+ };
7456
+ const lowerCursor = parameters.add(after.cursorId, "UInt64");
7457
+ const lower = `tuple(${lowerCursor}, ${parameters.add(after.traceId, "String")})`;
7458
+ const upper = deltaHead ? `AND tuple(cursorId, traceId) <= tuple(${parameters.add(deltaHead.cursorId, "UInt64")}, ${parameters.add(deltaHead.traceId, "String")})` : "";
7459
+ const ctes = compileClickHouseTraceCandidates(plan, TRACE_LIST_SELECT, parameters, {
7460
+ ...QUERY_TRACES_SCOPE,
7461
+ delta: `cursorId >= ${lowerCursor} AND tuple(cursorId, traceId) > ${lower} ${upper}`
7462
+ });
7271
7463
  const limit = parameters.add(plan.limit + 1, "UInt64");
7272
7464
  return {
7273
- query: `${candidates}
7274
- SELECT *
7275
- FROM candidates
7276
- ${pageCondition}
7277
- ORDER BY ${orderField} ${direction}, traceId ASC
7465
+ query: `WITH ${ctes.join(",\n")}
7466
+ SELECT c.*, toString(d.latestCursorId) AS __delta_cursor
7467
+ FROM candidates c
7468
+ INNER JOIN delta_candidates d ON c.traceId = d.traceId
7469
+ ORDER BY d.latestCursorId ASC, c.traceId ASC, c.${order.field} ${order.direction}, c.dedupeKey ASC, c.endedAt DESC
7470
+ LIMIT 1 BY c.traceId
7278
7471
  LIMIT ${limit}`,
7279
- query_params: parameters.params
7472
+ query_params: parameters.params,
7473
+ sharedSnapshot: true
7474
+ };
7475
+ }
7476
+ /** Map key matching a payload row to the listed row it was read for. */
7477
+ function traceRootRowKey(row) {
7478
+ return JSON.stringify([
7479
+ String(row.traceId),
7480
+ String(row.dedupeKey),
7481
+ row.organizationId == null ? null : String(row.organizationId),
7482
+ row.resourceId == null ? null : String(row.resourceId)
7483
+ ]);
7484
+ }
7485
+ /** A listed row's {@link ClickHouseTraceRootRowKey}. */
7486
+ function toTraceRootRowKey(row) {
7487
+ return {
7488
+ traceId: String(row.traceId),
7489
+ dedupeKey: String(row.dedupeKey),
7490
+ startedAt: asIsoTimestamp(row.startedAt),
7491
+ endedAt: asIsoTimestamp(row.endedAt),
7492
+ organizationId: row.organizationId == null ? null : String(row.organizationId),
7493
+ resourceId: row.resourceId == null ? null : String(row.resourceId)
7280
7494
  };
7281
7495
  }
7282
7496
  /**
7283
- * Fetches the metadata/input payloads for page-mode rows. Looks rows up by the
7284
- * trace_roots sort-key prefix `(startedAt, traceId)`, so only the page's
7285
- * granules are read. A root can have unmerged versions in different `endedAt`
7286
- * partitions, so `endedAt` is part of the key: the payload comes from the same
7287
- * version as the candidate row.
7497
+ * Fetches payloads for listed rows: `metadata` and the stored previews, or `input` / `output` /
7498
+ * `error` alone for rows written before the previews were stored. Each row is looked up by its
7499
+ * own identity: the trace_roots sort key `(startedAt, traceId, dedupeKey)`, so only the page's
7500
+ * granules are read; `endedAt`, so the payload comes from the same version as the listed row (a
7501
+ * root can have unmerged versions in different `endedAt` partitions); and its own
7502
+ * `organizationId` / `resourceId`, so a page holding several tenants' rows that share a trace and
7503
+ * span id never takes another tenant's payload. NULL tenant columns match only NULL. Callers
7504
+ * match the returned rows back with {@link traceRootRowKey}.
7288
7505
  */
7289
- function compileClickHouseTraceRootPayloads(keys) {
7506
+ function compileClickHouseTraceRootPayloads(keys, scope, columns = "payload") {
7290
7507
  const parameters = new ParameterBuilder();
7508
+ const tenant = (value) => `${parameters.add(value ?? "", "String")}, ${value === null ? 1 : 0}`;
7509
+ const tuples = keys.map((key) => `(${parameters.add(key.startedAt, "DateTime64(3, 'UTC')")}, ${parameters.add(key.traceId, "String")}, ${parameters.add(key.dedupeKey, "String")}, ${parameters.add(key.endedAt, "DateTime64(3, 'UTC')")}, ${tenant(key.organizationId)}, ${tenant(key.resourceId)})`);
7291
7510
  return {
7292
- query: `SELECT traceId, spanId AS rootSpanId, metadataRaw AS metadata, input
7511
+ query: `SELECT traceId, spanId AS rootSpanId, dedupeKey, organizationId, resourceId, ${columns === "payload" ? `metadataRaw AS metadata, ${PREVIEW_COLUMNS.join(", ")}` : "input, output, error"}
7293
7512
  FROM ${TABLE_TRACE_ROOTS}
7294
- WHERE (startedAt, traceId, spanId, endedAt) IN (${keys.map((key) => `(${parameters.add(key.startedAt, "DateTime64(3, 'UTC')")}, ${parameters.add(key.traceId, "String")}, ${parameters.add(key.rootSpanId, "String")}, ${parameters.add(key.endedAt, "DateTime64(3, 'UTC')")})`).join(", ")})
7295
- LIMIT 1 BY traceId, spanId`,
7513
+ WHERE (startedAt, traceId, dedupeKey, endedAt, ifNull(organizationId, ''), isNull(organizationId), ifNull(resourceId, ''), isNull(resourceId)) IN (${tuples.join(", ")})${compileTenantScope(scope, parameters)}
7514
+ LIMIT 1 BY traceId, dedupeKey, organizationId, resourceId`,
7296
7515
  query_params: parameters.params
7297
7516
  };
7298
7517
  }
@@ -7486,56 +7705,15 @@ async function queryTraces(client, plan, timeoutMs, strategy = null) {
7486
7705
  deltaCursor: coreStorage.encodeTraceQueryDeltaCursor(plan, "clickhouse", JSON.stringify(deltaHead))
7487
7706
  });
7488
7707
  }
7489
- if (plan.paginationMode === "page") {
7490
- const rows = await runWithClickHouseTraceQueryTimeout(client, { timeoutMs: deltaHead ? remaining() : timeoutMs }, compileClickHouseTraceQuery(plan, deltaHead));
7491
- const total = Number(rows.at(-1)?.total ?? 0);
7492
- const pageRows = rows.filter((row) => Number(row.__metadata) === 0);
7493
- const payloads = /* @__PURE__ */ new Map();
7494
- if (pageRows.length > 0) {
7495
- const payloadRows = await runWithClickHouseTraceQueryTimeout(client, { timeoutMs: remaining() }, compileClickHouseTraceRootPayloads(pageRows.map((row) => ({
7496
- traceId: String(row.traceId),
7497
- rootSpanId: String(row.rootSpanId),
7498
- startedAt: asIsoTimestamp(row.startedAt),
7499
- endedAt: asIsoTimestamp(row.endedAt)
7500
- }))));
7501
- for (const payload of payloadRows) payloads.set(`${payload.traceId}\u0000${payload.rootSpanId}`, payload);
7502
- }
7503
- const traces = pageRows.map((row) => ({
7504
- ...row,
7505
- ...payloads.get(`${row.traceId}\u0000${row.rootSpanId}`)
7506
- })).map((row) => ({
7507
- traceId: String(row.traceId),
7508
- rootSpanId: String(row.rootSpanId),
7509
- name: row.name,
7510
- entityId: row.entityId ?? null,
7511
- parentSpanId: row.parentSpanId ?? null,
7512
- createdAt: asIsoTimestamp(row.startedAt),
7513
- metadata: parseJson(row.metadata) ?? null,
7514
- inputPreview: coreStorage.buildInputPreview(row.input) ?? null,
7515
- threadId: row.threadId == null ? null : String(row.threadId),
7516
- resourceId: row.resourceId == null ? null : String(row.resourceId),
7517
- startedAt: asIsoTimestamp(row.startedAt),
7518
- endedAt: asIsoTimestamp(row.endedAt),
7519
- entityName: row.entityName == null ? null : String(row.entityName),
7520
- entityType: row.entityType == null ? null : String(row.entityType),
7521
- environment: row.environment == null ? null : String(row.environment),
7522
- status: row.status
7523
- }));
7524
- return coreStorage.traceQueryResponseSchema.parse({
7525
- traces,
7526
- ...deltaHead ? { deltaCursor: coreStorage.encodeTraceQueryDeltaCursor(plan, "clickhouse", JSON.stringify(deltaHead)) } : {},
7527
- pagination: {
7528
- total,
7529
- page: plan.page,
7530
- perPage: plan.perPage,
7531
- hasMore: (plan.page + 1) * plan.perPage < total
7532
- }
7533
- });
7534
- }
7535
- const rows = await runWithClickHouseTraceQueryTimeout(client, { timeoutMs: deltaHead ? remaining() : timeoutMs }, compileClickHouseTraceQuery(plan, deltaHead));
7536
- const visibleRows = rows.slice(0, plan.limit);
7708
+ let first = !deltaHead;
7709
+ const run = (compiled) => {
7710
+ const budget = first ? timeoutMs : remaining();
7711
+ first = false;
7712
+ return runWithClickHouseTraceQueryTimeout(client, { timeoutMs: budget }, compiled);
7713
+ };
7537
7714
  if (plan.result === "groups") {
7538
- const groups = visibleRows.map((row) => ({ threadId: String(row.threadId) }));
7715
+ const rows = await run(compileClickHouseTraceQuery(plan));
7716
+ const groups = rows.slice(0, plan.limit).map((row) => ({ threadId: String(row.threadId) }));
7539
7717
  const last = groups.at(-1);
7540
7718
  return coreStorage.traceQueryResponseSchema.parse({
7541
7719
  groups,
@@ -7545,7 +7723,32 @@ async function queryTraces(client, plan, timeoutMs, strategy = null) {
7545
7723
  }) : null }
7546
7724
  });
7547
7725
  }
7548
- const traces = visibleRows.map((row) => ({
7726
+ /** Rows written before the previews were stored have none; theirs are built from the payloads. */
7727
+ const toTraceRecords = async (rows) => {
7728
+ const missing = rows.filter(needsPreviewPayloads);
7729
+ const payloads = /* @__PURE__ */ new Map();
7730
+ if (missing.length > 0) {
7731
+ const payloadRows = await run(compileClickHouseTraceRootPayloads(missing.map(toTraceRootRowKey), plan.scope, "previewPayloads"));
7732
+ for (const row of payloadRows) payloads.set(traceRootRowKey(row), row);
7733
+ }
7734
+ return rows.map((row) => toTraceRecord({
7735
+ ...row,
7736
+ ...resolveStoredPreviews(row, payloads.get(traceRootRowKey(row)))
7737
+ }));
7738
+ };
7739
+ /** Lists rows, then fetches `metadata` and the previews for the rows that are returned. */
7740
+ const listTraces = async (rows) => {
7741
+ const payloads = /* @__PURE__ */ new Map();
7742
+ if (rows.length > 0) {
7743
+ const payloadRows = await run(compileClickHouseTraceRootPayloads(rows.map(toTraceRootRowKey), plan.scope));
7744
+ for (const payload of payloadRows) payloads.set(traceRootRowKey(payload), payload);
7745
+ }
7746
+ return toTraceRecords(rows.map((row) => ({
7747
+ ...row,
7748
+ ...payloads.get(traceRootRowKey(row))
7749
+ })));
7750
+ };
7751
+ const toTraceRecord = (row) => ({
7549
7752
  traceId: String(row.traceId),
7550
7753
  rootSpanId: String(row.rootSpanId),
7551
7754
  name: row.name,
@@ -7553,7 +7756,9 @@ async function queryTraces(client, plan, timeoutMs, strategy = null) {
7553
7756
  parentSpanId: row.parentSpanId ?? null,
7554
7757
  createdAt: asIsoTimestamp(row.startedAt),
7555
7758
  metadata: parseJson(row.metadata) ?? null,
7556
- inputPreview: coreStorage.buildInputPreview(row.input) ?? null,
7759
+ inputPreview: row.inputPreview ?? null,
7760
+ outputPreview: row.outputPreview ?? null,
7761
+ errorPreview: row.errorPreview ?? null,
7557
7762
  threadId: row.threadId == null ? null : String(row.threadId),
7558
7763
  resourceId: row.resourceId == null ? null : String(row.resourceId),
7559
7764
  startedAt: asIsoTimestamp(row.startedAt),
@@ -7562,9 +7767,11 @@ async function queryTraces(client, plan, timeoutMs, strategy = null) {
7562
7767
  entityType: row.entityType == null ? null : String(row.entityType),
7563
7768
  environment: row.environment == null ? null : String(row.environment),
7564
7769
  status: row.status
7565
- }));
7566
- const last = traces.at(-1);
7770
+ });
7567
7771
  if (plan.paginationMode === "delta") {
7772
+ const rows = await run(compileClickHouseTraceQuery(plan, deltaHead));
7773
+ const visibleRows = rows.slice(0, plan.limit);
7774
+ const traces = await listTraces(visibleRows);
7568
7775
  const lastRow = visibleRows.at(-1);
7569
7776
  let watermark = lastRow ? {
7570
7777
  cursorId: String(lastRow.__delta_cursor),
@@ -7581,12 +7788,36 @@ async function queryTraces(client, plan, timeoutMs, strategy = null) {
7581
7788
  deltaCursor: coreStorage.encodeTraceQueryDeltaCursor(plan, "clickhouse", JSON.stringify(watermark))
7582
7789
  });
7583
7790
  }
7791
+ const keys = await run(compileClickHouseTraceQueryKeys(plan));
7792
+ let total = Number(keys[0]?.__total ?? 0);
7793
+ if (plan.paginationMode === "page" && keys.length === 0 && plan.page > 0) {
7794
+ const [stats] = await run(compileClickHouseTraceQueryTotal(plan));
7795
+ total = Number(stats?.traces ?? 0);
7796
+ }
7797
+ const visible = plan.paginationMode === "page" ? keys : keys.slice(0, plan.limit);
7798
+ const rows = visible.length > 0 ? await run(compileClickHouseTraceRowsByKey(plan, visible.map((key) => ({
7799
+ traceId: String(key.traceId),
7800
+ dedupeKey: String(key.dedupeKey),
7801
+ sortValue: asIsoTimestamp(key.sortValue),
7802
+ rootStartedAt: asIsoTimestamp(key.rootStartedAt)
7803
+ })))) : [];
7804
+ if (plan.paginationMode === "page") return coreStorage.traceQueryResponseSchema.parse({
7805
+ traces: await toTraceRecords(rows),
7806
+ ...deltaHead ? { deltaCursor: coreStorage.encodeTraceQueryDeltaCursor(plan, "clickhouse", JSON.stringify(deltaHead)) } : {},
7807
+ pagination: {
7808
+ total,
7809
+ page: plan.page,
7810
+ perPage: plan.perPage,
7811
+ hasMore: (plan.page + 1) * plan.perPage < total
7812
+ }
7813
+ });
7814
+ const lastKey = visible.at(-1);
7584
7815
  return coreStorage.traceQueryResponseSchema.parse({
7585
- traces,
7586
- page: { next: rows.length > plan.limit && last ? coreStorage.encodeTraceQueryCursor(plan, {
7816
+ traces: await toTraceRecords(rows),
7817
+ page: { next: keys.length > plan.limit && lastKey ? coreStorage.encodeTraceQueryCursor(plan, {
7587
7818
  result: "traces",
7588
- sortValue: last[plan.orderBy.field],
7589
- traceId: last.traceId
7819
+ sortValue: asIsoTimestamp(lastKey.sortValue),
7820
+ traceId: String(lastKey.traceId)
7590
7821
  }) : null }
7591
7822
  });
7592
7823
  }
@@ -7756,13 +7987,29 @@ async function querySpans(client, plan, timeoutMs) {
7756
7987
  page: { next: null }
7757
7988
  };
7758
7989
  const payloadWhere = `tuple(${identitySetColumns}, endedAt) IN {page:Array(Tuple(UInt8, String, UInt8, String, String, String, DateTime64(3, 'UTC')))}`;
7759
- const payloads = await run({
7760
- query: `SELECT ${identities}, ${displayFields.map((field, index) => `data.${index + 1} AS ${field}`).join(", ")},
7761
- data.${displayFields.length + 1} AS inputPreview, data.${displayFields.length + 2} AS outputPreview
7762
- FROM (SELECT ${identities}, any(tuple(${displayFields.map(fieldExpression).join(", ")}, substringUTF8(input, 1, ${coreStorage.SPAN_QUERY_MAX_PREVIEW_CHARACTERS + 1}), substringUTF8(output, 1, ${coreStorage.SPAN_QUERY_MAX_PREVIEW_CHARACTERS + 1}))) AS data
7990
+ const pageParameter = (rows) => rows.map((row) => new TupleParam([...identityValues(row), row.endedAt.replace("T", " ").replace(/Z$/, "")]));
7991
+ const rowsWithPreviews = await run({
7992
+ query: `SELECT ${identities}, ${[...displayFields, ...PREVIEW_COLUMNS].map((field, index) => `data.${index + 1} AS ${field}`).join(", ")}
7993
+ FROM (SELECT ${identities}, any(tuple(${[...displayFields.map(fieldExpression), ...PREVIEW_COLUMNS].join(", ")})) AS data
7763
7994
  FROM ${TABLE_SPAN_EVENTS} e WHERE ${payloadWhere} GROUP BY ${identities})`,
7764
- query_params: { page: page.map((row) => new TupleParam([...identityValues(row), row.endedAt.replace("T", " ").replace(/Z$/, "")])) }
7995
+ query_params: { page: pageParameter(page) }
7765
7996
  });
7997
+ const missingKeys = new Set(rowsWithPreviews.filter(needsPreviewPayloads).map(coreStorage.spanQueryIdentityKey));
7998
+ const missing = page.filter((row) => missingKeys.has(coreStorage.spanQueryIdentityKey(row)));
7999
+ const previewPayloads = /* @__PURE__ */ new Map();
8000
+ if (missing.length) {
8001
+ const rows = await run({
8002
+ query: `SELECT ${identities}, data.1 AS input, data.2 AS output, data.3 AS error
8003
+ FROM (SELECT ${identities}, any(tuple(input, output, error)) AS data
8004
+ FROM ${TABLE_SPAN_EVENTS} e WHERE ${payloadWhere} GROUP BY ${identities})`,
8005
+ query_params: { page: pageParameter(missing) }
8006
+ });
8007
+ for (const row of rows) previewPayloads.set(coreStorage.spanQueryIdentityKey(row), row);
8008
+ }
8009
+ const payloads = rowsWithPreviews.map((row) => ({
8010
+ ...row,
8011
+ ...resolveStoredPreviews(row, previewPayloads.get(coreStorage.spanQueryIdentityKey(row)))
8012
+ }));
7766
8013
  const metricWhere = `${identitySetKey} IN {page:Array(Tuple(UInt8, String, UInt8, String, String, String))}`;
7767
8014
  const metrics = await run({
7768
8015
  query: `SELECT ${identities}, data.1 AS name, data.2 AS estimatedCost,
@@ -7776,17 +8023,16 @@ async function querySpans(client, plan, timeoutMs) {
7776
8023
  });
7777
8024
  if (metrics.length > coreStorage.SPAN_QUERY_MAX_COST_METRICS) throw new coreStorage.TraceQueryResourceLimitError();
7778
8025
  const byIdentity = new Map(payloads.map((row) => [coreStorage.spanQueryIdentityKey(row), row]));
7779
- const hydrated = page.flatMap((identity) => {
8026
+ const result = buildSpanQueryResponse(plan, page.flatMap((identity) => {
7780
8027
  const row = byIdentity.get(coreStorage.spanQueryIdentityKey(identity));
7781
8028
  if (!row) return [];
7782
- const { inputPreview: _input, outputPreview: _output, ...metadata } = row;
8029
+ const { inputPreview: _input, outputPreview: _output, errorPreview: _error, ...metadata } = row;
7783
8030
  return [{
7784
8031
  ...metadata,
7785
8032
  ...identity,
7786
8033
  durationMs: Number(row.durationMs)
7787
8034
  }];
7788
- });
7789
- const result = coreStorage.buildSpanQueryResponse(plan, hydrated, payloads, metrics);
8035
+ }), payloads, metrics);
7790
8036
  const last = page.at(-1);
7791
8037
  result.page.next = selected.length > plan.limit ? coreStorage.encodeSpanQueryCursor(plan, {
7792
8038
  organizationId: last.organizationId,
@@ -8090,9 +8336,9 @@ async function getRootSpan(client, args) {
8090
8336
  return { span: rowToSpanRecord(rows[0]) };
8091
8337
  }
8092
8338
  /**
8093
- * Columns a trace list renders. `input` is selected only so the row mapper can
8094
- * derive a short `inputPreview` at read time (see `rowToLightSpanRecord`); the
8095
- * raw blob itself never leaves the store.
8339
+ * Columns a trace list renders. The previews are the stored ones; rows written
8340
+ * before they were stored get theirs from the payload columns, fetched for just
8341
+ * those rows (see `withPreviewPayloads`).
8096
8342
  */
8097
8343
  const LIGHT_TRACE_ROOT_FIELDS = [
8098
8344
  "traceId",
@@ -8107,10 +8353,13 @@ const LIGHT_TRACE_ROOT_FIELDS = [
8107
8353
  "entityId",
8108
8354
  "entityName",
8109
8355
  "threadId",
8356
+ "organizationId",
8110
8357
  "resourceId",
8111
8358
  "error",
8112
8359
  "metadataRaw",
8113
- "input"
8360
+ "inputPreview",
8361
+ "outputPreview",
8362
+ "errorPreview"
8114
8363
  ];
8115
8364
  const FULL_PROJECTION = {
8116
8365
  innerSelect: "*",
@@ -8119,7 +8368,7 @@ const FULL_PROJECTION = {
8119
8368
  };
8120
8369
  const LIGHT_PROJECTION = {
8121
8370
  innerSelect: ["dedupeKey", ...LIGHT_TRACE_ROOT_FIELDS].join(", "),
8122
- outerSelect: LIGHT_TRACE_ROOT_FIELDS.join(", "),
8371
+ outerSelect: ["dedupeKey", ...LIGHT_TRACE_ROOT_FIELDS].join(", "),
8123
8372
  deltaSelect: LIGHT_TRACE_ROOT_FIELDS.filter((field) => field !== "startedAt" && field !== "traceId").map((field) => `r.${field}`).join(", ")
8124
8373
  };
8125
8374
  /**
@@ -8165,7 +8414,7 @@ async function listTraceRows(client, args, strategy, projection, mapRows) {
8165
8414
  const rows = await queryTracesAfterCursor(client, projection.deltaSelect, whereClause, params, limit, afterCursor);
8166
8415
  const visibleRows = rows.slice(0, limit);
8167
8416
  return {
8168
- spans: mapRows(visibleRows),
8417
+ spans: await mapRows(visibleRows),
8169
8418
  delta: {
8170
8419
  limit,
8171
8420
  hasMore: rows.length > limit
@@ -8240,7 +8489,7 @@ async function listTraceRows(client, args, strategy, projection, mapRows) {
8240
8489
  perPage,
8241
8490
  hasMore: (page + 1) * perPage < total
8242
8491
  },
8243
- spans: mapRows(rows),
8492
+ spans: await mapRows(rows),
8244
8493
  ...deltaCursorEnabled ? { deltaCursor: currentDeltaCursor } : {}
8245
8494
  };
8246
8495
  }
@@ -8249,12 +8498,32 @@ async function listTraces(client, args, strategy) {
8249
8498
  return listTraceRows(client, args, strategy, FULL_PROJECTION, (rows) => toTraceSpans(rows.map(rowToSpanRecord)));
8250
8499
  }
8251
8500
  /**
8501
+ * Adds `input` / `output` / `error` to the rows written before previews were stored, read from
8502
+ * the same root row version (same `endedAt`) the list kept. A page can hold several tenants' rows,
8503
+ * so each row is looked up by its own identity, tenant included, not by one shared scope.
8504
+ */
8505
+ async function withPreviewPayloads(client, rows) {
8506
+ const missing = rows.filter(needsPreviewPayloads);
8507
+ if (missing.length === 0) return rows;
8508
+ const compiled = compileClickHouseTraceRootPayloads(missing.map(toTraceRootRowKey), void 0, "previewPayloads");
8509
+ const result = await client.query({
8510
+ ...compiled,
8511
+ format: "JSONEachRow",
8512
+ clickhouse_settings: CH_SETTINGS
8513
+ });
8514
+ const payloads = new Map((await result.json()).map((row) => [traceRootRowKey(row), row]));
8515
+ return rows.map((row) => needsPreviewPayloads(row) ? {
8516
+ ...row,
8517
+ ...payloads.get(traceRootRowKey(row))
8518
+ } : row);
8519
+ }
8520
+ /**
8252
8521
  * List traces projecting only the columns a trace list renders.
8253
- * Skips the attributes/output blobs and reduces `input` to a short preview in
8254
- * the mapper, so the response payload stays flat as traces grow.
8522
+ * Skips the attributes/input/output blobs and reads the stored previews instead,
8523
+ * so the response payload stays flat as traces grow.
8255
8524
  */
8256
8525
  async function listTracesLight(client, args, strategy) {
8257
- return listTraceRows(client, args, strategy, LIGHT_PROJECTION, (rows) => rows.map(rowToLightSpanRecord));
8526
+ return listTraceRows(client, args, strategy, LIGHT_PROJECTION, async (rows) => (await withPreviewPayloads(client, rows)).map(rowToLightSpanRecord));
8258
8527
  }
8259
8528
  async function queryTracesAfterCursor(client, deltaSelect, whereClause, params, limit, cursorId) {
8260
8529
  const deltaKeys = `SELECT startedAt, traceId, dedupeKey FROM ${TABLE_TRACE_ROOTS_DELTA} WHERE cursorId > {afterCursor:UInt64}`;