- 新增 example_llm_call(97)、example_run/example_run_receipt/example_quality_result(98) - 加固:全对象幂等(IF NOT EXISTS/OR REPLACE)、append-only 表加 TRUNCATE 防护、 运行终态闭合约束、token/成本非负与 model_match 一致性 CHECK(已 DROP 空表重 apply 验证) - db.py:新增 execparams 参数化写(%s 服务端绑定+语句白名单+stdin 大对象); query 改只读连接(拒 INSERT...RETURNING);tables 用 sql.Identifier 安全拼接;connect() 统一入口 - 表映射登记 97/98、96 标不启用(仅本轮改动,未含他处未提交内容)
181 lines
11 KiB
PL/PgSQL
181 lines
11 KiB
PL/PgSQL
-- 运行级账本三表:example_run(运行注册)+ example_run_receipt(运行身份回执)+ example_quality_result(质量评判)。
|
||
-- 设计拍板 2026-07-30(docs/2026-07-30-落库与看板设计.md §2.2,已吸收四路评审 + Codex 加固)。
|
||
-- 三表分工:run 记"这是哪一次运行、哪个作品、谁触发、终态"(可变,运行时更新终态);
|
||
-- receipt 记"这次运行的每一段身份与哈希链"(append-only,字段平移自 runtime ExecutionReceipt ⊕ CAS 身份);
|
||
-- quality_result 记"这次运行判了什么"(append-only,检测/评分/审核/实验,绑候选哈希、带量表版本)。
|
||
-- 公共列分界(落库设计 §1):run 是可变注册表,带公共列六件套 + 软删;
|
||
-- receipt/quality_result 是 append-only 账本,只带归属列(creator/create_time/tenant_id),
|
||
-- 不带 updater/update_time/deleted 三死列(禁改删时永不写入)。
|
||
-- 哈希列按落库设计 §1 归一规则存裸 64 位小写 hex(剥 sha256: 前缀)。
|
||
-- 注意:列名用 trigger_source 而非 trigger——trigger 是 SQL 保留字。
|
||
-- 全部对象用 IF NOT EXISTS / OR REPLACE,重复 apply 幂等。
|
||
|
||
-- ══ example_run:运行注册(一次运行一行,所有运行级子表挂它;回应"run_id 无主")══
|
||
CREATE TABLE IF NOT EXISTS example_run (
|
||
run_id VARCHAR(64) PRIMARY KEY, -- 运行标识(自然键,全局唯一,子表软引用它)
|
||
work_id BIGINT, -- → muse_content_work.id(软引用;回答"哪个作品",08 §3 硬要求)
|
||
target_chapter INTEGER, -- 目标章序(可空:非章级运行,如拆书/诊断)
|
||
trigger_source VARCHAR(32) NOT NULL DEFAULT 'user', -- user/replay_eval/diagnostic(触发来源)
|
||
trigger_detail JSONB, -- 触发上下文(用户意图摘要等)
|
||
started_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
finished_at TIMESTAMP,
|
||
terminal_state VARCHAR(20) NOT NULL DEFAULT 'running', -- running/completed/failed
|
||
creator VARCHAR(64) NOT NULL DEFAULT '',
|
||
create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
updater VARCHAR(64) NOT NULL DEFAULT '',
|
||
update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
deleted BOOLEAN NOT NULL DEFAULT FALSE,
|
||
tenant_id BIGINT NOT NULL DEFAULT 0,
|
||
CONSTRAINT chk_example_run_trigger CHECK (trigger_source IN ('user','replay_eval','diagnostic')),
|
||
CONSTRAINT chk_example_run_terminal CHECK (terminal_state IN ('running','completed','failed')),
|
||
CONSTRAINT chk_example_run_finish_order CHECK (finished_at IS NULL OR finished_at >= started_at),
|
||
-- 终态闭合:终态必须有结束时间;running 不得带结束时间(看板不显示互相矛盾的运行事实)
|
||
CONSTRAINT chk_example_run_terminal_needs_finish
|
||
CHECK (terminal_state NOT IN ('completed','failed') OR finished_at IS NOT NULL),
|
||
CONSTRAINT chk_example_run_running_no_finish
|
||
CHECK (terminal_state <> 'running' OR finished_at IS NULL)
|
||
);
|
||
CREATE INDEX IF NOT EXISTS idx_example_run_work ON example_run(tenant_id, work_id, started_at DESC);
|
||
CREATE OR REPLACE TRIGGER trg_example_run_updated_at BEFORE UPDATE ON example_run FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||
|
||
-- 终态不得回退到 running(completed/failed 之间可修正,但不能退回进行中)
|
||
CREATE OR REPLACE FUNCTION reject_example_run_terminal_rollback()
|
||
RETURNS TRIGGER
|
||
LANGUAGE plpgsql
|
||
AS $$
|
||
BEGIN
|
||
IF OLD.terminal_state IN ('completed','failed') AND NEW.terminal_state NOT IN ('completed','failed') THEN
|
||
RAISE EXCEPTION '运行已到终态 %,不得回退到 %', OLD.terminal_state, NEW.terminal_state;
|
||
END IF;
|
||
RETURN NEW;
|
||
END;
|
||
$$;
|
||
|
||
CREATE OR REPLACE TRIGGER trg_example_run_no_terminal_rollback
|
||
BEFORE UPDATE ON example_run
|
||
FOR EACH ROW EXECUTE FUNCTION reject_example_run_terminal_rollback();
|
||
|
||
-- ══ example_run_receipt:运行身份回执(append-only;05 §3 绑定键 run_id+attempt+candidate_version+candidate_sha256)══
|
||
-- 身份沿用 CAS:revision 是"每个样本内"的单调号(不是按整次运行),故唯一键必须含 sample_id。
|
||
-- candidate_version/candidate_sha256 是数据列兼到候选表的连接键,不是回执自身身份;回执↔候选是四列 1:N。
|
||
-- revision/attempt/loop_seq 单调性由 CAS 写路径保证;DB 侧只做非负下界约束(完整链校验暂缓,见落库设计 §2.2)。
|
||
CREATE TABLE IF NOT EXISTS example_run_receipt (
|
||
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||
run_id VARCHAR(64) NOT NULL, -- → example_run.run_id(软引用)
|
||
sample_id VARCHAR(64) NOT NULL, -- CAS 样本标识(生产侧也造运行内标识,不得空)
|
||
revision INTEGER NOT NULL, -- CAS 样本内单调号(>= 1)
|
||
arm VARCHAR(32), -- 实验臂
|
||
attempt INTEGER NOT NULL DEFAULT 1, -- 失败重试次数(与补证/重写的 loop_seq 分工)
|
||
adapter_role VARCHAR(32), -- writer/planner/detector/judge…
|
||
stage_kind VARCHAR(20) NOT NULL DEFAULT 'generation', -- generation/detection/supplement/rewrite/accept
|
||
loop_seq INTEGER NOT NULL DEFAULT 0, -- 补证/重写循环序号(05 §2 的 ≤3/≤2 合同靠它计数)
|
||
candidate_version VARCHAR(32), -- 候选版本(候选连接键)
|
||
candidate_sha256 CHAR(64), -- 正文哈希(裸 64 hex;候选连接键,非回执身份)
|
||
context_sha256 CHAR(64), -- → example_context_freeze.context_sha256(冻结上下文,注意不是 manifest_sha256)
|
||
previous_state_sha256 CHAR(64), -- CAS 哈希链上一环
|
||
requested_model_id VARCHAR(64),
|
||
actual_model_id VARCHAR(64),
|
||
model_match BOOLEAN,
|
||
effort VARCHAR(16),
|
||
total_cost_usd NUMERIC(14,8),
|
||
usage JSONB, -- token 用量
|
||
stop_reason VARCHAR(32),
|
||
terminal_reason VARCHAR(32),
|
||
is_error BOOLEAN NOT NULL DEFAULT FALSE,
|
||
safe_summary JSONB, -- 检测/质量结论 + 失败类别的安全摘要(完整 prompt/response 落 raw)
|
||
result_sha256 CHAR(64),
|
||
raw_content_id BIGINT, -- → example_raw_content.id
|
||
creator VARCHAR(64) NOT NULL DEFAULT '',
|
||
create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
tenant_id BIGINT NOT NULL DEFAULT 0,
|
||
CONSTRAINT uk_example_run_receipt UNIQUE (tenant_id, run_id, sample_id, revision),
|
||
CONSTRAINT chk_example_run_receipt_counters
|
||
CHECK (revision >= 1 AND attempt >= 1 AND loop_seq >= 0),
|
||
CONSTRAINT chk_example_run_receipt_stage
|
||
CHECK (stage_kind IN ('generation','detection','supplement','rewrite','accept')),
|
||
CONSTRAINT chk_example_run_receipt_candidate_sha256
|
||
CHECK (candidate_sha256 IS NULL OR candidate_sha256 ~ '^[0-9a-f]{64}$'),
|
||
CONSTRAINT chk_example_run_receipt_context_sha256
|
||
CHECK (context_sha256 IS NULL OR context_sha256 ~ '^[0-9a-f]{64}$'),
|
||
CONSTRAINT chk_example_run_receipt_prev_sha256
|
||
CHECK (previous_state_sha256 IS NULL OR previous_state_sha256 ~ '^[0-9a-f]{64}$'),
|
||
CONSTRAINT chk_example_run_receipt_result_sha256
|
||
CHECK (result_sha256 IS NULL OR result_sha256 ~ '^[0-9a-f]{64}$')
|
||
);
|
||
CREATE INDEX IF NOT EXISTS idx_example_run_receipt_run ON example_run_receipt(tenant_id, run_id);
|
||
CREATE INDEX IF NOT EXISTS idx_example_run_receipt_candidate ON example_run_receipt(tenant_id, candidate_sha256) WHERE candidate_sha256 IS NOT NULL;
|
||
CREATE INDEX IF NOT EXISTS idx_example_run_receipt_context ON example_run_receipt(context_sha256) WHERE context_sha256 IS NOT NULL;
|
||
|
||
CREATE OR REPLACE FUNCTION example_reject_truncate()
|
||
RETURNS TRIGGER
|
||
LANGUAGE plpgsql
|
||
AS $$
|
||
BEGIN
|
||
RAISE EXCEPTION '% 是 append-only 表,禁止 TRUNCATE', TG_TABLE_NAME;
|
||
END;
|
||
$$;
|
||
|
||
CREATE OR REPLACE FUNCTION reject_example_run_receipt_mutation()
|
||
RETURNS TRIGGER
|
||
LANGUAGE plpgsql
|
||
AS $$
|
||
BEGIN
|
||
RAISE EXCEPTION 'example_run_receipt 是 append-only 表,禁止 UPDATE/DELETE';
|
||
END;
|
||
$$;
|
||
|
||
CREATE OR REPLACE TRIGGER trg_example_run_receipt_append_only
|
||
BEFORE UPDATE OR DELETE ON example_run_receipt
|
||
FOR EACH ROW EXECUTE FUNCTION reject_example_run_receipt_mutation();
|
||
|
||
CREATE OR REPLACE TRIGGER trg_example_run_receipt_no_truncate
|
||
BEFORE TRUNCATE ON example_run_receipt
|
||
FOR EACH STATEMENT EXECUTE FUNCTION example_reject_truncate();
|
||
|
||
-- ══ example_quality_result:质量评判账本(append-only;有评判就记录、就可见,06 §3/§5)══
|
||
-- 本期:每次检测/评分/审核/实验落一条,看板运行详情逐条可见;跨运行聚合视图二期(06 §9)。
|
||
CREATE TABLE IF NOT EXISTS example_quality_result (
|
||
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||
run_id VARCHAR(64) NOT NULL, -- → example_run.run_id(评判必属一次运行)
|
||
receipt_id BIGINT, -- → example_run_receipt.id(软引用)
|
||
candidate_sha256 CHAR(64), -- 绑候选(06 §5;书级评判可空)
|
||
judge_kind VARCHAR(20) NOT NULL, -- detection/scoring/review/experiment
|
||
dimension VARCHAR(32), -- 评分维度(评分类用,检测类空)
|
||
scale_version VARCHAR(32), -- 量表/质量政策版本(06 §3 量表版本化)
|
||
score NUMERIC, -- 分值(非评分类空)
|
||
conclusion VARCHAR(20), -- pass/fail/revise…
|
||
failure_class VARCHAR(32), -- 失败类别
|
||
detail JSONB, -- 审核的问题/修复/复验;实验的基线/假设/干预/预期/结论
|
||
raw_content_id BIGINT, -- → example_raw_content.id
|
||
creator VARCHAR(64) NOT NULL DEFAULT '',
|
||
create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
tenant_id BIGINT NOT NULL DEFAULT 0,
|
||
CONSTRAINT chk_example_quality_result_kind
|
||
CHECK (judge_kind IN ('detection','scoring','review','experiment')),
|
||
CONSTRAINT chk_example_quality_result_candidate_sha256
|
||
CHECK (candidate_sha256 IS NULL OR candidate_sha256 ~ '^[0-9a-f]{64}$')
|
||
);
|
||
-- 幂等唯一键用表达式索引(dimension/candidate_sha256 可空,UNIQUE 约束不能含表达式,故用唯一索引 + COALESCE)
|
||
CREATE UNIQUE INDEX IF NOT EXISTS uk_example_quality_result
|
||
ON example_quality_result(tenant_id, run_id, judge_kind, COALESCE(dimension, ''), COALESCE(candidate_sha256, ''));
|
||
CREATE INDEX IF NOT EXISTS idx_example_quality_result_candidate ON example_quality_result(candidate_sha256) WHERE candidate_sha256 IS NOT NULL;
|
||
CREATE INDEX IF NOT EXISTS idx_example_quality_result_run ON example_quality_result(run_id);
|
||
CREATE INDEX IF NOT EXISTS idx_example_quality_result_kind ON example_quality_result(judge_kind);
|
||
|
||
CREATE OR REPLACE FUNCTION reject_example_quality_result_mutation()
|
||
RETURNS TRIGGER
|
||
LANGUAGE plpgsql
|
||
AS $$
|
||
BEGIN
|
||
RAISE EXCEPTION 'example_quality_result 是 append-only 表,禁止 UPDATE/DELETE';
|
||
END;
|
||
$$;
|
||
|
||
CREATE OR REPLACE TRIGGER trg_example_quality_result_append_only
|
||
BEFORE UPDATE OR DELETE ON example_quality_result
|
||
FOR EACH ROW EXECUTE FUNCTION reject_example_quality_result_mutation();
|
||
|
||
CREATE OR REPLACE TRIGGER trg_example_quality_result_no_truncate
|
||
BEFORE TRUNCATE ON example_quality_result
|
||
FOR EACH STATEMENT EXECUTE FUNCTION example_reject_truncate();
|