Add database package
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
-- -*- 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. 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,
|
||||
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;
|
||||
|
||||
-- Approvals track tool calls that require user permission before
|
||||
-- execution. When the LLM requests a tool call that needs approval, a row
|
||||
-- is inserted with status "pending" and the agent loop pauses. The UI
|
||||
-- queries pending approvals to present an inbox to the user. When the
|
||||
-- user decides, the status transitions to "approved" (proceed) or
|
||||
-- "denied" (refuse), and the agent loop resumes.
|
||||
--
|
||||
-- Each approval 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 the approval.
|
||||
CREATE TABLE IF NOT EXISTS approvals (
|
||||
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 'pending',
|
||||
tool_call_id TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||
CHECK (status IN ('pending', 'approved', 'denied'))
|
||||
) STRICT;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_approvals_message ON approvals(message_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_approvals_pending ON approvals(status) WHERE status = 'pending';
|
||||
|
||||
-- Default user. Temporary workaround until authentication is built.
|
||||
INSERT INTO users (id, name, password, role)
|
||||
VALUES ('default', 'owner', '', 'owner');
|
||||
Reference in New Issue
Block a user