197 lines
8.3 KiB
SQL
197 lines
8.3 KiB
SQL
-- ==========================================
|
|
-- Chat Switchboard — 005 Channels & Conversations
|
|
-- ==========================================
|
|
-- Channels, messages, participants, models, cursors, folders, user prefs.
|
|
-- ICD §3 (Channels & Conversations)
|
|
-- Note: project_id and workspace_id FKs added by 010_projects.sql
|
|
-- ==========================================
|
|
|
|
-- =========================================
|
|
-- FOLDERS
|
|
-- =========================================
|
|
|
|
CREATE TABLE IF NOT EXISTS folders (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
name VARCHAR(200) NOT NULL,
|
|
parent_id UUID REFERENCES folders(id) ON DELETE CASCADE,
|
|
sort_order INT DEFAULT 0,
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
|
UNIQUE(user_id, name, parent_id)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_folders_user ON folders(user_id);
|
|
CREATE INDEX IF NOT EXISTS idx_folders_parent ON folders(parent_id);
|
|
DROP TRIGGER IF EXISTS folders_updated_at ON folders;
|
|
CREATE TRIGGER folders_updated_at BEFORE UPDATE ON folders
|
|
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
|
|
|
|
|
-- =========================================
|
|
-- CHANNELS
|
|
-- =========================================
|
|
|
|
CREATE TABLE IF NOT EXISTS channels (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
|
|
title VARCHAR(500) NOT NULL,
|
|
description TEXT,
|
|
type VARCHAR(20) DEFAULT 'direct'
|
|
CHECK (type IN ('direct', 'group', 'workflow')),
|
|
model VARCHAR(100),
|
|
system_prompt TEXT,
|
|
provider_config_id UUID REFERENCES provider_configs(id) ON DELETE SET NULL,
|
|
is_archived BOOLEAN DEFAULT false,
|
|
is_pinned BOOLEAN DEFAULT false,
|
|
folder_id UUID,
|
|
folder TEXT,
|
|
team_id UUID REFERENCES teams(id) ON DELETE SET NULL,
|
|
settings JSONB DEFAULT '{}'::jsonb,
|
|
tags TEXT[],
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_channels_user ON channels(user_id);
|
|
CREATE INDEX IF NOT EXISTS idx_channels_updated ON channels(updated_at DESC);
|
|
CREATE INDEX IF NOT EXISTS idx_channels_type ON channels(type);
|
|
CREATE INDEX IF NOT EXISTS idx_channels_team ON channels(team_id) WHERE team_id IS NOT NULL;
|
|
CREATE INDEX IF NOT EXISTS idx_channels_tags ON channels USING GIN(tags);
|
|
DROP TRIGGER IF EXISTS channels_updated_at ON channels;
|
|
CREATE TRIGGER channels_updated_at BEFORE UPDATE ON channels
|
|
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
|
|
|
-- Deferred FK: channels.folder_id → folders.id
|
|
DO $$ BEGIN
|
|
IF NOT EXISTS (
|
|
SELECT 1 FROM pg_constraint WHERE conname = 'fk_channels_folder'
|
|
) THEN
|
|
ALTER TABLE channels ADD CONSTRAINT fk_channels_folder
|
|
FOREIGN KEY (folder_id) REFERENCES folders(id) ON DELETE SET NULL;
|
|
END IF;
|
|
END $$;
|
|
CREATE INDEX IF NOT EXISTS idx_channels_folder ON channels(folder_id) WHERE folder_id IS NOT NULL;
|
|
|
|
COMMENT ON COLUMN channels.type IS 'direct=single-user chat, group=multi-user/multi-model, workflow=staged channel';
|
|
|
|
|
|
-- =========================================
|
|
-- MESSAGES
|
|
-- =========================================
|
|
|
|
CREATE TABLE IF NOT EXISTS messages (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
channel_id UUID NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
|
|
role VARCHAR(20) NOT NULL
|
|
CHECK (role IN ('user', 'assistant', 'system', 'tool')),
|
|
content TEXT NOT NULL,
|
|
model VARCHAR(100),
|
|
tokens_used INTEGER,
|
|
tool_calls JSONB,
|
|
metadata JSONB DEFAULT '{}'::jsonb,
|
|
parent_id UUID REFERENCES messages(id) ON DELETE SET NULL,
|
|
sibling_index INTEGER DEFAULT 0,
|
|
participant_type VARCHAR(10) DEFAULT 'user',
|
|
participant_id VARCHAR(255),
|
|
provider_config_id UUID REFERENCES provider_configs(id) ON DELETE SET NULL,
|
|
deleted_at TIMESTAMPTZ,
|
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_messages_channel ON messages(channel_id, created_at);
|
|
CREATE INDEX IF NOT EXISTS idx_messages_parent ON messages(parent_id);
|
|
CREATE INDEX IF NOT EXISTS idx_messages_alive ON messages(channel_id, created_at)
|
|
WHERE deleted_at IS NULL;
|
|
CREATE INDEX IF NOT EXISTS idx_messages_parent_alive ON messages(parent_id, sibling_index)
|
|
WHERE deleted_at IS NULL;
|
|
|
|
COMMENT ON COLUMN messages.parent_id IS 'Tree parent for conversation forking';
|
|
COMMENT ON COLUMN messages.sibling_index IS 'Position among siblings (0-indexed)';
|
|
|
|
|
|
-- =========================================
|
|
-- CHANNEL PARTICIPANTS & MODELS
|
|
-- =========================================
|
|
-- ICD §3.7: Polymorphic participant system.
|
|
-- participant_type determines what participant_id references:
|
|
-- user → users.id
|
|
-- persona → personas.id
|
|
-- session → opaque session token
|
|
-- =========================================
|
|
|
|
CREATE TABLE IF NOT EXISTS channel_participants (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
channel_id UUID NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
|
|
participant_type VARCHAR(10) NOT NULL DEFAULT 'user'
|
|
CHECK (participant_type IN ('user', 'persona', 'session')),
|
|
participant_id UUID NOT NULL,
|
|
role VARCHAR(10) NOT NULL DEFAULT 'member'
|
|
CHECK (role IN ('owner', 'member', 'observer')),
|
|
display_name VARCHAR(200),
|
|
avatar_url TEXT,
|
|
joined_at TIMESTAMPTZ DEFAULT NOW(),
|
|
last_read_at TIMESTAMPTZ DEFAULT NOW(),
|
|
UNIQUE(channel_id, participant_type, participant_id)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_channel_participants_channel ON channel_participants(channel_id);
|
|
CREATE INDEX IF NOT EXISTS idx_channel_participants_ref ON channel_participants(participant_type, participant_id);
|
|
|
|
CREATE TABLE IF NOT EXISTS channel_models (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
channel_id UUID NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
|
|
model_id VARCHAR(255) NOT NULL,
|
|
provider_config_id UUID REFERENCES provider_configs(id) ON DELETE SET NULL,
|
|
persona_id UUID REFERENCES personas(id) ON DELETE SET NULL,
|
|
display_name VARCHAR(100),
|
|
system_prompt TEXT,
|
|
settings JSONB DEFAULT '{}'::jsonb,
|
|
is_default BOOLEAN DEFAULT false,
|
|
added_at TIMESTAMPTZ DEFAULT NOW(),
|
|
UNIQUE(channel_id, model_id, provider_config_id)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_channel_models_channel ON channel_models(channel_id);
|
|
|
|
|
|
-- =========================================
|
|
-- CHANNEL CURSORS (forking navigation)
|
|
-- =========================================
|
|
|
|
CREATE TABLE IF NOT EXISTS channel_cursors (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
channel_id UUID NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
|
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
active_leaf_id UUID REFERENCES messages(id) ON DELETE SET NULL,
|
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
|
UNIQUE(channel_id, user_id)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_channel_cursors_channel ON channel_cursors(channel_id);
|
|
CREATE INDEX IF NOT EXISTS idx_channel_cursors_user ON channel_cursors(user_id);
|
|
|
|
|
|
-- =========================================
|
|
-- USER MODEL SETTINGS
|
|
-- =========================================
|
|
|
|
CREATE TABLE IF NOT EXISTS user_model_settings (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
model_id TEXT NOT NULL,
|
|
provider_config_id UUID REFERENCES provider_configs(id) ON DELETE CASCADE,
|
|
hidden BOOLEAN DEFAULT false,
|
|
preferred_temperature FLOAT,
|
|
preferred_max_tokens INT,
|
|
sort_order INT DEFAULT 0,
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
|
UNIQUE(user_id, model_id, provider_config_id)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_user_model_settings_user ON user_model_settings(user_id);
|
|
DROP TRIGGER IF EXISTS user_model_settings_updated_at ON user_model_settings;
|
|
CREATE TRIGGER user_model_settings_updated_at BEFORE UPDATE ON user_model_settings
|
|
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|