Changeset 0.19.0.1 (#82)
This commit is contained in:
157
server/database/migrations/006_v0190_projects.sql
Normal file
157
server/database/migrations/006_v0190_projects.sql
Normal file
@@ -0,0 +1,157 @@
|
||||
-- v0.19.0: Projects / Workspaces
|
||||
--
|
||||
-- New: projects table
|
||||
-- New: project_channels junction table (1:1 channel→project)
|
||||
-- New: project_knowledge_bases junction table
|
||||
-- New: project_notes junction table
|
||||
-- New: channels.project_id denormalized FK
|
||||
-- Changed: resource_grants.resource_type CHECK includes 'project'
|
||||
-- Deprecated: channels.folder (column retained, no longer written)
|
||||
|
||||
-- =========================================
|
||||
-- 1. Projects Table
|
||||
-- =========================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS projects (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name VARCHAR(200) NOT NULL,
|
||||
description TEXT,
|
||||
color VARCHAR(7), -- hex color e.g. '#3B82F6'
|
||||
icon VARCHAR(50), -- emoji or icon name
|
||||
scope VARCHAR(20) NOT NULL DEFAULT 'personal'
|
||||
CHECK (scope IN ('personal', 'team', 'global')),
|
||||
owner_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
team_id UUID REFERENCES teams(id) ON DELETE SET NULL,
|
||||
is_archived BOOLEAN NOT NULL DEFAULT false,
|
||||
settings JSONB DEFAULT '{}'::jsonb,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_projects_owner ON projects(owner_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_projects_team ON projects(team_id) WHERE team_id IS NOT NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_projects_scope ON projects(scope);
|
||||
|
||||
DROP TRIGGER IF EXISTS projects_updated_at ON projects;
|
||||
CREATE TRIGGER projects_updated_at BEFORE UPDATE ON projects
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
||||
|
||||
COMMENT ON TABLE projects IS 'Organizes channels, KBs, and notes into named workspaces';
|
||||
COMMENT ON COLUMN projects.scope IS 'personal=owner only, team=team members, global=all users';
|
||||
|
||||
-- =========================================
|
||||
-- 2. Project ↔ Channel Junction
|
||||
-- =========================================
|
||||
-- One project per channel (UNIQUE on channel_id).
|
||||
-- Source of truth for position and folder within the project.
|
||||
|
||||
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,
|
||||
position INTEGER NOT NULL DEFAULT 0,
|
||||
folder TEXT, -- sub-folder within the project
|
||||
added_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
PRIMARY KEY (project_id, channel_id),
|
||||
UNIQUE (channel_id) -- one project per channel
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_project_channels_project ON project_channels(project_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_project_channels_channel ON project_channels(channel_id);
|
||||
|
||||
COMMENT ON TABLE project_channels IS 'Links channels to projects. UNIQUE(channel_id) enforces one-project-per-channel.';
|
||||
COMMENT ON COLUMN project_channels.position IS 'Sort order within the project sidebar group';
|
||||
COMMENT ON COLUMN project_channels.folder IS 'Optional sub-folder path within the project';
|
||||
|
||||
-- =========================================
|
||||
-- 3. Project ↔ Knowledge Base Junction
|
||||
-- =========================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS project_knowledge_bases (
|
||||
project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
||||
kb_id UUID NOT NULL REFERENCES knowledge_bases(id) ON DELETE CASCADE,
|
||||
auto_search BOOLEAN NOT NULL DEFAULT false,
|
||||
added_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
PRIMARY KEY (project_id, kb_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_project_kb_project ON project_knowledge_bases(project_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_project_kb_kb ON project_knowledge_bases(kb_id);
|
||||
|
||||
COMMENT ON TABLE project_knowledge_bases IS 'Binds KBs to Projects — injected at completion time for all project channels';
|
||||
COMMENT ON COLUMN project_knowledge_bases.auto_search IS 'true = auto-prepend top-K results; false = kb_search tool only';
|
||||
|
||||
-- =========================================
|
||||
-- 4. Project ↔ Note Junction
|
||||
-- =========================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS project_notes (
|
||||
project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
||||
note_id UUID NOT NULL REFERENCES notes(id) ON DELETE CASCADE,
|
||||
added_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
PRIMARY KEY (project_id, note_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_project_notes_project ON project_notes(project_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_project_notes_note ON project_notes(note_id);
|
||||
|
||||
COMMENT ON TABLE project_notes IS 'Links notes to projects for organizational grouping';
|
||||
|
||||
-- =========================================
|
||||
-- 5. Denormalized project_id on Channels
|
||||
-- =========================================
|
||||
-- Fast sidebar query: SELECT ... FROM channels WHERE project_id = $1
|
||||
-- Source of truth is project_channels; this is updated atomically.
|
||||
|
||||
ALTER TABLE channels
|
||||
ADD COLUMN IF NOT EXISTS project_id UUID REFERENCES projects(id) ON DELETE SET NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_channels_project ON channels(project_id) WHERE project_id IS NOT NULL;
|
||||
|
||||
-- =========================================
|
||||
-- 6. Extend resource_grants CHECK
|
||||
-- =========================================
|
||||
-- Add 'project' to allowed resource_type values.
|
||||
-- Postgres ALTER TABLE ... ALTER CONSTRAINT is not supported; drop + re-add.
|
||||
|
||||
ALTER TABLE resource_grants DROP CONSTRAINT IF EXISTS resource_grants_resource_type_check;
|
||||
ALTER TABLE resource_grants ADD CONSTRAINT resource_grants_resource_type_check
|
||||
CHECK (resource_type IN ('persona', 'knowledge_base', 'project'));
|
||||
|
||||
-- =========================================
|
||||
-- 7. Best-effort folder → project migration
|
||||
-- =========================================
|
||||
-- Create a personal project for each distinct folder value, then
|
||||
-- associate the channels. Skip if no folder values exist.
|
||||
-- This is idempotent: re-running won't duplicate projects because
|
||||
-- we check for existing project_id.
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
_folder TEXT;
|
||||
_user_id UUID;
|
||||
_proj_id UUID;
|
||||
_ch_id UUID;
|
||||
BEGIN
|
||||
FOR _user_id, _folder IN
|
||||
SELECT DISTINCT user_id, folder FROM channels
|
||||
WHERE folder IS NOT NULL AND folder != '' AND project_id IS NULL
|
||||
LOOP
|
||||
-- Create project named after the folder
|
||||
INSERT INTO projects (name, scope, owner_id)
|
||||
VALUES (_folder, 'personal', _user_id)
|
||||
RETURNING id INTO _proj_id;
|
||||
|
||||
-- Associate channels
|
||||
FOR _ch_id IN
|
||||
SELECT id FROM channels
|
||||
WHERE user_id = _user_id AND folder = _folder AND project_id IS NULL
|
||||
LOOP
|
||||
INSERT INTO project_channels (project_id, channel_id, position)
|
||||
VALUES (_proj_id, _ch_id, 0)
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
UPDATE channels SET project_id = _proj_id WHERE id = _ch_id;
|
||||
END LOOP;
|
||||
END LOOP;
|
||||
END
|
||||
$$;
|
||||
Reference in New Issue
Block a user