39 lines
2.2 KiB
SQL
39 lines
2.2 KiB
SQL
-- +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;
|