"""为育种核心表补「部分唯一索引(未软删)」并发兜底,幂等,可 --check 预览。 背景:combination_code / tree_no / cultivar_name / personnel.name / (site_id, plot_code) 无 DB 层唯一约束,service 层 select-then-insert 存在并发竞态。此处补部分唯一索引 (WHERE is_deleted = false),与各 service 的查重语义(只查未软删)一致: - 未软删记录之间不允许重复 → 并发插入同一编号仅一条成功,另一条 409。 - 已软删记录不参与约束 → 删除后仍可重新使用该编号。 用法(backend/ 目录): python scripts/add_unique_constraints.py --check python scripts/add_unique_constraints.py """ import argparse import os import sys BACKEND_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) if BACKEND_DIR not in sys.path: sys.path.insert(0, BACKEND_DIR) from sqlalchemy import create_engine, text from app.config.setting import settings # (表, 索引名, 唯一列, 说明) UNIQUE_INDEXES = [ ("bre_cross_combination", "uniq_bre_cross_combination_code", "combination_code", "组合编号"), ("bre_tree", "uniq_bre_tree_no", "tree_no", "单株编号"), ("bre_germplasm", "uniq_bre_germplasm_cultivar_name", "cultivar_name", "品种/种质名称"), ("bre_personnel", "uniq_bre_personnel_name", "name", "人员姓名"), ("bre_plot", "uniq_bre_plot_site_code", "site_id, plot_code", "基地地块编号"), ] def main() -> None: parser = argparse.ArgumentParser(description="为育种表补部分唯一索引(未软删)") parser.add_argument("--check", action="store_true", help="仅打印将执行的 DDL,不落库") args = parser.parse_args() engine = create_engine(settings.DB_URI) print(f"# 连接: {settings.DB_URI}") with engine.connect() as conn: for table, idx_name, cols, label in UNIQUE_INDEXES: # 1) 未软删重复检查(部分索引只约束未软删行,故只查未软删) dup_sql = ( f"SELECT {cols} FROM {table} " "WHERE is_deleted = false " f"GROUP BY {cols} HAVING COUNT(*) > 1" ) try: dups = conn.execute(text(dup_sql)).fetchall() except Exception as e: print(f"# [跳过] {table}: 查重失败 {e!s}") continue if dups: print(f"# [跳过] {label}({table}.{cols})存在 {len(dups)} 组未软删重复,请先清洗数据:") for d in dups[:10]: print(" " + " / ".join(str(x) for x in d)) continue # 2) 索引是否已存在 exists = conn.execute( text("SELECT 1 FROM pg_indexes WHERE indexname = :name"), {"name": idx_name}, ).fetchone() if exists: print(f"# [已存在] {idx_name}") continue ddl = ( f"CREATE UNIQUE INDEX {idx_name} ON {table} ({cols}) " "WHERE is_deleted = false" ) print(" " + ddl) if not args.check: try: with engine.begin() as c: c.execute(text(ddl)) print("OK " + ddl) except Exception as e: print("ERR " + ddl + " -> " + str(e)[:160]) print("# --check 模式:未执行任何变更。" if args.check else "# 完成。") if __name__ == "__main__": main()