-- Narrow the client-upload idempotency index so it stops covering soft-deleted rows. -- -- The bug (H9). Migration 022 created the index partial on `client_upload_id IS NOT NULL` -- only, so a soft-deleted row kept occupying its key. But `find_by_client_upload_id` filters -- `deleted_at IS NULL` — deliberately, and its doc comment says so: "if the guest deleted the -- photo and their queue later retries, they should get a fresh upload rather than a -- resurrection of a deleted one." The index and the lookup therefore disagreed, and the -- disagreement is reachable by an ordinary guest: -- -- 1. guest uploads a photo, then deletes it (soft delete — the row stays, `deleted_at` set) -- 2. their queue retries the same item (reconnect requeue, or they tap "Erneut") -- 3. the whole body is re-streamed and re-validated, then `ON CONFLICT DO NOTHING` matches -- the DEAD row and inserts nothing -- 4. the replay lookup filters that row out and finds nothing, so the handler returns 409 -- 5. the client classifies 409 as terminal and DELETES the blob from IndexedDB -- -- The photo is now gone from the device with no row in the gallery, and there is no path back. -- Re-selecting the same file from the camera roll mints a new `client_upload_id`, so that does -- work — but the guest has no way to know that is what is required. -- -- Adding `deleted_at IS NULL` makes the index agree with the lookup: a key is claimed only -- while a LIVE row holds it, so step 3 inserts a fresh row and the retry succeeds. -- -- Uniqueness among live rows is what the feature actually needs. The property migration 022 -- was protecting — "the same photo must not land in the gallery twice" — is about rows the -- guest can see, and a soft-deleted row is not one of those. DROP INDEX IF EXISTS upload_client_upload_id_key; -- CONCURRENTLY is deliberately NOT used: sqlx runs each migration inside a transaction, and -- CREATE INDEX CONCURRENTLY cannot run in one. The table is small (one event's uploads) and -- this runs at boot before the server accepts requests, so the brief lock costs nothing. CREATE UNIQUE INDEX upload_client_upload_id_key ON upload (client_upload_id) WHERE client_upload_id IS NOT NULL AND deleted_at IS NULL;