@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.cjs CHANGED
@@ -34,6 +34,7 @@ let pg_connection_string = require("pg-connection-string");
34
34
  let _mastra_core_vector_filter = require("@mastra/core/vector/filter");
35
35
  let _mastra_core_base = require("@mastra/core/base");
36
36
  let crypto$1 = require("crypto");
37
+ let timers_promises = require("timers/promises");
37
38
  let _mastra_core_features = require("@mastra/core/features");
38
39
  let _mastra_core_agent = require("@mastra/core/agent");
39
40
  let _mastra_core_evals = require("@mastra/core/evals");
@@ -2276,6 +2277,9 @@ var PinnedClientAdapter = class {
2276
2277
  this.#tail = next.catch(() => void 0);
2277
2278
  return next;
2278
2279
  }
2280
+ async drain() {
2281
+ await this.#tail;
2282
+ }
2279
2283
  connect() {
2280
2284
  throw new Error("PinnedClientAdapter.connect() is not supported during PostgresStore.init(). All DDL must flow through the pinned client.");
2281
2285
  }
@@ -2458,6 +2462,39 @@ function buildConstraintName({ baseName, schemaName, maxLength = 63, hashWhenTru
2458
2462
  return truncateIdentifier(fullName, maxLength);
2459
2463
  }
2460
2464
  // src/storage/db/pg-errors.ts
2465
+ /**
2466
+ * Postgres error classifiers for concurrent DDL.
2467
+ *
2468
+ * `CREATE SCHEMA / TABLE / INDEX IF NOT EXISTS` checks aren't atomic against
2469
+ * concurrent backends. Two callers racing past the existence probe can both
2470
+ * reach the catalog insert; the loser sees a duplicate-object error. These
2471
+ * helpers identify the exact codes we treat as "already exists by the time we
2472
+ * look" so every other error surfaces normally.
2473
+ */
2474
+ var IndexBuildError = class extends Error {
2475
+ constructor(indexName, schemaName, state, cause) {
2476
+ super(`Could not create index ${schemaName}.${indexName}: ${state}`, { cause });
2477
+ this.name = "IndexBuildError";
2478
+ }
2479
+ };
2480
+ function* causes(error) {
2481
+ const seen = /* @__PURE__ */ new Set();
2482
+ while (typeof error === "object" && error !== null && !seen.has(error)) {
2483
+ seen.add(error);
2484
+ yield error;
2485
+ error = "cause" in error ? error.cause : void 0;
2486
+ }
2487
+ }
2488
+ /** Index recovery owns its retry budget, even through an outer MastraError. */
2489
+ function isRetryableDeadlock(error) {
2490
+ for (const cause of causes(error)) if (cause instanceof IndexBuildError) return false;
2491
+ for (const cause of causes(error)) if ("code" in cause && cause.code === "40P01") return true;
2492
+ return false;
2493
+ }
2494
+ async function backoffAfterDeadlock(attempt) {
2495
+ const base = 50 * 2 ** (attempt - 1);
2496
+ await new Promise((resolve) => setTimeout(resolve, base + Math.random() * base * 2));
2497
+ }
2461
2498
  function asPgError(error) {
2462
2499
  return error ?? {};
2463
2500
  }
@@ -2485,6 +2522,99 @@ function isDuplicateSchemaError(error) {
2485
2522
  if (code === "23505" && constraint === "pg_namespace_nspname_index") return true;
2486
2523
  return /schema .* already exists/i.test(message);
2487
2524
  }
2525
+ // src/storage/db/index-build.ts
2526
+ /** A cold snapshot cannot arbitrate DDL submitted by independent processes. */
2527
+ async function buildIndex(client, schemaName, indexName, build, logger) {
2528
+ const deadline = performance.now() + 3e4;
2529
+ let attempts = 0;
2530
+ let failure;
2531
+ let state = "not built";
2532
+ let repairFailures = 0;
2533
+ const observe = (db = client) => db.oneOrNone(`SELECT c.oid, i.indrelid AS table_oid, t.relname AS table_name,
2534
+ tn.nspname AS table_schema, i.indisvalid AS valid
2535
+ FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
2536
+ LEFT JOIN pg_index i ON i.indexrelid = c.oid
2537
+ LEFT JOIN pg_class t ON t.oid = i.indrelid
2538
+ LEFT JOIN pg_namespace tn ON tn.oid = t.relnamespace
2539
+ WHERE n.nspname = $1 AND c.relname = $2`, [schemaName, indexName]);
2540
+ while (true) {
2541
+ const relation = await observe();
2542
+ if (relation?.valid) return true;
2543
+ if (relation && relation.valid === null) throw new IndexBuildError(indexName, schemaName, "name belongs to a non-index relation", failure);
2544
+ if (relation) {
2545
+ if (await client.oneOrNone(`SELECT p.pid FROM pg_stat_progress_create_index p
2546
+ WHERE p.datid = (SELECT oid FROM pg_database WHERE datname = current_database())
2547
+ AND p.pid <> pg_backend_pid()
2548
+ AND (p.index_relid = $1 OR ((p.index_relid IS NULL OR p.index_relid = 0) AND EXISTS (
2549
+ SELECT 1 FROM pg_locks l WHERE l.pid = p.pid AND l.database = p.datid
2550
+ AND l.locktype = 'relation' AND l.relation = $2 AND l.granted
2551
+ AND l.mode IN ('ShareUpdateExclusiveLock', 'ShareLock')
2552
+ )))
2553
+ LIMIT 1`, [relation.oid, relation.table_oid])) {
2554
+ logger.warn(`Index ${indexName} is being built by another session; skipping`);
2555
+ return false;
2556
+ }
2557
+ if (performance.now() >= deadline && state === "table busy during invalid-index repair") {
2558
+ logger.warn(`Index ${indexName} could not be repaired because its table is busy; skipping`);
2559
+ return false;
2560
+ }
2561
+ if (attempts >= 3 || performance.now() >= deadline) break;
2562
+ state = "abandoned invalid index";
2563
+ attempts++;
2564
+ let lockUnavailable = false;
2565
+ try {
2566
+ const result = await client.tx(async (tx) => {
2567
+ const table = `${(0, pg.escapeIdentifier)(relation.table_schema)}.${(0, pg.escapeIdentifier)(relation.table_name)}`;
2568
+ await tx.none(`SET LOCAL lock_timeout = '100ms'`);
2569
+ try {
2570
+ await tx.none(`LOCK TABLE ${table} IN ACCESS EXCLUSIVE MODE`);
2571
+ } catch (error) {
2572
+ lockUnavailable = typeof error === "object" && error !== null && "code" in error && error.code === "55P03";
2573
+ throw error;
2574
+ }
2575
+ const current = await observe(tx);
2576
+ if (current?.valid) return "valid";
2577
+ const locked = await tx.one("SELECT to_regclass($1)::oid AS oid", [table]);
2578
+ if (!current || current.oid !== relation.oid || current.table_oid !== locked.oid) return "changed";
2579
+ logger.warn(`Dropping invalid index ${indexName} left by an interrupted build; rebuilding it`);
2580
+ await tx.none(`DROP INDEX ${(0, pg.escapeIdentifier)(schemaName)}.${(0, pg.escapeIdentifier)(indexName)}`);
2581
+ return "dropped";
2582
+ });
2583
+ if (result === "valid") return true;
2584
+ if (result === "changed") {
2585
+ attempts--;
2586
+ if (performance.now() >= deadline) break;
2587
+ continue;
2588
+ }
2589
+ } catch (error) {
2590
+ attempts--;
2591
+ if (lockUnavailable) {
2592
+ state = "table busy during invalid-index repair";
2593
+ failure ??= error;
2594
+ await (0, timers_promises.setTimeout)(100);
2595
+ continue;
2596
+ }
2597
+ if (!isDuplicateRelationError(error) && !isRetryableDeadlock(error) && !(typeof error === "object" && error !== null && "code" in error && error.code === "42P01")) throw error;
2598
+ failure ??= error;
2599
+ if (++repairFailures >= 3 || performance.now() >= deadline) break;
2600
+ continue;
2601
+ }
2602
+ } else {
2603
+ if (attempts >= 3 || performance.now() >= deadline) break;
2604
+ attempts++;
2605
+ }
2606
+ try {
2607
+ await build();
2608
+ } catch (error) {
2609
+ if (!isDuplicateRelationError(error) && !isRetryableDeadlock(error)) throw error;
2610
+ failure ??= error;
2611
+ const deadlock = isRetryableDeadlock(error);
2612
+ state = deadlock ? "index build deadlock" : "competing index creation";
2613
+ if (deadlock && attempts < 3 && performance.now() < deadline) await backoffAfterDeadlock(attempts);
2614
+ }
2615
+ }
2616
+ throw new IndexBuildError(indexName, schemaName, state, failure);
2617
+ }
2488
2618
  // src/storage/db/sanitize-json.ts
2489
2619
  const repair = (value) => value.replace(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g, "�").replaceAll("\0", "");
2490
2620
  function repairJson(value) {
@@ -2540,9 +2670,10 @@ async function loadSchemaSnapshot(client, schemaName) {
2540
2670
  AND c.relkind IN ('r', 'p')
2541
2671
  AND a.attnum > 0
2542
2672
  AND NOT a.attisdropped`, [schema]),
2543
- client.manyOrNone(`SELECT c.relname AS indexname, i.indisreplident AS is_replica_identity, i.indisprimary AS is_primary, i.indisvalid AS is_valid
2673
+ 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
2544
2674
  FROM pg_catalog.pg_index i
2545
2675
  JOIN pg_catalog.pg_class c ON c.oid = i.indexrelid
2676
+ JOIN pg_catalog.pg_class t ON t.oid = i.indrelid
2546
2677
  JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
2547
2678
  WHERE n.nspname = $1`, [schema])
2548
2679
  ]);
@@ -2573,13 +2704,11 @@ async function loadSchemaSnapshot(client, schemaName) {
2573
2704
  }
2574
2705
  const indexes = /* @__PURE__ */ new Set();
2575
2706
  const replicaIdentityIndexes = /* @__PURE__ */ new Set();
2576
- const primaryKeyIndexes = /* @__PURE__ */ new Set();
2577
- const invalidIndexes = /* @__PURE__ */ new Set();
2707
+ const primaryKeyIndexes = /* @__PURE__ */ new Map();
2578
2708
  for (const row of indexRows) {
2579
2709
  if (row.is_valid) indexes.add(row.indexname);
2580
- else invalidIndexes.add(row.indexname);
2581
2710
  if (row.is_replica_identity) replicaIdentityIndexes.add(row.indexname);
2582
- if (row.is_primary) primaryKeyIndexes.add(row.indexname.toLowerCase());
2711
+ if (row.is_primary) primaryKeyIndexes.set(row.indexname.toLowerCase(), row.table_name);
2583
2712
  }
2584
2713
  return {
2585
2714
  schemaName: schema,
@@ -2589,8 +2718,7 @@ async function loadSchemaSnapshot(client, schemaName) {
2589
2718
  notNullColumns,
2590
2719
  indexes,
2591
2720
  replicaIdentityIndexes,
2592
- primaryKeyIndexes,
2593
- invalidIndexes
2721
+ primaryKeyIndexes
2594
2722
  };
2595
2723
  }
2596
2724
  // src/storage/db/index.ts
@@ -3502,14 +3630,17 @@ MIGRATION REQUIRED: Duplicate spans detected in ${duplicateInfo.tableName}\n====
3502
3630
  baseName: "mastra_ai_spans_traceid_spanid_pk",
3503
3631
  schemaName: (this.schemaName ? schemaNamePrefix(this.schemaName) : "") || void 0
3504
3632
  });
3505
- const schemaFilter = this.schemaName || "public";
3633
+ const fullTableName = getTableName$6({
3634
+ indexName: _mastra_core_storage.TABLE_SPANS,
3635
+ schemaName: getSchemaName$6(this.schemaName)
3636
+ });
3506
3637
  const snapshot = this.schemaSnapshot;
3507
- if (snapshot) return snapshot.primaryKeyIndexes.has(constraintName.toLowerCase());
3508
- return this.spansPrimaryKeyExistsLive(constraintName, schemaFilter);
3638
+ if (snapshot) return snapshot.primaryKeyIndexes.get(constraintName.toLowerCase()) === _mastra_core_storage.TABLE_SPANS;
3639
+ return this.spansPrimaryKeyExistsLive(constraintName, fullTableName);
3509
3640
  }
3510
3641
  /** Live-catalog variant of {@link spansPrimaryKeyExists}, bypassing the snapshot. */
3511
- async spansPrimaryKeyExistsLive(constraintName, schemaFilter) {
3512
- 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;
3642
+ async spansPrimaryKeyExistsLive(constraintName, fullTableName) {
3643
+ 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;
3513
3644
  }
3514
3645
  /**
3515
3646
  * Adds the PRIMARY KEY constraint on (traceId, spanId) to the spans table.
@@ -3524,13 +3655,8 @@ MIGRATION REQUIRED: Duplicate spans detected in ${duplicateInfo.tableName}\n====
3524
3655
  baseName: "mastra_ai_spans_traceid_spanid_pk",
3525
3656
  schemaName: (this.schemaName ? schemaNamePrefix(this.schemaName) : "") || void 0
3526
3657
  });
3527
- const schemaFilter = this.schemaName || "public";
3528
3658
  try {
3529
- if ((await this.client.oneOrNone(`
3530
- SELECT EXISTS (
3531
- SELECT 1 FROM pg_constraint WHERE conname = lower($1) AND connamespace = (SELECT oid FROM pg_namespace WHERE nspname = $2)
3532
- ) as exists
3533
- `, [constraintName, schemaFilter]))?.exists) {
3659
+ if (await this.spansPrimaryKeyExistsLive(constraintName, fullTableName)) {
3534
3660
  this.logger?.debug?.(`PRIMARY KEY constraint ${constraintName} already exists on ${fullTableName}`);
3535
3661
  return;
3536
3662
  }
@@ -3539,12 +3665,13 @@ MIGRATION REQUIRED: Duplicate spans detected in ${duplicateInfo.tableName}\n====
3539
3665
  ADD CONSTRAINT ${constraintName}
3540
3666
  PRIMARY KEY ("traceId", "spanId")
3541
3667
  `);
3542
- this.schemaSnapshot?.primaryKeyIndexes.add(constraintName.toLowerCase());
3668
+ this.schemaSnapshot?.primaryKeyIndexes.set(constraintName.toLowerCase(), _mastra_core_storage.TABLE_SPANS);
3543
3669
  this.logger?.info?.(`Added PRIMARY KEY constraint ${constraintName} to ${fullTableName}`);
3544
3670
  } catch (error) {
3545
- if (isDuplicateRelationError(error)) {
3546
- if (await this.spansPrimaryKeyExistsLive(constraintName, schemaFilter)) {
3547
- this.schemaSnapshot?.primaryKeyIndexes.add(constraintName.toLowerCase());
3671
+ const primaryKeyRace = typeof error === "object" && error !== null && "code" in error && error.code === "42P16";
3672
+ if (isDuplicateRelationError(error) || primaryKeyRace) {
3673
+ if (await this.spansPrimaryKeyExistsLive(constraintName, fullTableName)) {
3674
+ this.schemaSnapshot?.primaryKeyIndexes.set(constraintName.toLowerCase(), _mastra_core_storage.TABLE_SPANS);
3548
3675
  this.logger?.debug?.(`PRIMARY KEY constraint ${constraintName} was created by another process`);
3549
3676
  return;
3550
3677
  }
@@ -3761,7 +3888,6 @@ MIGRATION REQUIRED: Duplicate spans detected in ${duplicateInfo.tableName}\n====
3761
3888
  });
3762
3889
  const snapshot = this.schemaSnapshot;
3763
3890
  if (snapshot?.indexes.has(name)) return;
3764
- if (!await this.prepareIndexBuild(name, schemaName)) return;
3765
3891
  const uniqueStr = unique ? "UNIQUE " : "";
3766
3892
  const concurrentStr = concurrent ? "CONCURRENTLY " : "";
3767
3893
  const methodStr = method !== "btree" ? `USING ${method} ` : "";
@@ -3779,9 +3905,8 @@ MIGRATION REQUIRED: Duplicate spans detected in ${duplicateInfo.tableName}\n====
3779
3905
  const tablespaceStr = tablespace ? ` TABLESPACE ${tablespace}` : "";
3780
3906
  let withStr = "";
3781
3907
  if (storage && Object.keys(storage).length > 0) withStr = ` WITH (${Object.entries(storage).map(([key, value]) => `${key} = ${value}`).join(", ")})`;
3782
- const sql = `CREATE ${uniqueStr}INDEX ${concurrentStr}${`"${(0, _mastra_core_utils.parseSqlIdentifier)(name, "index name")}"`} ON ${fullTableName} ${methodStr}(${columnsStr})${withStr}${tablespaceStr}${whereStr}`;
3783
- await this.timedIndexBuild(name, sql);
3784
- snapshot?.indexes.add(name);
3908
+ const sql = `CREATE ${uniqueStr}INDEX ${concurrentStr}IF NOT EXISTS ${`"${(0, _mastra_core_utils.parseSqlIdentifier)(name, "index name")}"`} ON ${fullTableName} ${methodStr}(${columnsStr})${withStr}${tablespaceStr}${whereStr}`;
3909
+ if (await buildIndex(this.client, schemaName, name, () => this.timedIndexBuild(name, sql), this.logger)) snapshot?.indexes.add(name);
3785
3910
  } catch (error) {
3786
3911
  throw new _mastra_core_error.MastraError({
3787
3912
  id: (0, _mastra_core_storage.createStorageErrorId)("PG", "INDEX_CREATE", "FAILED"),
@@ -3806,36 +3931,7 @@ MIGRATION REQUIRED: Duplicate spans detected in ${duplicateInfo.tableName}\n====
3806
3931
  async createIndexFromStatement(indexName, sql) {
3807
3932
  const snapshot = this.schemaSnapshot;
3808
3933
  if (snapshot?.indexes.has(indexName)) return;
3809
- if (!await this.prepareIndexBuild(indexName, this.schemaName || "public")) return;
3810
- await this.timedIndexBuild(indexName, sql);
3811
- snapshot?.indexes.add(indexName);
3812
- }
3813
- /**
3814
- * Decides whether `indexName` still needs building. Returns false when a valid
3815
- * index already exists, or when another session is currently building it.
3816
- * An invalid index left by an interrupted `CREATE INDEX CONCURRENTLY` is
3817
- * dropped (concurrently) so the caller rebuilds it — `IF NOT EXISTS` would
3818
- * otherwise keep the broken index forever.
3819
- */
3820
- async prepareIndexBuild(indexName, schemaName) {
3821
- const snapshot = this.schemaSnapshot;
3822
- if (snapshot && !snapshot.invalidIndexes.has(indexName)) return true;
3823
- const existing = await this.client.oneOrNone(`SELECT i.indisvalid AS is_valid,
3824
- EXISTS (SELECT 1 FROM pg_stat_progress_create_index p WHERE p.index_relid = c.oid) AS building
3825
- FROM pg_catalog.pg_index i
3826
- JOIN pg_catalog.pg_class c ON c.oid = i.indexrelid
3827
- JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
3828
- WHERE c.relname = $1 AND n.nspname = $2`, [indexName, schemaName]);
3829
- if (!existing) return true;
3830
- if (existing.is_valid) return false;
3831
- if (existing.building) {
3832
- this.logger.warn(`Index ${indexName} is being built by another session; skipping`);
3833
- return false;
3834
- }
3835
- this.logger.warn(`Dropping invalid index ${indexName} left by an interrupted build; rebuilding it`);
3836
- const quotedIndexName = `"${(0, _mastra_core_utils.parseSqlIdentifier)(indexName, "index name")}"`;
3837
- await this.client.none(`DROP INDEX CONCURRENTLY IF EXISTS ${getSchemaName$6(this.schemaName)}.${quotedIndexName}`);
3838
- return true;
3934
+ if (await buildIndex(this.client, this.schemaName || "public", indexName, () => this.timedIndexBuild(indexName, sql), this.logger)) snapshot?.indexes.add(indexName);
3839
3935
  }
3840
3936
  async timedIndexBuild(indexName, sql) {
3841
3937
  const startedAt = Date.now();
@@ -17322,6 +17418,203 @@ function parseUpdateFeedbackReviewStatusArgs(args) {
17322
17418
  reviewStatus: args.reviewStatus
17323
17419
  };
17324
17420
  }
17421
+ // src/storage/domains/observability/v-next/partitioning.ts
17422
+ const DEFAULT_FUTURE_DAYS = 14;
17423
+ async function detectTimescale(client) {
17424
+ const row = await client.oneOrNone(`SELECT EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'timescaledb') AS "exists"`);
17425
+ return Boolean(row?.exists);
17426
+ }
17427
+ async function detectPartman(client) {
17428
+ const row = await client.oneOrNone(`SELECT EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'pg_partman') AS "exists"`);
17429
+ return Boolean(row?.exists);
17430
+ }
17431
+ async function getPartmanVersion(client) {
17432
+ return (await client.oneOrNone(`SELECT extversion FROM pg_extension WHERE extname = 'pg_partman'`))?.extversion ?? null;
17433
+ }
17434
+ function getPartmanMajor(version) {
17435
+ if (!version) return null;
17436
+ const match = /^(\d+)/.exec(version);
17437
+ return match ? Number(match[1]) : null;
17438
+ }
17439
+ async function resolveMode(client, options = {}) {
17440
+ if (options.mode && options.mode !== "auto") return options.mode;
17441
+ if (await detectTimescale(client)) return "timescale";
17442
+ if (await detectPartman(client)) return "partman";
17443
+ return "native";
17444
+ }
17445
+ function dayBounds(d) {
17446
+ const year = d.getUTCFullYear();
17447
+ const month = String(d.getUTCMonth() + 1).padStart(2, "0");
17448
+ const day = String(d.getUTCDate()).padStart(2, "0");
17449
+ const start = `${year}-${month}-${day} 00:00:00+00`;
17450
+ const next = new Date(Date.UTC(year, d.getUTCMonth(), d.getUTCDate() + 1));
17451
+ return {
17452
+ start,
17453
+ end: `${next.getUTCFullYear()}-${String(next.getUTCMonth() + 1).padStart(2, "0")}-${String(next.getUTCDate()).padStart(2, "0")} 00:00:00+00`,
17454
+ suffix: `${year}${month}${day}`
17455
+ };
17456
+ }
17457
+ function partitionName(table, suffix) {
17458
+ return `${table}_p${suffix}`;
17459
+ }
17460
+ /**
17461
+ * Creates daily partition tables in [today - 1 day, today + futureDays].
17462
+ * Idempotent: uses CREATE TABLE IF NOT EXISTS.
17463
+ */
17464
+ async function ensureNativePartitions(client, schema, options = {}) {
17465
+ const windowDays = partitionWindow(options);
17466
+ for (const table of ALL_SIGNAL_TABLES) await ensureDayPartitions(client, schema, table, windowDays);
17467
+ }
17468
+ /** The UTC days init() pre-creates: yesterday (optional) through today + futureDays. */
17469
+ function partitionWindow(options = {}, nowMs = Date.now()) {
17470
+ const futureDays = options.futureDays ?? DEFAULT_FUTURE_DAYS;
17471
+ const start = options.includeYesterday ?? true ? -1 : 0;
17472
+ const days = [];
17473
+ for (let i = start; i <= futureDays; i++) days.push(new Date(nowMs + i * 864e5));
17474
+ return days;
17475
+ }
17476
+ /**
17477
+ * Keeps only the days inside the current partition window. The write path
17478
+ * creates partitions on demand within that window only, so a row with a
17479
+ * client-supplied timestamp far in the past or future cannot make the
17480
+ * database grow a table per day it names.
17481
+ */
17482
+ function withinPartitionWindow(days, options = {}) {
17483
+ const allowed = new Set(partitionWindow(options).map((d) => dayBounds(d).suffix));
17484
+ return days.filter((d) => allowed.has(dayBounds(d).suffix));
17485
+ }
17486
+ /**
17487
+ * Creates and attaches a partition of `schema.table` for each UTC day in
17488
+ * `days` that has none. Each day is a `CREATE TABLE (LIKE parent)` followed by
17489
+ * `ATTACH PARTITION`, in one transaction, so a failed attach leaves no
17490
+ * detached child behind. When either step fails, the catalog is read again: a
17491
+ * day that a concurrent caller attached meanwhile counts as done, and any
17492
+ * other failure is rethrown.
17493
+ */
17494
+ async function ensureDayPartitions(client, schema, table, days) {
17495
+ const parent = qualifiedTable(schema, table);
17496
+ const attached = await attachedPartitionNames(client, schema, table);
17497
+ for (const d of days) {
17498
+ const { start: partStart, end: partEnd, suffix } = dayBounds(d);
17499
+ const childName = partitionName(table, suffix);
17500
+ if (attached.has(childName)) continue;
17501
+ const child = qualifiedName(schema, childName);
17502
+ try {
17503
+ await client.tx(async (t) => {
17504
+ await t.none(`CREATE TABLE IF NOT EXISTS ${child} (LIKE ${parent} INCLUDING ALL)`);
17505
+ await t.none(`ALTER TABLE ${parent} ATTACH PARTITION ${child}
17506
+ FOR VALUES FROM ('${partStart}') TO ('${partEnd}')`);
17507
+ });
17508
+ } catch (error) {
17509
+ if (!(await attachedPartitionNames(client, schema, table)).has(childName)) throw error;
17510
+ }
17511
+ }
17512
+ }
17513
+ /** Names of the children attached to `schema.table`, read from the catalog so no search_path can shadow it. */
17514
+ async function attachedPartitionNames(client, schema, table) {
17515
+ const rows = await client.manyOrNone(`SELECT c.relname
17516
+ FROM pg_catalog.pg_inherits i
17517
+ JOIN pg_catalog.pg_class c ON c.oid = i.inhrelid
17518
+ WHERE i.inhparent = (
17519
+ SELECT p.oid
17520
+ FROM pg_catalog.pg_class p
17521
+ JOIN pg_catalog.pg_namespace n ON n.oid = p.relnamespace
17522
+ WHERE n.nspname = $1 AND p.relname = $2
17523
+ )`, [schema, table]);
17524
+ return new Set(rows.map((r) => r.relname));
17525
+ }
17526
+ /**
17527
+ * `23514` raised by `ExecFindPartition`: the row's day has no attached
17528
+ * partition. The message is matched as a fallback only, because Postgres
17529
+ * translates it when `lc_messages` is not English.
17530
+ */
17531
+ function isMissingPartitionError(error) {
17532
+ const { code, routine, message = "" } = error ?? {};
17533
+ return code === "23514" && (routine === "ExecFindPartition" || /no partition of relation/i.test(message));
17534
+ }
17535
+ /**
17536
+ * Calls `create_hypertable()` for each signal table.
17537
+ *
17538
+ * Postgres declarative partitioning and Timescale hypertables are mutually
17539
+ * exclusive, so the base DDL must have been generated with TableDDLMode
17540
+ * 'timescale' (no `PARTITION BY` clause). `if_not_exists` makes this
17541
+ * idempotent across re-inits.
17542
+ */
17543
+ async function ensureTimescaleHypertables(client, schema) {
17544
+ for (const table of ALL_SIGNAL_TABLES) {
17545
+ const tableExpr = qualifiedTable(schema, table);
17546
+ const timeColumn = SIGNAL_TIME_COLUMN[table].replace(/'/g, "''");
17547
+ await client.none(`SELECT create_hypertable($1::regclass, '${timeColumn}', chunk_time_interval => INTERVAL '1 day', if_not_exists => true)`, [tableExpr]);
17548
+ }
17549
+ }
17550
+ /**
17551
+ * Registers each signal table with pg_partman as a daily-interval parent and
17552
+ * lets partman create the child partitions.
17553
+ *
17554
+ * Retention is intentionally NOT configured here — it stays opt-in via the
17555
+ * future Mastra CLI surface.
17556
+ *
17557
+ * Prefers the pg_partman 5.x+ declarative API (`p_type := 'range'`) and keeps
17558
+ * a 4.x fallback (`p_type := 'native'`) for older installs. That lets the
17559
+ * adapter run against current arm64-friendly images without breaking existing
17560
+ * 4.x deployments.
17561
+ */
17562
+ async function partmanParentRegistered(client, schema, table) {
17563
+ const row = await client.oneOrNone(`SELECT EXISTS (
17564
+ SELECT 1 FROM partman.part_config
17565
+ WHERE parent_table = format('%I.%I', $1::text, $2::text)
17566
+ ) AS "exists"`, [schema, table]);
17567
+ return Boolean(row?.exists);
17568
+ }
17569
+ async function ensurePartmanHypertables(client, schema) {
17570
+ const partmanMajor = getPartmanMajor(await getPartmanVersion(client));
17571
+ for (const table of ALL_SIGNAL_TABLES) {
17572
+ if (await partmanParentRegistered(client, schema, table)) continue;
17573
+ for (let attempt = 0; attempt < 3; attempt++) try {
17574
+ if (partmanMajor != null && partmanMajor >= 5) await client.none(`SELECT partman.create_parent(
17575
+ p_parent_table := format('%I.%I', $1::text, $2::text),
17576
+ p_control := $3,
17577
+ p_interval := '1 day',
17578
+ p_type := 'range'
17579
+ )`, [
17580
+ schema,
17581
+ table,
17582
+ SIGNAL_TIME_COLUMN[table]
17583
+ ]);
17584
+ else await client.none(`SELECT partman.create_parent(
17585
+ p_parent_table := format('%I.%I', $1::text, $2::text),
17586
+ p_control := $3,
17587
+ p_type := 'native',
17588
+ p_interval := '1 day'
17589
+ )`, [
17590
+ schema,
17591
+ table,
17592
+ SIGNAL_TIME_COLUMN[table]
17593
+ ]);
17594
+ break;
17595
+ } catch (error) {
17596
+ const message = error?.message ?? "";
17597
+ if (error?.code === "23505" || /already managed by pg_partman/i.test(message) || /relation .* already exists/i.test(message)) break;
17598
+ if (!/deadlock detected/i.test(message)) throw error;
17599
+ if (await partmanParentRegistered(client, schema, table)) break;
17600
+ if (attempt === 2) throw error;
17601
+ await new Promise((resolve) => setTimeout(resolve, 25));
17602
+ }
17603
+ }
17604
+ }
17605
+ async function setupPartitioning(client, schema, options = {}) {
17606
+ switch (await resolveMode(client, options)) {
17607
+ case "timescale":
17608
+ await ensureTimescaleHypertables(client, schema);
17609
+ return "timescale";
17610
+ case "partman":
17611
+ await ensurePartmanHypertables(client, schema);
17612
+ return "partman";
17613
+ case "native":
17614
+ await ensureNativePartitions(client, schema, options);
17615
+ return "native";
17616
+ }
17617
+ }
17325
17618
  // src/storage/domains/observability/v-next/sql.ts
17326
17619
  /**
17327
17620
  * SQL helpers for the v-next Postgres observability domain.
@@ -17402,6 +17695,54 @@ ${onConflict}`,
17402
17695
  values
17403
17696
  };
17404
17697
  }
17698
+ /**
17699
+ * Insert into a signal table. In native partition mode, a row whose day is
17700
+ * inside the partition window but has no partition yet gets one created, then
17701
+ * the insert runs once more. TimescaleDB and pg_partman own their partitions,
17702
+ * so in those modes the original error propagates untouched. The mode is the
17703
+ * one the store resolved; a store that never ran init() (`disableInit`)
17704
+ * detects it here, after the failed insert, so the hot path stays one query.
17705
+ */
17706
+ async function insertRows(client, schema, table, records, partitioning, onConflict) {
17707
+ const insert = buildInsert(schema, table, records, onConflict);
17708
+ if (!insert) return;
17709
+ try {
17710
+ await client.query(insert.text, insert.values);
17711
+ } catch (error) {
17712
+ if (!isMissingPartitionError(error) || await resolveMode(client, partitioning) !== "native") throw error;
17713
+ const days = withinPartitionWindow(recordDays(records, SIGNAL_TIME_COLUMN[table]), partitioning);
17714
+ if (days.length === 0) throw error;
17715
+ for (const day of days) await ensureDayPartitions(client, schema, table, [day]).catch((cause) => {
17716
+ if (!isInsufficientPrivilegeError(cause)) throw cause;
17717
+ throw new Error(partitionCreateFailure({
17718
+ table,
17719
+ day,
17720
+ cause
17721
+ }), { cause });
17722
+ });
17723
+ await client.query(insert.text, insert.values);
17724
+ }
17725
+ }
17726
+ /** `42501`: the role may not create the child, or does not own the parent it must attach to. */
17727
+ function isInsufficientPrivilegeError(error) {
17728
+ return error?.code === "42501";
17729
+ }
17730
+ function partitionCreateFailure({ table, day, cause }) {
17731
+ const reason = cause instanceof Error ? cause.message : String(cause);
17732
+ 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.`;
17733
+ }
17734
+ function recordDays(records, timeColumn) {
17735
+ const days = /* @__PURE__ */ new Map();
17736
+ for (const record of records) {
17737
+ const value = record[timeColumn];
17738
+ if (value == null) continue;
17739
+ const d = new Date(value);
17740
+ if (isNaN(d.getTime())) continue;
17741
+ const day = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()));
17742
+ days.set(day.toISOString(), day);
17743
+ }
17744
+ return [...days.values()];
17745
+ }
17405
17746
  /** Primary key of the span table; also the conflict target for span upserts. */
17406
17747
  const SPAN_KEY_COLUMNS = [
17407
17748
  "traceId",
@@ -18312,16 +18653,14 @@ function pushFeedbackIdentity(acc, feedbackType, feedbackSource) {
18312
18653
  }
18313
18654
  acc.conditions.push(`"valueNumber" IS NOT NULL`);
18314
18655
  }
18315
- async function createFeedback(client, schema, args) {
18656
+ async function createFeedback(client, schema, args, partitioning) {
18316
18657
  const row = feedbackRecordToRow(args.feedback);
18317
- const insert = buildInsert(schema, TABLE_FEEDBACK_EVENTS, [row], FEEDBACK_UPSERT_CLAUSE);
18318
- if (insert) await client.query(insert.text, insert.values);
18658
+ await insertRows(client, schema, TABLE_FEEDBACK_EVENTS, [row], partitioning, FEEDBACK_UPSERT_CLAUSE);
18319
18659
  }
18320
- async function batchCreateFeedback(client, schema, args) {
18660
+ async function batchCreateFeedback(client, schema, args, partitioning) {
18321
18661
  if (args.feedbacks.length === 0) return;
18322
18662
  const rows = [...new Map(args.feedbacks.map((feedback) => [feedback.feedbackId, feedback])).values()].map(feedbackRecordToRow);
18323
- const insert = buildInsert(schema, TABLE_FEEDBACK_EVENTS, rows, FEEDBACK_UPSERT_CLAUSE);
18324
- if (insert) await client.query(insert.text, insert.values);
18663
+ await insertRows(client, schema, TABLE_FEEDBACK_EVENTS, rows, partitioning, FEEDBACK_UPSERT_CLAUSE);
18325
18664
  }
18326
18665
  async function updateFeedbackReviewStatus(client, schema, args) {
18327
18666
  const { feedbackId, reviewStatus } = parseUpdateFeedbackReviewStatusArgs(args);
@@ -18519,11 +18858,10 @@ function applyLogFilters(acc, filters) {
18519
18858
  applyCommonFilters(acc, filters);
18520
18859
  applySingleOrArrayFilter(acc, "level", filters?.level);
18521
18860
  }
18522
- async function batchCreateLogs(client, schema, args) {
18861
+ async function batchCreateLogs(client, schema, args, partitioning) {
18523
18862
  if (args.logs.length === 0) return;
18524
18863
  const rows = args.logs.map(logRecordToRow);
18525
- const insert = buildInsert(schema, TABLE_LOG_EVENTS, rows);
18526
- if (insert) await client.query(insert.text, insert.values);
18864
+ await insertRows(client, schema, TABLE_LOG_EVENTS, rows, partitioning);
18527
18865
  }
18528
18866
  async function listLogs(client, schema, args) {
18529
18867
  const { mode, filters, pagination, orderBy, after, limit } = _mastra_core_storage.listLogsArgsSchema.parse(args);
@@ -18594,11 +18932,10 @@ function pushMetricNameFilter(acc, names) {
18594
18932
  acc.conditions.push(`"name" IN (${placeholders})`);
18595
18933
  acc.params.push(...names);
18596
18934
  }
18597
- async function batchCreateMetrics(client, schema, args) {
18935
+ async function batchCreateMetrics(client, schema, args, partitioning) {
18598
18936
  if (args.metrics.length === 0) return;
18599
18937
  const rows = args.metrics.map(metricRecordToRow);
18600
- const insert = buildInsert(schema, TABLE_METRIC_EVENTS, rows);
18601
- if (insert) await client.query(insert.text, insert.values);
18938
+ await insertRows(client, schema, TABLE_METRIC_EVENTS, rows, partitioning);
18602
18939
  }
18603
18940
  async function listMetrics(client, schema, args) {
18604
18941
  const { mode, filters, pagination, orderBy, after, limit } = _mastra_core_storage.listMetricsArgsSchema.parse(args);
@@ -18800,165 +19137,6 @@ async function getMetricPercentiles(client, schema, args) {
18800
19137
  `;
18801
19138
  return { series: percentileSeriesFromRows(await client.manyOrNone(sql, acc.params), args.percentiles) };
18802
19139
  }
18803
- // src/storage/domains/observability/v-next/partitioning.ts
18804
- const DEFAULT_FUTURE_DAYS = 14;
18805
- async function detectTimescale(client) {
18806
- const row = await client.oneOrNone(`SELECT EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'timescaledb') AS "exists"`);
18807
- return Boolean(row?.exists);
18808
- }
18809
- async function detectPartman(client) {
18810
- const row = await client.oneOrNone(`SELECT EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'pg_partman') AS "exists"`);
18811
- return Boolean(row?.exists);
18812
- }
18813
- async function getPartmanVersion(client) {
18814
- return (await client.oneOrNone(`SELECT extversion FROM pg_extension WHERE extname = 'pg_partman'`))?.extversion ?? null;
18815
- }
18816
- function getPartmanMajor(version) {
18817
- if (!version) return null;
18818
- const match = /^(\d+)/.exec(version);
18819
- return match ? Number(match[1]) : null;
18820
- }
18821
- async function resolveMode(client, options = {}) {
18822
- if (options.mode && options.mode !== "auto") return options.mode;
18823
- if (await detectTimescale(client)) return "timescale";
18824
- if (await detectPartman(client)) return "partman";
18825
- return "native";
18826
- }
18827
- function dayBounds(d) {
18828
- const year = d.getUTCFullYear();
18829
- const month = String(d.getUTCMonth() + 1).padStart(2, "0");
18830
- const day = String(d.getUTCDate()).padStart(2, "0");
18831
- const start = `${year}-${month}-${day} 00:00:00+00`;
18832
- const next = new Date(Date.UTC(year, d.getUTCMonth(), d.getUTCDate() + 1));
18833
- return {
18834
- start,
18835
- end: `${next.getUTCFullYear()}-${String(next.getUTCMonth() + 1).padStart(2, "0")}-${String(next.getUTCDate()).padStart(2, "0")} 00:00:00+00`,
18836
- suffix: `${year}${month}${day}`
18837
- };
18838
- }
18839
- function partitionName(table, suffix) {
18840
- return `${table}_p${suffix}`;
18841
- }
18842
- /**
18843
- * Creates daily partition tables in [today - 1 day, today + futureDays].
18844
- * Idempotent: uses CREATE TABLE IF NOT EXISTS.
18845
- */
18846
- async function ensureNativePartitions(client, schema, options = {}) {
18847
- const futureDays = options.futureDays ?? DEFAULT_FUTURE_DAYS;
18848
- const start = options.includeYesterday ?? true ? -1 : 0;
18849
- const baseNowMs = Date.now();
18850
- for (const table of ALL_SIGNAL_TABLES) for (let i = start; i <= futureDays; i++) {
18851
- const { start: partStart, end: partEnd, suffix } = dayBounds(new Date(baseNowMs + i * 864e5));
18852
- const child = qualifiedName(schema, partitionName(table, suffix));
18853
- const parent = qualifiedTable(schema, table);
18854
- try {
18855
- await client.none(`CREATE TABLE IF NOT EXISTS ${child} (LIKE ${parent} INCLUDING ALL)`);
18856
- } catch (error) {
18857
- if (!isDuplicateRelationError(error)) throw error;
18858
- }
18859
- try {
18860
- await client.none(`ALTER TABLE ${parent} ATTACH PARTITION ${child}
18861
- FOR VALUES FROM ('${partStart}') TO ('${partEnd}')`);
18862
- } catch (error) {
18863
- if (!isAlreadyAttachedError(error)) throw error;
18864
- }
18865
- }
18866
- }
18867
- /**
18868
- * `ALTER TABLE ATTACH PARTITION` fails with `42P17 invalid_object_definition`
18869
- * when the target table is already a partition of *any* parent (including
18870
- * the one we want). On older PG it can also surface as 42710 or a generic
18871
- * message. We err on the side of treating any "already a partition" wording
18872
- * as success so concurrent inits converge cleanly.
18873
- */
18874
- function isAlreadyAttachedError(error) {
18875
- const code = error?.code;
18876
- const message = error?.message ?? "";
18877
- if (code === "42P17" || code === "42710") return /is already a partition/i.test(message) || /already a partition/i.test(message);
18878
- return /is already a partition/i.test(message);
18879
- }
18880
- /**
18881
- * Calls `create_hypertable()` for each signal table.
18882
- *
18883
- * Postgres declarative partitioning and Timescale hypertables are mutually
18884
- * exclusive, so the base DDL must have been generated with TableDDLMode
18885
- * 'timescale' (no `PARTITION BY` clause). `if_not_exists` makes this
18886
- * idempotent across re-inits.
18887
- */
18888
- async function ensureTimescaleHypertables(client, schema) {
18889
- for (const table of ALL_SIGNAL_TABLES) {
18890
- const tableExpr = qualifiedTable(schema, table);
18891
- const timeColumn = SIGNAL_TIME_COLUMN[table].replace(/'/g, "''");
18892
- await client.none(`SELECT create_hypertable($1::regclass, '${timeColumn}', chunk_time_interval => INTERVAL '1 day', if_not_exists => true)`, [tableExpr]);
18893
- }
18894
- }
18895
- /**
18896
- * Registers each signal table with pg_partman as a daily-interval parent and
18897
- * lets partman create the child partitions.
18898
- *
18899
- * Retention is intentionally NOT configured here — it stays opt-in via the
18900
- * future Mastra CLI surface.
18901
- *
18902
- * Prefers the pg_partman 5.x+ declarative API (`p_type := 'range'`) and keeps
18903
- * a 4.x fallback (`p_type := 'native'`) for older installs. That lets the
18904
- * adapter run against current arm64-friendly images without breaking existing
18905
- * 4.x deployments.
18906
- */
18907
- async function ensurePartmanHypertables(client, schema) {
18908
- const partmanMajor = getPartmanMajor(await getPartmanVersion(client));
18909
- for (const table of ALL_SIGNAL_TABLES) {
18910
- if ((await client.oneOrNone(`SELECT EXISTS (
18911
- SELECT 1 FROM partman.part_config
18912
- WHERE parent_table = format('%I.%I', $1::text, $2::text)
18913
- ) AS "exists"`, [schema, table]))?.exists) continue;
18914
- for (let attempt = 0; attempt < 3; attempt++) try {
18915
- if (partmanMajor != null && partmanMajor >= 5) await client.none(`SELECT partman.create_parent(
18916
- p_parent_table := format('%I.%I', $1::text, $2::text),
18917
- p_control := $3,
18918
- p_interval := '1 day',
18919
- p_type := 'range'
18920
- )`, [
18921
- schema,
18922
- table,
18923
- SIGNAL_TIME_COLUMN[table]
18924
- ]);
18925
- else await client.none(`SELECT partman.create_parent(
18926
- p_parent_table := format('%I.%I', $1::text, $2::text),
18927
- p_control := $3,
18928
- p_type := 'native',
18929
- p_interval := '1 day'
18930
- )`, [
18931
- schema,
18932
- table,
18933
- SIGNAL_TIME_COLUMN[table]
18934
- ]);
18935
- break;
18936
- } catch (error) {
18937
- const message = error?.message ?? "";
18938
- if (error?.code === "23505" || /already managed by pg_partman/i.test(message) || /relation .* already exists/i.test(message)) break;
18939
- if (!/deadlock detected/i.test(message)) throw error;
18940
- if ((await client.oneOrNone(`SELECT EXISTS (
18941
- SELECT 1 FROM partman.part_config
18942
- WHERE parent_table = format('%I.%I', $1::text, $2::text)
18943
- ) AS "exists"`, [schema, table]))?.exists) break;
18944
- if (attempt === 2) throw error;
18945
- await new Promise((resolve) => setTimeout(resolve, 25));
18946
- }
18947
- }
18948
- }
18949
- async function setupPartitioning(client, schema, options = {}) {
18950
- switch (await resolveMode(client, options)) {
18951
- case "timescale":
18952
- await ensureTimescaleHypertables(client, schema);
18953
- return "timescale";
18954
- case "partman":
18955
- await ensurePartmanHypertables(client, schema);
18956
- return "partman";
18957
- case "native":
18958
- await ensureNativePartitions(client, schema, options);
18959
- return "native";
18960
- }
18961
- }
18962
19140
  // src/storage/domains/observability/v-next/retention.ts
18963
19141
  /**
18964
19142
  * Age-based retention for the v-next Postgres observability domain.
@@ -19124,16 +19302,14 @@ function collapseExactScoreConflicts(rows) {
19124
19302
  }
19125
19303
  return [...records.values()];
19126
19304
  }
19127
- async function createScore(client, schema, args) {
19305
+ async function createScore(client, schema, args, partitioning) {
19128
19306
  const row = scoreRecordToRow(args.score);
19129
- const insert = buildInsert(schema, TABLE_SCORE_EVENTS, [row], scoreRewriteConflict(row));
19130
- if (insert) await client.query(insert.text, insert.values);
19307
+ await insertRows(client, schema, TABLE_SCORE_EVENTS, [row], partitioning, scoreRewriteConflict(row));
19131
19308
  }
19132
- async function batchCreateScores(client, schema, args) {
19309
+ async function batchCreateScores(client, schema, args, partitioning) {
19133
19310
  if (args.scores.length === 0) return;
19134
19311
  const rows = collapseExactScoreConflicts(args.scores.map(scoreRecordToRow));
19135
- const insert = buildInsert(schema, TABLE_SCORE_EVENTS, rows, scoreRewriteConflict(rows[0]));
19136
- if (insert) await client.query(insert.text, insert.values);
19312
+ await insertRows(client, schema, TABLE_SCORE_EVENTS, rows, partitioning, scoreRewriteConflict(rows[0]));
19137
19313
  }
19138
19314
  /**
19139
19315
  * Delete score events by scoreId. Optional `organizationId` and `resourceId`
@@ -19364,7 +19540,15 @@ const TRACE_STATUS_SQL = `CASE WHEN r."error" IS NOT NULL THEN 'error' ELSE 'suc
19364
19540
  const timeToFirstChunkSupported = "timeToFirstChunkMs" in _mastra_core_storage.TRACE_QUERY_FIELD_REGISTRY.trace;
19365
19541
  /** Core before the sort rejects `orderBy` on the field, so the sort is only declared when core plans it. */
19366
19542
  const timeToFirstChunkSortSupported = _mastra_core_storage.TRACE_QUERY_ORDER_FIELDS?.includes("timeToFirstChunkMs") === true;
19367
- const timeToFirstChunkFeatures = [...timeToFirstChunkSupported ? ["trace-query-time-to-first-chunk"] : [], ...timeToFirstChunkSortSupported ? ["trace-query-time-to-first-chunk-sort"] : []];
19543
+ /** Same rule for time to first token: returned and sorted only when core knows the field. */
19544
+ const timeToFirstTokenSupported = "timeToFirstTokenMs" in _mastra_core_storage.TRACE_QUERY_FIELD_REGISTRY.trace;
19545
+ const timeToFirstTokenSortSupported = _mastra_core_storage.TRACE_QUERY_ORDER_FIELDS?.includes("timeToFirstTokenMs") === true;
19546
+ const timingFeatures = [
19547
+ ...timeToFirstChunkSupported ? ["trace-query-time-to-first-chunk"] : [],
19548
+ ...timeToFirstChunkSortSupported ? ["trace-query-time-to-first-chunk-sort"] : [],
19549
+ ...timeToFirstTokenSupported ? ["trace-query-time-to-first-token"] : [],
19550
+ ...timeToFirstTokenSortSupported ? ["trace-query-time-to-first-token-sort"] : []
19551
+ ];
19368
19552
  /**
19369
19553
  * Keyset bounds for a cursor's sort value: terms for rows strictly past it in `direction`, and the
19370
19554
  * condition for rows level with it. Empty values sort last in both directions, so they are past
@@ -19397,6 +19581,7 @@ const TRACE_FIELDS = {
19397
19581
  endedAt: "r.\"endedAt\"",
19398
19582
  durationMs: durationMsSql("r.\"startedAt\"", "r.\"endedAt\""),
19399
19583
  timeToFirstChunkMs: "r.\"timeToFirstChunkMs\"",
19584
+ timeToFirstTokenMs: "r.\"timeToFirstTokenMs\"",
19400
19585
  entityId: "r.\"entityId\"",
19401
19586
  entityName: "r.\"entityName\"",
19402
19587
  entityType: "r.\"entityType\"",
@@ -19415,6 +19600,7 @@ const SPAN_FIELDS = {
19415
19600
  endedAt: "s.\"endedAt\"",
19416
19601
  durationMs: "s.\"durationMs\"",
19417
19602
  timeToFirstChunkMs: "s.\"timeToFirstChunkMs\"",
19603
+ timeToFirstTokenMs: "s.\"timeToFirstTokenMs\"",
19418
19604
  status: "s.\"status\"",
19419
19605
  error: "s.\"error\"",
19420
19606
  entityType: "s.\"entityType\"",
@@ -19485,6 +19671,7 @@ const TRACE_SELECT = `
19485
19671
  r."startedAt" AS "startedAt",
19486
19672
  r."endedAt" AS "endedAt",
19487
19673
  r."timeToFirstChunkMs" AS "timeToFirstChunkMs",
19674
+ r."timeToFirstTokenMs" AS "timeToFirstTokenMs",
19488
19675
  r."entityName" AS "entityName",
19489
19676
  r."entityType" AS "entityType",
19490
19677
  r."environment" AS "environment",
@@ -19854,6 +20041,7 @@ function compilePostgresTraceScope(schema, selection, relationCollections, scope
19854
20041
  ELSE ${durationMsSql("s.\"startedAt\"", "s.\"endedAt\"")}
19855
20042
  END AS "durationMs",
19856
20043
  s."timeToFirstChunkMs",
20044
+ s."timeToFirstTokenMs",
19857
20045
  CASE WHEN s."error" IS NOT NULL THEN 'error' ELSE 'success' END AS "status",
19858
20046
  s."error",
19859
20047
  s."entityType",
@@ -20228,6 +20416,7 @@ function traceRowToResult(row) {
20228
20416
  startedAt: asIsoTimestamp$1(row.startedAt),
20229
20417
  endedAt: asIsoTimestamp$1(row.endedAt),
20230
20418
  ...timeToFirstChunkSupported && { timeToFirstChunkMs: row.timeToFirstChunkMs == null ? null : Number(row.timeToFirstChunkMs) },
20419
+ ...timeToFirstTokenSupported && { timeToFirstTokenMs: row.timeToFirstTokenMs == null ? null : Number(row.timeToFirstTokenMs) },
20231
20420
  entityName: row.entityName == null ? null : String(row.entityName),
20232
20421
  entityType: row.entityType == null ? null : String(row.entityType),
20233
20422
  environment: row.environment == null ? null : String(row.environment),
@@ -20383,7 +20572,8 @@ const fields = [
20383
20572
  "experimentId",
20384
20573
  "environment",
20385
20574
  "executionSource",
20386
- "timeToFirstChunkMs"
20575
+ "timeToFirstChunkMs",
20576
+ "timeToFirstTokenMs"
20387
20577
  ];
20388
20578
  const identities = identityFields.map((field) => `"${field}"`).join(", ");
20389
20579
  const identityOrder = identityFields.map((field) => `"${field}" COLLATE "C" ASC NULLS LAST`).join(", ");
@@ -20515,9 +20705,10 @@ async function querySpans(client, schema, plan, timeoutMs) {
20515
20705
  return runWithPostgresTraceQueryTimeout(client, timeoutMs, async (tx) => {
20516
20706
  const query = compilePostgresSpanQuery(schema, plan);
20517
20707
  const rows = await tx.any(query.text, query.values);
20518
- const selected = rows.map(({ version: _version, startedAt, endedAt, timeToFirstChunkMs, ...row }) => ({
20708
+ const selected = rows.map(({ version: _version, startedAt, endedAt, timeToFirstChunkMs, timeToFirstTokenMs, ...row }) => ({
20519
20709
  ...row,
20520
20710
  ...timeToFirstChunkSupported && { timeToFirstChunkMs },
20711
+ ...timeToFirstTokenSupported && { timeToFirstTokenMs },
20521
20712
  startedAt: startedAt.toISOString(),
20522
20713
  endedAt: endedAt.toISOString()
20523
20714
  }));
@@ -20649,7 +20840,11 @@ function compilePostgresThreadRowsQuery(schema, plan, strategy = "full") {
20649
20840
  }
20650
20841
  const qualify = plan.timeRange.mode === "traces" ? "s.\"inWindow\"" : `s."${plan.timeRange.field}" >= $1 AND s."${plan.timeRange.field}" < $2`;
20651
20842
  const filtered = predicateFields(plan.where);
20652
- const firstContext = [...contextFields, "timeToFirstChunkMs"].filter((field) => field === "resourceId" || filtered.has(field));
20843
+ const firstContext = [
20844
+ ...contextFields,
20845
+ "timeToFirstChunkMs",
20846
+ "timeToFirstTokenMs"
20847
+ ].filter((field) => field === "resourceId" || filtered.has(field));
20653
20848
  const lastFields = [
20654
20849
  "traceId",
20655
20850
  "spanId",
@@ -20663,7 +20858,7 @@ function compilePostgresThreadRowsQuery(schema, plan, strategy = "full") {
20663
20858
  text: `WITH ${scopedRoots}, ${strategy === "candidates" ? `${candidates}, ` : ""}current_roots AS (
20664
20859
  SELECT DISTINCT ON ("organizationId", "traceId")
20665
20860
  "organizationId", "threadId", "traceId", "spanId", "cursorId", "startedAt", "endedAt", "isPending",
20666
- ${firstContext.map((field) => field === "timeToFirstChunkMs" ? `CASE WHEN "spanType" = 'agent_run' THEN "timeToFirstChunkMs" END AS "timeToFirstChunkMs"` : `"${field}"`).join(", ")}
20861
+ ${firstContext.map((field) => field === "timeToFirstChunkMs" || field === "timeToFirstTokenMs" ? `CASE WHEN "spanType" = 'agent_run' THEN "${field}" END AS "${field}"` : `"${field}"`).join(", ")}
20667
20862
  ${filtered.has("status") ? ", CASE WHEN error IS NULL THEN 'success' ELSE 'error' END AS status" : ""}
20668
20863
  FROM scoped_roots ${strategy === "candidates" ? `WHERE (${identity("traceId")}) IN (SELECT ${identity("traceId")} FROM candidate_traces)` : ""}
20669
20864
  ORDER BY "organizationId", "traceId", "startedAt" ASC, "spanId" COLLATE "C", "cursorId" DESC
@@ -20729,6 +20924,7 @@ async function queryThreadRows(client, schema, plan, timeoutMs, strategy = "auto
20729
20924
  }));
20730
20925
  const payloads = await run(`SELECT "organizationId", "traceId", "cursorId", ${contextFields.map((field) => `"${field}"`).join(", ")},
20731
20926
  CASE WHEN "spanType" = 'agent_run' THEN "timeToFirstChunkMs" END AS "timeToFirstChunkMs",
20927
+ CASE WHEN "spanType" = 'agent_run' THEN "timeToFirstTokenMs" END AS "timeToFirstTokenMs",
20732
20928
  CASE WHEN error IS NULL THEN 'success' ELSE 'error' END AS status,
20733
20929
  ${[
20734
20930
  "input",
@@ -20757,6 +20953,7 @@ async function queryThreadRows(client, schema, plan, timeoutMs, strategy = "auto
20757
20953
  endedAt: row.endedAt.toISOString(),
20758
20954
  traceCount: Number(row.traceCount),
20759
20955
  timeToFirstChunkMs: first.timeToFirstChunkMs,
20956
+ ...timeToFirstTokenSupported && { timeToFirstTokenMs: first.timeToFirstTokenMs },
20760
20957
  inputPreview: readPreview(first.inputPreview, first.input),
20761
20958
  outputPreview: readPreview(last.outputPreview, last.output),
20762
20959
  errorPreview: readPreview(last.errorPreview, last.error)
@@ -21713,10 +21910,9 @@ async function readBranchesStreamHeadCursor(client, span, filters) {
21713
21910
  return encodeDeltaCursor(await readSafeXactHorizon(client), 0);
21714
21911
  }
21715
21912
  // src/storage/domains/observability/v-next/tracing.ts
21716
- async function writeSpanRows(client, schema, rows) {
21913
+ async function writeSpanRows(client, schema, rows, partitioning) {
21717
21914
  const deduped = dedupeSpanRows(rows);
21718
- const insert = buildInsert(schema, TABLE_SPAN_EVENTS, deduped, spanConflictClause(Object.keys(deduped[0] ?? {})));
21719
- if (insert) await client.query(insert.text, insert.values);
21915
+ await insertRows(client, schema, TABLE_SPAN_EVENTS, deduped, partitioning, spanConflictClause(Object.keys(deduped[0] ?? {})));
21720
21916
  }
21721
21917
  /**
21722
21918
  * Collapse rows that share a primary key within a single batch. Postgres
@@ -21736,12 +21932,12 @@ function dedupeSpanRows(rows) {
21736
21932
  }
21737
21933
  return [...byKey.values()];
21738
21934
  }
21739
- async function createSpan(client, schema, args) {
21740
- await writeSpanRows(client, schema, [spanRecordToRow(args.span)]);
21935
+ async function createSpan(client, schema, args, partitioning) {
21936
+ await writeSpanRows(client, schema, [spanRecordToRow(args.span)], partitioning);
21741
21937
  }
21742
- async function batchCreateSpans(client, schema, args) {
21938
+ async function batchCreateSpans(client, schema, args, partitioning) {
21743
21939
  if (args.records.length === 0) return;
21744
- await writeSpanRows(client, schema, args.records.map(spanRecordToRow));
21940
+ await writeSpanRows(client, schema, args.records.map(spanRecordToRow), partitioning);
21745
21941
  }
21746
21942
  async function getSpans(client, schema, args) {
21747
21943
  if (args.spanIds.length === 0) return {
@@ -21962,6 +22158,18 @@ var ObservabilityStoragePostgresVNext = class ObservabilityStoragePostgresVNext
21962
22158
  return this.#partitionMode;
21963
22159
  }
21964
22160
  /**
22161
+ * Create any missing day partitions from yesterday through today +
22162
+ * `futureDays`, without the rest of init(). For apps whose runtime role
22163
+ * cannot run DDL: call it on a schedule with a role that owns the
22164
+ * observability tables. Safe to call repeatedly on one instance. Does
22165
+ * nothing with TimescaleDB or pg_partman, which create their own partitions.
22166
+ */
22167
+ async ensurePartitions() {
22168
+ return this.#run("VNEXT_ENSURE_PARTITIONS", async () => {
22169
+ if ((this.#partitionMode ??= await resolveMode(this.#client, this.#partitioning)) === "native") await ensureNativePartitions(this.#client, this.#schema, this.#partitioning);
22170
+ });
22171
+ }
22172
+ /**
21965
22173
  * All five signal tables are insert-only growth tables. The anchor column is
21966
22174
  * each table's partition / chunk key, so age-based expiry drops whole day
21967
22175
  * partitions instead of deleting rows (see `./retention.ts`). `indexed: true`
@@ -22065,7 +22273,7 @@ var ObservabilityStoragePostgresVNext = class ObservabilityStoragePostgresVNext
22065
22273
  "trace-query-tenant-scope",
22066
22274
  "feedback",
22067
22275
  "trace-query-context-ids",
22068
- ...timeToFirstChunkFeatures,
22276
+ ...timingFeatures,
22069
22277
  "trace-query-root-span-fields",
22070
22278
  "trace-query-root-entity-id"
22071
22279
  ];
@@ -22090,11 +22298,23 @@ var ObservabilityStoragePostgresVNext = class ObservabilityStoragePostgresVNext
22090
22298
  "trace-query-tenant-scope",
22091
22299
  "feedback",
22092
22300
  "trace-query-context-ids",
22093
- ...timeToFirstChunkFeatures,
22301
+ ...timingFeatures,
22094
22302
  "trace-query-root-span-fields",
22095
22303
  "trace-query-root-entity-id"
22096
22304
  ];
22097
22305
  }
22306
+ /**
22307
+ * Partitioning options for the write path, with `mode` fixed to the mode
22308
+ * this store resolved (in init(), or lazily in prune()) so the write path
22309
+ * does not detect it again. Until then the configured mode stands in and
22310
+ * the write path resolves it itself when a partition is missing.
22311
+ */
22312
+ #writePartitioning() {
22313
+ return {
22314
+ ...this.#partitioning,
22315
+ mode: this.#partitionMode ?? this.#partitioning.mode
22316
+ };
22317
+ }
22098
22318
  async #run(op, fn, details) {
22099
22319
  try {
22100
22320
  return await fn();
@@ -22103,13 +22323,13 @@ var ObservabilityStoragePostgresVNext = class ObservabilityStoragePostgresVNext
22103
22323
  }
22104
22324
  }
22105
22325
  async createSpan(args) {
22106
- await this.#run("CREATE_SPAN", () => createSpan(this.#client, this.#schema, args), {
22326
+ await this.#run("CREATE_SPAN", () => createSpan(this.#client, this.#schema, args, this.#writePartitioning()), {
22107
22327
  traceId: args.span.traceId,
22108
22328
  spanId: args.span.spanId
22109
22329
  });
22110
22330
  }
22111
22331
  async batchCreateSpans(args) {
22112
- await this.#run("BATCH_CREATE_SPANS", () => batchCreateSpans(this.#client, this.#schema, args), { count: args.records.length });
22332
+ await this.#run("BATCH_CREATE_SPANS", () => batchCreateSpans(this.#client, this.#schema, args, this.#writePartitioning()), { count: args.records.length });
22113
22333
  }
22114
22334
  async getSpan(args) {
22115
22335
  return this.#run("GET_SPAN", () => getSpan(this.#readClient, this.#schema, args), {
@@ -22163,22 +22383,22 @@ var ObservabilityStoragePostgresVNext = class ObservabilityStoragePostgresVNext
22163
22383
  return this.#run("LIST_BRANCHES", () => listBranches(this.#readClient, this.#schema, args));
22164
22384
  }
22165
22385
  async batchCreateLogs(args) {
22166
- await this.#run("BATCH_CREATE_LOGS", () => batchCreateLogs(this.#client, this.#schema, args), { count: args.logs.length });
22386
+ await this.#run("BATCH_CREATE_LOGS", () => batchCreateLogs(this.#client, this.#schema, args, this.#writePartitioning()), { count: args.logs.length });
22167
22387
  }
22168
22388
  async batchCreateMetrics(args) {
22169
- await this.#run("BATCH_CREATE_METRICS", () => batchCreateMetrics(this.#client, this.#schema, args), { count: args.metrics.length });
22389
+ await this.#run("BATCH_CREATE_METRICS", () => batchCreateMetrics(this.#client, this.#schema, args, this.#writePartitioning()), { count: args.metrics.length });
22170
22390
  }
22171
22391
  async createScore(args) {
22172
- await this.#run("CREATE_SCORE", () => createScore(this.#client, this.#schema, args));
22392
+ await this.#run("CREATE_SCORE", () => createScore(this.#client, this.#schema, args, this.#writePartitioning()));
22173
22393
  }
22174
22394
  async batchCreateScores(args) {
22175
- await this.#run("BATCH_CREATE_SCORES", () => batchCreateScores(this.#client, this.#schema, args), { count: args.scores.length });
22395
+ await this.#run("BATCH_CREATE_SCORES", () => batchCreateScores(this.#client, this.#schema, args, this.#writePartitioning()), { count: args.scores.length });
22176
22396
  }
22177
22397
  async createFeedback(args) {
22178
- await this.#run("CREATE_FEEDBACK", () => createFeedback(this.#client, this.#schema, args));
22398
+ await this.#run("CREATE_FEEDBACK", () => createFeedback(this.#client, this.#schema, args, this.#writePartitioning()));
22179
22399
  }
22180
22400
  async batchCreateFeedback(args) {
22181
- await this.#run("BATCH_CREATE_FEEDBACK", () => batchCreateFeedback(this.#client, this.#schema, args), { count: args.feedbacks.length });
22401
+ await this.#run("BATCH_CREATE_FEEDBACK", () => batchCreateFeedback(this.#client, this.#schema, args, this.#writePartitioning()), { count: args.feedbacks.length });
22182
22402
  }
22183
22403
  async deleteScores(args) {
22184
22404
  await this.#run("DELETE_SCORES", () => deleteScores(this.#client, this.#schema, args), { count: args.scoreIds.length });
@@ -27588,12 +27808,20 @@ var PostgresStore = class extends _mastra_core_storage.MastraCompositeStore {
27588
27808
  }
27589
27809
  async #runPinnedInit() {
27590
27810
  let pinnedClient;
27811
+ let pinned;
27591
27812
  try {
27592
27813
  pinnedClient = await this.#writePool.connect();
27593
- const pinned = new PinnedClientAdapter(this.#writePool, pinnedClient);
27814
+ pinned = new PinnedClientAdapter(this.#writePool, pinnedClient);
27594
27815
  this.#db.pin(pinned);
27595
- this.#db.setSchemaSnapshot(await loadSchemaSnapshot(pinned, this.schema));
27596
- await Promise.all(Object.entries(this.stores).filter(([name, store]) => name !== "knowledge" && store).map(([, store]) => store.init()));
27816
+ for (let attempt = 1; attempt <= 3; attempt++) {
27817
+ this.#db.setSchemaSnapshot(await loadSchemaSnapshot(pinned, this.schema));
27818
+ 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");
27819
+ if (failures.length === 0) break;
27820
+ const terminal = failures.find((result) => !isRetryableDeadlock(result.reason));
27821
+ if (terminal || attempt === 3) throw (terminal ?? failures[0]).reason;
27822
+ this.#db.setSchemaSnapshot(null);
27823
+ await backoffAfterDeadlock(attempt);
27824
+ }
27597
27825
  this.isInitialized = true;
27598
27826
  } catch (error) {
27599
27827
  this.#initPromise = null;
@@ -27604,6 +27832,7 @@ var PostgresStore = class extends _mastra_core_storage.MastraCompositeStore {
27604
27832
  category: _mastra_core_error.ErrorCategory.THIRD_PARTY
27605
27833
  }, error);
27606
27834
  } finally {
27835
+ await pinned?.drain();
27607
27836
  this.#db.setSchemaSnapshot(null);
27608
27837
  if (pinnedClient) {
27609
27838
  this.#db.unpin();
@@ -27758,6 +27987,10 @@ var PostgresStoreVNext = class extends PostgresStore {
27758
27987
  owned: true
27759
27988
  };
27760
27989
  }
27990
+ /** See {@link ObservabilityStoragePostgresVNext.ensurePartitions}. */
27991
+ async ensurePartitions() {
27992
+ await this.stores.observability.ensurePartitions();
27993
+ }
27761
27994
  /**
27762
27995
  * Closes both the primary pool (when owned) and the observability pool
27763
27996
  * (when this store created it). Safe to call multiple times.