Files
story/migrations/000077_memoirs.up.sql
T

121 lines
7.7 KiB
PL/PgSQL
Raw Normal View History

BEGIN;
CREATE TABLE memoir_chat_import (
id uuid PRIMARY KEY, tenant_id uuid NOT NULL REFERENCES tenant(id),
subject_id uuid NOT NULL REFERENCES subject_profile(id), created_by uuid NOT NULL REFERENCES user_account(id),
title text NOT NULL, transcript_digest text NOT NULL, created_at timestamptz NOT NULL DEFAULT now(),
status text NOT NULL DEFAULT 'PENDING' CHECK(status IN ('PENDING','READY','FAILED'))
);
CREATE TABLE memoir_import_turn (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(), import_id uuid NOT NULL REFERENCES memoir_chat_import(id),
sequence_no integer NOT NULL, speaker text NOT NULL CHECK(speaker IN ('subject','agent')),
content text NOT NULL, UNIQUE(import_id,sequence_no)
);
-- One call may contain several independent events. Keep the original story
-- origin for navigation, and carry every contributing source on revisions.
ALTER TABLE story_draft ADD COLUMN event_index integer NOT NULL DEFAULT 0 CHECK(event_index>=0);
ALTER TABLE story_draft ADD COLUMN event_time text NOT NULL DEFAULT '';
ALTER TABLE story_draft ADD COLUMN import_id uuid REFERENCES memoir_chat_import(id);
ALTER TABLE story_draft DROP CONSTRAINT story_origin_check;
ALTER TABLE story_draft ADD CONSTRAINT story_origin_check CHECK(
num_nonnulls(conversation_id,call_session_id,import_id)=1 AND
((call_session_id IS NOT NULL AND call_privacy_epoch IS NOT NULL) OR (call_session_id IS NULL AND call_privacy_epoch IS NULL)));
ALTER TABLE story_source ADD COLUMN import_turn_id uuid REFERENCES memoir_import_turn(id);
ALTER TABLE story_source DROP CONSTRAINT story_source_origin_check;
ALTER TABLE story_source ADD CONSTRAINT story_source_origin_check CHECK(
num_nonnulls(conversation_turn_id,call_session_id,import_turn_id)=1 AND
((call_session_id IS NOT NULL AND call_utterance_id IS NOT NULL AND call_source_revision IS NOT NULL) OR
(call_session_id IS NULL AND call_utterance_id IS NULL AND call_source_revision IS NULL)));
ALTER TABLE story_source ADD CONSTRAINT story_import_source_key UNIQUE(story_revision_id,import_turn_id);
ALTER TABLE story_draft DROP CONSTRAINT story_draft_generation_key;
ALTER TABLE story_draft DROP CONSTRAINT story_call_generation_key;
ALTER TABLE story_draft ADD CONSTRAINT story_draft_generation_key UNIQUE(conversation_id,generation_version,event_index);
ALTER TABLE story_draft ADD CONSTRAINT story_call_generation_key UNIQUE(call_session_id,generation_version,event_index);
ALTER TABLE story_draft ADD CONSTRAINT story_import_generation_key UNIQUE(import_id,generation_version,event_index);
CREATE TABLE chat_daily_event (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id uuid NOT NULL REFERENCES tenant(id),
subject_id uuid NOT NULL REFERENCES subject_profile(id),
conversation_id uuid REFERENCES conversation(id),
call_session_id uuid REFERENCES call_session(id),
import_id uuid REFERENCES memoir_chat_import(id),
generation_version bigint NOT NULL,
event_index integer NOT NULL,
event_date date NOT NULL,
title text NOT NULL,
content text NOT NULL,
source_refs jsonb NOT NULL,
model_ref text NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
CHECK(num_nonnulls(conversation_id,call_session_id,import_id)=1),
UNIQUE(conversation_id,generation_version,event_index),
UNIQUE(call_session_id,generation_version,event_index),
UNIQUE(import_id,generation_version,event_index)
);
CREATE TABLE story_generation_event (
origin_id uuid NOT NULL, generation_version bigint NOT NULL,
event_index integer NOT NULL, story_id uuid NOT NULL REFERENCES story_draft(id),
PRIMARY KEY(origin_id,generation_version,event_index)
);
CREATE TABLE memoir_document (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id uuid NOT NULL REFERENCES tenant(id),
subject_id uuid NOT NULL REFERENCES subject_profile(id),
version bigint NOT NULL DEFAULT 1,
source_digest text NOT NULL,
document jsonb NOT NULL,
model_ref text NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
UNIQUE(subject_id,version)
);
CREATE INDEX memoir_document_subject_idx ON memoir_document(subject_id,version DESC);
CREATE INDEX chat_daily_event_subject_idx ON chat_daily_event(subject_id,event_date DESC,created_at DESC);
CREATE TABLE memoir_generation_job (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(), subject_id uuid NOT NULL REFERENCES subject_profile(id),
tenant_id uuid NOT NULL REFERENCES tenant(id), actor_id uuid NOT NULL REFERENCES user_account(id),
source_digest text NOT NULL, state text NOT NULL DEFAULT 'PENDING' CHECK(state IN ('PENDING','RUNNING','READY','FAILED')),
error_code text NOT NULL DEFAULT '', created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE UNIQUE INDEX memoir_generation_active ON memoir_generation_job(subject_id) WHERE state IN ('PENDING','RUNNING');
CREATE TABLE memoir_illustration (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(), document_id uuid NOT NULL REFERENCES memoir_document(id) ON DELETE CASCADE,
chapter_index integer NOT NULL, content bytea NOT NULL, content_type text NOT NULL DEFAULT 'image/jpeg',
UNIQUE(document_id,chapter_index)
);
-- Every source in a merged story must remain valid, including other calls.
CREATE OR REPLACE FUNCTION call_story_available(story uuid) RETURNS boolean LANGUAGE sql STABLE AS $$
SELECT EXISTS(SELECT 1 FROM story_draft sd JOIN subject_profile sp ON sp.id=sd.subject_id WHERE sd.id=story AND sp.status='active'
AND NOT EXISTS(SELECT 1 FROM privacy_request pr WHERE pr.subject_id=sd.subject_id AND pr.request_type='DELETE')
AND EXISTS(SELECT 1 FROM story_source ss WHERE ss.story_revision_id=sd.current_revision_id)
AND NOT EXISTS(SELECT 1 FROM story_source ss
LEFT JOIN call_utterance u ON u.session_id=ss.call_session_id AND u.utterance_id=ss.call_utterance_id
LEFT JOIN call_session c ON c.id=ss.call_session_id
LEFT JOIN conversation_turn ct ON ct.id=ss.conversation_turn_id
LEFT JOIN conversation cv ON cv.id=ct.conversation_id
LEFT JOIN memoir_import_turn it ON it.id=ss.import_turn_id
LEFT JOIN memoir_chat_import ci ON ci.id=it.import_id
WHERE ss.story_revision_id=sd.current_revision_id AND (
(ss.call_session_id IS NOT NULL AND (c.tenant_id IS DISTINCT FROM sd.tenant_id OR c.subject_id IS DISTINCT FROM sd.subject_id
OR NOT call_story_authorized(c.id) OR u.source_revision IS DISTINCT FROM ss.call_source_revision
OR u.speaker<>'USER' OR u.text_content='[deleted]' OR length(trim(u.text_content))=0))
OR (ss.conversation_turn_id IS NOT NULL AND (cv.tenant_id IS DISTINCT FROM sd.tenant_id OR cv.subject_id IS DISTINCT FROM sd.subject_id
OR ct.speaker<>'subject' OR ct.final_text='[deleted]' OR length(trim(ct.final_text))=0
OR NOT EXISTS(SELECT 1 FROM consent_record cr WHERE cr.subject_id=sd.subject_id AND cr.consent_type='story_extraction'
AND cr.withdrawn_at IS NULL AND (cr.expires_at IS NULL OR cr.expires_at>now()))))
OR (ss.import_turn_id IS NOT NULL AND (ci.tenant_id IS DISTINCT FROM sd.tenant_id OR ci.subject_id IS DISTINCT FROM sd.subject_id
OR it.speaker<>'subject' OR it.content='[deleted]' OR length(trim(it.content))=0
OR NOT EXISTS(SELECT 1 FROM consent_record cr WHERE cr.subject_id=sd.subject_id AND cr.consent_type='story_extraction'
AND cr.withdrawn_at IS NULL AND (cr.expires_at IS NULL OR cr.expires_at>now()))))
OR EXISTS(SELECT 1 FROM memory_source_suppression ms CROSS JOIN LATERAL jsonb_array_elements(ms.source_refs) r
WHERE ms.subject_id=sd.subject_id AND (r->>'story_id'=sd.public_id::text OR
(ss.call_session_id IS NOT NULL AND r->>'session_id'=ss.call_session_id::text
AND (r->>'utterance_id' IS NULL OR r->>'utterance_id'=ss.call_utterance_id)) OR
(ss.conversation_turn_id IS NOT NULL AND r->>'turn_id'=ct.public_id::text) OR
(ss.import_turn_id IS NOT NULL AND r->>'import_id'=ci.id::text AND (r->>'turn_id' IS NULL OR r->>'turn_id'=it.id::text))))))
)
$$;
COMMIT;