修复:强化装填事务一致性

This commit is contained in:
焦龙言
2026-07-26 17:56:58 +08:00
parent 18abe861f9
commit f6e994d95f
4 changed files with 182 additions and 22 deletions
@@ -117,7 +117,7 @@ class MasterDao:
) -> Any | None: ) -> Any | None:
statement = select(model).where(id_column == resource_id) statement = select(model).where(id_column == resource_id)
if for_update: if for_update:
statement = statement.with_for_update() statement = statement.with_for_update().execution_options(populate_existing=True)
result = await db.execute(statement) result = await db.execute(statement)
return result.scalar_one_or_none() return result.scalar_one_or_none()
@@ -187,8 +187,20 @@ class MasterDao:
return await cls._paginate(db, statement, query_object, is_page) return await cls._paginate(db, statement, query_object, is_page)
@classmethod @classmethod
async def get_strain(cls, db: AsyncSession, strain_id: int) -> BizStrain | None: async def get_strain(
return await cls._get(db, BizStrain, BizStrain.strain_id, strain_id) cls,
db: AsyncSession,
strain_id: int,
*,
for_update: bool = False,
) -> BizStrain | None:
return await cls._get(
db,
BizStrain,
BizStrain.strain_id,
strain_id,
for_update=for_update,
)
@classmethod @classmethod
async def find_strain_by_code(cls, db: AsyncSession, strain_code: str) -> BizStrain | None: async def find_strain_by_code(cls, db: AsyncSession, strain_code: str) -> BizStrain | None:
@@ -96,6 +96,20 @@ class FillingService:
raise ServiceException(message=admin_message) raise ServiceException(message=admin_message)
raise PermissionException(data='', message='无权访问该托盘') raise PermissionException(data='', message='无权访问该托盘')
@classmethod
async def _assert_pallet_warehouse_access(
cls,
query_db: AsyncSession,
current_user: CurrentUserModel,
warehouse_id: int,
) -> None:
try:
await AccessService.assert_warehouse_access(query_db, current_user, warehouse_id)
except PermissionException:
if cls._is_admin(current_user):
raise
cls._raise_pallet_not_accessible(current_user)
@staticmethod @staticmethod
def _validate_empty_warehouse_pallet( def _validate_empty_warehouse_pallet(
pallet: BizPallet, pallet: BizPallet,
@@ -232,7 +246,7 @@ class FillingService:
cls._raise_pallet_not_accessible(current_user, '托盘库存不存在') cls._raise_pallet_not_accessible(current_user, '托盘库存不存在')
if inventory.warehouse_id is None: if inventory.warehouse_id is None:
cls._raise_pallet_not_accessible(current_user, '托盘不在仓库') cls._raise_pallet_not_accessible(current_user, '托盘不在仓库')
await AccessService.assert_warehouse_access( await cls._assert_pallet_warehouse_access(
query_db, query_db,
current_user, current_user,
inventory.warehouse_id, inventory.warehouse_id,
@@ -279,8 +293,6 @@ class FillingService:
warehouse_ids=warehouse_ids, warehouse_ids=warehouse_ids,
) )
if recovered is None: if recovered is None:
if not cls._is_admin(current_user):
raise PermissionException(data='', message='无权访问该装填记录')
raise ServiceException(message='装填失败,数据冲突') raise ServiceException(message='装填失败,数据冲突')
await AccessService.assert_warehouse_access( await AccessService.assert_warehouse_access(
query_db, query_db,
@@ -330,11 +342,6 @@ class FillingService:
current_user, current_user,
page_object.warehouse_id, page_object.warehouse_id,
) )
warehouse = await MasterDao.get_warehouse(query_db, page_object.warehouse_id)
if warehouse is None:
raise ServiceException(message='仓库不存在')
if warehouse.status != '0':
raise ServiceException(message='仓库已停用')
scanned_pallet = await InventoryDao.get_pallet_by_qr_token( scanned_pallet = await InventoryDao.get_pallet_by_qr_token(
query_db, query_db,
@@ -351,7 +358,7 @@ class FillingService:
cls._raise_pallet_not_accessible(current_user, '托盘库存不存在') cls._raise_pallet_not_accessible(current_user, '托盘库存不存在')
if inventory.warehouse_id is None: if inventory.warehouse_id is None:
cls._raise_pallet_not_accessible(current_user, '托盘不在仓库') cls._raise_pallet_not_accessible(current_user, '托盘不在仓库')
await AccessService.assert_warehouse_access( await cls._assert_pallet_warehouse_access(
query_db, query_db,
current_user, current_user,
inventory.warehouse_id, inventory.warehouse_id,
@@ -380,6 +387,23 @@ class FillingService:
current_user, current_user,
) )
warehouse = await MasterDao.get_warehouse(
query_db,
page_object.warehouse_id,
for_update=True,
)
if warehouse is None:
raise ServiceException(message='仓库不存在')
if warehouse.status != '0':
raise ServiceException(message='仓库已停用')
strain = cls._validate_active_strain(
await MasterDao.get_strain(
query_db,
page_object.strain_id,
for_update=True,
)
)
cls._validate_empty_warehouse_pallet( cls._validate_empty_warehouse_pallet(
pallet, pallet,
inventory, inventory,
@@ -391,7 +415,6 @@ class FillingService:
PalletFlowState(inventory.flow_state), PalletFlowState(inventory.flow_state),
PalletFlowState.FILLED_IN_WAREHOUSE, PalletFlowState.FILLED_IN_WAREHOUSE,
) )
strain = cls._validate_active_strain(await MasterDao.get_strain(query_db, page_object.strain_id))
current_time = datetime.now() current_time = datetime.now()
filling = BizFillingRecord( filling = BizFillingRecord(
@@ -332,10 +332,12 @@ class TestFillingReadServices(unittest.IsolatedAsyncioTestCase):
new=AsyncMock(return_value=make_inventory(flow_state=PalletFlowState.FILLED_IN_WAREHOUSE.value)), new=AsyncMock(return_value=make_inventory(flow_state=PalletFlowState.FILLED_IN_WAREHOUSE.value)),
), ),
patch.object(AccessService, 'assert_warehouse_access', new=access), patch.object(AccessService, 'assert_warehouse_access', new=access),
self.assertRaises(PermissionException), self.assertRaises(PermissionException) as raised,
): ):
await FillingService.scan_filling_pallet_services(self.db, 'qr-2', self.user) await FillingService.scan_filling_pallet_services(self.db, 'qr-2', self.user)
access.assert_awaited_once_with(self.db, self.user, 7) access.assert_awaited_once_with(self.db, self.user, 7)
self.assertEqual('', raised.exception.data)
self.assertEqual('无权访问该托盘', raised.exception.message)
async def test_scan_hides_nonexistent_pallet_from_normal_user_but_admin_gets_precise_error(self) -> None: async def test_scan_hides_nonexistent_pallet_from_normal_user_but_admin_gets_precise_error(self) -> None:
for user, exception_type in ((self.user, PermissionException), (make_user(1), ServiceException)): for user, exception_type in ((self.user, PermissionException), (make_user(1), ServiceException)):
@@ -381,7 +383,11 @@ class TestFillPalletService(unittest.IsolatedAsyncioTestCase):
actions.append('pallet-lock') actions.append('pallet-lock')
return [self.pallet] return [self.pallet]
async def get_strain(*_args: Any) -> BizStrain: async def get_warehouse(*_args: Any, **_kwargs: Any) -> SimpleNamespace:
actions.append('warehouse-read')
return self.warehouse
async def get_strain(*_args: Any, **_kwargs: Any) -> BizStrain:
actions.append('strain-read') actions.append('strain-read')
return self.strain return self.strain
@@ -408,6 +414,8 @@ class TestFillPalletService(unittest.IsolatedAsyncioTestCase):
captured['filling'].__dict__.pop('filling_id', None) captured['filling'].__dict__.pop('filling_id', None)
self.db.commit.side_effect = commit self.db.commit.side_effect = commit
warehouse_read = AsyncMock(side_effect=get_warehouse)
strain_read = AsyncMock(side_effect=get_strain)
with ( with (
patch.object(AccessDao, 'list_user_warehouse_ids', new=AsyncMock(return_value=[7])), patch.object(AccessDao, 'list_user_warehouse_ids', new=AsyncMock(return_value=[7])),
patch.object(AccessService, 'assert_warehouse_access', new=AsyncMock()), patch.object(AccessService, 'assert_warehouse_access', new=AsyncMock()),
@@ -415,8 +423,8 @@ class TestFillPalletService(unittest.IsolatedAsyncioTestCase):
patch.object(InventoryDao, 'get_pallet_by_qr_token', new=AsyncMock(return_value=self.pallet)), patch.object(InventoryDao, 'get_pallet_by_qr_token', new=AsyncMock(return_value=self.pallet)),
patch.object(InventoryDao, 'get_inventory_for_update', new=AsyncMock(side_effect=lock_inventory)), patch.object(InventoryDao, 'get_inventory_for_update', new=AsyncMock(side_effect=lock_inventory)),
patch.object(InventoryDao, 'get_pallets_by_ids', new=AsyncMock(side_effect=lock_pallets)), patch.object(InventoryDao, 'get_pallets_by_ids', new=AsyncMock(side_effect=lock_pallets)),
patch.object(MasterDao, 'get_warehouse', new=AsyncMock(return_value=self.warehouse)), patch.object(MasterDao, 'get_warehouse', new=warehouse_read),
patch.object(MasterDao, 'get_strain', new=AsyncMock(side_effect=get_strain)), patch.object(MasterDao, 'get_strain', new=strain_read),
patch.object(OperationDao, 'add_filling_record', new=AsyncMock(side_effect=add_filling)), patch.object(OperationDao, 'add_filling_record', new=AsyncMock(side_effect=add_filling)),
patch.object(InventoryDao, 'add_event', new=AsyncMock(side_effect=add_event)), patch.object(InventoryDao, 'add_event', new=AsyncMock(side_effect=add_event)),
patch('module_business.service.filling_service.datetime') as mocked_datetime, patch('module_business.service.filling_service.datetime') as mocked_datetime,
@@ -425,9 +433,19 @@ class TestFillPalletService(unittest.IsolatedAsyncioTestCase):
result = await FillingService.fill_pallet_services(self.db, self.page, self.user) result = await FillingService.fill_pallet_services(self.db, self.page, self.user)
self.assertEqual( self.assertEqual(
['inventory-lock', 'pallet-lock', 'strain-read', 'filling-flush', 'event-flush', 'commit'], [
'inventory-lock',
'pallet-lock',
'warehouse-read',
'strain-read',
'filling-flush',
'event-flush',
'commit',
],
actions, actions,
) )
warehouse_read.assert_awaited_once_with(self.db, 7, for_update=True)
strain_read.assert_awaited_once_with(self.db, 11, for_update=True)
filling = captured['filling'] filling = captured['filling']
self.assertTrue(filling.filling_no.startswith('FL-')) self.assertTrue(filling.filling_no.startswith('FL-'))
self.assertEqual(2, filling.pallet_id) self.assertEqual(2, filling.pallet_id)
@@ -514,6 +532,49 @@ class TestFillPalletService(unittest.IsolatedAsyncioTestCase):
async def test_fill_rejects_packages_over_persisted_pallet_capacity(self) -> None: async def test_fill_rejects_packages_over_persisted_pallet_capacity(self) -> None:
await self._assert_fill_rejected(page=make_page(initial_packages=21)) await self._assert_fill_rejected(page=make_page(initial_packages=21))
async def test_fill_rejects_stopped_warehouse_only_after_core_locks(self) -> None:
actions: list[str] = []
async def lock_inventory(*_args: Any) -> BizPalletInventory:
actions.append('inventory-lock')
return self.inventory
async def lock_pallets(*_args: Any) -> list[BizPallet]:
actions.append('pallet-lock')
return [self.pallet]
async def lock_warehouse(*_args: Any, **_kwargs: Any) -> SimpleNamespace:
actions.append('warehouse-read')
return SimpleNamespace(warehouse_id=7, status='1')
warehouse_read = AsyncMock(side_effect=lock_warehouse)
strain_read = AsyncMock()
add_filling = AsyncMock()
add_event = AsyncMock()
with (
patch.object(AccessDao, 'list_user_warehouse_ids', new=AsyncMock(return_value=[7])),
patch.object(AccessService, 'assert_warehouse_access', new=AsyncMock()),
patch.object(OperationDao, 'get_filling_by_idempotency_key', new=AsyncMock(return_value=None)),
patch.object(InventoryDao, 'get_pallet_by_qr_token', new=AsyncMock(return_value=self.pallet)),
patch.object(InventoryDao, 'get_inventory_for_update', new=AsyncMock(side_effect=lock_inventory)),
patch.object(InventoryDao, 'get_pallets_by_ids', new=AsyncMock(side_effect=lock_pallets)),
patch.object(MasterDao, 'get_warehouse', new=warehouse_read),
patch.object(MasterDao, 'get_strain', new=strain_read),
patch.object(OperationDao, 'add_filling_record', new=add_filling),
patch.object(InventoryDao, 'add_event', new=add_event),
self.assertRaises(ServiceException) as raised,
):
await FillingService.fill_pallet_services(self.db, self.page, self.user)
self.assertEqual('仓库已停用', raised.exception.message)
self.assertEqual(['inventory-lock', 'pallet-lock', 'warehouse-read'], actions)
warehouse_read.assert_awaited_once_with(self.db, 7, for_update=True)
strain_read.assert_not_awaited()
add_filling.assert_not_awaited()
add_event.assert_not_awaited()
self.db.commit.assert_not_awaited()
self.db.rollback.assert_awaited_once()
async def test_fill_rejects_stopped_pallet_and_stopped_or_missing_strain(self) -> None: async def test_fill_rejects_stopped_pallet_and_stopped_or_missing_strain(self) -> None:
await self._assert_fill_rejected(pallet=make_pallet(asset_status='1')) await self._assert_fill_rejected(pallet=make_pallet(asset_status='1'))
self.db.reset_mock() self.db.reset_mock()
@@ -556,6 +617,8 @@ class TestFillPalletService(unittest.IsolatedAsyncioTestCase):
existing = make_filling() existing = make_filling()
inventory_lock = AsyncMock() inventory_lock = AsyncMock()
access = AsyncMock() access = AsyncMock()
warehouse_read = AsyncMock()
strain_read = AsyncMock()
async def rollback() -> None: async def rollback() -> None:
existing.__dict__.pop('filling_id', None) existing.__dict__.pop('filling_id', None)
@@ -568,12 +631,16 @@ class TestFillPalletService(unittest.IsolatedAsyncioTestCase):
OperationDao, 'get_filling_by_idempotency_key', new=AsyncMock(return_value=existing) OperationDao, 'get_filling_by_idempotency_key', new=AsyncMock(return_value=existing)
) as get_key, ) as get_key,
patch.object(InventoryDao, 'get_inventory_for_update', new=inventory_lock), patch.object(InventoryDao, 'get_inventory_for_update', new=inventory_lock),
patch.object(MasterDao, 'get_warehouse', new=warehouse_read),
patch.object(MasterDao, 'get_strain', new=strain_read),
): ):
result = await FillingService.fill_pallet_services(self.db, self.page, self.user) result = await FillingService.fill_pallet_services(self.db, self.page, self.user)
get_key.assert_awaited_once_with(self.db, 'fill-key', warehouse_ids=[7]) get_key.assert_awaited_once_with(self.db, 'fill-key', warehouse_ids=[7])
access.assert_awaited_once_with(self.db, self.user, 7) access.assert_awaited_once_with(self.db, self.user, 7)
inventory_lock.assert_not_awaited() inventory_lock.assert_not_awaited()
warehouse_read.assert_not_awaited()
strain_read.assert_not_awaited()
self.db.commit.assert_not_awaited() self.db.commit.assert_not_awaited()
self.db.rollback.assert_awaited_once() self.db.rollback.assert_awaited_once()
self.assertTrue(result.is_success) self.assertTrue(result.is_success)
@@ -615,6 +682,9 @@ class TestFillPalletService(unittest.IsolatedAsyncioTestCase):
raise PermissionException(data='', message='无权访问该仓库') raise PermissionException(data='', message='无权访问该仓库')
access = AsyncMock(side_effect=assert_access) access = AsyncMock(side_effect=assert_access)
strain_read = AsyncMock()
add_filling = AsyncMock()
add_event = AsyncMock()
with ( with (
patch.object(AccessDao, 'list_user_warehouse_ids', new=AsyncMock(return_value=[7])), patch.object(AccessDao, 'list_user_warehouse_ids', new=AsyncMock(return_value=[7])),
patch.object(AccessService, 'assert_warehouse_access', new=access), patch.object(AccessService, 'assert_warehouse_access', new=access),
@@ -623,12 +693,20 @@ class TestFillPalletService(unittest.IsolatedAsyncioTestCase):
patch.object(InventoryDao, 'get_inventory_for_update', new=AsyncMock(return_value=inventory)), patch.object(InventoryDao, 'get_inventory_for_update', new=AsyncMock(return_value=inventory)),
patch.object(InventoryDao, 'get_pallets_by_ids', new=pallet_lock), patch.object(InventoryDao, 'get_pallets_by_ids', new=pallet_lock),
patch.object(MasterDao, 'get_warehouse', new=AsyncMock(return_value=self.warehouse)), patch.object(MasterDao, 'get_warehouse', new=AsyncMock(return_value=self.warehouse)),
self.assertRaises(PermissionException), patch.object(MasterDao, 'get_strain', new=strain_read),
patch.object(OperationDao, 'add_filling_record', new=add_filling),
patch.object(InventoryDao, 'add_event', new=add_event),
self.assertRaises(PermissionException) as raised,
): ):
await FillingService.fill_pallet_services(self.db, self.page, self.user) await FillingService.fill_pallet_services(self.db, self.page, self.user)
self.assertEqual([7, foreign_warehouse_id], [call.args[2] for call in access.await_args_list]) self.assertEqual([7, foreign_warehouse_id], [call.args[2] for call in access.await_args_list])
self.assertEqual('', raised.exception.data)
self.assertEqual('无权访问该托盘', raised.exception.message)
pallet_lock.assert_not_awaited() pallet_lock.assert_not_awaited()
strain_read.assert_not_awaited()
add_filling.assert_not_awaited()
add_event.assert_not_awaited()
self.db.rollback.assert_awaited_once() self.db.rollback.assert_awaited_once()
async def test_concurrent_same_key_recovers_after_waiting_for_inventory_lock(self) -> None: async def test_concurrent_same_key_recovers_after_waiting_for_inventory_lock(self) -> None:
@@ -655,6 +733,8 @@ class TestFillPalletService(unittest.IsolatedAsyncioTestCase):
add_filling = AsyncMock() add_filling = AsyncMock()
add_event = AsyncMock() add_event = AsyncMock()
warehouse_read = AsyncMock(return_value=self.warehouse)
strain_read = AsyncMock()
with ( with (
patch.object(AccessDao, 'list_user_warehouse_ids', new=AsyncMock(return_value=[7])), patch.object(AccessDao, 'list_user_warehouse_ids', new=AsyncMock(return_value=[7])),
patch.object(AccessService, 'assert_warehouse_access', new=AsyncMock()), patch.object(AccessService, 'assert_warehouse_access', new=AsyncMock()),
@@ -667,7 +747,8 @@ class TestFillPalletService(unittest.IsolatedAsyncioTestCase):
patch.object(InventoryDao, 'get_inventory_for_update', new=AsyncMock(side_effect=lock_inventory)), patch.object(InventoryDao, 'get_inventory_for_update', new=AsyncMock(side_effect=lock_inventory)),
patch.object(InventoryDao, 'get_pallets_by_ids', new=AsyncMock(side_effect=lock_pallet)), patch.object(InventoryDao, 'get_pallets_by_ids', new=AsyncMock(side_effect=lock_pallet)),
patch.object(OperationDao, 'get_active_filling_for_update', new=AsyncMock(side_effect=lock_active)), patch.object(OperationDao, 'get_active_filling_for_update', new=AsyncMock(side_effect=lock_active)),
patch.object(MasterDao, 'get_warehouse', new=AsyncMock(return_value=self.warehouse)), patch.object(MasterDao, 'get_warehouse', new=warehouse_read),
patch.object(MasterDao, 'get_strain', new=strain_read),
patch.object(OperationDao, 'add_filling_record', new=add_filling), patch.object(OperationDao, 'add_filling_record', new=add_filling),
patch.object(InventoryDao, 'add_event', new=add_event), patch.object(InventoryDao, 'add_event', new=add_event),
): ):
@@ -677,6 +758,8 @@ class TestFillPalletService(unittest.IsolatedAsyncioTestCase):
self.assertEqual(2, get_key.await_count) self.assertEqual(2, get_key.await_count)
self.assertEqual(2, self.db.rollback.await_count) self.assertEqual(2, self.db.rollback.await_count)
self.assertEqual(31, result.result) self.assertEqual(31, result.result)
warehouse_read.assert_not_awaited()
strain_read.assert_not_awaited()
add_filling.assert_not_awaited() add_filling.assert_not_awaited()
add_event.assert_not_awaited() add_event.assert_not_awaited()
self.db.commit.assert_not_awaited() self.db.commit.assert_not_awaited()
@@ -762,7 +845,30 @@ class TestFillPalletService(unittest.IsolatedAsyncioTestCase):
access.assert_any_await(self.db, self.user, 7) access.assert_any_await(self.db, self.user, 7)
self.assertTrue(result.is_success) self.assertTrue(result.is_success)
async def test_integrity_error_without_scoped_idempotent_result_is_hidden_and_never_succeeds(self) -> None: async def test_integrity_error_recovery_rejects_revoked_warehouse_access(self) -> None:
warehouse_scope = AsyncMock(side_effect=[[7], []])
key_reads = AsyncMock(return_value=None)
with (
patch.object(AccessDao, 'list_user_warehouse_ids', new=warehouse_scope),
patch.object(AccessService, 'assert_warehouse_access', new=AsyncMock()),
patch.object(OperationDao, 'get_filling_by_idempotency_key', new=key_reads),
patch.object(InventoryDao, 'get_pallet_by_qr_token', new=AsyncMock(return_value=self.pallet)),
patch.object(InventoryDao, 'get_inventory_for_update', new=AsyncMock(return_value=self.inventory)),
patch.object(InventoryDao, 'get_pallets_by_ids', new=AsyncMock(return_value=[self.pallet])),
patch.object(MasterDao, 'get_warehouse', new=AsyncMock(return_value=self.warehouse)),
patch.object(MasterDao, 'get_strain', new=AsyncMock(return_value=self.strain)),
patch.object(OperationDao, 'add_filling_record', new=AsyncMock(side_effect=integrity_error())),
self.assertRaises(PermissionException) as raised,
):
await FillingService.fill_pallet_services(self.db, self.page, self.user)
self.assertEqual('无权访问该仓库', raised.exception.message)
self.assertEqual(2, warehouse_scope.await_count)
self.assertEqual(1, key_reads.await_count)
self.assertEqual(2, self.db.rollback.await_count)
self.db.commit.assert_not_awaited()
async def test_integrity_error_without_idempotent_result_is_reported_as_data_conflict(self) -> None:
with ( with (
patch.object(AccessDao, 'list_user_warehouse_ids', new=AsyncMock(return_value=[7])), patch.object(AccessDao, 'list_user_warehouse_ids', new=AsyncMock(return_value=[7])),
patch.object(AccessService, 'assert_warehouse_access', new=AsyncMock()), patch.object(AccessService, 'assert_warehouse_access', new=AsyncMock()),
@@ -777,9 +883,10 @@ class TestFillPalletService(unittest.IsolatedAsyncioTestCase):
patch.object(MasterDao, 'get_warehouse', new=AsyncMock(return_value=self.warehouse)), patch.object(MasterDao, 'get_warehouse', new=AsyncMock(return_value=self.warehouse)),
patch.object(MasterDao, 'get_strain', new=AsyncMock(return_value=self.strain)), patch.object(MasterDao, 'get_strain', new=AsyncMock(return_value=self.strain)),
patch.object(OperationDao, 'add_filling_record', new=AsyncMock(side_effect=integrity_error())), patch.object(OperationDao, 'add_filling_record', new=AsyncMock(side_effect=integrity_error())),
self.assertRaises(PermissionException), self.assertRaises(ServiceException) as raised,
): ):
await FillingService.fill_pallet_services(self.db, self.page, self.user) await FillingService.fill_pallet_services(self.db, self.page, self.user)
self.assertEqual('装填失败,数据冲突', raised.exception.message)
self.assertEqual(2, self.db.rollback.await_count) self.assertEqual(2, self.db.rollback.await_count)
self.db.commit.assert_not_awaited() self.db.commit.assert_not_awaited()
@@ -502,6 +502,24 @@ class TestMasterDao(unittest.IsolatedAsyncioTestCase):
self.assertIn(f'FROM {table}', sql) self.assertIn(f'FROM {table}', sql)
self.assertIn(f'{table}.{id_field} = 9', sql) self.assertIn(f'{table}.{id_field} = 9', sql)
async def test_locking_master_gets_use_current_reads_and_refresh_identity_map(self) -> None:
strain_lock_parameter = inspect.signature(MasterDao.get_strain).parameters.get('for_update')
self.assertIsNotNone(strain_lock_parameter)
self.assertIs(inspect.Parameter.KEYWORD_ONLY, strain_lock_parameter.kind)
self.assertFalse(strain_lock_parameter.default)
for resource in ('warehouse', 'strain'):
with self.subTest(resource=resource):
result = MagicMock()
result.scalar_one_or_none.return_value = None
self.db.execute = AsyncMock(return_value=result)
await getattr(MasterDao, f'get_{resource}')(self.db, 9, for_update=True)
statement = self.db.execute.await_args.args[0]
self.assertIn('FOR UPDATE', compile_sql(statement))
self.assertIs(True, statement.get_execution_options().get('populate_existing'))
async def test_unique_queries_use_global_or_parent_scoped_predicates(self) -> None: async def test_unique_queries_use_global_or_parent_scoped_predicates(self) -> None:
cases = ( cases = (
('strain', ('S001',), 'biz_strain', ('strain_code',)), ('strain', ('S001',), 'biz_strain', ('strain_code',)),