@mastra/clickhouse 1.0.0-beta.8 → 1.0.0
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/CHANGELOG.md +993 -0
- package/README.md +1 -1
- package/dist/docs/README.md +31 -0
- package/dist/docs/SKILL.md +32 -0
- package/dist/docs/SOURCE_MAP.json +6 -0
- package/dist/docs/storage/01-reference.md +182 -0
- package/dist/index.cjs +392 -36
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +393 -37
- package/dist/index.js.map +1 -1
- package/dist/storage/db/index.d.ts +50 -0
- package/dist/storage/db/index.d.ts.map +1 -1
- package/dist/storage/db/utils.d.ts.map +1 -1
- package/dist/storage/domains/memory/index.d.ts +2 -2
- package/dist/storage/domains/memory/index.d.ts.map +1 -1
- package/dist/storage/domains/observability/index.d.ts +23 -0
- package/dist/storage/domains/observability/index.d.ts.map +1 -1
- package/dist/storage/index.d.ts +42 -9
- package/dist/storage/index.d.ts.map +1 -1
- package/package.json +9 -8
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createClient } from '@clickhouse/client';
|
|
2
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, MemoryStorage, TABLE_SCHEMAS, createStorageErrorId, normalizePerPage, calculatePagination, ObservabilityStorage, SPAN_SCHEMA, listTracesArgsSchema, ScoresStorage, transformScoreRow, SCORERS_SCHEMA, WorkflowsStorage,
|
|
3
|
+
import { TABLE_SPANS, TABLE_RESOURCES, TABLE_SCORERS, TABLE_THREADS, TABLE_TRACES, TABLE_WORKFLOW_SNAPSHOT, TABLE_MESSAGES, MemoryStorage, TABLE_SCHEMAS, createStorageErrorId, normalizePerPage, calculatePagination, ObservabilityStorage, SPAN_SCHEMA, listTracesArgsSchema, ScoresStorage, transformScoreRow, SCORERS_SCHEMA, WorkflowsStorage, MastraCompositeStore, TraceStatus, getSqlType, getDefaultValue, safelyParseJSON } from '@mastra/core/storage';
|
|
4
4
|
import { MessageList } from '@mastra/core/agent';
|
|
5
5
|
import { MastraBase } from '@mastra/core/base';
|
|
6
6
|
import { saveScorePayloadSchema } from '@mastra/core/evals';
|
|
@@ -13,8 +13,10 @@ var TABLE_ENGINES = {
|
|
|
13
13
|
[TABLE_THREADS]: `ReplacingMergeTree()`,
|
|
14
14
|
[TABLE_SCORERS]: `MergeTree()`,
|
|
15
15
|
[TABLE_RESOURCES]: `ReplacingMergeTree()`,
|
|
16
|
-
//
|
|
17
|
-
|
|
16
|
+
// ReplacingMergeTree(updatedAt) deduplicates rows with the same (traceId, spanId) sorting key,
|
|
17
|
+
// keeping the row with the highest updatedAt value. Combined with ORDER BY (traceId, spanId),
|
|
18
|
+
// this provides eventual uniqueness for the (traceId, spanId) composite key.
|
|
19
|
+
[TABLE_SPANS]: `ReplacingMergeTree(updatedAt)`,
|
|
18
20
|
mastra_agents: `ReplacingMergeTree()`
|
|
19
21
|
};
|
|
20
22
|
var COLUMN_TYPES = {
|
|
@@ -97,6 +99,213 @@ var ClickhouseDB = class extends MastraBase {
|
|
|
97
99
|
const columns = await result.json();
|
|
98
100
|
return columns.some((c) => c.name === column);
|
|
99
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* Checks if a table exists in the database.
|
|
104
|
+
*/
|
|
105
|
+
async tableExists(tableName) {
|
|
106
|
+
try {
|
|
107
|
+
const result = await this.client.query({
|
|
108
|
+
query: `EXISTS TABLE ${tableName}`,
|
|
109
|
+
format: "JSONEachRow"
|
|
110
|
+
});
|
|
111
|
+
const rows = await result.json();
|
|
112
|
+
return rows[0]?.result === 1;
|
|
113
|
+
} catch {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Gets the sorting key (ORDER BY columns) for a table.
|
|
119
|
+
* Returns null if the table doesn't exist.
|
|
120
|
+
*/
|
|
121
|
+
async getTableSortingKey(tableName) {
|
|
122
|
+
try {
|
|
123
|
+
const result = await this.client.query({
|
|
124
|
+
query: `SELECT sorting_key FROM system.tables WHERE database = currentDatabase() AND name = {tableName:String}`,
|
|
125
|
+
query_params: { tableName },
|
|
126
|
+
format: "JSONEachRow"
|
|
127
|
+
});
|
|
128
|
+
const rows = await result.json();
|
|
129
|
+
return rows[0]?.sorting_key ?? null;
|
|
130
|
+
} catch {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Checks if migration is needed for the spans table.
|
|
136
|
+
* Returns information about the current state.
|
|
137
|
+
*/
|
|
138
|
+
async checkSpansMigrationStatus(tableName) {
|
|
139
|
+
const exists = await this.tableExists(tableName);
|
|
140
|
+
if (!exists) {
|
|
141
|
+
return { needsMigration: false, currentSortingKey: null };
|
|
142
|
+
}
|
|
143
|
+
const currentSortingKey = await this.getTableSortingKey(tableName);
|
|
144
|
+
if (!currentSortingKey) {
|
|
145
|
+
return { needsMigration: false, currentSortingKey: null };
|
|
146
|
+
}
|
|
147
|
+
const needsMigration = currentSortingKey.toLowerCase().startsWith("createdat");
|
|
148
|
+
return { needsMigration, currentSortingKey };
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Checks for duplicate (traceId, spanId) combinations in the spans table.
|
|
152
|
+
* Returns information about duplicates for logging/CLI purposes.
|
|
153
|
+
*/
|
|
154
|
+
async checkForDuplicateSpans(tableName) {
|
|
155
|
+
try {
|
|
156
|
+
const result = await this.client.query({
|
|
157
|
+
query: `
|
|
158
|
+
SELECT count() as duplicate_count
|
|
159
|
+
FROM (
|
|
160
|
+
SELECT traceId, spanId
|
|
161
|
+
FROM ${tableName}
|
|
162
|
+
GROUP BY traceId, spanId
|
|
163
|
+
HAVING count() > 1
|
|
164
|
+
)
|
|
165
|
+
`,
|
|
166
|
+
format: "JSONEachRow"
|
|
167
|
+
});
|
|
168
|
+
const rows = await result.json();
|
|
169
|
+
const duplicateCount = parseInt(rows[0]?.duplicate_count ?? "0", 10);
|
|
170
|
+
return {
|
|
171
|
+
hasDuplicates: duplicateCount > 0,
|
|
172
|
+
duplicateCount
|
|
173
|
+
};
|
|
174
|
+
} catch (error) {
|
|
175
|
+
this.logger?.debug?.(`Could not check for duplicates: ${error}`);
|
|
176
|
+
return { hasDuplicates: false, duplicateCount: 0 };
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Migrates the spans table from the old sorting key (createdAt, traceId, spanId)
|
|
181
|
+
* to the new sorting key (traceId, spanId) for proper uniqueness enforcement.
|
|
182
|
+
*
|
|
183
|
+
* This migration:
|
|
184
|
+
* 1. Renames the old table to a backup
|
|
185
|
+
* 2. Creates a new table with the correct sorting key
|
|
186
|
+
* 3. Copies all data from the backup to the new table, deduplicating by (traceId, spanId)
|
|
187
|
+
* using priority-based selection:
|
|
188
|
+
* - First, prefer completed spans (those with endedAt set)
|
|
189
|
+
* - Then prefer the most recently updated span (highest updatedAt)
|
|
190
|
+
* - Finally use creation time as tiebreaker (highest createdAt)
|
|
191
|
+
* 4. Drops the backup table
|
|
192
|
+
*
|
|
193
|
+
* The deduplication strategy matches the PostgreSQL migration (PR #12073) to ensure
|
|
194
|
+
* consistent behavior across storage backends.
|
|
195
|
+
*
|
|
196
|
+
* The migration is idempotent - it only runs if the old sorting key is detected.
|
|
197
|
+
*
|
|
198
|
+
* @returns true if migration was performed, false if not needed
|
|
199
|
+
*/
|
|
200
|
+
async migrateSpansTableSortingKey({
|
|
201
|
+
tableName,
|
|
202
|
+
schema
|
|
203
|
+
}) {
|
|
204
|
+
if (tableName !== TABLE_SPANS) {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
const exists = await this.tableExists(tableName);
|
|
208
|
+
if (!exists) {
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
const currentSortingKey = await this.getTableSortingKey(tableName);
|
|
212
|
+
if (!currentSortingKey) {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
const needsMigration = currentSortingKey.toLowerCase().startsWith("createdat");
|
|
216
|
+
if (!needsMigration) {
|
|
217
|
+
this.logger?.debug?.(`Spans table already has correct sorting key: ${currentSortingKey}`);
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
this.logger?.info?.(`Migrating spans table from sorting key "${currentSortingKey}" to "(traceId, spanId)"`);
|
|
221
|
+
const backupTableName = `${tableName}_backup_${Date.now()}`;
|
|
222
|
+
const rowTtl = this.ttl?.[tableName]?.row;
|
|
223
|
+
try {
|
|
224
|
+
await this.client.command({
|
|
225
|
+
query: `RENAME TABLE ${tableName} TO ${backupTableName}`
|
|
226
|
+
});
|
|
227
|
+
const columns = Object.entries(schema).map(([name, def]) => {
|
|
228
|
+
let sqlType = this.getSqlType(def.type);
|
|
229
|
+
let isNullable = def.nullable === true;
|
|
230
|
+
if (tableName === TABLE_SPANS && name === "updatedAt") {
|
|
231
|
+
isNullable = false;
|
|
232
|
+
}
|
|
233
|
+
if (isNullable) {
|
|
234
|
+
sqlType = `Nullable(${sqlType})`;
|
|
235
|
+
}
|
|
236
|
+
const constraints = [];
|
|
237
|
+
if (name === "metadata" && (def.type === "text" || def.type === "jsonb") && isNullable) {
|
|
238
|
+
constraints.push("DEFAULT '{}'");
|
|
239
|
+
}
|
|
240
|
+
const columnTtl = this.ttl?.[tableName]?.columns?.[name];
|
|
241
|
+
return `"${name}" ${sqlType} ${constraints.join(" ")} ${columnTtl ? `TTL toDateTime(${columnTtl.ttlKey ?? "createdAt"}) + INTERVAL ${columnTtl.interval} ${columnTtl.unit}` : ""}`;
|
|
242
|
+
}).join(",\n");
|
|
243
|
+
const createSql = `
|
|
244
|
+
CREATE TABLE ${tableName} (
|
|
245
|
+
${columns}
|
|
246
|
+
)
|
|
247
|
+
ENGINE = ${TABLE_ENGINES[tableName] ?? "MergeTree()"}
|
|
248
|
+
PRIMARY KEY (traceId, spanId)
|
|
249
|
+
ORDER BY (traceId, spanId)
|
|
250
|
+
${rowTtl ? `TTL toDateTime(${rowTtl.ttlKey ?? "createdAt"}) + INTERVAL ${rowTtl.interval} ${rowTtl.unit}` : ""}
|
|
251
|
+
SETTINGS index_granularity = 8192
|
|
252
|
+
`;
|
|
253
|
+
await this.client.command({
|
|
254
|
+
query: createSql
|
|
255
|
+
});
|
|
256
|
+
const describeResult = await this.client.query({
|
|
257
|
+
query: `DESCRIBE TABLE ${backupTableName}`,
|
|
258
|
+
format: "JSONEachRow"
|
|
259
|
+
});
|
|
260
|
+
const backupColumns = await describeResult.json();
|
|
261
|
+
const backupColumnNames = new Set(backupColumns.map((c) => c.name));
|
|
262
|
+
const columnsToInsert = Object.keys(schema).filter((col) => backupColumnNames.has(col));
|
|
263
|
+
const columnList = columnsToInsert.map((c) => `"${c}"`).join(", ");
|
|
264
|
+
const selectExpressions = columnsToInsert.map((c) => c === "updatedAt" ? `COALESCE("updatedAt", "createdAt") as "updatedAt"` : `"${c}"`).join(", ");
|
|
265
|
+
await this.client.command({
|
|
266
|
+
query: `INSERT INTO ${tableName} (${columnList})
|
|
267
|
+
SELECT ${selectExpressions}
|
|
268
|
+
FROM ${backupTableName}
|
|
269
|
+
ORDER BY traceId, spanId,
|
|
270
|
+
(endedAt IS NOT NULL AND endedAt != '') DESC,
|
|
271
|
+
COALESCE(updatedAt, createdAt) DESC,
|
|
272
|
+
createdAt DESC
|
|
273
|
+
LIMIT 1 BY traceId, spanId`
|
|
274
|
+
});
|
|
275
|
+
await this.client.command({
|
|
276
|
+
query: `DROP TABLE ${backupTableName}`
|
|
277
|
+
});
|
|
278
|
+
this.logger?.info?.(`Successfully migrated spans table to new sorting key`);
|
|
279
|
+
return true;
|
|
280
|
+
} catch (error) {
|
|
281
|
+
this.logger?.error?.(`Migration failed: ${error.message}`);
|
|
282
|
+
try {
|
|
283
|
+
const originalExists = await this.tableExists(tableName);
|
|
284
|
+
const backupExists = await this.tableExists(backupTableName);
|
|
285
|
+
if (!originalExists && backupExists) {
|
|
286
|
+
this.logger?.info?.(`Restoring spans table from backup`);
|
|
287
|
+
await this.client.command({
|
|
288
|
+
query: `RENAME TABLE ${backupTableName} TO ${tableName}`
|
|
289
|
+
});
|
|
290
|
+
} else if (originalExists && backupExists) {
|
|
291
|
+
await this.client.command({
|
|
292
|
+
query: `DROP TABLE IF EXISTS ${backupTableName}`
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
} catch (restoreError) {
|
|
296
|
+
this.logger?.error?.(`Failed to restore from backup: ${restoreError}`);
|
|
297
|
+
}
|
|
298
|
+
throw new MastraError(
|
|
299
|
+
{
|
|
300
|
+
id: createStorageErrorId("CLICKHOUSE", "MIGRATE_SPANS_SORTING_KEY", "FAILED"),
|
|
301
|
+
domain: ErrorDomain.STORAGE,
|
|
302
|
+
category: ErrorCategory.THIRD_PARTY,
|
|
303
|
+
details: { tableName, currentSortingKey }
|
|
304
|
+
},
|
|
305
|
+
error
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
100
309
|
getSqlType(type) {
|
|
101
310
|
switch (type) {
|
|
102
311
|
case "text":
|
|
@@ -123,12 +332,15 @@ var ClickhouseDB = class extends MastraBase {
|
|
|
123
332
|
try {
|
|
124
333
|
const columns = Object.entries(schema).map(([name, def]) => {
|
|
125
334
|
let sqlType = this.getSqlType(def.type);
|
|
126
|
-
|
|
335
|
+
let isNullable = def.nullable === true;
|
|
336
|
+
if (tableName === TABLE_SPANS && name === "updatedAt") {
|
|
337
|
+
isNullable = false;
|
|
338
|
+
}
|
|
127
339
|
if (isNullable) {
|
|
128
340
|
sqlType = `Nullable(${sqlType})`;
|
|
129
341
|
}
|
|
130
342
|
const constraints = [];
|
|
131
|
-
if (name === "metadata" && def.type === "text" && isNullable) {
|
|
343
|
+
if (name === "metadata" && (def.type === "text" || def.type === "jsonb") && isNullable) {
|
|
132
344
|
constraints.push("DEFAULT '{}'");
|
|
133
345
|
}
|
|
134
346
|
const columnTtl = this.ttl?.[tableName]?.columns?.[name];
|
|
@@ -153,8 +365,8 @@ var ClickhouseDB = class extends MastraBase {
|
|
|
153
365
|
${columns}
|
|
154
366
|
)
|
|
155
367
|
ENGINE = ${TABLE_ENGINES[tableName] ?? "MergeTree()"}
|
|
156
|
-
PRIMARY KEY (
|
|
157
|
-
ORDER BY (
|
|
368
|
+
PRIMARY KEY (traceId, spanId)
|
|
369
|
+
ORDER BY (traceId, spanId)
|
|
158
370
|
${rowTtl ? `TTL toDateTime(${rowTtl.ttlKey ?? "createdAt"}) + INTERVAL ${rowTtl.interval} ${rowTtl.unit}` : ""}
|
|
159
371
|
SETTINGS index_granularity = 8192
|
|
160
372
|
`;
|
|
@@ -300,7 +512,9 @@ var ClickhouseDB = class extends MastraBase {
|
|
|
300
512
|
...Object.fromEntries(
|
|
301
513
|
Object.entries(record).map(([key, value]) => [
|
|
302
514
|
key,
|
|
303
|
-
|
|
515
|
+
// Only convert to Date if it's a timestamp column AND value is not null/undefined
|
|
516
|
+
// new Date(null) returns epoch date, not null, so we must check first
|
|
517
|
+
TABLE_SCHEMAS[tableName]?.[key]?.type === "timestamp" && value != null ? new Date(value).toISOString() : value
|
|
304
518
|
])
|
|
305
519
|
)
|
|
306
520
|
}));
|
|
@@ -561,12 +775,14 @@ var MemoryStorageClickhouse = class extends MemoryStorage {
|
|
|
561
775
|
}
|
|
562
776
|
if (filter?.dateRange?.start) {
|
|
563
777
|
const startDate = filter.dateRange.start instanceof Date ? filter.dateRange.start.toISOString() : new Date(filter.dateRange.start).toISOString();
|
|
564
|
-
|
|
778
|
+
const startOp = filter.dateRange.startExclusive ? ">" : ">=";
|
|
779
|
+
dataQuery += ` AND createdAt ${startOp} parseDateTime64BestEffort({fromDate:String}, 3)`;
|
|
565
780
|
dataParams.fromDate = startDate;
|
|
566
781
|
}
|
|
567
782
|
if (filter?.dateRange?.end) {
|
|
568
783
|
const endDate = filter.dateRange.end instanceof Date ? filter.dateRange.end.toISOString() : new Date(filter.dateRange.end).toISOString();
|
|
569
|
-
|
|
784
|
+
const endOp = filter.dateRange.endExclusive ? "<" : "<=";
|
|
785
|
+
dataQuery += ` AND createdAt ${endOp} parseDateTime64BestEffort({toDate:String}, 3)`;
|
|
570
786
|
dataParams.toDate = endDate;
|
|
571
787
|
}
|
|
572
788
|
const { field, direction } = this.parseOrderBy(orderBy, "ASC");
|
|
@@ -600,12 +816,14 @@ var MemoryStorageClickhouse = class extends MemoryStorage {
|
|
|
600
816
|
}
|
|
601
817
|
if (filter?.dateRange?.start) {
|
|
602
818
|
const startDate = filter.dateRange.start instanceof Date ? filter.dateRange.start.toISOString() : new Date(filter.dateRange.start).toISOString();
|
|
603
|
-
|
|
819
|
+
const startOp = filter.dateRange.startExclusive ? ">" : ">=";
|
|
820
|
+
countQuery += ` AND createdAt ${startOp} parseDateTime64BestEffort({fromDate:String}, 3)`;
|
|
604
821
|
countParams.fromDate = startDate;
|
|
605
822
|
}
|
|
606
823
|
if (filter?.dateRange?.end) {
|
|
607
824
|
const endDate = filter.dateRange.end instanceof Date ? filter.dateRange.end.toISOString() : new Date(filter.dateRange.end).toISOString();
|
|
608
|
-
|
|
825
|
+
const endOp = filter.dateRange.endExclusive ? "<" : "<=";
|
|
826
|
+
countQuery += ` AND createdAt ${endOp} parseDateTime64BestEffort({toDate:String}, 3)`;
|
|
609
827
|
countParams.toDate = endDate;
|
|
610
828
|
}
|
|
611
829
|
const countResult = await this.client.query({
|
|
@@ -1062,26 +1280,68 @@ var MemoryStorageClickhouse = class extends MemoryStorage {
|
|
|
1062
1280
|
);
|
|
1063
1281
|
}
|
|
1064
1282
|
}
|
|
1065
|
-
async
|
|
1066
|
-
const {
|
|
1283
|
+
async listThreads(args) {
|
|
1284
|
+
const { page = 0, perPage: perPageInput, orderBy, filter } = args;
|
|
1285
|
+
try {
|
|
1286
|
+
this.validatePaginationInput(page, perPageInput ?? 100);
|
|
1287
|
+
} catch (error) {
|
|
1288
|
+
throw new MastraError(
|
|
1289
|
+
{
|
|
1290
|
+
id: createStorageErrorId("CLICKHOUSE", "LIST_THREADS", "INVALID_PAGE"),
|
|
1291
|
+
domain: ErrorDomain.STORAGE,
|
|
1292
|
+
category: ErrorCategory.USER,
|
|
1293
|
+
details: { page, ...perPageInput !== void 0 && { perPage: perPageInput } }
|
|
1294
|
+
},
|
|
1295
|
+
error instanceof Error ? error : new Error("Invalid pagination parameters")
|
|
1296
|
+
);
|
|
1297
|
+
}
|
|
1067
1298
|
const perPage = normalizePerPage(perPageInput, 100);
|
|
1068
|
-
|
|
1299
|
+
try {
|
|
1300
|
+
this.validateMetadataKeys(filter?.metadata);
|
|
1301
|
+
} catch (error) {
|
|
1069
1302
|
throw new MastraError(
|
|
1070
1303
|
{
|
|
1071
|
-
id: createStorageErrorId("CLICKHOUSE", "
|
|
1304
|
+
id: createStorageErrorId("CLICKHOUSE", "LIST_THREADS", "INVALID_METADATA_KEY"),
|
|
1072
1305
|
domain: ErrorDomain.STORAGE,
|
|
1073
1306
|
category: ErrorCategory.USER,
|
|
1074
|
-
details: {
|
|
1307
|
+
details: { metadataKeys: filter?.metadata ? Object.keys(filter.metadata).join(", ") : "" }
|
|
1075
1308
|
},
|
|
1076
|
-
new Error("
|
|
1309
|
+
error instanceof Error ? error : new Error("Invalid metadata key")
|
|
1077
1310
|
);
|
|
1078
1311
|
}
|
|
1079
1312
|
const { offset, perPage: perPageForResponse } = calculatePagination(page, perPageInput, perPage);
|
|
1080
1313
|
const { field, direction } = this.parseOrderBy(orderBy);
|
|
1081
1314
|
try {
|
|
1315
|
+
const whereClauses = [];
|
|
1316
|
+
const queryParams = {};
|
|
1317
|
+
if (filter?.resourceId) {
|
|
1318
|
+
whereClauses.push("resourceId = {resourceId:String}");
|
|
1319
|
+
queryParams.resourceId = filter.resourceId;
|
|
1320
|
+
}
|
|
1321
|
+
if (filter?.metadata && Object.keys(filter.metadata).length > 0) {
|
|
1322
|
+
let metadataIndex = 0;
|
|
1323
|
+
for (const [key, value] of Object.entries(filter.metadata)) {
|
|
1324
|
+
const paramName = `metadata${metadataIndex}`;
|
|
1325
|
+
whereClauses.push(`JSONExtractRaw(metadata, '${key}') = {${paramName}:String}`);
|
|
1326
|
+
queryParams[paramName] = JSON.stringify(value);
|
|
1327
|
+
metadataIndex++;
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1082
1330
|
const countResult = await this.client.query({
|
|
1083
|
-
query: `
|
|
1084
|
-
|
|
1331
|
+
query: `
|
|
1332
|
+
WITH ranked_threads AS (
|
|
1333
|
+
SELECT
|
|
1334
|
+
id,
|
|
1335
|
+
resourceId,
|
|
1336
|
+
metadata,
|
|
1337
|
+
ROW_NUMBER() OVER (PARTITION BY id ORDER BY updatedAt DESC) as row_num
|
|
1338
|
+
FROM ${TABLE_THREADS}
|
|
1339
|
+
)
|
|
1340
|
+
SELECT count(*) as total
|
|
1341
|
+
FROM ranked_threads
|
|
1342
|
+
WHERE row_num = 1 ${whereClauses.length > 0 ? `AND ${whereClauses.join(" AND ")}` : ""}
|
|
1343
|
+
`,
|
|
1344
|
+
query_params: queryParams,
|
|
1085
1345
|
clickhouse_settings: {
|
|
1086
1346
|
date_time_input_format: "best_effort",
|
|
1087
1347
|
date_time_output_format: "iso",
|
|
@@ -1112,7 +1372,6 @@ var MemoryStorageClickhouse = class extends MemoryStorage {
|
|
|
1112
1372
|
toDateTime64(updatedAt, 3) as updatedAt,
|
|
1113
1373
|
ROW_NUMBER() OVER (PARTITION BY id ORDER BY updatedAt DESC) as row_num
|
|
1114
1374
|
FROM ${TABLE_THREADS}
|
|
1115
|
-
WHERE resourceId = {resourceId:String}
|
|
1116
1375
|
)
|
|
1117
1376
|
SELECT
|
|
1118
1377
|
id,
|
|
@@ -1122,12 +1381,12 @@ var MemoryStorageClickhouse = class extends MemoryStorage {
|
|
|
1122
1381
|
createdAt,
|
|
1123
1382
|
updatedAt
|
|
1124
1383
|
FROM ranked_threads
|
|
1125
|
-
WHERE row_num = 1
|
|
1384
|
+
WHERE row_num = 1 ${whereClauses.length > 0 ? `AND ${whereClauses.join(" AND ")}` : ""}
|
|
1126
1385
|
ORDER BY "${field}" ${direction === "DESC" ? "DESC" : "ASC"}
|
|
1127
1386
|
LIMIT {perPage:Int64} OFFSET {offset:Int64}
|
|
1128
1387
|
`,
|
|
1129
1388
|
query_params: {
|
|
1130
|
-
|
|
1389
|
+
...queryParams,
|
|
1131
1390
|
perPage,
|
|
1132
1391
|
offset
|
|
1133
1392
|
},
|
|
@@ -1153,10 +1412,14 @@ var MemoryStorageClickhouse = class extends MemoryStorage {
|
|
|
1153
1412
|
} catch (error) {
|
|
1154
1413
|
throw new MastraError(
|
|
1155
1414
|
{
|
|
1156
|
-
id: createStorageErrorId("CLICKHOUSE", "
|
|
1415
|
+
id: createStorageErrorId("CLICKHOUSE", "LIST_THREADS", "FAILED"),
|
|
1157
1416
|
domain: ErrorDomain.STORAGE,
|
|
1158
1417
|
category: ErrorCategory.THIRD_PARTY,
|
|
1159
|
-
details: {
|
|
1418
|
+
details: {
|
|
1419
|
+
...filter?.resourceId && { resourceId: filter.resourceId },
|
|
1420
|
+
hasMetadataFilter: !!filter?.metadata,
|
|
1421
|
+
page
|
|
1422
|
+
}
|
|
1160
1423
|
},
|
|
1161
1424
|
error
|
|
1162
1425
|
);
|
|
@@ -1464,7 +1727,7 @@ var MemoryStorageClickhouse = class extends MemoryStorage {
|
|
|
1464
1727
|
return {
|
|
1465
1728
|
id: resource.id,
|
|
1466
1729
|
workingMemory: resource.workingMemory && typeof resource.workingMemory === "object" ? JSON.stringify(resource.workingMemory) : resource.workingMemory,
|
|
1467
|
-
metadata:
|
|
1730
|
+
metadata: parseMetadata(resource.metadata),
|
|
1468
1731
|
createdAt: new Date(resource.createdAt),
|
|
1469
1732
|
updatedAt: new Date(resource.updatedAt)
|
|
1470
1733
|
};
|
|
@@ -1489,7 +1752,7 @@ var MemoryStorageClickhouse = class extends MemoryStorage {
|
|
|
1489
1752
|
{
|
|
1490
1753
|
id: resource.id,
|
|
1491
1754
|
workingMemory: resource.workingMemory,
|
|
1492
|
-
metadata:
|
|
1755
|
+
metadata: serializeMetadata(resource.metadata),
|
|
1493
1756
|
createdAt: resource.createdAt.toISOString(),
|
|
1494
1757
|
updatedAt: resource.updatedAt.toISOString()
|
|
1495
1758
|
}
|
|
@@ -1500,7 +1763,10 @@ var MemoryStorageClickhouse = class extends MemoryStorage {
|
|
|
1500
1763
|
output_format_json_quote_64bit_integers: 0
|
|
1501
1764
|
}
|
|
1502
1765
|
});
|
|
1503
|
-
return
|
|
1766
|
+
return {
|
|
1767
|
+
...resource,
|
|
1768
|
+
metadata: resource.metadata || {}
|
|
1769
|
+
};
|
|
1504
1770
|
} catch (error) {
|
|
1505
1771
|
throw new MastraError(
|
|
1506
1772
|
{
|
|
@@ -1590,11 +1856,101 @@ var ObservabilityStorageClickhouse = class extends ObservabilityStorage {
|
|
|
1590
1856
|
this.#db = new ClickhouseDB({ client, ttl });
|
|
1591
1857
|
}
|
|
1592
1858
|
async init() {
|
|
1859
|
+
const migrationStatus = await this.#db.checkSpansMigrationStatus(TABLE_SPANS);
|
|
1860
|
+
if (migrationStatus.needsMigration) {
|
|
1861
|
+
const duplicateInfo = await this.#db.checkForDuplicateSpans(TABLE_SPANS);
|
|
1862
|
+
const duplicateMessage = duplicateInfo.hasDuplicates ? `
|
|
1863
|
+
Found ${duplicateInfo.duplicateCount} duplicate (traceId, spanId) combinations that will be removed.
|
|
1864
|
+
` : "";
|
|
1865
|
+
const errorMessage = `
|
|
1866
|
+
===========================================================================
|
|
1867
|
+
MIGRATION REQUIRED: ClickHouse spans table needs sorting key update
|
|
1868
|
+
===========================================================================
|
|
1869
|
+
|
|
1870
|
+
The spans table structure has changed. ClickHouse requires a table recreation
|
|
1871
|
+
to update the sorting key from (traceId) to (traceId, spanId).
|
|
1872
|
+
` + duplicateMessage + `
|
|
1873
|
+
To fix this, run the manual migration command:
|
|
1874
|
+
|
|
1875
|
+
npx mastra migrate
|
|
1876
|
+
|
|
1877
|
+
This command will:
|
|
1878
|
+
1. Create a new table with the correct sorting key
|
|
1879
|
+
2. Copy data from the old table (deduplicating if needed)
|
|
1880
|
+
3. Replace the old table with the new one
|
|
1881
|
+
|
|
1882
|
+
WARNING: This migration involves table recreation and may take significant
|
|
1883
|
+
time for large tables. Please ensure you have a backup before proceeding.
|
|
1884
|
+
===========================================================================
|
|
1885
|
+
`;
|
|
1886
|
+
throw new MastraError({
|
|
1887
|
+
id: createStorageErrorId("CLICKHOUSE", "MIGRATION_REQUIRED", "SORTING_KEY_CHANGE"),
|
|
1888
|
+
domain: ErrorDomain.STORAGE,
|
|
1889
|
+
category: ErrorCategory.USER,
|
|
1890
|
+
text: errorMessage
|
|
1891
|
+
});
|
|
1892
|
+
}
|
|
1593
1893
|
await this.#db.createTable({ tableName: TABLE_SPANS, schema: SPAN_SCHEMA });
|
|
1594
1894
|
}
|
|
1595
1895
|
async dangerouslyClearAll() {
|
|
1596
1896
|
await this.#db.clearTable({ tableName: TABLE_SPANS });
|
|
1597
1897
|
}
|
|
1898
|
+
/**
|
|
1899
|
+
* Manually run the spans migration to deduplicate and update the sorting key.
|
|
1900
|
+
* This is intended to be called from the CLI when duplicates are detected.
|
|
1901
|
+
*
|
|
1902
|
+
* @returns Migration result with status and details
|
|
1903
|
+
*/
|
|
1904
|
+
async migrateSpans() {
|
|
1905
|
+
const migrationStatus = await this.#db.checkSpansMigrationStatus(TABLE_SPANS);
|
|
1906
|
+
if (!migrationStatus.needsMigration) {
|
|
1907
|
+
return {
|
|
1908
|
+
success: true,
|
|
1909
|
+
alreadyMigrated: true,
|
|
1910
|
+
duplicatesRemoved: 0,
|
|
1911
|
+
message: `Migration already complete. Spans table has correct sorting key.`
|
|
1912
|
+
};
|
|
1913
|
+
}
|
|
1914
|
+
const duplicateInfo = await this.#db.checkForDuplicateSpans(TABLE_SPANS);
|
|
1915
|
+
if (duplicateInfo.hasDuplicates) {
|
|
1916
|
+
this.logger?.info?.(
|
|
1917
|
+
`Found ${duplicateInfo.duplicateCount} duplicate (traceId, spanId) combinations. Starting migration with deduplication...`
|
|
1918
|
+
);
|
|
1919
|
+
} else {
|
|
1920
|
+
this.logger?.info?.(`No duplicate spans found. Starting sorting key migration...`);
|
|
1921
|
+
}
|
|
1922
|
+
await this.#db.migrateSpansTableSortingKey({ tableName: TABLE_SPANS, schema: SPAN_SCHEMA });
|
|
1923
|
+
return {
|
|
1924
|
+
success: true,
|
|
1925
|
+
alreadyMigrated: false,
|
|
1926
|
+
duplicatesRemoved: duplicateInfo.duplicateCount,
|
|
1927
|
+
message: duplicateInfo.hasDuplicates ? `Migration complete. Removed duplicates and updated sorting key for ${TABLE_SPANS}.` : `Migration complete. Updated sorting key for ${TABLE_SPANS}.`
|
|
1928
|
+
};
|
|
1929
|
+
}
|
|
1930
|
+
/**
|
|
1931
|
+
* Check migration status for the spans table.
|
|
1932
|
+
* Returns information about whether migration is needed.
|
|
1933
|
+
*/
|
|
1934
|
+
async checkSpansMigrationStatus() {
|
|
1935
|
+
const migrationStatus = await this.#db.checkSpansMigrationStatus(TABLE_SPANS);
|
|
1936
|
+
if (!migrationStatus.needsMigration) {
|
|
1937
|
+
return {
|
|
1938
|
+
needsMigration: false,
|
|
1939
|
+
hasDuplicates: false,
|
|
1940
|
+
duplicateCount: 0,
|
|
1941
|
+
constraintExists: true,
|
|
1942
|
+
tableName: TABLE_SPANS
|
|
1943
|
+
};
|
|
1944
|
+
}
|
|
1945
|
+
const duplicateInfo = await this.#db.checkForDuplicateSpans(TABLE_SPANS);
|
|
1946
|
+
return {
|
|
1947
|
+
needsMigration: true,
|
|
1948
|
+
hasDuplicates: duplicateInfo.hasDuplicates,
|
|
1949
|
+
duplicateCount: duplicateInfo.duplicateCount,
|
|
1950
|
+
constraintExists: false,
|
|
1951
|
+
tableName: TABLE_SPANS
|
|
1952
|
+
};
|
|
1953
|
+
}
|
|
1598
1954
|
get tracingStrategy() {
|
|
1599
1955
|
return {
|
|
1600
1956
|
preferred: "insert-only",
|
|
@@ -1928,10 +2284,10 @@ var ObservabilityStorageClickhouse = class extends ObservabilityStorage {
|
|
|
1928
2284
|
conditions.push(`(error IS NOT NULL AND error != '')`);
|
|
1929
2285
|
break;
|
|
1930
2286
|
case TraceStatus.RUNNING:
|
|
1931
|
-
conditions.push(`
|
|
2287
|
+
conditions.push(`endedAt IS NULL AND (error IS NULL OR error = '')`);
|
|
1932
2288
|
break;
|
|
1933
2289
|
case TraceStatus.SUCCESS:
|
|
1934
|
-
conditions.push(`
|
|
2290
|
+
conditions.push(`endedAt IS NOT NULL AND (error IS NULL OR error = '')`);
|
|
1935
2291
|
break;
|
|
1936
2292
|
}
|
|
1937
2293
|
}
|
|
@@ -1960,7 +2316,7 @@ var ObservabilityStorageClickhouse = class extends ObservabilityStorage {
|
|
|
1960
2316
|
if (sortField === "endedAt") {
|
|
1961
2317
|
const nullSortValue = sortDirection === "DESC" ? 0 : 1;
|
|
1962
2318
|
const nonNullSortValue = sortDirection === "DESC" ? 1 : 0;
|
|
1963
|
-
orderClause = `ORDER BY CASE WHEN ${sortField} IS NULL
|
|
2319
|
+
orderClause = `ORDER BY CASE WHEN ${sortField} IS NULL THEN ${nullSortValue} ELSE ${nonNullSortValue} END, ${sortField} ${sortDirection}`;
|
|
1964
2320
|
} else {
|
|
1965
2321
|
orderClause = `ORDER BY ${sortField} ${sortDirection}`;
|
|
1966
2322
|
}
|
|
@@ -2688,7 +3044,7 @@ var WorkflowsStorageClickhouse = class extends WorkflowsStorage {
|
|
|
2688
3044
|
try {
|
|
2689
3045
|
parsedSnapshot = JSON.parse(row.snapshot);
|
|
2690
3046
|
} catch (e) {
|
|
2691
|
-
|
|
3047
|
+
this.logger.warn(`Failed to parse snapshot for workflow ${row.workflow_name}: ${e}`);
|
|
2692
3048
|
}
|
|
2693
3049
|
}
|
|
2694
3050
|
return {
|
|
@@ -2726,7 +3082,7 @@ var WorkflowsStorageClickhouse = class extends WorkflowsStorage {
|
|
|
2726
3082
|
conditions.push(`resourceId = {var_resourceId:String}`);
|
|
2727
3083
|
values.var_resourceId = resourceId;
|
|
2728
3084
|
} else {
|
|
2729
|
-
|
|
3085
|
+
this.logger.warn(`[${TABLE_WORKFLOW_SNAPSHOT}] resourceId column not found. Skipping resourceId filter.`);
|
|
2730
3086
|
}
|
|
2731
3087
|
}
|
|
2732
3088
|
if (fromDate) {
|
|
@@ -2866,7 +3222,7 @@ var WorkflowsStorageClickhouse = class extends WorkflowsStorage {
|
|
|
2866
3222
|
var isClientConfig = (config) => {
|
|
2867
3223
|
return "client" in config;
|
|
2868
3224
|
};
|
|
2869
|
-
var ClickhouseStore = class extends
|
|
3225
|
+
var ClickhouseStore = class extends MastraCompositeStore {
|
|
2870
3226
|
db;
|
|
2871
3227
|
ttl = {};
|
|
2872
3228
|
stores;
|
|
@@ -2884,11 +3240,11 @@ var ClickhouseStore = class extends MastraStorage {
|
|
|
2884
3240
|
if (typeof config.password !== "string") {
|
|
2885
3241
|
throw new Error("ClickhouseStore: password must be a string.");
|
|
2886
3242
|
}
|
|
3243
|
+
const { id, ttl, disableInit, clickhouse_settings, ...clientOptions } = config;
|
|
2887
3244
|
this.db = createClient({
|
|
2888
|
-
|
|
2889
|
-
username: config.username,
|
|
2890
|
-
password: config.password,
|
|
3245
|
+
...clientOptions,
|
|
2891
3246
|
clickhouse_settings: {
|
|
3247
|
+
...clickhouse_settings,
|
|
2892
3248
|
date_time_input_format: "best_effort",
|
|
2893
3249
|
date_time_output_format: "iso",
|
|
2894
3250
|
// This is crucial
|