@mastra/mssql 1.0.0-beta.12 → 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/CHANGELOG.md CHANGED
@@ -1,5 +1,63 @@
1
1
  # @mastra/mssql
2
2
 
3
+ ## 1.0.0-beta.13
4
+
5
+ ### Patch Changes
6
+
7
+ - Fixed duplicate spans migration issue across all storage backends. When upgrading from older versions, existing duplicate (traceId, spanId) combinations in the spans table could prevent the unique constraint from being created. The migration deduplicates spans before adding the constraint. ([#12073](https://github.com/mastra-ai/mastra/pull/12073))
8
+
9
+ **Deduplication rules (in priority order):**
10
+ 1. Keep completed spans (those with `endedAt` set) over incomplete spans
11
+ 2. Among spans with the same completion status, keep the one with the newest `updatedAt`
12
+ 3. Use `createdAt` as the final tiebreaker
13
+
14
+ **What changed:**
15
+ - Added `migrateSpans()` method to observability stores for manual migration
16
+ - Added `checkSpansMigrationStatus()` method to check if migration is needed
17
+ - All stores use optimized single-query deduplication to avoid memory issues on large tables
18
+
19
+ **Usage example:**
20
+
21
+ ```typescript
22
+ const observability = await storage.getStore('observability');
23
+ const status = await observability.checkSpansMigrationStatus();
24
+ if (status.needsMigration) {
25
+ const result = await observability.migrateSpans();
26
+ console.log(`Migration complete: ${result.duplicatesRemoved} duplicates removed`);
27
+ }
28
+ ```
29
+
30
+ Fixes #11840
31
+
32
+ - Renamed MastraStorage to MastraCompositeStore for better clarity. The old MastraStorage name remains available as a deprecated alias for backward compatibility, but will be removed in a future version. ([#12093](https://github.com/mastra-ai/mastra/pull/12093))
33
+
34
+ **Migration:**
35
+
36
+ Update your imports and usage:
37
+
38
+ ```typescript
39
+ // Before
40
+ import { MastraStorage } from '@mastra/core/storage';
41
+
42
+ const storage = new MastraStorage({
43
+ id: 'composite',
44
+ domains: { ... }
45
+ });
46
+
47
+ // After
48
+ import { MastraCompositeStore } from '@mastra/core/storage';
49
+
50
+ const storage = new MastraCompositeStore({
51
+ id: 'composite',
52
+ domains: { ... }
53
+ });
54
+ ```
55
+
56
+ The new name better reflects that this is a composite storage implementation that routes different domains (workflows, traces, messages) to different underlying stores, avoiding confusion with the general "Mastra Storage" concept.
57
+
58
+ - Updated dependencies [[`026b848`](https://github.com/mastra-ai/mastra/commit/026b8483fbf5b6d977be8f7e6aac8d15c75558ac), [`ffa553a`](https://github.com/mastra-ai/mastra/commit/ffa553a3edc1bd17d73669fba66d6b6f4ac10897)]:
59
+ - @mastra/core@1.0.0-beta.26
60
+
3
61
  ## 1.0.0-beta.12
4
62
 
5
63
  ### Patch Changes
@@ -28,4 +28,4 @@ docs/
28
28
  ## Version
29
29
 
30
30
  Package: @mastra/mssql
31
- Version: 1.0.0-beta.12
31
+ Version: 1.0.0-beta.13
@@ -5,7 +5,7 @@ description: Documentation for @mastra/mssql. Includes links to type definitions
5
5
 
6
6
  # @mastra/mssql Documentation
7
7
 
8
- > **Version**: 1.0.0-beta.12
8
+ > **Version**: 1.0.0-beta.13
9
9
  > **Package**: @mastra/mssql
10
10
 
11
11
  ## Quick Navigation
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.0.0-beta.12",
2
+ "version": "1.0.0-beta.13",
3
3
  "package": "@mastra/mssql",
4
4
  "exports": {},
5
5
  "modules": {}
package/dist/index.cjs CHANGED
@@ -340,15 +340,49 @@ ${columns}
340
340
  );
341
341
  const pkExists = Array.isArray(pkResult.recordset) && pkResult.recordset.length > 0;
342
342
  if (!pkExists) {
343
- try {
344
- const addPkSql = `ALTER TABLE ${getTableName({ indexName: tableName, schemaName: getSchemaName(this.schemaName) })} ADD CONSTRAINT [${pkConstraintName}] PRIMARY KEY ([traceId], [spanId])`;
345
- await this.pool.request().query(addPkSql);
346
- } catch (pkError) {
347
- this.logger?.warn?.(`Failed to add composite primary key to spans table:`, pkError);
343
+ const duplicateInfo = await this.checkForDuplicateSpans();
344
+ if (duplicateInfo.hasDuplicates) {
345
+ const errorMessage = `
346
+ ===========================================================================
347
+ MIGRATION REQUIRED: Duplicate spans detected in ${duplicateInfo.tableName}
348
+ ===========================================================================
349
+
350
+ Found ${duplicateInfo.duplicateCount} duplicate (traceId, spanId) combinations.
351
+
352
+ The spans table requires a unique constraint on (traceId, spanId), but your
353
+ database contains duplicate entries that must be resolved first.
354
+
355
+ To fix this, run the manual migration command:
356
+
357
+ npx mastra migrate
358
+
359
+ This command will:
360
+ 1. Remove duplicate spans (keeping the most complete/recent version)
361
+ 2. Add the required unique constraint
362
+
363
+ Note: This migration may take some time for large tables.
364
+ ===========================================================================
365
+ `;
366
+ throw new error.MastraError({
367
+ id: storage.createStorageErrorId("MSSQL", "MIGRATION_REQUIRED", "DUPLICATE_SPANS"),
368
+ domain: error.ErrorDomain.STORAGE,
369
+ category: error.ErrorCategory.USER,
370
+ text: errorMessage
371
+ });
372
+ } else {
373
+ try {
374
+ const addPkSql = `ALTER TABLE ${getTableName({ indexName: tableName, schemaName: getSchemaName(this.schemaName) })} ADD CONSTRAINT [${pkConstraintName}] PRIMARY KEY ([traceId], [spanId])`;
375
+ await this.pool.request().query(addPkSql);
376
+ } catch (pkError) {
377
+ this.logger?.warn?.(`Failed to add composite primary key to spans table:`, pkError);
378
+ }
348
379
  }
349
380
  }
350
381
  }
351
382
  } catch (error$1) {
383
+ if (error$1 instanceof error.MastraError) {
384
+ throw error$1;
385
+ }
352
386
  throw new error.MastraError(
353
387
  {
354
388
  id: storage.createStorageErrorId("MSSQL", "CREATE_TABLE", "FAILED"),
@@ -390,6 +424,154 @@ ${columns}
390
424
  this.logger?.warn?.(`Failed to migrate spans table ${fullTableName}:`, error);
391
425
  }
392
426
  }
427
+ /**
428
+ * Deduplicates spans with the same (traceId, spanId) combination.
429
+ * This is needed for databases that existed before the unique constraint was added.
430
+ *
431
+ * Priority for keeping spans:
432
+ * 1. Completed spans (endedAt IS NOT NULL) over incomplete spans
433
+ * 2. Most recent updatedAt
434
+ * 3. Most recent createdAt (as tiebreaker)
435
+ *
436
+ * Note: This prioritizes migration completion over perfect data preservation.
437
+ * Old trace data may be lost, which is acceptable for this use case.
438
+ */
439
+ async deduplicateSpans() {
440
+ const fullTableName = getTableName({ indexName: storage.TABLE_SPANS, schemaName: getSchemaName(this.schemaName) });
441
+ try {
442
+ const duplicateCheck = await this.pool.request().query(`
443
+ SELECT TOP 1 1 as has_duplicates
444
+ FROM ${fullTableName}
445
+ GROUP BY [traceId], [spanId]
446
+ HAVING COUNT(*) > 1
447
+ `);
448
+ if (!duplicateCheck.recordset || duplicateCheck.recordset.length === 0) {
449
+ this.logger?.debug?.(`No duplicate spans found in ${fullTableName}`);
450
+ return;
451
+ }
452
+ this.logger?.info?.(`Duplicate spans detected in ${fullTableName}, starting deduplication...`);
453
+ const result = await this.pool.request().query(`
454
+ WITH RankedSpans AS (
455
+ SELECT *, ROW_NUMBER() OVER (
456
+ PARTITION BY [traceId], [spanId]
457
+ ORDER BY
458
+ CASE WHEN [endedAt] IS NOT NULL THEN 0 ELSE 1 END,
459
+ [updatedAt] DESC,
460
+ [createdAt] DESC
461
+ ) as rn
462
+ FROM ${fullTableName}
463
+ )
464
+ DELETE FROM RankedSpans WHERE rn > 1
465
+ `);
466
+ this.logger?.info?.(
467
+ `Deduplication complete: removed ${result.rowsAffected?.[0] ?? 0} duplicate spans from ${fullTableName}`
468
+ );
469
+ } catch (error) {
470
+ this.logger?.warn?.("Failed to deduplicate spans:", error);
471
+ }
472
+ }
473
+ /**
474
+ * Checks for duplicate (traceId, spanId) combinations in the spans table.
475
+ * Returns information about duplicates for logging/CLI purposes.
476
+ */
477
+ async checkForDuplicateSpans() {
478
+ const fullTableName = getTableName({ indexName: storage.TABLE_SPANS, schemaName: getSchemaName(this.schemaName) });
479
+ try {
480
+ const result = await this.pool.request().query(`
481
+ SELECT COUNT(*) as duplicate_count
482
+ FROM (
483
+ SELECT [traceId], [spanId]
484
+ FROM ${fullTableName}
485
+ GROUP BY [traceId], [spanId]
486
+ HAVING COUNT(*) > 1
487
+ ) duplicates
488
+ `);
489
+ const duplicateCount = result.recordset?.[0]?.duplicate_count ?? 0;
490
+ return {
491
+ hasDuplicates: duplicateCount > 0,
492
+ duplicateCount,
493
+ tableName: fullTableName
494
+ };
495
+ } catch (error) {
496
+ this.logger?.debug?.(`Could not check for duplicates: ${error}`);
497
+ return { hasDuplicates: false, duplicateCount: 0, tableName: fullTableName };
498
+ }
499
+ }
500
+ /**
501
+ * Checks if the PRIMARY KEY constraint on (traceId, spanId) already exists on the spans table.
502
+ */
503
+ async spansPrimaryKeyExists() {
504
+ const schemaPrefix = this.schemaName ? `${utils.parseSqlIdentifier(this.schemaName, "schema name")}_` : "";
505
+ const pkConstraintName = `${schemaPrefix}mastra_ai_spans_traceid_spanid_pk`;
506
+ const checkPkRequest = this.pool.request();
507
+ checkPkRequest.input("constraintName", pkConstraintName);
508
+ const pkResult = await checkPkRequest.query(
509
+ `SELECT 1 AS found FROM sys.key_constraints WHERE name = @constraintName`
510
+ );
511
+ return Array.isArray(pkResult.recordset) && pkResult.recordset.length > 0;
512
+ }
513
+ /**
514
+ * Manually run the spans migration to deduplicate and add the unique constraint.
515
+ * This is intended to be called from the CLI when duplicates are detected.
516
+ *
517
+ * @returns Migration result with status and details
518
+ */
519
+ async migrateSpans() {
520
+ const fullTableName = getTableName({ indexName: storage.TABLE_SPANS, schemaName: getSchemaName(this.schemaName) });
521
+ const pkExists = await this.spansPrimaryKeyExists();
522
+ if (pkExists) {
523
+ return {
524
+ success: true,
525
+ alreadyMigrated: true,
526
+ duplicatesRemoved: 0,
527
+ message: `Migration already complete. PRIMARY KEY constraint exists on ${fullTableName}.`
528
+ };
529
+ }
530
+ const duplicateInfo = await this.checkForDuplicateSpans();
531
+ if (duplicateInfo.hasDuplicates) {
532
+ this.logger?.info?.(
533
+ `Found ${duplicateInfo.duplicateCount} duplicate (traceId, spanId) combinations. Starting deduplication...`
534
+ );
535
+ await this.deduplicateSpans();
536
+ } else {
537
+ this.logger?.info?.(`No duplicate spans found.`);
538
+ }
539
+ const schemaPrefix = this.schemaName ? `${utils.parseSqlIdentifier(this.schemaName, "schema name")}_` : "";
540
+ const pkConstraintName = `${schemaPrefix}mastra_ai_spans_traceid_spanid_pk`;
541
+ const addPkSql = `ALTER TABLE ${fullTableName} ADD CONSTRAINT [${pkConstraintName}] PRIMARY KEY ([traceId], [spanId])`;
542
+ await this.pool.request().query(addPkSql);
543
+ return {
544
+ success: true,
545
+ alreadyMigrated: false,
546
+ duplicatesRemoved: duplicateInfo.duplicateCount,
547
+ message: duplicateInfo.hasDuplicates ? `Migration complete. Removed duplicates and added PRIMARY KEY constraint to ${fullTableName}.` : `Migration complete. Added PRIMARY KEY constraint to ${fullTableName}.`
548
+ };
549
+ }
550
+ /**
551
+ * Check migration status for the spans table.
552
+ * Returns information about whether migration is needed.
553
+ */
554
+ async checkSpansMigrationStatus() {
555
+ const fullTableName = getTableName({ indexName: storage.TABLE_SPANS, schemaName: getSchemaName(this.schemaName) });
556
+ const pkExists = await this.spansPrimaryKeyExists();
557
+ if (pkExists) {
558
+ return {
559
+ needsMigration: false,
560
+ hasDuplicates: false,
561
+ duplicateCount: 0,
562
+ constraintExists: true,
563
+ tableName: fullTableName
564
+ };
565
+ }
566
+ const duplicateInfo = await this.checkForDuplicateSpans();
567
+ return {
568
+ needsMigration: true,
569
+ hasDuplicates: duplicateInfo.hasDuplicates,
570
+ duplicateCount: duplicateInfo.duplicateCount,
571
+ constraintExists: false,
572
+ tableName: fullTableName
573
+ };
574
+ }
393
575
  /**
394
576
  * Alters table schema to add columns if they don't exist
395
577
  * @param tableName Name of the table
@@ -2268,6 +2450,22 @@ var ObservabilityMSSQL = class _ObservabilityMSSQL extends storage.Observability
2268
2450
  async dangerouslyClearAll() {
2269
2451
  await this.db.clearTable({ tableName: storage.TABLE_SPANS });
2270
2452
  }
2453
+ /**
2454
+ * Manually run the spans migration to deduplicate and add the unique constraint.
2455
+ * This is intended to be called from the CLI when duplicates are detected.
2456
+ *
2457
+ * @returns Migration result with status and details
2458
+ */
2459
+ async migrateSpans() {
2460
+ return this.db.migrateSpans();
2461
+ }
2462
+ /**
2463
+ * Check migration status for the spans table.
2464
+ * Returns information about whether migration is needed.
2465
+ */
2466
+ async checkSpansMigrationStatus() {
2467
+ return this.db.checkSpansMigrationStatus();
2468
+ }
2271
2469
  get tracingStrategy() {
2272
2470
  return {
2273
2471
  preferred: "batch-with-updates",
@@ -3667,7 +3865,7 @@ var WorkflowsMSSQL = class _WorkflowsMSSQL extends storage.WorkflowsStorage {
3667
3865
  var isPoolConfig = (config) => {
3668
3866
  return "pool" in config;
3669
3867
  };
3670
- var MSSQLStore = class extends storage.MastraStorage {
3868
+ var MSSQLStore = class extends storage.MastraCompositeStore {
3671
3869
  pool;
3672
3870
  schema;
3673
3871
  isConnected = null;
@@ -3738,6 +3936,9 @@ var MSSQLStore = class extends storage.MastraStorage {
3738
3936
  await super.init();
3739
3937
  } catch (error$1) {
3740
3938
  this.isConnected = null;
3939
+ if (error$1 instanceof error.MastraError) {
3940
+ throw error$1;
3941
+ }
3741
3942
  throw new error.MastraError(
3742
3943
  {
3743
3944
  id: storage.createStorageErrorId("MSSQL", "INIT", "FAILED"),