-- Durable idempotency for applying a consumable to a target. -- -- Same discipline as `match_completions` (migration 0022): ONE effect per -- (profile_id, action_identity). A sequential replay, a restart replay, a -- concurrent duplicate, or a retried HTTP request all collide on this UNIQUE and -- are refused BEFORE the target is mutated and BEFORE the source is consumed — -- so a consumable can never be spent twice, and its effect can never be applied -- twice from one spend. -- -- `action_identity` is opaque to Core: the game adapter/host derives a stable -- per-application token from its own wire request. Core never parses it. -- -- `source_owned_card_id` / `target_owned_card_id` are deliberately NOT foreign -- keys: the source row is DELETEd (or decremented to zero and deleted) by the -- very transaction that writes this record, and the target may later be sold. -- This table is an audit + replay record, not an ownership reference. -- -- `effect` is the caller-supplied outcome summary stored verbatim as JSON text. -- Core defines NO per-category formula: what a given consumable does to its -- target is the calling game adapter's reversed behaviour, and an unreversed -- behaviour must not be invented here. CREATE TABLE consumable_applications ( id TEXT PRIMARY KEY NOT NULL, profile_id TEXT NOT NULL REFERENCES profiles(id), action_identity TEXT NOT NULL, source_owned_card_id TEXT NOT NULL, source_card_id TEXT NOT NULL, source_content_kind TEXT NOT NULL, -- 1 = the source instance was destroyed; 0 = a stack was decremented. source_consumed INTEGER NOT NULL, -- Remaining stack size after a decrement, NULL when the instance was destroyed. source_quantity_after INTEGER, target_owned_card_id TEXT, effect TEXT NOT NULL, applied_at TEXT NOT NULL, UNIQUE(profile_id, action_identity) ); CREATE INDEX idx_consumable_applications_profile ON consumable_applications(profile_id);