feat: split initial social schema migration chain
replace monolithic migration with ordered scripts, include profiles/sessions in migration, and verify full downgrade/upgrade cycle for clean Supabase bootstrap
This commit is contained in:
@@ -0,0 +1,285 @@
|
||||
"""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")
|
||||
Reference in New Issue
Block a user