@mastra/libsql 0.13.8-alpha.0 → 0.13.8-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.
@@ -1,506 +0,0 @@
1
- import type { Client, InValue } from '@libsql/client';
2
- import { ErrorCategory, ErrorDomain, MastraError } from '@mastra/core/error';
3
- import { TABLE_WORKFLOW_SNAPSHOT, StoreOperations, TABLE_AI_SPANS } from '@mastra/core/storage';
4
- import type { StorageColumn, TABLE_NAMES } from '@mastra/core/storage';
5
- import { parseSqlIdentifier } from '@mastra/core/utils';
6
- import {
7
- createExecuteWriteOperationWithRetry,
8
- prepareDeleteStatement,
9
- prepareStatement,
10
- prepareUpdateStatement,
11
- } from '../utils';
12
-
13
- export class StoreOperationsLibSQL extends StoreOperations {
14
- private client: Client;
15
- /**
16
- * Maximum number of retries for write operations if an SQLITE_BUSY error occurs.
17
- * @default 5
18
- */
19
- maxRetries: number;
20
- /**
21
- * Initial backoff time in milliseconds for retrying write operations on SQLITE_BUSY.
22
- * The backoff time will double with each retry (exponential backoff).
23
- * @default 100
24
- */
25
- initialBackoffMs: number;
26
-
27
- constructor({
28
- client,
29
- maxRetries,
30
- initialBackoffMs,
31
- }: {
32
- client: Client;
33
- maxRetries?: number;
34
- initialBackoffMs?: number;
35
- }) {
36
- super();
37
- this.client = client;
38
-
39
- this.maxRetries = maxRetries ?? 5;
40
- this.initialBackoffMs = initialBackoffMs ?? 100;
41
- }
42
-
43
- async hasColumn(table: string, column: string): Promise<boolean> {
44
- const result = await this.client.execute({
45
- sql: `PRAGMA table_info(${table})`,
46
- });
47
- return (await result.rows)?.some((row: any) => row.name === column);
48
- }
49
-
50
- private getCreateTableSQL(tableName: TABLE_NAMES, schema: Record<string, StorageColumn>): string {
51
- const parsedTableName = parseSqlIdentifier(tableName, 'table name');
52
- const columns = Object.entries(schema).map(([name, col]) => {
53
- const parsedColumnName = parseSqlIdentifier(name, 'column name');
54
- let type = col.type.toUpperCase();
55
- if (type === 'TEXT') type = 'TEXT';
56
- if (type === 'TIMESTAMP') type = 'TEXT'; // Store timestamps as ISO strings
57
- // if (type === 'BIGINT') type = 'INTEGER';
58
-
59
- const nullable = col.nullable ? '' : 'NOT NULL';
60
- const primaryKey = col.primaryKey ? 'PRIMARY KEY' : '';
61
-
62
- return `${parsedColumnName} ${type} ${nullable} ${primaryKey}`.trim();
63
- });
64
-
65
- // For workflow_snapshot table, create a composite primary key
66
- if (tableName === TABLE_WORKFLOW_SNAPSHOT) {
67
- const stmnt = `CREATE TABLE IF NOT EXISTS ${parsedTableName} (
68
- ${columns.join(',\n')},
69
- PRIMARY KEY (workflow_name, run_id)
70
- )`;
71
- return stmnt;
72
- }
73
-
74
- if (tableName === TABLE_AI_SPANS) {
75
- const stmnt = `CREATE TABLE IF NOT EXISTS ${parsedTableName} (
76
- ${columns.join(',\n')},
77
- PRIMARY KEY (traceId, spanId)
78
- )`;
79
- return stmnt;
80
- }
81
-
82
- return `CREATE TABLE IF NOT EXISTS ${parsedTableName} (${columns.join(', ')})`;
83
- }
84
-
85
- async createTable({
86
- tableName,
87
- schema,
88
- }: {
89
- tableName: TABLE_NAMES;
90
- schema: Record<string, StorageColumn>;
91
- }): Promise<void> {
92
- try {
93
- this.logger.debug(`Creating database table`, { tableName, operation: 'schema init' });
94
- const sql = this.getCreateTableSQL(tableName, schema);
95
- await this.client.execute(sql);
96
- } catch (error) {
97
- throw new MastraError(
98
- {
99
- id: 'LIBSQL_STORE_CREATE_TABLE_FAILED',
100
- domain: ErrorDomain.STORAGE,
101
- category: ErrorCategory.THIRD_PARTY,
102
- details: {
103
- tableName,
104
- },
105
- },
106
- error,
107
- );
108
- }
109
- }
110
-
111
- protected getSqlType(type: StorageColumn['type']): string {
112
- switch (type) {
113
- case 'bigint':
114
- return 'INTEGER'; // SQLite uses INTEGER for all integer sizes
115
- case 'jsonb':
116
- return 'TEXT'; // Store JSON as TEXT in SQLite
117
- default:
118
- return super.getSqlType(type);
119
- }
120
- }
121
-
122
- private async doInsert({
123
- tableName,
124
- record,
125
- }: {
126
- tableName: TABLE_NAMES;
127
- record: Record<string, any>;
128
- }): Promise<void> {
129
- await this.client.execute(
130
- prepareStatement({
131
- tableName,
132
- record,
133
- }),
134
- );
135
- }
136
-
137
- public insert(args: { tableName: TABLE_NAMES; record: Record<string, any> }): Promise<void> {
138
- const executeWriteOperationWithRetry = createExecuteWriteOperationWithRetry({
139
- logger: this.logger,
140
- maxRetries: this.maxRetries,
141
- initialBackoffMs: this.initialBackoffMs,
142
- });
143
- return executeWriteOperationWithRetry(() => this.doInsert(args), `insert into table ${args.tableName}`);
144
- }
145
-
146
- async load<R>({ tableName, keys }: { tableName: TABLE_NAMES; keys: Record<string, string> }): Promise<R | null> {
147
- const parsedTableName = parseSqlIdentifier(tableName, 'table name');
148
-
149
- const parsedKeys = Object.keys(keys).map(key => parseSqlIdentifier(key, 'column name'));
150
-
151
- const conditions = parsedKeys.map(key => `${key} = ?`).join(' AND ');
152
- const values = Object.values(keys);
153
-
154
- const result = await this.client.execute({
155
- sql: `SELECT * FROM ${parsedTableName} WHERE ${conditions} ORDER BY createdAt DESC LIMIT 1`,
156
- args: values,
157
- });
158
-
159
- if (!result.rows || result.rows.length === 0) {
160
- return null;
161
- }
162
-
163
- const row = result.rows[0];
164
- // Checks whether the string looks like a JSON object ({}) or array ([])
165
- // If the string starts with { or [, it assumes it's JSON and parses it
166
- // Otherwise, it just returns, preventing unintended number conversions
167
- const parsed = Object.fromEntries(
168
- Object.entries(row || {}).map(([k, v]) => {
169
- try {
170
- return [k, typeof v === 'string' ? (v.startsWith('{') || v.startsWith('[') ? JSON.parse(v) : v) : v];
171
- } catch {
172
- return [k, v];
173
- }
174
- }),
175
- );
176
-
177
- return parsed as R;
178
- }
179
-
180
- async loadMany<R>({
181
- tableName,
182
- whereClause,
183
- orderBy,
184
- offset,
185
- limit,
186
- args,
187
- }: {
188
- tableName: TABLE_NAMES;
189
- whereClause?: { sql: string; args: InValue[] };
190
- orderBy?: string;
191
- offset?: number;
192
- limit?: number;
193
- args?: any[];
194
- }): Promise<R[]> {
195
- const parsedTableName = parseSqlIdentifier(tableName, 'table name');
196
-
197
- let statement = `SELECT * FROM ${parsedTableName}`;
198
-
199
- if (whereClause?.sql) {
200
- statement += `${whereClause.sql}`;
201
- }
202
-
203
- if (orderBy) {
204
- statement += ` ORDER BY ${orderBy}`;
205
- }
206
-
207
- if (limit) {
208
- statement += ` LIMIT ${limit}`;
209
- }
210
-
211
- if (offset) {
212
- statement += ` OFFSET ${offset}`;
213
- }
214
-
215
- const result = await this.client.execute({
216
- sql: statement,
217
- args: [...(whereClause?.args ?? []), ...(args ?? [])],
218
- });
219
-
220
- return result.rows as R[];
221
- }
222
-
223
- async loadTotalCount({
224
- tableName,
225
- whereClause,
226
- }: {
227
- tableName: TABLE_NAMES;
228
- whereClause?: { sql: string; args: InValue[] };
229
- }): Promise<number> {
230
- const parsedTableName = parseSqlIdentifier(tableName, 'table name');
231
-
232
- const statement = `SELECT COUNT(*) as count FROM ${parsedTableName} ${whereClause ? `${whereClause.sql}` : ''}`;
233
-
234
- const result = await this.client.execute({
235
- sql: statement,
236
- args: whereClause?.args ?? [],
237
- });
238
-
239
- if (!result.rows || result.rows.length === 0) {
240
- return 0;
241
- }
242
-
243
- return (result.rows[0]?.count as number) ?? 0;
244
- }
245
-
246
- public update(args: { tableName: TABLE_NAMES; keys: Record<string, any>; data: Record<string, any> }): Promise<void> {
247
- const executeWriteOperationWithRetry = createExecuteWriteOperationWithRetry({
248
- logger: this.logger,
249
- maxRetries: this.maxRetries,
250
- initialBackoffMs: this.initialBackoffMs,
251
- });
252
- return executeWriteOperationWithRetry(() => this.executeUpdate(args), `update table ${args.tableName}`);
253
- }
254
-
255
- private async executeUpdate({
256
- tableName,
257
- keys,
258
- data,
259
- }: {
260
- tableName: TABLE_NAMES;
261
- keys: Record<string, any>;
262
- data: Record<string, any>;
263
- }): Promise<void> {
264
- await this.client.execute(prepareUpdateStatement({ tableName, updates: data, keys }));
265
- }
266
-
267
- private async doBatchInsert({
268
- tableName,
269
- records,
270
- }: {
271
- tableName: TABLE_NAMES;
272
- records: Record<string, any>[];
273
- }): Promise<void> {
274
- if (records.length === 0) return;
275
- const batchStatements = records.map(r => prepareStatement({ tableName, record: r }));
276
- await this.client.batch(batchStatements, 'write');
277
- }
278
-
279
- public batchInsert(args: { tableName: TABLE_NAMES; records: Record<string, any>[] }): Promise<void> {
280
- const executeWriteOperationWithRetry = createExecuteWriteOperationWithRetry({
281
- logger: this.logger,
282
- maxRetries: this.maxRetries,
283
- initialBackoffMs: this.initialBackoffMs,
284
- });
285
-
286
- return executeWriteOperationWithRetry(
287
- () => this.doBatchInsert(args),
288
- `batch insert into table ${args.tableName}`,
289
- ).catch(error => {
290
- throw new MastraError(
291
- {
292
- id: 'LIBSQL_STORE_BATCH_INSERT_FAILED',
293
- domain: ErrorDomain.STORAGE,
294
- category: ErrorCategory.THIRD_PARTY,
295
- details: {
296
- tableName: args.tableName,
297
- },
298
- },
299
- error,
300
- );
301
- });
302
- }
303
-
304
- /**
305
- * Public batch update method with retry logic
306
- */
307
- public batchUpdate(args: {
308
- tableName: TABLE_NAMES;
309
- updates: Array<{
310
- keys: Record<string, any>;
311
- data: Record<string, any>;
312
- }>;
313
- }): Promise<void> {
314
- const executeWriteOperationWithRetry = createExecuteWriteOperationWithRetry({
315
- logger: this.logger,
316
- maxRetries: this.maxRetries,
317
- initialBackoffMs: this.initialBackoffMs,
318
- });
319
-
320
- return executeWriteOperationWithRetry(
321
- () => this.executeBatchUpdate(args),
322
- `batch update in table ${args.tableName}`,
323
- ).catch(error => {
324
- throw new MastraError(
325
- {
326
- id: 'LIBSQL_STORE_BATCH_UPDATE_FAILED',
327
- domain: ErrorDomain.STORAGE,
328
- category: ErrorCategory.THIRD_PARTY,
329
- details: {
330
- tableName: args.tableName,
331
- },
332
- },
333
- error,
334
- );
335
- });
336
- }
337
-
338
- /**
339
- * Updates multiple records in batch. Each record can be updated based on single or composite keys.
340
- */
341
- private async executeBatchUpdate({
342
- tableName,
343
- updates,
344
- }: {
345
- tableName: TABLE_NAMES;
346
- updates: Array<{
347
- keys: Record<string, any>;
348
- data: Record<string, any>;
349
- }>;
350
- }): Promise<void> {
351
- if (updates.length === 0) return;
352
-
353
- const batchStatements = updates.map(({ keys, data }) =>
354
- prepareUpdateStatement({
355
- tableName,
356
- updates: data,
357
- keys,
358
- }),
359
- );
360
-
361
- await this.client.batch(batchStatements, 'write');
362
- }
363
-
364
- /**
365
- * Public batch delete method with retry logic
366
- */
367
- public batchDelete({ tableName, keys }: { tableName: TABLE_NAMES; keys: Array<Record<string, any>> }): Promise<void> {
368
- const executeWriteOperationWithRetry = createExecuteWriteOperationWithRetry({
369
- logger: this.logger,
370
- maxRetries: this.maxRetries,
371
- initialBackoffMs: this.initialBackoffMs,
372
- });
373
-
374
- return executeWriteOperationWithRetry(
375
- () => this.executeBatchDelete({ tableName, keys }),
376
- `batch delete from table ${tableName}`,
377
- ).catch(error => {
378
- throw new MastraError(
379
- {
380
- id: 'LIBSQL_STORE_BATCH_DELETE_FAILED',
381
- domain: ErrorDomain.STORAGE,
382
- category: ErrorCategory.THIRD_PARTY,
383
- details: {
384
- tableName,
385
- },
386
- },
387
- error,
388
- );
389
- });
390
- }
391
-
392
- /**
393
- * Deletes multiple records in batch. Each record can be deleted based on single or composite keys.
394
- */
395
- private async executeBatchDelete({
396
- tableName,
397
- keys,
398
- }: {
399
- tableName: TABLE_NAMES;
400
- keys: Array<Record<string, any>>;
401
- }): Promise<void> {
402
- if (keys.length === 0) return;
403
-
404
- const batchStatements = keys.map(keyObj =>
405
- prepareDeleteStatement({
406
- tableName,
407
- keys: keyObj,
408
- }),
409
- );
410
-
411
- await this.client.batch(batchStatements, 'write');
412
- }
413
-
414
- /**
415
- * Alters table schema to add columns if they don't exist
416
- * @param tableName Name of the table
417
- * @param schema Schema of the table
418
- * @param ifNotExists Array of column names to add if they don't exist
419
- */
420
- async alterTable({
421
- tableName,
422
- schema,
423
- ifNotExists,
424
- }: {
425
- tableName: TABLE_NAMES;
426
- schema: Record<string, StorageColumn>;
427
- ifNotExists: string[];
428
- }): Promise<void> {
429
- const parsedTableName = parseSqlIdentifier(tableName, 'table name');
430
-
431
- try {
432
- // 1. Get existing columns using PRAGMA
433
- const pragmaQuery = `PRAGMA table_info(${parsedTableName})`;
434
- const result = await this.client.execute(pragmaQuery);
435
- const existingColumnNames = new Set(result.rows.map((row: any) => row.name.toLowerCase()));
436
-
437
- // 2. Add missing columns
438
- for (const columnName of ifNotExists) {
439
- if (!existingColumnNames.has(columnName.toLowerCase()) && schema[columnName]) {
440
- const columnDef = schema[columnName];
441
- const sqlType = this.getSqlType(columnDef.type); // ensure this exists or implement
442
- const nullable = columnDef.nullable === false ? 'NOT NULL' : '';
443
- // In SQLite, you must provide a DEFAULT if adding a NOT NULL column to a non-empty table
444
- const defaultValue = columnDef.nullable === false ? this.getDefaultValue(columnDef.type) : '';
445
- const alterSql =
446
- `ALTER TABLE ${parsedTableName} ADD COLUMN "${columnName}" ${sqlType} ${nullable} ${defaultValue}`.trim();
447
-
448
- await this.client.execute(alterSql);
449
- this.logger?.debug?.(`Added column ${columnName} to table ${parsedTableName}`);
450
- }
451
- }
452
- } catch (error) {
453
- throw new MastraError(
454
- {
455
- id: 'LIBSQL_STORE_ALTER_TABLE_FAILED',
456
- domain: ErrorDomain.STORAGE,
457
- category: ErrorCategory.THIRD_PARTY,
458
- details: {
459
- tableName,
460
- },
461
- },
462
- error,
463
- );
464
- }
465
- }
466
-
467
- async clearTable({ tableName }: { tableName: TABLE_NAMES }): Promise<void> {
468
- const parsedTableName = parseSqlIdentifier(tableName, 'table name');
469
- try {
470
- await this.client.execute(`DELETE FROM ${parsedTableName}`);
471
- } catch (e) {
472
- const mastraError = new MastraError(
473
- {
474
- id: 'LIBSQL_STORE_CLEAR_TABLE_FAILED',
475
- domain: ErrorDomain.STORAGE,
476
- category: ErrorCategory.THIRD_PARTY,
477
- details: {
478
- tableName,
479
- },
480
- },
481
- e,
482
- );
483
- this.logger?.trackException?.(mastraError);
484
- this.logger?.error?.(mastraError.toString());
485
- }
486
- }
487
-
488
- async dropTable({ tableName }: { tableName: TABLE_NAMES }): Promise<void> {
489
- const parsedTableName = parseSqlIdentifier(tableName, 'table name');
490
- try {
491
- await this.client.execute(`DROP TABLE IF EXISTS ${parsedTableName}`);
492
- } catch (e) {
493
- throw new MastraError(
494
- {
495
- id: 'LIBSQL_STORE_DROP_TABLE_FAILED',
496
- domain: ErrorDomain.STORAGE,
497
- category: ErrorCategory.THIRD_PARTY,
498
- details: {
499
- tableName,
500
- },
501
- },
502
- e,
503
- );
504
- }
505
- }
506
- }