-- Sidelinja: Initial database schema -- Kunnskapsgraf, jobbkø, meldinger, mediefiler -- -- Kjøres mot en tom PostgreSQL-database. -- Krever: PostgreSQL 15+ med pgcrypto (gen_random_uuid) BEGIN; -- ============================================================ -- Extensions -- ============================================================ CREATE EXTENSION IF NOT EXISTS "pgcrypto"; -- gen_random_uuid() CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; -- uuid_generate_v5() for deterministiske segment-UUIDs -- ============================================================ -- 1. Brukere (tynn cache fra Authentik) -- ============================================================ CREATE TABLE users ( authentik_id TEXT PRIMARY KEY, display_name TEXT NOT NULL, avatar_url TEXT, cached_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -- ============================================================ -- 2. Nodes — supertabell for alle grafentiteter -- ============================================================ CREATE TYPE node_type AS ENUM ( 'tema', 'aktør', 'faktoide', 'episode', 'segment', 'melding' ); CREATE TABLE nodes ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), node_type node_type NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -- ============================================================ -- 3. Relasjonstyper (seeded + utvidbar) -- ============================================================ CREATE TABLE relation_types ( name TEXT PRIMARY KEY, label TEXT NOT NULL, description TEXT, system BOOLEAN NOT NULL DEFAULT false ); INSERT INTO relation_types (name, label, description, system) VALUES ('MENTIONS', 'Nevner', 'Refererer til denne aktøren/temaet', true), ('ABOUT', 'Handler om', 'Faktoide eller klipp som omhandler', true), ('DISCUSSED_IN', 'Diskutert i', 'Tema diskutert i dette segmentet', true), ('WORKS_FOR', 'Jobber for', 'Person tilknyttet organisasjon', true), ('CONTRADICTS', 'Motsier', 'Står i motsetning til', true), ('PART_OF', 'Del av', 'Tilhører / er underordnet', true); -- ============================================================ -- 4. Graph edges -- ============================================================ CREATE TABLE graph_edges ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), source_id UUID NOT NULL REFERENCES nodes(id) ON DELETE CASCADE, target_id UUID NOT NULL REFERENCES nodes(id) ON DELETE CASCADE, relation_type TEXT NOT NULL REFERENCES relation_types(name), context_id UUID REFERENCES nodes(id) ON DELETE SET NULL, confidence REAL CHECK (confidence BETWEEN 0.0 AND 1.0), created_by TEXT REFERENCES users(authentik_id) ON DELETE SET NULL, origin TEXT NOT NULL DEFAULT 'system', created_at TIMESTAMPTZ NOT NULL DEFAULT now(), CONSTRAINT no_self_reference CHECK (source_id != target_id) ); CREATE INDEX idx_edges_source ON graph_edges(source_id); CREATE INDEX idx_edges_target ON graph_edges(target_id); CREATE INDEX idx_edges_relation ON graph_edges(relation_type); -- ============================================================ -- 5. Detailtabeller — aktører, temaer, episoder, segmenter -- ============================================================ CREATE TABLE actors ( id UUID PRIMARY KEY REFERENCES nodes(id) ON DELETE CASCADE, name TEXT NOT NULL, type TEXT -- 'person', 'organisasjon', etc. ); CREATE TABLE topics ( id UUID PRIMARY KEY REFERENCES nodes(id) ON DELETE CASCADE, name TEXT NOT NULL ); CREATE TABLE episodes ( id UUID PRIMARY KEY REFERENCES nodes(id) ON DELETE CASCADE, title TEXT NOT NULL, published_at TIMESTAMPTZ, guid TEXT UNIQUE NOT NULL ); CREATE TABLE segments ( id UUID PRIMARY KEY REFERENCES nodes(id) ON DELETE CASCADE, episode_id UUID NOT NULL REFERENCES episodes(id) ON DELETE CASCADE, start_time INTERVAL NOT NULL, end_time INTERVAL NOT NULL, transcript TEXT, CONSTRAINT valid_timerange CHECK (end_time > start_time) ); CREATE INDEX idx_segments_episode ON segments(episode_id); CREATE INDEX idx_segments_fts ON segments USING GIN (to_tsvector('norwegian', transcript)); -- ============================================================ -- 6. Faktoider -- ============================================================ CREATE TABLE factoids ( id UUID PRIMARY KEY REFERENCES nodes(id) ON DELETE CASCADE, body TEXT NOT NULL, source_url TEXT, created_by TEXT NOT NULL REFERENCES users(authentik_id) ON DELETE SET NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE TABLE factoid_votes ( factoid_id UUID NOT NULL REFERENCES factoids(id) ON DELETE CASCADE, user_id TEXT NOT NULL REFERENCES users(authentik_id) ON DELETE CASCADE, vote SMALLINT NOT NULL CHECK (vote IN (-1, 1)), created_at TIMESTAMPTZ NOT NULL DEFAULT now(), PRIMARY KEY (factoid_id, user_id) ); -- ============================================================ -- 7. Meldinger (chat) -- ============================================================ CREATE TYPE message_type AS ENUM ( 'text', -- Vanlig Markdown-melding 'research_clip', -- Innlimt artikkel (Ctrl+A) 'factoid', -- Faktoid delt i chat 'system' -- Automatisk systemmelding ); CREATE TABLE messages ( id UUID PRIMARY KEY REFERENCES nodes(id) ON DELETE CASCADE, topic_id UUID NOT NULL REFERENCES topics(id) ON DELETE CASCADE, reply_to UUID REFERENCES messages(id) ON DELETE SET NULL, author_id TEXT NOT NULL REFERENCES users(authentik_id) ON DELETE SET NULL, message_type message_type NOT NULL DEFAULT 'text', body TEXT NOT NULL, metadata JSONB, edited_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX idx_messages_topic ON messages(topic_id, created_at); CREATE TABLE message_revisions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), message_id UUID NOT NULL REFERENCES messages(id) ON DELETE CASCADE, body TEXT NOT NULL, edited_at TIMESTAMPTZ NOT NULL DEFAULT now(), CONSTRAINT revision_order UNIQUE (message_id, edited_at) ); CREATE TABLE message_votes ( message_id UUID NOT NULL REFERENCES messages(id) ON DELETE CASCADE, user_id TEXT NOT NULL REFERENCES users(authentik_id) ON DELETE CASCADE, vote SMALLINT NOT NULL CHECK (vote IN (-1, 1)), created_at TIMESTAMPTZ NOT NULL DEFAULT now(), PRIMARY KEY (message_id, user_id) ); -- ============================================================ -- 8. Mediefiler (content-addressable) -- ============================================================ CREATE TABLE media_files ( sha256 TEXT PRIMARY KEY, mime_type TEXT NOT NULL, file_size BIGINT NOT NULL, uploaded_by TEXT NOT NULL REFERENCES users(authentik_id) ON DELETE SET NULL, uploaded_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE TABLE message_attachments ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), message_id UUID NOT NULL REFERENCES messages(id) ON DELETE CASCADE, sha256 TEXT NOT NULL REFERENCES media_files(sha256), sort_order SMALLINT NOT NULL DEFAULT 0 ); -- ============================================================ -- 9. Jobbkø -- ============================================================ CREATE TYPE job_status AS ENUM ('pending', 'running', 'completed', 'error', 'retry'); CREATE TABLE job_queue ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), job_type TEXT NOT NULL, payload JSONB NOT NULL, status job_status NOT NULL DEFAULT 'pending', priority SMALLINT NOT NULL DEFAULT 0, result JSONB, error_msg TEXT, attempts SMALLINT NOT NULL DEFAULT 0, max_attempts SMALLINT NOT NULL DEFAULT 3, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), started_at TIMESTAMPTZ, completed_at TIMESTAMPTZ, scheduled_for TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX idx_job_queue_pending ON job_queue (priority DESC, scheduled_for ASC) WHERE status IN ('pending', 'retry'); COMMIT;