@mastra/pg 1.22.0-alpha.0 → 1.22.0-alpha.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +89 -0
- package/dist/docs/SKILL.md +1 -1
- package/dist/docs/assets/SOURCE_MAP.json +1 -1
- package/dist/docs/references/docs-deployment-workers.md +2 -2
- package/dist/docs/references/docs-storage.md +1 -0
- package/dist/docs/references/integrations-databases-postgresql.md +2 -0
- package/dist/docs/references/reference-rag-vector-databases.md +4 -4
- package/dist/docs/references/reference-vectors-pg.md +2 -0
- package/dist/index.cjs +394 -107
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +394 -107
- package/dist/index.js.map +1 -1
- package/dist/storage/db/sanitize-json.d.ts +11 -0
- package/dist/storage/db/sanitize-json.d.ts.map +1 -0
- package/dist/storage/domains/observability/v-next/ddl.d.ts +17 -0
- package/dist/storage/domains/observability/v-next/ddl.d.ts.map +1 -1
- package/dist/storage/domains/observability/v-next/helpers.d.ts.map +1 -1
- package/dist/storage/domains/observability/v-next/index.d.ts.map +1 -1
- package/dist/storage/domains/observability/v-next/signal-schema.d.ts +5 -1
- package/dist/storage/domains/observability/v-next/signal-schema.d.ts.map +1 -1
- package/dist/storage/domains/observability/v-next/sql.d.ts +29 -4
- package/dist/storage/domains/observability/v-next/sql.d.ts.map +1 -1
- package/dist/storage/domains/observability/v-next/traces.d.ts.map +1 -1
- package/dist/storage/domains/observability/v-next/tracing.d.ts +4 -3
- package/dist/storage/domains/observability/v-next/tracing.d.ts.map +1 -1
- package/dist/storage/domains/workflows/index.d.ts +2 -10
- package/dist/storage/domains/workflows/index.d.ts.map +1 -1
- package/dist/storage/factory-storage.d.ts.map +1 -1
- package/dist/vector/index.d.ts +60 -5
- package/dist/vector/index.d.ts.map +1 -1
- package/package.json +2 -2
package/dist/index.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}`, {
|
|
@@ -13567,6 +13694,11 @@ const SPAN_EVENT_COLUMNS = [
|
|
|
13567
13694
|
type: "boolean",
|
|
13568
13695
|
defaultSql: "false"
|
|
13569
13696
|
},
|
|
13697
|
+
{
|
|
13698
|
+
name: "isPending",
|
|
13699
|
+
type: "boolean",
|
|
13700
|
+
defaultSql: "false"
|
|
13701
|
+
},
|
|
13570
13702
|
{
|
|
13571
13703
|
name: "startedAt",
|
|
13572
13704
|
type: "timestamptz"
|
|
@@ -13863,6 +13995,7 @@ const SPAN_LIGHT_SELECT_COLUMN_NAMES = [
|
|
|
13863
13995
|
"spanType",
|
|
13864
13996
|
"error",
|
|
13865
13997
|
"isEvent",
|
|
13998
|
+
"isPending",
|
|
13866
13999
|
"startedAt",
|
|
13867
14000
|
"endedAt"
|
|
13868
14001
|
];
|
|
@@ -14257,6 +14390,26 @@ function allTableDDL(schema, mode) {
|
|
|
14257
14390
|
discoveryTableDDL(schema)
|
|
14258
14391
|
];
|
|
14259
14392
|
}
|
|
14393
|
+
/**
|
|
14394
|
+
* Additive column migrations for tables created by an older version of this
|
|
14395
|
+
* schema. `CREATE TABLE IF NOT EXISTS` is a no-op on an existing table, so new
|
|
14396
|
+
* columns have to be added out of band.
|
|
14397
|
+
*
|
|
14398
|
+
* `ALTER TABLE` takes an AccessExclusiveLock on the partitioned parent, so
|
|
14399
|
+
* callers must check {@link columnExistsSQL} first and only run the statement
|
|
14400
|
+
* when the column is genuinely missing. On Postgres 11+ the non-volatile
|
|
14401
|
+
* default does not rewrite the table, and the ALTER cascades to partitions.
|
|
14402
|
+
*/
|
|
14403
|
+
function additiveColumns(schema) {
|
|
14404
|
+
return [{
|
|
14405
|
+
table: TABLE_SPAN_EVENTS,
|
|
14406
|
+
column: "isPending",
|
|
14407
|
+
ddl: `ALTER TABLE ${qualifiedTable(schema, TABLE_SPAN_EVENTS)} ADD COLUMN IF NOT EXISTS "isPending" boolean NOT NULL DEFAULT false`
|
|
14408
|
+
}];
|
|
14409
|
+
}
|
|
14410
|
+
/** Existence probe for an additive column. Params: schema, table, column. */
|
|
14411
|
+
const columnExistsSQL = `SELECT 1 FROM information_schema.columns
|
|
14412
|
+
WHERE table_schema = $1 AND table_name = $2 AND column_name = $3`;
|
|
14260
14413
|
/** Index CREATEs. Safe to run repeatedly. */
|
|
14261
14414
|
function allIndexDDL(schema) {
|
|
14262
14415
|
return tableIndexes().map((spec) => indexDDL(schema, spec));
|
|
@@ -14645,6 +14798,7 @@ function commonContextToRow(record) {
|
|
|
14645
14798
|
};
|
|
14646
14799
|
}
|
|
14647
14800
|
function spanRecordToRow(span) {
|
|
14801
|
+
const isPending = !span.isEvent && span.endedAt == null;
|
|
14648
14802
|
const endedAt = span.isEvent ? span.startedAt : span.endedAt ?? span.startedAt;
|
|
14649
14803
|
const metadata = span.metadata ?? null;
|
|
14650
14804
|
return {
|
|
@@ -14655,6 +14809,7 @@ function spanRecordToRow(span) {
|
|
|
14655
14809
|
name: span.name,
|
|
14656
14810
|
spanType: span.spanType,
|
|
14657
14811
|
isEvent: Boolean(span.isEvent),
|
|
14812
|
+
isPending,
|
|
14658
14813
|
startedAt: toIsoOrDate(span.startedAt),
|
|
14659
14814
|
endedAt: toIsoOrDate(endedAt),
|
|
14660
14815
|
tags: normalizeTags(span.tags),
|
|
@@ -14669,9 +14824,19 @@ function spanRecordToRow(span) {
|
|
|
14669
14824
|
requestContext: jsonField(span.requestContext)
|
|
14670
14825
|
};
|
|
14671
14826
|
}
|
|
14827
|
+
/**
|
|
14828
|
+
* Recover a span's real `endedAt` from its row. A pending row carries a
|
|
14829
|
+
* synthesized `endedAt` (a copy of `startedAt`) because the column is part of
|
|
14830
|
+
* the primary key; callers must see null so the span reads as still running.
|
|
14831
|
+
*/
|
|
14832
|
+
function spanRowEndedAt(row, startedAt) {
|
|
14833
|
+
if (row.isEvent) return startedAt;
|
|
14834
|
+
if (row.isPending) return null;
|
|
14835
|
+
return toDateOrNull(row.endedAt);
|
|
14836
|
+
}
|
|
14672
14837
|
function rowToSpanRecord(row) {
|
|
14673
14838
|
const startedAt = toDate(row.startedAt);
|
|
14674
|
-
const endedAt = row
|
|
14839
|
+
const endedAt = spanRowEndedAt(row, startedAt);
|
|
14675
14840
|
const error = parsedJson(row.error);
|
|
14676
14841
|
const { executionSource, ...ctx } = rowToCommonContext(row);
|
|
14677
14842
|
return {
|
|
@@ -14705,7 +14870,7 @@ function rowToSpanRecord(row) {
|
|
|
14705
14870
|
*/
|
|
14706
14871
|
function rowToLightSpanRecord(row) {
|
|
14707
14872
|
const startedAt = toDate(row.startedAt);
|
|
14708
|
-
const endedAt = row
|
|
14873
|
+
const endedAt = spanRowEndedAt(row, startedAt);
|
|
14709
14874
|
return {
|
|
14710
14875
|
traceId: row.traceId,
|
|
14711
14876
|
spanId: row.spanId,
|
|
@@ -15236,22 +15401,42 @@ const COMPLEX_GROUP_BY_EXCLUDED = /* @__PURE__ */ new Set([
|
|
|
15236
15401
|
"requestContext"
|
|
15237
15402
|
]);
|
|
15238
15403
|
//#endregion
|
|
15404
|
+
//#region src/storage/db/sanitize-json.ts
|
|
15405
|
+
/**
|
|
15406
|
+
* Sanitizes JSON string for PostgreSQL jsonb:
|
|
15407
|
+
* - Removes problematic Unicode sequences:
|
|
15408
|
+
* - \u0000 (null character) - causes error 22P05 "unsupported Unicode escape sequence"
|
|
15409
|
+
* - \uD800-\uDFFF (unpaired surrogates) - causes "Unicode low surrogate must follow a high surrogate"
|
|
15410
|
+
* - \\uD800 (escaped-backslash + surrogate, e.g. from JS regex literals like [^\ud800-\udfff]):
|
|
15411
|
+
* removing just \uXXXX would leave a dangling backslash that creates a new invalid escape (e.g. \-)
|
|
15412
|
+
* - Escapes any remaining invalid JSON escape sequences (e.g. \v, \k, \-)
|
|
15413
|
+
*/
|
|
15414
|
+
function sanitizeJsonForPg(jsonString) {
|
|
15415
|
+
return jsonString.replace(/\\\\?u(0000|[Dd][89A-Fa-f][0-9A-Fa-f]{2})/g, "").replace(/(^|[^\\])(\\(?!["\\/bfnrtu]))/g, "$1\\\\");
|
|
15416
|
+
}
|
|
15417
|
+
//#endregion
|
|
15239
15418
|
//#region src/storage/domains/observability/v-next/sql.ts
|
|
15240
15419
|
/**
|
|
15241
15420
|
* SQL helpers for the v-next Postgres observability domain.
|
|
15242
15421
|
*
|
|
15243
|
-
* Provides a multi-row INSERT builder with
|
|
15244
|
-
*
|
|
15245
|
-
* pg driver doesn't have to guess column types.
|
|
15422
|
+
* Provides a multi-row INSERT builder with a configurable conflict clause
|
|
15423
|
+
* (`ON CONFLICT DO NOTHING` by default, for retry idempotency), and explicit
|
|
15424
|
+
* jsonb / text[] casts so the pg driver doesn't have to guess column types.
|
|
15246
15425
|
*/
|
|
15247
15426
|
/**
|
|
15248
15427
|
* Encode a JS value for a `$N::jsonb` cast. Always `JSON.stringify` so a
|
|
15249
15428
|
* plain string like `"hello"` becomes `"hello"` (a valid JSON scalar) and
|
|
15250
15429
|
* not the bare word `hello`, which Postgres rejects when cast to jsonb.
|
|
15430
|
+
*
|
|
15431
|
+
* The result is sanitized because Postgres rejects some sequences that are
|
|
15432
|
+
* legal in JSON: NUL (`\u0000`) fails with 22P05 and unpaired UTF-16
|
|
15433
|
+
* surrogates fail with 22P02. Inserts here are batched into a single
|
|
15434
|
+
* multi-row statement, so one bad value would otherwise reject the whole
|
|
15435
|
+
* batch of observability events.
|
|
15251
15436
|
*/
|
|
15252
15437
|
function encodeJsonb(value) {
|
|
15253
15438
|
if (value === null || value === void 0) return null;
|
|
15254
|
-
return JSON.stringify(value);
|
|
15439
|
+
return sanitizeJsonForPg(JSON.stringify(value));
|
|
15255
15440
|
}
|
|
15256
15441
|
/**
|
|
15257
15442
|
* Build a multi-row INSERT with explicit column types and ON CONFLICT DO NOTHING.
|
|
@@ -15262,7 +15447,7 @@ function encodeJsonb(value) {
|
|
|
15262
15447
|
* All records must have identical key sets.
|
|
15263
15448
|
* @returns { text, values } ready to pass to `client.query`.
|
|
15264
15449
|
*/
|
|
15265
|
-
function buildInsert(schema, table, records) {
|
|
15450
|
+
function buildInsert(schema, table, records, onConflict = "ON CONFLICT DO NOTHING") {
|
|
15266
15451
|
if (records.length === 0) return null;
|
|
15267
15452
|
const columns = Object.keys(records[0]).map((c) => parseSqlIdentifier(c, "column name"));
|
|
15268
15453
|
const quotedColumns = columns.map((c) => `"${c}"`).join(", ");
|
|
@@ -15288,10 +15473,44 @@ function buildInsert(schema, table, records) {
|
|
|
15288
15473
|
return {
|
|
15289
15474
|
text: `INSERT INTO ${qualifiedTable(schema, table)} (${quotedColumns})
|
|
15290
15475
|
VALUES ${rowPlaceholders.join(", ")}
|
|
15291
|
-
|
|
15476
|
+
${onConflict}`,
|
|
15292
15477
|
values
|
|
15293
15478
|
};
|
|
15294
15479
|
}
|
|
15480
|
+
/** Primary key of the span table; also the conflict target for span upserts. */
|
|
15481
|
+
const SPAN_KEY_COLUMNS = [
|
|
15482
|
+
"traceId",
|
|
15483
|
+
"spanId",
|
|
15484
|
+
"endedAt"
|
|
15485
|
+
];
|
|
15486
|
+
/**
|
|
15487
|
+
* Conflict clause for span writes.
|
|
15488
|
+
*
|
|
15489
|
+
* A span is written twice under the event-sourced strategy: once when it starts
|
|
15490
|
+
* (`isPending = true`, `endedAt` synthesized from `startedAt`) and once when it
|
|
15491
|
+
* ends. Those two rows normally differ in `endedAt` and so coexist under the
|
|
15492
|
+
* primary key. A zero-duration span is the exception — its real `endedAt`
|
|
15493
|
+
* equals its `startedAt`, so the end row collides with the pending row. Letting
|
|
15494
|
+
* `DO NOTHING` win there would strand the span as permanently running, so the
|
|
15495
|
+
* end row overwrites the pending one instead.
|
|
15496
|
+
*
|
|
15497
|
+
* The `WHERE` guard keeps the clause a no-op for every other conflict, which
|
|
15498
|
+
* preserves insert-only retry idempotency: a replayed end row cannot clobber
|
|
15499
|
+
* the row it duplicates, and a late-arriving start row cannot revert an ended
|
|
15500
|
+
* span to pending.
|
|
15501
|
+
*/
|
|
15502
|
+
function spanConflictClause(columns) {
|
|
15503
|
+
return `ON CONFLICT ("traceId", "spanId", "endedAt") DO UPDATE SET ${columns.filter((column) => !SPAN_KEY_COLUMNS.includes(column)).map((column) => `"${column}" = EXCLUDED."${column}"`).join(", ")}
|
|
15504
|
+
WHERE ${TABLE_SPAN_EVENTS}."isPending" AND NOT EXCLUDED."isPending"`;
|
|
15505
|
+
}
|
|
15506
|
+
/**
|
|
15507
|
+
* Ordering that picks the winning row for a span under the event-sourced write
|
|
15508
|
+
* model. An ended row always beats a pending row for the same span; among rows
|
|
15509
|
+
* of the same kind the latest write wins, with `cursorId` breaking ties between
|
|
15510
|
+
* rows that share a timestamp. Combine with `DISTINCT ON` (or `LIMIT 1`) after
|
|
15511
|
+
* the grouping key to collapse a span's rows down to one.
|
|
15512
|
+
*/
|
|
15513
|
+
const SPAN_COLLAPSE_ORDER = "\"isPending\" ASC, \"endedAt\" DESC, \"cursorId\" DESC";
|
|
15295
15514
|
/**
|
|
15296
15515
|
* Standard SELECT column list for tracing tables. The select projects every
|
|
15297
15516
|
* column the row→record converters expect.
|
|
@@ -16287,12 +16506,51 @@ async function getScorePercentiles(client, schema, args) {
|
|
|
16287
16506
|
function asIsoTimestamp(value) {
|
|
16288
16507
|
return value instanceof Date ? value.toISOString() : new Date(value).toISOString();
|
|
16289
16508
|
}
|
|
16509
|
+
/**
|
|
16510
|
+
* Ordering that picks the row representing a trace's current root.
|
|
16511
|
+
*
|
|
16512
|
+
* A trace can hold more than one root row. A span is written once when it
|
|
16513
|
+
* starts and again when it ends, and a durable run that suspends ends its root
|
|
16514
|
+
* span and opens a fresh one on resume. In both cases the newest row is the one
|
|
16515
|
+
* that describes the trace right now, and `cursorId` — a sequence assigned at
|
|
16516
|
+
* insert — is what "newest" means here. It also orders correctly where
|
|
16517
|
+
* timestamps cannot: a zero-duration span has `startedAt = endedAt`, and a
|
|
16518
|
+
* resumed root starts after the suspended root ended.
|
|
16519
|
+
*/
|
|
16520
|
+
const ROOT_COLLAPSE_ORDER = "\"cursorId\" DESC";
|
|
16521
|
+
/**
|
|
16522
|
+
* Predicate that keeps only the winning row for each span — the ended row when
|
|
16523
|
+
* one exists, the newest row otherwise. Mirrors `SPAN_COLLAPSE_ORDER` as an
|
|
16524
|
+
* anti-join, for the same reason as {@link latestRootPredicate}.
|
|
16525
|
+
*/
|
|
16526
|
+
function latestSpanPredicate(spanTable) {
|
|
16527
|
+
return `NOT EXISTS (
|
|
16528
|
+
SELECT 1 FROM ${spanTable} newer
|
|
16529
|
+
WHERE newer."traceId" = r."traceId"
|
|
16530
|
+
AND newer."spanId" = r."spanId"
|
|
16531
|
+
AND (newer."isPending" < r."isPending" OR (newer."isPending" = r."isPending" AND newer."cursorId" > r."cursorId"))
|
|
16532
|
+
)`;
|
|
16533
|
+
}
|
|
16534
|
+
/**
|
|
16535
|
+
* Predicate that keeps only a trace's current root row, as an anti-join rather
|
|
16536
|
+
* than a `DISTINCT ON`. Written this way so the outer query still scans the
|
|
16537
|
+
* partial root indexes and streams rows in `LIMIT`-order; grouping first would
|
|
16538
|
+
* force every root in the table to be read before the first page came back.
|
|
16539
|
+
*/
|
|
16540
|
+
function latestRootPredicate(spanTable) {
|
|
16541
|
+
return `NOT EXISTS (
|
|
16542
|
+
SELECT 1 FROM ${spanTable} newer
|
|
16543
|
+
WHERE newer."traceId" = r."traceId"
|
|
16544
|
+
AND newer."parentSpanId" IS NULL
|
|
16545
|
+
AND newer."cursorId" > r."cursorId"
|
|
16546
|
+
)`;
|
|
16547
|
+
}
|
|
16290
16548
|
async function getRootSpan(client, schema, args) {
|
|
16291
16549
|
const table = qualifiedTable(schema, TABLE_SPAN_EVENTS);
|
|
16292
16550
|
const row = await client.oneOrNone(`SELECT ${SPAN_SELECT_COLUMNS}
|
|
16293
16551
|
FROM ${table}
|
|
16294
16552
|
WHERE "traceId" = $1 AND "parentSpanId" IS NULL
|
|
16295
|
-
ORDER BY
|
|
16553
|
+
ORDER BY ${ROOT_COLLAPSE_ORDER}
|
|
16296
16554
|
LIMIT 1`, [args.traceId]);
|
|
16297
16555
|
if (!row) return null;
|
|
16298
16556
|
return { span: rowToSpanRecord(row) };
|
|
@@ -16304,7 +16562,7 @@ async function getRootSpan(client, schema, args) {
|
|
|
16304
16562
|
* planner. Starts numbering from `nextParamIdx`.
|
|
16305
16563
|
*/
|
|
16306
16564
|
function buildListTracesFilters(filters, spanTable, nextParamIdx) {
|
|
16307
|
-
const conditions = [`r."parentSpanId" IS NULL
|
|
16565
|
+
const conditions = [`r."parentSpanId" IS NULL`, latestRootPredicate(spanTable)];
|
|
16308
16566
|
const params = [];
|
|
16309
16567
|
let i = nextParamIdx;
|
|
16310
16568
|
if (!filters) return {
|
|
@@ -16321,11 +16579,11 @@ function buildListTracesFilters(filters, spanTable, nextParamIdx) {
|
|
|
16321
16579
|
params.push(asIsoTimestamp(filters.startedAt.end));
|
|
16322
16580
|
}
|
|
16323
16581
|
if (filters.endedAt?.start) {
|
|
16324
|
-
conditions.push(`r."endedAt" ${filters.endedAt.startExclusive ? ">" : ">="} $${i++}`);
|
|
16582
|
+
conditions.push(`(NOT r."isPending" AND r."endedAt" ${filters.endedAt.startExclusive ? ">" : ">="} $${i++})`);
|
|
16325
16583
|
params.push(asIsoTimestamp(filters.endedAt.start));
|
|
16326
16584
|
}
|
|
16327
16585
|
if (filters.endedAt?.end) {
|
|
16328
|
-
conditions.push(`r."endedAt" ${filters.endedAt.endExclusive ? "<" : "<="} $${i++}`);
|
|
16586
|
+
conditions.push(`(NOT r."isPending" AND r."endedAt" ${filters.endedAt.endExclusive ? "<" : "<="} $${i++})`);
|
|
16329
16587
|
params.push(asIsoTimestamp(filters.endedAt.end));
|
|
16330
16588
|
}
|
|
16331
16589
|
if (filters.spanType !== void 0) {
|
|
@@ -16397,10 +16655,10 @@ function buildListTracesFilters(filters, spanTable, nextParamIdx) {
|
|
|
16397
16655
|
conditions.push(`r."error" IS NOT NULL`);
|
|
16398
16656
|
break;
|
|
16399
16657
|
case TraceStatus.RUNNING:
|
|
16400
|
-
conditions.push(`
|
|
16658
|
+
conditions.push(`r."isPending"`);
|
|
16401
16659
|
break;
|
|
16402
16660
|
case TraceStatus.SUCCESS:
|
|
16403
|
-
conditions.push(`r."error" IS NULL`);
|
|
16661
|
+
conditions.push(`r."error" IS NULL AND NOT r."isPending"`);
|
|
16404
16662
|
break;
|
|
16405
16663
|
}
|
|
16406
16664
|
if (filters.hasChildError !== void 0) {
|
|
@@ -16537,8 +16795,8 @@ function buildBranchSpanTypeClause(userSpanType, params, startIdx) {
|
|
|
16537
16795
|
* NOT have a `parentSpanId IS NULL` predicate — branches can be nested under
|
|
16538
16796
|
* other spans. Returns the bound params and the next param index.
|
|
16539
16797
|
*/
|
|
16540
|
-
function buildListBranchesFilters(filters, spanType, nextParamIdx) {
|
|
16541
|
-
const conditions = [];
|
|
16798
|
+
function buildListBranchesFilters(filters, spanType, spanTable, nextParamIdx) {
|
|
16799
|
+
const conditions = [latestSpanPredicate(spanTable)];
|
|
16542
16800
|
const params = [];
|
|
16543
16801
|
let i = nextParamIdx;
|
|
16544
16802
|
const spanTypeClause = buildBranchSpanTypeClause(spanType, params, i);
|
|
@@ -16559,11 +16817,11 @@ function buildListBranchesFilters(filters, spanType, nextParamIdx) {
|
|
|
16559
16817
|
params.push(asIsoTimestamp(filters.startedAt.end));
|
|
16560
16818
|
}
|
|
16561
16819
|
if (filters.endedAt?.start) {
|
|
16562
|
-
conditions.push(`r."endedAt" ${filters.endedAt.startExclusive ? ">" : ">="} $${i++}`);
|
|
16820
|
+
conditions.push(`(NOT r."isPending" AND r."endedAt" ${filters.endedAt.startExclusive ? ">" : ">="} $${i++})`);
|
|
16563
16821
|
params.push(asIsoTimestamp(filters.endedAt.start));
|
|
16564
16822
|
}
|
|
16565
16823
|
if (filters.endedAt?.end) {
|
|
16566
|
-
conditions.push(`r."endedAt" ${filters.endedAt.endExclusive ? "<" : "<="} $${i++}`);
|
|
16824
|
+
conditions.push(`(NOT r."isPending" AND r."endedAt" ${filters.endedAt.endExclusive ? "<" : "<="} $${i++})`);
|
|
16567
16825
|
params.push(asIsoTimestamp(filters.endedAt.end));
|
|
16568
16826
|
}
|
|
16569
16827
|
if (filters.traceId !== void 0) {
|
|
@@ -16675,10 +16933,10 @@ function buildListBranchesFilters(filters, spanType, nextParamIdx) {
|
|
|
16675
16933
|
conditions.push(`r."error" IS NOT NULL`);
|
|
16676
16934
|
break;
|
|
16677
16935
|
case TraceStatus.RUNNING:
|
|
16678
|
-
conditions.push(`
|
|
16936
|
+
conditions.push(`r."isPending"`);
|
|
16679
16937
|
break;
|
|
16680
16938
|
case TraceStatus.SUCCESS:
|
|
16681
|
-
conditions.push(`r."error" IS NULL`);
|
|
16939
|
+
conditions.push(`r."error" IS NULL AND NOT r."isPending"`);
|
|
16682
16940
|
break;
|
|
16683
16941
|
}
|
|
16684
16942
|
return {
|
|
@@ -16697,7 +16955,7 @@ async function listBranches(client, schema, args) {
|
|
|
16697
16955
|
return listBranchesPage(client, span, filters, pagination.page, pagination.perPage, orderBy.field, orderBy.direction);
|
|
16698
16956
|
}
|
|
16699
16957
|
async function listBranchesPage(client, span, filters, page, perPage, orderField, orderDir) {
|
|
16700
|
-
const built = buildListBranchesFilters(filters, filters?.spanType, 1);
|
|
16958
|
+
const built = buildListBranchesFilters(filters, filters?.spanType, span, 1);
|
|
16701
16959
|
if (!built) {
|
|
16702
16960
|
const deltaCursor = deltaPollingFeatureEnabled() ? await readBranchesStreamHeadCursor(client, span, filters) : void 0;
|
|
16703
16961
|
return {
|
|
@@ -16751,7 +17009,7 @@ async function listBranchesDelta(client, span, filters, after, limit) {
|
|
|
16751
17009
|
deltaCursor
|
|
16752
17010
|
};
|
|
16753
17011
|
}
|
|
16754
|
-
const built = buildListBranchesFilters(filters, filters?.spanType, 1);
|
|
17012
|
+
const built = buildListBranchesFilters(filters, filters?.spanType, span, 1);
|
|
16755
17013
|
if (!built) return {
|
|
16756
17014
|
branches: [],
|
|
16757
17015
|
delta: {
|
|
@@ -16794,14 +17052,35 @@ async function readBranchesStreamHeadCursor(client, span, filters) {
|
|
|
16794
17052
|
}
|
|
16795
17053
|
//#endregion
|
|
16796
17054
|
//#region src/storage/domains/observability/v-next/tracing.ts
|
|
16797
|
-
async function
|
|
16798
|
-
const
|
|
17055
|
+
async function writeSpanRows(client, schema, rows) {
|
|
17056
|
+
const deduped = dedupeSpanRows(rows);
|
|
17057
|
+
const insert = buildInsert(schema, TABLE_SPAN_EVENTS, deduped, spanConflictClause(Object.keys(deduped[0] ?? {})));
|
|
16799
17058
|
if (insert) await client.query(insert.text, insert.values);
|
|
16800
17059
|
}
|
|
17060
|
+
/**
|
|
17061
|
+
* Collapse rows that share a primary key within a single batch. Postgres
|
|
17062
|
+
* rejects an `ON CONFLICT DO UPDATE` statement whose VALUES list would touch
|
|
17063
|
+
* the same row twice, which a batch containing both the start and the end of a
|
|
17064
|
+
* zero-duration span would do. The ended row wins, matching what the conflict
|
|
17065
|
+
* clause would have done across two separate statements.
|
|
17066
|
+
*/
|
|
17067
|
+
function dedupeSpanRows(rows) {
|
|
17068
|
+
if (rows.length < 2) return rows;
|
|
17069
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
17070
|
+
for (const row of rows) {
|
|
17071
|
+
const key = `${row.traceId}\u0000${row.spanId}\u0000${String(row.endedAt)}`;
|
|
17072
|
+
const existing = byKey.get(key);
|
|
17073
|
+
if (existing && !existing.isPending) continue;
|
|
17074
|
+
byKey.set(key, row);
|
|
17075
|
+
}
|
|
17076
|
+
return [...byKey.values()];
|
|
17077
|
+
}
|
|
17078
|
+
async function createSpan(client, schema, args) {
|
|
17079
|
+
await writeSpanRows(client, schema, [spanRecordToRow(args.span)]);
|
|
17080
|
+
}
|
|
16801
17081
|
async function batchCreateSpans(client, schema, args) {
|
|
16802
17082
|
if (args.records.length === 0) return;
|
|
16803
|
-
|
|
16804
|
-
if (insert) await client.query(insert.text, insert.values);
|
|
17083
|
+
await writeSpanRows(client, schema, args.records.map(spanRecordToRow));
|
|
16805
17084
|
}
|
|
16806
17085
|
async function getSpans(client, schema, args) {
|
|
16807
17086
|
if (args.spanIds.length === 0) return {
|
|
@@ -16809,10 +17088,13 @@ async function getSpans(client, schema, args) {
|
|
|
16809
17088
|
spans: []
|
|
16810
17089
|
};
|
|
16811
17090
|
const table = qualifiedTable(schema, TABLE_SPAN_EVENTS);
|
|
16812
|
-
const rows = await client.manyOrNone(`SELECT
|
|
16813
|
-
|
|
16814
|
-
|
|
16815
|
-
|
|
17091
|
+
const rows = await client.manyOrNone(`SELECT * FROM (
|
|
17092
|
+
SELECT DISTINCT ON ("spanId") ${SPAN_SELECT_COLUMNS}
|
|
17093
|
+
FROM ${table}
|
|
17094
|
+
WHERE "traceId" = $1
|
|
17095
|
+
AND "spanId" = ANY($2::text[])
|
|
17096
|
+
ORDER BY "spanId", ${SPAN_COLLAPSE_ORDER}
|
|
17097
|
+
) collapsed
|
|
16816
17098
|
ORDER BY "startedAt" ASC`, [args.traceId, args.spanIds]);
|
|
16817
17099
|
return {
|
|
16818
17100
|
traceId: args.traceId,
|
|
@@ -16824,16 +17106,19 @@ async function getSpan(client, schema, args) {
|
|
|
16824
17106
|
const row = await client.oneOrNone(`SELECT ${SPAN_SELECT_COLUMNS}
|
|
16825
17107
|
FROM ${table}
|
|
16826
17108
|
WHERE "traceId" = $1 AND "spanId" = $2
|
|
16827
|
-
ORDER BY
|
|
17109
|
+
ORDER BY ${SPAN_COLLAPSE_ORDER}
|
|
16828
17110
|
LIMIT 1`, [args.traceId, args.spanId]);
|
|
16829
17111
|
if (!row) return null;
|
|
16830
17112
|
return { span: rowToSpanRecord(row) };
|
|
16831
17113
|
}
|
|
16832
17114
|
async function getTrace(client, schema, args) {
|
|
16833
17115
|
const table = qualifiedTable(schema, TABLE_SPAN_EVENTS);
|
|
16834
|
-
const rows = await client.manyOrNone(`SELECT
|
|
16835
|
-
|
|
16836
|
-
|
|
17116
|
+
const rows = await client.manyOrNone(`SELECT * FROM (
|
|
17117
|
+
SELECT DISTINCT ON ("spanId") ${SPAN_SELECT_COLUMNS}
|
|
17118
|
+
FROM ${table}
|
|
17119
|
+
WHERE "traceId" = $1
|
|
17120
|
+
ORDER BY "spanId", ${SPAN_COLLAPSE_ORDER}
|
|
17121
|
+
) collapsed
|
|
16837
17122
|
ORDER BY "startedAt" ASC`, [args.traceId]);
|
|
16838
17123
|
if (!rows.length) return null;
|
|
16839
17124
|
return {
|
|
@@ -16843,9 +17128,12 @@ async function getTrace(client, schema, args) {
|
|
|
16843
17128
|
}
|
|
16844
17129
|
async function getTraceLight(client, schema, args) {
|
|
16845
17130
|
const table = qualifiedTable(schema, TABLE_SPAN_EVENTS);
|
|
16846
|
-
const rows = await client.manyOrNone(`SELECT
|
|
16847
|
-
|
|
16848
|
-
|
|
17131
|
+
const rows = await client.manyOrNone(`SELECT * FROM (
|
|
17132
|
+
SELECT DISTINCT ON ("spanId") ${SPAN_LIGHT_SELECT_COLUMNS}
|
|
17133
|
+
FROM ${table}
|
|
17134
|
+
WHERE "traceId" = $1
|
|
17135
|
+
ORDER BY "spanId", ${SPAN_COLLAPSE_ORDER}
|
|
17136
|
+
) collapsed
|
|
16849
17137
|
ORDER BY "startedAt" ASC`, [args.traceId]);
|
|
16850
17138
|
if (!rows.length) return null;
|
|
16851
17139
|
return {
|
|
@@ -16956,6 +17244,11 @@ var ObservabilityStoragePostgresVNext = class ObservabilityStoragePostgresVNext
|
|
|
16956
17244
|
} catch (error) {
|
|
16957
17245
|
if (!isDuplicateRelationError(error)) throw error;
|
|
16958
17246
|
}
|
|
17247
|
+
for (const { table, column, ddl } of additiveColumns(this.#schema)) if (!await this.#client.oneOrNone(columnExistsSQL, [
|
|
17248
|
+
this.#schema,
|
|
17249
|
+
table,
|
|
17250
|
+
column
|
|
17251
|
+
])) await this.#client.none(ddl);
|
|
16959
17252
|
for (const ddl of allIndexDDL(this.#schema)) try {
|
|
16960
17253
|
await this.#client.none(ddl);
|
|
16961
17254
|
} catch (error) {
|
|
@@ -17052,8 +17345,8 @@ var ObservabilityStoragePostgresVNext = class ObservabilityStoragePostgresVNext
|
|
|
17052
17345
|
}
|
|
17053
17346
|
get observabilityStrategy() {
|
|
17054
17347
|
return {
|
|
17055
|
-
preferred: "
|
|
17056
|
-
supported: ["
|
|
17348
|
+
preferred: "event-sourced",
|
|
17349
|
+
supported: ["event-sourced"]
|
|
17057
17350
|
};
|
|
17058
17351
|
}
|
|
17059
17352
|
getFeatures() {
|
|
@@ -20477,18 +20770,6 @@ function workflowSnapshotStatusIndexSQL(indexName, schemaName) {
|
|
|
20477
20770
|
schemaName: getSchemaName(schemaName)
|
|
20478
20771
|
})} (workflow_name, (snapshot ->> 'status'), "createdAt" DESC)`;
|
|
20479
20772
|
}
|
|
20480
|
-
/**
|
|
20481
|
-
* Sanitizes JSON string for PostgreSQL jsonb:
|
|
20482
|
-
* - Removes problematic Unicode sequences:
|
|
20483
|
-
* - \u0000 (null character) - causes error 22P05 "unsupported Unicode escape sequence"
|
|
20484
|
-
* - \uD800-\uDFFF (unpaired surrogates) - causes "Unicode low surrogate must follow a high surrogate"
|
|
20485
|
-
* - \\uD800 (escaped-backslash + surrogate, e.g. from JS regex literals like [^\ud800-\udfff]):
|
|
20486
|
-
* removing just \uXXXX would leave a dangling backslash that creates a new invalid escape (e.g. \-)
|
|
20487
|
-
* - Escapes any remaining invalid JSON escape sequences (e.g. \v, \k, \-)
|
|
20488
|
-
*/
|
|
20489
|
-
function sanitizeJsonForPg(jsonString) {
|
|
20490
|
-
return jsonString.replace(/\\\\?u(0000|[Dd][89A-Fa-f][0-9A-Fa-f]{2})/g, "").replace(/(^|[^\\])(\\(?!["\\/bfnrtu]))/g, "$1\\\\");
|
|
20491
|
-
}
|
|
20492
20773
|
var WorkflowsPG = class WorkflowsPG extends WorkflowsStorage {
|
|
20493
20774
|
#db;
|
|
20494
20775
|
#schema;
|
|
@@ -20768,11 +21049,11 @@ var WorkflowsPG = class WorkflowsPG extends WorkflowsStorage {
|
|
|
20768
21049
|
await this.#db.client.none(`INSERT INTO ${getTableName({
|
|
20769
21050
|
indexName: TABLE_WORKFLOW_SNAPSHOT,
|
|
20770
21051
|
schemaName: getSchemaName(this.#schema)
|
|
20771
|
-
})}
|
|
21052
|
+
})} AS t
|
|
20772
21053
|
(workflow_name, run_id, "resourceId", snapshot, "createdAt", "updatedAt", "createdAtZ", "updatedAtZ")
|
|
20773
21054
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
|
20774
21055
|
ON CONFLICT (workflow_name, run_id) DO UPDATE
|
|
20775
|
-
SET "resourceId" = $3, snapshot = $4, "updatedAt" = $6, "updatedAtZ" = $8`, [
|
|
21056
|
+
SET "resourceId" = COALESCE($3, t."resourceId"), snapshot = $4, "updatedAt" = $6, "updatedAtZ" = $8`, [
|
|
20776
21057
|
workflowName,
|
|
20777
21058
|
runId,
|
|
20778
21059
|
resourceId,
|
|
@@ -21730,6 +22011,12 @@ var PgFactoryStorageOps = class {
|
|
|
21730
22011
|
async findMany(collection, where, opts) {
|
|
21731
22012
|
return this.#select(this.#queryable, collection, where, opts);
|
|
21732
22013
|
}
|
|
22014
|
+
async count(collection, where) {
|
|
22015
|
+
const schema = this.#schema(collection);
|
|
22016
|
+
const filter = this.#buildWhere(schema, where);
|
|
22017
|
+
const result = await this.#queryable.query(`SELECT COUNT(*) AS count FROM "${schema.name}" WHERE ${filter.sql}`, filter.args);
|
|
22018
|
+
return Number(result.rows[0]?.count ?? 0);
|
|
22019
|
+
}
|
|
21733
22020
|
async insertOne(collection, row) {
|
|
21734
22021
|
const schema = this.#schema(collection);
|
|
21735
22022
|
const pk = primaryKeyOf(schema);
|