Files
zkxw/backend/tests/test_child_row_sync.py
zlwuandCursor 5e563a6669 fix: 子表保存改为就地同步,避免整批换 id 打断外键。
协议样本、生产产出和盘点明细按行更新并保留原 id;补齐存储证明质检预填、离职人员显示名,以及样本采集通知模板字段。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-28 10:51:53 +08:00

209 lines
6.6 KiB
Python

"""子表按行同步:更新原行、新增缺行、只软删去掉的行。"""
import unittest
from datetime import datetime
from sqlalchemy import Column, DateTime, Integer, SmallInteger, String, create_engine
from sqlalchemy.orm import sessionmaker
from app.core.database import Base
from app.crud.child_row_sync import sync_child_rows
class DemoChild(Base):
__tablename__ = "demo_child_row"
id = Column(String(64), primary_key=True)
parent_id = Column(String(64), nullable=False)
tenant_no = Column(String(64), nullable=False)
item_code = Column(String(64))
item_name = Column(String(128))
order_num = Column(Integer)
create_time = Column(DateTime)
update_by = Column(String(64))
update_time = Column(DateTime)
del_flag = Column(SmallInteger, nullable=False, default=0)
def _session():
engine = create_engine("sqlite://")
DemoChild.__table__.create(bind=engine)
return sessionmaker(bind=engine)()
def _row(**kwargs):
now = datetime(2026, 10, 27, 9, 0, 0)
data = dict(
parent_id="parent-1",
tenant_no="tenant-a",
item_code="R-1",
item_name="试剂",
order_num=0,
create_time=now,
update_time=now,
del_flag=0,
)
data.update(kwargs)
return DemoChild(**data)
def _sync(db, items):
now = datetime(2026, 10, 27, 10, 0, 0)
def build_row(row_id):
return DemoChild(
id=row_id,
parent_id="parent-1",
tenant_no="tenant-a",
create_time=now,
)
def apply_row(row, item, idx, _now):
row.item_code = item.get("item_code")
row.item_name = item.get("item_name")
row.order_num = idx
return sync_child_rows(
db,
DemoChild,
parent_attr="parent_id",
parent_id="parent-1",
tenant_no="tenant-a",
items=items,
user_id="user-1",
now=now,
build_row=build_row,
apply_fields=apply_row,
natural_key=lambda item: (item.get("item_code") or "").strip(),
row_natural_key=lambda row: (row.item_code or "").strip(),
)
class TestChildRowSync(unittest.TestCase):
def test_id_keeps_row_when_fields_change(self):
db = _session()
db.add(_row(id="row-keep", item_name="旧名称"))
db.commit()
saved = _sync(db, [{"id": "row-keep", "item_code": "R-2", "item_name": "新名称"}])
self.assertEqual(saved[0].id, "row-keep")
self.assertEqual(saved[0].item_name, "新名称")
self.assertEqual(db.query(DemoChild).filter_by(del_flag=0).count(), 1)
def test_natural_key_keeps_id_when_payload_omits_id(self):
db = _session()
db.add(_row(id="row-keep", item_code="R-1"))
db.commit()
saved = _sync(db, [{"item_code": "R-1", "item_name": "改名"}])
self.assertEqual([row.id for row in saved], ["row-keep"])
self.assertEqual(saved[0].item_name, "改名")
def test_insert_update_and_soft_delete_removed(self):
db = _session()
db.add(_row(id="row-keep", item_code="R-1"))
db.add(_row(id="row-drop", item_code="R-9", item_name="去掉"))
db.commit()
saved = _sync(
db,
[
{"id": "row-keep", "item_code": "R-1", "item_name": "留下"},
{"item_code": "R-NEW", "item_name": "新增"},
],
)
self.assertEqual(saved[0].id, "row-keep")
self.assertNotEqual(saved[1].id, "row-drop")
rows = {row.id: row for row in db.query(DemoChild).all()}
self.assertEqual(rows["row-keep"].del_flag, 0)
self.assertEqual(rows["row-drop"].del_flag, 1)
self.assertEqual(rows[saved[1].id].del_flag, 0)
def test_id_from_another_parent_is_rejected(self):
db = _session()
db.add(_row(id="row-other", parent_id="parent-2"))
db.commit()
with self.assertRaises(ValueError):
_sync(db, [{"id": "row-other", "item_code": "R-1", "item_name": "占用"}])
other = db.query(DemoChild).filter_by(id="row-other").one()
self.assertEqual(other.parent_id, "parent-2")
self.assertEqual(other.del_flag, 0)
def test_readded_business_key_restores_deleted_row(self):
db = _session()
db.add(_row(id="row-old", item_code="R-1", del_flag=1))
db.commit()
now = datetime(2026, 10, 27, 10, 0, 0)
def build_row(row_id):
return DemoChild(
id=row_id,
parent_id="parent-1",
tenant_no="tenant-a",
create_time=now,
)
def apply_row(row, item, idx, _now):
row.item_code = item.get("item_code")
row.item_name = item.get("item_name")
row.order_num = idx
saved = sync_child_rows(
db,
DemoChild,
parent_attr="parent_id",
parent_id="parent-1",
tenant_no="tenant-a",
items=[{"item_code": "R-1", "item_name": "恢复"}],
user_id="user-1",
now=now,
build_row=build_row,
apply_fields=apply_row,
natural_key=lambda item: (item.get("item_code") or "").strip(),
row_natural_key=lambda row: (row.item_code or "").strip(),
reuse_deleted=True,
)
db.commit()
self.assertEqual(saved[0].id, "row-old")
self.assertEqual(saved[0].del_flag, 0)
self.assertEqual(db.query(DemoChild).count(), 1)
def test_removing_live_row_clears_older_tombstone(self):
db = _session()
db.add(_row(id="row-live", item_code="R-1"))
db.add(_row(id="row-tomb", item_code="R-1", del_flag=1))
db.commit()
now = datetime(2026, 10, 27, 10, 0, 0)
sync_child_rows(
db,
DemoChild,
parent_attr="parent_id",
parent_id="parent-1",
tenant_no="tenant-a",
items=[],
user_id="user-1",
now=now,
build_row=lambda row_id: DemoChild(
id=row_id, parent_id="parent-1", tenant_no="tenant-a", create_time=now
),
apply_fields=lambda row, item, idx, _now: None,
natural_key=lambda item: (item.get("item_code") or "").strip(),
row_natural_key=lambda row: (row.item_code or "").strip(),
reuse_deleted=True,
)
db.commit()
self.assertIsNone(db.query(DemoChild).filter_by(id="row-tomb").first())
live = db.query(DemoChild).filter_by(id="row-live").one()
self.assertEqual(live.del_flag, 1)
if __name__ == "__main__":
unittest.main()