@mastra/clickhouse 0.0.0-vnext-inngest-20250508131921 → 0.0.0-vnext-20251119160359

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/dist/index.js CHANGED
@@ -1,20 +1,19 @@
1
1
  import { createClient } from '@clickhouse/client';
2
- import { TABLE_EVALS, TABLE_THREADS, TABLE_TRACES, TABLE_WORKFLOW_SNAPSHOT, TABLE_MESSAGES, MastraStorage, TABLE_SCHEMAS } from '@mastra/core/storage';
2
+ import { MastraError, ErrorCategory, ErrorDomain } from '@mastra/core/error';
3
+ import { TABLE_SPANS, TABLE_RESOURCES, TABLE_SCORERS, TABLE_THREADS, TABLE_TRACES, TABLE_WORKFLOW_SNAPSHOT, TABLE_MESSAGES, MastraStorage, StoreOperations, TABLE_SCHEMAS, WorkflowsStorage, normalizePerPage, ScoresStorage, safelyParseJSON, calculatePagination, MemoryStorage } from '@mastra/core/storage';
4
+ import { MessageList } from '@mastra/core/agent';
5
+ import { saveScorePayloadSchema } from '@mastra/core/evals';
3
6
 
4
7
  // src/storage/index.ts
5
- function safelyParseJSON(jsonString) {
6
- try {
7
- return JSON.parse(jsonString);
8
- } catch {
9
- return {};
10
- }
11
- }
12
8
  var TABLE_ENGINES = {
13
9
  [TABLE_MESSAGES]: `MergeTree()`,
14
10
  [TABLE_WORKFLOW_SNAPSHOT]: `ReplacingMergeTree()`,
15
11
  [TABLE_TRACES]: `MergeTree()`,
16
12
  [TABLE_THREADS]: `ReplacingMergeTree()`,
17
- [TABLE_EVALS]: `MergeTree()`
13
+ [TABLE_SCORERS]: `MergeTree()`,
14
+ [TABLE_RESOURCES]: `ReplacingMergeTree()`,
15
+ // TODO: verify this is the correct engine for Spans when implementing clickhouse storage
16
+ [TABLE_SPANS]: `ReplacingMergeTree()`
18
17
  };
19
18
  var COLUMN_TYPES = {
20
19
  text: "String",
@@ -22,11 +21,10 @@ var COLUMN_TYPES = {
22
21
  uuid: "String",
23
22
  jsonb: "String",
24
23
  integer: "Int64",
25
- bigint: "Int64"
24
+ float: "Float64",
25
+ bigint: "Int64",
26
+ boolean: "Bool"
26
27
  };
27
- function transformRows(rows) {
28
- return rows.map((row) => transformRow(row));
29
- }
30
28
  function transformRow(row) {
31
29
  if (!row) {
32
30
  return row;
@@ -37,54 +35,730 @@ function transformRow(row) {
37
35
  if (row.updatedAt) {
38
36
  row.updatedAt = new Date(row.updatedAt);
39
37
  }
38
+ if (row.content && typeof row.content === "string") {
39
+ row.content = safelyParseJSON(row.content);
40
+ }
40
41
  return row;
41
42
  }
42
- var ClickhouseStore = class extends MastraStorage {
43
- db;
44
- ttl = {};
45
- constructor(config) {
46
- super({ name: "ClickhouseStore" });
47
- this.db = createClient({
48
- url: config.url,
49
- username: config.username,
50
- password: config.password,
51
- clickhouse_settings: {
52
- date_time_input_format: "best_effort",
53
- date_time_output_format: "iso",
54
- // This is crucial
55
- use_client_time_zone: 1,
56
- output_format_json_quote_64bit_integers: 0
43
+ function transformRows(rows) {
44
+ return rows.map((row) => transformRow(row));
45
+ }
46
+
47
+ // src/storage/domains/memory/index.ts
48
+ function serializeMetadata(metadata) {
49
+ if (!metadata || Object.keys(metadata).length === 0) {
50
+ return "{}";
51
+ }
52
+ return JSON.stringify(metadata);
53
+ }
54
+ function parseMetadata(metadata) {
55
+ if (!metadata) return {};
56
+ if (typeof metadata === "object") return metadata;
57
+ if (typeof metadata !== "string") return {};
58
+ const trimmed = metadata.trim();
59
+ if (trimmed === "" || trimmed === "null") return {};
60
+ try {
61
+ return JSON.parse(trimmed);
62
+ } catch {
63
+ return {};
64
+ }
65
+ }
66
+ var MemoryStorageClickhouse = class extends MemoryStorage {
67
+ client;
68
+ operations;
69
+ constructor({ client, operations }) {
70
+ super();
71
+ this.client = client;
72
+ this.operations = operations;
73
+ }
74
+ async listMessagesById({ messageIds }) {
75
+ if (messageIds.length === 0) return { messages: [] };
76
+ try {
77
+ const result = await this.client.query({
78
+ query: `
79
+ SELECT
80
+ id,
81
+ content,
82
+ role,
83
+ type,
84
+ toDateTime64(createdAt, 3) as createdAt,
85
+ thread_id AS "threadId",
86
+ "resourceId"
87
+ FROM "${TABLE_MESSAGES}"
88
+ WHERE id IN {messageIds:Array(String)}
89
+ ORDER BY "createdAt" DESC
90
+ `,
91
+ query_params: {
92
+ messageIds
93
+ },
94
+ clickhouse_settings: {
95
+ // Allows to insert serialized JS Dates (such as '2023-12-06T10:54:48.000Z')
96
+ date_time_input_format: "best_effort",
97
+ date_time_output_format: "iso",
98
+ use_client_time_zone: 1,
99
+ output_format_json_quote_64bit_integers: 0
100
+ }
101
+ });
102
+ const rows = await result.json();
103
+ const messages = transformRows(rows.data);
104
+ messages.forEach((message) => {
105
+ if (typeof message.content === "string") {
106
+ try {
107
+ message.content = JSON.parse(message.content);
108
+ } catch {
109
+ }
110
+ }
111
+ });
112
+ const list = new MessageList().add(messages, "memory");
113
+ return { messages: list.get.all.db() };
114
+ } catch (error) {
115
+ throw new MastraError(
116
+ {
117
+ id: "CLICKHOUSE_STORAGE_LIST_MESSAGES_BY_ID_FAILED",
118
+ domain: ErrorDomain.STORAGE,
119
+ category: ErrorCategory.THIRD_PARTY,
120
+ details: { messageIds: JSON.stringify(messageIds) }
121
+ },
122
+ error
123
+ );
124
+ }
125
+ }
126
+ async listMessages(args) {
127
+ const { threadId, resourceId, include, filter, perPage: perPageInput, page = 0, orderBy } = args;
128
+ if (page < 0) {
129
+ throw new MastraError(
130
+ {
131
+ id: "STORAGE_CLICKHOUSE_LIST_MESSAGES_INVALID_PAGE",
132
+ domain: ErrorDomain.STORAGE,
133
+ category: ErrorCategory.USER,
134
+ details: { page }
135
+ },
136
+ new Error("page must be >= 0")
137
+ );
138
+ }
139
+ if (!threadId.trim()) {
140
+ throw new MastraError(
141
+ {
142
+ id: "STORAGE_CLICKHOUSE_LIST_MESSAGES_INVALID_THREAD_ID",
143
+ domain: ErrorDomain.STORAGE,
144
+ category: ErrorCategory.THIRD_PARTY,
145
+ details: { threadId }
146
+ },
147
+ new Error("threadId must be a non-empty string")
148
+ );
149
+ }
150
+ const perPageForQuery = normalizePerPage(perPageInput, 40);
151
+ const { offset, perPage: perPageForResponse } = calculatePagination(page, perPageInput, perPageForQuery);
152
+ try {
153
+ let dataQuery = `
154
+ SELECT
155
+ id,
156
+ content,
157
+ role,
158
+ type,
159
+ toDateTime64(createdAt, 3) as createdAt,
160
+ thread_id AS "threadId",
161
+ resourceId
162
+ FROM ${TABLE_MESSAGES}
163
+ WHERE thread_id = {threadId:String}
164
+ `;
165
+ const dataParams = { threadId };
166
+ if (resourceId) {
167
+ dataQuery += ` AND resourceId = {resourceId:String}`;
168
+ dataParams.resourceId = resourceId;
57
169
  }
58
- });
59
- this.ttl = config.ttl;
170
+ if (filter?.dateRange?.start) {
171
+ const startDate = filter.dateRange.start instanceof Date ? filter.dateRange.start.toISOString() : new Date(filter.dateRange.start).toISOString();
172
+ dataQuery += ` AND createdAt >= parseDateTime64BestEffort({fromDate:String}, 3)`;
173
+ dataParams.fromDate = startDate;
174
+ }
175
+ if (filter?.dateRange?.end) {
176
+ const endDate = filter.dateRange.end instanceof Date ? filter.dateRange.end.toISOString() : new Date(filter.dateRange.end).toISOString();
177
+ dataQuery += ` AND createdAt <= parseDateTime64BestEffort({toDate:String}, 3)`;
178
+ dataParams.toDate = endDate;
179
+ }
180
+ const { field, direction } = this.parseOrderBy(orderBy, "ASC");
181
+ dataQuery += ` ORDER BY "${field}" ${direction}`;
182
+ if (perPageForResponse === false) ; else {
183
+ dataQuery += ` LIMIT {limit:Int64} OFFSET {offset:Int64}`;
184
+ dataParams.limit = perPageForQuery;
185
+ dataParams.offset = offset;
186
+ }
187
+ const result = await this.client.query({
188
+ query: dataQuery,
189
+ query_params: dataParams,
190
+ clickhouse_settings: {
191
+ date_time_input_format: "best_effort",
192
+ date_time_output_format: "iso",
193
+ use_client_time_zone: 1,
194
+ output_format_json_quote_64bit_integers: 0
195
+ }
196
+ });
197
+ const rows = await result.json();
198
+ const paginatedMessages = transformRows(rows.data);
199
+ const paginatedCount = paginatedMessages.length;
200
+ let countQuery = `SELECT count() as total FROM ${TABLE_MESSAGES} WHERE thread_id = {threadId:String}`;
201
+ const countParams = { threadId };
202
+ if (resourceId) {
203
+ countQuery += ` AND resourceId = {resourceId:String}`;
204
+ countParams.resourceId = resourceId;
205
+ }
206
+ if (filter?.dateRange?.start) {
207
+ const startDate = filter.dateRange.start instanceof Date ? filter.dateRange.start.toISOString() : new Date(filter.dateRange.start).toISOString();
208
+ countQuery += ` AND createdAt >= parseDateTime64BestEffort({fromDate:String}, 3)`;
209
+ countParams.fromDate = startDate;
210
+ }
211
+ if (filter?.dateRange?.end) {
212
+ const endDate = filter.dateRange.end instanceof Date ? filter.dateRange.end.toISOString() : new Date(filter.dateRange.end).toISOString();
213
+ countQuery += ` AND createdAt <= parseDateTime64BestEffort({toDate:String}, 3)`;
214
+ countParams.toDate = endDate;
215
+ }
216
+ const countResult = await this.client.query({
217
+ query: countQuery,
218
+ query_params: countParams,
219
+ clickhouse_settings: {
220
+ date_time_input_format: "best_effort",
221
+ date_time_output_format: "iso",
222
+ use_client_time_zone: 1,
223
+ output_format_json_quote_64bit_integers: 0
224
+ }
225
+ });
226
+ const countData = await countResult.json();
227
+ const total = countData.data[0].total;
228
+ if (total === 0 && paginatedCount === 0 && (!include || include.length === 0)) {
229
+ return {
230
+ messages: [],
231
+ total: 0,
232
+ page,
233
+ perPage: perPageForResponse,
234
+ hasMore: false
235
+ };
236
+ }
237
+ const messageIds = new Set(paginatedMessages.map((m) => m.id));
238
+ let includeMessages = [];
239
+ if (include && include.length > 0) {
240
+ const unionQueries = [];
241
+ const params = [];
242
+ let paramIdx = 1;
243
+ for (const inc of include) {
244
+ const { id, withPreviousMessages = 0, withNextMessages = 0 } = inc;
245
+ const searchId = inc.threadId || threadId;
246
+ unionQueries.push(`
247
+ SELECT * FROM (
248
+ WITH numbered_messages AS (
249
+ SELECT
250
+ id, content, role, type, "createdAt", thread_id, "resourceId",
251
+ ROW_NUMBER() OVER (ORDER BY "createdAt" ASC) as row_num
252
+ FROM "${TABLE_MESSAGES}"
253
+ WHERE thread_id = {var_thread_id_${paramIdx}:String}
254
+ ),
255
+ target_positions AS (
256
+ SELECT row_num as target_pos
257
+ FROM numbered_messages
258
+ WHERE id = {var_include_id_${paramIdx}:String}
259
+ )
260
+ SELECT DISTINCT m.id, m.content, m.role, m.type, m."createdAt", m.thread_id AS "threadId", m."resourceId"
261
+ FROM numbered_messages m
262
+ CROSS JOIN target_positions t
263
+ WHERE m.row_num BETWEEN (t.target_pos - {var_withPreviousMessages_${paramIdx}:Int64}) AND (t.target_pos + {var_withNextMessages_${paramIdx}:Int64})
264
+ ) AS query_${paramIdx}
265
+ `);
266
+ params.push(
267
+ { [`var_thread_id_${paramIdx}`]: searchId },
268
+ { [`var_include_id_${paramIdx}`]: id },
269
+ { [`var_withPreviousMessages_${paramIdx}`]: withPreviousMessages },
270
+ { [`var_withNextMessages_${paramIdx}`]: withNextMessages }
271
+ );
272
+ paramIdx++;
273
+ }
274
+ const finalQuery = unionQueries.join(" UNION ALL ") + ' ORDER BY "createdAt" ASC';
275
+ const mergedParams = params.reduce((acc, paramObj) => ({ ...acc, ...paramObj }), {});
276
+ const includeResult = await this.client.query({
277
+ query: finalQuery,
278
+ query_params: mergedParams,
279
+ clickhouse_settings: {
280
+ date_time_input_format: "best_effort",
281
+ date_time_output_format: "iso",
282
+ use_client_time_zone: 1,
283
+ output_format_json_quote_64bit_integers: 0
284
+ }
285
+ });
286
+ const includeRows = await includeResult.json();
287
+ includeMessages = transformRows(includeRows.data);
288
+ for (const includeMsg of includeMessages) {
289
+ if (!messageIds.has(includeMsg.id)) {
290
+ paginatedMessages.push(includeMsg);
291
+ messageIds.add(includeMsg.id);
292
+ }
293
+ }
294
+ }
295
+ const list = new MessageList().add(paginatedMessages, "memory");
296
+ let finalMessages = list.get.all.db();
297
+ finalMessages = finalMessages.sort((a, b) => {
298
+ const isDateField = field === "createdAt" || field === "updatedAt";
299
+ const aValue = isDateField ? new Date(a[field]).getTime() : a[field];
300
+ const bValue = isDateField ? new Date(b[field]).getTime() : b[field];
301
+ if (aValue === bValue) {
302
+ return a.id.localeCompare(b.id);
303
+ }
304
+ if (typeof aValue === "number" && typeof bValue === "number") {
305
+ return direction === "ASC" ? aValue - bValue : bValue - aValue;
306
+ }
307
+ return direction === "ASC" ? String(aValue).localeCompare(String(bValue)) : String(bValue).localeCompare(String(aValue));
308
+ });
309
+ const returnedThreadMessageIds = new Set(finalMessages.filter((m) => m.threadId === threadId).map((m) => m.id));
310
+ const allThreadMessagesReturned = returnedThreadMessageIds.size >= total;
311
+ const hasMore = perPageForResponse === false ? false : allThreadMessagesReturned ? false : offset + paginatedCount < total;
312
+ return {
313
+ messages: finalMessages,
314
+ total,
315
+ page,
316
+ perPage: perPageForResponse,
317
+ hasMore
318
+ };
319
+ } catch (error) {
320
+ const mastraError = new MastraError(
321
+ {
322
+ id: "STORAGE_CLICKHOUSE_STORE_LIST_MESSAGES_FAILED",
323
+ domain: ErrorDomain.STORAGE,
324
+ category: ErrorCategory.THIRD_PARTY,
325
+ details: {
326
+ threadId,
327
+ resourceId: resourceId ?? ""
328
+ }
329
+ },
330
+ error
331
+ );
332
+ this.logger?.error?.(mastraError.toString());
333
+ this.logger?.trackException?.(mastraError);
334
+ return {
335
+ messages: [],
336
+ total: 0,
337
+ page,
338
+ perPage: perPageForResponse,
339
+ hasMore: false
340
+ };
341
+ }
342
+ }
343
+ async saveMessages(args) {
344
+ const { messages } = args;
345
+ if (messages.length === 0) return { messages };
346
+ for (const message of messages) {
347
+ const resourceId = message.resourceId;
348
+ if (!resourceId) {
349
+ throw new Error("Resource ID is required");
350
+ }
351
+ if (!message.threadId) {
352
+ throw new Error("Thread ID is required");
353
+ }
354
+ const thread = await this.getThreadById({ threadId: message.threadId });
355
+ if (!thread) {
356
+ throw new Error(`Thread ${message.threadId} not found`);
357
+ }
358
+ }
359
+ const threadIdSet = /* @__PURE__ */ new Map();
360
+ await Promise.all(
361
+ messages.map(async (m) => {
362
+ const resourceId = m.resourceId;
363
+ if (!resourceId) {
364
+ throw new Error("Resource ID is required");
365
+ }
366
+ if (!m.threadId) {
367
+ throw new Error("Thread ID is required");
368
+ }
369
+ const thread = await this.getThreadById({ threadId: m.threadId });
370
+ if (!thread) {
371
+ throw new Error(`Thread ${m.threadId} not found`);
372
+ }
373
+ threadIdSet.set(m.threadId, thread);
374
+ })
375
+ );
376
+ try {
377
+ const existingResult = await this.client.query({
378
+ query: `SELECT id, thread_id FROM ${TABLE_MESSAGES} WHERE id IN ({ids:Array(String)})`,
379
+ query_params: {
380
+ ids: messages.map((m) => m.id)
381
+ },
382
+ clickhouse_settings: {
383
+ // Allows to insert serialized JS Dates (such as '2023-12-06T10:54:48.000Z')
384
+ date_time_input_format: "best_effort",
385
+ date_time_output_format: "iso",
386
+ use_client_time_zone: 1,
387
+ output_format_json_quote_64bit_integers: 0
388
+ },
389
+ format: "JSONEachRow"
390
+ });
391
+ const existingRows = await existingResult.json();
392
+ const existingSet = new Set(existingRows.map((row) => `${row.id}::${row.thread_id}`));
393
+ const toInsert = messages.filter((m) => !existingSet.has(`${m.id}::${m.threadId}`));
394
+ const toUpdate = messages.filter((m) => existingSet.has(`${m.id}::${m.threadId}`));
395
+ const toMove = messages.filter((m) => {
396
+ const existingRow = existingRows.find((row) => row.id === m.id);
397
+ return existingRow && existingRow.thread_id !== m.threadId;
398
+ });
399
+ const deletePromises = toMove.map((message) => {
400
+ const existingRow = existingRows.find((row) => row.id === message.id);
401
+ if (!existingRow) return Promise.resolve();
402
+ return this.client.command({
403
+ query: `DELETE FROM ${TABLE_MESSAGES} WHERE id = {var_id:String} AND thread_id = {var_old_thread_id:String}`,
404
+ query_params: {
405
+ var_id: message.id,
406
+ var_old_thread_id: existingRow.thread_id
407
+ },
408
+ clickhouse_settings: {
409
+ date_time_input_format: "best_effort",
410
+ use_client_time_zone: 1,
411
+ output_format_json_quote_64bit_integers: 0
412
+ }
413
+ });
414
+ });
415
+ const updatePromises = toUpdate.map(
416
+ (message) => this.client.command({
417
+ query: `
418
+ ALTER TABLE ${TABLE_MESSAGES}
419
+ UPDATE content = {var_content:String}, role = {var_role:String}, type = {var_type:String}, resourceId = {var_resourceId:String}
420
+ WHERE id = {var_id:String} AND thread_id = {var_thread_id:String}
421
+ `,
422
+ query_params: {
423
+ var_content: typeof message.content === "string" ? message.content : JSON.stringify(message.content),
424
+ var_role: message.role,
425
+ var_type: message.type || "v2",
426
+ var_resourceId: message.resourceId,
427
+ var_id: message.id,
428
+ var_thread_id: message.threadId
429
+ },
430
+ clickhouse_settings: {
431
+ // Allows to insert serialized JS Dates (such as '2023-12-06T10:54:48.000Z')
432
+ date_time_input_format: "best_effort",
433
+ use_client_time_zone: 1,
434
+ output_format_json_quote_64bit_integers: 0
435
+ }
436
+ })
437
+ );
438
+ await Promise.all([
439
+ // Insert new messages (including moved messages)
440
+ this.client.insert({
441
+ table: TABLE_MESSAGES,
442
+ format: "JSONEachRow",
443
+ values: toInsert.map((message) => ({
444
+ id: message.id,
445
+ thread_id: message.threadId,
446
+ resourceId: message.resourceId,
447
+ content: typeof message.content === "string" ? message.content : JSON.stringify(message.content),
448
+ createdAt: message.createdAt.toISOString(),
449
+ role: message.role,
450
+ type: message.type || "v2"
451
+ })),
452
+ clickhouse_settings: {
453
+ // Allows to insert serialized JS Dates (such as '2023-12-06T10:54:48.000Z')
454
+ date_time_input_format: "best_effort",
455
+ use_client_time_zone: 1,
456
+ output_format_json_quote_64bit_integers: 0
457
+ }
458
+ }),
459
+ ...updatePromises,
460
+ ...deletePromises,
461
+ // Update thread's updatedAt timestamp
462
+ this.client.insert({
463
+ table: TABLE_THREADS,
464
+ format: "JSONEachRow",
465
+ values: Array.from(threadIdSet.values()).map((thread) => ({
466
+ id: thread.id,
467
+ resourceId: thread.resourceId,
468
+ title: thread.title,
469
+ metadata: serializeMetadata(thread.metadata),
470
+ createdAt: thread.createdAt,
471
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
472
+ })),
473
+ clickhouse_settings: {
474
+ date_time_input_format: "best_effort",
475
+ use_client_time_zone: 1,
476
+ output_format_json_quote_64bit_integers: 0
477
+ }
478
+ })
479
+ ]);
480
+ const list = new MessageList().add(messages, "memory");
481
+ return { messages: list.get.all.db() };
482
+ } catch (error) {
483
+ throw new MastraError(
484
+ {
485
+ id: "CLICKHOUSE_STORAGE_SAVE_MESSAGES_FAILED",
486
+ domain: ErrorDomain.STORAGE,
487
+ category: ErrorCategory.THIRD_PARTY
488
+ },
489
+ error
490
+ );
491
+ }
492
+ }
493
+ async getThreadById({ threadId }) {
494
+ try {
495
+ const result = await this.client.query({
496
+ query: `SELECT
497
+ id,
498
+ "resourceId",
499
+ title,
500
+ metadata,
501
+ toDateTime64(createdAt, 3) as createdAt,
502
+ toDateTime64(updatedAt, 3) as updatedAt
503
+ FROM "${TABLE_THREADS}"
504
+ WHERE id = {var_id:String}
505
+ ORDER BY updatedAt DESC
506
+ LIMIT 1`,
507
+ query_params: { var_id: threadId },
508
+ clickhouse_settings: {
509
+ // Allows to insert serialized JS Dates (such as '2023-12-06T10:54:48.000Z')
510
+ date_time_input_format: "best_effort",
511
+ date_time_output_format: "iso",
512
+ use_client_time_zone: 1,
513
+ output_format_json_quote_64bit_integers: 0
514
+ }
515
+ });
516
+ const rows = await result.json();
517
+ const thread = transformRow(rows.data[0]);
518
+ if (!thread) {
519
+ return null;
520
+ }
521
+ return {
522
+ ...thread,
523
+ metadata: parseMetadata(thread.metadata),
524
+ createdAt: thread.createdAt,
525
+ updatedAt: thread.updatedAt
526
+ };
527
+ } catch (error) {
528
+ throw new MastraError(
529
+ {
530
+ id: "CLICKHOUSE_STORAGE_GET_THREAD_BY_ID_FAILED",
531
+ domain: ErrorDomain.STORAGE,
532
+ category: ErrorCategory.THIRD_PARTY,
533
+ details: { threadId }
534
+ },
535
+ error
536
+ );
537
+ }
538
+ }
539
+ async saveThread({ thread }) {
540
+ try {
541
+ await this.client.insert({
542
+ table: TABLE_THREADS,
543
+ values: [
544
+ {
545
+ ...thread,
546
+ metadata: serializeMetadata(thread.metadata),
547
+ createdAt: thread.createdAt.toISOString(),
548
+ updatedAt: thread.updatedAt.toISOString()
549
+ }
550
+ ],
551
+ format: "JSONEachRow",
552
+ clickhouse_settings: {
553
+ // Allows to insert serialized JS Dates (such as '2023-12-06T10:54:48.000Z')
554
+ date_time_input_format: "best_effort",
555
+ use_client_time_zone: 1,
556
+ output_format_json_quote_64bit_integers: 0
557
+ }
558
+ });
559
+ return thread;
560
+ } catch (error) {
561
+ throw new MastraError(
562
+ {
563
+ id: "CLICKHOUSE_STORAGE_SAVE_THREAD_FAILED",
564
+ domain: ErrorDomain.STORAGE,
565
+ category: ErrorCategory.THIRD_PARTY,
566
+ details: { threadId: thread.id }
567
+ },
568
+ error
569
+ );
570
+ }
571
+ }
572
+ async updateThread({
573
+ id,
574
+ title,
575
+ metadata
576
+ }) {
577
+ try {
578
+ const existingThread = await this.getThreadById({ threadId: id });
579
+ if (!existingThread) {
580
+ throw new Error(`Thread ${id} not found`);
581
+ }
582
+ const mergedMetadata = {
583
+ ...existingThread.metadata,
584
+ ...metadata
585
+ };
586
+ const updatedThread = {
587
+ ...existingThread,
588
+ title,
589
+ metadata: mergedMetadata,
590
+ updatedAt: /* @__PURE__ */ new Date()
591
+ };
592
+ await this.client.insert({
593
+ table: TABLE_THREADS,
594
+ format: "JSONEachRow",
595
+ values: [
596
+ {
597
+ id: updatedThread.id,
598
+ resourceId: updatedThread.resourceId,
599
+ title: updatedThread.title,
600
+ metadata: serializeMetadata(updatedThread.metadata),
601
+ createdAt: updatedThread.createdAt,
602
+ updatedAt: updatedThread.updatedAt.toISOString()
603
+ }
604
+ ],
605
+ clickhouse_settings: {
606
+ date_time_input_format: "best_effort",
607
+ use_client_time_zone: 1,
608
+ output_format_json_quote_64bit_integers: 0
609
+ }
610
+ });
611
+ return updatedThread;
612
+ } catch (error) {
613
+ throw new MastraError(
614
+ {
615
+ id: "CLICKHOUSE_STORAGE_UPDATE_THREAD_FAILED",
616
+ domain: ErrorDomain.STORAGE,
617
+ category: ErrorCategory.THIRD_PARTY,
618
+ details: { threadId: id, title }
619
+ },
620
+ error
621
+ );
622
+ }
623
+ }
624
+ async deleteThread({ threadId }) {
625
+ try {
626
+ await this.client.command({
627
+ query: `DELETE FROM "${TABLE_MESSAGES}" WHERE thread_id = {var_thread_id:String};`,
628
+ query_params: { var_thread_id: threadId },
629
+ clickhouse_settings: {
630
+ output_format_json_quote_64bit_integers: 0
631
+ }
632
+ });
633
+ await this.client.command({
634
+ query: `DELETE FROM "${TABLE_THREADS}" WHERE id = {var_id:String};`,
635
+ query_params: { var_id: threadId },
636
+ clickhouse_settings: {
637
+ output_format_json_quote_64bit_integers: 0
638
+ }
639
+ });
640
+ } catch (error) {
641
+ throw new MastraError(
642
+ {
643
+ id: "CLICKHOUSE_STORAGE_DELETE_THREAD_FAILED",
644
+ domain: ErrorDomain.STORAGE,
645
+ category: ErrorCategory.THIRD_PARTY,
646
+ details: { threadId }
647
+ },
648
+ error
649
+ );
650
+ }
60
651
  }
61
- transformEvalRow(row) {
62
- row = transformRow(row);
63
- const resultValue = JSON.parse(row.result);
64
- const testInfoValue = row.test_info ? JSON.parse(row.test_info) : void 0;
65
- if (!resultValue || typeof resultValue !== "object" || !("score" in resultValue)) {
66
- throw new Error(`Invalid MetricResult format: ${JSON.stringify(resultValue)}`);
652
+ async listThreadsByResourceId(args) {
653
+ const { resourceId, page = 0, perPage: perPageInput, orderBy } = args;
654
+ const perPage = normalizePerPage(perPageInput, 100);
655
+ if (page < 0) {
656
+ throw new MastraError(
657
+ {
658
+ id: "STORAGE_CLICKHOUSE_LIST_THREADS_BY_RESOURCE_ID_INVALID_PAGE",
659
+ domain: ErrorDomain.STORAGE,
660
+ category: ErrorCategory.USER,
661
+ details: { page }
662
+ },
663
+ new Error("page must be >= 0")
664
+ );
665
+ }
666
+ const { offset, perPage: perPageForResponse } = calculatePagination(page, perPageInput, perPage);
667
+ const { field, direction } = this.parseOrderBy(orderBy);
668
+ try {
669
+ const countResult = await this.client.query({
670
+ query: `SELECT count(DISTINCT id) as total FROM ${TABLE_THREADS} WHERE resourceId = {resourceId:String}`,
671
+ query_params: { resourceId },
672
+ clickhouse_settings: {
673
+ date_time_input_format: "best_effort",
674
+ date_time_output_format: "iso",
675
+ use_client_time_zone: 1,
676
+ output_format_json_quote_64bit_integers: 0
677
+ }
678
+ });
679
+ const countData = await countResult.json();
680
+ const total = countData.data[0].total;
681
+ if (total === 0) {
682
+ return {
683
+ threads: [],
684
+ total: 0,
685
+ page,
686
+ perPage: perPageForResponse,
687
+ hasMore: false
688
+ };
689
+ }
690
+ const dataResult = await this.client.query({
691
+ query: `
692
+ WITH ranked_threads AS (
693
+ SELECT
694
+ id,
695
+ resourceId,
696
+ title,
697
+ metadata,
698
+ toDateTime64(createdAt, 3) as createdAt,
699
+ toDateTime64(updatedAt, 3) as updatedAt,
700
+ ROW_NUMBER() OVER (PARTITION BY id ORDER BY updatedAt DESC) as row_num
701
+ FROM ${TABLE_THREADS}
702
+ WHERE resourceId = {resourceId:String}
703
+ )
704
+ SELECT
705
+ id,
706
+ resourceId,
707
+ title,
708
+ metadata,
709
+ createdAt,
710
+ updatedAt
711
+ FROM ranked_threads
712
+ WHERE row_num = 1
713
+ ORDER BY "${field}" ${direction === "DESC" ? "DESC" : "ASC"}
714
+ LIMIT {perPage:Int64} OFFSET {offset:Int64}
715
+ `,
716
+ query_params: {
717
+ resourceId,
718
+ perPage,
719
+ offset
720
+ },
721
+ clickhouse_settings: {
722
+ date_time_input_format: "best_effort",
723
+ date_time_output_format: "iso",
724
+ use_client_time_zone: 1,
725
+ output_format_json_quote_64bit_integers: 0
726
+ }
727
+ });
728
+ const rows = await dataResult.json();
729
+ const threads = transformRows(rows.data).map((thread) => ({
730
+ ...thread,
731
+ metadata: parseMetadata(thread.metadata)
732
+ }));
733
+ return {
734
+ threads,
735
+ total,
736
+ page,
737
+ perPage: perPageForResponse,
738
+ hasMore: offset + perPage < total
739
+ };
740
+ } catch (error) {
741
+ throw new MastraError(
742
+ {
743
+ id: "CLICKHOUSE_STORAGE_LIST_THREADS_BY_RESOURCE_ID_FAILED",
744
+ domain: ErrorDomain.STORAGE,
745
+ category: ErrorCategory.THIRD_PARTY,
746
+ details: { resourceId, page }
747
+ },
748
+ error
749
+ );
67
750
  }
68
- return {
69
- input: row.input,
70
- output: row.output,
71
- result: resultValue,
72
- agentName: row.agent_name,
73
- metricName: row.metric_name,
74
- instructions: row.instructions,
75
- testInfo: testInfoValue,
76
- globalRunId: row.global_run_id,
77
- runId: row.run_id,
78
- createdAt: row.created_at
79
- };
80
751
  }
81
- async getEvalsByAgentName(agentName, type) {
752
+ async updateMessages(args) {
753
+ const { messages } = args;
754
+ if (messages.length === 0) {
755
+ return [];
756
+ }
82
757
  try {
83
- const baseQuery = `SELECT *, toDateTime64(createdAt, 3) as createdAt FROM ${TABLE_EVALS} WHERE agent_name = {var_agent_name:String}`;
84
- const typeCondition = type === "test" ? " AND test_info IS NOT NULL AND JSONExtractString(test_info, 'testPath') IS NOT NULL" : type === "live" ? " AND (test_info IS NULL OR JSONExtractString(test_info, 'testPath') IS NULL)" : "";
85
- const result = await this.db.query({
86
- query: `${baseQuery}${typeCondition} ORDER BY createdAt DESC`,
87
- query_params: { var_agent_name: agentName },
758
+ const messageIds = messages.map((m) => m.id);
759
+ const existingResult = await this.client.query({
760
+ 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(",")})`,
761
+ query_params: messageIds.reduce((acc, m, i) => ({ ...acc, [`id_${i}`]: m }), {}),
88
762
  clickhouse_settings: {
89
763
  date_time_input_format: "best_effort",
90
764
  date_time_output_format: "iso",
@@ -92,131 +766,437 @@ var ClickhouseStore = class extends MastraStorage {
92
766
  output_format_json_quote_64bit_integers: 0
93
767
  }
94
768
  });
95
- if (!result) {
769
+ const existingRows = await existingResult.json();
770
+ const existingMessages = transformRows(existingRows.data);
771
+ if (existingMessages.length === 0) {
96
772
  return [];
97
773
  }
98
- const rows = await result.json();
99
- return rows.data.map((row) => this.transformEvalRow(row));
774
+ const parsedExistingMessages = existingMessages.map((msg) => {
775
+ if (typeof msg.content === "string") {
776
+ try {
777
+ msg.content = JSON.parse(msg.content);
778
+ } catch {
779
+ }
780
+ }
781
+ return msg;
782
+ });
783
+ const threadIdsToUpdate = /* @__PURE__ */ new Set();
784
+ const updatePromises = [];
785
+ for (const existingMessage of parsedExistingMessages) {
786
+ const updatePayload = messages.find((m) => m.id === existingMessage.id);
787
+ if (!updatePayload) continue;
788
+ const { id, ...fieldsToUpdate } = updatePayload;
789
+ if (Object.keys(fieldsToUpdate).length === 0) continue;
790
+ threadIdsToUpdate.add(existingMessage.threadId);
791
+ if (updatePayload.threadId && updatePayload.threadId !== existingMessage.threadId) {
792
+ threadIdsToUpdate.add(updatePayload.threadId);
793
+ }
794
+ const setClauses = [];
795
+ const values = {};
796
+ let paramIdx = 1;
797
+ let newContent = null;
798
+ const updatableFields = { ...fieldsToUpdate };
799
+ if (updatableFields.content) {
800
+ const existingContent = existingMessage.content || {};
801
+ const existingMetadata = existingContent.metadata || {};
802
+ const updateMetadata = updatableFields.content.metadata || {};
803
+ newContent = {
804
+ ...existingContent,
805
+ ...updatableFields.content,
806
+ // Deep merge metadata
807
+ metadata: {
808
+ ...existingMetadata,
809
+ ...updateMetadata
810
+ }
811
+ };
812
+ setClauses.push(`content = {var_content_${paramIdx}:String}`);
813
+ values[`var_content_${paramIdx}`] = JSON.stringify(newContent);
814
+ paramIdx++;
815
+ delete updatableFields.content;
816
+ }
817
+ for (const key in updatableFields) {
818
+ if (Object.prototype.hasOwnProperty.call(updatableFields, key)) {
819
+ const dbColumn = key === "threadId" ? "thread_id" : key;
820
+ setClauses.push(`"${dbColumn}" = {var_${key}_${paramIdx}:String}`);
821
+ values[`var_${key}_${paramIdx}`] = updatableFields[key];
822
+ paramIdx++;
823
+ }
824
+ }
825
+ if (setClauses.length > 0) {
826
+ values[`var_id_${paramIdx}`] = id;
827
+ const updateQuery = `
828
+ ALTER TABLE ${TABLE_MESSAGES}
829
+ UPDATE ${setClauses.join(", ")}
830
+ WHERE id = {var_id_${paramIdx}:String}
831
+ `;
832
+ console.info("Updating message:", id, "with query:", updateQuery, "values:", values);
833
+ updatePromises.push(
834
+ this.client.command({
835
+ query: updateQuery,
836
+ query_params: values,
837
+ clickhouse_settings: {
838
+ date_time_input_format: "best_effort",
839
+ use_client_time_zone: 1,
840
+ output_format_json_quote_64bit_integers: 0
841
+ }
842
+ })
843
+ );
844
+ }
845
+ }
846
+ if (updatePromises.length > 0) {
847
+ await Promise.all(updatePromises);
848
+ }
849
+ await this.client.command({
850
+ query: `OPTIMIZE TABLE ${TABLE_MESSAGES} FINAL`,
851
+ clickhouse_settings: {
852
+ date_time_input_format: "best_effort",
853
+ use_client_time_zone: 1,
854
+ output_format_json_quote_64bit_integers: 0
855
+ }
856
+ });
857
+ for (const existingMessage of parsedExistingMessages) {
858
+ const updatePayload = messages.find((m) => m.id === existingMessage.id);
859
+ if (!updatePayload) continue;
860
+ const { id, ...fieldsToUpdate } = updatePayload;
861
+ if (Object.keys(fieldsToUpdate).length === 0) continue;
862
+ const verifyResult = await this.client.query({
863
+ query: `SELECT id, content, role, type, "createdAt", thread_id AS "threadId", "resourceId" FROM ${TABLE_MESSAGES} WHERE id = {messageId:String}`,
864
+ query_params: { messageId: id },
865
+ clickhouse_settings: {
866
+ date_time_input_format: "best_effort",
867
+ date_time_output_format: "iso",
868
+ use_client_time_zone: 1,
869
+ output_format_json_quote_64bit_integers: 0
870
+ }
871
+ });
872
+ const verifyRows = await verifyResult.json();
873
+ if (verifyRows.data.length > 0) {
874
+ const updatedMessage = transformRows(verifyRows.data)[0];
875
+ if (updatedMessage) {
876
+ let needsRetry = false;
877
+ for (const [key, value] of Object.entries(fieldsToUpdate)) {
878
+ if (key === "content") {
879
+ const expectedContent = typeof value === "string" ? value : JSON.stringify(value);
880
+ const actualContent = typeof updatedMessage.content === "string" ? updatedMessage.content : JSON.stringify(updatedMessage.content);
881
+ if (actualContent !== expectedContent) {
882
+ needsRetry = true;
883
+ break;
884
+ }
885
+ } else if (updatedMessage[key] !== value) {
886
+ needsRetry = true;
887
+ break;
888
+ }
889
+ }
890
+ if (needsRetry) {
891
+ console.info("Update not applied correctly, retrying with DELETE + INSERT for message:", id);
892
+ await this.client.command({
893
+ query: `DELETE FROM ${TABLE_MESSAGES} WHERE id = {messageId:String}`,
894
+ query_params: { messageId: id },
895
+ clickhouse_settings: {
896
+ date_time_input_format: "best_effort",
897
+ use_client_time_zone: 1,
898
+ output_format_json_quote_64bit_integers: 0
899
+ }
900
+ });
901
+ let updatedContent = existingMessage.content || {};
902
+ if (fieldsToUpdate.content) {
903
+ const existingContent = existingMessage.content || {};
904
+ const existingMetadata = existingContent.metadata || {};
905
+ const updateMetadata = fieldsToUpdate.content.metadata || {};
906
+ updatedContent = {
907
+ ...existingContent,
908
+ ...fieldsToUpdate.content,
909
+ metadata: {
910
+ ...existingMetadata,
911
+ ...updateMetadata
912
+ }
913
+ };
914
+ }
915
+ const updatedMessageData = {
916
+ ...existingMessage,
917
+ ...fieldsToUpdate,
918
+ content: updatedContent
919
+ };
920
+ await this.client.insert({
921
+ table: TABLE_MESSAGES,
922
+ format: "JSONEachRow",
923
+ values: [
924
+ {
925
+ id: updatedMessageData.id,
926
+ thread_id: updatedMessageData.threadId,
927
+ resourceId: updatedMessageData.resourceId,
928
+ content: typeof updatedMessageData.content === "string" ? updatedMessageData.content : JSON.stringify(updatedMessageData.content),
929
+ createdAt: updatedMessageData.createdAt.toISOString(),
930
+ role: updatedMessageData.role,
931
+ type: updatedMessageData.type || "v2"
932
+ }
933
+ ],
934
+ clickhouse_settings: {
935
+ date_time_input_format: "best_effort",
936
+ use_client_time_zone: 1,
937
+ output_format_json_quote_64bit_integers: 0
938
+ }
939
+ });
940
+ }
941
+ }
942
+ }
943
+ }
944
+ if (threadIdsToUpdate.size > 0) {
945
+ await new Promise((resolve) => setTimeout(resolve, 10));
946
+ const now = (/* @__PURE__ */ new Date()).toISOString().replace("Z", "");
947
+ const threadUpdatePromises = Array.from(threadIdsToUpdate).map(async (threadId) => {
948
+ const threadResult = await this.client.query({
949
+ query: `SELECT id, resourceId, title, metadata, createdAt FROM ${TABLE_THREADS} WHERE id = {threadId:String} ORDER BY updatedAt DESC LIMIT 1`,
950
+ query_params: { threadId },
951
+ clickhouse_settings: {
952
+ date_time_input_format: "best_effort",
953
+ date_time_output_format: "iso",
954
+ use_client_time_zone: 1,
955
+ output_format_json_quote_64bit_integers: 0
956
+ }
957
+ });
958
+ const threadRows = await threadResult.json();
959
+ if (threadRows.data.length > 0) {
960
+ const existingThread = threadRows.data[0];
961
+ await this.client.command({
962
+ query: `DELETE FROM ${TABLE_THREADS} WHERE id = {threadId:String}`,
963
+ query_params: { threadId },
964
+ clickhouse_settings: {
965
+ date_time_input_format: "best_effort",
966
+ use_client_time_zone: 1,
967
+ output_format_json_quote_64bit_integers: 0
968
+ }
969
+ });
970
+ await this.client.insert({
971
+ table: TABLE_THREADS,
972
+ format: "JSONEachRow",
973
+ values: [
974
+ {
975
+ id: existingThread.id,
976
+ resourceId: existingThread.resourceId,
977
+ title: existingThread.title,
978
+ metadata: typeof existingThread.metadata === "string" ? existingThread.metadata : serializeMetadata(existingThread.metadata),
979
+ createdAt: existingThread.createdAt,
980
+ updatedAt: now
981
+ }
982
+ ],
983
+ clickhouse_settings: {
984
+ date_time_input_format: "best_effort",
985
+ use_client_time_zone: 1,
986
+ output_format_json_quote_64bit_integers: 0
987
+ }
988
+ });
989
+ }
990
+ });
991
+ await Promise.all(threadUpdatePromises);
992
+ }
993
+ const updatedMessages = [];
994
+ for (const messageId of messageIds) {
995
+ const updatedResult = await this.client.query({
996
+ query: `SELECT id, content, role, type, "createdAt", thread_id AS "threadId", "resourceId" FROM ${TABLE_MESSAGES} WHERE id = {messageId:String}`,
997
+ query_params: { messageId },
998
+ clickhouse_settings: {
999
+ date_time_input_format: "best_effort",
1000
+ date_time_output_format: "iso",
1001
+ use_client_time_zone: 1,
1002
+ output_format_json_quote_64bit_integers: 0
1003
+ }
1004
+ });
1005
+ const updatedRows = await updatedResult.json();
1006
+ if (updatedRows.data.length > 0) {
1007
+ const message = transformRows(updatedRows.data)[0];
1008
+ if (message) {
1009
+ updatedMessages.push(message);
1010
+ }
1011
+ }
1012
+ }
1013
+ return updatedMessages.map((message) => {
1014
+ if (typeof message.content === "string") {
1015
+ try {
1016
+ message.content = JSON.parse(message.content);
1017
+ } catch {
1018
+ }
1019
+ }
1020
+ return message;
1021
+ });
100
1022
  } catch (error) {
101
- if (error instanceof Error && error.message.includes("no such table")) {
102
- return [];
1023
+ throw new MastraError(
1024
+ {
1025
+ id: "CLICKHOUSE_STORAGE_UPDATE_MESSAGES_FAILED",
1026
+ domain: ErrorDomain.STORAGE,
1027
+ category: ErrorCategory.THIRD_PARTY,
1028
+ details: { messageIds: messages.map((m) => m.id).join(",") }
1029
+ },
1030
+ error
1031
+ );
1032
+ }
1033
+ }
1034
+ async getResourceById({ resourceId }) {
1035
+ try {
1036
+ const result = await this.client.query({
1037
+ query: `SELECT id, workingMemory, metadata, createdAt, updatedAt FROM ${TABLE_RESOURCES} WHERE id = {resourceId:String} ORDER BY updatedAt DESC LIMIT 1`,
1038
+ query_params: { resourceId },
1039
+ clickhouse_settings: {
1040
+ date_time_input_format: "best_effort",
1041
+ date_time_output_format: "iso",
1042
+ use_client_time_zone: 1,
1043
+ output_format_json_quote_64bit_integers: 0
1044
+ }
1045
+ });
1046
+ const rows = await result.json();
1047
+ if (rows.data.length === 0) {
1048
+ return null;
103
1049
  }
104
- this.logger.error("Failed to get evals for the specified agent: " + error?.message);
105
- throw error;
1050
+ const resource = rows.data[0];
1051
+ return {
1052
+ id: resource.id,
1053
+ workingMemory: resource.workingMemory && typeof resource.workingMemory === "object" ? JSON.stringify(resource.workingMemory) : resource.workingMemory,
1054
+ metadata: resource.metadata && typeof resource.metadata === "string" ? JSON.parse(resource.metadata) : resource.metadata,
1055
+ createdAt: new Date(resource.createdAt),
1056
+ updatedAt: new Date(resource.updatedAt)
1057
+ };
1058
+ } catch (error) {
1059
+ throw new MastraError(
1060
+ {
1061
+ id: "CLICKHOUSE_STORAGE_GET_RESOURCE_BY_ID_FAILED",
1062
+ domain: ErrorDomain.STORAGE,
1063
+ category: ErrorCategory.THIRD_PARTY,
1064
+ details: { resourceId }
1065
+ },
1066
+ error
1067
+ );
106
1068
  }
107
1069
  }
108
- async batchInsert({ tableName, records }) {
1070
+ async saveResource({ resource }) {
109
1071
  try {
110
- await this.db.insert({
111
- table: tableName,
112
- values: records.map((record) => ({
113
- ...Object.fromEntries(
114
- Object.entries(record).map(([key, value]) => [
115
- key,
116
- TABLE_SCHEMAS[tableName]?.[key]?.type === "timestamp" ? new Date(value).toISOString() : value
117
- ])
118
- )
119
- })),
1072
+ await this.client.insert({
1073
+ table: TABLE_RESOURCES,
120
1074
  format: "JSONEachRow",
1075
+ values: [
1076
+ {
1077
+ id: resource.id,
1078
+ workingMemory: resource.workingMemory,
1079
+ metadata: JSON.stringify(resource.metadata),
1080
+ createdAt: resource.createdAt.toISOString(),
1081
+ updatedAt: resource.updatedAt.toISOString()
1082
+ }
1083
+ ],
121
1084
  clickhouse_settings: {
122
- // Allows to insert serialized JS Dates (such as '2023-12-06T10:54:48.000Z')
123
1085
  date_time_input_format: "best_effort",
124
1086
  use_client_time_zone: 1,
125
1087
  output_format_json_quote_64bit_integers: 0
126
1088
  }
127
1089
  });
1090
+ return resource;
128
1091
  } catch (error) {
129
- console.error(`Error inserting into ${tableName}:`, error);
130
- throw error;
1092
+ throw new MastraError(
1093
+ {
1094
+ id: "CLICKHOUSE_STORAGE_SAVE_RESOURCE_FAILED",
1095
+ domain: ErrorDomain.STORAGE,
1096
+ category: ErrorCategory.THIRD_PARTY,
1097
+ details: { resourceId: resource.id }
1098
+ },
1099
+ error
1100
+ );
131
1101
  }
132
1102
  }
133
- async getTraces({
134
- name,
135
- scope,
136
- page,
137
- perPage,
138
- attributes,
139
- filters,
140
- fromDate,
141
- toDate
1103
+ async updateResource({
1104
+ resourceId,
1105
+ workingMemory,
1106
+ metadata
142
1107
  }) {
143
- const limit = perPage;
144
- const offset = page * perPage;
145
- const args = {};
146
- const conditions = [];
147
- if (name) {
148
- conditions.push(`name LIKE CONCAT({var_name:String}, '%')`);
149
- args.var_name = name;
150
- }
151
- if (scope) {
152
- conditions.push(`scope = {var_scope:String}`);
153
- args.var_scope = scope;
154
- }
155
- if (attributes) {
156
- Object.entries(attributes).forEach(([key, value]) => {
157
- conditions.push(`JSONExtractString(attributes, '${key}') = {var_attr_${key}:String}`);
158
- args[`var_attr_${key}`] = value;
159
- });
160
- }
161
- if (filters) {
162
- Object.entries(filters).forEach(([key, value]) => {
163
- conditions.push(
164
- `${key} = {var_col_${key}:${COLUMN_TYPES[TABLE_SCHEMAS.mastra_traces?.[key]?.type ?? "text"]}}`
165
- );
166
- args[`var_col_${key}`] = value;
167
- });
168
- }
169
- if (fromDate) {
170
- conditions.push(`createdAt >= {var_from_date:DateTime64(3)}`);
171
- args.var_from_date = fromDate.getTime() / 1e3;
172
- }
173
- if (toDate) {
174
- conditions.push(`createdAt <= {var_to_date:DateTime64(3)}`);
175
- args.var_to_date = toDate.getTime() / 1e3;
176
- }
177
- const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
178
- const result = await this.db.query({
179
- query: `SELECT *, toDateTime64(createdAt, 3) as createdAt FROM ${TABLE_TRACES} ${whereClause} ORDER BY "createdAt" DESC LIMIT ${limit} OFFSET ${offset}`,
180
- query_params: args,
181
- clickhouse_settings: {
182
- // Allows to insert serialized JS Dates (such as '2023-12-06T10:54:48.000Z')
183
- date_time_input_format: "best_effort",
184
- date_time_output_format: "iso",
185
- use_client_time_zone: 1,
186
- output_format_json_quote_64bit_integers: 0
1108
+ try {
1109
+ const existingResource = await this.getResourceById({ resourceId });
1110
+ if (!existingResource) {
1111
+ const newResource = {
1112
+ id: resourceId,
1113
+ workingMemory,
1114
+ metadata: metadata || {},
1115
+ createdAt: /* @__PURE__ */ new Date(),
1116
+ updatedAt: /* @__PURE__ */ new Date()
1117
+ };
1118
+ return this.saveResource({ resource: newResource });
187
1119
  }
188
- });
189
- if (!result) {
190
- return [];
1120
+ const updatedResource = {
1121
+ ...existingResource,
1122
+ workingMemory: workingMemory !== void 0 ? workingMemory : existingResource.workingMemory,
1123
+ metadata: {
1124
+ ...existingResource.metadata,
1125
+ ...metadata
1126
+ },
1127
+ updatedAt: /* @__PURE__ */ new Date()
1128
+ };
1129
+ const updateQuery = `
1130
+ ALTER TABLE ${TABLE_RESOURCES}
1131
+ UPDATE workingMemory = {workingMemory:String}, metadata = {metadata:String}, updatedAt = {updatedAt:String}
1132
+ WHERE id = {resourceId:String}
1133
+ `;
1134
+ await this.client.command({
1135
+ query: updateQuery,
1136
+ query_params: {
1137
+ workingMemory: updatedResource.workingMemory,
1138
+ metadata: JSON.stringify(updatedResource.metadata),
1139
+ updatedAt: updatedResource.updatedAt.toISOString().replace("Z", ""),
1140
+ resourceId
1141
+ },
1142
+ clickhouse_settings: {
1143
+ date_time_input_format: "best_effort",
1144
+ use_client_time_zone: 1,
1145
+ output_format_json_quote_64bit_integers: 0
1146
+ }
1147
+ });
1148
+ await this.client.command({
1149
+ query: `OPTIMIZE TABLE ${TABLE_RESOURCES} FINAL`,
1150
+ clickhouse_settings: {
1151
+ date_time_input_format: "best_effort",
1152
+ use_client_time_zone: 1,
1153
+ output_format_json_quote_64bit_integers: 0
1154
+ }
1155
+ });
1156
+ return updatedResource;
1157
+ } catch (error) {
1158
+ throw new MastraError(
1159
+ {
1160
+ id: "CLICKHOUSE_STORAGE_UPDATE_RESOURCE_FAILED",
1161
+ domain: ErrorDomain.STORAGE,
1162
+ category: ErrorCategory.THIRD_PARTY,
1163
+ details: { resourceId }
1164
+ },
1165
+ error
1166
+ );
191
1167
  }
192
- const resp = await result.json();
193
- const rows = resp.data;
194
- return rows.map((row) => ({
195
- id: row.id,
196
- parentSpanId: row.parentSpanId,
197
- traceId: row.traceId,
198
- name: row.name,
199
- scope: row.scope,
200
- kind: row.kind,
201
- status: safelyParseJSON(row.status),
202
- events: safelyParseJSON(row.events),
203
- links: safelyParseJSON(row.links),
204
- attributes: safelyParseJSON(row.attributes),
205
- startTime: row.startTime,
206
- endTime: row.endTime,
207
- other: safelyParseJSON(row.other),
208
- createdAt: row.createdAt
209
- }));
210
1168
  }
211
- async optimizeTable({ tableName }) {
212
- await this.db.command({
213
- query: `OPTIMIZE TABLE ${tableName} FINAL`
214
- });
1169
+ };
1170
+ var StoreOperationsClickhouse = class extends StoreOperations {
1171
+ ttl;
1172
+ client;
1173
+ constructor({ client, ttl }) {
1174
+ super();
1175
+ this.ttl = ttl;
1176
+ this.client = client;
215
1177
  }
216
- async materializeTtl({ tableName }) {
217
- await this.db.command({
218
- query: `ALTER TABLE ${tableName} MATERIALIZE TTL;`
1178
+ async hasColumn(table, column) {
1179
+ const result = await this.client.query({
1180
+ query: `DESCRIBE TABLE ${table}`,
1181
+ format: "JSONEachRow"
219
1182
  });
1183
+ const columns = await result.json();
1184
+ return columns.some((c) => c.name === column);
1185
+ }
1186
+ getSqlType(type) {
1187
+ switch (type) {
1188
+ case "text":
1189
+ return "String";
1190
+ case "timestamp":
1191
+ return "DateTime64(3)";
1192
+ case "integer":
1193
+ case "bigint":
1194
+ return "Int64";
1195
+ case "jsonb":
1196
+ return "String";
1197
+ default:
1198
+ return super.getSqlType(type);
1199
+ }
220
1200
  }
221
1201
  async createTable({
222
1202
  tableName,
@@ -226,32 +1206,33 @@ var ClickhouseStore = class extends MastraStorage {
226
1206
  const columns = Object.entries(schema).map(([name, def]) => {
227
1207
  const constraints = [];
228
1208
  if (!def.nullable) constraints.push("NOT NULL");
1209
+ if (name === "metadata" && def.type === "text" && def.nullable) {
1210
+ constraints.push("DEFAULT '{}'");
1211
+ }
229
1212
  const columnTtl = this.ttl?.[tableName]?.columns?.[name];
230
1213
  return `"${name}" ${COLUMN_TYPES[def.type]} ${constraints.join(" ")} ${columnTtl ? `TTL toDateTime(${columnTtl.ttlKey ?? "createdAt"}) + INTERVAL ${columnTtl.interval} ${columnTtl.unit}` : ""}`;
231
1214
  }).join(",\n");
232
1215
  const rowTtl = this.ttl?.[tableName]?.row;
233
1216
  const sql = tableName === TABLE_WORKFLOW_SNAPSHOT ? `
234
- CREATE TABLE IF NOT EXISTS ${tableName} (
235
- ${["id String"].concat(columns)}
236
- )
237
- ENGINE = ${TABLE_ENGINES[tableName]}
238
- PARTITION BY "createdAt"
239
- PRIMARY KEY (createdAt, run_id, workflow_name)
240
- ORDER BY (createdAt, run_id, workflow_name)
241
- ${rowTtl ? `TTL toDateTime(${rowTtl.ttlKey ?? "createdAt"}) + INTERVAL ${rowTtl.interval} ${rowTtl.unit}` : ""}
242
- SETTINGS index_granularity = 8192
243
- ` : `
244
- CREATE TABLE IF NOT EXISTS ${tableName} (
245
- ${columns}
246
- )
247
- ENGINE = ${TABLE_ENGINES[tableName]}
248
- PARTITION BY "createdAt"
249
- PRIMARY KEY (createdAt, ${tableName === TABLE_EVALS ? "run_id" : "id"})
250
- ORDER BY (createdAt, ${tableName === TABLE_EVALS ? "run_id" : "id"})
251
- ${this.ttl?.[tableName]?.row ? `TTL toDateTime(createdAt) + INTERVAL ${this.ttl[tableName].row.interval} ${this.ttl[tableName].row.unit}` : ""}
252
- SETTINGS index_granularity = 8192
253
- `;
254
- await this.db.query({
1217
+ CREATE TABLE IF NOT EXISTS ${tableName} (
1218
+ ${["id String"].concat(columns)}
1219
+ )
1220
+ ENGINE = ${TABLE_ENGINES[tableName] ?? "MergeTree()"}
1221
+ PRIMARY KEY (createdAt, run_id, workflow_name)
1222
+ ORDER BY (createdAt, run_id, workflow_name)
1223
+ ${rowTtl ? `TTL toDateTime(${rowTtl.ttlKey ?? "createdAt"}) + INTERVAL ${rowTtl.interval} ${rowTtl.unit}` : ""}
1224
+ SETTINGS index_granularity = 8192
1225
+ ` : `
1226
+ CREATE TABLE IF NOT EXISTS ${tableName} (
1227
+ ${columns}
1228
+ )
1229
+ ENGINE = ${TABLE_ENGINES[tableName] ?? "MergeTree()"}
1230
+ PRIMARY KEY (createdAt, ${"id"})
1231
+ ORDER BY (createdAt, ${"id"})
1232
+ ${this.ttl?.[tableName]?.row ? `TTL toDateTime(createdAt) + INTERVAL ${this.ttl[tableName].row.interval} ${this.ttl[tableName].row.unit}` : ""}
1233
+ SETTINGS index_granularity = 8192
1234
+ `;
1235
+ await this.client.query({
255
1236
  query: sql,
256
1237
  clickhouse_settings: {
257
1238
  // Allows to insert serialized JS Dates (such as '2023-12-06T10:54:48.000Z')
@@ -262,13 +1243,59 @@ var ClickhouseStore = class extends MastraStorage {
262
1243
  }
263
1244
  });
264
1245
  } catch (error) {
265
- console.error(`Error creating table ${tableName}:`, error);
266
- throw error;
1246
+ throw new MastraError(
1247
+ {
1248
+ id: "CLICKHOUSE_STORAGE_CREATE_TABLE_FAILED",
1249
+ domain: ErrorDomain.STORAGE,
1250
+ category: ErrorCategory.THIRD_PARTY,
1251
+ details: { tableName }
1252
+ },
1253
+ error
1254
+ );
1255
+ }
1256
+ }
1257
+ async alterTable({
1258
+ tableName,
1259
+ schema,
1260
+ ifNotExists
1261
+ }) {
1262
+ try {
1263
+ const describeSql = `DESCRIBE TABLE ${tableName}`;
1264
+ const result = await this.client.query({
1265
+ query: describeSql
1266
+ });
1267
+ const rows = await result.json();
1268
+ const existingColumnNames = new Set(rows.data.map((row) => row.name.toLowerCase()));
1269
+ for (const columnName of ifNotExists) {
1270
+ if (!existingColumnNames.has(columnName.toLowerCase()) && schema[columnName]) {
1271
+ const columnDef = schema[columnName];
1272
+ let sqlType = this.getSqlType(columnDef.type);
1273
+ if (columnDef.nullable !== false) {
1274
+ sqlType = `Nullable(${sqlType})`;
1275
+ }
1276
+ const defaultValue = columnDef.nullable === false ? this.getDefaultValue(columnDef.type) : "";
1277
+ const alterSql = `ALTER TABLE ${tableName} ADD COLUMN IF NOT EXISTS "${columnName}" ${sqlType} ${defaultValue}`.trim();
1278
+ await this.client.query({
1279
+ query: alterSql
1280
+ });
1281
+ this.logger?.debug?.(`Added column ${columnName} to table ${tableName}`);
1282
+ }
1283
+ }
1284
+ } catch (error) {
1285
+ throw new MastraError(
1286
+ {
1287
+ id: "CLICKHOUSE_STORAGE_ALTER_TABLE_FAILED",
1288
+ domain: ErrorDomain.STORAGE,
1289
+ category: ErrorCategory.THIRD_PARTY,
1290
+ details: { tableName }
1291
+ },
1292
+ error
1293
+ );
267
1294
  }
268
1295
  }
269
1296
  async clearTable({ tableName }) {
270
1297
  try {
271
- await this.db.query({
1298
+ await this.client.query({
272
1299
  query: `TRUNCATE TABLE ${tableName}`,
273
1300
  clickhouse_settings: {
274
1301
  // Allows to insert serialized JS Dates (such as '2023-12-06T10:54:48.000Z')
@@ -279,19 +1306,33 @@ var ClickhouseStore = class extends MastraStorage {
279
1306
  }
280
1307
  });
281
1308
  } catch (error) {
282
- console.error(`Error clearing table ${tableName}:`, error);
283
- throw error;
1309
+ throw new MastraError(
1310
+ {
1311
+ id: "CLICKHOUSE_STORAGE_CLEAR_TABLE_FAILED",
1312
+ domain: ErrorDomain.STORAGE,
1313
+ category: ErrorCategory.THIRD_PARTY,
1314
+ details: { tableName }
1315
+ },
1316
+ error
1317
+ );
284
1318
  }
285
1319
  }
1320
+ async dropTable({ tableName }) {
1321
+ await this.client.query({
1322
+ query: `DROP TABLE IF EXISTS ${tableName}`
1323
+ });
1324
+ }
286
1325
  async insert({ tableName, record }) {
1326
+ const createdAt = (record.createdAt || record.created_at || /* @__PURE__ */ new Date()).toISOString();
1327
+ const updatedAt = (record.updatedAt || /* @__PURE__ */ new Date()).toISOString();
287
1328
  try {
288
- await this.db.insert({
1329
+ const result = await this.client.insert({
289
1330
  table: tableName,
290
1331
  values: [
291
1332
  {
292
1333
  ...record,
293
- createdAt: record.createdAt.toISOString(),
294
- updatedAt: record.updatedAt.toISOString()
1334
+ createdAt,
1335
+ updatedAt
295
1336
  }
296
1337
  ],
297
1338
  format: "JSONEachRow",
@@ -302,13 +1343,55 @@ var ClickhouseStore = class extends MastraStorage {
302
1343
  use_client_time_zone: 1
303
1344
  }
304
1345
  });
1346
+ console.info("INSERT RESULT", result);
1347
+ } catch (error) {
1348
+ throw new MastraError(
1349
+ {
1350
+ id: "CLICKHOUSE_STORAGE_INSERT_FAILED",
1351
+ domain: ErrorDomain.STORAGE,
1352
+ category: ErrorCategory.THIRD_PARTY,
1353
+ details: { tableName }
1354
+ },
1355
+ error
1356
+ );
1357
+ }
1358
+ }
1359
+ async batchInsert({ tableName, records }) {
1360
+ const recordsToBeInserted = records.map((record) => ({
1361
+ ...Object.fromEntries(
1362
+ Object.entries(record).map(([key, value]) => [
1363
+ key,
1364
+ TABLE_SCHEMAS[tableName]?.[key]?.type === "timestamp" ? new Date(value).toISOString() : value
1365
+ ])
1366
+ )
1367
+ }));
1368
+ try {
1369
+ await this.client.insert({
1370
+ table: tableName,
1371
+ values: recordsToBeInserted,
1372
+ format: "JSONEachRow",
1373
+ clickhouse_settings: {
1374
+ // Allows to insert serialized JS Dates (such as '2023-12-06T10:54:48.000Z')
1375
+ date_time_input_format: "best_effort",
1376
+ use_client_time_zone: 1,
1377
+ output_format_json_quote_64bit_integers: 0
1378
+ }
1379
+ });
305
1380
  } catch (error) {
306
- console.error(`Error inserting into ${tableName}:`, error);
307
- throw error;
1381
+ throw new MastraError(
1382
+ {
1383
+ id: "CLICKHOUSE_STORAGE_BATCH_INSERT_FAILED",
1384
+ domain: ErrorDomain.STORAGE,
1385
+ category: ErrorCategory.THIRD_PARTY,
1386
+ details: { tableName }
1387
+ },
1388
+ error
1389
+ );
308
1390
  }
309
1391
  }
310
1392
  async load({ tableName, keys }) {
311
1393
  try {
1394
+ const engine = TABLE_ENGINES[tableName] ?? "MergeTree()";
312
1395
  const keyEntries = Object.entries(keys);
313
1396
  const conditions = keyEntries.map(
314
1397
  ([key]) => `"${key}" = {var_${key}:${COLUMN_TYPES[TABLE_SCHEMAS[tableName]?.[key]?.type ?? "text"]}}`
@@ -316,8 +1399,10 @@ var ClickhouseStore = class extends MastraStorage {
316
1399
  const values = keyEntries.reduce((acc, [key, value]) => {
317
1400
  return { ...acc, [`var_${key}`]: value };
318
1401
  }, {});
319
- const result = await this.db.query({
320
- query: `SELECT *, toDateTime64(createdAt, 3) as createdAt, toDateTime64(updatedAt, 3) as updatedAt FROM ${tableName} ${TABLE_ENGINES[tableName].startsWith("ReplacingMergeTree") ? "FINAL" : ""} WHERE ${conditions}`,
1402
+ const hasUpdatedAt = TABLE_SCHEMAS[tableName]?.updatedAt;
1403
+ const selectClause = `SELECT *, toDateTime64(createdAt, 3) as createdAt${hasUpdatedAt ? ", toDateTime64(updatedAt, 3) as updatedAt" : ""}`;
1404
+ const result = await this.client.query({
1405
+ query: `${selectClause} FROM ${tableName} ${engine.startsWith("ReplacingMergeTree") ? "FINAL" : ""} WHERE ${conditions} ORDER BY createdAt DESC LIMIT 1`,
321
1406
  query_params: values,
322
1407
  clickhouse_settings: {
323
1408
  // Allows to insert serialized JS Dates (such as '2023-12-06T10:54:48.000Z')
@@ -344,24 +1429,57 @@ var ClickhouseStore = class extends MastraStorage {
344
1429
  const data = transformRow(rows.data[0]);
345
1430
  return data;
346
1431
  } catch (error) {
347
- console.error(`Error loading from ${tableName}:`, error);
348
- throw error;
1432
+ throw new MastraError(
1433
+ {
1434
+ id: "CLICKHOUSE_STORAGE_LOAD_FAILED",
1435
+ domain: ErrorDomain.STORAGE,
1436
+ category: ErrorCategory.THIRD_PARTY,
1437
+ details: { tableName }
1438
+ },
1439
+ error
1440
+ );
349
1441
  }
350
1442
  }
351
- async getThreadById({ threadId }) {
1443
+ };
1444
+ var ScoresStorageClickhouse = class extends ScoresStorage {
1445
+ client;
1446
+ operations;
1447
+ constructor({ client, operations }) {
1448
+ super();
1449
+ this.client = client;
1450
+ this.operations = operations;
1451
+ }
1452
+ transformScoreRow(row) {
1453
+ const scorer = safelyParseJSON(row.scorer);
1454
+ const preprocessStepResult = safelyParseJSON(row.preprocessStepResult);
1455
+ const analyzeStepResult = safelyParseJSON(row.analyzeStepResult);
1456
+ const metadata = safelyParseJSON(row.metadata);
1457
+ const input = safelyParseJSON(row.input);
1458
+ const output = safelyParseJSON(row.output);
1459
+ const additionalContext = safelyParseJSON(row.additionalContext);
1460
+ const requestContext = safelyParseJSON(row.requestContext);
1461
+ const entity = safelyParseJSON(row.entity);
1462
+ return {
1463
+ ...row,
1464
+ scorer,
1465
+ preprocessStepResult,
1466
+ analyzeStepResult,
1467
+ metadata,
1468
+ input,
1469
+ output,
1470
+ additionalContext,
1471
+ requestContext,
1472
+ entity,
1473
+ createdAt: new Date(row.createdAt),
1474
+ updatedAt: new Date(row.updatedAt)
1475
+ };
1476
+ }
1477
+ async getScoreById({ id }) {
352
1478
  try {
353
- const result = await this.db.query({
354
- query: `SELECT
355
- id,
356
- "resourceId",
357
- title,
358
- metadata,
359
- toDateTime64(createdAt, 3) as createdAt,
360
- toDateTime64(updatedAt, 3) as updatedAt
361
- FROM "${TABLE_THREADS}"
362
- FINAL
363
- WHERE id = {var_id:String}`,
364
- query_params: { var_id: threadId },
1479
+ const result = await this.client.query({
1480
+ query: `SELECT * FROM ${TABLE_SCORERS} WHERE id = {var_id:String}`,
1481
+ query_params: { var_id: id },
1482
+ format: "JSONEachRow",
365
1483
  clickhouse_settings: {
366
1484
  // Allows to insert serialized JS Dates (such as '2023-12-06T10:54:48.000Z')
367
1485
  date_time_input_format: "best_effort",
@@ -370,223 +1488,269 @@ var ClickhouseStore = class extends MastraStorage {
370
1488
  output_format_json_quote_64bit_integers: 0
371
1489
  }
372
1490
  });
373
- const rows = await result.json();
374
- const thread = transformRow(rows.data[0]);
375
- if (!thread) {
1491
+ const resultJson = await result.json();
1492
+ if (!Array.isArray(resultJson) || resultJson.length === 0) {
376
1493
  return null;
377
1494
  }
378
- return {
379
- ...thread,
380
- metadata: typeof thread.metadata === "string" ? JSON.parse(thread.metadata) : thread.metadata,
381
- createdAt: thread.createdAt,
382
- updatedAt: thread.updatedAt
383
- };
1495
+ return this.transformScoreRow(resultJson[0]);
384
1496
  } catch (error) {
385
- console.error(`Error getting thread ${threadId}:`, error);
386
- throw error;
1497
+ throw new MastraError(
1498
+ {
1499
+ id: "CLICKHOUSE_STORAGE_GET_SCORE_BY_ID_FAILED",
1500
+ domain: ErrorDomain.STORAGE,
1501
+ category: ErrorCategory.THIRD_PARTY,
1502
+ details: { scoreId: id }
1503
+ },
1504
+ error
1505
+ );
387
1506
  }
388
1507
  }
389
- async getThreadsByResourceId({ resourceId }) {
1508
+ async saveScore(score) {
1509
+ let parsedScore;
390
1510
  try {
391
- const result = await this.db.query({
392
- query: `SELECT
393
- id,
394
- "resourceId",
395
- title,
396
- metadata,
397
- toDateTime64(createdAt, 3) as createdAt,
398
- toDateTime64(updatedAt, 3) as updatedAt
399
- FROM "${TABLE_THREADS}"
400
- WHERE "resourceId" = {var_resourceId:String}`,
401
- query_params: { var_resourceId: resourceId },
402
- clickhouse_settings: {
403
- // Allows to insert serialized JS Dates (such as '2023-12-06T10:54:48.000Z')
404
- date_time_input_format: "best_effort",
405
- date_time_output_format: "iso",
406
- use_client_time_zone: 1,
407
- output_format_json_quote_64bit_integers: 0
408
- }
409
- });
410
- const rows = await result.json();
411
- const threads = transformRows(rows.data);
412
- return threads.map((thread) => ({
413
- ...thread,
414
- metadata: typeof thread.metadata === "string" ? JSON.parse(thread.metadata) : thread.metadata,
415
- createdAt: thread.createdAt,
416
- updatedAt: thread.updatedAt
417
- }));
1511
+ parsedScore = saveScorePayloadSchema.parse(score);
418
1512
  } catch (error) {
419
- console.error(`Error getting threads for resource ${resourceId}:`, error);
420
- throw error;
1513
+ throw new MastraError(
1514
+ {
1515
+ id: "CLICKHOUSE_STORAGE_SAVE_SCORE_FAILED_INVALID_SCORE_PAYLOAD",
1516
+ domain: ErrorDomain.STORAGE,
1517
+ category: ErrorCategory.USER,
1518
+ details: { scoreId: score.id }
1519
+ },
1520
+ error
1521
+ );
421
1522
  }
422
- }
423
- async saveThread({ thread }) {
424
1523
  try {
425
- await this.db.insert({
426
- table: TABLE_THREADS,
427
- values: [
428
- {
429
- ...thread,
430
- createdAt: thread.createdAt.toISOString(),
431
- updatedAt: thread.updatedAt.toISOString()
432
- }
433
- ],
1524
+ const record = {
1525
+ ...parsedScore
1526
+ };
1527
+ await this.client.insert({
1528
+ table: TABLE_SCORERS,
1529
+ values: [record],
434
1530
  format: "JSONEachRow",
435
1531
  clickhouse_settings: {
436
- // Allows to insert serialized JS Dates (such as '2023-12-06T10:54:48.000Z')
437
1532
  date_time_input_format: "best_effort",
438
1533
  use_client_time_zone: 1,
439
1534
  output_format_json_quote_64bit_integers: 0
440
1535
  }
441
1536
  });
442
- return thread;
1537
+ return { score };
443
1538
  } catch (error) {
444
- console.error("Error saving thread:", error);
445
- throw error;
1539
+ throw new MastraError(
1540
+ {
1541
+ id: "CLICKHOUSE_STORAGE_SAVE_SCORE_FAILED",
1542
+ domain: ErrorDomain.STORAGE,
1543
+ category: ErrorCategory.THIRD_PARTY,
1544
+ details: { scoreId: score.id }
1545
+ },
1546
+ error
1547
+ );
446
1548
  }
447
1549
  }
448
- async updateThread({
449
- id,
450
- title,
451
- metadata
1550
+ async listScoresByRunId({
1551
+ runId,
1552
+ pagination
452
1553
  }) {
453
1554
  try {
454
- const existingThread = await this.getThreadById({ threadId: id });
455
- if (!existingThread) {
456
- throw new Error(`Thread ${id} not found`);
1555
+ const countResult = await this.client.query({
1556
+ query: `SELECT COUNT(*) as count FROM ${TABLE_SCORERS} WHERE runId = {var_runId:String}`,
1557
+ query_params: { var_runId: runId },
1558
+ format: "JSONEachRow"
1559
+ });
1560
+ const countRows = await countResult.json();
1561
+ let total = 0;
1562
+ if (Array.isArray(countRows) && countRows.length > 0 && countRows[0]) {
1563
+ const countObj = countRows[0];
1564
+ total = Number(countObj.count);
457
1565
  }
458
- const mergedMetadata = {
459
- ...existingThread.metadata,
460
- ...metadata
461
- };
462
- const updatedThread = {
463
- ...existingThread,
464
- title,
465
- metadata: mergedMetadata,
466
- updatedAt: /* @__PURE__ */ new Date()
467
- };
468
- await this.db.insert({
469
- table: TABLE_THREADS,
470
- values: [
471
- {
472
- ...updatedThread,
473
- updatedAt: updatedThread.updatedAt.toISOString()
474
- }
475
- ],
1566
+ const { page, perPage: perPageInput } = pagination;
1567
+ if (!total) {
1568
+ return {
1569
+ pagination: {
1570
+ total: 0,
1571
+ page,
1572
+ perPage: perPageInput,
1573
+ hasMore: false
1574
+ },
1575
+ scores: []
1576
+ };
1577
+ }
1578
+ const perPage = normalizePerPage(perPageInput, 100);
1579
+ const { offset: start, perPage: perPageForResponse } = calculatePagination(page, perPageInput, perPage);
1580
+ const limitValue = perPageInput === false ? total : perPage;
1581
+ const end = perPageInput === false ? total : start + perPage;
1582
+ const result = await this.client.query({
1583
+ query: `SELECT * FROM ${TABLE_SCORERS} WHERE runId = {var_runId:String} ORDER BY createdAt DESC LIMIT {var_limit:Int64} OFFSET {var_offset:Int64}`,
1584
+ query_params: {
1585
+ var_runId: runId,
1586
+ var_limit: limitValue,
1587
+ var_offset: start
1588
+ },
476
1589
  format: "JSONEachRow",
477
1590
  clickhouse_settings: {
478
- // Allows to insert serialized JS Dates (such as '2023-12-06T10:54:48.000Z')
479
1591
  date_time_input_format: "best_effort",
1592
+ date_time_output_format: "iso",
480
1593
  use_client_time_zone: 1,
481
1594
  output_format_json_quote_64bit_integers: 0
482
1595
  }
483
1596
  });
484
- return updatedThread;
1597
+ const rows = await result.json();
1598
+ const scores = Array.isArray(rows) ? rows.map((row) => this.transformScoreRow(row)) : [];
1599
+ return {
1600
+ pagination: {
1601
+ total,
1602
+ page,
1603
+ perPage: perPageForResponse,
1604
+ hasMore: end < total
1605
+ },
1606
+ scores
1607
+ };
485
1608
  } catch (error) {
486
- console.error("Error updating thread:", error);
487
- throw error;
1609
+ throw new MastraError(
1610
+ {
1611
+ id: "CLICKHOUSE_STORAGE_GET_SCORES_BY_RUN_ID_FAILED",
1612
+ domain: ErrorDomain.STORAGE,
1613
+ category: ErrorCategory.THIRD_PARTY,
1614
+ details: { runId }
1615
+ },
1616
+ error
1617
+ );
488
1618
  }
489
1619
  }
490
- async deleteThread({ threadId }) {
1620
+ async listScoresByScorerId({
1621
+ scorerId,
1622
+ entityId,
1623
+ entityType,
1624
+ source,
1625
+ pagination
1626
+ }) {
1627
+ let whereClause = `scorerId = {var_scorerId:String}`;
1628
+ if (entityId) {
1629
+ whereClause += ` AND entityId = {var_entityId:String}`;
1630
+ }
1631
+ if (entityType) {
1632
+ whereClause += ` AND entityType = {var_entityType:String}`;
1633
+ }
1634
+ if (source) {
1635
+ whereClause += ` AND source = {var_source:String}`;
1636
+ }
491
1637
  try {
492
- await this.db.command({
493
- query: `DELETE FROM "${TABLE_MESSAGES}" WHERE thread_id = '${threadId}';`,
494
- query_params: { var_thread_id: threadId },
495
- clickhouse_settings: {
496
- output_format_json_quote_64bit_integers: 0
497
- }
1638
+ const countResult = await this.client.query({
1639
+ query: `SELECT COUNT(*) as count FROM ${TABLE_SCORERS} WHERE ${whereClause}`,
1640
+ query_params: {
1641
+ var_scorerId: scorerId,
1642
+ var_entityId: entityId,
1643
+ var_entityType: entityType,
1644
+ var_source: source
1645
+ },
1646
+ format: "JSONEachRow"
498
1647
  });
499
- await this.db.command({
500
- query: `DELETE FROM "${TABLE_THREADS}" WHERE id = {var_id:String};`,
501
- query_params: { var_id: threadId },
1648
+ const countRows = await countResult.json();
1649
+ let total = 0;
1650
+ if (Array.isArray(countRows) && countRows.length > 0 && countRows[0]) {
1651
+ const countObj = countRows[0];
1652
+ total = Number(countObj.count);
1653
+ }
1654
+ const { page, perPage: perPageInput } = pagination;
1655
+ if (!total) {
1656
+ return {
1657
+ pagination: {
1658
+ total: 0,
1659
+ page,
1660
+ perPage: perPageInput,
1661
+ hasMore: false
1662
+ },
1663
+ scores: []
1664
+ };
1665
+ }
1666
+ const perPage = normalizePerPage(perPageInput, 100);
1667
+ const { offset: start, perPage: perPageForResponse } = calculatePagination(page, perPageInput, perPage);
1668
+ const limitValue = perPageInput === false ? total : perPage;
1669
+ const end = perPageInput === false ? total : start + perPage;
1670
+ const result = await this.client.query({
1671
+ query: `SELECT * FROM ${TABLE_SCORERS} WHERE ${whereClause} ORDER BY createdAt DESC LIMIT {var_limit:Int64} OFFSET {var_offset:Int64}`,
1672
+ query_params: {
1673
+ var_scorerId: scorerId,
1674
+ var_limit: limitValue,
1675
+ var_offset: start,
1676
+ var_entityId: entityId,
1677
+ var_entityType: entityType,
1678
+ var_source: source
1679
+ },
1680
+ format: "JSONEachRow",
502
1681
  clickhouse_settings: {
1682
+ date_time_input_format: "best_effort",
1683
+ date_time_output_format: "iso",
1684
+ use_client_time_zone: 1,
503
1685
  output_format_json_quote_64bit_integers: 0
504
1686
  }
505
1687
  });
1688
+ const rows = await result.json();
1689
+ const scores = Array.isArray(rows) ? rows.map((row) => this.transformScoreRow(row)) : [];
1690
+ return {
1691
+ pagination: {
1692
+ total,
1693
+ page,
1694
+ perPage: perPageForResponse,
1695
+ hasMore: end < total
1696
+ },
1697
+ scores
1698
+ };
506
1699
  } catch (error) {
507
- console.error("Error deleting thread:", error);
508
- throw error;
1700
+ throw new MastraError(
1701
+ {
1702
+ id: "CLICKHOUSE_STORAGE_GET_SCORES_BY_SCORER_ID_FAILED",
1703
+ domain: ErrorDomain.STORAGE,
1704
+ category: ErrorCategory.THIRD_PARTY,
1705
+ details: { scorerId }
1706
+ },
1707
+ error
1708
+ );
509
1709
  }
510
1710
  }
511
- async getMessages({ threadId, selectBy }) {
1711
+ async listScoresByEntityId({
1712
+ entityId,
1713
+ entityType,
1714
+ pagination
1715
+ }) {
512
1716
  try {
513
- const messages = [];
514
- const limit = typeof selectBy?.last === `number` ? selectBy.last : 40;
515
- const include = selectBy?.include || [];
516
- if (include.length) {
517
- const includeResult = await this.db.query({
518
- query: `
519
- WITH ordered_messages AS (
520
- SELECT
521
- *,
522
- toDateTime64(createdAt, 3) as createdAt,
523
- toDateTime64(updatedAt, 3) as updatedAt,
524
- ROW_NUMBER() OVER (ORDER BY "createdAt" DESC) as row_num
525
- FROM "${TABLE_MESSAGES}"
526
- WHERE thread_id = {var_thread_id:String}
527
- )
528
- SELECT
529
- m.id AS id,
530
- m.content as content,
531
- m.role as role,
532
- m.type as type,
533
- m.createdAt as createdAt,
534
- m.updatedAt as updatedAt,
535
- m.thread_id AS "threadId"
536
- FROM ordered_messages m
537
- WHERE m.id = ANY({var_include:Array(String)})
538
- OR EXISTS (
539
- SELECT 1 FROM ordered_messages target
540
- WHERE target.id = ANY({var_include:Array(String)})
541
- AND (
542
- -- Get previous messages based on the max withPreviousMessages
543
- (m.row_num <= target.row_num + {var_withPreviousMessages:Int64} AND m.row_num > target.row_num)
544
- OR
545
- -- Get next messages based on the max withNextMessages
546
- (m.row_num >= target.row_num - {var_withNextMessages:Int64} AND m.row_num < target.row_num)
547
- )
548
- )
549
- ORDER BY m."createdAt" DESC
550
- `,
551
- query_params: {
552
- var_thread_id: threadId,
553
- var_include: include.map((i) => i.id),
554
- var_withPreviousMessages: Math.max(...include.map((i) => i.withPreviousMessages || 0)),
555
- var_withNextMessages: Math.max(...include.map((i) => i.withNextMessages || 0))
1717
+ const countResult = await this.client.query({
1718
+ query: `SELECT COUNT(*) as count FROM ${TABLE_SCORERS} WHERE entityId = {var_entityId:String} AND entityType = {var_entityType:String}`,
1719
+ query_params: { var_entityId: entityId, var_entityType: entityType },
1720
+ format: "JSONEachRow"
1721
+ });
1722
+ const countRows = await countResult.json();
1723
+ let total = 0;
1724
+ if (Array.isArray(countRows) && countRows.length > 0 && countRows[0]) {
1725
+ const countObj = countRows[0];
1726
+ total = Number(countObj.count);
1727
+ }
1728
+ const { page, perPage: perPageInput } = pagination;
1729
+ if (!total) {
1730
+ return {
1731
+ pagination: {
1732
+ total: 0,
1733
+ page,
1734
+ perPage: perPageInput,
1735
+ hasMore: false
556
1736
  },
557
- clickhouse_settings: {
558
- // Allows to insert serialized JS Dates (such as '2023-12-06T10:54:48.000Z')
559
- date_time_input_format: "best_effort",
560
- date_time_output_format: "iso",
561
- use_client_time_zone: 1,
562
- output_format_json_quote_64bit_integers: 0
563
- }
564
- });
565
- const rows2 = await includeResult.json();
566
- messages.push(...transformRows(rows2.data));
1737
+ scores: []
1738
+ };
567
1739
  }
568
- const result = await this.db.query({
569
- query: `
570
- SELECT
571
- id,
572
- content,
573
- role,
574
- type,
575
- toDateTime64(createdAt, 3) as createdAt,
576
- thread_id AS "threadId"
577
- FROM "${TABLE_MESSAGES}"
578
- WHERE thread_id = {threadId:String}
579
- AND id NOT IN ({exclude:Array(String)})
580
- ORDER BY "createdAt" DESC
581
- LIMIT {limit:Int64}
582
- `,
1740
+ const perPage = normalizePerPage(perPageInput, 100);
1741
+ const { offset: start, perPage: perPageForResponse } = calculatePagination(page, perPageInput, perPage);
1742
+ const limitValue = perPageInput === false ? total : perPage;
1743
+ const end = perPageInput === false ? total : start + perPage;
1744
+ const result = await this.client.query({
1745
+ query: `SELECT * FROM ${TABLE_SCORERS} WHERE entityId = {var_entityId:String} AND entityType = {var_entityType:String} ORDER BY createdAt DESC LIMIT {var_limit:Int64} OFFSET {var_offset:Int64}`,
583
1746
  query_params: {
584
- threadId,
585
- exclude: messages.map((m) => m.id),
586
- limit
1747
+ var_entityId: entityId,
1748
+ var_entityType: entityType,
1749
+ var_limit: limitValue,
1750
+ var_offset: start
587
1751
  },
1752
+ format: "JSONEachRow",
588
1753
  clickhouse_settings: {
589
- // Allows to insert serialized JS Dates (such as '2023-12-06T10:54:48.000Z')
590
1754
  date_time_input_format: "best_effort",
591
1755
  date_time_output_format: "iso",
592
1756
  use_client_time_zone: 1,
@@ -594,80 +1758,154 @@ var ClickhouseStore = class extends MastraStorage {
594
1758
  }
595
1759
  });
596
1760
  const rows = await result.json();
597
- messages.push(...transformRows(rows.data));
598
- messages.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
599
- messages.forEach((message) => {
600
- if (typeof message.content === "string") {
601
- try {
602
- message.content = JSON.parse(message.content);
603
- } catch {
604
- }
605
- }
606
- });
607
- return messages;
1761
+ const scores = Array.isArray(rows) ? rows.map((row) => this.transformScoreRow(row)) : [];
1762
+ return {
1763
+ pagination: {
1764
+ total,
1765
+ page,
1766
+ perPage: perPageForResponse,
1767
+ hasMore: end < total
1768
+ },
1769
+ scores
1770
+ };
608
1771
  } catch (error) {
609
- console.error("Error getting messages:", error);
610
- throw error;
1772
+ throw new MastraError(
1773
+ {
1774
+ id: "CLICKHOUSE_STORAGE_GET_SCORES_BY_ENTITY_ID_FAILED",
1775
+ domain: ErrorDomain.STORAGE,
1776
+ category: ErrorCategory.THIRD_PARTY,
1777
+ details: { entityId, entityType }
1778
+ },
1779
+ error
1780
+ );
611
1781
  }
612
1782
  }
613
- async saveMessages({ messages }) {
614
- if (messages.length === 0) return messages;
1783
+ async listScoresBySpan({
1784
+ traceId,
1785
+ spanId,
1786
+ pagination
1787
+ }) {
615
1788
  try {
616
- const threadId = messages[0]?.threadId;
617
- if (!threadId) {
618
- throw new Error("Thread ID is required");
1789
+ const countResult = await this.client.query({
1790
+ query: `SELECT COUNT(*) as count FROM ${TABLE_SCORERS} WHERE traceId = {var_traceId:String} AND spanId = {var_spanId:String}`,
1791
+ query_params: {
1792
+ var_traceId: traceId,
1793
+ var_spanId: spanId
1794
+ },
1795
+ format: "JSONEachRow"
1796
+ });
1797
+ const countRows = await countResult.json();
1798
+ let total = 0;
1799
+ if (Array.isArray(countRows) && countRows.length > 0 && countRows[0]) {
1800
+ const countObj = countRows[0];
1801
+ total = Number(countObj.count);
619
1802
  }
620
- const thread = await this.getThreadById({ threadId });
621
- if (!thread) {
622
- throw new Error(`Thread ${threadId} not found`);
1803
+ const { page, perPage: perPageInput } = pagination;
1804
+ if (!total) {
1805
+ return {
1806
+ pagination: {
1807
+ total: 0,
1808
+ page,
1809
+ perPage: perPageInput,
1810
+ hasMore: false
1811
+ },
1812
+ scores: []
1813
+ };
623
1814
  }
624
- await this.db.insert({
625
- table: TABLE_MESSAGES,
1815
+ const perPage = normalizePerPage(perPageInput, 100);
1816
+ const { offset: start, perPage: perPageForResponse } = calculatePagination(page, perPageInput, perPage);
1817
+ const limitValue = perPageInput === false ? total : perPage;
1818
+ const end = perPageInput === false ? total : start + perPage;
1819
+ const result = await this.client.query({
1820
+ query: `SELECT * FROM ${TABLE_SCORERS} WHERE traceId = {var_traceId:String} AND spanId = {var_spanId:String} ORDER BY createdAt DESC LIMIT {var_limit:Int64} OFFSET {var_offset:Int64}`,
1821
+ query_params: {
1822
+ var_traceId: traceId,
1823
+ var_spanId: spanId,
1824
+ var_limit: limitValue,
1825
+ var_offset: start
1826
+ },
626
1827
  format: "JSONEachRow",
627
- values: messages.map((message) => ({
628
- id: message.id,
629
- thread_id: threadId,
630
- content: typeof message.content === "string" ? message.content : JSON.stringify(message.content),
631
- createdAt: message.createdAt.toISOString(),
632
- role: message.role,
633
- type: message.type
634
- })),
635
1828
  clickhouse_settings: {
636
- // Allows to insert serialized JS Dates (such as '2023-12-06T10:54:48.000Z')
637
1829
  date_time_input_format: "best_effort",
1830
+ date_time_output_format: "iso",
638
1831
  use_client_time_zone: 1,
639
1832
  output_format_json_quote_64bit_integers: 0
640
1833
  }
641
1834
  });
642
- return messages;
1835
+ const rows = await result.json();
1836
+ const scores = Array.isArray(rows) ? rows.map((row) => this.transformScoreRow(row)) : [];
1837
+ return {
1838
+ pagination: {
1839
+ total,
1840
+ page,
1841
+ perPage: perPageForResponse,
1842
+ hasMore: end < total
1843
+ },
1844
+ scores
1845
+ };
643
1846
  } catch (error) {
644
- console.error("Error saving messages:", error);
645
- throw error;
1847
+ throw new MastraError(
1848
+ {
1849
+ id: "CLICKHOUSE_STORAGE_GET_SCORES_BY_SPAN_FAILED",
1850
+ domain: ErrorDomain.STORAGE,
1851
+ category: ErrorCategory.THIRD_PARTY,
1852
+ details: { traceId, spanId }
1853
+ },
1854
+ error
1855
+ );
646
1856
  }
647
1857
  }
1858
+ };
1859
+ var WorkflowsStorageClickhouse = class extends WorkflowsStorage {
1860
+ client;
1861
+ operations;
1862
+ constructor({ client, operations }) {
1863
+ super();
1864
+ this.operations = operations;
1865
+ this.client = client;
1866
+ }
1867
+ updateWorkflowResults({
1868
+ // workflowName,
1869
+ // runId,
1870
+ // stepId,
1871
+ // result,
1872
+ // requestContext,
1873
+ }) {
1874
+ throw new Error("Method not implemented.");
1875
+ }
1876
+ updateWorkflowState({
1877
+ // workflowName,
1878
+ // runId,
1879
+ // opts,
1880
+ }) {
1881
+ throw new Error("Method not implemented.");
1882
+ }
648
1883
  async persistWorkflowSnapshot({
649
1884
  workflowName,
650
1885
  runId,
1886
+ resourceId,
651
1887
  snapshot
652
1888
  }) {
653
1889
  try {
654
- const currentSnapshot = await this.load({
1890
+ const currentSnapshot = await this.operations.load({
655
1891
  tableName: TABLE_WORKFLOW_SNAPSHOT,
656
1892
  keys: { workflow_name: workflowName, run_id: runId }
657
1893
  });
658
1894
  const now = /* @__PURE__ */ new Date();
659
1895
  const persisting = currentSnapshot ? {
660
1896
  ...currentSnapshot,
1897
+ resourceId,
661
1898
  snapshot: JSON.stringify(snapshot),
662
1899
  updatedAt: now.toISOString()
663
1900
  } : {
664
1901
  workflow_name: workflowName,
665
1902
  run_id: runId,
1903
+ resourceId,
666
1904
  snapshot: JSON.stringify(snapshot),
667
1905
  createdAt: now.toISOString(),
668
1906
  updatedAt: now.toISOString()
669
1907
  };
670
- await this.db.insert({
1908
+ await this.client.insert({
671
1909
  table: TABLE_WORKFLOW_SNAPSHOT,
672
1910
  format: "JSONEachRow",
673
1911
  values: [persisting],
@@ -679,8 +1917,15 @@ var ClickhouseStore = class extends MastraStorage {
679
1917
  }
680
1918
  });
681
1919
  } catch (error) {
682
- console.error("Error persisting workflow snapshot:", error);
683
- throw error;
1920
+ throw new MastraError(
1921
+ {
1922
+ id: "CLICKHOUSE_STORAGE_PERSIST_WORKFLOW_SNAPSHOT_FAILED",
1923
+ domain: ErrorDomain.STORAGE,
1924
+ category: ErrorCategory.THIRD_PARTY,
1925
+ details: { workflowName, runId }
1926
+ },
1927
+ error
1928
+ );
684
1929
  }
685
1930
  }
686
1931
  async loadWorkflowSnapshot({
@@ -688,7 +1933,7 @@ var ClickhouseStore = class extends MastraStorage {
688
1933
  runId
689
1934
  }) {
690
1935
  try {
691
- const result = await this.load({
1936
+ const result = await this.operations.load({
692
1937
  tableName: TABLE_WORKFLOW_SNAPSHOT,
693
1938
  keys: {
694
1939
  workflow_name: workflowName,
@@ -700,8 +1945,15 @@ var ClickhouseStore = class extends MastraStorage {
700
1945
  }
701
1946
  return result.snapshot;
702
1947
  } catch (error) {
703
- console.error("Error loading workflow snapshot:", error);
704
- throw error;
1948
+ throw new MastraError(
1949
+ {
1950
+ id: "CLICKHOUSE_STORAGE_LOAD_WORKFLOW_SNAPSHOT_FAILED",
1951
+ domain: ErrorDomain.STORAGE,
1952
+ category: ErrorCategory.THIRD_PARTY,
1953
+ details: { workflowName, runId }
1954
+ },
1955
+ error
1956
+ );
705
1957
  }
706
1958
  }
707
1959
  parseWorkflowRun(row) {
@@ -722,13 +1974,14 @@ var ClickhouseStore = class extends MastraStorage {
722
1974
  resourceId: row.resourceId
723
1975
  };
724
1976
  }
725
- async getWorkflowRuns({
1977
+ async listWorkflowRuns({
726
1978
  workflowName,
727
1979
  fromDate,
728
1980
  toDate,
729
- limit,
730
- offset,
731
- resourceId
1981
+ page,
1982
+ perPage,
1983
+ resourceId,
1984
+ status
732
1985
  } = {}) {
733
1986
  try {
734
1987
  const conditions = [];
@@ -737,8 +1990,12 @@ var ClickhouseStore = class extends MastraStorage {
737
1990
  conditions.push(`workflow_name = {var_workflow_name:String}`);
738
1991
  values.var_workflow_name = workflowName;
739
1992
  }
1993
+ if (status) {
1994
+ conditions.push(`JSONExtractString(snapshot, 'status') = {var_status:String}`);
1995
+ values.var_status = status;
1996
+ }
740
1997
  if (resourceId) {
741
- const hasResourceId = await this.hasColumn(TABLE_WORKFLOW_SNAPSHOT, "resourceId");
1998
+ const hasResourceId = await this.operations.hasColumn(TABLE_WORKFLOW_SNAPSHOT, "resourceId");
742
1999
  if (hasResourceId) {
743
2000
  conditions.push(`resourceId = {var_resourceId:String}`);
744
2001
  values.var_resourceId = resourceId;
@@ -755,11 +2012,14 @@ var ClickhouseStore = class extends MastraStorage {
755
2012
  values.var_to_date = toDate.getTime() / 1e3;
756
2013
  }
757
2014
  const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
758
- const limitClause = limit !== void 0 ? `LIMIT ${limit}` : "";
759
- const offsetClause = offset !== void 0 ? `OFFSET ${offset}` : "";
2015
+ const usePagination = perPage !== void 0 && page !== void 0;
2016
+ const normalizedPerPage = usePagination ? normalizePerPage(perPage, Number.MAX_SAFE_INTEGER) : 0;
2017
+ const offset = usePagination ? page * normalizedPerPage : 0;
2018
+ const limitClause = usePagination ? `LIMIT ${normalizedPerPage}` : "";
2019
+ const offsetClause = usePagination ? `OFFSET ${offset}` : "";
760
2020
  let total = 0;
761
- if (limit !== void 0 && offset !== void 0) {
762
- const countResult = await this.db.query({
2021
+ if (usePagination) {
2022
+ const countResult = await this.client.query({
763
2023
  query: `SELECT COUNT(*) as count FROM ${TABLE_WORKFLOW_SNAPSHOT} ${TABLE_ENGINES[TABLE_WORKFLOW_SNAPSHOT].startsWith("ReplacingMergeTree") ? "FINAL" : ""} ${whereClause}`,
764
2024
  query_params: values,
765
2025
  format: "JSONEachRow"
@@ -767,21 +2027,21 @@ var ClickhouseStore = class extends MastraStorage {
767
2027
  const countRows = await countResult.json();
768
2028
  total = Number(countRows[0]?.count ?? 0);
769
2029
  }
770
- const result = await this.db.query({
2030
+ const result = await this.client.query({
771
2031
  query: `
772
- SELECT
773
- workflow_name,
774
- run_id,
775
- snapshot,
776
- toDateTime64(createdAt, 3) as createdAt,
777
- toDateTime64(updatedAt, 3) as updatedAt,
778
- resourceId
779
- FROM ${TABLE_WORKFLOW_SNAPSHOT} ${TABLE_ENGINES[TABLE_WORKFLOW_SNAPSHOT].startsWith("ReplacingMergeTree") ? "FINAL" : ""}
780
- ${whereClause}
781
- ORDER BY createdAt DESC
782
- ${limitClause}
783
- ${offsetClause}
784
- `,
2032
+ SELECT
2033
+ workflow_name,
2034
+ run_id,
2035
+ snapshot,
2036
+ toDateTime64(createdAt, 3) as createdAt,
2037
+ toDateTime64(updatedAt, 3) as updatedAt,
2038
+ resourceId
2039
+ FROM ${TABLE_WORKFLOW_SNAPSHOT} ${TABLE_ENGINES[TABLE_WORKFLOW_SNAPSHOT].startsWith("ReplacingMergeTree") ? "FINAL" : ""}
2040
+ ${whereClause}
2041
+ ORDER BY createdAt DESC
2042
+ ${limitClause}
2043
+ ${offsetClause}
2044
+ `,
785
2045
  query_params: values,
786
2046
  format: "JSONEachRow"
787
2047
  });
@@ -792,8 +2052,15 @@ var ClickhouseStore = class extends MastraStorage {
792
2052
  });
793
2053
  return { runs, total: total || runs.length };
794
2054
  } catch (error) {
795
- console.error("Error getting workflow runs:", error);
796
- throw error;
2055
+ throw new MastraError(
2056
+ {
2057
+ id: "CLICKHOUSE_STORAGE_LIST_WORKFLOW_RUNS_FAILED",
2058
+ domain: ErrorDomain.STORAGE,
2059
+ category: ErrorCategory.THIRD_PARTY,
2060
+ details: { workflowName: workflowName ?? "", resourceId: resourceId ?? "" }
2061
+ },
2062
+ error
2063
+ );
797
2064
  }
798
2065
  }
799
2066
  async getWorkflowRunById({
@@ -812,18 +2079,19 @@ var ClickhouseStore = class extends MastraStorage {
812
2079
  values.var_workflow_name = workflowName;
813
2080
  }
814
2081
  const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
815
- const result = await this.db.query({
2082
+ const result = await this.client.query({
816
2083
  query: `
817
- SELECT
818
- workflow_name,
819
- run_id,
820
- snapshot,
821
- toDateTime64(createdAt, 3) as createdAt,
822
- toDateTime64(updatedAt, 3) as updatedAt,
823
- resourceId
824
- FROM ${TABLE_WORKFLOW_SNAPSHOT} ${TABLE_ENGINES[TABLE_WORKFLOW_SNAPSHOT].startsWith("ReplacingMergeTree") ? "FINAL" : ""}
825
- ${whereClause}
826
- `,
2084
+ SELECT
2085
+ workflow_name,
2086
+ run_id,
2087
+ snapshot,
2088
+ toDateTime64(createdAt, 3) as createdAt,
2089
+ toDateTime64(updatedAt, 3) as updatedAt,
2090
+ resourceId
2091
+ FROM ${TABLE_WORKFLOW_SNAPSHOT} ${TABLE_ENGINES[TABLE_WORKFLOW_SNAPSHOT].startsWith("ReplacingMergeTree") ? "FINAL" : ""}
2092
+ ${whereClause}
2093
+ ORDER BY createdAt DESC LIMIT 1
2094
+ `,
827
2095
  query_params: values,
828
2096
  format: "JSONEachRow"
829
2097
  });
@@ -833,17 +2101,230 @@ var ClickhouseStore = class extends MastraStorage {
833
2101
  }
834
2102
  return this.parseWorkflowRun(resultJson[0]);
835
2103
  } catch (error) {
836
- console.error("Error getting workflow run by ID:", error);
837
- throw error;
2104
+ throw new MastraError(
2105
+ {
2106
+ id: "CLICKHOUSE_STORAGE_GET_WORKFLOW_RUN_BY_ID_FAILED",
2107
+ domain: ErrorDomain.STORAGE,
2108
+ category: ErrorCategory.THIRD_PARTY,
2109
+ details: { runId: runId ?? "", workflowName: workflowName ?? "" }
2110
+ },
2111
+ error
2112
+ );
838
2113
  }
839
2114
  }
840
- async hasColumn(table, column) {
841
- const result = await this.db.query({
842
- query: `DESCRIBE TABLE ${table}`,
843
- format: "JSONEachRow"
2115
+ };
2116
+
2117
+ // src/storage/index.ts
2118
+ var ClickhouseStore = class extends MastraStorage {
2119
+ db;
2120
+ ttl = {};
2121
+ stores;
2122
+ constructor(config) {
2123
+ super({ id: config.id, name: "ClickhouseStore" });
2124
+ this.db = createClient({
2125
+ url: config.url,
2126
+ username: config.username,
2127
+ password: config.password,
2128
+ clickhouse_settings: {
2129
+ date_time_input_format: "best_effort",
2130
+ date_time_output_format: "iso",
2131
+ // This is crucial
2132
+ use_client_time_zone: 1,
2133
+ output_format_json_quote_64bit_integers: 0
2134
+ }
844
2135
  });
845
- const columns = await result.json();
846
- return columns.some((c) => c.name === column);
2136
+ this.ttl = config.ttl;
2137
+ const operations = new StoreOperationsClickhouse({ client: this.db, ttl: this.ttl });
2138
+ const workflows = new WorkflowsStorageClickhouse({ client: this.db, operations });
2139
+ const scores = new ScoresStorageClickhouse({ client: this.db, operations });
2140
+ const memory = new MemoryStorageClickhouse({ client: this.db, operations });
2141
+ this.stores = {
2142
+ operations,
2143
+ workflows,
2144
+ scores,
2145
+ memory
2146
+ };
2147
+ }
2148
+ get supports() {
2149
+ return {
2150
+ selectByIncludeResourceScope: true,
2151
+ resourceWorkingMemory: true,
2152
+ hasColumn: true,
2153
+ createTable: true,
2154
+ deleteMessages: false,
2155
+ listScoresBySpan: true
2156
+ };
2157
+ }
2158
+ async batchInsert({ tableName, records }) {
2159
+ await this.stores.operations.batchInsert({ tableName, records });
2160
+ }
2161
+ async optimizeTable({ tableName }) {
2162
+ try {
2163
+ await this.db.command({
2164
+ query: `OPTIMIZE TABLE ${tableName} FINAL`
2165
+ });
2166
+ } catch (error) {
2167
+ throw new MastraError(
2168
+ {
2169
+ id: "CLICKHOUSE_STORAGE_OPTIMIZE_TABLE_FAILED",
2170
+ domain: ErrorDomain.STORAGE,
2171
+ category: ErrorCategory.THIRD_PARTY,
2172
+ details: { tableName }
2173
+ },
2174
+ error
2175
+ );
2176
+ }
2177
+ }
2178
+ async materializeTtl({ tableName }) {
2179
+ try {
2180
+ await this.db.command({
2181
+ query: `ALTER TABLE ${tableName} MATERIALIZE TTL;`
2182
+ });
2183
+ } catch (error) {
2184
+ throw new MastraError(
2185
+ {
2186
+ id: "CLICKHOUSE_STORAGE_MATERIALIZE_TTL_FAILED",
2187
+ domain: ErrorDomain.STORAGE,
2188
+ category: ErrorCategory.THIRD_PARTY,
2189
+ details: { tableName }
2190
+ },
2191
+ error
2192
+ );
2193
+ }
2194
+ }
2195
+ async createTable({
2196
+ tableName,
2197
+ schema
2198
+ }) {
2199
+ return this.stores.operations.createTable({ tableName, schema });
2200
+ }
2201
+ async dropTable({ tableName }) {
2202
+ return this.stores.operations.dropTable({ tableName });
2203
+ }
2204
+ async alterTable({
2205
+ tableName,
2206
+ schema,
2207
+ ifNotExists
2208
+ }) {
2209
+ return this.stores.operations.alterTable({ tableName, schema, ifNotExists });
2210
+ }
2211
+ async clearTable({ tableName }) {
2212
+ return this.stores.operations.clearTable({ tableName });
2213
+ }
2214
+ async insert({ tableName, record }) {
2215
+ return this.stores.operations.insert({ tableName, record });
2216
+ }
2217
+ async load({ tableName, keys }) {
2218
+ return this.stores.operations.load({ tableName, keys });
2219
+ }
2220
+ async updateWorkflowResults({
2221
+ workflowName,
2222
+ runId,
2223
+ stepId,
2224
+ result,
2225
+ requestContext
2226
+ }) {
2227
+ return this.stores.workflows.updateWorkflowResults({ workflowName, runId, stepId, result, requestContext });
2228
+ }
2229
+ async updateWorkflowState({
2230
+ workflowName,
2231
+ runId,
2232
+ opts
2233
+ }) {
2234
+ return this.stores.workflows.updateWorkflowState({ workflowName, runId, opts });
2235
+ }
2236
+ async persistWorkflowSnapshot({
2237
+ workflowName,
2238
+ runId,
2239
+ resourceId,
2240
+ snapshot
2241
+ }) {
2242
+ return this.stores.workflows.persistWorkflowSnapshot({ workflowName, runId, resourceId, snapshot });
2243
+ }
2244
+ async loadWorkflowSnapshot({
2245
+ workflowName,
2246
+ runId
2247
+ }) {
2248
+ return this.stores.workflows.loadWorkflowSnapshot({ workflowName, runId });
2249
+ }
2250
+ async listWorkflowRuns(args = {}) {
2251
+ return this.stores.workflows.listWorkflowRuns(args);
2252
+ }
2253
+ async getWorkflowRunById({
2254
+ runId,
2255
+ workflowName
2256
+ }) {
2257
+ return this.stores.workflows.getWorkflowRunById({ runId, workflowName });
2258
+ }
2259
+ async getThreadById({ threadId }) {
2260
+ return this.stores.memory.getThreadById({ threadId });
2261
+ }
2262
+ async saveThread({ thread }) {
2263
+ return this.stores.memory.saveThread({ thread });
2264
+ }
2265
+ async updateThread({
2266
+ id,
2267
+ title,
2268
+ metadata
2269
+ }) {
2270
+ return this.stores.memory.updateThread({ id, title, metadata });
2271
+ }
2272
+ async deleteThread({ threadId }) {
2273
+ return this.stores.memory.deleteThread({ threadId });
2274
+ }
2275
+ async saveMessages(args) {
2276
+ return this.stores.memory.saveMessages(args);
2277
+ }
2278
+ async updateMessages(args) {
2279
+ return this.stores.memory.updateMessages(args);
2280
+ }
2281
+ async getResourceById({ resourceId }) {
2282
+ return this.stores.memory.getResourceById({ resourceId });
2283
+ }
2284
+ async saveResource({ resource }) {
2285
+ return this.stores.memory.saveResource({ resource });
2286
+ }
2287
+ async updateResource({
2288
+ resourceId,
2289
+ workingMemory,
2290
+ metadata
2291
+ }) {
2292
+ return this.stores.memory.updateResource({ resourceId, workingMemory, metadata });
2293
+ }
2294
+ async getScoreById({ id }) {
2295
+ return this.stores.scores.getScoreById({ id });
2296
+ }
2297
+ async saveScore(_score) {
2298
+ return this.stores.scores.saveScore(_score);
2299
+ }
2300
+ async listScoresByRunId({
2301
+ runId,
2302
+ pagination
2303
+ }) {
2304
+ return this.stores.scores.listScoresByRunId({ runId, pagination });
2305
+ }
2306
+ async listScoresByEntityId({
2307
+ entityId,
2308
+ entityType,
2309
+ pagination
2310
+ }) {
2311
+ return this.stores.scores.listScoresByEntityId({ entityId, entityType, pagination });
2312
+ }
2313
+ async listScoresByScorerId({
2314
+ scorerId,
2315
+ pagination,
2316
+ entityId,
2317
+ entityType,
2318
+ source
2319
+ }) {
2320
+ return this.stores.scores.listScoresByScorerId({ scorerId, pagination, entityId, entityType, source });
2321
+ }
2322
+ async listScoresBySpan({
2323
+ traceId,
2324
+ spanId,
2325
+ pagination
2326
+ }) {
2327
+ return this.stores.scores.listScoresBySpan({ traceId, spanId, pagination });
847
2328
  }
848
2329
  async close() {
849
2330
  await this.db.close();
@@ -851,3 +2332,5 @@ var ClickhouseStore = class extends MastraStorage {
851
2332
  };
852
2333
 
853
2334
  export { COLUMN_TYPES, ClickhouseStore, TABLE_ENGINES };
2335
+ //# sourceMappingURL=index.js.map
2336
+ //# sourceMappingURL=index.js.map