feat(db): add document revision ledger schema 00028
Introduce immutable blobs/snapshots, versions, events, candidates, change reports, and ledger_* root pointers; rename freeform current_version_id to legacy_version_id. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
-- +goose Up
|
||||
-- Document revision ledger: immutable snapshots, Version/Revision, agent candidates, change reports.
|
||||
-- Pointer columns use ledger_*/head_* to avoid colliding with freeform_documents.current_version_id.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS document_content_blobs (
|
||||
content_sha256 CHAR(64) PRIMARY KEY,
|
||||
content_markdown TEXT NOT NULL,
|
||||
byte_size BIGINT NOT NULL CHECK (byte_size >= 0),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS document_snapshots (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
doc_id TEXT NOT NULL,
|
||||
content_sha256 CHAR(64) NOT NULL REFERENCES document_content_blobs(content_sha256),
|
||||
parent_snapshot_id BIGINT REFERENCES document_snapshots(id),
|
||||
block_map JSONB NOT NULL DEFAULT '[]'::jsonb,
|
||||
source_kind TEXT NOT NULL,
|
||||
actor_user_id BIGINT,
|
||||
actor_ref TEXT NOT NULL DEFAULT '',
|
||||
actor_snapshot JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
source_run_id TEXT NOT NULL DEFAULT '',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
CONSTRAINT chk_snapshot_source_kind CHECK (source_kind IN ('template','human','agent','agent_squad','migration','system'))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_document_snapshots_doc ON document_snapshots(doc_id, created_at);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS document_versions (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
doc_id TEXT NOT NULL,
|
||||
version_no INT NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
origin_kind TEXT NOT NULL,
|
||||
baseline_snapshot_id BIGINT NOT NULL REFERENCES document_snapshots(id),
|
||||
head_snapshot_id BIGINT NOT NULL REFERENCES document_snapshots(id),
|
||||
human_revision_count INT NOT NULL DEFAULT 0,
|
||||
source_run_id TEXT NOT NULL DEFAULT '',
|
||||
change_report_id BIGINT,
|
||||
opened_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
sealed_at TIMESTAMPTZ,
|
||||
CONSTRAINT uq_document_versions_doc_no UNIQUE (doc_id, version_no),
|
||||
CONSTRAINT chk_document_versions_status CHECK (status IN ('open','sealed')),
|
||||
CONSTRAINT chk_document_versions_origin CHECK (origin_kind IN ('template','agent','agent_squad','migration'))
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_document_versions_one_open
|
||||
ON document_versions(doc_id) WHERE status = 'open';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS document_change_events (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
doc_id TEXT NOT NULL,
|
||||
version_id BIGINT NOT NULL REFERENCES document_versions(id),
|
||||
event_no INT NOT NULL,
|
||||
from_snapshot_id BIGINT REFERENCES document_snapshots(id),
|
||||
to_snapshot_id BIGINT NOT NULL REFERENCES document_snapshots(id),
|
||||
change_kind TEXT NOT NULL,
|
||||
actor_kind TEXT NOT NULL,
|
||||
actor_user_id BIGINT,
|
||||
actor_ref TEXT NOT NULL DEFAULT '',
|
||||
actor_snapshot JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
change_set JSONB NOT NULL DEFAULT '[]'::jsonb,
|
||||
edit_session_id TEXT NOT NULL DEFAULT '',
|
||||
request_id TEXT NOT NULL DEFAULT '',
|
||||
idempotency_key TEXT NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
CONSTRAINT uq_document_change_events_doc_event UNIQUE (doc_id, event_no),
|
||||
CONSTRAINT uq_document_change_events_idemp UNIQUE (doc_id, idempotency_key),
|
||||
CONSTRAINT chk_change_kind CHECK (change_kind IN ('human_revision','agent_commit','agent_noop','import','restore_as_new')),
|
||||
CONSTRAINT chk_actor_kind CHECK (actor_kind IN ('human','agent','agent_squad','template','migration','system'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS document_change_reports (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
doc_id TEXT NOT NULL,
|
||||
from_snapshot_id BIGINT NOT NULL REFERENCES document_snapshots(id),
|
||||
to_snapshot_id BIGINT NOT NULL REFERENCES document_snapshots(id),
|
||||
schema_version INT NOT NULL DEFAULT 1,
|
||||
summary JSONB NOT NULL DEFAULT '[]'::jsonb,
|
||||
risk_level TEXT NOT NULL,
|
||||
risk_items JSONB NOT NULL DEFAULT '[]'::jsonb,
|
||||
project_impacts JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
document_impacts JSONB NOT NULL DEFAULT '[]'::jsonb,
|
||||
open_questions JSONB NOT NULL DEFAULT '[]'::jsonb,
|
||||
analysis_status TEXT NOT NULL,
|
||||
model_snapshot TEXT NOT NULL DEFAULT '',
|
||||
prompt_version TEXT NOT NULL DEFAULT '',
|
||||
deterministic_facts JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
corrects_report_id BIGINT REFERENCES document_change_reports(id),
|
||||
raw_json TEXT NOT NULL DEFAULT '',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
CONSTRAINT chk_report_risk CHECK (risk_level IN ('low','medium','high','critical')),
|
||||
CONSTRAINT chk_report_analysis CHECK (analysis_status IN ('complete','fallback','unassessed','corrected'))
|
||||
);
|
||||
|
||||
ALTER TABLE document_versions
|
||||
ADD CONSTRAINT fk_document_versions_change_report
|
||||
FOREIGN KEY (change_report_id) REFERENCES document_change_reports(id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS document_agent_candidates (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
doc_id TEXT NOT NULL,
|
||||
input_snapshot_id BIGINT NOT NULL REFERENCES document_snapshots(id),
|
||||
output_snapshot_id BIGINT REFERENCES document_snapshots(id),
|
||||
run_id TEXT NOT NULL,
|
||||
proposal_id TEXT NOT NULL DEFAULT '',
|
||||
status TEXT NOT NULL,
|
||||
authorized_scope TEXT NOT NULL DEFAULT '',
|
||||
quality_result TEXT NOT NULL DEFAULT '',
|
||||
change_report_id BIGINT REFERENCES document_change_reports(id),
|
||||
actor_kind TEXT NOT NULL,
|
||||
actor_snapshot JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
decided_at TIMESTAMPTZ,
|
||||
CONSTRAINT uq_document_agent_candidates_run UNIQUE (doc_id, run_id),
|
||||
CONSTRAINT chk_candidate_status CHECK (status IN ('ready','needs_confirmation','committed','rejected','stale','quality_failed')),
|
||||
CONSTRAINT chk_candidate_actor CHECK (actor_kind IN ('agent','agent_squad'))
|
||||
);
|
||||
|
||||
ALTER TABLE document_records
|
||||
ADD COLUMN IF NOT EXISTS ledger_version_id BIGINT REFERENCES document_versions(id),
|
||||
ADD COLUMN IF NOT EXISTS head_snapshot_id BIGINT REFERENCES document_snapshots(id),
|
||||
ADD COLUMN IF NOT EXISTS head_revision_no INT NOT NULL DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS review_snapshot_id BIGINT REFERENCES document_snapshots(id),
|
||||
ADD COLUMN IF NOT EXISTS revision_schema_version INT NOT NULL DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS row_revision BIGINT NOT NULL DEFAULT 0;
|
||||
|
||||
-- Rename freeform legacy pointer to avoid semantic collision with ledger version id.
|
||||
ALTER TABLE freeform_documents
|
||||
RENAME COLUMN current_version_id TO legacy_version_id;
|
||||
|
||||
ALTER TABLE freeform_documents
|
||||
ADD COLUMN IF NOT EXISTS ledger_version_id BIGINT REFERENCES document_versions(id),
|
||||
ADD COLUMN IF NOT EXISTS head_snapshot_id BIGINT REFERENCES document_snapshots(id),
|
||||
ADD COLUMN IF NOT EXISTS head_revision_no INT NOT NULL DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS revision_schema_version INT NOT NULL DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS row_revision BIGINT NOT NULL DEFAULT 0;
|
||||
|
||||
-- Prevent UPDATE/DELETE on immutable snapshot bodies via trigger.
|
||||
-- +goose StatementBegin
|
||||
CREATE OR REPLACE FUNCTION document_snapshots_immutable() RETURNS trigger AS $$
|
||||
BEGIN
|
||||
RAISE EXCEPTION 'document_snapshots is immutable';
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
-- +goose StatementEnd
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_document_snapshots_immutable ON document_snapshots;
|
||||
CREATE TRIGGER trg_document_snapshots_immutable
|
||||
BEFORE UPDATE OR DELETE ON document_snapshots
|
||||
FOR EACH ROW EXECUTE FUNCTION document_snapshots_immutable();
|
||||
|
||||
-- +goose StatementBegin
|
||||
CREATE OR REPLACE FUNCTION document_content_blobs_immutable() RETURNS trigger AS $$
|
||||
BEGIN
|
||||
RAISE EXCEPTION 'document_content_blobs is immutable';
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
-- +goose StatementEnd
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_document_content_blobs_immutable ON document_content_blobs;
|
||||
CREATE TRIGGER trg_document_content_blobs_immutable
|
||||
BEFORE UPDATE OR DELETE ON document_content_blobs
|
||||
FOR EACH ROW EXECUTE FUNCTION document_content_blobs_immutable();
|
||||
|
||||
-- +goose Down
|
||||
DROP TRIGGER IF EXISTS trg_document_content_blobs_immutable ON document_content_blobs;
|
||||
DROP FUNCTION IF EXISTS document_content_blobs_immutable();
|
||||
DROP TRIGGER IF EXISTS trg_document_snapshots_immutable ON document_snapshots;
|
||||
DROP FUNCTION IF EXISTS document_snapshots_immutable();
|
||||
|
||||
ALTER TABLE freeform_documents
|
||||
DROP COLUMN IF EXISTS row_revision,
|
||||
DROP COLUMN IF EXISTS revision_schema_version,
|
||||
DROP COLUMN IF EXISTS head_revision_no,
|
||||
DROP COLUMN IF EXISTS head_snapshot_id,
|
||||
DROP COLUMN IF EXISTS ledger_version_id;
|
||||
|
||||
ALTER TABLE freeform_documents
|
||||
RENAME COLUMN legacy_version_id TO current_version_id;
|
||||
|
||||
ALTER TABLE document_records
|
||||
DROP COLUMN IF EXISTS row_revision,
|
||||
DROP COLUMN IF EXISTS revision_schema_version,
|
||||
DROP COLUMN IF EXISTS review_snapshot_id,
|
||||
DROP COLUMN IF EXISTS head_revision_no,
|
||||
DROP COLUMN IF EXISTS head_snapshot_id,
|
||||
DROP COLUMN IF EXISTS ledger_version_id;
|
||||
|
||||
ALTER TABLE document_versions DROP CONSTRAINT IF EXISTS fk_document_versions_change_report;
|
||||
|
||||
DROP TABLE IF EXISTS document_agent_candidates;
|
||||
DROP TABLE IF EXISTS document_change_reports;
|
||||
DROP TABLE IF EXISTS document_change_events;
|
||||
DROP TABLE IF EXISTS document_versions;
|
||||
DROP TABLE IF EXISTS document_snapshots;
|
||||
DROP TABLE IF EXISTS document_content_blobs;
|
||||
Reference in New Issue
Block a user