diff --git a/backend-java/src/main/resources/inspection/01_orphan_job.sql b/backend-java/src/main/resources/inspection/01_orphan_job.sql new file mode 100644 index 00000000..af04924e --- /dev/null +++ b/backend-java/src/main/resources/inspection/01_orphan_job.sql @@ -0,0 +1,17 @@ +-- ============================================================= +-- 巡检 01:孤儿 task_file_job(job 无对应任务/结果)— 只读报表(task-194) +-- 对应 Java 巡检:OrphanJobInspector(module-09);仅副本库只读执行,无副作用。 +-- 结果按 updated_at 升序;每批建议 limit <= 500。 +-- ============================================================= +SELECT j.id AS job_id, + j.task_id, + j.result_id, + j.module_type, + j.status, + j.updated_at +FROM biz_task_file_job j +WHERE NOT EXISTS (SELECT 1 FROM biz_file_task t WHERE t.id = j.task_id) + AND (j.result_id IS NULL + OR NOT EXISTS (SELECT 1 FROM biz_file_result r WHERE r.id = j.result_id)) +ORDER BY j.updated_at ASC +LIMIT 200; diff --git a/backend-java/src/test/java/com/nanri/aiimage/config/InspectionSqlFileTestBase.java b/backend-java/src/test/java/com/nanri/aiimage/config/InspectionSqlFileTestBase.java new file mode 100644 index 00000000..030bd050 --- /dev/null +++ b/backend-java/src/test/java/com/nanri/aiimage/config/InspectionSqlFileTestBase.java @@ -0,0 +1,97 @@ +package com.nanri.aiimage.config; + +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * 巡检只读 SQL 报表静态契约基座(task-194..199)。 + * 校验各 inspection/*.sql:以 SELECT 开头、无任何写语句/锁语句、含 NOT EXISTS/JOIN 关联、 + * 带 LIMIT、分号结尾、头注释含目的与只读声明。子类提供文件名。 + */ +abstract class InspectionSqlFileTestBase { + + abstract String fileName(); + + private Path path() { + return Path.of(System.getProperty("user.dir"), "src/main/resources/inspection", fileName()); + } + + private String sql() throws IOException { + return Files.readString(path()); + } + + private String statements() throws IOException { + // 去掉整行注释与空行后的可执行 SQL 文本(头注释说明可能含"清理/删除"等字样,须排除) + StringBuilder sb = new StringBuilder(); + for (String line : sql().split("\n")) { + String t = line.trim(); + if (t.isEmpty() || t.startsWith("--")) { + continue; + } + sb.append(line).append('\n'); + } + return sb.toString(); + } + + @Test + void fileExists() throws IOException { + assertTrue(Files.exists(path()), "巡检 SQL 应存在: " + fileName()); + } + + @Test + void startsWithSelect() throws IOException { + String first = statements().stripLeading(); + assertTrue(first.toUpperCase().startsWith("SELECT"), "可执行 SQL 应以 SELECT 开头: " + fileName()); + } + + private static final String[] FORBIDDEN_WRITE_TOKENS = { + "INSERT", "UPDATE", "DELETE", "DROP", "TRUNCATE", "ALTER", "CREATE", + "GRANT", "REPLACE", "CALL", "LOAD", "DO" + }; + + @Test + void readOnlyNoWriteKeywords() throws IOException { + String upper = statements().toUpperCase(); + for (String kw : FORBIDDEN_WRITE_TOKENS) { + java.util.regex.Pattern p = java.util.regex.Pattern.compile("(?