"""add_rbac_enhancements Revision ID: fa7d4d10e358 Revises: 4951b2e50581 Create Date: 2025-11-13 16:51:25.010057 SQLite-compatible version """ from collections.abc import Sequence import sqlalchemy as sa from alembic import op # revision identifiers, used by Alembic. revision: str = "fa7d4d10e358" down_revision: str | None = "4951b2e50581" branch_labels: str | Sequence[str] | None = None depends_on: str | Sequence[str] | None = None def upgrade(): """Upgrade database schema for RBAC enhancements. SQLite-compatible version using batch operations for table modifications. """ # ======================================================================== # User table changes # ======================================================================== with op.batch_alter_table("users", schema=None) as batch_op: batch_op.add_column( sa.Column( "is_email_verified", sa.Boolean(), nullable=False, server_default="false", ) ) # Set existing active users as verified op.execute("UPDATE users SET is_email_verified = TRUE WHERE is_active = TRUE") # ======================================================================== # VendorUser table changes (requires table recreation for SQLite) # ======================================================================== with op.batch_alter_table("vendor_users", schema=None) as batch_op: # Add new columns batch_op.add_column( sa.Column( "user_type", sa.String(length=20), nullable=False, server_default="member", ) ) batch_op.add_column( sa.Column("invitation_token", sa.String(length=100), nullable=True) ) batch_op.add_column( sa.Column("invitation_sent_at", sa.DateTime(), nullable=True) ) batch_op.add_column( sa.Column("invitation_accepted_at", sa.DateTime(), nullable=True) ) # Create index on invitation_token batch_op.create_index("idx_vendor_users_invitation_token", ["invitation_token"]) # Modify role_id to be nullable (this recreates the table in SQLite) batch_op.alter_column("role_id", existing_type=sa.Integer(), nullable=True) # Change is_active default (this recreates the table in SQLite) batch_op.alter_column( "is_active", existing_type=sa.Boolean(), server_default="false" ) # Set owners correctly (after table modifications) # SQLite-compatible UPDATE with subquery op.execute( """ UPDATE vendor_users SET user_type = 'owner' WHERE (vendor_id, user_id) IN ( SELECT id, owner_user_id FROM vendors ) """ ) # Set existing owners as active op.execute( """ UPDATE vendor_users SET is_active = TRUE WHERE user_type = 'owner' """ ) # ======================================================================== # Role table changes # ======================================================================== with op.batch_alter_table("roles", schema=None) as batch_op: # Create index on vendor_id and name batch_op.create_index("idx_roles_vendor_name", ["vendor_id", "name"]) # Note: JSONB conversion only for PostgreSQL # SQLite stores JSON as TEXT by default, no conversion needed def downgrade(): """Downgrade database schema. SQLite-compatible version using batch operations. """ # ======================================================================== # Role table changes # ======================================================================== with op.batch_alter_table("roles", schema=None) as batch_op: batch_op.drop_index("idx_roles_vendor_name") # ======================================================================== # VendorUser table changes # ======================================================================== with op.batch_alter_table("vendor_users", schema=None) as batch_op: # Revert is_active default batch_op.alter_column( "is_active", existing_type=sa.Boolean(), server_default="true" ) # Revert role_id to NOT NULL # Note: This might fail if there are NULL values batch_op.alter_column("role_id", existing_type=sa.Integer(), nullable=False) # Drop indexes and columns batch_op.drop_index("idx_vendor_users_invitation_token") batch_op.drop_column("invitation_accepted_at") batch_op.drop_column("invitation_sent_at") batch_op.drop_column("invitation_token") batch_op.drop_column("user_type") # ======================================================================== # User table changes # ======================================================================== with op.batch_alter_table("users", schema=None) as batch_op: batch_op.drop_column("is_email_verified")