================================================================================
          数据库迁移指南（scripts/migrate_db.py）
        MySQL → PostgreSQL / SQL Server / SQLite / Oracle
================================================================================

一、脚本说明
================================================================================

migrate_db.py 是项目内置的跨库迁移脚本，把「源库」整体搬到「目标库」：

  - 目标库结构用模型 metadata 生成（Base.metadata.create_all，方言可移植，模型即 head，
    含 0002 的 city.parent_code nullable 等已落地模型定义）
  - 数据按拓扑序逐表 SELECT ... ORDER BY id，显式带 id 批量插入，保住无外键裸列引用
    （user.dept_id、role_menu.role_id/menu_id、menu.parent_id 等）
  - 迁移后按方言重置自增：
        PostgreSQL    SERIAL → setval(seq, MAX(id))（空表 START 1）
        SQL Server    IDENTITY → IDENTITY_INSERT 显式插 id + DBCC CHECKIDENT RESEED
        Oracle        补 sequence（START WITH MAX(id)+1）+ BEFORE INSERT 触发器
                      （create_all 只生成 INTEGER NOT NULL，无原生自增）
        MySQL/SQLite  显式插 id 后 auto_increment/rowid 自动跟随，无需处理
  - 全部耗时步骤都带「缓冲提示」（[·] ...请勿中断 → [✓] ...完成（耗时）），不会显得卡死

特点：源/目标连接全部由 CLI 显式参数给出，不依赖 .env，也无需先切 DB_DRIVER。


二、前置准备
================================================================================

1. 源库（MySQL）可正常访问。
2. 目标库所需驱动已包含在 requirements.txt，执行一次即可：
       pip install -r requirements.txt
   各目标库对应驱动：
       PostgreSQL   psycopg[binary]（psycopg3）
       SQL Server   pymssql
       SQLite       内置（无需额外驱动）
       Oracle       oracledb（thin 模式，无需安装 Oracle Client）
3. 可用 --dry-run 先干跑核对计划（只连源、逐表数行数、打印计划，不碰目标库）。


三、驱动与默认端口（含别名）
================================================================================

  目标库        --dst-driver      默认端口    --dst-db 填什么
  ---------------------------------------------------------------------
  MySQL         mysql             3306        数据库名
  PostgreSQL    postgresql        5432        数据库名（含点号会自动双引号）
  SQL Server    mssql             1433        数据库名
  SQLite        sqlite            无端口      SQLite 文件路径（相对/绝对皆可）
  Oracle        oracle            1521        SERVICE_NAME（如 ORCLPDB1、XE，非库名）

  驱动别名：postgres、pg → postgresql；sqlserver、sql_server → mssql；sqlite3 → sqlite


四、迁移命令与输出案例
================================================================================

以下源库均假设：
    MySQL 127.0.0.1:3306，库名 djangoadmin.fastapi.elevue，root / root
本项目共 23 张表（前缀 fastapi_），全量迁移约 4843 行（其中 fastapi_city 4150 行）。


4.1 MySQL → PostgreSQL
--------------------------------------------------------------------------------

命令：
    python scripts/migrate_db.py \
        --src-driver=mysql --src-host=127.0.0.1 --src-port=3306 \
        --src-db=djangoadmin.fastapi.elevue --src-user=root --src-pass=root \
        --dst-driver=postgresql --dst-host=localhost --dst-port=5432 \
        --dst-db=djangoadmin.fastapi.elevue --dst-user=postgres --dst-pass=你的密码 \
        --drop-target-first

输出案例：
    ==============================================================
    [迁移开始] 源: mysql:djangoadmin.fastapi.elevue → 目标: postgresql:djangoadmin.fastapi.elevue
    [迁移开始] 待迁移 23 张表
    ==============================================================
    [·] [1/5] 建目标库...（可能耗时较长，请勿中断）
      · 正在连接 postgres 维护库...
      · 正在删除旧目标库 "djangoadmin.fastapi.elevue"（WITH FORCE）...
      · 正在创建目标库 "djangoadmin.fastapi.elevue"（UTF8）...
    [✓] [1/5] 建目标库 完成（耗时 947ms）
    [·] [2/5] 建表结构（23 张表 create_all）...（可能耗时较长，请勿中断）
    [✓] [2/5] 建表结构（23 张表 create_all） 完成（耗时 1.1s），23 张表
    [·] [3/5] 逐表迁移数据...（可能耗时较长，请勿中断）
      · [1/23] 正在迁移 fastapi_dept...
      [✓ fastapi_dept] 共 23 行（耗时 12ms）
      · [2/23] 正在迁移 fastapi_menu...
      [✓ fastapi_menu] 共 54 行（耗时 110ms）
      · [3/23] 正在迁移 fastapi_city...
      [fastapi_city] +1000（累计 1000）
      [fastapi_city] +1000（累计 2000）
      [fastapi_city] +1000（累计 3000）
      [fastapi_city] +1000（累计 4000）
      [fastapi_city] +150（累计 4150）
      [✓ fastapi_city] 共 4150 行（耗时 850ms）
      · [4/23] 正在迁移 fastapi_user...
      ...
      · [23/23] 正在迁移 fastapi_login_log...
      [✓ fastapi_login_log] 共 5 行（耗时 13ms）
    [✓] [3/5] 逐表迁移数据 完成（耗时 3.5s），共 4843 行
    [·] [4/5] 重置自增序列...（可能耗时较长，请勿中断）
    [✓] [4/5] 重置自增序列 完成（耗时 412ms）      ← SERIAL setval 到 MAX(id)
    [·] [5/5] 逐表行数校验...（可能耗时较长，请勿中断）
      [fastapi_dept] source=23 target=23 PASS
      ...
      [fastapi_city] source=4150 target=4150 PASS
      ...
    [✓] [5/5] 逐表行数校验 完成（耗时 103ms），23 张表全部 PASS
    ==============================================================
    [迁移完成] 成功：4843 行 / 23 张表 / 总耗时 6.0s
    ==============================================================


4.2 MySQL → SQL Server（mssql）
--------------------------------------------------------------------------------

命令：
    python scripts/migrate_db.py \
        --src-driver=mysql --src-host=127.0.0.1 --src-port=3306 \
        --src-db=djangoadmin.fastapi.elevue --src-user=root --src-pass=root \
        --dst-driver=mssql --dst-host=localhost --dst-port=1433 \
        --dst-db=djangoadmin.fastapi.elevue --dst-user=sa --dst-pass=你的密码 \
        --drop-target-first

说明：mssql 迁移时自动 SET IDENTITY_INSERT ON 保留原 id，迁移后 DBCC CHECKIDENT RESEED。

输出案例（真实运行）：
    ==============================================================
    [迁移开始] 源: mysql:djangoadmin.fastapi.elevue → 目标: mssql:djangoadmin.fastapi.elevue
    [迁移开始] 待迁移 23 张表
    ==============================================================
    [·] [1/5] 建目标库...（可能耗时较长，请勿中断）
      · 正在连接 SQL Server（master 维护库）...
      · 正在删除旧目标库 [djangoadmin.fastapi.elevue]（SINGLE_USER + DROP）...
      · 正在创建目标库 [djangoadmin.fastapi.elevue]...
    [✓] [1/5] 建目标库 完成（耗时 304ms）
    [·] [2/5] 建表结构（23 张表 create_all）...（可能耗时较长，请勿中断）
    [✓] [2/5] 建表结构（23 张表 create_all） 完成（耗时 743ms），23 张表
    [·] [3/5] 逐表迁移数据...（可能耗时较长，请勿中断）
      · [1/23] 正在迁移 fastapi_dept...
      [fastapi_dept] +23（累计 23）
      [✓ fastapi_dept] 共 23 行（耗时 25ms）
      · [2/23] 正在迁移 fastapi_menu...
      [fastapi_menu] +54（累计 54）
      [✓ fastapi_menu] 共 54 行（耗时 102ms）
      · [3/23] 正在迁移 fastapi_role...
      ...
      · [6/23] 正在迁移 fastapi_city...
      [fastapi_city] +1000（累计 1000）
      ...
      [fastapi_city] +150（累计 4150）
      [✓ fastapi_city] 共 4150 行（耗时 3.9s）
      · [7/23] 正在迁移 fastapi_category...
      ...
      · [23/23] 正在迁移 fastapi_login_log...
      [✓ fastapi_login_log] 共 5 行（耗时 13ms）
    [✓] [3/5] 逐表迁移数据 完成（耗时 5.3s），共 4843 行
    [·] [4/5] 重置自增序列...（可能耗时较长，请勿中断）
    [✓] [4/5] 重置自增序列 完成（耗时 90ms）
    [·] [5/5] 逐表行数校验...（可能耗时较长，请勿中断）
      [fastapi_dept] source=23 target=23 PASS
      ...
      [fastapi_city] source=4150 target=4150 PASS
      ...
    [✓] [5/5] 逐表行数校验 完成（耗时 100ms），23 张表全部 PASS
    ==============================================================
    [迁移完成] 成功：4843 行 / 23 张表 / 总耗时 6.6s
    ==============================================================


4.3 MySQL → SQLite
--------------------------------------------------------------------------------

命令：
    python scripts/migrate_db.py \
        --src-driver=mysql --src-host=127.0.0.1 --src-port=3306 \
        --src-db=djangoadmin.fastapi.elevue --src-user=root --src-pass=root \
        --dst-driver=sqlite --dst-db=D:/data/djangoadmin.fastapi.elevue.db \
        --drop-target-first

说明：--dst-db 即数据库文件完整路径（相对/绝对皆可）；SQLite 无建库概念，
       --drop-target-first 时删除旧文件后重建；自增 id 由 rowid 自动跟随，无需重置。

输出案例：
    ==============================================================
    [迁移开始] 源: mysql:djangoadmin.fastapi.elevue → 目标: sqlite:D:/data/djangoadmin.fastapi.elevue.db
    [迁移开始] 待迁移 23 张表
    ==============================================================
    [·] [1/5] 建目标库...（可能耗时较长，请勿中断）
      · 检查/准备 sqlite 文件 D:\data\djangoadmin.fastapi.elevue.db...
      · 删除旧 sqlite 文件 D:\data\djangoadmin.fastapi.elevue.db...
    [✓] [1/5] 建目标库 完成（耗时 8ms）
    [·] [2/5] 建表结构（23 张表 create_all）...（可能耗时较长，请勿中断）
    [✓] [2/5] 建表结构（23 张表 create_all） 完成（耗时 96ms），23 张表
    [·] [3/5] 逐表迁移数据...（可能耗时较长，请勿中断）
      · [1/23] 正在迁移 fastapi_dept...
      [✓ fastapi_dept] 共 23 行（耗时 4ms）
      ...
      · [6/23] 正在迁移 fastapi_city...
      [✓ fastapi_city] 共 4150 行（耗时 850ms）
      ...
    [✓] [3/5] 逐表迁移数据 完成（耗时 1.2s），共 4843 行
    [·] [4/5] 重置自增序列...（可能耗时较长，请勿中断）
      · MySQL/SQLite 自动跟随，无需处理
    [✓] [4/5] 重置自增序列 完成（耗时 0ms）（无需处理）
    [·] [5/5] 逐表行数校验...（可能耗时较长，请勿中断）
    [✓] [5/5] 逐表行数校验 完成（耗时 100ms），23 张表全部 PASS
    ==============================================================
    [迁移完成] 成功：4843 行 / 23 张表 / 总耗时 1.4s
    ==============================================================


4.4 MySQL → Oracle
--------------------------------------------------------------------------------

命令：
    python scripts/migrate_db.py \
        --src-driver=mysql --src-host=127.0.0.1 --src-port=3306 \
        --src-db=djangoadmin.fastapi.elevue --src-user=root --src-pass=root \
        --dst-driver=oracle --dst-host=localhost --dst-port=1521 \
        --dst-db=ORCLPDB1 --dst-user=fastapi --dst-pass=你的密码 \
        --drop-target-first

说明：
    - Oracle 无"数据库名"，--dst-db 填 SERVICE_NAME（如 ORCLPDB1、XE）；
      schema 即 --dst-user 连接的用户（无需也无法"建库"，连上即视为就绪），
      该用户需有 CONNECT, RESOURCE 权限
    - Oracle create_all 对 Integer 主键只生成 INTEGER NOT NULL（无 identity），
      迁移后脚本会自动补 sequence（START WITH MAX(id)+1）+ BEFORE INSERT 触发器，
      保证后续 save() 能取到新 id
    - --drop-target-first 时尽力 DROP TABLE ... PURGE 清理旧项目表

输出案例：
    ==============================================================
    [迁移开始] 源: mysql:djangoadmin.fastapi.elevue → 目标: oracle:ORCLPDB1
    [迁移开始] 待迁移 23 张表
    ==============================================================
    [·] [1/5] 建目标库...（可能耗时较长，请勿中断）
      · 正在连接 Oracle 服务 ORCLPDB1（schema=fastapi）...
      · drop-first：清理 23 张旧项目表（DROP TABLE ... PURGE）...
    [✓] [1/5] 建目标库 完成（耗时 4.6s）
    [·] [2/5] 建表结构（23 张表 create_all）...（可能耗时较长，请勿中断）
    [✓] [2/5] 建表结构（23 张表 create_all） 完成（耗时 2.4s），23 张表
    [·] [3/5] 逐表迁移数据...（可能耗时较长，请勿中断）
      · [1/23] 正在迁移 fastapi_dept...
      [✓ fastapi_dept] 共 23 行（耗时 15ms）
      ...
      · [6/23] 正在迁移 fastapi_city...
      [✓ fastapi_city] 共 4150 行（耗时 2m12s）
      ...
    [✓] [3/5] 逐表迁移数据 完成（耗时 2m41s），共 4843 行
    [·] [4/5] 重置自增序列...（可能耗时较长，请勿中断）
    [✓] [4/5] 重置自增序列 完成（耗时 6.2s）      ← 补 sequence + BEFORE INSERT 触发器
    [·] [5/5] 逐表行数校验...（可能耗时较长，请勿中断）
    [✓] [5/5] 逐表行数校验 完成（耗时 2.6s），23 张表全部 PASS
    ==============================================================
    [迁移完成] 成功：4843 行 / 23 张表 / 总耗时 2m54s
    ==============================================================


五、常用参数详解
================================================================================

    --drop-target-first     先删目标库再重建（Oracle 清旧表、SQLite 删旧文件）。默认关；
                            重跑迁移时建议开启，保证幂等。
    --dry-run               只连源库、逐表数行数、打印计划，不触碰目标库。
    --chunk-size N          每批插入行数，默认 1000；大表可调大（如 5000）提升吞吐。
    --only-tables a,b       只迁移指定表（全名或去前缀短名，如 --only-tables user,role），续传用。
    --skip-existing         目标表行数已等于源表则跳过（配合续传/增量）。
    --skip-verify           跳过迁移后的逐表行数校验。
    --single-transaction    全部表包进一个大事务（默认每表一个事务；大表建议保持默认，
                            避免单事务过大）。
    口令脱敏                --src-pass/--dst-pass 留空时回落环境变量 SRC_DB_PASS/DST_DB_PASS，
                            避免口令进 shell 历史。


六、常见问题与注意事项
================================================================================

1. 目标库已存在且未加 --drop-target-first：脚本只做 create_all（IF NOT EXISTS 语义）+ 补数据，
   已存在表的数据会与迁移数据叠加，可能主键冲突。重跑请加 --drop-target-first。
2. 迁移后登录报 "no such table" / 表不存在：确认 --dst-db 指向正确（Oracle 填 SERVICE_NAME，
   不是库名；SQLite 填文件路径）。
3. 中文乱码：源 MySQL 连接串已带 charset=utf8mb4；PG 建库强制 UTF8/TEMPLATE template0；
   mssql 连接串 charset=utf8。若目标已建成非 UTF8 编码库，删除重建（--drop-target-first）。
4. Oracle 迁移后新增记录 id 异常：migrate_db.py 已自动补 sequence+触发器，
   若手动建过同名 sequence 会先删后建（user_sequences 里按名查）。
5. 自增 id 未延续原值：PG 用 setval、MSSQL 用 DBCC CHECKIDENT、Oracle 用 sequence 均已自动处理；
   如仍异常可手动：
       PostgreSQL：SELECT setval(pg_get_serial_sequence('fastapi_user','id'), (SELECT max(id) FROM fastapi_user));
       SQL Server：DBCC CHECKIDENT ('fastapi_user', RESEED, <max_id>)
       Oracle：    DROP SEQUENCE fastapi_user_id_seq; CREATE SEQUENCE fastapi_user_id_seq START WITH <max_id+1>;
6. 迁移过程被中断：已迁移的表按每表一个事务提交，未迁完的表重跑即可；配合 --only-tables / --skip-existing
   断点续传。

================================================================================
文档版本：1.0  |  更新日期：2026-08-14  |  适用范围：scripts/migrate_db.py
================================================================================


其他：整体流程
┌─────────────────────────────────────────────────────┐
│  1. 解析 CLI 参数                                   │
│     --src-* / --dst-* / --drop-target-first        │
└─────────────────────────────────────────────────────┘
                        ↓
┌─────────────────────────────────────────────────────┐
│  2. create_database_if_missing()                   │
│     ├─ MySQL:   CREATE DATABASE IF NOT EXISTS      │
│     ├─ PG:      CREATE DATABASE (连接 postgres)    │
│     ├─ MSSQL:   CREATE DATABASE (连接 master)      │
│     ├─ Oracle:  不建库，只清理表                   │
│     └─ SQLite:  mkdir + 可选删文件                 │
└─────────────────────────────────────────────────────┘
                        ↓
┌─────────────────────────────────────────────────────┐
│  3. Base.metadata.create_all(dst_engine)           │
│     ├─ 方言适配：自动生成对应数据库的 DDL          │
│     ├─ 按 ordered_tables() 顺序建表                │
│     └─ 包含所有模型定义（字段/索引/约束）          │
└─────────────────────────────────────────────────────┘
                        ↓
┌─────────────────────────────────────────────────────┐
│  4. 后续：逐表泵数据 → 重置序列 → 校验             │
└─────────────────────────────────────────────────────┘