@mastra/clickhouse 1.20.1 → 1.21.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.
@@ -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.20.1"
6
+ version: "1.21.0-alpha.1"
7
7
  ---
8
8
 
9
9
  ## When to use
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.20.1",
2
+ "version": "1.21.0-alpha.1",
3
3
  "package": "@mastra/clickhouse",
4
4
  "exports": {},
5
5
  "modules": {}
@@ -89,7 +89,7 @@ export const mastra = new Mastra({
89
89
 
90
90
  Trace deletion cascades to spans, trace roots and branches, metrics, logs, scores, and feedback linked by trace ID. Signals without a trace ID are preserved. Mastra records the deletion predicate, then waits for ClickHouse lightweight delete masks to be applied. Normal reads no longer return the rows matched by that operation when the call resolves.
91
91
 
92
- Lightweight deletion is a hide-only operation that marks rows with ClickHouse's `_row_exists` mask. Physical removal depends on merges and deployment-configured retention TTLs. `ObservabilityStorageClickhouseVNext` applies retention only when you provide a `RetentionConfig`; Mastra OSS doesn't configure a default retention TTL.
92
+ Lightweight deletion is a hide-only operation that marks rows with ClickHouse's `_row_exists` mask. Physical removal depends on merges and deployment-configured retention TTLs. `ObservabilityStorageClickhouseVNext` applies retention only when you pass the `retention` option. Mastra doesn't configure a default retention TTL.
93
93
 
94
94
  When all five observability signals have finite retention, Mastra also applies a TTL to deletion requests so they outlive the signal rows they protect. If any signal is unbounded, deletion requests remain unbounded. See [storage retention](https://mastra.ai/reference/storage/retention) for how the deletion-request TTL is calculated.
95
95
 
@@ -275,7 +275,7 @@ Don't set `replication` on ClickHouse Cloud. Cloud rewrites `MergeTree` to `Shar
275
275
 
276
276
  ### Observability domain options
277
277
 
278
- `ObservabilityStorageClickhouse` and `ObservabilityStorageClickhouseVNext` accept the same connection options as `ClickhouseStore` (`url`, `username`, `password`, or a pre-configured `client`).
278
+ `ObservabilityStorageClickhouse` and `ObservabilityStorageClickhouseVNext` accept the same connection options as `ClickhouseStore` (`url`, `username`, `password`, or a pre-configured `client`). `ObservabilityStorageClickhouseVNext` also accepts `retention`, the per-signal TTL in days described in [ClickHouse native TTL](https://mastra.ai/reference/storage/retention), and `traceQuery`, the execution limits for [advanced trace queries](https://mastra.ai/reference/observability/tracing/trace-query).
279
279
 
280
280
  ## Hosting options
281
281
 
@@ -15,7 +15,7 @@ Storage adapters use the shared core retention contract for `prune()`, or a data
15
15
  | Adapter | Mechanism | Retention support |
16
16
  | -------------------- | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
17
17
  | libSQL | `prune()` | All supported growth domains |
18
- | PostgreSQL | `prune()` | All supported growth domains. V-next observability drops expired partitions or chunks |
18
+ | PostgreSQL | `prune()` | All supported growth domains. vNext observability drops expired partitions or chunks |
19
19
  | MongoDB | `prune()` or native TTL | All supported growth domains. Native TTL indexes are also available |
20
20
  | DuckDB | `prune()` | Observability spans, metrics, logs, scores, and feedback |
21
21
  | MySQL | `prune()` | Observability spans |
@@ -102,10 +102,10 @@ Each domain specifies its age-prunable tables and the timestamp column that anch
102
102
  | `memory` | `resources` | `createdAt` | Resource age |
103
103
  | `threadState` | `threadState` | `updatedAt` | Inactivity: state for still-active threads survives |
104
104
  | `observability` | `spans` | `startedAt` | Span age |
105
- | `observability` | `metrics` | `timestamp` | Metric event age (v-next only) |
106
- | `observability` | `logs` | `timestamp` | Log event age (v-next only) |
107
- | `observability` | `scores` | `timestamp` | Score event age (v-next only) |
108
- | `observability` | `feedback` | `timestamp` | Feedback event age (v-next only) |
105
+ | `observability` | `metrics` | `timestamp` | Metric event age (vNext only) |
106
+ | `observability` | `logs` | `timestamp` | Log event age (vNext only) |
107
+ | `observability` | `scores` | `timestamp` | Score event age (vNext only) |
108
+ | `observability` | `feedback` | `timestamp` | Feedback event age (vNext only) |
109
109
  | `scores` | `scorers` | `createdAt` | Score record age |
110
110
  | `workflows` | `workflowSnapshot` | `updatedAt` | Inactivity, suspended or long-running workflows survive |
111
111
  | `backgroundTasks` | `backgroundTasks` | `completedAt` | Time since completion, in-flight tasks (`NULL`) are never pruned |
@@ -122,7 +122,7 @@ Each domain specifies its age-prunable tables and the timestamp column that anch
122
122
  > - On PostgreSQL, timestamp anchors use the timezone-aware mirror columns (for example `createdAtZ`, `completedAtZ`).
123
123
  > - DuckDB observability stores append-only events for all five signals. Its `spans` policy uses the event `timestamp` column rather than `startedAt`.
124
124
  > - LibSQL and PostgreSQL support all domains above except `harness`, which PostgreSQL doesn't implement. MongoDB supports all except `threadState` and `harness`. DuckDB, MySQL, Microsoft SQL Server, Oracle Database, Amazon Aurora DSQL, and Google Cloud Spanner currently support retention only in their `observability` domains, with the signal coverage shown in the support matrix.
125
- > - The v-next PostgreSQL observability domain stores signal events in day-partitioned tables (`spans`, `metrics`, `logs`, `scores`, `feedback`). For it, `prune()` drops whole day partitions (or TimescaleDB chunks) that are entirely older than the cutoff instead of deleting rows: effective level of detail is one day, and a partition is only dropped once its entire day is past `maxAge`. `PruneResult.deleted` reports the number of rows in the dropped partitions.
125
+ > - The vNext PostgreSQL observability domain stores signal events in day-partitioned tables (`spans`, `metrics`, `logs`, `scores`, `feedback`). For it, `prune()` drops whole day partitions (or TimescaleDB chunks) that are entirely older than the cutoff instead of deleting rows: effective level of detail is one day, and a partition is only dropped once its entire day is past `maxAge`. `PruneResult.deleted` reports the number of rows in the dropped partitions.
126
126
 
127
127
  ## Methods
128
128
 
@@ -208,7 +208,9 @@ You can also cancel a long-running prune with an `AbortSignal`: the loop stops b
208
208
 
209
209
  ClickHouse observability storage uses native table TTLs instead of `prune()`. Configure retention as days per signal. `init()` applies the TTLs to new and existing tables and skips `ALTER TABLE` statements when the configured TTL is already present.
210
210
 
211
- For deployments that need to update TTL configuration without running the full initialization path, call `applyRetention()` on the v-next observability store:
211
+ Omitted signals, and signals set to zero or less, get no TTL. When you remove a signal from `retention`, the next `init()` or `applyRetention()` removes that table's TTL. If Mastra can't read the current TTLs from `system.tables`, it applies the configured TTLs and leaves the others unchanged.
212
+
213
+ For deployments that need to update TTL configuration without running the full initialization path, call `applyRetention()` on the vNext observability store:
212
214
 
213
215
  ```typescript
214
216
  import { ObservabilityStorageClickhouseVNext } from '@mastra/clickhouse'
package/dist/index.cjs CHANGED
@@ -6552,6 +6552,10 @@ const TRACE_FIELDS = {
6552
6552
  status: {
6553
6553
  sql: TRACE_STATUS_SQL,
6554
6554
  parameterType: "String"
6555
+ },
6556
+ tags: {
6557
+ sql: "r.tags",
6558
+ parameterType: "String"
6555
6559
  }
6556
6560
  };
6557
6561
  const SPAN_FIELDS = {
@@ -6741,6 +6745,11 @@ function compileScalarPredicate(predicate, registry, parameters, allowMetadata =
6741
6745
  };
6742
6746
  })() : fieldDefinition(registry, predicate.field);
6743
6747
  if (predicate.type === "presence") return `${predicate.operator === "exists" ? "isNotNull" : "isNull"}(${field.sql})`;
6748
+ if (predicate.type === "collection") {
6749
+ if (!("value" in predicate)) return `${predicate.operator}(${field.sql})`;
6750
+ const member = parameters.add(predicate.value, field.parameterType);
6751
+ return predicate.operator === "includes" ? `has(${field.sql}, ${member})` : `notEmpty(${field.sql}) AND NOT has(${field.sql}, ${member})`;
6752
+ }
6744
6753
  if (predicate.type === "membership") {
6745
6754
  const values = predicate.values.map((value) => parameters.add(value, field.parameterType)).join(", ");
6746
6755
  return `ifNull(${`${field.sql} ${predicate.operator === "in" ? "IN" : "NOT IN"} (${values})`}, ${predicate.operator === "in" ? "0" : "1"})`;
@@ -6765,6 +6774,7 @@ function compileFeedbackScalarPredicate(predicate, parameters) {
6765
6774
  const present = `(isNotNull(s.valueString) OR isNotNull(s.valueNumber))`;
6766
6775
  return predicate.operator === "exists" ? present : `NOT ${present}`;
6767
6776
  }
6777
+ if (predicate.type === "collection") throw new Error("Unsupported trusted trace-query field: value");
6768
6778
  return compileScalarPredicate(predicate, typeof (predicate.type === "membership" ? predicate.values[0] : predicate.value) === "number" ? { value: {
6769
6779
  sql: "s.valueNumber",
6770
6780
  parameterType: "Float64"
@@ -6813,9 +6823,20 @@ function compileThreadPredicate(predicate, parameters) {
6813
6823
  if (predicate.type === "boolean") return predicate.args.map((arg) => `(${compileThreadPredicate(arg, parameters)})`).join(predicate.operator === "and" ? " AND " : " OR ");
6814
6824
  return `NOT (${compileThreadPredicate(predicate.arg, parameters)})`;
6815
6825
  }
6816
- function compileClickHouseTraceScope(selection, relationCollections, parameters) {
6826
+ /**
6827
+ * Tenant conditions ANDed into every root and related-signal scan. Columns are
6828
+ * `Nullable(String)`, so rows without a tenant never match a scope.
6829
+ */
6830
+ function compileTenantScope(scope, parameters) {
6831
+ if (!scope) return "";
6832
+ let sql = `\n AND organizationId = ${parameters.add(scope.organizationId, "String")}`;
6833
+ if (scope.resourceId !== void 0) sql += `\n AND resourceId = ${parameters.add(scope.resourceId, "String")}`;
6834
+ return sql;
6835
+ }
6836
+ function compileClickHouseTraceScope(selection, relationCollections, parameters, scope) {
6817
6837
  const from = parameters.add(selection.timeRange.from, "DateTime64(3, 'UTC')");
6818
6838
  const to = parameters.add(selection.timeRange.to, "DateTime64(3, 'UTC')");
6839
+ const tenant = compileTenantScope(scope, parameters);
6819
6840
  const ctes = [`current_roots AS (
6820
6841
  SELECT * FROM (
6821
6842
  SELECT *
@@ -6829,7 +6850,7 @@ function compileClickHouseTraceScope(selection, relationCollections, parameters)
6829
6850
  SELECT *
6830
6851
  FROM current_roots
6831
6852
  WHERE startedAt >= ${from}
6832
- AND startedAt < ${to}
6853
+ AND startedAt < ${to}${tenant}
6833
6854
  )`];
6834
6855
  if (relationCollections.has("spans")) ctes.push(`current_spans AS (
6835
6856
  SELECT
@@ -6851,7 +6872,7 @@ function compileClickHouseTraceScope(selection, relationCollections, parameters)
6851
6872
  rootEntityVersionId
6852
6873
  FROM ${TABLE_SPAN_EVENTS}
6853
6874
  WHERE isNotNull(traceId)
6854
- AND traceId IN (SELECT traceId FROM root_scope)
6875
+ AND traceId IN (SELECT traceId FROM root_scope)${tenant}
6855
6876
  ORDER BY dedupeKey
6856
6877
  LIMIT 1 BY dedupeKey
6857
6878
  )`);
@@ -6869,7 +6890,7 @@ function compileClickHouseTraceScope(selection, relationCollections, parameters)
6869
6890
  rootEntityVersionId
6870
6891
  FROM ${currentScoresRelation()} AS current
6871
6892
  WHERE isNotNull(current.traceId)
6872
- AND current.traceId IN (SELECT traceId FROM root_scope)
6893
+ AND current.traceId IN (SELECT traceId FROM root_scope)${tenant}
6873
6894
  )`);
6874
6895
  if (relationCollections.has("feedback")) ctes.push(`current_feedback AS (
6875
6896
  SELECT
@@ -6892,7 +6913,7 @@ function compileClickHouseTraceScope(selection, relationCollections, parameters)
6892
6913
  LIMIT 1 BY feedbackId
6893
6914
  ) AS current
6894
6915
  WHERE isNotNull(traceId)
6895
- AND traceId IN (SELECT traceId FROM root_scope)
6916
+ AND traceId IN (SELECT traceId FROM root_scope)${tenant}
6896
6917
  )`);
6897
6918
  return ctes;
6898
6919
  }
@@ -6909,7 +6930,7 @@ function parseDeltaWatermark(value) {
6909
6930
  }
6910
6931
  function compileClickHouseTraceQuery(plan, deltaHead) {
6911
6932
  const parameters = new ParameterBuilder();
6912
- const ctes = compileClickHouseTraceScope(plan, collectRelationCollections(plan.where), parameters);
6933
+ const ctes = compileClickHouseTraceScope(plan, collectRelationCollections(plan.where), parameters, plan.scope);
6913
6934
  const predicate = plan.where ? compilePredicate(plan.where, parameters) : "1";
6914
6935
  ctes.push(`candidates AS (
6915
6936
  SELECT ${TRACE_SELECT}
@@ -7012,7 +7033,7 @@ function compileClickHouseThreadQuery(plan) {
7012
7033
  const parameters = new ParameterBuilder();
7013
7034
  const relationCollections = collectRelationCollections(plan.traces.where);
7014
7035
  collectThreadRelationCollections(plan.where, relationCollections);
7015
- const ctes = compileClickHouseTraceScope(plan.traces, relationCollections, parameters);
7036
+ const ctes = compileClickHouseTraceScope(plan.traces, relationCollections, parameters, plan.scope);
7016
7037
  const eligibility = plan.traces.where ? compilePredicate(plan.traces.where, parameters) : "1";
7017
7038
  ctes.push(`eligible_roots AS (
7018
7039
  SELECT *
@@ -7060,7 +7081,7 @@ function discoveryCollections(scope) {
7060
7081
  }
7061
7082
  function compileClickHouseTraceQueryObservedFields(plan) {
7062
7083
  const parameters = new ParameterBuilder();
7063
- const ctes = compileClickHouseTraceScope(plan, /* @__PURE__ */ new Set(), parameters);
7084
+ const ctes = compileClickHouseTraceScope(plan, /* @__PURE__ */ new Set(), parameters, plan.scope);
7064
7085
  const search = plan.search ? `AND positionCaseInsensitiveUTF8(concat('metadata.', key), ${parameters.add(plan.search, "String")}) > 0` : "";
7065
7086
  const limit = parameters.add(plan.limit + 1, "UInt64");
7066
7087
  ctes.push(`metadata_entries AS (
@@ -7090,12 +7111,13 @@ LIMIT ${limit}`,
7090
7111
  }
7091
7112
  function compileClickHouseTraceQueryValues(plan) {
7092
7113
  const parameters = new ParameterBuilder();
7093
- const ctes = compileClickHouseTraceScope(plan, discoveryCollections(plan.predicateScope), parameters);
7114
+ const ctes = compileClickHouseTraceScope(plan, discoveryCollections(plan.predicateScope), parameters, plan.scope);
7094
7115
  let field;
7095
7116
  if (plan.predicateScope === "trace" && plan.path.startsWith("metadata.")) {
7096
7117
  const key = parameters.add(plan.path.slice(9), "String");
7097
7118
  field = `coalesce(if(mapContains(r.metadataSearch, ${key}), r.metadataSearch[${key}], NULL), nullIf(trim(JSONExtractString(r.metadataRaw, ${key})), ''))`;
7098
- } else field = fieldDefinition(discoveryRegistry(plan.predicateScope), plan.path).sql;
7119
+ } else if (plan.predicateScope === "trace" && plan.path === "tags") field = `arrayJoin(arrayDistinct(${TRACE_FIELDS.tags.sql}))`;
7120
+ else field = fieldDefinition(discoveryRegistry(plan.predicateScope), plan.path).sql;
7099
7121
  const search = plan.search ? `AND positionCaseInsensitiveUTF8(value, ${parameters.add(plan.search, "String")}) > 0` : "";
7100
7122
  const limit = parameters.add(plan.limit + 1, "UInt64");
7101
7123
  return {
@@ -8545,7 +8567,8 @@ var ObservabilityStorageClickhouseVNext = class extends _mastra_core_storage.Obs
8545
8567
  "logs",
8546
8568
  "trace-query",
8547
8569
  "trace-query-discovery",
8548
- "thread-query"
8570
+ "thread-query",
8571
+ "trace-query-tenant-scope"
8549
8572
  ];
8550
8573
  return [
8551
8574
  "metrics",
@@ -8553,7 +8576,8 @@ var ObservabilityStorageClickhouseVNext = class extends _mastra_core_storage.Obs
8553
8576
  "delta-polling",
8554
8577
  "trace-query",
8555
8578
  "trace-query-discovery",
8556
- "thread-query"
8579
+ "thread-query",
8580
+ "trace-query-tenant-scope"
8557
8581
  ];
8558
8582
  }
8559
8583
  async createSpan(args) {