@mastra/clickhouse 0.10.1 → 0.10.2-alpha.0
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/.turbo/turbo-build.log +8 -8
- package/CHANGELOG.md +11 -0
- package/dist/_tsup-dts-rollup.d.cts +12 -0
- package/dist/_tsup-dts-rollup.d.ts +12 -0
- package/dist/index.cjs +55 -0
- package/dist/index.js +55 -0
- package/package.json +9 -8
- package/src/storage/index.test.ts +128 -66
- package/src/storage/index.ts +67 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
|
|
2
|
-
> @mastra/clickhouse@0.10.
|
|
2
|
+
> @mastra/clickhouse@0.10.2-alpha.0 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
|
[34mCLI[39m Building entry: src/index.ts
|
|
6
6
|
[34mCLI[39m Using tsconfig: tsconfig.json
|
|
7
|
-
[34mCLI[39m tsup v8.
|
|
7
|
+
[34mCLI[39m tsup v8.5.0
|
|
8
8
|
[34mTSC[39m Build start
|
|
9
|
-
[32mTSC[39m ⚡️ Build success in
|
|
9
|
+
[32mTSC[39m ⚡️ Build success in 9329ms
|
|
10
10
|
[34mDTS[39m Build start
|
|
11
11
|
[34mCLI[39m Target: es2022
|
|
12
12
|
Analysis will use the bundled TypeScript version 5.8.3
|
|
13
13
|
[36mWriting package typings: /home/runner/work/mastra/mastra/stores/clickhouse/dist/_tsup-dts-rollup.d.ts[39m
|
|
14
14
|
Analysis will use the bundled TypeScript version 5.8.3
|
|
15
15
|
[36mWriting package typings: /home/runner/work/mastra/mastra/stores/clickhouse/dist/_tsup-dts-rollup.d.cts[39m
|
|
16
|
-
[32mDTS[39m ⚡️ Build success in
|
|
16
|
+
[32mDTS[39m ⚡️ Build success in 11005ms
|
|
17
17
|
[34mCLI[39m Cleaning output folder
|
|
18
18
|
[34mESM[39m Build start
|
|
19
19
|
[34mCJS[39m Build start
|
|
20
|
-
[32mESM[39m [1mdist/index.js [22m[
|
|
21
|
-
[32mESM[39m ⚡️ Build success in
|
|
22
|
-
[32mCJS[39m [1mdist/index.cjs [22m[
|
|
23
|
-
[32mCJS[39m ⚡️ Build success in
|
|
20
|
+
[32mESM[39m [1mdist/index.js [22m[32m30.08 KB[39m
|
|
21
|
+
[32mESM[39m ⚡️ Build success in 1046ms
|
|
22
|
+
[32mCJS[39m [1mdist/index.cjs [22m[32m30.33 KB[39m
|
|
23
|
+
[32mCJS[39m ⚡️ Build success in 1040ms
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# @mastra/clickhouse
|
|
2
2
|
|
|
3
|
+
## 0.10.2-alpha.0
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- dffb67b: updated stores to add alter table and change tests
|
|
8
|
+
- Updated dependencies [f6fd25f]
|
|
9
|
+
- Updated dependencies [dffb67b]
|
|
10
|
+
- Updated dependencies [f1309d3]
|
|
11
|
+
- Updated dependencies [f7f8293]
|
|
12
|
+
- @mastra/core@0.10.4-alpha.1
|
|
13
|
+
|
|
3
14
|
## 0.10.1
|
|
4
15
|
|
|
5
16
|
### Patch Changes
|
|
@@ -66,6 +66,18 @@ declare class ClickhouseStore extends MastraStorage {
|
|
|
66
66
|
tableName: TABLE_NAMES;
|
|
67
67
|
schema: Record<string, StorageColumn>;
|
|
68
68
|
}): Promise<void>;
|
|
69
|
+
protected getSqlType(type: StorageColumn['type']): string;
|
|
70
|
+
/**
|
|
71
|
+
* Alters table schema to add columns if they don't exist
|
|
72
|
+
* @param tableName Name of the table
|
|
73
|
+
* @param schema Schema of the table
|
|
74
|
+
* @param ifNotExists Array of column names to add if they don't exist
|
|
75
|
+
*/
|
|
76
|
+
alterTable({ tableName, schema, ifNotExists, }: {
|
|
77
|
+
tableName: TABLE_NAMES;
|
|
78
|
+
schema: Record<string, StorageColumn>;
|
|
79
|
+
ifNotExists: string[];
|
|
80
|
+
}): Promise<void>;
|
|
69
81
|
clearTable({ tableName }: {
|
|
70
82
|
tableName: TABLE_NAMES;
|
|
71
83
|
}): Promise<void>;
|
|
@@ -66,6 +66,18 @@ declare class ClickhouseStore extends MastraStorage {
|
|
|
66
66
|
tableName: TABLE_NAMES;
|
|
67
67
|
schema: Record<string, StorageColumn>;
|
|
68
68
|
}): Promise<void>;
|
|
69
|
+
protected getSqlType(type: StorageColumn['type']): string;
|
|
70
|
+
/**
|
|
71
|
+
* Alters table schema to add columns if they don't exist
|
|
72
|
+
* @param tableName Name of the table
|
|
73
|
+
* @param schema Schema of the table
|
|
74
|
+
* @param ifNotExists Array of column names to add if they don't exist
|
|
75
|
+
*/
|
|
76
|
+
alterTable({ tableName, schema, ifNotExists, }: {
|
|
77
|
+
tableName: TABLE_NAMES;
|
|
78
|
+
schema: Record<string, StorageColumn>;
|
|
79
|
+
ifNotExists: string[];
|
|
80
|
+
}): Promise<void>;
|
|
69
81
|
clearTable({ tableName }: {
|
|
70
82
|
tableName: TABLE_NAMES;
|
|
71
83
|
}): Promise<void>;
|
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({
|
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({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mastra/clickhouse",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.2-alpha.0",
|
|
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.
|
|
27
|
-
"@types/node": "^20.17.
|
|
28
|
-
"eslint": "^9.
|
|
29
|
-
"tsup": "^8.
|
|
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.
|
|
32
|
-
"@
|
|
33
|
-
"@internal/
|
|
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.1"
|
|
34
35
|
},
|
|
35
36
|
"peerDependencies": {
|
|
36
37
|
"@mastra/core": "^0.10.2-alpha.0"
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { randomUUID } from 'crypto';
|
|
2
|
-
import
|
|
2
|
+
import { createSampleMessageV1, createSampleThread, createSampleWorkflowSnapshot } from '@internal/storage-test-utils';
|
|
3
|
+
import type { MastraMessageV1, StorageColumn, WorkflowRunState } from '@mastra/core';
|
|
4
|
+
import type { TABLE_NAMES } from '@mastra/core/storage';
|
|
3
5
|
import { TABLE_THREADS, TABLE_MESSAGES, TABLE_WORKFLOW_SNAPSHOT } from '@mastra/core/storage';
|
|
4
6
|
import { describe, it, expect, beforeAll, beforeEach, afterAll, vi, afterEach } from 'vitest';
|
|
5
7
|
|
|
6
|
-
import { ClickhouseStore } from '.';
|
|
8
|
+
import { ClickhouseStore, TABLE_ENGINES } from '.';
|
|
7
9
|
import type { ClickhouseConfig } from '.';
|
|
8
10
|
|
|
9
11
|
vi.setConfig({ testTimeout: 60_000, hookTimeout: 60_000 });
|
|
@@ -24,33 +26,6 @@ const TEST_CONFIG: ClickhouseConfig = {
|
|
|
24
26
|
},
|
|
25
27
|
};
|
|
26
28
|
|
|
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
29
|
const createSampleTrace = () => ({
|
|
55
30
|
id: `trace-${randomUUID()}`,
|
|
56
31
|
name: 'Test Trace',
|
|
@@ -66,32 +41,6 @@ const createSampleEval = () => ({
|
|
|
66
41
|
createdAt: new Date(),
|
|
67
42
|
});
|
|
68
43
|
|
|
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
44
|
const checkWorkflowSnapshot = (snapshot: WorkflowRunState | string, stepId: string, status: string) => {
|
|
96
45
|
if (typeof snapshot === 'string') {
|
|
97
46
|
throw new Error('Expected WorkflowRunState, got string');
|
|
@@ -171,7 +120,10 @@ describe('ClickhouseStore', () => {
|
|
|
171
120
|
await store.saveThread({ thread });
|
|
172
121
|
|
|
173
122
|
// Add some messages
|
|
174
|
-
const messages = [
|
|
123
|
+
const messages = [
|
|
124
|
+
createSampleMessageV1({ threadId: thread.id, resourceId: 'clickhouse-test' }),
|
|
125
|
+
createSampleMessageV1({ threadId: thread.id, resourceId: 'clickhouse-test' }),
|
|
126
|
+
];
|
|
175
127
|
await store.saveMessages({ messages });
|
|
176
128
|
|
|
177
129
|
await store.deleteThread({ threadId: thread.id });
|
|
@@ -191,8 +143,12 @@ describe('ClickhouseStore', () => {
|
|
|
191
143
|
await store.saveThread({ thread });
|
|
192
144
|
|
|
193
145
|
const messages = [
|
|
194
|
-
|
|
195
|
-
|
|
146
|
+
createSampleMessageV1({
|
|
147
|
+
threadId: thread.id,
|
|
148
|
+
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 24),
|
|
149
|
+
resourceId: 'clickhouse-test',
|
|
150
|
+
}),
|
|
151
|
+
createSampleMessageV1({ threadId: thread.id, resourceId: 'clickhouse-test' }),
|
|
196
152
|
];
|
|
197
153
|
|
|
198
154
|
// Save messages
|
|
@@ -220,18 +176,30 @@ describe('ClickhouseStore', () => {
|
|
|
220
176
|
|
|
221
177
|
const messages: MastraMessageV1[] = [
|
|
222
178
|
{
|
|
223
|
-
...
|
|
224
|
-
|
|
179
|
+
...createSampleMessageV1({
|
|
180
|
+
threadId: thread.id,
|
|
181
|
+
createdAt: new Date(Date.now() - 1000 * 3),
|
|
182
|
+
content: 'First',
|
|
183
|
+
resourceId: 'clickhouse-test',
|
|
184
|
+
}),
|
|
225
185
|
role: 'user',
|
|
226
186
|
},
|
|
227
187
|
{
|
|
228
|
-
...
|
|
229
|
-
|
|
188
|
+
...createSampleMessageV1({
|
|
189
|
+
threadId: thread.id,
|
|
190
|
+
createdAt: new Date(Date.now() - 1000 * 2),
|
|
191
|
+
content: 'Second',
|
|
192
|
+
resourceId: 'clickhouse-test',
|
|
193
|
+
}),
|
|
230
194
|
role: 'assistant',
|
|
231
195
|
},
|
|
232
196
|
{
|
|
233
|
-
...
|
|
234
|
-
|
|
197
|
+
...createSampleMessageV1({
|
|
198
|
+
threadId: thread.id,
|
|
199
|
+
createdAt: new Date(Date.now() - 1000 * 1),
|
|
200
|
+
content: 'Third',
|
|
201
|
+
resourceId: 'clickhouse-test',
|
|
202
|
+
}),
|
|
235
203
|
role: 'user',
|
|
236
204
|
},
|
|
237
205
|
];
|
|
@@ -253,8 +221,8 @@ describe('ClickhouseStore', () => {
|
|
|
253
221
|
// await store.saveThread({ thread });
|
|
254
222
|
|
|
255
223
|
// const messages = [
|
|
256
|
-
//
|
|
257
|
-
// { ...
|
|
224
|
+
// createSampleMessageV1({ threadId: thread.id }),
|
|
225
|
+
// { ...createSampleMessageV1({ threadId: thread.id }), id: null }, // This will cause an error
|
|
258
226
|
// ];
|
|
259
227
|
|
|
260
228
|
// await expect(store.saveMessages({ messages })).rejects.toThrow();
|
|
@@ -842,6 +810,100 @@ describe('ClickhouseStore', () => {
|
|
|
842
810
|
});
|
|
843
811
|
});
|
|
844
812
|
|
|
813
|
+
describe('alterTable', () => {
|
|
814
|
+
const TEST_TABLE = 'test_alter_table';
|
|
815
|
+
const BASE_SCHEMA = {
|
|
816
|
+
id: { type: 'integer', primaryKey: true, nullable: false },
|
|
817
|
+
name: { type: 'text', nullable: true },
|
|
818
|
+
createdAt: { type: 'timestamp', nullable: false },
|
|
819
|
+
updatedAt: { type: 'timestamp', nullable: false },
|
|
820
|
+
} as Record<string, StorageColumn>;
|
|
821
|
+
|
|
822
|
+
TABLE_ENGINES[TEST_TABLE] = 'MergeTree()';
|
|
823
|
+
|
|
824
|
+
beforeEach(async () => {
|
|
825
|
+
await store.createTable({ tableName: TEST_TABLE as TABLE_NAMES, schema: BASE_SCHEMA });
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
afterEach(async () => {
|
|
829
|
+
await store.clearTable({ tableName: TEST_TABLE as TABLE_NAMES });
|
|
830
|
+
});
|
|
831
|
+
|
|
832
|
+
it('adds a new column to an existing table', async () => {
|
|
833
|
+
await store.alterTable({
|
|
834
|
+
tableName: TEST_TABLE as TABLE_NAMES,
|
|
835
|
+
schema: { ...BASE_SCHEMA, age: { type: 'integer', nullable: true } },
|
|
836
|
+
ifNotExists: ['age'],
|
|
837
|
+
});
|
|
838
|
+
|
|
839
|
+
await store.insert({
|
|
840
|
+
tableName: TEST_TABLE as TABLE_NAMES,
|
|
841
|
+
record: { id: 1, name: 'Alice', age: 42, createdAt: new Date(), updatedAt: new Date() },
|
|
842
|
+
});
|
|
843
|
+
|
|
844
|
+
const row = await store.load<{ id: string; name: string; age?: number }>({
|
|
845
|
+
tableName: TEST_TABLE as TABLE_NAMES,
|
|
846
|
+
keys: { id: '1' },
|
|
847
|
+
});
|
|
848
|
+
expect(row?.age).toBe(42);
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
it('is idempotent when adding an existing column', async () => {
|
|
852
|
+
await store.alterTable({
|
|
853
|
+
tableName: TEST_TABLE as TABLE_NAMES,
|
|
854
|
+
schema: { ...BASE_SCHEMA, foo: { type: 'text', nullable: true } },
|
|
855
|
+
ifNotExists: ['foo'],
|
|
856
|
+
});
|
|
857
|
+
// Add the column again (should not throw)
|
|
858
|
+
await expect(
|
|
859
|
+
store.alterTable({
|
|
860
|
+
tableName: TEST_TABLE as TABLE_NAMES,
|
|
861
|
+
schema: { ...BASE_SCHEMA, foo: { type: 'text', nullable: true } },
|
|
862
|
+
ifNotExists: ['foo'],
|
|
863
|
+
}),
|
|
864
|
+
).resolves.not.toThrow();
|
|
865
|
+
});
|
|
866
|
+
|
|
867
|
+
it('should add a default value to a column when using not null', async () => {
|
|
868
|
+
await store.insert({
|
|
869
|
+
tableName: TEST_TABLE as TABLE_NAMES,
|
|
870
|
+
record: { id: 1, name: 'Bob', createdAt: new Date(), updatedAt: new Date() },
|
|
871
|
+
});
|
|
872
|
+
|
|
873
|
+
await expect(
|
|
874
|
+
store.alterTable({
|
|
875
|
+
tableName: TEST_TABLE as TABLE_NAMES,
|
|
876
|
+
schema: { ...BASE_SCHEMA, text_column: { type: 'text', nullable: false } },
|
|
877
|
+
ifNotExists: ['text_column'],
|
|
878
|
+
}),
|
|
879
|
+
).resolves.not.toThrow();
|
|
880
|
+
|
|
881
|
+
await expect(
|
|
882
|
+
store.alterTable({
|
|
883
|
+
tableName: TEST_TABLE as TABLE_NAMES,
|
|
884
|
+
schema: { ...BASE_SCHEMA, timestamp_column: { type: 'timestamp', nullable: false } },
|
|
885
|
+
ifNotExists: ['timestamp_column'],
|
|
886
|
+
}),
|
|
887
|
+
).resolves.not.toThrow();
|
|
888
|
+
|
|
889
|
+
await expect(
|
|
890
|
+
store.alterTable({
|
|
891
|
+
tableName: TEST_TABLE as TABLE_NAMES,
|
|
892
|
+
schema: { ...BASE_SCHEMA, bigint_column: { type: 'bigint', nullable: false } },
|
|
893
|
+
ifNotExists: ['bigint_column'],
|
|
894
|
+
}),
|
|
895
|
+
).resolves.not.toThrow();
|
|
896
|
+
|
|
897
|
+
await expect(
|
|
898
|
+
store.alterTable({
|
|
899
|
+
tableName: TEST_TABLE as TABLE_NAMES,
|
|
900
|
+
schema: { ...BASE_SCHEMA, jsonb_column: { type: 'jsonb', nullable: false } },
|
|
901
|
+
ifNotExists: ['jsonb_column'],
|
|
902
|
+
}),
|
|
903
|
+
).resolves.not.toThrow();
|
|
904
|
+
});
|
|
905
|
+
});
|
|
906
|
+
|
|
845
907
|
afterAll(async () => {
|
|
846
908
|
await store.close();
|
|
847
909
|
});
|
package/src/storage/index.ts
CHANGED
|
@@ -371,6 +371,73 @@ export class ClickhouseStore extends MastraStorage {
|
|
|
371
371
|
}
|
|
372
372
|
}
|
|
373
373
|
|
|
374
|
+
protected getSqlType(type: StorageColumn['type']): string {
|
|
375
|
+
switch (type) {
|
|
376
|
+
case 'text':
|
|
377
|
+
return 'String';
|
|
378
|
+
case 'timestamp':
|
|
379
|
+
return 'DateTime64(3)';
|
|
380
|
+
case 'integer':
|
|
381
|
+
case 'bigint':
|
|
382
|
+
return 'Int64';
|
|
383
|
+
case 'jsonb':
|
|
384
|
+
return 'String';
|
|
385
|
+
default:
|
|
386
|
+
return super.getSqlType(type); // fallback to base implementation
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Alters table schema to add columns if they don't exist
|
|
392
|
+
* @param tableName Name of the table
|
|
393
|
+
* @param schema Schema of the table
|
|
394
|
+
* @param ifNotExists Array of column names to add if they don't exist
|
|
395
|
+
*/
|
|
396
|
+
async alterTable({
|
|
397
|
+
tableName,
|
|
398
|
+
schema,
|
|
399
|
+
ifNotExists,
|
|
400
|
+
}: {
|
|
401
|
+
tableName: TABLE_NAMES;
|
|
402
|
+
schema: Record<string, StorageColumn>;
|
|
403
|
+
ifNotExists: string[];
|
|
404
|
+
}): Promise<void> {
|
|
405
|
+
try {
|
|
406
|
+
// 1. Get existing columns
|
|
407
|
+
const describeSql = `DESCRIBE TABLE ${tableName}`;
|
|
408
|
+
const result = await this.db.query({
|
|
409
|
+
query: describeSql,
|
|
410
|
+
});
|
|
411
|
+
const rows = await result.json();
|
|
412
|
+
const existingColumnNames = new Set(rows.data.map((row: any) => row.name.toLowerCase()));
|
|
413
|
+
|
|
414
|
+
// 2. Add missing columns
|
|
415
|
+
for (const columnName of ifNotExists) {
|
|
416
|
+
if (!existingColumnNames.has(columnName.toLowerCase()) && schema[columnName]) {
|
|
417
|
+
const columnDef = schema[columnName];
|
|
418
|
+
let sqlType = this.getSqlType(columnDef.type);
|
|
419
|
+
if (columnDef.nullable !== false) {
|
|
420
|
+
sqlType = `Nullable(${sqlType})`;
|
|
421
|
+
}
|
|
422
|
+
const defaultValue = columnDef.nullable === false ? this.getDefaultValue(columnDef.type) : '';
|
|
423
|
+
// Use backticks or double quotes as needed for identifiers
|
|
424
|
+
const alterSql =
|
|
425
|
+
`ALTER TABLE ${tableName} ADD COLUMN IF NOT EXISTS "${columnName}" ${sqlType} ${defaultValue}`.trim();
|
|
426
|
+
|
|
427
|
+
await this.db.query({
|
|
428
|
+
query: alterSql,
|
|
429
|
+
});
|
|
430
|
+
this.logger?.debug?.(`Added column ${columnName} to table ${tableName}`);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
} catch (error) {
|
|
434
|
+
this.logger?.error?.(
|
|
435
|
+
`Error altering table ${tableName}: ${error instanceof Error ? error.message : String(error)}`,
|
|
436
|
+
);
|
|
437
|
+
throw new Error(`Failed to alter table ${tableName}: ${error}`);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
374
441
|
async clearTable({ tableName }: { tableName: TABLE_NAMES }): Promise<void> {
|
|
375
442
|
try {
|
|
376
443
|
await this.db.query({
|