@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 +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/02-storage.md +3 -3
- package/dist/docs/storage/01-reference.md +15 -15
- package/dist/index.cjs +231 -22
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +232 -23
- package/dist/index.js.map +1 -1
- package/dist/storage/db/index.d.ts +42 -1
- 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/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
|
package/dist/docs/README.md
CHANGED
package/dist/docs/SKILL.md
CHANGED
|
@@ -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 `
|
|
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 {
|
|
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
|
|
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
|
-
`
|
|
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
|
-
`
|
|
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 {
|
|
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
|
|
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 {
|
|
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
|
|
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
|
-
`
|
|
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 {
|
|
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
|
|
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 {
|
|
118
|
+
import { MastraCompositeStore } from "@mastra/core/storage";
|
|
119
119
|
import { MemoryPG } from "@mastra/pg";
|
|
120
120
|
|
|
121
|
-
const storage = new
|
|
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 {
|
|
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
|
|
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 {
|
|
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
|
|
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
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
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.
|
|
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.
|
|
4268
|
+
var LibSQLStore = class extends storage.MastraCompositeStore {
|
|
4060
4269
|
client;
|
|
4061
4270
|
maxRetries;
|
|
4062
4271
|
initialBackoffMs;
|