package com.baiyue.lastleg.schema; import org.junit.jupiter.api.Test; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; class SchemaChineseCommentContractTest { private static final Set MODULE_BASELINE_ONLY_TABLES = Set.of( "ll_us_truck_order_operation", "ll_local_looseboard_operation", "ll_receiving_warehouse_operation"); private static final Set MODULE_BASELINE_ONLY_COLUMNS = Set.of( "ll_order.overseas_warehouse_code", "ll_order.receiving_warehouse_version", "ll_receiving_container_session.overseas_warehouse_code"); 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[^;]*?COMMENT\\s*=\\s*'([^']*)'\\s*;"); @Test void allLastlegTablesAndColumnsHaveChineseComments() throws Exception { String baseline = Files.readString( Path.of("src", "main", "resources", "db", "lastleg.sql"), StandardCharsets.UTF_8); String incremental = Files.readString( Path.of("..", "scripts", "db", "incremental.sql"), StandardCharsets.UTF_8); String overseasUnified = Files.readString( Path.of("..", "deploy", "sql", "海外区域统一增量-MySQL8.sql"), StandardCharsets.UTF_8); List baselineTables = assertAllChineseComments(baseline, "基线 SQL", 56); assertAllChineseComments(incremental, "增量 SQL", 50); assertAllChineseComments(overseasUnified, "统一测试初始化 SQL", 126); assertFalse(overseasUnified.contains("CREATE TABLE ll_security_projection")); assertFalse(overseasUnified.contains("CREATE TABLE ll_security_event_inbox")); assertCommentMigrationCovers(incremental, overseasUnified, baselineTables); } @Test void newlyAddedColumnsCarryChineseCommentsInBaselineAndIncrementalSql() throws Exception { String baseline = Files.readString( Path.of("src", "main", "resources", "db", "lastleg.sql"), StandardCharsets.UTF_8); String incremental = Files.readString( Path.of("..", "scripts", "db", "incremental.sql"), StandardCharsets.UTF_8); List columns = List.of( "allocation_idempotency_key", "allocation_request_hash", "outbound_idempotency_key", "outbound_request_hash", "outbound_at", "pallet_count", "received_pallet_count", "allocated_pallet_count" ); for (String column : columns) { assertColumnHasChineseComment(baseline, column, "基线 SQL"); assertColumnHasChineseComment(incremental, column, "增量 SQL"); assertIncrementalAddColumnHasChineseComment(incremental, column); } for (String column : List.of("review_idempotency_key", "review_request_hash", "result_version")) { assertColumnHasChineseComment(baseline, column, "基线 SQL"); } } @Test void inboundPlanMigrationsPreserveChineseCommentsForEveryAlteredColumn() throws Exception { String baseline = Files.readString( Path.of("src", "main", "resources", "db", "lastleg.sql"), StandardCharsets.UTF_8); String overseasUnified = Files.readString( Path.of("..", "deploy", "sql", "海外区域统一增量-MySQL8.sql"), StandardCharsets.UTF_8); List required = List.of( new MigrationColumn("ll_inbound_plan", "status"), new MigrationColumn("ll_inbound_plan_line", "required_package_count"), new MigrationColumn("ll_inbound_plan_pallet", "actual_package_count"), new MigrationColumn("ll_inbound_plan_pallet", "actual_volume_cbm"), new MigrationColumn("ll_inbound_plan_pallet", "assignment_status"), new MigrationColumn("ll_inbound_plan_pallet", "locked_at"), new MigrationColumn("ll_inbound_plan_pallet", "warehouse_location_id"), new MigrationColumn("ll_inbound_plan_pallet", "warehouse_pallet_id"), new MigrationColumn("ll_inbound_plan_pallet", "warehouse_resource_lock_id"), new MigrationColumn("ll_inbound_plan_pallet_allocation", "actual_volume_cbm"), new MigrationColumn("ll_inbound_plan_pallet_allocation", "counted_package_count"), new MigrationColumn("ll_inbound_plan_pallet_allocation", "picked_package_count"), new MigrationColumn("ll_inbound_plan_pallet_allocation", "planned_package_count"), new MigrationColumn("ll_inbound_plan_pallet_allocation", "status"), new MigrationColumn("ll_inbound_plan_pallet_allocation", "version") ); for (MigrationColumn column : required) { assertSpecificAlterationHasChineseComment(baseline, column); } for (MigrationColumn column : required) { assertTableColumnHasChineseComment(overseasUnified, column, "海外统一初始化 SQL"); } } private static List assertAllChineseComments(String sql, String source, int expectedCount) { List tables = new ArrayList<>(); Matcher matcher = TABLE.matcher(sql); while (matcher.find()) { String table = matcher.group(1); assertTrue(CHINESE.matcher(matcher.group(3)).find(), source + " 表备注必须包含中文: " + table); List columns = splitTopLevel(matcher.group(2)); assertFalse(columns.isEmpty(), source + " 表定义没有字段: " + table); List columnNames = new ArrayList<>(); for (String definition : columns) { if (isConstraint(definition)) { continue; } String column = definition.stripLeading().split("\\s+", 2)[0].replace("`", ""); columnNames.add(column); assertTrue(Pattern.compile("(?is)\\bCOMMENT\\s+'[^']*[\\u4e00-\\u9fff][^']*'") .matcher(definition).find(), source + " 字段备注必须包含中文: " + table + "." + column); } tables.add(new TableDefinition(table, columnNames)); } assertTrue(tables.size() >= expectedCount, source + " 解析到的尾程表数量不足,实际=" + tables.size() + ",期望至少=" + expectedCount); return tables; } private static void assertCommentMigrationCovers( String incremental, String overseasUnified, List tables ) { int marker = incremental.indexOf("2026-08-18 / 025 Chinese table and column comments"); assertTrue(marker >= 0, "增量 SQL 缺少 025 中文备注迁移"); String migration = incremental.substring(marker); for (TableDefinition table : tables) { if (MODULE_BASELINE_ONLY_TABLES.contains(table.name())) { continue; } boolean createdInLegacy = hasCommentedCreate(migration, table.name()); boolean createdInUnified = hasCommentedCreate(overseasUnified, table.name()); assertTrue(createdInLegacy || createdInUnified || migration.contains("table_name='" + table.name() + "'"), "025 备注迁移缺少表: " + table.name()); if (createdInLegacy || createdInUnified) { continue; } for (String column : table.columns()) { if (MODULE_BASELINE_ONLY_COLUMNS.contains(table.name() + "." + column)) { continue; } boolean repaired = migration.contains("table_name='" + table.name() + "' AND column_name='" + column + "'"); boolean addedWithComment = hasCommentedAdd(migration, column) || hasCommentedAdd(overseasUnified, column); assertTrue(repaired || addedWithComment, "025 备注迁移缺少字段: " + table.name() + "." + column); } } } 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[^;]*?COMMENT\\s*=\\s*'[^']*[\\u4e00-\\u9fff][^']*'\\s*;") .matcher(sql).find(); } private static boolean hasCommentedAdd(String sql, String column) { return Pattern.compile( "(?is)ADD\\s+COLUMN\\s+" + Pattern.quote(column) + "\\b[^;]*?COMMENT\\s+''?[^;]*[\\u4e00-\\u9fff][^;]*''?") .matcher(sql).find(); } private static boolean isConstraint(String definition) { String normalized = definition.stripLeading().toUpperCase(); return normalized.startsWith("PRIMARY ") || normalized.startsWith("UNIQUE ") || normalized.startsWith("KEY ") || normalized.startsWith("CONSTRAINT ") || normalized.startsWith("FOREIGN ") || normalized.startsWith("CHECK "); } private static void assertColumnHasChineseComment(String sql, String column, String source) { Pattern columnPattern = Pattern.compile( "(?is)\\b" + Pattern.quote(column) + "\\b[^,;]*?COMMENT\\s+''?[^',;]*[\\u4e00-\\u9fff][^',;]*''?"); assertTrue(columnPattern.matcher(sql).find(), source + " 新增字段缺少中文备注: " + column); } private static void assertIncrementalAddColumnHasChineseComment(String sql, String column) { Pattern addColumnPattern = Pattern.compile( "(?is)ADD\\s+COLUMN\\s+" + Pattern.quote(column) + "\\b[^;]*?COMMENT\\s+''[^';]*[\\u4e00-\\u9fff][^';]*''"); assertTrue(addColumnPattern.matcher(sql).find(), "增量 SQL 的 ADD COLUMN 缺少中文备注: " + column); } private static void assertSpecificAlterationHasChineseComment(String sql, MigrationColumn target) { Pattern pattern = Pattern.compile( "(?is)ALTER\\s+TABLE\\s+" + Pattern.quote(target.table()) + "\\s+(?:ADD|MODIFY)\\s+COLUMN\\s+" + Pattern.quote(target.column()) + "\\b"); Matcher matcher = pattern.matcher(sql); assertTrue(matcher.find(), "缺少字段增量定义: " + target.table() + "." + target.column()); assertAlterationSegmentHasChineseComment(sql, matcher.start(), matcher.end(), "字段增量缺少中文备注: " + 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); int count = 0; while (matcher.find()) { count++; assertAlterationSegmentHasChineseComment(sql, matcher.start(), matcher.end(), source + " 字段增量缺少中文备注: " + matcher.group(1)); } assertTrue(count > 0, source + " 未解析到 ADD/MODIFY COLUMN"); } private static void assertAlterationSegmentHasChineseComment( String sql, int start, int tokenEnd, String message ) { Matcher next = Pattern.compile("(?i)\\b(?:ADD|MODIFY)\\s+COLUMN\\s+[a-z0-9_]+\\b") .matcher(sql); int end = sql.indexOf(';', tokenEnd); if (end < 0) end = sql.length(); if (next.find(tokenEnd) && next.start() < end) end = next.start(); String definition = sql.substring(start, end); assertTrue(Pattern.compile("(?is)\\bCOMMENT\\s+''?[^;]*[\\u4e00-\\u9fff][^;]*''?") .matcher(definition).find(), message); } private static List splitTopLevel(String body) { List definitions = new ArrayList<>(); int start = 0; int parentheses = 0; boolean quoted = false; for (int i = 0; i < body.length(); i++) { char current = body.charAt(i); if (current == '\'' && (i == 0 || body.charAt(i - 1) != '\\')) { quoted = !quoted; } else if (!quoted && current == '(') { parentheses++; } else if (!quoted && current == ')') { parentheses--; } else if (!quoted && parentheses == 0 && current == ',') { definitions.add(body.substring(start, i).trim()); start = i + 1; } } definitions.add(body.substring(start).trim()); return definitions.stream().filter(value -> !value.isEmpty()).toList(); } private record TableDefinition(String name, List columns) { } private record MigrationColumn(String table, String column) { } }