@mastra/pg 1.33.0 → 1.34.0-alpha.0

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.
Files changed (66) hide show
  1. package/dist/index.cjs +491 -258
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.js +492 -259
  4. package/dist/index.js.map +1 -1
  5. package/dist/storage/client.d.ts +1 -0
  6. package/dist/storage/client.d.ts.map +1 -1
  7. package/dist/storage/db/index-build.d.ts +5 -0
  8. package/dist/storage/db/index-build.d.ts.map +1 -0
  9. package/dist/storage/db/index.d.ts +0 -8
  10. package/dist/storage/db/index.d.ts.map +1 -1
  11. package/dist/storage/db/pg-errors.d.ts +6 -0
  12. package/dist/storage/db/pg-errors.d.ts.map +1 -1
  13. package/dist/storage/db/schema-snapshot.d.ts +4 -7
  14. package/dist/storage/db/schema-snapshot.d.ts.map +1 -1
  15. package/dist/storage/domains/observability/v-next/ddl.d.ts +2 -1
  16. package/dist/storage/domains/observability/v-next/ddl.d.ts.map +1 -1
  17. package/dist/storage/domains/observability/v-next/feedback.d.ts +3 -2
  18. package/dist/storage/domains/observability/v-next/feedback.d.ts.map +1 -1
  19. package/dist/storage/domains/observability/v-next/index.d.ts +9 -1
  20. package/dist/storage/domains/observability/v-next/index.d.ts.map +1 -1
  21. package/dist/storage/domains/observability/v-next/logs.d.ts +2 -1
  22. package/dist/storage/domains/observability/v-next/logs.d.ts.map +1 -1
  23. package/dist/storage/domains/observability/v-next/metrics.d.ts +2 -1
  24. package/dist/storage/domains/observability/v-next/metrics.d.ts.map +1 -1
  25. package/dist/storage/domains/observability/v-next/partitioning.d.ts +29 -19
  26. package/dist/storage/domains/observability/v-next/partitioning.d.ts.map +1 -1
  27. package/dist/storage/domains/observability/v-next/preview-compat.d.ts +1 -0
  28. package/dist/storage/domains/observability/v-next/preview-compat.d.ts.map +1 -1
  29. package/dist/storage/domains/observability/v-next/scores.d.ts +3 -2
  30. package/dist/storage/domains/observability/v-next/scores.d.ts.map +1 -1
  31. package/dist/storage/domains/observability/v-next/span-query.d.ts +1 -0
  32. package/dist/storage/domains/observability/v-next/span-query.d.ts.map +1 -1
  33. package/dist/storage/domains/observability/v-next/sql.d.ts +12 -0
  34. package/dist/storage/domains/observability/v-next/sql.d.ts.map +1 -1
  35. package/dist/storage/domains/observability/v-next/thread-query.d.ts +1 -0
  36. package/dist/storage/domains/observability/v-next/thread-query.d.ts.map +1 -1
  37. package/dist/storage/domains/observability/v-next/trace-query.d.ts +3 -1
  38. package/dist/storage/domains/observability/v-next/trace-query.d.ts.map +1 -1
  39. package/dist/storage/domains/observability/v-next/tracing.d.ts +3 -2
  40. package/dist/storage/domains/observability/v-next/tracing.d.ts.map +1 -1
  41. package/dist/storage/index.d.ts +2 -0
  42. package/dist/storage/index.d.ts.map +1 -1
  43. package/package.json +4 -4
  44. package/dist/docs/SKILL.md +0 -48
  45. package/dist/docs/assets/SOURCE_MAP.json +0 -6
  46. package/dist/docs/references/docs-deployment-workers.md +0 -404
  47. package/dist/docs/references/docs-memory-semantic-recall.md +0 -483
  48. package/dist/docs/references/docs-memory-working-memory.md +0 -450
  49. package/dist/docs/references/docs-storage.md +0 -224
  50. package/dist/docs/references/integrations-databases-clickhouse-managed-postgres.md +0 -82
  51. package/dist/docs/references/integrations-databases-dynamodb.md +0 -286
  52. package/dist/docs/references/integrations-databases-neon.md +0 -222
  53. package/dist/docs/references/integrations-databases-postgresql.md +0 -561
  54. package/dist/docs/references/reference-memory-memory-class.md +0 -160
  55. package/dist/docs/references/reference-processors-message-history-processor.md +0 -89
  56. package/dist/docs/references/reference-processors-semantic-recall-processor.md +0 -119
  57. package/dist/docs/references/reference-processors-working-memory-processor.md +0 -154
  58. package/dist/docs/references/reference-rag-chunking-and-embedding.md +0 -184
  59. package/dist/docs/references/reference-rag-metadata-filters.md +0 -238
  60. package/dist/docs/references/reference-rag-overview.md +0 -76
  61. package/dist/docs/references/reference-rag-retrieval.md +0 -668
  62. package/dist/docs/references/reference-rag-vector-databases.md +0 -808
  63. package/dist/docs/references/reference-storage-composite.md +0 -433
  64. package/dist/docs/references/reference-storage-retention.md +0 -310
  65. package/dist/docs/references/reference-tools-vector-query-tool.md +0 -500
  66. package/dist/docs/references/reference-vectors-pg.md +0 -439
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 Set();
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.add(row.indexname.toLowerCase());
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 schemaFilter = this.schemaName || "public";
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.has(constraintName.toLowerCase());
3485
- return this.spansPrimaryKeyExistsLive(constraintName, schemaFilter);
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, schemaFilter) {
3489
- return (await this.client.oneOrNone(`SELECT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = lower($1) AND connamespace = (SELECT oid FROM pg_namespace WHERE nspname = $2)) as exists`, [constraintName, schemaFilter]))?.exists ?? false;
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 ((await this.client.oneOrNone(`
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.add(constraintName.toLowerCase());
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
- if (isDuplicateRelationError(error)) {
3523
- if (await this.spansPrimaryKeyExistsLive(constraintName, schemaFilter)) {
3524
- this.schemaSnapshot?.primaryKeyIndexes.add(constraintName.toLowerCase());
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 (!await this.prepareIndexBuild(indexName, this.schemaName || "public")) return;
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();
@@ -17299,6 +17395,203 @@ function parseUpdateFeedbackReviewStatusArgs(args) {
17299
17395
  reviewStatus: args.reviewStatus
17300
17396
  };
17301
17397
  }
17398
+ // src/storage/domains/observability/v-next/partitioning.ts
17399
+ const DEFAULT_FUTURE_DAYS = 14;
17400
+ async function detectTimescale(client) {
17401
+ const row = await client.oneOrNone(`SELECT EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'timescaledb') AS "exists"`);
17402
+ return Boolean(row?.exists);
17403
+ }
17404
+ async function detectPartman(client) {
17405
+ const row = await client.oneOrNone(`SELECT EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'pg_partman') AS "exists"`);
17406
+ return Boolean(row?.exists);
17407
+ }
17408
+ async function getPartmanVersion(client) {
17409
+ return (await client.oneOrNone(`SELECT extversion FROM pg_extension WHERE extname = 'pg_partman'`))?.extversion ?? null;
17410
+ }
17411
+ function getPartmanMajor(version) {
17412
+ if (!version) return null;
17413
+ const match = /^(\d+)/.exec(version);
17414
+ return match ? Number(match[1]) : null;
17415
+ }
17416
+ async function resolveMode(client, options = {}) {
17417
+ if (options.mode && options.mode !== "auto") return options.mode;
17418
+ if (await detectTimescale(client)) return "timescale";
17419
+ if (await detectPartman(client)) return "partman";
17420
+ return "native";
17421
+ }
17422
+ function dayBounds(d) {
17423
+ const year = d.getUTCFullYear();
17424
+ const month = String(d.getUTCMonth() + 1).padStart(2, "0");
17425
+ const day = String(d.getUTCDate()).padStart(2, "0");
17426
+ const start = `${year}-${month}-${day} 00:00:00+00`;
17427
+ const next = new Date(Date.UTC(year, d.getUTCMonth(), d.getUTCDate() + 1));
17428
+ return {
17429
+ start,
17430
+ end: `${next.getUTCFullYear()}-${String(next.getUTCMonth() + 1).padStart(2, "0")}-${String(next.getUTCDate()).padStart(2, "0")} 00:00:00+00`,
17431
+ suffix: `${year}${month}${day}`
17432
+ };
17433
+ }
17434
+ function partitionName(table, suffix) {
17435
+ return `${table}_p${suffix}`;
17436
+ }
17437
+ /**
17438
+ * Creates daily partition tables in [today - 1 day, today + futureDays].
17439
+ * Idempotent: uses CREATE TABLE IF NOT EXISTS.
17440
+ */
17441
+ async function ensureNativePartitions(client, schema, options = {}) {
17442
+ const windowDays = partitionWindow(options);
17443
+ for (const table of ALL_SIGNAL_TABLES) await ensureDayPartitions(client, schema, table, windowDays);
17444
+ }
17445
+ /** The UTC days init() pre-creates: yesterday (optional) through today + futureDays. */
17446
+ function partitionWindow(options = {}, nowMs = Date.now()) {
17447
+ const futureDays = options.futureDays ?? DEFAULT_FUTURE_DAYS;
17448
+ const start = options.includeYesterday ?? true ? -1 : 0;
17449
+ const days = [];
17450
+ for (let i = start; i <= futureDays; i++) days.push(new Date(nowMs + i * 864e5));
17451
+ return days;
17452
+ }
17453
+ /**
17454
+ * Keeps only the days inside the current partition window. The write path
17455
+ * creates partitions on demand within that window only, so a row with a
17456
+ * client-supplied timestamp far in the past or future cannot make the
17457
+ * database grow a table per day it names.
17458
+ */
17459
+ function withinPartitionWindow(days, options = {}) {
17460
+ const allowed = new Set(partitionWindow(options).map((d) => dayBounds(d).suffix));
17461
+ return days.filter((d) => allowed.has(dayBounds(d).suffix));
17462
+ }
17463
+ /**
17464
+ * Creates and attaches a partition of `schema.table` for each UTC day in
17465
+ * `days` that has none. Each day is a `CREATE TABLE (LIKE parent)` followed by
17466
+ * `ATTACH PARTITION`, in one transaction, so a failed attach leaves no
17467
+ * detached child behind. When either step fails, the catalog is read again: a
17468
+ * day that a concurrent caller attached meanwhile counts as done, and any
17469
+ * other failure is rethrown.
17470
+ */
17471
+ async function ensureDayPartitions(client, schema, table, days) {
17472
+ const parent = qualifiedTable(schema, table);
17473
+ const attached = await attachedPartitionNames(client, schema, table);
17474
+ for (const d of days) {
17475
+ const { start: partStart, end: partEnd, suffix } = dayBounds(d);
17476
+ const childName = partitionName(table, suffix);
17477
+ if (attached.has(childName)) continue;
17478
+ const child = qualifiedName(schema, childName);
17479
+ try {
17480
+ await client.tx(async (t) => {
17481
+ await t.none(`CREATE TABLE IF NOT EXISTS ${child} (LIKE ${parent} INCLUDING ALL)`);
17482
+ await t.none(`ALTER TABLE ${parent} ATTACH PARTITION ${child}
17483
+ FOR VALUES FROM ('${partStart}') TO ('${partEnd}')`);
17484
+ });
17485
+ } catch (error) {
17486
+ if (!(await attachedPartitionNames(client, schema, table)).has(childName)) throw error;
17487
+ }
17488
+ }
17489
+ }
17490
+ /** Names of the children attached to `schema.table`, read from the catalog so no search_path can shadow it. */
17491
+ async function attachedPartitionNames(client, schema, table) {
17492
+ const rows = await client.manyOrNone(`SELECT c.relname
17493
+ FROM pg_catalog.pg_inherits i
17494
+ JOIN pg_catalog.pg_class c ON c.oid = i.inhrelid
17495
+ WHERE i.inhparent = (
17496
+ SELECT p.oid
17497
+ FROM pg_catalog.pg_class p
17498
+ JOIN pg_catalog.pg_namespace n ON n.oid = p.relnamespace
17499
+ WHERE n.nspname = $1 AND p.relname = $2
17500
+ )`, [schema, table]);
17501
+ return new Set(rows.map((r) => r.relname));
17502
+ }
17503
+ /**
17504
+ * `23514` raised by `ExecFindPartition`: the row's day has no attached
17505
+ * partition. The message is matched as a fallback only, because Postgres
17506
+ * translates it when `lc_messages` is not English.
17507
+ */
17508
+ function isMissingPartitionError(error) {
17509
+ const { code, routine, message = "" } = error ?? {};
17510
+ return code === "23514" && (routine === "ExecFindPartition" || /no partition of relation/i.test(message));
17511
+ }
17512
+ /**
17513
+ * Calls `create_hypertable()` for each signal table.
17514
+ *
17515
+ * Postgres declarative partitioning and Timescale hypertables are mutually
17516
+ * exclusive, so the base DDL must have been generated with TableDDLMode
17517
+ * 'timescale' (no `PARTITION BY` clause). `if_not_exists` makes this
17518
+ * idempotent across re-inits.
17519
+ */
17520
+ async function ensureTimescaleHypertables(client, schema) {
17521
+ for (const table of ALL_SIGNAL_TABLES) {
17522
+ const tableExpr = qualifiedTable(schema, table);
17523
+ const timeColumn = SIGNAL_TIME_COLUMN[table].replace(/'/g, "''");
17524
+ await client.none(`SELECT create_hypertable($1::regclass, '${timeColumn}', chunk_time_interval => INTERVAL '1 day', if_not_exists => true)`, [tableExpr]);
17525
+ }
17526
+ }
17527
+ /**
17528
+ * Registers each signal table with pg_partman as a daily-interval parent and
17529
+ * lets partman create the child partitions.
17530
+ *
17531
+ * Retention is intentionally NOT configured here — it stays opt-in via the
17532
+ * future Mastra CLI surface.
17533
+ *
17534
+ * Prefers the pg_partman 5.x+ declarative API (`p_type := 'range'`) and keeps
17535
+ * a 4.x fallback (`p_type := 'native'`) for older installs. That lets the
17536
+ * adapter run against current arm64-friendly images without breaking existing
17537
+ * 4.x deployments.
17538
+ */
17539
+ async function partmanParentRegistered(client, schema, table) {
17540
+ const row = await client.oneOrNone(`SELECT EXISTS (
17541
+ SELECT 1 FROM partman.part_config
17542
+ WHERE parent_table = format('%I.%I', $1::text, $2::text)
17543
+ ) AS "exists"`, [schema, table]);
17544
+ return Boolean(row?.exists);
17545
+ }
17546
+ async function ensurePartmanHypertables(client, schema) {
17547
+ const partmanMajor = getPartmanMajor(await getPartmanVersion(client));
17548
+ for (const table of ALL_SIGNAL_TABLES) {
17549
+ if (await partmanParentRegistered(client, schema, table)) continue;
17550
+ for (let attempt = 0; attempt < 3; attempt++) try {
17551
+ if (partmanMajor != null && partmanMajor >= 5) await client.none(`SELECT partman.create_parent(
17552
+ p_parent_table := format('%I.%I', $1::text, $2::text),
17553
+ p_control := $3,
17554
+ p_interval := '1 day',
17555
+ p_type := 'range'
17556
+ )`, [
17557
+ schema,
17558
+ table,
17559
+ SIGNAL_TIME_COLUMN[table]
17560
+ ]);
17561
+ else await client.none(`SELECT partman.create_parent(
17562
+ p_parent_table := format('%I.%I', $1::text, $2::text),
17563
+ p_control := $3,
17564
+ p_type := 'native',
17565
+ p_interval := '1 day'
17566
+ )`, [
17567
+ schema,
17568
+ table,
17569
+ SIGNAL_TIME_COLUMN[table]
17570
+ ]);
17571
+ break;
17572
+ } catch (error) {
17573
+ const message = error?.message ?? "";
17574
+ if (error?.code === "23505" || /already managed by pg_partman/i.test(message) || /relation .* already exists/i.test(message)) break;
17575
+ if (!/deadlock detected/i.test(message)) throw error;
17576
+ if (await partmanParentRegistered(client, schema, table)) break;
17577
+ if (attempt === 2) throw error;
17578
+ await new Promise((resolve) => setTimeout(resolve, 25));
17579
+ }
17580
+ }
17581
+ }
17582
+ async function setupPartitioning(client, schema, options = {}) {
17583
+ switch (await resolveMode(client, options)) {
17584
+ case "timescale":
17585
+ await ensureTimescaleHypertables(client, schema);
17586
+ return "timescale";
17587
+ case "partman":
17588
+ await ensurePartmanHypertables(client, schema);
17589
+ return "partman";
17590
+ case "native":
17591
+ await ensureNativePartitions(client, schema, options);
17592
+ return "native";
17593
+ }
17594
+ }
17302
17595
  // src/storage/domains/observability/v-next/sql.ts
17303
17596
  /**
17304
17597
  * SQL helpers for the v-next Postgres observability domain.
@@ -17379,6 +17672,54 @@ ${onConflict}`,
17379
17672
  values
17380
17673
  };
17381
17674
  }
17675
+ /**
17676
+ * Insert into a signal table. In native partition mode, a row whose day is
17677
+ * inside the partition window but has no partition yet gets one created, then
17678
+ * the insert runs once more. TimescaleDB and pg_partman own their partitions,
17679
+ * so in those modes the original error propagates untouched. The mode is the
17680
+ * one the store resolved; a store that never ran init() (`disableInit`)
17681
+ * detects it here, after the failed insert, so the hot path stays one query.
17682
+ */
17683
+ async function insertRows(client, schema, table, records, partitioning, onConflict) {
17684
+ const insert = buildInsert(schema, table, records, onConflict);
17685
+ if (!insert) return;
17686
+ try {
17687
+ await client.query(insert.text, insert.values);
17688
+ } catch (error) {
17689
+ if (!isMissingPartitionError(error) || await resolveMode(client, partitioning) !== "native") throw error;
17690
+ const days = withinPartitionWindow(recordDays(records, SIGNAL_TIME_COLUMN[table]), partitioning);
17691
+ if (days.length === 0) throw error;
17692
+ for (const day of days) await ensureDayPartitions(client, schema, table, [day]).catch((cause) => {
17693
+ if (!isInsufficientPrivilegeError(cause)) throw cause;
17694
+ throw new Error(partitionCreateFailure({
17695
+ table,
17696
+ day,
17697
+ cause
17698
+ }), { cause });
17699
+ });
17700
+ await client.query(insert.text, insert.values);
17701
+ }
17702
+ }
17703
+ /** `42501`: the role may not create the child, or does not own the parent it must attach to. */
17704
+ function isInsufficientPrivilegeError(error) {
17705
+ return error?.code === "42501";
17706
+ }
17707
+ function partitionCreateFailure({ table, day, cause }) {
17708
+ const reason = cause instanceof Error ? cause.message : String(cause);
17709
+ 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.`;
17710
+ }
17711
+ function recordDays(records, timeColumn) {
17712
+ const days = /* @__PURE__ */ new Map();
17713
+ for (const record of records) {
17714
+ const value = record[timeColumn];
17715
+ if (value == null) continue;
17716
+ const d = new Date(value);
17717
+ if (isNaN(d.getTime())) continue;
17718
+ const day = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()));
17719
+ days.set(day.toISOString(), day);
17720
+ }
17721
+ return [...days.values()];
17722
+ }
17382
17723
  /** Primary key of the span table; also the conflict target for span upserts. */
17383
17724
  const SPAN_KEY_COLUMNS = [
17384
17725
  "traceId",
@@ -18289,16 +18630,14 @@ function pushFeedbackIdentity(acc, feedbackType, feedbackSource) {
18289
18630
  }
18290
18631
  acc.conditions.push(`"valueNumber" IS NOT NULL`);
18291
18632
  }
18292
- async function createFeedback(client, schema, args) {
18633
+ async function createFeedback(client, schema, args, partitioning) {
18293
18634
  const row = feedbackRecordToRow(args.feedback);
18294
- const insert = buildInsert(schema, TABLE_FEEDBACK_EVENTS, [row], FEEDBACK_UPSERT_CLAUSE);
18295
- if (insert) await client.query(insert.text, insert.values);
18635
+ await insertRows(client, schema, TABLE_FEEDBACK_EVENTS, [row], partitioning, FEEDBACK_UPSERT_CLAUSE);
18296
18636
  }
18297
- async function batchCreateFeedback(client, schema, args) {
18637
+ async function batchCreateFeedback(client, schema, args, partitioning) {
18298
18638
  if (args.feedbacks.length === 0) return;
18299
18639
  const rows = [...new Map(args.feedbacks.map((feedback) => [feedback.feedbackId, feedback])).values()].map(feedbackRecordToRow);
18300
- const insert = buildInsert(schema, TABLE_FEEDBACK_EVENTS, rows, FEEDBACK_UPSERT_CLAUSE);
18301
- if (insert) await client.query(insert.text, insert.values);
18640
+ await insertRows(client, schema, TABLE_FEEDBACK_EVENTS, rows, partitioning, FEEDBACK_UPSERT_CLAUSE);
18302
18641
  }
18303
18642
  async function updateFeedbackReviewStatus(client, schema, args) {
18304
18643
  const { feedbackId, reviewStatus } = parseUpdateFeedbackReviewStatusArgs(args);
@@ -18496,11 +18835,10 @@ function applyLogFilters(acc, filters) {
18496
18835
  applyCommonFilters(acc, filters);
18497
18836
  applySingleOrArrayFilter(acc, "level", filters?.level);
18498
18837
  }
18499
- async function batchCreateLogs(client, schema, args) {
18838
+ async function batchCreateLogs(client, schema, args, partitioning) {
18500
18839
  if (args.logs.length === 0) return;
18501
18840
  const rows = args.logs.map(logRecordToRow);
18502
- const insert = buildInsert(schema, TABLE_LOG_EVENTS, rows);
18503
- if (insert) await client.query(insert.text, insert.values);
18841
+ await insertRows(client, schema, TABLE_LOG_EVENTS, rows, partitioning);
18504
18842
  }
18505
18843
  async function listLogs(client, schema, args) {
18506
18844
  const { mode, filters, pagination, orderBy, after, limit } = listLogsArgsSchema.parse(args);
@@ -18571,11 +18909,10 @@ function pushMetricNameFilter(acc, names) {
18571
18909
  acc.conditions.push(`"name" IN (${placeholders})`);
18572
18910
  acc.params.push(...names);
18573
18911
  }
18574
- async function batchCreateMetrics(client, schema, args) {
18912
+ async function batchCreateMetrics(client, schema, args, partitioning) {
18575
18913
  if (args.metrics.length === 0) return;
18576
18914
  const rows = args.metrics.map(metricRecordToRow);
18577
- const insert = buildInsert(schema, TABLE_METRIC_EVENTS, rows);
18578
- if (insert) await client.query(insert.text, insert.values);
18915
+ await insertRows(client, schema, TABLE_METRIC_EVENTS, rows, partitioning);
18579
18916
  }
18580
18917
  async function listMetrics(client, schema, args) {
18581
18918
  const { mode, filters, pagination, orderBy, after, limit } = listMetricsArgsSchema.parse(args);
@@ -18777,165 +19114,6 @@ async function getMetricPercentiles(client, schema, args) {
18777
19114
  `;
18778
19115
  return { series: percentileSeriesFromRows(await client.manyOrNone(sql, acc.params), args.percentiles) };
18779
19116
  }
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
19117
  // src/storage/domains/observability/v-next/retention.ts
18940
19118
  /**
18941
19119
  * Age-based retention for the v-next Postgres observability domain.
@@ -19101,16 +19279,14 @@ function collapseExactScoreConflicts(rows) {
19101
19279
  }
19102
19280
  return [...records.values()];
19103
19281
  }
19104
- async function createScore(client, schema, args) {
19282
+ async function createScore(client, schema, args, partitioning) {
19105
19283
  const row = scoreRecordToRow(args.score);
19106
- const insert = buildInsert(schema, TABLE_SCORE_EVENTS, [row], scoreRewriteConflict(row));
19107
- if (insert) await client.query(insert.text, insert.values);
19284
+ await insertRows(client, schema, TABLE_SCORE_EVENTS, [row], partitioning, scoreRewriteConflict(row));
19108
19285
  }
19109
- async function batchCreateScores(client, schema, args) {
19286
+ async function batchCreateScores(client, schema, args, partitioning) {
19110
19287
  if (args.scores.length === 0) return;
19111
19288
  const rows = collapseExactScoreConflicts(args.scores.map(scoreRecordToRow));
19112
- const insert = buildInsert(schema, TABLE_SCORE_EVENTS, rows, scoreRewriteConflict(rows[0]));
19113
- if (insert) await client.query(insert.text, insert.values);
19289
+ await insertRows(client, schema, TABLE_SCORE_EVENTS, rows, partitioning, scoreRewriteConflict(rows[0]));
19114
19290
  }
19115
19291
  /**
19116
19292
  * Delete score events by scoreId. Optional `organizationId` and `resourceId`
@@ -19341,7 +19517,15 @@ const TRACE_STATUS_SQL = `CASE WHEN r."error" IS NOT NULL THEN 'error' ELSE 'suc
19341
19517
  const timeToFirstChunkSupported = "timeToFirstChunkMs" in coreStorage.TRACE_QUERY_FIELD_REGISTRY.trace;
19342
19518
  /** Core before the sort rejects `orderBy` on the field, so the sort is only declared when core plans it. */
19343
19519
  const timeToFirstChunkSortSupported = coreStorage.TRACE_QUERY_ORDER_FIELDS?.includes("timeToFirstChunkMs") === true;
19344
- const timeToFirstChunkFeatures = [...timeToFirstChunkSupported ? ["trace-query-time-to-first-chunk"] : [], ...timeToFirstChunkSortSupported ? ["trace-query-time-to-first-chunk-sort"] : []];
19520
+ /** Same rule for time to first token: returned and sorted only when core knows the field. */
19521
+ const timeToFirstTokenSupported = "timeToFirstTokenMs" in coreStorage.TRACE_QUERY_FIELD_REGISTRY.trace;
19522
+ const timeToFirstTokenSortSupported = coreStorage.TRACE_QUERY_ORDER_FIELDS?.includes("timeToFirstTokenMs") === true;
19523
+ const timingFeatures = [
19524
+ ...timeToFirstChunkSupported ? ["trace-query-time-to-first-chunk"] : [],
19525
+ ...timeToFirstChunkSortSupported ? ["trace-query-time-to-first-chunk-sort"] : [],
19526
+ ...timeToFirstTokenSupported ? ["trace-query-time-to-first-token"] : [],
19527
+ ...timeToFirstTokenSortSupported ? ["trace-query-time-to-first-token-sort"] : []
19528
+ ];
19345
19529
  /**
19346
19530
  * Keyset bounds for a cursor's sort value: terms for rows strictly past it in `direction`, and the
19347
19531
  * condition for rows level with it. Empty values sort last in both directions, so they are past
@@ -19374,6 +19558,7 @@ const TRACE_FIELDS = {
19374
19558
  endedAt: "r.\"endedAt\"",
19375
19559
  durationMs: durationMsSql("r.\"startedAt\"", "r.\"endedAt\""),
19376
19560
  timeToFirstChunkMs: "r.\"timeToFirstChunkMs\"",
19561
+ timeToFirstTokenMs: "r.\"timeToFirstTokenMs\"",
19377
19562
  entityId: "r.\"entityId\"",
19378
19563
  entityName: "r.\"entityName\"",
19379
19564
  entityType: "r.\"entityType\"",
@@ -19392,6 +19577,7 @@ const SPAN_FIELDS = {
19392
19577
  endedAt: "s.\"endedAt\"",
19393
19578
  durationMs: "s.\"durationMs\"",
19394
19579
  timeToFirstChunkMs: "s.\"timeToFirstChunkMs\"",
19580
+ timeToFirstTokenMs: "s.\"timeToFirstTokenMs\"",
19395
19581
  status: "s.\"status\"",
19396
19582
  error: "s.\"error\"",
19397
19583
  entityType: "s.\"entityType\"",
@@ -19462,6 +19648,7 @@ const TRACE_SELECT = `
19462
19648
  r."startedAt" AS "startedAt",
19463
19649
  r."endedAt" AS "endedAt",
19464
19650
  r."timeToFirstChunkMs" AS "timeToFirstChunkMs",
19651
+ r."timeToFirstTokenMs" AS "timeToFirstTokenMs",
19465
19652
  r."entityName" AS "entityName",
19466
19653
  r."entityType" AS "entityType",
19467
19654
  r."environment" AS "environment",
@@ -19831,6 +20018,7 @@ function compilePostgresTraceScope(schema, selection, relationCollections, scope
19831
20018
  ELSE ${durationMsSql("s.\"startedAt\"", "s.\"endedAt\"")}
19832
20019
  END AS "durationMs",
19833
20020
  s."timeToFirstChunkMs",
20021
+ s."timeToFirstTokenMs",
19834
20022
  CASE WHEN s."error" IS NOT NULL THEN 'error' ELSE 'success' END AS "status",
19835
20023
  s."error",
19836
20024
  s."entityType",
@@ -20205,6 +20393,7 @@ function traceRowToResult(row) {
20205
20393
  startedAt: asIsoTimestamp$1(row.startedAt),
20206
20394
  endedAt: asIsoTimestamp$1(row.endedAt),
20207
20395
  ...timeToFirstChunkSupported && { timeToFirstChunkMs: row.timeToFirstChunkMs == null ? null : Number(row.timeToFirstChunkMs) },
20396
+ ...timeToFirstTokenSupported && { timeToFirstTokenMs: row.timeToFirstTokenMs == null ? null : Number(row.timeToFirstTokenMs) },
20208
20397
  entityName: row.entityName == null ? null : String(row.entityName),
20209
20398
  entityType: row.entityType == null ? null : String(row.entityType),
20210
20399
  environment: row.environment == null ? null : String(row.environment),
@@ -20360,7 +20549,8 @@ const fields = [
20360
20549
  "experimentId",
20361
20550
  "environment",
20362
20551
  "executionSource",
20363
- "timeToFirstChunkMs"
20552
+ "timeToFirstChunkMs",
20553
+ "timeToFirstTokenMs"
20364
20554
  ];
20365
20555
  const identities = identityFields.map((field) => `"${field}"`).join(", ");
20366
20556
  const identityOrder = identityFields.map((field) => `"${field}" COLLATE "C" ASC NULLS LAST`).join(", ");
@@ -20492,9 +20682,10 @@ async function querySpans(client, schema, plan, timeoutMs) {
20492
20682
  return runWithPostgresTraceQueryTimeout(client, timeoutMs, async (tx) => {
20493
20683
  const query = compilePostgresSpanQuery(schema, plan);
20494
20684
  const rows = await tx.any(query.text, query.values);
20495
- const selected = rows.map(({ version: _version, startedAt, endedAt, timeToFirstChunkMs, ...row }) => ({
20685
+ const selected = rows.map(({ version: _version, startedAt, endedAt, timeToFirstChunkMs, timeToFirstTokenMs, ...row }) => ({
20496
20686
  ...row,
20497
20687
  ...timeToFirstChunkSupported && { timeToFirstChunkMs },
20688
+ ...timeToFirstTokenSupported && { timeToFirstTokenMs },
20498
20689
  startedAt: startedAt.toISOString(),
20499
20690
  endedAt: endedAt.toISOString()
20500
20691
  }));
@@ -20626,7 +20817,11 @@ function compilePostgresThreadRowsQuery(schema, plan, strategy = "full") {
20626
20817
  }
20627
20818
  const qualify = plan.timeRange.mode === "traces" ? "s.\"inWindow\"" : `s."${plan.timeRange.field}" >= $1 AND s."${plan.timeRange.field}" < $2`;
20628
20819
  const filtered = predicateFields(plan.where);
20629
- const firstContext = [...contextFields, "timeToFirstChunkMs"].filter((field) => field === "resourceId" || filtered.has(field));
20820
+ const firstContext = [
20821
+ ...contextFields,
20822
+ "timeToFirstChunkMs",
20823
+ "timeToFirstTokenMs"
20824
+ ].filter((field) => field === "resourceId" || filtered.has(field));
20630
20825
  const lastFields = [
20631
20826
  "traceId",
20632
20827
  "spanId",
@@ -20640,7 +20835,7 @@ function compilePostgresThreadRowsQuery(schema, plan, strategy = "full") {
20640
20835
  text: `WITH ${scopedRoots}, ${strategy === "candidates" ? `${candidates}, ` : ""}current_roots AS (
20641
20836
  SELECT DISTINCT ON ("organizationId", "traceId")
20642
20837
  "organizationId", "threadId", "traceId", "spanId", "cursorId", "startedAt", "endedAt", "isPending",
20643
- ${firstContext.map((field) => field === "timeToFirstChunkMs" ? `CASE WHEN "spanType" = 'agent_run' THEN "timeToFirstChunkMs" END AS "timeToFirstChunkMs"` : `"${field}"`).join(", ")}
20838
+ ${firstContext.map((field) => field === "timeToFirstChunkMs" || field === "timeToFirstTokenMs" ? `CASE WHEN "spanType" = 'agent_run' THEN "${field}" END AS "${field}"` : `"${field}"`).join(", ")}
20644
20839
  ${filtered.has("status") ? ", CASE WHEN error IS NULL THEN 'success' ELSE 'error' END AS status" : ""}
20645
20840
  FROM scoped_roots ${strategy === "candidates" ? `WHERE (${identity("traceId")}) IN (SELECT ${identity("traceId")} FROM candidate_traces)` : ""}
20646
20841
  ORDER BY "organizationId", "traceId", "startedAt" ASC, "spanId" COLLATE "C", "cursorId" DESC
@@ -20706,6 +20901,7 @@ async function queryThreadRows(client, schema, plan, timeoutMs, strategy = "auto
20706
20901
  }));
20707
20902
  const payloads = await run(`SELECT "organizationId", "traceId", "cursorId", ${contextFields.map((field) => `"${field}"`).join(", ")},
20708
20903
  CASE WHEN "spanType" = 'agent_run' THEN "timeToFirstChunkMs" END AS "timeToFirstChunkMs",
20904
+ CASE WHEN "spanType" = 'agent_run' THEN "timeToFirstTokenMs" END AS "timeToFirstTokenMs",
20709
20905
  CASE WHEN error IS NULL THEN 'success' ELSE 'error' END AS status,
20710
20906
  ${[
20711
20907
  "input",
@@ -20734,6 +20930,7 @@ async function queryThreadRows(client, schema, plan, timeoutMs, strategy = "auto
20734
20930
  endedAt: row.endedAt.toISOString(),
20735
20931
  traceCount: Number(row.traceCount),
20736
20932
  timeToFirstChunkMs: first.timeToFirstChunkMs,
20933
+ ...timeToFirstTokenSupported && { timeToFirstTokenMs: first.timeToFirstTokenMs },
20737
20934
  inputPreview: readPreview(first.inputPreview, first.input),
20738
20935
  outputPreview: readPreview(last.outputPreview, last.output),
20739
20936
  errorPreview: readPreview(last.errorPreview, last.error)
@@ -21690,10 +21887,9 @@ async function readBranchesStreamHeadCursor(client, span, filters) {
21690
21887
  return encodeDeltaCursor(await readSafeXactHorizon(client), 0);
21691
21888
  }
21692
21889
  // src/storage/domains/observability/v-next/tracing.ts
21693
- async function writeSpanRows(client, schema, rows) {
21890
+ async function writeSpanRows(client, schema, rows, partitioning) {
21694
21891
  const deduped = dedupeSpanRows(rows);
21695
- const insert = buildInsert(schema, TABLE_SPAN_EVENTS, deduped, spanConflictClause(Object.keys(deduped[0] ?? {})));
21696
- if (insert) await client.query(insert.text, insert.values);
21892
+ await insertRows(client, schema, TABLE_SPAN_EVENTS, deduped, partitioning, spanConflictClause(Object.keys(deduped[0] ?? {})));
21697
21893
  }
21698
21894
  /**
21699
21895
  * Collapse rows that share a primary key within a single batch. Postgres
@@ -21713,12 +21909,12 @@ function dedupeSpanRows(rows) {
21713
21909
  }
21714
21910
  return [...byKey.values()];
21715
21911
  }
21716
- async function createSpan(client, schema, args) {
21717
- await writeSpanRows(client, schema, [spanRecordToRow(args.span)]);
21912
+ async function createSpan(client, schema, args, partitioning) {
21913
+ await writeSpanRows(client, schema, [spanRecordToRow(args.span)], partitioning);
21718
21914
  }
21719
- async function batchCreateSpans(client, schema, args) {
21915
+ async function batchCreateSpans(client, schema, args, partitioning) {
21720
21916
  if (args.records.length === 0) return;
21721
- await writeSpanRows(client, schema, args.records.map(spanRecordToRow));
21917
+ await writeSpanRows(client, schema, args.records.map(spanRecordToRow), partitioning);
21722
21918
  }
21723
21919
  async function getSpans(client, schema, args) {
21724
21920
  if (args.spanIds.length === 0) return {
@@ -21939,6 +22135,18 @@ var ObservabilityStoragePostgresVNext = class ObservabilityStoragePostgresVNext
21939
22135
  return this.#partitionMode;
21940
22136
  }
21941
22137
  /**
22138
+ * Create any missing day partitions from yesterday through today +
22139
+ * `futureDays`, without the rest of init(). For apps whose runtime role
22140
+ * cannot run DDL: call it on a schedule with a role that owns the
22141
+ * observability tables. Safe to call repeatedly on one instance. Does
22142
+ * nothing with TimescaleDB or pg_partman, which create their own partitions.
22143
+ */
22144
+ async ensurePartitions() {
22145
+ return this.#run("VNEXT_ENSURE_PARTITIONS", async () => {
22146
+ if ((this.#partitionMode ??= await resolveMode(this.#client, this.#partitioning)) === "native") await ensureNativePartitions(this.#client, this.#schema, this.#partitioning);
22147
+ });
22148
+ }
22149
+ /**
21942
22150
  * All five signal tables are insert-only growth tables. The anchor column is
21943
22151
  * each table's partition / chunk key, so age-based expiry drops whole day
21944
22152
  * partitions instead of deleting rows (see `./retention.ts`). `indexed: true`
@@ -22042,7 +22250,7 @@ var ObservabilityStoragePostgresVNext = class ObservabilityStoragePostgresVNext
22042
22250
  "trace-query-tenant-scope",
22043
22251
  "feedback",
22044
22252
  "trace-query-context-ids",
22045
- ...timeToFirstChunkFeatures,
22253
+ ...timingFeatures,
22046
22254
  "trace-query-root-span-fields",
22047
22255
  "trace-query-root-entity-id"
22048
22256
  ];
@@ -22067,11 +22275,23 @@ var ObservabilityStoragePostgresVNext = class ObservabilityStoragePostgresVNext
22067
22275
  "trace-query-tenant-scope",
22068
22276
  "feedback",
22069
22277
  "trace-query-context-ids",
22070
- ...timeToFirstChunkFeatures,
22278
+ ...timingFeatures,
22071
22279
  "trace-query-root-span-fields",
22072
22280
  "trace-query-root-entity-id"
22073
22281
  ];
22074
22282
  }
22283
+ /**
22284
+ * Partitioning options for the write path, with `mode` fixed to the mode
22285
+ * this store resolved (in init(), or lazily in prune()) so the write path
22286
+ * does not detect it again. Until then the configured mode stands in and
22287
+ * the write path resolves it itself when a partition is missing.
22288
+ */
22289
+ #writePartitioning() {
22290
+ return {
22291
+ ...this.#partitioning,
22292
+ mode: this.#partitionMode ?? this.#partitioning.mode
22293
+ };
22294
+ }
22075
22295
  async #run(op, fn, details) {
22076
22296
  try {
22077
22297
  return await fn();
@@ -22080,13 +22300,13 @@ var ObservabilityStoragePostgresVNext = class ObservabilityStoragePostgresVNext
22080
22300
  }
22081
22301
  }
22082
22302
  async createSpan(args) {
22083
- await this.#run("CREATE_SPAN", () => createSpan(this.#client, this.#schema, args), {
22303
+ await this.#run("CREATE_SPAN", () => createSpan(this.#client, this.#schema, args, this.#writePartitioning()), {
22084
22304
  traceId: args.span.traceId,
22085
22305
  spanId: args.span.spanId
22086
22306
  });
22087
22307
  }
22088
22308
  async batchCreateSpans(args) {
22089
- await this.#run("BATCH_CREATE_SPANS", () => batchCreateSpans(this.#client, this.#schema, args), { count: args.records.length });
22309
+ await this.#run("BATCH_CREATE_SPANS", () => batchCreateSpans(this.#client, this.#schema, args, this.#writePartitioning()), { count: args.records.length });
22090
22310
  }
22091
22311
  async getSpan(args) {
22092
22312
  return this.#run("GET_SPAN", () => getSpan(this.#readClient, this.#schema, args), {
@@ -22140,22 +22360,22 @@ var ObservabilityStoragePostgresVNext = class ObservabilityStoragePostgresVNext
22140
22360
  return this.#run("LIST_BRANCHES", () => listBranches(this.#readClient, this.#schema, args));
22141
22361
  }
22142
22362
  async batchCreateLogs(args) {
22143
- await this.#run("BATCH_CREATE_LOGS", () => batchCreateLogs(this.#client, this.#schema, args), { count: args.logs.length });
22363
+ await this.#run("BATCH_CREATE_LOGS", () => batchCreateLogs(this.#client, this.#schema, args, this.#writePartitioning()), { count: args.logs.length });
22144
22364
  }
22145
22365
  async batchCreateMetrics(args) {
22146
- await this.#run("BATCH_CREATE_METRICS", () => batchCreateMetrics(this.#client, this.#schema, args), { count: args.metrics.length });
22366
+ await this.#run("BATCH_CREATE_METRICS", () => batchCreateMetrics(this.#client, this.#schema, args, this.#writePartitioning()), { count: args.metrics.length });
22147
22367
  }
22148
22368
  async createScore(args) {
22149
- await this.#run("CREATE_SCORE", () => createScore(this.#client, this.#schema, args));
22369
+ await this.#run("CREATE_SCORE", () => createScore(this.#client, this.#schema, args, this.#writePartitioning()));
22150
22370
  }
22151
22371
  async batchCreateScores(args) {
22152
- await this.#run("BATCH_CREATE_SCORES", () => batchCreateScores(this.#client, this.#schema, args), { count: args.scores.length });
22372
+ await this.#run("BATCH_CREATE_SCORES", () => batchCreateScores(this.#client, this.#schema, args, this.#writePartitioning()), { count: args.scores.length });
22153
22373
  }
22154
22374
  async createFeedback(args) {
22155
- await this.#run("CREATE_FEEDBACK", () => createFeedback(this.#client, this.#schema, args));
22375
+ await this.#run("CREATE_FEEDBACK", () => createFeedback(this.#client, this.#schema, args, this.#writePartitioning()));
22156
22376
  }
22157
22377
  async batchCreateFeedback(args) {
22158
- await this.#run("BATCH_CREATE_FEEDBACK", () => batchCreateFeedback(this.#client, this.#schema, args), { count: args.feedbacks.length });
22378
+ await this.#run("BATCH_CREATE_FEEDBACK", () => batchCreateFeedback(this.#client, this.#schema, args, this.#writePartitioning()), { count: args.feedbacks.length });
22159
22379
  }
22160
22380
  async deleteScores(args) {
22161
22381
  await this.#run("DELETE_SCORES", () => deleteScores(this.#client, this.#schema, args), { count: args.scoreIds.length });
@@ -27565,12 +27785,20 @@ var PostgresStore = class extends MastraCompositeStore {
27565
27785
  }
27566
27786
  async #runPinnedInit() {
27567
27787
  let pinnedClient;
27788
+ let pinned;
27568
27789
  try {
27569
27790
  pinnedClient = await this.#writePool.connect();
27570
- const pinned = new PinnedClientAdapter(this.#writePool, pinnedClient);
27791
+ pinned = new PinnedClientAdapter(this.#writePool, pinnedClient);
27571
27792
  this.#db.pin(pinned);
27572
- this.#db.setSchemaSnapshot(await loadSchemaSnapshot(pinned, this.schema));
27573
- await Promise.all(Object.entries(this.stores).filter(([name, store]) => name !== "knowledge" && store).map(([, store]) => store.init()));
27793
+ for (let attempt = 1; attempt <= 3; attempt++) {
27794
+ this.#db.setSchemaSnapshot(await loadSchemaSnapshot(pinned, this.schema));
27795
+ 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");
27796
+ if (failures.length === 0) break;
27797
+ const terminal = failures.find((result) => !isRetryableDeadlock(result.reason));
27798
+ if (terminal || attempt === 3) throw (terminal ?? failures[0]).reason;
27799
+ this.#db.setSchemaSnapshot(null);
27800
+ await backoffAfterDeadlock(attempt);
27801
+ }
27574
27802
  this.isInitialized = true;
27575
27803
  } catch (error) {
27576
27804
  this.#initPromise = null;
@@ -27581,6 +27809,7 @@ var PostgresStore = class extends MastraCompositeStore {
27581
27809
  category: ErrorCategory.THIRD_PARTY
27582
27810
  }, error);
27583
27811
  } finally {
27812
+ await pinned?.drain();
27584
27813
  this.#db.setSchemaSnapshot(null);
27585
27814
  if (pinnedClient) {
27586
27815
  this.#db.unpin();
@@ -27735,6 +27964,10 @@ var PostgresStoreVNext = class extends PostgresStore {
27735
27964
  owned: true
27736
27965
  };
27737
27966
  }
27967
+ /** See {@link ObservabilityStoragePostgresVNext.ensurePartitions}. */
27968
+ async ensurePartitions() {
27969
+ await this.stores.observability.ensurePartitions();
27970
+ }
27738
27971
  /**
27739
27972
  * Closes both the primary pool (when owned) and the observability pool
27740
27973
  * (when this store created it). Safe to call multiple times.