oracle_gen_merge.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """从 MySQL 源库导出数据,生成 Oracle MERGE SQL,供 sqlplus 执行。"""
  4. from __future__ import annotations
  5. import argparse
  6. import sys
  7. from datetime import datetime
  8. from pathlib import Path
  9. from typing import Any, Iterable, List, Optional, Sequence, Tuple
  10. try:
  11. import pymysql
  12. except ImportError:
  13. print("ERROR: 需要安装 pymysql: pip install pymysql", file=sys.stderr)
  14. sys.exit(1)
  15. def connect_mysql(host: str, port: int, user: str, password: str, database: str):
  16. return pymysql.connect(
  17. host=host,
  18. port=port,
  19. user=user,
  20. password=password,
  21. database=database,
  22. charset="utf8mb4",
  23. cursorclass=pymysql.cursors.DictCursor,
  24. autocommit=True,
  25. )
  26. def q(val: Any, max_len: Optional[int] = None) -> str:
  27. """Python 值转 Oracle SQL 字面量。"""
  28. if val is None:
  29. return "NULL"
  30. if isinstance(val, datetime):
  31. s = val.strftime("%Y%m%d%H%M%S")
  32. else:
  33. s = str(val).strip()
  34. if s == "":
  35. return "NULL"
  36. if max_len is not None:
  37. s = s[:max_len]
  38. s = s.replace("'", "''")
  39. return f"'{s}'"
  40. def right10(val: Any) -> Optional[str]:
  41. if val is None:
  42. return None
  43. s = str(val).strip()
  44. if not s or s == "0":
  45. return None
  46. return s[-10:]
  47. def fmt_dt(val: Any, fmt: str = "%Y%m%d%H%M%S") -> Optional[str]:
  48. if val is None:
  49. return None
  50. if isinstance(val, datetime):
  51. return val.strftime(fmt)
  52. s = str(val).strip()
  53. return s or None
  54. def sex_code(val: Any) -> str:
  55. s = "" if val is None else str(val)
  56. if s == "0":
  57. return "1"
  58. if s == "1":
  59. return "2"
  60. return "0"
  61. def write_header(out) -> None:
  62. out.write("-- generated by oracle_gen_merge.py\n")
  63. out.write("WHENEVER SQLERROR EXIT SQL.SQLCODE\n")
  64. out.write("SET ECHO OFF\n")
  65. out.write("SET FEEDBACK OFF\n")
  66. out.write("SET DEFINE OFF\n")
  67. out.write("SET SERVEROUTPUT ON SIZE UNLIMITED\n")
  68. out.write("\n")
  69. def commit_batch(out, count: int, label: str) -> None:
  70. out.write("COMMIT;\n")
  71. out.write(f"PROMPT {label} committed batch, total so far: {count}\n")
  72. out.write("\n")
  73. def gen_dept(cur, out, incr_sql: str, batch_size: int) -> int:
  74. sql = f"""
  75. SELECT
  76. d.dept_id,
  77. d.dept_name,
  78. d.parent_id,
  79. d.status,
  80. d.del_flag,
  81. IFNULL(d.update_time, d.create_time) AS ts
  82. FROM sys_dept d
  83. WHERE 1=1
  84. {incr_sql}
  85. """
  86. cur.execute(sql)
  87. n = 0
  88. for row in cur:
  89. dwh = right10(row["dept_id"])
  90. if not dwh:
  91. continue
  92. lsdwh = right10(row["parent_id"])
  93. dwyxbs = "1" if row["status"] == "0" and row["del_flag"] == "0" else "0"
  94. scbz = "y" if row["del_flag"] == "1" else "n"
  95. zhgxsj = fmt_dt(row["ts"])
  96. out.write(
  97. f"""MERGE INTO GXXX_YXSDWJBSJZL t
  98. USING (SELECT {q(dwh, 10)} AS dwh FROM dual) s
  99. ON (t.dwh = s.dwh)
  100. WHEN MATCHED THEN UPDATE SET
  101. t.dwmc = {q(row['dept_name'], 60)},
  102. t.lsdwh = {q(lsdwh, 10)},
  103. t.dwyxbs = {q(dwyxbs, 1)},
  104. t.zhgxsj = {q(zhgxsj, 20)},
  105. t.scbz = {q(scbz, 1)}
  106. WHEN NOT MATCHED THEN INSERT (dwh, dwmc, lsdwh, dwyxbs, zhgxsj, scbz)
  107. VALUES ({q(dwh, 10)}, {q(row['dept_name'], 60)}, {q(lsdwh, 10)}, {q(dwyxbs, 1)}, {q(zhgxsj, 20)}, {q(scbz, 1)});
  108. """
  109. )
  110. n += 1
  111. if n % batch_size == 0:
  112. commit_batch(out, n, "DEPT")
  113. if n % batch_size != 0:
  114. commit_batch(out, n, "DEPT")
  115. out.write(f"PROMPT DEPT done: {n}\n\n")
  116. return n
  117. def gen_teacher(cur, out, incr_sql: str, batch_size: int) -> Tuple[int, int]:
  118. sql = f"""
  119. SELECT
  120. u.user_name,
  121. u.nick_name,
  122. u.dept_id,
  123. u.sex,
  124. u.status,
  125. u.del_flag,
  126. u.phonenumber,
  127. u.email,
  128. IFNULL(u.update_time, u.create_time) AS ts
  129. FROM sys_user u
  130. WHERE u.account_type = '1'
  131. {incr_sql}
  132. """
  133. cur.execute(sql)
  134. n_base = 0
  135. n_tx = 0
  136. for row in cur:
  137. gh = (row["user_name"] or "").strip()
  138. if not gh:
  139. continue
  140. gh = gh[:20]
  141. dwh = right10(row["dept_id"]) or "0"
  142. dwh = dwh[:10]
  143. xm = (row["nick_name"] or row["user_name"] or "")[:36]
  144. xbm = sex_code(row["sex"])
  145. dqztm = "11" if row["status"] == "0" else "99"
  146. scbz = "y" if row["del_flag"] == "1" else "n"
  147. zhgxsj = fmt_dt(row["ts"])
  148. yddh = row["phonenumber"] or None
  149. dzxx = row["email"] or None
  150. out.write(
  151. f"""MERGE INTO GXJG_JZGJCSJZLB t
  152. USING (SELECT {q(gh, 20)} AS gh FROM dual) s
  153. ON (t.gh = s.gh)
  154. WHEN MATCHED THEN UPDATE SET
  155. t.dwh = {q(dwh, 10)},
  156. t.xm = {q(xm, 36)},
  157. t.xbm = {q(xbm, 1)},
  158. t.dqztm = {q(dqztm, 2)},
  159. t.zhgxsj = {q(zhgxsj, 20)},
  160. t.scbz = {q(scbz, 1)}
  161. WHEN NOT MATCHED THEN INSERT (gh, dwh, xm, xbm, dqztm, zhgxsj, scbz)
  162. VALUES ({q(gh, 20)}, {q(dwh, 10)}, {q(xm, 36)}, {q(xbm, 1)}, {q(dqztm, 2)}, {q(zhgxsj, 20)}, {q(scbz, 1)});
  163. """
  164. )
  165. n_base += 1
  166. tid = gh[:48]
  167. out.write(
  168. f"""MERGE INTO GXJG_GRTXFSZL t
  169. USING (SELECT {q(tid, 48)} AS id FROM dual) s
  170. ON (t.id = s.id)
  171. WHEN MATCHED THEN UPDATE SET
  172. t.gh = {q(gh, 20)},
  173. t.yddh = {q(yddh, 30)},
  174. t.dzxx = {q(dzxx, 40)},
  175. t.zhgxsj = {q(zhgxsj, 20)},
  176. t.scbz = {q(scbz, 1)}
  177. WHEN NOT MATCHED THEN INSERT (id, gh, yddh, dzxx, zhgxsj, scbz)
  178. VALUES ({q(tid, 48)}, {q(gh, 20)}, {q(yddh, 30)}, {q(dzxx, 40)}, {q(zhgxsj, 20)}, {q(scbz, 1)});
  179. """
  180. )
  181. n_tx += 1
  182. if n_base % batch_size == 0:
  183. commit_batch(out, n_base, "TEACHER")
  184. if n_base % batch_size != 0:
  185. commit_batch(out, n_base, "TEACHER")
  186. out.write(f"PROMPT TEACHER done: base={n_base} tx={n_tx}\n\n")
  187. return n_base, n_tx
  188. def gen_student(cur, out, incr_sql: str, batch_size: int) -> Tuple[int, int]:
  189. sql = f"""
  190. SELECT
  191. s.student_no,
  192. s.name,
  193. s.name_english,
  194. s.former_name,
  195. s.gender,
  196. s.birth_date,
  197. s.native_place,
  198. s.id_card,
  199. s.political_status,
  200. s.blood_type,
  201. s.is_single_child,
  202. s.college_id,
  203. s.class_id,
  204. s.student_status,
  205. s.deleted,
  206. s.postal_code,
  207. s.current_address,
  208. s.home_address,
  209. s.phone,
  210. s.email,
  211. s.qq,
  212. s.wechat,
  213. c.class_code,
  214. IFNULL(s.update_time, s.create_time) AS ts
  215. FROM xg_student_msg s
  216. LEFT JOIN xg_class_msg c ON c.id = s.class_id AND c.deleted = 0
  217. WHERE 1=1
  218. {incr_sql}
  219. """
  220. cur.execute(sql)
  221. n_base = 0
  222. n_tx = 0
  223. for row in cur:
  224. xh = (row["student_no"] or "").strip()
  225. if not xh:
  226. continue
  227. xh = xh[:20]
  228. dwh = str(row["college_id"] or "0")[:20]
  229. if not dwh:
  230. dwh = "0"
  231. szbh = (row["class_code"] or row["class_id"] or "")
  232. szbh = str(szbh)[:20] if szbh else None
  233. status = "" if row["student_status"] is None else str(row["student_status"])
  234. sfzx = "1" if status in ("1", "2", "3") else "0"
  235. sfyby = "1" if status == "5" else "0"
  236. scbz = "y" if row["deleted"] == 1 else "n"
  237. zhgxsj = fmt_dt(row["ts"])
  238. csrq = fmt_dt(row["birth_date"], "%Y%m%d")
  239. sfdszn = "1" if row["is_single_child"] == 1 else "0"
  240. zzmmm = (row["political_status"] or None)
  241. if zzmmm is not None:
  242. zzmmm = str(zzmmm)[:2]
  243. txdz = row["current_address"] or row["home_address"]
  244. jstxh = row["qq"] or row["wechat"]
  245. out.write(
  246. f"""MERGE INTO GXXS_XSJBSJZL t
  247. USING (SELECT {q(xh, 20)} AS xh FROM dual) s
  248. ON (t.xh = s.xh)
  249. WHEN MATCHED THEN UPDATE SET
  250. t.xm = {q(row['name'], 60)},
  251. t.ywxm = {q(row['name_english'], 60)},
  252. t.cym = {q(row['former_name'], 60)},
  253. t.xbm = {q(sex_code(row['gender']), 1)},
  254. t.csrq = {q(csrq, 10)},
  255. t.jg = {q(row['native_place'], 60)},
  256. t.sfzjlxm = '1',
  257. t.sfzjh = {q(row['id_card'], 20)},
  258. t.zzmmm = {q(zzmmm, 2)},
  259. t.xxm = {q(row['blood_type'], 30)},
  260. t.sfdszn = {q(sfdszn, 1)},
  261. t.zhgxsj = {q(zhgxsj, 20)},
  262. t.scbz = {q(scbz, 1)},
  263. t.dwh = {q(dwh, 20)},
  264. t.szbh = {q(szbh, 20)},
  265. t.sfzx = {q(sfzx, 10)},
  266. t.sfyby = {q(sfyby, 6)}
  267. WHEN NOT MATCHED THEN INSERT (
  268. xh, xm, ywxm, cym, xbm, csrq, jg, sfzjlxm, sfzjh, zzmmm, xxm,
  269. sfdszn, zhgxsj, scbz, dwh, szbh, sfzx, sfyby
  270. ) VALUES (
  271. {q(xh, 20)}, {q(row['name'], 60)}, {q(row['name_english'], 60)}, {q(row['former_name'], 60)},
  272. {q(sex_code(row['gender']), 1)}, {q(csrq, 10)}, {q(row['native_place'], 60)}, '1',
  273. {q(row['id_card'], 20)}, {q(zzmmm, 2)}, {q(row['blood_type'], 30)}, {q(sfdszn, 1)},
  274. {q(zhgxsj, 20)}, {q(scbz, 1)}, {q(dwh, 20)}, {q(szbh, 20)}, {q(sfzx, 10)}, {q(sfyby, 6)}
  275. );
  276. """
  277. )
  278. n_base += 1
  279. out.write(
  280. f"""MERGE INTO GXXS_GRTXZL t
  281. USING (SELECT {q(xh, 20)} AS xh FROM dual) s
  282. ON (t.xh = s.xh)
  283. WHEN MATCHED THEN UPDATE SET
  284. t.yzbm = {q(row['postal_code'], 6)},
  285. t.txdz = {q(txdz, 180)},
  286. t.yddh = {q(row['phone'], 30)},
  287. t.dzxx = {q(row['email'], 40)},
  288. t.jstxh = {q(jstxh, 40)},
  289. t.zhgxsj = {q(zhgxsj, 20)},
  290. t.scbz = {q(scbz, 1)}
  291. WHEN NOT MATCHED THEN INSERT (xh, yzbm, txdz, yddh, dzxx, jstxh, zhgxsj, scbz)
  292. VALUES (
  293. {q(xh, 20)}, {q(row['postal_code'], 6)}, {q(txdz, 180)}, {q(row['phone'], 30)},
  294. {q(row['email'], 40)}, {q(jstxh, 40)}, {q(zhgxsj, 20)}, {q(scbz, 1)}
  295. );
  296. """
  297. )
  298. n_tx += 1
  299. if n_base % batch_size == 0:
  300. commit_batch(out, n_base, "STUDENT")
  301. if n_base % batch_size != 0:
  302. commit_batch(out, n_base, "STUDENT")
  303. out.write(f"PROMPT STUDENT done: base={n_base} tx={n_tx}\n\n")
  304. return n_base, n_tx
  305. def build_incr(alias: str, last_sync: str, full: bool) -> str:
  306. if full or not last_sync or last_sync.startswith("1970"):
  307. return ""
  308. return (
  309. f"AND ({alias}.update_time > '{last_sync}' "
  310. f"OR {alias}.create_time > '{last_sync}')"
  311. )
  312. def main() -> int:
  313. parser = argparse.ArgumentParser(description="生成 Oracle MERGE SQL")
  314. parser.add_argument("--host", required=True)
  315. parser.add_argument("--port", default="3306")
  316. parser.add_argument("--user", required=True)
  317. parser.add_argument("--password", required=True)
  318. parser.add_argument("--database", required=True)
  319. parser.add_argument("--output", required=True, help="输出 SQL 文件路径")
  320. parser.add_argument("--last-sync", default="1970-01-01 00:00:00")
  321. parser.add_argument("--full", action="store_true")
  322. parser.add_argument("--batch-size", type=int, default=200)
  323. args = parser.parse_args()
  324. out_path = Path(args.output)
  325. out_path.parent.mkdir(parents=True, exist_ok=True)
  326. conn = connect_mysql(
  327. args.host, int(args.port), args.user, args.password, args.database
  328. )
  329. try:
  330. with conn.cursor() as cur, out_path.open("w", encoding="utf-8") as out:
  331. write_header(out)
  332. dept_n = gen_dept(
  333. cur, out, build_incr("d", args.last_sync, args.full), args.batch_size
  334. )
  335. tea_n, tea_tx = gen_teacher(
  336. cur, out, build_incr("u", args.last_sync, args.full), args.batch_size
  337. )
  338. stu_n, stu_tx = gen_student(
  339. cur, out, build_incr("s", args.last_sync, args.full), args.batch_size
  340. )
  341. out.write("COMMIT;\n")
  342. out.write(
  343. f"PROMPT SYNC SUMMARY dept={dept_n} teacher={tea_n}/{tea_tx} "
  344. f"student={stu_n}/{stu_tx}\n"
  345. )
  346. out.write("EXIT;\n")
  347. print(
  348. f"OK dept={dept_n} teacher={tea_n} teacher_tx={tea_tx} "
  349. f"student={stu_n} student_tx={stu_tx} file={out_path}"
  350. )
  351. return 0
  352. finally:
  353. conn.close()
  354. if __name__ == "__main__":
  355. sys.exit(main())