"""add roles and user_roles tables Revision ID: add_roles_tables Revises: add_article_views Create Date: 2025-11-21 """ from typing import Tuple import sqlalchemy as sa from alembic import op from sqlalchemy import func, text from sqlalchemy.dialects.postgresql import JSONB revision = "add_roles_tables" down_revision = "add_article_views" branch_labels = None depends_on = None def timestamps() -> Tuple[sa.Column, sa.Column]: return ( sa.Column( "created_at", sa.TIMESTAMP(timezone=True), nullable=False, server_default=func.now(), ), sa.Column( "updated_at", sa.TIMESTAMP(timezone=True), nullable=False, server_default=func.now(), onupdate=func.current_timestamp(), ), ) def upgrade() -> None: op.create_table( "roles", sa.Column("id", sa.Integer, primary_key=True), sa.Column("name", sa.String(length=64), nullable=False, unique=True), sa.Column("description", sa.Text, nullable=False, server_default=""), sa.Column( "permissions", JSONB, nullable=False, server_default=text("'[]'::jsonb"), ), *timestamps(), ) op.execute( """ CREATE TRIGGER update_role_modtime BEFORE UPDATE ON roles FOR EACH ROW EXECUTE PROCEDURE update_updated_at_column(); """, ) op.create_table( "user_roles", sa.Column( "user_id", sa.Integer, sa.ForeignKey("users.id", ondelete="CASCADE"), nullable=False, ), sa.Column( "role_id", sa.Integer, sa.ForeignKey("roles.id", ondelete="CASCADE"), nullable=False, ), sa.Column( "assigned_at", sa.TIMESTAMP(timezone=True), nullable=False, server_default=func.now(), ), ) op.create_primary_key( "pk_user_roles", "user_roles", ["user_id", "role_id"], ) op.create_index("ix_user_roles_role_id", "user_roles", ["role_id"]) op.execute( """ INSERT INTO roles (name, description, permissions) VALUES ('admin', 'System administrator with full privileges', '["*"]') ON CONFLICT (name) DO NOTHING; """, ) op.execute( """ DO $$ DECLARE admin_role_id INTEGER; first_user_id INTEGER; BEGIN SELECT id INTO admin_role_id FROM roles WHERE name = 'admin'; SELECT id INTO first_user_id FROM users ORDER BY id ASC LIMIT 1; IF admin_role_id IS NOT NULL AND first_user_id IS NOT NULL THEN INSERT INTO user_roles (user_id, role_id) VALUES (first_user_id, admin_role_id) ON CONFLICT DO NOTHING; END IF; END $$; """, ) def downgrade() -> None: op.drop_index("ix_user_roles_role_id", table_name="user_roles") op.drop_table("user_roles") op.drop_table("roles")