Files
romm/backend/alembic/versions/0068_save_sync.py
Georges-Antoine Assi 1eba6da6af fix: gate MariaDB-specific FK lookup behind dialect check in 0068 downgrade
The information_schema query using DATABASE() only works on MariaDB/MySQL.
Move it inside the is_postgresql guard so PostgreSQL downgrades don't error.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 11:09:33 -04:00

145 lines
5.0 KiB
Python

"""Add device-based save synchronization
Revision ID: 0068_save_sync
Revises: 0067_romfile_category_enum_cheat
Create Date: 2026-01-17
"""
import sqlalchemy as sa
from alembic import op
from utils.database import is_postgresql
revision = "0068_save_sync"
down_revision = "0067_romfile_category_enum_cheat"
branch_labels = None
depends_on = None
def upgrade():
op.create_table(
"devices",
sa.Column("id", sa.String(255), primary_key=True),
sa.Column("user_id", sa.Integer(), nullable=False),
sa.Column("name", sa.String(255), nullable=True),
sa.Column("platform", sa.String(50), nullable=True),
sa.Column("client", sa.String(50), nullable=True),
sa.Column("client_version", sa.String(50), nullable=True),
sa.Column("ip_address", sa.String(45), nullable=True),
sa.Column("mac_address", sa.String(17), nullable=True),
sa.Column("hostname", sa.String(255), nullable=True),
sa.Column(
"sync_mode",
sa.Enum("API", "FILE_TRANSFER", "PUSH_PULL", name="syncmode"),
nullable=False,
server_default="API",
),
sa.Column("sync_enabled", sa.Boolean(), nullable=False, server_default="1"),
sa.Column("last_seen", sa.TIMESTAMP(timezone=True), nullable=True),
sa.Column(
"created_at",
sa.TIMESTAMP(timezone=True),
server_default=sa.text("now()"),
nullable=False,
),
sa.Column(
"updated_at",
sa.TIMESTAMP(timezone=True),
server_default=sa.text("now()"),
nullable=False,
),
sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"),
if_not_exists=True,
)
op.create_table(
"device_save_sync",
sa.Column("device_id", sa.String(255), nullable=False),
sa.Column("save_id", sa.Integer(), nullable=False),
sa.Column("last_synced_at", sa.TIMESTAMP(timezone=True), nullable=False),
sa.Column("is_untracked", sa.Boolean(), nullable=False, server_default="0"),
sa.Column(
"created_at",
sa.TIMESTAMP(timezone=True),
server_default=sa.text("now()"),
nullable=False,
),
sa.Column(
"updated_at",
sa.TIMESTAMP(timezone=True),
server_default=sa.text("now()"),
nullable=False,
),
sa.ForeignKeyConstraint(["device_id"], ["devices.id"], ondelete="CASCADE"),
sa.ForeignKeyConstraint(["save_id"], ["saves.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("device_id", "save_id"),
if_not_exists=True,
)
with op.batch_alter_table("saves", schema=None) as batch_op:
batch_op.add_column(
sa.Column("slot", sa.String(255), nullable=True), if_not_exists=True
)
batch_op.add_column(
sa.Column("content_hash", sa.String(32), nullable=True), if_not_exists=True
)
op.create_index("ix_devices_user_id", "devices", ["user_id"], if_not_exists=True)
op.create_index(
"ix_devices_last_seen", "devices", ["last_seen"], if_not_exists=True
)
op.create_index(
"ix_device_save_sync_save_id",
"device_save_sync",
["save_id"],
if_not_exists=True,
)
op.create_index("ix_saves_slot", "saves", ["slot"], if_not_exists=True)
op.create_index(
"ix_saves_rom_user_hash",
"saves",
["rom_id", "user_id", "content_hash"],
if_not_exists=True,
)
def downgrade():
conn = op.get_bind()
fk_names: list[str] = []
if not is_postgresql(conn):
# MariaDB won't let us drop an index that backs a FK constraint, so we
# need to temporarily drop the FK on saves.rom_id first.
fk_rows = conn.execute(
sa.text(
"SELECT CONSTRAINT_NAME FROM information_schema.KEY_COLUMN_USAGE "
"WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'saves' "
"AND COLUMN_NAME = 'rom_id' AND REFERENCED_TABLE_NAME IS NOT NULL"
)
).fetchall()
fk_names = [row[0] for row in fk_rows]
for fk_name in fk_names:
op.drop_constraint(fk_name, "saves", type_="foreignkey")
op.drop_index("ix_saves_rom_user_hash", "saves", if_exists=True)
op.drop_index("ix_saves_slot", "saves", if_exists=True)
if not is_postgresql(conn):
# Re-create the FK (MariaDB will auto-create an implicit index for it)
for fk_name in fk_names:
op.create_foreign_key(
fk_name, "saves", "roms", ["rom_id"], ["id"], ondelete="CASCADE"
)
with op.batch_alter_table("saves", schema=None) as batch_op:
batch_op.drop_column("content_hash", if_exists=True)
batch_op.drop_column("slot", if_exists=True)
# drop_table handles indexes and FK constraints, no need to drop them separately
op.drop_table("device_save_sync", if_exists=True)
op.drop_table("devices", if_exists=True)
if is_postgresql(op.get_bind()):
op.execute("DROP TYPE IF EXISTS syncmode")