Files
OpenFUT-Core/migrations/0028_owned_card_contract.sql
T
funman300 e8be289660
CI / Build, lint & test (push) Successful in 3m16s
feat(consume): durable per-instance contract state + HTTP apply route
`consume_item` was a complete, tested, atomic apply transaction with zero
production callers and no route -- it could not be reached over HTTP because
its effect is an in-process `ItemMutation` trait object and the host is a
separate process on a synchronous JSON boundary.

Closes that gap with a CLOSED, Core-validated effect vocabulary rather than a
pass-through: `InstanceEffect::AddContractMatches { amount, cap,
default_when_unset }`. A generic "apply this field/value" escape hatch would
hand economic authority back to the caller and break the architecture.

The read-modify-write runs INSIDE the caller's transaction
(`min(cap, COALESCE(contract_matches, default) + amount)`) so two concurrent
applies cannot lose an update, and the reported `granted` stays the requested
amount even when the cap clamps the total.

Migration 0028 adds `owned_cards.contract_matches` NULLABLE: NULL means "Core
tracks no contract here", which keeps the pack-fresh default (a FIFA-specific
7) out of Core and leaves every existing row unchanged in meaning. ADD COLUMN,
not a rebuild -- a rebuild would drop 0026's transfer trigger.

Two ordering fixes forced by putting this on the live path:
* consume_item moves from DEFERRED `pool.begin()` to `BEGIN IMMEDIATE`, the
  discipline economy.rs documents: three reads precede the first write, which
  is exactly the shape that returns SQLITE_BUSY past the busy handler.
* the replay answer now precedes source validation. With DestroyInstance the
  first apply deletes the source, so the old order answered a retry with 404
  instead of the recorded outcome -- replay semantics were unreachable.
2026-08-22 18:23:05 +00:00

20 lines
1.1 KiB
SQL

-- Per-instance match-contract counter on an owned instance.
--
-- NULLABLE ON PURPOSE. NULL means "Core tracks no contract for this instance",
-- which is NOT the same as zero: a game whose contracts start at a pack-fresh
-- default (FIFA 17 hands out 7) must supply that default itself, so the number
-- stays in the game adapter and never becomes a Core constant. Every row that
-- pre-dates this migration therefore reads back NULL and keeps its exact prior
-- meaning — the migration is a pure widening, not a backfill.
--
-- `>= 0` only: the cap is a per-application input (the caller's game rule), not
-- a schema invariant, so the CHECK refuses the one value that is nonsense in
-- every game rather than pinning someone else's ceiling.
--
-- ALTER TABLE ADD COLUMN, NEVER a table rebuild: `owned_cards` carries the
-- `clear_club_active_item_before_transfer` trigger installed by 0026, and a
-- DROP/recreate would silently take it with it — exactly the failure 0026:44-46
-- documents for 0024's trigger.
ALTER TABLE owned_cards ADD COLUMN contract_matches INTEGER
CHECK (contract_matches IS NULL OR contract_matches >= 0);