@mastra/pg 1.35.0 → 1.36.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.
Files changed (30) hide show
  1. package/dist/docs/SKILL.md +2 -13
  2. package/dist/docs/assets/SOURCE_MAP.json +158 -2
  3. package/dist/docs/references/integrations-databases-neon.md +37 -1
  4. package/dist/docs/references/integrations-databases-postgresql.md +46 -0
  5. package/dist/docs/references/reference-processors-message-history-processor.md +1 -1
  6. package/dist/docs/references/reference-processors-semantic-recall-processor.md +1 -1
  7. package/dist/docs/references/reference-tools-vector-query-tool.md +3 -3
  8. package/dist/index.cjs +797 -64
  9. package/dist/index.cjs.map +1 -1
  10. package/dist/index.js +798 -65
  11. package/dist/index.js.map +1 -1
  12. package/dist/storage/db/index.d.ts +3 -3
  13. package/dist/storage/db/index.d.ts.map +1 -1
  14. package/dist/storage/domains/observability/v-next/feedback.d.ts +1 -0
  15. package/dist/storage/domains/observability/v-next/feedback.d.ts.map +1 -1
  16. package/dist/storage/domains/observability/v-next/index.d.ts +1 -0
  17. package/dist/storage/domains/observability/v-next/index.d.ts.map +1 -1
  18. package/dist/storage/domains/workflows/index.d.ts +87 -7
  19. package/dist/storage/domains/workflows/index.d.ts.map +1 -1
  20. package/dist/storage/retention.d.ts +3 -1
  21. package/dist/storage/retention.d.ts.map +1 -1
  22. package/package.json +3 -3
  23. package/dist/docs/references/docs-deployment-workers.md +0 -404
  24. package/dist/docs/references/docs-memory-semantic-recall.md +0 -485
  25. package/dist/docs/references/docs-memory-working-memory.md +0 -450
  26. package/dist/docs/references/docs-storage.md +0 -224
  27. package/dist/docs/references/reference-rag-retrieval.md +0 -672
  28. package/dist/docs/references/reference-rag-vector-databases.md +0 -808
  29. package/dist/docs/references/reference-storage-composite.md +0 -433
  30. package/dist/docs/references/reference-storage-retention.md +0 -310
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { ErrorCategory, ErrorDomain, MastraError } from "@mastra/core/error";
2
2
  import * as coreStorage from "@mastra/core/storage";
3
- import { AgentsStorage, BRANCH_SPAN_TYPES, BackgroundTasksStorage, BlobStore, ChannelsStorage, DATASETS_SCHEMA, DATASET_ITEMS_SCHEMA, DATASET_VERSIONS_SCHEMA, DatasetsStorage, EXPERIMENTS_SCHEMA, EXPERIMENT_RESULTS_SCHEMA, EntityType, ExperimentsStorage, FactoryStorage, FavoritesStorage, KnowledgeConflictError, KnowledgeNotFoundError, KnowledgeStorage, MCPClientsStorage, MCPServersStorage, METRIC_DISTINCT_COLUMNS, MastraCompositeStore, MemoryStorage, NotificationsStorage, OBSERVATIONAL_MEMORY_TABLE_SCHEMA, ObservabilityStorage, PromptBlocksStorage, RUN_FENCING_TABLE_SCHEMAS, RunFenceConflictError, SchedulesStorage, ScorerDefinitionsStorage, ScoresStorage, SkillsStorage, TABLE_AGENTS, TABLE_AGENT_VERSIONS, TABLE_BACKGROUND_TASKS, TABLE_CHANNEL_CONFIG, TABLE_CHANNEL_INSTALLATIONS, TABLE_CONFIGS, TABLE_DATASETS, TABLE_DATASET_ITEMS, TABLE_DATASET_VERSIONS, TABLE_EXPERIMENTS, TABLE_EXPERIMENT_RESULTS, TABLE_FAVORITES, TABLE_MCP_CLIENTS, TABLE_MCP_CLIENT_VERSIONS, TABLE_MCP_SERVERS, TABLE_MCP_SERVER_VERSIONS, TABLE_MEMORY_RUN_FENCES, TABLE_MESSAGES, TABLE_NOTIFICATIONS, TABLE_PROMPT_BLOCKS, TABLE_PROMPT_BLOCK_VERSIONS, TABLE_RESOURCES, TABLE_SCHEDULES, TABLE_SCHEDULE_TRIGGERS, TABLE_SCHEMAS, TABLE_SCORERS, TABLE_SCORER_DEFINITIONS, TABLE_SCORER_DEFINITION_VERSIONS, TABLE_SKILLS, TABLE_SKILL_BLOBS, TABLE_SKILL_VERSIONS, TABLE_SPANS, TABLE_THREADS, TABLE_THREAD_STATE, TABLE_TOOL_PROVIDER_CONNECTIONS, TABLE_WORKFLOW_DEFINITIONS, TABLE_WORKFLOW_RUN_OWNERS, TABLE_WORKFLOW_SNAPSHOT, TABLE_WORKSPACES, TABLE_WORKSPACE_VERSIONS, ThreadStateStorage, ToolProviderConnectionsStorage, TraceStatus, UniqueViolationError, WorkflowDefinitionsStorage, WorkflowsStorage, WorkspacesStorage, assertWorkflowStepGuard, calculatePagination, computeTraceStatus, createKnowledgeUlid, createStorageErrorId, createVectorErrorId, ensureDate, executeRetentionPrune, getDefaultValue, getSqlType, getStringAttribute, hasErrorCode, isRunFenceConflictError, isWorkflowStepConflictError, listBranchesArgsSchema, listFeedbackArgsSchema, listLogsArgsSchema, listMetricsArgsSchema, listScoresArgsSchema, listTracesArgsSchema, matchesExpectedWorkflowStatus, matchesRunFence, mergeWorkflowStepResult, normalizePerPage, normalizeScheduleTarget, parseDuration, resolveListOrderBy, resolveRetentionTargets, resolveRunFence, retentionCutoffMs, runRetentionBatches, safelyParseJSON, storageMessageMatchesMetadataFilter, toTraceSpans, transformScoreRow, validateStorageMetadataFilter } from "@mastra/core/storage";
3
+ import { AgentsStorage, BRANCH_SPAN_TYPES, BackgroundTasksStorage, BlobStore, ChannelsStorage, DATASETS_SCHEMA, DATASET_ITEMS_SCHEMA, DATASET_VERSIONS_SCHEMA, DatasetsStorage, EXPERIMENTS_SCHEMA, EXPERIMENT_RESULTS_SCHEMA, EntityType, ExperimentsStorage, FactoryStorage, FavoritesStorage, KnowledgeConflictError, KnowledgeNotFoundError, KnowledgeStorage, MCPClientsStorage, MCPServersStorage, METRIC_DISTINCT_COLUMNS, MastraCompositeStore, MemoryStorage, NotificationsStorage, OBSERVATIONAL_MEMORY_TABLE_SCHEMA, ObservabilityStorage, PromptBlocksStorage, RUN_FENCING_TABLE_SCHEMAS, RunFenceConflictError, STEP_RESULT_SUMMARY_FIELDS, SchedulesStorage, ScorerDefinitionsStorage, ScoresStorage, SkillsStorage, TABLE_AGENTS, TABLE_AGENT_VERSIONS, TABLE_BACKGROUND_TASKS, TABLE_CHANNEL_CONFIG, TABLE_CHANNEL_INSTALLATIONS, TABLE_CONFIGS, TABLE_DATASETS, TABLE_DATASET_ITEMS, TABLE_DATASET_VERSIONS, TABLE_EXPERIMENTS, TABLE_EXPERIMENT_RESULTS, TABLE_FAVORITES, TABLE_MCP_CLIENTS, TABLE_MCP_CLIENT_VERSIONS, TABLE_MCP_SERVERS, TABLE_MCP_SERVER_VERSIONS, TABLE_MEMORY_RUN_FENCES, TABLE_MESSAGES, TABLE_NOTIFICATIONS, TABLE_PROMPT_BLOCKS, TABLE_PROMPT_BLOCK_VERSIONS, TABLE_RESOURCES, TABLE_SCHEDULES, TABLE_SCHEDULE_TRIGGERS, TABLE_SCHEMAS, TABLE_SCORERS, TABLE_SCORER_DEFINITIONS, TABLE_SCORER_DEFINITION_VERSIONS, TABLE_SKILLS, TABLE_SKILL_BLOBS, TABLE_SKILL_VERSIONS, TABLE_SPANS, TABLE_THREADS, TABLE_THREAD_STATE, TABLE_TOOL_PROVIDER_CONNECTIONS, TABLE_WORKFLOW_DEFINITIONS, TABLE_WORKFLOW_RUNS, TABLE_WORKFLOW_RUN_ENTRIES, TABLE_WORKFLOW_RUN_OWNERS, TABLE_WORKFLOW_SNAPSHOT, TABLE_WORKSPACES, TABLE_WORKSPACE_VERSIONS, ThreadStateStorage, ToolProviderConnectionsStorage, TraceStatus, UniqueViolationError, WORKFLOW_RUN_JOURNAL_TABLE_CONFIGS, WorkflowDefinitionsStorage, WorkflowsStorage, WorkspacesStorage, assertWorkflowStepGuard, calculatePagination, computeTraceStatus, createKnowledgeUlid, createStorageErrorId, createVectorErrorId, ensureDate, executeRetentionPrune, getDefaultValue, getSqlType, getStringAttribute, hasErrorCode, isRunFenceConflictError, isWorkflowStepConflictError, latestStepResults, listBranchesArgsSchema, listFeedbackArgsSchema, listLogsArgsSchema, listMetricsArgsSchema, listScoresArgsSchema, listTracesArgsSchema, matchesExpectedWorkflowStatus, matchesRunFence, materializeState, materializeWorkflowRun, mergeWorkflowStepResult, normalizePerPage, normalizeScheduleTarget, parseDuration, resolveListOrderBy, resolveRetentionTargets, resolveRunFence, retentionCutoffMs, runRetentionBatches, safelyParseJSON, stateBaseOf, storageMessageMatchesMetadataFilter, summarizeStepResult, toTraceSpans, transformScoreRow, validateStorageMetadataFilter } from "@mastra/core/storage";
4
4
  import { parseFieldKey, parseSqlIdentifier } from "@mastra/core/utils";
5
5
  import { MastraVector, validateTopK, validateUpsertInput } from "@mastra/core/vector";
6
6
  import { Mutex } from "async-mutex";
@@ -5219,13 +5219,13 @@ function cutoffFor(policy, anchorType, now = Date.now()) {
5219
5219
  return anchorType === "epoch-ms" ? cutoffMs : new Date(cutoffMs);
5220
5220
  }
5221
5221
  const runBatchedDelete = runRetentionBatches;
5222
- function runPrune({ db, domain, targets, options }) {
5222
+ function runPrune({ db, domain, targets, options, deleteBatch }) {
5223
5223
  return executeRetentionPrune({
5224
5224
  domain,
5225
5225
  targets,
5226
5226
  options,
5227
5227
  cutoffFor: (target, now) => cutoffFor(target.policy, target.anchorType ?? "timestamp", now),
5228
- deleteBatch: (target, cutoff, limit) => db.pruneBatch({
5228
+ deleteBatch: (target, cutoff, limit) => deleteBatch?.(target, cutoff, limit) ?? db.pruneBatch({
5229
5229
  tableName: target.table,
5230
5230
  column: target.column,
5231
5231
  cutoff,
@@ -18314,6 +18314,11 @@ async function batchCreateFeedback(client, schema, args, partitioning) {
18314
18314
  const rows = [...new Map(args.feedbacks.map((feedback) => [feedback.feedbackId, feedback])).values()].map(feedbackRecordToRow);
18315
18315
  await insertRows(client, schema, TABLE_FEEDBACK_EVENTS, rows, partitioning, FEEDBACK_UPSERT_CLAUSE);
18316
18316
  }
18317
+ async function getFeedbackById(client, schema, feedbackId) {
18318
+ const row = await client.oneOrNone(`SELECT ${FEEDBACK_SELECT_COLUMNS} FROM ${qualifiedTable(schema, TABLE_FEEDBACK_EVENTS)}
18319
+ WHERE "feedbackId" = $1 ORDER BY "cursorId" DESC LIMIT 1`, [feedbackId]);
18320
+ return row ? rowToFeedbackRecord(row) : null;
18321
+ }
18317
18322
  async function updateFeedbackReviewStatus(client, schema, args) {
18318
18323
  const { feedbackId, reviewStatus } = parseUpdateFeedbackReviewStatusArgs(args);
18319
18324
  const row = await client.oneOrNone(`UPDATE ${qualifiedTable(schema, TABLE_FEEDBACK_EVENTS)}
@@ -22149,6 +22154,9 @@ var ObservabilityStoragePostgresVNext = class ObservabilityStoragePostgresVNext
22149
22154
  async getScoreById(scoreId) {
22150
22155
  return this.#run("GET_SCORE_BY_ID", () => getScoreById(this.#readClient, this.#schema, scoreId), { scoreId });
22151
22156
  }
22157
+ async getFeedbackById(feedbackId) {
22158
+ return this.#run("GET_FEEDBACK_BY_ID", () => getFeedbackById(this.#readClient, this.#schema, feedbackId), { feedbackId });
22159
+ }
22152
22160
  async listFeedback(args) {
22153
22161
  return this.#run("LIST_FEEDBACK", () => listFeedback(this.#readClient, this.#schema, args));
22154
22162
  }
@@ -25602,6 +25610,16 @@ function workflowSnapshotStatusIndexSQL(indexName, schemaName, concurrently = fa
25602
25610
  });
25603
25611
  return `CREATE INDEX ${concurrently ? "CONCURRENTLY " : ""}IF NOT EXISTS "${indexName}" ON ${tableName} (workflow_name, (snapshot ->> 'status'), "createdAt" DESC)`;
25604
25612
  }
25613
+ /**
25614
+ * Storage v2 index names share the long `<schema>_mastra_workflow_runs_` prefix, so a long
25615
+ * schema name would collapse them to one 63-byte identifier under plain truncation; hash instead.
25616
+ */
25617
+ function runIndexName(schemaPrefix, baseName) {
25618
+ return buildConstraintName({
25619
+ baseName: `${schemaPrefix}${baseName}`,
25620
+ hashWhenTruncated: true
25621
+ });
25622
+ }
25605
25623
  /** Base name (before any schema prefix) of the expression index backing the threadId filter. */
25606
25624
  const WORKFLOW_SNAPSHOT_THREAD_ID_INDEX = "mastra_workflow_snapshot_threadid_idx";
25607
25625
  /**
@@ -25646,13 +25664,126 @@ function workflowSnapshotThreadIdIndexSQL(indexName, schemaName, concurrently =
25646
25664
  });
25647
25665
  return `CREATE INDEX ${concurrently ? "CONCURRENTLY " : ""}IF NOT EXISTS "${indexName}" ON ${tableName} ((${WORKFLOW_SNAPSHOT_THREAD_ID_EXPR}))`;
25648
25666
  }
25667
+ /** Base name (before any schema prefix) of the index finding the `state-base` entries built on a run. */
25668
+ const WORKFLOW_RUN_ENTRIES_STATE_BASE_INDEX = "mastra_workflow_run_entries_state_base_idx";
25669
+ /** The run a `state-base` entry builds on; `#detachStateChildren` must use these exact expressions. */
25670
+ const STATE_BASE_RUN_EXPR = `(payload ->> 'workflowName'), (payload ->> 'runId')`;
25671
+ /**
25672
+ * Partial expression index finding the `state-base` entries built on a run, so deleting a run whose
25673
+ * state others build on doesn't scan every entry. See {@link workflowSnapshotStatusIndexSQL} for
25674
+ * `concurrently`.
25675
+ */
25676
+ function stateBaseIndexSQL(indexName, schemaName, concurrently = false) {
25677
+ const tableName = getTableName({
25678
+ indexName: TABLE_WORKFLOW_RUN_ENTRIES,
25679
+ schemaName: getSchemaName(schemaName)
25680
+ });
25681
+ return `CREATE INDEX ${concurrently ? "CONCURRENTLY " : ""}IF NOT EXISTS "${indexName}" ON ${tableName} (${STATE_BASE_RUN_EXPR}) WHERE kind = 'state-base'`;
25682
+ }
25683
+ /**
25684
+ * The entries a storage v2 write adds: one per step, in order, then one per step patch, then the
25685
+ * state or state base (a checkpoint with neither starts the state over at `{}`), then the state
25686
+ * patch unless a full state was given.
25687
+ */
25688
+ function journalEntries(steps, { state, stateBase, statePatch }, checkpoint, stepPatches = {}) {
25689
+ const entries = Object.entries(steps).map(([stepId, result]) => ({
25690
+ kind: "step",
25691
+ step_id: stepId,
25692
+ payload: result
25693
+ }));
25694
+ for (const [stepId, ops] of Object.entries(stepPatches)) entries.push({
25695
+ kind: "step-patch",
25696
+ step_id: stepId,
25697
+ payload: ops
25698
+ });
25699
+ if (state !== void 0) entries.push({
25700
+ kind: "state",
25701
+ step_id: null,
25702
+ payload: state
25703
+ });
25704
+ else if (stateBase !== void 0) entries.push({
25705
+ kind: "state-base",
25706
+ step_id: null,
25707
+ payload: stateBase
25708
+ });
25709
+ else if (checkpoint) entries.push({
25710
+ kind: "state",
25711
+ step_id: null,
25712
+ payload: {}
25713
+ });
25714
+ if (state === void 0 && statePatch !== void 0) entries.push({
25715
+ kind: "state-patch",
25716
+ step_id: null,
25717
+ payload: statePatch
25718
+ });
25719
+ return entries;
25720
+ }
25721
+ /**
25722
+ * Rows `e(elem, ord)` of the jsonb array parameter `param` of {@link EntryInput}, and the
25723
+ * entry columns they insert. `elem -> 'payload'` keeps a JSON null apart from an undefined
25724
+ * payload (dropped by `JSON.stringify`), which becomes SQL NULL.
25725
+ */
25726
+ const entryRows = (param) => `jsonb_array_elements(${param}::jsonb) WITH ORDINALITY AS e(elem, ord)`;
25727
+ const ENTRY_VALUES = `e.elem ->> 'kind', e.elem ->> 'step_id', e.elem -> 'payload'`;
25728
+ /** The entries of {@link EntryRow} rows; an undefined payload reads back as undefined, as in v1. */
25729
+ function toEntries(rows) {
25730
+ return rows.map((row) => {
25731
+ const payload = row.absent ? void 0 : row.payload;
25732
+ switch (row.kind) {
25733
+ case "step": return {
25734
+ seq: row.seq,
25735
+ kind: "step",
25736
+ stepId: row.step_id,
25737
+ result: payload
25738
+ };
25739
+ case "step-patch": return {
25740
+ seq: row.seq,
25741
+ kind: "step-patch",
25742
+ stepId: row.step_id,
25743
+ ops: payload
25744
+ };
25745
+ case "state": return {
25746
+ seq: row.seq,
25747
+ kind: "state",
25748
+ state: payload
25749
+ };
25750
+ case "state-patch": return {
25751
+ seq: row.seq,
25752
+ kind: "state-patch",
25753
+ ops: payload
25754
+ };
25755
+ case "state-base": return {
25756
+ seq: row.seq,
25757
+ kind: "state-base",
25758
+ base: payload
25759
+ };
25760
+ case "state-draft": {
25761
+ const draft = payload;
25762
+ return "state" in draft ? {
25763
+ seq: row.seq,
25764
+ kind: "state-draft",
25765
+ state: draft.state
25766
+ } : {
25767
+ seq: row.seq,
25768
+ kind: "state-draft",
25769
+ on: draft.on,
25770
+ ops: draft.ops
25771
+ };
25772
+ }
25773
+ }
25774
+ });
25775
+ }
25649
25776
  var WorkflowsPG = class WorkflowsPG extends WorkflowsStorage {
25650
25777
  #db;
25651
25778
  #schema;
25652
25779
  #skipDefaultIndexes;
25653
25780
  #indexes;
25654
25781
  /** Tables managed by this domain */
25655
- static MANAGED_TABLES = [TABLE_WORKFLOW_SNAPSHOT];
25782
+ static MANAGED_TABLES = [
25783
+ TABLE_WORKFLOW_SNAPSHOT,
25784
+ TABLE_WORKFLOW_RUNS,
25785
+ TABLE_WORKFLOW_RUN_ENTRIES
25786
+ ];
25656
25787
  /**
25657
25788
  * Workflow run snapshots accumulate as runs execute. Anchored on the
25658
25789
  * timezone-aware `updatedAtZ` mirror column (last activity) so suspended or
@@ -25715,11 +25846,43 @@ var WorkflowsPG = class WorkflowsPG extends WorkflowsStorage {
25715
25846
  };
25716
25847
  }
25717
25848
  static getDefaultIndexDefs(schemaPrefix) {
25718
- return [{
25719
- name: `${schemaPrefix}mastra_workflow_snapshot_name_createdat_idx`,
25720
- table: TABLE_WORKFLOW_SNAPSHOT,
25721
- columns: ["workflow_name", "createdAt DESC"]
25722
- }];
25849
+ return [
25850
+ {
25851
+ name: `${schemaPrefix}mastra_workflow_snapshot_name_createdat_idx`,
25852
+ table: TABLE_WORKFLOW_SNAPSHOT,
25853
+ columns: ["workflow_name", "createdAt DESC"]
25854
+ },
25855
+ {
25856
+ name: runIndexName(schemaPrefix, "mastra_workflow_runs_name_createdat_idx"),
25857
+ table: TABLE_WORKFLOW_RUNS,
25858
+ columns: ["workflow_name", "createdAt DESC"]
25859
+ },
25860
+ {
25861
+ name: runIndexName(schemaPrefix, "mastra_workflow_runs_name_status_createdat_idx"),
25862
+ table: TABLE_WORKFLOW_RUNS,
25863
+ columns: [
25864
+ "workflow_name",
25865
+ "status",
25866
+ "createdAt DESC"
25867
+ ]
25868
+ },
25869
+ {
25870
+ name: runIndexName(schemaPrefix, "mastra_workflow_runs_threadid_idx"),
25871
+ table: TABLE_WORKFLOW_RUNS,
25872
+ columns: ["threadId"]
25873
+ },
25874
+ {
25875
+ name: runIndexName(schemaPrefix, "mastra_workflow_run_entries_step_idx"),
25876
+ table: TABLE_WORKFLOW_RUN_ENTRIES,
25877
+ columns: [
25878
+ "workflow_name",
25879
+ "run_id",
25880
+ "kind",
25881
+ "step_id",
25882
+ "seq"
25883
+ ]
25884
+ }
25885
+ ];
25723
25886
  }
25724
25887
  /**
25725
25888
  * Returns all DDL statements for this domain: table with unique constraint.
@@ -25735,9 +25898,17 @@ var WorkflowsPG = class WorkflowsPG extends WorkflowsStorage {
25735
25898
  schemaName,
25736
25899
  includeAllConstraints: true
25737
25900
  }));
25901
+ for (const [tableName, config] of Object.entries(WORKFLOW_RUN_JOURNAL_TABLE_CONFIGS)) statements.push(generateTableSQL({
25902
+ tableName,
25903
+ schema: config.columns,
25904
+ schemaName,
25905
+ compositePrimaryKey: config.compositePrimaryKey,
25906
+ includeAllConstraints: true
25907
+ }));
25738
25908
  for (const idx of WorkflowsPG.getDefaultIndexDefs(schemaPrefix)) statements.push(generateIndexSQL(idx, schemaName));
25739
25909
  statements.push(`${workflowSnapshotStatusIndexSQL(workflowSnapshotStatusIndexName(parsedSchema), schemaName)};`);
25740
25910
  statements.push(`${workflowSnapshotThreadIdIndexSQL(workflowSnapshotThreadIdIndexName(parsedSchema), schemaName)};`);
25911
+ statements.push(`${stateBaseIndexSQL(runIndexName(schemaPrefix, WORKFLOW_RUN_ENTRIES_STATE_BASE_INDEX), schemaName)};`);
25741
25912
  statements.push(generateTableSQL({
25742
25913
  tableName: TABLE_WORKFLOW_RUN_OWNERS,
25743
25914
  schema: RUN_FENCING_TABLE_SCHEMAS[TABLE_WORKFLOW_RUN_OWNERS],
@@ -25763,6 +25934,12 @@ var WorkflowsPG = class WorkflowsPG extends WorkflowsStorage {
25763
25934
  } catch (error) {
25764
25935
  this.logger?.warn?.(`Failed to create index ${indexDef.name}:`, error);
25765
25936
  }
25937
+ const stateBaseIndexName = runIndexName(this.#schema !== "public" ? `${schemaNamePrefix(this.#schema)}_` : "", WORKFLOW_RUN_ENTRIES_STATE_BASE_INDEX);
25938
+ try {
25939
+ await this.#db.createIndexFromStatement(stateBaseIndexName, stateBaseIndexSQL(stateBaseIndexName, this.#schema, true));
25940
+ } catch (error) {
25941
+ this.logger?.warn?.(`Failed to create index ${stateBaseIndexName}:`, error);
25942
+ }
25766
25943
  if (await this.#db.getColumnType(TABLE_WORKFLOW_SNAPSHOT, "snapshot") !== "jsonb") return;
25767
25944
  const indexName = workflowSnapshotStatusIndexName(this.#schema);
25768
25945
  try {
@@ -25791,6 +25968,11 @@ var WorkflowsPG = class WorkflowsPG extends WorkflowsStorage {
25791
25968
  tableName: TABLE_WORKFLOW_RUN_OWNERS,
25792
25969
  schema: RUN_FENCING_TABLE_SCHEMAS[TABLE_WORKFLOW_RUN_OWNERS]
25793
25970
  });
25971
+ for (const [tableName, config] of Object.entries(WORKFLOW_RUN_JOURNAL_TABLE_CONFIGS)) await this.#db.createTable({
25972
+ tableName,
25973
+ schema: config.columns,
25974
+ compositePrimaryKey: config.compositePrimaryKey
25975
+ });
25794
25976
  await this.createDefaultIndexes();
25795
25977
  await this.createCustomIndexes();
25796
25978
  }
@@ -25817,8 +25999,22 @@ var WorkflowsPG = class WorkflowsPG extends WorkflowsStorage {
25817
25999
  this.logger?.warn?.(`Failed to create retention index for ${entry.table}:`, error);
25818
26000
  }
25819
26001
  }
26002
+ if (!policies.workflowSnapshot) return;
26003
+ try {
26004
+ await this.#db.ensureIndex({
26005
+ indexName: `${prefix}mastra_workflow_runs_retention_idx`,
26006
+ tableName: TABLE_WORKFLOW_RUNS,
26007
+ column: "updatedAtZ"
26008
+ });
26009
+ } catch (error) {
26010
+ this.logger?.warn?.(`Failed to create retention index for ${TABLE_WORKFLOW_RUNS}:`, error);
26011
+ }
25820
26012
  }
25821
- /** Delete workflow run snapshots and run ownership rows older than their policy's `maxAge`, batched. */
26013
+ /**
26014
+ * Delete workflow runs and run ownership rows older than their policy's `maxAge`, batched.
26015
+ * The `workflowSnapshot` policy covers both storage versions: storage v2 runs are pruned by
26016
+ * their header's last write, together with their entries.
26017
+ */
25822
26018
  async prune(policies, options) {
25823
26019
  await this.ensureRetentionIndexes(policies);
25824
26020
  const targets = resolveTargets({
@@ -25826,11 +26022,40 @@ var WorkflowsPG = class WorkflowsPG extends WorkflowsStorage {
25826
26022
  descriptor: WorkflowsPG.retentionTables,
25827
26023
  order: ["workflowSnapshot", "runOwnership"]
25828
26024
  });
26025
+ const snapshotTarget = targets.findIndex((target) => target.table === TABLE_WORKFLOW_SNAPSHOT);
26026
+ if (snapshotTarget !== -1) targets.splice(snapshotTarget + 1, 0, {
26027
+ ...targets[snapshotTarget],
26028
+ table: TABLE_WORKFLOW_RUNS
26029
+ });
25829
26030
  return runPrune({
25830
26031
  db: this.#db,
25831
26032
  domain: "workflows",
25832
26033
  targets,
25833
- options
26034
+ options,
26035
+ deleteBatch: (target, cutoff, limit) => target.table === TABLE_WORKFLOW_RUNS ? this.#pruneRunsBatch(cutoff, limit) : void 0
26036
+ });
26037
+ }
26038
+ /**
26039
+ * Deletes up to `limit` aged storage v2 runs, header and entries together, first detaching the
26040
+ * runs whose state others build on (see `#detachStateChildren`).
26041
+ */
26042
+ async #pruneRunsBatch(cutoff, limit) {
26043
+ return this.#db.client.tx(async (t) => {
26044
+ const doomed = await t.manyOrNone(`SELECT workflow_name, run_id, "stateShared" FROM ${this.#runsTable()}
26045
+ WHERE "updatedAtZ" < $1
26046
+ ORDER BY "updatedAtZ"
26047
+ LIMIT $2
26048
+ FOR UPDATE SKIP LOCKED`, [cutoff, limit]);
26049
+ if (!doomed.length) return 0;
26050
+ for (const run of doomed) if (run.stateShared) await this.#detachStateChildren(t, run.workflow_name, run.run_id);
26051
+ return (await t.query(`WITH doomed AS (
26052
+ SELECT * FROM unnest($1::text[], $2::text[]) AS d(workflow_name, run_id)
26053
+ ), entries AS (
26054
+ DELETE FROM ${this.#entriesTable()} e USING doomed d
26055
+ WHERE e.workflow_name = d.workflow_name AND e.run_id = d.run_id
26056
+ )
26057
+ DELETE FROM ${this.#runsTable()} r USING doomed d
26058
+ WHERE r.workflow_name = d.workflow_name AND r.run_id = d.run_id`, [doomed.map((run) => run.workflow_name), doomed.map((run) => run.run_id)])).rowCount ?? 0;
25834
26059
  });
25835
26060
  }
25836
26061
  /**
@@ -25847,6 +26072,459 @@ var WorkflowsPG = class WorkflowsPG extends WorkflowsStorage {
25847
26072
  async dangerouslyClearAll() {
25848
26073
  await this.#db.clearTable({ tableName: TABLE_WORKFLOW_SNAPSHOT });
25849
26074
  await this.#db.clearTable({ tableName: TABLE_WORKFLOW_RUN_OWNERS });
26075
+ await this.#db.clearTable({ tableName: TABLE_WORKFLOW_RUNS });
26076
+ await this.#db.clearTable({ tableName: TABLE_WORKFLOW_RUN_ENTRIES });
26077
+ }
26078
+ supportsRunJournal() {
26079
+ return true;
26080
+ }
26081
+ #runsTable() {
26082
+ return getTableName({
26083
+ indexName: TABLE_WORKFLOW_RUNS,
26084
+ schemaName: getSchemaName(this.#schema)
26085
+ });
26086
+ }
26087
+ #entriesTable() {
26088
+ return getTableName({
26089
+ indexName: TABLE_WORKFLOW_RUN_ENTRIES,
26090
+ schemaName: getSchemaName(this.#schema)
26091
+ });
26092
+ }
26093
+ /**
26094
+ * A jsonb array of {@link EntryRow} for the storage v2 run `alias` refers to: each step's
26095
+ * latest entry, ordered by when each step was first written so `context` keys keep their write
26096
+ * order, the step patches after it (in `seq` order, for `latestStepResults`), and the entries
26097
+ * `materializeState` uses for the latest state: the latest full state or state base and every
26098
+ * state patch after it, in `seq` order. Drafts are left out.
26099
+ *
26100
+ * With `summaryFieldsParam` (the parameter holding `STEP_RESULT_SUMMARY_FIELDS`), successful
26101
+ * steps other than `input` come back as their summary, built here so their data is never sent,
26102
+ * unless step patches follow it (the caller summarizes the patched result instead).
26103
+ * Like `summarizeStepResult`, it keeps a field that is JSON null and leaves out one that is absent.
26104
+ */
26105
+ #entriesSql(alias, { summaryFieldsParam } = {}) {
26106
+ let payload = "x.payload";
26107
+ if (summaryFieldsParam) {
26108
+ const summarized = `(x.kind = 'step' AND x.step_id <> 'input' AND x.payload ->> 'status' = 'success'
26109
+ AND (x.patch_seq IS NULL OR x.patch_seq < x.seq))`;
26110
+ payload = `CASE WHEN ${summarized} THEN (
26111
+ SELECT COALESCE(jsonb_object_agg(f.key, f.value), '{}'::jsonb)
26112
+ FROM jsonb_each(x.payload) f WHERE f.key = ANY(${summaryFieldsParam}::text[])
26113
+ ) ELSE x.payload END, 'summarized', ${summarized}`;
26114
+ }
26115
+ return `(SELECT COALESCE(jsonb_agg(jsonb_build_object(
26116
+ 'seq', x.seq, 'kind', x.kind, 'step_id', x.step_id, 'absent', x.payload IS NULL, 'payload', ${payload}
26117
+ ) ORDER BY CASE WHEN x.kind = 'step' THEN x.first_seq ELSE x.seq END), '[]'::jsonb)
26118
+ FROM (
26119
+ SELECT e.seq, e.kind, e.step_id, e.payload,
26120
+ ROW_NUMBER() OVER (PARTITION BY e.kind, e.step_id ORDER BY e.seq DESC) AS rn,
26121
+ MIN(e.seq) OVER (PARTITION BY e.kind, e.step_id) AS first_seq,
26122
+ MAX(CASE WHEN e.kind = 'step' THEN e.seq END) OVER (PARTITION BY e.step_id) AS step_seq,
26123
+ MAX(CASE WHEN e.kind = 'step-patch' THEN e.seq END) OVER (PARTITION BY e.step_id) AS patch_seq,
26124
+ MAX(CASE WHEN e.kind IN ('state', 'state-base') THEN e.seq END) OVER () AS base_seq
26125
+ FROM ${this.#entriesTable()} e
26126
+ WHERE e.workflow_name = ${alias}.workflow_name AND e.run_id = ${alias}.run_id AND e.kind <> 'state-draft'
26127
+ ) x
26128
+ WHERE CASE x.kind
26129
+ WHEN 'step' THEN x.rn = 1
26130
+ WHEN 'step-patch' THEN x.seq > x.step_seq
26131
+ ELSE x.base_seq IS NULL OR x.seq >= x.base_seq
26132
+ END)`;
26133
+ }
26134
+ /**
26135
+ * A jsonb array of {@link EntryRow} with the entries `materializeState` uses for the state of the
26136
+ * storage v2 run `alias` refers to as of a version: `seqParam` its `seq`, `draftParam` whether
26137
+ * it is a draft. For a draft, every state entry up to it, as the drafts it is on can build on
26138
+ * any earlier version; otherwise the latest full state or state base up to it and the state
26139
+ * patches after it.
26140
+ */
26141
+ #stateEntriesSql(alias, seqParam, draftParam) {
26142
+ return `(SELECT COALESCE(jsonb_agg(jsonb_build_object(
26143
+ 'seq', x.seq, 'kind', x.kind, 'step_id', x.step_id, 'absent', x.payload IS NULL, 'payload', x.payload
26144
+ ) ORDER BY x.seq), '[]'::jsonb)
26145
+ FROM (
26146
+ SELECT e.seq, e.kind, e.step_id, e.payload,
26147
+ MAX(CASE WHEN e.kind IN ('state', 'state-base') THEN e.seq END) OVER () AS base_seq
26148
+ FROM ${this.#entriesTable()} e
26149
+ WHERE e.workflow_name = ${alias}.workflow_name AND e.run_id = ${alias}.run_id
26150
+ AND e.kind NOT IN ('step', 'step-patch') AND e.seq <= ${seqParam}::int
26151
+ ) x
26152
+ WHERE ${draftParam}::boolean OR (x.kind <> 'state-draft' AND (x.base_seq IS NULL OR x.seq >= x.base_seq)))`;
26153
+ }
26154
+ /**
26155
+ * Selects v1 snapshot rows and storage v2 runs as one result, newest first. Each version
26156
+ * has its own WHERE clause; `null` leaves it out. Storage v2 rows carry their header and
26157
+ * entries, or in summary mode only the summary snapshot. `page` is appended to the
26158
+ * combined, ordered rows (e.g. `LIMIT 1`).
26159
+ */
26160
+ async #runsQuery({ v1Where, v2Where, v1Snapshot = "snapshot", summary, page = "" }) {
26161
+ const branches = [];
26162
+ if (v1Where !== null) {
26163
+ const optional = {
26164
+ resourceId: "text",
26165
+ createdAtZ: "timestamptz",
26166
+ updatedAtZ: "timestamptz"
26167
+ };
26168
+ const columns = {};
26169
+ for (const [column, type] of Object.entries(optional)) columns[column] = await this.#db.getColumnType(TABLE_WORKFLOW_SNAPSHOT, column) !== null ? `"${column}"` : `NULL::${type} AS "${column}"`;
26170
+ branches.push(`SELECT workflow_name, run_id, ${columns.resourceId}, ${v1Snapshot} AS snapshot, NULL::jsonb AS header,
26171
+ "createdAt", ${columns.createdAtZ}, "updatedAt", ${columns.updatedAtZ}, 1 AS version
26172
+ FROM ${getTableName({
26173
+ indexName: TABLE_WORKFLOW_SNAPSHOT,
26174
+ schemaName: getSchemaName(this.#schema)
26175
+ })} ${v1Where}`);
26176
+ }
26177
+ if (v2Where !== null) {
26178
+ const columns = summary ? `jsonb_build_object('status', header -> 'status', 'timestamp', header -> 'timestamp') AS snapshot, NULL::jsonb AS header` : `NULL AS snapshot, header`;
26179
+ branches.push(`SELECT workflow_name, run_id, "resourceId", ${columns},
26180
+ "createdAt", "createdAtZ", "updatedAt", "updatedAtZ", 2 AS version
26181
+ FROM ${this.#runsTable()} ${v2Where}`);
26182
+ }
26183
+ return `SELECT p.*${summary ? "" : `, CASE WHEN p.version = 2 THEN ${this.#entriesSql("p")} END AS entries`}
26184
+ FROM (${branches.join(" UNION ALL ")} ORDER BY "createdAt" DESC ${page}) p
26185
+ ORDER BY p."createdAt" DESC`;
26186
+ }
26187
+ /**
26188
+ * The `base` that `materializeState` takes for a run's entries (as of `at`, or latest): the state
26189
+ * version of another run it builds on (`stateBaseOf`), resolved recursively, else undefined. `{}`
26190
+ * when that run isn't stored. One query per run in the chain.
26191
+ */
26192
+ async #stateBase(q, entries, at) {
26193
+ const ref = stateBaseOf(entries, at);
26194
+ if (!ref) return void 0;
26195
+ return (await this.#stateAt(q, ref.workflowName, ref.runId, ref))?.state ?? {};
26196
+ }
26197
+ /** A storage v2 run's state as of the version `at`, or null when the run isn't stored. */
26198
+ async #stateAt(q, workflowName, runId, at) {
26199
+ const row = await q.oneOrNone(`SELECT ${this.#stateEntriesSql("r", "$3", "$4")} AS entries
26200
+ FROM ${this.#runsTable()} r WHERE r.workflow_name = $1 AND r.run_id = $2`, [
26201
+ workflowName,
26202
+ runId,
26203
+ at.seq,
26204
+ at.draft === true
26205
+ ]);
26206
+ if (!row) return null;
26207
+ const entries = toEntries(row.entries);
26208
+ return materializeState(entries, {
26209
+ at,
26210
+ base: await this.#stateBase(q, entries, at)
26211
+ });
26212
+ }
26213
+ /**
26214
+ * Before a run is deleted, stores the full state in place of every `state-base` entry built on
26215
+ * it, so the runs built on it keep reading the same state. Runs inside the delete's transaction.
26216
+ */
26217
+ async #detachStateChildren(q, workflowName, runId) {
26218
+ const children = await q.manyOrNone(`SELECT workflow_name, run_id, seq, payload FROM ${this.#entriesTable()}
26219
+ WHERE kind = 'state-base' AND (${STATE_BASE_RUN_EXPR}) = ($1, $2)`, [workflowName, runId]);
26220
+ const states = /* @__PURE__ */ new Map();
26221
+ for (const child of children) {
26222
+ const version = `${child.payload.seq}:${child.payload.draft === true}`;
26223
+ let state = states.get(version);
26224
+ if (!state) {
26225
+ state = (await this.#stateAt(q, workflowName, runId, child.payload))?.state ?? {};
26226
+ states.set(version, state);
26227
+ }
26228
+ await q.none(`UPDATE ${this.#entriesTable()} SET kind = 'state', payload = $4::jsonb
26229
+ WHERE workflow_name = $1 AND run_id = $2 AND seq = $3`, [
26230
+ child.workflow_name,
26231
+ child.run_id,
26232
+ child.seq,
26233
+ toPgJson(state)
26234
+ ]);
26235
+ }
26236
+ }
26237
+ /** Rebuilds a storage v2 run's `WorkflowRunState` from its header and `#entriesSql` rows. */
26238
+ async #materializeRun(client, header, rows) {
26239
+ const entries = toEntries(rows);
26240
+ return materializeWorkflowRun(header, entries, await this.#stateBase(client, entries));
26241
+ }
26242
+ /** Parses a row of `#runsQuery`. */
26243
+ async #parseRun(client, row) {
26244
+ if (row.version !== 2) return this.parseWorkflowRun(row);
26245
+ const snapshot = row.entries ? await this.#materializeRun(client, row.header, row.entries) : row.snapshot;
26246
+ return {
26247
+ ...this.parseWorkflowRun({
26248
+ ...row,
26249
+ snapshot
26250
+ }),
26251
+ storageVersion: 2
26252
+ };
26253
+ }
26254
+ #runError(operation, workflowName, runId, error) {
26255
+ return new MastraError({
26256
+ id: createStorageErrorId("PG", operation, "FAILED"),
26257
+ domain: ErrorDomain.STORAGE,
26258
+ category: ErrorCategory.THIRD_PARTY,
26259
+ details: {
26260
+ workflowName,
26261
+ runId
26262
+ }
26263
+ }, error);
26264
+ }
26265
+ async loadWorkflowRunHeader({ workflowName, runId }) {
26266
+ try {
26267
+ return await this.#db.client.oneOrNone(`SELECT header, "lastSeq", "startStateSeq", "runToken" FROM ${this.#runsTable()} WHERE workflow_name = $1 AND run_id = $2`, [workflowName, runId]);
26268
+ } catch (error) {
26269
+ throw this.#runError("LOAD_WORKFLOW_RUN_HEADER", workflowName, runId, error);
26270
+ }
26271
+ }
26272
+ async loadWorkflowRun({ workflowName, runId, successfulSteps }) {
26273
+ try {
26274
+ const summary = successfulSteps === "summary";
26275
+ const run = await this.#db.client.oneOrNone(`SELECT r.header, r."lastSeq", r."startStateSeq", r."runToken", ${this.#entriesSql("r", { summaryFieldsParam: summary ? "$3" : void 0 })} AS entries
26276
+ FROM ${this.#runsTable()} r WHERE r.workflow_name = $1 AND r.run_id = $2`, summary ? [
26277
+ workflowName,
26278
+ runId,
26279
+ STEP_RESULT_SUMMARY_FIELDS
26280
+ ] : [workflowName, runId]);
26281
+ if (!run) return null;
26282
+ const entries = toEntries(run.entries);
26283
+ const summarizedInQuery = new Set(run.entries.filter((row) => row.summarized).map((row) => row.step_id));
26284
+ const steps = {};
26285
+ const summarizedStepIds = [];
26286
+ for (const [stepId, result] of latestStepResults(entries)) {
26287
+ steps[stepId] = result;
26288
+ if (summary && stepId !== "input" && result?.status === "success") {
26289
+ if (!summarizedInQuery.has(stepId)) steps[stepId] = summarizeStepResult(result);
26290
+ summarizedStepIds.push(stepId);
26291
+ }
26292
+ }
26293
+ const base = await this.#stateBase(this.#db.client, entries);
26294
+ const { state, statePatchBytes } = materializeState(entries, { base });
26295
+ return {
26296
+ header: run.header,
26297
+ lastSeq: run.lastSeq,
26298
+ startStateSeq: run.startStateSeq,
26299
+ runToken: run.runToken,
26300
+ state,
26301
+ statePatchBytes,
26302
+ steps,
26303
+ summarizedStepIds
26304
+ };
26305
+ } catch (error) {
26306
+ throw this.#runError("LOAD_WORKFLOW_RUN", workflowName, runId, error);
26307
+ }
26308
+ }
26309
+ async loadWorkflowRunSteps({ workflowName, runId, stepIds, expectedLastSeq, expectedRunToken }) {
26310
+ try {
26311
+ const run = await this.#db.client.oneOrNone(`SELECT r."lastSeq", r."runToken", (
26312
+ SELECT COALESCE(jsonb_agg(jsonb_build_object(
26313
+ 'seq', x.seq, 'kind', x.kind, 'step_id', x.step_id, 'absent', x.payload IS NULL, 'payload', x.payload
26314
+ ) ORDER BY x.seq), '[]'::jsonb)
26315
+ FROM (
26316
+ SELECT e.seq, e.kind, e.step_id, e.payload,
26317
+ MAX(CASE WHEN e.kind = 'step' THEN e.seq END) OVER (PARTITION BY e.step_id) AS step_seq
26318
+ FROM ${this.#entriesTable()} e
26319
+ WHERE e.workflow_name = r.workflow_name AND e.run_id = r.run_id
26320
+ AND e.kind IN ('step', 'step-patch') AND e.step_id = ANY($3::text[])
26321
+ ) x
26322
+ WHERE x.seq >= x.step_seq
26323
+ ) AS entries
26324
+ FROM ${this.#runsTable()} r WHERE r.workflow_name = $1 AND r.run_id = $2`, [
26325
+ workflowName,
26326
+ runId,
26327
+ stepIds
26328
+ ]);
26329
+ if (!run) return null;
26330
+ if (expectedLastSeq !== void 0 && run.lastSeq !== expectedLastSeq) return null;
26331
+ if (expectedRunToken !== void 0 && run.runToken !== expectedRunToken) return null;
26332
+ return Object.fromEntries(latestStepResults(toEntries(run.entries)));
26333
+ } catch (error) {
26334
+ throw this.#runError("LOAD_WORKFLOW_RUN_STEPS", workflowName, runId, error);
26335
+ }
26336
+ }
26337
+ async updateWorkflowRunHeader({ workflowName, runId, opts, fence: explicitFence }) {
26338
+ const fence = resolveRunFence(this, explicitFence, runId);
26339
+ try {
26340
+ return await this.#db.client.tx(async (t) => {
26341
+ if (fence) await assertRunFence(t, this.#runOwnersTable(), fence, "updateWorkflowRunHeader");
26342
+ const run = await t.oneOrNone(`SELECT header FROM ${this.#runsTable()} WHERE workflow_name = $1 AND run_id = $2 FOR UPDATE`, [workflowName, runId]);
26343
+ if (!run) return void 0;
26344
+ const { expectedStatus, ...patch } = opts;
26345
+ if (!matchesExpectedWorkflowStatus(run.header.status, expectedStatus)) return void 0;
26346
+ const header = {
26347
+ ...run.header,
26348
+ ...patch
26349
+ };
26350
+ const now = /* @__PURE__ */ new Date();
26351
+ return await t.one(`UPDATE ${this.#runsTable()}
26352
+ SET header = $3::jsonb, status = $4, "updatedAt" = $5, "updatedAtZ" = $6
26353
+ WHERE workflow_name = $1 AND run_id = $2
26354
+ RETURNING header, "lastSeq", "startStateSeq", "runToken"`, [
26355
+ workflowName,
26356
+ runId,
26357
+ toPgJson(header),
26358
+ header.status,
26359
+ now,
26360
+ now
26361
+ ]);
26362
+ });
26363
+ } catch (error) {
26364
+ if (isRunFenceConflictError(error)) throw error;
26365
+ throw this.#runError("UPDATE_WORKFLOW_RUN_HEADER", workflowName, runId, error);
26366
+ }
26367
+ }
26368
+ /**
26369
+ * Upserts a storage v2 run's header from `$1`–`$12` ({@link #headerParams}), adds `$7` (the
26370
+ * number of entries the write adds) to `lastSeq`, and returns the new `lastSeq`, `stateShared`
26371
+ * and `runToken`. `$12` is the run token of a run the write creates; a stored run keeps its own.
26372
+ * A checkpoint's state entries come last, so its `startStateSeq` is the new `lastSeq`; an append
26373
+ * keeps the stored one unless given `$13`. The header's row lock is held
26374
+ * until commit, so the writes to one run commit in `seq` order. With `expectedParam` (a text
26375
+ * array of statuses), a stored run whose status isn't in it is left as is and nothing is returned.
26376
+ */
26377
+ #upsertHeaderSql(checkpoint, expectedParam) {
26378
+ const startStateSeq = checkpoint ? {
26379
+ insert: "$7::int",
26380
+ update: "r.\"lastSeq\" + EXCLUDED.\"lastSeq\""
26381
+ } : {
26382
+ insert: "COALESCE($13::int, 0)",
26383
+ update: "COALESCE($13::int, r.\"startStateSeq\")"
26384
+ };
26385
+ return `INSERT INTO ${this.#runsTable()} AS r
26386
+ (workflow_name, run_id, "resourceId", "threadId", status, header, "lastSeq", "startStateSeq", "stateShared",
26387
+ "runToken", "createdAt", "updatedAt", "createdAtZ", "updatedAtZ")
26388
+ VALUES ($1, $2, $3, $4, $5, $6::jsonb, $7::int, ${startStateSeq.insert}, false, $12, $8, $9, $10, $11)
26389
+ ON CONFLICT (workflow_name, run_id) DO UPDATE
26390
+ SET "resourceId" = COALESCE(EXCLUDED."resourceId", r."resourceId"), "threadId" = EXCLUDED."threadId",
26391
+ status = EXCLUDED.status, header = EXCLUDED.header, "lastSeq" = r."lastSeq" + EXCLUDED."lastSeq",
26392
+ "startStateSeq" = ${startStateSeq.update},
26393
+ "updatedAt" = EXCLUDED."updatedAt", "updatedAtZ" = EXCLUDED."updatedAtZ"
26394
+ ${expectedParam ? `WHERE r.status = ANY(${expectedParam}::text[])` : ""}
26395
+ RETURNING "lastSeq", "stateShared", "runToken"`;
26396
+ }
26397
+ #headerParams(run, entryCount) {
26398
+ const now = /* @__PURE__ */ new Date();
26399
+ return [
26400
+ run.workflowName,
26401
+ run.runId,
26402
+ run.resourceId,
26403
+ run.threadId ?? null,
26404
+ run.header.status,
26405
+ toPgJson(run.header),
26406
+ entryCount,
26407
+ now,
26408
+ now,
26409
+ now,
26410
+ now,
26411
+ crypto.randomUUID()
26412
+ ];
26413
+ }
26414
+ async writeWorkflowRunCheckpoint({ fence: explicitFence, expectedStatus, steps, state, stateBase, statePatch, ...run }) {
26415
+ const entries = journalEntries(steps, {
26416
+ state,
26417
+ stateBase,
26418
+ statePatch
26419
+ }, true);
26420
+ const fence = resolveRunFence(this, explicitFence, run.runId);
26421
+ const expected = expectedStatus === void 0 ? [] : [[expectedStatus].flat()];
26422
+ try {
26423
+ return await this.#db.client.tx(async (t) => {
26424
+ if (fence) await assertRunFence(t, this.#runOwnersTable(), fence, "writeWorkflowRunCheckpoint");
26425
+ const base = state === void 0 ? stateBase : void 0;
26426
+ let baseLastSeq;
26427
+ if (base) baseLastSeq = (await t.oneOrNone(`SELECT "lastSeq" FROM ${this.#runsTable()} WHERE workflow_name = $1 AND run_id = $2 FOR KEY SHARE`, [base.workflowName, base.runId]))?.lastSeq;
26428
+ const header = await t.oneOrNone(this.#upsertHeaderSql(true, expected.length ? "$13" : void 0), [...this.#headerParams(run, entries.length), ...expected]);
26429
+ if (!header) return null;
26430
+ if (base) {
26431
+ await t.none(`UPDATE ${this.#runsTable()} SET "stateShared" = true
26432
+ WHERE workflow_name = $1 AND run_id = $2 AND NOT "stateShared"`, [base.workflowName, base.runId]);
26433
+ const { stored } = await t.one(`SELECT EXISTS (
26434
+ SELECT 1 FROM ${this.#entriesTable()} WHERE workflow_name = $1 AND run_id = $2
26435
+ AND CASE WHEN $4::boolean THEN kind = 'state-draft' AND seq = $3::int
26436
+ ELSE kind IN ('state', 'state-base') AND seq <= $3::int END
26437
+ ) AS stored`, [
26438
+ base.workflowName,
26439
+ base.runId,
26440
+ base.seq,
26441
+ base.draft === true
26442
+ ]);
26443
+ if (!stored || baseLastSeq === void 0 || base.seq > baseLastSeq) throw new Error(`Workflow run ${run.workflowName}/${run.runId} builds on state version ${base.seq} of ${base.workflowName}/${base.runId}, which is not stored`);
26444
+ }
26445
+ const { lastSeq, stateShared, runToken } = header;
26446
+ await t.none(`DELETE FROM ${this.#entriesTable()}
26447
+ WHERE workflow_name = $1 AND run_id = $2${stateShared ? ` AND kind IN ('step', 'step-patch')` : ""}`, [run.workflowName, run.runId]);
26448
+ await t.none(`INSERT INTO ${this.#entriesTable()} (workflow_name, run_id, seq, kind, step_id, payload)
26449
+ SELECT $1, $2, $3::int - $4::int + e.ord, ${ENTRY_VALUES}
26450
+ FROM ${entryRows("$5")}`, [
26451
+ run.workflowName,
26452
+ run.runId,
26453
+ lastSeq,
26454
+ entries.length,
26455
+ toPgJson(entries)
26456
+ ]);
26457
+ return {
26458
+ lastSeq,
26459
+ runToken
26460
+ };
26461
+ });
26462
+ } catch (error) {
26463
+ if (isRunFenceConflictError(error)) throw error;
26464
+ throw this.#runError("WRITE_WORKFLOW_RUN_CHECKPOINT", run.workflowName, run.runId, error);
26465
+ }
26466
+ }
26467
+ /** One statement: the header upsert numbers the entries inserted after it. */
26468
+ async appendWorkflowRunEntries({ fence, steps, stepPatches, state, statePatch, startStateSeq, expectedStatus, ...run }) {
26469
+ const entries = journalEntries(steps, {
26470
+ state,
26471
+ statePatch
26472
+ }, false, stepPatches);
26473
+ const expected = expectedStatus === void 0 ? [] : [[expectedStatus].flat()];
26474
+ try {
26475
+ return await withRunFence(this.#db.client, this.#runOwnersTable(), resolveRunFence(this, fence, run.runId), "appendWorkflowRunEntries", (q) => q.oneOrNone(`WITH h AS (${this.#upsertHeaderSql(false, expected.length ? "$15" : void 0)}),
26476
+ e AS (
26477
+ INSERT INTO ${this.#entriesTable()} (workflow_name, run_id, seq, kind, step_id, payload)
26478
+ SELECT $1, $2, h."lastSeq" - $7::int + e.ord, ${ENTRY_VALUES}
26479
+ FROM h, ${entryRows("$14")}
26480
+ )
26481
+ SELECT "lastSeq", "runToken" FROM h`, [
26482
+ ...this.#headerParams(run, entries.length),
26483
+ startStateSeq ?? null,
26484
+ toPgJson(entries),
26485
+ ...expected
26486
+ ]));
26487
+ } catch (error) {
26488
+ if (isRunFenceConflictError(error)) throw error;
26489
+ throw this.#runError("APPEND_WORKFLOW_RUN_ENTRIES", run.workflowName, run.runId, error);
26490
+ }
26491
+ }
26492
+ async loadWorkflowRunState({ workflowName, runId, seq }) {
26493
+ try {
26494
+ return (await this.#stateAt(this.#db.client, workflowName, runId, { seq }))?.state ?? null;
26495
+ } catch (error) {
26496
+ throw this.#runError("LOAD_WORKFLOW_RUN_STATE", workflowName, runId, error);
26497
+ }
26498
+ }
26499
+ async appendWorkflowRunStateDraft(args) {
26500
+ const { workflowName, runId } = args;
26501
+ const payload = "state" in args ? { state: args.state } : {
26502
+ on: args.on.draft ? {
26503
+ seq: args.on.seq,
26504
+ draft: true
26505
+ } : { seq: args.on.seq },
26506
+ ops: args.ops
26507
+ };
26508
+ let row;
26509
+ try {
26510
+ row = await withRunFence(this.#db.client, this.#runOwnersTable(), resolveRunFence(this, void 0, runId), "appendWorkflowRunStateDraft", (q) => q.oneOrNone(`WITH h AS (
26511
+ UPDATE ${this.#runsTable()} SET "lastSeq" = "lastSeq" + 1, "stateShared" = true
26512
+ WHERE workflow_name = $1 AND run_id = $2
26513
+ RETURNING "lastSeq"
26514
+ )
26515
+ INSERT INTO ${this.#entriesTable()} (workflow_name, run_id, seq, kind, step_id, payload)
26516
+ SELECT $1, $2, h."lastSeq", 'state-draft', NULL, $3::jsonb FROM h
26517
+ RETURNING seq`, [
26518
+ workflowName,
26519
+ runId,
26520
+ toPgJson(payload)
26521
+ ]));
26522
+ } catch (error) {
26523
+ if (isRunFenceConflictError(error)) throw error;
26524
+ throw this.#runError("APPEND_WORKFLOW_RUN_STATE_DRAFT", workflowName, runId, error);
26525
+ }
26526
+ if (!row) throw new Error(`Storage v2 run ${workflowName}/${runId} is not stored`);
26527
+ return row;
25850
26528
  }
25851
26529
  supportsRunFencing() {
25852
26530
  return true;
@@ -25975,6 +26653,7 @@ var WorkflowsPG = class WorkflowsPG extends WorkflowsStorage {
25975
26653
  });
25976
26654
  const existingSnapshotResult = await t.oneOrNone(`SELECT snapshot FROM ${tableName} WHERE workflow_name = $1 AND run_id = $2 FOR UPDATE`, [workflowName, runId]);
25977
26655
  if (!existingSnapshotResult) {
26656
+ if (await t.oneOrNone(`SELECT 1 FROM ${this.#runsTable()} WHERE workflow_name = $1 AND run_id = $2`, [workflowName, runId])) throw new Error(`updateWorkflowResults does not support storage v2 runs (run ${runId})`);
25978
26657
  assertWorkflowStepGuard({
25979
26658
  snapshot: null,
25980
26659
  stepId,
@@ -26047,11 +26726,36 @@ var WorkflowsPG = class WorkflowsPG extends WorkflowsStorage {
26047
26726
  }, error);
26048
26727
  }
26049
26728
  }
26050
- async updateWorkflowState({ workflowName, runId, opts, fence: explicitFence }) {
26729
+ async updateWorkflowState({ workflowName, runId, opts, fence: explicitFence, storageVersion }) {
26051
26730
  const fence = resolveRunFence(this, explicitFence, runId);
26052
26731
  try {
26053
26732
  return await this.#db.client.tx(async (t) => {
26054
26733
  if (fence) await assertRunFence(t, this.#runOwnersTable(), fence, "updateWorkflowState");
26734
+ if (storageVersion !== 1) {
26735
+ const run = await t.oneOrNone(`SELECT header FROM ${this.#runsTable()} WHERE workflow_name = $1 AND run_id = $2 FOR UPDATE`, [workflowName, runId]);
26736
+ if (run) {
26737
+ const { expectedStatus, ...patch } = opts;
26738
+ if (!matchesExpectedWorkflowStatus(run.header.status, expectedStatus)) return;
26739
+ const header = {
26740
+ ...run.header,
26741
+ ...patch
26742
+ };
26743
+ const now = /* @__PURE__ */ new Date();
26744
+ const { entries } = await t.one(`UPDATE ${this.#runsTable()} r
26745
+ SET header = $3::jsonb, status = $4, "updatedAt" = $5, "updatedAtZ" = $6
26746
+ WHERE r.workflow_name = $1 AND r.run_id = $2
26747
+ RETURNING ${this.#entriesSql("r")} AS entries`, [
26748
+ workflowName,
26749
+ runId,
26750
+ toPgJson(header),
26751
+ header.status,
26752
+ now,
26753
+ now
26754
+ ]);
26755
+ return this.#materializeRun(t, header, entries);
26756
+ }
26757
+ if (storageVersion === 2) return void 0;
26758
+ }
26055
26759
  const tableName = getTableName({
26056
26760
  indexName: TABLE_WORKFLOW_SNAPSHOT,
26057
26761
  schemaName: getSchemaName(this.#schema)
@@ -26131,10 +26835,14 @@ var WorkflowsPG = class WorkflowsPG extends WorkflowsStorage {
26131
26835
  async loadWorkflowRunStatus({ workflowName, runId }) {
26132
26836
  try {
26133
26837
  const statusExpr = await this.#statusExpression("snapshot");
26134
- return (await this.#db.client.oneOrNone(`SELECT ${statusExpr} AS status FROM ${getTableName({
26838
+ return (await this.#db.client.oneOrNone(`SELECT COALESCE(
26839
+ (SELECT status FROM ${this.#runsTable()} WHERE workflow_name = $1 AND run_id = $2),
26840
+ (SELECT ${statusExpr} FROM ${getTableName({
26135
26841
  indexName: TABLE_WORKFLOW_SNAPSHOT,
26136
26842
  schemaName: getSchemaName(this.#schema)
26137
- })} WHERE "workflow_name" = $1 AND "run_id" = $2 ORDER BY "createdAt" DESC LIMIT 1`, [workflowName, runId]))?.status ?? null;
26843
+ })}
26844
+ WHERE "workflow_name" = $1 AND "run_id" = $2 ORDER BY "createdAt" DESC LIMIT 1)
26845
+ ) AS status`, [workflowName, runId]))?.status ?? null;
26138
26846
  } catch (error) {
26139
26847
  throw new MastraError({
26140
26848
  id: createStorageErrorId("PG", "LOAD_WORKFLOW_RUN_STATUS", "FAILED"),
@@ -26147,8 +26855,14 @@ var WorkflowsPG = class WorkflowsPG extends WorkflowsStorage {
26147
26855
  }, error);
26148
26856
  }
26149
26857
  }
26150
- async loadWorkflowSnapshot({ workflowName, runId }) {
26858
+ async loadWorkflowSnapshot({ workflowName, runId, storageVersion }) {
26151
26859
  try {
26860
+ if (storageVersion !== 1) {
26861
+ const run = await this.#db.client.oneOrNone(`SELECT r.header, ${this.#entriesSql("r")} AS entries
26862
+ FROM ${this.#runsTable()} r WHERE r.workflow_name = $1 AND r.run_id = $2`, [workflowName, runId]);
26863
+ if (run) return await this.#materializeRun(this.#db.client, run.header, run.entries);
26864
+ if (storageVersion === 2) return null;
26865
+ }
26152
26866
  const result = await this.#db.load({
26153
26867
  tableName: TABLE_WORKFLOW_SNAPSHOT,
26154
26868
  keys: {
@@ -26165,7 +26879,7 @@ var WorkflowsPG = class WorkflowsPG extends WorkflowsStorage {
26165
26879
  }, error);
26166
26880
  }
26167
26881
  }
26168
- async getWorkflowRunById({ runId, workflowName }) {
26882
+ async getWorkflowRunById({ runId, workflowName, storageVersion }) {
26169
26883
  try {
26170
26884
  const conditions = [];
26171
26885
  const values = [];
@@ -26181,18 +26895,14 @@ var WorkflowsPG = class WorkflowsPG extends WorkflowsStorage {
26181
26895
  paramIndex++;
26182
26896
  }
26183
26897
  const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
26184
- const query = `
26185
- SELECT * FROM ${getTableName({
26186
- indexName: TABLE_WORKFLOW_SNAPSHOT,
26187
- schemaName: getSchemaName(this.#schema)
26188
- })}
26189
- ${whereClause}
26190
- ORDER BY "createdAt" DESC LIMIT 1
26191
- `;
26192
- const queryValues = values;
26193
- const result = await this.#db.readClient.oneOrNone(query, queryValues);
26898
+ const query = await this.#runsQuery({
26899
+ v1Where: storageVersion === 2 ? null : whereClause,
26900
+ v2Where: storageVersion === 1 ? null : whereClause,
26901
+ page: "LIMIT 1"
26902
+ });
26903
+ const result = await this.#db.readClient.oneOrNone(query, values);
26194
26904
  if (!result) return null;
26195
- return this.parseWorkflowRun(result);
26905
+ return await this.#parseRun(this.#db.readClient, result);
26196
26906
  } catch (error) {
26197
26907
  throw new MastraError({
26198
26908
  id: createStorageErrorId("PG", "GET_WORKFLOW_RUN_BY_ID", "FAILED"),
@@ -26205,12 +26915,28 @@ var WorkflowsPG = class WorkflowsPG extends WorkflowsStorage {
26205
26915
  }, error);
26206
26916
  }
26207
26917
  }
26208
- async deleteWorkflowRunById({ runId, workflowName, fence }) {
26918
+ async deleteWorkflowRunById({ runId, workflowName, storageVersion, fence }) {
26919
+ const resolvedFence = resolveRunFence(this, fence, runId);
26209
26920
  try {
26210
- await withRunFence(this.#db.client, this.#runOwnersTable(), resolveRunFence(this, fence, runId), "deleteWorkflowRunById", (q) => q.none(`DELETE FROM ${getTableName({
26211
- indexName: TABLE_WORKFLOW_SNAPSHOT,
26212
- schemaName: getSchemaName(this.#schema)
26213
- })} WHERE run_id = $1 AND workflow_name = $2`, [runId, workflowName]));
26921
+ if (storageVersion === 1) {
26922
+ await withRunFence(this.#db.client, this.#runOwnersTable(), resolvedFence, "deleteWorkflowRunById", (q) => q.none(`DELETE FROM ${getTableName({
26923
+ indexName: TABLE_WORKFLOW_SNAPSHOT,
26924
+ schemaName: getSchemaName(this.#schema)
26925
+ })} WHERE run_id = $1 AND workflow_name = $2`, [runId, workflowName]));
26926
+ return;
26927
+ }
26928
+ await this.#db.client.tx(async (t) => {
26929
+ if (resolvedFence) await assertRunFence(t, this.#runOwnersTable(), resolvedFence, "deleteWorkflowRunById");
26930
+ if ((await t.oneOrNone(`SELECT "stateShared" FROM ${this.#runsTable()} WHERE workflow_name = $1 AND run_id = $2 FOR UPDATE`, [workflowName, runId]))?.stateShared) await this.#detachStateChildren(t, workflowName, runId);
26931
+ for (const table of [
26932
+ this.#runsTable(),
26933
+ this.#entriesTable(),
26934
+ ...storageVersion === 2 ? [] : [getTableName({
26935
+ indexName: TABLE_WORKFLOW_SNAPSHOT,
26936
+ schemaName: getSchemaName(this.#schema)
26937
+ })]
26938
+ ]) await t.none(`DELETE FROM ${table} WHERE run_id = $1 AND workflow_name = $2`, [runId, workflowName]);
26939
+ });
26214
26940
  } catch (error) {
26215
26941
  if (isRunFenceConflictError(error)) throw error;
26216
26942
  throw new MastraError({
@@ -26227,77 +26953,84 @@ var WorkflowsPG = class WorkflowsPG extends WorkflowsStorage {
26227
26953
  async listWorkflowRuns({ workflowName, fromDate, toDate, perPage, page, resourceId, threadId, status, summary } = {}) {
26228
26954
  try {
26229
26955
  const conditions = [];
26956
+ const v2Conditions = [];
26230
26957
  const values = [];
26231
26958
  let paramIndex = 1;
26232
26959
  if (workflowName) {
26233
26960
  conditions.push(`workflow_name = $${paramIndex}`);
26961
+ v2Conditions.push(`workflow_name = $${paramIndex}`);
26234
26962
  values.push(workflowName);
26235
26963
  paramIndex++;
26236
26964
  }
26237
26965
  if (status) {
26238
26966
  const statusExpr = await this.#db.getColumnType(TABLE_WORKFLOW_SNAPSHOT, "snapshot") === "jsonb" ? `snapshot ->> 'status'` : `regexp_replace(snapshot::text, '\\\\u(0000|[Dd][89A-Fa-f][0-9A-Fa-f]{2})', '', 'g')::jsonb ->> 'status'`;
26239
26967
  conditions.push(`${statusExpr} = $${paramIndex}`);
26968
+ v2Conditions.push(`status = $${paramIndex}`);
26240
26969
  values.push(status);
26241
26970
  paramIndex++;
26242
26971
  }
26243
26972
  if (resourceId) {
26244
- if (await this.#db.hasColumn(TABLE_WORKFLOW_SNAPSHOT, "resourceId")) {
26245
- conditions.push(`"resourceId" = $${paramIndex}`);
26246
- values.push(resourceId);
26247
- paramIndex++;
26248
- } else this.logger?.warn?.(`[${TABLE_WORKFLOW_SNAPSHOT}] resourceId column not found. Skipping resourceId filter.`);
26973
+ if (await this.#db.hasColumn(TABLE_WORKFLOW_SNAPSHOT, "resourceId")) conditions.push(`"resourceId" = $${paramIndex}`);
26974
+ else this.logger?.warn?.(`[${TABLE_WORKFLOW_SNAPSHOT}] resourceId column not found. Skipping resourceId filter.`);
26975
+ v2Conditions.push(`"resourceId" = $${paramIndex}`);
26976
+ values.push(resourceId);
26977
+ paramIndex++;
26249
26978
  }
26250
26979
  if (threadId) {
26251
- if (await this.#db.getColumnType(TABLE_WORKFLOW_SNAPSHOT, "snapshot") === "jsonb") {
26252
- conditions.push(`${WORKFLOW_SNAPSHOT_THREAD_ID_EXPR} = $${paramIndex}`);
26253
- values.push(threadId);
26254
- paramIndex++;
26255
- } else this.logger?.warn?.(`[${TABLE_WORKFLOW_SNAPSHOT}] snapshot column is not jsonb. Skipping threadId filter.`);
26980
+ if (await this.#db.getColumnType(TABLE_WORKFLOW_SNAPSHOT, "snapshot") === "jsonb") conditions.push(`${WORKFLOW_SNAPSHOT_THREAD_ID_EXPR} = $${paramIndex}`);
26981
+ else this.logger?.warn?.(`[${TABLE_WORKFLOW_SNAPSHOT}] snapshot column is not jsonb. Skipping threadId filter.`);
26982
+ v2Conditions.push(`"threadId" = $${paramIndex}`);
26983
+ values.push(threadId);
26984
+ paramIndex++;
26256
26985
  }
26257
26986
  if (fromDate) {
26258
26987
  conditions.push(`"createdAt" >= $${paramIndex}`);
26988
+ v2Conditions.push(`"createdAt" >= $${paramIndex}`);
26259
26989
  values.push(fromDate);
26260
26990
  paramIndex++;
26261
26991
  }
26262
26992
  if (toDate) {
26263
26993
  conditions.push(`"createdAt" <= $${paramIndex}`);
26994
+ v2Conditions.push(`"createdAt" <= $${paramIndex}`);
26264
26995
  values.push(toDate);
26265
26996
  paramIndex++;
26266
26997
  }
26267
26998
  const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
26268
- let total = 0;
26999
+ const v2WhereClause = v2Conditions.length > 0 ? `WHERE ${v2Conditions.join(" AND ")}` : "";
26269
27000
  const usePagination = typeof perPage === "number" && typeof page === "number";
26270
- if (usePagination) {
26271
- const countResult = await this.#db.client.one(`SELECT COUNT(*) as count FROM ${getTableName({
26272
- indexName: TABLE_WORKFLOW_SNAPSHOT,
26273
- schemaName: getSchemaName(this.#schema)
26274
- })} ${whereClause}`, values);
26275
- total = Number(countResult.count);
26276
- }
26277
27001
  const normalizedPerPage = usePagination ? normalizePerPage(perPage, Number.MAX_SAFE_INTEGER) : 0;
26278
- const offset = usePagination ? page * normalizedPerPage : void 0;
26279
- let selectList = "*";
27002
+ const offset = usePagination ? page * normalizedPerPage : 0;
27003
+ let v1Snapshot = "snapshot";
26280
27004
  if (summary) {
26281
27005
  const snapshotJson = await this.#db.getColumnType(TABLE_WORKFLOW_SNAPSHOT, "snapshot") === "jsonb" ? "snapshot" : `regexp_replace(snapshot::text, '\\\\u(0000|[Dd][89A-Fa-f][0-9A-Fa-f]{2})', '', 'g')::jsonb`;
26282
- selectList = `workflow_name, run_id, "resourceId", "createdAt", "createdAtZ", "updatedAt", "updatedAtZ", jsonb_build_object('status', ${snapshotJson} -> 'status', 'timestamp', ${snapshotJson} -> 'timestamp') AS snapshot`;
27006
+ v1Snapshot = `jsonb_build_object('status', ${snapshotJson} -> 'status', 'timestamp', ${snapshotJson} -> 'timestamp')`;
26283
27007
  }
26284
- const query = `
26285
- SELECT ${selectList} FROM ${getTableName({
26286
- indexName: TABLE_WORKFLOW_SNAPSHOT,
26287
- schemaName: getSchemaName(this.#schema)
26288
- })}
26289
- ${whereClause}
26290
- ORDER BY "createdAt" DESC
26291
- ${usePagination ? ` LIMIT $${paramIndex} OFFSET $${paramIndex + 1}` : ""}
26292
- `;
27008
+ const query = await this.#runsQuery({
27009
+ v1Where: whereClause,
27010
+ v2Where: v2WhereClause,
27011
+ v1Snapshot,
27012
+ summary,
27013
+ page: usePagination ? `LIMIT $${paramIndex} OFFSET $${paramIndex + 1}` : ""
27014
+ });
26293
27015
  const queryValues = usePagination ? [
26294
27016
  ...values,
26295
27017
  normalizedPerPage,
26296
27018
  offset
26297
27019
  ] : values;
26298
- const runs = (await this.#db.client.manyOrNone(query, queryValues) || []).map((row) => {
26299
- return this.parseWorkflowRun(row);
26300
- });
27020
+ const result = await this.#db.client.manyOrNone(query, queryValues);
27021
+ const runs = await Promise.all((result || []).map((row) => this.#parseRun(this.#db.client, row)));
27022
+ let total = 0;
27023
+ if (usePagination) {
27024
+ total = offset + runs.length;
27025
+ if (runs.length === normalizedPerPage || runs.length === 0 && offset > 0) {
27026
+ const countResult = await this.#db.client.one(`SELECT (SELECT COUNT(*) FROM ${getTableName({
27027
+ indexName: TABLE_WORKFLOW_SNAPSHOT,
27028
+ schemaName: getSchemaName(this.#schema)
27029
+ })} ${whereClause})
27030
+ + (SELECT COUNT(*) FROM ${this.#runsTable()} ${v2WhereClause}) AS count`, values);
27031
+ total = Number(countResult.count);
27032
+ }
27033
+ }
26301
27034
  return {
26302
27035
  runs,
26303
27036
  total: total || runs.length