@mastra/libsql 0.10.0 → 0.10.1-alpha.1
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/.turbo/turbo-build.log +7 -7
- package/CHANGELOG.md +18 -0
- package/dist/_tsup-dts-rollup.d.cts +60 -20
- package/dist/_tsup-dts-rollup.d.ts +60 -20
- package/dist/index.cjs +190 -127
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +190 -127
- package/package.json +5 -5
- package/src/storage/index.test.ts +0 -1
- package/src/storage/index.ts +102 -31
- package/src/vector/index.ts +166 -128
package/src/vector/index.ts
CHANGED
|
@@ -18,24 +18,41 @@ import type { VectorFilter } from '@mastra/core/vector/filter';
|
|
|
18
18
|
import { LibSQLFilterTranslator } from './filter';
|
|
19
19
|
import { buildFilterQuery } from './sql-builder';
|
|
20
20
|
|
|
21
|
-
interface
|
|
21
|
+
interface LibSQLQueryVectorParams extends QueryVectorParams {
|
|
22
22
|
minScore?: number;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
export interface LibSQLVectorConfig {
|
|
26
|
+
connectionUrl: string;
|
|
27
|
+
authToken?: string;
|
|
28
|
+
syncUrl?: string;
|
|
29
|
+
syncInterval?: number;
|
|
30
|
+
/**
|
|
31
|
+
* Maximum number of retries for write operations if an SQLITE_BUSY error occurs.
|
|
32
|
+
* @default 5
|
|
33
|
+
*/
|
|
34
|
+
maxRetries?: number;
|
|
35
|
+
/**
|
|
36
|
+
* Initial backoff time in milliseconds for retrying write operations on SQLITE_BUSY.
|
|
37
|
+
* The backoff time will double with each retry (exponential backoff).
|
|
38
|
+
* @default 100
|
|
39
|
+
*/
|
|
40
|
+
initialBackoffMs?: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
25
43
|
export class LibSQLVector extends MastraVector {
|
|
26
44
|
private turso: TursoClient;
|
|
45
|
+
private readonly maxRetries: number;
|
|
46
|
+
private readonly initialBackoffMs: number;
|
|
27
47
|
|
|
28
48
|
constructor({
|
|
29
49
|
connectionUrl,
|
|
30
50
|
authToken,
|
|
31
51
|
syncUrl,
|
|
32
52
|
syncInterval,
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
syncUrl?: string;
|
|
37
|
-
syncInterval?: number;
|
|
38
|
-
}) {
|
|
53
|
+
maxRetries = 5,
|
|
54
|
+
initialBackoffMs = 100,
|
|
55
|
+
}: LibSQLVectorConfig) {
|
|
39
56
|
super();
|
|
40
57
|
|
|
41
58
|
this.turso = createClient({
|
|
@@ -44,13 +61,51 @@ export class LibSQLVector extends MastraVector {
|
|
|
44
61
|
authToken,
|
|
45
62
|
syncInterval,
|
|
46
63
|
});
|
|
64
|
+
this.maxRetries = maxRetries;
|
|
65
|
+
this.initialBackoffMs = initialBackoffMs;
|
|
47
66
|
|
|
48
67
|
if (connectionUrl.includes(`file:`) || connectionUrl.includes(`:memory:`)) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
68
|
+
this.turso
|
|
69
|
+
.execute('PRAGMA journal_mode=WAL;')
|
|
70
|
+
.then(() => this.logger.debug('LibSQLStore: PRAGMA journal_mode=WAL set.'))
|
|
71
|
+
.catch(err => this.logger.warn('LibSQLStore: Failed to set PRAGMA journal_mode=WAL.', err));
|
|
72
|
+
this.turso
|
|
73
|
+
.execute('PRAGMA busy_timeout = 5000;')
|
|
74
|
+
.then(() => this.logger.debug('LibSQLStore: PRAGMA busy_timeout=5000 set.'))
|
|
75
|
+
.catch(err => this.logger.warn('LibSQLStore: Failed to set PRAGMA busy_timeout=5000.', err));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private async executeWriteOperationWithRetry<T>(operation: () => Promise<T>, isTransaction = false): Promise<T> {
|
|
80
|
+
let attempts = 0;
|
|
81
|
+
let backoff = this.initialBackoffMs;
|
|
82
|
+
while (attempts < this.maxRetries) {
|
|
83
|
+
try {
|
|
84
|
+
return await operation();
|
|
85
|
+
} catch (error: any) {
|
|
86
|
+
if (
|
|
87
|
+
error.code === 'SQLITE_BUSY' ||
|
|
88
|
+
(error.message && error.message.toLowerCase().includes('database is locked'))
|
|
89
|
+
) {
|
|
90
|
+
attempts++;
|
|
91
|
+
if (attempts >= this.maxRetries) {
|
|
92
|
+
this.logger.error(
|
|
93
|
+
`LibSQLVector: Operation failed after ${this.maxRetries} attempts due to: ${error.message}`,
|
|
94
|
+
error,
|
|
95
|
+
);
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
this.logger.warn(
|
|
99
|
+
`LibSQLVector: Attempt ${attempts} failed due to ${isTransaction ? 'transaction ' : ''}database lock. Retrying in ${backoff}ms...`,
|
|
100
|
+
);
|
|
101
|
+
await new Promise(resolve => setTimeout(resolve, backoff));
|
|
102
|
+
backoff *= 2;
|
|
103
|
+
} else {
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
53
107
|
}
|
|
108
|
+
throw new Error('LibSQLVector: Max retries reached, but no error was re-thrown from the loop.');
|
|
54
109
|
}
|
|
55
110
|
|
|
56
111
|
transformFilter(filter?: VectorFilter) {
|
|
@@ -65,7 +120,7 @@ export class LibSQLVector extends MastraVector {
|
|
|
65
120
|
filter,
|
|
66
121
|
includeVector = false,
|
|
67
122
|
minScore = 0,
|
|
68
|
-
}:
|
|
123
|
+
}: LibSQLQueryVectorParams): Promise<QueryResult[]> {
|
|
69
124
|
try {
|
|
70
125
|
if (!Number.isInteger(topK) || topK <= 0) {
|
|
71
126
|
throw new Error('topK must be a positive integer');
|
|
@@ -84,20 +139,20 @@ export class LibSQLVector extends MastraVector {
|
|
|
84
139
|
filterValues.push(topK);
|
|
85
140
|
|
|
86
141
|
const query = `
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
142
|
+
WITH vector_scores AS (
|
|
143
|
+
SELECT
|
|
144
|
+
vector_id as id,
|
|
145
|
+
(1-vector_distance_cos(embedding, '${vectorStr}')) as score,
|
|
146
|
+
metadata
|
|
147
|
+
${includeVector ? ', vector_extract(embedding) as embedding' : ''}
|
|
148
|
+
FROM ${parsedIndexName}
|
|
149
|
+
${filterQuery}
|
|
150
|
+
)
|
|
151
|
+
SELECT *
|
|
152
|
+
FROM vector_scores
|
|
153
|
+
WHERE score > ?
|
|
154
|
+
ORDER BY score DESC
|
|
155
|
+
LIMIT ?`;
|
|
101
156
|
|
|
102
157
|
const result = await this.turso.execute({
|
|
103
158
|
sql: query,
|
|
@@ -115,32 +170,26 @@ export class LibSQLVector extends MastraVector {
|
|
|
115
170
|
}
|
|
116
171
|
}
|
|
117
172
|
|
|
118
|
-
|
|
119
|
-
|
|
173
|
+
public upsert(args: UpsertVectorParams): Promise<string[]> {
|
|
174
|
+
return this.executeWriteOperationWithRetry(() => this.doUpsert(args), true);
|
|
175
|
+
}
|
|
120
176
|
|
|
177
|
+
private async doUpsert({ indexName, vectors, metadata, ids }: UpsertVectorParams): Promise<string[]> {
|
|
178
|
+
const tx = await this.turso.transaction('write');
|
|
121
179
|
try {
|
|
122
180
|
const parsedIndexName = parseSqlIdentifier(indexName, 'index name');
|
|
123
181
|
const vectorIds = ids || vectors.map(() => crypto.randomUUID());
|
|
124
182
|
|
|
125
183
|
for (let i = 0; i < vectors.length; i++) {
|
|
126
184
|
const query = `
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
// console.log('INSERTQ', query, [
|
|
135
|
-
// vectorIds[i] as InValue,
|
|
136
|
-
// JSON.stringify(vectors[i]),
|
|
137
|
-
// JSON.stringify(metadata?.[i] || {}),
|
|
138
|
-
// JSON.stringify(vectors[i]),
|
|
139
|
-
// JSON.stringify(metadata?.[i] || {}),
|
|
140
|
-
// ]);
|
|
185
|
+
INSERT INTO ${parsedIndexName} (vector_id, embedding, metadata)
|
|
186
|
+
VALUES (?, vector32(?), ?)
|
|
187
|
+
ON CONFLICT(vector_id) DO UPDATE SET
|
|
188
|
+
embedding = vector32(?),
|
|
189
|
+
metadata = ?
|
|
190
|
+
`;
|
|
141
191
|
await tx.execute({
|
|
142
192
|
sql: query,
|
|
143
|
-
// @ts-ignore
|
|
144
193
|
args: [
|
|
145
194
|
vectorIds[i] as InValue,
|
|
146
195
|
JSON.stringify(vectors[i]),
|
|
@@ -150,7 +199,6 @@ export class LibSQLVector extends MastraVector {
|
|
|
150
199
|
],
|
|
151
200
|
});
|
|
152
201
|
}
|
|
153
|
-
|
|
154
202
|
await tx.commit();
|
|
155
203
|
return vectorIds;
|
|
156
204
|
} catch (error) {
|
|
@@ -169,56 +217,45 @@ export class LibSQLVector extends MastraVector {
|
|
|
169
217
|
}
|
|
170
218
|
}
|
|
171
219
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
if (!Number.isInteger(dimension) || dimension <= 0) {
|
|
176
|
-
throw new Error('Dimension must be a positive integer');
|
|
177
|
-
}
|
|
178
|
-
const parsedIndexName = parseSqlIdentifier(indexName, 'index name');
|
|
179
|
-
|
|
180
|
-
// Create the table with explicit schema
|
|
181
|
-
await this.turso.execute({
|
|
182
|
-
sql: `
|
|
183
|
-
CREATE TABLE IF NOT EXISTS ${parsedIndexName} (
|
|
184
|
-
id SERIAL PRIMARY KEY,
|
|
185
|
-
vector_id TEXT UNIQUE NOT NULL,
|
|
186
|
-
embedding F32_BLOB(${dimension}),
|
|
187
|
-
metadata TEXT DEFAULT '{}'
|
|
188
|
-
);
|
|
189
|
-
`,
|
|
190
|
-
args: [],
|
|
191
|
-
});
|
|
220
|
+
public createIndex(args: CreateIndexParams): Promise<void> {
|
|
221
|
+
return this.executeWriteOperationWithRetry(() => this.doCreateIndex(args));
|
|
222
|
+
}
|
|
192
223
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
ON ${parsedIndexName} (libsql_vector_idx(embedding))
|
|
197
|
-
`,
|
|
198
|
-
args: [],
|
|
199
|
-
});
|
|
200
|
-
} catch (error: any) {
|
|
201
|
-
console.error('Failed to create vector table:', error);
|
|
202
|
-
throw error;
|
|
203
|
-
} finally {
|
|
204
|
-
// client.release()
|
|
224
|
+
private async doCreateIndex({ indexName, dimension }: CreateIndexParams): Promise<void> {
|
|
225
|
+
if (!Number.isInteger(dimension) || dimension <= 0) {
|
|
226
|
+
throw new Error('Dimension must be a positive integer');
|
|
205
227
|
}
|
|
228
|
+
const parsedIndexName = parseSqlIdentifier(indexName, 'index name');
|
|
229
|
+
await this.turso.execute({
|
|
230
|
+
sql: `
|
|
231
|
+
CREATE TABLE IF NOT EXISTS ${parsedIndexName} (
|
|
232
|
+
id SERIAL PRIMARY KEY,
|
|
233
|
+
vector_id TEXT UNIQUE NOT NULL,
|
|
234
|
+
embedding F32_BLOB(${dimension}),
|
|
235
|
+
metadata TEXT DEFAULT '{}'
|
|
236
|
+
);
|
|
237
|
+
`,
|
|
238
|
+
args: [],
|
|
239
|
+
});
|
|
240
|
+
await this.turso.execute({
|
|
241
|
+
sql: `
|
|
242
|
+
CREATE INDEX IF NOT EXISTS ${parsedIndexName}_vector_idx
|
|
243
|
+
ON ${parsedIndexName} (libsql_vector_idx(embedding))
|
|
244
|
+
`,
|
|
245
|
+
args: [],
|
|
246
|
+
});
|
|
206
247
|
}
|
|
207
248
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
throw new Error(`Failed to delete vector table: ${error.message}`);
|
|
219
|
-
} finally {
|
|
220
|
-
// client.release()
|
|
221
|
-
}
|
|
249
|
+
public deleteIndex(args: DeleteIndexParams): Promise<void> {
|
|
250
|
+
return this.executeWriteOperationWithRetry(() => this.doDeleteIndex(args));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
private async doDeleteIndex({ indexName }: DeleteIndexParams): Promise<void> {
|
|
254
|
+
const parsedIndexName = parseSqlIdentifier(indexName, 'index name');
|
|
255
|
+
await this.turso.execute({
|
|
256
|
+
sql: `DROP TABLE IF EXISTS ${parsedIndexName}`,
|
|
257
|
+
args: [],
|
|
258
|
+
});
|
|
222
259
|
}
|
|
223
260
|
|
|
224
261
|
async listIndexes(): Promise<string[]> {
|
|
@@ -300,41 +337,38 @@ export class LibSQLVector extends MastraVector {
|
|
|
300
337
|
* @returns A promise that resolves when the update is complete.
|
|
301
338
|
* @throws Will throw an error if no updates are provided or if the update operation fails.
|
|
302
339
|
*/
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
const updates = [];
|
|
307
|
-
const args: InValue[] = [];
|
|
308
|
-
|
|
309
|
-
if (update.vector) {
|
|
310
|
-
updates.push('embedding = vector32(?)');
|
|
311
|
-
args.push(JSON.stringify(update.vector));
|
|
312
|
-
}
|
|
340
|
+
public updateVector(args: UpdateVectorParams): Promise<void> {
|
|
341
|
+
return this.executeWriteOperationWithRetry(() => this.doUpdateVector(args));
|
|
342
|
+
}
|
|
313
343
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
344
|
+
private async doUpdateVector({ indexName, id, update }: UpdateVectorParams): Promise<void> {
|
|
345
|
+
const parsedIndexName = parseSqlIdentifier(indexName, 'index name');
|
|
346
|
+
const updates = [];
|
|
347
|
+
const args: InValue[] = [];
|
|
318
348
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
349
|
+
if (update.vector) {
|
|
350
|
+
updates.push('embedding = vector32(?)');
|
|
351
|
+
args.push(JSON.stringify(update.vector));
|
|
352
|
+
}
|
|
322
353
|
|
|
323
|
-
|
|
354
|
+
if (update.metadata) {
|
|
355
|
+
updates.push('metadata = ?');
|
|
356
|
+
args.push(JSON.stringify(update.metadata));
|
|
357
|
+
}
|
|
324
358
|
|
|
325
|
-
|
|
359
|
+
if (updates.length === 0) {
|
|
360
|
+
throw new Error('No updates provided');
|
|
361
|
+
}
|
|
362
|
+
args.push(id);
|
|
363
|
+
const query = `
|
|
326
364
|
UPDATE ${parsedIndexName}
|
|
327
365
|
SET ${updates.join(', ')}
|
|
328
366
|
WHERE vector_id = ?;
|
|
329
367
|
`;
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
});
|
|
335
|
-
} catch (error: any) {
|
|
336
|
-
throw new Error(`Failed to update vector by id: ${id} for index: ${indexName}: ${error.message}`);
|
|
337
|
-
}
|
|
368
|
+
await this.turso.execute({
|
|
369
|
+
sql: query,
|
|
370
|
+
args,
|
|
371
|
+
});
|
|
338
372
|
}
|
|
339
373
|
|
|
340
374
|
/**
|
|
@@ -344,19 +378,23 @@ export class LibSQLVector extends MastraVector {
|
|
|
344
378
|
* @returns A promise that resolves when the deletion is complete.
|
|
345
379
|
* @throws Will throw an error if the deletion operation fails.
|
|
346
380
|
*/
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
}
|
|
381
|
+
public deleteVector(args: DeleteVectorParams): Promise<void> {
|
|
382
|
+
return this.executeWriteOperationWithRetry(() => this.doDeleteVector(args));
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
private async doDeleteVector({ indexName, id }: DeleteVectorParams): Promise<void> {
|
|
386
|
+
const parsedIndexName = parseSqlIdentifier(indexName, 'index name');
|
|
387
|
+
await this.turso.execute({
|
|
388
|
+
sql: `DELETE FROM ${parsedIndexName} WHERE vector_id = ?`,
|
|
389
|
+
args: [id],
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
public truncateIndex(args: DeleteIndexParams): Promise<void> {
|
|
394
|
+
return this.executeWriteOperationWithRetry(() => this._doTruncateIndex(args));
|
|
357
395
|
}
|
|
358
396
|
|
|
359
|
-
async
|
|
397
|
+
private async _doTruncateIndex({ indexName }: DeleteIndexParams): Promise<void> {
|
|
360
398
|
await this.turso.execute({
|
|
361
399
|
sql: `DELETE FROM ${parseSqlIdentifier(indexName, 'index name')}`,
|
|
362
400
|
args: [],
|