@mastra/pg 0.17.8 → 0.17.9

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,7 +5,7 @@ import { Mutex } from 'async-mutex';
5
5
  import * as pg from 'pg';
6
6
  import xxhash from 'xxhash-wasm';
7
7
  import { BaseFilterTranslator } from '@mastra/core/vector/filter';
8
- import { MastraStorage, StoreOperations, TABLE_SCHEMAS, TABLE_WORKFLOW_SNAPSHOT, TABLE_AI_SPANS, TABLE_THREADS, TABLE_MESSAGES, TABLE_TRACES, TABLE_EVALS, TABLE_SCORERS, ScoresStorage, TracesStorage, safelyParseJSON, WorkflowsStorage, LegacyEvalsStorage, MemoryStorage, resolveMessageLimit, TABLE_RESOURCES, ObservabilityStorage } from '@mastra/core/storage';
8
+ import { MastraStorage, StoreOperations, TABLE_SCHEMAS, TABLE_WORKFLOW_SNAPSHOT, TABLE_AI_SPANS, TABLE_THREADS, TABLE_MESSAGES, TABLE_TRACES, TABLE_EVALS, TABLE_SCORERS, ScoresStorage, TracesStorage, safelyParseJSON, WorkflowsStorage, LegacyEvalsStorage, MemoryStorage, resolveMessageLimit, TABLE_RESOURCES, ObservabilityStorage, SCORERS_SCHEMA } from '@mastra/core/storage';
9
9
  import pgPromise from 'pg-promise';
10
10
  import { MessageList } from '@mastra/core/agent';
11
11
  import { saveScorePayloadSchema } from '@mastra/core/scores';
@@ -123,12 +123,20 @@ var createBasicOperator = (symbol) => {
123
123
  };
124
124
  };
125
125
  var createNumericOperator = (symbol) => {
126
- return (key, paramIndex) => {
126
+ return (key, paramIndex, value) => {
127
127
  const jsonPathKey = parseJsonPathKey(key);
128
- return {
129
- sql: `(metadata#>>'{${jsonPathKey}}')::numeric ${symbol} $${paramIndex}`,
130
- needsValue: true
131
- };
128
+ const isNumeric = typeof value === "number" || typeof value === "string" && !isNaN(Number(value)) && value.trim() !== "";
129
+ if (isNumeric) {
130
+ return {
131
+ sql: `(metadata#>>'{${jsonPathKey}}')::numeric ${symbol} $${paramIndex}::numeric`,
132
+ needsValue: true
133
+ };
134
+ } else {
135
+ return {
136
+ sql: `metadata#>>'{${jsonPathKey}}' ${symbol} $${paramIndex}::text`,
137
+ needsValue: true
138
+ };
139
+ }
132
140
  };
133
141
  };
134
142
  function buildElemMatchConditions(value, paramIndex) {
@@ -309,6 +317,83 @@ var parseJsonPathKey = (key) => {
309
317
  function escapeLikePattern(str) {
310
318
  return str.replace(/([%_\\])/g, "\\$1");
311
319
  }
320
+ function buildDeleteFilterQuery(filter) {
321
+ const values = [];
322
+ function buildCondition(key, value, parentPath) {
323
+ if (["$and", "$or", "$not", "$nor"].includes(key)) {
324
+ return handleLogicalOperator(key, value);
325
+ }
326
+ if (!value || typeof value !== "object") {
327
+ values.push(value);
328
+ return `metadata#>>'{${parseJsonPathKey(key)}}' = $${values.length}`;
329
+ }
330
+ const [[operator, operatorValue] = []] = Object.entries(value);
331
+ if (operator === "$not") {
332
+ const entries = Object.entries(operatorValue);
333
+ const conditions2 = entries.map(([nestedOp, nestedValue]) => {
334
+ if (!FILTER_OPERATORS[nestedOp]) {
335
+ throw new Error(`Invalid operator in $not condition: ${nestedOp}`);
336
+ }
337
+ const operatorFn2 = FILTER_OPERATORS[nestedOp];
338
+ const operatorResult2 = operatorFn2(key, values.length + 1, nestedValue);
339
+ if (operatorResult2.needsValue) {
340
+ values.push(nestedValue);
341
+ }
342
+ return operatorResult2.sql;
343
+ }).join(" AND ");
344
+ return `NOT (${conditions2})`;
345
+ }
346
+ const operatorFn = FILTER_OPERATORS[operator];
347
+ const operatorResult = operatorFn(key, values.length + 1, operatorValue);
348
+ if (operatorResult.needsValue) {
349
+ const transformedValue = operatorResult.transformValue ? operatorResult.transformValue() : operatorValue;
350
+ if (Array.isArray(transformedValue) && operator === "$elemMatch") {
351
+ values.push(...transformedValue);
352
+ } else {
353
+ values.push(transformedValue);
354
+ }
355
+ }
356
+ return operatorResult.sql;
357
+ }
358
+ function handleLogicalOperator(key, value, parentPath) {
359
+ if (key === "$not") {
360
+ const entries = Object.entries(value);
361
+ const conditions3 = entries.map(([fieldKey, fieldValue]) => buildCondition(fieldKey, fieldValue)).join(" AND ");
362
+ return `NOT (${conditions3})`;
363
+ }
364
+ if (!value || value.length === 0) {
365
+ switch (key) {
366
+ case "$and":
367
+ case "$nor":
368
+ return "true";
369
+ // Empty $and/$nor match everything
370
+ case "$or":
371
+ return "false";
372
+ // Empty $or matches nothing
373
+ default:
374
+ return "true";
375
+ }
376
+ }
377
+ const joinOperator = key === "$or" || key === "$nor" ? "OR" : "AND";
378
+ const conditions2 = value.map((f) => {
379
+ const entries = Object.entries(f || {});
380
+ if (entries.length === 0) return "";
381
+ const [firstKey, firstValue] = entries[0] || [];
382
+ if (["$and", "$or", "$not", "$nor"].includes(firstKey)) {
383
+ return buildCondition(firstKey, firstValue);
384
+ }
385
+ return entries.map(([k, v]) => buildCondition(k, v)).join(` ${joinOperator} `);
386
+ });
387
+ const joined = conditions2.join(` ${joinOperator} `);
388
+ const operatorFn = FILTER_OPERATORS[key];
389
+ return operatorFn(joined, 0, value).sql;
390
+ }
391
+ if (!filter) {
392
+ return { sql: "", values };
393
+ }
394
+ const conditions = Object.entries(filter).map(([key, value]) => buildCondition(key, value)).filter(Boolean).join(" AND ");
395
+ return { sql: conditions ? `WHERE ${conditions}` : "", values };
396
+ }
312
397
  function buildFilterQuery(filter, minScore, topK) {
313
398
  const values = [minScore, topK];
314
399
  function buildCondition(key, value, parentPath) {
@@ -637,11 +722,31 @@ var PgVector = class extends MastraVector {
637
722
  client.release();
638
723
  }
639
724
  }
640
- async upsert({ indexName, vectors, metadata, ids }) {
725
+ async upsert({
726
+ indexName,
727
+ vectors,
728
+ metadata,
729
+ ids,
730
+ deleteFilter
731
+ }) {
641
732
  const { tableName } = this.getTableName(indexName);
642
733
  const client = await this.pool.connect();
643
734
  try {
644
735
  await client.query("BEGIN");
736
+ if (deleteFilter) {
737
+ this.logger?.debug(`Deleting vectors matching filter before upsert`, { indexName, deleteFilter });
738
+ const translatedFilter = this.transformFilter(deleteFilter);
739
+ const { sql: filterQuery, values: filterValues } = buildDeleteFilterQuery(translatedFilter);
740
+ const whereClause = filterQuery.trim().replace(/^WHERE\s+/i, "");
741
+ if (whereClause) {
742
+ const deleteQuery = `DELETE FROM ${tableName} WHERE ${whereClause}`;
743
+ const result = await client.query(deleteQuery, filterValues);
744
+ this.logger?.debug(`Deleted ${result.rowCount || 0} vectors before upsert`, {
745
+ indexName,
746
+ deletedCount: result.rowCount || 0
747
+ });
748
+ }
749
+ }
645
750
  const vectorIds = ids || vectors.map(() => crypto.randomUUID());
646
751
  const vectorType = this.getVectorTypeName();
647
752
  for (let i = 0; i < vectors.length; i++) {
@@ -657,6 +762,11 @@ var PgVector = class extends MastraVector {
657
762
  await client.query(query, [vectorIds[i], `[${vectors[i]?.join(",")}]`, JSON.stringify(metadata?.[i] || {})]);
658
763
  }
659
764
  await client.query("COMMIT");
765
+ this.logger?.debug(`Upserted ${vectors.length} vectors to ${indexName}`, {
766
+ indexName,
767
+ vectorCount: vectors.length,
768
+ hadDeleteFilter: !!deleteFilter
769
+ });
660
770
  return vectorIds;
661
771
  } catch (error) {
662
772
  await client.query("ROLLBACK");
@@ -1214,17 +1324,36 @@ var PgVector = class extends MastraVector {
1214
1324
  * @returns A promise that resolves when the update is complete.
1215
1325
  * @throws Will throw an error if no updates are provided or if the update operation fails.
1216
1326
  */
1217
- async updateVector({ indexName, id, update }) {
1327
+ async updateVector({ indexName, id, filter, update }) {
1218
1328
  let client;
1219
1329
  try {
1220
1330
  if (!update.vector && !update.metadata) {
1221
1331
  throw new Error("No updates provided");
1222
1332
  }
1333
+ if (!id && !filter) {
1334
+ throw new MastraError({
1335
+ id: "MASTRA_STORAGE_PG_VECTOR_UPDATE_MISSING_PARAMS",
1336
+ text: "Either id or filter must be provided",
1337
+ domain: ErrorDomain.MASTRA_VECTOR,
1338
+ category: ErrorCategory.USER,
1339
+ details: { indexName }
1340
+ });
1341
+ }
1342
+ if (id && filter) {
1343
+ throw new MastraError({
1344
+ id: "MASTRA_STORAGE_PG_VECTOR_UPDATE_CONFLICTING_PARAMS",
1345
+ text: "Cannot provide both id and filter - they are mutually exclusive",
1346
+ domain: ErrorDomain.MASTRA_VECTOR,
1347
+ category: ErrorCategory.USER,
1348
+ details: { indexName }
1349
+ });
1350
+ }
1223
1351
  client = await this.pool.connect();
1224
- let updateParts = [];
1225
- let values = [id];
1226
- let valueIndex = 2;
1352
+ const { tableName } = this.getTableName(indexName);
1227
1353
  const vectorType = this.getVectorTypeName();
1354
+ let updateParts = [];
1355
+ let values = [];
1356
+ let valueIndex = 1;
1228
1357
  if (update.vector) {
1229
1358
  updateParts.push(`embedding = $${valueIndex}::${vectorType}`);
1230
1359
  values.push(`[${update.vector.join(",")}]`);
@@ -1233,18 +1362,60 @@ var PgVector = class extends MastraVector {
1233
1362
  if (update.metadata) {
1234
1363
  updateParts.push(`metadata = $${valueIndex}::jsonb`);
1235
1364
  values.push(JSON.stringify(update.metadata));
1365
+ valueIndex++;
1236
1366
  }
1237
1367
  if (updateParts.length === 0) {
1238
1368
  return;
1239
1369
  }
1240
- const { tableName } = this.getTableName(indexName);
1370
+ let whereClause;
1371
+ let whereValues;
1372
+ if (id) {
1373
+ whereClause = `vector_id = $${valueIndex}`;
1374
+ whereValues = [id];
1375
+ } else {
1376
+ if (!filter || Object.keys(filter).length === 0) {
1377
+ throw new MastraError({
1378
+ id: "MASTRA_STORAGE_PG_VECTOR_UPDATE_EMPTY_FILTER",
1379
+ text: "Cannot update with empty filter",
1380
+ domain: ErrorDomain.MASTRA_VECTOR,
1381
+ category: ErrorCategory.USER,
1382
+ details: { indexName }
1383
+ });
1384
+ }
1385
+ const translatedFilter = this.transformFilter(filter);
1386
+ const { sql: filterQuery, values: filterValues } = buildDeleteFilterQuery(translatedFilter);
1387
+ whereClause = filterQuery.trim().replace(/^WHERE\s+/i, "");
1388
+ if (!whereClause) {
1389
+ throw new MastraError({
1390
+ id: "MASTRA_STORAGE_PG_VECTOR_UPDATE_INVALID_FILTER",
1391
+ text: "Filter produced empty WHERE clause",
1392
+ domain: ErrorDomain.MASTRA_VECTOR,
1393
+ category: ErrorCategory.USER,
1394
+ details: { indexName, filter: JSON.stringify(filter) }
1395
+ });
1396
+ }
1397
+ whereClause = whereClause.replace(/\$(\d+)/g, (match, num) => {
1398
+ const newIndex = parseInt(num) + valueIndex - 1;
1399
+ return `$${newIndex}`;
1400
+ });
1401
+ whereValues = filterValues;
1402
+ }
1241
1403
  const query = `
1242
1404
  UPDATE ${tableName}
1243
1405
  SET ${updateParts.join(", ")}
1244
- WHERE vector_id = $1
1406
+ WHERE ${whereClause}
1245
1407
  `;
1246
- await client.query(query, values);
1408
+ const result = await client.query(query, [...values, ...whereValues]);
1409
+ this.logger?.info(`Updated ${result.rowCount || 0} vectors in ${indexName}`, {
1410
+ indexName,
1411
+ id: id ? id : void 0,
1412
+ filter: filter ? filter : void 0,
1413
+ updatedCount: result.rowCount || 0
1414
+ });
1247
1415
  } catch (error) {
1416
+ if (error instanceof MastraError) {
1417
+ throw error;
1418
+ }
1248
1419
  const mastraError = new MastraError(
1249
1420
  {
1250
1421
  id: "MASTRA_STORAGE_PG_VECTOR_UPDATE_VECTOR_FAILED",
@@ -1252,7 +1423,8 @@ var PgVector = class extends MastraVector {
1252
1423
  category: ErrorCategory.THIRD_PARTY,
1253
1424
  details: {
1254
1425
  indexName,
1255
- id
1426
+ ...id && { id },
1427
+ ...filter && { filter: JSON.stringify(filter) }
1256
1428
  }
1257
1429
  },
1258
1430
  error
@@ -1299,6 +1471,106 @@ var PgVector = class extends MastraVector {
1299
1471
  client?.release();
1300
1472
  }
1301
1473
  }
1474
+ /**
1475
+ * Delete vectors matching a metadata filter.
1476
+ * @param indexName - The name of the index containing the vectors.
1477
+ * @param filter - The filter to match vectors for deletion.
1478
+ * @returns A promise that resolves when the deletion is complete.
1479
+ * @throws Will throw an error if the deletion operation fails.
1480
+ */
1481
+ async deleteVectors({ indexName, filter, ids }) {
1482
+ let client;
1483
+ try {
1484
+ client = await this.pool.connect();
1485
+ const { tableName } = this.getTableName(indexName);
1486
+ if (!filter && !ids) {
1487
+ throw new MastraError({
1488
+ id: "MASTRA_STORAGE_PG_VECTOR_DELETE_MISSING_PARAMS",
1489
+ text: "Either filter or ids must be provided",
1490
+ domain: ErrorDomain.MASTRA_VECTOR,
1491
+ category: ErrorCategory.USER,
1492
+ details: { indexName }
1493
+ });
1494
+ }
1495
+ if (filter && ids) {
1496
+ throw new MastraError({
1497
+ id: "MASTRA_STORAGE_PG_VECTOR_DELETE_CONFLICTING_PARAMS",
1498
+ text: "Cannot provide both filter and ids - they are mutually exclusive",
1499
+ domain: ErrorDomain.MASTRA_VECTOR,
1500
+ category: ErrorCategory.USER,
1501
+ details: { indexName }
1502
+ });
1503
+ }
1504
+ let query;
1505
+ let values;
1506
+ if (ids) {
1507
+ if (ids.length === 0) {
1508
+ throw new MastraError({
1509
+ id: "MASTRA_STORAGE_PG_VECTOR_DELETE_EMPTY_IDS",
1510
+ text: "Cannot delete with empty ids array",
1511
+ domain: ErrorDomain.MASTRA_VECTOR,
1512
+ category: ErrorCategory.USER,
1513
+ details: { indexName }
1514
+ });
1515
+ }
1516
+ const placeholders = ids.map((_, i) => `$${i + 1}`).join(", ");
1517
+ query = `DELETE FROM ${tableName} WHERE vector_id IN (${placeholders})`;
1518
+ values = ids;
1519
+ } else {
1520
+ if (!filter || Object.keys(filter).length === 0) {
1521
+ throw new MastraError({
1522
+ id: "MASTRA_STORAGE_PG_VECTOR_DELETE_EMPTY_FILTER",
1523
+ text: "Cannot delete with empty filter. Use deleteIndex to delete all vectors.",
1524
+ domain: ErrorDomain.MASTRA_VECTOR,
1525
+ category: ErrorCategory.USER,
1526
+ details: { indexName }
1527
+ });
1528
+ }
1529
+ const translatedFilter = this.transformFilter(filter);
1530
+ const { sql: filterQuery, values: filterValues } = buildDeleteFilterQuery(translatedFilter);
1531
+ const whereClause = filterQuery.trim().replace(/^WHERE\s+/i, "");
1532
+ if (!whereClause) {
1533
+ throw new MastraError({
1534
+ id: "MASTRA_STORAGE_PG_VECTOR_DELETE_INVALID_FILTER",
1535
+ text: "Filter produced empty WHERE clause",
1536
+ domain: ErrorDomain.MASTRA_VECTOR,
1537
+ category: ErrorCategory.USER,
1538
+ details: { indexName, filter: JSON.stringify(filter) }
1539
+ });
1540
+ }
1541
+ query = `DELETE FROM ${tableName} WHERE ${whereClause}`;
1542
+ values = filterValues;
1543
+ }
1544
+ const result = await client.query(query, values);
1545
+ this.logger?.info(`Deleted ${result.rowCount || 0} vectors from ${indexName}`, {
1546
+ indexName,
1547
+ filter: filter ? filter : void 0,
1548
+ ids: ids ? ids : void 0,
1549
+ deletedCount: result.rowCount || 0
1550
+ });
1551
+ } catch (error) {
1552
+ if (error instanceof MastraError) {
1553
+ throw error;
1554
+ }
1555
+ const mastraError = new MastraError(
1556
+ {
1557
+ id: "MASTRA_STORAGE_PG_VECTOR_DELETE_VECTORS_FAILED",
1558
+ domain: ErrorDomain.MASTRA_VECTOR,
1559
+ category: ErrorCategory.THIRD_PARTY,
1560
+ details: {
1561
+ indexName,
1562
+ ...filter && { filter: JSON.stringify(filter) },
1563
+ ...ids && { idsCount: ids.length }
1564
+ }
1565
+ },
1566
+ error
1567
+ );
1568
+ this.logger?.trackException(mastraError);
1569
+ throw mastraError;
1570
+ } finally {
1571
+ client?.release();
1572
+ }
1573
+ }
1302
1574
  };
1303
1575
  function getSchemaName(schema) {
1304
1576
  return schema ? `"${parseSqlIdentifier(schema, "schema name")}"` : void 0;
@@ -1998,7 +2270,10 @@ var MemoryPG = class extends MemoryStorage {
1998
2270
  return message;
1999
2271
  });
2000
2272
  const list = new MessageList().add(messagesWithParsedContent, "memory");
2001
- const messagesToReturn = format === `v2` ? list.get.all.v2() : list.get.all.v1();
2273
+ let messagesToReturn = format === `v2` ? list.get.all.v2() : list.get.all.v1();
2274
+ messagesToReturn = messagesToReturn.sort(
2275
+ (a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
2276
+ );
2002
2277
  return {
2003
2278
  messages: messagesToReturn,
2004
2279
  total,
@@ -3473,20 +3748,24 @@ var StoreOperationsPG = class extends StoreOperations {
3473
3748
  }
3474
3749
  };
3475
3750
  function transformScoreRow(row) {
3476
- return {
3751
+ const data = {
3477
3752
  ...row,
3478
- input: safelyParseJSON(row.input),
3479
- scorer: safelyParseJSON(row.scorer),
3480
- preprocessStepResult: safelyParseJSON(row.preprocessStepResult),
3481
- analyzeStepResult: safelyParseJSON(row.analyzeStepResult),
3482
- metadata: safelyParseJSON(row.metadata),
3483
- output: safelyParseJSON(row.output),
3484
- additionalContext: safelyParseJSON(row.additionalContext),
3485
- runtimeContext: safelyParseJSON(row.runtimeContext),
3486
- entity: safelyParseJSON(row.entity),
3753
+ input: row.input !== null ? safelyParseJSON(row.input) : void 0,
3754
+ scorer: row.scorer !== null ? safelyParseJSON(row.scorer) : void 0,
3755
+ preprocessStepResult: row.preprocessStepResult !== null ? safelyParseJSON(row.preprocessStepResult) : void 0,
3756
+ analyzeStepResult: row.analyzeStepResult !== null ? safelyParseJSON(row.analyzeStepResult) : void 0,
3757
+ output: row.output !== null ? safelyParseJSON(row.output) : void 0,
3758
+ additionalContext: row.additionalContext !== null ? safelyParseJSON(row.additionalContext) : void 0,
3759
+ runtimeContext: row.runtimeContext !== null ? safelyParseJSON(row.runtimeContext) : void 0,
3760
+ entity: row.entity !== null ? safelyParseJSON(row.entity) : void 0,
3487
3761
  createdAt: row.createdAtZ || row.createdAt,
3488
3762
  updatedAt: row.updatedAtZ || row.updatedAt
3489
3763
  };
3764
+ const result = {};
3765
+ for (const key in SCORERS_SCHEMA) {
3766
+ result[key] = data[key];
3767
+ }
3768
+ return result;
3490
3769
  }
3491
3770
  var ScoresPG = class extends ScoresStorage {
3492
3771
  client;