-- +goose Up CREATE TABLE execution_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, execution_id TEXT NOT NULL REFERENCES task_executions(id) ON UPDATE RESTRICT ON DELETE CASCADE, step TEXT NOT NULL CHECK ( length(trim(step)) > 0 AND length(CAST(step AS BLOB)) <= 64 ), event_type TEXT NOT NULL CHECK ( length(trim(event_type)) > 0 AND length(CAST(event_type AS BLOB)) <= 64 ), message TEXT NOT NULL CHECK (length(CAST(message AS BLOB)) <= 1000), occurred_at TEXT NOT NULL, received_at TEXT NOT NULL, received_after_execution_expiry INTEGER NOT NULL DEFAULT 0 CHECK (received_after_execution_expiry IN (0, 1)) ); CREATE INDEX execution_events_execution_occurred_idx ON execution_events (execution_id, occurred_at ASC, id ASC); CREATE TABLE execution_evidence_assets ( 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, execution_id TEXT NOT NULL REFERENCES task_executions(id) ON UPDATE RESTRICT ON DELETE CASCADE, media_type TEXT NOT NULL CHECK (media_type = 'image/jpeg'), size_bytes INTEGER NOT NULL CHECK (size_bytes > 0), sha256 TEXT NOT NULL CHECK ( length(sha256) = 64 AND sha256 NOT GLOB '*[^0-9a-f]*' ), storage_key TEXT NOT NULL UNIQUE CHECK ( length(storage_key) > 0 AND substr(storage_key, 1, 1) <> '/' AND instr(storage_key, '\') = 0 AND instr(storage_key, '..') = 0 ), created_at TEXT NOT NULL, received_after_execution_expiry INTEGER NOT NULL DEFAULT 0 CHECK (received_after_execution_expiry IN (0, 1)) ); CREATE INDEX execution_evidence_execution_created_idx ON execution_evidence_assets (execution_id, created_at ASC, id ASC); CREATE TABLE execution_candidate_batches ( execution_id TEXT PRIMARY KEY NOT NULL REFERENCES task_executions(id) ON UPDATE RESTRICT ON DELETE CASCADE, task_id TEXT NOT NULL REFERENCES purchase_tasks(id) ON UPDATE RESTRICT ON DELETE CASCADE, task_content_sha256 TEXT NOT NULL CHECK ( length(task_content_sha256) = 64 AND task_content_sha256 NOT GLOB '*[^0-9a-f]*' ), execution_mode TEXT NOT NULL CHECK (execution_mode IN ('MANUAL_FIRST', 'AI_ASSISTED')), search_query TEXT NOT NULL CHECK ( length(trim(search_query)) > 0 AND length(CAST(search_query AS BLOB)) <= 512 ), provenance_json TEXT, candidates_json TEXT NOT NULL CHECK (length(CAST(candidates_json AS BLOB)) <= 65536), recommendation_json TEXT, received_at TEXT NOT NULL, received_after_execution_expiry INTEGER NOT NULL DEFAULT 0 CHECK (received_after_execution_expiry IN (0, 1)) ); CREATE TABLE execution_outcomes ( execution_id TEXT PRIMARY KEY NOT NULL REFERENCES task_executions(id) ON UPDATE RESTRICT ON DELETE CASCADE, task_id TEXT NOT NULL REFERENCES purchase_tasks(id) ON UPDATE RESTRICT ON DELETE CASCADE, result_type TEXT NOT NULL CHECK (result_type IN ('COMPLETE', 'FAIL')), execution_mode TEXT CHECK (execution_mode IS NULL OR execution_mode IN ('MANUAL_FIRST', 'AI_ASSISTED')), task_content_sha256 TEXT CHECK ( task_content_sha256 IS NULL OR ( length(task_content_sha256) = 64 AND task_content_sha256 NOT GLOB '*[^0-9a-f]*' ) ), outcome TEXT CHECK ( outcome IS NULL OR outcome IN ( 'CANDIDATE_ACCEPTED', 'CANDIDATE_REJECTED', 'NO_MATCH', 'MANUAL_REQUIRED' ) ), operator_reason TEXT CHECK ( operator_reason IS NULL OR length(CAST(operator_reason AS BLOB)) <= 1000 ), selected_candidate_json TEXT, evidence_asset_ids_json TEXT, error_code TEXT CHECK (error_code IS NULL OR length(CAST(error_code AS BLOB)) <= 64), error_message TEXT CHECK (error_message IS NULL OR length(CAST(error_message AS BLOB)) <= 1000), error_step TEXT CHECK (error_step IS NULL OR length(CAST(error_step AS BLOB)) <= 64), retryable INTEGER CHECK (retryable IS NULL OR retryable IN (0, 1)), order_submitted INTEGER NOT NULL DEFAULT 0 CHECK (order_submitted = 0), received_at TEXT NOT NULL, received_after_execution_expiry INTEGER NOT NULL DEFAULT 0 CHECK (received_after_execution_expiry IN (0, 1)), CHECK ( (result_type = 'COMPLETE' AND execution_mode IS NOT NULL AND task_content_sha256 IS NOT NULL AND outcome IS NOT NULL AND operator_reason IS NOT NULL AND error_code IS NULL) OR (result_type = 'FAIL' AND error_code IS NOT NULL AND error_message IS NOT NULL AND error_step IS NOT NULL) ) ); CREATE TABLE execution_result_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 ('EVENTS', 'EVIDENCE', 'CANDIDATES', 'COMPLETE', 'FAIL')), 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]*' ), claim_token_sha256 TEXT NOT NULL CHECK ( length(claim_token_sha256) = 64 AND claim_token_sha256 NOT GLOB '*[^0-9a-f]*' ), task_id TEXT NOT NULL REFERENCES purchase_tasks(id) ON UPDATE RESTRICT ON DELETE RESTRICT, execution_id TEXT NOT NULL REFERENCES task_executions(id) ON UPDATE RESTRICT ON DELETE RESTRICT, resource_id TEXT, created_at TEXT NOT NULL, PRIMARY KEY (user_id, device_id, operation, idempotency_key) ); -- +goose Down CREATE TEMP TABLE execution_results_v5_down_guard ( allowed INTEGER NOT NULL CHECK (allowed = 1) ); INSERT INTO execution_results_v5_down_guard (allowed) SELECT CASE WHEN EXISTS (SELECT 1 FROM execution_events) OR EXISTS (SELECT 1 FROM execution_evidence_assets) OR EXISTS (SELECT 1 FROM execution_candidate_batches) OR EXISTS (SELECT 1 FROM execution_outcomes) OR EXISTS (SELECT 1 FROM execution_result_requests) THEN 0 ELSE 1 END; DROP TABLE execution_results_v5_down_guard; DROP TABLE execution_result_requests; DROP TABLE execution_outcomes; DROP TABLE execution_candidate_batches; DROP INDEX execution_evidence_execution_created_idx; DROP TABLE execution_evidence_assets; DROP INDEX execution_events_execution_occurred_idx; DROP TABLE execution_events;