-- +goose Up CREATE TABLE assets ( id TEXT PRIMARY KEY NOT NULL CHECK (length(id) = 36), creator_subject TEXT NOT NULL CHECK (length(trim(creator_subject)) > 0), purpose TEXT NOT NULL CHECK (purpose IN ('TASK_REFERENCE')), 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 ); CREATE INDEX assets_creator_created_idx ON assets (creator_subject, created_at DESC, id DESC); CREATE TABLE purchase_tasks ( id TEXT PRIMARY KEY NOT NULL CHECK (length(id) = 36), creator_subject TEXT NOT NULL CHECK (length(trim(creator_subject)) > 0), source_ref TEXT CHECK ( source_ref IS NULL OR ( length(trim(source_ref)) > 0 AND length(CAST(source_ref AS BLOB)) <= 256 ) ), title TEXT NOT NULL CHECK ( length(trim(title)) > 0 AND length(title) <= 120 AND length(CAST(title AS BLOB)) <= 2048 ), description TEXT NOT NULL CHECK (length(CAST(description AS BLOB)) <= 8192), sku TEXT NOT NULL CHECK ( length(trim(sku)) > 0 AND length(CAST(sku AS BLOB)) <= 512 ), image_asset_id TEXT NOT NULL UNIQUE REFERENCES assets(id) ON UPDATE RESTRICT ON DELETE RESTRICT, quantity INTEGER NOT NULL CHECK (quantity > 0), max_budget_cents INTEGER CHECK (max_budget_cents IS NULL OR max_budget_cents > 0), currency TEXT NOT NULL DEFAULT 'CNY' CHECK (currency = 'CNY'), status TEXT NOT NULL CHECK ( status IN ( 'PENDING', 'CLAIMED', 'RUNNING', 'WAITING_CONFIRMATION', 'SUCCEEDED', 'FAILED', 'CANCELED' ) ), version INTEGER NOT NULL CHECK (version > 0), cancel_reason TEXT CHECK ( cancel_reason IS NULL OR length(CAST(cancel_reason AS BLOB)) <= 500 ), canceled_at TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); CREATE UNIQUE INDEX purchase_tasks_creator_source_ref_idx ON purchase_tasks (creator_subject, source_ref) WHERE source_ref IS NOT NULL; CREATE INDEX purchase_tasks_creator_created_idx ON purchase_tasks (creator_subject, created_at DESC, id DESC); CREATE INDEX purchase_tasks_creator_status_created_idx ON purchase_tasks ( creator_subject, status, created_at DESC, id DESC ); 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 ); CREATE INDEX task_events_task_occurred_idx ON task_events (task_id, occurred_at ASC, id ASC); CREATE TABLE idempotency_records ( creator_subject TEXT NOT NULL, operation TEXT NOT NULL, idempotency_key TEXT NOT NULL, request_sha256 TEXT NOT NULL CHECK ( length(request_sha256) = 64 AND request_sha256 NOT GLOB '*[^0-9a-f]*' ), resource_type TEXT NOT NULL CHECK (resource_type IN ('ASSET', 'PURCHASE_TASK')), resource_id TEXT NOT NULL CHECK (length(resource_id) = 36), created_at TEXT NOT NULL, PRIMARY KEY (creator_subject, operation, idempotency_key) ); -- +goose Down DROP TABLE IF EXISTS idempotency_records; DROP INDEX IF EXISTS task_events_task_occurred_idx; DROP TABLE IF EXISTS task_events; DROP INDEX IF EXISTS purchase_tasks_creator_status_created_idx; DROP INDEX IF EXISTS purchase_tasks_creator_created_idx; DROP INDEX IF EXISTS purchase_tasks_creator_source_ref_idx; DROP TABLE IF EXISTS purchase_tasks; DROP INDEX IF EXISTS assets_creator_created_idx; DROP TABLE IF EXISTS assets;