- app/db/models.py: 13 таблиц (ARCHITECTURE §2.10); перечисления — VARCHAR + CHECK, JSON → JSONB на Postgres, BIGINT/INTEGER-ключи для SQLite - миграция 0001 (автогенерация + ручная чистка: импорт sa.Text, дубли CHECK) - миграции применяются при старте бота программно (app.db.migrate), script_location из пакета - SqlStorage: FSM aiogram поверх fsm_state — состояние переживает рестарт (перенесено из #4) - cleanup_loop: истёкшие черновики, снимки контейнеров и попытки входа старше суток - тесты: upgrade/downgrade, дрейф моделей и миграций, CHECK, CASCADE, FSM, чистка Refs #5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
312 lines
14 KiB
Python
312 lines
14 KiB
Python
"""initial schema
|
|
|
|
Revision ID: 0001
|
|
Revises:
|
|
Create Date: 2026-10-14 17:41:05.672912
|
|
"""
|
|
|
|
from collections.abc import Sequence
|
|
|
|
import sqlalchemy as sa
|
|
from alembic import op
|
|
from sqlalchemy.dialects import postgresql
|
|
|
|
revision: str = "0001"
|
|
down_revision: str | None = None
|
|
branch_labels: str | Sequence[str] | None = None
|
|
depends_on: str | Sequence[str] | None = None
|
|
|
|
|
|
def upgrade() -> None:
|
|
# ### commands auto generated by Alembic - please adjust! ###
|
|
op.create_table(
|
|
"access_attempts",
|
|
sa.Column("id", sa.BigInteger().with_variant(sa.Integer(), "sqlite"), nullable=False),
|
|
sa.Column("tg_id", sa.BigInteger(), nullable=False),
|
|
sa.Column("at", sa.DateTime(timezone=True), nullable=False),
|
|
sa.Column("ok", sa.Boolean(), nullable=False),
|
|
sa.PrimaryKeyConstraint("id", name=op.f("pk_access_attempts")),
|
|
)
|
|
with op.batch_alter_table("access_attempts", schema=None) as batch_op:
|
|
batch_op.create_index("ix_access_attempts_tg_id_at", ["tg_id", "at"], unique=False)
|
|
|
|
op.create_table(
|
|
"alerts",
|
|
sa.Column("id", sa.BigInteger().with_variant(sa.Integer(), "sqlite"), nullable=False),
|
|
sa.Column(
|
|
"kind",
|
|
sa.Enum(
|
|
"crash",
|
|
"oom",
|
|
"restart_loop",
|
|
"unhealthy",
|
|
"overload",
|
|
"deploy_refused",
|
|
"cd_failed",
|
|
"disk_low",
|
|
"dependency_down",
|
|
"agent_attack",
|
|
name="alert_kind",
|
|
native_enum=False,
|
|
create_constraint=True,
|
|
length=20,
|
|
),
|
|
nullable=False,
|
|
),
|
|
sa.Column("service", sa.String(length=100), nullable=True),
|
|
sa.Column(
|
|
"payload", sa.JSON().with_variant(postgresql.JSONB(astext_type=sa.Text()), "postgresql"), nullable=False
|
|
),
|
|
sa.Column("count", sa.Integer(), nullable=False),
|
|
sa.Column("opened_at", sa.DateTime(timezone=True), nullable=False),
|
|
sa.Column("last_at", sa.DateTime(timezone=True), nullable=False),
|
|
sa.Column("resolved_at", sa.DateTime(timezone=True), nullable=True),
|
|
sa.PrimaryKeyConstraint("id", name=op.f("pk_alerts")),
|
|
)
|
|
with op.batch_alter_table("alerts", schema=None) as batch_op:
|
|
batch_op.create_index("ix_alerts_kind_service_resolved_at", ["kind", "service", "resolved_at"], unique=False)
|
|
|
|
op.create_table(
|
|
"container_snapshots",
|
|
sa.Column("id", sa.BigInteger().with_variant(sa.Integer(), "sqlite"), nullable=False),
|
|
sa.Column("container", sa.String(length=100), nullable=False),
|
|
sa.Column("at", sa.DateTime(timezone=True), nullable=False),
|
|
sa.Column("restart_count", sa.Integer(), nullable=False),
|
|
sa.Column("cpu_pct", sa.Float(), nullable=False),
|
|
sa.Column("mem_pct", sa.Float(), nullable=False),
|
|
sa.Column("health", sa.String(length=20), nullable=True),
|
|
sa.PrimaryKeyConstraint("id", name=op.f("pk_container_snapshots")),
|
|
)
|
|
with op.batch_alter_table("container_snapshots", schema=None) as batch_op:
|
|
batch_op.create_index("ix_container_snapshots_container_at", ["container", "at"], unique=False)
|
|
|
|
op.create_table(
|
|
"fsm_state",
|
|
sa.Column("key", sa.String(length=255), nullable=False),
|
|
sa.Column("state", sa.String(length=255), nullable=True),
|
|
sa.Column(
|
|
"data", sa.JSON().with_variant(postgresql.JSONB(astext_type=sa.Text()), "postgresql"), nullable=False
|
|
),
|
|
sa.PrimaryKeyConstraint("key", name=op.f("pk_fsm_state")),
|
|
)
|
|
op.create_table(
|
|
"users",
|
|
sa.Column("tg_id", sa.BigInteger(), autoincrement=False, nullable=False),
|
|
sa.Column("username", sa.String(length=64), nullable=True),
|
|
sa.Column("full_name", sa.String(length=256), nullable=False),
|
|
sa.Column(
|
|
"role",
|
|
sa.Enum("member", "admin", name="role", native_enum=False, create_constraint=True, length=20),
|
|
nullable=False,
|
|
),
|
|
sa.Column("is_active", sa.Boolean(), nullable=False),
|
|
sa.Column("invited_by", sa.BigInteger(), nullable=True),
|
|
sa.Column("joined_at", sa.DateTime(timezone=True), nullable=False),
|
|
sa.Column("last_seen_at", sa.DateTime(timezone=True), nullable=True),
|
|
sa.ForeignKeyConstraint(["invited_by"], ["users.tg_id"], name=op.f("fk_users_invited_by_users")),
|
|
sa.PrimaryKeyConstraint("tg_id", name=op.f("pk_users")),
|
|
)
|
|
op.create_table(
|
|
"agent_requests",
|
|
sa.Column("id", sa.BigInteger().with_variant(sa.Integer(), "sqlite"), nullable=False),
|
|
sa.Column("user_id", sa.BigInteger(), nullable=False),
|
|
sa.Column("prompt", sa.Text(), nullable=False),
|
|
sa.Column(
|
|
"verdict",
|
|
sa.Enum("safe", "attack", "error", name="verdict", native_enum=False, create_constraint=True, length=20),
|
|
nullable=False,
|
|
),
|
|
sa.Column("reason", sa.Text(), nullable=True),
|
|
sa.Column(
|
|
"log_query", sa.JSON().with_variant(postgresql.JSONB(astext_type=sa.Text()), "postgresql"), nullable=True
|
|
),
|
|
sa.Column("tokens", sa.Integer(), nullable=False),
|
|
sa.Column("at", sa.DateTime(timezone=True), nullable=False),
|
|
sa.ForeignKeyConstraint(["user_id"], ["users.tg_id"], name=op.f("fk_agent_requests_user_id_users")),
|
|
sa.PrimaryKeyConstraint("id", name=op.f("pk_agent_requests")),
|
|
)
|
|
op.create_table(
|
|
"alert_deliveries",
|
|
sa.Column("id", sa.BigInteger().with_variant(sa.Integer(), "sqlite"), nullable=False),
|
|
sa.Column("alert_id", sa.BigInteger().with_variant(sa.Integer(), "sqlite"), nullable=False),
|
|
sa.Column("chat_id", sa.BigInteger(), nullable=False),
|
|
sa.Column("message_id", sa.BigInteger(), nullable=False),
|
|
sa.ForeignKeyConstraint(
|
|
["alert_id"], ["alerts.id"], name=op.f("fk_alert_deliveries_alert_id_alerts"), ondelete="CASCADE"
|
|
),
|
|
sa.PrimaryKeyConstraint("id", name=op.f("pk_alert_deliveries")),
|
|
sa.UniqueConstraint("alert_id", "chat_id", name=op.f("uq_alert_deliveries_alert_id")),
|
|
)
|
|
op.create_table(
|
|
"drafts",
|
|
sa.Column("id", sa.BigInteger().with_variant(sa.Integer(), "sqlite"), nullable=False),
|
|
sa.Column("user_id", sa.BigInteger(), nullable=False),
|
|
sa.Column("base_sha", sa.String(length=40), nullable=False),
|
|
sa.Column("message", sa.Text(), nullable=True),
|
|
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
|
|
sa.Column("expires_at", sa.DateTime(timezone=True), nullable=False),
|
|
sa.ForeignKeyConstraint(["user_id"], ["users.tg_id"], name=op.f("fk_drafts_user_id_users"), ondelete="CASCADE"),
|
|
sa.PrimaryKeyConstraint("id", name=op.f("pk_drafts")),
|
|
sa.UniqueConstraint("user_id", name=op.f("uq_drafts_user_id")),
|
|
)
|
|
op.create_table(
|
|
"invites",
|
|
sa.Column("id", sa.BigInteger().with_variant(sa.Integer(), "sqlite"), nullable=False),
|
|
sa.Column("token_hash", sa.String(length=64), nullable=False),
|
|
sa.Column("created_by", sa.BigInteger(), nullable=False),
|
|
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
|
|
sa.Column("expires_at", sa.DateTime(timezone=True), nullable=False),
|
|
sa.Column("used_by", sa.BigInteger(), nullable=True),
|
|
sa.Column("used_at", sa.DateTime(timezone=True), nullable=True),
|
|
sa.ForeignKeyConstraint(["created_by"], ["users.tg_id"], name=op.f("fk_invites_created_by_users")),
|
|
sa.ForeignKeyConstraint(["used_by"], ["users.tg_id"], name=op.f("fk_invites_used_by_users")),
|
|
sa.PrimaryKeyConstraint("id", name=op.f("pk_invites")),
|
|
sa.UniqueConstraint("token_hash", name=op.f("uq_invites_token_hash")),
|
|
)
|
|
op.create_table(
|
|
"log_queries",
|
|
sa.Column("id", sa.BigInteger().with_variant(sa.Integer(), "sqlite"), nullable=False),
|
|
sa.Column("user_id", sa.BigInteger(), nullable=False),
|
|
sa.Column("container", sa.String(length=100), nullable=False),
|
|
sa.Column(
|
|
"mode",
|
|
sa.Enum("tail", "grep", "agent", name="log_mode", native_enum=False, create_constraint=True, length=20),
|
|
nullable=False,
|
|
),
|
|
sa.Column(
|
|
"query", sa.JSON().with_variant(postgresql.JSONB(astext_type=sa.Text()), "postgresql"), nullable=False
|
|
),
|
|
sa.Column("lines", sa.Integer(), nullable=False),
|
|
sa.Column("at", sa.DateTime(timezone=True), nullable=False),
|
|
sa.ForeignKeyConstraint(["user_id"], ["users.tg_id"], name=op.f("fk_log_queries_user_id_users")),
|
|
sa.PrimaryKeyConstraint("id", name=op.f("pk_log_queries")),
|
|
)
|
|
op.create_table(
|
|
"pushes",
|
|
sa.Column("id", sa.BigInteger().with_variant(sa.Integer(), "sqlite"), nullable=False),
|
|
sa.Column("user_id", sa.BigInteger(), nullable=False),
|
|
sa.Column("sha", sa.String(length=40), nullable=False),
|
|
sa.Column("message", sa.Text(), nullable=False),
|
|
sa.Column(
|
|
"paths", sa.JSON().with_variant(postgresql.JSONB(astext_type=sa.Text()), "postgresql"), nullable=False
|
|
),
|
|
sa.Column("chat_id", sa.BigInteger(), nullable=False),
|
|
sa.Column("message_id", sa.BigInteger(), nullable=True),
|
|
sa.Column(
|
|
"ci_status",
|
|
sa.Enum(
|
|
"pending",
|
|
"running",
|
|
"success",
|
|
"failed",
|
|
"timeout",
|
|
"skipped",
|
|
name="ci_status",
|
|
native_enum=False,
|
|
create_constraint=True,
|
|
length=20,
|
|
),
|
|
nullable=False,
|
|
),
|
|
sa.Column(
|
|
"cd_status",
|
|
sa.Enum(
|
|
"pending",
|
|
"running",
|
|
"success",
|
|
"failed",
|
|
"timeout",
|
|
"skipped",
|
|
name="cd_status",
|
|
native_enum=False,
|
|
create_constraint=True,
|
|
length=20,
|
|
),
|
|
nullable=False,
|
|
),
|
|
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
|
|
sa.ForeignKeyConstraint(["user_id"], ["users.tg_id"], name=op.f("fk_pushes_user_id_users")),
|
|
sa.PrimaryKeyConstraint("id", name=op.f("pk_pushes")),
|
|
sa.UniqueConstraint("sha", name=op.f("uq_pushes_sha")),
|
|
)
|
|
op.create_table(
|
|
"draft_changes",
|
|
sa.Column("id", sa.BigInteger().with_variant(sa.Integer(), "sqlite"), nullable=False),
|
|
sa.Column("draft_id", sa.BigInteger().with_variant(sa.Integer(), "sqlite"), nullable=False),
|
|
sa.Column("path", sa.String(length=500), nullable=False),
|
|
sa.Column(
|
|
"op",
|
|
sa.Enum("add", "modify", "delete", name="change_op", native_enum=False, create_constraint=True, length=20),
|
|
nullable=False,
|
|
),
|
|
sa.Column("content", sa.LargeBinary(), nullable=True),
|
|
sa.ForeignKeyConstraint(
|
|
["draft_id"], ["drafts.id"], name=op.f("fk_draft_changes_draft_id_drafts"), ondelete="CASCADE"
|
|
),
|
|
sa.PrimaryKeyConstraint("id", name=op.f("pk_draft_changes")),
|
|
sa.UniqueConstraint("draft_id", "path", name=op.f("uq_draft_changes_draft_id")),
|
|
)
|
|
op.create_table(
|
|
"pipeline_runs",
|
|
sa.Column("id", sa.BigInteger().with_variant(sa.Integer(), "sqlite"), nullable=False),
|
|
sa.Column("push_id", sa.BigInteger().with_variant(sa.Integer(), "sqlite"), nullable=False),
|
|
sa.Column(
|
|
"stage",
|
|
sa.Enum("ci", "cd", name="stage", native_enum=False, create_constraint=True, length=20),
|
|
nullable=False,
|
|
),
|
|
sa.Column("run_slug", sa.String(length=50), nullable=False),
|
|
sa.Column(
|
|
"status",
|
|
sa.Enum(
|
|
"pending",
|
|
"running",
|
|
"success",
|
|
"failed",
|
|
"timeout",
|
|
"skipped",
|
|
name="run_status",
|
|
native_enum=False,
|
|
create_constraint=True,
|
|
length=20,
|
|
),
|
|
nullable=False,
|
|
),
|
|
sa.Column("failed_cube", sa.String(length=100), nullable=True),
|
|
sa.Column("url", sa.String(length=500), nullable=True),
|
|
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
|
|
sa.ForeignKeyConstraint(
|
|
["push_id"], ["pushes.id"], name=op.f("fk_pipeline_runs_push_id_pushes"), ondelete="CASCADE"
|
|
),
|
|
sa.PrimaryKeyConstraint("id", name=op.f("pk_pipeline_runs")),
|
|
sa.UniqueConstraint("push_id", "stage", name=op.f("uq_pipeline_runs_push_id")),
|
|
)
|
|
# ### end Alembic commands ###
|
|
|
|
|
|
def downgrade() -> None:
|
|
# ### commands auto generated by Alembic - please adjust! ###
|
|
op.drop_table("pipeline_runs")
|
|
op.drop_table("draft_changes")
|
|
op.drop_table("pushes")
|
|
op.drop_table("log_queries")
|
|
op.drop_table("invites")
|
|
op.drop_table("drafts")
|
|
op.drop_table("alert_deliveries")
|
|
op.drop_table("agent_requests")
|
|
op.drop_table("users")
|
|
op.drop_table("fsm_state")
|
|
with op.batch_alter_table("container_snapshots", schema=None) as batch_op:
|
|
batch_op.drop_index("ix_container_snapshots_container_at")
|
|
|
|
op.drop_table("container_snapshots")
|
|
with op.batch_alter_table("alerts", schema=None) as batch_op:
|
|
batch_op.drop_index("ix_alerts_kind_service_resolved_at")
|
|
|
|
op.drop_table("alerts")
|
|
with op.batch_alter_table("access_attempts", schema=None) as batch_op:
|
|
batch_op.drop_index("ix_access_attempts_tg_id_at")
|
|
|
|
op.drop_table("access_attempts")
|
|
# ### end Alembic commands ###
|