@mastra/clickhouse 0.12.0 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,319 @@
1
+ import type { ClickHouseClient } from '@clickhouse/client';
2
+ import { ErrorCategory, ErrorDomain, MastraError } from '@mastra/core/error';
3
+ import { StoreOperations, TABLE_WORKFLOW_SNAPSHOT, TABLE_EVALS, TABLE_SCHEMAS } from '@mastra/core/storage';
4
+ import type { StorageColumn, TABLE_NAMES } from '@mastra/core/storage';
5
+ import type { ClickhouseConfig } from '../utils';
6
+ import { COLUMN_TYPES, TABLE_ENGINES, transformRow } from '../utils';
7
+
8
+ export class StoreOperationsClickhouse extends StoreOperations {
9
+ protected ttl: ClickhouseConfig['ttl'];
10
+ protected client: ClickHouseClient;
11
+ constructor({ client, ttl }: { client: ClickHouseClient; ttl: ClickhouseConfig['ttl'] }) {
12
+ super();
13
+ this.ttl = ttl;
14
+ this.client = client;
15
+ }
16
+
17
+ async hasColumn(table: string, column: string): Promise<boolean> {
18
+ const result = await this.client.query({
19
+ query: `DESCRIBE TABLE ${table}`,
20
+ format: 'JSONEachRow',
21
+ });
22
+ const columns = (await result.json()) as { name: string }[];
23
+ return columns.some(c => c.name === column);
24
+ }
25
+
26
+ protected getSqlType(type: StorageColumn['type']): string {
27
+ switch (type) {
28
+ case 'text':
29
+ return 'String';
30
+ case 'timestamp':
31
+ return 'DateTime64(3)';
32
+ case 'integer':
33
+ case 'bigint':
34
+ return 'Int64';
35
+ case 'jsonb':
36
+ return 'String';
37
+ default:
38
+ return super.getSqlType(type); // fallback to base implementation
39
+ }
40
+ }
41
+
42
+ async createTable({
43
+ tableName,
44
+ schema,
45
+ }: {
46
+ tableName: TABLE_NAMES;
47
+ schema: Record<string, StorageColumn>;
48
+ }): Promise<void> {
49
+ try {
50
+ const columns = Object.entries(schema)
51
+ .map(([name, def]) => {
52
+ const constraints = [];
53
+ if (!def.nullable) constraints.push('NOT NULL');
54
+ const columnTtl = this.ttl?.[tableName]?.columns?.[name];
55
+ return `"${name}" ${COLUMN_TYPES[def.type]} ${constraints.join(' ')} ${columnTtl ? `TTL toDateTime(${columnTtl.ttlKey ?? 'createdAt'}) + INTERVAL ${columnTtl.interval} ${columnTtl.unit}` : ''}`;
56
+ })
57
+ .join(',\n');
58
+
59
+ const rowTtl = this.ttl?.[tableName]?.row;
60
+ const sql =
61
+ tableName === TABLE_WORKFLOW_SNAPSHOT
62
+ ? `
63
+ CREATE TABLE IF NOT EXISTS ${tableName} (
64
+ ${['id String'].concat(columns)}
65
+ )
66
+ ENGINE = ${TABLE_ENGINES[tableName] ?? 'MergeTree()'}
67
+ PRIMARY KEY (createdAt, run_id, workflow_name)
68
+ ORDER BY (createdAt, run_id, workflow_name)
69
+ ${rowTtl ? `TTL toDateTime(${rowTtl.ttlKey ?? 'createdAt'}) + INTERVAL ${rowTtl.interval} ${rowTtl.unit}` : ''}
70
+ SETTINGS index_granularity = 8192
71
+ `
72
+ : `
73
+ CREATE TABLE IF NOT EXISTS ${tableName} (
74
+ ${columns}
75
+ )
76
+ ENGINE = ${TABLE_ENGINES[tableName] ?? 'MergeTree()'}
77
+ PRIMARY KEY (createdAt, ${tableName === TABLE_EVALS ? 'run_id' : 'id'})
78
+ ORDER BY (createdAt, ${tableName === TABLE_EVALS ? 'run_id' : 'id'})
79
+ ${this.ttl?.[tableName]?.row ? `TTL toDateTime(createdAt) + INTERVAL ${this.ttl[tableName].row.interval} ${this.ttl[tableName].row.unit}` : ''}
80
+ SETTINGS index_granularity = 8192
81
+ `;
82
+
83
+ await this.client.query({
84
+ query: sql,
85
+ clickhouse_settings: {
86
+ // Allows to insert serialized JS Dates (such as '2023-12-06T10:54:48.000Z')
87
+ date_time_input_format: 'best_effort',
88
+ date_time_output_format: 'iso',
89
+ use_client_time_zone: 1,
90
+ output_format_json_quote_64bit_integers: 0,
91
+ },
92
+ });
93
+ } catch (error: any) {
94
+ throw new MastraError(
95
+ {
96
+ id: 'CLICKHOUSE_STORAGE_CREATE_TABLE_FAILED',
97
+ domain: ErrorDomain.STORAGE,
98
+ category: ErrorCategory.THIRD_PARTY,
99
+ details: { tableName },
100
+ },
101
+ error,
102
+ );
103
+ }
104
+ }
105
+
106
+ async alterTable({
107
+ tableName,
108
+ schema,
109
+ ifNotExists,
110
+ }: {
111
+ tableName: TABLE_NAMES;
112
+ schema: Record<string, StorageColumn>;
113
+ ifNotExists: string[];
114
+ }): Promise<void> {
115
+ try {
116
+ // 1. Get existing columns
117
+ const describeSql = `DESCRIBE TABLE ${tableName}`;
118
+ const result = await this.client.query({
119
+ query: describeSql,
120
+ });
121
+ const rows = await result.json();
122
+ const existingColumnNames = new Set(rows.data.map((row: any) => row.name.toLowerCase()));
123
+
124
+ // 2. Add missing columns
125
+ for (const columnName of ifNotExists) {
126
+ if (!existingColumnNames.has(columnName.toLowerCase()) && schema[columnName]) {
127
+ const columnDef = schema[columnName];
128
+ let sqlType = this.getSqlType(columnDef.type);
129
+ if (columnDef.nullable !== false) {
130
+ sqlType = `Nullable(${sqlType})`;
131
+ }
132
+ const defaultValue = columnDef.nullable === false ? this.getDefaultValue(columnDef.type) : '';
133
+ // Use backticks or double quotes as needed for identifiers
134
+ const alterSql =
135
+ `ALTER TABLE ${tableName} ADD COLUMN IF NOT EXISTS "${columnName}" ${sqlType} ${defaultValue}`.trim();
136
+
137
+ await this.client.query({
138
+ query: alterSql,
139
+ });
140
+ this.logger?.debug?.(`Added column ${columnName} to table ${tableName}`);
141
+ }
142
+ }
143
+ } catch (error: any) {
144
+ throw new MastraError(
145
+ {
146
+ id: 'CLICKHOUSE_STORAGE_ALTER_TABLE_FAILED',
147
+ domain: ErrorDomain.STORAGE,
148
+ category: ErrorCategory.THIRD_PARTY,
149
+ details: { tableName },
150
+ },
151
+ error,
152
+ );
153
+ }
154
+ }
155
+
156
+ async clearTable({ tableName }: { tableName: TABLE_NAMES }): Promise<void> {
157
+ try {
158
+ await this.client.query({
159
+ query: `TRUNCATE TABLE ${tableName}`,
160
+ clickhouse_settings: {
161
+ // Allows to insert serialized JS Dates (such as '2023-12-06T10:54:48.000Z')
162
+ date_time_input_format: 'best_effort',
163
+ date_time_output_format: 'iso',
164
+ use_client_time_zone: 1,
165
+ output_format_json_quote_64bit_integers: 0,
166
+ },
167
+ });
168
+ } catch (error: any) {
169
+ throw new MastraError(
170
+ {
171
+ id: 'CLICKHOUSE_STORAGE_CLEAR_TABLE_FAILED',
172
+ domain: ErrorDomain.STORAGE,
173
+ category: ErrorCategory.THIRD_PARTY,
174
+ details: { tableName },
175
+ },
176
+ error,
177
+ );
178
+ }
179
+ }
180
+
181
+ async dropTable({ tableName }: { tableName: TABLE_NAMES }): Promise<void> {
182
+ await this.client.query({
183
+ query: `DROP TABLE IF EXISTS ${tableName}`,
184
+ });
185
+ }
186
+
187
+ async insert({ tableName, record }: { tableName: TABLE_NAMES; record: Record<string, any> }): Promise<void> {
188
+ const createdAt = (record.createdAt || record.created_at || new Date()).toISOString();
189
+ const updatedAt = (record.updatedAt || new Date()).toISOString();
190
+
191
+ try {
192
+ const result = await this.client.insert({
193
+ table: tableName,
194
+ values: [
195
+ {
196
+ ...record,
197
+ createdAt,
198
+ updatedAt,
199
+ },
200
+ ],
201
+ format: 'JSONEachRow',
202
+ clickhouse_settings: {
203
+ // Allows to insert serialized JS Dates (such as '2023-12-06T10:54:48.000Z')
204
+ output_format_json_quote_64bit_integers: 0,
205
+ date_time_input_format: 'best_effort',
206
+ use_client_time_zone: 1,
207
+ },
208
+ });
209
+ console.log('INSERT RESULT', result);
210
+ } catch (error: any) {
211
+ throw new MastraError(
212
+ {
213
+ id: 'CLICKHOUSE_STORAGE_INSERT_FAILED',
214
+ domain: ErrorDomain.STORAGE,
215
+ category: ErrorCategory.THIRD_PARTY,
216
+ details: { tableName },
217
+ },
218
+ error,
219
+ );
220
+ }
221
+ }
222
+
223
+ async batchInsert({ tableName, records }: { tableName: TABLE_NAMES; records: Record<string, any>[] }): Promise<void> {
224
+ const recordsToBeInserted = records.map(record => ({
225
+ ...Object.fromEntries(
226
+ Object.entries(record).map(([key, value]) => [
227
+ key,
228
+ TABLE_SCHEMAS[tableName as TABLE_NAMES]?.[key]?.type === 'timestamp' ? new Date(value).toISOString() : value,
229
+ ]),
230
+ ),
231
+ }));
232
+
233
+ try {
234
+ await this.client.insert({
235
+ table: tableName,
236
+ values: recordsToBeInserted,
237
+ format: 'JSONEachRow',
238
+ clickhouse_settings: {
239
+ // Allows to insert serialized JS Dates (such as '2023-12-06T10:54:48.000Z')
240
+ date_time_input_format: 'best_effort',
241
+ use_client_time_zone: 1,
242
+ output_format_json_quote_64bit_integers: 0,
243
+ },
244
+ });
245
+ } catch (error: any) {
246
+ throw new MastraError(
247
+ {
248
+ id: 'CLICKHOUSE_STORAGE_BATCH_INSERT_FAILED',
249
+ domain: ErrorDomain.STORAGE,
250
+ category: ErrorCategory.THIRD_PARTY,
251
+ details: { tableName },
252
+ },
253
+ error,
254
+ );
255
+ }
256
+ }
257
+
258
+ async load<R>({ tableName, keys }: { tableName: TABLE_NAMES; keys: Record<string, string> }): Promise<R | null> {
259
+ try {
260
+ const engine = TABLE_ENGINES[tableName] ?? 'MergeTree()';
261
+ const keyEntries = Object.entries(keys);
262
+ const conditions = keyEntries
263
+ .map(
264
+ ([key]) =>
265
+ `"${key}" = {var_${key}:${COLUMN_TYPES[TABLE_SCHEMAS[tableName as TABLE_NAMES]?.[key]?.type ?? 'text']}}`,
266
+ )
267
+ .join(' AND ');
268
+ const values = keyEntries.reduce((acc, [key, value]) => {
269
+ return { ...acc, [`var_${key}`]: value };
270
+ }, {});
271
+
272
+ const hasUpdatedAt = TABLE_SCHEMAS[tableName as TABLE_NAMES]?.updatedAt;
273
+
274
+ const selectClause = `SELECT *, toDateTime64(createdAt, 3) as createdAt${hasUpdatedAt ? ', toDateTime64(updatedAt, 3) as updatedAt' : ''}`;
275
+
276
+ const result = await this.client.query({
277
+ query: `${selectClause} FROM ${tableName} ${engine.startsWith('ReplacingMergeTree') ? 'FINAL' : ''} WHERE ${conditions}`,
278
+ query_params: values,
279
+ clickhouse_settings: {
280
+ // Allows to insert serialized JS Dates (such as '2023-12-06T10:54:48.000Z')
281
+ date_time_input_format: 'best_effort',
282
+ date_time_output_format: 'iso',
283
+ use_client_time_zone: 1,
284
+ output_format_json_quote_64bit_integers: 0,
285
+ },
286
+ });
287
+
288
+ if (!result) {
289
+ return null;
290
+ }
291
+
292
+ const rows = await result.json();
293
+ // If this is a workflow snapshot, parse the snapshot field
294
+ if (tableName === TABLE_WORKFLOW_SNAPSHOT) {
295
+ const snapshot = rows.data[0] as any;
296
+ if (!snapshot) {
297
+ return null;
298
+ }
299
+ if (typeof snapshot.snapshot === 'string') {
300
+ snapshot.snapshot = JSON.parse(snapshot.snapshot);
301
+ }
302
+ return transformRow(snapshot);
303
+ }
304
+
305
+ const data: R = transformRow(rows.data[0]);
306
+ return data;
307
+ } catch (error) {
308
+ throw new MastraError(
309
+ {
310
+ id: 'CLICKHOUSE_STORAGE_LOAD_FAILED',
311
+ domain: ErrorDomain.STORAGE,
312
+ category: ErrorCategory.THIRD_PARTY,
313
+ details: { tableName },
314
+ },
315
+ error,
316
+ );
317
+ }
318
+ }
319
+ }
@@ -0,0 +1,326 @@
1
+ import type { ClickHouseClient } from '@clickhouse/client';
2
+ import { ErrorCategory, ErrorDomain, MastraError } from '@mastra/core/error';
3
+ import type { ScoreRowData } from '@mastra/core/scores';
4
+ import { ScoresStorage, TABLE_SCORERS, safelyParseJSON } from '@mastra/core/storage';
5
+ import type { PaginationInfo, StoragePagination } from '@mastra/core/storage';
6
+ import type { StoreOperationsClickhouse } from '../operations';
7
+
8
+ export class ScoresStorageClickhouse extends ScoresStorage {
9
+ protected client: ClickHouseClient;
10
+ protected operations: StoreOperationsClickhouse;
11
+ constructor({ client, operations }: { client: ClickHouseClient; operations: StoreOperationsClickhouse }) {
12
+ super();
13
+ this.client = client;
14
+ this.operations = operations;
15
+ }
16
+
17
+ private transformScoreRow(row: any): ScoreRowData {
18
+ const scorer = safelyParseJSON(row.scorer);
19
+ const extractStepResult = safelyParseJSON(row.extractStepResult);
20
+ const analyzeStepResult = safelyParseJSON(row.analyzeStepResult);
21
+ const metadata = safelyParseJSON(row.metadata);
22
+ const input = safelyParseJSON(row.input);
23
+ const output = safelyParseJSON(row.output);
24
+ const additionalContext = safelyParseJSON(row.additionalContext);
25
+ const runtimeContext = safelyParseJSON(row.runtimeContext);
26
+ const entity = safelyParseJSON(row.entity);
27
+
28
+ return {
29
+ ...row,
30
+ scorer,
31
+ extractStepResult,
32
+ analyzeStepResult,
33
+ metadata,
34
+ input,
35
+ output,
36
+ additionalContext,
37
+ runtimeContext,
38
+ entity,
39
+ createdAt: new Date(row.createdAt),
40
+ updatedAt: new Date(row.updatedAt),
41
+ };
42
+ }
43
+
44
+ async getScoreById({ id }: { id: string }): Promise<ScoreRowData | null> {
45
+ try {
46
+ const result = await this.client.query({
47
+ query: `SELECT * FROM ${TABLE_SCORERS} WHERE id = {var_id:String}`,
48
+ query_params: { var_id: id },
49
+ format: 'JSONEachRow',
50
+ clickhouse_settings: {
51
+ // Allows to insert serialized JS Dates (such as '2023-12-06T10:54:48.000Z')
52
+ date_time_input_format: 'best_effort',
53
+ date_time_output_format: 'iso',
54
+ use_client_time_zone: 1,
55
+ output_format_json_quote_64bit_integers: 0,
56
+ },
57
+ });
58
+
59
+ const resultJson = await result.json();
60
+ if (!Array.isArray(resultJson) || resultJson.length === 0) {
61
+ return null;
62
+ }
63
+
64
+ return this.transformScoreRow(resultJson[0]);
65
+ // return this.parseScoreRow(resultJson[0]);
66
+ } catch (error: any) {
67
+ throw new MastraError(
68
+ {
69
+ id: 'CLICKHOUSE_STORAGE_GET_SCORE_BY_ID_FAILED',
70
+ domain: ErrorDomain.STORAGE,
71
+ category: ErrorCategory.THIRD_PARTY,
72
+ details: { scoreId: id },
73
+ },
74
+ error,
75
+ );
76
+ }
77
+ }
78
+
79
+ async saveScore(score: ScoreRowData): Promise<{ score: ScoreRowData }> {
80
+ try {
81
+ const record = {
82
+ ...score,
83
+ };
84
+ await this.client.insert({
85
+ table: TABLE_SCORERS,
86
+ values: [record],
87
+ format: 'JSONEachRow',
88
+ clickhouse_settings: {
89
+ date_time_input_format: 'best_effort',
90
+ use_client_time_zone: 1,
91
+ output_format_json_quote_64bit_integers: 0,
92
+ },
93
+ });
94
+ return { score };
95
+ } catch (error) {
96
+ throw new MastraError(
97
+ {
98
+ id: 'CLICKHOUSE_STORAGE_SAVE_SCORE_FAILED',
99
+ domain: ErrorDomain.STORAGE,
100
+ category: ErrorCategory.THIRD_PARTY,
101
+ details: { scoreId: score.id },
102
+ },
103
+ error,
104
+ );
105
+ }
106
+ }
107
+
108
+ async getScoresByRunId({
109
+ runId,
110
+ pagination,
111
+ }: {
112
+ runId: string;
113
+ pagination: StoragePagination;
114
+ }): Promise<{ pagination: PaginationInfo; scores: ScoreRowData[] }> {
115
+ try {
116
+ // Get total count
117
+ const countResult = await this.client.query({
118
+ query: `SELECT COUNT(*) as count FROM ${TABLE_SCORERS} WHERE runId = {var_runId:String}`,
119
+ query_params: { var_runId: runId },
120
+ format: 'JSONEachRow',
121
+ });
122
+ const countRows = await countResult.json();
123
+ let total = 0;
124
+ if (Array.isArray(countRows) && countRows.length > 0 && countRows[0]) {
125
+ const countObj = countRows[0] as { count: string | number };
126
+ total = Number(countObj.count);
127
+ }
128
+ if (!total) {
129
+ return {
130
+ pagination: {
131
+ total: 0,
132
+ page: pagination.page,
133
+ perPage: pagination.perPage,
134
+ hasMore: false,
135
+ },
136
+ scores: [],
137
+ };
138
+ }
139
+ // Get paginated results
140
+ const offset = pagination.page * pagination.perPage;
141
+ const result = await this.client.query({
142
+ query: `SELECT * FROM ${TABLE_SCORERS} WHERE runId = {var_runId:String} ORDER BY createdAt DESC LIMIT {var_limit:Int64} OFFSET {var_offset:Int64}`,
143
+ query_params: {
144
+ var_runId: runId,
145
+ var_limit: pagination.perPage,
146
+ var_offset: offset,
147
+ },
148
+ format: 'JSONEachRow',
149
+ clickhouse_settings: {
150
+ date_time_input_format: 'best_effort',
151
+ date_time_output_format: 'iso',
152
+ use_client_time_zone: 1,
153
+ output_format_json_quote_64bit_integers: 0,
154
+ },
155
+ });
156
+ const rows = await result.json();
157
+ const scores = Array.isArray(rows) ? rows.map(row => this.transformScoreRow(row)) : [];
158
+ return {
159
+ pagination: {
160
+ total,
161
+ page: pagination.page,
162
+ perPage: pagination.perPage,
163
+ hasMore: total > (pagination.page + 1) * pagination.perPage,
164
+ },
165
+ scores,
166
+ };
167
+ } catch (error: any) {
168
+ throw new MastraError(
169
+ {
170
+ id: 'CLICKHOUSE_STORAGE_GET_SCORES_BY_RUN_ID_FAILED',
171
+ domain: ErrorDomain.STORAGE,
172
+ category: ErrorCategory.THIRD_PARTY,
173
+ details: { runId },
174
+ },
175
+ error,
176
+ );
177
+ }
178
+ }
179
+
180
+ async getScoresByScorerId({
181
+ scorerId,
182
+ pagination,
183
+ }: {
184
+ scorerId: string;
185
+ pagination: StoragePagination;
186
+ }): Promise<{ pagination: PaginationInfo; scores: ScoreRowData[] }> {
187
+ try {
188
+ // Get total count
189
+ const countResult = await this.client.query({
190
+ query: `SELECT COUNT(*) as count FROM ${TABLE_SCORERS} WHERE scorerId = {var_scorerId:String}`,
191
+ query_params: { var_scorerId: scorerId },
192
+ format: 'JSONEachRow',
193
+ });
194
+ const countRows = await countResult.json();
195
+ let total = 0;
196
+ if (Array.isArray(countRows) && countRows.length > 0 && countRows[0]) {
197
+ const countObj = countRows[0] as { count: string | number };
198
+ total = Number(countObj.count);
199
+ }
200
+ if (!total) {
201
+ return {
202
+ pagination: {
203
+ total: 0,
204
+ page: pagination.page,
205
+ perPage: pagination.perPage,
206
+ hasMore: false,
207
+ },
208
+ scores: [],
209
+ };
210
+ }
211
+ // Get paginated results
212
+ const offset = pagination.page * pagination.perPage;
213
+ const result = await this.client.query({
214
+ query: `SELECT * FROM ${TABLE_SCORERS} WHERE scorerId = {var_scorerId:String} ORDER BY createdAt DESC LIMIT {var_limit:Int64} OFFSET {var_offset:Int64}`,
215
+ query_params: {
216
+ var_scorerId: scorerId,
217
+ var_limit: pagination.perPage,
218
+ var_offset: offset,
219
+ },
220
+ format: 'JSONEachRow',
221
+ clickhouse_settings: {
222
+ date_time_input_format: 'best_effort',
223
+ date_time_output_format: 'iso',
224
+ use_client_time_zone: 1,
225
+ output_format_json_quote_64bit_integers: 0,
226
+ },
227
+ });
228
+ const rows = await result.json();
229
+ const scores = Array.isArray(rows) ? rows.map(row => this.transformScoreRow(row)) : [];
230
+ return {
231
+ pagination: {
232
+ total,
233
+ page: pagination.page,
234
+ perPage: pagination.perPage,
235
+ hasMore: total > (pagination.page + 1) * pagination.perPage,
236
+ },
237
+ scores,
238
+ };
239
+ } catch (error: any) {
240
+ throw new MastraError(
241
+ {
242
+ id: 'CLICKHOUSE_STORAGE_GET_SCORES_BY_SCORER_ID_FAILED',
243
+ domain: ErrorDomain.STORAGE,
244
+ category: ErrorCategory.THIRD_PARTY,
245
+ details: { scorerId },
246
+ },
247
+ error,
248
+ );
249
+ }
250
+ }
251
+
252
+ async getScoresByEntityId({
253
+ entityId,
254
+ entityType,
255
+ pagination,
256
+ }: {
257
+ pagination: StoragePagination;
258
+ entityId: string;
259
+ entityType: string;
260
+ }): Promise<{ pagination: PaginationInfo; scores: ScoreRowData[] }> {
261
+ try {
262
+ // Get total count
263
+ const countResult = await this.client.query({
264
+ query: `SELECT COUNT(*) as count FROM ${TABLE_SCORERS} WHERE entityId = {var_entityId:String} AND entityType = {var_entityType:String}`,
265
+ query_params: { var_entityId: entityId, var_entityType: entityType },
266
+ format: 'JSONEachRow',
267
+ });
268
+ const countRows = await countResult.json();
269
+ let total = 0;
270
+ if (Array.isArray(countRows) && countRows.length > 0 && countRows[0]) {
271
+ const countObj = countRows[0] as { count: string | number };
272
+ total = Number(countObj.count);
273
+ }
274
+ if (!total) {
275
+ return {
276
+ pagination: {
277
+ total: 0,
278
+ page: pagination.page,
279
+ perPage: pagination.perPage,
280
+ hasMore: false,
281
+ },
282
+ scores: [],
283
+ };
284
+ }
285
+ // Get paginated results
286
+ const offset = pagination.page * pagination.perPage;
287
+ const result = await this.client.query({
288
+ 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}`,
289
+ query_params: {
290
+ var_entityId: entityId,
291
+ var_entityType: entityType,
292
+ var_limit: pagination.perPage,
293
+ var_offset: offset,
294
+ },
295
+ format: 'JSONEachRow',
296
+ clickhouse_settings: {
297
+ date_time_input_format: 'best_effort',
298
+ date_time_output_format: 'iso',
299
+ use_client_time_zone: 1,
300
+ output_format_json_quote_64bit_integers: 0,
301
+ },
302
+ });
303
+ const rows = await result.json();
304
+ const scores = Array.isArray(rows) ? rows.map(row => this.transformScoreRow(row)) : [];
305
+ return {
306
+ pagination: {
307
+ total,
308
+ page: pagination.page,
309
+ perPage: pagination.perPage,
310
+ hasMore: total > (pagination.page + 1) * pagination.perPage,
311
+ },
312
+ scores,
313
+ };
314
+ } catch (error: any) {
315
+ throw new MastraError(
316
+ {
317
+ id: 'CLICKHOUSE_STORAGE_GET_SCORES_BY_ENTITY_ID_FAILED',
318
+ domain: ErrorDomain.STORAGE,
319
+ category: ErrorCategory.THIRD_PARTY,
320
+ details: { entityId, entityType },
321
+ },
322
+ error,
323
+ );
324
+ }
325
+ }
326
+ }