@mastra/clickhouse 0.12.0 → 0.12.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.
- package/.turbo/turbo-build.log +7 -7
- package/CHANGELOG.md +53 -0
- package/LICENSE.md +12 -4
- package/dist/_tsup-dts-rollup.d.cts +351 -64
- package/dist/_tsup-dts-rollup.d.ts +351 -64
- package/dist/index.cjs +2052 -609
- package/dist/index.d.cts +0 -2
- package/dist/index.d.ts +0 -2
- package/dist/index.js +2051 -606
- package/package.json +6 -6
- package/src/storage/domains/legacy-evals/index.ts +246 -0
- package/src/storage/domains/memory/index.ts +1393 -0
- package/src/storage/domains/operations/index.ts +319 -0
- package/src/storage/domains/scores/index.ts +326 -0
- package/src/storage/domains/traces/index.ts +275 -0
- package/src/storage/domains/utils.ts +86 -0
- package/src/storage/domains/workflows/index.ts +285 -0
- package/src/storage/index.test.ts +15 -1091
- package/src/storage/index.ts +184 -1246
|
@@ -0,0 +1,1393 @@
|
|
|
1
|
+
import type { ClickHouseClient } from '@clickhouse/client';
|
|
2
|
+
import { MessageList } from '@mastra/core/agent';
|
|
3
|
+
import type { MastraMessageContentV2 } from '@mastra/core/agent';
|
|
4
|
+
import { ErrorCategory, ErrorDomain, MastraError } from '@mastra/core/error';
|
|
5
|
+
import type { MastraMessageV1, MastraMessageV2, StorageThreadType } from '@mastra/core/memory';
|
|
6
|
+
import type { PaginationInfo, StorageGetMessagesArg, StorageResourceType } from '@mastra/core/storage';
|
|
7
|
+
import {
|
|
8
|
+
MemoryStorage,
|
|
9
|
+
resolveMessageLimit,
|
|
10
|
+
TABLE_MESSAGES,
|
|
11
|
+
TABLE_RESOURCES,
|
|
12
|
+
TABLE_THREADS,
|
|
13
|
+
} from '@mastra/core/storage';
|
|
14
|
+
import type { StoreOperationsClickhouse } from '../operations';
|
|
15
|
+
import { transformRow, transformRows } from '../utils';
|
|
16
|
+
|
|
17
|
+
export class MemoryStorageClickhouse extends MemoryStorage {
|
|
18
|
+
protected client: ClickHouseClient;
|
|
19
|
+
protected operations: StoreOperationsClickhouse;
|
|
20
|
+
constructor({ client, operations }: { client: ClickHouseClient; operations: StoreOperationsClickhouse }) {
|
|
21
|
+
super();
|
|
22
|
+
this.client = client;
|
|
23
|
+
this.operations = operations;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
public async getMessages(args: StorageGetMessagesArg & { format?: 'v1' }): Promise<MastraMessageV1[]>;
|
|
27
|
+
public async getMessages(args: StorageGetMessagesArg & { format: 'v2' }): Promise<MastraMessageV2[]>;
|
|
28
|
+
public async getMessages({
|
|
29
|
+
threadId,
|
|
30
|
+
resourceId,
|
|
31
|
+
selectBy,
|
|
32
|
+
format,
|
|
33
|
+
}: StorageGetMessagesArg & { format?: 'v1' | 'v2' }): Promise<MastraMessageV1[] | MastraMessageV2[]> {
|
|
34
|
+
try {
|
|
35
|
+
const messages: any[] = [];
|
|
36
|
+
const limit = resolveMessageLimit({ last: selectBy?.last, defaultLimit: 40 });
|
|
37
|
+
const include = selectBy?.include || [];
|
|
38
|
+
|
|
39
|
+
if (include.length) {
|
|
40
|
+
const unionQueries: string[] = [];
|
|
41
|
+
const params: any[] = [];
|
|
42
|
+
let paramIdx = 1;
|
|
43
|
+
|
|
44
|
+
for (const inc of include) {
|
|
45
|
+
const { id, withPreviousMessages = 0, withNextMessages = 0 } = inc;
|
|
46
|
+
// if threadId is provided, use it, otherwise use threadId from args
|
|
47
|
+
const searchId = inc.threadId || threadId;
|
|
48
|
+
|
|
49
|
+
unionQueries.push(`
|
|
50
|
+
SELECT * FROM (
|
|
51
|
+
WITH numbered_messages AS (
|
|
52
|
+
SELECT
|
|
53
|
+
id, content, role, type, "createdAt", thread_id, "resourceId",
|
|
54
|
+
ROW_NUMBER() OVER (ORDER BY "createdAt" ASC) as row_num
|
|
55
|
+
FROM "${TABLE_MESSAGES}"
|
|
56
|
+
WHERE thread_id = {var_thread_id_${paramIdx}:String}
|
|
57
|
+
),
|
|
58
|
+
target_positions AS (
|
|
59
|
+
SELECT row_num as target_pos
|
|
60
|
+
FROM numbered_messages
|
|
61
|
+
WHERE id = {var_include_id_${paramIdx}:String}
|
|
62
|
+
)
|
|
63
|
+
SELECT DISTINCT m.id, m.content, m.role, m.type, m."createdAt", m.thread_id AS "threadId"
|
|
64
|
+
FROM numbered_messages m
|
|
65
|
+
CROSS JOIN target_positions t
|
|
66
|
+
WHERE m.row_num BETWEEN (t.target_pos - {var_withPreviousMessages_${paramIdx}:Int64}) AND (t.target_pos + {var_withNextMessages_${paramIdx}:Int64})
|
|
67
|
+
) AS query_${paramIdx}
|
|
68
|
+
`);
|
|
69
|
+
|
|
70
|
+
params.push(
|
|
71
|
+
{ [`var_thread_id_${paramIdx}`]: searchId },
|
|
72
|
+
{ [`var_include_id_${paramIdx}`]: id },
|
|
73
|
+
{ [`var_withPreviousMessages_${paramIdx}`]: withPreviousMessages },
|
|
74
|
+
{ [`var_withNextMessages_${paramIdx}`]: withNextMessages },
|
|
75
|
+
);
|
|
76
|
+
paramIdx++;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const finalQuery = unionQueries.join(' UNION ALL ') + ' ORDER BY "createdAt" DESC';
|
|
80
|
+
|
|
81
|
+
// Merge all parameter objects
|
|
82
|
+
const mergedParams = params.reduce((acc, paramObj) => ({ ...acc, ...paramObj }), {});
|
|
83
|
+
|
|
84
|
+
const includeResult = await this.client.query({
|
|
85
|
+
query: finalQuery,
|
|
86
|
+
query_params: mergedParams,
|
|
87
|
+
clickhouse_settings: {
|
|
88
|
+
date_time_input_format: 'best_effort',
|
|
89
|
+
date_time_output_format: 'iso',
|
|
90
|
+
use_client_time_zone: 1,
|
|
91
|
+
output_format_json_quote_64bit_integers: 0,
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const rows = await includeResult.json();
|
|
96
|
+
const includedMessages = transformRows(rows.data);
|
|
97
|
+
|
|
98
|
+
// Deduplicate messages
|
|
99
|
+
const seen = new Set<string>();
|
|
100
|
+
const dedupedMessages = includedMessages.filter((message: any) => {
|
|
101
|
+
if (seen.has(message.id)) return false;
|
|
102
|
+
seen.add(message.id);
|
|
103
|
+
return true;
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
messages.push(...dedupedMessages);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Then get the remaining messages, excluding the ids we just fetched
|
|
110
|
+
const result = await this.client.query({
|
|
111
|
+
query: `
|
|
112
|
+
SELECT
|
|
113
|
+
id,
|
|
114
|
+
content,
|
|
115
|
+
role,
|
|
116
|
+
type,
|
|
117
|
+
toDateTime64(createdAt, 3) as createdAt,
|
|
118
|
+
thread_id AS "threadId"
|
|
119
|
+
FROM "${TABLE_MESSAGES}"
|
|
120
|
+
WHERE thread_id = {threadId:String}
|
|
121
|
+
AND id NOT IN ({exclude:Array(String)})
|
|
122
|
+
ORDER BY "createdAt" DESC
|
|
123
|
+
LIMIT {limit:Int64}
|
|
124
|
+
`,
|
|
125
|
+
query_params: {
|
|
126
|
+
threadId,
|
|
127
|
+
exclude: messages.map(m => m.id),
|
|
128
|
+
limit,
|
|
129
|
+
},
|
|
130
|
+
clickhouse_settings: {
|
|
131
|
+
// Allows to insert serialized JS Dates (such as '2023-12-06T10:54:48.000Z')
|
|
132
|
+
date_time_input_format: 'best_effort',
|
|
133
|
+
date_time_output_format: 'iso',
|
|
134
|
+
use_client_time_zone: 1,
|
|
135
|
+
output_format_json_quote_64bit_integers: 0,
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const rows = await result.json();
|
|
140
|
+
messages.push(...transformRows(rows.data));
|
|
141
|
+
|
|
142
|
+
// Sort all messages by creation date
|
|
143
|
+
messages.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
|
|
144
|
+
|
|
145
|
+
// Parse message content
|
|
146
|
+
messages.forEach(message => {
|
|
147
|
+
if (typeof message.content === 'string') {
|
|
148
|
+
try {
|
|
149
|
+
message.content = JSON.parse(message.content);
|
|
150
|
+
} catch {
|
|
151
|
+
// If parsing fails, leave as string
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const list = new MessageList({ threadId, resourceId }).add(messages, 'memory');
|
|
157
|
+
if (format === `v2`) return list.get.all.v2();
|
|
158
|
+
return list.get.all.v1();
|
|
159
|
+
} catch (error) {
|
|
160
|
+
throw new MastraError(
|
|
161
|
+
{
|
|
162
|
+
id: 'CLICKHOUSE_STORAGE_GET_MESSAGES_FAILED',
|
|
163
|
+
domain: ErrorDomain.STORAGE,
|
|
164
|
+
category: ErrorCategory.THIRD_PARTY,
|
|
165
|
+
details: { threadId, resourceId: resourceId ?? '' },
|
|
166
|
+
},
|
|
167
|
+
error,
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async saveMessages(args: { messages: MastraMessageV1[]; format?: undefined | 'v1' }): Promise<MastraMessageV1[]>;
|
|
173
|
+
async saveMessages(args: { messages: MastraMessageV2[]; format: 'v2' }): Promise<MastraMessageV2[]>;
|
|
174
|
+
async saveMessages(
|
|
175
|
+
args: { messages: MastraMessageV1[]; format?: undefined | 'v1' } | { messages: MastraMessageV2[]; format: 'v2' },
|
|
176
|
+
): Promise<MastraMessageV2[] | MastraMessageV1[]> {
|
|
177
|
+
const { messages, format = 'v1' } = args;
|
|
178
|
+
if (messages.length === 0) return messages;
|
|
179
|
+
|
|
180
|
+
for (const message of messages) {
|
|
181
|
+
const resourceId = message.resourceId;
|
|
182
|
+
if (!resourceId) {
|
|
183
|
+
throw new Error('Resource ID is required');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (!message.threadId) {
|
|
187
|
+
throw new Error('Thread ID is required');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Check if thread exists
|
|
191
|
+
const thread = await this.getThreadById({ threadId: message.threadId });
|
|
192
|
+
if (!thread) {
|
|
193
|
+
throw new Error(`Thread ${message.threadId} not found`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const threadIdSet = new Map();
|
|
198
|
+
|
|
199
|
+
await Promise.all(
|
|
200
|
+
messages.map(async m => {
|
|
201
|
+
const resourceId = m.resourceId;
|
|
202
|
+
if (!resourceId) {
|
|
203
|
+
throw new Error('Resource ID is required');
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (!m.threadId) {
|
|
207
|
+
throw new Error('Thread ID is required');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Check if thread exists
|
|
211
|
+
const thread = await this.getThreadById({ threadId: m.threadId });
|
|
212
|
+
if (!thread) {
|
|
213
|
+
throw new Error(`Thread ${m.threadId} not found`);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
threadIdSet.set(m.threadId, thread);
|
|
217
|
+
}),
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
try {
|
|
221
|
+
// Clickhouse's MergeTree engine does not support native upserts or unique constraints on (id, thread_id).
|
|
222
|
+
// Note: We cannot switch to ReplacingMergeTree without a schema migration,
|
|
223
|
+
// as it would require altering the table engine.
|
|
224
|
+
// To ensure correct upsert behavior, we first fetch existing (id, thread_id) pairs for the incoming messages.
|
|
225
|
+
const existingResult = await this.client.query({
|
|
226
|
+
query: `SELECT id, thread_id FROM ${TABLE_MESSAGES} WHERE id IN ({ids:Array(String)})`,
|
|
227
|
+
query_params: {
|
|
228
|
+
ids: messages.map(m => m.id),
|
|
229
|
+
},
|
|
230
|
+
clickhouse_settings: {
|
|
231
|
+
// Allows to insert serialized JS Dates (such as '2023-12-06T10:54:48.000Z')
|
|
232
|
+
date_time_input_format: 'best_effort',
|
|
233
|
+
date_time_output_format: 'iso',
|
|
234
|
+
use_client_time_zone: 1,
|
|
235
|
+
output_format_json_quote_64bit_integers: 0,
|
|
236
|
+
},
|
|
237
|
+
format: 'JSONEachRow',
|
|
238
|
+
});
|
|
239
|
+
const existingRows: Array<{ id: string; thread_id: string }> = await existingResult.json();
|
|
240
|
+
|
|
241
|
+
const existingSet = new Set(existingRows.map(row => `${row.id}::${row.thread_id}`));
|
|
242
|
+
|
|
243
|
+
// Partition the batch into different operations:
|
|
244
|
+
// 1. New messages (insert)
|
|
245
|
+
// 2. Existing messages with same (id, threadId) (update)
|
|
246
|
+
// 3. Messages with same id but different threadId (delete old + insert new)
|
|
247
|
+
const toInsert = messages.filter(m => !existingSet.has(`${m.id}::${m.threadId}`));
|
|
248
|
+
const toUpdate = messages.filter(m => existingSet.has(`${m.id}::${m.threadId}`));
|
|
249
|
+
|
|
250
|
+
// Find messages that need to be moved (same id, different threadId)
|
|
251
|
+
const toMove = messages.filter(m => {
|
|
252
|
+
const existingRow = existingRows.find(row => row.id === m.id);
|
|
253
|
+
return existingRow && existingRow.thread_id !== m.threadId;
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// Delete old messages that are being moved
|
|
257
|
+
const deletePromises = toMove.map(message => {
|
|
258
|
+
const existingRow = existingRows.find(row => row.id === message.id);
|
|
259
|
+
if (!existingRow) return Promise.resolve();
|
|
260
|
+
|
|
261
|
+
return this.client.command({
|
|
262
|
+
query: `DELETE FROM ${TABLE_MESSAGES} WHERE id = {var_id:String} AND thread_id = {var_old_thread_id:String}`,
|
|
263
|
+
query_params: {
|
|
264
|
+
var_id: message.id,
|
|
265
|
+
var_old_thread_id: existingRow.thread_id,
|
|
266
|
+
},
|
|
267
|
+
clickhouse_settings: {
|
|
268
|
+
date_time_input_format: 'best_effort',
|
|
269
|
+
use_client_time_zone: 1,
|
|
270
|
+
output_format_json_quote_64bit_integers: 0,
|
|
271
|
+
},
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
const updatePromises = toUpdate.map(message =>
|
|
276
|
+
this.client.command({
|
|
277
|
+
query: `
|
|
278
|
+
ALTER TABLE ${TABLE_MESSAGES}
|
|
279
|
+
UPDATE content = {var_content:String}, role = {var_role:String}, type = {var_type:String}, resourceId = {var_resourceId:String}
|
|
280
|
+
WHERE id = {var_id:String} AND thread_id = {var_thread_id:String}
|
|
281
|
+
`,
|
|
282
|
+
query_params: {
|
|
283
|
+
var_content: typeof message.content === 'string' ? message.content : JSON.stringify(message.content),
|
|
284
|
+
var_role: message.role,
|
|
285
|
+
var_type: message.type || 'v2',
|
|
286
|
+
var_resourceId: message.resourceId,
|
|
287
|
+
var_id: message.id,
|
|
288
|
+
var_thread_id: message.threadId,
|
|
289
|
+
},
|
|
290
|
+
clickhouse_settings: {
|
|
291
|
+
// Allows to insert serialized JS Dates (such as '2023-12-06T10:54:48.000Z')
|
|
292
|
+
date_time_input_format: 'best_effort',
|
|
293
|
+
use_client_time_zone: 1,
|
|
294
|
+
output_format_json_quote_64bit_integers: 0,
|
|
295
|
+
},
|
|
296
|
+
}),
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
// Execute message operations and thread update in parallel for better performance
|
|
300
|
+
await Promise.all([
|
|
301
|
+
// Insert new messages (including moved messages)
|
|
302
|
+
this.client.insert({
|
|
303
|
+
table: TABLE_MESSAGES,
|
|
304
|
+
format: 'JSONEachRow',
|
|
305
|
+
values: toInsert.map(message => ({
|
|
306
|
+
id: message.id,
|
|
307
|
+
thread_id: message.threadId,
|
|
308
|
+
resourceId: message.resourceId,
|
|
309
|
+
content: typeof message.content === 'string' ? message.content : JSON.stringify(message.content),
|
|
310
|
+
createdAt: message.createdAt.toISOString(),
|
|
311
|
+
role: message.role,
|
|
312
|
+
type: message.type || 'v2',
|
|
313
|
+
})),
|
|
314
|
+
clickhouse_settings: {
|
|
315
|
+
// Allows to insert serialized JS Dates (such as '2023-12-06T10:54:48.000Z')
|
|
316
|
+
date_time_input_format: 'best_effort',
|
|
317
|
+
use_client_time_zone: 1,
|
|
318
|
+
output_format_json_quote_64bit_integers: 0,
|
|
319
|
+
},
|
|
320
|
+
}),
|
|
321
|
+
...updatePromises,
|
|
322
|
+
...deletePromises,
|
|
323
|
+
// Update thread's updatedAt timestamp
|
|
324
|
+
this.client.insert({
|
|
325
|
+
table: TABLE_THREADS,
|
|
326
|
+
format: 'JSONEachRow',
|
|
327
|
+
values: Array.from(threadIdSet.values()).map(thread => ({
|
|
328
|
+
id: thread.id,
|
|
329
|
+
resourceId: thread.resourceId,
|
|
330
|
+
title: thread.title,
|
|
331
|
+
metadata: thread.metadata,
|
|
332
|
+
createdAt: thread.createdAt,
|
|
333
|
+
updatedAt: new Date().toISOString(),
|
|
334
|
+
})),
|
|
335
|
+
clickhouse_settings: {
|
|
336
|
+
date_time_input_format: 'best_effort',
|
|
337
|
+
use_client_time_zone: 1,
|
|
338
|
+
output_format_json_quote_64bit_integers: 0,
|
|
339
|
+
},
|
|
340
|
+
}),
|
|
341
|
+
]);
|
|
342
|
+
|
|
343
|
+
const list = new MessageList().add(messages, 'memory');
|
|
344
|
+
|
|
345
|
+
if (format === `v2`) return list.get.all.v2();
|
|
346
|
+
return list.get.all.v1();
|
|
347
|
+
} catch (error: any) {
|
|
348
|
+
throw new MastraError(
|
|
349
|
+
{
|
|
350
|
+
id: 'CLICKHOUSE_STORAGE_SAVE_MESSAGES_FAILED',
|
|
351
|
+
domain: ErrorDomain.STORAGE,
|
|
352
|
+
category: ErrorCategory.THIRD_PARTY,
|
|
353
|
+
},
|
|
354
|
+
error,
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
async getThreadById({ threadId }: { threadId: string }): Promise<StorageThreadType | null> {
|
|
360
|
+
try {
|
|
361
|
+
const result = await this.client.query({
|
|
362
|
+
query: `SELECT
|
|
363
|
+
id,
|
|
364
|
+
"resourceId",
|
|
365
|
+
title,
|
|
366
|
+
metadata,
|
|
367
|
+
toDateTime64(createdAt, 3) as createdAt,
|
|
368
|
+
toDateTime64(updatedAt, 3) as updatedAt
|
|
369
|
+
FROM "${TABLE_THREADS}"
|
|
370
|
+
FINAL
|
|
371
|
+
WHERE id = {var_id:String}`,
|
|
372
|
+
query_params: { var_id: threadId },
|
|
373
|
+
clickhouse_settings: {
|
|
374
|
+
// Allows to insert serialized JS Dates (such as '2023-12-06T10:54:48.000Z')
|
|
375
|
+
date_time_input_format: 'best_effort',
|
|
376
|
+
date_time_output_format: 'iso',
|
|
377
|
+
use_client_time_zone: 1,
|
|
378
|
+
output_format_json_quote_64bit_integers: 0,
|
|
379
|
+
},
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
const rows = await result.json();
|
|
383
|
+
const thread = transformRow(rows.data[0]) as StorageThreadType;
|
|
384
|
+
|
|
385
|
+
if (!thread) {
|
|
386
|
+
return null;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return {
|
|
390
|
+
...thread,
|
|
391
|
+
metadata: typeof thread.metadata === 'string' ? JSON.parse(thread.metadata) : thread.metadata,
|
|
392
|
+
createdAt: thread.createdAt,
|
|
393
|
+
updatedAt: thread.updatedAt,
|
|
394
|
+
};
|
|
395
|
+
} catch (error: any) {
|
|
396
|
+
throw new MastraError(
|
|
397
|
+
{
|
|
398
|
+
id: 'CLICKHOUSE_STORAGE_GET_THREAD_BY_ID_FAILED',
|
|
399
|
+
domain: ErrorDomain.STORAGE,
|
|
400
|
+
category: ErrorCategory.THIRD_PARTY,
|
|
401
|
+
details: { threadId },
|
|
402
|
+
},
|
|
403
|
+
error,
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
async getThreadsByResourceId({ resourceId }: { resourceId: string }): Promise<StorageThreadType[]> {
|
|
409
|
+
try {
|
|
410
|
+
const result = await this.client.query({
|
|
411
|
+
query: `SELECT
|
|
412
|
+
id,
|
|
413
|
+
"resourceId",
|
|
414
|
+
title,
|
|
415
|
+
metadata,
|
|
416
|
+
toDateTime64(createdAt, 3) as createdAt,
|
|
417
|
+
toDateTime64(updatedAt, 3) as updatedAt
|
|
418
|
+
FROM "${TABLE_THREADS}"
|
|
419
|
+
WHERE "resourceId" = {var_resourceId:String}`,
|
|
420
|
+
query_params: { var_resourceId: resourceId },
|
|
421
|
+
clickhouse_settings: {
|
|
422
|
+
// Allows to insert serialized JS Dates (such as '2023-12-06T10:54:48.000Z')
|
|
423
|
+
date_time_input_format: 'best_effort',
|
|
424
|
+
date_time_output_format: 'iso',
|
|
425
|
+
use_client_time_zone: 1,
|
|
426
|
+
output_format_json_quote_64bit_integers: 0,
|
|
427
|
+
},
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
const rows = await result.json();
|
|
431
|
+
const threads = transformRows(rows.data) as StorageThreadType[];
|
|
432
|
+
|
|
433
|
+
return threads.map((thread: StorageThreadType) => ({
|
|
434
|
+
...thread,
|
|
435
|
+
metadata: typeof thread.metadata === 'string' ? JSON.parse(thread.metadata) : thread.metadata,
|
|
436
|
+
createdAt: thread.createdAt,
|
|
437
|
+
updatedAt: thread.updatedAt,
|
|
438
|
+
}));
|
|
439
|
+
} catch (error) {
|
|
440
|
+
throw new MastraError(
|
|
441
|
+
{
|
|
442
|
+
id: 'CLICKHOUSE_STORAGE_GET_THREADS_BY_RESOURCE_ID_FAILED',
|
|
443
|
+
domain: ErrorDomain.STORAGE,
|
|
444
|
+
category: ErrorCategory.THIRD_PARTY,
|
|
445
|
+
details: { resourceId },
|
|
446
|
+
},
|
|
447
|
+
error,
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
async saveThread({ thread }: { thread: StorageThreadType }): Promise<StorageThreadType> {
|
|
453
|
+
try {
|
|
454
|
+
await this.client.insert({
|
|
455
|
+
table: TABLE_THREADS,
|
|
456
|
+
values: [
|
|
457
|
+
{
|
|
458
|
+
...thread,
|
|
459
|
+
createdAt: thread.createdAt.toISOString(),
|
|
460
|
+
updatedAt: thread.updatedAt.toISOString(),
|
|
461
|
+
},
|
|
462
|
+
],
|
|
463
|
+
format: 'JSONEachRow',
|
|
464
|
+
clickhouse_settings: {
|
|
465
|
+
// Allows to insert serialized JS Dates (such as '2023-12-06T10:54:48.000Z')
|
|
466
|
+
date_time_input_format: 'best_effort',
|
|
467
|
+
use_client_time_zone: 1,
|
|
468
|
+
output_format_json_quote_64bit_integers: 0,
|
|
469
|
+
},
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
return thread;
|
|
473
|
+
} catch (error) {
|
|
474
|
+
throw new MastraError(
|
|
475
|
+
{
|
|
476
|
+
id: 'CLICKHOUSE_STORAGE_SAVE_THREAD_FAILED',
|
|
477
|
+
domain: ErrorDomain.STORAGE,
|
|
478
|
+
category: ErrorCategory.THIRD_PARTY,
|
|
479
|
+
details: { threadId: thread.id },
|
|
480
|
+
},
|
|
481
|
+
error,
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
async updateThread({
|
|
487
|
+
id,
|
|
488
|
+
title,
|
|
489
|
+
metadata,
|
|
490
|
+
}: {
|
|
491
|
+
id: string;
|
|
492
|
+
title: string;
|
|
493
|
+
metadata: Record<string, unknown>;
|
|
494
|
+
}): Promise<StorageThreadType> {
|
|
495
|
+
try {
|
|
496
|
+
// First get the existing thread to merge metadata
|
|
497
|
+
const existingThread = await this.getThreadById({ threadId: id });
|
|
498
|
+
if (!existingThread) {
|
|
499
|
+
throw new Error(`Thread ${id} not found`);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Merge the existing metadata with the new metadata
|
|
503
|
+
const mergedMetadata = {
|
|
504
|
+
...existingThread.metadata,
|
|
505
|
+
...metadata,
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
const updatedThread = {
|
|
509
|
+
...existingThread,
|
|
510
|
+
title,
|
|
511
|
+
metadata: mergedMetadata,
|
|
512
|
+
updatedAt: new Date(),
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
await this.client.insert({
|
|
516
|
+
table: TABLE_THREADS,
|
|
517
|
+
format: 'JSONEachRow',
|
|
518
|
+
values: [
|
|
519
|
+
{
|
|
520
|
+
id: updatedThread.id,
|
|
521
|
+
resourceId: updatedThread.resourceId,
|
|
522
|
+
title: updatedThread.title,
|
|
523
|
+
metadata: updatedThread.metadata,
|
|
524
|
+
createdAt: updatedThread.createdAt,
|
|
525
|
+
updatedAt: updatedThread.updatedAt.toISOString(),
|
|
526
|
+
},
|
|
527
|
+
],
|
|
528
|
+
clickhouse_settings: {
|
|
529
|
+
date_time_input_format: 'best_effort',
|
|
530
|
+
use_client_time_zone: 1,
|
|
531
|
+
output_format_json_quote_64bit_integers: 0,
|
|
532
|
+
},
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
return updatedThread;
|
|
536
|
+
} catch (error) {
|
|
537
|
+
throw new MastraError(
|
|
538
|
+
{
|
|
539
|
+
id: 'CLICKHOUSE_STORAGE_UPDATE_THREAD_FAILED',
|
|
540
|
+
domain: ErrorDomain.STORAGE,
|
|
541
|
+
category: ErrorCategory.THIRD_PARTY,
|
|
542
|
+
details: { threadId: id, title },
|
|
543
|
+
},
|
|
544
|
+
error,
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
async deleteThread({ threadId }: { threadId: string }): Promise<void> {
|
|
550
|
+
try {
|
|
551
|
+
// First delete all messages associated with this thread
|
|
552
|
+
await this.client.command({
|
|
553
|
+
query: `DELETE FROM "${TABLE_MESSAGES}" WHERE thread_id = {var_thread_id:String};`,
|
|
554
|
+
query_params: { var_thread_id: threadId },
|
|
555
|
+
clickhouse_settings: {
|
|
556
|
+
output_format_json_quote_64bit_integers: 0,
|
|
557
|
+
},
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
// Then delete the thread
|
|
561
|
+
await this.client.command({
|
|
562
|
+
query: `DELETE FROM "${TABLE_THREADS}" WHERE id = {var_id:String};`,
|
|
563
|
+
query_params: { var_id: threadId },
|
|
564
|
+
clickhouse_settings: {
|
|
565
|
+
output_format_json_quote_64bit_integers: 0,
|
|
566
|
+
},
|
|
567
|
+
});
|
|
568
|
+
} catch (error) {
|
|
569
|
+
throw new MastraError(
|
|
570
|
+
{
|
|
571
|
+
id: 'CLICKHOUSE_STORAGE_DELETE_THREAD_FAILED',
|
|
572
|
+
domain: ErrorDomain.STORAGE,
|
|
573
|
+
category: ErrorCategory.THIRD_PARTY,
|
|
574
|
+
details: { threadId },
|
|
575
|
+
},
|
|
576
|
+
error,
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
async getThreadsByResourceIdPaginated(args: {
|
|
582
|
+
resourceId: string;
|
|
583
|
+
page?: number;
|
|
584
|
+
perPage?: number;
|
|
585
|
+
}): Promise<PaginationInfo & { threads: StorageThreadType[] }> {
|
|
586
|
+
const { resourceId, page = 0, perPage = 100 } = args;
|
|
587
|
+
|
|
588
|
+
try {
|
|
589
|
+
const currentOffset = page * perPage;
|
|
590
|
+
|
|
591
|
+
// Get total count
|
|
592
|
+
const countResult = await this.client.query({
|
|
593
|
+
query: `SELECT count() as total FROM ${TABLE_THREADS} WHERE resourceId = {resourceId:String}`,
|
|
594
|
+
query_params: { resourceId },
|
|
595
|
+
clickhouse_settings: {
|
|
596
|
+
date_time_input_format: 'best_effort',
|
|
597
|
+
date_time_output_format: 'iso',
|
|
598
|
+
use_client_time_zone: 1,
|
|
599
|
+
output_format_json_quote_64bit_integers: 0,
|
|
600
|
+
},
|
|
601
|
+
});
|
|
602
|
+
const countData = await countResult.json();
|
|
603
|
+
const total = (countData as any).data[0].total;
|
|
604
|
+
|
|
605
|
+
if (total === 0) {
|
|
606
|
+
return {
|
|
607
|
+
threads: [],
|
|
608
|
+
total: 0,
|
|
609
|
+
page,
|
|
610
|
+
perPage,
|
|
611
|
+
hasMore: false,
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// Get paginated threads
|
|
616
|
+
const dataResult = await this.client.query({
|
|
617
|
+
query: `
|
|
618
|
+
SELECT
|
|
619
|
+
id,
|
|
620
|
+
resourceId,
|
|
621
|
+
title,
|
|
622
|
+
metadata,
|
|
623
|
+
toDateTime64(createdAt, 3) as createdAt,
|
|
624
|
+
toDateTime64(updatedAt, 3) as updatedAt
|
|
625
|
+
FROM ${TABLE_THREADS}
|
|
626
|
+
WHERE resourceId = {resourceId:String}
|
|
627
|
+
ORDER BY createdAt DESC
|
|
628
|
+
LIMIT {limit:Int64} OFFSET {offset:Int64}
|
|
629
|
+
`,
|
|
630
|
+
query_params: {
|
|
631
|
+
resourceId,
|
|
632
|
+
limit: perPage,
|
|
633
|
+
offset: currentOffset,
|
|
634
|
+
},
|
|
635
|
+
clickhouse_settings: {
|
|
636
|
+
date_time_input_format: 'best_effort',
|
|
637
|
+
date_time_output_format: 'iso',
|
|
638
|
+
use_client_time_zone: 1,
|
|
639
|
+
output_format_json_quote_64bit_integers: 0,
|
|
640
|
+
},
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
const rows = await dataResult.json();
|
|
644
|
+
const threads = transformRows<StorageThreadType>(rows.data);
|
|
645
|
+
|
|
646
|
+
return {
|
|
647
|
+
threads,
|
|
648
|
+
total,
|
|
649
|
+
page,
|
|
650
|
+
perPage,
|
|
651
|
+
hasMore: currentOffset + threads.length < total,
|
|
652
|
+
};
|
|
653
|
+
} catch (error) {
|
|
654
|
+
throw new MastraError(
|
|
655
|
+
{
|
|
656
|
+
id: 'CLICKHOUSE_STORAGE_GET_THREADS_BY_RESOURCE_ID_PAGINATED_FAILED',
|
|
657
|
+
domain: ErrorDomain.STORAGE,
|
|
658
|
+
category: ErrorCategory.THIRD_PARTY,
|
|
659
|
+
details: { resourceId, page },
|
|
660
|
+
},
|
|
661
|
+
error,
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
async getMessagesPaginated(
|
|
667
|
+
args: StorageGetMessagesArg & { format?: 'v1' | 'v2' },
|
|
668
|
+
): Promise<PaginationInfo & { messages: MastraMessageV1[] | MastraMessageV2[] }> {
|
|
669
|
+
try {
|
|
670
|
+
const { threadId, selectBy, format = 'v1' } = args;
|
|
671
|
+
const page = selectBy?.pagination?.page || 0;
|
|
672
|
+
const perPageInput = selectBy?.pagination?.perPage;
|
|
673
|
+
const perPage =
|
|
674
|
+
perPageInput !== undefined ? perPageInput : resolveMessageLimit({ last: selectBy?.last, defaultLimit: 20 });
|
|
675
|
+
const offset = page * perPage;
|
|
676
|
+
const dateRange = selectBy?.pagination?.dateRange;
|
|
677
|
+
const fromDate = dateRange?.start;
|
|
678
|
+
const toDate = dateRange?.end;
|
|
679
|
+
|
|
680
|
+
const messages: MastraMessageV2[] = [];
|
|
681
|
+
|
|
682
|
+
// Get include messages first (like libsql)
|
|
683
|
+
if (selectBy?.include?.length) {
|
|
684
|
+
const include = selectBy.include;
|
|
685
|
+
const unionQueries: string[] = [];
|
|
686
|
+
const params: any[] = [];
|
|
687
|
+
let paramIdx = 1;
|
|
688
|
+
|
|
689
|
+
for (const inc of include) {
|
|
690
|
+
const { id, withPreviousMessages = 0, withNextMessages = 0 } = inc;
|
|
691
|
+
const searchId = inc.threadId || threadId;
|
|
692
|
+
|
|
693
|
+
unionQueries.push(`
|
|
694
|
+
SELECT * FROM (
|
|
695
|
+
WITH numbered_messages AS (
|
|
696
|
+
SELECT
|
|
697
|
+
id, content, role, type, "createdAt", thread_id, "resourceId",
|
|
698
|
+
ROW_NUMBER() OVER (ORDER BY "createdAt" ASC) as row_num
|
|
699
|
+
FROM "${TABLE_MESSAGES}"
|
|
700
|
+
WHERE thread_id = {var_thread_id_${paramIdx}:String}
|
|
701
|
+
),
|
|
702
|
+
target_positions AS (
|
|
703
|
+
SELECT row_num as target_pos
|
|
704
|
+
FROM numbered_messages
|
|
705
|
+
WHERE id = {var_include_id_${paramIdx}:String}
|
|
706
|
+
)
|
|
707
|
+
SELECT DISTINCT m.id, m.content, m.role, m.type, m."createdAt", m.thread_id AS "threadId"
|
|
708
|
+
FROM numbered_messages m
|
|
709
|
+
CROSS JOIN target_positions t
|
|
710
|
+
WHERE m.row_num BETWEEN (t.target_pos - {var_withPreviousMessages_${paramIdx}:Int64}) AND (t.target_pos + {var_withNextMessages_${paramIdx}:Int64})
|
|
711
|
+
) AS query_${paramIdx}
|
|
712
|
+
`);
|
|
713
|
+
|
|
714
|
+
params.push(
|
|
715
|
+
{ [`var_thread_id_${paramIdx}`]: searchId },
|
|
716
|
+
{ [`var_include_id_${paramIdx}`]: id },
|
|
717
|
+
{ [`var_withPreviousMessages_${paramIdx}`]: withPreviousMessages },
|
|
718
|
+
{ [`var_withNextMessages_${paramIdx}`]: withNextMessages },
|
|
719
|
+
);
|
|
720
|
+
paramIdx++;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
const finalQuery = unionQueries.join(' UNION ALL ') + ' ORDER BY "createdAt" DESC';
|
|
724
|
+
const mergedParams = params.reduce((acc, paramObj) => ({ ...acc, ...paramObj }), {});
|
|
725
|
+
|
|
726
|
+
const includeResult = await this.client.query({
|
|
727
|
+
query: finalQuery,
|
|
728
|
+
query_params: mergedParams,
|
|
729
|
+
clickhouse_settings: {
|
|
730
|
+
date_time_input_format: 'best_effort',
|
|
731
|
+
date_time_output_format: 'iso',
|
|
732
|
+
use_client_time_zone: 1,
|
|
733
|
+
output_format_json_quote_64bit_integers: 0,
|
|
734
|
+
},
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
const rows = await includeResult.json();
|
|
738
|
+
const includedMessages = transformRows<MastraMessageV2>(rows.data);
|
|
739
|
+
|
|
740
|
+
// Deduplicate messages
|
|
741
|
+
const seen = new Set<string>();
|
|
742
|
+
const dedupedMessages = includedMessages.filter((message: MastraMessageV2) => {
|
|
743
|
+
if (seen.has(message.id)) return false;
|
|
744
|
+
seen.add(message.id);
|
|
745
|
+
return true;
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
messages.push(...dedupedMessages);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// Get total count
|
|
752
|
+
let countQuery = `SELECT count() as total FROM ${TABLE_MESSAGES} WHERE thread_id = {threadId:String}`;
|
|
753
|
+
const countParams: any = { threadId };
|
|
754
|
+
|
|
755
|
+
if (fromDate) {
|
|
756
|
+
countQuery += ` AND createdAt >= parseDateTime64BestEffort({fromDate:String}, 3)`;
|
|
757
|
+
countParams.fromDate = fromDate.toISOString();
|
|
758
|
+
}
|
|
759
|
+
if (toDate) {
|
|
760
|
+
countQuery += ` AND createdAt <= parseDateTime64BestEffort({toDate:String}, 3)`;
|
|
761
|
+
countParams.toDate = toDate.toISOString();
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
const countResult = await this.client.query({
|
|
765
|
+
query: countQuery,
|
|
766
|
+
query_params: countParams,
|
|
767
|
+
clickhouse_settings: {
|
|
768
|
+
date_time_input_format: 'best_effort',
|
|
769
|
+
date_time_output_format: 'iso',
|
|
770
|
+
use_client_time_zone: 1,
|
|
771
|
+
output_format_json_quote_64bit_integers: 0,
|
|
772
|
+
},
|
|
773
|
+
});
|
|
774
|
+
const countData = await countResult.json();
|
|
775
|
+
const total = (countData as any).data[0].total;
|
|
776
|
+
|
|
777
|
+
if (total === 0 && messages.length === 0) {
|
|
778
|
+
return {
|
|
779
|
+
messages: [],
|
|
780
|
+
total: 0,
|
|
781
|
+
page,
|
|
782
|
+
perPage,
|
|
783
|
+
hasMore: false,
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
// Get regular paginated messages, excluding include message IDs
|
|
788
|
+
const excludeIds = messages.map(m => m.id);
|
|
789
|
+
let dataQuery = `
|
|
790
|
+
SELECT
|
|
791
|
+
id,
|
|
792
|
+
content,
|
|
793
|
+
role,
|
|
794
|
+
type,
|
|
795
|
+
toDateTime64(createdAt, 3) as createdAt,
|
|
796
|
+
thread_id AS "threadId",
|
|
797
|
+
resourceId
|
|
798
|
+
FROM ${TABLE_MESSAGES}
|
|
799
|
+
WHERE thread_id = {threadId:String}
|
|
800
|
+
`;
|
|
801
|
+
const dataParams: any = { threadId };
|
|
802
|
+
|
|
803
|
+
if (fromDate) {
|
|
804
|
+
dataQuery += ` AND createdAt >= parseDateTime64BestEffort({fromDate:String}, 3)`;
|
|
805
|
+
dataParams.fromDate = fromDate.toISOString();
|
|
806
|
+
}
|
|
807
|
+
if (toDate) {
|
|
808
|
+
dataQuery += ` AND createdAt <= parseDateTime64BestEffort({toDate:String}, 3)`;
|
|
809
|
+
dataParams.toDate = toDate.toISOString();
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// Exclude include message IDs
|
|
813
|
+
if (excludeIds.length > 0) {
|
|
814
|
+
dataQuery += ` AND id NOT IN ({excludeIds:Array(String)})`;
|
|
815
|
+
dataParams.excludeIds = excludeIds;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// For last N functionality, we need to get the most recent messages first, then sort them chronologically
|
|
819
|
+
if (selectBy?.last) {
|
|
820
|
+
dataQuery += `
|
|
821
|
+
ORDER BY createdAt DESC
|
|
822
|
+
LIMIT {limit:Int64}
|
|
823
|
+
`;
|
|
824
|
+
dataParams.limit = perPage;
|
|
825
|
+
} else {
|
|
826
|
+
dataQuery += `
|
|
827
|
+
ORDER BY createdAt ASC
|
|
828
|
+
LIMIT {limit:Int64} OFFSET {offset:Int64}
|
|
829
|
+
`;
|
|
830
|
+
dataParams.limit = perPage;
|
|
831
|
+
dataParams.offset = offset;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
const result = await this.client.query({
|
|
835
|
+
query: dataQuery,
|
|
836
|
+
query_params: dataParams,
|
|
837
|
+
clickhouse_settings: {
|
|
838
|
+
date_time_input_format: 'best_effort',
|
|
839
|
+
date_time_output_format: 'iso',
|
|
840
|
+
use_client_time_zone: 1,
|
|
841
|
+
output_format_json_quote_64bit_integers: 0,
|
|
842
|
+
},
|
|
843
|
+
});
|
|
844
|
+
|
|
845
|
+
const rows = await result.json();
|
|
846
|
+
const paginatedMessages = transformRows<MastraMessageV2>(rows.data);
|
|
847
|
+
messages.push(...paginatedMessages);
|
|
848
|
+
|
|
849
|
+
// For last N functionality, sort messages chronologically
|
|
850
|
+
if (selectBy?.last) {
|
|
851
|
+
messages.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
return {
|
|
855
|
+
messages: format === 'v2' ? messages : (messages as unknown as MastraMessageV1[]),
|
|
856
|
+
total,
|
|
857
|
+
page,
|
|
858
|
+
perPage,
|
|
859
|
+
hasMore: offset + perPage < total,
|
|
860
|
+
};
|
|
861
|
+
} catch (error: any) {
|
|
862
|
+
throw new MastraError(
|
|
863
|
+
{
|
|
864
|
+
id: 'CLICKHOUSE_STORAGE_GET_MESSAGES_PAGINATED_FAILED',
|
|
865
|
+
domain: ErrorDomain.STORAGE,
|
|
866
|
+
category: ErrorCategory.THIRD_PARTY,
|
|
867
|
+
},
|
|
868
|
+
error,
|
|
869
|
+
);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
async updateMessages(args: {
|
|
874
|
+
messages: (Partial<Omit<MastraMessageV2, 'createdAt'>> & {
|
|
875
|
+
id: string;
|
|
876
|
+
threadId?: string;
|
|
877
|
+
content?: { metadata?: MastraMessageContentV2['metadata']; content?: MastraMessageContentV2['content'] };
|
|
878
|
+
})[];
|
|
879
|
+
}): Promise<MastraMessageV2[]> {
|
|
880
|
+
const { messages } = args;
|
|
881
|
+
|
|
882
|
+
if (messages.length === 0) {
|
|
883
|
+
return [];
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
try {
|
|
887
|
+
const messageIds = messages.map(m => m.id);
|
|
888
|
+
|
|
889
|
+
// Get existing messages
|
|
890
|
+
const existingResult = await this.client.query({
|
|
891
|
+
query: `SELECT id, content, role, type, "createdAt", thread_id AS "threadId", "resourceId" FROM ${TABLE_MESSAGES} WHERE id IN (${messageIds.map((_, i) => `{id_${i}:String}`).join(',')})`,
|
|
892
|
+
query_params: messageIds.reduce((acc, m, i) => ({ ...acc, [`id_${i}`]: m }), {}),
|
|
893
|
+
clickhouse_settings: {
|
|
894
|
+
date_time_input_format: 'best_effort',
|
|
895
|
+
date_time_output_format: 'iso',
|
|
896
|
+
use_client_time_zone: 1,
|
|
897
|
+
output_format_json_quote_64bit_integers: 0,
|
|
898
|
+
},
|
|
899
|
+
});
|
|
900
|
+
|
|
901
|
+
const existingRows = await existingResult.json();
|
|
902
|
+
const existingMessages = transformRows<MastraMessageV2>(existingRows.data);
|
|
903
|
+
|
|
904
|
+
if (existingMessages.length === 0) {
|
|
905
|
+
return [];
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
// Parse content from string to object for merging
|
|
909
|
+
const parsedExistingMessages = existingMessages.map(msg => {
|
|
910
|
+
if (typeof msg.content === 'string') {
|
|
911
|
+
try {
|
|
912
|
+
msg.content = JSON.parse(msg.content);
|
|
913
|
+
} catch {
|
|
914
|
+
// ignore if not valid json
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
return msg;
|
|
918
|
+
});
|
|
919
|
+
|
|
920
|
+
const threadIdsToUpdate = new Set<string>();
|
|
921
|
+
const updatePromises: Promise<any>[] = [];
|
|
922
|
+
|
|
923
|
+
for (const existingMessage of parsedExistingMessages) {
|
|
924
|
+
const updatePayload = messages.find(m => m.id === existingMessage.id);
|
|
925
|
+
if (!updatePayload) continue;
|
|
926
|
+
|
|
927
|
+
const { id, ...fieldsToUpdate } = updatePayload;
|
|
928
|
+
if (Object.keys(fieldsToUpdate).length === 0) continue;
|
|
929
|
+
|
|
930
|
+
threadIdsToUpdate.add(existingMessage.threadId!);
|
|
931
|
+
if (updatePayload.threadId && updatePayload.threadId !== existingMessage.threadId) {
|
|
932
|
+
threadIdsToUpdate.add(updatePayload.threadId);
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
const setClauses: string[] = [];
|
|
936
|
+
const values: any = {};
|
|
937
|
+
let paramIdx = 1;
|
|
938
|
+
let newContent: any = null;
|
|
939
|
+
|
|
940
|
+
const updatableFields = { ...fieldsToUpdate };
|
|
941
|
+
|
|
942
|
+
// Special handling for content: merge in code, then update the whole field
|
|
943
|
+
if (updatableFields.content) {
|
|
944
|
+
const existingContent = existingMessage.content || {};
|
|
945
|
+
const existingMetadata = existingContent.metadata || {};
|
|
946
|
+
const updateMetadata = updatableFields.content.metadata || {};
|
|
947
|
+
|
|
948
|
+
newContent = {
|
|
949
|
+
...existingContent,
|
|
950
|
+
...updatableFields.content,
|
|
951
|
+
// Deep merge metadata
|
|
952
|
+
metadata: {
|
|
953
|
+
...existingMetadata,
|
|
954
|
+
...updateMetadata,
|
|
955
|
+
},
|
|
956
|
+
};
|
|
957
|
+
|
|
958
|
+
// Ensure we're updating the content field
|
|
959
|
+
setClauses.push(`content = {var_content_${paramIdx}:String}`);
|
|
960
|
+
values[`var_content_${paramIdx}`] = JSON.stringify(newContent);
|
|
961
|
+
paramIdx++;
|
|
962
|
+
delete updatableFields.content;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
// Handle other fields
|
|
966
|
+
for (const key in updatableFields) {
|
|
967
|
+
if (Object.prototype.hasOwnProperty.call(updatableFields, key)) {
|
|
968
|
+
const dbColumn = key === 'threadId' ? 'thread_id' : key;
|
|
969
|
+
setClauses.push(`"${dbColumn}" = {var_${key}_${paramIdx}:String}`);
|
|
970
|
+
values[`var_${key}_${paramIdx}`] = updatableFields[key as keyof typeof updatableFields];
|
|
971
|
+
paramIdx++;
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
if (setClauses.length > 0) {
|
|
976
|
+
values[`var_id_${paramIdx}`] = id;
|
|
977
|
+
|
|
978
|
+
// Use ALTER TABLE UPDATE for ClickHouse
|
|
979
|
+
const updateQuery = `
|
|
980
|
+
ALTER TABLE ${TABLE_MESSAGES}
|
|
981
|
+
UPDATE ${setClauses.join(', ')}
|
|
982
|
+
WHERE id = {var_id_${paramIdx}:String}
|
|
983
|
+
`;
|
|
984
|
+
|
|
985
|
+
console.log('Updating message:', id, 'with query:', updateQuery, 'values:', values);
|
|
986
|
+
|
|
987
|
+
updatePromises.push(
|
|
988
|
+
this.client.command({
|
|
989
|
+
query: updateQuery,
|
|
990
|
+
query_params: values,
|
|
991
|
+
clickhouse_settings: {
|
|
992
|
+
date_time_input_format: 'best_effort',
|
|
993
|
+
use_client_time_zone: 1,
|
|
994
|
+
output_format_json_quote_64bit_integers: 0,
|
|
995
|
+
},
|
|
996
|
+
}),
|
|
997
|
+
);
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
// Execute all updates
|
|
1002
|
+
if (updatePromises.length > 0) {
|
|
1003
|
+
await Promise.all(updatePromises);
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
// Optimize table to apply changes immediately
|
|
1007
|
+
await this.client.command({
|
|
1008
|
+
query: `OPTIMIZE TABLE ${TABLE_MESSAGES} FINAL`,
|
|
1009
|
+
clickhouse_settings: {
|
|
1010
|
+
date_time_input_format: 'best_effort',
|
|
1011
|
+
use_client_time_zone: 1,
|
|
1012
|
+
output_format_json_quote_64bit_integers: 0,
|
|
1013
|
+
},
|
|
1014
|
+
});
|
|
1015
|
+
|
|
1016
|
+
// Verify updates were applied and retry if needed
|
|
1017
|
+
for (const existingMessage of parsedExistingMessages) {
|
|
1018
|
+
const updatePayload = messages.find(m => m.id === existingMessage.id);
|
|
1019
|
+
if (!updatePayload) continue;
|
|
1020
|
+
|
|
1021
|
+
const { id, ...fieldsToUpdate } = updatePayload;
|
|
1022
|
+
if (Object.keys(fieldsToUpdate).length === 0) continue;
|
|
1023
|
+
|
|
1024
|
+
// Check if the update was actually applied
|
|
1025
|
+
const verifyResult = await this.client.query({
|
|
1026
|
+
query: `SELECT id, content, role, type, "createdAt", thread_id AS "threadId", "resourceId" FROM ${TABLE_MESSAGES} WHERE id = {messageId:String}`,
|
|
1027
|
+
query_params: { messageId: id },
|
|
1028
|
+
clickhouse_settings: {
|
|
1029
|
+
date_time_input_format: 'best_effort',
|
|
1030
|
+
date_time_output_format: 'iso',
|
|
1031
|
+
use_client_time_zone: 1,
|
|
1032
|
+
output_format_json_quote_64bit_integers: 0,
|
|
1033
|
+
},
|
|
1034
|
+
});
|
|
1035
|
+
|
|
1036
|
+
const verifyRows = await verifyResult.json();
|
|
1037
|
+
if (verifyRows.data.length > 0) {
|
|
1038
|
+
const updatedMessage = transformRows<MastraMessageV2>(verifyRows.data)[0];
|
|
1039
|
+
|
|
1040
|
+
if (updatedMessage) {
|
|
1041
|
+
// Check if the update was applied correctly
|
|
1042
|
+
let needsRetry = false;
|
|
1043
|
+
for (const [key, value] of Object.entries(fieldsToUpdate)) {
|
|
1044
|
+
if (key === 'content') {
|
|
1045
|
+
// For content updates, check if the content was updated
|
|
1046
|
+
const expectedContent = typeof value === 'string' ? value : JSON.stringify(value);
|
|
1047
|
+
const actualContent =
|
|
1048
|
+
typeof updatedMessage.content === 'string'
|
|
1049
|
+
? updatedMessage.content
|
|
1050
|
+
: JSON.stringify(updatedMessage.content);
|
|
1051
|
+
if (actualContent !== expectedContent) {
|
|
1052
|
+
needsRetry = true;
|
|
1053
|
+
break;
|
|
1054
|
+
}
|
|
1055
|
+
} else if (updatedMessage[key as keyof MastraMessageV2] !== value) {
|
|
1056
|
+
needsRetry = true;
|
|
1057
|
+
break;
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
if (needsRetry) {
|
|
1062
|
+
console.log('Update not applied correctly, retrying with DELETE + INSERT for message:', id);
|
|
1063
|
+
|
|
1064
|
+
// Use DELETE + INSERT as fallback
|
|
1065
|
+
await this.client.command({
|
|
1066
|
+
query: `DELETE FROM ${TABLE_MESSAGES} WHERE id = {messageId:String}`,
|
|
1067
|
+
query_params: { messageId: id },
|
|
1068
|
+
clickhouse_settings: {
|
|
1069
|
+
date_time_input_format: 'best_effort',
|
|
1070
|
+
use_client_time_zone: 1,
|
|
1071
|
+
output_format_json_quote_64bit_integers: 0,
|
|
1072
|
+
},
|
|
1073
|
+
});
|
|
1074
|
+
|
|
1075
|
+
// Reconstruct the updated content if needed
|
|
1076
|
+
let updatedContent = existingMessage.content || {};
|
|
1077
|
+
if (fieldsToUpdate.content) {
|
|
1078
|
+
const existingContent = existingMessage.content || {};
|
|
1079
|
+
const existingMetadata = existingContent.metadata || {};
|
|
1080
|
+
const updateMetadata = fieldsToUpdate.content.metadata || {};
|
|
1081
|
+
|
|
1082
|
+
updatedContent = {
|
|
1083
|
+
...existingContent,
|
|
1084
|
+
...fieldsToUpdate.content,
|
|
1085
|
+
metadata: {
|
|
1086
|
+
...existingMetadata,
|
|
1087
|
+
...updateMetadata,
|
|
1088
|
+
},
|
|
1089
|
+
};
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
const updatedMessageData = {
|
|
1093
|
+
...existingMessage,
|
|
1094
|
+
...fieldsToUpdate,
|
|
1095
|
+
content: updatedContent,
|
|
1096
|
+
};
|
|
1097
|
+
|
|
1098
|
+
await this.client.insert({
|
|
1099
|
+
table: TABLE_MESSAGES,
|
|
1100
|
+
format: 'JSONEachRow',
|
|
1101
|
+
values: [
|
|
1102
|
+
{
|
|
1103
|
+
id: updatedMessageData.id,
|
|
1104
|
+
thread_id: updatedMessageData.threadId,
|
|
1105
|
+
resourceId: updatedMessageData.resourceId,
|
|
1106
|
+
content:
|
|
1107
|
+
typeof updatedMessageData.content === 'string'
|
|
1108
|
+
? updatedMessageData.content
|
|
1109
|
+
: JSON.stringify(updatedMessageData.content),
|
|
1110
|
+
createdAt: updatedMessageData.createdAt.toISOString(),
|
|
1111
|
+
role: updatedMessageData.role,
|
|
1112
|
+
type: updatedMessageData.type || 'v2',
|
|
1113
|
+
},
|
|
1114
|
+
],
|
|
1115
|
+
clickhouse_settings: {
|
|
1116
|
+
date_time_input_format: 'best_effort',
|
|
1117
|
+
use_client_time_zone: 1,
|
|
1118
|
+
output_format_json_quote_64bit_integers: 0,
|
|
1119
|
+
},
|
|
1120
|
+
});
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
// Update thread timestamps with a small delay to ensure timestamp difference
|
|
1127
|
+
if (threadIdsToUpdate.size > 0) {
|
|
1128
|
+
// Add a small delay to ensure timestamp difference
|
|
1129
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
1130
|
+
|
|
1131
|
+
const now = new Date().toISOString().replace('Z', '');
|
|
1132
|
+
|
|
1133
|
+
// Get existing threads to preserve their data
|
|
1134
|
+
const threadUpdatePromises = Array.from(threadIdsToUpdate).map(async threadId => {
|
|
1135
|
+
// Get existing thread data
|
|
1136
|
+
const threadResult = await this.client.query({
|
|
1137
|
+
query: `SELECT id, resourceId, title, metadata, createdAt FROM ${TABLE_THREADS} WHERE id = {threadId:String}`,
|
|
1138
|
+
query_params: { threadId },
|
|
1139
|
+
clickhouse_settings: {
|
|
1140
|
+
date_time_input_format: 'best_effort',
|
|
1141
|
+
date_time_output_format: 'iso',
|
|
1142
|
+
use_client_time_zone: 1,
|
|
1143
|
+
output_format_json_quote_64bit_integers: 0,
|
|
1144
|
+
},
|
|
1145
|
+
});
|
|
1146
|
+
|
|
1147
|
+
const threadRows = await threadResult.json();
|
|
1148
|
+
if (threadRows.data.length > 0) {
|
|
1149
|
+
const existingThread = threadRows.data[0] as any;
|
|
1150
|
+
|
|
1151
|
+
// Delete existing thread
|
|
1152
|
+
await this.client.command({
|
|
1153
|
+
query: `DELETE FROM ${TABLE_THREADS} WHERE id = {threadId:String}`,
|
|
1154
|
+
query_params: { threadId },
|
|
1155
|
+
clickhouse_settings: {
|
|
1156
|
+
date_time_input_format: 'best_effort',
|
|
1157
|
+
use_client_time_zone: 1,
|
|
1158
|
+
output_format_json_quote_64bit_integers: 0,
|
|
1159
|
+
},
|
|
1160
|
+
});
|
|
1161
|
+
|
|
1162
|
+
// Insert updated thread with new timestamp
|
|
1163
|
+
await this.client.insert({
|
|
1164
|
+
table: TABLE_THREADS,
|
|
1165
|
+
format: 'JSONEachRow',
|
|
1166
|
+
values: [
|
|
1167
|
+
{
|
|
1168
|
+
id: existingThread.id,
|
|
1169
|
+
resourceId: existingThread.resourceId,
|
|
1170
|
+
title: existingThread.title,
|
|
1171
|
+
metadata: existingThread.metadata,
|
|
1172
|
+
createdAt: existingThread.createdAt,
|
|
1173
|
+
updatedAt: now,
|
|
1174
|
+
},
|
|
1175
|
+
],
|
|
1176
|
+
clickhouse_settings: {
|
|
1177
|
+
date_time_input_format: 'best_effort',
|
|
1178
|
+
use_client_time_zone: 1,
|
|
1179
|
+
output_format_json_quote_64bit_integers: 0,
|
|
1180
|
+
},
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
});
|
|
1184
|
+
|
|
1185
|
+
await Promise.all(threadUpdatePromises);
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
// Re-fetch to return the fully updated messages
|
|
1189
|
+
const updatedMessages: MastraMessageV2[] = [];
|
|
1190
|
+
for (const messageId of messageIds) {
|
|
1191
|
+
const updatedResult = await this.client.query({
|
|
1192
|
+
query: `SELECT id, content, role, type, "createdAt", thread_id AS "threadId", "resourceId" FROM ${TABLE_MESSAGES} WHERE id = {messageId:String}`,
|
|
1193
|
+
query_params: { messageId },
|
|
1194
|
+
clickhouse_settings: {
|
|
1195
|
+
date_time_input_format: 'best_effort',
|
|
1196
|
+
date_time_output_format: 'iso',
|
|
1197
|
+
use_client_time_zone: 1,
|
|
1198
|
+
output_format_json_quote_64bit_integers: 0,
|
|
1199
|
+
},
|
|
1200
|
+
});
|
|
1201
|
+
const updatedRows = await updatedResult.json();
|
|
1202
|
+
if (updatedRows.data.length > 0) {
|
|
1203
|
+
const message = transformRows<MastraMessageV2>(updatedRows.data)[0];
|
|
1204
|
+
if (message) {
|
|
1205
|
+
updatedMessages.push(message);
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
// Parse content back to objects
|
|
1211
|
+
return updatedMessages.map(message => {
|
|
1212
|
+
if (typeof message.content === 'string') {
|
|
1213
|
+
try {
|
|
1214
|
+
message.content = JSON.parse(message.content);
|
|
1215
|
+
} catch {
|
|
1216
|
+
// ignore if not valid json
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
return message;
|
|
1220
|
+
});
|
|
1221
|
+
} catch (error) {
|
|
1222
|
+
throw new MastraError(
|
|
1223
|
+
{
|
|
1224
|
+
id: 'CLICKHOUSE_STORAGE_UPDATE_MESSAGES_FAILED',
|
|
1225
|
+
domain: ErrorDomain.STORAGE,
|
|
1226
|
+
category: ErrorCategory.THIRD_PARTY,
|
|
1227
|
+
details: { messageIds: messages.map(m => m.id).join(',') },
|
|
1228
|
+
},
|
|
1229
|
+
error,
|
|
1230
|
+
);
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
async getResourceById({ resourceId }: { resourceId: string }): Promise<StorageResourceType | null> {
|
|
1235
|
+
try {
|
|
1236
|
+
const result = await this.client.query({
|
|
1237
|
+
query: `SELECT id, workingMemory, metadata, createdAt, updatedAt FROM ${TABLE_RESOURCES} WHERE id = {resourceId:String}`,
|
|
1238
|
+
query_params: { resourceId },
|
|
1239
|
+
clickhouse_settings: {
|
|
1240
|
+
date_time_input_format: 'best_effort',
|
|
1241
|
+
date_time_output_format: 'iso',
|
|
1242
|
+
use_client_time_zone: 1,
|
|
1243
|
+
output_format_json_quote_64bit_integers: 0,
|
|
1244
|
+
},
|
|
1245
|
+
});
|
|
1246
|
+
|
|
1247
|
+
const rows = await result.json();
|
|
1248
|
+
if (rows.data.length === 0) {
|
|
1249
|
+
return null;
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
const resource = rows.data[0] as any;
|
|
1253
|
+
return {
|
|
1254
|
+
id: resource.id,
|
|
1255
|
+
workingMemory:
|
|
1256
|
+
resource.workingMemory && typeof resource.workingMemory === 'object'
|
|
1257
|
+
? JSON.stringify(resource.workingMemory)
|
|
1258
|
+
: resource.workingMemory,
|
|
1259
|
+
metadata:
|
|
1260
|
+
resource.metadata && typeof resource.metadata === 'string'
|
|
1261
|
+
? JSON.parse(resource.metadata)
|
|
1262
|
+
: resource.metadata,
|
|
1263
|
+
createdAt: new Date(resource.createdAt),
|
|
1264
|
+
updatedAt: new Date(resource.updatedAt),
|
|
1265
|
+
};
|
|
1266
|
+
} catch (error) {
|
|
1267
|
+
throw new MastraError(
|
|
1268
|
+
{
|
|
1269
|
+
id: 'CLICKHOUSE_STORAGE_GET_RESOURCE_BY_ID_FAILED',
|
|
1270
|
+
domain: ErrorDomain.STORAGE,
|
|
1271
|
+
category: ErrorCategory.THIRD_PARTY,
|
|
1272
|
+
details: { resourceId },
|
|
1273
|
+
},
|
|
1274
|
+
error,
|
|
1275
|
+
);
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
async saveResource({ resource }: { resource: StorageResourceType }): Promise<StorageResourceType> {
|
|
1280
|
+
try {
|
|
1281
|
+
await this.client.insert({
|
|
1282
|
+
table: TABLE_RESOURCES,
|
|
1283
|
+
format: 'JSONEachRow',
|
|
1284
|
+
values: [
|
|
1285
|
+
{
|
|
1286
|
+
id: resource.id,
|
|
1287
|
+
workingMemory: resource.workingMemory,
|
|
1288
|
+
metadata: JSON.stringify(resource.metadata),
|
|
1289
|
+
createdAt: resource.createdAt.toISOString(),
|
|
1290
|
+
updatedAt: resource.updatedAt.toISOString(),
|
|
1291
|
+
},
|
|
1292
|
+
],
|
|
1293
|
+
clickhouse_settings: {
|
|
1294
|
+
date_time_input_format: 'best_effort',
|
|
1295
|
+
use_client_time_zone: 1,
|
|
1296
|
+
output_format_json_quote_64bit_integers: 0,
|
|
1297
|
+
},
|
|
1298
|
+
});
|
|
1299
|
+
|
|
1300
|
+
return resource;
|
|
1301
|
+
} catch (error) {
|
|
1302
|
+
throw new MastraError(
|
|
1303
|
+
{
|
|
1304
|
+
id: 'CLICKHOUSE_STORAGE_SAVE_RESOURCE_FAILED',
|
|
1305
|
+
domain: ErrorDomain.STORAGE,
|
|
1306
|
+
category: ErrorCategory.THIRD_PARTY,
|
|
1307
|
+
details: { resourceId: resource.id },
|
|
1308
|
+
},
|
|
1309
|
+
error,
|
|
1310
|
+
);
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
async updateResource({
|
|
1315
|
+
resourceId,
|
|
1316
|
+
workingMemory,
|
|
1317
|
+
metadata,
|
|
1318
|
+
}: {
|
|
1319
|
+
resourceId: string;
|
|
1320
|
+
workingMemory?: string;
|
|
1321
|
+
metadata?: Record<string, unknown>;
|
|
1322
|
+
}): Promise<StorageResourceType> {
|
|
1323
|
+
try {
|
|
1324
|
+
const existingResource = await this.getResourceById({ resourceId });
|
|
1325
|
+
|
|
1326
|
+
if (!existingResource) {
|
|
1327
|
+
// Create new resource if it doesn't exist
|
|
1328
|
+
const newResource: StorageResourceType = {
|
|
1329
|
+
id: resourceId,
|
|
1330
|
+
workingMemory,
|
|
1331
|
+
metadata: metadata || {},
|
|
1332
|
+
createdAt: new Date(),
|
|
1333
|
+
updatedAt: new Date(),
|
|
1334
|
+
};
|
|
1335
|
+
return this.saveResource({ resource: newResource });
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
const updatedResource = {
|
|
1339
|
+
...existingResource,
|
|
1340
|
+
workingMemory: workingMemory !== undefined ? workingMemory : existingResource.workingMemory,
|
|
1341
|
+
metadata: {
|
|
1342
|
+
...existingResource.metadata,
|
|
1343
|
+
...metadata,
|
|
1344
|
+
},
|
|
1345
|
+
updatedAt: new Date(),
|
|
1346
|
+
};
|
|
1347
|
+
|
|
1348
|
+
// Use ALTER TABLE UPDATE for ClickHouse
|
|
1349
|
+
const updateQuery = `
|
|
1350
|
+
ALTER TABLE ${TABLE_RESOURCES}
|
|
1351
|
+
UPDATE workingMemory = {workingMemory:String}, metadata = {metadata:String}, updatedAt = {updatedAt:String}
|
|
1352
|
+
WHERE id = {resourceId:String}
|
|
1353
|
+
`;
|
|
1354
|
+
|
|
1355
|
+
await this.client.command({
|
|
1356
|
+
query: updateQuery,
|
|
1357
|
+
query_params: {
|
|
1358
|
+
workingMemory: updatedResource.workingMemory,
|
|
1359
|
+
metadata: JSON.stringify(updatedResource.metadata),
|
|
1360
|
+
updatedAt: updatedResource.updatedAt.toISOString().replace('Z', ''),
|
|
1361
|
+
resourceId,
|
|
1362
|
+
},
|
|
1363
|
+
clickhouse_settings: {
|
|
1364
|
+
date_time_input_format: 'best_effort',
|
|
1365
|
+
use_client_time_zone: 1,
|
|
1366
|
+
output_format_json_quote_64bit_integers: 0,
|
|
1367
|
+
},
|
|
1368
|
+
});
|
|
1369
|
+
|
|
1370
|
+
// Optimize table to apply changes
|
|
1371
|
+
await this.client.command({
|
|
1372
|
+
query: `OPTIMIZE TABLE ${TABLE_RESOURCES} FINAL`,
|
|
1373
|
+
clickhouse_settings: {
|
|
1374
|
+
date_time_input_format: 'best_effort',
|
|
1375
|
+
use_client_time_zone: 1,
|
|
1376
|
+
output_format_json_quote_64bit_integers: 0,
|
|
1377
|
+
},
|
|
1378
|
+
});
|
|
1379
|
+
|
|
1380
|
+
return updatedResource;
|
|
1381
|
+
} catch (error) {
|
|
1382
|
+
throw new MastraError(
|
|
1383
|
+
{
|
|
1384
|
+
id: 'CLICKHOUSE_STORAGE_UPDATE_RESOURCE_FAILED',
|
|
1385
|
+
domain: ErrorDomain.STORAGE,
|
|
1386
|
+
category: ErrorCategory.THIRD_PARTY,
|
|
1387
|
+
details: { resourceId },
|
|
1388
|
+
},
|
|
1389
|
+
error,
|
|
1390
|
+
);
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
}
|