@mastra/clickhouse 0.10.1 → 0.10.2-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.
@@ -1,23 +1,23 @@
1
1
 
2
- > @mastra/clickhouse@0.10.1-alpha.2 build /home/runner/work/mastra/mastra/stores/clickhouse
2
+ > @mastra/clickhouse@0.10.2-alpha.1 build /home/runner/work/mastra/mastra/stores/clickhouse
3
3
  > tsup src/index.ts --format esm,cjs --experimental-dts --clean --treeshake=smallest --splitting
4
4
 
5
5
  CLI Building entry: src/index.ts
6
6
  CLI Using tsconfig: tsconfig.json
7
- CLI tsup v8.4.0
7
+ CLI tsup v8.5.0
8
8
  TSC Build start
9
- TSC ⚡️ Build success in 8613ms
9
+ TSC ⚡️ Build success in 8294ms
10
10
  DTS Build start
11
11
  CLI Target: es2022
12
12
  Analysis will use the bundled TypeScript version 5.8.3
13
13
  Writing package typings: /home/runner/work/mastra/mastra/stores/clickhouse/dist/_tsup-dts-rollup.d.ts
14
14
  Analysis will use the bundled TypeScript version 5.8.3
15
15
  Writing package typings: /home/runner/work/mastra/mastra/stores/clickhouse/dist/_tsup-dts-rollup.d.cts
16
- DTS ⚡️ Build success in 11358ms
16
+ DTS ⚡️ Build success in 11634ms
17
17
  CLI Cleaning output folder
18
18
  ESM Build start
19
19
  CJS Build start
20
- ESM dist/index.js 28.23 KB
21
- ESM ⚡️ Build success in 942ms
22
- CJS dist/index.cjs 28.49 KB
23
- CJS ⚡️ Build success in 943ms
20
+ ESM dist/index.js 30.35 KB
21
+ ESM ⚡️ Build success in 1068ms
22
+ CJS dist/index.cjs 30.60 KB
23
+ CJS ⚡️ Build success in 1069ms
package/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # @mastra/clickhouse
2
2
 
3
+ ## 0.10.2-alpha.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 925ab94: added paginated functions to base class and added boilerplate and updated imports
8
+ - Updated dependencies [925ab94]
9
+ - @mastra/core@0.10.4-alpha.3
10
+
11
+ ## 0.10.2-alpha.0
12
+
13
+ ### Patch Changes
14
+
15
+ - dffb67b: updated stores to add alter table and change tests
16
+ - Updated dependencies [f6fd25f]
17
+ - Updated dependencies [dffb67b]
18
+ - Updated dependencies [f1309d3]
19
+ - Updated dependencies [f7f8293]
20
+ - @mastra/core@0.10.4-alpha.1
21
+
3
22
  ## 0.10.1
4
23
 
5
24
  ### Patch Changes
@@ -1,13 +1,16 @@
1
1
  import type { ClickHouseClient } from '@clickhouse/client';
2
2
  import type { EvalRow } from '@mastra/core/storage';
3
3
  import type { MastraMessageV1 } from '@mastra/core/memory';
4
- import type { MastraMessageV2 } from '@mastra/core/agent';
4
+ import type { MastraMessageV2 } from '@mastra/core/memory';
5
5
  import { MastraStorage } from '@mastra/core/storage';
6
+ import type { PaginationInfo } from '@mastra/core/storage';
6
7
  import type { StorageColumn } from '@mastra/core/storage';
7
8
  import type { StorageGetMessagesArg } from '@mastra/core/storage';
9
+ import type { StorageGetTracesArg } from '@mastra/core/storage';
8
10
  import type { StorageThreadType } from '@mastra/core/memory';
9
11
  import type { TABLE_NAMES } from '@mastra/core/storage';
10
12
  import { TABLE_SCHEMAS } from '@mastra/core/storage';
13
+ import type { Trace } from '@mastra/core/telemetry';
11
14
  import type { WorkflowRun } from '@mastra/core/storage';
12
15
  import type { WorkflowRuns } from '@mastra/core/storage';
13
16
  import type { WorkflowRunState } from '@mastra/core/workflows';
@@ -66,6 +69,18 @@ declare class ClickhouseStore extends MastraStorage {
66
69
  tableName: TABLE_NAMES;
67
70
  schema: Record<string, StorageColumn>;
68
71
  }): Promise<void>;
72
+ protected getSqlType(type: StorageColumn['type']): string;
73
+ /**
74
+ * Alters table schema to add columns if they don't exist
75
+ * @param tableName Name of the table
76
+ * @param schema Schema of the table
77
+ * @param ifNotExists Array of column names to add if they don't exist
78
+ */
79
+ alterTable({ tableName, schema, ifNotExists, }: {
80
+ tableName: TABLE_NAMES;
81
+ schema: Record<string, StorageColumn>;
82
+ ifNotExists: string[];
83
+ }): Promise<void>;
69
84
  clearTable({ tableName }: {
70
85
  tableName: TABLE_NAMES;
71
86
  }): Promise<void>;
@@ -131,6 +146,19 @@ declare class ClickhouseStore extends MastraStorage {
131
146
  workflowName?: string;
132
147
  }): Promise<WorkflowRun | null>;
133
148
  private hasColumn;
149
+ getTracesPaginated(_args: StorageGetTracesArg): Promise<PaginationInfo & {
150
+ traces: Trace[];
151
+ }>;
152
+ getThreadsByResourceIdPaginated(_args: {
153
+ resourceId: string;
154
+ page?: number;
155
+ perPage?: number;
156
+ }): Promise<PaginationInfo & {
157
+ threads: StorageThreadType[];
158
+ }>;
159
+ getMessagesPaginated(_args: StorageGetMessagesArg): Promise<PaginationInfo & {
160
+ messages: MastraMessageV1[] | MastraMessageV2[];
161
+ }>;
134
162
  close(): Promise<void>;
135
163
  }
136
164
  export { ClickhouseStore }
@@ -1,13 +1,16 @@
1
1
  import type { ClickHouseClient } from '@clickhouse/client';
2
2
  import type { EvalRow } from '@mastra/core/storage';
3
3
  import type { MastraMessageV1 } from '@mastra/core/memory';
4
- import type { MastraMessageV2 } from '@mastra/core/agent';
4
+ import type { MastraMessageV2 } from '@mastra/core/memory';
5
5
  import { MastraStorage } from '@mastra/core/storage';
6
+ import type { PaginationInfo } from '@mastra/core/storage';
6
7
  import type { StorageColumn } from '@mastra/core/storage';
7
8
  import type { StorageGetMessagesArg } from '@mastra/core/storage';
9
+ import type { StorageGetTracesArg } from '@mastra/core/storage';
8
10
  import type { StorageThreadType } from '@mastra/core/memory';
9
11
  import type { TABLE_NAMES } from '@mastra/core/storage';
10
12
  import { TABLE_SCHEMAS } from '@mastra/core/storage';
13
+ import type { Trace } from '@mastra/core/telemetry';
11
14
  import type { WorkflowRun } from '@mastra/core/storage';
12
15
  import type { WorkflowRuns } from '@mastra/core/storage';
13
16
  import type { WorkflowRunState } from '@mastra/core/workflows';
@@ -66,6 +69,18 @@ declare class ClickhouseStore extends MastraStorage {
66
69
  tableName: TABLE_NAMES;
67
70
  schema: Record<string, StorageColumn>;
68
71
  }): Promise<void>;
72
+ protected getSqlType(type: StorageColumn['type']): string;
73
+ /**
74
+ * Alters table schema to add columns if they don't exist
75
+ * @param tableName Name of the table
76
+ * @param schema Schema of the table
77
+ * @param ifNotExists Array of column names to add if they don't exist
78
+ */
79
+ alterTable({ tableName, schema, ifNotExists, }: {
80
+ tableName: TABLE_NAMES;
81
+ schema: Record<string, StorageColumn>;
82
+ ifNotExists: string[];
83
+ }): Promise<void>;
69
84
  clearTable({ tableName }: {
70
85
  tableName: TABLE_NAMES;
71
86
  }): Promise<void>;
@@ -131,6 +146,19 @@ declare class ClickhouseStore extends MastraStorage {
131
146
  workflowName?: string;
132
147
  }): Promise<WorkflowRun | null>;
133
148
  private hasColumn;
149
+ getTracesPaginated(_args: StorageGetTracesArg): Promise<PaginationInfo & {
150
+ traces: Trace[];
151
+ }>;
152
+ getThreadsByResourceIdPaginated(_args: {
153
+ resourceId: string;
154
+ page?: number;
155
+ perPage?: number;
156
+ }): Promise<PaginationInfo & {
157
+ threads: StorageThreadType[];
158
+ }>;
159
+ getMessagesPaginated(_args: StorageGetMessagesArg): Promise<PaginationInfo & {
160
+ messages: MastraMessageV1[] | MastraMessageV2[];
161
+ }>;
134
162
  close(): Promise<void>;
135
163
  }
136
164
  export { ClickhouseStore }
package/dist/index.cjs CHANGED
@@ -267,6 +267,61 @@ var ClickhouseStore = class extends storage.MastraStorage {
267
267
  throw error;
268
268
  }
269
269
  }
270
+ getSqlType(type) {
271
+ switch (type) {
272
+ case "text":
273
+ return "String";
274
+ case "timestamp":
275
+ return "DateTime64(3)";
276
+ case "integer":
277
+ case "bigint":
278
+ return "Int64";
279
+ case "jsonb":
280
+ return "String";
281
+ default:
282
+ return super.getSqlType(type);
283
+ }
284
+ }
285
+ /**
286
+ * Alters table schema to add columns if they don't exist
287
+ * @param tableName Name of the table
288
+ * @param schema Schema of the table
289
+ * @param ifNotExists Array of column names to add if they don't exist
290
+ */
291
+ async alterTable({
292
+ tableName,
293
+ schema,
294
+ ifNotExists
295
+ }) {
296
+ try {
297
+ const describeSql = `DESCRIBE TABLE ${tableName}`;
298
+ const result = await this.db.query({
299
+ query: describeSql
300
+ });
301
+ const rows = await result.json();
302
+ const existingColumnNames = new Set(rows.data.map((row) => row.name.toLowerCase()));
303
+ for (const columnName of ifNotExists) {
304
+ if (!existingColumnNames.has(columnName.toLowerCase()) && schema[columnName]) {
305
+ const columnDef = schema[columnName];
306
+ let sqlType = this.getSqlType(columnDef.type);
307
+ if (columnDef.nullable !== false) {
308
+ sqlType = `Nullable(${sqlType})`;
309
+ }
310
+ const defaultValue = columnDef.nullable === false ? this.getDefaultValue(columnDef.type) : "";
311
+ const alterSql = `ALTER TABLE ${tableName} ADD COLUMN IF NOT EXISTS "${columnName}" ${sqlType} ${defaultValue}`.trim();
312
+ await this.db.query({
313
+ query: alterSql
314
+ });
315
+ this.logger?.debug?.(`Added column ${columnName} to table ${tableName}`);
316
+ }
317
+ }
318
+ } catch (error) {
319
+ this.logger?.error?.(
320
+ `Error altering table ${tableName}: ${error instanceof Error ? error.message : String(error)}`
321
+ );
322
+ throw new Error(`Failed to alter table ${tableName}: ${error}`);
323
+ }
324
+ }
270
325
  async clearTable({ tableName }) {
271
326
  try {
272
327
  await this.db.query({
@@ -857,6 +912,15 @@ var ClickhouseStore = class extends storage.MastraStorage {
857
912
  const columns = await result.json();
858
913
  return columns.some((c) => c.name === column);
859
914
  }
915
+ async getTracesPaginated(_args) {
916
+ throw new Error("Method not implemented.");
917
+ }
918
+ async getThreadsByResourceIdPaginated(_args) {
919
+ throw new Error("Method not implemented.");
920
+ }
921
+ async getMessagesPaginated(_args) {
922
+ throw new Error("Method not implemented.");
923
+ }
860
924
  async close() {
861
925
  await this.db.close();
862
926
  }
package/dist/index.js CHANGED
@@ -265,6 +265,61 @@ var ClickhouseStore = class extends MastraStorage {
265
265
  throw error;
266
266
  }
267
267
  }
268
+ getSqlType(type) {
269
+ switch (type) {
270
+ case "text":
271
+ return "String";
272
+ case "timestamp":
273
+ return "DateTime64(3)";
274
+ case "integer":
275
+ case "bigint":
276
+ return "Int64";
277
+ case "jsonb":
278
+ return "String";
279
+ default:
280
+ return super.getSqlType(type);
281
+ }
282
+ }
283
+ /**
284
+ * Alters table schema to add columns if they don't exist
285
+ * @param tableName Name of the table
286
+ * @param schema Schema of the table
287
+ * @param ifNotExists Array of column names to add if they don't exist
288
+ */
289
+ async alterTable({
290
+ tableName,
291
+ schema,
292
+ ifNotExists
293
+ }) {
294
+ try {
295
+ const describeSql = `DESCRIBE TABLE ${tableName}`;
296
+ const result = await this.db.query({
297
+ query: describeSql
298
+ });
299
+ const rows = await result.json();
300
+ const existingColumnNames = new Set(rows.data.map((row) => row.name.toLowerCase()));
301
+ for (const columnName of ifNotExists) {
302
+ if (!existingColumnNames.has(columnName.toLowerCase()) && schema[columnName]) {
303
+ const columnDef = schema[columnName];
304
+ let sqlType = this.getSqlType(columnDef.type);
305
+ if (columnDef.nullable !== false) {
306
+ sqlType = `Nullable(${sqlType})`;
307
+ }
308
+ const defaultValue = columnDef.nullable === false ? this.getDefaultValue(columnDef.type) : "";
309
+ const alterSql = `ALTER TABLE ${tableName} ADD COLUMN IF NOT EXISTS "${columnName}" ${sqlType} ${defaultValue}`.trim();
310
+ await this.db.query({
311
+ query: alterSql
312
+ });
313
+ this.logger?.debug?.(`Added column ${columnName} to table ${tableName}`);
314
+ }
315
+ }
316
+ } catch (error) {
317
+ this.logger?.error?.(
318
+ `Error altering table ${tableName}: ${error instanceof Error ? error.message : String(error)}`
319
+ );
320
+ throw new Error(`Failed to alter table ${tableName}: ${error}`);
321
+ }
322
+ }
268
323
  async clearTable({ tableName }) {
269
324
  try {
270
325
  await this.db.query({
@@ -855,6 +910,15 @@ var ClickhouseStore = class extends MastraStorage {
855
910
  const columns = await result.json();
856
911
  return columns.some((c) => c.name === column);
857
912
  }
913
+ async getTracesPaginated(_args) {
914
+ throw new Error("Method not implemented.");
915
+ }
916
+ async getThreadsByResourceIdPaginated(_args) {
917
+ throw new Error("Method not implemented.");
918
+ }
919
+ async getMessagesPaginated(_args) {
920
+ throw new Error("Method not implemented.");
921
+ }
858
922
  async close() {
859
923
  await this.db.close();
860
924
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mastra/clickhouse",
3
- "version": "0.10.1",
3
+ "version": "0.10.2-alpha.1",
4
4
  "description": "Clickhouse provider for Mastra - includes db storage capabilities",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -23,14 +23,15 @@
23
23
  "@clickhouse/client": "^1.11.0"
24
24
  },
25
25
  "devDependencies": {
26
- "@microsoft/api-extractor": "^7.52.5",
27
- "@types/node": "^20.17.27",
28
- "eslint": "^9.23.0",
29
- "tsup": "^8.4.0",
26
+ "@microsoft/api-extractor": "^7.52.8",
27
+ "@types/node": "^20.17.57",
28
+ "eslint": "^9.28.0",
29
+ "tsup": "^8.5.0",
30
30
  "typescript": "^5.8.2",
31
- "vitest": "^3.1.2",
32
- "@mastra/core": "0.10.2",
33
- "@internal/lint": "0.0.8"
31
+ "vitest": "^3.2.2",
32
+ "@internal/lint": "0.0.10",
33
+ "@internal/storage-test-utils": "0.0.6",
34
+ "@mastra/core": "0.10.4-alpha.3"
34
35
  },
35
36
  "peerDependencies": {
36
37
  "@mastra/core": "^0.10.2-alpha.0"
@@ -1,9 +1,16 @@
1
1
  import { randomUUID } from 'crypto';
2
- import type { MastraMessageV1, WorkflowRunState } from '@mastra/core';
2
+ import {
3
+ createSampleMessageV1,
4
+ createSampleThread,
5
+ createSampleWorkflowSnapshot,
6
+ checkWorkflowSnapshot,
7
+ } from '@internal/storage-test-utils';
8
+ import type { MastraMessageV1, StorageColumn, WorkflowRunState } from '@mastra/core';
9
+ import type { TABLE_NAMES } from '@mastra/core/storage';
3
10
  import { TABLE_THREADS, TABLE_MESSAGES, TABLE_WORKFLOW_SNAPSHOT } from '@mastra/core/storage';
4
11
  import { describe, it, expect, beforeAll, beforeEach, afterAll, vi, afterEach } from 'vitest';
5
12
 
6
- import { ClickhouseStore } from '.';
13
+ import { ClickhouseStore, TABLE_ENGINES } from '.';
7
14
  import type { ClickhouseConfig } from '.';
8
15
 
9
16
  vi.setConfig({ testTimeout: 60_000, hookTimeout: 60_000 });
@@ -24,33 +31,6 @@ const TEST_CONFIG: ClickhouseConfig = {
24
31
  },
25
32
  };
26
33
 
27
- // Sample test data factory functions
28
- const createSampleThread = () => ({
29
- id: `thread-${randomUUID()}`,
30
- resourceId: `clickhouse-test`,
31
- title: 'Test Thread',
32
- createdAt: new Date(),
33
- updatedAt: new Date(),
34
- metadata: { key: 'value' },
35
- });
36
-
37
- let role = `user`;
38
- const getRole = () => {
39
- if (role === `user`) role = `assistant`;
40
- else role = `user`;
41
- return role as 'user' | 'assistant';
42
- };
43
-
44
- const createSampleMessage = (threadId: string, createdAt: Date = new Date()): MastraMessageV1 => ({
45
- id: `msg-${randomUUID()}`,
46
- resourceId: `clickhouse-test`,
47
- role: getRole(),
48
- type: 'text',
49
- threadId,
50
- content: 'Hello',
51
- createdAt,
52
- });
53
-
54
34
  const createSampleTrace = () => ({
55
35
  id: `trace-${randomUUID()}`,
56
36
  name: 'Test Trace',
@@ -66,39 +46,6 @@ const createSampleEval = () => ({
66
46
  createdAt: new Date(),
67
47
  });
68
48
 
69
- const createSampleWorkflowSnapshot = (status: WorkflowRunState['context']['steps']['status'], createdAt?: Date) => {
70
- const runId = `run-${randomUUID()}`;
71
- const stepId = `step-${randomUUID()}`;
72
- const timestamp = createdAt || new Date();
73
- const snapshot = {
74
- result: { success: true },
75
- value: {},
76
- context: {
77
- [stepId]: {
78
- status,
79
- payload: {},
80
- error: undefined,
81
- startedAt: timestamp.getTime(),
82
- endedAt: new Date(timestamp.getTime() + 15000).getTime(),
83
- },
84
- input: {},
85
- },
86
- serializedStepGraph: [],
87
- activePaths: [],
88
- suspendedPaths: {},
89
- runId,
90
- timestamp: timestamp.getTime(),
91
- } as unknown as WorkflowRunState;
92
- return { snapshot, runId, stepId };
93
- };
94
-
95
- const checkWorkflowSnapshot = (snapshot: WorkflowRunState | string, stepId: string, status: string) => {
96
- if (typeof snapshot === 'string') {
97
- throw new Error('Expected WorkflowRunState, got string');
98
- }
99
- expect(snapshot.context?.[stepId]?.status).toBe(status);
100
- };
101
-
102
49
  describe('ClickhouseStore', () => {
103
50
  let store: ClickhouseStore;
104
51
 
@@ -171,7 +118,10 @@ describe('ClickhouseStore', () => {
171
118
  await store.saveThread({ thread });
172
119
 
173
120
  // Add some messages
174
- const messages = [createSampleMessage(thread.id), createSampleMessage(thread.id)];
121
+ const messages = [
122
+ createSampleMessageV1({ threadId: thread.id, resourceId: 'clickhouse-test' }),
123
+ createSampleMessageV1({ threadId: thread.id, resourceId: 'clickhouse-test' }),
124
+ ];
175
125
  await store.saveMessages({ messages });
176
126
 
177
127
  await store.deleteThread({ threadId: thread.id });
@@ -191,8 +141,12 @@ describe('ClickhouseStore', () => {
191
141
  await store.saveThread({ thread });
192
142
 
193
143
  const messages = [
194
- createSampleMessage(thread.id, new Date(Date.now() - 1000 * 60 * 60 * 24)),
195
- createSampleMessage(thread.id),
144
+ createSampleMessageV1({
145
+ threadId: thread.id,
146
+ createdAt: new Date(Date.now() - 1000 * 60 * 60 * 24),
147
+ resourceId: 'clickhouse-test',
148
+ }),
149
+ createSampleMessageV1({ threadId: thread.id, resourceId: 'clickhouse-test' }),
196
150
  ];
197
151
 
198
152
  // Save messages
@@ -218,23 +172,35 @@ describe('ClickhouseStore', () => {
218
172
  const thread = createSampleThread();
219
173
  await store.saveThread({ thread });
220
174
 
221
- const messages: MastraMessageV1[] = [
175
+ const messages = [
222
176
  {
223
- ...createSampleMessage(thread.id, new Date(Date.now() - 1000 * 3)),
224
- content: [{ type: 'text', text: 'First' }],
177
+ ...createSampleMessageV1({
178
+ threadId: thread.id,
179
+ createdAt: new Date(Date.now() - 1000 * 3),
180
+ content: 'First',
181
+ resourceId: 'clickhouse-test',
182
+ }),
225
183
  role: 'user',
226
184
  },
227
185
  {
228
- ...createSampleMessage(thread.id, new Date(Date.now() - 1000 * 2)),
229
- content: [{ type: 'text', text: 'Second' }],
186
+ ...createSampleMessageV1({
187
+ threadId: thread.id,
188
+ createdAt: new Date(Date.now() - 1000 * 2),
189
+ content: 'Second',
190
+ resourceId: 'clickhouse-test',
191
+ }),
230
192
  role: 'assistant',
231
193
  },
232
194
  {
233
- ...createSampleMessage(thread.id, new Date(Date.now() - 1000 * 1)),
234
- content: [{ type: 'text', text: 'Third' }],
195
+ ...createSampleMessageV1({
196
+ threadId: thread.id,
197
+ createdAt: new Date(Date.now() - 1000 * 1),
198
+ content: 'Third',
199
+ resourceId: 'clickhouse-test',
200
+ }),
235
201
  role: 'user',
236
202
  },
237
- ];
203
+ ] as MastraMessageV1[];
238
204
 
239
205
  await store.saveMessages({ messages });
240
206
 
@@ -248,13 +214,107 @@ describe('ClickhouseStore', () => {
248
214
  });
249
215
  }, 10e3);
250
216
 
217
+ // it('should retrieve messages w/ next/prev messages by message id + resource id', async () => {
218
+ // const messages: MastraMessageV2[] = [
219
+ // createSampleMessageV2({ threadId: 'thread-one', content: 'First', resourceId: 'cross-thread-resource' }),
220
+ // createSampleMessageV2({ threadId: 'thread-one', content: 'Second', resourceId: 'cross-thread-resource' }),
221
+ // createSampleMessageV2({ threadId: 'thread-one', content: 'Third', resourceId: 'cross-thread-resource' }),
222
+
223
+ // createSampleMessageV2({ threadId: 'thread-two', content: 'Fourth', resourceId: 'cross-thread-resource' }),
224
+ // createSampleMessageV2({ threadId: 'thread-two', content: 'Fifth', resourceId: 'cross-thread-resource' }),
225
+ // createSampleMessageV2({ threadId: 'thread-two', content: 'Sixth', resourceId: 'cross-thread-resource' }),
226
+
227
+ // createSampleMessageV2({ threadId: 'thread-three', content: 'Seventh', resourceId: 'other-resource' }),
228
+ // createSampleMessageV2({ threadId: 'thread-three', content: 'Eighth', resourceId: 'other-resource' }),
229
+ // ];
230
+
231
+ // await store.saveMessages({ messages: messages, format: 'v2' });
232
+
233
+ // const retrievedMessages = await store.getMessages({ threadId: 'thread-one', format: 'v2' });
234
+ // expect(retrievedMessages).toHaveLength(3);
235
+ // expect(retrievedMessages.map((m: any) => m.content.parts[0].text)).toEqual(['First', 'Second', 'Third']);
236
+
237
+ // const retrievedMessages2 = await store.getMessages({ threadId: 'thread-two', format: 'v2' });
238
+ // expect(retrievedMessages2).toHaveLength(3);
239
+ // expect(retrievedMessages2.map((m: any) => m.content.parts[0].text)).toEqual(['Fourth', 'Fifth', 'Sixth']);
240
+
241
+ // const retrievedMessages3 = await store.getMessages({ threadId: 'thread-three', format: 'v2' });
242
+ // expect(retrievedMessages3).toHaveLength(2);
243
+ // expect(retrievedMessages3.map((m: any) => m.content.parts[0].text)).toEqual(['Seventh', 'Eighth']);
244
+
245
+ // const crossThreadMessages = await store.getMessages({
246
+ // threadId: 'thread-doesnt-exist',
247
+ // resourceId: 'cross-thread-resource',
248
+ // format: 'v2',
249
+ // selectBy: {
250
+ // last: 0,
251
+ // include: [
252
+ // {
253
+ // id: messages[1].id,
254
+ // withNextMessages: 2,
255
+ // withPreviousMessages: 2,
256
+ // },
257
+ // {
258
+ // id: messages[4].id,
259
+ // withPreviousMessages: 2,
260
+ // withNextMessages: 2,
261
+ // },
262
+ // ],
263
+ // },
264
+ // });
265
+
266
+ // expect(crossThreadMessages).toHaveLength(6);
267
+ // expect(crossThreadMessages.filter(m => m.threadId === `thread-one`)).toHaveLength(3);
268
+ // expect(crossThreadMessages.filter(m => m.threadId === `thread-two`)).toHaveLength(3);
269
+
270
+ // const crossThreadMessages2 = await store.getMessages({
271
+ // threadId: 'thread-one',
272
+ // resourceId: 'cross-thread-resource',
273
+ // format: 'v2',
274
+ // selectBy: {
275
+ // last: 0,
276
+ // include: [
277
+ // {
278
+ // id: messages[4].id,
279
+ // withPreviousMessages: 1,
280
+ // withNextMessages: 30,
281
+ // },
282
+ // ],
283
+ // },
284
+ // });
285
+
286
+ // expect(crossThreadMessages2).toHaveLength(3);
287
+ // expect(crossThreadMessages2.filter(m => m.threadId === `thread-one`)).toHaveLength(0);
288
+ // expect(crossThreadMessages2.filter(m => m.threadId === `thread-two`)).toHaveLength(3);
289
+
290
+ // const crossThreadMessages3 = await store.getMessages({
291
+ // threadId: 'thread-two',
292
+ // resourceId: 'cross-thread-resource',
293
+ // format: 'v2',
294
+ // selectBy: {
295
+ // last: 0,
296
+ // include: [
297
+ // {
298
+ // id: messages[1].id,
299
+ // withNextMessages: 1,
300
+ // withPreviousMessages: 1,
301
+ // },
302
+ // ],
303
+ // },
304
+ // });
305
+
306
+ // expect(crossThreadMessages3).toHaveLength(3);
307
+ // expect(crossThreadMessages3.filter(m => m.threadId === `thread-one`)).toHaveLength(3);
308
+ // expect(crossThreadMessages3.filter(m => m.threadId === `thread-two`)).toHaveLength(0);
309
+ // });
310
+
251
311
  // it('should rollback on error during message save', async () => {
252
312
  // const thread = createSampleThread();
253
313
  // await store.saveThread({ thread });
254
314
 
255
315
  // const messages = [
256
- // createSampleMessage(thread.id),
257
- // { ...createSampleMessage(thread.id), id: null }, // This will cause an error
316
+ // createSampleMessageV1({ threadId: thread.id }),
317
+ // { ...createSampleMessageV1({ threadId: thread.id }), id: null }, // This will cause an error
258
318
  // ];
259
319
 
260
320
  // await expect(store.saveMessages({ messages })).rejects.toThrow();
@@ -842,6 +902,100 @@ describe('ClickhouseStore', () => {
842
902
  });
843
903
  });
844
904
 
905
+ describe('alterTable', () => {
906
+ const TEST_TABLE = 'test_alter_table';
907
+ const BASE_SCHEMA = {
908
+ id: { type: 'integer', primaryKey: true, nullable: false },
909
+ name: { type: 'text', nullable: true },
910
+ createdAt: { type: 'timestamp', nullable: false },
911
+ updatedAt: { type: 'timestamp', nullable: false },
912
+ } as Record<string, StorageColumn>;
913
+
914
+ TABLE_ENGINES[TEST_TABLE] = 'MergeTree()';
915
+
916
+ beforeEach(async () => {
917
+ await store.createTable({ tableName: TEST_TABLE as TABLE_NAMES, schema: BASE_SCHEMA });
918
+ });
919
+
920
+ afterEach(async () => {
921
+ await store.clearTable({ tableName: TEST_TABLE as TABLE_NAMES });
922
+ });
923
+
924
+ it('adds a new column to an existing table', async () => {
925
+ await store.alterTable({
926
+ tableName: TEST_TABLE as TABLE_NAMES,
927
+ schema: { ...BASE_SCHEMA, age: { type: 'integer', nullable: true } },
928
+ ifNotExists: ['age'],
929
+ });
930
+
931
+ await store.insert({
932
+ tableName: TEST_TABLE as TABLE_NAMES,
933
+ record: { id: 1, name: 'Alice', age: 42, createdAt: new Date(), updatedAt: new Date() },
934
+ });
935
+
936
+ const row = await store.load<{ id: string; name: string; age?: number }>({
937
+ tableName: TEST_TABLE as TABLE_NAMES,
938
+ keys: { id: '1' },
939
+ });
940
+ expect(row?.age).toBe(42);
941
+ });
942
+
943
+ it('is idempotent when adding an existing column', async () => {
944
+ await store.alterTable({
945
+ tableName: TEST_TABLE as TABLE_NAMES,
946
+ schema: { ...BASE_SCHEMA, foo: { type: 'text', nullable: true } },
947
+ ifNotExists: ['foo'],
948
+ });
949
+ // Add the column again (should not throw)
950
+ await expect(
951
+ store.alterTable({
952
+ tableName: TEST_TABLE as TABLE_NAMES,
953
+ schema: { ...BASE_SCHEMA, foo: { type: 'text', nullable: true } },
954
+ ifNotExists: ['foo'],
955
+ }),
956
+ ).resolves.not.toThrow();
957
+ });
958
+
959
+ it('should add a default value to a column when using not null', async () => {
960
+ await store.insert({
961
+ tableName: TEST_TABLE as TABLE_NAMES,
962
+ record: { id: 1, name: 'Bob', createdAt: new Date(), updatedAt: new Date() },
963
+ });
964
+
965
+ await expect(
966
+ store.alterTable({
967
+ tableName: TEST_TABLE as TABLE_NAMES,
968
+ schema: { ...BASE_SCHEMA, text_column: { type: 'text', nullable: false } },
969
+ ifNotExists: ['text_column'],
970
+ }),
971
+ ).resolves.not.toThrow();
972
+
973
+ await expect(
974
+ store.alterTable({
975
+ tableName: TEST_TABLE as TABLE_NAMES,
976
+ schema: { ...BASE_SCHEMA, timestamp_column: { type: 'timestamp', nullable: false } },
977
+ ifNotExists: ['timestamp_column'],
978
+ }),
979
+ ).resolves.not.toThrow();
980
+
981
+ await expect(
982
+ store.alterTable({
983
+ tableName: TEST_TABLE as TABLE_NAMES,
984
+ schema: { ...BASE_SCHEMA, bigint_column: { type: 'bigint', nullable: false } },
985
+ ifNotExists: ['bigint_column'],
986
+ }),
987
+ ).resolves.not.toThrow();
988
+
989
+ await expect(
990
+ store.alterTable({
991
+ tableName: TEST_TABLE as TABLE_NAMES,
992
+ schema: { ...BASE_SCHEMA, jsonb_column: { type: 'jsonb', nullable: false } },
993
+ ifNotExists: ['jsonb_column'],
994
+ }),
995
+ ).resolves.not.toThrow();
996
+ });
997
+ });
998
+
845
999
  afterAll(async () => {
846
1000
  await store.close();
847
1001
  });
@@ -1,9 +1,8 @@
1
1
  import type { ClickHouseClient } from '@clickhouse/client';
2
2
  import { createClient } from '@clickhouse/client';
3
3
  import { MessageList } from '@mastra/core/agent';
4
- import type { MastraMessageV2 } from '@mastra/core/agent';
5
4
  import type { MetricResult, TestInfo } from '@mastra/core/eval';
6
- import type { MastraMessageV1, StorageThreadType } from '@mastra/core/memory';
5
+ import type { MastraMessageV1, MastraMessageV2, StorageThreadType } from '@mastra/core/memory';
7
6
  import {
8
7
  MastraStorage,
9
8
  TABLE_EVALS,
@@ -15,12 +14,15 @@ import {
15
14
  } from '@mastra/core/storage';
16
15
  import type {
17
16
  EvalRow,
17
+ PaginationInfo,
18
18
  StorageColumn,
19
19
  StorageGetMessagesArg,
20
20
  TABLE_NAMES,
21
21
  WorkflowRun,
22
22
  WorkflowRuns,
23
+ StorageGetTracesArg,
23
24
  } from '@mastra/core/storage';
25
+ import type { Trace } from '@mastra/core/telemetry';
24
26
  import type { WorkflowRunState } from '@mastra/core/workflows';
25
27
 
26
28
  function safelyParseJSON(jsonString: string): any {
@@ -371,6 +373,73 @@ export class ClickhouseStore extends MastraStorage {
371
373
  }
372
374
  }
373
375
 
376
+ protected getSqlType(type: StorageColumn['type']): string {
377
+ switch (type) {
378
+ case 'text':
379
+ return 'String';
380
+ case 'timestamp':
381
+ return 'DateTime64(3)';
382
+ case 'integer':
383
+ case 'bigint':
384
+ return 'Int64';
385
+ case 'jsonb':
386
+ return 'String';
387
+ default:
388
+ return super.getSqlType(type); // fallback to base implementation
389
+ }
390
+ }
391
+
392
+ /**
393
+ * Alters table schema to add columns if they don't exist
394
+ * @param tableName Name of the table
395
+ * @param schema Schema of the table
396
+ * @param ifNotExists Array of column names to add if they don't exist
397
+ */
398
+ async alterTable({
399
+ tableName,
400
+ schema,
401
+ ifNotExists,
402
+ }: {
403
+ tableName: TABLE_NAMES;
404
+ schema: Record<string, StorageColumn>;
405
+ ifNotExists: string[];
406
+ }): Promise<void> {
407
+ try {
408
+ // 1. Get existing columns
409
+ const describeSql = `DESCRIBE TABLE ${tableName}`;
410
+ const result = await this.db.query({
411
+ query: describeSql,
412
+ });
413
+ const rows = await result.json();
414
+ const existingColumnNames = new Set(rows.data.map((row: any) => row.name.toLowerCase()));
415
+
416
+ // 2. Add missing columns
417
+ for (const columnName of ifNotExists) {
418
+ if (!existingColumnNames.has(columnName.toLowerCase()) && schema[columnName]) {
419
+ const columnDef = schema[columnName];
420
+ let sqlType = this.getSqlType(columnDef.type);
421
+ if (columnDef.nullable !== false) {
422
+ sqlType = `Nullable(${sqlType})`;
423
+ }
424
+ const defaultValue = columnDef.nullable === false ? this.getDefaultValue(columnDef.type) : '';
425
+ // Use backticks or double quotes as needed for identifiers
426
+ const alterSql =
427
+ `ALTER TABLE ${tableName} ADD COLUMN IF NOT EXISTS "${columnName}" ${sqlType} ${defaultValue}`.trim();
428
+
429
+ await this.db.query({
430
+ query: alterSql,
431
+ });
432
+ this.logger?.debug?.(`Added column ${columnName} to table ${tableName}`);
433
+ }
434
+ }
435
+ } catch (error) {
436
+ this.logger?.error?.(
437
+ `Error altering table ${tableName}: ${error instanceof Error ? error.message : String(error)}`,
438
+ );
439
+ throw new Error(`Failed to alter table ${tableName}: ${error}`);
440
+ }
441
+ }
442
+
374
443
  async clearTable({ tableName }: { tableName: TABLE_NAMES }): Promise<void> {
375
444
  try {
376
445
  await this.db.query({
@@ -1069,6 +1138,24 @@ export class ClickhouseStore extends MastraStorage {
1069
1138
  return columns.some(c => c.name === column);
1070
1139
  }
1071
1140
 
1141
+ async getTracesPaginated(_args: StorageGetTracesArg): Promise<PaginationInfo & { traces: Trace[] }> {
1142
+ throw new Error('Method not implemented.');
1143
+ }
1144
+
1145
+ async getThreadsByResourceIdPaginated(_args: {
1146
+ resourceId: string;
1147
+ page?: number;
1148
+ perPage?: number;
1149
+ }): Promise<PaginationInfo & { threads: StorageThreadType[] }> {
1150
+ throw new Error('Method not implemented.');
1151
+ }
1152
+
1153
+ async getMessagesPaginated(
1154
+ _args: StorageGetMessagesArg,
1155
+ ): Promise<PaginationInfo & { messages: MastraMessageV1[] | MastraMessageV2[] }> {
1156
+ throw new Error('Method not implemented.');
1157
+ }
1158
+
1072
1159
  async close(): Promise<void> {
1073
1160
  await this.db.close();
1074
1161
  }