@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.
- package/.turbo/turbo-build.log +7 -7
- package/CHANGELOG.md +24 -0
- package/dist/_tsup-dts-rollup.d.cts +46 -2
- package/dist/_tsup-dts-rollup.d.ts +46 -2
- package/dist/index.cjs +157 -11
- package/dist/index.js +157 -11
- package/package.json +2 -2
- package/src/storage/index.test.ts +287 -6
- package/src/storage/index.ts +228 -10
package/src/storage/index.ts
CHANGED
|
@@ -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
|
-
|
|
83
|
-
|
|
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}') = {
|
|
145
|
-
args[`
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|