@mastra/clickhouse 1.23.0-alpha.2 → 1.23.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.
package/dist/index.js CHANGED
@@ -1689,6 +1689,7 @@ var MemoryStorageClickhouse = class extends MemoryStorage {
1689
1689
  });
1690
1690
  const threadIdsToUpdate = /* @__PURE__ */ new Set();
1691
1691
  const updatePromises = [];
1692
+ const writtenContent = /* @__PURE__ */ new Map();
1692
1693
  for (const existingMessage of parsedExistingMessages) {
1693
1694
  const updatePayload = messages.find((m) => m.id === existingMessage.id);
1694
1695
  if (!updatePayload) continue;
@@ -1715,6 +1716,7 @@ var MemoryStorageClickhouse = class extends MemoryStorage {
1715
1716
  };
1716
1717
  setClauses.push(`content = {var_content_${paramIdx}:String}`);
1717
1718
  values[`var_content_${paramIdx}`] = JSON.stringify(newContent);
1719
+ writtenContent.set(id, values[`var_content_${paramIdx}`]);
1718
1720
  paramIdx++;
1719
1721
  delete updatableFields.content;
1720
1722
  }
@@ -1731,7 +1733,7 @@ var MemoryStorageClickhouse = class extends MemoryStorage {
1731
1733
  UPDATE ${setClauses.join(", ")}
1732
1734
  WHERE id = {var_id_${paramIdx}:String}
1733
1735
  `;
1734
- console.info("Updating message:", id, "with query:", updateQuery, "values:", values);
1736
+ this.logger?.debug?.("Updating message", { id });
1735
1737
  updatePromises.push(this.client.command({
1736
1738
  query: updateQuery,
1737
1739
  query_params: values,
@@ -1767,7 +1769,7 @@ var MemoryStorageClickhouse = class extends MemoryStorage {
1767
1769
  if (updatedMessage) {
1768
1770
  let needsRetry = false;
1769
1771
  for (const [key, value] of Object.entries(fieldsToUpdate)) if (key === "content") {
1770
- const expectedContent = typeof value === "string" ? value : JSON.stringify(value);
1772
+ const expectedContent = writtenContent.get(id);
1771
1773
  if ((typeof updatedMessage.content === "string" ? updatedMessage.content : JSON.stringify(updatedMessage.content)) !== expectedContent) {
1772
1774
  needsRetry = true;
1773
1775
  break;
@@ -1777,7 +1779,7 @@ var MemoryStorageClickhouse = class extends MemoryStorage {
1777
1779
  break;
1778
1780
  }
1779
1781
  if (needsRetry) {
1780
- console.info("Update not applied correctly, retrying with DELETE + INSERT for message:", id);
1782
+ this.logger?.warn?.("Update not applied correctly, retrying with DELETE + INSERT", { id });
1781
1783
  await this.client.command({
1782
1784
  query: `DELETE FROM ${TABLE_MESSAGES} WHERE id = {messageId:String}`,
1783
1785
  query_params: { messageId: id },
@@ -2412,7 +2414,7 @@ var ObservabilityStorageClickhouse = class extends ObservabilityStorage {
2412
2414
  if (filters.hasChildError !== void 0) {
2413
2415
  const finalClause = (TABLE_ENGINES[TABLE_SPANS] ?? "MergeTree()").startsWith("ReplacingMergeTree") ? "FINAL" : "";
2414
2416
  conditions.push(`traceId ${filters.hasChildError ? "IN" : "NOT IN"} (
2415
- SELECT c.traceId FROM ${TABLE_SPANS} ${finalClause} c
2417
+ SELECT c.traceId FROM ${TABLE_SPANS} c ${finalClause}
2416
2418
  WHERE c.error IS NOT NULL AND c.error != ''
2417
2419
  )`);
2418
2420
  }
@@ -7861,12 +7863,31 @@ function compileClickHouseTraceAggregate(plan) {
7861
7863
  if (intervalMs === void 0) throw new Error(`Unsupported trusted trace-aggregate interval: ${plan.interval}`);
7862
7864
  factColumns.push(`fromUnixTimestamp64Milli(intDiv(toUnixTimestamp64Milli(r.startedAt), ${intervalMs}) * ${intervalMs}, 'UTC') AS bucket`);
7863
7865
  }
7866
+ const measureKinds = new Set(plan.measures.map((measure) => measureRule(measure.name)?.kind));
7867
+ const hasCost = measureKinds.has("cost");
7868
+ let usageJoin = "";
7869
+ if (hasCost || measureKinds.has("tokens")) {
7870
+ ctes.push(compileUsageCte(plan, parameters));
7871
+ usageJoin = "\n LEFT JOIN usage u ON u.traceId = r.traceId";
7872
+ USAGE_METRIC_NAMES.forEach((_, index) => factColumns.push(`if(u.hasUsage = 1, u.t${index}, NULL) AS t${index}`));
7873
+ 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`);
7874
+ }
7864
7875
  ctes.push(`facts AS (
7865
7876
  SELECT ${factColumns.join(",\n ")}
7866
- FROM candidates r
7877
+ FROM candidates r${usageJoin}
7867
7878
  )`);
7879
+ const mixedUnitsSql = "minOrNull(unitMin) != maxOrNull(unitMax)";
7868
7880
  const measureSql = (name) => {
7869
7881
  if (name === "count") return "toFloat64(count())";
7882
+ const rule = measureRule(name);
7883
+ if (rule?.kind === "tokens" && rule.metricNames) {
7884
+ const value = rule.metricNames.map((metricName) => `t${usageMetricIndex(metricName)}`).join(" + ");
7885
+ return `toFloat64(${rule.statistic === "avg" ? "avgOrNull" : "sumOrNull"}(${value}))`;
7886
+ }
7887
+ if (rule?.kind === "cost") {
7888
+ const aggregate = rule.statistic === "avg" ? "avgOrNull" : "sumOrNull";
7889
+ return `if(${mixedUnitsSql}, NULL, toFloat64(${aggregate}(traceCost)))`;
7890
+ }
7870
7891
  if (name === "errorCount") return "toFloat64(countIf(isError))";
7871
7892
  if (name === "errorRate") return "countIf(isError) / count()";
7872
7893
  if (name === "duration.avg") return "avg(durationMs)";
@@ -7904,6 +7925,11 @@ function compileClickHouseTraceAggregate(plan) {
7904
7925
  const tiebreakSql = dimensionColumns.map((column) => `${column} ASC NULLS LAST`);
7905
7926
  const measureColumns = plan.measures.map((measure, index) => `${measureSql(measure.name)} AS m${index}`);
7906
7927
  const measureNames = plan.measures.map((_, index) => `m${index}`);
7928
+ if (hasCost) {
7929
+ const mixedUnit = parameters.add(coreStorage.TRACE_AGGREGATE_MIXED_COST_UNIT, "String");
7930
+ measureColumns.push(`countIf(covered) / nullIf(countIf(usageBearing), 0) AS costCoverage`, `if(${mixedUnitsSql}, ${mixedUnit}, minOrNull(unitMin)) AS costUnit`);
7931
+ measureNames.push("costCoverage", "costUnit");
7932
+ }
7907
7933
  if (plan.interval === void 0) {
7908
7934
  const limit = parameters.add(plan.limit + 1, "UInt64");
7909
7935
  return {
@@ -7925,7 +7951,7 @@ LIMIT ${limit}`,
7925
7951
  "bucket",
7926
7952
  "grouping(bucket) = 1 AS __isGroup",
7927
7953
  ...measureColumns,
7928
- `(${havingSql}) AS __keep`,
7954
+ `ifNull(${havingSql}, 0) AS __keep`,
7929
7955
  `${orderTarget} AS __order`
7930
7956
  ].join(", ")}
7931
7957
  FROM facts
@@ -7953,6 +7979,70 @@ ORDER BY __rank ASC, bucket ASC`,
7953
7979
  query_params: parameters.params
7954
7980
  };
7955
7981
  }
7982
+ const USAGE_METRIC_NAMES = coreStorage.TRACE_AGGREGATE_USAGE_METRIC_NAMES;
7983
+ function measureRule(name) {
7984
+ return coreStorage.isTraceAggregateCanonicalMeasure(name) ? coreStorage.TRACE_AGGREGATE_MEASURE_REGISTRY[name] : void 0;
7985
+ }
7986
+ function usageMetricIndex(name) {
7987
+ const index = USAGE_METRIC_NAMES.indexOf(name);
7988
+ if (index === -1) throw new Error(`Unsupported trusted trace-aggregate token metric: ${name}`);
7989
+ return index;
7990
+ }
7991
+ /**
7992
+ * Builds `usage`: one row per candidate trace that has at least one token metric row, holding
7993
+ * per-name token sums (`t<i>`, indexed by `TRACE_AGGREGATE_USAGE_METRIC_NAMES`) and the trace's
7994
+ * pricing state. Metric rows are matched by traceId, usage metric name, tenant scope, and
7995
+ * `timestamp >= from` only: a trace that starts in the window can emit metrics after `to`, and
7996
+ * the candidates semi-join already bounds the scan.
7997
+ *
7998
+ * Rows are deduplicated with `LIMIT 1 BY metricId` at query time. `ReplacingMergeTree` only
7999
+ * collapses rows with equal sort keys `(name, timestamp, metricId)`, so retried copies with
8000
+ * different timestamps never merge, and copies with equal keys stay separate until a background
8001
+ * merge runs.
8002
+ */
8003
+ function compileUsageCte(plan, parameters) {
8004
+ const names = USAGE_METRIC_NAMES.map((name) => parameters.add(name, "String"));
8005
+ const costRow = `name IN (${coreStorage.TRACE_AGGREGATE_COST_METRIC_NAMES.map((name) => names[usageMetricIndex(name)]).join(", ")})`;
8006
+ const from = parameters.add(plan.timeRange.from, "DateTime64(3, 'UTC')");
8007
+ const tenant = compileTenantScope(plan.scope, parameters);
8008
+ const priced = `${costRow} AND isNotNull(estimatedCost) AND isNotNull(costUnit) AND NOT hasError`;
8009
+ const failed = `${costRow} AND (hasError OR (isNotNull(estimatedCost) AND isNull(costUnit)))`;
8010
+ return `usage AS (
8011
+ SELECT traceId,
8012
+ toUInt8(1) AS hasUsage,
8013
+ ${names.map((name, index) => `sumIf(value, name = ${name}) AS t${index}`).join(",\n ")},
8014
+ sumIf(assumeNotNull(estimatedCost), ${priced}) AS cost,
8015
+ toUInt8(countIf(${priced}) > 0) AS priced,
8016
+ toUInt8(countIf(${failed}) > 0) AS pricingFailure,
8017
+ minIf(assumeNotNull(costUnit), ${priced}) AS unitMin,
8018
+ maxIf(assumeNotNull(costUnit), ${priced}) AS unitMax
8019
+ FROM (
8020
+ SELECT traceId,
8021
+ latest.1 AS name,
8022
+ latest.2 AS value,
8023
+ latest.3 AS estimatedCost,
8024
+ latest.4 AS costUnit,
8025
+ latest.5 AS hasError
8026
+ FROM (
8027
+ SELECT traceId,
8028
+ argMax(tuple(name, value, estimatedCost, costUnit, hasError), timestamp) AS latest
8029
+ FROM (
8030
+ SELECT traceId, metricId, timestamp, name, value, estimatedCost, costUnit,
8031
+ if(${costRow}, ifNull(JSONHas(costMetadata, 'error') AND JSONType(costMetadata, 'error') != 'Null', 0), 0) AS hasError
8032
+ FROM ${TABLE_METRIC_EVENTS}
8033
+ WHERE traceId IN (SELECT traceId FROM candidates)
8034
+ AND name IN (${names.join(", ")})
8035
+ AND timestamp >= ${from}${tenant}
8036
+ )
8037
+ GROUP BY traceId, metricId
8038
+ )
8039
+ )
8040
+ GROUP BY traceId
8041
+ )`;
8042
+ }
8043
+ function nullableNumber(value) {
8044
+ return value === null || value === void 0 ? null : Number(value);
8045
+ }
7956
8046
  async function aggregateTraces(client, plan, timeoutMs) {
7957
8047
  const result = await runWithClickHouseTraceQueryTimeout(client, { timeoutMs }, compileClickHouseTraceAggregate(plan));
7958
8048
  let truncated;
@@ -7966,7 +8056,11 @@ async function aggregateTraces(client, plan, timeoutMs) {
7966
8056
  }
7967
8057
  return {
7968
8058
  rows: resultRows.map((row) => {
7969
- const shaped = { measures: Object.fromEntries(plan.measures.map((measure, index) => [measure.name, Number(row[`m${index}`])])) };
8059
+ const shaped = { measures: Object.fromEntries(plan.measures.map((measure, index) => [measure.name, nullableNumber(row[`m${index}`])])) };
8060
+ if ("costCoverage" in row) shaped.cost = {
8061
+ coverage: nullableNumber(row.costCoverage),
8062
+ unit: row.costUnit ?? null
8063
+ };
7970
8064
  if (plan.dimensions.length > 0) shaped.dimensions = Object.fromEntries(plan.dimensions.map((dimension, index) => [dimension, row[`d${index}`] ?? null]));
7971
8065
  if (plan.interval !== void 0) shaped.bucket = new Date(row.bucket).toISOString();
7972
8066
  return shaped;