Skip to content

Commit 2baf5e2

Browse files
committed
release: v2.5.31 clob unicode fix and integration stability suite
1 parent 8627f6e commit 2baf5e2

101 files changed

Lines changed: 34117 additions & 16 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,20 @@ All notable changes to this project are documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/),
66
and this project adheres to [Semantic Versioning](https://semver.org/).
77

8+
## [2.5.31] - 2026-03-03
9+
10+
### Fixed
11+
12+
- 修复了大 CLOB 在特定 Unicode 模式(如 `稳态中文🚀X`)回读错位的问题,消除了 `U+FFFD` 替换字符导致的内容损坏。
13+
- 修复了 CLOB 分块写入在 UTF-8 边界处切分导致的潜在字符错位问题。
14+
15+
### Added
16+
17+
- 新增 P0 CLOB Unicode 回归测试集,覆盖高风险模式、`executemany`、长度契约与子进程防崩溃场景。
18+
- 新增并扩展 P0/P1/P2 集成测试分层与压力场景,提升稳定性回归覆盖。
19+
- 新增 DM 集成测试 CI 工作流与覆盖率产物上传。
20+
-`dpi_bridge` 引入补丁化本地 DM Go 驱动依赖,并记录补丁说明文档。
21+
822
## [2.5.30] - 2025-09-03
923

1024
### Fixed

Cursor.c

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2417,6 +2417,7 @@ Cursor_ExecuteMany(
24172417
{
24182418
PyObject* statement;
24192419
PyObject* argsList;
2420+
PyObject* argsIter = NULL;
24202421
PyObject* rowParams;
24212422
PyObject* retObj = NULL;
24222423

@@ -2427,16 +2428,17 @@ Cursor_ExecuteMany(
24272428

24282429
DMPYTHON_TRACE_INFO(dpy_trace(statement, argsList, "ENTER Cursor_ExecuteMany, after parse args\n"));
24292430

2430-
if (PyIter_Check(argsList))
2431+
argsIter = PyObject_GetIter(argsList);
2432+
if (argsIter != NULL)
24312433
{
24322434
Py_INCREF(Py_None);
24332435
retObj = Py_None;
24342436

2435-
while(1)
2436-
{
2437-
rowParams = PyIter_Next(argsList);
2438-
if (rowParams == NULL)
2439-
break;
2437+
while(1)
2438+
{
2439+
rowParams = PyIter_Next(argsIter);
2440+
if (rowParams == NULL)
2441+
break;
24402442

24412443
Py_XDECREF(retObj);
24422444
retObj = Cursor_Execute_inner(self, statement, rowParams, 0, 0, 0);
@@ -2449,12 +2451,23 @@ Cursor_ExecuteMany(
24492451
return NULL;
24502452
}
24512453

2452-
Py_DECREF(rowParams);
2453-
}
2454-
2455-
return retObj;
2454+
Py_DECREF(rowParams);
2455+
}
2456+
2457+
if (PyErr_Occurred())
2458+
{
2459+
Py_XDECREF(retObj);
2460+
Py_DECREF(argsIter);
2461+
return NULL;
2462+
}
2463+
2464+
Py_DECREF(argsIter);
2465+
2466+
return retObj;
24562467
}
2457-
2468+
2469+
PyErr_Clear();
2470+
24582471
retObj = Cursor_Execute_inner(self, statement, argsList, 1, 0, 0);
24592472

24602473
DMPYTHON_TRACE_INFO(dpy_trace(statement, argsList, "ENTER Cursor_ExecuteMany, Cursor_Execute_inner Per Row, %s\n", retObj == NULL ? "FAILED" : "SUCCESS"));
@@ -3813,4 +3826,4 @@ PyTypeObject g_CursorType = {
38133826
0, // tp_free
38143827
0, // tp_is_gc
38153828
0 // tp_bases
3816-
};
3829+
};

TECHNICAL_REPORT.md

Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
# 让达梦数据库跑在 Mac 上:dmPython-macOS 的诞生与开源实践
2+
3+
> 一个用 Go 桥接层替代专有 C 库、让达梦数据库 Python 驱动原生运行在 macOS ARM64 上的技术故事。
4+
5+
## 1. 背景:一个被遗忘的平台
6+
7+
### 达梦数据库与 dmPython
8+
9+
达梦数据库(DM8)是国产关系型数据库的代表产品,广泛应用于政企领域。[dmPython](https://github.com/DamengDB/dmPython) 是其官方 Python 驱动,遵循 Python DB-API 2.0 规范,底层是约 19000 行 C 代码的扩展模块,通过 DPI(Dameng Programming Interface)库与数据库通信。
10+
11+
### 问题在哪?
12+
13+
dmPython 的 C 代码通过动态链接调用 `libdmdpi.so`(Linux)或 `dmdpi.dll`(Windows)——这是达梦官方提供的**闭源专有库**
14+
15+
**macOS 没有对应的 `libdmdpi.dylib`**
16+
17+
这意味着:
18+
- macOS 上无法编译 dmPython
19+
- macOS 上无法 `pip install` 任何 dmPython wheel
20+
- 使用 Mac 做开发的工程师,如果项目用了达梦数据库,只能在 Linux 虚拟机或远程服务器上调试
21+
22+
对于日益增长的 macOS(尤其是 Apple Silicon)开发者群体来说,这是一个实际的痛点。
23+
24+
## 2. 解题思路:把“缺库问题”拆成“兼容层工程”
25+
26+
### 先定义约束,再选方案
27+
28+
这个项目不是简单“让代码跑起来”,而是一个多约束优化问题。核心约束有四条:
29+
30+
1. **兼容性约束**:上层 `dmPython` C 扩展(约 19K 行)尽量不改,避免 fork 长期分叉。
31+
2. **合规约束**:达梦 DPI 头文件属于专有资产,仓库不能直接公开分发。
32+
3. **交付约束**:目标是最终用户一条 `pip install` 即可安装,而不是“先装一堆前置环境”。
33+
4. **平台约束**:必须原生支持 macOS ARM64,不依赖 Linux 虚拟机或 Rosetta 绕行方案。
34+
35+
基于这些约束,架构上可选路径其实不多:
36+
37+
- 路线 A:重写 dmPython C 扩展。技术可行,但维护成本最高,与上游同步最困难。
38+
- 路线 B:直接在 Python 层改驱动协议栈。改动面过大,风险从 ABI 层转移到行为层。
39+
- 路线 C:保留 C 扩展,替换其依赖的 `libdmdpi`。改动边界最清晰,最符合“最小侵入”原则。
40+
41+
最终选择路线 C:把问题收敛为“实现一个与 DPI ABI 兼容的 `libdmdpi.dylib`”。
42+
43+
### 方案核心:以 DPI ABI 为边界的 Go 桥接层
44+
45+
达梦 Go 驱动 [dm](https://gitee.com/chunanyong/dm) 是纯 Go 网络协议实现,天然跨平台。项目将其作为底层能力,通过 Go `-buildmode=c-shared` 暴露 C 符号,向上伪装成 `libdmdpi.dylib`
46+
47+
```
48+
Python App
49+
50+
dmPython (官方 C Extension,零改动复用)
51+
↓ 调用 DPI C API
52+
libdmdpi.dylib (Go bridge, c-shared)
53+
↓ 调用 Go dm driver
54+
DM wire protocol (TCP)
55+
56+
DM8 Server
57+
```
58+
59+
这种分层的关键价值是“把变化锁在桥接层”:上游 C 代码保持稳定,平台适配与协议实现由 Go 层承担。
60+
61+
### 关键实现机制(项目内真实落地)
62+
63+
`dpi_bridge/` 按功能拆分为连接、语句、取数、绑定、诊断、LOB、元数据等模块,核心机制如下:
64+
65+
- **句柄模型对齐**`handle.go` 维护 `uintptr -> Go 对象` 的句柄池,对外表现为 C 可识别的 `void*` 句柄,保证 dmPython 原有句柄生命周期可复用。
66+
- **语句执行语义兼容**`dpi_stmt.go` 负责 `prepare/exec/attr`,并通过缓存结果行的方式支持 `dpi_row_count` 等依赖“已知行数”的调用路径。
67+
- **类型与内存布局转换**`dpi_fetch.go` 把 Go 值(如 `string``time.Time`、数值)写入 DPI 约定的 C 结构体(如 `dpi_timestamp_t``dpi_numeric_t`),同时维护 `indPtr/actLenPtr` 等长度与空值信息。
68+
- **错误诊断回传**`dpi_diag.go` 将 Go 侧错误统一映射为 DPI 诊断信息,保证上层仍通过 `dpi_get_diag_rec` 等标准接口拿到错误详情。
69+
70+
### 兼容边界与工程取舍
71+
72+
该桥接层优先覆盖 dmPython 主路径(连接、SQL 执行、结果读取、事务、常见元数据与 LOB);对象/BFILE 等复杂特性当前以“显式返回未支持错误”为策略,而不是静默行为偏差。
73+
这种取舍让系统在“可用性优先”与“行为可解释性”之间取得平衡,也为后续增量补齐能力留下明确路线。
74+
75+
## 3. CI/CD 实现:从可构建到可发布的自动化链路
76+
77+
### 触发策略与发布闸门
78+
79+
`.github/workflows/build-wheels.yml` 采用单文件双阶段设计:
80+
81+
- `pull_request``main`:执行完整构建与校验,但不发布。
82+
- `push tags: v*`:先构建,再进入 release 阶段发布。
83+
- `workflow_dispatch`:支持人工重跑与应急发布。
84+
85+
发布闸门由两个条件共同控制:`release` job 依赖 `build` 成功(`needs: build`),且仅在 tag 引用下触发(`if: startsWith(github.ref, 'refs/tags/v')`)。
86+
87+
### Build 阶段:矩阵并行产出 wheel
88+
89+
`build` job 在 `macos-14`(ARM64 runner)上执行,Python 版本矩阵为 `3.9~3.13`,每个版本独立产出 wheel。关键步骤如下:
90+
91+
1. `actions/checkout` 拉取源码。
92+
2. `setup-go@v5` 安装 Go 1.21,并通过 `cache-dependency-path: dpi_bridge/go.sum` 命中子目录依赖缓存。
93+
3. `setup-python@v5` 安装矩阵 Python。
94+
4.`DPI_HEADERS_TAR_B64` secret 解码专有头文件到 `dpi_include/`
95+
5. 编译 Go 桥接库:`go build -buildmode=c-shared -o libdmdpi.dylib`,并通过 `install_name_tool -id @rpath/libdmdpi.dylib` 修正动态库标识。
96+
6. 构建 wheel:设置 `MACOSX_DEPLOYMENT_TARGET=14.0``_PYTHON_HOST_PLATFORM=macosx-14.0-arm64`,再执行 `DMPYTHON_SKIP_GO_BUILD=1 python -m build --wheel`(避免重复编译 Go)。
97+
7. `delocate-wheel``libdmdpi.dylib` 内嵌进 wheel,形成可分发产物。
98+
8. 在临时虚拟环境安装 wheel 并执行 `import dmPython` 作为最小可用性验证。
99+
9. 通过 `actions/upload-artifact` 上传每个 Python 版本的 wheel。
100+
101+
### Release 阶段:聚合产物并发布
102+
103+
`release` job 下载前序矩阵产物(`pattern: wheel-*`, `merge-multiple: true`),随后调用:
104+
105+
```bash
106+
gh release create "$TAG_NAME" --generate-notes dist_fixed/*.whl
107+
```
108+
109+
实现“一次 tag -> 自动生成 GitHub Release + 附带全部 wheel”。
110+
111+
### CI 里踩过的坑与固定方案
112+
113+
| 问题 | 原因 | 固化方案 |
114+
|------|------|----------|
115+
| 专有头文件不能入库 | 合规要求 | Secret(Base64 压缩包)注入,流水线临时解码 |
116+
| wheel 标签不稳定 | 默认平台推断可能混入非目标架构 | 显式设置 `MACOSX_DEPLOYMENT_TARGET``_PYTHON_HOST_PLATFORM` |
117+
| Go 缓存未命中 | `go.sum` 位于子目录 | 指定 `cache-dependency-path: dpi_bridge/go.sum` |
118+
| release 阶段找不到仓库上下文 | `gh release` 需要 git 元数据 | release job 重新 `checkout` |
119+
120+
### 产物形态与当前覆盖边界
121+
122+
tag 发布后会生成 5 个 ARM64 wheel(对应 Python 3.9~3.13),命名形态如下:
123+
124+
```
125+
dmPython_macOS-2.5.30-cp39-cp39-macosx_14_0_arm64.whl
126+
dmPython_macOS-2.5.30-cp310-cp310-macosx_14_0_arm64.whl
127+
dmPython_macOS-2.5.30-cp311-cp311-macosx_14_0_arm64.whl
128+
dmPython_macOS-2.5.30-cp312-cp312-macosx_14_0_arm64.whl
129+
dmPython_macOS-2.5.30-cp313-cp313-macosx_14_0_arm64.whl
130+
```
131+
132+
当前 CI 的验证粒度是“构建成功 + 可安装 + 可导入”。它能有效拦截打包与链接问题,但还未覆盖真实数据库集成测试;这也是下一阶段最值得补强的质量门禁。
133+
134+
## 4. 开源项目规范化
135+
136+
将项目从"能用"提升到"规范的开源项目",一次性完成了以下工作:
137+
138+
### 4.1 元数据补全
139+
140+
- **GitHub 仓库**:设置 description、homepage、topics(dameng, dm8, database, python, db-api, macos, driver)
141+
- **pyproject.toml**:添加 authors、project.urls、Python 3.9-3.13 classifiers
142+
- **setup.py**:同步添加 author、url、project_urls
143+
144+
### 4.2 标准开源文件
145+
146+
| 文件 | 操作 | 说明 |
147+
|------|------|------|
148+
| `LICENSE` | 修复 | 拼写错误(KIDN→KIND)、更新年份(2017-2026)、去掉方括号 |
149+
| `README.md` | 重写 | 纯英文 + CI/License/Python/Platform badges |
150+
| `README_zh.md` | 新建 | 中文文档迁移,许可证从错误的 "PSF License" 改为 "Mulan PSL v2" |
151+
| `CHANGELOG.md` | 重命名+格式化 | `ChangeLogs.md``CHANGELOG.md`,转为 [Keep a Changelog](https://keepachangelog.com/) 格式 |
152+
| `CONTRIBUTING.md` | 新建 | 开发环境、构建命令、PR 流程、代码风格 |
153+
| `.gitignore` | 补充 | `.DS_Store``*.pyc``.pytest_cache/``dist_ci/` |
154+
155+
### 4.3 仓库卫生
156+
157+
- 清理已合并的 `improve-readme` 分支(本地 + 远程)
158+
- 检查版本历史:无专有头文件、二进制文件或秘钥泄漏
159+
- 确认 upstream remote 配置正确,便于未来同步上游更新
160+
161+
## 5. 技术亮点与经验总结
162+
163+
### 5.1 Go 作为"万能胶水"的价值
164+
165+
Go 语言在这个项目中展现了独特价值:
166+
167+
- **纯 Go 网络协议实现**:达梦 Go 驱动不依赖 C 库,天然跨平台
168+
- **`c-shared` 编译模式**:Go 代码可以编译为 C 兼容的动态库,暴露标准 C 函数符号
169+
- **CGo 双向互操作**:Go 函数可以接收 C 指针参数,也可以回写 C 结构体内存
170+
171+
这使得"用 Go 重写一个 C 库的实现"成为可行且高效的方案。
172+
173+
### 5.2 "不改上游代码"的 Fork 策略
174+
175+
我们刻意保持 dmPython 的 19000 行 C 代码**零修改**。好处是:
176+
177+
- 上游发布新版本时,可以直接 `git merge upstream/main`
178+
- 不需要理解和维护 C 代码的内部逻辑
179+
- Bug 修复和新功能自动继承
180+
181+
代价是 Go 桥接层必须**精确实现** DPI 头文件声明的所有函数签名和内存布局,没有偷懒的余地。
182+
183+
### 5.3 Wheel 打包的平台标签陷阱
184+
185+
macOS wheel 的平台标签直接影响 `pip install` 的兼容性判断。我们遇到的问题是:
186+
187+
```
188+
# 错误:混入非目标架构标签,pip 在 ARM64 上可能拒绝安装
189+
dmPython_macOS-2.5.30-cp312-cp312-macosx_10_9_x86_64.macosx_14_0_arm64.whl
190+
191+
# 正确:纯 ARM64 标签
192+
dmPython_macOS-2.5.30-cp312-cp312-macosx_14_0_arm64.whl
193+
```
194+
195+
解决方法是在构建时通过环境变量明确指定目标平台:
196+
197+
```bash
198+
MACOSX_DEPLOYMENT_TARGET=14.0 _PYTHON_HOST_PLATFORM=macosx-14.0-arm64 python -m build --wheel
199+
```
200+
201+
### 5.4 Secrets 管理:专有头文件的处理
202+
203+
DPI 头文件(`DPI.h``DPItypes.h` 等)是达梦的专有文件,不能公开分发。我们的方案:
204+
205+
1. `.gitignore` 排除 `dpi_include/` 目录
206+
2. 将头文件 tar.gz + Base64 编码后存入 GitHub Secrets (`DPI_HEADERS_TAR_B64`)
207+
3. CI 中解码到临时目录,构建完成后自动清理
208+
209+
这样既保护了专有文件,又实现了完全自动化的 CI 构建。
210+
211+
## 6. 项目数据
212+
213+
| 指标 | 数值 |
214+
|------|------|
215+
| C 源码(复用上游) | ~19,000 行 |
216+
| Go 桥接层(新写) | ~4,700 行 |
217+
| 支持 Python 版本 | 3.9, 3.10, 3.11, 3.12, 3.13 |
218+
| 支持平台 | macOS ARM64 (Apple Silicon) |
219+
| CI 构建时间 | ~1 分钟(5 版本并行) |
220+
| Wheel 大小 | ~8 MB(内嵌 libdmdpi.dylib) |
221+
| 依赖 | 零运行时依赖 |
222+
223+
## 7. 产生的价值
224+
225+
1. **填补平台空白**:macOS ARM64 开发者首次获得可直接 `pip install` 的达梦 Python 驱动
226+
2. **开发体验提升**:不再需要 Linux 虚拟机或远程服务器来调试涉及达梦数据库的 Python 代码
227+
3. **Go 桥接模式的验证**:证明了"用 Go 重实现 C 库接口"这一技术路线的可行性,可推广到其他缺乏跨平台支持的数据库驱动
228+
4. **开源最佳实践**:从 CI 自动构建、Release 自动发布,到标准的开源文件结构,提供了一个小型开源项目的完整范本
229+
5. **与上游共存**:零修改 fork 策略确保可以持续跟进上游更新,不会分裂社区
230+
231+
## 8. 未来方向
232+
233+
- [ ] 发布到 PyPI,支持 `pip install dmPython-macOS`
234+
- [ ] 补充自动化测试套件(连接 Docker 达梦实例)
235+
- [ ] 探索 Linux ARM64 支持(同样缺少官方 `libdmdpi.so`
236+
- [ ] 性能基准测试:Go 桥接层 vs 原生 DPI 库的开销对比
237+
- [ ] 向上游提议合并 Go 桥接方案,惠及更多平台
238+
239+
---
240+
241+
**项目地址**https://github.com/skhe/dmPython
242+
243+
**上游项目**https://github.com/DamengDB/dmPython
244+
245+
**许可证**:Mulan PSL v2

0 commit comments

Comments
 (0)