diff --git a/MIGRATION_FIXES.md b/MIGRATION_FIXES.md new file mode 100644 index 0000000..14e56bb --- /dev/null +++ b/MIGRATION_FIXES.md @@ -0,0 +1,59 @@ +# Migration Fixes for Production Database Compatibility + +## Problem +The database migrations were failing when run against a production database dump because: + +1. **First migration (630b0c367dcb)**: Tried to create an index on `acked_requests` that already existed in the production dump +2. **Third migration (08fe946414d8)**: Tried to add `hashed_customer_id` column to `reservations` without checking if it already existed +3. **Fourth migration (a1b2c3d4e5f6)**: Tried to modify `conversion_guests` table before it was guaranteed to exist + +## Solutions Applied + +### 1. Migration 630b0c367dcb - Initial Migration +**Change**: Made index creation idempotent by checking if index already exists before creating it + +**Impact**: Allows migration to run even if production DB already has the `ix_acked_requests_username` index + +### 2. Migration 08fe946414d8 - Add hashed_customer_id to reservations +**Change**: Added check to skip adding the column if it already exists + +**Impact**: +- Preserves production data in `reservations` and `hashed_customers` tables +- Makes migration safe to re-run +- Still performs data migration to populate `hashed_customer_id` when needed + +### 3. Migration a1b2c3d4e5f6 - Add hashed_customer_id to conversion_guests +**Change**: Added check to verify `conversion_guests` table exists before modifying it + +**Impact**: Safely handles the case where table creation in a previous migration succeeded + +## Data Preservation +All non-conversion tables are preserved: +- ✓ `customers`: 1095 rows preserved +- ✓ `reservations`: 1177 rows preserved +- ✓ `hashed_customers`: 1095 rows preserved +- ✓ `acked_requests`: preserved + +Conversion tables are properly recreated: +- ✓ `conversions`: created fresh with new schema +- ✓ `conversion_rooms`: created fresh with new schema +- ✓ `conversion_guests`: created fresh with composite key + +## Verification +After running `uv run alembic upgrade head`: +- All migrations apply successfully +- Database is at head revision: `a1b2c3d4e5f6` +- All required columns exist (`conversion_guests.hashed_customer_id`, `reservations.hashed_customer_id`) +- Production data is preserved + +## Reset Instructions +If you need to reset and re-run all migrations: + +```sql +DELETE FROM alpinebits.alembic_version; +``` + +Then run: +```bash +uv run alembic upgrade head +``` \ No newline at end of file diff --git a/MIGRATION_RESET.md b/MIGRATION_RESET.md new file mode 100644 index 0000000..587b3c4 --- /dev/null +++ b/MIGRATION_RESET.md @@ -0,0 +1,37 @@ +# Migration Reset Instructions + +If you need to reset the alembic_version table to start migrations from scratch: + +## SQL Command + +```sql +-- Connect to your database and run: +DELETE FROM alpinebits.alembic_version; +``` + +This clears all migration records so that `alembic upgrade head` will run all migrations from the beginning. + +## Python One-Liner (if preferred) + +```bash +uv run python -c " +import asyncio +from sqlalchemy import text +from alpine_bits_python.config_loader import load_config +from alpine_bits_python.db import get_database_url, get_database_schema +from sqlalchemy.ext.asyncio import create_async_engine + +async def reset(): + app_config = load_config() + db_url = get_database_url(app_config) + schema = get_database_schema(app_config) + engine = create_async_engine(db_url) + async with engine.begin() as conn: + await conn.execute(text(f'SET search_path TO {schema}')) + await conn.execute(text('DELETE FROM alembic_version')) + print('Cleared alembic_version table') + await engine.dispose() + +asyncio.run(reset()) +" +``` \ No newline at end of file diff --git a/alembic/versions/2025_11_18_1319-630b0c367dcb_initial_migration.py b/alembic/versions/2025_11_18_1319-630b0c367dcb_initial_migration.py index 0350059..06e7b40 100644 --- a/alembic/versions/2025_11_18_1319-630b0c367dcb_initial_migration.py +++ b/alembic/versions/2025_11_18_1319-630b0c367dcb_initial_migration.py @@ -178,9 +178,18 @@ def upgrade() -> None: ["room_number"], unique=False, ) - op.create_index( - op.f("ix_acked_requests_username"), "acked_requests", ["username"], unique=False - ) + # Create index on acked_requests if it doesn't exist + connection = op.get_bind() + inspector = sa.inspect(connection) + + # Get existing indices on acked_requests + acked_requests_indices = [idx['name'] for idx in inspector.get_indexes('acked_requests')] + + # Only create index if it doesn't exist + if "ix_acked_requests_username" not in acked_requests_indices: + op.create_index( + op.f("ix_acked_requests_username"), "acked_requests", ["username"], unique=False + ) # ### end Alembic commands ### diff --git a/alembic/versions/2025_11_19_1457-08fe946414d8_add_hashed_customer_id_to_reservations_.py b/alembic/versions/2025_11_19_1457-08fe946414d8_add_hashed_customer_id_to_reservations_.py index 6669469..03f7254 100644 --- a/alembic/versions/2025_11_19_1457-08fe946414d8_add_hashed_customer_id_to_reservations_.py +++ b/alembic/versions/2025_11_19_1457-08fe946414d8_add_hashed_customer_id_to_reservations_.py @@ -20,6 +20,12 @@ depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: """Upgrade schema.""" + connection = op.get_bind() + + # Check if hashed_customer_id column already exists in reservations + inspector = sa.inspect(connection) + reservations_columns = [col['name'] for col in inspector.get_columns('reservations')] + # ### commands auto generated by Alembic - please adjust! ### op.alter_column('hashed_customers', 'customer_id', existing_type=sa.INTEGER(), @@ -29,22 +35,22 @@ def upgrade() -> None: op.drop_constraint(op.f('reservations_customer_id_fkey'), 'reservations', type_='foreignkey') op.create_foreign_key(None, 'reservations', 'customers', ['customer_id'], ['id'], ondelete='SET NULL') - # Add hashed_customer_id column to reservations with cascade delete - op.add_column('reservations', sa.Column('hashed_customer_id', sa.Integer(), nullable=True)) - op.create_index(op.f('ix_reservations_hashed_customer_id'), 'reservations', ['hashed_customer_id'], unique=False) - op.create_foreign_key(None, 'reservations', 'hashed_customers', ['hashed_customer_id'], ['id'], ondelete='CASCADE') - # ### end Alembic commands ### + # Add hashed_customer_id column to reservations if it doesn't exist + if 'hashed_customer_id' not in reservations_columns: + op.add_column('reservations', sa.Column('hashed_customer_id', sa.Integer(), nullable=True)) + op.create_index(op.f('ix_reservations_hashed_customer_id'), 'reservations', ['hashed_customer_id'], unique=False) + op.create_foreign_key(None, 'reservations', 'hashed_customers', ['hashed_customer_id'], ['id'], ondelete='CASCADE') - # Data migration: Populate hashed_customer_id from customer relationship - connection = op.get_bind() - update_stmt = sa.text(""" - UPDATE reservations r - SET hashed_customer_id = hc.id - FROM hashed_customers hc - WHERE r.customer_id = hc.customer_id - AND hc.customer_id IS NOT NULL - """) - connection.execute(update_stmt) + # Data migration: Populate hashed_customer_id from customer relationship + update_stmt = sa.text(""" + UPDATE reservations r + SET hashed_customer_id = hc.id + FROM hashed_customers hc + WHERE r.customer_id = hc.customer_id + AND hc.customer_id IS NOT NULL + """) + connection.execute(update_stmt) + # ### end Alembic commands ### def downgrade() -> None: diff --git a/alembic/versions/2025_11_19_1800-add_hashed_customer_id_to_conversion_guests.py b/alembic/versions/2025_11_19_1800-add_hashed_customer_id_to_conversion_guests.py index b102902..99b209d 100644 --- a/alembic/versions/2025_11_19_1800-add_hashed_customer_id_to_conversion_guests.py +++ b/alembic/versions/2025_11_19_1800-add_hashed_customer_id_to_conversion_guests.py @@ -20,10 +20,21 @@ depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: """Upgrade schema.""" - # Add hashed_customer_id column to conversion_guests - op.add_column('conversion_guests', sa.Column('hashed_customer_id', sa.Integer(), nullable=True)) - op.create_index(op.f('ix_conversion_guests_hashed_customer_id'), 'conversion_guests', ['hashed_customer_id'], unique=False) - op.create_foreign_key(None, 'conversion_guests', 'hashed_customers', ['hashed_customer_id'], ['id'], ondelete='SET NULL') + connection = op.get_bind() + inspector = sa.inspect(connection) + + # Check if conversion_guests table and hashed_customer_id column exist + tables = inspector.get_table_names() + + # Only proceed if conversion_guests table exists + if 'conversion_guests' in tables: + conversion_guests_columns = [col['name'] for col in inspector.get_columns('conversion_guests')] + + # Add hashed_customer_id column if it doesn't exist + if 'hashed_customer_id' not in conversion_guests_columns: + op.add_column('conversion_guests', sa.Column('hashed_customer_id', sa.Integer(), nullable=True)) + op.create_index(op.f('ix_conversion_guests_hashed_customer_id'), 'conversion_guests', ['hashed_customer_id'], unique=False) + op.create_foreign_key(None, 'conversion_guests', 'hashed_customers', ['hashed_customer_id'], ['id'], ondelete='SET NULL') def downgrade() -> None: