@mastra/clickhouse 1.26.0-alpha.5 → 1.27.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,7 +3,7 @@ name: mastra-clickhouse
3
3
  description: Documentation for @mastra/clickhouse. Use when working with @mastra/clickhouse APIs, configuration, or implementation.
4
4
  metadata:
5
5
  package: "@mastra/clickhouse"
6
- version: "1.26.0-alpha.5"
6
+ version: "1.27.0-alpha.0"
7
7
  ---
8
8
 
9
9
  ## When to use
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.26.0-alpha.5",
2
+ "version": "1.27.0-alpha.0",
3
3
  "package": "@mastra/clickhouse",
4
4
  "exports": {},
5
5
  "modules": {}
package/dist/index.cjs CHANGED
@@ -16,7 +16,7 @@ var __copyProps = (to, from, except, desc) => {
16
16
  }
17
17
  return to;
18
18
  };
19
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
19
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
20
20
  value: mod,
21
21
  enumerable: true
22
22
  }) : target, mod));
@@ -1610,6 +1610,7 @@ var MemoryStorageClickhouse = class extends _mastra_core_storage.MemoryStorage {
1610
1610
  metadataIndex++;
1611
1611
  }
1612
1612
  }
1613
+ const idScope = filter?.resourceId ? `WHERE id IN (SELECT id FROM ${_mastra_core_storage.TABLE_THREADS} WHERE resourceId = {resourceId:String})` : "";
1613
1614
  const latestThreads = `
1614
1615
  latest_threads AS (
1615
1616
  SELECT
@@ -1620,7 +1621,7 @@ var MemoryStorageClickhouse = class extends _mastra_core_storage.MemoryStorage {
1620
1621
  toDateTime64(createdAt, 3) as createdAt,
1621
1622
  toDateTime64(updatedAt, 3) as updatedAt
1622
1623
  FROM ${_mastra_core_storage.TABLE_THREADS}
1623
- ${filter?.resourceId ? `WHERE id IN (SELECT id FROM ${_mastra_core_storage.TABLE_THREADS} WHERE resourceId = {resourceId:String})` : ""}
1624
+ ${idScope}
1624
1625
  ORDER BY updatedAt DESC
1625
1626
  LIMIT 1 BY id
1626
1627
  )`;
@@ -2441,9 +2442,7 @@ var ObservabilityStorageClickhouse = class extends _mastra_core_storage.Observab
2441
2442
  case _mastra_core_storage.TraceStatus.RUNNING:
2442
2443
  conditions.push(`endedAt IS NULL AND (error IS NULL OR error = '')`);
2443
2444
  break;
2444
- case _mastra_core_storage.TraceStatus.SUCCESS:
2445
- conditions.push(`endedAt IS NOT NULL AND (error IS NULL OR error = '')`);
2446
- break;
2445
+ case _mastra_core_storage.TraceStatus.SUCCESS: conditions.push(`endedAt IS NOT NULL AND (error IS NULL OR error = '')`);
2447
2446
  }
2448
2447
  if (filters.hasChildError !== void 0) {
2449
2448
  const finalClause = (TABLE_ENGINES[_mastra_core_storage.TABLE_SPANS] ?? "MergeTree()").startsWith("ReplacingMergeTree") ? "FINAL" : "";
@@ -3672,6 +3671,9 @@ const ALL_MIGRATIONS = [
3672
3671
  addColumn(TABLE_SPAN_EVENTS, "inputPreview", "Nullable(String)"),
3673
3672
  addColumn(TABLE_SPAN_EVENTS, "outputPreview", "Nullable(String)"),
3674
3673
  addColumn(TABLE_SPAN_EVENTS, "errorPreview", "Nullable(String)"),
3674
+ addColumn(TABLE_TRACE_BRANCHES, "entityVersionId", "Nullable(String)"),
3675
+ addColumn(TABLE_TRACE_BRANCHES, "parentEntityVersionId", "Nullable(String)"),
3676
+ addColumn(TABLE_TRACE_BRANCHES, "rootEntityVersionId", "Nullable(String)"),
3675
3677
  addColumn(TABLE_SPAN_EVENTS, "entityVersionId", "Nullable(String)"),
3676
3678
  addColumn(TABLE_SPAN_EVENTS, "parentEntityVersionId", "Nullable(String)"),
3677
3679
  addColumn(TABLE_SPAN_EVENTS, "rootEntityVersionId", "Nullable(String)"),
@@ -5284,7 +5286,8 @@ async function getFeedbackAggregate(client, args) {
5284
5286
  const aggSql = getAggregationSql$2(args.aggregation);
5285
5287
  const identity = buildFeedbackIdentityFilter(args);
5286
5288
  const combined = mergeFilters$2(identity, buildFeedbackFilterConditions(args.filters));
5287
- const result = await queryJson$2(client, `SELECT ${aggSql} AS value FROM ${TABLE_FEEDBACK_EVENTS} FINAL ${toWhereClause$2(combined)}`, combined.params);
5289
+ const whereClause = toWhereClause$2(combined);
5290
+ const result = await queryJson$2(client, `SELECT ${aggSql} AS value FROM ${TABLE_FEEDBACK_EVENTS} FINAL ${whereClause}`, combined.params);
5288
5291
  const value = result[0]?.value == null ? null : Number(result[0]?.value);
5289
5292
  if (args.comparePeriod && args.filters?.timestamp) {
5290
5293
  const ts = args.filters.timestamp;
@@ -5318,7 +5321,8 @@ async function getFeedbackAggregate(client, args) {
5318
5321
  endExclusive: ts.endExclusive
5319
5322
  }
5320
5323
  }));
5321
- const prevResult = await queryJson$2(client, `SELECT ${aggSql} AS value FROM ${TABLE_FEEDBACK_EVENTS} FINAL ${toWhereClause$2(prevCombined)}`, prevCombined.params);
5324
+ const prevWhereClause = toWhereClause$2(prevCombined);
5325
+ const prevResult = await queryJson$2(client, `SELECT ${aggSql} AS value FROM ${TABLE_FEEDBACK_EVENTS} FINAL ${prevWhereClause}`, prevCombined.params);
5322
5326
  const previousValue = prevResult[0]?.value == null ? null : Number(prevResult[0]?.value);
5323
5327
  let changePercent = null;
5324
5328
  if (previousValue !== null && previousValue !== 0 && value !== null) changePercent = (value - previousValue) / Math.abs(previousValue) * 100;
@@ -6225,8 +6229,10 @@ function buildLegacySpanSelectExprs(legacyColumnTypes) {
6225
6229
  const promotedIn = PROMOTED_METADATA_KEYS.map((k) => `'${k}'`).join(", ");
6226
6230
  const metadataSearchExpr = has("metadata") ? `CAST(arrayFilter(x -> x.1 NOT IN (${promotedIn}) AND trim(BOTH ' ' FROM x.2) != '', arrayMap(x -> (x.1, trim(BOTH ' ' FROM x.2)), JSONExtractKeysAndValues(COALESCE("metadata", '{}'), 'String'))), 'Map(String, String)')` : `CAST(map(), 'Map(String, String)')`;
6227
6231
  let tagsExpr = "[]";
6228
- if (has("tags")) if (legacyColumnTypes.get("tags").includes("Array")) tagsExpr = `arrayFilter(x -> x != '', arrayDistinct(arrayMap(x -> trim(BOTH ' ' FROM x), "tags")))`;
6229
- else tagsExpr = `arrayFilter(x -> x != '', arrayDistinct(arrayMap(x -> trim(BOTH ' ' FROM x), JSONExtract(COALESCE("tags", '[]'), 'Array(String)'))))`;
6232
+ if (has("tags")) {
6233
+ if (legacyColumnTypes.get("tags").includes("Array")) tagsExpr = `arrayFilter(x -> x != '', arrayDistinct(arrayMap(x -> trim(BOTH ' ' FROM x), "tags")))`;
6234
+ else tagsExpr = `arrayFilter(x -> x != '', arrayDistinct(arrayMap(x -> trim(BOTH ' ' FROM x), JSONExtract(COALESCE("tags", '[]'), 'Array(String)'))))`;
6235
+ }
6230
6236
  const exprs = {
6231
6237
  dedupeKey: `concat("traceId", ':', "spanId")`,
6232
6238
  traceId: `"traceId"`,
@@ -6473,10 +6479,11 @@ function toWhereClause(filter) {
6473
6479
  return filter.conditions.length ? `WHERE ${filter.conditions.join(" AND ")}` : "";
6474
6480
  }
6475
6481
  function currentScoresRelation(sourcePredicate) {
6482
+ const whereClause = sourcePredicate ? `WHERE ${sourcePredicate}` : "";
6476
6483
  return `(
6477
6484
  SELECT *
6478
6485
  FROM ${TABLE_SCORE_EVENTS_CURRENT} FINAL
6479
- ${sourcePredicate ? `WHERE ${sourcePredicate}` : ""}
6486
+ ${whereClause}
6480
6487
  )`;
6481
6488
  }
6482
6489
  /**
@@ -8008,8 +8015,7 @@ async function queryThreads(client, plan, timeoutMs) {
8008
8015
  });
8009
8016
  }
8010
8017
  /** Compile a direct span-query filter using its dedicated field mapping. */
8011
- function compileSpanQueryPredicate(predicate) {
8012
- const parameters = new ParameterBuilder();
8018
+ function compileSpanQueryPredicate(predicate, parameters = new ParameterBuilder()) {
8013
8019
  return {
8014
8020
  sql: compileScalarPredicate(predicate, SPAN_QUERY_FIELDS, parameters),
8015
8021
  params: parameters.params
@@ -8170,6 +8176,131 @@ function compileClickHouseSpanQuery(plan) {
8170
8176
  }
8171
8177
  };
8172
8178
  }
8179
+ const branchSpanTypes = new Set(BRANCH_SPAN_TYPE_VALUES);
8180
+ const sameColumn = (field) => [field];
8181
+ /**
8182
+ * Span fields `trace_branches` can serve, as the `trace_branches` columns each one reads. The two
8183
+ * tables are expected to diverge, so this list is explicit: a selection whose fields are not all
8184
+ * listed reads `span_events`. A new span-query field has to be classified here deliberately, and a
8185
+ * missing column then costs speed, not correctness.
8186
+ */
8187
+ const TRACE_BRANCHES_SPAN_FIELD_COLUMNS = {
8188
+ ...Object.fromEntries([
8189
+ "organizationId",
8190
+ "resourceId",
8191
+ "traceId",
8192
+ "spanId",
8193
+ "parentSpanId",
8194
+ "name",
8195
+ "spanType",
8196
+ "startedAt",
8197
+ "endedAt",
8198
+ "timeToFirstChunkMs",
8199
+ "threadId",
8200
+ "runId",
8201
+ "sessionId",
8202
+ "userId",
8203
+ "requestId",
8204
+ "serviceName",
8205
+ "experimentId",
8206
+ "environment",
8207
+ "executionSource",
8208
+ "entityType",
8209
+ "entityId",
8210
+ "entityName",
8211
+ "entityVersionId",
8212
+ "parentEntityType",
8213
+ "parentEntityId",
8214
+ "parentEntityName",
8215
+ "parentEntityVersionId",
8216
+ "rootEntityType",
8217
+ "rootEntityId",
8218
+ "rootEntityName",
8219
+ "rootEntityVersionId"
8220
+ ].map((field) => [field, sameColumn(field)])),
8221
+ status: ["error"],
8222
+ error: ["error"],
8223
+ durationMs: ["startedAt", "endedAt"]
8224
+ };
8225
+ /**
8226
+ * Whether a span selection reads `trace_branches`: `where` must restrict `spanType` to branch
8227
+ * types, and `trace_branches` must serve every selected field (`served`, for tests).
8228
+ */
8229
+ function readsTraceBranches(where, fields, served = TRACE_BRANCHES_SPAN_FIELD_COLUMNS) {
8230
+ 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));
8231
+ }
8232
+ /**
8233
+ * Top-level `spanType` / `name` equality or `in` conditions. Every span the predicate can match
8234
+ * satisfies them, and copies of a span never differ in either field, so they can be applied
8235
+ * before collapsing copies.
8236
+ */
8237
+ function positiveSpanConjuncts(where) {
8238
+ return (where?.type === "boolean" && where.operator === "and" ? where.args : where ? [where] : []).flatMap((conjunct) => {
8239
+ if (conjunct.type !== "comparison" && conjunct.type !== "membership") return [];
8240
+ if (conjunct.field !== "spanType" && conjunct.field !== "name") return [];
8241
+ if (conjunct.type === "comparison" && conjunct.operator === "eq") return [{
8242
+ field: conjunct.field,
8243
+ values: [conjunct.value]
8244
+ }];
8245
+ if (conjunct.type === "membership" && conjunct.operator === "in") return [{
8246
+ field: conjunct.field,
8247
+ values: conjunct.values
8248
+ }];
8249
+ return [];
8250
+ });
8251
+ }
8252
+ /**
8253
+ * The spans `querySpans()` selects, as CTEs ending in `selected` (alias each row as you like):
8254
+ * one row per current completed span with `startedAt` in the window, matching `where`, carrying
8255
+ * the identity, `startedAt`, `endedAt`, `status`, and `fields`.
8256
+ *
8257
+ * Copies of a span collapse to the copy with the latest `endedAt`. When `where` requires branch
8258
+ * span types (agent and workflow runs, tool calls, …) and `trace_branches` serves every selected
8259
+ * field, the spans are read from `trace_branches`, whose sort key starts with `spanType`, instead of
8260
+ * every span in the window. That path can differ from `querySpans()` in two ways:
8261
+ * - `trace_branches` is filled only by its materialized view, with no backfill, so an install that
8262
+ * upgraded from @mastra/clickhouse 1.4–1.6 has no branch rows for spans written before then.
8263
+ * - It is a `ReplacingMergeTree` without a version column, so once a background merge collapses a
8264
+ * retried span's copies it keeps the last-inserted copy, not the one with the latest `endedAt`.
8265
+ * Counts are unaffected; `status` and duration of an out-of-order retry can differ.
8266
+ *
8267
+ * Like `querySpans()`, the read has no upper bound on `endedAt` (`PREWHERE endedAt >= from` only,
8268
+ * since a span that starts in the window can end any time later), so a historical window reads
8269
+ * every span that ended since `from`.
8270
+ */
8271
+ function compileClickHouseSpanSelection(plan, fields, parameters) {
8272
+ const from = parameters.add(plan.timeRange.from, "DateTime64(3, 'UTC')");
8273
+ const to = parameters.add(plan.timeRange.to, "DateTime64(3, 'UTC')");
8274
+ 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("");
8275
+ const predicate = plan.where ? compileSpanQueryPredicate(plan.where, parameters).sql : "1";
8276
+ let tenant = "";
8277
+ if (plan.scope) {
8278
+ tenant += `\n AND e.organizationId = ${parameters.add(plan.scope.organizationId, "String")}`;
8279
+ if (plan.scope.resourceId !== void 0) tenant += `\n AND e.resourceId = ${parameters.add(plan.scope.resourceId, "String")}`;
8280
+ }
8281
+ const selectionFields = [.../* @__PURE__ */ new Set([
8282
+ "startedAt",
8283
+ "endedAt",
8284
+ "status",
8285
+ ...fields,
8286
+ ...predicateFields$1(plan.where)
8287
+ ])].filter((field) => !new Set(identityFields).has(field));
8288
+ const branches = readsTraceBranches(plan.where, selectionFields);
8289
+ const scan = `FROM ${branches ? TABLE_TRACE_BRANCHES : TABLE_SPAN_EVENTS} e
8290
+ PREWHERE e.endedAt >= ${from}
8291
+ WHERE e.startedAt >= ${from} AND e.startedAt < ${to}${tenant}${prefilter}`;
8292
+ return [`span_candidates AS (
8293
+ ${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(", ")}
8294
+ ${scan}
8295
+ GROUP BY ${identityFields.map((field) => `e.${field}`).join(", ")}` : `SELECT ${identities}, ${selectionFields.map((field) => `${fieldExpression(field)} AS ${field}`).join(", ")}
8296
+ ${scan}
8297
+ ORDER BY ${identities}, e.endedAt DESC
8298
+ LIMIT 1 BY ${identities}`}
8299
+ )`, `selected AS (
8300
+ SELECT * FROM span_candidates s
8301
+ WHERE s.startedAt >= ${from} AND s.startedAt < ${to} AND (${predicate})
8302
+ )`];
8303
+ }
8173
8304
  const LEGACY_PAYLOAD_BATCH_ROWS = 32;
8174
8305
  function iso$1(value) {
8175
8306
  return new Date(value.includes("T") ? value : `${value.replace(" ", "T")}Z`).toISOString();
@@ -8181,7 +8312,7 @@ async function querySpans(client, plan, timeoutMs) {
8181
8312
  if (remaining <= 0) throw new _mastra_core_storage.TraceQueryExecutionError();
8182
8313
  return await runWithClickHouseTraceQueryTimeout(client, {
8183
8314
  timeoutMs: remaining,
8184
- memoryLimitBytes: 512 * 1024 * 1024
8315
+ memoryLimitBytes: 536870912
8185
8316
  }, compiled);
8186
8317
  };
8187
8318
  const selected = (await run(compileClickHouseSpanQuery(plan))).map((row) => ({
@@ -8298,14 +8429,14 @@ function afterCursor(plan, parameters) {
8298
8429
  return `(${terms.join(" OR ")})`;
8299
8430
  }
8300
8431
  const THREAD_QUERY_EXECUTION_LIMITS = {
8301
- memoryLimitBytes: 512 * 1024 * 1024,
8432
+ memoryLimitBytes: 536870912,
8302
8433
  maxThreads: 2,
8303
8434
  maxBlockSize: 4096,
8304
- spillThresholdBytes: 32 * 1024 * 1024
8435
+ spillThresholdBytes: 33554432
8305
8436
  };
8306
8437
  const MAX_CANDIDATE_THREADS = 16384;
8307
8438
  const MAX_CANDIDATE_TRACES = 1e5;
8308
- const MAX_CANDIDATE_ID_BYTES = 4 * 1024 * 1024;
8439
+ const MAX_CANDIDATE_ID_BYTES = 4194304;
8309
8440
  const MAX_FULL_HISTORY_ROOTS = 25e4;
8310
8441
  function canUseThreadCandidates(probe) {
8311
8442
  return Number(probe.threadCount) <= MAX_CANDIDATE_THREADS && Number(probe.traceCount) <= MAX_CANDIDATE_TRACES && Number(probe.threadBytes) + Number(probe.traceBytes) <= MAX_CANDIDATE_ID_BYTES;
@@ -8415,7 +8546,7 @@ function* batches(rows, maxRows = 256) {
8415
8546
  let bytes = 0;
8416
8547
  for (const row of rows) {
8417
8548
  const size = Buffer.byteLength(JSON.stringify(row), "utf8") * 2;
8418
- if (batch.length && (batch.length >= maxRows || bytes + size > 48 * 1024)) {
8549
+ if (batch.length && (batch.length >= maxRows || bytes + size > 49152)) {
8419
8550
  yield batch;
8420
8551
  batch = [];
8421
8552
  bytes = 0;
@@ -8573,6 +8704,28 @@ const DIMENSION_SQL = {
8573
8704
  organizationId: "r.organizationId",
8574
8705
  experimentId: "r.experimentId"
8575
8706
  };
8707
+ /** Span column expressions (alias `r`, a `selected` span row) for each groupable span dimension. */
8708
+ const SPAN_DIMENSION_SQL = Object.fromEntries([
8709
+ "name",
8710
+ "spanType",
8711
+ "entityType",
8712
+ "entityId",
8713
+ "entityName",
8714
+ "parentEntityType",
8715
+ "parentEntityName",
8716
+ "rootEntityType",
8717
+ "rootEntityName",
8718
+ "environment",
8719
+ "status",
8720
+ "serviceName",
8721
+ "executionSource",
8722
+ "threadId",
8723
+ "resourceId",
8724
+ "userId",
8725
+ "sessionId",
8726
+ "organizationId",
8727
+ "experimentId"
8728
+ ].map((field) => [field, `r.${field}`]));
8576
8729
  const PERCENTILES = {
8577
8730
  "duration.p50": .5,
8578
8731
  "duration.p90": .9,
@@ -8599,13 +8752,41 @@ const COMPARISON_SQL = {
8599
8752
  */
8600
8753
  function compileClickHouseTraceAggregate(plan) {
8601
8754
  const parameters = new ParameterBuilder();
8602
- const ctes = compileClickHouseTraceCandidates(plan, "*", parameters, { oneRootPerTrace: true });
8603
- const dimensionSql = (field) => {
8604
- if (field.startsWith("metadata.")) return traceMetadataValueSql(parameters.add(field.slice(9), "String"));
8605
- const sql = Object.hasOwn(DIMENSION_SQL, field) ? DIMENSION_SQL[field] : void 0;
8606
- if (sql === void 0) throw new Error(`Unsupported trusted trace-aggregate field: ${field}`);
8607
- return sql;
8608
- };
8755
+ return compileClickHouseAggregate(plan, parameters, {
8756
+ ctes: compileClickHouseTraceCandidates(plan, "*", parameters, { oneRootPerTrace: true }),
8757
+ from: "candidates",
8758
+ dimensionSql: (field) => {
8759
+ if (field.startsWith("metadata.")) return traceMetadataValueSql(parameters.add(field.slice(9), "String"));
8760
+ return lookupDimensionSql(DIMENSION_SQL, field);
8761
+ },
8762
+ isErrorSql: "isNotNull(r.error)",
8763
+ usageKey: ["traceId"],
8764
+ usageBound: "traceId IN (SELECT traceId FROM candidates)"
8765
+ });
8766
+ }
8767
+ /**
8768
+ * Compiles a trusted span aggregate plan over the spans `querySpans()` selects. Usage is
8769
+ * attributed by `(traceId, spanId)`, so only the span that recorded a metric row carries it.
8770
+ */
8771
+ function compileClickHouseSpanAggregate(plan) {
8772
+ const parameters = new ParameterBuilder();
8773
+ return compileClickHouseAggregate(plan, parameters, {
8774
+ ctes: compileClickHouseSpanSelection(plan, [...plan.dimensions, ...plan.measures.flatMap((measure) => measure.type === "countDistinct" && measure.field !== "traceId" ? [measure.field] : [])], parameters),
8775
+ from: "selected",
8776
+ dimensionSql: (field) => lookupDimensionSql(SPAN_DIMENSION_SQL, field),
8777
+ isErrorSql: "r.status = 'error'",
8778
+ usageKey: ["traceId", "spanId"],
8779
+ usageBound: "isNotNull(spanId)"
8780
+ });
8781
+ }
8782
+ function lookupDimensionSql(columns, field) {
8783
+ const sql = Object.hasOwn(columns, field) ? columns[field] : void 0;
8784
+ if (sql === void 0) throw new Error(`Unsupported trusted aggregate field: ${field}`);
8785
+ return sql;
8786
+ }
8787
+ function compileClickHouseAggregate(plan, parameters, source) {
8788
+ const { ctes } = source;
8789
+ const dimensionSql = source.dimensionSql;
8609
8790
  const distinctSql = (field) => field === "traceId" ? "r.traceId" : dimensionSql(field);
8610
8791
  const dimensionColumns = plan.dimensions.map((_, index) => `d${index}`);
8611
8792
  const distinctColumns = /* @__PURE__ */ new Map();
@@ -8616,7 +8797,7 @@ function compileClickHouseTraceAggregate(plan) {
8616
8797
  distinctColumns.set(measure.field, column);
8617
8798
  factColumns.push(`CAST(${distinctSql(measure.field)} AS Nullable(String)) AS ${column}`);
8618
8799
  }
8619
- factColumns.push(`toFloat64(${durationMsSql("r.startedAt", "r.endedAt")}) AS durationMs`, `isNotNull(r.error) AS isError`, `cityHash64(r.traceId) AS traceSeed`);
8800
+ factColumns.push(`toFloat64(${durationMsSql("r.startedAt", "r.endedAt")}) AS durationMs`, `${source.isErrorSql} AS isError`, `cityHash64(${source.usageKey.map((column) => `r.${column}`).join(", ")}) AS traceSeed`);
8620
8801
  if (plan.interval !== void 0) {
8621
8802
  const intervalMs = _mastra_core_storage.TRACE_AGGREGATE_INTERVAL_MS[plan.interval];
8622
8803
  if (intervalMs === void 0) throw new Error(`Unsupported trusted trace-aggregate interval: ${plan.interval}`);
@@ -8626,14 +8807,14 @@ function compileClickHouseTraceAggregate(plan) {
8626
8807
  const hasCost = measureKinds.has("cost");
8627
8808
  let usageJoin = "";
8628
8809
  if (hasCost || measureKinds.has("tokens")) {
8629
- ctes.push(compileUsageCte(plan, parameters));
8630
- usageJoin = "\n LEFT JOIN usage u ON u.traceId = r.traceId";
8810
+ ctes.push(compileUsageCte(plan, parameters, source));
8811
+ usageJoin = `\n LEFT JOIN usage u ON ${source.usageKey.map((column) => `u.${column} = r.${column}`).join(" AND ")}`;
8631
8812
  USAGE_METRIC_NAMES.forEach((_, index) => factColumns.push(`if(u.hasUsage = 1, u.t${index}, NULL) AS t${index}`));
8632
8813
  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`);
8633
8814
  }
8634
8815
  ctes.push(`facts AS (
8635
8816
  SELECT ${factColumns.join(",\n ")}
8636
- FROM candidates r${usageJoin}
8817
+ FROM ${source.from} r${usageJoin}
8637
8818
  )`);
8638
8819
  const mixedUnitsSql = "minOrNull(unitMin) != maxOrNull(unitMax)";
8639
8820
  const measureSql = (name) => {
@@ -8748,26 +8929,28 @@ function usageMetricIndex(name) {
8748
8929
  return index;
8749
8930
  }
8750
8931
  /**
8751
- * Builds `usage`: one row per candidate trace that has at least one token metric row, holding
8752
- * per-name token sums (`t<i>`, indexed by `TRACE_AGGREGATE_USAGE_METRIC_NAMES`) and the trace's
8753
- * pricing state. Metric rows are matched by traceId, usage metric name, tenant scope, and
8754
- * `timestamp >= from` only: a trace that starts in the window can emit metrics after `to`, and
8755
- * the candidates semi-join already bounds the scan.
8932
+ * Builds `usage`: one row per usage key (a trace, or a span) that has at least one token metric
8933
+ * row, holding per-name token sums (`t<i>`, indexed by `TRACE_AGGREGATE_USAGE_METRIC_NAMES`) and
8934
+ * its pricing state. Metric rows are matched by the source's usage bound, usage metric name,
8935
+ * tenant scope, and `timestamp >= from` only: a trace or span that starts in the window can emit
8936
+ * metrics after `to`. For spans that makes the read cost grow with the age of `from` (see
8937
+ * `usageBound`).
8756
8938
  *
8757
8939
  * Rows are deduplicated with `LIMIT 1 BY metricId` at query time. `ReplacingMergeTree` only
8758
8940
  * collapses rows with equal sort keys `(name, timestamp, metricId)`, so retried copies with
8759
8941
  * different timestamps never merge, and copies with equal keys stay separate until a background
8760
8942
  * merge runs.
8761
8943
  */
8762
- function compileUsageCte(plan, parameters) {
8944
+ function compileUsageCte(plan, parameters, source) {
8945
+ const key = source.usageKey.join(", ");
8763
8946
  const names = USAGE_METRIC_NAMES.map((name) => parameters.add(name, "String"));
8764
8947
  const costRow = `name IN (${_mastra_core_storage.TRACE_AGGREGATE_COST_METRIC_NAMES.map((name) => names[usageMetricIndex(name)]).join(", ")})`;
8765
8948
  const from = parameters.add(plan.timeRange.from, "DateTime64(3, 'UTC')");
8766
8949
  const tenant = compileTenantScope(plan.scope, parameters);
8767
- const priced = `${costRow} AND isNotNull(estimatedCost) AND isNotNull(costUnit) AND NOT hasError`;
8950
+ const priced = `${costRow} AND isNotNull(estimatedCost) AND isNotNull(costUnit) AND (NOT hasError OR partialCost)`;
8768
8951
  const failed = `${costRow} AND (hasError OR (isNotNull(estimatedCost) AND isNull(costUnit)))`;
8769
8952
  return `usage AS (
8770
- SELECT traceId,
8953
+ SELECT ${key},
8771
8954
  toUInt8(1) AS hasUsage,
8772
8955
  ${names.map((name, index) => `sumIf(value, name = ${name}) AS t${index}`).join(",\n ")},
8773
8956
  sumIf(assumeNotNull(estimatedCost), ${priced}) AS cost,
@@ -8776,34 +8959,42 @@ function compileUsageCte(plan, parameters) {
8776
8959
  minIf(assumeNotNull(costUnit), ${priced}) AS unitMin,
8777
8960
  maxIf(assumeNotNull(costUnit), ${priced}) AS unitMax
8778
8961
  FROM (
8779
- SELECT traceId,
8962
+ SELECT ${key},
8780
8963
  latest.1 AS name,
8781
8964
  latest.2 AS value,
8782
8965
  latest.3 AS estimatedCost,
8783
8966
  latest.4 AS costUnit,
8784
- latest.5 AS hasError
8967
+ latest.5 AS hasError,
8968
+ latest.6 AS partialCost
8785
8969
  FROM (
8786
- SELECT traceId,
8787
- argMax(tuple(name, value, estimatedCost, costUnit, hasError), timestamp) AS latest
8970
+ SELECT ${key},
8971
+ argMax(tuple(name, value, estimatedCost, costUnit, hasError, partialCost), timestamp) AS latest
8788
8972
  FROM (
8789
- SELECT traceId, metricId, timestamp, name, value, estimatedCost, costUnit,
8790
- if(${costRow}, ifNull(JSONHas(costMetadata, 'error') AND JSONType(costMetadata, 'error') != 'Null', 0), 0) AS hasError
8973
+ SELECT ${key}, metricId, timestamp, name, value, estimatedCost, costUnit,
8974
+ if(${costRow}, ifNull(JSONHas(costMetadata, 'error') AND JSONType(costMetadata, 'error') != 'Null', 0), 0) AS hasError,
8975
+ if(${costRow}, ifNull(JSONExtractString(costMetadata, 'error') = 'partial_cost', 0), 0) AS partialCost
8791
8976
  FROM ${TABLE_METRIC_EVENTS}
8792
- WHERE traceId IN (SELECT traceId FROM candidates)
8977
+ WHERE ${source.usageBound}
8793
8978
  AND name IN (${names.join(", ")})
8794
8979
  AND timestamp >= ${from}${tenant}
8795
8980
  )
8796
- GROUP BY traceId, metricId
8981
+ GROUP BY ${key}, metricId
8797
8982
  )
8798
8983
  )
8799
- GROUP BY traceId
8984
+ GROUP BY ${key}
8800
8985
  )`;
8801
8986
  }
8802
8987
  function nullableNumber(value) {
8803
8988
  return value === null || value === void 0 ? null : Number(value);
8804
8989
  }
8805
- async function aggregateTraces(client, plan, timeoutMs) {
8806
- const result = await runWithClickHouseTraceQueryTimeout(client, { timeoutMs }, compileClickHouseTraceAggregate(plan));
8990
+ function aggregateTraces(client, plan, timeoutMs) {
8991
+ return runAggregate(client, plan, compileClickHouseTraceAggregate(plan), timeoutMs);
8992
+ }
8993
+ async function aggregateSpans(client, plan, timeoutMs) {
8994
+ return await runAggregate(client, plan, compileClickHouseSpanAggregate(plan), timeoutMs);
8995
+ }
8996
+ async function runAggregate(client, plan, compiled, timeoutMs) {
8997
+ const result = await runWithClickHouseTraceQueryTimeout(client, { timeoutMs }, compiled);
8807
8998
  let truncated;
8808
8999
  let resultRows;
8809
9000
  if (plan.interval === void 0) {
@@ -9699,6 +9890,7 @@ function buildBranchCursor(row) {
9699
9890
  //#endregion
9700
9891
  //#region src/storage/domains/observability/v-next/index.ts
9701
9892
  const spanQueryFeatures = typeof _mastra_core_storage.planSpanQuery === "function" ? ["span-query", "span-query-context-fields"] : [];
9893
+ const spanAggregateFeatures = typeof _mastra_core_storage.planSpanAggregate === "function" ? ["span-aggregate"] : [];
9702
9894
  const threadQueryFeatures = typeof _mastra_core_storage.planThreadRowsQuery === "function" ? ["thread-query-rows"] : [];
9703
9895
  function buildSignalMigrationRequiredMessage(args) {
9704
9896
  const tableList = args.tables.map((table) => ` - ${table.table} (${table.engine})`).join("\n");
@@ -9985,7 +10177,7 @@ async function detectExistingDeltaCursorStrategy(client) {
9985
10177
  }
9986
10178
  }
9987
10179
  const TRACE_QUERY_DISCOVERY_DEFAULT_TIMEOUT_MS = 5e3;
9988
- const TRACE_QUERY_DISCOVERY_DEFAULT_MEMORY_LIMIT_BYTES = 256 * 1024 * 1024;
10180
+ const TRACE_QUERY_DISCOVERY_DEFAULT_MEMORY_LIMIT_BYTES = 268435456;
9989
10181
  function resolveTraceQueryDiscoveryMemoryLimitBytes(memoryLimitBytes = TRACE_QUERY_DISCOVERY_DEFAULT_MEMORY_LIMIT_BYTES) {
9990
10182
  if (!Number.isSafeInteger(memoryLimitBytes) || memoryLimitBytes <= 0) throw new RangeError("traceQuery.discovery.memoryLimitBytes must be a positive safe integer");
9991
10183
  return memoryLimitBytes;
@@ -10142,6 +10334,7 @@ var ObservabilityStorageClickhouseVNext = class extends _mastra_core_storage.Obs
10142
10334
  "trace-query-root-entity-id",
10143
10335
  "trace-aggregate",
10144
10336
  ...spanQueryFeatures,
10337
+ ...spanAggregateFeatures,
10145
10338
  ...threadQueryFeatures
10146
10339
  ];
10147
10340
  return [
@@ -10166,6 +10359,7 @@ var ObservabilityStorageClickhouseVNext = class extends _mastra_core_storage.Obs
10166
10359
  "trace-query-root-entity-id",
10167
10360
  "trace-aggregate",
10168
10361
  ...spanQueryFeatures,
10362
+ ...spanAggregateFeatures,
10169
10363
  ...threadQueryFeatures
10170
10364
  ];
10171
10365
  }
@@ -10317,6 +10511,18 @@ var ObservabilityStorageClickhouseVNext = class extends _mastra_core_storage.Obs
10317
10511
  }, error);
10318
10512
  }
10319
10513
  }
10514
+ async aggregateSpans(plan) {
10515
+ try {
10516
+ return await aggregateSpans(this.#client, plan, this.#traceQueryTimeoutMs);
10517
+ } catch (error) {
10518
+ if (error instanceof _mastra_core_error.MastraError || error instanceof _mastra_core_storage.TraceQueryExecutionError || error instanceof _mastra_core_storage.TraceQueryResourceLimitError) throw error;
10519
+ throw new _mastra_core_error.MastraError({
10520
+ id: (0, _mastra_core_storage.createStorageErrorId)("CLICKHOUSE", "AGGREGATE_SPANS", "FAILED"),
10521
+ domain: _mastra_core_error.ErrorDomain.STORAGE,
10522
+ category: _mastra_core_error.ErrorCategory.THIRD_PARTY
10523
+ }, error);
10524
+ }
10525
+ }
10320
10526
  async getTraceQueryObservedFields(plan) {
10321
10527
  try {
10322
10528
  return await getTraceQueryObservedFields(this.#client, plan, this.#traceQueryDiscoveryLimits);
@@ -11378,10 +11584,12 @@ var WorkflowsStorageClickhouse = class extends _mastra_core_storage.WorkflowsSto
11378
11584
  conditions.push(`JSONExtractString(snapshot, 'status') = {var_status:String}`);
11379
11585
  values.var_status = status;
11380
11586
  }
11381
- if (resourceId) if (await this.#db.hasColumn(_mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT, "resourceId")) {
11382
- conditions.push(`resourceId = {var_resourceId:String}`);
11383
- values.var_resourceId = resourceId;
11384
- } else this.logger.warn(`[${_mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT}] resourceId column not found. Skipping resourceId filter.`);
11587
+ if (resourceId) {
11588
+ if (await this.#db.hasColumn(_mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT, "resourceId")) {
11589
+ conditions.push(`resourceId = {var_resourceId:String}`);
11590
+ values.var_resourceId = resourceId;
11591
+ } else this.logger.warn(`[${_mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT}] resourceId column not found. Skipping resourceId filter.`);
11592
+ }
11385
11593
  if (fromDate) {
11386
11594
  conditions.push(`createdAt >= {var_from_date:DateTime64(3)}`);
11387
11595
  values.var_from_date = fromDate.getTime() / 1e3;