@mastra/pg 0.12.3 → 0.12.4-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -5,9 +5,9 @@ import { Mutex } from 'async-mutex';
5
5
  import pg from 'pg';
6
6
  import xxhash from 'xxhash-wasm';
7
7
  import { BaseFilterTranslator } from '@mastra/core/vector/filter';
8
- import { MessageList } from '@mastra/core/agent';
9
- import { MastraStorage, TABLE_EVALS, TABLE_TRACES, TABLE_WORKFLOW_SNAPSHOT, TABLE_THREADS, TABLE_MESSAGES, TABLE_RESOURCES } from '@mastra/core/storage';
8
+ import { MastraStorage, StoreOperations, TABLE_WORKFLOW_SNAPSHOT, ScoresStorage, TABLE_SCORERS, TracesStorage, safelyParseJSON, TABLE_TRACES, WorkflowsStorage, LegacyEvalsStorage, MemoryStorage, resolveMessageLimit, TABLE_RESOURCES, TABLE_EVALS, TABLE_THREADS, TABLE_MESSAGES } from '@mastra/core/storage';
10
9
  import pgPromise from 'pg-promise';
10
+ import { MessageList } from '@mastra/core/agent';
11
11
 
12
12
  // src/vector/index.ts
13
13
  var PGFilterTranslator = class extends BaseFilterTranslator {
@@ -482,6 +482,7 @@ var PgVector = class extends MastraVector {
482
482
  }
483
483
  const client = await this.pool.connect();
484
484
  try {
485
+ await client.query("BEGIN");
485
486
  const vectorStr = `[${queryVector.join(",")}]`;
486
487
  const translatedFilter = this.transformFilter(filter);
487
488
  const { sql: filterQuery, values: filterValues } = buildFilterQuery(translatedFilter, minScore, topK);
@@ -511,6 +512,7 @@ var PgVector = class extends MastraVector {
511
512
  ORDER BY score DESC
512
513
  LIMIT $2`;
513
514
  const result = await client.query(query, filterValues);
515
+ await client.query("COMMIT");
514
516
  return result.rows.map(({ id, score, metadata, embedding }) => ({
515
517
  id,
516
518
  score,
@@ -518,6 +520,7 @@ var PgVector = class extends MastraVector {
518
520
  ...includeVector && embedding && { vector: JSON.parse(embedding) }
519
521
  }));
520
522
  } catch (error) {
523
+ await client.query("ROLLBACK");
521
524
  const mastraError = new MastraError(
522
525
  {
523
526
  id: "MASTRA_STORAGE_PG_VECTOR_QUERY_FAILED",
@@ -1096,77 +1099,55 @@ var PgVector = class extends MastraVector {
1096
1099
  }
1097
1100
  }
1098
1101
  };
1099
- var PostgresStore = class extends MastraStorage {
1100
- db;
1101
- pgp;
1102
- schema;
1103
- setupSchemaPromise = null;
1104
- schemaSetupComplete = void 0;
1105
- constructor(config) {
1102
+ function getSchemaName(schema) {
1103
+ return schema ? `"${parseSqlIdentifier(schema, "schema name")}"` : void 0;
1104
+ }
1105
+ function getTableName({ indexName, schemaName }) {
1106
+ const parsedIndexName = parseSqlIdentifier(indexName, "index name");
1107
+ const quotedIndexName = `"${parsedIndexName}"`;
1108
+ const quotedSchemaName = schemaName;
1109
+ return quotedSchemaName ? `${quotedSchemaName}.${quotedIndexName}` : quotedIndexName;
1110
+ }
1111
+
1112
+ // src/storage/domains/legacy-evals/index.ts
1113
+ function transformEvalRow(row) {
1114
+ let testInfoValue = null;
1115
+ if (row.test_info) {
1106
1116
  try {
1107
- if ("connectionString" in config) {
1108
- if (!config.connectionString || typeof config.connectionString !== "string" || config.connectionString.trim() === "") {
1109
- throw new Error(
1110
- "PostgresStore: connectionString must be provided and cannot be empty. Passing an empty string may cause fallback to local Postgres defaults."
1111
- );
1112
- }
1113
- } else {
1114
- const required = ["host", "database", "user", "password"];
1115
- for (const key of required) {
1116
- if (!(key in config) || typeof config[key] !== "string" || config[key].trim() === "") {
1117
- throw new Error(
1118
- `PostgresStore: ${key} must be provided and cannot be empty. Passing an empty string may cause fallback to local Postgres defaults.`
1119
- );
1120
- }
1121
- }
1122
- }
1123
- super({ name: "PostgresStore" });
1124
- this.pgp = pgPromise();
1125
- this.schema = config.schemaName;
1126
- this.db = this.pgp(
1127
- `connectionString` in config ? { connectionString: config.connectionString } : {
1128
- host: config.host,
1129
- port: config.port,
1130
- database: config.database,
1131
- user: config.user,
1132
- password: config.password,
1133
- ssl: config.ssl
1134
- }
1135
- );
1117
+ testInfoValue = typeof row.test_info === "string" ? JSON.parse(row.test_info) : row.test_info;
1136
1118
  } catch (e) {
1137
- throw new MastraError(
1138
- {
1139
- id: "MASTRA_STORAGE_PG_STORE_INITIALIZATION_FAILED",
1140
- domain: ErrorDomain.STORAGE,
1141
- category: ErrorCategory.USER
1142
- },
1143
- e
1144
- );
1119
+ console.warn("Failed to parse test_info:", e);
1145
1120
  }
1146
1121
  }
1147
- get supports() {
1148
- return {
1149
- selectByIncludeResourceScope: true,
1150
- resourceWorkingMemory: true
1151
- };
1152
- }
1153
- getTableName(indexName) {
1154
- const parsedIndexName = parseSqlIdentifier(indexName, "index name");
1155
- const quotedIndexName = `"${parsedIndexName}"`;
1156
- const quotedSchemaName = this.getSchemaName();
1157
- return quotedSchemaName ? `${quotedSchemaName}.${quotedIndexName}` : quotedIndexName;
1158
- }
1159
- getSchemaName() {
1160
- return this.schema ? `"${parseSqlIdentifier(this.schema, "schema name")}"` : void 0;
1122
+ return {
1123
+ agentName: row.agent_name,
1124
+ input: row.input,
1125
+ output: row.output,
1126
+ result: row.result,
1127
+ metricName: row.metric_name,
1128
+ instructions: row.instructions,
1129
+ testInfo: testInfoValue,
1130
+ globalRunId: row.global_run_id,
1131
+ runId: row.run_id,
1132
+ createdAt: row.created_atZ || row.created_at
1133
+ };
1134
+ }
1135
+ var LegacyEvalsPG = class extends LegacyEvalsStorage {
1136
+ client;
1137
+ schema;
1138
+ constructor({ client, schema }) {
1139
+ super();
1140
+ this.client = client;
1141
+ this.schema = schema;
1161
1142
  }
1162
1143
  /** @deprecated use getEvals instead */
1163
1144
  async getEvalsByAgentName(agentName, type) {
1164
1145
  try {
1165
- const baseQuery = `SELECT * FROM ${this.getTableName(TABLE_EVALS)} WHERE agent_name = $1`;
1146
+ const baseQuery = `SELECT * FROM ${getTableName({ indexName: TABLE_EVALS, schemaName: getSchemaName(this.schema) })} WHERE agent_name = $1`;
1166
1147
  const typeCondition = type === "test" ? " AND test_info IS NOT NULL AND test_info->>'testPath' IS NOT NULL" : type === "live" ? " AND (test_info IS NULL OR test_info->>'testPath' IS NULL)" : "";
1167
1148
  const query = `${baseQuery}${typeCondition} ORDER BY created_at DESC`;
1168
- const rows = await this.db.manyOrNone(query, [agentName]);
1169
- return rows?.map((row) => this.transformEvalRow(row)) ?? [];
1149
+ const rows = await this.client.manyOrNone(query, [agentName]);
1150
+ return rows?.map((row) => transformEvalRow(row)) ?? [];
1170
1151
  } catch (error) {
1171
1152
  if (error instanceof Error && error.message.includes("relation") && error.message.includes("does not exist")) {
1172
1153
  return [];
@@ -1175,486 +1156,522 @@ var PostgresStore = class extends MastraStorage {
1175
1156
  throw error;
1176
1157
  }
1177
1158
  }
1178
- transformEvalRow(row) {
1179
- let testInfoValue = null;
1180
- if (row.test_info) {
1181
- try {
1182
- testInfoValue = typeof row.test_info === "string" ? JSON.parse(row.test_info) : row.test_info;
1183
- } catch (e) {
1184
- console.warn("Failed to parse test_info:", e);
1185
- }
1159
+ async getEvals(options = {}) {
1160
+ const tableName = getTableName({ indexName: TABLE_EVALS, schemaName: getSchemaName(this.schema) });
1161
+ const { agentName, type, page = 0, perPage = 100, dateRange } = options;
1162
+ const fromDate = dateRange?.start;
1163
+ const toDate = dateRange?.end;
1164
+ const conditions = [];
1165
+ const queryParams = [];
1166
+ let paramIndex = 1;
1167
+ if (agentName) {
1168
+ conditions.push(`agent_name = $${paramIndex++}`);
1169
+ queryParams.push(agentName);
1186
1170
  }
1187
- return {
1188
- agentName: row.agent_name,
1189
- input: row.input,
1190
- output: row.output,
1191
- result: row.result,
1192
- metricName: row.metric_name,
1193
- instructions: row.instructions,
1194
- testInfo: testInfoValue,
1195
- globalRunId: row.global_run_id,
1196
- runId: row.run_id,
1197
- createdAt: row.created_at
1198
- };
1199
- }
1200
- async batchInsert({ tableName, records }) {
1171
+ if (type === "test") {
1172
+ conditions.push(`(test_info IS NOT NULL AND test_info->>'testPath' IS NOT NULL)`);
1173
+ } else if (type === "live") {
1174
+ conditions.push(`(test_info IS NULL OR test_info->>'testPath' IS NULL)`);
1175
+ }
1176
+ if (fromDate) {
1177
+ conditions.push(`created_at >= $${paramIndex++}`);
1178
+ queryParams.push(fromDate);
1179
+ }
1180
+ if (toDate) {
1181
+ conditions.push(`created_at <= $${paramIndex++}`);
1182
+ queryParams.push(toDate);
1183
+ }
1184
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
1185
+ const countQuery = `SELECT COUNT(*) FROM ${tableName} ${whereClause}`;
1201
1186
  try {
1202
- await this.db.query("BEGIN");
1203
- for (const record of records) {
1204
- await this.insert({ tableName, record });
1187
+ const countResult = await this.client.one(countQuery, queryParams);
1188
+ const total = parseInt(countResult.count, 10);
1189
+ const currentOffset = page * perPage;
1190
+ if (total === 0) {
1191
+ return {
1192
+ evals: [],
1193
+ total: 0,
1194
+ page,
1195
+ perPage,
1196
+ hasMore: false
1197
+ };
1205
1198
  }
1206
- await this.db.query("COMMIT");
1199
+ const dataQuery = `SELECT * FROM ${tableName} ${whereClause} ORDER BY created_at DESC LIMIT $${paramIndex++} OFFSET $${paramIndex++}`;
1200
+ const rows = await this.client.manyOrNone(dataQuery, [...queryParams, perPage, currentOffset]);
1201
+ return {
1202
+ evals: rows?.map((row) => transformEvalRow(row)) ?? [],
1203
+ total,
1204
+ page,
1205
+ perPage,
1206
+ hasMore: currentOffset + (rows?.length ?? 0) < total
1207
+ };
1207
1208
  } catch (error) {
1208
- await this.db.query("ROLLBACK");
1209
- throw new MastraError(
1209
+ const mastraError = new MastraError(
1210
1210
  {
1211
- id: "MASTRA_STORAGE_PG_STORE_BATCH_INSERT_FAILED",
1211
+ id: "MASTRA_STORAGE_PG_STORE_GET_EVALS_FAILED",
1212
1212
  domain: ErrorDomain.STORAGE,
1213
1213
  category: ErrorCategory.THIRD_PARTY,
1214
1214
  details: {
1215
- tableName,
1216
- numberOfRecords: records.length
1215
+ agentName: agentName || "all",
1216
+ type: type || "all",
1217
+ page,
1218
+ perPage
1217
1219
  }
1218
1220
  },
1219
1221
  error
1220
1222
  );
1223
+ this.logger?.error?.(mastraError.toString());
1224
+ this.logger?.trackException(mastraError);
1225
+ throw mastraError;
1221
1226
  }
1222
1227
  }
1223
- /**
1224
- * @deprecated use getTracesPaginated instead
1225
- */
1226
- async getTraces(args) {
1227
- if (args.fromDate || args.toDate) {
1228
- args.dateRange = {
1229
- start: args.fromDate,
1230
- end: args.toDate
1231
- };
1232
- }
1233
- const result = await this.getTracesPaginated(args);
1234
- return result.traces;
1228
+ };
1229
+ var MemoryPG = class extends MemoryStorage {
1230
+ client;
1231
+ schema;
1232
+ operations;
1233
+ constructor({
1234
+ client,
1235
+ schema,
1236
+ operations
1237
+ }) {
1238
+ super();
1239
+ this.client = client;
1240
+ this.schema = schema;
1241
+ this.operations = operations;
1235
1242
  }
1236
- async getTracesPaginated(args) {
1237
- const { name, scope, page = 0, perPage: perPageInput, attributes, filters, dateRange } = args;
1238
- const fromDate = dateRange?.start;
1239
- const toDate = dateRange?.end;
1240
- const perPage = perPageInput !== void 0 ? perPageInput : 100;
1241
- const currentOffset = page * perPage;
1242
- const queryParams = [];
1243
- const conditions = [];
1244
- let paramIndex = 1;
1245
- if (name) {
1246
- conditions.push(`name LIKE $${paramIndex++}`);
1247
- queryParams.push(`${name}%`);
1248
- }
1249
- if (scope) {
1250
- conditions.push(`scope = $${paramIndex++}`);
1251
- queryParams.push(scope);
1252
- }
1253
- if (attributes) {
1254
- Object.entries(attributes).forEach(([key, value]) => {
1255
- const parsedKey = parseFieldKey(key);
1256
- conditions.push(`attributes->>'${parsedKey}' = $${paramIndex++}`);
1257
- queryParams.push(value);
1258
- });
1259
- }
1260
- if (filters) {
1261
- Object.entries(filters).forEach(([key, value]) => {
1262
- const parsedKey = parseFieldKey(key);
1263
- conditions.push(`"${parsedKey}" = $${paramIndex++}`);
1264
- queryParams.push(value);
1265
- });
1266
- }
1267
- if (fromDate) {
1268
- conditions.push(`"createdAt" >= $${paramIndex++}`);
1269
- queryParams.push(fromDate);
1270
- }
1271
- if (toDate) {
1272
- conditions.push(`"createdAt" <= $${paramIndex++}`);
1273
- queryParams.push(toDate);
1274
- }
1275
- const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
1276
- const countQuery = `SELECT COUNT(*) FROM ${this.getTableName(TABLE_TRACES)} ${whereClause}`;
1277
- let total = 0;
1243
+ async getThreadById({ threadId }) {
1278
1244
  try {
1279
- const countResult = await this.db.one(countQuery, queryParams);
1280
- total = parseInt(countResult.count, 10);
1245
+ const tableName = getTableName({ indexName: TABLE_THREADS, schemaName: getSchemaName(this.schema) });
1246
+ const thread = await this.client.oneOrNone(
1247
+ `SELECT * FROM ${tableName} WHERE id = $1`,
1248
+ [threadId]
1249
+ );
1250
+ if (!thread) {
1251
+ return null;
1252
+ }
1253
+ return {
1254
+ id: thread.id,
1255
+ resourceId: thread.resourceId,
1256
+ title: thread.title,
1257
+ metadata: typeof thread.metadata === "string" ? JSON.parse(thread.metadata) : thread.metadata,
1258
+ createdAt: thread.createdAtZ || thread.createdAt,
1259
+ updatedAt: thread.updatedAtZ || thread.updatedAt
1260
+ };
1281
1261
  } catch (error) {
1282
1262
  throw new MastraError(
1283
1263
  {
1284
- id: "MASTRA_STORAGE_PG_STORE_GET_TRACES_PAGINATED_FAILED_TO_RETRIEVE_TOTAL_COUNT",
1264
+ id: "MASTRA_STORAGE_PG_STORE_GET_THREAD_BY_ID_FAILED",
1285
1265
  domain: ErrorDomain.STORAGE,
1286
1266
  category: ErrorCategory.THIRD_PARTY,
1287
1267
  details: {
1288
- name: args.name ?? "",
1289
- scope: args.scope ?? ""
1268
+ threadId
1290
1269
  }
1291
1270
  },
1292
1271
  error
1293
1272
  );
1294
1273
  }
1295
- if (total === 0) {
1296
- return {
1297
- traces: [],
1298
- total: 0,
1299
- page,
1300
- perPage,
1301
- hasMore: false
1302
- };
1274
+ }
1275
+ /**
1276
+ * @deprecated use getThreadsByResourceIdPaginated instead
1277
+ */
1278
+ async getThreadsByResourceId(args) {
1279
+ const { resourceId } = args;
1280
+ try {
1281
+ const tableName = getTableName({ indexName: TABLE_THREADS, schemaName: getSchemaName(this.schema) });
1282
+ const baseQuery = `FROM ${tableName} WHERE "resourceId" = $1`;
1283
+ const queryParams = [resourceId];
1284
+ const dataQuery = `SELECT id, "resourceId", title, metadata, "createdAt", "updatedAt" ${baseQuery} ORDER BY "createdAt" DESC`;
1285
+ const rows = await this.client.manyOrNone(dataQuery, queryParams);
1286
+ return (rows || []).map((thread) => ({
1287
+ ...thread,
1288
+ metadata: typeof thread.metadata === "string" ? JSON.parse(thread.metadata) : thread.metadata,
1289
+ createdAt: thread.createdAt,
1290
+ updatedAt: thread.updatedAt
1291
+ }));
1292
+ } catch (error) {
1293
+ this.logger.error(`Error getting threads for resource ${resourceId}:`, error);
1294
+ return [];
1303
1295
  }
1304
- const dataQuery = `SELECT * FROM ${this.getTableName(
1305
- TABLE_TRACES
1306
- )} ${whereClause} ORDER BY "createdAt" DESC LIMIT $${paramIndex++} OFFSET $${paramIndex++}`;
1307
- const finalQueryParams = [...queryParams, perPage, currentOffset];
1296
+ }
1297
+ async getThreadsByResourceIdPaginated(args) {
1298
+ const { resourceId, page = 0, perPage: perPageInput } = args;
1308
1299
  try {
1309
- const rows = await this.db.manyOrNone(dataQuery, finalQueryParams);
1310
- const traces = rows.map((row) => ({
1311
- id: row.id,
1312
- parentSpanId: row.parentSpanId,
1313
- traceId: row.traceId,
1314
- name: row.name,
1315
- scope: row.scope,
1316
- kind: row.kind,
1317
- status: row.status,
1318
- events: row.events,
1319
- links: row.links,
1320
- attributes: row.attributes,
1321
- startTime: row.startTime,
1322
- endTime: row.endTime,
1323
- other: row.other,
1324
- createdAt: row.createdAt
1300
+ const tableName = getTableName({ indexName: TABLE_THREADS, schemaName: getSchemaName(this.schema) });
1301
+ const baseQuery = `FROM ${tableName} WHERE "resourceId" = $1`;
1302
+ const queryParams = [resourceId];
1303
+ const perPage = perPageInput !== void 0 ? perPageInput : 100;
1304
+ const currentOffset = page * perPage;
1305
+ const countQuery = `SELECT COUNT(*) ${baseQuery}`;
1306
+ const countResult = await this.client.one(countQuery, queryParams);
1307
+ const total = parseInt(countResult.count, 10);
1308
+ if (total === 0) {
1309
+ return {
1310
+ threads: [],
1311
+ total: 0,
1312
+ page,
1313
+ perPage,
1314
+ hasMore: false
1315
+ };
1316
+ }
1317
+ const dataQuery = `SELECT id, "resourceId", title, metadata, "createdAt", "updatedAt" ${baseQuery} ORDER BY "createdAt" DESC LIMIT $2 OFFSET $3`;
1318
+ const rows = await this.client.manyOrNone(dataQuery, [...queryParams, perPage, currentOffset]);
1319
+ const threads = (rows || []).map((thread) => ({
1320
+ ...thread,
1321
+ metadata: typeof thread.metadata === "string" ? JSON.parse(thread.metadata) : thread.metadata,
1322
+ createdAt: thread.createdAt,
1323
+ // Assuming already Date objects or ISO strings
1324
+ updatedAt: thread.updatedAt
1325
1325
  }));
1326
1326
  return {
1327
- traces,
1327
+ threads,
1328
1328
  total,
1329
1329
  page,
1330
1330
  perPage,
1331
- hasMore: currentOffset + traces.length < total
1331
+ hasMore: currentOffset + threads.length < total
1332
1332
  };
1333
1333
  } catch (error) {
1334
- throw new MastraError(
1334
+ const mastraError = new MastraError(
1335
1335
  {
1336
- id: "MASTRA_STORAGE_PG_STORE_GET_TRACES_PAGINATED_FAILED_TO_RETRIEVE_TRACES",
1336
+ id: "MASTRA_STORAGE_PG_STORE_GET_THREADS_BY_RESOURCE_ID_PAGINATED_FAILED",
1337
1337
  domain: ErrorDomain.STORAGE,
1338
1338
  category: ErrorCategory.THIRD_PARTY,
1339
1339
  details: {
1340
- name: args.name ?? "",
1341
- scope: args.scope ?? ""
1340
+ resourceId,
1341
+ page
1342
1342
  }
1343
1343
  },
1344
1344
  error
1345
1345
  );
1346
+ this.logger?.error?.(mastraError.toString());
1347
+ this.logger?.trackException(mastraError);
1348
+ return { threads: [], total: 0, page, perPage: perPageInput || 100, hasMore: false };
1346
1349
  }
1347
1350
  }
1348
- async setupSchema() {
1349
- if (!this.schema || this.schemaSetupComplete) {
1350
- return;
1351
- }
1352
- if (!this.setupSchemaPromise) {
1353
- this.setupSchemaPromise = (async () => {
1354
- try {
1355
- const schemaExists = await this.db.oneOrNone(
1356
- `
1357
- SELECT EXISTS (
1358
- SELECT 1 FROM information_schema.schemata
1359
- WHERE schema_name = $1
1360
- )
1361
- `,
1362
- [this.schema]
1363
- );
1364
- if (!schemaExists?.exists) {
1365
- try {
1366
- await this.db.none(`CREATE SCHEMA IF NOT EXISTS ${this.getSchemaName()}`);
1367
- this.logger.info(`Schema "${this.schema}" created successfully`);
1368
- } catch (error) {
1369
- this.logger.error(`Failed to create schema "${this.schema}"`, { error });
1370
- throw new Error(
1371
- `Unable to create schema "${this.schema}". This requires CREATE privilege on the database. Either create the schema manually or grant CREATE privilege to the user.`
1372
- );
1373
- }
1374
- }
1375
- this.schemaSetupComplete = true;
1376
- this.logger.debug(`Schema "${this.schema}" is ready for use`);
1377
- } catch (error) {
1378
- this.schemaSetupComplete = void 0;
1379
- this.setupSchemaPromise = null;
1380
- throw error;
1381
- } finally {
1382
- this.setupSchemaPromise = null;
1383
- }
1384
- })();
1385
- }
1386
- await this.setupSchemaPromise;
1387
- }
1388
- async createTable({
1389
- tableName,
1390
- schema
1391
- }) {
1351
+ async saveThread({ thread }) {
1392
1352
  try {
1393
- const columns = Object.entries(schema).map(([name, def]) => {
1394
- const parsedName = parseSqlIdentifier(name, "column name");
1395
- const constraints = [];
1396
- if (def.primaryKey) constraints.push("PRIMARY KEY");
1397
- if (!def.nullable) constraints.push("NOT NULL");
1398
- return `"${parsedName}" ${def.type.toUpperCase()} ${constraints.join(" ")}`;
1399
- }).join(",\n");
1400
- if (this.schema) {
1401
- await this.setupSchema();
1402
- }
1403
- const sql = `
1404
- CREATE TABLE IF NOT EXISTS ${this.getTableName(tableName)} (
1405
- ${columns}
1406
- );
1407
- ${tableName === TABLE_WORKFLOW_SNAPSHOT ? `
1408
- DO $$ BEGIN
1409
- IF NOT EXISTS (
1410
- SELECT 1 FROM pg_constraint WHERE conname = 'mastra_workflow_snapshot_workflow_name_run_id_key'
1411
- ) THEN
1412
- ALTER TABLE ${this.getTableName(tableName)}
1413
- ADD CONSTRAINT mastra_workflow_snapshot_workflow_name_run_id_key
1414
- UNIQUE (workflow_name, run_id);
1415
- END IF;
1416
- END $$;
1417
- ` : ""}
1418
- `;
1419
- await this.db.none(sql);
1353
+ const tableName = getTableName({ indexName: TABLE_THREADS, schemaName: getSchemaName(this.schema) });
1354
+ await this.client.none(
1355
+ `INSERT INTO ${tableName} (
1356
+ id,
1357
+ "resourceId",
1358
+ title,
1359
+ metadata,
1360
+ "createdAt",
1361
+ "createdAtZ",
1362
+ "updatedAt",
1363
+ "updatedAtZ"
1364
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
1365
+ ON CONFLICT (id) DO UPDATE SET
1366
+ "resourceId" = EXCLUDED."resourceId",
1367
+ title = EXCLUDED.title,
1368
+ metadata = EXCLUDED.metadata,
1369
+ "createdAt" = EXCLUDED."createdAt",
1370
+ "createdAtZ" = EXCLUDED."createdAtZ",
1371
+ "updatedAt" = EXCLUDED."updatedAt",
1372
+ "updatedAtZ" = EXCLUDED."updatedAtZ"`,
1373
+ [
1374
+ thread.id,
1375
+ thread.resourceId,
1376
+ thread.title,
1377
+ thread.metadata ? JSON.stringify(thread.metadata) : null,
1378
+ thread.createdAt,
1379
+ thread.createdAt,
1380
+ thread.updatedAt,
1381
+ thread.updatedAt
1382
+ ]
1383
+ );
1384
+ return thread;
1420
1385
  } catch (error) {
1421
1386
  throw new MastraError(
1422
1387
  {
1423
- id: "MASTRA_STORAGE_PG_STORE_CREATE_TABLE_FAILED",
1388
+ id: "MASTRA_STORAGE_PG_STORE_SAVE_THREAD_FAILED",
1424
1389
  domain: ErrorDomain.STORAGE,
1425
1390
  category: ErrorCategory.THIRD_PARTY,
1426
1391
  details: {
1427
- tableName
1392
+ threadId: thread.id
1428
1393
  }
1429
1394
  },
1430
1395
  error
1431
1396
  );
1432
1397
  }
1433
1398
  }
1434
- getDefaultValue(type) {
1435
- switch (type) {
1436
- case "timestamp":
1437
- return "DEFAULT NOW()";
1438
- case "jsonb":
1439
- return "DEFAULT '{}'::jsonb";
1440
- default:
1441
- return super.getDefaultValue(type);
1442
- }
1443
- }
1444
- /**
1445
- * Alters table schema to add columns if they don't exist
1446
- * @param tableName Name of the table
1447
- * @param schema Schema of the table
1448
- * @param ifNotExists Array of column names to add if they don't exist
1449
- */
1450
- async alterTable({
1451
- tableName,
1452
- schema,
1453
- ifNotExists
1399
+ async updateThread({
1400
+ id,
1401
+ title,
1402
+ metadata
1454
1403
  }) {
1455
- const fullTableName = this.getTableName(tableName);
1456
- try {
1457
- for (const columnName of ifNotExists) {
1458
- if (schema[columnName]) {
1459
- const columnDef = schema[columnName];
1460
- const sqlType = this.getSqlType(columnDef.type);
1461
- const nullable = columnDef.nullable === false ? "NOT NULL" : "";
1462
- const defaultValue = columnDef.nullable === false ? this.getDefaultValue(columnDef.type) : "";
1463
- const parsedColumnName = parseSqlIdentifier(columnName, "column name");
1464
- const alterSql = `ALTER TABLE ${fullTableName} ADD COLUMN IF NOT EXISTS "${parsedColumnName}" ${sqlType} ${nullable} ${defaultValue}`.trim();
1465
- await this.db.none(alterSql);
1466
- this.logger?.debug?.(`Ensured column ${parsedColumnName} exists in table ${fullTableName}`);
1404
+ const threadTableName = getTableName({ indexName: TABLE_THREADS, schemaName: getSchemaName(this.schema) });
1405
+ const existingThread = await this.getThreadById({ threadId: id });
1406
+ if (!existingThread) {
1407
+ throw new MastraError({
1408
+ id: "MASTRA_STORAGE_PG_STORE_UPDATE_THREAD_FAILED",
1409
+ domain: ErrorDomain.STORAGE,
1410
+ category: ErrorCategory.USER,
1411
+ text: `Thread ${id} not found`,
1412
+ details: {
1413
+ threadId: id,
1414
+ title
1467
1415
  }
1468
- }
1416
+ });
1417
+ }
1418
+ const mergedMetadata = {
1419
+ ...existingThread.metadata,
1420
+ ...metadata
1421
+ };
1422
+ try {
1423
+ const thread = await this.client.one(
1424
+ `UPDATE ${threadTableName}
1425
+ SET
1426
+ title = $1,
1427
+ metadata = $2,
1428
+ "updatedAt" = $3,
1429
+ "updatedAtZ" = $3
1430
+ WHERE id = $4
1431
+ RETURNING *
1432
+ `,
1433
+ [title, mergedMetadata, (/* @__PURE__ */ new Date()).toISOString(), id]
1434
+ );
1435
+ return {
1436
+ id: thread.id,
1437
+ resourceId: thread.resourceId,
1438
+ title: thread.title,
1439
+ metadata: typeof thread.metadata === "string" ? JSON.parse(thread.metadata) : thread.metadata,
1440
+ createdAt: thread.createdAtZ || thread.createdAt,
1441
+ updatedAt: thread.updatedAtZ || thread.updatedAt
1442
+ };
1469
1443
  } catch (error) {
1470
1444
  throw new MastraError(
1471
1445
  {
1472
- id: "MASTRA_STORAGE_PG_STORE_ALTER_TABLE_FAILED",
1446
+ id: "MASTRA_STORAGE_PG_STORE_UPDATE_THREAD_FAILED",
1473
1447
  domain: ErrorDomain.STORAGE,
1474
1448
  category: ErrorCategory.THIRD_PARTY,
1475
1449
  details: {
1476
- tableName
1450
+ threadId: id,
1451
+ title
1477
1452
  }
1478
1453
  },
1479
1454
  error
1480
1455
  );
1481
1456
  }
1482
1457
  }
1483
- async clearTable({ tableName }) {
1458
+ async deleteThread({ threadId }) {
1484
1459
  try {
1485
- await this.db.none(`TRUNCATE TABLE ${this.getTableName(tableName)} CASCADE`);
1460
+ const tableName = getTableName({ indexName: TABLE_MESSAGES, schemaName: getSchemaName(this.schema) });
1461
+ const threadTableName = getTableName({ indexName: TABLE_THREADS, schemaName: getSchemaName(this.schema) });
1462
+ await this.client.tx(async (t) => {
1463
+ await t.none(`DELETE FROM ${tableName} WHERE thread_id = $1`, [threadId]);
1464
+ await t.none(`DELETE FROM ${threadTableName} WHERE id = $1`, [threadId]);
1465
+ });
1486
1466
  } catch (error) {
1487
1467
  throw new MastraError(
1488
1468
  {
1489
- id: "MASTRA_STORAGE_PG_STORE_CLEAR_TABLE_FAILED",
1469
+ id: "MASTRA_STORAGE_PG_STORE_DELETE_THREAD_FAILED",
1490
1470
  domain: ErrorDomain.STORAGE,
1491
1471
  category: ErrorCategory.THIRD_PARTY,
1492
1472
  details: {
1493
- tableName
1473
+ threadId
1494
1474
  }
1495
1475
  },
1496
1476
  error
1497
1477
  );
1498
1478
  }
1499
1479
  }
1500
- async insert({ tableName, record }) {
1480
+ async _getIncludedMessages({
1481
+ threadId,
1482
+ selectBy,
1483
+ orderByStatement
1484
+ }) {
1485
+ const include = selectBy?.include;
1486
+ if (!include) return null;
1487
+ const unionQueries = [];
1488
+ const params = [];
1489
+ let paramIdx = 1;
1490
+ const tableName = getTableName({ indexName: TABLE_MESSAGES, schemaName: getSchemaName(this.schema) });
1491
+ for (const inc of include) {
1492
+ const { id, withPreviousMessages = 0, withNextMessages = 0 } = inc;
1493
+ const searchId = inc.threadId || threadId;
1494
+ unionQueries.push(
1495
+ `
1496
+ SELECT * FROM (
1497
+ WITH ordered_messages AS (
1498
+ SELECT
1499
+ *,
1500
+ ROW_NUMBER() OVER (${orderByStatement}) as row_num
1501
+ FROM ${tableName}
1502
+ WHERE thread_id = $${paramIdx}
1503
+ )
1504
+ SELECT
1505
+ m.id,
1506
+ m.content,
1507
+ m.role,
1508
+ m.type,
1509
+ m."createdAt",
1510
+ m.thread_id AS "threadId",
1511
+ m."resourceId"
1512
+ FROM ordered_messages m
1513
+ WHERE m.id = $${paramIdx + 1}
1514
+ OR EXISTS (
1515
+ SELECT 1 FROM ordered_messages target
1516
+ WHERE target.id = $${paramIdx + 1}
1517
+ AND (
1518
+ -- Get previous messages based on the max withPreviousMessages
1519
+ (m.row_num <= target.row_num + $${paramIdx + 2} AND m.row_num > target.row_num)
1520
+ OR
1521
+ -- Get next messages based on the max withNextMessages
1522
+ (m.row_num >= target.row_num - $${paramIdx + 3} AND m.row_num < target.row_num)
1523
+ )
1524
+ )
1525
+ ) AS query_${paramIdx}
1526
+ `
1527
+ // Keep ASC for final sorting after fetching context
1528
+ );
1529
+ params.push(searchId, id, withPreviousMessages, withNextMessages);
1530
+ paramIdx += 4;
1531
+ }
1532
+ const finalQuery = unionQueries.join(" UNION ALL ") + ' ORDER BY "createdAt" ASC';
1533
+ const includedRows = await this.client.manyOrNone(finalQuery, params);
1534
+ const seen = /* @__PURE__ */ new Set();
1535
+ const dedupedRows = includedRows.filter((row) => {
1536
+ if (seen.has(row.id)) return false;
1537
+ seen.add(row.id);
1538
+ return true;
1539
+ });
1540
+ return dedupedRows;
1541
+ }
1542
+ async getMessages(args) {
1543
+ const { threadId, format, selectBy } = args;
1544
+ const selectStatement = `SELECT id, content, role, type, "createdAt", thread_id AS "threadId", "resourceId"`;
1545
+ const orderByStatement = `ORDER BY "createdAt" DESC`;
1546
+ const limit = resolveMessageLimit({ last: selectBy?.last, defaultLimit: 40 });
1501
1547
  try {
1502
- const columns = Object.keys(record).map((col) => parseSqlIdentifier(col, "column name"));
1503
- const values = Object.values(record);
1504
- const placeholders = values.map((_, i) => `$${i + 1}`).join(", ");
1505
- await this.db.none(
1506
- `INSERT INTO ${this.getTableName(tableName)} (${columns.map((c) => `"${c}"`).join(", ")}) VALUES (${placeholders})`,
1507
- values
1548
+ let rows = [];
1549
+ const include = selectBy?.include || [];
1550
+ if (include?.length) {
1551
+ const includeMessages = await this._getIncludedMessages({ threadId, selectBy, orderByStatement });
1552
+ if (includeMessages) {
1553
+ rows.push(...includeMessages);
1554
+ }
1555
+ }
1556
+ const excludeIds = rows.map((m) => m.id);
1557
+ const tableName = getTableName({ indexName: TABLE_MESSAGES, schemaName: getSchemaName(this.schema) });
1558
+ const excludeIdsParam = excludeIds.map((_, idx) => `$${idx + 2}`).join(", ");
1559
+ let query = `${selectStatement} FROM ${tableName} WHERE thread_id = $1
1560
+ ${excludeIds.length ? `AND id NOT IN (${excludeIdsParam})` : ""}
1561
+ ${orderByStatement}
1562
+ LIMIT $${excludeIds.length + 2}
1563
+ `;
1564
+ const queryParams = [threadId, ...excludeIds, limit];
1565
+ const remainingRows = await this.client.manyOrNone(query, queryParams);
1566
+ rows.push(...remainingRows);
1567
+ const fetchedMessages = (rows || []).map((message) => {
1568
+ if (typeof message.content === "string") {
1569
+ try {
1570
+ message.content = JSON.parse(message.content);
1571
+ } catch {
1572
+ }
1573
+ }
1574
+ if (message.type === "v2") delete message.type;
1575
+ return message;
1576
+ });
1577
+ const sortedMessages = fetchedMessages.sort(
1578
+ (a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
1508
1579
  );
1580
+ return format === "v2" ? sortedMessages.map(
1581
+ (m) => ({ ...m, content: m.content || { format: 2, parts: [{ type: "text", text: "" }] } })
1582
+ ) : sortedMessages;
1509
1583
  } catch (error) {
1510
- throw new MastraError(
1584
+ const mastraError = new MastraError(
1511
1585
  {
1512
- id: "MASTRA_STORAGE_PG_STORE_INSERT_FAILED",
1586
+ id: "MASTRA_STORAGE_PG_STORE_GET_MESSAGES_FAILED",
1513
1587
  domain: ErrorDomain.STORAGE,
1514
1588
  category: ErrorCategory.THIRD_PARTY,
1515
1589
  details: {
1516
- tableName
1517
- }
1518
- },
1519
- error
1520
- );
1521
- }
1522
- }
1523
- async load({ tableName, keys }) {
1524
- try {
1525
- const keyEntries = Object.entries(keys).map(([key, value]) => [parseSqlIdentifier(key, "column name"), value]);
1526
- const conditions = keyEntries.map(([key], index) => `"${key}" = $${index + 1}`).join(" AND ");
1527
- const values = keyEntries.map(([_, value]) => value);
1528
- const result = await this.db.oneOrNone(
1529
- `SELECT * FROM ${this.getTableName(tableName)} WHERE ${conditions}`,
1530
- values
1531
- );
1532
- if (!result) {
1533
- return null;
1534
- }
1535
- if (tableName === TABLE_WORKFLOW_SNAPSHOT) {
1536
- const snapshot = result;
1537
- if (typeof snapshot.snapshot === "string") {
1538
- snapshot.snapshot = JSON.parse(snapshot.snapshot);
1539
- }
1540
- return snapshot;
1541
- }
1542
- return result;
1543
- } catch (error) {
1544
- throw new MastraError(
1545
- {
1546
- id: "MASTRA_STORAGE_PG_STORE_LOAD_FAILED",
1547
- domain: ErrorDomain.STORAGE,
1548
- category: ErrorCategory.THIRD_PARTY,
1549
- details: {
1550
- tableName
1590
+ threadId
1551
1591
  }
1552
1592
  },
1553
1593
  error
1554
1594
  );
1595
+ this.logger?.error?.(mastraError.toString());
1596
+ this.logger?.trackException(mastraError);
1597
+ return [];
1555
1598
  }
1556
1599
  }
1557
- async getThreadById({ threadId }) {
1558
- try {
1559
- const thread = await this.db.oneOrNone(
1560
- `SELECT
1561
- id,
1562
- "resourceId",
1563
- title,
1564
- metadata,
1565
- "createdAt",
1566
- "updatedAt"
1567
- FROM ${this.getTableName(TABLE_THREADS)}
1568
- WHERE id = $1`,
1569
- [threadId]
1570
- );
1571
- if (!thread) {
1572
- return null;
1600
+ async getMessagesPaginated(args) {
1601
+ const { threadId, format, selectBy } = args;
1602
+ const { page = 0, perPage: perPageInput, dateRange } = selectBy?.pagination || {};
1603
+ const fromDate = dateRange?.start;
1604
+ const toDate = dateRange?.end;
1605
+ const selectStatement = `SELECT id, content, role, type, "createdAt", thread_id AS "threadId", "resourceId"`;
1606
+ const orderByStatement = `ORDER BY "createdAt" DESC`;
1607
+ const messages = [];
1608
+ if (selectBy?.include?.length) {
1609
+ const includeMessages = await this._getIncludedMessages({ threadId, selectBy, orderByStatement });
1610
+ if (includeMessages) {
1611
+ messages.push(...includeMessages);
1573
1612
  }
1574
- return {
1575
- ...thread,
1576
- metadata: typeof thread.metadata === "string" ? JSON.parse(thread.metadata) : thread.metadata,
1577
- createdAt: thread.createdAt,
1578
- updatedAt: thread.updatedAt
1579
- };
1580
- } catch (error) {
1581
- throw new MastraError(
1582
- {
1583
- id: "MASTRA_STORAGE_PG_STORE_GET_THREAD_BY_ID_FAILED",
1584
- domain: ErrorDomain.STORAGE,
1585
- category: ErrorCategory.THIRD_PARTY,
1586
- details: {
1587
- threadId
1588
- }
1589
- },
1590
- error
1591
- );
1592
- }
1593
- }
1594
- /**
1595
- * @deprecated use getThreadsByResourceIdPaginated instead
1596
- */
1597
- async getThreadsByResourceId(args) {
1598
- const { resourceId } = args;
1599
- try {
1600
- const baseQuery = `FROM ${this.getTableName(TABLE_THREADS)} WHERE "resourceId" = $1`;
1601
- const queryParams = [resourceId];
1602
- const dataQuery = `SELECT id, "resourceId", title, metadata, "createdAt", "updatedAt" ${baseQuery} ORDER BY "createdAt" DESC`;
1603
- const rows = await this.db.manyOrNone(dataQuery, queryParams);
1604
- return (rows || []).map((thread) => ({
1605
- ...thread,
1606
- metadata: typeof thread.metadata === "string" ? JSON.parse(thread.metadata) : thread.metadata,
1607
- createdAt: thread.createdAt,
1608
- updatedAt: thread.updatedAt
1609
- }));
1610
- } catch (error) {
1611
- this.logger.error(`Error getting threads for resource ${resourceId}:`, error);
1612
- return [];
1613
1613
  }
1614
- }
1615
- async getThreadsByResourceIdPaginated(args) {
1616
- const { resourceId, page = 0, perPage: perPageInput } = args;
1617
1614
  try {
1618
- const baseQuery = `FROM ${this.getTableName(TABLE_THREADS)} WHERE "resourceId" = $1`;
1619
- const queryParams = [resourceId];
1620
- const perPage = perPageInput !== void 0 ? perPageInput : 100;
1615
+ const perPage = perPageInput !== void 0 ? perPageInput : resolveMessageLimit({ last: selectBy?.last, defaultLimit: 40 });
1621
1616
  const currentOffset = page * perPage;
1622
- const countQuery = `SELECT COUNT(*) ${baseQuery}`;
1623
- const countResult = await this.db.one(countQuery, queryParams);
1617
+ const conditions = [`thread_id = $1`];
1618
+ const queryParams = [threadId];
1619
+ let paramIndex = 2;
1620
+ if (fromDate) {
1621
+ conditions.push(`"createdAt" >= $${paramIndex++}`);
1622
+ queryParams.push(fromDate);
1623
+ }
1624
+ if (toDate) {
1625
+ conditions.push(`"createdAt" <= $${paramIndex++}`);
1626
+ queryParams.push(toDate);
1627
+ }
1628
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
1629
+ const tableName = getTableName({ indexName: TABLE_MESSAGES, schemaName: getSchemaName(this.schema) });
1630
+ const countQuery = `SELECT COUNT(*) FROM ${tableName} ${whereClause}`;
1631
+ const countResult = await this.client.one(countQuery, queryParams);
1624
1632
  const total = parseInt(countResult.count, 10);
1625
- if (total === 0) {
1633
+ if (total === 0 && messages.length === 0) {
1626
1634
  return {
1627
- threads: [],
1635
+ messages: [],
1628
1636
  total: 0,
1629
1637
  page,
1630
1638
  perPage,
1631
1639
  hasMore: false
1632
1640
  };
1633
1641
  }
1634
- const dataQuery = `SELECT id, "resourceId", title, metadata, "createdAt", "updatedAt" ${baseQuery} ORDER BY "createdAt" DESC LIMIT $2 OFFSET $3`;
1635
- const rows = await this.db.manyOrNone(dataQuery, [...queryParams, perPage, currentOffset]);
1636
- const threads = (rows || []).map((thread) => ({
1637
- ...thread,
1638
- metadata: typeof thread.metadata === "string" ? JSON.parse(thread.metadata) : thread.metadata,
1639
- createdAt: thread.createdAt,
1640
- // Assuming already Date objects or ISO strings
1641
- updatedAt: thread.updatedAt
1642
- }));
1642
+ const excludeIds = messages.map((m) => m.id);
1643
+ const excludeIdsParam = excludeIds.map((_, idx) => `$${idx + paramIndex}`).join(", ");
1644
+ paramIndex += excludeIds.length;
1645
+ const dataQuery = `${selectStatement} FROM ${tableName} ${whereClause} ${excludeIds.length ? `AND id NOT IN (${excludeIdsParam})` : ""}${orderByStatement} LIMIT $${paramIndex++} OFFSET $${paramIndex++}`;
1646
+ const rows = await this.client.manyOrNone(dataQuery, [...queryParams, ...excludeIds, perPage, currentOffset]);
1647
+ messages.push(...rows || []);
1648
+ const messagesWithParsedContent = messages.map((message) => {
1649
+ if (typeof message.content === "string") {
1650
+ try {
1651
+ return { ...message, content: JSON.parse(message.content) };
1652
+ } catch {
1653
+ return message;
1654
+ }
1655
+ }
1656
+ return message;
1657
+ });
1658
+ const list = new MessageList().add(messagesWithParsedContent, "memory");
1659
+ const messagesToReturn = format === `v2` ? list.get.all.v2() : list.get.all.v1();
1643
1660
  return {
1644
- threads,
1661
+ messages: messagesToReturn,
1645
1662
  total,
1646
1663
  page,
1647
1664
  perPage,
1648
- hasMore: currentOffset + threads.length < total
1665
+ hasMore: currentOffset + rows.length < total
1649
1666
  };
1650
1667
  } catch (error) {
1651
1668
  const mastraError = new MastraError(
1652
1669
  {
1653
- id: "MASTRA_STORAGE_PG_STORE_GET_THREADS_BY_RESOURCE_ID_PAGINATED_FAILED",
1670
+ id: "MASTRA_STORAGE_PG_STORE_GET_MESSAGES_PAGINATED_FAILED",
1654
1671
  domain: ErrorDomain.STORAGE,
1655
1672
  category: ErrorCategory.THIRD_PARTY,
1656
1673
  details: {
1657
- resourceId,
1674
+ threadId,
1658
1675
  page
1659
1676
  }
1660
1677
  },
@@ -1662,872 +1679,1348 @@ var PostgresStore = class extends MastraStorage {
1662
1679
  );
1663
1680
  this.logger?.error?.(mastraError.toString());
1664
1681
  this.logger?.trackException(mastraError);
1665
- return { threads: [], total: 0, page, perPage: perPageInput || 100, hasMore: false };
1682
+ return { messages: [], total: 0, page, perPage: perPageInput || 40, hasMore: false };
1666
1683
  }
1667
1684
  }
1668
- async saveThread({ thread }) {
1685
+ async saveMessages({
1686
+ messages,
1687
+ format
1688
+ }) {
1689
+ if (messages.length === 0) return messages;
1690
+ const threadId = messages[0]?.threadId;
1691
+ if (!threadId) {
1692
+ throw new MastraError({
1693
+ id: "MASTRA_STORAGE_PG_STORE_SAVE_MESSAGES_FAILED",
1694
+ domain: ErrorDomain.STORAGE,
1695
+ category: ErrorCategory.THIRD_PARTY,
1696
+ text: `Thread ID is required`
1697
+ });
1698
+ }
1699
+ const thread = await this.getThreadById({ threadId });
1700
+ if (!thread) {
1701
+ throw new MastraError({
1702
+ id: "MASTRA_STORAGE_PG_STORE_SAVE_MESSAGES_FAILED",
1703
+ domain: ErrorDomain.STORAGE,
1704
+ category: ErrorCategory.THIRD_PARTY,
1705
+ text: `Thread ${threadId} not found`,
1706
+ details: {
1707
+ threadId
1708
+ }
1709
+ });
1710
+ }
1669
1711
  try {
1670
- await this.db.none(
1671
- `INSERT INTO ${this.getTableName(TABLE_THREADS)} (
1672
- id,
1673
- "resourceId",
1674
- title,
1675
- metadata,
1676
- "createdAt",
1677
- "updatedAt"
1678
- ) VALUES ($1, $2, $3, $4, $5, $6)
1679
- ON CONFLICT (id) DO UPDATE SET
1680
- "resourceId" = EXCLUDED."resourceId",
1681
- title = EXCLUDED.title,
1682
- metadata = EXCLUDED.metadata,
1683
- "createdAt" = EXCLUDED."createdAt",
1684
- "updatedAt" = EXCLUDED."updatedAt"`,
1685
- [
1686
- thread.id,
1687
- thread.resourceId,
1688
- thread.title,
1689
- thread.metadata ? JSON.stringify(thread.metadata) : null,
1690
- thread.createdAt,
1691
- thread.updatedAt
1692
- ]
1693
- );
1694
- return thread;
1712
+ const tableName = getTableName({ indexName: TABLE_MESSAGES, schemaName: getSchemaName(this.schema) });
1713
+ await this.client.tx(async (t) => {
1714
+ const messageInserts = messages.map((message) => {
1715
+ if (!message.threadId) {
1716
+ throw new Error(
1717
+ `Expected to find a threadId for message, but couldn't find one. An unexpected error has occurred.`
1718
+ );
1719
+ }
1720
+ if (!message.resourceId) {
1721
+ throw new Error(
1722
+ `Expected to find a resourceId for message, but couldn't find one. An unexpected error has occurred.`
1723
+ );
1724
+ }
1725
+ return t.none(
1726
+ `INSERT INTO ${tableName} (id, thread_id, content, "createdAt", "createdAtZ", role, type, "resourceId")
1727
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
1728
+ ON CONFLICT (id) DO UPDATE SET
1729
+ thread_id = EXCLUDED.thread_id,
1730
+ content = EXCLUDED.content,
1731
+ role = EXCLUDED.role,
1732
+ type = EXCLUDED.type,
1733
+ "resourceId" = EXCLUDED."resourceId"`,
1734
+ [
1735
+ message.id,
1736
+ message.threadId,
1737
+ typeof message.content === "string" ? message.content : JSON.stringify(message.content),
1738
+ message.createdAt || (/* @__PURE__ */ new Date()).toISOString(),
1739
+ message.createdAt || (/* @__PURE__ */ new Date()).toISOString(),
1740
+ message.role,
1741
+ message.type || "v2",
1742
+ message.resourceId
1743
+ ]
1744
+ );
1745
+ });
1746
+ const threadTableName = getTableName({ indexName: TABLE_THREADS, schemaName: getSchemaName(this.schema) });
1747
+ const threadUpdate = t.none(
1748
+ `UPDATE ${threadTableName}
1749
+ SET
1750
+ "updatedAt" = $1,
1751
+ "updatedAtZ" = $1
1752
+ WHERE id = $2
1753
+ `,
1754
+ [(/* @__PURE__ */ new Date()).toISOString(), threadId]
1755
+ );
1756
+ await Promise.all([...messageInserts, threadUpdate]);
1757
+ });
1758
+ const messagesWithParsedContent = messages.map((message) => {
1759
+ if (typeof message.content === "string") {
1760
+ try {
1761
+ return { ...message, content: JSON.parse(message.content) };
1762
+ } catch {
1763
+ return message;
1764
+ }
1765
+ }
1766
+ return message;
1767
+ });
1768
+ const list = new MessageList().add(messagesWithParsedContent, "memory");
1769
+ if (format === `v2`) return list.get.all.v2();
1770
+ return list.get.all.v1();
1695
1771
  } catch (error) {
1696
1772
  throw new MastraError(
1697
1773
  {
1698
- id: "MASTRA_STORAGE_PG_STORE_SAVE_THREAD_FAILED",
1774
+ id: "MASTRA_STORAGE_PG_STORE_SAVE_MESSAGES_FAILED",
1699
1775
  domain: ErrorDomain.STORAGE,
1700
1776
  category: ErrorCategory.THIRD_PARTY,
1701
1777
  details: {
1702
- threadId: thread.id
1778
+ threadId
1703
1779
  }
1704
1780
  },
1705
1781
  error
1706
1782
  );
1707
1783
  }
1708
1784
  }
1709
- async updateThread({
1710
- id,
1711
- title,
1712
- metadata
1785
+ async updateMessages({
1786
+ messages
1713
1787
  }) {
1714
- const existingThread = await this.getThreadById({ threadId: id });
1715
- if (!existingThread) {
1716
- throw new MastraError({
1717
- id: "MASTRA_STORAGE_PG_STORE_UPDATE_THREAD_FAILED",
1718
- domain: ErrorDomain.STORAGE,
1719
- category: ErrorCategory.USER,
1720
- text: `Thread ${id} not found`,
1721
- details: {
1722
- threadId: id,
1723
- title
1788
+ if (messages.length === 0) {
1789
+ return [];
1790
+ }
1791
+ const messageIds = messages.map((m) => m.id);
1792
+ const selectQuery = `SELECT id, content, role, type, "createdAt", thread_id AS "threadId", "resourceId" FROM ${getTableName({ indexName: TABLE_MESSAGES, schemaName: getSchemaName(this.schema) })} WHERE id IN ($1:list)`;
1793
+ const existingMessagesDb = await this.client.manyOrNone(selectQuery, [messageIds]);
1794
+ if (existingMessagesDb.length === 0) {
1795
+ return [];
1796
+ }
1797
+ const existingMessages = existingMessagesDb.map((msg) => {
1798
+ if (typeof msg.content === "string") {
1799
+ try {
1800
+ msg.content = JSON.parse(msg.content);
1801
+ } catch {
1724
1802
  }
1725
- });
1803
+ }
1804
+ return msg;
1805
+ });
1806
+ const threadIdsToUpdate = /* @__PURE__ */ new Set();
1807
+ await this.client.tx(async (t) => {
1808
+ const queries = [];
1809
+ const columnMapping = {
1810
+ threadId: "thread_id"
1811
+ };
1812
+ for (const existingMessage of existingMessages) {
1813
+ const updatePayload = messages.find((m) => m.id === existingMessage.id);
1814
+ if (!updatePayload) continue;
1815
+ const { id, ...fieldsToUpdate } = updatePayload;
1816
+ if (Object.keys(fieldsToUpdate).length === 0) continue;
1817
+ threadIdsToUpdate.add(existingMessage.threadId);
1818
+ if (updatePayload.threadId && updatePayload.threadId !== existingMessage.threadId) {
1819
+ threadIdsToUpdate.add(updatePayload.threadId);
1820
+ }
1821
+ const setClauses = [];
1822
+ const values = [];
1823
+ let paramIndex = 1;
1824
+ const updatableFields = { ...fieldsToUpdate };
1825
+ if (updatableFields.content) {
1826
+ const newContent = {
1827
+ ...existingMessage.content,
1828
+ ...updatableFields.content,
1829
+ // Deep merge metadata if it exists on both
1830
+ ...existingMessage.content?.metadata && updatableFields.content.metadata ? {
1831
+ metadata: {
1832
+ ...existingMessage.content.metadata,
1833
+ ...updatableFields.content.metadata
1834
+ }
1835
+ } : {}
1836
+ };
1837
+ setClauses.push(`content = $${paramIndex++}`);
1838
+ values.push(newContent);
1839
+ delete updatableFields.content;
1840
+ }
1841
+ for (const key in updatableFields) {
1842
+ if (Object.prototype.hasOwnProperty.call(updatableFields, key)) {
1843
+ const dbColumn = columnMapping[key] || key;
1844
+ setClauses.push(`"${dbColumn}" = $${paramIndex++}`);
1845
+ values.push(updatableFields[key]);
1846
+ }
1847
+ }
1848
+ if (setClauses.length > 0) {
1849
+ values.push(id);
1850
+ const sql = `UPDATE ${getTableName({ indexName: TABLE_MESSAGES, schemaName: getSchemaName(this.schema) })} SET ${setClauses.join(", ")} WHERE id = $${paramIndex}`;
1851
+ queries.push(t.none(sql, values));
1852
+ }
1853
+ }
1854
+ if (threadIdsToUpdate.size > 0) {
1855
+ queries.push(
1856
+ t.none(
1857
+ `UPDATE ${getTableName({ indexName: TABLE_THREADS, schemaName: getSchemaName(this.schema) })} SET "updatedAt" = NOW(), "updatedAtZ" = NOW() WHERE id IN ($1:list)`,
1858
+ [Array.from(threadIdsToUpdate)]
1859
+ )
1860
+ );
1861
+ }
1862
+ if (queries.length > 0) {
1863
+ await t.batch(queries);
1864
+ }
1865
+ });
1866
+ const updatedMessages = await this.client.manyOrNone(selectQuery, [messageIds]);
1867
+ return (updatedMessages || []).map((message) => {
1868
+ if (typeof message.content === "string") {
1869
+ try {
1870
+ message.content = JSON.parse(message.content);
1871
+ } catch {
1872
+ }
1873
+ }
1874
+ return message;
1875
+ });
1876
+ }
1877
+ async getResourceById({ resourceId }) {
1878
+ const tableName = getTableName({ indexName: TABLE_RESOURCES, schemaName: getSchemaName(this.schema) });
1879
+ const result = await this.client.oneOrNone(
1880
+ `SELECT * FROM ${tableName} WHERE id = $1`,
1881
+ [resourceId]
1882
+ );
1883
+ if (!result) {
1884
+ return null;
1726
1885
  }
1727
- const mergedMetadata = {
1728
- ...existingThread.metadata,
1729
- ...metadata
1886
+ return {
1887
+ id: result.id,
1888
+ createdAt: result.createdAtZ || result.createdAt,
1889
+ updatedAt: result.updatedAtZ || result.updatedAt,
1890
+ workingMemory: result.workingMemory,
1891
+ metadata: typeof result.metadata === "string" ? JSON.parse(result.metadata) : result.metadata
1892
+ };
1893
+ }
1894
+ async saveResource({ resource }) {
1895
+ await this.operations.insert({
1896
+ tableName: TABLE_RESOURCES,
1897
+ record: {
1898
+ ...resource,
1899
+ metadata: JSON.stringify(resource.metadata)
1900
+ }
1901
+ });
1902
+ return resource;
1903
+ }
1904
+ async updateResource({
1905
+ resourceId,
1906
+ workingMemory,
1907
+ metadata
1908
+ }) {
1909
+ const existingResource = await this.getResourceById({ resourceId });
1910
+ if (!existingResource) {
1911
+ const newResource = {
1912
+ id: resourceId,
1913
+ workingMemory,
1914
+ metadata: metadata || {},
1915
+ createdAt: /* @__PURE__ */ new Date(),
1916
+ updatedAt: /* @__PURE__ */ new Date()
1917
+ };
1918
+ return this.saveResource({ resource: newResource });
1919
+ }
1920
+ const updatedResource = {
1921
+ ...existingResource,
1922
+ workingMemory: workingMemory !== void 0 ? workingMemory : existingResource.workingMemory,
1923
+ metadata: {
1924
+ ...existingResource.metadata,
1925
+ ...metadata
1926
+ },
1927
+ updatedAt: /* @__PURE__ */ new Date()
1730
1928
  };
1929
+ const tableName = getTableName({ indexName: TABLE_RESOURCES, schemaName: getSchemaName(this.schema) });
1930
+ const updates = [];
1931
+ const values = [];
1932
+ let paramIndex = 1;
1933
+ if (workingMemory !== void 0) {
1934
+ updates.push(`"workingMemory" = $${paramIndex}`);
1935
+ values.push(workingMemory);
1936
+ paramIndex++;
1937
+ }
1938
+ if (metadata) {
1939
+ updates.push(`metadata = $${paramIndex}`);
1940
+ values.push(JSON.stringify(updatedResource.metadata));
1941
+ paramIndex++;
1942
+ }
1943
+ updates.push(`"updatedAt" = $${paramIndex}`);
1944
+ values.push(updatedResource.updatedAt.toISOString());
1945
+ updates.push(`"updatedAtZ" = $${paramIndex++}`);
1946
+ values.push(updatedResource.updatedAt.toISOString());
1947
+ paramIndex++;
1948
+ values.push(resourceId);
1949
+ await this.client.none(`UPDATE ${tableName} SET ${updates.join(", ")} WHERE id = $${paramIndex}`, values);
1950
+ return updatedResource;
1951
+ }
1952
+ };
1953
+ var StoreOperationsPG = class extends StoreOperations {
1954
+ client;
1955
+ schemaName;
1956
+ setupSchemaPromise = null;
1957
+ schemaSetupComplete = void 0;
1958
+ constructor({ client, schemaName }) {
1959
+ super();
1960
+ this.client = client;
1961
+ this.schemaName = schemaName;
1962
+ }
1963
+ async hasColumn(table, column) {
1964
+ const schema = this.schemaName || "public";
1965
+ const result = await this.client.oneOrNone(
1966
+ `SELECT 1 FROM information_schema.columns WHERE table_schema = $1 AND table_name = $2 AND (column_name = $3 OR column_name = $4)`,
1967
+ [schema, table, column, column.toLowerCase()]
1968
+ );
1969
+ return !!result;
1970
+ }
1971
+ async setupSchema() {
1972
+ if (!this.schemaName || this.schemaSetupComplete) {
1973
+ return;
1974
+ }
1975
+ const schemaName = getSchemaName(this.schemaName);
1976
+ if (!this.setupSchemaPromise) {
1977
+ this.setupSchemaPromise = (async () => {
1978
+ try {
1979
+ const schemaExists = await this.client.oneOrNone(
1980
+ `
1981
+ SELECT EXISTS (
1982
+ SELECT 1 FROM information_schema.schemata
1983
+ WHERE schema_name = $1
1984
+ )
1985
+ `,
1986
+ [this.schemaName]
1987
+ );
1988
+ if (!schemaExists?.exists) {
1989
+ try {
1990
+ await this.client.none(`CREATE SCHEMA IF NOT EXISTS ${schemaName}`);
1991
+ this.logger.info(`Schema "${this.schemaName}" created successfully`);
1992
+ } catch (error) {
1993
+ this.logger.error(`Failed to create schema "${this.schemaName}"`, { error });
1994
+ throw new Error(
1995
+ `Unable to create schema "${this.schemaName}". This requires CREATE privilege on the database. Either create the schema manually or grant CREATE privilege to the user.`
1996
+ );
1997
+ }
1998
+ }
1999
+ this.schemaSetupComplete = true;
2000
+ this.logger.debug(`Schema "${schemaName}" is ready for use`);
2001
+ } catch (error) {
2002
+ this.schemaSetupComplete = void 0;
2003
+ this.setupSchemaPromise = null;
2004
+ throw error;
2005
+ } finally {
2006
+ this.setupSchemaPromise = null;
2007
+ }
2008
+ })();
2009
+ }
2010
+ await this.setupSchemaPromise;
2011
+ }
2012
+ async insert({ tableName, record }) {
1731
2013
  try {
1732
- const thread = await this.db.one(
1733
- `UPDATE ${this.getTableName(TABLE_THREADS)}
1734
- SET title = $1,
1735
- metadata = $2,
1736
- "updatedAt" = $3
1737
- WHERE id = $4
1738
- RETURNING *`,
1739
- [title, mergedMetadata, (/* @__PURE__ */ new Date()).toISOString(), id]
2014
+ if (record.createdAt) {
2015
+ record.createdAtZ = record.createdAt;
2016
+ }
2017
+ if (record.created_at) {
2018
+ record.created_atZ = record.created_at;
2019
+ }
2020
+ if (record.updatedAt) {
2021
+ record.updatedAtZ = record.updatedAt;
2022
+ }
2023
+ const schemaName = getSchemaName(this.schemaName);
2024
+ const columns = Object.keys(record).map((col) => parseSqlIdentifier(col, "column name"));
2025
+ const values = Object.values(record);
2026
+ const placeholders = values.map((_, i) => `$${i + 1}`).join(", ");
2027
+ await this.client.none(
2028
+ `INSERT INTO ${getTableName({ indexName: tableName, schemaName })} (${columns.map((c) => `"${c}"`).join(", ")}) VALUES (${placeholders})`,
2029
+ values
1740
2030
  );
1741
- return {
1742
- ...thread,
1743
- metadata: typeof thread.metadata === "string" ? JSON.parse(thread.metadata) : thread.metadata,
1744
- createdAt: thread.createdAt,
1745
- updatedAt: thread.updatedAt
1746
- };
1747
2031
  } catch (error) {
1748
2032
  throw new MastraError(
1749
2033
  {
1750
- id: "MASTRA_STORAGE_PG_STORE_UPDATE_THREAD_FAILED",
2034
+ id: "MASTRA_STORAGE_PG_STORE_INSERT_FAILED",
1751
2035
  domain: ErrorDomain.STORAGE,
1752
2036
  category: ErrorCategory.THIRD_PARTY,
1753
2037
  details: {
1754
- threadId: id,
1755
- title
2038
+ tableName
1756
2039
  }
1757
2040
  },
1758
2041
  error
1759
2042
  );
1760
2043
  }
1761
2044
  }
1762
- async deleteThread({ threadId }) {
2045
+ async clearTable({ tableName }) {
1763
2046
  try {
1764
- await this.db.tx(async (t) => {
1765
- await t.none(`DELETE FROM ${this.getTableName(TABLE_MESSAGES)} WHERE thread_id = $1`, [threadId]);
1766
- await t.none(`DELETE FROM ${this.getTableName(TABLE_THREADS)} WHERE id = $1`, [threadId]);
1767
- });
2047
+ const schemaName = getSchemaName(this.schemaName);
2048
+ const tableNameWithSchema = getTableName({ indexName: tableName, schemaName });
2049
+ await this.client.none(`TRUNCATE TABLE ${tableNameWithSchema} CASCADE`);
1768
2050
  } catch (error) {
1769
2051
  throw new MastraError(
1770
2052
  {
1771
- id: "MASTRA_STORAGE_PG_STORE_DELETE_THREAD_FAILED",
2053
+ id: "MASTRA_STORAGE_PG_STORE_CLEAR_TABLE_FAILED",
1772
2054
  domain: ErrorDomain.STORAGE,
1773
2055
  category: ErrorCategory.THIRD_PARTY,
1774
2056
  details: {
1775
- threadId
2057
+ tableName
1776
2058
  }
1777
2059
  },
1778
2060
  error
1779
2061
  );
1780
2062
  }
1781
2063
  }
1782
- async _getIncludedMessages({
1783
- threadId,
1784
- selectBy,
1785
- orderByStatement
2064
+ getDefaultValue(type) {
2065
+ switch (type) {
2066
+ case "timestamp":
2067
+ return "DEFAULT NOW()";
2068
+ case "jsonb":
2069
+ return "DEFAULT '{}'::jsonb";
2070
+ default:
2071
+ return super.getDefaultValue(type);
2072
+ }
2073
+ }
2074
+ async createTable({
2075
+ tableName,
2076
+ schema
1786
2077
  }) {
1787
- const include = selectBy?.include;
1788
- if (!include) return null;
1789
- const unionQueries = [];
1790
- const params = [];
1791
- let paramIdx = 1;
1792
- for (const inc of include) {
1793
- const { id, withPreviousMessages = 0, withNextMessages = 0 } = inc;
1794
- const searchId = inc.threadId || threadId;
1795
- unionQueries.push(
1796
- `
1797
- SELECT * FROM (
1798
- WITH ordered_messages AS (
1799
- SELECT
1800
- *,
1801
- ROW_NUMBER() OVER (${orderByStatement}) as row_num
1802
- FROM ${this.getTableName(TABLE_MESSAGES)}
1803
- WHERE thread_id = $${paramIdx}
1804
- )
1805
- SELECT
1806
- m.id,
1807
- m.content,
1808
- m.role,
1809
- m.type,
1810
- m."createdAt",
1811
- m.thread_id AS "threadId",
1812
- m."resourceId"
1813
- FROM ordered_messages m
1814
- WHERE m.id = $${paramIdx + 1}
1815
- OR EXISTS (
1816
- SELECT 1 FROM ordered_messages target
1817
- WHERE target.id = $${paramIdx + 1}
1818
- AND (
1819
- -- Get previous messages based on the max withPreviousMessages
1820
- (m.row_num <= target.row_num + $${paramIdx + 2} AND m.row_num > target.row_num)
1821
- OR
1822
- -- Get next messages based on the max withNextMessages
1823
- (m.row_num >= target.row_num - $${paramIdx + 3} AND m.row_num < target.row_num)
1824
- )
1825
- )
1826
- ) AS query_${paramIdx}
1827
- `
1828
- // Keep ASC for final sorting after fetching context
2078
+ try {
2079
+ const timeZColumnNames = Object.entries(schema).filter(([_, def]) => def.type === "timestamp").map(([name]) => name);
2080
+ const timeZColumns = Object.entries(schema).filter(([_, def]) => def.type === "timestamp").map(([name]) => {
2081
+ const parsedName = parseSqlIdentifier(name, "column name");
2082
+ return `"${parsedName}Z" TIMESTAMPTZ DEFAULT NOW()`;
2083
+ });
2084
+ const columns = Object.entries(schema).map(([name, def]) => {
2085
+ const parsedName = parseSqlIdentifier(name, "column name");
2086
+ const constraints = [];
2087
+ if (def.primaryKey) constraints.push("PRIMARY KEY");
2088
+ if (!def.nullable) constraints.push("NOT NULL");
2089
+ return `"${parsedName}" ${def.type.toUpperCase()} ${constraints.join(" ")}`;
2090
+ });
2091
+ if (this.schemaName) {
2092
+ await this.setupSchema();
2093
+ }
2094
+ const finalColumns = [...columns, ...timeZColumns].join(",\n");
2095
+ const sql = `
2096
+ CREATE TABLE IF NOT EXISTS ${getTableName({ indexName: tableName, schemaName: getSchemaName(this.schemaName) })} (
2097
+ ${finalColumns}
2098
+ );
2099
+ ${tableName === TABLE_WORKFLOW_SNAPSHOT ? `
2100
+ DO $$ BEGIN
2101
+ IF NOT EXISTS (
2102
+ SELECT 1 FROM pg_constraint WHERE conname = 'mastra_workflow_snapshot_workflow_name_run_id_key'
2103
+ ) THEN
2104
+ ALTER TABLE ${getTableName({ indexName: tableName, schemaName: getSchemaName(this.schemaName) })}
2105
+ ADD CONSTRAINT mastra_workflow_snapshot_workflow_name_run_id_key
2106
+ UNIQUE (workflow_name, run_id);
2107
+ END IF;
2108
+ END $$;
2109
+ ` : ""}
2110
+ `;
2111
+ await this.client.none(sql);
2112
+ console.log("Alter Table SQL", tableName);
2113
+ console.log("timeZColumnNames", timeZColumnNames);
2114
+ await this.alterTable({
2115
+ tableName,
2116
+ schema,
2117
+ ifNotExists: timeZColumnNames
2118
+ });
2119
+ } catch (error) {
2120
+ throw new MastraError(
2121
+ {
2122
+ id: "MASTRA_STORAGE_PG_STORE_CREATE_TABLE_FAILED",
2123
+ domain: ErrorDomain.STORAGE,
2124
+ category: ErrorCategory.THIRD_PARTY,
2125
+ details: {
2126
+ tableName
2127
+ }
2128
+ },
2129
+ error
1829
2130
  );
1830
- params.push(searchId, id, withPreviousMessages, withNextMessages);
1831
- paramIdx += 4;
1832
2131
  }
1833
- const finalQuery = unionQueries.join(" UNION ALL ") + ' ORDER BY "createdAt" ASC';
1834
- const includedRows = await this.db.manyOrNone(finalQuery, params);
1835
- const seen = /* @__PURE__ */ new Set();
1836
- const dedupedRows = includedRows.filter((row) => {
1837
- if (seen.has(row.id)) return false;
1838
- seen.add(row.id);
1839
- return true;
1840
- });
1841
- return dedupedRows;
1842
2132
  }
1843
- async getMessages(args) {
1844
- const { threadId, format, selectBy } = args;
1845
- const selectStatement = `SELECT id, content, role, type, "createdAt", thread_id AS "threadId", "resourceId"`;
1846
- const orderByStatement = `ORDER BY "createdAt" DESC`;
1847
- const limit = this.resolveMessageLimit({ last: selectBy?.last, defaultLimit: 40 });
2133
+ /**
2134
+ * Alters table schema to add columns if they don't exist
2135
+ * @param tableName Name of the table
2136
+ * @param schema Schema of the table
2137
+ * @param ifNotExists Array of column names to add if they don't exist
2138
+ */
2139
+ async alterTable({
2140
+ tableName,
2141
+ schema,
2142
+ ifNotExists
2143
+ }) {
2144
+ const fullTableName = getTableName({ indexName: tableName, schemaName: getSchemaName(this.schemaName) });
1848
2145
  try {
1849
- let rows = [];
1850
- const include = selectBy?.include || [];
1851
- if (include?.length) {
1852
- const includeMessages = await this._getIncludedMessages({ threadId, selectBy, orderByStatement });
1853
- if (includeMessages) {
1854
- rows.push(...includeMessages);
1855
- }
1856
- }
1857
- const excludeIds = rows.map((m) => m.id);
1858
- const excludeIdsParam = excludeIds.map((_, idx) => `$${idx + 2}`).join(", ");
1859
- let query = `${selectStatement} FROM ${this.getTableName(TABLE_MESSAGES)} WHERE thread_id = $1
1860
- ${excludeIds.length ? `AND id NOT IN (${excludeIdsParam})` : ""}
1861
- ${orderByStatement}
1862
- LIMIT $${excludeIds.length + 2}
1863
- `;
1864
- const queryParams = [threadId, ...excludeIds, limit];
1865
- const remainingRows = await this.db.manyOrNone(query, queryParams);
1866
- rows.push(...remainingRows);
1867
- const fetchedMessages = (rows || []).map((message) => {
1868
- if (typeof message.content === "string") {
1869
- try {
1870
- message.content = JSON.parse(message.content);
1871
- } catch {
2146
+ for (const columnName of ifNotExists) {
2147
+ if (schema[columnName]) {
2148
+ const columnDef = schema[columnName];
2149
+ const sqlType = this.getSqlType(columnDef.type);
2150
+ const nullable = columnDef.nullable === false ? "NOT NULL" : "";
2151
+ const defaultValue = columnDef.nullable === false ? this.getDefaultValue(columnDef.type) : "";
2152
+ const parsedColumnName = parseSqlIdentifier(columnName, "column name");
2153
+ const alterSql = `ALTER TABLE ${fullTableName} ADD COLUMN IF NOT EXISTS "${parsedColumnName}" ${sqlType} ${nullable} ${defaultValue}`.trim();
2154
+ await this.client.none(alterSql);
2155
+ if (sqlType === "TIMESTAMP") {
2156
+ const alterSql2 = `ALTER TABLE ${fullTableName} ADD COLUMN IF NOT EXISTS "${parsedColumnName}Z" TIMESTAMPTZ DEFAULT NOW()`.trim();
2157
+ await this.client.none(alterSql2);
1872
2158
  }
2159
+ this.logger?.debug?.(`Ensured column ${parsedColumnName} exists in table ${fullTableName}`);
1873
2160
  }
1874
- if (message.type === "v2") delete message.type;
1875
- return message;
1876
- });
1877
- const sortedMessages = fetchedMessages.sort(
1878
- (a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
1879
- );
1880
- return format === "v2" ? sortedMessages.map(
1881
- (m) => ({ ...m, content: m.content || { format: 2, parts: [{ type: "text", text: "" }] } })
1882
- ) : sortedMessages;
2161
+ }
1883
2162
  } catch (error) {
1884
- const mastraError = new MastraError(
2163
+ throw new MastraError(
1885
2164
  {
1886
- id: "MASTRA_STORAGE_PG_STORE_GET_MESSAGES_FAILED",
2165
+ id: "MASTRA_STORAGE_PG_STORE_ALTER_TABLE_FAILED",
1887
2166
  domain: ErrorDomain.STORAGE,
1888
2167
  category: ErrorCategory.THIRD_PARTY,
1889
2168
  details: {
1890
- threadId
2169
+ tableName
1891
2170
  }
1892
2171
  },
1893
2172
  error
1894
2173
  );
1895
- this.logger?.error?.(mastraError.toString());
1896
- this.logger?.trackException(mastraError);
1897
- return [];
1898
2174
  }
1899
2175
  }
1900
- async getMessagesPaginated(args) {
1901
- const { threadId, format, selectBy } = args;
1902
- const { page = 0, perPage: perPageInput, dateRange } = selectBy?.pagination || {};
1903
- const fromDate = dateRange?.start;
1904
- const toDate = dateRange?.end;
1905
- const selectStatement = `SELECT id, content, role, type, "createdAt", thread_id AS "threadId", "resourceId"`;
1906
- const orderByStatement = `ORDER BY "createdAt" DESC`;
1907
- const messages = [];
1908
- if (selectBy?.include?.length) {
1909
- const includeMessages = await this._getIncludedMessages({ threadId, selectBy, orderByStatement });
1910
- if (includeMessages) {
1911
- messages.push(...includeMessages);
1912
- }
1913
- }
2176
+ async load({ tableName, keys }) {
1914
2177
  try {
1915
- const perPage = perPageInput !== void 0 ? perPageInput : this.resolveMessageLimit({ last: selectBy?.last, defaultLimit: 40 });
1916
- const currentOffset = page * perPage;
1917
- const conditions = [`thread_id = $1`];
1918
- const queryParams = [threadId];
1919
- let paramIndex = 2;
1920
- if (fromDate) {
1921
- conditions.push(`"createdAt" >= $${paramIndex++}`);
1922
- queryParams.push(fromDate);
1923
- }
1924
- if (toDate) {
1925
- conditions.push(`"createdAt" <= $${paramIndex++}`);
1926
- queryParams.push(toDate);
1927
- }
1928
- const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
1929
- const countQuery = `SELECT COUNT(*) FROM ${this.getTableName(TABLE_MESSAGES)} ${whereClause}`;
1930
- const countResult = await this.db.one(countQuery, queryParams);
1931
- const total = parseInt(countResult.count, 10);
1932
- if (total === 0 && messages.length === 0) {
1933
- return {
1934
- messages: [],
1935
- total: 0,
1936
- page,
1937
- perPage,
1938
- hasMore: false
1939
- };
2178
+ const keyEntries = Object.entries(keys).map(([key, value]) => [parseSqlIdentifier(key, "column name"), value]);
2179
+ const conditions = keyEntries.map(([key], index) => `"${key}" = $${index + 1}`).join(" AND ");
2180
+ const values = keyEntries.map(([_, value]) => value);
2181
+ const result = await this.client.oneOrNone(
2182
+ `SELECT * FROM ${getTableName({ indexName: tableName, schemaName: getSchemaName(this.schemaName) })} WHERE ${conditions}`,
2183
+ values
2184
+ );
2185
+ if (!result) {
2186
+ return null;
1940
2187
  }
1941
- const excludeIds = messages.map((m) => m.id);
1942
- const excludeIdsParam = excludeIds.map((_, idx) => `$${idx + paramIndex}`).join(", ");
1943
- paramIndex += excludeIds.length;
1944
- const dataQuery = `${selectStatement} FROM ${this.getTableName(
1945
- TABLE_MESSAGES
1946
- )} ${whereClause} ${excludeIds.length ? `AND id NOT IN (${excludeIdsParam})` : ""}${orderByStatement} LIMIT $${paramIndex++} OFFSET $${paramIndex++}`;
1947
- const rows = await this.db.manyOrNone(dataQuery, [...queryParams, ...excludeIds, perPage, currentOffset]);
1948
- messages.push(...rows || []);
1949
- const messagesWithParsedContent = messages.map((message) => {
1950
- if (typeof message.content === "string") {
1951
- try {
1952
- return { ...message, content: JSON.parse(message.content) };
1953
- } catch {
1954
- return message;
1955
- }
2188
+ if (tableName === TABLE_WORKFLOW_SNAPSHOT) {
2189
+ const snapshot = result;
2190
+ if (typeof snapshot.snapshot === "string") {
2191
+ snapshot.snapshot = JSON.parse(snapshot.snapshot);
1956
2192
  }
1957
- return message;
1958
- });
1959
- const list = new MessageList().add(messagesWithParsedContent, "memory");
1960
- const messagesToReturn = format === `v2` ? list.get.all.v2() : list.get.all.v1();
1961
- return {
1962
- messages: messagesToReturn,
1963
- total,
1964
- page,
1965
- perPage,
1966
- hasMore: currentOffset + rows.length < total
1967
- };
2193
+ return snapshot;
2194
+ }
2195
+ return result;
1968
2196
  } catch (error) {
1969
- const mastraError = new MastraError(
2197
+ throw new MastraError(
1970
2198
  {
1971
- id: "MASTRA_STORAGE_PG_STORE_GET_MESSAGES_PAGINATED_FAILED",
2199
+ id: "MASTRA_STORAGE_PG_STORE_LOAD_FAILED",
1972
2200
  domain: ErrorDomain.STORAGE,
1973
2201
  category: ErrorCategory.THIRD_PARTY,
1974
2202
  details: {
1975
- threadId,
1976
- page
2203
+ tableName
1977
2204
  }
1978
2205
  },
1979
2206
  error
1980
2207
  );
1981
- this.logger?.error?.(mastraError.toString());
1982
- this.logger?.trackException(mastraError);
1983
- return { messages: [], total: 0, page, perPage: perPageInput || 40, hasMore: false };
1984
2208
  }
1985
2209
  }
1986
- async saveMessages({
1987
- messages,
1988
- format
1989
- }) {
1990
- if (messages.length === 0) return messages;
1991
- const threadId = messages[0]?.threadId;
1992
- if (!threadId) {
1993
- throw new MastraError({
1994
- id: "MASTRA_STORAGE_PG_STORE_SAVE_MESSAGES_FAILED",
1995
- domain: ErrorDomain.STORAGE,
1996
- category: ErrorCategory.THIRD_PARTY,
1997
- text: `Thread ID is required`
1998
- });
1999
- }
2000
- const thread = await this.getThreadById({ threadId });
2001
- if (!thread) {
2002
- throw new MastraError({
2003
- id: "MASTRA_STORAGE_PG_STORE_SAVE_MESSAGES_FAILED",
2004
- domain: ErrorDomain.STORAGE,
2005
- category: ErrorCategory.THIRD_PARTY,
2006
- text: `Thread ${threadId} not found`,
2007
- details: {
2008
- threadId
2009
- }
2010
- });
2210
+ async batchInsert({ tableName, records }) {
2211
+ try {
2212
+ await this.client.query("BEGIN");
2213
+ for (const record of records) {
2214
+ await this.insert({ tableName, record });
2215
+ }
2216
+ await this.client.query("COMMIT");
2217
+ } catch (error) {
2218
+ await this.client.query("ROLLBACK");
2219
+ throw new MastraError(
2220
+ {
2221
+ id: "MASTRA_STORAGE_PG_STORE_BATCH_INSERT_FAILED",
2222
+ domain: ErrorDomain.STORAGE,
2223
+ category: ErrorCategory.THIRD_PARTY,
2224
+ details: {
2225
+ tableName,
2226
+ numberOfRecords: records.length
2227
+ }
2228
+ },
2229
+ error
2230
+ );
2011
2231
  }
2232
+ }
2233
+ async dropTable({ tableName }) {
2012
2234
  try {
2013
- await this.db.tx(async (t) => {
2014
- const messageInserts = messages.map((message) => {
2015
- if (!message.threadId) {
2016
- throw new Error(
2017
- `Expected to find a threadId for message, but couldn't find one. An unexpected error has occurred.`
2018
- );
2019
- }
2020
- if (!message.resourceId) {
2021
- throw new Error(
2022
- `Expected to find a resourceId for message, but couldn't find one. An unexpected error has occurred.`
2023
- );
2024
- }
2025
- return t.none(
2026
- `INSERT INTO ${this.getTableName(TABLE_MESSAGES)} (id, thread_id, content, "createdAt", role, type, "resourceId")
2027
- VALUES ($1, $2, $3, $4, $5, $6, $7)
2028
- ON CONFLICT (id) DO UPDATE SET
2029
- thread_id = EXCLUDED.thread_id,
2030
- content = EXCLUDED.content,
2031
- role = EXCLUDED.role,
2032
- type = EXCLUDED.type,
2033
- "resourceId" = EXCLUDED."resourceId"`,
2034
- [
2035
- message.id,
2036
- message.threadId,
2037
- typeof message.content === "string" ? message.content : JSON.stringify(message.content),
2038
- message.createdAt || (/* @__PURE__ */ new Date()).toISOString(),
2039
- message.role,
2040
- message.type || "v2",
2041
- message.resourceId
2042
- ]
2043
- );
2044
- });
2045
- const threadUpdate = t.none(
2046
- `UPDATE ${this.getTableName(TABLE_THREADS)}
2047
- SET "updatedAt" = $1
2048
- WHERE id = $2`,
2049
- [(/* @__PURE__ */ new Date()).toISOString(), threadId]
2050
- );
2051
- await Promise.all([...messageInserts, threadUpdate]);
2052
- });
2053
- const messagesWithParsedContent = messages.map((message) => {
2054
- if (typeof message.content === "string") {
2055
- try {
2056
- return { ...message, content: JSON.parse(message.content) };
2057
- } catch {
2058
- return message;
2059
- }
2060
- }
2061
- return message;
2062
- });
2063
- const list = new MessageList().add(messagesWithParsedContent, "memory");
2064
- if (format === `v2`) return list.get.all.v2();
2065
- return list.get.all.v1();
2066
- } catch (error) {
2067
- throw new MastraError(
2068
- {
2069
- id: "MASTRA_STORAGE_PG_STORE_SAVE_MESSAGES_FAILED",
2070
- domain: ErrorDomain.STORAGE,
2071
- category: ErrorCategory.THIRD_PARTY,
2072
- details: {
2073
- threadId
2235
+ const schemaName = getSchemaName(this.schemaName);
2236
+ const tableNameWithSchema = getTableName({ indexName: tableName, schemaName });
2237
+ await this.client.none(`DROP TABLE IF EXISTS ${tableNameWithSchema}`);
2238
+ } catch (error) {
2239
+ throw new MastraError(
2240
+ {
2241
+ id: "MASTRA_STORAGE_PG_STORE_DROP_TABLE_FAILED",
2242
+ domain: ErrorDomain.STORAGE,
2243
+ category: ErrorCategory.THIRD_PARTY,
2244
+ details: {
2245
+ tableName
2074
2246
  }
2075
2247
  },
2076
2248
  error
2077
2249
  );
2078
2250
  }
2079
2251
  }
2080
- async persistWorkflowSnapshot({
2081
- workflowName,
2082
- runId,
2083
- snapshot
2084
- }) {
2252
+ };
2253
+ function transformScoreRow(row) {
2254
+ let input = void 0;
2255
+ if (row.input) {
2085
2256
  try {
2086
- const now = (/* @__PURE__ */ new Date()).toISOString();
2087
- await this.db.none(
2088
- `INSERT INTO ${this.getTableName(TABLE_WORKFLOW_SNAPSHOT)} (
2089
- workflow_name,
2090
- run_id,
2091
- snapshot,
2092
- "createdAt",
2093
- "updatedAt"
2094
- ) VALUES ($1, $2, $3, $4, $5)
2095
- ON CONFLICT (workflow_name, run_id) DO UPDATE
2096
- SET snapshot = EXCLUDED.snapshot,
2097
- "updatedAt" = EXCLUDED."updatedAt"`,
2098
- [workflowName, runId, JSON.stringify(snapshot), now, now]
2099
- );
2257
+ input = JSON.parse(row.input);
2258
+ } catch {
2259
+ input = row.input;
2260
+ }
2261
+ }
2262
+ return {
2263
+ ...row,
2264
+ input,
2265
+ createdAt: row.createdAtZ || row.createdAt,
2266
+ updatedAt: row.updatedAtZ || row.updatedAt
2267
+ };
2268
+ }
2269
+ var ScoresPG = class extends ScoresStorage {
2270
+ client;
2271
+ operations;
2272
+ constructor({ client, operations }) {
2273
+ super();
2274
+ this.client = client;
2275
+ this.operations = operations;
2276
+ }
2277
+ async getScoreById({ id }) {
2278
+ try {
2279
+ const result = await this.client.oneOrNone(`SELECT * FROM ${TABLE_SCORERS} WHERE id = $1`, [id]);
2280
+ return transformScoreRow(result);
2100
2281
  } catch (error) {
2101
2282
  throw new MastraError(
2102
2283
  {
2103
- id: "MASTRA_STORAGE_PG_STORE_PERSIST_WORKFLOW_SNAPSHOT_FAILED",
2284
+ id: "MASTRA_STORAGE_PG_STORE_GET_SCORE_BY_ID_FAILED",
2104
2285
  domain: ErrorDomain.STORAGE,
2105
- category: ErrorCategory.THIRD_PARTY,
2106
- details: {
2107
- workflowName,
2108
- runId
2109
- }
2286
+ category: ErrorCategory.THIRD_PARTY
2110
2287
  },
2111
2288
  error
2112
2289
  );
2113
2290
  }
2114
2291
  }
2115
- async loadWorkflowSnapshot({
2116
- workflowName,
2117
- runId
2292
+ async getScoresByScorerId({
2293
+ scorerId,
2294
+ pagination
2118
2295
  }) {
2119
2296
  try {
2120
- const result = await this.load({
2121
- tableName: TABLE_WORKFLOW_SNAPSHOT,
2122
- keys: {
2123
- workflow_name: workflowName,
2124
- run_id: runId
2125
- }
2126
- });
2127
- if (!result) {
2128
- return null;
2297
+ const total = await this.client.oneOrNone(
2298
+ `SELECT COUNT(*) FROM ${TABLE_SCORERS} WHERE "scorerId" = $1`,
2299
+ [scorerId]
2300
+ );
2301
+ if (total?.count === "0" || !total?.count) {
2302
+ return {
2303
+ pagination: {
2304
+ total: 0,
2305
+ page: pagination.page,
2306
+ perPage: pagination.perPage,
2307
+ hasMore: false
2308
+ },
2309
+ scores: []
2310
+ };
2129
2311
  }
2130
- return result.snapshot;
2312
+ const result = await this.client.manyOrNone(
2313
+ `SELECT * FROM ${TABLE_SCORERS} WHERE "scorerId" = $1 LIMIT $2 OFFSET $3`,
2314
+ [scorerId, pagination.perPage, pagination.page * pagination.perPage]
2315
+ );
2316
+ return {
2317
+ pagination: {
2318
+ total: Number(total?.count) || 0,
2319
+ page: pagination.page,
2320
+ perPage: pagination.perPage,
2321
+ hasMore: Number(total?.count) > (pagination.page + 1) * pagination.perPage
2322
+ },
2323
+ scores: result
2324
+ };
2131
2325
  } catch (error) {
2132
2326
  throw new MastraError(
2133
2327
  {
2134
- id: "MASTRA_STORAGE_PG_STORE_LOAD_WORKFLOW_SNAPSHOT_FAILED",
2328
+ id: "MASTRA_STORAGE_PG_STORE_GET_SCORES_BY_SCORER_ID_FAILED",
2135
2329
  domain: ErrorDomain.STORAGE,
2136
- category: ErrorCategory.THIRD_PARTY,
2137
- details: {
2138
- workflowName,
2139
- runId
2140
- }
2330
+ category: ErrorCategory.THIRD_PARTY
2141
2331
  },
2142
2332
  error
2143
2333
  );
2144
2334
  }
2145
2335
  }
2146
- async hasColumn(table, column) {
2147
- const schema = this.schema || "public";
2148
- const result = await this.db.oneOrNone(
2149
- `SELECT 1 FROM information_schema.columns WHERE table_schema = $1 AND table_name = $2 AND (column_name = $3 OR column_name = $4)`,
2150
- [schema, table, column, column.toLowerCase()]
2151
- );
2152
- return !!result;
2153
- }
2154
- parseWorkflowRun(row) {
2155
- let parsedSnapshot = row.snapshot;
2156
- if (typeof parsedSnapshot === "string") {
2157
- try {
2158
- parsedSnapshot = JSON.parse(row.snapshot);
2159
- } catch (e) {
2160
- console.warn(`Failed to parse snapshot for workflow ${row.workflow_name}: ${e}`);
2161
- }
2162
- }
2163
- return {
2164
- workflowName: row.workflow_name,
2165
- runId: row.run_id,
2166
- snapshot: parsedSnapshot,
2167
- createdAt: row.createdAt,
2168
- updatedAt: row.updatedAt,
2169
- resourceId: row.resourceId
2170
- };
2171
- }
2172
- async getWorkflowRuns({
2173
- workflowName,
2174
- fromDate,
2175
- toDate,
2176
- limit,
2177
- offset,
2178
- resourceId
2179
- } = {}) {
2336
+ async saveScore(score) {
2180
2337
  try {
2181
- const conditions = [];
2182
- const values = [];
2183
- let paramIndex = 1;
2184
- if (workflowName) {
2185
- conditions.push(`workflow_name = $${paramIndex}`);
2186
- values.push(workflowName);
2187
- paramIndex++;
2188
- }
2189
- if (resourceId) {
2190
- const hasResourceId = await this.hasColumn(TABLE_WORKFLOW_SNAPSHOT, "resourceId");
2191
- if (hasResourceId) {
2192
- conditions.push(`"resourceId" = $${paramIndex}`);
2193
- values.push(resourceId);
2194
- paramIndex++;
2195
- } else {
2196
- console.warn(`[${TABLE_WORKFLOW_SNAPSHOT}] resourceId column not found. Skipping resourceId filter.`);
2338
+ const { input, ...rest } = score;
2339
+ await this.operations.insert({
2340
+ tableName: TABLE_SCORERS,
2341
+ record: {
2342
+ ...rest,
2343
+ input: JSON.stringify(input),
2344
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2345
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2197
2346
  }
2198
- }
2199
- if (fromDate) {
2200
- conditions.push(`"createdAt" >= $${paramIndex}`);
2201
- values.push(fromDate);
2202
- paramIndex++;
2203
- }
2204
- if (toDate) {
2205
- conditions.push(`"createdAt" <= $${paramIndex}`);
2206
- values.push(toDate);
2207
- paramIndex++;
2208
- }
2209
- const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
2210
- let total = 0;
2211
- if (limit !== void 0 && offset !== void 0) {
2212
- const countResult = await this.db.one(
2213
- `SELECT COUNT(*) as count FROM ${this.getTableName(TABLE_WORKFLOW_SNAPSHOT)} ${whereClause}`,
2214
- values
2215
- );
2216
- total = Number(countResult.count);
2217
- }
2218
- const query = `
2219
- SELECT * FROM ${this.getTableName(TABLE_WORKFLOW_SNAPSHOT)}
2220
- ${whereClause}
2221
- ORDER BY "createdAt" DESC
2222
- ${limit !== void 0 && offset !== void 0 ? ` LIMIT $${paramIndex} OFFSET $${paramIndex + 1}` : ""}
2223
- `;
2224
- const queryValues = limit !== void 0 && offset !== void 0 ? [...values, limit, offset] : values;
2225
- const result = await this.db.manyOrNone(query, queryValues);
2226
- const runs = (result || []).map((row) => {
2227
- return this.parseWorkflowRun(row);
2228
2347
  });
2229
- return { runs, total: total || runs.length };
2348
+ const scoreFromDb = await this.getScoreById({ id: score.id });
2349
+ return { score: scoreFromDb };
2230
2350
  } catch (error) {
2231
2351
  throw new MastraError(
2232
2352
  {
2233
- id: "MASTRA_STORAGE_PG_STORE_GET_WORKFLOW_RUNS_FAILED",
2353
+ id: "MASTRA_STORAGE_PG_STORE_SAVE_SCORE_FAILED",
2234
2354
  domain: ErrorDomain.STORAGE,
2235
- category: ErrorCategory.THIRD_PARTY,
2236
- details: {
2237
- workflowName: workflowName || "all"
2238
- }
2355
+ category: ErrorCategory.THIRD_PARTY
2239
2356
  },
2240
2357
  error
2241
2358
  );
2242
2359
  }
2243
2360
  }
2244
- async getWorkflowRunById({
2361
+ async getScoresByRunId({
2245
2362
  runId,
2246
- workflowName
2363
+ pagination
2247
2364
  }) {
2248
2365
  try {
2249
- const conditions = [];
2250
- const values = [];
2251
- let paramIndex = 1;
2252
- if (runId) {
2253
- conditions.push(`run_id = $${paramIndex}`);
2254
- values.push(runId);
2255
- paramIndex++;
2256
- }
2257
- if (workflowName) {
2258
- conditions.push(`workflow_name = $${paramIndex}`);
2259
- values.push(workflowName);
2260
- paramIndex++;
2261
- }
2262
- const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
2263
- const query = `
2264
- SELECT * FROM ${this.getTableName(TABLE_WORKFLOW_SNAPSHOT)}
2265
- ${whereClause}
2266
- `;
2267
- const queryValues = values;
2268
- const result = await this.db.oneOrNone(query, queryValues);
2269
- if (!result) {
2270
- return null;
2366
+ const total = await this.client.oneOrNone(
2367
+ `SELECT COUNT(*) FROM ${TABLE_SCORERS} WHERE "runId" = $1`,
2368
+ [runId]
2369
+ );
2370
+ console.log(`total: ${total?.count}`);
2371
+ console.log(`typeof total: ${typeof total?.count}`);
2372
+ if (total?.count === "0" || !total?.count) {
2373
+ return {
2374
+ pagination: {
2375
+ total: 0,
2376
+ page: pagination.page,
2377
+ perPage: pagination.perPage,
2378
+ hasMore: false
2379
+ },
2380
+ scores: []
2381
+ };
2271
2382
  }
2272
- return this.parseWorkflowRun(result);
2383
+ const result = await this.client.manyOrNone(
2384
+ `SELECT * FROM ${TABLE_SCORERS} WHERE "runId" = $1 LIMIT $2 OFFSET $3`,
2385
+ [runId, pagination.perPage, pagination.page * pagination.perPage]
2386
+ );
2387
+ return {
2388
+ pagination: {
2389
+ total: Number(total?.count) || 0,
2390
+ page: pagination.page,
2391
+ perPage: pagination.perPage,
2392
+ hasMore: Number(total?.count) > (pagination.page + 1) * pagination.perPage
2393
+ },
2394
+ scores: result
2395
+ };
2273
2396
  } catch (error) {
2274
2397
  throw new MastraError(
2275
2398
  {
2276
- id: "MASTRA_STORAGE_PG_STORE_GET_WORKFLOW_RUN_BY_ID_FAILED",
2399
+ id: "MASTRA_STORAGE_PG_STORE_GET_SCORES_BY_RUN_ID_FAILED",
2277
2400
  domain: ErrorDomain.STORAGE,
2278
- category: ErrorCategory.THIRD_PARTY,
2279
- details: {
2280
- runId,
2281
- workflowName: workflowName || ""
2282
- }
2401
+ category: ErrorCategory.THIRD_PARTY
2283
2402
  },
2284
2403
  error
2285
2404
  );
2286
2405
  }
2287
2406
  }
2288
- async close() {
2289
- this.pgp.end();
2290
- }
2291
- async getEvals(options = {}) {
2292
- const { agentName, type, page = 0, perPage = 100, dateRange } = options;
2293
- const fromDate = dateRange?.start;
2294
- const toDate = dateRange?.end;
2295
- const conditions = [];
2296
- const queryParams = [];
2297
- let paramIndex = 1;
2298
- if (agentName) {
2299
- conditions.push(`agent_name = $${paramIndex++}`);
2300
- queryParams.push(agentName);
2301
- }
2302
- if (type === "test") {
2303
- conditions.push(`(test_info IS NOT NULL AND test_info->>'testPath' IS NOT NULL)`);
2304
- } else if (type === "live") {
2305
- conditions.push(`(test_info IS NULL OR test_info->>'testPath' IS NULL)`);
2306
- }
2307
- if (fromDate) {
2308
- conditions.push(`created_at >= $${paramIndex++}`);
2309
- queryParams.push(fromDate);
2310
- }
2311
- if (toDate) {
2312
- conditions.push(`created_at <= $${paramIndex++}`);
2313
- queryParams.push(toDate);
2314
- }
2315
- const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
2316
- const countQuery = `SELECT COUNT(*) FROM ${this.getTableName(TABLE_EVALS)} ${whereClause}`;
2407
+ async getScoresByEntityId({
2408
+ entityId,
2409
+ entityType,
2410
+ pagination
2411
+ }) {
2317
2412
  try {
2318
- const countResult = await this.db.one(countQuery, queryParams);
2319
- const total = parseInt(countResult.count, 10);
2320
- const currentOffset = page * perPage;
2321
- if (total === 0) {
2413
+ const total = await this.client.oneOrNone(
2414
+ `SELECT COUNT(*) FROM ${TABLE_SCORERS} WHERE "entityId" = $1 AND "entityType" = $2`,
2415
+ [entityId, entityType]
2416
+ );
2417
+ if (total?.count === "0" || !total?.count) {
2322
2418
  return {
2323
- evals: [],
2324
- total: 0,
2325
- page,
2326
- perPage,
2327
- hasMore: false
2419
+ pagination: {
2420
+ total: 0,
2421
+ page: pagination.page,
2422
+ perPage: pagination.perPage,
2423
+ hasMore: false
2424
+ },
2425
+ scores: []
2328
2426
  };
2329
2427
  }
2330
- const dataQuery = `SELECT * FROM ${this.getTableName(
2331
- TABLE_EVALS
2332
- )} ${whereClause} ORDER BY created_at DESC LIMIT $${paramIndex++} OFFSET $${paramIndex++}`;
2333
- const rows = await this.db.manyOrNone(dataQuery, [...queryParams, perPage, currentOffset]);
2428
+ const result = await this.client.manyOrNone(
2429
+ `SELECT * FROM ${TABLE_SCORERS} WHERE "entityId" = $1 AND "entityType" = $2 LIMIT $3 OFFSET $4`,
2430
+ [entityId, entityType, pagination.perPage, pagination.page * pagination.perPage]
2431
+ );
2334
2432
  return {
2335
- evals: rows?.map((row) => this.transformEvalRow(row)) ?? [],
2336
- total,
2337
- page,
2338
- perPage,
2339
- hasMore: currentOffset + (rows?.length ?? 0) < total
2433
+ pagination: {
2434
+ total: Number(total?.count) || 0,
2435
+ page: pagination.page,
2436
+ perPage: pagination.perPage,
2437
+ hasMore: Number(total?.count) > (pagination.page + 1) * pagination.perPage
2438
+ },
2439
+ scores: result
2340
2440
  };
2341
2441
  } catch (error) {
2342
- const mastraError = new MastraError(
2442
+ throw new MastraError(
2343
2443
  {
2344
- id: "MASTRA_STORAGE_PG_STORE_GET_EVALS_FAILED",
2444
+ id: "MASTRA_STORAGE_PG_STORE_GET_SCORES_BY_ENTITY_ID_FAILED",
2345
2445
  domain: ErrorDomain.STORAGE,
2346
- category: ErrorCategory.THIRD_PARTY,
2347
- details: {
2348
- agentName: agentName || "all",
2349
- type: type || "all",
2350
- page,
2351
- perPage
2352
- }
2446
+ category: ErrorCategory.THIRD_PARTY
2353
2447
  },
2354
2448
  error
2355
2449
  );
2356
- this.logger?.error?.(mastraError.toString());
2357
- this.logger?.trackException(mastraError);
2358
- throw mastraError;
2359
2450
  }
2360
2451
  }
2361
- async updateMessages({
2362
- messages
2452
+ };
2453
+ var TracesPG = class extends TracesStorage {
2454
+ client;
2455
+ operations;
2456
+ schema;
2457
+ constructor({
2458
+ client,
2459
+ operations,
2460
+ schema
2363
2461
  }) {
2364
- if (messages.length === 0) {
2365
- return [];
2366
- }
2367
- const messageIds = messages.map((m) => m.id);
2368
- const selectQuery = `SELECT id, content, role, type, "createdAt", thread_id AS "threadId", "resourceId" FROM ${this.getTableName(
2369
- TABLE_MESSAGES
2370
- )} WHERE id IN ($1:list)`;
2371
- const existingMessagesDb = await this.db.manyOrNone(selectQuery, [messageIds]);
2372
- if (existingMessagesDb.length === 0) {
2373
- return [];
2462
+ super();
2463
+ this.client = client;
2464
+ this.operations = operations;
2465
+ this.schema = schema;
2466
+ }
2467
+ async getTraces(args) {
2468
+ if (args.fromDate || args.toDate) {
2469
+ args.dateRange = {
2470
+ start: args.fromDate,
2471
+ end: args.toDate
2472
+ };
2374
2473
  }
2375
- const existingMessages = existingMessagesDb.map((msg) => {
2376
- if (typeof msg.content === "string") {
2377
- try {
2378
- msg.content = JSON.parse(msg.content);
2379
- } catch {
2380
- }
2474
+ try {
2475
+ const result = await this.getTracesPaginated(args);
2476
+ return result.traces;
2477
+ } catch (error) {
2478
+ throw new MastraError(
2479
+ {
2480
+ id: "MASTRA_STORAGE_PG_STORE_GET_TRACES_FAILED",
2481
+ domain: ErrorDomain.STORAGE,
2482
+ category: ErrorCategory.THIRD_PARTY
2483
+ },
2484
+ error
2485
+ );
2486
+ }
2487
+ }
2488
+ async getTracesPaginated(args) {
2489
+ const { name, scope, page = 0, perPage = 100, attributes, filters, dateRange } = args;
2490
+ const fromDate = dateRange?.start;
2491
+ const toDate = dateRange?.end;
2492
+ const currentOffset = page * perPage;
2493
+ const queryParams = [];
2494
+ const conditions = [];
2495
+ let paramIndex = 1;
2496
+ if (name) {
2497
+ conditions.push(`name LIKE $${paramIndex++}`);
2498
+ queryParams.push(`${name}%`);
2499
+ }
2500
+ if (scope) {
2501
+ conditions.push(`scope = $${paramIndex++}`);
2502
+ queryParams.push(scope);
2503
+ }
2504
+ if (attributes) {
2505
+ Object.entries(attributes).forEach(([key, value]) => {
2506
+ const parsedKey = parseFieldKey(key);
2507
+ conditions.push(`attributes->>'${parsedKey}' = $${paramIndex++}`);
2508
+ queryParams.push(value);
2509
+ });
2510
+ }
2511
+ if (filters) {
2512
+ Object.entries(filters).forEach(([key, value]) => {
2513
+ const parsedKey = parseFieldKey(key);
2514
+ conditions.push(`"${parsedKey}" = $${paramIndex++}`);
2515
+ queryParams.push(value);
2516
+ });
2517
+ }
2518
+ if (fromDate) {
2519
+ conditions.push(`"createdAt" >= $${paramIndex++}`);
2520
+ queryParams.push(fromDate);
2521
+ }
2522
+ if (toDate) {
2523
+ conditions.push(`"createdAt" <= $${paramIndex++}`);
2524
+ queryParams.push(toDate);
2525
+ }
2526
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
2527
+ try {
2528
+ const countResult = await this.client.oneOrNone(
2529
+ `SELECT COUNT(*) FROM ${getTableName({ indexName: TABLE_TRACES, schemaName: getSchemaName(this.schema) })} ${whereClause}`,
2530
+ queryParams
2531
+ );
2532
+ const total = Number(countResult?.count ?? 0);
2533
+ if (total === 0) {
2534
+ return {
2535
+ traces: [],
2536
+ total: 0,
2537
+ page,
2538
+ perPage,
2539
+ hasMore: false
2540
+ };
2381
2541
  }
2382
- return msg;
2383
- });
2384
- const threadIdsToUpdate = /* @__PURE__ */ new Set();
2385
- await this.db.tx(async (t) => {
2386
- const queries = [];
2387
- const columnMapping = {
2388
- threadId: "thread_id"
2542
+ const dataResult = await this.client.manyOrNone(
2543
+ `SELECT * FROM ${getTableName({ indexName: TABLE_TRACES, schemaName: getSchemaName(this.schema) })} ${whereClause} ORDER BY "startTime" DESC LIMIT $${paramIndex++} OFFSET $${paramIndex++}`,
2544
+ [...queryParams, perPage, currentOffset]
2545
+ );
2546
+ const traces = dataResult.map((row) => ({
2547
+ id: row.id,
2548
+ parentSpanId: row.parentSpanId,
2549
+ traceId: row.traceId,
2550
+ name: row.name,
2551
+ scope: row.scope,
2552
+ kind: row.kind,
2553
+ status: safelyParseJSON(row.status),
2554
+ events: safelyParseJSON(row.events),
2555
+ links: safelyParseJSON(row.links),
2556
+ attributes: safelyParseJSON(row.attributes),
2557
+ startTime: row.startTime,
2558
+ endTime: row.endTime,
2559
+ other: safelyParseJSON(row.other),
2560
+ createdAt: row.createdAtZ || row.createdAt
2561
+ }));
2562
+ return {
2563
+ traces,
2564
+ total,
2565
+ page,
2566
+ perPage,
2567
+ hasMore: currentOffset + traces.length < total
2389
2568
  };
2390
- for (const existingMessage of existingMessages) {
2391
- const updatePayload = messages.find((m) => m.id === existingMessage.id);
2392
- if (!updatePayload) continue;
2393
- const { id, ...fieldsToUpdate } = updatePayload;
2394
- if (Object.keys(fieldsToUpdate).length === 0) continue;
2395
- threadIdsToUpdate.add(existingMessage.threadId);
2396
- if (updatePayload.threadId && updatePayload.threadId !== existingMessage.threadId) {
2397
- threadIdsToUpdate.add(updatePayload.threadId);
2398
- }
2399
- const setClauses = [];
2400
- const values = [];
2401
- let paramIndex = 1;
2402
- const updatableFields = { ...fieldsToUpdate };
2403
- if (updatableFields.content) {
2404
- const newContent = {
2405
- ...existingMessage.content,
2406
- ...updatableFields.content,
2407
- // Deep merge metadata if it exists on both
2408
- ...existingMessage.content?.metadata && updatableFields.content.metadata ? {
2409
- metadata: {
2410
- ...existingMessage.content.metadata,
2411
- ...updatableFields.content.metadata
2412
- }
2413
- } : {}
2414
- };
2415
- setClauses.push(`content = $${paramIndex++}`);
2416
- values.push(newContent);
2417
- delete updatableFields.content;
2418
- }
2419
- for (const key in updatableFields) {
2420
- if (Object.prototype.hasOwnProperty.call(updatableFields, key)) {
2421
- const dbColumn = columnMapping[key] || key;
2422
- setClauses.push(`"${dbColumn}" = $${paramIndex++}`);
2423
- values.push(updatableFields[key]);
2569
+ } catch (error) {
2570
+ throw new MastraError(
2571
+ {
2572
+ id: "MASTRA_STORAGE_PG_STORE_GET_TRACES_PAGINATED_FAILED",
2573
+ domain: ErrorDomain.STORAGE,
2574
+ category: ErrorCategory.THIRD_PARTY
2575
+ },
2576
+ error
2577
+ );
2578
+ }
2579
+ }
2580
+ async batchTraceInsert({ records }) {
2581
+ this.logger.debug("Batch inserting traces", { count: records.length });
2582
+ await this.operations.batchInsert({
2583
+ tableName: TABLE_TRACES,
2584
+ records
2585
+ });
2586
+ }
2587
+ };
2588
+ function parseWorkflowRun(row) {
2589
+ let parsedSnapshot = row.snapshot;
2590
+ if (typeof parsedSnapshot === "string") {
2591
+ try {
2592
+ parsedSnapshot = JSON.parse(row.snapshot);
2593
+ } catch (e) {
2594
+ console.warn(`Failed to parse snapshot for workflow ${row.workflow_name}: ${e}`);
2595
+ }
2596
+ }
2597
+ return {
2598
+ workflowName: row.workflow_name,
2599
+ runId: row.run_id,
2600
+ snapshot: parsedSnapshot,
2601
+ resourceId: row.resourceId,
2602
+ createdAt: new Date(row.createdAtZ || row.createdAt),
2603
+ updatedAt: new Date(row.updatedAtZ || row.updatedAt)
2604
+ };
2605
+ }
2606
+ var WorkflowsPG = class extends WorkflowsStorage {
2607
+ client;
2608
+ operations;
2609
+ schema;
2610
+ constructor({
2611
+ client,
2612
+ operations,
2613
+ schema
2614
+ }) {
2615
+ super();
2616
+ this.client = client;
2617
+ this.operations = operations;
2618
+ this.schema = schema;
2619
+ }
2620
+ async persistWorkflowSnapshot({
2621
+ workflowName,
2622
+ runId,
2623
+ snapshot
2624
+ }) {
2625
+ try {
2626
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2627
+ await this.client.none(
2628
+ `INSERT INTO ${TABLE_WORKFLOW_SNAPSHOT} (workflow_name, run_id, snapshot, "createdAt", "updatedAt")
2629
+ VALUES ($1, $2, $3, $4, $5)
2630
+ ON CONFLICT (workflow_name, run_id) DO UPDATE
2631
+ SET snapshot = $3, "updatedAt" = $5`,
2632
+ [workflowName, runId, JSON.stringify(snapshot), now, now]
2633
+ );
2634
+ } catch (error) {
2635
+ throw new MastraError(
2636
+ {
2637
+ id: "MASTRA_STORAGE_PG_STORE_PERSIST_WORKFLOW_SNAPSHOT_FAILED",
2638
+ domain: ErrorDomain.STORAGE,
2639
+ category: ErrorCategory.THIRD_PARTY
2640
+ },
2641
+ error
2642
+ );
2643
+ }
2644
+ }
2645
+ async loadWorkflowSnapshot({
2646
+ workflowName,
2647
+ runId
2648
+ }) {
2649
+ try {
2650
+ const result = await this.operations.load({
2651
+ tableName: TABLE_WORKFLOW_SNAPSHOT,
2652
+ keys: { workflow_name: workflowName, run_id: runId }
2653
+ });
2654
+ return result ? result.snapshot : null;
2655
+ } catch (error) {
2656
+ throw new MastraError(
2657
+ {
2658
+ id: "MASTRA_STORAGE_PG_STORE_LOAD_WORKFLOW_SNAPSHOT_FAILED",
2659
+ domain: ErrorDomain.STORAGE,
2660
+ category: ErrorCategory.THIRD_PARTY
2661
+ },
2662
+ error
2663
+ );
2664
+ }
2665
+ }
2666
+ async getWorkflowRunById({
2667
+ runId,
2668
+ workflowName
2669
+ }) {
2670
+ try {
2671
+ const conditions = [];
2672
+ const values = [];
2673
+ let paramIndex = 1;
2674
+ if (runId) {
2675
+ conditions.push(`run_id = $${paramIndex}`);
2676
+ values.push(runId);
2677
+ paramIndex++;
2678
+ }
2679
+ if (workflowName) {
2680
+ conditions.push(`workflow_name = $${paramIndex}`);
2681
+ values.push(workflowName);
2682
+ paramIndex++;
2683
+ }
2684
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
2685
+ const query = `
2686
+ SELECT * FROM ${getTableName({ indexName: TABLE_WORKFLOW_SNAPSHOT, schemaName: this.schema })}
2687
+ ${whereClause}
2688
+ `;
2689
+ const queryValues = values;
2690
+ const result = await this.client.oneOrNone(query, queryValues);
2691
+ if (!result) {
2692
+ return null;
2693
+ }
2694
+ return parseWorkflowRun(result);
2695
+ } catch (error) {
2696
+ throw new MastraError(
2697
+ {
2698
+ id: "MASTRA_STORAGE_PG_STORE_GET_WORKFLOW_RUN_BY_ID_FAILED",
2699
+ domain: ErrorDomain.STORAGE,
2700
+ category: ErrorCategory.THIRD_PARTY,
2701
+ details: {
2702
+ runId,
2703
+ workflowName: workflowName || ""
2424
2704
  }
2425
- }
2426
- if (setClauses.length > 0) {
2427
- values.push(id);
2428
- const sql = `UPDATE ${this.getTableName(
2429
- TABLE_MESSAGES
2430
- )} SET ${setClauses.join(", ")} WHERE id = $${paramIndex}`;
2431
- queries.push(t.none(sql, values));
2705
+ },
2706
+ error
2707
+ );
2708
+ }
2709
+ }
2710
+ async getWorkflowRuns({
2711
+ workflowName,
2712
+ fromDate,
2713
+ toDate,
2714
+ limit,
2715
+ offset,
2716
+ resourceId
2717
+ } = {}) {
2718
+ try {
2719
+ const conditions = [];
2720
+ const values = [];
2721
+ let paramIndex = 1;
2722
+ if (workflowName) {
2723
+ conditions.push(`workflow_name = $${paramIndex}`);
2724
+ values.push(workflowName);
2725
+ paramIndex++;
2726
+ }
2727
+ if (resourceId) {
2728
+ const hasResourceId = await this.operations.hasColumn(TABLE_WORKFLOW_SNAPSHOT, "resourceId");
2729
+ if (hasResourceId) {
2730
+ conditions.push(`"resourceId" = $${paramIndex}`);
2731
+ values.push(resourceId);
2732
+ paramIndex++;
2733
+ } else {
2734
+ console.warn(`[${TABLE_WORKFLOW_SNAPSHOT}] resourceId column not found. Skipping resourceId filter.`);
2432
2735
  }
2433
2736
  }
2434
- if (threadIdsToUpdate.size > 0) {
2435
- queries.push(
2436
- t.none(`UPDATE ${this.getTableName(TABLE_THREADS)} SET "updatedAt" = NOW() WHERE id IN ($1:list)`, [
2437
- Array.from(threadIdsToUpdate)
2438
- ])
2439
- );
2737
+ if (fromDate) {
2738
+ conditions.push(`"createdAt" >= $${paramIndex}`);
2739
+ values.push(fromDate);
2740
+ paramIndex++;
2440
2741
  }
2441
- if (queries.length > 0) {
2442
- await t.batch(queries);
2742
+ if (toDate) {
2743
+ conditions.push(`"createdAt" <= $${paramIndex}`);
2744
+ values.push(toDate);
2745
+ paramIndex++;
2443
2746
  }
2444
- });
2445
- const updatedMessages = await this.db.manyOrNone(selectQuery, [messageIds]);
2446
- return (updatedMessages || []).map((message) => {
2447
- if (typeof message.content === "string") {
2448
- try {
2449
- message.content = JSON.parse(message.content);
2450
- } catch {
2451
- }
2747
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
2748
+ let total = 0;
2749
+ if (limit !== void 0 && offset !== void 0) {
2750
+ const countResult = await this.client.one(
2751
+ `SELECT COUNT(*) as count FROM ${getTableName({ indexName: TABLE_WORKFLOW_SNAPSHOT, schemaName: this.schema })} ${whereClause}`,
2752
+ values
2753
+ );
2754
+ total = Number(countResult.count);
2452
2755
  }
2453
- return message;
2454
- });
2756
+ const query = `
2757
+ SELECT * FROM ${getTableName({ indexName: TABLE_WORKFLOW_SNAPSHOT, schemaName: this.schema })}
2758
+ ${whereClause}
2759
+ ORDER BY "createdAt" DESC
2760
+ ${limit !== void 0 && offset !== void 0 ? ` LIMIT $${paramIndex} OFFSET $${paramIndex + 1}` : ""}
2761
+ `;
2762
+ const queryValues = limit !== void 0 && offset !== void 0 ? [...values, limit, offset] : values;
2763
+ const result = await this.client.manyOrNone(query, queryValues);
2764
+ const runs = (result || []).map((row) => {
2765
+ return parseWorkflowRun(row);
2766
+ });
2767
+ return { runs, total: total || runs.length };
2768
+ } catch (error) {
2769
+ throw new MastraError(
2770
+ {
2771
+ id: "MASTRA_STORAGE_PG_STORE_GET_WORKFLOW_RUNS_FAILED",
2772
+ domain: ErrorDomain.STORAGE,
2773
+ category: ErrorCategory.THIRD_PARTY,
2774
+ details: {
2775
+ workflowName: workflowName || "all"
2776
+ }
2777
+ },
2778
+ error
2779
+ );
2780
+ }
2455
2781
  }
2456
- async getResourceById({ resourceId }) {
2457
- const tableName = this.getTableName(TABLE_RESOURCES);
2458
- const result = await this.db.oneOrNone(`SELECT * FROM ${tableName} WHERE id = $1`, [
2459
- resourceId
2460
- ]);
2461
- if (!result) {
2462
- return null;
2782
+ };
2783
+
2784
+ // src/storage/index.ts
2785
+ var PostgresStore = class extends MastraStorage {
2786
+ db;
2787
+ pgp;
2788
+ client;
2789
+ schema;
2790
+ stores;
2791
+ constructor(config) {
2792
+ try {
2793
+ if ("connectionString" in config) {
2794
+ if (!config.connectionString || typeof config.connectionString !== "string" || config.connectionString.trim() === "") {
2795
+ throw new Error(
2796
+ "PostgresStore: connectionString must be provided and cannot be empty. Passing an empty string may cause fallback to local Postgres defaults."
2797
+ );
2798
+ }
2799
+ } else {
2800
+ const required = ["host", "database", "user", "password"];
2801
+ for (const key of required) {
2802
+ if (!(key in config) || typeof config[key] !== "string" || config[key].trim() === "") {
2803
+ throw new Error(
2804
+ `PostgresStore: ${key} must be provided and cannot be empty. Passing an empty string may cause fallback to local Postgres defaults.`
2805
+ );
2806
+ }
2807
+ }
2808
+ }
2809
+ super({ name: "PostgresStore" });
2810
+ this.pgp = pgPromise();
2811
+ this.schema = config.schemaName || "public";
2812
+ this.db = this.pgp(
2813
+ `connectionString` in config ? { connectionString: config.connectionString } : {
2814
+ host: config.host,
2815
+ port: config.port,
2816
+ database: config.database,
2817
+ user: config.user,
2818
+ password: config.password,
2819
+ ssl: config.ssl
2820
+ }
2821
+ );
2822
+ this.client = this.db;
2823
+ const operations = new StoreOperationsPG({ client: this.client, schemaName: this.schema });
2824
+ const scores = new ScoresPG({ client: this.client, operations });
2825
+ const traces = new TracesPG({ client: this.client, operations, schema: this.schema });
2826
+ const workflows = new WorkflowsPG({ client: this.client, operations, schema: this.schema });
2827
+ const legacyEvals = new LegacyEvalsPG({ client: this.client, schema: this.schema });
2828
+ const memory = new MemoryPG({ client: this.client, schema: this.schema, operations });
2829
+ this.stores = {
2830
+ operations,
2831
+ scores,
2832
+ traces,
2833
+ workflows,
2834
+ legacyEvals,
2835
+ memory
2836
+ };
2837
+ } catch (e) {
2838
+ throw new MastraError(
2839
+ {
2840
+ id: "MASTRA_STORAGE_PG_STORE_INITIALIZATION_FAILED",
2841
+ domain: ErrorDomain.STORAGE,
2842
+ category: ErrorCategory.USER
2843
+ },
2844
+ e
2845
+ );
2463
2846
  }
2847
+ }
2848
+ get supports() {
2464
2849
  return {
2465
- ...result,
2466
- // Ensure workingMemory is always returned as a string, regardless of automatic parsing
2467
- workingMemory: typeof result.workingMemory === "object" ? JSON.stringify(result.workingMemory) : result.workingMemory,
2468
- metadata: typeof result.metadata === "string" ? JSON.parse(result.metadata) : result.metadata
2850
+ selectByIncludeResourceScope: true,
2851
+ resourceWorkingMemory: true,
2852
+ hasColumn: true,
2853
+ createTable: true
2469
2854
  };
2470
2855
  }
2856
+ /** @deprecated use getEvals instead */
2857
+ async getEvalsByAgentName(agentName, type) {
2858
+ return this.stores.legacyEvals.getEvalsByAgentName(agentName, type);
2859
+ }
2860
+ async getEvals(options = {}) {
2861
+ return this.stores.legacyEvals.getEvals(options);
2862
+ }
2863
+ /**
2864
+ * @deprecated use getTracesPaginated instead
2865
+ */
2866
+ async getTraces(args) {
2867
+ return this.stores.traces.getTraces(args);
2868
+ }
2869
+ async getTracesPaginated(args) {
2870
+ return this.stores.traces.getTracesPaginated(args);
2871
+ }
2872
+ async batchTraceInsert({ records }) {
2873
+ return this.stores.traces.batchTraceInsert({ records });
2874
+ }
2875
+ async createTable({
2876
+ tableName,
2877
+ schema
2878
+ }) {
2879
+ return this.stores.operations.createTable({ tableName, schema });
2880
+ }
2881
+ async alterTable({
2882
+ tableName,
2883
+ schema,
2884
+ ifNotExists
2885
+ }) {
2886
+ return this.stores.operations.alterTable({ tableName, schema, ifNotExists });
2887
+ }
2888
+ async clearTable({ tableName }) {
2889
+ return this.stores.operations.clearTable({ tableName });
2890
+ }
2891
+ async dropTable({ tableName }) {
2892
+ return this.stores.operations.dropTable({ tableName });
2893
+ }
2894
+ async insert({ tableName, record }) {
2895
+ return this.stores.operations.insert({ tableName, record });
2896
+ }
2897
+ async batchInsert({ tableName, records }) {
2898
+ return this.stores.operations.batchInsert({ tableName, records });
2899
+ }
2900
+ async load({ tableName, keys }) {
2901
+ return this.stores.operations.load({ tableName, keys });
2902
+ }
2903
+ /**
2904
+ * Memory
2905
+ */
2906
+ async getThreadById({ threadId }) {
2907
+ return this.stores.memory.getThreadById({ threadId });
2908
+ }
2909
+ /**
2910
+ * @deprecated use getThreadsByResourceIdPaginated instead
2911
+ */
2912
+ async getThreadsByResourceId(args) {
2913
+ return this.stores.memory.getThreadsByResourceId(args);
2914
+ }
2915
+ async getThreadsByResourceIdPaginated(args) {
2916
+ return this.stores.memory.getThreadsByResourceIdPaginated(args);
2917
+ }
2918
+ async saveThread({ thread }) {
2919
+ return this.stores.memory.saveThread({ thread });
2920
+ }
2921
+ async updateThread({
2922
+ id,
2923
+ title,
2924
+ metadata
2925
+ }) {
2926
+ return this.stores.memory.updateThread({ id, title, metadata });
2927
+ }
2928
+ async deleteThread({ threadId }) {
2929
+ return this.stores.memory.deleteThread({ threadId });
2930
+ }
2931
+ async getMessages(args) {
2932
+ return this.stores.memory.getMessages(args);
2933
+ }
2934
+ async getMessagesPaginated(args) {
2935
+ return this.stores.memory.getMessagesPaginated(args);
2936
+ }
2937
+ async saveMessages(args) {
2938
+ return this.stores.memory.saveMessages(args);
2939
+ }
2940
+ async updateMessages({
2941
+ messages
2942
+ }) {
2943
+ return this.stores.memory.updateMessages({ messages });
2944
+ }
2945
+ async getResourceById({ resourceId }) {
2946
+ return this.stores.memory.getResourceById({ resourceId });
2947
+ }
2471
2948
  async saveResource({ resource }) {
2472
- const tableName = this.getTableName(TABLE_RESOURCES);
2473
- await this.db.none(
2474
- `INSERT INTO ${tableName} (id, "workingMemory", metadata, "createdAt", "updatedAt")
2475
- VALUES ($1, $2, $3, $4, $5)`,
2476
- [
2477
- resource.id,
2478
- resource.workingMemory,
2479
- JSON.stringify(resource.metadata),
2480
- resource.createdAt.toISOString(),
2481
- resource.updatedAt.toISOString()
2482
- ]
2483
- );
2484
- return resource;
2949
+ return this.stores.memory.saveResource({ resource });
2485
2950
  }
2486
2951
  async updateResource({
2487
2952
  resourceId,
2488
2953
  workingMemory,
2489
2954
  metadata
2490
2955
  }) {
2491
- const existingResource = await this.getResourceById({ resourceId });
2492
- if (!existingResource) {
2493
- const newResource = {
2494
- id: resourceId,
2495
- workingMemory,
2496
- metadata: metadata || {},
2497
- createdAt: /* @__PURE__ */ new Date(),
2498
- updatedAt: /* @__PURE__ */ new Date()
2499
- };
2500
- return this.saveResource({ resource: newResource });
2501
- }
2502
- const updatedResource = {
2503
- ...existingResource,
2504
- workingMemory: workingMemory !== void 0 ? workingMemory : existingResource.workingMemory,
2505
- metadata: {
2506
- ...existingResource.metadata,
2507
- ...metadata
2508
- },
2509
- updatedAt: /* @__PURE__ */ new Date()
2510
- };
2511
- const tableName = this.getTableName(TABLE_RESOURCES);
2512
- const updates = [];
2513
- const values = [];
2514
- let paramIndex = 1;
2515
- if (workingMemory !== void 0) {
2516
- updates.push(`"workingMemory" = $${paramIndex}`);
2517
- values.push(workingMemory);
2518
- paramIndex++;
2519
- }
2520
- if (metadata) {
2521
- updates.push(`metadata = $${paramIndex}`);
2522
- values.push(JSON.stringify(updatedResource.metadata));
2523
- paramIndex++;
2524
- }
2525
- updates.push(`"updatedAt" = $${paramIndex}`);
2526
- values.push(updatedResource.updatedAt.toISOString());
2527
- paramIndex++;
2528
- values.push(resourceId);
2529
- await this.db.none(`UPDATE ${tableName} SET ${updates.join(", ")} WHERE id = $${paramIndex}`, values);
2530
- return updatedResource;
2956
+ return this.stores.memory.updateResource({ resourceId, workingMemory, metadata });
2957
+ }
2958
+ /**
2959
+ * Workflows
2960
+ */
2961
+ async persistWorkflowSnapshot({
2962
+ workflowName,
2963
+ runId,
2964
+ snapshot
2965
+ }) {
2966
+ return this.stores.workflows.persistWorkflowSnapshot({ workflowName, runId, snapshot });
2967
+ }
2968
+ async loadWorkflowSnapshot({
2969
+ workflowName,
2970
+ runId
2971
+ }) {
2972
+ return this.stores.workflows.loadWorkflowSnapshot({ workflowName, runId });
2973
+ }
2974
+ async getWorkflowRuns({
2975
+ workflowName,
2976
+ fromDate,
2977
+ toDate,
2978
+ limit,
2979
+ offset,
2980
+ resourceId
2981
+ } = {}) {
2982
+ return this.stores.workflows.getWorkflowRuns({ workflowName, fromDate, toDate, limit, offset, resourceId });
2983
+ }
2984
+ async getWorkflowRunById({
2985
+ runId,
2986
+ workflowName
2987
+ }) {
2988
+ return this.stores.workflows.getWorkflowRunById({ runId, workflowName });
2989
+ }
2990
+ async close() {
2991
+ this.pgp.end();
2992
+ }
2993
+ /**
2994
+ * Scorers
2995
+ */
2996
+ async getScoreById({ id: _id }) {
2997
+ return this.stores.scores.getScoreById({ id: _id });
2998
+ }
2999
+ async getScoresByScorerId({
3000
+ scorerId: _scorerId,
3001
+ pagination: _pagination
3002
+ }) {
3003
+ return this.stores.scores.getScoresByScorerId({ scorerId: _scorerId, pagination: _pagination });
3004
+ }
3005
+ async saveScore(_score) {
3006
+ return this.stores.scores.saveScore(_score);
3007
+ }
3008
+ async getScoresByRunId({
3009
+ runId: _runId,
3010
+ pagination: _pagination
3011
+ }) {
3012
+ return this.stores.scores.getScoresByRunId({ runId: _runId, pagination: _pagination });
3013
+ }
3014
+ async getScoresByEntityId({
3015
+ entityId: _entityId,
3016
+ entityType: _entityType,
3017
+ pagination: _pagination
3018
+ }) {
3019
+ return this.stores.scores.getScoresByEntityId({
3020
+ entityId: _entityId,
3021
+ entityType: _entityType,
3022
+ pagination: _pagination
3023
+ });
2531
3024
  }
2532
3025
  };
2533
3026