Commit b427992c authored by Data Governance Dev's avatar Data Governance Dev

docs(worklog): Oracle 端到端打通踩坑汇总(4 项)

之前已分别记录了 5 个 Oracle 相关条目(放开 Oracle / connect_timeout /
DPY-3010 / 路径外置 / DPI-1047 PE 解析)。本条目汇总本轮新加的 4 个修复:

1. health SQL 不分方言 → ORA-00923(SELECT 1 不 FROM)
2. 前端 UI 隐藏 Oracle Client 输入框
3. 抽数据字典 ORA-00904 → 11g ALL_TABLES 实际可用列清单
4. 抽数据字典 0 行 → service_name vs schema 名混淆

含关键对照表:
- MSVC 版本 vs MSVCR DLL(VC++ 2013 Redist 是关键)
- MySQL/达梦/Oracle 的 schema 名语义差异
parent 2a9cf418
...@@ -2,6 +2,592 @@ ...@@ -2,6 +2,592 @@
> 任务做完一次记一次。最近的在最上面。 > 任务做完一次记一次。最近的在最上面。
## 2026-08-13 · Oracle 端到端打通:health SQL 分方言 + UI 隐藏路径 + 11g ALL_TABLES 兼容 + schema 名修正
> 接续上面 5 个条目(放开 Oracle / connect_timeout / DPY-3010 / 路径外置 / DPI-1047 PE 解析)。
> 这一轮把 Oracle 从「能连上」推进到「能抽到数据字典」。共 4 个独立踩坑。
### 1. 健康检查 SQL 不分方言 → ORA-00923: FROM keyword not found where expected
- **现象**:thick 模式连接成功(`[DB] 连接已建立(耗时 907ms)`),但 `test_connection()` 跑 `SELECT 1` 报 `ORA-00923: FROM keyword not found where expected`。
- **根因**:Oracle 严格执行 SQL 标准 `SELECT 必须 FROM`,不像 MySQL/达梦可以裸 `SELECT 1`。`web/sql/health/check_connection.sql`(通用版)只写了 `SELECT 1`。
- **修复**:新建 [web/sql/health/check_connection.oracle.sql](web/sql/health/check_connection.oracle.sql),写 `SELECT 1 FROM dual`(dual 是 Oracle 内置单行单列表)。sql_loader 按 `<name>.<dialect>.sql` 优先 + `.sql` 回退的机制自动分流。
- **达梦为什么不修**:DM8 兼容 Oracle 语法有 dual,但同时也兼容 `SELECT 1`(no-FROM);用户从未在达梦上报过此错,保持通用版不动。
### 2. 前端 Oracle Client 输入框隐藏(路径改走 yaml 配置)
- **背景**:上一轮已经把 `oracle_client_dir` 字段加到了 `/api/db-defaults` 接口和 `web/configs/db_defaults.yaml`;但前端 UI 上还有个输入框让用户填。
- **用户诉求**:直接读配置文件,不要让用户填(避免误操作、避免泄露在浏览器历史)。
- **修复**:[web/static/index.html](web/static/index.html)
- 删 343-350 行那个 `el-row`("Oracle Client" 输入框整段)
- 保留 `form.oracle_client_dir` state(仍是 form 数据载体)
- 保留 `loadDbDefaults()` 注入(从 yaml 灌进 form)
- `testConnection()` 里 `...form` 展开会自动带上 `oracle_client_dir` → 后端 `cfg.oracle_client_dir` 拿到值
- **修改 Instant Client 路径的新流程**:改 `web/configs/db_defaults.yaml` → 重启 web 服务 → 测连接生效。
### 3. 抽数据字典 ORA-00904: "T"."BYTES" / "T"."CREATED" / "T"."LAST_DDL_TIME" invalid identifier(11g ALL_TABLES 实际可用列)
- **现象**:连接 + health SQL 都修好后,data_dict 抽数据字典时报 `ORA-00904: "T"."LAST_DDL_TIME": invalid identifier`,连续踩三次:
1. `LAST_DDL_TIME` → 改成 NULL
2. 接着报 `CREATED` → 也改 NULL
3. 接着报 `BYTES` → 也改 NULL
- **根因**:Oracle **12c 起**给 `ALL_TABLES / DBA_TABLES / USER_TABLES` 加了 `LAST_DDL_TIME`(最后 DDL 时间),**12.2 起**加了 `CREATED`(创建时间),**BYTES** 也是 12c+ 才有的字段。用户 Oracle 是 **11.2.0.4**,这三个列都不存在。
- **破局**:让用户跑 `SELECT column_name FROM all_tab_columns WHERE table_name='ALL_TABLES' AND owner='SYS' ORDER BY column_id;`(等价于 `DESC ALL_TABLES`),拿到精确列清单后一次性修对。
- **11.2.0.4 ALL_TABLES 实际可用列**(前 27 行,用户实地查的):
```
OWNER / TABLE_NAME / TABLESPACE_NAME / CLUSTER_NAME / IOT_NAME / STATUS /
PCT_FREE / PCT_USED / INI_TRANS / MAX_TRANS /
INITIAL_EXTENT / NEXT_EXTENT / MIN_EXTENTS / MAX_EXTENTS / PCT_INCREASE /
FREELISTS / FREELIST_GROUPS / LOGGING / BACKED_UP /
NUM_ROWS / BLOCKS / EMPTY_BLOCKS / AVG_SPACE / CHAIN_CNT / AVG_ROW_LEN /
AVG_SPACE_FREELIST_BLOCKS / NUM_FREELIST_BLOCKS
```
时间类字段只有 `LAST_ANALYZED`(最后统计时间,**不是** DDL 时间),不适合替代。
- **修复**:[web/sql/info_schema/list_tables.oracle.sql](web/sql/info_schema/list_tables.oracle.sql)
- `data_length` = `t.BLOCKS * 8192`(11g 没 BYTES,用 BLOCKS×8K 估算 Oracle 默认 block size;空表为 0 因未分配 segment)
- `create_time` / `update_time` = NULL
- 注释里贴了完整的列清单,下次维护不用再跑 DESC
- **数据准确性提醒**:`NUM_ROWS / BLOCKS` 都依赖 `DBMS_STATS` 统计信息;新建表或大量 DML 后是 0/过期值,「找大表」前最好 `EXEC DBMS_STATS.GATHER_SCHEMA_STATS('GJJ80')`。
- **未来接 12c+ 的方案**:新增 `list_tables.oracle.12c.sql`,sql_loader 自动优先选方言版本,不影响 11g。**当前不过度设计**。
### 4. 抽数据字典返回 0 行:service_name vs schema 名混淆
- **现象**:第 3 步修完后日志变成 `(表 0 张, 字段 0 个)` —— SQL 跑了但 WHERE 没匹配到。
- **根因**:[web/core/data_dict.py:45](web/core/data_dict.py#L45) 把 `cfg.database` 传给 `db.list_tables(cfg.database)`,但 Oracle 体系里 `ALL_TABLES.OWNER` 是 **用户名**(`GJJ80`),不是 service_name(`ZFGJJ`)。MySQL/达梦 `database` 既是连接用也是 schema 名,所以两者一致,Oracle 不一致。
- **关键概念区分**:
| 数据库 | 连接用字段 | schema 名 = ? |
|---|---|---|
| MySQL | database | database(一致) |
| 达梦 | database | database(一致) |
| **Oracle** | **service_name** (`ZFGJJ`) | **user** (`GJJ80`)(不一致) |
- **修复**:[web/core/data_dict.py](web/core/data_dict.py) 加分支:
```python
schema_name = cfg.user if cfg.db_type == "oracle" else cfg.database
columns = db.list_columns(schema_name)
tables = db.list_tables(schema_name)
```
**同时修了 list_columns**(两个 SQL 都有同样的 `WHERE OWNER = UPPER(:1)` 过滤)。
### 验证
重启 web 服务后日志:
```
[DB] 测试连接: oracle://GJJ80@192.168.20.196:1521/ZFGJJ
[DB] Oracle Instant Client 已加载(thick 模式)lib_dir=D:\Oracle\instantclient_...
[DB] 连接已建立(耗时 ~100ms)
[DB] 测试连接成功
[Cache] 写入数据字典: ('oracle', '192.168.20.196', 1521, 'ZFGJJ', 'GJJ80') (表 N 张, 字段 M 个) ← N>0, M>0
```
## 2026-08-13 · DPI-1047 诊断日志:自动扫描 oci.dll 的运行时依赖(MSVCR120.dll → VC++ 2013 Redist)
### 现象
按上一轮提示装了 [vc_redist.x64.exe](https://aka.ms/vs/17/release/vc_redist.x64.exe)(VC++ 2015-2022 Redistributable)并重启电脑,但 `_ensure_oracle_client()` **仍报 DPI-1047** → 回落到 thin → DPY-3010。
### 根因(关键发现)
**Instant Client 12.x 的 `oci.dll` 是用 MSVC 2013 编译的,依赖 `MSVCR120.dll`**,**不**依赖 `MSVCP140.dll` / `VCRUNTIME140.dll`(那是 MSVC 2015+)。装 VC++ 2015-2022 Redistributable **完全不解决**这个问题。
用 Python 解析 oci.dll 的 PE 文件导入表证实:
```
PE format: PE32+ (64-bit)
oci.dll 的依赖项(导入 DLL):
KERNEL32.dll [Windows System]
MSVCR120.dll [Windows System] ← 关键:MSVC 2013 C Runtime
ADVAPI32.dll [其他]
```
`MSVCR120.dll` 对应 **Visual C++ 2013 Redistributable (x64)** —— 文件名 `vcredist_x64.exe`,跟 2015-2022 的 `vc_redist.x64.exe` 名字相近但完全不同。
### MSVC 版本 vs MSVCR DLL 对照表
| MSVC 版本 | _MSC_VER | C Runtime DLL | 对应 Redist |
|---|---|---|---|
| MSVC 2013 | 1800 | `MSVCR120.dll` / `MSVCP120.dll` | **Visual C++ 2013 Redistributable** |
| MSVC 2015 | 1900 | `MSVCP140.dll` / `VCRUNTIME140.dll` | vc_redist 2015 |
| MSVC 2017 | 1910+ | 同上 | vc_redist 2017 |
| MSVC 2019/2022 | 1920+ | 同上 | vc_redist 2015-2022 |
**经验**:Oracle Instant Client 各版本的编译器版本不同(基本跟着 Oracle DB 主版本的发布时间):
- Instant Client 11.2 → MSVC 2005(`MSVCR80.dll`)
- Instant Client 12.1/12.2 → MSVC 2013(`MSVCR120.dll`) ← **用户现在这个**
- Instant Client 18/19 → MSVC 2017(`MSVCP140.dll`)
- Instant Client 19c → MSVC 2017/2019
- Instant Client 21c → MSVC 2019/2022
### 改动
**[web/core/db_adapter.py](web/core/db_adapter.py) `_ensure_oracle_client()` DPI-1047 诊断 —— 自动 PE 解析**
之前只笼统提示「装 VC++ Redistributable」。现在诊断逻辑:
1. 用户给了 lib_dir 且 oci.dll 存在 → 用 Python `struct` 解析 oci.dll 的 PE 导入表
2. 过滤掉 Windows 系统 DLL(KERNEL32 / ADVAPI32 / WS2_32 等)→ 列出**真正需要 Redist 的运行时 DLL**
3. 根据依赖 DLL 名给出对应的 Redist 下载链接:
- `MSVCR120.dll` → [Visual C++ 2013 Redistributable (x64)](https://www.microsoft.com/en-us/download/details.aspx?id=40784)
- `MSVCP140.dll` / `VCRUNTIME140.dll` → [vc_redist.x64.exe](https://aka.ms/vs/17/release/vc_redist.x64.exe)
4. 同时注明「Instant Client 12.x 是 MSVC 2013 编译的,装 vc_redist 2015-2022 不够」
PE 解析纯 Python `struct` 实现,不依赖 dumpbin / PowerShell / 任何外部工具。
```python
extra_hint = (
"\n 诊断:lib_dir 下 oci.dll 存在,但仍报 DPI-1047 → "
"最常见原因是缺 VC++ Redistributable"
+ missing_runtime_hint
)
```
### 烟测(用户当前场景)
```
[DB] 未找到 Oracle Instant Client(DPI-1047: ...)→ 使用 thin 模式
诊断:lib_dir 下 oci.dll 存在,但仍报 DPI-1047 → 最常见原因是缺 VC++ Redistributable
诊断:oci.dll 依赖的运行时 DLL = ['MSVCR120.dll']
(如果是 MSVCR120.dll → 装 Visual C++ 2013 Redistributable (x64):https://www.microsoft.com/en-us/download/details.aspx?id=40784)
(如果是 MSVCP140.dll / VCRUNTIME140.dll → 装 vc_redist.x64.exe:https://aka.ms/vs/17/release/vc_redist.x64.exe)
注意:Instant Client 12.x 是 MSVC 2013 编译的(_MSC_VER=1800),装 vc_redist 2015-2022 不够,必须装 2013!
```
### 用户当前问题的修复步骤
1. 下载安装 [Visual C++ 2013 Redistributable (x64)](https://www.microsoft.com/en-us/download/details.aspx?id=40784)(`vcredist_x64.exe`)
2. **重启电脑**(vcredist 要注册全局 DLL 到 `C:\Windows\System32\`,不重启 Python 进程仍可能找不到)
3. **重启 web 服务**(`_oracle_client_initialized` 进程级标志)
4. 浏览器刷新,再测连接 → 日志应出现 `[DB] Oracle Instant Client 已加载(thick 模式)`
### 踩坑 / 注意
- **vc_redist 2015-2022 ≠ vc_redist 2013**:名字相近但完全不同。`MSVCR120.dll` 在 2015-2022 里**不包含**(覆盖度是单向的:旧版包含新版 DLL 的子集,新版不补旧版)
- **PE 导入表解析的局限**:只看到 DLL 名,看不到具体函数 / 版本号。但 DLL 名足够反推 MSVC 版本 → Redist 版本
- **不依赖外部工具**:避免装 dumpbin(要 MSVC)/ PowerShell 反射 API(要管理员权限),纯 Python `struct` 跑得通
- **`is_thin_mode()` 在 thick 失败时仍返回 True**:oracledb 内部把 init_oracle_client 失败视为「继续走 thin」,所以从外部 API 没法直接区分 —— 只能依赖日志
### 后续
- 下次用户跑 Instant Client 19.x / 21c 时,依赖会变成 MSVCP140.dll → 自动指向 vc_redist.x64.exe,不需要再改代码
---
## 2026-08-13 · Oracle Client 路径外置到配置文件 + DPI-1047 加 VC++ Redist 提示
### 需求
> 用户原始反馈:「不要让用户填,把路径放在配置文件里面」
前一轮 Oracle 入口放开后,前端多了一个「Oracle Client」输入框让用户手动填 Instant Client 路径。但 Instant Client 路径是**运维层面的事**(要解压到固定路径),不该让普通用户在每次刷新页面时重填。
### 设计决策
1. **路径走配置文件 `web/configs/db_defaults.yaml`**:跟 `host / user / password / database` 同一套预填机制(运维改 YAML,重启 web 服务生效)
2. **配置文件 gitignore**:路径里如果带用户名 / 机器名也可能间接泄露环境信息,保留原 ignore 规约(这个文件本来就不入库)
3. **DPI-1047 + oci.dll 存在 → 提示装 VC++ Redistributable**:Windows 上 Instant Client 12.x+ 缺 vcredist 是 DPI-1047 的头号原因,加诊断提示免得用户再去翻文档
4. **前端「Oracle Client」输入框保留**:仍然显示且可编辑 —— YAML 没配时给空串兜底,用户临时切换路径也能用
### 改动
#### 1. [web/configs/db_defaults.yaml](web/configs/db_defaults.yaml) —— 新增 oracle_client_dir(不入库)
```yaml
db_defaults:
host: "192.168.20.196"
user: "GJJ80"
password: "GJJ80_Hg41RG"
database: "ZFGJJ"
# Oracle Instant Client 目录(仅 Oracle 有效,留空走 thin 模式)
oracle_client_dir: "D:\\Oracle\\instantclient-basic-windows.x64-12.2.0.1.0\\instantclient_12_2"
```
#### 2. [web/api/routes.py](web/api/routes.py) `/api/db-defaults` 端点 —— 透出 oracle_client_dir
```python
return {
k: defaults[k]
for k in ("host", "user", "password", "database", "oracle_client_dir") # ← 加 oracle_client_dir
if k in defaults
}
```
#### 3. [web/static/index.html](web/static/index.html) `loadDbDefaults()` —— 消费 oracle_client_dir
```js
// 2026-08-13:oracle_client_dir 也预填(仅 Oracle 有效,留空走 thin)
if (d.oracle_client_dir !== undefined) form.oracle_client_dir = d.oracle_client_dir;
```
#### 4. [web/core/db_adapter.py](web/core/db_adapter.py) `_ensure_oracle_client()` —— DPI-1047 加针对性诊断(L216-248)
之前的失败日志只打一行根因,没区分两种典型场景:
- **lib_dir 填错了**(找不到 oci.dll)
- **lib_dir 对了但 Windows 缺 VC++ Redist**(oci.dll 在但加载失败)
新增诊断:
```python
except Exception as e:
err_str = str(e)
if "DPI-1047" in err_str:
import os
oci_in_lib_dir = False
if lib_dir:
oci_in_lib_dir = os.path.exists(os.path.join(lib_dir, "oci.dll"))
extra_hint = ""
if lib_dir and oci_in_lib_dir:
# 场景:lib_dir 对、oci.dll 在、但 Windows 缺 vcredist
extra_hint = (
"\n 诊断:lib_dir 下 oci.dll 存在,但仍报 DPI-1047 → "
"最常见原因是缺 VC++ Redistributable\n"
" 修复:装 Microsoft Visual C++ 2015-2022 Redistributable (x64)\n"
" https://aka.ms/vs/17/release/vc_redist.x64.exe\n"
" (Instant Client 12.x+ 是 MSVC 2015+ 编译的,没装 vcredist 就加载失败)"
)
elif lib_dir:
# 场景:lib_dir 填错
extra_hint = (
f"\n 诊断:lib_dir={lib_dir} 下找不到 oci.dll → 路径不对\n"
" 修复:lib_dir 应该填到含 oci.dll 的那一级(不是上一级)"
)
logger.info(
f"[DB] 未找到 Oracle Instant Client(DPI-1047: {e})→ 使用 thin 模式{extra_hint}"
)
...
```
### 烟测
#### 端到端链路:YAML → API → 前端
```
=== /api/db-defaults 返回 ===
host: 192.168.20.196
user: GJJ80
password: ***
database: ZFGJJ
oracle_client_dir: D:\Oracle\instantclient-basic-windows.x64-12.2.0.1.0\instantclient_12_2
✓ oracle_client_dir 在返回里: True
✓ 前端 loadDbDefaults 包含 oracle_client_dir 字段赋值
✓ 端到端:YAML → API → 前端 链路打通
```
#### DPI-1047 诊断日志(用户当前场景:lib_dir 对、oci.dll 在、缺 vcredist)
```
[DB] 未找到 Oracle Instant Client(DPI-1047: DPI-1047: Cannot locate a 64-bit Oracle Client library: ...)→ 使用 thin 模式
诊断:lib_dir 下 oci.dll 存在,但仍报 DPI-1047 → 最常见原因是缺 VC++ Redistributable
修复:装 Microsoft Visual C++ 2015-2022 Redistributable (x64)
https://aka.ms/vs/17/release/vc_redist.x64.exe
(Instant Client 12.x+ 是 MSVC 2015+ 编译的,没装 vcredist 就加载失败)
```
### 用户当前问题的根因(推断)
1. 用户已解压 Instant Client 12.2.0.1 到 `D:\Oracle\instantclient-basic-windows.x64-12.2.0.1.0\instantclient_12_2\`
2. `oci.dll` 在该目录下 ✓
3. 但 `_ensure_oracle_client()` 仍报 DPI-1047 → **Windows 上没装 VC++ Redistributable**
4. 修复:装 [vc_redist.x64.exe](https://aka.ms/vs/17/release/vc_redist.x64.exe),重启电脑,再重启 web 服务
### 踩坑 / 注意
- **`db_defaults.yaml` 不入库**:跟之前一样,路径 / 凭证都不 commit;改完用户需要自己写到本地文件
- **`oracle_client_dir` 在 YAML 里用双反斜杠转义**:YAML 字符串 `"D:\\Oracle\\..."` 解析后是 `D:\Oracle\...`;前端表单接到的就是 `D:\Oracle\...` 单反斜杠(Windows 路径)
- **VC++ Redist 是 Windows-only 问题**:Linux / macOS 用 Instant Client 不需要 vcredist(DPI-1047 通常是 ld 找不到 libclntsh.so 的依赖)
- **`_oracle_client_initialized` 全局缓存**:lib_dir 改完后必须重启 web 服务才生效;前端改了 oracle_client_dir 输入框也是同理(值会传给后端,但模块级缓存不动)
- **Oracle Client 输入框仍保留**:用户临时切换 Instant Client 路径(比如测试不同版本)可以前端手填;YAML 是默认值兜底
---
## 2026-08-13 · 修复 Oracle DPY-3010(thin 模式不支持老 Oracle):日志加针对性安装指引
### 现象
用户实测 Oracle(192.168.20.196:1521/ZFGJJ,GJJ80)第二轮:
```
[DB] 未找到 Oracle Instant Client(DatabaseError: DPI-1047: Cannot locate a 64-bit Oracle Client library ...)→ 使用 thin 模式
[ERROR] [DB] 连接失败: NotSupportedError: DPY-3010: connections to this database server version are not supported by python-oracledb in thin mode
[ERROR] [DB] Oracle 诊断: oracledb.__version__=1.4.2, connect() 合法 kwarg=[...]
[ERROR] [DB] 测试连接失败: NotSupportedError: DPY-3010
```
`tcp_connect_timeout` 修复已生效(不再有 TypeError),但**这台 Oracle 服务端版本低于 11.2**(公积金老库常见 10g / 9i),thin 模式不支持。
### 根因
- `oracledb 1.4.x / 2.x thin 模式只支持 Oracle 11.2+** 服务端版本
- 老版本 Oracle(10g / 9i 等)必须走 **thick 模式**(依赖 Oracle Instant Client 的 OCI 原生库)
- 当前用户机器**未装 Instant Client**:`DPI-1047: Cannot locate a 64-bit Oracle Client library`
- `_ensure_oracle_client()` 自动搜索 PATH / 注册表 / ORACLE_HOME 全部失败 → 回落到 thin → DPY-3010
之前日志只打一行根因 + 上一轮加的 inspect 信息,**对「Instant Client 缺失」这种典型场景没给针对性指引**,用户不知道下一步该干嘛。
### 改动
**[web/core/db_adapter.py](web/core/db_adapter.py) `__enter__` Oracle except 分支 —— 错误类型分发(L340-378)**
识别 2 类典型错误,分别给针对性指引;其他错误仍走上一轮的 inspect 兜底:
```python
except Exception as e:
logger.error(f"[DB] 连接失败: {type(e).__name__}: {e}")
if self._driver == "oracledb":
err_str = str(e)
# ── 典型错误 1:thin 模式不支持老 Oracle(11.2 以下)──
if "DPY-3010" in err_str:
logger.error(
"[DB] Oracle 诊断: 当前 Oracle 服务端版本低于 11.2(或 thin 模式不支持)\n"
" 修复步骤:\n"
" 1. 下载 Instant Client Basic 包(约 100MB):\n"
" https://www.oracle.com/database/technologies/instant-client/downloads.html\n"
" 2. 解压到本地(Windows 示例)D:\\oracle\\instantclient_19_8\n"
" 3. 前端「Oracle Client」输入框填该路径,重启 web 服务后再测\n"
" (或加到 PATH / 设置 ORACLE_HOME 让 _ensure_oracle_client() 自动找到)"
)
# ── 典型错误 2:Instant Client 缺失(DPI-1047)──
elif "DPI-1047" in err_str:
logger.error(
"[DB] Oracle 诊断: Instant Client 缺失(DPI-1047)\n"
" 修复:同 DPY-3010 的步骤 1~3(下载解压并填到前端 Oracle Client 输入框)"
)
# ── 其他错误:照旧打 inspect 信息 ──
else:
try:
import inspect
import oracledb as _odb
valid_kwargs = list(inspect.signature(_odb.connect).parameters.keys())
logger.error(
f"[DB] Oracle 诊断: oracledb.__version__={_odb.__version__}, "
f"connect() 合法 kwarg={valid_kwargs}"
)
logger.error(f"[DB] Oracle 诊断: 实际传入 kwarg={self.cfg.to_oracle_kwargs()}")
except Exception as diag_e:
logger.error(f"[DB] Oracle 诊断打印失败: {diag_e}")
raise
```
### 不动的部分
- `to_oracle_kwargs()`:参数名已经是 `tcp_connect_timeout`(上一轮已修)
- `_ensure_oracle_client(lib_dir)`:自动检测 Instant Client 的逻辑不变
- 前端:「Oracle Client」输入框(仅在 db_type === 'oracle' 时显示)已经是用户填 Instant Client 路径的入口
### 烟测(mock 抛错验证日志输出)
#### 测试 1:DPY-3010
```
ERROR [DB] 连接失败: NotSupportedError: DPY-3010: connections to this database server version are not supported
ERROR [DB] Oracle 诊断: 当前 Oracle 服务端版本低于 11.2(或 thin 模式不支持)
修复步骤:
1. 下载 Instant Client Basic 包(约 100MB):
https://www.oracle.com/database/technologies/instant-client/downloads.html
2. 解压到本地(Windows 示例)D:\oracle\instantclient_19_8
3. 前端「Oracle Client」输入框填该路径,重启 web 服务后再测
(或加到 PATH / 设置 ORACLE_HOME 让 _ensure_oracle_client() 自动找到)
```
#### 测试 2:TypeError(kwarg 错误,fallback 路径)
```
ERROR [DB] 连接失败: TypeError: connect() got an unexpected keyword argument 'foo'
ERROR [DB] Oracle 诊断: oracledb.__version__=1.4.2, connect() 合法 kwarg=['kw']
ERROR [DB] Oracle 诊断: 实际传入 kwarg={...}
```
→ 两类错误都被准确捕获 + 针对性日志输出。
### 用户后续操作(DPY-3010 场景)
1. 浏览器打开 [https://www.oracle.com/database/technologies/instant-client/downloads.html](https://www.oracle.com/database/technologies/instant-client/downloads.html)
2. 下载 Instant Client Basic 包(Windows x64,约 100MB)—— 版本尽量和老 Oracle 服务端版本对齐(如 11g 服务端用 11.2 instant client;19c 用 19.x;不严格要求)
3. 解压到本地路径,如 `D:\oracle\instantclient_19_8`
4. 在前端「连接数据库」卡的「Oracle Client」输入框填这个路径
5. 重启 web 服务(`_oracle_client_initialized` 是进程级标志,重启才生效)
6. 再点「测试连接」→ 日志应出现「[DB] Oracle Instant Client 已加载(thick 模式)」→ 连接成功
### 踩坑 / 注意
- **`_oracle_client_initialized` 是模块级全局变量**:改 `oracle_client_dir` 后必须重启 web 服务才生效(`uvicorn.run(reload=False)`)—— 不重启的话 `init_oracle_client()` 第二次调用是 no-op
- **`oracledb.exceptions` 是子模块**:测试 stub 直接用 `oracledb.NotSupportedError`(顶层 import),不要 `oracledb.exceptions.NotSupportedError` —— 后者需要先 `import oracledb.exceptions` 才存在
- **错误识别靠字符串匹配 `"DPY-3010" in err_str`**:oracledb 各版本异常 message 格式可能微调,但 DPY-3010 / DPI-1047 这两个代码号稳定
- **不需要改 thick 模式的 kwargs**:`_ensure_oracle_client()` 切到 thick 后,`oracledb.connect()` 接收的 kwarg 一样(都是 `host/port/service_name/user/password/tcp_connect_timeout`),thin/thick 模式共用一套入参
- **如果用户 Instant Client 版本太新**:Oracle 服务端 < 12c 时可能要求 Instant Client ≥ 服务端版本。常见组合:服务端 10g → Instant Client 11.2;服务端 11g → Instant Client 11.2/12.x;服务端 19c → Instant Client 19.x
---
## 2026-08-13 · 修复 Oracle 连接 TypeError:connect_timeout → tcp_connect_timeout + 诊断日志
### 现象
用户实测 Oracle 连接(192.168.20.196:1521/ZFGJJ,GJJ80)报:
```
[ERROR] [DB] 连接失败: TypeError: connect() got an unexpected keyword argument 'connect_timeout'
[WARNING] 连接测试失败: 连接失败: connect() got an unexpected keyword argument 'connect_timeout'
```
### 根因
`web/core/db_adapter.py` `to_oracle_kwargs()` 之前用 `connect_timeout`,**当前装的 oracledb 不认这个 kwarg**。本地探针确认:
```bash
$ python -c "import oracledb, inspect; print(oracledb.__version__); print(list(inspect.signature(oracledb.connect).parameters))"
1.4.2
['dsn', 'pool', 'conn_class', 'params', 'user', 'proxy_user', 'password', 'newpassword', 'wallet_password', 'access_token', 'host', 'port', 'protocol', 'https_proxy', 'https_proxy_port', 'service_name', 'sid', 'server_type', 'cclass', 'purity', 'expire_time', 'retry_count', 'retry_delay', 'tcp_connect_timeout', 'ssl_server_dn_match', ...]
```
合法 kwarg 里**只有 `tcp_connect_timeout`**,根本没有 `connect_timeout`。
### 重要:修正之前 WORKLOG 里的错误历史记录
- 之前 [2026-08-11 fix(backend): Oracle thin 模式 DPY-3010 —— 自动切 thick 模式](#) 那条说 `connect_timeout` 是 1.x 风格、2.x 改名 `tcp_connect_timeout` —— **错的**
- 又 [2026-08-11 fix(backend): Oracle 11.2 DPY-3010 —— 锁定 oracledb 1.4.x](#) 那条把 `tcp_connect_timeout` 改回 `connect_timeout`,**也是错的**(当时以为 1.4.x 用老名字)
- **事实**:1.4.2 早就在用 `tcp_connect_timeout` —— `inspect.signature(oracledb.connect)` 实测没有 `connect_timeout`
### 改动
#### 1. `web/core/db_adapter.py` `to_oracle_kwargs()` —— 修复参数名(L82-98)
```python
# 旧
"connect_timeout": float(self.connect_timeout),
# 新
"tcp_connect_timeout": float(self.connect_timeout),
```
docstring 也同步更新:
- 删掉错误的「oracledb 1.4.x 风格」描述
- 改成「oracledb 1.4.x 起就用这个名;旧 1.x 风格的 connect_timeout 在 1.4.2+ 已被移除」
#### 2. `web/core/db_adapter.py` `__enter__` Oracle 分支 —— 失败时打诊断(L339-356)
原 except 只打一行根因;用户下次撞到「kwarg 找不到」「ORA-xxx」等问题时无法快速定位。增强:
```python
except Exception as e:
logger.error(f"[DB] 连接失败: {type(e).__name__}: {e}")
if self._driver == "oracledb":
try:
import inspect
import oracledb as _odb
valid_kwargs = list(inspect.signature(_odb.connect).parameters.keys())
logger.error(
f"[DB] Oracle 诊断: oracledb.__version__={_odb.__version__}, "
f"connect() 合法 kwarg={valid_kwargs}"
)
logger.error(f"[DB] Oracle 诊断: 实际传入 kwarg={self.cfg.to_oracle_kwargs()}")
except Exception as diag_e:
logger.error(f"[DB] Oracle 诊断打印失败: {diag_e}")
raise
```
`inspect.signature(oracledb.connect).parameters` 是唯一权威的合法 kwarg 来源 —— 比手记硬编码靠谱,任何 oracledb 版本变化都能自动反映。
### 烟测
#### 后端 kwargs 修复验证
```python
cfg = DBConfig(db_type='oracle', host='192.168.20.196', port=1521, user='GJJ80', password='x', database='ZFGJJ', connect_timeout=10)
kw = cfg.to_oracle_kwargs()
# {'user': 'GJJ80', 'password': 'x', 'host': '192.168.20.196', 'port': 1521, 'service_name': 'ZFGJJ', 'tcp_connect_timeout': 10.0}
assert 'tcp_connect_timeout' in kw
assert 'connect_timeout' not in kw
# ✓
# 对照 inspect.signature(oracledb.connect) 验全部合法
import inspect, oracledb
valid = set(inspect.signature(oracledb.connect).parameters.keys())
for k in kw: assert k in valid
# ✓ 所有 kwarg 都合法
```
#### 失败日志输出验证(本地连 127.0.0.1:1 模拟连接失败)
```
ERROR [DB] 连接失败: OperationalError: DPY-6005: cannot connect to database
ERROR [DB] Oracle 诊断: oracledb.__version__=1.4.2, connect() 合法 kwarg=['dsn', ..., 'tcp_connect_timeout', ...]
ERROR [DB] Oracle 诊断: 实际传入 kwarg={'user': 'x', 'password': 'x', 'host': '127.0.0.1', 'port': 1, 'service_name': 'X', 'tcp_connect_timeout': 1.0}
```
→ 用户下次撞到 Oracle 连接问题,日志直接给出版本 + 合法 kwarg 全集 + 实际入参对比 —— 不再需要 grep 源码。
### 影响
- 真实 Oracle(ZFGJJ / 任何 11.2+ 版本):连接应该能直接通过 `tcp_connect_timeout` 走通
- 老版本 Oracle(< 11.2):仍需装 Instant Client(填到 `Oracle Client` 输入框),行为不变
- requirements.txt 锁的 `oracledb>=1.4.2,<2.0` 不变(1.4.x 早就在用 `tcp_connect_timeout`)
### 踩坑 / 注意
- **之前 WORKLOG 的 connect_timeout 历史记录是错的**:1.4.2 没用过 `connect_timeout` 这个名。本条以 `inspect.signature(oracledb.connect).parameters` 实测为准
- **`inspect.signature` 是真相**:oracledb 各版本 kwarg 命名差异大(库作者改过名),不要凭印象写代码 —— 调试时直接 inspect 拿真实签名
- **`to_oracle_kwargs()` 没走 try/except**:之前参数错误被 `__enter__` 的 try/except 兜住,但实际入参在日志里看不到。加诊断日志后,问题能定位到「`to_oracle_kwargs()` 输出的 kwarg 列表 vs 当前 oracledb 接受列表」diff
---
## 2026-08-13 · 放开 Oracle 数据库连接(前端入口 + thin/thick 模式支持)
### 需求
> 用户原始反馈:「放开 Oracle 的连接」
之前 Oracle 的代码路径(db_adapter / SQL 模板 / `oracledb>=1.4.2,<2.0` / `_ensure_oracle_client()` / `DBConfig.oracle_client_dir` / `ConnectRequest.oracle_client_dir`)**全部已就位**,但前端 radio 被注释隐藏,用户根本选不到 —— 体验上看是「工具不支持 Oracle」。
### 设计决策
1. **Oracle radio 取消注释**:三选一(MySQL / 达梦 / Oracle)正式开放
2. **新增 `oracle_client_dir` 输入框**:仅在 db_type === 'oracle' 时显示(v-if 条件渲染)
- 空值 → 走 thin 模式(无需 Instant Client)
- 填路径 → 走 thick 模式(需 Instant Client,11.2 以下版本必需;走 `_ensure_oracle_client(lib_dir)` 自动 init)
3. **`database` 字段填 service_name**(如 `ORCL` / `ORCLPDB1` / `ZFGJJ`),不是 SID —— `onDbTypeChange` 切到 Oracle 时自动填默认 `ORCL`
4. **`port` 默认 1521**:`DB_DEFAULT_PORTS` 早已包含 oracle:1521(2026-08-11 起就位)
### 改动
**[web/static/index.html](web/static/index.html)** — 3 处微改
**1) radio 取消注释(L295-299)**
```html
<el-radio-group v-model="form.db_type" @change="onDbTypeChange">
<el-radio-button label="mysql">MySQL</el-radio-button>
<el-radio-button label="dameng">达梦 (DM)</el-radio-button>
<el-radio-button label="oracle">Oracle</el-radio-button> <!-- 取消注释 -->
</el-radio-group>
```
**2) 新增 Oracle Client 条件行(L343-350,charset 行后)**
```html
<el-row v-if="form.db_type === 'oracle'" :gutter="20">
<el-col :span="24">
<el-form-item label="Oracle Client">
<el-input v-model="form.oracle_client_dir"
placeholder="Oracle Instant Client 目录(留空走 thin 模式,例如 D:\oracle\instantclient_19_8)" />
</el-form-item>
</el-col>
</el-row>
```
**3) form reactive 新增 oracle_client_dir 字段(L1111-1113)**
```js
// Oracle Instant Client 目录(仅 Oracle 用;空字符串走 thin 模式)。
// 后端 ConnectRequest.oracle_client_dir 透传到 db_adapter._ensure_oracle_client()。
oracle_client_dir: '',
```
### 不动的部分(已就位)
- `web/core/db_adapter.py`:`to_oracle_kwargs()` 用 1.x 风格的 `connect_timeout`(锁 `oracledb>=1.4.2,<2.0` 兼容 11.2)
- `web/core/db_adapter.py::_ensure_oracle_client(lib_dir=None)`:自动检测 PATH / 注册表 / ORACLE_HOME,进程级幂等
- `web/sql/info_schema/list_*.oracle.sql`:Oracle 字典表 SQL 模板(LEFT JOIN `ALL_TAB_COMMENTS` / `ALL_COL_COMMENTS`)
- `web/core/models.py`:`ConnectRequest.oracle_client_dir` + `TestConnectionRequest.oracle_client_dir` 字段(之前已加)
- `web/api/routes.py`:`/api/connect/test` 端点把 `oracle_client_dir` 透传给 `DBConfig`
- `DB_DEFAULT_PORTS`:`oracle: 1521`
- `onDbTypeChange`:oracle 分支填默认 `ORCL`
- `web/configs/db_defaults.yaml`:Oracle service_name 注释(用户可填默认值)
- 字符集 al32utf8 / zhs16gbk:之前已加
### 烟测
```bash
$ PYTHONIOENCODING=utf-8 PYTHONPATH=. python -c "
from web.core.db_adapter import DBConfig, _ensure_oracle_client
from web.core.models import ConnectRequest, TestConnectionRequest
cfg = DBConfig(db_type='oracle', host='127.0.0.1', port=1521, user='u', password='', database='ORCL', oracle_client_dir='D:\\\\oracle\\\\instantclient_19_8')
print('DBConfig OK:', cfg.db_type, cfg.oracle_client_dir)
req = ConnectRequest(db_type='oracle', host='127.0.0.1', port=1521, user='u', password='', database='ORCL', tables=['t1'], oracle_client_dir='/opt/oci')
print('ConnectRequest OK:', req.oracle_client_dir)
print('to_oracle_kwargs:', cfg.to_oracle_kwargs())
print('mode:', _ensure_oracle_client(None))
"
DBConfig OK: oracle oracle_client_dir= D:\oracle\instantclient_19_8
ConnectRequest OK: /opt/oci
to_oracle_kwargs: {'user': 'u', 'password': '', 'host': '127.0.0.1', 'port': 1521, 'service_name': 'ORCL', 'connect_timeout': 10.0}
mode: thin # 没装 Instant Client → 降级到 thin,不抛
```
→ 后端 4 项关键路径全部 OK:
1. `DBConfig.oracle_client_dir` 字段生效
2. `ConnectRequest.oracle_client_dir` Pydantic 接受
3. `to_oracle_kwargs()` 输出 `service_name: 'ORCL'`(与前端自动填默认一致)
4. `_ensure_oracle_client(None)` 返回 `'thin'` —— 没装 Instant Client 时不报错
### 后续
- 用户自测路径:
1. 浏览器刷新 → 「连接数据库」卡应有 3 个 radio:MySQL / 达梦 / Oracle
2. 选 Oracle → 自动填 `port=1521, database=ORCL`;额外出现「Oracle Client」输入框
3. 留空 → thin 模式(oracledb 直接走,无 Instant Client 依赖)
4. 填路径(如 `D:\oracle\instantclient_19_8`)→ thick 模式(适合 11.2 以下老版本)
5. `database` 字段改填实际 service_name(如 `ZFGJJ` / `ORCLPDB1`)
6. 「测试连接」→ 后端 `_ensure_oracle_client()` 自动尝试加载 Instant Client → 连得上 OK,连不上提示 DPY-3010 + 下载链接
### 踩坑 / 注意
- **后端代码 100% 已就位**:本次只是前端入口放开。如果用户实测报 `DPY-3010`(11.2 thin 模式不支持),按 2026-08-11 那条文档下载 Oracle Instant Client 后填入 `Oracle Client` 输入框即可
- **`database` 字段语义是 service_name**:不是 SID!前端 placeholder 只显示 `smart-build`(MySQL 时代默认值),需要用户自己改成 service_name;`onDbTypeChange` 只在空值时填 `ORCL`,不覆盖用户填好的值
- **字符集 oracle 常用 `al32utf8` / `zhs16gbk`**:之前已加进 el-select,但 placeholder 还是 utf8mb4 —— 用户要手动切
- **`db_defaults.yaml` Oracle service_name 默认值暂未配**:如果用户希望「连接 Oracle 时自动填 service_name」,可在 `db_defaults.yaml` 里给 `database` 加一个 oracle 专属字段;本次不做
---
## 2026-08-13 · 「单位 / 法人 / 经办人 字段规范」整组下架 ## 2026-08-13 · 「单位 / 法人 / 经办人 字段规范」整组下架
### 需求 ### 需求
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment