You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
buffer_platform/buffer_platform_v2/scripts/migrate_data.py

453 lines
14 KiB

"""
数据库迁移脚本将原版两个SQLite数据库合并到V2统一数据库
原版数据库
- buffer.db5张表market_data, symbol_timestamps, scheduled_tasks, users, sessions
- futures_analysis.db14张表futures_analysis, watched_symbols, ai_model_configs, ...
V2数据库
- buffer_v2.db19张表合并上述两个库的所有表
使用方法
python scripts/migrate_data.py [--source-dir SOURCE_DIR] [--target-db TARGET_DB] [--dry-run]
示例
# 预览迁移(不实际执行)
python scripts/migrate_data.py --dry-run
# 执行迁移(默认路径)
python scripts/migrate_data.py
# 指定源目录
python scripts/migrate_data.py --source-dir ../data
"""
import argparse
import os
import shutil
import sqlite3
import sys
from datetime import datetime
from pathlib import Path
# 默认路径
DEFAULT_SOURCE_DIR = Path(__file__).parent.parent.parent / "data"
DEFAULT_TARGET_DB = Path(__file__).parent.parent / "data" / "buffer_v2.db"
# 从 buffer.db 迁移的表
BUFFER_DB_TABLES = [
"market_data",
"symbol_timestamps",
"scheduled_tasks",
"users",
"sessions",
]
# 从 futures_analysis.db 迁移的表
ANALYSIS_DB_TABLES = [
"futures_analysis",
"watched_symbols",
"ai_model_configs",
"analysis_settings",
"ai_analysis_cache",
"review_dates",
"symbol_rankings",
"trading_plans",
"symbol_scores_v2",
"trading_plans_v2",
"sector_heat",
"review_plans_v2",
"trade_records",
"trade_import_batches",
]
def get_table_row_count(conn: sqlite3.Connection, table: str) -> int:
"""获取表的行数"""
try:
cursor = conn.execute(f"SELECT COUNT(*) FROM {table}")
return cursor.fetchone()[0]
except sqlite3.OperationalError:
return -1 # 表不存在
def get_table_columns(conn: sqlite3.Connection, table: str) -> list[str]:
"""获取表的列名列表"""
cursor = conn.execute(f"PRAGMA table_info({table})")
return [row[1] for row in cursor.fetchall()]
def check_table_exists(conn: sqlite3.Connection, table: str) -> bool:
"""检查表是否存在"""
cursor = conn.execute(
"SELECT name FROM sqlite_master WHERE type='table' AND name=?", (table,)
)
return cursor.fetchone() is not None
def backup_database(db_path: Path) -> Path | None:
"""备份数据库文件"""
if not db_path.exists():
return None
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
backup_path = db_path.with_suffix(f".db.backup_{timestamp}")
shutil.copy2(db_path, backup_path)
return backup_path
def migrate_database(
source_dir: Path,
target_db: Path,
dry_run: bool = False,
) -> dict:
"""执行数据迁移
Args:
source_dir: 原始数据库所在目录
target_db: 目标V2数据库路径
dry_run: 仅预览不执行
Returns:
迁移统计字典
"""
buffer_db_path = source_dir / "buffer.db"
analysis_db_path = source_dir / "futures_analysis.db"
stats = {
"buffer_db_tables": {},
"analysis_db_tables": {},
"errors": [],
"dry_run": dry_run,
}
# -----------------------------------------------------------------------
# 1. 检查源数据库
# -----------------------------------------------------------------------
print("=" * 60)
print("数据库迁移buffer.db + futures_analysis.db → buffer_v2.db")
print("=" * 60)
if not buffer_db_path.exists():
stats["errors"].append(f"源数据库不存在: {buffer_db_path}")
print(f"[ERROR] 源数据库不存在: {buffer_db_path}")
return stats
if not analysis_db_path.exists():
stats["errors"].append(f"源数据库不存在: {analysis_db_path}")
print(f"[ERROR] 源数据库不存在: {analysis_db_path}")
return stats
print(f"\n[OK] 源数据库已找到:")
print(f" - {buffer_db_path}")
print(f" - {analysis_db_path}")
# -----------------------------------------------------------------------
# 2. 统计源数据库数据量
# -----------------------------------------------------------------------
print("\n" + "-" * 60)
print("源数据库统计:")
print("-" * 60)
# 统计 buffer.db
buffer_conn = sqlite3.connect(str(buffer_db_path))
print(f"\n buffer.db ({buffer_db_path}):")
for table in BUFFER_DB_TABLES:
count = get_table_row_count(buffer_conn, table)
stats["buffer_db_tables"][table] = count
status = f"{count}" if count >= 0 else "表不存在"
print(f" {table:30s} {status}")
buffer_conn.close()
# 统计 futures_analysis.db
analysis_conn = sqlite3.connect(str(analysis_db_path))
print(f"\n futures_analysis.db ({analysis_db_path}):")
for table in ANALYSIS_DB_TABLES:
count = get_table_row_count(analysis_conn, table)
stats["analysis_db_tables"][table] = count
status = f"{count}" if count >= 0 else "表不存在"
print(f" {table:30s} {status}")
analysis_conn.close()
total_rows = sum(v for v in stats["buffer_db_tables"].values() if v > 0)
total_rows += sum(v for v in stats["analysis_db_tables"].values() if v > 0)
print(f"\n 总计: {total_rows} 行数据待迁移")
# -----------------------------------------------------------------------
# 3. 如果 dry_run到此结束
# -----------------------------------------------------------------------
if dry_run:
print("\n[DRY RUN] 预览完成,未执行实际迁移")
return stats
# -----------------------------------------------------------------------
# 4. 备份目标数据库(如果已存在)
# -----------------------------------------------------------------------
if target_db.exists():
backup_path = backup_database(target_db)
print(f"\n[BACKUP] 已备份目标数据库: {backup_path}")
# 确保目标目录存在
target_db.parent.mkdir(parents=True, exist_ok=True)
# -----------------------------------------------------------------------
# 5. 创建目标数据库并建表
# -----------------------------------------------------------------------
print(f"\n[TARGET] 创建目标数据库: {target_db}")
# 使用V2项目的模型创建表
# 导入V2模型并创建表
sys.path.insert(0, str(Path(__file__).parent.parent))
from app.database import Base, engine
from app.models import ( # noqa: F401
MarketData,
SymbolTimestamp,
ScheduledTask,
User,
Session,
FuturesAnalysis,
WatchedSymbol,
AIModelConfig,
AnalysisSettings,
AIAnalysisCache,
ReviewDate,
SymbolRanking,
TradingPlan,
SymbolScoreV2,
TradingPlanV2,
SectorHeat,
ReviewPlanV2,
TradeRecord,
TradeImportBatch,
)
import asyncio
async def create_tables():
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
asyncio.run(create_tables())
print("[OK] 目标数据库表已创建")
# -----------------------------------------------------------------------
# 6. 迁移 buffer.db 数据
# -----------------------------------------------------------------------
print("\n" + "-" * 60)
print("迁移 buffer.db 数据...")
print("-" * 60)
# 使用 SQLite 的 ATTACH 方式直接迁移(最高效)
target_conn = sqlite3.connect(str(target_db))
target_conn.execute("PRAGMA journal_mode=WAL")
target_conn.execute("PRAGMA synchronous=NORMAL")
buffer_conn = sqlite3.connect(str(buffer_db_path))
for table in BUFFER_DB_TABLES:
if not check_table_exists(buffer_conn, table):
print(f" [SKIP] {table}: 源表不存在")
continue
source_count = get_table_row_count(buffer_conn, table)
if source_count == 0:
print(f" [SKIP] {table}: 源表为空")
continue
# 获取源表和目标表的列名,取交集
source_cols = get_table_columns(buffer_conn, table)
target_cols = get_table_columns(target_conn, table)
common_cols = [c for c in source_cols if c in target_cols]
if not common_cols:
print(f" [ERROR] {table}: 无匹配列")
stats["errors"].append(f"{table}: 无匹配列")
continue
cols_str = ", ".join(common_cols)
try:
# 从源数据库读取数据
cursor = buffer_conn.execute(f"SELECT {cols_str} FROM {table}")
rows = cursor.fetchall()
if not rows:
print(f" [SKIP] {table}: 无数据")
continue
# 插入到目标数据库
placeholders = ", ".join(["?"] * len(common_cols))
insert_sql = f"INSERT OR REPLACE INTO {table} ({cols_str}) VALUES ({placeholders})"
target_conn.executemany(insert_sql, rows)
target_conn.commit()
migrated_count = len(rows)
print(f" [OK] {table}: 迁移 {migrated_count}")
except sqlite3.Error as e:
print(f" [ERROR] {table}: {e}")
stats["errors"].append(f"{table}: {e}")
buffer_conn.close()
# -----------------------------------------------------------------------
# 7. 迁移 futures_analysis.db 数据
# -----------------------------------------------------------------------
print("\n" + "-" * 60)
print("迁移 futures_analysis.db 数据...")
print("-" * 60)
analysis_conn = sqlite3.connect(str(analysis_db_path))
for table in ANALYSIS_DB_TABLES:
if not check_table_exists(analysis_conn, table):
print(f" [SKIP] {table}: 源表不存在")
continue
source_count = get_table_row_count(analysis_conn, table)
if source_count == 0:
print(f" [SKIP] {table}: 源表为空")
continue
# 获取源表和目标表的列名,取交集
source_cols = get_table_columns(analysis_conn, table)
target_cols = get_table_columns(target_conn, table)
common_cols = [c for c in source_cols if c in target_cols]
if not common_cols:
print(f" [ERROR] {table}: 无匹配列")
stats["errors"].append(f"{table}: 无匹配列")
continue
cols_str = ", ".join(common_cols)
try:
# 从源数据库读取数据
cursor = analysis_conn.execute(f"SELECT {cols_str} FROM {table}")
rows = cursor.fetchall()
if not rows:
print(f" [SKIP] {table}: 无数据")
continue
# 插入到目标数据库
placeholders = ", ".join(["?"] * len(common_cols))
insert_sql = f"INSERT OR REPLACE INTO {table} ({cols_str}) VALUES ({placeholders})"
target_conn.executemany(insert_sql, rows)
target_conn.commit()
migrated_count = len(rows)
print(f" [OK] {table}: 迁移 {migrated_count}")
except sqlite3.Error as e:
print(f" [ERROR] {table}: {e}")
stats["errors"].append(f"{table}: {e}")
analysis_conn.close()
# -----------------------------------------------------------------------
# 8. 修复自增ID序列
# -----------------------------------------------------------------------
print("\n" + "-" * 60)
print("修复自增ID序列...")
print("-" * 60)
all_tables = BUFFER_DB_TABLES + ANALYSIS_DB_TABLES
for table in all_tables:
if not check_table_exists(target_conn, table):
continue
try:
# 获取当前最大ID
cursor = target_conn.execute(f"SELECT MAX(id) FROM {table}")
max_id = cursor.fetchone()[0]
if max_id is not None:
# 更新 sqlite_sequence 表
target_conn.execute(
"INSERT OR REPLACE INTO sqlite_sequence (name, seq) VALUES (?, ?)",
(table, max_id),
)
print(f" [OK] {table}: max_id = {max_id}")
except sqlite3.Error:
pass # 某些表可能没有id列
target_conn.commit()
# -----------------------------------------------------------------------
# 9. 验证迁移结果
# -----------------------------------------------------------------------
print("\n" + "-" * 60)
print("验证迁移结果...")
print("-" * 60)
migrated_total = 0
for table in all_tables:
if not check_table_exists(target_conn, table):
print(f" [WARN] {table}: 目标表不存在")
continue
count = get_table_row_count(target_conn, table)
if count > 0:
print(f" {table:30s} {count}")
migrated_total += count
target_conn.close()
print(f"\n 迁移总计: {migrated_total}")
# -----------------------------------------------------------------------
# 10. 完成
# -----------------------------------------------------------------------
print("\n" + "=" * 60)
if stats["errors"]:
print(f"迁移完成,但有 {len(stats['errors'])} 个错误:")
for err in stats["errors"]:
print(f" - {err}")
else:
print("迁移完成,无错误!")
print("=" * 60)
return stats
def main():
parser = argparse.ArgumentParser(description="数据库迁移脚本")
parser.add_argument(
"--source-dir",
type=Path,
default=DEFAULT_SOURCE_DIR,
help=f"原始数据库所在目录 (默认: {DEFAULT_SOURCE_DIR})",
)
parser.add_argument(
"--target-db",
type=Path,
default=DEFAULT_TARGET_DB,
help=f"目标V2数据库路径 (默认: {DEFAULT_TARGET_DB})",
)
parser.add_argument(
"--dry-run",
action="store_true",
help="仅预览迁移,不实际执行",
)
args = parser.parse_args()
print(f"\n配置:")
print(f" 源目录: {args.source_dir}")
print(f" 目标数据库: {args.target_db}")
print(f" 模式: {'预览' if args.dry_run else '执行'}")
print()
stats = migrate_database(
source_dir=args.source_dir,
target_db=args.target_db,
dry_run=args.dry_run,
)
# 退出码
if stats["errors"]:
sys.exit(1)
sys.exit(0)
if __name__ == "__main__":
main()