@mastra/pg 1.33.0-alpha.2 → 1.33.0-alpha.3

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
@@ -3996,6 +3996,42 @@ MIGRATION REQUIRED: Duplicate spans detected in ${duplicateInfo.tableName}\n====
3996
3996
  }, error);
3997
3997
  }
3998
3998
  }
3999
+ /**
4000
+ * Runs one UPDATE per record in a transaction, changing a row only while every
4001
+ * column in `nullColumns` is still NULL. Never inserts. Returns, in input order,
4002
+ * whether each update changed a row.
4003
+ */
4004
+ async batchUpdateWhereNull({ tableName, updates, nullColumns }) {
4005
+ const nullConditions = nullColumns.map((column) => ` AND "${parseSqlIdentifier(column, "column name")}" IS NULL`).join("");
4006
+ try {
4007
+ return await this.client.tx(async (tx) => {
4008
+ const applied = [];
4009
+ for (const update of updates) {
4010
+ const query = await this.buildUpdateQuery({
4011
+ tableName,
4012
+ ...update
4013
+ });
4014
+ if (!query) {
4015
+ applied.push(false);
4016
+ continue;
4017
+ }
4018
+ const rows = await tx.any(`${query.sql}${nullConditions} RETURNING 1`, query.values);
4019
+ applied.push(rows.length > 0);
4020
+ }
4021
+ return applied;
4022
+ });
4023
+ } catch (error) {
4024
+ throw new MastraError({
4025
+ id: createStorageErrorId("PG", "BATCH_UPDATE_WHERE_NULL", "FAILED"),
4026
+ domain: ErrorDomain.STORAGE,
4027
+ category: ErrorCategory.THIRD_PARTY,
4028
+ details: {
4029
+ tableName,
4030
+ numberOfRecords: updates.length
4031
+ }
4032
+ }, error);
4033
+ }
4034
+ }
3999
4035
  async batchDelete({ tableName, keys }) {
4000
4036
  try {
4001
4037
  if (keys.length === 0) return;
@@ -4116,9 +4152,14 @@ MIGRATION REQUIRED: Duplicate spans detected in ${duplicateInfo.tableName}\n====
4116
4152
  columns: [column]
4117
4153
  });
4118
4154
  }
4119
- async executeUpdate(client, { tableName, keys, data }) {
4155
+ async executeUpdate(client, args) {
4156
+ const query = await this.buildUpdateQuery(args);
4157
+ if (!query) return;
4158
+ await client.none(query.sql, query.values);
4159
+ }
4160
+ async buildUpdateQuery({ tableName, keys, data }) {
4120
4161
  const filteredData = await this.filterRecordToKnownColumns(tableName, data);
4121
- if (Object.keys(filteredData).length === 0) return;
4162
+ if (Object.keys(filteredData).length === 0) return null;
4122
4163
  const setColumns = [];
4123
4164
  const setValues = [];
4124
4165
  let paramIndex = 1;
@@ -4134,12 +4175,13 @@ MIGRATION REQUIRED: Duplicate spans detected in ${duplicateInfo.tableName}\n====
4134
4175
  whereConditions.push(`"${parsedKey}" = $${paramIndex++}`);
4135
4176
  whereValues.push(this.prepareValue(value, key, tableName));
4136
4177
  });
4137
- const sql = `UPDATE ${getTableName$6({
4138
- indexName: tableName,
4139
- schemaName: getSchemaName$6(this.schemaName)
4140
- })} SET ${setColumns.join(", ")} WHERE ${whereConditions.join(" AND ")}`;
4141
- const values = [...setValues, ...whereValues];
4142
- await client.none(sql, values);
4178
+ return {
4179
+ sql: `UPDATE ${getTableName$6({
4180
+ indexName: tableName,
4181
+ schemaName: getSchemaName$6(this.schemaName)
4182
+ })} SET ${setColumns.join(", ")} WHERE ${whereConditions.join(" AND ")}`,
4183
+ values: [...setValues, ...whereValues]
4184
+ };
4143
4185
  }
4144
4186
  };
4145
4187
  // src/storage/domains/utils.ts
@@ -12015,7 +12057,7 @@ var MemoryPG = class MemoryPG extends MemoryStorage {
12015
12057
  indexName: OM_TABLE,
12016
12058
  schemaName: quotedSchemaName
12017
12059
  });
12018
- const idxPrefix = schemaPrefix ? `${schemaPrefix}` : "";
12060
+ const idxPrefix = schemaPrefix ? schemaPrefix : "";
12019
12061
  statements.push(`CREATE INDEX IF NOT EXISTS "${idxPrefix}idx_om_lookup_key" ON ${fullOmTableName} ("lookupKey");`);
12020
12062
  }
12021
12063
  for (const idx of MemoryPG.getDefaultIndexDefs(schemaPrefix)) statements.push(generateIndexSQL(idx, schemaName));
@@ -15689,30 +15731,33 @@ var ObservabilityPG = class ObservabilityPG extends ObservabilityStorage {
15689
15731
  }, error);
15690
15732
  }
15691
15733
  }
15734
+ #toSpanUpdates(records) {
15735
+ return records.map((record) => {
15736
+ const data = { ...record.updates };
15737
+ if (data.endedAt instanceof Date) {
15738
+ const endedAt = data.endedAt.toISOString();
15739
+ data.endedAt = endedAt;
15740
+ data.endedAtZ = endedAt;
15741
+ }
15742
+ if (data.startedAt instanceof Date) {
15743
+ const startedAt = data.startedAt.toISOString();
15744
+ data.startedAt = startedAt;
15745
+ data.startedAtZ = startedAt;
15746
+ }
15747
+ return {
15748
+ keys: {
15749
+ spanId: record.spanId,
15750
+ traceId: record.traceId
15751
+ },
15752
+ data
15753
+ };
15754
+ });
15755
+ }
15692
15756
  async batchUpdateSpans(args) {
15693
15757
  try {
15694
15758
  return this.#db.batchUpdate({
15695
15759
  tableName: TABLE_SPANS,
15696
- updates: args.records.map((record) => {
15697
- const data = { ...record.updates };
15698
- if (data.endedAt instanceof Date) {
15699
- const endedAt = data.endedAt.toISOString();
15700
- data.endedAt = endedAt;
15701
- data.endedAtZ = endedAt;
15702
- }
15703
- if (data.startedAt instanceof Date) {
15704
- const startedAt = data.startedAt.toISOString();
15705
- data.startedAt = startedAt;
15706
- data.startedAtZ = startedAt;
15707
- }
15708
- return {
15709
- keys: {
15710
- spanId: record.spanId,
15711
- traceId: record.traceId
15712
- },
15713
- data
15714
- };
15715
- })
15760
+ updates: this.#toSpanUpdates(args.records)
15716
15761
  });
15717
15762
  } catch (error) {
15718
15763
  throw new MastraError({
@@ -15722,6 +15767,25 @@ var ObservabilityPG = class ObservabilityPG extends ObservabilityStorage {
15722
15767
  }, error);
15723
15768
  }
15724
15769
  }
15770
+ async batchUpdateUnendedSpans(args) {
15771
+ try {
15772
+ const applied = await this.#db.batchUpdateWhereNull({
15773
+ tableName: TABLE_SPANS,
15774
+ updates: this.#toSpanUpdates(args.records),
15775
+ nullColumns: ["endedAt"]
15776
+ });
15777
+ return { updated: args.records.filter((_, index) => applied[index]).map(({ traceId, spanId }) => ({
15778
+ traceId,
15779
+ spanId
15780
+ })) };
15781
+ } catch (error) {
15782
+ throw new MastraError({
15783
+ id: createStorageErrorId("PG", "BATCH_UPDATE_UNENDED_SPANS", "FAILED"),
15784
+ domain: ErrorDomain.STORAGE,
15785
+ category: ErrorCategory.USER
15786
+ }, error);
15787
+ }
15788
+ }
15725
15789
  async batchDeleteTraces(args) {
15726
15790
  this.assertUnscopedBatchDeleteTraces(args);
15727
15791
  try {
@@ -20560,7 +20624,7 @@ async function queryThreadRows(client, schema, plan, timeoutMs, strategy = "auto
20560
20624
  return `"${field}" ${field === "organizationId" || field === "resourceId" ? "IS NOT DISTINCT FROM" : "="} $${values.length}`;
20561
20625
  }).join(" AND ")})`;
20562
20626
  }));
20563
- const payloads = await run(`SELECT "organizationId", "traceId", "cursorId", "spanType", ${contextFields.map((field) => `"${field}"`).join(", ")},
20627
+ const payloads = await run(`SELECT "organizationId", "traceId", "cursorId", ${contextFields.map((field) => `"${field}"`).join(", ")},
20564
20628
  CASE WHEN "spanType" = 'agent_run' THEN "timeToFirstChunkMs" END AS "timeToFirstChunkMs",
20565
20629
  CASE WHEN error IS NULL THEN 'success' ELSE 'error' END AS status,
20566
20630
  ${[
@@ -20575,30 +20639,10 @@ async function queryThreadRows(client, schema, plan, timeoutMs, strategy = "auto
20575
20639
  String(version)
20576
20640
  ]);
20577
20641
  const byRoot = new Map(payloads.map((row) => [key(row.organizationId, row.traceId, row.cursorId), row]));
20578
- const agents = page.filter((row) => byRoot.get(key(row.organizationId, row.firstTraceId, row.firstCursorId))?.spanType === "agent_run");
20579
- const models = /* @__PURE__ */ new Map();
20580
- if (agents.length) {
20581
- const modelValues = [];
20582
- const rows = await run(`SELECT DISTINCT ON ("organizationId", "traceId") "organizationId", "traceId",
20583
- CASE WHEN jsonb_typeof(attributes -> 'model') = 'string' THEN attributes ->> 'model' END AS model,
20584
- CASE WHEN jsonb_typeof(attributes -> 'provider') = 'string' THEN attributes ->> 'provider' END AS provider
20585
- FROM ${table} WHERE "spanType" = 'model_generation' AND NOT "isPending" AND (${agents.map((row) => `(${Object.entries({
20586
- organizationId: row.organizationId,
20587
- resourceId: row.resourceId,
20588
- traceId: row.firstTraceId,
20589
- parentSpanId: row.firstSpanId
20590
- }).map(([field, value]) => {
20591
- modelValues.push(value);
20592
- return `"${field}" ${field === "organizationId" || field === "resourceId" ? "IS NOT DISTINCT FROM" : "="} $${modelValues.length}`;
20593
- }).join(" AND ")})`).join(" OR ")})
20594
- ORDER BY "organizationId", "traceId", "startedAt", "spanId" COLLATE "C", "cursorId" DESC`, modelValues);
20595
- for (const row of rows) models.set(JSON.stringify([row.organizationId, row.traceId]), row);
20596
- }
20597
20642
  const threads = page.flatMap((row) => {
20598
20643
  const first = byRoot.get(key(row.organizationId, row.firstTraceId, row.firstCursorId));
20599
20644
  const last = byRoot.get(key(row.organizationId, row.lastTraceId, row.lastCursorId));
20600
20645
  if (!first || !last) return [];
20601
- const model = models.get(JSON.stringify([row.organizationId, row.firstTraceId]));
20602
20646
  return [coreStorage.threadQueryRowSchema.parse({
20603
20647
  organizationId: row.organizationId,
20604
20648
  threadId: row.threadId,
@@ -20610,8 +20654,6 @@ async function queryThreadRows(client, schema, plan, timeoutMs, strategy = "auto
20610
20654
  endedAt: row.endedAt.toISOString(),
20611
20655
  traceCount: Number(row.traceCount),
20612
20656
  timeToFirstChunkMs: first.timeToFirstChunkMs,
20613
- model: model?.model ?? null,
20614
- provider: model?.provider ?? null,
20615
20657
  inputPreview: readPreview(first.inputPreview, first.input),
20616
20658
  outputPreview: readPreview(last.outputPreview, last.output),
20617
20659
  errorPreview: readPreview(last.errorPreview, last.error)
@@ -25595,6 +25637,13 @@ var WorkflowsPG = class WorkflowsPG extends WorkflowsStorage {
25595
25637
  supportsConcurrentUpdates() {
25596
25638
  return true;
25597
25639
  }
25640
+ supportsGuardedSnapshotWrites() {
25641
+ return true;
25642
+ }
25643
+ /** SQL for a snapshot's status. Legacy json/text columns can hold escapes that fail a jsonb cast (#11563). */
25644
+ async #statusExpression(column) {
25645
+ return await this.#db.getColumnType(TABLE_WORKFLOW_SNAPSHOT, "snapshot") === "jsonb" ? `${column} ->> 'status'` : `regexp_replace(${column}::text, '\\\\u(0000|[Dd][89A-Fa-f][0-9A-Fa-f]{2})', '', 'g')::jsonb ->> 'status'`;
25646
+ }
25598
25647
  parseWorkflowRun(row) {
25599
25648
  let parsedSnapshot = row.snapshot;
25600
25649
  if (typeof parsedSnapshot === "string") try {
@@ -25990,20 +26039,22 @@ var WorkflowsPG = class WorkflowsPG extends WorkflowsStorage {
25990
26039
  }, error);
25991
26040
  }
25992
26041
  }
25993
- async persistWorkflowSnapshot({ workflowName, runId, resourceId, snapshot, createdAt, updatedAt, fence }) {
26042
+ async persistWorkflowSnapshot({ workflowName, runId, resourceId, snapshot, createdAt, updatedAt, fence, expectedStatus }) {
25994
26043
  try {
25995
26044
  const now = /* @__PURE__ */ new Date();
25996
26045
  const createdAtValue = createdAt ? createdAt : now;
25997
26046
  const updatedAtValue = updatedAt ? updatedAt : now;
25998
26047
  const sanitizedSnapshot = toPgJson(snapshot);
25999
- await withRunFence(this.#db.client, this.#runOwnersTable(), resolveRunFence(this, fence, runId), "persistWorkflowSnapshot", (q) => q.none(`INSERT INTO ${getTableName({
26048
+ const guard = expectedStatus === void 0 ? "" : ` WHERE ${await this.#statusExpression("t.snapshot")} = ANY($9::text[])`;
26049
+ return await withRunFence(this.#db.client, this.#runOwnersTable(), resolveRunFence(this, fence, runId), "persistWorkflowSnapshot", (q) => q.oneOrNone(`INSERT INTO ${getTableName({
26000
26050
  indexName: TABLE_WORKFLOW_SNAPSHOT,
26001
26051
  schemaName: getSchemaName(this.#schema)
26002
26052
  })} AS t
26003
26053
  (workflow_name, run_id, "resourceId", snapshot, "createdAt", "updatedAt", "createdAtZ", "updatedAtZ")
26004
26054
  VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
26005
26055
  ON CONFLICT (workflow_name, run_id) DO UPDATE
26006
- SET "resourceId" = COALESCE($3, t."resourceId"), snapshot = $4, "updatedAt" = $6, "updatedAtZ" = $8`, [
26056
+ SET "resourceId" = COALESCE($3, t."resourceId"), snapshot = $4, "updatedAt" = $6, "updatedAtZ" = $8${guard}
26057
+ RETURNING 1 AS written`, [
26007
26058
  workflowName,
26008
26059
  runId,
26009
26060
  resourceId,
@@ -26011,8 +26062,9 @@ var WorkflowsPG = class WorkflowsPG extends WorkflowsStorage {
26011
26062
  createdAtValue,
26012
26063
  updatedAtValue,
26013
26064
  createdAtValue,
26014
- updatedAtValue
26015
- ]));
26065
+ updatedAtValue,
26066
+ ...expectedStatus === void 0 ? [] : [Array.isArray(expectedStatus) ? expectedStatus : [expectedStatus]]
26067
+ ])) !== null;
26016
26068
  } catch (error) {
26017
26069
  if (isRunFenceConflictError(error)) throw error;
26018
26070
  throw new MastraError({
@@ -26022,6 +26074,25 @@ var WorkflowsPG = class WorkflowsPG extends WorkflowsStorage {
26022
26074
  }, error);
26023
26075
  }
26024
26076
  }
26077
+ async loadWorkflowRunStatus({ workflowName, runId }) {
26078
+ try {
26079
+ const statusExpr = await this.#statusExpression("snapshot");
26080
+ return (await this.#db.client.oneOrNone(`SELECT ${statusExpr} AS status FROM ${getTableName({
26081
+ indexName: TABLE_WORKFLOW_SNAPSHOT,
26082
+ schemaName: getSchemaName(this.#schema)
26083
+ })} WHERE "workflow_name" = $1 AND "run_id" = $2 ORDER BY "createdAt" DESC LIMIT 1`, [workflowName, runId]))?.status ?? null;
26084
+ } catch (error) {
26085
+ throw new MastraError({
26086
+ id: createStorageErrorId("PG", "LOAD_WORKFLOW_RUN_STATUS", "FAILED"),
26087
+ domain: ErrorDomain.STORAGE,
26088
+ category: ErrorCategory.THIRD_PARTY,
26089
+ details: {
26090
+ workflowName,
26091
+ runId
26092
+ }
26093
+ }, error);
26094
+ }
26095
+ }
26025
26096
  async loadWorkflowSnapshot({ workflowName, runId }) {
26026
26097
  try {
26027
26098
  const result = await this.#db.load({