"""initial user schema Revision ID: 001 Revises: Create Date: 2026-01-01 00:00:00.000000 """ from typing import Sequence, Union from alembic import op import sqlalchemy as sa # revision identifiers, used by Alembic. revision: str = '001' down_revision: Union[str, None] = '000' branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: """Create all user data tables.""" # 1. User Sessions Table op.create_table( 'user_sessions', sa.Column('id', sa.BigInteger().with_variant(sa.Integer, "sqlite"), primary_key=True), sa.Column('user_id', sa.Integer(), sa.ForeignKey('mtgonline_users.id', ondelete='CASCADE'), nullable=False), sa.Column('session_token_hash', sa.String(255), unique=True, nullable=False), sa.Column('ip_address', sa.String(45), nullable=True), sa.Column('user_agent', sa.Text(), nullable=True), sa.Column('created_at', sa.DateTime(), server_default=sa.func.now()), sa.Column('expires_at', sa.DateTime(), nullable=False), sa.Column('is_active', sa.Boolean(), default=True), ) op.create_index('idx_sessions_user', 'user_sessions', ['user_id']) op.create_index('idx_sessions_token', 'user_sessions', ['session_token_hash']) op.create_index('idx_sessions_expires', 'user_sessions', ['expires_at']) # 2. Deck Versions Table op.create_table( 'deck_versions', sa.Column('id', sa.BigInteger().with_variant(sa.Integer, "sqlite"), primary_key=True), sa.Column('deck_id', sa.Integer(), sa.ForeignKey('mtgonline_decklist_files.id', ondelete='CASCADE'), nullable=False), sa.Column('version_number', sa.Integer(), nullable=False), sa.Column('content', sa.Text(), nullable=False), sa.Column('status', sa.String(20), server_default='DRAFT'), sa.Column('comment', sa.Text(), nullable=True), sa.Column('created_at', sa.DateTime(), server_default=sa.func.now()), ) op.create_index('idx_deck_versions_deck', 'deck_versions', ['deck_id']) # 4. Game Replays Table op.create_table( 'game_replays', sa.Column('id', sa.BigInteger().with_variant(sa.Integer, "sqlite"), primary_key=True), sa.Column('game_uuid', sa.String(36), unique=True, nullable=False), sa.Column('room_id', sa.Integer(), sa.ForeignKey('mtgonline_rooms.id'), nullable=True), sa.Column('game_type', sa.String(50), nullable=True), sa.Column('format', sa.String(50), nullable=True), sa.Column('duration_seconds', sa.Integer(), nullable=True), sa.Column('start_time', sa.DateTime(), nullable=False), sa.Column('end_time', sa.DateTime(), nullable=True), sa.Column('status', sa.String(20), server_default='IN_PROGRESS'), sa.Column('replay_data', sa.JSON(), nullable=True), sa.Column('created_at', sa.DateTime(), server_default=sa.func.now()), sa.Column('updated_at', sa.DateTime(), server_default=sa.func.now(), onupdate=sa.func.now()), ) op.create_index('idx_replays_room', 'game_replays', ['room_id']) op.create_index('idx_replays_start', 'game_replays', ['start_time']) op.create_index('idx_replays_status', 'game_replays', ['status']) # 5. Replay Players Table op.create_table( 'replay_players', sa.Column('id', sa.BigInteger().with_variant(sa.Integer, "sqlite"), primary_key=True), sa.Column('replay_id', sa.BigInteger(), sa.ForeignKey('game_replays.id', ondelete='CASCADE'), nullable=False), sa.Column('user_id', sa.Integer(), sa.ForeignKey('mtgonline_users.id'), nullable=False), sa.Column('position', sa.Integer(), nullable=True), sa.Column('deck_id', sa.Integer(), sa.ForeignKey('mtgonline_decklist_files.id'), nullable=True), sa.Column('won', sa.Boolean(), nullable=True), sa.Column('lost', sa.Boolean(), nullable=True), sa.Column('concession', sa.Boolean(), default=False), sa.Column('turn_one', sa.Boolean(), default=False), sa.Column('created_at', sa.DateTime(), server_default=sa.func.now()), ) op.create_index('idx_replay_players_replay', 'replay_players', ['replay_id']) op.create_index('idx_replay_players_user', 'replay_players', ['user_id']) # 6. Game Outcomes Table op.create_table( 'game_outcomes', sa.Column('id', sa.BigInteger().with_variant(sa.Integer, "sqlite"), primary_key=True), sa.Column('user_id', sa.Integer(), sa.ForeignKey('mtgonline_users.id'), nullable=False), sa.Column('game_uuid', sa.String(36), sa.ForeignKey('game_replays.game_uuid'), nullable=False), sa.Column('outcome', sa.String(20), nullable=False), sa.Column('opponent_id', sa.Integer(), sa.ForeignKey('mtgonline_users.id'), nullable=True), sa.Column('format', sa.String(50), nullable=True), sa.Column('rating_before', sa.Integer(), nullable=True), sa.Column('rating_after', sa.Integer(), nullable=True), sa.Column('rating_change', sa.Integer(), nullable=True), sa.Column('created_at', sa.DateTime(), server_default=sa.func.now()), ) op.create_index('idx_outcomes_user', 'game_outcomes', ['user_id']) op.create_index('idx_outcomes_game', 'game_outcomes', ['game_uuid']) op.create_index('idx_outcomes_outcome', 'game_outcomes', ['outcome']) # 7. User Statistics Table op.create_table( 'user_statistics', sa.Column('user_id', sa.Integer(), sa.ForeignKey('mtgonline_users.id'), primary_key=True), sa.Column('total_games', sa.Integer(), default=0), sa.Column('total_wins', sa.Integer(), default=0), sa.Column('total_losses', sa.Integer(), default=0), sa.Column('total_concessions', sa.Integer(), default=0), sa.Column('win_rate', sa.Float(), default=0.0), sa.Column('current_streak', sa.Integer(), default=0), sa.Column('best_streak', sa.Integer(), default=0), sa.Column('average_rating', sa.Float(), default=0.0), sa.Column('last_game_date', sa.DateTime(), nullable=True), sa.Column('updated_at', sa.DateTime(), server_default=sa.func.now(), onupdate=sa.func.now()), ) # 8. User Card Collection Table op.create_table( 'user_card_collection', sa.Column('id', sa.BigInteger().with_variant(sa.Integer, "sqlite"), primary_key=True), sa.Column('user_id', sa.Integer(), sa.ForeignKey('mtgonline_users.id', ondelete='CASCADE'), nullable=False), sa.Column('card_id', sa.Integer(), nullable=False), sa.Column('quantity', sa.Integer(), default=1), sa.Column('condition', sa.String(20), server_default='NEAR_MINT'), sa.Column('language', sa.String(5), server_default='EN'), sa.Column('is_foil', sa.Boolean(), default=False), sa.Column('is_alt_art', sa.Boolean(), default=False), sa.Column('acquired_date', sa.DateTime(), server_default=sa.func.now()), sa.Column('acquisition_method', sa.String(50), nullable=True), sa.Column('notes', sa.Text(), nullable=True), sa.Column('created_at', sa.DateTime(), server_default=sa.func.now()), sa.Column('updated_at', sa.DateTime(), server_default=sa.func.now(), onupdate=sa.func.now()), ) op.create_index('idx_collection_user_card', 'user_card_collection', ['user_id', 'card_id']) op.create_unique_constraint('uq_collection_unique', 'user_card_collection', ['user_id', 'card_id', 'is_foil', 'is_alt_art']) # 9. Card Wishlist Table op.create_table( 'card_wishlist', sa.Column('id', sa.BigInteger().with_variant(sa.Integer, "sqlite"), primary_key=True), sa.Column('user_id', sa.Integer(), sa.ForeignKey('mtgonline_users.id', ondelete='CASCADE'), nullable=False), sa.Column('card_id', sa.Integer(), nullable=False), sa.Column('max_price', sa.Float(), nullable=True), sa.Column('notes', sa.Text(), nullable=True), sa.Column('created_at', sa.DateTime(), server_default=sa.func.now()), ) op.create_unique_constraint('uq_wishlist_user_card', 'card_wishlist', ['user_id', 'card_id']) # 10. User Groups Table op.create_table( 'user_groups', sa.Column('id', sa.BigInteger().with_variant(sa.Integer, "sqlite"), primary_key=True), sa.Column('name', sa.String(100), nullable=False), sa.Column('description', sa.Text(), nullable=True), sa.Column('owner_id', sa.Integer(), sa.ForeignKey('mtgonline_users.id'), nullable=False), sa.Column('is_public', sa.Boolean(), default=True), sa.Column('max_members', sa.Integer(), default=50), sa.Column('created_at', sa.DateTime(), server_default=sa.func.now()), sa.Column('updated_at', sa.DateTime(), server_default=sa.func.now(), onupdate=sa.func.now()), ) op.create_index('idx_groups_owner', 'user_groups', ['owner_id']) # 11. Group Members Table op.create_table( 'group_members', sa.Column('id', sa.BigInteger().with_variant(sa.Integer, "sqlite"), primary_key=True), sa.Column('group_id', sa.BigInteger(), sa.ForeignKey('user_groups.id', ondelete='CASCADE'), nullable=False), sa.Column('user_id', sa.Integer(), sa.ForeignKey('mtgonline_users.id'), nullable=False), sa.Column('role', sa.String(20), server_default='MEMBER'), sa.Column('joined_at', sa.DateTime(), server_default=sa.func.now()), ) op.create_index('idx_members_group', 'group_members', ['group_id']) op.create_index('idx_members_user', 'group_members', ['user_id']) op.create_unique_constraint('uq_group_member', 'group_members', ['group_id', 'user_id']) # 12. Group Chat Messages Table op.create_table( 'group_chat_messages', sa.Column('id', sa.BigInteger().with_variant(sa.Integer, "sqlite"), primary_key=True), sa.Column('group_id', sa.BigInteger(), sa.ForeignKey('user_groups.id', ondelete='CASCADE'), nullable=False), sa.Column('sender_id', sa.Integer(), sa.ForeignKey('mtgonline_users.id'), nullable=False), sa.Column('message', sa.Text(), nullable=False), sa.Column('created_at', sa.DateTime(), server_default=sa.func.now()), ) op.create_index('idx_group_messages_group', 'group_chat_messages', ['group_id']) op.create_index('idx_group_messages_sender', 'group_chat_messages', ['sender_id']) op.create_index('idx_group_messages_created', 'group_chat_messages', ['created_at']) # 13. User Networks Table op.create_table( 'user_networks', sa.Column('id', sa.BigInteger().with_variant(sa.Integer, "sqlite"), primary_key=True), sa.Column('name', sa.String(100), nullable=False), sa.Column('description', sa.Text(), nullable=True), sa.Column('creator_id', sa.Integer(), sa.ForeignKey('mtgonline_users.id'), nullable=False), sa.Column('is_public', sa.Boolean(), default=True), sa.Column('created_at', sa.DateTime(), server_default=sa.func.now()), ) # 14. Network Members Table op.create_table( 'network_members', sa.Column('id', sa.BigInteger().with_variant(sa.Integer, "sqlite"), primary_key=True), sa.Column('network_id', sa.BigInteger(), sa.ForeignKey('user_networks.id', ondelete='CASCADE'), nullable=False), sa.Column('user_id', sa.Integer(), sa.ForeignKey('mtgonline_users.id'), nullable=False), sa.Column('role', sa.String(20), server_default='MEMBER'), sa.Column('joined_at', sa.DateTime(), server_default=sa.func.now()), ) op.create_index('idx_network_members_network', 'network_members', ['network_id']) op.create_index('idx_network_members_user', 'network_members', ['user_id']) op.create_unique_constraint('uq_network_member', 'network_members', ['network_id', 'user_id']) # 15. User Preferences Table op.create_table( 'user_preferences', sa.Column('user_id', sa.Integer(), sa.ForeignKey('mtgonline_users.id'), primary_key=True), sa.Column('theme', sa.String(20), server_default='light'), sa.Column('notifications_enabled', sa.Boolean(), default=True), sa.Column('email_notifications', sa.Boolean(), default=True), sa.Column('auto_save_decks', sa.Boolean(), default=True), sa.Column('default_format', sa.String(50), server_default='standard'), sa.Column('language', sa.String(5), server_default='EN'), sa.Column('updated_at', sa.DateTime(), server_default=sa.func.now(), onupdate=sa.func.now()), ) # 16. User Activity Log Table op.create_table( 'user_activity_log', sa.Column('id', sa.BigInteger().with_variant(sa.Integer, "sqlite"), primary_key=True), sa.Column('user_id', sa.Integer(), sa.ForeignKey('mtgonline_users.id'), nullable=False), sa.Column('activity_type', sa.String(50), nullable=False), sa.Column('activity_data', sa.JSON(), nullable=True), sa.Column('ip_address', sa.String(45), nullable=True), sa.Column('created_at', sa.DateTime(), server_default=sa.func.now()), ) op.create_index('idx_activity_user', 'user_activity_log', ['user_id']) op.create_index('idx_activity_type', 'user_activity_log', ['activity_type']) op.create_index('idx_activity_created', 'user_activity_log', ['created_at']) def downgrade() -> None: """Drop all user data tables.""" op.drop_table('user_activity_log') op.drop_table('user_preferences') op.drop_table('network_members') op.drop_table('user_networks') op.drop_table('group_chat_messages') op.drop_table('group_members') op.drop_table('user_groups') op.drop_table('card_wishlist') op.drop_table('user_card_collection') op.drop_table('user_statistics') op.drop_table('game_outcomes') op.drop_table('replay_players') op.drop_table('game_replays') op.drop_table('deck_versions') op.drop_table('user_sessions')