diff --git a/.agents/docs/2026-10-15-add-compat-fmt-plan.md b/.agents/docs/2026-10-15-add-compat-fmt-plan.md new file mode 100644 index 00000000..a2900e78 --- /dev/null +++ b/.agents/docs/2026-10-15-add-compat-fmt-plan.md @@ -0,0 +1,112 @@ +# 新增 compat.fmt 收录(header 形态,非模块) + +## 1. 来源与形态 + +- 收录 [fmtlib/fmt](https://github.com/fmtlib/fmt) **12.2.0**,为当前最新 tag + (`api.github.com/repos/fmtlib/fmt/tags` 首条;上游 tag 不带 release asset,release 页面也无归档附件)。 +- 归档 `https://github.com/fmtlib/fmt/archive/refs/tags/12.2.0.tar.gz`,**两次下载字节一致**, + SHA-256 `8b852bb5aa6e7d8564f9e81394055395dd1d1936d38dfd3a17792a02bebd7af0` —— 与仓内 + `pkgs/f/fmtlib.fmt.lua` 早已登记的散列**逐位相同**,互为印证。解包顶层为 `fmt-12.2.0/`。 +- License **MIT**。 +- 布局:`include/fmt/`(16 个头)、`src/{format.cc, os.cc, fmt.cc, fmt-c.cc}`。 +- 本次与仓内已有 **`fmtlib.fmt`(模块形态)** 的分工:后者用 `generated_files` 重写上游的 + `src/fmt.cc`,让消费者写 `import fmt;`;本包收的是**同一个 release 的头文件形态**,消费者写 + `#include ` 并链接。两份描述符的注释互相指认(fmtlib.fmt 的头注释早就自称是 + 「the module sibling of the header-based `compat.fmt` package」,而 `compat.fmt` 此前并不存在)。 + 分成两个身份是有意的:想要 `#include` 的工程不该被塞一个自己从不 `import` 的模块 BMI。 +- 形态判定:**编译库**(上游自带实现的非模块形态,不是纯头)。上游 CMake 的默认构建就是我们要的 + —— `add_library(fmt src/format.cc)` 加 `FMT_OS` 默认 ON 时的 `target_sources(fmt PRIVATE + src/os.cc)`;`src/fmt.cc` 是模块 unit(`export module fmt;`),归 `fmtlib.fmt`,不取。 + +## 2. 描述符设计 + +- 身份 `compat:fmt`,文件 `pkgs/c/compat.fmt.lua` —— 目录取**完整包名** `compat.fmt` 的首字母。 +- `include_dirs = {"*/include"}`,前导 `*` 只负责吞掉 `fmt-12.2.0/` 包装层。写成 `{"*"}` 的隐蔽之处 + 与 `compat.rapidcsv` 同形:本包照编不误,只有消费者 `#include ` 时才炸,所以由消费测试兜底。 +- `sources = {"*/src/format.cc", "*/src/os.cc"}`;`targets = { ["fmt"] = { kind = "lib" } }`; + `import_std = false`(非模块);无 `generated_files`(上游 `src/` 里就有实现)。 +- `c_standard = "c11"`:`fmt-c.h` 的 C 面用 `_Generic` 与复合字面量,是 C11。本包 TU 全是 C++, + 该键对本包无副作用(`compat.richdem` 同形先例)。 +- **不声明任何 define**,三条理由都实测过: + - `FMT_OS` 在 12.2.0 的**头文件里已不再被读取**(仅 `src/fmt.cc` 用到),所以 os.cc 无条件编入 + 即与「FMT_OS=ON」等价,两侧都不需要宏。 + - `FMT_SHARED` / `FMT_LIB_EXPORT` 只在 Win32 且非 header-only 时决定 dllexport/dllimport 词法; + 本目标是静态库,两个都不定义才是正确分支(与 `compat.concurrentqueue` 需要 + `MOODYCAMEL_STATIC` 的情形不同:fmt 的头不自带 dllimport 默认)。 + - `/utf-8` 属**消费侧**:上游为 MSVC 标记 PUBLIC 的唯一来源是 `chrono.h:581` 的 + `detail::use_utf8 ? "µs" : "us"`,而本包三个 TU 与它们包含的全部头(`base.h`、 + `format.h`、`format-inl.h`)实测**非 ASCII 字节数为 0**(逐字节统计)。源码字符集对本库 + 无影响,而 `cflags`/`cxxflags` 只作用于本包 TU,替消费侧表达不了 —— 故不声明,只写进注释, + 并由「成员不测 chrono」把这条边界钉住(测它等于在断言本机工具链,而不是断言这个包)。 +- **CN 镜像**:本次无 `mcpp-res` 写权限(无 `GITCODE_TOKEN`,也无 + `~/.config/gitcode-tool/config.json`),按 `docs/cn-mirror.md` 的回退路径写**纯字符串 url** + (`compat.rapidcsv` / `pybind11` / `etl` 同形)。日后补镜像只需改写成 `{ GLOBAL = …, CN = … }`, + sha256 不变(镜像按原样重托管 tag 归档)。 + +## 3. Feature 评估 + +- `c-api`:上游把 C API 做成**另一个**库(`add_library(fmt-c STATIC src/fmt-c.cc)`,头 + `include/fmt/fmt-c.h`),即「额外的可编译源码」—— 正是 sources-only feature 表能门控的东西 + (`compat.concurrentqueue` 的 `c-api`、`compat.cjson` 的 `utils` 先例),默认 OFF:只要 C++ 头的 + 消费者不付这个 TU。请求方式 `fmt = { version = "12.2.0", features = ["c-api"] }`,编入同一 lib 目标。 +- 不做 feature 的两样,及理由: + - `fmt-header-only` 是上游 define-only 的 INTERFACE target(`FMT_HEADER_ONLY=1`),feature 表 + 当前只门控 sources、不带 define,表达不了; + - `FMT_OS` 不再是源码开关(见 §2),把它做成 feature 只会把「编不编 os.cc」伪装成一个不存在的 + 可选项。 +- **负向验证(实测,非推断)**:把 `tests/examples/fmt-c/mcpp.toml` 的依赖临时改回 + `fmt = "12.2.0"`(去掉 `features`),同一成员立刻失败,且失败在**链接期**: + + ``` + c_api ... FAIL (compile, 0.23s) + failed: bin/fmt-c-tests/c_api.exe + lld-link: error: undefined symbol: fmt_vformat + lld-link: error: undefined symbol: fmt_vprint + ``` + + `fmt_arg` 的构造器是头里的 `static inline`,所以未解析符号只可能来自 `fmt-c.cc`;反向证据是 + 包构建的 unit 计数:**带 feature 报 3 units,不带报 2 units**。验证后已还原。 + +## 4. 测试与验证 + +两个成员,按「一个 feature 一个成员」的既有分工(gmp-gmpxx / concurrentqueue-c-api 同形): + +- `tests/examples/fmt`(`fmt-tests`,核心 lib): + - `tests/format.cpp`:`fmt::format` 的多组结果、**运行时格式串**的 `fmt::vformat`、` + format_to` 返回迭代器再续写。`vformat` 与那些显式实例化只在 `format-inl.h` 里定义、 + 只有 `src/format.cc` 编成库里的一份 —— 若这个包没有把 format.cc 放进链接,本例根本不链接。 + - `tests/os.cpp`:`fmt::getpagesize()`(纯函数,零 I/O,`os.cc:361` 定义)是「os.cc 真被链接」 + 的最省证据;再用 `fmt::pipe` 做一次 write→read 往返断言内容,不落任何文件、不联网 + (`compat.rapidcsv` 的原则)。 + - 不测 `fmt/chrono.h`,理由见 §2 的 `/utf-8` 一条。 +- `tests/examples/fmt-c`(`fmt-c-tests`,`features = ["c-api"]`):`tests/c_api.cpp` 走 + `extern "C"` 面调用 `fmt_vformat` / `fmt_vprint`,断言输出、截断契约(`vformat_to_n`:返回值是 + 本应写出的长度,缓冲区不补 NUL)与错误契约(未知 `fmt_type`、超过 16 槽的实参个数均返回 + `fmt_error_invalid_arg`)。**用 C++ 而不是 C 调用**是有意的:头里那套 `FMT_MAKE_ARG` 宏只在 + `#ifndef __cplusplus` 下存在,且要求 MSVC 的 `/Zc:preprocessor`(头里直接 `#error`)——那是描述符 + 表达不了的消费侧编译开关,而门控的论点两条路一样成立。 +- 两个成员都按 `rapidcsv` 的写法重复了单条 `[indices] compat = { path = "../../.." }`,并用限定写法 + `[dependencies.compat] fmt`(裸名请求会在默认命名空间解析,落回远端已发布索引)。 + +验证结论(本机 win32,`mcpp 2026.10.11.1`): + +- `mcpp xpkg parse pkgs/c/compat.fmt.lua` → `parse OK`。 +- `mcpp test -p fmt` → `test result ok. 2 passed; 0 failed`。 +- `mcpp test -p fmt-c` → `test result ok. 1 passed; 0 failed`。 +- 留痕:`.mcpp/.xlings/data/xpkgs/compat-x-fmt/12.2.0/` 下的 **`.xpkg.lua` 快照就是本 checkout + 这份描述符**(带 `["c-api"]` 表与注释原文),`fmt-12.2.0/include` 正是 `*/include` 展开的 + 落点 —— 同时证明 glob 映射正确、且解析用的是本 checkout 而非远端已发布索引。 +- 版本差异说明:CI pin 为 **2026.10.1.2**,本机为 **2026.10.11.1**(win32 主机跑不了 pinned 的 + linux 二进制)。本地结论只覆盖 windows 腿;linux/macOS 由 CI 裁决。 +- 本机无 `lua5.4` 与 `bash` → `tests/check_*.lua` 与 `tests/run_members.sh` 无法本地复现,交 CI 的 + `lint` 与 `workspace` 覆盖(与 `2026-10-15-add-rapidcsv-plan.md` 同一处境)。 + +## 5. 注意事项 + +- 根 `mcpp.toml` 本次只动了 `[workspace].members`(加两行)。**没有**顺带把根 `[indices]` 改写成 + `[workspace.indices]`:新写法要求 mcpp ≥ 2026.10.10.1,而 `index.toml` 的 `min_mcpp` 是 + 2026.9.18.3、CI pin 是 2026.10.1.2,两者要一起抬,属于另一件事。本地每次 `mcpp test` 都会打印那条 + 弃用警告,是既有状态,不是本次引入的。 +- 消费侧在 MSVC 上若要格式化非 ASCII 字面量(尤其经 `fmt/chrono.h`),需自行传 `/utf-8`;本包无法 + 替其表达,已在描述符注释里写明。 +- 合并后 `publish-artifact.yml` 自动重发索引 artifact;CN 镜像补传是独立动作。 diff --git a/.agents/docs/2026-10-15-add-soci-plan.md b/.agents/docs/2026-10-15-add-soci-plan.md new file mode 100644 index 00000000..efc80e2a --- /dev/null +++ b/.agents/docs/2026-10-15-add-soci-plan.md @@ -0,0 +1,140 @@ +# 新增 compat.soci 收录(SOCI 4.1.4,C++ 源码 compat + 编入的 sqlite3 后端) + +## 1. 来源与形态 + +- 收录 [SOCI/soci](https://github.com/SOCI/soci) **4.1.4**,为当前最新 tag(`git ls-remote --tags` + 里 `v4.1.4` 最大;注意 4.1.0 从未打 tag,该线自 `v4.1.1` 起连续),不在最新 tag 里挑旧线。 +- 归档 `https://github.com/SOCI/soci/archive/refs/tags/v4.1.4.tar.gz`,**两次下载字节一致**, + SHA-256 `144f017cccc2e2d806badb3313d6ab3c67a1925bccaa747a46fb3907108a615d`。解包顶层 + `soci-4.1.4/`。 +- License **BSL-1.0**(`LICENSE_1_0.txt` = Boost Software License 1.0)。 +- 布局:`include/soci/`(公开头,含每个后端一份 `soci//soci-.h`)、 + `include/private/`(core 用裸名包含的私有头,如 `soci-case.h`)、`src/core/`(22 个 TU)、 + `src/backends/{sqlite3,mysql,odbc,postgresql,oracle,db2,firebird,empty}/`、 + `3rdparty/{sqlite3,fmt}`(**git submodule,归档内为空目录**)。 +- 形态判定:**A(C++ 源码 compat)**。上游是 CMake 工程;本包把 `src/core/*.cpp`(22 TU, + 与 `add_library(soci_core …)` 列表逐项一致)与 `src/backends/sqlite3/*.cpp`(10 TU,与 + `soci_define_backend_target(... SOURCE_FILES ...)` 一致)编成**单一 lib 目标** `soci`。 + 上游拆成 `soci_core` + `soci_sqlite3` 两个库,但 mcpp 的 `sources` 是每包一张表,且没有后端的 + core 连 `session` 都开不了 —— 拆开对消费者没有价值。 +- 两个目录下**同名 TU 成对存在**(blob / error / session / statement),不构成冲突:mcpp 把 + wrap 内相对路径镜像进对象路径(`obj/compat_soci/soci-4.1.4/src/core/statement.o` 与 + `…/src/backends/sqlite3/statement.o`),已从本仓既有 `compat.sdl3` 的 `compile_commands.json` + 确认该布局,并在本地以同形路径分别编译过。 +- 上游要求 C++14(`soci-platform.h` 有 `#if __cplusplus < 201402L #error`);本包按仓内惯例以 + **c++23** 编译,全 32 个 TU 在 clang 23.1.3 / MSVC ABI 下实测无警告级问题。 + +## 2. 依赖:三处上游"没有,必须补" + +| 上游要什么 | 上游怎么给 | 本包怎么补 | +|---|---|---| +| sqlite3 客户端库 | `3rdparty/sqlite3` **子模块**(归档里没有) | `deps` 依赖边 → `compat.sqlite3` 3.45.3(与 `compat.sqlitecpp` 同构;两个消费者因此共用一份 amalgamation) | +| fmt | 系统 fmt,找不到就用 `3rdparty/fmt` 子模块(同样为空)的 **header-only** 目标 | `deps` → `compat.fmt` 12.2.0(编译库形态) | +| Boost | `find_package(Boost)`,找到才定义 `SOCI_USE_BOOST` | **不做依赖**:不加该宏时全部源码不碰 Boost(公开头里的 `` 全在 `SOCI_USE_BOOST` 之下) | + +**fmt 为什么必须是"普通形态",而且这是实测而非推断。** SOCI 4.1 起 core 与 sqlite3 后端共 +12 个 TU 无条件 `#include `。最初按"取 `fmtlib.fmt` 的头 + `FMT_HEADER_ONLY` +编进自己的 TU"实现,`mcpp test -p soci` 一路走到**链接**才失败: + +``` +lld-link: error: duplicate symbol: public: static unsigned int __cdecl + fmt::v12::detail::dragonbox::cache_accessor::compute_mul(...) +>>> defined at fmt-12.2.0/include/fmt/format-inl.h:1113 +>>> obj/fmtlib_fmt/mcpp_generated/fmt_module.m.o +>>> defined at obj/compat_soci/soci-4.1.4/src/core/values.o +``` + +模块对象的 COMDAT 键与 header-only 的不相撞,MSVC ABI 下就是重复符号 —— 即"模块 lib 与 +header-only 消费方不能同链"。换成**编译库形态的 fmt**(`src/format.cc` + `src/os.cc`,即 +`compat.fmt`)后无需任何 define,链接干净。这条也是 `pkgs/f/fmtlib.fmt.lua` 注释里那句 +"the header-based `compat.fmt` package"其实早该存在的直接原因;本次先把 `compat.fmt` 落地 +(独立提交),再让本包依赖它。 + +## 3. 描述符设计:三组 CMake PRIVATE 定义各自的落地方式 + +上游对 `soci_core` / `soci_sqlite3` 传了三组 PRIVATE 编译定义。mcpp 的 `cxxflags` 是**包私有**的 +(只到本包 TU、不到编译 `soci.h` 的消费者 —— `compat.zlib` 的长注释里有实测记录),所以三组都不 +存在外泄给消费者的问题;但它们**能不能穿过描述符**各不相同: + +| 上游定义 | 为什么不能直写 | 本包做法 | +|---|---|---| +| `SOCI_SQLITE3_SOURCE` | 无引号,能直写 | `cxxflags` 里的普通 `-D`。**容易漏且一漏就全红**:`soci-sqlite3.h` 把 `#include ` 放在 `namespace sqlite_api` **内部**,且只在 `#ifdef SOCI_SQLITE3_SOURCE` 下才包含 —— 缺它时后端只剩两句前置声明,每个 `sqlite3_*` 调用都报 "use of undeclared identifier"。消费者走的恰是**不定义**的那条路(只含前置声明),所以这个宏必须止步于本包 | +| `SOCI_LIB_PREFIX` / `SOCI_LIB_SUFFIX` / `SOCI_DEBUG_POSTFIX` | **带引号的字符串宏**,引号穿不过 描述符 → mcpp → 编译器 argv 管道(`compat.unixodbc` 用 `LT_CONFIG_H` 撞过同一堵墙) | 强制包含的 shim 头 `mcpp_soci_defines.h`(`generated_files` + `cxxflags = {"-include", …}`),值按上游算法给(win `soci_*.dll` / mac `libsoci_*.dylib` / else `libsoci_*.so`,DEBUG_POSTFIX 空)。它们只用于给 dlopen 拼后端 DSO 名,而本包不产出任何后端 DSO —— 后端就编在同一个 lib 里 | +| `SOCI_HAVE_SQLITE3`(`soci-config.h`) | 这个头上游**不发布,是 CMake `file(GENERATE)` 出来的**,而 `soci/soci-platform.h` 无条件包含它 | `generated_files` 逐字节复刻 CMake 在"只开 sqlite3"时的输出(含其余七行被注释掉的 `/* #define SOCI_HAVE_* */`) | + +其余两点: + +- `include_dirs` 带 `*/include/private`:core 的 TU 以裸名包含私有头(`soci-case.h`、`soci-mutex.h` …), + 而 mcpp 没有"只给本包用"的 include 路径。消费者会看到该目录,但它惰性(私有头不自洽), + 这是不搬动上游头的代价。 +- **CN 镜像**:无 `mcpp-res` 写权限,按 `docs/cn-mirror.md` 回退为**纯字符串 url** + (`compat.nanodbc` / `compat.sqlite3` 同形);日后补镜像只需改写成 `{ GLOBAL, CN }`,sha256 不变。 + +## 4. Feature 评估:其余后端**不做** feature,理由是可依赖项不存在 + +SOCI 的可选组件就是"另外七个后端",且它们确实是"额外可编译源码"——教科书式的 sources-only +feature 候选。但仍然不做,原因是 feature 只能带**本包声明的 deps**,而这些后端的客户端库: + +- `postgresql`(libpq)、`oracle`(OCI)、`db2`、`firebird`:**本索引里根本没有对应包**; +- `mysql`(`compat.libmysqlclient`)、`odbc`(`compat.unixodbc` / Windows SDK / macOS iODBC)虽有包, + 但 odbc 的驱动管理器**按平台来源完全不同**(linux 靠 `compat.unixodbc` 源码构建、windows/macOS 靠 + 系统 SDK),而 feature 的 `deps` 没有平台门控 —— 声明了就会在另两个平台拉错东西。 + +所以本次只收"能真正交付"的那一个后端,并在描述符注释里写明:以后加后端是**一行 `sources` 加一行 +`deps`**,不是重构。也没有把 core 单独做成 feature:没有后端的 core 连 `session` 都开不了, +门控它等于给消费者一个"编译得过、什么也干不了"的空挡。 + +## 5. 测试与验证 + +单成员 `tests/examples/soci`(`soci-tests`,依赖 `compat.soci = "4.1.4"`,按 `fmt`/`rapidcsv` +的写法重复单条 `[indices] compat = { path = "../../.." }` 并用限定写法请求)。测试选点围绕 +"这个包独有的三处风险": + +- `SOCI_VERSION == 400104` 与 `SOCI_LIB_VERSION == "4_1_4"` **精确断言**,证明编进来的头就是所 pin + 的那一版(与 `sqlite3` 成员断言 `sqlite3_libversion()` 同一手法)。 +- `select sqlite_version()` 必须返回真实版本号 —— 只有 `compat.sqlite3` 的 amalgamation 被真正链接 + 才可能(上游那份在归档里是空的子模块)。 +- 后端以 `soci::sqlite3` **工厂对象**取得,而非 `session("sqlite3", …)`:后者会走 SOCI 的动态后端 + 加载器去 dlopen 一个本包**故意不产出**的 `libsoci_sqlite3.so`。 +- NULL 用 `soci::indicator` 断言(`i_null` vs `i_ok`),这条只有 core 与后端协同才成立; + 事务覆盖显式 `rollback()`、**出作用域未 commit 自动回滚**、以及 `commit()` 三条路径; + 错误路径断言 `sqlite3_soci_error` 的 `get_backend_name() == "sqlite3"`、后端错误码非 0、 + 消息含表名,以及 `get_error_category() == constraint_violation`(主键冲突那条)。 + +验证结论(本机 win32,`mcpp 2026.10.11.1`,工具链 `llvm@23.1.3` + MSVC 14.44.35207, +target `x86_64-pc-windows-msvc`): + +- `mcpp xpkg parse pkgs/c/compat.soci.lua` → `parse OK`(2 sources / 3 include_dirs / + 2 generated / 1 target)。 +- `mcpp test -p soci`(清掉成员 `target/`、`.mcpp/` 后的冷验证)→ + `soci_test ... ok (0.49s)` / `test result ok. 1 passed; 0 failed; finished in 23.52s + (build 14.30s + run 0.49s)`。日志里 `Cached compat.sqlite3 v3.45.3 (index compat, 1 unit)`、 + `Cached compat.fmt v12.2.0 (index compat, 2 units)` —— 两个依赖都由**本 checkout 的 compat 索引** + 解析(`index compat`),不是远端已发布索引。 +- 消费侧命令行的三条旁证(取自本次构建重新生成的仓根 `compile_commands.json`): + 1. **没有 `-DNDEBUG`** → 文件里那批 `assert` 是活的,不是空测试(结尾另有一条 `return count == 4 ? 0 : 1` + 作双保险); + 2. **没有 `-DSOCI_SQLITE3_SOURCE`** → 包私有定义确实没外泄,消费者拿到的是"只含前置声明"的 + 正确消费模式; + 3. `-I …/compat-x-fmt/12.2.0/fmt-12.2.0/include` 与 `-I …/compat-x-sqlite3/3.45.3/sqlite-amalgamation-3450300` + 都在消费侧命令行上 → 依赖接口(`compat.fmt` 的头、`compat.sqlite3` 的头)按预期传播。 +- 版本差异说明:CI pin 为 **2026.10.1.2**,本机为 **2026.10.11.1**(win32 主机跑不了 pinned 的 + linux 二进制)。本地结论只覆盖 windows 腿;linux/macOS 由 CI 裁决。 +- 本机无 `lua5.4` 与 `bash` → `tests/check_*.lua` 与 `tests/run_members.sh` 无法本地复现,交 CI 的 + `lint` 与 `workspace` 覆盖。已按脚本逐条人工核对本描述符:纯字符串 url 免 `check_mirror_urls` + 的表校验(`type(u) == "string"` 直接 return)、`name = "soci"` 为单一原子段、`namespace = "compat"` + 非保留首段、三平台版本键齐平(仅 `4.1.4`)、无重复版本键、无 `install()` 跨包引用。 + +## 6. 注意事项 + +- 根 `mcpp.toml` 本次只动 `[workspace].members`(加一行 `tests/examples/soci`)。**没有**顺带把根 + `[indices]` 改写成 `[workspace.indices]`(同 `compat.fmt` 那一份的理由:新写法要求 + mcpp ≥ 2026.10.10.1,而 `index.toml` 的 `min_mcpp` 与 CI pin 都要一起抬)。 +- 后端以工厂对象取得这件事**必须写进给用户的说明**里:`session("sqlite3", …)` 在静态编入后端时 + 会去 dlopen 不存在的 DSO。描述符注释与测试注释都点明了。 +- 动态加载路径虽然惰性(本包不产出后端 DSO),但 `backend-loader.cpp` 仍会被编译并链接 dlfcn; + linux 上由 libc 提供 `dlopen`(glibc ≥ 2.34 已并入 libc),本包**未**声明 `-ldl` —— CI 的 linux 腿 + 会裁决这一点。 +- 合并后 `publish-artifact.yml` 自动重发索引 artifact;CN 镜像补传是独立动作。 +- 后续可做(不在本 PR 范围):`compat.fmt` 若被重切成 header-only,本包需同步加回 + `-DFMT_HEADER_ONLY=1`(描述符注释里已写明);新增其它后端时记得它们的客户端库得先有索引包。 diff --git a/CHANGELOG.md b/CHANGELOG.md index 7eb29d31..1a09290b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,80 @@ ### Added +- **`compat.soci` 4.1.4。** SOCI(C++ 数据库访问层)以 C++ 源码 compat 形态收录:上游 `src/core` + 的 22 个 TU 与 sqlite3 后端的 10 个 TU 编入**同一个 lib** —— 上游 CMake 把它们拆成 + `soci_core`/`soci_sqlite3` 两个库,但 mcpp 的 `sources` 是每包一张表,且没有后端的 core 连 + `session` 都开不了,所以拍平。两处"上游归档里没有、必须补"的东西:`3rdparty/sqlite3` 是**空的 + git submodule**,由 `compat.sqlite3` 3.45.3 的依赖边替代(与 `compat.sqlitecpp` 同构); + SOCI 4.1 起 core 与后端共 12 个 TU 无条件 `#include `,依赖刚落的 `compat.fmt` + 12.2.0 —— **必须是普通形态,且这是实测**:先按「取 `fmtlib.fmt` 的头 + `FMT_HEADER_ONLY` 编进 + 自己的 TU」实现过,`mcpp test -p soci` 一路走到**链接期**才失败, + `lld-link: error: duplicate symbol … fmt::v12::detail::dragonbox::cache_accessor::compute_mul` + (模块对象的 COMDAT 键与 header-only 的不相撞),换成编译库形态后无需任何 define。 + 三组 CMake PRIVATE 定义分别落地:`-DSOCI_SQLITE3_SOURCE=1` 进 `cxxflags`(缺它 + `soci-sqlite3.h` 只给两句前置声明,每个 `sqlite3_*` 调用都报 "use of undeclared identifier" —— + 消费者走的恰是**不定义**的那条路,所以它必须止步于本包);带引号的 + `SOCI_LIB_PREFIX`/`SOCI_LIB_SUFFIX`/`SOCI_DEBUG_POSTFIX` 走 `-include` shim(引号穿不过 + 描述符 → mcpp → argv 管道,`compat.unixodbc` 撞过同一堵墙);`SOCI_HAVE_SQLITE3` 所在的 + `soci-config.h` 是 CMake `file(GENERATE)` 产物、**不随 tarball 发布**,按「只开 sqlite3」 + 时的输出逐字节复刻进 `generated_files`。Boost 不做依赖:不加 `SOCI_USE_BOOST` 时全部源码不碰它。 + 其余七个后端**不做 feature**:它们的客户端库要么根本不在索引里(libpq / Oracle / DB2 / + Firebird),要么驱动管理器按平台来源天差地别而 feature 的 `deps` 没有平台门控(odbc 在 linux 靠 + `compat.unixodbc` 源码构建、windows/macOS 靠系统 SDK);以后加后端是一行 `sources` 加一行 + `deps`,不是重构。实测:`mcpp xpkg parse pkgs/c/compat.soci.lua` → `parse OK`; + `mcpp test -p soci` 冷验证 → `test result ok. 1 passed; 0 failed`(llvm 23.1.3 / MSVC ABI)—— + `:memory:` 建表、绑定参数双向往返、rowset 迭代、`indicator` 区分 NULL、事务三条路径 + (显式 `rollback()` / 出作用域未 commit 自动回滚 / `commit()`)、后端自带错误码与 + `constraint_violation` 分类、`SOCI_VERSION` 精确断言。消费侧命令行另有两条旁证:实测**无 + `-DNDEBUG`**(所以那批断言是活的,不是空测试)、**无 `-DSOCI_SQLITE3_SOURCE`**(私有定义 + 确实没外泄)。⚠️ 后端以 `soci::sqlite3` **工厂对象**取得:`session("sqlite3", …)` 会走 SOCI 的 + 动态加载器去 dlopen 一个本包故意不产出的 `libsoci_sqlite3.so`。⚠️ CN 镜像尚未发布到 + `mcpp-res`,按先例用纯字符串 url 回退 GLOBAL,补传后 sha256 不变。新增成员 + `tests/examples/soci`;linux/macOS 两腿交 CI 裁决。 + +- **`compat.fmt` 12.2.0。** {fmt} 的**头文件形态**(`#include ` + 链接)收录, + 即 `fmtlib.fmt`(模块形态)的非模块兄弟:同一个上游归档、同一个 sha256,身份却分开 —— + 想要 `#include` 写法的工程不必为此背上一个自己从不 `import` 的模块 BMI。形态是编译库: + `sources` 直接抄上游默认 lib 的两份实现 TU(`*/src/format.cc`、`*/src/os.cc`,即 CMake 的 + `add_library(fmt src/format.cc)` 加上 `FMT_OS` 默认 ON 的 `src/os.cc`), + `include_dirs = {"*/include"}`(前导 `*` 吃掉 `fmt-12.2.0/` 包装层)。没有 + `generated_files`、没有 anchor,也不需要任何 define:`FMT_OS` 在 12.2.0 已不被头文件读取, + `FMT_SHARED`/`FMT_LIB_EXPORT` 是静态链路上不该定义的 DLL 词法,而 `src/fmt.cc` 是模块 unit, + 归 `fmtlib.fmt`。上游那个**独立的** C API 静态库(`src/fmt-c.cc` + `include/fmt/fmt-c.h`) + 按仓内 sources-only feature 收成 `features = { ["c-api"] = … }`,默认 OFF:只要 C++ 头的 + 消费者不付这个 TU。**门控经负向实测**:把 `tests/examples/fmt-c` 的依赖改回纯版本号,它在 + **链接期**报 `lld-link: error: undefined symbol: fmt_vformat` 与 `fmt_vprint`(`fmt_arg` + 的构造在头里是 `static inline`,未解析符号只可能是 fmt-c.cc 的),带 feature 时包构建报 + 3 units、不带报 2 units。⚠️ MSVC 的 `/utf-8` 属**消费侧**:上游标记它 PUBLIC 的唯一来源是 + `chrono.h` 的 `"µs"` 字面量,而本包三个 TU 及它们包含的全部头(base.h / format.h / + format-inl.h)实测均为纯 ASCII;`cxxflags` 只作用于本包 TU,替消费侧表达不了,故不声明, + 只写进描述符注释。⚠️ CN 镜像尚未发布到 `mcpp-res`,按 `compat.rapidcsv` 的先例用纯字符串 + url 回退 GLOBAL;补传镜像后改回 `{ GLOBAL, CN }` 即可,sha256 不变。 + 实测:`mcpp xpkg parse pkgs/c/compat.fmt.lua` → `parse OK`;`mcpp test -p fmt` → + `test result ok. 2 passed; 0 failed`(format.cc 的编译库断言 + os.cc 的 + `getpagesize`/pipe 往返),`mcpp test -p fmt-c` → `1 passed; 0 failed`;`.xpkg.lua` 快照 + 里就是本 checkout 这份带 `c-api` 表的描述符,证明解析没落到远端已发布索引。新增成员 + `tests/examples/fmt` 与 `tests/examples/fmt-c`;linux/macOS 两腿交 CI 裁决。 + +- **`compat.sdl3` 3.4.18。** 在 3.4.2 之外新增版本键(旧键保留,与 `compat.ftxui` + 等既有做法一致)。**补丁号不等于补丁**:这份描述符里真正会失效的东西是源集与两份 + 签入 config,所以四处都按两个 tarball 实测过,而不是假定 —— `src/` 只增 4 个文件 + (两个 Vulkan 着色器头落在已整目录 glob 的目录里;另两个是 + `src/video/wayland/SDL_waylandutil.{c,h}`,wayland 本就不在任何列表里),签入的 + windows/macOS config 与 3.4.2 **逐字节相同**(两个平台的源集正是从它们反推的, + 所以这次抬版本它们照用),linux config 的模板只动三行(删掉的 `HAVE__UITOA` 本就 + 未定义,新增的 `SDL_EMSCRIPTEN_PERSISTENT_PATH_STRING` 只对 emscripten 有意义), + 上游 `CMakeLists.txt` 的源选择未变(唯一的 `.c/.m` 命中行是缩进)。 + ⚠️ 生成的 linux config **不重新生成**:它是冻结的 CMake 输出,3.4.18 新增而它不知道 + 的宏一律读作关闭 —— 与当初裁剪的方向一致,也与「config 只许声明索引真正打了包的 + 东西」同源。⚠️ CN 镜像 3.4.18 尚未发布到 `mcpp-res`,按 `compat.ftxui` 7.0.3 的先例 + 用纯字符串 url 回退到 GLOBAL(`list_cn_urls.lua` 明写忽略纯字符串 url,所以 + `mirror-cn-reachable` 对这一条是空集);补传镜像后改回 `{ GLOBAL, CN }` 即可, + sha256 不变(镜像按原样重托管 tag 归档)。 + 实测:windows 本地 `mcpp test -p sdl3` 编完并跑通 —— 3.4.18 报出 3 个视频驱动, + dummy 窗口 320x240、定时器前进,`test result ok. 1 passed`。linux/macOS 两条腿由 + CI 裁决:冻结的 linux config 是这次唯一没有被本地证伪的东西。 + - **`compat.libserial` 1.0.0。** LibSerial 的 `SerialPort`/`SerialStream` 以 compat 形态收录:上游 CMake `LIBSERIAL_SOURCES` 的三个 TU 编成一个 lib,公开头经 `include_dirs` 暴露。仅声明 `linux`(公开头以 `` 开头、实现触及 diff --git a/README.md b/README.md index e8f30042..45888654 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,8 @@ Additional example: [`compat.rapidcsv`](pkgs/c/compat.rapidcsv.lua) — Header-o Additional example: [`compat.pybind11`](pkgs/c/compat.pybind11.lua) — Header-only Python bindings; [`consumer test`](tests/examples/pybind11/mcpp.toml). Additional example: [`compat.etl`](pkgs/c/compat.etl.lua) — Header-only; [`consumer test`](tests/examples/etl/mcpp.toml). Additional example: [`compat.richdem`](pkgs/c/compat.richdem.lua) — C++ sources, serial terrain analysis; [`consumer test`](tests/examples/richdem/mcpp.toml). +Additional example: [`compat.fmt`](pkgs/c/compat.fmt.lua) — {fmt} in header form (`#include `; the non-module sibling of `fmtlib.fmt`), its C API behind a `c-api` feature; [`consumer test`](tests/examples/fmt/mcpp.toml), [`feature test`](tests/examples/fmt-c/mcpp.toml). +Additional example: [`compat.soci`](pkgs/c/compat.soci.lua) — C++ database API, with the SQLite3 backend compiled in and the engine taken from `compat.sqlite3`; [`consumer test`](tests/examples/soci/mcpp.toml). ## Documentation diff --git a/README.zh-CN.md b/README.zh-CN.md index 701bb3be..cc334fa8 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -63,6 +63,8 @@ agent(如 Claude Code)即可: 新增示例: [`compat.pybind11`](pkgs/c/compat.pybind11.lua) — Header-only Python bindings; [`消费测试`](tests/examples/pybind11/mcpp.toml)。 新增示例: [`compat.etl`](pkgs/c/compat.etl.lua) — Header-only; [`消费测试`](tests/examples/etl/mcpp.toml)。 新增示例: [`compat.richdem`](pkgs/c/compat.richdem.lua) — C++ sources, serial terrain analysis; [`消费测试`](tests/examples/richdem/mcpp.toml)。 +新增示例: [`compat.fmt`](pkgs/c/compat.fmt.lua) — {fmt} 头文件形态(`#include `,即 `fmtlib.fmt` 的非模块兄弟),C API 由 `c-api` feature 门控; [`消费测试`](tests/examples/fmt/mcpp.toml)、[`feature 测试`](tests/examples/fmt-c/mcpp.toml)。 +新增示例: [`compat.soci`](pkgs/c/compat.soci.lua) — C++ 数据库访问层,SQLite3 后端编入同一库,引擎来自 `compat.sqlite3`; [`消费测试`](tests/examples/soci/mcpp.toml)。 ## 文档 diff --git a/mcpp.toml b/mcpp.toml index b8a7c56a..d1d380a5 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -56,6 +56,8 @@ members = [ "tests/examples/eui-neo-window", "tests/examples/ffmpeg", "tests/examples/ffmpeg-module", + "tests/examples/fmt", + "tests/examples/fmt-c", "tests/examples/fmtlib.fmt", "tests/examples/ftxui-module", "tests/examples/gmp", @@ -149,6 +151,7 @@ members = [ "tests/examples/llmapi", "tests/examples/md4c", "tests/examples/spdlog-compiled", + "tests/examples/soci", "tests/examples/sqlite3", "tests/examples/sqlitecpp", "tests/examples/taskflow-module", diff --git a/pkgs/c/compat.fmt.lua b/pkgs/c/compat.fmt.lua new file mode 100644 index 00000000..14ee0058 --- /dev/null +++ b/pkgs/c/compat.fmt.lua @@ -0,0 +1,126 @@ +-- Form B inline descriptor for {fmt} in its header form: consumers write +-- `#include ` and link, exactly as upstream's README shows. +-- +-- This is the NON-module half of the pair. `pkgs/f/fmtlib.fmt.lua` is the +-- module sibling (`import fmt;`), and it is built from a rewritten +-- `src/fmt.cc`; the two carry deliberately separate identities under separate +-- namespaces because they are two ways to consume the same release. A consumer +-- picks one — a project that wants the `#include` spelling must not be forced +-- to take a module BMI it never imports, which is what a single identity +-- would do. +-- +-- SHAPE: upstream's own default library, transcribed rather than wrapped. +-- `src/format.cc` is `format-inl.h` plus the explicit instantiations, and +-- `src/os.cc` is the OS layer (file / buffered_file / pipe / ostream / +-- getpagesize). Both are what CMake's default configure produces: +-- `add_library(fmt src/format.cc)` and, under the default `FMT_OS=ON`, +-- `target_sources(fmt PRIVATE src/os.cc)`. There is no generated_files and no +-- anchor TU here — unlike the module package, nothing has to be synthesized. +-- +-- `src/fmt.cc` is NOT taken: it is upstream's C++20 module interface unit +-- (`export module fmt;`), i.e. the module build, covered by fmtlib.fmt. +-- +-- `include_dirs = {"*/include"}` with the leading glob absorbing the archive's +-- `fmt-12.2.0/` wrap layer; the public headers live under `include/fmt/`, so +-- `{"*"}` would compile this package and still leave every consumer's +-- `#include ` unresolvable. The same mistake the compat.rapidcsv +-- descriptor is annotated against. +-- +-- NO `defines`, and none are needed: +-- * `FMT_OS` is read by `src/fmt.cc` alone in this release — the headers no +-- longer consult it — so compiling os.cc unconditionally is the whole +-- story on both sides of the boundary. +-- * `FMT_SHARED` / `FMT_LIB_EXPORT` select a DLL's dllexport/dllimport +-- spelling in base.h and are only read on Win32 in a non-header-only +-- build. This target is a static lib in every consumer, so neither is +-- defined, and the plain `FMT_API` (empty) branch is the correct one. +-- +-- MSVC AND `/utf-8` (a consumer-side fact, not this package's): upstream marks +-- `/utf-8` PUBLIC for MSVC because `chrono.h` carries a UTF-8 literal — the +-- micro sign in the sub-second unit string, line 581, +-- `detail::use_utf8 ? "s" : "us"`. That is the ONLY non-ASCII +-- byte sequence reachable from the library: this package's own three TUs +-- (format.cc, os.cc, fmt-c.cc) and every header they include — base.h, +-- format.h, format-inl.h — are pure ASCII, measured byte by byte. So the +-- library itself is unaffected by the source charset, and `cxxflags` could not +-- fix the consumer's copy anyway (it applies to this package's TUs only). A +-- consumer on MSVC that formats Unicode through fmt/chrono.h must pass +-- `/utf-8` itself, the same way tests/examples/openssl and openblas pass their +-- own consumer-side flags. A chrono test here would be asserting the member's +-- toolchain, not the package, so this member does not include that header. +-- +-- NO CN mirror yet -- the url stays a plain string (the documented fallback +-- when there is no `mcpp-res` access, and the shape compat.rapidcsv / pybind11 +-- / etl use). Adding the mirror later only rewrites `url` into +-- `{ GLOBAL = …, CN = … }`; the sha256 does not change, since the mirror +-- re-hosts the tag archive byte for byte. +package = { + spec = "1", + namespace = "compat", + name = "fmt", + description = "A modern formatting library for C++ — header form (#include )", + licenses = {"MIT"}, + repo = "https://github.com/fmtlib/fmt", + type = "package", + + xpm = { + linux = { + ["12.2.0"] = { + url = "https://github.com/fmtlib/fmt/archive/refs/tags/12.2.0.tar.gz", + sha256 = "8b852bb5aa6e7d8564f9e81394055395dd1d1936d38dfd3a17792a02bebd7af0", + }, + }, + macosx = { + ["12.2.0"] = { + url = "https://github.com/fmtlib/fmt/archive/refs/tags/12.2.0.tar.gz", + sha256 = "8b852bb5aa6e7d8564f9e81394055395dd1d1936d38dfd3a17792a02bebd7af0", + }, + }, + windows = { + ["12.2.0"] = { + url = "https://github.com/fmtlib/fmt/archive/refs/tags/12.2.0.tar.gz", + sha256 = "8b852bb5aa6e7d8564f9e81394055395dd1d1936d38dfd3a17792a02bebd7af0", + }, + }, + }, + + mcpp = { + language = "c++23", + import_std = false, + -- fmt-c.h's C face is `_Generic` plus compound literals: C11. Inert for + -- this package's own TUs, which are all C++ (the compat.richdem shape). + c_standard = "c11", + include_dirs = { "*/include" }, + sources = { "*/src/format.cc", "*/src/os.cc" }, + targets = { ["fmt"] = { kind = "lib" } }, + + -- Upstream ships the C API as a SEPARATE library (`add_library(fmt-c + -- STATIC src/fmt-c.cc)`, header include/fmt/fmt-c.h): one extra TU that + -- the overwhelmingly common consumer — one that writes C++ and never + -- touches fmt/fmt-c.h — should not pay for. That is exactly the + -- "additional compilable source" the sources-only feature table is + -- for (the compat.concurrentqueue `c-api` and compat.cjson `utils` + -- precedent), and it is OFF by default. Requested as + -- `fmt = { version = "12.2.0", features = ["c-api"] }`, it joins the + -- same lib target. + -- + -- The gate is real, measured, not assumed. With the feature off, + -- tests/examples/fmt-c stops at LINK (Windows/LLVM wording): + -- lld-link: error: undefined symbol: fmt_vformat + -- lld-link: error: undefined symbol: fmt_vprint + -- The fmt_arg constructors are `static inline` in the header, so the + -- unresolved symbols can only be fmt-c.cc's; the package build reports + -- 3 units with the feature and 2 without it. Transcript in + -- .agents/docs/2026-10-15-add-compat-fmt-plan.md. + -- + -- NOT covered here, deliberately: grep the tarball for another optional + -- TU and there is none. fmt-header-only is a define-only INTERFACE + -- target (FMT_HEADER_ONLY), and FMT_OS is not a source toggle either + -- (the headers stopped reading it), so neither is expressible as a + -- sources-only feature. + features = { + ["c-api"] = { sources = { "*/src/fmt-c.cc" } }, + }, + deps = { }, + }, +} diff --git a/pkgs/c/compat.sdl3.lua b/pkgs/c/compat.sdl3.lua index 44cc28b1..926ea7bd 100644 --- a/pkgs/c/compat.sdl3.lua +++ b/pkgs/c/compat.sdl3.lua @@ -116,9 +116,42 @@ -- derived as above and confirmed by CI rather than locally — there is no way -- to compile either from here. -- --- CN mirror: `gitcode.com/mcpp-res/sdl3`, the upstream tarball re-hosted --- BYTE-IDENTICALLY (verified: the mirror's sha256 equals the one declared --- here). GLOBAL stays the default. +-- ── 3.4.18: a patch bump that moved no source list ───────────────────────── +-- +-- Everything above is a property of the SHAPE (config dispatcher, source +-- lists), and 3.4.2 → 3.4.18 leaves the shape alone. Measured against both +-- tarballs rather than assumed, because a patch does not imply a patch: +-- +-- * `git diff --no-index --name-status` over `src/`: **4 files added, 0 +-- removed.** Two are Vulkan shader headers in a directory that is already +-- globbed whole; the other two are `src/video/wayland/SDL_waylandutil.{c,h}` +-- and wayland is off here — `src/video/wayland` appears in no list above. +-- So no glob gains and none needs an entry. +-- * the CHECKED-IN windows/macOS configs (`include/build_config/`) are +-- byte-identical between the two tags — which is the whole reason the +-- windows/macOS source lists above survive the bump, since they were +-- derived from those headers. +-- * `include/build_config/SDL_build_config.h.cmake` — the template the linux +-- config was generated from — moves by three lines, neither of which +-- reaches the generated block below: `HAVE__UITOA` was dropped (already +-- `/* #undef */` here) and `SDL_EMSCRIPTEN_PERSISTENT_PATH_STRING` was +-- added (emscripten only). +-- * upstream's `CMakeLists.txt` selects the same sources: its diff touches +-- no `.c`/`.cpp`/`.m` line except one re-indent. +-- +-- ⚠️ The generated linux config is NOT regenerated, and that is deliberate: it +-- is a frozen CMake output, so a macro 3.4.18 added and this file never learned +-- about reads as "off" — the conservative direction, and the same one the +-- original generation was trimmed in. Regenerating it means re-running the +-- CMake invocation in the header above with the index toolchain and re-checking +-- the seven `dlopen` names against packages, so it is worth doing when a define +-- is actually needed, not per patch. +-- +-- CN mirror: `gitcode.com/mcpp-res/sdl3` — 3.4.2 only. The upstream tarball +-- re-hosted BYTE-IDENTICALLY (verified: the mirror's sha256 equals the one +-- declared here). 3.4.18 is not mirrored yet, so its entry below declares the +-- upstream url as a plain string and CN consumers fall back to GLOBAL; the +-- mirror is a `mcpp-res` upload away. GLOBAL stays the default either way. package = { spec = "1", namespace = "compat", @@ -137,6 +170,14 @@ package = { }, sha256 = "515ee4ad6e910e0ad8d8bbccb24ac52dd16c419e23eea3fb83541d20130c7aaa", }, + -- No CN mirror yet; plain-string url is the documented fallback + -- (compat.ftxui does the same for its un-mirrored 7.0.3). Flip to + -- { GLOBAL, CN } once uploaded — sha256 does not move, the mirror + -- re-hosts this archive byte-identically. + ["3.4.18"] = { + url = "https://github.com/libsdl-org/SDL/archive/refs/tags/release-3.4.18.tar.gz", + sha256 = "c4b08b950bd29d83caae0ab8d884298d27046a9d30700fb42841c50992665c90", + }, }, macosx = { ["3.4.2"] = { @@ -146,6 +187,11 @@ package = { }, sha256 = "515ee4ad6e910e0ad8d8bbccb24ac52dd16c419e23eea3fb83541d20130c7aaa", }, + -- No CN mirror yet; see the note on the linux block. + ["3.4.18"] = { + url = "https://github.com/libsdl-org/SDL/archive/refs/tags/release-3.4.18.tar.gz", + sha256 = "c4b08b950bd29d83caae0ab8d884298d27046a9d30700fb42841c50992665c90", + }, }, windows = { ["3.4.2"] = { @@ -155,6 +201,11 @@ package = { }, sha256 = "515ee4ad6e910e0ad8d8bbccb24ac52dd16c419e23eea3fb83541d20130c7aaa", }, + -- No CN mirror yet; see the note on the linux block. + ["3.4.18"] = { + url = "https://github.com/libsdl-org/SDL/archive/refs/tags/release-3.4.18.tar.gz", + sha256 = "c4b08b950bd29d83caae0ab8d884298d27046a9d30700fb42841c50992665c90", + }, }, }, diff --git a/pkgs/c/compat.soci.lua b/pkgs/c/compat.soci.lua new file mode 100644 index 00000000..221e9037 --- /dev/null +++ b/pkgs/c/compat.soci.lua @@ -0,0 +1,197 @@ +-- Form B inline descriptor for SOCI — a C++ database access layer that puts one +-- API in front of many engines. A consumer writes `#include ` and +-- links `soci`. +-- +-- SHAPE: A (C++ source compat). Upstream is CMake-built; this descriptor compiles +-- the TUs directly — `src/core/*.cpp` (upstream's 22-file `add_library(soci_core +-- …)` list) and `src/backends/sqlite3/*.cpp` (upstream's 10-file `soci_sqlite3` +-- list) — into ONE lib target. Upstream ships those as two libraries, but a +-- backend-less SOCI core cannot open a session at all, so the split buys a +-- consumer nothing here; and mcpp's `sources` is one list per package anyway. +-- Same-named TUs across the two directories (blob, error, session, statement +-- each exist twice) do not collide: mcpp mirrors the source path relative to the +-- wrap directory into the object path (`obj/compat_soci/soci-4.1.4/src/core/…`). +-- +-- WHY THE SQLITE3 BACKEND, AND WHY IT IS NOT GATED. A backend is what makes SOCI +-- usable, so a core-only package would be a package nobody can use. sqlite3 is +-- the only backend whose client library is BOTH in this index (`compat.sqlite3`) +-- and needs nothing from the host: upstream vendors it as the `3rdparty/sqlite3` +-- GIT SUBMODULE, and a source tarball carries no submodules — the dependency edge +-- replaces it, exactly as `compat.sqlitecpp` does (and two consumers of SQLite in +-- one link then share one amalgamation instead of embedding private copies). +-- The other seven backends (mysql, odbc, postgresql, oracle, db2, firebird, +-- empty) each need an outside library — libpq, Oracle, DB2 and Firebird clients +-- are not in this index at all — so they are left out rather than half-declared. +-- Adding one later is a `sources` line plus a `deps` line, not a rework. +-- +-- THE TWO DEPENDENCIES. +-- * `compat.fmt` — SOCI 4.1 made fmt a HARD dependency of the core: seven core +-- TUs (backend-loader, connection-parameters, error, row, session, statement, +-- values) `#include ` unconditionally, and the sqlite3 +-- backend's session/standard-use-type/vector-* TUs do too. What this package +-- needs is an ORDINARY fmt: headers plus a library of global-module symbols. +-- The module form cannot serve it, and that is measured rather than assumed — +-- compiling those headers header-only into SOCI's TUs while the C++23 module +-- package `fmtlib.fmt` sat in the same link produced real duplicate symbols +-- on the MSVC ABI (lld-link: duplicate symbol +-- `fmt::v12::detail::dragonbox::cache_accessor::compute_mul`, defined +-- once in the module object and once in SOCI's values.o), because a module +-- object's COMDAT keys do not collide with header-only ones. `compat.fmt` is +-- that ordinary fmt, and it is what the module package's own comment calls +-- its header-form sibling. Nothing here needs any define about fmt. +-- * `compat.sqlite3` — the sqlite3 backend's client library, i.e. what the +-- missing submodule would have provided. +-- +-- MACROS THAT CANNOT CROSS THE DESCRIPTOR. Upstream passes three groups of PRIVATE +-- compile definitions to `soci_core`/`soci_sqlite3`; mcpp's `cxxflags` is +-- package-private (it reaches this package's TUs and not a consumer compiling +-- `soci.h` — the property `compat.zlib` documents at length), so all three are +-- free of consumer-visible side effects: +-- +-- 1. `-DSOCI_SQLITE3_SOURCE=1`, a plain flag — load-bearing and easy to miss: +-- `soci-sqlite3.h` puts `#include ` INSIDE `namespace sqlite_api` +-- and only does so `#ifdef SOCI_SQLITE3_SOURCE`; without the define the +-- backend sees just the two forward declarations, and every `sqlite3_*` call +-- fails to compile with "use of undeclared identifier". Consumers are the +-- mirror image of that switch: including the header WITHOUT the define is +-- the supported consumer mode, which is why the define must not leak. +-- 2. `SOCI_LIB_PREFIX` / `SOCI_LIB_SUFFIX` / `SOCI_DEBUG_POSTFIX`, which +-- backend-loader.cpp uses to build backend shared-object names for dlopen(). +-- They are QUOTED string macros, and quotes do not survive the descriptor → +-- mcpp → compiler-argv pipeline (`compat.unixodbc` measured the same wall +-- with `LT_CONFIG_H`), so they arrive via a force-included shim header +-- instead. Their values mirror upstream's own computation; nothing here +-- ships or loads a backend DSO, so the name is inert — backends are compiled +-- INTO the same lib, and a consumer reaches the sqlite3 backend through the +-- `soci::sqlite3` factory object rather than through `session("sqlite3",…)`, +-- which would go looking for `libsoci_sqlite3.so`. +-- 3. `SOCI_HAVE_SQLITE3`, i.e. `soci-config.h` — the one header upstream's +-- CMake GENERATES rather than ships, and `soci/soci-platform.h` includes it +-- unconditionally. mcpp has no configure step, so it is reproduced verbatim +-- in `generated_files`, including the commented-out `SOCI_HAVE_*` lines for +-- the backends that are not built. +-- +-- Boost is NOT a dependency and is deliberately left off: SOCI's public headers +-- only reach for `` under `SOCI_USE_BOOST`, which upstream's CMake +-- defines only when it finds Boost, and no source uses Boost otherwise. +-- +-- `include_dirs` carries `*/include/private` because the core TUs include the +-- private headers (`soci-case.h`, `soci-mutex.h`, …) by bare name. mcpp has no +-- build-only include path, so consumers see that directory too; it is inert +-- (the private headers are not self-contained) and is the price of not copying +-- upstream's headers around. +-- +-- No CN mirror: no `mcpp-res` write access from here, so `url` stays a plain +-- string and CN users fall back to the GLOBAL source (docs/cn-mirror.md; the +-- precedent is compat.nanodbc / compat.sqlite3, and the maintainer can turn it +-- into a { GLOBAL, CN } table later without the sha256 changing). +package = { + spec = "1", + namespace = "compat", + name = "soci", + description = "SOCI — a C++ database access library with one API over many engines", + licenses = {"BSL-1.0"}, + repo = "https://github.com/SOCI/soci", + type = "package", + + xpm = { + linux = { + ["4.1.4"] = { + url = "https://github.com/SOCI/soci/archive/refs/tags/v4.1.4.tar.gz", + sha256 = "144f017cccc2e2d806badb3313d6ab3c67a1925bccaa747a46fb3907108a615d", + }, + }, + macosx = { + ["4.1.4"] = { + url = "https://github.com/SOCI/soci/archive/refs/tags/v4.1.4.tar.gz", + sha256 = "144f017cccc2e2d806badb3313d6ab3c67a1925bccaa747a46fb3907108a615d", + }, + }, + windows = { + ["4.1.4"] = { + url = "https://github.com/SOCI/soci/archive/refs/tags/v4.1.4.tar.gz", + sha256 = "144f017cccc2e2d806badb3313d6ab3c67a1925bccaa747a46fb3907108a615d", + }, + }, + }, + + mcpp = { + language = "c++23", + import_std = false, + -- `*/include` exposes and the per-backend + -- ; `*/include/private` is for the core's own + -- bare-name private includes; `mcpp_generated/include` carries the + -- generated soci-config.h and the force-included shim (see the header). + include_dirs = { "*/include", "*/include/private", "mcpp_generated/include" }, + + generated_files = { + -- Upstream's CMake `file(GENERATE)`s this from the SOCI_ cache + -- variables (`src/CMakeLists.txt`); it is the only header in the + -- public API set that does not exist in the tarball. Content below is + -- what CMake emits with the sqlite3 backend enabled and the others + -- off, byte-for-byte including the disabled `/* #define */` lines. + ["mcpp_generated/include/soci/soci-config.h"] = [==[ +#ifndef SOCI_SOCICONFIG_H_INCLUDED +#define SOCI_SOCICONFIG_H_INCLUDED + +/* #define SOCI_HAVE_EMPTY */ + +/* #define SOCI_HAVE_DB2 */ + +/* #define SOCI_HAVE_FIREBIRD */ + +/* #define SOCI_HAVE_MYSQL */ + +/* #define SOCI_HAVE_ODBC */ + +/* #define SOCI_HAVE_ORACLE */ + +/* #define SOCI_HAVE_POSTGRESQL */ + +#define SOCI_HAVE_SQLITE3 1 + +#endif +]==], + + -- Point (2) in the header comment: the quoted-string definitions CMake + -- passes PRIVATE to soci_core, which the descriptor cannot express. + -- Force-included (see cxxflags) so backend-loader.cpp sees them no + -- matter which TU is being compiled. + ["mcpp_generated/include/mcpp_soci_defines.h"] = [==[ +#ifndef MCPP_SOCI_DEFINES_H +#define MCPP_SOCI_DEFINES_H +#if defined(_WIN32) +#define SOCI_LIB_PREFIX "soci_" +#define SOCI_LIB_SUFFIX ".dll" +#elif defined(__APPLE__) +#define SOCI_LIB_PREFIX "libsoci_" +#define SOCI_LIB_SUFFIX ".dylib" +#else +#define SOCI_LIB_PREFIX "libsoci_" +#define SOCI_LIB_SUFFIX ".so" +#endif +#define SOCI_DEBUG_POSTFIX "" +#endif +]==], + }, + + cxxflags = { "-DSOCI_SQLITE3_SOURCE=1", + "-include", "mcpp_soci_defines.h" }, + + sources = { + "*/src/core/*.cpp", + "*/src/backends/sqlite3/*.cpp", + }, + + targets = { ["soci"] = { kind = "lib" } }, + + -- Must be an ORDINARY (non-module) fmt — see the header comment for the + -- measured reason. If `compat.fmt` is ever re-cut as a header-only + -- package, this package has to add `-DFMT_HEADER_ONLY=1` to `cxxflags` + -- to match it. + deps = { + ["compat.fmt"] = "12.2.0", + ["compat.sqlite3"] = "3.45.3", + }, + }, +} diff --git a/tests/examples/fmt-c/mcpp.toml b/tests/examples/fmt-c/mcpp.toml new file mode 100644 index 00000000..b9214158 --- /dev/null +++ b/tests/examples/fmt-c/mcpp.toml @@ -0,0 +1,30 @@ +# compat.fmt's `c-api` FEATURE path — the OTHER half of the package, the way +# tests/examples/concurrentqueue-c-api is the other half of compat.concurrentqueue. +# +# The base member (tests/examples/fmt) consumes the default build, which does +# NOT compile src/fmt-c.cc; this one asks for the feature in the long form and +# asserts the C API's two entry points actually resolve and behave, which is +# what proves the gate both compiles and links what it names. The negative half +# is structural and was measured: flip this dependency back to a plain +# `fmt = "12.2.0"` and this member fails to LINK, one undefined reference per +# entry point (fmt_arg's constructors are `static inline` in the header, so the +# unresolved symbols can only be fmt-c.cc's). +# +# The test source is C++ and calls the `extern "C"` functions directly rather +# than C with the header's fmt_format/fmt_print macros. Those macros live under +# `#ifndef __cplusplus` and demand MSVC's conformant preprocessor +# (/Zc:preprocessor, which the header #errors about) — a consumer-side compiler +# flag no descriptor can express. The linkage claim under test is the same +# either way. +# +# Same QUALIFIED-dependency reasoning as the base member: the request must +# resolve from this checkout's index. +[package] +name = "fmt-c-tests" +version = "0.1.0" + +[dependencies.compat] +fmt = { version = "12.2.0", features = ["c-api"] } + +[indices] +compat = { path = "../../.." } diff --git a/tests/examples/fmt-c/tests/c_api.cpp b/tests/examples/fmt-c/tests/c_api.cpp new file mode 100644 index 00000000..20331da3 --- /dev/null +++ b/tests/examples/fmt-c/tests/c_api.cpp @@ -0,0 +1,60 @@ +// compat.fmt's `c-api` feature: the two entry points of src/fmt-c.cc, called +// through fmt/fmt-c.h's `extern "C"` surface. Both are declared in the header, +// and only this TU (or a consumer's) can provide them — with the feature off +// this file does not link. See this member's mcpp.toml for why the calls are +// spelled this way instead of through the header's C-only fmt_format macro. +// +// Nothing here needs a file on disk: the format side is asserted in memory, +// and fmt_vprint runs against a stream the process owns (tmpfile is unlinked +// on close), so a passing test prints nothing. +#include + +#include +#include + +int main() { + fmt_arg args[2] = {}; + args[0].type = fmt_cstring; + args[0].value.cstring = "world"; + args[1].type = fmt_int; + args[1].value.int_value = 42; + + const std::string expected = "hello world #42"; + char buffer[64] = {}; + const int n = fmt_vformat(buffer, sizeof(buffer), "hello {} #{}", args, 2); + bool ok = n == static_cast(expected.size()) && expected == buffer; + + // Buffer contract, straight out of vformat_to_n: the return value is how + // much output there would have been, not how much fit, and the buffer is + // not NUL-terminated. + char small[6] = {}; + const int m = fmt_vformat(small, sizeof(small), "hello {} #{}", args, 2); + ok = ok && m == static_cast(expected.size()) + && std::string(small, sizeof(small)) == "hello "; + + // The error contract, both halves: an unknown fmt_type is refused before + // anything is written, and so is a call with more arguments than the API's + // fixed 16-slot table. + fmt_arg unknown[1] = {}; + unknown[0].type = static_cast(99); + ok = ok && fmt_vformat(buffer, sizeof(buffer), "{}", unknown, 1) + == fmt_error_invalid_arg; + + fmt_arg many[17] = {}; + ok = ok && fmt_vformat(buffer, sizeof(buffer), "{}", many, 17) + == fmt_error_invalid_arg; + + // The other entry point the feature compiles. + std::FILE* f = std::tmpfile(); + ok = ok && f != nullptr; + if (f != nullptr) { + ok = ok && fmt_vprint(f, "[{}]", args, 2) == 0; + std::rewind(f); // delivers the stream's unwritten data, then seeks back + char read[64] = {}; + const std::size_t got = std::fread(read, 1, sizeof(read) - 1, f); + ok = ok && std::string(read, got) == "[world]"; + std::fclose(f); + } + + return ok ? 0 : 1; +} diff --git a/tests/examples/fmt/mcpp.toml b/tests/examples/fmt/mcpp.toml new file mode 100644 index 00000000..f3ab8fe7 --- /dev/null +++ b/tests/examples/fmt/mcpp.toml @@ -0,0 +1,19 @@ +# {fmt} test project — the HEADER form: consume compat.fmt with +# `#include ` and a link against the compiled library, and assert +# that the package's own TUs (format.cc, os.cc) are what answers the calls +# rather than header-only inlines. The module sibling (`import fmt;`) is a +# different package with its own member, tests/examples/fmtlib.fmt. +# +# `compat.fmt` is spelled qualified so the workspace-root [indices] redirect +# applies, and this member repeats the one-entry redirect itself (same as +# rapidcsv/etl/gsl): the dependency must resolve to THIS checkout's descriptor, +# not to whatever the published remote index serves. +[package] +name = "fmt-tests" +version = "0.1.0" + +[dependencies.compat] +fmt = "12.2.0" + +[indices] +compat = { path = "../../.." } diff --git a/tests/examples/fmt/tests/format.cpp b/tests/examples/fmt/tests/format.cpp new file mode 100644 index 00000000..ef2fd1bb --- /dev/null +++ b/tests/examples/fmt/tests/format.cpp @@ -0,0 +1,42 @@ +// compat.fmt — the compiled core. `fmt::vformat` is defined in format-inl.h, +// which only src/format.cc compiles into a library TU (a consumer sees it as a +// declaration unless FMT_HEADER_ONLY is defined, which this member must not +// do), and the runtime format string is what makes that the path actually +// taken: a literal argument would let the compiler fold the call away, and +// without the package's format.cc on the link line nothing here would link at +// all. The rest below is upstream's documented surface, asserted rather than +// printed. +// +// Nothing here reads a file or the network. +#include + +#include +#include + +int main() { + bool ok = fmt::format("{} + {} = {}", 2, 3, 2 + 3) == "2 + 3 = 5"; + + // Runtime format string through the compiled vformat. + ok = ok && fmt::vformat("{0}-{1}-{0}", fmt::make_format_args("x", "y")) == "x-y-x"; + + // Padding/precision/width and alternate form. + ok = ok && fmt::format("{:08.3f}", 3.14159) == "0003.142"; + ok = ok && fmt::format("{:>8}", "fmt") == " fmt"; + ok = ok && fmt::format("{:#x}", 255) == "0xff"; + + // format_to writes through an output iterator and returns it, so a second + // call on the result appends rather than restarts. + std::string out; + auto it = fmt::format_to(std::back_inserter(out), "{}-{}", 1, 2); + fmt::format_to(it, "!"); + ok = ok && out == "1-2!"; + + // Deliberately NOT asserted here: fmt/chrono.h. Its micro-sign duration + // literal (chrono.h:581) is the one non-ASCII byte sequence reachable in + // the package, and on MSVC it is only read correctly under a source charset + // the descriptor cannot impose on a consumer, so a chrono assertion would + // be a statement about this member's toolchain rather than about the + // package. See pkgs/c/compat.fmt.lua. + + return ok ? 0 : 1; +} diff --git a/tests/examples/fmt/tests/os.cpp b/tests/examples/fmt/tests/os.cpp new file mode 100644 index 00000000..8992a06d --- /dev/null +++ b/tests/examples/fmt/tests/os.cpp @@ -0,0 +1,26 @@ +// compat.fmt — the package's second TU, src/os.cc, which is what +// declares and nothing else in the library defines. `fmt::getpagesize()` is +// the cheapest proof of that: a pure function, no descriptor, no file, no +// output. The pipe round trip then exercises the same TU's file objects +// behaviorally (a pipe rather than a path: no artifact is left behind, and +// nothing outside the process is touched). +#include + +#include + +int main() { + // Defined in src/os.cc only -- if that TU were not linked, this fails at + // LINK, not here. + bool ok = fmt::getpagesize() > 0; + + fmt::pipe p; + const std::string sent = "fmt/os.h"; + const auto written = p.write_end.write(sent.data(), sent.size()); + ok = ok && written == sent.size(); + + char buf[64] = {}; + const auto got = p.read_end.read(buf, sizeof(buf)); + ok = ok && got == sent.size() && std::string(buf, got) == sent; + + return ok ? 0 : 1; +} diff --git a/tests/examples/sdl3/mcpp.toml b/tests/examples/sdl3/mcpp.toml index bbc69d57..63f111df 100644 --- a/tests/examples/sdl3/mcpp.toml +++ b/tests/examples/sdl3/mcpp.toml @@ -9,4 +9,4 @@ name = "sdl3-tests" version = "0.1.0" [dependencies.compat] -sdl3 = "3.4.2" +sdl3 = "3.4.18" diff --git a/tests/examples/sdl3/tests/init.cpp b/tests/examples/sdl3/tests/init.cpp index c6d1dd5d..706ce59e 100644 --- a/tests/examples/sdl3/tests/init.cpp +++ b/tests/examples/sdl3/tests/init.cpp @@ -32,8 +32,8 @@ int main() { std::printf("header says %d, library says %d — two different SDLs\n", compiled, linked); return 1; } - if (linked != SDL_VERSIONNUM(3, 4, 2)) { - std::printf("SDL version is %d, expected 3.4.2\n", linked); + if (linked != SDL_VERSIONNUM(3, 4, 18)) { + std::printf("SDL version is %d, expected 3.4.18\n", linked); return 2; } @@ -119,7 +119,7 @@ int main() { SDL_DestroyWindow(window); SDL_Quit(); - std::printf("compat.sdl3: ok (version 3.4.2, %d video drivers, dummy window 320x240, timer advances)\n", + std::printf("compat.sdl3: ok (version 3.4.18, %d video drivers, dummy window 320x240, timer advances)\n", drivers); return 0; } diff --git a/tests/examples/soci/mcpp.toml b/tests/examples/soci/mcpp.toml new file mode 100644 index 00000000..9c0ccbc4 --- /dev/null +++ b/tests/examples/soci/mcpp.toml @@ -0,0 +1,28 @@ +# SOCI test project: a real SQLite session through SOCI's own session/statement +# layer. +# +# The package brings its own sqlite3 backend plus a dependency edge on +# compat.sqlite3, so this member is what proves upstream's missing +# `3rdparty/sqlite3` submodule has really been replaced: the assertions reach +# `sqlite_version()` and the backend's own error codes, which need a linked-in +# SQLite and not merely a header. It is also the first member to consume +# compat.fmt as a plain `#include ` dependency of a library. +# +# `compat.soci` is spelled qualified so the workspace-root [indices] redirect +# applies, and this member repeats the one-entry redirect itself (same as +# fmt/rapidcsv/etl/gsl): the dependency must resolve to THIS checkout's +# descriptor — which is unmerged — not to whatever the published remote index +# serves. +# +# No platform gating: all three xpm legs are declared and the backend builds +# against SQLite's amalgamation everywhere, so the same assertions run on linux, +# macOS and windows. +[package] +name = "soci-tests" +version = "0.1.0" + +[dependencies.compat] +soci = "4.1.4" + +[indices] +compat = { path = "../../.." } diff --git a/tests/examples/soci/tests/soci_test.cpp b/tests/examples/soci/tests/soci_test.cpp new file mode 100644 index 00000000..733257f6 --- /dev/null +++ b/tests/examples/soci/tests/soci_test.cpp @@ -0,0 +1,185 @@ +// Behavioral test: a real SQLite session driven through SOCI. +// +// SOCI is a thin layer over a backend, so a test that opens no session proves +// almost nothing. Every interesting way this package can be wrong — the backend +// TUs not compiled, the missing upstream git submodule not replaced by the +// compat.sqlite3 edge, the private SOCI_SQLITE3_SOURCE switch mismatched, a +// stale generated soci-config.h — shows up only once a statement runs. So every +// assertion below goes through soci::session against `:memory:`, and four of +// them are chosen to be unfakeable: +// +// * SOCI_VERSION is asserted EXACTLY: it identifies the pinned upstream +// release, the same way tests/examples/sqlite3 asserts sqlite3_libversion(). +// * `select sqlite_version()` must return a real SQLite version: the +// amalgamation can only have come from the compat.sqlite3 dependency, since +// upstream's own copy is a git submodule the source tarball omits. +// * the error paths must produce a sqlite3_soci_error carrying the backend's +// own backend name, error code and error CATEGORY — a real backend raising +// real errors, not a stub. +// * NULL round-tripping is asserted through soci::indicator, which is SOCI's +// own layer (the backend reports the column type, the core maps it). +// +// The backend is reached as a FACTORY OBJECT (soci::sqlite3) rather than through +// session("sqlite3", ...): the name-based overload hands off to SOCI's dynamic +// backend loader, which dlopen()s a backend shared object that this package +// deliberately does not ship, because the backend is compiled into the same lib. +#include +#include +// `soci.h` does not pull in the version macros; the exact-release assertion +// below needs the header upstream says is the only one guaranteed to change +// with every release. +#include + +#include +#include +#include +#include + +int main() { + // --- the pinned release --- + static_assert(SOCI_VERSION == 400104, "SOCI_VERSION is not 4.1.4"); + assert(std::string(SOCI_LIB_VERSION) == "4_1_4"); + + soci::session sql(soci::sqlite3, ":memory:"); + + // --- the dependency edge, and the engine behind it --- + { + std::string version; + sql << "select sqlite_version()", soci::into(version); + assert(!version.empty() && version[0] == '3'); + assert(version.find('.') != std::string::npos); + } + + // --- DDL, then one parameter round-trip in both directions --- + sql << "create table person (id integer primary key, name varchar(64), weight real)"; + { + int id = 1; + std::string name = "Maciej"; + double weight = 70.5; + sql << "insert into person (id, name, weight) values (:id, :name, :weight)", + soci::use(id), soci::use(name), soci::use(weight); + + // ... read back into DIFFERENT variables, so nothing can be a no-op + std::string got_name; + double got_weight = 0.0; + sql << "select name, weight from person where id = 1", + soci::into(got_name), soci::into(got_weight); + assert(got_name == name); + assert(got_weight == weight); + } + + // --- one prepared statement, executed repeatedly with rebound variables --- + { + int id = 0; + std::string name; + double weight = 0.0; + soci::statement ins = (sql.prepare + << "insert into person (id, name, weight) values (:id, :name, :weight)", + soci::use(id), soci::use(name), soci::use(weight)); + + const char* names[] = {"Vadim", "David"}; + for (int i = 0; i < 2; ++i) { + id = i + 2; + name = names[i]; + weight = 60.0 + i; + ins.execute(true); + } + sql << "select count(*) from person", soci::into(id); + assert(id == 3); + } + + // --- iteration over a rowset, with typed column access --- + { + soci::rowset rows = + (sql.prepare << "select id, name, weight from person order by id"); + std::size_t n = 0; + for (const soci::row& r : rows) { + assert(r.size() == 3); + assert(r.get(0) == static_cast(n) + 1); + assert(!r.get(1).empty()); + assert(r.get(2) > 0.0); + ++n; + } + assert(n == 3); + } + + // --- NULL is not "zero": the indicator is what distinguishes them --- + { + sql << "insert into person (id, name, weight) values (4, 'nobody', null)"; + + double weight = 123.0; + soci::indicator ind = soci::i_ok; + sql << "select weight from person where id = 4", soci::into(weight, ind); + assert(ind == soci::i_null); + + sql << "select weight from person where id = 1", soci::into(weight, ind); + assert(ind == soci::i_ok); + assert(weight == 70.5); + } + + // --- transactions: rollback (explicit, and by scope exit) and commit --- + long long count = 0; + sql << "select count(*) from person", soci::into(count); + assert(count == 4); + + { + soci::transaction tr(sql); + sql << "delete from person"; + tr.rollback(); + } + sql << "select count(*) from person", soci::into(count); + assert(count == 4); + + { + soci::transaction tr(sql); + sql << "delete from person"; // deliberately NOT committed + (void)tr; + } + sql << "select count(*) from person", soci::into(count); + assert(count == 4); + + { + soci::transaction tr(sql); + sql << "update person set weight = weight + 1"; + tr.commit(); + } + { + double weight = 0.0; + sql << "select weight from person where id = 1", soci::into(weight); + assert(weight == 71.5); + } + + // --- the error path: the backend's own type, code and category --- + { + bool threw = false; + try { + sql << "select * from no_such_table"; + } catch (const soci::sqlite3_soci_error& e) { + threw = true; + assert(e.get_backend_name() == "sqlite3"); + assert(e.get_backend_error_code() != 0); + assert(std::string(e.what()).find("no_such_table") != std::string::npos); + } + assert(threw); + } + + // --- and a constraint violation classified as one, not as a generic error --- + { + bool threw = false; + try { + sql << "insert into person (id, name) values (1, 'duplicate key')"; + } catch (const soci::sqlite3_soci_error& e) { + threw = true; + assert(e.get_error_category() == soci::soci_error::constraint_violation); + } + assert(threw); + } + + // The whole file asserts; this last one is deliberately NOT an assert. The + // build could in principle be configured with NDEBUG, in which case every + // assert() above evaporates and a broken package would still exit 0 -- so + // the one check that makes the exit code itself meaningful is written out + // longhand. + sql << "select count(*) from person", soci::into(count); + return count == 4 ? 0 : 1; +}