"""Idempotent DDL for the bot's own tables (mirrors the api's init_db.py convention — the ORM never creates tables; this script does, IF NOT EXISTS). """ import logging from sqlalchemy import text from sqlalchemy.ext.asyncio import AsyncSession from app.models import Base logger = logging.getLogger("bot.db_init") DDL = [ """ CREATE TABLE IF NOT EXISTS bot_links ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), chat_id BIGINT UNIQUE NOT NULL, user_id UUID, casdoor_username TEXT, display_name TEXT, role VARCHAR(32), created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ) """, "CREATE INDEX IF NOT EXISTS ix_bot_links_chat_id ON bot_links (chat_id)", """ CREATE TABLE IF NOT EXISTS bot_drafts ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), chat_id BIGINT NOT NULL, kind VARCHAR(16) NOT NULL, payload JSONB NOT NULL DEFAULT '{}'::jsonb, missing JSONB, status VARCHAR(16) NOT NULL DEFAULT 'active', source VARCHAR(16), source_message_id BIGINT, review_message_id BIGINT, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ) """, "CREATE INDEX IF NOT EXISTS ix_bot_drafts_chat_id ON bot_drafts (chat_id)", "CREATE INDEX IF NOT EXISTS ix_bot_drafts_chat_status ON bot_drafts (chat_id, status)", "ALTER TABLE bot_drafts ADD COLUMN IF NOT EXISTS last_bale_update_at TIMESTAMPTZ", "ALTER TABLE bot_drafts ADD COLUMN IF NOT EXISTS source_message_id BIGINT", "CREATE INDEX IF NOT EXISTS ix_bot_drafts_review_msg ON bot_drafts (review_message_id)", "CREATE INDEX IF NOT EXISTS ix_bot_drafts_source_msg ON bot_drafts (source_message_id)", """ CREATE TABLE IF NOT EXISTS bot_messages ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), chat_id BIGINT NOT NULL, direction VARCHAR(4) NOT NULL, kind VARCHAR(16) NOT NULL DEFAULT 'text', body TEXT, payload JSONB, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ) """, "CREATE INDEX IF NOT EXISTS ix_bot_messages_chat_id ON bot_messages (chat_id)", """ CREATE TABLE IF NOT EXISTS bot_meta ( key VARCHAR(64) PRIMARY KEY, value TEXT, updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ) """, # ---- Observability (phase 1) — silent pipeline recorder, DB-only ------- """ CREATE TABLE IF NOT EXISTS bot_traces ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), chat_id BIGINT NOT NULL, message_id BIGINT, kind VARCHAR(8) NOT NULL, source VARCHAR(16), draft_id UUID, draft_kind VARCHAR(16), committed_quote_id UUID, status VARCHAR(16) NOT NULL DEFAULT 'open', created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ) """, "CREATE INDEX IF NOT EXISTS ix_bot_traces_chat ON bot_traces (chat_id, created_at)", "CREATE INDEX IF NOT EXISTS ix_bot_traces_draft ON bot_traces (draft_id)", """ CREATE TABLE IF NOT EXISTS bot_trace_events ( id BIGSERIAL PRIMARY KEY, trace_id UUID NOT NULL REFERENCES bot_traces(id) ON DELETE CASCADE, stage VARCHAR(32) NOT NULL, ok BOOLEAN NOT NULL DEFAULT TRUE, latency_ms INTEGER, model VARCHAR(64), field VARCHAR(32), payload JSONB NOT NULL DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ) """, "CREATE INDEX IF NOT EXISTS ix_bot_trace_events_trace ON bot_trace_events (trace_id, created_at)", "CREATE INDEX IF NOT EXISTS ix_bot_trace_events_stage ON bot_trace_events (stage)", "CREATE INDEX IF NOT EXISTS ix_bot_trace_events_payload ON bot_trace_events USING GIN (payload jsonb_path_ops)", ] async def ensure_bot_tables(session: AsyncSession) -> None: for stmt in DDL: await session.execute(text(stmt)) await session.commit() logger.info("bot tables ready") async def ensure_schema() -> None: from app.db import SessionLocal async with SessionLocal() as session: await ensure_bot_tables(session)