From 1607dff2296155de3f6b65f23faae7b1c0f0cd40 Mon Sep 17 00:00:00 2001
From: zhuweiye
Date: Thu, 15 Oct 2026 20:42:36 +0800
Subject: [PATCH 01/11] =?UTF-8?q?feat(db):=20ODBC=20=E5=90=8E=E7=AB=AF=20?=
=?UTF-8?q?=E2=80=94=E2=80=94=20=E7=AC=AC=E5=9B=9B=E4=B8=AA=E5=90=8E?=
=?UTF-8?q?=E7=AB=AF=EF=BC=8C=E5=AF=B9=E7=9D=80=E7=9A=84=E6=98=AF=E6=8E=A5?=
=?UTF-8?q?=E5=8F=A3=E8=80=8C=E4=B8=8D=E6=98=AF=E6=9F=90=E4=B8=80=E7=A7=8D?=
=?UTF-8?q?=E5=BA=93=EF=BC=881.7.1-dev=EF=BC=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
`src/db/uvcpp_db_odbc.cpp`(新增,1478 行)走 unixODBC / Windows odbc32 那套
`SQLDriverConnectW` + 预处理语句,落进既有的 `uvcpp_db_driver` 接口,于是
`uvcpp_db_client` / `uvcpp_db_async` / `uvcpp_db_pool` 那三层一个字没改。进它的
直接原因是 SQL Server:微软没给 SQL Server 一个可链接的 C 客户端库,官方
`msodbcsql18` **本身就是** ODBC 驱动,想接它只能走这条路。
三处与另外三个后端**不同**的地方:
- 它对着一个**接口**而不是一种库:连什么、哪种方言、哪个厂商,全由连接串里的
`Driver=`(或 `DSN=`)决定,本驱动**只试那一个驱动名、不遍历**。所以
`UVCPP_DB_ODBC_ENABLE`("这份产物里有没有这个后端")与"要连的数据源装没装
驱动"是**两件事**,后者只有 `open()` 才知道 —— 那时报的是驱动管理器的原文
(驱动名写错时就是它那句"找不到数据源、也没有默认驱动"),不是我们编的话。
- **没有 `UVCPP_DB_RUNTIME_LOAD` 那条路**:驱动管理器本来就该在、而且很小;
真正按需加载的是它下面的**具体驱动**,那件事归驱动管理器自己管。
- 配置期只要 `sql.h` 与驱动管理器(Linux 的 `libodbc`、Windows 的系统 `odbc32`)。
找的是 `sql.h` 而不是 `sqltypes.h`:后者在很多包的残留里都有,前者才代表一套
完整的 ODBC 头。只装运行时 unixodbc 的机器上没有 `.so` 开发符号链接 ⇒ 落到
"找不到 sql.h" 那条 warning,而不是在库那一格报一句让人去猜原因的话。
一处头文件陷阱单独记:unixODBC 的 `sqltypes.h` 第 75 行是 `#define BOOL int`,它把
本文件里唯一一处 `uvcpp_db_type::BOOL` 展开成 `uvcpp_db_type::int`,报出来的错是
"expected unqualified-id before 'int'" —— 离真正的原因隔着一层。收尾是 ODBC 头
预处理完之后一句 `#undef BOOL`(本文件从头到尾不用 ODBC 的 `BOOL`)。微软那套
`sqltypes.h` 不发这个宏(Windows 上 `BOOL` 是 `` 里的 typedef,对
`enum class` 的限定名没影响),所以那行在 Windows 上是空操作。
实测:`cmake -S . -B build-odbc -DUVCPP_ENABLE_DB=ON` 输出
`-- db: ODBC 后端开(驱动管理器 …/libodbc.so)`,`cmake --build build-odbc` rc=0
(全树 [100%]),生成的 `uvcpp_config.h` 里 `UVCPP_DB_ODBC_ENABLE 1`。
门禁:`check_doc_versions.py` / `check_docs.py` / `check_doc_lines.py` 三条 rc=0。
行号锁:插进 `CMakeLists.txt` 的那几段把 65 处引用整体挪了号,按**内容**逐条重算
(不是盲跑 `--update`),刷锁后核过 —— 新旧锁的「哈希 + 尾部锚点」多重集**完全
一致**,65 个消失的键在锁里都能找到同哈希的新键;只有 `CMakeLists.txt:604` 这条是
同一个键换了所指内容(它原本指 web 那个强制置 OFF 的 `if()`,整体 +9 行之后那段
挪到 613,而这个键按 `doc/http2-guide.md` 的意思该指 OpenSSL 那个 —— 两行内容都
逐字核过)。`check_doc_snippets.py` 前提没满足(要 `--pkg`,本机没有装好的包),
一条都没判。
本条只做后端本体,还欠两件:`doc/db-guide.md` 的 ODBC 一节与
`tests/functional/db_odbc_func.cpp`。分支基点是 tag `v1.7.0`;`origin/master`
已经在 `1.7.2-dev`(领先 4 个提交),合并前要不要 rebase 待定。
---
.github/workflows/ci-linux-ubuntu.yml | 4 +-
.github/workflows/ci-macos.yml | 2 +-
.github/workflows/ci-mingw64.yml | 4 +-
.github/workflows/ci-windows-msvc.yml | 6 +-
.github/workflows/release.yml | 10 +-
CHANGELOG.md | 60 +-
CMakeLists.txt | 60 +-
README.md | 7 +-
README.zh.md | 7 +-
RELEASE.md | 4 +-
cmake/uvcpp_config.h.in | 12 +
doc/ci-guide.md | 6 +-
doc/expand-guide.md | 6 +-
doc/grpc-guide.md | 4 +-
doc/http2-guide.md | 10 +-
doc/http2-status.md | 6 +-
doc/http3-guide.md | 28 +-
doc/json-guide.md | 2 +-
doc/json-reflect-guide.md | 2 +-
doc/quic-guide.md | 24 +-
doc/ssl-guide.md | 2 +-
doc/web-http-guide.md | 8 +-
doc/webapp-support-guide.md | 2 +-
src/db/uvcpp_db_driver.h | 6 +-
src/db/uvcpp_db_factory.cpp | 21 +-
src/db/uvcpp_db_odbc.cpp | 1478 +++++++++++++++++++++++++
src/uvcpp/uvcpp_version.h | 4 +-
tests/tools/check_doc_snippets.py | 2 +-
tests/tools/check_docs.py | 4 +-
tests/tools/doc_line_refs.lock | 132 +--
tests/tools/package_release.py | 8 +-
31 files changed, 1757 insertions(+), 174 deletions(-)
create mode 100644 src/db/uvcpp_db_odbc.cpp
diff --git a/.github/workflows/ci-linux-ubuntu.yml b/.github/workflows/ci-linux-ubuntu.yml
index 46e86796..a3c073dd 100644
--- a/.github/workflows/ci-linux-ubuntu.yml
+++ b/.github/workflows/ci-linux-ubuntu.yml
@@ -104,7 +104,7 @@ jobs:
# 模块关掉时它们被 `tests/functional/CMakeLists.txt` 的 glob+FILTER 摘掉,
# 而 ctest 报 "100% tests passed" 时并不会说"少了几条"。
#
- # **不需要任何新包**:pugixml 走 FetchContent(`CMakeLists.txt:1667`),
+ # **不需要任何新包**:pugixml 走 FetchContent(`CMakeLists.txt:1713`),
# 装包表与 `web` 那一格逐字相同 —— 这样两边红绿不同只可能归因到那个开关。
# 四个门禁串见 doc/ci-guide.md §5。
- feature: wsdl
@@ -139,7 +139,7 @@ jobs:
# nlohmann 那侧照编照跑(`web_app_request_func` 里深度预扫描的五条断言
# 也在),yyjson 那侧的新用例才拿得到对照物。
#
- # **不需要任何新包**:yyjson 走 FetchContent(`CMakeLists.txt:1727`,
+ # **不需要任何新包**:yyjson 走 FetchContent(`CMakeLists.txt:1773`,
# `GIT_TAG` 钉在 `YYJSON_VERSION`),装包表与 `web` 那一格逐字相同 ——
# 这样两边红绿不同只可能归因到那个开关。
# 四个门禁串见 doc/ci-guide.md §5。
diff --git a/.github/workflows/ci-macos.yml b/.github/workflows/ci-macos.yml
index c8da07ee..4ce7dd1c 100644
--- a/.github/workflows/ci-macos.yml
+++ b/.github/workflows/ci-macos.yml
@@ -60,7 +60,7 @@ jobs:
# 与 wsdl 不同:macOS **没有** wsdl 格(§1 的"未覆盖的格"表里那一行写的
# 是"不发版、风险低"),而 yyjson 这一格加得起 —— 它不需要任何新包。
#
- # yyjson 走 FetchContent(根 `CMakeLists.txt:1727`,`GIT_TAG` 钉在
+ # yyjson 走 FetchContent(根 `CMakeLists.txt:1773`,`GIT_TAG` 钉在
# `YYJSON_VERSION`),**不需要新包**;装包表与 `web` 逐字相同 —— 这样两边
# 红绿不同只可能归因到那个开关。
# 四个门禁串见 doc/ci-guide.md §5。
diff --git a/.github/workflows/ci-mingw64.yml b/.github/workflows/ci-mingw64.yml
index 05309239..2eb33778 100644
--- a/.github/workflows/ci-mingw64.yml
+++ b/.github/workflows/ci-mingw64.yml
@@ -62,13 +62,13 @@ jobs:
# 不带 preset ⇒ 这个开关默认 OFF ⇒ `find_library` 在 MSYS2 里撞上
# C:/msys64/mingw64/lib/libuv.dll.a(runner 镜像自带 libuv)⇒ 链成
# 共享库,产物依赖 `libuv-1.dll`(MSYS2 那份的 DLL 名带 -1)。
- # 注意 `BUILD_SHARED_LIBS` 在这一路上救不了场:CMakeLists.txt:1404-1404 判的是
+ # 注意 `BUILD_SHARED_LIBS` 在这一路上救不了场:CMakeLists.txt:1450-1450 判的是
# `TARGET uv_a`,而 find_library 给的是一个**路径**,不是 target,
# 所以那个分支根本进不去。
#
# ② -DBUILD_SHARED_LIBS=OFF —— 管 llhttp(根 CMakeLists.txt:6 默认 ON ⇒
# llhttp_shared ⇒ 依赖 libllhttp.dll)。它不管 uvcpp 自己
- # (CMakeLists.txt:1915-1915 是显式的 `add_library(uvcpp SHARED ...)`)。
+ # (CMakeLists.txt:1961-1961 是显式的 `add_library(uvcpp SHARED ...)`)。
# 本地 A/B 过:OFF 时链接行是 `_deps/llhttp-build/libllhttp.a`,ON 时是
# `libllhttp.dll.a`。装到 FetchContent 的 libuv 上时它顺带也管 libuv
# (708 行挑 `uv_a`),但那一路在 CI 上本来就走不到。
diff --git a/.github/workflows/ci-windows-msvc.yml b/.github/workflows/ci-windows-msvc.yml
index 998e75f1..ca632918 100644
--- a/.github/workflows/ci-windows-msvc.yml
+++ b/.github/workflows/ci-windows-msvc.yml
@@ -63,7 +63,7 @@ jobs:
# **发版那一刻** —— 而 release.yml 的 msvc-x64 / msvc-arm64 两条腿都开着
# 它。这一格就是那两条腿在 push/PR 阶段的替身。
#
- # pugixml 走 FetchContent(根 `CMakeLists.txt:1667`,`GIT_TAG` 钉在
+ # pugixml 走 FetchContent(根 `CMakeLists.txt:1713`,`GIT_TAG` 钉在
# `PUGIXML_VERSION`),**不需要新包**;它与 zlib 同一个形状,所以这一格
# 的装包需求与 `web` 逐字相同。
# 四个门禁串见 doc/ci-guide.md §5。
@@ -95,7 +95,7 @@ jobs:
# nlohmann 再比 `==`」—— 平台间差异正是在这种地方现形。
# (yaml 里这段 `-D` 之所以放在块序列里、不折成一行,见 §1。)
#
- # yyjson 走 FetchContent(根 `CMakeLists.txt:1727`,`GIT_TAG` 钉在
+ # yyjson 走 FetchContent(根 `CMakeLists.txt:1773`,`GIT_TAG` 钉在
# `YYJSON_VERSION`),**不需要新包**;它与 zlib 同一个形状,所以这一格
# 的装包需求与 `web` 逐字相同。
# 四个门禁串见 doc/ci-guide.md §5。
@@ -340,7 +340,7 @@ jobs:
run: |
case "${{ matrix.feature }}" in
# wsdl 也在这一组:pugixml 走 FetchContent,与 zlib 一样不是系统依赖。
- # yyjson 同理(`CMakeLists.txt:1727` 的 FetchContent)。
+ # yyjson 同理(`CMakeLists.txt:1773` 的 FetchContent)。
# db 也是:它的 SQLite 是**从源码编**的(`UVCPP_DB_SQLITE_FROM_SOURCE`),
# 所以 Windows 上不需要任何 sqlite 包 —— 这一格一个系统依赖都没有,
# 别把它塞进 win-openssl-deps.sh(那是 OpenSSL 专用的)。
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 48e08a4a..713b7a2e 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -161,15 +161,15 @@ jobs:
# ① UVCPP_BUILD_LIBUV_FROM_SOURCE=ON:不带 preset 时它默认 OFF,
# `find_library` 会撞上 runner 镜像 MSYS2 自带的 libuv.dll.a,链成
# 共享库 ⇒ 依赖 libuv-1.dll。BUILD_SHARED_LIBS 救不了这一路
- # (CMakeLists.txt:1404-1404 判的是 TARGET uv_a,而 find_library 给的是路径)。
+ # (CMakeLists.txt:1450-1450 判的是 TARGET uv_a,而 find_library 给的是路径)。
# ② BUILD_SHARED_LIBS=OFF:管 llhttp(根 CMakeLists.txt:6 默认 ON ⇒
# llhttp_shared ⇒ 依赖 libllhttp.dll)。zlib 不受影响,
- # CMakeLists.txt:2284-2285 明确链 zlibstatic。
+ # CMakeLists.txt:2330-2331 明确链 zlibstatic。
# 本机所有树都走 CMakePresets(它有 ①),所以这两条从来没在本机复现过。
#
# ③ UVCPP_ENABLE_NGHTTP2=ON:这个开关**默认 OFF**(CMakeLists.txt:159-159),
# 而这几个打包装的 job 一直没传它 —— 于是包里根本没有 h2,README 却在
- # 介绍它。nghttp2 是唯一被强制静态的依赖(CMakeLists.txt:607-621),
+ # 介绍它。nghttp2 是唯一被强制静态的依赖(CMakeLists.txt:616-630),
# 所以开它不会给产物添任何 dll。六个打包 job 都必须传,任何一个漏了
# 就会发一个"同版本号但少一半功能"的包。
#
@@ -604,7 +604,7 @@ jobs:
- uses: actions/checkout@v4
# 不靠镜像里"碰巧装了 OpenSSL":没找到时 `find_package(OpenSSL QUIET)` 会把
- # UVCPP_ENABLE_OPENSSL 静默置 OFF(CMakeLists.txt:534-534),那样发出去的包
+ # UVCPP_ENABLE_OPENSSL 静默置 OFF(CMakeLists.txt:543-543),那样发出去的包
# 一句 SSL 都没有 —— 而它的生成头会**如实**说 0,使用者照配也拿不到 SSL。
#
# 这条要求由 `.github/scripts/win-openssl-deps.sh` 兑现,而且比原来那一行更硬:
@@ -635,7 +635,7 @@ jobs:
set -o pipefail
# 同 mingw-x64:① 让 libuv 从源码静态编(runner 上若有系统 libuv,
# 不带这个开关就会链成 uv.dll),② 让 llhttp 静态(否则 llhttp.dll)。
- # zlib 不受 ② 影响(CMakeLists.txt:2284-2285 明确链 zlibstatic)。
+ # zlib 不受 ② 影响(CMakeLists.txt:2330-2331 明确链 zlibstatic)。
# MSVC 侧 uvcpp 自己仍走 /MD + 包内自带 VC++ 运行库,与此无关。
cmake -S . -B build-msvc -G "Visual Studio 17 2022" -A x64 \
-DBUILD_SHARED_LIBS=OFF \
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9e072f79..4c1cb5ea 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,7 +12,7 @@
`1.5.1`–`1.5.3` → `v1.6.0`;`1.6.1`–`1.6.4` 是它之后的补丁版,不折叠、直接发布;
`1.6.5`–`1.6.61` → `v1.7.0`。想按时间线读,翻到最后的「逐个版本」。
-当前源码树是 **1.7.0**(发布版) —— 即 `src/uvcpp/uvcpp_version.h` 里 `UVCPP_VERSION_STRING`
+当前源码树是 **1.7.1-dev**(开发版) —— 即 `src/uvcpp/uvcpp_version.h` 里 `UVCPP_VERSION_STRING`
报告的那个串。已打过的 tag:`v1.0.0`、`v1.1.0`、`v1.2.0`、`v1.3.0`、`v1.4.0`、`v1.5.0`、`v1.6.0`、`v1.6.1`、`v1.6.2`、`v1.6.3`、`v1.6.4`、`v1.7.0`。
其中若干条来自本仓第一位外部贡献者 [@sercebr](https://github.com/sercebr) 报的 issue。
@@ -305,7 +305,7 @@
`doc/quic-guide.md` §8 的措辞是"契约改了",不是"缺口补了"(`1.4.1`)
- **那一对私有头。** `uvcpp_quic_session.h` 里是 `ngtcp2_conn*`、`SSL*` 与
`ngtcp2_path_storage`,字段布局跟着 ngtcp2 的版本走 —— 它是第二个私有头,与
- `uvcpp_quic_ngtcp2.h` 并列。两个都不安装(`CMakeLists.txt:3047-3047`)、打包也排除
+ `uvcpp_quic_ngtcp2.h` 并列。两个都不安装(`CMakeLists.txt:3095-3095`)、打包也排除
(`tests/tools/package_release.py` 的 `PRIVATE_HEADERS`);量过:
`cmake --install build-quic --prefix /tmp/inst` 落进 `include/quic/` 的正好是那
四个公开头(`1.4.1`)
@@ -642,7 +642,7 @@
`std::string` / `uint32_t` / `const char*`,**不认识连接、不认识响应**,所以
`UVCPP_BUILD_WEB=OFF` 时照编照装。`uvcpp_grpc_server.{h,cpp}` 把上面那半接到
`uvcpp_http_server` 上,它的每一个参数都是 web 层的类型,web 关掉时**从构建与安装
- 两处都摘掉**(`CMakeLists.txt:1951-1953`、`CMakeLists.txt:2939-2944`)——
+ 两处都摘掉**(`CMakeLists.txt:1997-1999`、`CMakeLists.txt:2987-2992`)——
实测:`-DUVCPP_BUILD_WEB=OFF` 的树上 `src/grpc/uvcpp_grpc.cpp` 照编、装到前缀里的
只有 `include/grpc/uvcpp_grpc.h`;`build-capi-all` 上前缀里两个头都在。
**不加 `UVCPP_ENABLE_GRPC`**:两半的依赖是死的,多一个旋钮只会多一个能与
@@ -746,11 +746,35 @@
### 数据库(`src/db/`)
-一个连接一个 `uvcpp_db_client`,**同一套接口盖住 SQLite / MySQL / PostgreSQL**
-三个后端,结果行按表取(列名或列号)。**默认关**(`UVCPP_ENABLE_DB`),而且是本库
-**唯一需要第三方客户端库**的模块(libsqlite3 / libmysqlclient / libpq)—— 三个后端
-一个都没找到时整个模块被强制关掉并打一条说明怎么修的 warning,而不是留一个
-"能配置、链不上"的组合(`1.5.3`)
+一个连接一个 `uvcpp_db_client`,**同一套接口盖住 SQLite / MySQL / PostgreSQL /
+ODBC** 四个后端,结果行按表取(列名或列号)。**默认关**(`UVCPP_ENABLE_DB`),而且是
+本库**唯一需要第三方客户端库**的模块(libsqlite3 / libmysqlclient / libpq / 驱动
+管理器 + 具体 ODBC 驱动)—— 四个后端一个都没找到时整个模块被强制关掉并打一条说明
+怎么修的 warning,而不是留一个"能配置、链不上"的组合(`1.5.3`)
+
+- **ODBC 后端:它是第四个后端,但与前三个不同类 —— 它对着的不是一种数据库,是一个
+ 接口**(`1.7.1-dev`)。前三个各自钉死一种客户端库与一种方言,`odbc://` 连到哪张
+ 库、哪种方言、甚至哪个厂商,全由连接串里的 `Driver=`(或 `DSN=`)决定;本驱动
+ **只试那一个驱动名,不遍历**。进它的直接原因是 SQL Server:微软没给 SQL Server
+ 一个可链接的 C 客户端库,官方 `msodbcsql18` **本身就是**一个 ODBC 驱动,想接它就
+ 只能走这条路(这也是本仓第一个不是"为了某一种库"而写的后端)。
+
+ 于是有两条**必须分开看**的事实:`UVCPP_DB_ODBC_ENABLE` 说的是"这份产物里有没有这个
+ 后端",而"要连的那个数据源有没有装驱动"**只有 `open()` 才知道** —— 驱动名写错时
+ 拿到的是驱动管理器给的原文("找不到数据源、也没有默认驱动"),不是我们编的话。
+ 配置期只要两样:`sql.h` 与驱动管理器(Linux 的 `libodbc`、Windows 的系统 `odbc32`)。
+ **没有 `UVCPP_DB_RUNTIME_LOAD` 那条路**:驱动管理器本来就该在、而且很小;真正按需
+ 加载的是它下面的**具体驱动**,那件事归驱动管理器自己管(本机 `-- db: ODBC 后端开
+ (驱动管理器 …/libodbc.so)`,编、链 rc=0)。
+
+ 写这一片时踩到一处值得记下来的头文件陷阱:unixODBC 的 `sqltypes.h` 第 75 行有一句
+ `#define BOOL int`(给没有 `` 的平台补齐 ODBC 那套类型表),它会把
+ `src/db/uvcpp_db_odbc.cpp` 里唯一一处 `uvcpp_db_type::BOOL` 展开成
+ `uvcpp_db_type::int`,报出来的错是"expected unqualified-id before 'int'" —— 离真正
+ 的原因隔着一层。收尾是一句 `#undef BOOL`(ODBC 头已经预处理完,这个宏使命结束,
+ 本文件从头到尾不用它)。微软那套 `sqltypes.h` 不发这个宏(Windows 上 `BOOL` 是
+ `` 里的 typedef,对 `enum class` 的限定名没影响),所以那行在 Windows 上
+ 是空操作。
- **MySQL / PostgreSQL 的客户端库改成可以运行时加载:`UVCPP_DB_RUNTIME_LOAD`**
(`1.6.54-dev`,默认 `OFF`)。直接链的后果不是"少个功能"而是**进程起不来**:
@@ -2583,17 +2607,17 @@
dlopen 只是把这条路变成**每个装了 libpq 的人**的默认路径。)把我们的嵌入依赖藏起来,
libpq 就会解析到**系统**那份、与它自己的 libcrypto 自洽。
- 修法一(`CMakeLists.txt:2445-2487`,`UNIX AND NOT APPLE`,只给共享目标
+ 修法一(`CMakeLists.txt:2493-2535`,`UNIX AND NOT APPLE`,只给共享目标
`uvcpp`/`uvcpp_s`):`target_link_options(… PRIVATE "LINKER:--exclude-libs,ALL")`。
它只藏**来自静态归档**的符号,而这两个目标是
- `add_library(uvcpp SHARED ${UVCPP_SOURCES} …)`(`CMakeLists.txt:2147-2147`)
+ `add_library(uvcpp SHARED ${UVCPP_SOURCES} …)`(`CMakeLists.txt:2193-2193`)
**直接拿我们自己的目标文件**建的、不经归档 ⇒ 我们自己的 API 与 `uvcpp_c_*` 那套
C ABI 照样导出。只给共享目标加是因为静态库没有导出表,藏了反而让 `uvcpp_a` 的消费者
在最终链接时找不到本该由我们提供的符号;只给 ELF 加是因为 Apple 的 ld 没有这个选项
(那边是 `-exported_symbols_list`),而发布腿里本来也没有 macOS。
修法二:`uvcpp_yyjson` 是 **OBJECT** 库,它的目标文件经
- `$`(`CMakeLists.txt:2776-2776`)**直接进最终链接、也不经归档** ⇒
+ `$`(`CMakeLists.txt:2824-2824`)**直接进最终链接、也不经归档** ⇒
上面那条对它无效(实测加不加都是 43 个 `yyjson_*`)。那边改用
`C_VISIBILITY_PRESET hidden` **加** `target_compile_definitions(uvcpp_yyjson PRIVATE
yyjson_api=)`。**第二半是承重的**:上游 `yyjson.h` 给每个公开函数**显式**贴了
@@ -2637,16 +2661,16 @@
记过是陷阱(它只骗得过 CMake 的 `find_*`,骗不过编译器自己的默认搜索路径)。
修法照 `UVCPP_DB_SQLITE_FROM_SOURCE`(`CMakeLists.txt:189`)的命名与形状加一个
- **默认 OFF** 的选项(`CMakeLists.txt:233-234`),把整段发现包进
- `if(NOT UVCPP_BROTLI_FROM_SOURCE)`(`CMakeLists.txt:409-409`)。注意是**连发现都不
+ **默认 OFF** 的选项(`CMakeLists.txt:242-243`),把整段发现包进
+ `if(NOT UVCPP_BROTLI_FROM_SOURCE)`(`CMakeLists.txt:418-418`)。注意是**连发现都不
调用**,不是"找到了也偏不用":`find_path`/`find_library` 的结果会写进 cache,
调用过一次,`UVCPP_BROTLI_INCLUDE_DIR` 之类就一直留在那棵树里,同一棵树翻开关时
会把上一轮的答案漏过来。所以每个"找到系统那份"的判据都**显式**带上
- `NOT UVCPP_BROTLI_FROM_SOURCE`(`CMakeLists.txt:420-420`、`CMakeLists.txt:424-424`、
- `CMakeLists.txt:432-432`),而不是指望"没调用 = 没找到"。取源码那条支路一个字没改:
+ `NOT UVCPP_BROTLI_FROM_SOURCE`(`CMakeLists.txt:429-429`、`CMakeLists.txt:433-433`、
+ `CMakeLists.txt:441-441`),而不是指望"没调用 = 没找到"。取源码那条支路一个字没改:
选项开着 ⇒ 跳过发现 ⇒ `BROTLIENC_LIB` 仍是 `NOTFOUND` ⇒ 自然落进那一支 ⇒
- 那里照旧 `set(UVCPP_BROTLI_FROM_SOURCE TRUE)`(`CMakeLists.txt:510-510`),链接段
- (`CMakeLists.txt:2629-2629`)也就照旧按 `$` 取静态库。默认值一个都没
+ 那里照旧 `set(UVCPP_BROTLI_FROM_SOURCE TRUE)`(`CMakeLists.txt:519-519`),链接段
+ (`CMakeLists.txt:2677-2677`)也就照旧按 `$` 取静态库。默认值一个都没
动,所以既有使用者的行为逐字节不变。
判据(本机,`build-brotli2`:`-DUVCPP_ENABLE_BROTLI=ON
@@ -2832,7 +2856,7 @@
`flags` = `web` 加一个 `-DUVCPP_ENABLE_YYJSON=ON`,四道门禁串
`UVCPP_ENABLE_YYJSON:BOOL=ON` / `yyjson integrated` / `Including yyjson module in
build` / `test_yyjson_doc_func`;装包表与 `web` **逐字相同**(yyjson 走 FetchContent
- —— `CMakeLists.txt:1727`,`GIT_TAG` 钉在 `YYJSON_VERSION` —— **不发包管理器**),
+ —— `CMakeLists.txt:1773`,`GIT_TAG` 钉在 `YYJSON_VERSION` —— **不发包管理器**),
这样两边红绿不同只可能归因到那个开关。
**为什么 macOS 有一格、而 `wsdl` 没有**:`ci-guide.md` §1 给缺 macOS `wsdl` 的理由是
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 8db4fc15..bf7fecc9 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -190,6 +190,15 @@ option(UVCPP_DB_SQLITE_FROM_SOURCE
"Build the SQLite backend from a pinned source amalgamation (static) instead of the system libsqlite3" OFF)
option(UVCPP_ENABLE_DB_MYSQL "Build the MySQL backend of the db module (needs libmysqlclient)" ON)
option(UVCPP_ENABLE_DB_PGSQL "Build the PostgreSQL backend of the db module (needs libpq)" ON)
+# ODBC 后端是**通用**的:它只要求一套 ODBC 头加一个驱动管理器,具体连什么库由
+# 连接串里的 `Driver=` 决定。接 SQL Server 走的就是这条路 —— 微软没有给 SQL Server
+# 一个可链接的 C 客户端库,`msodbcsql18` 本身就是 ODBC 驱动。
+#
+# 与另两个后端不同,它**没有** `UVCPP_DB_RUNTIME_LOAD` 那条路:驱动管理器
+# (Linux 的 libodbc、Windows 的 odbc32)本来就该在,而且很小。真正按需加载的是
+# 它下面的**具体驱动**,那件事由驱动管理器自己干,不归我们管。
+option(UVCPP_ENABLE_DB_ODBC
+ "Build the ODBC backend of the db module (needs ODBC headers + a driver manager; the data source is chosen by the connection string)" ON)
# MySQL / PostgreSQL 两个后端**不在链接行上出现**,改成运行时打开客户端库。
#
# 这是给**预编译包**用的:`DT_NEEDED`(Windows 上是导入表)是**加载期**解析的,
@@ -1217,12 +1226,49 @@ if(UVCPP_ENABLE_DB)
endif()
endif()
- # ---- 三个都关掉 ⇒ 整个模块关掉(见上面那段理由)----
+ if(UVCPP_ENABLE_DB_ODBC)
+ # 只要两样:头(sql.h)与驱动管理器(Linux 的 libodbc / Windows 的 odbc32)。
+ # 找的是 `sql.h` 而不是 `sqltypes.h` —— 后者在很多包的残留里都有,前者才代表
+ # 一套完整的 ODBC 头。
+ find_path(UVCPP_DB_ODBC_INCLUDE_DIR
+ NAMES sql.h
+ PATH_SUFFIXES include odbc include/odbc)
+ if(NOT UVCPP_DB_ODBC_INCLUDE_DIR)
+ message(WARNING
+ "UVCPP_ENABLE_DB_ODBC=ON 但找不到 sql.h。已强制关闭这个后端 —— "
+ "其余后端照编。Debian/Ubuntu 上装 unixodbc-dev,RHEL 系是 unixODBC-devel。")
+ set(UVCPP_ENABLE_DB_ODBC OFF)
+ elseif(WIN32)
+ # Windows 的 ODBC 是系统组件:头在 SDK 里,库就是 odbc32.dll(一定在)。
+ list(APPEND UVCPP_DB_INCLUDE_DIRS "${UVCPP_DB_ODBC_INCLUDE_DIR}")
+ list(APPEND UVCPP_DB_LINK_LIBS odbc32)
+ message(STATUS "db: ODBC 后端开(Windows 系统 odbc32)")
+ else()
+ # 这里找的是 unixODBC 的 `libodbc`。注意 `libodbc.so` 这个**开发符号链接**
+ # 是 unixodbc-dev 才给的:只装运行时的 unixodbc 时,`.so.2` 在而 `.so` 不在,
+ # 于是头与库都找不到 —— 会走到上面那条"找不到 sql.h",
+ # 而不是在这里报一句让人去猜"为什么装了 unixodbc 还不行"。
+ find_library(UVCPP_DB_ODBC_LIBRARY NAMES odbc)
+ if(UVCPP_DB_ODBC_LIBRARY)
+ list(APPEND UVCPP_DB_INCLUDE_DIRS "${UVCPP_DB_ODBC_INCLUDE_DIR}")
+ list(APPEND UVCPP_DB_LINK_LIBS "${UVCPP_DB_ODBC_LIBRARY}")
+ message(STATUS "db: ODBC 后端开(驱动管理器 ${UVCPP_DB_ODBC_LIBRARY})")
+ else()
+ message(WARNING
+ "UVCPP_ENABLE_DB_ODBC=ON 但只找到 sql.h、找不到 libodbc。已强制关闭这个"
+ "后端 —— 其余后端照编。Debian/Ubuntu 上装 unixodbc-dev(只装 unixodbc "
+ "运行时不带 .so 符号链接)。")
+ set(UVCPP_ENABLE_DB_ODBC OFF)
+ endif()
+ endif()
+ endif()
+
+ # ---- 四个都关掉 ⇒ 整个模块关掉(见上面那段理由)----
if(NOT UVCPP_ENABLE_DB_SQLITE AND NOT UVCPP_ENABLE_DB_MYSQL AND
- NOT UVCPP_ENABLE_DB_PGSQL)
+ NOT UVCPP_ENABLE_DB_PGSQL AND NOT UVCPP_ENABLE_DB_ODBC)
message(WARNING
- "UVCPP_ENABLE_DB=ON 但 SQLite / MySQL / PostgreSQL 三个后端一个都没成 —— "
- "这样的库会编出一个谁也打不开的 uvcpp_db_client。已强制关闭 db 模块。")
+ "UVCPP_ENABLE_DB=ON 但 SQLite / MySQL / PostgreSQL / ODBC 四个后端一个都没"
+ "成 —— 这样的库会编出一个谁也打不开的 uvcpp_db_client。已强制关闭 db 模块。")
set(UVCPP_ENABLE_DB OFF)
endif()
endif()
@@ -1870,11 +1916,11 @@ else()
add_compile_definitions(UVCPP_DB_ENABLE=0)
message(STATUS "Database module disabled (UVCPP_DB_ENABLE=0)")
endif()
-# 三个后端宏**不受主开关守卫**:主开关关掉时它们照样按"这个后端编不编"取 0。
+# 四个后端宏**不受主开关守卫**:主开关关掉时它们照样按"这个后端编不编"取 0。
# 写成 `if(UVCPP_ENABLE_DB) ... else 三个都 0` 的话,`uvcpp_db_factory.cpp` 里
# 那句 `#if UVCPP_DB_SQLITE_ENABLE` 在模块关掉时是**未定义标识符**(求值为 0,
# 结果碰巧一样)—— 碰巧一样的东西不该靠碰巧。
-foreach(_uvcpp_db_backend SQLITE MYSQL PGSQL)
+foreach(_uvcpp_db_backend SQLITE MYSQL PGSQL ODBC)
if(UVCPP_ENABLE_DB_${_uvcpp_db_backend})
add_compile_definitions(UVCPP_DB_${_uvcpp_db_backend}_ENABLE=1)
else()
@@ -2312,6 +2358,7 @@ _uvcpp_literal01(UVCPP_DB_ENABLE UVCPP_ENABLE_DB)
_uvcpp_literal01(UVCPP_DB_SQLITE_ENABLE UVCPP_ENABLE_DB_SQLITE)
_uvcpp_literal01(UVCPP_DB_MYSQL_ENABLE UVCPP_ENABLE_DB_MYSQL)
_uvcpp_literal01(UVCPP_DB_PGSQL_ENABLE UVCPP_ENABLE_DB_PGSQL)
+_uvcpp_literal01(UVCPP_DB_ODBC_ENABLE UVCPP_ENABLE_DB_ODBC)
# 名字两边**一样**,这是这份文件里唯一的一处(别的都是 `UVCPP_ENABLE_X` 对
# `UVCPP_X_ENABLE`)。理由:它不属于那两个族里的任何一个 —— 既不是"开关某个
# 模块"(`UVCPP_ENABLE_*`),也不只是"这份依赖怎么来"(`*_FROM_SOURCE`,那两个
@@ -2389,6 +2436,7 @@ foreach (_uvcpp_t uvcpp uvcpp_a uvcpp_s uvcpp_a_s)
UVCPP_DB_SQLITE_ENABLE=${UVCPP_DB_SQLITE_ENABLE}
UVCPP_DB_MYSQL_ENABLE=${UVCPP_DB_MYSQL_ENABLE}
UVCPP_DB_PGSQL_ENABLE=${UVCPP_DB_PGSQL_ENABLE}
+ UVCPP_DB_ODBC_ENABLE=${UVCPP_DB_ODBC_ENABLE}
UVCPP_DB_RUNTIME_LOAD=${UVCPP_DB_RUNTIME_LOAD}
UVCPP_TRY_WRITE_ENABLE=${UVCPP_TRY_WRITE_ENABLE}
UVCPP_TRY_WRITE_MIN_BYTES=${UVCPP_TRY_WRITE_MIN_BYTES}
diff --git a/README.md b/README.md
index 1ffabc8b..fff9e45e 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
-[](./RELEASE.md)
+[](./RELEASE.md)
[](./LICENSE)
[](https://github.com/Antruly/libuvcpp/actions/workflows/ci-linux-ubuntu.yml)
[](https://github.com/Antruly/libuvcpp/actions/workflows/ci-windows-msvc.yml)
@@ -14,7 +14,7 @@
🔧 Modern C++11 wrapper for [libuv](https://github.com/libuv/libuv) — event-driven I/O with
object-oriented APIs, dual-mode async/sync support, HTTP/1.1, WebSocket (RFC 6455), and SSL/TLS.
-- **Version**: `1.7.0` — **Author**: `zhuweiye` — **License**: `MIT`
+- **Version**: `1.7.1-dev` — **Author**: `zhuweiye` — **License**: `MIT`
- **Languages**: [English](./README.md) · [中文](./README.zh.md)
## Download
@@ -310,11 +310,12 @@ OpenSSL ≥ 3.2 with the QUIC API for QUIC and HTTP/3), and how the switches int
| `UVCPP_ENABLE_BROTLI` | `OFF` | Enable the **brotli body compressor** — `br` as a second `Content-Encoding` for HTTP response bodies next to gzip/deflate, picked by the same `Accept-Encoding` negotiation (`br` wins on `q`-ties with gzip because it is the smaller of the two; `*` and the zlib-less build are covered too). **What it buys is bytes on the wire, not speed**: at the default quality the same body is ~11 % smaller than gzip level 6, and because the compressed variant is served out of the response cache the per-request cost is the same as gzip's — an entry that compresses without a cache tag pays for the encode every request. The default quality is `5` (brotli's own default), deliberately not `0`. **Turned off, nothing changes** (no `br` in `Accept-Encoding` negotiation, and the exported symbol set / `DT_NEEDED` of the library are unchanged). brotli is a **private** dependency, found via pkg-config/`find_library` when the system has it and built from source via `FetchContent` (tag pinned to `BROTLI_VERSION`) otherwise — **two** static libraries, so a static-library user links `-lbrotlienc -lbrotlidec` themselves. **As of 1.7.0 it is in the release package** — every release leg passes `UVCPP_BROTLI_FROM_SOURCE=ON` so the package stays self-contained — and five CI jobs really build it: the Ubuntu `brotli` + `brotli-no-zlib`, macOS and MSVC matrix entries plus the single `ci-mingw64.yml` job. See [`doc/web-http-guide.md`](doc/web-http-guide.md) |
| `UVCPP_BROTLI_FROM_SOURCE` | `OFF` | Skip the system lookup entirely (`find_package(PkgConfig)` → `find_path`/`find_library`) and build brotli from the source pinned by `BROTLI_VERSION`, linking it statically. **The lookup runs system-first**, so on a machine that happens to have `libbrotli-dev` the "links the static `.a`" promise silently turns into three `DT_NEEDED` entries (`libbrotlienc` / `libbrotlidec` / `libbrotlicommon`) — and nothing turns red, because the release workflow's `ldd` blacklist does not name brotli. Any release leg that enables brotli passes this so its package stays self-contained. The discovery block is skipped *without being called* (its results land in the CMake cache, so calling it once would leak the previous answer into the next configure of the same tree). Default `OFF` leaves today's behaviour byte-for-byte. See [`doc/web-http-guide.md`](doc/web-http-guide.md) |
| `UVCPP_ENABLE_CAPI` | `OFF` | Export the **C ABI** (`src/capi/`, C99 headers for C#/P-Invoke and any other FFI) out of the same `uvcpp` library — no extra artifact. Requires `UVCPP_BUILD_NET=ON` and `UVCPP_BUILD_WEB=ON`; force-disabled without them (the C surface spans net/web/webapp). **Turned on for every release configuration.** See [`doc/capi-guide.md`](doc/capi-guide.md) |
-| `UVCPP_ENABLE_DB` | `OFF` | Enable the **database module** (`src/db/`): one `uvcpp_db_client` per connection over SQLite / MySQL / PostgreSQL, with table-style access to the result rows. It is the only module that **requires a third-party client library** (libsqlite3 / libmysqlclient / libpq) — force-disabled when none of the three is found. **Every release configuration turns it on, with all three backends**: SQLite is compiled in from source, and MySQL/PostgreSQL are in the package but loaded at run time (next rows). See [`doc/db-guide.md`](doc/db-guide.md) |
+| `UVCPP_ENABLE_DB` | `OFF` | Enable the **database module** (`src/db/`): one `uvcpp_db_client` per connection over SQLite / MySQL / PostgreSQL / ODBC, with table-style access to the result rows. It is the only module that **requires a third-party client library** (libsqlite3 / libmysqlclient / libpq / an ODBC driver manager) — force-disabled when none of the four is found. **Every release configuration turns it on, with all three client-library backends**: SQLite is compiled in from source, and MySQL/PostgreSQL are in the package but loaded at run time (next rows); whether the ODBC backend is in a given package depends on whether that build machine had `sql.h` (next-next row). See [`doc/db-guide.md`](doc/db-guide.md) |
| `UVCPP_ENABLE_DB_SQLITE` | `ON` | Build the SQLite backend of the db module (needs `sqlite3.h` + libsqlite3). Not found ⇒ **this one backend is force-disabled with a warning**; the others still build |
| `UVCPP_DB_SQLITE_FROM_SOURCE` | `OFF` | Build that SQLite backend from a **hash-pinned source amalgamation** (one static `sqlite3.c`, forced PIC) instead of `find_package(SQLite3)`. **The six release legs use it**: linking the system `libsqlite3.so` would add a `DT_NEEDED` and break the packages' self-contained promise, while the system `libsqlite3.a` on Ubuntu 24.04 is not PIC (verified: `R_X86_64_PC32 … can not be used when making a shared object`). Needs network at configure time, or the zip placed by hand; both are described in [`doc/db-guide.md`](doc/db-guide.md) |
| `UVCPP_ENABLE_DB_MYSQL` | `ON` | Build the MySQL backend (needs `mysql.h` + libmysqlclient). Non-standard installs: `-DCMAKE_PREFIX_PATH=…` or `-DUVCPP_DB_MYSQL_INCLUDE_DIR=… -DUVCPP_DB_MYSQL_LIBRARY=…`. **On by default, and on in every release package** (with `UVCPP_DB_RUNTIME_LOAD=ON`, which is what keeps the package loadable without libmysqlclient) |
| `UVCPP_ENABLE_DB_PGSQL` | `ON` | Build the PostgreSQL backend (needs `libpq-fe.h` + libpq). Non-standard installs: `-DCMAKE_PREFIX_PATH=…` or `-DPostgreSQL_INCLUDE_DIR=… -DPostgreSQL_LIBRARY=…`. **On by default, and on in every release package** (with `UVCPP_DB_RUNTIME_LOAD=ON`) |
+| `UVCPP_ENABLE_DB_ODBC` | `ON` | Build the ODBC backend of the db module (needs `sql.h` + a driver manager: `libodbc` on Linux, the system `odbc32` on Windows). Unlike the three above, it targets **an interface, not one database** — *what* it connects to is chosen by the connection string's `Driver=` / `DSN=` (Microsoft ships no linkable C client library for SQL Server; `msodbcsql18` *is* an ODBC driver), and it tries no other driver name. Not found ⇒ **this one backend is force-disabled with a warning**; the others still build. It deliberately has **no `UVCPP_DB_RUNTIME_LOAD` path**: the driver manager is expected to be present and is small, and the *actual* driver under it is loaded on demand by the manager, which is not our business. So "is this backend built" and "is there a driver for the data source you open" are two different questions — the second is answered at `open()`, in the driver manager's own words |
| `UVCPP_DB_RUNTIME_LOAD` | `OFF` | Take the MySQL / PostgreSQL symbols from a client library loaded **at run time** (`dlopen` / `LoadLibrary`) instead of linking it. **The six release legs use it, and the reason is the packages' users, not the library**: `DT_NEEDED` (the PE import table on Windows) is resolved at **load time**, so linking libmysqlclient/libpq means a machine that has never installed either cannot even *load* the library — even if it only ever runs HTTP and never touches `db`. With this on, such a machine runs everything except "actually connecting to MySQL/PG", where it gets `uvcpp_db_status::NO_DRIVER` plus a message naming the candidates that were tried (`UVCPP_DB_MYSQL_LIBRARY` / `UVCPP_DB_PGSQL_LIBRARY` override the search). It changes only *where the symbols come from*, not *which backends exist*: `mysql.h` / `libpq-fe.h` are still needed at configure time (`MYSQL_BIND`'s layout has to come from the real header), so `UVCPP_DB_MYSQL_ENABLE` / `UVCPP_DB_PGSQL_ENABLE` still mean "the header was found". Default `OFF` leaves today's behaviour byte-for-byte. See [`doc/db-guide.md`](doc/db-guide.md) |
| `UVCPP_USE_SYSTEM_LIBUV` | `ON` | Prefer system-installed libuv |
| `UVCPP_BUILD_LIBUV_FROM_SOURCE` | `OFF` | Fetch and build libuv from source via `FetchContent` |
diff --git a/README.zh.md b/README.zh.md
index 09d56bb7..faa76e47 100644
--- a/README.zh.md
+++ b/README.zh.md
@@ -2,7 +2,7 @@
-[](./RELEASE.md)
+[](./RELEASE.md)
[](./LICENSE)
[](https://github.com/Antruly/libuvcpp/actions/workflows/ci-linux-ubuntu.yml)
[](https://github.com/Antruly/libuvcpp/actions/workflows/ci-windows-msvc.yml)
@@ -14,7 +14,7 @@
🔧 基于 [libuv](https://github.com/libuv/libuv) 的现代 C++11 封装库 — 面向对象的异步 I/O,
支持双模式(异步回调/同步等待)、HTTP/1.1、WebSocket(RFC 6455)和 SSL/TLS。
-- **版本**:`1.7.0` — **作者**:`zhuweiye` — **许可证**:`MIT`
+- **版本**:`1.7.1-dev` — **作者**:`zhuweiye` — **许可证**:`MIT`
- **语言**:[English](./README.md) · [中文](./README.zh.md)
## 下载
@@ -302,11 +302,12 @@ WebSocket,`-DUVCPP_ENABLE_OPENSSL=ON` 是 HTTPS,`-DUVCPP_BUILD_WEBAPP=ON`
| `UVCPP_ENABLE_BROTLI` | `OFF` | 启用 **brotli 响应体编码器** —— 除 gzip/deflate 之外多一种 `Content-Encoding: br`,由同一套 `Accept-Encoding` 协商选出来(`q` 相同时 br 胜出,因为它更小;`*` 与"没编 zlib"的那种构建也各有断言)。**它换到的是线上字节数,不是速度**:默认质量下同一份响应体比 gzip 6 档小约 11 %,而压缩变体是从响应缓存里取的,所以每请求代价与 gzip 相同 —— 不填 cache tag 的条目会每请求重压一遍。默认质量是 `5`(brotli 自己的默认值),**故意不是 `0`**。**关掉时逐字不变**(协商里没有 `br`;库的导出符号集与 `DT_NEEDED` 都不动)。brotli 是**私有**依赖:系统里有就走 pkg-config/`find_library`,没有就走 `FetchContent` 取源码(tag 钉在 `BROTLI_VERSION`)—— **两个**静态库,所以静态库使用者要自己链 `-lbrotlienc -lbrotlidec`。**1.7.0 起它进了发布包** —— 每条发布腿都传 `UVCPP_BROTLI_FROM_SOURCE=ON`,产物才自包含 —— 另有五个 CI 作业真的开着它:Ubuntu 的 `brotli` 与 `brotli-no-zlib`、macOS、MSVC 四个矩阵条目,外加 `ci-mingw64.yml` 那条单 job。见 [`doc/web-http-guide.md`](doc/web-http-guide.md) |
| `UVCPP_BROTLI_FROM_SOURCE` | `OFF` | 跳过整个系统查找(`find_package(PkgConfig)` → `find_path`/`find_library`),直接用 `BROTLI_VERSION` 钉住的那份源码编 brotli,静态链进来。**查找顺序是先系统后源码**,所以在碰巧装了 `libbrotli-dev` 的机器上,"链的是静态 `.a`"会**静默**变成三条 `DT_NEEDED`(`libbrotlienc` / `libbrotlidec` / `libbrotlicommon`)—— 而且不会有任何断言变红,因为发布流程那条 `ldd` 黑名单里并没有 brotli。凡是打开 brotli 的发布腿都传它,产物才自包含。跳过是**连发现都不调用**(`find_*` 的结果会写进 CMake cache,调用过一次就会把上一轮的答案漏进同一棵树的下一轮配置)。默认 `OFF`,今天的行为逐字节不变。见 [`doc/web-http-guide.md`](doc/web-http-guide.md) |
| `UVCPP_ENABLE_CAPI` | `OFF` | 导出 **C ABI**(`src/capi/`,C99 头,给 C#/P-Invoke 以及别的 FFI 用),编进**同一个** `uvcpp` 库 —— 不多一个产物。需要 `UVCPP_BUILD_NET=ON` 与 `UVCPP_BUILD_WEB=ON`,缺一即强制关闭(C 面横跨 net/web/webapp)。**每一个发布配置都打开它。** 见 [`doc/capi-guide.md`](doc/capi-guide.md) |
-| `UVCPP_ENABLE_DB` | `OFF` | 启用**数据库模块**(`src/db/`):一个连接一个 `uvcpp_db_client`,底下是 SQLite / MySQL / PostgreSQL 三个后端,结果行按表的方式取。它是全仓**唯一一个必需第三方客户端库**(libsqlite3 / libmysqlclient / libpq)的模块 —— 三个都找不到就强制关闭。**每一个发布配置都打开它,而且三个后端都在**:SQLite 从源码编进来,MySQL/PostgreSQL 也在包里、但走运行时加载(见下面两行)。见 [`doc/db-guide.md`](doc/db-guide.md) |
+| `UVCPP_ENABLE_DB` | `OFF` | 启用**数据库模块**(`src/db/`):一个连接一个 `uvcpp_db_client`,底下是 SQLite / MySQL / PostgreSQL / ODBC 四个后端,结果行按表的方式取。它是全仓**唯一一个必需第三方客户端库**(libsqlite3 / libmysqlclient / libpq / 一份 ODBC 驱动管理器)的模块 —— 四个都找不到就强制关闭。**每一个发布配置都打开它,而且三个客户端库后端都在**:SQLite 从源码编进来,MySQL/PostgreSQL 也在包里、但走运行时加载(见下面两行);ODBC 那个在不在包里,取决于出包那台机器有没有 `sql.h`(见再下面一行)。见 [`doc/db-guide.md`](doc/db-guide.md) |
| `UVCPP_ENABLE_DB_SQLITE` | `ON` | 编 db 模块的 SQLite 后端(要 `sqlite3.h` 与 libsqlite3)。找不到时**只强制关掉这一个**并出声,其余后端照编 |
| `UVCPP_DB_SQLITE_FROM_SOURCE` | `OFF` | 这个 SQLite 后端改成用一份**钉死哈希的源码 amalgamation** 自己编(一个静态 `sqlite3.c`,强制 PIC),不走 `find_package(SQLite3)`。**六条发布腿走的就是它**:链系统的 `libsqlite3.so` 会多一条 `DT_NEEDED`、破坏发布包的"自包含"承诺,而 Ubuntu 24.04 上系统的 `libsqlite3.a` 不是 PIC(实测 `R_X86_64_PC32 … can not be used when making a shared object`)。配置期需要联网,或者自己把 zip 放到位 —— 两种做法都写在 [`doc/db-guide.md`](doc/db-guide.md) 里 |
| `UVCPP_ENABLE_DB_MYSQL` | `ON` | 编 MySQL 后端(要 `mysql.h` 与 libmysqlclient)。装在非标准位置时:`-DCMAKE_PREFIX_PATH=…` 或 `-DUVCPP_DB_MYSQL_INCLUDE_DIR=… -DUVCPP_DB_MYSQL_LIBRARY=…`。**默认就是开的,每个发布包里也都开着**(同时用 `UVCPP_DB_RUNTIME_LOAD=ON`,这正是"没装 libmysqlclient 也能加载这份包"的原因) |
| `UVCPP_ENABLE_DB_PGSQL` | `ON` | 编 PostgreSQL 后端(要 `libpq-fe.h` 与 libpq)。装在非标准位置时:`-DCMAKE_PREFIX_PATH=…` 或 `-DPostgreSQL_INCLUDE_DIR=… -DPostgreSQL_LIBRARY=…`。**默认就是开的,每个发布包里也都开着**(同时用 `UVCPP_DB_RUNTIME_LOAD=ON`) |
+| `UVCPP_ENABLE_DB_ODBC` | `ON` | 编 db 模块的 ODBC 后端(要 `sql.h` 与一份驱动管理器:Linux 上是 `libodbc`,Windows 上是系统的 `odbc32`)。它与上面三个不是一类:那三个各对着**一种**数据库,这个对着**一个接口** —— 连什么库由连接串里的 `Driver=` / `DSN=` 决定(微软没给 SQL Server 可链接的 C 客户端库,`msodbcsql18` 本身就是 ODBC 驱动),而且它不去试别的驱动名。找不到时**只强制关掉这一个**并出声,其余后端照编。它**故意没有** `UVCPP_DB_RUNTIME_LOAD` 那条路:驱动管理器本来就该在、而且很小,真正按需加载的是它下面的**具体驱动**,那件事由驱动管理器自己干。于是"这个后端编没编"与"你要连的数据源有没有驱动"是两件事 —— 后者到 `open()` 才有答案,用的是驱动管理器的原话。见 [`doc/db-guide.md`](doc/db-guide.md) |
| `UVCPP_DB_RUNTIME_LOAD` | `OFF` | MySQL / PostgreSQL 的符号不从链接行上来,改成**运行时**打开一份客户端库(`dlopen` / `LoadLibrary`)再取。**六条发布腿用的就是它,而理由在包的**使用者**身上,不在库里**:`DT_NEEDED`(Windows 上是导入表)是**加载期**解析的,链上 libmysqlclient/libpq 就等于"从没装过这两种客户端库的机器连**加载**这份库都做不到"—— 哪怕它只跑 HTTP、一行 `db` 代码都不碰。开了之后,那种机器上除"真去连 MySQL/PG"之外一切照常,而真去连时拿到的是 `uvcpp_db_status::NO_DRIVER` 加一句写清了试过哪些库名的话(可用环境变量 `UVCPP_DB_MYSQL_LIBRARY` / `UVCPP_DB_PGSQL_LIBRARY` 指定路径)。它改的只是**符号从哪来**,不是**有哪些后端**:配置期仍然要 `mysql.h` / `libpq-fe.h`(`MYSQL_BIND` 的布局必须从真的头里读),所以 `UVCPP_DB_MYSQL_ENABLE` / `UVCPP_DB_PGSQL_ENABLE` 的含义照旧是"头找到了"。默认 `OFF`,今天的行为逐字节不变。见 [`doc/db-guide.md`](doc/db-guide.md) |
| `UVCPP_USE_SYSTEM_LIBUV` | `ON` | 优先使用系统安装的 libuv |
| `UVCPP_BUILD_LIBUV_FROM_SOURCE` | `OFF` | 用 `FetchContent` 拉取并源码构建 libuv |
diff --git a/RELEASE.md b/RELEASE.md
index 8a388c7b..2dc7b4a1 100644
--- a/RELEASE.md
+++ b/RELEASE.md
@@ -1146,11 +1146,11 @@ g++ -std=c++11 -g -I include your_app.cpp -L lib -luvcppd -o your_app.exe
引入,它登记的 install 规则引用了一个从未构建的 `libuv.dll`,且它的规则排在
本项目的规则之前 —— 一失败就整体中止,本项目的头文件与库一个都装不出来
(Linux 上没有这个 `.dll`,同一条 install 实测走得通,2026-10-12 验过)。
- 本次的 zip 绕过它、照 `CMakeLists.txt:2870-3176` 的规则手工组装,与 install
+ 本次的 zip 绕过它、照 `CMakeLists.txt:2918-3224` 的规则手工组装,与 install
只剩**一处**刻意的差异:**libuv 的头放在 `include/` 顶层**(本库的公开头写的是
`#include `,放进 `include/libuv/` 会找不到)。原先还有第二处 —— zip 里
补了 `zlib.h` / `zconf.h`,而 install 规则里没有这一条 —— **2026-10-12 起
- install 规则自己也装这两个头**(`CMakeLists.txt:3156-3159`),两边落到同一个
+ install 规则自己也装这两个头**(`CMakeLists.txt:3204-3207`),两边落到同一个
位置,这处差异消失。
### MSVC 从源码树 / 安装树取头时要加 `/utf-8` —— 头文件侧已修(1.2.0)
diff --git a/cmake/uvcpp_config.h.in b/cmake/uvcpp_config.h.in
index 550a66ae..dbdad204 100644
--- a/cmake/uvcpp_config.h.in
+++ b/cmake/uvcpp_config.h.in
@@ -155,6 +155,18 @@
# define UVCPP_DB_PGSQL_ENABLE @UVCPP_DB_PGSQL_ENABLE@
#endif
+/* ODBC 后端。注意它与上面三个不是一类:SQLite/MySQL/PostgreSQL 各对着**一种**
+ 数据库,而这个对着**一个接口** —— 连什么库由连接串里的 `Driver=`(或 `DSN=`)
+ 决定,本驱动不去试别的驱动名。所以"这个后端在不在"与"要连的那个数据源有没有
+ 驱动"是两件事:后者要到 `open()` 才知道,报错是驱动管理器给的原文(驱动名写错
+ 时就是它那句"找不到数据源、也没有默认驱动")。 */
+#if defined(UVCPP_DB_ODBC_ENABLE) && (UVCPP_DB_ODBC_ENABLE != @UVCPP_DB_ODBC_ENABLE@)
+# error "UVCPP_DB_ODBC_ENABLE=@UVCPP_DB_ODBC_ENABLE@ in this build; remove your own definition of it"
+#endif
+#ifndef UVCPP_DB_ODBC_ENABLE
+# define UVCPP_DB_ODBC_ENABLE @UVCPP_DB_ODBC_ENABLE@
+#endif
+
/* MySQL / PostgreSQL 两个后端的符号是**链接进来的**还是**运行时取**的。
它是"这份产物长什么样",不是"有哪些模块",但同样必须让使用者看得见:这两条
路的**失败方式**不一样(链接的那条在加载期就报缺库、进程起不来;运行时加载的
diff --git a/doc/ci-guide.md b/doc/ci-guide.md
index 9bd81a35..c870862a 100644
--- a/doc/ci-guide.md
+++ b/doc/ci-guide.md
@@ -278,7 +278,7 @@ removed on 2026-09-17 after measuring them instead of trusting the label:
| `test_memory_pool` | "Pre-existing hang (multi-thread pool alloc on Windows)" | 0 failures, ≤1 s per run |
Both had been exclusions for defects fixed long before. `test_memory_pool`'s is
-documented: it was a missing-DLL-copy bug (see `CMakeLists.txt:3217-3217`), fixed and
+documented: it was a missing-DLL-copy bug (see `CMakeLists.txt:3265-3265`), fixed and
left in the exclude list anyway. `test_tcp_func`'s dual-loop teardown is most
likely the `~uvcpp_tcp_server` fix, which is what removed the two `sleep_for`
calls that were joining the worker thread — that is an inference from the
@@ -454,7 +454,7 @@ MSVC. MinGW and macOS are the gap (see §1).
entry at all and shipped two classes of defect (#35).
- **It is `web` plus one switch**, exactly as `zlib-off` is `web` minus one, and its dep list
is **byte-identical to `web`'s**. That is deliberate: `pugixml` is pulled by `FetchContent`
- (`CMakeLists.txt:1667`, `GIT_TAG` pinned to `PUGIXML_VERSION`), not by a package manager, so
+ (`CMakeLists.txt:1713`, `GIT_TAG` pinned to `PUGIXML_VERSION`), not by a package manager, so
there is nothing to install — and keeping the package set identical means a red/green
difference between the two legs can only be attributed to the switch itself.
- **All four gates are load-bearing here.** Gate ② is `pugixml integrated` (the FetchContent
@@ -501,7 +501,7 @@ table lost its `MinGW + yyjson` row rather than gaining a row here).
compiles this header with **libc++**, whose `double` formatting is not libstdc++'s. The
byte-identical write-back assertion is precisely the kind that differs across those two.
- **`flags` is `web` plus one switch, and the dep list is byte-identical to `web`'s on all
- three legs.** `yyjson` is pulled by `FetchContent` (`CMakeLists.txt:1727`, `GIT_TAG` pinned
+ three legs.** `yyjson` is pulled by `FetchContent` (`CMakeLists.txt:1773`, `GIT_TAG` pinned
to `YYJSON_VERSION`) and built as an OBJECT library, not found by a package manager, so
there is nothing to install — and keeping the package set identical means a red/green
difference can only be attributed to the switch.
diff --git a/doc/expand-guide.md b/doc/expand-guide.md
index 95e7efbc..0d9fc2cb 100644
--- a/doc/expand-guide.md
+++ b/doc/expand-guide.md
@@ -95,18 +95,18 @@ cmake -S . -B build -DUVCPP_BUILD_EXPAND=ON
| `-DUVCPP_BUILD_EXPAND=ON` | `UVCPP_ENABLE_MEMORY_POOL=1` |
| `-DUVCPP_BUILD_EXPAND=OFF`(默认) | `UVCPP_ENABLE_MEMORY_POOL=0` |
-选项到宏的映射在 `CMakeLists.txt:1544-1550` 一处完成。用预编译包时**什么都不用传** ——
+选项到宏的映射在 `CMakeLists.txt:1590-1596` 一处完成。用预编译包时**什么都不用传** ——
包里的 `include/uvcpp/uvcpp_config.h` 就是那次构建的值,自己再传一个冲突的值是硬 `#error`。
关掉时 `src/expand/` 整目录被 `list(FILTER ... EXCLUDE REGEX "src/expand/")` 排除
-(`CMakeLists.txt:1907-1916`)。于是"关掉之后包含它"的表现取决于**你的头是怎么来的**
+(`CMakeLists.txt:1953-1962`)。于是"关掉之后包含它"的表现取决于**你的头是怎么来的**
—— 而能不能编过,只由"那个头在不在你的 include 路径里"决定:
| 你怎么构建 | `#include ` 的结果 |
|---|---|
| 源码树自己构建(没开开关) | **编得过,链接失败** —— 头还在 `src/expand/` 下,且四个头里**没有任何一个用宏守卫自己**(唯一提到这个宏的地方是 `src/expand/uvcpp_page_heap.h:8` 的一句注释),没有 `#error` 兜底;但符号一个都没编进库 |
| 用**官方预编译包** | **什么都不用传,直接用** —— 六条发布腿的十处 configure 都传 `-DUVCPP_BUILD_EXPAND=ON`(`release.yml` 里十处都有),包里那个 `uvcpp_config.h` 的 `UVCPP_ENABLE_MEMORY_POOL` 是 **1**,`include/expand/` 那四个头也在。上面那句"用预编译包什么都不用传"在这里是**字面意思** |
-| 自己 `cmake --install` 到一个前缀(没开开关) | **编译期就报找不到头** —— 这条路按开关装头(`CMakeLists.txt:2920-2923`),`include/expand/` 不会被装出去 |
+| 自己 `cmake --install` 到一个前缀(没开开关) | **编译期就报找不到头** —— 这条路按开关装头(`CMakeLists.txt:2968-2971`),`include/expand/` 不会被装出去 |
| 自己跑 `package_release.py` 打的包(没开开关) | 与第一行同 —— **编得过、链接失败**。它拷头是按 `MODULES` 逐个目录拷的(`tests/tools/package_release.py`),**不看这个开关**,所以 `include/expand/` 照样在包里;但包里那个宏是 0、库里也没有那些符号 |
> **别把第三、四行记成一回事**:`cmake --install` 与 `package_release.py` 是两套独立的
diff --git a/doc/grpc-guide.md b/doc/grpc-guide.md
index 778c44f1..e53a6ef1 100644
--- a/doc/grpc-guide.md
+++ b/doc/grpc-guide.md
@@ -10,11 +10,11 @@
这个分法不是洁癖,是**依赖事实**:编解码那半的每一个参数都是 `std::string` / `uint32_t`
(`src/grpc/uvcpp_grpc.h:100-194`),而服务端那半的每个参数都是 web 层的类型
(`src/grpc/uvcpp_grpc_server.h:116-132`)。所以关掉 web 模块时前者照编照装、后者
-**从构建与安装里都摘掉**(`CMakeLists.txt:1951-1953`、`CMakeLists.txt:2939-2944`)——
+**从构建与安装里都摘掉**(`CMakeLists.txt:1997-1999`、`CMakeLists.txt:2987-2992`)——
装在 `include/grpc/` 下,而不是 `include/web/` 下。
- 打开方式:**没有开关**。编解码那半任何配置下都在;服务端那半跟着 `UVCPP_BUILD_WEB`
- 走(`CMakeLists.txt:1951-1951`)。加一个 `UVCPP_ENABLE_GRPC` 只会多一个能与
+ 走(`CMakeLists.txt:1997-1997`)。加一个 `UVCPP_ENABLE_GRPC` 只会多一个能与
`UVCPP_BUILD_WEB` 打架的旋钮,而两半的依赖是死的、不需要调用方表态。
- 包含方式:``、``。
- 依赖:帧与 varint 那半**只依赖 `` 与 `libuv` 的错误码**(
diff --git a/doc/http2-guide.md b/doc/http2-guide.md
index 2ce3cbf0..04e11fe8 100644
--- a/doc/http2-guide.md
+++ b/doc/http2-guide.md
@@ -11,14 +11,14 @@
`web/` 的两条线(`uvcpp_http_server` / `uvcpp_http_client`)各有一份接好的实现可以照抄,
本页在 [§5](#5-服务端怎么回一条响应) 与 [§6](#6-客户端怎么发一条请求) 给了它们的调用序列。
-- 打开方式:`-DUVCPP_ENABLE_NGHTTP2=ON`。**默认 OFF**(`CMakeLists.txt:236-236`),
+- 打开方式:`-DUVCPP_ENABLE_NGHTTP2=ON`。**默认 OFF**(`CMakeLists.txt:245-245`),
而且缺 OpenSSL 或缺 web 模块时会被**强制**置 OFF 并打 warning
- (`CMakeLists.txt:595-595`、`CMakeLists.txt:604-604`)—— 因为 `uvcpp_h2_stream`
+ (`CMakeLists.txt:604-604`、`CMakeLists.txt:613-613`)—— 因为 `uvcpp_h2_stream`
里直接放 `uvcpp_http_request` / `uvcpp_http_response`
(`src/http2/uvcpp_h2_session.h:70`)。
- 包含方式:``、``、
``。私有的 `` **不安装**
- (`CMakeLists.txt:3029-3029`)。
+ (`CMakeLists.txt:3077-3077`)。
> 本页讲**怎么用**。它与 [HTTP/2 支持现状](./http2-status.md) 分工:那篇讲
> **实现进度、写死的折衷、已知缺口**,本页不重复那些,需要时直接链过去。
@@ -72,12 +72,12 @@ int drain(std::string& out, size_t max_bytes = 0); // src/http2/u
## 2. 编译期条件
`UVCPP_NGHTTP2_ENABLE` **在任何构建里都有定义**,只是值不同:ON ⇒ `1`,OFF ⇒ `0`
-(`CMakeLists.txt:2307-2307`)。三个公开头把**全部内容**包在 `#if UVCPP_NGHTTP2_ENABLE` 里
+(`CMakeLists.txt:2353-2353`)。三个公开头把**全部内容**包在 `#if UVCPP_NGHTTP2_ENABLE` 里
(`src/http2/uvcpp_h2_common.h:19`、`src/http2/uvcpp_h2_session.h:33`、`src/http2/uvcpp_h2_connection.h:27`)。
为 0 时:头**还在**(源码树里能 include、也编得过),但里面**一个类型都不声明** ——
写 `uvcpp_h2_session s(true);` 会"未定义类型"。安装出来的包里则**根本没有这几个头**
-(`CMakeLists.txt:3029-3029` 只在开关 ON 时装)。
+(`CMakeLists.txt:3077-3077` 只在开关 ON 时装)。
所以使用者的写法是把自己的那段包起来:
diff --git a/doc/http2-status.md b/doc/http2-status.md
index 88d16a6c..c6d85993 100644
--- a/doc/http2-status.md
+++ b/doc/http2-status.md
@@ -82,13 +82,13 @@
一份更权威的 ALPN 名单 —— 设过就不覆盖。
- **关掉 nghttp2 也能编**:`http2/*.h` 不出现在任何公开头里,`src/web/uvcpp_http_client.h:43-45`
写明了理由(引了就把 nghttp2 的 include 路径扩散给每个使用者与测试 TU)。
- `UVCPP_ENABLE_NGHTTP2` 默认 OFF(`CMakeLists.txt:236-236`),缺 OpenSSL 或缺 web
+ `UVCPP_ENABLE_NGHTTP2` 默认 OFF(`CMakeLists.txt:245-245`),缺 OpenSSL 或缺 web
模块时还会被强制置 OFF 并告警。
### 1.4 依赖与产物
`nghttp2` 是**唯一被强制静态**的依赖:`uvcpp_nghttp2_configure()`
-(`CMakeLists.txt:626-626`,在 function 作用域里 `BUILD_STATIC_LIBS ON`)把一个
+(`CMakeLists.txt:635-635`,在 function 作用域里 `BUILD_STATIC_LIBS ON`)把一个
静态 `nghttp2_static` 交给链接器。实测 `pe_imports()`:开了 h2 的 DLL 与没开的
相比,非白名单依赖集合**都是** `llhttp.dll uv.dll zlib1.dll` —— 开 h2 不新增
任何 DLL 依赖。
@@ -377,7 +377,7 @@ ctest --test-dir build-h2 -C Release --timeout 60
- `copy_test_dlls` 是 `add_custom_target(... ALL ...)`,命令全是
`${CMAKE_COMMAND} -E copy_if_different`,**一个 `pwsh` 都不沾**,构建日志里
"Copy uvcpp/libuv DLLs into test folders" 那行就是它。它确实会跑。真正
- `0xc0000135` 满屏的根因是这张目标**曾经没带 `ALL`**(见 `CMakeLists.txt:3325-3325`
+ `0xc0000135` 满屏的根因是这张目标**曾经没带 `ALL`**(见 `CMakeLists.txt:3373-3373`
的注释),与 `pwsh` 无关。
- 日志里那句 `'pwsh.exe' 不是内部或外部命令` 来自 **vcpkg 的
`scripts/buildsystems/msbuild/vcpkg.targets`**(`applocal.ps1`),而且它自己
diff --git a/doc/http3-guide.md b/doc/http3-guide.md
index 5b64b45a..e6a8ac0b 100644
--- a/doc/http3-guide.md
+++ b/doc/http3-guide.md
@@ -24,18 +24,18 @@
- 打开方式:`-DUVCPP_ENABLE_HTTP3=ON`。**默认 OFF**(`CMakeLists.txt:143-143`),
而且下面两种情况会被**强制**置 OFF 并打 warning,而不是留一个"能配置、链不上、
跑不起来"的组合:
- 1. 没开 QUIC(`CMakeLists.txt:960-960`)—— HTTP/3 是 QUIC 之上的一条应用协议,
+ 1. 没开 QUIC(`CMakeLists.txt:969-969`)—— HTTP/3 是 QUIC 之上的一条应用协议,
没有 QUIC 它没有字节可以跑在上面;
- 2. 没开 web 层(`CMakeLists.txt:967-967`)—— 这条传输的公开面就在
+ 2. 没开 web 层(`CMakeLists.txt:976-976`)—— 这条传输的公开面就在
`uvcpp_http_client` / `uvcpp_http_server` 上,web 关掉时它没有使用者。
- 包含方式:``、``。
私有的 `` 与 `` **都不安装**
- (`CMakeLists.txt:3070-3070`)—— 理由见 [§7](#7-典型坑) 第一条。
+ (`CMakeLists.txt:3118-3118`)—— 理由见 [§7](#7-典型坑) 第一条。
- 两个公开头整段套在 `#if UVCPP_HTTP3_ENABLE` 里,web 层那边新加的
`listen_quic()` / `set_http3_enabled()` 也是,所以**不开关就一个名字都看不到**。
这与 `quic/`、`web/`、`ssl/`、`http2/`、`wsdl/` 同档。
-- 开关与依赖:nghttp3 走 FetchContent(`NGHTTP3_VERSION`,`CMakeLists.txt:250-250`),
- 静态链入 `nghttp3_static`(`CMakeLists.txt:2732-2732`)—— 详见
+- 开关与依赖:nghttp3 走 FetchContent(`NGHTTP3_VERSION`,`CMakeLists.txt:259-259`),
+ 静态链入 `nghttp3_static`(`CMakeLists.txt:2780-2780`)—— 详见
[§5](#5-cmake-接线与那条-check-撞名的坑)。
> 本指南里的签名、默认值、行为都对着当前源码核过。凡是"这一版没做"的地方都明确
@@ -360,14 +360,14 @@ void connect_over_http3(uvcpp_http_client& cli, int udp_port) {
## 5. CMake 接线与那条 `check` 撞名的坑
1. `option(UVCPP_ENABLE_HTTP3 ...)` 挨着 QUIC 那个(`CMakeLists.txt:143-143`),
- **默认 OFF**;两条守卫(`CMakeLists.txt:960-960`、`CMakeLists.txt:967-967`)用普通变量 `set(... OFF)` 降级并出声 ——
+ **默认 OFF**;两条守卫(`CMakeLists.txt:969-969`、`CMakeLists.txt:976-976`)用普通变量 `set(... OFF)` 降级并出声 ——
照 QUIC 那套。**别只 grep `CMakeCache.txt`**:降级不是 cache 写,所以 cache 里
仍然是 `UVCPP_ENABLE_HTTP3:BOOL=ON`,一个"被降级了"的配置和一个"真开了"的配置
在 cache 那一层长得一模一样。配置成功的标志是这行 STATUS:
```
- nghttp3 integrated (tag=v1.18.0, static) # CMakeLists.txt:1342-1342
- Including http3 module in build (nghttp3 v1.18.0) # CMakeLists.txt:2023-2023
+ nghttp3 integrated (tag=v1.18.0, static) # CMakeLists.txt:1388-1388
+ Including http3 module in build (nghttp3 v1.18.0) # CMakeLists.txt:2069-2069
```
2. **依赖用 `FetchContent_Declare` + `Populate` + `add_subdirectory(... EXCLUDE_FROM_ALL)`,
@@ -385,17 +385,17 @@ void connect_over_http3(uvcpp_http_client& cli, int udp_port) {
要删掉 `build*/_deps/uvcpp-nghttp3-src/` 再 configure。
4. **后置断言两条**:`if(NOT TARGET nghttp3_static)` FATAL
- (`CMakeLists.txt:1330-1330`;**裸目标名,没有 ALIAS**,别去猜
+ (`CMakeLists.txt:1376-1376`;**裸目标名,没有 ALIAS**,别去猜
`nghttp3::nghttp3_static`),以及把 `POSITION_INDEPENDENT_CODE` 读回来断言
- (`CMakeLists.txt:1265-1265`)—— 后者是"Linux 上 `libuvcpp.so` 链接失败"这件事的成因。
+ (`CMakeLists.txt:1311-1311`)—— 后者是"Linux 上 `libuvcpp.so` 链接失败"这件事的成因。
5. **三处编译定义同步**:`cmake/uvcpp_config.h.in` 的 `UVCPP_HTTP3_ENABLE` 段、
目录级 `add_compile_definitions`、PUBLIC `target_compile_definitions`,
- 外加 `_uvcpp_literal01(UVCPP_HTTP3_ENABLE UVCPP_ENABLE_HTTP3)`(`CMakeLists.txt:2309-2309`)。
+ 外加 `_uvcpp_literal01(UVCPP_HTTP3_ENABLE UVCPP_ENABLE_HTTP3)`(`CMakeLists.txt:2355-2355`)。
四处不一致会以 `#error` 或静默的 ABI 错配收场,`check_config_contract.py` 盯着
其中一个方向。
-6. **链接是 PRIVATE**(`CMakeLists.txt:2732-2732` 的 `$`):
+6. **链接是 PRIVATE**(`CMakeLists.txt:2780-2780` 的 `$`):
nghttp3 的头路径不传播给使用者,所以**功能测试不能** `#include
`;想知道版本就用 `uvcpp_h3_connection::nghttp3_version()`
(`src/http3/uvcpp_h3_connection.h:287`),它经公开头调用、链接期解析。
@@ -488,7 +488,7 @@ h1 写泵、TCP 的 ALPN 列表与分支 —— **全部不改**。唯一的例
1. **别指望从公开 h3 头里看到 nghttp3。** `` 只出现在私有的
`src/http3/uvcpp_h3_nghttp3.h` 里,而它和持有 nghttp3 句柄的
- `src/http3/uvcpp_h3_session.h` **两个都不安装**(`CMakeLists.txt:3070-3070`)、打包
+ `src/http3/uvcpp_h3_session.h` **两个都不安装**(`CMakeLists.txt:3118-3118`)、打包
也排除。理由与 quic 那一对逐字相同:一是使用者不该被逼着去配 nghttp3 的搜索
路径才能 include 一个本库的头;二是 `uvcpp_h3_session.h` 的字段布局直接跟着
nghttp3 的版本走 —— 一旦漏进公开面,那个版本就变成了本库的 ABI。这处排除与
@@ -530,7 +530,7 @@ h1 写泵、TCP 的 ALPN 列表与分支 —— **全部不改**。唯一的例
9. **`h3_header` / `h3_response` 这类小件不能花括号初始化,这是 C++11 的硬规矩。**
它们有一格带默认成员初值(`kind = h3_header_kind::REGULAR`),而"C++11 里带
默认成员初值的类**不算聚合体**"—— 这条限制到 C++14 才放开。本库的编译标准是
- C++11(`CMakeLists.txt:252-252` 的 `set(CMAKE_CXX_STANDARD 11)`),所以
+ C++11(`CMakeLists.txt:261-261` 的 `set(CMAKE_CXX_STANDARD 11)`),所以
`h3_header{"accept", "*/*", …}` 在消费者那里编不过,报的是
`no matching function for call to 'h3_header::h3_header()'`。**逐格赋值**不会踩到它,本库自己的代码也是这么写的
diff --git a/doc/json-guide.md b/doc/json-guide.md
index 1caa87b0..88f63c8b 100644
--- a/doc/json-guide.md
+++ b/doc/json-guide.md
@@ -44,7 +44,7 @@
## 2. 编译期条件与包含
- **没有开关。** 它是核心模块,`UVCPP_ENABLE_NET` / `_WEB` / `_WEBAPP` 一个都不影响它;
- `CMakeLists.txt:1443-1443` 那个收 `src/uvcpp/` 下源文件的 GLOB 已经把它收进去了。
+ `CMakeLists.txt:1489-1489` 那个收 `src/uvcpp/` 下源文件的 GLOB 已经把它收进去了。
- **不需要 nlohmann、不需要 OpenSSL、不需要 zlib。** 头文件只带
`` 与 ``,实现只多带 ` `。
- **C++11**(本仓没有 per-target 覆盖),所以没有 `std::string_view`、没有 `if constexpr`。
diff --git a/doc/json-reflect-guide.md b/doc/json-reflect-guide.md
index 925432be..6450ef92 100644
--- a/doc/json-reflect-guide.md
+++ b/doc/json-reflect-guide.md
@@ -93,7 +93,7 @@ struct doc_thing {
```
两个头都**不在 `` 里**:那个聚合头只收 `uvcpp_define/version/export` 与
-`handle/loop/req` 三组。两个头都被 `CMakeLists.txt:1443-1443` 与 webapp 那份同形的 GLOB 自动
+`handle/loop/req` 三组。两个头都被 `CMakeLists.txt:1489-1489` 与 webapp 那份同形的 GLOB 自动
收进构建与安装,**不需要往任何清单里加一行**。
- **没有开关。** 写出侧不吃 `UVCPP_ENABLE_*` 任何一个;读入侧只吃它本来就依赖的
diff --git a/doc/quic-guide.md b/doc/quic-guide.md
index a70fbeef..d7af5219 100644
--- a/doc/quic-guide.md
+++ b/doc/quic-guide.md
@@ -31,16 +31,16 @@
- 打开方式:`-DUVCPP_ENABLE_QUIC=ON`。**默认 OFF**(`CMakeLists.txt:122-122`),而且
下面三种情况会被**强制**置 OFF 并打 warning,而不是留一个"能配置、链不上、
跑不起来"的组合:
- 1. 没开 OpenSSL(`CMakeLists.txt:721-721`)—— QUIC 建在 TLS 1.3 上,ALPN 是 TLS
+ 1. 没开 OpenSSL(`CMakeLists.txt:730-730`)—— QUIC 建在 TLS 1.3 上,ALPN 是 TLS
扩展,**没有明文 QUIC** 这回事;
- 2. 没开 net 层(`CMakeLists.txt:728-728`)—— QUIC 是 net 层的一条链路协议,并且复用
+ 2. 没开 net 层(`CMakeLists.txt:737-737`)—— QUIC 是 net 层的一条链路协议,并且复用
`src/net/uvcpp_net_read.h` 的读事件契约;
- 3. OpenSSL 找得到、但**不带 QUIC API**(`CMakeLists.txt:774-774`)—— 判据见
+ 3. OpenSSL 找得到、但**不带 QUIC API**(`CMakeLists.txt:783-783`)—— 判据见
[§2](#2-编译期条件一份带-quic-api-的-openssl--32)。
- 包含方式:``、``、
``、``。私有的
`` 与 `` **都不安装**
- (`CMakeLists.txt:3047-3047`)—— 理由见 [§7](#7-典型坑) 第一条。
+ (`CMakeLists.txt:3095-3095`)—— 理由见 [§7](#7-典型坑) 第一条。
- 四个公开头**全部**整段套在 `#if UVCPP_QUIC_ENABLE` 里,所以**不开关就一个类都
看不到**。这与 `web/`、`ssl/`、`http2/`、`http3/`、`wsdl/` 同档。
@@ -154,7 +154,7 @@ push 一例:本臂跨度 67、组间差 42),所以判据落在噪声里
Windows 且 `UVCPP_ENABLE_QUIC=ON` → `ON`,其余一律 `OFF`。非 Windows 上还有一层
**降级**:显式传 `-DUVCPP_ENABLE_UDP_GSO=ON` 也会被按回 `OFF`(配置期报一行 note),
所以这个宏在非 Windows 上**恒为 0、整段代码不编进去** —— 是"不生效",不是"编进来
-但是死的"。派生值必须算在 QUIC 的**静默降级链之后**(`CMakeLists.txt:785-785` 之后),
+但是死的"。派生值必须算在 QUIC 的**静默降级链之后**(`CMakeLists.txt:794-794` 之后),
否则一条"QUIC 已被降级掉"的配置会把 GSO 打开。
**两层闸门。**
@@ -215,10 +215,10 @@ libngtcp2 支持两个分支,判据写在它自己的 `CMakeLists.txt` 里,
| 上面那个没有、`SSL_set_quic_tls_cbs` 有 | mainline OpenSSL ≥ 3.2(`HAVE_OSSL`) | **`ngtcp2_crypto_ossl_static`** ← 我们链的是这个 |
| 两个都没有 | — | 一个都不建,configure 期 `FATAL_ERROR` |
-本库的探针**复刻**了这个顺序(`CMakeLists.txt:764-764`、`CMakeLists.txt:767-767`)。这不是
+本库的探针**复刻**了这个顺序(`CMakeLists.txt:773-773`、`CMakeLists.txt:776-776`)。这不是
仪式:只查 `SSL_set_quic_tls_cbs` 的探针会在 quictls 树上"通过",然后去链一个**从来
没被建出来**的目标。所以探针的结论是"没有 provide、但有 cbs"才放行
-(`CMakeLists.txt:774-774`);另外两格都强制关闭并出声。
+(`CMakeLists.txt:783-783`);另外两格都强制关闭并出声。
> **Ubuntu 24.04 自带的 3.0.13 不行。** 3.0/3.1 没有那两个符号中的任何一个。这不是
> 保守估计 —— 对着本机那份 `libssl.so.3` 量过:两个符号数都是 0。
@@ -253,8 +253,8 @@ nghttp2 / zlib / pugixml 的用法一致。
配置成功的标志是两行 `message(STATUS)`,CI 也正是 grep 这两行:
```
-ngtcp2 integrated (tag=v1.25.0, static) # CMakeLists.txt:942-942
-Including quic module in build (ngtcp2 v1.25.0) # CMakeLists.txt:2000-2000
+ngtcp2 integrated (tag=v1.25.0, static) # CMakeLists.txt:951-951
+Including quic module in build (ngtcp2 v1.25.0) # CMakeLists.txt:2046-2046
```
> **只 grep `CMakeCache.txt` 是不够的。** 三条降级用的都是**普通变量**
@@ -614,7 +614,7 @@ TCP 的 `PEER_CLOSED` 本来就是对端 FIN —— 于是这个字段对 TCP
1. **别指望从公开头里看到 ngtcp2。** `` 只出现在私有的
`src/quic/uvcpp_quic_ngtcp2.h` 里,而它和持有 ngtcp2 句柄的
- `src/quic/uvcpp_quic_session.h` **两个都不安装**(`CMakeLists.txt:3047-3047`)、打包
+ `src/quic/uvcpp_quic_session.h` **两个都不安装**(`CMakeLists.txt:3095-3095`)、打包
也被排除。理由有两条:一是使用者不该被逼着去配 ngtcp2 的搜索路径才能 include
一个本库的头;二是 `uvcpp_quic_session.h` 的成员里就有 `ngtcp2_conn*`、`SSL*`、
`ngtcp2_path_storage`,它的字段布局直接跟着 ngtcp2 的版本走 —— 一旦漏进公开面,
@@ -628,8 +628,8 @@ TCP 的 `PEER_CLOSED` 本来就是对端 FIN —— 于是这个字段对 TCP
调用,符号在链接期解析,照样证明 ngtcp2 真被链上。
3. **`src/quic/` 的源文件被 `list(FILTER … EXCLUDE REGEX "src/quic/")` 排除**
- (`CMakeLists.txt:1995-1995`,头文件那一条在 `:1597`),而且"开了 QUIC 但目录是空的"
- 会**当场 FATAL**(`CMakeLists.txt:2009-2009`)。后者防的是一棵树同时骗过三道看起来
+ (`CMakeLists.txt:2041-2041`,头文件那一条在 `:1597`),而且"开了 QUIC 但目录是空的"
+ 会**当场 FATAL**(`CMakeLists.txt:2055-2055`)。后者防的是一棵树同时骗过三道看起来
很像门禁的东西:cache 里 `QUIC=ON`、日志里有 `ngtcp2 integrated`、编译也过 ——
而 `src/quic/` 一个 `.cpp` 都没有,**零行 QUIC 代码被编译过**。"没测"必须表现为
**失败**,不是表现为**通过**。
diff --git a/doc/ssl-guide.md b/doc/ssl-guide.md
index ddf22a22..b1c914e4 100644
--- a/doc/ssl-guide.md
+++ b/doc/ssl-guide.md
@@ -65,7 +65,7 @@ TLS 在这库里是**过滤层**,不是独立的传输实现:装到 `uvcpp_t
或链接错误**,只会在运行时读到错位的垃圾。
关掉这个选项时:`src/ssl/` 会被从源文件与头文件列表里排除,**而且根本不安装 ssl 头**
-(`CMakeLists.txt:3019-3020`)——使用者连 `#include ` 都会
+(`CMakeLists.txt:3067-3068`)——使用者连 `#include ` 都会
找不到文件。同理,`uvcpp_tcp_server::set_ssl_context()` 在关掉时**连声明都不存在**。
运行时"关掉 TLS"不是宏,是 `set_ssl_context(nullptr)`。
diff --git a/doc/web-http-guide.md b/doc/web-http-guide.md
index 457ac201..4987267c 100644
--- a/doc/web-http-guide.md
+++ b/doc/web-http-guide.md
@@ -87,10 +87,10 @@
| HTTP/2 | `UVCPP_NGHTTP2_ENABLE` | `set_http2_enabled()` 那一组消失 |
两行"响应压缩"是**同一个块的两个来源**:压缩器的总开关是
-`UVCPP_HTTP_COMPRESS_ENABLE`,它由那两个宏**或**出来(`CMakeLists.txt:1819-1819`)——
+`UVCPP_HTTP_COMPRESS_ENABLE`,它由那两个宏**或**出来(`CMakeLists.txt:1865-1865`)——
任一为 1 就有这个块,两个都为 0 才整块消失。三个"块"的开关都默认 OFF,brotli
-那条也是(`CMakeLists.txt:222-236` 那一组 `option()`,四个块开关默认全是 OFF)。编译进 `UVCPP_SRC_FILES` 的是
-`src/web/*.cpp` 全体(`CMakeLists.txt:1457-1457`),但每个 `.cpp` 里的功能块同样带宏。
+那条也是(`CMakeLists.txt:231-245` 那一组 `option()`,四个块开关默认全是 OFF)。编译进 `UVCPP_SRC_FILES` 的是
+`src/web/*.cpp` 全体(`CMakeLists.txt:1503-1503`),但每个 `.cpp` 里的功能块同样带宏。
`uvcpp_http_server` **自己不碰 TLS** —— 它没有任何 SSL API。要上 https 必须
`get_tcp_server()->set_ssl_context(...)`,**而且必须在 `listen()` 之前**
@@ -730,7 +730,7 @@ zlib 编进来时**压缩默认开**(`src/web/uvcpp_http_server.h:1814-1814`
1 档是 65.4 → 42.8 µs、1 566 → 1 957 字节),所以默认值一个字节都没动。
**第六个旋钮:`br`(brotli)。** `UVCPP_ENABLE_BROTLI`(默认 OFF,
-`CMakeLists.txt:223-223`)给压缩器加上第二种编码:`br` 与 gzip/deflate 走**同一套**
+`CMakeLists.txt:232-232`)给压缩器加上第二种编码:`br` 与 gzip/deflate 走**同一套**
协商(`http_compress::parse_accept_encoding()`,`Accept-Encoding` 里 `q` 相同时
`br` > `gzip` > `deflate`,`*` 兜底、`br;q=0` 排除、指名要一个本构建编不出来的
编码时**原样发原文**而不是拿另一种编码的字节冒充 ——
diff --git a/doc/webapp-support-guide.md b/doc/webapp-support-guide.md
index 88eae9cb..6569a31e 100644
--- a/doc/webapp-support-guide.md
+++ b/doc/webapp-support-guide.md
@@ -72,7 +72,7 @@
**这七个头里没有一个用 `UVCPP_WEBAPP_ENABLE` 守卫自己。** 21 个 webapp 头
里只有 `src/webapp/uvcpp_web_ws_client.h:52` 一处(`#endif` 在 `:402`)用了这个宏。
所以"没开 webapp 却 include 了这七个头"是**链接期失败**(那几个 `.cpp` 没进构建,
-`CMakeLists.txt:1959-1960`),不是编译期被 `#if` 挡住。这与 `uvcpp_web_ws_client.h`
+`CMakeLists.txt:2005-2006`),不是编译期被 `#if` 挡住。这与 `uvcpp_web_ws_client.h`
的做法不一致 —— 看上去是历史的,不是设计。
头在包里的位置是平铺的 `include/webapp/`:
diff --git a/src/db/uvcpp_db_driver.h b/src/db/uvcpp_db_driver.h
index bc415c47..9bb19b17 100644
--- a/src/db/uvcpp_db_driver.h
+++ b/src/db/uvcpp_db_driver.h
@@ -123,6 +123,10 @@ class uvcpp_db_driver {
std::unique_ptr uvcpp_db_make_sqlite();
std::unique_ptr uvcpp_db_make_mysql();
std::unique_ptr uvcpp_db_make_pgsql();
+/// ODBC 是**通用**驱动,不是 SQL Server 专用:口径由连接串里的 `Driver=` 决定。
+/// 之所以它在,是因为微软没给 SQL Server 一个可链接的 C 客户端库 —— 官方的
+/// `msodbcsql18` 就是 ODBC 驱动。见 `uvcpp_db_odbc.cpp` 的文件头。
+std::unique_ptr uvcpp_db_make_odbc();
/// 按 scheme 造驱动。没编进来的后端返回 `nullptr`(**不是**抛异常、
/// 也不是造一个打不开的壳)—— 调用方据此报 `NO_DRIVER`。
@@ -142,7 +146,7 @@ bool uvcpp_db_driver_available(const std::string& scheme);
/// 传进来的必须是 `uvcpp_db_url::parse()` 规范化之后的 scheme。
bool uvcpp_db_driver_known(const std::string& scheme);
-/// 编进来的后端名,逗号分隔、按 sqlite,mysql,postgres 顺序。空串表示一个都没有。
+/// 编进来的后端名,逗号分隔、按 sqlite,mysql,postgres,odbc 顺序。空串表示一个都没有。
std::string uvcpp_db_available_drivers();
} // namespace db_detail
diff --git a/src/db/uvcpp_db_factory.cpp b/src/db/uvcpp_db_factory.cpp
index cfc06fbc..37a2eba0 100644
--- a/src/db/uvcpp_db_factory.cpp
+++ b/src/db/uvcpp_db_factory.cpp
@@ -209,6 +209,13 @@ std::unique_ptr uvcpp_db_make_driver(
return uvcpp_db_make_pgsql();
#else
return nullptr;
+#endif
+ }
+ if (scheme == "odbc") {
+#if UVCPP_DB_ODBC_ENABLE
+ return uvcpp_db_make_odbc();
+#else
+ return nullptr;
#endif
}
return nullptr;
@@ -234,19 +241,27 @@ bool uvcpp_db_driver_available(const std::string& scheme) {
return true;
#else
return false;
+#endif
+ }
+ if (scheme == "odbc") {
+#if UVCPP_DB_ODBC_ENABLE
+ return true;
+#else
+ return false;
#endif
}
return false;
}
bool uvcpp_db_driver_known(const std::string& scheme) {
- return scheme == "sqlite" || scheme == "mysql" || scheme == "postgres";
+ return scheme == "sqlite" || scheme == "mysql" || scheme == "postgres" ||
+ scheme == "odbc";
}
std::string uvcpp_db_available_drivers() {
std::string out;
- const char* kAll[] = {"sqlite", "mysql", "postgres"};
- for (size_t i = 0; i < 3; ++i) {
+ const char* kAll[] = {"sqlite", "mysql", "postgres", "odbc"};
+ for (size_t i = 0; i < 4; ++i) {
if (!uvcpp_db_driver_available(kAll[i])) continue;
if (!out.empty()) out.push_back(',');
out.append(kAll[i]);
diff --git a/src/db/uvcpp_db_odbc.cpp b/src/db/uvcpp_db_odbc.cpp
new file mode 100644
index 00000000..cf0a741e
--- /dev/null
+++ b/src/db/uvcpp_db_odbc.cpp
@@ -0,0 +1,1478 @@
+/**
+ * @file src/db/uvcpp_db_odbc.cpp
+ * @brief ODBC 后端:一份驱动,覆盖所有提供 ODBC 驱动的数据源。
+ * @author zhuweiye
+ * @version 1.0.0
+ *
+ * ## 为什么接 SQL Server 这件事落在 ODBC 上
+ *
+ * 微软**没有**给 SQL Server 一个可以链接的 C 客户端库 —— 它给的就是 ODBC 驱动
+ * (`msodbcsql18`,Windows 上随系统或单独安装)。所以"接 SQL Server"在 C 层
+ * 就是"接 ODBC",不存在第二个选择。
+ *
+ * 而既然路口是通用的,这个后端就**按通用写**:能连 SQL Server,也能连任何装得出
+ * ODBC 驱动的数据源(Oracle、DB2、达梦……)。全文件没有一处 `if (是 SQL Server)`。
+ * 唯一带"默认值"色彩的是连接串里那个驱动名(见 `build_connection_string`),
+ * 而它只是**默认**,`?Driver=` 一写就换掉。
+ *
+ * ## 坑一:`SQLWCHAR` 的宽度不是固定的
+ *
+ * 微软的 ODBC 头里 `SQLWCHAR` 是 `unsigned short`(2 字节,UTF-16);unixODBC
+ * 默认是 `wchar_t`(Linux 上 4 字节,UTF-32)。同一份源码在两边"看起来一样、
+ * 跑起来不一样":按 2 字节算字符偏移,在 unixODBC 上会把每个字符的后半截当成
+ * 下一个字符的开头,中文表名第一个字就错,而且是**静默**错。
+ *
+ * 所以下面不假设宽度:转换按 `sizeof(SQLWCHAR)` 分支,2 字节走 UTF-16、4 字节走
+ * UTF-32。判据取**编译期的 `sizeof`**,不是平台宏 —— 平台宏会漏掉"Linux 上装了
+ * 微软驱动管理器"这种组合,而那正是 CI 与开发机常见的样子。
+ *
+ * ## 坑二:文本参数不能用 ANSI 形态送
+ *
+ * 文本参数一律绑 `SQL_C_WCHAR`,不绑 `SQL_C_CHAR`。绑 `SQL_C_CHAR` 时驱动按
+ * **连接的 ANSI 代码页**解释那串字节,而本库内部是 UTF-8:在中文 Windows
+ * (CP936)上恰好能蒙对,在英文系统(CP1252)上中文条件就变成乱码去匹配,
+ * 结果不是报错而是**查不到** —— 比报错难查得多。
+ *
+ * ## 坑三:不绑定列,逐格 `SQLGetData`
+ *
+ * `SQLBindCol` 要求给每一列一个**定长**缓冲,而定长这个数本身就是要猜的:猜小了
+ * 长文本被静默截断,猜大了每行都白占内存。这里改用 `SQLGetData` 的分块循环,
+ * 两头都不猜。代价是每格多一次函数调用 —— 对报表与表浏览这类用途完全划算。
+ */
+
+#include
+
+#if UVCPP_DB_ENABLE && UVCPP_DB_ODBC_ENABLE
+
+#include
+#include
+
+// unixODBC 的 `sqltypes.h` 第 75 行有一句 `#define BOOL int`(在没有 ``
+// 的平台上补齐 ODBC 那套类型表)。它会把这个文件里唯一一处 `uvcpp_db_type::BOOL`
+// 展开成 `uvcpp_db_type::int`,报出来的错是 "expected unqualified-id before 'int'"
+// —— 离真正的原因隔着一层,第一次遇到很费时间。
+//
+// 这里直接 `#undef`:ODBC 头已经预处理完,这个宏的使命结束了;本文件从头到尾
+// **不用** ODBC 的 `BOOL`(全文件只有上面那一个枚举常量叫 BOOL)。
+// 微软那套 `sqltypes.h` 不发这个宏(Windows 上 `BOOL` 是 `` 里的
+// typedef,对 `enum class` 的限定名没影响),所以这行只在 unixODBC 上起作用,
+// 在 Windows 上是个空操作。
+#undef BOOL
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace uvcpp {
+namespace db_detail {
+namespace {
+
+// ---------------------------------------------------------------------------
+// UTF-8 <-> SQLWCHAR
+//
+// 四个后端里只有 ODBC 需要这一层:SQLite 收 UTF-8 字节串、libpq 收 UTF-8 字节串、
+// MySQL 也能收 UTF-8;而 ODBC 的宽接口收的是 `SQLWCHAR`。自己写而不引 ICU —— 本仓
+// 的"薄封装"定位(见 doc/db-guide.md)不允许为一次编码转换拖进一个几十 MB 的依赖。
+// ---------------------------------------------------------------------------
+
+/// 取下一个 UTF-8 码点。`*i` 是读位置;越界或非法序列返回 false 并把 `*i` 推到
+/// 能确认的边界,调用方据此截断。
+///
+/// 非法序列**不抛也不替代**:截断在这里,调用方拿到的是转换到那一处为止的结果。
+/// 与"猜一个字符补上"相比,截断至少不会把编造的数据写进库里。
+bool utf8_next(const std::string& s, size_t* i, uint32_t* cp) {
+ if (*i >= s.size()) return false;
+ const unsigned char c0 = static_cast(s[*i]);
+ size_t extra = 0;
+ uint32_t v = 0;
+ if (c0 < 0x80) {
+ *cp = c0;
+ *i += 1;
+ return true;
+ } else if ((c0 & 0xE0) == 0xC0) {
+ extra = 1;
+ v = c0 & 0x1Fu;
+ } else if ((c0 & 0xF0) == 0xE0) {
+ extra = 2;
+ v = c0 & 0x0Fu;
+ } else if ((c0 & 0xF8) == 0xF0) {
+ extra = 3;
+ v = c0 & 0x07u;
+ } else {
+ *i += 1; // 落单的续字节,或 5 字节以上的老式序列
+ return false;
+ }
+ if (*i + 1 + extra > s.size()) { // 尾部被截断的多字节序列
+ *i = s.size();
+ return false;
+ }
+ for (size_t k = 1; k <= extra; ++k) {
+ const unsigned char ck = static_cast(s[*i + k]);
+ if ((ck & 0xC0) != 0x80) {
+ *i += 1;
+ return false;
+ }
+ v = (v << 6) | (ck & 0x3Fu);
+ }
+ *i += 1 + extra;
+ *cp = v;
+ return true;
+}
+
+/// UTF-8 串 -> `SQLWCHAR` 串。
+///
+/// `sizeof(SQLWCHAR)` 为 2 时写 UTF-16(含代理对),为 4 时直接写 UTF-32。超出
+/// U+10FFFF 的码点按替换字符处理 —— 那种码点不存在,写进去只会让驱动端报一个
+/// 位置很难定位的错。
+std::basic_string to_sqlwchar(const std::string& utf8) {
+ std::basic_string out;
+ out.reserve(utf8.size());
+ size_t i = 0;
+ uint32_t cp = 0;
+ while (utf8_next(utf8, &i, &cp)) {
+ if (cp > 0x10FFFF) cp = 0xFFFD;
+ if (sizeof(SQLWCHAR) == 2) {
+ if (cp <= 0xFFFF) {
+ out.push_back(static_cast(cp));
+ } else {
+ const uint32_t v = cp - 0x10000;
+ out.push_back(static_cast(0xD800 + (v >> 10)));
+ out.push_back(static_cast(0xDC00 + (v & 0x3FF)));
+ }
+ } else {
+ out.push_back(static_cast(cp));
+ }
+ }
+ return out;
+}
+
+/// `SQLWCHAR` 串 -> UTF-8 串。`n` 是**字符个数**,不是字节数 —— ODBC 的长度指示器
+/// 一律按字符计(唯一例外是 `SQL_C_WCHAR` 的返回长度,见 `read_text_column`),
+/// 把字符数当字节数用会把中文尾部整段截掉。
+std::string to_utf8(const SQLWCHAR* p, size_t n) {
+ std::string out;
+ if (!p) return out;
+ out.reserve(n * 3 / 2 + 1);
+ for (size_t k = 0; k < n; ++k) {
+ uint32_t cp = static_cast(p[k]);
+ if (sizeof(SQLWCHAR) == 2) {
+ if (cp >= 0xD800 && cp <= 0xDBFF && k + 1 < n) {
+ const uint32_t lo = static_cast(p[k + 1]);
+ if (lo >= 0xDC00 && lo <= 0xDFFF) {
+ cp = 0x10000 + ((cp - 0xD800) << 10) + (lo - 0xDC00);
+ ++k;
+ }
+ } else if (cp >= 0xD800 && cp <= 0xDFFF) {
+ cp = 0xFFFD; // 落单的代理码元
+ }
+ }
+ if (cp < 0x80) {
+ out.push_back(static_cast(cp));
+ } else if (cp < 0x800) {
+ out.push_back(static_cast(0xC0 | (cp >> 6)));
+ out.push_back(static_cast(0x80 | (cp & 0x3F)));
+ } else if (cp < 0x10000) {
+ out.push_back(static_cast(0xE0 | (cp >> 12)));
+ out.push_back(static_cast(0x80 | ((cp >> 6) & 0x3F)));
+ out.push_back(static_cast(0x80 | (cp & 0x3F)));
+ } else {
+ out.push_back(static_cast(0xF0 | (cp >> 18)));
+ out.push_back(static_cast(0x80 | ((cp >> 12) & 0x3F)));
+ out.push_back(static_cast(0x80 | ((cp >> 6) & 0x3F)));
+ out.push_back(static_cast(0x80 | (cp & 0x3F)));
+ }
+ }
+ return out;
+}
+
+/// 一个 `SQLWCHAR` 串 + 它的**字符**长度,喂给 ODBC 用。
+///
+/// 一律显式传长度、不传 `SQL_NTS`:后者要求串以 0 结尾,而 SQL 文本里出现
+/// U+0000 是可能的(字符串字面量里的二进制)。传长度是唯一正确的喂法。
+class wstr {
+ public:
+ explicit wstr(const std::string& utf8) : s_(to_sqlwchar(utf8)) {}
+ SQLWCHAR* p() { return s_.empty() ? nullptr : &s_[0]; }
+ /// `SQLExecDirectW` / `SQLPrepareW` 的 TextLength 是 `SQLINTEGER`。
+ SQLINTEGER len() const { return static_cast(s_.size()); }
+ /// `SQLTablesW` / `SQLColumnsW` / `SQLPrimaryKeysW` 的名字长度是 `SQLSMALLINT`。
+ /// 两个都留着而不是只留一个再到处 cast:这两个宽度在 ODBC 里就是不一样的,
+ /// 让调用点写成它真正要的那个类型,比在每个调用点写 `static_cast` 更难写错。
+ SQLSMALLINT slen() const {
+ return static_cast(s_.size());
+ }
+
+ private:
+ std::basic_string s_;
+};
+
+// ---------------------------------------------------------------------------
+// 诊断
+// ---------------------------------------------------------------------------
+
+/// 把一个 ODBC 句柄上挂着的**全部**诊断记录拼成一句话。
+///
+/// 只取第一条是不够的:SQL Server 的报错常常第一条是"语句无效"、第二条才是
+/// "对象名 'dbo.检测收费' 无效",而真正要看的是第二条。
+std::string odbc_diag(SQLSMALLINT handle_type, SQLHANDLE handle) {
+ std::string out;
+ SQLWCHAR state[8];
+ SQLWCHAR msg[1024];
+ SQLINTEGER native = 0;
+ SQLSMALLINT len = 0;
+ for (SQLSMALLINT rec = 1;; ++rec) {
+ const SQLRETURN rc = SQLGetDiagRecW(
+ handle_type, handle, rec, state, &native, msg,
+ static_cast(sizeof(msg) / sizeof(msg[0]) - 1), &len);
+ if (rc != SQL_SUCCESS && rc != SQL_SUCCESS_WITH_INFO) break;
+ if (!out.empty()) out += ";";
+ out += "[";
+ out += to_utf8(state, 5);
+ out += "] ";
+ out += to_utf8(msg, static_cast(len));
+ }
+ if (out.empty()) out = "(驱动没给出诊断信息)";
+ return out;
+}
+
+/// 第一诊断记录的 SQLSTATE,取不到给空串。
+std::string odbc_sqlstate(SQLSMALLINT handle_type, SQLHANDLE handle) {
+ SQLWCHAR state[8];
+ SQLINTEGER native = 0;
+ SQLSMALLINT len = 0;
+ if (!SQL_SUCCEEDED(SQLGetDiagRecW(handle_type, handle, 1, state, &native,
+ nullptr, 0, &len))) {
+ return std::string();
+ }
+ return to_utf8(state, 5);
+}
+
+constexpr int kDefaultTimeoutMs = 5000;
+/// `SQLGetData` 的分块大小(单位是 `SQLWCHAR` / 字节)。取 2048 是个折中:
+/// 报表行几十个字段时不会反复扩容,32 KB 的文本也只要 16 次往返。
+constexpr size_t kChunk = 2048;
+/// 超过这个长度就走 LONG 类型。SQL Server 的 nvarchar 上限是 4000,报成
+/// `SQL_WVARCHAR` 送长文本时驱动会报"字符串被截断"。
+constexpr size_t kMaxPlainChars = 4000;
+
+/// `SQLDescribeCol` 的结果,够本驱动映射值类型用了。
+struct column_desc {
+ std::string name;
+ SQLSMALLINT data_type = SQL_UNKNOWN_TYPE;
+ SQLSMALLINT nullable = SQL_NULLABLE_UNKNOWN;
+ SQLULEN size = 0;
+};
+
+/// SQL 摘要,进报错信息用。整条 SQL 可能上万字符,日志里留前 120 字足够定位。
+std::string sql_brief(const std::string& sql) {
+ std::string one;
+ one.reserve(sql.size());
+ for (size_t i = 0; i < sql.size(); ++i) {
+ const char c = sql[i];
+ one.push_back(c == '\n' || c == '\r' || c == '\t' ? ' ' : c);
+ }
+ if (one.size() <= 120) return one;
+ return one.substr(0, 120) + "…";
+}
+
+} // namespace
+
+class odbc_driver : public uvcpp_db_driver {
+ public:
+ odbc_driver() {}
+ ~odbc_driver() { close(); }
+
+ const char* name() const { return "odbc"; }
+ bool is_open() const { return dbc_ != nullptr; }
+
+ uvcpp_db_status open(const uvcpp_db_url& url, std::string* err) {
+ close();
+
+ // 每个连接一份 ENV。共享一份能省一点,但要引入引用计数和一条"谁最后走谁
+ // 释放"的时序 —— 而 `uvcpp_db_client` 本来就是一个连接一个驱动对象,
+ // 省下的那点内存换不来这份复杂度。
+ const uvcpp_db_status ost = open_env(err);
+ if (!uvcpp_db_ok(ost)) return ost;
+
+ SQLRETURN rc = SQLAllocHandle(SQL_HANDLE_DBC, env_, &dbc_);
+ if (!SQL_SUCCEEDED(rc)) {
+ *err = "分配 ODBC 连接句柄失败:" + odbc_diag(SQL_HANDLE_ENV, env_);
+ release_all();
+ return uvcpp_db_status::OPEN_FAILED;
+ }
+
+ // 只读模式:在**连接之前**设下去。
+ //
+ // 这不是本模块的护栏(护栏该在应用层),而是"这道连接只用来查"这件事让驱动
+ // 也记一份 —— 哪天上层的只读判断漏了,这里还有一道。ODBC 没规定驱动必须实现
+ // 它,不支持的驱动会返回错误,所以失败**当警告吞掉**:一个不支持只读模式的
+ // 数据源不该因此完全连不上。
+ SQLSetConnectAttr(
+ dbc_, SQL_ATTR_ACCESS_MODE,
+ reinterpret_cast(static_cast(SQL_MODE_READ_ONLY)),
+ 0);
+
+ const int login_s = login_timeout_ms_ / 1000;
+ if (login_s > 0) {
+ SQLSetConnectAttr(
+ dbc_, SQL_ATTR_LOGIN_TIMEOUT,
+ reinterpret_cast(static_cast(login_s)), 0);
+ }
+
+ const std::string conn = build_connection_string(url);
+ wstr wconn(conn);
+ SQLWCHAR out_buf[1024];
+ SQLSMALLINT out_len = 0;
+ rc = SQLDriverConnectW(
+ dbc_, nullptr, wconn.p(), wconn.slen(), out_buf,
+ static_cast(sizeof(out_buf) / sizeof(out_buf[0]) - 1),
+ &out_len, SQL_DRIVER_NOPROMPT);
+ if (!SQL_SUCCEEDED(rc)) {
+ *err = "ODBC 连接失败:" + odbc_diag(SQL_HANDLE_DBC, dbc_) +
+ "(连接串:" + redact(conn) + ")";
+ release_all();
+ return uvcpp_db_status::OPEN_FAILED;
+ }
+
+ quote_ = query_identifier_quote();
+
+ // 自动提交从"开"起步:事务由 begin/commit/rollback 显式开合。不预设成关,
+ // 是因为 `uvcpp_db_client` 之外还有人直接用 query(),那种用法下"忘了 commit"
+ // 比"没事务"更难查。
+ in_transaction_ = false;
+ return uvcpp_db_status::OK;
+ }
+
+ void close() {
+ if (dbc_) {
+ if (in_transaction_) {
+ // 开关事务的连接被直接关掉时,ODBC 的行为是"回滚",这里显式结束一次,
+ // 不把结果交给实现细节。
+ SQLEndTran(SQL_HANDLE_DBC, dbc_, SQL_ROLLBACK);
+ in_transaction_ = false;
+ }
+ SQLDisconnect(dbc_);
+ }
+ release_all();
+ }
+
+ uvcpp_db_status ping(std::string* err) {
+ if (!dbc_) {
+ *err = "ODBC 连接没打开";
+ return uvcpp_db_status::NOT_CONNECTED;
+ }
+ // 真发一次极轻的往返,而不是只看本地句柄非空 —— 连接被防火墙/NAT 掐掉时
+ // 本地句柄照样非空。
+ SQLHSTMT stmt = nullptr;
+ const uvcpp_db_status ast = alloc_stmt(&stmt, err);
+ if (!uvcpp_db_ok(ast)) return ast;
+ const SQLRETURN rc = SQLExecDirectW(stmt, const_cast(kPing),
+ SQL_NTS);
+ if (!SQL_SUCCEEDED(rc)) {
+ *err = "ODBC ping 失败:" + odbc_diag(SQL_HANDLE_STMT, stmt);
+ SQLFreeHandle(SQL_HANDLE_STMT, stmt);
+ return uvcpp_db_status::EXEC_FAILED;
+ }
+ const SQLRETURN fr = SQLFetch(stmt);
+ SQLFreeHandle(SQL_HANDLE_STMT, stmt);
+ if (!SQL_SUCCEEDED(fr)) {
+ *err = "ODBC ping 取不到结果";
+ return uvcpp_db_status::EXEC_FAILED;
+ }
+ return uvcpp_db_status::OK;
+ }
+
+ uvcpp_db_status query(const std::string& sql,
+ const std::vector& params,
+ uvcpp_db_table* out, std::string* err) {
+ if (!dbc_) {
+ *err = "ODBC 连接没打开";
+ return uvcpp_db_status::NOT_CONNECTED;
+ }
+ if (!out) {
+ *err = "query 的 out 是空的";
+ return uvcpp_db_status::MISUSE;
+ }
+ if (sql.empty()) {
+ *err = "SQL 是空串";
+ return uvcpp_db_status::MISUSE;
+ }
+
+ SQLHSTMT stmt = nullptr;
+ const uvcpp_db_status ast = alloc_stmt(&stmt, err);
+ if (!uvcpp_db_ok(ast)) return ast;
+ const uvcpp_db_status est = exec_on(stmt, sql, params, err);
+ if (!uvcpp_db_ok(est)) {
+ SQLFreeHandle(SQL_HANDLE_STMT, stmt);
+ return est;
+ }
+ const uvcpp_db_status rst = fetch_all(stmt, out, err);
+ SQLFreeHandle(SQL_HANDLE_STMT, stmt);
+ return rst;
+ }
+
+ uvcpp_db_status execute(const std::string& sql,
+ const std::vector& params,
+ int64_t* affected, std::string* err) {
+ if (affected) *affected = -1;
+ if (!dbc_) {
+ *err = "ODBC 连接没打开";
+ return uvcpp_db_status::NOT_CONNECTED;
+ }
+ if (sql.empty()) {
+ *err = "SQL 是空串";
+ return uvcpp_db_status::MISUSE;
+ }
+
+ SQLHSTMT stmt = nullptr;
+ const uvcpp_db_status ast = alloc_stmt(&stmt, err);
+ if (!uvcpp_db_ok(ast)) return ast;
+ const uvcpp_db_status est = exec_on(stmt, sql, params, err);
+ if (!uvcpp_db_ok(est)) {
+ SQLFreeHandle(SQL_HANDLE_STMT, stmt);
+ return est;
+ }
+
+ // DDL 与部分 INSERT 报不出行数,ODBC 用 SQL_NO_TOTAL(-1) 表达,而那正好是
+ // "报不出"这个契约(头文件写的:后端报不出时写 -1)。
+ SQLLEN rows = 0;
+ if (affected && SQL_SUCCEEDED(SQLRowCount(stmt, &rows)) && rows >= 0) {
+ *affected = static_cast(rows);
+ }
+ SQLFreeHandle(SQL_HANDLE_STMT, stmt);
+ return uvcpp_db_status::OK;
+ }
+
+ uvcpp_db_status last_insert_id(uvcpp_db_value* out, std::string* err) {
+ if (!out) {
+ *err = "last_insert_id 的 out 是空的";
+ return uvcpp_db_status::MISUSE;
+ }
+ out->set_null();
+ if (!dbc_) {
+ *err = "ODBC 连接没打开";
+ return uvcpp_db_status::NOT_CONNECTED;
+ }
+ // SQL Server 的正解是 `SCOPE_IDENTITY()`:只认**本作用域**里刚插的那行。
+ // `@@IDENTITY` 会被触发器里的插入顶掉(插 A 表却拿到触发器写进 B 表的 id),
+ // 那是这条路上最容易踩的错。别的数据源不认 SCOPE_IDENTITY 时会报错,那时
+ // 如实返回 UNSUPPORTED —— 契约里"报不出"就是这个码,不是硬编一个 0。
+ SQLHSTMT stmt = nullptr;
+ const uvcpp_db_status ast = alloc_stmt(&stmt, err);
+ if (!uvcpp_db_ok(ast)) return ast;
+ const SQLRETURN rc = SQLExecDirectW(
+ stmt, const_cast(kScopeIdentity), SQL_NTS);
+ if (!SQL_SUCCEEDED(rc)) {
+ SQLFreeHandle(SQL_HANDLE_STMT, stmt);
+ *err = "这个数据源报不出自增值(SCOPE_IDENTITY 不被支持)";
+ return uvcpp_db_status::UNSUPPORTED;
+ }
+ bool is_null = false;
+ int64_t v = 0;
+ const uvcpp_db_status st = read_scalar_int64(stmt, 1, &v, &is_null, err);
+ SQLFreeHandle(SQL_HANDLE_STMT, stmt);
+ if (!uvcpp_db_ok(st)) return st;
+ if (is_null) {
+ *err = "这条连接还没成功 INSERT 过,拿不到自增 id";
+ return uvcpp_db_status::UNSUPPORTED;
+ }
+ out->set_int64(v);
+ return uvcpp_db_status::OK;
+ }
+
+ uvcpp_db_status begin(std::string* err) {
+ if (!dbc_) {
+ *err = "ODBC 连接没打开";
+ return uvcpp_db_status::NOT_CONNECTED;
+ }
+ // ODBC 开事务的动作是"关自动提交",而重复关一次**不报错** —— 所以另三个
+ // 后端那种"重复 begin 由后端报错"在这里不会自然发生,得自己拦。
+ //
+ // 拦在这里是有意的:静默把两层事务折成一层,调用方内层的 rollback 会把外层的
+ // 写入一起回滚掉,而它以为自己只撤了内层。
+ if (in_transaction_) {
+ *err = "事务已经开着,本模块不支持嵌套事务";
+ return uvcpp_db_status::EXEC_FAILED;
+ }
+ const SQLRETURN rc = SQLSetConnectAttr(
+ dbc_, SQL_ATTR_AUTOCOMMIT,
+ reinterpret_cast(
+ static_cast(SQL_AUTOCOMMIT_OFF)),
+ 0);
+ if (!SQL_SUCCEEDED(rc)) {
+ *err = "开启 ODBC 事务失败:" + odbc_diag(SQL_HANDLE_DBC, dbc_);
+ return uvcpp_db_status::EXEC_FAILED;
+ }
+ in_transaction_ = true;
+ return uvcpp_db_status::OK;
+ }
+
+ uvcpp_db_status commit(std::string* err) { return end_tran(SQL_COMMIT, err); }
+ uvcpp_db_status rollback(std::string* err) {
+ return end_tran(SQL_ROLLBACK, err);
+ }
+
+ /// 转义**内容**,不含两侧引号。
+ ///
+ /// ODBC 没有"转义一个字符串字面量"的接口 —— 转义规则取决于下面接的是哪个
+ /// 数据源。所以这里按 SQL 标准的做法办:单引号翻倍,反斜杠**不动**。反斜杠是
+ /// MySQL 的规矩,在 SQL Server / PostgreSQL 上改它就是**改变数据**(`a\n`
+ /// 会存成 `an`)。
+ ///
+ /// 要绝对稳妥用 `query(sql, params, ...)` 的参数绑定 —— 那条路才是本模块推荐给
+ /// 调用方的,`escape()` 只给"拼 DDL、拼 IN 列表"这类绕不开的场合。
+ std::string escape(const std::string& text) {
+ std::string out;
+ out.reserve(text.size() + 8);
+ for (size_t i = 0; i < text.size(); ++i) {
+ if (text[i] == '\'') out.push_back('\'');
+ out.push_back(text[i]);
+ }
+ return out;
+ }
+
+ std::string quote_identifier(const std::string& ident) {
+ // 引号字符来自 `SQLGetInfo`,不是写死的 —— 这正是"通用驱动"该有的样子:
+ // SQL Server 报 `"`,有的数据源报 `` ` ``。
+ //
+ // 它是**串**而不是字符(ODBC 的类型就是串),虽然现实中总是单字符。所以
+ // 两侧按整串包,内部的转义只在单字符时做 —— 多字符引号没有"翻倍"这个说法,
+ // 硬编一种做法反而会在那种数据源上产生语法错的标识符。
+ std::string out;
+ out.reserve(ident.size() + 2 * quote_.size());
+ out += quote_;
+ if (quote_.size() == 1) {
+ for (size_t i = 0; i < ident.size(); ++i) {
+ if (ident[i] == quote_[0]) out += quote_;
+ out.push_back(ident[i]);
+ }
+ } else {
+ out += ident;
+ }
+ out += quote_;
+ return out;
+ }
+
+ uvcpp_db_status table_names(std::vector* out, std::string* err) {
+ if (!out) {
+ *err = "table_names 的 out 是空的";
+ return uvcpp_db_status::MISUSE;
+ }
+ if (!dbc_) {
+ *err = "ODBC 连接没打开";
+ return uvcpp_db_status::NOT_CONNECTED;
+ }
+ out->clear();
+
+ SQLHSTMT stmt = nullptr;
+ const uvcpp_db_status ast = alloc_stmt(&stmt, err);
+ if (!uvcpp_db_ok(ast)) return ast;
+
+ // SQLTables 的四组 (串, 长度) 依序是 目录 / 模式 / 表名 / **类型**。前三个给
+ // NULL = 不过滤(当前库、全部模式、全部名字),只有第四组钉住。
+ //
+ // 顺序在这里是很容易写错的一处:把 "TABLE" 填到**表名**那一格(第三组)会让
+ // 这个函数安静地只列出名字叫 "TABLE" 的表 —— 不报错,只是结果是空的。
+ static const SQLWCHAR kTableType[] = {'T', 'A', 'B', 'L', 'E', 0};
+ const SQLRETURN rc =
+ SQLTablesW(stmt, nullptr, 0, nullptr, 0, nullptr, 0,
+ const_cast(kTableType), SQL_NTS);
+ if (!SQL_SUCCEEDED(rc)) {
+ *err = "ODBC 列不出表:" + odbc_diag(SQL_HANDLE_STMT, stmt);
+ SQLFreeHandle(SQL_HANDLE_STMT, stmt);
+ return uvcpp_db_status::EXEC_FAILED;
+ }
+
+ // 结果集是 TABLE_CAT / TABLE_SCHEM / TABLE_NAME / TABLE_TYPE / REMARKS,
+ // 只取**表名**(不拼 `schema.table`),与另三个后端口径一致;套件钉的是
+ // "刚建的表能被列出来",拼上模式名会让它匹配不上。
+ std::vector names;
+ for (;;) {
+ const SQLRETURN fr = SQLFetch(stmt);
+ if (fr == SQL_NO_DATA) break;
+ if (!SQL_SUCCEEDED(fr)) {
+ *err = "ODBC 取表名失败:" + odbc_diag(SQL_HANDLE_STMT, stmt);
+ SQLFreeHandle(SQL_HANDLE_STMT, stmt);
+ return uvcpp_db_status::EXEC_FAILED;
+ }
+ bool is_null = false;
+ std::string name;
+ const uvcpp_db_status st = read_text_column(stmt, 3, &name, &is_null, err);
+ if (!uvcpp_db_ok(st)) {
+ SQLFreeHandle(SQL_HANDLE_STMT, stmt);
+ return st;
+ }
+ if (!is_null && !name.empty()) names.push_back(name);
+ }
+ SQLFreeHandle(SQL_HANDLE_STMT, stmt);
+
+ // 排序去重放在本地做:SQLTables 的顺序由驱动决定,而同一个库在 msodbcsql 与
+ // FreeTDS 下给出的顺序不一样 —— 那会让"同一份操作在四个后端得到同一份结果"
+ // 在表清单这一项上破掉。另外多模式库里同名表会出现多次,也在这里收掉。
+ std::sort(names.begin(), names.end());
+ names.erase(std::unique(names.begin(), names.end()), names.end());
+ *out = std::move(names);
+ return uvcpp_db_status::OK;
+ }
+
+ uvcpp_db_status table_schema(const std::string& table, uvcpp_db_table* out,
+ std::string* err) {
+ if (!out) {
+ *err = "table_schema 的 out 是空的";
+ return uvcpp_db_status::MISUSE;
+ }
+ if (!dbc_) {
+ *err = "ODBC 连接没打开";
+ return uvcpp_db_status::NOT_CONNECTED;
+ }
+
+ // 主键列先单独取一份,下面标 key 用 —— SQLColumns 的结果里没有主键信息。
+ std::vector pk_columns;
+ {
+ SQLHSTMT pk = nullptr;
+ if (uvcpp_db_ok(alloc_stmt(&pk, err))) {
+ wstr wt(table);
+ if (SQL_SUCCEEDED(SQLPrimaryKeysW(pk, nullptr, 0, nullptr, 0, wt.p(),
+ wt.slen()))) {
+ for (;;) {
+ const SQLRETURN fr = SQLFetch(pk);
+ if (fr == SQL_NO_DATA) break;
+ if (!SQL_SUCCEEDED(fr)) break;
+ bool is_null = false;
+ std::string name;
+ // 结果是 CAT/SCHEM/TABLE/COLUMN_NAME/KEY_SEQ/PK_NAME,列名在第 4 位。
+ if (uvcpp_db_ok(read_text_column(pk, 4, &name, &is_null, err)) &&
+ !is_null) {
+ pk_columns.push_back(name);
+ }
+ }
+ }
+ SQLFreeHandle(SQL_HANDLE_STMT, pk);
+ }
+ // 取不到主键不是错误:视图与无主键的表都取不到,而它们照样有列。
+ }
+
+ SQLHSTMT stmt = nullptr;
+ const uvcpp_db_status ast = alloc_stmt(&stmt, err);
+ if (!uvcpp_db_ok(ast)) return ast;
+ wstr wt(table);
+ const SQLRETURN rc = SQLColumnsW(stmt, nullptr, 0, nullptr, 0, wt.p(),
+ wt.slen(), nullptr, 0);
+ if (!SQL_SUCCEEDED(rc)) {
+ *err = "ODBC 取表结构失败:" + odbc_diag(SQL_HANDLE_STMT, stmt);
+ SQLFreeHandle(SQL_HANDLE_STMT, stmt);
+ return uvcpp_db_status::EXEC_FAILED;
+ }
+
+ // 归一成四个后端共同的形状:name / type / nullable / key / default。
+ // 列序号按 ODBC 3.x 的 SQLColumns 结果集定义(1 起):
+ // 4 COLUMN_NAME / 6 TYPE_NAME / 11 NULLABLE / 13 COLUMN_DEF
+ out->clear();
+ out->reset_columns({"name", "type", "nullable", "key", "default"});
+ out->set_table_name(table);
+
+ for (;;) {
+ const SQLRETURN fr = SQLFetch(stmt);
+ if (fr == SQL_NO_DATA) break;
+ if (!SQL_SUCCEEDED(fr)) {
+ *err = "ODBC 取表结构失败:" + odbc_diag(SQL_HANDLE_STMT, stmt);
+ SQLFreeHandle(SQL_HANDLE_STMT, stmt);
+ return uvcpp_db_status::EXEC_FAILED;
+ }
+ bool null_name = false, null_type = false, null_def = false;
+ std::string col_name, type_name, column_def;
+ uvcpp_db_status st = read_text_column(stmt, 4, &col_name, &null_name, err);
+ if (uvcpp_db_ok(st)) {
+ st = read_text_column(stmt, 6, &type_name, &null_type, err);
+ }
+ if (uvcpp_db_ok(st)) {
+ st = read_text_column(stmt, 13, &column_def, &null_def, err);
+ }
+ if (!uvcpp_db_ok(st)) {
+ SQLFreeHandle(SQL_HANDLE_STMT, stmt);
+ return st;
+ }
+ bool null_nullable = false;
+ int64_t nullable = 0;
+ st = read_scalar_int64(stmt, 11, &nullable, &null_nullable, err);
+ if (!uvcpp_db_ok(st)) {
+ SQLFreeHandle(SQL_HANDLE_STMT, stmt);
+ return st;
+ }
+
+ const bool in_pk =
+ std::find(pk_columns.begin(), pk_columns.end(), col_name) !=
+ pk_columns.end();
+
+ uvcpp_db_row& row = out->add_row();
+ row.push(uvcpp_db_value(col_name));
+ row.push(uvcpp_db_value(type_name));
+ // NULLABLE 是 SQL_NO_NULLS(0) / SQL_NULLABLE(1) / SQL_NULLABLE_UNKNOWN(2)。
+ // 未知按"可空"报:报 NO 会让调用方以为这列一定有值,报 YES 最多让人多判
+ // 一次空 —— 两种错的代价不对称。
+ row.push(uvcpp_db_value(nullable == SQL_NO_NULLS ? "NO" : "YES"));
+ row.push(uvcpp_db_value(in_pk ? "PRI" : ""));
+ row.push(null_def ? uvcpp_db_value() : uvcpp_db_value(column_def));
+ }
+ SQLFreeHandle(SQL_HANDLE_STMT, stmt);
+ return uvcpp_db_status::OK;
+ }
+
+ void set_timeout_ms(int ms) {
+ timeout_ms_ = ms > 0 ? ms : 0;
+ // 登录超时单独一份:查询超时是每语句的,管不到 TCP 建连那一段。
+ login_timeout_ms_ = ms > 0 ? ms : kDefaultTimeoutMs;
+ }
+
+ private:
+ // -------------------------------------------------------------------------
+ // 连接串
+ // -------------------------------------------------------------------------
+
+ /// 把本模块的 URL 拼成一条 ODBC 连接串。
+ ///
+ /// 形状(`odbc://` 后面照旧是 `user:password@host:port/dbname?opts`):
+ ///
+ /// odbc://sa:pw@172.25.183.9:1433/SYSDB_GB38900
+ /// odbc://sa:pw@host/db?Driver=ODBC+Driver+17+for+SQL+Server
+ /// odbc:///DSN=我的数据源;Uid=u;Pwd=p ← `?` 之后原样进串
+ /// odbc://user@host/db ← 不给口令 = Windows 集成认证
+ ///
+ /// 两条规矩:
+ /// * `?` 之后的原文**原样**进串,所以写 `?DSN=x`、`?Driver=y`,或写一整条
+ /// ODBC 串都行;
+ /// * URL 里给出的 host/port/database/user/password 只在**串里还没有**同名键时
+ /// 才补进去 —— 这样 `?Server=别的机器` 能盖掉 URL 里的 host,而不是出现两个
+ /// Server 键(ODBC 对重复键的行为各家不一,别赌)。
+ std::string build_connection_string(const uvcpp_db_url& url) const {
+ const std::vector present = scan_keys(url.options);
+ struct has_key {
+ const std::vector* keys;
+ bool operator()(const char* key) const {
+ for (size_t i = 0; i < keys->size(); ++i) {
+ if (equals_ci((*keys)[i], key)) return true;
+ }
+ return false;
+ }
+ };
+ const has_key has = {&present};
+
+ std::vector > kv;
+ if (!has("Server") && !url.host.empty()) {
+ // `tcp:host,port` 是 SQL Server 的写法;别的驱动认不出 `,port` 会忽略它或
+ // 报错 —— 这是"通用驱动"在连接串这一层的固有代价,写进注释而不是藏起来。
+ kv.push_back(std::make_pair(
+ std::string("Server"),
+ url.port ? "tcp:" + url.host + "," + std::to_string(url.port)
+ : "tcp:" + url.host));
+ }
+ if (!has("Database") && !url.database.empty()) {
+ kv.push_back(std::make_pair(std::string("Database"), url.database));
+ }
+ if (!has("Uid") && !has("User") && !url.user.empty()) {
+ kv.push_back(std::make_pair(std::string("Uid"), url.user));
+ }
+ if (!has("Pwd") && !has("Password") && !url.password.empty()) {
+ kv.push_back(std::make_pair(std::string("Pwd"), url.password));
+ }
+
+ const bool no_credentials = url.user.empty() && !has("Uid") && !has("User");
+ if (no_credentials && !has("Trusted_Connection")) {
+ // 不给账号 = 走 Windows 集成认证。这比"忘了填口令就报错"更好:集成认证本来
+ // 就是同域部署下更该用的方式(没有口令进连接串,也就不会进日志)。
+ kv.push_back(
+ std::make_pair(std::string("Trusted_Connection"), std::string("yes")));
+ }
+
+ std::string out;
+ for (size_t i = 0; i < kv.size(); ++i) {
+ out += kv[i].first;
+ out += "=";
+ out += kv[i].second;
+ out += ";";
+ }
+
+ // 默认驱动名。**只是默认**:`?Driver=` 或 `?DSN=` 一写就换掉,换成任何 ODBC
+ // 数据源都行。选它当默认是因为本后端的主要用途是 SQL Server,而这个驱动名是
+ // 微软在 Windows 与 Linux 上统一的正式名字。
+ if (!has("DSN") && !has("Driver") && !has("FILEDSN")) {
+ out += "Driver={ODBC Driver 18 for SQL Server};";
+ if (!has("Encrypt")) {
+ // 18 版默认强制加密,而内网库多半没配证书 —— 不显式关掉的话,使用者会先
+ // 撞上一个"证书不受信任"的握手错误,然后去怀疑连接串写错了。
+ out += "Encrypt=no;";
+ }
+ }
+
+ // `?` 之后的原文最后追加。上面的 has() 已经保证这里不会与前面的键重名。
+ if (!url.options.empty()) {
+ out += url.options;
+ if (!out.empty() && out[out.size() - 1] != ';') out += ";";
+ }
+ return out;
+ }
+
+ /// 报错里回显连接串时把口令抹掉 —— 连接串是会进日志的。
+ static std::string redact(const std::string& conn) {
+ static const char* const kKeys[] = {"Pwd=", "Password=", "PWD="};
+ std::string out = conn;
+ for (size_t k = 0; k < sizeof(kKeys) / sizeof(kKeys[0]); ++k) {
+ size_t pos = 0;
+ while ((pos = out.find(kKeys[k], pos)) != std::string::npos) {
+ const size_t begin = pos + std::strlen(kKeys[k]);
+ size_t end = out.find(';', begin);
+ if (end == std::string::npos) end = out.size();
+ out.replace(begin, end - begin, "***");
+ pos = begin + 3;
+ }
+ }
+ return out;
+ }
+
+ static bool equals_ci(const std::string& a, const char* b) {
+ const size_t n = std::strlen(b);
+ if (a.size() != n) return false;
+ for (size_t i = 0; i < n; ++i) {
+ char x = a[i], y = b[i];
+ if (x >= 'A' && x <= 'Z') x = static_cast(x - 'A' + 'a');
+ if (y >= 'A' && y <= 'Z') y = static_cast(y - 'A' + 'a');
+ if (x != y) return false;
+ }
+ return true;
+ }
+
+ /// 从 `Key=Value;Key=Value` 里抠出键名。
+ static std::vector scan_keys(const std::string& options) {
+ std::vector keys;
+ size_t pos = 0;
+ while (pos < options.size()) {
+ size_t semi = options.find(';', pos);
+ if (semi == std::string::npos) semi = options.size();
+ const std::string item = options.substr(pos, semi - pos);
+ const size_t eq = item.find('=');
+ if (eq != std::string::npos && eq > 0) {
+ std::string key = item.substr(0, eq);
+ while (!key.empty() &&
+ (key[key.size() - 1] == ' ' || key[key.size() - 1] == '\t')) {
+ key.erase(key.size() - 1);
+ }
+ if (!key.empty()) keys.push_back(key);
+ }
+ pos = semi + 1;
+ }
+ return keys;
+ }
+
+ // -------------------------------------------------------------------------
+ // 句柄
+ // -------------------------------------------------------------------------
+
+ uvcpp_db_status open_env(std::string* err) {
+ SQLRETURN rc = SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &env_);
+ if (!SQL_SUCCEEDED(rc)) {
+ *err = "分配 ODBC 环境句柄失败(装了 ODBC 驱动管理器吗?)";
+ return uvcpp_db_status::NO_DRIVER;
+ }
+ // 声明 ODBC 3.x 而不是驱动管理器的默认值:不声明时拿到的可能是 ODBC 2 的
+ // 行为,而 SQLColumns / 诊断记录的形状在 ODBC 2 与 3 下不一样。
+ rc = SQLSetEnvAttr(env_, SQL_ATTR_ODBC_VERSION,
+ reinterpret_cast(
+ static_cast(SQL_OV_ODBC3)),
+ 0);
+ if (!SQL_SUCCEEDED(rc)) {
+ *err = "这个 ODBC 驱动管理器不接受 ODBC 3.x:" +
+ odbc_diag(SQL_HANDLE_ENV, env_);
+ release_env();
+ return uvcpp_db_status::NO_DRIVER;
+ }
+ return uvcpp_db_status::OK;
+ }
+
+ void release_env() {
+ if (env_) {
+ SQLFreeHandle(SQL_HANDLE_ENV, env_);
+ env_ = nullptr;
+ }
+ }
+
+ void release_all() {
+ if (dbc_) {
+ SQLFreeHandle(SQL_HANDLE_DBC, dbc_);
+ dbc_ = nullptr;
+ }
+ release_env();
+ }
+
+ uvcpp_db_status alloc_stmt(SQLHSTMT* out, std::string* err) {
+ const SQLRETURN rc = SQLAllocHandle(SQL_HANDLE_STMT, dbc_, out);
+ if (!SQL_SUCCEEDED(rc)) {
+ *err = "分配 ODBC 语句句柄失败:" + odbc_diag(SQL_HANDLE_DBC, dbc_);
+ return uvcpp_db_status::NOT_CONNECTED;
+ }
+ if (timeout_ms_ > 0) {
+ // SQL_ATTR_QUERY_TIMEOUT 的单位是**秒**,且"0 = 不超时"。不足一秒的取值
+ // 向上取整成 1 —— 退成 0 会变成永不超时,那不是调用方要的。
+ int secs = (timeout_ms_ + 999) / 1000;
+ if (secs < 1) secs = 1;
+ SQLSetStmtAttr(*out, SQL_ATTR_QUERY_TIMEOUT,
+ reinterpret_cast(static_cast(secs)), 0);
+ }
+ return uvcpp_db_status::OK;
+ }
+
+ std::string query_identifier_quote() {
+ SQLWCHAR buf[16];
+ SQLSMALLINT len = 0;
+ const SQLRETURN rc = SQLGetInfoW(dbc_, SQL_IDENTIFIER_QUOTE_CHAR, buf,
+ sizeof(buf), &len);
+ if (SQL_SUCCEEDED(rc) && len >= 1) {
+ return to_utf8(buf, static_cast(len));
+ }
+ return "\""; // 主流数据源都是它,报不出来时按标准走
+ }
+
+ // -------------------------------------------------------------------------
+ // 执行
+ // -------------------------------------------------------------------------
+
+ uvcpp_db_status exec_on(SQLHSTMT stmt, const std::string& sql,
+ const std::vector& params,
+ std::string* err) {
+ if (params.empty()) {
+ // 无参数走 SQLExecDirect:省一次 prepare,而且 DDL 在有些驱动上压根
+ // prepare 不了(MySQL 那边为同一件事单独留了条裸通道,这里同理)。
+ wstr wsql(sql);
+ const SQLRETURN rc = SQLExecDirectW(stmt, wsql.p(), wsql.len());
+ if (!SQL_SUCCEEDED(rc)) {
+ *err = "ODBC 执行失败:" + odbc_diag(SQL_HANDLE_STMT, stmt) +
+ "(SQL:" + sql_brief(sql) + ")";
+ // 语法错 / 表不存在 / 对象名无效,ODBC 都在 execute 这一步报,而契约要求
+ // 它们是 PREPARE_FAILED。判据取 SQLSTATE 前两位:42 类是语法与访问规则错
+ // (42S02 表不存在、42S22 列不存在、42000 语法错)。
+ const std::string state = odbc_sqlstate(SQL_HANDLE_STMT, stmt);
+ return (state.size() >= 2 && state[0] == '4' && state[1] == '2')
+ ? uvcpp_db_status::PREPARE_FAILED
+ : uvcpp_db_status::EXEC_FAILED;
+ }
+ return uvcpp_db_status::OK;
+ }
+
+ wstr wsql(sql);
+ SQLRETURN rc = SQLPrepareW(stmt, wsql.p(), wsql.len());
+ if (!SQL_SUCCEEDED(rc)) {
+ *err = "ODBC 准备语句失败:" + odbc_diag(SQL_HANDLE_STMT, stmt) +
+ "(SQL:" + sql_brief(sql) + ")";
+ return uvcpp_db_status::PREPARE_FAILED;
+ }
+
+ // 占位符个数对不上要在**绑定之前**报出来。这个检查不是多余的:参数少给时有的
+ // 驱动会把没绑的占位符当 NULL 用,于是 `WHERE id = ?` 变成 `WHERE id = NULL`,
+ // 静默返回 0 行 —— 症状是"数据没了",而不是"报错了"。
+ SQLSMALLINT want = 0;
+ rc = SQLNumParams(stmt, &want);
+ if (SQL_SUCCEEDED(rc) && static_cast(want) != params.size()) {
+ *err = "参数个数对不上:SQL 里 " + std::to_string(want) + " 个占位符,传了 " +
+ std::to_string(params.size()) + " 个";
+ return uvcpp_db_status::BIND_FAILED;
+ }
+
+ const uvcpp_db_status bst = bind_all(stmt, params, err);
+ if (!uvcpp_db_ok(bst)) return bst;
+
+ rc = SQLExecute(stmt);
+ if (!SQL_SUCCEEDED(rc)) {
+ *err = "ODBC 执行失败:" + odbc_diag(SQL_HANDLE_STMT, stmt);
+ return uvcpp_db_status::EXEC_FAILED;
+ }
+ return uvcpp_db_status::OK;
+ }
+
+ /// 绑定全部参数。
+ ///
+ /// 文本一律送 `SQL_C_WCHAR`(理由见文件头坑二)。数值按各自的 C 类型送,让驱动
+ /// 端做转换 —— 全送文本也能跑,但那样服务端拿不到类型信息,索引与隐式转换都会
+ /// 退化。
+ uvcpp_db_status bind_all(SQLHSTMT stmt,
+ const std::vector& params,
+ std::string* err) {
+ // `SQLBindParameter` 只记指针,数据要活到 `SQLExecute`,所以这些缓冲是**成员**
+ // —— 不能用"每次循环一个局部 string 再取 c_str()"的写法,那样 SQLExecute 读到
+ // 的是一批已经析构的内存。
+ text_bufs_.clear();
+ text_bufs_.reserve(params.size());
+
+ for (size_t i = 0; i < params.size(); ++i) {
+ const uvcpp_db_value& v = params[i];
+ const SQLUSMALLINT index = static_cast(i + 1);
+ SQLRETURN rc = SQL_SUCCESS;
+
+ if (v.is_null()) {
+ // 空值的两个条件:C 类型随便(不读缓冲),长度指示器必须是 SQL_NULL_DATA。
+ rc = SQLBindParameter(stmt, index, SQL_PARAM_INPUT, SQL_C_CHAR,
+ SQL_VARCHAR, 0, 0, nullptr, 0, &null_ind_);
+ } else {
+ switch (v.type()) {
+ case uvcpp_db_type::INT64:
+ int64_buf_ = v.to_int64();
+ rc = SQLBindParameter(stmt, index, SQL_PARAM_INPUT, SQL_C_SBIGINT,
+ SQL_BIGINT, 0, 0, &int64_buf_, 0, nullptr);
+ break;
+ case uvcpp_db_type::UINT64:
+ uint64_buf_ = v.to_uint64();
+ // 超 INT64_MAX 的 uint64 不能塞进 SQL_BIGINT —— 那会在服务端变成负数。
+ // 本模块没有"无符号大整数"这种 SQL 参数类型,所以拒绝,而不是截断。
+ if (uint64_buf_ > static_cast(INT64_MAX)) {
+ *err = "第 " + std::to_string(i + 1) +
+ " 个参数是无符号数且超过 int64 上限,本模块不支持";
+ return uvcpp_db_status::BIND_FAILED;
+ }
+ rc = SQLBindParameter(stmt, index, SQL_PARAM_INPUT, SQL_C_UBIGINT,
+ SQL_BIGINT, 0, 0, &uint64_buf_, 0, nullptr);
+ break;
+ case uvcpp_db_type::DOUBLE:
+ double_buf_ = v.to_double();
+ rc = SQLBindParameter(stmt, index, SQL_PARAM_INPUT, SQL_C_DOUBLE,
+ SQL_DOUBLE, 0, 0, &double_buf_, 0, nullptr);
+ break;
+ case uvcpp_db_type::BOOL:
+ bit_buf_ = v.to_bool() ? 1 : 0;
+ rc = SQLBindParameter(stmt, index, SQL_PARAM_INPUT, SQL_C_BIT,
+ SQL_BIT, 0, 0, &bit_buf_, 0, nullptr);
+ break;
+ case uvcpp_db_type::BLOB:
+ bin_buf_ = v.bytes();
+ bin_len_ = static_cast(bin_buf_.size());
+ rc = SQLBindParameter(
+ stmt, index, SQL_PARAM_INPUT, SQL_C_BINARY,
+ bin_buf_.size() > 8000 ? SQL_LONGVARBINARY : SQL_VARBINARY,
+ static_cast(bin_buf_.size()), 0,
+ bin_buf_.empty() ? nullptr : &bin_buf_[0],
+ static_cast(bin_buf_.size()), &bin_len_);
+ break;
+ default: { // TEXT / DATE / TIME / DATETIME
+ text_bufs_.push_back(to_sqlwchar(v.bytes()));
+ std::basic_string& w = text_bufs_.back();
+ text_len_ = static_cast(w.size());
+ const bool long_text = w.size() > kMaxPlainChars;
+ rc = SQLBindParameter(
+ stmt, index, SQL_PARAM_INPUT, SQL_C_WCHAR,
+ long_text ? SQL_WLONGVARCHAR : SQL_WVARCHAR,
+ static_cast(w.size()), 0, w.empty() ? nullptr : &w[0],
+ static_cast(w.size() * sizeof(SQLWCHAR)), &text_len_);
+ break;
+ }
+ }
+ }
+
+ if (!SQL_SUCCEEDED(rc)) {
+ *err = "绑定第 " + std::to_string(i + 1) + " 个参数失败:" +
+ odbc_diag(SQL_HANDLE_STMT, stmt);
+ return uvcpp_db_status::BIND_FAILED;
+ }
+ }
+ return uvcpp_db_status::OK;
+ }
+
+ // -------------------------------------------------------------------------
+ // 取结果
+ // -------------------------------------------------------------------------
+
+ uvcpp_db_status fetch_all(SQLHSTMT stmt, uvcpp_db_table* out, std::string* err) {
+ SQLSMALLINT column_count = 0;
+ SQLRETURN rc = SQLNumResultCols(stmt, &column_count);
+ if (!SQL_SUCCEEDED(rc)) {
+ *err = "ODBC 取结果列数失败:" + odbc_diag(SQL_HANDLE_STMT, stmt);
+ return uvcpp_db_status::EXEC_FAILED;
+ }
+
+ // 先把每一列的名字与类型问清楚,再逐行走 SQLGetData(理由见文件头坑三)。
+ std::vector cols(static_cast(column_count));
+ std::vector names;
+ names.reserve(static_cast(column_count));
+
+ for (SQLSMALLINT i = 0; i < column_count; ++i) {
+ SQLWCHAR name_buf[256];
+ SQLSMALLINT name_len = 0;
+ SQLSMALLINT data_type = SQL_UNKNOWN_TYPE;
+ SQLULEN column_size = 0;
+ SQLSMALLINT decimal_digits = 0;
+ SQLSMALLINT nullable = SQL_NULLABLE_UNKNOWN;
+ rc = SQLDescribeColW(
+ stmt, static_cast(i + 1), name_buf,
+ static_cast(sizeof(name_buf) / sizeof(name_buf[0]) - 1),
+ &name_len, &data_type, &column_size, &decimal_digits, &nullable);
+ if (!SQL_SUCCEEDED(rc)) {
+ *err = "ODBC 取列描述失败:" + odbc_diag(SQL_HANDLE_STMT, stmt);
+ return uvcpp_db_status::EXEC_FAILED;
+ }
+ column_desc& ci = cols[static_cast(i)];
+ // name_len 可能超过缓冲(列名超 255 字符),那样拿到的是截断的名字 —— 记在
+ // 注释里而不加一段重取逻辑:256 字符的列名在真实库里不存在。
+ ci.name = to_utf8(name_buf, static_cast(name_len));
+ ci.data_type = data_type;
+ ci.nullable = nullable;
+ ci.size = column_size;
+ names.push_back(ci.name);
+ }
+ out->reset_columns(std::move(names));
+
+ for (;;) {
+ rc = SQLFetch(stmt);
+ if (rc == SQL_NO_DATA) break;
+ if (!SQL_SUCCEEDED(rc)) {
+ *err = "ODBC 取行失败:" + odbc_diag(SQL_HANDLE_STMT, stmt);
+ return uvcpp_db_status::EXEC_FAILED;
+ }
+ uvcpp_db_row& row = out->add_row();
+ for (size_t i = 0; i < cols.size(); ++i) {
+ const uvcpp_db_status st = cell_value(
+ stmt, static_cast(i + 1), cols[i].data_type, &row, err);
+ if (!uvcpp_db_ok(st)) return st;
+ }
+ }
+
+ // 一条语句可能带多个结果集(存储过程里中途再 SELECT 一次就是)。契约只描述
+ // "一个结果表",所以只取第一个,但要把剩下的**丢掉**,否则下一次在同一句柄上
+ // 执行会撞上"还有未读结果"。
+ while (SQL_SUCCEEDED(SQLMoreResults(stmt))) {
+ // 空转,直到没有更多结果集
+ }
+
+ // 注意:SELECT 不设 `affected_rows`(与 pgsql 后端的 query 路径一致,那里只有
+ // `execute` 的出参才带行数)。套件钉的是同一份操作在四个后端上得到同一份结果,
+ // 这里单独多填一项就会破。
+ return uvcpp_db_status::OK;
+ }
+
+ /// 读一个定长 C 类型。`buf`/`len` 由调用方按目标类型给。
+ /// 返回 false 表示这次 SQLGetData 真出错了(不是 NULL)。
+ bool read_fixed(SQLHSTMT stmt, SQLUSMALLINT col, SQLSMALLINT c_type, void* buf,
+ size_t len, bool* is_null) {
+ SQLLEN ind = 0;
+ const SQLRETURN rc = SQLGetData(stmt, col, c_type, buf, len, &ind);
+ if (rc == SQL_NO_DATA || ind == SQL_NULL_DATA) {
+ *is_null = true;
+ return true;
+ }
+ if (!SQL_SUCCEEDED(rc)) return false;
+ *is_null = false;
+ return true;
+ }
+
+ /// 取一格的 `int64`。给自增 id 与表结构里的 NULLABLE 用 —— 都是单列单值。
+ uvcpp_db_status read_scalar_int64(SQLHSTMT stmt, SQLUSMALLINT col, int64_t* out,
+ bool* is_null, std::string* err) {
+ SQLBIGINT v = 0;
+ if (read_fixed(stmt, col, SQL_C_SBIGINT, &v, sizeof(v), is_null)) {
+ if (!*is_null) *out = static_cast(v);
+ return uvcpp_db_status::OK;
+ }
+ // 撞上"这列不是数值"时退回文本再解析。
+ bool text_null = false;
+ std::string text;
+ const uvcpp_db_status st = read_text_column(stmt, col, &text, &text_null, err);
+ if (!uvcpp_db_ok(st)) return st;
+ if (text_null) {
+ *is_null = true;
+ return uvcpp_db_status::OK;
+ }
+ *is_null = false;
+ *out = static_cast(std::strtoll(text.c_str(), nullptr, 10));
+ return uvcpp_db_status::OK;
+ }
+
+ /// 取一格的文本。分块循环,所以再长的文本也不会被截断。
+ ///
+ /// 空串与 NULL 在这里分得很清:NULL 是 `ind == SQL_NULL_DATA`,空串是读到了内容
+ /// 而长度为 0。契约里"空串不是 NULL"落到的就是这一行。
+ uvcpp_db_status read_text_column(SQLHSTMT stmt, SQLUSMALLINT col,
+ std::string* out, bool* is_null,
+ std::string* err) {
+ std::basic_string acc;
+ SQLWCHAR buf[kChunk];
+ for (;;) {
+ SQLLEN ind = 0;
+ const SQLRETURN rc =
+ SQLGetData(stmt, col, SQL_C_WCHAR, buf, sizeof(buf), &ind);
+ if (rc == SQL_NO_DATA) break;
+ if (ind == SQL_NULL_DATA) {
+ *is_null = true;
+ out->clear();
+ return uvcpp_db_status::OK;
+ }
+ if (!SQL_SUCCEEDED(rc) && rc != SQL_SUCCESS_WITH_INFO) {
+ *err = "ODBC 取列值失败:" + odbc_diag(SQL_HANDLE_STMT, stmt);
+ return uvcpp_db_status::EXEC_FAILED;
+ }
+ // SQL_C_WCHAR 时 ind 报的是**字节数**(这是 ODBC 里唯一按字节报长度的
+ // 地方),换算成字符个数的锅在这里背 —— 直接把 ind 当字符数用,结果会
+ // 少一半,而且中文越长错得越明显。
+ size_t chars = static_cast(ind) / sizeof(SQLWCHAR);
+ if (chars > kChunk) chars = kChunk;
+ acc.append(buf, chars);
+ if (rc == SQL_SUCCESS) break;
+ // SQL_SUCCESS_WITH_INFO = 还有下一块
+ }
+ *is_null = false;
+ *out = to_utf8(acc.empty() ? nullptr : acc.data(), acc.size());
+ return uvcpp_db_status::OK;
+ }
+
+ /// 取一格的二进制。同样分块。
+ uvcpp_db_status read_binary_column(SQLHSTMT stmt, SQLUSMALLINT col,
+ std::string* out, bool* is_null,
+ std::string* err) {
+ out->clear();
+ unsigned char buf[kChunk];
+ for (;;) {
+ SQLLEN ind = 0;
+ const SQLRETURN rc =
+ SQLGetData(stmt, col, SQL_C_BINARY, buf, sizeof(buf), &ind);
+ if (rc == SQL_NO_DATA) break;
+ if (ind == SQL_NULL_DATA) {
+ *is_null = true;
+ return uvcpp_db_status::OK;
+ }
+ if (!SQL_SUCCEEDED(rc) && rc != SQL_SUCCESS_WITH_INFO) {
+ *err = "ODBC 取二进制列失败:" + odbc_diag(SQL_HANDLE_STMT, stmt);
+ return uvcpp_db_status::EXEC_FAILED;
+ }
+ // 与文本相反:SQL_C_BINARY 时 ind 就是字节数,而且内容可能带 0x00,
+ // 所以只能按长度收,不能靠 strlen。
+ size_t bytes = static_cast(ind);
+ if (bytes > kChunk) bytes = kChunk;
+ out->append(reinterpret_cast(buf), bytes);
+ if (rc == SQL_SUCCESS) break;
+ }
+ *is_null = false;
+ return uvcpp_db_status::OK;
+ }
+
+ /// 一格的 ODBC 类型 -> `uvcpp_db_value`。分类口径与另三个后端对齐(共用套件钉住
+ /// 的就是这件事),只有两处是 ODBC 特有的:
+ ///
+ /// * **定长(DECIMAL/NUMERIC)读成 double** —— 与 MySQL/PG 两个后端一致,本模块
+ /// 没有定点类型。整数形态的 numeric 走 int64(判据与 pgsql 的 `value_from_pg`
+ /// 逐字相同:没有 `.`/`e`/`E` 且整段落进 int64)。要精确的钱就把列建成 TEXT,
+ /// 见 doc/db-guide.md;
+ /// * **日期时间不交给驱动格式化**,拿 `SQL_DATE_STRUCT` 自己拼 `"YYYY-MM-DD"`。
+ /// 让驱动格式化的话,同一个 timestamp 在 msodbcsql 与 FreeTDS 下会给出不同的
+ /// 小数位与分隔符,而契约要求四个后端的文本形态逐字节相同。
+ uvcpp_db_status cell_value(SQLHSTMT stmt, SQLUSMALLINT col,
+ SQLSMALLINT data_type, uvcpp_db_row* row,
+ std::string* err) {
+ bool is_null = false;
+ switch (data_type) {
+ case SQL_BIT: {
+ unsigned char b = 0;
+ if (read_fixed(stmt, col, SQL_C_BIT, &b, sizeof(b), &is_null)) {
+ row->push(is_null ? uvcpp_db_value() : uvcpp_db_value(b != 0));
+ return uvcpp_db_status::OK;
+ }
+ break;
+ }
+ case SQL_TINYINT:
+ case SQL_SMALLINT:
+ case SQL_INTEGER:
+ case SQL_BIGINT: {
+ int64_t v = 0;
+ const uvcpp_db_status st = read_scalar_int64(stmt, col, &v, &is_null, err);
+ if (!uvcpp_db_ok(st)) return st;
+ row->push(is_null ? uvcpp_db_value() : uvcpp_db_value(v));
+ return uvcpp_db_status::OK;
+ }
+ case SQL_REAL:
+ case SQL_FLOAT:
+ case SQL_DOUBLE: {
+ double d = 0.0;
+ if (read_fixed(stmt, col, SQL_C_DOUBLE, &d, sizeof(d), &is_null)) {
+ row->push(is_null ? uvcpp_db_value() : uvcpp_db_value(d));
+ return uvcpp_db_status::OK;
+ }
+ break;
+ }
+ case SQL_DECIMAL:
+ case SQL_NUMERIC: {
+ // 取文本形态再自己判,而不是让驱动直接转 double —— 这里要能区分"整数
+ // 形态的 numeric"(走 int64)与"带小数点的"(走 double)。
+ bool text_null = false;
+ std::string text;
+ const uvcpp_db_status st =
+ read_text_column(stmt, col, &text, &text_null, err);
+ if (!uvcpp_db_ok(st)) return st;
+ row->push(text_null ? uvcpp_db_value() : numeric_value(text));
+ return uvcpp_db_status::OK;
+ }
+ case SQL_BINARY:
+ case SQL_VARBINARY:
+ case SQL_LONGVARBINARY: {
+ std::string bytes;
+ const uvcpp_db_status st =
+ read_binary_column(stmt, col, &bytes, &is_null, err);
+ if (!uvcpp_db_ok(st)) return st;
+ row->push(is_null ? uvcpp_db_value() : uvcpp_db_value::blob(bytes));
+ return uvcpp_db_status::OK;
+ }
+ case SQL_TYPE_DATE:
+ case SQL_DATE: {
+ SQL_DATE_STRUCT ds;
+ std::memset(&ds, 0, sizeof(ds));
+ if (read_fixed(stmt, col, SQL_C_TYPE_DATE, &ds, sizeof(ds), &is_null)) {
+ row->push(is_null ? uvcpp_db_value()
+ : uvcpp_db_value::date(format_date(ds)));
+ return uvcpp_db_status::OK;
+ }
+ break;
+ }
+ case SQL_TYPE_TIME:
+ case SQL_TIME: {
+ SQL_TIME_STRUCT ts;
+ std::memset(&ts, 0, sizeof(ts));
+ if (read_fixed(stmt, col, SQL_C_TYPE_TIME, &ts, sizeof(ts), &is_null)) {
+ row->push(is_null ? uvcpp_db_value()
+ : uvcpp_db_value::time(format_time(ts)));
+ return uvcpp_db_status::OK;
+ }
+ break;
+ }
+ case SQL_TYPE_TIMESTAMP:
+ case SQL_TIMESTAMP: {
+ SQL_TIMESTAMP_STRUCT ts;
+ std::memset(&ts, 0, sizeof(ts));
+ if (read_fixed(stmt, col, SQL_C_TYPE_TIMESTAMP, &ts, sizeof(ts),
+ &is_null)) {
+ row->push(is_null ? uvcpp_db_value()
+ : uvcpp_db_value::datetime(format_timestamp(ts)));
+ return uvcpp_db_status::OK;
+ }
+ break;
+ }
+ default:
+ break;
+ }
+
+ // 其余一律按文本收:`uniqueidentifier`、`xml`、各家私有的字符串类型都落在这里,
+ // 走 SQL_C_WCHAR 都能拿到人读的形态。
+ bool text_null = false;
+ std::string text;
+ const uvcpp_db_status st = read_text_column(stmt, col, &text, &text_null, err);
+ if (!uvcpp_db_ok(st)) return st;
+ row->push(text_null ? uvcpp_db_value() : uvcpp_db_value(text));
+ return uvcpp_db_status::OK;
+ }
+
+ /// 定长文本 -> int64 或 double。判据与 pgsql 的 `value_from_pg` 的 numeric 分支
+ /// 逐字相同,这样同一个 DECIMAL 列在两个后端上得到同一个类型标签。
+ static uvcpp_db_value numeric_value(const std::string& text) {
+ if (text.find_first_of(".eE") == std::string::npos) {
+ errno = 0;
+ char* end = nullptr;
+ const long long v = std::strtoll(text.c_str(), &end, 10);
+ if (errno == 0 && end && *end == '\0') {
+ return uvcpp_db_value(static_cast(v));
+ }
+ }
+ return uvcpp_db_value(std::strtod(text.c_str(), nullptr));
+ }
+
+ static std::string format_date(const SQL_DATE_STRUCT& d) {
+ char buf[32];
+ std::snprintf(buf, sizeof(buf), "%04d-%02d-%02d", static_cast(d.year),
+ static_cast(d.month), static_cast(d.day));
+ return std::string(buf);
+ }
+
+ static std::string format_time(const SQL_TIME_STRUCT& t) {
+ char buf[32];
+ std::snprintf(buf, sizeof(buf), "%02d:%02d:%02d", static_cast(t.hour),
+ static_cast(t.minute), static_cast(t.second));
+ return std::string(buf);
+ }
+
+ static std::string format_timestamp(const SQL_TIMESTAMP_STRUCT& t) {
+ char buf[48];
+ // 小数秒**丢掉**:另三个后端的 DATETIME 形态就是秒级
+ // (`"YYYY-MM-DD HH:MM:SS"`),这里多带几位小数会让同一个值在四个后端上
+ // 文本形态不一致。要小数秒的场合得显式 CAST 成字符串自己取。
+ std::snprintf(buf, sizeof(buf), "%04d-%02d-%02d %02d:%02d:%02d",
+ static_cast(t.year), static_cast(t.month),
+ static_cast(t.day), static_cast(t.hour),
+ static_cast(t.minute), static_cast(t.second));
+ return std::string(buf);
+ }
+
+ uvcpp_db_status end_tran(SQLSMALLINT how, std::string* err) {
+ if (!dbc_) {
+ *err = "ODBC 连接没打开";
+ return uvcpp_db_status::NOT_CONNECTED;
+ }
+ if (!in_transaction_) {
+ *err = "没有开着的事务";
+ return uvcpp_db_status::EXEC_FAILED;
+ }
+ const SQLRETURN rc = SQLEndTran(SQL_HANDLE_DBC, dbc_, how);
+ if (!SQL_SUCCEEDED(rc)) {
+ *err = std::string("ODBC ") + (how == SQL_COMMIT ? "提交" : "回滚") +
+ "失败:" + odbc_diag(SQL_HANDLE_DBC, dbc_);
+ return uvcpp_db_status::EXEC_FAILED;
+ }
+ // 回到自动提交,下一次 begin 才能重新关一次。
+ SQLSetConnectAttr(dbc_, SQL_ATTR_AUTOCOMMIT,
+ reinterpret_cast(
+ static_cast(SQL_AUTOCOMMIT_ON)),
+ 0);
+ in_transaction_ = false;
+ return uvcpp_db_status::OK;
+ }
+
+ /// 这两条常量**每一位单独写**,不能用 `L"SELECT 1"` —— `L"..."` 的宽度是
+ /// `wchar_t`(Linux 上 4 字节),而 `SQLWCHAR` 可能是 2 字节,那正好是文件头
+ /// 坑一的翻版:在 unixODBC 上编译得过、跑起来全错。
+ static const SQLWCHAR kPing[];
+ static const SQLWCHAR kScopeIdentity[];
+
+ SQLHENV env_ = nullptr;
+ SQLHDBC dbc_ = nullptr;
+ bool in_transaction_ = false;
+ int timeout_ms_ = kDefaultTimeoutMs;
+ int login_timeout_ms_ = kDefaultTimeoutMs;
+ /// 标识符引号(`SQLGetInfo(SQL_IDENTIFIER_QUOTE_CHAR)` 的结果)。连上之前
+ /// 按标准取 `"`。
+ std::string quote_ = "\"";
+
+ // 绑定缓冲。是成员而不是局部变量,因为 SQLBindParameter 只记指针、数据要活到
+ // SQLExecute —— 见 bind_all 里的说明。
+ std::vector > text_bufs_;
+ std::string bin_buf_;
+ int64_t int64_buf_ = 0;
+ uint64_t uint64_buf_ = 0;
+ double double_buf_ = 0.0;
+ unsigned char bit_buf_ = 0;
+ SQLLEN null_ind_ = SQL_NULL_DATA;
+ SQLLEN text_len_ = 0;
+ SQLLEN bin_len_ = 0;
+};
+
+const SQLWCHAR odbc_driver::kPing[] = {'S', 'E', 'L', 'E', 'C', 'T',
+ ' ', '1', 0};
+
+const SQLWCHAR odbc_driver::kScopeIdentity[] = {
+ 'S', 'E', 'L', 'E', 'C', 'T', ' ', 'C', 'A', 'S', 'T', '(', 'S', 'C', 'O',
+ 'P', 'E', '_', 'I', 'D', 'E', 'N', 'T', 'I', 'T', 'Y', '(', ')', ' ', 'A',
+ 'S', ' ', 'B', 'I', 'G', 'I', 'N', 'T', ')', 0};
+
+std::unique_ptr uvcpp_db_make_odbc() {
+ return std::unique_ptr(new odbc_driver());
+}
+
+} // namespace db_detail
+} // namespace uvcpp
+
+#endif // UVCPP_DB_ENABLE && UVCPP_DB_ODBC_ENABLE
diff --git a/src/uvcpp/uvcpp_version.h b/src/uvcpp/uvcpp_version.h
index 7d1ffd87..e748324a 100644
--- a/src/uvcpp/uvcpp_version.h
+++ b/src/uvcpp/uvcpp_version.h
@@ -14,10 +14,10 @@
#define UVCPP_VERSION_MAJOR 1
#define UVCPP_VERSION_MINOR 7
-#define UVCPP_VERSION_PATCH 0
+#define UVCPP_VERSION_PATCH 1
// 0 = 非发布版;1 = 发布版
-#define UVCPP_VERSION_IS_RELEASE 1
+#define UVCPP_VERSION_IS_RELEASE 0
// 可选后缀(例如用于构建元信息),默认为空字符串
#define UVCPP_VERSION_SUFFIX ""
diff --git a/tests/tools/check_doc_snippets.py b/tests/tools/check_doc_snippets.py
index 8566593d..a6705202 100644
--- a/tests/tools/check_doc_snippets.py
+++ b/tests/tools/check_doc_snippets.py
@@ -289,7 +289,7 @@ def first_line_marker(body):
# `quic/` 属于**第一档**("宏为 0 时类会消失"):本模块的**四个**公开头整段套在
# `#if UVCPP_QUIC_ENABLE` 里(另外两个头 `uvcpp_quic_ngtcp2.h` 与
# `uvcpp_quic_session.h` 是私有的,不进包、文档也不该引它们)。模块关掉时
-# `CMakeLists.txt:3049-3049` 的 install 规则确实不装它们,
+# `CMakeLists.txt:3097-3097` 的 install 规则确实不装它们,
# 但那条只挡住 `cmake --install`:`package_release.py` 是从 `src/` 逐目录拷头的、
# 与开关无关(同上面 wsdl 那段记的形状,实测过 —— 一份 `UVCPP_ENABLE_QUIC=OFF`
# 的包里有 `include/quic/` 四个头,而没有 `uvcpp_quic_ngtcp2.h`)。所以在**发布包**
diff --git a/tests/tools/check_docs.py b/tests/tools/check_docs.py
index d4aec7d8..eac093c4 100644
--- a/tests/tools/check_docs.py
+++ b/tests/tools/check_docs.py
@@ -65,13 +65,13 @@ def ok(what):
# ---------------------------------------------------------------------------
# **必须允许前导空白**:`UVCPP_BUILD_FUNCTIONAL` 是缩进在 `if(UVCPP_BUILD_TESTS)`
-# 里面的(CMakeLists.txt:2833-2833),写成 `^option\(` 会静默少一个 —— 而少的那一个
+# 里面的(CMakeLists.txt:2881-2881),写成 `^option\(` 会静默少一个 —— 而少的那一个
# 恰好是"只在某个前提成立时才存在"的那一类,也就是最该被盯住的。
OPTION_RE = re.compile(r"^\s*option\(\s*([A-Za-z_][A-Za-z0-9_]*)", re.M)
# `UVCPP_TRY_WRITE_MIN_BYTES` 跨两行(`set(` 换行后才写值),而 `[^)]*` 是取反字符类、
# 天然跨行,所以不用 DOTALL。**只认 `CACHE STRING`**:`set(UVCPP_LIB_TARGET ... CACHE
-# INTERNAL ...)`(CMakeLists.txt:2819-2819)同样是 `UVCPP_` 前缀 + CACHE 形状,但它是个内部
+# INTERNAL ...)`(CMakeLists.txt:2867-2867)同样是 `UVCPP_` 前缀 + CACHE 形状,但它是个内部
# 变量,不是旋钮。
CACHE_STRING_RE = re.compile(
r"^\s*set\(\s*(UVCPP_[A-Za-z0-9_]*)\s+[^)]*CACHE\s+STRING", re.M)
diff --git a/tests/tools/doc_line_refs.lock b/tests/tools/doc_line_refs.lock
index d734124a..a35c73ea 100644
--- a/tests/tools/doc_line_refs.lock
+++ b/tests/tools/doc_line_refs.lock
@@ -3,81 +3,81 @@
# 格式:<文件>:<起>-<止>
# 后两列是**尾部锚点**(`-` = 区间只有一行、没有这个锚点):用来判「区间被内部插/删了行」,理由见脚本文件头。
CMakeLists.txt:122-122 147faa0531ba28f7 147faa0531ba28f7 -
-CMakeLists.txt:1265-1265 bca1eb88b6c022d6 bca1eb88b6c022d6 -
CMakeLists.txt:13-15 8268b2f87aeffcb3 45a477e305de30bc a2610ceafefb36ae
-CMakeLists.txt:1330-1330 7507f15e2a39705f 7507f15e2a39705f -
-CMakeLists.txt:1342-1342 ae83b5db663cfc83 ae83b5db663cfc83 -
-CMakeLists.txt:1404-1404 4c32db63ce9707d7 4c32db63ce9707d7 -
+CMakeLists.txt:1311-1311 bca1eb88b6c022d6 bca1eb88b6c022d6 -
+CMakeLists.txt:1376-1376 7507f15e2a39705f 7507f15e2a39705f -
+CMakeLists.txt:1388-1388 ae83b5db663cfc83 ae83b5db663cfc83 -
CMakeLists.txt:143-143 c60affd86ed9f92a c60affd86ed9f92a -
-CMakeLists.txt:1443-1443 c9a89b64a410bd35 c9a89b64a410bd35 -
-CMakeLists.txt:1457-1457 1aa4a028404af983 1aa4a028404af983 -
-CMakeLists.txt:1528-1536 69e14c6236c163b2 d6de6970f4db1a07 b9d79421b34560fa
-CMakeLists.txt:1544-1550 c5fc893aa7d18fa3 d483a8300239fe81 84596bbfaa11fe0a
+CMakeLists.txt:1450-1450 4c32db63ce9707d7 4c32db63ce9707d7 -
+CMakeLists.txt:1489-1489 c9a89b64a410bd35 c9a89b64a410bd35 -
+CMakeLists.txt:1503-1503 1aa4a028404af983 1aa4a028404af983 -
+CMakeLists.txt:1574-1582 69e14c6236c163b2 d6de6970f4db1a07 b9d79421b34560fa
CMakeLists.txt:159-159 ec21dd9177da1017 ec21dd9177da1017 -
+CMakeLists.txt:1590-1596 c5fc893aa7d18fa3 d483a8300239fe81 84596bbfaa11fe0a
CMakeLists.txt:16-16 622cef3e23980a0b 622cef3e23980a0b -
-CMakeLists.txt:1667-1667 81476067003985a5 81476067003985a5 -
-CMakeLists.txt:1727-1727 81476067003985a5 81476067003985a5 -
-CMakeLists.txt:1819-1819 efedb37c4c19a3c0 efedb37c4c19a3c0 -
+CMakeLists.txt:1713-1713 81476067003985a5 81476067003985a5 -
+CMakeLists.txt:1773-1773 81476067003985a5 81476067003985a5 -
+CMakeLists.txt:1865-1865 efedb37c4c19a3c0 efedb37c4c19a3c0 -
CMakeLists.txt:189-189 2c0786b391ddc1c8 2c0786b391ddc1c8 -
-CMakeLists.txt:1907-1916 7e0525f67ce2a0c8 d483a8300239fe81 7e66cb3bad5c4ce1
-CMakeLists.txt:1915-1915 7e66cb3bad5c4ce1 7e66cb3bad5c4ce1 -
-CMakeLists.txt:1951-1951 e6eaf14767522ccb e6eaf14767522ccb -
-CMakeLists.txt:1951-1953 3b4c91b678196c72 2edef8311cc16c7d 4203c17caf76ca71
-CMakeLists.txt:1959-1960 1295d64247ee5ebf f6eb70bb68e31c3d bc0f1401494fb527
-CMakeLists.txt:1995-1995 dd1f1235f833821d dd1f1235f833821d -
-CMakeLists.txt:2000-2000 110771a2981514e9 110771a2981514e9 -
-CMakeLists.txt:2009-2009 519a4cb6beee59f4 519a4cb6beee59f4 -
-CMakeLists.txt:2023-2023 3639f058cd92a2cd 3639f058cd92a2cd -
-CMakeLists.txt:2147-2147 ba10ebbf2070090e ba10ebbf2070090e -
-CMakeLists.txt:222-236 59b22ad7fdc516ee 89e15674e4730156 c5cb01bc0af84c23
-CMakeLists.txt:223-223 695784a24021eabf 695784a24021eabf -
-CMakeLists.txt:2284-2285 544cae034c4b7dd9 ae23b666e16ecffe f0116ba37d513d24
-CMakeLists.txt:2307-2307 4169bb4ce7b4c0da 4169bb4ce7b4c0da -
-CMakeLists.txt:2309-2309 1d054c55978f517a 1d054c55978f517a -
-CMakeLists.txt:233-234 f98b9cc25b0f3082 9bdbf2dd785f40c4 7e30539740cb0a1b
-CMakeLists.txt:236-236 89e15674e4730156 89e15674e4730156 -
-CMakeLists.txt:2445-2487 a9e96e5f6c968f9c d483a8300239fe81 a0b5d54b55c00ee6
-CMakeLists.txt:250-250 5897d2538d488fb9 5897d2538d488fb9 -
-CMakeLists.txt:252-252 ee4796ba98a0da0b ee4796ba98a0da0b -
-CMakeLists.txt:2629-2629 f1e99550dd80dc7d f1e99550dd80dc7d -
-CMakeLists.txt:2732-2732 9b420934030132df 9b420934030132df -
-CMakeLists.txt:2776-2776 9e1f3efdb9da2727 9e1f3efdb9da2727 -
-CMakeLists.txt:2819-2819 017f72fdf67f5f9c 017f72fdf67f5f9c -
-CMakeLists.txt:2833-2833 ba7837489a6f698d ba7837489a6f698d -
-CMakeLists.txt:2870-3176 20ace9b2e9920247 04c7527b764b2205 da39a3ee5e6b4b0d
-CMakeLists.txt:2920-2923 9ee2634d8590c8a8 d483a8300239fe81 eedf8624b192179d
-CMakeLists.txt:2939-2944 136099b61c02b874 2da86ae33be9196d 439ae4e87a125249
+CMakeLists.txt:1953-1962 7e0525f67ce2a0c8 d483a8300239fe81 7e66cb3bad5c4ce1
+CMakeLists.txt:1961-1961 7e66cb3bad5c4ce1 7e66cb3bad5c4ce1 -
+CMakeLists.txt:1997-1997 e6eaf14767522ccb e6eaf14767522ccb -
+CMakeLists.txt:1997-1999 3b4c91b678196c72 2edef8311cc16c7d 4203c17caf76ca71
+CMakeLists.txt:2005-2006 1295d64247ee5ebf f6eb70bb68e31c3d bc0f1401494fb527
+CMakeLists.txt:2041-2041 dd1f1235f833821d dd1f1235f833821d -
+CMakeLists.txt:2046-2046 110771a2981514e9 110771a2981514e9 -
+CMakeLists.txt:2055-2055 519a4cb6beee59f4 519a4cb6beee59f4 -
+CMakeLists.txt:2069-2069 3639f058cd92a2cd 3639f058cd92a2cd -
+CMakeLists.txt:2193-2193 ba10ebbf2070090e ba10ebbf2070090e -
+CMakeLists.txt:231-245 59b22ad7fdc516ee 89e15674e4730156 c5cb01bc0af84c23
+CMakeLists.txt:232-232 695784a24021eabf 695784a24021eabf -
+CMakeLists.txt:2330-2331 544cae034c4b7dd9 ae23b666e16ecffe f0116ba37d513d24
+CMakeLists.txt:2353-2353 4169bb4ce7b4c0da 4169bb4ce7b4c0da -
+CMakeLists.txt:2355-2355 1d054c55978f517a 1d054c55978f517a -
+CMakeLists.txt:242-243 f98b9cc25b0f3082 9bdbf2dd785f40c4 7e30539740cb0a1b
+CMakeLists.txt:245-245 89e15674e4730156 89e15674e4730156 -
+CMakeLists.txt:2493-2535 a9e96e5f6c968f9c d483a8300239fe81 a0b5d54b55c00ee6
+CMakeLists.txt:259-259 5897d2538d488fb9 5897d2538d488fb9 -
+CMakeLists.txt:261-261 ee4796ba98a0da0b ee4796ba98a0da0b -
+CMakeLists.txt:2677-2677 f1e99550dd80dc7d f1e99550dd80dc7d -
+CMakeLists.txt:2780-2780 9b420934030132df 9b420934030132df -
+CMakeLists.txt:2824-2824 9e1f3efdb9da2727 9e1f3efdb9da2727 -
+CMakeLists.txt:2867-2867 017f72fdf67f5f9c 017f72fdf67f5f9c -
+CMakeLists.txt:2881-2881 ba7837489a6f698d ba7837489a6f698d -
+CMakeLists.txt:2918-3224 20ace9b2e9920247 04c7527b764b2205 da39a3ee5e6b4b0d
+CMakeLists.txt:2968-2971 9ee2634d8590c8a8 d483a8300239fe81 eedf8624b192179d
+CMakeLists.txt:2987-2992 136099b61c02b874 2da86ae33be9196d 439ae4e87a125249
CMakeLists.txt:30-37 0805a0e3033f88a3 1bc5922231c799b4 a0b5d54b55c00ee6
-CMakeLists.txt:3019-3020 0783bddb14762bbf 77ae7433df62df69 7b7da528bdd6b3dc
-CMakeLists.txt:3029-3029 dc47a271c1e60add dc47a271c1e60add -
-CMakeLists.txt:3047-3047 03e1cd70f2221b46 03e1cd70f2221b46 -
-CMakeLists.txt:3049-3049 67e118285cc2d304 67e118285cc2d304 -
-CMakeLists.txt:3070-3070 23328eefdf8e4fee 23328eefdf8e4fee -
-CMakeLists.txt:3156-3159 809b3c536e6c4740 d483a8300239fe81 2da83c91fe5c72e5
-CMakeLists.txt:3217-3217 714c97211cb46a65 714c97211cb46a65 -
-CMakeLists.txt:3325-3325 cd4e49682028492d cd4e49682028492d -
-CMakeLists.txt:409-409 285b3125d1799b48 285b3125d1799b48 -
-CMakeLists.txt:420-420 aa41a613c3dea36a aa41a613c3dea36a -
-CMakeLists.txt:424-424 2765438f70cc5afe 2765438f70cc5afe -
-CMakeLists.txt:432-432 9e61c1888f63da12 9e61c1888f63da12 -
-CMakeLists.txt:510-510 85fa3e4ffdce19a1 85fa3e4ffdce19a1 -
-CMakeLists.txt:534-534 adf1bab5cd4b11a0 adf1bab5cd4b11a0 -
-CMakeLists.txt:595-595 66f195432ad07018 66f195432ad07018 -
+CMakeLists.txt:3067-3068 0783bddb14762bbf 77ae7433df62df69 7b7da528bdd6b3dc
+CMakeLists.txt:3077-3077 dc47a271c1e60add dc47a271c1e60add -
+CMakeLists.txt:3095-3095 03e1cd70f2221b46 03e1cd70f2221b46 -
+CMakeLists.txt:3097-3097 67e118285cc2d304 67e118285cc2d304 -
+CMakeLists.txt:3118-3118 23328eefdf8e4fee 23328eefdf8e4fee -
+CMakeLists.txt:3204-3207 809b3c536e6c4740 d483a8300239fe81 2da83c91fe5c72e5
+CMakeLists.txt:3265-3265 714c97211cb46a65 714c97211cb46a65 -
+CMakeLists.txt:3373-3373 cd4e49682028492d cd4e49682028492d -
+CMakeLists.txt:418-418 285b3125d1799b48 285b3125d1799b48 -
+CMakeLists.txt:429-429 aa41a613c3dea36a aa41a613c3dea36a -
+CMakeLists.txt:433-433 2765438f70cc5afe 2765438f70cc5afe -
+CMakeLists.txt:441-441 9e61c1888f63da12 9e61c1888f63da12 -
+CMakeLists.txt:519-519 85fa3e4ffdce19a1 85fa3e4ffdce19a1 -
+CMakeLists.txt:543-543 adf1bab5cd4b11a0 adf1bab5cd4b11a0 -
CMakeLists.txt:6-6 835af525b88bec74 835af525b88bec74 -
-CMakeLists.txt:604-604 d8012e3f7923cf4f d8012e3f7923cf4f -
-CMakeLists.txt:607-621 97141d86d37823fc 75774cbf835cb0b5 fc4313fc81980d8b
-CMakeLists.txt:626-626 3a22c2d0cd98d748 3a22c2d0cd98d748 -
+CMakeLists.txt:604-604 66f195432ad07018 66f195432ad07018 -
+CMakeLists.txt:613-613 d8012e3f7923cf4f d8012e3f7923cf4f -
+CMakeLists.txt:616-630 97141d86d37823fc 75774cbf835cb0b5 fc4313fc81980d8b
+CMakeLists.txt:635-635 3a22c2d0cd98d748 3a22c2d0cd98d748 -
CMakeLists.txt:65-65 1dba8bdfc9071f92 1dba8bdfc9071f92 -
CMakeLists.txt:71-71 7b19e4255d610053 7b19e4255d610053 -
-CMakeLists.txt:721-721 13f759f390b3188b 13f759f390b3188b -
-CMakeLists.txt:728-728 1751d7d914bca144 1751d7d914bca144 -
-CMakeLists.txt:764-764 aa18c45ef7f6795a aa18c45ef7f6795a -
-CMakeLists.txt:767-767 162cf1539ee5438b 162cf1539ee5438b -
-CMakeLists.txt:774-774 955da32ed2122e62 955da32ed2122e62 -
-CMakeLists.txt:785-785 12974823b546187f 12974823b546187f -
-CMakeLists.txt:942-942 646230d9ebf93ef4 646230d9ebf93ef4 -
-CMakeLists.txt:960-960 075f8ca073d6e384 075f8ca073d6e384 -
-CMakeLists.txt:967-967 f209c420bc688e6b f209c420bc688e6b -
+CMakeLists.txt:730-730 13f759f390b3188b 13f759f390b3188b -
+CMakeLists.txt:737-737 1751d7d914bca144 1751d7d914bca144 -
+CMakeLists.txt:773-773 aa18c45ef7f6795a aa18c45ef7f6795a -
+CMakeLists.txt:776-776 162cf1539ee5438b 162cf1539ee5438b -
+CMakeLists.txt:783-783 955da32ed2122e62 955da32ed2122e62 -
+CMakeLists.txt:794-794 12974823b546187f 12974823b546187f -
+CMakeLists.txt:951-951 646230d9ebf93ef4 646230d9ebf93ef4 -
+CMakeLists.txt:969-969 075f8ca073d6e384 075f8ca073d6e384 -
+CMakeLists.txt:976-976 f209c420bc688e6b f209c420bc688e6b -
src/expand/uvcpp_memory_pool.cpp:256-273 f515fb3e4707d626 c2b7df6201fdd336 0c9608d989883adc
src/expand/uvcpp_memory_pool.cpp:275-292 b59d73701badb183 c2b7df6201fdd336 0c9608d989883adc
src/expand/uvcpp_memory_pool.cpp:361-361 24b885117f83459e 24b885117f83459e -
diff --git a/tests/tools/package_release.py b/tests/tools/package_release.py
index f5d98166..1a0eab31 100644
--- a/tests/tools/package_release.py
+++ b/tests/tools/package_release.py
@@ -30,7 +30,7 @@
ROOT = os.path.dirname(os.path.dirname(HERE)) # tests/tools -> tests -> repo
def _header_version():
- """版本号的唯一来源是 `src/uvcpp/uvcpp_version.h`(`CMakeLists.txt:1528-1536`
+ """版本号的唯一来源是 `src/uvcpp/uvcpp_version.h`(`CMakeLists.txt:1574-1582`
在 configure 期读的也是它)。这里以前写死 "1.1.0":标签打到 v1.1.5 时,产出的
zip 名和 uvcpp.pc 的 Version 仍然自称 1.1.0 —— 两者只差一个字符串,出包时谁也
不会去核对。读不到就停下,不自作主张退回默认值:一个名字说谎的包比不出包更坏。
@@ -122,9 +122,9 @@ def num(macro):
# 的错误码,任何配置下都发。
#
# `CMakeLists.txt` 的 `list(FILTER UVCPP_HEADERS EXCLUDE REGEX
-# "src/grpc/uvcpp_grpc_server\\.h")`(`CMakeLists.txt:1951-1953`)与
+# "src/grpc/uvcpp_grpc_server\\.h")`(`CMakeLists.txt:1997-1999`)与
# `list(FILTER GRPC_HEADER_FILES EXCLUDE REGEX "uvcpp_grpc_server\\.h$")`
-# (`CMakeLists.txt:2939-2944`)是同一条排除的两处 —— **三处必须一起改**:
+# (`CMakeLists.txt:2987-2992`)是同一条排除的两处 —— **三处必须一起改**:
# 这里漏了只影响发布包,CMake 那两处漏了只影响 `cmake --install`,三边都不报错。
WEB_GATED_HEADERS = {"uvcpp_grpc_server.h"}
@@ -1156,7 +1156,7 @@ def main():
# **曾经是差异 2**:补 zlib.h / zconf.h —— `web/uvcpp_ws_parser.h` 在
# UVCPP_ZLIB_ENABLE=1 时要 include 它,而 install 规则里原先没有这一条。
- # 2026-10-12 起 install 规则自己也装了(`CMakeLists.txt:3156-3159`),所以这
+ # 2026-10-12 起 install 规则自己也装了(`CMakeLists.txt:3204-3207`),所以这
# 不再是"刻意的差异",只是本脚本手工组装 stage、**不走 `cmake --install`** 时
# 对同一件事的重复一遍 —— 两边必须落到同一个位置(`include/` 顶层)。
z = dep_src(tree, repo, "zlib-131") or dep_src(tree, repo, "zlib")
From 03084be99c488dc5370a473a334fb3ef0c4f3e50 Mon Sep 17 00:00:00 2001
From: zhuweiye
Date: Thu, 15 Oct 2026 21:01:21 +0800
Subject: [PATCH 02/11] =?UTF-8?q?fix(db):=20ODBC=20=E6=8C=89=E5=AE=9E?=
=?UTF-8?q?=E6=B5=8B=E6=94=B9=E5=85=AD=E5=A4=84=EF=BC=8C=E5=B9=B6=E6=8A=8A?=
=?UTF-8?q?=E5=AE=83=E6=8E=A5=E8=BF=9B=E5=85=B1=E7=94=A8=E5=A5=97=E4=BB=B6?=
=?UTF-8?q?=EF=BC=881.7.1-dev=EF=BC=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
共用套件第一次跑在 ODBC 上,立刻抓出六处在只"编得过、链得上"时看不见的错 ——
每一条都是量出来的,不是推出来的:
- `SQLBindParameter` 只记指针(缓冲与 `StrLen_or_IndPtr` 都记),所以绑定的存储
必须活到 `SQLExecute` 之后,而且**每个参数一份**。共用一份时四个参数
`(1, 2, 't0', 'uu0')` 存进去读回来是 `(2, 2, 't', 'u')`:每个都只剩最后一次
写入的一个字节。改成 `std::vector bound_`,绑之前 `resize` 一次,
循环里只按下标取,不再 `push_back`(那会让已经交出去的指针悬空)。
- `SQL_C_WCHAR` 的长度指示器单位是**字节**:给字符数会把 `"张三"` 截成 `"张"`、
`'abcdef'` 截成 `'abc'`。改成 `w.size() * sizeof(SQLWCHAR)`。
- 空串与 NULL 是靠**指针**分的,不是靠长度:有效指针 + `ind=0` 是空串,
`nullptr` + `ind=0` 是 NULL。空串上 `std::string::data()` 给不给非空指针是实现
自由,所以备了两个哑元 `kEmptyByte` / `kEmptyWide`。这条错了的症状是写空串撞在
`NOT NULL DEFAULT ''` 上响亮地失败(比静默存成 NULL 好,但仍然是错)。
- 分块读时一个**满**块只给 `BufferUnits - 1` 个数据单元(驱动给 NUL 留位),按整块
计数会把 3000 字节读成 3001。宽字符那条路改成 `kChunk - 1`;二进制那条路保持
`kChunk`(`SQL_C_BINARY` 没有终止符,满块给满块)。
- `SQLGetInfo` 的 W 形态返回的长度也**是字节**:拿它当单元数会让引号字符 `"` 变成
2 个"字符",于是"内层引号双写"那一支被跳过,怪表名 `uvcpp_odd"name` 被拼成
`"uvcpp_odd"name"`。改成认尾部的 NUL,不看那个长度。
- 无参数那条快路径绕过了 `SQLNumParams`:语句里有 `?` 却一个参数都不给时报的是
`EXEC_FAILED`,而这是**绑定**阶段的错,该报 `BIND_FAILED`。改成
`!params.empty() || sql.find('?') != npos` 才走直连那条路。
另有两处是**驱动的自由**,不是我们的 bug —— 套件改成按声明的能力判,不硬凑断言:
- 新增 `dialect::prepares_eagerly()`(默认 `true`):sqliteodbc 的 `SQLPrepare` 只
登记不解析,语法错与表不存在要到 `SQLExecute` 才报,于是这类后端报的是
`EXEC_FAILED`。`test_errors()` 的两条判据按这个能力分开写,标签也改成只写"是
什么错"、不写"该报哪个码"。
- `accepts_dollar_placeholder()` 在 ODBC→SQLite 上是 `false`:sqliteodbc 只认 `?`
(实测 `$1` 报 `parameter marker count incorrect`)。原来那个 `true` 是照
"ODBC 应该有"猜的。
新用例 `tests/functional/db_odbc_func.cpp`:方言按 `Driver=` 认出来的驱动族分派
(`odbc_sqlite_dialect` / `odbc_mssql_dialect`),认不出的族退 **3**,
`UVCPP_DB_TEST_ODBC_URL` 不在也退 3。它还补了共用套件没走到的两条路:二进制跨块
(3 KB BLOB,逐字节相等)与 4 字节 UTF-8 字符(UTF-16 是代理对)正好骑在块边界上
—— 都过,且都钉成了断言。
读数(本机):ODBC 到 SQLite(sqliteodbc + unixODBC)**181 通过 / 0 失败 / 3 不适用**,
另有 **57 条 ODBC 专属**;SQLite 那一家按改过的共用套件重跑 **196 / 0 / 1**,证明
套件里那两处改动没有扰动别的后端。**ODBC 到 SQL Server 那一族没跑**,别当成对过。
文档:`doc/db-guide.md` §6 不再说"ODBC 不在共用套件里",补上它的读数与「ODBC 换了
一列」(差别在**驱动**里,不在后端里)、§10 补两条 ODBC 跑法、§11 补实测到哪一步;
`CHANGELOG.md` 逐条记上面这些实测。顺带一处旧读数:SQLite 那一行原写 157 / 2,在
干净 `v1.7.0` 树上重编重跑复现不出来(同样是 196 / 1),按现测改掉,并把口径
(这台机器、这份构建编了哪些后端)写进表下。
门禁:`check_doc_versions.py` / `check_docs.py` / `check_doc_lines.py` 三条 rc=0。
`check_doc_snippets.py` 用本机唯一一个装好的前缀(`/tmp/inst-web`,1.6.10)跑:
`doc/db-guide.md` **7/7 条绿**,全仓只有 `doc/grpc-guide.md:277` 一条红 —— 那条与
本次改动无关,在干净 `v1.7.0` 树上用同一个包**逐字复现**:是包比文档旧(1.6.10 的
`uvcpp_web_request` 与 1.7 的 `body_str()` 不是一回事),不是这一支引入的。
---
CHANGELOG.md | 41 +++
doc/db-guide.md | 221 ++++++++++++--
src/db/uvcpp_db_driver.h | 4 +-
src/db/uvcpp_db_odbc.cpp | 193 +++++++++---
tests/functional/CMakeLists.txt | 3 +
tests/functional/db_odbc_func.cpp | 491 ++++++++++++++++++++++++++++++
tests/functional/db_suite.h | 25 +-
7 files changed, 897 insertions(+), 81 deletions(-)
create mode 100644 tests/functional/db_odbc_func.cpp
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4c1cb5ea..9a14fee9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -776,6 +776,47 @@ ODBC** 四个后端,结果行按表取(列名或列号)。**默认关**(
`` 里的 typedef,对 `enum class` 的限定名没影响),所以那行在 Windows 上
是空操作。
+- **ODBC 后端进了共用套件,套件立刻抓出它自己的六处**(`1.7.1-dev`)。用例在
+ `tests/functional/db_odbc_func.cpp`:方言按 `Driver=` 认出来的驱动族分派
+ (`odbc_sqlite_dialect` / `odbc_mssql_dialect`),认不出的族退 **3**。本机对着
+ sqliteodbc + unixODBC 跑:**181 通过 / 0 失败 / 3 不适用**,另有 **57 条 ODBC
+ 专属**(日志里单独一段)。这一组数全是**量出来的**,不是照着文档推的:
+
+ 1. `SQLBindParameter` 只**记指针**(缓冲**和** `StrLen_or_IndPtr` 都记),绑定的
+ 存储必须活到 `SQLExecute` 之后,而且**每个参数各一份**。共用一份时,四个参数
+ `(1, 2, 't0', 'uu0')` 存进去读回来是 `(2, 2, 't', 'u')` —— 每个都只剩最后一次
+ 写入的一个字节;
+ 2. `SQL_C_WCHAR` 的长度指示器单位是**字节**:给字符数会把 `"张三"` 截成 `"张"`、
+ `'abcdef'` 截成 `'abc'`;
+ 3. 空串与 NULL 是靠**指针**分的:有效指针 + `ind=0` 是空串,`nullptr` + `ind=0` 是
+ NULL。空串上 `std::string::data()` 给不给非空指针是实现自由,所以代码里备了两个
+ 哑元(`kEmptyByte` / `kEmptyWide`)——这条错了的话,写空串会**响**在非空约束上,
+ 而不是静默存成 NULL;
+ 4. 分块读时一个**满**块只给 `BufferUnits - 1` 个数据单元(驱动给 NUL 留位),按整块
+ 计数会把 3000 字节读成 3001 字节;
+ 5. `SQLGetInfo` 的 W 形态返回的长度也**是字节**:拿它当单元数会让引号字符 `"` 变成
+ 2 个"字符",于是"内层引号双写"那一支被跳过,怪表名 `uvcpp_odd"name` 被拼成
+ `"uvcpp_odd"name"`。改成认尾部的 NUL,不看那个长度;
+ 6. 无参数那条快路径绕过了 `SQLNumParams` 检查:语句里写了 `?` 却一个参数都不给时
+ 报的是 `EXEC_FAILED`,而这是**绑定**阶段的错,该报 `BIND_FAILED`。
+
+ 另有两处是**驱动的自由**,不是我们的 bug —— 套件改成"按声明的能力判",不硬凑断言:
+ sqliteodbc 的 `SQLPrepare` 只**登记**不解析(语法错与表不存在要到 `SQLExecute` 才
+ 报),因此 `db_suite.h` 多了一个 `dialect::prepares_eagerly()`;它同样**不接受**
+ `$1` 占位符(`parameter marker count incorrect`),所以 ODBC→SQLite 的
+ `accepts_dollar_placeholder()` 是 `false`(原来是照"ODBC 应该有"猜的 `true`)。
+ 这两条改的是共用套件,所以 SQLite 那一家按同一份套件重跑过:**196 通过 / 0 失败 /
+ 1 不适用**,改动没有扰动别的后端。
+
+ 顺手把两条套件原本没走到的路也**量了**并钉成断言(都过):二进制跨块
+ (3 KB BLOB,逐字节相等 —— `SQL_C_BINARY` 没有终止符,满块给满 `kChunk`),以及
+ 4 字节 UTF-8 字符(UTF-16 是**代理对**)正好骑在块边界上(写路径按代理对编码,
+ 驱动不会在缓冲边界把它劈开)。
+
+ **没跑的是 ODBC 到 SQL Server 那一族**:`odbc_mssql_dialect` 写好了(`INT
+ IDENTITY(1,1) PRIMARY KEY` / `NVARCHAR(MAX)` / `VARBINARY(MAX)` / `DATETIME2` /
+ `BIT` / `DECIMAL(18,2)`),但这台机器上没装 msodbcsql18。别把它当成已经对过。
+
- **MySQL / PostgreSQL 的客户端库改成可以运行时加载:`UVCPP_DB_RUNTIME_LOAD`**
(`1.6.54-dev`,默认 `OFF`)。直接链的后果不是"少个功能"而是**进程起不来**:
`DT_NEEDED`(Windows 上是 PE 的导入表)是**加载期**解析的,产物上多一条
diff --git a/doc/db-guide.md b/doc/db-guide.md
index 23e6785a..41849d2c 100644
--- a/doc/db-guide.md
+++ b/doc/db-guide.md
@@ -1,8 +1,10 @@
# 数据库模块指南
`src/db/` 是**库外的一层薄封装**:一个连接 = 一个 `uvcpp_db_client`,底下挂
-SQLite / MySQL / PostgreSQL 三个后端,SQL 由调用方写,结果按**表**取(列名 →
-值)。它不参与事件循环,也不是 ORM —— 没有模型、没有迁移、没有关系映射。
+SQLite / MySQL / PostgreSQL / ODBC 四个后端,SQL 由调用方写,结果按**表**取
+(列名 → 值)。前三个各对着**一种**数据库,第四个对着的是一个**接口** —— 连什么库由
+连接串里的驱动名决定(见 [§1](#1-怎么开) 的「ODBC:驱动管理器与**具体驱动**」)。
+它不参与事件循环,也不是 ORM —— 没有模型、没有迁移、没有关系映射。
(建在它上面、替你把 `uv_queue_work` 那段样板写掉的 `uvcpp_db_async` 与
`uvcpp_db_pool` 才碰循环,见 [§3](#3-公开面);**底座本身**不认识 `uvcpp_loop`。)
@@ -10,12 +12,17 @@ SQLite / MySQL / PostgreSQL 三个后端,SQL 由调用方写,结果按**表*
>
> **同步的底座**(异步门面与连接池建在它上面)、**一个 client 一个连接**、
> **不自动重连**、**返回码不抛异常**,
-> 把三个后端在"同一条 SQL 该得到同一个结果"这件事上钉到一套契约里,
-> 并把三家**真不一样**的地方如实写在这里,而不是假装它们一样。
+> 把 SQLite / MySQL / PostgreSQL 三个后端在"同一条 SQL 该得到同一个结果"这件事上
+> 钉到一套契约里(第四个后端 ODBC 也进了这套套件,但它的方言按数据源分派 ——
+> [§6](#6-三个后端钉住的一致与如实列出的三处不同) 写了它偏在哪、
+> [§11](#11-没做的如实列出) 写了哪一族还没跑过),并把各家**真不一样**的地方如实
+> 写在这里,而不是假装它们一样。
>
> 它默认**关**(`UVCPP_ENABLE_DB=OFF`):它是全仓唯一一个必需第三方客户端库的
-> 模块。发布配置**显式打开**它,所以预编译包里带着这个模块 —— 而且**三个后端都在**
-> (1.7.0 起;SQLite 从源码编,MySQL/PG 编进来但库走运行时加载,见 §1 与 §3)。
+> 模块。发布配置**显式打开**它,所以预编译包里带着这个模块 —— `1.7.0` 那个包里
+> SQLite / MySQL / PostgreSQL **三个都在**(SQLite 从源码编,MySQL/PG 编进来但库走
+> 运行时加载,见 §1 与 §3)。ODBC 是 `1.7.1-dev` 才加的,**还没有哪个已发布的包
+> 带过它**。
## 目录
@@ -47,15 +54,16 @@ cmake --build build-db -j
| `UVCPP_DB_SQLITE_FROM_SOURCE` | `OFF` | 上面那个 SQLite 后端改用**钉死哈希的源码 amalgamation**自己编,不走 `find_package`。**六条发布腿都是这个**(见下一节) |
| `UVCPP_ENABLE_DB_MYSQL` | `ON` | 编 MySQL 后端(要 `mysql.h` + libmysqlclient) |
| `UVCPP_ENABLE_DB_PGSQL` | `ON` | 编 PostgreSQL 后端(要 `libpq-fe.h` + libpq) |
-| `UVCPP_DB_RUNTIME_LOAD` | `OFF` | MySQL/PG 的**客户端库**改成运行时加载(`dlopen`/`LoadLibrary`),只是**不在链接行上出现** —— 头还是要(见下面「运行时加载」一节) |
+| `UVCPP_ENABLE_DB_ODBC` | `ON` | 编 ODBC 后端(要 `sql.h` + 驱动管理器)。**连什么库不在这里定** —— 那是连接串里 `Driver=` 的事(见下面「ODBC:驱动管理器与**具体驱动**」) |
+| `UVCPP_DB_RUNTIME_LOAD` | `OFF` | MySQL/PG 的**客户端库**改成运行时加载(`dlopen`/`LoadLibrary`),只是**不在链接行上出现** —— 头还是要(见下面「运行时加载」一节)。ODBC 不走这条路,理由见那一节 |
-三个子开关的语义是**"找得到就编、找不到就降级并出声"**,不是"我要不要用它":
+四个子开关的语义是**"找得到就编、找不到就降级并出声"**,不是"我要不要用它":
想明确裁掉哪一个才写 `=OFF`(那时连"找不到"的警告都不会有)。
降级链是**两级**,两级都是 `message(WARNING)` + 普通变量 `set(... OFF)`:
1. 某一个后端找不到 → 关掉**那一个**,其余照编;
-2. 三个都关掉了 → 关掉**整个模块** —— 那样的库能编出一个 `uvcpp_db_client`,
+2. 四个都关掉了 → 关掉**整个模块** —— 那样的库能编出一个 `uvcpp_db_client`,
但它谁也打不开,属于"能配置、跑不起来"。
所以 `CMakeCache.txt` 里 `UVCPP_ENABLE_DB_MYSQL:BOOL=ON` **不代表**它编进去了
@@ -65,9 +73,15 @@ cmake --build build-db -j
-- db: SQLite 后端开(3.45.1)
-- db: MySQL 后端开(/usr/lib/x86_64-linux-gnu/libmysqlclient.so)
-- db: PostgreSQL 后端开(libpq 16.15)
+-- db: ODBC 后端开(驱动管理器 /usr/lib/x86_64-linux-gnu/libodbc.so)
-- Including db module in build (database integration)
```
+ODBC 那一行报的是**驱动管理器**(Windows 上是 `-- db: ODBC 后端开(Windows 系统
+odbc32)`)—— 它**不是**"这台机器连得上某个数据源":那件事只有 `open()` 才知道,
+见下面「ODBC:驱动管理器与**具体驱动**」。找不到 `sql.h`、或只找到头没找到
+`libodbc` 时,这一行换成一条 warning 并**只关掉 ODBC 这一个后端**,其余照编。
+
开了 `UVCPP_DB_RUNTIME_LOAD` 时这两行会换个说法(后半截是**头**的出处,
不再是库的出处):
@@ -84,13 +98,14 @@ cmake --build build-db -j
#include
int main() {
- // "sqlite,mysql,postgres" —— 只列**真的编进来**的那几个
+ // "sqlite,mysql,postgres,odbc" —— 只列**真的编进来**的那几个
std::printf("%s\n", uvcpp::uvcpp_db_drivers().c_str());
return 0;
}
```
-自己编的包少哪个后端时(**官方预编译包 1.7.0 起三个都在**,不会有这一行缺项),
+自己编的包少哪个后端时(**官方预编译包 1.7.0 里三个都在**,不会有这一行缺项;
+ODBC 从 `1.7.1-dev` 起进默认配置,还没有哪个已发布的包带过它),
**这一行是第一个该看的地方**。
> **注意这一行说的是"编进来没有",不是"这台机器上能不能用"。**
@@ -166,6 +181,52 @@ UVCPP_DB_PGSQL_LIBRARY=/nonexistent/libpq.so.5 ./myapp
于是**每个使用者**在加载这个 `.so` 时就炸。`--no-undefined` 把这条不变式交还
给链接器:漏一处就是链接期红。
+### ODBC:驱动管理器与**具体驱动**是两件事
+
+前三个后端各自钉死一种客户端库(libsqlite3 / libmysqlclient / libpq),ODBC 对着
+的是一个**接口**:编进来的是 ODBC **驱动管理器**(Linux 的 `libodbc`、Windows 的
+系统 `odbc32`),**连到哪张库**由连接串里的 `Driver=`(或 `DSN=`)决定。那个**具体
+驱动**(比如 SQL Server 的 `ODBC Driver 18 for SQL Server`,官方 `msodbcsql18`)
+**既不归构建管、也不归这个库管**:装没装、装的是哪一版,只有 `open()` 那一刻才
+知道。接 SQL Server 走的就是这条路 —— 微软没给它一个可链接的 C 客户端库,它给的
+就是 ODBC 驱动,所以在 C 层没有第二个选择;而这个后端本身是**按通用写的**:能连
+SQL Server,也能连任何装得出 ODBC 驱动的数据源(Oracle、DB2、达梦……),全文件
+没有一处"是 SQL Server 就怎么着"。
+
+由此有三条与前三个后端不同的、值得先知道的:
+
+* **开关说的是"这份产物有没有这个后端",不是"能不能连上"。** 配置期只要 `sql.h`
+ 与驱动管理器两样。缺 `sql.h` 时打一条 warning(Debian/Ubuntu 上装 `unixodbc-dev`,
+ RHEL 系 `unixODBC-devel`)并只关掉 ODBC 这一个后端。**只装运行时 `unixodbc` 的
+ 机器上也会走到这条**:那个包给的是 `libodbc.so.2`,而 `.so` 这个**开发符号链接**
+ 是 `unixodbc-dev` 才有的 —— 所以要的是 `unixodbc-dev`,不是 `unixodbc`。
+* **连接失败时报的是驱动管理器给的原文**(驱动名写错时就是它那句"找不到数据源、
+ 也没有默认驱动"),本模块不把它改写成自己的话:这句话是判"我的连接串写错了"
+ 还是"这台机器没装驱动"唯一有用的信息。它**不走 `NO_DRIVER` 那条路** ——
+ `NO_DRIVER` 的含义是"这个后端没编进这份包"(见 [§2](#2-连接串))。
+* **没有 `UVCPP_DB_RUNTIME_LOAD` 那条路,这是有意的。** 上面那一节存在的理由是
+ "缺了客户端库会让**进程起不来**";ODBC 这边不成立:驱动管理器本来就该在、而且
+ 很小,真正按需加载的是它下面的**具体驱动**,那件事归驱动管理器自己管。
+* **但它把驱动管理器**链**进来 —— 这一条的代价要单独知道。** SQLite 可以
+ (`UVCPP_DB_SQLITE_FROM_SOURCE`)编进产物、MySQL/PG 可以走运行时加载,ODBC 这份
+ 默认是**直接链** `libodbc.so.2`(Windows 上是系统 `odbc32.dll`,本来就在,不算
+ 代价)。于是 Linux 的产物上多一条 `DT_NEEDED`:
+
+ ```
+ $ objdump -p build-odbc/libuvcpp.so | grep NEEDED
+ NEEDED libuv.so.1
+ NEEDED libodbc.so.2
+ NEEDED libstdc++.so.6
+ ```
+
+ 没装 unixODBC 的机器上,这一条就是**加载期**失败 —— 正是上面「运行时加载」那一节
+ 要消灭的形状。要不要留它由你定:`-DUVCPP_ENABLE_DB_ODBC=OFF` 去掉这条依赖,其余
+ 三个后端照编。不改成运行时加载的理由不是"它小",而是**它本身就是找驱动的那个
+ 东西**:自己没有它,也就没法去找别的。
+
+连接串里的默认驱动名(`ODBC Driver 18 for SQL Server`)**只是默认**:`?Driver=` 或
+`?DSN=` 一写就换掉(见 [§2](#2-连接串))。
+
### 预编译包里的 SQLite 是**从源码编的**:`UVCPP_DB_SQLITE_FROM_SOURCE`
六条发布腿各传这几个 `-D`(**1.7.0 起**;在那之前后两行是 `=OFF`):
@@ -176,10 +237,16 @@ UVCPP_DB_PGSQL_LIBRARY=/nonexistent/libpq.so.5 ./myapp
-DUVCPP_DB_RUNTIME_LOAD=ON
```
-也就是说 —— **预编译包里的 db 模块三个后端都在**(`uvcpp_db_drivers()` 会打印
-`sqlite,mysql,postgres` —— 是**驱动的稳定短名**,`pgsql` 只是 `scheme` 的别名之一,
-见下面的别名表),但 MySQL/PG 的**客户端库**是运行时才打开的,链接行上
-不出现(见下面「客户端库运行时加载」那一节)。SQLite 那一半必须从源码编,不是漏配,
+也就是说 —— **`1.7.0` 那个预编译包里的 db 模块三个后端都在**(`uvcpp_db_drivers()`
+会打印 `sqlite,mysql,postgres` —— 是**驱动的稳定短名**,`pgsql` 只是 `scheme` 的
+别名之一,见下面的别名表),但 MySQL/PG 的**客户端库**是运行时才打开的,链接行上
+不出现(见下面「客户端库运行时加载」那一节)。**ODBC 是 `1.7.1-dev` 才加的,这六条
+腿的 `-D` 清单里还没有它** —— 它默认 `ON`,所以下一版的包里**会**有它,那意味着
+Linux 包上多一条 `DT_NEEDED libodbc.so.2`(Windows 上是系统 `odbc32`,不算代价)。
+这件事该由发布配置**显式**写出来(`-DUVCPP_ENABLE_DB_ODBC=ON` 或 `=OFF`),而不是
+靠默认值漂进去 —— 留在这里当一条待办。
+
+SQLite 那一半必须从源码编,不是漏配,
是两条实测理由把另两条路都堵死了:
| 想链系统的那份 | 实测结果 |
@@ -237,6 +304,10 @@ cmake -DUVCPP_ENABLE_DB=ON \
cmake -DUVCPP_ENABLE_DB=ON \
-DPostgreSQL_INCLUDE_DIR=/opt/pgsql/include \
-DPostgreSQL_LIBRARY=/opt/pgsql/lib/libpq.so ..
+# ODBC 是本模块自己的两个 find_*
+cmake -DUVCPP_ENABLE_DB=ON \
+ -DUVCPP_DB_ODBC_INCLUDE_DIR=/opt/unixodbc/include \
+ -DUVCPP_DB_ODBC_LIBRARY=/opt/unixodbc/lib/libodbc.so ..
```
**开了 `UVCPP_DB_RUNTIME_LOAD`**:位置的找法分成两截 ——
@@ -259,12 +330,13 @@ cmake -DUVCPP_ENABLE_DB=ON \
### 静态库上的已知缺口
-三个客户端库挂在 `$` + `PRIVATE` 下:**共享库**上
-`DT_NEEDED` 会跟着走,使用方什么都不用做;**静态库**(`uvcpp_a` / `uvcpp_a_s`)
-上那一条不传给消费者,使用方要自己链:
+四个后端用到的库(`sqlite3` / `pq` / `mysqlclient` / ODBC 的驱动管理器 `odbc`)
+挂在 `$` + `PRIVATE` 下:**共享库**上 `DT_NEEDED` 会跟着走,
+使用方什么都不用做;**静态库**(`uvcpp_a` / `uvcpp_a_s`)上那一条不传给消费者,
+使用方要自己链:
```cmake
-target_link_libraries(myapp PRIVATE uvcpp_a sqlite3 pq mysqlclient)
+target_link_libraries(myapp PRIVATE uvcpp_a sqlite3 pq mysqlclient odbc)
```
这是**故意**的取舍,与 zlib / nghttp2 那几处同形:把绝对路径写进导出集会过得了
@@ -274,7 +346,8 @@ target_link_libraries(myapp PRIVATE uvcpp_a sqlite3 pq mysqlclient)
**开了 `UVCPP_DB_RUNTIME_LOAD` 时这一节不适用**:MySQL/PG 的客户端库根本不在
`UVCPP_DB_LINK_LIBS` 里(那正是那个开关干的事),所以静态库的使用方也**不用**
再补 `pq`/`mysqlclient` —— 只剩 SQLite 那一条要不要补,取决于它是系统那份还是
-`UVCPP_DB_SQLITE_FROM_SOURCE` 编进来的静态目标。
+`UVCPP_DB_SQLITE_FROM_SOURCE` 编进来的静态目标。**ODBC 那一条不受这个开关影响**:
+驱动管理器照旧在链接行上,所以静态库的使用方照旧要补(Windows 上是 `odbc32`)。
---
@@ -284,16 +357,32 @@ target_link_libraries(myapp PRIVATE uvcpp_a sqlite3 pq mysqlclient)
sqlite:///abs/path/app.db sqlite://rel/path.db sqlite::memory:
mysql://user:pass@host:3306/dbname
postgres://user:pass@host:5432/dbname
+odbc://user:pass@host:1433/dbname?Driver={ODBC Driver 18 for SQL Server}
+odbc:?DSN=MyDsn
```
| 项 | 规则 |
|---|---|
| scheme 别名 | `postgres` / `postgresql` / `pgsql` / `pg` → `postgres`;`mariadb` → `mysql`。**别名归一化之后**才去做"认不认得"的判断 |
-| 缺省端口 | MySQL 3306、PostgreSQL 5432 |
+| 缺省端口 | MySQL 3306、PostgreSQL 5432。**ODBC 没有缺省端口**:不写就不是 `tcp:host,port` 而是 `Server=tcp:host` |
| 密码 | 可省(`mysql://root@host/db`)。URL 里带特殊字符要自己转义,本模块不做 URL 解码 |
| SQLite 的路径 | `sqlite://` 后面**原样**当一个路径用(不解析 user/host/port)。父目录不存在时驱动会**建**出来 |
+| ODBC 的 host / 库名 | `host:port` → `Server=tcp:host,port`,`/dbname` → `Database=`,`user:pass@` → `Uid=` / `Pwd=` |
+| ODBC 的 `?` 之后 | **原样**追加到 ODBC 连接串末尾(本模块不解析它,`;` 分隔),所以那里能写任何驱动认得的键 |
| 没带 scheme | `BAD_URL`。报错原文里会带上传进来的整串 |
+ODBC 那两行的**默认值只补缺**:连接串里已经写了 `Driver=` / `DSN=` / `FILEDSN=`
+就**一个都不补**(`?DSN=MyDsn` 那行就是完全不碰主机与库名的用法);没写时补的是
+`Driver={ODBC Driver 18 for SQL Server}` 加一条 `Encrypt=no`(18 版默认强制加密,
+而内网库多半没配证书 —— 不显式关掉的话,使用者会先撞上一个"证书不受信任"的握手
+错误,然后去怀疑连接串写错了)。同样,没给账号时补 `Trusted_Connection=yes`
+(Windows 集成认证:同域部署下更该用的方式,而且没有口令进连接串,也就不会进日志),
+`?Uid=` / `?User=` 里写了一个就不补。
+
+`Server=tcp:host,port` 这个**带逗号的写法是 SQL Server 的**。别的驱动的连接串文法
+各认各的,认不出 `,port` 的那个会忽略它或报错 —— 这是"通用驱动"在连接串这一层的
+固有代价,写在这里而不是藏起来:换数据源时 `?` 之后通常也要跟着换。
+
### `BAD_URL` 与 `NO_DRIVER` 是两件事
这是本模块唯一一处"两个返回码看着像、含义差很远"的分界,值得单说:
@@ -306,6 +395,11 @@ postgres://user:pass@host:5432/dbname
**不是**「这份构建里有没有」。用后者的话两种情况都是假,分不开 —— 而"打不开连接"
这个笼统说法,恰恰是最没用的一句错误信息。用例里两条都有断言。
+ODBC 上还有**第三种**"打不开",它在返回码上不算前两种:后端编进来了、驱动管理器
+也在,缺的是**那个数据源的驱动**(或者驱动名写错了)。这一种要到 `open()` 才知道,
+返回的是驱动管理器给的原话,`last_error()` 里就是它 —— 别去 `NO_DRIVER` 那边找
+(那条只会在"这份包没编 ODBC 后端"时出现)。
+
---
## 3. 公开面
@@ -703,16 +797,34 @@ std::string quote_table(uvcpp::uvcpp_db_client& db,
## 6. 三个后端:钉住的一致,与如实列出的三处不同
+> **这一节的标题说的是 SQLite / MySQL / PostgreSQL 三家**,ODBC 单独放在紧接着的
+> 「ODBC 换了一列」里。ODBC **已经进了同一个共用套件**
+> (`tests/functional/db_odbc_func.cpp`;方言按 `Driver=` 认出来的驱动族挑,读数见
+> 下面表格的最后一行),但它**跑过的那条路是 ODBC 到 SQLite**:SQL Server 那一族
+> 仍然是**没跑**的(见 [§11](#11-没做的如实列出))。读到"三个后端"、"三家"时,
+> ODBC 不在里面。
+
### 钉住的一致(共用套件 `tests/functional/db_suite.h`,三家各跑一遍)
本机实测的读数(数字各不相同是**设计如此**,见下面"三处真不一样"):
-| 后端 | 通过 | 不适用 | 日志里的那两行 |
+| 后端 | 通过 | 不适用 | 不适用的那几行 |
|---|---|---|---|
-| SQLite | 157 | 2 | `没编进来的后端 -> NO_DRIVER`、`原生时间类型标签`(它没有原生时间类型) |
-| MySQL | 159 | 1 | `没编进来的后端 -> NO_DRIVER`(这份构建三个后端都在) |
+| SQLite | 196 | 1 | `原生时间类型标签`(它没有原生时间类型) |
+| ODBC(到 SQLite) | 181 | 3 | `原生时间类型标签`、`last_insert_id`、`外键约束` |
+| MySQL | 159 | 1 | ——(沿用旧读数) |
| PostgreSQL | 159 | 1 | 同上 |
+ODBC 那一行之外还有**另一组 ODBC 专属用例**(日志里单独一段,`-- ODBC 专属 --`,
+57 通过 / 0 失败):共用套件判"三家一致"的那些契约,ODBC 专属那一组判它自己的坑。
+
+口径写清楚:读数是**这台机器、这份构建**(SQLite 与 ODBC 编进来,MySQL /
+PostgreSQL 没编)跑出来的;SQLite 那一行在 ODBC 关掉重编重跑时同样是 196 / 1,
+所以它不随"编不编 ODBC"变。MySQL / PostgreSQL 两行**是上一轮的读数**,本机没有
+可跑的 MySQL/PG 服务端,这一轮只重测了 SQLite 与 ODBC。SQLite 那一行同样和更早的
+文档对不上(旧数字是 157 / 2,本树上复现不出来)—— 复现不出来的旧读数不留着当
+"约等于",直接换成现测数字。
+
"不适用"是**独立的一格计数器**,与"通过"分开打印 —— 跳过永远不会在日志里
伪装成通过。
@@ -748,6 +860,31 @@ std::string quote_table(uvcpp::uvcpp_db_client& db,
`"YYYY-MM-DD HH:MM:SS"`):这是三家都能无损往返、且人眼直接可读的表示。
SQLite 本来就没有日期类型,PostgreSQL 的文本格式恰好就是这个形状。
+### ODBC 换了一列(差别在**驱动**里,不在后端里)
+
+前三家的差别在**库里**;ODBC 的差别在**驱动**里。同一个 `odbc` 后端,`Driver=` 指的
+是 SQLite 的 ODBC 驱动还是 SQL Server 的,占位符写法、自增主键声明、定点列声明就跟着
+变 —— 这也是它必须有两个 `dialect` 实现(`odbc_sqlite_dialect` / `odbc_mssql_dialect`)
+的原因:断开的是"后端"与"方言"的绑定。共用套件里"不能分叉断言"这条规矩对它同样成立,
+多出来的自由度**只**体现在方言上。
+
+| 项 | ODBC 到 SQLite(sqliteodbc,跑过) | ODBC 到 SQL Server(**没跑**) |
+|---|---|---|
+| `$1` 占位符 | **不接受**(`parameter marker count incorrect`) | —— |
+| 语法错 / 表不存在 | 报 `EXEC_FAILED`:`SQLPrepare` 只**登记**,不解析 SQL | —— |
+| 自增主键 | `INTEGER PRIMARY KEY AUTOINCREMENT` | —— |
+| 定点(钱) | 没有,`TEXT` 存字符串 | —— |
+| 外键 | 连接里不开 `PRAGMA foreign_keys` → **不**强制 | —— |
+| `last_insert_id()` | 没有这条路 → 不适用 | —— |
+
+右列的破折号**不是"待填"**,是"没跑" —— 那台机器上没装 msodbcsql18,右列的列声明
+只是写在 `odbc_mssql_dialect` 里等着跑,别当成已经对过。
+
+第二行那条是 `dialect::prepares_eagerly()` 这个钩子存在的理由:`SQLPrepare` 是当下
+解析 SQL、还是只把语句登记下来等 `SQLExecute` 才报错,是驱动的自由。套件不假定它,
+只按声明的能力判返回码 —— 说"预处理阶段失败"的地方在这类驱动上报的是 `EXEC_FAILED`,
+而那同样是真的失败,只是**晚一步**。
+
### 后端各自怎么读结果(想看清"为什么某条断言长这样"时看这里)
* **SQLite**:`sqlite3_column_*` 按**运行期**类型取,与列的声明类型无关
@@ -877,8 +1014,13 @@ uvcpp::uvcpp_db_status transfer(uvcpp::uvcpp_db_client& db) {
## 10. 本地怎么跑
-三条用例在 `tests/functional/`:`db_sqlite_func.cpp`(**不需要服务端**,永远真
-跑)、`db_mysql_func.cpp`、`db_pgsql_func.cpp`。共用套件在 `db_suite.h`。
+四条用例在 `tests/functional/`:`db_sqlite_func.cpp`(**不需要服务端**,永远真
+跑)、`db_mysql_func.cpp`、`db_pgsql_func.cpp`、`db_odbc_func.cpp`。共用套件在
+`db_suite.h`。
+
+**ODBC 那一条要一个真的数据源**(服务器、或一个本地文件库的驱动),所以它排在第
+四:给不出 URL 时退 3(未判定),而不是拿一份默认方言硬跑。它的**方言按 `Driver=`
+挑** —— ODBC 的方言属于数据源、不属于这个后端,认不出就退 3:
```bash
cmake -S . -B build-db3 -DUVCPP_BUILD_TESTS=ON -DUVCPP_ENABLE_DB=ON
@@ -892,6 +1034,16 @@ export UVCPP_DB_TEST_MYSQL_URL='mysql://root@127.0.0.1:3306/uvcpp_test'
export UVCPP_DB_TEST_PGSQL_URL='postgresql://uvcpp@127.0.0.1:5432/uvcpp_test'
./build-db3/tests/functional/test_db_mysql_func
./build-db3/tests/functional/test_db_pgsql_func
+
+# ODBC:SQL Server(本后端存在的理由)
+export UVCPP_DB_TEST_ODBC_URL='odbc://sa:pw@127.0.0.1:1433/uvcpp_test?Driver={ODBC Driver 18 for SQL Server}'
+./build-db3/tests/functional/test_db_odbc_func
+
+# ODBC:也可以走 sqliteodbc —— 不用服务端,一条命令就能把这条用例跑起来
+# (Ubuntu:apt-get install unixodbc libsqliteodbc,驱动落在
+# /usr/lib/x86_64-linux-gnu/odbc/libsqlite3odbc.so)
+export UVCPP_DB_TEST_ODBC_URL='odbc:?Driver=/usr/lib/x86_64-linux-gnu/odbc/libsqlite3odbc.so;Database=/tmp/uvcpp_odbc.db'
+./build-db3/tests/functional/test_db_odbc_func
```
**套件里有 `DROP TABLE` / `CREATE TABLE`,URL 必须指向一个一次性的库。**
@@ -901,8 +1053,13 @@ export UVCPP_DB_TEST_PGSQL_URL='postgresql://uvcpp@127.0.0.1:5432/uvcpp_test'
|---|---|
| `UVCPP_DB_TEST_MYSQL_URL` | MySQL 用例的连接串。没有它 → 退出码 **3**(未判定) |
| `UVCPP_DB_TEST_PGSQL_URL` | PostgreSQL 用例的连接串,同上 |
+| `UVCPP_DB_TEST_ODBC_URL` | ODBC 用例的连接串,同上(认不出 `Driver=` 是哪一族也退 3) |
| `UVCPP_DB_TEST_REQUIRE` | 设了它,缺 URL 就是**失败**(退出码 1),不是跳过 |
+**ODBC 这一条不在 CI 里**:那几台 runner 上没有任何 ODBC 数据源,装了驱动管理器也
+只够跑"驱动名不存在时报 `OPEN_FAILED`"(那是它自己的一条断言),跑不了共用套件。
+所以它的读数只能在本地产出 —— 见 [§11](#11-没做的如实列出) 里哪些还真没有。
+
退出码 3 是照本仓 gate 脚本的规矩来的:**「未判定」不是「通过」**。ctest 那边用
`SKIP_RETURN_CODE 3` 把它报成 `***Skipped` —— 与 `Passed` 在日志里是两个词。
CI 上服务容器都起了还缺 URL,说明是配置写错了,所以 CI 里额外设
@@ -926,6 +1083,18 @@ Windows 上走的是 `UVCPP_DB_SQLITE_FROM_SOURCE`(MSYS2 若装了
## 11. 没做的(如实列出)
+* **ODBC 到 SQL Server 本身。** 这个后端进了共用套件(`db_odbc_func.cpp`,
+ `UVCPP_DB_TEST_ODBC_URL`),但**跑过的那条路是 ODBC 到 SQLite**(sqliteodbc,
+ Ubuntu,驱动管理器 unixODBC;读数 181 通过 / 0 失败 / 3 不适用 + 57 条 ODBC 专属
+ 断言)。SQL Server 那一族的方言是**照语法写的、没跑过**:`msodbcsql18`、Windows、
+ DSN 形式的连接串、`SQL Server` 的 `IDENTITY` / `DATETIME2` / `BIT` 逐条落到读数
+ 上都还没有。要跑真 SQL Server 时,红的第一条先怀疑那份方言(`odbc_mssql_dialect`)
+ 的写法,而不是后端本身。**在那之前,别把"ODBC 到 SQL Server"当成与另外三家同等
+ "验证过"的后端。**
+* **ODBC 的 `last_insert_id()`(除了 SQL Server 那一族)。** 它发的就是
+ `SCOPE_IDENTITY()`,别的数据源不认,如实返回 `UNSUPPORTED`(实测:sqliteodbc 上
+ 报 `UNSUPPORTED` 并留下原因,不是编一个 0 出来)。别的数据源要报自增值,得先有
+ 一条按数据源分派的写法。
* **协程(`co_await` / C++20 那套)。** 异步门面给的是**回调 + future**两种形状
(见 [§3](#3-公开面)),本库是 C++11 的,不引入协程。
* **门面的背压。** 不限制在途条数 —— 一个连接本来就是串行的,排队等锁不会更快。
diff --git a/src/db/uvcpp_db_driver.h b/src/db/uvcpp_db_driver.h
index 9bb19b17..8fe3c496 100644
--- a/src/db/uvcpp_db_driver.h
+++ b/src/db/uvcpp_db_driver.h
@@ -54,7 +54,7 @@ namespace db_detail {
/// 密码里带 `@` 是常见的(`p@ssw0rd`),按第一个切会把密码截断、把剩下的
/// 当成主机名 —— 症状是「连接被拒」,而配置看起来完全正常。
struct uvcpp_db_url {
- std::string scheme; // 规范化后的 "sqlite" / "mysql" / "postgres"
+ std::string scheme; // 规范化后的 "sqlite" / "mysql" / "postgres" / "odbc"
std::string user;
std::string password;
std::string host;
@@ -71,7 +71,7 @@ class uvcpp_db_driver {
public:
virtual ~uvcpp_db_driver() = default;
- /// 驱动的稳定短名:"sqlite" / "mysql" / "postgres"。
+ /// 驱动的稳定短名:"sqlite" / "mysql" / "postgres" / "odbc"。
virtual const char* name() const = 0;
virtual uvcpp_db_status open(const uvcpp_db_url& url, std::string* err) = 0;
diff --git a/src/db/uvcpp_db_odbc.cpp b/src/db/uvcpp_db_odbc.cpp
index cf0a741e..42011b71 100644
--- a/src/db/uvcpp_db_odbc.cpp
+++ b/src/db/uvcpp_db_odbc.cpp
@@ -17,15 +17,23 @@
*
* ## 坑一:`SQLWCHAR` 的宽度不是固定的
*
- * 微软的 ODBC 头里 `SQLWCHAR` 是 `unsigned short`(2 字节,UTF-16);unixODBC
- * 默认是 `wchar_t`(Linux 上 4 字节,UTF-32)。同一份源码在两边"看起来一样、
- * 跑起来不一样":按 2 字节算字符偏移,在 unixODBC 上会把每个字符的后半截当成
- * 下一个字符的开头,中文表名第一个字就错,而且是**静默**错。
+ * 微软的 ODBC 头里 `SQLWCHAR` 是 `unsigned short`(2 字节,UTF-16)。unixODBC
+ * **默认也是 2 字节**(`sqltypes.h` 里 `typedef unsigned short WCHAR`),但它的
+ * `typedef` 被 `#ifdef SQL_WCHART_CONVERT` 夹着,而那个宏是**使用方**在
+ * `#include ` 之前定义的(头里的原话:"if this is set, then use a 4 byte
+ * unicode definition, instead of the 2 byte definition that MS use")。定义了就
+ * 变 `wchar_t`(Linux 上 4 字节,UTF-32)。
*
- * 所以下面不假设宽度:转换按 `sizeof(SQLWCHAR)` 分支,2 字节走 UTF-16、4 字节走
+ * 所以宽度既不能按平台假设,也不能按驱动管理器假设 —— 它是**编译这份代码的人**
+ * 定的。按 2 字节算字符偏移、实际是 4 字节时,每个字符的后半截会被当成下一个
+ * 字符的开头,中文表名第一个字就错,而且是**静默**错。
+ *
+ * 下面不假设宽度:转换按 `sizeof(SQLWCHAR)` 分支,2 字节走 UTF-16、4 字节走
* UTF-32。判据取**编译期的 `sizeof`**,不是平台宏 —— 平台宏会漏掉"Linux 上装了
* 微软驱动管理器"这种组合,而那正是 CI 与开发机常见的样子。
*
+ * 宽度定了之后,还有第二件要记的事:**长度指示器的单位是字节**(见坑四)。
+ *
* ## 坑二:文本参数不能用 ANSI 形态送
*
* 文本参数一律绑 `SQL_C_WCHAR`,不绑 `SQL_C_CHAR`。绑 `SQL_C_CHAR` 时驱动按
@@ -38,6 +46,24 @@
* `SQLBindCol` 要求给每一列一个**定长**缓冲,而定长这个数本身就是要猜的:猜小了
* 长文本被静默截断,猜大了每行都白占内存。这里改用 `SQLGetData` 的分块循环,
* 两头都不猜。代价是每格多一次函数调用 —— 对报表与表浏览这类用途完全划算。
+ *
+ * ## 坑四:`SQL_C_WCHAR` 的长度指示器是**字节**
+ *
+ * `SQLBindParameter` 的第 5 个参数(ColumnSize)在 `SQL_C_WCHAR` 下按**字符**数
+ * 给,而第 8 个(BufferLength)与长度指示器按**字节**数给。单位不一致,而且按字符
+ * 给指示器时**不报错**:驱动从缓冲里只搬 `n` 个字节,于是 `n` 个字符存进去变成
+ * `floor(n/2)` 个 —— "张三" 变 "张",纯 ASCII 就是砍一半。实测矩阵里,"2 字节单元 +
+ * 字节长度" 存回 'abcdef'/'张三' 完好,"2 字节单元 + 字符长度" 复现出 'abc'/'张',
+ * 与本模块当时的症状一致。
+ *
+ * ## 坑五:绑定缓冲必须是**每个参数一份**
+ *
+ * `SQLBindParameter` 只记指针 —— 缓冲、`StrLen_or_IndPtr` 都是。执行发生在**所有**
+ * 参数都绑完之后,所以拿几个成员变量在循环里反复赋值,等于让每个参数都指向最后一
+ * 轮写进去的那份数据;`(?, ?, ?, ?)` 逐行绑 (1,2,'t0','uu0')、(11,12,'t1','uu1')、
+ * (21,22,'t2','uu2'),实测库里进去的是 (2,2,'t','u')、(12,12,'t','u')、(22,22,'t','u')
+ * —— 四个参数全成了最后一个的值,**一个错都不报**。绑定存储因此是
+ * `std::vector`,下标一一对应,且**先 `resize` 再绑**。
*/
#include
@@ -282,6 +308,19 @@ std::string sql_brief(const std::string& sql) {
return one.substr(0, 120) + "…";
}
+/// 空串 / 空 BLOB 绑定时交给驱动的那枚**有效**指针。
+///
+/// 不能用 `nullptr` 顶替"零长度":实测 sqliteodbc 上 `nullptr` + 长度 0 会被存成
+/// **NULL**,而"有效指针 + 长度 0"才存成空串 —— 于是 `e_val NOT NULL DEFAULT ''`
+/// 那种列直接报约束冲突,症状是"插入失败",离真正的原因(缓冲指针是空的)隔着
+/// 一层。规范上 `SQL_NULL_DATA` 才是 NULL 的表达,长度 0 是空串;有的驱动把
+/// "缓冲是空指针"也当成 NULL 的一种写法,这一手就是防它。
+///
+/// 一字节哨兵够用:长度是 0,驱动一个字节都不会读,更不会写它 —— 所以几个线程
+/// 同时绑各自的空串也安全(这几个全局量从头到尾只被读)。
+unsigned char kEmptyByte = 0;
+SQLWCHAR kEmptyWide = 0;
+
} // namespace
class odbc_driver : public uvcpp_db_driver {
@@ -932,10 +971,24 @@ class odbc_driver : public uvcpp_db_driver {
SQLSMALLINT len = 0;
const SQLRETURN rc = SQLGetInfoW(dbc_, SQL_IDENTIFIER_QUOTE_CHAR, buf,
sizeof(buf), &len);
- if (SQL_SUCCEEDED(rc) && len >= 1) {
- return to_utf8(buf, static_cast(len));
- }
- return "\""; // 主流数据源都是它,报不出来时按标准走
+ if (!SQL_SUCCEEDED(rc)) return "\"";
+
+ // 取串用**结尾 NUL**,不用 `len`。
+ //
+ // `SQLGetInfo` 的 `len` 报的是**字节数**(W 形态也是),按"字符个数"用它就会
+ // 把结尾那个 NUL 也当内容收进引号串里 —— 于是 `quote_` 从 1 个字符变成
+ // 2 个(`"` 后面跟一个 NUL),下面 `quote_.size() == 1` 那条"内部引号翻倍"
+ // 的分支再也进不去。症状是**静默**的:普通标识符照旧能拼(NUL 在驱动那头被
+ // 截掉),一旦标识符里真有引号,拼出来的就是语法错的 SQL。实测:表名
+ // `uvcpp_odd"name` 拼成 `"uvcpp_odd"name"`,驱动报 unrecognized
+ // token;引号串 `"`(实测 `len=2` 字节)。
+ //
+ // 结尾 NUL 是规范保证的,而且"报字节数"和"报字符数"两种驱动都自洽 —— 比猜
+ // `len` 的单位稳。
+ size_t units = 0;
+ while (units < sizeof(buf) / sizeof(buf[0]) && buf[units] != 0) ++units;
+ if (units == 0) return "\""; // 空串按标准走
+ return to_utf8(buf, units);
}
// -------------------------------------------------------------------------
@@ -945,9 +998,18 @@ class odbc_driver : public uvcpp_db_driver {
uvcpp_db_status exec_on(SQLHSTMT stmt, const std::string& sql,
const std::vector& params,
std::string* err) {
- if (params.empty()) {
- // 无参数走 SQLExecDirect:省一次 prepare,而且 DDL 在有些驱动上压根
- // prepare 不了(MySQL 那边为同一件事单独留了条裸通道,这里同理)。
+ // 无参数走 SQLExecDirect:省一次 prepare,而且 DDL 在有些驱动上压根
+ // prepare 不了(MySQL 那边为同一件事单独留了条裸通道,这里同理)。
+ //
+ // 但 SQL 里**有占位符**却一个参数都没给时不能走这条:裸执行下去,有的驱动把
+ // 没绑的占位符当 NULL(`WHERE id = ?` 变 `WHERE id = NULL`,静默 0 行),有的
+ // 报 "unbound parameters"(`EXEC_FAILED`)。两种都不是要的答案 —— 参数个数
+ // 对不上是 `BIND_FAILED`,而且要**在执行之前**报出来。判据取"SQL 里有没有
+ // `?`"这个保守扫描:它在字符串字面量里也会命中,代价只是多走一次 prepare,
+ // 而漏判的代价是静默错数据(实测 sqliteodbc:0 个参数 + 1 个占位符,裸执行
+ // 拿到的是 `EXEC_FAILED: unbound parameters in query`)。
+ const bool may_bind = !params.empty() || sql.find('?') != std::string::npos;
+ if (!may_bind) {
wstr wsql(sql);
const SQLRETURN rc = SQLExecDirectW(stmt, wsql.p(), wsql.len());
if (!SQL_SUCCEEDED(rc)) {
@@ -999,73 +1061,87 @@ class odbc_driver : public uvcpp_db_driver {
/// 文本一律送 `SQL_C_WCHAR`(理由见文件头坑二)。数值按各自的 C 类型送,让驱动
/// 端做转换 —— 全送文本也能跑,但那样服务端拿不到类型信息,索引与隐式转换都会
/// 退化。
+ ///
+ /// 这里有两处**静默**失败的坑,都踩过:
+ ///
+ /// 1. 缓冲是**每个参数一份**(`bound_`),不是一份共用。`SQLBindParameter` 只记
+ /// 指针,共用一份的话最后绑的那个参数会把前面每个都覆盖掉 —— 实测四个参数
+ /// (1,2,'t0','uu0')/(11,12,'t1','uu1')… 全变成 (2,2,'t','u') 这类值,不报错。
+ /// 2. `SQL_C_WCHAR` 的长度指示器单位是**字节**,不是字符。按字符给的症状是长文本
+ /// 被砍掉一半(`floor(n/2)`):"张三" 存进去变成 "张",`n` 个字符存 `n/2` 个。
uvcpp_db_status bind_all(SQLHSTMT stmt,
const std::vector& params,
std::string* err) {
- // `SQLBindParameter` 只记指针,数据要活到 `SQLExecute`,所以这些缓冲是**成员**
- // —— 不能用"每次循环一个局部 string 再取 c_str()"的写法,那样 SQLExecute 读到
- // 的是一批已经析构的内存。
- text_bufs_.clear();
- text_bufs_.reserve(params.size());
+ // ★ 一次 resize 到位,然后在下面**只写元素、不增删**:`SQLBindParameter` 交出去
+ // 的是 `&bound_[i]` 里那些成员的地址,绑定中途再分配会把它们全变成野指针。
+ bound_.clear();
+ bound_.resize(params.size());
for (size_t i = 0; i < params.size(); ++i) {
const uvcpp_db_value& v = params[i];
+ bound_param& b = bound_[i];
const SQLUSMALLINT index = static_cast(i + 1);
SQLRETURN rc = SQL_SUCCESS;
if (v.is_null()) {
// 空值的两个条件:C 类型随便(不读缓冲),长度指示器必须是 SQL_NULL_DATA。
rc = SQLBindParameter(stmt, index, SQL_PARAM_INPUT, SQL_C_CHAR,
- SQL_VARCHAR, 0, 0, nullptr, 0, &null_ind_);
+ SQL_VARCHAR, 0, 0, nullptr, 0, &b.null_ind);
} else {
switch (v.type()) {
case uvcpp_db_type::INT64:
- int64_buf_ = v.to_int64();
+ b.i64 = v.to_int64();
rc = SQLBindParameter(stmt, index, SQL_PARAM_INPUT, SQL_C_SBIGINT,
- SQL_BIGINT, 0, 0, &int64_buf_, 0, nullptr);
+ SQL_BIGINT, 0, 0, &b.i64, 0, nullptr);
break;
case uvcpp_db_type::UINT64:
- uint64_buf_ = v.to_uint64();
+ b.u64 = v.to_uint64();
// 超 INT64_MAX 的 uint64 不能塞进 SQL_BIGINT —— 那会在服务端变成负数。
// 本模块没有"无符号大整数"这种 SQL 参数类型,所以拒绝,而不是截断。
- if (uint64_buf_ > static_cast(INT64_MAX)) {
+ if (b.u64 > static_cast(INT64_MAX)) {
*err = "第 " + std::to_string(i + 1) +
" 个参数是无符号数且超过 int64 上限,本模块不支持";
return uvcpp_db_status::BIND_FAILED;
}
rc = SQLBindParameter(stmt, index, SQL_PARAM_INPUT, SQL_C_UBIGINT,
- SQL_BIGINT, 0, 0, &uint64_buf_, 0, nullptr);
+ SQL_BIGINT, 0, 0, &b.u64, 0, nullptr);
break;
case uvcpp_db_type::DOUBLE:
- double_buf_ = v.to_double();
+ b.dbl = v.to_double();
rc = SQLBindParameter(stmt, index, SQL_PARAM_INPUT, SQL_C_DOUBLE,
- SQL_DOUBLE, 0, 0, &double_buf_, 0, nullptr);
+ SQL_DOUBLE, 0, 0, &b.dbl, 0, nullptr);
break;
case uvcpp_db_type::BOOL:
- bit_buf_ = v.to_bool() ? 1 : 0;
+ b.bit = v.to_bool() ? 1 : 0;
rc = SQLBindParameter(stmt, index, SQL_PARAM_INPUT, SQL_C_BIT,
- SQL_BIT, 0, 0, &bit_buf_, 0, nullptr);
+ SQL_BIT, 0, 0, &b.bit, 0, nullptr);
break;
case uvcpp_db_type::BLOB:
- bin_buf_ = v.bytes();
- bin_len_ = static_cast(bin_buf_.size());
+ b.bin = v.bytes();
+ b.len = static_cast(b.bin.size());
rc = SQLBindParameter(
stmt, index, SQL_PARAM_INPUT, SQL_C_BINARY,
- bin_buf_.size() > 8000 ? SQL_LONGVARBINARY : SQL_VARBINARY,
- static_cast(bin_buf_.size()), 0,
- bin_buf_.empty() ? nullptr : &bin_buf_[0],
- static_cast(bin_buf_.size()), &bin_len_);
+ b.bin.size() > 8000 ? SQL_LONGVARBINARY : SQL_VARBINARY,
+ static_cast(b.bin.size()), 0,
+ b.bin.empty() ? static_cast(&kEmptyByte)
+ : static_cast(&b.bin[0]),
+ static_cast(b.bin.size()), &b.len);
break;
default: { // TEXT / DATE / TIME / DATETIME
- text_bufs_.push_back(to_sqlwchar(v.bytes()));
- std::basic_string& w = text_bufs_.back();
- text_len_ = static_cast(w.size());
+ b.text = to_sqlwchar(v.bytes());
+ std::basic_string& w = b.text;
+ // 第 5 个参数(ColumnSize)在 `SQL_C_WCHAR` 下按**字符**数给,第 8 个
+ // (BufferLength)与指示器按**字节**数给 —— 两者的单位不一样,这正是
+ // 上面坑 2 的来处。
+ b.len = static_cast(w.size() * sizeof(SQLWCHAR));
const bool long_text = w.size() > kMaxPlainChars;
rc = SQLBindParameter(
stmt, index, SQL_PARAM_INPUT, SQL_C_WCHAR,
long_text ? SQL_WLONGVARCHAR : SQL_WVARCHAR,
- static_cast(w.size()), 0, w.empty() ? nullptr : &w[0],
- static_cast(w.size() * sizeof(SQLWCHAR)), &text_len_);
+ static_cast(w.size()), 0,
+ w.empty() ? static_cast(&kEmptyWide)
+ : static_cast(&w[0]),
+ static_cast(w.size() * sizeof(SQLWCHAR)), &b.len);
break;
}
}
@@ -1214,8 +1290,15 @@ class odbc_driver : public uvcpp_db_driver {
// SQL_C_WCHAR 时 ind 报的是**字节数**(这是 ODBC 里唯一按字节报长度的
// 地方),换算成字符个数的锅在这里背 —— 直接把 ind 当字符数用,结果会
// 少一半,而且中文越长错得越明显。
+ //
+ // 还有第二件事:`ind` 是**整个值**的字节数,不是这一块拿到的;而驱动在
+ // 填满缓冲时**不会**用光最后一个单元 —— 它要给结尾 NUL 留位置。所以一块
+ // 最多装 `kChunk - 1` 个数据单元,按 `kChunk` 夹会多收一个,把那个 NUL
+ // 当成数据。实测(sqliteodbc,3000 字符、缓冲 2048 单元):第 0 块报
+ // `ind=6000` 却只写了 2047 个数据单元 + NUL;按 `ind/宽度` 记成 2048,
+ // 最后读出来 3001 个字符,**尾巴上多一个 0x00 字节**。
size_t chars = static_cast(ind) / sizeof(SQLWCHAR);
- if (chars > kChunk) chars = kChunk;
+ if (chars > kChunk - 1) chars = kChunk - 1;
acc.append(buf, chars);
if (rc == SQL_SUCCESS) break;
// SQL_SUCCESS_WITH_INFO = 还有下一块
@@ -1447,17 +1530,27 @@ class odbc_driver : public uvcpp_db_driver {
/// 按标准取 `"`。
std::string quote_ = "\"";
- // 绑定缓冲。是成员而不是局部变量,因为 SQLBindParameter 只记指针、数据要活到
- // SQLExecute —— 见 bind_all 里的说明。
- std::vector > text_bufs_;
- std::string bin_buf_;
- int64_t int64_buf_ = 0;
- uint64_t uint64_buf_ = 0;
- double double_buf_ = 0.0;
- unsigned char bit_buf_ = 0;
- SQLLEN null_ind_ = SQL_NULL_DATA;
- SQLLEN text_len_ = 0;
- SQLLEN bin_len_ = 0;
+ /// 一个参数的**私有**绑定存储。
+ ///
+ /// `SQLBindParameter` 只记指针(`StrLen_or_IndPtr` 记的也是指针),而 `SQLExecute`
+ /// 发生在**所有**参数都绑完之后 —— 所以每个参数都得有自己的一份存储。用成员变量
+ /// 共用一份的症状是**静默**的:`(?, ?, ?, ?)` 逐行绑 (1,2,'t0','uu0')、(11,12,…)
+ /// 实测四列全变成最后一个参数的值,一个错都不报(见 `bind_all`)。
+ struct bound_param {
+ std::basic_string text; // SQL_C_WCHAR 的字符缓冲
+ std::string bin; // SQL_C_BINARY 的字节缓冲
+ int64_t i64 = 0;
+ uint64_t u64 = 0;
+ double dbl = 0.0;
+ unsigned char bit = 0;
+ SQLLEN len = 0; // 文本/二进制的长度指示器,单位**字节**
+ SQLLEN null_ind = SQL_NULL_DATA;
+ };
+
+ /// 绑定缓冲。是成员而不是局部变量,因为 `SQLBindParameter` 只记指针、数据要活到
+ /// `SQLExecute`。**先 `resize` 再绑**,绑定期间不许再让它重新分配(不要 `push_back`)
+ /// —— 那会让已经交出去的指针全部失效。见 `bind_all`。
+ std::vector bound_;
};
const SQLWCHAR odbc_driver::kPing[] = {'S', 'E', 'L', 'E', 'C', 'T',
diff --git a/tests/functional/CMakeLists.txt b/tests/functional/CMakeLists.txt
index 9dce7ea7..a6011478 100644
--- a/tests/functional/CMakeLists.txt
+++ b/tests/functional/CMakeLists.txt
@@ -135,6 +135,9 @@ endif()
if(NOT UVCPP_ENABLE_DB_PGSQL)
list(FILTER FUNC_SOURCES EXCLUDE REGEX "/[^/]*db_pgsql[^/]*\.cpp$")
endif()
+if(NOT UVCPP_ENABLE_DB_ODBC)
+ list(FILTER FUNC_SOURCES EXCLUDE REGEX "/[^/]*db_odbc[^/]*\.cpp$")
+endif()
foreach(test_src ${FUNC_SOURCES})
get_filename_component(test_name ${test_src} NAME_WE)
diff --git a/tests/functional/db_odbc_func.cpp b/tests/functional/db_odbc_func.cpp
new file mode 100644
index 00000000..1b4cdbac
--- /dev/null
+++ b/tests/functional/db_odbc_func.cpp
@@ -0,0 +1,491 @@
+/**
+ * @file tests/functional/db_odbc_func.cpp
+ * @brief 数据库模块 · ODBC 后端:跑共用套件(`db_suite.h`)+ 三条 ODBC 专属判据。
+ *
+ * ## 这一条要一个**真的数据源**,而"哪个数据源"是给出来的,不是猜的
+ *
+ * 连接串从环境变量 `UVCPP_DB_TEST_ODBC_URL` 取,例:
+ *
+ * # SQL Server(本后端存在的理由)
+ * UVCPP_DB_TEST_ODBC_URL='odbc://sa:pw@127.0.0.1:1433/uvcpp_test?Driver={ODBC Driver 18 for SQL Server}'
+ * # SQLite,走 sqliteodbc —— 不需要任何服务端,适合先跑通一条
+ * UVCPP_DB_TEST_ODBC_URL='odbc:?Driver=/usr/lib/x86_64-linux-gnu/odbc/libsqlite3odbc.so;Database=/tmp/uvcpp.db'
+ *
+ * 没给 URL 时:
+ *
+ * * 没设 `UVCPP_DB_TEST_REQUIRE` → 打印一行说明、**退出码 3**。ctest 那边用
+ * `SKIP_RETURN_CODE 3` 把它报成 `***Skipped`,与 `Passed` 是两个词。
+ * * 设了 `UVCPP_DB_TEST_REQUIRE=1` → **退出码 1(红)**。CI 上库都起了还
+ * 跳过,说明是配置写错了,这种时候"绿"是最坏的结果。
+ *
+ * 退出码 3 是照本仓 gate 脚本的规矩来的:**「未判定」不是「通过」**。
+ *
+ * ## 为什么这个文件里有**方言分支**,而另外三家没有
+ *
+ * ODBC 是通用驱动:**方言属于数据源,不属于这个后端**。同一份 `db_odbc_func.cpp`
+ * 连 SQL Server 与连 SQLite,`CREATE TABLE` 的写法、自增列的定义、有没有原生时间
+ * 类型,全都不一样。所以这里按 URL 里的 `Driver=` 挑一份 `dialect`:
+ *
+ * * 认得出 → 用对应的方言跑共用套件;
+ * * 认不出 → **退出码 3(未判定)**,不是拿一份默认方言硬跑。硬跑的结局是把
+ * "这个数据源的 DDL 和我们的套件不一致"报成"ODBC 后端坏了",而这两件事的
+ * 修法完全不同。
+ *
+ * 目前的方言表:`sqlite`(sqliteodbc)与 `sql server`(`msodbcsql*`)。加一族就是
+ * 加一个 `dialect` 子类 + 一条 `driver_family_of()` 分支。
+ *
+ * ## 这个库会被**改写**
+ *
+ * 套件里有 `DROP TABLE` / `CREATE TABLE`,所以 URL 必须指向一个**一次性的**库。
+ * 用例自己的表名都是 `uvcpp_db_*` 前缀,但别拿生产库试。
+ *
+ * ## 三条 ODBC 专属判据(共用套件覆盖不到的地方)
+ *
+ * 1. **四个参数各不相同、且各自是各自的值。** `SQLBindParameter` 只记指针,
+ * 缓冲共用一份时每个参数都会读到最后一个的值,**而且不报错** —— 见
+ * `test_binding_is_per_parameter()`。
+ * 2. **宽字符参数的来回是逐字节的。** 长度指示器按字符给会静默砍半
+ * (`floor(n/2)`,"张三" 变 "张"),见 `test_wide_text_round_trip()`。
+ * 3. **`?Driver=` 真的交给了驱动管理器。** 驱动名不存在时是"打不开",
+ * 不是"悄悄换了另一个驱动",见 `test_driver_passthrough()`。
+ */
+
+#include
+#include
+#include
+#include
+#include
+
+#include
+
+#include "db_suite.h"
+
+namespace {
+
+using namespace uvcpp_db_test;
+
+/// URL 里那个 `Driver=`(或 `DSN=`)指向哪一族数据源。
+enum driver_family { kFamilySqlite, kFamilySqlServer, kFamilyUnknown };
+
+std::string lower(std::string s) {
+ for (size_t i = 0; i < s.size(); ++i) {
+ if (s[i] >= 'A' && s[i] <= 'Z') s[i] = static_cast(s[i] - 'A' + 'a');
+ }
+ return s;
+}
+
+/// 认族**只看 URL 文本**,不去连。连上再问 `SQLGetInfo(SQL_DBMS_NAME)` 更准,
+/// 但那时已经晚了 —— 方言要在建表之前定,而建表是套件做的第一件事。
+driver_family driver_family_of(const std::string& url) {
+ const std::string u = lower(url);
+ if (u.find("sqlite") != std::string::npos) return kFamilySqlite;
+ // `msodbcsql18` / `{ODBC Driver 18 for SQL Server}` / `SQL Server Native Client`
+ // 都落在这里。
+ if (u.find("sql server") != std::string::npos ||
+ u.find("msodbcsql") != std::string::npos ||
+ u.find("sqlncli") != std::string::npos) {
+ return kFamilySqlServer;
+ }
+ return kFamilyUnknown;
+}
+
+/// 长文本那一列怎么声明。SQL Server 上 `TEXT` 已废弃、`VARCHAR` 又会按代码页折
+/// 中文,得用 `NVARCHAR(MAX)`;SQLite 只有 `TEXT`。
+std::string text_column_decl(driver_family fam) {
+ return fam == kFamilySqlServer ? "NVARCHAR(MAX)" : "TEXT";
+}
+
+/// 二进制那一列怎么声明。SQL Server 上没有 `BLOB` 这个类型名。
+std::string blob_column_decl(driver_family fam) {
+ return fam == kFamilySqlServer ? "VARBINARY(MAX)" : "BLOB";
+}
+
+/// SQLite,经由 sqliteodbc。
+///
+/// 与 `db_sqlite_func.cpp` 里那份**本库 SQLite 后端**的方言只差一处:
+/// `enforces_foreign_keys()` 在这条路上是 **false**。SQLite 的外键默认是关的,
+/// 本库的 SQLite 后端会替连接发一句 `PRAGMA foreign_keys=ON`,而 ODBC 这条路上
+/// **没人发这一句** —— ODBC 只有 SQL,没有"pragma"这种概念,驱动也不会替谁开。
+/// 同一个数据源、同一个库、同一个 schema,走两条路外键行为不同:这不是 bug,
+/// 是"谁负责开那个开关"的差别,而它值得在测试里钉住。
+class odbc_sqlite_dialect : public dialect {
+ public:
+ const char* name() const { return "odbc"; }
+
+ std::string ph(size_t) const { return "?"; }
+
+ /// **false,与"本库 SQLite 后端收 `$1`"相反** —— 这是量出来的:占位符是
+ /// **驱动**在数,sqliteodbc 只认 `?`,给它 `$1` 报
+ /// `parameter marker count incorrect`(`PREPARE_FAILED`)。所以"SQLite 收
+ /// `$1`"是那个后端的性质,不是这个数据库的性质;ODBC 这一层把它盖掉了。
+ bool accepts_dollar_placeholder() const { return false; }
+
+ /// **false**:sqliteodbc 的 `SQLPrepare` 是空操作(SQLite 要到第一次 step 才
+ /// 解析 SQL),于是语法错、表不存在都要等到执行才报 —— `test_errors()` 那两条
+ /// 得到的是 `EXEC_FAILED`。实测的。
+ bool prepares_eagerly() const { return false; }
+
+ std::string serial_pk_type() const { return "INTEGER PRIMARY KEY AUTOINCREMENT"; }
+
+ std::string create_types_table(const std::string& t) const {
+ // `flag_val` 是 INTEGER:SQLite 没有布尔类型,0/1 就是它的布尔 ——
+ // 判的是 `to_bool()` 的值,不是类型标签。
+ // `e_val` 特意带 `NOT NULL DEFAULT ''`:把空串折成 NULL 的实现会在这里
+ // 撞上非空约束,失败得**响亮**。
+ return "CREATE TABLE " + t +
+ " (id INTEGER PRIMARY KEY AUTOINCREMENT,"
+ " i_val INTEGER, d_val REAL, s_val TEXT, b_val BLOB,"
+ " n_val TEXT, e_val TEXT NOT NULL DEFAULT '', flag_val INTEGER,"
+ " dt_val TEXT, dd_val TEXT, tt_val TEXT, " + decimal_column_decl() + ")";
+ }
+
+ std::string create_serial_table(const std::string& t) const {
+ return "CREATE TABLE " + t + " (id " + serial_pk_type() + ", name TEXT)";
+ }
+
+ std::string decimal_column_decl() const { return "dec_val TEXT"; }
+
+ bool has_native_temporal() const { return false; }
+ bool decimal_is_native() const { return false; }
+
+ /// **本后端的 `last_insert_id()` 在这条路上报不出来**(它发的是 SQL Server 的
+ /// `SCOPE_IDENTITY()`,SQLite 不认),如实返回 `UNSUPPORTED`。所以这一条在共用
+ /// 套件里是"不适用",而它**真的**返回什么由
+ /// `test_last_insert_id_is_honest()` 单独钉住 —— 不适用不等于没人管。
+ bool supports_last_insert_id() const { return false; }
+
+ bool enforces_foreign_keys() const { return false; }
+};
+
+/// SQL Server,经由 `msodbcsql*`。
+///
+/// **这一支在本机没有跑过**(手上没有 SQL Server 实例),所以它是一份照着
+/// SQL Server 语法写的方言,不是量出来的读数:`INT IDENTITY(1,1)` 是自增列的
+/// 写法、`NVARCHAR(MAX)` 承接长文本、`BIT` 是布尔、`DECIMAL(18,2)` 是原生定点。
+/// 拿它去跑真 SQL Server 是**第一次**跑,读数以那次为准;哪条断言红了,先怀疑
+/// 这里是照书写错了,而不是先怀疑后端。
+class odbc_mssql_dialect : public dialect {
+ public:
+ const char* name() const { return "odbc"; }
+
+ /// SQL Server 的 ODBC 驱动只认 `?`,不认 `$1`。
+ std::string ph(size_t) const { return "?"; }
+
+ std::string serial_pk_type() const { return "INT IDENTITY(1,1) PRIMARY KEY"; }
+
+ std::string create_types_table(const std::string& t) const {
+ return "CREATE TABLE " + t +
+ " (id INT IDENTITY(1,1) PRIMARY KEY,"
+ " i_val BIGINT, d_val FLOAT, s_val " + text_column_decl(kFamilySqlServer) +
+ ", b_val VARBINARY(MAX), n_val NVARCHAR(64),"
+ " e_val NVARCHAR(64) NOT NULL DEFAULT '', flag_val BIT,"
+ " dt_val DATETIME2, dd_val DATE, tt_val TIME, " +
+ decimal_column_decl() + ")";
+ }
+
+ std::string create_serial_table(const std::string& t) const {
+ return "CREATE TABLE " + t + " (id " + serial_pk_type() +
+ ", name NVARCHAR(64))";
+ }
+
+ std::string decimal_column_decl() const { return "dec_val DECIMAL(18,2)"; }
+
+ bool has_native_temporal() const { return true; }
+ bool decimal_is_native() const { return true; }
+ bool supports_last_insert_id() const { return true; }
+ bool enforces_foreign_keys() const { return true; }
+};
+
+// ---------------------------------------------------------------------------
+// ODBC 专属判据
+// ---------------------------------------------------------------------------
+
+/// 坑五的见证:**每个参数都得有自己的缓冲**。
+///
+/// `SQLBindParameter` 只记指针(`StrLen_or_IndPtr` 也是一枚指针),而执行发生在
+/// **所有**参数都绑完之后。用几个成员变量在循环里反复赋值,等于让每个参数都指向
+/// 最后那一轮写进去的数据 —— 实测过:`(?, ?, ?, ?)` 逐行绑
+/// (1,2,'t0','uu0')、(11,12,'t1','uu1')、(21,22,'t2','uu2'),库里进去的是
+/// (2,2,'t','u')、(12,12,'t','u')、(22,22,'t','u'),**一个错都不报**。
+///
+/// 四种类型的列都放进来:整数两列、文本两列。只放一列数值的话,共用一份缓冲反而
+/// 恰好是对的(只有一个参数),这条判据就白写了。
+void test_binding_is_per_parameter(harness& h, driver_family fam) {
+ const std::string text_col = text_column_decl(fam);
+ const std::string table = "uvcpp_db_odbc_bind";
+ h.db->execute("DROP TABLE " + table);
+ h.ok(h.db->execute("CREATE TABLE " + table + " (a BIGINT, b BIGINT, c " +
+ text_col + ", d " + text_col + ")"),
+ "建绑定判据表");
+
+ const std::string sql =
+ "INSERT INTO " + table + " (a, b, c, d) VALUES (?, ?, ?, ?)";
+ for (int i = 0; i < 3; ++i) {
+ const std::string suffix(1, static_cast('0' + i));
+ h.ok(h.db->execute(sql, {int64_t(1 + i * 10), int64_t(2 + i * 10),
+ std::string("t") + suffix,
+ std::string("uu") + suffix}),
+ "插第 " + std::to_string(i) + " 行(四个参数各不同)");
+ }
+
+ uvcpp_db_table t;
+ h.ok(h.db->query("SELECT a, b, c, d FROM " + table + " ORDER BY a",
+ static_cast(&t)),
+ "读回四列");
+ h.eq_int(static_cast(t.row_count()), 3, "三行都在");
+ if (t.row_count() != 3) return;
+
+ // 四列都要对。只对第一列的话,"所有参数都成了最后一个的值"这种错会被漏掉
+ // —— 第一列恰好是它自己那一轮写进去的(共用的缓冲里最后是最后一行)。
+ const int64_t want_a[] = {1, 11, 21};
+ const int64_t want_b[] = {2, 12, 22};
+ const char* want_c[] = {"t0", "t1", "t2"};
+ const char* want_d[] = {"uu0", "uu1", "uu2"};
+ for (size_t r = 0; r < 3; ++r) {
+ const std::string row = "第 " + std::to_string(r) + " 行 ";
+ h.eq_int(t.at(r, 0).to_int64(), want_a[r], row + "a");
+ h.eq_int(t.at(r, 1).to_int64(), want_b[r], row + "b");
+ h.eq_text(t.at(r, 2).to_text(), want_c[r], row + "c");
+ h.eq_text(t.at(r, 3).to_text(), want_d[r], row + "d");
+ }
+
+ h.db->execute("DROP TABLE " + table);
+}
+
+/// 坑四的见证:**宽字符参数的长度指示器单位是字节**。
+///
+/// 按字符给的症状是长文本被砍掉一半(`floor(n/2)` 个字符活下来):"张三" 存成
+/// "张"。**ASCII 也中招**("abcdef" 存成 "abc"),所以"只有中文才出问题"这个判断
+/// 会把方向带偏 —— 这条判据里纯 ASCII 与中文都要有。
+void test_wide_text_round_trip(harness& h, driver_family fam) {
+ const std::string table = "uvcpp_db_odbc_wide";
+ h.db->execute("DROP TABLE " + table);
+ h.ok(h.db->execute("CREATE TABLE " + table + " (k BIGINT, v " +
+ text_column_decl(fam) + ")"),
+ "建宽字符判据表");
+
+ // 字符数取偶数(4/2/6)与奇数(3 字中文按 UTF-8 是 9 字节)都有:砍半在两种
+ // 长度上都看得见,奇数的还会掉尾字。
+ const char* samples[] = {"abcd", "ab", "张三", "张三丰测试",
+ "混合 mixed 文本 12345"};
+ const size_t n = sizeof(samples) / sizeof(samples[0]);
+ for (size_t i = 0; i < n; ++i) {
+ h.ok(h.db->execute("INSERT INTO " + table + " (k, v) VALUES (?, ?)",
+ {int64_t(i), std::string(samples[i])}),
+ std::string("插入 [") + samples[i] + "]");
+ }
+
+ uvcpp_db_table t;
+ h.ok(h.db->query("SELECT k, v FROM " + table + " ORDER BY k",
+ static_cast(&t)),
+ "读回宽字符列");
+ h.eq_int(static_cast(t.row_count()), static_cast(n),
+ "样本条数对得上");
+ if (t.row_count() != n) return;
+
+ for (size_t i = 0; i < n; ++i) {
+ h.eq_text(t.at(i, 1).to_text(), samples[i],
+ std::string("往返逐字节一致:[") + samples[i] + "]");
+ }
+
+ // 参数化的**条件**也要能查回来 —— 上面测的是"存进去对不对",这一步测的是
+ // "拿它当条件找得到吗",两条路都要过绑定与宽字符编码。
+ for (size_t i = 0; i < n; ++i) {
+ uvcpp_db_table one;
+ h.ok(h.db->query("SELECT k FROM " + table + " WHERE v = ?",
+ {std::string(samples[i])},
+ static_cast(&one)),
+ std::string("拿 [") + samples[i] + "] 当条件查");
+ h.eq_int(static_cast(one.row_count()), 1,
+ std::string("按文本条件查到 1 行:[") + samples[i] + "]");
+ }
+
+ h.db->execute("DROP TABLE " + table);
+}
+
+/// `last_insert_id()` 报不出来时必须是 `UNSUPPORTED`,不是 `OK` + 一个编出来的 0。
+///
+/// 共用套件在 `supports_last_insert_id() == false` 时把整组标成"不适用",所以
+/// **没有别的判据**在看这条路 —— 而它恰恰是最容易被写成"返回个 0 算了"的地方。
+/// 编出来的 0 会成为调用方 SQL 里的 `WHERE id = 0`,查不到任何行,症状是
+/// "刚插进去的数据不见了"。
+void test_last_insert_id_is_honest(harness& h, driver_family fam,
+ const std::string& url) {
+ if (fam == kFamilySqlServer) {
+ // SQL Server 上这条路是真的实现了的(`SCOPE_IDENTITY()`),归共用套件判。
+ h.skip("报不出来时返回 UNSUPPORTED", "SQL Server 上这条能报出来");
+ return;
+ }
+
+ uvcpp_db_client db;
+ if (db.open(url) != uvcpp_db_status::OK) {
+ h.check(false, "last_insert_id 判据要一个连接:" + db.last_error());
+ return;
+ }
+ uvcpp_db_value out;
+ const uvcpp_db_status st = db.last_insert_id(&out);
+ h.eq_status(st, uvcpp_db_status::UNSUPPORTED,
+ "SQLite 这一路上 last_insert_id -> UNSUPPORTED");
+ h.check(!db.last_error().empty(), "报不出来时必须留下人能读的原因");
+ db.close();
+}
+
+/// `?Driver=` 是**原样**交给驱动管理器的,所以驱动名写错就是"打不开"。
+///
+/// 这条判据挡的是另一种实现:把不认识的驱动名悄悄换成某个默认驱动 —— 那样
+/// `Driver=MySQL` 会连上一个完全不同的数据源,而调用方以为连对了。驱动名不存在时
+/// 驱动管理器返回 `SQL_ERROR`(`IM002` 一类),落在 **`OPEN_FAILED`**,不是
+/// `NO_DRIVER`:`NO_DRIVER` 在这个模块里的意思是"这个后端没编进这份库",而 ODBC
+/// 后端明明编进来了,缺的是数据源那头的驱动。两个码混起来,排障第一步就会走到
+/// "换个包"上去。
+void test_driver_passthrough(harness& h) {
+ uvcpp_db_client tmp;
+ const uvcpp_db_status st = tmp.open(
+ "odbc://u:p@127.0.0.1:1/db?Driver={uvcpp_no_such_driver_xyz}");
+ h.eq_status(st, uvcpp_db_status::OPEN_FAILED,
+ "不存在的 Driver= -> OPEN_FAILED(不是 NO_DRIVER)");
+ h.check(!tmp.is_open(), "打不开之后 is_open() 必须是 false");
+ h.check(!tmp.last_error().empty(), "打不开必须留下 last_error");
+
+ // 驱动名给了、但那个 `.so` 路径是假的:同样是打不开,不是"照默认驱动连"。
+ uvcpp_db_client tmp2;
+ const uvcpp_db_status st2 = tmp2.open(
+ "odbc:?Driver=/nonexistent/uvcpp_no_such_driver.so;Database=/tmp/x.db");
+ h.check(st2 == uvcpp_db_status::OPEN_FAILED ||
+ st2 == uvcpp_db_status::BAD_URL,
+ std::string("假的驱动 .so 路径 -> 打不开(实得 ") +
+ uvcpp::uvcpp_db_status_name(st2) + ")");
+}
+
+/// 分块读的两条边界,共用套件都够不着(它的 blob 只有 300 字节、文本虽然是 3000
+/// 字节但全是 ASCII)。
+///
+/// * **二进制跨块**:`SQL_C_BINARY` 没有结尾 NUL,可以用满整块 —— 与文本那条
+/// "每块只用 `kChunk - 1` 个单元"的规矩相反。两条分支搞混就是静默少一个字节
+/// 或多一个字节,而 300 字节的样本永远碰不到。
+/// * **4 字节 UTF-8 字符骑在块边界上**:`U+20000` 这类字符在 UTF-16 里是一对
+/// 代理,边界切在代理对中间就会读成两个 `U+FFFD`。样本特意把它的位置摆在
+/// "第一块正好写到它"的地方(前面 2046 个 ASCII),所以这条不只是"长文本能
+/// 读回来",而是"切在代理对中间会怎样"。实测 sqliteodbc 不切代理对,本库的
+/// `to_utf8()` 只在**同一块内**拼代理对。
+void test_chunk_boundaries(harness& h, driver_family fam) {
+ const std::string table = "uvcpp_db_odbc_chunk";
+ h.db->execute("DROP TABLE " + table);
+ h.ok(h.db->execute("CREATE TABLE " + table + " (k BIGINT, b " +
+ blob_column_decl(fam) + ", s " + text_column_decl(fam) + ")"),
+ "建分块边界表");
+
+ // 3000 字节的 blob:> kChunk(2048),必然跨块。
+ std::string blob;
+ for (int i = 0; i < 3000; ++i) {
+ blob.push_back(static_cast(i % 251)); // 251 是质数:三个周期不重合
+ }
+ h.ok(h.db->execute("INSERT INTO " + table + " (k, b) VALUES (?, ?)",
+ {int64_t(1), uvcpp_db_value::blob(blob)}),
+ "插入 3000 字节 blob");
+
+ // 2046 个 'a' + U+20000(UTF-8 四字节 / UTF-16 一对代理)+ 100 个 'b'
+ std::string wide;
+ for (int i = 0; i < 2046; ++i) wide += "a";
+ wide += "\xF0\xA0\x80\x80";
+ for (int i = 0; i < 100; ++i) wide += "b";
+ h.ok(h.db->execute("INSERT INTO " + table + " (k, s) VALUES (?, ?)",
+ {int64_t(2), wide}),
+ "插入代理对骑在块边界上的样本");
+
+ uvcpp_db_table t;
+ h.ok(h.db->query("SELECT b, s FROM " + table + " ORDER BY k",
+ static_cast(&t)),
+ "读回两行");
+ h.eq_int(static_cast(t.row_count()), 2, "两行都在");
+ if (t.row_count() != 2) return;
+
+ h.eq_int(static_cast(t.at(0, 0).bytes().size()), 3000,
+ "二进制跨块:长度是 3000 字节");
+ h.check(t.at(0, 0).bytes() == blob, "二进制跨块:逐字节相等");
+ h.check(t.at(1, 1).to_text() == wide,
+ "代理对跨块:逐字节相等(实得 " +
+ std::to_string(t.at(1, 1).to_text().size()) + " 字节,真值 " +
+ std::to_string(wide.size()) + ")");
+
+ // 拿它当**条件**查:写路径(UTF-8 -> UTF-16 的代理对编码)也要对得上。
+ uvcpp_db_table one;
+ h.ok(h.db->query("SELECT k FROM " + table + " WHERE s = ?", {wide},
+ static_cast(&one)),
+ "拿代理对样本当条件查");
+ h.eq_int(static_cast(one.row_count()), 1, "条件命中 1 行");
+
+ h.db->execute("DROP TABLE " + table);
+}
+
+} // namespace
+
+int main() {
+ const char* url = std::getenv("UVCPP_DB_TEST_ODBC_URL");
+ const char* require = std::getenv("UVCPP_DB_TEST_REQUIRE");
+
+ if (url == nullptr || *url == '\0') {
+ if (require != nullptr && *require != '\0') {
+ std::cerr << "UVCPP_DB_TEST_REQUIRE 已设,但 UVCPP_DB_TEST_ODBC_URL 是空的"
+ << " —— 这是失败,不是跳过(CI 上数据源应该已经起来了)"
+ << std::endl;
+ return 1;
+ }
+ std::cout << "SKIP: 没给 UVCPP_DB_TEST_ODBC_URL,ODBC 后端这一条未判定"
+ << std::endl;
+ return 3; // ctest 的 SKIP_RETURN_CODE
+ }
+
+ // 先确认这个库真的带了 ODBC 后端。CMake 那边已经把"没编进来"的用例摘掉了,
+ // 所以走到这里还说没有,就是配置错了 —— 报错,不跳过。
+ if (uvcpp::uvcpp_db_drivers().find("odbc") == std::string::npos) {
+ std::cerr << "这份构建没有 ODBC 后端(已编入:" << uvcpp::uvcpp_db_drivers()
+ << ")—— 这一条用例不该被注册" << std::endl;
+ return 1;
+ }
+
+ const std::string u(url);
+ const driver_family fam = driver_family_of(u);
+
+ if (fam == kFamilyUnknown) {
+ // 认不出方言时**退出码 3**:这不是"ODBC 后端坏了",是"这个数据源的 DDL
+ // 本用例还不会写"。硬拿一份默认方言跑,红的会是套件里的建表语句,而那条红
+ // 会指向完全错误的方向。
+ std::cout << "SKIP: 认不出这个 URL 的方言(" << u
+ << ")—— 共用套件要按数据源的 DDL 跑,本用例现在只认 sqlite 与 "
+ "sql server 两族。ODBC 后端这一条**未判定**" << std::endl;
+ return 3;
+ }
+
+ odbc_sqlite_dialect sqlite_d;
+ odbc_mssql_dialect mssql_d;
+ const dialect& d = (fam == kFamilySqlite)
+ ? static_cast(sqlite_d)
+ : static_cast(mssql_d);
+
+ std::cout << "-- ODBC (" << u << ") --" << std::endl;
+ const int suite_rc = run_suite(u, d);
+
+ std::cout << "-- ODBC 专属 --" << std::endl;
+ harness h;
+ uvcpp_db_client db;
+ h.db = &db;
+ h.d = &d;
+ if (db.open(u) == uvcpp_db_status::OK) {
+ test_binding_is_per_parameter(h, fam);
+ test_wide_text_round_trip(h, fam);
+ test_chunk_boundaries(h, fam);
+ test_last_insert_id_is_honest(h, fam, u);
+ test_driver_passthrough(h);
+ db.close();
+ } else {
+ h.check(false, "专属判据要一个打开的连接,但 open 失败:" + db.last_error());
+ }
+
+ std::cout << "-- ODBC 专属:" << h.passed << " 通过 / " << h.failed << " 失败 / "
+ << h.not_applicable << " 不适用 --" << std::endl;
+ std::cout << (suite_rc == 0 && h.failed == 0 ? "db_odbc_func: 全部通过"
+ : "db_odbc_func: 有失败")
+ << std::endl;
+ return (suite_rc == 0 && h.failed == 0) ? 0 : 1;
+}
diff --git a/tests/functional/db_suite.h b/tests/functional/db_suite.h
index e080a11d..7ddf827d 100644
--- a/tests/functional/db_suite.h
+++ b/tests/functional/db_suite.h
@@ -73,8 +73,20 @@ class dialect {
/// 在 SQLite 上跑绿,**不**代表它在 PostgreSQL 上也行 —— 那边报
/// `PREPARE_FAILED`(`syntax error at end of input`)。所以"绿了"这件事得按
/// 后端分开说,见证见 `test_placeholder_dialect()`。
+ ///
+ /// **同一个数据源走 ODBC 会变成 false**:ODBC 到 SQLite 这条路上占位符是
+ /// 驱动在数,sqliteodbc 只认 `?`(实测 `$1` 报
+ /// `parameter marker count incorrect`)。所以"SQLite 收 `$1`"是**那个后端**的
+ /// 性质,不是 SQLite 这个数据库的性质。
virtual bool accepts_dollar_placeholder() const { return false; }
+ /// `SQLPrepare` 在这个后端/驱动上是不是**真的**准备了。默认 true。
+ ///
+ /// false 表示准备是空操作,SQL 要到执行那一刻才被解析 —— 于是语法错、表不存在
+ /// 这类错在 `test_errors()` 里报的是 `EXEC_FAILED` 而不是 `PREPARE_FAILED`。
+ /// ODBC 把"什么时候解析"交给驱动,sqliteodbc 就是这种(读数是实测的)。
+ virtual bool prepares_eagerly() const { return true; }
+
/// 一张"每种类型来一列"的表。`e_val` 三家的声明都必须带
/// `NOT NULL DEFAULT ''` —— 见 `test_empty_string_is_not_null()`。
virtual std::string create_types_table(const std::string& t) const = 0;
@@ -620,14 +632,21 @@ inline void test_schema(harness& h, const std::string& table) {
/// 10. 错误路径。**每一条都必须"不执行任何语句"地失败**,而不是执行一半。
inline void test_errors(harness& h, const std::string& table) {
+ // 语法错与表不存在,在**准备期**就报出来的后端是 PREPARE_FAILED;准备是空操作
+ // 的那种驱动(`prepares_eagerly() == false`,ODBC 到 sqliteodbc 就是)要到执行
+ // 那一刻才知道,报的必然是 EXEC_FAILED。这不是两种"坏",是同一个错被两条路
+ // 发现 —— 所以判据按后端分开写,而不是把某个后端的读数当成通用契约。
+ const uvcpp_db_status want_prepare =
+ h.d->prepares_eagerly() ? uvcpp_db_status::PREPARE_FAILED
+ : uvcpp_db_status::EXEC_FAILED;
+
const std::string bad_sql = "SELCT * FROM " + table;
uvcpp_db_table t;
- h.eq_status(h.db->query(bad_sql, &t), uvcpp_db_status::PREPARE_FAILED,
- "语法错的 SQL -> PREPARE_FAILED");
+ h.eq_status(h.db->query(bad_sql, &t), want_prepare, "语法错的 SQL");
uvcpp_db_table t2;
h.eq_status(h.db->query("SELECT * FROM uvcpp_no_such_table_xyz", &t2),
- uvcpp_db_status::PREPARE_FAILED, "不存在的表 -> PREPARE_FAILED");
+ want_prepare, "不存在的表");
// 参数个数对不上:三家必须在**本地**或**服务端**同一处拒绝,且报同一个码。
// 不统一的话,调用方就没法写一句 `if (st == BIND_FAILED) 修参数`。
From f40700ddf770f0252dc6a8b97d290069e5d7ab32 Mon Sep 17 00:00:00 2001
From: zhuweiye
Date: Thu, 15 Oct 2026 22:02:49 +0800
Subject: [PATCH 03/11] =?UTF-8?q?feat(db):=20ODBC=20=E4=B9=9F=E6=8E=A5?=
=?UTF-8?q?=E4=B8=8A=20UVCPP=5FDB=5FRUNTIME=5FLOAD=20=E2=80=94=E2=80=94=20?=
=?UTF-8?q?=E9=A9=B1=E5=8A=A8=E7=AE=A1=E7=90=86=E5=99=A8=E5=B0=B1=E6=98=AF?=
=?UTF-8?q?=E5=AE=83=E7=9A=84"=E5=AE=A2=E6=88=B7=E7=AB=AF=E5=BA=93"?=
=?UTF-8?q?=EF=BC=881.7.1-dev=EF=BC=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
上一个提交里写着"ODBC **没有** `UVCPP_DB_RUNTIME_LOAD` 那条路:驱动管理器本来就该在、
而且很小"。那句话**只在 Windows 上成立**(`odbc32.dll` 属于操作系统,和 `ws2_32` 同类,
链接它没有代价);Linux / macOS 上驱动管理器是一个普通包(unixODBC),链上它就是在
产物里加一条 `DT_NEEDED libodbc.so.2` —— 与 libmysqlclient / libpq 完全同构:**没装过
的机器连加载这份 `.so` 都做不到**,哪怕一行 db 代码都不碰。所以 ODBC 与 MySQL/PG 走同
一条规矩,**默认档不动**(`OFF`)。
实测(两棵树只差这一个开关,其余开关逐条相同):
- `/tmp/uvcpp-off`(`OFF`):`readelf -d` 的 `NEEDED` 里有 `libodbc.so.2`;
`nm -u …/uvcpp_db_odbc.cpp.o` 里 `SQL*` 未定义符号 **24** 个。
- `/tmp/uvcpp-rt`(`-DUVCPP_DB_RUNTIME_LOAD=ON`):那条 `NEEDED` 不见了;同一份目标
文件里 `SQL*` 未定义符号 **0** 个。
- 两棵树各跑两套(都对着 sqliteodbc + unixODBC):ODBC **181 通过 / 0 失败 / 3 不适用**
(另有 **57** 条 ODBC 专属),SQLite **196 / 0 / 1** —— 换个开关不动读数。
- 负路径:`UVCPP_DB_ODBC_LIBRARY=/nonexistent/libodbc.so.2` ⇒ `no_driver`,消息里逐个
列出试过的库名与各自的原因,并说明这是**管理器**那一层。
实现与 MySQL/PG 同形:新私有头 `src/db/uvcpp_db_odbc_api.h`(24 个入口,**从 `nm -u`
的目标文件里数出来的**,不是照 grep 抄的 —— `SQLGetInfo` 在文件里只出现在一句注释里,
照文本抄就是往表里塞一个没人提供的符号),64 处调用点改成 `api().SQLxxx(...)`,`open()`
第一件事是看 `uvcpp_db_odbc_load_error()`。候选名按平台惯例(Linux `libodbc.so.2` /
`libodbc.so`、macOS `libodbc.2.dylib` / `libodbc.dylib`、Windows `odbc32.dll`);
**iODBC 有意不在候选名里**:它是另一个驱动管理器实现,符号名一样但没在本仓量过,要用
就显式指路,别让"打开成功"盖住"换了一个没量过的实现"。`ODBC_STD` / `UNICODE` 那两个
条件重定向宏(`SQLAllocHandle` → `SQLAllocHandleStd`、`SQLSetStmtAttr` →
`SQLSetStmtAttrW`)会让"按名字取符号"与"直接链"两种构建**分叉**,所以那份 api 头把它们
堵成 `#error` —— 与 pgsql 那份同一个形状。
`CMakeLists.txt`:ODBC 选项的注释与 `UVCPP_DB_RUNTIME_LOAD` 的描述按新事实改写("链上
的是驱动管理器"与"Windows 上 `odbc32.dll` 是系统件"这条例外写清);
`UVCPP_DB_RUNTIME_LOAD=ON` 时 Windows 腿不再链 `odbc32` 导入库、非
Windows 腿**不找库**(于是"这台机器没装 unixODBC 运行库"不再等于"编不出这个后端");
`uvcpp_db_odbc_api.h` 进 `DB_HEADER_FILES` 的安装排除,`package_release.py` 的
`PRIVATE_HEADERS` 同步(三份 api 头)。`--no-undefined` 那条配套因此多一个由头:这一档
下 `nm -u` 里 0 个 `SQL*`,漏一处调用点当场红。
文档:`doc/db-guide.md` §1 的 ODBC 条目改成"它也走这条路"(含上面那对读数、候选名、
iODBC 之所以不在)、「运行时加载」那一节两张表与 `--no-undefined` 段落补 ODBC 一列、
§6 的读数表;两版 README 的 `UVCPP_ENABLE_DB_ODBC` / `UVCPP_DB_RUNTIME_LOAD` 两行;上面
那条 ODBC 条目里"没有这条路"一句一并改掉 —— 同一个 `1.7.1-dev` 里自相矛盾,留着比改掉
更糟。
门禁:`check_doc_versions.py` / `check_docs.py` / `check_doc_lines.py` 三条 rc=0。
`CMakeLists.txt` 这一改撑动了行号(净 +23 行,另有 +4 那一处),`doc_line_refs.lock` 按
`--update` 的复核报告刷新:**平移 63 条**(内容逐字节相同,改了 22 个文档里的 80 处行
号)、人工定的 **2 条**(`FetchContent_Declare` 那两行内容在文件里重了 4 处,按引用正文
认下 pugixml ↔ yyjson)、**1 条 `[胀]`**(`RELEASE.md` 引的 install 规则那一段,本次在
它内部插了一行 `list(FILTER DB_HEADER_FILES …)`,已核过引述仍然成立)。刷新前后 1051 条
键的哈希多重集逐条比对:只有那一段(已复核)的内容哈希变了,其余是纯平移。
`check_doc_snippets.py --pkg /tmp/inst-web`:`doc/db-guide.md` **7/7 绿**,全仓仍只有
`doc/grpc-guide.md:277` 那一条红,与本次无关(本机那个包是 1.6.10,比文档旧,上一提交
已在干净 `v1.7.0` 树上逐字复现过)。
没动、但要记一笔:`.github/workflows/release.yml` 的六条腿传的是 `UVCPP_ENABLE_DB=ON …
-DUVCPP_DB_RUNTIME_LOAD=ON`,**没有** `-DUVCPP_ENABLE_DB_ODBC`,而 Linux 腿只装
`zlib1g-dev ninja-build` —— 没有 `sql.h`,ODBC 后端在那些包上会被 warning 强制关掉。
要让发布的 Linux 包带 ODBC,得补 `unixodbc-dev` 并显式打开这个开关;那是另一件事,本次
没做。
---
.github/workflows/ci-linux-ubuntu.yml | 4 +-
.github/workflows/ci-macos.yml | 2 +-
.github/workflows/ci-mingw64.yml | 4 +-
.github/workflows/ci-windows-msvc.yml | 6 +-
.github/workflows/release.yml | 10 +-
CHANGELOG.md | 62 ++++--
CMakeLists.txt | 67 +++++--
README.md | 4 +-
README.zh.md | 4 +-
RELEASE.md | 4 +-
doc/ci-guide.md | 6 +-
doc/db-guide.md | 97 +++++----
doc/expand-guide.md | 6 +-
doc/grpc-guide.md | 4 +-
doc/http2-guide.md | 10 +-
doc/http2-status.md | 6 +-
doc/http3-guide.md | 28 +--
doc/json-guide.md | 2 +-
doc/json-reflect-guide.md | 2 +-
doc/quic-guide.md | 24 +--
doc/ssl-guide.md | 2 +-
doc/web-http-guide.md | 8 +-
doc/webapp-support-guide.md | 2 +-
src/db/uvcpp_db_odbc.cpp | 271 ++++++++++++++++++--------
src/db/uvcpp_db_odbc_api.h | 124 ++++++++++++
tests/tools/check_doc_snippets.py | 2 +-
tests/tools/check_docs.py | 4 +-
tests/tools/doc_line_refs.lock | 132 ++++++-------
tests/tools/package_release.py | 15 +-
29 files changed, 617 insertions(+), 295 deletions(-)
create mode 100644 src/db/uvcpp_db_odbc_api.h
diff --git a/.github/workflows/ci-linux-ubuntu.yml b/.github/workflows/ci-linux-ubuntu.yml
index a3c073dd..8c1b02a2 100644
--- a/.github/workflows/ci-linux-ubuntu.yml
+++ b/.github/workflows/ci-linux-ubuntu.yml
@@ -104,7 +104,7 @@ jobs:
# 模块关掉时它们被 `tests/functional/CMakeLists.txt` 的 glob+FILTER 摘掉,
# 而 ctest 报 "100% tests passed" 时并不会说"少了几条"。
#
- # **不需要任何新包**:pugixml 走 FetchContent(`CMakeLists.txt:1713`),
+ # **不需要任何新包**:pugixml 走 FetchContent(`CMakeLists.txt:1736`),
# 装包表与 `web` 那一格逐字相同 —— 这样两边红绿不同只可能归因到那个开关。
# 四个门禁串见 doc/ci-guide.md §5。
- feature: wsdl
@@ -139,7 +139,7 @@ jobs:
# nlohmann 那侧照编照跑(`web_app_request_func` 里深度预扫描的五条断言
# 也在),yyjson 那侧的新用例才拿得到对照物。
#
- # **不需要任何新包**:yyjson 走 FetchContent(`CMakeLists.txt:1773`,
+ # **不需要任何新包**:yyjson 走 FetchContent(`CMakeLists.txt:1796`,
# `GIT_TAG` 钉在 `YYJSON_VERSION`),装包表与 `web` 那一格逐字相同 ——
# 这样两边红绿不同只可能归因到那个开关。
# 四个门禁串见 doc/ci-guide.md §5。
diff --git a/.github/workflows/ci-macos.yml b/.github/workflows/ci-macos.yml
index 4ce7dd1c..c8b22d2c 100644
--- a/.github/workflows/ci-macos.yml
+++ b/.github/workflows/ci-macos.yml
@@ -60,7 +60,7 @@ jobs:
# 与 wsdl 不同:macOS **没有** wsdl 格(§1 的"未覆盖的格"表里那一行写的
# 是"不发版、风险低"),而 yyjson 这一格加得起 —— 它不需要任何新包。
#
- # yyjson 走 FetchContent(根 `CMakeLists.txt:1773`,`GIT_TAG` 钉在
+ # yyjson 走 FetchContent(根 `CMakeLists.txt:1796`,`GIT_TAG` 钉在
# `YYJSON_VERSION`),**不需要新包**;装包表与 `web` 逐字相同 —— 这样两边
# 红绿不同只可能归因到那个开关。
# 四个门禁串见 doc/ci-guide.md §5。
diff --git a/.github/workflows/ci-mingw64.yml b/.github/workflows/ci-mingw64.yml
index 2eb33778..7cfd768b 100644
--- a/.github/workflows/ci-mingw64.yml
+++ b/.github/workflows/ci-mingw64.yml
@@ -62,13 +62,13 @@ jobs:
# 不带 preset ⇒ 这个开关默认 OFF ⇒ `find_library` 在 MSYS2 里撞上
# C:/msys64/mingw64/lib/libuv.dll.a(runner 镜像自带 libuv)⇒ 链成
# 共享库,产物依赖 `libuv-1.dll`(MSYS2 那份的 DLL 名带 -1)。
- # 注意 `BUILD_SHARED_LIBS` 在这一路上救不了场:CMakeLists.txt:1450-1450 判的是
+ # 注意 `BUILD_SHARED_LIBS` 在这一路上救不了场:CMakeLists.txt:1473-1473 判的是
# `TARGET uv_a`,而 find_library 给的是一个**路径**,不是 target,
# 所以那个分支根本进不去。
#
# ② -DBUILD_SHARED_LIBS=OFF —— 管 llhttp(根 CMakeLists.txt:6 默认 ON ⇒
# llhttp_shared ⇒ 依赖 libllhttp.dll)。它不管 uvcpp 自己
- # (CMakeLists.txt:1961-1961 是显式的 `add_library(uvcpp SHARED ...)`)。
+ # (CMakeLists.txt:1984-1984 是显式的 `add_library(uvcpp SHARED ...)`)。
# 本地 A/B 过:OFF 时链接行是 `_deps/llhttp-build/libllhttp.a`,ON 时是
# `libllhttp.dll.a`。装到 FetchContent 的 libuv 上时它顺带也管 libuv
# (708 行挑 `uv_a`),但那一路在 CI 上本来就走不到。
diff --git a/.github/workflows/ci-windows-msvc.yml b/.github/workflows/ci-windows-msvc.yml
index ca632918..5ad92b8e 100644
--- a/.github/workflows/ci-windows-msvc.yml
+++ b/.github/workflows/ci-windows-msvc.yml
@@ -63,7 +63,7 @@ jobs:
# **发版那一刻** —— 而 release.yml 的 msvc-x64 / msvc-arm64 两条腿都开着
# 它。这一格就是那两条腿在 push/PR 阶段的替身。
#
- # pugixml 走 FetchContent(根 `CMakeLists.txt:1713`,`GIT_TAG` 钉在
+ # pugixml 走 FetchContent(根 `CMakeLists.txt:1736`,`GIT_TAG` 钉在
# `PUGIXML_VERSION`),**不需要新包**;它与 zlib 同一个形状,所以这一格
# 的装包需求与 `web` 逐字相同。
# 四个门禁串见 doc/ci-guide.md §5。
@@ -95,7 +95,7 @@ jobs:
# nlohmann 再比 `==`」—— 平台间差异正是在这种地方现形。
# (yaml 里这段 `-D` 之所以放在块序列里、不折成一行,见 §1。)
#
- # yyjson 走 FetchContent(根 `CMakeLists.txt:1773`,`GIT_TAG` 钉在
+ # yyjson 走 FetchContent(根 `CMakeLists.txt:1796`,`GIT_TAG` 钉在
# `YYJSON_VERSION`),**不需要新包**;它与 zlib 同一个形状,所以这一格
# 的装包需求与 `web` 逐字相同。
# 四个门禁串见 doc/ci-guide.md §5。
@@ -340,7 +340,7 @@ jobs:
run: |
case "${{ matrix.feature }}" in
# wsdl 也在这一组:pugixml 走 FetchContent,与 zlib 一样不是系统依赖。
- # yyjson 同理(`CMakeLists.txt:1773` 的 FetchContent)。
+ # yyjson 同理(`CMakeLists.txt:1796` 的 FetchContent)。
# db 也是:它的 SQLite 是**从源码编**的(`UVCPP_DB_SQLITE_FROM_SOURCE`),
# 所以 Windows 上不需要任何 sqlite 包 —— 这一格一个系统依赖都没有,
# 别把它塞进 win-openssl-deps.sh(那是 OpenSSL 专用的)。
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 713b7a2e..ea9b7b79 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -161,15 +161,15 @@ jobs:
# ① UVCPP_BUILD_LIBUV_FROM_SOURCE=ON:不带 preset 时它默认 OFF,
# `find_library` 会撞上 runner 镜像 MSYS2 自带的 libuv.dll.a,链成
# 共享库 ⇒ 依赖 libuv-1.dll。BUILD_SHARED_LIBS 救不了这一路
- # (CMakeLists.txt:1450-1450 判的是 TARGET uv_a,而 find_library 给的是路径)。
+ # (CMakeLists.txt:1473-1473 判的是 TARGET uv_a,而 find_library 给的是路径)。
# ② BUILD_SHARED_LIBS=OFF:管 llhttp(根 CMakeLists.txt:6 默认 ON ⇒
# llhttp_shared ⇒ 依赖 libllhttp.dll)。zlib 不受影响,
- # CMakeLists.txt:2330-2331 明确链 zlibstatic。
+ # CMakeLists.txt:2353-2354 明确链 zlibstatic。
# 本机所有树都走 CMakePresets(它有 ①),所以这两条从来没在本机复现过。
#
# ③ UVCPP_ENABLE_NGHTTP2=ON:这个开关**默认 OFF**(CMakeLists.txt:159-159),
# 而这几个打包装的 job 一直没传它 —— 于是包里根本没有 h2,README 却在
- # 介绍它。nghttp2 是唯一被强制静态的依赖(CMakeLists.txt:616-630),
+ # 介绍它。nghttp2 是唯一被强制静态的依赖(CMakeLists.txt:620-634),
# 所以开它不会给产物添任何 dll。六个打包 job 都必须传,任何一个漏了
# 就会发一个"同版本号但少一半功能"的包。
#
@@ -604,7 +604,7 @@ jobs:
- uses: actions/checkout@v4
# 不靠镜像里"碰巧装了 OpenSSL":没找到时 `find_package(OpenSSL QUIET)` 会把
- # UVCPP_ENABLE_OPENSSL 静默置 OFF(CMakeLists.txt:543-543),那样发出去的包
+ # UVCPP_ENABLE_OPENSSL 静默置 OFF(CMakeLists.txt:547-547),那样发出去的包
# 一句 SSL 都没有 —— 而它的生成头会**如实**说 0,使用者照配也拿不到 SSL。
#
# 这条要求由 `.github/scripts/win-openssl-deps.sh` 兑现,而且比原来那一行更硬:
@@ -635,7 +635,7 @@ jobs:
set -o pipefail
# 同 mingw-x64:① 让 libuv 从源码静态编(runner 上若有系统 libuv,
# 不带这个开关就会链成 uv.dll),② 让 llhttp 静态(否则 llhttp.dll)。
- # zlib 不受 ② 影响(CMakeLists.txt:2330-2331 明确链 zlibstatic)。
+ # zlib 不受 ② 影响(CMakeLists.txt:2353-2354 明确链 zlibstatic)。
# MSVC 侧 uvcpp 自己仍走 /MD + 包内自带 VC++ 运行库,与此无关。
cmake -S . -B build-msvc -G "Visual Studio 17 2022" -A x64 \
-DBUILD_SHARED_LIBS=OFF \
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9a14fee9..03edb475 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -305,7 +305,7 @@
`doc/quic-guide.md` §8 的措辞是"契约改了",不是"缺口补了"(`1.4.1`)
- **那一对私有头。** `uvcpp_quic_session.h` 里是 `ngtcp2_conn*`、`SSL*` 与
`ngtcp2_path_storage`,字段布局跟着 ngtcp2 的版本走 —— 它是第二个私有头,与
- `uvcpp_quic_ngtcp2.h` 并列。两个都不安装(`CMakeLists.txt:3095-3095`)、打包也排除
+ `uvcpp_quic_ngtcp2.h` 并列。两个都不安装(`CMakeLists.txt:3118-3118`)、打包也排除
(`tests/tools/package_release.py` 的 `PRIVATE_HEADERS`);量过:
`cmake --install build-quic --prefix /tmp/inst` 落进 `include/quic/` 的正好是那
四个公开头(`1.4.1`)
@@ -642,7 +642,7 @@
`std::string` / `uint32_t` / `const char*`,**不认识连接、不认识响应**,所以
`UVCPP_BUILD_WEB=OFF` 时照编照装。`uvcpp_grpc_server.{h,cpp}` 把上面那半接到
`uvcpp_http_server` 上,它的每一个参数都是 web 层的类型,web 关掉时**从构建与安装
- 两处都摘掉**(`CMakeLists.txt:1997-1999`、`CMakeLists.txt:2987-2992`)——
+ 两处都摘掉**(`CMakeLists.txt:2020-2022`、`CMakeLists.txt:3010-3015`)——
实测:`-DUVCPP_BUILD_WEB=OFF` 的树上 `src/grpc/uvcpp_grpc.cpp` 照编、装到前缀里的
只有 `include/grpc/uvcpp_grpc.h`;`build-capi-all` 上前缀里两个头都在。
**不加 `UVCPP_ENABLE_GRPC`**:两半的依赖是死的,多一个旋钮只会多一个能与
@@ -763,9 +763,9 @@ ODBC** 四个后端,结果行按表取(列名或列号)。**默认关**(
后端",而"要连的那个数据源有没有装驱动"**只有 `open()` 才知道** —— 驱动名写错时
拿到的是驱动管理器给的原文("找不到数据源、也没有默认驱动"),不是我们编的话。
配置期只要两样:`sql.h` 与驱动管理器(Linux 的 `libodbc`、Windows 的系统 `odbc32`)。
- **没有 `UVCPP_DB_RUNTIME_LOAD` 那条路**:驱动管理器本来就该在、而且很小;真正按需
- 加载的是它下面的**具体驱动**,那件事归驱动管理器自己管(本机 `-- db: ODBC 后端开
- (驱动管理器 …/libodbc.so)`,编、链 rc=0)。
+ **它也认 `UVCPP_DB_RUNTIME_LOAD`**:起先这里写的是"驱动管理器本来就该在、而且很小,
+ 所以不走那条路",那句话**只在 Windows 上成立** —— 下一节第一条就是按实测改它的
+ (本机 `-- db: ODBC 后端开(驱动管理器 …/libodbc.so)`,编、链 rc=0)。
写这一片时踩到一处值得记下来的头文件陷阱:unixODBC 的 `sqltypes.h` 第 75 行有一句
`#define BOOL int`(给没有 `` 的平台补齐 ODBC 那套类型表),它会把
@@ -817,6 +817,38 @@ ODBC** 四个后端,结果行按表取(列名或列号)。**默认关**(
IDENTITY(1,1) PRIMARY KEY` / `NVARCHAR(MAX)` / `VARBINARY(MAX)` / `DATETIME2` /
`BIT` / `DECIMAL(18,2)`),但这台机器上没装 msodbcsql18。别把它当成已经对过。
+- **ODBC 也接上了 `UVCPP_DB_RUNTIME_LOAD`:它的"客户端库"就是驱动管理器**
+ (`1.7.1-dev`,默认 `OFF`)。修的是上面那条里的一句判断:把驱动管理器链上,在
+ **Linux / macOS 上就是一条 `DT_NEEDED libodbc.so.2`** —— 正是这个开关要消灭的形状
+ ("没装 unixODBC 的机器连加载这份 `.so` 都做不到")。**只有 Windows 是例外**:
+ `odbc32.dll` 属于操作系统,和 `ws2_32` 同类,链接它没有代价。本机两档实测 ——
+ 两棵树(`/tmp/uvcpp-off` 与 `/tmp/uvcpp-rt`)**只差这一个开关**,其余开关逐条相同:
+
+ ```
+ $ readelf -d /tmp/uvcpp-off/libuvcpp.so | grep NEEDED # 关(默认)
+ NEEDED libuv.so.1
+ NEEDED libsqlite3.so.0
+ NEEDED libodbc.so.2
+ $ readelf -d /tmp/uvcpp-rt/libuvcpp.so | grep NEEDED # -DUVCPP_DB_RUNTIME_LOAD=ON
+ NEEDED libuv.so.1
+ NEEDED libsqlite3.so.0
+ $ nm -u …/uvcpp_db_odbc.cpp.o | grep -c ' U SQL' # 24 → 0
+ ```
+
+ 那张函数表是**从 `nm -u` 的目标文件里数出来的 24 个入口**,不是照着 grep 抄的:
+ `SQLGetInfo` 在文件里只出现在一句注释里(用的是 W 形态 `SQLGetInfoW`),照着文本
+ 抄会把一个没人提供的符号抄进表里。新私有头 `src/db/uvcpp_db_odbc_api.h` 与
+ `uvcpp_db_pgsql_api.h` 同形(进安装排除与 `package_release.py` 的
+ `PRIVATE_HEADERS`),64 处调用点改成 `api().SQLxxx(...)`,`open()` 先看
+ `uvcpp_db_odbc_load_error()`,非空就报 `NO_DRIVER` 并把候选名与各自的失败原因写在
+ 里面。**`#error` 那道闸门这次是必需的**:`ODBC_STD` 会把 `SQLAllocHandle` 重定向到
+ `SQLAllocHandleStd`、`UNICODE` 会把 `SQLSetStmtAttr` 重定向到 `SQLSetStmtAttrW`
+ (两处都在头文件里的条件宏里),按名字取符号的构建方式与直接链的会**分叉**,所以
+ 编这个 `.cpp` 时两者都必须未定义,否则当场红。负路径也量了:
+ `UVCPP_DB_ODBC_LIBRARY=/nonexistent/libodbc.so.2` ⇒ `no_driver` 加那句逐个列出试过
+ 哪些库名的话。两档各跑一遍套件,读数一致:ODBC **181 / 0 / 3**(另 57 条 ODBC
+ 专属)、SQLite **196 / 0 / 1**。默认 `OFF` 的行为逐字节不变。
+
- **MySQL / PostgreSQL 的客户端库改成可以运行时加载:`UVCPP_DB_RUNTIME_LOAD`**
(`1.6.54-dev`,默认 `OFF`)。直接链的后果不是"少个功能"而是**进程起不来**:
`DT_NEEDED`(Windows 上是 PE 的导入表)是**加载期**解析的,产物上多一条
@@ -2648,17 +2680,17 @@ ODBC** 四个后端,结果行按表取(列名或列号)。**默认关**(
dlopen 只是把这条路变成**每个装了 libpq 的人**的默认路径。)把我们的嵌入依赖藏起来,
libpq 就会解析到**系统**那份、与它自己的 libcrypto 自洽。
- 修法一(`CMakeLists.txt:2493-2535`,`UNIX AND NOT APPLE`,只给共享目标
+ 修法一(`CMakeLists.txt:2516-2558`,`UNIX AND NOT APPLE`,只给共享目标
`uvcpp`/`uvcpp_s`):`target_link_options(… PRIVATE "LINKER:--exclude-libs,ALL")`。
它只藏**来自静态归档**的符号,而这两个目标是
- `add_library(uvcpp SHARED ${UVCPP_SOURCES} …)`(`CMakeLists.txt:2193-2193`)
+ `add_library(uvcpp SHARED ${UVCPP_SOURCES} …)`(`CMakeLists.txt:2216-2216`)
**直接拿我们自己的目标文件**建的、不经归档 ⇒ 我们自己的 API 与 `uvcpp_c_*` 那套
C ABI 照样导出。只给共享目标加是因为静态库没有导出表,藏了反而让 `uvcpp_a` 的消费者
在最终链接时找不到本该由我们提供的符号;只给 ELF 加是因为 Apple 的 ld 没有这个选项
(那边是 `-exported_symbols_list`),而发布腿里本来也没有 macOS。
修法二:`uvcpp_yyjson` 是 **OBJECT** 库,它的目标文件经
- `$`(`CMakeLists.txt:2824-2824`)**直接进最终链接、也不经归档** ⇒
+ `$`(`CMakeLists.txt:2847-2847`)**直接进最终链接、也不经归档** ⇒
上面那条对它无效(实测加不加都是 43 个 `yyjson_*`)。那边改用
`C_VISIBILITY_PRESET hidden` **加** `target_compile_definitions(uvcpp_yyjson PRIVATE
yyjson_api=)`。**第二半是承重的**:上游 `yyjson.h` 给每个公开函数**显式**贴了
@@ -2702,16 +2734,16 @@ ODBC** 四个后端,结果行按表取(列名或列号)。**默认关**(
记过是陷阱(它只骗得过 CMake 的 `find_*`,骗不过编译器自己的默认搜索路径)。
修法照 `UVCPP_DB_SQLITE_FROM_SOURCE`(`CMakeLists.txt:189`)的命名与形状加一个
- **默认 OFF** 的选项(`CMakeLists.txt:242-243`),把整段发现包进
- `if(NOT UVCPP_BROTLI_FROM_SOURCE)`(`CMakeLists.txt:418-418`)。注意是**连发现都不
+ **默认 OFF** 的选项(`CMakeLists.txt:246-247`),把整段发现包进
+ `if(NOT UVCPP_BROTLI_FROM_SOURCE)`(`CMakeLists.txt:422-422`)。注意是**连发现都不
调用**,不是"找到了也偏不用":`find_path`/`find_library` 的结果会写进 cache,
调用过一次,`UVCPP_BROTLI_INCLUDE_DIR` 之类就一直留在那棵树里,同一棵树翻开关时
会把上一轮的答案漏过来。所以每个"找到系统那份"的判据都**显式**带上
- `NOT UVCPP_BROTLI_FROM_SOURCE`(`CMakeLists.txt:429-429`、`CMakeLists.txt:433-433`、
- `CMakeLists.txt:441-441`),而不是指望"没调用 = 没找到"。取源码那条支路一个字没改:
+ `NOT UVCPP_BROTLI_FROM_SOURCE`(`CMakeLists.txt:433-433`、`CMakeLists.txt:437-437`、
+ `CMakeLists.txt:445-445`),而不是指望"没调用 = 没找到"。取源码那条支路一个字没改:
选项开着 ⇒ 跳过发现 ⇒ `BROTLIENC_LIB` 仍是 `NOTFOUND` ⇒ 自然落进那一支 ⇒
- 那里照旧 `set(UVCPP_BROTLI_FROM_SOURCE TRUE)`(`CMakeLists.txt:519-519`),链接段
- (`CMakeLists.txt:2677-2677`)也就照旧按 `$` 取静态库。默认值一个都没
+ 那里照旧 `set(UVCPP_BROTLI_FROM_SOURCE TRUE)`(`CMakeLists.txt:523-523`),链接段
+ (`CMakeLists.txt:2700-2700`)也就照旧按 `$` 取静态库。默认值一个都没
动,所以既有使用者的行为逐字节不变。
判据(本机,`build-brotli2`:`-DUVCPP_ENABLE_BROTLI=ON
@@ -2897,7 +2929,7 @@ ODBC** 四个后端,结果行按表取(列名或列号)。**默认关**(
`flags` = `web` 加一个 `-DUVCPP_ENABLE_YYJSON=ON`,四道门禁串
`UVCPP_ENABLE_YYJSON:BOOL=ON` / `yyjson integrated` / `Including yyjson module in
build` / `test_yyjson_doc_func`;装包表与 `web` **逐字相同**(yyjson 走 FetchContent
- —— `CMakeLists.txt:1773`,`GIT_TAG` 钉在 `YYJSON_VERSION` —— **不发包管理器**),
+ —— `CMakeLists.txt:1796`,`GIT_TAG` 钉在 `YYJSON_VERSION` —— **不发包管理器**),
这样两边红绿不同只可能归因到那个开关。
**为什么 macOS 有一格、而 `wsdl` 没有**:`ci-guide.md` §1 给缺 macOS `wsdl` 的理由是
diff --git a/CMakeLists.txt b/CMakeLists.txt
index bf7fecc9..19a81a3b 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -194,28 +194,32 @@ option(UVCPP_ENABLE_DB_PGSQL "Build the PostgreSQL backend of the db module (ne
# 连接串里的 `Driver=` 决定。接 SQL Server 走的就是这条路 —— 微软没有给 SQL Server
# 一个可链接的 C 客户端库,`msodbcsql18` 本身就是 ODBC 驱动。
#
-# 与另两个后端不同,它**没有** `UVCPP_DB_RUNTIME_LOAD` 那条路:驱动管理器
-# (Linux 的 libodbc、Windows 的 odbc32)本来就该在,而且很小。真正按需加载的是
-# 它下面的**具体驱动**,那件事由驱动管理器自己干,不归我们管。
+# 与另两个后端一样,它也认 `UVCPP_DB_RUNTIME_LOAD`:链上的是**驱动管理器**
+# (Linux 的 libodbc、macOS 的 libodbc.dylib、Windows 的 odbc32),而 Linux/macOS
+# 上它是一个普通包 —— 没装的机器会栽在那条 `DT_NEEDED libodbc.so.2` 上,与
+# libmysqlclient/libpq 完全同构。Windows 上 `odbc32.dll` 属于操作系统,链接它没有
+# 这个代价,所以这一档主要在 Linux/macOS 的包上说话。真正按需加载的**具体驱动**
+# 是另一件事,那件事由驱动管理器自己干,不归我们管。
option(UVCPP_ENABLE_DB_ODBC
"Build the ODBC backend of the db module (needs ODBC headers + a driver manager; the data source is chosen by the connection string)" ON)
-# MySQL / PostgreSQL 两个后端**不在链接行上出现**,改成运行时打开客户端库。
+# MySQL / PostgreSQL / ODBC 三个后端**不在链接行上出现**,改成运行时打开客户端库
+# (ODBC 那一份是**驱动管理器**,具体驱动由它自己加载)。
#
# 这是给**预编译包**用的:`DT_NEEDED`(Windows 上是导入表)是**加载期**解析的,
-# 链上 libmysqlclient/libpq 就等于"没装这两种客户端库的机器上连加载都失败"——
-# 哪怕使用者只跑 HTTP、一行 db 代码都不碰。开了这个开关之后,那种机器上除"真去连
-# MySQL/PG"之外一切照常,而真去连时拿到的是 `uvcpp_db_status::NO_DRIVER` 加一句
-# 写清了试过哪些库名的话(`uvcpp_db_client::last_error()`)。
+# 链上 libmysqlclient/libpq/libodbc 就等于"没装这些客户端库的机器上连加载都失败"
+# ——哪怕使用者只跑 HTTP、一行 db 代码都不碰。开了这个开关之后,那种机器上除"真去
+# 连 MySQL/PG/ODBC"之外一切照常,而真去连时拿到的是 `uvcpp_db_status::NO_DRIVER`
+# 加一句写清了试过哪些库名的话(`uvcpp_db_client::last_error()`)。
#
-# 注意它**只管"符号从哪来"**,不改"后端在不在":仍然要有 `mysql.h`/`libpq-fe.h`
-# 才能把后端编进来(头是编译期的东西,运行时加载也省不掉),`UVCPP_DB_*_ENABLE`
-# 的取值照旧由头在不在决定。所以这个开关**不会**让一个原本编不出 MySQL 后端的
-# 环境凭空多出这个后端。
+# 注意它**只管"符号从哪来"**,不改"后端在不在":仍然要有
+# `mysql.h`/`libpq-fe.h`/`sql.h` 才能把后端编进来(头是编译期的东西,运行时加载也
+# 省不掉),`UVCPP_DB_*_ENABLE` 的取值照旧由头在不在决定。所以这个开关**不会**让
+# 一个原本编不出某个后端的环境凭空多出这个后端。
#
# 与 `UVCPP_DB_SQLITE_FROM_SOURCE` / `UVCPP_BROTLI_FROM_SOURCE`(上面)是同一个
# 形状:名字说的是"这份依赖怎么来"。
option(UVCPP_DB_RUNTIME_LOAD
- "Load the MySQL/PostgreSQL client libraries at run time instead of linking them (keeps the artifact free of DT_NEEDED entries for them)" OFF)
+ "Load the MySQL/PostgreSQL/ODBC client libraries at run time instead of linking them (keeps the artifact free of DT_NEEDED entries for them)" OFF)
# 可运行示例(examples/)。默认关:它是给使用者看的,不进库本身。
option(UVCPP_BUILD_EXAMPLES "Build the examples in examples/" OFF)
@@ -1240,16 +1244,35 @@ if(UVCPP_ENABLE_DB)
set(UVCPP_ENABLE_DB_ODBC OFF)
elseif(WIN32)
# Windows 的 ODBC 是系统组件:头在 SDK 里,库就是 odbc32.dll(一定在)。
+ # `UVCPP_DB_RUNTIME_LOAD=ON` 时连这条导入库也不要(符号由 `LoadLibrary` 取),
+ # 差别只是链接行上少一条 —— 这个平台上两种做法都安全。
list(APPEND UVCPP_DB_INCLUDE_DIRS "${UVCPP_DB_ODBC_INCLUDE_DIR}")
- list(APPEND UVCPP_DB_LINK_LIBS odbc32)
- message(STATUS "db: ODBC 后端开(Windows 系统 odbc32)")
+ if(UVCPP_DB_RUNTIME_LOAD)
+ message(STATUS
+ "db: ODBC 后端开(运行时加载 odbc32.dll,不链导入库;"
+ "头来自 ${UVCPP_DB_ODBC_INCLUDE_DIR})")
+ else()
+ list(APPEND UVCPP_DB_LINK_LIBS odbc32)
+ message(STATUS "db: ODBC 后端开(Windows 系统 odbc32)")
+ endif()
else()
# 这里找的是 unixODBC 的 `libodbc`。注意 `libodbc.so` 这个**开发符号链接**
# 是 unixodbc-dev 才给的:只装运行时的 unixodbc 时,`.so.2` 在而 `.so` 不在,
# 于是头与库都找不到 —— 会走到上面那条"找不到 sql.h",
# 而不是在这里报一句让人去猜"为什么装了 unixodbc 还不行"。
- find_library(UVCPP_DB_ODBC_LIBRARY NAMES odbc)
- if(UVCPP_DB_ODBC_LIBRARY)
+ #
+ # `UVCPP_DB_RUNTIME_LOAD=ON` 时**不找库**(与 MySQL 那段同一条理由:链接行上
+ # 多一条 libodbc 正是这个开关要消掉的东西)—— 于是配置期只剩"有没有 sql.h"
+ # 这一件事,"这台机器没装 unixODBC 运行库"不再等于"编不出这个后端"。
+ if(NOT UVCPP_DB_RUNTIME_LOAD)
+ find_library(UVCPP_DB_ODBC_LIBRARY NAMES odbc)
+ endif()
+ if(UVCPP_DB_RUNTIME_LOAD)
+ list(APPEND UVCPP_DB_INCLUDE_DIRS "${UVCPP_DB_ODBC_INCLUDE_DIR}")
+ message(STATUS
+ "db: ODBC 后端开(运行时加载,不链 libodbc;"
+ "头来自 ${UVCPP_DB_ODBC_INCLUDE_DIR})")
+ elseif(UVCPP_DB_ODBC_LIBRARY)
list(APPEND UVCPP_DB_INCLUDE_DIRS "${UVCPP_DB_ODBC_INCLUDE_DIR}")
list(APPEND UVCPP_DB_LINK_LIBS "${UVCPP_DB_ODBC_LIBRARY}")
message(STATUS "db: ODBC 后端开(驱动管理器 ${UVCPP_DB_ODBC_LIBRARY})")
@@ -1257,7 +1280,7 @@ if(UVCPP_ENABLE_DB)
message(WARNING
"UVCPP_ENABLE_DB_ODBC=ON 但只找到 sql.h、找不到 libodbc。已强制关闭这个"
"后端 —— 其余后端照编。Debian/Ubuntu 上装 unixodbc-dev(只装 unixodbc "
- "运行时不带 .so 符号链接)。")
+ "运行时不带 .so 符号链接),或者用 -DUVCPP_DB_RUNTIME_LOAD=ON 只要头。")
set(UVCPP_ENABLE_DB_ODBC OFF)
endif()
endif()
@@ -3158,9 +3181,10 @@ endif()
# 它一旦装出去,"驱动接口可以随便改"这句话就不成立了。三个后端的 .cpp 与
# `uvcpp_db_factory.cpp` 都带它,所以它必须留在源树里。
#
-# 另外三份也是私有的(`UVCPP_DB_RUNTIME_LOAD` 那套):
-# * `uvcpp_db_mysql_api.h` / `uvcpp_db_pgsql_api.h` —— 函数指针表,就是后端
-# 的内部 ABI;它们还各自 `#include ` / ``,
+# 另外四份也是私有的(`UVCPP_DB_RUNTIME_LOAD` 那套):
+# * `uvcpp_db_mysql_api.h` / `uvcpp_db_pgsql_api.h` / `uvcpp_db_odbc_api.h`
+# —— 函数指针表,就是后端的内部 ABI;它们还各自
+# `#include ` / `` / ``,
# 而**公开面一个客户端库的类型都不出现**(`uvcpp_db_client.h` 只带自己的
# 头),装出去等于让使用者以为那份客户端头也是我们的依赖。
# * `uvcpp_db_runtime_load.h` —— `dlopen`/`dlsym` 的两个原语,纯内部。
@@ -3172,6 +3196,7 @@ file(GLOB DB_HEADER_FILES "${CMAKE_SOURCE_DIR}/src/db/*.h")
list(FILTER DB_HEADER_FILES EXCLUDE REGEX "uvcpp_db_driver\\.h$")
list(FILTER DB_HEADER_FILES EXCLUDE REGEX "uvcpp_db_mysql_api\\.h$")
list(FILTER DB_HEADER_FILES EXCLUDE REGEX "uvcpp_db_pgsql_api\\.h$")
+list(FILTER DB_HEADER_FILES EXCLUDE REGEX "uvcpp_db_odbc_api\\.h$")
list(FILTER DB_HEADER_FILES EXCLUDE REGEX "uvcpp_db_runtime_load\\.h$")
if(DB_HEADER_FILES AND UVCPP_ENABLE_DB)
install(FILES ${DB_HEADER_FILES} DESTINATION include/db)
diff --git a/README.md b/README.md
index fff9e45e..308a12de 100644
--- a/README.md
+++ b/README.md
@@ -315,8 +315,8 @@ OpenSSL ≥ 3.2 with the QUIC API for QUIC and HTTP/3), and how the switches int
| `UVCPP_DB_SQLITE_FROM_SOURCE` | `OFF` | Build that SQLite backend from a **hash-pinned source amalgamation** (one static `sqlite3.c`, forced PIC) instead of `find_package(SQLite3)`. **The six release legs use it**: linking the system `libsqlite3.so` would add a `DT_NEEDED` and break the packages' self-contained promise, while the system `libsqlite3.a` on Ubuntu 24.04 is not PIC (verified: `R_X86_64_PC32 … can not be used when making a shared object`). Needs network at configure time, or the zip placed by hand; both are described in [`doc/db-guide.md`](doc/db-guide.md) |
| `UVCPP_ENABLE_DB_MYSQL` | `ON` | Build the MySQL backend (needs `mysql.h` + libmysqlclient). Non-standard installs: `-DCMAKE_PREFIX_PATH=…` or `-DUVCPP_DB_MYSQL_INCLUDE_DIR=… -DUVCPP_DB_MYSQL_LIBRARY=…`. **On by default, and on in every release package** (with `UVCPP_DB_RUNTIME_LOAD=ON`, which is what keeps the package loadable without libmysqlclient) |
| `UVCPP_ENABLE_DB_PGSQL` | `ON` | Build the PostgreSQL backend (needs `libpq-fe.h` + libpq). Non-standard installs: `-DCMAKE_PREFIX_PATH=…` or `-DPostgreSQL_INCLUDE_DIR=… -DPostgreSQL_LIBRARY=…`. **On by default, and on in every release package** (with `UVCPP_DB_RUNTIME_LOAD=ON`) |
-| `UVCPP_ENABLE_DB_ODBC` | `ON` | Build the ODBC backend of the db module (needs `sql.h` + a driver manager: `libodbc` on Linux, the system `odbc32` on Windows). Unlike the three above, it targets **an interface, not one database** — *what* it connects to is chosen by the connection string's `Driver=` / `DSN=` (Microsoft ships no linkable C client library for SQL Server; `msodbcsql18` *is* an ODBC driver), and it tries no other driver name. Not found ⇒ **this one backend is force-disabled with a warning**; the others still build. It deliberately has **no `UVCPP_DB_RUNTIME_LOAD` path**: the driver manager is expected to be present and is small, and the *actual* driver under it is loaded on demand by the manager, which is not our business. So "is this backend built" and "is there a driver for the data source you open" are two different questions — the second is answered at `open()`, in the driver manager's own words |
-| `UVCPP_DB_RUNTIME_LOAD` | `OFF` | Take the MySQL / PostgreSQL symbols from a client library loaded **at run time** (`dlopen` / `LoadLibrary`) instead of linking it. **The six release legs use it, and the reason is the packages' users, not the library**: `DT_NEEDED` (the PE import table on Windows) is resolved at **load time**, so linking libmysqlclient/libpq means a machine that has never installed either cannot even *load* the library — even if it only ever runs HTTP and never touches `db`. With this on, such a machine runs everything except "actually connecting to MySQL/PG", where it gets `uvcpp_db_status::NO_DRIVER` plus a message naming the candidates that were tried (`UVCPP_DB_MYSQL_LIBRARY` / `UVCPP_DB_PGSQL_LIBRARY` override the search). It changes only *where the symbols come from*, not *which backends exist*: `mysql.h` / `libpq-fe.h` are still needed at configure time (`MYSQL_BIND`'s layout has to come from the real header), so `UVCPP_DB_MYSQL_ENABLE` / `UVCPP_DB_PGSQL_ENABLE` still mean "the header was found". Default `OFF` leaves today's behaviour byte-for-byte. See [`doc/db-guide.md`](doc/db-guide.md) |
+| `UVCPP_ENABLE_DB_ODBC` | `ON` | Build the ODBC backend of the db module (needs `sql.h` + a driver manager: `libodbc` on Linux, the system `odbc32` on Windows). Unlike the three above, it targets **an interface, not one database** — *what* it connects to is chosen by the connection string's `Driver=` / `DSN=` (Microsoft ships no linkable C client library for SQL Server; `msodbcsql18` *is* an ODBC driver), and it tries no other driver name. Not found ⇒ **this one backend is force-disabled with a warning**; the others still build. It honours `UVCPP_DB_RUNTIME_LOAD` like MySQL/PG: what gets linked is the **driver manager**, which is a client library of the same kind (a plain unixODBC package on Linux/macOS; on Windows `odbc32.dll` belongs to the OS, so linking it costs nothing). So "is this backend built" and "is there a driver for the data source you open" are two different questions — the second is answered at `open()`, in the driver manager's own words |
+| `UVCPP_DB_RUNTIME_LOAD` | `OFF` | Take the MySQL / PostgreSQL / ODBC symbols from a client library loaded **at run time** (`dlopen` / `LoadLibrary`) instead of linking it; for ODBC that library is the **driver manager** (the actual driver is still loaded by the manager itself). **The six release legs use it, and the reason is the packages' users, not the library**: `DT_NEEDED` (the PE import table on Windows) is resolved at **load time**, so linking libmysqlclient/libpq/libodbc means a machine that has never installed those libraries cannot even *load* the library — even if it only ever runs HTTP and never touches `db`. With this on, such a machine runs everything except "actually connecting to MySQL/PG/ODBC", where it gets `uvcpp_db_status::NO_DRIVER` plus a message naming the candidates that were tried (`UVCPP_DB_MYSQL_LIBRARY` / `UVCPP_DB_PGSQL_LIBRARY` / `UVCPP_DB_ODBC_LIBRARY` override the search). It changes only *where the symbols come from*, not *which backends exist*: `mysql.h` / `libpq-fe.h` / `sql.h` are still needed at configure time (`MYSQL_BIND`'s layout has to come from the real header), so `UVCPP_DB_MYSQL_ENABLE` / `UVCPP_DB_PGSQL_ENABLE` / `UVCPP_DB_ODBC_ENABLE` still mean "the header was found". Default `OFF` leaves today's behaviour byte-for-byte. See [`doc/db-guide.md`](doc/db-guide.md) |
| `UVCPP_USE_SYSTEM_LIBUV` | `ON` | Prefer system-installed libuv |
| `UVCPP_BUILD_LIBUV_FROM_SOURCE` | `OFF` | Fetch and build libuv from source via `FetchContent` |
| `UVCPP_STATIC_RUNTIME` | `OFF` | Statically link the compiler runtime (`libgcc`/`libstdc++`) into the library. **MinGW and Linux only — a no-op on MSVC**, which uses `/MD` and ships `vcruntime`/`msvcp` in the package. **On Linux it also applies to every executable, and a *shared* `libuvcpp.so` built with it cannot be combined with a consumer that uses the system `libstdc++.so.6`** — both copies instantiate the same COMDAT templates (`std::once_flag`'s `__once_proxy`, `std::__future_base::_State_baseV2`) and ELF's global symbol scope makes one side jump into the other's frame, so the failure is a run-time segfault or `double free`, **not a link error**. **That is why the release workflow's two Linux legs no longer pass it** (1.7.0 onwards) — the Linux packages therefore need the system `libstdc++.so.6` / `libgcc_s.so.1`, which every distro with a C++ runtime already has. The two MinGW legs still pass it: PE has no global symbol scope, so there is nothing to interpose, and a MinGW user's machine may not have `libstdc++-6.dll`. See [`doc/build-guide.md`](doc/build-guide.md) |
diff --git a/README.zh.md b/README.zh.md
index faa76e47..8430e555 100644
--- a/README.zh.md
+++ b/README.zh.md
@@ -307,8 +307,8 @@ WebSocket,`-DUVCPP_ENABLE_OPENSSL=ON` 是 HTTPS,`-DUVCPP_BUILD_WEBAPP=ON`
| `UVCPP_DB_SQLITE_FROM_SOURCE` | `OFF` | 这个 SQLite 后端改成用一份**钉死哈希的源码 amalgamation** 自己编(一个静态 `sqlite3.c`,强制 PIC),不走 `find_package(SQLite3)`。**六条发布腿走的就是它**:链系统的 `libsqlite3.so` 会多一条 `DT_NEEDED`、破坏发布包的"自包含"承诺,而 Ubuntu 24.04 上系统的 `libsqlite3.a` 不是 PIC(实测 `R_X86_64_PC32 … can not be used when making a shared object`)。配置期需要联网,或者自己把 zip 放到位 —— 两种做法都写在 [`doc/db-guide.md`](doc/db-guide.md) 里 |
| `UVCPP_ENABLE_DB_MYSQL` | `ON` | 编 MySQL 后端(要 `mysql.h` 与 libmysqlclient)。装在非标准位置时:`-DCMAKE_PREFIX_PATH=…` 或 `-DUVCPP_DB_MYSQL_INCLUDE_DIR=… -DUVCPP_DB_MYSQL_LIBRARY=…`。**默认就是开的,每个发布包里也都开着**(同时用 `UVCPP_DB_RUNTIME_LOAD=ON`,这正是"没装 libmysqlclient 也能加载这份包"的原因) |
| `UVCPP_ENABLE_DB_PGSQL` | `ON` | 编 PostgreSQL 后端(要 `libpq-fe.h` 与 libpq)。装在非标准位置时:`-DCMAKE_PREFIX_PATH=…` 或 `-DPostgreSQL_INCLUDE_DIR=… -DPostgreSQL_LIBRARY=…`。**默认就是开的,每个发布包里也都开着**(同时用 `UVCPP_DB_RUNTIME_LOAD=ON`) |
-| `UVCPP_ENABLE_DB_ODBC` | `ON` | 编 db 模块的 ODBC 后端(要 `sql.h` 与一份驱动管理器:Linux 上是 `libodbc`,Windows 上是系统的 `odbc32`)。它与上面三个不是一类:那三个各对着**一种**数据库,这个对着**一个接口** —— 连什么库由连接串里的 `Driver=` / `DSN=` 决定(微软没给 SQL Server 可链接的 C 客户端库,`msodbcsql18` 本身就是 ODBC 驱动),而且它不去试别的驱动名。找不到时**只强制关掉这一个**并出声,其余后端照编。它**故意没有** `UVCPP_DB_RUNTIME_LOAD` 那条路:驱动管理器本来就该在、而且很小,真正按需加载的是它下面的**具体驱动**,那件事由驱动管理器自己干。于是"这个后端编没编"与"你要连的数据源有没有驱动"是两件事 —— 后者到 `open()` 才有答案,用的是驱动管理器的原话。见 [`doc/db-guide.md`](doc/db-guide.md) |
-| `UVCPP_DB_RUNTIME_LOAD` | `OFF` | MySQL / PostgreSQL 的符号不从链接行上来,改成**运行时**打开一份客户端库(`dlopen` / `LoadLibrary`)再取。**六条发布腿用的就是它,而理由在包的**使用者**身上,不在库里**:`DT_NEEDED`(Windows 上是导入表)是**加载期**解析的,链上 libmysqlclient/libpq 就等于"从没装过这两种客户端库的机器连**加载**这份库都做不到"—— 哪怕它只跑 HTTP、一行 `db` 代码都不碰。开了之后,那种机器上除"真去连 MySQL/PG"之外一切照常,而真去连时拿到的是 `uvcpp_db_status::NO_DRIVER` 加一句写清了试过哪些库名的话(可用环境变量 `UVCPP_DB_MYSQL_LIBRARY` / `UVCPP_DB_PGSQL_LIBRARY` 指定路径)。它改的只是**符号从哪来**,不是**有哪些后端**:配置期仍然要 `mysql.h` / `libpq-fe.h`(`MYSQL_BIND` 的布局必须从真的头里读),所以 `UVCPP_DB_MYSQL_ENABLE` / `UVCPP_DB_PGSQL_ENABLE` 的含义照旧是"头找到了"。默认 `OFF`,今天的行为逐字节不变。见 [`doc/db-guide.md`](doc/db-guide.md) |
+| `UVCPP_ENABLE_DB_ODBC` | `ON` | 编 db 模块的 ODBC 后端(要 `sql.h` 与一份驱动管理器:Linux 上是 `libodbc`,Windows 上是系统的 `odbc32`)。它与上面三个不是一类:那三个各对着**一种**数据库,这个对着**一个接口** —— 连什么库由连接串里的 `Driver=` / `DSN=` 决定(微软没给 SQL Server 可链接的 C 客户端库,`msodbcsql18` 本身就是 ODBC 驱动),而且它不去试别的驱动名。找不到时**只强制关掉这一个**并出声,其余后端照编。它与 MySQL/PG 一样认 `UVCPP_DB_RUNTIME_LOAD`:链上的是**驱动管理器**,那也是"客户端库"那一类(Linux / macOS 上是普通包 unixODBC;Windows 上 `odbc32.dll` 属于操作系统,链接它不算代价)。于是"这个后端编没编"与"你要连的数据源有没有驱动"是两件事 —— 后者到 `open()` 才有答案,用的是驱动管理器的原话。见 [`doc/db-guide.md`](doc/db-guide.md) |
+| `UVCPP_DB_RUNTIME_LOAD` | `OFF` | MySQL / PostgreSQL / ODBC 的符号不从链接行上来,改成**运行时**打开一份客户端库(`dlopen` / `LoadLibrary`)再取;ODBC 那一份打开的是**驱动管理器**,具体驱动仍由它自己加载。**六条发布腿用的就是它,而理由在包的**使用者**身上,不在库里**:`DT_NEEDED`(Windows 上是导入表)是**加载期**解析的,链上 libmysqlclient/libpq/libodbc 就等于"从没装过这些客户端库的机器连**加载**这份库都做不到"—— 哪怕它只跑 HTTP、一行 `db` 代码都不碰。开了之后,那种机器上除"真去连 MySQL/PG/ODBC"之外一切照常,而真去连时拿到的是 `uvcpp_db_status::NO_DRIVER` 加一句写清了试过哪些库名的话(可用环境变量 `UVCPP_DB_MYSQL_LIBRARY` / `UVCPP_DB_PGSQL_LIBRARY` / `UVCPP_DB_ODBC_LIBRARY` 指定路径)。它改的只是**符号从哪来**,不是**有哪些后端**:配置期仍然要 `mysql.h` / `libpq-fe.h` / `sql.h`(`MYSQL_BIND` 的布局必须从真的头里读),所以 `UVCPP_DB_MYSQL_ENABLE` / `UVCPP_DB_PGSQL_ENABLE` / `UVCPP_DB_ODBC_ENABLE` 的含义照旧是"头找到了"。默认 `OFF`,今天的行为逐字节不变。见 [`doc/db-guide.md`](doc/db-guide.md) |
| `UVCPP_USE_SYSTEM_LIBUV` | `ON` | 优先使用系统安装的 libuv |
| `UVCPP_BUILD_LIBUV_FROM_SOURCE` | `OFF` | 用 `FetchContent` 拉取并源码构建 libuv |
| `UVCPP_STATIC_RUNTIME` | `OFF` | 把编译器运行时(`libgcc`/`libstdc++`)静态链进库。**仅 MinGW 与 Linux 有效,MSVC 上是空操作** —— MSVC 用 `/MD`,发布包里自带 `vcruntime`/`msvcp`。**在 Linux 上它还同时作用于每个可执行文件,而带它的*共享* `libuvcpp.so` 与"用系统 `libstdc++.so.6` 的使用者"是互斥的** —— 两份 libstdc++ 各自实例化同一批 COMDAT 模板(`std::once_flag` 的 `__once_proxy`、`std::__future_base::_State_baseV2`),而 ELF 的全局符号作用域让一侧跳进另一侧的栈帧,于是失败形状是运行期段错误或 `double free`,**不是链接错误**。**这就是发布流程的两条 Linux 腿都不再传它的原因**(1.7.0 起)—— Linux 包因此需要系统里的 `libstdc++.so.6` / `libgcc_s.so.1`,任何带 C++ 运行时的发行版本来就有。两条 MinGW 腿照旧传:PE 没有全局符号作用域,不存在互相顶掉,而 MinGW 用户机器上未必有 `libstdc++-6.dll`。见 [`doc/build-guide.md`](doc/build-guide.md) |
diff --git a/RELEASE.md b/RELEASE.md
index 2dc7b4a1..cd60a195 100644
--- a/RELEASE.md
+++ b/RELEASE.md
@@ -1146,11 +1146,11 @@ g++ -std=c++11 -g -I include your_app.cpp -L lib -luvcppd -o your_app.exe
引入,它登记的 install 规则引用了一个从未构建的 `libuv.dll`,且它的规则排在
本项目的规则之前 —— 一失败就整体中止,本项目的头文件与库一个都装不出来
(Linux 上没有这个 `.dll`,同一条 install 实测走得通,2026-10-12 验过)。
- 本次的 zip 绕过它、照 `CMakeLists.txt:2918-3224` 的规则手工组装,与 install
+ 本次的 zip 绕过它、照 `CMakeLists.txt:2941-3249` 的规则手工组装,与 install
只剩**一处**刻意的差异:**libuv 的头放在 `include/` 顶层**(本库的公开头写的是
`#include `,放进 `include/libuv/` 会找不到)。原先还有第二处 —— zip 里
补了 `zlib.h` / `zconf.h`,而 install 规则里没有这一条 —— **2026-10-12 起
- install 规则自己也装这两个头**(`CMakeLists.txt:3204-3207`),两边落到同一个
+ install 规则自己也装这两个头**(`CMakeLists.txt:3229-3232`),两边落到同一个
位置,这处差异消失。
### MSVC 从源码树 / 安装树取头时要加 `/utf-8` —— 头文件侧已修(1.2.0)
diff --git a/doc/ci-guide.md b/doc/ci-guide.md
index c870862a..aae80520 100644
--- a/doc/ci-guide.md
+++ b/doc/ci-guide.md
@@ -278,7 +278,7 @@ removed on 2026-09-17 after measuring them instead of trusting the label:
| `test_memory_pool` | "Pre-existing hang (multi-thread pool alloc on Windows)" | 0 failures, ≤1 s per run |
Both had been exclusions for defects fixed long before. `test_memory_pool`'s is
-documented: it was a missing-DLL-copy bug (see `CMakeLists.txt:3265-3265`), fixed and
+documented: it was a missing-DLL-copy bug (see `CMakeLists.txt:3290-3290`), fixed and
left in the exclude list anyway. `test_tcp_func`'s dual-loop teardown is most
likely the `~uvcpp_tcp_server` fix, which is what removed the two `sleep_for`
calls that were joining the worker thread — that is an inference from the
@@ -454,7 +454,7 @@ MSVC. MinGW and macOS are the gap (see §1).
entry at all and shipped two classes of defect (#35).
- **It is `web` plus one switch**, exactly as `zlib-off` is `web` minus one, and its dep list
is **byte-identical to `web`'s**. That is deliberate: `pugixml` is pulled by `FetchContent`
- (`CMakeLists.txt:1713`, `GIT_TAG` pinned to `PUGIXML_VERSION`), not by a package manager, so
+ (`CMakeLists.txt:1736`, `GIT_TAG` pinned to `PUGIXML_VERSION`), not by a package manager, so
there is nothing to install — and keeping the package set identical means a red/green
difference between the two legs can only be attributed to the switch itself.
- **All four gates are load-bearing here.** Gate ② is `pugixml integrated` (the FetchContent
@@ -501,7 +501,7 @@ table lost its `MinGW + yyjson` row rather than gaining a row here).
compiles this header with **libc++**, whose `double` formatting is not libstdc++'s. The
byte-identical write-back assertion is precisely the kind that differs across those two.
- **`flags` is `web` plus one switch, and the dep list is byte-identical to `web`'s on all
- three legs.** `yyjson` is pulled by `FetchContent` (`CMakeLists.txt:1773`, `GIT_TAG` pinned
+ three legs.** `yyjson` is pulled by `FetchContent` (`CMakeLists.txt:1796`, `GIT_TAG` pinned
to `YYJSON_VERSION`) and built as an OBJECT library, not found by a package manager, so
there is nothing to install — and keeping the package set identical means a red/green
difference can only be attributed to the switch.
diff --git a/doc/db-guide.md b/doc/db-guide.md
index 41849d2c..1e7cf1fd 100644
--- a/doc/db-guide.md
+++ b/doc/db-guide.md
@@ -22,7 +22,8 @@ SQLite / MySQL / PostgreSQL / ODBC 四个后端,SQL 由调用方写,结果
> 模块。发布配置**显式打开**它,所以预编译包里带着这个模块 —— `1.7.0` 那个包里
> SQLite / MySQL / PostgreSQL **三个都在**(SQLite 从源码编,MySQL/PG 编进来但库走
> 运行时加载,见 §1 与 §3)。ODBC 是 `1.7.1-dev` 才加的,**还没有哪个已发布的包
-> 带过它**。
+> 带过它**;它和 MySQL/PG 走同一条运行时加载(链上的是**驱动管理器**,也是"客户端
+> 库"那一类)。
## 目录
@@ -55,7 +56,7 @@ cmake --build build-db -j
| `UVCPP_ENABLE_DB_MYSQL` | `ON` | 编 MySQL 后端(要 `mysql.h` + libmysqlclient) |
| `UVCPP_ENABLE_DB_PGSQL` | `ON` | 编 PostgreSQL 后端(要 `libpq-fe.h` + libpq) |
| `UVCPP_ENABLE_DB_ODBC` | `ON` | 编 ODBC 后端(要 `sql.h` + 驱动管理器)。**连什么库不在这里定** —— 那是连接串里 `Driver=` 的事(见下面「ODBC:驱动管理器与**具体驱动**」) |
-| `UVCPP_DB_RUNTIME_LOAD` | `OFF` | MySQL/PG 的**客户端库**改成运行时加载(`dlopen`/`LoadLibrary`),只是**不在链接行上出现** —— 头还是要(见下面「运行时加载」一节)。ODBC 不走这条路,理由见那一节 |
+| `UVCPP_DB_RUNTIME_LOAD` | `OFF` | MySQL/PG/ODBC 的**客户端库**改成运行时加载(`dlopen`/`LoadLibrary`),只是**不在链接行上出现** —— 头还是要(见下面「运行时加载」一节)。ODBC 那一份加载的是**驱动管理器**,具体驱动仍由它自己加载 |
四个子开关的语义是**"找得到就编、找不到就降级并出声"**,不是"我要不要用它":
想明确裁掉哪一个才写 `=OFF`(那时连"找不到"的警告都不会有)。
@@ -115,10 +116,12 @@ ODBC 从 `1.7.1-dev` 起进默认配置,还没有哪个已发布的包带过
### 客户端库运行时加载:`UVCPP_DB_RUNTIME_LOAD`
-**为什么要有它。** 直接链客户端库时,`libpq.so.5` / `libmysqlclient.so.21` 会
-变成产物的一条 `DT_NEEDED`,而 **`DT_NEEDED` 是加载期解析的**:装了 libuvcpp
-的机器上只要缺这一条,**进程根本起不来** —— 一行 db 代码都没跑,甚至这个程序
-压根不碰数据库。Windows 同理(PE 的导入表)。
+**为什么要有它。** 直接链客户端库时,`libpq.so.5` / `libmysqlclient.so.21` /
+`libodbc.so.2` 会变成产物的一条 `DT_NEEDED`,而 **`DT_NEEDED` 是加载期解析的**:
+装了 libuvcpp 的机器上只要缺这一条,**进程根本起不来** —— 一行 db 代码都没跑,
+甚至这个程序压根不碰数据库。Windows 同理(PE 的导入表),**只有 `odbc32.dll` 是
+例外**:它属于操作系统,链接它不算代价 —— 所以这一档对 ODBC 的意义集中在
+Linux / macOS 的产物上(那上面 ODBC 的"客户端库"是普通包 unixODBC)。
本机实测(同一个 `hello`,只 `printf`,一行 db 代码不跑):
@@ -136,24 +139,25 @@ $ ./hello
```bash
cmake -S . -B build-db -DUVCPP_ENABLE_DB=ON \
-DUVCPP_ENABLE_DB_MYSQL=ON -DUVCPP_ENABLE_DB_PGSQL=ON \
+ -DUVCPP_ENABLE_DB_ODBC=ON \
-DUVCPP_DB_RUNTIME_LOAD=ON
```
| | 直接链(默认,`OFF`) | 运行时加载(`ON`) |
|---|---|---|
-| 链接期要 | `mysql.h`/`libpq-fe.h` **+** 客户端库 | 只要头(`libmysqlclient-dev` / `libpq-dev`) |
-| 产物的 `DT_NEEDED` | 多两条 | **一条都不多** |
-| 机器上没装客户端库 | **进程起不来** | 进程照常跑;HTTP、SQLite 一切照常 |
-| 真去连 MySQL/PG | 能连 | 返回 `NO_DRIVER` + `last_error()` 里写明**试过哪几个库名** |
-| 找库的办法 | 链接器(`/etc/ld.so.cache`、`RPATH`) | 固定的候选名列表,或 `UVCPP_DB_MYSQL_LIBRARY` / `UVCPP_DB_PGSQL_LIBRARY` |
+| 链接期要 | `mysql.h`/`libpq-fe.h`/`sql.h` **+** 客户端库 | 只要头(`libmysqlclient-dev` / `libpq-dev` / `unixodbc-dev`) |
+| 产物的 `DT_NEEDED` | 多三条 | **一条都不多** |
+| 机器上没装那些库 | **进程起不来** | 进程照常跑;HTTP、SQLite 一切照常 |
+| 真去连 MySQL/PG/ODBC | 能连 | 返回 `NO_DRIVER` + `last_error()` 里写明**试过哪几个库名** |
+| 找库的办法 | 链接器(`/etc/ld.so.cache`、`RPATH`) | 固定的候选名列表,或 `UVCPP_DB_MYSQL_LIBRARY` / `UVCPP_DB_PGSQL_LIBRARY` / `UVCPP_DB_ODBC_LIBRARY` |
候选名(按顺序试,全试完才报错):
-| 平台 | MySQL | libpq |
-|---|---|---|
-| Linux | `libmysqlclient.so.21`、`libmysqlclient.so.24`、`libmariadb.so.3`、`libmysqlclient.so` | `libpq.so.5`、`libpq.so` |
-| Windows | `libmysql.dll`、`libmariadb.dll`、`mysqlclient.dll` | `libpq.dll` |
-| macOS | `libmysqlclient.21.dylib`、`libmariadb.3.dylib`、`libmysqlclient.dylib` | `libpq.5.dylib`、`libpq.dylib` |
+| 平台 | MySQL | libpq | ODBC(驱动管理器) |
+|---|---|---|---|
+| Linux | `libmysqlclient.so.21`、`libmysqlclient.so.24`、`libmariadb.so.3`、`libmysqlclient.so` | `libpq.so.5`、`libpq.so` | `libodbc.so.2`、`libodbc.so` |
+| Windows | `libmysql.dll`、`libmariadb.dll`、`mysqlclient.dll` | `libpq.dll` | `odbc32.dll`(系统件,一定在) |
+| macOS | `libmysqlclient.21.dylib`、`libmariadb.3.dylib`、`libmysqlclient.dylib` | `libpq.5.dylib`、`libpq.dylib` | `libodbc.2.dylib`、`libodbc.dylib` |
两个环境变量是**显式指定**,设了它就**只试这一个**(不再走候选名列表)——
装在非标准前缀下的人用它,而它同时是**"没装库"那条路的确定性旋钮**:
@@ -170,16 +174,17 @@ UVCPP_DB_PGSQL_LIBRARY=/nonexistent/libpq.so.5 ./myapp
都可能还在),卸掉比留着危险。
**它只改"符号从哪来",不改后端在不在**:`UVCPP_DB_MYSQL_ENABLE` /
-`UVCPP_DB_PGSQL_ENABLE` 那两个宏照旧是"头找得到就是 1",
+`UVCPP_DB_PGSQL_ENABLE` / `UVCPP_DB_ODBC_ENABLE` 那三个宏照旧是"头找得到就是 1",
`uvcpp_db_drivers()` 的输出也不变。换句话说 `NO_DRIVER` 现在有**两条**来路
(后端没编进来 / 编进来了但这台机器没库),`last_error()` 会说清是哪一条 ——
详见 `uvcpp_db_status.h` 里 `NO_DRIVER` 那一段的文档注释。
**共享库上这一档还多一条链接选项 `--no-undefined`**(ELF):调用点全改成经
`api()` 那张 `dlsym` 函数指针表走,而**共享库默认允许未定义符号** ——
-漏绑/漏改一处,`ld` 不吭声,产物却带着一个没人提供的 `mysql_query`,
-于是**每个使用者**在加载这个 `.so` 时就炸。`--no-undefined` 把这条不变式交还
-给链接器:漏一处就是链接期红。
+漏绑/漏改一处,`ld` 不吭声,产物却带着一个没人提供的 `mysql_query`(或
+`SQLExecDirectW`),于是**每个使用者**在加载这个 `.so` 时就炸。`--no-undefined`
+把这条不变式交还给链接器:漏一处就是链接期红。加 ODBC 那一份时这条兜底直接生效
+过 —— `nm -u …/uvcpp_db_odbc.cpp.o` 里剩下 **0 个** `SQL*` 未定义符号。
### ODBC:驱动管理器与**具体驱动**是两件事
@@ -204,25 +209,37 @@ SQL Server,也能连任何装得出 ODBC 驱动的数据源(Oracle、DB2、
也没有默认驱动"),本模块不把它改写成自己的话:这句话是判"我的连接串写错了"
还是"这台机器没装驱动"唯一有用的信息。它**不走 `NO_DRIVER` 那条路** ——
`NO_DRIVER` 的含义是"这个后端没编进这份包"(见 [§2](#2-连接串))。
-* **没有 `UVCPP_DB_RUNTIME_LOAD` 那条路,这是有意的。** 上面那一节存在的理由是
- "缺了客户端库会让**进程起不来**";ODBC 这边不成立:驱动管理器本来就该在、而且
- 很小,真正按需加载的是它下面的**具体驱动**,那件事归驱动管理器自己管。
-* **但它把驱动管理器**链**进来 —— 这一条的代价要单独知道。** SQLite 可以
- (`UVCPP_DB_SQLITE_FROM_SOURCE`)编进产物、MySQL/PG 可以走运行时加载,ODBC 这份
- 默认是**直接链** `libodbc.so.2`(Windows 上是系统 `odbc32.dll`,本来就在,不算
- 代价)。于是 Linux 的产物上多一条 `DT_NEEDED`:
+* **它也走 `UVCPP_DB_RUNTIME_LOAD`:链上的是驱动管理器,那也是"客户端库"。**
+ 起先这里写的是"ODBC 不走这条路,因为驱动管理器本来就该在、而且很小" —— 那句话
+ **只在 Windows 上成立**(`odbc32.dll` 属于操作系统,和 `ws2_32` 同类,链接它没有
+ 代价);Linux / macOS 上驱动管理器是一个普通包(unixODBC),链上它就是在产物里
+ 加一条 `DT_NEEDED libodbc.so.2`,正是上面「运行时加载」那一节要消灭的形状:
```
- $ objdump -p build-odbc/libuvcpp.so | grep NEEDED
+ 两棵树只差 `-DUVCPP_DB_RUNTIME_LOAD` 这一个开关(其余逐条相同):
+
+ $ readelf -d /tmp/uvcpp-off/libuvcpp.so | grep NEEDED # 关(默认)
NEEDED libuv.so.1
+ NEEDED libsqlite3.so.0
NEEDED libodbc.so.2
NEEDED libstdc++.so.6
+
+ $ readelf -d /tmp/uvcpp-rt/libuvcpp.so | grep NEEDED # -DUVCPP_DB_RUNTIME_LOAD=ON
+ NEEDED libuv.so.1
+ NEEDED libsqlite3.so.0
+ NEEDED libstdc++.so.6
```
- 没装 unixODBC 的机器上,这一条就是**加载期**失败 —— 正是上面「运行时加载」那一节
- 要消灭的形状。要不要留它由你定:`-DUVCPP_ENABLE_DB_ODBC=OFF` 去掉这条依赖,其余
- 三个后端照编。不改成运行时加载的理由不是"它小",而是**它本身就是找驱动的那个
- 东西**:自己没有它,也就没法去找别的。
+ 于是它和 MySQL/PG 同一条规矩:**开** ⇒ 产物里不出现这个库,"这台机器没装
+ unixODBC"推迟到**真去连**那一次(拿到的是 `NO_DRIVER` 加一句写清了试过哪些库名
+ 的话);**关**(默认)⇒ 老实链上。两件事别混:**驱动管理器**是这一层的依赖,
+ 它下面的**具体驱动**由它自己 `dlopen`,那是 `open()` 那一刻的事(上面第一条)。
+
+ 候选名是各平台的惯例名(Linux `libodbc.so.2` / `libodbc.so`、macOS
+ `libodbc.2.dylib` / `libodbc.dylib`、Windows `odbc32.dll`),要指到别处用
+ `UVCPP_DB_ODBC_LIBRARY`。**iODBC 有意不在候选名里**:它是另一个驱动管理器实现,
+ 符号名一样但行为没在本仓量过 —— 想用它就显式指路,别让"打开成功"盖住"换了一个
+ 没量过的实现"。
连接串里的默认驱动名(`ODBC Driver 18 for SQL Server`)**只是默认**:`?Driver=` 或
`?DSN=` 一写就换掉(见 [§2](#2-连接串))。
@@ -240,11 +257,17 @@ SQL Server,也能连任何装得出 ODBC 驱动的数据源(Oracle、DB2、
也就是说 —— **`1.7.0` 那个预编译包里的 db 模块三个后端都在**(`uvcpp_db_drivers()`
会打印 `sqlite,mysql,postgres` —— 是**驱动的稳定短名**,`pgsql` 只是 `scheme` 的
别名之一,见下面的别名表),但 MySQL/PG 的**客户端库**是运行时才打开的,链接行上
-不出现(见下面「客户端库运行时加载」那一节)。**ODBC 是 `1.7.1-dev` 才加的,这六条
-腿的 `-D` 清单里还没有它** —— 它默认 `ON`,所以下一版的包里**会**有它,那意味着
-Linux 包上多一条 `DT_NEEDED libodbc.so.2`(Windows 上是系统 `odbc32`,不算代价)。
-这件事该由发布配置**显式**写出来(`-DUVCPP_ENABLE_DB_ODBC=ON` 或 `=OFF`),而不是
-靠默认值漂进去 —— 留在这里当一条待办。
+不出现(见下面「客户端库运行时加载」那一节)。
+
+**ODBC 是 `1.7.1-dev` 才加的,这六条腿的 `-D` 清单里还没有它。**
+它默认 `ON`,但决定它在不在包里的不止 `-D`:**Linux 那几条腿上还得装
+`unixodbc-dev`** —— 现在的 `apt-get install` 只装 `zlib1g-dev ninja-build`,runner
+上没有 `sql.h`,上面那条 warning 会把 ODBC 后端**强制关掉**,于是 Linux 包照样没有
+它(Windows 腿上 `sql.h` 来自 SDK,所以 Windows 包里会有)。补上之后不必担心多出
+依赖:腿里已经有 `-DUVCPP_DB_RUNTIME_LOAD=ON`,ODBC 跟着它走,**不会**往产物里添
+`DT_NEEDED libodbc.so.2`,那六条腿只需要装头。这件事该由发布配置**显式**写出来
+(`-DUVCPP_ENABLE_DB_ODBC=ON` + runner 上的 `unixodbc-dev`),而不是靠默认值漂进去
+—— 留在这里当一条待办。
SQLite 那一半必须从源码编,不是漏配,
是两条实测理由把另两条路都堵死了:
diff --git a/doc/expand-guide.md b/doc/expand-guide.md
index 0d9fc2cb..b3ecab2a 100644
--- a/doc/expand-guide.md
+++ b/doc/expand-guide.md
@@ -95,18 +95,18 @@ cmake -S . -B build -DUVCPP_BUILD_EXPAND=ON
| `-DUVCPP_BUILD_EXPAND=ON` | `UVCPP_ENABLE_MEMORY_POOL=1` |
| `-DUVCPP_BUILD_EXPAND=OFF`(默认) | `UVCPP_ENABLE_MEMORY_POOL=0` |
-选项到宏的映射在 `CMakeLists.txt:1590-1596` 一处完成。用预编译包时**什么都不用传** ——
+选项到宏的映射在 `CMakeLists.txt:1613-1619` 一处完成。用预编译包时**什么都不用传** ——
包里的 `include/uvcpp/uvcpp_config.h` 就是那次构建的值,自己再传一个冲突的值是硬 `#error`。
关掉时 `src/expand/` 整目录被 `list(FILTER ... EXCLUDE REGEX "src/expand/")` 排除
-(`CMakeLists.txt:1953-1962`)。于是"关掉之后包含它"的表现取决于**你的头是怎么来的**
+(`CMakeLists.txt:1976-1985`)。于是"关掉之后包含它"的表现取决于**你的头是怎么来的**
—— 而能不能编过,只由"那个头在不在你的 include 路径里"决定:
| 你怎么构建 | `#include ` 的结果 |
|---|---|
| 源码树自己构建(没开开关) | **编得过,链接失败** —— 头还在 `src/expand/` 下,且四个头里**没有任何一个用宏守卫自己**(唯一提到这个宏的地方是 `src/expand/uvcpp_page_heap.h:8` 的一句注释),没有 `#error` 兜底;但符号一个都没编进库 |
| 用**官方预编译包** | **什么都不用传,直接用** —— 六条发布腿的十处 configure 都传 `-DUVCPP_BUILD_EXPAND=ON`(`release.yml` 里十处都有),包里那个 `uvcpp_config.h` 的 `UVCPP_ENABLE_MEMORY_POOL` 是 **1**,`include/expand/` 那四个头也在。上面那句"用预编译包什么都不用传"在这里是**字面意思** |
-| 自己 `cmake --install` 到一个前缀(没开开关) | **编译期就报找不到头** —— 这条路按开关装头(`CMakeLists.txt:2968-2971`),`include/expand/` 不会被装出去 |
+| 自己 `cmake --install` 到一个前缀(没开开关) | **编译期就报找不到头** —— 这条路按开关装头(`CMakeLists.txt:2991-2994`),`include/expand/` 不会被装出去 |
| 自己跑 `package_release.py` 打的包(没开开关) | 与第一行同 —— **编得过、链接失败**。它拷头是按 `MODULES` 逐个目录拷的(`tests/tools/package_release.py`),**不看这个开关**,所以 `include/expand/` 照样在包里;但包里那个宏是 0、库里也没有那些符号 |
> **别把第三、四行记成一回事**:`cmake --install` 与 `package_release.py` 是两套独立的
diff --git a/doc/grpc-guide.md b/doc/grpc-guide.md
index e53a6ef1..c60cbd72 100644
--- a/doc/grpc-guide.md
+++ b/doc/grpc-guide.md
@@ -10,11 +10,11 @@
这个分法不是洁癖,是**依赖事实**:编解码那半的每一个参数都是 `std::string` / `uint32_t`
(`src/grpc/uvcpp_grpc.h:100-194`),而服务端那半的每个参数都是 web 层的类型
(`src/grpc/uvcpp_grpc_server.h:116-132`)。所以关掉 web 模块时前者照编照装、后者
-**从构建与安装里都摘掉**(`CMakeLists.txt:1997-1999`、`CMakeLists.txt:2987-2992`)——
+**从构建与安装里都摘掉**(`CMakeLists.txt:2020-2022`、`CMakeLists.txt:3010-3015`)——
装在 `include/grpc/` 下,而不是 `include/web/` 下。
- 打开方式:**没有开关**。编解码那半任何配置下都在;服务端那半跟着 `UVCPP_BUILD_WEB`
- 走(`CMakeLists.txt:1997-1997`)。加一个 `UVCPP_ENABLE_GRPC` 只会多一个能与
+ 走(`CMakeLists.txt:2020-2020`)。加一个 `UVCPP_ENABLE_GRPC` 只会多一个能与
`UVCPP_BUILD_WEB` 打架的旋钮,而两半的依赖是死的、不需要调用方表态。
- 包含方式:``、``。
- 依赖:帧与 varint 那半**只依赖 `` 与 `libuv` 的错误码**(
diff --git a/doc/http2-guide.md b/doc/http2-guide.md
index 04e11fe8..1c1ae1c2 100644
--- a/doc/http2-guide.md
+++ b/doc/http2-guide.md
@@ -11,14 +11,14 @@
`web/` 的两条线(`uvcpp_http_server` / `uvcpp_http_client`)各有一份接好的实现可以照抄,
本页在 [§5](#5-服务端怎么回一条响应) 与 [§6](#6-客户端怎么发一条请求) 给了它们的调用序列。
-- 打开方式:`-DUVCPP_ENABLE_NGHTTP2=ON`。**默认 OFF**(`CMakeLists.txt:245-245`),
+- 打开方式:`-DUVCPP_ENABLE_NGHTTP2=ON`。**默认 OFF**(`CMakeLists.txt:249-249`),
而且缺 OpenSSL 或缺 web 模块时会被**强制**置 OFF 并打 warning
- (`CMakeLists.txt:604-604`、`CMakeLists.txt:613-613`)—— 因为 `uvcpp_h2_stream`
+ (`CMakeLists.txt:608-608`、`CMakeLists.txt:617-617`)—— 因为 `uvcpp_h2_stream`
里直接放 `uvcpp_http_request` / `uvcpp_http_response`
(`src/http2/uvcpp_h2_session.h:70`)。
- 包含方式:``、``、
``。私有的 `` **不安装**
- (`CMakeLists.txt:3077-3077`)。
+ (`CMakeLists.txt:3100-3100`)。
> 本页讲**怎么用**。它与 [HTTP/2 支持现状](./http2-status.md) 分工:那篇讲
> **实现进度、写死的折衷、已知缺口**,本页不重复那些,需要时直接链过去。
@@ -72,12 +72,12 @@ int drain(std::string& out, size_t max_bytes = 0); // src/http2/u
## 2. 编译期条件
`UVCPP_NGHTTP2_ENABLE` **在任何构建里都有定义**,只是值不同:ON ⇒ `1`,OFF ⇒ `0`
-(`CMakeLists.txt:2353-2353`)。三个公开头把**全部内容**包在 `#if UVCPP_NGHTTP2_ENABLE` 里
+(`CMakeLists.txt:2376-2376`)。三个公开头把**全部内容**包在 `#if UVCPP_NGHTTP2_ENABLE` 里
(`src/http2/uvcpp_h2_common.h:19`、`src/http2/uvcpp_h2_session.h:33`、`src/http2/uvcpp_h2_connection.h:27`)。
为 0 时:头**还在**(源码树里能 include、也编得过),但里面**一个类型都不声明** ——
写 `uvcpp_h2_session s(true);` 会"未定义类型"。安装出来的包里则**根本没有这几个头**
-(`CMakeLists.txt:3077-3077` 只在开关 ON 时装)。
+(`CMakeLists.txt:3100-3100` 只在开关 ON 时装)。
所以使用者的写法是把自己的那段包起来:
diff --git a/doc/http2-status.md b/doc/http2-status.md
index c6d85993..73255312 100644
--- a/doc/http2-status.md
+++ b/doc/http2-status.md
@@ -82,13 +82,13 @@
一份更权威的 ALPN 名单 —— 设过就不覆盖。
- **关掉 nghttp2 也能编**:`http2/*.h` 不出现在任何公开头里,`src/web/uvcpp_http_client.h:43-45`
写明了理由(引了就把 nghttp2 的 include 路径扩散给每个使用者与测试 TU)。
- `UVCPP_ENABLE_NGHTTP2` 默认 OFF(`CMakeLists.txt:245-245`),缺 OpenSSL 或缺 web
+ `UVCPP_ENABLE_NGHTTP2` 默认 OFF(`CMakeLists.txt:249-249`),缺 OpenSSL 或缺 web
模块时还会被强制置 OFF 并告警。
### 1.4 依赖与产物
`nghttp2` 是**唯一被强制静态**的依赖:`uvcpp_nghttp2_configure()`
-(`CMakeLists.txt:635-635`,在 function 作用域里 `BUILD_STATIC_LIBS ON`)把一个
+(`CMakeLists.txt:639-639`,在 function 作用域里 `BUILD_STATIC_LIBS ON`)把一个
静态 `nghttp2_static` 交给链接器。实测 `pe_imports()`:开了 h2 的 DLL 与没开的
相比,非白名单依赖集合**都是** `llhttp.dll uv.dll zlib1.dll` —— 开 h2 不新增
任何 DLL 依赖。
@@ -377,7 +377,7 @@ ctest --test-dir build-h2 -C Release --timeout 60
- `copy_test_dlls` 是 `add_custom_target(... ALL ...)`,命令全是
`${CMAKE_COMMAND} -E copy_if_different`,**一个 `pwsh` 都不沾**,构建日志里
"Copy uvcpp/libuv DLLs into test folders" 那行就是它。它确实会跑。真正
- `0xc0000135` 满屏的根因是这张目标**曾经没带 `ALL`**(见 `CMakeLists.txt:3373-3373`
+ `0xc0000135` 满屏的根因是这张目标**曾经没带 `ALL`**(见 `CMakeLists.txt:3398-3398`
的注释),与 `pwsh` 无关。
- 日志里那句 `'pwsh.exe' 不是内部或外部命令` 来自 **vcpkg 的
`scripts/buildsystems/msbuild/vcpkg.targets`**(`applocal.ps1`),而且它自己
diff --git a/doc/http3-guide.md b/doc/http3-guide.md
index e6a8ac0b..3d410286 100644
--- a/doc/http3-guide.md
+++ b/doc/http3-guide.md
@@ -24,18 +24,18 @@
- 打开方式:`-DUVCPP_ENABLE_HTTP3=ON`。**默认 OFF**(`CMakeLists.txt:143-143`),
而且下面两种情况会被**强制**置 OFF 并打 warning,而不是留一个"能配置、链不上、
跑不起来"的组合:
- 1. 没开 QUIC(`CMakeLists.txt:969-969`)—— HTTP/3 是 QUIC 之上的一条应用协议,
+ 1. 没开 QUIC(`CMakeLists.txt:973-973`)—— HTTP/3 是 QUIC 之上的一条应用协议,
没有 QUIC 它没有字节可以跑在上面;
- 2. 没开 web 层(`CMakeLists.txt:976-976`)—— 这条传输的公开面就在
+ 2. 没开 web 层(`CMakeLists.txt:980-980`)—— 这条传输的公开面就在
`uvcpp_http_client` / `uvcpp_http_server` 上,web 关掉时它没有使用者。
- 包含方式:``、``。
私有的 `` 与 `` **都不安装**
- (`CMakeLists.txt:3118-3118`)—— 理由见 [§7](#7-典型坑) 第一条。
+ (`CMakeLists.txt:3141-3141`)—— 理由见 [§7](#7-典型坑) 第一条。
- 两个公开头整段套在 `#if UVCPP_HTTP3_ENABLE` 里,web 层那边新加的
`listen_quic()` / `set_http3_enabled()` 也是,所以**不开关就一个名字都看不到**。
这与 `quic/`、`web/`、`ssl/`、`http2/`、`wsdl/` 同档。
-- 开关与依赖:nghttp3 走 FetchContent(`NGHTTP3_VERSION`,`CMakeLists.txt:259-259`),
- 静态链入 `nghttp3_static`(`CMakeLists.txt:2780-2780`)—— 详见
+- 开关与依赖:nghttp3 走 FetchContent(`NGHTTP3_VERSION`,`CMakeLists.txt:263-263`),
+ 静态链入 `nghttp3_static`(`CMakeLists.txt:2803-2803`)—— 详见
[§5](#5-cmake-接线与那条-check-撞名的坑)。
> 本指南里的签名、默认值、行为都对着当前源码核过。凡是"这一版没做"的地方都明确
@@ -360,14 +360,14 @@ void connect_over_http3(uvcpp_http_client& cli, int udp_port) {
## 5. CMake 接线与那条 `check` 撞名的坑
1. `option(UVCPP_ENABLE_HTTP3 ...)` 挨着 QUIC 那个(`CMakeLists.txt:143-143`),
- **默认 OFF**;两条守卫(`CMakeLists.txt:969-969`、`CMakeLists.txt:976-976`)用普通变量 `set(... OFF)` 降级并出声 ——
+ **默认 OFF**;两条守卫(`CMakeLists.txt:973-973`、`CMakeLists.txt:980-980`)用普通变量 `set(... OFF)` 降级并出声 ——
照 QUIC 那套。**别只 grep `CMakeCache.txt`**:降级不是 cache 写,所以 cache 里
仍然是 `UVCPP_ENABLE_HTTP3:BOOL=ON`,一个"被降级了"的配置和一个"真开了"的配置
在 cache 那一层长得一模一样。配置成功的标志是这行 STATUS:
```
- nghttp3 integrated (tag=v1.18.0, static) # CMakeLists.txt:1388-1388
- Including http3 module in build (nghttp3 v1.18.0) # CMakeLists.txt:2069-2069
+ nghttp3 integrated (tag=v1.18.0, static) # CMakeLists.txt:1411-1411
+ Including http3 module in build (nghttp3 v1.18.0) # CMakeLists.txt:2092-2092
```
2. **依赖用 `FetchContent_Declare` + `Populate` + `add_subdirectory(... EXCLUDE_FROM_ALL)`,
@@ -385,17 +385,17 @@ void connect_over_http3(uvcpp_http_client& cli, int udp_port) {
要删掉 `build*/_deps/uvcpp-nghttp3-src/` 再 configure。
4. **后置断言两条**:`if(NOT TARGET nghttp3_static)` FATAL
- (`CMakeLists.txt:1376-1376`;**裸目标名,没有 ALIAS**,别去猜
+ (`CMakeLists.txt:1399-1399`;**裸目标名,没有 ALIAS**,别去猜
`nghttp3::nghttp3_static`),以及把 `POSITION_INDEPENDENT_CODE` 读回来断言
- (`CMakeLists.txt:1311-1311`)—— 后者是"Linux 上 `libuvcpp.so` 链接失败"这件事的成因。
+ (`CMakeLists.txt:1334-1334`)—— 后者是"Linux 上 `libuvcpp.so` 链接失败"这件事的成因。
5. **三处编译定义同步**:`cmake/uvcpp_config.h.in` 的 `UVCPP_HTTP3_ENABLE` 段、
目录级 `add_compile_definitions`、PUBLIC `target_compile_definitions`,
- 外加 `_uvcpp_literal01(UVCPP_HTTP3_ENABLE UVCPP_ENABLE_HTTP3)`(`CMakeLists.txt:2355-2355`)。
+ 外加 `_uvcpp_literal01(UVCPP_HTTP3_ENABLE UVCPP_ENABLE_HTTP3)`(`CMakeLists.txt:2378-2378`)。
四处不一致会以 `#error` 或静默的 ABI 错配收场,`check_config_contract.py` 盯着
其中一个方向。
-6. **链接是 PRIVATE**(`CMakeLists.txt:2780-2780` 的 `$`):
+6. **链接是 PRIVATE**(`CMakeLists.txt:2803-2803` 的 `$`):
nghttp3 的头路径不传播给使用者,所以**功能测试不能** `#include
`;想知道版本就用 `uvcpp_h3_connection::nghttp3_version()`
(`src/http3/uvcpp_h3_connection.h:287`),它经公开头调用、链接期解析。
@@ -488,7 +488,7 @@ h1 写泵、TCP 的 ALPN 列表与分支 —— **全部不改**。唯一的例
1. **别指望从公开 h3 头里看到 nghttp3。** `` 只出现在私有的
`src/http3/uvcpp_h3_nghttp3.h` 里,而它和持有 nghttp3 句柄的
- `src/http3/uvcpp_h3_session.h` **两个都不安装**(`CMakeLists.txt:3118-3118`)、打包
+ `src/http3/uvcpp_h3_session.h` **两个都不安装**(`CMakeLists.txt:3141-3141`)、打包
也排除。理由与 quic 那一对逐字相同:一是使用者不该被逼着去配 nghttp3 的搜索
路径才能 include 一个本库的头;二是 `uvcpp_h3_session.h` 的字段布局直接跟着
nghttp3 的版本走 —— 一旦漏进公开面,那个版本就变成了本库的 ABI。这处排除与
@@ -530,7 +530,7 @@ h1 写泵、TCP 的 ALPN 列表与分支 —— **全部不改**。唯一的例
9. **`h3_header` / `h3_response` 这类小件不能花括号初始化,这是 C++11 的硬规矩。**
它们有一格带默认成员初值(`kind = h3_header_kind::REGULAR`),而"C++11 里带
默认成员初值的类**不算聚合体**"—— 这条限制到 C++14 才放开。本库的编译标准是
- C++11(`CMakeLists.txt:261-261` 的 `set(CMAKE_CXX_STANDARD 11)`),所以
+ C++11(`CMakeLists.txt:265-265` 的 `set(CMAKE_CXX_STANDARD 11)`),所以
`h3_header{"accept", "*/*", …}` 在消费者那里编不过,报的是
`no matching function for call to 'h3_header::h3_header()'`。**逐格赋值**不会踩到它,本库自己的代码也是这么写的
diff --git a/doc/json-guide.md b/doc/json-guide.md
index 88f63c8b..0490d435 100644
--- a/doc/json-guide.md
+++ b/doc/json-guide.md
@@ -44,7 +44,7 @@
## 2. 编译期条件与包含
- **没有开关。** 它是核心模块,`UVCPP_ENABLE_NET` / `_WEB` / `_WEBAPP` 一个都不影响它;
- `CMakeLists.txt:1489-1489` 那个收 `src/uvcpp/` 下源文件的 GLOB 已经把它收进去了。
+ `CMakeLists.txt:1512-1512` 那个收 `src/uvcpp/` 下源文件的 GLOB 已经把它收进去了。
- **不需要 nlohmann、不需要 OpenSSL、不需要 zlib。** 头文件只带
`` 与 `