@mastra/pg 1.31.0-alpha.0 → 1.31.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/dist/index.js CHANGED
@@ -2483,7 +2483,7 @@ async function loadSchemaSnapshot(client, schemaName) {
2483
2483
  AND c.relkind IN ('r', 'p')
2484
2484
  AND a.attnum > 0
2485
2485
  AND NOT a.attisdropped`, [schema]),
2486
- client.manyOrNone(`SELECT c.relname AS indexname, i.indisreplident AS is_replica_identity, i.indisprimary AS is_primary
2486
+ client.manyOrNone(`SELECT c.relname AS indexname, i.indisreplident AS is_replica_identity, i.indisprimary AS is_primary, i.indisvalid AS is_valid
2487
2487
  FROM pg_catalog.pg_index i
2488
2488
  JOIN pg_catalog.pg_class c ON c.oid = i.indexrelid
2489
2489
  JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
@@ -2508,8 +2508,10 @@ async function loadSchemaSnapshot(client, schemaName) {
2508
2508
  const indexes = /* @__PURE__ */ new Set();
2509
2509
  const replicaIdentityIndexes = /* @__PURE__ */ new Set();
2510
2510
  const primaryKeyIndexes = /* @__PURE__ */ new Set();
2511
+ const invalidIndexes = /* @__PURE__ */ new Set();
2511
2512
  for (const row of indexRows) {
2512
- indexes.add(row.indexname);
2513
+ if (row.is_valid) indexes.add(row.indexname);
2514
+ else invalidIndexes.add(row.indexname);
2513
2515
  if (row.is_replica_identity) replicaIdentityIndexes.add(row.indexname);
2514
2516
  if (row.is_primary) primaryKeyIndexes.add(row.indexname.toLowerCase());
2515
2517
  }
@@ -2520,7 +2522,8 @@ async function loadSchemaSnapshot(client, schemaName) {
2520
2522
  columnTypes,
2521
2523
  indexes,
2522
2524
  replicaIdentityIndexes,
2523
- primaryKeyIndexes
2525
+ primaryKeyIndexes,
2526
+ invalidIndexes
2524
2527
  };
2525
2528
  }
2526
2529
  //#endregion
@@ -3668,11 +3671,8 @@ MIGRATION REQUIRED: Duplicate spans detected in ${duplicateInfo.tableName}\n====
3668
3671
  schemaName: getSchemaName$6(this.schemaName)
3669
3672
  });
3670
3673
  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;
3674
+ if (snapshot?.indexes.has(name)) return;
3675
+ if (!await this.prepareIndexBuild(name, schemaName)) return;
3676
3676
  const uniqueStr = unique ? "UNIQUE " : "";
3677
3677
  const concurrentStr = concurrent ? "CONCURRENTLY " : "";
3678
3678
  const methodStr = method !== "btree" ? `USING ${method} ` : "";
@@ -3691,16 +3691,9 @@ MIGRATION REQUIRED: Duplicate spans detected in ${duplicateInfo.tableName}\n====
3691
3691
  let withStr = "";
3692
3692
  if (storage && Object.keys(storage).length > 0) withStr = ` WITH (${Object.entries(storage).map(([key, value]) => `${key} = ${value}`).join(", ")})`;
3693
3693
  const sql = `CREATE ${uniqueStr}INDEX ${concurrentStr}${`"${parseSqlIdentifier(name, "index name")}"`} ON ${fullTableName} ${methodStr}(${columnsStr})${withStr}${tablespaceStr}${whereStr}`;
3694
- await this.client.none(sql);
3694
+ await this.timedIndexBuild(name, sql);
3695
3695
  snapshot?.indexes.add(name);
3696
3696
  } 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
3697
  throw new MastraError({
3705
3698
  id: createStorageErrorId("PG", "INDEX_CREATE", "FAILED"),
3706
3699
  domain: ErrorDomain.STORAGE,
@@ -3724,9 +3717,43 @@ MIGRATION REQUIRED: Duplicate spans detected in ${duplicateInfo.tableName}\n====
3724
3717
  async createIndexFromStatement(indexName, sql) {
3725
3718
  const snapshot = this.schemaSnapshot;
3726
3719
  if (snapshot?.indexes.has(indexName)) return;
3727
- await this.client.none(sql);
3720
+ if (!await this.prepareIndexBuild(indexName, this.schemaName || "public")) return;
3721
+ await this.timedIndexBuild(indexName, sql);
3728
3722
  snapshot?.indexes.add(indexName);
3729
3723
  }
3724
+ /**
3725
+ * Decides whether `indexName` still needs building. Returns false when a valid
3726
+ * index already exists, or when another session is currently building it.
3727
+ * An invalid index left by an interrupted `CREATE INDEX CONCURRENTLY` is
3728
+ * dropped (concurrently) so the caller rebuilds it — `IF NOT EXISTS` would
3729
+ * otherwise keep the broken index forever.
3730
+ */
3731
+ async prepareIndexBuild(indexName, schemaName) {
3732
+ const snapshot = this.schemaSnapshot;
3733
+ if (snapshot && !snapshot.invalidIndexes.has(indexName)) return true;
3734
+ const existing = await this.client.oneOrNone(`SELECT i.indisvalid AS is_valid,
3735
+ EXISTS (SELECT 1 FROM pg_stat_progress_create_index p WHERE p.index_relid = c.oid) AS building
3736
+ FROM pg_catalog.pg_index i
3737
+ JOIN pg_catalog.pg_class c ON c.oid = i.indexrelid
3738
+ JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
3739
+ WHERE c.relname = $1 AND n.nspname = $2`, [indexName, schemaName]);
3740
+ if (!existing) return true;
3741
+ if (existing.is_valid) return false;
3742
+ if (existing.building) {
3743
+ this.logger.warn(`Index ${indexName} is being built by another session; skipping`);
3744
+ return false;
3745
+ }
3746
+ this.logger.warn(`Dropping invalid index ${indexName} left by an interrupted build; rebuilding it`);
3747
+ const quotedIndexName = `"${parseSqlIdentifier(indexName, "index name")}"`;
3748
+ await this.client.none(`DROP INDEX CONCURRENTLY IF EXISTS ${getSchemaName$6(this.schemaName)}.${quotedIndexName}`);
3749
+ return true;
3750
+ }
3751
+ async timedIndexBuild(indexName, sql) {
3752
+ const startedAt = Date.now();
3753
+ this.logger.info(`Creating index ${indexName}`);
3754
+ await this.client.none(sql);
3755
+ this.logger.info(`Created index ${indexName} in ${Date.now() - startedAt}ms`);
3756
+ }
3730
3757
  async dropIndex(indexName) {
3731
3758
  try {
3732
3759
  const schemaName = this.schemaName || "public";
@@ -10471,6 +10498,7 @@ function dedupeMessagesForSave(messages) {
10471
10498
  var MemoryPG = class MemoryPG extends MemoryStorage {
10472
10499
  supportsPartialThreadUpdate = true;
10473
10500
  supportsObservationalMemory = true;
10501
+ supportsAtomicWorkingMemoryMerge = true;
10474
10502
  supportsObservationalMemoryHistorySearch = true;
10475
10503
  /**
10476
10504
  * Retention-eligible tables. `threads`, `messages`, and `resources` all anchor
@@ -11817,6 +11845,57 @@ var MemoryPG = class MemoryPG extends MemoryStorage {
11817
11845
  metadata: metadataJson ? JSON.parse(metadataJson) : resource.metadata
11818
11846
  };
11819
11847
  }
11848
+ async mergeResourceWorkingMemory({ resourceId, merge }) {
11849
+ const tableName = getTableName$3({
11850
+ indexName: TABLE_RESOURCES,
11851
+ schemaName: getSchemaName$3(this.#schema)
11852
+ });
11853
+ const callbackError = {};
11854
+ try {
11855
+ return await this.#db.client.tx(async (t) => {
11856
+ const now = (/* @__PURE__ */ new Date()).toISOString();
11857
+ await t.none(`INSERT INTO ${tableName} (id, metadata, "createdAt", "createdAtZ", "updatedAt", "updatedAtZ")
11858
+ VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (id) DO NOTHING`, [
11859
+ resourceId,
11860
+ toPgJson({}),
11861
+ now,
11862
+ now,
11863
+ now,
11864
+ now
11865
+ ]);
11866
+ await t.one(`SELECT id FROM ${tableName} WHERE id = $1 FOR UPDATE`, [resourceId]);
11867
+ const existing = await this.#getResourceById(t, resourceId);
11868
+ let workingMemory;
11869
+ try {
11870
+ workingMemory = merge(existing.workingMemory ?? void 0);
11871
+ } catch (error) {
11872
+ callbackError.error = error;
11873
+ throw error;
11874
+ }
11875
+ const updatedAt = /* @__PURE__ */ new Date();
11876
+ const updatedAtStr = updatedAt.toISOString();
11877
+ await t.none(`UPDATE ${tableName} SET "workingMemory" = $1, "updatedAt" = $2, "updatedAtZ" = $3 WHERE id = $4`, [
11878
+ workingMemory,
11879
+ updatedAtStr,
11880
+ updatedAtStr,
11881
+ resourceId
11882
+ ]);
11883
+ return {
11884
+ ...existing,
11885
+ workingMemory,
11886
+ updatedAt
11887
+ };
11888
+ });
11889
+ } catch (error) {
11890
+ if (error === callbackError.error || error instanceof MastraError) throw error;
11891
+ throw new MastraError({
11892
+ id: createStorageErrorId("PG", "MERGE_RESOURCE_WORKING_MEMORY", "FAILED"),
11893
+ domain: ErrorDomain.STORAGE,
11894
+ category: ErrorCategory.THIRD_PARTY,
11895
+ details: { resourceId }
11896
+ }, error);
11897
+ }
11898
+ }
11820
11899
  async updateResource({ resourceId, workingMemory, metadata }) {
11821
11900
  const existingResource = await this.#getResourceById(this.#db.client, resourceId);
11822
11901
  if (!existingResource) {
@@ -23184,12 +23263,17 @@ function workflowSnapshotStatusIndexName(schemaName) {
23184
23263
  /**
23185
23264
  * Expression index on `(workflow_name, snapshot->>'status', "createdAt" DESC)` so
23186
23265
  * listWorkflowRuns() status filters can use an index instead of scanning every snapshot.
23266
+ *
23267
+ * `concurrently` is set by init, which may build the index on a large existing table while
23268
+ * other processes keep writing to it. The exported DDL stays plain because migration
23269
+ * tooling often wraps statements in a transaction, where CONCURRENTLY is rejected.
23187
23270
  */
23188
- function workflowSnapshotStatusIndexSQL(indexName, schemaName) {
23189
- return `CREATE INDEX IF NOT EXISTS "${indexName}" ON ${getTableName({
23271
+ function workflowSnapshotStatusIndexSQL(indexName, schemaName, concurrently = false) {
23272
+ const tableName = getTableName({
23190
23273
  indexName: TABLE_WORKFLOW_SNAPSHOT,
23191
23274
  schemaName: getSchemaName(schemaName)
23192
- })} (workflow_name, (snapshot ->> 'status'), "createdAt" DESC)`;
23275
+ });
23276
+ return `CREATE INDEX ${concurrently ? "CONCURRENTLY " : ""}IF NOT EXISTS "${indexName}" ON ${tableName} (workflow_name, (snapshot ->> 'status'), "createdAt" DESC)`;
23193
23277
  }
23194
23278
  /** Base name (before any schema prefix) of the expression index backing the threadId filter. */
23195
23279
  const WORKFLOW_SNAPSHOT_THREAD_ID_INDEX = "mastra_workflow_snapshot_threadid_idx";
@@ -23226,12 +23310,14 @@ const WORKFLOW_SNAPSHOT_THREAD_ID_EXPR = `COALESCE(jsonb_path_query_first(snapsh
23226
23310
  /**
23227
23311
  * Expression index on the snapshot-embedded thread id so listWorkflowRuns() threadId filters
23228
23312
  * (Agent.listSuspendedRuns) can use an index instead of detoasting every snapshot.
23313
+ * See {@link workflowSnapshotStatusIndexSQL} for `concurrently`.
23229
23314
  */
23230
- function workflowSnapshotThreadIdIndexSQL(indexName, schemaName) {
23231
- return `CREATE INDEX IF NOT EXISTS "${indexName}" ON ${getTableName({
23315
+ function workflowSnapshotThreadIdIndexSQL(indexName, schemaName, concurrently = false) {
23316
+ const tableName = getTableName({
23232
23317
  indexName: TABLE_WORKFLOW_SNAPSHOT,
23233
23318
  schemaName: getSchemaName(schemaName)
23234
- })} ((${WORKFLOW_SNAPSHOT_THREAD_ID_EXPR}))`;
23319
+ });
23320
+ return `CREATE INDEX ${concurrently ? "CONCURRENTLY " : ""}IF NOT EXISTS "${indexName}" ON ${tableName} ((${WORKFLOW_SNAPSHOT_THREAD_ID_EXPR}))`;
23235
23321
  }
23236
23322
  var WorkflowsPG = class WorkflowsPG extends WorkflowsStorage {
23237
23323
  #db;
@@ -23328,13 +23414,13 @@ var WorkflowsPG = class WorkflowsPG extends WorkflowsStorage {
23328
23414
  if (await this.#db.getColumnType(TABLE_WORKFLOW_SNAPSHOT, "snapshot") !== "jsonb") return;
23329
23415
  const indexName = workflowSnapshotStatusIndexName(this.#schema);
23330
23416
  try {
23331
- await this.#db.createIndexFromStatement(indexName, workflowSnapshotStatusIndexSQL(indexName, this.#schema));
23417
+ await this.#db.createIndexFromStatement(indexName, workflowSnapshotStatusIndexSQL(indexName, this.#schema, true));
23332
23418
  } catch (error) {
23333
23419
  this.logger?.warn?.(`Failed to create index ${indexName}:`, error);
23334
23420
  }
23335
23421
  const threadIdIndexName = workflowSnapshotThreadIdIndexName(this.#schema);
23336
23422
  try {
23337
- await this.#db.createIndexFromStatement(threadIdIndexName, workflowSnapshotThreadIdIndexSQL(threadIdIndexName, this.#schema));
23423
+ await this.#db.createIndexFromStatement(threadIdIndexName, workflowSnapshotThreadIdIndexSQL(threadIdIndexName, this.#schema, true));
23338
23424
  } catch (error) {
23339
23425
  this.logger?.warn?.(`Failed to create index ${threadIdIndexName}:`, error);
23340
23426
  }