@mastra/pg 0.11.0 → 0.11.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.
@@ -1,5 +1,6 @@
1
1
  import { MessageList } from '@mastra/core/agent';
2
- import type { MastraMessageV2 } from '@mastra/core/agent';
2
+ import type { MastraMessageContentV2, MastraMessageV2 } from '@mastra/core/agent';
3
+ import { ErrorCategory, ErrorDomain, MastraError } from '@mastra/core/error';
3
4
  import type { MetricResult } from '@mastra/core/eval';
4
5
  import type { MastraMessageV1, StorageThreadType } from '@mastra/core/memory';
5
6
  import {
@@ -50,41 +51,52 @@ export class PostgresStore extends MastraStorage {
50
51
 
51
52
  constructor(config: PostgresConfig) {
52
53
  // Validation: connectionString or host/database/user/password must not be empty
53
- if ('connectionString' in config) {
54
- if (
55
- !config.connectionString ||
56
- typeof config.connectionString !== 'string' ||
57
- config.connectionString.trim() === ''
58
- ) {
59
- throw new Error(
60
- 'PostgresStore: connectionString must be provided and cannot be empty. Passing an empty string may cause fallback to local Postgres defaults.',
61
- );
62
- }
63
- } else {
64
- const required = ['host', 'database', 'user', 'password'];
65
- for (const key of required) {
66
- if (!(key in config) || typeof (config as any)[key] !== 'string' || (config as any)[key].trim() === '') {
54
+ try {
55
+ if ('connectionString' in config) {
56
+ if (
57
+ !config.connectionString ||
58
+ typeof config.connectionString !== 'string' ||
59
+ config.connectionString.trim() === ''
60
+ ) {
67
61
  throw new Error(
68
- `PostgresStore: ${key} must be provided and cannot be empty. Passing an empty string may cause fallback to local Postgres defaults.`,
62
+ 'PostgresStore: connectionString must be provided and cannot be empty. Passing an empty string may cause fallback to local Postgres defaults.',
69
63
  );
70
64
  }
65
+ } else {
66
+ const required = ['host', 'database', 'user', 'password'];
67
+ for (const key of required) {
68
+ if (!(key in config) || typeof (config as any)[key] !== 'string' || (config as any)[key].trim() === '') {
69
+ throw new Error(
70
+ `PostgresStore: ${key} must be provided and cannot be empty. Passing an empty string may cause fallback to local Postgres defaults.`,
71
+ );
72
+ }
73
+ }
71
74
  }
75
+ super({ name: 'PostgresStore' });
76
+ this.pgp = pgPromise();
77
+ this.schema = config.schemaName;
78
+ this.db = this.pgp(
79
+ `connectionString` in config
80
+ ? { connectionString: config.connectionString }
81
+ : {
82
+ host: config.host,
83
+ port: config.port,
84
+ database: config.database,
85
+ user: config.user,
86
+ password: config.password,
87
+ ssl: config.ssl,
88
+ },
89
+ );
90
+ } catch (e) {
91
+ throw new MastraError(
92
+ {
93
+ id: 'MASTRA_STORAGE_PG_STORE_INITIALIZATION_FAILED',
94
+ domain: ErrorDomain.STORAGE,
95
+ category: ErrorCategory.USER,
96
+ },
97
+ e,
98
+ );
72
99
  }
73
- super({ name: 'PostgresStore' });
74
- this.pgp = pgPromise();
75
- this.schema = config.schemaName;
76
- this.db = this.pgp(
77
- `connectionString` in config
78
- ? { connectionString: config.connectionString }
79
- : {
80
- host: config.host,
81
- port: config.port,
82
- database: config.database,
83
- user: config.user,
84
- password: config.password,
85
- ssl: config.ssl,
86
- },
87
- );
88
100
  }
89
101
 
90
102
  public get supports(): {
@@ -163,9 +175,19 @@ export class PostgresStore extends MastraStorage {
163
175
  }
164
176
  await this.db.query('COMMIT');
165
177
  } catch (error) {
166
- console.error(`Error inserting into ${tableName}:`, error);
167
178
  await this.db.query('ROLLBACK');
168
- throw error;
179
+ throw new MastraError(
180
+ {
181
+ id: 'MASTRA_STORAGE_PG_STORE_BATCH_INSERT_FAILED',
182
+ domain: ErrorDomain.STORAGE,
183
+ category: ErrorCategory.THIRD_PARTY,
184
+ details: {
185
+ tableName,
186
+ numberOfRecords: records.length,
187
+ },
188
+ },
189
+ error,
190
+ );
169
191
  }
170
192
  }
171
193
 
@@ -250,8 +272,24 @@ export class PostgresStore extends MastraStorage {
250
272
 
251
273
  // Get total count
252
274
  const countQuery = `SELECT COUNT(*) FROM ${this.getTableName(TABLE_TRACES)} ${whereClause}`;
253
- const countResult = await this.db.one(countQuery, queryParams);
254
- const total = parseInt(countResult.count, 10);
275
+ let total = 0;
276
+ try {
277
+ const countResult = await this.db.one(countQuery, queryParams);
278
+ total = parseInt(countResult.count, 10);
279
+ } catch (error) {
280
+ throw new MastraError(
281
+ {
282
+ id: 'MASTRA_STORAGE_PG_STORE_GET_TRACES_PAGINATED_FAILED_TO_RETRIEVE_TOTAL_COUNT',
283
+ domain: ErrorDomain.STORAGE,
284
+ category: ErrorCategory.THIRD_PARTY,
285
+ details: {
286
+ name: args.name ?? '',
287
+ scope: args.scope ?? '',
288
+ },
289
+ },
290
+ error,
291
+ );
292
+ }
255
293
 
256
294
  if (total === 0) {
257
295
  return {
@@ -268,31 +306,46 @@ export class PostgresStore extends MastraStorage {
268
306
  )} ${whereClause} ORDER BY "createdAt" DESC LIMIT $${paramIndex++} OFFSET $${paramIndex++}`;
269
307
  const finalQueryParams = [...queryParams, perPage, currentOffset];
270
308
 
271
- const rows = await this.db.manyOrNone<any>(dataQuery, finalQueryParams);
272
- const traces = rows.map(row => ({
273
- id: row.id,
274
- parentSpanId: row.parentSpanId,
275
- traceId: row.traceId,
276
- name: row.name,
277
- scope: row.scope,
278
- kind: row.kind,
279
- status: row.status,
280
- events: row.events,
281
- links: row.links,
282
- attributes: row.attributes,
283
- startTime: row.startTime,
284
- endTime: row.endTime,
285
- other: row.other,
286
- createdAt: row.createdAt,
287
- }));
309
+ try {
310
+ const rows = await this.db.manyOrNone<any>(dataQuery, finalQueryParams);
311
+ const traces = rows.map(row => ({
312
+ id: row.id,
313
+ parentSpanId: row.parentSpanId,
314
+ traceId: row.traceId,
315
+ name: row.name,
316
+ scope: row.scope,
317
+ kind: row.kind,
318
+ status: row.status,
319
+ events: row.events,
320
+ links: row.links,
321
+ attributes: row.attributes,
322
+ startTime: row.startTime,
323
+ endTime: row.endTime,
324
+ other: row.other,
325
+ createdAt: row.createdAt,
326
+ }));
288
327
 
289
- return {
290
- traces,
291
- total,
292
- page,
293
- perPage,
294
- hasMore: currentOffset + traces.length < total,
295
- };
328
+ return {
329
+ traces,
330
+ total,
331
+ page,
332
+ perPage,
333
+ hasMore: currentOffset + traces.length < total,
334
+ };
335
+ } catch (error) {
336
+ throw new MastraError(
337
+ {
338
+ id: 'MASTRA_STORAGE_PG_STORE_GET_TRACES_PAGINATED_FAILED_TO_RETRIEVE_TRACES',
339
+ domain: ErrorDomain.STORAGE,
340
+ category: ErrorCategory.THIRD_PARTY,
341
+ details: {
342
+ name: args.name ?? '',
343
+ scope: args.scope ?? '',
344
+ },
345
+ },
346
+ error,
347
+ );
348
+ }
296
349
  }
297
350
 
298
351
  private async setupSchema() {
@@ -390,8 +443,17 @@ export class PostgresStore extends MastraStorage {
390
443
 
391
444
  await this.db.none(sql);
392
445
  } catch (error) {
393
- console.error(`Error creating table ${tableName}:`, error);
394
- throw error;
446
+ throw new MastraError(
447
+ {
448
+ id: 'MASTRA_STORAGE_PG_STORE_CREATE_TABLE_FAILED',
449
+ domain: ErrorDomain.STORAGE,
450
+ category: ErrorCategory.THIRD_PARTY,
451
+ details: {
452
+ tableName,
453
+ },
454
+ },
455
+ error,
456
+ );
395
457
  }
396
458
  }
397
459
 
@@ -439,10 +501,17 @@ export class PostgresStore extends MastraStorage {
439
501
  }
440
502
  }
441
503
  } catch (error) {
442
- this.logger?.error?.(
443
- `Error altering table ${tableName}: ${error instanceof Error ? error.message : String(error)}`,
504
+ throw new MastraError(
505
+ {
506
+ id: 'MASTRA_STORAGE_PG_STORE_ALTER_TABLE_FAILED',
507
+ domain: ErrorDomain.STORAGE,
508
+ category: ErrorCategory.THIRD_PARTY,
509
+ details: {
510
+ tableName,
511
+ },
512
+ },
513
+ error,
444
514
  );
445
- throw new Error(`Failed to alter table ${tableName}: ${error}`);
446
515
  }
447
516
  }
448
517
 
@@ -450,8 +519,17 @@ export class PostgresStore extends MastraStorage {
450
519
  try {
451
520
  await this.db.none(`TRUNCATE TABLE ${this.getTableName(tableName)} CASCADE`);
452
521
  } catch (error) {
453
- console.error(`Error clearing table ${tableName}:`, error);
454
- throw error;
522
+ throw new MastraError(
523
+ {
524
+ id: 'MASTRA_STORAGE_PG_STORE_CLEAR_TABLE_FAILED',
525
+ domain: ErrorDomain.STORAGE,
526
+ category: ErrorCategory.THIRD_PARTY,
527
+ details: {
528
+ tableName,
529
+ },
530
+ },
531
+ error,
532
+ );
455
533
  }
456
534
  }
457
535
 
@@ -466,8 +544,17 @@ export class PostgresStore extends MastraStorage {
466
544
  values,
467
545
  );
468
546
  } catch (error) {
469
- console.error(`Error inserting into ${tableName}:`, error);
470
- throw error;
547
+ throw new MastraError(
548
+ {
549
+ id: 'MASTRA_STORAGE_PG_STORE_INSERT_FAILED',
550
+ domain: ErrorDomain.STORAGE,
551
+ category: ErrorCategory.THIRD_PARTY,
552
+ details: {
553
+ tableName,
554
+ },
555
+ },
556
+ error,
557
+ );
471
558
  }
472
559
  }
473
560
 
@@ -497,8 +584,17 @@ export class PostgresStore extends MastraStorage {
497
584
 
498
585
  return result;
499
586
  } catch (error) {
500
- console.error(`Error loading from ${tableName}:`, error);
501
- throw error;
587
+ throw new MastraError(
588
+ {
589
+ id: 'MASTRA_STORAGE_PG_STORE_LOAD_FAILED',
590
+ domain: ErrorDomain.STORAGE,
591
+ category: ErrorCategory.THIRD_PARTY,
592
+ details: {
593
+ tableName,
594
+ },
595
+ },
596
+ error,
597
+ );
502
598
  }
503
599
  }
504
600
 
@@ -528,8 +624,17 @@ export class PostgresStore extends MastraStorage {
528
624
  updatedAt: thread.updatedAt,
529
625
  };
530
626
  } catch (error) {
531
- console.error(`Error getting thread ${threadId}:`, error);
532
- throw error;
627
+ throw new MastraError(
628
+ {
629
+ id: 'MASTRA_STORAGE_PG_STORE_GET_THREAD_BY_ID_FAILED',
630
+ domain: ErrorDomain.STORAGE,
631
+ category: ErrorCategory.THIRD_PARTY,
632
+ details: {
633
+ threadId,
634
+ },
635
+ },
636
+ error,
637
+ );
533
638
  }
534
639
  }
535
640
 
@@ -601,7 +706,20 @@ export class PostgresStore extends MastraStorage {
601
706
  hasMore: currentOffset + threads.length < total,
602
707
  };
603
708
  } catch (error) {
604
- this.logger.error(`Error getting threads for resource ${resourceId}:`, error);
709
+ const mastraError = new MastraError(
710
+ {
711
+ id: 'MASTRA_STORAGE_PG_STORE_GET_THREADS_BY_RESOURCE_ID_PAGINATED_FAILED',
712
+ domain: ErrorDomain.STORAGE,
713
+ category: ErrorCategory.THIRD_PARTY,
714
+ details: {
715
+ resourceId,
716
+ page,
717
+ },
718
+ },
719
+ error,
720
+ );
721
+ this.logger?.error?.(mastraError.toString());
722
+ this.logger?.trackException(mastraError);
605
723
  return { threads: [], total: 0, page, perPage: perPageInput || 100, hasMore: false };
606
724
  }
607
725
  }
@@ -635,8 +753,17 @@ export class PostgresStore extends MastraStorage {
635
753
 
636
754
  return thread;
637
755
  } catch (error) {
638
- console.error('Error saving thread:', error);
639
- throw error;
756
+ throw new MastraError(
757
+ {
758
+ id: 'MASTRA_STORAGE_PG_STORE_SAVE_THREAD_FAILED',
759
+ domain: ErrorDomain.STORAGE,
760
+ category: ErrorCategory.THIRD_PARTY,
761
+ details: {
762
+ threadId: thread.id,
763
+ },
764
+ },
765
+ error,
766
+ );
640
767
  }
641
768
  }
642
769
 
@@ -649,24 +776,33 @@ export class PostgresStore extends MastraStorage {
649
776
  title: string;
650
777
  metadata: Record<string, unknown>;
651
778
  }): Promise<StorageThreadType> {
652
- try {
653
- // First get the existing thread to merge metadata
654
- const existingThread = await this.getThreadById({ threadId: id });
655
- if (!existingThread) {
656
- throw new Error(`Thread ${id} not found`);
657
- }
779
+ // First get the existing thread to merge metadata
780
+ const existingThread = await this.getThreadById({ threadId: id });
781
+ if (!existingThread) {
782
+ throw new MastraError({
783
+ id: 'MASTRA_STORAGE_PG_STORE_UPDATE_THREAD_FAILED',
784
+ domain: ErrorDomain.STORAGE,
785
+ category: ErrorCategory.USER,
786
+ text: `Thread ${id} not found`,
787
+ details: {
788
+ threadId: id,
789
+ title,
790
+ },
791
+ });
792
+ }
658
793
 
659
- // Merge the existing metadata with the new metadata
660
- const mergedMetadata = {
661
- ...existingThread.metadata,
662
- ...metadata,
663
- };
794
+ // Merge the existing metadata with the new metadata
795
+ const mergedMetadata = {
796
+ ...existingThread.metadata,
797
+ ...metadata,
798
+ };
664
799
 
800
+ try {
665
801
  const thread = await this.db.one<StorageThreadType>(
666
802
  `UPDATE ${this.getTableName(TABLE_THREADS)}
667
803
  SET title = $1,
668
- metadata = $2,
669
- "updatedAt" = $3
804
+ metadata = $2,
805
+ "updatedAt" = $3
670
806
  WHERE id = $4
671
807
  RETURNING *`,
672
808
  [title, mergedMetadata, new Date().toISOString(), id],
@@ -679,8 +815,18 @@ export class PostgresStore extends MastraStorage {
679
815
  updatedAt: thread.updatedAt,
680
816
  };
681
817
  } catch (error) {
682
- console.error('Error updating thread:', error);
683
- throw error;
818
+ throw new MastraError(
819
+ {
820
+ id: 'MASTRA_STORAGE_PG_STORE_UPDATE_THREAD_FAILED',
821
+ domain: ErrorDomain.STORAGE,
822
+ category: ErrorCategory.THIRD_PARTY,
823
+ details: {
824
+ threadId: id,
825
+ title,
826
+ },
827
+ },
828
+ error,
829
+ );
684
830
  }
685
831
  }
686
832
 
@@ -694,41 +840,42 @@ export class PostgresStore extends MastraStorage {
694
840
  await t.none(`DELETE FROM ${this.getTableName(TABLE_THREADS)} WHERE id = $1`, [threadId]);
695
841
  });
696
842
  } catch (error) {
697
- console.error('Error deleting thread:', error);
698
- throw error;
843
+ throw new MastraError(
844
+ {
845
+ id: 'MASTRA_STORAGE_PG_STORE_DELETE_THREAD_FAILED',
846
+ domain: ErrorDomain.STORAGE,
847
+ category: ErrorCategory.THIRD_PARTY,
848
+ details: {
849
+ threadId,
850
+ },
851
+ },
852
+ error,
853
+ );
699
854
  }
700
855
  }
701
856
 
702
- /**
703
- * @deprecated use getMessagesPaginated instead
704
- */
705
- public async getMessages(args: StorageGetMessagesArg & { format?: 'v1' }): Promise<MastraMessageV1[]>;
706
- public async getMessages(args: StorageGetMessagesArg & { format: 'v2' }): Promise<MastraMessageV2[]>;
707
- public async getMessages(
708
- args: StorageGetMessagesArg & {
709
- format?: 'v1' | 'v2';
710
- },
711
- ): Promise<MastraMessageV1[] | MastraMessageV2[]> {
712
- const { threadId, format, selectBy } = args;
713
-
714
- const selectStatement = `SELECT id, content, role, type, "createdAt", thread_id AS "threadId"`;
715
- const orderByStatement = `ORDER BY "createdAt" DESC`;
716
-
717
- try {
718
- let rows: any[] = [];
719
- const include = selectBy?.include || [];
720
-
721
- if (include.length) {
722
- const unionQueries: string[] = [];
723
- const params: any[] = [];
724
- let paramIdx = 1;
725
-
726
- for (const inc of include) {
727
- const { id, withPreviousMessages = 0, withNextMessages = 0 } = inc;
728
- // if threadId is provided, use it, otherwise use threadId from args
729
- const searchId = inc.threadId || threadId;
730
- unionQueries.push(
731
- `
857
+ private async _getIncludedMessages({
858
+ threadId,
859
+ selectBy,
860
+ orderByStatement,
861
+ }: {
862
+ threadId: string;
863
+ selectBy: StorageGetMessagesArg['selectBy'];
864
+ orderByStatement: string;
865
+ }) {
866
+ const include = selectBy?.include;
867
+ if (!include) return null;
868
+
869
+ const unionQueries: string[] = [];
870
+ const params: any[] = [];
871
+ let paramIdx = 1;
872
+
873
+ for (const inc of include) {
874
+ const { id, withPreviousMessages = 0, withNextMessages = 0 } = inc;
875
+ // if threadId is provided, use it, otherwise use threadId from args
876
+ const searchId = inc.threadId || threadId;
877
+ unionQueries.push(
878
+ `
732
879
  SELECT * FROM (
733
880
  WITH ordered_messages AS (
734
881
  SELECT
@@ -760,37 +907,59 @@ export class PostgresStore extends MastraStorage {
760
907
  )
761
908
  ) AS query_${paramIdx}
762
909
  `, // Keep ASC for final sorting after fetching context
763
- );
764
- params.push(searchId, id, withPreviousMessages, withNextMessages);
765
- paramIdx += 4;
766
- }
767
- const finalQuery = unionQueries.join(' UNION ALL ') + ' ORDER BY "createdAt" ASC';
768
- const includedRows = await this.db.manyOrNone(finalQuery, params);
769
- const seen = new Set<string>();
770
- const dedupedRows = includedRows.filter(row => {
771
- if (seen.has(row.id)) return false;
772
- seen.add(row.id);
773
- return true;
774
- });
775
- rows = dedupedRows;
776
- } else {
777
- const limit = typeof selectBy?.last === `number` ? selectBy.last : 40;
778
- if (limit === 0 && selectBy?.last !== false) {
779
- // if last is explicitly false, we fetch all
780
- // Do nothing, rows will be empty, and we return empty array later.
781
- } else {
782
- let query = `${selectStatement} FROM ${this.getTableName(
783
- TABLE_MESSAGES,
784
- )} WHERE thread_id = $1 ${orderByStatement}`;
785
- const queryParams: any[] = [threadId];
786
- if (limit !== undefined && selectBy?.last !== false) {
787
- query += ` LIMIT $2`;
788
- queryParams.push(limit);
789
- }
790
- rows = await this.db.manyOrNone(query, queryParams);
910
+ );
911
+ params.push(searchId, id, withPreviousMessages, withNextMessages);
912
+ paramIdx += 4;
913
+ }
914
+ const finalQuery = unionQueries.join(' UNION ALL ') + ' ORDER BY "createdAt" ASC';
915
+ const includedRows = await this.db.manyOrNone(finalQuery, params);
916
+ const seen = new Set<string>();
917
+ const dedupedRows = includedRows.filter(row => {
918
+ if (seen.has(row.id)) return false;
919
+ seen.add(row.id);
920
+ return true;
921
+ });
922
+ return dedupedRows;
923
+ }
924
+
925
+ /**
926
+ * @deprecated use getMessagesPaginated instead
927
+ */
928
+ public async getMessages(args: StorageGetMessagesArg & { format?: 'v1' }): Promise<MastraMessageV1[]>;
929
+ public async getMessages(args: StorageGetMessagesArg & { format: 'v2' }): Promise<MastraMessageV2[]>;
930
+ public async getMessages(
931
+ args: StorageGetMessagesArg & {
932
+ format?: 'v1' | 'v2';
933
+ },
934
+ ): Promise<MastraMessageV1[] | MastraMessageV2[]> {
935
+ const { threadId, format, selectBy } = args;
936
+
937
+ const selectStatement = `SELECT id, content, role, type, "createdAt", thread_id AS "threadId"`;
938
+ const orderByStatement = `ORDER BY "createdAt" DESC`;
939
+ const limit = this.resolveMessageLimit({ last: selectBy?.last, defaultLimit: 40 });
940
+
941
+ try {
942
+ let rows: any[] = [];
943
+ const include = selectBy?.include || [];
944
+
945
+ if (include?.length) {
946
+ const includeMessages = await this._getIncludedMessages({ threadId, selectBy, orderByStatement });
947
+ if (includeMessages) {
948
+ rows.push(...includeMessages);
791
949
  }
792
950
  }
793
951
 
952
+ const excludeIds = rows.map(m => m.id);
953
+ const excludeIdsParam = excludeIds.map((_, idx) => `$${idx + 2}`).join(', ');
954
+ let query = `${selectStatement} FROM ${this.getTableName(TABLE_MESSAGES)} WHERE thread_id = $1
955
+ ${excludeIds.length ? `AND id NOT IN (${excludeIdsParam})` : ''}
956
+ ${orderByStatement}
957
+ LIMIT $${excludeIds.length + 2}
958
+ `;
959
+ const queryParams: any[] = [threadId, ...excludeIds, limit];
960
+ const remainingRows = await this.db.manyOrNone(query, queryParams);
961
+ rows.push(...remainingRows);
962
+
794
963
  const fetchedMessages = (rows || []).map(message => {
795
964
  if (typeof message.content === 'string') {
796
965
  try {
@@ -815,7 +984,19 @@ export class PostgresStore extends MastraStorage {
815
984
  )
816
985
  : sortedMessages;
817
986
  } catch (error) {
818
- this.logger.error('Error getting messages:', error);
987
+ const mastraError = new MastraError(
988
+ {
989
+ id: 'MASTRA_STORAGE_PG_STORE_GET_MESSAGES_FAILED',
990
+ domain: ErrorDomain.STORAGE,
991
+ category: ErrorCategory.THIRD_PARTY,
992
+ details: {
993
+ threadId,
994
+ },
995
+ },
996
+ error,
997
+ );
998
+ this.logger?.error?.(mastraError.toString());
999
+ this.logger?.trackException(mastraError);
819
1000
  return [];
820
1001
  }
821
1002
  }
@@ -833,6 +1014,15 @@ export class PostgresStore extends MastraStorage {
833
1014
  const selectStatement = `SELECT id, content, role, type, "createdAt", thread_id AS "threadId"`;
834
1015
  const orderByStatement = `ORDER BY "createdAt" DESC`;
835
1016
 
1017
+ const messages: MastraMessageV2[] = [];
1018
+
1019
+ if (selectBy?.include?.length) {
1020
+ const includeMessages = await this._getIncludedMessages({ threadId, selectBy, orderByStatement });
1021
+ if (includeMessages) {
1022
+ messages.push(...includeMessages);
1023
+ }
1024
+ }
1025
+
836
1026
  try {
837
1027
  const perPage = perPageInput !== undefined ? perPageInput : 40;
838
1028
  const currentOffset = page * perPage;
@@ -869,8 +1059,9 @@ export class PostgresStore extends MastraStorage {
869
1059
  TABLE_MESSAGES,
870
1060
  )} ${whereClause} ${orderByStatement} LIMIT $${paramIndex++} OFFSET $${paramIndex++}`;
871
1061
  const rows = await this.db.manyOrNone(dataQuery, [...queryParams, perPage, currentOffset]);
1062
+ messages.push(...(rows || []));
872
1063
 
873
- const list = new MessageList().add(rows || [], 'memory');
1064
+ const list = new MessageList().add(messages, 'memory');
874
1065
  const messagesToReturn = format === `v2` ? list.get.all.v2() : list.get.all.v1();
875
1066
 
876
1067
  return {
@@ -881,7 +1072,20 @@ export class PostgresStore extends MastraStorage {
881
1072
  hasMore: currentOffset + rows.length < total,
882
1073
  };
883
1074
  } catch (error) {
884
- this.logger.error('Error getting messages:', error);
1075
+ const mastraError = new MastraError(
1076
+ {
1077
+ id: 'MASTRA_STORAGE_PG_STORE_GET_MESSAGES_PAGINATED_FAILED',
1078
+ domain: ErrorDomain.STORAGE,
1079
+ category: ErrorCategory.THIRD_PARTY,
1080
+ details: {
1081
+ threadId,
1082
+ page,
1083
+ },
1084
+ },
1085
+ error,
1086
+ );
1087
+ this.logger?.error?.(mastraError.toString());
1088
+ this.logger?.trackException(mastraError);
885
1089
  return { messages: [], total: 0, page, perPage: perPageInput || 40, hasMore: false };
886
1090
  }
887
1091
  }
@@ -896,18 +1100,31 @@ export class PostgresStore extends MastraStorage {
896
1100
  | { messages: MastraMessageV2[]; format: 'v2' }): Promise<MastraMessageV2[] | MastraMessageV1[]> {
897
1101
  if (messages.length === 0) return messages;
898
1102
 
899
- try {
900
- const threadId = messages[0]?.threadId;
901
- if (!threadId) {
902
- throw new Error('Thread ID is required');
903
- }
1103
+ const threadId = messages[0]?.threadId;
1104
+ if (!threadId) {
1105
+ throw new MastraError({
1106
+ id: 'MASTRA_STORAGE_PG_STORE_SAVE_MESSAGES_FAILED',
1107
+ domain: ErrorDomain.STORAGE,
1108
+ category: ErrorCategory.THIRD_PARTY,
1109
+ text: `Thread ID is required`,
1110
+ });
1111
+ }
904
1112
 
905
- // Check if thread exists
906
- const thread = await this.getThreadById({ threadId });
907
- if (!thread) {
908
- throw new Error(`Thread ${threadId} not found`);
909
- }
1113
+ // Check if thread exists
1114
+ const thread = await this.getThreadById({ threadId });
1115
+ if (!thread) {
1116
+ throw new MastraError({
1117
+ id: 'MASTRA_STORAGE_PG_STORE_SAVE_MESSAGES_FAILED',
1118
+ domain: ErrorDomain.STORAGE,
1119
+ category: ErrorCategory.THIRD_PARTY,
1120
+ text: `Thread ${threadId} not found`,
1121
+ details: {
1122
+ threadId,
1123
+ },
1124
+ });
1125
+ }
910
1126
 
1127
+ try {
911
1128
  await this.db.tx(async t => {
912
1129
  // Execute message inserts and thread update in parallel for better performance
913
1130
  const messageInserts = messages.map(message => {
@@ -950,8 +1167,17 @@ export class PostgresStore extends MastraStorage {
950
1167
  if (format === `v2`) return list.get.all.v2();
951
1168
  return list.get.all.v1();
952
1169
  } catch (error) {
953
- console.error('Error saving messages:', error);
954
- throw error;
1170
+ throw new MastraError(
1171
+ {
1172
+ id: 'MASTRA_STORAGE_PG_STORE_SAVE_MESSAGES_FAILED',
1173
+ domain: ErrorDomain.STORAGE,
1174
+ category: ErrorCategory.THIRD_PARTY,
1175
+ details: {
1176
+ threadId,
1177
+ },
1178
+ },
1179
+ error,
1180
+ );
955
1181
  }
956
1182
  }
957
1183
 
@@ -980,8 +1206,18 @@ export class PostgresStore extends MastraStorage {
980
1206
  [workflowName, runId, JSON.stringify(snapshot), now, now],
981
1207
  );
982
1208
  } catch (error) {
983
- console.error('Error persisting workflow snapshot:', error);
984
- throw error;
1209
+ throw new MastraError(
1210
+ {
1211
+ id: 'MASTRA_STORAGE_PG_STORE_PERSIST_WORKFLOW_SNAPSHOT_FAILED',
1212
+ domain: ErrorDomain.STORAGE,
1213
+ category: ErrorCategory.THIRD_PARTY,
1214
+ details: {
1215
+ workflowName,
1216
+ runId,
1217
+ },
1218
+ },
1219
+ error,
1220
+ );
985
1221
  }
986
1222
  }
987
1223
 
@@ -1007,8 +1243,18 @@ export class PostgresStore extends MastraStorage {
1007
1243
 
1008
1244
  return (result as any).snapshot;
1009
1245
  } catch (error) {
1010
- console.error('Error loading workflow snapshot:', error);
1011
- throw error;
1246
+ throw new MastraError(
1247
+ {
1248
+ id: 'MASTRA_STORAGE_PG_STORE_LOAD_WORKFLOW_SNAPSHOT_FAILED',
1249
+ domain: ErrorDomain.STORAGE,
1250
+ category: ErrorCategory.THIRD_PARTY,
1251
+ details: {
1252
+ workflowName,
1253
+ runId,
1254
+ },
1255
+ },
1256
+ error,
1257
+ );
1012
1258
  }
1013
1259
  }
1014
1260
 
@@ -1122,8 +1368,17 @@ export class PostgresStore extends MastraStorage {
1122
1368
  // Use runs.length as total when not paginating
1123
1369
  return { runs, total: total || runs.length };
1124
1370
  } catch (error) {
1125
- console.error('Error getting workflow runs:', error);
1126
- throw error;
1371
+ throw new MastraError(
1372
+ {
1373
+ id: 'MASTRA_STORAGE_PG_STORE_GET_WORKFLOW_RUNS_FAILED',
1374
+ domain: ErrorDomain.STORAGE,
1375
+ category: ErrorCategory.THIRD_PARTY,
1376
+ details: {
1377
+ workflowName: workflowName || 'all',
1378
+ },
1379
+ },
1380
+ error,
1381
+ );
1127
1382
  }
1128
1383
  }
1129
1384
 
@@ -1169,8 +1424,18 @@ export class PostgresStore extends MastraStorage {
1169
1424
 
1170
1425
  return this.parseWorkflowRun(result);
1171
1426
  } catch (error) {
1172
- console.error('Error getting workflow run by ID:', error);
1173
- throw error;
1427
+ throw new MastraError(
1428
+ {
1429
+ id: 'MASTRA_STORAGE_PG_STORE_GET_WORKFLOW_RUN_BY_ID_FAILED',
1430
+ domain: ErrorDomain.STORAGE,
1431
+ category: ErrorCategory.THIRD_PARTY,
1432
+ details: {
1433
+ runId,
1434
+ workflowName: workflowName || '',
1435
+ },
1436
+ },
1437
+ error,
1438
+ );
1174
1439
  }
1175
1440
  }
1176
1441
 
@@ -1216,31 +1481,181 @@ export class PostgresStore extends MastraStorage {
1216
1481
  const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
1217
1482
 
1218
1483
  const countQuery = `SELECT COUNT(*) FROM ${this.getTableName(TABLE_EVALS)} ${whereClause}`;
1219
- const countResult = await this.db.one(countQuery, queryParams);
1220
- const total = parseInt(countResult.count, 10);
1221
- const currentOffset = page * perPage;
1484
+ try {
1485
+ const countResult = await this.db.one(countQuery, queryParams);
1486
+ const total = parseInt(countResult.count, 10);
1487
+ const currentOffset = page * perPage;
1488
+
1489
+ if (total === 0) {
1490
+ return {
1491
+ evals: [],
1492
+ total: 0,
1493
+ page,
1494
+ perPage,
1495
+ hasMore: false,
1496
+ };
1497
+ }
1498
+
1499
+ const dataQuery = `SELECT * FROM ${this.getTableName(
1500
+ TABLE_EVALS,
1501
+ )} ${whereClause} ORDER BY created_at DESC LIMIT $${paramIndex++} OFFSET $${paramIndex++}`;
1502
+ const rows = await this.db.manyOrNone(dataQuery, [...queryParams, perPage, currentOffset]);
1222
1503
 
1223
- if (total === 0) {
1224
1504
  return {
1225
- evals: [],
1226
- total: 0,
1505
+ evals: rows?.map(row => this.transformEvalRow(row)) ?? [],
1506
+ total,
1227
1507
  page,
1228
1508
  perPage,
1229
- hasMore: false,
1509
+ hasMore: currentOffset + (rows?.length ?? 0) < total,
1230
1510
  };
1511
+ } catch (error) {
1512
+ const mastraError = new MastraError(
1513
+ {
1514
+ id: 'MASTRA_STORAGE_PG_STORE_GET_EVALS_FAILED',
1515
+ domain: ErrorDomain.STORAGE,
1516
+ category: ErrorCategory.THIRD_PARTY,
1517
+ details: {
1518
+ agentName: agentName || 'all',
1519
+ type: type || 'all',
1520
+ page,
1521
+ perPage,
1522
+ },
1523
+ },
1524
+ error,
1525
+ );
1526
+ this.logger?.error?.(mastraError.toString());
1527
+ this.logger?.trackException(mastraError);
1528
+ throw mastraError;
1231
1529
  }
1530
+ }
1232
1531
 
1233
- const dataQuery = `SELECT * FROM ${this.getTableName(
1234
- TABLE_EVALS,
1235
- )} ${whereClause} ORDER BY created_at DESC LIMIT $${paramIndex++} OFFSET $${paramIndex++}`;
1236
- const rows = await this.db.manyOrNone(dataQuery, [...queryParams, perPage, currentOffset]);
1532
+ async updateMessages({
1533
+ messages,
1534
+ }: {
1535
+ messages: (Partial<Omit<MastraMessageV2, 'createdAt'>> & {
1536
+ id: string;
1537
+ content?: {
1538
+ metadata?: MastraMessageContentV2['metadata'];
1539
+ content?: MastraMessageContentV2['content'];
1540
+ };
1541
+ })[];
1542
+ }): Promise<MastraMessageV2[]> {
1543
+ if (messages.length === 0) {
1544
+ return [];
1545
+ }
1237
1546
 
1238
- return {
1239
- evals: rows?.map(row => this.transformEvalRow(row)) ?? [],
1240
- total,
1241
- page,
1242
- perPage,
1243
- hasMore: currentOffset + (rows?.length ?? 0) < total,
1244
- };
1547
+ const messageIds = messages.map(m => m.id);
1548
+
1549
+ const selectQuery = `SELECT id, content, role, type, "createdAt", thread_id AS "threadId", "resourceId" FROM ${this.getTableName(
1550
+ TABLE_MESSAGES,
1551
+ )} WHERE id IN ($1:list)`;
1552
+
1553
+ const existingMessagesDb = await this.db.manyOrNone(selectQuery, [messageIds]);
1554
+
1555
+ if (existingMessagesDb.length === 0) {
1556
+ return [];
1557
+ }
1558
+
1559
+ // Parse content from string to object for merging
1560
+ const existingMessages: MastraMessageV2[] = existingMessagesDb.map(msg => {
1561
+ if (typeof msg.content === 'string') {
1562
+ try {
1563
+ msg.content = JSON.parse(msg.content);
1564
+ } catch {
1565
+ // ignore if not valid json
1566
+ }
1567
+ }
1568
+ return msg as MastraMessageV2;
1569
+ });
1570
+
1571
+ const threadIdsToUpdate = new Set<string>();
1572
+
1573
+ await this.db.tx(async t => {
1574
+ const queries = [];
1575
+ const columnMapping: Record<string, string> = {
1576
+ threadId: 'thread_id',
1577
+ };
1578
+
1579
+ for (const existingMessage of existingMessages) {
1580
+ const updatePayload = messages.find(m => m.id === existingMessage.id);
1581
+ if (!updatePayload) continue;
1582
+
1583
+ const { id, ...fieldsToUpdate } = updatePayload;
1584
+ if (Object.keys(fieldsToUpdate).length === 0) continue;
1585
+
1586
+ threadIdsToUpdate.add(existingMessage.threadId!);
1587
+ if (updatePayload.threadId && updatePayload.threadId !== existingMessage.threadId) {
1588
+ threadIdsToUpdate.add(updatePayload.threadId);
1589
+ }
1590
+
1591
+ const setClauses: string[] = [];
1592
+ const values: any[] = [];
1593
+ let paramIndex = 1;
1594
+
1595
+ const updatableFields = { ...fieldsToUpdate };
1596
+
1597
+ // Special handling for content: merge in code, then update the whole field
1598
+ if (updatableFields.content) {
1599
+ const newContent = {
1600
+ ...existingMessage.content,
1601
+ ...updatableFields.content,
1602
+ // Deep merge metadata if it exists on both
1603
+ ...(existingMessage.content?.metadata && updatableFields.content.metadata
1604
+ ? {
1605
+ metadata: {
1606
+ ...existingMessage.content.metadata,
1607
+ ...updatableFields.content.metadata,
1608
+ },
1609
+ }
1610
+ : {}),
1611
+ };
1612
+ setClauses.push(`content = $${paramIndex++}`);
1613
+ values.push(newContent);
1614
+ delete updatableFields.content;
1615
+ }
1616
+
1617
+ for (const key in updatableFields) {
1618
+ if (Object.prototype.hasOwnProperty.call(updatableFields, key)) {
1619
+ const dbColumn = columnMapping[key] || key;
1620
+ setClauses.push(`"${dbColumn}" = $${paramIndex++}`);
1621
+ values.push(updatableFields[key as keyof typeof updatableFields]);
1622
+ }
1623
+ }
1624
+
1625
+ if (setClauses.length > 0) {
1626
+ values.push(id);
1627
+ const sql = `UPDATE ${this.getTableName(
1628
+ TABLE_MESSAGES,
1629
+ )} SET ${setClauses.join(', ')} WHERE id = $${paramIndex}`;
1630
+ queries.push(t.none(sql, values));
1631
+ }
1632
+ }
1633
+
1634
+ if (threadIdsToUpdate.size > 0) {
1635
+ queries.push(
1636
+ t.none(`UPDATE ${this.getTableName(TABLE_THREADS)} SET "updatedAt" = NOW() WHERE id IN ($1:list)`, [
1637
+ Array.from(threadIdsToUpdate),
1638
+ ]),
1639
+ );
1640
+ }
1641
+
1642
+ if (queries.length > 0) {
1643
+ await t.batch(queries);
1644
+ }
1645
+ });
1646
+
1647
+ // Re-fetch to return the fully updated messages
1648
+ const updatedMessages = await this.db.manyOrNone<MastraMessageV2>(selectQuery, [messageIds]);
1649
+
1650
+ return (updatedMessages || []).map(message => {
1651
+ if (typeof message.content === 'string') {
1652
+ try {
1653
+ message.content = JSON.parse(message.content);
1654
+ } catch {
1655
+ /* ignore */
1656
+ }
1657
+ }
1658
+ return message;
1659
+ });
1245
1660
  }
1246
1661
  }