"""Add app_settings table and expand perceptual_hash to 64 chars for 256-bit hashes Revision ID: b26011901 Revises: a46e641430b8 Create Date: 2026-01-19 """ from alembic import op import sqlalchemy as sa # revision identifiers, used by Alembic. revision = 'b26011901' down_revision = 'a46e641430b8' branch_labels = None depends_on = None def upgrade(): # Create app_settings table op.create_table( 'app_settings', sa.Column('id', sa.Integer(), nullable=False), sa.Column('key', sa.String(length=64), nullable=False), sa.Column('value', sa.Text(), nullable=True), sa.Column('updated_at', sa.DateTime(), nullable=True), sa.PrimaryKeyConstraint('id'), sa.UniqueConstraint('key') ) # Expand perceptual_hash column from 16 to 64 chars bind = op.get_bind() dialect = bind.dialect.name if dialect == "sqlite": # SQLite requires table recreation for column type changes. # IMPORTANT: When recreating image_record, we must also recreate # image_tags to preserve foreign key relationships. # Step 1: Backup image_tags data op.execute("CREATE TABLE image_tags_backup AS SELECT * FROM image_tags") # Step 2: Drop image_tags (it references image_record) op.drop_table("image_tags") # Step 3: Recreate image_record with new column size with op.batch_alter_table("image_record", recreate="always") as batch_op: batch_op.alter_column( 'perceptual_hash', existing_type=sa.String(length=16), type_=sa.String(length=64), existing_nullable=True ) # Step 4: Recreate image_tags with proper FK constraints op.create_table( "image_tags", sa.Column("image_id", sa.Integer(), nullable=False), sa.Column("tag_id", sa.Integer(), nullable=False), sa.PrimaryKeyConstraint("image_id", "tag_id"), sa.ForeignKeyConstraint(["image_id"], ["image_record.id"], ondelete="CASCADE"), sa.ForeignKeyConstraint(["tag_id"], ["tag.id"], ondelete="CASCADE"), sa.UniqueConstraint("image_id", "tag_id", name="uq_image_tag"), ) # Step 5: Restore image_tags data op.execute("INSERT INTO image_tags (image_id, tag_id) SELECT image_id, tag_id FROM image_tags_backup") # Step 6: Drop backup table op.drop_table("image_tags_backup") else: # PostgreSQL and others can alter directly op.alter_column( 'image_record', 'perceptual_hash', existing_type=sa.String(length=16), type_=sa.String(length=64), existing_nullable=True ) def downgrade(): bind = op.get_bind() dialect = bind.dialect.name # Shrink perceptual_hash back to 16 chars (will truncate data!) if dialect == "sqlite": # Same approach as upgrade: backup image_tags, recreate image_record, # then restore image_tags to preserve FK relationships # Step 1: Backup image_tags data op.execute("CREATE TABLE image_tags_backup AS SELECT * FROM image_tags") # Step 2: Drop image_tags (it references image_record) op.drop_table("image_tags") # Step 3: Recreate image_record with old column size with op.batch_alter_table("image_record", recreate="always") as batch_op: batch_op.alter_column( 'perceptual_hash', existing_type=sa.String(length=64), type_=sa.String(length=16), existing_nullable=True ) # Step 4: Recreate image_tags with proper FK constraints op.create_table( "image_tags", sa.Column("image_id", sa.Integer(), nullable=False), sa.Column("tag_id", sa.Integer(), nullable=False), sa.PrimaryKeyConstraint("image_id", "tag_id"), sa.ForeignKeyConstraint(["image_id"], ["image_record.id"], ondelete="CASCADE"), sa.ForeignKeyConstraint(["tag_id"], ["tag.id"], ondelete="CASCADE"), sa.UniqueConstraint("image_id", "tag_id", name="uq_image_tag"), ) # Step 5: Restore image_tags data op.execute("INSERT INTO image_tags (image_id, tag_id) SELECT image_id, tag_id FROM image_tags_backup") # Step 6: Drop backup table op.drop_table("image_tags_backup") else: op.alter_column( 'image_record', 'perceptual_hash', existing_type=sa.String(length=64), type_=sa.String(length=16), existing_nullable=True ) # Drop app_settings table op.drop_table('app_settings')