textmachine/platform/internal/pgstore/migrations/00017_idempotency.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;