-- +goose Up -- `Idempotency-Key` (canon §IdempotencyKey): what makes a retry of a write safe. -- -- It exists because the contract itself advises retrying a 408 on the book upload — while RFC 9110 -- §9.2.2 says a client SHOULD NOT retry a non-idempotent method "unless it has some means to know -- that the request semantics are actually idempotent … or some means to detect that the original -- request was never applied". Every POST /books made a new book, and until 0.3.0 there was no way -- to remove the duplicate at all. create table idempotency_keys ( -- The scope is (principal, method, path), and it is what makes the same key on another operation -- ANOTHER key: a client that mints one key per user action would otherwise have two different -- actions collide. ⚠ 00018 keeps this scope and only changes how it is KEYED — by the path's -- digest, because an unbounded path overflows the index tuple. user_id text not null references users (id) on delete cascade, method text not null, path text not null, key text not null, -- The fingerprint of the REQUEST, so that the same key presented with different parameters is a -- conflict rather than a wrong replay. On a multipart body it covers the declared parts — the -- metadata and the file's name — and never the bytes: a server does not hold a book in memory to -- compare it, and a retry of an interrupted upload re-sends the same file. request_sha256 bytea not null, -- The response to replay. Null while the first attempt is still in flight, which is exactly what -- makes a concurrent repeat answerable with `key_in_flight` rather than with a second attempt. status integer, location text not null default '', body bytea, claimed_at timestamptz not null default now(), finished_at timestamptz, primary key (user_id, method, path, key) ); -- The record is kept at least 24 hours and then forgotten (canon). The index is what a retention -- sweep walks; it is also what answers "is this claim stale" for a first attempt whose process died. create index idempotency_keys_claimed_idx on idempotency_keys (claimed_at); -- +goose Down drop table idempotency_keys;