Files
mtgonline/backend/alembic/versions/001_initial_user_schema.py
T

266 lines
14 KiB
Python

"""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', 'user_card_collection', ['user_id'])
op.create_index('idx_collection_card', 'user_card_collection', ['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')