From 39f36b4ac5e83685c07bc525aeddde7c38fc19e1 Mon Sep 17 00:00:00 2001 From: Amruth Pillai Date: Thu, 20 Aug 2026 09:25:44 +0200 Subject: [PATCH] fix(resume): guard patch versions in the transaction, not in sql Postgres defaultNow() stores microseconds while JS Dates are millisecond-truncated, so the SQL equality guard matched zero rows on freshly created resumes and every guarded agent patch failed with a permanent version conflict. The SELECT ... FOR UPDATE lock plus the in-transaction ms-precision check already provide the guarantee; drop the SQL predicate. Verified A/B against a live database. --- packages/api/src/features/resume/service.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/api/src/features/resume/service.ts b/packages/api/src/features/resume/service.ts index ed0344a8e..cc24a7927 100644 --- a/packages/api/src/features/resume/service.ts +++ b/packages/api/src/features/resume/service.ts @@ -160,16 +160,15 @@ async function applyResumePatchTx( } patchedData = parseWritableResumeData(patchedData); + // The version guard is the ms-precision JS check above, under the SELECT ... FOR UPDATE lock. + // Never compare expectedUpdatedAt in SQL: rows stamped by Postgres now() (defaultNow() on + // insert) carry microseconds, while JS Dates are ms-truncated — SQL equality then matches + // zero rows and every guarded patch on a fresh resume reports a version conflict forever. const [resume] = await client .update(schema.resume) .set({ data: patchedData }) .where( - and( - eq(schema.resume.id, input.id), - eq(schema.resume.isLocked, false), - eq(schema.resume.userId, input.userId), - ...(input.expectedUpdatedAt ? [eq(schema.resume.updatedAt, input.expectedUpdatedAt)] : []), - ), + and(eq(schema.resume.id, input.id), eq(schema.resume.isLocked, false), eq(schema.resume.userId, input.userId)), ) .returning({ id: schema.resume.id,