迁移脚本补一条查漏刷的语句

原来那条只查重复组,而重复组本来就该留着原样,漏刷的行它一条都报不出来。补的那条报的是所有还留在原写法的行。两条都写成用例,测试从脚本里把语句取出来在真库上跑一遍,坏掉会直接报错。
This commit is contained in:
hrz committed 2026-10-23 06:37:34 +00:00
1 parent 425210d89e
commit 92ecb4cf07
2 files changed
+116 -1

No files matched your search

@@ -100,6 +100,33 @@ WHERE cnt.`n` = 1
-- WHERE `mac_address` REGEXP '^([0-9A-Fa-f]{2}[-:_]?){5}[0-9A-Fa-f]{2}$'
-- GROUP BY `nmac` HAVING `c` > 1;
--
-- ── 漏刷检查:刷完之后还有哪些行是原写法 ──────────────────────────────────
-- 上一条只找重复组,而重复组本来就该留着原样,所以它查不出漏刷:
-- 判重之外的行要是压根没被刷到,它一条也报不出来。
-- 下面这条报的是「还是原写法」的所有行。跑出结果不一定是漏刷——
-- 重复组在上面那条名单里的那些是故意留的,剩下的是真漏刷。
--
-- 第 ① 段在有重复组的表上整段刷不动过一次:MySQL 26.7.0 实测,
-- 该刷的行一行没动、ROW_COUNT() 报 0、退出码 0、不打日志、不报错。
-- 没有报错可等,只能靠这条查。
--
-- SELECT `id`, `mac_address` AS `still_original`,
-- LOWER(CONCAT(SUBSTRING(REPLACE(REPLACE(REPLACE(`mac_address`,':',''),'-',''),'_',''), 1, 2), ':',
-- SUBSTRING(REPLACE(REPLACE(REPLACE(`mac_address`,':',''),'-',''),'_',''), 3, 2), ':',
-- SUBSTRING(REPLACE(REPLACE(REPLACE(`mac_address`,':',''),'-',''),'_',''), 5, 2), ':',
-- SUBSTRING(REPLACE(REPLACE(REPLACE(`mac_address`,':',''),'-',''),'_',''), 7, 2), ':',
-- SUBSTRING(REPLACE(REPLACE(REPLACE(`mac_address`,':',''),'-',''),'_',''), 9, 2), ':',
-- SUBSTRING(REPLACE(REPLACE(REPLACE(`mac_address`,':',''),'-',''),'_',''), 11, 2))) AS `should_be`
-- FROM `ai_device`
-- WHERE `mac_address` REGEXP '^([0-9A-Fa-f]{2}[-:_]?){5}[0-9A-Fa-f]{2}$'
-- AND BINARY `mac_address` <> BINARY LOWER(CONCAT(SUBSTRING(REPLACE(REPLACE(REPLACE(`mac_address`,':',''),'-',''),'_',''), 1, 2), ':',
-- SUBSTRING(REPLACE(REPLACE(REPLACE(`mac_address`,':',''),'-',''),'_',''), 3, 2), ':',
-- SUBSTRING(REPLACE(REPLACE(REPLACE(`mac_address`,':',''),'-',''),'_',''), 5, 2), ':',
-- SUBSTRING(REPLACE(REPLACE(REPLACE(`mac_address`,':',''),'-',''),'_',''), 7, 2), ':',
-- SUBSTRING(REPLACE(REPLACE(REPLACE(`mac_address`,':',''),'-',''),'_',''), 9, 2), ':',
-- SUBSTRING(REPLACE(REPLACE(REPLACE(`mac_address`,':',''),'-',''),'_',''), 11, 2)))
-- ORDER BY `id`;
--
-- 三段合在一个 changeset 里,也就是一个事务(Liquibase 默认 runInTransaction 为真),
-- 任何一段报错都会整体回滚、服务起不来。所以它们不该去撞,上面的 cnt 就是为这个。
-- 值已经是对的三段都是空操作,重跑安全。
@@ -1,6 +1,7 @@
package xiaozhi.modules.device.service.impl;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -211,6 +212,45 @@ class MacAddressMigrationSqlTest {
"撞的是主键唯一性,没撞上说明这组数据造得不对:" + failure.getMessage());
}
@Test
@DisplayName("排查重复的那条 SQL 报出被跳过的组")
void duplicateDiagnosticListsTheSkippedGroup() throws SQLException {
insertDevice("d1", "AA-BB-CC-DD-EE-01");
insertDevice("d2", "aabbccddee01");
insertDevice("x1", "11-22-33-44-55-66");
runMigration();
assertEquals(List.of("aa:bb:cc:dd:ee:01"),
queryColumn(diagnostic("③ 刷完之后跑这个"), 0),
"只该报出被判重跳过的那个组,刷完了的行不该出现在名单里");
}
@Test
@DisplayName("漏刷检查报出还留在原写法的行")
void leftoverCheckFindsRowsStillInOriginalSpelling() throws SQLException {
insertDevice("d1", "AA-BB-CC-DD-EE-01");
insertDevice("d2", "aabbccddee01");
insertDevice("x1", "11-22-33-44-55-66");
insertDevice("u1", "AA:BB:CC:DD:EE:99");
runMigration();
assertEquals(List.of("AA-BB-CC-DD-EE-01", "aabbccddee01"),
queryColumn(diagnostic("漏刷检查"), 1),
"刷完只剩判重跳过的那两行是原写法,x1、u1 都该刷掉");
}
@Test
@DisplayName("漏刷检查能报出该刷没刷出来的行")
void leftoverCheckFindsRowsTheMigrationNeverTouched() throws SQLException {
insertDevice("d1", "AA-BB-CC-DD-EE-01");
insertDevice("d2", "aabbccddee01");
insertDevice("x1", "11-22-33-44-55-66");
assertEquals(List.of("d1", "d2", "x1"),
queryColumn(diagnostic("漏刷检查"), 0),
"整段迁移一行没动时漏刷检查要全报出来,不然迁移只做了一半没人知道");
}
/** 按 Liquibase 的 sqlFile 那样读脚本:去注释,按分号切,在一个事务里逐句执行 */
private void runMigration() throws SQLException {
run(readChangelog("202610231200.sql"));
@@ -298,4 +338,52 @@ class MacAddressMigrationSqlTest {
throw new IllegalStateException("读不到 " + fileName, e);
}
}
}
/**
* 把脚本里注释掉的那段排查 SQL 整条取出来
*
* <p>
* 脚本里那一段是给人手工跑的,跑不走就等于没写。取出来在真库上执行一遍,
* 段里的 SQL 坏了、引用了不存在的列,这里会直接报错
*/
private static String diagnostic(String marker) {
List<String> collected = new ArrayList<>();
boolean inBlock = false;
for (String line : readChangelog("202610231200.sql").split("\n")) {
if (!line.stripLeading().startsWith("--")) {
if (!collected.isEmpty()) {
break;
}
continue;
}
if (!inBlock) {
inBlock = line.contains(marker);
if (!inBlock) {
continue;
}
}
String body = line.stripLeading().substring(2).strip();
if (collected.isEmpty() && !body.startsWith("SELECT")) {
// 说明文字,去掉行首的 -- 之后就不是注释了,不能拼进语句里
continue;
}
collected.add(body);
if (body.endsWith(";")) {
break;
}
}
assertFalse(collected.isEmpty(), "脚本里找不到 " + marker + " 那段排查 SQL");
return String.join("\n", collected);
}
private List<String> queryColumn(String sql, int column) throws SQLException {
List<String> values = new ArrayList<>();
try (Statement statement = connection.createStatement();
ResultSet rows = statement.executeQuery(sql)) {
while (rows.next()) {
values.add(rows.getString(column + 1));
}
}
return values;
}
}