Changeset 0.6.0 (#36)
This commit is contained in:
114
server/database/migrations/006_channels_unify.sql
Normal file
114
server/database/migrations/006_channels_unify.sql
Normal file
@@ -0,0 +1,114 @@
|
||||
-- ==========================================
|
||||
-- Migration 006: Unify Chats → Channels
|
||||
-- ==========================================
|
||||
-- "Everything is a channel." Merges the separate chats/channels
|
||||
-- schema into a single unified channel model.
|
||||
--
|
||||
-- The old channels/channel_members/channel_messages tables (from 001)
|
||||
-- were never populated — safe to drop and rebuild on top of chats.
|
||||
-- ==========================================
|
||||
|
||||
-- ── 1. Drop unused legacy channel tables ────
|
||||
-- These were placeholders from 001; no data, no handlers.
|
||||
-- Order matters: drop dependents first.
|
||||
|
||||
-- Remove FK references in tool_usage_log before dropping
|
||||
ALTER TABLE tool_usage_log DROP CONSTRAINT IF EXISTS tool_usage_log_channel_id_fkey;
|
||||
ALTER TABLE tool_usage_log DROP COLUMN IF EXISTS channel_id;
|
||||
|
||||
DROP TABLE IF EXISTS channel_messages CASCADE;
|
||||
DROP TABLE IF EXISTS channel_members CASCADE;
|
||||
DROP TABLE IF EXISTS channels CASCADE;
|
||||
|
||||
-- Drop the old trigger (will re-create after rename)
|
||||
DROP TRIGGER IF EXISTS channels_updated_at ON channels;
|
||||
|
||||
-- ── 2. Rename chats → channels ──────────────
|
||||
|
||||
ALTER TABLE chats RENAME TO channels;
|
||||
ALTER TABLE chat_messages RENAME TO messages;
|
||||
|
||||
-- Rename columns to match new schema
|
||||
ALTER TABLE messages RENAME COLUMN chat_id TO channel_id;
|
||||
|
||||
-- Rename indexes
|
||||
ALTER INDEX IF EXISTS idx_chats_user RENAME TO idx_channels_user;
|
||||
ALTER INDEX IF EXISTS idx_chats_updated RENAME TO idx_channels_updated;
|
||||
ALTER INDEX IF EXISTS idx_chats_tags RENAME TO idx_channels_tags;
|
||||
ALTER INDEX IF EXISTS idx_chat_messages_chat RENAME TO idx_messages_channel;
|
||||
|
||||
-- Rename constraints (PK and FK auto-renamed with table on some PG versions,
|
||||
-- but let's be explicit for the FK)
|
||||
-- Note: PG auto-renames PKs but not FKs or check constraints
|
||||
|
||||
-- Rename the updated_at trigger
|
||||
DROP TRIGGER IF EXISTS chats_updated_at ON channels;
|
||||
CREATE TRIGGER channels_updated_at BEFORE UPDATE ON channels
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
||||
|
||||
-- ── 3. Add channel type + description ───────
|
||||
|
||||
ALTER TABLE channels ADD COLUMN IF NOT EXISTS type VARCHAR(20) DEFAULT 'direct';
|
||||
ALTER TABLE channels ADD COLUMN IF NOT EXISTS description TEXT;
|
||||
|
||||
COMMENT ON COLUMN channels.type IS 'direct=1:1 AI chat, group=multi-model, channel=named persistent';
|
||||
|
||||
-- Backfill: all existing rows are 1:1 AI chats
|
||||
UPDATE channels SET type = 'direct' WHERE type IS NULL;
|
||||
|
||||
-- Index on type for filtered queries
|
||||
CREATE INDEX IF NOT EXISTS idx_channels_type ON channels(type);
|
||||
|
||||
-- ── 4. Add message tree (parent_id) ─────────
|
||||
|
||||
ALTER TABLE messages ADD COLUMN IF NOT EXISTS parent_id UUID REFERENCES messages(id) ON DELETE SET NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_messages_parent ON messages(parent_id);
|
||||
|
||||
-- Backfill linear parent chains on existing messages.
|
||||
-- Each message's parent is the previous message in the same channel (by created_at).
|
||||
WITH ordered AS (
|
||||
SELECT id, channel_id, created_at,
|
||||
LAG(id) OVER (PARTITION BY channel_id ORDER BY created_at) AS prev_id
|
||||
FROM messages
|
||||
)
|
||||
UPDATE messages m
|
||||
SET parent_id = o.prev_id
|
||||
FROM ordered o
|
||||
WHERE m.id = o.id AND o.prev_id IS NOT NULL AND m.parent_id IS NULL;
|
||||
|
||||
-- ── 5. Add participant columns on messages ──
|
||||
-- Decouples messages from user-only: AI models are participants too.
|
||||
|
||||
ALTER TABLE messages ADD COLUMN IF NOT EXISTS participant_type VARCHAR(10) DEFAULT 'user';
|
||||
ALTER TABLE messages ADD COLUMN IF NOT EXISTS participant_id VARCHAR(255);
|
||||
|
||||
COMMENT ON COLUMN messages.participant_type IS 'user or model';
|
||||
COMMENT ON COLUMN messages.participant_id IS 'user UUID or model identifier string';
|
||||
|
||||
-- Backfill: user messages get the channel owner's user_id;
|
||||
-- assistant messages get the model name as participant_id.
|
||||
UPDATE messages m
|
||||
SET participant_type = CASE WHEN m.role = 'assistant' THEN 'model' ELSE 'user' END,
|
||||
participant_id = CASE
|
||||
WHEN m.role = 'assistant' THEN COALESCE(m.model, 'unknown')
|
||||
ELSE (SELECT c.user_id::text FROM channels c WHERE c.id = m.channel_id)
|
||||
END
|
||||
WHERE m.participant_id IS NULL;
|
||||
|
||||
-- ── 6. Update model_routing_log FK ──────────
|
||||
|
||||
-- The FK column was named chat_id — rename it
|
||||
ALTER TABLE model_routing_log RENAME COLUMN chat_id TO channel_id;
|
||||
ALTER INDEX IF EXISTS idx_routing_log_chat RENAME TO idx_routing_log_channel;
|
||||
|
||||
-- message_id FK still valid (messages table was renamed, FK follows)
|
||||
|
||||
-- ── 7. Update tool_usage_log ────────────────
|
||||
-- We already dropped the old channel_id column above.
|
||||
-- Re-add it pointing to the unified channels table.
|
||||
-- Also rename the old chat_id column.
|
||||
|
||||
ALTER TABLE tool_usage_log RENAME COLUMN chat_id TO channel_id;
|
||||
-- The FK auto-follows the table rename, but let's be safe:
|
||||
-- (chat_id FK pointed to chats(id), which is now channels(id) — PG handles this)
|
||||
56
server/database/migrations/007_channel_members_models.sql
Normal file
56
server/database/migrations/007_channel_members_models.sql
Normal file
@@ -0,0 +1,56 @@
|
||||
-- ==========================================
|
||||
-- Migration 007: Channel Members & Models
|
||||
-- ==========================================
|
||||
-- Membership and model assignment tables for
|
||||
-- multi-user and multi-model channels.
|
||||
-- ==========================================
|
||||
|
||||
-- ── Channel Members ─────────────────────────
|
||||
-- Who is in this channel (human participants).
|
||||
|
||||
CREATE TABLE IF NOT EXISTS channel_members (
|
||||
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,
|
||||
role VARCHAR(20) DEFAULT 'member', -- owner, admin, member
|
||||
joined_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
last_read_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
UNIQUE(channel_id, user_id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_channel_members_channel ON channel_members(channel_id);
|
||||
CREATE INDEX idx_channel_members_user ON channel_members(user_id);
|
||||
|
||||
-- Backfill: existing channels are 1:1, so the channel owner is the sole member.
|
||||
INSERT INTO channel_members (channel_id, user_id, role)
|
||||
SELECT id, user_id, 'owner'
|
||||
FROM channels
|
||||
WHERE user_id IS NOT NULL
|
||||
ON CONFLICT (channel_id, user_id) DO NOTHING;
|
||||
|
||||
-- ── Channel Models ──────────────────────────
|
||||
-- Which AI models are assigned to this channel.
|
||||
-- For direct chats this is one model; for group/channel
|
||||
-- there can be multiple with @mention routing.
|
||||
|
||||
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, -- e.g. 'claude-sonnet-4'
|
||||
api_config_id UUID REFERENCES api_configs(id) ON DELETE SET NULL,
|
||||
display_name VARCHAR(100), -- optional alias in this channel
|
||||
system_prompt TEXT, -- per-model system prompt override
|
||||
settings JSONB DEFAULT '{}'::jsonb, -- temperature, max_tokens overrides
|
||||
is_default BOOLEAN DEFAULT false, -- auto-complete (no @mention needed)
|
||||
added_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
UNIQUE(channel_id, model_id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_channel_models_channel ON channel_models(channel_id);
|
||||
|
||||
-- Backfill: existing channels have a model in the channels.model column.
|
||||
INSERT INTO channel_models (channel_id, model_id, api_config_id, is_default)
|
||||
SELECT id, model, api_config_id, true
|
||||
FROM channels
|
||||
WHERE model IS NOT NULL AND model != ''
|
||||
ON CONFLICT (channel_id, model_id) DO NOTHING;
|
||||
29
server/database/migrations/008_channel_cursors.sql
Normal file
29
server/database/migrations/008_channel_cursors.sql
Normal file
@@ -0,0 +1,29 @@
|
||||
-- ==========================================
|
||||
-- Migration 008: Channel Cursors
|
||||
-- ==========================================
|
||||
-- Tracks each user's active branch position
|
||||
-- per channel. Essential for conversation forking.
|
||||
-- ==========================================
|
||||
|
||||
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 idx_channel_cursors_channel ON channel_cursors(channel_id);
|
||||
CREATE INDEX idx_channel_cursors_user ON channel_cursors(user_id);
|
||||
|
||||
-- Backfill: set cursor to the last message in each channel for the owner.
|
||||
INSERT INTO channel_cursors (channel_id, user_id, active_leaf_id)
|
||||
SELECT c.id, c.user_id, (
|
||||
SELECT m.id FROM messages m
|
||||
WHERE m.channel_id = c.id
|
||||
ORDER BY m.created_at DESC LIMIT 1
|
||||
)
|
||||
FROM channels c
|
||||
WHERE c.user_id IS NOT NULL
|
||||
ON CONFLICT (channel_id, user_id) DO NOTHING;
|
||||
57
server/database/migrations/009_folders_projects.sql
Normal file
57
server/database/migrations/009_folders_projects.sql
Normal file
@@ -0,0 +1,57 @@
|
||||
-- ==========================================
|
||||
-- Migration 009: Folders & Projects
|
||||
-- ==========================================
|
||||
-- Organizational structures for channels.
|
||||
-- Folders are simple containers; projects are
|
||||
-- tagged collections that can span folders.
|
||||
-- ==========================================
|
||||
|
||||
-- ── 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 idx_folders_user ON folders(user_id);
|
||||
CREATE INDEX idx_folders_parent ON folders(parent_id);
|
||||
|
||||
CREATE TRIGGER folders_updated_at BEFORE UPDATE ON folders
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
||||
|
||||
-- Add folder_id to channels (replaces the old text folder column)
|
||||
ALTER TABLE channels ADD COLUMN IF NOT EXISTS folder_id UUID REFERENCES folders(id) ON DELETE SET NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_channels_folder ON channels(folder_id);
|
||||
|
||||
-- ── Projects ────────────────────────────────
|
||||
|
||||
CREATE TABLE IF NOT EXISTS projects (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
name VARCHAR(200) NOT NULL,
|
||||
description TEXT,
|
||||
color VARCHAR(7), -- hex color for UI badge
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_projects_user ON projects(user_id);
|
||||
|
||||
CREATE TRIGGER projects_updated_at BEFORE UPDATE ON projects
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
||||
|
||||
-- Junction: channels can belong to multiple projects
|
||||
CREATE TABLE IF NOT EXISTS project_channels (
|
||||
project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
||||
channel_id UUID NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
|
||||
added_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
PRIMARY KEY (project_id, channel_id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_project_channels_channel ON project_channels(channel_id);
|
||||
32
server/database/migrations/010_banners.sql
Normal file
32
server/database/migrations/010_banners.sql
Normal file
@@ -0,0 +1,32 @@
|
||||
-- ==========================================
|
||||
-- Migration 010: Environment Banners
|
||||
-- ==========================================
|
||||
-- Environment banners for deployment context
|
||||
-- (dev, staging, production, etc). Stored in
|
||||
-- global_settings with a dedicated key.
|
||||
-- ==========================================
|
||||
|
||||
-- Seed default banner config (disabled).
|
||||
-- Schema: { enabled, text, position, bg, fg }
|
||||
INSERT INTO global_settings (key, value) VALUES
|
||||
('banner', '{
|
||||
"enabled": false,
|
||||
"text": "",
|
||||
"position": "both",
|
||||
"bg": "#007a33",
|
||||
"fg": "#ffffff"
|
||||
}'::jsonb)
|
||||
ON CONFLICT (key) DO NOTHING;
|
||||
|
||||
-- Banner presets for quick selection in admin UI.
|
||||
-- Generic environment labels — admins can set custom text.
|
||||
INSERT INTO global_settings (key, value) VALUES
|
||||
('banner_presets', '{
|
||||
"development": { "text": "DEVELOPMENT", "bg": "#007a33", "fg": "#ffffff" },
|
||||
"testing": { "text": "TESTING", "bg": "#502b85", "fg": "#ffffff" },
|
||||
"staging": { "text": "STAGING", "bg": "#0033a0", "fg": "#ffffff" },
|
||||
"production": { "text": "PRODUCTION", "bg": "#c8102e", "fg": "#ffffff" },
|
||||
"training": { "text": "TRAINING", "bg": "#ff8c00", "fg": "#000000" },
|
||||
"demo": { "text": "DEMO", "bg": "#fce83a", "fg": "#000000" }
|
||||
}'::jsonb)
|
||||
ON CONFLICT (key) DO NOTHING;
|
||||
18
server/database/migrations/011_banner_presets_fix.sql
Normal file
18
server/database/migrations/011_banner_presets_fix.sql
Normal file
@@ -0,0 +1,18 @@
|
||||
-- ==========================================
|
||||
-- Migration 011: Replace banner presets
|
||||
-- ==========================================
|
||||
-- Replaces legacy presets with
|
||||
-- generic environment labels. Existing databases
|
||||
-- that ran 010 have the old presets; this overwrites.
|
||||
-- ==========================================
|
||||
|
||||
UPDATE global_settings
|
||||
SET value = '{
|
||||
"development": { "text": "DEVELOPMENT", "bg": "#007a33", "fg": "#ffffff" },
|
||||
"testing": { "text": "TESTING", "bg": "#502b85", "fg": "#ffffff" },
|
||||
"staging": { "text": "STAGING", "bg": "#0033a0", "fg": "#ffffff" },
|
||||
"production": { "text": "PRODUCTION", "bg": "#c8102e", "fg": "#ffffff" },
|
||||
"training": { "text": "TRAINING", "bg": "#ff8c00", "fg": "#000000" },
|
||||
"demo": { "text": "DEMO", "bg": "#fce83a", "fg": "#000000" }
|
||||
}'::jsonb
|
||||
WHERE key = 'banner_presets';
|
||||
Reference in New Issue
Block a user