Skip to content

Commit 5162005

Browse files
committed
fix(workflow-persistence): make persistMigratedBlocks' updated_at guard millisecond-tolerant
1 parent 2798467 commit 5162005

1 file changed

Lines changed: 23 additions & 2 deletions

File tree

  • packages/workflow-persistence/src

packages/workflow-persistence/src/load.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { db, workflow, workflowBlocks, workflowEdges, workflowSubflows } from '@
22
import { createLogger } from '@sim/logger'
33
import type { BlockState, Loop, Parallel } from '@sim/workflow-types/workflow'
44
import { SUBFLOW_TYPES } from '@sim/workflow-types/workflow'
5-
import { and, eq, isNull } from 'drizzle-orm'
5+
import { and, eq, gte, isNull, lt } from 'drizzle-orm'
66
import type { Edge } from 'reactflow'
77
import { clampParallelBatchSize } from './subflow-helpers'
88
import type { DbOrTx, NormalizedWorkflowData } from './types'
@@ -180,6 +180,27 @@ export async function loadWorkflowFromNormalizedTablesRaw(
180180
}
181181
}
182182

183+
/**
184+
* Optimistic-concurrency guard matching a row's `updated_at` against the value
185+
* read earlier through Drizzle.
186+
*
187+
* Postgres stores `timestamp` columns with microsecond precision, but Drizzle's
188+
* default `date` mode surfaces them as JS `Date`s, which truncate to
189+
* milliseconds. A strict equality guard therefore never matches rows whose
190+
* `updated_at` was stamped by SQL `now()`/`defaultNow()` (non-zero
191+
* sub-millisecond digits are lost on read), silently turning the UPDATE into a
192+
* permanent no-op — migrations then re-run on every load without ever
193+
* persisting. Matching the enclosing millisecond restores the intended
194+
* semantics: any concurrent writer stamps a fresh `updated_at`, which falls
195+
* outside this window.
196+
*/
197+
function updatedAtMatches(expected: Date) {
198+
return and(
199+
gte(workflowBlocks.updatedAt, expected),
200+
lt(workflowBlocks.updatedAt, new Date(expected.getTime() + 1))
201+
)
202+
}
203+
183204
export async function persistMigratedBlocks(
184205
workflowId: string,
185206
originalBlocks: Record<string, BlockState>,
@@ -197,7 +218,7 @@ export async function persistMigratedBlocks(
197218
eq(workflowBlocks.workflowId, workflowId),
198219
expectedUpdatedAt === null
199220
? isNull(workflowBlocks.updatedAt)
200-
: eq(workflowBlocks.updatedAt, expectedUpdatedAt)
221+
: updatedAtMatches(expectedUpdatedAt)
201222
)
202223
: and(eq(workflowBlocks.id, blockId), eq(workflowBlocks.workflowId, workflowId))
203224

0 commit comments

Comments
 (0)