Changeset 0.17.1 (#76)
This commit is contained in:
22
server/database/migrations/sqlite/001_embedding_patch.sql
Normal file
22
server/database/migrations/sqlite/001_embedding_patch.sql
Normal file
@@ -0,0 +1,22 @@
|
||||
-- Schema patch for 001_v017_schema.sql
|
||||
-- Change the kb_chunks table to include the embedding column:
|
||||
--
|
||||
-- BEFORE:
|
||||
-- CREATE TABLE IF NOT EXISTS kb_chunks (
|
||||
-- ...
|
||||
-- token_count INTEGER NOT NULL DEFAULT 0,
|
||||
-- -- embedding column omitted: vector search feature-gated for SQLite.
|
||||
-- -- Embedding bytes can be stored as BLOB if sqlite-vec is available.
|
||||
-- metadata TEXT NOT NULL DEFAULT '{}',
|
||||
-- ...
|
||||
--
|
||||
-- AFTER:
|
||||
-- CREATE TABLE IF NOT EXISTS kb_chunks (
|
||||
-- ...
|
||||
-- token_count INTEGER NOT NULL DEFAULT 0,
|
||||
-- embedding TEXT, -- JSON array of float64 for app-level cosine similarity
|
||||
-- metadata TEXT NOT NULL DEFAULT '{}',
|
||||
-- ...
|
||||
--
|
||||
-- For existing SQLite databases, run:
|
||||
-- ALTER TABLE kb_chunks ADD COLUMN embedding TEXT;
|
||||
791
server/database/migrations/sqlite/001_v017_schema.sql
Normal file
791
server/database/migrations/sqlite/001_v017_schema.sql
Normal file
@@ -0,0 +1,791 @@
|
||||
-- ==========================================
|
||||
-- Chat Switchboard — v0.16.0 Consolidated Schema (SQLite)
|
||||
-- ==========================================
|
||||
-- SQLite equivalent of the Postgres schema.
|
||||
--
|
||||
-- Key differences from Postgres:
|
||||
-- • No extensions (pgcrypto, pgvector)
|
||||
-- • UUID generated application-side (Go uuid.New())
|
||||
-- • JSONB → TEXT (JSON stored as text)
|
||||
-- • TEXT[] / UUID[] → TEXT (JSON arrays as text)
|
||||
-- • TIMESTAMPTZ → TEXT (ISO 8601 strings)
|
||||
-- • BYTEA → BLOB
|
||||
-- • NUMERIC → REAL
|
||||
-- • TSVECTOR/VECTOR columns omitted (feature-gated)
|
||||
-- • No COMMENT ON statements
|
||||
-- • Triggers use SQLite syntax (WHEN guard to prevent recursion)
|
||||
-- • No partial index WHERE clauses on GIN indexes
|
||||
-- ==========================================
|
||||
|
||||
-- ── WAL mode (also set via connection pragma) ──
|
||||
PRAGMA journal_mode = WAL;
|
||||
PRAGMA foreign_keys = ON;
|
||||
|
||||
-- =========================================
|
||||
-- 1. USERS
|
||||
-- =========================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id TEXT PRIMARY KEY,
|
||||
username TEXT NOT NULL,
|
||||
email TEXT NOT NULL,
|
||||
password_hash TEXT NOT NULL,
|
||||
display_name TEXT,
|
||||
avatar_url TEXT,
|
||||
role TEXT DEFAULT 'user'
|
||||
CHECK (role IN ('user', 'admin')),
|
||||
is_active INTEGER DEFAULT 1,
|
||||
settings TEXT DEFAULT '{}',
|
||||
|
||||
-- Vault: per-user encryption key (UEK) for BYOK API keys
|
||||
encrypted_uek BLOB,
|
||||
uek_salt BLOB,
|
||||
uek_nonce BLOB,
|
||||
vault_set INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
created_at TEXT DEFAULT (datetime('now')),
|
||||
updated_at TEXT DEFAULT (datetime('now')),
|
||||
last_login_at TEXT
|
||||
);
|
||||
|
||||
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 INDEX IF NOT EXISTS idx_users_username ON users(username);
|
||||
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
|
||||
|
||||
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;
|
||||
|
||||
|
||||
-- =========================================
|
||||
-- 2. AUTH
|
||||
-- =========================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS refresh_tokens (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
token_hash TEXT NOT NULL UNIQUE,
|
||||
expires_at TEXT NOT NULL,
|
||||
created_at TEXT DEFAULT (datetime('now')),
|
||||
revoked_at TEXT
|
||||
);
|
||||
|
||||
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;
|
||||
|
||||
|
||||
-- =========================================
|
||||
-- 3. TEAMS
|
||||
-- =========================================
|
||||
|
||||
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,
|
||||
is_active INTEGER DEFAULT 1,
|
||||
settings TEXT DEFAULT '{}',
|
||||
created_at TEXT DEFAULT (datetime('now')),
|
||||
updated_at TEXT DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_teams_active ON teams(is_active) WHERE is_active = 1;
|
||||
|
||||
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,
|
||||
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
role TEXT NOT NULL DEFAULT 'member'
|
||||
CHECK (role IN ('admin', 'member')),
|
||||
joined_at TEXT DEFAULT (datetime('now')),
|
||||
UNIQUE(team_id, user_id)
|
||||
);
|
||||
|
||||
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);
|
||||
|
||||
|
||||
-- =========================================
|
||||
-- 4. GROUPS
|
||||
-- =========================================
|
||||
|
||||
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')),
|
||||
team_id TEXT REFERENCES teams(id) ON DELETE CASCADE,
|
||||
created_by TEXT NOT NULL REFERENCES users(id),
|
||||
created_at TEXT DEFAULT (datetime('now')),
|
||||
updated_at TEXT DEFAULT (datetime('now')),
|
||||
|
||||
CONSTRAINT groups_scope_team CHECK (
|
||||
(scope = 'global' AND team_id IS NULL) OR
|
||||
(scope = 'team' AND team_id IS NOT NULL)
|
||||
)
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_groups_name_scope
|
||||
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,
|
||||
group_id TEXT NOT NULL REFERENCES groups(id) ON DELETE CASCADE,
|
||||
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
added_by TEXT NOT NULL REFERENCES users(id),
|
||||
added_at TEXT DEFAULT (datetime('now')),
|
||||
UNIQUE(group_id, user_id)
|
||||
);
|
||||
|
||||
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);
|
||||
|
||||
|
||||
-- =========================================
|
||||
-- 5. PROVIDER CONFIGS
|
||||
-- =========================================
|
||||
|
||||
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,
|
||||
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)
|
||||
WHERE owner_id IS NOT NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_provider_configs_active ON provider_configs(is_active)
|
||||
WHERE is_active = 1;
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS provider_configs_updated_at AFTER UPDATE ON provider_configs
|
||||
FOR EACH ROW WHEN NEW.updated_at = OLD.updated_at
|
||||
BEGIN
|
||||
UPDATE provider_configs SET updated_at = datetime('now') WHERE id = NEW.id;
|
||||
END;
|
||||
|
||||
|
||||
-- =========================================
|
||||
-- 6. MODEL CATALOG
|
||||
-- =========================================
|
||||
|
||||
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)
|
||||
WHERE visibility = 'enabled';
|
||||
CREATE INDEX IF NOT EXISTS idx_model_catalog_type ON model_catalog(model_type);
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS model_catalog_updated_at AFTER UPDATE ON model_catalog
|
||||
FOR EACH ROW WHEN NEW.updated_at = OLD.updated_at
|
||||
BEGIN
|
||||
UPDATE model_catalog SET updated_at = datetime('now') WHERE id = NEW.id;
|
||||
END;
|
||||
|
||||
|
||||
-- =========================================
|
||||
-- 7. PERSONAS
|
||||
-- =========================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS personas (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
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,
|
||||
|
||||
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) WHERE owner_id IS NOT NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_personas_active ON personas(is_active) WHERE is_active = 1;
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS personas_updated_at AFTER UPDATE ON personas
|
||||
FOR EACH ROW WHEN NEW.updated_at = OLD.updated_at
|
||||
BEGIN
|
||||
UPDATE personas SET updated_at = datetime('now') WHERE id = NEW.id;
|
||||
END;
|
||||
|
||||
|
||||
-- =========================================
|
||||
-- 8. PERSONA GRANTS
|
||||
-- =========================================
|
||||
|
||||
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);
|
||||
|
||||
|
||||
-- =========================================
|
||||
-- 9. RESOURCE GRANTS
|
||||
-- =========================================
|
||||
-- granted_groups stored as JSON text array: '["uuid1","uuid2"]'
|
||||
|
||||
CREATE TABLE IF NOT EXISTS resource_grants (
|
||||
id TEXT PRIMARY KEY,
|
||||
resource_type TEXT NOT NULL
|
||||
CHECK (resource_type IN ('persona', 'knowledge_base')),
|
||||
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;
|
||||
|
||||
|
||||
-- =========================================
|
||||
-- 10. PLATFORM POLICIES
|
||||
-- =========================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS platform_policies (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL,
|
||||
updated_by TEXT REFERENCES users(id),
|
||||
updated_at TEXT DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
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');
|
||||
|
||||
|
||||
-- =========================================
|
||||
-- 11. GLOBAL SETTINGS
|
||||
-- =========================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS global_settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL DEFAULT '{}',
|
||||
updated_at TEXT DEFAULT (datetime('now')),
|
||||
updated_by TEXT REFERENCES users(id)
|
||||
);
|
||||
|
||||
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": "", "position": "both", "bg": "#007a33", "fg": "#ffffff"}'),
|
||||
('banner_presets', '{"development": {"text": "DEVELOPMENT", "bg": "#007a33", "fg": "#ffffff"}, "testing": {"text": "TESTING", "bg": "#502b85", "fg": "#ffffff"}, "staging": {"text": "STAGING", "bg": "#0033a0", "fg": "#ffffff"}, "production": {"text": "PRODUCTION", "bg": "#c8102e", "fg": "#ffffff"}, "training": {"text": "TRAINING", "bg": "#ff8c00", "fg": "#000000"}, "demo": {"text": "DEMO", "bg": "#fce83a", "fg": "#000000"}}'),
|
||||
('model_roles', '{"utility": {"primary": null, "fallback": null}, "embedding": {"primary": null, "fallback": null}, "generation": {"primary": null, "fallback": null}}');
|
||||
|
||||
|
||||
-- =========================================
|
||||
-- 12. USER MODEL SETTINGS
|
||||
-- =========================================
|
||||
|
||||
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,
|
||||
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)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_user_model_settings_user ON user_model_settings(user_id);
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS user_model_settings_updated_at AFTER UPDATE ON user_model_settings
|
||||
FOR EACH ROW WHEN NEW.updated_at = OLD.updated_at
|
||||
BEGIN
|
||||
UPDATE user_model_settings SET updated_at = datetime('now') WHERE id = NEW.id;
|
||||
END;
|
||||
|
||||
|
||||
-- =========================================
|
||||
-- 13. CHANNELS
|
||||
-- =========================================
|
||||
-- tags stored as JSON text array: '["tag1","tag2"]'
|
||||
|
||||
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', 'group', 'channel')),
|
||||
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,
|
||||
folder TEXT,
|
||||
team_id TEXT REFERENCES teams(id) ON DELETE SET NULL,
|
||||
settings TEXT DEFAULT '{}',
|
||||
tags TEXT DEFAULT '[]',
|
||||
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 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 TRIGGER IF NOT EXISTS channels_updated_at AFTER UPDATE ON channels
|
||||
FOR EACH ROW WHEN NEW.updated_at = OLD.updated_at
|
||||
BEGIN
|
||||
UPDATE channels SET updated_at = datetime('now') WHERE id = NEW.id;
|
||||
END;
|
||||
|
||||
|
||||
-- =========================================
|
||||
-- 14. MESSAGES
|
||||
-- =========================================
|
||||
|
||||
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,
|
||||
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 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;
|
||||
|
||||
|
||||
-- =========================================
|
||||
-- 15. CHANNEL MEMBERS & MODELS
|
||||
-- =========================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS channel_members (
|
||||
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,
|
||||
role TEXT DEFAULT 'member',
|
||||
joined_at TEXT DEFAULT (datetime('now')),
|
||||
last_read_at TEXT DEFAULT (datetime('now')),
|
||||
UNIQUE(channel_id, user_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_channel_members_channel ON channel_members(channel_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_channel_members_user ON channel_members(user_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,
|
||||
display_name TEXT,
|
||||
system_prompt TEXT,
|
||||
settings TEXT DEFAULT '{}',
|
||||
is_default INTEGER DEFAULT 0,
|
||||
added_at TEXT DEFAULT (datetime('now')),
|
||||
UNIQUE(channel_id, model_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_channel_models_channel ON channel_models(channel_id);
|
||||
|
||||
|
||||
-- =========================================
|
||||
-- 16. CHANNEL CURSORS
|
||||
-- =========================================
|
||||
|
||||
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);
|
||||
|
||||
|
||||
-- =========================================
|
||||
-- 17. FOLDERS
|
||||
-- =========================================
|
||||
|
||||
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 INDEX IF NOT EXISTS idx_channels_folder ON channels(folder_id) WHERE folder_id IS NOT NULL;
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS folders_updated_at AFTER UPDATE ON folders
|
||||
FOR EACH ROW WHEN NEW.updated_at = OLD.updated_at
|
||||
BEGIN
|
||||
UPDATE folders SET updated_at = datetime('now') WHERE id = NEW.id;
|
||||
END;
|
||||
|
||||
|
||||
-- =========================================
|
||||
-- 18. NOTES
|
||||
-- =========================================
|
||||
-- search_vector and embedding columns omitted (Postgres-only features).
|
||||
-- Full-text search uses LIKE fallback. Semantic search feature-gated.
|
||||
-- tags stored as JSON text array.
|
||||
|
||||
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,
|
||||
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 DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_notes_team ON notes(team_id) WHERE team_id IS NOT NULL;
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS notes_updated_at AFTER UPDATE ON notes
|
||||
FOR EACH ROW WHEN NEW.updated_at = OLD.updated_at
|
||||
BEGIN
|
||||
UPDATE notes SET updated_at = datetime('now') WHERE id = NEW.id;
|
||||
END;
|
||||
|
||||
|
||||
-- =========================================
|
||||
-- 19. AUDIT LOG
|
||||
-- =========================================
|
||||
|
||||
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 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);
|
||||
|
||||
|
||||
-- =========================================
|
||||
-- 20. USAGE TRACKING
|
||||
-- =========================================
|
||||
|
||||
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,
|
||||
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);
|
||||
|
||||
|
||||
-- =========================================
|
||||
-- 21. MODEL PRICING
|
||||
-- =========================================
|
||||
|
||||
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)
|
||||
);
|
||||
|
||||
|
||||
-- =========================================
|
||||
-- 22. EXTENSIONS
|
||||
-- =========================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS extensions (
|
||||
id TEXT PRIMARY KEY,
|
||||
ext_id TEXT NOT NULL UNIQUE,
|
||||
name TEXT NOT NULL,
|
||||
version TEXT NOT NULL DEFAULT '0.0.0',
|
||||
tier TEXT NOT NULL DEFAULT 'browser',
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
author TEXT NOT NULL DEFAULT '',
|
||||
manifest TEXT NOT NULL DEFAULT '{}',
|
||||
is_system INTEGER NOT NULL DEFAULT 0,
|
||||
is_enabled INTEGER NOT NULL DEFAULT 1,
|
||||
scope TEXT NOT NULL DEFAULT 'global',
|
||||
team_id TEXT REFERENCES teams(id) ON DELETE CASCADE,
|
||||
installed_by TEXT REFERENCES users(id) ON DELETE SET NULL,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_extensions_tier ON extensions(tier);
|
||||
CREATE INDEX IF NOT EXISTS idx_extensions_enabled ON extensions(is_enabled)
|
||||
WHERE is_enabled = 1;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS extension_user_settings (
|
||||
extension_id TEXT NOT NULL REFERENCES extensions(id) ON DELETE CASCADE,
|
||||
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
settings TEXT NOT NULL DEFAULT '{}',
|
||||
is_enabled INTEGER NOT NULL DEFAULT 1,
|
||||
PRIMARY KEY (extension_id, user_id)
|
||||
);
|
||||
|
||||
|
||||
-- =========================================
|
||||
-- 23. ATTACHMENTS
|
||||
-- =========================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS attachments (
|
||||
id TEXT PRIMARY KEY,
|
||||
channel_id TEXT NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
|
||||
user_id TEXT NOT NULL REFERENCES users(id),
|
||||
message_id TEXT REFERENCES messages(id) ON DELETE SET NULL,
|
||||
filename TEXT NOT NULL,
|
||||
content_type TEXT NOT NULL,
|
||||
size_bytes INTEGER NOT NULL,
|
||||
storage_key TEXT NOT NULL,
|
||||
extracted_text TEXT,
|
||||
metadata TEXT DEFAULT '{}',
|
||||
created_at TEXT DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_attachments_channel ON attachments(channel_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_attachments_user_size ON attachments(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_attachments_message ON attachments(message_id)
|
||||
WHERE message_id IS NOT NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_attachments_orphan ON attachments(created_at)
|
||||
WHERE message_id IS NULL;
|
||||
|
||||
|
||||
-- =========================================
|
||||
-- 24. KNOWLEDGE BASES
|
||||
-- =========================================
|
||||
-- Vector columns: kb_chunks.embedding stored as JSON TEXT for app-level cosine similarity.
|
||||
-- search_vector columns omitted (Postgres-only tsvector feature).
|
||||
-- KB ingestion works (stores text chunks) but similarity search is
|
||||
-- feature-gated: requires sqlite-vec extension or returns graceful error.
|
||||
|
||||
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')),
|
||||
|
||||
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;
|
||||
|
||||
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,
|
||||
embedding TEXT, -- JSON array of float64 for app-level cosine similarity
|
||||
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)
|
||||
);
|
||||
|
||||
|
||||
-- =========================================
|
||||
-- 25. PERSONA-KB BINDING (v0.17.0)
|
||||
-- =========================================
|
||||
|
||||
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);
|
||||
|
||||
-- v0.17.0 policy
|
||||
INSERT OR IGNORE INTO platform_policies (key, value) VALUES
|
||||
('kb_direct_access', 'true');
|
||||
Reference in New Issue
Block a user