@@ -109,6 +109,24 @@ WHERE NOT EXISTS (
109109ON CONFLICT (advisory_package_id, COALESCE(introduced_version, ''), COALESCE(fixed_version, ''), COALESCE(last_affected, '')) DO NOTHING
110110`
111111
112+ // Runs as prepareSql (uncounted) ahead of ADVISORY_AFFECTED_RANGES_MERGE_SQL, in the
113+ // same transaction. Row-locks every advisory_packages this chunk will touch, in id
114+ // order, before the NOT EXISTS ownership check runs. OSV's upsertOne (services/apps/
115+ // packages_worker/src/osv/upsertAdvisory.ts) takes the matching per-row lock before it
116+ // writes advisory_affected_ranges for that advisory_package, so whichever transaction
117+ // (deps.dev chunk or OSV record) locks the row first now forces the other to wait and
118+ // see its committed writes — closing the race the two independently-scheduled write
119+ // paths would otherwise have on the ownership check (ADR-0001 §Write semantics).
120+ const ADVISORY_PACKAGES_LOCK_SQL = `
121+ SELECT ap.id
122+ FROM advisory_packages ap
123+ JOIN staging.osspckgs_advisory_packages_raw s
124+ ON s.ecosystem = ap.ecosystem AND s.package_name = ap.package_name
125+ JOIN advisories adv ON adv.osv_id = s.osv_id AND adv.id = ap.advisory_id
126+ ORDER BY ap.id
127+ FOR UPDATE OF ap
128+ `
129+
112130const ADVISORIES_PG_COLUMNS = [
113131 'osv_id' ,
114132 'source' ,
@@ -274,6 +292,7 @@ export async function ingestAdvisories(opts: {
274292
275293 const { rowsAffected, tableRowCounts } = await mergeStagingToTable ( {
276294 jobId : pkgsExport . jobId ,
295+ prepareSql : ADVISORY_PACKAGES_LOCK_SQL ,
277296 mergeSql : [ ADVISORY_PACKAGES_MERGE_SQL , ADVISORY_AFFECTED_RANGES_MERGE_SQL ] ,
278297 tableNames : [ 'advisory_packages' , 'advisory_affected_ranges' ] ,
279298 isFinal,
0 commit comments