341 lines
11 KiB
Python
341 lines
11 KiB
Python
#!/usr/bin/env python
|
|
# -*- coding: utf-8 -*-
|
|
|
|
"""
|
|
数据库迁移执行脚本
|
|
|
|
用法:
|
|
python scripts/run_migration.py [--revision 版本号]
|
|
|
|
示例:
|
|
# 升级到最新版本
|
|
python scripts/run_migration.py
|
|
|
|
# 升级到指定版本
|
|
python scripts/run_migration.py --revision 001
|
|
|
|
# 降级到指定版本
|
|
python scripts/run_migration.py --revision 001 --downgrade
|
|
|
|
# 生成新的迁移脚本
|
|
python scripts/run_migration.py --generate "添加新字段"
|
|
|
|
# 为特定表生成迁移脚本
|
|
python scripts/run_migration.py --generate "为用户表添加邮箱字段" --table users
|
|
|
|
# 为多个表生成迁移脚本
|
|
python scripts/run_migration.py --generate "添加审计字段" --table users,orders,products
|
|
"""
|
|
|
|
import argparse
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
import subprocess
|
|
import shutil
|
|
import tempfile
|
|
import locale
|
|
import datetime
|
|
|
|
# 将项目根目录添加到 Python 路径
|
|
root_dir = Path(__file__).parent.parent
|
|
sys.path.insert(0, str(root_dir))
|
|
|
|
# 导入项目日志模块
|
|
from utils.logger import get_logger, setup_logger
|
|
|
|
# 设置日志
|
|
setup_logger()
|
|
logger = get_logger('migration')
|
|
|
|
def create_ascii_config(original_config_path):
|
|
"""
|
|
创建一个 ASCII 编码的配置文件副本
|
|
|
|
Args:
|
|
original_config_path (Path): 原始配置文件路径
|
|
|
|
Returns:
|
|
Path: 临时配置文件路径
|
|
"""
|
|
# 如果临时文件已存在,直接使用
|
|
temp_config_path = original_config_path.with_suffix('.ini.tmp')
|
|
if temp_config_path.exists():
|
|
logger.info(f"使用已存在的临时配置文件: {temp_config_path}")
|
|
return temp_config_path
|
|
|
|
# 创建临时文件
|
|
logger.info(f"创建 ASCII 编码的临时配置文件: {temp_config_path}")
|
|
|
|
# 复制配置文件内容,去除中文注释
|
|
with open(temp_config_path, 'w', encoding='ascii', errors='ignore') as temp_file:
|
|
temp_file.write("""
|
|
# Alembic Configuration File
|
|
# This file contains basic configuration for Alembic
|
|
|
|
[alembic]
|
|
# Path to migration scripts
|
|
script_location = migrations
|
|
|
|
# Template uses jinja2 format
|
|
output_encoding = utf-8
|
|
|
|
# Database connection configuration
|
|
# In practice, this value will be overridden by the configuration in env.py
|
|
sqlalchemy.url = driver://user:pass@localhost/dbname
|
|
|
|
# Logging configuration
|
|
[loggers]
|
|
keys = root,sqlalchemy,alembic
|
|
|
|
[handlers]
|
|
keys = console
|
|
|
|
[formatters]
|
|
keys = generic
|
|
|
|
[logger_root]
|
|
level = WARN
|
|
handlers = console
|
|
qualname =
|
|
|
|
[logger_sqlalchemy]
|
|
level = WARN
|
|
handlers =
|
|
qualname = sqlalchemy.engine
|
|
|
|
[logger_alembic]
|
|
level = INFO
|
|
handlers =
|
|
qualname = alembic
|
|
|
|
[handler_console]
|
|
class = StreamHandler
|
|
args = (sys.stderr,)
|
|
level = NOTSET
|
|
formatter = generic
|
|
|
|
[formatter_generic]
|
|
format = %(levelname)-5.5s [%(name)s] %(message)s
|
|
datefmt = %H:%M:%S
|
|
""")
|
|
|
|
return temp_config_path
|
|
|
|
def run_migration_command(command, args):
|
|
"""
|
|
执行迁移命令
|
|
|
|
Args:
|
|
command (str): 命令名称,如 'upgrade', 'downgrade', 'revision'
|
|
args (argparse.Namespace): 命令行参数
|
|
|
|
Returns:
|
|
bool: 是否成功执行命令
|
|
"""
|
|
# 获取项目根目录
|
|
root_dir = Path(__file__).parent.parent
|
|
|
|
# 原始配置文件路径
|
|
original_config_path = root_dir / "migrations" / "alembic.ini"
|
|
|
|
# 创建 ASCII 编码的临时配置文件
|
|
temp_config_path = create_ascii_config(original_config_path)
|
|
|
|
# 构建命令
|
|
cmd = ["alembic", "-c", str(temp_config_path)]
|
|
|
|
if args.verbose:
|
|
cmd.append("--verbose")
|
|
|
|
# 打印所有参数,帮助调试
|
|
logger.info(f"命令参数: {vars(args)}")
|
|
|
|
if command == 'upgrade':
|
|
cmd.extend(["upgrade", args.revision or "head"])
|
|
elif command == 'downgrade':
|
|
cmd.extend(["downgrade", args.revision or "base"])
|
|
elif command == 'revision':
|
|
logger.info(f"生成迁移脚本,描述: {args.message}")
|
|
cmd.extend(["revision", "--autogenerate", "-m", args.message])
|
|
|
|
# 如果指定了表名,创建一个临时的 env.py 文件,只包含指定的表
|
|
if args.table:
|
|
# 将表名列表转换为 Python 列表字符串
|
|
tables = [f"'{table.strip()}'" for table in args.table.split(',')]
|
|
tables_str = f"[{', '.join(tables)}]"
|
|
|
|
# 创建临时环境变量,传递表名列表
|
|
os.environ["ALEMBIC_TABLES"] = args.table
|
|
logger.info(f"指定迁移表: {args.table}")
|
|
|
|
if args.branch:
|
|
cmd.extend(["--branch", args.branch])
|
|
elif command == 'history':
|
|
cmd.append("history")
|
|
if args.verbose:
|
|
cmd.append("-v")
|
|
elif command == 'current':
|
|
cmd.append("current")
|
|
elif command == 'show':
|
|
cmd.extend(["show", args.revision or "head"])
|
|
elif command == 'list_tables':
|
|
# 这不是 alembic 命令,而是我们自定义的命令
|
|
return list_database_tables()
|
|
else:
|
|
logger.error(f"未知命令: {command}")
|
|
return False
|
|
|
|
# 执行命令
|
|
logger.info(f"执行命令: {' '.join(cmd)}")
|
|
logger.info(f"工作目录: {root_dir}")
|
|
logger.info(f"Python 编码: {sys.getdefaultencoding()}")
|
|
logger.info(f"文件系统编码: {sys.getfilesystemencoding()}")
|
|
logger.info(f"系统默认编码: {locale.getpreferredencoding()}")
|
|
|
|
# 设置环境变量,确保使用 UTF-8 编码
|
|
env = os.environ.copy()
|
|
env["PYTHONIOENCODING"] = "utf-8"
|
|
|
|
# 创建命令输出日志文件 - 使用项目日志目录
|
|
from config.settings import LogConfig
|
|
log_config = LogConfig.as_dict()
|
|
log_dir = Path(os.path.dirname(log_config["file"]))
|
|
log_dir.mkdir(exist_ok=True)
|
|
|
|
# 使用日期和命令类型命名日志文件
|
|
today = datetime.datetime.now().strftime('%Y%m%d')
|
|
log_file = log_dir / f"migration_{command}_{today}.log"
|
|
|
|
try:
|
|
# 使用直接执行命令的方式,避免 subprocess 的编码问题
|
|
logger.info(f"正在执行迁移,日志将写入 {log_file}...")
|
|
|
|
# 使用 subprocess.Popen 而不是 subprocess.run
|
|
with open(log_file, "a", encoding="utf-8") as f_out:
|
|
# 添加分隔线和时间戳
|
|
f_out.write(f"\n\n{'='*80}\n")
|
|
f_out.write(f"执行时间: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
|
|
f_out.write(f"执行命令: {' '.join(cmd)}\n")
|
|
f_out.write(f"{'='*80}\n\n")
|
|
|
|
process = subprocess.Popen(
|
|
cmd,
|
|
cwd=str(root_dir),
|
|
env=env,
|
|
stdout=f_out,
|
|
stderr=subprocess.STDOUT,
|
|
text=True,
|
|
encoding="utf-8",
|
|
errors="replace" # 使用 replace 策略处理无法解码的字符
|
|
)
|
|
process.wait()
|
|
|
|
# 读取日志文件
|
|
with open(log_file, "r", encoding="utf-8") as f:
|
|
# 只读取最后 50 行,避免日志过长
|
|
lines = f.readlines()
|
|
output = ''.join(lines[-50:]) if len(lines) > 50 else ''.join(lines)
|
|
|
|
logger.info(f"命令执行状态: {'成功' if process.returncode == 0 else '失败'}")
|
|
logger.info(f"输出(最后部分):\n{output}")
|
|
|
|
# 清理环境变量
|
|
if 'ALEMBIC_TABLES' in os.environ:
|
|
del os.environ['ALEMBIC_TABLES']
|
|
|
|
return process.returncode == 0
|
|
except Exception as e:
|
|
logger.error(f"执行命令时发生错误: {e}", exc_info=True)
|
|
|
|
# 清理环境变量
|
|
if 'ALEMBIC_TABLES' in os.environ:
|
|
del os.environ['ALEMBIC_TABLES']
|
|
|
|
return False
|
|
|
|
def list_database_tables():
|
|
"""
|
|
列出数据库中的所有表
|
|
|
|
Returns:
|
|
bool: 是否成功执行命令
|
|
"""
|
|
try:
|
|
# 导入数据库配置
|
|
from config.database import DBConfig
|
|
|
|
# 获取数据库连接
|
|
engine = DBConfig.engine
|
|
|
|
# 获取所有表名
|
|
from sqlalchemy import inspect
|
|
inspector = inspect(engine)
|
|
tables = inspector.get_table_names()
|
|
|
|
logger.info("数据库中的表:")
|
|
for i, table in enumerate(tables, 1):
|
|
logger.info(f"{i}. {table}")
|
|
|
|
return True
|
|
except Exception as e:
|
|
logger.error(f"获取数据库表时发生错误: {e}", exc_info=True)
|
|
return False
|
|
|
|
def run_migration(args):
|
|
"""
|
|
执行数据库迁移
|
|
|
|
Args:
|
|
args (argparse.Namespace): 命令行参数
|
|
|
|
Returns:
|
|
bool: 是否成功执行迁移
|
|
"""
|
|
# 打印所有参数,帮助调试
|
|
logger.info(f"运行迁移,参数: {vars(args)}")
|
|
|
|
if args.list_tables:
|
|
# 列出数据库中的所有表
|
|
return run_migration_command('list_tables', args)
|
|
elif args.message:
|
|
# 生成新的迁移脚本
|
|
logger.info(f"生成新的迁移脚本,描述: {args.message}")
|
|
return run_migration_command('revision', args)
|
|
elif args.history:
|
|
# 显示迁移历史
|
|
return run_migration_command('history', args)
|
|
elif args.current:
|
|
# 显示当前版本
|
|
return run_migration_command('current', args)
|
|
elif args.show:
|
|
# 显示指定版本的详细信息
|
|
return run_migration_command('show', args)
|
|
elif args.downgrade:
|
|
# 降级到指定版本
|
|
return run_migration_command('downgrade', args)
|
|
else:
|
|
# 升级到指定版本
|
|
return run_migration_command('upgrade', args)
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="执行数据库迁移")
|
|
parser.add_argument("--revision", help="版本号,为空表示升级到最新版本")
|
|
parser.add_argument("--downgrade", action="store_true", help="是否降级")
|
|
parser.add_argument("--verbose", "-v", action="store_true", help="显示详细日志")
|
|
parser.add_argument("--generate", "--gen", "-m", dest="message", help="生成新的迁移脚本,需要提供描述信息")
|
|
parser.add_argument("--branch", "-b", help="分支名称,用于生成迁移脚本")
|
|
parser.add_argument("--history", action="store_true", help="显示迁移历史")
|
|
parser.add_argument("--current", action="store_true", help="显示当前版本")
|
|
parser.add_argument("--show", action="store_true", help="显示指定版本的详细信息")
|
|
parser.add_argument("--table", "-t", help="指定要迁移的表名,多个表用逗号分隔")
|
|
parser.add_argument("--list-tables", action="store_true", help="列出数据库中的所有表")
|
|
|
|
args = parser.parse_args()
|
|
|
|
success = run_migration(args)
|
|
sys.exit(0 if success else 1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main() |