@mastra/libsql 0.10.1 → 0.10.2-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.
@@ -6,20 +6,23 @@ import type { MetricResult, TestInfo } from '@mastra/core/eval';
6
6
  import type { MastraMessageV1, StorageThreadType } from '@mastra/core/memory';
7
7
  import {
8
8
  MastraStorage,
9
+ TABLE_EVALS,
9
10
  TABLE_MESSAGES,
10
11
  TABLE_THREADS,
11
12
  TABLE_TRACES,
12
13
  TABLE_WORKFLOW_SNAPSHOT,
13
- TABLE_EVALS,
14
14
  } from '@mastra/core/storage';
15
15
  import type {
16
16
  EvalRow,
17
+ PaginationArgs,
18
+ PaginationInfo,
17
19
  StorageColumn,
18
20
  StorageGetMessagesArg,
19
21
  TABLE_NAMES,
20
22
  WorkflowRun,
21
23
  WorkflowRuns,
22
24
  } from '@mastra/core/storage';
25
+ import type { Trace } from '@mastra/core/telemetry';
23
26
  import { parseSqlIdentifier } from '@mastra/core/utils';
24
27
  import type { WorkflowRunState } from '@mastra/core/workflows';
25
28
 
@@ -122,6 +125,63 @@ export class LibSQLStore extends MastraStorage {
122
125
  }
123
126
  }
124
127
 
128
+ protected getSqlType(type: StorageColumn['type']): string {
129
+ switch (type) {
130
+ case 'bigint':
131
+ return 'INTEGER'; // SQLite uses INTEGER for all integer sizes
132
+ case 'jsonb':
133
+ return 'TEXT'; // Store JSON as TEXT in SQLite
134
+ default:
135
+ return super.getSqlType(type);
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Alters table schema to add columns if they don't exist
141
+ * @param tableName Name of the table
142
+ * @param schema Schema of the table
143
+ * @param ifNotExists Array of column names to add if they don't exist
144
+ */
145
+ async alterTable({
146
+ tableName,
147
+ schema,
148
+ ifNotExists,
149
+ }: {
150
+ tableName: TABLE_NAMES;
151
+ schema: Record<string, StorageColumn>;
152
+ ifNotExists: string[];
153
+ }): Promise<void> {
154
+ const parsedTableName = parseSqlIdentifier(tableName, 'table name');
155
+
156
+ try {
157
+ // 1. Get existing columns using PRAGMA
158
+ const pragmaQuery = `PRAGMA table_info(${parsedTableName})`;
159
+ const result = await this.client.execute(pragmaQuery);
160
+ const existingColumnNames = new Set(result.rows.map((row: any) => row.name.toLowerCase()));
161
+
162
+ // 2. Add missing columns
163
+ for (const columnName of ifNotExists) {
164
+ if (!existingColumnNames.has(columnName.toLowerCase()) && schema[columnName]) {
165
+ const columnDef = schema[columnName];
166
+ const sqlType = this.getSqlType(columnDef.type); // ensure this exists or implement
167
+ const nullable = columnDef.nullable === false ? 'NOT NULL' : '';
168
+ // In SQLite, you must provide a DEFAULT if adding a NOT NULL column to a non-empty table
169
+ const defaultValue = columnDef.nullable === false ? this.getDefaultValue(columnDef.type) : '';
170
+ const alterSql =
171
+ `ALTER TABLE ${parsedTableName} ADD COLUMN "${columnName}" ${sqlType} ${nullable} ${defaultValue}`.trim();
172
+
173
+ await this.client.execute(alterSql);
174
+ this.logger?.debug?.(`Added column ${columnName} to table ${parsedTableName}`);
175
+ }
176
+ }
177
+ } catch (error) {
178
+ this.logger?.error?.(
179
+ `Error altering table ${tableName}: ${error instanceof Error ? error.message : String(error)}`,
180
+ );
181
+ throw new Error(`Failed to alter table ${tableName}: ${error}`);
182
+ }
183
+ }
184
+
125
185
  async clearTable({ tableName }: { tableName: TABLE_NAMES }): Promise<void> {
126
186
  const parsedTableName = parseSqlIdentifier(tableName, 'table name');
127
187
  try {
@@ -274,24 +334,97 @@ export class LibSQLStore extends MastraStorage {
274
334
  };
275
335
  }
276
336
 
277
- async getThreadsByResourceId({ resourceId }: { resourceId: string }): Promise<StorageThreadType[]> {
278
- const result = await this.client.execute({
279
- sql: `SELECT * FROM ${TABLE_THREADS} WHERE resourceId = ?`,
280
- args: [resourceId],
281
- });
337
+ /**
338
+ * @deprecated use getThreadsByResourceIdPaginated instead for paginated results.
339
+ */
340
+ public async getThreadsByResourceId(args: { resourceId: string }): Promise<StorageThreadType[]> {
341
+ const { resourceId } = args;
342
+
343
+ try {
344
+ const baseQuery = `FROM ${TABLE_THREADS} WHERE resourceId = ?`;
345
+ const queryParams: InValue[] = [resourceId];
346
+
347
+ const mapRowToStorageThreadType = (row: any): StorageThreadType => ({
348
+ id: row.id as string,
349
+ resourceId: row.resourceId as string,
350
+ title: row.title as string,
351
+ createdAt: new Date(row.createdAt as string), // Convert string to Date
352
+ updatedAt: new Date(row.updatedAt as string), // Convert string to Date
353
+ metadata: typeof row.metadata === 'string' ? JSON.parse(row.metadata) : row.metadata,
354
+ });
355
+
356
+ // Non-paginated path
357
+ const result = await this.client.execute({
358
+ sql: `SELECT * ${baseQuery} ORDER BY createdAt DESC`,
359
+ args: queryParams,
360
+ });
282
361
 
283
- if (!result.rows) {
362
+ if (!result.rows) {
363
+ return [];
364
+ }
365
+ return result.rows.map(mapRowToStorageThreadType);
366
+ } catch (error) {
367
+ this.logger.error(`Error getting threads for resource ${resourceId}:`, error);
284
368
  return [];
285
369
  }
370
+ }
371
+
372
+ public async getThreadsByResourceIdPaginated(
373
+ args: {
374
+ resourceId: string;
375
+ } & PaginationArgs,
376
+ ): Promise<PaginationInfo & { threads: StorageThreadType[] }> {
377
+ const { resourceId, page = 0, perPage = 100 } = args;
378
+
379
+ try {
380
+ const baseQuery = `FROM ${TABLE_THREADS} WHERE resourceId = ?`;
381
+ const queryParams: InValue[] = [resourceId];
382
+
383
+ const mapRowToStorageThreadType = (row: any): StorageThreadType => ({
384
+ id: row.id as string,
385
+ resourceId: row.resourceId as string,
386
+ title: row.title as string,
387
+ createdAt: new Date(row.createdAt as string), // Convert string to Date
388
+ updatedAt: new Date(row.updatedAt as string), // Convert string to Date
389
+ metadata: typeof row.metadata === 'string' ? JSON.parse(row.metadata) : row.metadata,
390
+ });
391
+
392
+ const currentOffset = page * perPage;
286
393
 
287
- return result.rows.map(thread => ({
288
- id: thread.id,
289
- resourceId: thread.resourceId,
290
- title: thread.title,
291
- createdAt: thread.createdAt,
292
- updatedAt: thread.updatedAt,
293
- metadata: typeof thread.metadata === 'string' ? JSON.parse(thread.metadata) : thread.metadata,
294
- })) as any as StorageThreadType[];
394
+ const countResult = await this.client.execute({
395
+ sql: `SELECT COUNT(*) as count ${baseQuery}`,
396
+ args: queryParams,
397
+ });
398
+ const total = Number(countResult.rows?.[0]?.count ?? 0);
399
+
400
+ if (total === 0) {
401
+ return {
402
+ threads: [],
403
+ total: 0,
404
+ page,
405
+ perPage,
406
+ hasMore: false,
407
+ };
408
+ }
409
+
410
+ const dataResult = await this.client.execute({
411
+ sql: `SELECT * ${baseQuery} ORDER BY createdAt DESC LIMIT ? OFFSET ?`,
412
+ args: [...queryParams, perPage, currentOffset],
413
+ });
414
+
415
+ const threads = (dataResult.rows || []).map(mapRowToStorageThreadType);
416
+
417
+ return {
418
+ threads,
419
+ total,
420
+ page,
421
+ perPage,
422
+ hasMore: currentOffset + threads.length < total,
423
+ };
424
+ } catch (error) {
425
+ this.logger.error(`Error getting threads for resource ${resourceId}:`, error);
426
+ return { threads: [], total: 0, page, perPage, hasMore: false };
427
+ }
295
428
  }
296
429
 
297
430
  async saveThread({ thread }: { thread: StorageThreadType }): Promise<StorageThreadType> {
@@ -338,11 +471,17 @@ export class LibSQLStore extends MastraStorage {
338
471
  }
339
472
 
340
473
  async deleteThread({ threadId }: { threadId: string }): Promise<void> {
474
+ // Delete messages for this thread (manual step)
475
+ await this.client.execute({
476
+ sql: `DELETE FROM ${TABLE_MESSAGES} WHERE thread_id = ?`,
477
+ args: [threadId],
478
+ });
479
+
341
480
  await this.client.execute({
342
481
  sql: `DELETE FROM ${TABLE_THREADS} WHERE id = ?`,
343
482
  args: [threadId],
344
483
  });
345
- // Messages will be automatically deleted due to CASCADE constraint
484
+ // TODO: Need to check if CASCADE is enabled so that messages will be automatically deleted due to CASCADE constraint
346
485
  }
347
486
 
348
487
  private parseRow(row: any): MastraMessageV2 {
@@ -358,92 +497,82 @@ export class LibSQLStore extends MastraStorage {
358
497
  role: row.role,
359
498
  createdAt: new Date(row.createdAt as string),
360
499
  threadId: row.thread_id,
500
+ resourceId: row.resourceId,
361
501
  } as MastraMessageV2;
362
502
  if (row.type && row.type !== `v2`) result.type = row.type;
363
503
  return result;
364
504
  }
365
505
 
506
+ private async _getIncludedMessages(threadId: string, selectBy: StorageGetMessagesArg['selectBy']) {
507
+ const include = selectBy?.include;
508
+ if (!include) return null;
509
+
510
+ const includeIds = include.map(i => i.id);
511
+ const maxPrev = Math.max(...include.map(i => i.withPreviousMessages || 0));
512
+ const maxNext = Math.max(...include.map(i => i.withNextMessages || 0));
513
+
514
+ const includeResult = await this.client.execute({
515
+ sql: `
516
+ WITH numbered_messages AS (
517
+ SELECT
518
+ id, content, role, type, "createdAt", thread_id,
519
+ ROW_NUMBER() OVER (ORDER BY "createdAt" ASC) as row_num
520
+ FROM "${TABLE_MESSAGES}"
521
+ WHERE thread_id = ?
522
+ ),
523
+ target_positions AS (
524
+ SELECT row_num as target_pos
525
+ FROM numbered_messages
526
+ WHERE id IN (${includeIds.map(() => '?').join(', ')})
527
+ )
528
+ SELECT DISTINCT m.*
529
+ FROM numbered_messages m
530
+ CROSS JOIN target_positions t
531
+ WHERE m.row_num BETWEEN (t.target_pos - ?) AND (t.target_pos + ?)
532
+ ORDER BY m."createdAt" ASC
533
+ `,
534
+ args: [threadId, ...includeIds, maxPrev, maxNext],
535
+ });
536
+ return includeResult.rows?.map((row: any) => this.parseRow(row));
537
+ }
538
+
539
+ /**
540
+ * @deprecated use getMessagesPaginated instead for paginated results.
541
+ */
366
542
  public async getMessages(args: StorageGetMessagesArg & { format?: 'v1' }): Promise<MastraMessageV1[]>;
367
543
  public async getMessages(args: StorageGetMessagesArg & { format: 'v2' }): Promise<MastraMessageV2[]>;
368
544
  public async getMessages({
369
545
  threadId,
370
546
  selectBy,
371
547
  format,
372
- }: StorageGetMessagesArg & { format?: 'v1' | 'v2' }): Promise<MastraMessageV1[] | MastraMessageV2[]> {
548
+ }: StorageGetMessagesArg & {
549
+ format?: 'v1' | 'v2';
550
+ }): Promise<MastraMessageV1[] | MastraMessageV2[]> {
373
551
  try {
374
552
  const messages: MastraMessageV2[] = [];
375
553
  const limit = typeof selectBy?.last === `number` ? selectBy.last : 40;
376
554
 
377
- // If we have specific messages to select
378
555
  if (selectBy?.include?.length) {
379
- const includeIds = selectBy.include.map(i => i.id);
380
- const maxPrev = Math.max(...selectBy.include.map(i => i.withPreviousMessages || 0));
381
- const maxNext = Math.max(...selectBy.include.map(i => i.withNextMessages || 0));
382
-
383
- // Get messages around all specified IDs in one query using row numbers
384
- const includeResult = await this.client.execute({
385
- sql: `
386
- WITH numbered_messages AS (
387
- SELECT
388
- id,
389
- content,
390
- role,
391
- type,
392
- "createdAt",
393
- thread_id,
394
- ROW_NUMBER() OVER (ORDER BY "createdAt" ASC) as row_num
395
- FROM "${TABLE_MESSAGES}"
396
- WHERE thread_id = ?
397
- ),
398
- target_positions AS (
399
- SELECT row_num as target_pos
400
- FROM numbered_messages
401
- WHERE id IN (${includeIds.map(() => '?').join(', ')})
402
- )
403
- SELECT DISTINCT m.*
404
- FROM numbered_messages m
405
- CROSS JOIN target_positions t
406
- WHERE m.row_num BETWEEN (t.target_pos - ?) AND (t.target_pos + ?)
407
- ORDER BY m."createdAt" ASC
408
- `,
409
- args: [threadId, ...includeIds, maxPrev, maxNext],
410
- });
411
-
412
- if (includeResult.rows) {
413
- messages.push(...includeResult.rows.map((row: any) => this.parseRow(row)));
556
+ const includeMessages = await this._getIncludedMessages(threadId, selectBy);
557
+ if (includeMessages) {
558
+ messages.push(...includeMessages);
414
559
  }
415
560
  }
416
561
 
417
- // Get remaining messages, excluding already fetched IDs
418
562
  const excludeIds = messages.map(m => m.id);
419
563
  const remainingSql = `
420
- SELECT
421
- id,
422
- content,
423
- role,
424
- type,
425
- "createdAt",
426
- thread_id
427
- FROM "${TABLE_MESSAGES}"
428
- WHERE thread_id = ?
429
- ${excludeIds.length ? `AND id NOT IN (${excludeIds.map(() => '?').join(', ')})` : ''}
430
- ORDER BY "createdAt" DESC
431
- LIMIT ?
432
- `;
564
+ SELECT id, content, role, type, "createdAt", thread_id
565
+ FROM "${TABLE_MESSAGES}"
566
+ WHERE thread_id = ?
567
+ ${excludeIds.length ? `AND id NOT IN (${excludeIds.map(() => '?').join(', ')})` : ''}
568
+ ORDER BY "createdAt" DESC LIMIT ?
569
+ `;
433
570
  const remainingArgs = [threadId, ...(excludeIds.length ? excludeIds : []), limit];
434
-
435
- const remainingResult = await this.client.execute({
436
- sql: remainingSql,
437
- args: remainingArgs,
438
- });
439
-
571
+ const remainingResult = await this.client.execute({ sql: remainingSql, args: remainingArgs });
440
572
  if (remainingResult.rows) {
441
573
  messages.push(...remainingResult.rows.map((row: any) => this.parseRow(row)));
442
574
  }
443
-
444
- // Sort all messages by creation date
445
575
  messages.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
446
-
447
576
  const list = new MessageList().add(messages, 'memory');
448
577
  if (format === `v2`) return list.get.all.v2();
449
578
  return list.get.all.v1();
@@ -453,6 +582,82 @@ export class LibSQLStore extends MastraStorage {
453
582
  }
454
583
  }
455
584
 
585
+ public async getMessagesPaginated(
586
+ args: StorageGetMessagesArg & {
587
+ format?: 'v1' | 'v2';
588
+ },
589
+ ): Promise<PaginationInfo & { messages: MastraMessageV1[] | MastraMessageV2[] }> {
590
+ const { threadId, format, selectBy } = args;
591
+ const { page = 0, perPage = 40, dateRange } = selectBy?.pagination || {};
592
+ const fromDate = dateRange?.start;
593
+ const toDate = dateRange?.end;
594
+
595
+ const messages: MastraMessageV2[] = [];
596
+
597
+ if (selectBy?.include?.length) {
598
+ const includeMessages = await this._getIncludedMessages(threadId, selectBy);
599
+ if (includeMessages) {
600
+ messages.push(...includeMessages);
601
+ }
602
+ }
603
+
604
+ try {
605
+ const currentOffset = page * perPage;
606
+
607
+ const conditions: string[] = [`thread_id = ?`];
608
+ const queryParams: InValue[] = [threadId];
609
+
610
+ if (fromDate) {
611
+ conditions.push(`"createdAt" >= ?`);
612
+ queryParams.push(fromDate.toISOString());
613
+ }
614
+ if (toDate) {
615
+ conditions.push(`"createdAt" <= ?`);
616
+ queryParams.push(toDate.toISOString());
617
+ }
618
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
619
+
620
+ const countResult = await this.client.execute({
621
+ sql: `SELECT COUNT(*) as count FROM ${TABLE_MESSAGES} ${whereClause}`,
622
+ args: queryParams,
623
+ });
624
+ const total = Number(countResult.rows?.[0]?.count ?? 0);
625
+
626
+ if (total === 0) {
627
+ return {
628
+ messages: [],
629
+ total: 0,
630
+ page,
631
+ perPage,
632
+ hasMore: false,
633
+ };
634
+ }
635
+
636
+ const dataResult = await this.client.execute({
637
+ sql: `SELECT id, content, role, type, "createdAt", thread_id FROM ${TABLE_MESSAGES} ${whereClause} ORDER BY "createdAt" DESC LIMIT ? OFFSET ?`,
638
+ args: [...queryParams, perPage, currentOffset],
639
+ });
640
+
641
+ messages.push(...(dataResult.rows || []).map((row: any) => this.parseRow(row)));
642
+
643
+ const messagesToReturn =
644
+ format === 'v1'
645
+ ? new MessageList().add(messages, 'memory').get.all.v1()
646
+ : new MessageList().add(messages, 'memory').get.all.v2();
647
+
648
+ return {
649
+ messages: messagesToReturn,
650
+ total,
651
+ page,
652
+ perPage,
653
+ hasMore: currentOffset + messages.length < total,
654
+ };
655
+ } catch (error) {
656
+ this.logger.error('Error getting paginated messages:', error as Error);
657
+ return { messages: [], total: 0, page, perPage, hasMore: false };
658
+ }
659
+ }
660
+
456
661
  async saveMessages(args: { messages: MastraMessageV1[]; format?: undefined | 'v1' }): Promise<MastraMessageV1[]>;
457
662
  async saveMessages(args: { messages: MastraMessageV2[]; format: 'v2' }): Promise<MastraMessageV2[]>;
458
663
  async saveMessages({
@@ -526,6 +731,7 @@ export class LibSQLStore extends MastraStorage {
526
731
  };
527
732
  }
528
733
 
734
+ /** @deprecated use getEvals instead */
529
735
  async getEvalsByAgentName(agentName: string, type?: 'test' | 'live'): Promise<EvalRow[]> {
530
736
  try {
531
737
  const baseQuery = `SELECT * FROM ${TABLE_EVALS} WHERE agent_name = ?`;
@@ -552,120 +758,194 @@ export class LibSQLStore extends MastraStorage {
552
758
  }
553
759
  }
554
760
 
555
- // TODO: add types
556
- async getTraces(
557
- {
558
- name,
559
- scope,
761
+ async getEvals(
762
+ options: {
763
+ agentName?: string;
764
+ type?: 'test' | 'live';
765
+ } & PaginationArgs = {},
766
+ ): Promise<PaginationInfo & { evals: EvalRow[] }> {
767
+ const { agentName, type, page = 0, perPage = 100, dateRange } = options;
768
+ const fromDate = dateRange?.start;
769
+ const toDate = dateRange?.end;
770
+
771
+ const conditions: string[] = [];
772
+ const queryParams: InValue[] = [];
773
+
774
+ if (agentName) {
775
+ conditions.push(`agent_name = ?`);
776
+ queryParams.push(agentName);
777
+ }
778
+
779
+ if (type === 'test') {
780
+ conditions.push(`(test_info IS NOT NULL AND json_extract(test_info, '$.testPath') IS NOT NULL)`);
781
+ } else if (type === 'live') {
782
+ conditions.push(`(test_info IS NULL OR json_extract(test_info, '$.testPath') IS NULL)`);
783
+ }
784
+
785
+ if (fromDate) {
786
+ conditions.push(`created_at >= ?`);
787
+ queryParams.push(fromDate.toISOString());
788
+ }
789
+
790
+ if (toDate) {
791
+ conditions.push(`created_at <= ?`);
792
+ queryParams.push(toDate.toISOString());
793
+ }
794
+
795
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
796
+
797
+ const countResult = await this.client.execute({
798
+ sql: `SELECT COUNT(*) as count FROM ${TABLE_EVALS} ${whereClause}`,
799
+ args: queryParams,
800
+ });
801
+ const total = Number(countResult.rows?.[0]?.count ?? 0);
802
+
803
+ const currentOffset = page * perPage;
804
+ const hasMore = currentOffset + perPage < total;
805
+
806
+ if (total === 0) {
807
+ return {
808
+ evals: [],
809
+ total: 0,
810
+ page,
811
+ perPage,
812
+ hasMore: false,
813
+ };
814
+ }
815
+
816
+ const dataResult = await this.client.execute({
817
+ sql: `SELECT * FROM ${TABLE_EVALS} ${whereClause} ORDER BY created_at DESC LIMIT ? OFFSET ?`,
818
+ args: [...queryParams, perPage, currentOffset],
819
+ });
820
+
821
+ return {
822
+ evals: dataResult.rows?.map(row => this.transformEvalRow(row)) ?? [],
823
+ total,
560
824
  page,
561
825
  perPage,
562
- attributes,
563
- filters,
564
- fromDate,
565
- toDate,
566
- }: {
826
+ hasMore,
827
+ };
828
+ }
829
+
830
+ /**
831
+ * @deprecated use getTracesPaginated instead.
832
+ */
833
+ public async getTraces(args: {
834
+ name?: string;
835
+ scope?: string;
836
+ page: number;
837
+ perPage: number;
838
+ attributes?: Record<string, string>;
839
+ filters?: Record<string, any>;
840
+ fromDate?: Date;
841
+ toDate?: Date;
842
+ }): Promise<Trace[]> {
843
+ if (args.fromDate || args.toDate) {
844
+ (args as any).dateRange = {
845
+ start: args.fromDate,
846
+ end: args.toDate,
847
+ };
848
+ }
849
+ const result = await this.getTracesPaginated(args);
850
+ return result.traces;
851
+ }
852
+
853
+ public async getTracesPaginated(
854
+ args: {
567
855
  name?: string;
568
856
  scope?: string;
569
- page: number;
570
- perPage: number;
571
857
  attributes?: Record<string, string>;
572
858
  filters?: Record<string, any>;
573
- fromDate?: Date;
574
- toDate?: Date;
575
- } = {
576
- page: 0,
577
- perPage: 100,
578
- },
579
- ): Promise<any[]> {
580
- const limit = perPage;
581
- const offset = page * perPage;
582
-
583
- const args: (string | number)[] = [];
584
-
859
+ } & PaginationArgs,
860
+ ): Promise<PaginationInfo & { traces: Trace[] }> {
861
+ const { name, scope, page = 0, perPage = 100, attributes, filters, dateRange } = args;
862
+ const fromDate = dateRange?.start;
863
+ const toDate = dateRange?.end;
864
+ const currentOffset = page * perPage;
865
+
866
+ const queryArgs: InValue[] = [];
585
867
  const conditions: string[] = [];
868
+
586
869
  if (name) {
587
- conditions.push("name LIKE CONCAT(?, '%')");
870
+ conditions.push('name LIKE ?');
871
+ queryArgs.push(`${name}%`);
588
872
  }
589
873
  if (scope) {
590
874
  conditions.push('scope = ?');
875
+ queryArgs.push(scope);
591
876
  }
592
877
  if (attributes) {
593
- Object.keys(attributes).forEach(key => {
594
- conditions.push(`attributes->>'$.${key}' = ?`);
878
+ Object.entries(attributes).forEach(([key, value]) => {
879
+ conditions.push(`json_extract(attributes, '$.${key}') = ?`);
880
+ queryArgs.push(value);
595
881
  });
596
882
  }
597
-
598
883
  if (filters) {
599
- Object.entries(filters).forEach(([key, _value]) => {
600
- conditions.push(`${key} = ?`);
884
+ Object.entries(filters).forEach(([key, value]) => {
885
+ conditions.push(`${parseSqlIdentifier(key, 'filter key')} = ?`);
886
+ queryArgs.push(value);
601
887
  });
602
888
  }
603
-
604
889
  if (fromDate) {
605
890
  conditions.push('createdAt >= ?');
891
+ queryArgs.push(fromDate.toISOString());
606
892
  }
607
-
608
893
  if (toDate) {
609
894
  conditions.push('createdAt <= ?');
895
+ queryArgs.push(toDate.toISOString());
610
896
  }
611
897
 
612
898
  const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
613
899
 
614
- if (name) {
615
- args.push(name);
616
- }
617
-
618
- if (scope) {
619
- args.push(scope);
620
- }
621
-
622
- if (attributes) {
623
- for (const [, value] of Object.entries(attributes)) {
624
- args.push(value);
625
- }
626
- }
627
-
628
- if (filters) {
629
- for (const [, value] of Object.entries(filters)) {
630
- args.push(value);
631
- }
632
- }
633
-
634
- if (fromDate) {
635
- args.push(fromDate.toISOString());
636
- }
637
-
638
- if (toDate) {
639
- args.push(toDate.toISOString());
900
+ const countResult = await this.client.execute({
901
+ sql: `SELECT COUNT(*) as count FROM ${TABLE_TRACES} ${whereClause}`,
902
+ args: queryArgs,
903
+ });
904
+ const total = Number(countResult.rows?.[0]?.count ?? 0);
905
+
906
+ if (total === 0) {
907
+ return {
908
+ traces: [],
909
+ total: 0,
910
+ page,
911
+ perPage,
912
+ hasMore: false,
913
+ };
640
914
  }
641
915
 
642
- args.push(limit, offset);
643
-
644
- const result = await this.client.execute({
916
+ const dataResult = await this.client.execute({
645
917
  sql: `SELECT * FROM ${TABLE_TRACES} ${whereClause} ORDER BY "startTime" DESC LIMIT ? OFFSET ?`,
646
- args,
918
+ args: [...queryArgs, perPage, currentOffset],
647
919
  });
648
920
 
649
- if (!result.rows) {
650
- return [];
651
- }
921
+ const traces =
922
+ dataResult.rows?.map(
923
+ row =>
924
+ ({
925
+ id: row.id,
926
+ parentSpanId: row.parentSpanId,
927
+ traceId: row.traceId,
928
+ name: row.name,
929
+ scope: row.scope,
930
+ kind: row.kind,
931
+ status: safelyParseJSON(row.status as string),
932
+ events: safelyParseJSON(row.events as string),
933
+ links: safelyParseJSON(row.links as string),
934
+ attributes: safelyParseJSON(row.attributes as string),
935
+ startTime: row.startTime,
936
+ endTime: row.endTime,
937
+ other: safelyParseJSON(row.other as string),
938
+ createdAt: row.createdAt,
939
+ }) as Trace,
940
+ ) ?? [];
652
941
 
653
- return result.rows.map(row => ({
654
- id: row.id,
655
- parentSpanId: row.parentSpanId,
656
- traceId: row.traceId,
657
- name: row.name,
658
- scope: row.scope,
659
- kind: row.kind,
660
- status: safelyParseJSON(row.status as string),
661
- events: safelyParseJSON(row.events as string),
662
- links: safelyParseJSON(row.links as string),
663
- attributes: safelyParseJSON(row.attributes as string),
664
- startTime: row.startTime,
665
- endTime: row.endTime,
666
- other: safelyParseJSON(row.other as string),
667
- createdAt: row.createdAt,
668
- })) as any;
942
+ return {
943
+ traces,
944
+ total,
945
+ page,
946
+ perPage,
947
+ hasMore: currentOffset + traces.length < total,
948
+ };
669
949
  }
670
950
 
671
951
  async getWorkflowRuns({