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

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 (31) hide show
  1. package/CHANGELOG.md +89 -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 +1 -0
  6. package/dist/docs/references/integrations-databases-postgresql.md +2 -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 +394 -107
  10. package/dist/index.cjs.map +1 -1
  11. package/dist/index.js +394 -107
  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/observability/v-next/ddl.d.ts +17 -0
  16. package/dist/storage/domains/observability/v-next/ddl.d.ts.map +1 -1
  17. package/dist/storage/domains/observability/v-next/helpers.d.ts.map +1 -1
  18. package/dist/storage/domains/observability/v-next/index.d.ts.map +1 -1
  19. package/dist/storage/domains/observability/v-next/signal-schema.d.ts +5 -1
  20. package/dist/storage/domains/observability/v-next/signal-schema.d.ts.map +1 -1
  21. package/dist/storage/domains/observability/v-next/sql.d.ts +29 -4
  22. package/dist/storage/domains/observability/v-next/sql.d.ts.map +1 -1
  23. package/dist/storage/domains/observability/v-next/traces.d.ts.map +1 -1
  24. package/dist/storage/domains/observability/v-next/tracing.d.ts +4 -3
  25. package/dist/storage/domains/observability/v-next/tracing.d.ts.map +1 -1
  26. package/dist/storage/domains/workflows/index.d.ts +2 -10
  27. package/dist/storage/domains/workflows/index.d.ts.map +1 -1
  28. package/dist/storage/factory-storage.d.ts.map +1 -1
  29. package/dist/vector/index.d.ts +60 -5
  30. package/dist/vector/index.d.ts.map +1 -1
  31. package/package.json +2 -2
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}`, {
@@ -13591,6 +13718,11 @@ const SPAN_EVENT_COLUMNS = [
13591
13718
  type: "boolean",
13592
13719
  defaultSql: "false"
13593
13720
  },
13721
+ {
13722
+ name: "isPending",
13723
+ type: "boolean",
13724
+ defaultSql: "false"
13725
+ },
13594
13726
  {
13595
13727
  name: "startedAt",
13596
13728
  type: "timestamptz"
@@ -13887,6 +14019,7 @@ const SPAN_LIGHT_SELECT_COLUMN_NAMES = [
13887
14019
  "spanType",
13888
14020
  "error",
13889
14021
  "isEvent",
14022
+ "isPending",
13890
14023
  "startedAt",
13891
14024
  "endedAt"
13892
14025
  ];
@@ -14281,6 +14414,26 @@ function allTableDDL(schema, mode) {
14281
14414
  discoveryTableDDL(schema)
14282
14415
  ];
14283
14416
  }
14417
+ /**
14418
+ * Additive column migrations for tables created by an older version of this
14419
+ * schema. `CREATE TABLE IF NOT EXISTS` is a no-op on an existing table, so new
14420
+ * columns have to be added out of band.
14421
+ *
14422
+ * `ALTER TABLE` takes an AccessExclusiveLock on the partitioned parent, so
14423
+ * callers must check {@link columnExistsSQL} first and only run the statement
14424
+ * when the column is genuinely missing. On Postgres 11+ the non-volatile
14425
+ * default does not rewrite the table, and the ALTER cascades to partitions.
14426
+ */
14427
+ function additiveColumns(schema) {
14428
+ return [{
14429
+ table: TABLE_SPAN_EVENTS,
14430
+ column: "isPending",
14431
+ ddl: `ALTER TABLE ${qualifiedTable(schema, TABLE_SPAN_EVENTS)} ADD COLUMN IF NOT EXISTS "isPending" boolean NOT NULL DEFAULT false`
14432
+ }];
14433
+ }
14434
+ /** Existence probe for an additive column. Params: schema, table, column. */
14435
+ const columnExistsSQL = `SELECT 1 FROM information_schema.columns
14436
+ WHERE table_schema = $1 AND table_name = $2 AND column_name = $3`;
14284
14437
  /** Index CREATEs. Safe to run repeatedly. */
14285
14438
  function allIndexDDL(schema) {
14286
14439
  return tableIndexes().map((spec) => indexDDL(schema, spec));
@@ -14669,6 +14822,7 @@ function commonContextToRow(record) {
14669
14822
  };
14670
14823
  }
14671
14824
  function spanRecordToRow(span) {
14825
+ const isPending = !span.isEvent && span.endedAt == null;
14672
14826
  const endedAt = span.isEvent ? span.startedAt : span.endedAt ?? span.startedAt;
14673
14827
  const metadata = span.metadata ?? null;
14674
14828
  return {
@@ -14679,6 +14833,7 @@ function spanRecordToRow(span) {
14679
14833
  name: span.name,
14680
14834
  spanType: span.spanType,
14681
14835
  isEvent: Boolean(span.isEvent),
14836
+ isPending,
14682
14837
  startedAt: toIsoOrDate(span.startedAt),
14683
14838
  endedAt: toIsoOrDate(endedAt),
14684
14839
  tags: normalizeTags(span.tags),
@@ -14693,9 +14848,19 @@ function spanRecordToRow(span) {
14693
14848
  requestContext: jsonField(span.requestContext)
14694
14849
  };
14695
14850
  }
14851
+ /**
14852
+ * Recover a span's real `endedAt` from its row. A pending row carries a
14853
+ * synthesized `endedAt` (a copy of `startedAt`) because the column is part of
14854
+ * the primary key; callers must see null so the span reads as still running.
14855
+ */
14856
+ function spanRowEndedAt(row, startedAt) {
14857
+ if (row.isEvent) return startedAt;
14858
+ if (row.isPending) return null;
14859
+ return toDateOrNull(row.endedAt);
14860
+ }
14696
14861
  function rowToSpanRecord(row) {
14697
14862
  const startedAt = toDate(row.startedAt);
14698
- const endedAt = row.isEvent ? startedAt : toDateOrNull(row.endedAt);
14863
+ const endedAt = spanRowEndedAt(row, startedAt);
14699
14864
  const error = parsedJson(row.error);
14700
14865
  const { executionSource, ...ctx } = rowToCommonContext(row);
14701
14866
  return {
@@ -14729,7 +14894,7 @@ function rowToSpanRecord(row) {
14729
14894
  */
14730
14895
  function rowToLightSpanRecord(row) {
14731
14896
  const startedAt = toDate(row.startedAt);
14732
- const endedAt = row.isEvent ? startedAt : toDateOrNull(row.endedAt);
14897
+ const endedAt = spanRowEndedAt(row, startedAt);
14733
14898
  return {
14734
14899
  traceId: row.traceId,
14735
14900
  spanId: row.spanId,
@@ -15260,22 +15425,42 @@ const COMPLEX_GROUP_BY_EXCLUDED = /* @__PURE__ */ new Set([
15260
15425
  "requestContext"
15261
15426
  ]);
15262
15427
  //#endregion
15428
+ //#region src/storage/db/sanitize-json.ts
15429
+ /**
15430
+ * Sanitizes JSON string for PostgreSQL jsonb:
15431
+ * - Removes problematic Unicode sequences:
15432
+ * - \u0000 (null character) - causes error 22P05 "unsupported Unicode escape sequence"
15433
+ * - \uD800-\uDFFF (unpaired surrogates) - causes "Unicode low surrogate must follow a high surrogate"
15434
+ * - \\uD800 (escaped-backslash + surrogate, e.g. from JS regex literals like [^\ud800-\udfff]):
15435
+ * removing just \uXXXX would leave a dangling backslash that creates a new invalid escape (e.g. \-)
15436
+ * - Escapes any remaining invalid JSON escape sequences (e.g. \v, \k, \-)
15437
+ */
15438
+ function sanitizeJsonForPg(jsonString) {
15439
+ return jsonString.replace(/\\\\?u(0000|[Dd][89A-Fa-f][0-9A-Fa-f]{2})/g, "").replace(/(^|[^\\])(\\(?!["\\/bfnrtu]))/g, "$1\\\\");
15440
+ }
15441
+ //#endregion
15263
15442
  //#region src/storage/domains/observability/v-next/sql.ts
15264
15443
  /**
15265
15444
  * SQL helpers for the v-next Postgres observability domain.
15266
15445
  *
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.
15446
+ * Provides a multi-row INSERT builder with a configurable conflict clause
15447
+ * (`ON CONFLICT DO NOTHING` by default, for retry idempotency), and explicit
15448
+ * jsonb / text[] casts so the pg driver doesn't have to guess column types.
15270
15449
  */
15271
15450
  /**
15272
15451
  * Encode a JS value for a `$N::jsonb` cast. Always `JSON.stringify` so a
15273
15452
  * plain string like `"hello"` becomes `"hello"` (a valid JSON scalar) and
15274
15453
  * not the bare word `hello`, which Postgres rejects when cast to jsonb.
15454
+ *
15455
+ * The result is sanitized because Postgres rejects some sequences that are
15456
+ * legal in JSON: NUL (`\u0000`) fails with 22P05 and unpaired UTF-16
15457
+ * surrogates fail with 22P02. Inserts here are batched into a single
15458
+ * multi-row statement, so one bad value would otherwise reject the whole
15459
+ * batch of observability events.
15275
15460
  */
15276
15461
  function encodeJsonb(value) {
15277
15462
  if (value === null || value === void 0) return null;
15278
- return JSON.stringify(value);
15463
+ return sanitizeJsonForPg(JSON.stringify(value));
15279
15464
  }
15280
15465
  /**
15281
15466
  * Build a multi-row INSERT with explicit column types and ON CONFLICT DO NOTHING.
@@ -15286,7 +15471,7 @@ function encodeJsonb(value) {
15286
15471
  * All records must have identical key sets.
15287
15472
  * @returns { text, values } ready to pass to `client.query`.
15288
15473
  */
15289
- function buildInsert(schema, table, records) {
15474
+ function buildInsert(schema, table, records, onConflict = "ON CONFLICT DO NOTHING") {
15290
15475
  if (records.length === 0) return null;
15291
15476
  const columns = Object.keys(records[0]).map((c) => (0, _mastra_core_utils.parseSqlIdentifier)(c, "column name"));
15292
15477
  const quotedColumns = columns.map((c) => `"${c}"`).join(", ");
@@ -15312,10 +15497,44 @@ function buildInsert(schema, table, records) {
15312
15497
  return {
15313
15498
  text: `INSERT INTO ${qualifiedTable(schema, table)} (${quotedColumns})
15314
15499
  VALUES ${rowPlaceholders.join(", ")}
15315
- ON CONFLICT DO NOTHING`,
15500
+ ${onConflict}`,
15316
15501
  values
15317
15502
  };
15318
15503
  }
15504
+ /** Primary key of the span table; also the conflict target for span upserts. */
15505
+ const SPAN_KEY_COLUMNS = [
15506
+ "traceId",
15507
+ "spanId",
15508
+ "endedAt"
15509
+ ];
15510
+ /**
15511
+ * Conflict clause for span writes.
15512
+ *
15513
+ * A span is written twice under the event-sourced strategy: once when it starts
15514
+ * (`isPending = true`, `endedAt` synthesized from `startedAt`) and once when it
15515
+ * ends. Those two rows normally differ in `endedAt` and so coexist under the
15516
+ * primary key. A zero-duration span is the exception — its real `endedAt`
15517
+ * equals its `startedAt`, so the end row collides with the pending row. Letting
15518
+ * `DO NOTHING` win there would strand the span as permanently running, so the
15519
+ * end row overwrites the pending one instead.
15520
+ *
15521
+ * The `WHERE` guard keeps the clause a no-op for every other conflict, which
15522
+ * preserves insert-only retry idempotency: a replayed end row cannot clobber
15523
+ * the row it duplicates, and a late-arriving start row cannot revert an ended
15524
+ * span to pending.
15525
+ */
15526
+ function spanConflictClause(columns) {
15527
+ return `ON CONFLICT ("traceId", "spanId", "endedAt") DO UPDATE SET ${columns.filter((column) => !SPAN_KEY_COLUMNS.includes(column)).map((column) => `"${column}" = EXCLUDED."${column}"`).join(", ")}
15528
+ WHERE ${TABLE_SPAN_EVENTS}."isPending" AND NOT EXCLUDED."isPending"`;
15529
+ }
15530
+ /**
15531
+ * Ordering that picks the winning row for a span under the event-sourced write
15532
+ * model. An ended row always beats a pending row for the same span; among rows
15533
+ * of the same kind the latest write wins, with `cursorId` breaking ties between
15534
+ * rows that share a timestamp. Combine with `DISTINCT ON` (or `LIMIT 1`) after
15535
+ * the grouping key to collapse a span's rows down to one.
15536
+ */
15537
+ const SPAN_COLLAPSE_ORDER = "\"isPending\" ASC, \"endedAt\" DESC, \"cursorId\" DESC";
15319
15538
  /**
15320
15539
  * Standard SELECT column list for tracing tables. The select projects every
15321
15540
  * column the row→record converters expect.
@@ -16311,12 +16530,51 @@ async function getScorePercentiles(client, schema, args) {
16311
16530
  function asIsoTimestamp(value) {
16312
16531
  return value instanceof Date ? value.toISOString() : new Date(value).toISOString();
16313
16532
  }
16533
+ /**
16534
+ * Ordering that picks the row representing a trace's current root.
16535
+ *
16536
+ * A trace can hold more than one root row. A span is written once when it
16537
+ * starts and again when it ends, and a durable run that suspends ends its root
16538
+ * span and opens a fresh one on resume. In both cases the newest row is the one
16539
+ * that describes the trace right now, and `cursorId` — a sequence assigned at
16540
+ * insert — is what "newest" means here. It also orders correctly where
16541
+ * timestamps cannot: a zero-duration span has `startedAt = endedAt`, and a
16542
+ * resumed root starts after the suspended root ended.
16543
+ */
16544
+ const ROOT_COLLAPSE_ORDER = "\"cursorId\" DESC";
16545
+ /**
16546
+ * Predicate that keeps only the winning row for each span — the ended row when
16547
+ * one exists, the newest row otherwise. Mirrors `SPAN_COLLAPSE_ORDER` as an
16548
+ * anti-join, for the same reason as {@link latestRootPredicate}.
16549
+ */
16550
+ function latestSpanPredicate(spanTable) {
16551
+ return `NOT EXISTS (
16552
+ SELECT 1 FROM ${spanTable} newer
16553
+ WHERE newer."traceId" = r."traceId"
16554
+ AND newer."spanId" = r."spanId"
16555
+ AND (newer."isPending" < r."isPending" OR (newer."isPending" = r."isPending" AND newer."cursorId" > r."cursorId"))
16556
+ )`;
16557
+ }
16558
+ /**
16559
+ * Predicate that keeps only a trace's current root row, as an anti-join rather
16560
+ * than a `DISTINCT ON`. Written this way so the outer query still scans the
16561
+ * partial root indexes and streams rows in `LIMIT`-order; grouping first would
16562
+ * force every root in the table to be read before the first page came back.
16563
+ */
16564
+ function latestRootPredicate(spanTable) {
16565
+ return `NOT EXISTS (
16566
+ SELECT 1 FROM ${spanTable} newer
16567
+ WHERE newer."traceId" = r."traceId"
16568
+ AND newer."parentSpanId" IS NULL
16569
+ AND newer."cursorId" > r."cursorId"
16570
+ )`;
16571
+ }
16314
16572
  async function getRootSpan(client, schema, args) {
16315
16573
  const table = qualifiedTable(schema, TABLE_SPAN_EVENTS);
16316
16574
  const row = await client.oneOrNone(`SELECT ${SPAN_SELECT_COLUMNS}
16317
16575
  FROM ${table}
16318
16576
  WHERE "traceId" = $1 AND "parentSpanId" IS NULL
16319
- ORDER BY "endedAt" DESC
16577
+ ORDER BY ${ROOT_COLLAPSE_ORDER}
16320
16578
  LIMIT 1`, [args.traceId]);
16321
16579
  if (!row) return null;
16322
16580
  return { span: rowToSpanRecord(row) };
@@ -16328,7 +16586,7 @@ async function getRootSpan(client, schema, args) {
16328
16586
  * planner. Starts numbering from `nextParamIdx`.
16329
16587
  */
16330
16588
  function buildListTracesFilters(filters, spanTable, nextParamIdx) {
16331
- const conditions = [`r."parentSpanId" IS NULL`];
16589
+ const conditions = [`r."parentSpanId" IS NULL`, latestRootPredicate(spanTable)];
16332
16590
  const params = [];
16333
16591
  let i = nextParamIdx;
16334
16592
  if (!filters) return {
@@ -16345,11 +16603,11 @@ function buildListTracesFilters(filters, spanTable, nextParamIdx) {
16345
16603
  params.push(asIsoTimestamp(filters.startedAt.end));
16346
16604
  }
16347
16605
  if (filters.endedAt?.start) {
16348
- conditions.push(`r."endedAt" ${filters.endedAt.startExclusive ? ">" : ">="} $${i++}`);
16606
+ conditions.push(`(NOT r."isPending" AND r."endedAt" ${filters.endedAt.startExclusive ? ">" : ">="} $${i++})`);
16349
16607
  params.push(asIsoTimestamp(filters.endedAt.start));
16350
16608
  }
16351
16609
  if (filters.endedAt?.end) {
16352
- conditions.push(`r."endedAt" ${filters.endedAt.endExclusive ? "<" : "<="} $${i++}`);
16610
+ conditions.push(`(NOT r."isPending" AND r."endedAt" ${filters.endedAt.endExclusive ? "<" : "<="} $${i++})`);
16353
16611
  params.push(asIsoTimestamp(filters.endedAt.end));
16354
16612
  }
16355
16613
  if (filters.spanType !== void 0) {
@@ -16421,10 +16679,10 @@ function buildListTracesFilters(filters, spanTable, nextParamIdx) {
16421
16679
  conditions.push(`r."error" IS NOT NULL`);
16422
16680
  break;
16423
16681
  case _mastra_core_storage.TraceStatus.RUNNING:
16424
- conditions.push(`FALSE`);
16682
+ conditions.push(`r."isPending"`);
16425
16683
  break;
16426
16684
  case _mastra_core_storage.TraceStatus.SUCCESS:
16427
- conditions.push(`r."error" IS NULL`);
16685
+ conditions.push(`r."error" IS NULL AND NOT r."isPending"`);
16428
16686
  break;
16429
16687
  }
16430
16688
  if (filters.hasChildError !== void 0) {
@@ -16561,8 +16819,8 @@ function buildBranchSpanTypeClause(userSpanType, params, startIdx) {
16561
16819
  * NOT have a `parentSpanId IS NULL` predicate — branches can be nested under
16562
16820
  * other spans. Returns the bound params and the next param index.
16563
16821
  */
16564
- function buildListBranchesFilters(filters, spanType, nextParamIdx) {
16565
- const conditions = [];
16822
+ function buildListBranchesFilters(filters, spanType, spanTable, nextParamIdx) {
16823
+ const conditions = [latestSpanPredicate(spanTable)];
16566
16824
  const params = [];
16567
16825
  let i = nextParamIdx;
16568
16826
  const spanTypeClause = buildBranchSpanTypeClause(spanType, params, i);
@@ -16583,11 +16841,11 @@ function buildListBranchesFilters(filters, spanType, nextParamIdx) {
16583
16841
  params.push(asIsoTimestamp(filters.startedAt.end));
16584
16842
  }
16585
16843
  if (filters.endedAt?.start) {
16586
- conditions.push(`r."endedAt" ${filters.endedAt.startExclusive ? ">" : ">="} $${i++}`);
16844
+ conditions.push(`(NOT r."isPending" AND r."endedAt" ${filters.endedAt.startExclusive ? ">" : ">="} $${i++})`);
16587
16845
  params.push(asIsoTimestamp(filters.endedAt.start));
16588
16846
  }
16589
16847
  if (filters.endedAt?.end) {
16590
- conditions.push(`r."endedAt" ${filters.endedAt.endExclusive ? "<" : "<="} $${i++}`);
16848
+ conditions.push(`(NOT r."isPending" AND r."endedAt" ${filters.endedAt.endExclusive ? "<" : "<="} $${i++})`);
16591
16849
  params.push(asIsoTimestamp(filters.endedAt.end));
16592
16850
  }
16593
16851
  if (filters.traceId !== void 0) {
@@ -16699,10 +16957,10 @@ function buildListBranchesFilters(filters, spanType, nextParamIdx) {
16699
16957
  conditions.push(`r."error" IS NOT NULL`);
16700
16958
  break;
16701
16959
  case _mastra_core_storage.TraceStatus.RUNNING:
16702
- conditions.push(`FALSE`);
16960
+ conditions.push(`r."isPending"`);
16703
16961
  break;
16704
16962
  case _mastra_core_storage.TraceStatus.SUCCESS:
16705
- conditions.push(`r."error" IS NULL`);
16963
+ conditions.push(`r."error" IS NULL AND NOT r."isPending"`);
16706
16964
  break;
16707
16965
  }
16708
16966
  return {
@@ -16721,7 +16979,7 @@ async function listBranches(client, schema, args) {
16721
16979
  return listBranchesPage(client, span, filters, pagination.page, pagination.perPage, orderBy.field, orderBy.direction);
16722
16980
  }
16723
16981
  async function listBranchesPage(client, span, filters, page, perPage, orderField, orderDir) {
16724
- const built = buildListBranchesFilters(filters, filters?.spanType, 1);
16982
+ const built = buildListBranchesFilters(filters, filters?.spanType, span, 1);
16725
16983
  if (!built) {
16726
16984
  const deltaCursor = deltaPollingFeatureEnabled() ? await readBranchesStreamHeadCursor(client, span, filters) : void 0;
16727
16985
  return {
@@ -16775,7 +17033,7 @@ async function listBranchesDelta(client, span, filters, after, limit) {
16775
17033
  deltaCursor
16776
17034
  };
16777
17035
  }
16778
- const built = buildListBranchesFilters(filters, filters?.spanType, 1);
17036
+ const built = buildListBranchesFilters(filters, filters?.spanType, span, 1);
16779
17037
  if (!built) return {
16780
17038
  branches: [],
16781
17039
  delta: {
@@ -16818,14 +17076,35 @@ async function readBranchesStreamHeadCursor(client, span, filters) {
16818
17076
  }
16819
17077
  //#endregion
16820
17078
  //#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)]);
17079
+ async function writeSpanRows(client, schema, rows) {
17080
+ const deduped = dedupeSpanRows(rows);
17081
+ const insert = buildInsert(schema, TABLE_SPAN_EVENTS, deduped, spanConflictClause(Object.keys(deduped[0] ?? {})));
16823
17082
  if (insert) await client.query(insert.text, insert.values);
16824
17083
  }
17084
+ /**
17085
+ * Collapse rows that share a primary key within a single batch. Postgres
17086
+ * rejects an `ON CONFLICT DO UPDATE` statement whose VALUES list would touch
17087
+ * the same row twice, which a batch containing both the start and the end of a
17088
+ * zero-duration span would do. The ended row wins, matching what the conflict
17089
+ * clause would have done across two separate statements.
17090
+ */
17091
+ function dedupeSpanRows(rows) {
17092
+ if (rows.length < 2) return rows;
17093
+ const byKey = /* @__PURE__ */ new Map();
17094
+ for (const row of rows) {
17095
+ const key = `${row.traceId}\u0000${row.spanId}\u0000${String(row.endedAt)}`;
17096
+ const existing = byKey.get(key);
17097
+ if (existing && !existing.isPending) continue;
17098
+ byKey.set(key, row);
17099
+ }
17100
+ return [...byKey.values()];
17101
+ }
17102
+ async function createSpan(client, schema, args) {
17103
+ await writeSpanRows(client, schema, [spanRecordToRow(args.span)]);
17104
+ }
16825
17105
  async function batchCreateSpans(client, schema, args) {
16826
17106
  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);
17107
+ await writeSpanRows(client, schema, args.records.map(spanRecordToRow));
16829
17108
  }
16830
17109
  async function getSpans(client, schema, args) {
16831
17110
  if (args.spanIds.length === 0) return {
@@ -16833,10 +17112,13 @@ async function getSpans(client, schema, args) {
16833
17112
  spans: []
16834
17113
  };
16835
17114
  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[])
17115
+ const rows = await client.manyOrNone(`SELECT * FROM (
17116
+ SELECT DISTINCT ON ("spanId") ${SPAN_SELECT_COLUMNS}
17117
+ FROM ${table}
17118
+ WHERE "traceId" = $1
17119
+ AND "spanId" = ANY($2::text[])
17120
+ ORDER BY "spanId", ${SPAN_COLLAPSE_ORDER}
17121
+ ) collapsed
16840
17122
  ORDER BY "startedAt" ASC`, [args.traceId, args.spanIds]);
16841
17123
  return {
16842
17124
  traceId: args.traceId,
@@ -16848,16 +17130,19 @@ async function getSpan(client, schema, args) {
16848
17130
  const row = await client.oneOrNone(`SELECT ${SPAN_SELECT_COLUMNS}
16849
17131
  FROM ${table}
16850
17132
  WHERE "traceId" = $1 AND "spanId" = $2
16851
- ORDER BY "endedAt" DESC
17133
+ ORDER BY ${SPAN_COLLAPSE_ORDER}
16852
17134
  LIMIT 1`, [args.traceId, args.spanId]);
16853
17135
  if (!row) return null;
16854
17136
  return { span: rowToSpanRecord(row) };
16855
17137
  }
16856
17138
  async function getTrace(client, schema, args) {
16857
17139
  const table = qualifiedTable(schema, TABLE_SPAN_EVENTS);
16858
- const rows = await client.manyOrNone(`SELECT ${SPAN_SELECT_COLUMNS}
16859
- FROM ${table}
16860
- WHERE "traceId" = $1
17140
+ const rows = await client.manyOrNone(`SELECT * FROM (
17141
+ SELECT DISTINCT ON ("spanId") ${SPAN_SELECT_COLUMNS}
17142
+ FROM ${table}
17143
+ WHERE "traceId" = $1
17144
+ ORDER BY "spanId", ${SPAN_COLLAPSE_ORDER}
17145
+ ) collapsed
16861
17146
  ORDER BY "startedAt" ASC`, [args.traceId]);
16862
17147
  if (!rows.length) return null;
16863
17148
  return {
@@ -16867,9 +17152,12 @@ async function getTrace(client, schema, args) {
16867
17152
  }
16868
17153
  async function getTraceLight(client, schema, args) {
16869
17154
  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
17155
+ const rows = await client.manyOrNone(`SELECT * FROM (
17156
+ SELECT DISTINCT ON ("spanId") ${SPAN_LIGHT_SELECT_COLUMNS}
17157
+ FROM ${table}
17158
+ WHERE "traceId" = $1
17159
+ ORDER BY "spanId", ${SPAN_COLLAPSE_ORDER}
17160
+ ) collapsed
16873
17161
  ORDER BY "startedAt" ASC`, [args.traceId]);
16874
17162
  if (!rows.length) return null;
16875
17163
  return {
@@ -16980,6 +17268,11 @@ var ObservabilityStoragePostgresVNext = class ObservabilityStoragePostgresVNext
16980
17268
  } catch (error) {
16981
17269
  if (!isDuplicateRelationError(error)) throw error;
16982
17270
  }
17271
+ for (const { table, column, ddl } of additiveColumns(this.#schema)) if (!await this.#client.oneOrNone(columnExistsSQL, [
17272
+ this.#schema,
17273
+ table,
17274
+ column
17275
+ ])) await this.#client.none(ddl);
16983
17276
  for (const ddl of allIndexDDL(this.#schema)) try {
16984
17277
  await this.#client.none(ddl);
16985
17278
  } catch (error) {
@@ -17076,8 +17369,8 @@ var ObservabilityStoragePostgresVNext = class ObservabilityStoragePostgresVNext
17076
17369
  }
17077
17370
  get observabilityStrategy() {
17078
17371
  return {
17079
- preferred: "insert-only",
17080
- supported: ["insert-only"]
17372
+ preferred: "event-sourced",
17373
+ supported: ["event-sourced"]
17081
17374
  };
17082
17375
  }
17083
17376
  getFeatures() {
@@ -20501,18 +20794,6 @@ function workflowSnapshotStatusIndexSQL(indexName, schemaName) {
20501
20794
  schemaName: getSchemaName(schemaName)
20502
20795
  })} (workflow_name, (snapshot ->> 'status'), "createdAt" DESC)`;
20503
20796
  }
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
20797
  var WorkflowsPG = class WorkflowsPG extends _mastra_core_storage.WorkflowsStorage {
20517
20798
  #db;
20518
20799
  #schema;
@@ -20792,11 +21073,11 @@ var WorkflowsPG = class WorkflowsPG extends _mastra_core_storage.WorkflowsStorag
20792
21073
  await this.#db.client.none(`INSERT INTO ${getTableName({
20793
21074
  indexName: _mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT,
20794
21075
  schemaName: getSchemaName(this.#schema)
20795
- })}
21076
+ })} AS t
20796
21077
  (workflow_name, run_id, "resourceId", snapshot, "createdAt", "updatedAt", "createdAtZ", "updatedAtZ")
20797
21078
  VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
20798
21079
  ON CONFLICT (workflow_name, run_id) DO UPDATE
20799
- SET "resourceId" = $3, snapshot = $4, "updatedAt" = $6, "updatedAtZ" = $8`, [
21080
+ SET "resourceId" = COALESCE($3, t."resourceId"), snapshot = $4, "updatedAt" = $6, "updatedAtZ" = $8`, [
20800
21081
  workflowName,
20801
21082
  runId,
20802
21083
  resourceId,
@@ -21754,6 +22035,12 @@ var PgFactoryStorageOps = class {
21754
22035
  async findMany(collection, where, opts) {
21755
22036
  return this.#select(this.#queryable, collection, where, opts);
21756
22037
  }
22038
+ async count(collection, where) {
22039
+ const schema = this.#schema(collection);
22040
+ const filter = this.#buildWhere(schema, where);
22041
+ const result = await this.#queryable.query(`SELECT COUNT(*) AS count FROM "${schema.name}" WHERE ${filter.sql}`, filter.args);
22042
+ return Number(result.rows[0]?.count ?? 0);
22043
+ }
21757
22044
  async insertOne(collection, row) {
21758
22045
  const schema = this.#schema(collection);
21759
22046
  const pk = primaryKeyOf(schema);