| 1 | -- Fix round 1 on Task 5 (fuzzing/race review): two concurrent POSTs to |
| 2 | -- the same document could both read the same MAX(seq) before either |
| 3 | -- had committed its INSERT, and both land a row at the same (doc_id, |
| 4 | -- seq). Harmless to the document itself -- the CRDT converges |
| 5 | -- regardless of the order updates replay in, and each request runs |
| 6 | -- its own core.Handle -- but a later compaction or snapshot plan that |
| 7 | -- assumes seq is unique per document would be quietly wrong about |
| 8 | -- rows already on disk. store.go's ApplyUpdate now retries on this |
| 9 | -- constraint rather than trusting its own MAX(seq) read to still be |
| 10 | -- current by the time it writes (its own doc comment). |
| 11 | -- |
| 12 | -- Like 0002_owners.sql's owner_id backfill, this is written for a |
| 13 | -- database that may already have rows, not just a fresh one -- and |
| 14 | -- CREATE UNIQUE INDEX simply refuses to apply if a duplicate (doc_id, |
| 15 | -- seq) pair already exists, rather than silently dropping one of the |
| 16 | -- two rows to make room for it. That refusal is the correct behaviour |
| 17 | -- here, not a gap to paper over with a DELETE: both racing rows are a |
| 18 | -- real, already-persisted update apiece, and picking one to discard |
| 19 | -- during a migration would be exactly the kind of silent data loss |
| 20 | -- this whole plan's GET /d/{sqid}/updates cap (handlers.go's |
| 21 | -- maxLogBytes) refuses to commit elsewhere. In practice this is safe |
| 22 | -- to apply as-is: this plan is what puts POST /d/{sqid}/updates |
| 23 | -- (and therefore this race) in front of any real traffic for the |
| 24 | -- first time, and this migration ships in the same change as the |
| 25 | -- fix -- there is no window in which the vulnerable code could have |
| 26 | -- run against a database this migration is later applied to. |
| 27 | CREATE UNIQUE INDEX IF NOT EXISTS idx_doc_updates_doc_id_seq ON doc_updates (doc_id, seq); |
| 28 | |