@mastra/pg 1.19.0-alpha.0 → 1.19.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # @mastra/pg
2
2
 
3
+ ## 1.19.0-alpha.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Improved PostgresStore startup: init now reads the schema catalog up front (3 read-only queries) instead of issuing hundreds of per-object existence checks and no-op DDL statements. On an already-migrated database this cuts init from ~350 serialized queries to 6, dropping init time on a 50ms connection from ~18.5s to ~0.5s. Fixed init failing for roles without CREATE privileges when the schema already exists, and removed a table lock that could block writers while init re-created triggers that were already in place. Fresh and partially-migrated databases are set up exactly as before. ([#20394](https://github.com/mastra-ai/mastra/pull/20394))
8
+
9
+ - Updated dependencies [[`594f7b2`](https://github.com/mastra-ai/mastra/commit/594f7b28f5263fb9982fd50d95c471fb971ea984), [`311f943`](https://github.com/mastra-ai/mastra/commit/311f943bee60e8fdf5c84499ea50e884276c936c), [`0c89896`](https://github.com/mastra-ai/mastra/commit/0c8989673fb7d106837098398131e570c6023b68), [`23b4238`](https://github.com/mastra-ai/mastra/commit/23b423844ad0bcf2a502a68dd62866d6160f9f6d), [`e320a76`](https://github.com/mastra-ai/mastra/commit/e320a763feaf65c6be3cebecf746defcbde161b3), [`03b4918`](https://github.com/mastra-ai/mastra/commit/03b4918c80d188ce375334c393e131c6e94bd7eb), [`14ef73a`](https://github.com/mastra-ai/mastra/commit/14ef73a4bbd73e7808414816eb0628ce1d80b5d7), [`1d677d5`](https://github.com/mastra-ai/mastra/commit/1d677d5f99d7db403f7828585e8c25f299f72628), [`93e28ec`](https://github.com/mastra-ai/mastra/commit/93e28ecce9031c02397e0ae8406593e5c7a95883), [`729dab4`](https://github.com/mastra-ai/mastra/commit/729dab408faccfaef0cbb048e5a4338f9172847e), [`484003d`](https://github.com/mastra-ai/mastra/commit/484003d33ff59330c86b19863e4a38732d7e4155), [`933d291`](https://github.com/mastra-ai/mastra/commit/933d291146b789c19442ad206f94da3e4be90c64)]:
10
+ - @mastra/core@1.56.0-alpha.3
11
+
3
12
  ## 1.19.0-alpha.0
4
13
 
5
14
  ### Minor Changes
@@ -3,7 +3,7 @@ name: mastra-pg
3
3
  description: Documentation for @mastra/pg. Use when working with @mastra/pg APIs, configuration, or implementation.
4
4
  metadata:
5
5
  package: "@mastra/pg"
6
- version: "1.19.0-alpha.0"
6
+ version: "1.19.0-alpha.1"
7
7
  ---
8
8
 
9
9
  ## When to use
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.19.0-alpha.0",
2
+ "version": "1.19.0-alpha.1",
3
3
  "package": "@mastra/pg",
4
4
  "exports": {},
5
5
  "modules": {}
package/dist/index.cjs CHANGED
@@ -1971,9 +1971,25 @@ var PinnedClientAdapter = class {
1971
1971
  var RoutingDbClient = class {
1972
1972
  #base;
1973
1973
  #pinned = null;
1974
+ #schemaSnapshot = null;
1974
1975
  constructor(base) {
1975
1976
  this.#base = base;
1976
1977
  }
1978
+ /**
1979
+ * Catalog snapshot for the current init window, or `null` outside it.
1980
+ *
1981
+ * It lives here rather than on `PgDB` because every storage domain builds its
1982
+ * own `PgDB` over this one shared client — hanging the snapshot off the
1983
+ * client means one load serves all of them, and its lifetime lines up exactly
1984
+ * with the pinned-init window that `pin()`/`unpin()` already delimit.
1985
+ */
1986
+ get schemaSnapshot() {
1987
+ return this.#schemaSnapshot;
1988
+ }
1989
+ /** Install (or clear, with `null`) the init-window catalog snapshot. */
1990
+ setSchemaSnapshot(snapshot) {
1991
+ this.#schemaSnapshot = snapshot;
1992
+ }
1977
1993
  /** Returns the currently active client (pinned if set, otherwise base). */
1978
1994
  get active() {
1979
1995
  return this.#pinned ?? this.#base;
@@ -2074,6 +2090,67 @@ function isDuplicateSchemaError(error) {
2074
2090
  return /schema .* already exists/i.test(message);
2075
2091
  }
2076
2092
  //#endregion
2093
+ //#region src/storage/db/schema-snapshot.ts
2094
+ /**
2095
+ * Returns the snapshot currently installed on `client`, but only when it was
2096
+ * taken from the schema the caller operates on. A store configured for another
2097
+ * schema (or a client with no snapshot at all) gets `null` and falls back to
2098
+ * probing the live catalog.
2099
+ */
2100
+ function getSchemaSnapshot(client, schemaName) {
2101
+ const snapshot = client?.schemaSnapshot;
2102
+ if (!snapshot) return null;
2103
+ return snapshot.schemaName === (schemaName || "public") ? snapshot : null;
2104
+ }
2105
+ /**
2106
+ * Reads the catalog for `schemaName` in three queries. Must be called on the
2107
+ * pinned init client so the snapshot reflects what that connection will see.
2108
+ */
2109
+ async function loadSchemaSnapshot(client, schemaName) {
2110
+ const schema = schemaName || "public";
2111
+ const [tableRows, columnRows, indexRows] = await Promise.all([
2112
+ client.manyOrNone(`SELECT tablename FROM pg_catalog.pg_tables WHERE schemaname = $1`, [schema]),
2113
+ client.manyOrNone(`SELECT c.relname AS table_name, a.attname AS column_name
2114
+ FROM pg_catalog.pg_class c
2115
+ JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
2116
+ JOIN pg_catalog.pg_attribute a ON a.attrelid = c.oid
2117
+ WHERE n.nspname = $1
2118
+ AND c.relkind IN ('r', 'p')
2119
+ AND a.attnum > 0
2120
+ AND NOT a.attisdropped`, [schema]),
2121
+ client.manyOrNone(`SELECT c.relname AS indexname, i.indisreplident AS is_replica_identity, i.indisprimary AS is_primary
2122
+ FROM pg_catalog.pg_index i
2123
+ JOIN pg_catalog.pg_class c ON c.oid = i.indexrelid
2124
+ JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
2125
+ WHERE n.nspname = $1`, [schema])
2126
+ ]);
2127
+ const columns = /* @__PURE__ */ new Map();
2128
+ for (const row of columnRows) {
2129
+ let set = columns.get(row.table_name);
2130
+ if (!set) {
2131
+ set = /* @__PURE__ */ new Set();
2132
+ columns.set(row.table_name, set);
2133
+ }
2134
+ set.add(row.column_name);
2135
+ }
2136
+ const indexes = /* @__PURE__ */ new Set();
2137
+ const replicaIdentityIndexes = /* @__PURE__ */ new Set();
2138
+ const primaryKeyIndexes = /* @__PURE__ */ new Set();
2139
+ for (const row of indexRows) {
2140
+ indexes.add(row.indexname);
2141
+ if (row.is_replica_identity) replicaIdentityIndexes.add(row.indexname);
2142
+ if (row.is_primary) primaryKeyIndexes.add(row.indexname.toLowerCase());
2143
+ }
2144
+ return {
2145
+ schemaName: schema,
2146
+ tables: new Set(tableRows.map((r) => r.tablename)),
2147
+ columns,
2148
+ indexes,
2149
+ replicaIdentityIndexes,
2150
+ primaryKeyIndexes
2151
+ };
2152
+ }
2153
+ //#endregion
2077
2154
  //#region src/storage/db/index.ts
2078
2155
  /**
2079
2156
  * Resolves PgDomainConfig to a database client and schema.
@@ -2253,7 +2330,8 @@ function generateTimestampTriggerSQL(tableName, schemaName) {
2253
2330
  schemaName: quotedSchemaName
2254
2331
  });
2255
2332
  const functionName = `${quotedSchemaName}.trigger_set_timestamps`;
2256
- const triggerName = `"${(0, _mastra_core_utils.parseSqlIdentifier)(`${tableName}_timestamps`, "trigger name")}"`;
2333
+ const parsedTriggerName = (0, _mastra_core_utils.parseSqlIdentifier)(`${tableName}_timestamps`, "trigger name");
2334
+ const triggerName = `"${parsedTriggerName}"`;
2257
2335
  return `CREATE OR REPLACE FUNCTION ${functionName}()
2258
2336
  RETURNS TRIGGER AS $$
2259
2337
  BEGIN
@@ -2272,12 +2350,38 @@ BEGIN
2272
2350
  END;
2273
2351
  $$ LANGUAGE plpgsql;
2274
2352
 
2275
- DROP TRIGGER IF EXISTS ${triggerName} ON ${fullTableName};
2353
+ DO $mastra_timestamps_trigger$
2354
+ BEGIN
2355
+ -- Recreating the trigger unconditionally would take an ACCESS EXCLUSIVE
2356
+ -- lock on the table (DROP TRIGGER does, even when nothing changes), and
2357
+ -- init runs on every process start. Skip when the trigger is already
2358
+ -- exactly what the CREATE below would produce.
2359
+ --
2360
+ -- tgtype 23 = ROW (1) | BEFORE (2) | INSERT (4) | UPDATE (16), so a trigger
2361
+ -- whose timing or events differ still falls through and gets rebuilt. The
2362
+ -- behaviour itself lives in the function, which is replaced above on every
2363
+ -- init, so an upgraded function body lands without touching the trigger.
2364
+ IF NOT EXISTS (
2365
+ SELECT 1
2366
+ FROM pg_catalog.pg_trigger tg
2367
+ JOIN pg_catalog.pg_class c ON c.oid = tg.tgrelid
2368
+ JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
2369
+ WHERE tg.tgname = ${`'${parsedTriggerName}'`}
2370
+ AND c.relname = ${`'${(0, _mastra_core_utils.parseSqlIdentifier)(tableName, "table name")}'`}
2371
+ AND n.nspname = ${schemaName ? `'${(0, _mastra_core_utils.parseSqlIdentifier)(schemaName, "schema name")}'` : `'public'`}
2372
+ AND NOT tg.tgisinternal
2373
+ AND tg.tgtype = 23
2374
+ AND tg.tgfoid = '${functionName}()'::regprocedure
2375
+ ) THEN
2376
+ DROP TRIGGER IF EXISTS ${triggerName} ON ${fullTableName};
2276
2377
 
2277
- CREATE TRIGGER ${triggerName}
2278
- BEFORE INSERT OR UPDATE ON ${fullTableName}
2279
- FOR EACH ROW
2280
- EXECUTE FUNCTION ${functionName}();`;
2378
+ CREATE TRIGGER ${triggerName}
2379
+ BEFORE INSERT OR UPDATE ON ${fullTableName}
2380
+ FOR EACH ROW
2381
+ EXECUTE FUNCTION ${functionName}();
2382
+ END IF;
2383
+ END
2384
+ $mastra_timestamps_trigger$;`;
2281
2385
  }
2282
2386
  const schemaSetupRegistry = /* @__PURE__ */ new Map();
2283
2387
  /**
@@ -2304,6 +2408,99 @@ var PgDB = class extends _mastra_core_base.MastraBase {
2304
2408
  this.skipDefaultIndexes = config.skipDefaultIndexes;
2305
2409
  }
2306
2410
  /**
2411
+ * Catalog snapshot for the current init window, or `null` outside it.
2412
+ *
2413
+ * When non-null, the init-path methods below answer existence questions from
2414
+ * it instead of round-tripping to the server, and record the objects they
2415
+ * create so later callers in the same init see them. See
2416
+ * {@link SchemaSnapshot} for why it is scoped to init only.
2417
+ */
2418
+ get schemaSnapshot() {
2419
+ return getSchemaSnapshot(this.client, this.schemaName);
2420
+ }
2421
+ /**
2422
+ * Whether the snapshot proves `generateTableSQL` would be a no-op for this
2423
+ * table — i.e. the CREATE statement can be skipped.
2424
+ *
2425
+ * For most tables that is just "the table exists". `workflow_snapshot` is the
2426
+ * exception: its generated SQL also carries a DO block that back-fills the
2427
+ * `(workflow_name, run_id)` unique constraint and promotes it to the table's
2428
+ * replica identity, so a table created by an older version still needs the
2429
+ * statement to run.
2430
+ */
2431
+ snapshotShowsTableConverged(snapshot, tableName) {
2432
+ if (!snapshot.tables.has(tableName)) return false;
2433
+ if (tableName === _mastra_core_storage.TABLE_WORKFLOW_SNAPSHOT) {
2434
+ const constraintName = buildConstraintName({
2435
+ baseName: "mastra_workflow_snapshot_workflow_name_run_id_key",
2436
+ schemaName: this.schemaName ? (0, _mastra_core_utils.parseSqlIdentifier)(this.schemaName, "schema name") : void 0
2437
+ }).toLowerCase();
2438
+ return snapshot.indexes.has(constraintName) && snapshot.replicaIdentityIndexes.has(constraintName);
2439
+ }
2440
+ return true;
2441
+ }
2442
+ /** Column set for `tableName` in the snapshot, created empty if absent. */
2443
+ snapshotColumns(snapshot, tableName) {
2444
+ let columns = snapshot.columns.get(tableName);
2445
+ if (!columns) {
2446
+ columns = /* @__PURE__ */ new Set();
2447
+ snapshot.columns.set(tableName, columns);
2448
+ }
2449
+ return columns;
2450
+ }
2451
+ /**
2452
+ * Records an out-of-band `ALTER TABLE … RENAME TO` in the init snapshot.
2453
+ *
2454
+ * Init-time migrations that issue raw DDL on `this.client` (instead of going
2455
+ * through createTable/alterTable/createIndex, which maintain the snapshot
2456
+ * themselves) MUST report it through these `note*` methods. A snapshot that
2457
+ * still lists a renamed-away table makes a later createTable() in the same
2458
+ * init skip the rebuild the migration depends on — stranding data. No-op
2459
+ * outside the init window.
2460
+ *
2461
+ * Indexes riding along with a rename keep their names, so the snapshot's
2462
+ * index set stays accurate without changes here.
2463
+ */
2464
+ noteTableRenamed(oldName, newName) {
2465
+ const snapshot = this.schemaSnapshot;
2466
+ if (snapshot) {
2467
+ if (snapshot.tables.delete(oldName)) snapshot.tables.add(newName);
2468
+ const columns = snapshot.columns.get(oldName);
2469
+ if (columns) {
2470
+ snapshot.columns.delete(oldName);
2471
+ snapshot.columns.set(newName, columns);
2472
+ }
2473
+ }
2474
+ this.tableColumnsCache.delete(oldName);
2475
+ this.tableColumnsCache.delete(newName);
2476
+ }
2477
+ /**
2478
+ * Records an out-of-band `DROP TABLE` in the init snapshot. See
2479
+ * {@link noteTableRenamed} for why raw-DDL migrations must call this.
2480
+ *
2481
+ * The dropped table's indexes vanish with it, but the snapshot's flat index
2482
+ * set cannot map names back to tables. Stale entries only make a later
2483
+ * createIndex() skip a recreate until the next init re-reads the catalog —
2484
+ * the same self-healing bound the rest of the snapshot design accepts.
2485
+ */
2486
+ noteTableDropped(tableName) {
2487
+ const snapshot = this.schemaSnapshot;
2488
+ if (snapshot) {
2489
+ snapshot.tables.delete(tableName);
2490
+ snapshot.columns.delete(tableName);
2491
+ }
2492
+ this.tableColumnsCache.delete(tableName);
2493
+ }
2494
+ /**
2495
+ * Records an out-of-band `ALTER TABLE … ADD COLUMN` in the init snapshot.
2496
+ * See {@link noteTableRenamed} for why raw-DDL migrations must call this.
2497
+ */
2498
+ noteColumnAdded(tableName, column) {
2499
+ const snapshot = this.schemaSnapshot;
2500
+ if (snapshot) this.snapshotColumns(snapshot, tableName).add(column);
2501
+ this.tableColumnsCache.delete(tableName);
2502
+ }
2503
+ /**
2307
2504
  * Gets the set of column names that actually exist in the database table.
2308
2505
  * Results are cached; the cache is invalidated when alterTable() adds new columns.
2309
2506
  */
@@ -2330,6 +2527,12 @@ var PgDB = class extends _mastra_core_base.MastraBase {
2330
2527
  }
2331
2528
  async hasColumn(table, column) {
2332
2529
  const schema = this.schemaName || "public";
2530
+ const snapshot = this.schemaSnapshot;
2531
+ if (snapshot) {
2532
+ const columns = snapshot.columns.get(table);
2533
+ if (!columns) return false;
2534
+ return columns.has(column) || columns.has(column.toLowerCase());
2535
+ }
2333
2536
  return !!await this.client.oneOrNone(`SELECT 1 FROM information_schema.columns WHERE table_schema = $1 AND table_name = $2 AND (column_name = $3 OR column_name = $4)`, [
2334
2537
  schema,
2335
2538
  table,
@@ -2368,6 +2571,14 @@ var PgDB = class extends _mastra_core_base.MastraBase {
2368
2571
  if (!this.schemaName) return;
2369
2572
  let registryEntry = schemaSetupRegistry.get(this.schemaName);
2370
2573
  if (registryEntry?.complete) return;
2574
+ const snapshot = this.schemaSnapshot;
2575
+ if (snapshot && snapshot.tables.size > 0) {
2576
+ schemaSetupRegistry.set(this.schemaName, {
2577
+ promise: null,
2578
+ complete: true
2579
+ });
2580
+ return;
2581
+ }
2371
2582
  const quotedSchemaName = getSchemaName$6(this.schemaName);
2372
2583
  if (!registryEntry?.promise) {
2373
2584
  const schemaNameCapture = this.schemaName;
@@ -2472,16 +2683,28 @@ var PgDB = class extends _mastra_core_base.MastraBase {
2472
2683
  try {
2473
2684
  const timeZColumnNames = Object.entries(schema).filter(([_, def]) => def.type === "timestamp").map(([name]) => name);
2474
2685
  if (this.schemaName) await this.setupSchema();
2475
- const sql = generateTableSQL({
2476
- tableName,
2477
- schema,
2478
- schemaName: this.schemaName,
2479
- compositePrimaryKey
2480
- });
2481
- try {
2482
- await this.client.none(sql);
2483
- } catch (error) {
2484
- if (!isDuplicateRelationError(error)) throw error;
2686
+ const snapshot = this.schemaSnapshot;
2687
+ if (!snapshot || !this.snapshotShowsTableConverged(snapshot, tableName)) {
2688
+ const sql = generateTableSQL({
2689
+ tableName,
2690
+ schema,
2691
+ schemaName: this.schemaName,
2692
+ compositePrimaryKey
2693
+ });
2694
+ try {
2695
+ await this.client.none(sql);
2696
+ } catch (error) {
2697
+ if (!isDuplicateRelationError(error)) throw error;
2698
+ }
2699
+ if (snapshot) {
2700
+ snapshot.tables.add(tableName);
2701
+ const created = this.snapshotColumns(snapshot, tableName);
2702
+ for (const [name, def] of Object.entries(schema)) {
2703
+ const parsedName = (0, _mastra_core_utils.parseSqlIdentifier)(name, "column name");
2704
+ created.add(parsedName);
2705
+ if (def.type === "timestamp") created.add(`${parsedName}Z`);
2706
+ }
2707
+ }
2485
2708
  }
2486
2709
  await this.alterTable({
2487
2710
  tableName,
@@ -2547,10 +2770,12 @@ MIGRATION REQUIRED: Duplicate spans detected in ${duplicateInfo.tableName}\n====
2547
2770
  const sqlType = mapToSqlType(columnDef.type);
2548
2771
  const alterSql = `ALTER TABLE ${fullTableName} ADD COLUMN IF NOT EXISTS "${parsedColumnName}" ${sqlType} ${columnDef.nullable ? "" : "NOT NULL"} ${!columnDef.nullable ? this.getDefaultValue(columnDef.type) : ""}`.trim();
2549
2772
  await this.client.none(alterSql);
2773
+ this.noteColumnAdded(_mastra_core_storage.TABLE_SPANS, columnName);
2550
2774
  this.logger?.debug?.(`Added column '${columnName}' to ${fullTableName}`);
2551
2775
  if (sqlType === "TIMESTAMP") {
2552
2776
  const timestampZSql = `ALTER TABLE ${fullTableName} ADD COLUMN IF NOT EXISTS "${parsedColumnName}Z" TIMESTAMPTZ DEFAULT NOW()`.trim();
2553
2777
  await this.client.none(timestampZSql);
2778
+ this.noteColumnAdded(_mastra_core_storage.TABLE_SPANS, `${columnName}Z`);
2554
2779
  this.logger?.debug?.(`Added timezone column '${columnName}Z' to ${fullTableName}`);
2555
2780
  }
2556
2781
  }
@@ -2559,6 +2784,7 @@ MIGRATION REQUIRED: Duplicate spans detected in ${duplicateInfo.tableName}\n====
2559
2784
  if (!await this.hasColumn(_mastra_core_storage.TABLE_SPANS, tzColumnName)) {
2560
2785
  const timestampZSql = `ALTER TABLE ${fullTableName} ADD COLUMN IF NOT EXISTS "${(0, _mastra_core_utils.parseSqlIdentifier)(tzColumnName, "column name")}" TIMESTAMPTZ DEFAULT NOW()`.trim();
2561
2786
  await this.client.none(timestampZSql);
2787
+ this.noteColumnAdded(_mastra_core_storage.TABLE_SPANS, tzColumnName);
2562
2788
  this.logger?.debug?.(`Added timezone column '${tzColumnName}' to ${fullTableName}`);
2563
2789
  }
2564
2790
  }
@@ -2681,6 +2907,12 @@ MIGRATION REQUIRED: Duplicate spans detected in ${duplicateInfo.tableName}\n====
2681
2907
  schemaName: (this.schemaName ? (0, _mastra_core_utils.parseSqlIdentifier)(this.schemaName, "schema name") : "") || void 0
2682
2908
  });
2683
2909
  const schemaFilter = this.schemaName || "public";
2910
+ const snapshot = this.schemaSnapshot;
2911
+ if (snapshot) return snapshot.primaryKeyIndexes.has(constraintName.toLowerCase());
2912
+ return this.spansPrimaryKeyExistsLive(constraintName, schemaFilter);
2913
+ }
2914
+ /** Live-catalog variant of {@link spansPrimaryKeyExists}, bypassing the snapshot. */
2915
+ async spansPrimaryKeyExistsLive(constraintName, schemaFilter) {
2684
2916
  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;
2685
2917
  }
2686
2918
  /**
@@ -2711,10 +2943,12 @@ MIGRATION REQUIRED: Duplicate spans detected in ${duplicateInfo.tableName}\n====
2711
2943
  ADD CONSTRAINT ${constraintName}
2712
2944
  PRIMARY KEY ("traceId", "spanId")
2713
2945
  `);
2946
+ this.schemaSnapshot?.primaryKeyIndexes.add(constraintName.toLowerCase());
2714
2947
  this.logger?.info?.(`Added PRIMARY KEY constraint ${constraintName} to ${fullTableName}`);
2715
2948
  } catch (error) {
2716
2949
  if (isDuplicateRelationError(error)) {
2717
- if (await this.spansPrimaryKeyExists()) {
2950
+ if (await this.spansPrimaryKeyExistsLive(constraintName, schemaFilter)) {
2951
+ this.schemaSnapshot?.primaryKeyIndexes.add(constraintName.toLowerCase());
2718
2952
  this.logger?.debug?.(`PRIMARY KEY constraint ${constraintName} was created by another process`);
2719
2953
  return;
2720
2954
  }
@@ -2796,16 +3030,25 @@ MIGRATION REQUIRED: Duplicate spans detected in ${duplicateInfo.tableName}\n====
2796
3030
  indexName: tableName,
2797
3031
  schemaName: getSchemaName$6(this.schemaName)
2798
3032
  });
3033
+ const snapshot = this.schemaSnapshot;
3034
+ const knownColumns = snapshot ? this.snapshotColumns(snapshot, tableName) : null;
2799
3035
  try {
2800
3036
  for (const columnName of ifNotExists) if (schema[columnName]) {
2801
3037
  const columnDef = schema[columnName];
2802
3038
  const parsedColumnName = (0, _mastra_core_utils.parseSqlIdentifier)(columnName, "column name");
2803
3039
  const sqlType = mapToSqlType(columnDef.type);
2804
3040
  const alterSql = `ALTER TABLE ${fullTableName} ADD COLUMN IF NOT EXISTS "${parsedColumnName}" ${sqlType} ${columnDef.nullable ? "" : "NOT NULL"} ${!columnDef.nullable ? this.getDefaultValue(columnDef.type) : ""}`.trim();
2805
- await this.client.none(alterSql);
3041
+ if (!knownColumns?.has(parsedColumnName)) {
3042
+ await this.client.none(alterSql);
3043
+ knownColumns?.add(parsedColumnName);
3044
+ }
2806
3045
  if (sqlType === "TIMESTAMP") {
2807
- const timestampZSql = `ALTER TABLE ${fullTableName} ADD COLUMN IF NOT EXISTS "${parsedColumnName}Z" TIMESTAMPTZ DEFAULT NOW()`.trim();
2808
- await this.client.none(timestampZSql);
3046
+ const tzColumnName = `${parsedColumnName}Z`;
3047
+ if (!knownColumns?.has(tzColumnName)) {
3048
+ const timestampZSql = `ALTER TABLE ${fullTableName} ADD COLUMN IF NOT EXISTS "${tzColumnName}" TIMESTAMPTZ DEFAULT NOW()`.trim();
3049
+ await this.client.none(timestampZSql);
3050
+ knownColumns?.add(tzColumnName);
3051
+ }
2809
3052
  }
2810
3053
  this.logger?.debug?.(`Ensured column ${parsedColumnName} exists in table ${fullTableName}`);
2811
3054
  }
@@ -2891,7 +3134,10 @@ MIGRATION REQUIRED: Duplicate spans detected in ${duplicateInfo.tableName}\n====
2891
3134
  indexName: table,
2892
3135
  schemaName: getSchemaName$6(this.schemaName)
2893
3136
  });
2894
- if (await this.client.oneOrNone(`SELECT 1 FROM pg_indexes
3137
+ const snapshot = this.schemaSnapshot;
3138
+ if (snapshot) {
3139
+ if (snapshot.indexes.has(name)) return;
3140
+ } else if (await this.client.oneOrNone(`SELECT 1 FROM pg_indexes
2895
3141
  WHERE indexname = $1
2896
3142
  AND schemaname = $2`, [name, schemaName])) return;
2897
3143
  const uniqueStr = unique ? "UNIQUE " : "";
@@ -2913,6 +3159,7 @@ MIGRATION REQUIRED: Duplicate spans detected in ${duplicateInfo.tableName}\n====
2913
3159
  if (storage && Object.keys(storage).length > 0) withStr = ` WITH (${Object.entries(storage).map(([key, value]) => `${key} = ${value}`).join(", ")})`;
2914
3160
  const sql = `CREATE ${uniqueStr}INDEX ${concurrentStr}${`"${(0, _mastra_core_utils.parseSqlIdentifier)(name, "index name")}"`} ON ${fullTableName} ${methodStr}(${columnsStr})${withStr}${tablespaceStr}${whereStr}`;
2915
3161
  await this.client.none(sql);
3162
+ snapshot?.indexes.add(name);
2916
3163
  } catch (error) {
2917
3164
  if (error instanceof Error && error.message.includes("CONCURRENTLY")) {
2918
3165
  const retryOptions = {
@@ -2932,15 +3179,34 @@ MIGRATION REQUIRED: Duplicate spans detected in ${duplicateInfo.tableName}\n====
2932
3179
  }, error);
2933
3180
  }
2934
3181
  }
3182
+ /**
3183
+ * Runs a caller-built `CREATE INDEX IF NOT EXISTS` statement, unless the init
3184
+ * snapshot already proves `indexName` exists.
3185
+ *
3186
+ * `createIndex` covers the indexes described by {@link CreateIndexOptions};
3187
+ * this is for the two init paths that hand-write their statement (a partial
3188
+ * or otherwise non-standard index) and would otherwise send a no-op DDL on
3189
+ * every warm init.
3190
+ */
3191
+ async createIndexFromStatement(indexName, sql) {
3192
+ const snapshot = this.schemaSnapshot;
3193
+ if (snapshot?.indexes.has(indexName)) return;
3194
+ await this.client.none(sql);
3195
+ snapshot?.indexes.add(indexName);
3196
+ }
2935
3197
  async dropIndex(indexName) {
2936
3198
  try {
2937
3199
  const schemaName = this.schemaName || "public";
2938
- if (!await this.client.oneOrNone(`SELECT 1 FROM pg_indexes
3200
+ const snapshot = this.schemaSnapshot;
3201
+ if (snapshot) {
3202
+ if (!snapshot.indexes.has(indexName)) return;
3203
+ } else if (!await this.client.oneOrNone(`SELECT 1 FROM pg_indexes
2939
3204
  WHERE indexname = $1
2940
3205
  AND schemaname = $2`, [indexName, schemaName])) return;
2941
3206
  const quotedIndexName = `"${(0, _mastra_core_utils.parseSqlIdentifier)(indexName, "index name")}"`;
2942
3207
  const sql = `DROP INDEX IF EXISTS ${getSchemaName$6(this.schemaName)}.${quotedIndexName}`;
2943
3208
  await this.client.none(sql);
3209
+ snapshot?.indexes.delete(indexName);
2944
3210
  } catch (error) {
2945
3211
  throw new _mastra_core_error.MastraError({
2946
3212
  id: (0, _mastra_core_storage.createStorageErrorId)("PG", "INDEX_DROP", "FAILED"),
@@ -3437,7 +3703,9 @@ var AgentsPG = class AgentsPG extends _mastra_core_storage.AgentsStorage {
3437
3703
  });
3438
3704
  if (await this.#db.hasColumn(_mastra_core_storage.TABLE_AGENTS, "name")) {
3439
3705
  await this.#db.client.none(`ALTER TABLE ${fullTableName} RENAME TO "${_mastra_core_storage.TABLE_AGENTS}_legacy"`);
3706
+ this.#db.noteTableRenamed(_mastra_core_storage.TABLE_AGENTS, `${_mastra_core_storage.TABLE_AGENTS}_legacy`);
3440
3707
  await this.#db.client.none(`DROP TABLE IF EXISTS ${fullVersionsTableName}`);
3708
+ this.#db.noteTableDropped(_mastra_core_storage.TABLE_AGENT_VERSIONS);
3441
3709
  }
3442
3710
  if (!await this.#db.hasColumn(`${_mastra_core_storage.TABLE_AGENTS}_legacy`, "name")) return;
3443
3711
  const oldAgents = await this.#db.client.manyOrNone(`SELECT * FROM ${legacyTableName}`);
@@ -3494,6 +3762,7 @@ var AgentsPG = class AgentsPG extends _mastra_core_storage.AgentsStorage {
3494
3762
  ]);
3495
3763
  }
3496
3764
  await this.#db.client.none(`DROP TABLE IF EXISTS ${legacyTableName}`);
3765
+ this.#db.noteTableDropped(`${_mastra_core_storage.TABLE_AGENTS}_legacy`);
3497
3766
  }
3498
3767
  /**
3499
3768
  * Migrates the agent_versions table from the old snapshot-based schema (single `snapshot` JSON column)
@@ -3511,7 +3780,9 @@ var AgentsPG = class AgentsPG extends _mastra_core_storage.AgentsStorage {
3511
3780
  schemaName: getSchemaName$5(this.#schema)
3512
3781
  });
3513
3782
  await this.#db.client.none(`DROP TABLE IF EXISTS ${fullVersionsTableName}`);
3783
+ this.#db.noteTableDropped(_mastra_core_storage.TABLE_AGENT_VERSIONS);
3514
3784
  await this.#db.client.none(`DROP TABLE IF EXISTS ${legacyTableName}`);
3785
+ this.#db.noteTableDropped(`${_mastra_core_storage.TABLE_AGENTS}_legacy`);
3515
3786
  }
3516
3787
  /**
3517
3788
  * Migrates the tools field from string[] format to JSONB format { "tool-key": { "description": "..." } }.
@@ -5039,6 +5310,7 @@ var DatasetsPG = class DatasetsPG extends _mastra_core_storage.DatasetsStorage {
5039
5310
  schemaName: getSchemaName$5(this.#schema)
5040
5311
  });
5041
5312
  await this.#db.client.none(`ALTER TABLE ${fullTableName} ADD COLUMN "${column}" ${sqlType}`);
5313
+ this.#db.noteColumnAdded(table, column);
5042
5314
  }
5043
5315
  }
5044
5316
  getDefaultIndexDefinitions() {
@@ -6908,7 +7180,7 @@ var FavoritesPG = class FavoritesPG extends _mastra_core_storage.FavoritesStorag
6908
7180
  indexName: _mastra_core_storage.TABLE_FAVORITES,
6909
7181
  schemaName: getSchemaName$5(this.#schema)
6910
7182
  });
6911
- await this.#db.client.none(`CREATE INDEX IF NOT EXISTS idx_favorites_entity ON ${fullTableName} ("entityType", "entityId")`);
7183
+ await this.#db.createIndexFromStatement("idx_favorites_entity", `CREATE INDEX IF NOT EXISTS idx_favorites_entity ON ${fullTableName} ("entityType", "entityId")`);
6912
7184
  }
6913
7185
  async dangerouslyClearAll() {
6914
7186
  const fullTableName = getTableName$5({
@@ -8404,7 +8676,7 @@ var MemoryPG = class MemoryPG extends _mastra_core_storage.MemoryStorage {
8404
8676
  indexName: OM_TABLE,
8405
8677
  schemaName: getSchemaName$3(this.#schema)
8406
8678
  });
8407
- await this.#db.client.none(`CREATE INDEX IF NOT EXISTS idx_om_lookup_key ON ${omTableName} ("lookupKey")`);
8679
+ await this.#db.createIndexFromStatement("idx_om_lookup_key", `CREATE INDEX IF NOT EXISTS idx_om_lookup_key ON ${omTableName} ("lookupKey")`);
8408
8680
  }
8409
8681
  await this.createDefaultIndexes();
8410
8682
  await this.createCustomIndexes();
@@ -20125,6 +20397,7 @@ var PostgresStore = class extends _mastra_core_storage.MastraCompositeStore {
20125
20397
  pinnedClient = await this.#pool.connect();
20126
20398
  const pinned = new PinnedClientAdapter(this.#pool, pinnedClient);
20127
20399
  this.#db.pin(pinned);
20400
+ this.#db.setSchemaSnapshot(await loadSchemaSnapshot(pinned, this.schema));
20128
20401
  await super.init();
20129
20402
  this.isInitialized = true;
20130
20403
  } catch (error) {
@@ -20136,6 +20409,7 @@ var PostgresStore = class extends _mastra_core_storage.MastraCompositeStore {
20136
20409
  category: _mastra_core_error.ErrorCategory.THIRD_PARTY
20137
20410
  }, error);
20138
20411
  } finally {
20412
+ this.#db.setSchemaSnapshot(null);
20139
20413
  if (pinnedClient) {
20140
20414
  this.#db.unpin();
20141
20415
  pinnedClient.release();