kb-demo / backend /app /schema.sql
RayLi-Git
fix: 檔案移至根目錄 + 套用示範 README
a7cd101
Raw
History Blame Contribute Delete
24.4 kB
-- =====================================================================
-- 知識庫系統 V5 — DB Schema(L1, PRD §5 / §27.5 / §34 / §49 對齊)
-- SQLite WAL, foreign_keys=ON, busy_timeout=30000
-- 對 PRD 模糊處的補完,皆於該表上方註解 "PRD-FILL" 標註。
-- =====================================================================
-- ---- 系統 / migration ----
CREATE TABLE IF NOT EXISTS schema_migrations (
version INTEGER PRIMARY KEY,
name TEXT NOT NULL,
applied_at TEXT NOT NULL
);
-- =====================================================================
-- 核心 7 張(PRD §5.1)
-- =====================================================================
-- ---- users(PRD §5.1)----
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
email TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
role TEXT NOT NULL CHECK(role IN ('admin','senior_manager','manager','user','guest')),
permissions TEXT NOT NULL DEFAULT '{"categories":["all"]}',
department TEXT NOT NULL DEFAULT '',
expires_at TEXT, -- §50.2.2 guest 帳號自動失效時間
guest_allowed_depts TEXT NOT NULL DEFAULT '[]', -- §50.2.2 guest 可看部門 JSON
created_at TEXT NOT NULL,
last_login TEXT,
is_active INTEGER NOT NULL DEFAULT 1,
is_deleted INTEGER NOT NULL DEFAULT 0,
deleted_at TEXT,
deleted_by INTEGER REFERENCES users(id),
force_password_change INTEGER NOT NULL DEFAULT 0,
jwt_version INTEGER NOT NULL DEFAULT 1,
totp_secret TEXT,
totp_enabled INTEGER NOT NULL DEFAULT 0,
totp_grace_until TEXT,
failed_login_count INTEGER NOT NULL DEFAULT 0,
locked_until TEXT
);
-- PRD-FILL: documents 表 PRD §5.1 僅列擴充欄位,核心欄位依 §6 frontmatter + §11 visibility + §15 embedding 推導
CREATE TABLE IF NOT EXISTS documents (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
category TEXT NOT NULL,
tags TEXT NOT NULL DEFAULT '[]', -- JSON array
summary TEXT,
body TEXT NOT NULL, -- Markdown
vault_path TEXT UNIQUE, -- vault/ 對應 md 路徑
author_id INTEGER NOT NULL REFERENCES users(id),
owner_department TEXT NOT NULL DEFAULT '',
visibility TEXT NOT NULL DEFAULT 'department'
CHECK(visibility IN ('public','department','selected_departments','restricted','confidential')),
allowed_departments TEXT NOT NULL DEFAULT '[]', -- JSON array of dept names
access_list TEXT NOT NULL DEFAULT '[]', -- JSON array of user_ids
related TEXT NOT NULL DEFAULT '[]', -- §41 JSON array of related doc ids(防護C 跨部門互掛)
up_moc_id INTEGER REFERENCES documents(id), -- §41 所屬統一 MOC 文件 id(防護D)
keywords TEXT NOT NULL DEFAULT '[]', -- §8.5 JSON array of ai_keywords
last_restored_from INTEGER, -- §7.5 最近一次還原的來源版次(任何 edit 後清空)
template_id INTEGER REFERENCES document_templates(id),
referrer_count INTEGER NOT NULL DEFAULT 0,
view_count INTEGER NOT NULL DEFAULT 0,
favorite_count INTEGER NOT NULL DEFAULT 0,
status TEXT NOT NULL DEFAULT 'active'
CHECK(status IN ('active','archived')),
embedding BLOB, -- 384-dim float32
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
is_deleted INTEGER NOT NULL DEFAULT 0,
deleted_at TEXT,
deleted_by INTEGER REFERENCES users(id)
);
-- PRD-FILL: pending_documents 核心欄位(status CHECK 依 PRD §5.1 完整列舉)
CREATE TABLE IF NOT EXISTS pending_documents (
id INTEGER PRIMARY KEY AUTOINCREMENT,
submitter_id INTEGER NOT NULL REFERENCES users(id),
submitter_department TEXT NOT NULL DEFAULT '',
original_content TEXT NOT NULL,
ai_result TEXT, -- AI 整理後完整 markdown
ai_meta_json TEXT, -- AI 提取的 frontmatter(標題/分類/標籤)
target_document_id INTEGER REFERENCES documents(id),
merge_target_title TEXT,
status TEXT NOT NULL DEFAULT 'processing'
CHECK(status IN ('processing','ai_ready','pending','approved','rejected','draft','failed')),
reject_reason TEXT,
reviewer_id INTEGER REFERENCES users(id),
reviewed_at TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
-- PRD-FILL: document_history 版本追蹤
CREATE TABLE IF NOT EXISTS document_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
document_id INTEGER NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
version INTEGER NOT NULL,
title TEXT NOT NULL,
body TEXT NOT NULL,
summary TEXT,
tags TEXT NOT NULL DEFAULT '[]',
edited_by INTEGER NOT NULL REFERENCES users(id),
edited_at TEXT NOT NULL,
change_note TEXT,
UNIQUE(document_id, version)
);
-- PRD-FILL: detection_rules(PRD §5.1 補欄位 + §49 layer1 規則)
CREATE TABLE IF NOT EXISTS detection_rules (
id INTEGER PRIMARY KEY AUTOINCREMENT,
rule_name TEXT NOT NULL UNIQUE,
pattern TEXT NOT NULL, -- regex
layer INTEGER NOT NULL DEFAULT 1, -- 1=regex, 2=variant, 3=topic, 4=sample
action TEXT NOT NULL DEFAULT 'block'
CHECK(action IN ('block','warn','mask','log')),
is_active INTEGER NOT NULL DEFAULT 1,
is_deleted INTEGER NOT NULL DEFAULT 0,
created_by INTEGER REFERENCES users(id),
created_at TEXT NOT NULL,
updated_at TEXT,
updated_by INTEGER REFERENCES users(id)
);
-- PRD-FILL: audit_log(PRD §5.1 + §10.7 永久保留)
CREATE TABLE IF NOT EXISTS audit_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER REFERENCES users(id),
action TEXT NOT NULL,
resource_type TEXT,
resource_id TEXT,
detail TEXT, -- JSON
ip_address TEXT,
user_agent TEXT,
result TEXT CHECK(result IN ('success','fail','blocked')) DEFAULT 'success',
timestamp TEXT NOT NULL
);
-- PRD-FILL: notifications(PRD §5.1 30 天 TTL)
CREATE TABLE IF NOT EXISTS notifications (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
type TEXT NOT NULL, -- pending_review / approved / rejected / ...
title TEXT NOT NULL,
body TEXT,
link TEXT, -- 點擊跳轉路徑
is_read INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL,
read_at TEXT
);
-- =====================================================================
-- 衍生 5 張(PRD §5.2 / §27.3 / §30.2 / §31.1 / §32.2)
-- =====================================================================
CREATE TABLE IF NOT EXISTS detection_rules_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
rule_id INTEGER REFERENCES detection_rules(id),
action TEXT NOT NULL CHECK(action IN ('create','update','delete','restore')),
old_pattern TEXT,
new_pattern TEXT,
old_rule_name TEXT,
new_rule_name TEXT,
changed_by INTEGER REFERENCES users(id),
changed_at TEXT NOT NULL,
created_at TEXT NOT NULL
);
-- PRD §27.3
CREATE TABLE IF NOT EXISTS ai_usage (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id),
department TEXT,
provider TEXT,
model TEXT,
action TEXT,
input_tokens INTEGER,
output_tokens INTEGER,
cost_usd_x1000 INTEGER,
cache_hit INTEGER NOT NULL DEFAULT 0,
timestamp TEXT NOT NULL,
pending_id INTEGER REFERENCES pending_documents(id)
);
-- PRD §30.2
CREATE TABLE IF NOT EXISTS sessions (
id TEXT PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
user_agent TEXT,
ip_address TEXT,
created_at TEXT NOT NULL,
last_used_at TEXT NOT NULL,
expires_at TEXT NOT NULL
);
-- PRD §31.1
CREATE TABLE IF NOT EXISTS feedback (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER REFERENCES users(id),
category TEXT NOT NULL CHECK(category IN ('bug','feature','improvement','question','document_error')),
title TEXT NOT NULL,
description TEXT NOT NULL,
page_url TEXT,
doc_id INTEGER REFERENCES documents(id), -- §50.3.2 報告錯誤關聯文件
user_agent TEXT,
status TEXT NOT NULL DEFAULT 'open' CHECK(status IN ('open','reviewing','resolved','wontfix')),
admin_reply TEXT,
created_at TEXT NOT NULL,
updated_at TEXT
);
-- PRD §32.2
CREATE TABLE IF NOT EXISTS user_preferences (
user_id INTEGER PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
notify_pending INTEGER NOT NULL DEFAULT 1,
notify_account_apply INTEGER NOT NULL DEFAULT 1,
notify_visibility INTEGER NOT NULL DEFAULT 1,
notify_abnormal INTEGER NOT NULL DEFAULT 1,
notify_announcement INTEGER NOT NULL DEFAULT 1,
default_view TEXT NOT NULL DEFAULT 'grid' CHECK(default_view IN ('grid','list')),
font_size TEXT NOT NULL DEFAULT 'medium' CHECK(font_size IN ('small','medium','large')),
page_size INTEGER NOT NULL DEFAULT 50,
seen_whatsnew_version TEXT,
onboarding_completed INTEGER NOT NULL DEFAULT 0,
onboarding_checklist_progress TEXT NOT NULL DEFAULT '{}' -- §50.3.1 {doc_id: viewed_at}
);
-- =====================================================================
-- 其他(PRD §27.5 / §34 / §47 / §48 / §49 / 標籤 / 待辦 / 分享)
-- =====================================================================
-- PRD §27.5.1
CREATE TABLE IF NOT EXISTS departments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
is_active INTEGER NOT NULL DEFAULT 1,
sort_order INTEGER NOT NULL DEFAULT 0,
created_by INTEGER REFERENCES users(id),
created_at TEXT NOT NULL,
deactivated_at TEXT,
deactivated_by INTEGER REFERENCES users(id),
onboarding_doc_ids TEXT NOT NULL DEFAULT '[]' -- §50.3.1 部門新人必讀文件 id JSON
);
-- PRD §34.1
CREATE TABLE IF NOT EXISTS ai_cache (
content_hash TEXT PRIMARY KEY,
ai_result_json TEXT NOT NULL,
provider TEXT NOT NULL,
model TEXT NOT NULL,
cached_at TEXT NOT NULL,
hit_count INTEGER NOT NULL DEFAULT 0,
last_hit_at TEXT
);
-- PRD-FILL: bookmarks(PRD §47 收藏)
CREATE TABLE IF NOT EXISTS bookmarks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
document_id INTEGER NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
folder TEXT NOT NULL DEFAULT '預設', -- 收藏分類
sort_order INTEGER NOT NULL DEFAULT 0,
note TEXT,
created_at TEXT NOT NULL,
UNIQUE(user_id, document_id)
);
-- PRD-FILL: bookmark_folders(PRD §12.25 收藏資料夾;支援空資料夾 + 改名/刪除)
CREATE TABLE IF NOT EXISTS bookmark_folders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
name TEXT NOT NULL,
icon TEXT,
color TEXT,
sort_order INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL,
UNIQUE(user_id, name)
);
-- PRD §48
-- PRD §48.4 文檔模板系統:三件套(structure_md 骨架 / ai_prompt 指令 / example_content 範例)
CREATE TABLE IF NOT EXISTS document_templates (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
description TEXT,
scope TEXT NOT NULL CHECK(scope IN ('global','department')),
department_id INTEGER REFERENCES departments(id), -- scope='department' 時必填
structure_md TEXT NOT NULL DEFAULT '', -- 區塊骨架,AI 必須產出對應段落
ai_prompt TEXT NOT NULL DEFAULT '', -- 給 AI 的具體指令
example_content TEXT, -- 完整範例(選填)
is_active INTEGER NOT NULL DEFAULT 1,
is_default INTEGER NOT NULL DEFAULT 0, -- 該範圍的預設模板
created_by INTEGER REFERENCES users(id),
created_at TEXT NOT NULL,
updated_at TEXT,
updated_by INTEGER REFERENCES users(id)
);
-- PRD §41 防護D:跨部門同主題追蹤(admin 跨部門主題管理頁 §41.5)
CREATE TABLE IF NOT EXISTS cross_dept_topics (
id INTEGER PRIMARY KEY AUTOINCREMENT,
topic TEXT NOT NULL, -- 主題名(取自文件標題)
doc_ids TEXT NOT NULL DEFAULT '[]', -- JSON 涉及文件 ids
departments TEXT NOT NULL DEFAULT '[]', -- JSON 涉及部門
status TEXT NOT NULL DEFAULT 'pending'
CHECK(status IN ('pending','moc_created','ignored')),
moc_id INTEGER REFERENCES documents(id), -- 建立的統一 MOC 文件
ignored_until TEXT, -- ignored:30 天內不再通知
created_at TEXT NOT NULL,
updated_at TEXT
);
-- PRD §49.6
CREATE TABLE IF NOT EXISTS sensitive_topics (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
description TEXT,
is_builtin INTEGER NOT NULL DEFAULT 0,
is_active INTEGER NOT NULL DEFAULT 1,
threshold REAL NOT NULL DEFAULT 0.65,
centroid BLOB,
created_by INTEGER REFERENCES users(id),
created_at TEXT NOT NULL,
updated_at TEXT
);
CREATE TABLE IF NOT EXISTS sensitive_topic_samples (
id INTEGER PRIMARY KEY AUTOINCREMENT,
topic_id INTEGER NOT NULL REFERENCES sensitive_topics(id) ON DELETE CASCADE,
sample_text TEXT NOT NULL,
embedding BLOB NOT NULL,
added_by INTEGER REFERENCES users(id),
added_at TEXT NOT NULL
);
-- PRD §49.7
CREATE TABLE IF NOT EXISTS confidential_samples (
id INTEGER PRIMARY KEY AUTOINCREMENT,
label TEXT NOT NULL,
description TEXT,
embedding BLOB NOT NULL,
text_length INTEGER,
text_hash TEXT,
added_by INTEGER REFERENCES users(id),
added_at TEXT NOT NULL,
is_active INTEGER NOT NULL DEFAULT 1
);
-- PRD §49.9
CREATE TABLE IF NOT EXISTS false_positive_appeals (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id),
text_snippet TEXT NOT NULL,
layer INTEGER NOT NULL,
matched_rule TEXT,
similarity REAL,
user_reason TEXT,
status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending','approved','rejected')),
reviewed_by INTEGER REFERENCES users(id),
reviewed_at TEXT,
admin_note TEXT,
created_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS whitelist_samples (
id INTEGER PRIMARY KEY AUTOINCREMENT,
embedding BLOB NOT NULL,
label TEXT,
added_by INTEGER REFERENCES users(id),
added_at TEXT NOT NULL
);
-- PRD-FILL: tags + document_tags(PRD §12.21 標籤頁所需;§6 frontmatter tags 為 JSON,這裡是正規化版本)
CREATE TABLE IF NOT EXISTS tags (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
color TEXT,
created_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS document_tags (
document_id INTEGER NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
tag_id INTEGER NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
PRIMARY KEY(document_id, tag_id)
);
-- PRD-FILL: tasks(PRD §12.24 個人待辦)
CREATE TABLE IF NOT EXISTS tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
title TEXT NOT NULL,
body TEXT,
status TEXT NOT NULL DEFAULT 'open'
CHECK(status IN ('open','in_progress','done','archived')),
due_at TEXT,
sort_order INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL,
updated_at TEXT
);
-- PRD-FILL: share_links(PRD §11 分享連結)
CREATE TABLE IF NOT EXISTS share_links (
id INTEGER PRIMARY KEY AUTOINCREMENT,
document_id INTEGER NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
token TEXT NOT NULL UNIQUE,
created_by INTEGER NOT NULL REFERENCES users(id),
password_hash TEXT,
require_login INTEGER NOT NULL DEFAULT 0,
expires_at TEXT,
revoked_at TEXT,
created_at TEXT NOT NULL,
last_used_at TEXT
);
-- =====================================================================
-- 索引(PRD §5.3)
-- =====================================================================
CREATE INDEX IF NOT EXISTS idx_documents_category ON documents(category);
CREATE INDEX IF NOT EXISTS idx_documents_visibility ON documents(visibility, is_deleted);
CREATE INDEX IF NOT EXISTS idx_pending_status ON pending_documents(status);
CREATE INDEX IF NOT EXISTS idx_history_document ON document_history(document_id);
CREATE INDEX IF NOT EXISTS idx_audit_user ON audit_log(user_id);
CREATE INDEX IF NOT EXISTS idx_audit_timestamp ON audit_log(timestamp);
CREATE INDEX IF NOT EXISTS idx_notifications_user ON notifications(user_id, is_read);
CREATE INDEX IF NOT EXISTS idx_users_department ON users(department, is_deleted);
CREATE INDEX IF NOT EXISTS idx_ai_usage_user ON ai_usage(user_id, timestamp);
CREATE INDEX IF NOT EXISTS idx_ai_usage_dept ON ai_usage(department, timestamp);
CREATE INDEX IF NOT EXISTS idx_departments_active ON departments(is_active, sort_order);
CREATE INDEX IF NOT EXISTS idx_ai_cache_cached_at ON ai_cache(cached_at);
CREATE INDEX IF NOT EXISTS idx_conf_samples_active ON confidential_samples(is_active);
CREATE INDEX IF NOT EXISTS idx_sessions_user ON sessions(user_id);
CREATE INDEX IF NOT EXISTS idx_bookmarks_user ON bookmarks(user_id, sort_order);
CREATE INDEX IF NOT EXISTS idx_tasks_user ON tasks(user_id, status, sort_order);
CREATE INDEX IF NOT EXISTS idx_share_links_doc ON share_links(document_id);
CREATE INDEX IF NOT EXISTS idx_templates_scope ON document_templates(scope, department_id, is_active);
CREATE INDEX IF NOT EXISTS idx_templates_default ON document_templates(is_default, is_active);
-- =====================================================================
-- FTS5 全文索引(PRD §5.3)
-- 採 contentless 對應 documents 主表,由觸發器同步
-- =====================================================================
CREATE VIRTUAL TABLE IF NOT EXISTS documents_fts
USING fts5(title, summary, body, content='documents', content_rowid='id');
CREATE TRIGGER IF NOT EXISTS documents_ai AFTER INSERT ON documents BEGIN
INSERT INTO documents_fts(rowid, title, summary, body)
VALUES (new.id, new.title, new.summary, new.body);
END;
CREATE TRIGGER IF NOT EXISTS documents_ad AFTER DELETE ON documents BEGIN
INSERT INTO documents_fts(documents_fts, rowid, title, summary, body)
VALUES ('delete', old.id, old.title, old.summary, old.body);
END;
CREATE TRIGGER IF NOT EXISTS documents_au AFTER UPDATE ON documents BEGIN
INSERT INTO documents_fts(documents_fts, rowid, title, summary, body)
VALUES ('delete', old.id, old.title, old.summary, old.body);
INSERT INTO documents_fts(rowid, title, summary, body)
VALUES (new.id, new.title, new.summary, new.body);
END;
-- =====================================================================
-- L3+ trusted_devices(記住此裝置以跳過 2FA),對應 HTML「記住我 14 天」
-- PRD §10.1 / §10.2 衍生:經過 2FA 驗證的裝置在 TTL 內登入可跳過 2FA。
-- 撤銷方式:使用者於 /account/sessions 移除、改密、jwt_version+1、過期 cron。
-- 只存 token 的 SHA-256,避免 DB 外洩。
-- =====================================================================
-- =====================================================================
-- L3+ users profile 擴充(migration v3):display_name / extension /
-- secondary_departments(兼任部門 JSON)。PRD §5 未列,依使用者需求 + §7.3 gap。
-- 註:欄位若已存在,db.py 的 migration 會 try/except 略過(SQLite 無 IF NOT EXISTS for ADD COLUMN)。
-- =====================================================================
CREATE TABLE IF NOT EXISTS trusted_devices (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
token_hash TEXT NOT NULL,
label TEXT,
user_agent TEXT,
ip_address TEXT,
created_at TEXT NOT NULL,
last_used_at TEXT NOT NULL,
expires_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_trusted_devices_user ON trusted_devices(user_id, expires_at);
CREATE UNIQUE INDEX IF NOT EXISTS idx_trusted_devices_token ON trusted_devices(token_hash);
-- =====================================================================
-- L10:AI Provider 設定 / 模型對照 / 切換旗標 / 匯率快取(PRD §6.x / §27.3 / §33.4)
-- key-value JSON 設定表(PRD §33.4 system_config.exchange_rates 等)。
-- =====================================================================
CREATE TABLE IF NOT EXISTS system_config (
key TEXT PRIMARY KEY,
value TEXT NOT NULL, -- JSON
updated_at TEXT NOT NULL,
updated_by INTEGER REFERENCES users(id)
);
-- §50.2.1 多部門兼任(users_departments 多對多,D234)。
-- 註:PRD 範例用 department_id,但本系統全鏈路為「部門名稱制」(owner_department / allowed_departments
-- 皆存名稱),為一致性與降低重構風險,此處改存部門名稱 department(§7.1 取較一致一方)。
CREATE TABLE IF NOT EXISTS users_departments (
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
department TEXT NOT NULL,
is_primary INTEGER NOT NULL DEFAULT 0,
assigned_at TEXT NOT NULL,
assigned_by INTEGER REFERENCES users(id),
PRIMARY KEY (user_id, department)
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_user_primary ON users_departments(user_id) WHERE is_primary=1;
CREATE INDEX IF NOT EXISTS idx_ai_usage_user ON ai_usage(user_id, timestamp);
CREATE INDEX IF NOT EXISTS idx_ai_usage_dept ON ai_usage(department, timestamp);
CREATE INDEX IF NOT EXISTS idx_ai_cache_cached_at ON ai_cache(cached_at);