@mastra/pg 1.23.0-alpha.5 → 1.23.0-alpha.7

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
@@ -560,6 +560,8 @@ var PgVector = class extends MastraVector {
560
560
  indexMetadataCache = /* @__PURE__ */ new Map();
561
561
  createdIndexes = /* @__PURE__ */ new Map();
562
562
  namespaceReadyIndexes = /* @__PURE__ */ new Set();
563
+ /** In-flight lazy namespace migrations, keyed by index name (see {@link ensureNamespaceReady}). */
564
+ namespaceReadyCache = /* @__PURE__ */ new Map();
563
565
  indexVectorTypes = /* @__PURE__ */ new Map();
564
566
  mutexesByName = /* @__PURE__ */ new Map();
565
567
  schema;
@@ -804,8 +806,10 @@ var PgVector = class extends MastraVector {
804
806
  AND attnum > 0
805
807
  AND NOT attisdropped`, [tableName])).rowCount === 0) return;
806
808
  await client.query(`ALTER TABLE ${tableName} ADD COLUMN IF NOT EXISTS namespace VARCHAR(255) NOT NULL DEFAULT '${DEFAULT_NAMESPACE}'`);
807
- const namespaceIndexName = this.getNamespaceIndexName(parsedIndexName);
808
- await client.query(`CREATE UNIQUE INDEX IF NOT EXISTS "${namespaceIndexName}" ON ${tableName} (namespace, vector_id)`);
809
+ if (!(await this.getNamespaceSchemaState(tableName, client)).composite_index) {
810
+ const namespaceIndexName = this.getNamespaceIndexName(parsedIndexName);
811
+ await client.query(`CREATE UNIQUE INDEX IF NOT EXISTS "${namespaceIndexName}" ON ${tableName} (namespace, vector_id)`);
812
+ }
809
813
  const legacyConstraints = await client.query(`SELECT c.conname
810
814
  FROM pg_constraint c
811
815
  WHERE c.conrelid = to_regclass($1)
@@ -816,21 +820,84 @@ var PgVector = class extends MastraVector {
816
820
  await client.query(`ALTER TABLE ${tableName} DROP CONSTRAINT "${parsedConstraintName}"`);
817
821
  }
818
822
  }
819
- /**
820
- * Name of the unique (namespace, vector_id) index for a table.
821
- *
822
- * The full `<index>_namespace_vector_id_idx` name is kept whenever it fits so tables that
823
- * were already migrated keep matching `IF NOT EXISTS`. Longer index names would exceed
824
- * Postgres' 63-char identifier limit, so those fall back to a truncated prefix plus a hash
825
- * of the index name to distinguish tables that
826
- * share a long prefix.
827
- */
828
823
  getNamespaceIndexName(parsedIndexName) {
829
824
  const fullName = `${parsedIndexName}_namespace_vector_id_idx`;
830
825
  if (fullName.length <= 63) return fullName;
831
826
  const suffix = `_ns_${createHash("sha256").update(parsedIndexName).digest("hex").slice(0, 32)}_idx`;
832
827
  return `${parsedIndexName.slice(0, 63 - suffix.length)}${suffix}`;
833
828
  }
829
+ async getNamespaceSchemaState(tableName, client) {
830
+ return (await client.query(`SELECT
831
+ EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = to_regclass($1)
832
+ AND attname = 'vector_id' AND attnum > 0 AND NOT attisdropped) AS vector_id,
833
+ EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = to_regclass($1)
834
+ AND attname = 'namespace' AND attnum > 0 AND NOT attisdropped) AS namespace,
835
+ EXISTS (
836
+ SELECT 1 FROM pg_index i
837
+ WHERE i.indrelid = to_regclass($1)
838
+ AND i.indisunique AND i.indisvalid AND i.indisready AND i.indimmediate
839
+ AND i.indpred IS NULL AND i.indexprs IS NULL AND i.indnkeyatts = 2
840
+ AND (SELECT array_agg(a.attname::text ORDER BY a.attname)
841
+ FROM unnest(i.indkey) WITH ORDINALITY AS k(attnum, position)
842
+ JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = k.attnum
843
+ WHERE k.position <= i.indnkeyatts) = ARRAY['namespace', 'vector_id']
844
+ ) AS composite_index,
845
+ EXISTS (SELECT 1 FROM pg_constraint WHERE conrelid = to_regclass($1)
846
+ AND contype = 'u' AND pg_get_constraintdef(oid) = 'UNIQUE (vector_id)') AS legacy_constraint`, [tableName])).rows[0];
847
+ }
848
+ async reconcileNamespace(indexName, client) {
849
+ const { tableName } = this.getTableName(indexName);
850
+ await client.query("BEGIN");
851
+ try {
852
+ await client.query("SELECT pg_advisory_xact_lock((1936876916::bigint << 32) | to_regclass($1)::oid::bigint)", [tableName]);
853
+ const state = await this.getNamespaceSchemaState(tableName, client);
854
+ if (!state.vector_id) {
855
+ await client.query("ROLLBACK");
856
+ return false;
857
+ }
858
+ if (!state.namespace || !state.composite_index || state.legacy_constraint) {
859
+ if (this.disableInit || process.env.MASTRA_DISABLE_STORAGE_INIT === "true") throw new MastraError({
860
+ id: createVectorErrorId("PG", "ENSURE_NAMESPACE", "MIGRATION_REQUIRED"),
861
+ domain: ErrorDomain.MASTRA_VECTOR,
862
+ category: ErrorCategory.USER,
863
+ text: `Vector index "${indexName}" requires namespace migration and schema changes are disabled. Call createIndex({ indexName: "${indexName}", dimension }) with init enabled, or apply the namespace migration SQL from the @mastra/pg changelog.`,
864
+ details: { indexName }
865
+ });
866
+ await this.ensureNamespaceSchema(indexName, client);
867
+ const migrated = await this.getNamespaceSchemaState(tableName, client);
868
+ if (!migrated.namespace || !migrated.composite_index || migrated.legacy_constraint) throw new MastraError({
869
+ id: createVectorErrorId("PG", "ENSURE_NAMESPACE", "MIGRATION_REQUIRED"),
870
+ domain: ErrorDomain.MASTRA_VECTOR,
871
+ category: ErrorCategory.USER,
872
+ text: `Vector index "${indexName}" requires a valid unique (namespace, vector_id) index. Resolve conflicting index names and retry the namespace migration.`,
873
+ details: { indexName }
874
+ });
875
+ }
876
+ await client.query("COMMIT");
877
+ return true;
878
+ } catch (error) {
879
+ await client.query("ROLLBACK");
880
+ throw error;
881
+ }
882
+ }
883
+ /**
884
+ * Every data path filters on `namespace`, but tables created before that column existed are
885
+ * only migrated by `createIndex()`, which callers are not required to invoke before reading.
886
+ * Run the migration lazily instead, once per index per process.
887
+ */
888
+ ensureNamespaceReady(indexName) {
889
+ if (this.namespaceReadyIndexes.has(indexName)) return Promise.resolve();
890
+ return this.memoize(this.namespaceReadyCache, indexName, () => this.getMutexByName(`create-${indexName}`).runExclusive(async () => {
891
+ if (this.namespaceReadyIndexes.has(indexName)) return;
892
+ const client = await this.pool.connect();
893
+ try {
894
+ if (await this.reconcileNamespace(indexName, client)) this.namespaceReadyIndexes.add(indexName);
895
+ else this.namespaceReadyCache.delete(indexName);
896
+ } finally {
897
+ client.release();
898
+ }
899
+ }));
900
+ }
834
901
  transformFilter(filter) {
835
902
  return new PGFilterTranslator().translate(filter);
836
903
  }
@@ -888,8 +955,10 @@ var PgVector = class extends MastraVector {
888
955
  throw mastraError;
889
956
  }
890
957
  if (queryVector === void 0) {
891
- const client = await this.pool.connect();
958
+ let client;
892
959
  try {
960
+ await this.ensureNamespaceReady(indexName);
961
+ client = await this.pool.connect();
893
962
  const { sql: filterQuery, values: filterValues } = buildDeleteFilterQuery(this.transformFilter(filter));
894
963
  const { tableName } = this.getTableName(indexName);
895
964
  const filterClause = filterQuery.trim().replace(/^WHERE\s+/i, "");
@@ -915,6 +984,10 @@ var PgVector = class extends MastraVector {
915
984
  ...includeVector && embedding && { vector: JSON.parse(embedding) }
916
985
  }));
917
986
  } catch (error) {
987
+ if (error instanceof MastraError) {
988
+ this.logger?.trackException(error);
989
+ throw error;
990
+ }
918
991
  const mastraError = new MastraError({
919
992
  id: createVectorErrorId("PG", "QUERY", "FAILED"),
920
993
  domain: ErrorDomain.MASTRA_VECTOR,
@@ -924,12 +997,14 @@ var PgVector = class extends MastraVector {
924
997
  this.logger?.trackException(mastraError);
925
998
  throw mastraError;
926
999
  } finally {
927
- client.release();
1000
+ client?.release();
928
1001
  }
929
1002
  }
930
- const indexInfo = await this.getIndexMetadata({ indexName });
931
- const client = await this.pool.connect();
1003
+ let client;
932
1004
  try {
1005
+ await this.ensureNamespaceReady(indexName);
1006
+ const indexInfo = await this.getIndexMetadata({ indexName });
1007
+ client = await this.pool.connect();
933
1008
  await this.ensureSearchPath(client);
934
1009
  await client.query("BEGIN");
935
1010
  const { sql: filterQuery, values: filterValues } = buildFilterQuery(this.transformFilter(filter), minScore, topK);
@@ -991,7 +1066,11 @@ var PgVector = class extends MastraVector {
991
1066
  ...includeVector && embedding && { vector: ops.parseEmbedding(embedding) }
992
1067
  }));
993
1068
  } catch (error) {
994
- await client.query("ROLLBACK");
1069
+ await client?.query("ROLLBACK");
1070
+ if (error instanceof MastraError) {
1071
+ this.logger?.trackException(error);
1072
+ throw error;
1073
+ }
995
1074
  const mastraError = new MastraError({
996
1075
  id: createVectorErrorId("PG", "QUERY", "FAILED"),
997
1076
  domain: ErrorDomain.MASTRA_VECTOR,
@@ -1001,15 +1080,17 @@ var PgVector = class extends MastraVector {
1001
1080
  this.logger?.trackException(mastraError);
1002
1081
  throw mastraError;
1003
1082
  } finally {
1004
- client.release();
1083
+ client?.release();
1005
1084
  }
1006
1085
  }
1007
1086
  async upsert({ indexName, vectors, metadata, ids, deleteFilter, namespace = DEFAULT_NAMESPACE }) {
1008
1087
  validateUpsertInput("PG", vectors, metadata, ids);
1009
1088
  const { tableName } = this.getTableName(indexName);
1010
- const indexInfo = await this.getIndexMetadata({ indexName });
1011
- const client = await this.pool.connect();
1089
+ let client;
1012
1090
  try {
1091
+ const indexInfo = await this.getIndexMetadata({ indexName });
1092
+ await this.ensureNamespaceReady(indexName);
1093
+ client = await this.pool.connect();
1013
1094
  await this.ensureSearchPath(client);
1014
1095
  await client.query("BEGIN");
1015
1096
  if (deleteFilter) {
@@ -1075,7 +1156,11 @@ var PgVector = class extends MastraVector {
1075
1156
  });
1076
1157
  return vectorIds;
1077
1158
  } catch (error) {
1078
- await client.query("ROLLBACK");
1159
+ await client?.query("ROLLBACK");
1160
+ if (error instanceof MastraError) {
1161
+ this.logger?.trackException(error);
1162
+ throw error;
1163
+ }
1079
1164
  if (error instanceof Error && error.message?.includes("expected") && error.message?.includes("dimensions")) {
1080
1165
  const match = error.message.match(/expected (\d+) dimensions, not (\d+)/);
1081
1166
  if (match) {
@@ -1104,7 +1189,7 @@ var PgVector = class extends MastraVector {
1104
1189
  this.logger?.trackException(mastraError);
1105
1190
  throw mastraError;
1106
1191
  } finally {
1107
- client.release();
1192
+ client?.release();
1108
1193
  }
1109
1194
  }
1110
1195
  hasher = xxhash();
@@ -1232,9 +1317,8 @@ var PgVector = class extends MastraVector {
1232
1317
  namespace VARCHAR(255) NOT NULL DEFAULT '${DEFAULT_NAMESPACE}'
1233
1318
  );
1234
1319
  `);
1235
- await this.ensureNamespaceSchema(indexName, client);
1320
+ if (await this.reconcileNamespace(indexName, client)) this.namespaceReadyIndexes.add(indexName);
1236
1321
  this.createdIndexes.set(indexName, indexCacheKey);
1237
- this.namespaceReadyIndexes.add(indexName);
1238
1322
  this.indexVectorTypes.set(indexName, vectorType);
1239
1323
  if (buildIndex) await this.setupIndex({
1240
1324
  indexName,
@@ -1246,6 +1330,7 @@ var PgVector = class extends MastraVector {
1246
1330
  } catch (error) {
1247
1331
  this.createdIndexes.delete(indexName);
1248
1332
  this.namespaceReadyIndexes.delete(indexName);
1333
+ this.namespaceReadyCache.delete(indexName);
1249
1334
  this.indexVectorTypes.delete(indexName);
1250
1335
  throw error;
1251
1336
  } finally {
@@ -1620,27 +1705,30 @@ var PgVector = class extends MastraVector {
1620
1705
  }
1621
1706
  }
1622
1707
  async deleteIndex({ indexName }) {
1623
- const client = await this.pool.connect();
1624
- try {
1625
- const { tableName } = this.getTableName(indexName);
1626
- await client.query(`DROP TABLE IF EXISTS ${tableName} CASCADE`);
1627
- this.createdIndexes.delete(indexName);
1628
- this.namespaceReadyIndexes.delete(indexName);
1629
- this.indexVectorTypes.delete(indexName);
1630
- this.invalidateIndexCaches(indexName);
1631
- } catch (error) {
1632
- await client.query("ROLLBACK");
1633
- const mastraError = new MastraError({
1634
- id: createVectorErrorId("PG", "DELETE_INDEX", "FAILED"),
1635
- domain: ErrorDomain.MASTRA_VECTOR,
1636
- category: ErrorCategory.THIRD_PARTY,
1637
- details: { indexName }
1638
- }, error);
1639
- this.logger?.trackException(mastraError);
1640
- throw mastraError;
1641
- } finally {
1642
- client.release();
1643
- }
1708
+ await this.getMutexByName(`create-${indexName}`).runExclusive(async () => {
1709
+ const client = await this.pool.connect();
1710
+ try {
1711
+ const { tableName } = this.getTableName(indexName);
1712
+ await client.query(`DROP TABLE IF EXISTS ${tableName} CASCADE`);
1713
+ this.createdIndexes.delete(indexName);
1714
+ this.namespaceReadyIndexes.delete(indexName);
1715
+ this.namespaceReadyCache.delete(indexName);
1716
+ this.indexVectorTypes.delete(indexName);
1717
+ this.invalidateIndexCaches(indexName);
1718
+ } catch (error) {
1719
+ await client.query("ROLLBACK");
1720
+ const mastraError = new MastraError({
1721
+ id: createVectorErrorId("PG", "DELETE_INDEX", "FAILED"),
1722
+ domain: ErrorDomain.MASTRA_VECTOR,
1723
+ category: ErrorCategory.THIRD_PARTY,
1724
+ details: { indexName }
1725
+ }, error);
1726
+ this.logger?.trackException(mastraError);
1727
+ throw mastraError;
1728
+ } finally {
1729
+ client.release();
1730
+ }
1731
+ });
1644
1732
  }
1645
1733
  async truncateIndex({ indexName }) {
1646
1734
  const client = await this.pool.connect();
@@ -1696,6 +1784,7 @@ var PgVector = class extends MastraVector {
1696
1784
  details: { indexName }
1697
1785
  });
1698
1786
  const indexInfo = await this.getIndexMetadata({ indexName });
1787
+ await this.ensureNamespaceReady(indexName);
1699
1788
  client = await this.pool.connect();
1700
1789
  await this.ensureSearchPath(client);
1701
1790
  const { tableName } = this.getTableName(indexName);
@@ -1789,6 +1878,7 @@ var PgVector = class extends MastraVector {
1789
1878
  async deleteVector({ indexName, id, namespace = DEFAULT_NAMESPACE }) {
1790
1879
  let client;
1791
1880
  try {
1881
+ await this.ensureNamespaceReady(indexName);
1792
1882
  client = await this.pool.connect();
1793
1883
  const { tableName } = this.getTableName(indexName);
1794
1884
  const query = `
@@ -1797,6 +1887,7 @@ var PgVector = class extends MastraVector {
1797
1887
  `;
1798
1888
  await client.query(query, [id, namespace]);
1799
1889
  } catch (error) {
1890
+ if (error instanceof MastraError) throw error;
1800
1891
  const mastraError = new MastraError({
1801
1892
  id: createVectorErrorId("PG", "DELETE_VECTOR", "FAILED"),
1802
1893
  domain: ErrorDomain.MASTRA_VECTOR,
@@ -1823,6 +1914,7 @@ var PgVector = class extends MastraVector {
1823
1914
  let client;
1824
1915
  const effectiveNamespace = namespace ?? DEFAULT_NAMESPACE;
1825
1916
  try {
1917
+ await this.ensureNamespaceReady(indexName);
1826
1918
  client = await this.pool.connect();
1827
1919
  const { tableName } = this.getTableName(indexName);
1828
1920
  if (!filter && !ids && namespace === void 0) throw new MastraError({
@@ -5553,6 +5645,17 @@ var ChannelsPG = class ChannelsPG extends ChannelsStorage {
5553
5645
  function jsonbArg(value) {
5554
5646
  return value === void 0 || value === null ? null : JSON.stringify(value);
5555
5647
  }
5648
+ function parseStoredJSON(value) {
5649
+ if (typeof value === "string") try {
5650
+ return JSON.parse(value);
5651
+ } catch {
5652
+ return value;
5653
+ }
5654
+ return value;
5655
+ }
5656
+ function parseOptionalJSON(value, emptyValue) {
5657
+ return value === null || value === void 0 ? emptyValue : parseStoredJSON(value);
5658
+ }
5556
5659
  var DatasetsPG = class DatasetsPG extends DatasetsStorage {
5557
5660
  #db;
5558
5661
  #schema;
@@ -5729,6 +5832,8 @@ var DatasetsPG = class DatasetsPG extends DatasetsStorage {
5729
5832
  };
5730
5833
  }
5731
5834
  transformItemRow(row) {
5835
+ const metadata = parseOptionalJSON(row.metadata, void 0);
5836
+ const emptyValue = metadata?.__purged === true ? null : void 0;
5732
5837
  return {
5733
5838
  id: row.id,
5734
5839
  datasetId: row.datasetId,
@@ -5736,20 +5841,22 @@ var DatasetsPG = class DatasetsPG extends DatasetsStorage {
5736
5841
  externalId: row.externalId ?? null,
5737
5842
  organizationId: row.organizationId ?? null,
5738
5843
  projectId: row.projectId ?? null,
5739
- input: safelyParseJSON(row.input),
5740
- groundTruth: row.groundTruth ? safelyParseJSON(row.groundTruth) : void 0,
5741
- expectedTrajectory: row.expectedTrajectory ? safelyParseJSON(row.expectedTrajectory) : void 0,
5742
- toolMocks: row.toolMocks ? safelyParseJSON(row.toolMocks) : void 0,
5743
- unmockedToolPolicy: row.unmockedToolPolicy ?? void 0,
5744
- scorerIds: row.scorerIds ? safelyParseJSON(row.scorerIds) : void 0,
5745
- requestContext: row.requestContext ? safelyParseJSON(row.requestContext) : void 0,
5746
- metadata: row.metadata ? safelyParseJSON(row.metadata) : void 0,
5747
- source: row.source ? safelyParseJSON(row.source) : void 0,
5844
+ input: row.input === null ? null : parseStoredJSON(row.input),
5845
+ groundTruth: parseOptionalJSON(row.groundTruth, emptyValue),
5846
+ expectedTrajectory: parseOptionalJSON(row.expectedTrajectory, emptyValue),
5847
+ toolMocks: parseOptionalJSON(row.toolMocks, emptyValue),
5848
+ unmockedToolPolicy: row.unmockedToolPolicy ?? emptyValue,
5849
+ scorerIds: parseOptionalJSON(row.scorerIds, emptyValue),
5850
+ requestContext: parseOptionalJSON(row.requestContext, emptyValue),
5851
+ metadata,
5852
+ source: parseOptionalJSON(row.source, emptyValue),
5748
5853
  createdAt: ensureDate(row.createdAtZ || row.createdAt),
5749
5854
  updatedAt: ensureDate(row.updatedAtZ || row.updatedAt)
5750
5855
  };
5751
5856
  }
5752
5857
  transformItemRowFull(row) {
5858
+ const metadata = parseOptionalJSON(row.metadata, void 0);
5859
+ const emptyValue = metadata?.__purged === true ? null : void 0;
5753
5860
  return {
5754
5861
  id: row.id,
5755
5862
  datasetId: row.datasetId,
@@ -5759,15 +5866,15 @@ var DatasetsPG = class DatasetsPG extends DatasetsStorage {
5759
5866
  projectId: row.projectId ?? null,
5760
5867
  validTo: row.validTo,
5761
5868
  isDeleted: Boolean(row.isDeleted),
5762
- input: safelyParseJSON(row.input),
5763
- groundTruth: row.groundTruth ? safelyParseJSON(row.groundTruth) : void 0,
5764
- expectedTrajectory: row.expectedTrajectory ? safelyParseJSON(row.expectedTrajectory) : void 0,
5765
- toolMocks: row.toolMocks ? safelyParseJSON(row.toolMocks) : void 0,
5766
- unmockedToolPolicy: row.unmockedToolPolicy ?? void 0,
5767
- scorerIds: row.scorerIds ? safelyParseJSON(row.scorerIds) : void 0,
5768
- requestContext: row.requestContext ? safelyParseJSON(row.requestContext) : void 0,
5769
- metadata: row.metadata ? safelyParseJSON(row.metadata) : void 0,
5770
- source: row.source ? safelyParseJSON(row.source) : void 0,
5869
+ input: row.input === null ? null : parseStoredJSON(row.input),
5870
+ groundTruth: parseOptionalJSON(row.groundTruth, emptyValue),
5871
+ expectedTrajectory: parseOptionalJSON(row.expectedTrajectory, emptyValue),
5872
+ toolMocks: parseOptionalJSON(row.toolMocks, emptyValue),
5873
+ unmockedToolPolicy: row.unmockedToolPolicy ?? emptyValue,
5874
+ scorerIds: parseOptionalJSON(row.scorerIds, emptyValue),
5875
+ requestContext: parseOptionalJSON(row.requestContext, emptyValue),
5876
+ metadata,
5877
+ source: parseOptionalJSON(row.source, emptyValue),
5771
5878
  createdAt: ensureDate(row.createdAtZ || row.createdAt),
5772
5879
  updatedAt: ensureDate(row.updatedAtZ || row.updatedAt)
5773
5880
  };
@@ -6343,6 +6450,52 @@ var DatasetsPG = class DatasetsPG extends DatasetsStorage {
6343
6450
  }, error);
6344
6451
  }
6345
6452
  }
6453
+ async _doPurgeItem({ id, datasetId }) {
6454
+ try {
6455
+ const datasetsTable = getTableName$5({
6456
+ indexName: TABLE_DATASETS,
6457
+ schemaName: getSchemaName$5(this.#schema)
6458
+ });
6459
+ const itemsTable = getTableName$5({
6460
+ indexName: TABLE_DATASET_ITEMS,
6461
+ schemaName: getSchemaName$5(this.#schema)
6462
+ });
6463
+ const experimentsTable = getTableName$5({
6464
+ indexName: TABLE_EXPERIMENTS,
6465
+ schemaName: getSchemaName$5(this.#schema)
6466
+ });
6467
+ const experimentResultsTable = getTableName$5({
6468
+ indexName: TABLE_EXPERIMENT_RESULTS,
6469
+ schemaName: getSchemaName$5(this.#schema)
6470
+ });
6471
+ const purgedAt = (/* @__PURE__ */ new Date()).toISOString();
6472
+ const purgedMetadata = JSON.stringify({
6473
+ __purged: true,
6474
+ purgedAt
6475
+ });
6476
+ await this.#db.client.tx(async (t) => {
6477
+ if (!await t.oneOrNone(`SELECT "id" FROM ${datasetsTable} WHERE "id" = $1 FOR UPDATE`, [datasetId])) return;
6478
+ if (!await t.oneOrNone(`SELECT "id" FROM ${itemsTable} WHERE "id" = $1 AND "datasetId" = $2 LIMIT 1 FOR UPDATE`, [id, datasetId])) return;
6479
+ await t.none(`UPDATE ${itemsTable} SET "input" = 'null'::jsonb, "groundTruth" = NULL, "expectedTrajectory" = NULL, "toolMocks" = NULL, "unmockedToolPolicy" = NULL, "scorerIds" = NULL, "requestContext" = NULL, "metadata" = $2::jsonb, "source" = NULL WHERE "id" = $1 AND "datasetId" = $3`, [
6480
+ id,
6481
+ purgedMetadata,
6482
+ datasetId
6483
+ ]);
6484
+ if ((await t.one(`SELECT to_regclass($1) IS NOT NULL AND to_regclass($2) IS NOT NULL AS exists`, [experimentResultsTable, experimentsTable])).exists) await t.none(`UPDATE ${experimentResultsTable} SET "input" = 'null'::jsonb, "output" = NULL, "groundTruth" = NULL, "error" = NULL, "toolMockReport" = NULL, "tags" = NULL, "comment" = NULL, "metadata" = $2::jsonb WHERE "itemId" = $1 AND "experimentId" IN (SELECT "id" FROM ${experimentsTable} WHERE "datasetId" = $3)`, [
6485
+ id,
6486
+ purgedMetadata,
6487
+ datasetId
6488
+ ]);
6489
+ });
6490
+ } catch (error) {
6491
+ if (error instanceof MastraError) throw error;
6492
+ throw new MastraError({
6493
+ id: createStorageErrorId("PG", "PURGE_ITEM", "FAILED"),
6494
+ domain: ErrorDomain.STORAGE,
6495
+ category: ErrorCategory.THIRD_PARTY
6496
+ }, error);
6497
+ }
6498
+ }
6346
6499
  async _doBatchInsertItems(input) {
6347
6500
  try {
6348
6501
  if (input.items.length === 0) return [];
@@ -6994,7 +7147,7 @@ var ExperimentsPG = class ExperimentsPG extends ExperimentsStorage {
6994
7147
  itemDatasetVersion: row.itemDatasetVersion != null ? row.itemDatasetVersion : null,
6995
7148
  organizationId: row.organizationId ?? null,
6996
7149
  projectId: row.projectId ?? null,
6997
- input: safelyParseJSON(row.input),
7150
+ input: row.input === null ? null : safelyParseJSON(row.input),
6998
7151
  output: row.output ? safelyParseJSON(row.output) : null,
6999
7152
  groundTruth: row.groundTruth ? safelyParseJSON(row.groundTruth) : null,
7000
7153
  metadata: row.metadata ? safelyParseJSON(row.metadata) : null,
@@ -7296,58 +7449,65 @@ var ExperimentsPG = class ExperimentsPG extends ExperimentsStorage {
7296
7449
  }, error);
7297
7450
  }
7298
7451
  }
7452
+ async #resolvePurgeMetadata(t, datasetId, itemId) {
7453
+ if (!datasetId) return null;
7454
+ const datasetsTable = getTableName$5({
7455
+ indexName: TABLE_DATASETS,
7456
+ schemaName: getSchemaName$5(this.#schema)
7457
+ });
7458
+ const itemsTable = getTableName$5({
7459
+ indexName: TABLE_DATASET_ITEMS,
7460
+ schemaName: getSchemaName$5(this.#schema)
7461
+ });
7462
+ if (!await t.oneOrNone(`SELECT "id" FROM ${datasetsTable} WHERE "id" = $1 FOR UPDATE`, [datasetId])) return null;
7463
+ return (await t.oneOrNone(`SELECT "metadata" FROM ${itemsTable}
7464
+ WHERE "id" = $1 AND "datasetId" = $2 AND "metadata"->>'__purged' = 'true'
7465
+ LIMIT 1`, [itemId, datasetId]))?.metadata ?? null;
7466
+ }
7299
7467
  async addExperimentResult(input) {
7300
7468
  try {
7301
7469
  const id = input.id ?? crypto.randomUUID();
7302
- const now = /* @__PURE__ */ new Date();
7303
- const nowIso = now.toISOString();
7304
- await this.#db.insert({
7305
- tableName: TABLE_EXPERIMENT_RESULTS,
7306
- record: {
7470
+ const resultsTable = getTableName$5({
7471
+ indexName: TABLE_EXPERIMENT_RESULTS,
7472
+ schemaName: getSchemaName$5(this.#schema)
7473
+ });
7474
+ const experimentsTable = getTableName$5({
7475
+ indexName: TABLE_EXPERIMENTS,
7476
+ schemaName: getSchemaName$5(this.#schema)
7477
+ });
7478
+ const row = await this.#db.client.tx(async (t) => {
7479
+ const owner = await t.oneOrNone(`SELECT "datasetId" FROM ${experimentsTable} WHERE "id" = $1`, [input.experimentId]);
7480
+ const purgeMetadata = await this.#resolvePurgeMetadata(t, owner?.datasetId, input.itemId);
7481
+ const nowIso = (/* @__PURE__ */ new Date()).toISOString();
7482
+ return t.one(`INSERT INTO ${resultsTable} (
7483
+ "id", "experimentId", "itemId", "itemDatasetVersion", "organizationId", "projectId",
7484
+ "input", "output", "groundTruth", "metadata", "error", "startedAt", "completedAt",
7485
+ "retryCount", "attempt", "traceId", "status", "tags", "toolMockReport", "createdAt"
7486
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20)
7487
+ RETURNING *`, [
7307
7488
  id,
7308
- experimentId: input.experimentId,
7309
- itemId: input.itemId,
7310
- itemDatasetVersion: input.itemDatasetVersion ?? null,
7311
- organizationId: input.organizationId ?? null,
7312
- projectId: input.projectId ?? null,
7313
- input: input.input,
7314
- output: input.output ?? null,
7315
- groundTruth: input.groundTruth ?? null,
7316
- metadata: input.metadata ?? null,
7317
- error: input.error ?? null,
7318
- startedAt: input.startedAt.toISOString(),
7319
- completedAt: input.completedAt.toISOString(),
7320
- retryCount: input.retryCount,
7321
- attempt: input.attempt ?? 0,
7322
- traceId: input.traceId ?? null,
7323
- status: input.status ?? null,
7324
- tags: input.tags ?? null,
7325
- toolMockReport: input.toolMockReport ?? null,
7326
- createdAt: nowIso
7327
- }
7489
+ input.experimentId,
7490
+ input.itemId,
7491
+ input.itemDatasetVersion ?? null,
7492
+ input.organizationId ?? null,
7493
+ input.projectId ?? null,
7494
+ JSON.stringify(purgeMetadata ? null : input.input),
7495
+ purgeMetadata || input.output == null ? null : JSON.stringify(input.output),
7496
+ purgeMetadata || input.groundTruth == null ? null : JSON.stringify(input.groundTruth),
7497
+ JSON.stringify(purgeMetadata ?? input.metadata ?? null),
7498
+ purgeMetadata || input.error == null ? null : JSON.stringify(input.error),
7499
+ input.startedAt.toISOString(),
7500
+ input.completedAt.toISOString(),
7501
+ input.retryCount,
7502
+ input.attempt ?? 0,
7503
+ input.traceId ?? null,
7504
+ input.status ?? null,
7505
+ purgeMetadata || input.tags == null ? null : JSON.stringify(input.tags),
7506
+ purgeMetadata || input.toolMockReport == null ? null : JSON.stringify(input.toolMockReport),
7507
+ nowIso
7508
+ ]);
7328
7509
  });
7329
- return {
7330
- id,
7331
- experimentId: input.experimentId,
7332
- itemId: input.itemId,
7333
- itemDatasetVersion: input.itemDatasetVersion ?? null,
7334
- organizationId: input.organizationId ?? null,
7335
- projectId: input.projectId ?? null,
7336
- input: input.input,
7337
- output: input.output ?? null,
7338
- groundTruth: input.groundTruth ?? null,
7339
- metadata: input.metadata ?? null,
7340
- error: input.error ?? null,
7341
- startedAt: input.startedAt,
7342
- completedAt: input.completedAt,
7343
- retryCount: input.retryCount,
7344
- attempt: input.attempt ?? 0,
7345
- traceId: input.traceId ?? null,
7346
- status: input.status ?? null,
7347
- tags: input.tags ?? null,
7348
- toolMockReport: input.toolMockReport ?? null,
7349
- createdAt: now
7350
- };
7510
+ return this.transformExperimentResultRow(row);
7351
7511
  } catch (error) {
7352
7512
  throw new MastraError({
7353
7513
  id: createStorageErrorId("PG", "ADD_EXPERIMENT_RESULT", "FAILED"),
@@ -7362,8 +7522,14 @@ var ExperimentsPG = class ExperimentsPG extends ExperimentsStorage {
7362
7522
  indexName: TABLE_EXPERIMENT_RESULTS,
7363
7523
  schemaName: getSchemaName$5(this.#schema)
7364
7524
  });
7525
+ const experimentsTable = getTableName$5({
7526
+ indexName: TABLE_EXPERIMENTS,
7527
+ schemaName: getSchemaName$5(this.#schema)
7528
+ });
7365
7529
  const attempt = input.attempt ?? 0;
7366
7530
  const row = await this.#db.client.tx(async (t) => {
7531
+ const owner = await t.oneOrNone(`SELECT "datasetId" FROM ${experimentsTable} WHERE "id" = $1`, [input.experimentId]);
7532
+ const purgeMetadata = await this.#resolvePurgeMetadata(t, owner?.datasetId, input.itemId);
7367
7533
  const existing = await t.oneOrNone(`SELECT "id" FROM ${tableName} WHERE "experimentId" = $1 AND "itemId" = $2 AND COALESCE("attempt", 0) = $3 FOR UPDATE`, [
7368
7534
  input.experimentId,
7369
7535
  input.itemId,
@@ -7383,19 +7549,19 @@ var ExperimentsPG = class ExperimentsPG extends ExperimentsStorage {
7383
7549
  input.itemDatasetVersion ?? null,
7384
7550
  input.organizationId ?? null,
7385
7551
  input.projectId ?? null,
7386
- JSON.stringify(input.input),
7387
- input.output != null ? JSON.stringify(input.output) : null,
7388
- input.groundTruth != null ? JSON.stringify(input.groundTruth) : null,
7389
- input.metadata != null ? JSON.stringify(input.metadata) : null,
7390
- input.error != null ? JSON.stringify(input.error) : null,
7552
+ JSON.stringify(purgeMetadata ? null : input.input),
7553
+ purgeMetadata || input.output == null ? null : JSON.stringify(input.output),
7554
+ purgeMetadata || input.groundTruth == null ? null : JSON.stringify(input.groundTruth),
7555
+ JSON.stringify(purgeMetadata ?? input.metadata ?? null),
7556
+ purgeMetadata || input.error == null ? null : JSON.stringify(input.error),
7391
7557
  input.startedAt.toISOString(),
7392
7558
  input.completedAt.toISOString(),
7393
7559
  input.retryCount,
7394
7560
  attempt,
7395
7561
  input.traceId ?? null,
7396
7562
  input.status ?? null,
7397
- input.tags != null ? JSON.stringify(input.tags) : null,
7398
- input.toolMockReport != null ? JSON.stringify(input.toolMockReport) : null,
7563
+ purgeMetadata || input.tags == null ? null : JSON.stringify(input.tags),
7564
+ purgeMetadata || input.toolMockReport == null ? null : JSON.stringify(input.toolMockReport),
7399
7565
  (/* @__PURE__ */ new Date()).toISOString()
7400
7566
  ]);
7401
7567
  }
@@ -7409,19 +7575,19 @@ var ExperimentsPG = class ExperimentsPG extends ExperimentsStorage {
7409
7575
  input.itemDatasetVersion ?? null,
7410
7576
  input.organizationId ?? null,
7411
7577
  input.projectId ?? null,
7412
- JSON.stringify(input.input),
7413
- input.output != null ? JSON.stringify(input.output) : null,
7414
- input.groundTruth != null ? JSON.stringify(input.groundTruth) : null,
7415
- input.metadata != null ? JSON.stringify(input.metadata) : null,
7416
- input.error != null ? JSON.stringify(input.error) : null,
7578
+ JSON.stringify(purgeMetadata ? null : input.input),
7579
+ purgeMetadata || input.output == null ? null : JSON.stringify(input.output),
7580
+ purgeMetadata || input.groundTruth == null ? null : JSON.stringify(input.groundTruth),
7581
+ JSON.stringify(purgeMetadata ?? input.metadata ?? null),
7582
+ purgeMetadata || input.error == null ? null : JSON.stringify(input.error),
7417
7583
  input.startedAt.toISOString(),
7418
7584
  input.completedAt.toISOString(),
7419
7585
  input.retryCount,
7420
7586
  attempt,
7421
7587
  input.traceId ?? null,
7422
7588
  input.status ?? null,
7423
- input.tags != null ? JSON.stringify(input.tags) : null,
7424
- input.toolMockReport != null ? JSON.stringify(input.toolMockReport) : null
7589
+ purgeMetadata || input.tags == null ? null : JSON.stringify(input.tags),
7590
+ purgeMetadata || input.toolMockReport == null ? null : JSON.stringify(input.toolMockReport)
7425
7591
  ]);
7426
7592
  });
7427
7593
  return this.transformExperimentResultRow(row);
@@ -7439,39 +7605,34 @@ var ExperimentsPG = class ExperimentsPG extends ExperimentsStorage {
7439
7605
  indexName: TABLE_EXPERIMENT_RESULTS,
7440
7606
  schemaName: getSchemaName$5(this.#schema)
7441
7607
  });
7442
- const setClauses = [];
7443
- const values = [];
7444
- let paramIndex = 1;
7445
- if (input.status !== void 0) {
7446
- setClauses.push(`"status" = $${paramIndex++}`);
7447
- values.push(input.status);
7448
- }
7449
- if (input.tags !== void 0) {
7450
- setClauses.push(`"tags" = $${paramIndex++}`);
7451
- values.push(JSON.stringify(input.tags));
7452
- }
7453
- if (input.comment !== void 0) {
7454
- setClauses.push(`"comment" = $${paramIndex++}`);
7455
- values.push(input.comment);
7456
- }
7457
- if (setClauses.length === 0) {
7458
- const existing = await this.#getExperimentResultById(this.#db.client, { id: input.id });
7459
- if (!existing) throw new MastraError({
7460
- id: createStorageErrorId("PG", "UPDATE_EXPERIMENT_RESULT", "NOT_FOUND"),
7461
- domain: ErrorDomain.STORAGE,
7462
- category: ErrorCategory.USER,
7463
- details: { resultId: input.id }
7464
- });
7465
- return existing;
7466
- }
7467
- values.push(input.id);
7468
- let whereClause = `"id" = $${paramIndex}`;
7469
- if (input.experimentId) {
7470
- paramIndex++;
7471
- values.push(input.experimentId);
7472
- whereClause += ` AND "experimentId" = $${paramIndex}`;
7473
- }
7474
- const row = await this.#db.client.oneOrNone(`UPDATE ${tableName} SET ${setClauses.join(", ")} WHERE ${whereClause} RETURNING *`, values);
7608
+ const experimentsTable = getTableName$5({
7609
+ indexName: TABLE_EXPERIMENTS,
7610
+ schemaName: getSchemaName$5(this.#schema)
7611
+ });
7612
+ const row = await this.#db.client.tx(async (t) => {
7613
+ const owner = await t.oneOrNone(`SELECT e."datasetId", r."itemId"
7614
+ FROM ${tableName} r
7615
+ JOIN ${experimentsTable} e ON e."id" = r."experimentId"
7616
+ WHERE r."id" = $1${input.experimentId !== void 0 ? " AND r.\"experimentId\" = $2" : ""}`, input.experimentId !== void 0 ? [input.id, input.experimentId] : [input.id]);
7617
+ if (!owner) return null;
7618
+ const purgeMetadata = await this.#resolvePurgeMetadata(t, owner.datasetId, owner.itemId);
7619
+ return t.oneOrNone(`UPDATE ${tableName}
7620
+ SET "status" = CASE WHEN $2 THEN $3 ELSE "status" END,
7621
+ "tags" = CASE WHEN $4 THEN NULL WHEN $5 THEN $6::jsonb ELSE "tags" END,
7622
+ "comment" = CASE WHEN $4 THEN NULL WHEN $7 THEN $8 ELSE "comment" END
7623
+ WHERE "id" = $1${input.experimentId !== void 0 ? " AND \"experimentId\" = $9" : ""}
7624
+ RETURNING *`, [
7625
+ input.id,
7626
+ input.status !== void 0,
7627
+ input.status ?? null,
7628
+ Boolean(purgeMetadata),
7629
+ input.tags !== void 0,
7630
+ input.tags === void 0 ? null : JSON.stringify(input.tags),
7631
+ input.comment !== void 0,
7632
+ input.comment ?? null,
7633
+ ...input.experimentId !== void 0 ? [input.experimentId] : []
7634
+ ]);
7635
+ });
7475
7636
  if (!row) throw new MastraError({
7476
7637
  id: createStorageErrorId("PG", "UPDATE_EXPERIMENT_RESULT", "NOT_FOUND"),
7477
7638
  domain: ErrorDomain.STORAGE,
@@ -10871,12 +11032,13 @@ var MemoryPG = class MemoryPG extends MemoryStorage {
10871
11032
  /**
10872
11033
  * Reads one page of messages together with the total row count.
10873
11034
  *
10874
- * `COUNT(*) OVER ()` reports the count over the whole WHERE result on the same
10875
- * statement as the page, so the page costs one database round-trip instead of
10876
- * two. The page and the count also come from one snapshot, so the count always
10877
- * describes the returned rows. A separate `COUNT(*)` runs only when the page is
10878
- * empty and the caller asked for a page after the last row, because a window
10879
- * function has no row to carry the count on.
11035
+ * Every page query carries a skinny scalar `(SELECT COUNT(*) ...)` subquery that
11036
+ * reports the total over the whole WHERE result on the same statement as the page,
11037
+ * so the page costs one database round-trip instead of two. The page and the count
11038
+ * also come from one snapshot, so the count always describes the returned rows. A
11039
+ * separate `COUNT(*)` runs only as a fallback when the page is empty and the caller
11040
+ * asked for a page after the last row, because there is then no row to carry the
11041
+ * count on.
10880
11042
  */
10881
11043
  async #fetchMessagePage({ selectStatement, tableName, whereClause, orderByStatement, queryParams, perPageInput, perPage, offset }) {
10882
11044
  const limitClause = perPageInput === false ? "" : ` LIMIT $${queryParams.length + 1} OFFSET $${queryParams.length + 2}`;
@@ -10885,7 +11047,7 @@ var MemoryPG = class MemoryPG extends MemoryStorage {
10885
11047
  perPage,
10886
11048
  offset
10887
11049
  ];
10888
- const rows = await this.#db.readClient.manyOrNone(`${selectStatement}, COUNT(*) OVER () AS "__total" FROM ${tableName} ${whereClause} ${orderByStatement}${limitClause}`, dataParams) || [];
11050
+ const rows = await this.#db.readClient.manyOrNone(`${selectStatement}, (SELECT COUNT(*) FROM ${tableName} ${whereClause}) AS "__total" FROM ${tableName} ${whereClause} ${orderByStatement}${limitClause}`, dataParams) || [];
10889
11051
  if (rows.length > 0) return {
10890
11052
  total: Number(rows[0].__total),
10891
11053
  messages: rows
@@ -10924,7 +11086,7 @@ var MemoryPG = class MemoryPG extends MemoryStorage {
10924
11086
  const metadataFilter = validateStorageMetadataFilter(filter?.metadata);
10925
11087
  try {
10926
11088
  const { field, direction } = this.parseOrderBy(orderBy, "ASC");
10927
- const orderByStatement = `ORDER BY COALESCE("${field}Z", "${field}") ${direction}`;
11089
+ const orderByStatement = `ORDER BY "${field}" ${direction}`;
10928
11090
  const selectStatement = `SELECT id, content, role, type, "createdAt", "createdAtZ", thread_id AS "threadId", "resourceId"`;
10929
11091
  const tableName = getTableName$3({
10930
11092
  indexName: TABLE_MESSAGES,
@@ -11072,7 +11234,7 @@ var MemoryPG = class MemoryPG extends MemoryStorage {
11072
11234
  const metadataFilter = validateStorageMetadataFilter(filter?.metadata);
11073
11235
  try {
11074
11236
  const { field, direction } = this.parseOrderBy(orderBy, "ASC");
11075
- const orderByStatement = `ORDER BY COALESCE("${field}Z", "${field}") ${direction}`;
11237
+ const orderByStatement = `ORDER BY "${field}" ${direction}`;
11076
11238
  const selectStatement = `SELECT id, content, role, type, "createdAt", "createdAtZ", thread_id AS "threadId", "resourceId"`;
11077
11239
  const tableName = getTableName$3({
11078
11240
  indexName: TABLE_MESSAGES,