153 lines
5.6 KiB
SQL
153 lines
5.6 KiB
SQL
-- ==========================================
|
|
-- Chat Switchboard — 010 Projects
|
|
-- ==========================================
|
|
-- Projects, junction tables, and cross-domain FK columns on
|
|
-- channels/projects for workspace/project bindings.
|
|
-- ICD §8 (Projects)
|
|
-- ==========================================
|
|
|
|
-- =========================================
|
|
-- PROJECTS
|
|
-- =========================================
|
|
|
|
CREATE TABLE IF NOT EXISTS projects (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
name VARCHAR(200) NOT NULL,
|
|
description TEXT,
|
|
color VARCHAR(7),
|
|
icon VARCHAR(50),
|
|
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,
|
|
workspace_id UUID REFERENCES workspaces(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);
|
|
CREATE INDEX IF NOT EXISTS idx_projects_workspace ON projects(workspace_id) WHERE workspace_id IS NOT NULL;
|
|
|
|
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';
|
|
|
|
|
|
-- =========================================
|
|
-- PROJECT ↔ CHANNEL JUNCTION
|
|
-- =========================================
|
|
|
|
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,
|
|
added_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
PRIMARY KEY (project_id, channel_id),
|
|
UNIQUE (channel_id)
|
|
);
|
|
|
|
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.';
|
|
|
|
|
|
-- =========================================
|
|
-- PROJECT ↔ KB 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);
|
|
|
|
|
|
-- =========================================
|
|
-- 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);
|
|
|
|
|
|
-- =========================================
|
|
-- CROSS-DOMAIN FK COLUMNS
|
|
-- =========================================
|
|
-- Deferred FKs: channels.project_id and workspace_id declared in 005
|
|
-- without FK constraints (referenced tables didn't exist yet).
|
|
|
|
DO $$ BEGIN
|
|
IF NOT EXISTS (
|
|
SELECT 1 FROM pg_constraint WHERE conname = 'fk_channels_project'
|
|
) THEN
|
|
ALTER TABLE channels ADD CONSTRAINT fk_channels_project
|
|
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE SET NULL;
|
|
END IF;
|
|
END $$;
|
|
|
|
DO $$ BEGIN
|
|
IF NOT EXISTS (
|
|
SELECT 1 FROM pg_constraint WHERE conname = 'fk_channels_workspace'
|
|
) THEN
|
|
ALTER TABLE channels ADD CONSTRAINT fk_channels_workspace
|
|
FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE SET NULL;
|
|
END IF;
|
|
END $$;
|
|
|
|
|
|
-- =========================================
|
|
-- FOLDER → PROJECT MIGRATION (idempotent)
|
|
-- =========================================
|
|
-- Best-effort migration of legacy folder values into projects.
|
|
-- Only runs if there are channels with a folder value and no 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
|
|
INSERT INTO projects (name, scope, owner_id)
|
|
VALUES (_folder, 'personal', _user_id)
|
|
RETURNING id INTO _proj_id;
|
|
|
|
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
|
|
$$;
|