@mastra/clickhouse 1.22.1 → 1.23.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.22.1"
6
+ version: "1.23.0-alpha.0"
7
7
  ---
8
8
 
9
9
  ## When to use
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.22.1",
2
+ "version": "1.23.0-alpha.0",
3
3
  "package": "@mastra/clickhouse",
4
4
  "exports": {},
5
5
  "modules": {}
package/dist/index.cjs CHANGED
@@ -6580,6 +6580,10 @@ const TRACE_STATUS_SQL = `if(isNotNull(r.error), 'error', 'success')`;
6580
6580
  function durationMsSql(startedAt, endedAt) {
6581
6581
  return `dateDiff('millisecond', ${startedAt}, ${endedAt})`;
6582
6582
  }
6583
+ /** Top-level metadata string value on a trace root (alias `r`); `key` is a bound parameter placeholder. */
6584
+ function traceMetadataValueSql(key) {
6585
+ return `coalesce(if(mapContains(r.metadataSearch, ${key}), r.metadataSearch[${key}], NULL), nullIf(trim(JSONExtractString(r.metadataRaw, ${key})), ''))`;
6586
+ }
6583
6587
  const TRACE_FIELDS = {
6584
6588
  traceId: {
6585
6589
  sql: "r.traceId",
@@ -6838,9 +6842,8 @@ function compileScalarPredicate(predicate, registry, parameters, allowMetadata =
6838
6842
  if (predicate.type === "not") return `NOT (${compileScalarPredicate(predicate.arg, registry, parameters, allowMetadata)})`;
6839
6843
  const field = isMetadataField(predicate.field) ? (() => {
6840
6844
  if (!allowMetadata) throw new Error(`Unsupported trusted trace-query field: ${predicate.field}`);
6841
- const key = parameters.add(predicate.field.slice(9), "String");
6842
6845
  return {
6843
- sql: `coalesce(if(mapContains(r.metadataSearch, ${key}), r.metadataSearch[${key}], NULL), nullIf(trim(JSONExtractString(r.metadataRaw, ${key})), ''))`,
6846
+ sql: traceMetadataValueSql(parameters.add(predicate.field.slice(9), "String")),
6844
6847
  parameterType: "String"
6845
6848
  };
6846
6849
  })() : fieldDefinition(registry, predicate.field);
@@ -6933,7 +6936,7 @@ function compileTenantScope(scope, parameters) {
6933
6936
  if (scope.resourceId !== void 0) sql += `\n AND resourceId = ${parameters.add(scope.resourceId, "String")}`;
6934
6937
  return sql;
6935
6938
  }
6936
- function compileClickHouseTraceScope(selection, relationCollections, parameters, scope) {
6939
+ function compileClickHouseTraceScope(selection, relationCollections, parameters, scope, seedConjuncts = []) {
6937
6940
  const from = parameters.add(selection.timeRange.from, "DateTime64(3, 'UTC')");
6938
6941
  const to = parameters.add(selection.timeRange.to, "DateTime64(3, 'UTC')");
6939
6942
  const tenant = compileTenantScope(scope, parameters);
@@ -6943,9 +6946,9 @@ function compileClickHouseTraceScope(selection, relationCollections, parameters,
6943
6946
  FROM ${TABLE_TRACE_ROOTS}
6944
6947
  WHERE traceId IN (
6945
6948
  SELECT traceId
6946
- FROM ${TABLE_TRACE_ROOTS}
6949
+ FROM ${TABLE_TRACE_ROOTS} r
6947
6950
  WHERE startedAt >= ${from}
6948
- AND startedAt < ${to}${tenant}
6951
+ AND startedAt < ${to}${tenant}${seedConjuncts.map((conjunct) => `\n AND (${compilePredicate(conjunct, parameters)})`).join("")}
6949
6952
  )
6950
6953
  ORDER BY dedupeKey
6951
6954
  LIMIT 1 BY dedupeKey
@@ -7038,16 +7041,25 @@ function parseDeltaWatermark(value) {
7038
7041
  throw new _mastra_core_storage.TraceQueryCursorError("TRACE_QUERY_CURSOR_MALFORMED");
7039
7042
  }
7040
7043
  }
7041
- function compileClickHouseTraceQuery(plan, deltaHead) {
7042
- const parameters = new ParameterBuilder();
7043
- const ctes = compileClickHouseTraceScope(plan, collectRelationCollections(plan.where), parameters, plan.scope);
7044
- const predicate = plan.where ? compilePredicate(plan.where, parameters) : "1";
7044
+ /**
7045
+ * Builds the CTE chain ending in `candidates`: the completed, current trace roots in the
7046
+ * selection's time range and tenant scope that match its `where` predicate. Trace queries and
7047
+ * trace aggregates both select from this CTE so they always see the same population.
7048
+ */
7049
+ function compileClickHouseTraceCandidates(selection, columns, parameters) {
7050
+ const rootConjuncts = selection.where ? (selection.where.type === "boolean" && selection.where.operator === "and" ? selection.where.args : [selection.where]).filter((conjunct) => collectRelationCollections(conjunct).size === 0) : [];
7051
+ const ctes = compileClickHouseTraceScope(selection, collectRelationCollections(selection.where), parameters, selection.scope, rootConjuncts);
7052
+ const predicate = selection.where ? compilePredicate(selection.where, parameters) : "1";
7045
7053
  ctes.push(`candidates AS (
7046
- SELECT ${TRACE_SELECT}
7054
+ SELECT ${columns}
7047
7055
  FROM root_scope r
7048
7056
  WHERE ${predicate}
7049
7057
  )`);
7050
- const candidates = `WITH ${ctes.join(",\n")}`;
7058
+ return ctes;
7059
+ }
7060
+ function compileClickHouseTraceQuery(plan, deltaHead) {
7061
+ const parameters = new ParameterBuilder();
7062
+ const candidates = `WITH ${compileClickHouseTraceCandidates(plan, TRACE_SELECT, parameters).join(",\n")}`;
7051
7063
  if (plan.result === "groups") return {
7052
7064
  query: `${candidates}
7053
7065
  SELECT threadId
@@ -7223,10 +7235,8 @@ function compileClickHouseTraceQueryValues(plan) {
7223
7235
  const parameters = new ParameterBuilder();
7224
7236
  const ctes = compileClickHouseTraceScope(plan, discoveryCollections(plan.predicateScope), parameters, plan.scope);
7225
7237
  let field;
7226
- if (plan.predicateScope === "trace" && plan.path.startsWith("metadata.")) {
7227
- const key = parameters.add(plan.path.slice(9), "String");
7228
- field = `coalesce(if(mapContains(r.metadataSearch, ${key}), r.metadataSearch[${key}], NULL), nullIf(trim(JSONExtractString(r.metadataRaw, ${key})), ''))`;
7229
- } else if (plan.predicateScope === "trace" && plan.path === "tags") field = `arrayJoin(arrayDistinct(${TRACE_FIELDS.tags.sql}))`;
7238
+ if (plan.predicateScope === "trace" && plan.path.startsWith("metadata.")) field = traceMetadataValueSql(parameters.add(plan.path.slice(9), "String"));
7239
+ else if (plan.predicateScope === "trace" && plan.path === "tags") field = `arrayJoin(arrayDistinct(${TRACE_FIELDS.tags.sql}))`;
7230
7240
  else field = fieldDefinition(discoveryRegistry(plan.predicateScope), plan.path).sql;
7231
7241
  const search = plan.search ? `AND positionCaseInsensitiveUTF8(value, ${parameters.add(plan.search, "String")}) > 0` : "";
7232
7242
  const limit = parameters.add(plan.limit + 1, "UInt64");
@@ -7434,6 +7444,185 @@ async function queryThreads(client, plan, timeoutMs) {
7434
7444
  });
7435
7445
  }
7436
7446
  //#endregion
7447
+ //#region src/storage/domains/observability/v-next/trace-aggregate.ts
7448
+ /** Trace-root column expressions (alias `r`) for each groupable dimension; same values `where` sees. */
7449
+ const DIMENSION_SQL = {
7450
+ entityType: "r.entityType",
7451
+ entityName: "r.entityName",
7452
+ environment: "r.environment",
7453
+ status: TRACE_STATUS_SQL,
7454
+ serviceName: "r.serviceName",
7455
+ executionSource: "r.executionSource",
7456
+ threadId: "r.threadId",
7457
+ resourceId: "r.resourceId",
7458
+ userId: "r.userId",
7459
+ sessionId: "r.sessionId",
7460
+ organizationId: "r.organizationId",
7461
+ experimentId: "r.experimentId"
7462
+ };
7463
+ const PERCENTILES = {
7464
+ "duration.p50": .5,
7465
+ "duration.p90": .9,
7466
+ "duration.p95": .95,
7467
+ "duration.p99": .99
7468
+ };
7469
+ const COMPARISON_SQL = {
7470
+ eq: "=",
7471
+ ne: "!=",
7472
+ lt: "<",
7473
+ lte: "<=",
7474
+ gt: ">",
7475
+ gte: ">="
7476
+ };
7477
+ /**
7478
+ * Compiles a trusted aggregate plan. The plan's caps are enforced by the planner and trusted here.
7479
+ *
7480
+ * Every candidate root is first reduced to a `facts` row (dimension values, duration, error flag,
7481
+ * countDistinct inputs, bucket). Without `interval`, groups are filtered, ordered, and limited in
7482
+ * one grouped select. With `interval`, one `GROUPING SETS` pass computes both the whole-window
7483
+ * group rows and the per-bucket rows: group rows are ranked (having → orderBy), and each bucket
7484
+ * row inherits its group's rank. ClickHouse re-executes a CTE at every reference, so this keeps
7485
+ * the candidate scan to a single pass instead of joining bucket rows back to a ranked CTE.
7486
+ */
7487
+ function compileClickHouseTraceAggregate(plan) {
7488
+ const parameters = new ParameterBuilder();
7489
+ const ctes = compileClickHouseTraceCandidates(plan, "*", parameters);
7490
+ const dimensionSql = (field) => {
7491
+ if (field.startsWith("metadata.")) return traceMetadataValueSql(parameters.add(field.slice(9), "String"));
7492
+ const sql = Object.hasOwn(DIMENSION_SQL, field) ? DIMENSION_SQL[field] : void 0;
7493
+ if (sql === void 0) throw new Error(`Unsupported trusted trace-aggregate field: ${field}`);
7494
+ return sql;
7495
+ };
7496
+ const distinctSql = (field) => field === "traceId" ? "r.traceId" : dimensionSql(field);
7497
+ const dimensionColumns = plan.dimensions.map((_, index) => `d${index}`);
7498
+ const distinctColumns = /* @__PURE__ */ new Map();
7499
+ const factColumns = plan.dimensions.map((dimension, index) => `CAST(${dimensionSql(dimension)} AS Nullable(String)) AS d${index}`);
7500
+ for (const measure of plan.measures) {
7501
+ if (measure.type !== "countDistinct" || distinctColumns.has(measure.field)) continue;
7502
+ const column = `cd${distinctColumns.size}`;
7503
+ distinctColumns.set(measure.field, column);
7504
+ factColumns.push(`CAST(${distinctSql(measure.field)} AS Nullable(String)) AS ${column}`);
7505
+ }
7506
+ factColumns.push(`toFloat64(${durationMsSql("r.startedAt", "r.endedAt")}) AS durationMs`, `isNotNull(r.error) AS isError`, `cityHash64(r.traceId) AS traceSeed`);
7507
+ if (plan.interval !== void 0) {
7508
+ const intervalMs = _mastra_core_storage.TRACE_AGGREGATE_INTERVAL_MS[plan.interval];
7509
+ if (intervalMs === void 0) throw new Error(`Unsupported trusted trace-aggregate interval: ${plan.interval}`);
7510
+ factColumns.push(`fromUnixTimestamp64Milli(intDiv(toUnixTimestamp64Milli(r.startedAt), ${intervalMs}) * ${intervalMs}, 'UTC') AS bucket`);
7511
+ }
7512
+ ctes.push(`facts AS (
7513
+ SELECT ${factColumns.join(",\n ")}
7514
+ FROM candidates r
7515
+ )`);
7516
+ const measureSql = (name) => {
7517
+ if (name === "count") return "toFloat64(count())";
7518
+ if (name === "errorCount") return "toFloat64(countIf(isError))";
7519
+ if (name === "errorRate") return "countIf(isError) / count()";
7520
+ if (name === "duration.avg") return "avg(durationMs)";
7521
+ if (name === "duration.min") return "min(durationMs)";
7522
+ if (name === "duration.max") return "max(durationMs)";
7523
+ const percentile = PERCENTILES[name];
7524
+ if (percentile !== void 0) return `quantileDeterministic(${percentile})(durationMs, traceSeed)`;
7525
+ const measure = plan.measures.find((candidate) => candidate.type === "countDistinct" && candidate.name === name);
7526
+ const distinctColumn = measure && distinctColumns.get(measure.field);
7527
+ if (distinctColumn === void 0) throw new Error(`Unsupported trusted trace-aggregate measure: ${name}`);
7528
+ return `toFloat64(uniqExact(${distinctColumn}))`;
7529
+ };
7530
+ const compileHaving = (predicate) => {
7531
+ if (predicate.type === "boolean") return predicate.args.map((arg) => `(${compileHaving(arg)})`).join(predicate.operator === "and" ? " AND " : " OR ");
7532
+ if (predicate.type === "not") return `NOT (${compileHaving(predicate.arg)})`;
7533
+ const measure = measureSql(predicate.measure);
7534
+ if (predicate.type === "membership") {
7535
+ if (predicate.values.length === 0) return predicate.operator === "in" ? "0" : "1";
7536
+ const list = predicate.values.map((value) => parameters.add(value, "Float64")).join(", ");
7537
+ return `${measure} ${predicate.operator === "in" ? "IN" : "NOT IN"} (${list})`;
7538
+ }
7539
+ const operator = COMPARISON_SQL[predicate.operator];
7540
+ if (operator === void 0) throw new Error(`Unsupported trusted trace-aggregate operator: ${predicate.operator}`);
7541
+ return `${measure} ${operator} ${parameters.add(predicate.value, "Float64")}`;
7542
+ };
7543
+ const havingSql = ["count() > 0", ...plan.having ? [`(${compileHaving(plan.having)})`] : []].join(" AND ");
7544
+ const direction = plan.orderBy.direction === "asc" ? "ASC" : "DESC";
7545
+ let orderTarget;
7546
+ if (plan.orderBy.target === "measure") orderTarget = measureSql(plan.orderBy.measure);
7547
+ else {
7548
+ const index = plan.dimensions.indexOf(plan.orderBy.dimension);
7549
+ if (index === -1) throw new Error(`Unsupported trusted trace-aggregate order dimension: ${plan.orderBy.dimension}`);
7550
+ orderTarget = dimensionColumns[index];
7551
+ }
7552
+ const tiebreakSql = dimensionColumns.map((column) => `${column} ASC NULLS LAST`);
7553
+ const measureColumns = plan.measures.map((measure, index) => `${measureSql(measure.name)} AS m${index}`);
7554
+ const measureNames = plan.measures.map((_, index) => `m${index}`);
7555
+ if (plan.interval === void 0) {
7556
+ const limit = parameters.add(plan.limit + 1, "UInt64");
7557
+ return {
7558
+ query: `WITH ${ctes.join(",\n")}
7559
+ SELECT ${[...dimensionColumns, ...measureColumns].join(", ")}
7560
+ FROM facts
7561
+ ${dimensionColumns.length > 0 ? `GROUP BY ${dimensionColumns.join(", ")}\n` : ""}HAVING ${havingSql}
7562
+ ORDER BY ${[`${orderTarget} ${direction} NULLS LAST`, ...tiebreakSql].join(", ")}
7563
+ LIMIT ${limit}`,
7564
+ query_params: parameters.params
7565
+ };
7566
+ }
7567
+ const maxRank = parameters.add(plan.limit, "UInt64");
7568
+ const groupOrderSql = [`__order ${direction} NULLS LAST`, ...tiebreakSql].join(", ");
7569
+ const groupPartition = dimensionColumns.length > 0 ? `PARTITION BY ${dimensionColumns.join(", ")}` : "";
7570
+ ctes.push(`grouped AS (
7571
+ SELECT ${[
7572
+ ...dimensionColumns,
7573
+ "bucket",
7574
+ "grouping(bucket) = 1 AS __isGroup",
7575
+ ...measureColumns,
7576
+ `(${havingSql}) AS __keep`,
7577
+ `${orderTarget} AS __order`
7578
+ ].join(", ")}
7579
+ FROM facts
7580
+ GROUP BY GROUPING SETS ((${[...dimensionColumns, "bucket"].join(", ")}), (${dimensionColumns.join(", ")}))
7581
+ )`, `ranked AS (
7582
+ SELECT *,
7583
+ if(__isGroup AND __keep, row_number() OVER (PARTITION BY __isGroup, __keep ORDER BY ${groupOrderSql}), 0) AS __groupRank,
7584
+ countIf(__isGroup AND __keep) OVER () AS __survivingGroups
7585
+ FROM grouped
7586
+ )`, `expanded AS (
7587
+ SELECT *, maxIf(__groupRank, __isGroup) OVER (${groupPartition}) AS __rank
7588
+ FROM ranked
7589
+ )`);
7590
+ return {
7591
+ query: `WITH ${ctes.join(",\n")}
7592
+ SELECT ${[
7593
+ ...dimensionColumns,
7594
+ "bucket",
7595
+ ...measureNames,
7596
+ `toUInt8(__survivingGroups > ${maxRank}) AS truncated`
7597
+ ].join(", ")}
7598
+ FROM expanded
7599
+ WHERE NOT __isGroup AND __rank BETWEEN 1 AND ${maxRank}
7600
+ ORDER BY __rank ASC, bucket ASC`,
7601
+ query_params: parameters.params
7602
+ };
7603
+ }
7604
+ async function aggregateTraces(client, plan, timeoutMs) {
7605
+ const result = await runWithClickHouseTraceQueryTimeout(client, { timeoutMs }, compileClickHouseTraceAggregate(plan));
7606
+ let truncated;
7607
+ let resultRows;
7608
+ if (plan.interval === void 0) {
7609
+ truncated = result.length > plan.limit;
7610
+ resultRows = result.slice(0, plan.limit);
7611
+ } else {
7612
+ truncated = Number(result[0]?.truncated ?? 0) === 1;
7613
+ resultRows = result;
7614
+ }
7615
+ return {
7616
+ rows: resultRows.map((row) => {
7617
+ const shaped = { measures: Object.fromEntries(plan.measures.map((measure, index) => [measure.name, Number(row[`m${index}`])])) };
7618
+ if (plan.dimensions.length > 0) shaped.dimensions = Object.fromEntries(plan.dimensions.map((dimension, index) => [dimension, row[`d${index}`] ?? null]));
7619
+ if (plan.interval !== void 0) shaped.bucket = new Date(row.bucket).toISOString();
7620
+ return shaped;
7621
+ }),
7622
+ truncated
7623
+ };
7624
+ }
7625
+ //#endregion
7437
7626
  //#region src/storage/domains/observability/v-next/trace-roots.ts
7438
7627
  /**
7439
7628
  * Get the root span for a trace, reading from trace_roots as compatibility path.
@@ -8681,7 +8870,8 @@ var ObservabilityStorageClickhouseVNext = class extends _mastra_core_storage.Obs
8681
8870
  "thread-query",
8682
8871
  "trace-query-tenant-scope",
8683
8872
  "feedback",
8684
- "trace-query-context-ids"
8873
+ "trace-query-context-ids",
8874
+ "trace-aggregate"
8685
8875
  ];
8686
8876
  return [
8687
8877
  "metrics",
@@ -8699,7 +8889,8 @@ var ObservabilityStorageClickhouseVNext = class extends _mastra_core_storage.Obs
8699
8889
  "thread-query",
8700
8890
  "trace-query-tenant-scope",
8701
8891
  "feedback",
8702
- "trace-query-context-ids"
8892
+ "trace-query-context-ids",
8893
+ "trace-aggregate"
8703
8894
  ];
8704
8895
  }
8705
8896
  async createSpan(args) {
@@ -8826,6 +9017,18 @@ var ObservabilityStorageClickhouseVNext = class extends _mastra_core_storage.Obs
8826
9017
  }, error);
8827
9018
  }
8828
9019
  }
9020
+ async aggregateTraces(plan) {
9021
+ try {
9022
+ return await aggregateTraces(this.#client, plan, this.#traceQueryTimeoutMs);
9023
+ } catch (error) {
9024
+ if (error instanceof _mastra_core_error.MastraError || error instanceof _mastra_core_storage.TraceQueryExecutionError || error instanceof _mastra_core_storage.TraceQueryResourceLimitError) throw error;
9025
+ throw new _mastra_core_error.MastraError({
9026
+ id: (0, _mastra_core_storage.createStorageErrorId)("CLICKHOUSE", "AGGREGATE_TRACES", "FAILED"),
9027
+ domain: _mastra_core_error.ErrorDomain.STORAGE,
9028
+ category: _mastra_core_error.ErrorCategory.THIRD_PARTY
9029
+ }, error);
9030
+ }
9031
+ }
8829
9032
  async getTraceQueryObservedFields(plan) {
8830
9033
  try {
8831
9034
  return await getTraceQueryObservedFields(this.#client, plan, this.#traceQueryDiscoveryLimits);