""" DAO 写库回归测试:验证 upsert(INSERT ... ON DUPLICATE KEY UPDATE)而非 pandas replace, 确保不 DROP 表、不丢失数据、DELETE+INSERT 不再分事务。 不使用真实 DB(用假 engine 捕获 SQL)。 """ import sys import os sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) import pandas as pd import pytest from unittest.mock import patch from database import dao from database.models import StockDaily from sqlalchemy.dialects import mysql class _RecConn: """记录最近一次执行的 SQL。""" def __init__(self): self.sql = None def begin(self): return self def __enter__(self): return self def __exit__(self, *a): pass def execute(self, stmt): self.sql = str(stmt.compile(dialect=mysql.dialect(), compile_kwargs={"literal_binds": True})) return type("R", (), {"rowcount": 2})() class _RecEngine: def __init__(self): self.conn = _RecConn() def begin(self): return self.conn def _daily_df(): return pd.DataFrame({ 'ts_code': ['000001.SZ', '600519.SH'], 'trade_date': ['20260601', '20260601'], 'open': [10.1, 99.0], 'high': [10.8, 101.0], 'low': [10.0, 98.0], 'close': [10.5, 100.0], 'pre_close': [10.2, 99.5], 'change': [0.3, 0.5], 'pct_chg': [2.94, 0.50], 'vol': [100, 200], 'amount': [1050, 19900], 'turnover_rate': [0.5, 0.2], }) def test_save_daily_generates_upsert_not_replace(): eng = _RecEngine() with patch('database.dao.get_engine', return_value=eng): dao.save_daily(_daily_df()) sql = eng.conn.sql assert "INSERT INTO" in sql and "mac_stock_daily" in sql assert "ON DUPLICATE KEY UPDATE" in sql, "save_daily 必须用 upsert,避免旧版 DELETE+INSERT" # 确保不会走 pandas replace(整表重建) assert "DROP TABLE" not in sql.upper() def test_save_daily_no_multi_transaction(): """save_daily 只构造一次 upsert 语句,DELETE 与 INSERT 合并为原子语句。""" eng = _RecEngine() with patch('database.dao.get_engine', return_value=eng): dao.save_daily(_daily_df()) # 生成的 SQL 不含独立 DELETE,确保无"删除已发生但插入失败"的非原子风险 assert "DELETE" not in eng.conn.sql.upper()