数据:增加测试批次模型与迁移契约
This commit is contained in:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user