dealspace/migrations/001_initial.sql

153 lines
5.5 KiB
SQL

-- Dealspace initial schema
-- All content fields (search_key, search_key2, summary, data) are encrypted blobs.
-- Plain fields (entry_id, project_id, parent_id, type, depth, stage, timestamps, IDs) are indexed.
PRAGMA journal_mode = WAL;
PRAGMA foreign_keys = ON;
-- Users
CREATE TABLE IF NOT EXISTS users (
user_id TEXT PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
password TEXT NOT NULL, -- bcrypt hash
org_id TEXT NOT NULL DEFAULT '',
org_name TEXT NOT NULL DEFAULT '',
mfa_secret TEXT NOT NULL DEFAULT '', -- TOTP secret, encrypted
active INTEGER NOT NULL DEFAULT 1,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
CREATE INDEX IF NOT EXISTS idx_users_org ON users(org_id);
-- Entries: the single tree
CREATE TABLE IF NOT EXISTS entries (
entry_id TEXT PRIMARY KEY,
project_id TEXT NOT NULL,
parent_id TEXT NOT NULL DEFAULT '',
type TEXT NOT NULL,
depth INTEGER NOT NULL DEFAULT 0,
search_key BLOB, -- blind index (HMAC-SHA256)
search_key2 BLOB, -- blind index (HMAC-SHA256)
summary BLOB, -- packed: zstd + AES-256-GCM
data BLOB, -- packed: zstd + AES-256-GCM
stage TEXT NOT NULL DEFAULT 'pre_dataroom',
assignee_id TEXT NOT NULL DEFAULT '',
return_to_id TEXT NOT NULL DEFAULT '',
origin_id TEXT NOT NULL DEFAULT '',
version INTEGER NOT NULL DEFAULT 1,
deleted_at INTEGER,
deleted_by TEXT,
key_version INTEGER NOT NULL DEFAULT 1,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
created_by TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_entries_project ON entries(project_id);
CREATE INDEX IF NOT EXISTS idx_entries_parent ON entries(parent_id);
CREATE INDEX IF NOT EXISTS idx_entries_type ON entries(type);
CREATE INDEX IF NOT EXISTS idx_entries_stage ON entries(stage);
CREATE INDEX IF NOT EXISTS idx_entries_assignee ON entries(assignee_id);
CREATE INDEX IF NOT EXISTS idx_entries_return ON entries(return_to_id);
CREATE INDEX IF NOT EXISTS idx_entries_origin ON entries(origin_id);
CREATE INDEX IF NOT EXISTS idx_entries_search_key ON entries(search_key);
CREATE INDEX IF NOT EXISTS idx_entries_deleted ON entries(deleted_at);
-- Answer ↔ Request links (many-to-many)
CREATE TABLE IF NOT EXISTS answer_links (
answer_id TEXT NOT NULL REFERENCES entries(entry_id),
request_id TEXT NOT NULL REFERENCES entries(entry_id),
linked_by TEXT NOT NULL,
linked_at INTEGER NOT NULL,
confirmed INTEGER NOT NULL DEFAULT 0,
ai_score REAL,
status TEXT NOT NULL DEFAULT 'pending', -- pending | confirmed | rejected
reviewed_by TEXT,
reviewed_at INTEGER,
reject_reason TEXT,
PRIMARY KEY (answer_id, request_id)
);
-- Access / RBAC
CREATE TABLE IF NOT EXISTS access (
id TEXT PRIMARY KEY,
project_id TEXT NOT NULL,
workstream_id TEXT,
user_id TEXT NOT NULL,
role TEXT NOT NULL,
ops TEXT NOT NULL,
can_grant INTEGER NOT NULL DEFAULT 0,
granted_by TEXT NOT NULL,
granted_at INTEGER NOT NULL,
revoked_at INTEGER,
revoked_by TEXT
);
CREATE INDEX IF NOT EXISTS idx_access_user ON access(user_id);
CREATE INDEX IF NOT EXISTS idx_access_project ON access(project_id);
-- Entry events (workflow thread)
CREATE TABLE IF NOT EXISTS entry_events (
id TEXT PRIMARY KEY,
entry_id TEXT NOT NULL REFERENCES entries(entry_id),
actor_id TEXT NOT NULL,
channel TEXT NOT NULL,
action BLOB NOT NULL, -- packed
data BLOB NOT NULL, -- packed
ts INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_events_entry ON entry_events(entry_id);
CREATE INDEX IF NOT EXISTS idx_events_actor ON entry_events(actor_id);
-- Channel threads (email/Slack/Teams → entry mapping)
CREATE TABLE IF NOT EXISTS channel_threads (
id TEXT PRIMARY KEY,
entry_id TEXT NOT NULL REFERENCES entries(entry_id),
channel TEXT NOT NULL,
thread_id TEXT NOT NULL,
project_id TEXT NOT NULL,
UNIQUE(channel, thread_id)
);
-- Embeddings (AI matching)
CREATE TABLE IF NOT EXISTS embeddings (
entry_id TEXT PRIMARY KEY REFERENCES entries(entry_id),
vector BLOB NOT NULL
);
-- Audit log
CREATE TABLE IF NOT EXISTS audit (
id TEXT PRIMARY KEY,
project_id TEXT NOT NULL,
actor_id TEXT NOT NULL,
action BLOB NOT NULL, -- packed
target_id TEXT,
details BLOB, -- packed
ip TEXT,
ts INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_audit_project ON audit(project_id);
CREATE INDEX IF NOT EXISTS idx_audit_actor ON audit(actor_id);
CREATE INDEX IF NOT EXISTS idx_audit_ts ON audit(ts);
-- Sessions
CREATE TABLE IF NOT EXISTS sessions (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(user_id),
fingerprint TEXT NOT NULL DEFAULT '',
created_at INTEGER NOT NULL,
expires_at INTEGER NOT NULL,
revoked INTEGER NOT NULL DEFAULT 0
);
CREATE INDEX IF NOT EXISTS idx_sessions_user ON sessions(user_id);
-- Broadcasts (idempotency)
CREATE TABLE IF NOT EXISTS broadcasts (
id TEXT PRIMARY KEY,
answer_id TEXT NOT NULL,
request_id TEXT NOT NULL,
recipient_id TEXT NOT NULL,
sent_at INTEGER NOT NULL,
UNIQUE(answer_id, request_id, recipient_id)
);