@mastra/pg 1.21.1 → 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.
- package/CHANGELOG.md +108 -0
- package/dist/docs/SKILL.md +1 -1
- package/dist/docs/assets/SOURCE_MAP.json +1 -1
- package/dist/docs/references/docs-deployment-workers.md +2 -2
- package/dist/docs/references/docs-storage.md +1 -0
- package/dist/docs/references/integrations-databases-postgresql.md +2 -0
- package/dist/docs/references/reference-rag-vector-databases.md +4 -4
- package/dist/docs/references/reference-vectors-pg.md +2 -0
- package/dist/index.cjs +398 -107
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +398 -107
- package/dist/index.js.map +1 -1
- package/dist/storage/db/sanitize-json.d.ts +11 -0
- package/dist/storage/db/sanitize-json.d.ts.map +1 -0
- package/dist/storage/domains/observability/v-next/ddl.d.ts +17 -0
- package/dist/storage/domains/observability/v-next/ddl.d.ts.map +1 -1
- package/dist/storage/domains/observability/v-next/helpers.d.ts.map +1 -1
- package/dist/storage/domains/observability/v-next/index.d.ts.map +1 -1
- package/dist/storage/domains/observability/v-next/scores.d.ts.map +1 -1
- package/dist/storage/domains/observability/v-next/signal-schema.d.ts +5 -1
- package/dist/storage/domains/observability/v-next/signal-schema.d.ts.map +1 -1
- package/dist/storage/domains/observability/v-next/sql.d.ts +29 -4
- package/dist/storage/domains/observability/v-next/sql.d.ts.map +1 -1
- package/dist/storage/domains/observability/v-next/traces.d.ts.map +1 -1
- package/dist/storage/domains/observability/v-next/tracing.d.ts +4 -3
- package/dist/storage/domains/observability/v-next/tracing.d.ts.map +1 -1
- package/dist/storage/domains/workflows/index.d.ts +2 -10
- package/dist/storage/domains/workflows/index.d.ts.map +1 -1
- package/dist/storage/factory-storage.d.ts.map +1 -1
- package/dist/vector/index.d.ts +60 -5
- package/dist/vector/index.d.ts.map +1 -1
- 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
|
|
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.
|
|
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
|
-
|
|
812
|
-
|
|
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
|
-
|
|
910
|
+
WHERE namespace = $${namespaceParam}
|
|
911
|
+
${filterClause ? `AND (${filterClause})` : ""}
|
|
842
912
|
ORDER BY vector_id
|
|
843
|
-
LIMIT $${
|
|
844
|
-
return (await client.query(query, [
|
|
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.
|
|
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
|
|
883
|
-
const
|
|
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
|
-
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
15268
|
-
*
|
|
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
|
-
|
|
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.
|
|
@@ -16112,6 +16331,10 @@ function applyScoreFilters(acc, filters) {
|
|
|
16112
16331
|
acc.conditions.push(`"scoreSource" = $${acc.next++}`);
|
|
16113
16332
|
acc.params.push(filters.scoreSource ?? filters.source);
|
|
16114
16333
|
}
|
|
16334
|
+
if (filters?.metadata && Object.keys(filters.metadata).length > 0) for (const [key, value] of Object.entries(filters.metadata)) {
|
|
16335
|
+
acc.conditions.push(`"metadata"->($${acc.next++}::text) = $${acc.next++}::jsonb`);
|
|
16336
|
+
acc.params.push(key, JSON.stringify(value ?? null));
|
|
16337
|
+
}
|
|
16115
16338
|
}
|
|
16116
16339
|
/** OLAP queries take an explicit scorerId / scoreSource pair as identity. */
|
|
16117
16340
|
function pushScoreIdentity(acc, scorerId, scoreSource) {
|
|
@@ -16307,12 +16530,51 @@ async function getScorePercentiles(client, schema, args) {
|
|
|
16307
16530
|
function asIsoTimestamp(value) {
|
|
16308
16531
|
return value instanceof Date ? value.toISOString() : new Date(value).toISOString();
|
|
16309
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
|
+
}
|
|
16310
16572
|
async function getRootSpan(client, schema, args) {
|
|
16311
16573
|
const table = qualifiedTable(schema, TABLE_SPAN_EVENTS);
|
|
16312
16574
|
const row = await client.oneOrNone(`SELECT ${SPAN_SELECT_COLUMNS}
|
|
16313
16575
|
FROM ${table}
|
|
16314
16576
|
WHERE "traceId" = $1 AND "parentSpanId" IS NULL
|
|
16315
|
-
ORDER BY
|
|
16577
|
+
ORDER BY ${ROOT_COLLAPSE_ORDER}
|
|
16316
16578
|
LIMIT 1`, [args.traceId]);
|
|
16317
16579
|
if (!row) return null;
|
|
16318
16580
|
return { span: rowToSpanRecord(row) };
|
|
@@ -16324,7 +16586,7 @@ async function getRootSpan(client, schema, args) {
|
|
|
16324
16586
|
* planner. Starts numbering from `nextParamIdx`.
|
|
16325
16587
|
*/
|
|
16326
16588
|
function buildListTracesFilters(filters, spanTable, nextParamIdx) {
|
|
16327
|
-
const conditions = [`r."parentSpanId" IS NULL
|
|
16589
|
+
const conditions = [`r."parentSpanId" IS NULL`, latestRootPredicate(spanTable)];
|
|
16328
16590
|
const params = [];
|
|
16329
16591
|
let i = nextParamIdx;
|
|
16330
16592
|
if (!filters) return {
|
|
@@ -16341,11 +16603,11 @@ function buildListTracesFilters(filters, spanTable, nextParamIdx) {
|
|
|
16341
16603
|
params.push(asIsoTimestamp(filters.startedAt.end));
|
|
16342
16604
|
}
|
|
16343
16605
|
if (filters.endedAt?.start) {
|
|
16344
|
-
conditions.push(`r."endedAt" ${filters.endedAt.startExclusive ? ">" : ">="} $${i++}`);
|
|
16606
|
+
conditions.push(`(NOT r."isPending" AND r."endedAt" ${filters.endedAt.startExclusive ? ">" : ">="} $${i++})`);
|
|
16345
16607
|
params.push(asIsoTimestamp(filters.endedAt.start));
|
|
16346
16608
|
}
|
|
16347
16609
|
if (filters.endedAt?.end) {
|
|
16348
|
-
conditions.push(`r."endedAt" ${filters.endedAt.endExclusive ? "<" : "<="} $${i++}`);
|
|
16610
|
+
conditions.push(`(NOT r."isPending" AND r."endedAt" ${filters.endedAt.endExclusive ? "<" : "<="} $${i++})`);
|
|
16349
16611
|
params.push(asIsoTimestamp(filters.endedAt.end));
|
|
16350
16612
|
}
|
|
16351
16613
|
if (filters.spanType !== void 0) {
|
|
@@ -16417,10 +16679,10 @@ function buildListTracesFilters(filters, spanTable, nextParamIdx) {
|
|
|
16417
16679
|
conditions.push(`r."error" IS NOT NULL`);
|
|
16418
16680
|
break;
|
|
16419
16681
|
case _mastra_core_storage.TraceStatus.RUNNING:
|
|
16420
|
-
conditions.push(`
|
|
16682
|
+
conditions.push(`r."isPending"`);
|
|
16421
16683
|
break;
|
|
16422
16684
|
case _mastra_core_storage.TraceStatus.SUCCESS:
|
|
16423
|
-
conditions.push(`r."error" IS NULL`);
|
|
16685
|
+
conditions.push(`r."error" IS NULL AND NOT r."isPending"`);
|
|
16424
16686
|
break;
|
|
16425
16687
|
}
|
|
16426
16688
|
if (filters.hasChildError !== void 0) {
|
|
@@ -16557,8 +16819,8 @@ function buildBranchSpanTypeClause(userSpanType, params, startIdx) {
|
|
|
16557
16819
|
* NOT have a `parentSpanId IS NULL` predicate — branches can be nested under
|
|
16558
16820
|
* other spans. Returns the bound params and the next param index.
|
|
16559
16821
|
*/
|
|
16560
|
-
function buildListBranchesFilters(filters, spanType, nextParamIdx) {
|
|
16561
|
-
const conditions = [];
|
|
16822
|
+
function buildListBranchesFilters(filters, spanType, spanTable, nextParamIdx) {
|
|
16823
|
+
const conditions = [latestSpanPredicate(spanTable)];
|
|
16562
16824
|
const params = [];
|
|
16563
16825
|
let i = nextParamIdx;
|
|
16564
16826
|
const spanTypeClause = buildBranchSpanTypeClause(spanType, params, i);
|
|
@@ -16579,11 +16841,11 @@ function buildListBranchesFilters(filters, spanType, nextParamIdx) {
|
|
|
16579
16841
|
params.push(asIsoTimestamp(filters.startedAt.end));
|
|
16580
16842
|
}
|
|
16581
16843
|
if (filters.endedAt?.start) {
|
|
16582
|
-
conditions.push(`r."endedAt" ${filters.endedAt.startExclusive ? ">" : ">="} $${i++}`);
|
|
16844
|
+
conditions.push(`(NOT r."isPending" AND r."endedAt" ${filters.endedAt.startExclusive ? ">" : ">="} $${i++})`);
|
|
16583
16845
|
params.push(asIsoTimestamp(filters.endedAt.start));
|
|
16584
16846
|
}
|
|
16585
16847
|
if (filters.endedAt?.end) {
|
|
16586
|
-
conditions.push(`r."endedAt" ${filters.endedAt.endExclusive ? "<" : "<="} $${i++}`);
|
|
16848
|
+
conditions.push(`(NOT r."isPending" AND r."endedAt" ${filters.endedAt.endExclusive ? "<" : "<="} $${i++})`);
|
|
16587
16849
|
params.push(asIsoTimestamp(filters.endedAt.end));
|
|
16588
16850
|
}
|
|
16589
16851
|
if (filters.traceId !== void 0) {
|
|
@@ -16695,10 +16957,10 @@ function buildListBranchesFilters(filters, spanType, nextParamIdx) {
|
|
|
16695
16957
|
conditions.push(`r."error" IS NOT NULL`);
|
|
16696
16958
|
break;
|
|
16697
16959
|
case _mastra_core_storage.TraceStatus.RUNNING:
|
|
16698
|
-
conditions.push(`
|
|
16960
|
+
conditions.push(`r."isPending"`);
|
|
16699
16961
|
break;
|
|
16700
16962
|
case _mastra_core_storage.TraceStatus.SUCCESS:
|
|
16701
|
-
conditions.push(`r."error" IS NULL`);
|
|
16963
|
+
conditions.push(`r."error" IS NULL AND NOT r."isPending"`);
|
|
16702
16964
|
break;
|
|
16703
16965
|
}
|
|
16704
16966
|
return {
|
|
@@ -16717,7 +16979,7 @@ async function listBranches(client, schema, args) {
|
|
|
16717
16979
|
return listBranchesPage(client, span, filters, pagination.page, pagination.perPage, orderBy.field, orderBy.direction);
|
|
16718
16980
|
}
|
|
16719
16981
|
async function listBranchesPage(client, span, filters, page, perPage, orderField, orderDir) {
|
|
16720
|
-
const built = buildListBranchesFilters(filters, filters?.spanType, 1);
|
|
16982
|
+
const built = buildListBranchesFilters(filters, filters?.spanType, span, 1);
|
|
16721
16983
|
if (!built) {
|
|
16722
16984
|
const deltaCursor = deltaPollingFeatureEnabled() ? await readBranchesStreamHeadCursor(client, span, filters) : void 0;
|
|
16723
16985
|
return {
|
|
@@ -16771,7 +17033,7 @@ async function listBranchesDelta(client, span, filters, after, limit) {
|
|
|
16771
17033
|
deltaCursor
|
|
16772
17034
|
};
|
|
16773
17035
|
}
|
|
16774
|
-
const built = buildListBranchesFilters(filters, filters?.spanType, 1);
|
|
17036
|
+
const built = buildListBranchesFilters(filters, filters?.spanType, span, 1);
|
|
16775
17037
|
if (!built) return {
|
|
16776
17038
|
branches: [],
|
|
16777
17039
|
delta: {
|
|
@@ -16814,14 +17076,35 @@ async function readBranchesStreamHeadCursor(client, span, filters) {
|
|
|
16814
17076
|
}
|
|
16815
17077
|
//#endregion
|
|
16816
17078
|
//#region src/storage/domains/observability/v-next/tracing.ts
|
|
16817
|
-
async function
|
|
16818
|
-
const
|
|
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] ?? {})));
|
|
16819
17082
|
if (insert) await client.query(insert.text, insert.values);
|
|
16820
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
|
+
}
|
|
16821
17105
|
async function batchCreateSpans(client, schema, args) {
|
|
16822
17106
|
if (args.records.length === 0) return;
|
|
16823
|
-
|
|
16824
|
-
if (insert) await client.query(insert.text, insert.values);
|
|
17107
|
+
await writeSpanRows(client, schema, args.records.map(spanRecordToRow));
|
|
16825
17108
|
}
|
|
16826
17109
|
async function getSpans(client, schema, args) {
|
|
16827
17110
|
if (args.spanIds.length === 0) return {
|
|
@@ -16829,10 +17112,13 @@ async function getSpans(client, schema, args) {
|
|
|
16829
17112
|
spans: []
|
|
16830
17113
|
};
|
|
16831
17114
|
const table = qualifiedTable(schema, TABLE_SPAN_EVENTS);
|
|
16832
|
-
const rows = await client.manyOrNone(`SELECT
|
|
16833
|
-
|
|
16834
|
-
|
|
16835
|
-
|
|
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
|
|
16836
17122
|
ORDER BY "startedAt" ASC`, [args.traceId, args.spanIds]);
|
|
16837
17123
|
return {
|
|
16838
17124
|
traceId: args.traceId,
|
|
@@ -16844,16 +17130,19 @@ async function getSpan(client, schema, args) {
|
|
|
16844
17130
|
const row = await client.oneOrNone(`SELECT ${SPAN_SELECT_COLUMNS}
|
|
16845
17131
|
FROM ${table}
|
|
16846
17132
|
WHERE "traceId" = $1 AND "spanId" = $2
|
|
16847
|
-
ORDER BY
|
|
17133
|
+
ORDER BY ${SPAN_COLLAPSE_ORDER}
|
|
16848
17134
|
LIMIT 1`, [args.traceId, args.spanId]);
|
|
16849
17135
|
if (!row) return null;
|
|
16850
17136
|
return { span: rowToSpanRecord(row) };
|
|
16851
17137
|
}
|
|
16852
17138
|
async function getTrace(client, schema, args) {
|
|
16853
17139
|
const table = qualifiedTable(schema, TABLE_SPAN_EVENTS);
|
|
16854
|
-
const rows = await client.manyOrNone(`SELECT
|
|
16855
|
-
|
|
16856
|
-
|
|
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
|
|
16857
17146
|
ORDER BY "startedAt" ASC`, [args.traceId]);
|
|
16858
17147
|
if (!rows.length) return null;
|
|
16859
17148
|
return {
|
|
@@ -16863,9 +17152,12 @@ async function getTrace(client, schema, args) {
|
|
|
16863
17152
|
}
|
|
16864
17153
|
async function getTraceLight(client, schema, args) {
|
|
16865
17154
|
const table = qualifiedTable(schema, TABLE_SPAN_EVENTS);
|
|
16866
|
-
const rows = await client.manyOrNone(`SELECT
|
|
16867
|
-
|
|
16868
|
-
|
|
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
|
|
16869
17161
|
ORDER BY "startedAt" ASC`, [args.traceId]);
|
|
16870
17162
|
if (!rows.length) return null;
|
|
16871
17163
|
return {
|
|
@@ -16976,6 +17268,11 @@ var ObservabilityStoragePostgresVNext = class ObservabilityStoragePostgresVNext
|
|
|
16976
17268
|
} catch (error) {
|
|
16977
17269
|
if (!isDuplicateRelationError(error)) throw error;
|
|
16978
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);
|
|
16979
17276
|
for (const ddl of allIndexDDL(this.#schema)) try {
|
|
16980
17277
|
await this.#client.none(ddl);
|
|
16981
17278
|
} catch (error) {
|
|
@@ -17072,8 +17369,8 @@ var ObservabilityStoragePostgresVNext = class ObservabilityStoragePostgresVNext
|
|
|
17072
17369
|
}
|
|
17073
17370
|
get observabilityStrategy() {
|
|
17074
17371
|
return {
|
|
17075
|
-
preferred: "
|
|
17076
|
-
supported: ["
|
|
17372
|
+
preferred: "event-sourced",
|
|
17373
|
+
supported: ["event-sourced"]
|
|
17077
17374
|
};
|
|
17078
17375
|
}
|
|
17079
17376
|
getFeatures() {
|
|
@@ -20497,18 +20794,6 @@ function workflowSnapshotStatusIndexSQL(indexName, schemaName) {
|
|
|
20497
20794
|
schemaName: getSchemaName(schemaName)
|
|
20498
20795
|
})} (workflow_name, (snapshot ->> 'status'), "createdAt" DESC)`;
|
|
20499
20796
|
}
|
|
20500
|
-
/**
|
|
20501
|
-
* Sanitizes JSON string for PostgreSQL jsonb:
|
|
20502
|
-
* - Removes problematic Unicode sequences:
|
|
20503
|
-
* - \u0000 (null character) - causes error 22P05 "unsupported Unicode escape sequence"
|
|
20504
|
-
* - \uD800-\uDFFF (unpaired surrogates) - causes "Unicode low surrogate must follow a high surrogate"
|
|
20505
|
-
* - \\uD800 (escaped-backslash + surrogate, e.g. from JS regex literals like [^\ud800-\udfff]):
|
|
20506
|
-
* removing just \uXXXX would leave a dangling backslash that creates a new invalid escape (e.g. \-)
|
|
20507
|
-
* - Escapes any remaining invalid JSON escape sequences (e.g. \v, \k, \-)
|
|
20508
|
-
*/
|
|
20509
|
-
function sanitizeJsonForPg(jsonString) {
|
|
20510
|
-
return jsonString.replace(/\\\\?u(0000|[Dd][89A-Fa-f][0-9A-Fa-f]{2})/g, "").replace(/(^|[^\\])(\\(?!["\\/bfnrtu]))/g, "$1\\\\");
|
|
20511
|
-
}
|
|
20512
20797
|
var WorkflowsPG = class WorkflowsPG extends _mastra_core_storage.WorkflowsStorage {
|
|
20513
20798
|
#db;
|
|
20514
20799
|
#schema;
|
|
@@ -20788,11 +21073,11 @@ var WorkflowsPG = class WorkflowsPG extends _mastra_core_storage.WorkflowsStorag
|
|
|
20788
21073
|
await this.#db.client.none(`INSERT INTO ${getTableName({
|
|
20789
21074
|
indexName: _mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT,
|
|
20790
21075
|
schemaName: getSchemaName(this.#schema)
|
|
20791
|
-
})}
|
|
21076
|
+
})} AS t
|
|
20792
21077
|
(workflow_name, run_id, "resourceId", snapshot, "createdAt", "updatedAt", "createdAtZ", "updatedAtZ")
|
|
20793
21078
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
|
20794
21079
|
ON CONFLICT (workflow_name, run_id) DO UPDATE
|
|
20795
|
-
SET "resourceId" = $3, snapshot = $4, "updatedAt" = $6, "updatedAtZ" = $8`, [
|
|
21080
|
+
SET "resourceId" = COALESCE($3, t."resourceId"), snapshot = $4, "updatedAt" = $6, "updatedAtZ" = $8`, [
|
|
20796
21081
|
workflowName,
|
|
20797
21082
|
runId,
|
|
20798
21083
|
resourceId,
|
|
@@ -21750,6 +22035,12 @@ var PgFactoryStorageOps = class {
|
|
|
21750
22035
|
async findMany(collection, where, opts) {
|
|
21751
22036
|
return this.#select(this.#queryable, collection, where, opts);
|
|
21752
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
|
+
}
|
|
21753
22044
|
async insertOne(collection, row) {
|
|
21754
22045
|
const schema = this.#schema(collection);
|
|
21755
22046
|
const pk = primaryKeyOf(schema);
|