fix(cleanup_orphans): 补 <> '' 非空守卫(空串=未挂载非孤儿,防误删32会话/23功能);补充无项目/无迭代/无任务全量引用表

This commit is contained in:
ymq 2026-08-26 15:22:18 +08:00
parent 8eeb196515
commit 36c131e348

View File

@ -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", {})