@mastra/clickhouse 1.23.0-alpha.2 → 1.23.0-alpha.3

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.23.0-alpha.2"
6
+ version: "1.23.0-alpha.3"
7
7
  ---
8
8
 
9
9
  ## When to use
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.23.0-alpha.2",
2
+ "version": "1.23.0-alpha.3",
3
3
  "package": "@mastra/clickhouse",
4
4
  "exports": {},
5
5
  "modules": {}
package/dist/index.cjs CHANGED
@@ -1712,6 +1712,7 @@ var MemoryStorageClickhouse = class extends _mastra_core_storage.MemoryStorage {
1712
1712
  });
1713
1713
  const threadIdsToUpdate = /* @__PURE__ */ new Set();
1714
1714
  const updatePromises = [];
1715
+ const writtenContent = /* @__PURE__ */ new Map();
1715
1716
  for (const existingMessage of parsedExistingMessages) {
1716
1717
  const updatePayload = messages.find((m) => m.id === existingMessage.id);
1717
1718
  if (!updatePayload) continue;
@@ -1738,6 +1739,7 @@ var MemoryStorageClickhouse = class extends _mastra_core_storage.MemoryStorage {
1738
1739
  };
1739
1740
  setClauses.push(`content = {var_content_${paramIdx}:String}`);
1740
1741
  values[`var_content_${paramIdx}`] = JSON.stringify(newContent);
1742
+ writtenContent.set(id, values[`var_content_${paramIdx}`]);
1741
1743
  paramIdx++;
1742
1744
  delete updatableFields.content;
1743
1745
  }
@@ -1754,7 +1756,7 @@ var MemoryStorageClickhouse = class extends _mastra_core_storage.MemoryStorage {
1754
1756
  UPDATE ${setClauses.join(", ")}
1755
1757
  WHERE id = {var_id_${paramIdx}:String}
1756
1758
  `;
1757
- console.info("Updating message:", id, "with query:", updateQuery, "values:", values);
1759
+ this.logger?.debug?.("Updating message", { id });
1758
1760
  updatePromises.push(this.client.command({
1759
1761
  query: updateQuery,
1760
1762
  query_params: values,
@@ -1790,7 +1792,7 @@ var MemoryStorageClickhouse = class extends _mastra_core_storage.MemoryStorage {
1790
1792
  if (updatedMessage) {
1791
1793
  let needsRetry = false;
1792
1794
  for (const [key, value] of Object.entries(fieldsToUpdate)) if (key === "content") {
1793
- const expectedContent = typeof value === "string" ? value : JSON.stringify(value);
1795
+ const expectedContent = writtenContent.get(id);
1794
1796
  if ((typeof updatedMessage.content === "string" ? updatedMessage.content : JSON.stringify(updatedMessage.content)) !== expectedContent) {
1795
1797
  needsRetry = true;
1796
1798
  break;
@@ -1800,7 +1802,7 @@ var MemoryStorageClickhouse = class extends _mastra_core_storage.MemoryStorage {
1800
1802
  break;
1801
1803
  }
1802
1804
  if (needsRetry) {
1803
- console.info("Update not applied correctly, retrying with DELETE + INSERT for message:", id);
1805
+ this.logger?.warn?.("Update not applied correctly, retrying with DELETE + INSERT", { id });
1804
1806
  await this.client.command({
1805
1807
  query: `DELETE FROM ${_mastra_core_storage.TABLE_MESSAGES} WHERE id = {messageId:String}`,
1806
1808
  query_params: { messageId: id },
@@ -2435,7 +2437,7 @@ var ObservabilityStorageClickhouse = class extends _mastra_core_storage.Observab
2435
2437
  if (filters.hasChildError !== void 0) {
2436
2438
  const finalClause = (TABLE_ENGINES[_mastra_core_storage.TABLE_SPANS] ?? "MergeTree()").startsWith("ReplacingMergeTree") ? "FINAL" : "";
2437
2439
  conditions.push(`traceId ${filters.hasChildError ? "IN" : "NOT IN"} (
2438
- SELECT c.traceId FROM ${_mastra_core_storage.TABLE_SPANS} ${finalClause} c
2440
+ SELECT c.traceId FROM ${_mastra_core_storage.TABLE_SPANS} c ${finalClause}
2439
2441
  WHERE c.error IS NOT NULL AND c.error != ''
2440
2442
  )`);
2441
2443
  }
@@ -7884,12 +7886,31 @@ function compileClickHouseTraceAggregate(plan) {
7884
7886
  if (intervalMs === void 0) throw new Error(`Unsupported trusted trace-aggregate interval: ${plan.interval}`);
7885
7887
  factColumns.push(`fromUnixTimestamp64Milli(intDiv(toUnixTimestamp64Milli(r.startedAt), ${intervalMs}) * ${intervalMs}, 'UTC') AS bucket`);
7886
7888
  }
7889
+ const measureKinds = new Set(plan.measures.map((measure) => measureRule(measure.name)?.kind));
7890
+ const hasCost = measureKinds.has("cost");
7891
+ let usageJoin = "";
7892
+ if (hasCost || measureKinds.has("tokens")) {
7893
+ ctes.push(compileUsageCte(plan, parameters));
7894
+ usageJoin = "\n LEFT JOIN usage u ON u.traceId = r.traceId";
7895
+ USAGE_METRIC_NAMES.forEach((_, index) => factColumns.push(`if(u.hasUsage = 1, u.t${index}, NULL) AS t${index}`));
7896
+ 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`);
7897
+ }
7887
7898
  ctes.push(`facts AS (
7888
7899
  SELECT ${factColumns.join(",\n ")}
7889
- FROM candidates r
7900
+ FROM candidates r${usageJoin}
7890
7901
  )`);
7902
+ const mixedUnitsSql = "minOrNull(unitMin) != maxOrNull(unitMax)";
7891
7903
  const measureSql = (name) => {
7892
7904
  if (name === "count") return "toFloat64(count())";
7905
+ const rule = measureRule(name);
7906
+ if (rule?.kind === "tokens" && rule.metricNames) {
7907
+ const value = rule.metricNames.map((metricName) => `t${usageMetricIndex(metricName)}`).join(" + ");
7908
+ return `toFloat64(${rule.statistic === "avg" ? "avgOrNull" : "sumOrNull"}(${value}))`;
7909
+ }
7910
+ if (rule?.kind === "cost") {
7911
+ const aggregate = rule.statistic === "avg" ? "avgOrNull" : "sumOrNull";
7912
+ return `if(${mixedUnitsSql}, NULL, toFloat64(${aggregate}(traceCost)))`;
7913
+ }
7893
7914
  if (name === "errorCount") return "toFloat64(countIf(isError))";
7894
7915
  if (name === "errorRate") return "countIf(isError) / count()";
7895
7916
  if (name === "duration.avg") return "avg(durationMs)";
@@ -7927,6 +7948,11 @@ function compileClickHouseTraceAggregate(plan) {
7927
7948
  const tiebreakSql = dimensionColumns.map((column) => `${column} ASC NULLS LAST`);
7928
7949
  const measureColumns = plan.measures.map((measure, index) => `${measureSql(measure.name)} AS m${index}`);
7929
7950
  const measureNames = plan.measures.map((_, index) => `m${index}`);
7951
+ if (hasCost) {
7952
+ const mixedUnit = parameters.add(_mastra_core_storage.TRACE_AGGREGATE_MIXED_COST_UNIT, "String");
7953
+ measureColumns.push(`countIf(covered) / nullIf(countIf(usageBearing), 0) AS costCoverage`, `if(${mixedUnitsSql}, ${mixedUnit}, minOrNull(unitMin)) AS costUnit`);
7954
+ measureNames.push("costCoverage", "costUnit");
7955
+ }
7930
7956
  if (plan.interval === void 0) {
7931
7957
  const limit = parameters.add(plan.limit + 1, "UInt64");
7932
7958
  return {
@@ -7948,7 +7974,7 @@ LIMIT ${limit}`,
7948
7974
  "bucket",
7949
7975
  "grouping(bucket) = 1 AS __isGroup",
7950
7976
  ...measureColumns,
7951
- `(${havingSql}) AS __keep`,
7977
+ `ifNull(${havingSql}, 0) AS __keep`,
7952
7978
  `${orderTarget} AS __order`
7953
7979
  ].join(", ")}
7954
7980
  FROM facts
@@ -7976,6 +8002,70 @@ ORDER BY __rank ASC, bucket ASC`,
7976
8002
  query_params: parameters.params
7977
8003
  };
7978
8004
  }
8005
+ const USAGE_METRIC_NAMES = _mastra_core_storage.TRACE_AGGREGATE_USAGE_METRIC_NAMES;
8006
+ function measureRule(name) {
8007
+ return _mastra_core_storage.isTraceAggregateCanonicalMeasure(name) ? _mastra_core_storage.TRACE_AGGREGATE_MEASURE_REGISTRY[name] : void 0;
8008
+ }
8009
+ function usageMetricIndex(name) {
8010
+ const index = USAGE_METRIC_NAMES.indexOf(name);
8011
+ if (index === -1) throw new Error(`Unsupported trusted trace-aggregate token metric: ${name}`);
8012
+ return index;
8013
+ }
8014
+ /**
8015
+ * Builds `usage`: one row per candidate trace that has at least one token metric row, holding
8016
+ * per-name token sums (`t<i>`, indexed by `TRACE_AGGREGATE_USAGE_METRIC_NAMES`) and the trace's
8017
+ * pricing state. Metric rows are matched by traceId, usage metric name, tenant scope, and
8018
+ * `timestamp >= from` only: a trace that starts in the window can emit metrics after `to`, and
8019
+ * the candidates semi-join already bounds the scan.
8020
+ *
8021
+ * Rows are deduplicated with `LIMIT 1 BY metricId` at query time. `ReplacingMergeTree` only
8022
+ * collapses rows with equal sort keys `(name, timestamp, metricId)`, so retried copies with
8023
+ * different timestamps never merge, and copies with equal keys stay separate until a background
8024
+ * merge runs.
8025
+ */
8026
+ function compileUsageCte(plan, parameters) {
8027
+ const names = USAGE_METRIC_NAMES.map((name) => parameters.add(name, "String"));
8028
+ const costRow = `name IN (${_mastra_core_storage.TRACE_AGGREGATE_COST_METRIC_NAMES.map((name) => names[usageMetricIndex(name)]).join(", ")})`;
8029
+ const from = parameters.add(plan.timeRange.from, "DateTime64(3, 'UTC')");
8030
+ const tenant = compileTenantScope(plan.scope, parameters);
8031
+ const priced = `${costRow} AND isNotNull(estimatedCost) AND isNotNull(costUnit) AND NOT hasError`;
8032
+ const failed = `${costRow} AND (hasError OR (isNotNull(estimatedCost) AND isNull(costUnit)))`;
8033
+ return `usage AS (
8034
+ SELECT traceId,
8035
+ toUInt8(1) AS hasUsage,
8036
+ ${names.map((name, index) => `sumIf(value, name = ${name}) AS t${index}`).join(",\n ")},
8037
+ sumIf(assumeNotNull(estimatedCost), ${priced}) AS cost,
8038
+ toUInt8(countIf(${priced}) > 0) AS priced,
8039
+ toUInt8(countIf(${failed}) > 0) AS pricingFailure,
8040
+ minIf(assumeNotNull(costUnit), ${priced}) AS unitMin,
8041
+ maxIf(assumeNotNull(costUnit), ${priced}) AS unitMax
8042
+ FROM (
8043
+ SELECT traceId,
8044
+ latest.1 AS name,
8045
+ latest.2 AS value,
8046
+ latest.3 AS estimatedCost,
8047
+ latest.4 AS costUnit,
8048
+ latest.5 AS hasError
8049
+ FROM (
8050
+ SELECT traceId,
8051
+ argMax(tuple(name, value, estimatedCost, costUnit, hasError), timestamp) AS latest
8052
+ FROM (
8053
+ SELECT traceId, metricId, timestamp, name, value, estimatedCost, costUnit,
8054
+ if(${costRow}, ifNull(JSONHas(costMetadata, 'error') AND JSONType(costMetadata, 'error') != 'Null', 0), 0) AS hasError
8055
+ FROM ${TABLE_METRIC_EVENTS}
8056
+ WHERE traceId IN (SELECT traceId FROM candidates)
8057
+ AND name IN (${names.join(", ")})
8058
+ AND timestamp >= ${from}${tenant}
8059
+ )
8060
+ GROUP BY traceId, metricId
8061
+ )
8062
+ )
8063
+ GROUP BY traceId
8064
+ )`;
8065
+ }
8066
+ function nullableNumber(value) {
8067
+ return value === null || value === void 0 ? null : Number(value);
8068
+ }
7979
8069
  async function aggregateTraces(client, plan, timeoutMs) {
7980
8070
  const result = await runWithClickHouseTraceQueryTimeout(client, { timeoutMs }, compileClickHouseTraceAggregate(plan));
7981
8071
  let truncated;
@@ -7989,7 +8079,11 @@ async function aggregateTraces(client, plan, timeoutMs) {
7989
8079
  }
7990
8080
  return {
7991
8081
  rows: resultRows.map((row) => {
7992
- const shaped = { measures: Object.fromEntries(plan.measures.map((measure, index) => [measure.name, Number(row[`m${index}`])])) };
8082
+ const shaped = { measures: Object.fromEntries(plan.measures.map((measure, index) => [measure.name, nullableNumber(row[`m${index}`])])) };
8083
+ if ("costCoverage" in row) shaped.cost = {
8084
+ coverage: nullableNumber(row.costCoverage),
8085
+ unit: row.costUnit ?? null
8086
+ };
7993
8087
  if (plan.dimensions.length > 0) shaped.dimensions = Object.fromEntries(plan.dimensions.map((dimension, index) => [dimension, row[`d${index}`] ?? null]));
7994
8088
  if (plan.interval !== void 0) shaped.bucket = new Date(row.bucket).toISOString();
7995
8089
  return shaped;