@mastra/pg 1.33.0 → 1.34.0-alpha.1
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/README.md +1 -1
- package/dist/docs/SKILL.md +2 -2
- package/dist/docs/assets/SOURCE_MAP.json +1 -1
- package/dist/docs/references/docs-memory-semantic-recall.md +20 -20
- package/dist/docs/references/integrations-databases-neon.md +1 -1
- package/dist/docs/references/integrations-databases-postgresql.md +31 -0
- package/dist/docs/references/{reference-vectors-pg.md → integrations-vectors-pg.md} +12 -0
- package/dist/docs/references/reference-memory-memory-class.md +77 -0
- package/dist/docs/references/reference-rag-metadata-filters.md +11 -11
- package/dist/docs/references/reference-rag-vector-databases.md +3 -3
- package/dist/index.cjs +641 -304
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +642 -305
- package/dist/index.js.map +1 -1
- package/dist/storage/client.d.ts +1 -0
- package/dist/storage/client.d.ts.map +1 -1
- package/dist/storage/db/index-build.d.ts +5 -0
- package/dist/storage/db/index-build.d.ts.map +1 -0
- package/dist/storage/db/index.d.ts +0 -8
- package/dist/storage/db/index.d.ts.map +1 -1
- package/dist/storage/db/pg-errors.d.ts +6 -0
- package/dist/storage/db/pg-errors.d.ts.map +1 -1
- package/dist/storage/db/schema-snapshot.d.ts +4 -7
- package/dist/storage/db/schema-snapshot.d.ts.map +1 -1
- package/dist/storage/domains/background-tasks/index.d.ts +1 -0
- package/dist/storage/domains/background-tasks/index.d.ts.map +1 -1
- package/dist/storage/domains/experiments/index.d.ts +3 -3
- package/dist/storage/domains/experiments/index.d.ts.map +1 -1
- package/dist/storage/domains/observability/v-next/ddl.d.ts +2 -1
- package/dist/storage/domains/observability/v-next/ddl.d.ts.map +1 -1
- package/dist/storage/domains/observability/v-next/feedback.d.ts +3 -2
- package/dist/storage/domains/observability/v-next/feedback.d.ts.map +1 -1
- package/dist/storage/domains/observability/v-next/index.d.ts +9 -1
- package/dist/storage/domains/observability/v-next/index.d.ts.map +1 -1
- package/dist/storage/domains/observability/v-next/logs.d.ts +2 -1
- package/dist/storage/domains/observability/v-next/logs.d.ts.map +1 -1
- package/dist/storage/domains/observability/v-next/metrics.d.ts +2 -1
- package/dist/storage/domains/observability/v-next/metrics.d.ts.map +1 -1
- package/dist/storage/domains/observability/v-next/partitioning.d.ts +29 -19
- package/dist/storage/domains/observability/v-next/partitioning.d.ts.map +1 -1
- package/dist/storage/domains/observability/v-next/preview-compat.d.ts +4 -0
- package/dist/storage/domains/observability/v-next/preview-compat.d.ts.map +1 -1
- package/dist/storage/domains/observability/v-next/scores.d.ts +3 -2
- package/dist/storage/domains/observability/v-next/scores.d.ts.map +1 -1
- package/dist/storage/domains/observability/v-next/span-query.d.ts +4 -0
- package/dist/storage/domains/observability/v-next/span-query.d.ts.map +1 -1
- package/dist/storage/domains/observability/v-next/sql.d.ts +12 -0
- package/dist/storage/domains/observability/v-next/sql.d.ts.map +1 -1
- package/dist/storage/domains/observability/v-next/thread-query.d.ts +1 -0
- package/dist/storage/domains/observability/v-next/thread-query.d.ts.map +1 -1
- package/dist/storage/domains/observability/v-next/trace-aggregate.d.ts.map +1 -1
- package/dist/storage/domains/observability/v-next/trace-query.d.ts +10 -1
- package/dist/storage/domains/observability/v-next/trace-query.d.ts.map +1 -1
- package/dist/storage/domains/observability/v-next/tracing.d.ts +3 -2
- package/dist/storage/domains/observability/v-next/tracing.d.ts.map +1 -1
- package/dist/storage/index.d.ts +2 -0
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/test-utils.d.ts.map +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -5,12 +5,13 @@ import { parseFieldKey, parseSqlIdentifier } from "@mastra/core/utils";
|
|
|
5
5
|
import { MastraVector, validateTopK, validateUpsertInput } from "@mastra/core/vector";
|
|
6
6
|
import { Mutex } from "async-mutex";
|
|
7
7
|
import * as pg$1 from "pg";
|
|
8
|
-
import pg, { Pool, defaults } from "pg";
|
|
8
|
+
import pg, { Pool, defaults, escapeIdentifier } from "pg";
|
|
9
9
|
import xxhash from "xxhash-wasm";
|
|
10
10
|
import { parse } from "pg-connection-string";
|
|
11
11
|
import { BaseFilterTranslator } from "@mastra/core/vector/filter";
|
|
12
12
|
import { MastraBase } from "@mastra/core/base";
|
|
13
13
|
import { createHash, randomUUID } from "crypto";
|
|
14
|
+
import { setTimeout as setTimeout$1 } from "timers/promises";
|
|
14
15
|
import { coreFeatures } from "@mastra/core/features";
|
|
15
16
|
import { MessageList } from "@mastra/core/agent";
|
|
16
17
|
import { saveScorePayloadSchema } from "@mastra/core/evals";
|
|
@@ -2253,6 +2254,9 @@ var PinnedClientAdapter = class {
|
|
|
2253
2254
|
this.#tail = next.catch(() => void 0);
|
|
2254
2255
|
return next;
|
|
2255
2256
|
}
|
|
2257
|
+
async drain() {
|
|
2258
|
+
await this.#tail;
|
|
2259
|
+
}
|
|
2256
2260
|
connect() {
|
|
2257
2261
|
throw new Error("PinnedClientAdapter.connect() is not supported during PostgresStore.init(). All DDL must flow through the pinned client.");
|
|
2258
2262
|
}
|
|
@@ -2435,6 +2439,39 @@ function buildConstraintName({ baseName, schemaName, maxLength = 63, hashWhenTru
|
|
|
2435
2439
|
return truncateIdentifier(fullName, maxLength);
|
|
2436
2440
|
}
|
|
2437
2441
|
// src/storage/db/pg-errors.ts
|
|
2442
|
+
/**
|
|
2443
|
+
* Postgres error classifiers for concurrent DDL.
|
|
2444
|
+
*
|
|
2445
|
+
* `CREATE SCHEMA / TABLE / INDEX IF NOT EXISTS` checks aren't atomic against
|
|
2446
|
+
* concurrent backends. Two callers racing past the existence probe can both
|
|
2447
|
+
* reach the catalog insert; the loser sees a duplicate-object error. These
|
|
2448
|
+
* helpers identify the exact codes we treat as "already exists by the time we
|
|
2449
|
+
* look" so every other error surfaces normally.
|
|
2450
|
+
*/
|
|
2451
|
+
var IndexBuildError = class extends Error {
|
|
2452
|
+
constructor(indexName, schemaName, state, cause) {
|
|
2453
|
+
super(`Could not create index ${schemaName}.${indexName}: ${state}`, { cause });
|
|
2454
|
+
this.name = "IndexBuildError";
|
|
2455
|
+
}
|
|
2456
|
+
};
|
|
2457
|
+
function* causes(error) {
|
|
2458
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2459
|
+
while (typeof error === "object" && error !== null && !seen.has(error)) {
|
|
2460
|
+
seen.add(error);
|
|
2461
|
+
yield error;
|
|
2462
|
+
error = "cause" in error ? error.cause : void 0;
|
|
2463
|
+
}
|
|
2464
|
+
}
|
|
2465
|
+
/** Index recovery owns its retry budget, even through an outer MastraError. */
|
|
2466
|
+
function isRetryableDeadlock(error) {
|
|
2467
|
+
for (const cause of causes(error)) if (cause instanceof IndexBuildError) return false;
|
|
2468
|
+
for (const cause of causes(error)) if ("code" in cause && cause.code === "40P01") return true;
|
|
2469
|
+
return false;
|
|
2470
|
+
}
|
|
2471
|
+
async function backoffAfterDeadlock(attempt) {
|
|
2472
|
+
const base = 50 * 2 ** (attempt - 1);
|
|
2473
|
+
await new Promise((resolve) => setTimeout(resolve, base + Math.random() * base * 2));
|
|
2474
|
+
}
|
|
2438
2475
|
function asPgError(error) {
|
|
2439
2476
|
return error ?? {};
|
|
2440
2477
|
}
|
|
@@ -2462,6 +2499,99 @@ function isDuplicateSchemaError(error) {
|
|
|
2462
2499
|
if (code === "23505" && constraint === "pg_namespace_nspname_index") return true;
|
|
2463
2500
|
return /schema .* already exists/i.test(message);
|
|
2464
2501
|
}
|
|
2502
|
+
// src/storage/db/index-build.ts
|
|
2503
|
+
/** A cold snapshot cannot arbitrate DDL submitted by independent processes. */
|
|
2504
|
+
async function buildIndex(client, schemaName, indexName, build, logger) {
|
|
2505
|
+
const deadline = performance.now() + 3e4;
|
|
2506
|
+
let attempts = 0;
|
|
2507
|
+
let failure;
|
|
2508
|
+
let state = "not built";
|
|
2509
|
+
let repairFailures = 0;
|
|
2510
|
+
const observe = (db = client) => db.oneOrNone(`SELECT c.oid, i.indrelid AS table_oid, t.relname AS table_name,
|
|
2511
|
+
tn.nspname AS table_schema, i.indisvalid AS valid
|
|
2512
|
+
FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
2513
|
+
LEFT JOIN pg_index i ON i.indexrelid = c.oid
|
|
2514
|
+
LEFT JOIN pg_class t ON t.oid = i.indrelid
|
|
2515
|
+
LEFT JOIN pg_namespace tn ON tn.oid = t.relnamespace
|
|
2516
|
+
WHERE n.nspname = $1 AND c.relname = $2`, [schemaName, indexName]);
|
|
2517
|
+
while (true) {
|
|
2518
|
+
const relation = await observe();
|
|
2519
|
+
if (relation?.valid) return true;
|
|
2520
|
+
if (relation && relation.valid === null) throw new IndexBuildError(indexName, schemaName, "name belongs to a non-index relation", failure);
|
|
2521
|
+
if (relation) {
|
|
2522
|
+
if (await client.oneOrNone(`SELECT p.pid FROM pg_stat_progress_create_index p
|
|
2523
|
+
WHERE p.datid = (SELECT oid FROM pg_database WHERE datname = current_database())
|
|
2524
|
+
AND p.pid <> pg_backend_pid()
|
|
2525
|
+
AND (p.index_relid = $1 OR ((p.index_relid IS NULL OR p.index_relid = 0) AND EXISTS (
|
|
2526
|
+
SELECT 1 FROM pg_locks l WHERE l.pid = p.pid AND l.database = p.datid
|
|
2527
|
+
AND l.locktype = 'relation' AND l.relation = $2 AND l.granted
|
|
2528
|
+
AND l.mode IN ('ShareUpdateExclusiveLock', 'ShareLock')
|
|
2529
|
+
)))
|
|
2530
|
+
LIMIT 1`, [relation.oid, relation.table_oid])) {
|
|
2531
|
+
logger.warn(`Index ${indexName} is being built by another session; skipping`);
|
|
2532
|
+
return false;
|
|
2533
|
+
}
|
|
2534
|
+
if (performance.now() >= deadline && state === "table busy during invalid-index repair") {
|
|
2535
|
+
logger.warn(`Index ${indexName} could not be repaired because its table is busy; skipping`);
|
|
2536
|
+
return false;
|
|
2537
|
+
}
|
|
2538
|
+
if (attempts >= 3 || performance.now() >= deadline) break;
|
|
2539
|
+
state = "abandoned invalid index";
|
|
2540
|
+
attempts++;
|
|
2541
|
+
let lockUnavailable = false;
|
|
2542
|
+
try {
|
|
2543
|
+
const result = await client.tx(async (tx) => {
|
|
2544
|
+
const table = `${escapeIdentifier(relation.table_schema)}.${escapeIdentifier(relation.table_name)}`;
|
|
2545
|
+
await tx.none(`SET LOCAL lock_timeout = '100ms'`);
|
|
2546
|
+
try {
|
|
2547
|
+
await tx.none(`LOCK TABLE ${table} IN ACCESS EXCLUSIVE MODE`);
|
|
2548
|
+
} catch (error) {
|
|
2549
|
+
lockUnavailable = typeof error === "object" && error !== null && "code" in error && error.code === "55P03";
|
|
2550
|
+
throw error;
|
|
2551
|
+
}
|
|
2552
|
+
const current = await observe(tx);
|
|
2553
|
+
if (current?.valid) return "valid";
|
|
2554
|
+
const locked = await tx.one("SELECT to_regclass($1)::oid AS oid", [table]);
|
|
2555
|
+
if (!current || current.oid !== relation.oid || current.table_oid !== locked.oid) return "changed";
|
|
2556
|
+
logger.warn(`Dropping invalid index ${indexName} left by an interrupted build; rebuilding it`);
|
|
2557
|
+
await tx.none(`DROP INDEX ${escapeIdentifier(schemaName)}.${escapeIdentifier(indexName)}`);
|
|
2558
|
+
return "dropped";
|
|
2559
|
+
});
|
|
2560
|
+
if (result === "valid") return true;
|
|
2561
|
+
if (result === "changed") {
|
|
2562
|
+
attempts--;
|
|
2563
|
+
if (performance.now() >= deadline) break;
|
|
2564
|
+
continue;
|
|
2565
|
+
}
|
|
2566
|
+
} catch (error) {
|
|
2567
|
+
attempts--;
|
|
2568
|
+
if (lockUnavailable) {
|
|
2569
|
+
state = "table busy during invalid-index repair";
|
|
2570
|
+
failure ??= error;
|
|
2571
|
+
await setTimeout$1(100);
|
|
2572
|
+
continue;
|
|
2573
|
+
}
|
|
2574
|
+
if (!isDuplicateRelationError(error) && !isRetryableDeadlock(error) && !(typeof error === "object" && error !== null && "code" in error && error.code === "42P01")) throw error;
|
|
2575
|
+
failure ??= error;
|
|
2576
|
+
if (++repairFailures >= 3 || performance.now() >= deadline) break;
|
|
2577
|
+
continue;
|
|
2578
|
+
}
|
|
2579
|
+
} else {
|
|
2580
|
+
if (attempts >= 3 || performance.now() >= deadline) break;
|
|
2581
|
+
attempts++;
|
|
2582
|
+
}
|
|
2583
|
+
try {
|
|
2584
|
+
await build();
|
|
2585
|
+
} catch (error) {
|
|
2586
|
+
if (!isDuplicateRelationError(error) && !isRetryableDeadlock(error)) throw error;
|
|
2587
|
+
failure ??= error;
|
|
2588
|
+
const deadlock = isRetryableDeadlock(error);
|
|
2589
|
+
state = deadlock ? "index build deadlock" : "competing index creation";
|
|
2590
|
+
if (deadlock && attempts < 3 && performance.now() < deadline) await backoffAfterDeadlock(attempts);
|
|
2591
|
+
}
|
|
2592
|
+
}
|
|
2593
|
+
throw new IndexBuildError(indexName, schemaName, state, failure);
|
|
2594
|
+
}
|
|
2465
2595
|
// src/storage/db/sanitize-json.ts
|
|
2466
2596
|
const repair = (value) => value.replace(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g, "�").replaceAll("\0", "");
|
|
2467
2597
|
function repairJson(value) {
|
|
@@ -2517,9 +2647,10 @@ async function loadSchemaSnapshot(client, schemaName) {
|
|
|
2517
2647
|
AND c.relkind IN ('r', 'p')
|
|
2518
2648
|
AND a.attnum > 0
|
|
2519
2649
|
AND NOT a.attisdropped`, [schema]),
|
|
2520
|
-
client.manyOrNone(`SELECT c.relname AS indexname, i.indisreplident AS is_replica_identity, i.indisprimary AS is_primary, i.indisvalid AS is_valid
|
|
2650
|
+
client.manyOrNone(`SELECT c.relname AS indexname, t.relname AS table_name, i.indisreplident AS is_replica_identity, i.indisprimary AS is_primary, i.indisvalid AS is_valid
|
|
2521
2651
|
FROM pg_catalog.pg_index i
|
|
2522
2652
|
JOIN pg_catalog.pg_class c ON c.oid = i.indexrelid
|
|
2653
|
+
JOIN pg_catalog.pg_class t ON t.oid = i.indrelid
|
|
2523
2654
|
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
|
|
2524
2655
|
WHERE n.nspname = $1`, [schema])
|
|
2525
2656
|
]);
|
|
@@ -2550,13 +2681,11 @@ async function loadSchemaSnapshot(client, schemaName) {
|
|
|
2550
2681
|
}
|
|
2551
2682
|
const indexes = /* @__PURE__ */ new Set();
|
|
2552
2683
|
const replicaIdentityIndexes = /* @__PURE__ */ new Set();
|
|
2553
|
-
const primaryKeyIndexes = /* @__PURE__ */ new
|
|
2554
|
-
const invalidIndexes = /* @__PURE__ */ new Set();
|
|
2684
|
+
const primaryKeyIndexes = /* @__PURE__ */ new Map();
|
|
2555
2685
|
for (const row of indexRows) {
|
|
2556
2686
|
if (row.is_valid) indexes.add(row.indexname);
|
|
2557
|
-
else invalidIndexes.add(row.indexname);
|
|
2558
2687
|
if (row.is_replica_identity) replicaIdentityIndexes.add(row.indexname);
|
|
2559
|
-
if (row.is_primary) primaryKeyIndexes.
|
|
2688
|
+
if (row.is_primary) primaryKeyIndexes.set(row.indexname.toLowerCase(), row.table_name);
|
|
2560
2689
|
}
|
|
2561
2690
|
return {
|
|
2562
2691
|
schemaName: schema,
|
|
@@ -2566,8 +2695,7 @@ async function loadSchemaSnapshot(client, schemaName) {
|
|
|
2566
2695
|
notNullColumns,
|
|
2567
2696
|
indexes,
|
|
2568
2697
|
replicaIdentityIndexes,
|
|
2569
|
-
primaryKeyIndexes
|
|
2570
|
-
invalidIndexes
|
|
2698
|
+
primaryKeyIndexes
|
|
2571
2699
|
};
|
|
2572
2700
|
}
|
|
2573
2701
|
// src/storage/db/index.ts
|
|
@@ -3479,14 +3607,17 @@ MIGRATION REQUIRED: Duplicate spans detected in ${duplicateInfo.tableName}\n====
|
|
|
3479
3607
|
baseName: "mastra_ai_spans_traceid_spanid_pk",
|
|
3480
3608
|
schemaName: (this.schemaName ? schemaNamePrefix(this.schemaName) : "") || void 0
|
|
3481
3609
|
});
|
|
3482
|
-
const
|
|
3610
|
+
const fullTableName = getTableName$6({
|
|
3611
|
+
indexName: TABLE_SPANS,
|
|
3612
|
+
schemaName: getSchemaName$6(this.schemaName)
|
|
3613
|
+
});
|
|
3483
3614
|
const snapshot = this.schemaSnapshot;
|
|
3484
|
-
if (snapshot) return snapshot.primaryKeyIndexes.
|
|
3485
|
-
return this.spansPrimaryKeyExistsLive(constraintName,
|
|
3615
|
+
if (snapshot) return snapshot.primaryKeyIndexes.get(constraintName.toLowerCase()) === TABLE_SPANS;
|
|
3616
|
+
return this.spansPrimaryKeyExistsLive(constraintName, fullTableName);
|
|
3486
3617
|
}
|
|
3487
3618
|
/** Live-catalog variant of {@link spansPrimaryKeyExists}, bypassing the snapshot. */
|
|
3488
|
-
async spansPrimaryKeyExistsLive(constraintName,
|
|
3489
|
-
return (await this.client.oneOrNone(`SELECT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = lower($1) AND
|
|
3619
|
+
async spansPrimaryKeyExistsLive(constraintName, fullTableName) {
|
|
3620
|
+
return (await this.client.oneOrNone(`SELECT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = lower($1) AND contype = 'p' AND conrelid = to_regclass($2)) as exists`, [constraintName, fullTableName]))?.exists ?? false;
|
|
3490
3621
|
}
|
|
3491
3622
|
/**
|
|
3492
3623
|
* Adds the PRIMARY KEY constraint on (traceId, spanId) to the spans table.
|
|
@@ -3501,13 +3632,8 @@ MIGRATION REQUIRED: Duplicate spans detected in ${duplicateInfo.tableName}\n====
|
|
|
3501
3632
|
baseName: "mastra_ai_spans_traceid_spanid_pk",
|
|
3502
3633
|
schemaName: (this.schemaName ? schemaNamePrefix(this.schemaName) : "") || void 0
|
|
3503
3634
|
});
|
|
3504
|
-
const schemaFilter = this.schemaName || "public";
|
|
3505
3635
|
try {
|
|
3506
|
-
if (
|
|
3507
|
-
SELECT EXISTS (
|
|
3508
|
-
SELECT 1 FROM pg_constraint WHERE conname = lower($1) AND connamespace = (SELECT oid FROM pg_namespace WHERE nspname = $2)
|
|
3509
|
-
) as exists
|
|
3510
|
-
`, [constraintName, schemaFilter]))?.exists) {
|
|
3636
|
+
if (await this.spansPrimaryKeyExistsLive(constraintName, fullTableName)) {
|
|
3511
3637
|
this.logger?.debug?.(`PRIMARY KEY constraint ${constraintName} already exists on ${fullTableName}`);
|
|
3512
3638
|
return;
|
|
3513
3639
|
}
|
|
@@ -3516,12 +3642,13 @@ MIGRATION REQUIRED: Duplicate spans detected in ${duplicateInfo.tableName}\n====
|
|
|
3516
3642
|
ADD CONSTRAINT ${constraintName}
|
|
3517
3643
|
PRIMARY KEY ("traceId", "spanId")
|
|
3518
3644
|
`);
|
|
3519
|
-
this.schemaSnapshot?.primaryKeyIndexes.
|
|
3645
|
+
this.schemaSnapshot?.primaryKeyIndexes.set(constraintName.toLowerCase(), TABLE_SPANS);
|
|
3520
3646
|
this.logger?.info?.(`Added PRIMARY KEY constraint ${constraintName} to ${fullTableName}`);
|
|
3521
3647
|
} catch (error) {
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3648
|
+
const primaryKeyRace = typeof error === "object" && error !== null && "code" in error && error.code === "42P16";
|
|
3649
|
+
if (isDuplicateRelationError(error) || primaryKeyRace) {
|
|
3650
|
+
if (await this.spansPrimaryKeyExistsLive(constraintName, fullTableName)) {
|
|
3651
|
+
this.schemaSnapshot?.primaryKeyIndexes.set(constraintName.toLowerCase(), TABLE_SPANS);
|
|
3525
3652
|
this.logger?.debug?.(`PRIMARY KEY constraint ${constraintName} was created by another process`);
|
|
3526
3653
|
return;
|
|
3527
3654
|
}
|
|
@@ -3738,7 +3865,6 @@ MIGRATION REQUIRED: Duplicate spans detected in ${duplicateInfo.tableName}\n====
|
|
|
3738
3865
|
});
|
|
3739
3866
|
const snapshot = this.schemaSnapshot;
|
|
3740
3867
|
if (snapshot?.indexes.has(name)) return;
|
|
3741
|
-
if (!await this.prepareIndexBuild(name, schemaName)) return;
|
|
3742
3868
|
const uniqueStr = unique ? "UNIQUE " : "";
|
|
3743
3869
|
const concurrentStr = concurrent ? "CONCURRENTLY " : "";
|
|
3744
3870
|
const methodStr = method !== "btree" ? `USING ${method} ` : "";
|
|
@@ -3756,9 +3882,8 @@ MIGRATION REQUIRED: Duplicate spans detected in ${duplicateInfo.tableName}\n====
|
|
|
3756
3882
|
const tablespaceStr = tablespace ? ` TABLESPACE ${tablespace}` : "";
|
|
3757
3883
|
let withStr = "";
|
|
3758
3884
|
if (storage && Object.keys(storage).length > 0) withStr = ` WITH (${Object.entries(storage).map(([key, value]) => `${key} = ${value}`).join(", ")})`;
|
|
3759
|
-
const sql = `CREATE ${uniqueStr}INDEX ${concurrentStr}${`"${parseSqlIdentifier(name, "index name")}"`} ON ${fullTableName} ${methodStr}(${columnsStr})${withStr}${tablespaceStr}${whereStr}`;
|
|
3760
|
-
await this.timedIndexBuild(name, sql);
|
|
3761
|
-
snapshot?.indexes.add(name);
|
|
3885
|
+
const sql = `CREATE ${uniqueStr}INDEX ${concurrentStr}IF NOT EXISTS ${`"${parseSqlIdentifier(name, "index name")}"`} ON ${fullTableName} ${methodStr}(${columnsStr})${withStr}${tablespaceStr}${whereStr}`;
|
|
3886
|
+
if (await buildIndex(this.client, schemaName, name, () => this.timedIndexBuild(name, sql), this.logger)) snapshot?.indexes.add(name);
|
|
3762
3887
|
} catch (error) {
|
|
3763
3888
|
throw new MastraError({
|
|
3764
3889
|
id: createStorageErrorId("PG", "INDEX_CREATE", "FAILED"),
|
|
@@ -3783,36 +3908,7 @@ MIGRATION REQUIRED: Duplicate spans detected in ${duplicateInfo.tableName}\n====
|
|
|
3783
3908
|
async createIndexFromStatement(indexName, sql) {
|
|
3784
3909
|
const snapshot = this.schemaSnapshot;
|
|
3785
3910
|
if (snapshot?.indexes.has(indexName)) return;
|
|
3786
|
-
if (
|
|
3787
|
-
await this.timedIndexBuild(indexName, sql);
|
|
3788
|
-
snapshot?.indexes.add(indexName);
|
|
3789
|
-
}
|
|
3790
|
-
/**
|
|
3791
|
-
* Decides whether `indexName` still needs building. Returns false when a valid
|
|
3792
|
-
* index already exists, or when another session is currently building it.
|
|
3793
|
-
* An invalid index left by an interrupted `CREATE INDEX CONCURRENTLY` is
|
|
3794
|
-
* dropped (concurrently) so the caller rebuilds it — `IF NOT EXISTS` would
|
|
3795
|
-
* otherwise keep the broken index forever.
|
|
3796
|
-
*/
|
|
3797
|
-
async prepareIndexBuild(indexName, schemaName) {
|
|
3798
|
-
const snapshot = this.schemaSnapshot;
|
|
3799
|
-
if (snapshot && !snapshot.invalidIndexes.has(indexName)) return true;
|
|
3800
|
-
const existing = await this.client.oneOrNone(`SELECT i.indisvalid AS is_valid,
|
|
3801
|
-
EXISTS (SELECT 1 FROM pg_stat_progress_create_index p WHERE p.index_relid = c.oid) AS building
|
|
3802
|
-
FROM pg_catalog.pg_index i
|
|
3803
|
-
JOIN pg_catalog.pg_class c ON c.oid = i.indexrelid
|
|
3804
|
-
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
|
|
3805
|
-
WHERE c.relname = $1 AND n.nspname = $2`, [indexName, schemaName]);
|
|
3806
|
-
if (!existing) return true;
|
|
3807
|
-
if (existing.is_valid) return false;
|
|
3808
|
-
if (existing.building) {
|
|
3809
|
-
this.logger.warn(`Index ${indexName} is being built by another session; skipping`);
|
|
3810
|
-
return false;
|
|
3811
|
-
}
|
|
3812
|
-
this.logger.warn(`Dropping invalid index ${indexName} left by an interrupted build; rebuilding it`);
|
|
3813
|
-
const quotedIndexName = `"${parseSqlIdentifier(indexName, "index name")}"`;
|
|
3814
|
-
await this.client.none(`DROP INDEX CONCURRENTLY IF EXISTS ${getSchemaName$6(this.schemaName)}.${quotedIndexName}`);
|
|
3815
|
-
return true;
|
|
3911
|
+
if (await buildIndex(this.client, this.schemaName || "public", indexName, () => this.timedIndexBuild(indexName, sql), this.logger)) snapshot?.indexes.add(indexName);
|
|
3816
3912
|
}
|
|
3817
3913
|
async timedIndexBuild(indexName, sql) {
|
|
3818
3914
|
const startedAt = Date.now();
|
|
@@ -5166,6 +5262,7 @@ function rowToTask(row) {
|
|
|
5166
5262
|
const suspendedAt = row.suspendedAtZ || row.suspendedAt;
|
|
5167
5263
|
const completedAt = row.completedAtZ || row.completedAt;
|
|
5168
5264
|
const leaseExpiresAt = row.leaseExpiresAtZ || row.leaseExpiresAt;
|
|
5265
|
+
const expiresAt = row.expiresAt != null ? row.expiresAtZ || row.expiresAt : void 0;
|
|
5169
5266
|
return {
|
|
5170
5267
|
id: row.id,
|
|
5171
5268
|
status: row.status,
|
|
@@ -5187,10 +5284,13 @@ function rowToTask(row) {
|
|
|
5187
5284
|
suspendedAt: suspendedAt ? suspendedAt instanceof Date ? suspendedAt : new Date(suspendedAt) : void 0,
|
|
5188
5285
|
completedAt: completedAt ? completedAt instanceof Date ? completedAt : new Date(completedAt) : void 0,
|
|
5189
5286
|
ownerId: row.ownerId ?? void 0,
|
|
5190
|
-
leaseExpiresAt: leaseExpiresAt ? leaseExpiresAt instanceof Date ? leaseExpiresAt : new Date(leaseExpiresAt) : void 0
|
|
5287
|
+
leaseExpiresAt: leaseExpiresAt ? leaseExpiresAt instanceof Date ? leaseExpiresAt : new Date(leaseExpiresAt) : void 0,
|
|
5288
|
+
expiresAt: expiresAt ? expiresAt instanceof Date ? expiresAt : new Date(expiresAt) : void 0,
|
|
5289
|
+
metadata: parseJson$2(row.metadata)
|
|
5191
5290
|
};
|
|
5192
5291
|
}
|
|
5193
5292
|
var BackgroundTasksPG = class BackgroundTasksPG extends BackgroundTasksStorage {
|
|
5293
|
+
supportsTaskExpiry = true;
|
|
5194
5294
|
#db;
|
|
5195
5295
|
#schema;
|
|
5196
5296
|
#skipDefaultIndexes;
|
|
@@ -5231,7 +5331,9 @@ var BackgroundTasksPG = class BackgroundTasksPG extends BackgroundTasksStorage {
|
|
|
5231
5331
|
"suspend_payload",
|
|
5232
5332
|
"suspendedAt",
|
|
5233
5333
|
"ownerId",
|
|
5234
|
-
"leaseExpiresAt"
|
|
5334
|
+
"leaseExpiresAt",
|
|
5335
|
+
"expiresAt",
|
|
5336
|
+
"metadata"
|
|
5235
5337
|
]
|
|
5236
5338
|
});
|
|
5237
5339
|
await this.createDefaultIndexes();
|
|
@@ -5365,7 +5467,10 @@ var BackgroundTasksPG = class BackgroundTasksPG extends BackgroundTasksStorage {
|
|
|
5365
5467
|
completedAtZ: task.completedAt?.toISOString() ?? null,
|
|
5366
5468
|
ownerId: task.ownerId ?? null,
|
|
5367
5469
|
leaseExpiresAt: task.leaseExpiresAt?.toISOString() ?? null,
|
|
5368
|
-
leaseExpiresAtZ: task.leaseExpiresAt?.toISOString() ?? null
|
|
5470
|
+
leaseExpiresAtZ: task.leaseExpiresAt?.toISOString() ?? null,
|
|
5471
|
+
expiresAt: task.expiresAt?.toISOString() ?? null,
|
|
5472
|
+
expiresAtZ: task.expiresAt?.toISOString() ?? null,
|
|
5473
|
+
metadata: serializeJson(task.metadata)
|
|
5369
5474
|
}
|
|
5370
5475
|
});
|
|
5371
5476
|
}
|
|
@@ -5421,6 +5526,16 @@ var BackgroundTasksPG = class BackgroundTasksPG extends BackgroundTasksStorage {
|
|
|
5421
5526
|
const val = update.leaseExpiresAt?.toISOString() ?? null;
|
|
5422
5527
|
params.push(val, val);
|
|
5423
5528
|
}
|
|
5529
|
+
if ("expiresAt" in update) {
|
|
5530
|
+
setClauses.push(`"expiresAt" = $${paramIdx++}`);
|
|
5531
|
+
setClauses.push(`"expiresAtZ" = $${paramIdx++}`);
|
|
5532
|
+
const val = update.expiresAt?.toISOString() ?? null;
|
|
5533
|
+
params.push(val, val);
|
|
5534
|
+
}
|
|
5535
|
+
if ("metadata" in update) {
|
|
5536
|
+
setClauses.push(`"metadata" = $${paramIdx++}`);
|
|
5537
|
+
params.push(serializeJson(update.metadata));
|
|
5538
|
+
}
|
|
5424
5539
|
if (setClauses.length === 0) return false;
|
|
5425
5540
|
const table = getTableName$4(getSchemaName$4(this.#schema));
|
|
5426
5541
|
params.push(taskId);
|
|
@@ -5443,6 +5558,13 @@ var BackgroundTasksPG = class BackgroundTasksPG extends BackgroundTasksStorage {
|
|
|
5443
5558
|
params.push(options.expectedLeaseExpiresAt.toISOString());
|
|
5444
5559
|
}
|
|
5445
5560
|
}
|
|
5561
|
+
if (options?.expectedSuspendedAt !== void 0) {
|
|
5562
|
+
if (options.expectedSuspendedAt === null) where += ` AND "suspendedAtZ" IS NULL`;
|
|
5563
|
+
else {
|
|
5564
|
+
where += ` AND "suspendedAtZ" = $${paramIdx++}`;
|
|
5565
|
+
params.push(options.expectedSuspendedAt.toISOString());
|
|
5566
|
+
}
|
|
5567
|
+
}
|
|
5446
5568
|
return ((await this.#db.client.query(`UPDATE ${table} SET ${setClauses.join(", ")} WHERE ${where}`, params)).rowCount ?? 0) > 0;
|
|
5447
5569
|
}
|
|
5448
5570
|
async getTask(taskId) {
|
|
@@ -5485,7 +5607,9 @@ var BackgroundTasksPG = class BackgroundTasksPG extends BackgroundTasksStorage {
|
|
|
5485
5607
|
conditions.push(`"tool_call_id" = $${paramIdx++}`);
|
|
5486
5608
|
params.push(filter.toolCallId);
|
|
5487
5609
|
}
|
|
5488
|
-
const dateCol = filter.dateFilterBy === "startedAt" ? "\"startedAt\"" : filter.dateFilterBy === "suspendedAt" ? "\"suspendedAt\"" : filter.dateFilterBy === "completedAt" ? "\"completedAt\"" : "\"createdAt\"";
|
|
5610
|
+
const dateCol = filter.dateFilterBy === "startedAt" ? "\"startedAt\"" : filter.dateFilterBy === "suspendedAt" ? "\"suspendedAt\"" : filter.dateFilterBy === "completedAt" ? "\"completedAt\"" : filter.dateFilterBy === "expiresAt" ? "\"expiresAtZ\"" : "\"createdAt\"";
|
|
5611
|
+
if (filter.hasExpiresAt !== void 0) conditions.push(filter.hasExpiresAt ? "\"expiresAt\" IS NOT NULL" : "\"expiresAt\" IS NULL");
|
|
5612
|
+
if (filter.dateFilterBy === "expiresAt" && (filter.fromDate || filter.toDate)) conditions.push("\"expiresAt\" IS NOT NULL");
|
|
5489
5613
|
if (filter.fromDate) {
|
|
5490
5614
|
conditions.push(`${dateCol} >= $${paramIdx++}`);
|
|
5491
5615
|
params.push(filter.fromDate.toISOString());
|
|
@@ -5526,7 +5650,9 @@ var BackgroundTasksPG = class BackgroundTasksPG extends BackgroundTasksStorage {
|
|
|
5526
5650
|
conditions.push(`"status" IN (${placeholders.join(", ")})`);
|
|
5527
5651
|
params.push(...statuses);
|
|
5528
5652
|
}
|
|
5529
|
-
const dateCol = filter.dateFilterBy === "startedAt" ? "\"startedAt\"" : filter.dateFilterBy === "suspendedAt" ? "\"suspendedAt\"" : filter.dateFilterBy === "completedAt" ? "\"completedAt\"" : "\"createdAt\"";
|
|
5653
|
+
const dateCol = filter.dateFilterBy === "startedAt" ? "\"startedAt\"" : filter.dateFilterBy === "suspendedAt" ? "\"suspendedAt\"" : filter.dateFilterBy === "completedAt" ? "\"completedAt\"" : filter.dateFilterBy === "expiresAt" ? "\"expiresAtZ\"" : "\"createdAt\"";
|
|
5654
|
+
if (filter.hasExpiresAt !== void 0) conditions.push(filter.hasExpiresAt ? "\"expiresAt\" IS NOT NULL" : "\"expiresAt\" IS NULL");
|
|
5655
|
+
if (filter.dateFilterBy === "expiresAt" && (filter.fromDate || filter.toDate)) conditions.push("\"expiresAt\" IS NOT NULL");
|
|
5530
5656
|
if (filter.fromDate) {
|
|
5531
5657
|
conditions.push(`${dateCol} >= $${paramIdx++}`);
|
|
5532
5658
|
params.push(filter.fromDate.toISOString());
|
|
@@ -7142,6 +7268,19 @@ var DatasetsPG = class DatasetsPG extends DatasetsStorage {
|
|
|
7142
7268
|
};
|
|
7143
7269
|
// src/storage/domains/experiments/index.ts
|
|
7144
7270
|
const DEFAULT_PRUNE_BATCH_SIZE = 1e3;
|
|
7271
|
+
/**
|
|
7272
|
+
* Column list for reading experiment rows. Each `timestamp` column comes back
|
|
7273
|
+
* twice: as itself, and as its `*Z` mirror derived in the query with
|
|
7274
|
+
* `AT TIME ZONE 'UTC'`. A plain `timestamp` otherwise round-trips through the
|
|
7275
|
+
* session timezone, and reading the stored `*Z` column instead would return the
|
|
7276
|
+
* insert time for rows written before the mirrors were filled in. Deriving it
|
|
7277
|
+
* here makes legacy rows read correctly with no backfill to run at startup.
|
|
7278
|
+
*/
|
|
7279
|
+
function selectWithMirrors(schema) {
|
|
7280
|
+
return Object.entries(schema).flatMap(([name, definition]) => definition.type === "timestamp" ? [`"${name}"`, `"${name}" AT TIME ZONE 'UTC' AS "${name}Z"`] : [`"${name}"`]).join(", ");
|
|
7281
|
+
}
|
|
7282
|
+
const EXPERIMENT_COLUMNS = selectWithMirrors(EXPERIMENTS_SCHEMA);
|
|
7283
|
+
const EXPERIMENT_RESULT_COLUMNS = selectWithMirrors(EXPERIMENT_RESULTS_SCHEMA);
|
|
7145
7284
|
var ExperimentsPG = class ExperimentsPG extends ExperimentsStorage {
|
|
7146
7285
|
#db;
|
|
7147
7286
|
#schema;
|
|
@@ -7150,9 +7289,9 @@ var ExperimentsPG = class ExperimentsPG extends ExperimentsStorage {
|
|
|
7150
7289
|
static MANAGED_TABLES = [TABLE_EXPERIMENTS, TABLE_EXPERIMENT_RESULTS];
|
|
7151
7290
|
/**
|
|
7152
7291
|
* Experiments prune as whole units: an aged experiment and its result rows go
|
|
7153
|
-
* together, mirroring `deleteExperiment`. Anchored on `completedAt`
|
|
7154
|
-
* `completedAtZ` mirror
|
|
7155
|
-
*
|
|
7292
|
+
* together, mirroring `deleteExperiment`. Anchored on `completedAt` rather
|
|
7293
|
+
* than its `completedAtZ` mirror: both are written together, but
|
|
7294
|
+
* `completedAt` is the column the retention index covers. `completedAt` is
|
|
7156
7295
|
* written as a UTC ISO string and stays NULL while running, so
|
|
7157
7296
|
* `completedAt < cutoff` is false for in-flight experiments.
|
|
7158
7297
|
*/
|
|
@@ -7472,7 +7611,11 @@ var ExperimentsPG = class ExperimentsPG extends ExperimentsStorage {
|
|
|
7472
7611
|
startedAt: null,
|
|
7473
7612
|
completedAt: null,
|
|
7474
7613
|
createdAt: nowIso,
|
|
7475
|
-
updatedAt: nowIso
|
|
7614
|
+
updatedAt: nowIso,
|
|
7615
|
+
startedAtZ: null,
|
|
7616
|
+
completedAtZ: null,
|
|
7617
|
+
createdAtZ: nowIso,
|
|
7618
|
+
updatedAtZ: nowIso
|
|
7476
7619
|
}
|
|
7477
7620
|
});
|
|
7478
7621
|
return {
|
|
@@ -7561,12 +7704,14 @@ var ExperimentsPG = class ExperimentsPG extends ExperimentsStorage {
|
|
|
7561
7704
|
values.push(input.skippedCount);
|
|
7562
7705
|
}
|
|
7563
7706
|
if (input.startedAt !== void 0) {
|
|
7564
|
-
|
|
7565
|
-
|
|
7707
|
+
const iso = input.startedAt?.toISOString() ?? null;
|
|
7708
|
+
setClauses.push(`"startedAt" = $${paramIndex++}`, `"startedAtZ" = $${paramIndex++}`);
|
|
7709
|
+
values.push(iso, iso);
|
|
7566
7710
|
}
|
|
7567
7711
|
if (input.completedAt !== void 0) {
|
|
7568
|
-
|
|
7569
|
-
|
|
7712
|
+
const iso = input.completedAt?.toISOString() ?? null;
|
|
7713
|
+
setClauses.push(`"completedAt" = $${paramIndex++}`, `"completedAtZ" = $${paramIndex++}`);
|
|
7714
|
+
values.push(iso, iso);
|
|
7570
7715
|
}
|
|
7571
7716
|
values.push(input.id);
|
|
7572
7717
|
await this.#db.client.none(`UPDATE ${tableName} SET ${setClauses.join(", ")} WHERE "id" = $${paramIndex}`, values);
|
|
@@ -7595,7 +7740,7 @@ var ExperimentsPG = class ExperimentsPG extends ExperimentsStorage {
|
|
|
7595
7740
|
});
|
|
7596
7741
|
const { conditions, params } = tenancyWhere(filters, 2);
|
|
7597
7742
|
const whereSql = ["\"id\" = $1", ...conditions].join(" AND ");
|
|
7598
|
-
const result = await client.oneOrNone(`SELECT
|
|
7743
|
+
const result = await client.oneOrNone(`SELECT ${EXPERIMENT_COLUMNS} FROM ${tableName} WHERE ${whereSql}`, [id, ...params]);
|
|
7599
7744
|
return result ? this.transformExperimentRow(result) : null;
|
|
7600
7745
|
} catch (error) {
|
|
7601
7746
|
throw new MastraError({
|
|
@@ -7682,7 +7827,7 @@ var ExperimentsPG = class ExperimentsPG extends ExperimentsStorage {
|
|
|
7682
7827
|
const { offset, perPage: perPageForResponse } = calculatePagination(page, perPageInput, perPage);
|
|
7683
7828
|
const limitValue = perPageInput === false ? total : perPage;
|
|
7684
7829
|
return {
|
|
7685
|
-
experiments: (await this.#db.readClient.manyOrNone(`SELECT
|
|
7830
|
+
experiments: (await this.#db.readClient.manyOrNone(`SELECT ${EXPERIMENT_COLUMNS} FROM ${tableName} ${whereClause} ORDER BY "${orderBy.field}" ${orderBy.direction}, "id" ASC LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`, [
|
|
7686
7831
|
...queryParams,
|
|
7687
7832
|
limitValue,
|
|
7688
7833
|
offset
|
|
@@ -7760,9 +7905,11 @@ var ExperimentsPG = class ExperimentsPG extends ExperimentsStorage {
|
|
|
7760
7905
|
return t.one(`INSERT INTO ${resultsTable} (
|
|
7761
7906
|
"id", "experimentId", "itemId", "itemDatasetVersion", "organizationId", "projectId",
|
|
7762
7907
|
"input", "output", "groundTruth", "metadata", "error", "startedAt", "completedAt",
|
|
7763
|
-
"retryCount", "attempt", "traceId", "status", "tags", "toolMockReport", "createdAt"
|
|
7764
|
-
|
|
7765
|
-
|
|
7908
|
+
"retryCount", "attempt", "traceId", "status", "tags", "toolMockReport", "createdAt",
|
|
7909
|
+
"startedAtZ", "completedAtZ", "createdAtZ"
|
|
7910
|
+
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20,
|
|
7911
|
+
$21, $22, $23)
|
|
7912
|
+
RETURNING ${EXPERIMENT_RESULT_COLUMNS}`, [
|
|
7766
7913
|
id,
|
|
7767
7914
|
input.experimentId,
|
|
7768
7915
|
input.itemId,
|
|
@@ -7782,6 +7929,9 @@ var ExperimentsPG = class ExperimentsPG extends ExperimentsStorage {
|
|
|
7782
7929
|
input.status ?? null,
|
|
7783
7930
|
purgeMetadata || input.tags == null ? null : toPgJson(input.tags),
|
|
7784
7931
|
purgeMetadata || input.toolMockReport == null ? null : toPgJson(input.toolMockReport),
|
|
7932
|
+
nowIso,
|
|
7933
|
+
input.startedAt.toISOString(),
|
|
7934
|
+
input.completedAt.toISOString(),
|
|
7785
7935
|
nowIso
|
|
7786
7936
|
]);
|
|
7787
7937
|
});
|
|
@@ -7808,6 +7958,7 @@ var ExperimentsPG = class ExperimentsPG extends ExperimentsStorage {
|
|
|
7808
7958
|
const row = await this.#db.client.tx(async (t) => {
|
|
7809
7959
|
const owner = await t.oneOrNone(`SELECT "datasetId" FROM ${experimentsTable} WHERE "id" = $1`, [input.experimentId]);
|
|
7810
7960
|
const purgeMetadata = await this.#resolvePurgeMetadata(t, owner?.datasetId, input.itemId);
|
|
7961
|
+
const nowIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
7811
7962
|
const existing = await t.oneOrNone(`SELECT "id" FROM ${tableName} WHERE "experimentId" = $1 AND "itemId" = $2 AND COALESCE("attempt", 0) = $3 FOR UPDATE`, [
|
|
7812
7963
|
input.experimentId,
|
|
7813
7964
|
input.itemId,
|
|
@@ -7818,9 +7969,11 @@ var ExperimentsPG = class ExperimentsPG extends ExperimentsStorage {
|
|
|
7818
7969
|
return t.one(`INSERT INTO ${tableName} (
|
|
7819
7970
|
"id", "experimentId", "itemId", "itemDatasetVersion", "organizationId", "projectId",
|
|
7820
7971
|
"input", "output", "groundTruth", "metadata", "error", "startedAt", "completedAt",
|
|
7821
|
-
"retryCount", "attempt", "traceId", "status", "tags", "toolMockReport", "createdAt"
|
|
7822
|
-
|
|
7823
|
-
|
|
7972
|
+
"retryCount", "attempt", "traceId", "status", "tags", "toolMockReport", "createdAt",
|
|
7973
|
+
"startedAtZ", "completedAtZ", "createdAtZ"
|
|
7974
|
+
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20,
|
|
7975
|
+
$21, $22, $23)
|
|
7976
|
+
RETURNING ${EXPERIMENT_RESULT_COLUMNS}`, [
|
|
7824
7977
|
id,
|
|
7825
7978
|
input.experimentId,
|
|
7826
7979
|
input.itemId,
|
|
@@ -7840,15 +7993,19 @@ var ExperimentsPG = class ExperimentsPG extends ExperimentsStorage {
|
|
|
7840
7993
|
input.status ?? null,
|
|
7841
7994
|
purgeMetadata || input.tags == null ? null : toPgJson(input.tags),
|
|
7842
7995
|
purgeMetadata || input.toolMockReport == null ? null : toPgJson(input.toolMockReport),
|
|
7843
|
-
|
|
7996
|
+
nowIso,
|
|
7997
|
+
input.startedAt.toISOString(),
|
|
7998
|
+
input.completedAt.toISOString(),
|
|
7999
|
+
nowIso
|
|
7844
8000
|
]);
|
|
7845
8001
|
}
|
|
7846
8002
|
return t.one(`UPDATE ${tableName} SET
|
|
7847
8003
|
"itemDatasetVersion" = $2, "organizationId" = $3, "projectId" = $4,
|
|
7848
8004
|
"input" = $5, "output" = $6, "groundTruth" = $7, "metadata" = $8, "error" = $9,
|
|
7849
8005
|
"startedAt" = $10, "completedAt" = $11, "retryCount" = $12, "attempt" = $13,
|
|
7850
|
-
"traceId" = $14, "status" = $15, "tags" = $16, "toolMockReport" = $17
|
|
7851
|
-
|
|
8006
|
+
"traceId" = $14, "status" = $15, "tags" = $16, "toolMockReport" = $17,
|
|
8007
|
+
"startedAtZ" = $18, "completedAtZ" = $19
|
|
8008
|
+
WHERE "id" = $1 RETURNING ${EXPERIMENT_RESULT_COLUMNS}`, [
|
|
7852
8009
|
existing.id,
|
|
7853
8010
|
input.itemDatasetVersion ?? null,
|
|
7854
8011
|
input.organizationId ?? null,
|
|
@@ -7865,7 +8022,9 @@ var ExperimentsPG = class ExperimentsPG extends ExperimentsStorage {
|
|
|
7865
8022
|
input.traceId ?? null,
|
|
7866
8023
|
input.status ?? null,
|
|
7867
8024
|
purgeMetadata || input.tags == null ? null : toPgJson(input.tags),
|
|
7868
|
-
purgeMetadata || input.toolMockReport == null ? null : toPgJson(input.toolMockReport)
|
|
8025
|
+
purgeMetadata || input.toolMockReport == null ? null : toPgJson(input.toolMockReport),
|
|
8026
|
+
input.startedAt.toISOString(),
|
|
8027
|
+
input.completedAt.toISOString()
|
|
7869
8028
|
]);
|
|
7870
8029
|
});
|
|
7871
8030
|
return this.transformExperimentResultRow(row);
|
|
@@ -7899,7 +8058,7 @@ var ExperimentsPG = class ExperimentsPG extends ExperimentsStorage {
|
|
|
7899
8058
|
"tags" = CASE WHEN $4 THEN NULL WHEN $5 THEN $6::jsonb ELSE "tags" END,
|
|
7900
8059
|
"comment" = CASE WHEN $4 THEN NULL WHEN $7 THEN $8 ELSE "comment" END
|
|
7901
8060
|
WHERE "id" = $1${input.experimentId !== void 0 ? " AND \"experimentId\" = $9" : ""}
|
|
7902
|
-
RETURNING
|
|
8061
|
+
RETURNING ${EXPERIMENT_RESULT_COLUMNS}`, [
|
|
7903
8062
|
input.id,
|
|
7904
8063
|
input.status !== void 0,
|
|
7905
8064
|
input.status ?? null,
|
|
@@ -7942,7 +8101,7 @@ var ExperimentsPG = class ExperimentsPG extends ExperimentsStorage {
|
|
|
7942
8101
|
});
|
|
7943
8102
|
const { conditions, params } = tenancyWhere(filters, 2);
|
|
7944
8103
|
const whereSql = ["\"id\" = $1", ...conditions].join(" AND ");
|
|
7945
|
-
const result = await client.oneOrNone(`SELECT
|
|
8104
|
+
const result = await client.oneOrNone(`SELECT ${EXPERIMENT_RESULT_COLUMNS} FROM ${tableName} WHERE ${whereSql}`, [id, ...params]);
|
|
7946
8105
|
return result ? this.transformExperimentResultRow(result) : null;
|
|
7947
8106
|
} catch (error) {
|
|
7948
8107
|
throw new MastraError({
|
|
@@ -8005,7 +8164,7 @@ var ExperimentsPG = class ExperimentsPG extends ExperimentsStorage {
|
|
|
8005
8164
|
const { offset, perPage: perPageForResponse } = calculatePagination(page, perPageInput, perPage);
|
|
8006
8165
|
const limitValue = perPageInput === false ? total : perPage;
|
|
8007
8166
|
return {
|
|
8008
|
-
results: (await this.#db.readClient.manyOrNone(`SELECT
|
|
8167
|
+
results: (await this.#db.readClient.manyOrNone(`SELECT ${EXPERIMENT_RESULT_COLUMNS} FROM ${tableName} ${whereClause} ORDER BY "${orderBy.field}" ${orderBy.direction}, "id" ASC LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`, [
|
|
8009
8168
|
...queryParams,
|
|
8010
8169
|
limitValue,
|
|
8011
8170
|
offset
|
|
@@ -17299,6 +17458,203 @@ function parseUpdateFeedbackReviewStatusArgs(args) {
|
|
|
17299
17458
|
reviewStatus: args.reviewStatus
|
|
17300
17459
|
};
|
|
17301
17460
|
}
|
|
17461
|
+
// src/storage/domains/observability/v-next/partitioning.ts
|
|
17462
|
+
const DEFAULT_FUTURE_DAYS = 14;
|
|
17463
|
+
async function detectTimescale(client) {
|
|
17464
|
+
const row = await client.oneOrNone(`SELECT EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'timescaledb') AS "exists"`);
|
|
17465
|
+
return Boolean(row?.exists);
|
|
17466
|
+
}
|
|
17467
|
+
async function detectPartman(client) {
|
|
17468
|
+
const row = await client.oneOrNone(`SELECT EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'pg_partman') AS "exists"`);
|
|
17469
|
+
return Boolean(row?.exists);
|
|
17470
|
+
}
|
|
17471
|
+
async function getPartmanVersion(client) {
|
|
17472
|
+
return (await client.oneOrNone(`SELECT extversion FROM pg_extension WHERE extname = 'pg_partman'`))?.extversion ?? null;
|
|
17473
|
+
}
|
|
17474
|
+
function getPartmanMajor(version) {
|
|
17475
|
+
if (!version) return null;
|
|
17476
|
+
const match = /^(\d+)/.exec(version);
|
|
17477
|
+
return match ? Number(match[1]) : null;
|
|
17478
|
+
}
|
|
17479
|
+
async function resolveMode(client, options = {}) {
|
|
17480
|
+
if (options.mode && options.mode !== "auto") return options.mode;
|
|
17481
|
+
if (await detectTimescale(client)) return "timescale";
|
|
17482
|
+
if (await detectPartman(client)) return "partman";
|
|
17483
|
+
return "native";
|
|
17484
|
+
}
|
|
17485
|
+
function dayBounds(d) {
|
|
17486
|
+
const year = d.getUTCFullYear();
|
|
17487
|
+
const month = String(d.getUTCMonth() + 1).padStart(2, "0");
|
|
17488
|
+
const day = String(d.getUTCDate()).padStart(2, "0");
|
|
17489
|
+
const start = `${year}-${month}-${day} 00:00:00+00`;
|
|
17490
|
+
const next = new Date(Date.UTC(year, d.getUTCMonth(), d.getUTCDate() + 1));
|
|
17491
|
+
return {
|
|
17492
|
+
start,
|
|
17493
|
+
end: `${next.getUTCFullYear()}-${String(next.getUTCMonth() + 1).padStart(2, "0")}-${String(next.getUTCDate()).padStart(2, "0")} 00:00:00+00`,
|
|
17494
|
+
suffix: `${year}${month}${day}`
|
|
17495
|
+
};
|
|
17496
|
+
}
|
|
17497
|
+
function partitionName(table, suffix) {
|
|
17498
|
+
return `${table}_p${suffix}`;
|
|
17499
|
+
}
|
|
17500
|
+
/**
|
|
17501
|
+
* Creates daily partition tables in [today - 1 day, today + futureDays].
|
|
17502
|
+
* Idempotent: uses CREATE TABLE IF NOT EXISTS.
|
|
17503
|
+
*/
|
|
17504
|
+
async function ensureNativePartitions(client, schema, options = {}) {
|
|
17505
|
+
const windowDays = partitionWindow(options);
|
|
17506
|
+
for (const table of ALL_SIGNAL_TABLES) await ensureDayPartitions(client, schema, table, windowDays);
|
|
17507
|
+
}
|
|
17508
|
+
/** The UTC days init() pre-creates: yesterday (optional) through today + futureDays. */
|
|
17509
|
+
function partitionWindow(options = {}, nowMs = Date.now()) {
|
|
17510
|
+
const futureDays = options.futureDays ?? DEFAULT_FUTURE_DAYS;
|
|
17511
|
+
const start = options.includeYesterday ?? true ? -1 : 0;
|
|
17512
|
+
const days = [];
|
|
17513
|
+
for (let i = start; i <= futureDays; i++) days.push(new Date(nowMs + i * 864e5));
|
|
17514
|
+
return days;
|
|
17515
|
+
}
|
|
17516
|
+
/**
|
|
17517
|
+
* Keeps only the days inside the current partition window. The write path
|
|
17518
|
+
* creates partitions on demand within that window only, so a row with a
|
|
17519
|
+
* client-supplied timestamp far in the past or future cannot make the
|
|
17520
|
+
* database grow a table per day it names.
|
|
17521
|
+
*/
|
|
17522
|
+
function withinPartitionWindow(days, options = {}) {
|
|
17523
|
+
const allowed = new Set(partitionWindow(options).map((d) => dayBounds(d).suffix));
|
|
17524
|
+
return days.filter((d) => allowed.has(dayBounds(d).suffix));
|
|
17525
|
+
}
|
|
17526
|
+
/**
|
|
17527
|
+
* Creates and attaches a partition of `schema.table` for each UTC day in
|
|
17528
|
+
* `days` that has none. Each day is a `CREATE TABLE (LIKE parent)` followed by
|
|
17529
|
+
* `ATTACH PARTITION`, in one transaction, so a failed attach leaves no
|
|
17530
|
+
* detached child behind. When either step fails, the catalog is read again: a
|
|
17531
|
+
* day that a concurrent caller attached meanwhile counts as done, and any
|
|
17532
|
+
* other failure is rethrown.
|
|
17533
|
+
*/
|
|
17534
|
+
async function ensureDayPartitions(client, schema, table, days) {
|
|
17535
|
+
const parent = qualifiedTable(schema, table);
|
|
17536
|
+
const attached = await attachedPartitionNames(client, schema, table);
|
|
17537
|
+
for (const d of days) {
|
|
17538
|
+
const { start: partStart, end: partEnd, suffix } = dayBounds(d);
|
|
17539
|
+
const childName = partitionName(table, suffix);
|
|
17540
|
+
if (attached.has(childName)) continue;
|
|
17541
|
+
const child = qualifiedName(schema, childName);
|
|
17542
|
+
try {
|
|
17543
|
+
await client.tx(async (t) => {
|
|
17544
|
+
await t.none(`CREATE TABLE IF NOT EXISTS ${child} (LIKE ${parent} INCLUDING ALL)`);
|
|
17545
|
+
await t.none(`ALTER TABLE ${parent} ATTACH PARTITION ${child}
|
|
17546
|
+
FOR VALUES FROM ('${partStart}') TO ('${partEnd}')`);
|
|
17547
|
+
});
|
|
17548
|
+
} catch (error) {
|
|
17549
|
+
if (!(await attachedPartitionNames(client, schema, table)).has(childName)) throw error;
|
|
17550
|
+
}
|
|
17551
|
+
}
|
|
17552
|
+
}
|
|
17553
|
+
/** Names of the children attached to `schema.table`, read from the catalog so no search_path can shadow it. */
|
|
17554
|
+
async function attachedPartitionNames(client, schema, table) {
|
|
17555
|
+
const rows = await client.manyOrNone(`SELECT c.relname
|
|
17556
|
+
FROM pg_catalog.pg_inherits i
|
|
17557
|
+
JOIN pg_catalog.pg_class c ON c.oid = i.inhrelid
|
|
17558
|
+
WHERE i.inhparent = (
|
|
17559
|
+
SELECT p.oid
|
|
17560
|
+
FROM pg_catalog.pg_class p
|
|
17561
|
+
JOIN pg_catalog.pg_namespace n ON n.oid = p.relnamespace
|
|
17562
|
+
WHERE n.nspname = $1 AND p.relname = $2
|
|
17563
|
+
)`, [schema, table]);
|
|
17564
|
+
return new Set(rows.map((r) => r.relname));
|
|
17565
|
+
}
|
|
17566
|
+
/**
|
|
17567
|
+
* `23514` raised by `ExecFindPartition`: the row's day has no attached
|
|
17568
|
+
* partition. The message is matched as a fallback only, because Postgres
|
|
17569
|
+
* translates it when `lc_messages` is not English.
|
|
17570
|
+
*/
|
|
17571
|
+
function isMissingPartitionError(error) {
|
|
17572
|
+
const { code, routine, message = "" } = error ?? {};
|
|
17573
|
+
return code === "23514" && (routine === "ExecFindPartition" || /no partition of relation/i.test(message));
|
|
17574
|
+
}
|
|
17575
|
+
/**
|
|
17576
|
+
* Calls `create_hypertable()` for each signal table.
|
|
17577
|
+
*
|
|
17578
|
+
* Postgres declarative partitioning and Timescale hypertables are mutually
|
|
17579
|
+
* exclusive, so the base DDL must have been generated with TableDDLMode
|
|
17580
|
+
* 'timescale' (no `PARTITION BY` clause). `if_not_exists` makes this
|
|
17581
|
+
* idempotent across re-inits.
|
|
17582
|
+
*/
|
|
17583
|
+
async function ensureTimescaleHypertables(client, schema) {
|
|
17584
|
+
for (const table of ALL_SIGNAL_TABLES) {
|
|
17585
|
+
const tableExpr = qualifiedTable(schema, table);
|
|
17586
|
+
const timeColumn = SIGNAL_TIME_COLUMN[table].replace(/'/g, "''");
|
|
17587
|
+
await client.none(`SELECT create_hypertable($1::regclass, '${timeColumn}', chunk_time_interval => INTERVAL '1 day', if_not_exists => true)`, [tableExpr]);
|
|
17588
|
+
}
|
|
17589
|
+
}
|
|
17590
|
+
/**
|
|
17591
|
+
* Registers each signal table with pg_partman as a daily-interval parent and
|
|
17592
|
+
* lets partman create the child partitions.
|
|
17593
|
+
*
|
|
17594
|
+
* Retention is intentionally NOT configured here — it stays opt-in via the
|
|
17595
|
+
* future Mastra CLI surface.
|
|
17596
|
+
*
|
|
17597
|
+
* Prefers the pg_partman 5.x+ declarative API (`p_type := 'range'`) and keeps
|
|
17598
|
+
* a 4.x fallback (`p_type := 'native'`) for older installs. That lets the
|
|
17599
|
+
* adapter run against current arm64-friendly images without breaking existing
|
|
17600
|
+
* 4.x deployments.
|
|
17601
|
+
*/
|
|
17602
|
+
async function partmanParentRegistered(client, schema, table) {
|
|
17603
|
+
const row = await client.oneOrNone(`SELECT EXISTS (
|
|
17604
|
+
SELECT 1 FROM partman.part_config
|
|
17605
|
+
WHERE parent_table = format('%I.%I', $1::text, $2::text)
|
|
17606
|
+
) AS "exists"`, [schema, table]);
|
|
17607
|
+
return Boolean(row?.exists);
|
|
17608
|
+
}
|
|
17609
|
+
async function ensurePartmanHypertables(client, schema) {
|
|
17610
|
+
const partmanMajor = getPartmanMajor(await getPartmanVersion(client));
|
|
17611
|
+
for (const table of ALL_SIGNAL_TABLES) {
|
|
17612
|
+
if (await partmanParentRegistered(client, schema, table)) continue;
|
|
17613
|
+
for (let attempt = 0; attempt < 3; attempt++) try {
|
|
17614
|
+
if (partmanMajor != null && partmanMajor >= 5) await client.none(`SELECT partman.create_parent(
|
|
17615
|
+
p_parent_table := format('%I.%I', $1::text, $2::text),
|
|
17616
|
+
p_control := $3,
|
|
17617
|
+
p_interval := '1 day',
|
|
17618
|
+
p_type := 'range'
|
|
17619
|
+
)`, [
|
|
17620
|
+
schema,
|
|
17621
|
+
table,
|
|
17622
|
+
SIGNAL_TIME_COLUMN[table]
|
|
17623
|
+
]);
|
|
17624
|
+
else await client.none(`SELECT partman.create_parent(
|
|
17625
|
+
p_parent_table := format('%I.%I', $1::text, $2::text),
|
|
17626
|
+
p_control := $3,
|
|
17627
|
+
p_type := 'native',
|
|
17628
|
+
p_interval := '1 day'
|
|
17629
|
+
)`, [
|
|
17630
|
+
schema,
|
|
17631
|
+
table,
|
|
17632
|
+
SIGNAL_TIME_COLUMN[table]
|
|
17633
|
+
]);
|
|
17634
|
+
break;
|
|
17635
|
+
} catch (error) {
|
|
17636
|
+
const message = error?.message ?? "";
|
|
17637
|
+
if (error?.code === "23505" || /already managed by pg_partman/i.test(message) || /relation .* already exists/i.test(message)) break;
|
|
17638
|
+
if (!/deadlock detected/i.test(message)) throw error;
|
|
17639
|
+
if (await partmanParentRegistered(client, schema, table)) break;
|
|
17640
|
+
if (attempt === 2) throw error;
|
|
17641
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
17642
|
+
}
|
|
17643
|
+
}
|
|
17644
|
+
}
|
|
17645
|
+
async function setupPartitioning(client, schema, options = {}) {
|
|
17646
|
+
switch (await resolveMode(client, options)) {
|
|
17647
|
+
case "timescale":
|
|
17648
|
+
await ensureTimescaleHypertables(client, schema);
|
|
17649
|
+
return "timescale";
|
|
17650
|
+
case "partman":
|
|
17651
|
+
await ensurePartmanHypertables(client, schema);
|
|
17652
|
+
return "partman";
|
|
17653
|
+
case "native":
|
|
17654
|
+
await ensureNativePartitions(client, schema, options);
|
|
17655
|
+
return "native";
|
|
17656
|
+
}
|
|
17657
|
+
}
|
|
17302
17658
|
// src/storage/domains/observability/v-next/sql.ts
|
|
17303
17659
|
/**
|
|
17304
17660
|
* SQL helpers for the v-next Postgres observability domain.
|
|
@@ -17379,6 +17735,54 @@ ${onConflict}`,
|
|
|
17379
17735
|
values
|
|
17380
17736
|
};
|
|
17381
17737
|
}
|
|
17738
|
+
/**
|
|
17739
|
+
* Insert into a signal table. In native partition mode, a row whose day is
|
|
17740
|
+
* inside the partition window but has no partition yet gets one created, then
|
|
17741
|
+
* the insert runs once more. TimescaleDB and pg_partman own their partitions,
|
|
17742
|
+
* so in those modes the original error propagates untouched. The mode is the
|
|
17743
|
+
* one the store resolved; a store that never ran init() (`disableInit`)
|
|
17744
|
+
* detects it here, after the failed insert, so the hot path stays one query.
|
|
17745
|
+
*/
|
|
17746
|
+
async function insertRows(client, schema, table, records, partitioning, onConflict) {
|
|
17747
|
+
const insert = buildInsert(schema, table, records, onConflict);
|
|
17748
|
+
if (!insert) return;
|
|
17749
|
+
try {
|
|
17750
|
+
await client.query(insert.text, insert.values);
|
|
17751
|
+
} catch (error) {
|
|
17752
|
+
if (!isMissingPartitionError(error) || await resolveMode(client, partitioning) !== "native") throw error;
|
|
17753
|
+
const days = withinPartitionWindow(recordDays(records, SIGNAL_TIME_COLUMN[table]), partitioning);
|
|
17754
|
+
if (days.length === 0) throw error;
|
|
17755
|
+
for (const day of days) await ensureDayPartitions(client, schema, table, [day]).catch((cause) => {
|
|
17756
|
+
if (!isInsufficientPrivilegeError(cause)) throw cause;
|
|
17757
|
+
throw new Error(partitionCreateFailure({
|
|
17758
|
+
table,
|
|
17759
|
+
day,
|
|
17760
|
+
cause
|
|
17761
|
+
}), { cause });
|
|
17762
|
+
});
|
|
17763
|
+
await client.query(insert.text, insert.values);
|
|
17764
|
+
}
|
|
17765
|
+
}
|
|
17766
|
+
/** `42501`: the role may not create the child, or does not own the parent it must attach to. */
|
|
17767
|
+
function isInsufficientPrivilegeError(error) {
|
|
17768
|
+
return error?.code === "42501";
|
|
17769
|
+
}
|
|
17770
|
+
function partitionCreateFailure({ table, day, cause }) {
|
|
17771
|
+
const reason = cause instanceof Error ? cause.message : String(cause);
|
|
17772
|
+
return `Could not create the ${table} partition for ${day.toISOString().slice(0, 10)} (UTC): ${reason}. Create the partitions with a role that can create tables in the schema and owns the observability tables, for example by calling ensurePartitions() on a schedule.`;
|
|
17773
|
+
}
|
|
17774
|
+
function recordDays(records, timeColumn) {
|
|
17775
|
+
const days = /* @__PURE__ */ new Map();
|
|
17776
|
+
for (const record of records) {
|
|
17777
|
+
const value = record[timeColumn];
|
|
17778
|
+
if (value == null) continue;
|
|
17779
|
+
const d = new Date(value);
|
|
17780
|
+
if (isNaN(d.getTime())) continue;
|
|
17781
|
+
const day = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()));
|
|
17782
|
+
days.set(day.toISOString(), day);
|
|
17783
|
+
}
|
|
17784
|
+
return [...days.values()];
|
|
17785
|
+
}
|
|
17382
17786
|
/** Primary key of the span table; also the conflict target for span upserts. */
|
|
17383
17787
|
const SPAN_KEY_COLUMNS = [
|
|
17384
17788
|
"traceId",
|
|
@@ -18289,16 +18693,14 @@ function pushFeedbackIdentity(acc, feedbackType, feedbackSource) {
|
|
|
18289
18693
|
}
|
|
18290
18694
|
acc.conditions.push(`"valueNumber" IS NOT NULL`);
|
|
18291
18695
|
}
|
|
18292
|
-
async function createFeedback(client, schema, args) {
|
|
18696
|
+
async function createFeedback(client, schema, args, partitioning) {
|
|
18293
18697
|
const row = feedbackRecordToRow(args.feedback);
|
|
18294
|
-
|
|
18295
|
-
if (insert) await client.query(insert.text, insert.values);
|
|
18698
|
+
await insertRows(client, schema, TABLE_FEEDBACK_EVENTS, [row], partitioning, FEEDBACK_UPSERT_CLAUSE);
|
|
18296
18699
|
}
|
|
18297
|
-
async function batchCreateFeedback(client, schema, args) {
|
|
18700
|
+
async function batchCreateFeedback(client, schema, args, partitioning) {
|
|
18298
18701
|
if (args.feedbacks.length === 0) return;
|
|
18299
18702
|
const rows = [...new Map(args.feedbacks.map((feedback) => [feedback.feedbackId, feedback])).values()].map(feedbackRecordToRow);
|
|
18300
|
-
|
|
18301
|
-
if (insert) await client.query(insert.text, insert.values);
|
|
18703
|
+
await insertRows(client, schema, TABLE_FEEDBACK_EVENTS, rows, partitioning, FEEDBACK_UPSERT_CLAUSE);
|
|
18302
18704
|
}
|
|
18303
18705
|
async function updateFeedbackReviewStatus(client, schema, args) {
|
|
18304
18706
|
const { feedbackId, reviewStatus } = parseUpdateFeedbackReviewStatusArgs(args);
|
|
@@ -18496,11 +18898,10 @@ function applyLogFilters(acc, filters) {
|
|
|
18496
18898
|
applyCommonFilters(acc, filters);
|
|
18497
18899
|
applySingleOrArrayFilter(acc, "level", filters?.level);
|
|
18498
18900
|
}
|
|
18499
|
-
async function batchCreateLogs(client, schema, args) {
|
|
18901
|
+
async function batchCreateLogs(client, schema, args, partitioning) {
|
|
18500
18902
|
if (args.logs.length === 0) return;
|
|
18501
18903
|
const rows = args.logs.map(logRecordToRow);
|
|
18502
|
-
|
|
18503
|
-
if (insert) await client.query(insert.text, insert.values);
|
|
18904
|
+
await insertRows(client, schema, TABLE_LOG_EVENTS, rows, partitioning);
|
|
18504
18905
|
}
|
|
18505
18906
|
async function listLogs(client, schema, args) {
|
|
18506
18907
|
const { mode, filters, pagination, orderBy, after, limit } = listLogsArgsSchema.parse(args);
|
|
@@ -18571,11 +18972,10 @@ function pushMetricNameFilter(acc, names) {
|
|
|
18571
18972
|
acc.conditions.push(`"name" IN (${placeholders})`);
|
|
18572
18973
|
acc.params.push(...names);
|
|
18573
18974
|
}
|
|
18574
|
-
async function batchCreateMetrics(client, schema, args) {
|
|
18975
|
+
async function batchCreateMetrics(client, schema, args, partitioning) {
|
|
18575
18976
|
if (args.metrics.length === 0) return;
|
|
18576
18977
|
const rows = args.metrics.map(metricRecordToRow);
|
|
18577
|
-
|
|
18578
|
-
if (insert) await client.query(insert.text, insert.values);
|
|
18978
|
+
await insertRows(client, schema, TABLE_METRIC_EVENTS, rows, partitioning);
|
|
18579
18979
|
}
|
|
18580
18980
|
async function listMetrics(client, schema, args) {
|
|
18581
18981
|
const { mode, filters, pagination, orderBy, after, limit } = listMetricsArgsSchema.parse(args);
|
|
@@ -18777,165 +19177,6 @@ async function getMetricPercentiles(client, schema, args) {
|
|
|
18777
19177
|
`;
|
|
18778
19178
|
return { series: percentileSeriesFromRows(await client.manyOrNone(sql, acc.params), args.percentiles) };
|
|
18779
19179
|
}
|
|
18780
|
-
// src/storage/domains/observability/v-next/partitioning.ts
|
|
18781
|
-
const DEFAULT_FUTURE_DAYS = 14;
|
|
18782
|
-
async function detectTimescale(client) {
|
|
18783
|
-
const row = await client.oneOrNone(`SELECT EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'timescaledb') AS "exists"`);
|
|
18784
|
-
return Boolean(row?.exists);
|
|
18785
|
-
}
|
|
18786
|
-
async function detectPartman(client) {
|
|
18787
|
-
const row = await client.oneOrNone(`SELECT EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'pg_partman') AS "exists"`);
|
|
18788
|
-
return Boolean(row?.exists);
|
|
18789
|
-
}
|
|
18790
|
-
async function getPartmanVersion(client) {
|
|
18791
|
-
return (await client.oneOrNone(`SELECT extversion FROM pg_extension WHERE extname = 'pg_partman'`))?.extversion ?? null;
|
|
18792
|
-
}
|
|
18793
|
-
function getPartmanMajor(version) {
|
|
18794
|
-
if (!version) return null;
|
|
18795
|
-
const match = /^(\d+)/.exec(version);
|
|
18796
|
-
return match ? Number(match[1]) : null;
|
|
18797
|
-
}
|
|
18798
|
-
async function resolveMode(client, options = {}) {
|
|
18799
|
-
if (options.mode && options.mode !== "auto") return options.mode;
|
|
18800
|
-
if (await detectTimescale(client)) return "timescale";
|
|
18801
|
-
if (await detectPartman(client)) return "partman";
|
|
18802
|
-
return "native";
|
|
18803
|
-
}
|
|
18804
|
-
function dayBounds(d) {
|
|
18805
|
-
const year = d.getUTCFullYear();
|
|
18806
|
-
const month = String(d.getUTCMonth() + 1).padStart(2, "0");
|
|
18807
|
-
const day = String(d.getUTCDate()).padStart(2, "0");
|
|
18808
|
-
const start = `${year}-${month}-${day} 00:00:00+00`;
|
|
18809
|
-
const next = new Date(Date.UTC(year, d.getUTCMonth(), d.getUTCDate() + 1));
|
|
18810
|
-
return {
|
|
18811
|
-
start,
|
|
18812
|
-
end: `${next.getUTCFullYear()}-${String(next.getUTCMonth() + 1).padStart(2, "0")}-${String(next.getUTCDate()).padStart(2, "0")} 00:00:00+00`,
|
|
18813
|
-
suffix: `${year}${month}${day}`
|
|
18814
|
-
};
|
|
18815
|
-
}
|
|
18816
|
-
function partitionName(table, suffix) {
|
|
18817
|
-
return `${table}_p${suffix}`;
|
|
18818
|
-
}
|
|
18819
|
-
/**
|
|
18820
|
-
* Creates daily partition tables in [today - 1 day, today + futureDays].
|
|
18821
|
-
* Idempotent: uses CREATE TABLE IF NOT EXISTS.
|
|
18822
|
-
*/
|
|
18823
|
-
async function ensureNativePartitions(client, schema, options = {}) {
|
|
18824
|
-
const futureDays = options.futureDays ?? DEFAULT_FUTURE_DAYS;
|
|
18825
|
-
const start = options.includeYesterday ?? true ? -1 : 0;
|
|
18826
|
-
const baseNowMs = Date.now();
|
|
18827
|
-
for (const table of ALL_SIGNAL_TABLES) for (let i = start; i <= futureDays; i++) {
|
|
18828
|
-
const { start: partStart, end: partEnd, suffix } = dayBounds(new Date(baseNowMs + i * 864e5));
|
|
18829
|
-
const child = qualifiedName(schema, partitionName(table, suffix));
|
|
18830
|
-
const parent = qualifiedTable(schema, table);
|
|
18831
|
-
try {
|
|
18832
|
-
await client.none(`CREATE TABLE IF NOT EXISTS ${child} (LIKE ${parent} INCLUDING ALL)`);
|
|
18833
|
-
} catch (error) {
|
|
18834
|
-
if (!isDuplicateRelationError(error)) throw error;
|
|
18835
|
-
}
|
|
18836
|
-
try {
|
|
18837
|
-
await client.none(`ALTER TABLE ${parent} ATTACH PARTITION ${child}
|
|
18838
|
-
FOR VALUES FROM ('${partStart}') TO ('${partEnd}')`);
|
|
18839
|
-
} catch (error) {
|
|
18840
|
-
if (!isAlreadyAttachedError(error)) throw error;
|
|
18841
|
-
}
|
|
18842
|
-
}
|
|
18843
|
-
}
|
|
18844
|
-
/**
|
|
18845
|
-
* `ALTER TABLE ATTACH PARTITION` fails with `42P17 invalid_object_definition`
|
|
18846
|
-
* when the target table is already a partition of *any* parent (including
|
|
18847
|
-
* the one we want). On older PG it can also surface as 42710 or a generic
|
|
18848
|
-
* message. We err on the side of treating any "already a partition" wording
|
|
18849
|
-
* as success so concurrent inits converge cleanly.
|
|
18850
|
-
*/
|
|
18851
|
-
function isAlreadyAttachedError(error) {
|
|
18852
|
-
const code = error?.code;
|
|
18853
|
-
const message = error?.message ?? "";
|
|
18854
|
-
if (code === "42P17" || code === "42710") return /is already a partition/i.test(message) || /already a partition/i.test(message);
|
|
18855
|
-
return /is already a partition/i.test(message);
|
|
18856
|
-
}
|
|
18857
|
-
/**
|
|
18858
|
-
* Calls `create_hypertable()` for each signal table.
|
|
18859
|
-
*
|
|
18860
|
-
* Postgres declarative partitioning and Timescale hypertables are mutually
|
|
18861
|
-
* exclusive, so the base DDL must have been generated with TableDDLMode
|
|
18862
|
-
* 'timescale' (no `PARTITION BY` clause). `if_not_exists` makes this
|
|
18863
|
-
* idempotent across re-inits.
|
|
18864
|
-
*/
|
|
18865
|
-
async function ensureTimescaleHypertables(client, schema) {
|
|
18866
|
-
for (const table of ALL_SIGNAL_TABLES) {
|
|
18867
|
-
const tableExpr = qualifiedTable(schema, table);
|
|
18868
|
-
const timeColumn = SIGNAL_TIME_COLUMN[table].replace(/'/g, "''");
|
|
18869
|
-
await client.none(`SELECT create_hypertable($1::regclass, '${timeColumn}', chunk_time_interval => INTERVAL '1 day', if_not_exists => true)`, [tableExpr]);
|
|
18870
|
-
}
|
|
18871
|
-
}
|
|
18872
|
-
/**
|
|
18873
|
-
* Registers each signal table with pg_partman as a daily-interval parent and
|
|
18874
|
-
* lets partman create the child partitions.
|
|
18875
|
-
*
|
|
18876
|
-
* Retention is intentionally NOT configured here — it stays opt-in via the
|
|
18877
|
-
* future Mastra CLI surface.
|
|
18878
|
-
*
|
|
18879
|
-
* Prefers the pg_partman 5.x+ declarative API (`p_type := 'range'`) and keeps
|
|
18880
|
-
* a 4.x fallback (`p_type := 'native'`) for older installs. That lets the
|
|
18881
|
-
* adapter run against current arm64-friendly images without breaking existing
|
|
18882
|
-
* 4.x deployments.
|
|
18883
|
-
*/
|
|
18884
|
-
async function ensurePartmanHypertables(client, schema) {
|
|
18885
|
-
const partmanMajor = getPartmanMajor(await getPartmanVersion(client));
|
|
18886
|
-
for (const table of ALL_SIGNAL_TABLES) {
|
|
18887
|
-
if ((await client.oneOrNone(`SELECT EXISTS (
|
|
18888
|
-
SELECT 1 FROM partman.part_config
|
|
18889
|
-
WHERE parent_table = format('%I.%I', $1::text, $2::text)
|
|
18890
|
-
) AS "exists"`, [schema, table]))?.exists) continue;
|
|
18891
|
-
for (let attempt = 0; attempt < 3; attempt++) try {
|
|
18892
|
-
if (partmanMajor != null && partmanMajor >= 5) await client.none(`SELECT partman.create_parent(
|
|
18893
|
-
p_parent_table := format('%I.%I', $1::text, $2::text),
|
|
18894
|
-
p_control := $3,
|
|
18895
|
-
p_interval := '1 day',
|
|
18896
|
-
p_type := 'range'
|
|
18897
|
-
)`, [
|
|
18898
|
-
schema,
|
|
18899
|
-
table,
|
|
18900
|
-
SIGNAL_TIME_COLUMN[table]
|
|
18901
|
-
]);
|
|
18902
|
-
else await client.none(`SELECT partman.create_parent(
|
|
18903
|
-
p_parent_table := format('%I.%I', $1::text, $2::text),
|
|
18904
|
-
p_control := $3,
|
|
18905
|
-
p_type := 'native',
|
|
18906
|
-
p_interval := '1 day'
|
|
18907
|
-
)`, [
|
|
18908
|
-
schema,
|
|
18909
|
-
table,
|
|
18910
|
-
SIGNAL_TIME_COLUMN[table]
|
|
18911
|
-
]);
|
|
18912
|
-
break;
|
|
18913
|
-
} catch (error) {
|
|
18914
|
-
const message = error?.message ?? "";
|
|
18915
|
-
if (error?.code === "23505" || /already managed by pg_partman/i.test(message) || /relation .* already exists/i.test(message)) break;
|
|
18916
|
-
if (!/deadlock detected/i.test(message)) throw error;
|
|
18917
|
-
if ((await client.oneOrNone(`SELECT EXISTS (
|
|
18918
|
-
SELECT 1 FROM partman.part_config
|
|
18919
|
-
WHERE parent_table = format('%I.%I', $1::text, $2::text)
|
|
18920
|
-
) AS "exists"`, [schema, table]))?.exists) break;
|
|
18921
|
-
if (attempt === 2) throw error;
|
|
18922
|
-
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
18923
|
-
}
|
|
18924
|
-
}
|
|
18925
|
-
}
|
|
18926
|
-
async function setupPartitioning(client, schema, options = {}) {
|
|
18927
|
-
switch (await resolveMode(client, options)) {
|
|
18928
|
-
case "timescale":
|
|
18929
|
-
await ensureTimescaleHypertables(client, schema);
|
|
18930
|
-
return "timescale";
|
|
18931
|
-
case "partman":
|
|
18932
|
-
await ensurePartmanHypertables(client, schema);
|
|
18933
|
-
return "partman";
|
|
18934
|
-
case "native":
|
|
18935
|
-
await ensureNativePartitions(client, schema, options);
|
|
18936
|
-
return "native";
|
|
18937
|
-
}
|
|
18938
|
-
}
|
|
18939
19180
|
// src/storage/domains/observability/v-next/retention.ts
|
|
18940
19181
|
/**
|
|
18941
19182
|
* Age-based retention for the v-next Postgres observability domain.
|
|
@@ -19101,16 +19342,14 @@ function collapseExactScoreConflicts(rows) {
|
|
|
19101
19342
|
}
|
|
19102
19343
|
return [...records.values()];
|
|
19103
19344
|
}
|
|
19104
|
-
async function createScore(client, schema, args) {
|
|
19345
|
+
async function createScore(client, schema, args, partitioning) {
|
|
19105
19346
|
const row = scoreRecordToRow(args.score);
|
|
19106
|
-
|
|
19107
|
-
if (insert) await client.query(insert.text, insert.values);
|
|
19347
|
+
await insertRows(client, schema, TABLE_SCORE_EVENTS, [row], partitioning, scoreRewriteConflict(row));
|
|
19108
19348
|
}
|
|
19109
|
-
async function batchCreateScores(client, schema, args) {
|
|
19349
|
+
async function batchCreateScores(client, schema, args, partitioning) {
|
|
19110
19350
|
if (args.scores.length === 0) return;
|
|
19111
19351
|
const rows = collapseExactScoreConflicts(args.scores.map(scoreRecordToRow));
|
|
19112
|
-
|
|
19113
|
-
if (insert) await client.query(insert.text, insert.values);
|
|
19352
|
+
await insertRows(client, schema, TABLE_SCORE_EVENTS, rows, partitioning, scoreRewriteConflict(rows[0]));
|
|
19114
19353
|
}
|
|
19115
19354
|
/**
|
|
19116
19355
|
* Delete score events by scoreId. Optional `organizationId` and `resourceId`
|
|
@@ -19341,7 +19580,22 @@ const TRACE_STATUS_SQL = `CASE WHEN r."error" IS NOT NULL THEN 'error' ELSE 'suc
|
|
|
19341
19580
|
const timeToFirstChunkSupported = "timeToFirstChunkMs" in coreStorage.TRACE_QUERY_FIELD_REGISTRY.trace;
|
|
19342
19581
|
/** Core before the sort rejects `orderBy` on the field, so the sort is only declared when core plans it. */
|
|
19343
19582
|
const timeToFirstChunkSortSupported = coreStorage.TRACE_QUERY_ORDER_FIELDS?.includes("timeToFirstChunkMs") === true;
|
|
19344
|
-
|
|
19583
|
+
/** Same rule for time to first token: returned and sorted only when core knows the field. */
|
|
19584
|
+
const timeToFirstTokenSupported = "timeToFirstTokenMs" in coreStorage.TRACE_QUERY_FIELD_REGISTRY.trace;
|
|
19585
|
+
const timeToFirstTokenSortSupported = coreStorage.TRACE_QUERY_ORDER_FIELDS?.includes("timeToFirstTokenMs") === true;
|
|
19586
|
+
/**
|
|
19587
|
+
* Outcome fields follow the same rule: core before them rejects their predicates and strict rows,
|
|
19588
|
+
* so they are only returned and declared when core knows them.
|
|
19589
|
+
*/
|
|
19590
|
+
const spanQueryOutcomeFieldsSupported = "outcome" in (coreStorage.SPAN_QUERY_FIELD_REGISTRY ?? {});
|
|
19591
|
+
const traceQueryOutcomeFieldsSupported = "outcome" in (coreStorage.TRACE_QUERY_FIELD_REGISTRY?.trace ?? {});
|
|
19592
|
+
const outcomeFeatures = [...spanQueryOutcomeFieldsSupported ? ["span-query-outcome-fields"] : [], ...traceQueryOutcomeFieldsSupported ? ["trace-query-outcome-fields"] : []];
|
|
19593
|
+
const timingFeatures = [
|
|
19594
|
+
...timeToFirstChunkSupported ? ["trace-query-time-to-first-chunk"] : [],
|
|
19595
|
+
...timeToFirstChunkSortSupported ? ["trace-query-time-to-first-chunk-sort"] : [],
|
|
19596
|
+
...timeToFirstTokenSupported ? ["trace-query-time-to-first-token"] : [],
|
|
19597
|
+
...timeToFirstTokenSortSupported ? ["trace-query-time-to-first-token-sort"] : []
|
|
19598
|
+
];
|
|
19345
19599
|
/**
|
|
19346
19600
|
* Keyset bounds for a cursor's sort value: terms for rows strictly past it in `direction`, and the
|
|
19347
19601
|
* condition for rows level with it. Empty values sort last in both directions, so they are past
|
|
@@ -19374,6 +19628,7 @@ const TRACE_FIELDS = {
|
|
|
19374
19628
|
endedAt: "r.\"endedAt\"",
|
|
19375
19629
|
durationMs: durationMsSql("r.\"startedAt\"", "r.\"endedAt\""),
|
|
19376
19630
|
timeToFirstChunkMs: "r.\"timeToFirstChunkMs\"",
|
|
19631
|
+
timeToFirstTokenMs: "r.\"timeToFirstTokenMs\"",
|
|
19377
19632
|
entityId: "r.\"entityId\"",
|
|
19378
19633
|
entityName: "r.\"entityName\"",
|
|
19379
19634
|
entityType: "r.\"entityType\"",
|
|
@@ -19381,6 +19636,8 @@ const TRACE_FIELDS = {
|
|
|
19381
19636
|
spanType: "r.\"spanType\"",
|
|
19382
19637
|
environment: "r.\"environment\"",
|
|
19383
19638
|
status: TRACE_STATUS_SQL,
|
|
19639
|
+
outcome: "r.\"outcome\"",
|
|
19640
|
+
stopReason: "r.\"stopReason\"",
|
|
19384
19641
|
tags: "r.\"tags\""
|
|
19385
19642
|
};
|
|
19386
19643
|
const SPAN_FIELDS = {
|
|
@@ -19392,7 +19649,10 @@ const SPAN_FIELDS = {
|
|
|
19392
19649
|
endedAt: "s.\"endedAt\"",
|
|
19393
19650
|
durationMs: "s.\"durationMs\"",
|
|
19394
19651
|
timeToFirstChunkMs: "s.\"timeToFirstChunkMs\"",
|
|
19652
|
+
timeToFirstTokenMs: "s.\"timeToFirstTokenMs\"",
|
|
19395
19653
|
status: "s.\"status\"",
|
|
19654
|
+
outcome: "s.\"outcome\"",
|
|
19655
|
+
stopReason: "s.\"stopReason\"",
|
|
19396
19656
|
error: "s.\"error\"",
|
|
19397
19657
|
entityType: "s.\"entityType\"",
|
|
19398
19658
|
entityId: "s.\"entityId\"",
|
|
@@ -19462,10 +19722,14 @@ const TRACE_SELECT = `
|
|
|
19462
19722
|
r."startedAt" AS "startedAt",
|
|
19463
19723
|
r."endedAt" AS "endedAt",
|
|
19464
19724
|
r."timeToFirstChunkMs" AS "timeToFirstChunkMs",
|
|
19725
|
+
r."timeToFirstTokenMs" AS "timeToFirstTokenMs",
|
|
19465
19726
|
r."entityName" AS "entityName",
|
|
19466
19727
|
r."entityType" AS "entityType",
|
|
19467
19728
|
r."environment" AS "environment",
|
|
19468
|
-
${TRACE_STATUS_SQL} AS "status"
|
|
19729
|
+
${TRACE_STATUS_SQL} AS "status",
|
|
19730
|
+
r."outcome" AS "outcome",
|
|
19731
|
+
r."stopReason" AS "stopReason",
|
|
19732
|
+
r."stopReasonContext" AS "stopReasonContext"`;
|
|
19469
19733
|
function fieldSql(registry, field) {
|
|
19470
19734
|
const sql = registry[field];
|
|
19471
19735
|
if (sql === void 0) throw new Error(`Unsupported trusted trace-query field: ${field}`);
|
|
@@ -19822,8 +20086,8 @@ function compilePostgresTraceScope(schema, selection, relationCollections, scope
|
|
|
19822
20086
|
s."traceId",
|
|
19823
20087
|
s."name",
|
|
19824
20088
|
s."spanType",
|
|
19825
|
-
CASE WHEN jsonb_typeof(s."attributes" -> 'model') = 'string' THEN s."attributes" ->> 'model' END AS "model",
|
|
19826
|
-
CASE WHEN jsonb_typeof(s."attributes" -> 'provider') = 'string' THEN s."attributes" ->> 'provider' END AS "provider",
|
|
20089
|
+
COALESCE(s."model", CASE WHEN jsonb_typeof(s."attributes" -> 'model') = 'string' THEN s."attributes" ->> 'model' END) AS "model",
|
|
20090
|
+
COALESCE(s."provider", CASE WHEN jsonb_typeof(s."attributes" -> 'provider') = 'string' THEN s."attributes" ->> 'provider' END) AS "provider",
|
|
19827
20091
|
s."startedAt",
|
|
19828
20092
|
CASE WHEN s."isPending" THEN NULL ELSE s."endedAt" END AS "endedAt",
|
|
19829
20093
|
CASE
|
|
@@ -19831,7 +20095,10 @@ function compilePostgresTraceScope(schema, selection, relationCollections, scope
|
|
|
19831
20095
|
ELSE ${durationMsSql("s.\"startedAt\"", "s.\"endedAt\"")}
|
|
19832
20096
|
END AS "durationMs",
|
|
19833
20097
|
s."timeToFirstChunkMs",
|
|
20098
|
+
s."timeToFirstTokenMs",
|
|
19834
20099
|
CASE WHEN s."error" IS NOT NULL THEN 'error' ELSE 'success' END AS "status",
|
|
20100
|
+
s."outcome",
|
|
20101
|
+
s."stopReason",
|
|
19835
20102
|
s."error",
|
|
19836
20103
|
s."entityType",
|
|
19837
20104
|
s."entityId",
|
|
@@ -20205,10 +20472,16 @@ function traceRowToResult(row) {
|
|
|
20205
20472
|
startedAt: asIsoTimestamp$1(row.startedAt),
|
|
20206
20473
|
endedAt: asIsoTimestamp$1(row.endedAt),
|
|
20207
20474
|
...timeToFirstChunkSupported && { timeToFirstChunkMs: row.timeToFirstChunkMs == null ? null : Number(row.timeToFirstChunkMs) },
|
|
20475
|
+
...timeToFirstTokenSupported && { timeToFirstTokenMs: row.timeToFirstTokenMs == null ? null : Number(row.timeToFirstTokenMs) },
|
|
20208
20476
|
entityName: row.entityName == null ? null : String(row.entityName),
|
|
20209
20477
|
entityType: row.entityType == null ? null : String(row.entityType),
|
|
20210
20478
|
environment: row.environment == null ? null : String(row.environment),
|
|
20211
|
-
status: row.status
|
|
20479
|
+
status: row.status,
|
|
20480
|
+
...traceQueryOutcomeFieldsSupported && {
|
|
20481
|
+
outcome: row.outcome == null ? null : String(row.outcome),
|
|
20482
|
+
stopReason: row.stopReason == null ? null : String(row.stopReason),
|
|
20483
|
+
stopReasonContext: row.stopReasonContext ?? null
|
|
20484
|
+
}
|
|
20212
20485
|
};
|
|
20213
20486
|
}
|
|
20214
20487
|
async function queryTraces(client, schema, plan, timeoutMs) {
|
|
@@ -20360,7 +20633,10 @@ const fields = [
|
|
|
20360
20633
|
"experimentId",
|
|
20361
20634
|
"environment",
|
|
20362
20635
|
"executionSource",
|
|
20363
|
-
"timeToFirstChunkMs"
|
|
20636
|
+
"timeToFirstChunkMs",
|
|
20637
|
+
"timeToFirstTokenMs",
|
|
20638
|
+
"outcome",
|
|
20639
|
+
"stopReason"
|
|
20364
20640
|
];
|
|
20365
20641
|
const identities = identityFields.map((field) => `"${field}"`).join(", ");
|
|
20366
20642
|
const identityOrder = identityFields.map((field) => `"${field}" COLLATE "C" ASC NULLS LAST`).join(", ");
|
|
@@ -20410,8 +20686,6 @@ function compilePostgresSpanSelection(schema, plan) {
|
|
|
20410
20686
|
};
|
|
20411
20687
|
values.push(...predicate.values);
|
|
20412
20688
|
const table = qualifiedTable(schema, TABLE_SPAN_EVENTS);
|
|
20413
|
-
const legacyModelFields = coreStorage.SPAN_QUERY_FIELD_REGISTRY ? "" : `CASE WHEN jsonb_typeof(attributes -> 'model') = 'string' THEN attributes ->> 'model' END AS model,
|
|
20414
|
-
CASE WHEN jsonb_typeof(attributes -> 'provider') = 'string' THEN attributes ->> 'provider' END AS provider,`;
|
|
20415
20689
|
return {
|
|
20416
20690
|
ctes: [
|
|
20417
20691
|
`candidate_ids AS MATERIALIZED (
|
|
@@ -20422,7 +20696,7 @@ function compilePostgresSpanSelection(schema, plan) {
|
|
|
20422
20696
|
`current_spans AS MATERIALIZED (
|
|
20423
20697
|
SELECT DISTINCT ON (${identities})
|
|
20424
20698
|
${fields.map((field) => `"${field}"`).join(", ")}, "startedAt", "endedAt", "cursorId"::text AS version,
|
|
20425
|
-
|
|
20699
|
+
"model", "provider",
|
|
20426
20700
|
(EXTRACT(EPOCH FROM ("endedAt" - "startedAt")) * 1000)::float8 AS "durationMs",
|
|
20427
20701
|
CASE WHEN error IS NULL THEN 'success' ELSE 'error' END AS status,
|
|
20428
20702
|
error IS NOT NULL AS "hasError"
|
|
@@ -20492,9 +20766,14 @@ async function querySpans(client, schema, plan, timeoutMs) {
|
|
|
20492
20766
|
return runWithPostgresTraceQueryTimeout(client, timeoutMs, async (tx) => {
|
|
20493
20767
|
const query = compilePostgresSpanQuery(schema, plan);
|
|
20494
20768
|
const rows = await tx.any(query.text, query.values);
|
|
20495
|
-
const selected = rows.map(({ version: _version, startedAt, endedAt, timeToFirstChunkMs, ...row }) => ({
|
|
20769
|
+
const selected = rows.map(({ version: _version, startedAt, endedAt, timeToFirstChunkMs, timeToFirstTokenMs, outcome, stopReason, ...row }) => ({
|
|
20496
20770
|
...row,
|
|
20497
20771
|
...timeToFirstChunkSupported && { timeToFirstChunkMs },
|
|
20772
|
+
...timeToFirstTokenSupported && { timeToFirstTokenMs },
|
|
20773
|
+
...spanQueryOutcomeFieldsSupported && {
|
|
20774
|
+
outcome,
|
|
20775
|
+
stopReason
|
|
20776
|
+
},
|
|
20498
20777
|
startedAt: startedAt.toISOString(),
|
|
20499
20778
|
endedAt: endedAt.toISOString()
|
|
20500
20779
|
}));
|
|
@@ -20512,18 +20791,22 @@ async function querySpans(client, schema, plan, timeoutMs) {
|
|
|
20512
20791
|
payloadValues.push(row.version);
|
|
20513
20792
|
return `(${key} AND "cursorId" = $${payloadValues.length}::bigint)`;
|
|
20514
20793
|
}).join(" OR ");
|
|
20515
|
-
const payloadRows = await tx.any(`SELECT ${identities},
|
|
20516
|
-
CASE WHEN jsonb_typeof(attributes -> 'model') = 'string' THEN attributes ->> 'model' END AS model,
|
|
20517
|
-
CASE WHEN jsonb_typeof(attributes -> 'provider') = 'string' THEN attributes ->> 'provider' END AS provider,
|
|
20794
|
+
const payloadRows = await tx.any(`SELECT ${identities}, "stopReasonContext",
|
|
20795
|
+
COALESCE("model", CASE WHEN jsonb_typeof(attributes -> 'model') = 'string' THEN attributes ->> 'model' END) AS model,
|
|
20796
|
+
COALESCE("provider", CASE WHEN jsonb_typeof(attributes -> 'provider') = 'string' THEN attributes ->> 'provider' END) AS provider,
|
|
20518
20797
|
${PREVIEW_PAYLOADS$1.map(([preview, payload]) => `"${preview}", CASE WHEN ${missingPreviewSql(`"${preview}"`)} THEN "${payload}" END AS "${payload}"`).join(",\n ")}
|
|
20519
20798
|
FROM ${qualifiedTable(schema, TABLE_SPAN_EVENTS)} WHERE ${payloadWhere}`, payloadValues);
|
|
20520
20799
|
const payloads = payloadRows.map(toSpanQueryPayload);
|
|
20521
20800
|
const metadata = new Map(payloadRows.map((row) => [coreStorage.spanQueryIdentityKey(row), row]));
|
|
20522
|
-
const hydrated = selected.map((row) =>
|
|
20523
|
-
|
|
20524
|
-
|
|
20525
|
-
|
|
20526
|
-
|
|
20801
|
+
const hydrated = selected.map((row) => {
|
|
20802
|
+
const stored = metadata.get(coreStorage.spanQueryIdentityKey(row));
|
|
20803
|
+
return {
|
|
20804
|
+
...row,
|
|
20805
|
+
model: stored?.model ?? null,
|
|
20806
|
+
provider: stored?.provider ?? null,
|
|
20807
|
+
...spanQueryOutcomeFieldsSupported && { stopReasonContext: stored?.stopReasonContext ?? null }
|
|
20808
|
+
};
|
|
20809
|
+
});
|
|
20527
20810
|
let metrics = [];
|
|
20528
20811
|
if (!coreStorage.SPAN_QUERY_FIELD_REGISTRY) {
|
|
20529
20812
|
await remaining();
|
|
@@ -20626,7 +20909,11 @@ function compilePostgresThreadRowsQuery(schema, plan, strategy = "full") {
|
|
|
20626
20909
|
}
|
|
20627
20910
|
const qualify = plan.timeRange.mode === "traces" ? "s.\"inWindow\"" : `s."${plan.timeRange.field}" >= $1 AND s."${plan.timeRange.field}" < $2`;
|
|
20628
20911
|
const filtered = predicateFields(plan.where);
|
|
20629
|
-
const firstContext = [
|
|
20912
|
+
const firstContext = [
|
|
20913
|
+
...contextFields,
|
|
20914
|
+
"timeToFirstChunkMs",
|
|
20915
|
+
"timeToFirstTokenMs"
|
|
20916
|
+
].filter((field) => field === "resourceId" || filtered.has(field));
|
|
20630
20917
|
const lastFields = [
|
|
20631
20918
|
"traceId",
|
|
20632
20919
|
"spanId",
|
|
@@ -20640,7 +20927,7 @@ function compilePostgresThreadRowsQuery(schema, plan, strategy = "full") {
|
|
|
20640
20927
|
text: `WITH ${scopedRoots}, ${strategy === "candidates" ? `${candidates}, ` : ""}current_roots AS (
|
|
20641
20928
|
SELECT DISTINCT ON ("organizationId", "traceId")
|
|
20642
20929
|
"organizationId", "threadId", "traceId", "spanId", "cursorId", "startedAt", "endedAt", "isPending",
|
|
20643
|
-
${firstContext.map((field) => field === "timeToFirstChunkMs" ? `CASE WHEN "spanType" = 'agent_run' THEN "
|
|
20930
|
+
${firstContext.map((field) => field === "timeToFirstChunkMs" || field === "timeToFirstTokenMs" ? `CASE WHEN "spanType" = 'agent_run' THEN "${field}" END AS "${field}"` : `"${field}"`).join(", ")}
|
|
20644
20931
|
${filtered.has("status") ? ", CASE WHEN error IS NULL THEN 'success' ELSE 'error' END AS status" : ""}
|
|
20645
20932
|
FROM scoped_roots ${strategy === "candidates" ? `WHERE (${identity("traceId")}) IN (SELECT ${identity("traceId")} FROM candidate_traces)` : ""}
|
|
20646
20933
|
ORDER BY "organizationId", "traceId", "startedAt" ASC, "spanId" COLLATE "C", "cursorId" DESC
|
|
@@ -20706,6 +20993,7 @@ async function queryThreadRows(client, schema, plan, timeoutMs, strategy = "auto
|
|
|
20706
20993
|
}));
|
|
20707
20994
|
const payloads = await run(`SELECT "organizationId", "traceId", "cursorId", ${contextFields.map((field) => `"${field}"`).join(", ")},
|
|
20708
20995
|
CASE WHEN "spanType" = 'agent_run' THEN "timeToFirstChunkMs" END AS "timeToFirstChunkMs",
|
|
20996
|
+
CASE WHEN "spanType" = 'agent_run' THEN "timeToFirstTokenMs" END AS "timeToFirstTokenMs",
|
|
20709
20997
|
CASE WHEN error IS NULL THEN 'success' ELSE 'error' END AS status,
|
|
20710
20998
|
${[
|
|
20711
20999
|
"input",
|
|
@@ -20734,6 +21022,7 @@ async function queryThreadRows(client, schema, plan, timeoutMs, strategy = "auto
|
|
|
20734
21022
|
endedAt: row.endedAt.toISOString(),
|
|
20735
21023
|
traceCount: Number(row.traceCount),
|
|
20736
21024
|
timeToFirstChunkMs: first.timeToFirstChunkMs,
|
|
21025
|
+
...timeToFirstTokenSupported && { timeToFirstTokenMs: first.timeToFirstTokenMs },
|
|
20737
21026
|
inputPreview: readPreview(first.inputPreview, first.input),
|
|
20738
21027
|
outputPreview: readPreview(last.outputPreview, last.output),
|
|
20739
21028
|
errorPreview: readPreview(last.errorPreview, last.error)
|
|
@@ -20759,6 +21048,8 @@ const DIMENSION_SQL = {
|
|
|
20759
21048
|
entityId: "r.\"entityId\"",
|
|
20760
21049
|
environment: "r.\"environment\"",
|
|
20761
21050
|
status: TRACE_STATUS_SQL,
|
|
21051
|
+
outcome: "r.\"outcome\"",
|
|
21052
|
+
stopReason: "r.\"stopReason\"",
|
|
20762
21053
|
serviceName: "r.\"serviceName\"",
|
|
20763
21054
|
executionSource: "r.\"executionSource\"",
|
|
20764
21055
|
threadId: "r.\"threadId\"",
|
|
@@ -20781,6 +21072,8 @@ const SPAN_DIMENSION_SQL = Object.fromEntries([
|
|
|
20781
21072
|
"rootEntityName",
|
|
20782
21073
|
"environment",
|
|
20783
21074
|
"status",
|
|
21075
|
+
"outcome",
|
|
21076
|
+
"stopReason",
|
|
20784
21077
|
"serviceName",
|
|
20785
21078
|
"executionSource",
|
|
20786
21079
|
"threadId",
|
|
@@ -20788,7 +21081,9 @@ const SPAN_DIMENSION_SQL = Object.fromEntries([
|
|
|
20788
21081
|
"userId",
|
|
20789
21082
|
"sessionId",
|
|
20790
21083
|
"organizationId",
|
|
20791
|
-
"experimentId"
|
|
21084
|
+
"experimentId",
|
|
21085
|
+
"model",
|
|
21086
|
+
"provider"
|
|
20792
21087
|
].map((field) => [field, `r."${field}"`]));
|
|
20793
21088
|
const PERCENTILES = {
|
|
20794
21089
|
"duration.p50": .5,
|
|
@@ -21690,10 +21985,9 @@ async function readBranchesStreamHeadCursor(client, span, filters) {
|
|
|
21690
21985
|
return encodeDeltaCursor(await readSafeXactHorizon(client), 0);
|
|
21691
21986
|
}
|
|
21692
21987
|
// src/storage/domains/observability/v-next/tracing.ts
|
|
21693
|
-
async function writeSpanRows(client, schema, rows) {
|
|
21988
|
+
async function writeSpanRows(client, schema, rows, partitioning) {
|
|
21694
21989
|
const deduped = dedupeSpanRows(rows);
|
|
21695
|
-
|
|
21696
|
-
if (insert) await client.query(insert.text, insert.values);
|
|
21990
|
+
await insertRows(client, schema, TABLE_SPAN_EVENTS, deduped, partitioning, spanConflictClause(Object.keys(deduped[0] ?? {})));
|
|
21697
21991
|
}
|
|
21698
21992
|
/**
|
|
21699
21993
|
* Collapse rows that share a primary key within a single batch. Postgres
|
|
@@ -21713,12 +22007,12 @@ function dedupeSpanRows(rows) {
|
|
|
21713
22007
|
}
|
|
21714
22008
|
return [...byKey.values()];
|
|
21715
22009
|
}
|
|
21716
|
-
async function createSpan(client, schema, args) {
|
|
21717
|
-
await writeSpanRows(client, schema, [spanRecordToRow(args.span)]);
|
|
22010
|
+
async function createSpan(client, schema, args, partitioning) {
|
|
22011
|
+
await writeSpanRows(client, schema, [spanRecordToRow(args.span)], partitioning);
|
|
21718
22012
|
}
|
|
21719
|
-
async function batchCreateSpans(client, schema, args) {
|
|
22013
|
+
async function batchCreateSpans(client, schema, args, partitioning) {
|
|
21720
22014
|
if (args.records.length === 0) return;
|
|
21721
|
-
await writeSpanRows(client, schema, args.records.map(spanRecordToRow));
|
|
22015
|
+
await writeSpanRows(client, schema, args.records.map(spanRecordToRow), partitioning);
|
|
21722
22016
|
}
|
|
21723
22017
|
async function getSpans(client, schema, args) {
|
|
21724
22018
|
if (args.spanIds.length === 0) return {
|
|
@@ -21842,8 +22136,12 @@ async function dangerouslyClearTracing(client, schema) {
|
|
|
21842
22136
|
* application database — observability writes will degrade app performance.
|
|
21843
22137
|
* Use it through `MastraCompositeStore` with a dedicated Postgres connection.
|
|
21844
22138
|
*/
|
|
21845
|
-
const spanQueryFeatures = typeof coreStorage.planSpanQuery === "function" ? [
|
|
21846
|
-
|
|
22139
|
+
const spanQueryFeatures = typeof coreStorage.planSpanQuery === "function" ? [
|
|
22140
|
+
"span-query",
|
|
22141
|
+
"span-query-context-fields",
|
|
22142
|
+
..."model" in (coreStorage.SPAN_QUERY_FIELD_REGISTRY ?? {}) ? ["span-query-model-fields"] : []
|
|
22143
|
+
] : [];
|
|
22144
|
+
const spanAggregateFeatures = typeof coreStorage.planSpanAggregate === "function" ? ["span-aggregate", ..."model" in (coreStorage.SPAN_AGGREGATE_DIMENSION_REGISTRY ?? {}) ? ["span-aggregate-model-dimensions"] : []] : [];
|
|
21847
22145
|
function wrapError(op, error, details) {
|
|
21848
22146
|
if (error instanceof MastraError || error instanceof coreStorage.TraceQueryCursorError || error instanceof coreStorage.TraceQueryExecutionError || error instanceof coreStorage.TraceQueryResourceLimitError) throw error;
|
|
21849
22147
|
throw new MastraError({
|
|
@@ -21939,6 +22237,18 @@ var ObservabilityStoragePostgresVNext = class ObservabilityStoragePostgresVNext
|
|
|
21939
22237
|
return this.#partitionMode;
|
|
21940
22238
|
}
|
|
21941
22239
|
/**
|
|
22240
|
+
* Create any missing day partitions from yesterday through today +
|
|
22241
|
+
* `futureDays`, without the rest of init(). For apps whose runtime role
|
|
22242
|
+
* cannot run DDL: call it on a schedule with a role that owns the
|
|
22243
|
+
* observability tables. Safe to call repeatedly on one instance. Does
|
|
22244
|
+
* nothing with TimescaleDB or pg_partman, which create their own partitions.
|
|
22245
|
+
*/
|
|
22246
|
+
async ensurePartitions() {
|
|
22247
|
+
return this.#run("VNEXT_ENSURE_PARTITIONS", async () => {
|
|
22248
|
+
if ((this.#partitionMode ??= await resolveMode(this.#client, this.#partitioning)) === "native") await ensureNativePartitions(this.#client, this.#schema, this.#partitioning);
|
|
22249
|
+
});
|
|
22250
|
+
}
|
|
22251
|
+
/**
|
|
21942
22252
|
* All five signal tables are insert-only growth tables. The anchor column is
|
|
21943
22253
|
* each table's partition / chunk key, so age-based expiry drops whole day
|
|
21944
22254
|
* partitions instead of deleting rows (see `./retention.ts`). `indexed: true`
|
|
@@ -22042,7 +22352,8 @@ var ObservabilityStoragePostgresVNext = class ObservabilityStoragePostgresVNext
|
|
|
22042
22352
|
"trace-query-tenant-scope",
|
|
22043
22353
|
"feedback",
|
|
22044
22354
|
"trace-query-context-ids",
|
|
22045
|
-
...
|
|
22355
|
+
...timingFeatures,
|
|
22356
|
+
...outcomeFeatures,
|
|
22046
22357
|
"trace-query-root-span-fields",
|
|
22047
22358
|
"trace-query-root-entity-id"
|
|
22048
22359
|
];
|
|
@@ -22067,11 +22378,24 @@ var ObservabilityStoragePostgresVNext = class ObservabilityStoragePostgresVNext
|
|
|
22067
22378
|
"trace-query-tenant-scope",
|
|
22068
22379
|
"feedback",
|
|
22069
22380
|
"trace-query-context-ids",
|
|
22070
|
-
...
|
|
22381
|
+
...timingFeatures,
|
|
22382
|
+
...outcomeFeatures,
|
|
22071
22383
|
"trace-query-root-span-fields",
|
|
22072
22384
|
"trace-query-root-entity-id"
|
|
22073
22385
|
];
|
|
22074
22386
|
}
|
|
22387
|
+
/**
|
|
22388
|
+
* Partitioning options for the write path, with `mode` fixed to the mode
|
|
22389
|
+
* this store resolved (in init(), or lazily in prune()) so the write path
|
|
22390
|
+
* does not detect it again. Until then the configured mode stands in and
|
|
22391
|
+
* the write path resolves it itself when a partition is missing.
|
|
22392
|
+
*/
|
|
22393
|
+
#writePartitioning() {
|
|
22394
|
+
return {
|
|
22395
|
+
...this.#partitioning,
|
|
22396
|
+
mode: this.#partitionMode ?? this.#partitioning.mode
|
|
22397
|
+
};
|
|
22398
|
+
}
|
|
22075
22399
|
async #run(op, fn, details) {
|
|
22076
22400
|
try {
|
|
22077
22401
|
return await fn();
|
|
@@ -22080,13 +22404,13 @@ var ObservabilityStoragePostgresVNext = class ObservabilityStoragePostgresVNext
|
|
|
22080
22404
|
}
|
|
22081
22405
|
}
|
|
22082
22406
|
async createSpan(args) {
|
|
22083
|
-
await this.#run("CREATE_SPAN", () => createSpan(this.#client, this.#schema, args), {
|
|
22407
|
+
await this.#run("CREATE_SPAN", () => createSpan(this.#client, this.#schema, args, this.#writePartitioning()), {
|
|
22084
22408
|
traceId: args.span.traceId,
|
|
22085
22409
|
spanId: args.span.spanId
|
|
22086
22410
|
});
|
|
22087
22411
|
}
|
|
22088
22412
|
async batchCreateSpans(args) {
|
|
22089
|
-
await this.#run("BATCH_CREATE_SPANS", () => batchCreateSpans(this.#client, this.#schema, args), { count: args.records.length });
|
|
22413
|
+
await this.#run("BATCH_CREATE_SPANS", () => batchCreateSpans(this.#client, this.#schema, args, this.#writePartitioning()), { count: args.records.length });
|
|
22090
22414
|
}
|
|
22091
22415
|
async getSpan(args) {
|
|
22092
22416
|
return this.#run("GET_SPAN", () => getSpan(this.#readClient, this.#schema, args), {
|
|
@@ -22140,22 +22464,22 @@ var ObservabilityStoragePostgresVNext = class ObservabilityStoragePostgresVNext
|
|
|
22140
22464
|
return this.#run("LIST_BRANCHES", () => listBranches(this.#readClient, this.#schema, args));
|
|
22141
22465
|
}
|
|
22142
22466
|
async batchCreateLogs(args) {
|
|
22143
|
-
await this.#run("BATCH_CREATE_LOGS", () => batchCreateLogs(this.#client, this.#schema, args), { count: args.logs.length });
|
|
22467
|
+
await this.#run("BATCH_CREATE_LOGS", () => batchCreateLogs(this.#client, this.#schema, args, this.#writePartitioning()), { count: args.logs.length });
|
|
22144
22468
|
}
|
|
22145
22469
|
async batchCreateMetrics(args) {
|
|
22146
|
-
await this.#run("BATCH_CREATE_METRICS", () => batchCreateMetrics(this.#client, this.#schema, args), { count: args.metrics.length });
|
|
22470
|
+
await this.#run("BATCH_CREATE_METRICS", () => batchCreateMetrics(this.#client, this.#schema, args, this.#writePartitioning()), { count: args.metrics.length });
|
|
22147
22471
|
}
|
|
22148
22472
|
async createScore(args) {
|
|
22149
|
-
await this.#run("CREATE_SCORE", () => createScore(this.#client, this.#schema, args));
|
|
22473
|
+
await this.#run("CREATE_SCORE", () => createScore(this.#client, this.#schema, args, this.#writePartitioning()));
|
|
22150
22474
|
}
|
|
22151
22475
|
async batchCreateScores(args) {
|
|
22152
|
-
await this.#run("BATCH_CREATE_SCORES", () => batchCreateScores(this.#client, this.#schema, args), { count: args.scores.length });
|
|
22476
|
+
await this.#run("BATCH_CREATE_SCORES", () => batchCreateScores(this.#client, this.#schema, args, this.#writePartitioning()), { count: args.scores.length });
|
|
22153
22477
|
}
|
|
22154
22478
|
async createFeedback(args) {
|
|
22155
|
-
await this.#run("CREATE_FEEDBACK", () => createFeedback(this.#client, this.#schema, args));
|
|
22479
|
+
await this.#run("CREATE_FEEDBACK", () => createFeedback(this.#client, this.#schema, args, this.#writePartitioning()));
|
|
22156
22480
|
}
|
|
22157
22481
|
async batchCreateFeedback(args) {
|
|
22158
|
-
await this.#run("BATCH_CREATE_FEEDBACK", () => batchCreateFeedback(this.#client, this.#schema, args), { count: args.feedbacks.length });
|
|
22482
|
+
await this.#run("BATCH_CREATE_FEEDBACK", () => batchCreateFeedback(this.#client, this.#schema, args, this.#writePartitioning()), { count: args.feedbacks.length });
|
|
22159
22483
|
}
|
|
22160
22484
|
async deleteScores(args) {
|
|
22161
22485
|
await this.#run("DELETE_SCORES", () => deleteScores(this.#client, this.#schema, args), { count: args.scoreIds.length });
|
|
@@ -27565,12 +27889,20 @@ var PostgresStore = class extends MastraCompositeStore {
|
|
|
27565
27889
|
}
|
|
27566
27890
|
async #runPinnedInit() {
|
|
27567
27891
|
let pinnedClient;
|
|
27892
|
+
let pinned;
|
|
27568
27893
|
try {
|
|
27569
27894
|
pinnedClient = await this.#writePool.connect();
|
|
27570
|
-
|
|
27895
|
+
pinned = new PinnedClientAdapter(this.#writePool, pinnedClient);
|
|
27571
27896
|
this.#db.pin(pinned);
|
|
27572
|
-
|
|
27573
|
-
|
|
27897
|
+
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
27898
|
+
this.#db.setSchemaSnapshot(await loadSchemaSnapshot(pinned, this.schema));
|
|
27899
|
+
const failures = (await Promise.allSettled(Object.entries(this.stores).filter(([name, store]) => name !== "knowledge" && store).map(([, store]) => Promise.resolve().then(() => store.init())))).filter((result) => result.status === "rejected");
|
|
27900
|
+
if (failures.length === 0) break;
|
|
27901
|
+
const terminal = failures.find((result) => !isRetryableDeadlock(result.reason));
|
|
27902
|
+
if (terminal || attempt === 3) throw (terminal ?? failures[0]).reason;
|
|
27903
|
+
this.#db.setSchemaSnapshot(null);
|
|
27904
|
+
await backoffAfterDeadlock(attempt);
|
|
27905
|
+
}
|
|
27574
27906
|
this.isInitialized = true;
|
|
27575
27907
|
} catch (error) {
|
|
27576
27908
|
this.#initPromise = null;
|
|
@@ -27581,6 +27913,7 @@ var PostgresStore = class extends MastraCompositeStore {
|
|
|
27581
27913
|
category: ErrorCategory.THIRD_PARTY
|
|
27582
27914
|
}, error);
|
|
27583
27915
|
} finally {
|
|
27916
|
+
await pinned?.drain();
|
|
27584
27917
|
this.#db.setSchemaSnapshot(null);
|
|
27585
27918
|
if (pinnedClient) {
|
|
27586
27919
|
this.#db.unpin();
|
|
@@ -27735,6 +28068,10 @@ var PostgresStoreVNext = class extends PostgresStore {
|
|
|
27735
28068
|
owned: true
|
|
27736
28069
|
};
|
|
27737
28070
|
}
|
|
28071
|
+
/** See {@link ObservabilityStoragePostgresVNext.ensurePartitions}. */
|
|
28072
|
+
async ensurePartitions() {
|
|
28073
|
+
await this.stores.observability.ensurePartitions();
|
|
28074
|
+
}
|
|
27738
28075
|
/**
|
|
27739
28076
|
* Closes both the primary pool (when owned) and the observability pool
|
|
27740
28077
|
* (when this store created it). Safe to call multiple times.
|