@mastra/clickhouse 1.26.0 → 1.27.0-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
@@ -8,7 +8,7 @@ import { parseFieldKey } from "@mastra/core/utils";
8
8
  import { coreFeatures } from "@mastra/core/features";
9
9
  import { z } from "zod/v4";
10
10
  import { saveScorePayloadSchema } from "@mastra/core/evals";
11
- //#region src/storage/db/replication.ts
11
+ // src/storage/db/replication.ts
12
12
  const DEFAULT_ZOOKEEPER_PATH = "/clickhouse/tables/{shard}/{database}/{table}";
13
13
  const DEFAULT_REPLICA_NAME = "{replica}";
14
14
  const SUPPORTED_ENGINE_NAMES = /* @__PURE__ */ new Set([
@@ -208,8 +208,7 @@ function rewriteEngineClauses(sql, replication) {
208
208
  function applyReplicationToDDL(sql, replication) {
209
209
  return addOnClusterToDDL(replication ? rewriteEngineClauses(sql, replication) : sql, replication);
210
210
  }
211
- //#endregion
212
- //#region src/storage/db/utils.ts
211
+ // src/storage/db/utils.ts
213
212
  const TABLE_ENGINES = {
214
213
  [TABLE_MESSAGES]: `MergeTree()`,
215
214
  [TABLE_WORKFLOW_SNAPSHOT]: `ReplacingMergeTree()`,
@@ -318,8 +317,7 @@ function transformRow(row) {
318
317
  function transformRows(rows) {
319
318
  return rows.map((row) => transformRow(row));
320
319
  }
321
- //#endregion
322
- //#region src/storage/db/index.ts
320
+ // src/storage/db/index.ts
323
321
  /**
324
322
  * Resolves ClickhouseDomainConfig to a ClickHouse client and ttl config.
325
323
  * Handles creating a new client if config is provided.
@@ -845,8 +843,7 @@ var ClickhouseDB = class extends MastraBase {
845
843
  }
846
844
  }
847
845
  };
848
- //#endregion
849
- //#region src/storage/domains/memory/index.ts
846
+ // src/storage/domains/memory/index.ts
850
847
  /**
851
848
  * Serialize metadata object to JSON string for storage in ClickHouse.
852
849
  * Ensures we always store valid JSON, defaulting to '{}' for null/undefined.
@@ -1989,8 +1986,7 @@ var MemoryStorageClickhouse = class extends MemoryStorage {
1989
1986
  }
1990
1987
  }
1991
1988
  };
1992
- //#endregion
1993
- //#region src/storage/domains/observability/index.ts
1989
+ // src/storage/domains/observability/index.ts
1994
1990
  var ObservabilityStorageClickhouse = class extends ObservabilityStorage {
1995
1991
  client;
1996
1992
  #db;
@@ -2595,8 +2591,7 @@ var ObservabilityStorageClickhouse = class extends ObservabilityStorage {
2595
2591
  }
2596
2592
  }
2597
2593
  };
2598
- //#endregion
2599
- //#region src/storage/domains/observability/v-next/ddl.ts
2594
+ // src/storage/domains/observability/v-next/ddl.ts
2600
2595
  const TABLE_SPAN_EVENTS = "mastra_span_events";
2601
2596
  const TABLE_TRACE_ROOTS = "mastra_trace_roots";
2602
2597
  const TABLE_TRACE_BRANCHES = "mastra_trace_branches";
@@ -3648,6 +3643,9 @@ const ALL_MIGRATIONS = [
3648
3643
  addColumn(TABLE_SPAN_EVENTS, "inputPreview", "Nullable(String)"),
3649
3644
  addColumn(TABLE_SPAN_EVENTS, "outputPreview", "Nullable(String)"),
3650
3645
  addColumn(TABLE_SPAN_EVENTS, "errorPreview", "Nullable(String)"),
3646
+ addColumn(TABLE_TRACE_BRANCHES, "entityVersionId", "Nullable(String)"),
3647
+ addColumn(TABLE_TRACE_BRANCHES, "parentEntityVersionId", "Nullable(String)"),
3648
+ addColumn(TABLE_TRACE_BRANCHES, "rootEntityVersionId", "Nullable(String)"),
3651
3649
  addColumn(TABLE_SPAN_EVENTS, "entityVersionId", "Nullable(String)"),
3652
3650
  addColumn(TABLE_SPAN_EVENTS, "parentEntityVersionId", "Nullable(String)"),
3653
3651
  addColumn(TABLE_SPAN_EVENTS, "rootEntityVersionId", "Nullable(String)"),
@@ -3796,8 +3794,7 @@ function parseTtlExpression(expr) {
3796
3794
  days
3797
3795
  };
3798
3796
  }
3799
- //#endregion
3800
- //#region src/storage/domains/observability/v-next/preview-compat.ts
3797
+ // src/storage/domains/observability/v-next/preview-compat.ts
3801
3798
  const generator = coreStorage.buildStoredPreview;
3802
3799
  const canGeneratePreviews = typeof generator === "function";
3803
3800
  function needsPreview(value) {
@@ -3850,8 +3847,7 @@ function buildSpanQueryResponse(...args) {
3850
3847
  }
3851
3848
  return response;
3852
3849
  }
3853
- //#endregion
3854
- //#region src/storage/domains/observability/v-next/previews.ts
3850
+ // src/storage/domains/observability/v-next/previews.ts
3855
3851
  /**
3856
3852
  * Stored span previews (`inputPreview` / `outputPreview` / `errorPreview`).
3857
3853
  *
@@ -3886,8 +3882,7 @@ function resolveStoredPreviews(row, payloads = row) {
3886
3882
  }
3887
3883
  return previews;
3888
3884
  }
3889
- //#endregion
3890
- //#region src/storage/domains/observability/v-next/review-status.ts
3885
+ // src/storage/domains/observability/v-next/review-status.ts
3891
3886
  const FEEDBACK_REVIEW_STATUSES = ["needs-review", "reviewed"];
3892
3887
  function isFeedbackReviewStatus(value) {
3893
3888
  return FEEDBACK_REVIEW_STATUSES.some((status) => status === value);
@@ -3911,8 +3906,7 @@ function parseUpdateFeedbackReviewStatusArgs(args) {
3911
3906
  reviewStatus: args.reviewStatus
3912
3907
  };
3913
3908
  }
3914
- //#endregion
3915
- //#region src/storage/domains/observability/v-next/helpers.ts
3909
+ // src/storage/domains/observability/v-next/helpers.ts
3916
3910
  const CH_SETTINGS = {
3917
3911
  date_time_input_format: "best_effort",
3918
3912
  date_time_output_format: "iso",
@@ -4482,8 +4476,7 @@ function feedbackRecordToRow(feedback) {
4482
4476
  scope: jsonEncode(feedback.scope)
4483
4477
  };
4484
4478
  }
4485
- //#endregion
4486
- //#region src/storage/domains/observability/v-next/deletion-requests.ts
4479
+ // src/storage/domains/observability/v-next/deletion-requests.ts
4487
4480
  const EPOCH = "1970-01-01T00:00:00.000Z";
4488
4481
  async function recordDeletionRequest(client, args) {
4489
4482
  const row = {
@@ -4540,8 +4533,7 @@ async function insertDeletionRequest(client, row, replication) {
4540
4533
  } : CH_INSERT_SETTINGS
4541
4534
  });
4542
4535
  }
4543
- //#endregion
4544
- //#region src/storage/domains/observability/v-next/discovery.ts
4536
+ // src/storage/domains/observability/v-next/discovery.ts
4545
4537
  async function queryJson$3(client, query, params = {}) {
4546
4538
  return await (await client.query({
4547
4539
  query,
@@ -4581,8 +4573,7 @@ async function getTags(client, args) {
4581
4573
  }
4582
4574
  return { tags: (await queryJson$3(client, `SELECT DISTINCT value FROM ${TABLE_DISCOVERY_VALUES} WHERE ${conditions.join(" AND ")} ORDER BY value`, params)).map((r) => r.value) };
4583
4575
  }
4584
- //#endregion
4585
- //#region src/storage/domains/observability/v-next/filters.ts
4576
+ // src/storage/domains/observability/v-next/filters.ts
4586
4577
  /**
4587
4578
  * ClickHouse v-next filter builders.
4588
4579
  *
@@ -4854,8 +4845,7 @@ function buildPaginationClause(pagination) {
4854
4845
  offset: page * perPage
4855
4846
  };
4856
4847
  }
4857
- //#endregion
4858
- //#region src/storage/domains/observability/v-next/polling.ts
4848
+ // src/storage/domains/observability/v-next/polling.ts
4859
4849
  const OBSERVABILITY_DELTA_POLLING_FEATURE = "observability-delta-polling";
4860
4850
  function deltaPollingFeatureEnabled() {
4861
4851
  return coreFeatures.has(OBSERVABILITY_DELTA_POLLING_FEATURE);
@@ -4886,8 +4876,7 @@ function appendWhere(whereClause, ...conditions) {
4886
4876
  const parts = [whereClause.replace(/^\s*WHERE\s+/i, ""), ...conditions].filter(Boolean);
4887
4877
  return parts.length > 0 ? `WHERE ${parts.join(" AND ")}` : "";
4888
4878
  }
4889
- //#endregion
4890
- //#region src/storage/domains/observability/v-next/feedback.ts
4879
+ // src/storage/domains/observability/v-next/feedback.ts
4891
4880
  const FEEDBACK_TYPED_COLUMNS = /* @__PURE__ */ new Set([
4892
4881
  "timestamp",
4893
4882
  "traceId",
@@ -5389,8 +5378,7 @@ async function getFeedbackPercentiles(client, args) {
5389
5378
  }))
5390
5379
  })) };
5391
5380
  }
5392
- //#endregion
5393
- //#region src/storage/domains/observability/v-next/logs.ts
5381
+ // src/storage/domains/observability/v-next/logs.ts
5394
5382
  async function batchCreateLogs(client, args) {
5395
5383
  if (args.logs.length === 0) return;
5396
5384
  await client.insert({
@@ -5529,8 +5517,7 @@ async function getStreamHeadCursor$4(client) {
5529
5517
  function buildLogsCursor(row) {
5530
5518
  return row.cursorId ?? "0";
5531
5519
  }
5532
- //#endregion
5533
- //#region src/storage/domains/observability/v-next/metrics.ts
5520
+ // src/storage/domains/observability/v-next/metrics.ts
5534
5521
  /** Map typed columns to their ClickHouse column names. */
5535
5522
  const METRIC_TYPED_COLUMNS = /* @__PURE__ */ new Set([
5536
5523
  "timestamp",
@@ -6027,8 +6014,7 @@ async function getMetricLabelValues(client, args) {
6027
6014
  if (args.limit) params.valLimit = args.limit;
6028
6015
  return { values: (await queryJson$1(client, `SELECT DISTINCT value FROM ${TABLE_DISCOVERY_PAIRS} WHERE ${conditions.join(" AND ")} ORDER BY value ${limitClause}`, params)).map((r) => r.value) };
6029
6016
  }
6030
- //#endregion
6031
- //#region src/storage/domains/observability/v-next/migration.ts
6017
+ // src/storage/domains/observability/v-next/migration.ts
6032
6018
  const SIGNAL_MIGRATIONS = [
6033
6019
  {
6034
6020
  table: TABLE_METRIC_EVENTS,
@@ -6344,8 +6330,7 @@ async function migrateSignalTables(client, logger) {
6344
6330
  }
6345
6331
  }
6346
6332
  }
6347
- //#endregion
6348
- //#region src/storage/domains/observability/v-next/score-current.ts
6333
+ // src/storage/domains/observability/v-next/score-current.ts
6349
6334
  const SCORE_CURRENT_BACKFILL_MARKER = "v1";
6350
6335
  async function backfillCurrentScores(client) {
6351
6336
  const markerRows = await (await client.query({
@@ -6374,8 +6359,7 @@ async function backfillCurrentScores(client) {
6374
6359
  query_params: { marker: SCORE_CURRENT_BACKFILL_MARKER }
6375
6360
  });
6376
6361
  }
6377
- //#endregion
6378
- //#region src/storage/domains/observability/v-next/scores.ts
6362
+ // src/storage/domains/observability/v-next/scores.ts
6379
6363
  const SCORE_TYPED_COLUMNS = /* @__PURE__ */ new Set([
6380
6364
  "timestamp",
6381
6365
  "traceId",
@@ -6842,8 +6826,7 @@ async function getScorePercentiles(client, args) {
6842
6826
  }))
6843
6827
  })) };
6844
6828
  }
6845
- //#endregion
6846
- //#region src/storage/domains/observability/v-next/trace-query.ts
6829
+ // src/storage/domains/observability/v-next/trace-query.ts
6847
6830
  const TRACE_STATUS_SQL = `if(isNotNull(r.error), 'error', 'success')`;
6848
6831
  /**
6849
6832
  * Core versions without the field reject it in strict query results, so it is only returned when core knows it.
@@ -7989,8 +7972,7 @@ async function queryThreads(client, plan, timeoutMs) {
7989
7972
  });
7990
7973
  }
7991
7974
  /** Compile a direct span-query filter using its dedicated field mapping. */
7992
- function compileSpanQueryPredicate(predicate) {
7993
- const parameters = new ParameterBuilder();
7975
+ function compileSpanQueryPredicate(predicate, parameters = new ParameterBuilder()) {
7994
7976
  return {
7995
7977
  sql: compileScalarPredicate(predicate, SPAN_QUERY_FIELDS, parameters),
7996
7978
  params: parameters.params
@@ -8006,8 +7988,7 @@ function compileThreadQueryPredicate(predicate, parameters = new ParameterBuilde
8006
7988
  params: parameters.params
8007
7989
  };
8008
7990
  }
8009
- //#endregion
8010
- //#region src/storage/domains/observability/v-next/span-query.ts
7991
+ // src/storage/domains/observability/v-next/span-query.ts
8011
7992
  const identityFields = [
8012
7993
  "organizationId",
8013
7994
  "resourceId",
@@ -8151,6 +8132,131 @@ function compileClickHouseSpanQuery(plan) {
8151
8132
  }
8152
8133
  };
8153
8134
  }
8135
+ const branchSpanTypes = new Set(BRANCH_SPAN_TYPE_VALUES);
8136
+ const sameColumn = (field) => [field];
8137
+ /**
8138
+ * Span fields `trace_branches` can serve, as the `trace_branches` columns each one reads. The two
8139
+ * tables are expected to diverge, so this list is explicit: a selection whose fields are not all
8140
+ * listed reads `span_events`. A new span-query field has to be classified here deliberately, and a
8141
+ * missing column then costs speed, not correctness.
8142
+ */
8143
+ const TRACE_BRANCHES_SPAN_FIELD_COLUMNS = {
8144
+ ...Object.fromEntries([
8145
+ "organizationId",
8146
+ "resourceId",
8147
+ "traceId",
8148
+ "spanId",
8149
+ "parentSpanId",
8150
+ "name",
8151
+ "spanType",
8152
+ "startedAt",
8153
+ "endedAt",
8154
+ "timeToFirstChunkMs",
8155
+ "threadId",
8156
+ "runId",
8157
+ "sessionId",
8158
+ "userId",
8159
+ "requestId",
8160
+ "serviceName",
8161
+ "experimentId",
8162
+ "environment",
8163
+ "executionSource",
8164
+ "entityType",
8165
+ "entityId",
8166
+ "entityName",
8167
+ "entityVersionId",
8168
+ "parentEntityType",
8169
+ "parentEntityId",
8170
+ "parentEntityName",
8171
+ "parentEntityVersionId",
8172
+ "rootEntityType",
8173
+ "rootEntityId",
8174
+ "rootEntityName",
8175
+ "rootEntityVersionId"
8176
+ ].map((field) => [field, sameColumn(field)])),
8177
+ status: ["error"],
8178
+ error: ["error"],
8179
+ durationMs: ["startedAt", "endedAt"]
8180
+ };
8181
+ /**
8182
+ * Whether a span selection reads `trace_branches`: `where` must restrict `spanType` to branch
8183
+ * types, and `trace_branches` must serve every selected field (`served`, for tests).
8184
+ */
8185
+ function readsTraceBranches(where, fields, served = TRACE_BRANCHES_SPAN_FIELD_COLUMNS) {
8186
+ return positiveSpanConjuncts(where).some(({ field, values }) => field === "spanType" && values.length > 0 && values.every((value) => branchSpanTypes.has(String(value)))) && [...fields].every((field) => Object.hasOwn(served, field));
8187
+ }
8188
+ /**
8189
+ * Top-level `spanType` / `name` equality or `in` conditions. Every span the predicate can match
8190
+ * satisfies them, and copies of a span never differ in either field, so they can be applied
8191
+ * before collapsing copies.
8192
+ */
8193
+ function positiveSpanConjuncts(where) {
8194
+ return (where?.type === "boolean" && where.operator === "and" ? where.args : where ? [where] : []).flatMap((conjunct) => {
8195
+ if (conjunct.type !== "comparison" && conjunct.type !== "membership") return [];
8196
+ if (conjunct.field !== "spanType" && conjunct.field !== "name") return [];
8197
+ if (conjunct.type === "comparison" && conjunct.operator === "eq") return [{
8198
+ field: conjunct.field,
8199
+ values: [conjunct.value]
8200
+ }];
8201
+ if (conjunct.type === "membership" && conjunct.operator === "in") return [{
8202
+ field: conjunct.field,
8203
+ values: conjunct.values
8204
+ }];
8205
+ return [];
8206
+ });
8207
+ }
8208
+ /**
8209
+ * The spans `querySpans()` selects, as CTEs ending in `selected` (alias each row as you like):
8210
+ * one row per current completed span with `startedAt` in the window, matching `where`, carrying
8211
+ * the identity, `startedAt`, `endedAt`, `status`, and `fields`.
8212
+ *
8213
+ * Copies of a span collapse to the copy with the latest `endedAt`. When `where` requires branch
8214
+ * span types (agent and workflow runs, tool calls, …) and `trace_branches` serves every selected
8215
+ * field, the spans are read from `trace_branches`, whose sort key starts with `spanType`, instead of
8216
+ * every span in the window. That path can differ from `querySpans()` in two ways:
8217
+ * - `trace_branches` is filled only by its materialized view, with no backfill, so an install that
8218
+ * upgraded from @mastra/clickhouse 1.4–1.6 has no branch rows for spans written before then.
8219
+ * - It is a `ReplacingMergeTree` without a version column, so once a background merge collapses a
8220
+ * retried span's copies it keeps the last-inserted copy, not the one with the latest `endedAt`.
8221
+ * Counts are unaffected; `status` and duration of an out-of-order retry can differ.
8222
+ *
8223
+ * Like `querySpans()`, the read has no upper bound on `endedAt` (`PREWHERE endedAt >= from` only,
8224
+ * since a span that starts in the window can end any time later), so a historical window reads
8225
+ * every span that ended since `from`.
8226
+ */
8227
+ function compileClickHouseSpanSelection(plan, fields, parameters) {
8228
+ const from = parameters.add(plan.timeRange.from, "DateTime64(3, 'UTC')");
8229
+ const to = parameters.add(plan.timeRange.to, "DateTime64(3, 'UTC')");
8230
+ const prefilter = positiveSpanConjuncts(plan.where).map(({ field, values }) => values.length ? `\n AND e.${field} IN (${values.map((value) => parameters.add(String(value), "String")).join(", ")})` : "\n AND 0").join("");
8231
+ const predicate = plan.where ? compileSpanQueryPredicate(plan.where, parameters).sql : "1";
8232
+ let tenant = "";
8233
+ if (plan.scope) {
8234
+ tenant += `\n AND e.organizationId = ${parameters.add(plan.scope.organizationId, "String")}`;
8235
+ if (plan.scope.resourceId !== void 0) tenant += `\n AND e.resourceId = ${parameters.add(plan.scope.resourceId, "String")}`;
8236
+ }
8237
+ const selectionFields = [.../* @__PURE__ */ new Set([
8238
+ "startedAt",
8239
+ "endedAt",
8240
+ "status",
8241
+ ...fields,
8242
+ ...predicateFields$1(plan.where)
8243
+ ])].filter((field) => !new Set(identityFields).has(field));
8244
+ const branches = readsTraceBranches(plan.where, selectionFields);
8245
+ const scan = `FROM ${branches ? TABLE_TRACE_BRANCHES : TABLE_SPAN_EVENTS} e
8246
+ PREWHERE e.endedAt >= ${from}
8247
+ WHERE e.startedAt >= ${from} AND e.startedAt < ${to}${tenant}${prefilter}`;
8248
+ return [`span_candidates AS (
8249
+ ${branches ? `SELECT ${identityFields.map((field) => `e.${field} AS ${field}`).join(", ")}, ${selectionFields.map((field) => field === "endedAt" ? "max(e.endedAt) AS endedAt" : `argMax(tuple(${fieldExpression(field)}), e.endedAt).1 AS ${field}`).join(", ")}
8250
+ ${scan}
8251
+ GROUP BY ${identityFields.map((field) => `e.${field}`).join(", ")}` : `SELECT ${identities}, ${selectionFields.map((field) => `${fieldExpression(field)} AS ${field}`).join(", ")}
8252
+ ${scan}
8253
+ ORDER BY ${identities}, e.endedAt DESC
8254
+ LIMIT 1 BY ${identities}`}
8255
+ )`, `selected AS (
8256
+ SELECT * FROM span_candidates s
8257
+ WHERE s.startedAt >= ${from} AND s.startedAt < ${to} AND (${predicate})
8258
+ )`];
8259
+ }
8154
8260
  const LEGACY_PAYLOAD_BATCH_ROWS = 32;
8155
8261
  function iso$1(value) {
8156
8262
  return new Date(value.includes("T") ? value : `${value.replace(" ", "T")}Z`).toISOString();
@@ -8239,8 +8345,7 @@ async function querySpans(client, plan, timeoutMs) {
8239
8345
  }) : null;
8240
8346
  return result;
8241
8347
  }
8242
- //#endregion
8243
- //#region src/storage/domains/observability/v-next/thread-query.ts
8348
+ // src/storage/domains/observability/v-next/thread-query.ts
8244
8349
  const contextFields = [
8245
8350
  "resourceId",
8246
8351
  "userId",
@@ -8534,8 +8639,7 @@ async function queryThreadRows(client, plan, timeoutMs, strategy = "auto") {
8534
8639
  }) : null }
8535
8640
  };
8536
8641
  }
8537
- //#endregion
8538
- //#region src/storage/domains/observability/v-next/trace-aggregate.ts
8642
+ // src/storage/domains/observability/v-next/trace-aggregate.ts
8539
8643
  /** Trace-root column expressions (alias `r`) for each groupable dimension; same values `where` sees. */
8540
8644
  const DIMENSION_SQL = {
8541
8645
  entityType: "r.entityType",
@@ -8554,6 +8658,28 @@ const DIMENSION_SQL = {
8554
8658
  organizationId: "r.organizationId",
8555
8659
  experimentId: "r.experimentId"
8556
8660
  };
8661
+ /** Span column expressions (alias `r`, a `selected` span row) for each groupable span dimension. */
8662
+ const SPAN_DIMENSION_SQL = Object.fromEntries([
8663
+ "name",
8664
+ "spanType",
8665
+ "entityType",
8666
+ "entityId",
8667
+ "entityName",
8668
+ "parentEntityType",
8669
+ "parentEntityName",
8670
+ "rootEntityType",
8671
+ "rootEntityName",
8672
+ "environment",
8673
+ "status",
8674
+ "serviceName",
8675
+ "executionSource",
8676
+ "threadId",
8677
+ "resourceId",
8678
+ "userId",
8679
+ "sessionId",
8680
+ "organizationId",
8681
+ "experimentId"
8682
+ ].map((field) => [field, `r.${field}`]));
8557
8683
  const PERCENTILES = {
8558
8684
  "duration.p50": .5,
8559
8685
  "duration.p90": .9,
@@ -8580,13 +8706,41 @@ const COMPARISON_SQL = {
8580
8706
  */
8581
8707
  function compileClickHouseTraceAggregate(plan) {
8582
8708
  const parameters = new ParameterBuilder();
8583
- const ctes = compileClickHouseTraceCandidates(plan, "*", parameters, { oneRootPerTrace: true });
8584
- const dimensionSql = (field) => {
8585
- if (field.startsWith("metadata.")) return traceMetadataValueSql(parameters.add(field.slice(9), "String"));
8586
- const sql = Object.hasOwn(DIMENSION_SQL, field) ? DIMENSION_SQL[field] : void 0;
8587
- if (sql === void 0) throw new Error(`Unsupported trusted trace-aggregate field: ${field}`);
8588
- return sql;
8589
- };
8709
+ return compileClickHouseAggregate(plan, parameters, {
8710
+ ctes: compileClickHouseTraceCandidates(plan, "*", parameters, { oneRootPerTrace: true }),
8711
+ from: "candidates",
8712
+ dimensionSql: (field) => {
8713
+ if (field.startsWith("metadata.")) return traceMetadataValueSql(parameters.add(field.slice(9), "String"));
8714
+ return lookupDimensionSql(DIMENSION_SQL, field);
8715
+ },
8716
+ isErrorSql: "isNotNull(r.error)",
8717
+ usageKey: ["traceId"],
8718
+ usageBound: "traceId IN (SELECT traceId FROM candidates)"
8719
+ });
8720
+ }
8721
+ /**
8722
+ * Compiles a trusted span aggregate plan over the spans `querySpans()` selects. Usage is
8723
+ * attributed by `(traceId, spanId)`, so only the span that recorded a metric row carries it.
8724
+ */
8725
+ function compileClickHouseSpanAggregate(plan) {
8726
+ const parameters = new ParameterBuilder();
8727
+ return compileClickHouseAggregate(plan, parameters, {
8728
+ ctes: compileClickHouseSpanSelection(plan, [...plan.dimensions, ...plan.measures.flatMap((measure) => measure.type === "countDistinct" && measure.field !== "traceId" ? [measure.field] : [])], parameters),
8729
+ from: "selected",
8730
+ dimensionSql: (field) => lookupDimensionSql(SPAN_DIMENSION_SQL, field),
8731
+ isErrorSql: "r.status = 'error'",
8732
+ usageKey: ["traceId", "spanId"],
8733
+ usageBound: "isNotNull(spanId)"
8734
+ });
8735
+ }
8736
+ function lookupDimensionSql(columns, field) {
8737
+ const sql = Object.hasOwn(columns, field) ? columns[field] : void 0;
8738
+ if (sql === void 0) throw new Error(`Unsupported trusted aggregate field: ${field}`);
8739
+ return sql;
8740
+ }
8741
+ function compileClickHouseAggregate(plan, parameters, source) {
8742
+ const { ctes } = source;
8743
+ const dimensionSql = source.dimensionSql;
8590
8744
  const distinctSql = (field) => field === "traceId" ? "r.traceId" : dimensionSql(field);
8591
8745
  const dimensionColumns = plan.dimensions.map((_, index) => `d${index}`);
8592
8746
  const distinctColumns = /* @__PURE__ */ new Map();
@@ -8597,7 +8751,7 @@ function compileClickHouseTraceAggregate(plan) {
8597
8751
  distinctColumns.set(measure.field, column);
8598
8752
  factColumns.push(`CAST(${distinctSql(measure.field)} AS Nullable(String)) AS ${column}`);
8599
8753
  }
8600
- factColumns.push(`toFloat64(${durationMsSql("r.startedAt", "r.endedAt")}) AS durationMs`, `isNotNull(r.error) AS isError`, `cityHash64(r.traceId) AS traceSeed`);
8754
+ factColumns.push(`toFloat64(${durationMsSql("r.startedAt", "r.endedAt")}) AS durationMs`, `${source.isErrorSql} AS isError`, `cityHash64(${source.usageKey.map((column) => `r.${column}`).join(", ")}) AS traceSeed`);
8601
8755
  if (plan.interval !== void 0) {
8602
8756
  const intervalMs = coreStorage.TRACE_AGGREGATE_INTERVAL_MS[plan.interval];
8603
8757
  if (intervalMs === void 0) throw new Error(`Unsupported trusted trace-aggregate interval: ${plan.interval}`);
@@ -8607,14 +8761,14 @@ function compileClickHouseTraceAggregate(plan) {
8607
8761
  const hasCost = measureKinds.has("cost");
8608
8762
  let usageJoin = "";
8609
8763
  if (hasCost || measureKinds.has("tokens")) {
8610
- ctes.push(compileUsageCte(plan, parameters));
8611
- usageJoin = "\n LEFT JOIN usage u ON u.traceId = r.traceId";
8764
+ ctes.push(compileUsageCte(plan, parameters, source));
8765
+ usageJoin = `\n LEFT JOIN usage u ON ${source.usageKey.map((column) => `u.${column} = r.${column}`).join(" AND ")}`;
8612
8766
  USAGE_METRIC_NAMES.forEach((_, index) => factColumns.push(`if(u.hasUsage = 1, u.t${index}, NULL) AS t${index}`));
8613
8767
  factColumns.push(`u.hasUsage = 1 AS usageBearing`, `if(u.hasUsage = 1 AND u.priced = 1, u.cost, NULL) AS traceCost`, `u.hasUsage = 1 AND u.priced = 1 AND u.pricingFailure = 0 AS covered`, `if(u.hasUsage = 1 AND u.priced = 1, u.unitMin, NULL) AS unitMin`, `if(u.hasUsage = 1 AND u.priced = 1, u.unitMax, NULL) AS unitMax`);
8614
8768
  }
8615
8769
  ctes.push(`facts AS (
8616
8770
  SELECT ${factColumns.join(",\n ")}
8617
- FROM candidates r${usageJoin}
8771
+ FROM ${source.from} r${usageJoin}
8618
8772
  )`);
8619
8773
  const mixedUnitsSql = "minOrNull(unitMin) != maxOrNull(unitMax)";
8620
8774
  const measureSql = (name) => {
@@ -8729,26 +8883,28 @@ function usageMetricIndex(name) {
8729
8883
  return index;
8730
8884
  }
8731
8885
  /**
8732
- * Builds `usage`: one row per candidate trace that has at least one token metric row, holding
8733
- * per-name token sums (`t<i>`, indexed by `TRACE_AGGREGATE_USAGE_METRIC_NAMES`) and the trace's
8734
- * pricing state. Metric rows are matched by traceId, usage metric name, tenant scope, and
8735
- * `timestamp >= from` only: a trace that starts in the window can emit metrics after `to`, and
8736
- * the candidates semi-join already bounds the scan.
8886
+ * Builds `usage`: one row per usage key (a trace, or a span) that has at least one token metric
8887
+ * row, holding per-name token sums (`t<i>`, indexed by `TRACE_AGGREGATE_USAGE_METRIC_NAMES`) and
8888
+ * its pricing state. Metric rows are matched by the source's usage bound, usage metric name,
8889
+ * tenant scope, and `timestamp >= from` only: a trace or span that starts in the window can emit
8890
+ * metrics after `to`. For spans that makes the read cost grow with the age of `from` (see
8891
+ * `usageBound`).
8737
8892
  *
8738
8893
  * Rows are deduplicated with `LIMIT 1 BY metricId` at query time. `ReplacingMergeTree` only
8739
8894
  * collapses rows with equal sort keys `(name, timestamp, metricId)`, so retried copies with
8740
8895
  * different timestamps never merge, and copies with equal keys stay separate until a background
8741
8896
  * merge runs.
8742
8897
  */
8743
- function compileUsageCte(plan, parameters) {
8898
+ function compileUsageCte(plan, parameters, source) {
8899
+ const key = source.usageKey.join(", ");
8744
8900
  const names = USAGE_METRIC_NAMES.map((name) => parameters.add(name, "String"));
8745
8901
  const costRow = `name IN (${coreStorage.TRACE_AGGREGATE_COST_METRIC_NAMES.map((name) => names[usageMetricIndex(name)]).join(", ")})`;
8746
8902
  const from = parameters.add(plan.timeRange.from, "DateTime64(3, 'UTC')");
8747
8903
  const tenant = compileTenantScope(plan.scope, parameters);
8748
- const priced = `${costRow} AND isNotNull(estimatedCost) AND isNotNull(costUnit) AND NOT hasError`;
8904
+ const priced = `${costRow} AND isNotNull(estimatedCost) AND isNotNull(costUnit) AND (NOT hasError OR partialCost)`;
8749
8905
  const failed = `${costRow} AND (hasError OR (isNotNull(estimatedCost) AND isNull(costUnit)))`;
8750
8906
  return `usage AS (
8751
- SELECT traceId,
8907
+ SELECT ${key},
8752
8908
  toUInt8(1) AS hasUsage,
8753
8909
  ${names.map((name, index) => `sumIf(value, name = ${name}) AS t${index}`).join(",\n ")},
8754
8910
  sumIf(assumeNotNull(estimatedCost), ${priced}) AS cost,
@@ -8757,34 +8913,42 @@ function compileUsageCte(plan, parameters) {
8757
8913
  minIf(assumeNotNull(costUnit), ${priced}) AS unitMin,
8758
8914
  maxIf(assumeNotNull(costUnit), ${priced}) AS unitMax
8759
8915
  FROM (
8760
- SELECT traceId,
8916
+ SELECT ${key},
8761
8917
  latest.1 AS name,
8762
8918
  latest.2 AS value,
8763
8919
  latest.3 AS estimatedCost,
8764
8920
  latest.4 AS costUnit,
8765
- latest.5 AS hasError
8921
+ latest.5 AS hasError,
8922
+ latest.6 AS partialCost
8766
8923
  FROM (
8767
- SELECT traceId,
8768
- argMax(tuple(name, value, estimatedCost, costUnit, hasError), timestamp) AS latest
8924
+ SELECT ${key},
8925
+ argMax(tuple(name, value, estimatedCost, costUnit, hasError, partialCost), timestamp) AS latest
8769
8926
  FROM (
8770
- SELECT traceId, metricId, timestamp, name, value, estimatedCost, costUnit,
8771
- if(${costRow}, ifNull(JSONHas(costMetadata, 'error') AND JSONType(costMetadata, 'error') != 'Null', 0), 0) AS hasError
8927
+ SELECT ${key}, metricId, timestamp, name, value, estimatedCost, costUnit,
8928
+ if(${costRow}, ifNull(JSONHas(costMetadata, 'error') AND JSONType(costMetadata, 'error') != 'Null', 0), 0) AS hasError,
8929
+ if(${costRow}, ifNull(JSONExtractString(costMetadata, 'error') = 'partial_cost', 0), 0) AS partialCost
8772
8930
  FROM ${TABLE_METRIC_EVENTS}
8773
- WHERE traceId IN (SELECT traceId FROM candidates)
8931
+ WHERE ${source.usageBound}
8774
8932
  AND name IN (${names.join(", ")})
8775
8933
  AND timestamp >= ${from}${tenant}
8776
8934
  )
8777
- GROUP BY traceId, metricId
8935
+ GROUP BY ${key}, metricId
8778
8936
  )
8779
8937
  )
8780
- GROUP BY traceId
8938
+ GROUP BY ${key}
8781
8939
  )`;
8782
8940
  }
8783
8941
  function nullableNumber(value) {
8784
8942
  return value === null || value === void 0 ? null : Number(value);
8785
8943
  }
8786
- async function aggregateTraces(client, plan, timeoutMs) {
8787
- const result = await runWithClickHouseTraceQueryTimeout(client, { timeoutMs }, compileClickHouseTraceAggregate(plan));
8944
+ function aggregateTraces(client, plan, timeoutMs) {
8945
+ return runAggregate(client, plan, compileClickHouseTraceAggregate(plan), timeoutMs);
8946
+ }
8947
+ async function aggregateSpans(client, plan, timeoutMs) {
8948
+ return await runAggregate(client, plan, compileClickHouseSpanAggregate(plan), timeoutMs);
8949
+ }
8950
+ async function runAggregate(client, plan, compiled, timeoutMs) {
8951
+ const result = await runWithClickHouseTraceQueryTimeout(client, { timeoutMs }, compiled);
8788
8952
  let truncated;
8789
8953
  let resultRows;
8790
8954
  if (plan.interval === void 0) {
@@ -8808,8 +8972,7 @@ async function aggregateTraces(client, plan, timeoutMs) {
8808
8972
  truncated
8809
8973
  };
8810
8974
  }
8811
- //#endregion
8812
- //#region src/storage/domains/observability/v-next/trace-roots.ts
8975
+ // src/storage/domains/observability/v-next/trace-roots.ts
8813
8976
  /**
8814
8977
  * Get the root span for a trace, reading from trace_roots as compatibility path.
8815
8978
  * Uses ordinary LIMIT 1 (duplicates are byte-identical per design).
@@ -9086,8 +9249,7 @@ async function getStreamHeadCursor$1(client) {
9086
9249
  function buildTraceCursor(row) {
9087
9250
  return row.cursorId ?? "0";
9088
9251
  }
9089
- //#endregion
9090
- //#region src/storage/domains/observability/v-next/tracing.ts
9252
+ // src/storage/domains/observability/v-next/tracing.ts
9091
9253
  const BRANCH_SPAN_TYPE_SQL_LIST = BRANCH_SPAN_TYPES.map((t) => `'${t}'`).join(", ");
9092
9254
  /** Insert a single completed span. */
9093
9255
  async function createSpan(client, args) {
@@ -9677,9 +9839,9 @@ async function getStreamHeadCursor(client) {
9677
9839
  function buildBranchCursor(row) {
9678
9840
  return row.cursorId ?? "0";
9679
9841
  }
9680
- //#endregion
9681
- //#region src/storage/domains/observability/v-next/index.ts
9842
+ // src/storage/domains/observability/v-next/index.ts
9682
9843
  const spanQueryFeatures = typeof coreStorage.planSpanQuery === "function" ? ["span-query", "span-query-context-fields"] : [];
9844
+ const spanAggregateFeatures = typeof coreStorage.planSpanAggregate === "function" ? ["span-aggregate"] : [];
9683
9845
  const threadQueryFeatures = typeof coreStorage.planThreadRowsQuery === "function" ? ["thread-query-rows"] : [];
9684
9846
  function buildSignalMigrationRequiredMessage(args) {
9685
9847
  const tableList = args.tables.map((table) => ` - ${table.table} (${table.engine})`).join("\n");
@@ -10123,6 +10285,7 @@ var ObservabilityStorageClickhouseVNext = class extends ObservabilityStorage {
10123
10285
  "trace-query-root-entity-id",
10124
10286
  "trace-aggregate",
10125
10287
  ...spanQueryFeatures,
10288
+ ...spanAggregateFeatures,
10126
10289
  ...threadQueryFeatures
10127
10290
  ];
10128
10291
  return [
@@ -10147,6 +10310,7 @@ var ObservabilityStorageClickhouseVNext = class extends ObservabilityStorage {
10147
10310
  "trace-query-root-entity-id",
10148
10311
  "trace-aggregate",
10149
10312
  ...spanQueryFeatures,
10313
+ ...spanAggregateFeatures,
10150
10314
  ...threadQueryFeatures
10151
10315
  ];
10152
10316
  }
@@ -10298,6 +10462,18 @@ var ObservabilityStorageClickhouseVNext = class extends ObservabilityStorage {
10298
10462
  }, error);
10299
10463
  }
10300
10464
  }
10465
+ async aggregateSpans(plan) {
10466
+ try {
10467
+ return await aggregateSpans(this.#client, plan, this.#traceQueryTimeoutMs);
10468
+ } catch (error) {
10469
+ if (error instanceof MastraError || error instanceof coreStorage.TraceQueryExecutionError || error instanceof coreStorage.TraceQueryResourceLimitError) throw error;
10470
+ throw new MastraError({
10471
+ id: createStorageErrorId("CLICKHOUSE", "AGGREGATE_SPANS", "FAILED"),
10472
+ domain: ErrorDomain.STORAGE,
10473
+ category: ErrorCategory.THIRD_PARTY
10474
+ }, error);
10475
+ }
10476
+ }
10301
10477
  async getTraceQueryObservedFields(plan) {
10302
10478
  try {
10303
10479
  return await getTraceQueryObservedFields(this.#client, plan, this.#traceQueryDiscoveryLimits);
@@ -10801,8 +10977,7 @@ var ObservabilityStorageClickhouseVNext = class extends ObservabilityStorage {
10801
10977
  }
10802
10978
  }
10803
10979
  };
10804
- //#endregion
10805
- //#region src/storage/domains/scores/index.ts
10980
+ // src/storage/domains/scores/index.ts
10806
10981
  /**
10807
10982
  * Builds a multi-tenant scope SQL fragment and its named query params.
10808
10983
  * Returns a clause prefixed with ` AND ` (empty when no filters) plus params.
@@ -11223,8 +11398,7 @@ var ScoresStorageClickhouse = class extends ScoresStorage {
11223
11398
  }
11224
11399
  }
11225
11400
  };
11226
- //#endregion
11227
- //#region src/storage/domains/workflows/index.ts
11401
+ // src/storage/domains/workflows/index.ts
11228
11402
  var WorkflowsStorageClickhouse = class extends WorkflowsStorage {
11229
11403
  client;
11230
11404
  #db;
@@ -11491,8 +11665,7 @@ var WorkflowsStorageClickhouse = class extends WorkflowsStorage {
11491
11665
  }
11492
11666
  }
11493
11667
  };
11494
- //#endregion
11495
- //#region src/storage/index.ts
11668
+ // src/storage/index.ts
11496
11669
  /**
11497
11670
  * Type guard for pre-configured client config
11498
11671
  */
@@ -11655,7 +11828,6 @@ var ClickhouseStoreVNext = class extends ClickhouseStore {
11655
11828
  };
11656
11829
  }
11657
11830
  };
11658
- //#endregion
11659
11831
  export { COLUMN_TYPES, ClickhouseStore, ClickhouseStoreVNext, MemoryStorageClickhouse, ObservabilityStorageClickhouse, ObservabilityStorageClickhouseVNext, ScoresStorageClickhouse, TABLE_DELETION_REQUESTS, TABLE_ENGINES, WorkflowsStorageClickhouse, applyClickHouseRetention, markDeletionRequestApplied, recordDeletionRequest };
11660
11832
 
11661
11833
  //# sourceMappingURL=index.js.map