@mastra/mssql 1.0.0-beta.11 → 1.0.0-beta.13

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/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { MastraError, ErrorCategory, ErrorDomain } from '@mastra/core/error';
2
- import { MemoryStorage, TABLE_THREADS, TABLE_MESSAGES, TABLE_RESOURCES, TABLE_SCHEMAS, createStorageErrorId, normalizePerPage, calculatePagination, ObservabilityStorage, TABLE_SPANS, SPAN_SCHEMA, listTracesArgsSchema, ScoresStorage, TABLE_SCORERS, WorkflowsStorage, TABLE_WORKFLOW_SNAPSHOT, MastraStorage, TraceStatus, getDefaultValue, transformScoreRow as transformScoreRow$1 } from '@mastra/core/storage';
2
+ import { MemoryStorage, TABLE_THREADS, TABLE_MESSAGES, TABLE_RESOURCES, TABLE_SCHEMAS, createStorageErrorId, normalizePerPage, calculatePagination, ObservabilityStorage, TABLE_SPANS, SPAN_SCHEMA, listTracesArgsSchema, ScoresStorage, TABLE_SCORERS, WorkflowsStorage, TABLE_WORKFLOW_SNAPSHOT, MastraCompositeStore, TraceStatus, getDefaultValue, transformScoreRow as transformScoreRow$1 } from '@mastra/core/storage';
3
3
  import sql from 'mssql';
4
4
  import { MessageList } from '@mastra/core/agent';
5
5
  import { MastraBase } from '@mastra/core/base';
@@ -334,15 +334,49 @@ ${columns}
334
334
  );
335
335
  const pkExists = Array.isArray(pkResult.recordset) && pkResult.recordset.length > 0;
336
336
  if (!pkExists) {
337
- try {
338
- const addPkSql = `ALTER TABLE ${getTableName({ indexName: tableName, schemaName: getSchemaName(this.schemaName) })} ADD CONSTRAINT [${pkConstraintName}] PRIMARY KEY ([traceId], [spanId])`;
339
- await this.pool.request().query(addPkSql);
340
- } catch (pkError) {
341
- this.logger?.warn?.(`Failed to add composite primary key to spans table:`, pkError);
337
+ const duplicateInfo = await this.checkForDuplicateSpans();
338
+ if (duplicateInfo.hasDuplicates) {
339
+ const errorMessage = `
340
+ ===========================================================================
341
+ MIGRATION REQUIRED: Duplicate spans detected in ${duplicateInfo.tableName}
342
+ ===========================================================================
343
+
344
+ Found ${duplicateInfo.duplicateCount} duplicate (traceId, spanId) combinations.
345
+
346
+ The spans table requires a unique constraint on (traceId, spanId), but your
347
+ database contains duplicate entries that must be resolved first.
348
+
349
+ To fix this, run the manual migration command:
350
+
351
+ npx mastra migrate
352
+
353
+ This command will:
354
+ 1. Remove duplicate spans (keeping the most complete/recent version)
355
+ 2. Add the required unique constraint
356
+
357
+ Note: This migration may take some time for large tables.
358
+ ===========================================================================
359
+ `;
360
+ throw new MastraError({
361
+ id: createStorageErrorId("MSSQL", "MIGRATION_REQUIRED", "DUPLICATE_SPANS"),
362
+ domain: ErrorDomain.STORAGE,
363
+ category: ErrorCategory.USER,
364
+ text: errorMessage
365
+ });
366
+ } else {
367
+ try {
368
+ const addPkSql = `ALTER TABLE ${getTableName({ indexName: tableName, schemaName: getSchemaName(this.schemaName) })} ADD CONSTRAINT [${pkConstraintName}] PRIMARY KEY ([traceId], [spanId])`;
369
+ await this.pool.request().query(addPkSql);
370
+ } catch (pkError) {
371
+ this.logger?.warn?.(`Failed to add composite primary key to spans table:`, pkError);
372
+ }
342
373
  }
343
374
  }
344
375
  }
345
376
  } catch (error) {
377
+ if (error instanceof MastraError) {
378
+ throw error;
379
+ }
346
380
  throw new MastraError(
347
381
  {
348
382
  id: createStorageErrorId("MSSQL", "CREATE_TABLE", "FAILED"),
@@ -384,6 +418,154 @@ ${columns}
384
418
  this.logger?.warn?.(`Failed to migrate spans table ${fullTableName}:`, error);
385
419
  }
386
420
  }
421
+ /**
422
+ * Deduplicates spans with the same (traceId, spanId) combination.
423
+ * This is needed for databases that existed before the unique constraint was added.
424
+ *
425
+ * Priority for keeping spans:
426
+ * 1. Completed spans (endedAt IS NOT NULL) over incomplete spans
427
+ * 2. Most recent updatedAt
428
+ * 3. Most recent createdAt (as tiebreaker)
429
+ *
430
+ * Note: This prioritizes migration completion over perfect data preservation.
431
+ * Old trace data may be lost, which is acceptable for this use case.
432
+ */
433
+ async deduplicateSpans() {
434
+ const fullTableName = getTableName({ indexName: TABLE_SPANS, schemaName: getSchemaName(this.schemaName) });
435
+ try {
436
+ const duplicateCheck = await this.pool.request().query(`
437
+ SELECT TOP 1 1 as has_duplicates
438
+ FROM ${fullTableName}
439
+ GROUP BY [traceId], [spanId]
440
+ HAVING COUNT(*) > 1
441
+ `);
442
+ if (!duplicateCheck.recordset || duplicateCheck.recordset.length === 0) {
443
+ this.logger?.debug?.(`No duplicate spans found in ${fullTableName}`);
444
+ return;
445
+ }
446
+ this.logger?.info?.(`Duplicate spans detected in ${fullTableName}, starting deduplication...`);
447
+ const result = await this.pool.request().query(`
448
+ WITH RankedSpans AS (
449
+ SELECT *, ROW_NUMBER() OVER (
450
+ PARTITION BY [traceId], [spanId]
451
+ ORDER BY
452
+ CASE WHEN [endedAt] IS NOT NULL THEN 0 ELSE 1 END,
453
+ [updatedAt] DESC,
454
+ [createdAt] DESC
455
+ ) as rn
456
+ FROM ${fullTableName}
457
+ )
458
+ DELETE FROM RankedSpans WHERE rn > 1
459
+ `);
460
+ this.logger?.info?.(
461
+ `Deduplication complete: removed ${result.rowsAffected?.[0] ?? 0} duplicate spans from ${fullTableName}`
462
+ );
463
+ } catch (error) {
464
+ this.logger?.warn?.("Failed to deduplicate spans:", error);
465
+ }
466
+ }
467
+ /**
468
+ * Checks for duplicate (traceId, spanId) combinations in the spans table.
469
+ * Returns information about duplicates for logging/CLI purposes.
470
+ */
471
+ async checkForDuplicateSpans() {
472
+ const fullTableName = getTableName({ indexName: TABLE_SPANS, schemaName: getSchemaName(this.schemaName) });
473
+ try {
474
+ const result = await this.pool.request().query(`
475
+ SELECT COUNT(*) as duplicate_count
476
+ FROM (
477
+ SELECT [traceId], [spanId]
478
+ FROM ${fullTableName}
479
+ GROUP BY [traceId], [spanId]
480
+ HAVING COUNT(*) > 1
481
+ ) duplicates
482
+ `);
483
+ const duplicateCount = result.recordset?.[0]?.duplicate_count ?? 0;
484
+ return {
485
+ hasDuplicates: duplicateCount > 0,
486
+ duplicateCount,
487
+ tableName: fullTableName
488
+ };
489
+ } catch (error) {
490
+ this.logger?.debug?.(`Could not check for duplicates: ${error}`);
491
+ return { hasDuplicates: false, duplicateCount: 0, tableName: fullTableName };
492
+ }
493
+ }
494
+ /**
495
+ * Checks if the PRIMARY KEY constraint on (traceId, spanId) already exists on the spans table.
496
+ */
497
+ async spansPrimaryKeyExists() {
498
+ const schemaPrefix = this.schemaName ? `${parseSqlIdentifier(this.schemaName, "schema name")}_` : "";
499
+ const pkConstraintName = `${schemaPrefix}mastra_ai_spans_traceid_spanid_pk`;
500
+ const checkPkRequest = this.pool.request();
501
+ checkPkRequest.input("constraintName", pkConstraintName);
502
+ const pkResult = await checkPkRequest.query(
503
+ `SELECT 1 AS found FROM sys.key_constraints WHERE name = @constraintName`
504
+ );
505
+ return Array.isArray(pkResult.recordset) && pkResult.recordset.length > 0;
506
+ }
507
+ /**
508
+ * Manually run the spans migration to deduplicate and add the unique constraint.
509
+ * This is intended to be called from the CLI when duplicates are detected.
510
+ *
511
+ * @returns Migration result with status and details
512
+ */
513
+ async migrateSpans() {
514
+ const fullTableName = getTableName({ indexName: TABLE_SPANS, schemaName: getSchemaName(this.schemaName) });
515
+ const pkExists = await this.spansPrimaryKeyExists();
516
+ if (pkExists) {
517
+ return {
518
+ success: true,
519
+ alreadyMigrated: true,
520
+ duplicatesRemoved: 0,
521
+ message: `Migration already complete. PRIMARY KEY constraint exists on ${fullTableName}.`
522
+ };
523
+ }
524
+ const duplicateInfo = await this.checkForDuplicateSpans();
525
+ if (duplicateInfo.hasDuplicates) {
526
+ this.logger?.info?.(
527
+ `Found ${duplicateInfo.duplicateCount} duplicate (traceId, spanId) combinations. Starting deduplication...`
528
+ );
529
+ await this.deduplicateSpans();
530
+ } else {
531
+ this.logger?.info?.(`No duplicate spans found.`);
532
+ }
533
+ const schemaPrefix = this.schemaName ? `${parseSqlIdentifier(this.schemaName, "schema name")}_` : "";
534
+ const pkConstraintName = `${schemaPrefix}mastra_ai_spans_traceid_spanid_pk`;
535
+ const addPkSql = `ALTER TABLE ${fullTableName} ADD CONSTRAINT [${pkConstraintName}] PRIMARY KEY ([traceId], [spanId])`;
536
+ await this.pool.request().query(addPkSql);
537
+ return {
538
+ success: true,
539
+ alreadyMigrated: false,
540
+ duplicatesRemoved: duplicateInfo.duplicateCount,
541
+ message: duplicateInfo.hasDuplicates ? `Migration complete. Removed duplicates and added PRIMARY KEY constraint to ${fullTableName}.` : `Migration complete. Added PRIMARY KEY constraint to ${fullTableName}.`
542
+ };
543
+ }
544
+ /**
545
+ * Check migration status for the spans table.
546
+ * Returns information about whether migration is needed.
547
+ */
548
+ async checkSpansMigrationStatus() {
549
+ const fullTableName = getTableName({ indexName: TABLE_SPANS, schemaName: getSchemaName(this.schemaName) });
550
+ const pkExists = await this.spansPrimaryKeyExists();
551
+ if (pkExists) {
552
+ return {
553
+ needsMigration: false,
554
+ hasDuplicates: false,
555
+ duplicateCount: 0,
556
+ constraintExists: true,
557
+ tableName: fullTableName
558
+ };
559
+ }
560
+ const duplicateInfo = await this.checkForDuplicateSpans();
561
+ return {
562
+ needsMigration: true,
563
+ hasDuplicates: duplicateInfo.hasDuplicates,
564
+ duplicateCount: duplicateInfo.duplicateCount,
565
+ constraintExists: false,
566
+ tableName: fullTableName
567
+ };
568
+ }
387
569
  /**
388
570
  * Alters table schema to add columns if they don't exist
389
571
  * @param tableName Name of the table
@@ -1259,28 +1441,76 @@ var MemoryMSSQL = class _MemoryMSSQL extends MemoryStorage {
1259
1441
  );
1260
1442
  }
1261
1443
  }
1262
- async listThreadsByResourceId(args) {
1263
- const { resourceId, page = 0, perPage: perPageInput, orderBy } = args;
1264
- if (page < 0) {
1444
+ async listThreads(args) {
1445
+ const { page = 0, perPage: perPageInput, orderBy, filter } = args;
1446
+ try {
1447
+ this.validatePaginationInput(page, perPageInput ?? 100);
1448
+ } catch (error) {
1265
1449
  throw new MastraError({
1266
- id: createStorageErrorId("MSSQL", "LIST_THREADS_BY_RESOURCE_ID", "INVALID_PAGE"),
1450
+ id: createStorageErrorId("MSSQL", "LIST_THREADS", "INVALID_PAGE"),
1267
1451
  domain: ErrorDomain.STORAGE,
1268
1452
  category: ErrorCategory.USER,
1269
- text: "Page number must be non-negative",
1270
- details: {
1271
- resourceId,
1272
- page
1273
- }
1453
+ text: error instanceof Error ? error.message : "Invalid pagination parameters",
1454
+ details: { page, ...perPageInput !== void 0 && { perPage: perPageInput } }
1274
1455
  });
1275
1456
  }
1276
1457
  const perPage = normalizePerPage(perPageInput, 100);
1458
+ try {
1459
+ this.validateMetadataKeys(filter?.metadata);
1460
+ } catch (error) {
1461
+ throw new MastraError({
1462
+ id: createStorageErrorId("MSSQL", "LIST_THREADS", "INVALID_METADATA_KEY"),
1463
+ domain: ErrorDomain.STORAGE,
1464
+ category: ErrorCategory.USER,
1465
+ text: error instanceof Error ? error.message : "Invalid metadata key",
1466
+ details: { metadataKeys: filter?.metadata ? Object.keys(filter.metadata).join(", ") : "" }
1467
+ });
1468
+ }
1277
1469
  const { offset, perPage: perPageForResponse } = calculatePagination(page, perPageInput, perPage);
1278
1470
  const { field, direction } = this.parseOrderBy(orderBy);
1279
1471
  try {
1280
- const baseQuery = `FROM ${getTableName2({ indexName: TABLE_THREADS, schemaName: getSchemaName2(this.schema) })} WHERE [resourceId] = @resourceId`;
1472
+ const tableName = getTableName2({ indexName: TABLE_THREADS, schemaName: getSchemaName2(this.schema) });
1473
+ const whereClauses = [];
1474
+ const params = {};
1475
+ if (filter?.resourceId) {
1476
+ whereClauses.push("[resourceId] = @resourceId");
1477
+ params.resourceId = filter.resourceId;
1478
+ }
1479
+ if (filter?.metadata && Object.keys(filter.metadata).length > 0) {
1480
+ let metadataIndex = 0;
1481
+ for (const [key, value] of Object.entries(filter.metadata)) {
1482
+ if (value !== null && typeof value === "object") {
1483
+ throw new MastraError({
1484
+ id: createStorageErrorId("MSSQL", "LIST_THREADS", "INVALID_METADATA_VALUE"),
1485
+ domain: ErrorDomain.STORAGE,
1486
+ category: ErrorCategory.USER,
1487
+ text: `Metadata filter value for key "${key}" must be a scalar type (string, number, boolean, or null), got ${Array.isArray(value) ? "array" : "object"}`,
1488
+ details: { key, valueType: Array.isArray(value) ? "array" : "object" }
1489
+ });
1490
+ }
1491
+ if (value === null) {
1492
+ whereClauses.push(`JSON_VALUE(metadata, '$.${key}') IS NULL`);
1493
+ } else {
1494
+ const paramName = `metadata${metadataIndex}`;
1495
+ whereClauses.push(`JSON_VALUE(metadata, '$.${key}') = @${paramName}`);
1496
+ if (typeof value === "string") {
1497
+ params[paramName] = value;
1498
+ } else if (typeof value === "boolean") {
1499
+ params[paramName] = value ? "true" : "false";
1500
+ } else {
1501
+ params[paramName] = String(value);
1502
+ }
1503
+ }
1504
+ metadataIndex++;
1505
+ }
1506
+ }
1507
+ const whereClause = whereClauses.length > 0 ? `WHERE ${whereClauses.join(" AND ")}` : "";
1508
+ const baseQuery = `FROM ${tableName} ${whereClause}`;
1281
1509
  const countQuery = `SELECT COUNT(*) as count ${baseQuery}`;
1282
1510
  const countRequest = this.pool.request();
1283
- countRequest.input("resourceId", resourceId);
1511
+ for (const [key, value] of Object.entries(params)) {
1512
+ countRequest.input(key, value);
1513
+ }
1284
1514
  const countResult = await countRequest.query(countQuery);
1285
1515
  const total = parseInt(countResult.recordset[0]?.count ?? "0", 10);
1286
1516
  if (total === 0) {
@@ -1297,7 +1527,9 @@ var MemoryMSSQL = class _MemoryMSSQL extends MemoryStorage {
1297
1527
  const limitValue = perPageInput === false ? total : perPage;
1298
1528
  const dataQuery = `SELECT id, [resourceId], title, metadata, [createdAt], [updatedAt] ${baseQuery} ORDER BY ${orderByField} ${dir} OFFSET @offset ROWS FETCH NEXT @perPage ROWS ONLY`;
1299
1529
  const dataRequest = this.pool.request();
1300
- dataRequest.input("resourceId", resourceId);
1530
+ for (const [key, value] of Object.entries(params)) {
1531
+ dataRequest.input(key, value);
1532
+ }
1301
1533
  dataRequest.input("offset", offset);
1302
1534
  if (limitValue > 2147483647) {
1303
1535
  dataRequest.input("perPage", sql.BigInt, limitValue);
@@ -1320,13 +1552,17 @@ var MemoryMSSQL = class _MemoryMSSQL extends MemoryStorage {
1320
1552
  hasMore: perPageInput === false ? false : offset + perPage < total
1321
1553
  };
1322
1554
  } catch (error) {
1555
+ if (error instanceof MastraError && error.category === ErrorCategory.USER) {
1556
+ throw error;
1557
+ }
1323
1558
  const mastraError = new MastraError(
1324
1559
  {
1325
- id: createStorageErrorId("MSSQL", "LIST_THREADS_BY_RESOURCE_ID", "FAILED"),
1560
+ id: createStorageErrorId("MSSQL", "LIST_THREADS", "FAILED"),
1326
1561
  domain: ErrorDomain.STORAGE,
1327
1562
  category: ErrorCategory.THIRD_PARTY,
1328
1563
  details: {
1329
- resourceId,
1564
+ ...filter?.resourceId && { resourceId: filter.resourceId },
1565
+ hasMetadataFilter: !!filter?.metadata,
1330
1566
  page
1331
1567
  }
1332
1568
  },
@@ -2208,6 +2444,22 @@ var ObservabilityMSSQL = class _ObservabilityMSSQL extends ObservabilityStorage
2208
2444
  async dangerouslyClearAll() {
2209
2445
  await this.db.clearTable({ tableName: TABLE_SPANS });
2210
2446
  }
2447
+ /**
2448
+ * Manually run the spans migration to deduplicate and add the unique constraint.
2449
+ * This is intended to be called from the CLI when duplicates are detected.
2450
+ *
2451
+ * @returns Migration result with status and details
2452
+ */
2453
+ async migrateSpans() {
2454
+ return this.db.migrateSpans();
2455
+ }
2456
+ /**
2457
+ * Check migration status for the spans table.
2458
+ * Returns information about whether migration is needed.
2459
+ */
2460
+ async checkSpansMigrationStatus() {
2461
+ return this.db.checkSpansMigrationStatus();
2462
+ }
2211
2463
  get tracingStrategy() {
2212
2464
  return {
2213
2465
  preferred: "batch-with-updates",
@@ -3607,7 +3859,7 @@ var WorkflowsMSSQL = class _WorkflowsMSSQL extends WorkflowsStorage {
3607
3859
  var isPoolConfig = (config) => {
3608
3860
  return "pool" in config;
3609
3861
  };
3610
- var MSSQLStore = class extends MastraStorage {
3862
+ var MSSQLStore = class extends MastraCompositeStore {
3611
3863
  pool;
3612
3864
  schema;
3613
3865
  isConnected = null;
@@ -3678,6 +3930,9 @@ var MSSQLStore = class extends MastraStorage {
3678
3930
  await super.init();
3679
3931
  } catch (error) {
3680
3932
  this.isConnected = null;
3933
+ if (error instanceof MastraError) {
3934
+ throw error;
3935
+ }
3681
3936
  throw new MastraError(
3682
3937
  {
3683
3938
  id: createStorageErrorId("MSSQL", "INIT", "FAILED"),