-- +goose Up ALTER TABLE execution_result_requests RENAME TO execution_result_requests_v5; 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', 'HUMAN_REVIEW', '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) ); INSERT INTO execution_result_requests ( user_id, device_id, operation, idempotency_key, request_sha256, claim_token_sha256, task_id, execution_id, resource_id, created_at ) SELECT user_id, device_id, operation, idempotency_key, request_sha256, claim_token_sha256, task_id, execution_id, resource_id, created_at FROM execution_result_requests_v5; DROP TABLE execution_result_requests_v5; CREATE TABLE candidate_search_runs ( 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 ), app_version TEXT, android_version TEXT, pdd_version TEXT, started_at TEXT NOT NULL, received_at TEXT NOT NULL, observation_count INTEGER NOT NULL CHECK (observation_count BETWEEN 0 AND 5), collection_complete INTEGER NOT NULL CHECK (collection_complete = 1), received_after_execution_expiry INTEGER NOT NULL DEFAULT 0 CHECK (received_after_execution_expiry IN (0, 1)) ); CREATE TABLE candidate_observations ( execution_id TEXT NOT NULL REFERENCES candidate_search_runs(execution_id) ON UPDATE RESTRICT ON DELETE CASCADE, task_id TEXT NOT NULL REFERENCES purchase_tasks(id) ON UPDATE RESTRICT ON DELETE CASCADE, ordinal INTEGER NOT NULL CHECK (ordinal BETWEEN 1 AND 5), title TEXT NOT NULL CHECK ( length(trim(title)) > 0 AND length(CAST(title AS BLOB)) <= 512 ), sku_text TEXT NOT NULL CHECK (length(CAST(sku_text AS BLOB)) <= 512), price_text TEXT NOT NULL CHECK (length(CAST(price_text AS BLOB)) <= 64), product_url TEXT NOT NULL CHECK (length(CAST(product_url AS BLOB)) <= 2048), image_url TEXT NOT NULL CHECK (length(CAST(image_url AS BLOB)) <= 2048), evidence_asset_ids_json TEXT NOT NULL CHECK (length(CAST(evidence_asset_ids_json AS BLOB)) <= 4096), collection_status TEXT NOT NULL CHECK (collection_status IN ('COMPLETE', 'PARTIAL')), observed_at TEXT NOT NULL, PRIMARY KEY (execution_id, ordinal) ); CREATE TABLE model_runs ( execution_id TEXT PRIMARY KEY NOT NULL REFERENCES candidate_search_runs(execution_id) ON UPDATE RESTRICT ON DELETE CASCADE, provider_id TEXT NOT NULL CHECK ( length(trim(provider_id)) > 0 AND length(CAST(provider_id AS BLOB)) <= 128 ), model TEXT NOT NULL CHECK ( length(trim(model)) > 0 AND length(CAST(model AS BLOB)) <= 256 ), prompt_version TEXT NOT NULL CHECK ( length(trim(prompt_version)) > 0 AND length(CAST(prompt_version AS BLOB)) <= 128 ), schema_version INTEGER NOT NULL CHECK (schema_version BETWEEN 1 AND 1000), recommendation_threshold REAL NOT NULL CHECK (recommendation_threshold BETWEEN 0 AND 1), request_sha256 TEXT NOT NULL CHECK ( length(request_sha256) = 64 AND request_sha256 NOT GLOB '*[^0-9a-f]*' ), result_sha256 TEXT NOT NULL CHECK ( length(result_sha256) = 64 AND result_sha256 NOT GLOB '*[^0-9a-f]*' ), duration_millis INTEGER, input_tokens INTEGER, output_tokens INTEGER, cost_microunits INTEGER, created_at TEXT NOT NULL ); CREATE TABLE candidate_evaluations ( execution_id TEXT NOT NULL, candidate_ordinal INTEGER NOT NULL, decision TEXT NOT NULL CHECK (decision IN ('REVIEW', 'REJECT', 'MANUAL_REQUIRED')), score REAL NOT NULL CHECK (score BETWEEN 0 AND 1), confidence REAL NOT NULL CHECK (confidence BETWEEN 0 AND 1), matched_json TEXT NOT NULL, missing_or_uncertain_json TEXT NOT NULL, rejection_reasons_json TEXT NOT NULL, hard_constraints_json TEXT NOT NULL, created_at TEXT NOT NULL, PRIMARY KEY (execution_id, candidate_ordinal), FOREIGN KEY (execution_id) REFERENCES model_runs(execution_id) ON UPDATE RESTRICT ON DELETE CASCADE, FOREIGN KEY (execution_id, candidate_ordinal) REFERENCES candidate_observations(execution_id, ordinal) ON UPDATE RESTRICT ON DELETE CASCADE ); CREATE TABLE candidate_recommendations ( execution_id TEXT PRIMARY KEY NOT NULL, candidate_ordinal INTEGER NOT NULL, conclusion TEXT NOT NULL CHECK (conclusion = 'SUGGESTED'), policy_version TEXT NOT NULL CHECK ( length(trim(policy_version)) > 0 AND length(CAST(policy_version AS BLOB)) <= 128 ), reasons_json TEXT NOT NULL CHECK (length(CAST(reasons_json AS BLOB)) <= 4096), created_at TEXT NOT NULL, FOREIGN KEY (execution_id, candidate_ordinal) REFERENCES candidate_observations(execution_id, ordinal) ON UPDATE RESTRICT ON DELETE CASCADE ); CREATE TABLE candidate_human_reviews ( id TEXT PRIMARY KEY NOT NULL CHECK (length(id) = 36), execution_id TEXT NOT NULL REFERENCES candidate_search_runs(execution_id) ON UPDATE RESTRICT ON DELETE CASCADE, task_id TEXT NOT NULL REFERENCES purchase_tasks(id) ON UPDATE RESTRICT ON DELETE CASCADE, version INTEGER NOT NULL CHECK (version > 0), reason_schema_version INTEGER NOT NULL CHECK (reason_schema_version = 1), outcome TEXT NOT NULL CHECK ( outcome IN ( 'CANDIDATE_ACCEPTED', 'CANDIDATE_REJECTED', 'NO_MATCH', 'MANUAL_REQUIRED' ) ), selected_candidate_ordinal INTEGER, primary_reason_code TEXT NOT NULL, note TEXT NOT NULL CHECK (length(CAST(note AS BLOB)) <= 800), supersedes_review_id TEXT UNIQUE REFERENCES candidate_human_reviews(id) ON UPDATE RESTRICT ON DELETE RESTRICT, actor_user_id TEXT NOT NULL REFERENCES users(id) ON UPDATE RESTRICT ON DELETE RESTRICT, actor_device_id TEXT REFERENCES devices(id) ON UPDATE RESTRICT ON DELETE RESTRICT, created_at TEXT NOT NULL, received_after_execution_expiry INTEGER NOT NULL DEFAULT 0 CHECK (received_after_execution_expiry IN (0, 1)), UNIQUE (execution_id, version), FOREIGN KEY (execution_id, selected_candidate_ordinal) REFERENCES candidate_observations(execution_id, ordinal) ON UPDATE RESTRICT ON DELETE RESTRICT ); CREATE INDEX candidate_human_reviews_execution_version_idx ON candidate_human_reviews (execution_id, version DESC); CREATE TABLE candidate_human_review_items ( review_id TEXT NOT NULL REFERENCES candidate_human_reviews(id) ON UPDATE RESTRICT ON DELETE CASCADE, candidate_ordinal INTEGER NOT NULL, label TEXT NOT NULL CHECK (label IN ('ACCEPT', 'REJECT')), primary_reason_code TEXT NOT NULL, note TEXT NOT NULL CHECK (length(CAST(note AS BLOB)) <= 800), PRIMARY KEY (review_id, candidate_ordinal) ); CREATE TABLE candidate_human_review_reasons ( review_id TEXT NOT NULL, candidate_ordinal INTEGER NOT NULL, reason_code TEXT NOT NULL, PRIMARY KEY (review_id, candidate_ordinal, reason_code), FOREIGN KEY (review_id, candidate_ordinal) REFERENCES candidate_human_review_items(review_id, candidate_ordinal) ON UPDATE RESTRICT ON DELETE CASCADE ); -- +goose Down CREATE TEMP TABLE candidate_decisions_v6_down_guard ( allowed INTEGER NOT NULL CHECK (allowed = 1) ); INSERT INTO candidate_decisions_v6_down_guard (allowed) SELECT CASE WHEN EXISTS (SELECT 1 FROM candidate_search_runs) OR EXISTS (SELECT 1 FROM candidate_human_reviews) OR EXISTS ( SELECT 1 FROM execution_result_requests WHERE operation = 'HUMAN_REVIEW' ) THEN 0 ELSE 1 END; DROP TABLE candidate_decisions_v6_down_guard; DROP TABLE candidate_human_review_reasons; DROP TABLE candidate_human_review_items; DROP INDEX candidate_human_reviews_execution_version_idx; DROP TABLE candidate_human_reviews; DROP TABLE candidate_recommendations; DROP TABLE candidate_evaluations; DROP TABLE model_runs; DROP TABLE candidate_observations; DROP TABLE candidate_search_runs; ALTER TABLE execution_result_requests RENAME TO execution_result_requests_v6; 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) ); INSERT INTO execution_result_requests ( user_id, device_id, operation, idempotency_key, request_sha256, claim_token_sha256, task_id, execution_id, resource_id, created_at ) SELECT user_id, device_id, operation, idempotency_key, request_sha256, claim_token_sha256, task_id, execution_id, resource_id, created_at FROM execution_result_requests_v6; DROP TABLE execution_result_requests_v6;