step 4: fresh kernel-only migrations
Deleted all 23+23 old chat-switchboard migration files. Wrote 9+9 clean kernel-only migrations in postgres/ and sqlite/ subdirs. 27 tables per dialect covering all 20 store interfaces: 001: users, refresh_tokens, policies, settings, presence, oidc 002: teams, team_members, groups, group_members 003: packages, package_user_settings, ext_permissions, ext_data_tables 004: ext_connections, ext_dependencies, resource_grants 005: notifications, notification_preferences 006: audit_log 007: workflows, workflow_stages, workflow_versions 008: tasks, task_runs 009: ws_tickets, rate_limit_counters Dropped: providers, personas, channels, messages, knowledge, notes, memory, workspaces, projects, folders, files, usage_log, tool_health, workflow_assignments, ext_view_channels. Schema changes vs old: - pgvector extension removed (no KB embeddings) - resource_grants: resource_type CHECK removed (open-ended) - workflow_stages: persona_id kept as nullable TEXT (no FK) - workflow_stages: stage_mode default=form_only, added custom - tasks: dropped output_channel_id, provider_config_id columns - tasks: task_type removed prompt, output_mode: notification|webhook|log - task_runs: dropped channel_id - platform_policies: kernel-only seeds - global_settings: kernel-only seeds, site name=Switchboard Core - Everyone group: kernel permissions (extension.use, workflow.submit) -2815/+487 lines.
This commit is contained in:
@@ -1,183 +0,0 @@
|
||||
-- ==========================================
|
||||
-- Chat Switchboard — 003 Providers & Routing
|
||||
-- ==========================================
|
||||
-- Provider configs, model catalog, pricing, health, capability
|
||||
-- overrides, and routing policies.
|
||||
-- Consolidated v0.28.0: adds capability_match policy type.
|
||||
-- ==========================================
|
||||
|
||||
-- =========================================
|
||||
-- PROVIDER CONFIGS
|
||||
-- =========================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS provider_configs (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
scope VARCHAR(10) NOT NULL
|
||||
CHECK (scope IN ('global', 'team', 'personal')),
|
||||
owner_id UUID,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
provider VARCHAR(50) NOT NULL,
|
||||
endpoint TEXT NOT NULL,
|
||||
|
||||
-- Encrypted API key (AES-256-GCM)
|
||||
api_key_enc BYTEA,
|
||||
key_nonce BYTEA,
|
||||
key_scope TEXT NOT NULL DEFAULT 'global',
|
||||
|
||||
model_default VARCHAR(100),
|
||||
config JSONB DEFAULT '{}'::jsonb,
|
||||
headers JSONB DEFAULT '{}'::jsonb,
|
||||
settings JSONB DEFAULT '{}'::jsonb,
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
is_private BOOLEAN DEFAULT false,
|
||||
proxy_mode TEXT NOT NULL DEFAULT 'system'
|
||||
CHECK (proxy_mode IN ('system', 'direct', 'custom')),
|
||||
proxy_url TEXT,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
CONSTRAINT chk_proxy_url CHECK (proxy_mode != 'custom' OR proxy_url IS NOT NULL)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_provider_configs_scope ON provider_configs(scope);
|
||||
CREATE INDEX IF NOT EXISTS idx_provider_configs_owner ON provider_configs(owner_id) WHERE owner_id IS NOT NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_provider_configs_active ON provider_configs(is_active) WHERE is_active = true;
|
||||
DROP TRIGGER IF EXISTS provider_configs_updated_at ON provider_configs;
|
||||
CREATE TRIGGER provider_configs_updated_at BEFORE UPDATE ON provider_configs
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
||||
|
||||
COMMENT ON COLUMN provider_configs.scope IS 'global=admin-managed, team=team-scoped, personal=user BYOK';
|
||||
COMMENT ON COLUMN provider_configs.owner_id IS 'NULL for global; teams.id for team scope; users.id for personal scope';
|
||||
COMMENT ON COLUMN provider_configs.api_key_enc IS 'AES-256-GCM encrypted API key (BYTEA)';
|
||||
COMMENT ON COLUMN provider_configs.key_nonce IS 'AES-GCM nonce for api_key_enc';
|
||||
COMMENT ON COLUMN provider_configs.key_scope IS 'Encryption tier: global/team use env key, personal uses UEK';
|
||||
COMMENT ON COLUMN provider_configs.headers IS 'Custom HTTP headers (e.g. OpenRouter HTTP-Referer)';
|
||||
COMMENT ON COLUMN provider_configs.settings IS 'Provider-specific params (e.g. Venice web_search, OpenRouter route)';
|
||||
COMMENT ON COLUMN provider_configs.is_private IS 'Data stays on-prem (local/self-hosted provider)';
|
||||
COMMENT ON COLUMN provider_configs.proxy_mode IS 'system=env proxy, direct=no proxy, custom=explicit URL';
|
||||
COMMENT ON COLUMN provider_configs.proxy_url IS 'Explicit proxy URL when proxy_mode=custom (http/https/socks5)';
|
||||
|
||||
|
||||
-- =========================================
|
||||
-- MODEL CATALOG
|
||||
-- =========================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS model_catalog (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
provider_config_id UUID NOT NULL REFERENCES provider_configs(id) ON DELETE CASCADE,
|
||||
model_id TEXT NOT NULL,
|
||||
display_name TEXT,
|
||||
model_type VARCHAR(20) DEFAULT 'chat',
|
||||
capabilities JSONB NOT NULL DEFAULT '{}',
|
||||
pricing JSONB,
|
||||
visibility VARCHAR(10) DEFAULT 'disabled'
|
||||
CHECK (visibility IN ('enabled', 'disabled', 'team')),
|
||||
last_synced_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
UNIQUE(provider_config_id, model_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_model_catalog_provider ON model_catalog(provider_config_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_model_catalog_enabled ON model_catalog(visibility) WHERE visibility = 'enabled';
|
||||
CREATE INDEX IF NOT EXISTS idx_model_catalog_type ON model_catalog(model_type);
|
||||
DROP TRIGGER IF EXISTS model_catalog_updated_at ON model_catalog;
|
||||
CREATE TRIGGER model_catalog_updated_at BEFORE UPDATE ON model_catalog
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
||||
|
||||
COMMENT ON COLUMN model_catalog.visibility IS 'disabled by default — admin must enable for global models';
|
||||
COMMENT ON COLUMN model_catalog.model_type IS 'chat, embedding, image — from provider API sync';
|
||||
COMMENT ON COLUMN model_catalog.capabilities IS 'Intrinsic: {"streaming","tool_calling","vision","thinking","reasoning","code_optimized","web_search","max_context","max_output_tokens"}';
|
||||
|
||||
|
||||
-- =========================================
|
||||
-- MODEL PRICING
|
||||
-- =========================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS model_pricing (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
provider_config_id UUID NOT NULL REFERENCES provider_configs(id) ON DELETE CASCADE,
|
||||
model_id TEXT NOT NULL,
|
||||
input_per_m NUMERIC(10,6),
|
||||
output_per_m NUMERIC(10,6),
|
||||
cache_create_per_m NUMERIC(10,6),
|
||||
cache_read_per_m NUMERIC(10,6),
|
||||
currency TEXT NOT NULL DEFAULT 'USD',
|
||||
source TEXT NOT NULL DEFAULT 'manual',
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_by UUID REFERENCES users(id),
|
||||
UNIQUE(provider_config_id, model_id)
|
||||
);
|
||||
|
||||
|
||||
-- =========================================
|
||||
-- PROVIDER HEALTH
|
||||
-- =========================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS provider_health (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
provider_config_id UUID NOT NULL REFERENCES provider_configs(id) ON DELETE CASCADE,
|
||||
window_start TIMESTAMPTZ NOT NULL,
|
||||
request_count INT NOT NULL DEFAULT 0,
|
||||
error_count INT NOT NULL DEFAULT 0,
|
||||
timeout_count INT NOT NULL DEFAULT 0,
|
||||
total_latency_ms BIGINT NOT NULL DEFAULT 0,
|
||||
max_latency_ms INT NOT NULL DEFAULT 0,
|
||||
rate_limit_count INT NOT NULL DEFAULT 0,
|
||||
last_error TEXT,
|
||||
last_error_at TIMESTAMPTZ,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
|
||||
UNIQUE (provider_config_id, window_start)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_provider_health_window
|
||||
ON provider_health (provider_config_id, window_start DESC);
|
||||
|
||||
|
||||
-- =========================================
|
||||
-- CAPABILITY OVERRIDES
|
||||
-- =========================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS capability_overrides (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
provider_config_id UUID REFERENCES provider_configs(id) ON DELETE CASCADE,
|
||||
model_id TEXT NOT NULL,
|
||||
field TEXT NOT NULL,
|
||||
value TEXT NOT NULL,
|
||||
set_by UUID REFERENCES users(id) ON DELETE SET NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
|
||||
UNIQUE (provider_config_id, model_id, field)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_capability_overrides_model
|
||||
ON capability_overrides (model_id);
|
||||
|
||||
|
||||
-- =========================================
|
||||
-- ROUTING POLICIES
|
||||
-- =========================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS routing_policies (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name VARCHAR(200) NOT NULL,
|
||||
scope VARCHAR(10) NOT NULL DEFAULT 'global'
|
||||
CHECK (scope IN ('global', 'team')),
|
||||
team_id UUID REFERENCES teams(id) ON DELETE CASCADE,
|
||||
priority INTEGER NOT NULL DEFAULT 100,
|
||||
policy_type VARCHAR(30) NOT NULL
|
||||
CHECK (policy_type IN ('provider_prefer', 'team_route', 'cost_limit', 'model_alias', 'capability_match')),
|
||||
config JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_routing_policies_active
|
||||
ON routing_policies(is_active, priority) WHERE is_active = true;
|
||||
CREATE INDEX IF NOT EXISTS idx_routing_policies_team
|
||||
ON routing_policies(team_id) WHERE team_id IS NOT NULL;
|
||||
|
||||
DROP TRIGGER IF EXISTS routing_policies_updated_at ON routing_policies;
|
||||
CREATE TRIGGER routing_policies_updated_at BEFORE UPDATE ON routing_policies
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
||||
@@ -1,151 +0,0 @@
|
||||
-- ==========================================
|
||||
-- Chat Switchboard — 004 Personas & Grants
|
||||
-- ==========================================
|
||||
-- Persona definitions, persona capability grants, and resource access grants.
|
||||
-- ICD §4 (Personas)
|
||||
-- ==========================================
|
||||
|
||||
-- =========================================
|
||||
-- PERSONAS
|
||||
-- =========================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS personas (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name VARCHAR(100) NOT NULL,
|
||||
handle VARCHAR(50),
|
||||
description TEXT DEFAULT '',
|
||||
icon VARCHAR(10) DEFAULT '',
|
||||
avatar TEXT DEFAULT '',
|
||||
|
||||
-- Base model binding
|
||||
base_model_id TEXT NOT NULL,
|
||||
provider_config_id UUID REFERENCES provider_configs(id) ON DELETE SET NULL,
|
||||
|
||||
-- Behavioral configuration
|
||||
system_prompt TEXT DEFAULT '',
|
||||
temperature FLOAT,
|
||||
max_tokens INT,
|
||||
thinking_budget INT,
|
||||
top_p FLOAT,
|
||||
|
||||
-- Memory configuration (v0.18.0)
|
||||
memory_enabled BOOLEAN NOT NULL DEFAULT true,
|
||||
memory_extraction_prompt TEXT,
|
||||
|
||||
-- Scope & ownership
|
||||
scope VARCHAR(10) NOT NULL
|
||||
CHECK (scope IN ('global', 'team', 'personal')),
|
||||
owner_id UUID,
|
||||
created_by UUID NOT NULL REFERENCES users(id),
|
||||
|
||||
-- State
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
is_shared BOOLEAN DEFAULT false,
|
||||
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_personas_scope ON personas(scope);
|
||||
CREATE INDEX IF NOT EXISTS idx_personas_owner ON personas(owner_id) WHERE owner_id IS NOT NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_personas_active ON personas(is_active) WHERE is_active = true;
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_personas_handle ON personas(handle) WHERE handle IS NOT NULL;
|
||||
DROP TRIGGER IF EXISTS personas_updated_at ON personas;
|
||||
CREATE TRIGGER personas_updated_at BEFORE UPDATE ON personas
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
||||
|
||||
COMMENT ON COLUMN personas.scope IS 'global=all users, team=team members, personal=creator only';
|
||||
COMMENT ON COLUMN personas.owner_id IS 'NULL for global; teams.id for team scope; users.id for personal';
|
||||
COMMENT ON COLUMN personas.handle IS 'Unique @mention handle, e.g. veronica-sharpe. Auto-generated from name.';
|
||||
COMMENT ON COLUMN personas.is_shared IS 'Personal personas shared with others (read-only)';
|
||||
COMMENT ON COLUMN personas.memory_enabled IS 'Whether memory tools and extraction are active for this persona';
|
||||
COMMENT ON COLUMN personas.memory_extraction_prompt IS 'Custom extraction prompt; NULL = use system default';
|
||||
|
||||
|
||||
-- =========================================
|
||||
-- PERSONA GROUPS (v0.23.0)
|
||||
-- =========================================
|
||||
-- Saved roster templates. A group is a named set of personas
|
||||
-- that can be stamped onto any new conversation.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS persona_groups (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name VARCHAR(100) NOT NULL,
|
||||
description TEXT DEFAULT '',
|
||||
owner_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
scope VARCHAR(10) NOT NULL DEFAULT 'personal'
|
||||
CHECK (scope IN ('global', 'team', 'personal')),
|
||||
team_id UUID REFERENCES teams(id) ON DELETE CASCADE,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_persona_groups_owner ON persona_groups(owner_id);
|
||||
DROP TRIGGER IF EXISTS persona_groups_updated_at ON persona_groups;
|
||||
CREATE TRIGGER persona_groups_updated_at BEFORE UPDATE ON persona_groups
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
||||
|
||||
CREATE TABLE IF NOT EXISTS persona_group_members (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
group_id UUID NOT NULL REFERENCES persona_groups(id) ON DELETE CASCADE,
|
||||
persona_id UUID NOT NULL REFERENCES personas(id) ON DELETE CASCADE,
|
||||
is_leader BOOLEAN NOT NULL DEFAULT false,
|
||||
sort_order INT NOT NULL DEFAULT 0,
|
||||
UNIQUE(group_id, persona_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_persona_group_members_group ON persona_group_members(group_id);
|
||||
|
||||
|
||||
-- =========================================
|
||||
-- PERSONA GRANTS (what a persona can do)
|
||||
-- =========================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS persona_grants (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
persona_id UUID NOT NULL REFERENCES personas(id) ON DELETE CASCADE,
|
||||
grant_type VARCHAR(30) NOT NULL,
|
||||
grant_ref TEXT NOT NULL,
|
||||
config JSONB DEFAULT '{}'::jsonb,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
UNIQUE(persona_id, grant_type, grant_ref)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_persona_grants_persona ON persona_grants(persona_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_persona_grants_type ON persona_grants(grant_type);
|
||||
|
||||
COMMENT ON TABLE persona_grants IS 'What a persona can do: tools it can call, KBs it can search, etc.';
|
||||
COMMENT ON COLUMN persona_grants.grant_type IS 'Extensible: tool, knowledge_base, api_endpoint';
|
||||
COMMENT ON COLUMN persona_grants.grant_ref IS 'tool: function name. knowledge_base: UUID. api_endpoint: identifier.';
|
||||
|
||||
|
||||
-- =========================================
|
||||
-- RESOURCE GRANTS (who can USE a resource)
|
||||
-- =========================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS resource_grants (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
resource_type VARCHAR(30) NOT NULL
|
||||
CHECK (resource_type IN ('persona', 'knowledge_base', 'project')),
|
||||
resource_id UUID NOT NULL,
|
||||
grant_scope VARCHAR(20) NOT NULL DEFAULT 'team_only'
|
||||
CHECK (grant_scope IN ('team_only', 'global', 'groups')),
|
||||
granted_groups UUID[] NOT NULL DEFAULT '{}',
|
||||
created_by UUID NOT NULL REFERENCES users(id),
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
|
||||
UNIQUE(resource_type, resource_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_resource_grants_resource
|
||||
ON resource_grants(resource_type, resource_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_resource_grants_groups
|
||||
ON resource_grants USING gin(granted_groups);
|
||||
|
||||
DROP TRIGGER IF EXISTS resource_grants_updated_at ON resource_grants;
|
||||
CREATE TRIGGER resource_grants_updated_at BEFORE UPDATE ON resource_grants
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
||||
|
||||
COMMENT ON TABLE resource_grants IS 'Who can USE a resource (personas, KBs, projects). Cross-team access via groups.';
|
||||
COMMENT ON COLUMN resource_grants.grant_scope IS 'team_only: existing team behavior. global: all users. groups: specific group list.';
|
||||
@@ -1,262 +0,0 @@
|
||||
-- ==========================================
|
||||
-- Chat Switchboard — 005 Channels & Conversations
|
||||
-- ==========================================
|
||||
-- Channels (all types, all columns), messages, participants,
|
||||
-- models, cursors, folders, user prefs, session participants.
|
||||
-- Consolidated v0.28.0: merges 005 + 016 (multiuser) + 017 (unread)
|
||||
-- + 021 (sessions) + 024 (workflow instance cols) + 026 (service type)
|
||||
-- + v0.28.0 (kb_auto_inject).
|
||||
--
|
||||
-- Note: project_id/workspace_id FKs added by 010_projects.sql.
|
||||
-- Note: workflow_id FK added by 018_workflows.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', 'dm', 'group', 'channel', 'workflow', 'service')),
|
||||
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 REFERENCES folders(id) ON DELETE SET NULL,
|
||||
folder TEXT, -- legacy text folder (compat)
|
||||
team_id UUID REFERENCES teams(id) ON DELETE SET NULL,
|
||||
settings JSONB DEFAULT '{}'::jsonb,
|
||||
tags TEXT[],
|
||||
|
||||
-- Multi-user (v0.23.1)
|
||||
ai_mode VARCHAR(20) NOT NULL DEFAULT 'auto'
|
||||
CHECK (ai_mode IN ('auto', 'mention_only', 'off')),
|
||||
topic TEXT,
|
||||
|
||||
-- Anonymous sessions (v0.24.3)
|
||||
allow_anonymous BOOLEAN NOT NULL DEFAULT false,
|
||||
|
||||
-- Workflow instance columns (v0.26.0)
|
||||
-- workflow_id FK added by 018_workflows.sql (deferred — table doesn't exist yet)
|
||||
workflow_id UUID,
|
||||
workflow_version INTEGER,
|
||||
current_stage INTEGER DEFAULT 0,
|
||||
stage_data JSONB DEFAULT '{}'::jsonb,
|
||||
workflow_status TEXT DEFAULT 'active'
|
||||
CHECK (workflow_status IN ('active', 'completed', 'stale', 'cancelled')),
|
||||
last_activity_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
stage_entered_at TIMESTAMPTZ,
|
||||
|
||||
-- KB auto-injection (v0.28.0)
|
||||
kb_auto_inject BOOLEAN NOT NULL DEFAULT false,
|
||||
|
||||
-- Cross-domain FKs (added by 010_projects.sql)
|
||||
project_id UUID,
|
||||
workspace_id UUID,
|
||||
|
||||
-- Retention (v0.37.14)
|
||||
purge_after TIMESTAMPTZ,
|
||||
|
||||
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);
|
||||
CREATE INDEX IF NOT EXISTS idx_channels_folder ON channels(folder_id) WHERE folder_id IS NOT NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_channels_project ON channels(project_id) WHERE project_id IS NOT NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_channels_workspace ON channels(workspace_id) WHERE workspace_id IS NOT NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_channels_workflow ON channels(workflow_id) WHERE workflow_id IS NOT NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_channels_workflow_status ON channels(workflow_status) WHERE workflow_status = 'active';
|
||||
CREATE INDEX IF NOT EXISTS idx_channels_purge_after ON channels(purge_after) WHERE purge_after IS NOT NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_channels_workflow_active ON channels(workflow_id, workflow_status) WHERE workflow_id IS NOT NULL AND workflow_status = 'active';
|
||||
|
||||
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();
|
||||
|
||||
COMMENT ON COLUMN channels.type IS 'direct=single-user, dm=human-to-human, group=multi-participant, channel=named persistent, workflow=staged, service=autonomous task';
|
||||
COMMENT ON COLUMN channels.ai_mode IS 'auto=respond always, mention_only=@mention required, off=AI disabled';
|
||||
COMMENT ON COLUMN channels.topic IS 'Short channel description shown in header';
|
||||
COMMENT ON COLUMN channels.allow_anonymous IS 'When true, unauthenticated visitors can join via session token';
|
||||
COMMENT ON COLUMN channels.workflow_id IS 'FK to workflows — set when type=workflow (constraint added by 018)';
|
||||
COMMENT ON COLUMN channels.workflow_version IS 'Pinned version number from workflow_versions';
|
||||
COMMENT ON COLUMN channels.current_stage IS 'Ordinal of the active workflow stage';
|
||||
COMMENT ON COLUMN channels.stage_data IS 'Accumulated form data collected across stages';
|
||||
COMMENT ON COLUMN channels.workflow_status IS 'active/completed/stale/cancelled';
|
||||
COMMENT ON COLUMN channels.last_activity_at IS 'Updated on each message — used by staleness sweep';
|
||||
COMMENT ON COLUMN channels.stage_entered_at IS 'Timestamp when current workflow stage was entered — used for SLA computation';
|
||||
COMMENT ON COLUMN channels.kb_auto_inject IS 'When true, top-K KB chunks auto-prepend to context';
|
||||
|
||||
|
||||
-- =========================================
|
||||
-- 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
|
||||
-- =========================================
|
||||
|
||||
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(),
|
||||
last_read_message_id UUID, -- v0.23.2: unread cursor
|
||||
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()
|
||||
);
|
||||
|
||||
-- Raw model entries (no persona): unique per channel+model+provider
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_channel_models_raw
|
||||
ON channel_models(channel_id, model_id, provider_config_id) WHERE persona_id IS NULL;
|
||||
-- Persona entries: one per persona per channel (same model allowed for different personas)
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_channel_models_persona
|
||||
ON channel_models(channel_id, persona_id) WHERE persona_id IS NOT NULL;
|
||||
|
||||
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 NOT NULL 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();
|
||||
|
||||
|
||||
-- =========================================
|
||||
-- SESSION PARTICIPANTS (v0.24.3)
|
||||
-- =========================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS session_participants (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
session_token TEXT NOT NULL UNIQUE,
|
||||
channel_id UUID NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
|
||||
display_name TEXT NOT NULL DEFAULT 'Visitor',
|
||||
fingerprint TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_session_participants_channel
|
||||
ON session_participants(channel_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_session_participants_token
|
||||
ON session_participants(session_token);
|
||||
|
||||
COMMENT ON TABLE session_participants IS 'Ephemeral identities for anonymous workflow channel visitors.';
|
||||
@@ -1,114 +0,0 @@
|
||||
-- ==========================================
|
||||
-- Chat Switchboard — 006 Knowledge Bases
|
||||
-- ==========================================
|
||||
-- Knowledge bases, documents, chunks, and bindings to channels/personas.
|
||||
-- ICD §5 (Knowledge Bases)
|
||||
-- ==========================================
|
||||
|
||||
-- =========================================
|
||||
-- KNOWLEDGE BASES
|
||||
-- =========================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS knowledge_bases (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name TEXT NOT NULL,
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
scope TEXT NOT NULL DEFAULT 'global',
|
||||
owner_id UUID REFERENCES users(id) ON DELETE CASCADE,
|
||||
team_id UUID REFERENCES teams(id) ON DELETE CASCADE,
|
||||
embedding_config JSONB NOT NULL DEFAULT '{}',
|
||||
document_count INT NOT NULL DEFAULT 0,
|
||||
chunk_count INT NOT NULL DEFAULT 0,
|
||||
total_bytes BIGINT NOT NULL DEFAULT 0,
|
||||
status TEXT NOT NULL DEFAULT 'active',
|
||||
discoverable BOOLEAN NOT NULL DEFAULT true,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
|
||||
CONSTRAINT kb_scope_check CHECK (
|
||||
(scope = 'global' AND owner_id IS NULL) OR
|
||||
(scope = 'team' AND team_id IS NOT NULL) OR
|
||||
(scope = 'personal' AND owner_id IS NOT NULL)
|
||||
)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_kb_scope ON knowledge_bases(scope);
|
||||
CREATE INDEX IF NOT EXISTS idx_kb_owner ON knowledge_bases(owner_id) WHERE owner_id IS NOT NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_kb_team ON knowledge_bases(team_id) WHERE team_id IS NOT NULL;
|
||||
|
||||
COMMENT ON COLUMN knowledge_bases.discoverable IS 'false = hidden from user KB lists, only accessible through persona binding';
|
||||
|
||||
|
||||
-- =========================================
|
||||
-- KB DOCUMENTS
|
||||
-- =========================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS kb_documents (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
kb_id UUID NOT NULL REFERENCES knowledge_bases(id) ON DELETE CASCADE,
|
||||
filename TEXT NOT NULL,
|
||||
content_type TEXT NOT NULL,
|
||||
size_bytes BIGINT NOT NULL,
|
||||
storage_key TEXT NOT NULL,
|
||||
extracted_text TEXT,
|
||||
chunk_count INT NOT NULL DEFAULT 0,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
error TEXT,
|
||||
uploaded_by UUID NOT NULL REFERENCES users(id),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_kbdoc_kb ON kb_documents(kb_id);
|
||||
|
||||
|
||||
-- =========================================
|
||||
-- KB CHUNKS
|
||||
-- =========================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS kb_chunks (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
kb_id UUID NOT NULL REFERENCES knowledge_bases(id) ON DELETE CASCADE,
|
||||
document_id UUID NOT NULL REFERENCES kb_documents(id) ON DELETE CASCADE,
|
||||
chunk_index INT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
token_count INT NOT NULL DEFAULT 0,
|
||||
embedding VECTOR(3072),
|
||||
metadata JSONB NOT NULL DEFAULT '{}',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_kbchunk_kb ON kb_chunks(kb_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_kbchunk_doc ON kb_chunks(document_id);
|
||||
|
||||
|
||||
-- =========================================
|
||||
-- CHANNEL ↔ KB BINDING
|
||||
-- =========================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS channel_knowledge_bases (
|
||||
channel_id UUID NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
|
||||
kb_id UUID NOT NULL REFERENCES knowledge_bases(id) ON DELETE CASCADE,
|
||||
enabled BOOLEAN NOT NULL DEFAULT true,
|
||||
added_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (channel_id, kb_id)
|
||||
);
|
||||
|
||||
|
||||
-- =========================================
|
||||
-- PERSONA ↔ KB BINDING (v0.17.0)
|
||||
-- =========================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS persona_knowledge_bases (
|
||||
persona_id UUID NOT NULL REFERENCES personas(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 (persona_id, kb_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_persona_kb_persona ON persona_knowledge_bases(persona_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_persona_kb_kb ON persona_knowledge_bases(kb_id);
|
||||
|
||||
COMMENT ON TABLE persona_knowledge_bases IS 'Binds KBs to personas — the persona becomes a gateway to its KBs';
|
||||
COMMENT ON COLUMN persona_knowledge_bases.auto_search IS 'true = auto-prepend top-K results to context; false = kb_search tool only';
|
||||
@@ -1,75 +0,0 @@
|
||||
-- ==========================================
|
||||
-- Chat Switchboard — 007 Notes & Links
|
||||
-- ==========================================
|
||||
-- Notes with full-text search, wikilink graph edges.
|
||||
-- ICD §6 (Notes)
|
||||
-- ==========================================
|
||||
|
||||
-- =========================================
|
||||
-- NOTES
|
||||
-- =========================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS notes (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
title VARCHAR(500) NOT NULL,
|
||||
content TEXT DEFAULT '',
|
||||
folder_path TEXT DEFAULT '/',
|
||||
tags TEXT[] DEFAULT '{}',
|
||||
metadata JSONB DEFAULT '{}',
|
||||
source_channel_id UUID REFERENCES channels(id) ON DELETE SET NULL,
|
||||
source_message_id UUID,
|
||||
team_id UUID REFERENCES teams(id) ON DELETE SET NULL,
|
||||
search_vector TSVECTOR,
|
||||
embedding VECTOR(3072),
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_notes_user ON notes(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_notes_search ON notes USING GIN(search_vector);
|
||||
CREATE INDEX IF NOT EXISTS idx_notes_folder ON notes(user_id, folder_path);
|
||||
CREATE INDEX IF NOT EXISTS idx_notes_tags ON notes USING GIN(tags);
|
||||
CREATE INDEX IF NOT EXISTS idx_notes_updated ON notes(user_id, updated_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_notes_team ON notes(team_id) WHERE team_id IS NOT NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_notes_source_msg ON notes(source_message_id)
|
||||
WHERE source_message_id IS NOT NULL;
|
||||
|
||||
CREATE OR REPLACE FUNCTION notes_search_update_fn()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.search_vector :=
|
||||
setweight(to_tsvector('pg_catalog.english', COALESCE(NEW.title, '')), 'A') ||
|
||||
setweight(to_tsvector('pg_catalog.english', COALESCE(NEW.content, '')), 'B');
|
||||
NEW.updated_at := NOW();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
DROP TRIGGER IF EXISTS notes_search_update ON notes;
|
||||
CREATE TRIGGER notes_search_update
|
||||
BEFORE INSERT OR UPDATE OF title, content ON notes
|
||||
FOR EACH ROW EXECUTE FUNCTION notes_search_update_fn();
|
||||
|
||||
|
||||
-- =========================================
|
||||
-- NOTE LINKS (v0.17.3 — wikilink graph)
|
||||
-- =========================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS note_links (
|
||||
source_note_id UUID NOT NULL REFERENCES notes(id) ON DELETE CASCADE,
|
||||
target_note_id UUID REFERENCES notes(id) ON DELETE CASCADE,
|
||||
target_title TEXT NOT NULL,
|
||||
display_text TEXT,
|
||||
is_transclusion BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
created_at TIMESTAMPTZ DEFAULT now(),
|
||||
PRIMARY KEY (source_note_id, target_title)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_note_links_target ON note_links(target_note_id)
|
||||
WHERE target_note_id IS NOT NULL;
|
||||
|
||||
COMMENT ON TABLE note_links IS 'Directed links between notes, extracted from [[wikilink]] syntax on save';
|
||||
COMMENT ON COLUMN note_links.target_note_id IS 'NULL for unresolved links (target note does not exist yet)';
|
||||
COMMENT ON COLUMN note_links.target_title IS 'Raw [[title]] text — preserved for re-resolution after renames';
|
||||
COMMENT ON COLUMN note_links.is_transclusion IS 'true for ![[embed]] syntax, false for regular [[link]]';
|
||||
@@ -1,89 +0,0 @@
|
||||
-- ==========================================
|
||||
-- Chat Switchboard — 008 Memory
|
||||
-- ==========================================
|
||||
-- Long-term memory across conversations, extraction tracking.
|
||||
-- Consolidated v0.28.0: adds last_compacted_at + decay_rate.
|
||||
-- ==========================================
|
||||
|
||||
-- =========================================
|
||||
-- MEMORIES
|
||||
-- =========================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS memories (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
scope TEXT NOT NULL CHECK (scope IN ('user', 'persona', 'persona_user')),
|
||||
owner_id UUID NOT NULL,
|
||||
user_id UUID,
|
||||
key TEXT NOT NULL,
|
||||
value TEXT NOT NULL,
|
||||
source_channel_id UUID,
|
||||
confidence REAL NOT NULL DEFAULT 1.0,
|
||||
status TEXT NOT NULL DEFAULT 'active'
|
||||
CHECK (status IN ('active', 'pending_review', 'archived')),
|
||||
embedding vector(3072),
|
||||
|
||||
-- Compaction support (v0.28.0)
|
||||
last_compacted_at TIMESTAMPTZ,
|
||||
decay_rate REAL NOT NULL DEFAULT 0.0,
|
||||
|
||||
created_at TIMESTAMPTZ DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_memories_scope_owner
|
||||
ON memories(scope, owner_id, status)
|
||||
WHERE status = 'active';
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_memories_persona_user
|
||||
ON memories(owner_id, user_id)
|
||||
WHERE scope = 'persona_user' AND status = 'active';
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_memories_unique_key
|
||||
ON memories(scope, owner_id, COALESCE(user_id, '00000000-0000-0000-0000-000000000000'), key);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_memories_fts
|
||||
ON memories USING gin(to_tsvector('english', key || ' ' || value));
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_memories_source_channel
|
||||
ON memories(source_channel_id)
|
||||
WHERE source_channel_id IS NOT NULL;
|
||||
|
||||
COMMENT ON TABLE memories IS 'Long-term memory facts persisted across conversations';
|
||||
COMMENT ON COLUMN memories.scope IS 'user = personal facts; persona = shared; persona_user = per-user within a persona';
|
||||
COMMENT ON COLUMN memories.owner_id IS 'user_id for user scope, persona_id for persona/persona_user scopes';
|
||||
COMMENT ON COLUMN memories.confidence IS '0.0-1.0 confidence score — higher = more certain';
|
||||
COMMENT ON COLUMN memories.last_compacted_at IS 'Last time this memory was evaluated by the compaction process';
|
||||
COMMENT ON COLUMN memories.decay_rate IS 'Rate at which confidence decreases over time (0.0 = no decay)';
|
||||
|
||||
CREATE OR REPLACE FUNCTION update_memories_updated_at()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = now();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_memories_updated_at ON memories;
|
||||
CREATE TRIGGER trg_memories_updated_at
|
||||
BEFORE UPDATE ON memories
|
||||
FOR EACH ROW EXECUTE FUNCTION update_memories_updated_at();
|
||||
|
||||
|
||||
-- =========================================
|
||||
-- MEMORY EXTRACTION LOG
|
||||
-- =========================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS memory_extraction_log (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
channel_id UUID NOT NULL,
|
||||
user_id UUID NOT NULL,
|
||||
last_message_id UUID NOT NULL,
|
||||
extracted_at TIMESTAMPTZ DEFAULT now(),
|
||||
memory_count INT NOT NULL DEFAULT 0,
|
||||
UNIQUE(channel_id, user_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_mem_extract_log_channel
|
||||
ON memory_extraction_log(channel_id);
|
||||
|
||||
COMMENT ON TABLE memory_extraction_log IS 'Tracks extraction progress per channel+user to prevent re-processing';
|
||||
@@ -1,126 +0,0 @@
|
||||
-- ==========================================
|
||||
-- Chat Switchboard — 009 Workspaces & Git
|
||||
-- ==========================================
|
||||
-- File workspaces, workspace file index, semantic chunks, git credentials.
|
||||
-- ICD §7 (Workspaces & Git)
|
||||
-- ==========================================
|
||||
|
||||
-- =========================================
|
||||
-- WORKSPACES
|
||||
-- =========================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS workspaces (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
owner_type VARCHAR(20) NOT NULL
|
||||
CHECK (owner_type IN ('user', 'project', 'channel', 'team')),
|
||||
owner_id UUID NOT NULL,
|
||||
name VARCHAR(200) NOT NULL,
|
||||
root_path TEXT NOT NULL,
|
||||
max_bytes BIGINT,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'active'
|
||||
CHECK (status IN ('active', 'archived', 'deleting')),
|
||||
indexing_enabled BOOLEAN NOT NULL DEFAULT true,
|
||||
-- Git integration (v0.21.4)
|
||||
git_remote_url TEXT,
|
||||
git_branch TEXT,
|
||||
git_credential_id UUID,
|
||||
git_last_sync TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_workspaces_owner ON workspaces(owner_type, owner_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_workspaces_status ON workspaces(status) WHERE status != 'deleting';
|
||||
DROP TRIGGER IF EXISTS workspaces_updated_at ON workspaces;
|
||||
CREATE TRIGGER workspaces_updated_at BEFORE UPDATE ON workspaces
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
||||
|
||||
COMMENT ON TABLE workspaces IS 'File workspaces bound to users, projects, channels, or teams';
|
||||
COMMENT ON COLUMN workspaces.root_path IS 'PVC-relative path to workspace root directory';
|
||||
|
||||
|
||||
-- =========================================
|
||||
-- WORKSPACE FILES
|
||||
-- =========================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS workspace_files (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
workspace_id UUID NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
|
||||
path TEXT NOT NULL,
|
||||
is_directory BOOLEAN NOT NULL DEFAULT false,
|
||||
content_type VARCHAR(100),
|
||||
size_bytes BIGINT NOT NULL DEFAULT 0,
|
||||
sha256 VARCHAR(64),
|
||||
index_status VARCHAR(20) DEFAULT 'pending'
|
||||
CHECK (index_status IN ('pending', 'indexing', 'ready', 'error', 'skipped')),
|
||||
chunk_count INT NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_workspace_files_path
|
||||
ON workspace_files(workspace_id, path);
|
||||
CREATE INDEX IF NOT EXISTS idx_workspace_files_workspace ON workspace_files(workspace_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_workspace_files_type
|
||||
ON workspace_files(workspace_id, content_type) WHERE content_type IS NOT NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_workspace_files_index_status
|
||||
ON workspace_files(workspace_id, index_status)
|
||||
WHERE index_status IN ('pending', 'indexing');
|
||||
|
||||
DROP TRIGGER IF EXISTS workspace_files_updated_at ON workspace_files;
|
||||
CREATE TRIGGER workspace_files_updated_at BEFORE UPDATE ON workspace_files
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
||||
|
||||
COMMENT ON TABLE workspace_files IS 'Metadata index for workspace files — filesystem is source of truth';
|
||||
|
||||
|
||||
-- =========================================
|
||||
-- WORKSPACE CHUNKS (v0.21.2)
|
||||
-- =========================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS workspace_chunks (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
workspace_id UUID NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
|
||||
file_id UUID NOT NULL REFERENCES workspace_files(id) ON DELETE CASCADE,
|
||||
chunk_index INT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
token_count INT NOT NULL DEFAULT 0,
|
||||
embedding vector(3072),
|
||||
metadata JSONB DEFAULT '{}',
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_workspace_chunks_file ON workspace_chunks(file_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_workspace_chunks_workspace ON workspace_chunks(workspace_id);
|
||||
|
||||
COMMENT ON TABLE workspace_chunks IS 'Text chunks with vector embeddings for workspace file semantic search';
|
||||
|
||||
|
||||
-- =========================================
|
||||
-- GIT CREDENTIALS (v0.21.4)
|
||||
-- =========================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS git_credentials (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL DEFAULT '',
|
||||
auth_type TEXT NOT NULL DEFAULT 'https_pat',
|
||||
encrypted_data BYTEA NOT NULL DEFAULT '',
|
||||
nonce BYTEA NOT NULL DEFAULT '',
|
||||
public_key TEXT NOT NULL DEFAULT '',
|
||||
fingerprint TEXT NOT NULL DEFAULT '',
|
||||
persona_id UUID REFERENCES personas(id) ON DELETE SET NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_git_credentials_user ON git_credentials(user_id);
|
||||
|
||||
-- Deferred FK: workspaces.git_credential_id → git_credentials.id
|
||||
DO $$ BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint WHERE conname = 'fk_workspaces_git_credential'
|
||||
) THEN
|
||||
ALTER TABLE workspaces ADD CONSTRAINT fk_workspaces_git_credential
|
||||
FOREIGN KEY (git_credential_id) REFERENCES git_credentials(id) ON DELETE SET NULL;
|
||||
END IF;
|
||||
END $$;
|
||||
@@ -1,152 +0,0 @@
|
||||
-- ==========================================
|
||||
-- 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
|
||||
$$;
|
||||
@@ -1,8 +0,0 @@
|
||||
-- ==========================================
|
||||
-- Chat Switchboard — 012 (placeholder)
|
||||
-- ==========================================
|
||||
-- Previously: extensions + extension_user_settings tables.
|
||||
-- v0.28.7: Both tables removed. Unified package registry in 016.
|
||||
-- package_user_settings also in 016 (FK dependency on packages).
|
||||
-- This file is intentionally empty to preserve migration numbering.
|
||||
-- ==========================================
|
||||
@@ -1,33 +0,0 @@
|
||||
-- ==========================================
|
||||
-- Chat Switchboard — 013 Files
|
||||
-- ==========================================
|
||||
-- Unified files table. Fresh install — no legacy attachments migration.
|
||||
-- Consolidated v0.28.0: removes attachments→files migration dance.
|
||||
-- ==========================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS files (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
channel_id UUID REFERENCES channels(id) ON DELETE CASCADE,
|
||||
message_id UUID REFERENCES messages(id) ON DELETE SET NULL,
|
||||
user_id UUID REFERENCES users(id) ON DELETE SET NULL,
|
||||
project_id UUID REFERENCES projects(id) ON DELETE SET NULL,
|
||||
origin VARCHAR(20) NOT NULL DEFAULT 'user_upload'
|
||||
CHECK (origin IN ('user_upload', 'tool_output', 'system')),
|
||||
filename VARCHAR(255) NOT NULL,
|
||||
content_type VARCHAR(127) NOT NULL DEFAULT 'application/octet-stream',
|
||||
size_bytes BIGINT NOT NULL DEFAULT 0,
|
||||
storage_key TEXT NOT NULL,
|
||||
display_hint VARCHAR(20) NOT NULL DEFAULT 'download'
|
||||
CHECK (display_hint IN ('inline', 'download', 'thumbnail')),
|
||||
extracted_text TEXT,
|
||||
metadata JSONB DEFAULT '{}'::jsonb,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_files_channel ON files(channel_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_files_message ON files(message_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_files_user ON files(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_files_project ON files(project_id) WHERE project_id IS NOT NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_files_origin ON files(origin);
|
||||
CREATE INDEX IF NOT EXISTS idx_files_orphan ON files(created_at) WHERE message_id IS NULL AND origin = 'user_upload';
|
||||
@@ -1,58 +0,0 @@
|
||||
-- ==========================================
|
||||
-- Chat Switchboard — 014 Audit & Usage
|
||||
-- ==========================================
|
||||
-- Audit trail and usage/cost tracking.
|
||||
-- ICD §16 (Platform Administration), §17 (Export & Utility)
|
||||
-- ==========================================
|
||||
|
||||
-- =========================================
|
||||
-- AUDIT LOG
|
||||
-- =========================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS audit_log (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
actor_id UUID REFERENCES users(id) ON DELETE SET NULL,
|
||||
action VARCHAR(100) NOT NULL,
|
||||
resource_type VARCHAR(50) NOT NULL,
|
||||
resource_id VARCHAR(255),
|
||||
metadata JSONB DEFAULT '{}'::jsonb,
|
||||
ip_address VARCHAR(45),
|
||||
user_agent TEXT DEFAULT '',
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_log_created ON audit_log(created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_log_actor ON audit_log(actor_id) WHERE actor_id IS NOT NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_log_resource ON audit_log(resource_type, resource_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_log_action ON audit_log(action);
|
||||
|
||||
COMMENT ON TABLE audit_log IS 'Immutable audit trail of all mutating operations';
|
||||
|
||||
|
||||
-- =========================================
|
||||
-- USAGE LOG
|
||||
-- =========================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS usage_log (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
channel_id UUID REFERENCES channels(id) ON DELETE SET NULL,
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
provider_config_id UUID REFERENCES provider_configs(id) ON DELETE SET NULL,
|
||||
provider_scope TEXT NOT NULL DEFAULT 'global',
|
||||
model_id TEXT NOT NULL,
|
||||
role TEXT,
|
||||
prompt_tokens INT NOT NULL DEFAULT 0,
|
||||
completion_tokens INT NOT NULL DEFAULT 0,
|
||||
cache_creation_tokens INT NOT NULL DEFAULT 0,
|
||||
cache_read_tokens INT NOT NULL DEFAULT 0,
|
||||
cost_input NUMERIC(12,6),
|
||||
cost_output NUMERIC(12,6),
|
||||
routing_decision JSONB,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_usage_log_user ON usage_log(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_usage_log_created ON usage_log(created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_usage_log_provider ON usage_log(provider_config_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_usage_log_model ON usage_log(model_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_usage_log_scope ON usage_log(provider_scope);
|
||||
@@ -1,23 +0,0 @@
|
||||
-- ==========================================
|
||||
-- Chat Switchboard — 015 Tool Health
|
||||
-- ==========================================
|
||||
-- Health tracking for built-in tools (web_search, url_fetch, etc.).
|
||||
-- ==========================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tool_health (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tool_name TEXT NOT NULL,
|
||||
window_start TIMESTAMPTZ NOT NULL,
|
||||
request_count INT NOT NULL DEFAULT 0,
|
||||
error_count INT NOT NULL DEFAULT 0,
|
||||
total_latency_ms BIGINT NOT NULL DEFAULT 0,
|
||||
max_latency_ms INT NOT NULL DEFAULT 0,
|
||||
last_error TEXT,
|
||||
last_error_at TIMESTAMPTZ,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
|
||||
UNIQUE (tool_name, window_start)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_tool_health_window
|
||||
ON tool_health (tool_name, window_start DESC);
|
||||
@@ -1,129 +0,0 @@
|
||||
-- ==========================================
|
||||
-- Chat Switchboard — 016 Packages
|
||||
-- ==========================================
|
||||
-- Unified package registry. Replaces surface_registry (v0.25.0)
|
||||
-- and extensions (v0.11.0) tables. A package is a surface, an
|
||||
-- extension, or both.
|
||||
--
|
||||
-- v0.28.7: packages table + package_user_settings in 012.
|
||||
-- v0.29.0: status column + extension_permissions table.
|
||||
-- v0.29.2: ext_data_tables catalog + platform read views.
|
||||
-- ==========================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS packages (
|
||||
id TEXT PRIMARY KEY,
|
||||
title TEXT NOT NULL,
|
||||
type TEXT NOT NULL DEFAULT 'surface'
|
||||
CHECK (type IN ('surface', 'extension', 'full', 'workflow')),
|
||||
version TEXT NOT NULL DEFAULT '0.0.0',
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
author TEXT NOT NULL DEFAULT '',
|
||||
tier TEXT NOT NULL DEFAULT 'browser'
|
||||
CHECK (tier IN ('browser', 'starlark', 'sidecar')),
|
||||
is_system BOOLEAN NOT NULL DEFAULT false,
|
||||
scope TEXT NOT NULL DEFAULT 'global'
|
||||
CHECK (scope IN ('global', 'team', 'personal')),
|
||||
team_id UUID REFERENCES teams(id) ON DELETE CASCADE,
|
||||
installed_by UUID REFERENCES users(id) ON DELETE SET NULL,
|
||||
manifest JSONB NOT NULL DEFAULT '{}',
|
||||
enabled BOOLEAN NOT NULL DEFAULT true,
|
||||
status TEXT NOT NULL DEFAULT 'active'
|
||||
CHECK (status IN ('active', 'pending_review', 'suspended')),
|
||||
schema_version INTEGER NOT NULL DEFAULT 0,
|
||||
package_settings JSONB NOT NULL DEFAULT '{}',
|
||||
source TEXT NOT NULL DEFAULT 'core'
|
||||
CHECK (source IN ('core', 'builtin', 'extension', 'registry')),
|
||||
installed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_packages_type ON packages(type);
|
||||
CREATE INDEX IF NOT EXISTS idx_packages_enabled ON packages(enabled) WHERE enabled = true;
|
||||
CREATE INDEX IF NOT EXISTS idx_packages_team ON packages(team_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_packages_source ON packages(source);
|
||||
CREATE INDEX IF NOT EXISTS idx_packages_status ON packages(status);
|
||||
|
||||
COMMENT ON TABLE packages IS 'Unified package registry. Surfaces, extensions, and full packages. Replaces surface_registry + extensions tables.';
|
||||
COMMENT ON COLUMN packages.id IS 'Slug identifier from manifest "id" field. Used in URLs: /s/:id';
|
||||
COMMENT ON COLUMN packages.type IS 'surface = routable page, extension = hooks/tools/pipes, full = both, workflow = bundled workflow definition';
|
||||
COMMENT ON COLUMN packages.source IS 'core = page-engine seeded, builtin = extensions/builtin/ seeded, extension = admin-uploaded .pkg';
|
||||
COMMENT ON COLUMN packages.enabled IS 'Admin toggle — disabled surfaces redirect to / and hide from nav';
|
||||
COMMENT ON COLUMN packages.status IS 'Lifecycle: active (running), pending_review (needs admin permission grant), suspended (permission revoked)';
|
||||
|
||||
|
||||
-- =========================================
|
||||
-- PACKAGE USER SETTINGS
|
||||
-- =========================================
|
||||
-- Per-user overrides for packages (enable/disable, custom config).
|
||||
-- Replaces extension_user_settings from the former extensions schema.
|
||||
-- Lives in 016 (not 012) because of FK dependency on packages table.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS package_user_settings (
|
||||
package_id TEXT NOT NULL REFERENCES packages(id) ON DELETE CASCADE,
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
settings JSONB NOT NULL DEFAULT '{}',
|
||||
is_enabled BOOLEAN NOT NULL DEFAULT true,
|
||||
PRIMARY KEY (package_id, user_id)
|
||||
);
|
||||
|
||||
|
||||
-- =========================================
|
||||
-- EXTENSION PERMISSIONS
|
||||
-- =========================================
|
||||
-- Declared capabilities from package manifests. Admin grants control
|
||||
-- which modules the Starlark sandbox injects at runtime.
|
||||
-- v0.29.0: Permission model for extension sandboxing.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS extension_permissions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
package_id TEXT NOT NULL REFERENCES packages(id) ON DELETE CASCADE,
|
||||
permission TEXT NOT NULL,
|
||||
granted BOOLEAN NOT NULL DEFAULT false,
|
||||
granted_by UUID REFERENCES users(id) ON DELETE SET NULL,
|
||||
granted_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
UNIQUE(package_id, permission)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_ext_perm_package ON extension_permissions(package_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_ext_perm_granted ON extension_permissions(granted) WHERE granted = true;
|
||||
|
||||
COMMENT ON TABLE extension_permissions IS 'Declared permissions from package manifests. Admin grants control runtime module injection.';
|
||||
COMMENT ON COLUMN extension_permissions.permission IS 'Capability key: secrets.read, notifications.send, filters.pre_completion, db.read, db.write, api.http';
|
||||
COMMENT ON COLUMN extension_permissions.granted IS 'Admin has reviewed and approved this capability for the package';
|
||||
|
||||
|
||||
-- =========================================
|
||||
-- EXTENSION DATA TABLES CATALOG
|
||||
-- =========================================
|
||||
-- Tracks namespaced tables created for each extension on install.
|
||||
-- Used by uninstall hooks (DROP TABLE) and db.list_tables().
|
||||
-- Table names stored without the ext_{id}_ prefix.
|
||||
-- v0.29.2: Extension data storage.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ext_data_tables (
|
||||
package_id TEXT NOT NULL REFERENCES packages(id) ON DELETE CASCADE,
|
||||
table_name TEXT NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
PRIMARY KEY (package_id, table_name)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_ext_data_tables_pkg ON ext_data_tables(package_id);
|
||||
|
||||
COMMENT ON TABLE ext_data_tables IS 'Catalog of namespaced tables created by extension install hooks. Drives uninstall cleanup and db.list_tables().';
|
||||
COMMENT ON COLUMN ext_data_tables.table_name IS 'Logical name (without ext_{id}_ prefix). Physical table is ext_{package_id}_{table_name}.';
|
||||
|
||||
|
||||
-- =========================================
|
||||
-- PLATFORM VIEWS (extension read access)
|
||||
-- =========================================
|
||||
-- Column-allowlisted views over core platform tables.
|
||||
-- Extensions query these via db.view("users"), db.view("channels").
|
||||
-- Only safe, non-sensitive columns are exposed.
|
||||
-- v0.29.2: Read contract for cross-platform data access.
|
||||
|
||||
CREATE OR REPLACE VIEW ext_view_users AS
|
||||
SELECT id, display_name, email FROM users;
|
||||
|
||||
CREATE OR REPLACE VIEW ext_view_channels AS
|
||||
SELECT id, title, type, team_id FROM channels;
|
||||
@@ -1,18 +0,0 @@
|
||||
-- ==========================================
|
||||
-- Chat Switchboard — 017 Auth Providers
|
||||
-- ==========================================
|
||||
-- OIDC authorization state tracking (ephemeral).
|
||||
-- Consolidated v0.28.0: renumbered from 019. groups.source moved to 002.
|
||||
-- ==========================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS oidc_auth_state (
|
||||
state TEXT PRIMARY KEY,
|
||||
nonce TEXT NOT NULL,
|
||||
redirect_to TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_oidc_state_created
|
||||
ON oidc_auth_state(created_at);
|
||||
|
||||
COMMENT ON TABLE oidc_auth_state IS 'Ephemeral OIDC authorization code flow state. Rows deleted after callback.';
|
||||
@@ -1,149 +0,0 @@
|
||||
-- ==========================================
|
||||
-- Chat Switchboard — 018 Workflows
|
||||
-- ==========================================
|
||||
-- Workflow definitions, stages, version snapshots, assignments.
|
||||
-- Consolidated v0.28.0: merges 023 + 025 (assignments) + 027 (webhooks).
|
||||
-- Also adds deferred FK: channels.workflow_id → workflows.id.
|
||||
-- ==========================================
|
||||
|
||||
-- =========================================
|
||||
-- WORKFLOW DEFINITIONS
|
||||
-- =========================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS workflows (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
team_id UUID REFERENCES teams(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
slug TEXT NOT NULL,
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
branding JSONB NOT NULL DEFAULT '{}',
|
||||
entry_mode TEXT NOT NULL DEFAULT 'public_link'
|
||||
CHECK (entry_mode IN ('public_link', 'team_only')),
|
||||
is_active BOOLEAN NOT NULL DEFAULT false,
|
||||
version INTEGER NOT NULL DEFAULT 1,
|
||||
on_complete JSONB,
|
||||
retention JSONB NOT NULL DEFAULT '{"mode": "archive"}',
|
||||
|
||||
-- Webhooks (v0.27.3)
|
||||
webhook_url TEXT,
|
||||
webhook_secret TEXT,
|
||||
|
||||
created_by UUID NOT NULL REFERENCES users(id),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_workflows_team_slug
|
||||
ON workflows(team_id, slug) WHERE team_id IS NOT NULL;
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_workflows_global_slug
|
||||
ON workflows(slug) WHERE team_id IS NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_workflows_active
|
||||
ON workflows(is_active) WHERE is_active = true;
|
||||
|
||||
DROP TRIGGER IF EXISTS workflows_updated_at ON workflows;
|
||||
CREATE TRIGGER workflows_updated_at
|
||||
BEFORE UPDATE ON workflows
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
||||
|
||||
COMMENT ON TABLE workflows IS 'Team-owned or global workflow definitions with staged processes.';
|
||||
COMMENT ON COLUMN workflows.slug IS 'URL-safe identifier, unique within scope (team or global).';
|
||||
COMMENT ON COLUMN workflows.branding IS '{"accent_color": "#hex", "logo_url": "...", "tagline": "..."}';
|
||||
COMMENT ON COLUMN workflows.on_complete IS 'Chaining hook — triggers another workflow on completion.';
|
||||
COMMENT ON COLUMN workflows.retention IS '{"mode": "archive"|"delete", "delete_after_days": N}';
|
||||
|
||||
|
||||
-- =========================================
|
||||
-- WORKFLOW STAGES
|
||||
-- =========================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS workflow_stages (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
workflow_id UUID NOT NULL REFERENCES workflows(id) ON DELETE CASCADE,
|
||||
ordinal INTEGER NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
persona_id UUID REFERENCES personas(id) ON DELETE SET NULL,
|
||||
assignment_team_id UUID REFERENCES teams(id) ON DELETE SET NULL,
|
||||
form_template JSONB NOT NULL DEFAULT '{}',
|
||||
stage_mode TEXT NOT NULL DEFAULT 'chat_only'
|
||||
CHECK (stage_mode IN ('chat_only', 'form_only', 'form_chat', 'review')),
|
||||
history_mode TEXT NOT NULL DEFAULT 'full'
|
||||
CHECK (history_mode IN ('full', 'summary', 'fresh')),
|
||||
auto_transition BOOLEAN NOT NULL DEFAULT false,
|
||||
transition_rules JSONB NOT NULL DEFAULT '{}',
|
||||
surface_pkg_id TEXT REFERENCES packages(id) ON DELETE SET NULL,
|
||||
sla_seconds INTEGER,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_workflow_stages_workflow
|
||||
ON workflow_stages(workflow_id, ordinal);
|
||||
|
||||
COMMENT ON TABLE workflow_stages IS 'Ordered stages within a workflow. Each stage has a driving persona and optional human assignment.';
|
||||
COMMENT ON COLUMN workflow_stages.history_mode IS 'full=complete history, summary=utility-role summary, fresh=clean slate';
|
||||
COMMENT ON COLUMN workflow_stages.form_template IS 'JSON schema of fields the persona should collect from the visitor.';
|
||||
COMMENT ON COLUMN workflow_stages.surface_pkg_id IS 'Optional package that provides a custom stage surface. NULL = built-in surface based on stage_mode.';
|
||||
COMMENT ON COLUMN workflow_stages.sla_seconds IS 'SLA budget in seconds for this stage. NULL = no SLA.';
|
||||
|
||||
|
||||
-- =========================================
|
||||
-- WORKFLOW VERSIONS (immutable snapshots)
|
||||
-- =========================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS workflow_versions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
workflow_id UUID NOT NULL REFERENCES workflows(id) ON DELETE CASCADE,
|
||||
version_number INTEGER NOT NULL,
|
||||
snapshot JSONB NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
UNIQUE (workflow_id, version_number)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_workflow_versions_workflow
|
||||
ON workflow_versions(workflow_id, version_number DESC);
|
||||
|
||||
COMMENT ON TABLE workflow_versions IS 'Immutable snapshots of workflow definitions. Active instances run against a pinned version.';
|
||||
COMMENT ON COLUMN workflow_versions.snapshot IS 'Full JSON: definition + stages + persona tool grants at publish time.';
|
||||
|
||||
|
||||
-- =========================================
|
||||
-- WORKFLOW ASSIGNMENTS
|
||||
-- =========================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS workflow_assignments (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
channel_id UUID NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
|
||||
stage INTEGER NOT NULL,
|
||||
team_id UUID NOT NULL REFERENCES teams(id) ON DELETE CASCADE,
|
||||
assigned_to UUID REFERENCES users(id) ON DELETE SET NULL,
|
||||
status TEXT NOT NULL DEFAULT 'unassigned'
|
||||
CHECK (status IN ('unassigned', 'claimed', 'completed')),
|
||||
review_comments JSONB NOT NULL DEFAULT '[]',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
claimed_at TIMESTAMPTZ,
|
||||
completed_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_workflow_assignments_channel
|
||||
ON workflow_assignments(channel_id, stage);
|
||||
CREATE INDEX IF NOT EXISTS idx_workflow_assignments_team_status
|
||||
ON workflow_assignments(team_id, status) WHERE status = 'unassigned';
|
||||
CREATE INDEX IF NOT EXISTS idx_workflow_assignments_user
|
||||
ON workflow_assignments(assigned_to) WHERE assigned_to IS NOT NULL;
|
||||
|
||||
COMMENT ON TABLE workflow_assignments IS 'Queue entries for human review stages in workflows.';
|
||||
COMMENT ON COLUMN workflow_assignments.status IS 'unassigned=waiting, claimed=being worked, completed=done';
|
||||
|
||||
|
||||
-- =========================================
|
||||
-- DEFERRED FK: channels.workflow_id → workflows
|
||||
-- =========================================
|
||||
|
||||
DO $$ BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint WHERE conname = 'fk_channels_workflow'
|
||||
) THEN
|
||||
ALTER TABLE channels ADD CONSTRAINT fk_channels_workflow
|
||||
FOREIGN KEY (workflow_id) REFERENCES workflows(id) ON DELETE SET NULL;
|
||||
END IF;
|
||||
END $$;
|
||||
@@ -1,37 +0,0 @@
|
||||
-- ==========================================
|
||||
-- Chat Switchboard — 020 Multi-Replica HA
|
||||
-- ==========================================
|
||||
-- Shared state tables for multi-replica operation.
|
||||
-- v0.32.0: ws_tickets, rate_limit_counters.
|
||||
-- ==========================================
|
||||
|
||||
-- =========================================
|
||||
-- WEBSOCKET TICKETS (replaces in-memory sync.Map)
|
||||
-- =========================================
|
||||
-- Short-lived, single-use tokens for WebSocket authentication.
|
||||
-- Ticket issued on pod-1 must validate on pod-2.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ws_tickets (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
expires_at TIMESTAMPTZ NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_ws_tickets_expires
|
||||
ON ws_tickets (expires_at);
|
||||
|
||||
-- =========================================
|
||||
-- RATE LIMIT COUNTERS (replaces in-memory token bucket)
|
||||
-- =========================================
|
||||
-- Fixed-window counters keyed by scope:identifier and time bucket.
|
||||
-- Key format: "{scope}:{identifier}" e.g. "auth:192.168.1.1"
|
||||
|
||||
CREATE TABLE IF NOT EXISTS rate_limit_counters (
|
||||
key TEXT NOT NULL,
|
||||
window_start TIMESTAMPTZ NOT NULL,
|
||||
tokens REAL NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (key, window_start)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_rate_limit_counters_window
|
||||
ON rate_limit_counters (window_start);
|
||||
@@ -1,18 +0,0 @@
|
||||
-- ==========================================
|
||||
-- Chat Switchboard — 021 Data Portability
|
||||
-- ==========================================
|
||||
-- v0.34.0: Indexes for data export and GDPR operations.
|
||||
-- ==========================================
|
||||
|
||||
-- Partial index for batch message export (only non-deleted)
|
||||
CREATE INDEX IF NOT EXISTS idx_messages_channel_active
|
||||
ON messages (channel_id, created_at)
|
||||
WHERE deleted_at IS NULL;
|
||||
|
||||
-- Index for user-scoped channel listing (export)
|
||||
CREATE INDEX IF NOT EXISTS idx_channels_user_id
|
||||
ON channels (user_id);
|
||||
|
||||
-- Index for file lookup by user (export)
|
||||
CREATE INDEX IF NOT EXISTS idx_files_user_id
|
||||
ON files (user_id);
|
||||
@@ -1,37 +0,0 @@
|
||||
-- ==========================================
|
||||
-- Chat Switchboard — 022 Extension Connections
|
||||
-- ==========================================
|
||||
-- v0.38.1: Scoped credential management for extensions.
|
||||
-- Same scope/resolution pattern as provider_configs.
|
||||
-- ==========================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ext_connections (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
type TEXT NOT NULL,
|
||||
package_id TEXT NOT NULL,
|
||||
scope VARCHAR(10) NOT NULL
|
||||
CHECK (scope IN ('global', 'team', 'personal')),
|
||||
owner_id TEXT NOT NULL DEFAULT '',
|
||||
name TEXT NOT NULL,
|
||||
config JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
UNIQUE(type, scope, owner_id, name)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_ext_connections_type ON ext_connections(type);
|
||||
CREATE INDEX IF NOT EXISTS idx_ext_connections_scope ON ext_connections(scope);
|
||||
CREATE INDEX IF NOT EXISTS idx_ext_connections_owner ON ext_connections(owner_id) WHERE owner_id != '';
|
||||
CREATE INDEX IF NOT EXISTS idx_ext_connections_active ON ext_connections(is_active) WHERE is_active = true;
|
||||
|
||||
DROP TRIGGER IF EXISTS ext_connections_updated_at ON ext_connections;
|
||||
CREATE TRIGGER ext_connections_updated_at BEFORE UPDATE ON ext_connections
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
||||
|
||||
COMMENT ON TABLE ext_connections IS 'v0.38.1: Extension connection credentials (scoped, encrypted secrets in config JSON)';
|
||||
COMMENT ON COLUMN ext_connections.type IS 'Connection type identifier, shared across packages (e.g. "gitea", "github")';
|
||||
COMMENT ON COLUMN ext_connections.package_id IS 'Declaring package ID (UI attribution only, not access control)';
|
||||
COMMENT ON COLUMN ext_connections.scope IS 'global=admin-managed, team=team-scoped, personal=user-owned';
|
||||
COMMENT ON COLUMN ext_connections.owner_id IS 'Empty for global; team_id for team scope; user_id for personal scope';
|
||||
COMMENT ON COLUMN ext_connections.config IS 'JSON config blob; secret-type fields encrypted at rest by handler layer';
|
||||
@@ -1,31 +0,0 @@
|
||||
-- ==========================================
|
||||
-- Chat Switchboard — 023 Extension Dependencies
|
||||
-- ==========================================
|
||||
-- v0.38.2: Library package dependency tracking.
|
||||
-- Consumers declare which libraries they depend on.
|
||||
-- Libraries with active consumers cannot be uninstalled.
|
||||
-- ==========================================
|
||||
|
||||
-- Add 'library' to the packages type constraint.
|
||||
ALTER TABLE packages DROP CONSTRAINT IF EXISTS packages_type_check;
|
||||
ALTER TABLE packages ADD CONSTRAINT packages_type_check
|
||||
CHECK (type IN ('surface', 'extension', 'full', 'workflow', 'library'));
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ext_dependencies (
|
||||
consumer_id TEXT NOT NULL,
|
||||
library_id TEXT NOT NULL,
|
||||
version_spec TEXT NOT NULL DEFAULT '>=0.0.0',
|
||||
resolved_ver TEXT NOT NULL DEFAULT '0.0.0',
|
||||
PRIMARY KEY (consumer_id, library_id),
|
||||
FOREIGN KEY (consumer_id) REFERENCES packages(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (library_id) REFERENCES packages(id) ON DELETE RESTRICT
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_ext_deps_consumer ON ext_dependencies(consumer_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_ext_deps_library ON ext_dependencies(library_id);
|
||||
|
||||
COMMENT ON TABLE ext_dependencies IS 'v0.38.2: Package dependency graph — consumers → libraries';
|
||||
COMMENT ON COLUMN ext_dependencies.consumer_id IS 'Package that depends on the library';
|
||||
COMMENT ON COLUMN ext_dependencies.library_id IS 'Library package being depended upon';
|
||||
COMMENT ON COLUMN ext_dependencies.version_spec IS 'Semver version spec declared by consumer (e.g. ">=1.0.0")';
|
||||
COMMENT ON COLUMN ext_dependencies.resolved_ver IS 'Actual library version at dependency creation time';
|
||||
@@ -1,17 +1,10 @@
|
||||
-- ==========================================
|
||||
-- Chat Switchboard — 001 Core
|
||||
-- Switchboard Core — 001 Core
|
||||
-- ==========================================
|
||||
-- Users (with auth fields), refresh tokens, platform policies,
|
||||
-- global settings, user presence.
|
||||
-- Consolidated v0.28.0: merges 001 + 018 (auth_abstraction) + 016 (presence).
|
||||
-- Users, auth, platform policies, global settings, presence, OIDC.
|
||||
-- ==========================================
|
||||
|
||||
-- ── Extensions ──────────────────────────────
|
||||
|
||||
CREATE EXTENSION IF NOT EXISTS "pgcrypto"; -- gen_random_uuid()
|
||||
CREATE EXTENSION IF NOT EXISTS "vector"; -- pgvector (KB embeddings)
|
||||
|
||||
-- ── Utility: auto-update updated_at ─────────
|
||||
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
|
||||
|
||||
CREATE OR REPLACE FUNCTION update_updated_at()
|
||||
RETURNS TRIGGER AS $$
|
||||
@@ -21,47 +14,37 @@ BEGIN
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
|
||||
-- =========================================
|
||||
-- USERS
|
||||
-- =========================================
|
||||
-- ── Users ───────────────────────────────────
|
||||
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
username VARCHAR(50) NOT NULL,
|
||||
email VARCHAR(255) NOT NULL,
|
||||
password_hash TEXT, -- nullable for mTLS/OIDC
|
||||
password_hash TEXT,
|
||||
display_name VARCHAR(100),
|
||||
avatar_url TEXT,
|
||||
role VARCHAR(20) DEFAULT 'user'
|
||||
CHECK (role IN ('user', 'admin')),
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
settings JSONB DEFAULT '{}'::jsonb,
|
||||
|
||||
-- Auth abstraction (v0.24.0)
|
||||
auth_source VARCHAR(20) NOT NULL DEFAULT 'builtin'
|
||||
CHECK (auth_source IN ('builtin', 'mtls', 'oidc')),
|
||||
external_id TEXT,
|
||||
handle VARCHAR(100) NOT NULL,
|
||||
|
||||
-- Vault: per-user encryption key (UEK) for BYOK API keys
|
||||
encrypted_uek BYTEA,
|
||||
uek_salt BYTEA,
|
||||
uek_nonce BYTEA,
|
||||
vault_set BOOLEAN NOT NULL DEFAULT false,
|
||||
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
last_login_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
-- Case-insensitive unique indexes
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS users_username_ci ON users (LOWER(username));
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS users_email_ci ON users (LOWER(email));
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_users_handle ON users(LOWER(handle));
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_users_external_id
|
||||
ON users(auth_source, external_id) WHERE external_id IS NOT NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
|
||||
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
|
||||
|
||||
@@ -69,18 +52,7 @@ DROP TRIGGER IF EXISTS users_updated_at ON users;
|
||||
CREATE TRIGGER users_updated_at BEFORE UPDATE ON users
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
||||
|
||||
COMMENT ON COLUMN users.auth_source IS 'builtin=password, mtls=cert, oidc=SSO';
|
||||
COMMENT ON COLUMN users.external_id IS 'IdP subject claim (OIDC) or cert fingerprint (mTLS)';
|
||||
COMMENT ON COLUMN users.handle IS 'Unique @mention handle, auto-generated from username';
|
||||
COMMENT ON COLUMN users.encrypted_uek IS 'UEK encrypted with Argon2id(password)-derived key';
|
||||
COMMENT ON COLUMN users.uek_salt IS 'Argon2id salt for UEK derivation';
|
||||
COMMENT ON COLUMN users.uek_nonce IS 'AES-GCM nonce for UEK wrapping';
|
||||
COMMENT ON COLUMN users.vault_set IS 'true once UEK has been generated and wrapped';
|
||||
|
||||
|
||||
-- =========================================
|
||||
-- AUTH — REFRESH TOKENS
|
||||
-- =========================================
|
||||
-- ── Refresh Tokens ──────────────────────────
|
||||
|
||||
CREATE TABLE IF NOT EXISTS refresh_tokens (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
@@ -94,10 +66,7 @@ CREATE TABLE IF NOT EXISTS refresh_tokens (
|
||||
CREATE INDEX IF NOT EXISTS idx_refresh_tokens_user ON refresh_tokens(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_refresh_tokens_hash ON refresh_tokens(token_hash) WHERE revoked_at IS NULL;
|
||||
|
||||
|
||||
-- =========================================
|
||||
-- PLATFORM POLICIES
|
||||
-- =========================================
|
||||
-- ── Platform Policies ───────────────────────
|
||||
|
||||
CREATE TABLE IF NOT EXISTS platform_policies (
|
||||
key VARCHAR(50) PRIMARY KEY,
|
||||
@@ -107,21 +76,11 @@ CREATE TABLE IF NOT EXISTS platform_policies (
|
||||
);
|
||||
|
||||
INSERT INTO platform_policies (key, value) VALUES
|
||||
('allow_user_byok', 'false'),
|
||||
('allow_user_personas', 'false'),
|
||||
('allow_raw_model_access', 'true'),
|
||||
('allow_registration', 'true'),
|
||||
('default_user_active', 'false'),
|
||||
('allow_team_providers', 'true'),
|
||||
('kb_direct_access', 'true')
|
||||
('allow_registration', 'true'),
|
||||
('default_user_active', 'false')
|
||||
ON CONFLICT (key) DO NOTHING;
|
||||
|
||||
COMMENT ON TABLE platform_policies IS 'Global admin switches controlling platform behavior';
|
||||
|
||||
|
||||
-- =========================================
|
||||
-- GLOBAL SETTINGS
|
||||
-- =========================================
|
||||
-- ── Global Settings ─────────────────────────
|
||||
|
||||
CREATE TABLE IF NOT EXISTS global_settings (
|
||||
key VARCHAR(100) PRIMARY KEY,
|
||||
@@ -132,20 +91,11 @@ CREATE TABLE IF NOT EXISTS global_settings (
|
||||
|
||||
INSERT INTO global_settings (key, value) VALUES
|
||||
('registration', '{"enabled": true}'::jsonb),
|
||||
('site', '{"name": "Chat Switchboard", "tagline": "Multi-Model AI Chat"}'::jsonb),
|
||||
('banner', '{"enabled": false, "text": "", "bg": "#007a33", "fg": "#ffffff"}'::jsonb),
|
||||
('model_roles', '{
|
||||
"utility": { "primary": null, "fallback": null },
|
||||
"embedding": { "primary": null, "fallback": null },
|
||||
"generation": { "primary": null, "fallback": null }
|
||||
}'::jsonb),
|
||||
('retention_ttl_days', '{"value": 0}'::jsonb)
|
||||
('site', '{"name": "Switchboard Core", "tagline": "Extension Platform"}'::jsonb),
|
||||
('banner', '{"enabled": false, "text": "", "bg": "#007a33", "fg": "#ffffff"}'::jsonb)
|
||||
ON CONFLICT (key) DO NOTHING;
|
||||
|
||||
|
||||
-- =========================================
|
||||
-- USER PRESENCE (v0.23.1)
|
||||
-- =========================================
|
||||
-- ── User Presence ───────────────────────────
|
||||
|
||||
CREATE TABLE IF NOT EXISTS user_presence (
|
||||
user_id UUID PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
|
||||
@@ -156,4 +106,13 @@ CREATE TABLE IF NOT EXISTS user_presence (
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_user_presence_last_seen ON user_presence(last_seen DESC);
|
||||
|
||||
COMMENT ON TABLE user_presence IS 'Heartbeat-driven presence. Rows upserted every 30s by active clients.';
|
||||
-- ── OIDC Auth State ─────────────────────────
|
||||
|
||||
CREATE TABLE IF NOT EXISTS oidc_auth_state (
|
||||
state TEXT PRIMARY KEY,
|
||||
nonce TEXT NOT NULL,
|
||||
redirect_to TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_oidc_state_created ON oidc_auth_state(created_at);
|
||||
@@ -1,14 +1,6 @@
|
||||
-- ==========================================
|
||||
-- Chat Switchboard — 002 Teams & Access Control
|
||||
-- Switchboard Core — 002 Teams & Access Control
|
||||
-- ==========================================
|
||||
-- Teams, team membership, groups (with permissions, budgets, source),
|
||||
-- group membership, Everyone group seed.
|
||||
-- Consolidated v0.28.0: merges 002 + 019 (source) + 020 (permissions).
|
||||
-- ==========================================
|
||||
|
||||
-- =========================================
|
||||
-- TEAMS
|
||||
-- =========================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS teams (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
@@ -26,8 +18,6 @@ DROP TRIGGER IF EXISTS teams_updated_at ON teams;
|
||||
CREATE TRIGGER teams_updated_at BEFORE UPDATE ON teams
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
||||
|
||||
COMMENT ON COLUMN teams.settings IS 'Team policies: {"require_private_providers": false}';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS team_members (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
team_id UUID NOT NULL REFERENCES teams(id) ON DELETE CASCADE,
|
||||
@@ -41,10 +31,7 @@ CREATE TABLE IF NOT EXISTS team_members (
|
||||
CREATE INDEX IF NOT EXISTS idx_team_members_team ON team_members(team_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_team_members_user ON team_members(user_id);
|
||||
|
||||
|
||||
-- =========================================
|
||||
-- GROUPS (ACLs + Permissions)
|
||||
-- =========================================
|
||||
-- ── Groups ──────────────────────────────────
|
||||
|
||||
CREATE TABLE IF NOT EXISTS groups (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
@@ -53,19 +40,15 @@ CREATE TABLE IF NOT EXISTS groups (
|
||||
scope VARCHAR(20) NOT NULL DEFAULT 'global'
|
||||
CHECK (scope IN ('global', 'team')),
|
||||
team_id UUID REFERENCES teams(id) ON DELETE CASCADE,
|
||||
created_by UUID REFERENCES users(id), -- nullable for system-seeded
|
||||
created_by UUID REFERENCES users(id),
|
||||
source VARCHAR(20) NOT NULL DEFAULT 'manual'
|
||||
CHECK (source IN ('manual', 'oidc', 'system')),
|
||||
|
||||
-- Fine-grained permissions (v0.24.2)
|
||||
permissions JSONB NOT NULL DEFAULT '[]'::jsonb,
|
||||
token_budget_daily BIGINT,
|
||||
token_budget_monthly BIGINT,
|
||||
allowed_models JSONB,
|
||||
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT groups_scope_team CHECK (
|
||||
(scope = 'global' AND team_id IS NULL) OR
|
||||
(scope = 'team' AND team_id IS NOT NULL)
|
||||
@@ -74,19 +57,10 @@ CREATE TABLE IF NOT EXISTS groups (
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_groups_name_scope
|
||||
ON groups(name, COALESCE(team_id, '00000000-0000-0000-0000-000000000000'));
|
||||
|
||||
DROP TRIGGER IF EXISTS groups_updated_at ON groups;
|
||||
CREATE TRIGGER groups_updated_at BEFORE UPDATE ON groups
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
||||
|
||||
COMMENT ON TABLE groups IS 'Access-control groups with permission grants. Decouple resource visibility from team membership.';
|
||||
COMMENT ON COLUMN groups.scope IS 'global: admin-managed. team: team-admin-managed.';
|
||||
COMMENT ON COLUMN groups.source IS 'manual=admin-created, oidc=synced from IdP, system=platform-seeded';
|
||||
COMMENT ON COLUMN groups.permissions IS 'Array of permission strings granted to group members';
|
||||
COMMENT ON COLUMN groups.token_budget_daily IS 'Daily token ceiling for members (NULL = unlimited)';
|
||||
COMMENT ON COLUMN groups.token_budget_monthly IS 'Monthly token ceiling for members (NULL = unlimited)';
|
||||
COMMENT ON COLUMN groups.allowed_models IS 'Array of model_id strings this group may use. NULL = unrestricted.';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS group_members (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
group_id UUID NOT NULL REFERENCES groups(id) ON DELETE CASCADE,
|
||||
@@ -99,15 +73,13 @@ CREATE TABLE IF NOT EXISTS group_members (
|
||||
CREATE INDEX IF NOT EXISTS idx_group_members_group ON group_members(group_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_group_members_user ON group_members(user_id);
|
||||
|
||||
-- Seed the Everyone group with a stable well-known ID.
|
||||
-- Seed Everyone group
|
||||
INSERT INTO groups (id, name, description, scope, created_by, source, permissions)
|
||||
VALUES (
|
||||
'00000000-0000-0000-0000-000000000001',
|
||||
'Everyone',
|
||||
'Implicit group — all authenticated users receive these permissions.',
|
||||
'global',
|
||||
NULL,
|
||||
'system',
|
||||
'["model.use","kb.read","channel.create"]'::jsonb
|
||||
'global', NULL, 'system',
|
||||
'["extension.use","workflow.submit"]'::jsonb
|
||||
)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
78
server/database/migrations/postgres/003_packages.sql
Normal file
78
server/database/migrations/postgres/003_packages.sql
Normal file
@@ -0,0 +1,78 @@
|
||||
-- ==========================================
|
||||
-- Switchboard Core — 003 Packages & Extensions
|
||||
-- ==========================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS packages (
|
||||
id TEXT PRIMARY KEY,
|
||||
title TEXT NOT NULL,
|
||||
type TEXT NOT NULL DEFAULT 'surface'
|
||||
CHECK (type IN ('surface', 'extension', 'full', 'workflow', 'library')),
|
||||
version TEXT NOT NULL DEFAULT '0.0.0',
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
author TEXT NOT NULL DEFAULT '',
|
||||
tier TEXT NOT NULL DEFAULT 'browser'
|
||||
CHECK (tier IN ('browser', 'starlark', 'sidecar')),
|
||||
is_system BOOLEAN NOT NULL DEFAULT false,
|
||||
scope TEXT NOT NULL DEFAULT 'global'
|
||||
CHECK (scope IN ('global', 'team', 'personal')),
|
||||
team_id UUID REFERENCES teams(id) ON DELETE CASCADE,
|
||||
installed_by UUID REFERENCES users(id) ON DELETE SET NULL,
|
||||
manifest JSONB NOT NULL DEFAULT '{}',
|
||||
enabled BOOLEAN NOT NULL DEFAULT true,
|
||||
status TEXT NOT NULL DEFAULT 'active'
|
||||
CHECK (status IN ('active', 'pending_review', 'suspended')),
|
||||
schema_version INTEGER NOT NULL DEFAULT 0,
|
||||
package_settings JSONB NOT NULL DEFAULT '{}',
|
||||
source TEXT NOT NULL DEFAULT 'core'
|
||||
CHECK (source IN ('core', 'builtin', 'extension', 'registry')),
|
||||
installed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_packages_type ON packages(type);
|
||||
CREATE INDEX IF NOT EXISTS idx_packages_enabled ON packages(enabled) WHERE enabled = true;
|
||||
CREATE INDEX IF NOT EXISTS idx_packages_team ON packages(team_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_packages_source ON packages(source);
|
||||
CREATE INDEX IF NOT EXISTS idx_packages_status ON packages(status);
|
||||
|
||||
-- ── Package User Settings ───────────────────
|
||||
|
||||
CREATE TABLE IF NOT EXISTS package_user_settings (
|
||||
package_id TEXT NOT NULL REFERENCES packages(id) ON DELETE CASCADE,
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
settings JSONB NOT NULL DEFAULT '{}',
|
||||
is_enabled BOOLEAN NOT NULL DEFAULT true,
|
||||
PRIMARY KEY (package_id, user_id)
|
||||
);
|
||||
|
||||
-- ── Extension Permissions ───────────────────
|
||||
|
||||
CREATE TABLE IF NOT EXISTS extension_permissions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
package_id TEXT NOT NULL REFERENCES packages(id) ON DELETE CASCADE,
|
||||
permission TEXT NOT NULL,
|
||||
granted BOOLEAN NOT NULL DEFAULT false,
|
||||
granted_by UUID REFERENCES users(id) ON DELETE SET NULL,
|
||||
granted_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
UNIQUE(package_id, permission)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_ext_perm_package ON extension_permissions(package_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_ext_perm_granted ON extension_permissions(granted) WHERE granted = true;
|
||||
|
||||
-- ── Extension Data Tables Catalog ───────────
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ext_data_tables (
|
||||
package_id TEXT NOT NULL REFERENCES packages(id) ON DELETE CASCADE,
|
||||
table_name TEXT NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
PRIMARY KEY (package_id, table_name)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_ext_data_tables_pkg ON ext_data_tables(package_id);
|
||||
|
||||
-- ── Platform Read View ──────────────────────
|
||||
|
||||
CREATE OR REPLACE VIEW ext_view_users AS
|
||||
SELECT id, display_name, email FROM users;
|
||||
66
server/database/migrations/postgres/004_connections.sql
Normal file
66
server/database/migrations/postgres/004_connections.sql
Normal file
@@ -0,0 +1,66 @@
|
||||
-- ==========================================
|
||||
-- Switchboard Core — 004 Connections & Dependencies
|
||||
-- ==========================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ext_connections (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
type TEXT NOT NULL,
|
||||
package_id TEXT NOT NULL,
|
||||
scope VARCHAR(10) NOT NULL
|
||||
CHECK (scope IN ('global', 'team', 'personal')),
|
||||
owner_id TEXT NOT NULL DEFAULT '',
|
||||
name TEXT NOT NULL,
|
||||
config JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
UNIQUE(type, scope, owner_id, name)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_ext_connections_type ON ext_connections(type);
|
||||
CREATE INDEX IF NOT EXISTS idx_ext_connections_scope ON ext_connections(scope);
|
||||
CREATE INDEX IF NOT EXISTS idx_ext_connections_owner ON ext_connections(owner_id) WHERE owner_id != '';
|
||||
CREATE INDEX IF NOT EXISTS idx_ext_connections_active ON ext_connections(is_active) WHERE is_active = true;
|
||||
|
||||
DROP TRIGGER IF EXISTS ext_connections_updated_at ON ext_connections;
|
||||
CREATE TRIGGER ext_connections_updated_at BEFORE UPDATE ON ext_connections
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
||||
|
||||
-- ── Extension Dependencies ──────────────────
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ext_dependencies (
|
||||
consumer_id TEXT NOT NULL,
|
||||
library_id TEXT NOT NULL,
|
||||
version_spec TEXT NOT NULL DEFAULT '>=0.0.0',
|
||||
resolved_ver TEXT NOT NULL DEFAULT '0.0.0',
|
||||
PRIMARY KEY (consumer_id, library_id),
|
||||
FOREIGN KEY (consumer_id) REFERENCES packages(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (library_id) REFERENCES packages(id) ON DELETE RESTRICT
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_ext_deps_consumer ON ext_dependencies(consumer_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_ext_deps_library ON ext_dependencies(library_id);
|
||||
|
||||
-- ── Resource Grants ─────────────────────────
|
||||
|
||||
CREATE TABLE IF NOT EXISTS resource_grants (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
resource_type VARCHAR(30) NOT NULL,
|
||||
resource_id UUID NOT NULL,
|
||||
grant_scope VARCHAR(20) NOT NULL DEFAULT 'team_only'
|
||||
CHECK (grant_scope IN ('team_only', 'global', 'groups')),
|
||||
granted_groups UUID[] NOT NULL DEFAULT '{}',
|
||||
created_by UUID NOT NULL REFERENCES users(id),
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
UNIQUE(resource_type, resource_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_resource_grants_resource
|
||||
ON resource_grants(resource_type, resource_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_resource_grants_groups
|
||||
ON resource_grants USING gin(granted_groups);
|
||||
|
||||
DROP TRIGGER IF EXISTS resource_grants_updated_at ON resource_grants;
|
||||
CREATE TRIGGER resource_grants_updated_at BEFORE UPDATE ON resource_grants
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
||||
@@ -1,13 +1,6 @@
|
||||
-- ==========================================
|
||||
-- Chat Switchboard — 011 Notifications
|
||||
-- Switchboard Core — 005 Notifications
|
||||
-- ==========================================
|
||||
-- Notification delivery and per-user preferences.
|
||||
-- ICD §12 (Notifications)
|
||||
-- ==========================================
|
||||
|
||||
-- =========================================
|
||||
-- NOTIFICATIONS
|
||||
-- =========================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS notifications (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
@@ -21,13 +14,10 @@ CREATE TABLE IF NOT EXISTS notifications (
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_notifications_user_unread ON notifications(user_id, is_read, created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_notifications_user_created ON notifications(user_id, created_at DESC);
|
||||
|
||||
|
||||
-- =========================================
|
||||
-- NOTIFICATION PREFERENCES
|
||||
-- =========================================
|
||||
CREATE INDEX IF NOT EXISTS idx_notifications_user_unread
|
||||
ON notifications(user_id, is_read, created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_notifications_user_created
|
||||
ON notifications(user_id, created_at DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS notification_preferences (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
20
server/database/migrations/postgres/006_audit.sql
Normal file
20
server/database/migrations/postgres/006_audit.sql
Normal file
@@ -0,0 +1,20 @@
|
||||
-- ==========================================
|
||||
-- Switchboard Core — 006 Audit
|
||||
-- ==========================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS audit_log (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
actor_id UUID REFERENCES users(id) ON DELETE SET NULL,
|
||||
action VARCHAR(100) NOT NULL,
|
||||
resource_type VARCHAR(50) NOT NULL,
|
||||
resource_id VARCHAR(255),
|
||||
metadata JSONB DEFAULT '{}'::jsonb,
|
||||
ip_address VARCHAR(45),
|
||||
user_agent TEXT DEFAULT '',
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_log_created ON audit_log(created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_log_actor ON audit_log(actor_id) WHERE actor_id IS NOT NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_log_resource ON audit_log(resource_type, resource_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_log_action ON audit_log(action);
|
||||
76
server/database/migrations/postgres/007_workflows.sql
Normal file
76
server/database/migrations/postgres/007_workflows.sql
Normal file
@@ -0,0 +1,76 @@
|
||||
-- ==========================================
|
||||
-- Switchboard Core — 007 Workflows
|
||||
-- ==========================================
|
||||
-- Workflow definitions, stages, version snapshots.
|
||||
-- persona_id kept as nullable TEXT (no FK — personas are extensions now).
|
||||
-- workflow_assignments dropped (channel-dependent, rebuild as needed).
|
||||
-- ==========================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS workflows (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
team_id UUID REFERENCES teams(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
slug TEXT NOT NULL,
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
branding JSONB NOT NULL DEFAULT '{}',
|
||||
entry_mode TEXT NOT NULL DEFAULT 'public_link'
|
||||
CHECK (entry_mode IN ('public_link', 'team_only')),
|
||||
is_active BOOLEAN NOT NULL DEFAULT false,
|
||||
version INTEGER NOT NULL DEFAULT 1,
|
||||
on_complete JSONB,
|
||||
retention JSONB NOT NULL DEFAULT '{"mode": "archive"}',
|
||||
webhook_url TEXT,
|
||||
webhook_secret TEXT,
|
||||
created_by UUID NOT NULL REFERENCES users(id),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_workflows_team_slug
|
||||
ON workflows(team_id, slug) WHERE team_id IS NOT NULL;
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_workflows_global_slug
|
||||
ON workflows(slug) WHERE team_id IS NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_workflows_active
|
||||
ON workflows(is_active) WHERE is_active = true;
|
||||
|
||||
DROP TRIGGER IF EXISTS workflows_updated_at ON workflows;
|
||||
CREATE TRIGGER workflows_updated_at BEFORE UPDATE ON workflows
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
||||
|
||||
-- ── Workflow Stages ─────────────────────────
|
||||
|
||||
CREATE TABLE IF NOT EXISTS workflow_stages (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
workflow_id UUID NOT NULL REFERENCES workflows(id) ON DELETE CASCADE,
|
||||
ordinal INTEGER NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
persona_id TEXT,
|
||||
assignment_team_id UUID REFERENCES teams(id) ON DELETE SET NULL,
|
||||
form_template JSONB NOT NULL DEFAULT '{}',
|
||||
stage_mode TEXT NOT NULL DEFAULT 'form_only'
|
||||
CHECK (stage_mode IN ('form_only', 'form_chat', 'review', 'custom')),
|
||||
history_mode TEXT NOT NULL DEFAULT 'full'
|
||||
CHECK (history_mode IN ('full', 'summary', 'fresh')),
|
||||
auto_transition BOOLEAN NOT NULL DEFAULT false,
|
||||
transition_rules JSONB NOT NULL DEFAULT '{}',
|
||||
surface_pkg_id TEXT REFERENCES packages(id) ON DELETE SET NULL,
|
||||
sla_seconds INTEGER,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_workflow_stages_workflow
|
||||
ON workflow_stages(workflow_id, ordinal);
|
||||
|
||||
-- ── Workflow Versions ───────────────────────
|
||||
|
||||
CREATE TABLE IF NOT EXISTS workflow_versions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
workflow_id UUID NOT NULL REFERENCES workflows(id) ON DELETE CASCADE,
|
||||
version_number INTEGER NOT NULL,
|
||||
snapshot JSONB NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
UNIQUE (workflow_id, version_number)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_workflow_versions_workflow
|
||||
ON workflow_versions(workflow_id, version_number DESC);
|
||||
@@ -1,15 +1,11 @@
|
||||
-- ==========================================
|
||||
-- Chat Switchboard — 019 Tasks
|
||||
-- Switchboard Core — 008 Tasks
|
||||
-- ==========================================
|
||||
-- Task definitions and run history for autonomous agents.
|
||||
-- Consolidated v0.28.0: merges 026 + 027 (webhook_secret inline).
|
||||
-- v0.28.0-cs3: trigger_token, action task_type, queued run status.
|
||||
-- Task scheduling and run history.
|
||||
-- Stripped: persona_id (kept nullable, no FK), output_channel_id,
|
||||
-- provider_config_id. Output modes: notification | webhook | log.
|
||||
-- ==========================================
|
||||
|
||||
-- =========================================
|
||||
-- TASK DEFINITIONS
|
||||
-- =========================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tasks (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
owner_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
@@ -18,44 +14,28 @@ CREATE TABLE IF NOT EXISTS tasks (
|
||||
description TEXT DEFAULT '',
|
||||
scope TEXT NOT NULL DEFAULT 'personal'
|
||||
CHECK (scope IN ('personal', 'team', 'global')),
|
||||
|
||||
-- What to run
|
||||
task_type TEXT NOT NULL DEFAULT 'prompt'
|
||||
CHECK (task_type IN ('prompt', 'workflow', 'action', 'system')),
|
||||
task_type TEXT NOT NULL DEFAULT 'action'
|
||||
CHECK (task_type IN ('action', 'workflow', 'system')),
|
||||
system_function TEXT DEFAULT '',
|
||||
persona_id UUID REFERENCES personas(id) ON DELETE SET NULL,
|
||||
persona_id TEXT,
|
||||
model_id TEXT,
|
||||
system_prompt TEXT DEFAULT '',
|
||||
user_prompt TEXT DEFAULT '',
|
||||
workflow_id UUID REFERENCES workflows(id) ON DELETE SET NULL,
|
||||
tool_grants JSONB,
|
||||
|
||||
-- Schedule: cron expression, "once", or "webhook"
|
||||
schedule TEXT NOT NULL,
|
||||
timezone TEXT NOT NULL DEFAULT 'UTC',
|
||||
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||
|
||||
-- Webhook trigger (inbound) — token-based auth for /hooks/t/:token
|
||||
trigger_token TEXT UNIQUE,
|
||||
|
||||
-- Execution policy
|
||||
max_tokens INTEGER NOT NULL DEFAULT 4096,
|
||||
max_tool_calls INTEGER NOT NULL DEFAULT 10,
|
||||
max_wall_clock INTEGER NOT NULL DEFAULT 300,
|
||||
output_mode TEXT NOT NULL DEFAULT 'channel'
|
||||
CHECK (output_mode IN ('channel', 'note', 'webhook')),
|
||||
output_channel_id UUID REFERENCES channels(id) ON DELETE SET NULL,
|
||||
output_mode TEXT NOT NULL DEFAULT 'log'
|
||||
CHECK (output_mode IN ('notification', 'webhook', 'log')),
|
||||
webhook_url TEXT,
|
||||
webhook_secret TEXT,
|
||||
|
||||
-- Provider routing
|
||||
provider_config_id UUID REFERENCES provider_configs(id) ON DELETE SET NULL,
|
||||
|
||||
-- Notifications
|
||||
notify_on_complete BOOLEAN NOT NULL DEFAULT false,
|
||||
notify_on_failure BOOLEAN NOT NULL DEFAULT true,
|
||||
|
||||
-- Bookkeeping
|
||||
last_run_at TIMESTAMPTZ,
|
||||
next_run_at TIMESTAMPTZ,
|
||||
run_count INTEGER NOT NULL DEFAULT 0,
|
||||
@@ -63,20 +43,17 @@ CREATE TABLE IF NOT EXISTS tasks (
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_tasks_next_run ON tasks (next_run_at) WHERE is_active = true;
|
||||
CREATE INDEX IF NOT EXISTS idx_tasks_owner ON tasks (owner_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_tasks_team ON tasks (team_id);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_tasks_trigger_token ON tasks (trigger_token) WHERE trigger_token IS NOT NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_tasks_next_run ON tasks(next_run_at) WHERE is_active = true;
|
||||
CREATE INDEX IF NOT EXISTS idx_tasks_owner ON tasks(owner_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_tasks_team ON tasks(team_id);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_tasks_trigger_token
|
||||
ON tasks(trigger_token) WHERE trigger_token IS NOT NULL;
|
||||
|
||||
|
||||
-- =========================================
|
||||
-- TASK RUN HISTORY
|
||||
-- =========================================
|
||||
-- ── Task Runs ───────────────────────────────
|
||||
|
||||
CREATE TABLE IF NOT EXISTS task_runs (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
task_id UUID NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
||||
channel_id UUID REFERENCES channels(id) ON DELETE SET NULL,
|
||||
status TEXT NOT NULL DEFAULT 'running'
|
||||
CHECK (status IN ('queued', 'running', 'completed', 'failed', 'budget_exceeded', 'cancelled')),
|
||||
trigger_payload TEXT,
|
||||
@@ -88,5 +65,5 @@ CREATE TABLE IF NOT EXISTS task_runs (
|
||||
error TEXT
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_task_runs_task ON task_runs (task_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_task_runs_status ON task_runs (task_id, status);
|
||||
CREATE INDEX IF NOT EXISTS idx_task_runs_task ON task_runs(task_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_task_runs_status ON task_runs(task_id, status);
|
||||
21
server/database/migrations/postgres/009_ha.sql
Normal file
21
server/database/migrations/postgres/009_ha.sql
Normal file
@@ -0,0 +1,21 @@
|
||||
-- ==========================================
|
||||
-- Switchboard Core — 009 Multi-Replica HA
|
||||
-- ==========================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ws_tickets (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
expires_at TIMESTAMPTZ NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_ws_tickets_expires ON ws_tickets(expires_at);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS rate_limit_counters (
|
||||
key TEXT NOT NULL,
|
||||
window_start TIMESTAMPTZ NOT NULL,
|
||||
tokens REAL NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (key, window_start)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_rate_limit_counters_window
|
||||
ON rate_limit_counters(window_start);
|
||||
@@ -1,7 +1,6 @@
|
||||
-- ==========================================
|
||||
-- Chat Switchboard — 001 Core (SQLite)
|
||||
-- Switchboard Core — 001 Core (SQLite)
|
||||
-- ==========================================
|
||||
-- Consolidated v0.28.0
|
||||
|
||||
PRAGMA journal_mode = WAL;
|
||||
PRAGMA foreign_keys = ON;
|
||||
@@ -59,13 +58,8 @@ CREATE TABLE IF NOT EXISTS platform_policies (
|
||||
);
|
||||
|
||||
INSERT OR IGNORE INTO platform_policies (key, value) VALUES
|
||||
('allow_user_byok', 'false'),
|
||||
('allow_user_personas', 'false'),
|
||||
('allow_raw_model_access', 'true'),
|
||||
('allow_registration', 'true'),
|
||||
('default_user_active', 'false'),
|
||||
('allow_team_providers', 'true'),
|
||||
('kb_direct_access', 'true');
|
||||
('allow_registration', 'true'),
|
||||
('default_user_active', 'false');
|
||||
|
||||
CREATE TABLE IF NOT EXISTS global_settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
@@ -76,15 +70,23 @@ CREATE TABLE IF NOT EXISTS global_settings (
|
||||
|
||||
INSERT OR IGNORE INTO global_settings (key, value) VALUES
|
||||
('registration', '{"enabled": true}'),
|
||||
('site', '{"name": "Chat Switchboard", "tagline": "Multi-Model AI Chat"}'),
|
||||
('banner', '{"enabled": false, "text": "", "bg": "#007a33", "fg": "#ffffff"}'),
|
||||
('model_roles', '{"utility":{"primary":null,"fallback":null},"embedding":{"primary":null,"fallback":null},"generation":{"primary":null,"fallback":null}}'),
|
||||
('retention_ttl_days', '{"value": 0}');
|
||||
('site', '{"name": "Switchboard Core", "tagline": "Extension Platform"}'),
|
||||
('banner', '{"enabled": false, "text": "", "bg": "#007a33", "fg": "#ffffff"}');
|
||||
|
||||
CREATE TABLE IF NOT EXISTS user_presence (
|
||||
user_id TEXT PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
|
||||
last_seen TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
status TEXT NOT NULL DEFAULT 'online'
|
||||
CHECK (status IN ('online', 'away', 'offline'))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_user_presence_last_seen ON user_presence(last_seen DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_presence_last_seen ON user_presence(last_seen);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS oidc_auth_state (
|
||||
state TEXT PRIMARY KEY,
|
||||
nonce TEXT NOT NULL,
|
||||
redirect_to TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_oidc_state_created ON oidc_auth_state(created_at);
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
-- Chat Switchboard — 002 Teams & Access Control (SQLite)
|
||||
-- Consolidated v0.28.0
|
||||
-- ==========================================
|
||||
-- Switchboard Core — 002 Teams & Access Control (SQLite)
|
||||
-- ==========================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS teams (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
description TEXT DEFAULT '',
|
||||
created_by TEXT NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
|
||||
created_by TEXT NOT NULL REFERENCES users(id),
|
||||
is_active INTEGER DEFAULT 1,
|
||||
settings TEXT DEFAULT '{}',
|
||||
created_at TEXT DEFAULT (datetime('now')),
|
||||
@@ -14,6 +15,10 @@ CREATE TABLE IF NOT EXISTS teams (
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_teams_active ON teams(is_active);
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS teams_updated_at AFTER UPDATE ON teams
|
||||
FOR EACH ROW WHEN NEW.updated_at = OLD.updated_at
|
||||
BEGIN UPDATE teams SET updated_at = datetime('now') WHERE id = NEW.id; END;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS team_members (
|
||||
id TEXT PRIMARY KEY,
|
||||
team_id TEXT NOT NULL REFERENCES teams(id) ON DELETE CASCADE,
|
||||
@@ -43,7 +48,11 @@ CREATE TABLE IF NOT EXISTS groups (
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_groups_name_scope
|
||||
ON groups(name, COALESCE(team_id, ''));
|
||||
ON groups(name, COALESCE(team_id, '00000000-0000-0000-0000-000000000000'));
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS groups_updated_at AFTER UPDATE ON groups
|
||||
FOR EACH ROW WHEN NEW.updated_at = OLD.updated_at
|
||||
BEGIN UPDATE groups SET updated_at = datetime('now') WHERE id = NEW.id; END;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS group_members (
|
||||
id TEXT PRIMARY KEY,
|
||||
@@ -57,13 +66,11 @@ CREATE TABLE IF NOT EXISTS group_members (
|
||||
CREATE INDEX IF NOT EXISTS idx_group_members_group ON group_members(group_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_group_members_user ON group_members(user_id);
|
||||
|
||||
INSERT OR IGNORE INTO groups
|
||||
(id, name, description, scope, created_by, source, permissions, created_at, updated_at)
|
||||
INSERT OR IGNORE INTO groups (id, name, description, scope, created_by, source, permissions)
|
||||
VALUES (
|
||||
'00000000-0000-0000-0000-000000000001',
|
||||
'Everyone',
|
||||
'Implicit group — all authenticated users receive these permissions.',
|
||||
'global', NULL, 'system',
|
||||
'["model.use","kb.read","channel.create"]',
|
||||
datetime('now'), datetime('now')
|
||||
'["extension.use","workflow.submit"]'
|
||||
);
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
-- Chat Switchboard — 016 Packages (SQLite)
|
||||
-- Unified package registry. Replaces surface_registry + extensions.
|
||||
-- v0.28.7: packages + package_user_settings
|
||||
-- v0.29.0: status column + extension_permissions table
|
||||
-- v0.29.2: ext_data_tables catalog + platform read views
|
||||
-- ==========================================
|
||||
-- Switchboard Core — 003 Packages & Extensions (SQLite)
|
||||
-- ==========================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS packages (
|
||||
id TEXT PRIMARY KEY,
|
||||
title TEXT NOT NULL,
|
||||
type TEXT NOT NULL DEFAULT 'surface'
|
||||
CHECK (type IN ('surface', 'extension', 'full', 'workflow')),
|
||||
CHECK (type IN ('surface', 'extension', 'full', 'workflow', 'library')),
|
||||
version TEXT NOT NULL DEFAULT '0.0.0',
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
author TEXT NOT NULL DEFAULT '',
|
||||
@@ -37,20 +35,14 @@ CREATE INDEX IF NOT EXISTS idx_packages_team ON packages(team_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_packages_source ON packages(source);
|
||||
CREATE INDEX IF NOT EXISTS idx_packages_status ON packages(status);
|
||||
|
||||
|
||||
-- Per-user overrides for packages (enable/disable, custom config).
|
||||
|
||||
CREATE TABLE IF NOT EXISTS package_user_settings (
|
||||
package_id TEXT NOT NULL REFERENCES packages(id) ON DELETE CASCADE,
|
||||
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
settings TEXT NOT NULL DEFAULT '{}',
|
||||
settings TEXT NOT NULL DEFAULT '{}',
|
||||
is_enabled INTEGER NOT NULL DEFAULT 1,
|
||||
PRIMARY KEY (package_id, user_id)
|
||||
);
|
||||
|
||||
|
||||
-- Extension permissions (v0.29.0)
|
||||
|
||||
CREATE TABLE IF NOT EXISTS extension_permissions (
|
||||
id TEXT PRIMARY KEY,
|
||||
package_id TEXT NOT NULL REFERENCES packages(id) ON DELETE CASCADE,
|
||||
@@ -65,10 +57,6 @@ CREATE TABLE IF NOT EXISTS extension_permissions (
|
||||
CREATE INDEX IF NOT EXISTS idx_ext_perm_package ON extension_permissions(package_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_ext_perm_granted ON extension_permissions(granted);
|
||||
|
||||
|
||||
-- Extension data tables catalog (v0.29.2)
|
||||
-- Tracks namespaced tables created by extension install hooks.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ext_data_tables (
|
||||
package_id TEXT NOT NULL REFERENCES packages(id) ON DELETE CASCADE,
|
||||
table_name TEXT NOT NULL,
|
||||
@@ -78,14 +66,5 @@ CREATE TABLE IF NOT EXISTS ext_data_tables (
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_ext_data_tables_pkg ON ext_data_tables(package_id);
|
||||
|
||||
|
||||
-- Platform views — column-allowlisted read access for extensions (v0.29.2)
|
||||
-- Extensions query these via db.view("users"), db.view("channels").
|
||||
|
||||
DROP VIEW IF EXISTS ext_view_users;
|
||||
CREATE VIEW ext_view_users AS
|
||||
CREATE VIEW IF NOT EXISTS ext_view_users AS
|
||||
SELECT id, display_name, email FROM users;
|
||||
|
||||
DROP VIEW IF EXISTS ext_view_channels;
|
||||
CREATE VIEW ext_view_channels AS
|
||||
SELECT id, title, type, team_id FROM channels;
|
||||
@@ -1,110 +0,0 @@
|
||||
-- Chat Switchboard — 003 Providers & Routing (SQLite)
|
||||
-- Consolidated v0.28.0: adds capability_match policy type
|
||||
|
||||
CREATE TABLE IF NOT EXISTS provider_configs (
|
||||
id TEXT PRIMARY KEY,
|
||||
scope TEXT NOT NULL CHECK (scope IN ('global', 'team', 'personal')),
|
||||
owner_id TEXT,
|
||||
name TEXT NOT NULL,
|
||||
provider TEXT NOT NULL,
|
||||
endpoint TEXT NOT NULL,
|
||||
api_key_enc BLOB,
|
||||
key_nonce BLOB,
|
||||
key_scope TEXT NOT NULL DEFAULT 'global',
|
||||
model_default TEXT,
|
||||
config TEXT DEFAULT '{}',
|
||||
headers TEXT DEFAULT '{}',
|
||||
settings TEXT DEFAULT '{}',
|
||||
is_active INTEGER DEFAULT 1,
|
||||
is_private INTEGER DEFAULT 0,
|
||||
proxy_mode TEXT NOT NULL DEFAULT 'system' CHECK (proxy_mode IN ('system', 'direct', 'custom')),
|
||||
proxy_url TEXT,
|
||||
created_at TEXT DEFAULT (datetime('now')),
|
||||
updated_at TEXT DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_provider_configs_scope ON provider_configs(scope);
|
||||
CREATE INDEX IF NOT EXISTS idx_provider_configs_owner ON provider_configs(owner_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_provider_configs_active ON provider_configs(is_active);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS model_catalog (
|
||||
id TEXT PRIMARY KEY,
|
||||
provider_config_id TEXT NOT NULL REFERENCES provider_configs(id) ON DELETE CASCADE,
|
||||
model_id TEXT NOT NULL,
|
||||
display_name TEXT,
|
||||
model_type TEXT DEFAULT 'chat',
|
||||
capabilities TEXT NOT NULL DEFAULT '{}',
|
||||
pricing TEXT,
|
||||
visibility TEXT DEFAULT 'disabled' CHECK (visibility IN ('enabled', 'disabled', 'team')),
|
||||
last_synced_at TEXT,
|
||||
created_at TEXT DEFAULT (datetime('now')),
|
||||
updated_at TEXT DEFAULT (datetime('now')),
|
||||
UNIQUE(provider_config_id, model_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_model_catalog_provider ON model_catalog(provider_config_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_model_catalog_enabled ON model_catalog(visibility);
|
||||
CREATE INDEX IF NOT EXISTS idx_model_catalog_type ON model_catalog(model_type);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS model_pricing (
|
||||
id TEXT PRIMARY KEY,
|
||||
provider_config_id TEXT NOT NULL REFERENCES provider_configs(id) ON DELETE CASCADE,
|
||||
model_id TEXT NOT NULL,
|
||||
input_per_m REAL,
|
||||
output_per_m REAL,
|
||||
cache_create_per_m REAL,
|
||||
cache_read_per_m REAL,
|
||||
currency TEXT NOT NULL DEFAULT 'USD',
|
||||
source TEXT NOT NULL DEFAULT 'manual',
|
||||
updated_at TEXT DEFAULT (datetime('now')),
|
||||
updated_by TEXT REFERENCES users(id),
|
||||
UNIQUE(provider_config_id, model_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS provider_health (
|
||||
id TEXT PRIMARY KEY,
|
||||
provider_config_id TEXT NOT NULL REFERENCES provider_configs(id) ON DELETE CASCADE,
|
||||
window_start TEXT NOT NULL,
|
||||
request_count INTEGER NOT NULL DEFAULT 0,
|
||||
error_count INTEGER NOT NULL DEFAULT 0,
|
||||
timeout_count INTEGER NOT NULL DEFAULT 0,
|
||||
total_latency_ms INTEGER NOT NULL DEFAULT 0,
|
||||
max_latency_ms INTEGER NOT NULL DEFAULT 0,
|
||||
rate_limit_count INTEGER NOT NULL DEFAULT 0,
|
||||
last_error TEXT,
|
||||
last_error_at TEXT,
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
UNIQUE (provider_config_id, window_start)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_provider_health_window
|
||||
ON provider_health (provider_config_id, window_start DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS capability_overrides (
|
||||
id TEXT PRIMARY KEY,
|
||||
provider_config_id TEXT REFERENCES provider_configs(id) ON DELETE CASCADE,
|
||||
model_id TEXT NOT NULL,
|
||||
field TEXT NOT NULL,
|
||||
value TEXT NOT NULL,
|
||||
set_by TEXT REFERENCES users(id) ON DELETE SET NULL,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
UNIQUE (provider_config_id, model_id, field)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_capability_overrides_model ON capability_overrides(model_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS routing_policies (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
scope TEXT NOT NULL DEFAULT 'global' CHECK (scope IN ('global', 'team')),
|
||||
team_id TEXT REFERENCES teams(id) ON DELETE CASCADE,
|
||||
priority INTEGER NOT NULL DEFAULT 100,
|
||||
policy_type TEXT NOT NULL
|
||||
CHECK (policy_type IN ('provider_prefer', 'team_route', 'cost_limit', 'model_alias', 'capability_match')),
|
||||
config TEXT NOT NULL DEFAULT '{}',
|
||||
is_active INTEGER NOT NULL DEFAULT 1,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_routing_policies_active ON routing_policies(is_active, priority);
|
||||
59
server/database/migrations/sqlite/004_connections.sql
Normal file
59
server/database/migrations/sqlite/004_connections.sql
Normal file
@@ -0,0 +1,59 @@
|
||||
-- ==========================================
|
||||
-- Switchboard Core — 004 Connections & Dependencies (SQLite)
|
||||
-- ==========================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ext_connections (
|
||||
id TEXT PRIMARY KEY,
|
||||
type TEXT NOT NULL,
|
||||
package_id TEXT NOT NULL,
|
||||
scope TEXT NOT NULL CHECK (scope IN ('global', 'team', 'personal')),
|
||||
owner_id TEXT NOT NULL DEFAULT '',
|
||||
name TEXT NOT NULL,
|
||||
config TEXT NOT NULL DEFAULT '{}',
|
||||
is_active INTEGER DEFAULT 1,
|
||||
created_at TEXT DEFAULT (datetime('now')),
|
||||
updated_at TEXT DEFAULT (datetime('now')),
|
||||
UNIQUE(type, scope, owner_id, name)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_ext_connections_type ON ext_connections(type);
|
||||
CREATE INDEX IF NOT EXISTS idx_ext_connections_scope ON ext_connections(scope);
|
||||
CREATE INDEX IF NOT EXISTS idx_ext_connections_owner ON ext_connections(owner_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_ext_connections_active ON ext_connections(is_active);
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS ext_connections_updated_at AFTER UPDATE ON ext_connections
|
||||
FOR EACH ROW WHEN NEW.updated_at = OLD.updated_at
|
||||
BEGIN UPDATE ext_connections SET updated_at = datetime('now') WHERE id = NEW.id; END;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ext_dependencies (
|
||||
consumer_id TEXT NOT NULL,
|
||||
library_id TEXT NOT NULL,
|
||||
version_spec TEXT NOT NULL DEFAULT '>=0.0.0',
|
||||
resolved_ver TEXT NOT NULL DEFAULT '0.0.0',
|
||||
PRIMARY KEY (consumer_id, library_id),
|
||||
FOREIGN KEY (consumer_id) REFERENCES packages(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (library_id) REFERENCES packages(id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_ext_deps_consumer ON ext_dependencies(consumer_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_ext_deps_library ON ext_dependencies(library_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS resource_grants (
|
||||
id TEXT PRIMARY KEY,
|
||||
resource_type TEXT NOT NULL,
|
||||
resource_id TEXT NOT NULL,
|
||||
grant_scope TEXT NOT NULL DEFAULT 'team_only'
|
||||
CHECK (grant_scope IN ('team_only', 'global', 'groups')),
|
||||
granted_groups TEXT NOT NULL DEFAULT '[]',
|
||||
created_by TEXT NOT NULL REFERENCES users(id),
|
||||
created_at TEXT DEFAULT (datetime('now')),
|
||||
updated_at TEXT DEFAULT (datetime('now')),
|
||||
UNIQUE(resource_type, resource_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_resource_grants_resource
|
||||
ON resource_grants(resource_type, resource_id);
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS resource_grants_updated_at AFTER UPDATE ON resource_grants
|
||||
FOR EACH ROW WHEN NEW.updated_at = OLD.updated_at
|
||||
BEGIN UPDATE resource_grants SET updated_at = datetime('now') WHERE id = NEW.id; END;
|
||||
@@ -1,83 +0,0 @@
|
||||
-- Chat Switchboard — 004 Personas & Grants (SQLite)
|
||||
|
||||
CREATE TABLE IF NOT EXISTS personas (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
handle TEXT,
|
||||
description TEXT DEFAULT '',
|
||||
icon TEXT DEFAULT '',
|
||||
avatar TEXT DEFAULT '',
|
||||
base_model_id TEXT NOT NULL,
|
||||
provider_config_id TEXT REFERENCES provider_configs(id) ON DELETE SET NULL,
|
||||
system_prompt TEXT DEFAULT '',
|
||||
temperature REAL,
|
||||
max_tokens INTEGER,
|
||||
thinking_budget INTEGER,
|
||||
top_p REAL,
|
||||
memory_enabled INTEGER NOT NULL DEFAULT 1,
|
||||
memory_extraction_prompt TEXT,
|
||||
scope TEXT NOT NULL CHECK (scope IN ('global', 'team', 'personal')),
|
||||
owner_id TEXT,
|
||||
created_by TEXT NOT NULL REFERENCES users(id),
|
||||
is_active INTEGER DEFAULT 1,
|
||||
is_shared INTEGER DEFAULT 0,
|
||||
created_at TEXT DEFAULT (datetime('now')),
|
||||
updated_at TEXT DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_personas_scope ON personas(scope);
|
||||
CREATE INDEX IF NOT EXISTS idx_personas_owner ON personas(owner_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_personas_active ON personas(is_active);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_personas_handle ON personas(handle) WHERE handle IS NOT NULL;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS persona_groups (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT DEFAULT '',
|
||||
owner_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
scope TEXT NOT NULL DEFAULT 'personal' CHECK (scope IN ('global', 'team', 'personal')),
|
||||
team_id TEXT REFERENCES teams(id) ON DELETE CASCADE,
|
||||
created_at TEXT DEFAULT (datetime('now')),
|
||||
updated_at TEXT DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_persona_groups_owner ON persona_groups(owner_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS persona_group_members (
|
||||
id TEXT PRIMARY KEY,
|
||||
group_id TEXT NOT NULL REFERENCES persona_groups(id) ON DELETE CASCADE,
|
||||
persona_id TEXT NOT NULL REFERENCES personas(id) ON DELETE CASCADE,
|
||||
is_leader INTEGER NOT NULL DEFAULT 0,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
UNIQUE(group_id, persona_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_persona_group_members_group ON persona_group_members(group_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS persona_grants (
|
||||
id TEXT PRIMARY KEY,
|
||||
persona_id TEXT NOT NULL REFERENCES personas(id) ON DELETE CASCADE,
|
||||
grant_type TEXT NOT NULL,
|
||||
grant_ref TEXT NOT NULL,
|
||||
config TEXT DEFAULT '{}',
|
||||
created_at TEXT DEFAULT (datetime('now')),
|
||||
UNIQUE(persona_id, grant_type, grant_ref)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_persona_grants_persona ON persona_grants(persona_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_persona_grants_type ON persona_grants(grant_type);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS resource_grants (
|
||||
id TEXT PRIMARY KEY,
|
||||
resource_type TEXT NOT NULL CHECK (resource_type IN ('persona', 'knowledge_base', 'project')),
|
||||
resource_id TEXT NOT NULL,
|
||||
grant_scope TEXT NOT NULL DEFAULT 'team_only'
|
||||
CHECK (grant_scope IN ('team_only', 'global', 'groups')),
|
||||
granted_groups TEXT NOT NULL DEFAULT '[]',
|
||||
created_by TEXT NOT NULL REFERENCES users(id),
|
||||
created_at TEXT DEFAULT (datetime('now')),
|
||||
updated_at TEXT DEFAULT (datetime('now')),
|
||||
UNIQUE(resource_type, resource_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_resource_grants_resource ON resource_grants(resource_type, resource_id);
|
||||
@@ -1,160 +0,0 @@
|
||||
-- Chat Switchboard — 005 Channels & Conversations (SQLite)
|
||||
-- Consolidated v0.28.0
|
||||
|
||||
CREATE TABLE IF NOT EXISTS folders (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
parent_id TEXT REFERENCES folders(id) ON DELETE CASCADE,
|
||||
sort_order INTEGER DEFAULT 0,
|
||||
created_at TEXT DEFAULT (datetime('now')),
|
||||
updated_at TEXT DEFAULT (datetime('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);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS channels (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT REFERENCES users(id) ON DELETE CASCADE,
|
||||
title TEXT NOT NULL,
|
||||
description TEXT,
|
||||
type TEXT DEFAULT 'direct'
|
||||
CHECK (type IN ('direct', 'dm', 'group', 'channel', 'workflow', 'service')),
|
||||
model TEXT,
|
||||
system_prompt TEXT,
|
||||
provider_config_id TEXT REFERENCES provider_configs(id) ON DELETE SET NULL,
|
||||
is_archived INTEGER DEFAULT 0,
|
||||
is_pinned INTEGER DEFAULT 0,
|
||||
folder_id TEXT REFERENCES folders(id) ON DELETE SET NULL,
|
||||
folder TEXT,
|
||||
team_id TEXT REFERENCES teams(id) ON DELETE SET NULL,
|
||||
settings TEXT DEFAULT '{}',
|
||||
tags TEXT DEFAULT '[]',
|
||||
ai_mode TEXT NOT NULL DEFAULT 'auto',
|
||||
topic TEXT,
|
||||
allow_anonymous INTEGER NOT NULL DEFAULT 0,
|
||||
workflow_id TEXT,
|
||||
workflow_version INTEGER,
|
||||
current_stage INTEGER DEFAULT 0,
|
||||
stage_data TEXT DEFAULT '{}',
|
||||
workflow_status TEXT DEFAULT 'active'
|
||||
CHECK (workflow_status IN ('active', 'completed', 'stale', 'cancelled')),
|
||||
last_activity_at TEXT DEFAULT (datetime('now')),
|
||||
stage_entered_at TEXT,
|
||||
kb_auto_inject INTEGER NOT NULL DEFAULT 0,
|
||||
project_id TEXT,
|
||||
workspace_id TEXT,
|
||||
purge_after TEXT,
|
||||
created_at TEXT DEFAULT (datetime('now')),
|
||||
updated_at TEXT DEFAULT (datetime('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);
|
||||
CREATE INDEX IF NOT EXISTS idx_channels_type ON channels(type);
|
||||
CREATE INDEX IF NOT EXISTS idx_channels_team ON channels(team_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_channels_folder ON channels(folder_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_channels_project ON channels(project_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_channels_workspace ON channels(workspace_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_channels_workflow ON channels(workflow_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS messages (
|
||||
id TEXT PRIMARY KEY,
|
||||
channel_id TEXT NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
|
||||
role TEXT NOT NULL CHECK (role IN ('user', 'assistant', 'system', 'tool')),
|
||||
content TEXT NOT NULL,
|
||||
model TEXT,
|
||||
tokens_used INTEGER,
|
||||
tool_calls TEXT,
|
||||
metadata TEXT DEFAULT '{}',
|
||||
parent_id TEXT REFERENCES messages(id) ON DELETE SET NULL,
|
||||
sibling_index INTEGER DEFAULT 0,
|
||||
participant_type TEXT DEFAULT 'user',
|
||||
participant_id TEXT,
|
||||
provider_config_id TEXT REFERENCES provider_configs(id) ON DELETE SET NULL,
|
||||
deleted_at TEXT,
|
||||
created_at TEXT DEFAULT (datetime('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 TABLE IF NOT EXISTS channel_participants (
|
||||
id TEXT PRIMARY KEY,
|
||||
channel_id TEXT NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
|
||||
participant_type TEXT NOT NULL DEFAULT 'user'
|
||||
CHECK (participant_type IN ('user', 'persona', 'session')),
|
||||
participant_id TEXT NOT NULL,
|
||||
role TEXT NOT NULL DEFAULT 'member'
|
||||
CHECK (role IN ('owner', 'member', 'observer')),
|
||||
display_name TEXT,
|
||||
avatar_url TEXT,
|
||||
joined_at TEXT DEFAULT (datetime('now')),
|
||||
last_read_at TEXT DEFAULT (datetime('now')),
|
||||
last_read_message_id TEXT,
|
||||
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 TEXT PRIMARY KEY,
|
||||
channel_id TEXT NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
|
||||
model_id TEXT NOT NULL,
|
||||
provider_config_id TEXT REFERENCES provider_configs(id) ON DELETE SET NULL,
|
||||
persona_id TEXT REFERENCES personas(id) ON DELETE SET NULL,
|
||||
display_name TEXT,
|
||||
system_prompt TEXT,
|
||||
settings TEXT DEFAULT '{}',
|
||||
is_default INTEGER DEFAULT 0,
|
||||
added_at TEXT DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_channel_models_raw
|
||||
ON channel_models(channel_id, model_id, provider_config_id) WHERE persona_id IS NULL;
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_channel_models_persona
|
||||
ON channel_models(channel_id, persona_id) WHERE persona_id IS NOT NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_channel_models_channel ON channel_models(channel_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS channel_cursors (
|
||||
id TEXT PRIMARY KEY,
|
||||
channel_id TEXT NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
|
||||
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
active_leaf_id TEXT REFERENCES messages(id) ON DELETE SET NULL,
|
||||
updated_at TEXT DEFAULT (datetime('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);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS user_model_settings (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
model_id TEXT NOT NULL,
|
||||
provider_config_id TEXT NOT NULL REFERENCES provider_configs(id) ON DELETE CASCADE,
|
||||
hidden INTEGER DEFAULT 0,
|
||||
preferred_temperature REAL,
|
||||
preferred_max_tokens INTEGER,
|
||||
sort_order INTEGER DEFAULT 0,
|
||||
created_at TEXT DEFAULT (datetime('now')),
|
||||
updated_at TEXT DEFAULT (datetime('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);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS session_participants (
|
||||
id TEXT PRIMARY KEY,
|
||||
session_token TEXT NOT NULL UNIQUE,
|
||||
channel_id TEXT NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
|
||||
display_name TEXT NOT NULL DEFAULT 'Visitor',
|
||||
fingerprint TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_session_participants_channel ON session_participants(channel_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_session_participants_token ON session_participants(session_token);
|
||||
@@ -1,4 +1,6 @@
|
||||
-- Chat Switchboard — 011 Notifications (SQLite)
|
||||
-- ==========================================
|
||||
-- Switchboard Core — 005 Notifications (SQLite)
|
||||
-- ==========================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS notifications (
|
||||
id TEXT PRIMARY KEY,
|
||||
@@ -12,8 +14,10 @@ CREATE TABLE IF NOT EXISTS notifications (
|
||||
created_at TEXT DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_notifications_user_unread ON notifications(user_id, is_read, created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_notifications_user_created ON notifications(user_id, created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_notifications_user_unread
|
||||
ON notifications(user_id, is_read, created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_notifications_user_created
|
||||
ON notifications(user_id, created_at);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS notification_preferences (
|
||||
id TEXT PRIMARY KEY,
|
||||
20
server/database/migrations/sqlite/006_audit.sql
Normal file
20
server/database/migrations/sqlite/006_audit.sql
Normal file
@@ -0,0 +1,20 @@
|
||||
-- ==========================================
|
||||
-- Switchboard Core — 006 Audit (SQLite)
|
||||
-- ==========================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS audit_log (
|
||||
id TEXT PRIMARY KEY,
|
||||
actor_id TEXT REFERENCES users(id) ON DELETE SET NULL,
|
||||
action TEXT NOT NULL,
|
||||
resource_type TEXT NOT NULL,
|
||||
resource_id TEXT,
|
||||
metadata TEXT DEFAULT '{}',
|
||||
ip_address TEXT,
|
||||
user_agent TEXT DEFAULT '',
|
||||
created_at TEXT DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_log_created ON audit_log(created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_log_actor ON audit_log(actor_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_log_resource ON audit_log(resource_type, resource_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_log_action ON audit_log(action);
|
||||
@@ -1,73 +0,0 @@
|
||||
-- Chat Switchboard — 006 Knowledge Bases (SQLite)
|
||||
|
||||
CREATE TABLE IF NOT EXISTS knowledge_bases (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
scope TEXT NOT NULL DEFAULT 'global',
|
||||
owner_id TEXT REFERENCES users(id) ON DELETE CASCADE,
|
||||
team_id TEXT REFERENCES teams(id) ON DELETE CASCADE,
|
||||
embedding_config TEXT NOT NULL DEFAULT '{}',
|
||||
document_count INTEGER NOT NULL DEFAULT 0,
|
||||
chunk_count INTEGER NOT NULL DEFAULT 0,
|
||||
total_bytes INTEGER NOT NULL DEFAULT 0,
|
||||
status TEXT NOT NULL DEFAULT 'active',
|
||||
discoverable INTEGER NOT NULL DEFAULT 1,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_kb_scope ON knowledge_bases(scope);
|
||||
CREATE INDEX IF NOT EXISTS idx_kb_owner ON knowledge_bases(owner_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_kb_team ON knowledge_bases(team_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS kb_documents (
|
||||
id TEXT PRIMARY KEY,
|
||||
kb_id TEXT NOT NULL REFERENCES knowledge_bases(id) ON DELETE CASCADE,
|
||||
filename TEXT NOT NULL,
|
||||
content_type TEXT NOT NULL,
|
||||
size_bytes INTEGER NOT NULL,
|
||||
storage_key TEXT NOT NULL,
|
||||
extracted_text TEXT,
|
||||
chunk_count INTEGER NOT NULL DEFAULT 0,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
error TEXT,
|
||||
uploaded_by TEXT NOT NULL REFERENCES users(id),
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_kbdoc_kb ON kb_documents(kb_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS kb_chunks (
|
||||
id TEXT PRIMARY KEY,
|
||||
kb_id TEXT NOT NULL REFERENCES knowledge_bases(id) ON DELETE CASCADE,
|
||||
document_id TEXT NOT NULL REFERENCES kb_documents(id) ON DELETE CASCADE,
|
||||
chunk_index INTEGER NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
token_count INTEGER NOT NULL DEFAULT 0,
|
||||
metadata TEXT NOT NULL DEFAULT '{}',
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_kbchunk_kb ON kb_chunks(kb_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_kbchunk_doc ON kb_chunks(document_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS channel_knowledge_bases (
|
||||
channel_id TEXT NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
|
||||
kb_id TEXT NOT NULL REFERENCES knowledge_bases(id) ON DELETE CASCADE,
|
||||
enabled INTEGER NOT NULL DEFAULT 1,
|
||||
added_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
PRIMARY KEY (channel_id, kb_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS persona_knowledge_bases (
|
||||
persona_id TEXT NOT NULL REFERENCES personas(id) ON DELETE CASCADE,
|
||||
kb_id TEXT NOT NULL REFERENCES knowledge_bases(id) ON DELETE CASCADE,
|
||||
auto_search INTEGER NOT NULL DEFAULT 0,
|
||||
added_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
PRIMARY KEY (persona_id, kb_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_persona_kb_persona ON persona_knowledge_bases(persona_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_persona_kb_kb ON persona_knowledge_bases(kb_id);
|
||||
@@ -1,34 +0,0 @@
|
||||
-- Chat Switchboard — 007 Notes & Links (SQLite)
|
||||
|
||||
CREATE TABLE IF NOT EXISTS notes (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
title TEXT NOT NULL,
|
||||
content TEXT DEFAULT '',
|
||||
folder_path TEXT DEFAULT '/',
|
||||
tags TEXT DEFAULT '[]',
|
||||
metadata TEXT DEFAULT '{}',
|
||||
source_channel_id TEXT REFERENCES channels(id) ON DELETE SET NULL,
|
||||
source_message_id TEXT,
|
||||
team_id TEXT REFERENCES teams(id) ON DELETE SET NULL,
|
||||
created_at TEXT DEFAULT (datetime('now')),
|
||||
updated_at TEXT DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_notes_user ON notes(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_notes_folder ON notes(user_id, folder_path);
|
||||
CREATE INDEX IF NOT EXISTS idx_notes_updated ON notes(user_id, updated_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_notes_team ON notes(team_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_notes_source_msg ON notes(source_message_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS note_links (
|
||||
source_note_id TEXT NOT NULL REFERENCES notes(id) ON DELETE CASCADE,
|
||||
target_note_id TEXT REFERENCES notes(id) ON DELETE CASCADE,
|
||||
target_title TEXT NOT NULL,
|
||||
display_text TEXT,
|
||||
is_transclusion INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT DEFAULT (datetime('now')),
|
||||
PRIMARY KEY (source_note_id, target_title)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_note_links_target ON note_links(target_note_id);
|
||||
@@ -1,5 +1,6 @@
|
||||
-- Chat Switchboard — 018 Workflows (SQLite)
|
||||
-- Consolidated v0.28.0: merges 023 + 025 + 027
|
||||
-- ==========================================
|
||||
-- Switchboard Core — 007 Workflows (SQLite)
|
||||
-- ==========================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS workflows (
|
||||
id TEXT PRIMARY KEY,
|
||||
@@ -22,25 +23,28 @@ CREATE TABLE IF NOT EXISTS workflows (
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_workflows_team_slug
|
||||
ON workflows(team_id, slug) WHERE team_id IS NOT NULL;
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_workflows_global_slug
|
||||
ON workflows(slug) WHERE team_id IS NULL;
|
||||
ON workflows(team_id, slug);
|
||||
CREATE INDEX IF NOT EXISTS idx_workflows_active ON workflows(is_active);
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS workflows_updated_at AFTER UPDATE ON workflows
|
||||
FOR EACH ROW WHEN NEW.updated_at = OLD.updated_at
|
||||
BEGIN UPDATE workflows SET updated_at = datetime('now') WHERE id = NEW.id; END;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS workflow_stages (
|
||||
id TEXT PRIMARY KEY,
|
||||
workflow_id TEXT NOT NULL REFERENCES workflows(id) ON DELETE CASCADE,
|
||||
ordinal INTEGER NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
persona_id TEXT REFERENCES personas(id) ON DELETE SET NULL,
|
||||
persona_id TEXT,
|
||||
assignment_team_id TEXT REFERENCES teams(id) ON DELETE SET NULL,
|
||||
form_template TEXT NOT NULL DEFAULT '{}',
|
||||
stage_mode TEXT NOT NULL DEFAULT 'chat_only'
|
||||
CHECK (stage_mode IN ('chat_only', 'form_only', 'form_chat', 'review')),
|
||||
stage_mode TEXT NOT NULL DEFAULT 'form_only'
|
||||
CHECK (stage_mode IN ('form_only', 'form_chat', 'review', 'custom')),
|
||||
history_mode TEXT NOT NULL DEFAULT 'full'
|
||||
CHECK (history_mode IN ('full', 'summary', 'fresh')),
|
||||
auto_transition INTEGER NOT NULL DEFAULT 0,
|
||||
transition_rules TEXT NOT NULL DEFAULT '{}',
|
||||
surface_pkg_id TEXT,
|
||||
surface_pkg_id TEXT REFERENCES packages(id) ON DELETE SET NULL,
|
||||
sla_seconds INTEGER,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
@@ -59,22 +63,3 @@ CREATE TABLE IF NOT EXISTS workflow_versions (
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_workflow_versions_workflow
|
||||
ON workflow_versions(workflow_id, version_number);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS workflow_assignments (
|
||||
id TEXT PRIMARY KEY,
|
||||
channel_id TEXT NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
|
||||
stage INTEGER NOT NULL,
|
||||
team_id TEXT NOT NULL REFERENCES teams(id) ON DELETE CASCADE,
|
||||
assigned_to TEXT REFERENCES users(id) ON DELETE SET NULL,
|
||||
status TEXT NOT NULL DEFAULT 'unassigned'
|
||||
CHECK (status IN ('unassigned', 'claimed', 'completed')),
|
||||
review_comments TEXT NOT NULL DEFAULT '[]',
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
claimed_at TEXT,
|
||||
completed_at TEXT
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_workflow_assignments_channel
|
||||
ON workflow_assignments(channel_id, stage);
|
||||
CREATE INDEX IF NOT EXISTS idx_workflow_assignments_team_status
|
||||
ON workflow_assignments(team_id, status);
|
||||
@@ -1,45 +0,0 @@
|
||||
-- Chat Switchboard — 008 Memory (SQLite)
|
||||
-- Consolidated v0.28.0: adds last_compacted_at + decay_rate
|
||||
|
||||
CREATE TABLE IF NOT EXISTS memories (
|
||||
id TEXT PRIMARY KEY,
|
||||
scope TEXT NOT NULL CHECK (scope IN ('user', 'persona', 'persona_user')),
|
||||
owner_id TEXT NOT NULL,
|
||||
user_id TEXT,
|
||||
key TEXT NOT NULL,
|
||||
value TEXT NOT NULL,
|
||||
source_channel_id TEXT,
|
||||
confidence REAL NOT NULL DEFAULT 1.0,
|
||||
status TEXT NOT NULL DEFAULT 'active'
|
||||
CHECK (status IN ('active', 'pending_review', 'archived')),
|
||||
embedding TEXT,
|
||||
last_compacted_at TEXT,
|
||||
decay_rate REAL NOT NULL DEFAULT 0.0,
|
||||
created_at TEXT DEFAULT (datetime('now')),
|
||||
updated_at TEXT DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_memories_scope_owner
|
||||
ON memories(scope, owner_id, status);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_memories_persona_user
|
||||
ON memories(owner_id, user_id);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_memories_unique_key
|
||||
ON memories(scope, owner_id, COALESCE(user_id, '00000000-0000-0000-0000-000000000000'), key);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_memories_source_channel
|
||||
ON memories(source_channel_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS memory_extraction_log (
|
||||
id TEXT PRIMARY KEY,
|
||||
channel_id TEXT NOT NULL,
|
||||
user_id TEXT NOT NULL,
|
||||
last_message_id TEXT NOT NULL,
|
||||
extracted_at TEXT DEFAULT (datetime('now')),
|
||||
memory_count INTEGER NOT NULL DEFAULT 0,
|
||||
UNIQUE(channel_id, user_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_mem_extract_log_channel
|
||||
ON memory_extraction_log(channel_id);
|
||||
@@ -1,6 +1,6 @@
|
||||
-- Chat Switchboard — 019 Tasks (SQLite)
|
||||
-- Consolidated v0.28.0: merges 026 + 027
|
||||
-- v0.28.0-cs3: trigger_token, action task_type, queued run status.
|
||||
-- ==========================================
|
||||
-- Switchboard Core — 008 Tasks (SQLite)
|
||||
-- ==========================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tasks (
|
||||
id TEXT PRIMARY KEY,
|
||||
@@ -8,10 +8,12 @@ CREATE TABLE IF NOT EXISTS tasks (
|
||||
team_id TEXT REFERENCES teams(id) ON DELETE SET NULL,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT DEFAULT '',
|
||||
scope TEXT NOT NULL DEFAULT 'personal',
|
||||
task_type TEXT NOT NULL DEFAULT 'prompt',
|
||||
scope TEXT NOT NULL DEFAULT 'personal'
|
||||
CHECK (scope IN ('personal', 'team', 'global')),
|
||||
task_type TEXT NOT NULL DEFAULT 'action'
|
||||
CHECK (task_type IN ('action', 'workflow', 'system')),
|
||||
system_function TEXT DEFAULT '',
|
||||
persona_id TEXT REFERENCES personas(id) ON DELETE SET NULL,
|
||||
persona_id TEXT,
|
||||
model_id TEXT,
|
||||
system_prompt TEXT DEFAULT '',
|
||||
user_prompt TEXT DEFAULT '',
|
||||
@@ -24,11 +26,10 @@ CREATE TABLE IF NOT EXISTS tasks (
|
||||
max_tokens INTEGER NOT NULL DEFAULT 4096,
|
||||
max_tool_calls INTEGER NOT NULL DEFAULT 10,
|
||||
max_wall_clock INTEGER NOT NULL DEFAULT 300,
|
||||
output_mode TEXT NOT NULL DEFAULT 'channel',
|
||||
output_channel_id TEXT REFERENCES channels(id) ON DELETE SET NULL,
|
||||
output_mode TEXT NOT NULL DEFAULT 'log'
|
||||
CHECK (output_mode IN ('notification', 'webhook', 'log')),
|
||||
webhook_url TEXT,
|
||||
webhook_secret TEXT,
|
||||
provider_config_id TEXT REFERENCES provider_configs(id) ON DELETE SET NULL,
|
||||
notify_on_complete INTEGER NOT NULL DEFAULT 0,
|
||||
notify_on_failure INTEGER NOT NULL DEFAULT 1,
|
||||
last_run_at TEXT,
|
||||
@@ -38,16 +39,16 @@ CREATE TABLE IF NOT EXISTS tasks (
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_tasks_next_run ON tasks (next_run_at) WHERE is_active = 1;
|
||||
CREATE INDEX IF NOT EXISTS idx_tasks_owner ON tasks (owner_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_tasks_team ON tasks (team_id);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_tasks_trigger_token ON tasks (trigger_token) WHERE trigger_token IS NOT NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_tasks_next_run ON tasks(next_run_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_tasks_owner ON tasks(owner_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_tasks_team ON tasks(team_id);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_tasks_trigger_token ON tasks(trigger_token);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS task_runs (
|
||||
id TEXT PRIMARY KEY,
|
||||
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
||||
channel_id TEXT REFERENCES channels(id) ON DELETE SET NULL,
|
||||
status TEXT NOT NULL DEFAULT 'running',
|
||||
status TEXT NOT NULL DEFAULT 'running'
|
||||
CHECK (status IN ('queued', 'running', 'completed', 'failed', 'budget_exceeded', 'cancelled')),
|
||||
trigger_payload TEXT,
|
||||
started_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
completed_at TEXT,
|
||||
@@ -57,5 +58,5 @@ CREATE TABLE IF NOT EXISTS task_runs (
|
||||
error TEXT
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_task_runs_task ON task_runs (task_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_task_runs_status ON task_runs (task_id, status);
|
||||
CREATE INDEX IF NOT EXISTS idx_task_runs_task ON task_runs(task_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_task_runs_status ON task_runs(task_id, status);
|
||||
21
server/database/migrations/sqlite/009_ha.sql
Normal file
21
server/database/migrations/sqlite/009_ha.sql
Normal file
@@ -0,0 +1,21 @@
|
||||
-- ==========================================
|
||||
-- Switchboard Core — 009 Multi-Replica HA (SQLite)
|
||||
-- ==========================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ws_tickets (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
expires_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_ws_tickets_expires ON ws_tickets(expires_at);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS rate_limit_counters (
|
||||
key TEXT NOT NULL,
|
||||
window_start TEXT NOT NULL,
|
||||
tokens REAL NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (key, window_start)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_rate_limit_counters_window
|
||||
ON rate_limit_counters(window_start);
|
||||
@@ -1,69 +0,0 @@
|
||||
-- Chat Switchboard — 009 Workspaces & Git (SQLite)
|
||||
|
||||
CREATE TABLE IF NOT EXISTS workspaces (
|
||||
id TEXT PRIMARY KEY,
|
||||
owner_type TEXT NOT NULL CHECK (owner_type IN ('user', 'project', 'channel', 'team')),
|
||||
owner_id TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
root_path TEXT NOT NULL,
|
||||
max_bytes INTEGER,
|
||||
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'archived', 'deleting')),
|
||||
indexing_enabled INTEGER NOT NULL DEFAULT 1,
|
||||
git_remote_url TEXT,
|
||||
git_branch TEXT,
|
||||
git_credential_id TEXT,
|
||||
git_last_sync TEXT,
|
||||
created_at TEXT DEFAULT (datetime('now')),
|
||||
updated_at TEXT DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_workspaces_owner ON workspaces(owner_type, owner_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_workspaces_status ON workspaces(status);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS workspace_files (
|
||||
id TEXT PRIMARY KEY,
|
||||
workspace_id TEXT NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
|
||||
path TEXT NOT NULL,
|
||||
is_directory INTEGER NOT NULL DEFAULT 0,
|
||||
content_type TEXT,
|
||||
size_bytes INTEGER NOT NULL DEFAULT 0,
|
||||
sha256 TEXT,
|
||||
index_status TEXT DEFAULT 'pending' CHECK (index_status IN ('pending', 'indexing', 'ready', 'error', 'skipped')),
|
||||
chunk_count INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT DEFAULT (datetime('now')),
|
||||
updated_at TEXT DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_workspace_files_path ON workspace_files(workspace_id, path);
|
||||
CREATE INDEX IF NOT EXISTS idx_workspace_files_workspace ON workspace_files(workspace_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_workspace_files_type ON workspace_files(workspace_id, content_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_workspace_files_index_status ON workspace_files(workspace_id, index_status);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS workspace_chunks (
|
||||
id TEXT PRIMARY KEY,
|
||||
workspace_id TEXT NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
|
||||
file_id TEXT NOT NULL REFERENCES workspace_files(id) ON DELETE CASCADE,
|
||||
chunk_index INTEGER NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
token_count INTEGER NOT NULL DEFAULT 0,
|
||||
metadata TEXT DEFAULT '{}',
|
||||
created_at TEXT DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_workspace_chunks_file ON workspace_chunks(file_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_workspace_chunks_workspace ON workspace_chunks(workspace_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS git_credentials (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL DEFAULT '',
|
||||
auth_type TEXT NOT NULL DEFAULT 'https_pat',
|
||||
encrypted_data BLOB NOT NULL DEFAULT '',
|
||||
nonce BLOB NOT NULL DEFAULT '',
|
||||
public_key TEXT NOT NULL DEFAULT '',
|
||||
fingerprint TEXT NOT NULL DEFAULT '',
|
||||
persona_id TEXT REFERENCES personas(id) ON DELETE SET NULL,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_git_credentials_user ON git_credentials(user_id);
|
||||
@@ -1,62 +0,0 @@
|
||||
-- Chat Switchboard — 010 Projects (SQLite)
|
||||
|
||||
CREATE TABLE IF NOT EXISTS projects (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
color TEXT,
|
||||
icon TEXT,
|
||||
scope TEXT NOT NULL DEFAULT 'personal' CHECK (scope IN ('personal', 'team', 'global')),
|
||||
owner_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
team_id TEXT REFERENCES teams(id) ON DELETE SET NULL,
|
||||
workspace_id TEXT REFERENCES workspaces(id) ON DELETE SET NULL,
|
||||
is_archived INTEGER NOT NULL DEFAULT 0,
|
||||
settings TEXT DEFAULT '{}',
|
||||
created_at TEXT DEFAULT (datetime('now')),
|
||||
updated_at TEXT DEFAULT (datetime('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);
|
||||
CREATE INDEX IF NOT EXISTS idx_projects_scope ON projects(scope);
|
||||
CREATE INDEX IF NOT EXISTS idx_projects_workspace ON projects(workspace_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS project_channels (
|
||||
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
||||
channel_id TEXT NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
|
||||
position INTEGER NOT NULL DEFAULT 0,
|
||||
folder TEXT,
|
||||
added_at TEXT NOT NULL DEFAULT (datetime('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);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS project_knowledge_bases (
|
||||
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
||||
kb_id TEXT NOT NULL REFERENCES knowledge_bases(id) ON DELETE CASCADE,
|
||||
auto_search INTEGER NOT NULL DEFAULT 0,
|
||||
added_at TEXT NOT NULL DEFAULT (datetime('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);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS project_notes (
|
||||
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
||||
note_id TEXT NOT NULL REFERENCES notes(id) ON DELETE CASCADE,
|
||||
added_at TEXT NOT NULL DEFAULT (datetime('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 on channels (SQLite cannot ADD COLUMN with FK in all cases,
|
||||
-- so these may already exist from initial channel creation — safe to skip on error)
|
||||
-- ALTER TABLE channels ADD COLUMN project_id TEXT REFERENCES projects(id) ON DELETE SET NULL;
|
||||
-- ALTER TABLE channels ADD COLUMN workspace_id TEXT REFERENCES workspaces(id) ON DELETE SET NULL;
|
||||
-- Note: SQLite ADD COLUMN IF NOT EXISTS not supported; channels table created with these columns.
|
||||
@@ -1,3 +0,0 @@
|
||||
-- Chat Switchboard — 012 (placeholder, SQLite)
|
||||
-- Previously: extensions + extension_user_settings tables.
|
||||
-- v0.28.7: Both tables removed. Unified package registry in 016.
|
||||
@@ -1,28 +0,0 @@
|
||||
-- Chat Switchboard — 013 Files (SQLite)
|
||||
-- Consolidated v0.28.0: clean install, no legacy attachments migration
|
||||
|
||||
CREATE TABLE IF NOT EXISTS files (
|
||||
id TEXT PRIMARY KEY,
|
||||
channel_id TEXT REFERENCES channels(id) ON DELETE CASCADE,
|
||||
message_id TEXT REFERENCES messages(id) ON DELETE SET NULL,
|
||||
user_id TEXT REFERENCES users(id) ON DELETE SET NULL,
|
||||
project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
|
||||
origin TEXT NOT NULL DEFAULT 'user_upload'
|
||||
CHECK (origin IN ('user_upload', 'tool_output', 'system')),
|
||||
filename TEXT NOT NULL,
|
||||
content_type TEXT NOT NULL DEFAULT 'application/octet-stream',
|
||||
size_bytes INTEGER NOT NULL DEFAULT 0,
|
||||
storage_key TEXT NOT NULL,
|
||||
display_hint TEXT NOT NULL DEFAULT 'download'
|
||||
CHECK (display_hint IN ('inline', 'download', 'thumbnail')),
|
||||
extracted_text TEXT,
|
||||
metadata TEXT DEFAULT '{}',
|
||||
created_at TEXT DEFAULT (datetime('now')),
|
||||
updated_at TEXT DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_files_channel ON files(channel_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_files_message ON files(message_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_files_user ON files(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_files_project ON files(project_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_files_origin ON files(origin);
|
||||
@@ -1,42 +0,0 @@
|
||||
-- Chat Switchboard — 014 Audit & Usage (SQLite)
|
||||
|
||||
CREATE TABLE IF NOT EXISTS audit_log (
|
||||
id TEXT PRIMARY KEY,
|
||||
actor_id TEXT REFERENCES users(id) ON DELETE SET NULL,
|
||||
action TEXT NOT NULL,
|
||||
resource_type TEXT NOT NULL,
|
||||
resource_id TEXT,
|
||||
metadata TEXT DEFAULT '{}',
|
||||
ip_address TEXT,
|
||||
user_agent TEXT DEFAULT '',
|
||||
created_at TEXT DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_log_created ON audit_log(created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_log_actor ON audit_log(actor_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_log_resource ON audit_log(resource_type, resource_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_log_action ON audit_log(action);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS usage_log (
|
||||
id TEXT PRIMARY KEY,
|
||||
channel_id TEXT REFERENCES channels(id) ON DELETE SET NULL,
|
||||
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
provider_config_id TEXT REFERENCES provider_configs(id) ON DELETE SET NULL,
|
||||
provider_scope TEXT NOT NULL DEFAULT 'global',
|
||||
model_id TEXT NOT NULL,
|
||||
role TEXT,
|
||||
prompt_tokens INTEGER NOT NULL DEFAULT 0,
|
||||
completion_tokens INTEGER NOT NULL DEFAULT 0,
|
||||
cache_creation_tokens INTEGER NOT NULL DEFAULT 0,
|
||||
cache_read_tokens INTEGER NOT NULL DEFAULT 0,
|
||||
cost_input REAL,
|
||||
cost_output REAL,
|
||||
routing_decision TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_usage_log_user ON usage_log(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_usage_log_created ON usage_log(created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_usage_log_provider ON usage_log(provider_config_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_usage_log_model ON usage_log(model_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_usage_log_scope ON usage_log(provider_scope);
|
||||
@@ -1,17 +0,0 @@
|
||||
-- Chat Switchboard — 015 Tool Health (SQLite)
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tool_health (
|
||||
id TEXT PRIMARY KEY,
|
||||
tool_name TEXT NOT NULL,
|
||||
window_start TEXT NOT NULL,
|
||||
request_count INTEGER NOT NULL DEFAULT 0,
|
||||
error_count INTEGER NOT NULL DEFAULT 0,
|
||||
total_latency_ms INTEGER NOT NULL DEFAULT 0,
|
||||
max_latency_ms INTEGER NOT NULL DEFAULT 0,
|
||||
last_error TEXT,
|
||||
last_error_at TEXT,
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
UNIQUE (tool_name, window_start)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_tool_health_window ON tool_health(tool_name, window_start);
|
||||
@@ -1,11 +0,0 @@
|
||||
-- Chat Switchboard — 017 Auth Providers (SQLite)
|
||||
-- Consolidated v0.28.0
|
||||
|
||||
CREATE TABLE IF NOT EXISTS oidc_auth_state (
|
||||
state TEXT PRIMARY KEY,
|
||||
nonce TEXT NOT NULL,
|
||||
redirect_to TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_oidc_state_created ON oidc_auth_state(created_at);
|
||||
@@ -1,34 +0,0 @@
|
||||
-- ==========================================
|
||||
-- Chat Switchboard — 020 Multi-Replica HA
|
||||
-- ==========================================
|
||||
-- Shared state tables for multi-replica operation.
|
||||
-- v0.32.0: ws_tickets, rate_limit_counters.
|
||||
-- SQLite parity — functional for single-process test coverage.
|
||||
-- ==========================================
|
||||
|
||||
-- =========================================
|
||||
-- WEBSOCKET TICKETS
|
||||
-- =========================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ws_tickets (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
expires_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_ws_tickets_expires
|
||||
ON ws_tickets (expires_at);
|
||||
|
||||
-- =========================================
|
||||
-- RATE LIMIT COUNTERS
|
||||
-- =========================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS rate_limit_counters (
|
||||
key TEXT NOT NULL,
|
||||
window_start TEXT NOT NULL,
|
||||
tokens REAL NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (key, window_start)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_rate_limit_counters_window
|
||||
ON rate_limit_counters (window_start);
|
||||
@@ -1,15 +0,0 @@
|
||||
-- ==========================================
|
||||
-- Chat Switchboard — 021 Data Portability
|
||||
-- ==========================================
|
||||
-- v0.34.0: Indexes for data export and GDPR operations.
|
||||
-- SQLite version (no partial indexes).
|
||||
-- ==========================================
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_messages_channel_active
|
||||
ON messages (channel_id, created_at);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_channels_user_id
|
||||
ON channels (user_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_files_user_id
|
||||
ON files (user_id);
|
||||
@@ -1,24 +0,0 @@
|
||||
-- ==========================================
|
||||
-- Chat Switchboard — 022 Extension Connections
|
||||
-- ==========================================
|
||||
-- v0.38.1: Scoped credential management for extensions.
|
||||
-- SQLite version (no partial indexes, TEXT types).
|
||||
-- ==========================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ext_connections (
|
||||
id TEXT PRIMARY KEY,
|
||||
type TEXT NOT NULL,
|
||||
package_id TEXT NOT NULL,
|
||||
scope TEXT NOT NULL CHECK (scope IN ('global', 'team', 'personal')),
|
||||
owner_id TEXT NOT NULL DEFAULT '',
|
||||
name TEXT NOT NULL,
|
||||
config TEXT NOT NULL DEFAULT '{}',
|
||||
is_active INTEGER DEFAULT 1,
|
||||
created_at TEXT,
|
||||
updated_at TEXT,
|
||||
UNIQUE(type, scope, owner_id, name)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_ext_connections_type ON ext_connections(type);
|
||||
CREATE INDEX IF NOT EXISTS idx_ext_connections_scope ON ext_connections(scope);
|
||||
CREATE INDEX IF NOT EXISTS idx_ext_connections_owner ON ext_connections(owner_id);
|
||||
@@ -1,62 +0,0 @@
|
||||
-- ==========================================
|
||||
-- Chat Switchboard — 023 Extension Dependencies
|
||||
-- ==========================================
|
||||
-- v0.38.2: Library package dependency tracking.
|
||||
-- SQLite version.
|
||||
--
|
||||
-- Also adds 'library' to the packages type CHECK constraint.
|
||||
-- SQLite doesn't support ALTER CONSTRAINT, so we rebuild the table.
|
||||
-- ==========================================
|
||||
|
||||
-- Step 1: Rebuild packages table with updated CHECK constraint.
|
||||
CREATE TABLE IF NOT EXISTS packages_new (
|
||||
id TEXT PRIMARY KEY,
|
||||
title TEXT NOT NULL,
|
||||
type TEXT NOT NULL DEFAULT 'surface'
|
||||
CHECK (type IN ('surface', 'extension', 'full', 'workflow', 'library')),
|
||||
version TEXT NOT NULL DEFAULT '0.0.0',
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
author TEXT NOT NULL DEFAULT '',
|
||||
tier TEXT NOT NULL DEFAULT 'browser'
|
||||
CHECK (tier IN ('browser', 'starlark', 'sidecar')),
|
||||
is_system INTEGER NOT NULL DEFAULT 0,
|
||||
scope TEXT NOT NULL DEFAULT 'global'
|
||||
CHECK (scope IN ('global', 'team', 'personal')),
|
||||
team_id TEXT REFERENCES teams(id) ON DELETE CASCADE,
|
||||
installed_by TEXT REFERENCES users(id) ON DELETE SET NULL,
|
||||
manifest TEXT NOT NULL DEFAULT '{}',
|
||||
enabled INTEGER NOT NULL DEFAULT 1,
|
||||
status TEXT NOT NULL DEFAULT 'active'
|
||||
CHECK (status IN ('active', 'pending_review', 'suspended')),
|
||||
schema_version INTEGER NOT NULL DEFAULT 0,
|
||||
package_settings TEXT NOT NULL DEFAULT '{}',
|
||||
source TEXT NOT NULL DEFAULT 'core'
|
||||
CHECK (source IN ('core', 'builtin', 'extension', 'registry')),
|
||||
installed_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
INSERT INTO packages_new SELECT * FROM packages;
|
||||
DROP TABLE packages;
|
||||
ALTER TABLE packages_new RENAME TO packages;
|
||||
|
||||
-- Recreate indexes (dropped with old table)
|
||||
CREATE INDEX IF NOT EXISTS idx_packages_type ON packages(type);
|
||||
CREATE INDEX IF NOT EXISTS idx_packages_enabled ON packages(enabled);
|
||||
CREATE INDEX IF NOT EXISTS idx_packages_team ON packages(team_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_packages_source ON packages(source);
|
||||
CREATE INDEX IF NOT EXISTS idx_packages_status ON packages(status);
|
||||
|
||||
-- Step 2: Dependencies table.
|
||||
CREATE TABLE IF NOT EXISTS ext_dependencies (
|
||||
consumer_id TEXT NOT NULL,
|
||||
library_id TEXT NOT NULL,
|
||||
version_spec TEXT NOT NULL DEFAULT '>=0.0.0',
|
||||
resolved_ver TEXT NOT NULL DEFAULT '0.0.0',
|
||||
PRIMARY KEY (consumer_id, library_id),
|
||||
FOREIGN KEY (consumer_id) REFERENCES packages(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (library_id) REFERENCES packages(id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_ext_deps_consumer ON ext_dependencies(consumer_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_ext_deps_library ON ext_dependencies(library_id);
|
||||
Reference in New Issue
Block a user