@mastra/pg 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.
@@ -12,11 +12,13 @@ import {
12
12
  } from '@mastra/core/storage';
13
13
  import type {
14
14
  EvalRow,
15
+ PaginationInfo,
15
16
  StorageColumn,
16
17
  StorageGetMessagesArg,
17
18
  TABLE_NAMES,
18
19
  WorkflowRun,
19
20
  WorkflowRuns,
21
+ PaginationArgs,
20
22
  } from '@mastra/core/storage';
21
23
  import { parseSqlIdentifier } from '@mastra/core/utils';
22
24
  import type { WorkflowRunState } from '@mastra/core/workflows';
@@ -91,6 +93,7 @@ export class PostgresStore extends MastraStorage {
91
93
  return parsedSchemaName ? `${parsedSchemaName}."${parsedIndexName}"` : `"${parsedIndexName}"`;
92
94
  }
93
95
 
96
+ /** @deprecated use getEvals instead */
94
97
  async getEvalsByAgentName(agentName: string, type?: 'test' | 'live'): Promise<EvalRow[]> {
95
98
  try {
96
99
  const baseQuery = `SELECT * FROM ${this.getTableName(TABLE_EVALS)} WHERE agent_name = $1`;
@@ -153,115 +156,107 @@ export class PostgresStore extends MastraStorage {
153
156
  }
154
157
  }
155
158
 
156
- async getTraces({
157
- name,
158
- scope,
159
- page,
160
- perPage,
161
- attributes,
162
- filters,
163
- fromDate,
164
- toDate,
165
- }: {
159
+ /**
160
+ * @deprecated use getTracesPaginated instead
161
+ */
162
+ public async getTraces(args: {
166
163
  name?: string;
167
164
  scope?: string;
168
- page: number;
169
- perPage: number;
170
165
  attributes?: Record<string, string>;
171
166
  filters?: Record<string, any>;
167
+ page: number;
168
+ perPage?: number;
172
169
  fromDate?: Date;
173
170
  toDate?: Date;
174
171
  }): Promise<any[]> {
175
- let idx = 1;
176
- const limit = perPage;
177
- const offset = page * perPage;
172
+ if (args.fromDate || args.toDate) {
173
+ (args as any).dateRange = {
174
+ start: args.fromDate,
175
+ end: args.toDate,
176
+ };
177
+ }
178
+ const result = await this.getTracesPaginated(args);
179
+ return result.traces;
180
+ }
181
+
182
+ public async getTracesPaginated(
183
+ args: {
184
+ name?: string;
185
+ scope?: string;
186
+ attributes?: Record<string, string>;
187
+ filters?: Record<string, any>;
188
+ } & PaginationArgs,
189
+ ): Promise<
190
+ PaginationInfo & {
191
+ traces: any[];
192
+ }
193
+ > {
194
+ const { name, scope, page = 0, perPage: perPageInput, attributes, filters, dateRange } = args;
195
+ const fromDate = dateRange?.start;
196
+ const toDate = dateRange?.end;
178
197
 
179
- const args: (string | number)[] = [];
198
+ const perPage = perPageInput !== undefined ? perPageInput : 100; // Default perPage
199
+ const currentOffset = page * perPage;
180
200
 
201
+ const queryParams: any[] = [];
181
202
  const conditions: string[] = [];
203
+ let paramIndex = 1;
204
+
182
205
  if (name) {
183
- conditions.push(`name LIKE CONCAT(\$${idx++}, '%')`);
206
+ conditions.push(`name LIKE $${paramIndex++}`);
207
+ queryParams.push(`${name}%`); // Add wildcard for LIKE
184
208
  }
185
209
  if (scope) {
186
- conditions.push(`scope = \$${idx++}`);
210
+ conditions.push(`scope = $${paramIndex++}`);
211
+ queryParams.push(scope);
187
212
  }
188
213
  if (attributes) {
189
- Object.keys(attributes).forEach(key => {
214
+ Object.entries(attributes).forEach(([key, value]) => {
190
215
  const parsedKey = parseSqlIdentifier(key, 'attribute key');
191
- conditions.push(`attributes->>'${parsedKey}' = \$${idx++}`);
216
+ conditions.push(`attributes->>'${parsedKey}' = $${paramIndex++}`);
217
+ queryParams.push(value);
192
218
  });
193
219
  }
194
-
195
220
  if (filters) {
196
- Object.entries(filters).forEach(([key]) => {
221
+ Object.entries(filters).forEach(([key, value]) => {
197
222
  const parsedKey = parseSqlIdentifier(key, 'filter key');
198
- conditions.push(`${parsedKey} = \$${idx++}`);
223
+ conditions.push(`"${parsedKey}" = $${paramIndex++}`); // Ensure filter keys are quoted if they are column names
224
+ queryParams.push(value);
199
225
  });
200
226
  }
201
-
202
227
  if (fromDate) {
203
- conditions.push(`createdAt >= \$${idx++}`);
228
+ conditions.push(`"createdAt" >= $${paramIndex++}`);
229
+ queryParams.push(fromDate);
204
230
  }
205
-
206
231
  if (toDate) {
207
- conditions.push(`createdAt <= \$${idx++}`);
232
+ conditions.push(`"createdAt" <= $${paramIndex++}`);
233
+ queryParams.push(toDate);
208
234
  }
209
235
 
210
236
  const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
211
237
 
212
- if (name) {
213
- args.push(name);
214
- }
215
-
216
- if (scope) {
217
- args.push(scope);
218
- }
219
-
220
- if (attributes) {
221
- for (const [_key, value] of Object.entries(attributes)) {
222
- args.push(value);
223
- }
224
- }
225
-
226
- if (filters) {
227
- for (const [, value] of Object.entries(filters)) {
228
- args.push(value);
229
- }
230
- }
238
+ // Get total count
239
+ const countQuery = `SELECT COUNT(*) FROM ${this.getTableName(TABLE_TRACES)} ${whereClause}`;
240
+ const countResult = await this.db.one(countQuery, queryParams);
241
+ const total = parseInt(countResult.count, 10);
231
242
 
232
- if (fromDate) {
233
- args.push(fromDate.toISOString());
243
+ if (total === 0) {
244
+ return {
245
+ traces: [],
246
+ total: 0,
247
+ page,
248
+ perPage,
249
+ hasMore: false,
250
+ };
234
251
  }
235
252
 
236
- if (toDate) {
237
- args.push(toDate.toISOString());
238
- }
239
-
240
- const result = await this.db.manyOrNone<{
241
- id: string;
242
- parentSpanId: string;
243
- traceId: string;
244
- name: string;
245
- scope: string;
246
- kind: string;
247
- events: any[];
248
- links: any[];
249
- status: any;
250
- attributes: Record<string, any>;
251
- startTime: string;
252
- endTime: string;
253
- other: any;
254
- createdAt: string;
255
- }>(
256
- `SELECT * FROM ${this.getTableName(TABLE_TRACES)} ${whereClause} ORDER BY "createdAt" DESC LIMIT ${limit} OFFSET ${offset}`,
257
- args,
258
- );
253
+ const dataQuery = `SELECT * FROM ${this.getTableName(
254
+ TABLE_TRACES,
255
+ )} ${whereClause} ORDER BY "createdAt" DESC LIMIT $${paramIndex++} OFFSET $${paramIndex++}`;
256
+ const finalQueryParams = [...queryParams, perPage, currentOffset];
259
257
 
260
- if (!result) {
261
- return [];
262
- }
263
-
264
- return result.map(row => ({
258
+ const rows = await this.db.manyOrNone<any>(dataQuery, finalQueryParams);
259
+ const traces = rows.map(row => ({
265
260
  id: row.id,
266
261
  parentSpanId: row.parentSpanId,
267
262
  traceId: row.traceId,
@@ -276,7 +271,15 @@ export class PostgresStore extends MastraStorage {
276
271
  endTime: row.endTime,
277
272
  other: row.other,
278
273
  createdAt: row.createdAt,
279
- })) as any;
274
+ }));
275
+
276
+ return {
277
+ traces,
278
+ total,
279
+ page,
280
+ perPage,
281
+ hasMore: currentOffset + traces.length < total,
282
+ };
280
283
  }
281
284
 
282
285
  private async setupSchema() {
@@ -379,6 +382,57 @@ export class PostgresStore extends MastraStorage {
379
382
  }
380
383
  }
381
384
 
385
+ protected getDefaultValue(type: StorageColumn['type']): string {
386
+ switch (type) {
387
+ case 'timestamp':
388
+ return 'DEFAULT NOW()';
389
+ case 'jsonb':
390
+ return "DEFAULT '{}'::jsonb";
391
+ default:
392
+ return super.getDefaultValue(type);
393
+ }
394
+ }
395
+
396
+ /**
397
+ * Alters table schema to add columns if they don't exist
398
+ * @param tableName Name of the table
399
+ * @param schema Schema of the table
400
+ * @param ifNotExists Array of column names to add if they don't exist
401
+ */
402
+ async alterTable({
403
+ tableName,
404
+ schema,
405
+ ifNotExists,
406
+ }: {
407
+ tableName: TABLE_NAMES;
408
+ schema: Record<string, StorageColumn>;
409
+ ifNotExists: string[];
410
+ }): Promise<void> {
411
+ const fullTableName = this.getTableName(tableName);
412
+
413
+ try {
414
+ for (const columnName of ifNotExists) {
415
+ if (schema[columnName]) {
416
+ const columnDef = schema[columnName];
417
+ const sqlType = this.getSqlType(columnDef.type);
418
+ const nullable = columnDef.nullable === false ? 'NOT NULL' : '';
419
+ const defaultValue = columnDef.nullable === false ? this.getDefaultValue(columnDef.type) : '';
420
+ const parsedColumnName = parseSqlIdentifier(columnName, 'column name');
421
+ const alterSql =
422
+ `ALTER TABLE ${fullTableName} ADD COLUMN IF NOT EXISTS "${parsedColumnName}" ${sqlType} ${nullable} ${defaultValue}`.trim();
423
+
424
+ await this.db.none(alterSql);
425
+ this.logger?.debug?.(`Ensured column ${parsedColumnName} exists in table ${fullTableName}`);
426
+ }
427
+ }
428
+ } catch (error) {
429
+ this.logger?.error?.(
430
+ `Error altering table ${tableName}: ${error instanceof Error ? error.message : String(error)}`,
431
+ );
432
+ throw new Error(`Failed to alter table ${tableName}: ${error}`);
433
+ }
434
+ }
435
+
382
436
  async clearTable({ tableName }: { tableName: TABLE_NAMES }): Promise<void> {
383
437
  try {
384
438
  await this.db.none(`TRUNCATE TABLE ${this.getTableName(tableName)} CASCADE`);
@@ -466,30 +520,76 @@ export class PostgresStore extends MastraStorage {
466
520
  }
467
521
  }
468
522
 
469
- async getThreadsByResourceId({ resourceId }: { resourceId: string }): Promise<StorageThreadType[]> {
523
+ /**
524
+ * @deprecated use getThreadsByResourceIdPaginated instead
525
+ */
526
+ public async getThreadsByResourceId(args: { resourceId: string }): Promise<StorageThreadType[]> {
527
+ const { resourceId } = args;
528
+
470
529
  try {
471
- const threads = await this.db.manyOrNone<StorageThreadType>(
472
- `SELECT
473
- id,
474
- "resourceId",
475
- title,
476
- metadata,
477
- "createdAt",
478
- "updatedAt"
479
- FROM ${this.getTableName(TABLE_THREADS)}
480
- WHERE "resourceId" = $1`,
481
- [resourceId],
482
- );
530
+ const baseQuery = `FROM ${this.getTableName(TABLE_THREADS)} WHERE "resourceId" = $1`;
531
+ const queryParams: any[] = [resourceId];
483
532
 
484
- return threads.map(thread => ({
533
+ const dataQuery = `SELECT id, "resourceId", title, metadata, "createdAt", "updatedAt" ${baseQuery} ORDER BY "createdAt" DESC`;
534
+ const rows = await this.db.manyOrNone(dataQuery, queryParams);
535
+ return (rows || []).map(thread => ({
485
536
  ...thread,
486
537
  metadata: typeof thread.metadata === 'string' ? JSON.parse(thread.metadata) : thread.metadata,
487
538
  createdAt: thread.createdAt,
488
539
  updatedAt: thread.updatedAt,
489
540
  }));
490
541
  } catch (error) {
491
- console.error(`Error getting threads for resource ${resourceId}:`, error);
492
- throw error;
542
+ this.logger.error(`Error getting threads for resource ${resourceId}:`, error);
543
+ return [];
544
+ }
545
+ }
546
+
547
+ public async getThreadsByResourceIdPaginated(
548
+ args: {
549
+ resourceId: string;
550
+ } & PaginationArgs,
551
+ ): Promise<PaginationInfo & { threads: StorageThreadType[] }> {
552
+ const { resourceId, page = 0, perPage: perPageInput } = args;
553
+ try {
554
+ const baseQuery = `FROM ${this.getTableName(TABLE_THREADS)} WHERE "resourceId" = $1`;
555
+ const queryParams: any[] = [resourceId];
556
+ const perPage = perPageInput !== undefined ? perPageInput : 100;
557
+ const currentOffset = page * perPage;
558
+
559
+ const countQuery = `SELECT COUNT(*) ${baseQuery}`;
560
+ const countResult = await this.db.one(countQuery, queryParams);
561
+ const total = parseInt(countResult.count, 10);
562
+
563
+ if (total === 0) {
564
+ return {
565
+ threads: [],
566
+ total: 0,
567
+ page,
568
+ perPage,
569
+ hasMore: false,
570
+ };
571
+ }
572
+
573
+ const dataQuery = `SELECT id, "resourceId", title, metadata, "createdAt", "updatedAt" ${baseQuery} ORDER BY "createdAt" DESC LIMIT $2 OFFSET $3`;
574
+ const rows = await this.db.manyOrNone(dataQuery, [...queryParams, perPage, currentOffset]);
575
+
576
+ const threads = (rows || []).map(thread => ({
577
+ ...thread,
578
+ metadata: typeof thread.metadata === 'string' ? JSON.parse(thread.metadata) : thread.metadata,
579
+ createdAt: thread.createdAt, // Assuming already Date objects or ISO strings
580
+ updatedAt: thread.updatedAt,
581
+ }));
582
+
583
+ return {
584
+ threads,
585
+ total,
586
+ page,
587
+ perPage,
588
+ hasMore: currentOffset + threads.length < total,
589
+ };
590
+ } catch (error) {
591
+ this.logger.error(`Error getting threads for resource ${resourceId}:`, error);
592
+ return { threads: [], total: 0, page, perPage: perPageInput || 100, hasMore: false };
493
593
  }
494
594
  }
495
595
 
@@ -586,95 +686,174 @@ export class PostgresStore extends MastraStorage {
586
686
  }
587
687
  }
588
688
 
589
- async getMessages<T = unknown>({ threadId, selectBy }: StorageGetMessagesArg): Promise<T[]> {
689
+ /**
690
+ * @deprecated use getMessagesPaginated instead
691
+ */
692
+ public async getMessages(args: StorageGetMessagesArg & { format?: 'v1' }): Promise<MastraMessageV1[]>;
693
+ public async getMessages(args: StorageGetMessagesArg & { format: 'v2' }): Promise<MastraMessageV2[]>;
694
+ public async getMessages(
695
+ args: StorageGetMessagesArg & {
696
+ format?: 'v1' | 'v2';
697
+ },
698
+ ): Promise<MastraMessageV1[] | MastraMessageV2[]> {
699
+ const { threadId, format, selectBy } = args;
700
+
701
+ const selectStatement = `SELECT id, content, role, type, "createdAt", thread_id AS "threadId"`;
702
+ const orderByStatement = `ORDER BY "createdAt" DESC`;
703
+
590
704
  try {
591
- const messages: any[] = [];
592
- const limit = typeof selectBy?.last === `number` ? selectBy.last : 40;
705
+ let rows: any[] = [];
593
706
  const include = selectBy?.include || [];
594
707
 
595
708
  if (include.length) {
596
- const includeResult = await this.db.manyOrNone(
709
+ rows = await this.db.manyOrNone(
597
710
  `
598
- WITH ordered_messages AS (
599
- SELECT
600
- *,
601
- ROW_NUMBER() OVER (ORDER BY "createdAt" DESC) as row_num
602
- FROM ${this.getTableName(TABLE_MESSAGES)}
603
- WHERE thread_id = $1
604
- )
605
- SELECT
606
- m.id,
607
- m.content,
608
- m.role,
609
- m.type,
610
- m."createdAt",
611
- m.thread_id AS "threadId"
612
- FROM ordered_messages m
613
- WHERE m.id = ANY($2)
614
- OR EXISTS (
615
- SELECT 1 FROM ordered_messages target
616
- WHERE target.id = ANY($2)
617
- AND (
618
- -- Get previous messages based on the max withPreviousMessages
619
- (m.row_num <= target.row_num + $3 AND m.row_num > target.row_num)
620
- OR
621
- -- Get next messages based on the max withNextMessages
622
- (m.row_num >= target.row_num - $4 AND m.row_num < target.row_num)
711
+ WITH ordered_messages AS (
712
+ SELECT
713
+ *,
714
+ ROW_NUMBER() OVER (${orderByStatement}) as row_num
715
+ FROM ${this.getTableName(TABLE_MESSAGES)}
716
+ WHERE thread_id = $1
623
717
  )
624
- )
625
- ORDER BY m."createdAt" DESC
626
- `,
718
+ SELECT
719
+ m.id,
720
+ m.content,
721
+ m.role,
722
+ m.type,
723
+ m."createdAt",
724
+ m.thread_id AS "threadId"
725
+ FROM ordered_messages m
726
+ WHERE m.id = ANY($2)
727
+ OR EXISTS (
728
+ SELECT 1 FROM ordered_messages target
729
+ WHERE target.id = ANY($2)
730
+ AND (
731
+ -- Get previous messages based on the max withPreviousMessages
732
+ (m.row_num <= target.row_num + $3 AND m.row_num > target.row_num)
733
+ OR
734
+ -- Get next messages based on the max withNextMessages
735
+ (m.row_num >= target.row_num - $4 AND m.row_num < target.row_num)
736
+ )
737
+ )
738
+ ORDER BY m."createdAt" ASC
739
+ `, // Keep ASC for final sorting after fetching context
627
740
  [
628
741
  threadId,
629
742
  include.map(i => i.id),
630
- Math.max(...include.map(i => i.withPreviousMessages || 0)),
631
- Math.max(...include.map(i => i.withNextMessages || 0)),
743
+ Math.max(0, ...include.map(i => i.withPreviousMessages || 0)), // Ensure non-negative
744
+ Math.max(0, ...include.map(i => i.withNextMessages || 0)), // Ensure non-negative
632
745
  ],
633
746
  );
634
-
635
- messages.push(...includeResult);
747
+ } else {
748
+ const limit = typeof selectBy?.last === `number` ? selectBy.last : 40;
749
+ if (limit === 0 && selectBy?.last !== false) {
750
+ // if last is explicitly false, we fetch all
751
+ // Do nothing, rows will be empty, and we return empty array later.
752
+ } else {
753
+ let query = `${selectStatement} FROM ${this.getTableName(
754
+ TABLE_MESSAGES,
755
+ )} WHERE thread_id = $1 ${orderByStatement}`;
756
+ const queryParams: any[] = [threadId];
757
+ if (limit !== undefined && selectBy?.last !== false) {
758
+ query += ` LIMIT $2`;
759
+ queryParams.push(limit);
760
+ }
761
+ rows = await this.db.manyOrNone(query, queryParams);
762
+ }
636
763
  }
637
764
 
638
- // Then get the remaining messages, excluding the ids we just fetched
639
- const result = await this.db.manyOrNone(
640
- `
641
- SELECT
642
- id,
643
- content,
644
- role,
645
- type,
646
- "createdAt",
647
- thread_id AS "threadId"
648
- FROM ${this.getTableName(TABLE_MESSAGES)}
649
- WHERE thread_id = $1
650
- AND id != ALL($2)
651
- ORDER BY "createdAt" DESC
652
- LIMIT $3
653
- `,
654
- [threadId, messages.map(m => m.id), limit],
655
- );
656
-
657
- messages.push(...result);
658
-
659
- // Sort all messages by creation date
660
- messages.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
661
-
662
- // Parse message content
663
- messages.forEach(message => {
765
+ const fetchedMessages = (rows || []).map(message => {
664
766
  if (typeof message.content === 'string') {
665
767
  try {
666
768
  message.content = JSON.parse(message.content);
667
769
  } catch {
668
- // If parsing fails, leave as string
770
+ /* ignore */
669
771
  }
670
772
  }
671
- if (message.type === `v2`) delete message.type;
773
+ if (message.type === 'v2') delete message.type;
774
+ return message as MastraMessageV1;
672
775
  });
673
776
 
674
- return messages as T[];
777
+ // Sort all messages by creation date
778
+ const sortedMessages = fetchedMessages.sort(
779
+ (a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(),
780
+ );
781
+
782
+ return format === 'v2'
783
+ ? sortedMessages.map(
784
+ m =>
785
+ ({ ...m, content: m.content || { format: 2, parts: [{ type: 'text', text: '' }] } }) as MastraMessageV2,
786
+ )
787
+ : sortedMessages;
675
788
  } catch (error) {
676
- console.error('Error getting messages:', error);
677
- throw error;
789
+ this.logger.error('Error getting messages:', error);
790
+ return [];
791
+ }
792
+ }
793
+
794
+ public async getMessagesPaginated(
795
+ args: StorageGetMessagesArg & {
796
+ format?: 'v1' | 'v2';
797
+ },
798
+ ): Promise<PaginationInfo & { messages: MastraMessageV1[] | MastraMessageV2[] }> {
799
+ const { threadId, format, selectBy } = args;
800
+ const { page = 0, perPage: perPageInput, dateRange } = selectBy?.pagination || {};
801
+ const fromDate = dateRange?.start;
802
+ const toDate = dateRange?.end;
803
+
804
+ const selectStatement = `SELECT id, content, role, type, "createdAt", thread_id AS "threadId"`;
805
+ const orderByStatement = `ORDER BY "createdAt" DESC`;
806
+
807
+ try {
808
+ const perPage = perPageInput !== undefined ? perPageInput : 40;
809
+ const currentOffset = page * perPage;
810
+
811
+ const conditions: string[] = [`thread_id = $1`];
812
+ const queryParams: any[] = [threadId];
813
+ let paramIndex = 2;
814
+
815
+ if (fromDate) {
816
+ conditions.push(`"createdAt" >= $${paramIndex++}`);
817
+ queryParams.push(fromDate);
818
+ }
819
+ if (toDate) {
820
+ conditions.push(`"createdAt" <= $${paramIndex++}`);
821
+ queryParams.push(toDate);
822
+ }
823
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
824
+
825
+ const countQuery = `SELECT COUNT(*) FROM ${this.getTableName(TABLE_MESSAGES)} ${whereClause}`;
826
+ const countResult = await this.db.one(countQuery, queryParams);
827
+ const total = parseInt(countResult.count, 10);
828
+
829
+ if (total === 0) {
830
+ return {
831
+ messages: [],
832
+ total: 0,
833
+ page,
834
+ perPage,
835
+ hasMore: false,
836
+ };
837
+ }
838
+
839
+ const dataQuery = `${selectStatement} FROM ${this.getTableName(
840
+ TABLE_MESSAGES,
841
+ )} ${whereClause} ${orderByStatement} LIMIT $${paramIndex++} OFFSET $${paramIndex++}`;
842
+ const rows = await this.db.manyOrNone(dataQuery, [...queryParams, perPage, currentOffset]);
843
+
844
+ const list = new MessageList().add(rows || [], 'memory');
845
+ const messagesToReturn = format === `v2` ? list.get.all.v2() : list.get.all.v1();
846
+
847
+ return {
848
+ messages: messagesToReturn,
849
+ total,
850
+ page,
851
+ perPage,
852
+ hasMore: currentOffset + rows.length < total,
853
+ };
854
+ } catch (error) {
855
+ this.logger.error('Error getting messages:', error);
856
+ return { messages: [], total: 0, page, perPage: perPageInput || 40, hasMore: false };
678
857
  }
679
858
  }
680
859
 
@@ -948,4 +1127,70 @@ export class PostgresStore extends MastraStorage {
948
1127
  async close(): Promise<void> {
949
1128
  this.pgp.end();
950
1129
  }
1130
+
1131
+ async getEvals(
1132
+ options: {
1133
+ agentName?: string;
1134
+ type?: 'test' | 'live';
1135
+ } & PaginationArgs = {},
1136
+ ): Promise<PaginationInfo & { evals: EvalRow[] }> {
1137
+ const { agentName, type, page = 0, perPage = 100, dateRange } = options;
1138
+ const fromDate = dateRange?.start;
1139
+ const toDate = dateRange?.end;
1140
+
1141
+ const conditions: string[] = [];
1142
+ const queryParams: any[] = [];
1143
+ let paramIndex = 1;
1144
+
1145
+ if (agentName) {
1146
+ conditions.push(`agent_name = $${paramIndex++}`);
1147
+ queryParams.push(agentName);
1148
+ }
1149
+
1150
+ if (type === 'test') {
1151
+ conditions.push(`(test_info IS NOT NULL AND test_info->>'testPath' IS NOT NULL)`);
1152
+ } else if (type === 'live') {
1153
+ conditions.push(`(test_info IS NULL OR test_info->>'testPath' IS NULL)`);
1154
+ }
1155
+
1156
+ if (fromDate) {
1157
+ conditions.push(`created_at >= $${paramIndex++}`);
1158
+ queryParams.push(fromDate);
1159
+ }
1160
+
1161
+ if (toDate) {
1162
+ conditions.push(`created_at <= $${paramIndex++}`);
1163
+ queryParams.push(toDate);
1164
+ }
1165
+
1166
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
1167
+
1168
+ const countQuery = `SELECT COUNT(*) FROM ${this.getTableName(TABLE_EVALS)} ${whereClause}`;
1169
+ const countResult = await this.db.one(countQuery, queryParams);
1170
+ const total = parseInt(countResult.count, 10);
1171
+ const currentOffset = page * perPage;
1172
+
1173
+ if (total === 0) {
1174
+ return {
1175
+ evals: [],
1176
+ total: 0,
1177
+ page,
1178
+ perPage,
1179
+ hasMore: false,
1180
+ };
1181
+ }
1182
+
1183
+ const dataQuery = `SELECT * FROM ${this.getTableName(
1184
+ TABLE_EVALS,
1185
+ )} ${whereClause} ORDER BY created_at DESC LIMIT $${paramIndex++} OFFSET $${paramIndex++}`;
1186
+ const rows = await this.db.manyOrNone(dataQuery, [...queryParams, perPage, currentOffset]);
1187
+
1188
+ return {
1189
+ evals: rows?.map(row => this.transformEvalRow(row)) ?? [],
1190
+ total,
1191
+ page,
1192
+ perPage,
1193
+ hasMore: currentOffset + (rows?.length ?? 0) < total,
1194
+ };
1195
+ }
951
1196
  }