@mastra/pg 1.30.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) {
@@ -17516,6 +17595,35 @@ function isMetadataField(field) {
17516
17595
  function placeholders(values, offset) {
17517
17596
  return values.map((_, index) => `$${offset + index}`).join(", ");
17518
17597
  }
17598
+ /**
17599
+ * Unicode letters, marks, and digits as PostgreSQL bracket ranges, so `[^<ranges>]` splits words
17600
+ * like the planner's `[^\p{L}\p{M}\p{N}]`. PostgreSQL regexes have no `\p{…}` classes, and
17601
+ * `[[:alnum:]]` depends on the locale: it leaves out digits such as `² ½ ₂ ①`, and glibc files
17602
+ * some marks, such as the Devanagari virama, under punct. Derived once from the runtime's own
17603
+ * Unicode tables. A run may also absorb unassigned code points, which are never stored.
17604
+ * Private-use code points are not absorbed: the other stores treat them as separators.
17605
+ */
17606
+ let pgWordRanges;
17607
+ function getPgWordRanges() {
17608
+ if (pgWordRanges !== void 0) return pgWordRanges;
17609
+ const escape = (cp) => cp > 65535 ? `\\U${cp.toString(16).padStart(8, "0")}` : `\\u${cp.toString(16).padStart(4, "0")}`;
17610
+ const chunks = [];
17611
+ for (let start = 0; start <= 1114111; start += 8192) {
17612
+ const codePoints = [];
17613
+ for (let cp = start; cp < start + 8192 && cp <= 1114111; cp++) if (cp < 55296 || cp > 57343) codePoints.push(cp);
17614
+ chunks.push(String.fromCodePoint(...codePoints));
17615
+ }
17616
+ const text = chunks.join("");
17617
+ const ranges = [];
17618
+ for (const match of text.matchAll(/[\p{L}\p{M}\p{N}\p{Cn}]+/gu)) {
17619
+ const first = match[0].codePointAt(0);
17620
+ const lastUnit = match.index + match[0].length - 1;
17621
+ const last = text.codePointAt(text.charCodeAt(lastUnit) >= 56320 && text.charCodeAt(lastUnit) <= 57343 ? lastUnit - 1 : lastUnit);
17622
+ ranges.push(first === last ? escape(first) : `${escape(first)}-${escape(last)}`);
17623
+ }
17624
+ pgWordRanges = ranges.join("");
17625
+ return pgWordRanges;
17626
+ }
17519
17627
  function compileScalarPredicate(predicate, registry, parameterOffset, allowMetadata = false) {
17520
17628
  if (predicate.type === "boolean") {
17521
17629
  const values = [];
@@ -17559,6 +17667,13 @@ function compileScalarPredicate(predicate, registry, parameterOffset, allowMetad
17559
17667
  values: fieldValues
17560
17668
  };
17561
17669
  }
17670
+ if (predicate.type === "text") {
17671
+ const found = `strpos(${`' ' || replace(lower(regexp_replace(normalize(${field}), '[^${getPgWordRanges()}]+', ' ', 'g')), 'ς', 'σ') || ' '`}, $${parameterOffset}) > 0`;
17672
+ return {
17673
+ sql: `COALESCE(${predicate.operator === "matches" ? found : `NOT (${found})`}, false)`,
17674
+ values: [...fieldValues, ` ${predicate.value} `]
17675
+ };
17676
+ }
17562
17677
  if (predicate.type === "membership") {
17563
17678
  const list = placeholders(predicate.values, parameterOffset);
17564
17679
  if (predicate.operator === "in") return {
@@ -23148,12 +23263,17 @@ function workflowSnapshotStatusIndexName(schemaName) {
23148
23263
  /**
23149
23264
  * Expression index on `(workflow_name, snapshot->>'status', "createdAt" DESC)` so
23150
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.
23151
23270
  */
23152
- function workflowSnapshotStatusIndexSQL(indexName, schemaName) {
23153
- return `CREATE INDEX IF NOT EXISTS "${indexName}" ON ${getTableName({
23271
+ function workflowSnapshotStatusIndexSQL(indexName, schemaName, concurrently = false) {
23272
+ const tableName = getTableName({
23154
23273
  indexName: TABLE_WORKFLOW_SNAPSHOT,
23155
23274
  schemaName: getSchemaName(schemaName)
23156
- })} (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)`;
23157
23277
  }
23158
23278
  /** Base name (before any schema prefix) of the expression index backing the threadId filter. */
23159
23279
  const WORKFLOW_SNAPSHOT_THREAD_ID_INDEX = "mastra_workflow_snapshot_threadid_idx";
@@ -23190,12 +23310,14 @@ const WORKFLOW_SNAPSHOT_THREAD_ID_EXPR = `COALESCE(jsonb_path_query_first(snapsh
23190
23310
  /**
23191
23311
  * Expression index on the snapshot-embedded thread id so listWorkflowRuns() threadId filters
23192
23312
  * (Agent.listSuspendedRuns) can use an index instead of detoasting every snapshot.
23313
+ * See {@link workflowSnapshotStatusIndexSQL} for `concurrently`.
23193
23314
  */
23194
- function workflowSnapshotThreadIdIndexSQL(indexName, schemaName) {
23195
- return `CREATE INDEX IF NOT EXISTS "${indexName}" ON ${getTableName({
23315
+ function workflowSnapshotThreadIdIndexSQL(indexName, schemaName, concurrently = false) {
23316
+ const tableName = getTableName({
23196
23317
  indexName: TABLE_WORKFLOW_SNAPSHOT,
23197
23318
  schemaName: getSchemaName(schemaName)
23198
- })} ((${WORKFLOW_SNAPSHOT_THREAD_ID_EXPR}))`;
23319
+ });
23320
+ return `CREATE INDEX ${concurrently ? "CONCURRENTLY " : ""}IF NOT EXISTS "${indexName}" ON ${tableName} ((${WORKFLOW_SNAPSHOT_THREAD_ID_EXPR}))`;
23199
23321
  }
23200
23322
  var WorkflowsPG = class WorkflowsPG extends WorkflowsStorage {
23201
23323
  #db;
@@ -23292,13 +23414,13 @@ var WorkflowsPG = class WorkflowsPG extends WorkflowsStorage {
23292
23414
  if (await this.#db.getColumnType(TABLE_WORKFLOW_SNAPSHOT, "snapshot") !== "jsonb") return;
23293
23415
  const indexName = workflowSnapshotStatusIndexName(this.#schema);
23294
23416
  try {
23295
- await this.#db.createIndexFromStatement(indexName, workflowSnapshotStatusIndexSQL(indexName, this.#schema));
23417
+ await this.#db.createIndexFromStatement(indexName, workflowSnapshotStatusIndexSQL(indexName, this.#schema, true));
23296
23418
  } catch (error) {
23297
23419
  this.logger?.warn?.(`Failed to create index ${indexName}:`, error);
23298
23420
  }
23299
23421
  const threadIdIndexName = workflowSnapshotThreadIdIndexName(this.#schema);
23300
23422
  try {
23301
- await this.#db.createIndexFromStatement(threadIdIndexName, workflowSnapshotThreadIdIndexSQL(threadIdIndexName, this.#schema));
23423
+ await this.#db.createIndexFromStatement(threadIdIndexName, workflowSnapshotThreadIdIndexSQL(threadIdIndexName, this.#schema, true));
23302
23424
  } catch (error) {
23303
23425
  this.logger?.warn?.(`Failed to create index ${threadIdIndexName}:`, error);
23304
23426
  }