mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-17 20:23:57 +08:00
fix(database): harden fresh migration chain compatibility (#6278)
This commit is contained in:
@@ -145,6 +145,13 @@ DOWNGRADE_RESTORE_INDEXES = {
|
||||
|
||||
def _load_schema_state(inspector: sa.Inspector):
|
||||
tables = set(inspector.get_table_names())
|
||||
table_columns = {
|
||||
table_name: {
|
||||
column["name"]
|
||||
for column in inspector.get_columns(table_name)
|
||||
}
|
||||
for table_name in tables
|
||||
}
|
||||
table_indexes = {
|
||||
table_name: {
|
||||
index["name"]: {
|
||||
@@ -155,7 +162,7 @@ def _load_schema_state(inspector: sa.Inspector):
|
||||
}
|
||||
for table_name in tables
|
||||
}
|
||||
return tables, table_indexes
|
||||
return tables, table_columns, table_indexes
|
||||
|
||||
|
||||
def _drop_index(
|
||||
@@ -215,10 +222,13 @@ def _create_index(
|
||||
index_name: str,
|
||||
columns: list[str],
|
||||
tables: set[str],
|
||||
table_columns: dict[str, set[str]],
|
||||
table_indexes: dict[str, dict[str, dict[str, object]]],
|
||||
) -> None:
|
||||
if table_name not in tables:
|
||||
return
|
||||
if not set(columns).issubset(table_columns[table_name]):
|
||||
return
|
||||
if index_name in table_indexes[table_name]:
|
||||
return
|
||||
if _has_index_signature(table_name, columns, tables, table_indexes, unique=False):
|
||||
@@ -231,8 +241,9 @@ def _create_index(
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""以字段签名幂等替换 2.2.4 高频查询索引。"""
|
||||
inspector = sa.inspect(op.get_bind())
|
||||
tables, table_indexes = _load_schema_state(inspector)
|
||||
tables, table_columns, table_indexes = _load_schema_state(inspector)
|
||||
|
||||
for table_name, index_specs in REDUNDANT_ID_INDEXES.items():
|
||||
for index_name, columns in index_specs:
|
||||
@@ -258,12 +269,20 @@ def upgrade() -> None:
|
||||
|
||||
for table_name, index_specs in CREATE_INDEXES.items():
|
||||
for index_name, columns in index_specs:
|
||||
_create_index(table_name, index_name, columns, tables, table_indexes)
|
||||
_create_index(
|
||||
table_name,
|
||||
index_name,
|
||||
columns,
|
||||
tables,
|
||||
table_columns,
|
||||
table_indexes,
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""移除组合索引并恢复适用于当前表结构的旧索引。"""
|
||||
inspector = sa.inspect(op.get_bind())
|
||||
tables, table_indexes = _load_schema_state(inspector)
|
||||
tables, table_columns, table_indexes = _load_schema_state(inspector)
|
||||
|
||||
for table_name, index_specs in CREATE_INDEXES.items():
|
||||
for index_name, _ in index_specs:
|
||||
@@ -271,8 +290,22 @@ def downgrade() -> None:
|
||||
|
||||
for table_name, index_specs in DOWNGRADE_RESTORE_INDEXES.items():
|
||||
for index_name, columns in index_specs:
|
||||
_create_index(table_name, index_name, columns, tables, table_indexes)
|
||||
_create_index(
|
||||
table_name,
|
||||
index_name,
|
||||
columns,
|
||||
tables,
|
||||
table_columns,
|
||||
table_indexes,
|
||||
)
|
||||
|
||||
for table_name, index_specs in REDUNDANT_ID_INDEXES.items():
|
||||
for index_name, columns in index_specs:
|
||||
_create_index(table_name, index_name, columns, tables, table_indexes)
|
||||
_create_index(
|
||||
table_name,
|
||||
index_name,
|
||||
columns,
|
||||
tables,
|
||||
table_columns,
|
||||
table_indexes,
|
||||
)
|
||||
|
||||
@@ -36,55 +36,87 @@ def _has_column(
|
||||
)
|
||||
|
||||
|
||||
def _has_index(
|
||||
inspector: sa.Inspector,
|
||||
table_name: str,
|
||||
index_name: str,
|
||||
) -> bool:
|
||||
"""检查数据表是否已存在指定索引。"""
|
||||
if table_name not in inspector.get_table_names():
|
||||
return False
|
||||
return any(
|
||||
index["name"] == index_name
|
||||
for index in inspector.get_indexes(table_name)
|
||||
)
|
||||
|
||||
|
||||
def _column_names(inspector: sa.Inspector, table_name: str) -> set[str]:
|
||||
"""读取数据表当前全部字段名。"""
|
||||
if table_name not in inspector.get_table_names():
|
||||
return set()
|
||||
return {
|
||||
column["name"]
|
||||
for column in inspector.get_columns(table_name)
|
||||
}
|
||||
|
||||
|
||||
def _ensure_column_and_index(column_name: str) -> None:
|
||||
"""独立补齐整理历史的规范身份字段及其索引。"""
|
||||
table_name = "transferhistory"
|
||||
index_name = f"ix_{table_name}_{column_name}"
|
||||
inspector = sa.inspect(op.get_bind())
|
||||
if not _has_column(inspector, table_name, column_name):
|
||||
op.add_column(
|
||||
table_name,
|
||||
sa.Column(column_name, sa.String(), nullable=True),
|
||||
)
|
||||
|
||||
inspector = sa.inspect(op.get_bind())
|
||||
if not _has_index(inspector, table_name, index_name):
|
||||
op.create_index(index_name, table_name, [column_name])
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""升级整理历史数据源字段。"""
|
||||
inspector = sa.inspect(op.get_bind())
|
||||
if not _has_column(inspector, "transferhistory", "media_source"):
|
||||
op.add_column(
|
||||
"transferhistory",
|
||||
sa.Column("media_source", sa.String(), nullable=True),
|
||||
)
|
||||
op.create_index(
|
||||
"ix_transferhistory_media_source",
|
||||
"transferhistory",
|
||||
["media_source"],
|
||||
)
|
||||
_ensure_column_and_index("media_source")
|
||||
_ensure_column_and_index("media_id")
|
||||
|
||||
inspector = sa.inspect(op.get_bind())
|
||||
if not _has_column(inspector, "transferhistory", "media_id"):
|
||||
op.add_column(
|
||||
"transferhistory",
|
||||
sa.Column("media_id", sa.String(), nullable=True),
|
||||
)
|
||||
op.create_index(
|
||||
"ix_transferhistory_media_id",
|
||||
"transferhistory",
|
||||
["media_id"],
|
||||
)
|
||||
|
||||
transfer_history = sa.table(
|
||||
"transferhistory",
|
||||
sa.column("tmdbid", sa.Integer()),
|
||||
sa.column("doubanid", sa.String()),
|
||||
columns = _column_names(sa.inspect(op.get_bind()), "transferhistory")
|
||||
table_columns = [
|
||||
sa.column("media_source", sa.String()),
|
||||
sa.column("media_id", sa.String()),
|
||||
)
|
||||
]
|
||||
if "tmdbid" in columns:
|
||||
table_columns.append(sa.column("tmdbid", sa.Integer()))
|
||||
if "doubanid" in columns:
|
||||
table_columns.append(sa.column("doubanid", sa.String()))
|
||||
transfer_history = sa.table("transferhistory", *table_columns)
|
||||
connection = op.get_bind()
|
||||
connection.execute(
|
||||
transfer_history.update()
|
||||
.where(transfer_history.c.tmdbid.is_not(None))
|
||||
.where(transfer_history.c.media_id.is_(None))
|
||||
.values(
|
||||
media_source="themoviedb",
|
||||
media_id=sa.cast(transfer_history.c.tmdbid, sa.String()),
|
||||
|
||||
if "tmdbid" in columns:
|
||||
connection.execute(
|
||||
transfer_history.update()
|
||||
.where(transfer_history.c.tmdbid.is_not(None))
|
||||
.where(transfer_history.c.media_id.is_(None))
|
||||
.values(
|
||||
media_source="themoviedb",
|
||||
media_id=sa.cast(transfer_history.c.tmdbid, sa.String()),
|
||||
)
|
||||
)
|
||||
)
|
||||
connection.execute(
|
||||
|
||||
if "doubanid" not in columns:
|
||||
return
|
||||
douban_update = (
|
||||
transfer_history.update()
|
||||
.where(transfer_history.c.tmdbid.is_(None))
|
||||
.where(transfer_history.c.doubanid.is_not(None))
|
||||
.where(transfer_history.c.media_id.is_(None))
|
||||
.values(
|
||||
)
|
||||
if "tmdbid" in columns:
|
||||
douban_update = douban_update.where(
|
||||
transfer_history.c.tmdbid.is_(None)
|
||||
)
|
||||
connection.execute(
|
||||
douban_update.values(
|
||||
media_source="douban",
|
||||
media_id=transfer_history.c.doubanid,
|
||||
)
|
||||
@@ -93,15 +125,12 @@ def upgrade() -> None:
|
||||
|
||||
def downgrade() -> None:
|
||||
"""回滚整理历史数据源字段。"""
|
||||
inspector = sa.inspect(op.get_bind())
|
||||
if _has_column(inspector, "transferhistory", "media_id"):
|
||||
op.drop_index("ix_transferhistory_media_id", table_name="transferhistory")
|
||||
op.drop_column("transferhistory", "media_id")
|
||||
for column_name in ("media_id", "media_source"):
|
||||
index_name = f"ix_transferhistory_{column_name}"
|
||||
inspector = sa.inspect(op.get_bind())
|
||||
if _has_index(inspector, "transferhistory", index_name):
|
||||
op.drop_index(index_name, table_name="transferhistory")
|
||||
|
||||
inspector = sa.inspect(op.get_bind())
|
||||
if _has_column(inspector, "transferhistory", "media_source"):
|
||||
op.drop_index(
|
||||
"ix_transferhistory_media_source",
|
||||
table_name="transferhistory",
|
||||
)
|
||||
op.drop_column("transferhistory", "media_source")
|
||||
inspector = sa.inspect(op.get_bind())
|
||||
if _has_column(inspector, "transferhistory", column_name):
|
||||
op.drop_column("transferhistory", column_name)
|
||||
|
||||
@@ -46,6 +46,17 @@ def _has_index(
|
||||
)
|
||||
|
||||
|
||||
def _column_names(table_name: str) -> set[str]:
|
||||
"""读取数据表当前全部字段名。"""
|
||||
inspector = sa.inspect(op.get_bind())
|
||||
if table_name not in inspector.get_table_names():
|
||||
return set()
|
||||
return {
|
||||
column["name"]
|
||||
for column in inspector.get_columns(table_name)
|
||||
}
|
||||
|
||||
|
||||
def _add_columns(table_name: str, columns: Iterable[sa.Column]) -> None:
|
||||
"""为指定表补充尚不存在的字段。"""
|
||||
for column in columns:
|
||||
@@ -61,22 +72,29 @@ def _create_index(table_name: str, index_name: str, columns: list[str]) -> None:
|
||||
op.create_index(index_name, table_name, columns)
|
||||
|
||||
|
||||
def _backfill_media_identity(table_name: str, has_mediaid: bool = False) -> None:
|
||||
"""使用兼容 ID 幂等回填统一媒体身份。"""
|
||||
def _backfill_media_identity(table_name: str) -> None:
|
||||
"""使用表中实际存在的兼容 ID 幂等回填统一媒体身份。"""
|
||||
existing_columns = _column_names(table_name)
|
||||
columns = [
|
||||
sa.column("tmdbid", sa.Integer()),
|
||||
sa.column("doubanid", sa.String()),
|
||||
sa.column("bangumiid", sa.Integer()),
|
||||
sa.column("anilistid", sa.Integer()),
|
||||
sa.column("media_source", sa.String()),
|
||||
sa.column("media_id", sa.String()),
|
||||
]
|
||||
if has_mediaid:
|
||||
columns.append(sa.column("mediaid", sa.String()))
|
||||
identity_columns = {
|
||||
"mediaid": sa.String,
|
||||
"tmdbid": sa.Integer,
|
||||
"doubanid": sa.String,
|
||||
"bangumiid": sa.Integer,
|
||||
"anilistid": sa.Integer,
|
||||
}
|
||||
columns.extend(
|
||||
sa.column(column_name, column_type())
|
||||
for column_name, column_type in identity_columns.items()
|
||||
if column_name in existing_columns
|
||||
)
|
||||
table = sa.table(table_name, *columns)
|
||||
connection = op.get_bind()
|
||||
|
||||
if has_mediaid:
|
||||
if "mediaid" in existing_columns:
|
||||
for prefix, source in (
|
||||
("tmdb", "themoviedb"),
|
||||
("themoviedb", "themoviedb"),
|
||||
@@ -100,6 +118,8 @@ def _backfill_media_identity(table_name: str, has_mediaid: bool = False) -> None
|
||||
("bangumi", "bangumiid"),
|
||||
("anilist", "anilistid"),
|
||||
):
|
||||
if field not in existing_columns:
|
||||
continue
|
||||
identity_column = table.c[field]
|
||||
connection.execute(
|
||||
table.update()
|
||||
@@ -162,8 +182,8 @@ def upgrade() -> None:
|
||||
["type", "media_source", "media_id", "site"],
|
||||
)
|
||||
|
||||
_backfill_media_identity("subscribe", has_mediaid=True)
|
||||
_backfill_media_identity("subscribehistory", has_mediaid=True)
|
||||
_backfill_media_identity("subscribe")
|
||||
_backfill_media_identity("subscribehistory")
|
||||
_backfill_media_identity("downloadhistory")
|
||||
_backfill_media_identity("transferhistory")
|
||||
_backfill_media_identity("downloadfailure")
|
||||
|
||||
Reference in New Issue
Block a user