安全:增加测试批次原子清理
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
import hashlib
|
||||
import heapq
|
||||
import json
|
||||
import secrets
|
||||
from collections import defaultdict, deque
|
||||
from dataclasses import dataclass
|
||||
|
||||
from sqlalchemy import Column, select
|
||||
from sqlalchemy import Column, MetaData, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from config.database import Base
|
||||
@@ -98,6 +99,25 @@ BATCH_LABELS = {
|
||||
'biz_pallet_event': '托盘事件',
|
||||
}
|
||||
|
||||
_DELETE_TIE_BREAK_ORDER = (
|
||||
'biz_pallet_inventory',
|
||||
'biz_usage_record',
|
||||
'biz_return_item',
|
||||
'biz_shipment_item',
|
||||
'biz_inbound_item',
|
||||
'biz_return_order',
|
||||
'biz_shipment_order',
|
||||
'biz_inbound_order',
|
||||
'biz_pallet_event',
|
||||
'biz_filling_record',
|
||||
'biz_production_line',
|
||||
'biz_customer_site',
|
||||
'biz_customer',
|
||||
'biz_warehouse',
|
||||
'biz_strain',
|
||||
'biz_pallet',
|
||||
)
|
||||
|
||||
ROOT_TABLES = {
|
||||
'strain': 'biz_strain',
|
||||
'customer': 'biz_customer',
|
||||
@@ -135,6 +155,42 @@ def issue_preview_token() -> tuple[str, str]:
|
||||
return raw, hashlib.sha256(raw.encode()).hexdigest()
|
||||
|
||||
|
||||
def derive_batch_delete_order(metadata: MetaData) -> list[str]:
|
||||
"""从实际外键图生成子表优先的确定性删除顺序。"""
|
||||
safe_tables = set(BATCH_MODELS)
|
||||
metadata_batch_tables = {table.name for table in metadata.tables.values() if 'test_batch_id' in table.columns}
|
||||
if metadata_batch_tables != safe_tables or set(_DELETE_TIE_BREAK_ORDER) != safe_tables:
|
||||
raise RuntimeError('测试批次清理安全表集合不一致')
|
||||
|
||||
children_by_parent: dict[str, set[str]] = {table_name: set() for table_name in safe_tables}
|
||||
parents_by_child: dict[str, set[str]] = {table_name: set() for table_name in safe_tables}
|
||||
for child_name in sorted(safe_tables):
|
||||
for foreign_key in metadata.tables[child_name].foreign_keys:
|
||||
parent_name = foreign_key.target_fullname.rsplit('.', maxsplit=1)[0]
|
||||
if parent_name not in safe_tables:
|
||||
continue
|
||||
children_by_parent[parent_name].add(child_name)
|
||||
parents_by_child[child_name].add(parent_name)
|
||||
|
||||
priority = {table_name: index for index, table_name in enumerate(_DELETE_TIE_BREAK_ORDER)}
|
||||
ready = [(priority[table_name], table_name) for table_name, children in children_by_parent.items() if not children]
|
||||
heapq.heapify(ready)
|
||||
order: list[str] = []
|
||||
while ready:
|
||||
_, child_name = heapq.heappop(ready)
|
||||
order.append(child_name)
|
||||
for parent_name in sorted(parents_by_child[child_name]):
|
||||
children_by_parent[parent_name].remove(child_name)
|
||||
if not children_by_parent[parent_name]:
|
||||
heapq.heappush(ready, (priority[parent_name], parent_name))
|
||||
|
||||
if len(order) != len(safe_tables):
|
||||
raise RuntimeError('测试批次清理外键图存在环')
|
||||
if set(order) != safe_tables:
|
||||
raise RuntimeError('测试批次清理安全表集合不一致')
|
||||
return order
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class _GraphEdge:
|
||||
source_table: str
|
||||
|
||||
Reference in New Issue
Block a user