数据:增加测试批次模型与迁移契约

This commit is contained in:
焦龙言
2026-09-22 14:37:31 +08:00
parent 40c56c6b33
commit 17bfa2c5fe
12 changed files with 1278 additions and 32 deletions
@@ -9,11 +9,33 @@ INITIALIZERS = ('shierfeng-fastapi.sql', 'shierfeng-fastapi-pg.sql')
PATCH_PATH = SQL_ROOT / 'patches/20260724_pallet_workflow_mysql.sql'
MENU_PROMOTION_PATCH_PATH = SQL_ROOT / 'patches/20260727_promote_business_menus_mysql.sql'
BUSINESS_MENU_ICON_PATCH_PATH = SQL_ROOT / 'patches/20260728_add_business_menu_icons_mysql.sql'
TEST_BATCH_MYSQL_PATCH_PATH = SQL_ROOT / 'patches/20260922_test_data_batch_mysql.sql'
TEST_BATCH_PG_PATCH_PATH = SQL_ROOT / 'patches/20260922_test_data_batch_pg.sql'
SYS_MENU_VALUE_COUNT = 20
SYS_MENU_PERMS_INDEX = 13
SYS_MENU_ICON_INDEX = 14
REMOVED_AI_DICT_CODES = set(range(33, 70))
EXPECTED_TEST_BATCH_TABLES = {'biz_test_batch', 'biz_test_batch_user'}
TEST_BATCH_OWNED_TABLES = {
'biz_strain',
'biz_customer',
'biz_customer_site',
'biz_production_line',
'biz_warehouse',
'biz_pallet',
'biz_inbound_order',
'biz_inbound_item',
'biz_filling_record',
'biz_shipment_order',
'biz_shipment_item',
'biz_usage_record',
'biz_return_order',
'biz_return_item',
'biz_pallet_inventory',
'biz_pallet_event',
}
EXPECTED_TABLES = {
'biz_strain',
'biz_warehouse',
@@ -346,6 +368,83 @@ EXPECTED_CHECKS = {
},
}
LEGACY_EXPECTED_TABLES = set(EXPECTED_TABLES)
LEGACY_EXPECTED_COLUMNS = {table: set(columns) for table, columns in EXPECTED_COLUMNS.items()}
LEGACY_EXPECTED_PRIMARY_KEYS = dict(EXPECTED_PRIMARY_KEYS)
LEGACY_EXPECTED_UNIQUE_KEYS = {table: set(keys) for table, keys in EXPECTED_UNIQUE_KEYS.items()}
LEGACY_EXPECTED_FOREIGN_KEYS = {table: set(keys) for table, keys in EXPECTED_FOREIGN_KEYS.items()}
LEGACY_EXPECTED_CHECKS = {table: set(checks) for table, checks in EXPECTED_CHECKS.items()}
EXPECTED_TABLES |= EXPECTED_TEST_BATCH_TABLES
for table_name in TEST_BATCH_OWNED_TABLES:
EXPECTED_COLUMNS[table_name].add('test_batch_id')
EXPECTED_FOREIGN_KEYS[table_name].add(('test_batch_id', 'biz_test_batch.batch_id'))
EXPECTED_COLUMNS.update(
{
'biz_test_batch': {
'batch_id',
'batch_code',
'batch_name',
'status',
'create_by',
'create_time',
'update_by',
'update_time',
'closed_by',
'closed_at',
'purged_by',
'purged_at',
'purge_summary',
'preview_action',
'preview_token_hash',
'preview_scope_hash',
'preview_expires_at',
'remark',
},
'biz_test_batch_user': {'batch_id', 'user_id', 'create_by', 'create_time'},
}
)
EXPECTED_DATETIME_COLUMNS.update(
{
'biz_test_batch': {'create_time', 'update_time', 'closed_at', 'purged_at', 'preview_expires_at'},
'biz_test_batch_user': {'create_time'},
}
)
EXPECTED_PRIMARY_KEYS.update(
{
'biz_test_batch': ('batch_id',),
'biz_test_batch_user': ('batch_id', 'user_id'),
}
)
EXPECTED_UNIQUE_KEYS.update(
{
'biz_test_batch': {('batch_code',)},
'biz_test_batch_user': set(),
}
)
EXPECTED_FOREIGN_KEYS.update(
{
'biz_test_batch': {
('closed_by', 'sys_user.user_id'),
('purged_by', 'sys_user.user_id'),
},
'biz_test_batch_user': {
('batch_id', 'biz_test_batch.batch_id'),
('user_id', 'sys_user.user_id'),
},
}
)
EXPECTED_CHECKS.update(
{
'biz_test_batch': {
"statusin('active','closed','purged')",
"preview_actionisnullorpreview_actionin('adopt','purge')",
},
'biz_test_batch_user': set(),
}
)
EXPECTED_PAGES = {
502: ('菌种资料', 121, 1, 'strain', 'master/strain/index', 'master:strain:list'),
503: ('客户资料', 121, 2, 'customer', 'master/customer/index', 'master:customer:list'),
@@ -873,6 +972,74 @@ def _is_postgres_naive_timestamp_with_default_precision(definition: str) -> bool
)
def _assert_test_batch_sql_contract(
test_case: unittest.TestCase,
sql: str,
*,
dialect: str,
is_patch: bool,
) -> None:
clean_sql = _strip_sql_comments(sql).replace('`', '')
normalized = re.sub(r'\s+', ' ', clean_sql).strip().lower()
tables = _create_table_bodies(sql)
test_case.assertNotRegex(normalized, r'\bon\s+delete\s+cascade\b')
if is_patch:
test_case.assertNotRegex(normalized, r'\bupdate\b')
for table_name in EXPECTED_TEST_BATCH_TABLES:
test_case.assertIn(table_name, tables)
columns, primary_key, unique_keys, foreign_keys, checks = _table_contract(tables[table_name])
test_case.assertEqual(EXPECTED_COLUMNS[table_name], columns, table_name)
test_case.assertEqual(EXPECTED_PRIMARY_KEYS[table_name], primary_key, table_name)
test_case.assertEqual(EXPECTED_UNIQUE_KEYS[table_name], unique_keys, table_name)
test_case.assertEqual(EXPECTED_FOREIGN_KEYS[table_name], foreign_keys, table_name)
test_case.assertEqual(EXPECTED_CHECKS[table_name], checks, table_name)
create_batch = normalized.index('create table biz_test_batch (')
create_batch_user = normalized.index('create table biz_test_batch_user (')
owned_marker = 'alter table' if is_patch else 'create table'
first_owned = min(normalized.index(f'{owned_marker} {table_name} ') for table_name in TEST_BATCH_OWNED_TABLES)
test_case.assertLess(create_batch, create_batch_user)
test_case.assertLess(create_batch_user, first_owned)
for table_name in TEST_BATCH_OWNED_TABLES:
with test_case.subTest(dialect=dialect, patch=is_patch, table=table_name):
if is_patch:
test_case.assertRegex(
normalized,
rf'\balter table {table_name}\s+add column test_batch_id bigint null\b',
)
else:
definitions = _column_definitions(tables[table_name])
test_case.assertIn('test_batch_id', definitions)
test_case.assertRegex(definitions['test_batch_id'], r'\bbigint\b')
test_case.assertNotRegex(definitions['test_batch_id'], r'\bnot\s+null\b')
index_name = f'idx_{table_name}_test_batch_id'
if dialect == 'mysql':
index_pattern = rf'\b(?:add\s+)?(?:index|key)\s+{index_name}\s*\(\s*test_batch_id\s*\)'
test_case.assertRegex(normalized, index_pattern)
test_case.assertIn("comment '测试数据批次id;null表示正式数据'", normalized)
else:
index_pattern = (
rf'\bcreate\s+index\s+{index_name}\s+on\s+{table_name}\s*\(\s*test_batch_id\s*\)'
)
test_case.assertRegex(normalized, index_pattern)
test_case.assertRegex(
normalized,
rf"comment\s+on\s+column\s+{table_name}\.test_batch_id\s+is\s+"
r"'测试数据批次id;null表示正式数据'",
)
constraint_name = f'fk_{table_name}_test_batch_id'
test_case.assertRegex(
normalized,
rf'\bconstraint\s+{constraint_name}\s+foreign\s+key\s*\(\s*test_batch_id\s*\)\s*'
r'references\s+biz_test_batch\s*\(\s*batch_id\s*\)',
)
def _menu_rows(sql: str, menu_ids: set[int]) -> dict[int, list]:
rows = _insert_rows(sql, 'sys_menu')
selected = [row for row in rows if row and str(row[0]).isdigit() and int(row[0]) in menu_ids]
@@ -1064,9 +1231,7 @@ def _assert_business_menu_icon_guard_primary_key(test_case: unittest.TestCase, s
def _assert_business_menu_icon_guard_structure(test_case: unittest.TestCase, sql: str) -> tuple[str, str]:
normalized = [_normalized_sql_statement(statement) for statement in _sql_statements(sql)]
guards = [
statement for statement in normalized if statement.startswith('insert into business_menu_icon_guard ')
]
guards = [statement for statement in normalized if statement.startswith('insert into business_menu_icon_guard ')]
test_case.assertEqual(2, len(guards))
pre_guard, post_guard = guards
identity_fields = (
@@ -1128,6 +1293,23 @@ def _assert_business_menu_icon_guard_structure(test_case: unittest.TestCase, sql
class SqlContractTest(unittest.TestCase):
def test_test_batch_initializers_and_patches_match_contract(self) -> None:
scripts = (
(SQL_ROOT / 'shierfeng-fastapi.sql', 'mysql', False),
(SQL_ROOT / 'shierfeng-fastapi-pg.sql', 'postgresql', False),
(TEST_BATCH_MYSQL_PATCH_PATH, 'mysql', True),
(TEST_BATCH_PG_PATCH_PATH, 'postgresql', True),
)
for path, dialect, is_patch in scripts:
with self.subTest(path=path.name):
self.assertTrue(path.is_file(), f'missing SQL: {path}')
_assert_test_batch_sql_contract(
self,
path.read_text(encoding='utf-8'),
dialect=dialect,
is_patch=is_patch,
)
def test_initializers_define_exact_business_table_set(self) -> None:
for filename in INITIALIZERS:
with self.subTest(filename=filename):
@@ -1149,7 +1331,7 @@ class SqlContractTest(unittest.TestCase):
self.assertEqual(EXPECTED_CHECKS[table], checks)
def test_postgres_business_datetimes_preserve_default_microsecond_precision(self) -> None:
self.assertEqual(34, sum(len(columns) for columns in EXPECTED_DATETIME_COLUMNS.values()))
self.assertEqual(40, sum(len(columns) for columns in EXPECTED_DATETIME_COLUMNS.values()))
tables = _create_table_bodies((SQL_ROOT / 'shierfeng-fastapi-pg.sql').read_text(encoding='utf-8'))
invalid_columns = []
for table, expected_columns in EXPECTED_DATETIME_COLUMNS.items():
@@ -1290,9 +1472,7 @@ class SqlContractTest(unittest.TestCase):
self.assertEqual(1, normalized.count('start transaction'))
self.assertEqual(1, normalized.count('commit'))
transaction_body = normalized[start_index + 1 : commit_index]
lock_statement = (
'select menu_id from sys_menu where menu_id in (121, 122, 123) order by menu_id for update'
)
lock_statement = 'select menu_id from sys_menu where menu_id in (121, 122, 123) order by menu_id for update'
expected_update = (
"update sys_menu set icon = case menu_id when 121 then 'documentation' when 122 then 'build' "
"when 123 then 'chart' else icon end where menu_id in (121, 122, 123)"
@@ -1476,6 +1656,8 @@ class SqlContractTest(unittest.TestCase):
'biz_customer',
'biz_warehouse',
'biz_strain',
'biz_test_batch_user',
'biz_test_batch',
)
for filename in INITIALIZERS:
with self.subTest(filename=filename):
@@ -1777,15 +1959,15 @@ class SqlContractTest(unittest.TestCase):
self.assertLess(lower.index('ddl'), lower.index('dml'))
self.assertEqual(18, len(re.findall(r'create\s+table\s+if\s+not\s+exists\s+`?biz_', lower)))
tables = _create_table_bodies(raw)
self.assertEqual(EXPECTED_TABLES, {table for table in tables if table.startswith('biz_')})
for table in EXPECTED_TABLES:
self.assertEqual(LEGACY_EXPECTED_TABLES, {table for table in tables if table.startswith('biz_')})
for table in LEGACY_EXPECTED_TABLES:
with self.subTest(table=table):
columns, primary_key, unique_keys, foreign_keys, checks = _table_contract(tables[table])
self.assertEqual(EXPECTED_COLUMNS[table], columns)
self.assertEqual(EXPECTED_PRIMARY_KEYS[table], primary_key)
self.assertEqual(EXPECTED_UNIQUE_KEYS[table], unique_keys)
self.assertEqual(EXPECTED_FOREIGN_KEYS[table], foreign_keys)
self.assertEqual(EXPECTED_CHECKS[table], checks)
self.assertEqual(LEGACY_EXPECTED_COLUMNS[table], columns)
self.assertEqual(LEGACY_EXPECTED_PRIMARY_KEYS[table], primary_key)
self.assertEqual(LEGACY_EXPECTED_UNIQUE_KEYS[table], unique_keys)
self.assertEqual(LEGACY_EXPECTED_FOREIGN_KEYS[table], foreign_keys)
self.assertEqual(LEGACY_EXPECTED_CHECKS[table], checks)
_assert_business_menu_fields(self, raw, LEGACY_PATCH_BUSINESS_MENU_FIELDS)
_assert_business_permission_inventory(self, raw)
patch_rows = _business_menu_rows(raw)
@@ -1793,9 +1975,7 @@ class SqlContractTest(unittest.TestCase):
compared_indexes = (0, 1, *range(4, 15))
expected_legacy_rows = {
menu_id: tuple(
'#'
if index == SYS_MENU_ICON_INDEX and menu_id in EXPECTED_BUSINESS_ROOT_ICONS
else row[index]
'#' if index == SYS_MENU_ICON_INDEX and menu_id in EXPECTED_BUSINESS_ROOT_ICONS else row[index]
for index in compared_indexes
)
for menu_id, row in mysql_rows.items()