@mastra/pg 1.22.0-alpha.0 → 1.22.0-alpha.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +110 -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 +2 -0
- package/dist/docs/references/integrations-databases-postgresql.md +26 -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 +515 -133
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +515 -133
- 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/background-tasks/index.d.ts +3 -1
- package/dist/storage/domains/background-tasks/index.d.ts.map +1 -1
- package/dist/storage/domains/experiments/index.d.ts.map +1 -1
- 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/discovery.d.ts +14 -0
- package/dist/storage/domains/observability/v-next/discovery.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/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 +3 -3
package/dist/index.cjs
CHANGED
|
@@ -565,11 +565,22 @@ function buildFilterQuery(filter, minScore, topK) {
|
|
|
565
565
|
}
|
|
566
566
|
//#endregion
|
|
567
567
|
//#region src/vector/index.ts
|
|
568
|
-
const
|
|
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}`, {
|
|
@@ -4918,7 +5045,7 @@ var BackgroundTasksPG = class BackgroundTasksPG extends _mastra_core_storage.Bac
|
|
|
4918
5045
|
}
|
|
4919
5046
|
});
|
|
4920
5047
|
}
|
|
4921
|
-
async updateTask(taskId, update) {
|
|
5048
|
+
async updateTask(taskId, update, options) {
|
|
4922
5049
|
const setClauses = [];
|
|
4923
5050
|
const params = [];
|
|
4924
5051
|
let paramIdx = 1;
|
|
@@ -4960,10 +5087,15 @@ var BackgroundTasksPG = class BackgroundTasksPG extends _mastra_core_storage.Bac
|
|
|
4960
5087
|
const val = update.completedAt?.toISOString() ?? null;
|
|
4961
5088
|
params.push(val, val);
|
|
4962
5089
|
}
|
|
4963
|
-
if (setClauses.length === 0) return;
|
|
5090
|
+
if (setClauses.length === 0) return false;
|
|
4964
5091
|
const table = getTableName$4(getSchemaName$4(this.#schema));
|
|
4965
5092
|
params.push(taskId);
|
|
4966
|
-
|
|
5093
|
+
let where = `"id" = $${paramIdx++}`;
|
|
5094
|
+
if (options?.expectedStatus) {
|
|
5095
|
+
where += ` AND "status" = $${paramIdx}`;
|
|
5096
|
+
params.push(options.expectedStatus);
|
|
5097
|
+
}
|
|
5098
|
+
return ((await this.#db.client.query(`UPDATE ${table} SET ${setClauses.join(", ")} WHERE ${where}`, params)).rowCount ?? 0) > 0;
|
|
4967
5099
|
}
|
|
4968
5100
|
async getTask(taskId) {
|
|
4969
5101
|
const table = getTableName$4(getSchemaName$4(this.#schema));
|
|
@@ -6608,6 +6740,7 @@ var ExperimentsPG = class ExperimentsPG extends _mastra_core_storage.Experiments
|
|
|
6608
6740
|
"tags",
|
|
6609
6741
|
"comment",
|
|
6610
6742
|
"toolMockReport",
|
|
6743
|
+
"metadata",
|
|
6611
6744
|
"organizationId",
|
|
6612
6745
|
"projectId",
|
|
6613
6746
|
"attempt"
|
|
@@ -6804,6 +6937,7 @@ var ExperimentsPG = class ExperimentsPG extends _mastra_core_storage.Experiments
|
|
|
6804
6937
|
input: (0, _mastra_core_storage.safelyParseJSON)(row.input),
|
|
6805
6938
|
output: row.output ? (0, _mastra_core_storage.safelyParseJSON)(row.output) : null,
|
|
6806
6939
|
groundTruth: row.groundTruth ? (0, _mastra_core_storage.safelyParseJSON)(row.groundTruth) : null,
|
|
6940
|
+
metadata: row.metadata ? (0, _mastra_core_storage.safelyParseJSON)(row.metadata) : null,
|
|
6807
6941
|
error: row.error ? (0, _mastra_core_storage.safelyParseJSON)(row.error) : null,
|
|
6808
6942
|
startedAt: (0, _mastra_core_storage.ensureDate)(row.startedAtZ || row.startedAt),
|
|
6809
6943
|
completedAt: (0, _mastra_core_storage.ensureDate)(row.completedAtZ || row.completedAt),
|
|
@@ -7112,6 +7246,7 @@ var ExperimentsPG = class ExperimentsPG extends _mastra_core_storage.Experiments
|
|
|
7112
7246
|
input: input.input,
|
|
7113
7247
|
output: input.output ?? null,
|
|
7114
7248
|
groundTruth: input.groundTruth ?? null,
|
|
7249
|
+
metadata: input.metadata ?? null,
|
|
7115
7250
|
error: input.error ?? null,
|
|
7116
7251
|
startedAt: input.startedAt.toISOString(),
|
|
7117
7252
|
completedAt: input.completedAt.toISOString(),
|
|
@@ -7134,6 +7269,7 @@ var ExperimentsPG = class ExperimentsPG extends _mastra_core_storage.Experiments
|
|
|
7134
7269
|
input: input.input,
|
|
7135
7270
|
output: input.output ?? null,
|
|
7136
7271
|
groundTruth: input.groundTruth ?? null,
|
|
7272
|
+
metadata: input.metadata ?? null,
|
|
7137
7273
|
error: input.error ?? null,
|
|
7138
7274
|
startedAt: input.startedAt,
|
|
7139
7275
|
completedAt: input.completedAt,
|
|
@@ -7170,9 +7306,9 @@ var ExperimentsPG = class ExperimentsPG extends _mastra_core_storage.Experiments
|
|
|
7170
7306
|
const id = crypto.randomUUID();
|
|
7171
7307
|
return t.one(`INSERT INTO ${tableName} (
|
|
7172
7308
|
"id", "experimentId", "itemId", "itemDatasetVersion", "organizationId", "projectId",
|
|
7173
|
-
"input", "output", "groundTruth", "error", "startedAt", "completedAt",
|
|
7309
|
+
"input", "output", "groundTruth", "metadata", "error", "startedAt", "completedAt",
|
|
7174
7310
|
"retryCount", "attempt", "traceId", "status", "tags", "toolMockReport", "createdAt"
|
|
7175
|
-
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19)
|
|
7311
|
+
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20)
|
|
7176
7312
|
RETURNING *`, [
|
|
7177
7313
|
id,
|
|
7178
7314
|
input.experimentId,
|
|
@@ -7183,6 +7319,7 @@ var ExperimentsPG = class ExperimentsPG extends _mastra_core_storage.Experiments
|
|
|
7183
7319
|
JSON.stringify(input.input),
|
|
7184
7320
|
input.output != null ? JSON.stringify(input.output) : null,
|
|
7185
7321
|
input.groundTruth != null ? JSON.stringify(input.groundTruth) : null,
|
|
7322
|
+
input.metadata != null ? JSON.stringify(input.metadata) : null,
|
|
7186
7323
|
input.error != null ? JSON.stringify(input.error) : null,
|
|
7187
7324
|
input.startedAt.toISOString(),
|
|
7188
7325
|
input.completedAt.toISOString(),
|
|
@@ -7197,9 +7334,9 @@ var ExperimentsPG = class ExperimentsPG extends _mastra_core_storage.Experiments
|
|
|
7197
7334
|
}
|
|
7198
7335
|
return t.one(`UPDATE ${tableName} SET
|
|
7199
7336
|
"itemDatasetVersion" = $2, "organizationId" = $3, "projectId" = $4,
|
|
7200
|
-
"input" = $5, "output" = $6, "groundTruth" = $7, "
|
|
7201
|
-
"startedAt" = $
|
|
7202
|
-
"traceId" = $
|
|
7337
|
+
"input" = $5, "output" = $6, "groundTruth" = $7, "metadata" = $8, "error" = $9,
|
|
7338
|
+
"startedAt" = $10, "completedAt" = $11, "retryCount" = $12, "attempt" = $13,
|
|
7339
|
+
"traceId" = $14, "status" = $15, "tags" = $16, "toolMockReport" = $17
|
|
7203
7340
|
WHERE "id" = $1 RETURNING *`, [
|
|
7204
7341
|
existing.id,
|
|
7205
7342
|
input.itemDatasetVersion ?? null,
|
|
@@ -7208,6 +7345,7 @@ var ExperimentsPG = class ExperimentsPG extends _mastra_core_storage.Experiments
|
|
|
7208
7345
|
JSON.stringify(input.input),
|
|
7209
7346
|
input.output != null ? JSON.stringify(input.output) : null,
|
|
7210
7347
|
input.groundTruth != null ? JSON.stringify(input.groundTruth) : null,
|
|
7348
|
+
input.metadata != null ? JSON.stringify(input.metadata) : null,
|
|
7211
7349
|
input.error != null ? JSON.stringify(input.error) : null,
|
|
7212
7350
|
input.startedAt.toISOString(),
|
|
7213
7351
|
input.completedAt.toISOString(),
|
|
@@ -13591,6 +13729,11 @@ const SPAN_EVENT_COLUMNS = [
|
|
|
13591
13729
|
type: "boolean",
|
|
13592
13730
|
defaultSql: "false"
|
|
13593
13731
|
},
|
|
13732
|
+
{
|
|
13733
|
+
name: "isPending",
|
|
13734
|
+
type: "boolean",
|
|
13735
|
+
defaultSql: "false"
|
|
13736
|
+
},
|
|
13594
13737
|
{
|
|
13595
13738
|
name: "startedAt",
|
|
13596
13739
|
type: "timestamptz"
|
|
@@ -13887,6 +14030,7 @@ const SPAN_LIGHT_SELECT_COLUMN_NAMES = [
|
|
|
13887
14030
|
"spanType",
|
|
13888
14031
|
"error",
|
|
13889
14032
|
"isEvent",
|
|
14033
|
+
"isPending",
|
|
13890
14034
|
"startedAt",
|
|
13891
14035
|
"endedAt"
|
|
13892
14036
|
];
|
|
@@ -14281,6 +14425,26 @@ function allTableDDL(schema, mode) {
|
|
|
14281
14425
|
discoveryTableDDL(schema)
|
|
14282
14426
|
];
|
|
14283
14427
|
}
|
|
14428
|
+
/**
|
|
14429
|
+
* Additive column migrations for tables created by an older version of this
|
|
14430
|
+
* schema. `CREATE TABLE IF NOT EXISTS` is a no-op on an existing table, so new
|
|
14431
|
+
* columns have to be added out of band.
|
|
14432
|
+
*
|
|
14433
|
+
* `ALTER TABLE` takes an AccessExclusiveLock on the partitioned parent, so
|
|
14434
|
+
* callers must check {@link columnExistsSQL} first and only run the statement
|
|
14435
|
+
* when the column is genuinely missing. On Postgres 11+ the non-volatile
|
|
14436
|
+
* default does not rewrite the table, and the ALTER cascades to partitions.
|
|
14437
|
+
*/
|
|
14438
|
+
function additiveColumns(schema) {
|
|
14439
|
+
return [{
|
|
14440
|
+
table: TABLE_SPAN_EVENTS,
|
|
14441
|
+
column: "isPending",
|
|
14442
|
+
ddl: `ALTER TABLE ${qualifiedTable(schema, TABLE_SPAN_EVENTS)} ADD COLUMN IF NOT EXISTS "isPending" boolean NOT NULL DEFAULT false`
|
|
14443
|
+
}];
|
|
14444
|
+
}
|
|
14445
|
+
/** Existence probe for an additive column. Params: schema, table, column. */
|
|
14446
|
+
const columnExistsSQL = `SELECT 1 FROM information_schema.columns
|
|
14447
|
+
WHERE table_schema = $1 AND table_name = $2 AND column_name = $3`;
|
|
14284
14448
|
/** Index CREATEs. Safe to run repeatedly. */
|
|
14285
14449
|
function allIndexDDL(schema) {
|
|
14286
14450
|
return tableIndexes().map((spec) => indexDDL(schema, spec));
|
|
@@ -14288,6 +14452,7 @@ function allIndexDDL(schema) {
|
|
|
14288
14452
|
//#endregion
|
|
14289
14453
|
//#region src/storage/domains/observability/v-next/discovery.ts
|
|
14290
14454
|
const DEFAULT_TTL_SECONDS = 300;
|
|
14455
|
+
const DEFAULT_LOOKBACK_SECONDS = 720 * 60 * 60;
|
|
14291
14456
|
/** All signal tables that contain a column. Used by cross-signal discovery. */
|
|
14292
14457
|
const SIGNAL_TABLES_WITH_CONTEXT = [
|
|
14293
14458
|
TABLE_SPAN_EVENTS,
|
|
@@ -14352,22 +14517,71 @@ function startOrJoinRefresh(dedupeKey, cacheKey, refresh, upsert, logger) {
|
|
|
14352
14517
|
async function readWithRefresh(client, schema, cacheKey, refresh, ttlSeconds, logger) {
|
|
14353
14518
|
const table = qualifiedTable(schema, TABLE_DISCOVERY);
|
|
14354
14519
|
const row = await client.oneOrNone(`SELECT "values", "refreshedAt" FROM ${table} WHERE "cacheKey" = $1`, [cacheKey]);
|
|
14355
|
-
const
|
|
14356
|
-
|
|
14357
|
-
const refreshing = startOrJoinRefresh(`${schema}:${cacheKey}`, cacheKey, refresh, (values) => upsertCache(client, schema, cacheKey, values), logger);
|
|
14520
|
+
const dedupeKey = `${schema}:${cacheKey}`;
|
|
14521
|
+
const startRefresh = () => startOrJoinRefresh(dedupeKey, cacheKey, refresh, (values) => upsertCache(client, schema, cacheKey, values), logger);
|
|
14358
14522
|
if (ttlSeconds <= 0) try {
|
|
14359
|
-
return await
|
|
14523
|
+
return await startRefresh();
|
|
14360
14524
|
} catch {
|
|
14361
14525
|
return row?.values ?? [];
|
|
14362
14526
|
}
|
|
14527
|
+
const refreshedAtMs = row ? new Date(row.refreshedAt).getTime() : 0;
|
|
14528
|
+
if (!(!row || Date.now() - refreshedAtMs > ttlSeconds * 1e3)) return row.values;
|
|
14363
14529
|
if (!row) try {
|
|
14364
|
-
return await
|
|
14530
|
+
return await startRefresh();
|
|
14365
14531
|
} catch {
|
|
14366
14532
|
return [];
|
|
14367
14533
|
}
|
|
14368
|
-
|
|
14534
|
+
const previousRefreshedAt = row.refreshedAt;
|
|
14535
|
+
claimRefresh(client, schema, cacheKey, ttlSeconds).then((claimedAt) => {
|
|
14536
|
+
if (!claimedAt) return;
|
|
14537
|
+
return startRefresh().then(() => {}, () => releaseClaim(client, schema, cacheKey, previousRefreshedAt, claimedAt));
|
|
14538
|
+
}).catch(() => {});
|
|
14369
14539
|
return row.values;
|
|
14370
14540
|
}
|
|
14541
|
+
/**
|
|
14542
|
+
* Try to become the process that refreshes `cacheKey`.
|
|
14543
|
+
*
|
|
14544
|
+
* The conditional UPDATE re-checks staleness in the database and stamps
|
|
14545
|
+
* `refreshedAt` in the same statement, so exactly one caller can transition a
|
|
14546
|
+
* given cache row out of the stale window — losers see zero rows updated and
|
|
14547
|
+
* skip their refresh. This is the cross-process counterpart to
|
|
14548
|
+
* `inFlightRefreshes`: N frontends going stale together run one refresh
|
|
14549
|
+
* instead of N.
|
|
14550
|
+
*
|
|
14551
|
+
* It reuses the cache table rather than an advisory lock because `DbClient`
|
|
14552
|
+
* is pool-backed, so a transaction-scoped lock would mean threading explicit
|
|
14553
|
+
* transaction handling through the discovery read path.
|
|
14554
|
+
*
|
|
14555
|
+
* The stamp is provisional in both directions: a completed refresh overwrites
|
|
14556
|
+
* it via `upsertCache`, and a failed one restores it via `releaseClaim`.
|
|
14557
|
+
*
|
|
14558
|
+
* Returns the exact provisional stamp written by the claim (or `null` when
|
|
14559
|
+
* the claim was lost) so `releaseClaim` can restore the row only while it
|
|
14560
|
+
* still holds this claim's stamp.
|
|
14561
|
+
*/
|
|
14562
|
+
async function claimRefresh(client, schema, cacheKey, ttlSeconds) {
|
|
14563
|
+
const table = qualifiedTable(schema, TABLE_DISCOVERY);
|
|
14564
|
+
return (await client.oneOrNone(`UPDATE ${table} SET "refreshedAt" = NOW()
|
|
14565
|
+
WHERE "cacheKey" = $1 AND "refreshedAt" <= NOW() - ($2 || ' seconds')::interval
|
|
14566
|
+
RETURNING "refreshedAt"::text AS "refreshedAt"`, [cacheKey, Math.floor(ttlSeconds)]))?.refreshedAt ?? null;
|
|
14567
|
+
}
|
|
14568
|
+
/**
|
|
14569
|
+
* Hand back a claim whose refresh failed by restoring the timestamp the
|
|
14570
|
+
* claiming reader observed. Guarded on the row still holding this claim's
|
|
14571
|
+
* exact provisional stamp so a refresh that completed in the meantime
|
|
14572
|
+
* (another process, or a later claim) isn't dragged backwards into staleness.
|
|
14573
|
+
*/
|
|
14574
|
+
async function releaseClaim(client, schema, cacheKey, previousRefreshedAt, claimedAt) {
|
|
14575
|
+
const table = qualifiedTable(schema, TABLE_DISCOVERY);
|
|
14576
|
+
try {
|
|
14577
|
+
await client.query(`UPDATE ${table} SET "refreshedAt" = $2
|
|
14578
|
+
WHERE "cacheKey" = $1 AND "refreshedAt" = $3::timestamptz`, [
|
|
14579
|
+
cacheKey,
|
|
14580
|
+
previousRefreshedAt,
|
|
14581
|
+
claimedAt
|
|
14582
|
+
]);
|
|
14583
|
+
} catch {}
|
|
14584
|
+
}
|
|
14371
14585
|
async function upsertCache(client, schema, cacheKey, values) {
|
|
14372
14586
|
const table = qualifiedTable(schema, TABLE_DISCOVERY);
|
|
14373
14587
|
await client.query(`INSERT INTO ${table} ("cacheKey", "refreshedAt", "values")
|
|
@@ -14376,61 +14590,95 @@ async function upsertCache(client, schema, cacheKey, values) {
|
|
|
14376
14590
|
"refreshedAt" = EXCLUDED."refreshedAt",
|
|
14377
14591
|
"values" = EXCLUDED."values"`, [cacheKey, JSON.stringify(values)]);
|
|
14378
14592
|
}
|
|
14379
|
-
|
|
14593
|
+
/**
|
|
14594
|
+
* Time predicate that bounds a discovery refresh to recent events.
|
|
14595
|
+
*
|
|
14596
|
+
* Each signal table is range-partitioned on its own time column
|
|
14597
|
+
* (`SIGNAL_TIME_COLUMN`: `endedAt` for spans, `timestamp` for everything
|
|
14598
|
+
* else), so the predicate has to be built per table rather than shared across
|
|
14599
|
+
* a UNION. Bounding on that exact column is what lets the planner prune
|
|
14600
|
+
* partitions/chunks instead of scanning all history.
|
|
14601
|
+
*
|
|
14602
|
+
* The interval is interpolated rather than bound to a parameter on purpose:
|
|
14603
|
+
* the UNION branches in `distinctAcrossTables` and `getTags` all reference the
|
|
14604
|
+
* same positional placeholders, so adding a per-branch parameter would break
|
|
14605
|
+
* `$N` numbering. Safety comes from coercing to a finite integer here — the
|
|
14606
|
+
* value originates from `DiscoveryConfig`, never from user input.
|
|
14607
|
+
*/
|
|
14608
|
+
function lookbackPredicate(table, lookbackSeconds) {
|
|
14609
|
+
if (!Number.isFinite(lookbackSeconds) || lookbackSeconds <= 0) return "";
|
|
14610
|
+
const seconds = Math.floor(lookbackSeconds);
|
|
14611
|
+
const timeColumn = SIGNAL_TIME_COLUMN[table];
|
|
14612
|
+
if (!timeColumn) return "";
|
|
14613
|
+
return `AND "${timeColumn}" >= NOW() - INTERVAL '${seconds} seconds'`;
|
|
14614
|
+
}
|
|
14615
|
+
async function distinctAcrossTables(client, schema, column, tables, lookbackSeconds, filterSql = "", filterParams = []) {
|
|
14380
14616
|
const safeColumn = (0, _mastra_core_utils.parseSqlIdentifier)(column, "column name");
|
|
14381
|
-
const unions = tables.map((t) => `SELECT DISTINCT "${safeColumn}" AS v FROM ${qualifiedTable(schema, t)} WHERE "${safeColumn}" IS NOT NULL AND "${safeColumn}" <> '' ${filterSql}`).join(" UNION ");
|
|
14617
|
+
const unions = tables.map((t) => `SELECT DISTINCT "${safeColumn}" AS v FROM ${qualifiedTable(schema, t)} WHERE "${safeColumn}" IS NOT NULL AND "${safeColumn}" <> '' ${filterSql} ${lookbackPredicate(t, lookbackSeconds)}`).join(" UNION ");
|
|
14382
14618
|
return (await client.manyOrNone(`SELECT v FROM (${unions}) sub ORDER BY v`, filterParams)).map((r) => r.v);
|
|
14383
14619
|
}
|
|
14384
14620
|
async function getEntityTypes(client, schema, _args, config) {
|
|
14385
|
-
|
|
14621
|
+
const ttl = config.ttlSeconds ?? DEFAULT_TTL_SECONDS;
|
|
14622
|
+
const lookback = config.lookbackSeconds ?? DEFAULT_LOOKBACK_SECONDS;
|
|
14623
|
+
return { entityTypes: await readWithRefresh(client, schema, "entity_types", () => distinctAcrossTables(client, schema, "entityType", ENTITY_DISCOVERY_TABLES, lookback), ttl, config.logger) };
|
|
14386
14624
|
}
|
|
14387
14625
|
async function getEntityNames(client, schema, args, config) {
|
|
14388
14626
|
const ttl = config.ttlSeconds ?? DEFAULT_TTL_SECONDS;
|
|
14627
|
+
const lookback = config.lookbackSeconds ?? DEFAULT_LOOKBACK_SECONDS;
|
|
14389
14628
|
const cacheKey = args.entityType ? `entity_names:${args.entityType}` : "entity_names";
|
|
14390
14629
|
const filterSql = args.entityType ? `AND "entityType" = $1` : "";
|
|
14391
14630
|
const filterParams = args.entityType ? [args.entityType] : [];
|
|
14392
|
-
return { names: await readWithRefresh(client, schema, cacheKey, () => distinctAcrossTables(client, schema, "entityName", ENTITY_DISCOVERY_TABLES, filterSql, filterParams), ttl, config.logger) };
|
|
14631
|
+
return { names: await readWithRefresh(client, schema, cacheKey, () => distinctAcrossTables(client, schema, "entityName", ENTITY_DISCOVERY_TABLES, lookback, filterSql, filterParams), ttl, config.logger) };
|
|
14393
14632
|
}
|
|
14394
14633
|
async function getServiceNames(client, schema, _args, config) {
|
|
14395
|
-
|
|
14634
|
+
const ttl = config.ttlSeconds ?? DEFAULT_TTL_SECONDS;
|
|
14635
|
+
const lookback = config.lookbackSeconds ?? DEFAULT_LOOKBACK_SECONDS;
|
|
14636
|
+
return { serviceNames: await readWithRefresh(client, schema, "service_names", () => distinctAcrossTables(client, schema, "serviceName", SIGNAL_TABLES_WITH_CONTEXT, lookback), ttl, config.logger) };
|
|
14396
14637
|
}
|
|
14397
14638
|
async function getEnvironments(client, schema, _args, config) {
|
|
14398
|
-
|
|
14639
|
+
const ttl = config.ttlSeconds ?? DEFAULT_TTL_SECONDS;
|
|
14640
|
+
const lookback = config.lookbackSeconds ?? DEFAULT_LOOKBACK_SECONDS;
|
|
14641
|
+
return { environments: await readWithRefresh(client, schema, "environments", () => distinctAcrossTables(client, schema, "environment", SIGNAL_TABLES_WITH_CONTEXT, lookback), ttl, config.logger) };
|
|
14399
14642
|
}
|
|
14400
14643
|
async function getTags(client, schema, args, config) {
|
|
14401
14644
|
const ttl = config.ttlSeconds ?? DEFAULT_TTL_SECONDS;
|
|
14645
|
+
const lookback = config.lookbackSeconds ?? DEFAULT_LOOKBACK_SECONDS;
|
|
14402
14646
|
const cacheKey = args.entityType ? `tags:${args.entityType}` : "tags";
|
|
14403
14647
|
const refresh = async () => {
|
|
14404
14648
|
const filter = args.entityType ? `AND "entityType" = $1` : "";
|
|
14405
14649
|
const params = args.entityType ? [args.entityType] : [];
|
|
14406
|
-
const unions = ENTITY_DISCOVERY_TABLES.map((t) => `SELECT DISTINCT UNNEST("tags") AS v FROM ${qualifiedTable(schema, t)} WHERE array_length("tags", 1) > 0 ${filter}`).join(" UNION ");
|
|
14650
|
+
const unions = ENTITY_DISCOVERY_TABLES.map((t) => `SELECT DISTINCT UNNEST("tags") AS v FROM ${qualifiedTable(schema, t)} WHERE array_length("tags", 1) > 0 ${filter} ${lookbackPredicate(t, lookback)}`).join(" UNION ");
|
|
14407
14651
|
return (await client.manyOrNone(`SELECT v FROM (${unions}) sub WHERE v IS NOT NULL AND v <> '' ORDER BY v`, params)).map((r) => r.v);
|
|
14408
14652
|
};
|
|
14409
14653
|
return { tags: await readWithRefresh(client, schema, cacheKey, refresh, ttl, config.logger) };
|
|
14410
14654
|
}
|
|
14411
14655
|
async function getMetricNames(client, schema, args, config) {
|
|
14656
|
+
const ttl = config.ttlSeconds ?? DEFAULT_TTL_SECONDS;
|
|
14657
|
+
const lookback = config.lookbackSeconds ?? DEFAULT_LOOKBACK_SECONDS;
|
|
14412
14658
|
let filtered = await readWithRefresh(client, schema, "metric_names", async () => {
|
|
14413
14659
|
return (await client.manyOrNone(`SELECT DISTINCT "name" AS v FROM ${qualifiedTable(schema, TABLE_METRIC_EVENTS)}
|
|
14414
|
-
WHERE "name" IS NOT NULL AND "name" <> '' ORDER BY "name"`)).map((r) => r.v);
|
|
14415
|
-
},
|
|
14660
|
+
WHERE "name" IS NOT NULL AND "name" <> '' ${lookbackPredicate(TABLE_METRIC_EVENTS, lookback)} ORDER BY "name"`)).map((r) => r.v);
|
|
14661
|
+
}, ttl, config.logger);
|
|
14416
14662
|
if (args.prefix) filtered = filtered.filter((v) => v.startsWith(args.prefix));
|
|
14417
14663
|
if (args.limit) filtered = filtered.slice(0, args.limit);
|
|
14418
14664
|
return { names: filtered };
|
|
14419
14665
|
}
|
|
14420
14666
|
async function getMetricLabelKeys(client, schema, args, config) {
|
|
14421
14667
|
const ttl = config.ttlSeconds ?? DEFAULT_TTL_SECONDS;
|
|
14668
|
+
const lookback = config.lookbackSeconds ?? DEFAULT_LOOKBACK_SECONDS;
|
|
14422
14669
|
return { keys: await readWithRefresh(client, schema, `metric_label_keys:${args.metricName}`, async () => {
|
|
14423
14670
|
return (await client.manyOrNone(`SELECT DISTINCT k AS v
|
|
14424
14671
|
FROM ${qualifiedTable(schema, TABLE_METRIC_EVENTS)}, jsonb_object_keys("labels") k
|
|
14425
|
-
WHERE "name" = $1 ORDER BY k`, [args.metricName])).map((r) => r.v);
|
|
14672
|
+
WHERE "name" = $1 ${lookbackPredicate(TABLE_METRIC_EVENTS, lookback)} ORDER BY k`, [args.metricName])).map((r) => r.v);
|
|
14426
14673
|
}, ttl, config.logger) };
|
|
14427
14674
|
}
|
|
14428
14675
|
async function getMetricLabelValues(client, schema, args, config) {
|
|
14429
14676
|
const ttl = config.ttlSeconds ?? DEFAULT_TTL_SECONDS;
|
|
14677
|
+
const lookback = config.lookbackSeconds ?? DEFAULT_LOOKBACK_SECONDS;
|
|
14430
14678
|
let filtered = await readWithRefresh(client, schema, `metric_label_values:${args.metricName}:${args.labelKey}`, async () => {
|
|
14431
14679
|
return (await client.manyOrNone(`SELECT DISTINCT "labels" ->> $2 AS v
|
|
14432
14680
|
FROM ${qualifiedTable(schema, TABLE_METRIC_EVENTS)}
|
|
14433
|
-
WHERE "name" = $1 AND "labels" ? $2
|
|
14681
|
+
WHERE "name" = $1 AND "labels" ? $2 ${lookbackPredicate(TABLE_METRIC_EVENTS, lookback)}
|
|
14434
14682
|
ORDER BY v`, [args.metricName, args.labelKey])).map((r) => r.v).filter((v) => v != null && v !== "");
|
|
14435
14683
|
}, ttl, config.logger);
|
|
14436
14684
|
if (args.prefix) filtered = filtered.filter((v) => v.startsWith(args.prefix));
|
|
@@ -14669,6 +14917,7 @@ function commonContextToRow(record) {
|
|
|
14669
14917
|
};
|
|
14670
14918
|
}
|
|
14671
14919
|
function spanRecordToRow(span) {
|
|
14920
|
+
const isPending = !span.isEvent && span.endedAt == null;
|
|
14672
14921
|
const endedAt = span.isEvent ? span.startedAt : span.endedAt ?? span.startedAt;
|
|
14673
14922
|
const metadata = span.metadata ?? null;
|
|
14674
14923
|
return {
|
|
@@ -14679,6 +14928,7 @@ function spanRecordToRow(span) {
|
|
|
14679
14928
|
name: span.name,
|
|
14680
14929
|
spanType: span.spanType,
|
|
14681
14930
|
isEvent: Boolean(span.isEvent),
|
|
14931
|
+
isPending,
|
|
14682
14932
|
startedAt: toIsoOrDate(span.startedAt),
|
|
14683
14933
|
endedAt: toIsoOrDate(endedAt),
|
|
14684
14934
|
tags: normalizeTags(span.tags),
|
|
@@ -14693,9 +14943,19 @@ function spanRecordToRow(span) {
|
|
|
14693
14943
|
requestContext: jsonField(span.requestContext)
|
|
14694
14944
|
};
|
|
14695
14945
|
}
|
|
14946
|
+
/**
|
|
14947
|
+
* Recover a span's real `endedAt` from its row. A pending row carries a
|
|
14948
|
+
* synthesized `endedAt` (a copy of `startedAt`) because the column is part of
|
|
14949
|
+
* the primary key; callers must see null so the span reads as still running.
|
|
14950
|
+
*/
|
|
14951
|
+
function spanRowEndedAt(row, startedAt) {
|
|
14952
|
+
if (row.isEvent) return startedAt;
|
|
14953
|
+
if (row.isPending) return null;
|
|
14954
|
+
return toDateOrNull(row.endedAt);
|
|
14955
|
+
}
|
|
14696
14956
|
function rowToSpanRecord(row) {
|
|
14697
14957
|
const startedAt = toDate(row.startedAt);
|
|
14698
|
-
const endedAt = row
|
|
14958
|
+
const endedAt = spanRowEndedAt(row, startedAt);
|
|
14699
14959
|
const error = parsedJson(row.error);
|
|
14700
14960
|
const { executionSource, ...ctx } = rowToCommonContext(row);
|
|
14701
14961
|
return {
|
|
@@ -14729,7 +14989,7 @@ function rowToSpanRecord(row) {
|
|
|
14729
14989
|
*/
|
|
14730
14990
|
function rowToLightSpanRecord(row) {
|
|
14731
14991
|
const startedAt = toDate(row.startedAt);
|
|
14732
|
-
const endedAt = row
|
|
14992
|
+
const endedAt = spanRowEndedAt(row, startedAt);
|
|
14733
14993
|
return {
|
|
14734
14994
|
traceId: row.traceId,
|
|
14735
14995
|
spanId: row.spanId,
|
|
@@ -15260,22 +15520,42 @@ const COMPLEX_GROUP_BY_EXCLUDED = /* @__PURE__ */ new Set([
|
|
|
15260
15520
|
"requestContext"
|
|
15261
15521
|
]);
|
|
15262
15522
|
//#endregion
|
|
15523
|
+
//#region src/storage/db/sanitize-json.ts
|
|
15524
|
+
/**
|
|
15525
|
+
* Sanitizes JSON string for PostgreSQL jsonb:
|
|
15526
|
+
* - Removes problematic Unicode sequences:
|
|
15527
|
+
* - \u0000 (null character) - causes error 22P05 "unsupported Unicode escape sequence"
|
|
15528
|
+
* - \uD800-\uDFFF (unpaired surrogates) - causes "Unicode low surrogate must follow a high surrogate"
|
|
15529
|
+
* - \\uD800 (escaped-backslash + surrogate, e.g. from JS regex literals like [^\ud800-\udfff]):
|
|
15530
|
+
* removing just \uXXXX would leave a dangling backslash that creates a new invalid escape (e.g. \-)
|
|
15531
|
+
* - Escapes any remaining invalid JSON escape sequences (e.g. \v, \k, \-)
|
|
15532
|
+
*/
|
|
15533
|
+
function sanitizeJsonForPg(jsonString) {
|
|
15534
|
+
return jsonString.replace(/\\\\?u(0000|[Dd][89A-Fa-f][0-9A-Fa-f]{2})/g, "").replace(/(^|[^\\])(\\(?!["\\/bfnrtu]))/g, "$1\\\\");
|
|
15535
|
+
}
|
|
15536
|
+
//#endregion
|
|
15263
15537
|
//#region src/storage/domains/observability/v-next/sql.ts
|
|
15264
15538
|
/**
|
|
15265
15539
|
* SQL helpers for the v-next Postgres observability domain.
|
|
15266
15540
|
*
|
|
15267
|
-
* Provides a multi-row INSERT builder with
|
|
15268
|
-
*
|
|
15269
|
-
* pg driver doesn't have to guess column types.
|
|
15541
|
+
* Provides a multi-row INSERT builder with a configurable conflict clause
|
|
15542
|
+
* (`ON CONFLICT DO NOTHING` by default, for retry idempotency), and explicit
|
|
15543
|
+
* jsonb / text[] casts so the pg driver doesn't have to guess column types.
|
|
15270
15544
|
*/
|
|
15271
15545
|
/**
|
|
15272
15546
|
* Encode a JS value for a `$N::jsonb` cast. Always `JSON.stringify` so a
|
|
15273
15547
|
* plain string like `"hello"` becomes `"hello"` (a valid JSON scalar) and
|
|
15274
15548
|
* not the bare word `hello`, which Postgres rejects when cast to jsonb.
|
|
15549
|
+
*
|
|
15550
|
+
* The result is sanitized because Postgres rejects some sequences that are
|
|
15551
|
+
* legal in JSON: NUL (`\u0000`) fails with 22P05 and unpaired UTF-16
|
|
15552
|
+
* surrogates fail with 22P02. Inserts here are batched into a single
|
|
15553
|
+
* multi-row statement, so one bad value would otherwise reject the whole
|
|
15554
|
+
* batch of observability events.
|
|
15275
15555
|
*/
|
|
15276
15556
|
function encodeJsonb(value) {
|
|
15277
15557
|
if (value === null || value === void 0) return null;
|
|
15278
|
-
return JSON.stringify(value);
|
|
15558
|
+
return sanitizeJsonForPg(JSON.stringify(value));
|
|
15279
15559
|
}
|
|
15280
15560
|
/**
|
|
15281
15561
|
* Build a multi-row INSERT with explicit column types and ON CONFLICT DO NOTHING.
|
|
@@ -15286,7 +15566,7 @@ function encodeJsonb(value) {
|
|
|
15286
15566
|
* All records must have identical key sets.
|
|
15287
15567
|
* @returns { text, values } ready to pass to `client.query`.
|
|
15288
15568
|
*/
|
|
15289
|
-
function buildInsert(schema, table, records) {
|
|
15569
|
+
function buildInsert(schema, table, records, onConflict = "ON CONFLICT DO NOTHING") {
|
|
15290
15570
|
if (records.length === 0) return null;
|
|
15291
15571
|
const columns = Object.keys(records[0]).map((c) => (0, _mastra_core_utils.parseSqlIdentifier)(c, "column name"));
|
|
15292
15572
|
const quotedColumns = columns.map((c) => `"${c}"`).join(", ");
|
|
@@ -15312,10 +15592,44 @@ function buildInsert(schema, table, records) {
|
|
|
15312
15592
|
return {
|
|
15313
15593
|
text: `INSERT INTO ${qualifiedTable(schema, table)} (${quotedColumns})
|
|
15314
15594
|
VALUES ${rowPlaceholders.join(", ")}
|
|
15315
|
-
|
|
15595
|
+
${onConflict}`,
|
|
15316
15596
|
values
|
|
15317
15597
|
};
|
|
15318
15598
|
}
|
|
15599
|
+
/** Primary key of the span table; also the conflict target for span upserts. */
|
|
15600
|
+
const SPAN_KEY_COLUMNS = [
|
|
15601
|
+
"traceId",
|
|
15602
|
+
"spanId",
|
|
15603
|
+
"endedAt"
|
|
15604
|
+
];
|
|
15605
|
+
/**
|
|
15606
|
+
* Conflict clause for span writes.
|
|
15607
|
+
*
|
|
15608
|
+
* A span is written twice under the event-sourced strategy: once when it starts
|
|
15609
|
+
* (`isPending = true`, `endedAt` synthesized from `startedAt`) and once when it
|
|
15610
|
+
* ends. Those two rows normally differ in `endedAt` and so coexist under the
|
|
15611
|
+
* primary key. A zero-duration span is the exception — its real `endedAt`
|
|
15612
|
+
* equals its `startedAt`, so the end row collides with the pending row. Letting
|
|
15613
|
+
* `DO NOTHING` win there would strand the span as permanently running, so the
|
|
15614
|
+
* end row overwrites the pending one instead.
|
|
15615
|
+
*
|
|
15616
|
+
* The `WHERE` guard keeps the clause a no-op for every other conflict, which
|
|
15617
|
+
* preserves insert-only retry idempotency: a replayed end row cannot clobber
|
|
15618
|
+
* the row it duplicates, and a late-arriving start row cannot revert an ended
|
|
15619
|
+
* span to pending.
|
|
15620
|
+
*/
|
|
15621
|
+
function spanConflictClause(columns) {
|
|
15622
|
+
return `ON CONFLICT ("traceId", "spanId", "endedAt") DO UPDATE SET ${columns.filter((column) => !SPAN_KEY_COLUMNS.includes(column)).map((column) => `"${column}" = EXCLUDED."${column}"`).join(", ")}
|
|
15623
|
+
WHERE ${TABLE_SPAN_EVENTS}."isPending" AND NOT EXCLUDED."isPending"`;
|
|
15624
|
+
}
|
|
15625
|
+
/**
|
|
15626
|
+
* Ordering that picks the winning row for a span under the event-sourced write
|
|
15627
|
+
* model. An ended row always beats a pending row for the same span; among rows
|
|
15628
|
+
* of the same kind the latest write wins, with `cursorId` breaking ties between
|
|
15629
|
+
* rows that share a timestamp. Combine with `DISTINCT ON` (or `LIMIT 1`) after
|
|
15630
|
+
* the grouping key to collapse a span's rows down to one.
|
|
15631
|
+
*/
|
|
15632
|
+
const SPAN_COLLAPSE_ORDER = "\"isPending\" ASC, \"endedAt\" DESC, \"cursorId\" DESC";
|
|
15319
15633
|
/**
|
|
15320
15634
|
* Standard SELECT column list for tracing tables. The select projects every
|
|
15321
15635
|
* column the row→record converters expect.
|
|
@@ -16311,12 +16625,51 @@ async function getScorePercentiles(client, schema, args) {
|
|
|
16311
16625
|
function asIsoTimestamp(value) {
|
|
16312
16626
|
return value instanceof Date ? value.toISOString() : new Date(value).toISOString();
|
|
16313
16627
|
}
|
|
16628
|
+
/**
|
|
16629
|
+
* Ordering that picks the row representing a trace's current root.
|
|
16630
|
+
*
|
|
16631
|
+
* A trace can hold more than one root row. A span is written once when it
|
|
16632
|
+
* starts and again when it ends, and a durable run that suspends ends its root
|
|
16633
|
+
* span and opens a fresh one on resume. In both cases the newest row is the one
|
|
16634
|
+
* that describes the trace right now, and `cursorId` — a sequence assigned at
|
|
16635
|
+
* insert — is what "newest" means here. It also orders correctly where
|
|
16636
|
+
* timestamps cannot: a zero-duration span has `startedAt = endedAt`, and a
|
|
16637
|
+
* resumed root starts after the suspended root ended.
|
|
16638
|
+
*/
|
|
16639
|
+
const ROOT_COLLAPSE_ORDER = "\"cursorId\" DESC";
|
|
16640
|
+
/**
|
|
16641
|
+
* Predicate that keeps only the winning row for each span — the ended row when
|
|
16642
|
+
* one exists, the newest row otherwise. Mirrors `SPAN_COLLAPSE_ORDER` as an
|
|
16643
|
+
* anti-join, for the same reason as {@link latestRootPredicate}.
|
|
16644
|
+
*/
|
|
16645
|
+
function latestSpanPredicate(spanTable) {
|
|
16646
|
+
return `NOT EXISTS (
|
|
16647
|
+
SELECT 1 FROM ${spanTable} newer
|
|
16648
|
+
WHERE newer."traceId" = r."traceId"
|
|
16649
|
+
AND newer."spanId" = r."spanId"
|
|
16650
|
+
AND (newer."isPending" < r."isPending" OR (newer."isPending" = r."isPending" AND newer."cursorId" > r."cursorId"))
|
|
16651
|
+
)`;
|
|
16652
|
+
}
|
|
16653
|
+
/**
|
|
16654
|
+
* Predicate that keeps only a trace's current root row, as an anti-join rather
|
|
16655
|
+
* than a `DISTINCT ON`. Written this way so the outer query still scans the
|
|
16656
|
+
* partial root indexes and streams rows in `LIMIT`-order; grouping first would
|
|
16657
|
+
* force every root in the table to be read before the first page came back.
|
|
16658
|
+
*/
|
|
16659
|
+
function latestRootPredicate(spanTable) {
|
|
16660
|
+
return `NOT EXISTS (
|
|
16661
|
+
SELECT 1 FROM ${spanTable} newer
|
|
16662
|
+
WHERE newer."traceId" = r."traceId"
|
|
16663
|
+
AND newer."parentSpanId" IS NULL
|
|
16664
|
+
AND newer."cursorId" > r."cursorId"
|
|
16665
|
+
)`;
|
|
16666
|
+
}
|
|
16314
16667
|
async function getRootSpan(client, schema, args) {
|
|
16315
16668
|
const table = qualifiedTable(schema, TABLE_SPAN_EVENTS);
|
|
16316
16669
|
const row = await client.oneOrNone(`SELECT ${SPAN_SELECT_COLUMNS}
|
|
16317
16670
|
FROM ${table}
|
|
16318
16671
|
WHERE "traceId" = $1 AND "parentSpanId" IS NULL
|
|
16319
|
-
ORDER BY
|
|
16672
|
+
ORDER BY ${ROOT_COLLAPSE_ORDER}
|
|
16320
16673
|
LIMIT 1`, [args.traceId]);
|
|
16321
16674
|
if (!row) return null;
|
|
16322
16675
|
return { span: rowToSpanRecord(row) };
|
|
@@ -16328,7 +16681,7 @@ async function getRootSpan(client, schema, args) {
|
|
|
16328
16681
|
* planner. Starts numbering from `nextParamIdx`.
|
|
16329
16682
|
*/
|
|
16330
16683
|
function buildListTracesFilters(filters, spanTable, nextParamIdx) {
|
|
16331
|
-
const conditions = [`r."parentSpanId" IS NULL
|
|
16684
|
+
const conditions = [`r."parentSpanId" IS NULL`, latestRootPredicate(spanTable)];
|
|
16332
16685
|
const params = [];
|
|
16333
16686
|
let i = nextParamIdx;
|
|
16334
16687
|
if (!filters) return {
|
|
@@ -16345,11 +16698,11 @@ function buildListTracesFilters(filters, spanTable, nextParamIdx) {
|
|
|
16345
16698
|
params.push(asIsoTimestamp(filters.startedAt.end));
|
|
16346
16699
|
}
|
|
16347
16700
|
if (filters.endedAt?.start) {
|
|
16348
|
-
conditions.push(`r."endedAt" ${filters.endedAt.startExclusive ? ">" : ">="} $${i++}`);
|
|
16701
|
+
conditions.push(`(NOT r."isPending" AND r."endedAt" ${filters.endedAt.startExclusive ? ">" : ">="} $${i++})`);
|
|
16349
16702
|
params.push(asIsoTimestamp(filters.endedAt.start));
|
|
16350
16703
|
}
|
|
16351
16704
|
if (filters.endedAt?.end) {
|
|
16352
|
-
conditions.push(`r."endedAt" ${filters.endedAt.endExclusive ? "<" : "<="} $${i++}`);
|
|
16705
|
+
conditions.push(`(NOT r."isPending" AND r."endedAt" ${filters.endedAt.endExclusive ? "<" : "<="} $${i++})`);
|
|
16353
16706
|
params.push(asIsoTimestamp(filters.endedAt.end));
|
|
16354
16707
|
}
|
|
16355
16708
|
if (filters.spanType !== void 0) {
|
|
@@ -16421,10 +16774,10 @@ function buildListTracesFilters(filters, spanTable, nextParamIdx) {
|
|
|
16421
16774
|
conditions.push(`r."error" IS NOT NULL`);
|
|
16422
16775
|
break;
|
|
16423
16776
|
case _mastra_core_storage.TraceStatus.RUNNING:
|
|
16424
|
-
conditions.push(`
|
|
16777
|
+
conditions.push(`r."isPending"`);
|
|
16425
16778
|
break;
|
|
16426
16779
|
case _mastra_core_storage.TraceStatus.SUCCESS:
|
|
16427
|
-
conditions.push(`r."error" IS NULL`);
|
|
16780
|
+
conditions.push(`r."error" IS NULL AND NOT r."isPending"`);
|
|
16428
16781
|
break;
|
|
16429
16782
|
}
|
|
16430
16783
|
if (filters.hasChildError !== void 0) {
|
|
@@ -16561,8 +16914,8 @@ function buildBranchSpanTypeClause(userSpanType, params, startIdx) {
|
|
|
16561
16914
|
* NOT have a `parentSpanId IS NULL` predicate — branches can be nested under
|
|
16562
16915
|
* other spans. Returns the bound params and the next param index.
|
|
16563
16916
|
*/
|
|
16564
|
-
function buildListBranchesFilters(filters, spanType, nextParamIdx) {
|
|
16565
|
-
const conditions = [];
|
|
16917
|
+
function buildListBranchesFilters(filters, spanType, spanTable, nextParamIdx) {
|
|
16918
|
+
const conditions = [latestSpanPredicate(spanTable)];
|
|
16566
16919
|
const params = [];
|
|
16567
16920
|
let i = nextParamIdx;
|
|
16568
16921
|
const spanTypeClause = buildBranchSpanTypeClause(spanType, params, i);
|
|
@@ -16583,11 +16936,11 @@ function buildListBranchesFilters(filters, spanType, nextParamIdx) {
|
|
|
16583
16936
|
params.push(asIsoTimestamp(filters.startedAt.end));
|
|
16584
16937
|
}
|
|
16585
16938
|
if (filters.endedAt?.start) {
|
|
16586
|
-
conditions.push(`r."endedAt" ${filters.endedAt.startExclusive ? ">" : ">="} $${i++}`);
|
|
16939
|
+
conditions.push(`(NOT r."isPending" AND r."endedAt" ${filters.endedAt.startExclusive ? ">" : ">="} $${i++})`);
|
|
16587
16940
|
params.push(asIsoTimestamp(filters.endedAt.start));
|
|
16588
16941
|
}
|
|
16589
16942
|
if (filters.endedAt?.end) {
|
|
16590
|
-
conditions.push(`r."endedAt" ${filters.endedAt.endExclusive ? "<" : "<="} $${i++}`);
|
|
16943
|
+
conditions.push(`(NOT r."isPending" AND r."endedAt" ${filters.endedAt.endExclusive ? "<" : "<="} $${i++})`);
|
|
16591
16944
|
params.push(asIsoTimestamp(filters.endedAt.end));
|
|
16592
16945
|
}
|
|
16593
16946
|
if (filters.traceId !== void 0) {
|
|
@@ -16699,10 +17052,10 @@ function buildListBranchesFilters(filters, spanType, nextParamIdx) {
|
|
|
16699
17052
|
conditions.push(`r."error" IS NOT NULL`);
|
|
16700
17053
|
break;
|
|
16701
17054
|
case _mastra_core_storage.TraceStatus.RUNNING:
|
|
16702
|
-
conditions.push(`
|
|
17055
|
+
conditions.push(`r."isPending"`);
|
|
16703
17056
|
break;
|
|
16704
17057
|
case _mastra_core_storage.TraceStatus.SUCCESS:
|
|
16705
|
-
conditions.push(`r."error" IS NULL`);
|
|
17058
|
+
conditions.push(`r."error" IS NULL AND NOT r."isPending"`);
|
|
16706
17059
|
break;
|
|
16707
17060
|
}
|
|
16708
17061
|
return {
|
|
@@ -16721,7 +17074,7 @@ async function listBranches(client, schema, args) {
|
|
|
16721
17074
|
return listBranchesPage(client, span, filters, pagination.page, pagination.perPage, orderBy.field, orderBy.direction);
|
|
16722
17075
|
}
|
|
16723
17076
|
async function listBranchesPage(client, span, filters, page, perPage, orderField, orderDir) {
|
|
16724
|
-
const built = buildListBranchesFilters(filters, filters?.spanType, 1);
|
|
17077
|
+
const built = buildListBranchesFilters(filters, filters?.spanType, span, 1);
|
|
16725
17078
|
if (!built) {
|
|
16726
17079
|
const deltaCursor = deltaPollingFeatureEnabled() ? await readBranchesStreamHeadCursor(client, span, filters) : void 0;
|
|
16727
17080
|
return {
|
|
@@ -16775,7 +17128,7 @@ async function listBranchesDelta(client, span, filters, after, limit) {
|
|
|
16775
17128
|
deltaCursor
|
|
16776
17129
|
};
|
|
16777
17130
|
}
|
|
16778
|
-
const built = buildListBranchesFilters(filters, filters?.spanType, 1);
|
|
17131
|
+
const built = buildListBranchesFilters(filters, filters?.spanType, span, 1);
|
|
16779
17132
|
if (!built) return {
|
|
16780
17133
|
branches: [],
|
|
16781
17134
|
delta: {
|
|
@@ -16818,14 +17171,35 @@ async function readBranchesStreamHeadCursor(client, span, filters) {
|
|
|
16818
17171
|
}
|
|
16819
17172
|
//#endregion
|
|
16820
17173
|
//#region src/storage/domains/observability/v-next/tracing.ts
|
|
16821
|
-
async function
|
|
16822
|
-
const
|
|
17174
|
+
async function writeSpanRows(client, schema, rows) {
|
|
17175
|
+
const deduped = dedupeSpanRows(rows);
|
|
17176
|
+
const insert = buildInsert(schema, TABLE_SPAN_EVENTS, deduped, spanConflictClause(Object.keys(deduped[0] ?? {})));
|
|
16823
17177
|
if (insert) await client.query(insert.text, insert.values);
|
|
16824
17178
|
}
|
|
17179
|
+
/**
|
|
17180
|
+
* Collapse rows that share a primary key within a single batch. Postgres
|
|
17181
|
+
* rejects an `ON CONFLICT DO UPDATE` statement whose VALUES list would touch
|
|
17182
|
+
* the same row twice, which a batch containing both the start and the end of a
|
|
17183
|
+
* zero-duration span would do. The ended row wins, matching what the conflict
|
|
17184
|
+
* clause would have done across two separate statements.
|
|
17185
|
+
*/
|
|
17186
|
+
function dedupeSpanRows(rows) {
|
|
17187
|
+
if (rows.length < 2) return rows;
|
|
17188
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
17189
|
+
for (const row of rows) {
|
|
17190
|
+
const key = `${row.traceId}\u0000${row.spanId}\u0000${String(row.endedAt)}`;
|
|
17191
|
+
const existing = byKey.get(key);
|
|
17192
|
+
if (existing && !existing.isPending) continue;
|
|
17193
|
+
byKey.set(key, row);
|
|
17194
|
+
}
|
|
17195
|
+
return [...byKey.values()];
|
|
17196
|
+
}
|
|
17197
|
+
async function createSpan(client, schema, args) {
|
|
17198
|
+
await writeSpanRows(client, schema, [spanRecordToRow(args.span)]);
|
|
17199
|
+
}
|
|
16825
17200
|
async function batchCreateSpans(client, schema, args) {
|
|
16826
17201
|
if (args.records.length === 0) return;
|
|
16827
|
-
|
|
16828
|
-
if (insert) await client.query(insert.text, insert.values);
|
|
17202
|
+
await writeSpanRows(client, schema, args.records.map(spanRecordToRow));
|
|
16829
17203
|
}
|
|
16830
17204
|
async function getSpans(client, schema, args) {
|
|
16831
17205
|
if (args.spanIds.length === 0) return {
|
|
@@ -16833,10 +17207,13 @@ async function getSpans(client, schema, args) {
|
|
|
16833
17207
|
spans: []
|
|
16834
17208
|
};
|
|
16835
17209
|
const table = qualifiedTable(schema, TABLE_SPAN_EVENTS);
|
|
16836
|
-
const rows = await client.manyOrNone(`SELECT
|
|
16837
|
-
|
|
16838
|
-
|
|
16839
|
-
|
|
17210
|
+
const rows = await client.manyOrNone(`SELECT * FROM (
|
|
17211
|
+
SELECT DISTINCT ON ("spanId") ${SPAN_SELECT_COLUMNS}
|
|
17212
|
+
FROM ${table}
|
|
17213
|
+
WHERE "traceId" = $1
|
|
17214
|
+
AND "spanId" = ANY($2::text[])
|
|
17215
|
+
ORDER BY "spanId", ${SPAN_COLLAPSE_ORDER}
|
|
17216
|
+
) collapsed
|
|
16840
17217
|
ORDER BY "startedAt" ASC`, [args.traceId, args.spanIds]);
|
|
16841
17218
|
return {
|
|
16842
17219
|
traceId: args.traceId,
|
|
@@ -16848,16 +17225,19 @@ async function getSpan(client, schema, args) {
|
|
|
16848
17225
|
const row = await client.oneOrNone(`SELECT ${SPAN_SELECT_COLUMNS}
|
|
16849
17226
|
FROM ${table}
|
|
16850
17227
|
WHERE "traceId" = $1 AND "spanId" = $2
|
|
16851
|
-
ORDER BY
|
|
17228
|
+
ORDER BY ${SPAN_COLLAPSE_ORDER}
|
|
16852
17229
|
LIMIT 1`, [args.traceId, args.spanId]);
|
|
16853
17230
|
if (!row) return null;
|
|
16854
17231
|
return { span: rowToSpanRecord(row) };
|
|
16855
17232
|
}
|
|
16856
17233
|
async function getTrace(client, schema, args) {
|
|
16857
17234
|
const table = qualifiedTable(schema, TABLE_SPAN_EVENTS);
|
|
16858
|
-
const rows = await client.manyOrNone(`SELECT
|
|
16859
|
-
|
|
16860
|
-
|
|
17235
|
+
const rows = await client.manyOrNone(`SELECT * FROM (
|
|
17236
|
+
SELECT DISTINCT ON ("spanId") ${SPAN_SELECT_COLUMNS}
|
|
17237
|
+
FROM ${table}
|
|
17238
|
+
WHERE "traceId" = $1
|
|
17239
|
+
ORDER BY "spanId", ${SPAN_COLLAPSE_ORDER}
|
|
17240
|
+
) collapsed
|
|
16861
17241
|
ORDER BY "startedAt" ASC`, [args.traceId]);
|
|
16862
17242
|
if (!rows.length) return null;
|
|
16863
17243
|
return {
|
|
@@ -16867,9 +17247,12 @@ async function getTrace(client, schema, args) {
|
|
|
16867
17247
|
}
|
|
16868
17248
|
async function getTraceLight(client, schema, args) {
|
|
16869
17249
|
const table = qualifiedTable(schema, TABLE_SPAN_EVENTS);
|
|
16870
|
-
const rows = await client.manyOrNone(`SELECT
|
|
16871
|
-
|
|
16872
|
-
|
|
17250
|
+
const rows = await client.manyOrNone(`SELECT * FROM (
|
|
17251
|
+
SELECT DISTINCT ON ("spanId") ${SPAN_LIGHT_SELECT_COLUMNS}
|
|
17252
|
+
FROM ${table}
|
|
17253
|
+
WHERE "traceId" = $1
|
|
17254
|
+
ORDER BY "spanId", ${SPAN_COLLAPSE_ORDER}
|
|
17255
|
+
) collapsed
|
|
16873
17256
|
ORDER BY "startedAt" ASC`, [args.traceId]);
|
|
16874
17257
|
if (!rows.length) return null;
|
|
16875
17258
|
return {
|
|
@@ -16980,6 +17363,11 @@ var ObservabilityStoragePostgresVNext = class ObservabilityStoragePostgresVNext
|
|
|
16980
17363
|
} catch (error) {
|
|
16981
17364
|
if (!isDuplicateRelationError(error)) throw error;
|
|
16982
17365
|
}
|
|
17366
|
+
for (const { table, column, ddl } of additiveColumns(this.#schema)) if (!await this.#client.oneOrNone(columnExistsSQL, [
|
|
17367
|
+
this.#schema,
|
|
17368
|
+
table,
|
|
17369
|
+
column
|
|
17370
|
+
])) await this.#client.none(ddl);
|
|
16983
17371
|
for (const ddl of allIndexDDL(this.#schema)) try {
|
|
16984
17372
|
await this.#client.none(ddl);
|
|
16985
17373
|
} catch (error) {
|
|
@@ -17076,8 +17464,8 @@ var ObservabilityStoragePostgresVNext = class ObservabilityStoragePostgresVNext
|
|
|
17076
17464
|
}
|
|
17077
17465
|
get observabilityStrategy() {
|
|
17078
17466
|
return {
|
|
17079
|
-
preferred: "
|
|
17080
|
-
supported: ["
|
|
17467
|
+
preferred: "event-sourced",
|
|
17468
|
+
supported: ["event-sourced"]
|
|
17081
17469
|
};
|
|
17082
17470
|
}
|
|
17083
17471
|
getFeatures() {
|
|
@@ -20501,18 +20889,6 @@ function workflowSnapshotStatusIndexSQL(indexName, schemaName) {
|
|
|
20501
20889
|
schemaName: getSchemaName(schemaName)
|
|
20502
20890
|
})} (workflow_name, (snapshot ->> 'status'), "createdAt" DESC)`;
|
|
20503
20891
|
}
|
|
20504
|
-
/**
|
|
20505
|
-
* Sanitizes JSON string for PostgreSQL jsonb:
|
|
20506
|
-
* - Removes problematic Unicode sequences:
|
|
20507
|
-
* - \u0000 (null character) - causes error 22P05 "unsupported Unicode escape sequence"
|
|
20508
|
-
* - \uD800-\uDFFF (unpaired surrogates) - causes "Unicode low surrogate must follow a high surrogate"
|
|
20509
|
-
* - \\uD800 (escaped-backslash + surrogate, e.g. from JS regex literals like [^\ud800-\udfff]):
|
|
20510
|
-
* removing just \uXXXX would leave a dangling backslash that creates a new invalid escape (e.g. \-)
|
|
20511
|
-
* - Escapes any remaining invalid JSON escape sequences (e.g. \v, \k, \-)
|
|
20512
|
-
*/
|
|
20513
|
-
function sanitizeJsonForPg(jsonString) {
|
|
20514
|
-
return jsonString.replace(/\\\\?u(0000|[Dd][89A-Fa-f][0-9A-Fa-f]{2})/g, "").replace(/(^|[^\\])(\\(?!["\\/bfnrtu]))/g, "$1\\\\");
|
|
20515
|
-
}
|
|
20516
20892
|
var WorkflowsPG = class WorkflowsPG extends _mastra_core_storage.WorkflowsStorage {
|
|
20517
20893
|
#db;
|
|
20518
20894
|
#schema;
|
|
@@ -20792,11 +21168,11 @@ var WorkflowsPG = class WorkflowsPG extends _mastra_core_storage.WorkflowsStorag
|
|
|
20792
21168
|
await this.#db.client.none(`INSERT INTO ${getTableName({
|
|
20793
21169
|
indexName: _mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT,
|
|
20794
21170
|
schemaName: getSchemaName(this.#schema)
|
|
20795
|
-
})}
|
|
21171
|
+
})} AS t
|
|
20796
21172
|
(workflow_name, run_id, "resourceId", snapshot, "createdAt", "updatedAt", "createdAtZ", "updatedAtZ")
|
|
20797
21173
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
|
20798
21174
|
ON CONFLICT (workflow_name, run_id) DO UPDATE
|
|
20799
|
-
SET "resourceId" = $3, snapshot = $4, "updatedAt" = $6, "updatedAtZ" = $8`, [
|
|
21175
|
+
SET "resourceId" = COALESCE($3, t."resourceId"), snapshot = $4, "updatedAt" = $6, "updatedAtZ" = $8`, [
|
|
20800
21176
|
workflowName,
|
|
20801
21177
|
runId,
|
|
20802
21178
|
resourceId,
|
|
@@ -21650,7 +22026,7 @@ var PgFactoryStorageOps = class {
|
|
|
21650
22026
|
row[name] = Boolean(value);
|
|
21651
22027
|
break;
|
|
21652
22028
|
case "json":
|
|
21653
|
-
row[name] =
|
|
22029
|
+
row[name] = value;
|
|
21654
22030
|
break;
|
|
21655
22031
|
case "bigint":
|
|
21656
22032
|
case "integer":
|
|
@@ -21754,6 +22130,12 @@ var PgFactoryStorageOps = class {
|
|
|
21754
22130
|
async findMany(collection, where, opts) {
|
|
21755
22131
|
return this.#select(this.#queryable, collection, where, opts);
|
|
21756
22132
|
}
|
|
22133
|
+
async count(collection, where) {
|
|
22134
|
+
const schema = this.#schema(collection);
|
|
22135
|
+
const filter = this.#buildWhere(schema, where);
|
|
22136
|
+
const result = await this.#queryable.query(`SELECT COUNT(*) AS count FROM "${schema.name}" WHERE ${filter.sql}`, filter.args);
|
|
22137
|
+
return Number(result.rows[0]?.count ?? 0);
|
|
22138
|
+
}
|
|
21757
22139
|
async insertOne(collection, row) {
|
|
21758
22140
|
const schema = this.#schema(collection);
|
|
21759
22141
|
const pk = primaryKeyOf(schema);
|