Files
geMoldInsight/scripts/migrate_multi_upload.py
T
2026-03-07 03:14:16 +08:00

213 lines
8.1 KiB
Python

"""
数据库迁移脚本 - 添加多上传支持字段
运行方式: python scripts/migrate_multi_upload.py
此脚本将:
1. 添加 upload_batch 字段到 stp_files 表
2. 添加 volume, surface_area, product_weight 快速查询字段到 stp_files 表
3. 移除 file_hash 字段的唯一约束(如果存在)
4. 为新字段创建索引
"""
import asyncio
import sys
import os
from pathlib import Path
project_root = Path(__file__).parent.parent
sys.path.insert(0, str(project_root))
sys.path.insert(0, str(project_root / "src"))
from sqlalchemy import text
from src.database.database import async_engine
from src.utils.logger import get_logger
logger = get_logger(__name__)
async def check_column_exists(conn, table_name: str, column_name: str) -> bool:
"""检查列是否存在"""
result = await conn.execute(text("""
SELECT column_name
FROM information_schema.columns
WHERE table_name = :table_name
AND column_name = :column_name
"""), {"table_name": table_name, "column_name": column_name})
return result.fetchone() is not None
async def check_index_exists(conn, index_name: str) -> bool:
"""检查索引是否存在"""
result = await conn.execute(text("""
SELECT indexname
FROM pg_indexes
WHERE indexname = :index_name
"""), {"index_name": index_name})
return result.fetchone() is not None
async def check_unique_constraint_exists(conn, table_name: str, column_name: str) -> bool:
"""检查唯一约束是否存在"""
result = await conn.execute(text("""
SELECT conname
FROM pg_constraint
WHERE conrelid = :table_name::regclass
AND contype = 'u'
AND conname LIKE :pattern
"""), {"table_name": table_name, "pattern": f"%{column_name}%"})
return result.fetchone() is not None
async def run_migration():
"""执行迁移"""
logger.info("开始数据库迁移 - 多上传支持...")
async with async_engine.begin() as conn:
try:
await conn.execute(text("SELECT 1"))
logger.info("数据库连接成功")
except Exception as e:
logger.error(f"数据库连接失败: {e}")
return False
async with async_engine.begin() as conn:
migration_steps = []
if not await check_column_exists(conn, "stp_files", "upload_batch"):
migration_steps.append("添加 upload_batch 字段")
await conn.execute(text("""
ALTER TABLE stp_files
ADD COLUMN upload_batch VARCHAR(36)
"""))
logger.info("✓ 添加 upload_batch 字段")
if not await check_column_exists(conn, "stp_files", "volume"):
migration_steps.append("添加 volume 字段")
await conn.execute(text("""
ALTER TABLE stp_files
ADD COLUMN volume FLOAT
"""))
logger.info("✓ 添加 volume 字段")
if not await check_column_exists(conn, "stp_files", "surface_area"):
migration_steps.append("添加 surface_area 字段")
await conn.execute(text("""
ALTER TABLE stp_files
ADD COLUMN surface_area FLOAT
"""))
logger.info("✓ 添加 surface_area 字段")
if not await check_column_exists(conn, "stp_files", "product_weight"):
migration_steps.append("添加 product_weight 字段")
await conn.execute(text("""
ALTER TABLE stp_files
ADD COLUMN product_weight FLOAT
"""))
logger.info("✓ 添加 product_weight 字段")
if not await check_index_exists(conn, "ix_stp_files_upload_batch"):
migration_steps.append("创建 upload_batch 索引")
await conn.execute(text("""
CREATE INDEX ix_stp_files_upload_batch
ON stp_files(upload_batch)
"""))
logger.info("✓ 创建 upload_batch 索引")
if not await check_index_exists(conn, "ix_stp_files_file_hash"):
migration_steps.append("创建 file_hash 索引")
await conn.execute(text("""
CREATE INDEX ix_stp_files_file_hash
ON stp_files(file_hash)
"""))
logger.info("✓ 创建 file_hash 索引")
if not await check_index_exists(conn, "ix_stp_files_original_filename"):
migration_steps.append("创建 original_filename 索引")
await conn.execute(text("""
CREATE INDEX ix_stp_files_original_filename
ON stp_files(original_filename)
"""))
logger.info("✓ 创建 original_filename 索引")
try:
result = await conn.execute(text("""
SELECT conname
FROM pg_constraint
WHERE conrelid = 'stp_files'::regclass
AND contype = 'u'
"""))
unique_constraints = result.fetchall()
for constraint in unique_constraints:
constraint_name = constraint[0]
if 'file_hash' in constraint_name.lower():
migration_steps.append(f"删除唯一约束 {constraint_name}")
await conn.execute(text(f"""
ALTER TABLE stp_files
DROP CONSTRAINT {constraint_name}
"""))
logger.info(f"✓ 删除唯一约束: {constraint_name}")
except Exception as e:
logger.warning(f"检查唯一约束时出错(可能不存在): {e}")
if migration_steps:
logger.info(f"\n迁移完成,执行了 {len(migration_steps)} 个步骤:")
for step in migration_steps:
logger.info(f" - {step}")
else:
logger.info("\n无需迁移,所有字段和索引已存在")
logger.info("数据库迁移完成!")
return True
async def rollback_migration():
"""回滚迁移"""
logger.info("开始回滚数据库迁移...")
async with async_engine.begin() as conn:
try:
if await check_index_exists(conn, "ix_stp_files_upload_batch"):
await conn.execute(text("DROP INDEX IF EXISTS ix_stp_files_upload_batch"))
logger.info("✓ 删除 upload_batch 索引")
if await check_index_exists(conn, "ix_stp_files_file_hash"):
await conn.execute(text("DROP INDEX IF EXISTS ix_stp_files_file_hash"))
logger.info("✓ 删除 file_hash 索引")
if await check_column_exists(conn, "stp_files", "upload_batch"):
await conn.execute(text("ALTER TABLE stp_files DROP COLUMN upload_batch"))
logger.info("✓ 删除 upload_batch 字段")
if await check_column_exists(conn, "stp_files", "volume"):
await conn.execute(text("ALTER TABLE stp_files DROP COLUMN volume"))
logger.info("✓ 删除 volume 字段")
if await check_column_exists(conn, "stp_files", "surface_area"):
await conn.execute(text("ALTER TABLE stp_files DROP COLUMN surface_area"))
logger.info("✓ 删除 surface_area 字段")
if await check_column_exists(conn, "stp_files", "product_weight"):
await conn.execute(text("ALTER TABLE stp_files DROP COLUMN product_weight"))
logger.info("✓ 删除 product_weight 字段")
logger.info("回滚完成!")
except Exception as e:
logger.error(f"回滚失败: {e}")
return False
return True
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description="数据库迁移脚本 - 多上传支持")
parser.add_argument("--rollback", action="store_true", help="回滚迁移")
args = parser.parse_args()
if args.rollback:
asyncio.run(rollback_migration())
else:
asyncio.run(run_migration())