Files
dpb/backend/scripts/add_unique_constraints.py
T
34047007@qq.com b95053c52c init: 初始化 dpb 桃育种系统代码库
前后端 + 后端 FastAPI 全量源码、部署脚本与文档。
2026-08-06 00:17:49 +08:00

89 lines
3.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""为育种核心表补「部分唯一索引(未软删)」并发兜底,幂等,可 --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()