@mastra/libsql 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/libsql
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
@@ -36,4 +36,4 @@ docs/
36
36
  ## Version
37
37
 
38
38
  Package: @mastra/libsql
39
- Version: 1.0.0-beta.13
39
+ Version: 1.0.0-beta.14
@@ -5,7 +5,7 @@ description: Documentation for @mastra/libsql. Includes links to type definition
5
5
 
6
6
  # @mastra/libsql Documentation
7
7
 
8
- > **Version**: 1.0.0-beta.13
8
+ > **Version**: 1.0.0-beta.14
9
9
  > **Package**: @mastra/libsql
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/libsql",
4
4
  "exports": {},
5
5
  "modules": {}
@@ -63,17 +63,17 @@ This is useful when all primitives share the same storage backend and have simil
63
63
 
64
64
  #### Composite storage
65
65
 
66
- Add storage to your Mastra instance using `MastraStorage` and configure individual storage domains to use different storage providers.
66
+ Add storage to your Mastra instance using `MastraCompositeStore` and configure individual storage domains to use different storage providers.
67
67
 
68
68
  ```typescript title="src/mastra/index.ts"
69
69
  import { Mastra } from "@mastra/core";
70
- import { MastraStorage } from "@mastra/core/storage";
70
+ import { MastraCompositeStore } from "@mastra/core/storage";
71
71
  import { MemoryLibSQL } from "@mastra/libsql";
72
72
  import { WorkflowsPG } from "@mastra/pg";
73
73
  import { ObservabilityStorageClickhouse } from "@mastra/clickhouse";
74
74
 
75
75
  export const mastra = new Mastra({
76
- storage: new MastraStorage({
76
+ storage: new MastraCompositeStore({
77
77
  id: "composite",
78
78
  domains: {
79
79
  memory: new MemoryLibSQL({ url: "file:./memory.db" }),
@@ -9,11 +9,11 @@
9
9
 
10
10
  > Documentation for combining multiple storage backends in Mastra.
11
11
 
12
- `MastraStorage` can compose storage domains from different providers. Use it when you need different databases for different purposes. For example, use LibSQL for memory and PostgreSQL for workflows.
12
+ `MastraCompositeStore` can compose storage domains from different providers. Use it when you need different databases for different purposes. For example, use LibSQL for memory and PostgreSQL for workflows.
13
13
 
14
14
  ## Installation
15
15
 
16
- `MastraStorage` is included in `@mastra/core`:
16
+ `MastraCompositeStore` is included in `@mastra/core`:
17
17
 
18
18
  ```bash
19
19
  npm install @mastra/core@beta
@@ -44,13 +44,13 @@ Mastra organizes storage into five specialized domains, each handling a specific
44
44
  Import domain classes directly from each store package and compose them:
45
45
 
46
46
  ```typescript title="src/mastra/index.ts"
47
- import { MastraStorage } from "@mastra/core/storage";
47
+ import { MastraCompositeStore } from "@mastra/core/storage";
48
48
  import { WorkflowsPG, ScoresPG } from "@mastra/pg";
49
49
  import { MemoryLibSQL } from "@mastra/libsql";
50
50
  import { Mastra } from "@mastra/core";
51
51
 
52
52
  export const mastra = new Mastra({
53
- storage: new MastraStorage({
53
+ storage: new MastraCompositeStore({
54
54
  id: "composite",
55
55
  domains: {
56
56
  memory: new MemoryLibSQL({ url: "file:./local.db" }),
@@ -66,7 +66,7 @@ export const mastra = new Mastra({
66
66
  Use `default` to specify a fallback storage, then override specific domains:
67
67
 
68
68
  ```typescript title="src/mastra/index.ts"
69
- import { MastraStorage } from "@mastra/core/storage";
69
+ import { MastraCompositeStore } from "@mastra/core/storage";
70
70
  import { PostgresStore } from "@mastra/pg";
71
71
  import { MemoryLibSQL } from "@mastra/libsql";
72
72
  import { Mastra } from "@mastra/core";
@@ -77,7 +77,7 @@ const pgStore = new PostgresStore({
77
77
  });
78
78
 
79
79
  export const mastra = new Mastra({
80
- storage: new MastraStorage({
80
+ storage: new MastraCompositeStore({
81
81
  id: "composite",
82
82
  default: pgStore,
83
83
  domains: {
@@ -91,14 +91,14 @@ export const mastra = new Mastra({
91
91
 
92
92
  ## Initialization
93
93
 
94
- `MastraStorage` initializes each configured domain independently. When passed to the Mastra class, `init()` is called automatically:
94
+ `MastraCompositeStore` initializes each configured domain independently. When passed to the Mastra class, `init()` is called automatically:
95
95
 
96
96
  ```typescript title="src/mastra/index.ts"
97
- import { MastraStorage } from "@mastra/core/storage";
97
+ import { MastraCompositeStore } from "@mastra/core/storage";
98
98
  import { MemoryPG, WorkflowsPG, ScoresPG } from "@mastra/pg";
99
99
  import { Mastra } from "@mastra/core";
100
100
 
101
- const storage = new MastraStorage({
101
+ const storage = new MastraCompositeStore({
102
102
  id: "composite",
103
103
  domains: {
104
104
  memory: new MemoryPG({ connectionString: process.env.DATABASE_URL }),
@@ -115,10 +115,10 @@ export const mastra = new Mastra({
115
115
  If using storage directly, call `init()` explicitly:
116
116
 
117
117
  ```typescript
118
- import { MastraStorage } from "@mastra/core/storage";
118
+ import { MastraCompositeStore } from "@mastra/core/storage";
119
119
  import { MemoryPG } from "@mastra/pg";
120
120
 
121
- const storage = new MastraStorage({
121
+ const storage = new MastraCompositeStore({
122
122
  id: "composite",
123
123
  domains: {
124
124
  memory: new MemoryPG({ connectionString: process.env.DATABASE_URL }),
@@ -139,11 +139,11 @@ const thread = await memoryStore?.getThreadById({ threadId: "..." });
139
139
  Use a local database for development while keeping production data in a managed service:
140
140
 
141
141
  ```typescript
142
- import { MastraStorage } from "@mastra/core/storage";
142
+ import { MastraCompositeStore } from "@mastra/core/storage";
143
143
  import { MemoryPG, WorkflowsPG, ScoresPG } from "@mastra/pg";
144
144
  import { MemoryLibSQL } from "@mastra/libsql";
145
145
 
146
- const storage = new MastraStorage({
146
+ const storage = new MastraCompositeStore({
147
147
  id: "composite",
148
148
  domains: {
149
149
  // Use local SQLite for development, PostgreSQL for production
@@ -162,11 +162,11 @@ const storage = new MastraStorage({
162
162
  Use a time-series database for traces while keeping other data in PostgreSQL:
163
163
 
164
164
  ```typescript
165
- import { MastraStorage } from "@mastra/core/storage";
165
+ import { MastraCompositeStore } from "@mastra/core/storage";
166
166
  import { MemoryPG, WorkflowsPG, ScoresPG } from "@mastra/pg";
167
167
  import { ObservabilityStorageClickhouse } from "@mastra/clickhouse";
168
168
 
169
- const storage = new MastraStorage({
169
+ const storage = new MastraCompositeStore({
170
170
  id: "composite",
171
171
  domains: {
172
172
  memory: new MemoryPG({ connectionString: process.env.DATABASE_URL }),
package/dist/index.cjs CHANGED
@@ -244,10 +244,10 @@ var FILTER_OPERATORS = {
244
244
  };
245
245
  },
246
246
  // Element Operators
247
- $exists: (key) => {
247
+ $exists: (key, value) => {
248
248
  const jsonPath = getJsonPath(key);
249
249
  return {
250
- sql: `json_extract(metadata, ${jsonPath}) IS NOT NULL`,
250
+ sql: value === false ? `json_extract(metadata, ${jsonPath}) IS NULL` : `json_extract(metadata, ${jsonPath}) IS NOT NULL`,
251
251
  needsValue: false
252
252
  };
253
253
  },
@@ -540,7 +540,7 @@ var LibSQLVector = class extends vector.MastraVector {
540
540
  try {
541
541
  return await operation();
542
542
  } catch (error) {
543
- if (error.code === "SQLITE_BUSY" || error.message && error.message.toLowerCase().includes("database is locked")) {
543
+ if (error.code === "SQLITE_BUSY" || error.code === "SQLITE_LOCKED" || error.code === "SQLITE_LOCKED_SHAREDCACHE" || error.message && error.message.toLowerCase().includes("database is locked") || error.message && error.message.toLowerCase().includes("database table is locked")) {
544
544
  attempts++;
545
545
  if (attempts >= this.maxRetries) {
546
546
  this.logger.error(
@@ -574,22 +574,14 @@ var LibSQLVector = class extends vector.MastraVector {
574
574
  minScore = -1
575
575
  // Default to -1 to include all results (cosine similarity ranges from -1 to 1)
576
576
  }) {
577
- try {
578
- if (!Number.isInteger(topK) || topK <= 0) {
579
- throw new Error("topK must be a positive integer");
580
- }
581
- if (!Array.isArray(queryVector) || !queryVector.every((x) => typeof x === "number" && Number.isFinite(x))) {
582
- throw new Error("queryVector must be an array of finite numbers");
583
- }
584
- } catch (error$1) {
585
- throw new error.MastraError(
586
- {
587
- id: storage.createVectorErrorId("LIBSQL", "QUERY", "INVALID_ARGS"),
588
- domain: error.ErrorDomain.STORAGE,
589
- category: error.ErrorCategory.USER
590
- },
591
- error$1
592
- );
577
+ vector.validateTopK("LIBSQL", topK);
578
+ if (!Array.isArray(queryVector) || !queryVector.every((x) => typeof x === "number" && Number.isFinite(x))) {
579
+ throw new error.MastraError({
580
+ id: storage.createVectorErrorId("LIBSQL", "QUERY", "INVALID_ARGS"),
581
+ domain: error.ErrorDomain.STORAGE,
582
+ category: error.ErrorCategory.USER,
583
+ details: { message: "queryVector must be an array of finite numbers" }
584
+ });
593
585
  }
594
586
  try {
595
587
  const parsedIndexName = utils.parseSqlIdentifier(indexName, "index name");
@@ -649,6 +641,7 @@ var LibSQLVector = class extends vector.MastraVector {
649
641
  }
650
642
  }
651
643
  async doUpsert({ indexName, vectors, metadata, ids }) {
644
+ vector.validateUpsertInput("LIBSQL", vectors, metadata, ids);
652
645
  const tx = await this.turso.transaction("write");
653
646
  try {
654
647
  const parsedIndexName = utils.parseSqlIdentifier(indexName, "index name");
@@ -1712,6 +1705,9 @@ var LibSQLDB = class extends base.MastraBase {
1712
1705
  if (tableName === storage.TABLE_WORKFLOW_SNAPSHOT) {
1713
1706
  tableConstraints.push("UNIQUE (workflow_name, run_id)");
1714
1707
  }
1708
+ if (tableName === storage.TABLE_SPANS) {
1709
+ tableConstraints.push("UNIQUE (spanId, traceId)");
1710
+ }
1715
1711
  const allDefinitions = [...columnDefinitions, ...tableConstraints].join(",\n ");
1716
1712
  const sql = `CREATE TABLE IF NOT EXISTS ${parsedTableName} (
1717
1713
  ${allDefinitions}
@@ -1722,6 +1718,9 @@ var LibSQLDB = class extends base.MastraBase {
1722
1718
  await this.migrateSpansTable();
1723
1719
  }
1724
1720
  } catch (error$1) {
1721
+ if (error$1 instanceof error.MastraError) {
1722
+ throw error$1;
1723
+ }
1725
1724
  throw new error.MastraError(
1726
1725
  {
1727
1726
  id: storage.createStorageErrorId("LIBSQL", "CREATE_TABLE", "FAILED"),
@@ -1735,7 +1734,7 @@ var LibSQLDB = class extends base.MastraBase {
1735
1734
  }
1736
1735
  /**
1737
1736
  * Migrates the spans table schema from OLD_SPAN_SCHEMA to current SPAN_SCHEMA.
1738
- * This adds new columns that don't exist in old schema.
1737
+ * This adds new columns that don't exist in old schema and ensures required indexes exist.
1739
1738
  */
1740
1739
  async migrateSpansTable() {
1741
1740
  const schema = storage.TABLE_SCHEMAS[storage.TABLE_SPANS];
@@ -1749,9 +1748,203 @@ var LibSQLDB = class extends base.MastraBase {
1749
1748
  this.logger.debug(`LibSQLDB: Added column '${columnName}' to ${storage.TABLE_SPANS}`);
1750
1749
  }
1751
1750
  }
1751
+ const indexExists = await this.spansUniqueIndexExists();
1752
+ if (!indexExists) {
1753
+ const duplicateInfo = await this.checkForDuplicateSpans();
1754
+ if (duplicateInfo.hasDuplicates) {
1755
+ const errorMessage = `
1756
+ ===========================================================================
1757
+ MIGRATION REQUIRED: Duplicate spans detected in ${storage.TABLE_SPANS}
1758
+ ===========================================================================
1759
+
1760
+ Found ${duplicateInfo.duplicateCount} duplicate (traceId, spanId) combinations.
1761
+
1762
+ The spans table requires a unique constraint on (traceId, spanId), but your
1763
+ database contains duplicate entries that must be resolved first.
1764
+
1765
+ To fix this, run the manual migration command:
1766
+
1767
+ npx mastra migrate
1768
+
1769
+ This command will:
1770
+ 1. Remove duplicate spans (keeping the most complete/recent version)
1771
+ 2. Add the required unique constraint
1772
+
1773
+ Note: This migration may take some time for large tables.
1774
+ ===========================================================================
1775
+ `;
1776
+ throw new error.MastraError({
1777
+ id: storage.createStorageErrorId("LIBSQL", "MIGRATION_REQUIRED", "DUPLICATE_SPANS"),
1778
+ domain: error.ErrorDomain.STORAGE,
1779
+ category: error.ErrorCategory.USER,
1780
+ text: errorMessage
1781
+ });
1782
+ } else {
1783
+ await this.client.execute(
1784
+ `CREATE UNIQUE INDEX IF NOT EXISTS "mastra_ai_spans_spanid_traceid_idx" ON "${storage.TABLE_SPANS}" ("spanId", "traceId")`
1785
+ );
1786
+ this.logger.debug(`LibSQLDB: Created unique index on (spanId, traceId) for ${storage.TABLE_SPANS}`);
1787
+ }
1788
+ }
1752
1789
  this.logger.info(`LibSQLDB: Migration completed for ${storage.TABLE_SPANS}`);
1790
+ } catch (error$1) {
1791
+ if (error$1 instanceof error.MastraError) {
1792
+ throw error$1;
1793
+ }
1794
+ this.logger.warn(`LibSQLDB: Failed to migrate spans table ${storage.TABLE_SPANS}:`, error$1);
1795
+ }
1796
+ }
1797
+ /**
1798
+ * Checks if the unique index on (spanId, traceId) already exists on the spans table.
1799
+ * Used to skip deduplication when the index already exists (migration already complete).
1800
+ */
1801
+ async spansUniqueIndexExists() {
1802
+ try {
1803
+ const result = await this.client.execute(
1804
+ `SELECT 1 FROM sqlite_master WHERE type = 'index' AND name = 'mastra_ai_spans_spanid_traceid_idx'`
1805
+ );
1806
+ return (result.rows?.length ?? 0) > 0;
1807
+ } catch {
1808
+ return false;
1809
+ }
1810
+ }
1811
+ /**
1812
+ * Checks for duplicate (traceId, spanId) combinations in the spans table.
1813
+ * Returns information about duplicates for logging/CLI purposes.
1814
+ */
1815
+ async checkForDuplicateSpans() {
1816
+ try {
1817
+ const result = await this.client.execute(`
1818
+ SELECT COUNT(*) as duplicate_count FROM (
1819
+ SELECT "spanId", "traceId"
1820
+ FROM "${storage.TABLE_SPANS}"
1821
+ GROUP BY "spanId", "traceId"
1822
+ HAVING COUNT(*) > 1
1823
+ )
1824
+ `);
1825
+ const duplicateCount = Number(result.rows?.[0]?.duplicate_count ?? 0);
1826
+ return {
1827
+ hasDuplicates: duplicateCount > 0,
1828
+ duplicateCount
1829
+ };
1753
1830
  } catch (error) {
1754
- this.logger.warn(`LibSQLDB: Failed to migrate spans table ${storage.TABLE_SPANS}:`, error);
1831
+ this.logger.debug(`LibSQLDB: Could not check for duplicates: ${error}`);
1832
+ return { hasDuplicates: false, duplicateCount: 0 };
1833
+ }
1834
+ }
1835
+ /**
1836
+ * Manually run the spans migration to deduplicate and add the unique constraint.
1837
+ * This is intended to be called from the CLI when duplicates are detected.
1838
+ *
1839
+ * @returns Migration result with status and details
1840
+ */
1841
+ async migrateSpans() {
1842
+ const indexExists = await this.spansUniqueIndexExists();
1843
+ if (indexExists) {
1844
+ return {
1845
+ success: true,
1846
+ alreadyMigrated: true,
1847
+ duplicatesRemoved: 0,
1848
+ message: `Migration already complete. Unique index exists on ${storage.TABLE_SPANS}.`
1849
+ };
1850
+ }
1851
+ const duplicateInfo = await this.checkForDuplicateSpans();
1852
+ if (duplicateInfo.hasDuplicates) {
1853
+ this.logger.info(
1854
+ `Found ${duplicateInfo.duplicateCount} duplicate (traceId, spanId) combinations. Starting deduplication...`
1855
+ );
1856
+ await this.deduplicateSpans();
1857
+ } else {
1858
+ this.logger.info(`No duplicate spans found.`);
1859
+ }
1860
+ await this.client.execute(
1861
+ `CREATE UNIQUE INDEX IF NOT EXISTS "mastra_ai_spans_spanid_traceid_idx" ON "${storage.TABLE_SPANS}" ("spanId", "traceId")`
1862
+ );
1863
+ return {
1864
+ success: true,
1865
+ alreadyMigrated: false,
1866
+ duplicatesRemoved: duplicateInfo.duplicateCount,
1867
+ message: duplicateInfo.hasDuplicates ? `Migration complete. Removed duplicates and added unique index to ${storage.TABLE_SPANS}.` : `Migration complete. Added unique index to ${storage.TABLE_SPANS}.`
1868
+ };
1869
+ }
1870
+ /**
1871
+ * Check migration status for the spans table.
1872
+ * Returns information about whether migration is needed.
1873
+ */
1874
+ async checkSpansMigrationStatus() {
1875
+ const indexExists = await this.spansUniqueIndexExists();
1876
+ if (indexExists) {
1877
+ return {
1878
+ needsMigration: false,
1879
+ hasDuplicates: false,
1880
+ duplicateCount: 0,
1881
+ constraintExists: true,
1882
+ tableName: storage.TABLE_SPANS
1883
+ };
1884
+ }
1885
+ const duplicateInfo = await this.checkForDuplicateSpans();
1886
+ return {
1887
+ needsMigration: true,
1888
+ hasDuplicates: duplicateInfo.hasDuplicates,
1889
+ duplicateCount: duplicateInfo.duplicateCount,
1890
+ constraintExists: false,
1891
+ tableName: storage.TABLE_SPANS
1892
+ };
1893
+ }
1894
+ /**
1895
+ * Deduplicates spans table by removing duplicate (spanId, traceId) combinations.
1896
+ * Keeps the "best" record for each duplicate group based on:
1897
+ * 1. Completed spans (endedAt IS NOT NULL) over incomplete ones
1898
+ * 2. Most recently updated (updatedAt DESC)
1899
+ * 3. Most recently created (createdAt DESC) as tiebreaker
1900
+ */
1901
+ async deduplicateSpans() {
1902
+ try {
1903
+ const duplicateCheck = await this.client.execute(`
1904
+ SELECT COUNT(*) as duplicate_count FROM (
1905
+ SELECT "spanId", "traceId"
1906
+ FROM "${storage.TABLE_SPANS}"
1907
+ GROUP BY "spanId", "traceId"
1908
+ HAVING COUNT(*) > 1
1909
+ )
1910
+ `);
1911
+ const duplicateCount = Number(duplicateCheck.rows?.[0]?.duplicate_count ?? 0);
1912
+ if (duplicateCount === 0) {
1913
+ this.logger.debug(`LibSQLDB: No duplicate spans found, skipping deduplication`);
1914
+ return;
1915
+ }
1916
+ this.logger.warn(`LibSQLDB: Found ${duplicateCount} duplicate (spanId, traceId) combinations, deduplicating...`);
1917
+ const deleteResult = await this.client.execute(`
1918
+ DELETE FROM "${storage.TABLE_SPANS}"
1919
+ WHERE rowid NOT IN (
1920
+ SELECT MIN(best_rowid) FROM (
1921
+ SELECT
1922
+ rowid as best_rowid,
1923
+ "spanId",
1924
+ "traceId",
1925
+ ROW_NUMBER() OVER (
1926
+ PARTITION BY "spanId", "traceId"
1927
+ ORDER BY
1928
+ CASE WHEN "endedAt" IS NOT NULL THEN 0 ELSE 1 END,
1929
+ "updatedAt" DESC,
1930
+ "createdAt" DESC
1931
+ ) as rn
1932
+ FROM "${storage.TABLE_SPANS}"
1933
+ ) ranked
1934
+ WHERE rn = 1
1935
+ GROUP BY "spanId", "traceId"
1936
+ )
1937
+ AND ("spanId", "traceId") IN (
1938
+ SELECT "spanId", "traceId"
1939
+ FROM "${storage.TABLE_SPANS}"
1940
+ GROUP BY "spanId", "traceId"
1941
+ HAVING COUNT(*) > 1
1942
+ )
1943
+ `);
1944
+ const deletedCount = deleteResult.rowsAffected ?? 0;
1945
+ this.logger.warn(`LibSQLDB: Deleted ${deletedCount} duplicate span records`);
1946
+ } catch (error) {
1947
+ this.logger.warn(`LibSQLDB: Failed to deduplicate spans:`, error);
1755
1948
  }
1756
1949
  }
1757
1950
  /**
@@ -3021,6 +3214,22 @@ var ObservabilityLibSQL = class extends storage.ObservabilityStorage {
3021
3214
  async dangerouslyClearAll() {
3022
3215
  await this.#db.deleteData({ tableName: storage.TABLE_SPANS });
3023
3216
  }
3217
+ /**
3218
+ * Manually run the spans migration to deduplicate and add the unique constraint.
3219
+ * This is intended to be called from the CLI when duplicates are detected.
3220
+ *
3221
+ * @returns Migration result with status and details
3222
+ */
3223
+ async migrateSpans() {
3224
+ return this.#db.migrateSpans();
3225
+ }
3226
+ /**
3227
+ * Check migration status for the spans table.
3228
+ * Returns information about whether migration is needed.
3229
+ */
3230
+ async checkSpansMigrationStatus() {
3231
+ return this.#db.checkSpansMigrationStatus();
3232
+ }
3024
3233
  get tracingStrategy() {
3025
3234
  return {
3026
3235
  preferred: "batch-with-updates",
@@ -4056,7 +4265,7 @@ var WorkflowsLibSQL = class extends storage.WorkflowsStorage {
4056
4265
  };
4057
4266
 
4058
4267
  // src/storage/index.ts
4059
- var LibSQLStore = class extends storage.MastraStorage {
4268
+ var LibSQLStore = class extends storage.MastraCompositeStore {
4060
4269
  client;
4061
4270
  maxRetries;
4062
4271
  initialBackoffMs;