| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394 |
- #!/usr/bin/env python3
- # -*- coding: utf-8 -*-
- """从 MySQL 源库导出数据,生成 Oracle MERGE SQL,供 sqlplus 执行。"""
- from __future__ import annotations
- import argparse
- import sys
- from datetime import datetime
- from pathlib import Path
- from typing import Any, Iterable, List, Optional, Sequence, Tuple
- try:
- import pymysql
- except ImportError:
- print("ERROR: 需要安装 pymysql: pip install pymysql", file=sys.stderr)
- sys.exit(1)
- def connect_mysql(host: str, port: int, user: str, password: str, database: str):
- return pymysql.connect(
- host=host,
- port=port,
- user=user,
- password=password,
- database=database,
- charset="utf8mb4",
- cursorclass=pymysql.cursors.DictCursor,
- autocommit=True,
- )
- def q(val: Any, max_len: Optional[int] = None) -> str:
- """Python 值转 Oracle SQL 字面量。"""
- if val is None:
- return "NULL"
- if isinstance(val, datetime):
- s = val.strftime("%Y%m%d%H%M%S")
- else:
- s = str(val).strip()
- if s == "":
- return "NULL"
- if max_len is not None:
- s = s[:max_len]
- s = s.replace("'", "''")
- return f"'{s}'"
- def right10(val: Any) -> Optional[str]:
- if val is None:
- return None
- s = str(val).strip()
- if not s or s == "0":
- return None
- return s[-10:]
- def fmt_dt(val: Any, fmt: str = "%Y%m%d%H%M%S") -> Optional[str]:
- if val is None:
- return None
- if isinstance(val, datetime):
- return val.strftime(fmt)
- s = str(val).strip()
- return s or None
- def sex_code(val: Any) -> str:
- s = "" if val is None else str(val)
- if s == "0":
- return "1"
- if s == "1":
- return "2"
- return "0"
- def write_header(out) -> None:
- out.write("-- generated by oracle_gen_merge.py\n")
- out.write("WHENEVER SQLERROR EXIT SQL.SQLCODE\n")
- out.write("SET ECHO OFF\n")
- out.write("SET FEEDBACK OFF\n")
- out.write("SET DEFINE OFF\n")
- out.write("SET SERVEROUTPUT ON SIZE UNLIMITED\n")
- out.write("\n")
- def commit_batch(out, count: int, label: str) -> None:
- out.write("COMMIT;\n")
- out.write(f"PROMPT {label} committed batch, total so far: {count}\n")
- out.write("\n")
- def gen_dept(cur, out, incr_sql: str, batch_size: int) -> int:
- sql = f"""
- SELECT
- d.dept_id,
- d.dept_name,
- d.parent_id,
- d.status,
- d.del_flag,
- IFNULL(d.update_time, d.create_time) AS ts
- FROM sys_dept d
- WHERE 1=1
- {incr_sql}
- """
- cur.execute(sql)
- n = 0
- for row in cur:
- dwh = right10(row["dept_id"])
- if not dwh:
- continue
- lsdwh = right10(row["parent_id"])
- dwyxbs = "1" if row["status"] == "0" and row["del_flag"] == "0" else "0"
- scbz = "y" if row["del_flag"] == "1" else "n"
- zhgxsj = fmt_dt(row["ts"])
- out.write(
- f"""MERGE INTO GXXX_YXSDWJBSJZL t
- USING (SELECT {q(dwh, 10)} AS dwh FROM dual) s
- ON (t.dwh = s.dwh)
- WHEN MATCHED THEN UPDATE SET
- t.dwmc = {q(row['dept_name'], 60)},
- t.lsdwh = {q(lsdwh, 10)},
- t.dwyxbs = {q(dwyxbs, 1)},
- t.zhgxsj = {q(zhgxsj, 20)},
- t.scbz = {q(scbz, 1)}
- WHEN NOT MATCHED THEN INSERT (dwh, dwmc, lsdwh, dwyxbs, zhgxsj, scbz)
- VALUES ({q(dwh, 10)}, {q(row['dept_name'], 60)}, {q(lsdwh, 10)}, {q(dwyxbs, 1)}, {q(zhgxsj, 20)}, {q(scbz, 1)});
- """
- )
- n += 1
- if n % batch_size == 0:
- commit_batch(out, n, "DEPT")
- if n % batch_size != 0:
- commit_batch(out, n, "DEPT")
- out.write(f"PROMPT DEPT done: {n}\n\n")
- return n
- def gen_teacher(cur, out, incr_sql: str, batch_size: int) -> Tuple[int, int]:
- sql = f"""
- SELECT
- u.user_name,
- u.nick_name,
- u.dept_id,
- u.sex,
- u.status,
- u.del_flag,
- u.phonenumber,
- u.email,
- IFNULL(u.update_time, u.create_time) AS ts
- FROM sys_user u
- WHERE u.account_type = '1'
- {incr_sql}
- """
- cur.execute(sql)
- n_base = 0
- n_tx = 0
- for row in cur:
- gh = (row["user_name"] or "").strip()
- if not gh:
- continue
- gh = gh[:20]
- dwh = right10(row["dept_id"]) or "0"
- dwh = dwh[:10]
- xm = (row["nick_name"] or row["user_name"] or "")[:36]
- xbm = sex_code(row["sex"])
- dqztm = "11" if row["status"] == "0" else "99"
- scbz = "y" if row["del_flag"] == "1" else "n"
- zhgxsj = fmt_dt(row["ts"])
- yddh = row["phonenumber"] or None
- dzxx = row["email"] or None
- out.write(
- f"""MERGE INTO GXJG_JZGJCSJZLB t
- USING (SELECT {q(gh, 20)} AS gh FROM dual) s
- ON (t.gh = s.gh)
- WHEN MATCHED THEN UPDATE SET
- t.dwh = {q(dwh, 10)},
- t.xm = {q(xm, 36)},
- t.xbm = {q(xbm, 1)},
- t.dqztm = {q(dqztm, 2)},
- t.zhgxsj = {q(zhgxsj, 20)},
- t.scbz = {q(scbz, 1)}
- WHEN NOT MATCHED THEN INSERT (gh, dwh, xm, xbm, dqztm, zhgxsj, scbz)
- VALUES ({q(gh, 20)}, {q(dwh, 10)}, {q(xm, 36)}, {q(xbm, 1)}, {q(dqztm, 2)}, {q(zhgxsj, 20)}, {q(scbz, 1)});
- """
- )
- n_base += 1
- tid = gh[:48]
- out.write(
- f"""MERGE INTO GXJG_GRTXFSZL t
- USING (SELECT {q(tid, 48)} AS id FROM dual) s
- ON (t.id = s.id)
- WHEN MATCHED THEN UPDATE SET
- t.gh = {q(gh, 20)},
- t.yddh = {q(yddh, 30)},
- t.dzxx = {q(dzxx, 40)},
- t.zhgxsj = {q(zhgxsj, 20)},
- t.scbz = {q(scbz, 1)}
- WHEN NOT MATCHED THEN INSERT (id, gh, yddh, dzxx, zhgxsj, scbz)
- VALUES ({q(tid, 48)}, {q(gh, 20)}, {q(yddh, 30)}, {q(dzxx, 40)}, {q(zhgxsj, 20)}, {q(scbz, 1)});
- """
- )
- n_tx += 1
- if n_base % batch_size == 0:
- commit_batch(out, n_base, "TEACHER")
- if n_base % batch_size != 0:
- commit_batch(out, n_base, "TEACHER")
- out.write(f"PROMPT TEACHER done: base={n_base} tx={n_tx}\n\n")
- return n_base, n_tx
- def gen_student(cur, out, incr_sql: str, batch_size: int) -> Tuple[int, int]:
- sql = f"""
- SELECT
- s.student_no,
- s.name,
- s.name_english,
- s.former_name,
- s.gender,
- s.birth_date,
- s.native_place,
- s.id_card,
- s.political_status,
- s.blood_type,
- s.is_single_child,
- s.college_id,
- s.class_id,
- s.student_status,
- s.deleted,
- s.postal_code,
- s.current_address,
- s.home_address,
- s.phone,
- s.email,
- s.qq,
- s.wechat,
- c.class_code,
- IFNULL(s.update_time, s.create_time) AS ts
- FROM xg_student_msg s
- LEFT JOIN xg_class_msg c ON c.id = s.class_id AND c.deleted = 0
- WHERE 1=1
- {incr_sql}
- """
- cur.execute(sql)
- n_base = 0
- n_tx = 0
- for row in cur:
- xh = (row["student_no"] or "").strip()
- if not xh:
- continue
- xh = xh[:20]
- dwh = str(row["college_id"] or "0")[:20]
- if not dwh:
- dwh = "0"
- szbh = (row["class_code"] or row["class_id"] or "")
- szbh = str(szbh)[:20] if szbh else None
- status = "" if row["student_status"] is None else str(row["student_status"])
- sfzx = "1" if status in ("1", "2", "3") else "0"
- sfyby = "1" if status == "5" else "0"
- scbz = "y" if row["deleted"] == 1 else "n"
- zhgxsj = fmt_dt(row["ts"])
- csrq = fmt_dt(row["birth_date"], "%Y%m%d")
- sfdszn = "1" if row["is_single_child"] == 1 else "0"
- zzmmm = (row["political_status"] or None)
- if zzmmm is not None:
- zzmmm = str(zzmmm)[:2]
- txdz = row["current_address"] or row["home_address"]
- jstxh = row["qq"] or row["wechat"]
- out.write(
- f"""MERGE INTO GXXS_XSJBSJZL t
- USING (SELECT {q(xh, 20)} AS xh FROM dual) s
- ON (t.xh = s.xh)
- WHEN MATCHED THEN UPDATE SET
- t.xm = {q(row['name'], 60)},
- t.ywxm = {q(row['name_english'], 60)},
- t.cym = {q(row['former_name'], 60)},
- t.xbm = {q(sex_code(row['gender']), 1)},
- t.csrq = {q(csrq, 10)},
- t.jg = {q(row['native_place'], 60)},
- t.sfzjlxm = '1',
- t.sfzjh = {q(row['id_card'], 20)},
- t.zzmmm = {q(zzmmm, 2)},
- t.xxm = {q(row['blood_type'], 30)},
- t.sfdszn = {q(sfdszn, 1)},
- t.zhgxsj = {q(zhgxsj, 20)},
- t.scbz = {q(scbz, 1)},
- t.dwh = {q(dwh, 20)},
- t.szbh = {q(szbh, 20)},
- t.sfzx = {q(sfzx, 10)},
- t.sfyby = {q(sfyby, 6)}
- WHEN NOT MATCHED THEN INSERT (
- xh, xm, ywxm, cym, xbm, csrq, jg, sfzjlxm, sfzjh, zzmmm, xxm,
- sfdszn, zhgxsj, scbz, dwh, szbh, sfzx, sfyby
- ) VALUES (
- {q(xh, 20)}, {q(row['name'], 60)}, {q(row['name_english'], 60)}, {q(row['former_name'], 60)},
- {q(sex_code(row['gender']), 1)}, {q(csrq, 10)}, {q(row['native_place'], 60)}, '1',
- {q(row['id_card'], 20)}, {q(zzmmm, 2)}, {q(row['blood_type'], 30)}, {q(sfdszn, 1)},
- {q(zhgxsj, 20)}, {q(scbz, 1)}, {q(dwh, 20)}, {q(szbh, 20)}, {q(sfzx, 10)}, {q(sfyby, 6)}
- );
- """
- )
- n_base += 1
- out.write(
- f"""MERGE INTO GXXS_GRTXZL t
- USING (SELECT {q(xh, 20)} AS xh FROM dual) s
- ON (t.xh = s.xh)
- WHEN MATCHED THEN UPDATE SET
- t.yzbm = {q(row['postal_code'], 6)},
- t.txdz = {q(txdz, 180)},
- t.yddh = {q(row['phone'], 30)},
- t.dzxx = {q(row['email'], 40)},
- t.jstxh = {q(jstxh, 40)},
- t.zhgxsj = {q(zhgxsj, 20)},
- t.scbz = {q(scbz, 1)}
- WHEN NOT MATCHED THEN INSERT (xh, yzbm, txdz, yddh, dzxx, jstxh, zhgxsj, scbz)
- VALUES (
- {q(xh, 20)}, {q(row['postal_code'], 6)}, {q(txdz, 180)}, {q(row['phone'], 30)},
- {q(row['email'], 40)}, {q(jstxh, 40)}, {q(zhgxsj, 20)}, {q(scbz, 1)}
- );
- """
- )
- n_tx += 1
- if n_base % batch_size == 0:
- commit_batch(out, n_base, "STUDENT")
- if n_base % batch_size != 0:
- commit_batch(out, n_base, "STUDENT")
- out.write(f"PROMPT STUDENT done: base={n_base} tx={n_tx}\n\n")
- return n_base, n_tx
- def build_incr(alias: str, last_sync: str, full: bool) -> str:
- if full or not last_sync or last_sync.startswith("1970"):
- return ""
- return (
- f"AND ({alias}.update_time > '{last_sync}' "
- f"OR {alias}.create_time > '{last_sync}')"
- )
- def main() -> int:
- parser = argparse.ArgumentParser(description="生成 Oracle MERGE SQL")
- parser.add_argument("--host", required=True)
- parser.add_argument("--port", default="3306")
- parser.add_argument("--user", required=True)
- parser.add_argument("--password", required=True)
- parser.add_argument("--database", required=True)
- parser.add_argument("--output", required=True, help="输出 SQL 文件路径")
- parser.add_argument("--last-sync", default="1970-01-01 00:00:00")
- parser.add_argument("--full", action="store_true")
- parser.add_argument("--batch-size", type=int, default=200)
- args = parser.parse_args()
- out_path = Path(args.output)
- out_path.parent.mkdir(parents=True, exist_ok=True)
- conn = connect_mysql(
- args.host, int(args.port), args.user, args.password, args.database
- )
- try:
- with conn.cursor() as cur, out_path.open("w", encoding="utf-8") as out:
- write_header(out)
- dept_n = gen_dept(
- cur, out, build_incr("d", args.last_sync, args.full), args.batch_size
- )
- tea_n, tea_tx = gen_teacher(
- cur, out, build_incr("u", args.last_sync, args.full), args.batch_size
- )
- stu_n, stu_tx = gen_student(
- cur, out, build_incr("s", args.last_sync, args.full), args.batch_size
- )
- out.write("COMMIT;\n")
- out.write(
- f"PROMPT SYNC SUMMARY dept={dept_n} teacher={tea_n}/{tea_tx} "
- f"student={stu_n}/{stu_tx}\n"
- )
- out.write("EXIT;\n")
- print(
- f"OK dept={dept_n} teacher={tea_n} teacher_tx={tea_tx} "
- f"student={stu_n} student_tx={stu_tx} file={out_path}"
- )
- return 0
- finally:
- conn.close()
- if __name__ == "__main__":
- sys.exit(main())
|