6641eba9df
replace monolithic migration with ordered scripts, include profiles/sessions in migration, and verify full downgrade/upgrade cycle for clean Supabase bootstrap
286 lines
9.5 KiB
Python
286 lines
9.5 KiB
Python
"""initial schema part 1: foundation tables
|
|
|
|
Revision ID: 202602260001
|
|
Revises:
|
|
Create Date: 2026-02-26 20:10:00
|
|
"""
|
|
|
|
from typing import Sequence, Union
|
|
|
|
from alembic import op
|
|
import sqlalchemy as sa
|
|
from sqlalchemy.dialects import postgresql
|
|
|
|
revision: str = "202602260001"
|
|
down_revision: Union[str, Sequence[str], None] = None
|
|
branch_labels: Union[str, Sequence[str], None] = None
|
|
depends_on: Union[str, Sequence[str], None] = None
|
|
|
|
|
|
def upgrade() -> None:
|
|
op.create_table(
|
|
"llm_factory",
|
|
sa.Column("id", sa.UUID(), nullable=False),
|
|
sa.Column("name", sa.String(length=50), nullable=False),
|
|
sa.Column("request_url", sa.String(length=255), nullable=False),
|
|
sa.Column("avatar", sa.Text(), nullable=True),
|
|
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,
|
|
),
|
|
sa.Column("deleted_at", sa.DateTime(timezone=True), nullable=True),
|
|
sa.PrimaryKeyConstraint("id"),
|
|
sa.UniqueConstraint("name"),
|
|
)
|
|
op.create_index("ix_llm_factory_name", "llm_factory", ["name"], unique=True)
|
|
_enable_rls("llm_factory")
|
|
|
|
op.create_table(
|
|
"llms",
|
|
sa.Column("id", sa.UUID(), nullable=False),
|
|
sa.Column("factory_id", sa.UUID(), nullable=False),
|
|
sa.Column("model_code", sa.String(length=50), nullable=False),
|
|
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,
|
|
),
|
|
sa.Column("deleted_at", sa.DateTime(timezone=True), nullable=True),
|
|
sa.PrimaryKeyConstraint("id"),
|
|
sa.UniqueConstraint("model_code"),
|
|
)
|
|
op.create_index("ix_llms_factory_id", "llms", ["factory_id"], unique=False)
|
|
op.create_index("ix_llms_model_code", "llms", ["model_code"], unique=True)
|
|
op.create_foreign_key(
|
|
"fk_llms_factory_id",
|
|
"llms",
|
|
"llm_factory",
|
|
["factory_id"],
|
|
["id"],
|
|
ondelete="RESTRICT",
|
|
)
|
|
_enable_rls("llms")
|
|
|
|
op.create_table(
|
|
"profiles",
|
|
sa.Column("id", sa.UUID(), nullable=False),
|
|
sa.Column("username", sa.String(length=30), nullable=False),
|
|
sa.Column("avatar_url", sa.Text(), nullable=True),
|
|
sa.Column("bio", sa.String(length=200), nullable=True),
|
|
sa.Column(
|
|
"settings",
|
|
postgresql.JSONB(astext_type=sa.Text()),
|
|
server_default="{}",
|
|
nullable=False,
|
|
),
|
|
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,
|
|
),
|
|
sa.Column("deleted_at", sa.DateTime(timezone=True), nullable=True),
|
|
sa.PrimaryKeyConstraint("id"),
|
|
)
|
|
op.create_index("ix_profiles_username", "profiles", ["username"], unique=False)
|
|
op.create_index(
|
|
"ix_profiles_settings_gin",
|
|
"profiles",
|
|
["settings"],
|
|
unique=False,
|
|
postgresql_using="gin",
|
|
)
|
|
op.create_foreign_key(
|
|
"fk_profiles_id",
|
|
"profiles",
|
|
"users",
|
|
["id"],
|
|
["id"],
|
|
referent_schema="auth",
|
|
ondelete="CASCADE",
|
|
)
|
|
_enable_rls("profiles")
|
|
op.execute(
|
|
"""
|
|
CREATE OR REPLACE FUNCTION public.create_profile_for_new_user()
|
|
RETURNS trigger
|
|
LANGUAGE plpgsql
|
|
SECURITY DEFINER
|
|
SET search_path = public
|
|
AS $$
|
|
BEGIN
|
|
INSERT INTO public.profiles (id, username, avatar_url, bio, settings, created_at, updated_at)
|
|
VALUES (
|
|
NEW.id,
|
|
COALESCE(
|
|
NEW.raw_user_meta_data ->> 'username',
|
|
split_part(NEW.email, '@', 1),
|
|
'user_' || substring(NEW.id::text, 1, 8)
|
|
),
|
|
NULL,
|
|
NULL,
|
|
'{}'::jsonb,
|
|
now(),
|
|
now()
|
|
)
|
|
ON CONFLICT (id) DO NOTHING;
|
|
|
|
RETURN NEW;
|
|
END;
|
|
$$;
|
|
"""
|
|
)
|
|
op.execute("DROP TRIGGER IF EXISTS on_auth_user_created ON auth.users")
|
|
op.execute(
|
|
"""
|
|
CREATE TRIGGER on_auth_user_created
|
|
AFTER INSERT ON auth.users
|
|
FOR EACH ROW EXECUTE FUNCTION public.create_profile_for_new_user();
|
|
"""
|
|
)
|
|
|
|
op.create_table(
|
|
"automation_jobs",
|
|
sa.Column("id", sa.UUID(), nullable=False),
|
|
sa.Column("owner_id", sa.UUID(), nullable=False),
|
|
sa.Column("title", sa.String(length=255), nullable=False),
|
|
sa.Column("prompt", sa.Text(), nullable=False),
|
|
sa.Column("schedule_type", sa.String(length=20), nullable=False),
|
|
sa.Column("run_at", sa.DateTime(timezone=True), nullable=False),
|
|
sa.Column("next_run_at", sa.DateTime(timezone=True), nullable=False),
|
|
sa.Column("timezone", sa.String(length=50), nullable=False),
|
|
sa.Column("last_run_at", sa.DateTime(timezone=True), nullable=True),
|
|
sa.Column("status", sa.String(length=20), nullable=False),
|
|
sa.Column("created_by", sa.UUID(), nullable=True),
|
|
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,
|
|
),
|
|
sa.Column("deleted_at", sa.DateTime(timezone=True), nullable=True),
|
|
sa.PrimaryKeyConstraint("id"),
|
|
sa.UniqueConstraint("id", "owner_id", name="uq_automation_jobs_id_owner"),
|
|
)
|
|
op.create_index(
|
|
"ix_automation_jobs_owner_status",
|
|
"automation_jobs",
|
|
["owner_id", "status"],
|
|
unique=False,
|
|
)
|
|
op.create_index(
|
|
"ix_automation_jobs_status_next_run",
|
|
"automation_jobs",
|
|
["status", "next_run_at"],
|
|
unique=False,
|
|
)
|
|
op.execute(
|
|
"ALTER TABLE automation_jobs ADD CONSTRAINT chk_automation_job_schedule_type CHECK (schedule_type IN ('daily', 'weekly'))"
|
|
)
|
|
op.execute(
|
|
"ALTER TABLE automation_jobs ADD CONSTRAINT chk_automation_job_status CHECK (status IN ('active', 'disabled'))"
|
|
)
|
|
op.create_foreign_key(
|
|
"fk_automation_jobs_owner_id",
|
|
"automation_jobs",
|
|
"users",
|
|
["owner_id"],
|
|
["id"],
|
|
referent_schema="auth",
|
|
ondelete="CASCADE",
|
|
)
|
|
op.create_foreign_key(
|
|
"fk_automation_jobs_created_by",
|
|
"automation_jobs",
|
|
"users",
|
|
["created_by"],
|
|
["id"],
|
|
referent_schema="auth",
|
|
ondelete="SET NULL",
|
|
)
|
|
_enable_rls("automation_jobs")
|
|
|
|
op.execute("REVOKE ALL ON TABLE public.alembic_version FROM anon")
|
|
op.execute("REVOKE ALL ON TABLE public.alembic_version FROM authenticated")
|
|
|
|
|
|
def downgrade() -> None:
|
|
_drop_rls("automation_jobs")
|
|
op.drop_constraint(
|
|
"fk_automation_jobs_created_by", "automation_jobs", type_="foreignkey"
|
|
)
|
|
op.drop_constraint(
|
|
"fk_automation_jobs_owner_id", "automation_jobs", type_="foreignkey"
|
|
)
|
|
op.drop_table("automation_jobs")
|
|
|
|
_drop_rls("profiles")
|
|
op.execute("DROP TRIGGER IF EXISTS on_auth_user_created ON auth.users")
|
|
op.execute("DROP FUNCTION IF EXISTS public.create_profile_for_new_user()")
|
|
op.drop_constraint("fk_profiles_id", "profiles", type_="foreignkey")
|
|
op.drop_table("profiles")
|
|
|
|
_drop_rls("llms")
|
|
op.drop_constraint("fk_llms_factory_id", "llms", type_="foreignkey")
|
|
op.drop_table("llms")
|
|
_drop_rls("llm_factory")
|
|
op.drop_table("llm_factory")
|
|
|
|
|
|
def _enable_rls(table_name: str) -> None:
|
|
for role in ["anon", "authenticated"]:
|
|
for action in ["select", "insert", "update", "delete"]:
|
|
op.execute(
|
|
f"DROP POLICY IF EXISTS {role}_{action}_{table_name} ON {table_name}"
|
|
)
|
|
op.execute(f"ALTER TABLE {table_name} ENABLE ROW LEVEL SECURITY")
|
|
for role in ["anon", "authenticated"]:
|
|
op.execute(
|
|
f"CREATE POLICY {role}_select_{table_name} ON {table_name} FOR SELECT TO {role} USING (false)"
|
|
)
|
|
op.execute(
|
|
f"CREATE POLICY {role}_insert_{table_name} ON {table_name} FOR INSERT TO {role} WITH CHECK (false)"
|
|
)
|
|
op.execute(
|
|
f"CREATE POLICY {role}_update_{table_name} ON {table_name} FOR UPDATE TO {role} USING (false) WITH CHECK (false)"
|
|
)
|
|
op.execute(
|
|
f"CREATE POLICY {role}_delete_{table_name} ON {table_name} FOR DELETE TO {role} USING (false)"
|
|
)
|
|
|
|
|
|
def _drop_rls(table_name: str) -> None:
|
|
for role in ["anon", "authenticated"]:
|
|
op.execute(f"DROP POLICY IF EXISTS {role}_delete_{table_name} ON {table_name}")
|
|
op.execute(f"DROP POLICY IF EXISTS {role}_update_{table_name} ON {table_name}")
|
|
op.execute(f"DROP POLICY IF EXISTS {role}_insert_{table_name} ON {table_name}")
|
|
op.execute(f"DROP POLICY IF EXISTS {role}_select_{table_name} ON {table_name}")
|
|
op.execute(f"ALTER TABLE {table_name} DISABLE ROW LEVEL SECURITY")
|