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

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

"""
数据库迁移脚本将原版两个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()