Skip to content

Commit 8ce8449

Browse files
committed
fix(migrations): make DDL idempotent on partial-failure reruns
Migration 0084 could fail on startup with "Duplicate key name 'idx_roms_name_fs_name_fulltext'" when a prior run created the FULLTEXT index but failed at a later step before Alembic recorded the revision. On rerun, CREATE FULLTEXT INDEX (which has no portable IF NOT EXISTS on MySQL/MariaDB) crashed re-creating the existing index. Guard the raw DDL so re-running a partially-applied migration is safe: - 0084: check existing indexes before creating/dropping the FULLTEXT index - 0014: check existing columns before the raw ALTER TABLE ADD COLUMN id - 0033: use checkfirst=True on the Postgres enum create/drop Generated-By: PostHog Code Task-Id: 87d70ab6-f915-48c0-afa7-55064c97d1a5
1 parent 030952b commit 8ce8449

3 files changed

Lines changed: 31 additions & 15 deletions

File tree

backend/alembic/versions/0014_asset_files.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -168,13 +168,18 @@ def upgrade() -> None:
168168
batch_op.drop_constraint(constraint_name=pk_constraint_name, type_="primary")
169169
batch_op.drop_column("n_roms", if_exists=True)
170170

171-
# Switch to new id column as platform primary key
172-
if is_postgresql(connection):
173-
op.execute("ALTER TABLE platforms ADD COLUMN id SERIAL PRIMARY KEY")
174-
else:
175-
op.execute(
176-
"ALTER TABLE platforms ADD COLUMN id INTEGER(11) NOT NULL AUTO_INCREMENT PRIMARY KEY"
177-
)
171+
# Switch to new id column as platform primary key. Raw DDL has no portable
172+
# "IF NOT EXISTS", so guard it to stay idempotent on a partial-failure rerun.
173+
platform_columns = {
174+
column["name"] for column in sa.inspect(connection).get_columns("platforms")
175+
}
176+
if "id" not in platform_columns:
177+
if is_postgresql(connection):
178+
op.execute("ALTER TABLE platforms ADD COLUMN id SERIAL PRIMARY KEY")
179+
else:
180+
op.execute(
181+
"ALTER TABLE platforms ADD COLUMN id INTEGER(11) NOT NULL AUTO_INCREMENT PRIMARY KEY"
182+
)
178183

179184
# Add new columns to roms table
180185
with op.batch_alter_table("roms", schema=None) as batch_op:

backend/alembic/versions/0033_rom_file_and_hashes.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ def upgrade() -> None:
3737
name="romfilecategory",
3838
create_type=False,
3939
)
40-
rom_file_category_enum.create(connection, checkfirst=False)
40+
rom_file_category_enum.create(connection, checkfirst=True)
4141
else:
4242
rom_file_category_enum = sa.Enum(
4343
"GAME",
@@ -263,4 +263,4 @@ def downgrade() -> None:
263263
op.drop_table("rom_files", if_exists=True)
264264

265265
if is_postgresql(connection):
266-
ENUM(name="romfilecategory").drop(connection, checkfirst=False)
266+
ENUM(name="romfilecategory").drop(connection, checkfirst=True)

backend/alembic/versions/0084_add_roms_search_index.py

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,18 @@ def upgrade() -> None:
4747

4848
# 1. DB-specific search indexes on roms.name and roms.fs_name.
4949
if is_mysql(bind) or is_mariadb(bind):
50-
op.execute(
51-
sa.text(
52-
f"CREATE FULLTEXT INDEX {FULLTEXT_INDEX_NAME} "
53-
"ON roms (name, fs_name)"
50+
# MySQL/MariaDB have no portable "IF NOT EXISTS" for FULLTEXT indexes, so
51+
# guard the create to stay idempotent if a prior run partially applied.
52+
existing_indexes = {
53+
index["name"] for index in sa.inspect(bind).get_indexes("roms")
54+
}
55+
if FULLTEXT_INDEX_NAME not in existing_indexes:
56+
op.execute(
57+
sa.text(
58+
f"CREATE FULLTEXT INDEX {FULLTEXT_INDEX_NAME} "
59+
"ON roms (name, fs_name)"
60+
)
5461
)
55-
)
5662
elif is_postgresql(bind):
5763
# pg_trgm is a trusted extension since PostgreSQL 13, so a non-superuser
5864
# with CREATE on the database can install it.
@@ -130,7 +136,12 @@ def downgrade() -> None:
130136

131137
# 1. DB-specific search indexes.
132138
if is_mysql(bind) or is_mariadb(bind):
133-
op.execute(sa.text(f"DROP INDEX {FULLTEXT_INDEX_NAME} ON roms"))
139+
# MySQL has no portable "IF EXISTS" for DROP INDEX, so guard the drop.
140+
existing_indexes = {
141+
index["name"] for index in sa.inspect(bind).get_indexes("roms")
142+
}
143+
if FULLTEXT_INDEX_NAME in existing_indexes:
144+
op.execute(sa.text(f"DROP INDEX {FULLTEXT_INDEX_NAME} ON roms"))
134145
elif is_postgresql(bind):
135146
# Leave the pg_trgm extension in place; other objects may depend on it.
136147
op.execute(sa.text(f"DROP INDEX IF EXISTS {PG_FS_NAME_INDEX}"))

0 commit comments

Comments
 (0)