From 36c131e3482f9e6ba5f9e4b07f86660b14390e85 Mon Sep 17 00:00:00 2001 From: ymq Date: Wed, 26 Aug 2026 15:22:18 +0800 Subject: [PATCH] =?UTF-8?q?fix(cleanup=5Forphans):=20=E8=A1=A5=20<>=20''?= =?UTF-8?q?=20=E9=9D=9E=E7=A9=BA=E5=AE=88=E5=8D=AB(=E7=A9=BA=E4=B8=B2=3D?= =?UTF-8?q?=E6=9C=AA=E6=8C=82=E8=BD=BD=E9=9D=9E=E5=AD=A4=E5=84=BF=EF=BC=8C?= =?UTF-8?q?=E9=98=B2=E8=AF=AF=E5=88=A032=E4=BC=9A=E8=AF=9D/23=E5=8A=9F?= =?UTF-8?q?=E8=83=BD)=EF=BC=9B=E8=A1=A5=E5=85=85=E6=97=A0=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE/=E6=97=A0=E8=BF=AD=E4=BB=A3/=E6=97=A0=E4=BB=BB?= =?UTF-8?q?=E5=8A=A1=E5=85=A8=E9=87=8F=E5=BC=95=E7=94=A8=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pipeline_service/project_capability.py | 91 +++++++++++--------------- 1 file changed, 40 insertions(+), 51 deletions(-) diff --git a/pipeline_service/project_capability.py b/pipeline_service/project_capability.py index 0b5a42d..98d1673 100644 --- a/pipeline_service/project_capability.py +++ b/pipeline_service/project_capability.py @@ -463,62 +463,50 @@ async def delete_project(project_id, who=None, agent_id=None, confirm=False): # ── 孤儿清扫 ──────────────────────────────────────────────────────── # -# 历史缺陷与手工测试会留下「指向已删项目/迭代/任务的孤儿行」。本函数逐表清扫: -# 孤儿判据 = 外键列非空,但其指向的主键在父表中已不存在。全部幂等,可反复执行。 +# 历史缺陷与手工测试会留下「指向已删项目/迭代/任务的孤儿行」。本函数逐表清扫。 # -# 列 collation 不一(sd_projects.id 是 unicode_ci,sd_project_repos/pipeline_deliverables/ -# audit_log 的引用列是 general_ci),跨表比较处显式 COLLATE utf8mb4_unicode_ci 规避 1267。 +# 孤儿判据 = 引用列【非空】且指向的主键在父表中已不存在。 +# 注意:空串 '' 与 NULL 都表示「未挂载」(合法),不是孤儿——若不加 `<> ''` 守卫, +# `NOT IN` 会把空串当成孤儿误删(实测 sd_conversations 32 条 / sd_features 23 条 iteration_id='' 被误判)。 +# +# 列 collation 不一(sd_projects.id / sd_iterations.id / pipeline_tasks.id 是 unicode_ci; +# sd_project_repos / pipeline_deliverables / sd_conversations 的引用列是 general_ci), +# 跨表比较处显式 COLLATE utf8mb4_unicode_ci 规避 1267。 +# (label, 表, 引用列, 父表, 是否需 COLLATE) _ORPHAN_CLEANUPS = [ - # (label, 计数SQL, 删除SQL) — 计数用 SELECT COUNT(*),删除用对应 DELETE - ("sd_project_repos(无项目)", - "SELECT COUNT(*) AS rcnt FROM sd_project_repos WHERE project_id COLLATE utf8mb4_unicode_ci NOT IN (SELECT id FROM sd_projects)", - "DELETE FROM sd_project_repos WHERE project_id COLLATE utf8mb4_unicode_ci NOT IN (SELECT id FROM sd_projects)"), - ("sd_features(无项目)", - "SELECT COUNT(*) AS rcnt FROM sd_features WHERE project_id NOT IN (SELECT id FROM sd_projects)", - "DELETE FROM sd_features WHERE project_id NOT IN (SELECT id FROM sd_projects)"), - ("sd_project_role_models(无项目)", - "SELECT COUNT(*) AS rcnt FROM sd_project_role_models WHERE project_id NOT IN (SELECT id FROM sd_projects)", - "DELETE FROM sd_project_role_models WHERE project_id NOT IN (SELECT id FROM sd_projects)"), - ("pipeline_deliverables(无项目)", - "SELECT COUNT(*) AS rcnt FROM pipeline_deliverables WHERE project_id COLLATE utf8mb4_unicode_ci NOT IN (SELECT id FROM sd_projects)", - "DELETE FROM pipeline_deliverables WHERE project_id COLLATE utf8mb4_unicode_ci NOT IN (SELECT id FROM sd_projects)"), - ("pipeline_deliverables(无任务)", - "SELECT COUNT(*) AS rcnt FROM pipeline_deliverables WHERE task_id COLLATE utf8mb4_unicode_ci NOT IN (SELECT id FROM pipeline_tasks)", - "DELETE FROM pipeline_deliverables WHERE task_id COLLATE utf8mb4_unicode_ci NOT IN (SELECT id FROM pipeline_tasks)"), - ("pipeline_artifacts(无任务)", - "SELECT COUNT(*) AS rcnt FROM pipeline_artifacts WHERE task_id NOT IN (SELECT id FROM pipeline_tasks)", - "DELETE FROM pipeline_artifacts WHERE task_id NOT IN (SELECT id FROM pipeline_tasks)"), - ("pipeline_task_steps(无任务)", - "SELECT COUNT(*) AS rcnt FROM pipeline_task_steps WHERE task_id NOT IN (SELECT id FROM pipeline_tasks)", - "DELETE FROM pipeline_task_steps WHERE task_id NOT IN (SELECT id FROM pipeline_tasks)"), - ("pipeline_human_tasks(无任务)", - "SELECT COUNT(*) AS rcnt FROM pipeline_human_tasks WHERE task_id NOT IN (SELECT id FROM pipeline_tasks)", - "DELETE FROM pipeline_human_tasks WHERE task_id NOT IN (SELECT id FROM pipeline_tasks)"), - ("pipeline_agent_questions(无任务)", - "SELECT COUNT(*) AS rcnt FROM pipeline_agent_questions WHERE task_id NOT IN (SELECT id FROM pipeline_tasks)", - "DELETE FROM pipeline_agent_questions WHERE task_id NOT IN (SELECT id FROM pipeline_tasks)"), - ("sd_features(无迭代)", - "SELECT COUNT(*) AS rcnt FROM sd_features WHERE iteration_id NOT IN (SELECT id FROM sd_iterations)", - "DELETE FROM sd_features WHERE iteration_id NOT IN (SELECT id FROM sd_iterations)"), - ("sd_bugs(无迭代)", - "SELECT COUNT(*) AS rcnt FROM sd_bugs WHERE iteration_id NOT IN (SELECT id FROM sd_iterations)", - "DELETE FROM sd_bugs WHERE iteration_id NOT IN (SELECT id FROM sd_iterations)"), - ("sd_test_plans(无迭代)", - "SELECT COUNT(*) AS rcnt FROM sd_test_plans WHERE iteration_id NOT IN (SELECT id FROM sd_iterations)", - "DELETE FROM sd_test_plans WHERE iteration_id NOT IN (SELECT id FROM sd_iterations)"), - ("sd_test_cases(无计划)", - "SELECT COUNT(*) AS rcnt FROM sd_test_cases WHERE plan_id NOT IN (SELECT id FROM sd_test_plans)", - "DELETE FROM sd_test_cases WHERE plan_id NOT IN (SELECT id FROM sd_test_plans)"), - ("sd_conversations(无迭代)", - "SELECT COUNT(*) AS rcnt FROM sd_conversations WHERE iteration_id COLLATE utf8mb4_unicode_ci NOT IN (SELECT id FROM sd_iterations)", - "DELETE FROM sd_conversations WHERE iteration_id COLLATE utf8mb4_unicode_ci NOT IN (SELECT id FROM sd_iterations)"), - ("sd_iterations(无项目)", - "SELECT COUNT(*) AS rcnt FROM sd_iterations WHERE project_id NOT IN (SELECT id FROM sd_projects)", - "DELETE FROM sd_iterations WHERE project_id NOT IN (SELECT id FROM sd_projects)"), + ("sd_project_repos(无项目)", "sd_project_repos", "project_id", "sd_projects", True), + ("sd_features(无项目)", "sd_features", "project_id", "sd_projects", False), + ("sd_project_role_models(无项目)", "sd_project_role_models", "project_id", "sd_projects", False), + ("sd_deploy_envs(无项目)", "sd_deploy_envs", "project_id", "sd_projects", False), + ("pipeline_deliverables(无项目)", "pipeline_deliverables", "project_id", "sd_projects", True), + ("sd_iterations(无项目)", "sd_iterations", "project_id", "sd_projects", False), + ("sd_features(无迭代)", "sd_features", "iteration_id", "sd_iterations", False), + ("sd_bugs(无迭代)", "sd_bugs", "iteration_id", "sd_iterations", False), + ("sd_test_plans(无迭代)", "sd_test_plans", "iteration_id", "sd_iterations", False), + ("sd_conversations(无迭代)", "sd_conversations", "iteration_id", "sd_iterations", True), + ("pipeline_deliverables(无迭代)", "pipeline_deliverables", "iteration_id", "sd_iterations", True), + ("sd_test_cases(无计划)", "sd_test_cases", "plan_id", "sd_test_plans", False), + ("pipeline_deliverables(无任务)", "pipeline_deliverables", "task_id", "pipeline_tasks", True), + ("pipeline_artifacts(无任务)", "pipeline_artifacts", "task_id", "pipeline_tasks", False), + ("pipeline_task_steps(无任务)", "pipeline_task_steps", "task_id", "pipeline_tasks", False), + ("pipeline_human_tasks(无任务)", "pipeline_human_tasks", "task_id", "pipeline_tasks", False), + ("pipeline_agent_questions(无任务)", "pipeline_agent_questions", "task_id", "pipeline_tasks", False), + ("sd_features(无任务)", "sd_features", "task_id", "pipeline_tasks", False), + ("sd_iterations(无任务)", "sd_iterations", "task_id", "pipeline_tasks", False), + ("sd_conversations(无任务)", "sd_conversations", "task_id", "pipeline_tasks", True), ] +def _orphan_sql(label, table, col, parent, use_collate): + """生成 (计数SQL, 删除SQL)。守卫:引用列非空(<> '' 排除 NULL 与空串)。""" + lhs = col + " COLLATE utf8mb4_unicode_ci" if use_collate else col + guard = f"{lhs} <> '' AND {lhs} NOT IN (SELECT id FROM {parent})" + cnt = f"SELECT COUNT(*) AS rcnt FROM {table} WHERE {guard}" + dlt = f"DELETE FROM {table} WHERE {guard}" + return cnt, dlt + + def _count_rows(recs): """从 SELECT COUNT(*) AS rcnt 结果取整数;兼容 DictObject/dict/list。""" if not recs: @@ -548,7 +536,8 @@ async def cleanup_orphans(confirm=False, dry_run=False): async with db.sqlorContext(dbname) as sor: results = [] total = 0 - for label, cnt_sql, del_sql in _ORPHAN_CLEANUPS: + for label, table, col, parent, use_collate in _ORPHAN_CLEANUPS: + cnt_sql, del_sql = _orphan_sql(label, table, col, parent, use_collate) try: recs = await sor.sqlExe(cnt_sql, {}) await sor.sqlExe("COMMIT", {})