@mastra/clickhouse 0.0.0-vnextWorkflows-20250422142014 → 0.0.0-workflow-deno-20250616115451

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,8 @@
1
1
  import type { ClickHouseClient } from '@clickhouse/client';
2
2
  import { createClient } from '@clickhouse/client';
3
+ import { MessageList } from '@mastra/core/agent';
3
4
  import type { MetricResult, TestInfo } from '@mastra/core/eval';
4
- import type { MessageType, StorageThreadType } from '@mastra/core/memory';
5
+ import type { MastraMessageV1, MastraMessageV2, StorageThreadType } from '@mastra/core/memory';
5
6
  import {
6
7
  MastraStorage,
7
8
  TABLE_EVALS,
@@ -11,7 +12,17 @@ import {
11
12
  TABLE_TRACES,
12
13
  TABLE_WORKFLOW_SNAPSHOT,
13
14
  } from '@mastra/core/storage';
14
- import type { EvalRow, StorageColumn, StorageGetMessagesArg, TABLE_NAMES } from '@mastra/core/storage';
15
+ import type {
16
+ EvalRow,
17
+ PaginationInfo,
18
+ StorageColumn,
19
+ StorageGetMessagesArg,
20
+ TABLE_NAMES,
21
+ WorkflowRun,
22
+ WorkflowRuns,
23
+ StorageGetTracesArg,
24
+ } from '@mastra/core/storage';
25
+ import type { Trace } from '@mastra/core/telemetry';
15
26
  import type { WorkflowRunState } from '@mastra/core/workflows';
16
27
 
17
28
  function safelyParseJSON(jsonString: string): any {
@@ -203,6 +214,8 @@ export class ClickhouseStore extends MastraStorage {
203
214
  perPage,
204
215
  attributes,
205
216
  filters,
217
+ fromDate,
218
+ toDate,
206
219
  }: {
207
220
  name?: string;
208
221
  scope?: string;
@@ -210,6 +223,8 @@ export class ClickhouseStore extends MastraStorage {
210
223
  perPage: number;
211
224
  attributes?: Record<string, string>;
212
225
  filters?: Record<string, any>;
226
+ fromDate?: Date;
227
+ toDate?: Date;
213
228
  }): Promise<any[]> {
214
229
  const limit = perPage;
215
230
  const offset = page * perPage;
@@ -241,6 +256,16 @@ export class ClickhouseStore extends MastraStorage {
241
256
  });
242
257
  }
243
258
 
259
+ if (fromDate) {
260
+ conditions.push(`createdAt >= {var_from_date:DateTime64(3)}`);
261
+ args.var_from_date = fromDate.getTime() / 1000; // Convert to Unix timestamp
262
+ }
263
+
264
+ if (toDate) {
265
+ conditions.push(`createdAt <= {var_to_date:DateTime64(3)}`);
266
+ args.var_to_date = toDate.getTime() / 1000; // Convert to Unix timestamp
267
+ }
268
+
244
269
  const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
245
270
 
246
271
  const result = await this.db.query({
@@ -316,7 +341,6 @@ export class ClickhouseStore extends MastraStorage {
316
341
  ${['id String'].concat(columns)}
317
342
  )
318
343
  ENGINE = ${TABLE_ENGINES[tableName]}
319
- PARTITION BY "createdAt"
320
344
  PRIMARY KEY (createdAt, run_id, workflow_name)
321
345
  ORDER BY (createdAt, run_id, workflow_name)
322
346
  ${rowTtl ? `TTL toDateTime(${rowTtl.ttlKey ?? 'createdAt'}) + INTERVAL ${rowTtl.interval} ${rowTtl.unit}` : ''}
@@ -327,7 +351,6 @@ export class ClickhouseStore extends MastraStorage {
327
351
  ${columns}
328
352
  )
329
353
  ENGINE = ${TABLE_ENGINES[tableName]}
330
- PARTITION BY "createdAt"
331
354
  PRIMARY KEY (createdAt, ${tableName === TABLE_EVALS ? 'run_id' : 'id'})
332
355
  ORDER BY (createdAt, ${tableName === TABLE_EVALS ? 'run_id' : 'id'})
333
356
  ${this.ttl?.[tableName]?.row ? `TTL toDateTime(createdAt) + INTERVAL ${this.ttl[tableName].row.interval} ${this.ttl[tableName].row.unit}` : ''}
@@ -350,6 +373,73 @@ export class ClickhouseStore extends MastraStorage {
350
373
  }
351
374
  }
352
375
 
376
+ protected getSqlType(type: StorageColumn['type']): string {
377
+ switch (type) {
378
+ case 'text':
379
+ return 'String';
380
+ case 'timestamp':
381
+ return 'DateTime64(3)';
382
+ case 'integer':
383
+ case 'bigint':
384
+ return 'Int64';
385
+ case 'jsonb':
386
+ return 'String';
387
+ default:
388
+ return super.getSqlType(type); // fallback to base implementation
389
+ }
390
+ }
391
+
392
+ /**
393
+ * Alters table schema to add columns if they don't exist
394
+ * @param tableName Name of the table
395
+ * @param schema Schema of the table
396
+ * @param ifNotExists Array of column names to add if they don't exist
397
+ */
398
+ async alterTable({
399
+ tableName,
400
+ schema,
401
+ ifNotExists,
402
+ }: {
403
+ tableName: TABLE_NAMES;
404
+ schema: Record<string, StorageColumn>;
405
+ ifNotExists: string[];
406
+ }): Promise<void> {
407
+ try {
408
+ // 1. Get existing columns
409
+ const describeSql = `DESCRIBE TABLE ${tableName}`;
410
+ const result = await this.db.query({
411
+ query: describeSql,
412
+ });
413
+ const rows = await result.json();
414
+ const existingColumnNames = new Set(rows.data.map((row: any) => row.name.toLowerCase()));
415
+
416
+ // 2. Add missing columns
417
+ for (const columnName of ifNotExists) {
418
+ if (!existingColumnNames.has(columnName.toLowerCase()) && schema[columnName]) {
419
+ const columnDef = schema[columnName];
420
+ let sqlType = this.getSqlType(columnDef.type);
421
+ if (columnDef.nullable !== false) {
422
+ sqlType = `Nullable(${sqlType})`;
423
+ }
424
+ const defaultValue = columnDef.nullable === false ? this.getDefaultValue(columnDef.type) : '';
425
+ // Use backticks or double quotes as needed for identifiers
426
+ const alterSql =
427
+ `ALTER TABLE ${tableName} ADD COLUMN IF NOT EXISTS "${columnName}" ${sqlType} ${defaultValue}`.trim();
428
+
429
+ await this.db.query({
430
+ query: alterSql,
431
+ });
432
+ this.logger?.debug?.(`Added column ${columnName} to table ${tableName}`);
433
+ }
434
+ }
435
+ } catch (error) {
436
+ this.logger?.error?.(
437
+ `Error altering table ${tableName}: ${error instanceof Error ? error.message : String(error)}`,
438
+ );
439
+ throw new Error(`Failed to alter table ${tableName}: ${error}`);
440
+ }
441
+ }
442
+
353
443
  async clearTable({ tableName }: { tableName: TABLE_NAMES }): Promise<void> {
354
444
  try {
355
445
  await this.db.query({
@@ -580,15 +670,18 @@ export class ClickhouseStore extends MastraStorage {
580
670
 
581
671
  await this.db.insert({
582
672
  table: TABLE_THREADS,
673
+ format: 'JSONEachRow',
583
674
  values: [
584
675
  {
585
- ...updatedThread,
676
+ id: updatedThread.id,
677
+ resourceId: updatedThread.resourceId,
678
+ title: updatedThread.title,
679
+ metadata: updatedThread.metadata,
680
+ createdAt: updatedThread.createdAt,
586
681
  updatedAt: updatedThread.updatedAt.toISOString(),
587
682
  },
588
683
  ],
589
- format: 'JSONEachRow',
590
684
  clickhouse_settings: {
591
- // Allows to insert serialized JS Dates (such as '2023-12-06T10:54:48.000Z')
592
685
  date_time_input_format: 'best_effort',
593
686
  use_client_time_zone: 1,
594
687
  output_format_json_quote_64bit_integers: 0,
@@ -606,7 +699,7 @@ export class ClickhouseStore extends MastraStorage {
606
699
  try {
607
700
  // First delete all messages associated with this thread
608
701
  await this.db.command({
609
- query: `DELETE FROM "${TABLE_MESSAGES}" WHERE thread_id = '${threadId}';`,
702
+ query: `DELETE FROM "${TABLE_MESSAGES}" WHERE thread_id = {var_thread_id:String};`,
610
703
  query_params: { var_thread_id: threadId },
611
704
  clickhouse_settings: {
612
705
  output_format_json_quote_64bit_integers: 0,
@@ -627,7 +720,14 @@ export class ClickhouseStore extends MastraStorage {
627
720
  }
628
721
  }
629
722
 
630
- async getMessages<T = unknown>({ threadId, selectBy }: StorageGetMessagesArg): Promise<T> {
723
+ public async getMessages(args: StorageGetMessagesArg & { format?: 'v1' }): Promise<MastraMessageV1[]>;
724
+ public async getMessages(args: StorageGetMessagesArg & { format: 'v2' }): Promise<MastraMessageV2[]>;
725
+ public async getMessages({
726
+ threadId,
727
+ resourceId,
728
+ selectBy,
729
+ format,
730
+ }: StorageGetMessagesArg & { format?: 'v1' | 'v2' }): Promise<MastraMessageV1[] | MastraMessageV2[]> {
631
731
  try {
632
732
  const messages: any[] = [];
633
733
  const limit = typeof selectBy?.last === `number` ? selectBy.last : 40;
@@ -734,18 +834,26 @@ export class ClickhouseStore extends MastraStorage {
734
834
  }
735
835
  });
736
836
 
737
- return messages as T;
837
+ const list = new MessageList({ threadId, resourceId }).add(messages, 'memory');
838
+ if (format === `v2`) return list.get.all.v2();
839
+ return list.get.all.v1();
738
840
  } catch (error) {
739
841
  console.error('Error getting messages:', error);
740
842
  throw error;
741
843
  }
742
844
  }
743
845
 
744
- async saveMessages({ messages }: { messages: MessageType[] }): Promise<MessageType[]> {
846
+ async saveMessages(args: { messages: MastraMessageV1[]; format?: undefined | 'v1' }): Promise<MastraMessageV1[]>;
847
+ async saveMessages(args: { messages: MastraMessageV2[]; format: 'v2' }): Promise<MastraMessageV2[]>;
848
+ async saveMessages(
849
+ args: { messages: MastraMessageV1[]; format?: undefined | 'v1' } | { messages: MastraMessageV2[]; format: 'v2' },
850
+ ): Promise<MastraMessageV2[] | MastraMessageV1[]> {
851
+ const { messages, format = 'v1' } = args;
745
852
  if (messages.length === 0) return messages;
746
853
 
747
854
  try {
748
855
  const threadId = messages[0]?.threadId;
856
+ const resourceId = messages[0]?.resourceId;
749
857
  if (!threadId) {
750
858
  throw new Error('Thread ID is required');
751
859
  }
@@ -756,26 +864,52 @@ export class ClickhouseStore extends MastraStorage {
756
864
  throw new Error(`Thread ${threadId} not found`);
757
865
  }
758
866
 
759
- await this.db.insert({
760
- table: TABLE_MESSAGES,
761
- format: 'JSONEachRow',
762
- values: messages.map(message => ({
763
- id: message.id,
764
- thread_id: threadId,
765
- content: typeof message.content === 'string' ? message.content : JSON.stringify(message.content),
766
- createdAt: message.createdAt.toISOString(),
767
- role: message.role,
768
- type: message.type,
769
- })),
770
- clickhouse_settings: {
771
- // Allows to insert serialized JS Dates (such as '2023-12-06T10:54:48.000Z')
772
- date_time_input_format: 'best_effort',
773
- use_client_time_zone: 1,
774
- output_format_json_quote_64bit_integers: 0,
775
- },
776
- });
867
+ // Execute message inserts and thread update in parallel for better performance
868
+ await Promise.all([
869
+ // Insert messages
870
+ this.db.insert({
871
+ table: TABLE_MESSAGES,
872
+ format: 'JSONEachRow',
873
+ values: messages.map(message => ({
874
+ id: message.id,
875
+ thread_id: threadId,
876
+ content: typeof message.content === 'string' ? message.content : JSON.stringify(message.content),
877
+ createdAt: message.createdAt.toISOString(),
878
+ role: message.role,
879
+ type: message.type || 'v2',
880
+ })),
881
+ clickhouse_settings: {
882
+ // Allows to insert serialized JS Dates (such as '2023-12-06T10:54:48.000Z')
883
+ date_time_input_format: 'best_effort',
884
+ use_client_time_zone: 1,
885
+ output_format_json_quote_64bit_integers: 0,
886
+ },
887
+ }),
888
+ // Update thread's updatedAt timestamp
889
+ this.db.insert({
890
+ table: TABLE_THREADS,
891
+ format: 'JSONEachRow',
892
+ values: [
893
+ {
894
+ id: thread.id,
895
+ resourceId: thread.resourceId,
896
+ title: thread.title,
897
+ metadata: thread.metadata,
898
+ createdAt: thread.createdAt,
899
+ updatedAt: new Date().toISOString(),
900
+ },
901
+ ],
902
+ clickhouse_settings: {
903
+ date_time_input_format: 'best_effort',
904
+ use_client_time_zone: 1,
905
+ output_format_json_quote_64bit_integers: 0,
906
+ },
907
+ }),
908
+ ]);
777
909
 
778
- return messages;
910
+ const list = new MessageList({ threadId, resourceId }).add(messages, 'memory');
911
+ if (format === `v2`) return list.get.all.v2();
912
+ return list.get.all.v1();
779
913
  } catch (error) {
780
914
  console.error('Error saving messages:', error);
781
915
  throw error;
@@ -856,28 +990,42 @@ export class ClickhouseStore extends MastraStorage {
856
990
  }
857
991
  }
858
992
 
993
+ private parseWorkflowRun(row: any): WorkflowRun {
994
+ let parsedSnapshot: WorkflowRunState | string = row.snapshot as string;
995
+ if (typeof parsedSnapshot === 'string') {
996
+ try {
997
+ parsedSnapshot = JSON.parse(row.snapshot as string) as WorkflowRunState;
998
+ } catch (e) {
999
+ // If parsing fails, return the raw snapshot string
1000
+ console.warn(`Failed to parse snapshot for workflow ${row.workflow_name}: ${e}`);
1001
+ }
1002
+ }
1003
+
1004
+ return {
1005
+ workflowName: row.workflow_name,
1006
+ runId: row.run_id,
1007
+ snapshot: parsedSnapshot,
1008
+ createdAt: new Date(row.createdAt),
1009
+ updatedAt: new Date(row.updatedAt),
1010
+ resourceId: row.resourceId,
1011
+ };
1012
+ }
1013
+
859
1014
  async getWorkflowRuns({
860
1015
  workflowName,
861
1016
  fromDate,
862
1017
  toDate,
863
1018
  limit,
864
1019
  offset,
1020
+ resourceId,
865
1021
  }: {
866
1022
  workflowName?: string;
867
1023
  fromDate?: Date;
868
1024
  toDate?: Date;
869
1025
  limit?: number;
870
1026
  offset?: number;
871
- } = {}): Promise<{
872
- runs: Array<{
873
- workflowName: string;
874
- runId: string;
875
- snapshot: WorkflowRunState | string;
876
- createdAt: Date;
877
- updatedAt: Date;
878
- }>;
879
- total: number;
880
- }> {
1027
+ resourceId?: string;
1028
+ } = {}): Promise<WorkflowRuns> {
881
1029
  try {
882
1030
  const conditions: string[] = [];
883
1031
  const values: Record<string, any> = {};
@@ -887,6 +1035,16 @@ export class ClickhouseStore extends MastraStorage {
887
1035
  values.var_workflow_name = workflowName;
888
1036
  }
889
1037
 
1038
+ if (resourceId) {
1039
+ const hasResourceId = await this.hasColumn(TABLE_WORKFLOW_SNAPSHOT, 'resourceId');
1040
+ if (hasResourceId) {
1041
+ conditions.push(`resourceId = {var_resourceId:String}`);
1042
+ values.var_resourceId = resourceId;
1043
+ } else {
1044
+ console.warn(`[${TABLE_WORKFLOW_SNAPSHOT}] resourceId column not found. Skipping resourceId filter.`);
1045
+ }
1046
+ }
1047
+
890
1048
  if (fromDate) {
891
1049
  conditions.push(`createdAt >= {var_from_date:DateTime64(3)}`);
892
1050
  values.var_from_date = fromDate.getTime() / 1000; // Convert to Unix timestamp
@@ -921,7 +1079,8 @@ export class ClickhouseStore extends MastraStorage {
921
1079
  run_id,
922
1080
  snapshot,
923
1081
  toDateTime64(createdAt, 3) as createdAt,
924
- toDateTime64(updatedAt, 3) as updatedAt
1082
+ toDateTime64(updatedAt, 3) as updatedAt,
1083
+ resourceId
925
1084
  FROM ${TABLE_WORKFLOW_SNAPSHOT} ${TABLE_ENGINES[TABLE_WORKFLOW_SNAPSHOT].startsWith('ReplacingMergeTree') ? 'FINAL' : ''}
926
1085
  ${whereClause}
927
1086
  ORDER BY createdAt DESC
@@ -935,23 +1094,7 @@ export class ClickhouseStore extends MastraStorage {
935
1094
  const resultJson = await result.json();
936
1095
  const rows = resultJson as any[];
937
1096
  const runs = rows.map(row => {
938
- let parsedSnapshot: WorkflowRunState | string = row.snapshot;
939
- if (typeof parsedSnapshot === 'string') {
940
- try {
941
- parsedSnapshot = JSON.parse(row.snapshot) as WorkflowRunState;
942
- } catch (e) {
943
- // If parsing fails, return the raw snapshot string
944
- console.warn(`Failed to parse snapshot for workflow ${row.workflow_name}: ${e}`);
945
- }
946
- }
947
-
948
- return {
949
- workflowName: row.workflow_name,
950
- runId: row.run_id,
951
- snapshot: parsedSnapshot,
952
- createdAt: new Date(row.createdAt),
953
- updatedAt: new Date(row.updatedAt),
954
- };
1097
+ return this.parseWorkflowRun(row);
955
1098
  });
956
1099
 
957
1100
  // Use runs.length as total when not paginating
@@ -962,6 +1105,84 @@ export class ClickhouseStore extends MastraStorage {
962
1105
  }
963
1106
  }
964
1107
 
1108
+ async getWorkflowRunById({
1109
+ runId,
1110
+ workflowName,
1111
+ }: {
1112
+ runId: string;
1113
+ workflowName?: string;
1114
+ }): Promise<WorkflowRun | null> {
1115
+ try {
1116
+ const conditions: string[] = [];
1117
+ const values: Record<string, any> = {};
1118
+
1119
+ if (runId) {
1120
+ conditions.push(`run_id = {var_runId:String}`);
1121
+ values.var_runId = runId;
1122
+ }
1123
+
1124
+ if (workflowName) {
1125
+ conditions.push(`workflow_name = {var_workflow_name:String}`);
1126
+ values.var_workflow_name = workflowName;
1127
+ }
1128
+
1129
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
1130
+
1131
+ // Get results
1132
+ const result = await this.db.query({
1133
+ query: `
1134
+ SELECT
1135
+ workflow_name,
1136
+ run_id,
1137
+ snapshot,
1138
+ toDateTime64(createdAt, 3) as createdAt,
1139
+ toDateTime64(updatedAt, 3) as updatedAt,
1140
+ resourceId
1141
+ FROM ${TABLE_WORKFLOW_SNAPSHOT} ${TABLE_ENGINES[TABLE_WORKFLOW_SNAPSHOT].startsWith('ReplacingMergeTree') ? 'FINAL' : ''}
1142
+ ${whereClause}
1143
+ `,
1144
+ query_params: values,
1145
+ format: 'JSONEachRow',
1146
+ });
1147
+
1148
+ const resultJson = await result.json();
1149
+ if (!Array.isArray(resultJson) || resultJson.length === 0) {
1150
+ return null;
1151
+ }
1152
+ return this.parseWorkflowRun(resultJson[0]);
1153
+ } catch (error) {
1154
+ console.error('Error getting workflow run by ID:', error);
1155
+ throw error;
1156
+ }
1157
+ }
1158
+
1159
+ private async hasColumn(table: string, column: string): Promise<boolean> {
1160
+ const result = await this.db.query({
1161
+ query: `DESCRIBE TABLE ${table}`,
1162
+ format: 'JSONEachRow',
1163
+ });
1164
+ const columns = (await result.json()) as { name: string }[];
1165
+ return columns.some(c => c.name === column);
1166
+ }
1167
+
1168
+ async getTracesPaginated(_args: StorageGetTracesArg): Promise<PaginationInfo & { traces: Trace[] }> {
1169
+ throw new Error('Method not implemented.');
1170
+ }
1171
+
1172
+ async getThreadsByResourceIdPaginated(_args: {
1173
+ resourceId: string;
1174
+ page?: number;
1175
+ perPage?: number;
1176
+ }): Promise<PaginationInfo & { threads: StorageThreadType[] }> {
1177
+ throw new Error('Method not implemented.');
1178
+ }
1179
+
1180
+ async getMessagesPaginated(
1181
+ _args: StorageGetMessagesArg,
1182
+ ): Promise<PaginationInfo & { messages: MastraMessageV1[] | MastraMessageV2[] }> {
1183
+ throw new Error('Method not implemented.');
1184
+ }
1185
+
965
1186
  async close(): Promise<void> {
966
1187
  await this.db.close();
967
1188
  }