Files
query-orchestration/internal/database/migrations/00000000000131_create_chatbot_tables.up.sql
Jay Brown 72de690894 Merged in feature/chatbot1 (pull request #223)
Chatbot functionality

* baseline working

* missing test file

* more tests
2026-05-07 20:56:18 +00:00

103 lines
4.6 KiB
SQL

-- Chatbot backend support, milestone M1: chatbot tables.
-- See plans/chatbot_plan_codex.v10.md §4 (Data Model) for the canonical
-- definition. Existing tables retain camelCase column names (per migrations
-- 1-130); new bot_* tables use snake_case to match migrations 127-130.
--
-- Tables created in dependency order:
-- 1. bot_sessions — owner-scoped chatbot conversations
-- 2. bot_session_documents — explicit per-session document scope
-- 3. bot_session_folders — explicit per-session folder scope
-- 4. bot_turns — append-only turn log per session
-- ---------- bot_sessions ----------
CREATE TABLE bot_sessions (
id uuid NOT NULL DEFAULT uuid_generate_v7(),
client_id varchar(255) NOT NULL REFERENCES clients(clientId) ON DELETE CASCADE,
created_by varchar(255) NOT NULL,
created_at timestamptz NOT NULL DEFAULT NOW(),
updated_at timestamptz NOT NULL DEFAULT NOW(),
title text NOT NULL DEFAULT '',
state jsonb NOT NULL DEFAULT '{}'::jsonb,
is_deleted boolean NOT NULL DEFAULT false,
CONSTRAINT pk_bot_sessions PRIMARY KEY (id)
);
CREATE INDEX idx_bot_sessions_client_user_active
ON bot_sessions (client_id, created_by, updated_at DESC)
WHERE is_deleted = false;
CREATE INDEX idx_bot_sessions_client_active
ON bot_sessions (client_id, updated_at DESC)
WHERE is_deleted = false;
-- ---------- bot_session_documents (scope) ----------
CREATE TABLE bot_session_documents (
session_id uuid NOT NULL REFERENCES bot_sessions(id) ON DELETE CASCADE,
document_id uuid NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
CONSTRAINT pk_bot_session_documents PRIMARY KEY (session_id, document_id)
);
CREATE INDEX idx_bsd_document ON bot_session_documents(document_id);
-- ---------- bot_session_folders (scope) ----------
CREATE TABLE bot_session_folders (
session_id uuid NOT NULL REFERENCES bot_sessions(id) ON DELETE CASCADE,
folder_id uuid NOT NULL REFERENCES folders(id) ON DELETE CASCADE,
CONSTRAINT pk_bot_session_folders PRIMARY KEY (session_id, folder_id)
);
CREATE INDEX idx_bsf_folder ON bot_session_folders(folder_id);
-- ---------- bot_turns ----------
--
-- bot_turns_status_fields_consistent groups session_deleted with errored
-- and abandoned: completion IS NULL AND error IS NOT NULL. This matches
-- plan §4 verbatim.
CREATE TABLE bot_turns (
session_id uuid NOT NULL REFERENCES bot_sessions(id) ON DELETE CASCADE,
ordinal int NOT NULL CHECK (ordinal > 0),
prompt text NOT NULL,
completion text,
status text NOT NULL DEFAULT 'in_flight',
attempt_id uuid NOT NULL,
attempt_started_at timestamptz NOT NULL DEFAULT NOW(),
created_at timestamptz NOT NULL DEFAULT NOW(),
latency_ms int,
tokens_in int,
tokens_out int,
error jsonb,
CONSTRAINT pk_bot_turns PRIMARY KEY (session_id, ordinal),
CONSTRAINT bot_turns_status_values CHECK (
status IN ('in_flight', 'completed', 'errored', 'abandoned', 'session_deleted')
),
-- bot_turns_status_fields_consistent enforces the (status, completion,
-- error) consistency matrix from plan §4. The trailing OR-clause
-- (`status NOT IN (...)`) is an evaluation-order guard, not a relaxation
-- of the rule: Postgres evaluates table-level CHECK constraints in
-- alphabetical order by constraint name, so without this guard a row
-- with an UNKNOWN status (e.g. 'frobnicated') trips this constraint
-- before bot_turns_status_values gets a chance to reject the unknown
-- enum value. With the guard, unknown statuses pass this consistency
-- check (every branch becomes irrelevant) and bot_turns_status_values
-- owns the rejection. For every documented status value the plan §4
-- literal still holds verbatim.
CONSTRAINT bot_turns_status_fields_consistent CHECK (
(status = 'in_flight' AND completion IS NULL AND error IS NULL)
OR (status = 'completed' AND completion IS NOT NULL AND error IS NULL)
OR (status IN ('errored', 'abandoned', 'session_deleted') AND completion IS NULL AND error IS NOT NULL)
OR status NOT IN ('in_flight', 'completed', 'errored', 'abandoned', 'session_deleted')
)
);
CREATE UNIQUE INDEX bot_turns_one_inflight_per_session
ON bot_turns (session_id)
WHERE status = 'in_flight';
CREATE INDEX idx_bot_turns_session_completed
ON bot_turns (session_id, ordinal)
WHERE status = 'completed';