@mastra/clickhouse 1.17.0-alpha.0 → 1.17.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.17.0-alpha.0"
6
+ version: "1.17.0-alpha.1"
7
7
  ---
8
8
 
9
9
  ## When to use
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.17.0-alpha.0",
2
+ "version": "1.17.0-alpha.1",
3
3
  "package": "@mastra/clickhouse",
4
4
  "exports": {},
5
5
  "modules": {}
@@ -87,6 +87,12 @@ export const mastra = new Mastra({
87
87
 
88
88
  `MastraStorageExporter` automatically selects the `insert-only` strategy when ClickHouse is the observability backend, which gives the highest write throughput. See [tracing strategies](https://mastra.ai/docs/observability/integrations/exporters/mastra-storage) for details.
89
89
 
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
+
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.
93
+
94
+ Deletion requests aren't purged automatically in Mastra OSS. Automatic retirement will be introduced with future database-agnostic retention configuration.
95
+
90
96
  ### Observability with the legacy domain
91
97
 
92
98
  `ObservabilityStorageClickhouse` is the original observability adapter and remains supported for projects that haven't migrated to the vNext schema. The configuration shape is the same as the vNext class.
package/dist/index.cjs CHANGED
@@ -29,6 +29,7 @@ let _mastra_core_agent = require("@mastra/core/agent");
29
29
  let _mastra_core_base = require("@mastra/core/base");
30
30
  let _mastra_core_utils = require("@mastra/core/utils");
31
31
  let _mastra_core_features = require("@mastra/core/features");
32
+ let crypto$1 = require("crypto");
32
33
  let _mastra_core_evals = require("@mastra/core/evals");
33
34
  //#region src/storage/db/replication.ts
34
35
  const DEFAULT_ZOOKEEPER_PATH = "/clickhouse/tables/{shard}/{database}/{table}";
@@ -94,12 +95,22 @@ function getEngineNameAndArgs(engine) {
94
95
  }
95
96
  function hasBalancedParens(s) {
96
97
  let depth = 0;
97
- for (const c of s) if (c === "(") depth++;
98
- else if (c === ")") {
99
- depth--;
100
- if (depth < 0) return false;
98
+ let quote;
99
+ for (let i = 0; i < s.length; i++) {
100
+ const c = s[i];
101
+ if (quote) {
102
+ if (c === "\\") i++;
103
+ else if (c === quote) quote = void 0;
104
+ continue;
105
+ }
106
+ if (c === "`" || c === "\"" || c === "'") quote = c;
107
+ else if (c === "(") depth++;
108
+ else if (c === ")") {
109
+ depth--;
110
+ if (depth < 0) return false;
111
+ }
101
112
  }
102
- return depth === 0;
113
+ return depth === 0 && quote === void 0;
103
114
  }
104
115
  function buildReplicatedTableEngine(engine, replication) {
105
116
  if (!replication) return engine;
@@ -171,20 +182,51 @@ function addOnClusterToDDL(sql, replication) {
171
182
  return out;
172
183
  }
173
184
  function rewriteEngineClauses(sql, replication) {
174
- return sql.replace(/ENGINE\s*=\s*(\w+)\s*/gi, (match, engineName, offset, source) => {
175
- const argsStart = offset + match.length;
176
- if (source[argsStart] !== "(") return `ENGINE = ${buildReplicatedTableEngine(engineName, replication)}`;
177
- let depth = 0;
178
- for (let i = argsStart; i < source.length; i++) {
179
- const char = source[i];
180
- if (char === "(") depth++;
181
- else if (char === ")") {
182
- depth--;
183
- if (depth === 0) return `ENGINE = ${buildReplicatedTableEngine(`${engineName}${source.slice(argsStart, i + 1)}`, replication)}`;
185
+ const enginePattern = /ENGINE\s*=\s*(\w+)/gi;
186
+ let result = "";
187
+ let consumedUntil = 0;
188
+ for (let match = enginePattern.exec(sql); match; match = enginePattern.exec(sql)) {
189
+ const engineName = match[1];
190
+ if (!engineName) continue;
191
+ let engineEnd = enginePattern.lastIndex;
192
+ let argsStart = engineEnd;
193
+ while (/\s/.test(sql[argsStart] ?? "")) argsStart++;
194
+ let engine = engineName;
195
+ if (sql[argsStart] === "(") {
196
+ let depth = 0;
197
+ let closingParenEnd;
198
+ let quote;
199
+ for (let i = argsStart; i < sql.length; i++) {
200
+ const char = sql[i];
201
+ if (quote) {
202
+ if (char === "\\") i++;
203
+ else if (char === quote) quote = void 0;
204
+ continue;
205
+ }
206
+ if (char === "`" || char === "\"" || char === "'") quote = char;
207
+ else if (char === "(") depth++;
208
+ else if (char === ")") {
209
+ depth--;
210
+ if (depth === 0) {
211
+ closingParenEnd = i + 1;
212
+ break;
213
+ }
214
+ }
184
215
  }
216
+ if (closingParenEnd === void 0) {
217
+ result += sql.slice(consumedUntil, enginePattern.lastIndex);
218
+ consumedUntil = enginePattern.lastIndex;
219
+ continue;
220
+ }
221
+ engineEnd = closingParenEnd;
222
+ engine = `${engineName}${sql.slice(argsStart, engineEnd)}`;
185
223
  }
186
- return match;
187
- });
224
+ result += sql.slice(consumedUntil, match.index);
225
+ result += `ENGINE = ${buildReplicatedTableEngine(engine, replication)}`;
226
+ consumedUntil = engineEnd;
227
+ enginePattern.lastIndex = engineEnd;
228
+ }
229
+ return result + sql.slice(consumedUntil);
188
230
  }
189
231
  function applyReplicationToDDL(sql, replication) {
190
232
  return addOnClusterToDDL(replication ? rewriteEngineClauses(sql, replication) : sql, replication);
@@ -2501,6 +2543,7 @@ var ObservabilityStorageClickhouse = class extends _mastra_core_storage.Observab
2501
2543
  }
2502
2544
  }
2503
2545
  async batchDeleteTraces(args) {
2546
+ this.assertUnscopedBatchDeleteTraces(args);
2504
2547
  try {
2505
2548
  if (args.traceIds.length === 0) return;
2506
2549
  await this.client.command({
@@ -2527,6 +2570,7 @@ const TABLE_METRIC_EVENTS = "mastra_metric_events";
2527
2570
  const TABLE_LOG_EVENTS = "mastra_log_events";
2528
2571
  const TABLE_SCORE_EVENTS = "mastra_score_events";
2529
2572
  const TABLE_FEEDBACK_EVENTS = "mastra_feedback_events";
2573
+ const TABLE_DELETION_REQUESTS = "mastra_deletion_requests";
2530
2574
  const TABLE_METRIC_EVENTS_DELTA = "mastra_metric_events_delta";
2531
2575
  const TABLE_LOG_EVENTS_DELTA = "mastra_log_events_delta";
2532
2576
  const TABLE_SCORE_EVENTS_DELTA = "mastra_score_events_delta";
@@ -3222,6 +3266,23 @@ PARTITION BY toDate(timestamp)
3222
3266
  ORDER BY (traceId, timestamp, feedbackId)
3223
3267
  SETTINGS allow_nullable_key = 1
3224
3268
  `;
3269
+ const DELETION_REQUESTS_DDL = `
3270
+ CREATE TABLE IF NOT EXISTS ${TABLE_DELETION_REQUESTS} (
3271
+ requestId String,
3272
+ organizationId String DEFAULT '',
3273
+ resourceId String DEFAULT '',
3274
+ signal LowCardinality(String),
3275
+ predicateType LowCardinality(String),
3276
+ predicateValues Array(String),
3277
+ requestedAt DateTime64(3),
3278
+ requestedBy String DEFAULT '',
3279
+ lastAppliedAt DateTime64(3) DEFAULT 0,
3280
+ purgeVerifiedAt DateTime64(3) DEFAULT 0,
3281
+ updatedAt DateTime64(3)
3282
+ )
3283
+ ENGINE = ReplacingMergeTree(updatedAt)
3284
+ ORDER BY (organizationId, resourceId, requestId)
3285
+ `;
3225
3286
  function buildFeedbackEventsDeltaDDL() {
3226
3287
  return `
3227
3288
  CREATE TABLE IF NOT EXISTS ${TABLE_FEEDBACK_EVENTS_DELTA} (
@@ -3371,6 +3432,7 @@ const BASE_TABLE_DDL = [
3371
3432
  LOG_EVENTS_DDL,
3372
3433
  SCORE_EVENTS_DDL,
3373
3434
  FEEDBACK_EVENTS_DDL,
3435
+ DELETION_REQUESTS_DDL,
3374
3436
  DISCOVERY_VALUES_DDL,
3375
3437
  DISCOVERY_PAIRS_DDL
3376
3438
  ];
@@ -3455,6 +3517,7 @@ const ALL_TABLE_NAMES = [
3455
3517
  TABLE_LOG_EVENTS,
3456
3518
  TABLE_SCORE_EVENTS,
3457
3519
  TABLE_FEEDBACK_EVENTS,
3520
+ TABLE_DELETION_REQUESTS,
3458
3521
  TABLE_METRIC_EVENTS_DELTA,
3459
3522
  TABLE_LOG_EVENTS_DELTA,
3460
3523
  TABLE_SCORE_EVENTS_DELTA,
@@ -4112,6 +4175,35 @@ function feedbackRecordToRow(feedback) {
4112
4175
  };
4113
4176
  }
4114
4177
  //#endregion
4178
+ //#region src/storage/domains/observability/v-next/deletion-requests.ts
4179
+ const EPOCH = "1970-01-01T00:00:00.000Z";
4180
+ async function recordDeletionRequest(client, args) {
4181
+ const row = {
4182
+ requestId: args.requestId,
4183
+ organizationId: args.organizationId ?? "",
4184
+ resourceId: args.resourceId ?? "",
4185
+ signal: args.signal,
4186
+ predicateType: args.predicateType,
4187
+ predicateValues: args.predicateValues,
4188
+ requestedAt: args.requestedAt,
4189
+ requestedBy: args.requestedBy ?? "",
4190
+ lastAppliedAt: EPOCH,
4191
+ purgeVerifiedAt: EPOCH,
4192
+ updatedAt: args.requestedAt
4193
+ };
4194
+ await client.insert({
4195
+ table: TABLE_DELETION_REQUESTS,
4196
+ values: [row],
4197
+ format: "JSONEachRow",
4198
+ clickhouse_settings: isReplicationConfigured(args.replication) ? {
4199
+ ...CH_INSERT_SETTINGS,
4200
+ insert_quorum: "auto",
4201
+ insert_quorum_parallel: 1
4202
+ } : CH_INSERT_SETTINGS
4203
+ });
4204
+ return row;
4205
+ }
4206
+ //#endregion
4115
4207
  //#region src/storage/domains/observability/v-next/discovery.ts
4116
4208
  async function queryJson$3(client, query, params = {}) {
4117
4209
  return await (await client.query({
@@ -6614,6 +6706,17 @@ function buildTraceCursor(row) {
6614
6706
  }
6615
6707
  //#endregion
6616
6708
  //#region src/storage/domains/observability/v-next/tracing.ts
6709
+ /**
6710
+ * Tracing operations for ClickHouse v-next observability.
6711
+ *
6712
+ * Owns: batchCreateSpans, getSpan, getSpans, getTrace, getTraceLight,
6713
+ * listBranches, batchDeleteTraces, dangerouslyClearSpanEvents.
6714
+ * Delegates to trace-roots.ts: listTraces, getRootSpan.
6715
+ *
6716
+ * `listBranches` reads from the MV-fed `mastra_trace_branches` table (one row
6717
+ * per branch anchor span). It lives here -- alongside the other read paths
6718
+ * over the trace data -- since branches are conceptually a subset of traces.
6719
+ */
6617
6720
  const BRANCH_SPAN_TYPE_SQL_LIST = _mastra_core_storage.BRANCH_SPAN_TYPES.map((t) => `'${t}'`).join(", ");
6618
6721
  /** Insert a single completed span. */
6619
6722
  async function createSpan(client, args) {
@@ -6753,16 +6856,40 @@ async function getTraceLight(client, args) {
6753
6856
  };
6754
6857
  }
6755
6858
  /**
6756
- * Delete traces by traceId.
6757
- * Issues lightweight DELETE against both span_events and trace_roots.
6859
+ * Delete traces by traceId, cascading to trace-derived tables and trace-linked
6860
+ * signal events (metrics, logs, scores, feedback). Signal rows with a NULL
6861
+ * traceId are never affected.
6862
+ *
6863
+ * On the tracing tables, rows are targeted by tracing identity: traceId +
6864
+ * dedupeKey (which starts with traceId). The dedupeKey condition is redundant
6865
+ * for correctness (dedupeKey = traceId:spanId) but satisfies the design-doc
6866
+ * requirement that trace deletes reference dedupeKey and helps the engine
6867
+ * narrow within the sorted ORDER BY key. Signal tables key on their own event
6868
+ * ids, so they are targeted by traceId alone.
6869
+ *
6870
+ * `trace_branches` must be deleted explicitly: its MV fires on insert only,
6871
+ * so span deletes never propagate to it. Delta tables self-expire via TTL and
6872
+ * discovery tables self-heal, so neither needs explicit deletes.
6758
6873
  *
6759
- * Targets rows by tracing identity: traceId + dedupeKey (which starts with traceId).
6760
- * The dedupeKey condition is redundant for correctness (dedupeKey = traceId:spanId)
6761
- * but satisfies the design-doc requirement that trace deletes reference dedupeKey
6762
- * and helps the engine narrow within the sorted ORDER BY key.
6874
+ * Records the predicate before using lightweight DELETE FROM on every table.
6875
+ * Lightweight deletes hide rows through ClickHouse's delete mask; physical
6876
+ * removal depends on the deployment's configured retention and merge policy.
6877
+ *
6878
+ * When the optional tenant scope (`organizationId` / `resourceId`) is set,
6879
+ * every DELETE additionally requires the row's tenant columns to match.
6763
6880
  */
6764
- async function batchDeleteTraces(client, args) {
6881
+ async function batchDeleteTraces(client, args, replication) {
6765
6882
  if (args.traceIds.length === 0) return;
6883
+ await recordDeletionRequest(client, {
6884
+ requestId: (0, crypto$1.randomUUID)(),
6885
+ organizationId: args.organizationId,
6886
+ resourceId: args.resourceId,
6887
+ signal: "traces",
6888
+ predicateType: "traceIds",
6889
+ predicateValues: [...args.traceIds],
6890
+ requestedAt: (/* @__PURE__ */ new Date()).toISOString(),
6891
+ replication
6892
+ });
6766
6893
  const params = {};
6767
6894
  const traceInPlaceholders = [];
6768
6895
  const dedupeOrParts = [];
@@ -6776,13 +6903,35 @@ async function batchDeleteTraces(client, args) {
6776
6903
  }
6777
6904
  const traceInList = traceInPlaceholders.join(", ");
6778
6905
  const dedupeCondition = dedupeOrParts.length === 1 ? dedupeOrParts[0] : `(${dedupeOrParts.join(" OR ")})`;
6779
- await Promise.all([client.command({
6780
- query: `DELETE FROM ${TABLE_SPAN_EVENTS} WHERE traceId IN (${traceInList}) AND ${dedupeCondition}`,
6781
- query_params: params
6782
- }), client.command({
6783
- query: `DELETE FROM ${TABLE_TRACE_ROOTS} WHERE traceId IN (${traceInList}) AND ${dedupeCondition}`,
6784
- query_params: params
6785
- })]);
6906
+ let scopeCondition = "";
6907
+ if (args.organizationId !== void 0) {
6908
+ params.scope_org = args.organizationId;
6909
+ scopeCondition += ` AND organizationId = {scope_org:String}`;
6910
+ }
6911
+ if (args.resourceId !== void 0) {
6912
+ params.scope_res = args.resourceId;
6913
+ scopeCondition += ` AND resourceId = {scope_res:String}`;
6914
+ }
6915
+ const tracingTables = [
6916
+ TABLE_SPAN_EVENTS,
6917
+ TABLE_TRACE_ROOTS,
6918
+ TABLE_TRACE_BRANCHES
6919
+ ];
6920
+ const signalTables = [
6921
+ TABLE_METRIC_EVENTS,
6922
+ TABLE_LOG_EVENTS,
6923
+ TABLE_SCORE_EVENTS,
6924
+ TABLE_FEEDBACK_EVENTS
6925
+ ];
6926
+ await Promise.all([...tracingTables.map((table) => client.command({
6927
+ query: `DELETE FROM ${table} WHERE traceId IN (${traceInList}) AND ${dedupeCondition}${scopeCondition}`,
6928
+ query_params: params,
6929
+ clickhouse_settings: { lightweight_deletes_sync: "2" }
6930
+ })), ...signalTables.map((table) => client.command({
6931
+ query: `DELETE FROM ${table} WHERE traceId IN (${traceInList})${scopeCondition}`,
6932
+ query_params: params,
6933
+ clickhouse_settings: { lightweight_deletes_sync: "2" }
6934
+ }))]);
6786
6935
  }
6787
6936
  /**
6788
6937
  * List trace branches with optional filtering, pagination, and ordering.
@@ -7985,7 +8134,7 @@ var ObservabilityStorageClickhouseVNext = class extends _mastra_core_storage.Obs
7985
8134
  }
7986
8135
  async batchDeleteTraces(args) {
7987
8136
  try {
7988
- await batchDeleteTraces(this.#client, args);
8137
+ await batchDeleteTraces(this.#client, args, this.#replication);
7989
8138
  } catch (error) {
7990
8139
  if (error instanceof _mastra_core_error.MastraError) throw error;
7991
8140
  throw new _mastra_core_error.MastraError({
@@ -8862,7 +9011,9 @@ exports.MemoryStorageClickhouse = MemoryStorageClickhouse;
8862
9011
  exports.ObservabilityStorageClickhouse = ObservabilityStorageClickhouse;
8863
9012
  exports.ObservabilityStorageClickhouseVNext = ObservabilityStorageClickhouseVNext;
8864
9013
  exports.ScoresStorageClickhouse = ScoresStorageClickhouse;
9014
+ exports.TABLE_DELETION_REQUESTS = TABLE_DELETION_REQUESTS;
8865
9015
  exports.TABLE_ENGINES = TABLE_ENGINES;
8866
9016
  exports.WorkflowsStorageClickhouse = WorkflowsStorageClickhouse;
9017
+ exports.recordDeletionRequest = recordDeletionRequest;
8867
9018
 
8868
9019
  //# sourceMappingURL=index.cjs.map