2026-09-20 12:27:30 +00:00
|
|
|
-- -*- 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
|
2026-10-23 23:41:35 +00:00
|
|
|
-- stores the wire-format JSON for the message — the exact JSON object that
|
|
|
|
|
-- appears in the OpenAI Chat Completions messages array; a CHECK constraint
|
|
|
|
|
-- keeps it valid JSON. 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).
|
2026-09-20 12:27:30 +00:00
|
|
|
CREATE TABLE IF NOT EXISTS messages (
|
|
|
|
|
conversation_id TEXT NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
|
2026-10-23 23:41:35 +00:00
|
|
|
content TEXT NOT NULL CHECK (json_valid(content)),
|
2026-09-20 12:27:30 +00:00
|
|
|
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
|
|
|
id TEXT PRIMARY KEY,
|
2026-10-09 13:57:48 +00:00
|
|
|
metadata BLOB CHECK (metadata IS NULL OR json_valid(metadata, 8)),
|
2026-09-20 12:27:30 +00:00
|
|
|
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;
|
|
|
|
|
|
2026-10-03 03:42:52 +00:00
|
|
|
-- 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.
|
2026-09-20 12:27:30 +00:00
|
|
|
--
|
2026-10-03 03:42:52 +00:00
|
|
|
-- 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 (
|
2026-09-20 12:27:30 +00:00
|
|
|
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,
|
2026-10-03 03:42:52 +00:00
|
|
|
status TEXT NOT NULL DEFAULT 'running',
|
2026-09-20 12:27:30 +00:00
|
|
|
tool_call_id TEXT NOT NULL,
|
|
|
|
|
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
2026-10-03 03:42:52 +00:00
|
|
|
CHECK (status IN ('approved', 'completed', 'denied', 'pending', 'running'))
|
2026-09-20 12:27:30 +00:00
|
|
|
) STRICT;
|
|
|
|
|
|
2026-10-03 03:42:52 +00:00
|
|
|
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';
|
2026-10-09 13:57:48 +00:00
|
|
|
-- 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);
|
2026-09-20 12:27:30 +00:00
|
|
|
|
|
|
|
|
-- Default user. Temporary workaround until authentication is built.
|
|
|
|
|
INSERT INTO users (id, name, password, role)
|
|
|
|
|
VALUES ('default', 'owner', '', 'owner');
|