@mastra/clickhouse 1.16.1-alpha.0 → 1.16.1-alpha.2

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/README.md CHANGED
@@ -8,13 +8,6 @@ Clickhouse implementation for Mastra, providing efficient storage capabilities w
8
8
  npm install @mastra/clickhouse
9
9
  ```
10
10
 
11
- ## Prerequisites
12
-
13
- - Clickhouse server (version 23.3 or higher required for delete operations; earlier versions may work for read/write operations)
14
- - Lightweight `DELETE FROM` requires ClickHouse 22.8+ with `allow_experimental_lightweight_delete = 1` (for 22.8–23.2), or 23.3+ where it is generally available.
15
- - The `deleteTask`, `deleteTasks`, and `deleteMessages` methods use `DELETE FROM` — ensure your server supports lightweight delete before using those operations.
16
- - Node.js 22.13.0 or later
17
-
18
11
  ## Usage
19
12
 
20
13
  ```typescript
@@ -59,120 +52,15 @@ const { messages } = await store.listMessages({ threadId: 'thread-123' });
59
52
  await store.close();
60
53
  ```
61
54
 
62
- ## Configuration
63
-
64
- The Clickhouse store can be initialized with the following configuration:
65
-
66
- ```typescript
67
- type ClickhouseConfig = {
68
- url: string; // Clickhouse HTTP interface URL
69
- username: string; // Database username
70
- password: string; // Database password
71
- replication?: {
72
- cluster?: string; // Adds ON CLUSTER to Mastra-owned DDL when set
73
- zookeeperPath?: string; // Defaults to '/clickhouse/tables/{shard}/{database}/{table}'
74
- replicaName?: string; // Defaults to '{replica}'
75
- };
76
- };
77
- ```
78
-
79
- ### Replicated ClickHouse clusters
80
-
81
- Set `replication` when Mastra writes to a multi-replica ClickHouse cluster through a load balancer. Mastra will create its tables with replicated MergeTree engines and add `ON CLUSTER` to Mastra-owned DDL when `cluster` is provided.
82
-
83
- ```typescript
84
- const store = new ClickhouseStore({
85
- url: 'http://clickhouse-lb:8123',
86
- username: 'default',
87
- password: 'password',
88
- replication: {
89
- cluster: 'company_cluster',
90
- },
91
- });
92
- ```
93
-
94
- The default `zookeeperPath` is `/clickhouse/tables/{shard}/{database}/{table}`. If your cluster's existing tables use a different layout (for example `/clickhouse/tables/{shard}/{table}` without the `{database}` segment), set `zookeeperPath` explicitly to match. Mastra does not infer your cluster's convention from Keeper.
95
-
96
- Manual maintenance such as `optimizeTable()` and `materializeTtl()` runs on every replica when `cluster` is set. These operations can be expensive on a large cluster. Prefer running them outside peak hours.
97
-
98
- If Mastra finds an existing local `MergeTree` or `ReplacingMergeTree` table while replication is enabled, initialization fails instead of silently mixing local and replicated tables. Migrate existing local tables manually before enabling this option.
99
-
100
- ## Features
101
-
102
- ### Storage Features
103
-
104
- - Thread and message storage with JSON support
105
- - Efficient batch operations
106
- - Rich metadata support
107
- - Timestamp tracking
108
- - Workflow snapshot persistence
109
- - Optimized for high-volume data ingestion
110
- - Uses Clickhouse's MergeTree and ReplacingMergeTree engines for optimal performance
111
-
112
- ### Table Engines
113
-
114
- The store uses different table engines for different types of data:
115
-
116
- - `MergeTree()`: Used for messages, traces, and evals
117
- - `ReplacingMergeTree()`: Used for threads and workflow snapshots
118
- - `ReplicatedMergeTree(...)` / `ReplicatedReplacingMergeTree(...)`: Used instead when `replication` is enabled
119
-
120
- ## Storage Methods
121
-
122
- ### Thread Operations
123
-
124
- - `saveThread({ thread })`: Create or update a thread
125
- - `getThreadById({ threadId })`: Get a thread by ID
126
- - `listThreadsByResourceId({ resourceId, offset, limit, orderBy? })`: List paginated threads for a resource
127
- - `updateThread({ id, title, metadata })`: Update thread title and metadata
128
- - `deleteThread({ threadId })`: Delete a thread and its messages
129
-
130
- ### Message Operations
131
-
132
- - `saveMessages({ messages })`: Save multiple messages
133
- - `listMessages({ threadId, perPage?, page? })`: Get messages for a thread with pagination
134
- - `updateMessages({ messages })`: Update existing messages
135
-
136
- ### Resource Operations
137
-
138
- - `getResourceById({ resourceId })`: Get a resource by ID
139
- - `saveResource({ resource })`: Create or save a resource
140
- - `updateResource({ resourceId, workingMemory })`: Update resource working memory
141
-
142
- ### Workflow Operations
143
-
144
- - `persistWorkflowSnapshot({ workflowName, runId, snapshot })`: Save workflow state
145
- - `loadWorkflowSnapshot({ workflowName, runId })`: Load workflow state
146
- - `listWorkflowRuns({ workflowName, pagination })`: List workflow runs with pagination
147
- - `getWorkflowRunById({ workflowName, runId })`: Get a specific workflow run
148
-
149
- ### Evaluation/Scoring Operations
150
-
151
- - `getScoreById({ id })`: Get a score by ID
152
- - `saveScore(score)`: Save an evaluation score
153
- - `listScoresByScorerId({ scorerId, pagination })`: List scores by scorer with pagination
154
- - `listScoresByRunId({ runId, pagination })`: List scores by run with pagination
155
- - `listScoresByEntityId({ entityId, entityType, pagination })`: List scores by entity with pagination
156
- - `listScoresBySpan({ traceId, spanId, pagination })`: List scores by span with pagination
157
-
158
- ### Operations Not Currently Supported
159
-
160
- - AI Observability (traces/spans): Not currently supported
55
+ ## Documentation
161
56
 
162
- ## Data Types
57
+ - [ClickHouse integration guide](https://mastra.ai/integrations/databases/clickhouse)
58
+ - [Storage reference](https://mastra.ai/reference/storage/overview)
163
59
 
164
- The store supports the following data types:
60
+ ## Changelog
165
61
 
166
- - `text`: String
167
- - `timestamp`: DateTime64(3)
168
- - `uuid`: String
169
- - `jsonb`: String (JSON serialized)
170
- - `integer`: Int64
171
- - `bigint`: Int64
172
- - `float`: Float64
173
- - `boolean`: Bool
62
+ See the [package changelog](https://github.com/mastra-ai/mastra/blob/main/stores/clickhouse/CHANGELOG.md) for version history and release notes.
174
63
 
175
- ## Related Links
64
+ ## Support
176
65
 
177
- - [Clickhouse Documentation](https://clickhouse.com/docs)
178
- - [Clickhouse Node.js Client](https://github.com/clickhouse/clickhouse-js)
66
+ We have an [open community Discord](https://discord.gg/mastra-ai). Come and say hello and let us know if you have any questions or need any help getting things running.
@@ -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.16.1-alpha.0"
6
+ version: "1.16.1-alpha.2"
7
7
  ---
8
8
 
9
9
  ## When to use
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.16.1-alpha.0",
2
+ "version": "1.16.1-alpha.2",
3
3
  "package": "@mastra/clickhouse",
4
4
  "exports": {},
5
5
  "modules": {}
package/dist/index.cjs CHANGED
@@ -3173,6 +3173,9 @@ CREATE TABLE IF NOT EXISTS ${TABLE_FEEDBACK_EVENTS} (
3173
3173
  feedbackUserId Nullable(String),
3174
3174
  sourceId Nullable(String),
3175
3175
 
3176
+ -- Review workflow
3177
+ reviewStatus LowCardinality(String) DEFAULT 'needs-review',
3178
+
3176
3179
  -- Feedback identity
3177
3180
  feedbackSource LowCardinality(String),
3178
3181
  feedbackType LowCardinality(String),
@@ -3406,6 +3409,7 @@ const ALL_MIGRATIONS = [
3406
3409
  addColumn(TABLE_SCORE_EVENTS, "parentEntityVersionId", "Nullable(String)"),
3407
3410
  addColumn(TABLE_SCORE_EVENTS, "rootEntityVersionId", "Nullable(String)"),
3408
3411
  addColumn(TABLE_FEEDBACK_EVENTS, "entityVersionId", "Nullable(String)"),
3412
+ addColumn(TABLE_FEEDBACK_EVENTS, "reviewStatus", "LowCardinality(String) DEFAULT 'needs-review'"),
3409
3413
  addColumn(TABLE_FEEDBACK_EVENTS, "parentEntityVersionId", "Nullable(String)"),
3410
3414
  addColumn(TABLE_FEEDBACK_EVENTS, "rootEntityVersionId", "Nullable(String)"),
3411
3415
  addBloomIndex(TABLE_METRIC_EVENTS, "idx_traceId", "traceId"),
@@ -3498,6 +3502,31 @@ function parseTtlExpression(expr) {
3498
3502
  };
3499
3503
  }
3500
3504
  //#endregion
3505
+ //#region src/storage/domains/observability/v-next/review-status.ts
3506
+ const FEEDBACK_REVIEW_STATUSES = ["needs-review", "reviewed"];
3507
+ function isFeedbackReviewStatus(value) {
3508
+ return FEEDBACK_REVIEW_STATUSES.some((status) => status === value);
3509
+ }
3510
+ /** Normalize a stored value to a review status, defaulting legacy/unknown values to `needs-review`. */
3511
+ function coerceFeedbackReviewStatus(value) {
3512
+ return isFeedbackReviewStatus(value) ? value : "needs-review";
3513
+ }
3514
+ function parseUpdateFeedbackReviewStatusArgs(args) {
3515
+ const invalid = (text) => new _mastra_core_error.MastraError({
3516
+ id: "OBSERVABILITY_UPDATE_FEEDBACK_REVIEW_STATUS_INVALID_ARGS",
3517
+ domain: _mastra_core_error.ErrorDomain.STORAGE,
3518
+ category: _mastra_core_error.ErrorCategory.USER,
3519
+ text
3520
+ });
3521
+ if (typeof args !== "object" || args === null) throw invalid("args must be an object");
3522
+ if (typeof args.feedbackId !== "string" || args.feedbackId.length === 0) throw invalid("feedbackId is required");
3523
+ if (!isFeedbackReviewStatus(args.reviewStatus)) throw invalid(`reviewStatus must be one of: ${FEEDBACK_REVIEW_STATUSES.join(", ")}`);
3524
+ return {
3525
+ feedbackId: args.feedbackId,
3526
+ reviewStatus: args.reviewStatus
3527
+ };
3528
+ }
3529
+ //#endregion
3501
3530
  //#region src/storage/domains/observability/v-next/helpers.ts
3502
3531
  const CH_SETTINGS = {
3503
3532
  date_time_input_format: "best_effort",
@@ -4004,6 +4033,7 @@ function rowToFeedbackRecord(row) {
4004
4033
  serviceName: nullableString(row.serviceName),
4005
4034
  feedbackUserId,
4006
4035
  sourceId: nullableString(row.sourceId),
4036
+ reviewStatus: coerceFeedbackReviewStatus(row.reviewStatus),
4007
4037
  feedbackSource,
4008
4038
  feedbackType: row.feedbackType,
4009
4039
  value: hasNumber ? Number(row.valueNumber) : nullableString(row.valueString) ?? "",
@@ -4047,6 +4077,7 @@ function feedbackRecordToRow(feedback) {
4047
4077
  serviceName: feedback.serviceName ?? null,
4048
4078
  feedbackUserId,
4049
4079
  sourceId: feedback.sourceId ?? null,
4080
+ reviewStatus: feedback.reviewStatus ?? "needs-review",
4050
4081
  feedbackSource,
4051
4082
  feedbackType: feedback.feedbackType,
4052
4083
  valueString: typeof feedback.value === "string" ? feedback.value : null,
@@ -4344,6 +4375,7 @@ function buildFeedbackFilterConditions(filters, tableAlias) {
4344
4375
  const fbActor = filters.feedbackUserId ?? filters.userId;
4345
4376
  addEq(col("feedbackUserId"), fbActor, "feedbackUserId", "String", out);
4346
4377
  addEq(col("feedbackSource"), filters.feedbackSource, "feedbackSource", "String", out);
4378
+ addEq(col("reviewStatus"), filters.reviewStatus, "reviewStatus", "String", out);
4347
4379
  if (typeof filters.feedbackType === "string") addEq(col("feedbackType"), filters.feedbackType, "feedbackType", "String", out);
4348
4380
  else if (Array.isArray(filters.feedbackType)) addIn(col("feedbackType"), filters.feedbackType, "feedbackTypes", out);
4349
4381
  return out;
@@ -4521,6 +4553,28 @@ async function batchCreateFeedback(client, args) {
4521
4553
  clickhouse_settings: CH_INSERT_SETTINGS
4522
4554
  });
4523
4555
  }
4556
+ async function updateFeedbackReviewStatus(client, args) {
4557
+ const { feedbackId, reviewStatus } = parseUpdateFeedbackReviewStatusArgs(args);
4558
+ const existing = await queryJson$2(client, `SELECT * FROM ${TABLE_FEEDBACK_EVENTS} FINAL WHERE feedbackId = {feedbackId:String} LIMIT 1`, { feedbackId });
4559
+ if (!existing[0]) throw new _mastra_core_error.MastraError({
4560
+ id: "OBSERVABILITY_UPDATE_FEEDBACK_REVIEW_STATUS_NOT_FOUND",
4561
+ domain: _mastra_core_error.ErrorDomain.MASTRA_OBSERVABILITY,
4562
+ category: _mastra_core_error.ErrorCategory.USER,
4563
+ text: "Feedback record not found",
4564
+ details: { feedbackId }
4565
+ });
4566
+ const updated = rowToFeedbackRecord({
4567
+ ...existing[0],
4568
+ reviewStatus
4569
+ });
4570
+ await client.insert({
4571
+ table: TABLE_FEEDBACK_EVENTS,
4572
+ values: [feedbackRecordToRow(updated)],
4573
+ format: "JSONEachRow",
4574
+ clickhouse_settings: CH_INSERT_SETTINGS
4575
+ });
4576
+ return updated;
4577
+ }
4524
4578
  async function listFeedback(client, args, strategy) {
4525
4579
  const parsed = _mastra_core_storage.listFeedbackArgsSchema.parse(args);
4526
4580
  const deltaCursorEnabled = deltaPollingSupported(strategy);
@@ -4552,8 +4606,8 @@ async function listFeedback(client, args, strategy) {
4552
4606
  };
4553
4607
  }
4554
4608
  const currentDeltaCursor = deltaCursorEnabled ? await getDeltaCursor$5(client, whereClause, filter.params) : void 0;
4555
- const countResult = await queryJson$2(client, `SELECT count() AS total FROM ${TABLE_FEEDBACK_EVENTS} AS f ${whereClause}`, filter.params);
4556
- const rows = await queryJson$2(client, `SELECT * FROM ${TABLE_FEEDBACK_EVENTS} AS f ${whereClause} ORDER BY ${orderBy} LIMIT {limit:UInt32} OFFSET {offset:UInt32}`, {
4609
+ const countResult = await queryJson$2(client, `SELECT count() AS total FROM ${TABLE_FEEDBACK_EVENTS} AS f FINAL ${whereClause}`, filter.params);
4610
+ const rows = await queryJson$2(client, `SELECT * FROM ${TABLE_FEEDBACK_EVENTS} AS f FINAL ${whereClause} ORDER BY ${orderBy} LIMIT {limit:UInt32} OFFSET {offset:UInt32}`, {
4557
4611
  ...filter.params,
4558
4612
  limit: pagination.limit,
4559
4613
  offset: pagination.offset
@@ -4579,7 +4633,7 @@ async function queryFeedbackAfterCursor(client, whereClause, params, limit, curs
4579
4633
  f.feedbackId AS feedbackId,
4580
4634
  toString(d.cursorId) AS cursorId
4581
4635
  FROM ${TABLE_FEEDBACK_EVENTS_DELTA} d
4582
- INNER JOIN ${TABLE_FEEDBACK_EVENTS} f
4636
+ INNER JOIN ${TABLE_FEEDBACK_EVENTS} f FINAL
4583
4637
  ON ((f.traceId = d.traceId) OR (f.traceId IS NULL AND d.traceId IS NULL))
4584
4638
  AND f.timestamp = d.timestamp
4585
4639
  AND f.feedbackId = d.feedbackId
@@ -4596,7 +4650,7 @@ async function getDeltaCursor$5(client, whereClause, params) {
4596
4650
  const cursorId = (await queryJson$2(client, `
4597
4651
  SELECT toString(max(d.cursorId)) AS cursorId
4598
4652
  FROM mastra_feedback_events_delta d
4599
- INNER JOIN mastra_feedback_events f
4653
+ INNER JOIN mastra_feedback_events f FINAL
4600
4654
  ON ((f.traceId = d.traceId) OR (f.traceId IS NULL AND d.traceId IS NULL))
4601
4655
  AND f.timestamp = d.timestamp
4602
4656
  AND f.feedbackId = d.feedbackId
@@ -4615,7 +4669,7 @@ async function getFeedbackAggregate(client, args) {
4615
4669
  const aggSql = getAggregationSql$2(args.aggregation);
4616
4670
  const identity = buildFeedbackIdentityFilter(args);
4617
4671
  const combined = mergeFilters$2(identity, buildFeedbackFilterConditions(args.filters));
4618
- const result = await queryJson$2(client, `SELECT ${aggSql} AS value FROM ${TABLE_FEEDBACK_EVENTS} ${toWhereClause$2(combined)}`, combined.params);
4672
+ const result = await queryJson$2(client, `SELECT ${aggSql} AS value FROM ${TABLE_FEEDBACK_EVENTS} FINAL ${toWhereClause$2(combined)}`, combined.params);
4619
4673
  const value = result[0]?.value == null ? null : Number(result[0]?.value);
4620
4674
  if (args.comparePeriod && args.filters?.timestamp) {
4621
4675
  const ts = args.filters.timestamp;
@@ -4649,7 +4703,7 @@ async function getFeedbackAggregate(client, args) {
4649
4703
  endExclusive: ts.endExclusive
4650
4704
  }
4651
4705
  }));
4652
- const prevResult = await queryJson$2(client, `SELECT ${aggSql} AS value FROM ${TABLE_FEEDBACK_EVENTS} ${toWhereClause$2(prevCombined)}`, prevCombined.params);
4706
+ const prevResult = await queryJson$2(client, `SELECT ${aggSql} AS value FROM ${TABLE_FEEDBACK_EVENTS} FINAL ${toWhereClause$2(prevCombined)}`, prevCombined.params);
4653
4707
  const previousValue = prevResult[0]?.value == null ? null : Number(prevResult[0]?.value);
4654
4708
  let changePercent = null;
4655
4709
  if (previousValue !== null && previousValue !== 0 && value !== null) changePercent = (value - previousValue) / Math.abs(previousValue) * 100;
@@ -4667,7 +4721,7 @@ async function getFeedbackBreakdown(client, args) {
4667
4721
  const combined = mergeFilters$2(buildFeedbackIdentityFilter(args), buildFeedbackFilterConditions(args.filters));
4668
4722
  const whereClause = toWhereClause$2(combined);
4669
4723
  const resolved = resolveFeedbackGroupBy(args.groupBy);
4670
- return { groups: (await queryJson$2(client, `SELECT ${resolved.map((e) => e.selectSql).join(", ")}, ${aggSql} AS value FROM ${TABLE_FEEDBACK_EVENTS} ${whereClause} GROUP BY ${resolved.map((e) => e.groupSql).join(", ")} ORDER BY value DESC`, combined.params)).map((row) => ({
4724
+ return { groups: (await queryJson$2(client, `SELECT ${resolved.map((e) => e.selectSql).join(", ")}, ${aggSql} AS value FROM ${TABLE_FEEDBACK_EVENTS} FINAL ${whereClause} GROUP BY ${resolved.map((e) => e.groupSql).join(", ")} ORDER BY value DESC`, combined.params)).map((row) => ({
4671
4725
  dimensions: Object.fromEntries(resolved.map((entry, index) => {
4672
4726
  const v = row[`group_by_${index}`];
4673
4727
  return [entry.key, v == null ? null : String(v)];
@@ -4686,7 +4740,7 @@ async function getFeedbackTimeSeries(client, args) {
4686
4740
  SELECT toStartOfInterval(timestamp, ${intervalSql}) AS bucket,
4687
4741
  ${resolved.map((e) => e.selectSql).join(", ")},
4688
4742
  ${aggSql} AS value
4689
- FROM ${TABLE_FEEDBACK_EVENTS} ${whereClause}
4743
+ FROM ${TABLE_FEEDBACK_EVENTS} FINAL ${whereClause}
4690
4744
  GROUP BY bucket, ${resolved.map((e) => e.groupSql).join(", ")}
4691
4745
  ORDER BY bucket
4692
4746
  `, combined.params);
@@ -4708,7 +4762,7 @@ async function getFeedbackTimeSeries(client, args) {
4708
4762
  const rows = await queryJson$2(client, `
4709
4763
  SELECT toStartOfInterval(timestamp, ${intervalSql}) AS bucket,
4710
4764
  ${aggSql} AS value
4711
- FROM ${TABLE_FEEDBACK_EVENTS} ${whereClause}
4765
+ FROM ${TABLE_FEEDBACK_EVENTS} FINAL ${whereClause}
4712
4766
  GROUP BY bucket
4713
4767
  ORDER BY bucket
4714
4768
  `, combined.params);
@@ -4731,7 +4785,7 @@ async function getFeedbackPercentiles(client, args) {
4731
4785
  const rows = await queryJson$2(client, `
4732
4786
  SELECT toStartOfInterval(timestamp, ${intervalSql}) AS bucket,
4733
4787
  quantile(${p})(valueNumber) AS pvalue
4734
- FROM ${TABLE_FEEDBACK_EVENTS}
4788
+ FROM ${TABLE_FEEDBACK_EVENTS} FINAL
4735
4789
  ${whereClause}
4736
4790
  GROUP BY bucket
4737
4791
  ORDER BY bucket
@@ -7359,6 +7413,19 @@ var ObservabilityStorageClickhouseVNext = class extends _mastra_core_storage.Obs
7359
7413
  }, error);
7360
7414
  }
7361
7415
  }
7416
+ async updateFeedbackReviewStatus(args) {
7417
+ try {
7418
+ return await updateFeedbackReviewStatus(this.#client, args);
7419
+ } catch (error) {
7420
+ if (error instanceof _mastra_core_error.MastraError) throw error;
7421
+ throw new _mastra_core_error.MastraError({
7422
+ id: (0, _mastra_core_storage.createStorageErrorId)("CLICKHOUSE", "UPDATE_FEEDBACK_REVIEW_STATUS", "FAILED"),
7423
+ domain: _mastra_core_error.ErrorDomain.STORAGE,
7424
+ category: _mastra_core_error.ErrorCategory.THIRD_PARTY,
7425
+ details: { feedbackId: args.feedbackId }
7426
+ }, error);
7427
+ }
7428
+ }
7362
7429
  async listFeedback(args) {
7363
7430
  try {
7364
7431
  return await listFeedback(this.#client, args, this.#deltaCursorStrategy);