@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.js CHANGED
@@ -541,11 +541,22 @@ function buildFilterQuery(filter, minScore, topK) {
541
541
  }
542
542
  //#endregion
543
543
  //#region src/vector/index.ts
544
- const MAX_UPSERT_ROWS_PER_STATEMENT = Math.floor(65535 / 3);
544
+ const DEFAULT_NAMESPACE = "default";
545
+ const MAX_UPSERT_ROWS_PER_STATEMENT = Math.floor(65535 / 4);
545
546
  var PgVector = class extends MastraVector {
546
547
  pool;
548
+ /**
549
+ * Cache for the public `getIndexInfo()`. Holds the in-flight promise rather than the
550
+ * resolved value so concurrent callers on a cold cache share a single round trip.
551
+ */
547
552
  describeIndexCache = /* @__PURE__ */ new Map();
553
+ /**
554
+ * Cache for the catalog-only index metadata used by the internal hot paths
555
+ * (cache warmup, query, upsert, updateVector, setupIndex). Also memoizes the promise.
556
+ */
557
+ indexMetadataCache = /* @__PURE__ */ new Map();
548
558
  createdIndexes = /* @__PURE__ */ new Map();
559
+ namespaceReadyIndexes = /* @__PURE__ */ new Set();
549
560
  indexVectorTypes = /* @__PURE__ */ new Map();
550
561
  mutexesByName = /* @__PURE__ */ new Map();
551
562
  schema;
@@ -601,7 +612,7 @@ var PgVector = class extends MastraVector {
601
612
  try {
602
613
  const existingIndexes = await this.listIndexes();
603
614
  await Promise.all(existingIndexes.map(async (indexName) => {
604
- const info = await this.getIndexInfo({ indexName });
615
+ const info = await this.getIndexMetadata({ indexName });
605
616
  const key = await this.getIndexCacheKey({
606
617
  indexName,
607
618
  metric: info.metric,
@@ -774,20 +785,76 @@ var PgVector = class extends MastraVector {
774
785
  const quotedVectorName = `"${parsedIndexName}_vector_idx"`;
775
786
  return {
776
787
  tableName: quotedSchemaName ? `${quotedSchemaName}.${quotedIndexName}` : quotedIndexName,
777
- vectorIndexName: quotedVectorName
788
+ vectorIndexName: quotedVectorName,
789
+ parsedIndexName
778
790
  };
779
791
  }
780
792
  getSchemaName() {
781
793
  return this.schema ? `"${parseSqlIdentifier(this.schema, "schema name")}"` : void 0;
782
794
  }
795
+ async ensureNamespaceSchema(indexName, client) {
796
+ const { tableName, parsedIndexName } = this.getTableName(indexName);
797
+ const schemaName = this.schema ? parseSqlIdentifier(this.schema, "schema name") : "public";
798
+ if ((await client.query(`SELECT 1
799
+ FROM information_schema.columns
800
+ WHERE table_schema = $1 AND table_name = $2 AND column_name = 'vector_id'`, [schemaName, parsedIndexName])).rowCount === 0) return;
801
+ await client.query(`ALTER TABLE ${tableName} ADD COLUMN IF NOT EXISTS namespace VARCHAR(255) NOT NULL DEFAULT '${DEFAULT_NAMESPACE}'`);
802
+ const legacyConstraints = await client.query(`SELECT c.conname
803
+ FROM pg_constraint c
804
+ JOIN pg_class t ON t.oid = c.conrelid
805
+ JOIN pg_namespace n ON n.oid = t.relnamespace
806
+ WHERE n.nspname = $1
807
+ AND t.relname = $2
808
+ AND c.contype = 'u'
809
+ AND pg_get_constraintdef(c.oid) = 'UNIQUE (vector_id)'`, [schemaName, parsedIndexName]);
810
+ for (const { conname } of legacyConstraints.rows) {
811
+ const parsedConstraintName = parseSqlIdentifier(conname, "constraint name");
812
+ await client.query(`ALTER TABLE ${tableName} DROP CONSTRAINT "${parsedConstraintName}"`);
813
+ }
814
+ const namespaceIndexName = parseSqlIdentifier(`${parsedIndexName}_namespace_vector_id_idx`, "index name");
815
+ await client.query(`CREATE UNIQUE INDEX IF NOT EXISTS "${namespaceIndexName}" ON ${tableName} (namespace, vector_id)`);
816
+ }
783
817
  transformFilter(filter) {
784
818
  return new PGFilterTranslator().translate(filter);
785
819
  }
820
+ /**
821
+ * Cached variant of {@link describeIndex}, including the row count.
822
+ *
823
+ * Internal code paths do not use this - they use the catalog-only
824
+ * {@link getIndexMetadata}, which never scans the table.
825
+ */
786
826
  async getIndexInfo({ indexName }) {
787
- if (!this.describeIndexCache.has(indexName)) this.describeIndexCache.set(indexName, await this.describeIndex({ indexName }));
788
- return this.describeIndexCache.get(indexName);
827
+ return this.memoize(this.describeIndexCache, indexName, () => this.describeIndex({ indexName }));
828
+ }
829
+ /**
830
+ * Cached index metadata read from the Postgres catalog only.
831
+ *
832
+ * This is what every internal caller needs: none of them read `count`, and paying for
833
+ * `SELECT COUNT(*)` on a large index costs a full heap scan per call.
834
+ */
835
+ getIndexMetadata({ indexName }) {
836
+ return this.memoize(this.indexMetadataCache, indexName, () => this.describeIndexMetadata({ indexName }));
837
+ }
838
+ /**
839
+ * Stores the in-flight promise in `cache` so concurrent callers on a cold cache share one
840
+ * round trip, and drops the entry if it rejects so a transient failure is not cached forever.
841
+ */
842
+ memoize(cache, key, load) {
843
+ const cached = cache.get(key);
844
+ if (cached) return cached;
845
+ const pending = load();
846
+ cache.set(key, pending);
847
+ pending.catch(() => {
848
+ if (cache.get(key) === pending) cache.delete(key);
849
+ });
850
+ return pending;
851
+ }
852
+ /** Drops every cached view of an index, e.g. after its index definition or table changed. */
853
+ invalidateIndexCaches(indexName) {
854
+ this.describeIndexCache.delete(indexName);
855
+ this.indexMetadataCache.delete(indexName);
789
856
  }
790
- async query({ indexName, queryVector, topK = 10, filter, includeVector = false, minScore = -1, ef, probes }) {
857
+ async query({ indexName, queryVector, topK = 10, filter, includeVector = false, minScore = -1, ef, probes, namespace = DEFAULT_NAMESPACE }) {
791
858
  try {
792
859
  validateTopK("PG", topK);
793
860
  if (queryVector !== void 0) {
@@ -808,16 +875,23 @@ var PgVector = class extends MastraVector {
808
875
  try {
809
876
  const { sql: filterQuery, values: filterValues } = buildDeleteFilterQuery(this.transformFilter(filter));
810
877
  const { tableName } = this.getTableName(indexName);
878
+ const filterClause = filterQuery.trim().replace(/^WHERE\s+/i, "");
879
+ const namespaceParam = filterValues.length + 1;
811
880
  const query = `
812
881
  SELECT
813
882
  vector_id as id,
814
883
  metadata
815
884
  ${includeVector ? ", embedding" : ""}
816
885
  FROM ${tableName}
817
- ${filterQuery}
886
+ WHERE namespace = $${namespaceParam}
887
+ ${filterClause ? `AND (${filterClause})` : ""}
818
888
  ORDER BY vector_id
819
- LIMIT $${filterValues.length + 1}`;
820
- return (await client.query(query, [...filterValues, topK])).rows.map(({ id, metadata, embedding }) => ({
889
+ LIMIT $${namespaceParam + 1}`;
890
+ return (await client.query(query, [
891
+ ...filterValues,
892
+ namespace,
893
+ topK
894
+ ])).rows.map(({ id, metadata, embedding }) => ({
821
895
  id,
822
896
  score: 0,
823
897
  metadata,
@@ -841,7 +915,7 @@ var PgVector = class extends MastraVector {
841
915
  await this.ensureSearchPath(client);
842
916
  await client.query("BEGIN");
843
917
  const { sql: filterQuery, values: filterValues } = buildFilterQuery(this.transformFilter(filter), minScore, topK);
844
- const indexInfo = await this.getIndexInfo({ indexName });
918
+ const indexInfo = await this.getIndexMetadata({ indexName });
845
919
  const metric = indexInfo.metric ?? "cosine";
846
920
  const ops = this.getVectorOps(indexInfo.vectorType, metric);
847
921
  const vectorStr = ops.formatVector(queryVector, indexInfo.dimension);
@@ -849,14 +923,18 @@ var PgVector = class extends MastraVector {
849
923
  const calculatedEf = ef ?? Math.max(topK, (indexInfo?.config?.m ?? 16) * topK);
850
924
  const searchEf = Math.min(1e3, Math.max(1, calculatedEf));
851
925
  await client.query(`SET LOCAL hnsw.ef_search = ${searchEf}`);
926
+ await client.query(`SET LOCAL hnsw.iterative_scan = strict_order`);
852
927
  }
853
928
  if (indexInfo.type === "ivfflat" && probes) await client.query(`SET LOCAL ivfflat.probes = ${probes}`);
854
929
  const { tableName } = this.getTableName(indexName);
855
930
  const qualifiedVectorType = this.getVectorTypeName(indexInfo.vectorType, indexInfo.dimension);
856
931
  const distanceExpr = `embedding ${ops.distanceOperator} '${vectorStr}'::${qualifiedVectorType}`;
857
932
  const scoreExpr = ops.scoreExpr(distanceExpr);
858
- const hasFilter = filterQuery.trim().length > 0;
859
- const query = indexInfo.type === "hnsw" && !hasFilter && minScore <= 0 ? `
933
+ const filterClause = filterQuery.trim().replace(/^WHERE\s+/i, "");
934
+ const hasFilter = filterClause.length > 0;
935
+ const useIndexedOrder = indexInfo.type === "hnsw" && !hasFilter && minScore <= 0;
936
+ const namespaceParam = filterValues.length + 1;
937
+ const query = useIndexedOrder ? `
860
938
  WITH vector_scores AS (
861
939
  SELECT
862
940
  vector_id as id,
@@ -864,6 +942,7 @@ var PgVector = class extends MastraVector {
864
942
  metadata
865
943
  ${includeVector ? ", embedding" : ""}
866
944
  FROM ${tableName}
945
+ WHERE namespace = $${namespaceParam}
867
946
  ORDER BY ${distanceExpr}
868
947
  LIMIT $2
869
948
  )
@@ -878,14 +957,15 @@ var PgVector = class extends MastraVector {
878
957
  metadata
879
958
  ${includeVector ? ", embedding" : ""}
880
959
  FROM ${tableName}
881
- ${filterQuery}
960
+ WHERE namespace = $${namespaceParam}
961
+ ${filterClause ? `AND (${filterClause})` : ""}
882
962
  )
883
963
  SELECT *
884
964
  FROM vector_scores
885
965
  WHERE score > $1
886
966
  ORDER BY score DESC
887
967
  LIMIT $2`;
888
- const result = await client.query(query, filterValues);
968
+ const result = await client.query(query, [...filterValues, namespace]);
889
969
  await client.query("COMMIT");
890
970
  return result.rows.map(({ id, score, metadata, embedding }) => ({
891
971
  id,
@@ -907,7 +987,7 @@ var PgVector = class extends MastraVector {
907
987
  client.release();
908
988
  }
909
989
  }
910
- async upsert({ indexName, vectors, metadata, ids, deleteFilter }) {
990
+ async upsert({ indexName, vectors, metadata, ids, deleteFilter, namespace = DEFAULT_NAMESPACE }) {
911
991
  validateUpsertInput("PG", vectors, metadata, ids);
912
992
  const { tableName } = this.getTableName(indexName);
913
993
  const client = await this.pool.connect();
@@ -922,8 +1002,8 @@ var PgVector = class extends MastraVector {
922
1002
  const { sql: filterQuery, values: filterValues } = buildDeleteFilterQuery(this.transformFilter(deleteFilter));
923
1003
  const whereClause = filterQuery.trim().replace(/^WHERE\s+/i, "");
924
1004
  if (whereClause) {
925
- const deleteQuery = `DELETE FROM ${tableName} WHERE ${whereClause}`;
926
- const result = await client.query(deleteQuery, filterValues);
1005
+ const deleteQuery = `DELETE FROM ${tableName} WHERE namespace = $${filterValues.length + 1} AND (${whereClause})`;
1006
+ const result = await client.query(deleteQuery, [...filterValues, namespace]);
927
1007
  this.logger?.debug(`Deleted ${result.rowCount || 0} vectors before upsert`, {
928
1008
  indexName,
929
1009
  deletedCount: result.rowCount || 0
@@ -931,15 +1011,15 @@ var PgVector = class extends MastraVector {
931
1011
  }
932
1012
  }
933
1013
  const vectorIds = ids || vectors.map(() => crypto.randomUUID());
934
- const indexInfo = await this.getIndexInfo({ indexName });
1014
+ const indexInfo = await this.getIndexMetadata({ indexName });
935
1015
  const qualifiedVectorType = this.getVectorTypeName(indexInfo.vectorType, indexInfo.dimension);
936
1016
  const ops = this.getVectorOps(indexInfo.vectorType, indexInfo.metric ?? "cosine");
937
1017
  if (new Set(vectorIds).size !== vectorIds.length) for (let i = 0; i < vectors.length; i++) {
938
1018
  const vectorStr = ops.formatVector(vectors[i], indexInfo.dimension);
939
1019
  const query = `
940
- INSERT INTO ${tableName} (vector_id, embedding, metadata)
941
- VALUES ($1, $2::${qualifiedVectorType}, $3::jsonb)
942
- ON CONFLICT (vector_id)
1020
+ INSERT INTO ${tableName} (vector_id, embedding, metadata, namespace)
1021
+ VALUES ($1, $2::${qualifiedVectorType}, $3::jsonb, $4)
1022
+ ON CONFLICT (namespace, vector_id)
943
1023
  DO UPDATE SET
944
1024
  embedding = $2::${qualifiedVectorType},
945
1025
  metadata = $3::jsonb
@@ -947,7 +1027,8 @@ var PgVector = class extends MastraVector {
947
1027
  await client.query(query, [
948
1028
  vectorIds[i],
949
1029
  vectorStr,
950
- JSON.stringify(metadata?.[i] || {})
1030
+ JSON.stringify(metadata?.[i] || {}),
1031
+ namespace
951
1032
  ]);
952
1033
  }
953
1034
  else for (let start = 0; start < vectors.length; start += MAX_UPSERT_ROWS_PER_STATEMENT) {
@@ -956,13 +1037,13 @@ var PgVector = class extends MastraVector {
956
1037
  const values = [];
957
1038
  for (let i = start; i < end; i++) {
958
1039
  const base = values.length;
959
- rows.push(`($${base + 1}, $${base + 2}::${qualifiedVectorType}, $${base + 3}::jsonb)`);
960
- values.push(vectorIds[i], ops.formatVector(vectors[i], indexInfo.dimension), JSON.stringify(metadata?.[i] || {}));
1040
+ rows.push(`($${base + 1}, $${base + 2}::${qualifiedVectorType}, $${base + 3}::jsonb, $${base + 4})`);
1041
+ values.push(vectorIds[i], ops.formatVector(vectors[i], indexInfo.dimension), JSON.stringify(metadata?.[i] || {}), namespace);
961
1042
  }
962
1043
  const query = `
963
- INSERT INTO ${tableName} (vector_id, embedding, metadata)
1044
+ INSERT INTO ${tableName} (vector_id, embedding, metadata, namespace)
964
1045
  VALUES ${rows.join(", ")}
965
- ON CONFLICT (vector_id)
1046
+ ON CONFLICT (namespace, vector_id)
966
1047
  DO UPDATE SET
967
1048
  embedding = EXCLUDED.embedding,
968
1049
  metadata = EXCLUDED.metadata
@@ -1082,9 +1163,9 @@ var PgVector = class extends MastraVector {
1082
1163
  vectorType,
1083
1164
  metadataIndexes
1084
1165
  });
1085
- if (this.cachedIndexExists(indexName, indexCacheKey)) return;
1166
+ if (this.cachedIndexExists(indexName, indexCacheKey) && this.namespaceReadyIndexes.has(indexName)) return;
1086
1167
  await this.getMutexByName(`create-${indexName}`).runExclusive(async () => {
1087
- if (this.cachedIndexExists(indexName, indexCacheKey)) return;
1168
+ if (this.cachedIndexExists(indexName, indexCacheKey) && this.namespaceReadyIndexes.has(indexName)) return;
1088
1169
  const client = await this.pool.connect();
1089
1170
  try {
1090
1171
  await this.setupSchema(client);
@@ -1128,12 +1209,15 @@ var PgVector = class extends MastraVector {
1128
1209
  await client.query(`
1129
1210
  CREATE TABLE IF NOT EXISTS ${tableName} (
1130
1211
  id SERIAL PRIMARY KEY,
1131
- vector_id TEXT UNIQUE NOT NULL,
1212
+ vector_id TEXT NOT NULL,
1132
1213
  embedding ${qualifiedVectorType}(${dimension}),
1133
- metadata JSONB DEFAULT '{}'::jsonb
1214
+ metadata JSONB DEFAULT '{}'::jsonb,
1215
+ namespace VARCHAR(255) NOT NULL DEFAULT '${DEFAULT_NAMESPACE}'
1134
1216
  );
1135
1217
  `);
1218
+ await this.ensureNamespaceSchema(indexName, client);
1136
1219
  this.createdIndexes.set(indexName, indexCacheKey);
1220
+ this.namespaceReadyIndexes.add(indexName);
1137
1221
  this.indexVectorTypes.set(indexName, vectorType);
1138
1222
  if (buildIndex) await this.setupIndex({
1139
1223
  indexName,
@@ -1144,6 +1228,7 @@ var PgVector = class extends MastraVector {
1144
1228
  if (metadataIndexes?.length) await this.createMetadataIndexes(tableName, indexName, metadataIndexes);
1145
1229
  } catch (error) {
1146
1230
  this.createdIndexes.delete(indexName);
1231
+ this.namespaceReadyIndexes.delete(indexName);
1147
1232
  this.indexVectorTypes.delete(indexName);
1148
1233
  throw error;
1149
1234
  } finally {
@@ -1214,7 +1299,7 @@ var PgVector = class extends MastraVector {
1214
1299
  let existingIndexInfo = null;
1215
1300
  let dimension = 0;
1216
1301
  try {
1217
- existingIndexInfo = await this.getIndexInfo({ indexName });
1302
+ existingIndexInfo = await this.getIndexMetadata({ indexName });
1218
1303
  dimension = existingIndexInfo.dimension;
1219
1304
  if (isConfigEmpty && existingIndexInfo.metric === metric) if (existingIndexInfo.type === "flat") this.logger?.debug(`No index exists for ${vectorIndexName}, will create default ivfflat index`);
1220
1305
  else {
@@ -1249,12 +1334,12 @@ var PgVector = class extends MastraVector {
1249
1334
  }
1250
1335
  this.logger?.info(`Index ${vectorIndexName} configuration changed, rebuilding index`);
1251
1336
  await client.query(`DROP INDEX IF EXISTS ${vectorIndexName}`);
1252
- this.describeIndexCache.delete(indexName);
1337
+ this.invalidateIndexCaches(indexName);
1253
1338
  } catch {
1254
1339
  this.logger?.debug(`Index ${indexName} doesn't exist yet, will create it`);
1255
1340
  }
1256
1341
  if (indexType === "flat") {
1257
- this.describeIndexCache.delete(indexName);
1342
+ this.invalidateIndexCaches(indexName);
1258
1343
  return;
1259
1344
  }
1260
1345
  await this.ensureSearchPath(client);
@@ -1404,6 +1489,19 @@ var PgVector = class extends MastraVector {
1404
1489
  * @returns A promise that resolves to the index statistics including dimension, count and metric
1405
1490
  */
1406
1491
  async describeIndex({ indexName }) {
1492
+ const metadata = await this.describeIndexMetadata({ indexName });
1493
+ const count = await this.countIndexRows({ indexName });
1494
+ return {
1495
+ ...metadata,
1496
+ count
1497
+ };
1498
+ }
1499
+ /**
1500
+ * Reads the index metadata that lives in the Postgres catalog. Unlike
1501
+ * {@link describeIndex} it issues no `COUNT(*)`, so its cost does not grow with the
1502
+ * number of rows in the table.
1503
+ */
1504
+ async describeIndexMetadata({ indexName }) {
1407
1505
  const client = await this.pool.connect();
1408
1506
  try {
1409
1507
  const { tableName } = this.getTableName(indexName);
@@ -1423,10 +1521,6 @@ var PgVector = class extends MastraVector {
1423
1521
  FROM pg_attribute
1424
1522
  WHERE attrelid = $1::regclass
1425
1523
  AND attname = 'embedding';
1426
- `;
1427
- const countQuery = `
1428
- SELECT COUNT(*) as count
1429
- FROM ${tableName};
1430
1524
  `;
1431
1525
  const indexQuery = `
1432
1526
  SELECT
@@ -1442,7 +1536,6 @@ var PgVector = class extends MastraVector {
1442
1536
  AND n.nspname = $2;
1443
1537
  `;
1444
1538
  const dimResult = await client.query(dimensionQuery, [tableName]);
1445
- const countResult = await client.query(countQuery);
1446
1539
  const { index_method, index_def, operator_class } = (await client.query(indexQuery, [`${indexName}_vector_idx`, this.schema || "public"])).rows[0] || {
1447
1540
  index_method: "flat",
1448
1541
  index_def: "",
@@ -1461,7 +1554,6 @@ var PgVector = class extends MastraVector {
1461
1554
  }
1462
1555
  return {
1463
1556
  dimension: dimResult.rows[0].dimension,
1464
- count: parseInt(countResult.rows[0].count),
1465
1557
  metric,
1466
1558
  type: index_method,
1467
1559
  vectorType,
@@ -1481,14 +1573,41 @@ var PgVector = class extends MastraVector {
1481
1573
  client.release();
1482
1574
  }
1483
1575
  }
1576
+ /**
1577
+ * Exact row count for an index. This is a full scan of the table, so it is only issued
1578
+ * for the public {@link describeIndex}, never from an internal code path.
1579
+ */
1580
+ async countIndexRows({ indexName }) {
1581
+ const client = await this.pool.connect();
1582
+ try {
1583
+ const { tableName } = this.getTableName(indexName);
1584
+ const countResult = await client.query(`
1585
+ SELECT COUNT(*) as count
1586
+ FROM ${tableName};
1587
+ `);
1588
+ return parseInt(countResult.rows[0].count);
1589
+ } catch (e) {
1590
+ const mastraError = new MastraError({
1591
+ id: createVectorErrorId("PG", "DESCRIBE_INDEX", "FAILED"),
1592
+ domain: ErrorDomain.MASTRA_VECTOR,
1593
+ category: ErrorCategory.THIRD_PARTY,
1594
+ details: { indexName }
1595
+ }, e);
1596
+ this.logger?.trackException(mastraError);
1597
+ throw mastraError;
1598
+ } finally {
1599
+ client.release();
1600
+ }
1601
+ }
1484
1602
  async deleteIndex({ indexName }) {
1485
1603
  const client = await this.pool.connect();
1486
1604
  try {
1487
1605
  const { tableName } = this.getTableName(indexName);
1488
1606
  await client.query(`DROP TABLE IF EXISTS ${tableName} CASCADE`);
1489
1607
  this.createdIndexes.delete(indexName);
1608
+ this.namespaceReadyIndexes.delete(indexName);
1490
1609
  this.indexVectorTypes.delete(indexName);
1491
- this.describeIndexCache.delete(indexName);
1610
+ this.invalidateIndexCaches(indexName);
1492
1611
  } catch (error) {
1493
1612
  await client.query("ROLLBACK");
1494
1613
  const mastraError = new MastraError({
@@ -1538,7 +1657,7 @@ var PgVector = class extends MastraVector {
1538
1657
  * @returns A promise that resolves when the update is complete.
1539
1658
  * @throws Will throw an error if no updates are provided or if the update operation fails.
1540
1659
  */
1541
- async updateVector({ indexName, id, filter, update }) {
1660
+ async updateVector({ indexName, id, filter, update, namespace = DEFAULT_NAMESPACE }) {
1542
1661
  let client;
1543
1662
  try {
1544
1663
  if (!update.vector && !update.metadata) throw new Error("No updates provided");
@@ -1559,7 +1678,7 @@ var PgVector = class extends MastraVector {
1559
1678
  client = await this.pool.connect();
1560
1679
  await this.ensureSearchPath(client);
1561
1680
  const { tableName } = this.getTableName(indexName);
1562
- const indexInfo = await this.getIndexInfo({ indexName });
1681
+ const indexInfo = await this.getIndexMetadata({ indexName });
1563
1682
  const qualifiedVectorType = this.getVectorTypeName(indexInfo.vectorType, indexInfo.dimension);
1564
1683
  const ops = this.getVectorOps(indexInfo.vectorType, indexInfo.metric ?? "cosine");
1565
1684
  let updateParts = [];
@@ -1578,8 +1697,11 @@ var PgVector = class extends MastraVector {
1578
1697
  if (updateParts.length === 0) return;
1579
1698
  let whereClause;
1580
1699
  let whereValues;
1700
+ const namespaceIndex = valueIndex;
1701
+ values.push(namespace);
1702
+ valueIndex++;
1581
1703
  if (id) {
1582
- whereClause = `vector_id = $${valueIndex}`;
1704
+ whereClause = `namespace = $${namespaceIndex} AND vector_id = $${valueIndex}`;
1583
1705
  whereValues = [id];
1584
1706
  } else {
1585
1707
  if (!filter || Object.keys(filter).length === 0) throw new MastraError({
@@ -1604,6 +1726,7 @@ var PgVector = class extends MastraVector {
1604
1726
  whereClause = whereClause.replace(/\$(\d+)/g, (match, num) => {
1605
1727
  return `$${parseInt(num) + valueIndex - 1}`;
1606
1728
  });
1729
+ whereClause = `namespace = $${namespaceIndex} AND (${whereClause})`;
1607
1730
  whereValues = filterValues;
1608
1731
  }
1609
1732
  const query = `
@@ -1643,16 +1766,16 @@ var PgVector = class extends MastraVector {
1643
1766
  * @returns A promise that resolves when the deletion is complete.
1644
1767
  * @throws Will throw an error if the deletion operation fails.
1645
1768
  */
1646
- async deleteVector({ indexName, id }) {
1769
+ async deleteVector({ indexName, id, namespace = DEFAULT_NAMESPACE }) {
1647
1770
  let client;
1648
1771
  try {
1649
1772
  client = await this.pool.connect();
1650
1773
  const { tableName } = this.getTableName(indexName);
1651
1774
  const query = `
1652
1775
  DELETE FROM ${tableName}
1653
- WHERE vector_id = $1
1776
+ WHERE vector_id = $1 AND namespace = $2
1654
1777
  `;
1655
- await client.query(query, [id]);
1778
+ await client.query(query, [id, namespace]);
1656
1779
  } catch (error) {
1657
1780
  const mastraError = new MastraError({
1658
1781
  id: createVectorErrorId("PG", "DELETE_VECTOR", "FAILED"),
@@ -1676,14 +1799,15 @@ var PgVector = class extends MastraVector {
1676
1799
  * @returns A promise that resolves when the deletion is complete.
1677
1800
  * @throws Will throw an error if the deletion operation fails.
1678
1801
  */
1679
- async deleteVectors({ indexName, filter, ids }) {
1802
+ async deleteVectors({ indexName, filter, ids, namespace }) {
1680
1803
  let client;
1804
+ const effectiveNamespace = namespace ?? DEFAULT_NAMESPACE;
1681
1805
  try {
1682
1806
  client = await this.pool.connect();
1683
1807
  const { tableName } = this.getTableName(indexName);
1684
- if (!filter && !ids) throw new MastraError({
1808
+ if (!filter && !ids && namespace === void 0) throw new MastraError({
1685
1809
  id: createVectorErrorId("PG", "DELETE_VECTORS", "NO_TARGET"),
1686
- text: "Either filter or ids must be provided",
1810
+ text: "Either filter or ids must be provided, unless an explicit namespace is used",
1687
1811
  domain: ErrorDomain.MASTRA_VECTOR,
1688
1812
  category: ErrorCategory.USER,
1689
1813
  details: { indexName }
@@ -1705,9 +1829,9 @@ var PgVector = class extends MastraVector {
1705
1829
  category: ErrorCategory.USER,
1706
1830
  details: { indexName }
1707
1831
  });
1708
- query = `DELETE FROM ${tableName} WHERE vector_id IN (${ids.map((_, i) => `$${i + 1}`).join(", ")})`;
1709
- values = ids;
1710
- } else {
1832
+ query = `DELETE FROM ${tableName} WHERE vector_id IN (${ids.map((_, i) => `$${i + 1}`).join(", ")}) AND namespace = $${ids.length + 1}`;
1833
+ values = [...ids, effectiveNamespace];
1834
+ } else if (filter) {
1711
1835
  if (!filter || Object.keys(filter).length === 0) throw new MastraError({
1712
1836
  id: createVectorErrorId("PG", "DELETE_VECTORS", "EMPTY_FILTER"),
1713
1837
  text: "Cannot delete with empty filter. Use deleteIndex to delete all vectors.",
@@ -1727,8 +1851,11 @@ var PgVector = class extends MastraVector {
1727
1851
  filter: JSON.stringify(filter)
1728
1852
  }
1729
1853
  });
1730
- query = `DELETE FROM ${tableName} WHERE ${whereClause}`;
1731
- values = filterValues;
1854
+ query = `DELETE FROM ${tableName} WHERE namespace = $${filterValues.length + 1} AND (${whereClause})`;
1855
+ values = [...filterValues, effectiveNamespace];
1856
+ } else {
1857
+ query = `DELETE FROM ${tableName} WHERE namespace = $1`;
1858
+ values = [effectiveNamespace];
1732
1859
  }
1733
1860
  const result = await client.query(query, values);
1734
1861
  this.logger?.info(`Deleted ${result.rowCount || 0} vectors from ${indexName}`, {
@@ -13567,6 +13694,11 @@ const SPAN_EVENT_COLUMNS = [
13567
13694
  type: "boolean",
13568
13695
  defaultSql: "false"
13569
13696
  },
13697
+ {
13698
+ name: "isPending",
13699
+ type: "boolean",
13700
+ defaultSql: "false"
13701
+ },
13570
13702
  {
13571
13703
  name: "startedAt",
13572
13704
  type: "timestamptz"
@@ -13863,6 +13995,7 @@ const SPAN_LIGHT_SELECT_COLUMN_NAMES = [
13863
13995
  "spanType",
13864
13996
  "error",
13865
13997
  "isEvent",
13998
+ "isPending",
13866
13999
  "startedAt",
13867
14000
  "endedAt"
13868
14001
  ];
@@ -14257,6 +14390,26 @@ function allTableDDL(schema, mode) {
14257
14390
  discoveryTableDDL(schema)
14258
14391
  ];
14259
14392
  }
14393
+ /**
14394
+ * Additive column migrations for tables created by an older version of this
14395
+ * schema. `CREATE TABLE IF NOT EXISTS` is a no-op on an existing table, so new
14396
+ * columns have to be added out of band.
14397
+ *
14398
+ * `ALTER TABLE` takes an AccessExclusiveLock on the partitioned parent, so
14399
+ * callers must check {@link columnExistsSQL} first and only run the statement
14400
+ * when the column is genuinely missing. On Postgres 11+ the non-volatile
14401
+ * default does not rewrite the table, and the ALTER cascades to partitions.
14402
+ */
14403
+ function additiveColumns(schema) {
14404
+ return [{
14405
+ table: TABLE_SPAN_EVENTS,
14406
+ column: "isPending",
14407
+ ddl: `ALTER TABLE ${qualifiedTable(schema, TABLE_SPAN_EVENTS)} ADD COLUMN IF NOT EXISTS "isPending" boolean NOT NULL DEFAULT false`
14408
+ }];
14409
+ }
14410
+ /** Existence probe for an additive column. Params: schema, table, column. */
14411
+ const columnExistsSQL = `SELECT 1 FROM information_schema.columns
14412
+ WHERE table_schema = $1 AND table_name = $2 AND column_name = $3`;
14260
14413
  /** Index CREATEs. Safe to run repeatedly. */
14261
14414
  function allIndexDDL(schema) {
14262
14415
  return tableIndexes().map((spec) => indexDDL(schema, spec));
@@ -14645,6 +14798,7 @@ function commonContextToRow(record) {
14645
14798
  };
14646
14799
  }
14647
14800
  function spanRecordToRow(span) {
14801
+ const isPending = !span.isEvent && span.endedAt == null;
14648
14802
  const endedAt = span.isEvent ? span.startedAt : span.endedAt ?? span.startedAt;
14649
14803
  const metadata = span.metadata ?? null;
14650
14804
  return {
@@ -14655,6 +14809,7 @@ function spanRecordToRow(span) {
14655
14809
  name: span.name,
14656
14810
  spanType: span.spanType,
14657
14811
  isEvent: Boolean(span.isEvent),
14812
+ isPending,
14658
14813
  startedAt: toIsoOrDate(span.startedAt),
14659
14814
  endedAt: toIsoOrDate(endedAt),
14660
14815
  tags: normalizeTags(span.tags),
@@ -14669,9 +14824,19 @@ function spanRecordToRow(span) {
14669
14824
  requestContext: jsonField(span.requestContext)
14670
14825
  };
14671
14826
  }
14827
+ /**
14828
+ * Recover a span's real `endedAt` from its row. A pending row carries a
14829
+ * synthesized `endedAt` (a copy of `startedAt`) because the column is part of
14830
+ * the primary key; callers must see null so the span reads as still running.
14831
+ */
14832
+ function spanRowEndedAt(row, startedAt) {
14833
+ if (row.isEvent) return startedAt;
14834
+ if (row.isPending) return null;
14835
+ return toDateOrNull(row.endedAt);
14836
+ }
14672
14837
  function rowToSpanRecord(row) {
14673
14838
  const startedAt = toDate(row.startedAt);
14674
- const endedAt = row.isEvent ? startedAt : toDateOrNull(row.endedAt);
14839
+ const endedAt = spanRowEndedAt(row, startedAt);
14675
14840
  const error = parsedJson(row.error);
14676
14841
  const { executionSource, ...ctx } = rowToCommonContext(row);
14677
14842
  return {
@@ -14705,7 +14870,7 @@ function rowToSpanRecord(row) {
14705
14870
  */
14706
14871
  function rowToLightSpanRecord(row) {
14707
14872
  const startedAt = toDate(row.startedAt);
14708
- const endedAt = row.isEvent ? startedAt : toDateOrNull(row.endedAt);
14873
+ const endedAt = spanRowEndedAt(row, startedAt);
14709
14874
  return {
14710
14875
  traceId: row.traceId,
14711
14876
  spanId: row.spanId,
@@ -15236,22 +15401,42 @@ const COMPLEX_GROUP_BY_EXCLUDED = /* @__PURE__ */ new Set([
15236
15401
  "requestContext"
15237
15402
  ]);
15238
15403
  //#endregion
15404
+ //#region src/storage/db/sanitize-json.ts
15405
+ /**
15406
+ * Sanitizes JSON string for PostgreSQL jsonb:
15407
+ * - Removes problematic Unicode sequences:
15408
+ * - \u0000 (null character) - causes error 22P05 "unsupported Unicode escape sequence"
15409
+ * - \uD800-\uDFFF (unpaired surrogates) - causes "Unicode low surrogate must follow a high surrogate"
15410
+ * - \\uD800 (escaped-backslash + surrogate, e.g. from JS regex literals like [^\ud800-\udfff]):
15411
+ * removing just \uXXXX would leave a dangling backslash that creates a new invalid escape (e.g. \-)
15412
+ * - Escapes any remaining invalid JSON escape sequences (e.g. \v, \k, \-)
15413
+ */
15414
+ function sanitizeJsonForPg(jsonString) {
15415
+ return jsonString.replace(/\\\\?u(0000|[Dd][89A-Fa-f][0-9A-Fa-f]{2})/g, "").replace(/(^|[^\\])(\\(?!["\\/bfnrtu]))/g, "$1\\\\");
15416
+ }
15417
+ //#endregion
15239
15418
  //#region src/storage/domains/observability/v-next/sql.ts
15240
15419
  /**
15241
15420
  * SQL helpers for the v-next Postgres observability domain.
15242
15421
  *
15243
- * Provides a multi-row INSERT builder with `ON CONFLICT DO NOTHING` for
15244
- * insert-only retry idempotency, and explicit jsonb / text[] casts so the
15245
- * pg driver doesn't have to guess column types.
15422
+ * Provides a multi-row INSERT builder with a configurable conflict clause
15423
+ * (`ON CONFLICT DO NOTHING` by default, for retry idempotency), and explicit
15424
+ * jsonb / text[] casts so the pg driver doesn't have to guess column types.
15246
15425
  */
15247
15426
  /**
15248
15427
  * Encode a JS value for a `$N::jsonb` cast. Always `JSON.stringify` so a
15249
15428
  * plain string like `"hello"` becomes `"hello"` (a valid JSON scalar) and
15250
15429
  * not the bare word `hello`, which Postgres rejects when cast to jsonb.
15430
+ *
15431
+ * The result is sanitized because Postgres rejects some sequences that are
15432
+ * legal in JSON: NUL (`\u0000`) fails with 22P05 and unpaired UTF-16
15433
+ * surrogates fail with 22P02. Inserts here are batched into a single
15434
+ * multi-row statement, so one bad value would otherwise reject the whole
15435
+ * batch of observability events.
15251
15436
  */
15252
15437
  function encodeJsonb(value) {
15253
15438
  if (value === null || value === void 0) return null;
15254
- return JSON.stringify(value);
15439
+ return sanitizeJsonForPg(JSON.stringify(value));
15255
15440
  }
15256
15441
  /**
15257
15442
  * Build a multi-row INSERT with explicit column types and ON CONFLICT DO NOTHING.
@@ -15262,7 +15447,7 @@ function encodeJsonb(value) {
15262
15447
  * All records must have identical key sets.
15263
15448
  * @returns { text, values } ready to pass to `client.query`.
15264
15449
  */
15265
- function buildInsert(schema, table, records) {
15450
+ function buildInsert(schema, table, records, onConflict = "ON CONFLICT DO NOTHING") {
15266
15451
  if (records.length === 0) return null;
15267
15452
  const columns = Object.keys(records[0]).map((c) => parseSqlIdentifier(c, "column name"));
15268
15453
  const quotedColumns = columns.map((c) => `"${c}"`).join(", ");
@@ -15288,10 +15473,44 @@ function buildInsert(schema, table, records) {
15288
15473
  return {
15289
15474
  text: `INSERT INTO ${qualifiedTable(schema, table)} (${quotedColumns})
15290
15475
  VALUES ${rowPlaceholders.join(", ")}
15291
- ON CONFLICT DO NOTHING`,
15476
+ ${onConflict}`,
15292
15477
  values
15293
15478
  };
15294
15479
  }
15480
+ /** Primary key of the span table; also the conflict target for span upserts. */
15481
+ const SPAN_KEY_COLUMNS = [
15482
+ "traceId",
15483
+ "spanId",
15484
+ "endedAt"
15485
+ ];
15486
+ /**
15487
+ * Conflict clause for span writes.
15488
+ *
15489
+ * A span is written twice under the event-sourced strategy: once when it starts
15490
+ * (`isPending = true`, `endedAt` synthesized from `startedAt`) and once when it
15491
+ * ends. Those two rows normally differ in `endedAt` and so coexist under the
15492
+ * primary key. A zero-duration span is the exception — its real `endedAt`
15493
+ * equals its `startedAt`, so the end row collides with the pending row. Letting
15494
+ * `DO NOTHING` win there would strand the span as permanently running, so the
15495
+ * end row overwrites the pending one instead.
15496
+ *
15497
+ * The `WHERE` guard keeps the clause a no-op for every other conflict, which
15498
+ * preserves insert-only retry idempotency: a replayed end row cannot clobber
15499
+ * the row it duplicates, and a late-arriving start row cannot revert an ended
15500
+ * span to pending.
15501
+ */
15502
+ function spanConflictClause(columns) {
15503
+ return `ON CONFLICT ("traceId", "spanId", "endedAt") DO UPDATE SET ${columns.filter((column) => !SPAN_KEY_COLUMNS.includes(column)).map((column) => `"${column}" = EXCLUDED."${column}"`).join(", ")}
15504
+ WHERE ${TABLE_SPAN_EVENTS}."isPending" AND NOT EXCLUDED."isPending"`;
15505
+ }
15506
+ /**
15507
+ * Ordering that picks the winning row for a span under the event-sourced write
15508
+ * model. An ended row always beats a pending row for the same span; among rows
15509
+ * of the same kind the latest write wins, with `cursorId` breaking ties between
15510
+ * rows that share a timestamp. Combine with `DISTINCT ON` (or `LIMIT 1`) after
15511
+ * the grouping key to collapse a span's rows down to one.
15512
+ */
15513
+ const SPAN_COLLAPSE_ORDER = "\"isPending\" ASC, \"endedAt\" DESC, \"cursorId\" DESC";
15295
15514
  /**
15296
15515
  * Standard SELECT column list for tracing tables. The select projects every
15297
15516
  * column the row→record converters expect.
@@ -16287,12 +16506,51 @@ async function getScorePercentiles(client, schema, args) {
16287
16506
  function asIsoTimestamp(value) {
16288
16507
  return value instanceof Date ? value.toISOString() : new Date(value).toISOString();
16289
16508
  }
16509
+ /**
16510
+ * Ordering that picks the row representing a trace's current root.
16511
+ *
16512
+ * A trace can hold more than one root row. A span is written once when it
16513
+ * starts and again when it ends, and a durable run that suspends ends its root
16514
+ * span and opens a fresh one on resume. In both cases the newest row is the one
16515
+ * that describes the trace right now, and `cursorId` — a sequence assigned at
16516
+ * insert — is what "newest" means here. It also orders correctly where
16517
+ * timestamps cannot: a zero-duration span has `startedAt = endedAt`, and a
16518
+ * resumed root starts after the suspended root ended.
16519
+ */
16520
+ const ROOT_COLLAPSE_ORDER = "\"cursorId\" DESC";
16521
+ /**
16522
+ * Predicate that keeps only the winning row for each span — the ended row when
16523
+ * one exists, the newest row otherwise. Mirrors `SPAN_COLLAPSE_ORDER` as an
16524
+ * anti-join, for the same reason as {@link latestRootPredicate}.
16525
+ */
16526
+ function latestSpanPredicate(spanTable) {
16527
+ return `NOT EXISTS (
16528
+ SELECT 1 FROM ${spanTable} newer
16529
+ WHERE newer."traceId" = r."traceId"
16530
+ AND newer."spanId" = r."spanId"
16531
+ AND (newer."isPending" < r."isPending" OR (newer."isPending" = r."isPending" AND newer."cursorId" > r."cursorId"))
16532
+ )`;
16533
+ }
16534
+ /**
16535
+ * Predicate that keeps only a trace's current root row, as an anti-join rather
16536
+ * than a `DISTINCT ON`. Written this way so the outer query still scans the
16537
+ * partial root indexes and streams rows in `LIMIT`-order; grouping first would
16538
+ * force every root in the table to be read before the first page came back.
16539
+ */
16540
+ function latestRootPredicate(spanTable) {
16541
+ return `NOT EXISTS (
16542
+ SELECT 1 FROM ${spanTable} newer
16543
+ WHERE newer."traceId" = r."traceId"
16544
+ AND newer."parentSpanId" IS NULL
16545
+ AND newer."cursorId" > r."cursorId"
16546
+ )`;
16547
+ }
16290
16548
  async function getRootSpan(client, schema, args) {
16291
16549
  const table = qualifiedTable(schema, TABLE_SPAN_EVENTS);
16292
16550
  const row = await client.oneOrNone(`SELECT ${SPAN_SELECT_COLUMNS}
16293
16551
  FROM ${table}
16294
16552
  WHERE "traceId" = $1 AND "parentSpanId" IS NULL
16295
- ORDER BY "endedAt" DESC
16553
+ ORDER BY ${ROOT_COLLAPSE_ORDER}
16296
16554
  LIMIT 1`, [args.traceId]);
16297
16555
  if (!row) return null;
16298
16556
  return { span: rowToSpanRecord(row) };
@@ -16304,7 +16562,7 @@ async function getRootSpan(client, schema, args) {
16304
16562
  * planner. Starts numbering from `nextParamIdx`.
16305
16563
  */
16306
16564
  function buildListTracesFilters(filters, spanTable, nextParamIdx) {
16307
- const conditions = [`r."parentSpanId" IS NULL`];
16565
+ const conditions = [`r."parentSpanId" IS NULL`, latestRootPredicate(spanTable)];
16308
16566
  const params = [];
16309
16567
  let i = nextParamIdx;
16310
16568
  if (!filters) return {
@@ -16321,11 +16579,11 @@ function buildListTracesFilters(filters, spanTable, nextParamIdx) {
16321
16579
  params.push(asIsoTimestamp(filters.startedAt.end));
16322
16580
  }
16323
16581
  if (filters.endedAt?.start) {
16324
- conditions.push(`r."endedAt" ${filters.endedAt.startExclusive ? ">" : ">="} $${i++}`);
16582
+ conditions.push(`(NOT r."isPending" AND r."endedAt" ${filters.endedAt.startExclusive ? ">" : ">="} $${i++})`);
16325
16583
  params.push(asIsoTimestamp(filters.endedAt.start));
16326
16584
  }
16327
16585
  if (filters.endedAt?.end) {
16328
- conditions.push(`r."endedAt" ${filters.endedAt.endExclusive ? "<" : "<="} $${i++}`);
16586
+ conditions.push(`(NOT r."isPending" AND r."endedAt" ${filters.endedAt.endExclusive ? "<" : "<="} $${i++})`);
16329
16587
  params.push(asIsoTimestamp(filters.endedAt.end));
16330
16588
  }
16331
16589
  if (filters.spanType !== void 0) {
@@ -16397,10 +16655,10 @@ function buildListTracesFilters(filters, spanTable, nextParamIdx) {
16397
16655
  conditions.push(`r."error" IS NOT NULL`);
16398
16656
  break;
16399
16657
  case TraceStatus.RUNNING:
16400
- conditions.push(`FALSE`);
16658
+ conditions.push(`r."isPending"`);
16401
16659
  break;
16402
16660
  case TraceStatus.SUCCESS:
16403
- conditions.push(`r."error" IS NULL`);
16661
+ conditions.push(`r."error" IS NULL AND NOT r."isPending"`);
16404
16662
  break;
16405
16663
  }
16406
16664
  if (filters.hasChildError !== void 0) {
@@ -16537,8 +16795,8 @@ function buildBranchSpanTypeClause(userSpanType, params, startIdx) {
16537
16795
  * NOT have a `parentSpanId IS NULL` predicate — branches can be nested under
16538
16796
  * other spans. Returns the bound params and the next param index.
16539
16797
  */
16540
- function buildListBranchesFilters(filters, spanType, nextParamIdx) {
16541
- const conditions = [];
16798
+ function buildListBranchesFilters(filters, spanType, spanTable, nextParamIdx) {
16799
+ const conditions = [latestSpanPredicate(spanTable)];
16542
16800
  const params = [];
16543
16801
  let i = nextParamIdx;
16544
16802
  const spanTypeClause = buildBranchSpanTypeClause(spanType, params, i);
@@ -16559,11 +16817,11 @@ function buildListBranchesFilters(filters, spanType, nextParamIdx) {
16559
16817
  params.push(asIsoTimestamp(filters.startedAt.end));
16560
16818
  }
16561
16819
  if (filters.endedAt?.start) {
16562
- conditions.push(`r."endedAt" ${filters.endedAt.startExclusive ? ">" : ">="} $${i++}`);
16820
+ conditions.push(`(NOT r."isPending" AND r."endedAt" ${filters.endedAt.startExclusive ? ">" : ">="} $${i++})`);
16563
16821
  params.push(asIsoTimestamp(filters.endedAt.start));
16564
16822
  }
16565
16823
  if (filters.endedAt?.end) {
16566
- conditions.push(`r."endedAt" ${filters.endedAt.endExclusive ? "<" : "<="} $${i++}`);
16824
+ conditions.push(`(NOT r."isPending" AND r."endedAt" ${filters.endedAt.endExclusive ? "<" : "<="} $${i++})`);
16567
16825
  params.push(asIsoTimestamp(filters.endedAt.end));
16568
16826
  }
16569
16827
  if (filters.traceId !== void 0) {
@@ -16675,10 +16933,10 @@ function buildListBranchesFilters(filters, spanType, nextParamIdx) {
16675
16933
  conditions.push(`r."error" IS NOT NULL`);
16676
16934
  break;
16677
16935
  case TraceStatus.RUNNING:
16678
- conditions.push(`FALSE`);
16936
+ conditions.push(`r."isPending"`);
16679
16937
  break;
16680
16938
  case TraceStatus.SUCCESS:
16681
- conditions.push(`r."error" IS NULL`);
16939
+ conditions.push(`r."error" IS NULL AND NOT r."isPending"`);
16682
16940
  break;
16683
16941
  }
16684
16942
  return {
@@ -16697,7 +16955,7 @@ async function listBranches(client, schema, args) {
16697
16955
  return listBranchesPage(client, span, filters, pagination.page, pagination.perPage, orderBy.field, orderBy.direction);
16698
16956
  }
16699
16957
  async function listBranchesPage(client, span, filters, page, perPage, orderField, orderDir) {
16700
- const built = buildListBranchesFilters(filters, filters?.spanType, 1);
16958
+ const built = buildListBranchesFilters(filters, filters?.spanType, span, 1);
16701
16959
  if (!built) {
16702
16960
  const deltaCursor = deltaPollingFeatureEnabled() ? await readBranchesStreamHeadCursor(client, span, filters) : void 0;
16703
16961
  return {
@@ -16751,7 +17009,7 @@ async function listBranchesDelta(client, span, filters, after, limit) {
16751
17009
  deltaCursor
16752
17010
  };
16753
17011
  }
16754
- const built = buildListBranchesFilters(filters, filters?.spanType, 1);
17012
+ const built = buildListBranchesFilters(filters, filters?.spanType, span, 1);
16755
17013
  if (!built) return {
16756
17014
  branches: [],
16757
17015
  delta: {
@@ -16794,14 +17052,35 @@ async function readBranchesStreamHeadCursor(client, span, filters) {
16794
17052
  }
16795
17053
  //#endregion
16796
17054
  //#region src/storage/domains/observability/v-next/tracing.ts
16797
- async function createSpan(client, schema, args) {
16798
- const insert = buildInsert(schema, TABLE_SPAN_EVENTS, [spanRecordToRow(args.span)]);
17055
+ async function writeSpanRows(client, schema, rows) {
17056
+ const deduped = dedupeSpanRows(rows);
17057
+ const insert = buildInsert(schema, TABLE_SPAN_EVENTS, deduped, spanConflictClause(Object.keys(deduped[0] ?? {})));
16799
17058
  if (insert) await client.query(insert.text, insert.values);
16800
17059
  }
17060
+ /**
17061
+ * Collapse rows that share a primary key within a single batch. Postgres
17062
+ * rejects an `ON CONFLICT DO UPDATE` statement whose VALUES list would touch
17063
+ * the same row twice, which a batch containing both the start and the end of a
17064
+ * zero-duration span would do. The ended row wins, matching what the conflict
17065
+ * clause would have done across two separate statements.
17066
+ */
17067
+ function dedupeSpanRows(rows) {
17068
+ if (rows.length < 2) return rows;
17069
+ const byKey = /* @__PURE__ */ new Map();
17070
+ for (const row of rows) {
17071
+ const key = `${row.traceId}\u0000${row.spanId}\u0000${String(row.endedAt)}`;
17072
+ const existing = byKey.get(key);
17073
+ if (existing && !existing.isPending) continue;
17074
+ byKey.set(key, row);
17075
+ }
17076
+ return [...byKey.values()];
17077
+ }
17078
+ async function createSpan(client, schema, args) {
17079
+ await writeSpanRows(client, schema, [spanRecordToRow(args.span)]);
17080
+ }
16801
17081
  async function batchCreateSpans(client, schema, args) {
16802
17082
  if (args.records.length === 0) return;
16803
- const insert = buildInsert(schema, TABLE_SPAN_EVENTS, args.records.map(spanRecordToRow));
16804
- if (insert) await client.query(insert.text, insert.values);
17083
+ await writeSpanRows(client, schema, args.records.map(spanRecordToRow));
16805
17084
  }
16806
17085
  async function getSpans(client, schema, args) {
16807
17086
  if (args.spanIds.length === 0) return {
@@ -16809,10 +17088,13 @@ async function getSpans(client, schema, args) {
16809
17088
  spans: []
16810
17089
  };
16811
17090
  const table = qualifiedTable(schema, TABLE_SPAN_EVENTS);
16812
- const rows = await client.manyOrNone(`SELECT ${SPAN_SELECT_COLUMNS}
16813
- FROM ${table}
16814
- WHERE "traceId" = $1
16815
- AND "spanId" = ANY($2::text[])
17091
+ const rows = await client.manyOrNone(`SELECT * FROM (
17092
+ SELECT DISTINCT ON ("spanId") ${SPAN_SELECT_COLUMNS}
17093
+ FROM ${table}
17094
+ WHERE "traceId" = $1
17095
+ AND "spanId" = ANY($2::text[])
17096
+ ORDER BY "spanId", ${SPAN_COLLAPSE_ORDER}
17097
+ ) collapsed
16816
17098
  ORDER BY "startedAt" ASC`, [args.traceId, args.spanIds]);
16817
17099
  return {
16818
17100
  traceId: args.traceId,
@@ -16824,16 +17106,19 @@ async function getSpan(client, schema, args) {
16824
17106
  const row = await client.oneOrNone(`SELECT ${SPAN_SELECT_COLUMNS}
16825
17107
  FROM ${table}
16826
17108
  WHERE "traceId" = $1 AND "spanId" = $2
16827
- ORDER BY "endedAt" DESC
17109
+ ORDER BY ${SPAN_COLLAPSE_ORDER}
16828
17110
  LIMIT 1`, [args.traceId, args.spanId]);
16829
17111
  if (!row) return null;
16830
17112
  return { span: rowToSpanRecord(row) };
16831
17113
  }
16832
17114
  async function getTrace(client, schema, args) {
16833
17115
  const table = qualifiedTable(schema, TABLE_SPAN_EVENTS);
16834
- const rows = await client.manyOrNone(`SELECT ${SPAN_SELECT_COLUMNS}
16835
- FROM ${table}
16836
- WHERE "traceId" = $1
17116
+ const rows = await client.manyOrNone(`SELECT * FROM (
17117
+ SELECT DISTINCT ON ("spanId") ${SPAN_SELECT_COLUMNS}
17118
+ FROM ${table}
17119
+ WHERE "traceId" = $1
17120
+ ORDER BY "spanId", ${SPAN_COLLAPSE_ORDER}
17121
+ ) collapsed
16837
17122
  ORDER BY "startedAt" ASC`, [args.traceId]);
16838
17123
  if (!rows.length) return null;
16839
17124
  return {
@@ -16843,9 +17128,12 @@ async function getTrace(client, schema, args) {
16843
17128
  }
16844
17129
  async function getTraceLight(client, schema, args) {
16845
17130
  const table = qualifiedTable(schema, TABLE_SPAN_EVENTS);
16846
- const rows = await client.manyOrNone(`SELECT ${SPAN_LIGHT_SELECT_COLUMNS}
16847
- FROM ${table}
16848
- WHERE "traceId" = $1
17131
+ const rows = await client.manyOrNone(`SELECT * FROM (
17132
+ SELECT DISTINCT ON ("spanId") ${SPAN_LIGHT_SELECT_COLUMNS}
17133
+ FROM ${table}
17134
+ WHERE "traceId" = $1
17135
+ ORDER BY "spanId", ${SPAN_COLLAPSE_ORDER}
17136
+ ) collapsed
16849
17137
  ORDER BY "startedAt" ASC`, [args.traceId]);
16850
17138
  if (!rows.length) return null;
16851
17139
  return {
@@ -16956,6 +17244,11 @@ var ObservabilityStoragePostgresVNext = class ObservabilityStoragePostgresVNext
16956
17244
  } catch (error) {
16957
17245
  if (!isDuplicateRelationError(error)) throw error;
16958
17246
  }
17247
+ for (const { table, column, ddl } of additiveColumns(this.#schema)) if (!await this.#client.oneOrNone(columnExistsSQL, [
17248
+ this.#schema,
17249
+ table,
17250
+ column
17251
+ ])) await this.#client.none(ddl);
16959
17252
  for (const ddl of allIndexDDL(this.#schema)) try {
16960
17253
  await this.#client.none(ddl);
16961
17254
  } catch (error) {
@@ -17052,8 +17345,8 @@ var ObservabilityStoragePostgresVNext = class ObservabilityStoragePostgresVNext
17052
17345
  }
17053
17346
  get observabilityStrategy() {
17054
17347
  return {
17055
- preferred: "insert-only",
17056
- supported: ["insert-only"]
17348
+ preferred: "event-sourced",
17349
+ supported: ["event-sourced"]
17057
17350
  };
17058
17351
  }
17059
17352
  getFeatures() {
@@ -20477,18 +20770,6 @@ function workflowSnapshotStatusIndexSQL(indexName, schemaName) {
20477
20770
  schemaName: getSchemaName(schemaName)
20478
20771
  })} (workflow_name, (snapshot ->> 'status'), "createdAt" DESC)`;
20479
20772
  }
20480
- /**
20481
- * Sanitizes JSON string for PostgreSQL jsonb:
20482
- * - Removes problematic Unicode sequences:
20483
- * - \u0000 (null character) - causes error 22P05 "unsupported Unicode escape sequence"
20484
- * - \uD800-\uDFFF (unpaired surrogates) - causes "Unicode low surrogate must follow a high surrogate"
20485
- * - \\uD800 (escaped-backslash + surrogate, e.g. from JS regex literals like [^\ud800-\udfff]):
20486
- * removing just \uXXXX would leave a dangling backslash that creates a new invalid escape (e.g. \-)
20487
- * - Escapes any remaining invalid JSON escape sequences (e.g. \v, \k, \-)
20488
- */
20489
- function sanitizeJsonForPg(jsonString) {
20490
- return jsonString.replace(/\\\\?u(0000|[Dd][89A-Fa-f][0-9A-Fa-f]{2})/g, "").replace(/(^|[^\\])(\\(?!["\\/bfnrtu]))/g, "$1\\\\");
20491
- }
20492
20773
  var WorkflowsPG = class WorkflowsPG extends WorkflowsStorage {
20493
20774
  #db;
20494
20775
  #schema;
@@ -20768,11 +21049,11 @@ var WorkflowsPG = class WorkflowsPG extends WorkflowsStorage {
20768
21049
  await this.#db.client.none(`INSERT INTO ${getTableName({
20769
21050
  indexName: TABLE_WORKFLOW_SNAPSHOT,
20770
21051
  schemaName: getSchemaName(this.#schema)
20771
- })}
21052
+ })} AS t
20772
21053
  (workflow_name, run_id, "resourceId", snapshot, "createdAt", "updatedAt", "createdAtZ", "updatedAtZ")
20773
21054
  VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
20774
21055
  ON CONFLICT (workflow_name, run_id) DO UPDATE
20775
- SET "resourceId" = $3, snapshot = $4, "updatedAt" = $6, "updatedAtZ" = $8`, [
21056
+ SET "resourceId" = COALESCE($3, t."resourceId"), snapshot = $4, "updatedAt" = $6, "updatedAtZ" = $8`, [
20776
21057
  workflowName,
20777
21058
  runId,
20778
21059
  resourceId,
@@ -21730,6 +22011,12 @@ var PgFactoryStorageOps = class {
21730
22011
  async findMany(collection, where, opts) {
21731
22012
  return this.#select(this.#queryable, collection, where, opts);
21732
22013
  }
22014
+ async count(collection, where) {
22015
+ const schema = this.#schema(collection);
22016
+ const filter = this.#buildWhere(schema, where);
22017
+ const result = await this.#queryable.query(`SELECT COUNT(*) AS count FROM "${schema.name}" WHERE ${filter.sql}`, filter.args);
22018
+ return Number(result.rows[0]?.count ?? 0);
22019
+ }
21733
22020
  async insertOne(collection, row) {
21734
22021
  const schema = this.#schema(collection);
21735
22022
  const pk = primaryKeyOf(schema);