| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208 |
- """子表按行同步:更新原行、新增缺行、只软删去掉的行。"""
- 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()
|