package com.nanri.aiimage.config; import org.junit.jupiter.api.Test; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.List; import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; /** * 任务 116:索引迁移 V{N+1}(审计清单落地)。 * 迁移文件存在且版本续接;按 115 审计清单追加 (module_type, job_type, task_id) 索引; * 含验证 SQL、锁表风险标注、回滚步骤;不改历史迁移。 */ class IndexMigrationV101Test { private static final Path DB_DIR = Paths.get("src", "main", "resources", "db"); private static final Path MIGRATION = DB_DIR.resolve("V101__task_file_job_module_type_task_index.sql"); private static final Path AUDIT = Paths.get("src", "main", "resources", "..", "..", "..", "docs", "explain-index-audit.md").normalize(); private static String read(Path path) throws IOException { return new String(Files.readAllBytes(path), StandardCharsets.UTF_8); } private static List allMigrations() throws IOException { try (Stream stream = Files.list(DB_DIR)) { return stream.filter(p -> p.getFileName().toString().matches("V\\d+__.*\\.sql")) .sorted() .toList(); } } private static int versionOf(Path path) { String name = path.getFileName().toString(); return Integer.parseInt(name.substring(1, name.indexOf('_'))); } @Test void test_migration_file_exists() { assertTrue(Files.isRegularFile(MIGRATION), "迁移文件必须存在: " + MIGRATION); } @Test void test_migration_version_sequential() throws Exception { List all = allMigrations(); assertTrue(!all.isEmpty(), "存在迁移文件"); int maxVersion = all.stream().mapToInt(IndexMigrationV101Test::versionOf).max().orElse(0); assertTrue(maxVersion >= 100, "现有迁移最大版本 >= V100: " + maxVersion); assertTrue(versionOf(MIGRATION) == 101, "新迁移版本号 V101 续接 V100 之后"); } @Test void test_migration_index_created() throws Exception { String sql = read(MIGRATION); assertTrue(sql.contains("biz_task_file_job"), "迁移作用于 biz_task_file_job"); assertTrue(sql.contains("idx_file_job_module_type_task"), "索引名与审计清单一致"); assertTrue(sql.contains("module_type") && sql.contains("job_type") && sql.contains("task_id"), "索引列为 (module_type, job_type, task_id)"); assertTrue(sql.contains("ADD INDEX"), "使用 ADD INDEX"); } @Test void test_migration_conditional_repeatable() throws Exception { String sql = read(MIGRATION); assertTrue(sql.contains("information_schema.STATISTICS"), "通过 information_schema 判存在"); assertTrue(sql.contains("INDEX_NAME"), "按索引名判断"); assertTrue(sql.contains("PREPARE") && sql.contains("EXECUTE"), "条件执行(幂等,可重复运行)"); } @Test void test_migration_validated() throws Exception { String sql = read(MIGRATION); assertTrue(sql.contains("SELECT 1"), "验证 SQL(幂等分支)"); String audit = read(AUDIT); assertTrue(audit.contains("idx_file_job_module_type_task"), "审计清单已列出该索引"); assertTrue(audit.contains("收益") && audit.contains("风险"), "审计清单含收益/风险"); } @Test void test_explain_improved() throws Exception { String audit = read(AUDIT); assertTrue(audit.contains("findAssembleJobsByTaskIds"), "审计覆盖 progress/light Job 查询"); assertTrue(audit.contains("执行后重跑") || audit.contains("前后计划") || audit.contains("对比"), "审计含迁移前后 EXPLAIN 对比步骤"); } @Test void test_migration_no_alter_legacy() throws Exception { List all = allMigrations(); List others = all.stream() .filter(p -> versionOf(p) != 101) .toList(); for (Path legacy : others) { String content = read(legacy); assertFalse(content.contains("idx_file_job_module_type_task"), "历史迁移不得包含新索引: " + legacy.getFileName()); } } @Test void test_migration_rollback_doc() throws Exception { String sql = read(MIGRATION); assertTrue(sql.contains("DROP INDEX") || sql.contains("drop index"), "迁移文件含回滚语句(DROP INDEX idx_file_job_module_type_task)"); String audit = read(AUDIT); assertTrue(audit.contains("回滚") || audit.contains("DROP"), "审计文档含回滚步骤说明"); } @Test void test_migration_lock_window() throws Exception { String sql = read(MIGRATION); assertTrue(sql.contains("LOCK") || sql.contains("低峰") || sql.contains("lock") || sql.contains("窗口"), "迁移标注锁表风险/窗口"); } @Test void test_migration_backward_safe() throws Exception { String sql = read(MIGRATION); assertTrue(sql.contains("ALTER TABLE"), "普通 BTREE 索引(不重建表、不删列)"); assertFalse(sql.contains("DROP COLUMN"), "不删列"); assertFalse(sql.contains("RENAME"), "不重命名"); assertTrue(sql.contains("ADD INDEX"), "仅追加索引,向后兼容"); } }