diff --git a/migrations/versions/002_add_user_full_name.py b/migrations/versions/002_add_user_full_name.py index cfe4902..a94c7bf 100644 --- a/migrations/versions/002_add_user_full_name.py +++ b/migrations/versions/002_add_user_full_name.py @@ -16,10 +16,57 @@ depends_on = None def upgrade(): - op.add_column('users', sa.Column('full_name', sa.String(length=200), nullable=True)) + """Add full_name column to users table""" + from sqlalchemy import inspect + bind = op.get_bind() + inspector = inspect(bind) + + existing_tables = inspector.get_table_names() + if 'users' not in existing_tables: + return + + users_columns = {c['name'] for c in inspector.get_columns('users')} + + if 'full_name' in users_columns: + print("✓ Column full_name already exists in users table") + return + + try: + op.add_column('users', sa.Column('full_name', sa.String(length=200), nullable=True)) + print("✓ Added full_name column to users table") + except Exception as e: + error_msg = str(e) + if 'already exists' in error_msg.lower() or 'duplicate' in error_msg.lower(): + print("✓ Column full_name already exists in users table (detected via error)") + else: + print(f"✗ Error adding full_name column: {e}") + raise def downgrade(): - op.drop_column('users', 'full_name') + """Remove full_name column from users table""" + from sqlalchemy import inspect + bind = op.get_bind() + inspector = inspect(bind) + + existing_tables = inspector.get_table_names() + if 'users' not in existing_tables: + return + + users_columns = {c['name'] for c in inspector.get_columns('users')} + + if 'full_name' not in users_columns: + print("⊘ Column full_name does not exist in users table, skipping") + return + + try: + op.drop_column('users', 'full_name') + print("✓ Dropped full_name column from users table") + except Exception as e: + error_msg = str(e) + if 'does not exist' in error_msg.lower() or 'no such column' in error_msg.lower(): + print("⊘ Column full_name does not exist in users table (detected via error)") + else: + print(f"⚠ Warning: Could not drop full_name column: {e}") diff --git a/migrations/versions/004_add_task_activities_table.py b/migrations/versions/004_add_task_activities_table.py index f60207f..154a943 100644 --- a/migrations/versions/004_add_task_activities_table.py +++ b/migrations/versions/004_add_task_activities_table.py @@ -17,28 +17,88 @@ depends_on = None def upgrade() -> None: - op.create_table( - 'task_activities', - sa.Column('id', sa.Integer(), primary_key=True), - sa.Column('task_id', sa.Integer(), sa.ForeignKey('tasks.id', ondelete='CASCADE'), nullable=False, index=True), - sa.Column('user_id', sa.Integer(), sa.ForeignKey('users.id', ondelete='SET NULL'), nullable=True, index=True), - sa.Column('event', sa.String(length=50), nullable=False, index=True), - sa.Column('details', sa.Text(), nullable=True), - sa.Column('created_at', sa.DateTime(), nullable=False, server_default=sa.text('CURRENT_TIMESTAMP')), - ) + """Create task_activities table""" + from sqlalchemy import inspect + bind = op.get_bind() + inspector = inspect(bind) + + existing_tables = inspector.get_table_names() + + if 'task_activities' in existing_tables: + print("✓ Table task_activities already exists") + # Ensure indexes exist + try: + existing_indexes = [idx['name'] for idx in inspector.get_indexes('task_activities')] + indexes_to_create = [ + ('idx_task_activities_task_id', ['task_id']), + ('idx_task_activities_user_id', ['user_id']), + ('idx_task_activities_event', ['event']), + ('idx_task_activities_created_at', ['created_at']), + ] + for idx_name, cols in indexes_to_create: + if idx_name not in existing_indexes: + try: + op.create_index(idx_name, 'task_activities', cols) + except Exception: + pass + except Exception: + pass + return + + try: + op.create_table( + 'task_activities', + sa.Column('id', sa.Integer(), primary_key=True), + sa.Column('task_id', sa.Integer(), sa.ForeignKey('tasks.id', ondelete='CASCADE'), nullable=False, index=True), + sa.Column('user_id', sa.Integer(), sa.ForeignKey('users.id', ondelete='SET NULL'), nullable=True, index=True), + sa.Column('event', sa.String(length=50), nullable=False, index=True), + sa.Column('details', sa.Text(), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=False, server_default=sa.text('CURRENT_TIMESTAMP')), + ) - # Explicit indexes (in addition to inline index=True for portability) - op.create_index('idx_task_activities_task_id', 'task_activities', ['task_id']) - op.create_index('idx_task_activities_user_id', 'task_activities', ['user_id']) - op.create_index('idx_task_activities_event', 'task_activities', ['event']) - op.create_index('idx_task_activities_created_at', 'task_activities', ['created_at']) + # Explicit indexes (in addition to inline index=True for portability) + op.create_index('idx_task_activities_task_id', 'task_activities', ['task_id']) + op.create_index('idx_task_activities_user_id', 'task_activities', ['user_id']) + op.create_index('idx_task_activities_event', 'task_activities', ['event']) + op.create_index('idx_task_activities_created_at', 'task_activities', ['created_at']) + print("✓ Created task_activities table") + except Exception as e: + error_msg = str(e) + if 'already exists' in error_msg.lower() or 'duplicate' in error_msg.lower(): + print("✓ Table task_activities already exists (detected via error)") + else: + print(f"✗ Error creating task_activities table: {e}") + raise def downgrade() -> None: - op.drop_index('idx_task_activities_created_at', table_name='task_activities') - op.drop_index('idx_task_activities_event', table_name='task_activities') - op.drop_index('idx_task_activities_user_id', table_name='task_activities') - op.drop_index('idx_task_activities_task_id', table_name='task_activities') - op.drop_table('task_activities') + """Drop task_activities table""" + from sqlalchemy import inspect + bind = op.get_bind() + inspector = inspect(bind) + + existing_tables = inspector.get_table_names() + + if 'task_activities' not in existing_tables: + print("⊘ Table task_activities does not exist, skipping") + return + + try: + existing_indexes = [idx['name'] for idx in inspector.get_indexes('task_activities')] + for idx_name in ['idx_task_activities_created_at', 'idx_task_activities_event', + 'idx_task_activities_user_id', 'idx_task_activities_task_id']: + if idx_name in existing_indexes: + try: + op.drop_index(idx_name, table_name='task_activities') + except Exception: + pass + op.drop_table('task_activities') + print("✓ Dropped task_activities table") + except Exception as e: + error_msg = str(e) + if 'does not exist' in error_msg.lower() or 'no such table' in error_msg.lower(): + print("⊘ Table task_activities does not exist (detected via error)") + else: + print(f"⚠ Warning: Could not drop task_activities table: {e}") diff --git a/migrations/versions/013_add_comments_table.py b/migrations/versions/013_add_comments_table.py index d8e226d..74a248c 100644 --- a/migrations/versions/013_add_comments_table.py +++ b/migrations/versions/013_add_comments_table.py @@ -38,12 +38,32 @@ def upgrade() -> None: sa.PrimaryKeyConstraint('id') ) - # Create indexes for better performance - op.create_index('ix_comments_project_id', 'comments', ['project_id'], unique=False) - op.create_index('ix_comments_task_id', 'comments', ['task_id'], unique=False) - op.create_index('ix_comments_user_id', 'comments', ['user_id'], unique=False) - op.create_index('ix_comments_parent_id', 'comments', ['parent_id'], unique=False) - op.create_index('ix_comments_created_at', 'comments', ['created_at'], unique=False) + # Create indexes for better performance (idempotent) + try: + existing_indexes = [idx['name'] for idx in inspector.get_indexes('comments')] + indexes_to_create = [ + ('ix_comments_project_id', ['project_id']), + ('ix_comments_task_id', ['task_id']), + ('ix_comments_user_id', ['user_id']), + ('ix_comments_parent_id', ['parent_id']), + ('ix_comments_created_at', ['created_at']), + ] + for idx_name, cols in indexes_to_create: + if idx_name not in existing_indexes: + try: + op.create_index(idx_name, 'comments', cols, unique=False) + except Exception: + pass # Index might already exist + except Exception: + # If we can't check indexes, try to create them anyway (best effort) + try: + op.create_index('ix_comments_project_id', 'comments', ['project_id'], unique=False) + op.create_index('ix_comments_task_id', 'comments', ['task_id'], unique=False) + op.create_index('ix_comments_user_id', 'comments', ['user_id'], unique=False) + op.create_index('ix_comments_parent_id', 'comments', ['parent_id'], unique=False) + op.create_index('ix_comments_created_at', 'comments', ['created_at'], unique=False) + except Exception: + pass # Indexes might already exist def downgrade() -> None: diff --git a/migrations/versions/021_add_extra_goods_table.py b/migrations/versions/021_add_extra_goods_table.py index e1ad6e9..a90e2bd 100644 --- a/migrations/versions/021_add_extra_goods_table.py +++ b/migrations/versions/021_add_extra_goods_table.py @@ -107,16 +107,33 @@ def upgrade() -> None: print(f"[Migration 021] ✗ Error creating table: {e}") raise - # Create indexes + # Create indexes (idempotent) print("[Migration 021] Creating indexes...") try: - op.create_index('ix_extra_goods_project_id', 'extra_goods', ['project_id']) - op.create_index('ix_extra_goods_invoice_id', 'extra_goods', ['invoice_id']) - op.create_index('ix_extra_goods_created_by', 'extra_goods', ['created_by']) - print("[Migration 021] ✓ Indexes created") + existing_indexes = [idx['name'] for idx in inspector.get_indexes('extra_goods')] + indexes_to_create = [ + ('ix_extra_goods_project_id', ['project_id']), + ('ix_extra_goods_invoice_id', ['invoice_id']), + ('ix_extra_goods_created_by', ['created_by']), + ] + for idx_name, cols in indexes_to_create: + if idx_name not in existing_indexes: + try: + op.create_index(idx_name, 'extra_goods', cols) + except Exception as e: + error_msg = str(e) + if 'already exists' not in error_msg.lower() and 'duplicate' not in error_msg.lower(): + print(f"[Migration 021] ⚠ Warning creating index {idx_name}: {e}") + print("[Migration 021] ✓ Indexes created/verified") except Exception as e: - print(f"[Migration 021] ✗ Error creating indexes: {e}") - raise + print(f"[Migration 021] ⚠ Warning checking/creating indexes: {e}") + # Try to create indexes anyway (best effort) + try: + op.create_index('ix_extra_goods_project_id', 'extra_goods', ['project_id']) + op.create_index('ix_extra_goods_invoice_id', 'extra_goods', ['invoice_id']) + op.create_index('ix_extra_goods_created_by', 'extra_goods', ['created_by']) + except Exception: + pass # Indexes might already exist print("[Migration 021] ✓ Migration completed successfully") else: diff --git a/migrations/versions/031_add_standard_hours_per_day.py b/migrations/versions/031_add_standard_hours_per_day.py index d1d8580..cd10be4 100644 --- a/migrations/versions/031_add_standard_hours_per_day.py +++ b/migrations/versions/031_add_standard_hours_per_day.py @@ -17,12 +17,57 @@ depends_on = None def upgrade(): """Add standard_hours_per_day column to users table""" - op.add_column('users', - sa.Column('standard_hours_per_day', sa.Float(), nullable=False, server_default='8.0') - ) + from sqlalchemy import inspect + bind = op.get_bind() + inspector = inspect(bind) + + existing_tables = inspector.get_table_names() + if 'users' not in existing_tables: + return + + users_columns = {c['name'] for c in inspector.get_columns('users')} + + if 'standard_hours_per_day' in users_columns: + print("✓ Column standard_hours_per_day already exists in users table") + return + + try: + op.add_column('users', + sa.Column('standard_hours_per_day', sa.Float(), nullable=False, server_default='8.0') + ) + print("✓ Added standard_hours_per_day column to users table") + except Exception as e: + error_msg = str(e) + if 'already exists' in error_msg.lower() or 'duplicate' in error_msg.lower(): + print("✓ Column standard_hours_per_day already exists in users table (detected via error)") + else: + print(f"✗ Error adding standard_hours_per_day column: {e}") + raise def downgrade(): """Remove standard_hours_per_day column from users table""" - op.drop_column('users', 'standard_hours_per_day') + from sqlalchemy import inspect + bind = op.get_bind() + inspector = inspect(bind) + + existing_tables = inspector.get_table_names() + if 'users' not in existing_tables: + return + + users_columns = {c['name'] for c in inspector.get_columns('users')} + + if 'standard_hours_per_day' not in users_columns: + print("⊘ Column standard_hours_per_day does not exist in users table, skipping") + return + + try: + op.drop_column('users', 'standard_hours_per_day') + print("✓ Dropped standard_hours_per_day column from users table") + except Exception as e: + error_msg = str(e) + if 'does not exist' in error_msg.lower() or 'no such column' in error_msg.lower(): + print("⊘ Column standard_hours_per_day does not exist in users table (detected via error)") + else: + print(f"⚠ Warning: Could not drop standard_hours_per_day column: {e}") diff --git a/migrations/versions/034_add_calendar_events_table.py b/migrations/versions/034_add_calendar_events_table.py index b004c8e..8e83faf 100644 --- a/migrations/versions/034_add_calendar_events_table.py +++ b/migrations/versions/034_add_calendar_events_table.py @@ -18,50 +18,119 @@ depends_on = None def upgrade(): """Create calendar_events table""" - op.create_table( - 'calendar_events', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('user_id', sa.Integer(), nullable=False), - sa.Column('title', sa.String(length=200), nullable=False), - sa.Column('description', sa.Text(), nullable=True), - sa.Column('start_time', sa.DateTime(), nullable=False), - sa.Column('end_time', sa.DateTime(), nullable=False), - sa.Column('all_day', sa.Boolean(), nullable=False, server_default='0'), - sa.Column('location', sa.String(length=200), nullable=True), - sa.Column('event_type', sa.String(length=50), nullable=False, server_default='event'), - sa.Column('project_id', sa.Integer(), nullable=True), - sa.Column('task_id', sa.Integer(), nullable=True), - sa.Column('client_id', sa.Integer(), nullable=True), - sa.Column('is_recurring', sa.Boolean(), nullable=False, server_default='0'), - sa.Column('recurrence_rule', sa.String(length=200), nullable=True), - sa.Column('recurrence_end_date', sa.DateTime(), nullable=True), - sa.Column('parent_event_id', sa.Integer(), nullable=True), - sa.Column('reminder_minutes', sa.Integer(), nullable=True), - sa.Column('color', sa.String(length=7), nullable=True), - sa.Column('is_private', sa.Boolean(), nullable=False, server_default='0'), - sa.Column('created_at', sa.DateTime(), nullable=False), - sa.Column('updated_at', sa.DateTime(), nullable=False), - sa.ForeignKeyConstraint(['user_id'], ['users.id'], name='fk_calendar_events_user_id'), - sa.ForeignKeyConstraint(['project_id'], ['projects.id'], name='fk_calendar_events_project_id'), - sa.ForeignKeyConstraint(['task_id'], ['tasks.id'], name='fk_calendar_events_task_id'), - sa.ForeignKeyConstraint(['client_id'], ['clients.id'], name='fk_calendar_events_client_id'), - sa.ForeignKeyConstraint(['parent_event_id'], ['calendar_events.id'], name='fk_calendar_events_parent_event_id'), - sa.PrimaryKeyConstraint('id') - ) + from sqlalchemy import inspect + bind = op.get_bind() + inspector = inspect(bind) - # Create indexes for better query performance - with op.batch_alter_table('calendar_events', schema=None) as batch_op: - batch_op.create_index('ix_calendar_events_user_id', ['user_id']) - batch_op.create_index('ix_calendar_events_start_time', ['start_time']) - batch_op.create_index('ix_calendar_events_end_time', ['end_time']) - batch_op.create_index('ix_calendar_events_event_type', ['event_type']) - batch_op.create_index('ix_calendar_events_project_id', ['project_id']) - batch_op.create_index('ix_calendar_events_task_id', ['task_id']) - batch_op.create_index('ix_calendar_events_client_id', ['client_id']) - batch_op.create_index('ix_calendar_events_parent_event_id', ['parent_event_id']) + existing_tables = inspector.get_table_names() + + if 'calendar_events' in existing_tables: + print("✓ Table calendar_events already exists") + # Ensure indexes exist + try: + existing_indexes = [idx['name'] for idx in inspector.get_indexes('calendar_events')] + indexes_to_create = [ + ('ix_calendar_events_user_id', ['user_id']), + ('ix_calendar_events_start_time', ['start_time']), + ('ix_calendar_events_end_time', ['end_time']), + ('ix_calendar_events_event_type', ['event_type']), + ('ix_calendar_events_project_id', ['project_id']), + ('ix_calendar_events_task_id', ['task_id']), + ('ix_calendar_events_client_id', ['client_id']), + ('ix_calendar_events_parent_event_id', ['parent_event_id']), + ] + for idx_name, cols in indexes_to_create: + if idx_name not in existing_indexes: + try: + op.create_index(idx_name, 'calendar_events', cols, unique=False) + except Exception: + pass + except Exception: + pass + return + + try: + op.create_table( + 'calendar_events', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('title', sa.String(length=200), nullable=False), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('start_time', sa.DateTime(), nullable=False), + sa.Column('end_time', sa.DateTime(), nullable=False), + sa.Column('all_day', sa.Boolean(), nullable=False, server_default='0'), + sa.Column('location', sa.String(length=200), nullable=True), + sa.Column('event_type', sa.String(length=50), nullable=False, server_default='event'), + sa.Column('project_id', sa.Integer(), nullable=True), + sa.Column('task_id', sa.Integer(), nullable=True), + sa.Column('client_id', sa.Integer(), nullable=True), + sa.Column('is_recurring', sa.Boolean(), nullable=False, server_default='0'), + sa.Column('recurrence_rule', sa.String(length=200), nullable=True), + sa.Column('recurrence_end_date', sa.DateTime(), nullable=True), + sa.Column('parent_event_id', sa.Integer(), nullable=True), + sa.Column('reminder_minutes', sa.Integer(), nullable=True), + sa.Column('color', sa.String(length=7), nullable=True), + sa.Column('is_private', sa.Boolean(), nullable=False, server_default='0'), + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.Column('updated_at', sa.DateTime(), nullable=False), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], name='fk_calendar_events_user_id'), + sa.ForeignKeyConstraint(['project_id'], ['projects.id'], name='fk_calendar_events_project_id'), + sa.ForeignKeyConstraint(['task_id'], ['tasks.id'], name='fk_calendar_events_task_id'), + sa.ForeignKeyConstraint(['client_id'], ['clients.id'], name='fk_calendar_events_client_id'), + sa.ForeignKeyConstraint(['parent_event_id'], ['calendar_events.id'], name='fk_calendar_events_parent_event_id'), + sa.PrimaryKeyConstraint('id') + ) + + # Create indexes for better query performance + with op.batch_alter_table('calendar_events', schema=None) as batch_op: + batch_op.create_index('ix_calendar_events_user_id', ['user_id']) + batch_op.create_index('ix_calendar_events_start_time', ['start_time']) + batch_op.create_index('ix_calendar_events_end_time', ['end_time']) + batch_op.create_index('ix_calendar_events_event_type', ['event_type']) + batch_op.create_index('ix_calendar_events_project_id', ['project_id']) + batch_op.create_index('ix_calendar_events_task_id', ['task_id']) + batch_op.create_index('ix_calendar_events_client_id', ['client_id']) + batch_op.create_index('ix_calendar_events_parent_event_id', ['parent_event_id']) + print("✓ Created calendar_events table") + except Exception as e: + error_msg = str(e) + if 'already exists' in error_msg.lower() or 'duplicate' in error_msg.lower(): + print("✓ Table calendar_events already exists (detected via error)") + else: + print(f"✗ Error creating calendar_events table: {e}") + raise def downgrade(): """Drop calendar_events table""" - op.drop_table('calendar_events') + from sqlalchemy import inspect + bind = op.get_bind() + inspector = inspect(bind) + + existing_tables = inspector.get_table_names() + + if 'calendar_events' not in existing_tables: + print("⊘ Table calendar_events does not exist, skipping") + return + + try: + # Drop indexes first + try: + existing_indexes = [idx['name'] for idx in inspector.get_indexes('calendar_events')] + for idx_name in existing_indexes: + try: + op.drop_index(idx_name, table_name='calendar_events') + except Exception: + pass + except Exception: + pass + + op.drop_table('calendar_events') + print("✓ Dropped calendar_events table") + except Exception as e: + error_msg = str(e) + if 'does not exist' in error_msg.lower() or 'no such table' in error_msg.lower(): + print("⊘ Table calendar_events does not exist (detected via error)") + else: + print(f"⚠ Warning: Could not drop calendar_events table: {e}") diff --git a/migrations/versions/044_add_audit_logs_table.py b/migrations/versions/044_add_audit_logs_table.py index c6dc372..c6f4db1 100644 --- a/migrations/versions/044_add_audit_logs_table.py +++ b/migrations/versions/044_add_audit_logs_table.py @@ -19,47 +19,112 @@ depends_on = None def upgrade(): """Create audit_logs table for comprehensive change tracking""" + from sqlalchemy import inspect + bind = op.get_bind() + inspector = inspect(bind) - op.create_table('audit_logs', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('user_id', sa.Integer(), nullable=True), - sa.Column('entity_type', sa.String(length=50), nullable=False), - sa.Column('entity_id', sa.Integer(), nullable=False), - sa.Column('entity_name', sa.String(length=500), nullable=True), - sa.Column('action', sa.String(length=20), nullable=False), - sa.Column('field_name', sa.String(length=100), nullable=True), - sa.Column('old_value', sa.Text(), nullable=True), - sa.Column('new_value', sa.Text(), nullable=True), - sa.Column('change_description', sa.Text(), nullable=True), - sa.Column('ip_address', sa.String(length=45), nullable=True), - sa.Column('user_agent', sa.Text(), nullable=True), - sa.Column('request_path', sa.String(length=500), nullable=True), - sa.Column('created_at', sa.DateTime(), nullable=False), - sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='SET NULL'), - sa.PrimaryKeyConstraint('id') - ) + existing_tables = inspector.get_table_names() - # Create indexes for common queries - op.create_index('ix_audit_logs_entity', 'audit_logs', ['entity_type', 'entity_id']) - op.create_index('ix_audit_logs_user_created', 'audit_logs', ['user_id', 'created_at']) - op.create_index('ix_audit_logs_created_at', 'audit_logs', ['created_at']) - op.create_index('ix_audit_logs_action', 'audit_logs', ['action']) - op.create_index('ix_audit_logs_entity_type', 'audit_logs', ['entity_type']) - op.create_index('ix_audit_logs_entity_id', 'audit_logs', ['entity_id']) - op.create_index('ix_audit_logs_user_id', 'audit_logs', ['user_id']) - op.create_index('ix_audit_logs_field_name', 'audit_logs', ['field_name']) + if 'audit_logs' in existing_tables: + print("✓ Table audit_logs already exists") + # Ensure indexes exist + try: + existing_indexes = [idx['name'] for idx in inspector.get_indexes('audit_logs')] + indexes_to_create = [ + ('ix_audit_logs_entity', ['entity_type', 'entity_id']), + ('ix_audit_logs_user_created', ['user_id', 'created_at']), + ('ix_audit_logs_created_at', ['created_at']), + ('ix_audit_logs_action', ['action']), + ('ix_audit_logs_entity_type', ['entity_type']), + ('ix_audit_logs_entity_id', ['entity_id']), + ('ix_audit_logs_user_id', ['user_id']), + ('ix_audit_logs_field_name', ['field_name']), + ] + for idx_name, cols in indexes_to_create: + if idx_name not in existing_indexes: + try: + op.create_index(idx_name, 'audit_logs', cols, unique=False) + except Exception: + pass + except Exception: + pass + return + + try: + op.create_table('audit_logs', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=True), + sa.Column('entity_type', sa.String(length=50), nullable=False), + sa.Column('entity_id', sa.Integer(), nullable=False), + sa.Column('entity_name', sa.String(length=500), nullable=True), + sa.Column('action', sa.String(length=20), nullable=False), + sa.Column('field_name', sa.String(length=100), nullable=True), + sa.Column('old_value', sa.Text(), nullable=True), + sa.Column('new_value', sa.Text(), nullable=True), + sa.Column('change_description', sa.Text(), nullable=True), + sa.Column('ip_address', sa.String(length=45), nullable=True), + sa.Column('user_agent', sa.Text(), nullable=True), + sa.Column('request_path', sa.String(length=500), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='SET NULL'), + sa.PrimaryKeyConstraint('id') + ) + + # Create indexes for common queries + op.create_index('ix_audit_logs_entity', 'audit_logs', ['entity_type', 'entity_id']) + op.create_index('ix_audit_logs_user_created', 'audit_logs', ['user_id', 'created_at']) + op.create_index('ix_audit_logs_created_at', 'audit_logs', ['created_at']) + op.create_index('ix_audit_logs_action', 'audit_logs', ['action']) + op.create_index('ix_audit_logs_entity_type', 'audit_logs', ['entity_type']) + op.create_index('ix_audit_logs_entity_id', 'audit_logs', ['entity_id']) + op.create_index('ix_audit_logs_user_id', 'audit_logs', ['user_id']) + op.create_index('ix_audit_logs_field_name', 'audit_logs', ['field_name']) + print("✓ Created audit_logs table") + except Exception as e: + error_msg = str(e) + if 'already exists' in error_msg.lower() or 'duplicate' in error_msg.lower(): + print("✓ Table audit_logs already exists (detected via error)") + else: + print(f"✗ Error creating audit_logs table: {e}") + raise def downgrade(): """Remove audit_logs table""" + from sqlalchemy import inspect + bind = op.get_bind() + inspector = inspect(bind) - op.drop_index('ix_audit_logs_field_name', table_name='audit_logs') - op.drop_index('ix_audit_logs_user_id', table_name='audit_logs') - op.drop_index('ix_audit_logs_entity_id', table_name='audit_logs') - op.drop_index('ix_audit_logs_entity_type', table_name='audit_logs') - op.drop_index('ix_audit_logs_action', table_name='audit_logs') - op.drop_index('ix_audit_logs_created_at', table_name='audit_logs') - op.drop_index('ix_audit_logs_user_created', table_name='audit_logs') - op.drop_index('ix_audit_logs_entity', table_name='audit_logs') - op.drop_table('audit_logs') + existing_tables = inspector.get_table_names() + + if 'audit_logs' not in existing_tables: + print("⊘ Table audit_logs does not exist, skipping") + return + + try: + existing_indexes = [idx['name'] for idx in inspector.get_indexes('audit_logs')] + indexes_to_drop = [ + 'ix_audit_logs_field_name', + 'ix_audit_logs_user_id', + 'ix_audit_logs_entity_id', + 'ix_audit_logs_entity_type', + 'ix_audit_logs_action', + 'ix_audit_logs_created_at', + 'ix_audit_logs_user_created', + 'ix_audit_logs_entity', + ] + for idx_name in indexes_to_drop: + if idx_name in existing_indexes: + try: + op.drop_index(idx_name, table_name='audit_logs') + except Exception: + pass + op.drop_table('audit_logs') + print("✓ Dropped audit_logs table") + except Exception as e: + error_msg = str(e) + if 'does not exist' in error_msg.lower() or 'no such table' in error_msg.lower(): + print("⊘ Table audit_logs does not exist (detected via error)") + else: + print(f"⚠ Warning: Could not drop audit_logs table: {e}") diff --git a/migrations/versions/046_add_webhooks_system.py b/migrations/versions/046_add_webhooks_system.py index 8384298..011925e 100644 --- a/migrations/versions/046_add_webhooks_system.py +++ b/migrations/versions/046_add_webhooks_system.py @@ -19,86 +19,178 @@ depends_on = None def upgrade(): """Create webhooks and webhook_deliveries tables""" + from sqlalchemy import inspect + bind = op.get_bind() + inspector = inspect(bind) + + existing_tables = inspector.get_table_names() # Create webhooks table - op.create_table('webhooks', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('name', sa.String(length=200), nullable=False), - sa.Column('description', sa.Text(), nullable=True), - sa.Column('url', sa.String(length=500), nullable=False), - sa.Column('secret', sa.String(length=128), nullable=True), - sa.Column('events', sa.JSON(), nullable=False, server_default='[]'), - sa.Column('http_method', sa.String(length=10), nullable=False, server_default='POST'), - sa.Column('content_type', sa.String(length=50), nullable=False, server_default='application/json'), - sa.Column('headers', sa.JSON(), nullable=True), - sa.Column('is_active', sa.Boolean(), nullable=False, server_default='1'), - sa.Column('user_id', sa.Integer(), nullable=False), - sa.Column('max_retries', sa.Integer(), nullable=False, server_default='3'), - sa.Column('retry_delay_seconds', sa.Integer(), nullable=False, server_default='60'), - sa.Column('timeout_seconds', sa.Integer(), nullable=False, server_default='30'), - sa.Column('total_deliveries', sa.Integer(), nullable=False, server_default='0'), - sa.Column('successful_deliveries', sa.Integer(), nullable=False, server_default='0'), - sa.Column('failed_deliveries', sa.Integer(), nullable=False, server_default='0'), - sa.Column('last_delivery_at', sa.DateTime(), nullable=True), - sa.Column('last_success_at', sa.DateTime(), nullable=True), - sa.Column('last_failure_at', sa.DateTime(), nullable=True), - sa.Column('created_at', sa.DateTime(), nullable=False, server_default=sa.text('CURRENT_TIMESTAMP')), - sa.Column('updated_at', sa.DateTime(), nullable=False, server_default=sa.text('CURRENT_TIMESTAMP')), - sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id') - ) - - # Create indexes for webhooks - op.create_index('ix_webhooks_user_id', 'webhooks', ['user_id']) - op.create_index('ix_webhooks_is_active', 'webhooks', ['is_active']) - op.create_index('ix_webhooks_created_at', 'webhooks', ['created_at']) + if 'webhooks' in existing_tables: + print("✓ Table webhooks already exists") + # Ensure indexes exist + try: + existing_indexes = [idx['name'] for idx in inspector.get_indexes('webhooks')] + for idx_name, cols in [ + ('ix_webhooks_user_id', ['user_id']), + ('ix_webhooks_is_active', ['is_active']), + ('ix_webhooks_created_at', ['created_at']), + ]: + if idx_name not in existing_indexes: + try: + op.create_index(idx_name, 'webhooks', cols, unique=False) + except Exception: + pass + except Exception: + pass + else: + try: + op.create_table('webhooks', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(length=200), nullable=False), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('url', sa.String(length=500), nullable=False), + sa.Column('secret', sa.String(length=128), nullable=True), + sa.Column('events', sa.JSON(), nullable=False, server_default='[]'), + sa.Column('http_method', sa.String(length=10), nullable=False, server_default='POST'), + sa.Column('content_type', sa.String(length=50), nullable=False, server_default='application/json'), + sa.Column('headers', sa.JSON(), nullable=True), + sa.Column('is_active', sa.Boolean(), nullable=False, server_default='1'), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('max_retries', sa.Integer(), nullable=False, server_default='3'), + sa.Column('retry_delay_seconds', sa.Integer(), nullable=False, server_default='60'), + sa.Column('timeout_seconds', sa.Integer(), nullable=False, server_default='30'), + sa.Column('total_deliveries', sa.Integer(), nullable=False, server_default='0'), + sa.Column('successful_deliveries', sa.Integer(), nullable=False, server_default='0'), + sa.Column('failed_deliveries', sa.Integer(), nullable=False, server_default='0'), + sa.Column('last_delivery_at', sa.DateTime(), nullable=True), + sa.Column('last_success_at', sa.DateTime(), nullable=True), + sa.Column('last_failure_at', sa.DateTime(), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=False, server_default=sa.text('CURRENT_TIMESTAMP')), + sa.Column('updated_at', sa.DateTime(), nullable=False, server_default=sa.text('CURRENT_TIMESTAMP')), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + + # Create indexes for webhooks + op.create_index('ix_webhooks_user_id', 'webhooks', ['user_id']) + op.create_index('ix_webhooks_is_active', 'webhooks', ['is_active']) + op.create_index('ix_webhooks_created_at', 'webhooks', ['created_at']) + print("✓ Created webhooks table") + except Exception as e: + error_msg = str(e) + if 'already exists' in error_msg.lower() or 'duplicate' in error_msg.lower(): + print("✓ Table webhooks already exists (detected via error)") + else: + print(f"✗ Error creating webhooks table: {e}") + raise # Create webhook_deliveries table - op.create_table('webhook_deliveries', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('webhook_id', sa.Integer(), nullable=False), - sa.Column('event_type', sa.String(length=100), nullable=False), - sa.Column('event_id', sa.String(length=100), nullable=True), - sa.Column('payload', sa.Text(), nullable=False), - sa.Column('payload_hash', sa.String(length=64), nullable=True), - sa.Column('status', sa.String(length=20), nullable=False, server_default='pending'), - sa.Column('attempt_number', sa.Integer(), nullable=False, server_default='1'), - sa.Column('response_status_code', sa.Integer(), nullable=True), - sa.Column('response_body', sa.Text(), nullable=True), - sa.Column('response_headers', sa.JSON(), nullable=True), - sa.Column('error_message', sa.Text(), nullable=True), - sa.Column('error_type', sa.String(length=100), nullable=True), - sa.Column('started_at', sa.DateTime(), nullable=False, server_default=sa.text('CURRENT_TIMESTAMP')), - sa.Column('completed_at', sa.DateTime(), nullable=True), - sa.Column('duration_ms', sa.Integer(), nullable=True), - sa.Column('next_retry_at', sa.DateTime(), nullable=True), - sa.Column('retry_count', sa.Integer(), nullable=False, server_default='0'), - sa.ForeignKeyConstraint(['webhook_id'], ['webhooks.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id') - ) - - # Create indexes for webhook_deliveries - op.create_index('ix_webhook_deliveries_webhook_id', 'webhook_deliveries', ['webhook_id']) - op.create_index('ix_webhook_deliveries_status', 'webhook_deliveries', ['status']) - op.create_index('ix_webhook_deliveries_event_type', 'webhook_deliveries', ['event_type']) - op.create_index('ix_webhook_deliveries_next_retry_at', 'webhook_deliveries', ['next_retry_at']) - op.create_index('ix_webhook_deliveries_started_at', 'webhook_deliveries', ['started_at']) + if 'webhook_deliveries' in existing_tables: + print("✓ Table webhook_deliveries already exists") + # Ensure indexes exist + try: + existing_indexes = [idx['name'] for idx in inspector.get_indexes('webhook_deliveries')] + for idx_name, cols in [ + ('ix_webhook_deliveries_webhook_id', ['webhook_id']), + ('ix_webhook_deliveries_status', ['status']), + ('ix_webhook_deliveries_event_type', ['event_type']), + ('ix_webhook_deliveries_next_retry_at', ['next_retry_at']), + ('ix_webhook_deliveries_started_at', ['started_at']), + ]: + if idx_name not in existing_indexes: + try: + op.create_index(idx_name, 'webhook_deliveries', cols, unique=False) + except Exception: + pass + except Exception: + pass + else: + try: + op.create_table('webhook_deliveries', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('webhook_id', sa.Integer(), nullable=False), + sa.Column('event_type', sa.String(length=100), nullable=False), + sa.Column('event_id', sa.String(length=100), nullable=True), + sa.Column('payload', sa.Text(), nullable=False), + sa.Column('payload_hash', sa.String(length=64), nullable=True), + sa.Column('status', sa.String(length=20), nullable=False, server_default='pending'), + sa.Column('attempt_number', sa.Integer(), nullable=False, server_default='1'), + sa.Column('response_status_code', sa.Integer(), nullable=True), + sa.Column('response_body', sa.Text(), nullable=True), + sa.Column('response_headers', sa.JSON(), nullable=True), + sa.Column('error_message', sa.Text(), nullable=True), + sa.Column('error_type', sa.String(length=100), nullable=True), + sa.Column('started_at', sa.DateTime(), nullable=False, server_default=sa.text('CURRENT_TIMESTAMP')), + sa.Column('completed_at', sa.DateTime(), nullable=True), + sa.Column('duration_ms', sa.Integer(), nullable=True), + sa.Column('next_retry_at', sa.DateTime(), nullable=True), + sa.Column('retry_count', sa.Integer(), nullable=False, server_default='0'), + sa.ForeignKeyConstraint(['webhook_id'], ['webhooks.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + + # Create indexes for webhook_deliveries + op.create_index('ix_webhook_deliveries_webhook_id', 'webhook_deliveries', ['webhook_id']) + op.create_index('ix_webhook_deliveries_status', 'webhook_deliveries', ['status']) + op.create_index('ix_webhook_deliveries_event_type', 'webhook_deliveries', ['event_type']) + op.create_index('ix_webhook_deliveries_next_retry_at', 'webhook_deliveries', ['next_retry_at']) + op.create_index('ix_webhook_deliveries_started_at', 'webhook_deliveries', ['started_at']) + print("✓ Created webhook_deliveries table") + except Exception as e: + error_msg = str(e) + if 'already exists' in error_msg.lower() or 'duplicate' in error_msg.lower(): + print("✓ Table webhook_deliveries already exists (detected via error)") + else: + print(f"✗ Error creating webhook_deliveries table: {e}") + raise def downgrade(): """Remove webhooks system tables""" + from sqlalchemy import inspect + bind = op.get_bind() + inspector = inspect(bind) + + existing_tables = inspector.get_table_names() # Drop webhook_deliveries table - op.drop_index('ix_webhook_deliveries_started_at', table_name='webhook_deliveries') - op.drop_index('ix_webhook_deliveries_next_retry_at', table_name='webhook_deliveries') - op.drop_index('ix_webhook_deliveries_event_type', table_name='webhook_deliveries') - op.drop_index('ix_webhook_deliveries_status', table_name='webhook_deliveries') - op.drop_index('ix_webhook_deliveries_webhook_id', table_name='webhook_deliveries') - op.drop_table('webhook_deliveries') + if 'webhook_deliveries' in existing_tables: + try: + existing_indexes = [idx['name'] for idx in inspector.get_indexes('webhook_deliveries')] + for idx_name in ['ix_webhook_deliveries_started_at', 'ix_webhook_deliveries_next_retry_at', + 'ix_webhook_deliveries_event_type', 'ix_webhook_deliveries_status', + 'ix_webhook_deliveries_webhook_id']: + if idx_name in existing_indexes: + try: + op.drop_index(idx_name, table_name='webhook_deliveries') + except Exception: + pass + op.drop_table('webhook_deliveries') + print("✓ Dropped webhook_deliveries table") + except Exception as e: + error_msg = str(e) + if 'does not exist' in error_msg.lower() or 'no such table' in error_msg.lower(): + print("⊘ Table webhook_deliveries does not exist (detected via error)") + else: + print(f"⚠ Warning: Could not drop webhook_deliveries table: {e}") # Drop webhooks table - op.drop_index('ix_webhooks_created_at', table_name='webhooks') - op.drop_index('ix_webhooks_is_active', table_name='webhooks') - op.drop_index('ix_webhooks_user_id', table_name='webhooks') - op.drop_table('webhooks') + if 'webhooks' in existing_tables: + try: + existing_indexes = [idx['name'] for idx in inspector.get_indexes('webhooks')] + for idx_name in ['ix_webhooks_created_at', 'ix_webhooks_is_active', 'ix_webhooks_user_id']: + if idx_name in existing_indexes: + try: + op.drop_index(idx_name, table_name='webhooks') + except Exception: + pass + op.drop_table('webhooks') + print("✓ Dropped webhooks table") + except Exception as e: + error_msg = str(e) + if 'does not exist' in error_msg.lower() or 'no such table' in error_msg.lower(): + print("⊘ Table webhooks does not exist (detected via error)") + else: + print(f"⚠ Warning: Could not drop webhooks table: {e}") diff --git a/migrations/versions/071_add_recurring_tasks.py b/migrations/versions/071_add_recurring_tasks.py index 79881a2..d812c54 100644 --- a/migrations/versions/071_add_recurring_tasks.py +++ b/migrations/versions/071_add_recurring_tasks.py @@ -47,8 +47,20 @@ def upgrade(): sa.ForeignKeyConstraint(['created_by'], ['users.id'], ), sa.PrimaryKeyConstraint('id') ) - op.create_index(op.f('ix_recurring_tasks_project_id'), 'recurring_tasks', ['project_id'], unique=False) - op.create_index(op.f('ix_recurring_tasks_assigned_to'), 'recurring_tasks', ['assigned_to'], unique=False) + # Create indexes (idempotent) + try: + existing_indexes = [idx['name'] for idx in inspector.get_indexes('recurring_tasks')] + if op.f('ix_recurring_tasks_project_id') not in existing_indexes: + op.create_index(op.f('ix_recurring_tasks_project_id'), 'recurring_tasks', ['project_id'], unique=False) + if op.f('ix_recurring_tasks_assigned_to') not in existing_indexes: + op.create_index(op.f('ix_recurring_tasks_assigned_to'), 'recurring_tasks', ['assigned_to'], unique=False) + except Exception: + # If we can't check, try to create indexes anyway + try: + op.create_index(op.f('ix_recurring_tasks_project_id'), 'recurring_tasks', ['project_id'], unique=False) + op.create_index(op.f('ix_recurring_tasks_assigned_to'), 'recurring_tasks', ['assigned_to'], unique=False) + except Exception: + pass # Indexes might already exist def downgrade(): diff --git a/migrations/versions/086_add_project_and_client_attachments.py b/migrations/versions/086_add_project_and_client_attachments.py index 7b0032d..f1134bb 100644 --- a/migrations/versions/086_add_project_and_client_attachments.py +++ b/migrations/versions/086_add_project_and_client_attachments.py @@ -20,53 +20,146 @@ depends_on = None def upgrade(): """Create project_attachments and client_attachments tables""" + from sqlalchemy import inspect + bind = op.get_bind() + inspector = inspect(bind) + + existing_tables = inspector.get_table_names() + # Create project_attachments table - op.create_table('project_attachments', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('project_id', sa.Integer(), nullable=False), - sa.Column('filename', sa.String(length=255), nullable=False), - sa.Column('original_filename', sa.String(length=255), nullable=False), - sa.Column('file_path', sa.String(length=500), nullable=False), - sa.Column('file_size', sa.Integer(), nullable=False), - sa.Column('mime_type', sa.String(length=100), nullable=True), - sa.Column('description', sa.Text(), nullable=True), - sa.Column('is_visible_to_client', sa.Boolean(), nullable=False, server_default='false'), - sa.Column('uploaded_by', sa.Integer(), nullable=False), - sa.Column('uploaded_at', sa.DateTime(), nullable=False), - sa.ForeignKeyConstraint(['project_id'], ['projects.id'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['uploaded_by'], ['users.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id') - ) - op.create_index('ix_project_attachments_project_id', 'project_attachments', ['project_id'], unique=False) - op.create_index('ix_project_attachments_uploaded_by', 'project_attachments', ['uploaded_by'], unique=False) + if 'project_attachments' in existing_tables: + print("✓ Table project_attachments already exists") + # Ensure indexes exist + try: + existing_indexes = [idx['name'] for idx in inspector.get_indexes('project_attachments')] + for idx_name, cols in [ + ('ix_project_attachments_project_id', ['project_id']), + ('ix_project_attachments_uploaded_by', ['uploaded_by']), + ]: + if idx_name not in existing_indexes: + try: + op.create_index(idx_name, 'project_attachments', cols, unique=False) + except Exception: + pass + except Exception: + pass + else: + try: + op.create_table('project_attachments', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('project_id', sa.Integer(), nullable=False), + sa.Column('filename', sa.String(length=255), nullable=False), + sa.Column('original_filename', sa.String(length=255), nullable=False), + sa.Column('file_path', sa.String(length=500), nullable=False), + sa.Column('file_size', sa.Integer(), nullable=False), + sa.Column('mime_type', sa.String(length=100), nullable=True), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('is_visible_to_client', sa.Boolean(), nullable=False, server_default='false'), + sa.Column('uploaded_by', sa.Integer(), nullable=False), + sa.Column('uploaded_at', sa.DateTime(), nullable=False), + sa.ForeignKeyConstraint(['project_id'], ['projects.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['uploaded_by'], ['users.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('ix_project_attachments_project_id', 'project_attachments', ['project_id'], unique=False) + op.create_index('ix_project_attachments_uploaded_by', 'project_attachments', ['uploaded_by'], unique=False) + print("✓ Created project_attachments table") + except Exception as e: + error_msg = str(e) + if 'already exists' in error_msg.lower() or 'duplicate' in error_msg.lower(): + print("✓ Table project_attachments already exists (detected via error)") + else: + print(f"✗ Error creating project_attachments table: {e}") + raise # Create client_attachments table - op.create_table('client_attachments', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('client_id', sa.Integer(), nullable=False), - sa.Column('filename', sa.String(length=255), nullable=False), - sa.Column('original_filename', sa.String(length=255), nullable=False), - sa.Column('file_path', sa.String(length=500), nullable=False), - sa.Column('file_size', sa.Integer(), nullable=False), - sa.Column('mime_type', sa.String(length=100), nullable=True), - sa.Column('description', sa.Text(), nullable=True), - sa.Column('is_visible_to_client', sa.Boolean(), nullable=False, server_default='false'), - sa.Column('uploaded_by', sa.Integer(), nullable=False), - sa.Column('uploaded_at', sa.DateTime(), nullable=False), - sa.ForeignKeyConstraint(['client_id'], ['clients.id'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['uploaded_by'], ['users.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id') - ) - op.create_index('ix_client_attachments_client_id', 'client_attachments', ['client_id'], unique=False) - op.create_index('ix_client_attachments_uploaded_by', 'client_attachments', ['uploaded_by'], unique=False) + if 'client_attachments' in existing_tables: + print("✓ Table client_attachments already exists") + # Ensure indexes exist + try: + existing_indexes = [idx['name'] for idx in inspector.get_indexes('client_attachments')] + for idx_name, cols in [ + ('ix_client_attachments_client_id', ['client_id']), + ('ix_client_attachments_uploaded_by', ['uploaded_by']), + ]: + if idx_name not in existing_indexes: + try: + op.create_index(idx_name, 'client_attachments', cols, unique=False) + except Exception: + pass + except Exception: + pass + else: + try: + op.create_table('client_attachments', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('client_id', sa.Integer(), nullable=False), + sa.Column('filename', sa.String(length=255), nullable=False), + sa.Column('original_filename', sa.String(length=255), nullable=False), + sa.Column('file_path', sa.String(length=500), nullable=False), + sa.Column('file_size', sa.Integer(), nullable=False), + sa.Column('mime_type', sa.String(length=100), nullable=True), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('is_visible_to_client', sa.Boolean(), nullable=False, server_default='false'), + sa.Column('uploaded_by', sa.Integer(), nullable=False), + sa.Column('uploaded_at', sa.DateTime(), nullable=False), + sa.ForeignKeyConstraint(['client_id'], ['clients.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['uploaded_by'], ['users.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('ix_client_attachments_client_id', 'client_attachments', ['client_id'], unique=False) + op.create_index('ix_client_attachments_uploaded_by', 'client_attachments', ['uploaded_by'], unique=False) + print("✓ Created client_attachments table") + except Exception as e: + error_msg = str(e) + if 'already exists' in error_msg.lower() or 'duplicate' in error_msg.lower(): + print("✓ Table client_attachments already exists (detected via error)") + else: + print(f"✗ Error creating client_attachments table: {e}") + raise def downgrade(): """Drop project_attachments and client_attachments tables""" - op.drop_index('ix_client_attachments_uploaded_by', table_name='client_attachments') - op.drop_index('ix_client_attachments_client_id', table_name='client_attachments') - op.drop_table('client_attachments') - op.drop_index('ix_project_attachments_uploaded_by', table_name='project_attachments') - op.drop_index('ix_project_attachments_project_id', table_name='project_attachments') - op.drop_table('project_attachments') + from sqlalchemy import inspect + bind = op.get_bind() + inspector = inspect(bind) + + existing_tables = inspector.get_table_names() + + if 'client_attachments' in existing_tables: + try: + existing_indexes = [idx['name'] for idx in inspector.get_indexes('client_attachments')] + for idx_name in ['ix_client_attachments_uploaded_by', 'ix_client_attachments_client_id']: + if idx_name in existing_indexes: + try: + op.drop_index(idx_name, table_name='client_attachments') + except Exception: + pass + op.drop_table('client_attachments') + print("✓ Dropped client_attachments table") + except Exception as e: + error_msg = str(e) + if 'does not exist' in error_msg.lower() or 'no such table' in error_msg.lower(): + print("⊘ Table client_attachments does not exist (detected via error)") + else: + print(f"⚠ Warning: Could not drop client_attachments table: {e}") + + if 'project_attachments' in existing_tables: + try: + existing_indexes = [idx['name'] for idx in inspector.get_indexes('project_attachments')] + for idx_name in ['ix_project_attachments_uploaded_by', 'ix_project_attachments_project_id']: + if idx_name in existing_indexes: + try: + op.drop_index(idx_name, table_name='project_attachments') + except Exception: + pass + op.drop_table('project_attachments') + print("✓ Dropped project_attachments table") + except Exception as e: + error_msg = str(e) + if 'does not exist' in error_msg.lower() or 'no such table' in error_msg.lower(): + print("⊘ Table project_attachments does not exist (detected via error)") + else: + print(f"⚠ Warning: Could not drop project_attachments table: {e}") diff --git a/migrations/versions/087_add_salesman_email_mapping.py b/migrations/versions/087_add_salesman_email_mapping.py index 6b2db23..7226c03 100644 --- a/migrations/versions/087_add_salesman_email_mapping.py +++ b/migrations/versions/087_add_salesman_email_mapping.py @@ -19,7 +19,32 @@ depends_on = None def upgrade(): """Create salesman_email_mappings table""" - op.create_table('salesman_email_mappings', + from sqlalchemy import inspect + bind = op.get_bind() + inspector = inspect(bind) + + existing_tables = inspector.get_table_names() + + if 'salesman_email_mappings' in existing_tables: + print("✓ Table salesman_email_mappings already exists") + # Ensure indexes exist + try: + existing_indexes = [idx['name'] for idx in inspector.get_indexes('salesman_email_mappings')] + for idx_name, cols in [ + ('ix_salesman_email_mappings_initial', ['salesman_initial']), + ('ix_salesman_email_mappings_active', ['is_active']), + ]: + if idx_name not in existing_indexes: + try: + op.create_index(idx_name, 'salesman_email_mappings', cols, unique=False) + except Exception: + pass + except Exception: + pass + return + + try: + op.create_table('salesman_email_mappings', sa.Column('id', sa.Integer(), nullable=False), sa.Column('salesman_initial', sa.String(length=20), nullable=False), sa.Column('email_address', sa.String(length=255), nullable=True), @@ -30,15 +55,46 @@ def upgrade(): sa.Column('created_at', sa.DateTime(), nullable=False), sa.Column('updated_at', sa.DateTime(), nullable=False), sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('salesman_initial', name='uq_salesman_email_mapping_initial') - ) - op.create_index('ix_salesman_email_mappings_initial', 'salesman_email_mappings', ['salesman_initial'], unique=False) - op.create_index('ix_salesman_email_mappings_active', 'salesman_email_mappings', ['is_active'], unique=False) + sa.UniqueConstraint('salesman_initial', name='uq_salesman_email_mapping_initial') + ) + op.create_index('ix_salesman_email_mappings_initial', 'salesman_email_mappings', ['salesman_initial'], unique=False) + op.create_index('ix_salesman_email_mappings_active', 'salesman_email_mappings', ['is_active'], unique=False) + print("✓ Created salesman_email_mappings table") + except Exception as e: + error_msg = str(e) + if 'already exists' in error_msg.lower() or 'duplicate' in error_msg.lower(): + print("✓ Table salesman_email_mappings already exists (detected via error)") + else: + print(f"✗ Error creating salesman_email_mappings table: {e}") + raise def downgrade(): """Drop salesman_email_mappings table""" - op.drop_index('ix_salesman_email_mappings_active', table_name='salesman_email_mappings') - op.drop_index('ix_salesman_email_mappings_initial', table_name='salesman_email_mappings') - op.drop_table('salesman_email_mappings') + from sqlalchemy import inspect + bind = op.get_bind() + inspector = inspect(bind) + + existing_tables = inspector.get_table_names() + + if 'salesman_email_mappings' not in existing_tables: + print("⊘ Table salesman_email_mappings does not exist, skipping") + return + + try: + existing_indexes = [idx['name'] for idx in inspector.get_indexes('salesman_email_mappings')] + for idx_name in ['ix_salesman_email_mappings_active', 'ix_salesman_email_mappings_initial']: + if idx_name in existing_indexes: + try: + op.drop_index(idx_name, table_name='salesman_email_mappings') + except Exception: + pass + op.drop_table('salesman_email_mappings') + print("✓ Dropped salesman_email_mappings table") + except Exception as e: + error_msg = str(e) + if 'does not exist' in error_msg.lower() or 'no such table' in error_msg.lower(): + print("⊘ Table salesman_email_mappings does not exist (detected via error)") + else: + print(f"⚠ Warning: Could not drop salesman_email_mappings table: {e}")