@mastra/pg 1.31.0-alpha.0 → 1.31.0-alpha.2

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/dist/index.js CHANGED
@@ -2475,7 +2475,8 @@ async function loadSchemaSnapshot(client, schemaName) {
2475
2475
  const schema = schemaName || "public";
2476
2476
  const [tableRows, columnRows, indexRows] = await Promise.all([
2477
2477
  client.manyOrNone(`SELECT tablename FROM pg_catalog.pg_tables WHERE schemaname = $1`, [schema]),
2478
- client.manyOrNone(`SELECT c.relname AS table_name, a.attname AS column_name, format_type(a.atttypid, a.atttypmod) AS data_type
2478
+ client.manyOrNone(`SELECT c.relname AS table_name, a.attname AS column_name, format_type(a.atttypid, a.atttypmod) AS data_type,
2479
+ a.attnotnull AS not_null
2479
2480
  FROM pg_catalog.pg_class c
2480
2481
  JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
2481
2482
  JOIN pg_catalog.pg_attribute a ON a.attrelid = c.oid
@@ -2483,7 +2484,7 @@ async function loadSchemaSnapshot(client, schemaName) {
2483
2484
  AND c.relkind IN ('r', 'p')
2484
2485
  AND a.attnum > 0
2485
2486
  AND NOT a.attisdropped`, [schema]),
2486
- client.manyOrNone(`SELECT c.relname AS indexname, i.indisreplident AS is_replica_identity, i.indisprimary AS is_primary
2487
+ client.manyOrNone(`SELECT c.relname AS indexname, i.indisreplident AS is_replica_identity, i.indisprimary AS is_primary, i.indisvalid AS is_valid
2487
2488
  FROM pg_catalog.pg_index i
2488
2489
  JOIN pg_catalog.pg_class c ON c.oid = i.indexrelid
2489
2490
  JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
@@ -2491,6 +2492,7 @@ async function loadSchemaSnapshot(client, schemaName) {
2491
2492
  ]);
2492
2493
  const columns = /* @__PURE__ */ new Map();
2493
2494
  const columnTypes = /* @__PURE__ */ new Map();
2495
+ const notNullColumns = /* @__PURE__ */ new Map();
2494
2496
  for (const row of columnRows) {
2495
2497
  let set = columns.get(row.table_name);
2496
2498
  if (!set) {
@@ -2504,12 +2506,22 @@ async function loadSchemaSnapshot(client, schemaName) {
2504
2506
  columnTypes.set(row.table_name, types);
2505
2507
  }
2506
2508
  types.set(row.column_name, row.data_type);
2509
+ if (row.not_null) {
2510
+ let notNull = notNullColumns.get(row.table_name);
2511
+ if (!notNull) {
2512
+ notNull = /* @__PURE__ */ new Set();
2513
+ notNullColumns.set(row.table_name, notNull);
2514
+ }
2515
+ notNull.add(row.column_name);
2516
+ }
2507
2517
  }
2508
2518
  const indexes = /* @__PURE__ */ new Set();
2509
2519
  const replicaIdentityIndexes = /* @__PURE__ */ new Set();
2510
2520
  const primaryKeyIndexes = /* @__PURE__ */ new Set();
2521
+ const invalidIndexes = /* @__PURE__ */ new Set();
2511
2522
  for (const row of indexRows) {
2512
- indexes.add(row.indexname);
2523
+ if (row.is_valid) indexes.add(row.indexname);
2524
+ else invalidIndexes.add(row.indexname);
2513
2525
  if (row.is_replica_identity) replicaIdentityIndexes.add(row.indexname);
2514
2526
  if (row.is_primary) primaryKeyIndexes.add(row.indexname.toLowerCase());
2515
2527
  }
@@ -2518,9 +2530,11 @@ async function loadSchemaSnapshot(client, schemaName) {
2518
2530
  tables: new Set(tableRows.map((r) => r.tablename)),
2519
2531
  columns,
2520
2532
  columnTypes,
2533
+ notNullColumns,
2521
2534
  indexes,
2522
2535
  replicaIdentityIndexes,
2523
- primaryKeyIndexes
2536
+ primaryKeyIndexes,
2537
+ invalidIndexes
2524
2538
  };
2525
2539
  }
2526
2540
  //#endregion
@@ -3595,6 +3609,33 @@ MIGRATION REQUIRED: Duplicate spans detected in ${duplicateInfo.tableName}\n====
3595
3609
  this.columnTypeCache.delete(tableName);
3596
3610
  }
3597
3611
  }
3612
+ /**
3613
+ * Drops NOT NULL from `columns` on `tableName` where it is still set. Answered
3614
+ * from the init snapshot when one is installed, so a converged schema issues
3615
+ * no query and never takes the ACCESS EXCLUSIVE lock.
3616
+ */
3617
+ async dropNotNull({ tableName, columns }) {
3618
+ const parsedColumns = columns.map((c) => parseSqlIdentifier(c, "column name"));
3619
+ const snapshot = this.schemaSnapshot;
3620
+ let toAlter;
3621
+ if (snapshot) {
3622
+ const notNull = snapshot.notNullColumns.get(tableName);
3623
+ toAlter = parsedColumns.filter((c) => notNull?.has(c));
3624
+ } else toAlter = (await this.client.any(`SELECT column_name FROM information_schema.columns
3625
+ WHERE table_schema = $1 AND table_name = $2 AND column_name = ANY($3) AND is_nullable = 'NO'`, [
3626
+ this.schemaName || "public",
3627
+ tableName,
3628
+ parsedColumns
3629
+ ])).map((r) => r.column_name);
3630
+ if (toAlter.length === 0) return;
3631
+ const fullTableName = getTableName$6({
3632
+ indexName: tableName,
3633
+ schemaName: getSchemaName$6(this.schemaName)
3634
+ });
3635
+ await this.client.none(`ALTER TABLE ${fullTableName} ${toAlter.map((c) => `ALTER COLUMN "${c}" DROP NOT NULL`).join(", ")}`);
3636
+ const notNull = snapshot?.notNullColumns.get(tableName);
3637
+ for (const c of toAlter) notNull?.delete(c);
3638
+ }
3598
3639
  async load({ tableName, keys }) {
3599
3640
  try {
3600
3641
  const keyEntries = Object.entries(keys).map(([key, value]) => [parseSqlIdentifier(key, "column name"), value]);
@@ -3668,11 +3709,8 @@ MIGRATION REQUIRED: Duplicate spans detected in ${duplicateInfo.tableName}\n====
3668
3709
  schemaName: getSchemaName$6(this.schemaName)
3669
3710
  });
3670
3711
  const snapshot = this.schemaSnapshot;
3671
- if (snapshot) {
3672
- if (snapshot.indexes.has(name)) return;
3673
- } else if (await this.client.oneOrNone(`SELECT 1 FROM pg_indexes
3674
- WHERE indexname = $1
3675
- AND schemaname = $2`, [name, schemaName])) return;
3712
+ if (snapshot?.indexes.has(name)) return;
3713
+ if (!await this.prepareIndexBuild(name, schemaName)) return;
3676
3714
  const uniqueStr = unique ? "UNIQUE " : "";
3677
3715
  const concurrentStr = concurrent ? "CONCURRENTLY " : "";
3678
3716
  const methodStr = method !== "btree" ? `USING ${method} ` : "";
@@ -3691,16 +3729,9 @@ MIGRATION REQUIRED: Duplicate spans detected in ${duplicateInfo.tableName}\n====
3691
3729
  let withStr = "";
3692
3730
  if (storage && Object.keys(storage).length > 0) withStr = ` WITH (${Object.entries(storage).map(([key, value]) => `${key} = ${value}`).join(", ")})`;
3693
3731
  const sql = `CREATE ${uniqueStr}INDEX ${concurrentStr}${`"${parseSqlIdentifier(name, "index name")}"`} ON ${fullTableName} ${methodStr}(${columnsStr})${withStr}${tablespaceStr}${whereStr}`;
3694
- await this.client.none(sql);
3732
+ await this.timedIndexBuild(name, sql);
3695
3733
  snapshot?.indexes.add(name);
3696
3734
  } catch (error) {
3697
- if (error instanceof Error && error.message.includes("CONCURRENTLY")) {
3698
- const retryOptions = {
3699
- ...options,
3700
- concurrent: false
3701
- };
3702
- return this.createIndex(retryOptions);
3703
- }
3704
3735
  throw new MastraError({
3705
3736
  id: createStorageErrorId("PG", "INDEX_CREATE", "FAILED"),
3706
3737
  domain: ErrorDomain.STORAGE,
@@ -3724,9 +3755,43 @@ MIGRATION REQUIRED: Duplicate spans detected in ${duplicateInfo.tableName}\n====
3724
3755
  async createIndexFromStatement(indexName, sql) {
3725
3756
  const snapshot = this.schemaSnapshot;
3726
3757
  if (snapshot?.indexes.has(indexName)) return;
3727
- await this.client.none(sql);
3758
+ if (!await this.prepareIndexBuild(indexName, this.schemaName || "public")) return;
3759
+ await this.timedIndexBuild(indexName, sql);
3728
3760
  snapshot?.indexes.add(indexName);
3729
3761
  }
3762
+ /**
3763
+ * Decides whether `indexName` still needs building. Returns false when a valid
3764
+ * index already exists, or when another session is currently building it.
3765
+ * An invalid index left by an interrupted `CREATE INDEX CONCURRENTLY` is
3766
+ * dropped (concurrently) so the caller rebuilds it — `IF NOT EXISTS` would
3767
+ * otherwise keep the broken index forever.
3768
+ */
3769
+ async prepareIndexBuild(indexName, schemaName) {
3770
+ const snapshot = this.schemaSnapshot;
3771
+ if (snapshot && !snapshot.invalidIndexes.has(indexName)) return true;
3772
+ const existing = await this.client.oneOrNone(`SELECT i.indisvalid AS is_valid,
3773
+ EXISTS (SELECT 1 FROM pg_stat_progress_create_index p WHERE p.index_relid = c.oid) AS building
3774
+ FROM pg_catalog.pg_index i
3775
+ JOIN pg_catalog.pg_class c ON c.oid = i.indexrelid
3776
+ JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
3777
+ WHERE c.relname = $1 AND n.nspname = $2`, [indexName, schemaName]);
3778
+ if (!existing) return true;
3779
+ if (existing.is_valid) return false;
3780
+ if (existing.building) {
3781
+ this.logger.warn(`Index ${indexName} is being built by another session; skipping`);
3782
+ return false;
3783
+ }
3784
+ this.logger.warn(`Dropping invalid index ${indexName} left by an interrupted build; rebuilding it`);
3785
+ const quotedIndexName = `"${parseSqlIdentifier(indexName, "index name")}"`;
3786
+ await this.client.none(`DROP INDEX CONCURRENTLY IF EXISTS ${getSchemaName$6(this.schemaName)}.${quotedIndexName}`);
3787
+ return true;
3788
+ }
3789
+ async timedIndexBuild(indexName, sql) {
3790
+ const startedAt = Date.now();
3791
+ this.logger.info(`Creating index ${indexName}`);
3792
+ await this.client.none(sql);
3793
+ this.logger.info(`Created index ${indexName} in ${Date.now() - startedAt}ms`);
3794
+ }
3730
3795
  async dropIndex(indexName) {
3731
3796
  try {
3732
3797
  const schemaName = this.schemaName || "public";
@@ -7061,6 +7126,10 @@ var ExperimentsPG = class ExperimentsPG extends ExperimentsStorage {
7061
7126
  "scorerIds"
7062
7127
  ]
7063
7128
  });
7129
+ await this.#db.dropNotNull({
7130
+ tableName: TABLE_EXPERIMENTS,
7131
+ columns: ["targetType", "targetId"]
7132
+ });
7064
7133
  await this.#db.alterTable({
7065
7134
  tableName: TABLE_EXPERIMENT_RESULTS,
7066
7135
  schema: EXPERIMENT_RESULTS_SCHEMA,
@@ -10471,6 +10540,7 @@ function dedupeMessagesForSave(messages) {
10471
10540
  var MemoryPG = class MemoryPG extends MemoryStorage {
10472
10541
  supportsPartialThreadUpdate = true;
10473
10542
  supportsObservationalMemory = true;
10543
+ supportsAtomicWorkingMemoryMerge = true;
10474
10544
  supportsObservationalMemoryHistorySearch = true;
10475
10545
  /**
10476
10546
  * Retention-eligible tables. `threads`, `messages`, and `resources` all anchor
@@ -11817,6 +11887,57 @@ var MemoryPG = class MemoryPG extends MemoryStorage {
11817
11887
  metadata: metadataJson ? JSON.parse(metadataJson) : resource.metadata
11818
11888
  };
11819
11889
  }
11890
+ async mergeResourceWorkingMemory({ resourceId, merge }) {
11891
+ const tableName = getTableName$3({
11892
+ indexName: TABLE_RESOURCES,
11893
+ schemaName: getSchemaName$3(this.#schema)
11894
+ });
11895
+ const callbackError = {};
11896
+ try {
11897
+ return await this.#db.client.tx(async (t) => {
11898
+ const now = (/* @__PURE__ */ new Date()).toISOString();
11899
+ await t.none(`INSERT INTO ${tableName} (id, metadata, "createdAt", "createdAtZ", "updatedAt", "updatedAtZ")
11900
+ VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (id) DO NOTHING`, [
11901
+ resourceId,
11902
+ toPgJson({}),
11903
+ now,
11904
+ now,
11905
+ now,
11906
+ now
11907
+ ]);
11908
+ await t.one(`SELECT id FROM ${tableName} WHERE id = $1 FOR UPDATE`, [resourceId]);
11909
+ const existing = await this.#getResourceById(t, resourceId);
11910
+ let workingMemory;
11911
+ try {
11912
+ workingMemory = merge(existing.workingMemory ?? void 0);
11913
+ } catch (error) {
11914
+ callbackError.error = error;
11915
+ throw error;
11916
+ }
11917
+ const updatedAt = /* @__PURE__ */ new Date();
11918
+ const updatedAtStr = updatedAt.toISOString();
11919
+ await t.none(`UPDATE ${tableName} SET "workingMemory" = $1, "updatedAt" = $2, "updatedAtZ" = $3 WHERE id = $4`, [
11920
+ workingMemory,
11921
+ updatedAtStr,
11922
+ updatedAtStr,
11923
+ resourceId
11924
+ ]);
11925
+ return {
11926
+ ...existing,
11927
+ workingMemory,
11928
+ updatedAt
11929
+ };
11930
+ });
11931
+ } catch (error) {
11932
+ if (error === callbackError.error || error instanceof MastraError) throw error;
11933
+ throw new MastraError({
11934
+ id: createStorageErrorId("PG", "MERGE_RESOURCE_WORKING_MEMORY", "FAILED"),
11935
+ domain: ErrorDomain.STORAGE,
11936
+ category: ErrorCategory.THIRD_PARTY,
11937
+ details: { resourceId }
11938
+ }, error);
11939
+ }
11940
+ }
11820
11941
  async updateResource({ resourceId, workingMemory, metadata }) {
11821
11942
  const existingResource = await this.#getResourceById(this.#db.client, resourceId);
11822
11943
  if (!existingResource) {
@@ -23184,12 +23305,17 @@ function workflowSnapshotStatusIndexName(schemaName) {
23184
23305
  /**
23185
23306
  * Expression index on `(workflow_name, snapshot->>'status', "createdAt" DESC)` so
23186
23307
  * listWorkflowRuns() status filters can use an index instead of scanning every snapshot.
23308
+ *
23309
+ * `concurrently` is set by init, which may build the index on a large existing table while
23310
+ * other processes keep writing to it. The exported DDL stays plain because migration
23311
+ * tooling often wraps statements in a transaction, where CONCURRENTLY is rejected.
23187
23312
  */
23188
- function workflowSnapshotStatusIndexSQL(indexName, schemaName) {
23189
- return `CREATE INDEX IF NOT EXISTS "${indexName}" ON ${getTableName({
23313
+ function workflowSnapshotStatusIndexSQL(indexName, schemaName, concurrently = false) {
23314
+ const tableName = getTableName({
23190
23315
  indexName: TABLE_WORKFLOW_SNAPSHOT,
23191
23316
  schemaName: getSchemaName(schemaName)
23192
- })} (workflow_name, (snapshot ->> 'status'), "createdAt" DESC)`;
23317
+ });
23318
+ return `CREATE INDEX ${concurrently ? "CONCURRENTLY " : ""}IF NOT EXISTS "${indexName}" ON ${tableName} (workflow_name, (snapshot ->> 'status'), "createdAt" DESC)`;
23193
23319
  }
23194
23320
  /** Base name (before any schema prefix) of the expression index backing the threadId filter. */
23195
23321
  const WORKFLOW_SNAPSHOT_THREAD_ID_INDEX = "mastra_workflow_snapshot_threadid_idx";
@@ -23226,12 +23352,14 @@ const WORKFLOW_SNAPSHOT_THREAD_ID_EXPR = `COALESCE(jsonb_path_query_first(snapsh
23226
23352
  /**
23227
23353
  * Expression index on the snapshot-embedded thread id so listWorkflowRuns() threadId filters
23228
23354
  * (Agent.listSuspendedRuns) can use an index instead of detoasting every snapshot.
23355
+ * See {@link workflowSnapshotStatusIndexSQL} for `concurrently`.
23229
23356
  */
23230
- function workflowSnapshotThreadIdIndexSQL(indexName, schemaName) {
23231
- return `CREATE INDEX IF NOT EXISTS "${indexName}" ON ${getTableName({
23357
+ function workflowSnapshotThreadIdIndexSQL(indexName, schemaName, concurrently = false) {
23358
+ const tableName = getTableName({
23232
23359
  indexName: TABLE_WORKFLOW_SNAPSHOT,
23233
23360
  schemaName: getSchemaName(schemaName)
23234
- })} ((${WORKFLOW_SNAPSHOT_THREAD_ID_EXPR}))`;
23361
+ });
23362
+ return `CREATE INDEX ${concurrently ? "CONCURRENTLY " : ""}IF NOT EXISTS "${indexName}" ON ${tableName} ((${WORKFLOW_SNAPSHOT_THREAD_ID_EXPR}))`;
23235
23363
  }
23236
23364
  var WorkflowsPG = class WorkflowsPG extends WorkflowsStorage {
23237
23365
  #db;
@@ -23328,13 +23456,13 @@ var WorkflowsPG = class WorkflowsPG extends WorkflowsStorage {
23328
23456
  if (await this.#db.getColumnType(TABLE_WORKFLOW_SNAPSHOT, "snapshot") !== "jsonb") return;
23329
23457
  const indexName = workflowSnapshotStatusIndexName(this.#schema);
23330
23458
  try {
23331
- await this.#db.createIndexFromStatement(indexName, workflowSnapshotStatusIndexSQL(indexName, this.#schema));
23459
+ await this.#db.createIndexFromStatement(indexName, workflowSnapshotStatusIndexSQL(indexName, this.#schema, true));
23332
23460
  } catch (error) {
23333
23461
  this.logger?.warn?.(`Failed to create index ${indexName}:`, error);
23334
23462
  }
23335
23463
  const threadIdIndexName = workflowSnapshotThreadIdIndexName(this.#schema);
23336
23464
  try {
23337
- await this.#db.createIndexFromStatement(threadIdIndexName, workflowSnapshotThreadIdIndexSQL(threadIdIndexName, this.#schema));
23465
+ await this.#db.createIndexFromStatement(threadIdIndexName, workflowSnapshotThreadIdIndexSQL(threadIdIndexName, this.#schema, true));
23338
23466
  } catch (error) {
23339
23467
  this.logger?.warn?.(`Failed to create index ${threadIdIndexName}:`, error);
23340
23468
  }
@@ -23405,7 +23533,7 @@ var WorkflowsPG = class WorkflowsPG extends WorkflowsStorage {
23405
23533
  async dangerouslyClearAll() {
23406
23534
  await this.#db.clearTable({ tableName: TABLE_WORKFLOW_SNAPSHOT });
23407
23535
  }
23408
- async updateWorkflowResults({ workflowName, runId, stepId, result, requestContext }) {
23536
+ async updateWorkflowResults({ workflowName, runId, stepId, result, requestContext, state }) {
23409
23537
  try {
23410
23538
  return await this.#db.client.tx(async (t) => {
23411
23539
  const tableName = getTableName({
@@ -23436,7 +23564,8 @@ var WorkflowsPG = class WorkflowsPG extends WorkflowsStorage {
23436
23564
  snapshot,
23437
23565
  stepId,
23438
23566
  result,
23439
- requestContext
23567
+ requestContext,
23568
+ state
23440
23569
  });
23441
23570
  const now = /* @__PURE__ */ new Date();
23442
23571
  const sanitizedSnapshot = toPgJson(snapshot);