@mastra/mongodb 1.0.0-beta.13 → 1.0.0-beta.14

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/mongodb
2
2
 
3
+ ## 1.0.0-beta.14
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.13
4
62
 
5
63
  ### Patch Changes
@@ -31,4 +31,4 @@ docs/
31
31
  ## Version
32
32
 
33
33
  Package: @mastra/mongodb
34
- Version: 1.0.0-beta.13
34
+ Version: 1.0.0-beta.14
@@ -5,7 +5,7 @@ description: Documentation for @mastra/mongodb. Includes links to type definitio
5
5
 
6
6
  # @mastra/mongodb Documentation
7
7
 
8
- > **Version**: 1.0.0-beta.13
8
+ > **Version**: 1.0.0-beta.14
9
9
  > **Package**: @mastra/mongodb
10
10
 
11
11
  ## Quick Navigation
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.0.0-beta.13",
2
+ "version": "1.0.0-beta.14",
3
3
  "package": "@mastra/mongodb",
4
4
  "exports": {},
5
5
  "modules": {}
package/dist/index.cjs CHANGED
@@ -13,7 +13,7 @@ var evals = require('@mastra/core/evals');
13
13
 
14
14
  // package.json
15
15
  var package_default = {
16
- version: "1.0.0-beta.13"};
16
+ version: "1.0.0-beta.14"};
17
17
  var MongoDBFilterTranslator = class extends filter.BaseFilterTranslator {
18
18
  getSupportedOperators() {
19
19
  return {
@@ -280,6 +280,8 @@ var MongoDBVector = class extends vector.MastraVector {
280
280
  throw new Error(`Index "${indexNameInternal}" did not become ready within timeout`);
281
281
  }
282
282
  async upsert({ indexName, vectors, metadata, ids, documents }) {
283
+ vector.validateUpsertInput("MONGODB", vectors, metadata, ids);
284
+ vector.validateVectorValues("MONGODB", vectors);
283
285
  try {
284
286
  const collection = await this.getCollection(indexName);
285
287
  this.collectionForValidation = collection;
@@ -355,8 +357,8 @@ var MongoDBVector = class extends vector.MastraVector {
355
357
  index: indexNameInternal,
356
358
  queryVector,
357
359
  path: this.embeddingFieldName,
358
- numCandidates: 100,
359
- limit: topK
360
+ numCandidates: Math.min(1e4, Math.max(100, topK)),
361
+ limit: Math.min(1e4, topK)
360
362
  };
361
363
  if (Object.keys(combinedFilter).length > 0) {
362
364
  const filterWithExclusion = {
@@ -1950,9 +1952,221 @@ var ObservabilityMongoDB = class _ObservabilityMongoDB extends storage.Observabi
1950
1952
  }
1951
1953
  }
1952
1954
  async init() {
1955
+ const uniqueIndexExists = await this.spansUniqueIndexExists();
1956
+ if (!uniqueIndexExists) {
1957
+ const duplicateInfo = await this.checkForDuplicateSpans();
1958
+ if (duplicateInfo.hasDuplicates) {
1959
+ const errorMessage = `
1960
+ ===========================================================================
1961
+ MIGRATION REQUIRED: Duplicate spans detected in ${storage.TABLE_SPANS} collection
1962
+ ===========================================================================
1963
+
1964
+ Found ${duplicateInfo.duplicateCount} duplicate (traceId, spanId) combinations.
1965
+
1966
+ The spans collection requires a unique index on (traceId, spanId), but your
1967
+ database contains duplicate entries that must be resolved first.
1968
+
1969
+ To fix this, run the manual migration command:
1970
+
1971
+ npx mastra migrate
1972
+
1973
+ This command will:
1974
+ 1. Remove duplicate spans (keeping the most complete/recent version)
1975
+ 2. Add the required unique index
1976
+
1977
+ Note: This migration may take some time for large collections.
1978
+ ===========================================================================
1979
+ `;
1980
+ throw new error.MastraError({
1981
+ id: storage.createStorageErrorId("MONGODB", "MIGRATION_REQUIRED", "DUPLICATE_SPANS"),
1982
+ domain: error.ErrorDomain.STORAGE,
1983
+ category: error.ErrorCategory.USER,
1984
+ text: errorMessage
1985
+ });
1986
+ }
1987
+ }
1953
1988
  await this.createDefaultIndexes();
1954
1989
  await this.createCustomIndexes();
1955
1990
  }
1991
+ /**
1992
+ * Checks if the unique index on (spanId, traceId) already exists on the spans collection.
1993
+ * Used to skip deduplication when the index already exists (migration already complete).
1994
+ */
1995
+ async spansUniqueIndexExists() {
1996
+ try {
1997
+ const collection = await this.getCollection(storage.TABLE_SPANS);
1998
+ const indexes = await collection.indexes();
1999
+ return indexes.some((idx) => idx.unique === true && idx.key?.spanId === 1 && idx.key?.traceId === 1);
2000
+ } catch {
2001
+ return false;
2002
+ }
2003
+ }
2004
+ /**
2005
+ * Checks for duplicate (traceId, spanId) combinations in the spans collection.
2006
+ * Returns information about duplicates for logging/CLI purposes.
2007
+ */
2008
+ async checkForDuplicateSpans() {
2009
+ try {
2010
+ const collection = await this.getCollection(storage.TABLE_SPANS);
2011
+ const result = await collection.aggregate([
2012
+ {
2013
+ $group: {
2014
+ _id: { traceId: "$traceId", spanId: "$spanId" },
2015
+ count: { $sum: 1 }
2016
+ }
2017
+ },
2018
+ { $match: { count: { $gt: 1 } } },
2019
+ { $count: "duplicateCount" }
2020
+ ]).toArray();
2021
+ const duplicateCount = result[0]?.duplicateCount ?? 0;
2022
+ return {
2023
+ hasDuplicates: duplicateCount > 0,
2024
+ duplicateCount
2025
+ };
2026
+ } catch (error) {
2027
+ this.logger?.debug?.(`Could not check for duplicates: ${error}`);
2028
+ return { hasDuplicates: false, duplicateCount: 0 };
2029
+ }
2030
+ }
2031
+ /**
2032
+ * Manually run the spans migration to deduplicate and add the unique index.
2033
+ * This is intended to be called from the CLI when duplicates are detected.
2034
+ *
2035
+ * @returns Migration result with status and details
2036
+ */
2037
+ async migrateSpans() {
2038
+ const indexExists = await this.spansUniqueIndexExists();
2039
+ if (indexExists) {
2040
+ return {
2041
+ success: true,
2042
+ alreadyMigrated: true,
2043
+ duplicatesRemoved: 0,
2044
+ message: `Migration already complete. Unique index exists on ${storage.TABLE_SPANS} collection.`
2045
+ };
2046
+ }
2047
+ const duplicateInfo = await this.checkForDuplicateSpans();
2048
+ if (duplicateInfo.hasDuplicates) {
2049
+ this.logger?.info?.(
2050
+ `Found ${duplicateInfo.duplicateCount} duplicate (traceId, spanId) combinations. Starting deduplication...`
2051
+ );
2052
+ await this.deduplicateSpans();
2053
+ } else {
2054
+ this.logger?.info?.(`No duplicate spans found.`);
2055
+ }
2056
+ await this.createDefaultIndexes();
2057
+ await this.createCustomIndexes();
2058
+ return {
2059
+ success: true,
2060
+ alreadyMigrated: false,
2061
+ duplicatesRemoved: duplicateInfo.duplicateCount,
2062
+ message: duplicateInfo.hasDuplicates ? `Migration complete. Removed duplicates and added unique index to ${storage.TABLE_SPANS} collection.` : `Migration complete. Added unique index to ${storage.TABLE_SPANS} collection.`
2063
+ };
2064
+ }
2065
+ /**
2066
+ * Check migration status for the spans collection.
2067
+ * Returns information about whether migration is needed.
2068
+ */
2069
+ async checkSpansMigrationStatus() {
2070
+ const indexExists = await this.spansUniqueIndexExists();
2071
+ if (indexExists) {
2072
+ return {
2073
+ needsMigration: false,
2074
+ hasDuplicates: false,
2075
+ duplicateCount: 0,
2076
+ constraintExists: true,
2077
+ tableName: storage.TABLE_SPANS
2078
+ };
2079
+ }
2080
+ const duplicateInfo = await this.checkForDuplicateSpans();
2081
+ return {
2082
+ needsMigration: true,
2083
+ hasDuplicates: duplicateInfo.hasDuplicates,
2084
+ duplicateCount: duplicateInfo.duplicateCount,
2085
+ constraintExists: false,
2086
+ tableName: storage.TABLE_SPANS
2087
+ };
2088
+ }
2089
+ /**
2090
+ * Deduplicates spans with the same (traceId, spanId) combination.
2091
+ * This is needed for databases that existed before the unique constraint was added.
2092
+ *
2093
+ * Priority for keeping spans:
2094
+ * 1. Completed spans (endedAt IS NOT NULL) over incomplete spans
2095
+ * 2. Most recent updatedAt
2096
+ * 3. Most recent createdAt (as tiebreaker)
2097
+ *
2098
+ * Note: This prioritizes migration completion over perfect data preservation.
2099
+ * Old trace data may be lost, which is acceptable for this use case.
2100
+ */
2101
+ async deduplicateSpans() {
2102
+ try {
2103
+ const collection = await this.getCollection(storage.TABLE_SPANS);
2104
+ const duplicateCheck = await collection.aggregate([
2105
+ {
2106
+ $group: {
2107
+ _id: { traceId: "$traceId", spanId: "$spanId" },
2108
+ count: { $sum: 1 }
2109
+ }
2110
+ },
2111
+ { $match: { count: { $gt: 1 } } },
2112
+ { $limit: 1 }
2113
+ ]).toArray();
2114
+ if (duplicateCheck.length === 0) {
2115
+ this.logger?.debug?.("No duplicate spans found");
2116
+ return;
2117
+ }
2118
+ this.logger?.info?.("Duplicate spans detected, starting deduplication...");
2119
+ const idsToDelete = await collection.aggregate([
2120
+ // Sort by priority (affects which document $first picks within each group)
2121
+ {
2122
+ $sort: {
2123
+ // Completed spans first (endedAt exists and is not null)
2124
+ endedAt: -1,
2125
+ updatedAt: -1,
2126
+ createdAt: -1
2127
+ }
2128
+ },
2129
+ // Group by (traceId, spanId), keeping the first (best) _id and all _ids
2130
+ {
2131
+ $group: {
2132
+ _id: { traceId: "$traceId", spanId: "$spanId" },
2133
+ keepId: { $first: "$_id" },
2134
+ // The best one to keep (after sort)
2135
+ allIds: { $push: "$_id" },
2136
+ // All ObjectIds (just 12 bytes each, not full docs)
2137
+ count: { $sum: 1 }
2138
+ }
2139
+ },
2140
+ // Only consider groups with duplicates
2141
+ { $match: { count: { $gt: 1 } } },
2142
+ // Get IDs to delete (allIds minus keepId)
2143
+ {
2144
+ $project: {
2145
+ idsToDelete: {
2146
+ $filter: {
2147
+ input: "$allIds",
2148
+ cond: { $ne: ["$$this", "$keepId"] }
2149
+ }
2150
+ }
2151
+ }
2152
+ },
2153
+ // Unwind to get flat list of IDs
2154
+ { $unwind: "$idsToDelete" },
2155
+ // Just output the ID
2156
+ { $project: { _id: "$idsToDelete" } }
2157
+ ]).toArray();
2158
+ if (idsToDelete.length === 0) {
2159
+ this.logger?.debug?.("No duplicates to delete after aggregation");
2160
+ return;
2161
+ }
2162
+ const deleteResult = await collection.deleteMany({
2163
+ _id: { $in: idsToDelete.map((d) => d._id) }
2164
+ });
2165
+ this.logger?.info?.(`Deduplication complete: removed ${deleteResult.deletedCount} duplicate spans`);
2166
+ } catch (error) {
2167
+ this.logger?.warn?.("Failed to deduplicate spans:", error);
2168
+ }
2169
+ }
1956
2170
  async dangerouslyClearAll() {
1957
2171
  const collection = await this.getCollection(storage.TABLE_SPANS);
1958
2172
  await collection.deleteMany({});
@@ -2233,7 +2447,7 @@ var ObservabilityMongoDB = class _ObservabilityMongoDB extends storage.Observabi
2233
2447
  // No children with errors
2234
2448
  }
2235
2449
  ];
2236
- const countResult = await collection.aggregate([...pipeline, { $count: "total" }]).toArray();
2450
+ const countResult = await collection.aggregate([...pipeline, { $count: "total" }], { allowDiskUse: true }).toArray();
2237
2451
  const count2 = countResult[0]?.total || 0;
2238
2452
  if (count2 === 0) {
2239
2453
  return {
@@ -2270,7 +2484,7 @@ var ObservabilityMongoDB = class _ObservabilityMongoDB extends storage.Observabi
2270
2484
  { $project: { _errorSpans: 0 } }
2271
2485
  ];
2272
2486
  }
2273
- const spans2 = await collection.aggregate(aggregationPipeline).toArray();
2487
+ const spans2 = await collection.aggregate(aggregationPipeline, { allowDiskUse: true }).toArray();
2274
2488
  return {
2275
2489
  pagination: {
2276
2490
  total: count2,
@@ -2296,18 +2510,21 @@ var ObservabilityMongoDB = class _ObservabilityMongoDB extends storage.Observabi
2296
2510
  let spans;
2297
2511
  if (sortField === "endedAt") {
2298
2512
  const nullSortValue = sortDirection === -1 ? 0 : 1;
2299
- spans = await collection.aggregate([
2300
- { $match: mongoFilter },
2301
- {
2302
- $addFields: {
2303
- _endedAtNull: { $cond: [{ $eq: ["$endedAt", null] }, nullSortValue, sortDirection === -1 ? 1 : 0] }
2304
- }
2305
- },
2306
- { $sort: { _endedAtNull: 1, [sortField]: sortDirection } },
2307
- { $skip: page * perPage },
2308
- { $limit: perPage },
2309
- { $project: { _endedAtNull: 0 } }
2310
- ]).toArray();
2513
+ spans = await collection.aggregate(
2514
+ [
2515
+ { $match: mongoFilter },
2516
+ {
2517
+ $addFields: {
2518
+ _endedAtNull: { $cond: [{ $eq: ["$endedAt", null] }, nullSortValue, sortDirection === -1 ? 1 : 0] }
2519
+ }
2520
+ },
2521
+ { $sort: { _endedAtNull: 1, [sortField]: sortDirection } },
2522
+ { $skip: page * perPage },
2523
+ { $limit: perPage },
2524
+ { $project: { _endedAtNull: 0 } }
2525
+ ],
2526
+ { allowDiskUse: true }
2527
+ ).toArray();
2311
2528
  } else {
2312
2529
  spans = await collection.find(mongoFilter).sort({ [sortField]: sortDirection }).skip(page * perPage).limit(perPage).toArray();
2313
2530
  }
@@ -3081,7 +3298,7 @@ var WorkflowsStorageMongoDB = class _WorkflowsStorageMongoDB extends storage.Wor
3081
3298
  };
3082
3299
 
3083
3300
  // src/storage/index.ts
3084
- var MongoDBStore = class extends storage.MastraStorage {
3301
+ var MongoDBStore = class extends storage.MastraCompositeStore {
3085
3302
  #connector;
3086
3303
  stores;
3087
3304
  constructor(config) {