@mastra/pg 1.0.0-beta.14 → 1.0.0-beta.15
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 +58 -0
- package/dist/docs/README.md +1 -1
- package/dist/docs/SKILL.md +1 -1
- package/dist/docs/SOURCE_MAP.json +1 -1
- package/dist/docs/memory/01-storage.md +3 -3
- package/dist/docs/storage/01-reference.md +15 -15
- package/dist/index.cjs +414 -23
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +416 -25
- package/dist/index.js.map +1 -1
- package/dist/storage/db/index.d.ts +46 -0
- package/dist/storage/db/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 +2 -2
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/vector/index.d.ts.map +1 -1
- package/dist/vector/sql-builder.d.ts.map +1 -1
- package/package.json +3 -3
package/dist/index.cjs
CHANGED
|
@@ -285,8 +285,14 @@ var FILTER_OPERATORS = {
|
|
|
285
285
|
};
|
|
286
286
|
},
|
|
287
287
|
// Element Operators
|
|
288
|
-
$exists: (key) => {
|
|
288
|
+
$exists: (key, paramIndex, value) => {
|
|
289
289
|
const jsonPathKey = parseJsonPathKey(key);
|
|
290
|
+
if (value === false) {
|
|
291
|
+
return {
|
|
292
|
+
sql: `NOT (metadata ? '${jsonPathKey}')`,
|
|
293
|
+
needsValue: false
|
|
294
|
+
};
|
|
295
|
+
}
|
|
290
296
|
return {
|
|
291
297
|
sql: `metadata ? '${jsonPathKey}'`,
|
|
292
298
|
needsValue: false
|
|
@@ -364,17 +370,62 @@ function buildDeleteFilterQuery(filter) {
|
|
|
364
370
|
values.push(value);
|
|
365
371
|
return `metadata#>>'{${parseJsonPathKey(key)}}' = $${values.length}`;
|
|
366
372
|
}
|
|
367
|
-
const
|
|
373
|
+
const entries = Object.entries(value);
|
|
374
|
+
if (entries.length > 1) {
|
|
375
|
+
const conditions2 = entries.map(([operator2, operatorValue2]) => {
|
|
376
|
+
if (operator2 === "$not") {
|
|
377
|
+
const nestedEntries = Object.entries(operatorValue2);
|
|
378
|
+
const nestedConditions = nestedEntries.map(([nestedOp, nestedValue]) => {
|
|
379
|
+
if (!FILTER_OPERATORS[nestedOp]) {
|
|
380
|
+
throw new Error(`Invalid operator in $not condition: ${nestedOp}`);
|
|
381
|
+
}
|
|
382
|
+
const operatorFn3 = FILTER_OPERATORS[nestedOp];
|
|
383
|
+
const operatorResult3 = operatorFn3(key, values.length + 1, nestedValue);
|
|
384
|
+
if (operatorResult3.needsValue) {
|
|
385
|
+
const transformedValue = operatorResult3.transformValue ? operatorResult3.transformValue() : nestedValue;
|
|
386
|
+
if (Array.isArray(transformedValue) && nestedOp === "$elemMatch") {
|
|
387
|
+
values.push(...transformedValue);
|
|
388
|
+
} else {
|
|
389
|
+
values.push(transformedValue);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
return operatorResult3.sql;
|
|
393
|
+
}).join(" AND ");
|
|
394
|
+
return `NOT (${nestedConditions})`;
|
|
395
|
+
}
|
|
396
|
+
if (!FILTER_OPERATORS[operator2]) {
|
|
397
|
+
throw new Error(`Invalid operator: ${operator2}`);
|
|
398
|
+
}
|
|
399
|
+
const operatorFn2 = FILTER_OPERATORS[operator2];
|
|
400
|
+
const operatorResult2 = operatorFn2(key, values.length + 1, operatorValue2);
|
|
401
|
+
if (operatorResult2.needsValue) {
|
|
402
|
+
const transformedValue = operatorResult2.transformValue ? operatorResult2.transformValue() : operatorValue2;
|
|
403
|
+
if (Array.isArray(transformedValue) && operator2 === "$elemMatch") {
|
|
404
|
+
values.push(...transformedValue);
|
|
405
|
+
} else {
|
|
406
|
+
values.push(transformedValue);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
return operatorResult2.sql;
|
|
410
|
+
});
|
|
411
|
+
return conditions2.join(" AND ");
|
|
412
|
+
}
|
|
413
|
+
const [[operator, operatorValue] = []] = entries;
|
|
368
414
|
if (operator === "$not") {
|
|
369
|
-
const
|
|
370
|
-
const conditions2 =
|
|
415
|
+
const nestedEntries = Object.entries(operatorValue);
|
|
416
|
+
const conditions2 = nestedEntries.map(([nestedOp, nestedValue]) => {
|
|
371
417
|
if (!FILTER_OPERATORS[nestedOp]) {
|
|
372
418
|
throw new Error(`Invalid operator in $not condition: ${nestedOp}`);
|
|
373
419
|
}
|
|
374
420
|
const operatorFn2 = FILTER_OPERATORS[nestedOp];
|
|
375
421
|
const operatorResult2 = operatorFn2(key, values.length + 1, nestedValue);
|
|
376
422
|
if (operatorResult2.needsValue) {
|
|
377
|
-
|
|
423
|
+
const transformedValue = operatorResult2.transformValue ? operatorResult2.transformValue() : nestedValue;
|
|
424
|
+
if (Array.isArray(transformedValue) && nestedOp === "$elemMatch") {
|
|
425
|
+
values.push(...transformedValue);
|
|
426
|
+
} else {
|
|
427
|
+
values.push(transformedValue);
|
|
428
|
+
}
|
|
378
429
|
}
|
|
379
430
|
return operatorResult2.sql;
|
|
380
431
|
}).join(" AND ");
|
|
@@ -441,17 +492,62 @@ function buildFilterQuery(filter, minScore, topK) {
|
|
|
441
492
|
values.push(value);
|
|
442
493
|
return `metadata#>>'{${parseJsonPathKey(key)}}' = $${values.length}`;
|
|
443
494
|
}
|
|
444
|
-
const
|
|
495
|
+
const entries = Object.entries(value);
|
|
496
|
+
if (entries.length > 1) {
|
|
497
|
+
const conditions2 = entries.map(([operator2, operatorValue2]) => {
|
|
498
|
+
if (operator2 === "$not") {
|
|
499
|
+
const nestedEntries = Object.entries(operatorValue2);
|
|
500
|
+
const nestedConditions = nestedEntries.map(([nestedOp, nestedValue]) => {
|
|
501
|
+
if (!FILTER_OPERATORS[nestedOp]) {
|
|
502
|
+
throw new Error(`Invalid operator in $not condition: ${nestedOp}`);
|
|
503
|
+
}
|
|
504
|
+
const operatorFn3 = FILTER_OPERATORS[nestedOp];
|
|
505
|
+
const operatorResult3 = operatorFn3(key, values.length + 1, nestedValue);
|
|
506
|
+
if (operatorResult3.needsValue) {
|
|
507
|
+
const transformedValue = operatorResult3.transformValue ? operatorResult3.transformValue() : nestedValue;
|
|
508
|
+
if (Array.isArray(transformedValue) && nestedOp === "$elemMatch") {
|
|
509
|
+
values.push(...transformedValue);
|
|
510
|
+
} else {
|
|
511
|
+
values.push(transformedValue);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
return operatorResult3.sql;
|
|
515
|
+
}).join(" AND ");
|
|
516
|
+
return `NOT (${nestedConditions})`;
|
|
517
|
+
}
|
|
518
|
+
if (!FILTER_OPERATORS[operator2]) {
|
|
519
|
+
throw new Error(`Invalid operator: ${operator2}`);
|
|
520
|
+
}
|
|
521
|
+
const operatorFn2 = FILTER_OPERATORS[operator2];
|
|
522
|
+
const operatorResult2 = operatorFn2(key, values.length + 1, operatorValue2);
|
|
523
|
+
if (operatorResult2.needsValue) {
|
|
524
|
+
const transformedValue = operatorResult2.transformValue ? operatorResult2.transformValue() : operatorValue2;
|
|
525
|
+
if (Array.isArray(transformedValue) && operator2 === "$elemMatch") {
|
|
526
|
+
values.push(...transformedValue);
|
|
527
|
+
} else {
|
|
528
|
+
values.push(transformedValue);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
return operatorResult2.sql;
|
|
532
|
+
});
|
|
533
|
+
return conditions2.join(" AND ");
|
|
534
|
+
}
|
|
535
|
+
const [[operator, operatorValue] = []] = entries;
|
|
445
536
|
if (operator === "$not") {
|
|
446
|
-
const
|
|
447
|
-
const conditions2 =
|
|
537
|
+
const nestedEntries = Object.entries(operatorValue);
|
|
538
|
+
const conditions2 = nestedEntries.map(([nestedOp, nestedValue]) => {
|
|
448
539
|
if (!FILTER_OPERATORS[nestedOp]) {
|
|
449
540
|
throw new Error(`Invalid operator in $not condition: ${nestedOp}`);
|
|
450
541
|
}
|
|
451
542
|
const operatorFn2 = FILTER_OPERATORS[nestedOp];
|
|
452
543
|
const operatorResult2 = operatorFn2(key, values.length + 1, nestedValue);
|
|
453
544
|
if (operatorResult2.needsValue) {
|
|
454
|
-
|
|
545
|
+
const transformedValue = operatorResult2.transformValue ? operatorResult2.transformValue() : nestedValue;
|
|
546
|
+
if (Array.isArray(transformedValue) && nestedOp === "$elemMatch") {
|
|
547
|
+
values.push(...transformedValue);
|
|
548
|
+
} else {
|
|
549
|
+
values.push(transformedValue);
|
|
550
|
+
}
|
|
455
551
|
}
|
|
456
552
|
return operatorResult2.sql;
|
|
457
553
|
}).join(" AND ");
|
|
@@ -711,9 +807,7 @@ var PgVector = class extends vector.MastraVector {
|
|
|
711
807
|
probes
|
|
712
808
|
}) {
|
|
713
809
|
try {
|
|
714
|
-
|
|
715
|
-
throw new Error("topK must be a positive integer");
|
|
716
|
-
}
|
|
810
|
+
vector.validateTopK("PG", topK);
|
|
717
811
|
if (!Array.isArray(queryVector) || !queryVector.every((x) => typeof x === "number" && Number.isFinite(x))) {
|
|
718
812
|
throw new Error("queryVector must be an array of finite numbers");
|
|
719
813
|
}
|
|
@@ -798,6 +892,7 @@ var PgVector = class extends vector.MastraVector {
|
|
|
798
892
|
ids,
|
|
799
893
|
deleteFilter
|
|
800
894
|
}) {
|
|
895
|
+
vector.validateUpsertInput("PG", vectors, metadata, ids);
|
|
801
896
|
const { tableName } = this.getTableName(indexName);
|
|
802
897
|
const client = await this.pool.connect();
|
|
803
898
|
try {
|
|
@@ -1873,7 +1968,8 @@ function mapToSqlType(type) {
|
|
|
1873
1968
|
function generateTableSQL({
|
|
1874
1969
|
tableName,
|
|
1875
1970
|
schema,
|
|
1876
|
-
schemaName
|
|
1971
|
+
schemaName,
|
|
1972
|
+
includeAllConstraints = false
|
|
1877
1973
|
}) {
|
|
1878
1974
|
const timeZColumns = Object.entries(schema).filter(([_, def]) => def.type === "timestamp").map(([name]) => {
|
|
1879
1975
|
const parsedName = utils.parseSqlIdentifier(name, "column name");
|
|
@@ -1897,9 +1993,9 @@ function generateTableSQL({
|
|
|
1897
1993
|
${tableName === storage.TABLE_WORKFLOW_SNAPSHOT ? `
|
|
1898
1994
|
DO $$ BEGIN
|
|
1899
1995
|
IF NOT EXISTS (
|
|
1900
|
-
SELECT 1 FROM pg_constraint WHERE conname = '${constraintPrefix}mastra_workflow_snapshot_workflow_name_run_id_key'
|
|
1996
|
+
SELECT 1 FROM pg_constraint WHERE conname = lower('${constraintPrefix}mastra_workflow_snapshot_workflow_name_run_id_key')
|
|
1901
1997
|
) AND NOT EXISTS (
|
|
1902
|
-
SELECT 1 FROM pg_indexes WHERE indexname = '${constraintPrefix}mastra_workflow_snapshot_workflow_name_run_id_key'
|
|
1998
|
+
SELECT 1 FROM pg_indexes WHERE indexname = lower('${constraintPrefix}mastra_workflow_snapshot_workflow_name_run_id_key')
|
|
1903
1999
|
) THEN
|
|
1904
2000
|
ALTER TABLE ${getTableName({ indexName: tableName, schemaName: quotedSchemaName })}
|
|
1905
2001
|
ADD CONSTRAINT ${constraintPrefix}mastra_workflow_snapshot_workflow_name_run_id_key
|
|
@@ -1907,10 +2003,11 @@ function generateTableSQL({
|
|
|
1907
2003
|
END IF;
|
|
1908
2004
|
END $$;
|
|
1909
2005
|
` : ""}
|
|
1910
|
-
${
|
|
2006
|
+
${// For spans table: Include PRIMARY KEY in exports, but not in runtime (handled after deduplication)
|
|
2007
|
+
tableName === storage.TABLE_SPANS && includeAllConstraints ? `
|
|
1911
2008
|
DO $$ BEGIN
|
|
1912
2009
|
IF NOT EXISTS (
|
|
1913
|
-
SELECT 1 FROM pg_constraint WHERE conname = '${constraintPrefix}mastra_ai_spans_traceid_spanid_pk'
|
|
2010
|
+
SELECT 1 FROM pg_constraint WHERE conname = lower('${constraintPrefix}mastra_ai_spans_traceid_spanid_pk')
|
|
1914
2011
|
) THEN
|
|
1915
2012
|
ALTER TABLE ${getTableName({ indexName: tableName, schemaName: quotedSchemaName })}
|
|
1916
2013
|
ADD CONSTRAINT ${constraintPrefix}mastra_ai_spans_traceid_spanid_pk
|
|
@@ -1934,7 +2031,9 @@ function exportSchemas(schemaName) {
|
|
|
1934
2031
|
const sql = generateTableSQL({
|
|
1935
2032
|
tableName,
|
|
1936
2033
|
schema,
|
|
1937
|
-
schemaName
|
|
2034
|
+
schemaName,
|
|
2035
|
+
includeAllConstraints: true
|
|
2036
|
+
// Include all constraints for exports/documentation
|
|
1938
2037
|
});
|
|
1939
2038
|
statements.push(sql.trim());
|
|
1940
2039
|
statements.push("");
|
|
@@ -2075,10 +2174,27 @@ var PgDB = class extends base.MastraBase {
|
|
|
2075
2174
|
const columns = Object.keys(record).map((col) => utils.parseSqlIdentifier(col, "column name"));
|
|
2076
2175
|
const values = this.prepareValuesForInsert(record, tableName);
|
|
2077
2176
|
const placeholders = values.map((_, i) => `$${i + 1}`).join(", ");
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2177
|
+
const fullTableName = getTableName({ indexName: tableName, schemaName });
|
|
2178
|
+
const columnList = columns.map((c) => `"${c}"`).join(", ");
|
|
2179
|
+
if (tableName === storage.TABLE_SPANS) {
|
|
2180
|
+
const updateColumns = columns.filter((c) => c !== "traceId" && c !== "spanId");
|
|
2181
|
+
if (updateColumns.length > 0) {
|
|
2182
|
+
const updateClause = updateColumns.map((c) => `"${c}" = EXCLUDED."${c}"`).join(", ");
|
|
2183
|
+
await this.client.none(
|
|
2184
|
+
`INSERT INTO ${fullTableName} (${columnList}) VALUES (${placeholders})
|
|
2185
|
+
ON CONFLICT ("traceId", "spanId") DO UPDATE SET ${updateClause}`,
|
|
2186
|
+
values
|
|
2187
|
+
);
|
|
2188
|
+
} else {
|
|
2189
|
+
await this.client.none(
|
|
2190
|
+
`INSERT INTO ${fullTableName} (${columnList}) VALUES (${placeholders})
|
|
2191
|
+
ON CONFLICT ("traceId", "spanId") DO NOTHING`,
|
|
2192
|
+
values
|
|
2193
|
+
);
|
|
2194
|
+
}
|
|
2195
|
+
} else {
|
|
2196
|
+
await this.client.none(`INSERT INTO ${fullTableName} (${columnList}) VALUES (${placeholders})`, values);
|
|
2197
|
+
}
|
|
2082
2198
|
} catch (error$1) {
|
|
2083
2199
|
throw new error.MastraError(
|
|
2084
2200
|
{
|
|
@@ -2140,8 +2256,46 @@ var PgDB = class extends base.MastraBase {
|
|
|
2140
2256
|
if (tableName === storage.TABLE_SPANS) {
|
|
2141
2257
|
await this.setupTimestampTriggers(tableName);
|
|
2142
2258
|
await this.migrateSpansTable();
|
|
2259
|
+
const pkExists = await this.spansPrimaryKeyExists();
|
|
2260
|
+
if (!pkExists) {
|
|
2261
|
+
const duplicateInfo = await this.checkForDuplicateSpans();
|
|
2262
|
+
if (duplicateInfo.hasDuplicates) {
|
|
2263
|
+
const errorMessage = `
|
|
2264
|
+
===========================================================================
|
|
2265
|
+
MIGRATION REQUIRED: Duplicate spans detected in ${duplicateInfo.tableName}
|
|
2266
|
+
===========================================================================
|
|
2267
|
+
|
|
2268
|
+
Found ${duplicateInfo.duplicateCount} duplicate (traceId, spanId) combinations.
|
|
2269
|
+
|
|
2270
|
+
The spans table requires a unique constraint on (traceId, spanId), but your
|
|
2271
|
+
database contains duplicate entries that must be resolved first.
|
|
2272
|
+
|
|
2273
|
+
To fix this, run the manual migration command:
|
|
2274
|
+
|
|
2275
|
+
npx mastra migrate
|
|
2276
|
+
|
|
2277
|
+
This command will:
|
|
2278
|
+
1. Remove duplicate spans (keeping the most complete/recent version)
|
|
2279
|
+
2. Add the required unique constraint
|
|
2280
|
+
|
|
2281
|
+
Note: This migration may take some time for large tables.
|
|
2282
|
+
===========================================================================
|
|
2283
|
+
`;
|
|
2284
|
+
throw new error.MastraError({
|
|
2285
|
+
id: storage.createStorageErrorId("PG", "MIGRATION_REQUIRED", "DUPLICATE_SPANS"),
|
|
2286
|
+
domain: error.ErrorDomain.STORAGE,
|
|
2287
|
+
category: error.ErrorCategory.USER,
|
|
2288
|
+
text: errorMessage
|
|
2289
|
+
});
|
|
2290
|
+
} else {
|
|
2291
|
+
await this.addSpansPrimaryKey();
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2143
2294
|
}
|
|
2144
2295
|
} catch (error$1) {
|
|
2296
|
+
if (error$1 instanceof error.MastraError) {
|
|
2297
|
+
throw error$1;
|
|
2298
|
+
}
|
|
2145
2299
|
throw new error.MastraError(
|
|
2146
2300
|
{
|
|
2147
2301
|
id: storage.createStorageErrorId("PG", "CREATE_TABLE", "FAILED"),
|
|
@@ -2234,6 +2388,224 @@ var PgDB = class extends base.MastraBase {
|
|
|
2234
2388
|
this.logger?.warn?.(`Failed to migrate spans table ${fullTableName}:`, error);
|
|
2235
2389
|
}
|
|
2236
2390
|
}
|
|
2391
|
+
/**
|
|
2392
|
+
* Deduplicates spans in the mastra_ai_spans table before adding the PRIMARY KEY constraint.
|
|
2393
|
+
* Keeps spans based on priority: completed (endedAt NOT NULL) > most recent updatedAt > most recent createdAt.
|
|
2394
|
+
*
|
|
2395
|
+
* Note: This prioritizes migration completion over perfect data preservation.
|
|
2396
|
+
* Old trace data may be lost, which is acceptable for this use case.
|
|
2397
|
+
*/
|
|
2398
|
+
async deduplicateSpans() {
|
|
2399
|
+
const fullTableName = getTableName({ indexName: storage.TABLE_SPANS, schemaName: getSchemaName(this.schemaName) });
|
|
2400
|
+
try {
|
|
2401
|
+
const duplicateCheck = await this.client.oneOrNone(`
|
|
2402
|
+
SELECT EXISTS (
|
|
2403
|
+
SELECT 1
|
|
2404
|
+
FROM ${fullTableName}
|
|
2405
|
+
GROUP BY "traceId", "spanId"
|
|
2406
|
+
HAVING COUNT(*) > 1
|
|
2407
|
+
LIMIT 1
|
|
2408
|
+
) as has_duplicates
|
|
2409
|
+
`);
|
|
2410
|
+
if (!duplicateCheck?.has_duplicates) {
|
|
2411
|
+
this.logger?.debug?.(`No duplicate spans found in ${fullTableName}`);
|
|
2412
|
+
return;
|
|
2413
|
+
}
|
|
2414
|
+
this.logger?.info?.(`Duplicate spans detected in ${fullTableName}, starting deduplication...`);
|
|
2415
|
+
const result = await this.client.query(`
|
|
2416
|
+
DELETE FROM ${fullTableName} t1
|
|
2417
|
+
USING ${fullTableName} t2
|
|
2418
|
+
WHERE t1."traceId" = t2."traceId"
|
|
2419
|
+
AND t1."spanId" = t2."spanId"
|
|
2420
|
+
AND (
|
|
2421
|
+
-- Keep completed spans over incomplete
|
|
2422
|
+
(t1."endedAt" IS NULL AND t2."endedAt" IS NOT NULL)
|
|
2423
|
+
OR
|
|
2424
|
+
-- If both have same completion status, keep more recent updatedAt
|
|
2425
|
+
(
|
|
2426
|
+
(t1."endedAt" IS NULL) = (t2."endedAt" IS NULL)
|
|
2427
|
+
AND (
|
|
2428
|
+
(t1."updatedAt" < t2."updatedAt")
|
|
2429
|
+
OR (t1."updatedAt" IS NULL AND t2."updatedAt" IS NOT NULL)
|
|
2430
|
+
OR
|
|
2431
|
+
-- If updatedAt is the same, keep more recent createdAt
|
|
2432
|
+
(
|
|
2433
|
+
(t1."updatedAt" = t2."updatedAt" OR (t1."updatedAt" IS NULL AND t2."updatedAt" IS NULL))
|
|
2434
|
+
AND (
|
|
2435
|
+
(t1."createdAt" < t2."createdAt")
|
|
2436
|
+
OR (t1."createdAt" IS NULL AND t2."createdAt" IS NOT NULL)
|
|
2437
|
+
OR
|
|
2438
|
+
-- If all else equal, use ctid as tiebreaker
|
|
2439
|
+
(
|
|
2440
|
+
(t1."createdAt" = t2."createdAt" OR (t1."createdAt" IS NULL AND t2."createdAt" IS NULL))
|
|
2441
|
+
AND t1.ctid < t2.ctid
|
|
2442
|
+
)
|
|
2443
|
+
)
|
|
2444
|
+
)
|
|
2445
|
+
)
|
|
2446
|
+
)
|
|
2447
|
+
)
|
|
2448
|
+
`);
|
|
2449
|
+
this.logger?.info?.(
|
|
2450
|
+
`Deduplication complete: removed ${result.rowCount ?? 0} duplicate spans from ${fullTableName}`
|
|
2451
|
+
);
|
|
2452
|
+
} catch (error$1) {
|
|
2453
|
+
throw new error.MastraError(
|
|
2454
|
+
{
|
|
2455
|
+
id: storage.createStorageErrorId("PG", "DEDUPLICATE_SPANS", "FAILED"),
|
|
2456
|
+
domain: error.ErrorDomain.STORAGE,
|
|
2457
|
+
category: error.ErrorCategory.THIRD_PARTY,
|
|
2458
|
+
details: {
|
|
2459
|
+
tableName: storage.TABLE_SPANS
|
|
2460
|
+
}
|
|
2461
|
+
},
|
|
2462
|
+
error$1
|
|
2463
|
+
);
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
/**
|
|
2467
|
+
* Checks for duplicate (traceId, spanId) combinations in the spans table.
|
|
2468
|
+
* Returns information about duplicates for logging/CLI purposes.
|
|
2469
|
+
*/
|
|
2470
|
+
async checkForDuplicateSpans() {
|
|
2471
|
+
const fullTableName = getTableName({ indexName: storage.TABLE_SPANS, schemaName: getSchemaName(this.schemaName) });
|
|
2472
|
+
try {
|
|
2473
|
+
const result = await this.client.oneOrNone(`
|
|
2474
|
+
SELECT COUNT(*) as duplicate_count
|
|
2475
|
+
FROM (
|
|
2476
|
+
SELECT "traceId", "spanId"
|
|
2477
|
+
FROM ${fullTableName}
|
|
2478
|
+
GROUP BY "traceId", "spanId"
|
|
2479
|
+
HAVING COUNT(*) > 1
|
|
2480
|
+
) duplicates
|
|
2481
|
+
`);
|
|
2482
|
+
const duplicateCount = parseInt(result?.duplicate_count ?? "0", 10);
|
|
2483
|
+
return {
|
|
2484
|
+
hasDuplicates: duplicateCount > 0,
|
|
2485
|
+
duplicateCount,
|
|
2486
|
+
tableName: fullTableName
|
|
2487
|
+
};
|
|
2488
|
+
} catch (error) {
|
|
2489
|
+
this.logger?.debug?.(`Could not check for duplicates: ${error}`);
|
|
2490
|
+
return { hasDuplicates: false, duplicateCount: 0, tableName: fullTableName };
|
|
2491
|
+
}
|
|
2492
|
+
}
|
|
2493
|
+
/**
|
|
2494
|
+
* Checks if the PRIMARY KEY constraint on (traceId, spanId) already exists on the spans table.
|
|
2495
|
+
* Used to skip deduplication when the constraint already exists (migration already complete).
|
|
2496
|
+
*/
|
|
2497
|
+
async spansPrimaryKeyExists() {
|
|
2498
|
+
const parsedSchemaName = this.schemaName ? utils.parseSqlIdentifier(this.schemaName, "schema name") : "";
|
|
2499
|
+
const constraintPrefix = parsedSchemaName ? `${parsedSchemaName}_` : "";
|
|
2500
|
+
const constraintName = `${constraintPrefix}mastra_ai_spans_traceid_spanid_pk`;
|
|
2501
|
+
const result = await this.client.oneOrNone(
|
|
2502
|
+
`SELECT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = $1) as exists`,
|
|
2503
|
+
[constraintName]
|
|
2504
|
+
);
|
|
2505
|
+
return result?.exists ?? false;
|
|
2506
|
+
}
|
|
2507
|
+
/**
|
|
2508
|
+
* Adds the PRIMARY KEY constraint on (traceId, spanId) to the spans table.
|
|
2509
|
+
* Should be called AFTER deduplication to ensure no duplicate key violations.
|
|
2510
|
+
*/
|
|
2511
|
+
async addSpansPrimaryKey() {
|
|
2512
|
+
const fullTableName = getTableName({ indexName: storage.TABLE_SPANS, schemaName: getSchemaName(this.schemaName) });
|
|
2513
|
+
const parsedSchemaName = this.schemaName ? utils.parseSqlIdentifier(this.schemaName, "schema name") : "";
|
|
2514
|
+
const constraintPrefix = parsedSchemaName ? `${parsedSchemaName}_` : "";
|
|
2515
|
+
const constraintName = `${constraintPrefix}mastra_ai_spans_traceid_spanid_pk`;
|
|
2516
|
+
try {
|
|
2517
|
+
const constraintExists = await this.client.oneOrNone(
|
|
2518
|
+
`
|
|
2519
|
+
SELECT EXISTS (
|
|
2520
|
+
SELECT 1 FROM pg_constraint WHERE conname = $1
|
|
2521
|
+
) as exists
|
|
2522
|
+
`,
|
|
2523
|
+
[constraintName]
|
|
2524
|
+
);
|
|
2525
|
+
if (constraintExists?.exists) {
|
|
2526
|
+
this.logger?.debug?.(`PRIMARY KEY constraint ${constraintName} already exists on ${fullTableName}`);
|
|
2527
|
+
return;
|
|
2528
|
+
}
|
|
2529
|
+
await this.client.none(`
|
|
2530
|
+
ALTER TABLE ${fullTableName}
|
|
2531
|
+
ADD CONSTRAINT ${constraintName}
|
|
2532
|
+
PRIMARY KEY ("traceId", "spanId")
|
|
2533
|
+
`);
|
|
2534
|
+
this.logger?.info?.(`Added PRIMARY KEY constraint ${constraintName} to ${fullTableName}`);
|
|
2535
|
+
} catch (error$1) {
|
|
2536
|
+
throw new error.MastraError(
|
|
2537
|
+
{
|
|
2538
|
+
id: storage.createStorageErrorId("PG", "ADD_SPANS_PRIMARY_KEY", "FAILED"),
|
|
2539
|
+
domain: error.ErrorDomain.STORAGE,
|
|
2540
|
+
category: error.ErrorCategory.THIRD_PARTY,
|
|
2541
|
+
details: {
|
|
2542
|
+
tableName: storage.TABLE_SPANS,
|
|
2543
|
+
constraintName
|
|
2544
|
+
}
|
|
2545
|
+
},
|
|
2546
|
+
error$1
|
|
2547
|
+
);
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2550
|
+
/**
|
|
2551
|
+
* Manually run the spans migration to deduplicate and add the unique constraint.
|
|
2552
|
+
* This is intended to be called from the CLI when duplicates are detected.
|
|
2553
|
+
*
|
|
2554
|
+
* @returns Migration result with status and details
|
|
2555
|
+
*/
|
|
2556
|
+
async migrateSpans() {
|
|
2557
|
+
const fullTableName = getTableName({ indexName: storage.TABLE_SPANS, schemaName: getSchemaName(this.schemaName) });
|
|
2558
|
+
const pkExists = await this.spansPrimaryKeyExists();
|
|
2559
|
+
if (pkExists) {
|
|
2560
|
+
return {
|
|
2561
|
+
success: true,
|
|
2562
|
+
alreadyMigrated: true,
|
|
2563
|
+
duplicatesRemoved: 0,
|
|
2564
|
+
message: `Migration already complete. PRIMARY KEY constraint exists on ${fullTableName}.`
|
|
2565
|
+
};
|
|
2566
|
+
}
|
|
2567
|
+
const duplicateInfo = await this.checkForDuplicateSpans();
|
|
2568
|
+
if (duplicateInfo.hasDuplicates) {
|
|
2569
|
+
this.logger?.info?.(
|
|
2570
|
+
`Found ${duplicateInfo.duplicateCount} duplicate (traceId, spanId) combinations. Starting deduplication...`
|
|
2571
|
+
);
|
|
2572
|
+
await this.deduplicateSpans();
|
|
2573
|
+
} else {
|
|
2574
|
+
this.logger?.info?.(`No duplicate spans found.`);
|
|
2575
|
+
}
|
|
2576
|
+
await this.addSpansPrimaryKey();
|
|
2577
|
+
return {
|
|
2578
|
+
success: true,
|
|
2579
|
+
alreadyMigrated: false,
|
|
2580
|
+
duplicatesRemoved: duplicateInfo.duplicateCount,
|
|
2581
|
+
message: duplicateInfo.hasDuplicates ? `Migration complete. Removed duplicates and added PRIMARY KEY constraint to ${fullTableName}.` : `Migration complete. Added PRIMARY KEY constraint to ${fullTableName}.`
|
|
2582
|
+
};
|
|
2583
|
+
}
|
|
2584
|
+
/**
|
|
2585
|
+
* Check migration status for the spans table.
|
|
2586
|
+
* Returns information about whether migration is needed.
|
|
2587
|
+
*/
|
|
2588
|
+
async checkSpansMigrationStatus() {
|
|
2589
|
+
const fullTableName = getTableName({ indexName: storage.TABLE_SPANS, schemaName: getSchemaName(this.schemaName) });
|
|
2590
|
+
const pkExists = await this.spansPrimaryKeyExists();
|
|
2591
|
+
if (pkExists) {
|
|
2592
|
+
return {
|
|
2593
|
+
needsMigration: false,
|
|
2594
|
+
hasDuplicates: false,
|
|
2595
|
+
duplicateCount: 0,
|
|
2596
|
+
constraintExists: true,
|
|
2597
|
+
tableName: fullTableName
|
|
2598
|
+
};
|
|
2599
|
+
}
|
|
2600
|
+
const duplicateInfo = await this.checkForDuplicateSpans();
|
|
2601
|
+
return {
|
|
2602
|
+
needsMigration: true,
|
|
2603
|
+
hasDuplicates: duplicateInfo.hasDuplicates,
|
|
2604
|
+
duplicateCount: duplicateInfo.duplicateCount,
|
|
2605
|
+
constraintExists: false,
|
|
2606
|
+
tableName: fullTableName
|
|
2607
|
+
};
|
|
2608
|
+
}
|
|
2237
2609
|
/**
|
|
2238
2610
|
* Alters table schema to add columns if they don't exist
|
|
2239
2611
|
* @param tableName Name of the table
|
|
@@ -4271,6 +4643,22 @@ var ObservabilityPG = class _ObservabilityPG extends storage.ObservabilityStorag
|
|
|
4271
4643
|
}
|
|
4272
4644
|
}
|
|
4273
4645
|
}
|
|
4646
|
+
/**
|
|
4647
|
+
* Manually run the spans migration to deduplicate and add the unique constraint.
|
|
4648
|
+
* This is intended to be called from the CLI when duplicates are detected.
|
|
4649
|
+
*
|
|
4650
|
+
* @returns Migration result with status and details
|
|
4651
|
+
*/
|
|
4652
|
+
async migrateSpans() {
|
|
4653
|
+
return this.#db.migrateSpans();
|
|
4654
|
+
}
|
|
4655
|
+
/**
|
|
4656
|
+
* Check migration status for the spans table.
|
|
4657
|
+
* Returns information about whether migration is needed.
|
|
4658
|
+
*/
|
|
4659
|
+
async checkSpansMigrationStatus() {
|
|
4660
|
+
return this.#db.checkSpansMigrationStatus();
|
|
4661
|
+
}
|
|
4274
4662
|
async dangerouslyClearAll() {
|
|
4275
4663
|
await this.#db.clearTable({ tableName: storage.TABLE_SPANS });
|
|
4276
4664
|
}
|
|
@@ -5441,7 +5829,7 @@ var WorkflowsPG = class _WorkflowsPG extends storage.WorkflowsStorage {
|
|
|
5441
5829
|
// src/storage/index.ts
|
|
5442
5830
|
var DEFAULT_MAX_CONNECTIONS = 20;
|
|
5443
5831
|
var DEFAULT_IDLE_TIMEOUT_MS = 3e4;
|
|
5444
|
-
var PostgresStore = class extends storage.
|
|
5832
|
+
var PostgresStore = class extends storage.MastraCompositeStore {
|
|
5445
5833
|
#pool;
|
|
5446
5834
|
#db;
|
|
5447
5835
|
#ownsPool;
|
|
@@ -5520,6 +5908,9 @@ var PostgresStore = class extends storage.MastraStorage {
|
|
|
5520
5908
|
await super.init();
|
|
5521
5909
|
} catch (error$1) {
|
|
5522
5910
|
this.isInitialized = false;
|
|
5911
|
+
if (error$1 instanceof error.MastraError) {
|
|
5912
|
+
throw error$1;
|
|
5913
|
+
}
|
|
5523
5914
|
throw new error.MastraError(
|
|
5524
5915
|
{
|
|
5525
5916
|
id: storage.createStorageErrorId("PG", "INIT", "FAILED"),
|