Files
cmroubao/backend-api/migrations/00006_candidate_decisions.sql
T

366 lines
12 KiB
SQL

-- +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;