调整部分前端布局和后端增删改逻辑

This commit is contained in:
焦龙言
2026-09-26 23:11:05 +08:00
parent 977e7f996e
commit 17d991a74a
117 changed files with 311994 additions and 3192 deletions
@@ -3,7 +3,7 @@ import unittest
from types import SimpleNamespace
from unittest.mock import AsyncMock, MagicMock, patch
from sqlalchemy.dialects import mysql, postgresql
from sqlalchemy.dialects import mysql
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import InstrumentedAttribute
from sqlalchemy.sql import Select, operators
@@ -205,7 +205,7 @@ class AccessDaoTest(unittest.IsolatedAsyncioTestCase):
await AccessDao.user_has_warehouse(db, 8, 99)
statement = db.execute.await_args.args[0]
for dialect in (mysql.dialect(), postgresql.dialect()):
for dialect in (mysql.dialect(),):
sql = ' '.join(
str(statement.compile(dialect=dialect, compile_kwargs={'literal_binds': True})).split()
)
@@ -250,7 +250,7 @@ class AccessDaoTest(unittest.IsolatedAsyncioTestCase):
await AccessDao.user_has_site(db, 8, 7)
statement = db.execute.await_args.args[0]
for dialect in (mysql.dialect(), postgresql.dialect()):
for dialect in (mysql.dialect(),):
sql = ' '.join(
str(statement.compile(dialect=dialect, compile_kwargs={'literal_binds': True})).split()
)
@@ -70,7 +70,7 @@ class AiRemovalContractTest(unittest.TestCase):
def test_ai_dependencies_are_removed(self) -> None:
backend_dependencies = set()
for filename in ('requirements.txt', 'requirements-pg.txt'):
for filename in ('requirements.txt',):
backend_dependencies.update(_parse_requirement_names(BACKEND_ROOT / filename))
self.assertEqual([], sorted(AI_DEPENDENCIES & backend_dependencies))
@@ -82,7 +82,7 @@ class AiRemovalContractTest(unittest.TestCase):
def test_initialization_sql_has_no_ai_or_external_menu(self) -> None:
forbidden_terms = ('ai_models', 'ai_chat_config', 'ai_provider_type', '施尔丰仓库')
violations = []
for filename in ('shierfeng-fastapi.sql', 'shierfeng-fastapi-pg.sql'):
for filename in ('shierfeng-fastapi.sql',):
sql = (BACKEND_ROOT / 'sql' / filename).read_text(encoding='utf-8').lower()
violations.extend(f'{filename}: {term}' for term in forbidden_terms if term.lower() in sql)
self.assertEqual([], violations)
@@ -649,12 +649,11 @@ class ApiPermissionContractTest(unittest.TestCase):
):
_extract_guarded_vue_permissions(source)
def test_independent_expected_permissions_reject_four_layers_missing_same_permission(self) -> None:
def test_independent_expected_permissions_reject_layers_missing_same_permission(self) -> None:
missing = 'master:strain:query'
mutated = EXPECTED_PERMISSIONS - {missing}
layers = {
'mysql': mutated,
'postgresql': mutated,
'controller': mutated,
'vue': mutated,
}
@@ -5,7 +5,7 @@ from types import SimpleNamespace
from typing import Any
from unittest.mock import AsyncMock, MagicMock, patch
from sqlalchemy.dialects import mysql, postgresql
from sqlalchemy.dialects import mysql
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.sql import operators
@@ -39,7 +39,7 @@ from utils.page_util import PageUtil
def compile_sql(statement: Any, dialect: Any | None = None) -> str:
compiled = statement.compile(
dialect=dialect or postgresql.dialect(),
dialect=dialect or mysql.dialect(),
compile_kwargs={'literal_binds': True},
)
return ' '.join(str(compiled).split()).replace('%%', '%')
@@ -289,7 +289,7 @@ class TestInventoryDao(unittest.IsolatedAsyncioTestCase):
statement = self.db.execute.await_args.args[0]
self.assertTrue(statement.get_final_froms()[0].compare(BizPalletInventory.__table__))
self.assertIs(True, statement.get_execution_options().get('populate_existing'))
for dialect in (mysql.dialect(), postgresql.dialect()):
for dialect in (mysql.dialect(),):
sql = compile_sql(statement, dialect)
self.assertIn('biz_pallet_inventory.pallet_id = 7', sql)
self.assertIn('FOR UPDATE', sql)
@@ -310,7 +310,7 @@ class TestInventoryDao(unittest.IsolatedAsyncioTestCase):
statement = self.db.execute.await_args.args[0]
self.assertTrue(statement.get_final_froms()[0].compare(BizPalletInventory.__table__))
self.assertIs(True, statement.get_execution_options().get('populate_existing'))
for dialect in (mysql.dialect(), postgresql.dialect()):
for dialect in (mysql.dialect(),):
sql = compile_sql(statement, dialect)
self.assertIn('biz_pallet_inventory.pallet_id IN (2, 5, 9)', sql)
self.assertIn('ORDER BY biz_pallet_inventory.pallet_id', sql)
@@ -353,7 +353,7 @@ class TestInventoryDao(unittest.IsolatedAsyncioTestCase):
statement = self.db.execute.await_args.args[0]
self.assertTrue(statement.get_final_froms()[0].compare(BizPallet.__table__))
self.assertIs(True, statement.get_execution_options().get('populate_existing'))
for dialect in (mysql.dialect(), postgresql.dialect()):
for dialect in (mysql.dialect(),):
sql = compile_sql(statement, dialect)
self.assertIn('biz_pallet.pallet_id IN (2, 5, 9)', sql)
self.assertIn('ORDER BY biz_pallet.pallet_id', sql)
@@ -412,7 +412,7 @@ class TestInventoryDao(unittest.IsolatedAsyncioTestCase):
statement = self.db.execute.await_args.args[0]
self.assertTrue(statement.selected_columns[0].compare(BizPalletEvent.event_seq.property.columns[0]))
self.assertTrue(statement.get_final_froms()[0].compare(BizPalletEvent.__table__))
for dialect in (mysql.dialect(), postgresql.dialect()):
for dialect in (mysql.dialect(),):
sql = compile_sql(statement, dialect)
self.assertNotIn('max(', sql.lower())
self.assertIn('biz_pallet_event.event_id = 40', sql)
@@ -483,7 +483,7 @@ class TestOperationDaoReads(unittest.IsolatedAsyncioTestCase):
statement = self.db.execute.await_args.args[0]
self.assertTrue(statement.get_final_froms()[0].compare(model.__table__))
self.assertIs(True, statement.get_execution_options().get('populate_existing'))
for dialect in (mysql.dialect(), postgresql.dialect()):
for dialect in (mysql.dialect(),):
sql = compile_sql(statement, dialect)
self.assertIn(f'{model.__tablename__}.{id_field} = {record_id}', sql)
self.assertIn('FOR UPDATE', sql)
@@ -519,7 +519,7 @@ class TestOperationDaoReads(unittest.IsolatedAsyncioTestCase):
self.assertTrue(item_statement.get_final_froms()[0].compare(BizShipmentItem.__table__))
self.assertIs(True, parent_statement.get_execution_options().get('populate_existing'))
self.assertIs(True, item_statement.get_execution_options().get('populate_existing'))
for dialect in (mysql.dialect(), postgresql.dialect()):
for dialect in (mysql.dialect(),):
with self.subTest(dialect=dialect.name):
parent_sql = compile_sql(parent_statement, dialect)
item_sql = compile_sql(item_statement, dialect)
@@ -551,9 +551,7 @@ class TestOperationDaoReads(unittest.IsolatedAsyncioTestCase):
self.db.execute.assert_awaited_once()
parent_statement = self.db.execute.await_args.args[0]
mysql_sql = compile_sql(parent_statement, mysql.dialect())
postgresql_sql = compile_sql(parent_statement, postgresql.dialect())
self.assertIn('false = 1', mysql_sql)
self.assertIn('false', postgresql_sql)
async def test_inbound_for_update_locks_parent_then_current_items(self) -> None:
inbound = SimpleNamespace(inbound_id=11)
@@ -578,7 +576,7 @@ class TestOperationDaoReads(unittest.IsolatedAsyncioTestCase):
self.assertTrue(item_statement.get_final_froms()[0].compare(BizInboundItem.__table__))
self.assertIs(True, parent_statement.get_execution_options().get('populate_existing'))
self.assertIs(True, item_statement.get_execution_options().get('populate_existing'))
for dialect in (mysql.dialect(), postgresql.dialect()):
for dialect in (mysql.dialect(),):
with self.subTest(dialect=dialect.name):
parent_sql = compile_sql(parent_statement, dialect)
item_sql = compile_sql(item_statement, dialect)
@@ -610,7 +608,7 @@ class TestOperationDaoReads(unittest.IsolatedAsyncioTestCase):
self.assertIsNone(actual)
self.db.execute.assert_awaited_once()
parent_statement = self.db.execute.await_args.args[0]
for dialect in (mysql.dialect(), postgresql.dialect()):
for dialect in (mysql.dialect(),):
with self.subTest(dialect=dialect.name):
sql = compile_sql(parent_statement, dialect)
self.assertIn('biz_inbound_order.warehouse_id IN (3, 7)', sql)
@@ -684,7 +682,7 @@ class TestOperationDaoReads(unittest.IsolatedAsyncioTestCase):
self.assertIsNone(actual)
statement = self.db.execute.await_args.args[0]
for dialect in (mysql.dialect(), postgresql.dialect()):
for dialect in (mysql.dialect(),):
with self.subTest(dialect=dialect.name):
sql = compile_sql(statement, dialect)
self.assertIn("biz_inbound_order.confirm_idempotency_key = 'confirm-key'", sql)
@@ -850,7 +848,7 @@ class TestOperationDaoReads(unittest.IsolatedAsyncioTestCase):
self.assertIs(marker, actual)
statement = paginate.await_args.args[1]
for dialect in (mysql.dialect(), postgresql.dialect()):
for dialect in (mysql.dialect(),):
with self.subTest(dialect=dialect.name):
sql = compile_sql(statement, dialect)
self.assertIn('biz_inbound_order.warehouse_id IN (2, 5)', sql)
@@ -871,9 +869,7 @@ class TestOperationDaoReads(unittest.IsolatedAsyncioTestCase):
self.assertIs(marker, actual)
statement = paginate.await_args.args[1]
mysql_sql = compile_sql(statement, mysql.dialect())
postgresql_sql = compile_sql(statement, postgresql.dialect())
self.assertIn('WHERE false = 1', mysql_sql)
self.assertIn('WHERE false', postgresql_sql)
self.assertEqual((query.page_num, query.page_size, True), paginate.await_args.args[2:])
async def test_inbound_list_none_scope_has_no_authorization_predicate(self) -> None:
@@ -906,7 +902,7 @@ class TestOperationDaoReads(unittest.IsolatedAsyncioTestCase):
self.assertIs(marker, actual)
statement = paginate.await_args.args[1]
for dialect in (mysql.dialect(), postgresql.dialect()):
for dialect in (mysql.dialect(),):
with self.subTest(dialect=dialect.name):
sql = compile_sql(statement, dialect)
self.assertIn('biz_shipment_order.source_warehouse_id IN (3, 7)', sql)
@@ -922,7 +918,6 @@ class TestOperationDaoReads(unittest.IsolatedAsyncioTestCase):
)
empty_statement = empty_paginate.await_args.args[1]
self.assertIn('false = 1', compile_sql(empty_statement, mysql.dialect()))
self.assertIn('false', compile_sql(empty_statement, postgresql.dialect()))
async def test_shipment_detail_and_confirm_key_push_both_scopes_into_parent_sql(self) -> None:
parent_result = MagicMock()
@@ -953,7 +948,7 @@ class TestOperationDaoReads(unittest.IsolatedAsyncioTestCase):
)
)
key_statement = self.db.execute.await_args.args[0]
for dialect in (mysql.dialect(), postgresql.dialect()):
for dialect in (mysql.dialect(),):
with self.subTest(dialect=dialect.name):
detail_sql = compile_sql(detail_statement, dialect)
key_sql = compile_sql(key_statement, dialect)
@@ -994,7 +989,7 @@ class TestOperationDaoReads(unittest.IsolatedAsyncioTestCase):
)
key_statement = self.db.execute.await_args.args[0]
for dialect in (mysql.dialect(), postgresql.dialect()):
for dialect in (mysql.dialect(),):
with self.subTest(dialect=dialect.name):
for statement in (list_statement, detail_statement, key_statement):
sql = compile_sql(statement, dialect)
@@ -1016,7 +1011,7 @@ class TestOperationDaoReads(unittest.IsolatedAsyncioTestCase):
)
empty_key = self.db.execute.await_args.args[0]
for dialect in (mysql.dialect(), postgresql.dialect()):
for dialect in (mysql.dialect(),):
with self.subTest(dialect=f'{dialect.name}-empty'):
for statement in (empty_list, empty_detail, empty_key):
sql = compile_sql(statement, dialect)
@@ -1055,7 +1050,7 @@ class TestOperationDaoReads(unittest.IsolatedAsyncioTestCase):
statement = self.db.execute.await_args.args[0]
self.assertFalse(statement.get_execution_options().get('populate_existing', False))
for dialect in (mysql.dialect(), postgresql.dialect()):
for dialect in (mysql.dialect(),):
sql = compile_sql(statement, dialect)
self.assertNotIn('FOR UPDATE', sql)
if expected_scope is not None:
@@ -1154,9 +1149,7 @@ class TestOperationDaoReads(unittest.IsolatedAsyncioTestCase):
self.db.execute.assert_awaited_once()
parent_statement = self.db.execute.await_args.args[0]
mysql_sql = compile_sql(parent_statement, mysql.dialect())
postgresql_sql = compile_sql(parent_statement, postgresql.dialect())
self.assertIn('false = 1', mysql_sql)
self.assertIn('false', postgresql_sql)
async def test_simple_details_target_primary_keys(self) -> None:
cases = (
@@ -1315,7 +1308,7 @@ class TestOperationDaoWrites(unittest.IsolatedAsyncioTestCase):
order_statement = self.db.execute.await_args_list[1].args[0]
self.assertTrue(item_statement.table.compare(item_model.__table__))
self.assertTrue(order_statement.table.compare(order_model.__table__))
for dialect in (mysql.dialect(), postgresql.dialect()):
for dialect in (mysql.dialect(),):
self.assertIn(
f'{item_model.__tablename__}.{id_field} = {record_id}',
compile_sql(item_statement, dialect),
@@ -1,7 +1,7 @@
import unittest
from sqlalchemy import CHAR, BigInteger, CheckConstraint, DateTime, Integer, String, Table, Text, UniqueConstraint
from sqlalchemy.dialects import mysql, postgresql
from sqlalchemy.dialects import mysql
from sqlalchemy.schema import CreateColumn, CreateTable
from config.database import Base
@@ -1061,12 +1061,12 @@ def _normalized_check_sql(table: Table) -> dict[str, str]:
class BusinessModelContractTest(unittest.TestCase):
def test_batch_related_tables_compile_for_mysql_and_postgresql(self) -> None:
def test_batch_related_tables_compile_for_mysql(self) -> None:
self.assertIs(Base.metadata.tables['sys_user'], user_do.SysUser.__table__)
batch_tables = EXPECTED_TEST_BATCH_TABLES | TEST_BATCH_OWNED_TABLES
self.assertEqual(18, len(batch_tables))
for table_name in batch_tables:
for dialect_name, dialect in (('mysql', mysql.dialect()), ('postgresql', postgresql.dialect())):
for dialect_name, dialect in (('mysql', mysql.dialect()),):
with self.subTest(table_name=table_name, dialect=dialect_name):
compiled = str(CreateTable(Base.metadata.tables[table_name]).compile(dialect=dialect))
self.assertIn(table_name, compiled)
@@ -1187,7 +1187,7 @@ class BusinessModelContractTest(unittest.TestCase):
self.assertEqual(EXPECTED_EMPTY_AUDIT_SERVER_DEFAULTS, actual_audit_columns)
violations = []
dialects = {'mysql': mysql.dialect(), 'postgresql': postgresql.dialect()}
dialects = {'mysql': mysql.dialect()}
for table_name, column_name in sorted(EXPECTED_EMPTY_AUDIT_SERVER_DEFAULTS):
column = Base.metadata.tables[table_name].c[column_name]
for dialect_name, dialect in dialects.items():
@@ -0,0 +1,77 @@
import unittest
from unittest.mock import AsyncMock, MagicMock, patch
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from common.vo import PageModel
from module_business.dao.master_dao import MasterDao
from module_business.dao.operation_dao import OperationDao
from module_business.dao.report_dao import ReportDao
from module_business.entity.do.master_do import BizStrain
from module_business.entity.do.operation_do import BizInboundOrder
from module_business.entity.vo.master_vo import StrainPageQueryModel
from module_business.entity.vo.operation_vo import InboundPageQueryModel
from module_business.entity.vo.report_vo import FillingReportQueryModel
from utils.page_util import PageUtil
class TestPageUtil(unittest.IsolatedAsyncioTestCase):
async def test_paginate_excludes_internal_test_batch_field_from_serialized_rows(self) -> None:
record = BizStrain(
strain_id=1,
strain_code='S-1',
strain_name='菌种',
status='0',
test_batch_id=None,
)
count_result = MagicMock()
count_result.scalar.return_value = 1
query_result = MagicMock()
query_result.__iter__.return_value = iter([(record,)])
db = MagicMock(spec=AsyncSession)
db.execute = AsyncMock(side_effect=[count_result, query_result])
page = await PageUtil.paginate(
db,
select(BizStrain),
page_num=1,
page_size=10,
is_page=True,
exclude_fields={'test_batch_id'},
)
self.assertEqual('S-1', page.rows[0]['strainCode'])
self.assertNotIn('testBatchId', page.rows[0])
async def test_business_paginators_hide_internal_test_batch_field(self) -> None:
db = MagicMock(spec=AsyncSession)
page = PageModel(rows=[], pageNum=1, pageSize=10, total=0, hasNext=False)
cases = (
(
MasterDao._paginate,
(db, select(BizStrain), StrainPageQueryModel(), True),
),
(
OperationDao._paginate,
(db, select(BizInboundOrder), InboundPageQueryModel(), True),
),
(
ReportDao._paginate,
(db, select(BizInboundOrder), FillingReportQueryModel()),
),
)
for paginator, args in cases:
with (
self.subTest(paginator=paginator.__qualname__),
patch.object(
PageUtil,
'paginate',
new=AsyncMock(return_value=page),
) as paginate,
):
result = await paginator(*args)
self.assertIs(page, result)
self.assertEqual({'test_batch_id'}, paginate.await_args.kwargs['exclude_fields'])
@@ -10,7 +10,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
from fastapi.routing import APIRoute
from pydantic import BaseModel, ValidationError
from sqlalchemy import Select
from sqlalchemy.dialects import mysql, postgresql
from sqlalchemy.dialects import mysql
from sqlalchemy.ext.asyncio import AsyncSession
from common.aspect.interface_auth import CheckUserInterfaceAuth
@@ -355,7 +355,7 @@ class TestReportDaoQueries(unittest.IsolatedAsyncioTestCase):
def _assert_compiles_for_supported_databases(self, statement: Select) -> tuple[str, str]:
compiled = tuple(
' '.join(str(statement.compile(dialect=dialect, compile_kwargs={'literal_binds': True})).lower().split())
for dialect in (mysql.dialect(), postgresql.dialect())
for dialect in (mysql.dialect(),)
)
self.assertTrue(all(sql.startswith('select ') for sql in compiled))
return compiled
@@ -10,7 +10,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
from fastapi import FastAPI
from fastapi.routing import APIRoute
from sqlalchemy.dialects import mysql, postgresql
from sqlalchemy.dialects import mysql
from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession
@@ -270,7 +270,7 @@ class ReturnDaoScopeRedTest(unittest.IsolatedAsyncioTestCase):
self.assertTrue(item_statement.get_final_froms()[0].compare(BizReturnItem.__table__))
self.assertIs(True, parent_statement.get_execution_options().get('populate_existing'))
self.assertIs(True, item_statement.get_execution_options().get('populate_existing'))
for dialect in (mysql.dialect(), postgresql.dialect()):
for dialect in (mysql.dialect(),):
parent_sql = compile_sql(parent_statement, dialect)
item_sql = compile_sql(item_statement, dialect)
self.assertIn('biz_return_order.return_id = 13', parent_sql)
@@ -299,7 +299,7 @@ class ReturnDaoScopeRedTest(unittest.IsolatedAsyncioTestCase):
)
statement = self.db.execute.await_args.args[0]
sql = compile_sql(statement, postgresql.dialect())
sql = compile_sql(statement, mysql.dialect())
self.assertIn('biz_return_order.source_site_id IN (9)', sql)
self.assertIn('biz_return_order.target_warehouse_id IN (7)', sql)
self.assertIn(' OR ', sql)
@@ -332,7 +332,7 @@ class ReturnDaoScopeRedTest(unittest.IsolatedAsyncioTestCase):
statement = self.db.execute.await_args.args[0]
self.assertTrue(statement.get_final_froms()[0].compare(BizReturnOrder.__table__))
for dialect in (mysql.dialect(), postgresql.dialect()):
for dialect in (mysql.dialect(),):
sql = compile_sql(statement, dialect)
self.assertIn(f"biz_return_order.{column} = '{key}'", sql)
self.assertIn(f'biz_return_order.source_site_id = {source_site_id}', sql)
@@ -10,7 +10,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
from fastapi import FastAPI
from fastapi.routing import APIRoute
from sqlalchemy.dialects import postgresql
from sqlalchemy.dialects import mysql
from sqlalchemy.exc import IntegrityError, MultipleResultsFound
from sqlalchemy.ext.asyncio import AsyncSession
@@ -322,7 +322,7 @@ class ShipmentDetailDaoTest(unittest.IsolatedAsyncioTestCase):
item_sql = ' '.join(
str(
item_statement.compile(
dialect=postgresql.dialect(),
dialect=mysql.dialect(),
compile_kwargs={'literal_binds': True},
)
).split()
@@ -374,7 +374,7 @@ class ShipmentDetailDaoTest(unittest.IsolatedAsyncioTestCase):
item_sql = ' '.join(
str(
item_statement.compile(
dialect=postgresql.dialect(),
dialect=mysql.dialect(),
compile_kwargs={'literal_binds': True},
)
).split()
@@ -5,12 +5,11 @@ from collections.abc import Callable
from pathlib import Path
SQL_ROOT = Path(__file__).resolve().parents[2] / 'sql'
INITIALIZERS = ('shierfeng-fastapi.sql', 'shierfeng-fastapi-pg.sql')
INITIALIZERS = ('shierfeng-fastapi.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
@@ -1007,18 +1006,6 @@ def _column_definitions(body: str) -> dict[str, str]:
return definitions
def _is_postgres_naive_timestamp_with_default_precision(definition: str) -> bool:
return (
re.fullmatch(
r'[`"]?[a-z_][a-z0-9_]*[`"]?\s+'
r'timestamp(?:\s+without\s+time\s+zone)?(?:\s+not\s+null)?',
definition.strip(),
flags=re.I,
)
is not None
)
def _assert_test_batch_sql_contract(
test_case: unittest.TestCase,
sql: str,
@@ -1064,18 +1051,9 @@ def _assert_test_batch_sql_contract(
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表示正式数据'",
)
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)
constraint_name = f'fk_{table_name}_test_batch_id'
test_case.assertRegex(
@@ -1345,9 +1323,7 @@ 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):
@@ -1361,7 +1337,7 @@ class SqlContractTest(unittest.TestCase):
def test_test_batch_patches_migrate_legacy_menu_permissions_without_role_grants(self) -> None:
legacy_removed_ids = set(LEGACY_REMOVED_BUTTON_SPECS)
for path in (TEST_BATCH_MYSQL_PATCH_PATH, TEST_BATCH_PG_PATCH_PATH):
for path in (TEST_BATCH_MYSQL_PATCH_PATH,):
with self.subTest(path=path.name):
raw = path.read_text(encoding='utf-8')
statements = _sql_statements(raw)
@@ -1430,32 +1406,24 @@ class SqlContractTest(unittest.TestCase):
self.assertEqual(EXPECTED_FOREIGN_KEYS[table], foreign_keys)
self.assertEqual(EXPECTED_CHECKS[table], checks)
def test_postgres_business_datetimes_preserve_default_microsecond_precision(self) -> None:
def test_mysql_business_datetimes_use_native_datetime_columns(self) -> None:
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'))
tables = _create_table_bodies((SQL_ROOT / 'shierfeng-fastapi.sql').read_text(encoding='utf-8'))
invalid_columns = []
for table, expected_columns in EXPECTED_DATETIME_COLUMNS.items():
definitions = _column_definitions(tables[table])
actual_columns = {
column
for column, definition in definitions.items()
if re.search(r'\btimestamp\b', definition, flags=re.I)
if re.search(r'\bdatetime\b', definition, flags=re.I)
}
self.assertEqual(expected_columns, actual_columns, table)
for column in expected_columns:
definition = definitions[column]
if not _is_postgres_naive_timestamp_with_default_precision(definition):
if not re.search(r'\bdatetime\b', definition, flags=re.I):
invalid_columns.append(f'{table}.{column}')
self.assertEqual([], sorted(invalid_columns))
def test_postgres_datetime_type_validator_rejects_precision_and_timezone_mutations(self) -> None:
self.assertTrue(_is_postgres_naive_timestamp_with_default_precision('occurred_at timestamp'))
self.assertTrue(
_is_postgres_naive_timestamp_with_default_precision('occurred_at timestamp without time zone not null')
)
self.assertFalse(_is_postgres_naive_timestamp_with_default_precision('occurred_at timestamp(3)'))
self.assertFalse(_is_postgres_naive_timestamp_with_default_precision('occurred_at timestamp with time zone'))
def test_initializers_define_fixed_business_menus_and_permissions(self) -> None:
self.assertEqual(18, len(EXPECTED_PAGE_PERMISSIONS))
self.assertEqual(43, len(EXPECTED_BUTTON_PERMISSIONS))
@@ -1679,18 +1647,6 @@ class SqlContractTest(unittest.TestCase):
with self.assertRaises(AssertionError):
_assert_business_menu_fields(self, mutated)
def test_mysql_and_postgresql_business_menu_fields_are_identical(self) -> None:
mysql_rows = _business_menu_rows((SQL_ROOT / INITIALIZERS[0]).read_text(encoding='utf-8'))
postgres_rows = _business_menu_rows((SQL_ROOT / INITIALIZERS[1]).read_text(encoding='utf-8'))
compared_indexes = tuple(range(15))
mysql_contract = {
menu_id: tuple(row[index] for index in compared_indexes) for menu_id, row in mysql_rows.items()
}
postgres_contract = {
menu_id: tuple(row[index] for index in compared_indexes) for menu_id, row in postgres_rows.items()
}
self.assertEqual(mysql_contract, postgres_contract)
def test_initializers_do_not_grant_new_business_menus_to_roles(self) -> None:
for filename in INITIALIZERS:
with self.subTest(filename=filename):
@@ -1717,14 +1673,10 @@ class SqlContractTest(unittest.TestCase):
any(int(row[0]) in REMOVED_AI_DICT_CODES or row[4] == 'ai_provider_type' for row in dict_data)
)
def test_initializers_keep_menu_allocator_and_use_native_dialects(self) -> None:
def test_initializer_uses_mysql_native_menu_allocator(self) -> None:
mysql = (SQL_ROOT / 'shierfeng-fastapi.sql').read_text(encoding='utf-8').lower()
postgres = (SQL_ROOT / 'shierfeng-fastapi-pg.sql').read_text(encoding='utf-8').lower()
self.assertRegex(mysql, r'auto_increment\s*=\s*2000')
self.assertNotRegex(mysql, r'\bbigserial\b|\balter\s+sequence\b|\bcomment\s+on\b')
self.assertRegex(postgres, r'alter\s+sequence\s+sys_menu_menu_id_seq\s+restart\s+2000')
self.assertNotIn('`', postgres)
self.assertNotRegex(postgres, r'\bauto_increment\b|\bengine\s*=')
def test_initializers_create_event_before_inventory_foreign_key(self) -> None:
for filename in INITIALIZERS:
@@ -3,7 +3,7 @@ from types import SimpleNamespace
from unittest.mock import AsyncMock, MagicMock, patch
from fastapi import params
from sqlalchemy.dialects import postgresql
from sqlalchemy.dialects import mysql
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.sql import select
from starlette.requests import Request
@@ -32,7 +32,7 @@ def make_request(batch_id: str | None = None) -> Request:
def compile_sql(statement: object) -> str:
return ' '.join(
str(statement.compile(dialect=postgresql.dialect(), compile_kwargs={'literal_binds': True})).split()
str(statement.compile(dialect=mysql.dialect(), compile_kwargs={'literal_binds': True})).split()
)
@@ -4,7 +4,7 @@ from pathlib import Path
from types import SimpleNamespace
from unittest.mock import AsyncMock, MagicMock, patch
from sqlalchemy.dialects import mysql, postgresql
from sqlalchemy.dialects import mysql
from sqlalchemy.ext.asyncio import AsyncSession
from common.vo import PageModel
@@ -75,7 +75,7 @@ def make_test_scope(batch_id: int) -> TestBatchScope:
def _compile(statement: object) -> str:
return ' '.join(
' '.join(str(statement.compile(dialect=dialect, compile_kwargs={'literal_binds': True})).lower().split())
for dialect in (mysql.dialect(), postgresql.dialect())
for dialect in (mysql.dialect(),)
)
@@ -6,7 +6,7 @@ from types import SimpleNamespace
from typing import get_type_hints
from unittest.mock import AsyncMock, MagicMock, patch
from sqlalchemy.dialects import postgresql
from sqlalchemy.dialects import mysql
from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession
@@ -77,7 +77,7 @@ def make_batch(
def compile_sql(statement: object) -> str:
return ' '.join(
str(statement.compile(dialect=postgresql.dialect(), compile_kwargs={'literal_binds': True})).split()
str(statement.compile(dialect=mysql.dialect(), compile_kwargs={'literal_binds': True})).split()
)