@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.js
CHANGED
|
@@ -541,11 +541,22 @@ function buildFilterQuery(filter, minScore, topK) {
|
|
|
541
541
|
}
|
|
542
542
|
//#endregion
|
|
543
543
|
//#region src/vector/index.ts
|
|
544
|
-
const
|
|
544
|
+
const DEFAULT_NAMESPACE = "default";
|
|
545
|
+
const MAX_UPSERT_ROWS_PER_STATEMENT = Math.floor(65535 / 4);
|
|
545
546
|
var PgVector = class extends MastraVector {
|
|
546
547
|
pool;
|
|
548
|
+
/**
|
|
549
|
+
* Cache for the public `getIndexInfo()`. Holds the in-flight promise rather than the
|
|
550
|
+
* resolved value so concurrent callers on a cold cache share a single round trip.
|
|
551
|
+
*/
|
|
547
552
|
describeIndexCache = /* @__PURE__ */ new Map();
|
|
553
|
+
/**
|
|
554
|
+
* Cache for the catalog-only index metadata used by the internal hot paths
|
|
555
|
+
* (cache warmup, query, upsert, updateVector, setupIndex). Also memoizes the promise.
|
|
556
|
+
*/
|
|
557
|
+
indexMetadataCache = /* @__PURE__ */ new Map();
|
|
548
558
|
createdIndexes = /* @__PURE__ */ new Map();
|
|
559
|
+
namespaceReadyIndexes = /* @__PURE__ */ new Set();
|
|
549
560
|
indexVectorTypes = /* @__PURE__ */ new Map();
|
|
550
561
|
mutexesByName = /* @__PURE__ */ new Map();
|
|
551
562
|
schema;
|
|
@@ -601,7 +612,7 @@ var PgVector = class extends MastraVector {
|
|
|
601
612
|
try {
|
|
602
613
|
const existingIndexes = await this.listIndexes();
|
|
603
614
|
await Promise.all(existingIndexes.map(async (indexName) => {
|
|
604
|
-
const info = await this.
|
|
615
|
+
const info = await this.getIndexMetadata({ indexName });
|
|
605
616
|
const key = await this.getIndexCacheKey({
|
|
606
617
|
indexName,
|
|
607
618
|
metric: info.metric,
|
|
@@ -774,20 +785,76 @@ var PgVector = class extends MastraVector {
|
|
|
774
785
|
const quotedVectorName = `"${parsedIndexName}_vector_idx"`;
|
|
775
786
|
return {
|
|
776
787
|
tableName: quotedSchemaName ? `${quotedSchemaName}.${quotedIndexName}` : quotedIndexName,
|
|
777
|
-
vectorIndexName: quotedVectorName
|
|
788
|
+
vectorIndexName: quotedVectorName,
|
|
789
|
+
parsedIndexName
|
|
778
790
|
};
|
|
779
791
|
}
|
|
780
792
|
getSchemaName() {
|
|
781
793
|
return this.schema ? `"${parseSqlIdentifier(this.schema, "schema name")}"` : void 0;
|
|
782
794
|
}
|
|
795
|
+
async ensureNamespaceSchema(indexName, client) {
|
|
796
|
+
const { tableName, parsedIndexName } = this.getTableName(indexName);
|
|
797
|
+
const schemaName = this.schema ? parseSqlIdentifier(this.schema, "schema name") : "public";
|
|
798
|
+
if ((await client.query(`SELECT 1
|
|
799
|
+
FROM information_schema.columns
|
|
800
|
+
WHERE table_schema = $1 AND table_name = $2 AND column_name = 'vector_id'`, [schemaName, parsedIndexName])).rowCount === 0) return;
|
|
801
|
+
await client.query(`ALTER TABLE ${tableName} ADD COLUMN IF NOT EXISTS namespace VARCHAR(255) NOT NULL DEFAULT '${DEFAULT_NAMESPACE}'`);
|
|
802
|
+
const legacyConstraints = await client.query(`SELECT c.conname
|
|
803
|
+
FROM pg_constraint c
|
|
804
|
+
JOIN pg_class t ON t.oid = c.conrelid
|
|
805
|
+
JOIN pg_namespace n ON n.oid = t.relnamespace
|
|
806
|
+
WHERE n.nspname = $1
|
|
807
|
+
AND t.relname = $2
|
|
808
|
+
AND c.contype = 'u'
|
|
809
|
+
AND pg_get_constraintdef(c.oid) = 'UNIQUE (vector_id)'`, [schemaName, parsedIndexName]);
|
|
810
|
+
for (const { conname } of legacyConstraints.rows) {
|
|
811
|
+
const parsedConstraintName = parseSqlIdentifier(conname, "constraint name");
|
|
812
|
+
await client.query(`ALTER TABLE ${tableName} DROP CONSTRAINT "${parsedConstraintName}"`);
|
|
813
|
+
}
|
|
814
|
+
const namespaceIndexName = parseSqlIdentifier(`${parsedIndexName}_namespace_vector_id_idx`, "index name");
|
|
815
|
+
await client.query(`CREATE UNIQUE INDEX IF NOT EXISTS "${namespaceIndexName}" ON ${tableName} (namespace, vector_id)`);
|
|
816
|
+
}
|
|
783
817
|
transformFilter(filter) {
|
|
784
818
|
return new PGFilterTranslator().translate(filter);
|
|
785
819
|
}
|
|
820
|
+
/**
|
|
821
|
+
* Cached variant of {@link describeIndex}, including the row count.
|
|
822
|
+
*
|
|
823
|
+
* Internal code paths do not use this - they use the catalog-only
|
|
824
|
+
* {@link getIndexMetadata}, which never scans the table.
|
|
825
|
+
*/
|
|
786
826
|
async getIndexInfo({ indexName }) {
|
|
787
|
-
|
|
788
|
-
|
|
827
|
+
return this.memoize(this.describeIndexCache, indexName, () => this.describeIndex({ indexName }));
|
|
828
|
+
}
|
|
829
|
+
/**
|
|
830
|
+
* Cached index metadata read from the Postgres catalog only.
|
|
831
|
+
*
|
|
832
|
+
* This is what every internal caller needs: none of them read `count`, and paying for
|
|
833
|
+
* `SELECT COUNT(*)` on a large index costs a full heap scan per call.
|
|
834
|
+
*/
|
|
835
|
+
getIndexMetadata({ indexName }) {
|
|
836
|
+
return this.memoize(this.indexMetadataCache, indexName, () => this.describeIndexMetadata({ indexName }));
|
|
837
|
+
}
|
|
838
|
+
/**
|
|
839
|
+
* Stores the in-flight promise in `cache` so concurrent callers on a cold cache share one
|
|
840
|
+
* round trip, and drops the entry if it rejects so a transient failure is not cached forever.
|
|
841
|
+
*/
|
|
842
|
+
memoize(cache, key, load) {
|
|
843
|
+
const cached = cache.get(key);
|
|
844
|
+
if (cached) return cached;
|
|
845
|
+
const pending = load();
|
|
846
|
+
cache.set(key, pending);
|
|
847
|
+
pending.catch(() => {
|
|
848
|
+
if (cache.get(key) === pending) cache.delete(key);
|
|
849
|
+
});
|
|
850
|
+
return pending;
|
|
851
|
+
}
|
|
852
|
+
/** Drops every cached view of an index, e.g. after its index definition or table changed. */
|
|
853
|
+
invalidateIndexCaches(indexName) {
|
|
854
|
+
this.describeIndexCache.delete(indexName);
|
|
855
|
+
this.indexMetadataCache.delete(indexName);
|
|
789
856
|
}
|
|
790
|
-
async query({ indexName, queryVector, topK = 10, filter, includeVector = false, minScore = -1, ef, probes }) {
|
|
857
|
+
async query({ indexName, queryVector, topK = 10, filter, includeVector = false, minScore = -1, ef, probes, namespace = DEFAULT_NAMESPACE }) {
|
|
791
858
|
try {
|
|
792
859
|
validateTopK("PG", topK);
|
|
793
860
|
if (queryVector !== void 0) {
|
|
@@ -808,16 +875,23 @@ var PgVector = class extends MastraVector {
|
|
|
808
875
|
try {
|
|
809
876
|
const { sql: filterQuery, values: filterValues } = buildDeleteFilterQuery(this.transformFilter(filter));
|
|
810
877
|
const { tableName } = this.getTableName(indexName);
|
|
878
|
+
const filterClause = filterQuery.trim().replace(/^WHERE\s+/i, "");
|
|
879
|
+
const namespaceParam = filterValues.length + 1;
|
|
811
880
|
const query = `
|
|
812
881
|
SELECT
|
|
813
882
|
vector_id as id,
|
|
814
883
|
metadata
|
|
815
884
|
${includeVector ? ", embedding" : ""}
|
|
816
885
|
FROM ${tableName}
|
|
817
|
-
|
|
886
|
+
WHERE namespace = $${namespaceParam}
|
|
887
|
+
${filterClause ? `AND (${filterClause})` : ""}
|
|
818
888
|
ORDER BY vector_id
|
|
819
|
-
LIMIT $${
|
|
820
|
-
return (await client.query(query, [
|
|
889
|
+
LIMIT $${namespaceParam + 1}`;
|
|
890
|
+
return (await client.query(query, [
|
|
891
|
+
...filterValues,
|
|
892
|
+
namespace,
|
|
893
|
+
topK
|
|
894
|
+
])).rows.map(({ id, metadata, embedding }) => ({
|
|
821
895
|
id,
|
|
822
896
|
score: 0,
|
|
823
897
|
metadata,
|
|
@@ -841,7 +915,7 @@ var PgVector = class extends MastraVector {
|
|
|
841
915
|
await this.ensureSearchPath(client);
|
|
842
916
|
await client.query("BEGIN");
|
|
843
917
|
const { sql: filterQuery, values: filterValues } = buildFilterQuery(this.transformFilter(filter), minScore, topK);
|
|
844
|
-
const indexInfo = await this.
|
|
918
|
+
const indexInfo = await this.getIndexMetadata({ indexName });
|
|
845
919
|
const metric = indexInfo.metric ?? "cosine";
|
|
846
920
|
const ops = this.getVectorOps(indexInfo.vectorType, metric);
|
|
847
921
|
const vectorStr = ops.formatVector(queryVector, indexInfo.dimension);
|
|
@@ -849,14 +923,18 @@ var PgVector = class extends MastraVector {
|
|
|
849
923
|
const calculatedEf = ef ?? Math.max(topK, (indexInfo?.config?.m ?? 16) * topK);
|
|
850
924
|
const searchEf = Math.min(1e3, Math.max(1, calculatedEf));
|
|
851
925
|
await client.query(`SET LOCAL hnsw.ef_search = ${searchEf}`);
|
|
926
|
+
await client.query(`SET LOCAL hnsw.iterative_scan = strict_order`);
|
|
852
927
|
}
|
|
853
928
|
if (indexInfo.type === "ivfflat" && probes) await client.query(`SET LOCAL ivfflat.probes = ${probes}`);
|
|
854
929
|
const { tableName } = this.getTableName(indexName);
|
|
855
930
|
const qualifiedVectorType = this.getVectorTypeName(indexInfo.vectorType, indexInfo.dimension);
|
|
856
931
|
const distanceExpr = `embedding ${ops.distanceOperator} '${vectorStr}'::${qualifiedVectorType}`;
|
|
857
932
|
const scoreExpr = ops.scoreExpr(distanceExpr);
|
|
858
|
-
const
|
|
859
|
-
const
|
|
933
|
+
const filterClause = filterQuery.trim().replace(/^WHERE\s+/i, "");
|
|
934
|
+
const hasFilter = filterClause.length > 0;
|
|
935
|
+
const useIndexedOrder = indexInfo.type === "hnsw" && !hasFilter && minScore <= 0;
|
|
936
|
+
const namespaceParam = filterValues.length + 1;
|
|
937
|
+
const query = useIndexedOrder ? `
|
|
860
938
|
WITH vector_scores AS (
|
|
861
939
|
SELECT
|
|
862
940
|
vector_id as id,
|
|
@@ -864,6 +942,7 @@ var PgVector = class extends MastraVector {
|
|
|
864
942
|
metadata
|
|
865
943
|
${includeVector ? ", embedding" : ""}
|
|
866
944
|
FROM ${tableName}
|
|
945
|
+
WHERE namespace = $${namespaceParam}
|
|
867
946
|
ORDER BY ${distanceExpr}
|
|
868
947
|
LIMIT $2
|
|
869
948
|
)
|
|
@@ -878,14 +957,15 @@ var PgVector = class extends MastraVector {
|
|
|
878
957
|
metadata
|
|
879
958
|
${includeVector ? ", embedding" : ""}
|
|
880
959
|
FROM ${tableName}
|
|
881
|
-
|
|
960
|
+
WHERE namespace = $${namespaceParam}
|
|
961
|
+
${filterClause ? `AND (${filterClause})` : ""}
|
|
882
962
|
)
|
|
883
963
|
SELECT *
|
|
884
964
|
FROM vector_scores
|
|
885
965
|
WHERE score > $1
|
|
886
966
|
ORDER BY score DESC
|
|
887
967
|
LIMIT $2`;
|
|
888
|
-
const result = await client.query(query, filterValues);
|
|
968
|
+
const result = await client.query(query, [...filterValues, namespace]);
|
|
889
969
|
await client.query("COMMIT");
|
|
890
970
|
return result.rows.map(({ id, score, metadata, embedding }) => ({
|
|
891
971
|
id,
|
|
@@ -907,7 +987,7 @@ var PgVector = class extends MastraVector {
|
|
|
907
987
|
client.release();
|
|
908
988
|
}
|
|
909
989
|
}
|
|
910
|
-
async upsert({ indexName, vectors, metadata, ids, deleteFilter }) {
|
|
990
|
+
async upsert({ indexName, vectors, metadata, ids, deleteFilter, namespace = DEFAULT_NAMESPACE }) {
|
|
911
991
|
validateUpsertInput("PG", vectors, metadata, ids);
|
|
912
992
|
const { tableName } = this.getTableName(indexName);
|
|
913
993
|
const client = await this.pool.connect();
|
|
@@ -922,8 +1002,8 @@ var PgVector = class extends MastraVector {
|
|
|
922
1002
|
const { sql: filterQuery, values: filterValues } = buildDeleteFilterQuery(this.transformFilter(deleteFilter));
|
|
923
1003
|
const whereClause = filterQuery.trim().replace(/^WHERE\s+/i, "");
|
|
924
1004
|
if (whereClause) {
|
|
925
|
-
const deleteQuery = `DELETE FROM ${tableName} WHERE ${whereClause}`;
|
|
926
|
-
const result = await client.query(deleteQuery, filterValues);
|
|
1005
|
+
const deleteQuery = `DELETE FROM ${tableName} WHERE namespace = $${filterValues.length + 1} AND (${whereClause})`;
|
|
1006
|
+
const result = await client.query(deleteQuery, [...filterValues, namespace]);
|
|
927
1007
|
this.logger?.debug(`Deleted ${result.rowCount || 0} vectors before upsert`, {
|
|
928
1008
|
indexName,
|
|
929
1009
|
deletedCount: result.rowCount || 0
|
|
@@ -931,15 +1011,15 @@ var PgVector = class extends MastraVector {
|
|
|
931
1011
|
}
|
|
932
1012
|
}
|
|
933
1013
|
const vectorIds = ids || vectors.map(() => crypto.randomUUID());
|
|
934
|
-
const indexInfo = await this.
|
|
1014
|
+
const indexInfo = await this.getIndexMetadata({ indexName });
|
|
935
1015
|
const qualifiedVectorType = this.getVectorTypeName(indexInfo.vectorType, indexInfo.dimension);
|
|
936
1016
|
const ops = this.getVectorOps(indexInfo.vectorType, indexInfo.metric ?? "cosine");
|
|
937
1017
|
if (new Set(vectorIds).size !== vectorIds.length) for (let i = 0; i < vectors.length; i++) {
|
|
938
1018
|
const vectorStr = ops.formatVector(vectors[i], indexInfo.dimension);
|
|
939
1019
|
const query = `
|
|
940
|
-
INSERT INTO ${tableName} (vector_id, embedding, metadata)
|
|
941
|
-
VALUES ($1, $2::${qualifiedVectorType}, $3::jsonb)
|
|
942
|
-
ON CONFLICT (vector_id)
|
|
1020
|
+
INSERT INTO ${tableName} (vector_id, embedding, metadata, namespace)
|
|
1021
|
+
VALUES ($1, $2::${qualifiedVectorType}, $3::jsonb, $4)
|
|
1022
|
+
ON CONFLICT (namespace, vector_id)
|
|
943
1023
|
DO UPDATE SET
|
|
944
1024
|
embedding = $2::${qualifiedVectorType},
|
|
945
1025
|
metadata = $3::jsonb
|
|
@@ -947,7 +1027,8 @@ var PgVector = class extends MastraVector {
|
|
|
947
1027
|
await client.query(query, [
|
|
948
1028
|
vectorIds[i],
|
|
949
1029
|
vectorStr,
|
|
950
|
-
JSON.stringify(metadata?.[i] || {})
|
|
1030
|
+
JSON.stringify(metadata?.[i] || {}),
|
|
1031
|
+
namespace
|
|
951
1032
|
]);
|
|
952
1033
|
}
|
|
953
1034
|
else for (let start = 0; start < vectors.length; start += MAX_UPSERT_ROWS_PER_STATEMENT) {
|
|
@@ -956,13 +1037,13 @@ var PgVector = class extends MastraVector {
|
|
|
956
1037
|
const values = [];
|
|
957
1038
|
for (let i = start; i < end; i++) {
|
|
958
1039
|
const base = values.length;
|
|
959
|
-
rows.push(`($${base + 1}, $${base + 2}::${qualifiedVectorType}, $${base + 3}::jsonb)`);
|
|
960
|
-
values.push(vectorIds[i], ops.formatVector(vectors[i], indexInfo.dimension), JSON.stringify(metadata?.[i] || {}));
|
|
1040
|
+
rows.push(`($${base + 1}, $${base + 2}::${qualifiedVectorType}, $${base + 3}::jsonb, $${base + 4})`);
|
|
1041
|
+
values.push(vectorIds[i], ops.formatVector(vectors[i], indexInfo.dimension), JSON.stringify(metadata?.[i] || {}), namespace);
|
|
961
1042
|
}
|
|
962
1043
|
const query = `
|
|
963
|
-
INSERT INTO ${tableName} (vector_id, embedding, metadata)
|
|
1044
|
+
INSERT INTO ${tableName} (vector_id, embedding, metadata, namespace)
|
|
964
1045
|
VALUES ${rows.join(", ")}
|
|
965
|
-
ON CONFLICT (vector_id)
|
|
1046
|
+
ON CONFLICT (namespace, vector_id)
|
|
966
1047
|
DO UPDATE SET
|
|
967
1048
|
embedding = EXCLUDED.embedding,
|
|
968
1049
|
metadata = EXCLUDED.metadata
|
|
@@ -1082,9 +1163,9 @@ var PgVector = class extends MastraVector {
|
|
|
1082
1163
|
vectorType,
|
|
1083
1164
|
metadataIndexes
|
|
1084
1165
|
});
|
|
1085
|
-
if (this.cachedIndexExists(indexName, indexCacheKey)) return;
|
|
1166
|
+
if (this.cachedIndexExists(indexName, indexCacheKey) && this.namespaceReadyIndexes.has(indexName)) return;
|
|
1086
1167
|
await this.getMutexByName(`create-${indexName}`).runExclusive(async () => {
|
|
1087
|
-
if (this.cachedIndexExists(indexName, indexCacheKey)) return;
|
|
1168
|
+
if (this.cachedIndexExists(indexName, indexCacheKey) && this.namespaceReadyIndexes.has(indexName)) return;
|
|
1088
1169
|
const client = await this.pool.connect();
|
|
1089
1170
|
try {
|
|
1090
1171
|
await this.setupSchema(client);
|
|
@@ -1128,12 +1209,15 @@ var PgVector = class extends MastraVector {
|
|
|
1128
1209
|
await client.query(`
|
|
1129
1210
|
CREATE TABLE IF NOT EXISTS ${tableName} (
|
|
1130
1211
|
id SERIAL PRIMARY KEY,
|
|
1131
|
-
vector_id TEXT
|
|
1212
|
+
vector_id TEXT NOT NULL,
|
|
1132
1213
|
embedding ${qualifiedVectorType}(${dimension}),
|
|
1133
|
-
metadata JSONB DEFAULT '{}'::jsonb
|
|
1214
|
+
metadata JSONB DEFAULT '{}'::jsonb,
|
|
1215
|
+
namespace VARCHAR(255) NOT NULL DEFAULT '${DEFAULT_NAMESPACE}'
|
|
1134
1216
|
);
|
|
1135
1217
|
`);
|
|
1218
|
+
await this.ensureNamespaceSchema(indexName, client);
|
|
1136
1219
|
this.createdIndexes.set(indexName, indexCacheKey);
|
|
1220
|
+
this.namespaceReadyIndexes.add(indexName);
|
|
1137
1221
|
this.indexVectorTypes.set(indexName, vectorType);
|
|
1138
1222
|
if (buildIndex) await this.setupIndex({
|
|
1139
1223
|
indexName,
|
|
@@ -1144,6 +1228,7 @@ var PgVector = class extends MastraVector {
|
|
|
1144
1228
|
if (metadataIndexes?.length) await this.createMetadataIndexes(tableName, indexName, metadataIndexes);
|
|
1145
1229
|
} catch (error) {
|
|
1146
1230
|
this.createdIndexes.delete(indexName);
|
|
1231
|
+
this.namespaceReadyIndexes.delete(indexName);
|
|
1147
1232
|
this.indexVectorTypes.delete(indexName);
|
|
1148
1233
|
throw error;
|
|
1149
1234
|
} finally {
|
|
@@ -1214,7 +1299,7 @@ var PgVector = class extends MastraVector {
|
|
|
1214
1299
|
let existingIndexInfo = null;
|
|
1215
1300
|
let dimension = 0;
|
|
1216
1301
|
try {
|
|
1217
|
-
existingIndexInfo = await this.
|
|
1302
|
+
existingIndexInfo = await this.getIndexMetadata({ indexName });
|
|
1218
1303
|
dimension = existingIndexInfo.dimension;
|
|
1219
1304
|
if (isConfigEmpty && existingIndexInfo.metric === metric) if (existingIndexInfo.type === "flat") this.logger?.debug(`No index exists for ${vectorIndexName}, will create default ivfflat index`);
|
|
1220
1305
|
else {
|
|
@@ -1249,12 +1334,12 @@ var PgVector = class extends MastraVector {
|
|
|
1249
1334
|
}
|
|
1250
1335
|
this.logger?.info(`Index ${vectorIndexName} configuration changed, rebuilding index`);
|
|
1251
1336
|
await client.query(`DROP INDEX IF EXISTS ${vectorIndexName}`);
|
|
1252
|
-
this.
|
|
1337
|
+
this.invalidateIndexCaches(indexName);
|
|
1253
1338
|
} catch {
|
|
1254
1339
|
this.logger?.debug(`Index ${indexName} doesn't exist yet, will create it`);
|
|
1255
1340
|
}
|
|
1256
1341
|
if (indexType === "flat") {
|
|
1257
|
-
this.
|
|
1342
|
+
this.invalidateIndexCaches(indexName);
|
|
1258
1343
|
return;
|
|
1259
1344
|
}
|
|
1260
1345
|
await this.ensureSearchPath(client);
|
|
@@ -1404,6 +1489,19 @@ var PgVector = class extends MastraVector {
|
|
|
1404
1489
|
* @returns A promise that resolves to the index statistics including dimension, count and metric
|
|
1405
1490
|
*/
|
|
1406
1491
|
async describeIndex({ indexName }) {
|
|
1492
|
+
const metadata = await this.describeIndexMetadata({ indexName });
|
|
1493
|
+
const count = await this.countIndexRows({ indexName });
|
|
1494
|
+
return {
|
|
1495
|
+
...metadata,
|
|
1496
|
+
count
|
|
1497
|
+
};
|
|
1498
|
+
}
|
|
1499
|
+
/**
|
|
1500
|
+
* Reads the index metadata that lives in the Postgres catalog. Unlike
|
|
1501
|
+
* {@link describeIndex} it issues no `COUNT(*)`, so its cost does not grow with the
|
|
1502
|
+
* number of rows in the table.
|
|
1503
|
+
*/
|
|
1504
|
+
async describeIndexMetadata({ indexName }) {
|
|
1407
1505
|
const client = await this.pool.connect();
|
|
1408
1506
|
try {
|
|
1409
1507
|
const { tableName } = this.getTableName(indexName);
|
|
@@ -1423,10 +1521,6 @@ var PgVector = class extends MastraVector {
|
|
|
1423
1521
|
FROM pg_attribute
|
|
1424
1522
|
WHERE attrelid = $1::regclass
|
|
1425
1523
|
AND attname = 'embedding';
|
|
1426
|
-
`;
|
|
1427
|
-
const countQuery = `
|
|
1428
|
-
SELECT COUNT(*) as count
|
|
1429
|
-
FROM ${tableName};
|
|
1430
1524
|
`;
|
|
1431
1525
|
const indexQuery = `
|
|
1432
1526
|
SELECT
|
|
@@ -1442,7 +1536,6 @@ var PgVector = class extends MastraVector {
|
|
|
1442
1536
|
AND n.nspname = $2;
|
|
1443
1537
|
`;
|
|
1444
1538
|
const dimResult = await client.query(dimensionQuery, [tableName]);
|
|
1445
|
-
const countResult = await client.query(countQuery);
|
|
1446
1539
|
const { index_method, index_def, operator_class } = (await client.query(indexQuery, [`${indexName}_vector_idx`, this.schema || "public"])).rows[0] || {
|
|
1447
1540
|
index_method: "flat",
|
|
1448
1541
|
index_def: "",
|
|
@@ -1461,7 +1554,6 @@ var PgVector = class extends MastraVector {
|
|
|
1461
1554
|
}
|
|
1462
1555
|
return {
|
|
1463
1556
|
dimension: dimResult.rows[0].dimension,
|
|
1464
|
-
count: parseInt(countResult.rows[0].count),
|
|
1465
1557
|
metric,
|
|
1466
1558
|
type: index_method,
|
|
1467
1559
|
vectorType,
|
|
@@ -1481,14 +1573,41 @@ var PgVector = class extends MastraVector {
|
|
|
1481
1573
|
client.release();
|
|
1482
1574
|
}
|
|
1483
1575
|
}
|
|
1576
|
+
/**
|
|
1577
|
+
* Exact row count for an index. This is a full scan of the table, so it is only issued
|
|
1578
|
+
* for the public {@link describeIndex}, never from an internal code path.
|
|
1579
|
+
*/
|
|
1580
|
+
async countIndexRows({ indexName }) {
|
|
1581
|
+
const client = await this.pool.connect();
|
|
1582
|
+
try {
|
|
1583
|
+
const { tableName } = this.getTableName(indexName);
|
|
1584
|
+
const countResult = await client.query(`
|
|
1585
|
+
SELECT COUNT(*) as count
|
|
1586
|
+
FROM ${tableName};
|
|
1587
|
+
`);
|
|
1588
|
+
return parseInt(countResult.rows[0].count);
|
|
1589
|
+
} catch (e) {
|
|
1590
|
+
const mastraError = new MastraError({
|
|
1591
|
+
id: createVectorErrorId("PG", "DESCRIBE_INDEX", "FAILED"),
|
|
1592
|
+
domain: ErrorDomain.MASTRA_VECTOR,
|
|
1593
|
+
category: ErrorCategory.THIRD_PARTY,
|
|
1594
|
+
details: { indexName }
|
|
1595
|
+
}, e);
|
|
1596
|
+
this.logger?.trackException(mastraError);
|
|
1597
|
+
throw mastraError;
|
|
1598
|
+
} finally {
|
|
1599
|
+
client.release();
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1484
1602
|
async deleteIndex({ indexName }) {
|
|
1485
1603
|
const client = await this.pool.connect();
|
|
1486
1604
|
try {
|
|
1487
1605
|
const { tableName } = this.getTableName(indexName);
|
|
1488
1606
|
await client.query(`DROP TABLE IF EXISTS ${tableName} CASCADE`);
|
|
1489
1607
|
this.createdIndexes.delete(indexName);
|
|
1608
|
+
this.namespaceReadyIndexes.delete(indexName);
|
|
1490
1609
|
this.indexVectorTypes.delete(indexName);
|
|
1491
|
-
this.
|
|
1610
|
+
this.invalidateIndexCaches(indexName);
|
|
1492
1611
|
} catch (error) {
|
|
1493
1612
|
await client.query("ROLLBACK");
|
|
1494
1613
|
const mastraError = new MastraError({
|
|
@@ -1538,7 +1657,7 @@ var PgVector = class extends MastraVector {
|
|
|
1538
1657
|
* @returns A promise that resolves when the update is complete.
|
|
1539
1658
|
* @throws Will throw an error if no updates are provided or if the update operation fails.
|
|
1540
1659
|
*/
|
|
1541
|
-
async updateVector({ indexName, id, filter, update }) {
|
|
1660
|
+
async updateVector({ indexName, id, filter, update, namespace = DEFAULT_NAMESPACE }) {
|
|
1542
1661
|
let client;
|
|
1543
1662
|
try {
|
|
1544
1663
|
if (!update.vector && !update.metadata) throw new Error("No updates provided");
|
|
@@ -1559,7 +1678,7 @@ var PgVector = class extends MastraVector {
|
|
|
1559
1678
|
client = await this.pool.connect();
|
|
1560
1679
|
await this.ensureSearchPath(client);
|
|
1561
1680
|
const { tableName } = this.getTableName(indexName);
|
|
1562
|
-
const indexInfo = await this.
|
|
1681
|
+
const indexInfo = await this.getIndexMetadata({ indexName });
|
|
1563
1682
|
const qualifiedVectorType = this.getVectorTypeName(indexInfo.vectorType, indexInfo.dimension);
|
|
1564
1683
|
const ops = this.getVectorOps(indexInfo.vectorType, indexInfo.metric ?? "cosine");
|
|
1565
1684
|
let updateParts = [];
|
|
@@ -1578,8 +1697,11 @@ var PgVector = class extends MastraVector {
|
|
|
1578
1697
|
if (updateParts.length === 0) return;
|
|
1579
1698
|
let whereClause;
|
|
1580
1699
|
let whereValues;
|
|
1700
|
+
const namespaceIndex = valueIndex;
|
|
1701
|
+
values.push(namespace);
|
|
1702
|
+
valueIndex++;
|
|
1581
1703
|
if (id) {
|
|
1582
|
-
whereClause = `vector_id = $${valueIndex}`;
|
|
1704
|
+
whereClause = `namespace = $${namespaceIndex} AND vector_id = $${valueIndex}`;
|
|
1583
1705
|
whereValues = [id];
|
|
1584
1706
|
} else {
|
|
1585
1707
|
if (!filter || Object.keys(filter).length === 0) throw new MastraError({
|
|
@@ -1604,6 +1726,7 @@ var PgVector = class extends MastraVector {
|
|
|
1604
1726
|
whereClause = whereClause.replace(/\$(\d+)/g, (match, num) => {
|
|
1605
1727
|
return `$${parseInt(num) + valueIndex - 1}`;
|
|
1606
1728
|
});
|
|
1729
|
+
whereClause = `namespace = $${namespaceIndex} AND (${whereClause})`;
|
|
1607
1730
|
whereValues = filterValues;
|
|
1608
1731
|
}
|
|
1609
1732
|
const query = `
|
|
@@ -1643,16 +1766,16 @@ var PgVector = class extends MastraVector {
|
|
|
1643
1766
|
* @returns A promise that resolves when the deletion is complete.
|
|
1644
1767
|
* @throws Will throw an error if the deletion operation fails.
|
|
1645
1768
|
*/
|
|
1646
|
-
async deleteVector({ indexName, id }) {
|
|
1769
|
+
async deleteVector({ indexName, id, namespace = DEFAULT_NAMESPACE }) {
|
|
1647
1770
|
let client;
|
|
1648
1771
|
try {
|
|
1649
1772
|
client = await this.pool.connect();
|
|
1650
1773
|
const { tableName } = this.getTableName(indexName);
|
|
1651
1774
|
const query = `
|
|
1652
1775
|
DELETE FROM ${tableName}
|
|
1653
|
-
WHERE vector_id = $1
|
|
1776
|
+
WHERE vector_id = $1 AND namespace = $2
|
|
1654
1777
|
`;
|
|
1655
|
-
await client.query(query, [id]);
|
|
1778
|
+
await client.query(query, [id, namespace]);
|
|
1656
1779
|
} catch (error) {
|
|
1657
1780
|
const mastraError = new MastraError({
|
|
1658
1781
|
id: createVectorErrorId("PG", "DELETE_VECTOR", "FAILED"),
|
|
@@ -1676,14 +1799,15 @@ var PgVector = class extends MastraVector {
|
|
|
1676
1799
|
* @returns A promise that resolves when the deletion is complete.
|
|
1677
1800
|
* @throws Will throw an error if the deletion operation fails.
|
|
1678
1801
|
*/
|
|
1679
|
-
async deleteVectors({ indexName, filter, ids }) {
|
|
1802
|
+
async deleteVectors({ indexName, filter, ids, namespace }) {
|
|
1680
1803
|
let client;
|
|
1804
|
+
const effectiveNamespace = namespace ?? DEFAULT_NAMESPACE;
|
|
1681
1805
|
try {
|
|
1682
1806
|
client = await this.pool.connect();
|
|
1683
1807
|
const { tableName } = this.getTableName(indexName);
|
|
1684
|
-
if (!filter && !ids) throw new MastraError({
|
|
1808
|
+
if (!filter && !ids && namespace === void 0) throw new MastraError({
|
|
1685
1809
|
id: createVectorErrorId("PG", "DELETE_VECTORS", "NO_TARGET"),
|
|
1686
|
-
text: "Either filter or ids must be provided",
|
|
1810
|
+
text: "Either filter or ids must be provided, unless an explicit namespace is used",
|
|
1687
1811
|
domain: ErrorDomain.MASTRA_VECTOR,
|
|
1688
1812
|
category: ErrorCategory.USER,
|
|
1689
1813
|
details: { indexName }
|
|
@@ -1705,9 +1829,9 @@ var PgVector = class extends MastraVector {
|
|
|
1705
1829
|
category: ErrorCategory.USER,
|
|
1706
1830
|
details: { indexName }
|
|
1707
1831
|
});
|
|
1708
|
-
query = `DELETE FROM ${tableName} WHERE vector_id IN (${ids.map((_, i) => `$${i + 1}`).join(", ")})`;
|
|
1709
|
-
values = ids;
|
|
1710
|
-
} else {
|
|
1832
|
+
query = `DELETE FROM ${tableName} WHERE vector_id IN (${ids.map((_, i) => `$${i + 1}`).join(", ")}) AND namespace = $${ids.length + 1}`;
|
|
1833
|
+
values = [...ids, effectiveNamespace];
|
|
1834
|
+
} else if (filter) {
|
|
1711
1835
|
if (!filter || Object.keys(filter).length === 0) throw new MastraError({
|
|
1712
1836
|
id: createVectorErrorId("PG", "DELETE_VECTORS", "EMPTY_FILTER"),
|
|
1713
1837
|
text: "Cannot delete with empty filter. Use deleteIndex to delete all vectors.",
|
|
@@ -1727,8 +1851,11 @@ var PgVector = class extends MastraVector {
|
|
|
1727
1851
|
filter: JSON.stringify(filter)
|
|
1728
1852
|
}
|
|
1729
1853
|
});
|
|
1730
|
-
query = `DELETE FROM ${tableName} WHERE ${whereClause}`;
|
|
1731
|
-
values = filterValues;
|
|
1854
|
+
query = `DELETE FROM ${tableName} WHERE namespace = $${filterValues.length + 1} AND (${whereClause})`;
|
|
1855
|
+
values = [...filterValues, effectiveNamespace];
|
|
1856
|
+
} else {
|
|
1857
|
+
query = `DELETE FROM ${tableName} WHERE namespace = $1`;
|
|
1858
|
+
values = [effectiveNamespace];
|
|
1732
1859
|
}
|
|
1733
1860
|
const result = await client.query(query, values);
|
|
1734
1861
|
this.logger?.info(`Deleted ${result.rowCount || 0} vectors from ${indexName}`, {
|
|
@@ -4894,7 +5021,7 @@ var BackgroundTasksPG = class BackgroundTasksPG extends BackgroundTasksStorage {
|
|
|
4894
5021
|
}
|
|
4895
5022
|
});
|
|
4896
5023
|
}
|
|
4897
|
-
async updateTask(taskId, update) {
|
|
5024
|
+
async updateTask(taskId, update, options) {
|
|
4898
5025
|
const setClauses = [];
|
|
4899
5026
|
const params = [];
|
|
4900
5027
|
let paramIdx = 1;
|
|
@@ -4936,10 +5063,15 @@ var BackgroundTasksPG = class BackgroundTasksPG extends BackgroundTasksStorage {
|
|
|
4936
5063
|
const val = update.completedAt?.toISOString() ?? null;
|
|
4937
5064
|
params.push(val, val);
|
|
4938
5065
|
}
|
|
4939
|
-
if (setClauses.length === 0) return;
|
|
5066
|
+
if (setClauses.length === 0) return false;
|
|
4940
5067
|
const table = getTableName$4(getSchemaName$4(this.#schema));
|
|
4941
5068
|
params.push(taskId);
|
|
4942
|
-
|
|
5069
|
+
let where = `"id" = $${paramIdx++}`;
|
|
5070
|
+
if (options?.expectedStatus) {
|
|
5071
|
+
where += ` AND "status" = $${paramIdx}`;
|
|
5072
|
+
params.push(options.expectedStatus);
|
|
5073
|
+
}
|
|
5074
|
+
return ((await this.#db.client.query(`UPDATE ${table} SET ${setClauses.join(", ")} WHERE ${where}`, params)).rowCount ?? 0) > 0;
|
|
4943
5075
|
}
|
|
4944
5076
|
async getTask(taskId) {
|
|
4945
5077
|
const table = getTableName$4(getSchemaName$4(this.#schema));
|
|
@@ -6584,6 +6716,7 @@ var ExperimentsPG = class ExperimentsPG extends ExperimentsStorage {
|
|
|
6584
6716
|
"tags",
|
|
6585
6717
|
"comment",
|
|
6586
6718
|
"toolMockReport",
|
|
6719
|
+
"metadata",
|
|
6587
6720
|
"organizationId",
|
|
6588
6721
|
"projectId",
|
|
6589
6722
|
"attempt"
|
|
@@ -6780,6 +6913,7 @@ var ExperimentsPG = class ExperimentsPG extends ExperimentsStorage {
|
|
|
6780
6913
|
input: safelyParseJSON(row.input),
|
|
6781
6914
|
output: row.output ? safelyParseJSON(row.output) : null,
|
|
6782
6915
|
groundTruth: row.groundTruth ? safelyParseJSON(row.groundTruth) : null,
|
|
6916
|
+
metadata: row.metadata ? safelyParseJSON(row.metadata) : null,
|
|
6783
6917
|
error: row.error ? safelyParseJSON(row.error) : null,
|
|
6784
6918
|
startedAt: ensureDate(row.startedAtZ || row.startedAt),
|
|
6785
6919
|
completedAt: ensureDate(row.completedAtZ || row.completedAt),
|
|
@@ -7088,6 +7222,7 @@ var ExperimentsPG = class ExperimentsPG extends ExperimentsStorage {
|
|
|
7088
7222
|
input: input.input,
|
|
7089
7223
|
output: input.output ?? null,
|
|
7090
7224
|
groundTruth: input.groundTruth ?? null,
|
|
7225
|
+
metadata: input.metadata ?? null,
|
|
7091
7226
|
error: input.error ?? null,
|
|
7092
7227
|
startedAt: input.startedAt.toISOString(),
|
|
7093
7228
|
completedAt: input.completedAt.toISOString(),
|
|
@@ -7110,6 +7245,7 @@ var ExperimentsPG = class ExperimentsPG extends ExperimentsStorage {
|
|
|
7110
7245
|
input: input.input,
|
|
7111
7246
|
output: input.output ?? null,
|
|
7112
7247
|
groundTruth: input.groundTruth ?? null,
|
|
7248
|
+
metadata: input.metadata ?? null,
|
|
7113
7249
|
error: input.error ?? null,
|
|
7114
7250
|
startedAt: input.startedAt,
|
|
7115
7251
|
completedAt: input.completedAt,
|
|
@@ -7146,9 +7282,9 @@ var ExperimentsPG = class ExperimentsPG extends ExperimentsStorage {
|
|
|
7146
7282
|
const id = crypto.randomUUID();
|
|
7147
7283
|
return t.one(`INSERT INTO ${tableName} (
|
|
7148
7284
|
"id", "experimentId", "itemId", "itemDatasetVersion", "organizationId", "projectId",
|
|
7149
|
-
"input", "output", "groundTruth", "error", "startedAt", "completedAt",
|
|
7285
|
+
"input", "output", "groundTruth", "metadata", "error", "startedAt", "completedAt",
|
|
7150
7286
|
"retryCount", "attempt", "traceId", "status", "tags", "toolMockReport", "createdAt"
|
|
7151
|
-
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19)
|
|
7287
|
+
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20)
|
|
7152
7288
|
RETURNING *`, [
|
|
7153
7289
|
id,
|
|
7154
7290
|
input.experimentId,
|
|
@@ -7159,6 +7295,7 @@ var ExperimentsPG = class ExperimentsPG extends ExperimentsStorage {
|
|
|
7159
7295
|
JSON.stringify(input.input),
|
|
7160
7296
|
input.output != null ? JSON.stringify(input.output) : null,
|
|
7161
7297
|
input.groundTruth != null ? JSON.stringify(input.groundTruth) : null,
|
|
7298
|
+
input.metadata != null ? JSON.stringify(input.metadata) : null,
|
|
7162
7299
|
input.error != null ? JSON.stringify(input.error) : null,
|
|
7163
7300
|
input.startedAt.toISOString(),
|
|
7164
7301
|
input.completedAt.toISOString(),
|
|
@@ -7173,9 +7310,9 @@ var ExperimentsPG = class ExperimentsPG extends ExperimentsStorage {
|
|
|
7173
7310
|
}
|
|
7174
7311
|
return t.one(`UPDATE ${tableName} SET
|
|
7175
7312
|
"itemDatasetVersion" = $2, "organizationId" = $3, "projectId" = $4,
|
|
7176
|
-
"input" = $5, "output" = $6, "groundTruth" = $7, "
|
|
7177
|
-
"startedAt" = $
|
|
7178
|
-
"traceId" = $
|
|
7313
|
+
"input" = $5, "output" = $6, "groundTruth" = $7, "metadata" = $8, "error" = $9,
|
|
7314
|
+
"startedAt" = $10, "completedAt" = $11, "retryCount" = $12, "attempt" = $13,
|
|
7315
|
+
"traceId" = $14, "status" = $15, "tags" = $16, "toolMockReport" = $17
|
|
7179
7316
|
WHERE "id" = $1 RETURNING *`, [
|
|
7180
7317
|
existing.id,
|
|
7181
7318
|
input.itemDatasetVersion ?? null,
|
|
@@ -7184,6 +7321,7 @@ var ExperimentsPG = class ExperimentsPG extends ExperimentsStorage {
|
|
|
7184
7321
|
JSON.stringify(input.input),
|
|
7185
7322
|
input.output != null ? JSON.stringify(input.output) : null,
|
|
7186
7323
|
input.groundTruth != null ? JSON.stringify(input.groundTruth) : null,
|
|
7324
|
+
input.metadata != null ? JSON.stringify(input.metadata) : null,
|
|
7187
7325
|
input.error != null ? JSON.stringify(input.error) : null,
|
|
7188
7326
|
input.startedAt.toISOString(),
|
|
7189
7327
|
input.completedAt.toISOString(),
|
|
@@ -13567,6 +13705,11 @@ const SPAN_EVENT_COLUMNS = [
|
|
|
13567
13705
|
type: "boolean",
|
|
13568
13706
|
defaultSql: "false"
|
|
13569
13707
|
},
|
|
13708
|
+
{
|
|
13709
|
+
name: "isPending",
|
|
13710
|
+
type: "boolean",
|
|
13711
|
+
defaultSql: "false"
|
|
13712
|
+
},
|
|
13570
13713
|
{
|
|
13571
13714
|
name: "startedAt",
|
|
13572
13715
|
type: "timestamptz"
|
|
@@ -13863,6 +14006,7 @@ const SPAN_LIGHT_SELECT_COLUMN_NAMES = [
|
|
|
13863
14006
|
"spanType",
|
|
13864
14007
|
"error",
|
|
13865
14008
|
"isEvent",
|
|
14009
|
+
"isPending",
|
|
13866
14010
|
"startedAt",
|
|
13867
14011
|
"endedAt"
|
|
13868
14012
|
];
|
|
@@ -14257,6 +14401,26 @@ function allTableDDL(schema, mode) {
|
|
|
14257
14401
|
discoveryTableDDL(schema)
|
|
14258
14402
|
];
|
|
14259
14403
|
}
|
|
14404
|
+
/**
|
|
14405
|
+
* Additive column migrations for tables created by an older version of this
|
|
14406
|
+
* schema. `CREATE TABLE IF NOT EXISTS` is a no-op on an existing table, so new
|
|
14407
|
+
* columns have to be added out of band.
|
|
14408
|
+
*
|
|
14409
|
+
* `ALTER TABLE` takes an AccessExclusiveLock on the partitioned parent, so
|
|
14410
|
+
* callers must check {@link columnExistsSQL} first and only run the statement
|
|
14411
|
+
* when the column is genuinely missing. On Postgres 11+ the non-volatile
|
|
14412
|
+
* default does not rewrite the table, and the ALTER cascades to partitions.
|
|
14413
|
+
*/
|
|
14414
|
+
function additiveColumns(schema) {
|
|
14415
|
+
return [{
|
|
14416
|
+
table: TABLE_SPAN_EVENTS,
|
|
14417
|
+
column: "isPending",
|
|
14418
|
+
ddl: `ALTER TABLE ${qualifiedTable(schema, TABLE_SPAN_EVENTS)} ADD COLUMN IF NOT EXISTS "isPending" boolean NOT NULL DEFAULT false`
|
|
14419
|
+
}];
|
|
14420
|
+
}
|
|
14421
|
+
/** Existence probe for an additive column. Params: schema, table, column. */
|
|
14422
|
+
const columnExistsSQL = `SELECT 1 FROM information_schema.columns
|
|
14423
|
+
WHERE table_schema = $1 AND table_name = $2 AND column_name = $3`;
|
|
14260
14424
|
/** Index CREATEs. Safe to run repeatedly. */
|
|
14261
14425
|
function allIndexDDL(schema) {
|
|
14262
14426
|
return tableIndexes().map((spec) => indexDDL(schema, spec));
|
|
@@ -14264,6 +14428,7 @@ function allIndexDDL(schema) {
|
|
|
14264
14428
|
//#endregion
|
|
14265
14429
|
//#region src/storage/domains/observability/v-next/discovery.ts
|
|
14266
14430
|
const DEFAULT_TTL_SECONDS = 300;
|
|
14431
|
+
const DEFAULT_LOOKBACK_SECONDS = 720 * 60 * 60;
|
|
14267
14432
|
/** All signal tables that contain a column. Used by cross-signal discovery. */
|
|
14268
14433
|
const SIGNAL_TABLES_WITH_CONTEXT = [
|
|
14269
14434
|
TABLE_SPAN_EVENTS,
|
|
@@ -14328,22 +14493,71 @@ function startOrJoinRefresh(dedupeKey, cacheKey, refresh, upsert, logger) {
|
|
|
14328
14493
|
async function readWithRefresh(client, schema, cacheKey, refresh, ttlSeconds, logger) {
|
|
14329
14494
|
const table = qualifiedTable(schema, TABLE_DISCOVERY);
|
|
14330
14495
|
const row = await client.oneOrNone(`SELECT "values", "refreshedAt" FROM ${table} WHERE "cacheKey" = $1`, [cacheKey]);
|
|
14331
|
-
const
|
|
14332
|
-
|
|
14333
|
-
const refreshing = startOrJoinRefresh(`${schema}:${cacheKey}`, cacheKey, refresh, (values) => upsertCache(client, schema, cacheKey, values), logger);
|
|
14496
|
+
const dedupeKey = `${schema}:${cacheKey}`;
|
|
14497
|
+
const startRefresh = () => startOrJoinRefresh(dedupeKey, cacheKey, refresh, (values) => upsertCache(client, schema, cacheKey, values), logger);
|
|
14334
14498
|
if (ttlSeconds <= 0) try {
|
|
14335
|
-
return await
|
|
14499
|
+
return await startRefresh();
|
|
14336
14500
|
} catch {
|
|
14337
14501
|
return row?.values ?? [];
|
|
14338
14502
|
}
|
|
14503
|
+
const refreshedAtMs = row ? new Date(row.refreshedAt).getTime() : 0;
|
|
14504
|
+
if (!(!row || Date.now() - refreshedAtMs > ttlSeconds * 1e3)) return row.values;
|
|
14339
14505
|
if (!row) try {
|
|
14340
|
-
return await
|
|
14506
|
+
return await startRefresh();
|
|
14341
14507
|
} catch {
|
|
14342
14508
|
return [];
|
|
14343
14509
|
}
|
|
14344
|
-
|
|
14510
|
+
const previousRefreshedAt = row.refreshedAt;
|
|
14511
|
+
claimRefresh(client, schema, cacheKey, ttlSeconds).then((claimedAt) => {
|
|
14512
|
+
if (!claimedAt) return;
|
|
14513
|
+
return startRefresh().then(() => {}, () => releaseClaim(client, schema, cacheKey, previousRefreshedAt, claimedAt));
|
|
14514
|
+
}).catch(() => {});
|
|
14345
14515
|
return row.values;
|
|
14346
14516
|
}
|
|
14517
|
+
/**
|
|
14518
|
+
* Try to become the process that refreshes `cacheKey`.
|
|
14519
|
+
*
|
|
14520
|
+
* The conditional UPDATE re-checks staleness in the database and stamps
|
|
14521
|
+
* `refreshedAt` in the same statement, so exactly one caller can transition a
|
|
14522
|
+
* given cache row out of the stale window — losers see zero rows updated and
|
|
14523
|
+
* skip their refresh. This is the cross-process counterpart to
|
|
14524
|
+
* `inFlightRefreshes`: N frontends going stale together run one refresh
|
|
14525
|
+
* instead of N.
|
|
14526
|
+
*
|
|
14527
|
+
* It reuses the cache table rather than an advisory lock because `DbClient`
|
|
14528
|
+
* is pool-backed, so a transaction-scoped lock would mean threading explicit
|
|
14529
|
+
* transaction handling through the discovery read path.
|
|
14530
|
+
*
|
|
14531
|
+
* The stamp is provisional in both directions: a completed refresh overwrites
|
|
14532
|
+
* it via `upsertCache`, and a failed one restores it via `releaseClaim`.
|
|
14533
|
+
*
|
|
14534
|
+
* Returns the exact provisional stamp written by the claim (or `null` when
|
|
14535
|
+
* the claim was lost) so `releaseClaim` can restore the row only while it
|
|
14536
|
+
* still holds this claim's stamp.
|
|
14537
|
+
*/
|
|
14538
|
+
async function claimRefresh(client, schema, cacheKey, ttlSeconds) {
|
|
14539
|
+
const table = qualifiedTable(schema, TABLE_DISCOVERY);
|
|
14540
|
+
return (await client.oneOrNone(`UPDATE ${table} SET "refreshedAt" = NOW()
|
|
14541
|
+
WHERE "cacheKey" = $1 AND "refreshedAt" <= NOW() - ($2 || ' seconds')::interval
|
|
14542
|
+
RETURNING "refreshedAt"::text AS "refreshedAt"`, [cacheKey, Math.floor(ttlSeconds)]))?.refreshedAt ?? null;
|
|
14543
|
+
}
|
|
14544
|
+
/**
|
|
14545
|
+
* Hand back a claim whose refresh failed by restoring the timestamp the
|
|
14546
|
+
* claiming reader observed. Guarded on the row still holding this claim's
|
|
14547
|
+
* exact provisional stamp so a refresh that completed in the meantime
|
|
14548
|
+
* (another process, or a later claim) isn't dragged backwards into staleness.
|
|
14549
|
+
*/
|
|
14550
|
+
async function releaseClaim(client, schema, cacheKey, previousRefreshedAt, claimedAt) {
|
|
14551
|
+
const table = qualifiedTable(schema, TABLE_DISCOVERY);
|
|
14552
|
+
try {
|
|
14553
|
+
await client.query(`UPDATE ${table} SET "refreshedAt" = $2
|
|
14554
|
+
WHERE "cacheKey" = $1 AND "refreshedAt" = $3::timestamptz`, [
|
|
14555
|
+
cacheKey,
|
|
14556
|
+
previousRefreshedAt,
|
|
14557
|
+
claimedAt
|
|
14558
|
+
]);
|
|
14559
|
+
} catch {}
|
|
14560
|
+
}
|
|
14347
14561
|
async function upsertCache(client, schema, cacheKey, values) {
|
|
14348
14562
|
const table = qualifiedTable(schema, TABLE_DISCOVERY);
|
|
14349
14563
|
await client.query(`INSERT INTO ${table} ("cacheKey", "refreshedAt", "values")
|
|
@@ -14352,61 +14566,95 @@ async function upsertCache(client, schema, cacheKey, values) {
|
|
|
14352
14566
|
"refreshedAt" = EXCLUDED."refreshedAt",
|
|
14353
14567
|
"values" = EXCLUDED."values"`, [cacheKey, JSON.stringify(values)]);
|
|
14354
14568
|
}
|
|
14355
|
-
|
|
14569
|
+
/**
|
|
14570
|
+
* Time predicate that bounds a discovery refresh to recent events.
|
|
14571
|
+
*
|
|
14572
|
+
* Each signal table is range-partitioned on its own time column
|
|
14573
|
+
* (`SIGNAL_TIME_COLUMN`: `endedAt` for spans, `timestamp` for everything
|
|
14574
|
+
* else), so the predicate has to be built per table rather than shared across
|
|
14575
|
+
* a UNION. Bounding on that exact column is what lets the planner prune
|
|
14576
|
+
* partitions/chunks instead of scanning all history.
|
|
14577
|
+
*
|
|
14578
|
+
* The interval is interpolated rather than bound to a parameter on purpose:
|
|
14579
|
+
* the UNION branches in `distinctAcrossTables` and `getTags` all reference the
|
|
14580
|
+
* same positional placeholders, so adding a per-branch parameter would break
|
|
14581
|
+
* `$N` numbering. Safety comes from coercing to a finite integer here — the
|
|
14582
|
+
* value originates from `DiscoveryConfig`, never from user input.
|
|
14583
|
+
*/
|
|
14584
|
+
function lookbackPredicate(table, lookbackSeconds) {
|
|
14585
|
+
if (!Number.isFinite(lookbackSeconds) || lookbackSeconds <= 0) return "";
|
|
14586
|
+
const seconds = Math.floor(lookbackSeconds);
|
|
14587
|
+
const timeColumn = SIGNAL_TIME_COLUMN[table];
|
|
14588
|
+
if (!timeColumn) return "";
|
|
14589
|
+
return `AND "${timeColumn}" >= NOW() - INTERVAL '${seconds} seconds'`;
|
|
14590
|
+
}
|
|
14591
|
+
async function distinctAcrossTables(client, schema, column, tables, lookbackSeconds, filterSql = "", filterParams = []) {
|
|
14356
14592
|
const safeColumn = parseSqlIdentifier(column, "column name");
|
|
14357
|
-
const unions = tables.map((t) => `SELECT DISTINCT "${safeColumn}" AS v FROM ${qualifiedTable(schema, t)} WHERE "${safeColumn}" IS NOT NULL AND "${safeColumn}" <> '' ${filterSql}`).join(" UNION ");
|
|
14593
|
+
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 ");
|
|
14358
14594
|
return (await client.manyOrNone(`SELECT v FROM (${unions}) sub ORDER BY v`, filterParams)).map((r) => r.v);
|
|
14359
14595
|
}
|
|
14360
14596
|
async function getEntityTypes(client, schema, _args, config) {
|
|
14361
|
-
|
|
14597
|
+
const ttl = config.ttlSeconds ?? DEFAULT_TTL_SECONDS;
|
|
14598
|
+
const lookback = config.lookbackSeconds ?? DEFAULT_LOOKBACK_SECONDS;
|
|
14599
|
+
return { entityTypes: await readWithRefresh(client, schema, "entity_types", () => distinctAcrossTables(client, schema, "entityType", ENTITY_DISCOVERY_TABLES, lookback), ttl, config.logger) };
|
|
14362
14600
|
}
|
|
14363
14601
|
async function getEntityNames(client, schema, args, config) {
|
|
14364
14602
|
const ttl = config.ttlSeconds ?? DEFAULT_TTL_SECONDS;
|
|
14603
|
+
const lookback = config.lookbackSeconds ?? DEFAULT_LOOKBACK_SECONDS;
|
|
14365
14604
|
const cacheKey = args.entityType ? `entity_names:${args.entityType}` : "entity_names";
|
|
14366
14605
|
const filterSql = args.entityType ? `AND "entityType" = $1` : "";
|
|
14367
14606
|
const filterParams = args.entityType ? [args.entityType] : [];
|
|
14368
|
-
return { names: await readWithRefresh(client, schema, cacheKey, () => distinctAcrossTables(client, schema, "entityName", ENTITY_DISCOVERY_TABLES, filterSql, filterParams), ttl, config.logger) };
|
|
14607
|
+
return { names: await readWithRefresh(client, schema, cacheKey, () => distinctAcrossTables(client, schema, "entityName", ENTITY_DISCOVERY_TABLES, lookback, filterSql, filterParams), ttl, config.logger) };
|
|
14369
14608
|
}
|
|
14370
14609
|
async function getServiceNames(client, schema, _args, config) {
|
|
14371
|
-
|
|
14610
|
+
const ttl = config.ttlSeconds ?? DEFAULT_TTL_SECONDS;
|
|
14611
|
+
const lookback = config.lookbackSeconds ?? DEFAULT_LOOKBACK_SECONDS;
|
|
14612
|
+
return { serviceNames: await readWithRefresh(client, schema, "service_names", () => distinctAcrossTables(client, schema, "serviceName", SIGNAL_TABLES_WITH_CONTEXT, lookback), ttl, config.logger) };
|
|
14372
14613
|
}
|
|
14373
14614
|
async function getEnvironments(client, schema, _args, config) {
|
|
14374
|
-
|
|
14615
|
+
const ttl = config.ttlSeconds ?? DEFAULT_TTL_SECONDS;
|
|
14616
|
+
const lookback = config.lookbackSeconds ?? DEFAULT_LOOKBACK_SECONDS;
|
|
14617
|
+
return { environments: await readWithRefresh(client, schema, "environments", () => distinctAcrossTables(client, schema, "environment", SIGNAL_TABLES_WITH_CONTEXT, lookback), ttl, config.logger) };
|
|
14375
14618
|
}
|
|
14376
14619
|
async function getTags(client, schema, args, config) {
|
|
14377
14620
|
const ttl = config.ttlSeconds ?? DEFAULT_TTL_SECONDS;
|
|
14621
|
+
const lookback = config.lookbackSeconds ?? DEFAULT_LOOKBACK_SECONDS;
|
|
14378
14622
|
const cacheKey = args.entityType ? `tags:${args.entityType}` : "tags";
|
|
14379
14623
|
const refresh = async () => {
|
|
14380
14624
|
const filter = args.entityType ? `AND "entityType" = $1` : "";
|
|
14381
14625
|
const params = args.entityType ? [args.entityType] : [];
|
|
14382
|
-
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 ");
|
|
14626
|
+
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 ");
|
|
14383
14627
|
return (await client.manyOrNone(`SELECT v FROM (${unions}) sub WHERE v IS NOT NULL AND v <> '' ORDER BY v`, params)).map((r) => r.v);
|
|
14384
14628
|
};
|
|
14385
14629
|
return { tags: await readWithRefresh(client, schema, cacheKey, refresh, ttl, config.logger) };
|
|
14386
14630
|
}
|
|
14387
14631
|
async function getMetricNames(client, schema, args, config) {
|
|
14632
|
+
const ttl = config.ttlSeconds ?? DEFAULT_TTL_SECONDS;
|
|
14633
|
+
const lookback = config.lookbackSeconds ?? DEFAULT_LOOKBACK_SECONDS;
|
|
14388
14634
|
let filtered = await readWithRefresh(client, schema, "metric_names", async () => {
|
|
14389
14635
|
return (await client.manyOrNone(`SELECT DISTINCT "name" AS v FROM ${qualifiedTable(schema, TABLE_METRIC_EVENTS)}
|
|
14390
|
-
WHERE "name" IS NOT NULL AND "name" <> '' ORDER BY "name"`)).map((r) => r.v);
|
|
14391
|
-
},
|
|
14636
|
+
WHERE "name" IS NOT NULL AND "name" <> '' ${lookbackPredicate(TABLE_METRIC_EVENTS, lookback)} ORDER BY "name"`)).map((r) => r.v);
|
|
14637
|
+
}, ttl, config.logger);
|
|
14392
14638
|
if (args.prefix) filtered = filtered.filter((v) => v.startsWith(args.prefix));
|
|
14393
14639
|
if (args.limit) filtered = filtered.slice(0, args.limit);
|
|
14394
14640
|
return { names: filtered };
|
|
14395
14641
|
}
|
|
14396
14642
|
async function getMetricLabelKeys(client, schema, args, config) {
|
|
14397
14643
|
const ttl = config.ttlSeconds ?? DEFAULT_TTL_SECONDS;
|
|
14644
|
+
const lookback = config.lookbackSeconds ?? DEFAULT_LOOKBACK_SECONDS;
|
|
14398
14645
|
return { keys: await readWithRefresh(client, schema, `metric_label_keys:${args.metricName}`, async () => {
|
|
14399
14646
|
return (await client.manyOrNone(`SELECT DISTINCT k AS v
|
|
14400
14647
|
FROM ${qualifiedTable(schema, TABLE_METRIC_EVENTS)}, jsonb_object_keys("labels") k
|
|
14401
|
-
WHERE "name" = $1 ORDER BY k`, [args.metricName])).map((r) => r.v);
|
|
14648
|
+
WHERE "name" = $1 ${lookbackPredicate(TABLE_METRIC_EVENTS, lookback)} ORDER BY k`, [args.metricName])).map((r) => r.v);
|
|
14402
14649
|
}, ttl, config.logger) };
|
|
14403
14650
|
}
|
|
14404
14651
|
async function getMetricLabelValues(client, schema, args, config) {
|
|
14405
14652
|
const ttl = config.ttlSeconds ?? DEFAULT_TTL_SECONDS;
|
|
14653
|
+
const lookback = config.lookbackSeconds ?? DEFAULT_LOOKBACK_SECONDS;
|
|
14406
14654
|
let filtered = await readWithRefresh(client, schema, `metric_label_values:${args.metricName}:${args.labelKey}`, async () => {
|
|
14407
14655
|
return (await client.manyOrNone(`SELECT DISTINCT "labels" ->> $2 AS v
|
|
14408
14656
|
FROM ${qualifiedTable(schema, TABLE_METRIC_EVENTS)}
|
|
14409
|
-
WHERE "name" = $1 AND "labels" ? $2
|
|
14657
|
+
WHERE "name" = $1 AND "labels" ? $2 ${lookbackPredicate(TABLE_METRIC_EVENTS, lookback)}
|
|
14410
14658
|
ORDER BY v`, [args.metricName, args.labelKey])).map((r) => r.v).filter((v) => v != null && v !== "");
|
|
14411
14659
|
}, ttl, config.logger);
|
|
14412
14660
|
if (args.prefix) filtered = filtered.filter((v) => v.startsWith(args.prefix));
|
|
@@ -14645,6 +14893,7 @@ function commonContextToRow(record) {
|
|
|
14645
14893
|
};
|
|
14646
14894
|
}
|
|
14647
14895
|
function spanRecordToRow(span) {
|
|
14896
|
+
const isPending = !span.isEvent && span.endedAt == null;
|
|
14648
14897
|
const endedAt = span.isEvent ? span.startedAt : span.endedAt ?? span.startedAt;
|
|
14649
14898
|
const metadata = span.metadata ?? null;
|
|
14650
14899
|
return {
|
|
@@ -14655,6 +14904,7 @@ function spanRecordToRow(span) {
|
|
|
14655
14904
|
name: span.name,
|
|
14656
14905
|
spanType: span.spanType,
|
|
14657
14906
|
isEvent: Boolean(span.isEvent),
|
|
14907
|
+
isPending,
|
|
14658
14908
|
startedAt: toIsoOrDate(span.startedAt),
|
|
14659
14909
|
endedAt: toIsoOrDate(endedAt),
|
|
14660
14910
|
tags: normalizeTags(span.tags),
|
|
@@ -14669,9 +14919,19 @@ function spanRecordToRow(span) {
|
|
|
14669
14919
|
requestContext: jsonField(span.requestContext)
|
|
14670
14920
|
};
|
|
14671
14921
|
}
|
|
14922
|
+
/**
|
|
14923
|
+
* Recover a span's real `endedAt` from its row. A pending row carries a
|
|
14924
|
+
* synthesized `endedAt` (a copy of `startedAt`) because the column is part of
|
|
14925
|
+
* the primary key; callers must see null so the span reads as still running.
|
|
14926
|
+
*/
|
|
14927
|
+
function spanRowEndedAt(row, startedAt) {
|
|
14928
|
+
if (row.isEvent) return startedAt;
|
|
14929
|
+
if (row.isPending) return null;
|
|
14930
|
+
return toDateOrNull(row.endedAt);
|
|
14931
|
+
}
|
|
14672
14932
|
function rowToSpanRecord(row) {
|
|
14673
14933
|
const startedAt = toDate(row.startedAt);
|
|
14674
|
-
const endedAt = row
|
|
14934
|
+
const endedAt = spanRowEndedAt(row, startedAt);
|
|
14675
14935
|
const error = parsedJson(row.error);
|
|
14676
14936
|
const { executionSource, ...ctx } = rowToCommonContext(row);
|
|
14677
14937
|
return {
|
|
@@ -14705,7 +14965,7 @@ function rowToSpanRecord(row) {
|
|
|
14705
14965
|
*/
|
|
14706
14966
|
function rowToLightSpanRecord(row) {
|
|
14707
14967
|
const startedAt = toDate(row.startedAt);
|
|
14708
|
-
const endedAt = row
|
|
14968
|
+
const endedAt = spanRowEndedAt(row, startedAt);
|
|
14709
14969
|
return {
|
|
14710
14970
|
traceId: row.traceId,
|
|
14711
14971
|
spanId: row.spanId,
|
|
@@ -15236,22 +15496,42 @@ const COMPLEX_GROUP_BY_EXCLUDED = /* @__PURE__ */ new Set([
|
|
|
15236
15496
|
"requestContext"
|
|
15237
15497
|
]);
|
|
15238
15498
|
//#endregion
|
|
15499
|
+
//#region src/storage/db/sanitize-json.ts
|
|
15500
|
+
/**
|
|
15501
|
+
* Sanitizes JSON string for PostgreSQL jsonb:
|
|
15502
|
+
* - Removes problematic Unicode sequences:
|
|
15503
|
+
* - \u0000 (null character) - causes error 22P05 "unsupported Unicode escape sequence"
|
|
15504
|
+
* - \uD800-\uDFFF (unpaired surrogates) - causes "Unicode low surrogate must follow a high surrogate"
|
|
15505
|
+
* - \\uD800 (escaped-backslash + surrogate, e.g. from JS regex literals like [^\ud800-\udfff]):
|
|
15506
|
+
* removing just \uXXXX would leave a dangling backslash that creates a new invalid escape (e.g. \-)
|
|
15507
|
+
* - Escapes any remaining invalid JSON escape sequences (e.g. \v, \k, \-)
|
|
15508
|
+
*/
|
|
15509
|
+
function sanitizeJsonForPg(jsonString) {
|
|
15510
|
+
return jsonString.replace(/\\\\?u(0000|[Dd][89A-Fa-f][0-9A-Fa-f]{2})/g, "").replace(/(^|[^\\])(\\(?!["\\/bfnrtu]))/g, "$1\\\\");
|
|
15511
|
+
}
|
|
15512
|
+
//#endregion
|
|
15239
15513
|
//#region src/storage/domains/observability/v-next/sql.ts
|
|
15240
15514
|
/**
|
|
15241
15515
|
* SQL helpers for the v-next Postgres observability domain.
|
|
15242
15516
|
*
|
|
15243
|
-
* Provides a multi-row INSERT builder with
|
|
15244
|
-
*
|
|
15245
|
-
* pg driver doesn't have to guess column types.
|
|
15517
|
+
* Provides a multi-row INSERT builder with a configurable conflict clause
|
|
15518
|
+
* (`ON CONFLICT DO NOTHING` by default, for retry idempotency), and explicit
|
|
15519
|
+
* jsonb / text[] casts so the pg driver doesn't have to guess column types.
|
|
15246
15520
|
*/
|
|
15247
15521
|
/**
|
|
15248
15522
|
* Encode a JS value for a `$N::jsonb` cast. Always `JSON.stringify` so a
|
|
15249
15523
|
* plain string like `"hello"` becomes `"hello"` (a valid JSON scalar) and
|
|
15250
15524
|
* not the bare word `hello`, which Postgres rejects when cast to jsonb.
|
|
15525
|
+
*
|
|
15526
|
+
* The result is sanitized because Postgres rejects some sequences that are
|
|
15527
|
+
* legal in JSON: NUL (`\u0000`) fails with 22P05 and unpaired UTF-16
|
|
15528
|
+
* surrogates fail with 22P02. Inserts here are batched into a single
|
|
15529
|
+
* multi-row statement, so one bad value would otherwise reject the whole
|
|
15530
|
+
* batch of observability events.
|
|
15251
15531
|
*/
|
|
15252
15532
|
function encodeJsonb(value) {
|
|
15253
15533
|
if (value === null || value === void 0) return null;
|
|
15254
|
-
return JSON.stringify(value);
|
|
15534
|
+
return sanitizeJsonForPg(JSON.stringify(value));
|
|
15255
15535
|
}
|
|
15256
15536
|
/**
|
|
15257
15537
|
* Build a multi-row INSERT with explicit column types and ON CONFLICT DO NOTHING.
|
|
@@ -15262,7 +15542,7 @@ function encodeJsonb(value) {
|
|
|
15262
15542
|
* All records must have identical key sets.
|
|
15263
15543
|
* @returns { text, values } ready to pass to `client.query`.
|
|
15264
15544
|
*/
|
|
15265
|
-
function buildInsert(schema, table, records) {
|
|
15545
|
+
function buildInsert(schema, table, records, onConflict = "ON CONFLICT DO NOTHING") {
|
|
15266
15546
|
if (records.length === 0) return null;
|
|
15267
15547
|
const columns = Object.keys(records[0]).map((c) => parseSqlIdentifier(c, "column name"));
|
|
15268
15548
|
const quotedColumns = columns.map((c) => `"${c}"`).join(", ");
|
|
@@ -15288,10 +15568,44 @@ function buildInsert(schema, table, records) {
|
|
|
15288
15568
|
return {
|
|
15289
15569
|
text: `INSERT INTO ${qualifiedTable(schema, table)} (${quotedColumns})
|
|
15290
15570
|
VALUES ${rowPlaceholders.join(", ")}
|
|
15291
|
-
|
|
15571
|
+
${onConflict}`,
|
|
15292
15572
|
values
|
|
15293
15573
|
};
|
|
15294
15574
|
}
|
|
15575
|
+
/** Primary key of the span table; also the conflict target for span upserts. */
|
|
15576
|
+
const SPAN_KEY_COLUMNS = [
|
|
15577
|
+
"traceId",
|
|
15578
|
+
"spanId",
|
|
15579
|
+
"endedAt"
|
|
15580
|
+
];
|
|
15581
|
+
/**
|
|
15582
|
+
* Conflict clause for span writes.
|
|
15583
|
+
*
|
|
15584
|
+
* A span is written twice under the event-sourced strategy: once when it starts
|
|
15585
|
+
* (`isPending = true`, `endedAt` synthesized from `startedAt`) and once when it
|
|
15586
|
+
* ends. Those two rows normally differ in `endedAt` and so coexist under the
|
|
15587
|
+
* primary key. A zero-duration span is the exception — its real `endedAt`
|
|
15588
|
+
* equals its `startedAt`, so the end row collides with the pending row. Letting
|
|
15589
|
+
* `DO NOTHING` win there would strand the span as permanently running, so the
|
|
15590
|
+
* end row overwrites the pending one instead.
|
|
15591
|
+
*
|
|
15592
|
+
* The `WHERE` guard keeps the clause a no-op for every other conflict, which
|
|
15593
|
+
* preserves insert-only retry idempotency: a replayed end row cannot clobber
|
|
15594
|
+
* the row it duplicates, and a late-arriving start row cannot revert an ended
|
|
15595
|
+
* span to pending.
|
|
15596
|
+
*/
|
|
15597
|
+
function spanConflictClause(columns) {
|
|
15598
|
+
return `ON CONFLICT ("traceId", "spanId", "endedAt") DO UPDATE SET ${columns.filter((column) => !SPAN_KEY_COLUMNS.includes(column)).map((column) => `"${column}" = EXCLUDED."${column}"`).join(", ")}
|
|
15599
|
+
WHERE ${TABLE_SPAN_EVENTS}."isPending" AND NOT EXCLUDED."isPending"`;
|
|
15600
|
+
}
|
|
15601
|
+
/**
|
|
15602
|
+
* Ordering that picks the winning row for a span under the event-sourced write
|
|
15603
|
+
* model. An ended row always beats a pending row for the same span; among rows
|
|
15604
|
+
* of the same kind the latest write wins, with `cursorId` breaking ties between
|
|
15605
|
+
* rows that share a timestamp. Combine with `DISTINCT ON` (or `LIMIT 1`) after
|
|
15606
|
+
* the grouping key to collapse a span's rows down to one.
|
|
15607
|
+
*/
|
|
15608
|
+
const SPAN_COLLAPSE_ORDER = "\"isPending\" ASC, \"endedAt\" DESC, \"cursorId\" DESC";
|
|
15295
15609
|
/**
|
|
15296
15610
|
* Standard SELECT column list for tracing tables. The select projects every
|
|
15297
15611
|
* column the row→record converters expect.
|
|
@@ -16287,12 +16601,51 @@ async function getScorePercentiles(client, schema, args) {
|
|
|
16287
16601
|
function asIsoTimestamp(value) {
|
|
16288
16602
|
return value instanceof Date ? value.toISOString() : new Date(value).toISOString();
|
|
16289
16603
|
}
|
|
16604
|
+
/**
|
|
16605
|
+
* Ordering that picks the row representing a trace's current root.
|
|
16606
|
+
*
|
|
16607
|
+
* A trace can hold more than one root row. A span is written once when it
|
|
16608
|
+
* starts and again when it ends, and a durable run that suspends ends its root
|
|
16609
|
+
* span and opens a fresh one on resume. In both cases the newest row is the one
|
|
16610
|
+
* that describes the trace right now, and `cursorId` — a sequence assigned at
|
|
16611
|
+
* insert — is what "newest" means here. It also orders correctly where
|
|
16612
|
+
* timestamps cannot: a zero-duration span has `startedAt = endedAt`, and a
|
|
16613
|
+
* resumed root starts after the suspended root ended.
|
|
16614
|
+
*/
|
|
16615
|
+
const ROOT_COLLAPSE_ORDER = "\"cursorId\" DESC";
|
|
16616
|
+
/**
|
|
16617
|
+
* Predicate that keeps only the winning row for each span — the ended row when
|
|
16618
|
+
* one exists, the newest row otherwise. Mirrors `SPAN_COLLAPSE_ORDER` as an
|
|
16619
|
+
* anti-join, for the same reason as {@link latestRootPredicate}.
|
|
16620
|
+
*/
|
|
16621
|
+
function latestSpanPredicate(spanTable) {
|
|
16622
|
+
return `NOT EXISTS (
|
|
16623
|
+
SELECT 1 FROM ${spanTable} newer
|
|
16624
|
+
WHERE newer."traceId" = r."traceId"
|
|
16625
|
+
AND newer."spanId" = r."spanId"
|
|
16626
|
+
AND (newer."isPending" < r."isPending" OR (newer."isPending" = r."isPending" AND newer."cursorId" > r."cursorId"))
|
|
16627
|
+
)`;
|
|
16628
|
+
}
|
|
16629
|
+
/**
|
|
16630
|
+
* Predicate that keeps only a trace's current root row, as an anti-join rather
|
|
16631
|
+
* than a `DISTINCT ON`. Written this way so the outer query still scans the
|
|
16632
|
+
* partial root indexes and streams rows in `LIMIT`-order; grouping first would
|
|
16633
|
+
* force every root in the table to be read before the first page came back.
|
|
16634
|
+
*/
|
|
16635
|
+
function latestRootPredicate(spanTable) {
|
|
16636
|
+
return `NOT EXISTS (
|
|
16637
|
+
SELECT 1 FROM ${spanTable} newer
|
|
16638
|
+
WHERE newer."traceId" = r."traceId"
|
|
16639
|
+
AND newer."parentSpanId" IS NULL
|
|
16640
|
+
AND newer."cursorId" > r."cursorId"
|
|
16641
|
+
)`;
|
|
16642
|
+
}
|
|
16290
16643
|
async function getRootSpan(client, schema, args) {
|
|
16291
16644
|
const table = qualifiedTable(schema, TABLE_SPAN_EVENTS);
|
|
16292
16645
|
const row = await client.oneOrNone(`SELECT ${SPAN_SELECT_COLUMNS}
|
|
16293
16646
|
FROM ${table}
|
|
16294
16647
|
WHERE "traceId" = $1 AND "parentSpanId" IS NULL
|
|
16295
|
-
ORDER BY
|
|
16648
|
+
ORDER BY ${ROOT_COLLAPSE_ORDER}
|
|
16296
16649
|
LIMIT 1`, [args.traceId]);
|
|
16297
16650
|
if (!row) return null;
|
|
16298
16651
|
return { span: rowToSpanRecord(row) };
|
|
@@ -16304,7 +16657,7 @@ async function getRootSpan(client, schema, args) {
|
|
|
16304
16657
|
* planner. Starts numbering from `nextParamIdx`.
|
|
16305
16658
|
*/
|
|
16306
16659
|
function buildListTracesFilters(filters, spanTable, nextParamIdx) {
|
|
16307
|
-
const conditions = [`r."parentSpanId" IS NULL
|
|
16660
|
+
const conditions = [`r."parentSpanId" IS NULL`, latestRootPredicate(spanTable)];
|
|
16308
16661
|
const params = [];
|
|
16309
16662
|
let i = nextParamIdx;
|
|
16310
16663
|
if (!filters) return {
|
|
@@ -16321,11 +16674,11 @@ function buildListTracesFilters(filters, spanTable, nextParamIdx) {
|
|
|
16321
16674
|
params.push(asIsoTimestamp(filters.startedAt.end));
|
|
16322
16675
|
}
|
|
16323
16676
|
if (filters.endedAt?.start) {
|
|
16324
|
-
conditions.push(`r."endedAt" ${filters.endedAt.startExclusive ? ">" : ">="} $${i++}`);
|
|
16677
|
+
conditions.push(`(NOT r."isPending" AND r."endedAt" ${filters.endedAt.startExclusive ? ">" : ">="} $${i++})`);
|
|
16325
16678
|
params.push(asIsoTimestamp(filters.endedAt.start));
|
|
16326
16679
|
}
|
|
16327
16680
|
if (filters.endedAt?.end) {
|
|
16328
|
-
conditions.push(`r."endedAt" ${filters.endedAt.endExclusive ? "<" : "<="} $${i++}`);
|
|
16681
|
+
conditions.push(`(NOT r."isPending" AND r."endedAt" ${filters.endedAt.endExclusive ? "<" : "<="} $${i++})`);
|
|
16329
16682
|
params.push(asIsoTimestamp(filters.endedAt.end));
|
|
16330
16683
|
}
|
|
16331
16684
|
if (filters.spanType !== void 0) {
|
|
@@ -16397,10 +16750,10 @@ function buildListTracesFilters(filters, spanTable, nextParamIdx) {
|
|
|
16397
16750
|
conditions.push(`r."error" IS NOT NULL`);
|
|
16398
16751
|
break;
|
|
16399
16752
|
case TraceStatus.RUNNING:
|
|
16400
|
-
conditions.push(`
|
|
16753
|
+
conditions.push(`r."isPending"`);
|
|
16401
16754
|
break;
|
|
16402
16755
|
case TraceStatus.SUCCESS:
|
|
16403
|
-
conditions.push(`r."error" IS NULL`);
|
|
16756
|
+
conditions.push(`r."error" IS NULL AND NOT r."isPending"`);
|
|
16404
16757
|
break;
|
|
16405
16758
|
}
|
|
16406
16759
|
if (filters.hasChildError !== void 0) {
|
|
@@ -16537,8 +16890,8 @@ function buildBranchSpanTypeClause(userSpanType, params, startIdx) {
|
|
|
16537
16890
|
* NOT have a `parentSpanId IS NULL` predicate — branches can be nested under
|
|
16538
16891
|
* other spans. Returns the bound params and the next param index.
|
|
16539
16892
|
*/
|
|
16540
|
-
function buildListBranchesFilters(filters, spanType, nextParamIdx) {
|
|
16541
|
-
const conditions = [];
|
|
16893
|
+
function buildListBranchesFilters(filters, spanType, spanTable, nextParamIdx) {
|
|
16894
|
+
const conditions = [latestSpanPredicate(spanTable)];
|
|
16542
16895
|
const params = [];
|
|
16543
16896
|
let i = nextParamIdx;
|
|
16544
16897
|
const spanTypeClause = buildBranchSpanTypeClause(spanType, params, i);
|
|
@@ -16559,11 +16912,11 @@ function buildListBranchesFilters(filters, spanType, nextParamIdx) {
|
|
|
16559
16912
|
params.push(asIsoTimestamp(filters.startedAt.end));
|
|
16560
16913
|
}
|
|
16561
16914
|
if (filters.endedAt?.start) {
|
|
16562
|
-
conditions.push(`r."endedAt" ${filters.endedAt.startExclusive ? ">" : ">="} $${i++}`);
|
|
16915
|
+
conditions.push(`(NOT r."isPending" AND r."endedAt" ${filters.endedAt.startExclusive ? ">" : ">="} $${i++})`);
|
|
16563
16916
|
params.push(asIsoTimestamp(filters.endedAt.start));
|
|
16564
16917
|
}
|
|
16565
16918
|
if (filters.endedAt?.end) {
|
|
16566
|
-
conditions.push(`r."endedAt" ${filters.endedAt.endExclusive ? "<" : "<="} $${i++}`);
|
|
16919
|
+
conditions.push(`(NOT r."isPending" AND r."endedAt" ${filters.endedAt.endExclusive ? "<" : "<="} $${i++})`);
|
|
16567
16920
|
params.push(asIsoTimestamp(filters.endedAt.end));
|
|
16568
16921
|
}
|
|
16569
16922
|
if (filters.traceId !== void 0) {
|
|
@@ -16675,10 +17028,10 @@ function buildListBranchesFilters(filters, spanType, nextParamIdx) {
|
|
|
16675
17028
|
conditions.push(`r."error" IS NOT NULL`);
|
|
16676
17029
|
break;
|
|
16677
17030
|
case TraceStatus.RUNNING:
|
|
16678
|
-
conditions.push(`
|
|
17031
|
+
conditions.push(`r."isPending"`);
|
|
16679
17032
|
break;
|
|
16680
17033
|
case TraceStatus.SUCCESS:
|
|
16681
|
-
conditions.push(`r."error" IS NULL`);
|
|
17034
|
+
conditions.push(`r."error" IS NULL AND NOT r."isPending"`);
|
|
16682
17035
|
break;
|
|
16683
17036
|
}
|
|
16684
17037
|
return {
|
|
@@ -16697,7 +17050,7 @@ async function listBranches(client, schema, args) {
|
|
|
16697
17050
|
return listBranchesPage(client, span, filters, pagination.page, pagination.perPage, orderBy.field, orderBy.direction);
|
|
16698
17051
|
}
|
|
16699
17052
|
async function listBranchesPage(client, span, filters, page, perPage, orderField, orderDir) {
|
|
16700
|
-
const built = buildListBranchesFilters(filters, filters?.spanType, 1);
|
|
17053
|
+
const built = buildListBranchesFilters(filters, filters?.spanType, span, 1);
|
|
16701
17054
|
if (!built) {
|
|
16702
17055
|
const deltaCursor = deltaPollingFeatureEnabled() ? await readBranchesStreamHeadCursor(client, span, filters) : void 0;
|
|
16703
17056
|
return {
|
|
@@ -16751,7 +17104,7 @@ async function listBranchesDelta(client, span, filters, after, limit) {
|
|
|
16751
17104
|
deltaCursor
|
|
16752
17105
|
};
|
|
16753
17106
|
}
|
|
16754
|
-
const built = buildListBranchesFilters(filters, filters?.spanType, 1);
|
|
17107
|
+
const built = buildListBranchesFilters(filters, filters?.spanType, span, 1);
|
|
16755
17108
|
if (!built) return {
|
|
16756
17109
|
branches: [],
|
|
16757
17110
|
delta: {
|
|
@@ -16794,14 +17147,35 @@ async function readBranchesStreamHeadCursor(client, span, filters) {
|
|
|
16794
17147
|
}
|
|
16795
17148
|
//#endregion
|
|
16796
17149
|
//#region src/storage/domains/observability/v-next/tracing.ts
|
|
16797
|
-
async function
|
|
16798
|
-
const
|
|
17150
|
+
async function writeSpanRows(client, schema, rows) {
|
|
17151
|
+
const deduped = dedupeSpanRows(rows);
|
|
17152
|
+
const insert = buildInsert(schema, TABLE_SPAN_EVENTS, deduped, spanConflictClause(Object.keys(deduped[0] ?? {})));
|
|
16799
17153
|
if (insert) await client.query(insert.text, insert.values);
|
|
16800
17154
|
}
|
|
17155
|
+
/**
|
|
17156
|
+
* Collapse rows that share a primary key within a single batch. Postgres
|
|
17157
|
+
* rejects an `ON CONFLICT DO UPDATE` statement whose VALUES list would touch
|
|
17158
|
+
* the same row twice, which a batch containing both the start and the end of a
|
|
17159
|
+
* zero-duration span would do. The ended row wins, matching what the conflict
|
|
17160
|
+
* clause would have done across two separate statements.
|
|
17161
|
+
*/
|
|
17162
|
+
function dedupeSpanRows(rows) {
|
|
17163
|
+
if (rows.length < 2) return rows;
|
|
17164
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
17165
|
+
for (const row of rows) {
|
|
17166
|
+
const key = `${row.traceId}\u0000${row.spanId}\u0000${String(row.endedAt)}`;
|
|
17167
|
+
const existing = byKey.get(key);
|
|
17168
|
+
if (existing && !existing.isPending) continue;
|
|
17169
|
+
byKey.set(key, row);
|
|
17170
|
+
}
|
|
17171
|
+
return [...byKey.values()];
|
|
17172
|
+
}
|
|
17173
|
+
async function createSpan(client, schema, args) {
|
|
17174
|
+
await writeSpanRows(client, schema, [spanRecordToRow(args.span)]);
|
|
17175
|
+
}
|
|
16801
17176
|
async function batchCreateSpans(client, schema, args) {
|
|
16802
17177
|
if (args.records.length === 0) return;
|
|
16803
|
-
|
|
16804
|
-
if (insert) await client.query(insert.text, insert.values);
|
|
17178
|
+
await writeSpanRows(client, schema, args.records.map(spanRecordToRow));
|
|
16805
17179
|
}
|
|
16806
17180
|
async function getSpans(client, schema, args) {
|
|
16807
17181
|
if (args.spanIds.length === 0) return {
|
|
@@ -16809,10 +17183,13 @@ async function getSpans(client, schema, args) {
|
|
|
16809
17183
|
spans: []
|
|
16810
17184
|
};
|
|
16811
17185
|
const table = qualifiedTable(schema, TABLE_SPAN_EVENTS);
|
|
16812
|
-
const rows = await client.manyOrNone(`SELECT
|
|
16813
|
-
|
|
16814
|
-
|
|
16815
|
-
|
|
17186
|
+
const rows = await client.manyOrNone(`SELECT * FROM (
|
|
17187
|
+
SELECT DISTINCT ON ("spanId") ${SPAN_SELECT_COLUMNS}
|
|
17188
|
+
FROM ${table}
|
|
17189
|
+
WHERE "traceId" = $1
|
|
17190
|
+
AND "spanId" = ANY($2::text[])
|
|
17191
|
+
ORDER BY "spanId", ${SPAN_COLLAPSE_ORDER}
|
|
17192
|
+
) collapsed
|
|
16816
17193
|
ORDER BY "startedAt" ASC`, [args.traceId, args.spanIds]);
|
|
16817
17194
|
return {
|
|
16818
17195
|
traceId: args.traceId,
|
|
@@ -16824,16 +17201,19 @@ async function getSpan(client, schema, args) {
|
|
|
16824
17201
|
const row = await client.oneOrNone(`SELECT ${SPAN_SELECT_COLUMNS}
|
|
16825
17202
|
FROM ${table}
|
|
16826
17203
|
WHERE "traceId" = $1 AND "spanId" = $2
|
|
16827
|
-
ORDER BY
|
|
17204
|
+
ORDER BY ${SPAN_COLLAPSE_ORDER}
|
|
16828
17205
|
LIMIT 1`, [args.traceId, args.spanId]);
|
|
16829
17206
|
if (!row) return null;
|
|
16830
17207
|
return { span: rowToSpanRecord(row) };
|
|
16831
17208
|
}
|
|
16832
17209
|
async function getTrace(client, schema, args) {
|
|
16833
17210
|
const table = qualifiedTable(schema, TABLE_SPAN_EVENTS);
|
|
16834
|
-
const rows = await client.manyOrNone(`SELECT
|
|
16835
|
-
|
|
16836
|
-
|
|
17211
|
+
const rows = await client.manyOrNone(`SELECT * FROM (
|
|
17212
|
+
SELECT DISTINCT ON ("spanId") ${SPAN_SELECT_COLUMNS}
|
|
17213
|
+
FROM ${table}
|
|
17214
|
+
WHERE "traceId" = $1
|
|
17215
|
+
ORDER BY "spanId", ${SPAN_COLLAPSE_ORDER}
|
|
17216
|
+
) collapsed
|
|
16837
17217
|
ORDER BY "startedAt" ASC`, [args.traceId]);
|
|
16838
17218
|
if (!rows.length) return null;
|
|
16839
17219
|
return {
|
|
@@ -16843,9 +17223,12 @@ async function getTrace(client, schema, args) {
|
|
|
16843
17223
|
}
|
|
16844
17224
|
async function getTraceLight(client, schema, args) {
|
|
16845
17225
|
const table = qualifiedTable(schema, TABLE_SPAN_EVENTS);
|
|
16846
|
-
const rows = await client.manyOrNone(`SELECT
|
|
16847
|
-
|
|
16848
|
-
|
|
17226
|
+
const rows = await client.manyOrNone(`SELECT * FROM (
|
|
17227
|
+
SELECT DISTINCT ON ("spanId") ${SPAN_LIGHT_SELECT_COLUMNS}
|
|
17228
|
+
FROM ${table}
|
|
17229
|
+
WHERE "traceId" = $1
|
|
17230
|
+
ORDER BY "spanId", ${SPAN_COLLAPSE_ORDER}
|
|
17231
|
+
) collapsed
|
|
16849
17232
|
ORDER BY "startedAt" ASC`, [args.traceId]);
|
|
16850
17233
|
if (!rows.length) return null;
|
|
16851
17234
|
return {
|
|
@@ -16956,6 +17339,11 @@ var ObservabilityStoragePostgresVNext = class ObservabilityStoragePostgresVNext
|
|
|
16956
17339
|
} catch (error) {
|
|
16957
17340
|
if (!isDuplicateRelationError(error)) throw error;
|
|
16958
17341
|
}
|
|
17342
|
+
for (const { table, column, ddl } of additiveColumns(this.#schema)) if (!await this.#client.oneOrNone(columnExistsSQL, [
|
|
17343
|
+
this.#schema,
|
|
17344
|
+
table,
|
|
17345
|
+
column
|
|
17346
|
+
])) await this.#client.none(ddl);
|
|
16959
17347
|
for (const ddl of allIndexDDL(this.#schema)) try {
|
|
16960
17348
|
await this.#client.none(ddl);
|
|
16961
17349
|
} catch (error) {
|
|
@@ -17052,8 +17440,8 @@ var ObservabilityStoragePostgresVNext = class ObservabilityStoragePostgresVNext
|
|
|
17052
17440
|
}
|
|
17053
17441
|
get observabilityStrategy() {
|
|
17054
17442
|
return {
|
|
17055
|
-
preferred: "
|
|
17056
|
-
supported: ["
|
|
17443
|
+
preferred: "event-sourced",
|
|
17444
|
+
supported: ["event-sourced"]
|
|
17057
17445
|
};
|
|
17058
17446
|
}
|
|
17059
17447
|
getFeatures() {
|
|
@@ -20477,18 +20865,6 @@ function workflowSnapshotStatusIndexSQL(indexName, schemaName) {
|
|
|
20477
20865
|
schemaName: getSchemaName(schemaName)
|
|
20478
20866
|
})} (workflow_name, (snapshot ->> 'status'), "createdAt" DESC)`;
|
|
20479
20867
|
}
|
|
20480
|
-
/**
|
|
20481
|
-
* Sanitizes JSON string for PostgreSQL jsonb:
|
|
20482
|
-
* - Removes problematic Unicode sequences:
|
|
20483
|
-
* - \u0000 (null character) - causes error 22P05 "unsupported Unicode escape sequence"
|
|
20484
|
-
* - \uD800-\uDFFF (unpaired surrogates) - causes "Unicode low surrogate must follow a high surrogate"
|
|
20485
|
-
* - \\uD800 (escaped-backslash + surrogate, e.g. from JS regex literals like [^\ud800-\udfff]):
|
|
20486
|
-
* removing just \uXXXX would leave a dangling backslash that creates a new invalid escape (e.g. \-)
|
|
20487
|
-
* - Escapes any remaining invalid JSON escape sequences (e.g. \v, \k, \-)
|
|
20488
|
-
*/
|
|
20489
|
-
function sanitizeJsonForPg(jsonString) {
|
|
20490
|
-
return jsonString.replace(/\\\\?u(0000|[Dd][89A-Fa-f][0-9A-Fa-f]{2})/g, "").replace(/(^|[^\\])(\\(?!["\\/bfnrtu]))/g, "$1\\\\");
|
|
20491
|
-
}
|
|
20492
20868
|
var WorkflowsPG = class WorkflowsPG extends WorkflowsStorage {
|
|
20493
20869
|
#db;
|
|
20494
20870
|
#schema;
|
|
@@ -20768,11 +21144,11 @@ var WorkflowsPG = class WorkflowsPG extends WorkflowsStorage {
|
|
|
20768
21144
|
await this.#db.client.none(`INSERT INTO ${getTableName({
|
|
20769
21145
|
indexName: TABLE_WORKFLOW_SNAPSHOT,
|
|
20770
21146
|
schemaName: getSchemaName(this.#schema)
|
|
20771
|
-
})}
|
|
21147
|
+
})} AS t
|
|
20772
21148
|
(workflow_name, run_id, "resourceId", snapshot, "createdAt", "updatedAt", "createdAtZ", "updatedAtZ")
|
|
20773
21149
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
|
20774
21150
|
ON CONFLICT (workflow_name, run_id) DO UPDATE
|
|
20775
|
-
SET "resourceId" = $3, snapshot = $4, "updatedAt" = $6, "updatedAtZ" = $8`, [
|
|
21151
|
+
SET "resourceId" = COALESCE($3, t."resourceId"), snapshot = $4, "updatedAt" = $6, "updatedAtZ" = $8`, [
|
|
20776
21152
|
workflowName,
|
|
20777
21153
|
runId,
|
|
20778
21154
|
resourceId,
|
|
@@ -21626,7 +22002,7 @@ var PgFactoryStorageOps = class {
|
|
|
21626
22002
|
row[name] = Boolean(value);
|
|
21627
22003
|
break;
|
|
21628
22004
|
case "json":
|
|
21629
|
-
row[name] =
|
|
22005
|
+
row[name] = value;
|
|
21630
22006
|
break;
|
|
21631
22007
|
case "bigint":
|
|
21632
22008
|
case "integer":
|
|
@@ -21730,6 +22106,12 @@ var PgFactoryStorageOps = class {
|
|
|
21730
22106
|
async findMany(collection, where, opts) {
|
|
21731
22107
|
return this.#select(this.#queryable, collection, where, opts);
|
|
21732
22108
|
}
|
|
22109
|
+
async count(collection, where) {
|
|
22110
|
+
const schema = this.#schema(collection);
|
|
22111
|
+
const filter = this.#buildWhere(schema, where);
|
|
22112
|
+
const result = await this.#queryable.query(`SELECT COUNT(*) AS count FROM "${schema.name}" WHERE ${filter.sql}`, filter.args);
|
|
22113
|
+
return Number(result.rows[0]?.count ?? 0);
|
|
22114
|
+
}
|
|
21733
22115
|
async insertOne(collection, row) {
|
|
21734
22116
|
const schema = this.#schema(collection);
|
|
21735
22117
|
const pk = primaryKeyOf(schema);
|