Changeset 0.28.0 (#172)

This commit is contained in:
2026-03-11 11:22:38 +00:00
parent 07432233f7
commit 93c72daadf
31 changed files with 580 additions and 431 deletions

View File

@@ -1,11 +1,9 @@
-- ==========================================
-- Chat Switchboard — 001 Core
-- ==========================================
-- Users, authentication, platform policies, and global settings.
-- Foundation tables with no external FK dependencies.
--
-- ICD §2 (Auth), §14 (User Profile & Settings), §16 (Platform Admin)
-- v0.22.8: Domain-grouped migration (replaces monolith 001 + incrementals)
-- Users (with auth fields), refresh tokens, platform policies,
-- global settings, user presence.
-- Consolidated v0.28.0: merges 001 + 018 (auth_abstraction) + 016 (presence).
-- ==========================================
-- ── Extensions ──────────────────────────────
@@ -32,7 +30,7 @@ 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 NOT NULL,
password_hash TEXT, -- nullable for mTLS/OIDC
display_name VARCHAR(100),
avatar_url TEXT,
role VARCHAR(20) DEFAULT 'user'
@@ -40,6 +38,12 @@ CREATE TABLE IF NOT EXISTS users (
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,
@@ -51,9 +55,12 @@ CREATE TABLE IF NOT EXISTS users (
last_login_at TIMESTAMPTZ
);
-- Case-insensitive unique indexes (no bare UNIQUE constraint)
-- 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);
@@ -62,6 +69,9 @@ 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';
@@ -96,7 +106,6 @@ CREATE TABLE IF NOT EXISTS platform_policies (
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Secure by default
INSERT INTO platform_policies (key, value) VALUES
('allow_user_byok', 'false'),
('allow_user_personas', 'false'),
@@ -131,3 +140,19 @@ INSERT INTO global_settings (key, value) VALUES
"generation": { "primary": null, "fallback": null }
}'::jsonb)
ON CONFLICT (key) DO NOTHING;
-- =========================================
-- USER PRESENCE (v0.23.1)
-- =========================================
CREATE TABLE IF NOT EXISTS user_presence (
user_id UUID PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
last_seen TIMESTAMPTZ NOT NULL DEFAULT NOW(),
status VARCHAR(20) 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);
COMMENT ON TABLE user_presence IS 'Heartbeat-driven presence. Rows upserted every 30s by active clients.';

View File

@@ -1,8 +1,9 @@
-- ==========================================
-- Chat Switchboard — 002 Teams & Access Control
-- ==========================================
-- Teams, team membership, groups (ACLs), and group membership.
-- ICD §15 (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).
-- ==========================================
-- =========================================
@@ -42,9 +43,8 @@ CREATE INDEX IF NOT EXISTS idx_team_members_user ON team_members(user_id);
-- =========================================
-- GROUPS (ACLs)
-- GROUPS (ACLs + Permissions)
-- =========================================
-- Pure access-control lists. Decouple resource visibility from team membership.
CREATE TABLE IF NOT EXISTS groups (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
@@ -53,7 +53,16 @@ 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 NOT NULL REFERENCES users(id),
created_by UUID REFERENCES users(id), -- nullable for system-seeded
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(),
@@ -70,8 +79,13 @@ 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. Decouple resource visibility from team membership.';
COMMENT ON COLUMN groups.scope IS 'global: admin-managed, can span teams. team: team-admin-managed, team-internal.';
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(),
@@ -84,3 +98,16 @@ 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.
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
)
ON CONFLICT (id) DO NOTHING;

View File

@@ -3,7 +3,7 @@
-- ==========================================
-- Provider configs, model catalog, pricing, health, capability
-- overrides, and routing policies.
-- ICD §10 (Providers & Routing), §11 (Models & Preferences)
-- Consolidated v0.28.0: adds capability_match policy type.
-- ==========================================
-- =========================================
@@ -110,7 +110,7 @@ CREATE TABLE IF NOT EXISTS model_pricing (
-- =========================================
-- PROVIDER HEALTH (v0.22.0)
-- PROVIDER HEALTH
-- =========================================
CREATE TABLE IF NOT EXISTS provider_health (
@@ -135,7 +135,7 @@ CREATE INDEX IF NOT EXISTS idx_provider_health_window
-- =========================================
-- CAPABILITY OVERRIDES (v0.22.0)
-- CAPABILITY OVERRIDES
-- =========================================
CREATE TABLE IF NOT EXISTS capability_overrides (
@@ -155,7 +155,7 @@ CREATE INDEX IF NOT EXISTS idx_capability_overrides_model
-- =========================================
-- ROUTING POLICIES (v0.22.2)
-- ROUTING POLICIES
-- =========================================
CREATE TABLE IF NOT EXISTS routing_policies (
@@ -166,7 +166,7 @@ CREATE TABLE IF NOT EXISTS routing_policies (
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')),
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(),

View File

@@ -1,9 +1,14 @@
-- ==========================================
-- Chat Switchboard — 005 Channels & Conversations
-- ==========================================
-- Channels, messages, participants, models, cursors, folders, user prefs.
-- ICD §3 (Channels & Conversations)
-- Note: project_id and workspace_id FKs added by 010_projects.sql
-- 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.
-- ==========================================
-- =========================================
@@ -38,17 +43,43 @@ CREATE TABLE IF NOT EXISTS channels (
title VARCHAR(500) NOT NULL,
description TEXT,
type VARCHAR(20) DEFAULT 'direct'
CHECK (type IN ('direct', 'group', 'workflow')),
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,
folder TEXT,
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(),
-- 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,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
@@ -58,22 +89,27 @@ 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';
DROP TRIGGER IF EXISTS channels_updated_at ON channels;
CREATE TRIGGER channels_updated_at BEFORE UPDATE ON channels
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
-- Deferred FK: channels.folder_id → folders.id
DO $$ BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint WHERE conname = 'fk_channels_folder'
) THEN
ALTER TABLE channels ADD CONSTRAINT fk_channels_folder
FOREIGN KEY (folder_id) REFERENCES folders(id) ON DELETE SET NULL;
END IF;
END $$;
CREATE INDEX IF NOT EXISTS idx_channels_folder ON channels(folder_id) WHERE folder_id IS NOT NULL;
COMMENT ON COLUMN channels.type IS 'direct=single-user chat, group=multi-user/multi-model, workflow=staged channel';
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.kb_auto_inject IS 'When true, top-K KB chunks auto-prepend to context';
-- =========================================
@@ -113,12 +149,6 @@ COMMENT ON COLUMN messages.sibling_index IS 'Position among siblings (0-indexed)
-- =========================================
-- CHANNEL PARTICIPANTS & MODELS
-- =========================================
-- ICD §3.7: Polymorphic participant system.
-- participant_type determines what participant_id references:
-- user → users.id
-- persona → personas.id
-- session → opaque session token
-- =========================================
CREATE TABLE IF NOT EXISTS channel_participants (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
@@ -132,6 +162,7 @@ CREATE TABLE IF NOT EXISTS channel_participants (
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)
);
@@ -200,3 +231,25 @@ CREATE INDEX IF NOT EXISTS idx_user_model_settings_user ON user_model_settings(u
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.';

View File

@@ -2,7 +2,7 @@
-- Chat Switchboard — 008 Memory
-- ==========================================
-- Long-term memory across conversations, extraction tracking.
-- ICD §9 (Memory)
-- Consolidated v0.28.0: adds last_compacted_at + decay_rate.
-- ==========================================
-- =========================================
@@ -21,6 +21,11 @@ CREATE TABLE IF NOT EXISTS memories (
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()
);
@@ -47,6 +52,8 @@ COMMENT ON TABLE memories IS 'Long-term memory facts persisted across conversati
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 $$
@@ -63,7 +70,7 @@ CREATE TRIGGER trg_memories_updated_at
-- =========================================
-- MEMORY EXTRACTION LOG (v0.18.0 Phase 2)
-- MEMORY EXTRACTION LOG
-- =========================================
CREATE TABLE IF NOT EXISTS memory_extraction_log (

View File

@@ -94,16 +94,26 @@ CREATE INDEX IF NOT EXISTS idx_project_notes_note ON project_notes(note_id);
-- =========================================
-- CROSS-DOMAIN FK COLUMNS
-- =========================================
-- Denormalized project_id + workspace_id on channels.
-- Deferred FKs: channels.project_id and workspace_id declared in 005
-- without FK constraints (referenced tables didn't exist yet).
ALTER TABLE channels
ADD COLUMN IF NOT EXISTS project_id UUID REFERENCES projects(id) ON DELETE SET NULL;
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 $$;
ALTER TABLE channels
ADD COLUMN IF NOT EXISTS workspace_id UUID REFERENCES workspaces(id) ON DELETE SET 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;
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 $$;
-- =========================================

View File

@@ -1,8 +1,8 @@
-- ==========================================
-- Chat Switchboard — 013 Files
-- ==========================================
-- Unified files table replacing attachments.
-- Handles both upgrade (attachments exists) and fresh install.
-- Unified files table. Fresh install — no legacy attachments migration.
-- Consolidated v0.28.0: removes attachments→files migration dance.
-- ==========================================
CREATE TABLE IF NOT EXISTS files (
@@ -25,38 +25,6 @@ CREATE TABLE IF NOT EXISTS files (
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Ensure attachments table exists so the INSERT is always valid.
-- Upgrade: no-op (table already has data). Fresh install: empty table.
CREATE TABLE IF NOT EXISTS attachments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
channel_id UUID, user_id UUID, message_id UUID, project_id UUID,
filename VARCHAR(255), content_type VARCHAR(127), size_bytes BIGINT,
storage_key TEXT, extracted_text TEXT, metadata JSONB, created_at TIMESTAMPTZ
);
INSERT INTO files (
id, channel_id, message_id, user_id, project_id,
origin, filename, content_type, size_bytes, storage_key,
display_hint, extracted_text, metadata, created_at, updated_at
)
SELECT
id, channel_id, message_id, user_id, project_id,
'user_upload',
filename, content_type, size_bytes, storage_key,
CASE
WHEN content_type LIKE 'image/%' THEN 'inline'
WHEN content_type LIKE 'video/%' THEN 'inline'
WHEN content_type LIKE 'audio/%' THEN 'inline'
WHEN content_type = 'application/pdf' THEN 'thumbnail'
WHEN content_type LIKE 'text/%' THEN 'inline'
ELSE 'download'
END,
extracted_text, metadata, created_at, created_at
FROM attachments
ON CONFLICT (id) DO NOTHING;
DROP TABLE IF EXISTS attachments;
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);

View File

@@ -1,6 +1,10 @@
-- 022_surfaces.sql
-- Surface registry for dynamic surface lifecycle (v0.25.0)
-- Core surfaces are seeded at startup. Extension surfaces are uploaded via admin.
-- ==========================================
-- Chat Switchboard — 016 Surfaces
-- ==========================================
-- Surface registry for dynamic surface lifecycle.
-- Core surfaces seeded at startup. Extension surfaces uploaded via admin.
-- Consolidated v0.28.0: renumbered from 022.
-- ==========================================
CREATE TABLE IF NOT EXISTS surface_registry (
id TEXT PRIMARY KEY,

View File

@@ -1,46 +0,0 @@
-- ==========================================
-- Chat Switchboard — 006 Multi-User: DMs + Channels
-- ==========================================
-- Extends channels with 'dm' and 'channel' types, ai_mode, and topic.
-- Adds presence tracking table.
-- ICD §3 (Channels & Conversations) — v0.23.1
-- ==========================================
-- ── Channel type extension ───────────────────────────────────────────
-- Add 'dm' (human-to-human, AI silent unless @mentioned)
-- Add 'channel' (named persistent space, configurable ai_mode)
-- Existing 'direct', 'group', 'workflow' are unchanged.
ALTER TABLE channels DROP CONSTRAINT IF EXISTS channels_type_check;
ALTER TABLE channels ADD CONSTRAINT channels_type_check
CHECK (type IN ('direct', 'dm', 'group', 'channel', 'workflow'));
-- ── ai_mode ──────────────────────────────────────────────────────────
-- auto : AI responds to every message (existing behavior for 'direct')
-- mention_only : AI silent unless @mentioned (default for 'dm')
-- off : AI disabled entirely
ALTER TABLE channels
ADD COLUMN IF NOT EXISTS ai_mode VARCHAR(20) NOT NULL DEFAULT 'auto'
CHECK (ai_mode IN ('auto', 'mention_only', 'off'));
-- Back-fill: DM channels should be mention_only
-- (No existing rows should have type='dm' yet, but be safe)
UPDATE channels SET ai_mode = 'mention_only' WHERE type = 'dm' AND ai_mode = 'auto';
-- ── topic ─────────────────────────────────────────────────────────────
ALTER TABLE channels
ADD COLUMN IF NOT EXISTS topic TEXT;
-- ── Presence (in-memory heartbeat, DB stores last_seen for persistence) ──
-- Not queried at high frequency — updated by heartbeat (30s), read on load.
CREATE TABLE IF NOT EXISTS user_presence (
user_id UUID PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
last_seen TIMESTAMPTZ NOT NULL DEFAULT NOW(),
status VARCHAR(20) 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);
COMMENT ON TABLE user_presence IS 'Heartbeat-driven presence. Rows upserted every 30s by active clients.';
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';

View File

@@ -0,0 +1,18 @@
-- ==========================================
-- 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.';

View File

@@ -1,10 +0,0 @@
-- ==========================================
-- Chat Switchboard — 017 Unread Counts
-- ==========================================
-- Adds last_read_message_id to channel_participants for
-- per-user unread count computation.
-- ICD §3 (Channels & Conversations) — v0.23.2
-- ==========================================
ALTER TABLE channel_participants
ADD COLUMN IF NOT EXISTS last_read_message_id UUID;

View File

@@ -1,13 +0,0 @@
-- Chat Switchboard — 018 Auth Abstraction (v0.24.0)
ALTER TABLE users
ADD COLUMN IF NOT EXISTS auth_source VARCHAR(20) NOT NULL DEFAULT 'builtin'
CHECK (auth_source IN ('builtin', 'mtls', 'oidc'));
ALTER TABLE users ADD COLUMN IF NOT EXISTS external_id TEXT;
ALTER TABLE users ADD COLUMN IF NOT EXISTS handle VARCHAR(100);
ALTER TABLE users ALTER COLUMN password_hash DROP NOT NULL;
UPDATE users SET handle = LOWER(REPLACE(REPLACE(username, ' ', '-'), '_', '-'))
WHERE handle IS NULL;
ALTER TABLE users ALTER COLUMN handle SET NOT NULL;
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;

View File

@@ -1,12 +1,18 @@
-- 023_workflows.sql
-- Workflow definitions, stages, and version snapshots (v0.26.1)
-- Depends on: teams, personas, users
-- ==========================================
-- 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 ────────────────────
-- =========================================
-- 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, -- NULL = global
team_id UUID REFERENCES teams(id) ON DELETE CASCADE,
name TEXT NOT NULL,
slug TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '',
@@ -15,29 +21,40 @@ CREATE TABLE IF NOT EXISTS workflows (
CHECK (entry_mode IN ('public_link', 'team_only')),
is_active BOOLEAN NOT NULL DEFAULT false,
version INTEGER NOT NULL DEFAULT 1,
on_complete JSONB, -- v0.27.0 chaining hook, NULL for now
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()
);
-- Slug unique within scope: team-scoped workflows + global 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;
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 'v0.27.0: chaining hook — triggers another workflow on completion.';
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 ─────────────────────────
-- =========================================
-- WORKFLOW STAGES
-- =========================================
CREATE TABLE IF NOT EXISTS workflow_stages (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
@@ -61,7 +78,10 @@ COMMENT ON TABLE workflow_stages IS 'Ordered stages within a workflow. Each stag
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.';
-- ── Workflow Versions (immutable snapshots) ─
-- =========================================
-- WORKFLOW VERSIONS (immutable snapshots)
-- =========================================
CREATE TABLE IF NOT EXISTS workflow_versions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
@@ -79,8 +99,44 @@ CREATE INDEX IF NOT EXISTS idx_workflow_versions_workflow
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.';
-- ── updated_at trigger ──────────────────────
CREATE TRIGGER workflows_updated_at
BEFORE UPDATE ON workflows
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
-- =========================================
-- 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')),
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 $$;

View File

@@ -1,30 +0,0 @@
-- ==========================================
-- Chat Switchboard — 019 Auth Providers
-- ==========================================
-- OIDC authorization state tracking.
-- Group source column for OIDC-synced groups.
-- ICD §1 (Users), §2 (Groups) — v0.24.1
-- ==========================================
-- ── OIDC authorization state (short-lived) ───────────────────────
-- Stores state + nonce for the authorization code flow.
-- Rows deleted after callback completes. Stale rows cleaned by age.
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);
-- ── Group source tracking ────────────────────────────────────────
-- Distinguishes manually-created groups from OIDC-synced groups.
-- OIDC groups are managed exclusively by the sync process.
ALTER TABLE groups
ADD COLUMN IF NOT EXISTS source VARCHAR(20) NOT NULL DEFAULT 'manual'
CHECK (source IN ('manual', 'oidc'));
COMMENT ON TABLE oidc_auth_state IS 'Ephemeral OIDC authorization code flow state. Rows deleted after callback.';
COMMENT ON COLUMN groups.source IS 'manual=admin-created, oidc=synced from IdP groups claim';

View File

@@ -1,11 +1,14 @@
-- 026_tasks.sql — Task scheduling for autonomous agents (v0.27.1)
-- ==========================================
-- Chat Switchboard — 019 Tasks
-- ==========================================
-- Task definitions and run history for autonomous agents.
-- Consolidated v0.28.0: merges 026 + 027 (webhook_secret inline).
-- ==========================================
-- Extend channel type to include 'service' (no human participants)
ALTER TABLE channels DROP CONSTRAINT IF EXISTS channels_type_check;
ALTER TABLE channels ADD CONSTRAINT channels_type_check
CHECK (type IN ('direct', 'dm', 'group', 'channel', 'workflow', 'service'));
-- =========================================
-- TASK DEFINITIONS
-- =========================================
-- 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,
@@ -38,6 +41,7 @@ CREATE TABLE IF NOT EXISTS tasks (
CHECK (output_mode IN ('channel', 'note', 'webhook')),
output_channel_id UUID REFERENCES channels(id) ON DELETE SET NULL,
webhook_url TEXT,
webhook_secret TEXT,
-- Provider routing
provider_config_id UUID REFERENCES provider_configs(id) ON DELETE SET NULL,
@@ -58,7 +62,11 @@ CREATE INDEX IF NOT EXISTS idx_tasks_next_run ON tasks (next_run_at) WHERE is_ac
CREATE INDEX IF NOT EXISTS idx_tasks_owner ON tasks (owner_id);
CREATE INDEX IF NOT EXISTS idx_tasks_team ON tasks (team_id);
-- Task run history
-- =========================================
-- TASK RUN HISTORY
-- =========================================
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,

View File

@@ -1,36 +0,0 @@
-- 020_permissions.sql
-- Fine-grained permissions on groups (v0.24.2)
-- Extend the groups.source CHECK to include 'system' (system-seeded groups).
ALTER TABLE groups DROP CONSTRAINT IF EXISTS groups_source_check;
ALTER TABLE groups ADD CONSTRAINT groups_source_check
CHECK (source IN ('manual', 'oidc', 'system'));
-- created_by is meaningless for system-seeded groups; allow NULL.
ALTER TABLE groups ALTER COLUMN created_by DROP NOT NULL;
ALTER TABLE groups
ADD COLUMN IF NOT EXISTS permissions JSONB NOT NULL DEFAULT '[]'::jsonb,
ADD COLUMN IF NOT EXISTS token_budget_daily BIGINT,
ADD COLUMN IF NOT EXISTS token_budget_monthly BIGINT,
ADD COLUMN IF NOT EXISTS allowed_models JSONB;
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.';
-- Seed the Everyone group with a stable well-known ID.
-- All authenticated users receive its permissions implicitly (no membership row needed).
-- source='system' prevents deletion via the store guard.
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
)
ON CONFLICT (id) DO NOTHING;

View File

@@ -1,24 +0,0 @@
-- 021_sessions.sql
-- Anonymous / session participants for workflow channels (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.';
-- Allow channels to opt in to anonymous session access.
ALTER TABLE channels ADD COLUMN IF NOT EXISTS allow_anonymous BOOLEAN NOT NULL DEFAULT FALSE;
COMMENT ON COLUMN channels.allow_anonymous IS 'When true, unauthenticated visitors can join via session token (workflow channels only).';

View File

@@ -1,25 +0,0 @@
-- 024_workflow_instances.sql
-- Workflow instance columns on channels (v0.26.2)
-- Channels with type='workflow' become runtime containers for workflow instances.
ALTER TABLE channels
ADD COLUMN IF NOT EXISTS workflow_id UUID REFERENCES workflows(id) ON DELETE SET NULL,
ADD COLUMN IF NOT EXISTS workflow_version INTEGER,
ADD COLUMN IF NOT EXISTS current_stage INTEGER DEFAULT 0,
ADD COLUMN IF NOT EXISTS stage_data JSONB DEFAULT '{}',
ADD COLUMN IF NOT EXISTS workflow_status TEXT DEFAULT 'active'
CHECK (workflow_status IN ('active', 'completed', 'stale', 'cancelled')),
ADD COLUMN IF NOT EXISTS last_activity_at TIMESTAMPTZ DEFAULT NOW();
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';
COMMENT ON COLUMN channels.workflow_id IS 'FK to workflows — set when type=workflow';
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';

View File

@@ -1,27 +0,0 @@
-- 025_workflow_assignments.sql
-- Assignment queue for workflow stages (v0.26.4)
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')),
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';

View File

@@ -1,8 +0,0 @@
-- 027_webhooks.sql — Webhook secrets + workflow webhook support (v0.27.3)
-- Per-task webhook secret for HMAC signing
ALTER TABLE tasks ADD COLUMN IF NOT EXISTS webhook_secret TEXT;
-- Workflows can also fire webhooks on completion
ALTER TABLE workflows ADD COLUMN IF NOT EXISTS webhook_url TEXT;
ALTER TABLE workflows ADD COLUMN IF NOT EXISTS webhook_secret TEXT;

View File

@@ -1,28 +1,24 @@
-- ==========================================
-- Chat Switchboard — 001 Core (SQLite)
-- ==========================================
-- Users, authentication, platform policies, global settings.
-- v0.22.8: Domain-grouped migration
-- ==========================================
-- Consolidated v0.28.0
PRAGMA journal_mode = WAL;
PRAGMA foreign_keys = ON;
-- =========================================
-- USERS
-- =========================================
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
username TEXT NOT NULL,
email TEXT NOT NULL,
password_hash TEXT NOT NULL,
password_hash TEXT,
display_name TEXT,
avatar_url TEXT,
role TEXT DEFAULT 'user'
CHECK (role IN ('user', 'admin')),
role TEXT DEFAULT 'user' CHECK (role IN ('user', 'admin')),
is_active INTEGER DEFAULT 1,
settings TEXT DEFAULT '{}',
auth_source TEXT NOT NULL DEFAULT 'builtin',
external_id TEXT,
handle TEXT NOT NULL,
encrypted_uek BLOB,
uek_salt BLOB,
uek_nonce BLOB,
@@ -34,6 +30,8 @@ CREATE TABLE IF NOT EXISTS users (
CREATE UNIQUE INDEX IF NOT EXISTS users_username_ci ON users (username COLLATE NOCASE);
CREATE UNIQUE INDEX IF NOT EXISTS users_email_ci ON users (email COLLATE NOCASE);
CREATE UNIQUE INDEX IF NOT EXISTS idx_users_handle ON users(handle COLLATE NOCASE);
CREATE UNIQUE INDEX IF NOT EXISTS idx_users_external_id ON users(auth_source, external_id);
CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
@@ -41,11 +39,6 @@ CREATE TRIGGER IF NOT EXISTS users_updated_at AFTER UPDATE ON users
FOR EACH ROW WHEN NEW.updated_at = OLD.updated_at
BEGIN UPDATE users SET updated_at = datetime('now') WHERE id = NEW.id; END;
-- =========================================
-- AUTH — REFRESH TOKENS
-- =========================================
CREATE TABLE IF NOT EXISTS refresh_tokens (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
@@ -58,11 +51,6 @@ 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);
-- =========================================
-- PLATFORM POLICIES
-- =========================================
CREATE TABLE IF NOT EXISTS platform_policies (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
@@ -79,11 +67,6 @@ INSERT OR IGNORE INTO platform_policies (key, value) VALUES
('allow_team_providers', 'true'),
('kb_direct_access', 'true');
-- =========================================
-- GLOBAL SETTINGS
-- =========================================
CREATE TABLE IF NOT EXISTS global_settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL DEFAULT '{}',
@@ -96,3 +79,11 @@ INSERT OR IGNORE INTO global_settings (key, value) VALUES
('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}}');
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'
);
CREATE INDEX IF NOT EXISTS idx_user_presence_last_seen ON user_presence(last_seen DESC);

View File

@@ -1,4 +1,5 @@
-- Chat Switchboard — 002 Teams & Access Control (SQLite)
-- Consolidated v0.28.0
CREATE TABLE IF NOT EXISTS teams (
id TEXT PRIMARY KEY,
@@ -17,8 +18,7 @@ CREATE TABLE IF NOT EXISTS team_members (
id TEXT PRIMARY KEY,
team_id TEXT NOT NULL REFERENCES teams(id) ON DELETE CASCADE,
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
role TEXT NOT NULL DEFAULT 'member'
CHECK (role IN ('admin', 'member')),
role TEXT NOT NULL DEFAULT 'member' CHECK (role IN ('admin', 'member')),
joined_at TEXT DEFAULT (datetime('now')),
UNIQUE(team_id, user_id)
);
@@ -30,22 +30,18 @@ CREATE TABLE IF NOT EXISTS groups (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '',
scope TEXT NOT NULL DEFAULT 'global'
CHECK (scope IN ('global', 'team')),
scope TEXT NOT NULL DEFAULT 'global' CHECK (scope IN ('global', 'team')),
team_id TEXT REFERENCES teams(id) ON DELETE CASCADE,
created_by TEXT NOT NULL REFERENCES users(id),
created_by TEXT REFERENCES users(id),
source TEXT NOT NULL DEFAULT 'manual' CHECK (source IN ('manual', 'oidc', 'system')),
permissions TEXT NOT NULL DEFAULT '[]',
token_budget_daily INTEGER,
token_budget_monthly INTEGER,
allowed_models TEXT,
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now')),
CHECK (
(scope = 'global' AND team_id IS NULL) OR
(scope = 'team' AND team_id IS NOT NULL)
)
updated_at TEXT DEFAULT (datetime('now'))
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_groups_name_scope
ON groups(name, COALESCE(team_id, ''));
CREATE TABLE IF NOT EXISTS group_members (
id TEXT PRIMARY KEY,
group_id TEXT NOT NULL REFERENCES groups(id) ON DELETE CASCADE,
@@ -57,3 +53,14 @@ 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)
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')
);

View File

@@ -1,4 +1,5 @@
-- 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,
@@ -19,8 +20,7 @@ CREATE TABLE IF NOT EXISTS provider_configs (
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')),
CHECK (proxy_mode != 'custom' OR proxy_url IS NOT NULL)
updated_at TEXT DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_provider_configs_scope ON provider_configs(scope);
@@ -77,7 +77,8 @@ CREATE TABLE IF NOT EXISTS provider_health (
UNIQUE (provider_config_id, window_start)
);
CREATE INDEX IF NOT EXISTS idx_provider_health_window ON provider_health(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,
@@ -98,7 +99,8 @@ CREATE TABLE IF NOT EXISTS routing_policies (
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')),
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')),
@@ -106,4 +108,3 @@ CREATE TABLE IF NOT EXISTS routing_policies (
);
CREATE INDEX IF NOT EXISTS idx_routing_policies_active ON routing_policies(is_active, priority);
CREATE INDEX IF NOT EXISTS idx_routing_policies_team ON routing_policies(team_id);

View File

@@ -1,4 +1,5 @@
-- Chat Switchboard — 005 Channels & Conversations (SQLite)
-- Consolidated v0.28.0
CREATE TABLE IF NOT EXISTS folders (
id TEXT PRIMARY KEY,
@@ -19,7 +20,8 @@ CREATE TABLE IF NOT EXISTS channels (
user_id TEXT REFERENCES users(id) ON DELETE CASCADE,
title TEXT NOT NULL,
description TEXT,
type TEXT DEFAULT 'direct' CHECK (type IN ('direct', 'group', 'workflow')),
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,
@@ -30,6 +32,17 @@ CREATE TABLE IF NOT EXISTS channels (
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')),
kb_auto_inject INTEGER NOT NULL DEFAULT 0,
project_id TEXT,
workspace_id TEXT,
created_at TEXT DEFAULT (datetime('now')),
@@ -43,6 +56,7 @@ 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,
@@ -77,6 +91,7 @@ CREATE TABLE IF NOT EXISTS channel_participants (
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)
);
@@ -96,13 +111,10 @@ CREATE TABLE IF NOT EXISTS channel_models (
added_at TEXT DEFAULT (datetime('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
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 (
@@ -132,3 +144,15 @@ CREATE TABLE IF NOT EXISTS user_model_settings (
);
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);

View File

@@ -1,4 +1,5 @@
-- 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,
@@ -11,15 +12,23 @@ CREATE TABLE IF NOT EXISTS memories (
confidence REAL NOT NULL DEFAULT 1.0,
status TEXT NOT NULL DEFAULT 'active'
CHECK (status IN ('active', 'pending_review', 'archived')),
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 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 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,
@@ -31,4 +40,5 @@ CREATE TABLE IF NOT EXISTS memory_extraction_log (
UNIQUE(channel_id, user_id)
);
CREATE INDEX IF NOT EXISTS idx_mem_extract_log_channel ON memory_extraction_log(channel_id);
CREATE INDEX IF NOT EXISTS idx_mem_extract_log_channel
ON memory_extraction_log(channel_id);

View File

@@ -1,6 +1,5 @@
-- Chat Switchboard — 013 Files (SQLite)
-- Unified files table replacing attachments.
-- Handles both upgrade (attachments exists) and fresh install.
-- Consolidated v0.28.0: clean install, no legacy attachments migration
CREATE TABLE IF NOT EXISTS files (
id TEXT PRIMARY KEY,
@@ -22,40 +21,8 @@ CREATE TABLE IF NOT EXISTS files (
updated_at TEXT DEFAULT (datetime('now'))
);
-- Ensure attachments table exists so the INSERT is always valid.
-- Upgrade: no-op (table already has data). Fresh install: empty table.
CREATE TABLE IF NOT EXISTS attachments (
id TEXT PRIMARY KEY, channel_id TEXT, user_id TEXT, message_id TEXT,
project_id TEXT, filename TEXT, content_type TEXT, size_bytes INTEGER,
storage_key TEXT, extracted_text TEXT, metadata TEXT, created_at TEXT
);
INSERT OR IGNORE INTO files (
id, channel_id, message_id, user_id, project_id,
origin, filename, content_type, size_bytes, storage_key,
display_hint, extracted_text, metadata, created_at, updated_at
)
SELECT
id, channel_id, message_id, user_id, project_id,
'user_upload',
filename, content_type, size_bytes, storage_key,
CASE
WHEN content_type LIKE 'image/%' THEN 'inline'
WHEN content_type LIKE 'video/%' THEN 'inline'
WHEN content_type LIKE 'audio/%' THEN 'inline'
WHEN content_type = 'application/pdf' THEN 'thumbnail'
WHEN content_type LIKE 'text/%' THEN 'inline'
ELSE 'download'
END,
extracted_text, metadata, created_at, created_at
FROM attachments;
DROP TABLE IF EXISTS attachments;
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);
CREATE INDEX IF NOT EXISTS idx_files_orphan ON files(created_at)
WHERE message_id IS NULL AND origin = 'user_upload';

View File

@@ -0,0 +1,12 @@
-- Chat Switchboard — 016 Surfaces (SQLite)
-- Consolidated v0.28.0: was missing from SQLite entirely
CREATE TABLE IF NOT EXISTS surface_registry (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
manifest TEXT NOT NULL DEFAULT '{}',
enabled INTEGER NOT NULL DEFAULT 1,
source TEXT NOT NULL DEFAULT 'core',
installed_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);

View File

@@ -0,0 +1,11 @@
-- 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);

View File

@@ -0,0 +1,75 @@
-- Chat Switchboard — 018 Workflows (SQLite)
-- Consolidated v0.28.0: merges 023 + 025 + 027
CREATE TABLE IF NOT EXISTS workflows (
id TEXT PRIMARY KEY,
team_id TEXT REFERENCES teams(id) ON DELETE CASCADE,
name TEXT NOT NULL,
slug TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '',
branding TEXT NOT NULL DEFAULT '{}',
entry_mode TEXT NOT NULL DEFAULT 'public_link'
CHECK (entry_mode IN ('public_link', 'team_only')),
is_active INTEGER NOT NULL DEFAULT 0,
version INTEGER NOT NULL DEFAULT 1,
on_complete TEXT,
retention TEXT NOT NULL DEFAULT '{"mode": "archive"}',
webhook_url TEXT,
webhook_secret TEXT,
created_by TEXT NOT NULL REFERENCES users(id),
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('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 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,
assignment_team_id TEXT REFERENCES teams(id) ON DELETE SET NULL,
form_template TEXT NOT NULL DEFAULT '{}',
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 '{}',
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_workflow_stages_workflow
ON workflow_stages(workflow_id, ordinal);
CREATE TABLE IF NOT EXISTS workflow_versions (
id TEXT PRIMARY KEY,
workflow_id TEXT NOT NULL REFERENCES workflows(id) ON DELETE CASCADE,
version_number INTEGER NOT NULL,
snapshot TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
UNIQUE (workflow_id, version_number)
);
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')),
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);

View File

@@ -0,0 +1,55 @@
-- Chat Switchboard — 019 Tasks (SQLite)
-- Consolidated v0.28.0: merges 026 + 027
CREATE TABLE IF NOT EXISTS tasks (
id TEXT PRIMARY KEY,
owner_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
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',
persona_id TEXT REFERENCES personas(id) ON DELETE SET NULL,
model_id TEXT,
system_prompt TEXT DEFAULT '',
user_prompt TEXT DEFAULT '',
workflow_id TEXT REFERENCES workflows(id) ON DELETE SET NULL,
tool_grants TEXT,
schedule TEXT NOT NULL,
timezone TEXT NOT NULL DEFAULT 'UTC',
is_active INTEGER NOT NULL DEFAULT 1,
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,
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,
next_run_at TEXT,
run_count INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
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 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',
started_at TEXT NOT NULL DEFAULT (datetime('now')),
completed_at TEXT,
tokens_used INTEGER DEFAULT 0,
tool_calls INTEGER DEFAULT 0,
wall_clock INTEGER DEFAULT 0,
error TEXT
);
CREATE INDEX IF NOT EXISTS idx_task_runs_task ON task_runs (task_id);

View File

@@ -253,46 +253,85 @@ func TruncateAll(t *testing.T) {
}
tables := []string{
// Task system
"task_runs",
"tasks",
// Workflow system
"workflow_assignments",
"workflow_versions",
"workflow_stages",
"workflows",
// Sessions
"session_participants",
// Files
"files",
// Resource grants & groups
"resource_grants",
"group_members",
"groups",
// Notifications
"notification_preferences",
"notifications",
// Usage & audit
"usage_log",
"model_pricing",
"notes",
"audit_log",
// Channel internals
"channel_cursors",
"messages",
"channel_models",
"channel_participants",
"channel_knowledge_bases",
"persona_knowledge_bases",
"user_model_settings",
// Projects
"project_notes",
"project_knowledge_bases",
"project_channels",
// Knowledge bases
"persona_knowledge_bases",
"kb_chunks",
"kb_documents",
"knowledge_bases",
// Notes
"note_links",
"notes",
// Memory
"memory_extraction_log",
"memories",
// Workspaces
"workspace_chunks",
"workspace_files",
"git_credentials",
"workspaces",
// Channels & folders
"channels",
"folders",
"projects",
"user_model_settings",
// Providers & health
"model_pricing",
"provider_health",
"capability_overrides",
"routing_policies",
"tool_health",
"model_catalog",
// Personas
"persona_grants",
"persona_group_members",
"persona_groups",
"personas",
"provider_configs",
// Teams & users
"team_members",
"teams",
"refresh_tokens",
"user_presence",
"users",
// Config & extensions
"platform_policies",
"global_config",
"global_settings",
"folders",
"attachments",
"extension_user_settings",
"extensions",
"surface_registry",
"oidc_auth_state",
}
if IsSQLite() {