@mastra/clickhouse 0.2.7-alpha.1 → 0.2.7-alpha.3

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,3 +1,6 @@
1
+ import type { ClickHouseClient } from '@clickhouse/client';
2
+ import { createClient } from '@clickhouse/client';
3
+ import type { MetricResult, TestInfo } from '@mastra/core/eval';
1
4
  import type { MessageType, StorageThreadType } from '@mastra/core/memory';
2
5
  import {
3
6
  MastraStorage,
@@ -10,7 +13,6 @@ import {
10
13
  } from '@mastra/core/storage';
11
14
  import type { EvalRow, StorageColumn, StorageGetMessagesArg, TABLE_NAMES } from '@mastra/core/storage';
12
15
  import type { WorkflowRunState } from '@mastra/core/workflows';
13
- import { createClient, ClickHouseClient } from '@clickhouse/client';
14
16
 
15
17
  function safelyParseJSON(jsonString: string): any {
16
18
  try {
@@ -20,10 +22,35 @@ function safelyParseJSON(jsonString: string): any {
20
22
  }
21
23
  }
22
24
 
25
+ type IntervalUnit =
26
+ | 'NANOSECOND'
27
+ | 'MICROSECOND'
28
+ | 'MILLISECOND'
29
+ | 'SECOND'
30
+ | 'MINUTE'
31
+ | 'HOUR'
32
+ | 'DAY'
33
+ | 'WEEK'
34
+ | 'MONTH'
35
+ | 'QUARTER'
36
+ | 'YEAR';
37
+
23
38
  export type ClickhouseConfig = {
24
39
  url: string;
25
40
  username: string;
26
41
  password: string;
42
+ ttl?: {
43
+ [TableKey in TABLE_NAMES]?: {
44
+ row?: { interval: number; unit: IntervalUnit; ttlKey?: string };
45
+ columns?: Partial<{
46
+ [ColumnKey in keyof (typeof TABLE_SCHEMAS)[TableKey]]: {
47
+ interval: number;
48
+ unit: IntervalUnit;
49
+ ttlKey?: string;
50
+ };
51
+ }>;
52
+ };
53
+ };
27
54
  };
28
55
 
29
56
  const TABLE_ENGINES: Record<TABLE_NAMES, string> = {
@@ -63,6 +90,7 @@ function transformRow<R>(row: any): R {
63
90
 
64
91
  export class ClickhouseStore extends MastraStorage {
65
92
  private db: ClickHouseClient;
93
+ private ttl: ClickhouseConfig['ttl'] = {};
66
94
 
67
95
  constructor(config: ClickhouseConfig) {
68
96
  super({ name: 'ClickhouseStore' });
@@ -77,10 +105,67 @@ export class ClickhouseStore extends MastraStorage {
77
105
  output_format_json_quote_64bit_integers: 0,
78
106
  },
79
107
  });
108
+ this.ttl = config.ttl;
80
109
  }
81
110
 
82
- getEvalsByAgentName(_agentName: string, _type?: 'test' | 'live'): Promise<EvalRow[]> {
83
- throw new Error('Method not implemented.');
111
+ private transformEvalRow(row: Record<string, any>): EvalRow {
112
+ row = transformRow(row);
113
+ const resultValue = JSON.parse(row.result as string);
114
+ const testInfoValue = row.test_info ? JSON.parse(row.test_info as string) : undefined;
115
+
116
+ if (!resultValue || typeof resultValue !== 'object' || !('score' in resultValue)) {
117
+ throw new Error(`Invalid MetricResult format: ${JSON.stringify(resultValue)}`);
118
+ }
119
+
120
+ return {
121
+ input: row.input as string,
122
+ output: row.output as string,
123
+ result: resultValue as MetricResult,
124
+ agentName: row.agent_name as string,
125
+ metricName: row.metric_name as string,
126
+ instructions: row.instructions as string,
127
+ testInfo: testInfoValue as TestInfo,
128
+ globalRunId: row.global_run_id as string,
129
+ runId: row.run_id as string,
130
+ createdAt: row.created_at as string,
131
+ };
132
+ }
133
+
134
+ async getEvalsByAgentName(agentName: string, type?: 'test' | 'live'): Promise<EvalRow[]> {
135
+ try {
136
+ const baseQuery = `SELECT *, toDateTime64(createdAt, 3) as createdAt FROM ${TABLE_EVALS} WHERE agent_name = {var_agent_name:String}`;
137
+ const typeCondition =
138
+ type === 'test'
139
+ ? " AND test_info IS NOT NULL AND JSONExtractString(test_info, 'testPath') IS NOT NULL"
140
+ : type === 'live'
141
+ ? " AND (test_info IS NULL OR JSONExtractString(test_info, 'testPath') IS NULL)"
142
+ : '';
143
+
144
+ const result = await this.db.query({
145
+ query: `${baseQuery}${typeCondition} ORDER BY createdAt DESC`,
146
+ query_params: { var_agent_name: agentName },
147
+ clickhouse_settings: {
148
+ date_time_input_format: 'best_effort',
149
+ date_time_output_format: 'iso',
150
+ use_client_time_zone: 1,
151
+ output_format_json_quote_64bit_integers: 0,
152
+ },
153
+ });
154
+
155
+ if (!result) {
156
+ return [];
157
+ }
158
+
159
+ const rows = await result.json();
160
+ return rows.data.map((row: any) => this.transformEvalRow(row));
161
+ } catch (error) {
162
+ // Handle case where table doesn't exist yet
163
+ if (error instanceof Error && error.message.includes('no such table')) {
164
+ return [];
165
+ }
166
+ this.logger.error('Failed to get evals for the specified agent: ' + (error as any)?.message);
167
+ throw error;
168
+ }
84
169
  }
85
170
 
86
171
  async batchInsert({ tableName, records }: { tableName: TABLE_NAMES; records: Record<string, any>[] }): Promise<void> {
@@ -117,12 +202,14 @@ export class ClickhouseStore extends MastraStorage {
117
202
  page,
118
203
  perPage,
119
204
  attributes,
205
+ filters,
120
206
  }: {
121
207
  name?: string;
122
208
  scope?: string;
123
209
  page: number;
124
210
  perPage: number;
125
211
  attributes?: Record<string, string>;
212
+ filters?: Record<string, any>;
126
213
  }): Promise<any[]> {
127
214
  let idx = 1;
128
215
  const limit = perPage;
@@ -141,8 +228,17 @@ export class ClickhouseStore extends MastraStorage {
141
228
  }
142
229
  if (attributes) {
143
230
  Object.entries(attributes).forEach(([key, value]) => {
144
- conditions.push(`JSONExtractString(attributes, '${key}') = {var_${key}:String}`);
145
- args[`var_${key}`] = value;
231
+ conditions.push(`JSONExtractString(attributes, '${key}') = {var_attr_${key}:String}`);
232
+ args[`var_attr_${key}`] = value;
233
+ });
234
+ }
235
+
236
+ if (filters) {
237
+ Object.entries(filters).forEach(([key, value]) => {
238
+ conditions.push(
239
+ `${key} = {var_col_${key}:${COLUMN_TYPES[TABLE_SCHEMAS.mastra_traces?.[key]?.type ?? 'text']}}`,
240
+ );
241
+ args[`var_col_${key}`] = value;
146
242
  });
147
243
  }
148
244
 
@@ -184,6 +280,18 @@ export class ClickhouseStore extends MastraStorage {
184
280
  }));
185
281
  }
186
282
 
283
+ async optimizeTable({ tableName }: { tableName: TABLE_NAMES }): Promise<void> {
284
+ await this.db.command({
285
+ query: `OPTIMIZE TABLE ${tableName} FINAL`,
286
+ });
287
+ }
288
+
289
+ async materializeTtl({ tableName }: { tableName: TABLE_NAMES }): Promise<void> {
290
+ await this.db.command({
291
+ query: `ALTER TABLE ${tableName} MATERIALIZE TTL;`,
292
+ });
293
+ }
294
+
187
295
  async createTable({
188
296
  tableName,
189
297
  schema,
@@ -196,10 +304,12 @@ export class ClickhouseStore extends MastraStorage {
196
304
  .map(([name, def]) => {
197
305
  const constraints = [];
198
306
  if (!def.nullable) constraints.push('NOT NULL');
199
- return `"${name}" ${COLUMN_TYPES[def.type]} ${constraints.join(' ')}`;
307
+ const columnTtl = this.ttl?.[tableName]?.columns?.[name];
308
+ return `"${name}" ${COLUMN_TYPES[def.type]} ${constraints.join(' ')} ${columnTtl ? `TTL toDateTime(${columnTtl.ttlKey ?? 'createdAt'}) + INTERVAL ${columnTtl.interval} ${columnTtl.unit}` : ''}`;
200
309
  })
201
310
  .join(',\n');
202
311
 
312
+ const rowTtl = this.ttl?.[tableName]?.row;
203
313
  const sql =
204
314
  tableName === TABLE_WORKFLOW_SNAPSHOT
205
315
  ? `
@@ -210,7 +320,8 @@ export class ClickhouseStore extends MastraStorage {
210
320
  PARTITION BY "createdAt"
211
321
  PRIMARY KEY (createdAt, run_id, workflow_name)
212
322
  ORDER BY (createdAt, run_id, workflow_name)
213
- SETTINGS index_granularity = 8192;
323
+ ${rowTtl ? `TTL toDateTime(${rowTtl.ttlKey ?? 'createdAt'}) + INTERVAL ${rowTtl.interval} ${rowTtl.unit}` : ''}
324
+ SETTINGS index_granularity = 8192
214
325
  `
215
326
  : `
216
327
  CREATE TABLE IF NOT EXISTS ${tableName} (
@@ -218,9 +329,10 @@ export class ClickhouseStore extends MastraStorage {
218
329
  )
219
330
  ENGINE = ${TABLE_ENGINES[tableName]}
220
331
  PARTITION BY "createdAt"
221
- PRIMARY KEY (createdAt, id)
222
- ORDER BY (createdAt, id)
223
- SETTINGS index_granularity = 8192;
332
+ PRIMARY KEY (createdAt, ${tableName === TABLE_EVALS ? 'run_id' : 'id'})
333
+ ORDER BY (createdAt, ${tableName === TABLE_EVALS ? 'run_id' : 'id'})
334
+ ${this.ttl?.[tableName]?.row ? `TTL toDateTime(createdAt) + INTERVAL ${this.ttl[tableName].row.interval} ${this.ttl[tableName].row.unit}` : ''}
335
+ SETTINGS index_granularity = 8192
224
336
  `;
225
337
 
226
338
  await this.db.query({
@@ -745,6 +857,112 @@ export class ClickhouseStore extends MastraStorage {
745
857
  }
746
858
  }
747
859
 
860
+ async getWorkflowRuns({
861
+ workflowName,
862
+ fromDate,
863
+ toDate,
864
+ limit,
865
+ offset,
866
+ }: {
867
+ workflowName?: string;
868
+ fromDate?: Date;
869
+ toDate?: Date;
870
+ limit?: number;
871
+ offset?: number;
872
+ } = {}): Promise<{
873
+ runs: Array<{
874
+ workflowName: string;
875
+ runId: string;
876
+ snapshot: WorkflowRunState | string;
877
+ createdAt: Date;
878
+ updatedAt: Date;
879
+ }>;
880
+ total: number;
881
+ }> {
882
+ try {
883
+ const conditions: string[] = [];
884
+ const values: Record<string, any> = {};
885
+
886
+ if (workflowName) {
887
+ conditions.push(`workflow_name = {var_workflow_name:String}`);
888
+ values.var_workflow_name = workflowName;
889
+ }
890
+
891
+ if (fromDate) {
892
+ conditions.push(`createdAt >= {var_from_date:DateTime64(3)}`);
893
+ values.var_from_date = fromDate.getTime() / 1000; // Convert to Unix timestamp
894
+ }
895
+
896
+ if (toDate) {
897
+ conditions.push(`createdAt <= {var_to_date:DateTime64(3)}`);
898
+ values.var_to_date = toDate.getTime() / 1000; // Convert to Unix timestamp
899
+ }
900
+
901
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
902
+ const limitClause = limit !== undefined ? `LIMIT ${limit}` : '';
903
+ const offsetClause = offset !== undefined ? `OFFSET ${offset}` : '';
904
+
905
+ let total = 0;
906
+ // Only get total count when using pagination
907
+ if (limit !== undefined && offset !== undefined) {
908
+ const countResult = await this.db.query({
909
+ query: `SELECT COUNT(*) as count FROM ${TABLE_WORKFLOW_SNAPSHOT} ${TABLE_ENGINES[TABLE_WORKFLOW_SNAPSHOT].startsWith('ReplacingMergeTree') ? 'FINAL' : ''} ${whereClause}`,
910
+ query_params: values,
911
+ format: 'JSONEachRow',
912
+ });
913
+ const countRows = await countResult.json();
914
+ total = Number((countRows as Array<{ count: string | number }>)[0]?.count ?? 0);
915
+ }
916
+
917
+ // Get results
918
+ const result = await this.db.query({
919
+ query: `
920
+ SELECT
921
+ workflow_name,
922
+ run_id,
923
+ snapshot,
924
+ toDateTime64(createdAt, 3) as createdAt,
925
+ toDateTime64(updatedAt, 3) as updatedAt
926
+ FROM ${TABLE_WORKFLOW_SNAPSHOT} ${TABLE_ENGINES[TABLE_WORKFLOW_SNAPSHOT].startsWith('ReplacingMergeTree') ? 'FINAL' : ''}
927
+ ${whereClause}
928
+ ORDER BY createdAt DESC
929
+ ${limitClause}
930
+ ${offsetClause}
931
+ `,
932
+ query_params: values,
933
+ format: 'JSONEachRow',
934
+ });
935
+
936
+ const resultJson = await result.json();
937
+ const rows = resultJson as any[];
938
+ const runs = rows.map(row => {
939
+ let parsedSnapshot: WorkflowRunState | string = row.snapshot;
940
+ if (typeof parsedSnapshot === 'string') {
941
+ try {
942
+ parsedSnapshot = JSON.parse(row.snapshot) as WorkflowRunState;
943
+ } catch (e) {
944
+ // If parsing fails, return the raw snapshot string
945
+ console.warn(`Failed to parse snapshot for workflow ${row.workflow_name}: ${e}`);
946
+ }
947
+ }
948
+
949
+ return {
950
+ workflowName: row.workflow_name,
951
+ runId: row.run_id,
952
+ snapshot: parsedSnapshot,
953
+ createdAt: new Date(row.createdAt),
954
+ updatedAt: new Date(row.updatedAt),
955
+ };
956
+ });
957
+
958
+ // Use runs.length as total when not paginating
959
+ return { runs, total: total || runs.length };
960
+ } catch (error) {
961
+ console.error('Error getting workflow runs:', error);
962
+ throw error;
963
+ }
964
+ }
965
+
748
966
  async close(): Promise<void> {
749
967
  await this.db.close();
750
968
  }