@mastra/pg 1.22.0-alpha.0 → 1.22.0-alpha.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/CHANGELOG.md +110 -0
  2. package/dist/docs/SKILL.md +1 -1
  3. package/dist/docs/assets/SOURCE_MAP.json +1 -1
  4. package/dist/docs/references/docs-deployment-workers.md +2 -2
  5. package/dist/docs/references/docs-storage.md +2 -0
  6. package/dist/docs/references/integrations-databases-postgresql.md +26 -0
  7. package/dist/docs/references/reference-rag-vector-databases.md +4 -4
  8. package/dist/docs/references/reference-vectors-pg.md +2 -0
  9. package/dist/index.cjs +515 -133
  10. package/dist/index.cjs.map +1 -1
  11. package/dist/index.js +515 -133
  12. package/dist/index.js.map +1 -1
  13. package/dist/storage/db/sanitize-json.d.ts +11 -0
  14. package/dist/storage/db/sanitize-json.d.ts.map +1 -0
  15. package/dist/storage/domains/background-tasks/index.d.ts +3 -1
  16. package/dist/storage/domains/background-tasks/index.d.ts.map +1 -1
  17. package/dist/storage/domains/experiments/index.d.ts.map +1 -1
  18. package/dist/storage/domains/observability/v-next/ddl.d.ts +17 -0
  19. package/dist/storage/domains/observability/v-next/ddl.d.ts.map +1 -1
  20. package/dist/storage/domains/observability/v-next/discovery.d.ts +14 -0
  21. package/dist/storage/domains/observability/v-next/discovery.d.ts.map +1 -1
  22. package/dist/storage/domains/observability/v-next/helpers.d.ts.map +1 -1
  23. package/dist/storage/domains/observability/v-next/index.d.ts.map +1 -1
  24. package/dist/storage/domains/observability/v-next/signal-schema.d.ts +5 -1
  25. package/dist/storage/domains/observability/v-next/signal-schema.d.ts.map +1 -1
  26. package/dist/storage/domains/observability/v-next/sql.d.ts +29 -4
  27. package/dist/storage/domains/observability/v-next/sql.d.ts.map +1 -1
  28. package/dist/storage/domains/observability/v-next/traces.d.ts.map +1 -1
  29. package/dist/storage/domains/observability/v-next/tracing.d.ts +4 -3
  30. package/dist/storage/domains/observability/v-next/tracing.d.ts.map +1 -1
  31. package/dist/storage/domains/workflows/index.d.ts +2 -10
  32. package/dist/storage/domains/workflows/index.d.ts.map +1 -1
  33. package/dist/storage/factory-storage.d.ts.map +1 -1
  34. package/dist/vector/index.d.ts +60 -5
  35. package/dist/vector/index.d.ts.map +1 -1
  36. package/package.json +3 -3
package/dist/index.cjs CHANGED
@@ -565,11 +565,22 @@ function buildFilterQuery(filter, minScore, topK) {
565
565
  }
566
566
  //#endregion
567
567
  //#region src/vector/index.ts
568
- const MAX_UPSERT_ROWS_PER_STATEMENT = Math.floor(65535 / 3);
568
+ const DEFAULT_NAMESPACE = "default";
569
+ const MAX_UPSERT_ROWS_PER_STATEMENT = Math.floor(65535 / 4);
569
570
  var PgVector = class extends _mastra_core_vector.MastraVector {
570
571
  pool;
572
+ /**
573
+ * Cache for the public `getIndexInfo()`. Holds the in-flight promise rather than the
574
+ * resolved value so concurrent callers on a cold cache share a single round trip.
575
+ */
571
576
  describeIndexCache = /* @__PURE__ */ new Map();
577
+ /**
578
+ * Cache for the catalog-only index metadata used by the internal hot paths
579
+ * (cache warmup, query, upsert, updateVector, setupIndex). Also memoizes the promise.
580
+ */
581
+ indexMetadataCache = /* @__PURE__ */ new Map();
572
582
  createdIndexes = /* @__PURE__ */ new Map();
583
+ namespaceReadyIndexes = /* @__PURE__ */ new Set();
573
584
  indexVectorTypes = /* @__PURE__ */ new Map();
574
585
  mutexesByName = /* @__PURE__ */ new Map();
575
586
  schema;
@@ -625,7 +636,7 @@ var PgVector = class extends _mastra_core_vector.MastraVector {
625
636
  try {
626
637
  const existingIndexes = await this.listIndexes();
627
638
  await Promise.all(existingIndexes.map(async (indexName) => {
628
- const info = await this.getIndexInfo({ indexName });
639
+ const info = await this.getIndexMetadata({ indexName });
629
640
  const key = await this.getIndexCacheKey({
630
641
  indexName,
631
642
  metric: info.metric,
@@ -798,20 +809,76 @@ var PgVector = class extends _mastra_core_vector.MastraVector {
798
809
  const quotedVectorName = `"${parsedIndexName}_vector_idx"`;
799
810
  return {
800
811
  tableName: quotedSchemaName ? `${quotedSchemaName}.${quotedIndexName}` : quotedIndexName,
801
- vectorIndexName: quotedVectorName
812
+ vectorIndexName: quotedVectorName,
813
+ parsedIndexName
802
814
  };
803
815
  }
804
816
  getSchemaName() {
805
817
  return this.schema ? `"${(0, _mastra_core_utils.parseSqlIdentifier)(this.schema, "schema name")}"` : void 0;
806
818
  }
819
+ async ensureNamespaceSchema(indexName, client) {
820
+ const { tableName, parsedIndexName } = this.getTableName(indexName);
821
+ const schemaName = this.schema ? (0, _mastra_core_utils.parseSqlIdentifier)(this.schema, "schema name") : "public";
822
+ if ((await client.query(`SELECT 1
823
+ FROM information_schema.columns
824
+ WHERE table_schema = $1 AND table_name = $2 AND column_name = 'vector_id'`, [schemaName, parsedIndexName])).rowCount === 0) return;
825
+ await client.query(`ALTER TABLE ${tableName} ADD COLUMN IF NOT EXISTS namespace VARCHAR(255) NOT NULL DEFAULT '${DEFAULT_NAMESPACE}'`);
826
+ const legacyConstraints = await client.query(`SELECT c.conname
827
+ FROM pg_constraint c
828
+ JOIN pg_class t ON t.oid = c.conrelid
829
+ JOIN pg_namespace n ON n.oid = t.relnamespace
830
+ WHERE n.nspname = $1
831
+ AND t.relname = $2
832
+ AND c.contype = 'u'
833
+ AND pg_get_constraintdef(c.oid) = 'UNIQUE (vector_id)'`, [schemaName, parsedIndexName]);
834
+ for (const { conname } of legacyConstraints.rows) {
835
+ const parsedConstraintName = (0, _mastra_core_utils.parseSqlIdentifier)(conname, "constraint name");
836
+ await client.query(`ALTER TABLE ${tableName} DROP CONSTRAINT "${parsedConstraintName}"`);
837
+ }
838
+ const namespaceIndexName = (0, _mastra_core_utils.parseSqlIdentifier)(`${parsedIndexName}_namespace_vector_id_idx`, "index name");
839
+ await client.query(`CREATE UNIQUE INDEX IF NOT EXISTS "${namespaceIndexName}" ON ${tableName} (namespace, vector_id)`);
840
+ }
807
841
  transformFilter(filter) {
808
842
  return new PGFilterTranslator().translate(filter);
809
843
  }
844
+ /**
845
+ * Cached variant of {@link describeIndex}, including the row count.
846
+ *
847
+ * Internal code paths do not use this - they use the catalog-only
848
+ * {@link getIndexMetadata}, which never scans the table.
849
+ */
810
850
  async getIndexInfo({ indexName }) {
811
- if (!this.describeIndexCache.has(indexName)) this.describeIndexCache.set(indexName, await this.describeIndex({ indexName }));
812
- return this.describeIndexCache.get(indexName);
851
+ return this.memoize(this.describeIndexCache, indexName, () => this.describeIndex({ indexName }));
852
+ }
853
+ /**
854
+ * Cached index metadata read from the Postgres catalog only.
855
+ *
856
+ * This is what every internal caller needs: none of them read `count`, and paying for
857
+ * `SELECT COUNT(*)` on a large index costs a full heap scan per call.
858
+ */
859
+ getIndexMetadata({ indexName }) {
860
+ return this.memoize(this.indexMetadataCache, indexName, () => this.describeIndexMetadata({ indexName }));
861
+ }
862
+ /**
863
+ * Stores the in-flight promise in `cache` so concurrent callers on a cold cache share one
864
+ * round trip, and drops the entry if it rejects so a transient failure is not cached forever.
865
+ */
866
+ memoize(cache, key, load) {
867
+ const cached = cache.get(key);
868
+ if (cached) return cached;
869
+ const pending = load();
870
+ cache.set(key, pending);
871
+ pending.catch(() => {
872
+ if (cache.get(key) === pending) cache.delete(key);
873
+ });
874
+ return pending;
875
+ }
876
+ /** Drops every cached view of an index, e.g. after its index definition or table changed. */
877
+ invalidateIndexCaches(indexName) {
878
+ this.describeIndexCache.delete(indexName);
879
+ this.indexMetadataCache.delete(indexName);
813
880
  }
814
- async query({ indexName, queryVector, topK = 10, filter, includeVector = false, minScore = -1, ef, probes }) {
881
+ async query({ indexName, queryVector, topK = 10, filter, includeVector = false, minScore = -1, ef, probes, namespace = DEFAULT_NAMESPACE }) {
815
882
  try {
816
883
  (0, _mastra_core_vector.validateTopK)("PG", topK);
817
884
  if (queryVector !== void 0) {
@@ -832,16 +899,23 @@ var PgVector = class extends _mastra_core_vector.MastraVector {
832
899
  try {
833
900
  const { sql: filterQuery, values: filterValues } = buildDeleteFilterQuery(this.transformFilter(filter));
834
901
  const { tableName } = this.getTableName(indexName);
902
+ const filterClause = filterQuery.trim().replace(/^WHERE\s+/i, "");
903
+ const namespaceParam = filterValues.length + 1;
835
904
  const query = `
836
905
  SELECT
837
906
  vector_id as id,
838
907
  metadata
839
908
  ${includeVector ? ", embedding" : ""}
840
909
  FROM ${tableName}
841
- ${filterQuery}
910
+ WHERE namespace = $${namespaceParam}
911
+ ${filterClause ? `AND (${filterClause})` : ""}
842
912
  ORDER BY vector_id
843
- LIMIT $${filterValues.length + 1}`;
844
- return (await client.query(query, [...filterValues, topK])).rows.map(({ id, metadata, embedding }) => ({
913
+ LIMIT $${namespaceParam + 1}`;
914
+ return (await client.query(query, [
915
+ ...filterValues,
916
+ namespace,
917
+ topK
918
+ ])).rows.map(({ id, metadata, embedding }) => ({
845
919
  id,
846
920
  score: 0,
847
921
  metadata,
@@ -865,7 +939,7 @@ var PgVector = class extends _mastra_core_vector.MastraVector {
865
939
  await this.ensureSearchPath(client);
866
940
  await client.query("BEGIN");
867
941
  const { sql: filterQuery, values: filterValues } = buildFilterQuery(this.transformFilter(filter), minScore, topK);
868
- const indexInfo = await this.getIndexInfo({ indexName });
942
+ const indexInfo = await this.getIndexMetadata({ indexName });
869
943
  const metric = indexInfo.metric ?? "cosine";
870
944
  const ops = this.getVectorOps(indexInfo.vectorType, metric);
871
945
  const vectorStr = ops.formatVector(queryVector, indexInfo.dimension);
@@ -873,14 +947,18 @@ var PgVector = class extends _mastra_core_vector.MastraVector {
873
947
  const calculatedEf = ef ?? Math.max(topK, (indexInfo?.config?.m ?? 16) * topK);
874
948
  const searchEf = Math.min(1e3, Math.max(1, calculatedEf));
875
949
  await client.query(`SET LOCAL hnsw.ef_search = ${searchEf}`);
950
+ await client.query(`SET LOCAL hnsw.iterative_scan = strict_order`);
876
951
  }
877
952
  if (indexInfo.type === "ivfflat" && probes) await client.query(`SET LOCAL ivfflat.probes = ${probes}`);
878
953
  const { tableName } = this.getTableName(indexName);
879
954
  const qualifiedVectorType = this.getVectorTypeName(indexInfo.vectorType, indexInfo.dimension);
880
955
  const distanceExpr = `embedding ${ops.distanceOperator} '${vectorStr}'::${qualifiedVectorType}`;
881
956
  const scoreExpr = ops.scoreExpr(distanceExpr);
882
- const hasFilter = filterQuery.trim().length > 0;
883
- const query = indexInfo.type === "hnsw" && !hasFilter && minScore <= 0 ? `
957
+ const filterClause = filterQuery.trim().replace(/^WHERE\s+/i, "");
958
+ const hasFilter = filterClause.length > 0;
959
+ const useIndexedOrder = indexInfo.type === "hnsw" && !hasFilter && minScore <= 0;
960
+ const namespaceParam = filterValues.length + 1;
961
+ const query = useIndexedOrder ? `
884
962
  WITH vector_scores AS (
885
963
  SELECT
886
964
  vector_id as id,
@@ -888,6 +966,7 @@ var PgVector = class extends _mastra_core_vector.MastraVector {
888
966
  metadata
889
967
  ${includeVector ? ", embedding" : ""}
890
968
  FROM ${tableName}
969
+ WHERE namespace = $${namespaceParam}
891
970
  ORDER BY ${distanceExpr}
892
971
  LIMIT $2
893
972
  )
@@ -902,14 +981,15 @@ var PgVector = class extends _mastra_core_vector.MastraVector {
902
981
  metadata
903
982
  ${includeVector ? ", embedding" : ""}
904
983
  FROM ${tableName}
905
- ${filterQuery}
984
+ WHERE namespace = $${namespaceParam}
985
+ ${filterClause ? `AND (${filterClause})` : ""}
906
986
  )
907
987
  SELECT *
908
988
  FROM vector_scores
909
989
  WHERE score > $1
910
990
  ORDER BY score DESC
911
991
  LIMIT $2`;
912
- const result = await client.query(query, filterValues);
992
+ const result = await client.query(query, [...filterValues, namespace]);
913
993
  await client.query("COMMIT");
914
994
  return result.rows.map(({ id, score, metadata, embedding }) => ({
915
995
  id,
@@ -931,7 +1011,7 @@ var PgVector = class extends _mastra_core_vector.MastraVector {
931
1011
  client.release();
932
1012
  }
933
1013
  }
934
- async upsert({ indexName, vectors, metadata, ids, deleteFilter }) {
1014
+ async upsert({ indexName, vectors, metadata, ids, deleteFilter, namespace = DEFAULT_NAMESPACE }) {
935
1015
  (0, _mastra_core_vector.validateUpsertInput)("PG", vectors, metadata, ids);
936
1016
  const { tableName } = this.getTableName(indexName);
937
1017
  const client = await this.pool.connect();
@@ -946,8 +1026,8 @@ var PgVector = class extends _mastra_core_vector.MastraVector {
946
1026
  const { sql: filterQuery, values: filterValues } = buildDeleteFilterQuery(this.transformFilter(deleteFilter));
947
1027
  const whereClause = filterQuery.trim().replace(/^WHERE\s+/i, "");
948
1028
  if (whereClause) {
949
- const deleteQuery = `DELETE FROM ${tableName} WHERE ${whereClause}`;
950
- const result = await client.query(deleteQuery, filterValues);
1029
+ const deleteQuery = `DELETE FROM ${tableName} WHERE namespace = $${filterValues.length + 1} AND (${whereClause})`;
1030
+ const result = await client.query(deleteQuery, [...filterValues, namespace]);
951
1031
  this.logger?.debug(`Deleted ${result.rowCount || 0} vectors before upsert`, {
952
1032
  indexName,
953
1033
  deletedCount: result.rowCount || 0
@@ -955,15 +1035,15 @@ var PgVector = class extends _mastra_core_vector.MastraVector {
955
1035
  }
956
1036
  }
957
1037
  const vectorIds = ids || vectors.map(() => crypto.randomUUID());
958
- const indexInfo = await this.getIndexInfo({ indexName });
1038
+ const indexInfo = await this.getIndexMetadata({ indexName });
959
1039
  const qualifiedVectorType = this.getVectorTypeName(indexInfo.vectorType, indexInfo.dimension);
960
1040
  const ops = this.getVectorOps(indexInfo.vectorType, indexInfo.metric ?? "cosine");
961
1041
  if (new Set(vectorIds).size !== vectorIds.length) for (let i = 0; i < vectors.length; i++) {
962
1042
  const vectorStr = ops.formatVector(vectors[i], indexInfo.dimension);
963
1043
  const query = `
964
- INSERT INTO ${tableName} (vector_id, embedding, metadata)
965
- VALUES ($1, $2::${qualifiedVectorType}, $3::jsonb)
966
- ON CONFLICT (vector_id)
1044
+ INSERT INTO ${tableName} (vector_id, embedding, metadata, namespace)
1045
+ VALUES ($1, $2::${qualifiedVectorType}, $3::jsonb, $4)
1046
+ ON CONFLICT (namespace, vector_id)
967
1047
  DO UPDATE SET
968
1048
  embedding = $2::${qualifiedVectorType},
969
1049
  metadata = $3::jsonb
@@ -971,7 +1051,8 @@ var PgVector = class extends _mastra_core_vector.MastraVector {
971
1051
  await client.query(query, [
972
1052
  vectorIds[i],
973
1053
  vectorStr,
974
- JSON.stringify(metadata?.[i] || {})
1054
+ JSON.stringify(metadata?.[i] || {}),
1055
+ namespace
975
1056
  ]);
976
1057
  }
977
1058
  else for (let start = 0; start < vectors.length; start += MAX_UPSERT_ROWS_PER_STATEMENT) {
@@ -980,13 +1061,13 @@ var PgVector = class extends _mastra_core_vector.MastraVector {
980
1061
  const values = [];
981
1062
  for (let i = start; i < end; i++) {
982
1063
  const base = values.length;
983
- rows.push(`($${base + 1}, $${base + 2}::${qualifiedVectorType}, $${base + 3}::jsonb)`);
984
- values.push(vectorIds[i], ops.formatVector(vectors[i], indexInfo.dimension), JSON.stringify(metadata?.[i] || {}));
1064
+ rows.push(`($${base + 1}, $${base + 2}::${qualifiedVectorType}, $${base + 3}::jsonb, $${base + 4})`);
1065
+ values.push(vectorIds[i], ops.formatVector(vectors[i], indexInfo.dimension), JSON.stringify(metadata?.[i] || {}), namespace);
985
1066
  }
986
1067
  const query = `
987
- INSERT INTO ${tableName} (vector_id, embedding, metadata)
1068
+ INSERT INTO ${tableName} (vector_id, embedding, metadata, namespace)
988
1069
  VALUES ${rows.join(", ")}
989
- ON CONFLICT (vector_id)
1070
+ ON CONFLICT (namespace, vector_id)
990
1071
  DO UPDATE SET
991
1072
  embedding = EXCLUDED.embedding,
992
1073
  metadata = EXCLUDED.metadata
@@ -1106,9 +1187,9 @@ var PgVector = class extends _mastra_core_vector.MastraVector {
1106
1187
  vectorType,
1107
1188
  metadataIndexes
1108
1189
  });
1109
- if (this.cachedIndexExists(indexName, indexCacheKey)) return;
1190
+ if (this.cachedIndexExists(indexName, indexCacheKey) && this.namespaceReadyIndexes.has(indexName)) return;
1110
1191
  await this.getMutexByName(`create-${indexName}`).runExclusive(async () => {
1111
- if (this.cachedIndexExists(indexName, indexCacheKey)) return;
1192
+ if (this.cachedIndexExists(indexName, indexCacheKey) && this.namespaceReadyIndexes.has(indexName)) return;
1112
1193
  const client = await this.pool.connect();
1113
1194
  try {
1114
1195
  await this.setupSchema(client);
@@ -1152,12 +1233,15 @@ var PgVector = class extends _mastra_core_vector.MastraVector {
1152
1233
  await client.query(`
1153
1234
  CREATE TABLE IF NOT EXISTS ${tableName} (
1154
1235
  id SERIAL PRIMARY KEY,
1155
- vector_id TEXT UNIQUE NOT NULL,
1236
+ vector_id TEXT NOT NULL,
1156
1237
  embedding ${qualifiedVectorType}(${dimension}),
1157
- metadata JSONB DEFAULT '{}'::jsonb
1238
+ metadata JSONB DEFAULT '{}'::jsonb,
1239
+ namespace VARCHAR(255) NOT NULL DEFAULT '${DEFAULT_NAMESPACE}'
1158
1240
  );
1159
1241
  `);
1242
+ await this.ensureNamespaceSchema(indexName, client);
1160
1243
  this.createdIndexes.set(indexName, indexCacheKey);
1244
+ this.namespaceReadyIndexes.add(indexName);
1161
1245
  this.indexVectorTypes.set(indexName, vectorType);
1162
1246
  if (buildIndex) await this.setupIndex({
1163
1247
  indexName,
@@ -1168,6 +1252,7 @@ var PgVector = class extends _mastra_core_vector.MastraVector {
1168
1252
  if (metadataIndexes?.length) await this.createMetadataIndexes(tableName, indexName, metadataIndexes);
1169
1253
  } catch (error) {
1170
1254
  this.createdIndexes.delete(indexName);
1255
+ this.namespaceReadyIndexes.delete(indexName);
1171
1256
  this.indexVectorTypes.delete(indexName);
1172
1257
  throw error;
1173
1258
  } finally {
@@ -1238,7 +1323,7 @@ var PgVector = class extends _mastra_core_vector.MastraVector {
1238
1323
  let existingIndexInfo = null;
1239
1324
  let dimension = 0;
1240
1325
  try {
1241
- existingIndexInfo = await this.getIndexInfo({ indexName });
1326
+ existingIndexInfo = await this.getIndexMetadata({ indexName });
1242
1327
  dimension = existingIndexInfo.dimension;
1243
1328
  if (isConfigEmpty && existingIndexInfo.metric === metric) if (existingIndexInfo.type === "flat") this.logger?.debug(`No index exists for ${vectorIndexName}, will create default ivfflat index`);
1244
1329
  else {
@@ -1273,12 +1358,12 @@ var PgVector = class extends _mastra_core_vector.MastraVector {
1273
1358
  }
1274
1359
  this.logger?.info(`Index ${vectorIndexName} configuration changed, rebuilding index`);
1275
1360
  await client.query(`DROP INDEX IF EXISTS ${vectorIndexName}`);
1276
- this.describeIndexCache.delete(indexName);
1361
+ this.invalidateIndexCaches(indexName);
1277
1362
  } catch {
1278
1363
  this.logger?.debug(`Index ${indexName} doesn't exist yet, will create it`);
1279
1364
  }
1280
1365
  if (indexType === "flat") {
1281
- this.describeIndexCache.delete(indexName);
1366
+ this.invalidateIndexCaches(indexName);
1282
1367
  return;
1283
1368
  }
1284
1369
  await this.ensureSearchPath(client);
@@ -1428,6 +1513,19 @@ var PgVector = class extends _mastra_core_vector.MastraVector {
1428
1513
  * @returns A promise that resolves to the index statistics including dimension, count and metric
1429
1514
  */
1430
1515
  async describeIndex({ indexName }) {
1516
+ const metadata = await this.describeIndexMetadata({ indexName });
1517
+ const count = await this.countIndexRows({ indexName });
1518
+ return {
1519
+ ...metadata,
1520
+ count
1521
+ };
1522
+ }
1523
+ /**
1524
+ * Reads the index metadata that lives in the Postgres catalog. Unlike
1525
+ * {@link describeIndex} it issues no `COUNT(*)`, so its cost does not grow with the
1526
+ * number of rows in the table.
1527
+ */
1528
+ async describeIndexMetadata({ indexName }) {
1431
1529
  const client = await this.pool.connect();
1432
1530
  try {
1433
1531
  const { tableName } = this.getTableName(indexName);
@@ -1447,10 +1545,6 @@ var PgVector = class extends _mastra_core_vector.MastraVector {
1447
1545
  FROM pg_attribute
1448
1546
  WHERE attrelid = $1::regclass
1449
1547
  AND attname = 'embedding';
1450
- `;
1451
- const countQuery = `
1452
- SELECT COUNT(*) as count
1453
- FROM ${tableName};
1454
1548
  `;
1455
1549
  const indexQuery = `
1456
1550
  SELECT
@@ -1466,7 +1560,6 @@ var PgVector = class extends _mastra_core_vector.MastraVector {
1466
1560
  AND n.nspname = $2;
1467
1561
  `;
1468
1562
  const dimResult = await client.query(dimensionQuery, [tableName]);
1469
- const countResult = await client.query(countQuery);
1470
1563
  const { index_method, index_def, operator_class } = (await client.query(indexQuery, [`${indexName}_vector_idx`, this.schema || "public"])).rows[0] || {
1471
1564
  index_method: "flat",
1472
1565
  index_def: "",
@@ -1485,7 +1578,6 @@ var PgVector = class extends _mastra_core_vector.MastraVector {
1485
1578
  }
1486
1579
  return {
1487
1580
  dimension: dimResult.rows[0].dimension,
1488
- count: parseInt(countResult.rows[0].count),
1489
1581
  metric,
1490
1582
  type: index_method,
1491
1583
  vectorType,
@@ -1505,14 +1597,41 @@ var PgVector = class extends _mastra_core_vector.MastraVector {
1505
1597
  client.release();
1506
1598
  }
1507
1599
  }
1600
+ /**
1601
+ * Exact row count for an index. This is a full scan of the table, so it is only issued
1602
+ * for the public {@link describeIndex}, never from an internal code path.
1603
+ */
1604
+ async countIndexRows({ indexName }) {
1605
+ const client = await this.pool.connect();
1606
+ try {
1607
+ const { tableName } = this.getTableName(indexName);
1608
+ const countResult = await client.query(`
1609
+ SELECT COUNT(*) as count
1610
+ FROM ${tableName};
1611
+ `);
1612
+ return parseInt(countResult.rows[0].count);
1613
+ } catch (e) {
1614
+ const mastraError = new _mastra_core_error.MastraError({
1615
+ id: (0, _mastra_core_storage.createVectorErrorId)("PG", "DESCRIBE_INDEX", "FAILED"),
1616
+ domain: _mastra_core_error.ErrorDomain.MASTRA_VECTOR,
1617
+ category: _mastra_core_error.ErrorCategory.THIRD_PARTY,
1618
+ details: { indexName }
1619
+ }, e);
1620
+ this.logger?.trackException(mastraError);
1621
+ throw mastraError;
1622
+ } finally {
1623
+ client.release();
1624
+ }
1625
+ }
1508
1626
  async deleteIndex({ indexName }) {
1509
1627
  const client = await this.pool.connect();
1510
1628
  try {
1511
1629
  const { tableName } = this.getTableName(indexName);
1512
1630
  await client.query(`DROP TABLE IF EXISTS ${tableName} CASCADE`);
1513
1631
  this.createdIndexes.delete(indexName);
1632
+ this.namespaceReadyIndexes.delete(indexName);
1514
1633
  this.indexVectorTypes.delete(indexName);
1515
- this.describeIndexCache.delete(indexName);
1634
+ this.invalidateIndexCaches(indexName);
1516
1635
  } catch (error) {
1517
1636
  await client.query("ROLLBACK");
1518
1637
  const mastraError = new _mastra_core_error.MastraError({
@@ -1562,7 +1681,7 @@ var PgVector = class extends _mastra_core_vector.MastraVector {
1562
1681
  * @returns A promise that resolves when the update is complete.
1563
1682
  * @throws Will throw an error if no updates are provided or if the update operation fails.
1564
1683
  */
1565
- async updateVector({ indexName, id, filter, update }) {
1684
+ async updateVector({ indexName, id, filter, update, namespace = DEFAULT_NAMESPACE }) {
1566
1685
  let client;
1567
1686
  try {
1568
1687
  if (!update.vector && !update.metadata) throw new Error("No updates provided");
@@ -1583,7 +1702,7 @@ var PgVector = class extends _mastra_core_vector.MastraVector {
1583
1702
  client = await this.pool.connect();
1584
1703
  await this.ensureSearchPath(client);
1585
1704
  const { tableName } = this.getTableName(indexName);
1586
- const indexInfo = await this.getIndexInfo({ indexName });
1705
+ const indexInfo = await this.getIndexMetadata({ indexName });
1587
1706
  const qualifiedVectorType = this.getVectorTypeName(indexInfo.vectorType, indexInfo.dimension);
1588
1707
  const ops = this.getVectorOps(indexInfo.vectorType, indexInfo.metric ?? "cosine");
1589
1708
  let updateParts = [];
@@ -1602,8 +1721,11 @@ var PgVector = class extends _mastra_core_vector.MastraVector {
1602
1721
  if (updateParts.length === 0) return;
1603
1722
  let whereClause;
1604
1723
  let whereValues;
1724
+ const namespaceIndex = valueIndex;
1725
+ values.push(namespace);
1726
+ valueIndex++;
1605
1727
  if (id) {
1606
- whereClause = `vector_id = $${valueIndex}`;
1728
+ whereClause = `namespace = $${namespaceIndex} AND vector_id = $${valueIndex}`;
1607
1729
  whereValues = [id];
1608
1730
  } else {
1609
1731
  if (!filter || Object.keys(filter).length === 0) throw new _mastra_core_error.MastraError({
@@ -1628,6 +1750,7 @@ var PgVector = class extends _mastra_core_vector.MastraVector {
1628
1750
  whereClause = whereClause.replace(/\$(\d+)/g, (match, num) => {
1629
1751
  return `$${parseInt(num) + valueIndex - 1}`;
1630
1752
  });
1753
+ whereClause = `namespace = $${namespaceIndex} AND (${whereClause})`;
1631
1754
  whereValues = filterValues;
1632
1755
  }
1633
1756
  const query = `
@@ -1667,16 +1790,16 @@ var PgVector = class extends _mastra_core_vector.MastraVector {
1667
1790
  * @returns A promise that resolves when the deletion is complete.
1668
1791
  * @throws Will throw an error if the deletion operation fails.
1669
1792
  */
1670
- async deleteVector({ indexName, id }) {
1793
+ async deleteVector({ indexName, id, namespace = DEFAULT_NAMESPACE }) {
1671
1794
  let client;
1672
1795
  try {
1673
1796
  client = await this.pool.connect();
1674
1797
  const { tableName } = this.getTableName(indexName);
1675
1798
  const query = `
1676
1799
  DELETE FROM ${tableName}
1677
- WHERE vector_id = $1
1800
+ WHERE vector_id = $1 AND namespace = $2
1678
1801
  `;
1679
- await client.query(query, [id]);
1802
+ await client.query(query, [id, namespace]);
1680
1803
  } catch (error) {
1681
1804
  const mastraError = new _mastra_core_error.MastraError({
1682
1805
  id: (0, _mastra_core_storage.createVectorErrorId)("PG", "DELETE_VECTOR", "FAILED"),
@@ -1700,14 +1823,15 @@ var PgVector = class extends _mastra_core_vector.MastraVector {
1700
1823
  * @returns A promise that resolves when the deletion is complete.
1701
1824
  * @throws Will throw an error if the deletion operation fails.
1702
1825
  */
1703
- async deleteVectors({ indexName, filter, ids }) {
1826
+ async deleteVectors({ indexName, filter, ids, namespace }) {
1704
1827
  let client;
1828
+ const effectiveNamespace = namespace ?? DEFAULT_NAMESPACE;
1705
1829
  try {
1706
1830
  client = await this.pool.connect();
1707
1831
  const { tableName } = this.getTableName(indexName);
1708
- if (!filter && !ids) throw new _mastra_core_error.MastraError({
1832
+ if (!filter && !ids && namespace === void 0) throw new _mastra_core_error.MastraError({
1709
1833
  id: (0, _mastra_core_storage.createVectorErrorId)("PG", "DELETE_VECTORS", "NO_TARGET"),
1710
- text: "Either filter or ids must be provided",
1834
+ text: "Either filter or ids must be provided, unless an explicit namespace is used",
1711
1835
  domain: _mastra_core_error.ErrorDomain.MASTRA_VECTOR,
1712
1836
  category: _mastra_core_error.ErrorCategory.USER,
1713
1837
  details: { indexName }
@@ -1729,9 +1853,9 @@ var PgVector = class extends _mastra_core_vector.MastraVector {
1729
1853
  category: _mastra_core_error.ErrorCategory.USER,
1730
1854
  details: { indexName }
1731
1855
  });
1732
- query = `DELETE FROM ${tableName} WHERE vector_id IN (${ids.map((_, i) => `$${i + 1}`).join(", ")})`;
1733
- values = ids;
1734
- } else {
1856
+ query = `DELETE FROM ${tableName} WHERE vector_id IN (${ids.map((_, i) => `$${i + 1}`).join(", ")}) AND namespace = $${ids.length + 1}`;
1857
+ values = [...ids, effectiveNamespace];
1858
+ } else if (filter) {
1735
1859
  if (!filter || Object.keys(filter).length === 0) throw new _mastra_core_error.MastraError({
1736
1860
  id: (0, _mastra_core_storage.createVectorErrorId)("PG", "DELETE_VECTORS", "EMPTY_FILTER"),
1737
1861
  text: "Cannot delete with empty filter. Use deleteIndex to delete all vectors.",
@@ -1751,8 +1875,11 @@ var PgVector = class extends _mastra_core_vector.MastraVector {
1751
1875
  filter: JSON.stringify(filter)
1752
1876
  }
1753
1877
  });
1754
- query = `DELETE FROM ${tableName} WHERE ${whereClause}`;
1755
- values = filterValues;
1878
+ query = `DELETE FROM ${tableName} WHERE namespace = $${filterValues.length + 1} AND (${whereClause})`;
1879
+ values = [...filterValues, effectiveNamespace];
1880
+ } else {
1881
+ query = `DELETE FROM ${tableName} WHERE namespace = $1`;
1882
+ values = [effectiveNamespace];
1756
1883
  }
1757
1884
  const result = await client.query(query, values);
1758
1885
  this.logger?.info(`Deleted ${result.rowCount || 0} vectors from ${indexName}`, {
@@ -4918,7 +5045,7 @@ var BackgroundTasksPG = class BackgroundTasksPG extends _mastra_core_storage.Bac
4918
5045
  }
4919
5046
  });
4920
5047
  }
4921
- async updateTask(taskId, update) {
5048
+ async updateTask(taskId, update, options) {
4922
5049
  const setClauses = [];
4923
5050
  const params = [];
4924
5051
  let paramIdx = 1;
@@ -4960,10 +5087,15 @@ var BackgroundTasksPG = class BackgroundTasksPG extends _mastra_core_storage.Bac
4960
5087
  const val = update.completedAt?.toISOString() ?? null;
4961
5088
  params.push(val, val);
4962
5089
  }
4963
- if (setClauses.length === 0) return;
5090
+ if (setClauses.length === 0) return false;
4964
5091
  const table = getTableName$4(getSchemaName$4(this.#schema));
4965
5092
  params.push(taskId);
4966
- await this.#db.client.none(`UPDATE ${table} SET ${setClauses.join(", ")} WHERE "id" = $${paramIdx}`, params);
5093
+ let where = `"id" = $${paramIdx++}`;
5094
+ if (options?.expectedStatus) {
5095
+ where += ` AND "status" = $${paramIdx}`;
5096
+ params.push(options.expectedStatus);
5097
+ }
5098
+ return ((await this.#db.client.query(`UPDATE ${table} SET ${setClauses.join(", ")} WHERE ${where}`, params)).rowCount ?? 0) > 0;
4967
5099
  }
4968
5100
  async getTask(taskId) {
4969
5101
  const table = getTableName$4(getSchemaName$4(this.#schema));
@@ -6608,6 +6740,7 @@ var ExperimentsPG = class ExperimentsPG extends _mastra_core_storage.Experiments
6608
6740
  "tags",
6609
6741
  "comment",
6610
6742
  "toolMockReport",
6743
+ "metadata",
6611
6744
  "organizationId",
6612
6745
  "projectId",
6613
6746
  "attempt"
@@ -6804,6 +6937,7 @@ var ExperimentsPG = class ExperimentsPG extends _mastra_core_storage.Experiments
6804
6937
  input: (0, _mastra_core_storage.safelyParseJSON)(row.input),
6805
6938
  output: row.output ? (0, _mastra_core_storage.safelyParseJSON)(row.output) : null,
6806
6939
  groundTruth: row.groundTruth ? (0, _mastra_core_storage.safelyParseJSON)(row.groundTruth) : null,
6940
+ metadata: row.metadata ? (0, _mastra_core_storage.safelyParseJSON)(row.metadata) : null,
6807
6941
  error: row.error ? (0, _mastra_core_storage.safelyParseJSON)(row.error) : null,
6808
6942
  startedAt: (0, _mastra_core_storage.ensureDate)(row.startedAtZ || row.startedAt),
6809
6943
  completedAt: (0, _mastra_core_storage.ensureDate)(row.completedAtZ || row.completedAt),
@@ -7112,6 +7246,7 @@ var ExperimentsPG = class ExperimentsPG extends _mastra_core_storage.Experiments
7112
7246
  input: input.input,
7113
7247
  output: input.output ?? null,
7114
7248
  groundTruth: input.groundTruth ?? null,
7249
+ metadata: input.metadata ?? null,
7115
7250
  error: input.error ?? null,
7116
7251
  startedAt: input.startedAt.toISOString(),
7117
7252
  completedAt: input.completedAt.toISOString(),
@@ -7134,6 +7269,7 @@ var ExperimentsPG = class ExperimentsPG extends _mastra_core_storage.Experiments
7134
7269
  input: input.input,
7135
7270
  output: input.output ?? null,
7136
7271
  groundTruth: input.groundTruth ?? null,
7272
+ metadata: input.metadata ?? null,
7137
7273
  error: input.error ?? null,
7138
7274
  startedAt: input.startedAt,
7139
7275
  completedAt: input.completedAt,
@@ -7170,9 +7306,9 @@ var ExperimentsPG = class ExperimentsPG extends _mastra_core_storage.Experiments
7170
7306
  const id = crypto.randomUUID();
7171
7307
  return t.one(`INSERT INTO ${tableName} (
7172
7308
  "id", "experimentId", "itemId", "itemDatasetVersion", "organizationId", "projectId",
7173
- "input", "output", "groundTruth", "error", "startedAt", "completedAt",
7309
+ "input", "output", "groundTruth", "metadata", "error", "startedAt", "completedAt",
7174
7310
  "retryCount", "attempt", "traceId", "status", "tags", "toolMockReport", "createdAt"
7175
- ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19)
7311
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20)
7176
7312
  RETURNING *`, [
7177
7313
  id,
7178
7314
  input.experimentId,
@@ -7183,6 +7319,7 @@ var ExperimentsPG = class ExperimentsPG extends _mastra_core_storage.Experiments
7183
7319
  JSON.stringify(input.input),
7184
7320
  input.output != null ? JSON.stringify(input.output) : null,
7185
7321
  input.groundTruth != null ? JSON.stringify(input.groundTruth) : null,
7322
+ input.metadata != null ? JSON.stringify(input.metadata) : null,
7186
7323
  input.error != null ? JSON.stringify(input.error) : null,
7187
7324
  input.startedAt.toISOString(),
7188
7325
  input.completedAt.toISOString(),
@@ -7197,9 +7334,9 @@ var ExperimentsPG = class ExperimentsPG extends _mastra_core_storage.Experiments
7197
7334
  }
7198
7335
  return t.one(`UPDATE ${tableName} SET
7199
7336
  "itemDatasetVersion" = $2, "organizationId" = $3, "projectId" = $4,
7200
- "input" = $5, "output" = $6, "groundTruth" = $7, "error" = $8,
7201
- "startedAt" = $9, "completedAt" = $10, "retryCount" = $11, "attempt" = $12,
7202
- "traceId" = $13, "status" = $14, "tags" = $15, "toolMockReport" = $16
7337
+ "input" = $5, "output" = $6, "groundTruth" = $7, "metadata" = $8, "error" = $9,
7338
+ "startedAt" = $10, "completedAt" = $11, "retryCount" = $12, "attempt" = $13,
7339
+ "traceId" = $14, "status" = $15, "tags" = $16, "toolMockReport" = $17
7203
7340
  WHERE "id" = $1 RETURNING *`, [
7204
7341
  existing.id,
7205
7342
  input.itemDatasetVersion ?? null,
@@ -7208,6 +7345,7 @@ var ExperimentsPG = class ExperimentsPG extends _mastra_core_storage.Experiments
7208
7345
  JSON.stringify(input.input),
7209
7346
  input.output != null ? JSON.stringify(input.output) : null,
7210
7347
  input.groundTruth != null ? JSON.stringify(input.groundTruth) : null,
7348
+ input.metadata != null ? JSON.stringify(input.metadata) : null,
7211
7349
  input.error != null ? JSON.stringify(input.error) : null,
7212
7350
  input.startedAt.toISOString(),
7213
7351
  input.completedAt.toISOString(),
@@ -13591,6 +13729,11 @@ const SPAN_EVENT_COLUMNS = [
13591
13729
  type: "boolean",
13592
13730
  defaultSql: "false"
13593
13731
  },
13732
+ {
13733
+ name: "isPending",
13734
+ type: "boolean",
13735
+ defaultSql: "false"
13736
+ },
13594
13737
  {
13595
13738
  name: "startedAt",
13596
13739
  type: "timestamptz"
@@ -13887,6 +14030,7 @@ const SPAN_LIGHT_SELECT_COLUMN_NAMES = [
13887
14030
  "spanType",
13888
14031
  "error",
13889
14032
  "isEvent",
14033
+ "isPending",
13890
14034
  "startedAt",
13891
14035
  "endedAt"
13892
14036
  ];
@@ -14281,6 +14425,26 @@ function allTableDDL(schema, mode) {
14281
14425
  discoveryTableDDL(schema)
14282
14426
  ];
14283
14427
  }
14428
+ /**
14429
+ * Additive column migrations for tables created by an older version of this
14430
+ * schema. `CREATE TABLE IF NOT EXISTS` is a no-op on an existing table, so new
14431
+ * columns have to be added out of band.
14432
+ *
14433
+ * `ALTER TABLE` takes an AccessExclusiveLock on the partitioned parent, so
14434
+ * callers must check {@link columnExistsSQL} first and only run the statement
14435
+ * when the column is genuinely missing. On Postgres 11+ the non-volatile
14436
+ * default does not rewrite the table, and the ALTER cascades to partitions.
14437
+ */
14438
+ function additiveColumns(schema) {
14439
+ return [{
14440
+ table: TABLE_SPAN_EVENTS,
14441
+ column: "isPending",
14442
+ ddl: `ALTER TABLE ${qualifiedTable(schema, TABLE_SPAN_EVENTS)} ADD COLUMN IF NOT EXISTS "isPending" boolean NOT NULL DEFAULT false`
14443
+ }];
14444
+ }
14445
+ /** Existence probe for an additive column. Params: schema, table, column. */
14446
+ const columnExistsSQL = `SELECT 1 FROM information_schema.columns
14447
+ WHERE table_schema = $1 AND table_name = $2 AND column_name = $3`;
14284
14448
  /** Index CREATEs. Safe to run repeatedly. */
14285
14449
  function allIndexDDL(schema) {
14286
14450
  return tableIndexes().map((spec) => indexDDL(schema, spec));
@@ -14288,6 +14452,7 @@ function allIndexDDL(schema) {
14288
14452
  //#endregion
14289
14453
  //#region src/storage/domains/observability/v-next/discovery.ts
14290
14454
  const DEFAULT_TTL_SECONDS = 300;
14455
+ const DEFAULT_LOOKBACK_SECONDS = 720 * 60 * 60;
14291
14456
  /** All signal tables that contain a column. Used by cross-signal discovery. */
14292
14457
  const SIGNAL_TABLES_WITH_CONTEXT = [
14293
14458
  TABLE_SPAN_EVENTS,
@@ -14352,22 +14517,71 @@ function startOrJoinRefresh(dedupeKey, cacheKey, refresh, upsert, logger) {
14352
14517
  async function readWithRefresh(client, schema, cacheKey, refresh, ttlSeconds, logger) {
14353
14518
  const table = qualifiedTable(schema, TABLE_DISCOVERY);
14354
14519
  const row = await client.oneOrNone(`SELECT "values", "refreshedAt" FROM ${table} WHERE "cacheKey" = $1`, [cacheKey]);
14355
- const refreshedAtMs = row ? new Date(row.refreshedAt).getTime() : 0;
14356
- if (!(!row || Date.now() - refreshedAtMs > ttlSeconds * 1e3)) return row.values;
14357
- const refreshing = startOrJoinRefresh(`${schema}:${cacheKey}`, cacheKey, refresh, (values) => upsertCache(client, schema, cacheKey, values), logger);
14520
+ const dedupeKey = `${schema}:${cacheKey}`;
14521
+ const startRefresh = () => startOrJoinRefresh(dedupeKey, cacheKey, refresh, (values) => upsertCache(client, schema, cacheKey, values), logger);
14358
14522
  if (ttlSeconds <= 0) try {
14359
- return await refreshing;
14523
+ return await startRefresh();
14360
14524
  } catch {
14361
14525
  return row?.values ?? [];
14362
14526
  }
14527
+ const refreshedAtMs = row ? new Date(row.refreshedAt).getTime() : 0;
14528
+ if (!(!row || Date.now() - refreshedAtMs > ttlSeconds * 1e3)) return row.values;
14363
14529
  if (!row) try {
14364
- return await refreshing;
14530
+ return await startRefresh();
14365
14531
  } catch {
14366
14532
  return [];
14367
14533
  }
14368
- refreshing.catch(() => {});
14534
+ const previousRefreshedAt = row.refreshedAt;
14535
+ claimRefresh(client, schema, cacheKey, ttlSeconds).then((claimedAt) => {
14536
+ if (!claimedAt) return;
14537
+ return startRefresh().then(() => {}, () => releaseClaim(client, schema, cacheKey, previousRefreshedAt, claimedAt));
14538
+ }).catch(() => {});
14369
14539
  return row.values;
14370
14540
  }
14541
+ /**
14542
+ * Try to become the process that refreshes `cacheKey`.
14543
+ *
14544
+ * The conditional UPDATE re-checks staleness in the database and stamps
14545
+ * `refreshedAt` in the same statement, so exactly one caller can transition a
14546
+ * given cache row out of the stale window — losers see zero rows updated and
14547
+ * skip their refresh. This is the cross-process counterpart to
14548
+ * `inFlightRefreshes`: N frontends going stale together run one refresh
14549
+ * instead of N.
14550
+ *
14551
+ * It reuses the cache table rather than an advisory lock because `DbClient`
14552
+ * is pool-backed, so a transaction-scoped lock would mean threading explicit
14553
+ * transaction handling through the discovery read path.
14554
+ *
14555
+ * The stamp is provisional in both directions: a completed refresh overwrites
14556
+ * it via `upsertCache`, and a failed one restores it via `releaseClaim`.
14557
+ *
14558
+ * Returns the exact provisional stamp written by the claim (or `null` when
14559
+ * the claim was lost) so `releaseClaim` can restore the row only while it
14560
+ * still holds this claim's stamp.
14561
+ */
14562
+ async function claimRefresh(client, schema, cacheKey, ttlSeconds) {
14563
+ const table = qualifiedTable(schema, TABLE_DISCOVERY);
14564
+ return (await client.oneOrNone(`UPDATE ${table} SET "refreshedAt" = NOW()
14565
+ WHERE "cacheKey" = $1 AND "refreshedAt" <= NOW() - ($2 || ' seconds')::interval
14566
+ RETURNING "refreshedAt"::text AS "refreshedAt"`, [cacheKey, Math.floor(ttlSeconds)]))?.refreshedAt ?? null;
14567
+ }
14568
+ /**
14569
+ * Hand back a claim whose refresh failed by restoring the timestamp the
14570
+ * claiming reader observed. Guarded on the row still holding this claim's
14571
+ * exact provisional stamp so a refresh that completed in the meantime
14572
+ * (another process, or a later claim) isn't dragged backwards into staleness.
14573
+ */
14574
+ async function releaseClaim(client, schema, cacheKey, previousRefreshedAt, claimedAt) {
14575
+ const table = qualifiedTable(schema, TABLE_DISCOVERY);
14576
+ try {
14577
+ await client.query(`UPDATE ${table} SET "refreshedAt" = $2
14578
+ WHERE "cacheKey" = $1 AND "refreshedAt" = $3::timestamptz`, [
14579
+ cacheKey,
14580
+ previousRefreshedAt,
14581
+ claimedAt
14582
+ ]);
14583
+ } catch {}
14584
+ }
14371
14585
  async function upsertCache(client, schema, cacheKey, values) {
14372
14586
  const table = qualifiedTable(schema, TABLE_DISCOVERY);
14373
14587
  await client.query(`INSERT INTO ${table} ("cacheKey", "refreshedAt", "values")
@@ -14376,61 +14590,95 @@ async function upsertCache(client, schema, cacheKey, values) {
14376
14590
  "refreshedAt" = EXCLUDED."refreshedAt",
14377
14591
  "values" = EXCLUDED."values"`, [cacheKey, JSON.stringify(values)]);
14378
14592
  }
14379
- async function distinctAcrossTables(client, schema, column, tables, filterSql = "", filterParams = []) {
14593
+ /**
14594
+ * Time predicate that bounds a discovery refresh to recent events.
14595
+ *
14596
+ * Each signal table is range-partitioned on its own time column
14597
+ * (`SIGNAL_TIME_COLUMN`: `endedAt` for spans, `timestamp` for everything
14598
+ * else), so the predicate has to be built per table rather than shared across
14599
+ * a UNION. Bounding on that exact column is what lets the planner prune
14600
+ * partitions/chunks instead of scanning all history.
14601
+ *
14602
+ * The interval is interpolated rather than bound to a parameter on purpose:
14603
+ * the UNION branches in `distinctAcrossTables` and `getTags` all reference the
14604
+ * same positional placeholders, so adding a per-branch parameter would break
14605
+ * `$N` numbering. Safety comes from coercing to a finite integer here — the
14606
+ * value originates from `DiscoveryConfig`, never from user input.
14607
+ */
14608
+ function lookbackPredicate(table, lookbackSeconds) {
14609
+ if (!Number.isFinite(lookbackSeconds) || lookbackSeconds <= 0) return "";
14610
+ const seconds = Math.floor(lookbackSeconds);
14611
+ const timeColumn = SIGNAL_TIME_COLUMN[table];
14612
+ if (!timeColumn) return "";
14613
+ return `AND "${timeColumn}" >= NOW() - INTERVAL '${seconds} seconds'`;
14614
+ }
14615
+ async function distinctAcrossTables(client, schema, column, tables, lookbackSeconds, filterSql = "", filterParams = []) {
14380
14616
  const safeColumn = (0, _mastra_core_utils.parseSqlIdentifier)(column, "column name");
14381
- const unions = tables.map((t) => `SELECT DISTINCT "${safeColumn}" AS v FROM ${qualifiedTable(schema, t)} WHERE "${safeColumn}" IS NOT NULL AND "${safeColumn}" <> '' ${filterSql}`).join(" UNION ");
14617
+ const unions = tables.map((t) => `SELECT DISTINCT "${safeColumn}" AS v FROM ${qualifiedTable(schema, t)} WHERE "${safeColumn}" IS NOT NULL AND "${safeColumn}" <> '' ${filterSql} ${lookbackPredicate(t, lookbackSeconds)}`).join(" UNION ");
14382
14618
  return (await client.manyOrNone(`SELECT v FROM (${unions}) sub ORDER BY v`, filterParams)).map((r) => r.v);
14383
14619
  }
14384
14620
  async function getEntityTypes(client, schema, _args, config) {
14385
- return { entityTypes: await readWithRefresh(client, schema, "entity_types", () => distinctAcrossTables(client, schema, "entityType", ENTITY_DISCOVERY_TABLES), config.ttlSeconds ?? DEFAULT_TTL_SECONDS, config.logger) };
14621
+ const ttl = config.ttlSeconds ?? DEFAULT_TTL_SECONDS;
14622
+ const lookback = config.lookbackSeconds ?? DEFAULT_LOOKBACK_SECONDS;
14623
+ return { entityTypes: await readWithRefresh(client, schema, "entity_types", () => distinctAcrossTables(client, schema, "entityType", ENTITY_DISCOVERY_TABLES, lookback), ttl, config.logger) };
14386
14624
  }
14387
14625
  async function getEntityNames(client, schema, args, config) {
14388
14626
  const ttl = config.ttlSeconds ?? DEFAULT_TTL_SECONDS;
14627
+ const lookback = config.lookbackSeconds ?? DEFAULT_LOOKBACK_SECONDS;
14389
14628
  const cacheKey = args.entityType ? `entity_names:${args.entityType}` : "entity_names";
14390
14629
  const filterSql = args.entityType ? `AND "entityType" = $1` : "";
14391
14630
  const filterParams = args.entityType ? [args.entityType] : [];
14392
- return { names: await readWithRefresh(client, schema, cacheKey, () => distinctAcrossTables(client, schema, "entityName", ENTITY_DISCOVERY_TABLES, filterSql, filterParams), ttl, config.logger) };
14631
+ return { names: await readWithRefresh(client, schema, cacheKey, () => distinctAcrossTables(client, schema, "entityName", ENTITY_DISCOVERY_TABLES, lookback, filterSql, filterParams), ttl, config.logger) };
14393
14632
  }
14394
14633
  async function getServiceNames(client, schema, _args, config) {
14395
- return { serviceNames: await readWithRefresh(client, schema, "service_names", () => distinctAcrossTables(client, schema, "serviceName", SIGNAL_TABLES_WITH_CONTEXT), config.ttlSeconds ?? DEFAULT_TTL_SECONDS, config.logger) };
14634
+ const ttl = config.ttlSeconds ?? DEFAULT_TTL_SECONDS;
14635
+ const lookback = config.lookbackSeconds ?? DEFAULT_LOOKBACK_SECONDS;
14636
+ return { serviceNames: await readWithRefresh(client, schema, "service_names", () => distinctAcrossTables(client, schema, "serviceName", SIGNAL_TABLES_WITH_CONTEXT, lookback), ttl, config.logger) };
14396
14637
  }
14397
14638
  async function getEnvironments(client, schema, _args, config) {
14398
- return { environments: await readWithRefresh(client, schema, "environments", () => distinctAcrossTables(client, schema, "environment", SIGNAL_TABLES_WITH_CONTEXT), config.ttlSeconds ?? DEFAULT_TTL_SECONDS, config.logger) };
14639
+ const ttl = config.ttlSeconds ?? DEFAULT_TTL_SECONDS;
14640
+ const lookback = config.lookbackSeconds ?? DEFAULT_LOOKBACK_SECONDS;
14641
+ return { environments: await readWithRefresh(client, schema, "environments", () => distinctAcrossTables(client, schema, "environment", SIGNAL_TABLES_WITH_CONTEXT, lookback), ttl, config.logger) };
14399
14642
  }
14400
14643
  async function getTags(client, schema, args, config) {
14401
14644
  const ttl = config.ttlSeconds ?? DEFAULT_TTL_SECONDS;
14645
+ const lookback = config.lookbackSeconds ?? DEFAULT_LOOKBACK_SECONDS;
14402
14646
  const cacheKey = args.entityType ? `tags:${args.entityType}` : "tags";
14403
14647
  const refresh = async () => {
14404
14648
  const filter = args.entityType ? `AND "entityType" = $1` : "";
14405
14649
  const params = args.entityType ? [args.entityType] : [];
14406
- const unions = ENTITY_DISCOVERY_TABLES.map((t) => `SELECT DISTINCT UNNEST("tags") AS v FROM ${qualifiedTable(schema, t)} WHERE array_length("tags", 1) > 0 ${filter}`).join(" UNION ");
14650
+ const unions = ENTITY_DISCOVERY_TABLES.map((t) => `SELECT DISTINCT UNNEST("tags") AS v FROM ${qualifiedTable(schema, t)} WHERE array_length("tags", 1) > 0 ${filter} ${lookbackPredicate(t, lookback)}`).join(" UNION ");
14407
14651
  return (await client.manyOrNone(`SELECT v FROM (${unions}) sub WHERE v IS NOT NULL AND v <> '' ORDER BY v`, params)).map((r) => r.v);
14408
14652
  };
14409
14653
  return { tags: await readWithRefresh(client, schema, cacheKey, refresh, ttl, config.logger) };
14410
14654
  }
14411
14655
  async function getMetricNames(client, schema, args, config) {
14656
+ const ttl = config.ttlSeconds ?? DEFAULT_TTL_SECONDS;
14657
+ const lookback = config.lookbackSeconds ?? DEFAULT_LOOKBACK_SECONDS;
14412
14658
  let filtered = await readWithRefresh(client, schema, "metric_names", async () => {
14413
14659
  return (await client.manyOrNone(`SELECT DISTINCT "name" AS v FROM ${qualifiedTable(schema, TABLE_METRIC_EVENTS)}
14414
- WHERE "name" IS NOT NULL AND "name" <> '' ORDER BY "name"`)).map((r) => r.v);
14415
- }, config.ttlSeconds ?? DEFAULT_TTL_SECONDS, config.logger);
14660
+ WHERE "name" IS NOT NULL AND "name" <> '' ${lookbackPredicate(TABLE_METRIC_EVENTS, lookback)} ORDER BY "name"`)).map((r) => r.v);
14661
+ }, ttl, config.logger);
14416
14662
  if (args.prefix) filtered = filtered.filter((v) => v.startsWith(args.prefix));
14417
14663
  if (args.limit) filtered = filtered.slice(0, args.limit);
14418
14664
  return { names: filtered };
14419
14665
  }
14420
14666
  async function getMetricLabelKeys(client, schema, args, config) {
14421
14667
  const ttl = config.ttlSeconds ?? DEFAULT_TTL_SECONDS;
14668
+ const lookback = config.lookbackSeconds ?? DEFAULT_LOOKBACK_SECONDS;
14422
14669
  return { keys: await readWithRefresh(client, schema, `metric_label_keys:${args.metricName}`, async () => {
14423
14670
  return (await client.manyOrNone(`SELECT DISTINCT k AS v
14424
14671
  FROM ${qualifiedTable(schema, TABLE_METRIC_EVENTS)}, jsonb_object_keys("labels") k
14425
- WHERE "name" = $1 ORDER BY k`, [args.metricName])).map((r) => r.v);
14672
+ WHERE "name" = $1 ${lookbackPredicate(TABLE_METRIC_EVENTS, lookback)} ORDER BY k`, [args.metricName])).map((r) => r.v);
14426
14673
  }, ttl, config.logger) };
14427
14674
  }
14428
14675
  async function getMetricLabelValues(client, schema, args, config) {
14429
14676
  const ttl = config.ttlSeconds ?? DEFAULT_TTL_SECONDS;
14677
+ const lookback = config.lookbackSeconds ?? DEFAULT_LOOKBACK_SECONDS;
14430
14678
  let filtered = await readWithRefresh(client, schema, `metric_label_values:${args.metricName}:${args.labelKey}`, async () => {
14431
14679
  return (await client.manyOrNone(`SELECT DISTINCT "labels" ->> $2 AS v
14432
14680
  FROM ${qualifiedTable(schema, TABLE_METRIC_EVENTS)}
14433
- WHERE "name" = $1 AND "labels" ? $2
14681
+ WHERE "name" = $1 AND "labels" ? $2 ${lookbackPredicate(TABLE_METRIC_EVENTS, lookback)}
14434
14682
  ORDER BY v`, [args.metricName, args.labelKey])).map((r) => r.v).filter((v) => v != null && v !== "");
14435
14683
  }, ttl, config.logger);
14436
14684
  if (args.prefix) filtered = filtered.filter((v) => v.startsWith(args.prefix));
@@ -14669,6 +14917,7 @@ function commonContextToRow(record) {
14669
14917
  };
14670
14918
  }
14671
14919
  function spanRecordToRow(span) {
14920
+ const isPending = !span.isEvent && span.endedAt == null;
14672
14921
  const endedAt = span.isEvent ? span.startedAt : span.endedAt ?? span.startedAt;
14673
14922
  const metadata = span.metadata ?? null;
14674
14923
  return {
@@ -14679,6 +14928,7 @@ function spanRecordToRow(span) {
14679
14928
  name: span.name,
14680
14929
  spanType: span.spanType,
14681
14930
  isEvent: Boolean(span.isEvent),
14931
+ isPending,
14682
14932
  startedAt: toIsoOrDate(span.startedAt),
14683
14933
  endedAt: toIsoOrDate(endedAt),
14684
14934
  tags: normalizeTags(span.tags),
@@ -14693,9 +14943,19 @@ function spanRecordToRow(span) {
14693
14943
  requestContext: jsonField(span.requestContext)
14694
14944
  };
14695
14945
  }
14946
+ /**
14947
+ * Recover a span's real `endedAt` from its row. A pending row carries a
14948
+ * synthesized `endedAt` (a copy of `startedAt`) because the column is part of
14949
+ * the primary key; callers must see null so the span reads as still running.
14950
+ */
14951
+ function spanRowEndedAt(row, startedAt) {
14952
+ if (row.isEvent) return startedAt;
14953
+ if (row.isPending) return null;
14954
+ return toDateOrNull(row.endedAt);
14955
+ }
14696
14956
  function rowToSpanRecord(row) {
14697
14957
  const startedAt = toDate(row.startedAt);
14698
- const endedAt = row.isEvent ? startedAt : toDateOrNull(row.endedAt);
14958
+ const endedAt = spanRowEndedAt(row, startedAt);
14699
14959
  const error = parsedJson(row.error);
14700
14960
  const { executionSource, ...ctx } = rowToCommonContext(row);
14701
14961
  return {
@@ -14729,7 +14989,7 @@ function rowToSpanRecord(row) {
14729
14989
  */
14730
14990
  function rowToLightSpanRecord(row) {
14731
14991
  const startedAt = toDate(row.startedAt);
14732
- const endedAt = row.isEvent ? startedAt : toDateOrNull(row.endedAt);
14992
+ const endedAt = spanRowEndedAt(row, startedAt);
14733
14993
  return {
14734
14994
  traceId: row.traceId,
14735
14995
  spanId: row.spanId,
@@ -15260,22 +15520,42 @@ const COMPLEX_GROUP_BY_EXCLUDED = /* @__PURE__ */ new Set([
15260
15520
  "requestContext"
15261
15521
  ]);
15262
15522
  //#endregion
15523
+ //#region src/storage/db/sanitize-json.ts
15524
+ /**
15525
+ * Sanitizes JSON string for PostgreSQL jsonb:
15526
+ * - Removes problematic Unicode sequences:
15527
+ * - \u0000 (null character) - causes error 22P05 "unsupported Unicode escape sequence"
15528
+ * - \uD800-\uDFFF (unpaired surrogates) - causes "Unicode low surrogate must follow a high surrogate"
15529
+ * - \\uD800 (escaped-backslash + surrogate, e.g. from JS regex literals like [^\ud800-\udfff]):
15530
+ * removing just \uXXXX would leave a dangling backslash that creates a new invalid escape (e.g. \-)
15531
+ * - Escapes any remaining invalid JSON escape sequences (e.g. \v, \k, \-)
15532
+ */
15533
+ function sanitizeJsonForPg(jsonString) {
15534
+ return jsonString.replace(/\\\\?u(0000|[Dd][89A-Fa-f][0-9A-Fa-f]{2})/g, "").replace(/(^|[^\\])(\\(?!["\\/bfnrtu]))/g, "$1\\\\");
15535
+ }
15536
+ //#endregion
15263
15537
  //#region src/storage/domains/observability/v-next/sql.ts
15264
15538
  /**
15265
15539
  * SQL helpers for the v-next Postgres observability domain.
15266
15540
  *
15267
- * Provides a multi-row INSERT builder with `ON CONFLICT DO NOTHING` for
15268
- * insert-only retry idempotency, and explicit jsonb / text[] casts so the
15269
- * pg driver doesn't have to guess column types.
15541
+ * Provides a multi-row INSERT builder with a configurable conflict clause
15542
+ * (`ON CONFLICT DO NOTHING` by default, for retry idempotency), and explicit
15543
+ * jsonb / text[] casts so the pg driver doesn't have to guess column types.
15270
15544
  */
15271
15545
  /**
15272
15546
  * Encode a JS value for a `$N::jsonb` cast. Always `JSON.stringify` so a
15273
15547
  * plain string like `"hello"` becomes `"hello"` (a valid JSON scalar) and
15274
15548
  * not the bare word `hello`, which Postgres rejects when cast to jsonb.
15549
+ *
15550
+ * The result is sanitized because Postgres rejects some sequences that are
15551
+ * legal in JSON: NUL (`\u0000`) fails with 22P05 and unpaired UTF-16
15552
+ * surrogates fail with 22P02. Inserts here are batched into a single
15553
+ * multi-row statement, so one bad value would otherwise reject the whole
15554
+ * batch of observability events.
15275
15555
  */
15276
15556
  function encodeJsonb(value) {
15277
15557
  if (value === null || value === void 0) return null;
15278
- return JSON.stringify(value);
15558
+ return sanitizeJsonForPg(JSON.stringify(value));
15279
15559
  }
15280
15560
  /**
15281
15561
  * Build a multi-row INSERT with explicit column types and ON CONFLICT DO NOTHING.
@@ -15286,7 +15566,7 @@ function encodeJsonb(value) {
15286
15566
  * All records must have identical key sets.
15287
15567
  * @returns { text, values } ready to pass to `client.query`.
15288
15568
  */
15289
- function buildInsert(schema, table, records) {
15569
+ function buildInsert(schema, table, records, onConflict = "ON CONFLICT DO NOTHING") {
15290
15570
  if (records.length === 0) return null;
15291
15571
  const columns = Object.keys(records[0]).map((c) => (0, _mastra_core_utils.parseSqlIdentifier)(c, "column name"));
15292
15572
  const quotedColumns = columns.map((c) => `"${c}"`).join(", ");
@@ -15312,10 +15592,44 @@ function buildInsert(schema, table, records) {
15312
15592
  return {
15313
15593
  text: `INSERT INTO ${qualifiedTable(schema, table)} (${quotedColumns})
15314
15594
  VALUES ${rowPlaceholders.join(", ")}
15315
- ON CONFLICT DO NOTHING`,
15595
+ ${onConflict}`,
15316
15596
  values
15317
15597
  };
15318
15598
  }
15599
+ /** Primary key of the span table; also the conflict target for span upserts. */
15600
+ const SPAN_KEY_COLUMNS = [
15601
+ "traceId",
15602
+ "spanId",
15603
+ "endedAt"
15604
+ ];
15605
+ /**
15606
+ * Conflict clause for span writes.
15607
+ *
15608
+ * A span is written twice under the event-sourced strategy: once when it starts
15609
+ * (`isPending = true`, `endedAt` synthesized from `startedAt`) and once when it
15610
+ * ends. Those two rows normally differ in `endedAt` and so coexist under the
15611
+ * primary key. A zero-duration span is the exception — its real `endedAt`
15612
+ * equals its `startedAt`, so the end row collides with the pending row. Letting
15613
+ * `DO NOTHING` win there would strand the span as permanently running, so the
15614
+ * end row overwrites the pending one instead.
15615
+ *
15616
+ * The `WHERE` guard keeps the clause a no-op for every other conflict, which
15617
+ * preserves insert-only retry idempotency: a replayed end row cannot clobber
15618
+ * the row it duplicates, and a late-arriving start row cannot revert an ended
15619
+ * span to pending.
15620
+ */
15621
+ function spanConflictClause(columns) {
15622
+ return `ON CONFLICT ("traceId", "spanId", "endedAt") DO UPDATE SET ${columns.filter((column) => !SPAN_KEY_COLUMNS.includes(column)).map((column) => `"${column}" = EXCLUDED."${column}"`).join(", ")}
15623
+ WHERE ${TABLE_SPAN_EVENTS}."isPending" AND NOT EXCLUDED."isPending"`;
15624
+ }
15625
+ /**
15626
+ * Ordering that picks the winning row for a span under the event-sourced write
15627
+ * model. An ended row always beats a pending row for the same span; among rows
15628
+ * of the same kind the latest write wins, with `cursorId` breaking ties between
15629
+ * rows that share a timestamp. Combine with `DISTINCT ON` (or `LIMIT 1`) after
15630
+ * the grouping key to collapse a span's rows down to one.
15631
+ */
15632
+ const SPAN_COLLAPSE_ORDER = "\"isPending\" ASC, \"endedAt\" DESC, \"cursorId\" DESC";
15319
15633
  /**
15320
15634
  * Standard SELECT column list for tracing tables. The select projects every
15321
15635
  * column the row→record converters expect.
@@ -16311,12 +16625,51 @@ async function getScorePercentiles(client, schema, args) {
16311
16625
  function asIsoTimestamp(value) {
16312
16626
  return value instanceof Date ? value.toISOString() : new Date(value).toISOString();
16313
16627
  }
16628
+ /**
16629
+ * Ordering that picks the row representing a trace's current root.
16630
+ *
16631
+ * A trace can hold more than one root row. A span is written once when it
16632
+ * starts and again when it ends, and a durable run that suspends ends its root
16633
+ * span and opens a fresh one on resume. In both cases the newest row is the one
16634
+ * that describes the trace right now, and `cursorId` — a sequence assigned at
16635
+ * insert — is what "newest" means here. It also orders correctly where
16636
+ * timestamps cannot: a zero-duration span has `startedAt = endedAt`, and a
16637
+ * resumed root starts after the suspended root ended.
16638
+ */
16639
+ const ROOT_COLLAPSE_ORDER = "\"cursorId\" DESC";
16640
+ /**
16641
+ * Predicate that keeps only the winning row for each span — the ended row when
16642
+ * one exists, the newest row otherwise. Mirrors `SPAN_COLLAPSE_ORDER` as an
16643
+ * anti-join, for the same reason as {@link latestRootPredicate}.
16644
+ */
16645
+ function latestSpanPredicate(spanTable) {
16646
+ return `NOT EXISTS (
16647
+ SELECT 1 FROM ${spanTable} newer
16648
+ WHERE newer."traceId" = r."traceId"
16649
+ AND newer."spanId" = r."spanId"
16650
+ AND (newer."isPending" < r."isPending" OR (newer."isPending" = r."isPending" AND newer."cursorId" > r."cursorId"))
16651
+ )`;
16652
+ }
16653
+ /**
16654
+ * Predicate that keeps only a trace's current root row, as an anti-join rather
16655
+ * than a `DISTINCT ON`. Written this way so the outer query still scans the
16656
+ * partial root indexes and streams rows in `LIMIT`-order; grouping first would
16657
+ * force every root in the table to be read before the first page came back.
16658
+ */
16659
+ function latestRootPredicate(spanTable) {
16660
+ return `NOT EXISTS (
16661
+ SELECT 1 FROM ${spanTable} newer
16662
+ WHERE newer."traceId" = r."traceId"
16663
+ AND newer."parentSpanId" IS NULL
16664
+ AND newer."cursorId" > r."cursorId"
16665
+ )`;
16666
+ }
16314
16667
  async function getRootSpan(client, schema, args) {
16315
16668
  const table = qualifiedTable(schema, TABLE_SPAN_EVENTS);
16316
16669
  const row = await client.oneOrNone(`SELECT ${SPAN_SELECT_COLUMNS}
16317
16670
  FROM ${table}
16318
16671
  WHERE "traceId" = $1 AND "parentSpanId" IS NULL
16319
- ORDER BY "endedAt" DESC
16672
+ ORDER BY ${ROOT_COLLAPSE_ORDER}
16320
16673
  LIMIT 1`, [args.traceId]);
16321
16674
  if (!row) return null;
16322
16675
  return { span: rowToSpanRecord(row) };
@@ -16328,7 +16681,7 @@ async function getRootSpan(client, schema, args) {
16328
16681
  * planner. Starts numbering from `nextParamIdx`.
16329
16682
  */
16330
16683
  function buildListTracesFilters(filters, spanTable, nextParamIdx) {
16331
- const conditions = [`r."parentSpanId" IS NULL`];
16684
+ const conditions = [`r."parentSpanId" IS NULL`, latestRootPredicate(spanTable)];
16332
16685
  const params = [];
16333
16686
  let i = nextParamIdx;
16334
16687
  if (!filters) return {
@@ -16345,11 +16698,11 @@ function buildListTracesFilters(filters, spanTable, nextParamIdx) {
16345
16698
  params.push(asIsoTimestamp(filters.startedAt.end));
16346
16699
  }
16347
16700
  if (filters.endedAt?.start) {
16348
- conditions.push(`r."endedAt" ${filters.endedAt.startExclusive ? ">" : ">="} $${i++}`);
16701
+ conditions.push(`(NOT r."isPending" AND r."endedAt" ${filters.endedAt.startExclusive ? ">" : ">="} $${i++})`);
16349
16702
  params.push(asIsoTimestamp(filters.endedAt.start));
16350
16703
  }
16351
16704
  if (filters.endedAt?.end) {
16352
- conditions.push(`r."endedAt" ${filters.endedAt.endExclusive ? "<" : "<="} $${i++}`);
16705
+ conditions.push(`(NOT r."isPending" AND r."endedAt" ${filters.endedAt.endExclusive ? "<" : "<="} $${i++})`);
16353
16706
  params.push(asIsoTimestamp(filters.endedAt.end));
16354
16707
  }
16355
16708
  if (filters.spanType !== void 0) {
@@ -16421,10 +16774,10 @@ function buildListTracesFilters(filters, spanTable, nextParamIdx) {
16421
16774
  conditions.push(`r."error" IS NOT NULL`);
16422
16775
  break;
16423
16776
  case _mastra_core_storage.TraceStatus.RUNNING:
16424
- conditions.push(`FALSE`);
16777
+ conditions.push(`r."isPending"`);
16425
16778
  break;
16426
16779
  case _mastra_core_storage.TraceStatus.SUCCESS:
16427
- conditions.push(`r."error" IS NULL`);
16780
+ conditions.push(`r."error" IS NULL AND NOT r."isPending"`);
16428
16781
  break;
16429
16782
  }
16430
16783
  if (filters.hasChildError !== void 0) {
@@ -16561,8 +16914,8 @@ function buildBranchSpanTypeClause(userSpanType, params, startIdx) {
16561
16914
  * NOT have a `parentSpanId IS NULL` predicate — branches can be nested under
16562
16915
  * other spans. Returns the bound params and the next param index.
16563
16916
  */
16564
- function buildListBranchesFilters(filters, spanType, nextParamIdx) {
16565
- const conditions = [];
16917
+ function buildListBranchesFilters(filters, spanType, spanTable, nextParamIdx) {
16918
+ const conditions = [latestSpanPredicate(spanTable)];
16566
16919
  const params = [];
16567
16920
  let i = nextParamIdx;
16568
16921
  const spanTypeClause = buildBranchSpanTypeClause(spanType, params, i);
@@ -16583,11 +16936,11 @@ function buildListBranchesFilters(filters, spanType, nextParamIdx) {
16583
16936
  params.push(asIsoTimestamp(filters.startedAt.end));
16584
16937
  }
16585
16938
  if (filters.endedAt?.start) {
16586
- conditions.push(`r."endedAt" ${filters.endedAt.startExclusive ? ">" : ">="} $${i++}`);
16939
+ conditions.push(`(NOT r."isPending" AND r."endedAt" ${filters.endedAt.startExclusive ? ">" : ">="} $${i++})`);
16587
16940
  params.push(asIsoTimestamp(filters.endedAt.start));
16588
16941
  }
16589
16942
  if (filters.endedAt?.end) {
16590
- conditions.push(`r."endedAt" ${filters.endedAt.endExclusive ? "<" : "<="} $${i++}`);
16943
+ conditions.push(`(NOT r."isPending" AND r."endedAt" ${filters.endedAt.endExclusive ? "<" : "<="} $${i++})`);
16591
16944
  params.push(asIsoTimestamp(filters.endedAt.end));
16592
16945
  }
16593
16946
  if (filters.traceId !== void 0) {
@@ -16699,10 +17052,10 @@ function buildListBranchesFilters(filters, spanType, nextParamIdx) {
16699
17052
  conditions.push(`r."error" IS NOT NULL`);
16700
17053
  break;
16701
17054
  case _mastra_core_storage.TraceStatus.RUNNING:
16702
- conditions.push(`FALSE`);
17055
+ conditions.push(`r."isPending"`);
16703
17056
  break;
16704
17057
  case _mastra_core_storage.TraceStatus.SUCCESS:
16705
- conditions.push(`r."error" IS NULL`);
17058
+ conditions.push(`r."error" IS NULL AND NOT r."isPending"`);
16706
17059
  break;
16707
17060
  }
16708
17061
  return {
@@ -16721,7 +17074,7 @@ async function listBranches(client, schema, args) {
16721
17074
  return listBranchesPage(client, span, filters, pagination.page, pagination.perPage, orderBy.field, orderBy.direction);
16722
17075
  }
16723
17076
  async function listBranchesPage(client, span, filters, page, perPage, orderField, orderDir) {
16724
- const built = buildListBranchesFilters(filters, filters?.spanType, 1);
17077
+ const built = buildListBranchesFilters(filters, filters?.spanType, span, 1);
16725
17078
  if (!built) {
16726
17079
  const deltaCursor = deltaPollingFeatureEnabled() ? await readBranchesStreamHeadCursor(client, span, filters) : void 0;
16727
17080
  return {
@@ -16775,7 +17128,7 @@ async function listBranchesDelta(client, span, filters, after, limit) {
16775
17128
  deltaCursor
16776
17129
  };
16777
17130
  }
16778
- const built = buildListBranchesFilters(filters, filters?.spanType, 1);
17131
+ const built = buildListBranchesFilters(filters, filters?.spanType, span, 1);
16779
17132
  if (!built) return {
16780
17133
  branches: [],
16781
17134
  delta: {
@@ -16818,14 +17171,35 @@ async function readBranchesStreamHeadCursor(client, span, filters) {
16818
17171
  }
16819
17172
  //#endregion
16820
17173
  //#region src/storage/domains/observability/v-next/tracing.ts
16821
- async function createSpan(client, schema, args) {
16822
- const insert = buildInsert(schema, TABLE_SPAN_EVENTS, [spanRecordToRow(args.span)]);
17174
+ async function writeSpanRows(client, schema, rows) {
17175
+ const deduped = dedupeSpanRows(rows);
17176
+ const insert = buildInsert(schema, TABLE_SPAN_EVENTS, deduped, spanConflictClause(Object.keys(deduped[0] ?? {})));
16823
17177
  if (insert) await client.query(insert.text, insert.values);
16824
17178
  }
17179
+ /**
17180
+ * Collapse rows that share a primary key within a single batch. Postgres
17181
+ * rejects an `ON CONFLICT DO UPDATE` statement whose VALUES list would touch
17182
+ * the same row twice, which a batch containing both the start and the end of a
17183
+ * zero-duration span would do. The ended row wins, matching what the conflict
17184
+ * clause would have done across two separate statements.
17185
+ */
17186
+ function dedupeSpanRows(rows) {
17187
+ if (rows.length < 2) return rows;
17188
+ const byKey = /* @__PURE__ */ new Map();
17189
+ for (const row of rows) {
17190
+ const key = `${row.traceId}\u0000${row.spanId}\u0000${String(row.endedAt)}`;
17191
+ const existing = byKey.get(key);
17192
+ if (existing && !existing.isPending) continue;
17193
+ byKey.set(key, row);
17194
+ }
17195
+ return [...byKey.values()];
17196
+ }
17197
+ async function createSpan(client, schema, args) {
17198
+ await writeSpanRows(client, schema, [spanRecordToRow(args.span)]);
17199
+ }
16825
17200
  async function batchCreateSpans(client, schema, args) {
16826
17201
  if (args.records.length === 0) return;
16827
- const insert = buildInsert(schema, TABLE_SPAN_EVENTS, args.records.map(spanRecordToRow));
16828
- if (insert) await client.query(insert.text, insert.values);
17202
+ await writeSpanRows(client, schema, args.records.map(spanRecordToRow));
16829
17203
  }
16830
17204
  async function getSpans(client, schema, args) {
16831
17205
  if (args.spanIds.length === 0) return {
@@ -16833,10 +17207,13 @@ async function getSpans(client, schema, args) {
16833
17207
  spans: []
16834
17208
  };
16835
17209
  const table = qualifiedTable(schema, TABLE_SPAN_EVENTS);
16836
- const rows = await client.manyOrNone(`SELECT ${SPAN_SELECT_COLUMNS}
16837
- FROM ${table}
16838
- WHERE "traceId" = $1
16839
- AND "spanId" = ANY($2::text[])
17210
+ const rows = await client.manyOrNone(`SELECT * FROM (
17211
+ SELECT DISTINCT ON ("spanId") ${SPAN_SELECT_COLUMNS}
17212
+ FROM ${table}
17213
+ WHERE "traceId" = $1
17214
+ AND "spanId" = ANY($2::text[])
17215
+ ORDER BY "spanId", ${SPAN_COLLAPSE_ORDER}
17216
+ ) collapsed
16840
17217
  ORDER BY "startedAt" ASC`, [args.traceId, args.spanIds]);
16841
17218
  return {
16842
17219
  traceId: args.traceId,
@@ -16848,16 +17225,19 @@ async function getSpan(client, schema, args) {
16848
17225
  const row = await client.oneOrNone(`SELECT ${SPAN_SELECT_COLUMNS}
16849
17226
  FROM ${table}
16850
17227
  WHERE "traceId" = $1 AND "spanId" = $2
16851
- ORDER BY "endedAt" DESC
17228
+ ORDER BY ${SPAN_COLLAPSE_ORDER}
16852
17229
  LIMIT 1`, [args.traceId, args.spanId]);
16853
17230
  if (!row) return null;
16854
17231
  return { span: rowToSpanRecord(row) };
16855
17232
  }
16856
17233
  async function getTrace(client, schema, args) {
16857
17234
  const table = qualifiedTable(schema, TABLE_SPAN_EVENTS);
16858
- const rows = await client.manyOrNone(`SELECT ${SPAN_SELECT_COLUMNS}
16859
- FROM ${table}
16860
- WHERE "traceId" = $1
17235
+ const rows = await client.manyOrNone(`SELECT * FROM (
17236
+ SELECT DISTINCT ON ("spanId") ${SPAN_SELECT_COLUMNS}
17237
+ FROM ${table}
17238
+ WHERE "traceId" = $1
17239
+ ORDER BY "spanId", ${SPAN_COLLAPSE_ORDER}
17240
+ ) collapsed
16861
17241
  ORDER BY "startedAt" ASC`, [args.traceId]);
16862
17242
  if (!rows.length) return null;
16863
17243
  return {
@@ -16867,9 +17247,12 @@ async function getTrace(client, schema, args) {
16867
17247
  }
16868
17248
  async function getTraceLight(client, schema, args) {
16869
17249
  const table = qualifiedTable(schema, TABLE_SPAN_EVENTS);
16870
- const rows = await client.manyOrNone(`SELECT ${SPAN_LIGHT_SELECT_COLUMNS}
16871
- FROM ${table}
16872
- WHERE "traceId" = $1
17250
+ const rows = await client.manyOrNone(`SELECT * FROM (
17251
+ SELECT DISTINCT ON ("spanId") ${SPAN_LIGHT_SELECT_COLUMNS}
17252
+ FROM ${table}
17253
+ WHERE "traceId" = $1
17254
+ ORDER BY "spanId", ${SPAN_COLLAPSE_ORDER}
17255
+ ) collapsed
16873
17256
  ORDER BY "startedAt" ASC`, [args.traceId]);
16874
17257
  if (!rows.length) return null;
16875
17258
  return {
@@ -16980,6 +17363,11 @@ var ObservabilityStoragePostgresVNext = class ObservabilityStoragePostgresVNext
16980
17363
  } catch (error) {
16981
17364
  if (!isDuplicateRelationError(error)) throw error;
16982
17365
  }
17366
+ for (const { table, column, ddl } of additiveColumns(this.#schema)) if (!await this.#client.oneOrNone(columnExistsSQL, [
17367
+ this.#schema,
17368
+ table,
17369
+ column
17370
+ ])) await this.#client.none(ddl);
16983
17371
  for (const ddl of allIndexDDL(this.#schema)) try {
16984
17372
  await this.#client.none(ddl);
16985
17373
  } catch (error) {
@@ -17076,8 +17464,8 @@ var ObservabilityStoragePostgresVNext = class ObservabilityStoragePostgresVNext
17076
17464
  }
17077
17465
  get observabilityStrategy() {
17078
17466
  return {
17079
- preferred: "insert-only",
17080
- supported: ["insert-only"]
17467
+ preferred: "event-sourced",
17468
+ supported: ["event-sourced"]
17081
17469
  };
17082
17470
  }
17083
17471
  getFeatures() {
@@ -20501,18 +20889,6 @@ function workflowSnapshotStatusIndexSQL(indexName, schemaName) {
20501
20889
  schemaName: getSchemaName(schemaName)
20502
20890
  })} (workflow_name, (snapshot ->> 'status'), "createdAt" DESC)`;
20503
20891
  }
20504
- /**
20505
- * Sanitizes JSON string for PostgreSQL jsonb:
20506
- * - Removes problematic Unicode sequences:
20507
- * - \u0000 (null character) - causes error 22P05 "unsupported Unicode escape sequence"
20508
- * - \uD800-\uDFFF (unpaired surrogates) - causes "Unicode low surrogate must follow a high surrogate"
20509
- * - \\uD800 (escaped-backslash + surrogate, e.g. from JS regex literals like [^\ud800-\udfff]):
20510
- * removing just \uXXXX would leave a dangling backslash that creates a new invalid escape (e.g. \-)
20511
- * - Escapes any remaining invalid JSON escape sequences (e.g. \v, \k, \-)
20512
- */
20513
- function sanitizeJsonForPg(jsonString) {
20514
- return jsonString.replace(/\\\\?u(0000|[Dd][89A-Fa-f][0-9A-Fa-f]{2})/g, "").replace(/(^|[^\\])(\\(?!["\\/bfnrtu]))/g, "$1\\\\");
20515
- }
20516
20892
  var WorkflowsPG = class WorkflowsPG extends _mastra_core_storage.WorkflowsStorage {
20517
20893
  #db;
20518
20894
  #schema;
@@ -20792,11 +21168,11 @@ var WorkflowsPG = class WorkflowsPG extends _mastra_core_storage.WorkflowsStorag
20792
21168
  await this.#db.client.none(`INSERT INTO ${getTableName({
20793
21169
  indexName: _mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT,
20794
21170
  schemaName: getSchemaName(this.#schema)
20795
- })}
21171
+ })} AS t
20796
21172
  (workflow_name, run_id, "resourceId", snapshot, "createdAt", "updatedAt", "createdAtZ", "updatedAtZ")
20797
21173
  VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
20798
21174
  ON CONFLICT (workflow_name, run_id) DO UPDATE
20799
- SET "resourceId" = $3, snapshot = $4, "updatedAt" = $6, "updatedAtZ" = $8`, [
21175
+ SET "resourceId" = COALESCE($3, t."resourceId"), snapshot = $4, "updatedAt" = $6, "updatedAtZ" = $8`, [
20800
21176
  workflowName,
20801
21177
  runId,
20802
21178
  resourceId,
@@ -21650,7 +22026,7 @@ var PgFactoryStorageOps = class {
21650
22026
  row[name] = Boolean(value);
21651
22027
  break;
21652
22028
  case "json":
21653
- row[name] = typeof value === "string" ? JSON.parse(value) : value;
22029
+ row[name] = value;
21654
22030
  break;
21655
22031
  case "bigint":
21656
22032
  case "integer":
@@ -21754,6 +22130,12 @@ var PgFactoryStorageOps = class {
21754
22130
  async findMany(collection, where, opts) {
21755
22131
  return this.#select(this.#queryable, collection, where, opts);
21756
22132
  }
22133
+ async count(collection, where) {
22134
+ const schema = this.#schema(collection);
22135
+ const filter = this.#buildWhere(schema, where);
22136
+ const result = await this.#queryable.query(`SELECT COUNT(*) AS count FROM "${schema.name}" WHERE ${filter.sql}`, filter.args);
22137
+ return Number(result.rows[0]?.count ?? 0);
22138
+ }
21757
22139
  async insertOne(collection, row) {
21758
22140
  const schema = this.#schema(collection);
21759
22141
  const pk = primaryKeyOf(schema);