-- -*- mode: sql; sql-product: sqlite; -*- -- This file defines the odidere database schema. -- -- Conversations contain messages stored as individual rows. Each -- message stores its wire-format JSON in the content column, matching -- the OpenAI Chat Completions API message structure. This avoids -- per-part decomposition while keeping messages individually addressable -- for editing, deletion, and branching. -- -- Timestamps are stored as TEXT in RFC3339 format. Tables use STRICT typing. -- Users are authenticated accounts. The name is the login identifier, -- unique case-insensitively. The role column is constrained to a fixed set; -- at this time only the "owner" role is supported. -- Deleting a user cascades to all their sessions and conversations. CREATE TABLE IF NOT EXISTS users ( created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), deactivated_at TEXT, id TEXT PRIMARY KEY, name TEXT NOT NULL UNIQUE COLLATE NOCASE, password TEXT NOT NULL, role TEXT NOT NULL, updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), CHECK (role IN ('owner')) ) STRICT; CREATE INDEX IF NOT EXISTS idx_users_deactivated ON users(deactivated_at); -- Sessions represent authenticated login sessions. The session id is used -- as the bearer token in HTTP requests. CREATE TABLE IF NOT EXISTS sessions ( created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), deactivated_at TEXT, id TEXT PRIMARY KEY, updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE ) STRICT; CREATE INDEX IF NOT EXISTS idx_sessions_active ON sessions(user_id, created_at) WHERE deactivated_at IS NULL; -- Conversations are top-level containers for message threads. Each -- conversation belongs to a single user. Deleting a conversation -- cascades to all its messages. CREATE TABLE IF NOT EXISTS conversations ( created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), id TEXT PRIMARY KEY, title TEXT, updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE ) STRICT; CREATE INDEX IF NOT EXISTS idx_conv_user ON conversations(user_id); -- Messages are ordered entries within a conversation. The content column -- stores the wire-format JSON for the message — the exact JSON object -- that appears in the OpenAI Chat Completions messages array. The role -- column is extracted for querying without JSON parsing. Nullable metadata -- stores inference JSONB separately from model-visible content. The sequence -- column defines ordering within the conversation. Deleting a message -- cascades to nothing further (all message data is in the row). CREATE TABLE IF NOT EXISTS messages ( conversation_id TEXT NOT NULL REFERENCES conversations(id) ON DELETE CASCADE, content TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), id TEXT PRIMARY KEY, metadata BLOB CHECK (metadata IS NULL OR json_valid(metadata, 8)), role TEXT NOT NULL, sequence INTEGER NOT NULL, updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), CHECK (role IN ('system', 'developer', 'user', 'assistant', 'tool')), UNIQUE(conversation_id, sequence) ) STRICT; CREATE INDEX IF NOT EXISTS idx_msg_conv ON messages(conversation_id); CREATE INDEX IF NOT EXISTS idx_msg_conv_seq ON messages(conversation_id, sequence); -- A conversation's timestamp is its synchronization token. -- Keep it current whenever the conversation message set changes. CREATE TRIGGER IF NOT EXISTS messages_insert_conversation_updated_at AFTER INSERT ON messages BEGIN UPDATE conversations SET updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE id = NEW.conversation_id; END; CREATE TRIGGER IF NOT EXISTS messages_update_conversation_updated_at AFTER UPDATE ON messages BEGIN UPDATE conversations SET updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE id = NEW.conversation_id; END; CREATE TRIGGER IF NOT EXISTS messages_delete_conversation_updated_at AFTER DELETE ON messages BEGIN UPDATE conversations SET updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE id = OLD.conversation_id; END; -- Executions track the state of tool calls. Each row references a -- specific tool call within an assistant message via message_id and -- tool_call_id. The tool call's name and arguments are stored in the -- message's content JSON; they are not duplicated here. The tool's -- execution result (stdout, stderr, exit code) is stored as a -- separate tool-role message in the messages table, not in this row. -- -- Lifecycle, by tool kind: -- -- A. Approval not required: -- -- ┌────────────┐ ┌────────────┐ -- │ running ├┄┄┄▶│ completed │ -- └────────────┘ └────────────┘ -- -- B. Approval required: -- -- ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐ -- │ pending ├───▶│ approved ├───▶│ running ├┄┄┄▶│ completed │ -- └────────────┘ └────────────┘ └────────────┘ └────────────┘ -- │ -- ▼ -- ┌────────────┐ -- │ denied │ -- └────────────┘ -- -- The dashed arrow from running to completed indicates the possibility that a -- tool call may not complete. if a tool call is interrupted, the row ends at -- running. -- -- approved: tool call approved by user. The call has not launched yet. -- completed: the tool call returned output or an error. Terminal. -- denied: user refused the tool call. Terminal. -- pending: the call awaits a user decision. -- running: the tool call either: -- A. is about to be started, -- B. was started and is still executing, -- C. was interrupted before it started or finished (terminal). CREATE TABLE IF NOT EXISTS executions ( created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), id TEXT PRIMARY KEY, message_id TEXT NOT NULL REFERENCES messages(id) ON DELETE CASCADE, status TEXT NOT NULL DEFAULT 'running', tool_call_id TEXT NOT NULL, updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), CHECK (status IN ('approved', 'completed', 'denied', 'pending', 'running')) ) STRICT; CREATE INDEX IF NOT EXISTS idx_executions_message ON executions(message_id); CREATE INDEX IF NOT EXISTS idx_executions_pending ON executions(status) WHERE status = 'pending'; -- Prevent duplicate execution rows for the same tool call in a message. CREATE UNIQUE INDEX IF NOT EXISTS idx_executions_call ON executions(message_id, tool_call_id); -- Default user. Temporary workaround until authentication is built. INSERT INTO users (id, name, password, role) VALUES ('default', 'owner', '', 'owner');