refactor(migrations): add idempotency checks to migration scripts

This commit is contained in:
fawney19
2025-12-16 17:46:38 +08:00
parent 46ff5a1a50
commit d696c575e6
3 changed files with 226 additions and 119 deletions

View File

@@ -26,16 +26,66 @@ branch_labels = None
depends_on = None depends_on = None
def column_exists(bind, table_name: str, column_name: str) -> bool:
"""检查列是否存在"""
result = bind.execute(
sa.text(
"""
SELECT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = :table_name AND column_name = :column_name
)
"""
),
{"table_name": table_name, "column_name": column_name},
)
return result.scalar()
def table_exists(bind, table_name: str) -> bool:
"""检查表是否存在"""
result = bind.execute(
sa.text(
"""
SELECT EXISTS (
SELECT 1 FROM information_schema.tables
WHERE table_name = :table_name
)
"""
),
{"table_name": table_name},
)
return result.scalar()
def index_exists(bind, index_name: str) -> bool:
"""检查索引是否存在"""
result = bind.execute(
sa.text(
"""
SELECT EXISTS (
SELECT 1 FROM pg_indexes
WHERE indexname = :index_name
)
"""
),
{"index_name": index_name},
)
return result.scalar()
def upgrade() -> None: def upgrade() -> None:
"""添加 provider_model_aliases 字段,迁移数据,删除 model_mappings 表""" """添加 provider_model_aliases 字段,迁移数据,删除 model_mappings 表"""
# 1. 添加 provider_model_aliases 字段 bind = op.get_bind()
# 1. 添加 provider_model_aliases 字段(如果不存在)
if not column_exists(bind, "models", "provider_model_aliases"):
op.add_column( op.add_column(
'models', 'models',
sa.Column('provider_model_aliases', sa.JSON(), nullable=True) sa.Column('provider_model_aliases', sa.JSON(), nullable=True)
) )
# 2. 迁移 model_mappings 数据 # 2. 迁移 model_mappings 数据(如果表存在)
bind = op.get_bind()
session = Session(bind=bind) session = Session(bind=bind)
model_mappings_table = sa.table( model_mappings_table = sa.table(
@@ -96,6 +146,8 @@ def upgrade() -> None:
# 查询所有活跃的 provider 级别 alias只迁移 is_active=True 且 mapping_type='alias' 的) # 查询所有活跃的 provider 级别 alias只迁移 is_active=True 且 mapping_type='alias' 的)
# 全局别名/映射不迁移(新架构不再支持 source_model -> GlobalModel.name 的解析) # 全局别名/映射不迁移(新架构不再支持 source_model -> GlobalModel.name 的解析)
# 仅当 model_mappings 表存在时执行迁移
if table_exists(bind, "model_mappings"):
mappings = session.execute( mappings = session.execute(
sa.select( sa.select(
model_mappings_table.c.source_model, model_mappings_table.c.source_model,
@@ -177,7 +229,8 @@ def upgrade() -> None:
op.drop_table('model_mappings') op.drop_table('model_mappings')
# 4. 添加索引优化别名解析性能 # 4. 添加索引优化别名解析性能
# provider_model_name 索引(支持精确匹配) # provider_model_name 索引(支持精确匹配,如果不存在
if not index_exists(bind, "idx_model_provider_model_name"):
op.create_index( op.create_index(
"idx_model_provider_model_name", "idx_model_provider_model_name",
"models", "models",
@@ -189,11 +242,22 @@ def upgrade() -> None:
# provider_model_aliases GIN 索引(支持 JSONB 查询,仅 PostgreSQL # provider_model_aliases GIN 索引(支持 JSONB 查询,仅 PostgreSQL
if bind.dialect.name == "postgresql": if bind.dialect.name == "postgresql":
# 将 json 列转为 jsonbjsonb 性能更好且支持 GIN 索引) # 将 json 列转为 jsonbjsonb 性能更好且支持 GIN 索引)
# 使用 IF NOT EXISTS 风格的检查来避免重复转换
op.execute( op.execute(
""" """
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'models'
AND column_name = 'provider_model_aliases'
AND data_type = 'json'
) THEN
ALTER TABLE models ALTER TABLE models
ALTER COLUMN provider_model_aliases TYPE jsonb ALTER COLUMN provider_model_aliases TYPE jsonb
USING provider_model_aliases::jsonb USING provider_model_aliases::jsonb;
END IF;
END $$;
""" """
) )
# 创建 GIN 索引 # 创建 GIN 索引

View File

@@ -5,8 +5,8 @@ Revises: e9b3d63f0cbf
Create Date: 2025-12-15 17:07:44.631032+00:00 Create Date: 2025-12-15 17:07:44.631032+00:00
""" """
from alembic import op
import sqlalchemy as sa import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic. # revision identifiers, used by Alembic.
@@ -16,9 +16,28 @@ branch_labels = None
depends_on = None depends_on = None
def column_exists(bind, table_name: str, column_name: str) -> bool:
"""检查列是否存在"""
result = bind.execute(
sa.text(
"""
SELECT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = :table_name AND column_name = :column_name
)
"""
),
{"table_name": table_name, "column_name": column_name},
)
return result.scalar()
def upgrade() -> None: def upgrade() -> None:
"""应用迁移:升级到新版本""" """应用迁移:升级到新版本"""
# 添加首字时间字段到 usage 表 bind = op.get_bind()
# 添加首字时间字段到 usage 表(如果不存在)
if not column_exists(bind, "usage", "first_byte_time_ms"):
op.add_column('usage', sa.Column('first_byte_time_ms', sa.Integer(), nullable=True)) op.add_column('usage', sa.Column('first_byte_time_ms', sa.Integer(), nullable=True))

View File

@@ -5,8 +5,8 @@ Revises: 180e63a9c83a
Create Date: 2025-12-16 03:11:32.480976+00:00 Create Date: 2025-12-16 03:11:32.480976+00:00
""" """
from alembic import op
import sqlalchemy as sa import sqlalchemy as sa
from alembic import op
from sqlalchemy.dialects import postgresql from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic. # revision identifiers, used by Alembic.
@@ -16,6 +16,22 @@ branch_labels = None
depends_on = None depends_on = None
def column_exists(bind, table_name: str, column_name: str) -> bool:
"""检查列是否存在"""
result = bind.execute(
sa.text(
"""
SELECT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = :table_name AND column_name = :column_name
)
"""
),
{"table_name": table_name, "column_name": column_name},
)
return result.scalar()
def upgrade() -> None: def upgrade() -> None:
"""应用迁移:升级到新版本 """应用迁移:升级到新版本
@@ -23,14 +39,22 @@ def upgrade() -> None:
2. 把旧数据迁移到 config 2. 把旧数据迁移到 config
3. 删除旧列 3. 删除旧列
""" """
bind = op.get_bind()
# 检查是否已经迁移过config 列存在且旧列不存在)
has_config = column_exists(bind, "global_models", "config")
has_old_columns = column_exists(bind, "global_models", "default_supports_streaming")
if has_config and not has_old_columns:
# 已完成迁移,跳过
return
# 1. 添加 config 列(使用 JSONB 类型,支持索引和更高效的查询) # 1. 添加 config 列(使用 JSONB 类型,支持索引和更高效的查询)
if not has_config:
op.add_column('global_models', sa.Column('config', postgresql.JSONB(), nullable=True)) op.add_column('global_models', sa.Column('config', postgresql.JSONB(), nullable=True))
# 2. 迁移数据:把旧字段合并到 config JSON # 2. 迁移数据:把旧字段合并到 config JSON(仅当旧列存在时)
# 注意:使用 COALESCE 为布尔字段设置默认值,避免数据丢失 if has_old_columns:
# - streaming 默认 true大多数模型支持
# - 其他能力默认 false
# - jsonb_strip_nulls 只移除 null 字段,不影响 false 值
op.execute(""" op.execute("""
UPDATE global_models UPDATE global_models
SET config = jsonb_strip_nulls(jsonb_build_object( SET config = jsonb_strip_nulls(jsonb_build_object(