zcbot/db/migrations/versions/20260514_0930_0001_initial_...

126 lines
5.8 KiB
Python

"""initial schema -- users / tasks / messages / runs / usage_events
Revision ID: 0001
Revises:
Create Date: 2026-05-14
DESIGN.md section 7.4 schema. First migration.
- pgcrypto extension fallback (PG 13+ has gen_random_uuid built-in;
older versions need the extension).
- messages.payload GIN index (jsonb_path_ops).
- tasks (user_id, task_dir) and (user_id, status) composite indexes.
- Local sentinel user is INSERTed by core.storage.ensure_local_sentinel
at CLI startup, NOT in this migration (avoids stray sentinel rows on
the SaaS instance).
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
revision: str = "0001"
down_revision: Union[str, None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.execute("CREATE EXTENSION IF NOT EXISTS pgcrypto")
op.create_table(
"users",
sa.Column("user_id", postgresql.UUID(as_uuid=True), primary_key=True,
server_default=sa.text("gen_random_uuid()")),
sa.Column("email", sa.Text(), nullable=True),
sa.Column("oidc_subject", sa.Text(), nullable=True),
sa.Column("password_hash", sa.Text(), nullable=True),
sa.Column("plan", sa.Text(), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True),
server_default=sa.text("now()"), nullable=False),
)
op.create_table(
"tasks",
sa.Column("task_id", postgresql.UUID(as_uuid=True), primary_key=True,
server_default=sa.text("gen_random_uuid()")),
sa.Column("user_id", postgresql.UUID(as_uuid=True),
sa.ForeignKey("users.user_id"), nullable=False),
sa.Column("task_dir", sa.Text(), nullable=False),
sa.Column("mode", sa.Text(), nullable=False, server_default=""),
sa.Column("description", sa.Text(), nullable=False, server_default=""),
sa.Column("status", sa.Text(), nullable=False, server_default="active"),
sa.Column("model", sa.Text(), nullable=False, server_default=""),
sa.Column("model_profile", sa.Text(), nullable=False, server_default=""),
sa.Column("reasoning_effort", sa.Text(), nullable=False, server_default=""),
sa.Column("tokens_prompt", sa.Integer(), nullable=False, server_default="0"),
sa.Column("tokens_completion", sa.Integer(), nullable=False, server_default="0"),
sa.Column("cost_usd", sa.Numeric(12, 6), nullable=False, server_default="0"),
sa.Column("created_at", sa.DateTime(timezone=True),
server_default=sa.text("now()"), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True),
server_default=sa.text("now()"), nullable=False),
)
op.create_index("ix_tasks_user_task_dir", "tasks", ["user_id", "task_dir"])
op.create_index("ix_tasks_user_status", "tasks", ["user_id", "status"])
op.create_table(
"messages",
sa.Column("message_id", postgresql.UUID(as_uuid=True), primary_key=True,
server_default=sa.text("gen_random_uuid()")),
sa.Column("task_id", postgresql.UUID(as_uuid=True),
sa.ForeignKey("tasks.task_id", ondelete="CASCADE"), nullable=False),
sa.Column("idx", sa.Integer(), nullable=False),
sa.Column("payload", postgresql.JSONB(), nullable=False),
sa.Column("tokens_in", sa.Integer(), nullable=True),
sa.Column("tokens_out", sa.Integer(), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True),
server_default=sa.text("now()"), nullable=False),
sa.UniqueConstraint("task_id", "idx", name="uq_messages_task_idx"),
)
op.create_index(
"ix_messages_payload_gin", "messages", ["payload"],
postgresql_using="gin", postgresql_ops={"payload": "jsonb_path_ops"},
)
op.create_table(
"runs",
sa.Column("run_id", postgresql.UUID(as_uuid=True), primary_key=True,
server_default=sa.text("gen_random_uuid()")),
sa.Column("task_id", postgresql.UUID(as_uuid=True),
sa.ForeignKey("tasks.task_id", ondelete="CASCADE"), nullable=False),
sa.Column("status", sa.Text(), nullable=False, server_default="pending"),
sa.Column("started_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("finished_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("error", sa.Text(), nullable=True),
sa.Column("tokens_p", sa.Integer(), nullable=False, server_default="0"),
sa.Column("tokens_c", sa.Integer(), nullable=False, server_default="0"),
)
op.create_index("ix_runs_task", "runs", ["task_id"])
op.create_table(
"usage_events",
sa.Column("id", sa.BigInteger(), primary_key=True, autoincrement=True),
sa.Column("user_id", postgresql.UUID(as_uuid=True), nullable=False),
sa.Column("task_id", postgresql.UUID(as_uuid=True), nullable=True),
sa.Column("run_id", postgresql.UUID(as_uuid=True), nullable=True),
sa.Column("kind", sa.Text(), nullable=False),
sa.Column("value", sa.Numeric(20, 8), nullable=False),
sa.Column("ts", sa.DateTime(timezone=True),
server_default=sa.text("now()"), nullable=False),
)
op.create_index("ix_usage_user_ts", "usage_events", ["user_id", "ts"])
def downgrade() -> None:
op.drop_index("ix_usage_user_ts", table_name="usage_events")
op.drop_table("usage_events")
op.drop_index("ix_runs_task", table_name="runs")
op.drop_table("runs")
op.drop_index("ix_messages_payload_gin", table_name="messages")
op.drop_table("messages")
op.drop_index("ix_tasks_user_status", table_name="tasks")
op.drop_index("ix_tasks_user_task_dir", table_name="tasks")
op.drop_table("tasks")
op.drop_table("users")