-- +goose Up -- v1 的试选/锁旧价记录无法安全推断为单趟执行事实。先在同一事务中拒绝它们, -- 避免删除审计数据后再尝试猜测映射。 CREATE TABLE single_pass_upgrade_guard ( valid INTEGER NOT NULL CHECK (valid = 1) ); INSERT INTO single_pass_upgrade_guard (valid) SELECT CASE WHEN (SELECT COUNT(*) FROM spec_trials) = 0 AND (SELECT COUNT(*) FROM order_authorizations) = 0 AND (SELECT COUNT(*) FROM order_submissions) = 0 AND (SELECT COUNT(*) FROM tasks WHERE source <> 'MANUAL' OR status <> 'DRAFT') = 0 -- v2 的金额边界是严格正数;不把 v1 中不能无损纳入该边界的数据悄悄改写。 AND (SELECT COUNT(*) FROM tasks WHERE max_total_price = '' OR max_total_price GLOB '*[^0-9.]*' OR length(max_total_price) - length(replace(max_total_price, '.', '')) > 1 OR max_total_price = '.' OR (instr(max_total_price, '.') > 0 AND ( instr(max_total_price, '.') = 1 OR length(max_total_price) = instr(max_total_price, '.') OR length(max_total_price) - instr(max_total_price, '.') > 2 )) OR replace(replace(max_total_price, '.', ''), '0', '') = '' ) = 0 THEN 1 ELSE 0 END; DROP TABLE single_pass_upgrade_guard; ALTER TABLE tasks RENAME TO tasks_v1; DROP TABLE order_submissions; DROP TABLE order_authorizations; DROP TABLE spec_trials; CREATE TABLE tasks ( id TEXT PRIMARY KEY, source TEXT NOT NULL CHECK (source IN ('MANUAL', 'EXCEL', 'ERP')), source_ref TEXT, title TEXT NOT NULL, goods_id TEXT NOT NULL, sku_color TEXT NOT NULL, sku_size TEXT NOT NULL, quantity INTEGER NOT NULL CHECK (quantity > 0 AND typeof(quantity) = 'integer'), max_total_price TEXT NOT NULL CHECK ( max_total_price <> '' AND max_total_price NOT GLOB '*[^0-9.]*' AND length(max_total_price) - length(replace(max_total_price, '.', '')) <= 1 AND max_total_price <> '.' AND (instr(max_total_price, '.') = 0 OR ( instr(max_total_price, '.') > 1 AND length(max_total_price) > instr(max_total_price, '.') AND length(max_total_price) - instr(max_total_price, '.') <= 2 )) AND replace(replace(max_total_price, '.', ''), '0', '') <> '' ), reference_asset_id TEXT, status TEXT NOT NULL CHECK (status IN ( 'DRAFT', 'PENDING', 'CLAIMED', 'ORDERING', 'NEEDS_MANUAL', 'WAITING_PAYMENT', 'RECONCILIATION_REQUIRED', 'SUCCEEDED', 'FAILED', 'CANCELED' )), version INTEGER NOT NULL DEFAULT 1 CHECK (version > 0 AND typeof(version) = 'integer'), created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); INSERT INTO tasks ( id, source, source_ref, title, goods_id, sku_color, sku_size, quantity, max_total_price, reference_asset_id, status, version, created_at, updated_at ) SELECT id, source, source_ref, title, goods_id, sku_color, sku_size, quantity, max_total_price, reference_asset_id, status, version, created_at, updated_at FROM tasks_v1; DROP TABLE tasks_v1; CREATE TABLE order_authorizations ( id TEXT PRIMARY KEY, task_id TEXT NOT NULL REFERENCES tasks(id), task_version INTEGER NOT NULL CHECK (task_version > 0 AND typeof(task_version) = 'integer'), start_key TEXT NOT NULL, goods_id TEXT NOT NULL, sku_color TEXT NOT NULL, sku_size TEXT NOT NULL, quantity INTEGER NOT NULL CHECK (quantity > 0 AND typeof(quantity) = 'integer'), total_price_cap TEXT NOT NULL CHECK ( total_price_cap <> '' AND total_price_cap NOT GLOB '*[^0-9.]*' AND length(total_price_cap) - length(replace(total_price_cap, '.', '')) <= 1 AND total_price_cap <> '.' AND (instr(total_price_cap, '.') = 0 OR ( instr(total_price_cap, '.') > 1 AND length(total_price_cap) > instr(total_price_cap, '.') AND length(total_price_cap) - instr(total_price_cap, '.') <= 2 )) AND replace(replace(total_price_cap, '.', ''), '0', '') <> '' ), status TEXT NOT NULL CHECK (status IN ('ACTIVE', 'CLAIMED', 'FENCED', 'CONSUMED', 'EXPIRED', 'ABANDONED')), created_by TEXT NOT NULL, created_at TEXT NOT NULL, expires_at TEXT NOT NULL, UNIQUE (task_id, task_version), UNIQUE (start_key, task_id), UNIQUE (task_id, id) ); CREATE TABLE purchase_attempts ( id TEXT PRIMARY KEY, task_id TEXT NOT NULL, authorization_id TEXT NOT NULL, claim_generation INTEGER NOT NULL CHECK (claim_generation > 0 AND typeof(claim_generation) = 'integer'), status TEXT NOT NULL CHECK (status IN ('CLAIMED', 'ORDERING', 'FAILED', 'FENCED', 'ABANDONED')), gate1_unit_price TEXT CHECK ( gate1_unit_price IS NULL OR ( gate1_unit_price <> '' AND gate1_unit_price NOT GLOB '*[^0-9.]*' AND length(gate1_unit_price) - length(replace(gate1_unit_price, '.', '')) <= 1 AND gate1_unit_price <> '.' AND (instr(gate1_unit_price, '.') = 0 OR ( instr(gate1_unit_price, '.') > 1 AND length(gate1_unit_price) > instr(gate1_unit_price, '.') AND length(gate1_unit_price) - instr(gate1_unit_price, '.') <= 2 )) AND replace(replace(gate1_unit_price, '.', ''), '0', '') <> '' ) ), gate2_unit_price TEXT CHECK ( gate2_unit_price IS NULL OR ( gate2_unit_price <> '' AND gate2_unit_price NOT GLOB '*[^0-9.]*' AND length(gate2_unit_price) - length(replace(gate2_unit_price, '.', '')) <= 1 AND gate2_unit_price <> '.' AND (instr(gate2_unit_price, '.') = 0 OR ( instr(gate2_unit_price, '.') > 1 AND length(gate2_unit_price) > instr(gate2_unit_price, '.') AND length(gate2_unit_price) - instr(gate2_unit_price, '.') <= 2 )) AND replace(replace(gate2_unit_price, '.', ''), '0', '') <> '' ) ), quantity_read INTEGER CHECK (quantity_read IS NULL OR (quantity_read > 0 AND typeof(quantity_read) = 'integer')), confirm_amount TEXT CHECK ( confirm_amount IS NULL OR ( confirm_amount <> '' AND confirm_amount NOT GLOB '*[^0-9.]*' AND length(confirm_amount) - length(replace(confirm_amount, '.', '')) <= 1 AND confirm_amount <> '.' AND (instr(confirm_amount, '.') = 0 OR ( instr(confirm_amount, '.') > 1 AND length(confirm_amount) > instr(confirm_amount, '.') AND length(confirm_amount) - instr(confirm_amount, '.') <= 2 )) AND replace(replace(confirm_amount, '.', ''), '0', '') <> '' ) ), failure_code TEXT CHECK (failure_code IS NULL OR failure_code IN ( 'AUTHORIZATION_EXPIRED', 'LEASE_LOST', 'GATE_1_REJECTED', 'QUANTITY_MISMATCH', 'GATE_2_REJECTED', 'GATE_3_REJECTED', 'FENCE_REJECTED', 'SAFE_ABORTED' )), started_at TEXT NOT NULL, finished_at TEXT, UNIQUE (task_id, claim_generation), UNIQUE (task_id, id), UNIQUE (task_id, authorization_id, id), FOREIGN KEY (task_id, authorization_id) REFERENCES order_authorizations(task_id, id) ); CREATE TABLE order_submissions ( id TEXT PRIMARY KEY, task_id TEXT NOT NULL, authorization_id TEXT NOT NULL, attempt_id TEXT NOT NULL, status TEXT NOT NULL CHECK (status IN ('FENCED', 'SUBMITTED', 'RECONCILIATION_REQUIRED', 'MANUAL_RESOLVED')), gate1_unit_price TEXT NOT NULL CHECK ( gate1_unit_price <> '' AND gate1_unit_price NOT GLOB '*[^0-9.]*' AND length(gate1_unit_price) - length(replace(gate1_unit_price, '.', '')) <= 1 AND gate1_unit_price <> '.' AND (instr(gate1_unit_price, '.') = 0 OR ( instr(gate1_unit_price, '.') > 1 AND length(gate1_unit_price) > instr(gate1_unit_price, '.') AND length(gate1_unit_price) - instr(gate1_unit_price, '.') <= 2 )) AND replace(replace(gate1_unit_price, '.', ''), '0', '') <> '' ), gate2_unit_price TEXT NOT NULL CHECK ( gate2_unit_price <> '' AND gate2_unit_price NOT GLOB '*[^0-9.]*' AND length(gate2_unit_price) - length(replace(gate2_unit_price, '.', '')) <= 1 AND gate2_unit_price <> '.' AND (instr(gate2_unit_price, '.') = 0 OR ( instr(gate2_unit_price, '.') > 1 AND length(gate2_unit_price) > instr(gate2_unit_price, '.') AND length(gate2_unit_price) - instr(gate2_unit_price, '.') <= 2 )) AND replace(replace(gate2_unit_price, '.', ''), '0', '') <> '' ), quantity_read INTEGER NOT NULL CHECK (quantity_read > 0 AND typeof(quantity_read) = 'integer'), confirm_amount TEXT NOT NULL CHECK ( confirm_amount <> '' AND confirm_amount NOT GLOB '*[^0-9.]*' AND length(confirm_amount) - length(replace(confirm_amount, '.', '')) <= 1 AND confirm_amount <> '.' AND (instr(confirm_amount, '.') = 0 OR ( instr(confirm_amount, '.') > 1 AND length(confirm_amount) > instr(confirm_amount, '.') AND length(confirm_amount) - instr(confirm_amount, '.') <= 2 )) AND replace(replace(confirm_amount, '.', ''), '0', '') <> '' ), created_at TEXT NOT NULL, resolved_at TEXT, UNIQUE (authorization_id), UNIQUE (attempt_id), FOREIGN KEY (task_id, authorization_id, attempt_id) REFERENCES purchase_attempts(task_id, authorization_id, id) ); -- +goose Down -- 只有尚未产生任何单趟授权或执行事实的纯 MANUAL/DRAFT 数据才能无损回到 v1。 CREATE TABLE single_pass_downgrade_guard ( valid INTEGER NOT NULL CHECK (valid = 1) ); INSERT INTO single_pass_downgrade_guard (valid) SELECT CASE WHEN (SELECT COUNT(*) FROM order_authorizations) = 0 AND (SELECT COUNT(*) FROM purchase_attempts) = 0 AND (SELECT COUNT(*) FROM order_submissions) = 0 AND (SELECT COUNT(*) FROM tasks WHERE source <> 'MANUAL' OR status <> 'DRAFT') = 0 THEN 1 ELSE 0 END; DROP TABLE single_pass_downgrade_guard; ALTER TABLE tasks RENAME TO tasks_v2; DROP TABLE order_submissions; DROP TABLE purchase_attempts; DROP TABLE order_authorizations; CREATE TABLE tasks ( id TEXT PRIMARY KEY, source TEXT NOT NULL CHECK (source IN ('MANUAL', 'EXCEL', 'ERP')), source_ref TEXT, title TEXT NOT NULL, goods_id TEXT NOT NULL, sku_color TEXT NOT NULL, sku_size TEXT NOT NULL, quantity INTEGER NOT NULL CHECK (quantity > 0 AND typeof(quantity) = 'integer'), max_total_price TEXT NOT NULL CHECK ( max_total_price <> '' AND max_total_price NOT GLOB '*[^0-9.]*' AND length(max_total_price) - length(replace(max_total_price, '.', '')) <= 1 AND max_total_price <> '.' AND (instr(max_total_price, '.') = 0 OR ( instr(max_total_price, '.') > 1 AND length(max_total_price) > instr(max_total_price, '.') AND length(max_total_price) - instr(max_total_price, '.') <= 2 )) ), reference_asset_id TEXT, status TEXT NOT NULL CHECK (status IN ( 'DRAFT', 'PENDING', 'CLAIMED', 'RUNNING', 'WAITING_CONFIRMATION', 'PENDING_RETRIAL', 'AUTHORIZED', 'ORDERING', 'WAITING_PAYMENT', 'RECONCILIATION_REQUIRED', 'NEEDS_MANUAL', 'SUCCEEDED', 'CANCELED' )), version INTEGER NOT NULL DEFAULT 1 CHECK (version > 0 AND typeof(version) = 'integer'), created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); INSERT INTO tasks ( id, source, source_ref, title, goods_id, sku_color, sku_size, quantity, max_total_price, reference_asset_id, status, version, created_at, updated_at ) SELECT id, source, source_ref, title, goods_id, sku_color, sku_size, quantity, max_total_price, reference_asset_id, status, version, created_at, updated_at FROM tasks_v2; DROP TABLE tasks_v2; CREATE TABLE spec_trials ( id TEXT PRIMARY KEY, task_id TEXT NOT NULL REFERENCES tasks(id), attempt INTEGER NOT NULL CHECK (attempt > 0 AND typeof(attempt) = 'integer'), product_title TEXT NOT NULL, selected_color TEXT NOT NULL, selected_size TEXT NOT NULL, unit_price TEXT NOT NULL CHECK (unit_price <> '' AND unit_price NOT GLOB '*[^0-9.]*' AND length(unit_price) - length(replace(unit_price, '.', '')) <= 1 AND unit_price <> '.' AND (instr(unit_price, '.') = 0 OR (instr(unit_price, '.') > 1 AND length(unit_price) > instr(unit_price, '.') AND length(unit_price) - instr(unit_price, '.') <= 2))), total_price TEXT NOT NULL CHECK (total_price <> '' AND total_price NOT GLOB '*[^0-9.]*' AND length(total_price) - length(replace(total_price, '.', '')) <= 1 AND total_price <> '.' AND (instr(total_price, '.') = 0 OR (instr(total_price, '.') > 1 AND length(total_price) > instr(total_price, '.') AND length(total_price) - instr(total_price, '.') <= 2))), evidence_sha256 TEXT NOT NULL, created_at TEXT NOT NULL, UNIQUE (task_id, attempt), UNIQUE (task_id, id) ); CREATE TABLE order_authorizations ( id TEXT PRIMARY KEY, task_id TEXT NOT NULL REFERENCES tasks(id), spec_trial_id TEXT NOT NULL REFERENCES spec_trials(id), version INTEGER NOT NULL CHECK (version > 0 AND typeof(version) = 'integer'), goods_id TEXT NOT NULL, sku_color TEXT NOT NULL, sku_size TEXT NOT NULL, quantity INTEGER NOT NULL CHECK (quantity > 0 AND typeof(quantity) = 'integer'), authorized_unit_price TEXT NOT NULL CHECK (authorized_unit_price <> '' AND authorized_unit_price NOT GLOB '*[^0-9.]*' AND length(authorized_unit_price) - length(replace(authorized_unit_price, '.', '')) <= 1 AND authorized_unit_price <> '.' AND (instr(authorized_unit_price, '.') = 0 OR (instr(authorized_unit_price, '.') > 1 AND length(authorized_unit_price) > instr(authorized_unit_price, '.') AND length(authorized_unit_price) - instr(authorized_unit_price, '.') <= 2))), total_price_cap TEXT NOT NULL CHECK (total_price_cap <> '' AND total_price_cap NOT GLOB '*[^0-9.]*' AND length(total_price_cap) - length(replace(total_price_cap, '.', '')) <= 1 AND total_price_cap <> '.' AND (instr(total_price_cap, '.') = 0 OR (instr(total_price_cap, '.') > 1 AND length(total_price_cap) > instr(total_price_cap, '.') AND length(total_price_cap) - instr(total_price_cap, '.') <= 2))), note TEXT, status TEXT NOT NULL CHECK (status IN ('PENDING_DELIVERY', 'DELIVERED', 'ACKNOWLEDGED', 'EXECUTING', 'FENCED', 'CONSUMED', 'SUPERSEDED', 'EXPIRED')), created_by TEXT NOT NULL, created_at TEXT NOT NULL, expires_at TEXT NOT NULL, UNIQUE (task_id, version), UNIQUE (task_id, id), FOREIGN KEY (task_id, spec_trial_id) REFERENCES spec_trials(task_id, id) ); CREATE TABLE order_submissions ( id TEXT PRIMARY KEY, task_id TEXT NOT NULL REFERENCES tasks(id), authorization_id TEXT NOT NULL REFERENCES order_authorizations(id), command_id TEXT NOT NULL, dry_run_id TEXT NOT NULL, status TEXT NOT NULL CHECK (status IN ('FENCED', 'SUBMITTED', 'RECONCILIATION_REQUIRED', 'MANUAL_RESOLVED')), verified_unit_price TEXT NOT NULL CHECK (verified_unit_price <> '' AND verified_unit_price NOT GLOB '*[^0-9.]*' AND length(verified_unit_price) - length(replace(verified_unit_price, '.', '')) <= 1 AND verified_unit_price <> '.' AND (instr(verified_unit_price, '.') = 0 OR (instr(verified_unit_price, '.') > 1 AND length(verified_unit_price) > instr(verified_unit_price, '.') AND length(verified_unit_price) - instr(verified_unit_price, '.') <= 2))), quantity_read INTEGER NOT NULL CHECK (quantity_read > 0 AND typeof(quantity_read) = 'integer'), confirm_page_amount TEXT NOT NULL CHECK (confirm_page_amount <> '' AND confirm_page_amount NOT GLOB '*[^0-9.]*' AND length(confirm_page_amount) - length(replace(confirm_page_amount, '.', '')) <= 1 AND confirm_page_amount <> '.' AND (instr(confirm_page_amount, '.') = 0 OR (instr(confirm_page_amount, '.') > 1 AND length(confirm_page_amount) > instr(confirm_page_amount, '.') AND length(confirm_page_amount) - instr(confirm_page_amount, '.') <= 2))), created_at TEXT NOT NULL, resolved_at TEXT, UNIQUE (authorization_id), UNIQUE (command_id), FOREIGN KEY (task_id, authorization_id) REFERENCES order_authorizations(task_id, id) );