test(sql): align contracts with clean initialization

This commit is contained in:
2026-09-17 15:10:27 +08:00
parent 1fe99eb6d0
commit 9dc344bdef
68 changed files with 851 additions and 1448 deletions
@@ -11,7 +11,7 @@ import static org.assertj.core.api.Assertions.assertThat;
class LastlegOrderDetailSchemaContractTest {
@Test
void orderDetailHasCustomerIsolationInBaselineAndRepeatableUpgrade() throws Exception {
void orderDetailHasCustomerIsolationInBaselineAndCleanInitialization() throws Exception {
String baseline = new ClassPathResource("db/lastleg.sql")
.getContentAsString(StandardCharsets.UTF_8);
String unified = Files.readString(Path.of("..", "deploy", "sql", "海外区域统一增量-MySQL8.sql"),
@@ -24,11 +24,10 @@ class LastlegOrderDetailSchemaContractTest {
.contains("customer_id VARCHAR(128) NOT NULL COMMENT '客户数据隔离键'")
.contains("KEY idx_ll_detail_customer_order(customer_id,order_id,id)");
assertThat(unified)
.contains("table_name='ll_order_detail' AND column_name='customer_id'")
.contains("UPDATE ll_order_detail detail")
.contains("SET detail.customer_id=orders.customer_id")
.contains("MODIFY COLUMN customer_id VARCHAR(128) NOT NULL COMMENT '客户数据隔离键'")
.contains("ADD KEY idx_ll_detail_customer_order(customer_id,order_id,id)");
.contains("CREATE TABLE ll_order_detail")
.contains("customer_id varchar(128)")
.contains("KEY idx_ll_detail_customer_order (customer_id,order_id,id)")
.doesNotContain("UPDATE ll_order_detail", "ALTER TABLE ll_order_detail");
assertThat(service)
.contains(".eq(LastlegOrderDetail::getCustomerId, order.getCustomerId())")
.contains(".eq(LastlegOrderStatusHistory::getCustomerId, order.getCustomerId())");
@@ -16,14 +16,16 @@ class Phase5ExceptionVisitSchemaContractTest {
StandardCharsets.UTF_8);
for (String sql : new String[] { baseline, overseas }) {
String normalized = sql.replaceAll("\\s+", "");
assertThat(normalized).contains(
"ll_exception_order(",
"UNIQUEKEYuk_ll_exception_no(customer_id,exception_no)",
"UNIQUEKEYuk_ll_exception_delivery_return(customer_id,delivery_order_id,exception_type)",
"KEYidx_ll_exception_inbound(customer_id,inbound_code)",
"ll_customer_visit(",
"UNIQUEKEYuk_ll_customer_visit_delivery(customer_id,delivery_order_id)",
"KEYidx_ll_customer_visit_list(customer_id,status,delivery_completed_at)");
assertThat(sql).contains(
"CREATE TABLE IF NOT EXISTS ll_exception_order",
"UNIQUE KEY uk_ll_exception_no(customer_id,exception_no)",
"UNIQUE KEY uk_ll_exception_delivery_return(customer_id,delivery_order_id,exception_type)",
"KEY idx_ll_exception_inbound(customer_id,inbound_code)",
"CREATE TABLE IF NOT EXISTS ll_customer_visit",
"UNIQUE KEY uk_ll_customer_visit_delivery(customer_id,delivery_order_id)",
"KEY idx_ll_customer_visit_list(customer_id,status,delivery_completed_at)",
"COMMENT='海外仓异常订单'",
"COMMENT='派送单维度客户回访记录'");
}
@@ -20,88 +20,73 @@ class Phase5OverseasUpgradeSqlContractTest {
"ll_exception_return_item", "ll_exception_order_command", "ll_customer_visit");
@Test
void overseasFixedEntryCreatesEveryPhase5TableBeforeAnyPhase5AlterOrAssertion() throws Exception {
void overseasFixedEntryCreatesEveryPhase5TableExactlyOnce() throws Exception {
String overseas = repositoryFile("deploy/sql/海外区域统一增量-MySQL8.sql");
int phase5Gate = overseas.indexOf("-- 五期排车、出库与海外PDA正式业务号/幂等约束");
assertThat(phase5Gate).isPositive();
for (String table : PHASE5_TABLES) {
String create = "CREATE TABLE IF NOT EXISTS " + table + " (";
assertThat(overseas.indexOf(create)).as(table).isBetween(0, phase5Gate - 1);
String create = "CREATE TABLE " + table + " (";
assertThat(overseas.indexOf(create)).as(table).isGreaterThanOrEqualTo(0);
assertThat(count(overseas, create)).as(table).isEqualTo(1);
assertThat(overseas.indexOf(table)).as(table + " first reference")
.isEqualTo(overseas.indexOf(create) + "CREATE TABLE IF NOT EXISTS ".length());
.isEqualTo(overseas.indexOf(create) + "CREATE TABLE ".length());
}
assertThat(overseas).contains("<> 23", "五期完整业务表集合未部署");
assertThat(overseas).doesNotContain("assert_phase5_lastleg_identifiers_migratable", "ALTER TABLE ll_");
}
@Test
void overseasFixedEntryConvergesKnownIntermediatePhase5ShapesWithoutBackfill() throws Exception {
void overseasFixedEntryContainsFinalPhase5ShapesWithoutMigrationBackfill() throws Exception {
String overseas = repositoryFile("deploy/sql/海外区域统一增量-MySQL8.sql");
String normalized = normalize(overseas);
assertThat(overseas).contains(
"ALTER TABLE ll_outbound_order RENAME COLUMN tenant_id TO customer_id",
"ALTER TABLE ll_outbound_pallet_item RENAME COLUMN tenant_id TO customer_id",
"ALTER TABLE ll_delivery_order RENAME COLUMN tenant_id TO customer_id",
"ALTER TABLE ll_outbound_order ADD COLUMN outbound_type",
"ALTER TABLE ll_outbound_order ADD COLUMN lx_codes",
"ALTER TABLE ll_outbound_pallet_item ADD COLUMN picked_package_count",
"ALTER TABLE ll_delivery_order ADD COLUMN overseas_warehouse_name",
"ALTER TABLE ll_delivery_order ADD COLUMN waybill_no",
"ALTER TABLE ll_dropship_order_profile ADD COLUMN source_pallet_no",
"ALTER TABLE ll_dropship_order_profile ADD COLUMN manifest_version",
"ALTER TABLE ll_dropship_order_profile ADD COLUMN manifest_package_count",
"ALTER TABLE ll_dropship_order_profile ADD COLUMN manifest_fragment_count",
"ALTER TABLE ll_dropship_order_profile ADD COLUMN manifest_hash",
"ALTER TABLE ll_inbound_plan_pallet ADD COLUMN version",
"ADD KEY idx_ll_dropship_profile_manifest(source_pallet_no,manifest_hash)",
"ADD UNIQUE KEY uk_ll_dropship_sku_inventory(profile_id,sku_code)",
"MODIFY COLUMN source_pallet_no VARCHAR(128) NOT NULL");
int relabelCreateEnd = overseas.indexOf(") ENGINE=InnoDB COMMENT='换标货物冻结明细';");
int relabelModify = overseas.indexOf("MODIFY COLUMN destination_fba_warehouse_code", relabelCreateEnd);
for (String column : List.of("destination_fba_warehouse_code", "source_inventory_balance_id",
"source_inventory_position_id", "source_inbound_order_id")) {
assertThat(overseas.indexOf("ALTER TABLE ll_relabel_item ADD COLUMN " + column, relabelCreateEnd))
.as(column + " converges before grouped MODIFY")
.isBetween(relabelCreateEnd, relabelModify - 1);
}
int start = overseas.indexOf("-- 五期正式结构必须由海外固定入口自行补齐");
int end = overseas.indexOf("USE baiyue_lastleg;", start);
String phase5 = overseas.substring(start, end);
assertThat(phase5).doesNotContain("\nUPDATE ", "\nDELETE ", "\nINSERT ");
assertThat(createStatement(overseas, "ll_outbound_order")).contains(
"customer_id", "outbound_type", "lx_codes", "request_hash");
assertThat(createStatement(overseas, "ll_outbound_pallet_item")).contains("picked_package_count");
assertThat(createStatement(overseas, "ll_delivery_order")).contains(
"customer_id", "overseas_warehouse_name", "waybill_no");
assertThat(createStatement(overseas, "ll_dropship_order_profile")).contains(
"source_pallet_no", "manifest_version", "manifest_package_count",
"manifest_fragment_count", "manifest_hash");
assertThat(normalized).contains(
"keyidx_ll_dropship_profile_manifest(source_pallet_no,manifest_hash)",
"uniquekeyuk_ll_dropship_sku_inventory(profile_id,sku_code)");
assertThat(createStatement(overseas, "ll_relabel_item")).contains(
"destination_fba_warehouse_code", "source_inventory_balance_id",
"source_inventory_position_id", "source_inbound_order_id");
assertThat(overseas).doesNotContain("\nUPDATE ", "\nDELETE ", "ALTER TABLE ll_");
}
@Test
void overseasFixedEntryUsesTheCurrentBaselineDefinitions() throws Exception {
void overseasFixedEntryAndModuleBaselineBothDefineCompletePhase5Tables() throws Exception {
String baseline = repositoryFile("lastleg-service/src/main/resources/db/lastleg.sql");
String overseas = repositoryFile("deploy/sql/海外区域统一增量-MySQL8.sql");
for (String table : PHASE5_TABLES) {
assertThat(normalize(createStatement(overseas, table)))
.as(table)
.isEqualTo(normalize(createStatement(baseline, table)));
assertThat(createStatement(overseas, table)).as(table)
.contains("PRIMARY KEY", "COMMENT='");
assertThat(createStatement(baseline, table)).as(table)
.contains("PRIMARY KEY", "COMMENT='");
}
}
@Test
void overseasFixedAdminProjectionAssertionMatchesItsAuthorityArray() throws Exception {
String overseas = repositoryFile("deploy/sql/海外区域统一增量-MySQL8.sql");
int start = overseas.indexOf("SET @platform_admin_overseas_authorities = JSON_ARRAY(");
int end = overseas.indexOf(");", start);
String authorityArray = overseas.substring(start, end);
int authorityCount = count(authorityArray, "'platform:");
int start = overseas.indexOf("VALUES ('AUTHORIZATION','1000001'");
int end = overseas.indexOf(";", start);
String authorityRow = overseas.substring(start, end);
int authorityCount = count(authorityRow, "platform:");
assertThat(authorityCount).isPositive();
assertThat(overseas).contains("JSON_LENGTH(authorities_json) <> " + authorityCount);
assertThat(authorityRow).contains("baseline-platform-admin-authorization-v0");
assertThat(overseas).doesNotContain("@platform_admin_overseas_authorities");
}
@Test
void overseasReturnOrderBackfillNormalizesLegacyColumnCollations() throws Exception {
void overseasReturnOrdersUseTheFinalCleanSchemaWithoutLegacyBackfill() throws Exception {
String overseas = repositoryFile("deploy/sql/海外区域统一增量-MySQL8.sql");
assertThat(overseas).contains(
"o.source_order_id COLLATE utf8mb4_unicode_ci = CAST(r.id AS CHAR) COLLATE utf8mb4_unicode_ci",
"o.source_order_no COLLATE utf8mb4_unicode_ci = r.return_no COLLATE utf8mb4_unicode_ci");
assertThat(overseas).contains("CREATE TABLE ll_return_order (", "CREATE TABLE ll_return_order_item (")
.doesNotContain("o.source_order_id COLLATE", "o.source_order_no COLLATE");
}
@Test
@@ -122,6 +107,10 @@ class Phase5OverseasUpgradeSqlContractTest {
private static String createStatement(String sql, String table) {
String token = "CREATE TABLE IF NOT EXISTS " + table + " (";
int start = sql.indexOf(token);
if (start < 0) {
token = "CREATE TABLE " + table + " (";
start = sql.indexOf(token);
}
int end = sql.indexOf(';', start);
assertThat(start).as(table).isGreaterThanOrEqualTo(0);
assertThat(end).as(table).isGreaterThan(start);
@@ -25,8 +25,8 @@ class SchemaChineseCommentContractTest {
private static final Pattern CHINESE = Pattern.compile("[\\u4e00-\\u9fff]");
private static final Pattern TABLE = Pattern.compile(
"(?is)CREATE\\s+TABLE\\s+IF\\s+NOT\\s+EXISTS\\s+(ll_[a-z0-9_]+)"
+ "\\s*\\((.*?)\\)\\s*ENGINE\\s*=\\s*InnoDB\\s+COMMENT\\s*=\\s*'([^']*)'\\s*;");
"(?is)CREATE\\s+TABLE\\s+(?:IF\\s+NOT\\s+EXISTS\\s+)?(ll_[a-z0-9_]+)"
+ "\\s*\\((.*?)\\)\\s*ENGINE\\s*=\\s*InnoDB[^;]*?COMMENT\\s*=\\s*'([^']*)'\\s*;");
@Test
void allLastlegTablesAndColumnsHaveChineseComments() throws Exception {
@@ -39,7 +39,7 @@ class SchemaChineseCommentContractTest {
List<TableDefinition> baselineTables = assertAllChineseComments(baseline, "基线 SQL", 56);
assertAllChineseComments(incremental, "增量 SQL", 50);
assertAllChineseComments(overseasUnified, "海外统一增量 SQL", 10);
assertAllChineseComments(overseasUnified, "海外统一初始化 SQL", 127);
assertCommentMigrationCovers(incremental, overseasUnified, baselineTables);
}
@@ -92,7 +92,9 @@ class SchemaChineseCommentContractTest {
for (MigrationColumn column : required) {
assertSpecificAlterationHasChineseComment(baseline, column);
}
assertAllAlteredColumnsHaveChineseComments(overseasUnified, "海外统一增量 SQL");
for (MigrationColumn column : required) {
assertTableColumnHasChineseComment(overseasUnified, column, "海外统一初始化 SQL");
}
}
private static List<TableDefinition> assertAllChineseComments(String sql, String source, int expectedCount) {
@@ -157,8 +159,8 @@ class SchemaChineseCommentContractTest {
private static boolean hasCommentedCreate(String sql, String table) {
return Pattern.compile(
"(?is)CREATE\\s+TABLE\\s+IF\\s+NOT\\s+EXISTS\\s+" + Pattern.quote(table)
+ "\\s*\\(.*?\\)\\s*ENGINE\\s*=\\s*InnoDB\\s+COMMENT\\s*=\\s*'[^']*[\\u4e00-\\u9fff][^']*'\\s*;")
"(?is)CREATE\\s+TABLE\\s+(?:IF\\s+NOT\\s+EXISTS\\s+)?" + Pattern.quote(table)
+ "\\s*\\(.*?\\)\\s*ENGINE\\s*=\\s*InnoDB[^;]*?COMMENT\\s*=\\s*'[^']*[\\u4e00-\\u9fff][^']*'\\s*;")
.matcher(sql).find();
}
@@ -199,6 +201,19 @@ class SchemaChineseCommentContractTest {
"字段增量缺少中文备注: " + target.table() + "." + target.column());
}
private static void assertTableColumnHasChineseComment(String sql, MigrationColumn target, String source) {
Matcher table = Pattern.compile(
"(?is)CREATE\\s+TABLE\\s+(?:IF\\s+NOT\\s+EXISTS\\s+)?" + Pattern.quote(target.table())
+ "\\s*\\((.*?)\\)\\s*ENGINE\\s*=\\s*InnoDB").matcher(sql);
assertTrue(table.find(), source + " 缺少表: " + target.table());
String definition = splitTopLevel(table.group(1)).stream()
.filter(value -> value.stripLeading().replace("`", "").startsWith(target.column() + " "))
.findFirst().orElse("");
assertTrue(Pattern.compile("(?is)\\bCOMMENT\\s+'[^']*[\\u4e00-\\u9fff][^']*'")
.matcher(definition).find(),
source + " 字段缺少中文备注: " + target.table() + "." + target.column());
}
private static void assertAllAlteredColumnsHaveChineseComments(String sql, String source) {
Pattern pattern = Pattern.compile("(?i)\\b(?:ADD|MODIFY)\\s+COLUMN\\s+([a-z0-9_]+)\\b");
Matcher matcher = pattern.matcher(sql);