Compare commits
2
Commits
7d4d3400e1
...
88af651eb4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
88af651eb4 | ||
|
|
1be3c31ec1 |
@@ -42,7 +42,7 @@
|
|||||||
|
|
||||||
- `App.xaml`:应用资源、HandyControl 主题和 `MainViewModel` 资源注册;启动页是 `Views/HomeWindow.xaml`。
|
- `App.xaml`:应用资源、HandyControl 主题和 `MainViewModel` 资源注册;启动页是 `Views/HomeWindow.xaml`。
|
||||||
- `App.xaml.cs`:Velopack 初始化、旧版 `current/config.ini` 更新迁移、后台/手动更新、更新前后 `config.ini`、`data/` 与 `pre/` 备份恢复、全局异常日志。
|
- `App.xaml.cs`:Velopack 初始化、旧版 `current/config.ini` 更新迁移、后台/手动更新、更新前后 `config.ini`、`data/` 与 `pre/` 备份恢复、全局异常日志。
|
||||||
- `Views/HomeWindow.xaml`:单窗口主界面,包含五个迁移 Tab、连接状态、SQL 编辑区和批量操作。
|
- `Views/HomeWindow.xaml`:单窗口主界面,包含六个迁移 Tab、连接状态、SQL 编辑区和批量操作。
|
||||||
- `Views/HomeWindow.xaml.cs`:只处理窗口关闭和手动检查更新等窗口事件。
|
- `Views/HomeWindow.xaml.cs`:只处理窗口关闭和手动检查更新等窗口事件。
|
||||||
- `Views/ConnectionDialog.*`:源库/目标库连接配置与测试。
|
- `Views/ConnectionDialog.*`:源库/目标库连接配置与测试。
|
||||||
- `Views/ErrorDialog.*`:错误详情展示和复制。
|
- `Views/ErrorDialog.*`:错误详情展示和复制。
|
||||||
@@ -50,7 +50,7 @@
|
|||||||
|
|
||||||
### 状态与编排
|
### 状态与编排
|
||||||
|
|
||||||
- `ViewModels/MainViewModel.cs`:主状态容器和命令编排;初始化五个 `TabServices`,管理数据库连接、单行/批量读写、SQL 编辑与断点续写。
|
- `ViewModels/MainViewModel.cs`:主状态容器和命令编排;初始化六个 `TabServices`,管理数据库连接、单行/批量读写、SQL 编辑与断点续写。
|
||||||
- `Models/TableRowModel.cs`:每个迁移对象的名称、所属 Tab、读写状态。
|
- `Models/TableRowModel.cs`:每个迁移对象的名称、所属 Tab、读写状态。
|
||||||
- `Enums/`:Tab、状态、工作类型和输出扩展名枚举。
|
- `Enums/`:Tab、状态、工作类型和输出扩展名枚举。
|
||||||
|
|
||||||
@@ -60,6 +60,7 @@
|
|||||||
- `Services/TabServices.cs`:索引列表加载/保存、单行读写、排序、删除和状态切换。
|
- `Services/TabServices.cs`:索引列表加载/保存、单行读写、排序、删除和状态切换。
|
||||||
- `Services/DbServices.cs`:Oracle SQL 的提取与执行核心:
|
- `Services/DbServices.cs`:Oracle SQL 的提取与执行核心:
|
||||||
- `GetTableSql`:读取表 DDL、注释、主外键、唯一键和索引;
|
- `GetTableSql`:读取表 DDL、注释、主外键、唯一键和索引;
|
||||||
|
- `GetTableField`:按 `TABLE.COLUMN` 读取单个物理表字段,不查询 BOS 元数据;目标字段不存在时新增,已存在时跳过;
|
||||||
- `GetTableBos`:读取 BOS 配置;
|
- `GetTableBos`:读取 BOS 配置;
|
||||||
- `GetTableBosField`:按 `TABLE.COLUMN` 读取单个 BOS 字段配置,生成目标物理字段补充 SQL,并包含关联限定值/引用元数据;
|
- `GetTableBosField`:按 `TABLE.COLUMN` 读取单个 BOS 字段配置,生成目标物理字段补充 SQL,并包含关联限定值/引用元数据;
|
||||||
- `GetPrcSql` / `GetAllPrc`:读取存储过程或函数源码;
|
- `GetPrcSql` / `GetAllPrc`:读取存储过程或函数源码;
|
||||||
@@ -71,11 +72,12 @@
|
|||||||
|
|
||||||
## 真实数据流
|
## 真实数据流
|
||||||
|
|
||||||
五个 Tab 的映射由 `MainViewModel` 构造函数定义:
|
六个 Tab 的映射由 `MainViewModel` 构造函数定义:
|
||||||
|
|
||||||
| Tab | 索引文件 | 内容文件 | 源库读取方法 |
|
| Tab | 索引文件 | 内容文件 | 源库读取方法 |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| 表配置 | `data/from/table.txt` | `data/{对象名}_creat.txt` | `DbServices.GetTableSql` |
|
| 表配置 | `data/from/table.txt` | `data/{对象名}_creat.txt` | `DbServices.GetTableSql` |
|
||||||
|
| 表字段 | `data/from/table_field.txt` | `data/{表名}.{字段名}_table_field.txt` | `DbServices.GetTableField` |
|
||||||
| BOS 配置 | `data/from/bos.txt` | `data/{对象名}_bos.txt` | `DbServices.GetTableBos` |
|
| BOS 配置 | `data/from/bos.txt` | `data/{对象名}_bos.txt` | `DbServices.GetTableBos` |
|
||||||
| BOS 字段配置 | `data/from/bos_field.txt` | `data/{表名}.{字段名}_bos_field.txt` | `DbServices.GetTableBosField` |
|
| BOS 字段配置 | `data/from/bos_field.txt` | `data/{表名}.{字段名}_bos_field.txt` | `DbServices.GetTableBosField` |
|
||||||
| 存储过程 | `data/from/prc.txt` | `data/{对象名}_prc.txt` | `DbServices.GetPrcSql` |
|
| 存储过程 | `data/from/prc.txt` | `data/{对象名}_prc.txt` | `DbServices.GetPrcSql` |
|
||||||
@@ -83,11 +85,13 @@
|
|||||||
|
|
||||||
读取流程:索引项 -> 调用源库读取委托 -> 生成 SQL 文本 -> 保存到 `data/` -> 更新读取状态并显示内容。
|
读取流程:索引项 -> 调用源库读取委托 -> 生成 SQL 文本 -> 保存到 `data/` -> 更新读取状态并显示内容。
|
||||||
|
|
||||||
|
“表字段”位于“表配置”之后,列表项必须使用 `TABLE.COLUMN`。它只读取 Oracle 物理表字段定义和字段注释,不检查 `AD_TABLE`、`AD_COLUMN` 等 BOS 元数据。源物理表或字段不存在时读取失败;目标表不存在时写入失败;目标字段已存在时整项跳过,否则按源字段类型、默认值、可空性和注释新增。
|
||||||
|
|
||||||
读取 BOS 配置前会检查源库当前用户下的辅助过程。整表读取使用 `HMIG_GET_TABLE_BOS_31`,单字段读取使用 `HMIG_GET_TABLE_BOS_FIELD_31`;若过程不存在,程序从运行目录的同名 `pre/*.prc` 脚本安装,检查 `USER_ERRORS` 和对象状态后再读取。单字段列表项必须采用 `TABLE.COLUMN` 格式。界面不提供手动“预处理”入口。
|
读取 BOS 配置前会检查源库当前用户下的辅助过程。整表读取使用 `HMIG_GET_TABLE_BOS_31`,单字段读取使用 `HMIG_GET_TABLE_BOS_FIELD_31`;若过程不存在,程序从运行目录的同名 `pre/*.prc` 脚本安装,检查 `USER_ERRORS` 和对象状态后再读取。单字段列表项必须采用 `TABLE.COLUMN` 格式。界面不提供手动“预处理”入口。
|
||||||
|
|
||||||
单字段 SQL 写入目标库时,限定值组按名称、限定值按组和值判断,只补充缺失项;引用元数据按引用表名和字段 `DBNAME` 映射,目标引用字段不存在或不唯一时直接报错,不自动创建。
|
单字段 SQL 写入目标库时,限定值组按名称、限定值按组和值判断,只补充缺失项;引用元数据按引用表名和字段 `DBNAME` 映射,目标引用字段不存在或不唯一时直接报错,不自动创建。
|
||||||
|
|
||||||
单字段读取还会查询源库 `USER_TABLES`、`USER_TAB_COLUMNS`。源对象是物理表且源字段存在时,输出文件首先检查目标库物理表和字段:目标表不存在时报错,目标字段不存在时按源字段的数据类型、长度、精度和字符语义执行 `ALTER TABLE ... ADD`。源对象是视图或 BOS 计算字段时不生成物理字段 DDL。
|
BOS 单字段读取先按 `AD_TABLE.NAME` 定位 BOS 表;`REALTABLE_ID` 非空时沿 `REALTABLE_ID -> AD_TABLE.ID` 解析最终实际表名称,支持多级映射,映射记录缺失、不唯一或循环引用时报错。BOS 配置仍按原 BOS 表名导出,物理字段按最终实际表名读取和写入。同名表或映射后的实际表必须命中 `USER_TABLES`,并排除 `USER_VIEWS`、`USER_MVIEWS`(物化视图也可能出现在 `USER_TABLES` 中)。源物理字段存在时,输出文件先检查目标同名普通物理表和字段:目标表不存在或为视图/物化视图时报错,目标字段不存在时按源字段类型、长度、精度和字符语义执行 `ALTER TABLE ... ADD`,已存在则跳过物理 DDL。源对象是视图、物化视图或源物理字段不存在时仅导出 BOS 配置。独立“表字段”页签不解析 BOS 映射。
|
||||||
|
|
||||||
写入流程:索引项 -> 读取对应 `data/*_{后缀}.txt` -> `DbServices.ExecuteCreate` -> 在目标库执行 -> 更新写入状态。批量写入遇到第一条失败即停止,并在内存中记录断点;断点不会跨进程持久化。
|
写入流程:索引项 -> 读取对应 `data/*_{后缀}.txt` -> `DbServices.ExecuteCreate` -> 在目标库执行 -> 更新写入状态。批量写入遇到第一条失败即停止,并在内存中记录断点;断点不会跨进程持久化。
|
||||||
|
|
||||||
@@ -143,11 +147,17 @@ dotnet build .\henlo_migration_wpf10\henlo_migration_wpf10.csproj -c Debug
|
|||||||
dotnet build .\henlo_migration_wpf10\henlo_migration_wpf10.csproj -c Debug --no-restore
|
dotnet build .\henlo_migration_wpf10\henlo_migration_wpf10.csproj -c Debug --no-restore
|
||||||
```
|
```
|
||||||
|
|
||||||
当前没有测试项目。最低验证要求:
|
离线 BOS 实际表/字段回归(使用模拟元数据,不启动应用、不连接数据库):
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
dotnet run --project .\tests\BosPhysicalColumnRegression\BosPhysicalColumnRegression.csproj
|
||||||
|
```
|
||||||
|
|
||||||
|
最低验证要求:
|
||||||
|
|
||||||
1. 所有 C#/XAML 修改至少执行一次主项目构建。
|
1. 所有 C#/XAML 修改至少执行一次主项目构建。
|
||||||
2. 仅文档修改可不构建,但要重新读取文件检查 Markdown、路径和命令。
|
2. 仅文档修改可不构建,但要重新读取文件检查 Markdown、路径和命令。
|
||||||
3. XAML/绑定修改除构建外,应在 Windows 上启动应用,检查五个 Tab、连接弹窗、按钮可用性和错误弹窗;不要因此连接生产库。
|
3. XAML/绑定修改除构建外,应在 Windows 上启动应用,检查六个 Tab、连接弹窗、按钮可用性和错误弹窗;不要因此连接生产库。
|
||||||
4. SQL 生成/拆分修改需用不含真实数据的代表性 SQL 验证表 DDL、注释、约束、PL/SQL 和 INSERT。
|
4. SQL 生成/拆分修改需用不含真实数据的代表性 SQL 验证表 DDL、注释、约束、PL/SQL 和 INSERT。
|
||||||
5. 更新逻辑修改需分别考虑普通 `dotnet run`(非 Velopack 安装)和 Velopack 安装态。
|
5. 更新逻辑修改需分别考虑普通 `dotnet run`(非 Velopack 安装)和 Velopack 安装态。
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,19 @@
|
|||||||
# 更新记录
|
# 更新记录
|
||||||
|
|
||||||
|
## 1.0.20 - 2026-09-03
|
||||||
|
|
||||||
|
### 修复
|
||||||
|
|
||||||
|
- BOS 字段迁移按 `REALTABLE_ID` 解析实际表,目标实际字段已存在则跳过物理 DDL,不存在才新增;BOS 配置仍迁移到原 BOS 表。
|
||||||
|
- 普通视图、物化视图不再生成物理字段 DDL;增加映射缺失、重复和循环引用检查及离线回归用例。
|
||||||
|
|
||||||
|
## 1.0.19 - 2026-08-26
|
||||||
|
|
||||||
|
### 新增
|
||||||
|
|
||||||
|
- 新增“表字段”迁移页签,支持按 `TABLE.COLUMN` 从源库读取物理字段定义并生成独立 SQL 文件。
|
||||||
|
- 写入目标库时,目标字段已存在则跳过;目标字段不存在时按源库字段类型、默认值、非空约束及注释新增,不依赖 BOS 字段配置。
|
||||||
|
|
||||||
## 1.0.18 - 2026-08-26
|
## 1.0.18 - 2026-08-26
|
||||||
|
|
||||||
### 修复
|
### 修复
|
||||||
|
|||||||
+1
-1
@@ -74,7 +74,7 @@ cd .\henlo_migration_wpf10
|
|||||||
|
|
||||||
- `.csproj` 只复制 `pre/*.txt|*.fnc|*.prc` 等程序辅助脚本,不复制本机 `config.ini` 或 `data/from/*.txt`;`pack.bat` 还会在打包前再次删除 `publish/config.ini`,并创建空的 `data/from/` 目录。
|
- `.csproj` 只复制 `pre/*.txt|*.fnc|*.prc` 等程序辅助脚本,不复制本机 `config.ini` 或 `data/from/*.txt`;`pack.bat` 还会在打包前再次删除 `publish/config.ini`,并创建空的 `data/from/` 目录。
|
||||||
- `pre/hmig_get_table_bos_31.prc`、`pre/hmig_get_table_bos_field_31.prc` 等运行时辅助过程必须出现在发布输出中。
|
- `pre/hmig_get_table_bos_31.prc`、`pre/hmig_get_table_bos_field_31.prc` 等运行时辅助过程必须出现在发布输出中。
|
||||||
- 新安装的索引列表为空;程序添加第一条对应配置时会在 `data/from/` 创建 `table.txt`、`bos.txt`、`bos_field.txt`、`prc.txt` 或 `data.txt`。
|
- 新安装的索引列表为空;程序添加第一条对应配置时会在 `data/from/` 创建 `table.txt`、`table_field.txt`、`bos.txt`、`bos_field.txt`、`prc.txt` 或 `data.txt`。
|
||||||
- 新安装首次运行时会在 Velopack 根目录(与 `Update.exe` 同级)创建值为空的 `config.ini`;普通未打包运行时仍在程序目录创建。不得把真实或样例账号密码放入发布产物。
|
- 新安装首次运行时会在 Velopack 根目录(与 `Update.exe` 同级)创建值为空的 `config.ini`;普通未打包运行时仍在程序目录创建。不得把真实或样例账号密码放入发布产物。
|
||||||
|
|
||||||
## Velopack 产物
|
## Velopack 产物
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ namespace henlo_migration.Common
|
|||||||
/// <summary>Oracle 连接配置文件路径</summary>
|
/// <summary>Oracle 连接配置文件路径</summary>
|
||||||
public static string ConfigPath => Path.Combine(PersistentRootPath, "config.ini");
|
public static string ConfigPath => Path.Combine(PersistentRootPath, "config.ini");
|
||||||
|
|
||||||
/// <summary>源数据文件夹(存放 table.txt、bos.txt、prc.txt、data.txt 及各表的 .txt 文件)</summary>
|
/// <summary>源数据文件夹(存放 table.txt、table_field.txt、bos.txt、prc.txt、data.txt 及各对象的 .txt 文件)</summary>
|
||||||
public static string DataPath
|
public static string DataPath
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
@@ -49,7 +49,7 @@ namespace henlo_migration.Common
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>索引文件目录(存放 table.txt、bos.txt、prc.txt、data.txt)</summary>
|
/// <summary>索引文件目录(存放 table.txt、table_field.txt、bos.txt、bos_field.txt、prc.txt、data.txt)</summary>
|
||||||
public static string DataFromPath
|
public static string DataFromPath
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ namespace henlo_migration.Enums
|
|||||||
bos_field,
|
bos_field,
|
||||||
creat,
|
creat,
|
||||||
data,
|
data,
|
||||||
prc
|
prc,
|
||||||
|
table_field
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,10 @@ namespace henlo_migration.Enums
|
|||||||
public enum TabsEnum
|
public enum TabsEnum
|
||||||
{
|
{
|
||||||
表 = 0,
|
表 = 0,
|
||||||
BOS表配置 = 1,
|
表字段 = 1,
|
||||||
BOS字段配置 = 2,
|
BOS表配置 = 2,
|
||||||
存储过程 = 3,
|
BOS字段配置 = 3,
|
||||||
表数据 = 4
|
存储过程 = 4,
|
||||||
|
表数据 = 5
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
| 功能 | 说明 |
|
| 功能 | 说明 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| **表结构迁移** | 读取源库表结构(DDL)、表注释、字段注释、主键/外键/索引,写入目标库 |
|
| **表结构迁移** | 读取源库表结构(DDL)、表注释、字段注释、主键/外键/索引,写入目标库 |
|
||||||
|
| **表字段迁移** | 按 `TABLE.COLUMN` 读取单个物理字段;目标字段不存在时新增,已存在时跳过 |
|
||||||
| **存储过程迁移** | 读取/写入存储过程和函数源码 |
|
| **存储过程迁移** | 读取/写入存储过程和函数源码 |
|
||||||
| **BOS 配置迁移** | 读取 BOS 表配置写入目标库 |
|
| **BOS 配置迁移** | 读取 BOS 表配置写入目标库 |
|
||||||
| **BOS 字段配置迁移** | 按 `TABLE.COLUMN` 补充目标物理字段并迁移字段配置、限定值和引用元数据 |
|
| **BOS 字段配置迁移** | 按 `TABLE.COLUMN` 补充目标物理字段并迁移字段配置、限定值和引用元数据 |
|
||||||
@@ -101,17 +102,22 @@ henlo_migration_wpf10/
|
|||||||
| 文件 | 用途 |
|
| 文件 | 用途 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| `table.txt` | 表结构列表 |
|
| `table.txt` | 表结构列表 |
|
||||||
|
| `table_field.txt` | 物理表字段列表,每行使用 `TABLE.COLUMN` 格式 |
|
||||||
| `bos.txt` | BOS 配置列表 |
|
| `bos.txt` | BOS 配置列表 |
|
||||||
| `bos_field.txt` | BOS 单字段配置列表,每行使用 `TABLE.COLUMN` 格式 |
|
| `bos_field.txt` | BOS 单字段配置列表,每行使用 `TABLE.COLUMN` 格式 |
|
||||||
| `prc.txt` | 存储过程/函数列表 |
|
| `prc.txt` | 存储过程/函数列表 |
|
||||||
| `data.txt` | 表数据列表 |
|
| `data.txt` | 表数据列表 |
|
||||||
|
|
||||||
每行一个对象名称,读取数据库时会自动更新内容。BOS 单字段配置生成
|
每行一个对象名称,读取数据库时会自动更新内容。表字段生成
|
||||||
|
`data/{表名}.{字段名}_table_field.txt`,只读取源库物理表字段和字段注释,不检查 BOS 是否维护该字段;写入时目标表不存在会报错,目标字段已存在则跳过,否则按源字段类型、默认值、可空性和注释新增。
|
||||||
|
|
||||||
|
BOS 单字段配置生成
|
||||||
`data/{表名}.{字段名}_bos_field.txt`;源库缺少单字段读取过程时,程序会从
|
`data/{表名}.{字段名}_bos_field.txt`;源库缺少单字段读取过程时,程序会从
|
||||||
`pre/hmig_get_table_bos_field_31.prc` 自动安装后再读取。
|
`pre/hmig_get_table_bos_field_31.prc` 自动安装后再读取。
|
||||||
|
|
||||||
单字段配置写入时,目标库缺少的限定值组和限定值会自动补充;引用字段只做存在性和唯一性检查,不存在或不唯一时停止并报错。
|
单字段配置写入时,目标库缺少的限定值组和限定值会自动补充;引用字段只做存在性和唯一性检查,不存在或不唯一时停止并报错。
|
||||||
若源对象是物理表且存在同名物理字段,生成的 SQL 还会检查目标表字段,并在字段缺失时按源字段类型执行 `ALTER TABLE ... ADD`;目标物理表不存在时直接报错。视图字段和 BOS 计算字段不会创建物理字段。
|
BOS 单字段读取会先解析 `AD_TABLE.REALTABLE_ID`:未设置时使用原 BOS 表名,已设置时沿映射找到最终实际表名(支持多级映射,缺失、不唯一或循环引用时报错)。物理字段读取和目标库写入均使用实际表名,BOS 配置仍按原 BOS 表名迁移。
|
||||||
|
只有实际对象是普通物理表且源字段存在时,才生成 `ALTER TABLE ... ADD`:写入前检查目标实际表字段,已存在则跳过物理 DDL,不存在才新增;目标实际表不存在或为视图/物化视图时报错。源普通视图、物化视图和没有物理字段的 BOS 计算字段仅导出 BOS 配置,不生成物理 DDL。独立“表字段”页签不受此 BOS 映射规则影响。
|
||||||
|
|
||||||
### 3. 基本操作流程
|
### 3. 基本操作流程
|
||||||
|
|
||||||
|
|||||||
@@ -229,8 +229,8 @@ namespace henlo_migration.Services
|
|||||||
return textResult.Rows.Count > 0 ? textResult.Rows[0][colName]?.ToString() ?? "" : "";
|
return textResult.Rows.Count > 0 ? textResult.Rows[0][colName]?.ToString() ?? "" : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>解析 TABLE.COLUMN 格式的 BOS 字段名称</summary>
|
/// <summary>解析 TABLE.COLUMN 格式的表字段名称</summary>
|
||||||
public static bool TryParseBosFieldName(string qualifiedFieldName, out string tableName, out string columnName)
|
public static bool TryParseTableFieldName(string qualifiedFieldName, out string tableName, out string columnName)
|
||||||
{
|
{
|
||||||
tableName = "";
|
tableName = "";
|
||||||
columnName = "";
|
columnName = "";
|
||||||
@@ -247,10 +247,84 @@ namespace henlo_migration.Services
|
|||||||
return tableName.Length > 0 && columnName.Length > 0;
|
return tableName.Length > 0 && columnName.Length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>读取源库单个物理表字段,生成目标字段不存在时才新增的 SQL;不读取 BOS 元数据</summary>
|
||||||
|
public static string GetTableField(string qualifiedFieldName)
|
||||||
|
{
|
||||||
|
if (!TryParseTableFieldName(qualifiedFieldName, out string tableName, out string columnName))
|
||||||
|
throw new ArgumentException("表字段必须使用 TABLE.COLUMN 格式。", nameof(qualifiedFieldName));
|
||||||
|
|
||||||
|
var db = SqlSugarHelper.DbOrig;
|
||||||
|
int sourceTableCount = db.Ado.GetInt(
|
||||||
|
"SELECT COUNT(*) FROM USER_TABLES WHERE TABLE_NAME = :table_name",
|
||||||
|
new SugarParameter("table_name", tableName));
|
||||||
|
if (sourceTableCount == 0)
|
||||||
|
throw new InvalidOperationException($"源库不存在物理表:{tableName}");
|
||||||
|
|
||||||
|
var columnResult = db.Ado.GetDataTable(@"
|
||||||
|
SELECT DATA_TYPE, DATA_LENGTH, DATA_PRECISION, DATA_SCALE,
|
||||||
|
CHAR_LENGTH, CHAR_USED, NULLABLE, DATA_DEFAULT
|
||||||
|
FROM USER_TAB_COLUMNS
|
||||||
|
WHERE TABLE_NAME = :table_name
|
||||||
|
AND COLUMN_NAME = :column_name",
|
||||||
|
new SugarParameter("table_name", tableName),
|
||||||
|
new SugarParameter("column_name", columnName));
|
||||||
|
if (columnResult.Rows.Count == 0)
|
||||||
|
throw new InvalidOperationException($"源库物理表 {tableName} 不存在字段:{columnName}");
|
||||||
|
|
||||||
|
DataRow columnRow = columnResult.Rows[0];
|
||||||
|
string columnDefinition = BuildOracleColumnType(columnRow);
|
||||||
|
string dataDefault = columnRow["DATA_DEFAULT"] == DBNull.Value
|
||||||
|
? ""
|
||||||
|
: columnRow["DATA_DEFAULT"]?.ToString()?.Trim() ?? "";
|
||||||
|
if (!string.IsNullOrWhiteSpace(dataDefault))
|
||||||
|
columnDefinition += $" DEFAULT {dataDefault}";
|
||||||
|
if (string.Equals(columnRow["NULLABLE"]?.ToString(), "N", StringComparison.OrdinalIgnoreCase))
|
||||||
|
columnDefinition += " NOT NULL";
|
||||||
|
|
||||||
|
string quotedTable = QuoteOracleIdentifier(tableName);
|
||||||
|
string quotedColumn = QuoteOracleIdentifier(columnName);
|
||||||
|
string alterSql = $"ALTER TABLE {quotedTable} ADD ({quotedColumn} {columnDefinition})";
|
||||||
|
|
||||||
|
string commentSql = "";
|
||||||
|
var commentResult = db.Ado.GetDataTable(@"
|
||||||
|
SELECT COMMENTS
|
||||||
|
FROM USER_COL_COMMENTS
|
||||||
|
WHERE TABLE_NAME = :table_name
|
||||||
|
AND COLUMN_NAME = :column_name
|
||||||
|
AND COMMENTS IS NOT NULL",
|
||||||
|
new SugarParameter("table_name", tableName),
|
||||||
|
new SugarParameter("column_name", columnName));
|
||||||
|
if (commentResult.Rows.Count > 0)
|
||||||
|
{
|
||||||
|
string comment = EscapeQuote(commentResult.Rows[0]["COMMENTS"]?.ToString());
|
||||||
|
commentSql = $"COMMENT ON COLUMN {quotedTable}.{quotedColumn} IS '{comment}'";
|
||||||
|
}
|
||||||
|
|
||||||
|
var sql = new StringBuilder();
|
||||||
|
sql.AppendLine($"-- 新增物理表字段(已存在则跳过):{tableName}.{columnName}");
|
||||||
|
sql.AppendLine("DECLARE");
|
||||||
|
sql.AppendLine(" v_table_count NUMBER;");
|
||||||
|
sql.AppendLine(" v_column_count NUMBER;");
|
||||||
|
sql.AppendLine("BEGIN");
|
||||||
|
sql.AppendLine($" SELECT COUNT(*) INTO v_table_count FROM user_tables WHERE table_name = '{EscapeQuote(tableName)}';");
|
||||||
|
sql.AppendLine(" IF v_table_count = 0 THEN");
|
||||||
|
sql.AppendLine($" RAISE_APPLICATION_ERROR(-20201, '目标库缺少物理表:{EscapeQuote(tableName)}');");
|
||||||
|
sql.AppendLine(" END IF;");
|
||||||
|
sql.AppendLine($" SELECT COUNT(*) INTO v_column_count FROM user_tab_columns WHERE table_name = '{EscapeQuote(tableName)}' AND column_name = '{EscapeQuote(columnName)}';");
|
||||||
|
sql.AppendLine(" IF v_column_count = 0 THEN");
|
||||||
|
sql.AppendLine($" EXECUTE IMMEDIATE '{EscapeQuote(alterSql)}';");
|
||||||
|
if (!string.IsNullOrWhiteSpace(commentSql))
|
||||||
|
sql.AppendLine($" EXECUTE IMMEDIATE '{EscapeQuote(commentSql)}';");
|
||||||
|
sql.AppendLine(" END IF;");
|
||||||
|
sql.AppendLine("END;");
|
||||||
|
sql.AppendLine("/");
|
||||||
|
return sql.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>获取单个 BOS 字段配置,返回可在目标库执行的完整 PL/SQL</summary>
|
/// <summary>获取单个 BOS 字段配置,返回可在目标库执行的完整 PL/SQL</summary>
|
||||||
public static string GetTableBosField(string qualifiedFieldName)
|
public static string GetTableBosField(string qualifiedFieldName)
|
||||||
{
|
{
|
||||||
if (!TryParseBosFieldName(qualifiedFieldName, out string tableName, out string columnName))
|
if (!TryParseTableFieldName(qualifiedFieldName, out string tableName, out string columnName))
|
||||||
throw new ArgumentException("BOS 字段配置必须使用 TABLE.COLUMN 格式。", nameof(qualifiedFieldName));
|
throw new ArgumentException("BOS 字段配置必须使用 TABLE.COLUMN 格式。", nameof(qualifiedFieldName));
|
||||||
|
|
||||||
string physicalColumnSql = BuildPhysicalColumnEnsureSql(tableName, columnName);
|
string physicalColumnSql = BuildPhysicalColumnEnsureSql(tableName, columnName);
|
||||||
@@ -294,10 +368,10 @@ namespace henlo_migration.Services
|
|||||||
else
|
else
|
||||||
bosSql = output.Value.ToString() ?? "";
|
bosSql = output.Value.ToString() ?? "";
|
||||||
|
|
||||||
// 旧版过程输出中的说明只针对其自身生成的 BOS 块;最终文件已在开头加入物理字段检查。
|
// 文件开头会说明物理字段的实际表来源;视图和物化视图不生成物理字段 DDL。
|
||||||
bosSql = bosSql.Replace(
|
bosSql = bosSql.Replace(
|
||||||
"-- 注意:本文件不包含 Oracle 物理字段 DDL。",
|
"-- 注意:本文件不包含 Oracle 物理字段 DDL。",
|
||||||
"-- BOS 字段配置块;物理字段检查位于本文件开头。",
|
"-- BOS 字段配置块;是否包含物理字段 DDL 请参见本文件开头说明。",
|
||||||
StringComparison.Ordinal);
|
StringComparison.Ordinal);
|
||||||
return physicalColumnSql + bosSql;
|
return physicalColumnSql + bosSql;
|
||||||
}
|
}
|
||||||
@@ -308,26 +382,67 @@ namespace henlo_migration.Services
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>生成目标库物理字段存在性检查;源对象不是物理表或字段时只输出说明</summary>
|
/// <summary>解析 BOS 实际表,仅为普通物理表生成字段检查,排除视图和物化视图</summary>
|
||||||
private static string BuildPhysicalColumnEnsureSql(string tableName, string columnName)
|
private static string BuildPhysicalColumnEnsureSql(string tableName, string columnName)
|
||||||
{
|
{
|
||||||
var db = SqlSugarHelper.DbOrig;
|
var db = SqlSugarHelper.DbOrig;
|
||||||
int sourceTableCount = db.Ado.GetInt(
|
return BuildPhysicalColumnEnsureSql(tableName, columnName,
|
||||||
"SELECT COUNT(*) FROM USER_TABLES WHERE TABLE_NAME = :table_name",
|
(sql, parameters) => db.Ado.GetDataTable(sql, parameters));
|
||||||
new SugarParameter("table_name", tableName));
|
}
|
||||||
if (sourceTableCount == 0)
|
|
||||||
return $"-- 源库对象 {tableName} 不是物理表,不生成物理字段 DDL。\n";
|
|
||||||
|
|
||||||
var columnResult = db.Ado.GetDataTable(@"
|
// 查询委托便于离线验证分类分支,避免回归测试连接真实 Oracle。
|
||||||
|
private static string BuildPhysicalColumnEnsureSql(string tableName, string columnName,
|
||||||
|
Func<string, SugarParameter[], DataTable> query)
|
||||||
|
{
|
||||||
|
var bosTables = query(
|
||||||
|
"SELECT ID, NAME, REALTABLE_ID FROM AD_TABLE WHERE UPPER(NAME) = :table_name",
|
||||||
|
new[] { new SugarParameter("table_name", tableName) });
|
||||||
|
if (bosTables.Rows.Count == 0)
|
||||||
|
throw new InvalidOperationException($"BOS表[{tableName}]不存在");
|
||||||
|
if (bosTables.Rows.Count != 1)
|
||||||
|
throw new InvalidOperationException($"BOS表[{tableName}]存在多条AD_TABLE配置");
|
||||||
|
string bosTableName = tableName;
|
||||||
|
var visited = new HashSet<decimal>();
|
||||||
|
DataRow bosTable = bosTables.Rows[0];
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
if (!visited.Add(Convert.ToDecimal(bosTable["ID"])))
|
||||||
|
throw new InvalidOperationException($"BOS表[{bosTableName}]的 REALTABLE_ID 存在循环引用。");
|
||||||
|
tableName = bosTable["NAME"]?.ToString()?.Trim().ToUpperInvariant() ?? "";
|
||||||
|
if (string.IsNullOrWhiteSpace(tableName))
|
||||||
|
throw new InvalidOperationException($"BOS表[{bosTableName}]的实际表名称为空。");
|
||||||
|
if (bosTable["REALTABLE_ID"] == DBNull.Value)
|
||||||
|
break;
|
||||||
|
|
||||||
|
var realTables = query(
|
||||||
|
"SELECT ID, NAME, REALTABLE_ID FROM AD_TABLE WHERE ID = :table_id",
|
||||||
|
new[] { new SugarParameter("table_id", bosTable["REALTABLE_ID"]) });
|
||||||
|
if (realTables.Rows.Count != 1)
|
||||||
|
throw new InvalidOperationException($"BOS表[{bosTableName}]的 REALTABLE_ID 指向的表配置不存在或不唯一。");
|
||||||
|
bosTable = realTables.Rows[0];
|
||||||
|
}
|
||||||
|
string sourceNote = $"-- BOS表 {bosTableName} 的物理字段来源:{tableName}.{columnName}\n";
|
||||||
|
|
||||||
|
var sourceTables = query(@"
|
||||||
|
SELECT COUNT(*) FROM USER_TABLES t
|
||||||
|
WHERE t.TABLE_NAME = :table_name
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM USER_VIEWS v WHERE v.VIEW_NAME = t.TABLE_NAME)
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM USER_MVIEWS m WHERE m.MVIEW_NAME = t.TABLE_NAME)",
|
||||||
|
new[] { new SugarParameter("table_name", tableName) });
|
||||||
|
int sourceTableCount = Convert.ToInt32(sourceTables.Rows[0][0]);
|
||||||
|
if (sourceTableCount == 0)
|
||||||
|
return sourceNote + $"-- 源库对象 {tableName} 不是普通物理表(可能为视图或物化视图),仅导出 BOS 字段配置。\n";
|
||||||
|
|
||||||
|
var columnResult = query(@"
|
||||||
SELECT DATA_TYPE, DATA_LENGTH, DATA_PRECISION, DATA_SCALE,
|
SELECT DATA_TYPE, DATA_LENGTH, DATA_PRECISION, DATA_SCALE,
|
||||||
CHAR_LENGTH, CHAR_USED
|
CHAR_LENGTH, CHAR_USED
|
||||||
FROM USER_TAB_COLUMNS
|
FROM USER_TAB_COLUMNS
|
||||||
WHERE TABLE_NAME = :table_name
|
WHERE TABLE_NAME = :table_name
|
||||||
AND COLUMN_NAME = :column_name",
|
AND COLUMN_NAME = :column_name",
|
||||||
new SugarParameter("table_name", tableName),
|
new[] { new SugarParameter("table_name", tableName),
|
||||||
new SugarParameter("column_name", columnName));
|
new SugarParameter("column_name", columnName) });
|
||||||
if (columnResult.Rows.Count == 0)
|
if (columnResult.Rows.Count == 0)
|
||||||
return $"-- 源库物理表 {tableName} 不存在字段 {columnName},仅导出 BOS 字段配置。\n";
|
return sourceNote + $"-- 源库物理表 {tableName} 不存在字段 {columnName},仅导出 BOS 字段配置。\n";
|
||||||
|
|
||||||
string columnType = BuildOracleColumnType(columnResult.Rows[0]);
|
string columnType = BuildOracleColumnType(columnResult.Rows[0]);
|
||||||
string quotedTable = QuoteOracleIdentifier(tableName);
|
string quotedTable = QuoteOracleIdentifier(tableName);
|
||||||
@@ -335,12 +450,15 @@ namespace henlo_migration.Services
|
|||||||
string alterSql = $"ALTER TABLE {quotedTable} ADD ({quotedColumn} {columnType})";
|
string alterSql = $"ALTER TABLE {quotedTable} ADD ({quotedColumn} {columnType})";
|
||||||
|
|
||||||
var sql = new StringBuilder();
|
var sql = new StringBuilder();
|
||||||
|
sql.Append(sourceNote);
|
||||||
sql.AppendLine($"-- 确保目标库物理字段存在:{tableName}.{columnName}");
|
sql.AppendLine($"-- 确保目标库物理字段存在:{tableName}.{columnName}");
|
||||||
sql.AppendLine("DECLARE");
|
sql.AppendLine("DECLARE");
|
||||||
sql.AppendLine(" v_table_count NUMBER;");
|
sql.AppendLine(" v_table_count NUMBER;");
|
||||||
sql.AppendLine(" v_column_count NUMBER;");
|
sql.AppendLine(" v_column_count NUMBER;");
|
||||||
sql.AppendLine("BEGIN");
|
sql.AppendLine("BEGIN");
|
||||||
sql.AppendLine($" SELECT COUNT(*) INTO v_table_count FROM user_tables WHERE table_name = '{EscapeQuote(tableName)}';");
|
sql.AppendLine($" SELECT COUNT(*) INTO v_table_count FROM user_tables t WHERE t.table_name = '{EscapeQuote(tableName)}'");
|
||||||
|
sql.AppendLine(" AND NOT EXISTS (SELECT 1 FROM user_views v WHERE v.view_name = t.table_name)");
|
||||||
|
sql.AppendLine(" AND NOT EXISTS (SELECT 1 FROM user_mviews m WHERE m.mview_name = t.table_name);");
|
||||||
sql.AppendLine(" IF v_table_count = 0 THEN");
|
sql.AppendLine(" IF v_table_count = 0 THEN");
|
||||||
sql.AppendLine($" RAISE_APPLICATION_ERROR(-20201, '目标库缺少物理表:{EscapeQuote(tableName)}');");
|
sql.AppendLine($" RAISE_APPLICATION_ERROR(-20201, '目标库缺少物理表:{EscapeQuote(tableName)}');");
|
||||||
sql.AppendLine(" END IF;");
|
sql.AppendLine(" END IF;");
|
||||||
|
|||||||
@@ -16,8 +16,9 @@ namespace henlo_migration.ViewModels
|
|||||||
{
|
{
|
||||||
public class MainViewModel : INotifyPropertyChanged
|
public class MainViewModel : INotifyPropertyChanged
|
||||||
{
|
{
|
||||||
// 五个 Tab 的数据
|
// 六个 Tab 的数据
|
||||||
public TabServices TableTab { get; }
|
public TabServices TableTab { get; }
|
||||||
|
public TabServices TableFieldTab { get; }
|
||||||
public TabServices BosTab { get; }
|
public TabServices BosTab { get; }
|
||||||
public TabServices BosFieldTab { get; }
|
public TabServices BosFieldTab { get; }
|
||||||
public TabServices PrcTab { get; }
|
public TabServices PrcTab { get; }
|
||||||
@@ -33,6 +34,7 @@ namespace henlo_migration.ViewModels
|
|||||||
_currentTab = value;
|
_currentTab = value;
|
||||||
OnPropertyChanged();
|
OnPropertyChanged();
|
||||||
OnPropertyChanged(nameof(TablesData));
|
OnPropertyChanged(nameof(TablesData));
|
||||||
|
OnPropertyChanged(nameof(TableFieldsData));
|
||||||
OnPropertyChanged(nameof(BosTablesData));
|
OnPropertyChanged(nameof(BosTablesData));
|
||||||
OnPropertyChanged(nameof(BosFieldsData));
|
OnPropertyChanged(nameof(BosFieldsData));
|
||||||
OnPropertyChanged(nameof(StoredProcsData));
|
OnPropertyChanged(nameof(StoredProcsData));
|
||||||
@@ -51,14 +53,16 @@ namespace henlo_migration.ViewModels
|
|||||||
CurrentTab = value switch
|
CurrentTab = value switch
|
||||||
{
|
{
|
||||||
0 => TableTab,
|
0 => TableTab,
|
||||||
1 => BosTab,
|
1 => TableFieldTab,
|
||||||
2 => BosFieldTab,
|
2 => BosTab,
|
||||||
3 => PrcTab,
|
3 => BosFieldTab,
|
||||||
4 => DataTab,
|
4 => PrcTab,
|
||||||
|
5 => DataTab,
|
||||||
_ => TableTab
|
_ => TableTab
|
||||||
};
|
};
|
||||||
OnPropertyChanged();
|
OnPropertyChanged();
|
||||||
OnPropertyChanged(nameof(TablesData));
|
OnPropertyChanged(nameof(TablesData));
|
||||||
|
OnPropertyChanged(nameof(TableFieldsData));
|
||||||
OnPropertyChanged(nameof(BosTablesData));
|
OnPropertyChanged(nameof(BosTablesData));
|
||||||
OnPropertyChanged(nameof(BosFieldsData));
|
OnPropertyChanged(nameof(BosFieldsData));
|
||||||
OnPropertyChanged(nameof(StoredProcsData));
|
OnPropertyChanged(nameof(StoredProcsData));
|
||||||
@@ -68,8 +72,9 @@ namespace henlo_migration.ViewModels
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 五个Tab数据源别名(供DataGrid绑定)
|
// 六个Tab数据源别名(供DataGrid绑定)
|
||||||
public ObservableCollection<TableRowModel> TablesData => TableTab.List;
|
public ObservableCollection<TableRowModel> TablesData => TableTab.List;
|
||||||
|
public ObservableCollection<TableRowModel> TableFieldsData => TableFieldTab.List;
|
||||||
public ObservableCollection<TableRowModel> BosTablesData => BosTab.List;
|
public ObservableCollection<TableRowModel> BosTablesData => BosTab.List;
|
||||||
public ObservableCollection<TableRowModel> BosFieldsData => BosFieldTab.List;
|
public ObservableCollection<TableRowModel> BosFieldsData => BosFieldTab.List;
|
||||||
public ObservableCollection<TableRowModel> StoredProcsData => PrcTab.List;
|
public ObservableCollection<TableRowModel> StoredProcsData => PrcTab.List;
|
||||||
@@ -78,10 +83,11 @@ namespace henlo_migration.ViewModels
|
|||||||
public string CurrentTabName => SelectedTabIndex switch
|
public string CurrentTabName => SelectedTabIndex switch
|
||||||
{
|
{
|
||||||
0 => "表配置",
|
0 => "表配置",
|
||||||
1 => "BOS配置",
|
1 => "表字段",
|
||||||
2 => "BOS字段配置",
|
2 => "BOS配置",
|
||||||
3 => "存储过程",
|
3 => "BOS字段配置",
|
||||||
4 => "表数据",
|
4 => "存储过程",
|
||||||
|
5 => "表数据",
|
||||||
_ => ""
|
_ => ""
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -269,10 +275,13 @@ namespace henlo_migration.ViewModels
|
|||||||
DestUser = SysConfig.GetDestUser();
|
DestUser = SysConfig.GetDestUser();
|
||||||
DestPwd = SysConfig.GetDestPwd();
|
DestPwd = SysConfig.GetDestPwd();
|
||||||
|
|
||||||
// 初始化五个 TabServices(索引文件在 from/,内容文件在 data/)
|
// 初始化六个 TabServices(索引文件在 from/,内容文件在 data/)
|
||||||
TableTab = new TabServices(new TabConfig(TabsEnum.表, CommonHelper.DataFromPath, CommonHelper.DataPath, "table.txt", CodeExt.creat,
|
TableTab = new TabServices(new TabConfig(TabsEnum.表, CommonHelper.DataFromPath, CommonHelper.DataPath, "table.txt", CodeExt.creat,
|
||||||
DbServices.GetTableSql, (p, c) => SqlCode = c));
|
DbServices.GetTableSql, (p, c) => SqlCode = c));
|
||||||
|
|
||||||
|
TableFieldTab = new TabServices(new TabConfig(TabsEnum.表字段, CommonHelper.DataFromPath, CommonHelper.DataPath, "table_field.txt", CodeExt.table_field,
|
||||||
|
DbServices.GetTableField, (p, c) => SqlCode = c));
|
||||||
|
|
||||||
BosTab = new TabServices(new TabConfig(TabsEnum.BOS表配置, CommonHelper.DataFromPath, CommonHelper.DataPath, "bos.txt", CodeExt.bos,
|
BosTab = new TabServices(new TabConfig(TabsEnum.BOS表配置, CommonHelper.DataFromPath, CommonHelper.DataPath, "bos.txt", CodeExt.bos,
|
||||||
DbServices.GetTableBos, (p, c) => SqlCode = c));
|
DbServices.GetTableBos, (p, c) => SqlCode = c));
|
||||||
|
|
||||||
@@ -286,7 +295,7 @@ namespace henlo_migration.ViewModels
|
|||||||
DbServices.GetTableData, (p, c) => SqlCode = c));
|
DbServices.GetTableData, (p, c) => SqlCode = c));
|
||||||
|
|
||||||
// Tab行点击 -> 显示SQL
|
// Tab行点击 -> 显示SQL
|
||||||
foreach (var tab in new[] { TableTab, BosTab, BosFieldTab, PrcTab, DataTab })
|
foreach (var tab in new[] { TableTab, TableFieldTab, BosTab, BosFieldTab, PrcTab, DataTab })
|
||||||
{
|
{
|
||||||
tab.RowClickEvent += (path, code) =>
|
tab.RowClickEvent += (path, code) =>
|
||||||
{
|
{
|
||||||
@@ -380,11 +389,12 @@ namespace henlo_migration.ViewModels
|
|||||||
if (!string.IsNullOrWhiteSpace(InputText))
|
if (!string.IsNullOrWhiteSpace(InputText))
|
||||||
{
|
{
|
||||||
string itemName = InputText.Trim();
|
string itemName = InputText.Trim();
|
||||||
if (CurrentTab == BosFieldTab)
|
if (CurrentTab == TableFieldTab || CurrentTab == BosFieldTab)
|
||||||
{
|
{
|
||||||
if (!DbServices.TryParseBosFieldName(itemName, out string tableName, out string columnName))
|
if (!DbServices.TryParseTableFieldName(itemName, out string tableName, out string columnName))
|
||||||
{
|
{
|
||||||
HandyControl.Controls.Growl.Warning("BOS 字段配置必须使用 TABLE.COLUMN 格式");
|
string tabName = CurrentTab == TableFieldTab ? "表字段" : "BOS 字段配置";
|
||||||
|
HandyControl.Controls.Growl.Warning($"{tabName}必须使用 TABLE.COLUMN 格式");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
itemName = $"{tableName}.{columnName}";
|
itemName = $"{tableName}.{columnName}";
|
||||||
|
|||||||
@@ -345,6 +345,78 @@
|
|||||||
</DataGrid>
|
</DataGrid>
|
||||||
</TabItem>
|
</TabItem>
|
||||||
|
|
||||||
|
<!-- 表字段 -->
|
||||||
|
<TabItem Header="表字段">
|
||||||
|
<DataGrid x:Name="TableFieldsGrid" ItemsSource="{Binding TableFieldsData}"
|
||||||
|
SelectedItem="{Binding SelectedRow}"
|
||||||
|
RowStyle="{StaticResource DataGridRowStyleFix}"
|
||||||
|
CellStyle="{StaticResource DataGridCellStyleFix}"
|
||||||
|
AutoGenerateColumns="False" CanUserAddRows="False"
|
||||||
|
IsReadOnly="True" HeadersVisibility="Column"
|
||||||
|
GridLinesVisibility="Horizontal" BorderThickness="1"
|
||||||
|
BorderBrush="#d0d7de">
|
||||||
|
<DataGrid.Columns>
|
||||||
|
<DataGridTemplateColumn Header="表名.字段名" Width="*" MinWidth="180">
|
||||||
|
<DataGridTemplateColumn.CellTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<TextBlock Text="{Binding Name}" Style="{StaticResource CopyableNameWhenSelected}" b:ClickToCopy.Selectable="True" VerticalAlignment="Center"/>
|
||||||
|
</DataTemplate>
|
||||||
|
</DataGridTemplateColumn.CellTemplate>
|
||||||
|
</DataGridTemplateColumn>
|
||||||
|
<DataGridTemplateColumn Header="状态" Width="160">
|
||||||
|
<DataGridTemplateColumn.CellTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||||
|
<Border Style="{StaticResource StatusBadge}">
|
||||||
|
<Border.Background>
|
||||||
|
<MultiBinding Converter="{StaticResource ReadStatusColorConverter}">
|
||||||
|
<Binding Path="RStatus"/>
|
||||||
|
</MultiBinding>
|
||||||
|
</Border.Background>
|
||||||
|
<TextBlock Text="{Binding ReadStatusText}" Style="{StaticResource StatusBadgeText}"/>
|
||||||
|
</Border>
|
||||||
|
<Border Style="{StaticResource StatusBadge}" Margin="6,0,0,0">
|
||||||
|
<Border.Background>
|
||||||
|
<MultiBinding Converter="{StaticResource WriteStatusColorConverter}">
|
||||||
|
<Binding Path="WStatus"/>
|
||||||
|
</MultiBinding>
|
||||||
|
</Border.Background>
|
||||||
|
<TextBlock Text="{Binding WriteStatusText}" Style="{StaticResource StatusBadgeText}"/>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
</DataTemplate>
|
||||||
|
</DataGridTemplateColumn.CellTemplate>
|
||||||
|
</DataGridTemplateColumn>
|
||||||
|
<DataGridTemplateColumn Header="操作" Width="320">
|
||||||
|
<DataGridTemplateColumn.CellTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<Button Content="读取" Style="{StaticResource SmallBtn}"
|
||||||
|
Command="{Binding DataContext.ReadCommand, RelativeSource={RelativeSource AncestorType=DataGrid}}"
|
||||||
|
CommandParameter="{Binding}"/>
|
||||||
|
<Button Content="写入" Style="{StaticResource SmallSuccessBtn}"
|
||||||
|
Command="{Binding DataContext.WriteCommand, RelativeSource={RelativeSource AncestorType=DataGrid}}"
|
||||||
|
CommandParameter="{Binding}"/>
|
||||||
|
<Button Content="删除" Style="{StaticResource SmallWarnBtn}"
|
||||||
|
Command="{Binding DataContext.DeleteCommand, RelativeSource={RelativeSource AncestorType=DataGrid}}"
|
||||||
|
CommandParameter="{Binding}"/>
|
||||||
|
<Button Content="↑" Style="{StaticResource SmallDefaultBtn}" MinWidth="36"
|
||||||
|
Command="{Binding DataContext.MoveUpCommand, RelativeSource={RelativeSource AncestorType=DataGrid}}"
|
||||||
|
CommandParameter="{Binding}"/>
|
||||||
|
<Button Content="↓" Style="{StaticResource SmallDefaultBtn}" MinWidth="36"
|
||||||
|
Command="{Binding DataContext.MoveDownCommand, RelativeSource={RelativeSource AncestorType=DataGrid}}"
|
||||||
|
CommandParameter="{Binding}"/>
|
||||||
|
<Button Content="Top" Style="{StaticResource SmallDefaultBtn}" MinWidth="40"
|
||||||
|
Command="{Binding DataContext.MoveTopCommand, RelativeSource={RelativeSource AncestorType=DataGrid}}"
|
||||||
|
CommandParameter="{Binding}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</DataTemplate>
|
||||||
|
</DataGridTemplateColumn.CellTemplate>
|
||||||
|
</DataGridTemplateColumn>
|
||||||
|
</DataGrid.Columns>
|
||||||
|
</DataGrid>
|
||||||
|
</TabItem>
|
||||||
|
|
||||||
<!-- BOS配置 -->
|
<!-- BOS配置 -->
|
||||||
<TabItem Header="BOS配置">
|
<TabItem Header="BOS配置">
|
||||||
<DataGrid x:Name="BosGrid" ItemsSource="{Binding BosTablesData}"
|
<DataGrid x:Name="BosGrid" ItemsSource="{Binding BosTablesData}"
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<Nullable>disable</Nullable>
|
<Nullable>disable</Nullable>
|
||||||
<ApplicationIcon>Resources\favicon.ico</ApplicationIcon>
|
<ApplicationIcon>Resources\favicon.ico</ApplicationIcon>
|
||||||
<Deterministic>true</Deterministic>
|
<Deterministic>true</Deterministic>
|
||||||
<Version>1.0.18</Version>
|
<Version>1.0.20</Version>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net10.0-windows</TargetFramework>
|
||||||
|
<UseWPF>true</UseWPF>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="../../henlo_migration_wpf10/henlo_migration_wpf10.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
using System.Data;
|
||||||
|
using System.Reflection;
|
||||||
|
using henlo_migration.Services;
|
||||||
|
using SqlSugar;
|
||||||
|
|
||||||
|
// Only the injected metadata reader is called. No app startup, configuration or Oracle connection.
|
||||||
|
var builder = typeof(DbServices).GetMethod("BuildPhysicalColumnEnsureSql",
|
||||||
|
BindingFlags.NonPublic | BindingFlags.Static, null,
|
||||||
|
[typeof(string), typeof(string), typeof(Func<string, SugarParameter[], DataTable>)], null)!;
|
||||||
|
var splitter = typeof(DbServices).GetMethod("SplitStatements", BindingFlags.NonPublic | BindingFlags.Static)!;
|
||||||
|
var passed = 0;
|
||||||
|
|
||||||
|
void Check(bool condition, string message)
|
||||||
|
{
|
||||||
|
if (!condition) throw new Exception(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Run(string label, TableInfo[] tables, string kind, string? expectedTable,
|
||||||
|
bool columnExists = true, string? expectedError = null)
|
||||||
|
{
|
||||||
|
var columnReads = 0;
|
||||||
|
string? queriedTable = null;
|
||||||
|
DataTable Read(string sql, SugarParameter[] parameters)
|
||||||
|
{
|
||||||
|
var result = new DataTable();
|
||||||
|
if (sql.Contains("FROM AD_TABLE"))
|
||||||
|
{
|
||||||
|
result.Columns.Add("ID", typeof(decimal));
|
||||||
|
result.Columns.Add("NAME", typeof(string));
|
||||||
|
result.Columns.Add("REALTABLE_ID", typeof(decimal));
|
||||||
|
var matches = sql.Contains("UPPER(NAME)")
|
||||||
|
? tables.Where(t => t.Name.Equals(parameters[0].Value.ToString(), StringComparison.OrdinalIgnoreCase))
|
||||||
|
: tables.Where(t => t.Id == Convert.ToDecimal(parameters[0].Value));
|
||||||
|
foreach (var t in matches)
|
||||||
|
result.Rows.Add(t.Id, t.Name, t.RealId.HasValue ? (object)t.RealId.Value : DBNull.Value);
|
||||||
|
}
|
||||||
|
else if (sql.Contains("FROM USER_TABLES"))
|
||||||
|
{
|
||||||
|
Check(sql.Contains("NOT EXISTS (SELECT 1 FROM USER_VIEWS"), "源库未排除视图");
|
||||||
|
Check(sql.Contains("NOT EXISTS (SELECT 1 FROM USER_MVIEWS"), "源库未排除物化视图");
|
||||||
|
queriedTable = parameters[0].Value.ToString();
|
||||||
|
result.Columns.Add("COUNT", typeof(int));
|
||||||
|
// Simulate the filtered dictionary result, including MVIEW's backing USER_TABLES row.
|
||||||
|
result.Rows.Add(kind == "TABLE" ? 1 : 0);
|
||||||
|
}
|
||||||
|
else if (sql.Contains("FROM USER_TAB_COLUMNS"))
|
||||||
|
{
|
||||||
|
columnReads++;
|
||||||
|
Check(parameters[0].Value.ToString() == queriedTable, "字段查询未使用实际表名");
|
||||||
|
Check(parameters[1].Value.ToString() == "TEST_COL", "字段名称改变");
|
||||||
|
foreach (var name in new[] { "DATA_TYPE", "DATA_LENGTH", "DATA_PRECISION", "DATA_SCALE", "CHAR_LENGTH", "CHAR_USED" })
|
||||||
|
result.Columns.Add(name, typeof(object));
|
||||||
|
if (columnExists) result.Rows.Add("VARCHAR2", 120, DBNull.Value, DBNull.Value, 30, "C");
|
||||||
|
}
|
||||||
|
else throw new Exception("意外查询:" + sql);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var sql = (string)builder.Invoke(null,
|
||||||
|
["BOS_TEST", "TEST_COL", (Func<string, SugarParameter[], DataTable>)Read])!;
|
||||||
|
Check(expectedError == null, label + " 应报错");
|
||||||
|
if (expectedTable != null)
|
||||||
|
{
|
||||||
|
Check(queriedTable == expectedTable, label + " 实际表解析错误");
|
||||||
|
Check(columnReads == 1, label + " 未读取实际字段");
|
||||||
|
Check(sql.Contains($"ALTER TABLE \"{expectedTable}\" ADD (\"TEST_COL\" VARCHAR2(30 CHAR))"), label + " DDL不正确");
|
||||||
|
Check(sql.Contains("NOT EXISTS (SELECT 1 FROM user_mviews"), "目标库未排除物化视图");
|
||||||
|
Check(sql.Contains($"table_name = '{expectedTable}' AND column_name = 'TEST_COL'"), "目标字段检查错误");
|
||||||
|
Check(sql.IndexOf("IF v_column_count = 0 THEN", StringComparison.Ordinal)
|
||||||
|
< sql.IndexOf("EXECUTE IMMEDIATE", StringComparison.Ordinal), "已有字段没有跳过保护");
|
||||||
|
Check(sql.Contains("IF v_table_count = 0 THEN") && sql.Contains("RAISE_APPLICATION_ERROR"), "目标表缺失未报错");
|
||||||
|
var blocks = (string[])splitter.Invoke(null, [sql + "BEGIN NULL; END;\n/\n"])!;
|
||||||
|
Check(blocks.Length == 2 && blocks[0].EndsWith("END;") && blocks[1] == "BEGIN NULL; END;", "PL/SQL分隔错误");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Check(!sql.Contains("ALTER TABLE") && !sql.Contains("EXECUTE IMMEDIATE"), label + " 不应生成物理DDL");
|
||||||
|
Check(columnReads == (kind == "TABLE" ? 1 : 0), label + " 发生非预期字段读取");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (TargetInvocationException ex) when (expectedError != null)
|
||||||
|
{
|
||||||
|
Check(ex.InnerException is InvalidOperationException
|
||||||
|
&& ex.InnerException.Message.Contains(expectedError), label + " 异常不正确:" + ex.InnerException?.Message);
|
||||||
|
Check(columnReads == 0, "无效映射仍读取字段");
|
||||||
|
}
|
||||||
|
Console.WriteLine("PASS " + label);
|
||||||
|
passed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
Run("同名普通表", [new(1, "BOS_TEST", null)], "TABLE", "BOS_TEST");
|
||||||
|
Run("REALTABLE_ID映射", [new(1, "BOS_TEST", 2), new(2, "REAL_TEST", null)], "TABLE", "REAL_TEST");
|
||||||
|
Run("多级映射", [new(1, "BOS_TEST", 2), new(2, "MID_TEST", 3), new(3, "REAL_TEST", null)], "TABLE", "REAL_TEST");
|
||||||
|
Run("普通视图", [new(1, "BOS_TEST", null)], "VIEW", null);
|
||||||
|
Run("物化视图及其底层表", [new(1, "BOS_TEST", null)], "MVIEW", null);
|
||||||
|
Run("映射到视图", [new(1, "BOS_TEST", 2), new(2, "REAL_TEST", null)], "VIEW", null);
|
||||||
|
Run("映射到物化视图", [new(1, "BOS_TEST", 2), new(2, "REAL_TEST", null)], "MVIEW", null);
|
||||||
|
Run("源物理对象不存在", [new(1, "BOS_TEST", null)], "MISSING", null);
|
||||||
|
Run("BOS计算字段", [new(1, "BOS_TEST", null)], "TABLE", null, columnExists: false);
|
||||||
|
Run("映射实际表缺少字段", [new(1, "BOS_TEST", 2), new(2, "REAL_TEST", null)], "TABLE", null, columnExists: false);
|
||||||
|
Run("映射记录缺失", [new(1, "BOS_TEST", 2)], "TABLE", null, expectedError: "不存在或不唯一");
|
||||||
|
Run("映射循环", [new(1, "BOS_TEST", 2), new(2, "REAL_TEST", 1)], "TABLE", null, expectedError: "循环引用");
|
||||||
|
Run("自引用", [new(1, "BOS_TEST", 1)], "TABLE", null, expectedError: "循环引用");
|
||||||
|
Run("BOS表不存在", [], "TABLE", null, expectedError: "不存在");
|
||||||
|
Run("BOS表名重复", [new(1, "BOS_TEST", null), new(2, "BOS_TEST", null)], "TABLE", null, expectedError: "多条");
|
||||||
|
Run("映射ID不唯一", [new(1, "BOS_TEST", 2), new(2, "REAL_A", null), new(2, "REAL_B", null)], "TABLE", null, expectedError: "不存在或不唯一");
|
||||||
|
Run("实际表名称为空", [new(1, "BOS_TEST", 2), new(2, "", null)], "TABLE", null, expectedError: "名称为空");
|
||||||
|
Run("实际表名称规范化", [new(1, "BOS_TEST", 2), new(2, "real_test", null)], "TABLE", "REAL_TEST");
|
||||||
|
var representativeSql = "CREATE TABLE TEST_T (ID NUMBER);\n/\n"
|
||||||
|
+ "COMMENT ON COLUMN TEST_T.ID IS '编号';\n/\n"
|
||||||
|
+ "ALTER TABLE TEST_T ADD CONSTRAINT TEST_PK PRIMARY KEY (ID);\n/\n"
|
||||||
|
+ "BEGIN NULL; END;\n/\nINSERT INTO TEST_T (ID) VALUES (1);\n/\n";
|
||||||
|
var statements = (string[])splitter.Invoke(null, [representativeSql])!;
|
||||||
|
Check(statements.Length == 5 && statements[3] == "BEGIN NULL; END;", "代表性DDL/注释/约束/PLSQL/INSERT分隔回归失败");
|
||||||
|
Console.WriteLine("PASS DDL/注释/约束/PLSQL/INSERT分隔");
|
||||||
|
Console.WriteLine($"通过 {passed} 个离线回归场景(未连接数据库)。");
|
||||||
|
|
||||||
|
record TableInfo(decimal Id, string Name, decimal? RealId);
|
||||||
Reference in New Issue
Block a user