Første migrasjon (0001) med komplett databasearkitektur: - nodes/graph_edges grafmodell med relasjonstyper - Faktoider og meldinger med stemme-system (opp/ned) - Chat med redigeringshistorikk, tråding og research-clips - Content-addressable mediefiler (SHA-256 deduplisering) - PostgreSQL-basert jobbkø med SKIP LOCKED - Brukertabell som tynn cache fra Authentik Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
220 lines
8.4 KiB
PL/PgSQL
220 lines
8.4 KiB
PL/PgSQL
-- 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;
|