正式台账 doc_proj* 上,旧键与发布键不能再并存。孤立旧键和 doc_test_db 测试残留不动,项目 143 无重复行所以不会被改写。 Co-authored-by: Cursor <cursoragent@cursor.com>
177 lines
7.1 KiB
PL/PgSQL
177 lines
7.1 KiB
PL/PgSQL
-- +goose Up
|
|
-- 正式台账 doc_proj* 上,旧任务键与发布键(intake 与 bd_intake)不能再并存。
|
|
-- 只归档「同一项目里已经有发布键行」的旧键行;孤立旧键行保留,避免晋升时丢掉唯一数据。
|
|
-- 唯一索引不覆盖 doc_test_db_*:dev 库项目 1 有大量同键测试残留,全表唯一会误伤。
|
|
-- document_snapshots / document_content_blobs 有不可变触发器,不删除;正文副本写入归档 payload。
|
|
|
|
-- +goose StatementBegin
|
|
CREATE OR REPLACE FUNCTION canonical_document_task_key(task_key text)
|
|
RETURNS text
|
|
LANGUAGE sql
|
|
IMMUTABLE
|
|
PARALLEL SAFE
|
|
AS $$
|
|
SELECT CASE btrim(COALESCE(task_key, ''))
|
|
WHEN 'intake' THEN 'bd_intake'
|
|
WHEN 'research' THEN 'bd_research'
|
|
WHEN 'feasibility_dev' THEN 'bd_feasibility_dev'
|
|
WHEN 'feasibility_reg' THEN 'bd_feasibility_reg'
|
|
WHEN 'user_requirements' THEN 'bd_user_requirements'
|
|
WHEN 'prelim_design' THEN 'bd_prelim_design'
|
|
WHEN 'cdo_quote' THEN 'bd_cdo_quote'
|
|
WHEN 'system_reqs' THEN 'pm_system_reqs'
|
|
WHEN 'test_manager' THEN 'tm_system_verification'
|
|
WHEN 'software_charter' THEN 'sw_software_charter'
|
|
WHEN 'source_inventory' THEN 'sw_source_inventory'
|
|
WHEN 'current_state_audit' THEN 'sw_current_state_audit'
|
|
WHEN 'architecture_baseline' THEN 'sw_architecture_baseline'
|
|
WHEN 'software_risk_register' THEN 'sw_software_risk_register'
|
|
WHEN 'product_requirements' THEN 'sw_product_requirements'
|
|
WHEN 'scope_backlog' THEN 'sw_scope_backlog'
|
|
WHEN 'target_architecture' THEN 'sw_target_architecture'
|
|
WHEN 'decision_log' THEN 'sw_decision_log'
|
|
WHEN 'bootstrap_roadmap' THEN 'sw_bootstrap_roadmap'
|
|
WHEN 'verification_strategy' THEN 'sw_verification_strategy'
|
|
WHEN 'operations_baseline' THEN 'sw_operations_baseline'
|
|
WHEN 'bootstrap_readiness' THEN 'sw_bootstrap_readiness'
|
|
WHEN 'change_brief' THEN 'sw_change_brief'
|
|
WHEN 'implementation_plan' THEN 'sw_implementation_plan'
|
|
WHEN 'change_manifest' THEN 'sw_change_manifest'
|
|
WHEN 'adr_delta' THEN 'sw_adr_delta'
|
|
WHEN 'test_report' THEN 'sw_test_report'
|
|
WHEN 'security_review' THEN 'sw_security_review'
|
|
WHEN 'release_plan' THEN 'sw_release_plan'
|
|
WHEN 'release_notes' THEN 'sw_release_notes'
|
|
WHEN 'retrospective' THEN 'sw_retrospective'
|
|
WHEN 'backlog_delta' THEN 'sw_backlog_delta'
|
|
ELSE btrim(COALESCE(task_key, ''))
|
|
END;
|
|
$$;
|
|
-- +goose StatementEnd
|
|
|
|
CREATE TABLE document_records_legacy_key_archive (LIKE document_records INCLUDING DEFAULTS);
|
|
ALTER TABLE document_records_legacy_key_archive ALTER COLUMN id DROP DEFAULT;
|
|
ALTER TABLE document_records_legacy_key_archive
|
|
ADD COLUMN archive_id BIGSERIAL PRIMARY KEY,
|
|
ADD COLUMN archived_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
ADD COLUMN archive_reason TEXT NOT NULL DEFAULT '',
|
|
ADD COLUMN related_payload JSONB NOT NULL DEFAULT '{}'::jsonb;
|
|
|
|
CREATE TABLE document_content_projections_legacy_key_archive (LIKE document_content_projections INCLUDING DEFAULTS);
|
|
ALTER TABLE document_content_projections_legacy_key_archive
|
|
ADD COLUMN archive_id BIGSERIAL PRIMARY KEY,
|
|
ADD COLUMN archived_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
ADD COLUMN archive_reason TEXT NOT NULL DEFAULT '';
|
|
|
|
CREATE TEMP TABLE legacy_key_docs_to_archive ON COMMIT DROP AS
|
|
SELECT legacy.doc_id
|
|
FROM document_records legacy
|
|
JOIN document_records canon
|
|
ON canon.project_id = legacy.project_id
|
|
AND canon.doc_id <> legacy.doc_id
|
|
AND canon.doc_id LIKE 'doc_proj%'
|
|
AND legacy.doc_id LIKE 'doc_proj%'
|
|
AND canonical_document_task_key(canon.task_key) = canonical_document_task_key(legacy.task_key)
|
|
AND canon.task_key = canonical_document_task_key(canon.task_key)
|
|
AND legacy.task_key <> canonical_document_task_key(legacy.task_key);
|
|
|
|
INSERT INTO document_records_legacy_key_archive
|
|
SELECT d.*,
|
|
nextval('document_records_legacy_key_archive_archive_id_seq'),
|
|
now(),
|
|
'coexisting_legacy_task_key',
|
|
'{}'::jsonb
|
|
FROM document_records d
|
|
JOIN legacy_key_docs_to_archive x ON x.doc_id = d.doc_id;
|
|
|
|
UPDATE document_records_legacy_key_archive a
|
|
SET related_payload = jsonb_build_object(
|
|
'comments', COALESCE((
|
|
SELECT jsonb_agg(to_jsonb(c)) FROM document_comments c WHERE c.doc_id = a.doc_id
|
|
), '[]'::jsonb),
|
|
'change_proposals', COALESCE((
|
|
SELECT jsonb_agg(to_jsonb(p)) FROM document_change_proposals p WHERE p.doc_id = a.doc_id
|
|
), '[]'::jsonb),
|
|
'audit_logs', COALESCE((
|
|
SELECT jsonb_agg(to_jsonb(l)) FROM document_audit_logs l WHERE l.doc_id = a.doc_id
|
|
), '[]'::jsonb),
|
|
'versions', COALESCE((
|
|
SELECT jsonb_agg(to_jsonb(v)) FROM document_versions v WHERE v.doc_id = a.doc_id
|
|
), '[]'::jsonb),
|
|
'snapshots', COALESCE((
|
|
SELECT jsonb_agg(jsonb_build_object(
|
|
'id', s.id,
|
|
'content_sha256', s.content_sha256,
|
|
'source_kind', s.source_kind,
|
|
'created_at', s.created_at,
|
|
'markdown', b.content_markdown
|
|
))
|
|
FROM document_snapshots s
|
|
JOIN document_content_blobs b ON b.content_sha256 = s.content_sha256
|
|
WHERE s.doc_id = a.doc_id
|
|
), '[]'::jsonb)
|
|
);
|
|
|
|
INSERT INTO document_content_projections_legacy_key_archive
|
|
SELECT p.*,
|
|
nextval('document_content_projections_legacy_key_archive_archive_id_seq'),
|
|
now(),
|
|
'coexisting_legacy_task_key'
|
|
FROM document_content_projections p
|
|
JOIN legacy_key_docs_to_archive x ON x.doc_id = p.doc_id;
|
|
|
|
DELETE FROM document_content_projections p
|
|
USING legacy_key_docs_to_archive x
|
|
WHERE p.doc_id = x.doc_id;
|
|
|
|
DELETE FROM document_records d
|
|
USING legacy_key_docs_to_archive x
|
|
WHERE d.doc_id = x.doc_id;
|
|
|
|
-- +goose StatementBegin
|
|
DO $$
|
|
DECLARE
|
|
dup text;
|
|
BEGIN
|
|
SELECT string_agg(project_id::text || ':' || canon, ', ')
|
|
INTO dup
|
|
FROM (
|
|
SELECT project_id, canonical_document_task_key(task_key) AS canon
|
|
FROM document_records
|
|
WHERE btrim(task_key) <> '' AND doc_id LIKE 'doc_proj%'
|
|
GROUP BY project_id, canonical_document_task_key(task_key)
|
|
HAVING count(*) > 1
|
|
) s;
|
|
IF dup IS NOT NULL THEN
|
|
RAISE EXCEPTION 'doc_proj canonical task key still duplicated: %', dup;
|
|
END IF;
|
|
END $$;
|
|
-- +goose StatementEnd
|
|
|
|
CREATE UNIQUE INDEX uq_document_records_doc_proj_canonical_task_key
|
|
ON document_records (project_id, canonical_document_task_key(task_key))
|
|
WHERE btrim(task_key) <> '' AND doc_id LIKE 'doc_proj%';
|
|
|
|
-- +goose Down
|
|
DROP INDEX IF EXISTS uq_document_records_doc_proj_canonical_task_key;
|
|
|
|
INSERT INTO document_records
|
|
SELECT (jsonb_populate_record(
|
|
NULL::document_records,
|
|
to_jsonb(a) - 'archive_id' - 'archived_at' - 'archive_reason' - 'related_payload'
|
|
)).*
|
|
FROM document_records_legacy_key_archive a
|
|
ON CONFLICT (doc_id) DO NOTHING;
|
|
|
|
INSERT INTO document_content_projections
|
|
SELECT (jsonb_populate_record(
|
|
NULL::document_content_projections,
|
|
to_jsonb(a) - 'archive_id' - 'archived_at' - 'archive_reason'
|
|
)).*
|
|
FROM document_content_projections_legacy_key_archive a
|
|
ON CONFLICT (doc_id, snapshot_id) DO NOTHING;
|
|
|
|
DROP TABLE IF EXISTS document_content_projections_legacy_key_archive;
|
|
DROP TABLE IF EXISTS document_records_legacy_key_archive;
|
|
DROP FUNCTION IF EXISTS canonical_document_task_key(text);
|