@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.
@@ -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 LibSQLQueryParams extends QueryVectorParams {
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
- connectionUrl: string;
35
- authToken?: string;
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
- void this.turso.execute({
50
- sql: 'PRAGMA journal_mode=WAL;',
51
- args: {},
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
- }: LibSQLQueryParams): Promise<QueryResult[]> {
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
- WITH vector_scores AS (
88
- SELECT
89
- vector_id as id,
90
- (1-vector_distance_cos(embedding, '${vectorStr}')) as score,
91
- metadata
92
- ${includeVector ? ', vector_extract(embedding) as embedding' : ''}
93
- FROM ${parsedIndexName}
94
- ${filterQuery}
95
- )
96
- SELECT *
97
- FROM vector_scores
98
- WHERE score > ?
99
- ORDER BY score DESC
100
- LIMIT ?`;
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
- async upsert({ indexName, vectors, metadata, ids }: UpsertVectorParams): Promise<string[]> {
119
- const tx = await this.turso.transaction('write');
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
- INSERT INTO ${parsedIndexName} (vector_id, embedding, metadata)
128
- VALUES (?, vector32(?), ?)
129
- ON CONFLICT(vector_id) DO UPDATE SET
130
- embedding = vector32(?),
131
- metadata = ?
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
- async createIndex({ indexName, dimension }: CreateIndexParams): Promise<void> {
173
- try {
174
- // Validate inputs
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
- await this.turso.execute({
194
- sql: `
195
- CREATE INDEX IF NOT EXISTS ${parsedIndexName}_vector_idx
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
- async deleteIndex({ indexName }: DeleteIndexParams): Promise<void> {
209
- try {
210
- const parsedIndexName = parseSqlIdentifier(indexName, 'index name');
211
- // Drop the table
212
- await this.turso.execute({
213
- sql: `DROP TABLE IF EXISTS ${parsedIndexName}`,
214
- args: [],
215
- });
216
- } catch (error: any) {
217
- console.error('Failed to delete vector table:', error);
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
- async updateVector({ indexName, id, update }: UpdateVectorParams): Promise<void> {
304
- try {
305
- const parsedIndexName = parseSqlIdentifier(indexName, 'index name');
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
- if (update.metadata) {
315
- updates.push('metadata = ?');
316
- args.push(JSON.stringify(update.metadata));
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
- if (updates.length === 0) {
320
- throw new Error('No updates provided');
321
- }
349
+ if (update.vector) {
350
+ updates.push('embedding = vector32(?)');
351
+ args.push(JSON.stringify(update.vector));
352
+ }
322
353
 
323
- args.push(id);
354
+ if (update.metadata) {
355
+ updates.push('metadata = ?');
356
+ args.push(JSON.stringify(update.metadata));
357
+ }
324
358
 
325
- const query = `
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
- await this.turso.execute({
332
- sql: query,
333
- args,
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
- async deleteVector({ indexName, id }: DeleteVectorParams): Promise<void> {
348
- try {
349
- const parsedIndexName = parseSqlIdentifier(indexName, 'index name');
350
- await this.turso.execute({
351
- sql: `DELETE FROM ${parsedIndexName} WHERE vector_id = ?`,
352
- args: [id],
353
- });
354
- } catch (error: any) {
355
- throw new Error(`Failed to delete vector by id: ${id} for index: ${indexName}: ${error.message}`);
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 truncateIndex({ indexName }: DeleteIndexParams): Promise<void> {
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: [],