调整部分前端布局和后端增删改逻辑
This commit is contained in:
@@ -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()
|
||||
)
|
||||
|
||||
|
||||
|
||||
+2
-2
@@ -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()
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user