@mastra/libsql 0.11.0-alpha.3 → 0.11.1-alpha.0

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.
@@ -0,0 +1,296 @@
1
+ import type { Client } from '@libsql/client';
2
+ import { ErrorCategory, ErrorDomain, MastraError } from '@mastra/core/error';
3
+ import { TABLE_WORKFLOW_SNAPSHOT, StoreOperations } from '@mastra/core/storage';
4
+ import type { StorageColumn, TABLE_NAMES } from '@mastra/core/storage';
5
+ import { parseSqlIdentifier } from '@mastra/core/utils';
6
+ import { createExecuteWriteOperationWithRetry, prepareStatement } from '../utils';
7
+
8
+ export class StoreOperationsLibSQL extends StoreOperations {
9
+ private client: Client;
10
+ /**
11
+ * Maximum number of retries for write operations if an SQLITE_BUSY error occurs.
12
+ * @default 5
13
+ */
14
+ maxRetries: number;
15
+ /**
16
+ * Initial backoff time in milliseconds for retrying write operations on SQLITE_BUSY.
17
+ * The backoff time will double with each retry (exponential backoff).
18
+ * @default 100
19
+ */
20
+ initialBackoffMs: number;
21
+
22
+ constructor({
23
+ client,
24
+ maxRetries,
25
+ initialBackoffMs,
26
+ }: {
27
+ client: Client;
28
+ maxRetries?: number;
29
+ initialBackoffMs?: number;
30
+ }) {
31
+ super();
32
+ this.client = client;
33
+
34
+ this.maxRetries = maxRetries ?? 5;
35
+ this.initialBackoffMs = initialBackoffMs ?? 100;
36
+ }
37
+
38
+ async hasColumn(table: string, column: string): Promise<boolean> {
39
+ const result = await this.client.execute({
40
+ sql: `PRAGMA table_info(${table})`,
41
+ });
42
+ return (await result.rows)?.some((row: any) => row.name === column);
43
+ }
44
+
45
+ private getCreateTableSQL(tableName: TABLE_NAMES, schema: Record<string, StorageColumn>): string {
46
+ const parsedTableName = parseSqlIdentifier(tableName, 'table name');
47
+ const columns = Object.entries(schema).map(([name, col]) => {
48
+ const parsedColumnName = parseSqlIdentifier(name, 'column name');
49
+ let type = col.type.toUpperCase();
50
+ if (type === 'TEXT') type = 'TEXT';
51
+ if (type === 'TIMESTAMP') type = 'TEXT'; // Store timestamps as ISO strings
52
+ // if (type === 'BIGINT') type = 'INTEGER';
53
+
54
+ const nullable = col.nullable ? '' : 'NOT NULL';
55
+ const primaryKey = col.primaryKey ? 'PRIMARY KEY' : '';
56
+
57
+ return `${parsedColumnName} ${type} ${nullable} ${primaryKey}`.trim();
58
+ });
59
+
60
+ // For workflow_snapshot table, create a composite primary key
61
+ if (tableName === TABLE_WORKFLOW_SNAPSHOT) {
62
+ const stmnt = `CREATE TABLE IF NOT EXISTS ${parsedTableName} (
63
+ ${columns.join(',\n')},
64
+ PRIMARY KEY (workflow_name, run_id)
65
+ )`;
66
+ return stmnt;
67
+ }
68
+
69
+ return `CREATE TABLE IF NOT EXISTS ${parsedTableName} (${columns.join(', ')})`;
70
+ }
71
+
72
+ async createTable({
73
+ tableName,
74
+ schema,
75
+ }: {
76
+ tableName: TABLE_NAMES;
77
+ schema: Record<string, StorageColumn>;
78
+ }): Promise<void> {
79
+ try {
80
+ this.logger.debug(`Creating database table`, { tableName, operation: 'schema init' });
81
+ const sql = this.getCreateTableSQL(tableName, schema);
82
+ await this.client.execute(sql);
83
+ } catch (error) {
84
+ throw new MastraError(
85
+ {
86
+ id: 'LIBSQL_STORE_CREATE_TABLE_FAILED',
87
+ domain: ErrorDomain.STORAGE,
88
+ category: ErrorCategory.THIRD_PARTY,
89
+ details: {
90
+ tableName,
91
+ },
92
+ },
93
+ error,
94
+ );
95
+ }
96
+ }
97
+
98
+ protected getSqlType(type: StorageColumn['type']): string {
99
+ switch (type) {
100
+ case 'bigint':
101
+ return 'INTEGER'; // SQLite uses INTEGER for all integer sizes
102
+ case 'jsonb':
103
+ return 'TEXT'; // Store JSON as TEXT in SQLite
104
+ default:
105
+ return super.getSqlType(type);
106
+ }
107
+ }
108
+
109
+ private async doInsert({
110
+ tableName,
111
+ record,
112
+ }: {
113
+ tableName: TABLE_NAMES;
114
+ record: Record<string, any>;
115
+ }): Promise<void> {
116
+ await this.client.execute(
117
+ prepareStatement({
118
+ tableName,
119
+ record,
120
+ }),
121
+ );
122
+ }
123
+
124
+ public insert(args: { tableName: TABLE_NAMES; record: Record<string, any> }): Promise<void> {
125
+ const executeWriteOperationWithRetry = createExecuteWriteOperationWithRetry({
126
+ logger: this.logger,
127
+ maxRetries: this.maxRetries,
128
+ initialBackoffMs: this.initialBackoffMs,
129
+ });
130
+ return executeWriteOperationWithRetry(() => this.doInsert(args), `insert into table ${args.tableName}`);
131
+ }
132
+
133
+ async load<R>({ tableName, keys }: { tableName: TABLE_NAMES; keys: Record<string, string> }): Promise<R | null> {
134
+ const parsedTableName = parseSqlIdentifier(tableName, 'table name');
135
+
136
+ const parsedKeys = Object.keys(keys).map(key => parseSqlIdentifier(key, 'column name'));
137
+
138
+ const conditions = parsedKeys.map(key => `${key} = ?`).join(' AND ');
139
+ const values = Object.values(keys);
140
+
141
+ const result = await this.client.execute({
142
+ sql: `SELECT * FROM ${parsedTableName} WHERE ${conditions} ORDER BY createdAt DESC LIMIT 1`,
143
+ args: values,
144
+ });
145
+
146
+ if (!result.rows || result.rows.length === 0) {
147
+ return null;
148
+ }
149
+
150
+ const row = result.rows[0];
151
+ // Checks whether the string looks like a JSON object ({}) or array ([])
152
+ // If the string starts with { or [, it assumes it's JSON and parses it
153
+ // Otherwise, it just returns, preventing unintended number conversions
154
+ const parsed = Object.fromEntries(
155
+ Object.entries(row || {}).map(([k, v]) => {
156
+ try {
157
+ return [k, typeof v === 'string' ? (v.startsWith('{') || v.startsWith('[') ? JSON.parse(v) : v) : v];
158
+ } catch {
159
+ return [k, v];
160
+ }
161
+ }),
162
+ );
163
+
164
+ return parsed as R;
165
+ }
166
+
167
+ private async doBatchInsert({
168
+ tableName,
169
+ records,
170
+ }: {
171
+ tableName: TABLE_NAMES;
172
+ records: Record<string, any>[];
173
+ }): Promise<void> {
174
+ if (records.length === 0) return;
175
+ const batchStatements = records.map(r => prepareStatement({ tableName, record: r }));
176
+ await this.client.batch(batchStatements, 'write');
177
+ }
178
+
179
+ public batchInsert(args: { tableName: TABLE_NAMES; records: Record<string, any>[] }): Promise<void> {
180
+ const executeWriteOperationWithRetry = createExecuteWriteOperationWithRetry({
181
+ logger: this.logger,
182
+ maxRetries: this.maxRetries,
183
+ initialBackoffMs: this.initialBackoffMs,
184
+ });
185
+
186
+ return executeWriteOperationWithRetry(
187
+ () => this.doBatchInsert(args),
188
+ `batch insert into table ${args.tableName}`,
189
+ ).catch(error => {
190
+ throw new MastraError(
191
+ {
192
+ id: 'LIBSQL_STORE_BATCH_INSERT_FAILED',
193
+ domain: ErrorDomain.STORAGE,
194
+ category: ErrorCategory.THIRD_PARTY,
195
+ details: {
196
+ tableName: args.tableName,
197
+ },
198
+ },
199
+ error,
200
+ );
201
+ });
202
+ }
203
+
204
+ /**
205
+ * Alters table schema to add columns if they don't exist
206
+ * @param tableName Name of the table
207
+ * @param schema Schema of the table
208
+ * @param ifNotExists Array of column names to add if they don't exist
209
+ */
210
+ async alterTable({
211
+ tableName,
212
+ schema,
213
+ ifNotExists,
214
+ }: {
215
+ tableName: TABLE_NAMES;
216
+ schema: Record<string, StorageColumn>;
217
+ ifNotExists: string[];
218
+ }): Promise<void> {
219
+ const parsedTableName = parseSqlIdentifier(tableName, 'table name');
220
+
221
+ try {
222
+ // 1. Get existing columns using PRAGMA
223
+ const pragmaQuery = `PRAGMA table_info(${parsedTableName})`;
224
+ const result = await this.client.execute(pragmaQuery);
225
+ const existingColumnNames = new Set(result.rows.map((row: any) => row.name.toLowerCase()));
226
+
227
+ // 2. Add missing columns
228
+ for (const columnName of ifNotExists) {
229
+ if (!existingColumnNames.has(columnName.toLowerCase()) && schema[columnName]) {
230
+ const columnDef = schema[columnName];
231
+ const sqlType = this.getSqlType(columnDef.type); // ensure this exists or implement
232
+ const nullable = columnDef.nullable === false ? 'NOT NULL' : '';
233
+ // In SQLite, you must provide a DEFAULT if adding a NOT NULL column to a non-empty table
234
+ const defaultValue = columnDef.nullable === false ? this.getDefaultValue(columnDef.type) : '';
235
+ const alterSql =
236
+ `ALTER TABLE ${parsedTableName} ADD COLUMN "${columnName}" ${sqlType} ${nullable} ${defaultValue}`.trim();
237
+
238
+ await this.client.execute(alterSql);
239
+ this.logger?.debug?.(`Added column ${columnName} to table ${parsedTableName}`);
240
+ }
241
+ }
242
+ } catch (error) {
243
+ throw new MastraError(
244
+ {
245
+ id: 'LIBSQL_STORE_ALTER_TABLE_FAILED',
246
+ domain: ErrorDomain.STORAGE,
247
+ category: ErrorCategory.THIRD_PARTY,
248
+ details: {
249
+ tableName,
250
+ },
251
+ },
252
+ error,
253
+ );
254
+ }
255
+ }
256
+
257
+ async clearTable({ tableName }: { tableName: TABLE_NAMES }): Promise<void> {
258
+ const parsedTableName = parseSqlIdentifier(tableName, 'table name');
259
+ try {
260
+ await this.client.execute(`DELETE FROM ${parsedTableName}`);
261
+ } catch (e) {
262
+ const mastraError = new MastraError(
263
+ {
264
+ id: 'LIBSQL_STORE_CLEAR_TABLE_FAILED',
265
+ domain: ErrorDomain.STORAGE,
266
+ category: ErrorCategory.THIRD_PARTY,
267
+ details: {
268
+ tableName,
269
+ },
270
+ },
271
+ e,
272
+ );
273
+ this.logger?.trackException?.(mastraError);
274
+ this.logger?.error?.(mastraError.toString());
275
+ }
276
+ }
277
+
278
+ async dropTable({ tableName }: { tableName: TABLE_NAMES }): Promise<void> {
279
+ const parsedTableName = parseSqlIdentifier(tableName, 'table name');
280
+ try {
281
+ await this.client.execute(`DROP TABLE IF EXISTS ${parsedTableName}`);
282
+ } catch (e) {
283
+ throw new MastraError(
284
+ {
285
+ id: 'LIBSQL_STORE_DROP_TABLE_FAILED',
286
+ domain: ErrorDomain.STORAGE,
287
+ category: ErrorCategory.THIRD_PARTY,
288
+ details: {
289
+ tableName,
290
+ },
291
+ },
292
+ e,
293
+ );
294
+ }
295
+ }
296
+ }
@@ -0,0 +1,217 @@
1
+ import type { Client, InValue } from '@libsql/client';
2
+ import { ErrorCategory, ErrorDomain, MastraError } from '@mastra/core/error';
3
+ import type { ScoreRowData } from '@mastra/core/scores';
4
+ import { TABLE_SCORERS, ScoresStorage } from '@mastra/core/storage';
5
+ import type { PaginationInfo, StoragePagination } from '@mastra/core/storage';
6
+ import type { StoreOperationsLibSQL } from '../operations';
7
+
8
+ export class ScoresLibSQL extends ScoresStorage {
9
+ private operations: StoreOperationsLibSQL;
10
+ private client: Client;
11
+ constructor({ client, operations }: { client: Client; operations: StoreOperationsLibSQL }) {
12
+ super();
13
+ this.operations = operations;
14
+ this.client = client;
15
+ }
16
+
17
+ async getScoresByRunId({
18
+ runId,
19
+ pagination,
20
+ }: {
21
+ runId: string;
22
+ pagination: StoragePagination;
23
+ }): Promise<{ pagination: PaginationInfo; scores: ScoreRowData[] }> {
24
+ try {
25
+ const result = await this.client.execute({
26
+ sql: `SELECT * FROM ${TABLE_SCORERS} WHERE runId = ? ORDER BY createdAt DESC LIMIT ? OFFSET ?`,
27
+ args: [runId, pagination.perPage + 1, pagination.page * pagination.perPage],
28
+ });
29
+ return {
30
+ scores: result.rows?.slice(0, pagination.perPage).map(row => this.transformScoreRow(row)) ?? [],
31
+ pagination: {
32
+ total: result.rows?.length ?? 0,
33
+ page: pagination.page,
34
+ perPage: pagination.perPage,
35
+ hasMore: result.rows?.length > pagination.perPage,
36
+ },
37
+ };
38
+ } catch (error) {
39
+ throw new MastraError(
40
+ {
41
+ id: 'LIBSQL_STORE_GET_SCORES_BY_RUN_ID_FAILED',
42
+ domain: ErrorDomain.STORAGE,
43
+ category: ErrorCategory.THIRD_PARTY,
44
+ },
45
+ error,
46
+ );
47
+ }
48
+ }
49
+
50
+ async getScoresByScorerId({
51
+ scorerId,
52
+ entityId,
53
+ entityType,
54
+ pagination,
55
+ }: {
56
+ scorerId: string;
57
+ entityId?: string;
58
+ entityType?: string;
59
+ pagination: StoragePagination;
60
+ }): Promise<{ pagination: PaginationInfo; scores: ScoreRowData[] }> {
61
+ try {
62
+ const conditions: string[] = [];
63
+ const queryParams: InValue[] = [];
64
+
65
+ if (scorerId) {
66
+ conditions.push(`scorerId = ?`);
67
+ queryParams.push(scorerId);
68
+ }
69
+
70
+ if (entityId) {
71
+ conditions.push(`entityId = ?`);
72
+ queryParams.push(entityId);
73
+ }
74
+
75
+ if (entityType) {
76
+ conditions.push(`entityType = ?`);
77
+ queryParams.push(entityType);
78
+ }
79
+
80
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
81
+
82
+ const result = await this.client.execute({
83
+ sql: `SELECT * FROM ${TABLE_SCORERS} ${whereClause} ORDER BY createdAt DESC LIMIT ? OFFSET ?`,
84
+ args: [...queryParams, pagination.perPage + 1, pagination.page * pagination.perPage],
85
+ });
86
+
87
+ return {
88
+ scores: result.rows?.slice(0, pagination.perPage).map(row => this.transformScoreRow(row)) ?? [],
89
+ pagination: {
90
+ total: result.rows?.length ?? 0,
91
+ page: pagination.page,
92
+ perPage: pagination.perPage,
93
+ hasMore: result.rows?.length > pagination.perPage,
94
+ },
95
+ };
96
+ } catch (error) {
97
+ throw new MastraError(
98
+ {
99
+ id: 'LIBSQL_STORE_GET_SCORES_BY_SCORER_ID_FAILED',
100
+ domain: ErrorDomain.STORAGE,
101
+ category: ErrorCategory.THIRD_PARTY,
102
+ },
103
+ error,
104
+ );
105
+ }
106
+ }
107
+
108
+ private transformScoreRow(row: Record<string, any>): ScoreRowData {
109
+ const scorerValue = JSON.parse(row.scorer ?? '{}');
110
+ const inputValue = JSON.parse(row.input ?? '{}');
111
+ const outputValue = JSON.parse(row.output ?? '{}');
112
+ const additionalLLMContextValue = row.additionalLLMContext ? JSON.parse(row.additionalLLMContext) : null;
113
+ const runtimeContextValue = row.runtimeContext ? JSON.parse(row.runtimeContext) : null;
114
+ const metadataValue = row.metadata ? JSON.parse(row.metadata) : null;
115
+ const entityValue = row.entity ? JSON.parse(row.entity) : null;
116
+ const extractStepResultValue = row.extractStepResult ? JSON.parse(row.extractStepResult) : null;
117
+ const analyzeStepResultValue = row.analyzeStepResult ? JSON.parse(row.analyzeStepResult) : null;
118
+
119
+ return {
120
+ id: row.id,
121
+ traceId: row.traceId,
122
+ runId: row.runId,
123
+ scorer: scorerValue,
124
+ score: row.score,
125
+ reason: row.reason,
126
+ extractStepResult: extractStepResultValue,
127
+ analyzeStepResult: analyzeStepResultValue,
128
+ analyzePrompt: row.analyzePrompt,
129
+ extractPrompt: row.extractPrompt,
130
+ metadata: metadataValue,
131
+ input: inputValue,
132
+ output: outputValue,
133
+ additionalContext: additionalLLMContextValue,
134
+ runtimeContext: runtimeContextValue,
135
+ entityType: row.entityType,
136
+ entity: entityValue,
137
+ entityId: row.entityId,
138
+ scorerId: row.scorerId,
139
+ source: row.source,
140
+ resourceId: row.resourceId,
141
+ threadId: row.threadId,
142
+ createdAt: row.createdAt,
143
+ updatedAt: row.updatedAt,
144
+ };
145
+ }
146
+
147
+ async getScoreById({ id }: { id: string }): Promise<ScoreRowData | null> {
148
+ const result = await this.client.execute({
149
+ sql: `SELECT * FROM ${TABLE_SCORERS} WHERE id = ?`,
150
+ args: [id],
151
+ });
152
+ return result.rows?.[0] ? this.transformScoreRow(result.rows[0]) : null;
153
+ }
154
+
155
+ async saveScore(score: Omit<ScoreRowData, 'id' | 'createdAt' | 'updatedAt'>): Promise<{ score: ScoreRowData }> {
156
+ try {
157
+ const id = crypto.randomUUID();
158
+
159
+ await this.operations.insert({
160
+ tableName: TABLE_SCORERS,
161
+ record: {
162
+ id,
163
+ createdAt: new Date().toISOString(),
164
+ updatedAt: new Date().toISOString(),
165
+ ...score,
166
+ },
167
+ });
168
+
169
+ const scoreFromDb = await this.getScoreById({ id });
170
+ return { score: scoreFromDb! };
171
+ } catch (error) {
172
+ throw new MastraError(
173
+ {
174
+ id: 'LIBSQL_STORE_SAVE_SCORE_FAILED',
175
+ domain: ErrorDomain.STORAGE,
176
+ category: ErrorCategory.THIRD_PARTY,
177
+ },
178
+ error,
179
+ );
180
+ }
181
+ }
182
+
183
+ async getScoresByEntityId({
184
+ entityId,
185
+ entityType,
186
+ pagination,
187
+ }: {
188
+ pagination: StoragePagination;
189
+ entityId: string;
190
+ entityType: string;
191
+ }): Promise<{ pagination: PaginationInfo; scores: ScoreRowData[] }> {
192
+ try {
193
+ const result = await this.client.execute({
194
+ sql: `SELECT * FROM ${TABLE_SCORERS} WHERE entityId = ? AND entityType = ? ORDER BY createdAt DESC LIMIT ? OFFSET ?`,
195
+ args: [entityId, entityType, pagination.perPage + 1, pagination.page * pagination.perPage],
196
+ });
197
+ return {
198
+ scores: result.rows?.slice(0, pagination.perPage).map(row => this.transformScoreRow(row)) ?? [],
199
+ pagination: {
200
+ total: result.rows?.length ?? 0,
201
+ page: pagination.page,
202
+ perPage: pagination.perPage,
203
+ hasMore: result.rows?.length > pagination.perPage,
204
+ },
205
+ };
206
+ } catch (error) {
207
+ throw new MastraError(
208
+ {
209
+ id: 'LIBSQL_STORE_GET_SCORES_BY_ENTITY_ID_FAILED',
210
+ domain: ErrorDomain.STORAGE,
211
+ category: ErrorCategory.THIRD_PARTY,
212
+ },
213
+ error,
214
+ );
215
+ }
216
+ }
217
+ }
@@ -0,0 +1,150 @@
1
+ import type { Client, InValue } from '@libsql/client';
2
+ import { ErrorCategory, ErrorDomain, MastraError } from '@mastra/core/error';
3
+ import { TABLE_TRACES, TracesStorage, safelyParseJSON } from '@mastra/core/storage';
4
+ import type { StorageGetTracesArg, StorageGetTracesPaginatedArg, PaginationInfo } from '@mastra/core/storage';
5
+ import type { Trace } from '@mastra/core/telemetry';
6
+ import { parseSqlIdentifier } from '@mastra/core/utils';
7
+ import type { StoreOperationsLibSQL } from '../operations';
8
+
9
+ export class TracesLibSQL extends TracesStorage {
10
+ private client: Client;
11
+ private operations: StoreOperationsLibSQL;
12
+
13
+ constructor({ client, operations }: { client: Client; operations: StoreOperationsLibSQL }) {
14
+ super();
15
+ this.client = client;
16
+ this.operations = operations;
17
+ }
18
+
19
+ async getTraces(args: StorageGetTracesArg): Promise<Trace[]> {
20
+ if (args.fromDate || args.toDate) {
21
+ (args as any).dateRange = {
22
+ start: args.fromDate,
23
+ end: args.toDate,
24
+ };
25
+ }
26
+ try {
27
+ const result = await this.getTracesPaginated(args);
28
+ return result.traces;
29
+ } catch (error) {
30
+ throw new MastraError(
31
+ {
32
+ id: 'LIBSQL_STORE_GET_TRACES_FAILED',
33
+ domain: ErrorDomain.STORAGE,
34
+ category: ErrorCategory.THIRD_PARTY,
35
+ },
36
+ error,
37
+ );
38
+ }
39
+ }
40
+
41
+ async getTracesPaginated(args: StorageGetTracesPaginatedArg): Promise<PaginationInfo & { traces: Trace[] }> {
42
+ const { name, scope, page = 0, perPage = 100, attributes, filters, dateRange } = args;
43
+ const fromDate = dateRange?.start;
44
+ const toDate = dateRange?.end;
45
+ const currentOffset = page * perPage;
46
+
47
+ const queryArgs: InValue[] = [];
48
+ const conditions: string[] = [];
49
+
50
+ if (name) {
51
+ conditions.push('name LIKE ?');
52
+ queryArgs.push(`${name}%`);
53
+ }
54
+ if (scope) {
55
+ conditions.push('scope = ?');
56
+ queryArgs.push(scope);
57
+ }
58
+ if (attributes) {
59
+ Object.entries(attributes).forEach(([key, value]) => {
60
+ conditions.push(`json_extract(attributes, '$.${key}') = ?`);
61
+ queryArgs.push(value);
62
+ });
63
+ }
64
+ if (filters) {
65
+ Object.entries(filters).forEach(([key, value]) => {
66
+ conditions.push(`${parseSqlIdentifier(key, 'filter key')} = ?`);
67
+ queryArgs.push(value);
68
+ });
69
+ }
70
+ if (fromDate) {
71
+ conditions.push('createdAt >= ?');
72
+ queryArgs.push(fromDate.toISOString());
73
+ }
74
+ if (toDate) {
75
+ conditions.push('createdAt <= ?');
76
+ queryArgs.push(toDate.toISOString());
77
+ }
78
+
79
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
80
+
81
+ try {
82
+ const countResult = await this.client.execute({
83
+ sql: `SELECT COUNT(*) as count FROM ${TABLE_TRACES} ${whereClause}`,
84
+ args: queryArgs,
85
+ });
86
+ const total = Number(countResult.rows?.[0]?.count ?? 0);
87
+
88
+ if (total === 0) {
89
+ return {
90
+ traces: [],
91
+ total: 0,
92
+ page,
93
+ perPage,
94
+ hasMore: false,
95
+ };
96
+ }
97
+
98
+ const dataResult = await this.client.execute({
99
+ sql: `SELECT * FROM ${TABLE_TRACES} ${whereClause} ORDER BY "startTime" DESC LIMIT ? OFFSET ?`,
100
+ args: [...queryArgs, perPage, currentOffset],
101
+ });
102
+
103
+ const traces =
104
+ dataResult.rows?.map(
105
+ row =>
106
+ ({
107
+ id: row.id,
108
+ parentSpanId: row.parentSpanId,
109
+ traceId: row.traceId,
110
+ name: row.name,
111
+ scope: row.scope,
112
+ kind: row.kind,
113
+ status: safelyParseJSON(row.status as string),
114
+ events: safelyParseJSON(row.events as string),
115
+ links: safelyParseJSON(row.links as string),
116
+ attributes: safelyParseJSON(row.attributes as string),
117
+ startTime: row.startTime,
118
+ endTime: row.endTime,
119
+ other: safelyParseJSON(row.other as string),
120
+ createdAt: row.createdAt,
121
+ }) as Trace,
122
+ ) ?? [];
123
+
124
+ return {
125
+ traces,
126
+ total,
127
+ page,
128
+ perPage,
129
+ hasMore: currentOffset + traces.length < total,
130
+ };
131
+ } catch (error) {
132
+ throw new MastraError(
133
+ {
134
+ id: 'LIBSQL_STORE_GET_TRACES_PAGINATED_FAILED',
135
+ domain: ErrorDomain.STORAGE,
136
+ category: ErrorCategory.THIRD_PARTY,
137
+ },
138
+ error,
139
+ );
140
+ }
141
+ }
142
+
143
+ async batchTraceInsert({ records }: { records: Record<string, any>[] }): Promise<void> {
144
+ this.logger.debug('Batch inserting traces', { count: records.length });
145
+ await this.operations.batchInsert({
146
+ tableName: TABLE_TRACES,
147
+ records,
148
+ });
149
+ }
150
+ }