fix(repos): int 强转跳过 String 声明列(列类型优先于 _INT_FIELDS 按名集合)
Task 1 评审遗留:_INT_FIELDS 含 type_raw(为 Message int code 加),但
MomentsPost.type_raw 是 String(16)——按名强转会把非数字 code(如
'text')置 None,违背列声明。_build_column_values 现在检查目标列
声明类型:isinstance(col.type, sa.String)(含 Text)则跳过强转,
Message.type_raw(Integer) 照旧。另修正 upsert_chat_group 文档串里
过时的 'PK = room_id'(复合键自 8a99940)。
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -36,6 +36,7 @@ from app.db.models import (
|
||||
WhatsappAccount,
|
||||
)
|
||||
from app.storage.repos import (
|
||||
_build_column_values,
|
||||
_coerce_bool,
|
||||
_coerce_int,
|
||||
upsert_chat_group,
|
||||
@@ -1459,3 +1460,63 @@ async def test_upsert_moments_posts_idempotent_keeps_json_lists_and_coerces_crea
|
||||
assert rows[0].type_raw == "1" # MomentsPost type_raw is a string code
|
||||
finally:
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# int 强转 vs String 列声明(2026-08-25 评审遗留修复)
|
||||
# _INT_FIELDS 按列名全局命中,但列类型优先:目标列声明为 String(Text 继承)
|
||||
# 则跳过强转 —— MomentsPost.type_raw 是 String(16),非数字 code(如 "text")
|
||||
# 不能被 _coerce_int 置 None;Message.type_raw(Integer)照旧强转。
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
async def test_int_coercion_skipped_for_string_columns():
|
||||
"""String 声明的 type_raw 原样保留;Integer 声明的照旧强转。"""
|
||||
# MomentsPost.type_raw = String(16) -> 不强转
|
||||
vals = _build_column_values(
|
||||
MomentsPost, {"id": "x", "wechat_id": "w", "type_raw": "1"}
|
||||
)
|
||||
assert vals["type_raw"] == "1"
|
||||
vals = _build_column_values(
|
||||
MomentsPost, {"id": "x", "wechat_id": "w", "type_raw": "text"}
|
||||
)
|
||||
assert vals["type_raw"] == "text"
|
||||
|
||||
# Message.type_raw = Integer -> "1" -> 1,非数字 -> None
|
||||
vals = _build_column_values(
|
||||
Message, {"msg_id": "m", "channel": "wechat", "type_raw": "1"}
|
||||
)
|
||||
assert vals["type_raw"] == 1
|
||||
vals = _build_column_values(
|
||||
Message, {"msg_id": "m", "channel": "wechat", "type_raw": "text"}
|
||||
)
|
||||
assert vals["type_raw"] is None
|
||||
|
||||
|
||||
async def test_moments_post_non_numeric_type_raw_round_trip():
|
||||
"""非数字 type_raw code 入库读回仍是原字符串(不被 _coerce_int 置 None)。"""
|
||||
engine = await _make_engine()
|
||||
try:
|
||||
SessionLocal = async_sessionmaker(engine, expire_on_commit=False)
|
||||
async with SessionLocal() as session:
|
||||
await upsert_moments_posts(
|
||||
session,
|
||||
[
|
||||
{
|
||||
"id": "p1",
|
||||
"wechat_id": "wxid_x",
|
||||
"type_raw": "video", # 假设未来出现的非数字 code
|
||||
"create_time": 1776302051000,
|
||||
}
|
||||
],
|
||||
)
|
||||
await session.commit()
|
||||
rows = (
|
||||
await session.execute(
|
||||
sa.select(MomentsPost).execution_options(populate_existing=True)
|
||||
)
|
||||
).scalars().all()
|
||||
assert len(rows) == 1
|
||||
assert rows[0].type_raw == "video"
|
||||
finally:
|
||||
await engine.dispose()
|
||||
|
||||
Reference in New Issue
Block a user