-- +goose Up ALTER TABLE devices ADD COLUMN accessibility_enabled INTEGER NOT NULL DEFAULT 0 CHECK (accessibility_enabled IN (0, 1)); ALTER TABLE devices ADD COLUMN pdd_installed INTEGER NOT NULL DEFAULT 0 CHECK (pdd_installed IN (0, 1)); ALTER TABLE devices ADD COLUMN readiness_reported_at TEXT; ALTER TABLE purchase_tasks ADD COLUMN claimed_by_user_id TEXT REFERENCES users(id) ON UPDATE RESTRICT ON DELETE RESTRICT; ALTER TABLE purchase_tasks ADD COLUMN claimed_by_device_id TEXT REFERENCES devices(id) ON UPDATE RESTRICT ON DELETE RESTRICT; ALTER TABLE purchase_tasks ADD COLUMN claim_generation INTEGER NOT NULL DEFAULT 0 CHECK (claim_generation >= 0); ALTER TABLE purchase_tasks ADD COLUMN claim_token_hash TEXT CHECK ( claim_token_hash IS NULL OR ( length(claim_token_hash) = 64 AND claim_token_hash NOT GLOB '*[^0-9a-f]*' ) ); ALTER TABLE purchase_tasks ADD COLUMN claim_issued_at TEXT; ALTER TABLE purchase_tasks ADD COLUMN claim_expires_at TEXT; ALTER TABLE purchase_tasks ADD COLUMN cancel_requested_at TEXT; ALTER TABLE purchase_tasks ADD COLUMN cancel_requested_by_user_id TEXT REFERENCES users(id) ON UPDATE RESTRICT ON DELETE RESTRICT; CREATE UNIQUE INDEX purchase_tasks_active_device_idx ON purchase_tasks (claimed_by_device_id) WHERE claimed_by_device_id IS NOT NULL AND status IN ('CLAIMED', 'RUNNING', 'WAITING_CONFIRMATION'); CREATE TABLE task_executions ( id TEXT PRIMARY KEY NOT NULL CHECK (length(id) = 36), task_id TEXT NOT NULL REFERENCES purchase_tasks(id) ON UPDATE RESTRICT ON DELETE CASCADE, attempt_no INTEGER NOT NULL CHECK (attempt_no > 0), claim_generation INTEGER NOT NULL CHECK (claim_generation > 0), user_id TEXT NOT NULL REFERENCES users(id) ON UPDATE RESTRICT ON DELETE RESTRICT, device_id TEXT NOT NULL REFERENCES devices(id) ON UPDATE RESTRICT ON DELETE RESTRICT, current_step TEXT NOT NULL CHECK ( length(trim(current_step)) > 0 AND length(CAST(current_step AS BLOB)) <= 64 ), last_heartbeat_at TEXT NOT NULL, order_submitted INTEGER NOT NULL DEFAULT 0 CHECK (order_submitted = 0), started_at TEXT NOT NULL, finished_at TEXT, UNIQUE (task_id, attempt_no), UNIQUE (task_id, claim_generation) ); CREATE UNIQUE INDEX task_executions_active_task_idx ON task_executions (task_id) WHERE finished_at IS NULL; CREATE INDEX task_executions_device_heartbeat_idx ON task_executions (device_id, last_heartbeat_at DESC); CREATE TABLE lifecycle_requests ( user_id TEXT NOT NULL REFERENCES users(id) ON UPDATE RESTRICT ON DELETE RESTRICT, device_id TEXT NOT NULL REFERENCES devices(id) ON UPDATE RESTRICT ON DELETE RESTRICT, operation TEXT NOT NULL CHECK ( operation IN ('CLAIM_NEXT', 'START', 'RELEASE', 'CANCEL_ACK') ), idempotency_key TEXT NOT NULL CHECK ( length(trim(idempotency_key)) > 0 AND length(CAST(idempotency_key AS BLOB)) <= 128 ), request_sha256 TEXT NOT NULL CHECK ( length(request_sha256) = 64 AND request_sha256 NOT GLOB '*[^0-9a-f]*' ), result_kind TEXT NOT NULL CHECK (result_kind IN ('TASK', 'NO_TASK', 'EXECUTION')), task_id TEXT REFERENCES purchase_tasks(id) ON UPDATE RESTRICT ON DELETE RESTRICT, claim_generation INTEGER CHECK (claim_generation IS NULL OR claim_generation > 0), execution_id TEXT REFERENCES task_executions(id) ON UPDATE RESTRICT ON DELETE RESTRICT, created_at TEXT NOT NULL, PRIMARY KEY (user_id, device_id, operation, idempotency_key), CHECK ( ( operation = 'CLAIM_NEXT' AND result_kind = 'NO_TASK' AND task_id IS NULL AND claim_generation IS NULL AND execution_id IS NULL ) OR ( operation = 'CLAIM_NEXT' AND result_kind = 'TASK' AND task_id IS NOT NULL AND claim_generation IS NOT NULL AND execution_id IS NULL ) OR ( operation = 'START' AND result_kind = 'EXECUTION' AND task_id IS NOT NULL AND claim_generation IS NOT NULL AND execution_id IS NOT NULL ) OR ( operation = 'RELEASE' AND result_kind = 'TASK' AND task_id IS NOT NULL AND claim_generation IS NOT NULL AND execution_id IS NULL ) OR ( operation = 'CANCEL_ACK' AND result_kind = 'TASK' AND task_id IS NOT NULL AND claim_generation IS NOT NULL AND execution_id IS NOT NULL ) ) ); ALTER TABLE task_events RENAME TO task_events_v3; CREATE TABLE task_events ( id TEXT PRIMARY KEY NOT NULL CHECK (length(id) = 36), task_id TEXT NOT NULL REFERENCES purchase_tasks(id) ON UPDATE RESTRICT ON DELETE CASCADE, event_type TEXT NOT NULL CHECK ( event_type IN ( 'TASK_CREATED', 'TASK_CLAIMED', 'TASK_RECLAIMED', 'TASK_RELEASED', 'TASK_STARTED', 'TASK_CANCEL_REQUESTED', 'TASK_CANCELED' ) ), message TEXT NOT NULL, occurred_at TEXT NOT NULL, actor_user_id TEXT REFERENCES users(id) ON UPDATE RESTRICT ON DELETE RESTRICT, actor_device_id TEXT REFERENCES devices(id) ON UPDATE RESTRICT ON DELETE RESTRICT ); INSERT INTO task_events ( id, task_id, event_type, message, occurred_at, actor_user_id, actor_device_id ) SELECT id, task_id, event_type, message, occurred_at, actor_user_id, NULL FROM task_events_v3; DROP TABLE task_events_v3; CREATE INDEX task_events_task_occurred_idx ON task_events (task_id, occurred_at ASC, id ASC); CREATE INDEX task_events_actor_user_idx ON task_events (actor_user_id, occurred_at DESC, id DESC); CREATE INDEX task_events_actor_device_idx ON task_events (actor_device_id, occurred_at DESC, id DESC); -- +goose Down CREATE TEMP TABLE task_events_v4_down_guard ( allowed INTEGER NOT NULL CHECK (allowed = 1) ); INSERT INTO task_events_v4_down_guard (allowed) SELECT CASE WHEN EXISTS ( SELECT 1 FROM task_events WHERE event_type NOT IN ('TASK_CREATED', 'TASK_CANCELED') OR actor_device_id IS NOT NULL ) THEN 0 ELSE 1 END; DROP TABLE task_events_v4_down_guard; ALTER TABLE task_events RENAME TO task_events_v4; CREATE TABLE task_events ( id TEXT PRIMARY KEY NOT NULL CHECK (length(id) = 36), task_id TEXT NOT NULL REFERENCES purchase_tasks(id) ON UPDATE RESTRICT ON DELETE CASCADE, event_type TEXT NOT NULL CHECK (event_type IN ('TASK_CREATED', 'TASK_CANCELED')), message TEXT NOT NULL, occurred_at TEXT NOT NULL, actor_user_id TEXT REFERENCES users(id) ON UPDATE RESTRICT ON DELETE RESTRICT ); INSERT INTO task_events ( id, task_id, event_type, message, occurred_at, actor_user_id ) SELECT id, task_id, event_type, message, occurred_at, actor_user_id FROM task_events_v4; DROP TABLE task_events_v4; CREATE INDEX task_events_task_occurred_idx ON task_events (task_id, occurred_at ASC, id ASC); CREATE INDEX task_events_actor_user_idx ON task_events (actor_user_id, occurred_at DESC, id DESC); DROP TABLE lifecycle_requests; DROP INDEX task_executions_device_heartbeat_idx; DROP INDEX task_executions_active_task_idx; DROP TABLE task_executions; DROP INDEX purchase_tasks_active_device_idx; ALTER TABLE purchase_tasks DROP COLUMN cancel_requested_by_user_id; ALTER TABLE purchase_tasks DROP COLUMN cancel_requested_at; ALTER TABLE purchase_tasks DROP COLUMN claim_expires_at; ALTER TABLE purchase_tasks DROP COLUMN claim_issued_at; ALTER TABLE purchase_tasks DROP COLUMN claim_token_hash; ALTER TABLE purchase_tasks DROP COLUMN claim_generation; ALTER TABLE purchase_tasks DROP COLUMN claimed_by_device_id; ALTER TABLE purchase_tasks DROP COLUMN claimed_by_user_id; ALTER TABLE devices DROP COLUMN readiness_reported_at; ALTER TABLE devices DROP COLUMN pdd_installed; ALTER TABLE devices DROP COLUMN accessibility_enabled;