@mastra/pg 0.12.3 → 0.12.4

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,32 +1,33 @@
1
- import { MessageList } from '@mastra/core/agent';
2
1
  import type { MastraMessageContentV2, MastraMessageV2 } from '@mastra/core/agent';
3
2
  import { ErrorCategory, ErrorDomain, MastraError } from '@mastra/core/error';
4
- import type { MetricResult } from '@mastra/core/eval';
5
3
  import type { MastraMessageV1, StorageThreadType } from '@mastra/core/memory';
6
- import {
7
- MastraStorage,
8
- TABLE_MESSAGES,
9
- TABLE_THREADS,
10
- TABLE_TRACES,
11
- TABLE_RESOURCES,
12
- TABLE_WORKFLOW_SNAPSHOT,
13
- TABLE_EVALS,
14
- } from '@mastra/core/storage';
4
+ import type { ScoreRowData } from '@mastra/core/scores';
5
+ import { MastraStorage } from '@mastra/core/storage';
15
6
  import type {
16
7
  EvalRow,
17
8
  PaginationInfo,
18
9
  StorageColumn,
19
10
  StorageGetMessagesArg,
11
+ StorageGetTracesArg,
12
+ StorageGetTracesPaginatedArg,
20
13
  StorageResourceType,
21
14
  TABLE_NAMES,
22
15
  WorkflowRun,
23
16
  WorkflowRuns,
24
17
  PaginationArgs,
18
+ StoragePagination,
19
+ StorageDomains,
25
20
  } from '@mastra/core/storage';
26
- import { parseSqlIdentifier, parseFieldKey } from '@mastra/core/utils';
21
+ import type { Trace } from '@mastra/core/telemetry';
27
22
  import type { WorkflowRunState } from '@mastra/core/workflows';
28
23
  import pgPromise from 'pg-promise';
29
24
  import type { ISSLConfig } from 'pg-promise/typescript/pg-subset';
25
+ import { LegacyEvalsPG } from './domains/legacy-evals';
26
+ import { MemoryPG } from './domains/memory';
27
+ import { StoreOperationsPG } from './domains/operations';
28
+ import { ScoresPG } from './domains/scores';
29
+ import { TracesPG } from './domains/traces';
30
+ import { WorkflowsPG } from './domains/workflows';
30
31
 
31
32
  export type PostgresConfig = {
32
33
  schemaName?: string;
@@ -47,9 +48,10 @@ export type PostgresConfig = {
47
48
  export class PostgresStore extends MastraStorage {
48
49
  public db: pgPromise.IDatabase<{}>;
49
50
  public pgp: pgPromise.IMain;
51
+ private client: pgPromise.IDatabase<{}>;
50
52
  private schema?: string;
51
- private setupSchemaPromise: Promise<void> | null = null;
52
- private schemaSetupComplete: boolean | undefined = undefined;
53
+
54
+ stores: StorageDomains;
53
55
 
54
56
  constructor(config: PostgresConfig) {
55
57
  // Validation: connectionString or host/database/user/password must not be empty
@@ -76,7 +78,7 @@ export class PostgresStore extends MastraStorage {
76
78
  }
77
79
  super({ name: 'PostgresStore' });
78
80
  this.pgp = pgPromise();
79
- this.schema = config.schemaName;
81
+ this.schema = config.schemaName || 'public';
80
82
  this.db = this.pgp(
81
83
  `connectionString` in config
82
84
  ? { connectionString: config.connectionString }
@@ -89,6 +91,24 @@ export class PostgresStore extends MastraStorage {
89
91
  ssl: config.ssl,
90
92
  },
91
93
  );
94
+
95
+ this.client = this.db;
96
+
97
+ const operations = new StoreOperationsPG({ client: this.client, schemaName: this.schema });
98
+ const scores = new ScoresPG({ client: this.client, operations });
99
+ const traces = new TracesPG({ client: this.client, operations, schema: this.schema });
100
+ const workflows = new WorkflowsPG({ client: this.client, operations, schema: this.schema });
101
+ const legacyEvals = new LegacyEvalsPG({ client: this.client, schema: this.schema });
102
+ const memory = new MemoryPG({ client: this.client, schema: this.schema, operations });
103
+
104
+ this.stores = {
105
+ operations,
106
+ scores,
107
+ traces,
108
+ workflows,
109
+ legacyEvals,
110
+ memory,
111
+ };
92
112
  } catch (e) {
93
113
  throw new MastraError(
94
114
  {
@@ -101,304 +121,42 @@ export class PostgresStore extends MastraStorage {
101
121
  }
102
122
  }
103
123
 
104
- public get supports(): {
105
- selectByIncludeResourceScope: boolean;
106
- resourceWorkingMemory: boolean;
107
- } {
124
+ public get supports() {
108
125
  return {
109
126
  selectByIncludeResourceScope: true,
110
127
  resourceWorkingMemory: true,
128
+ hasColumn: true,
129
+ createTable: true,
111
130
  };
112
131
  }
113
132
 
114
- private getTableName(indexName: string) {
115
- const parsedIndexName = parseSqlIdentifier(indexName, 'index name');
116
- const quotedIndexName = `"${parsedIndexName}"`;
117
- const quotedSchemaName = this.getSchemaName();
118
- return quotedSchemaName ? `${quotedSchemaName}.${quotedIndexName}` : quotedIndexName;
119
- }
120
-
121
- private getSchemaName() {
122
- return this.schema ? `"${parseSqlIdentifier(this.schema, 'schema name')}"` : undefined;
123
- }
124
-
125
133
  /** @deprecated use getEvals instead */
126
134
  async getEvalsByAgentName(agentName: string, type?: 'test' | 'live'): Promise<EvalRow[]> {
127
- try {
128
- const baseQuery = `SELECT * FROM ${this.getTableName(TABLE_EVALS)} WHERE agent_name = $1`;
129
- const typeCondition =
130
- type === 'test'
131
- ? " AND test_info IS NOT NULL AND test_info->>'testPath' IS NOT NULL"
132
- : type === 'live'
133
- ? " AND (test_info IS NULL OR test_info->>'testPath' IS NULL)"
134
- : '';
135
-
136
- const query = `${baseQuery}${typeCondition} ORDER BY created_at DESC`;
137
-
138
- const rows = await this.db.manyOrNone(query, [agentName]);
139
- return rows?.map(row => this.transformEvalRow(row)) ?? [];
140
- } catch (error) {
141
- // Handle case where table doesn't exist yet
142
- if (error instanceof Error && error.message.includes('relation') && error.message.includes('does not exist')) {
143
- return [];
144
- }
145
- console.error('Failed to get evals for the specified agent: ' + (error as any)?.message);
146
- throw error;
147
- }
135
+ return this.stores.legacyEvals.getEvalsByAgentName(agentName, type);
148
136
  }
149
137
 
150
- private transformEvalRow(row: Record<string, any>): EvalRow {
151
- let testInfoValue = null;
152
- if (row.test_info) {
153
- try {
154
- testInfoValue = typeof row.test_info === 'string' ? JSON.parse(row.test_info) : row.test_info;
155
- } catch (e) {
156
- console.warn('Failed to parse test_info:', e);
157
- }
158
- }
159
-
160
- return {
161
- agentName: row.agent_name as string,
162
- input: row.input as string,
163
- output: row.output as string,
164
- result: row.result as MetricResult,
165
- metricName: row.metric_name as string,
166
- instructions: row.instructions as string,
167
- testInfo: testInfoValue,
168
- globalRunId: row.global_run_id as string,
169
- runId: row.run_id as string,
170
- createdAt: row.created_at as string,
171
- };
172
- }
173
-
174
- async batchInsert({ tableName, records }: { tableName: TABLE_NAMES; records: Record<string, any>[] }): Promise<void> {
175
- try {
176
- await this.db.query('BEGIN');
177
- for (const record of records) {
178
- await this.insert({ tableName, record });
179
- }
180
- await this.db.query('COMMIT');
181
- } catch (error) {
182
- await this.db.query('ROLLBACK');
183
- throw new MastraError(
184
- {
185
- id: 'MASTRA_STORAGE_PG_STORE_BATCH_INSERT_FAILED',
186
- domain: ErrorDomain.STORAGE,
187
- category: ErrorCategory.THIRD_PARTY,
188
- details: {
189
- tableName,
190
- numberOfRecords: records.length,
191
- },
192
- },
193
- error,
194
- );
195
- }
138
+ async getEvals(
139
+ options: {
140
+ agentName?: string;
141
+ type?: 'test' | 'live';
142
+ } & PaginationArgs = {},
143
+ ): Promise<PaginationInfo & { evals: EvalRow[] }> {
144
+ return this.stores.legacyEvals.getEvals(options);
196
145
  }
197
146
 
198
147
  /**
199
148
  * @deprecated use getTracesPaginated instead
200
149
  */
201
- public async getTraces(args: {
202
- name?: string;
203
- scope?: string;
204
- attributes?: Record<string, string>;
205
- filters?: Record<string, any>;
206
- page: number;
207
- perPage?: number;
208
- fromDate?: Date;
209
- toDate?: Date;
210
- }): Promise<any[]> {
211
- if (args.fromDate || args.toDate) {
212
- (args as any).dateRange = {
213
- start: args.fromDate,
214
- end: args.toDate,
215
- };
216
- }
217
- const result = await this.getTracesPaginated(args);
218
- return result.traces;
150
+ public async getTraces(args: StorageGetTracesArg): Promise<Trace[]> {
151
+ return this.stores.traces.getTraces(args);
219
152
  }
220
153
 
221
- public async getTracesPaginated(
222
- args: {
223
- name?: string;
224
- scope?: string;
225
- attributes?: Record<string, string>;
226
- filters?: Record<string, any>;
227
- } & PaginationArgs,
228
- ): Promise<
229
- PaginationInfo & {
230
- traces: any[];
231
- }
232
- > {
233
- const { name, scope, page = 0, perPage: perPageInput, attributes, filters, dateRange } = args;
234
- const fromDate = dateRange?.start;
235
- const toDate = dateRange?.end;
236
-
237
- const perPage = perPageInput !== undefined ? perPageInput : 100; // Default perPage
238
- const currentOffset = page * perPage;
239
-
240
- const queryParams: any[] = [];
241
- const conditions: string[] = [];
242
- let paramIndex = 1;
243
-
244
- if (name) {
245
- conditions.push(`name LIKE $${paramIndex++}`);
246
- queryParams.push(`${name}%`); // Add wildcard for LIKE
247
- }
248
- if (scope) {
249
- conditions.push(`scope = $${paramIndex++}`);
250
- queryParams.push(scope);
251
- }
252
- if (attributes) {
253
- Object.entries(attributes).forEach(([key, value]) => {
254
- const parsedKey = parseFieldKey(key);
255
- conditions.push(`attributes->>'${parsedKey}' = $${paramIndex++}`);
256
- queryParams.push(value);
257
- });
258
- }
259
- if (filters) {
260
- Object.entries(filters).forEach(([key, value]) => {
261
- const parsedKey = parseFieldKey(key);
262
- conditions.push(`"${parsedKey}" = $${paramIndex++}`); // Ensure filter keys are quoted if they are column names
263
- queryParams.push(value);
264
- });
265
- }
266
- if (fromDate) {
267
- conditions.push(`"createdAt" >= $${paramIndex++}`);
268
- queryParams.push(fromDate);
269
- }
270
- if (toDate) {
271
- conditions.push(`"createdAt" <= $${paramIndex++}`);
272
- queryParams.push(toDate);
273
- }
274
-
275
- const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
276
-
277
- // Get total count
278
- const countQuery = `SELECT COUNT(*) FROM ${this.getTableName(TABLE_TRACES)} ${whereClause}`;
279
- let total = 0;
280
- try {
281
- const countResult = await this.db.one(countQuery, queryParams);
282
- total = parseInt(countResult.count, 10);
283
- } catch (error) {
284
- throw new MastraError(
285
- {
286
- id: 'MASTRA_STORAGE_PG_STORE_GET_TRACES_PAGINATED_FAILED_TO_RETRIEVE_TOTAL_COUNT',
287
- domain: ErrorDomain.STORAGE,
288
- category: ErrorCategory.THIRD_PARTY,
289
- details: {
290
- name: args.name ?? '',
291
- scope: args.scope ?? '',
292
- },
293
- },
294
- error,
295
- );
296
- }
297
-
298
- if (total === 0) {
299
- return {
300
- traces: [],
301
- total: 0,
302
- page,
303
- perPage,
304
- hasMore: false,
305
- };
306
- }
307
-
308
- const dataQuery = `SELECT * FROM ${this.getTableName(
309
- TABLE_TRACES,
310
- )} ${whereClause} ORDER BY "createdAt" DESC LIMIT $${paramIndex++} OFFSET $${paramIndex++}`;
311
- const finalQueryParams = [...queryParams, perPage, currentOffset];
312
-
313
- try {
314
- const rows = await this.db.manyOrNone<any>(dataQuery, finalQueryParams);
315
- const traces = rows.map(row => ({
316
- id: row.id,
317
- parentSpanId: row.parentSpanId,
318
- traceId: row.traceId,
319
- name: row.name,
320
- scope: row.scope,
321
- kind: row.kind,
322
- status: row.status,
323
- events: row.events,
324
- links: row.links,
325
- attributes: row.attributes,
326
- startTime: row.startTime,
327
- endTime: row.endTime,
328
- other: row.other,
329
- createdAt: row.createdAt,
330
- }));
331
-
332
- return {
333
- traces,
334
- total,
335
- page,
336
- perPage,
337
- hasMore: currentOffset + traces.length < total,
338
- };
339
- } catch (error) {
340
- throw new MastraError(
341
- {
342
- id: 'MASTRA_STORAGE_PG_STORE_GET_TRACES_PAGINATED_FAILED_TO_RETRIEVE_TRACES',
343
- domain: ErrorDomain.STORAGE,
344
- category: ErrorCategory.THIRD_PARTY,
345
- details: {
346
- name: args.name ?? '',
347
- scope: args.scope ?? '',
348
- },
349
- },
350
- error,
351
- );
352
- }
154
+ public async getTracesPaginated(args: StorageGetTracesPaginatedArg): Promise<PaginationInfo & { traces: Trace[] }> {
155
+ return this.stores.traces.getTracesPaginated(args);
353
156
  }
354
157
 
355
- private async setupSchema() {
356
- if (!this.schema || this.schemaSetupComplete) {
357
- return;
358
- }
359
-
360
- if (!this.setupSchemaPromise) {
361
- this.setupSchemaPromise = (async () => {
362
- try {
363
- // First check if schema exists and we have usage permission
364
- const schemaExists = await this.db.oneOrNone(
365
- `
366
- SELECT EXISTS (
367
- SELECT 1 FROM information_schema.schemata
368
- WHERE schema_name = $1
369
- )
370
- `,
371
- [this.schema],
372
- );
373
-
374
- if (!schemaExists?.exists) {
375
- try {
376
- await this.db.none(`CREATE SCHEMA IF NOT EXISTS ${this.getSchemaName()}`);
377
- this.logger.info(`Schema "${this.schema}" created successfully`);
378
- } catch (error) {
379
- this.logger.error(`Failed to create schema "${this.schema}"`, { error });
380
- throw new Error(
381
- `Unable to create schema "${this.schema}". This requires CREATE privilege on the database. ` +
382
- `Either create the schema manually or grant CREATE privilege to the user.`,
383
- );
384
- }
385
- }
386
-
387
- // If we got here, schema exists and we can use it
388
- this.schemaSetupComplete = true;
389
- this.logger.debug(`Schema "${this.schema}" is ready for use`);
390
- } catch (error) {
391
- // Reset flags so we can retry
392
- this.schemaSetupComplete = undefined;
393
- this.setupSchemaPromise = null;
394
- throw error;
395
- } finally {
396
- this.setupSchemaPromise = null;
397
- }
398
- })();
399
- }
400
-
401
- await this.setupSchemaPromise;
158
+ async batchTraceInsert({ records }: { records: Record<string, any>[] }): Promise<void> {
159
+ return this.stores.traces.batchTraceInsert({ records });
402
160
  }
403
161
 
404
162
  async createTable({
@@ -408,76 +166,9 @@ export class PostgresStore extends MastraStorage {
408
166
  tableName: TABLE_NAMES;
409
167
  schema: Record<string, StorageColumn>;
410
168
  }): Promise<void> {
411
- try {
412
- const columns = Object.entries(schema)
413
- .map(([name, def]) => {
414
- const parsedName = parseSqlIdentifier(name, 'column name');
415
- const constraints = [];
416
- if (def.primaryKey) constraints.push('PRIMARY KEY');
417
- if (!def.nullable) constraints.push('NOT NULL');
418
- return `"${parsedName}" ${def.type.toUpperCase()} ${constraints.join(' ')}`;
419
- })
420
- .join(',\n');
421
-
422
- // Create schema if it doesn't exist
423
- if (this.schema) {
424
- await this.setupSchema();
425
- }
426
-
427
- const sql = `
428
- CREATE TABLE IF NOT EXISTS ${this.getTableName(tableName)} (
429
- ${columns}
430
- );
431
- ${
432
- tableName === TABLE_WORKFLOW_SNAPSHOT
433
- ? `
434
- DO $$ BEGIN
435
- IF NOT EXISTS (
436
- SELECT 1 FROM pg_constraint WHERE conname = 'mastra_workflow_snapshot_workflow_name_run_id_key'
437
- ) THEN
438
- ALTER TABLE ${this.getTableName(tableName)}
439
- ADD CONSTRAINT mastra_workflow_snapshot_workflow_name_run_id_key
440
- UNIQUE (workflow_name, run_id);
441
- END IF;
442
- END $$;
443
- `
444
- : ''
445
- }
446
- `;
447
-
448
- await this.db.none(sql);
449
- } catch (error) {
450
- throw new MastraError(
451
- {
452
- id: 'MASTRA_STORAGE_PG_STORE_CREATE_TABLE_FAILED',
453
- domain: ErrorDomain.STORAGE,
454
- category: ErrorCategory.THIRD_PARTY,
455
- details: {
456
- tableName,
457
- },
458
- },
459
- error,
460
- );
461
- }
169
+ return this.stores.operations.createTable({ tableName, schema });
462
170
  }
463
171
 
464
- protected getDefaultValue(type: StorageColumn['type']): string {
465
- switch (type) {
466
- case 'timestamp':
467
- return 'DEFAULT NOW()';
468
- case 'jsonb':
469
- return "DEFAULT '{}'::jsonb";
470
- default:
471
- return super.getDefaultValue(type);
472
- }
473
- }
474
-
475
- /**
476
- * Alters table schema to add columns if they don't exist
477
- * @param tableName Name of the table
478
- * @param schema Schema of the table
479
- * @param ifNotExists Array of column names to add if they don't exist
480
- */
481
172
  async alterTable({
482
173
  tableName,
483
174
  schema,
@@ -487,288 +178,54 @@ export class PostgresStore extends MastraStorage {
487
178
  schema: Record<string, StorageColumn>;
488
179
  ifNotExists: string[];
489
180
  }): Promise<void> {
490
- const fullTableName = this.getTableName(tableName);
491
-
492
- try {
493
- for (const columnName of ifNotExists) {
494
- if (schema[columnName]) {
495
- const columnDef = schema[columnName];
496
- const sqlType = this.getSqlType(columnDef.type);
497
- const nullable = columnDef.nullable === false ? 'NOT NULL' : '';
498
- const defaultValue = columnDef.nullable === false ? this.getDefaultValue(columnDef.type) : '';
499
- const parsedColumnName = parseSqlIdentifier(columnName, 'column name');
500
- const alterSql =
501
- `ALTER TABLE ${fullTableName} ADD COLUMN IF NOT EXISTS "${parsedColumnName}" ${sqlType} ${nullable} ${defaultValue}`.trim();
502
-
503
- await this.db.none(alterSql);
504
- this.logger?.debug?.(`Ensured column ${parsedColumnName} exists in table ${fullTableName}`);
505
- }
506
- }
507
- } catch (error) {
508
- throw new MastraError(
509
- {
510
- id: 'MASTRA_STORAGE_PG_STORE_ALTER_TABLE_FAILED',
511
- domain: ErrorDomain.STORAGE,
512
- category: ErrorCategory.THIRD_PARTY,
513
- details: {
514
- tableName,
515
- },
516
- },
517
- error,
518
- );
519
- }
181
+ return this.stores.operations.alterTable({ tableName, schema, ifNotExists });
520
182
  }
521
183
 
522
184
  async clearTable({ tableName }: { tableName: TABLE_NAMES }): Promise<void> {
523
- try {
524
- await this.db.none(`TRUNCATE TABLE ${this.getTableName(tableName)} CASCADE`);
525
- } catch (error) {
526
- throw new MastraError(
527
- {
528
- id: 'MASTRA_STORAGE_PG_STORE_CLEAR_TABLE_FAILED',
529
- domain: ErrorDomain.STORAGE,
530
- category: ErrorCategory.THIRD_PARTY,
531
- details: {
532
- tableName,
533
- },
534
- },
535
- error,
536
- );
537
- }
185
+ return this.stores.operations.clearTable({ tableName });
186
+ }
187
+
188
+ async dropTable({ tableName }: { tableName: TABLE_NAMES }): Promise<void> {
189
+ return this.stores.operations.dropTable({ tableName });
538
190
  }
539
191
 
540
192
  async insert({ tableName, record }: { tableName: TABLE_NAMES; record: Record<string, any> }): Promise<void> {
541
- try {
542
- const columns = Object.keys(record).map(col => parseSqlIdentifier(col, 'column name'));
543
- const values = Object.values(record);
544
- const placeholders = values.map((_, i) => `$${i + 1}`).join(', ');
193
+ return this.stores.operations.insert({ tableName, record });
194
+ }
545
195
 
546
- await this.db.none(
547
- `INSERT INTO ${this.getTableName(tableName)} (${columns.map(c => `"${c}"`).join(', ')}) VALUES (${placeholders})`,
548
- values,
549
- );
550
- } catch (error) {
551
- throw new MastraError(
552
- {
553
- id: 'MASTRA_STORAGE_PG_STORE_INSERT_FAILED',
554
- domain: ErrorDomain.STORAGE,
555
- category: ErrorCategory.THIRD_PARTY,
556
- details: {
557
- tableName,
558
- },
559
- },
560
- error,
561
- );
562
- }
196
+ async batchInsert({ tableName, records }: { tableName: TABLE_NAMES; records: Record<string, any>[] }): Promise<void> {
197
+ return this.stores.operations.batchInsert({ tableName, records });
563
198
  }
564
199
 
565
200
  async load<R>({ tableName, keys }: { tableName: TABLE_NAMES; keys: Record<string, string> }): Promise<R | null> {
566
- try {
567
- const keyEntries = Object.entries(keys).map(([key, value]) => [parseSqlIdentifier(key, 'column name'), value]);
568
- const conditions = keyEntries.map(([key], index) => `"${key}" = $${index + 1}`).join(' AND ');
569
- const values = keyEntries.map(([_, value]) => value);
570
-
571
- const result = await this.db.oneOrNone<R>(
572
- `SELECT * FROM ${this.getTableName(tableName)} WHERE ${conditions}`,
573
- values,
574
- );
575
-
576
- if (!result) {
577
- return null;
578
- }
579
-
580
- // If this is a workflow snapshot, parse the snapshot field
581
- if (tableName === TABLE_WORKFLOW_SNAPSHOT) {
582
- const snapshot = result as any;
583
- if (typeof snapshot.snapshot === 'string') {
584
- snapshot.snapshot = JSON.parse(snapshot.snapshot);
585
- }
586
- return snapshot;
587
- }
588
-
589
- return result;
590
- } catch (error) {
591
- throw new MastraError(
592
- {
593
- id: 'MASTRA_STORAGE_PG_STORE_LOAD_FAILED',
594
- domain: ErrorDomain.STORAGE,
595
- category: ErrorCategory.THIRD_PARTY,
596
- details: {
597
- tableName,
598
- },
599
- },
600
- error,
601
- );
602
- }
201
+ return this.stores.operations.load({ tableName, keys });
603
202
  }
604
203
 
605
- async getThreadById({ threadId }: { threadId: string }): Promise<StorageThreadType | null> {
606
- try {
607
- const thread = await this.db.oneOrNone<StorageThreadType>(
608
- `SELECT
609
- id,
610
- "resourceId",
611
- title,
612
- metadata,
613
- "createdAt",
614
- "updatedAt"
615
- FROM ${this.getTableName(TABLE_THREADS)}
616
- WHERE id = $1`,
617
- [threadId],
618
- );
619
-
620
- if (!thread) {
621
- return null;
622
- }
204
+ /**
205
+ * Memory
206
+ */
623
207
 
624
- return {
625
- ...thread,
626
- metadata: typeof thread.metadata === 'string' ? JSON.parse(thread.metadata) : thread.metadata,
627
- createdAt: thread.createdAt,
628
- updatedAt: thread.updatedAt,
629
- };
630
- } catch (error) {
631
- throw new MastraError(
632
- {
633
- id: 'MASTRA_STORAGE_PG_STORE_GET_THREAD_BY_ID_FAILED',
634
- domain: ErrorDomain.STORAGE,
635
- category: ErrorCategory.THIRD_PARTY,
636
- details: {
637
- threadId,
638
- },
639
- },
640
- error,
641
- );
642
- }
208
+ async getThreadById({ threadId }: { threadId: string }): Promise<StorageThreadType | null> {
209
+ return this.stores.memory.getThreadById({ threadId });
643
210
  }
644
211
 
645
212
  /**
646
213
  * @deprecated use getThreadsByResourceIdPaginated instead
647
214
  */
648
215
  public async getThreadsByResourceId(args: { resourceId: string }): Promise<StorageThreadType[]> {
649
- const { resourceId } = args;
650
-
651
- try {
652
- const baseQuery = `FROM ${this.getTableName(TABLE_THREADS)} WHERE "resourceId" = $1`;
653
- const queryParams: any[] = [resourceId];
654
-
655
- const dataQuery = `SELECT id, "resourceId", title, metadata, "createdAt", "updatedAt" ${baseQuery} ORDER BY "createdAt" DESC`;
656
- const rows = await this.db.manyOrNone(dataQuery, queryParams);
657
- return (rows || []).map(thread => ({
658
- ...thread,
659
- metadata: typeof thread.metadata === 'string' ? JSON.parse(thread.metadata) : thread.metadata,
660
- createdAt: thread.createdAt,
661
- updatedAt: thread.updatedAt,
662
- }));
663
- } catch (error) {
664
- this.logger.error(`Error getting threads for resource ${resourceId}:`, error);
665
- return [];
666
- }
216
+ return this.stores.memory.getThreadsByResourceId(args);
667
217
  }
668
218
 
669
- public async getThreadsByResourceIdPaginated(
670
- args: {
671
- resourceId: string;
672
- } & PaginationArgs,
673
- ): Promise<PaginationInfo & { threads: StorageThreadType[] }> {
674
- const { resourceId, page = 0, perPage: perPageInput } = args;
675
- try {
676
- const baseQuery = `FROM ${this.getTableName(TABLE_THREADS)} WHERE "resourceId" = $1`;
677
- const queryParams: any[] = [resourceId];
678
- const perPage = perPageInput !== undefined ? perPageInput : 100;
679
- const currentOffset = page * perPage;
680
-
681
- const countQuery = `SELECT COUNT(*) ${baseQuery}`;
682
- const countResult = await this.db.one(countQuery, queryParams);
683
- const total = parseInt(countResult.count, 10);
684
-
685
- if (total === 0) {
686
- return {
687
- threads: [],
688
- total: 0,
689
- page,
690
- perPage,
691
- hasMore: false,
692
- };
693
- }
694
-
695
- const dataQuery = `SELECT id, "resourceId", title, metadata, "createdAt", "updatedAt" ${baseQuery} ORDER BY "createdAt" DESC LIMIT $2 OFFSET $3`;
696
- const rows = await this.db.manyOrNone(dataQuery, [...queryParams, perPage, currentOffset]);
697
-
698
- const threads = (rows || []).map(thread => ({
699
- ...thread,
700
- metadata: typeof thread.metadata === 'string' ? JSON.parse(thread.metadata) : thread.metadata,
701
- createdAt: thread.createdAt, // Assuming already Date objects or ISO strings
702
- updatedAt: thread.updatedAt,
703
- }));
704
-
705
- return {
706
- threads,
707
- total,
708
- page,
709
- perPage,
710
- hasMore: currentOffset + threads.length < total,
711
- };
712
- } catch (error) {
713
- const mastraError = new MastraError(
714
- {
715
- id: 'MASTRA_STORAGE_PG_STORE_GET_THREADS_BY_RESOURCE_ID_PAGINATED_FAILED',
716
- domain: ErrorDomain.STORAGE,
717
- category: ErrorCategory.THIRD_PARTY,
718
- details: {
719
- resourceId,
720
- page,
721
- },
722
- },
723
- error,
724
- );
725
- this.logger?.error?.(mastraError.toString());
726
- this.logger?.trackException(mastraError);
727
- return { threads: [], total: 0, page, perPage: perPageInput || 100, hasMore: false };
728
- }
219
+ public async getThreadsByResourceIdPaginated(args: {
220
+ resourceId: string;
221
+ page: number;
222
+ perPage: number;
223
+ }): Promise<PaginationInfo & { threads: StorageThreadType[] }> {
224
+ return this.stores.memory.getThreadsByResourceIdPaginated(args);
729
225
  }
730
226
 
731
227
  async saveThread({ thread }: { thread: StorageThreadType }): Promise<StorageThreadType> {
732
- try {
733
- await this.db.none(
734
- `INSERT INTO ${this.getTableName(TABLE_THREADS)} (
735
- id,
736
- "resourceId",
737
- title,
738
- metadata,
739
- "createdAt",
740
- "updatedAt"
741
- ) VALUES ($1, $2, $3, $4, $5, $6)
742
- ON CONFLICT (id) DO UPDATE SET
743
- "resourceId" = EXCLUDED."resourceId",
744
- title = EXCLUDED.title,
745
- metadata = EXCLUDED.metadata,
746
- "createdAt" = EXCLUDED."createdAt",
747
- "updatedAt" = EXCLUDED."updatedAt"`,
748
- [
749
- thread.id,
750
- thread.resourceId,
751
- thread.title,
752
- thread.metadata ? JSON.stringify(thread.metadata) : null,
753
- thread.createdAt,
754
- thread.updatedAt,
755
- ],
756
- );
757
-
758
- return thread;
759
- } catch (error) {
760
- throw new MastraError(
761
- {
762
- id: 'MASTRA_STORAGE_PG_STORE_SAVE_THREAD_FAILED',
763
- domain: ErrorDomain.STORAGE,
764
- category: ErrorCategory.THIRD_PARTY,
765
- details: {
766
- threadId: thread.id,
767
- },
768
- },
769
- error,
770
- );
771
- }
228
+ return this.stores.memory.saveThread({ thread });
772
229
  }
773
230
 
774
231
  async updateThread({
@@ -780,150 +237,11 @@ export class PostgresStore extends MastraStorage {
780
237
  title: string;
781
238
  metadata: Record<string, unknown>;
782
239
  }): Promise<StorageThreadType> {
783
- // First get the existing thread to merge metadata
784
- const existingThread = await this.getThreadById({ threadId: id });
785
- if (!existingThread) {
786
- throw new MastraError({
787
- id: 'MASTRA_STORAGE_PG_STORE_UPDATE_THREAD_FAILED',
788
- domain: ErrorDomain.STORAGE,
789
- category: ErrorCategory.USER,
790
- text: `Thread ${id} not found`,
791
- details: {
792
- threadId: id,
793
- title,
794
- },
795
- });
796
- }
797
-
798
- // Merge the existing metadata with the new metadata
799
- const mergedMetadata = {
800
- ...existingThread.metadata,
801
- ...metadata,
802
- };
803
-
804
- try {
805
- const thread = await this.db.one<StorageThreadType>(
806
- `UPDATE ${this.getTableName(TABLE_THREADS)}
807
- SET title = $1,
808
- metadata = $2,
809
- "updatedAt" = $3
810
- WHERE id = $4
811
- RETURNING *`,
812
- [title, mergedMetadata, new Date().toISOString(), id],
813
- );
814
-
815
- return {
816
- ...thread,
817
- metadata: typeof thread.metadata === 'string' ? JSON.parse(thread.metadata) : thread.metadata,
818
- createdAt: thread.createdAt,
819
- updatedAt: thread.updatedAt,
820
- };
821
- } catch (error) {
822
- throw new MastraError(
823
- {
824
- id: 'MASTRA_STORAGE_PG_STORE_UPDATE_THREAD_FAILED',
825
- domain: ErrorDomain.STORAGE,
826
- category: ErrorCategory.THIRD_PARTY,
827
- details: {
828
- threadId: id,
829
- title,
830
- },
831
- },
832
- error,
833
- );
834
- }
240
+ return this.stores.memory.updateThread({ id, title, metadata });
835
241
  }
836
242
 
837
243
  async deleteThread({ threadId }: { threadId: string }): Promise<void> {
838
- try {
839
- await this.db.tx(async t => {
840
- // First delete all messages associated with this thread
841
- await t.none(`DELETE FROM ${this.getTableName(TABLE_MESSAGES)} WHERE thread_id = $1`, [threadId]);
842
-
843
- // Then delete the thread
844
- await t.none(`DELETE FROM ${this.getTableName(TABLE_THREADS)} WHERE id = $1`, [threadId]);
845
- });
846
- } catch (error) {
847
- throw new MastraError(
848
- {
849
- id: 'MASTRA_STORAGE_PG_STORE_DELETE_THREAD_FAILED',
850
- domain: ErrorDomain.STORAGE,
851
- category: ErrorCategory.THIRD_PARTY,
852
- details: {
853
- threadId,
854
- },
855
- },
856
- error,
857
- );
858
- }
859
- }
860
-
861
- private async _getIncludedMessages({
862
- threadId,
863
- selectBy,
864
- orderByStatement,
865
- }: {
866
- threadId: string;
867
- selectBy: StorageGetMessagesArg['selectBy'];
868
- orderByStatement: string;
869
- }) {
870
- const include = selectBy?.include;
871
- if (!include) return null;
872
-
873
- const unionQueries: string[] = [];
874
- const params: any[] = [];
875
- let paramIdx = 1;
876
-
877
- for (const inc of include) {
878
- const { id, withPreviousMessages = 0, withNextMessages = 0 } = inc;
879
- // if threadId is provided, use it, otherwise use threadId from args
880
- const searchId = inc.threadId || threadId;
881
- unionQueries.push(
882
- `
883
- SELECT * FROM (
884
- WITH ordered_messages AS (
885
- SELECT
886
- *,
887
- ROW_NUMBER() OVER (${orderByStatement}) as row_num
888
- FROM ${this.getTableName(TABLE_MESSAGES)}
889
- WHERE thread_id = $${paramIdx}
890
- )
891
- SELECT
892
- m.id,
893
- m.content,
894
- m.role,
895
- m.type,
896
- m."createdAt",
897
- m.thread_id AS "threadId",
898
- m."resourceId"
899
- FROM ordered_messages m
900
- WHERE m.id = $${paramIdx + 1}
901
- OR EXISTS (
902
- SELECT 1 FROM ordered_messages target
903
- WHERE target.id = $${paramIdx + 1}
904
- AND (
905
- -- Get previous messages based on the max withPreviousMessages
906
- (m.row_num <= target.row_num + $${paramIdx + 2} AND m.row_num > target.row_num)
907
- OR
908
- -- Get next messages based on the max withNextMessages
909
- (m.row_num >= target.row_num - $${paramIdx + 3} AND m.row_num < target.row_num)
910
- )
911
- )
912
- ) AS query_${paramIdx}
913
- `, // Keep ASC for final sorting after fetching context
914
- );
915
- params.push(searchId, id, withPreviousMessages, withNextMessages);
916
- paramIdx += 4;
917
- }
918
- const finalQuery = unionQueries.join(' UNION ALL ') + ' ORDER BY "createdAt" ASC';
919
- const includedRows = await this.db.manyOrNone(finalQuery, params);
920
- const seen = new Set<string>();
921
- const dedupedRows = includedRows.filter(row => {
922
- if (seen.has(row.id)) return false;
923
- seen.add(row.id);
924
- return true;
925
- });
926
- return dedupedRows;
244
+ return this.stores.memory.deleteThread({ threadId });
927
245
  }
928
246
 
929
247
  /**
@@ -936,73 +254,7 @@ export class PostgresStore extends MastraStorage {
936
254
  format?: 'v1' | 'v2';
937
255
  },
938
256
  ): Promise<MastraMessageV1[] | MastraMessageV2[]> {
939
- const { threadId, format, selectBy } = args;
940
-
941
- const selectStatement = `SELECT id, content, role, type, "createdAt", thread_id AS "threadId", "resourceId"`;
942
- const orderByStatement = `ORDER BY "createdAt" DESC`;
943
- const limit = this.resolveMessageLimit({ last: selectBy?.last, defaultLimit: 40 });
944
-
945
- try {
946
- let rows: any[] = [];
947
- const include = selectBy?.include || [];
948
-
949
- if (include?.length) {
950
- const includeMessages = await this._getIncludedMessages({ threadId, selectBy, orderByStatement });
951
- if (includeMessages) {
952
- rows.push(...includeMessages);
953
- }
954
- }
955
-
956
- const excludeIds = rows.map(m => m.id);
957
- const excludeIdsParam = excludeIds.map((_, idx) => `$${idx + 2}`).join(', ');
958
- let query = `${selectStatement} FROM ${this.getTableName(TABLE_MESSAGES)} WHERE thread_id = $1
959
- ${excludeIds.length ? `AND id NOT IN (${excludeIdsParam})` : ''}
960
- ${orderByStatement}
961
- LIMIT $${excludeIds.length + 2}
962
- `;
963
- const queryParams: any[] = [threadId, ...excludeIds, limit];
964
- const remainingRows = await this.db.manyOrNone(query, queryParams);
965
- rows.push(...remainingRows);
966
-
967
- const fetchedMessages = (rows || []).map(message => {
968
- if (typeof message.content === 'string') {
969
- try {
970
- message.content = JSON.parse(message.content);
971
- } catch {
972
- /* ignore */
973
- }
974
- }
975
- if (message.type === 'v2') delete message.type;
976
- return message as MastraMessageV1;
977
- });
978
-
979
- // Sort all messages by creation date
980
- const sortedMessages = fetchedMessages.sort(
981
- (a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(),
982
- );
983
-
984
- return format === 'v2'
985
- ? sortedMessages.map(
986
- m =>
987
- ({ ...m, content: m.content || { format: 2, parts: [{ type: 'text', text: '' }] } }) as MastraMessageV2,
988
- )
989
- : sortedMessages;
990
- } catch (error) {
991
- const mastraError = new MastraError(
992
- {
993
- id: 'MASTRA_STORAGE_PG_STORE_GET_MESSAGES_FAILED',
994
- domain: ErrorDomain.STORAGE,
995
- category: ErrorCategory.THIRD_PARTY,
996
- details: {
997
- threadId,
998
- },
999
- },
1000
- error,
1001
- );
1002
- this.logger?.error?.(mastraError.toString());
1003
- this.logger?.trackException(mastraError);
1004
- return [];
1005
- }
257
+ return this.stores.memory.getMessages(args);
1006
258
  }
1007
259
 
1008
260
  public async getMessagesPaginated(
@@ -1010,220 +262,54 @@ export class PostgresStore extends MastraStorage {
1010
262
  format?: 'v1' | 'v2';
1011
263
  },
1012
264
  ): Promise<PaginationInfo & { messages: MastraMessageV1[] | MastraMessageV2[] }> {
1013
- const { threadId, format, selectBy } = args;
1014
- const { page = 0, perPage: perPageInput, dateRange } = selectBy?.pagination || {};
1015
- const fromDate = dateRange?.start;
1016
- const toDate = dateRange?.end;
1017
-
1018
- const selectStatement = `SELECT id, content, role, type, "createdAt", thread_id AS "threadId", "resourceId"`;
1019
- const orderByStatement = `ORDER BY "createdAt" DESC`;
1020
-
1021
- const messages: MastraMessageV2[] = [];
1022
-
1023
- if (selectBy?.include?.length) {
1024
- const includeMessages = await this._getIncludedMessages({ threadId, selectBy, orderByStatement });
1025
- if (includeMessages) {
1026
- messages.push(...includeMessages);
1027
- }
1028
- }
1029
-
1030
- try {
1031
- const perPage =
1032
- perPageInput !== undefined
1033
- ? perPageInput
1034
- : this.resolveMessageLimit({ last: selectBy?.last, defaultLimit: 40 });
1035
- const currentOffset = page * perPage;
1036
-
1037
- const conditions: string[] = [`thread_id = $1`];
1038
- const queryParams: any[] = [threadId];
1039
- let paramIndex = 2;
1040
-
1041
- if (fromDate) {
1042
- conditions.push(`"createdAt" >= $${paramIndex++}`);
1043
- queryParams.push(fromDate);
1044
- }
1045
- if (toDate) {
1046
- conditions.push(`"createdAt" <= $${paramIndex++}`);
1047
- queryParams.push(toDate);
1048
- }
1049
- const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
1050
-
1051
- const countQuery = `SELECT COUNT(*) FROM ${this.getTableName(TABLE_MESSAGES)} ${whereClause}`;
1052
- const countResult = await this.db.one(countQuery, queryParams);
1053
- const total = parseInt(countResult.count, 10);
1054
-
1055
- if (total === 0 && messages.length === 0) {
1056
- return {
1057
- messages: [],
1058
- total: 0,
1059
- page,
1060
- perPage,
1061
- hasMore: false,
1062
- };
1063
- }
1064
-
1065
- const excludeIds = messages.map(m => m.id);
1066
- const excludeIdsParam = excludeIds.map((_, idx) => `$${idx + paramIndex}`).join(', ');
1067
- paramIndex += excludeIds.length;
1068
-
1069
- const dataQuery = `${selectStatement} FROM ${this.getTableName(
1070
- TABLE_MESSAGES,
1071
- )} ${whereClause} ${excludeIds.length ? `AND id NOT IN (${excludeIdsParam})` : ''}${orderByStatement} LIMIT $${paramIndex++} OFFSET $${paramIndex++}`;
1072
- const rows = await this.db.manyOrNone(dataQuery, [...queryParams, ...excludeIds, perPage, currentOffset]);
1073
- messages.push(...(rows || []));
1074
-
1075
- // Parse content back to objects if they were stringified during storage
1076
- const messagesWithParsedContent = messages.map(message => {
1077
- if (typeof message.content === 'string') {
1078
- try {
1079
- return { ...message, content: JSON.parse(message.content) };
1080
- } catch {
1081
- // If parsing fails, leave as string (V1 message)
1082
- return message;
1083
- }
1084
- }
1085
- return message;
1086
- });
1087
-
1088
- const list = new MessageList().add(messagesWithParsedContent, 'memory');
1089
- const messagesToReturn = format === `v2` ? list.get.all.v2() : list.get.all.v1();
1090
-
1091
- return {
1092
- messages: messagesToReturn,
1093
- total,
1094
- page,
1095
- perPage,
1096
- hasMore: currentOffset + rows.length < total,
1097
- };
1098
- } catch (error) {
1099
- const mastraError = new MastraError(
1100
- {
1101
- id: 'MASTRA_STORAGE_PG_STORE_GET_MESSAGES_PAGINATED_FAILED',
1102
- domain: ErrorDomain.STORAGE,
1103
- category: ErrorCategory.THIRD_PARTY,
1104
- details: {
1105
- threadId,
1106
- page,
1107
- },
1108
- },
1109
- error,
1110
- );
1111
- this.logger?.error?.(mastraError.toString());
1112
- this.logger?.trackException(mastraError);
1113
- return { messages: [], total: 0, page, perPage: perPageInput || 40, hasMore: false };
1114
- }
265
+ return this.stores.memory.getMessagesPaginated(args);
1115
266
  }
1116
267
 
1117
268
  async saveMessages(args: { messages: MastraMessageV1[]; format?: undefined | 'v1' }): Promise<MastraMessageV1[]>;
1118
269
  async saveMessages(args: { messages: MastraMessageV2[]; format: 'v2' }): Promise<MastraMessageV2[]>;
1119
- async saveMessages({
1120
- messages,
1121
- format,
1122
- }:
1123
- | { messages: MastraMessageV1[]; format?: undefined | 'v1' }
1124
- | { messages: MastraMessageV2[]; format: 'v2' }): Promise<MastraMessageV2[] | MastraMessageV1[]> {
1125
- if (messages.length === 0) return messages;
1126
-
1127
- const threadId = messages[0]?.threadId;
1128
- if (!threadId) {
1129
- throw new MastraError({
1130
- id: 'MASTRA_STORAGE_PG_STORE_SAVE_MESSAGES_FAILED',
1131
- domain: ErrorDomain.STORAGE,
1132
- category: ErrorCategory.THIRD_PARTY,
1133
- text: `Thread ID is required`,
1134
- });
1135
- }
1136
-
1137
- // Check if thread exists
1138
- const thread = await this.getThreadById({ threadId });
1139
- if (!thread) {
1140
- throw new MastraError({
1141
- id: 'MASTRA_STORAGE_PG_STORE_SAVE_MESSAGES_FAILED',
1142
- domain: ErrorDomain.STORAGE,
1143
- category: ErrorCategory.THIRD_PARTY,
1144
- text: `Thread ${threadId} not found`,
1145
- details: {
1146
- threadId,
1147
- },
1148
- });
1149
- }
1150
-
1151
- try {
1152
- await this.db.tx(async t => {
1153
- // Execute message inserts and thread update in parallel for better performance
1154
- const messageInserts = messages.map(message => {
1155
- if (!message.threadId) {
1156
- throw new Error(
1157
- `Expected to find a threadId for message, but couldn't find one. An unexpected error has occurred.`,
1158
- );
1159
- }
1160
- if (!message.resourceId) {
1161
- throw new Error(
1162
- `Expected to find a resourceId for message, but couldn't find one. An unexpected error has occurred.`,
1163
- );
1164
- }
1165
- return t.none(
1166
- `INSERT INTO ${this.getTableName(TABLE_MESSAGES)} (id, thread_id, content, "createdAt", role, type, "resourceId")
1167
- VALUES ($1, $2, $3, $4, $5, $6, $7)
1168
- ON CONFLICT (id) DO UPDATE SET
1169
- thread_id = EXCLUDED.thread_id,
1170
- content = EXCLUDED.content,
1171
- role = EXCLUDED.role,
1172
- type = EXCLUDED.type,
1173
- "resourceId" = EXCLUDED."resourceId"`,
1174
- [
1175
- message.id,
1176
- message.threadId,
1177
- typeof message.content === 'string' ? message.content : JSON.stringify(message.content),
1178
- message.createdAt || new Date().toISOString(),
1179
- message.role,
1180
- message.type || 'v2',
1181
- message.resourceId,
1182
- ],
1183
- );
1184
- });
270
+ async saveMessages(
271
+ args: { messages: MastraMessageV1[]; format?: undefined | 'v1' } | { messages: MastraMessageV2[]; format: 'v2' },
272
+ ): Promise<MastraMessageV2[] | MastraMessageV1[]> {
273
+ return this.stores.memory.saveMessages(args);
274
+ }
1185
275
 
1186
- const threadUpdate = t.none(
1187
- `UPDATE ${this.getTableName(TABLE_THREADS)}
1188
- SET "updatedAt" = $1
1189
- WHERE id = $2`,
1190
- [new Date().toISOString(), threadId],
1191
- );
276
+ async updateMessages({
277
+ messages,
278
+ }: {
279
+ messages: (Partial<Omit<MastraMessageV2, 'createdAt'>> & {
280
+ id: string;
281
+ content?: {
282
+ metadata?: MastraMessageContentV2['metadata'];
283
+ content?: MastraMessageContentV2['content'];
284
+ };
285
+ })[];
286
+ }): Promise<MastraMessageV2[]> {
287
+ return this.stores.memory.updateMessages({ messages });
288
+ }
1192
289
 
1193
- await Promise.all([...messageInserts, threadUpdate]);
1194
- });
290
+ async getResourceById({ resourceId }: { resourceId: string }): Promise<StorageResourceType | null> {
291
+ return this.stores.memory.getResourceById({ resourceId });
292
+ }
1195
293
 
1196
- // Parse content back to objects if they were stringified during storage
1197
- const messagesWithParsedContent = messages.map(message => {
1198
- if (typeof message.content === 'string') {
1199
- try {
1200
- return { ...message, content: JSON.parse(message.content) };
1201
- } catch {
1202
- // If parsing fails, leave as string (V1 message)
1203
- return message;
1204
- }
1205
- }
1206
- return message;
1207
- });
294
+ async saveResource({ resource }: { resource: StorageResourceType }): Promise<StorageResourceType> {
295
+ return this.stores.memory.saveResource({ resource });
296
+ }
1208
297
 
1209
- const list = new MessageList().add(messagesWithParsedContent, 'memory');
1210
- if (format === `v2`) return list.get.all.v2();
1211
- return list.get.all.v1();
1212
- } catch (error) {
1213
- throw new MastraError(
1214
- {
1215
- id: 'MASTRA_STORAGE_PG_STORE_SAVE_MESSAGES_FAILED',
1216
- domain: ErrorDomain.STORAGE,
1217
- category: ErrorCategory.THIRD_PARTY,
1218
- details: {
1219
- threadId,
1220
- },
1221
- },
1222
- error,
1223
- );
1224
- }
298
+ async updateResource({
299
+ resourceId,
300
+ workingMemory,
301
+ metadata,
302
+ }: {
303
+ resourceId: string;
304
+ workingMemory?: string;
305
+ metadata?: Record<string, unknown>;
306
+ }): Promise<StorageResourceType> {
307
+ return this.stores.memory.updateResource({ resourceId, workingMemory, metadata });
1225
308
  }
1226
309
 
310
+ /**
311
+ * Workflows
312
+ */
1227
313
  async persistWorkflowSnapshot({
1228
314
  workflowName,
1229
315
  runId,
@@ -1233,35 +319,7 @@ export class PostgresStore extends MastraStorage {
1233
319
  runId: string;
1234
320
  snapshot: WorkflowRunState;
1235
321
  }): Promise<void> {
1236
- try {
1237
- const now = new Date().toISOString();
1238
- await this.db.none(
1239
- `INSERT INTO ${this.getTableName(TABLE_WORKFLOW_SNAPSHOT)} (
1240
- workflow_name,
1241
- run_id,
1242
- snapshot,
1243
- "createdAt",
1244
- "updatedAt"
1245
- ) VALUES ($1, $2, $3, $4, $5)
1246
- ON CONFLICT (workflow_name, run_id) DO UPDATE
1247
- SET snapshot = EXCLUDED.snapshot,
1248
- "updatedAt" = EXCLUDED."updatedAt"`,
1249
- [workflowName, runId, JSON.stringify(snapshot), now, now],
1250
- );
1251
- } catch (error) {
1252
- throw new MastraError(
1253
- {
1254
- id: 'MASTRA_STORAGE_PG_STORE_PERSIST_WORKFLOW_SNAPSHOT_FAILED',
1255
- domain: ErrorDomain.STORAGE,
1256
- category: ErrorCategory.THIRD_PARTY,
1257
- details: {
1258
- workflowName,
1259
- runId,
1260
- },
1261
- },
1262
- error,
1263
- );
1264
- }
322
+ return this.stores.workflows.persistWorkflowSnapshot({ workflowName, runId, snapshot });
1265
323
  }
1266
324
 
1267
325
  async loadWorkflowSnapshot({
@@ -1271,65 +329,7 @@ export class PostgresStore extends MastraStorage {
1271
329
  workflowName: string;
1272
330
  runId: string;
1273
331
  }): Promise<WorkflowRunState | null> {
1274
- try {
1275
- const result = await this.load({
1276
- tableName: TABLE_WORKFLOW_SNAPSHOT,
1277
- keys: {
1278
- workflow_name: workflowName,
1279
- run_id: runId,
1280
- },
1281
- });
1282
-
1283
- if (!result) {
1284
- return null;
1285
- }
1286
-
1287
- return (result as any).snapshot;
1288
- } catch (error) {
1289
- throw new MastraError(
1290
- {
1291
- id: 'MASTRA_STORAGE_PG_STORE_LOAD_WORKFLOW_SNAPSHOT_FAILED',
1292
- domain: ErrorDomain.STORAGE,
1293
- category: ErrorCategory.THIRD_PARTY,
1294
- details: {
1295
- workflowName,
1296
- runId,
1297
- },
1298
- },
1299
- error,
1300
- );
1301
- }
1302
- }
1303
-
1304
- private async hasColumn(table: string, column: string): Promise<boolean> {
1305
- // Use this.schema to scope the check
1306
- const schema = this.schema || 'public';
1307
- const result = await this.db.oneOrNone(
1308
- `SELECT 1 FROM information_schema.columns WHERE table_schema = $1 AND table_name = $2 AND (column_name = $3 OR column_name = $4)`,
1309
- [schema, table, column, column.toLowerCase()],
1310
- );
1311
- return !!result;
1312
- }
1313
-
1314
- private parseWorkflowRun(row: any): WorkflowRun {
1315
- let parsedSnapshot: WorkflowRunState | string = row.snapshot as string;
1316
- if (typeof parsedSnapshot === 'string') {
1317
- try {
1318
- parsedSnapshot = JSON.parse(row.snapshot as string) as WorkflowRunState;
1319
- } catch (e) {
1320
- // If parsing fails, return the raw snapshot string
1321
- console.warn(`Failed to parse snapshot for workflow ${row.workflow_name}: ${e}`);
1322
- }
1323
- }
1324
-
1325
- return {
1326
- workflowName: row.workflow_name,
1327
- runId: row.run_id,
1328
- snapshot: parsedSnapshot,
1329
- createdAt: row.createdAt,
1330
- updatedAt: row.updatedAt,
1331
- resourceId: row.resourceId,
1332
- };
332
+ return this.stores.workflows.loadWorkflowSnapshot({ workflowName, runId });
1333
333
  }
1334
334
 
1335
335
  async getWorkflowRuns({
@@ -1347,82 +347,7 @@ export class PostgresStore extends MastraStorage {
1347
347
  offset?: number;
1348
348
  resourceId?: string;
1349
349
  } = {}): Promise<WorkflowRuns> {
1350
- try {
1351
- const conditions: string[] = [];
1352
- const values: any[] = [];
1353
- let paramIndex = 1;
1354
-
1355
- if (workflowName) {
1356
- conditions.push(`workflow_name = $${paramIndex}`);
1357
- values.push(workflowName);
1358
- paramIndex++;
1359
- }
1360
-
1361
- if (resourceId) {
1362
- const hasResourceId = await this.hasColumn(TABLE_WORKFLOW_SNAPSHOT, 'resourceId');
1363
- if (hasResourceId) {
1364
- conditions.push(`"resourceId" = $${paramIndex}`);
1365
- values.push(resourceId);
1366
- paramIndex++;
1367
- } else {
1368
- console.warn(`[${TABLE_WORKFLOW_SNAPSHOT}] resourceId column not found. Skipping resourceId filter.`);
1369
- }
1370
- }
1371
-
1372
- if (fromDate) {
1373
- conditions.push(`"createdAt" >= $${paramIndex}`);
1374
- values.push(fromDate);
1375
- paramIndex++;
1376
- }
1377
-
1378
- if (toDate) {
1379
- conditions.push(`"createdAt" <= $${paramIndex}`);
1380
- values.push(toDate);
1381
- paramIndex++;
1382
- }
1383
- const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
1384
-
1385
- let total = 0;
1386
- // Only get total count when using pagination
1387
- if (limit !== undefined && offset !== undefined) {
1388
- const countResult = await this.db.one(
1389
- `SELECT COUNT(*) as count FROM ${this.getTableName(TABLE_WORKFLOW_SNAPSHOT)} ${whereClause}`,
1390
- values,
1391
- );
1392
- total = Number(countResult.count);
1393
- }
1394
-
1395
- // Get results
1396
- const query = `
1397
- SELECT * FROM ${this.getTableName(TABLE_WORKFLOW_SNAPSHOT)}
1398
- ${whereClause}
1399
- ORDER BY "createdAt" DESC
1400
- ${limit !== undefined && offset !== undefined ? ` LIMIT $${paramIndex} OFFSET $${paramIndex + 1}` : ''}
1401
- `;
1402
-
1403
- const queryValues = limit !== undefined && offset !== undefined ? [...values, limit, offset] : values;
1404
-
1405
- const result = await this.db.manyOrNone(query, queryValues);
1406
-
1407
- const runs = (result || []).map(row => {
1408
- return this.parseWorkflowRun(row);
1409
- });
1410
-
1411
- // Use runs.length as total when not paginating
1412
- return { runs, total: total || runs.length };
1413
- } catch (error) {
1414
- throw new MastraError(
1415
- {
1416
- id: 'MASTRA_STORAGE_PG_STORE_GET_WORKFLOW_RUNS_FAILED',
1417
- domain: ErrorDomain.STORAGE,
1418
- category: ErrorCategory.THIRD_PARTY,
1419
- details: {
1420
- workflowName: workflowName || 'all',
1421
- },
1422
- },
1423
- error,
1424
- );
1425
- }
350
+ return this.stores.workflows.getWorkflowRuns({ workflowName, fromDate, toDate, limit, offset, resourceId });
1426
351
  }
1427
352
 
1428
353
  async getWorkflowRunById({
@@ -1432,370 +357,57 @@ export class PostgresStore extends MastraStorage {
1432
357
  runId: string;
1433
358
  workflowName?: string;
1434
359
  }): Promise<WorkflowRun | null> {
1435
- try {
1436
- const conditions: string[] = [];
1437
- const values: any[] = [];
1438
- let paramIndex = 1;
1439
-
1440
- if (runId) {
1441
- conditions.push(`run_id = $${paramIndex}`);
1442
- values.push(runId);
1443
- paramIndex++;
1444
- }
1445
-
1446
- if (workflowName) {
1447
- conditions.push(`workflow_name = $${paramIndex}`);
1448
- values.push(workflowName);
1449
- paramIndex++;
1450
- }
1451
-
1452
- const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
1453
-
1454
- // Get results
1455
- const query = `
1456
- SELECT * FROM ${this.getTableName(TABLE_WORKFLOW_SNAPSHOT)}
1457
- ${whereClause}
1458
- `;
1459
-
1460
- const queryValues = values;
1461
-
1462
- const result = await this.db.oneOrNone(query, queryValues);
1463
-
1464
- if (!result) {
1465
- return null;
1466
- }
1467
-
1468
- return this.parseWorkflowRun(result);
1469
- } catch (error) {
1470
- throw new MastraError(
1471
- {
1472
- id: 'MASTRA_STORAGE_PG_STORE_GET_WORKFLOW_RUN_BY_ID_FAILED',
1473
- domain: ErrorDomain.STORAGE,
1474
- category: ErrorCategory.THIRD_PARTY,
1475
- details: {
1476
- runId,
1477
- workflowName: workflowName || '',
1478
- },
1479
- },
1480
- error,
1481
- );
1482
- }
360
+ return this.stores.workflows.getWorkflowRunById({ runId, workflowName });
1483
361
  }
1484
362
 
1485
363
  async close(): Promise<void> {
1486
364
  this.pgp.end();
1487
365
  }
1488
366
 
1489
- async getEvals(
1490
- options: {
1491
- agentName?: string;
1492
- type?: 'test' | 'live';
1493
- } & PaginationArgs = {},
1494
- ): Promise<PaginationInfo & { evals: EvalRow[] }> {
1495
- const { agentName, type, page = 0, perPage = 100, dateRange } = options;
1496
- const fromDate = dateRange?.start;
1497
- const toDate = dateRange?.end;
1498
-
1499
- const conditions: string[] = [];
1500
- const queryParams: any[] = [];
1501
- let paramIndex = 1;
1502
-
1503
- if (agentName) {
1504
- conditions.push(`agent_name = $${paramIndex++}`);
1505
- queryParams.push(agentName);
1506
- }
1507
-
1508
- if (type === 'test') {
1509
- conditions.push(`(test_info IS NOT NULL AND test_info->>'testPath' IS NOT NULL)`);
1510
- } else if (type === 'live') {
1511
- conditions.push(`(test_info IS NULL OR test_info->>'testPath' IS NULL)`);
1512
- }
1513
-
1514
- if (fromDate) {
1515
- conditions.push(`created_at >= $${paramIndex++}`);
1516
- queryParams.push(fromDate);
1517
- }
1518
-
1519
- if (toDate) {
1520
- conditions.push(`created_at <= $${paramIndex++}`);
1521
- queryParams.push(toDate);
1522
- }
1523
-
1524
- const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
1525
-
1526
- const countQuery = `SELECT COUNT(*) FROM ${this.getTableName(TABLE_EVALS)} ${whereClause}`;
1527
- try {
1528
- const countResult = await this.db.one(countQuery, queryParams);
1529
- const total = parseInt(countResult.count, 10);
1530
- const currentOffset = page * perPage;
1531
-
1532
- if (total === 0) {
1533
- return {
1534
- evals: [],
1535
- total: 0,
1536
- page,
1537
- perPage,
1538
- hasMore: false,
1539
- };
1540
- }
1541
-
1542
- const dataQuery = `SELECT * FROM ${this.getTableName(
1543
- TABLE_EVALS,
1544
- )} ${whereClause} ORDER BY created_at DESC LIMIT $${paramIndex++} OFFSET $${paramIndex++}`;
1545
- const rows = await this.db.manyOrNone(dataQuery, [...queryParams, perPage, currentOffset]);
1546
-
1547
- return {
1548
- evals: rows?.map(row => this.transformEvalRow(row)) ?? [],
1549
- total,
1550
- page,
1551
- perPage,
1552
- hasMore: currentOffset + (rows?.length ?? 0) < total,
1553
- };
1554
- } catch (error) {
1555
- const mastraError = new MastraError(
1556
- {
1557
- id: 'MASTRA_STORAGE_PG_STORE_GET_EVALS_FAILED',
1558
- domain: ErrorDomain.STORAGE,
1559
- category: ErrorCategory.THIRD_PARTY,
1560
- details: {
1561
- agentName: agentName || 'all',
1562
- type: type || 'all',
1563
- page,
1564
- perPage,
1565
- },
1566
- },
1567
- error,
1568
- );
1569
- this.logger?.error?.(mastraError.toString());
1570
- this.logger?.trackException(mastraError);
1571
- throw mastraError;
1572
- }
367
+ /**
368
+ * Scorers
369
+ */
370
+ async getScoreById({ id: _id }: { id: string }): Promise<ScoreRowData | null> {
371
+ return this.stores.scores.getScoreById({ id: _id });
1573
372
  }
1574
373
 
1575
- async updateMessages({
1576
- messages,
374
+ async getScoresByScorerId({
375
+ scorerId: _scorerId,
376
+ pagination: _pagination,
1577
377
  }: {
1578
- messages: (Partial<Omit<MastraMessageV2, 'createdAt'>> & {
1579
- id: string;
1580
- content?: {
1581
- metadata?: MastraMessageContentV2['metadata'];
1582
- content?: MastraMessageContentV2['content'];
1583
- };
1584
- })[];
1585
- }): Promise<MastraMessageV2[]> {
1586
- if (messages.length === 0) {
1587
- return [];
1588
- }
1589
-
1590
- const messageIds = messages.map(m => m.id);
1591
-
1592
- const selectQuery = `SELECT id, content, role, type, "createdAt", thread_id AS "threadId", "resourceId" FROM ${this.getTableName(
1593
- TABLE_MESSAGES,
1594
- )} WHERE id IN ($1:list)`;
1595
-
1596
- const existingMessagesDb = await this.db.manyOrNone(selectQuery, [messageIds]);
1597
-
1598
- if (existingMessagesDb.length === 0) {
1599
- return [];
1600
- }
1601
-
1602
- // Parse content from string to object for merging
1603
- const existingMessages: MastraMessageV2[] = existingMessagesDb.map(msg => {
1604
- if (typeof msg.content === 'string') {
1605
- try {
1606
- msg.content = JSON.parse(msg.content);
1607
- } catch {
1608
- // ignore if not valid json
1609
- }
1610
- }
1611
- return msg as MastraMessageV2;
1612
- });
1613
-
1614
- const threadIdsToUpdate = new Set<string>();
1615
-
1616
- await this.db.tx(async t => {
1617
- const queries = [];
1618
- const columnMapping: Record<string, string> = {
1619
- threadId: 'thread_id',
1620
- };
1621
-
1622
- for (const existingMessage of existingMessages) {
1623
- const updatePayload = messages.find(m => m.id === existingMessage.id);
1624
- if (!updatePayload) continue;
1625
-
1626
- const { id, ...fieldsToUpdate } = updatePayload;
1627
- if (Object.keys(fieldsToUpdate).length === 0) continue;
1628
-
1629
- threadIdsToUpdate.add(existingMessage.threadId!);
1630
- if (updatePayload.threadId && updatePayload.threadId !== existingMessage.threadId) {
1631
- threadIdsToUpdate.add(updatePayload.threadId);
1632
- }
1633
-
1634
- const setClauses: string[] = [];
1635
- const values: any[] = [];
1636
- let paramIndex = 1;
1637
-
1638
- const updatableFields = { ...fieldsToUpdate };
1639
-
1640
- // Special handling for content: merge in code, then update the whole field
1641
- if (updatableFields.content) {
1642
- const newContent = {
1643
- ...existingMessage.content,
1644
- ...updatableFields.content,
1645
- // Deep merge metadata if it exists on both
1646
- ...(existingMessage.content?.metadata && updatableFields.content.metadata
1647
- ? {
1648
- metadata: {
1649
- ...existingMessage.content.metadata,
1650
- ...updatableFields.content.metadata,
1651
- },
1652
- }
1653
- : {}),
1654
- };
1655
- setClauses.push(`content = $${paramIndex++}`);
1656
- values.push(newContent);
1657
- delete updatableFields.content;
1658
- }
1659
-
1660
- for (const key in updatableFields) {
1661
- if (Object.prototype.hasOwnProperty.call(updatableFields, key)) {
1662
- const dbColumn = columnMapping[key] || key;
1663
- setClauses.push(`"${dbColumn}" = $${paramIndex++}`);
1664
- values.push(updatableFields[key as keyof typeof updatableFields]);
1665
- }
1666
- }
1667
-
1668
- if (setClauses.length > 0) {
1669
- values.push(id);
1670
- const sql = `UPDATE ${this.getTableName(
1671
- TABLE_MESSAGES,
1672
- )} SET ${setClauses.join(', ')} WHERE id = $${paramIndex}`;
1673
- queries.push(t.none(sql, values));
1674
- }
1675
- }
1676
-
1677
- if (threadIdsToUpdate.size > 0) {
1678
- queries.push(
1679
- t.none(`UPDATE ${this.getTableName(TABLE_THREADS)} SET "updatedAt" = NOW() WHERE id IN ($1:list)`, [
1680
- Array.from(threadIdsToUpdate),
1681
- ]),
1682
- );
1683
- }
1684
-
1685
- if (queries.length > 0) {
1686
- await t.batch(queries);
1687
- }
1688
- });
1689
-
1690
- // Re-fetch to return the fully updated messages
1691
- const updatedMessages = await this.db.manyOrNone<MastraMessageV2>(selectQuery, [messageIds]);
1692
-
1693
- return (updatedMessages || []).map(message => {
1694
- if (typeof message.content === 'string') {
1695
- try {
1696
- message.content = JSON.parse(message.content);
1697
- } catch {
1698
- /* ignore */
1699
- }
1700
- }
1701
- return message;
1702
- });
378
+ scorerId: string;
379
+ pagination: StoragePagination;
380
+ }): Promise<{ pagination: PaginationInfo; scores: ScoreRowData[] }> {
381
+ return this.stores.scores.getScoresByScorerId({ scorerId: _scorerId, pagination: _pagination });
1703
382
  }
1704
383
 
1705
- async getResourceById({ resourceId }: { resourceId: string }): Promise<StorageResourceType | null> {
1706
- const tableName = this.getTableName(TABLE_RESOURCES);
1707
- const result = await this.db.oneOrNone<StorageResourceType>(`SELECT * FROM ${tableName} WHERE id = $1`, [
1708
- resourceId,
1709
- ]);
1710
-
1711
- if (!result) {
1712
- return null;
1713
- }
1714
-
1715
- return {
1716
- ...result,
1717
- // Ensure workingMemory is always returned as a string, regardless of automatic parsing
1718
- workingMemory:
1719
- typeof result.workingMemory === 'object' ? JSON.stringify(result.workingMemory) : result.workingMemory,
1720
- metadata: typeof result.metadata === 'string' ? JSON.parse(result.metadata) : result.metadata,
1721
- };
384
+ async saveScore(_score: ScoreRowData): Promise<{ score: ScoreRowData }> {
385
+ return this.stores.scores.saveScore(_score);
1722
386
  }
1723
387
 
1724
- async saveResource({ resource }: { resource: StorageResourceType }): Promise<StorageResourceType> {
1725
- const tableName = this.getTableName(TABLE_RESOURCES);
1726
- await this.db.none(
1727
- `INSERT INTO ${tableName} (id, "workingMemory", metadata, "createdAt", "updatedAt")
1728
- VALUES ($1, $2, $3, $4, $5)`,
1729
- [
1730
- resource.id,
1731
- resource.workingMemory,
1732
- JSON.stringify(resource.metadata),
1733
- resource.createdAt.toISOString(),
1734
- resource.updatedAt.toISOString(),
1735
- ],
1736
- );
1737
-
1738
- return resource;
388
+ async getScoresByRunId({
389
+ runId: _runId,
390
+ pagination: _pagination,
391
+ }: {
392
+ runId: string;
393
+ pagination: StoragePagination;
394
+ }): Promise<{ pagination: PaginationInfo; scores: ScoreRowData[] }> {
395
+ return this.stores.scores.getScoresByRunId({ runId: _runId, pagination: _pagination });
1739
396
  }
1740
397
 
1741
- async updateResource({
1742
- resourceId,
1743
- workingMemory,
1744
- metadata,
398
+ async getScoresByEntityId({
399
+ entityId: _entityId,
400
+ entityType: _entityType,
401
+ pagination: _pagination,
1745
402
  }: {
1746
- resourceId: string;
1747
- workingMemory?: string;
1748
- metadata?: Record<string, unknown>;
1749
- }): Promise<StorageResourceType> {
1750
- const existingResource = await this.getResourceById({ resourceId });
1751
-
1752
- if (!existingResource) {
1753
- // Create new resource if it doesn't exist
1754
- const newResource: StorageResourceType = {
1755
- id: resourceId,
1756
- workingMemory,
1757
- metadata: metadata || {},
1758
- createdAt: new Date(),
1759
- updatedAt: new Date(),
1760
- };
1761
- return this.saveResource({ resource: newResource });
1762
- }
1763
-
1764
- const updatedResource = {
1765
- ...existingResource,
1766
- workingMemory: workingMemory !== undefined ? workingMemory : existingResource.workingMemory,
1767
- metadata: {
1768
- ...existingResource.metadata,
1769
- ...metadata,
1770
- },
1771
- updatedAt: new Date(),
1772
- };
1773
-
1774
- const tableName = this.getTableName(TABLE_RESOURCES);
1775
- const updates: string[] = [];
1776
- const values: any[] = [];
1777
- let paramIndex = 1;
1778
-
1779
- if (workingMemory !== undefined) {
1780
- updates.push(`"workingMemory" = $${paramIndex}`);
1781
- values.push(workingMemory);
1782
- paramIndex++;
1783
- }
1784
-
1785
- if (metadata) {
1786
- updates.push(`metadata = $${paramIndex}`);
1787
- values.push(JSON.stringify(updatedResource.metadata));
1788
- paramIndex++;
1789
- }
1790
-
1791
- updates.push(`"updatedAt" = $${paramIndex}`);
1792
- values.push(updatedResource.updatedAt.toISOString());
1793
- paramIndex++;
1794
-
1795
- values.push(resourceId);
1796
-
1797
- await this.db.none(`UPDATE ${tableName} SET ${updates.join(', ')} WHERE id = $${paramIndex}`, values);
1798
-
1799
- return updatedResource;
403
+ pagination: StoragePagination;
404
+ entityId: string;
405
+ entityType: string;
406
+ }): Promise<{ pagination: PaginationInfo; scores: ScoreRowData[] }> {
407
+ return this.stores.scores.getScoresByEntityId({
408
+ entityId: _entityId,
409
+ entityType: _entityType,
410
+ pagination: _pagination,
411
+ });
1800
412
  }
1801
413
  }