ADO 到了 Linux,学会了 ODBC
← 目录
作者: 一了 <
[email protected]
> 日期: 2026-11-06 一句话总结: 八笔提交,八百四十四行,Linux 上的 ADO 多了一条 unixODBC 的路。
# ADO 到了 Linux,学会了 ODBC Windows 上的 `lib-ado` 一直是同一副面孔:`ADODB.Connection` 一开,SQL Server、Access,谁装了驱动谁就能连。Linux 上的 `lib-ado-unx` 没这个福气,这些年它只认识内嵌的 SQLite。要接 MySQL 就得再写一个后端,一个数据库一份代码,写完还得各配一套测试。 11 月 6 日中午,上游连着八笔提交(`4db5f7d..350cdba`),把 unixODBC 后端放了进来。新增的 `lib-ado-odbc-unx.fun` 现在八百四十四行,测试两百零九行。提交时间从 12:18 到 12:40,二十二分钟,看着像一气呵成,实际是先立大梁,再一根一根补钉子。 ## 一、连接串就是岔路口 选路不复杂,看连接串: ```fun var db = ADO('odbc:DSN=prod'); var db = ADO('ODBC;DRIVER=MySQL;DATABASE=test'); var db = ADO('DRIVER=PostgreSQL;DATABASE=test'); ``` 带 `odbc:` 或 `ODBC;` 前缀,串里含 `DRIVER=`、`DSN=` 的,走 ODBC;其余照旧走内嵌 SQLite。`mydb.sqlite`、`:memory:`、`Data Source=...` 都不用改,原来怎么用现在还怎么用。 这一步是刻意的。Linux 上不是每台机器都装了 unixODBC,为一个可选能力加一个硬依赖,不划算。所以 `OdbcApi()` 是惰性的:模块加载时只声明一张空表,第一次真正连库才 `DyLib('libodbc.so.2')`,把二十个函数指针一次取回来。没装 libodbc 的机器,加载 `lib-ado-unx` 不会出事,用 SQLite 也不会出事。 (吐槽:库名里带个 `unx`,等于先承认自己不打算假装跨平台。) ## 二、参数不自己转 ODBC 的参数绑定有个好处:C 类型和 SQL 类型分开填,转换交给驱动。 Fun 的 ORM 内部用 `DataType` 0 到 12 记类型,后端照着映射:0 是 SMALLINT,1 是 INTEGER,2 是 BIGINT,3 是 REAL,4 是 DOUBLE,5 是 NUMERIC,6 是 BIT,7/8/9 都是 VARCHAR,10 是 LONGVARCHAR,11 是 VARBINARY,12 是 LONGVARBINARY。C 这一侧除了二进制,一律 `SQL_C_CHAR`,连布尔和数字也先转成字符串,让驱动去认。 为什么不自己编码 double:省事。IEEE-754 的字节序、宽度、驱动自己的转换规则,全交给驱动,少一处自己写错的机会。代价是多一次字符串分配,在数据库这条路上,这点开销不算什么。严格如 PostgreSQL,也认这种带类型的参数写法。 几个细节: - 布尔发 `'1'` 和 `'0'`,不发 `true`、`false`; - NULL 走 indicator 的 `SQL_NULL_DATA`,空串则给一个空格的非空指针。Fun 里 `0` 和 `''` 都贴过 NULL 的边,这里再小心一次; - NUMERIC 按字面量里小数点的位置算 scale,`-123.456` 就是 3; - 大文本和大二进制(`dt` 10、12)绑 `LONGVARCHAR`、`LONGVARBINARY`,别让驱动按短字段截断。 结果反过来读:整数用 `SQL_C_SBIGINT`,`9007199254740993`(2^53+1)从 SQLite 写进去再读回来,一位不差;浮点和文本按字符串读回,再 `toNum()`;二进制走 `SQL_C_BINARY`。大字段不假设一次能取完,4096 字节一块循环,直到 `SQL_SUCCESS`。测试里塞了 2000 个「中」(6000 字节)和 5000 字节的 BLOB,都是跨块读回来的。 ## 三、一条 SQL 里可能有两句 `SQLExecDirect` 一次只认一条语句。ORM 不这么想,它要的是: ```sql INSERT INTO t(name,n) VALUES('carol',1); SELECT last_insert_rowid() AS autoId ``` 于是得自己切。切分器认单引号、双引号、反引号里的内容,认 `--` 行注释和 `/* */` 块注释,只有露在外面的 `;` 才算分隔。切完第一条带参数,后面的不带,这个 `first` 标志不能省。 顺手加了个预检:数一数第一段里引号外的 `?`,和传入的参数个数对不上就先报错。不预检的话,驱动会报一句参数个数不对,离真正出错的 SQL 可能隔着半个 ORM。(吐槽:让驱动报错,等于让物业告诉你家里哪根管子漏了。) ## 四、元数据比数据难伺候 `OpenSchema` 要四样东西:表、列、主键、外键。ODBC 这边对应 `SQLTables`、`SQLColumns`、`SQLPrimaryKeys`、`SQLForeignKeys`,结果再映射回 ADO 的列名,比如 `TABLE_SCHEMA`、`TABLE_NAME`、`COLUMN_NAME`、`DATA_TYPE`、`Column_Flags`。 麻烦在各家不一样: - PostgreSQL 和 SQL Server 给 `TABLE_SCHEM`,MySQL 只给 `TABLE_CAT`。取 schema,没有就取 catalog,再没有才用驱动缺省值。SQLite 的缺省 schema 是 `main`。 - 复合主键、复合外键,ODBC 一行一列地回,拼起来才是完整的键。 - 主键标记的索引键要带上 schema,不然两个 schema 里同名的表会串味。 - unixODBC 不接全 NULL 的 `SQLForeignKeys`(HY009),只能逐表问。 - SQLite 的内部表按 `sqlite_` 前缀排掉,注意是七个字符带下划线,不是 `sqlite` 开头就算。 类型映射也补了一轮:`BIT` 回 `adBoolean`,`BIGINT` 回 `adBigInt`,`NUMERIC`、`DECIMAL` 回 `adNumeric`,`LONGVARBINARY` 回 `adLongVarBinary`,`GUID` 回 `adGUID`。这些数字在 ADO 里都有固定值,照抄就行,抄错一个,上层的 ORM 就把字段当成了别的种类。 ## 五、栽的五个跟头 第一笔提交只是把架子搭起来,后面七笔全是撞出来的。 一,事务没开连接就落到 SQLite 路径。`OrmSave` 先 `Begin()` 再 `Execute()`,一个新的 ODBC 连接还没 `Open()`,`Begin` 直接去拿 SQLite 句柄,拿到的是 nil。修法是 `Begin`、`Commit`、`Rollback` 先看 `Closed()`,该开就开。 二,`Init()` 只占了个位。Windows 那边的 `Init()` 是有事的,Linux 这边原来空着;可 ORM RPC 在生成 SQL 之前会调它,指望它把标识符引号装好。MySQL 要反引号,默认还是 `"`,生成出来的 SQL 一到驱动就报错。现在 `Init()` 里直接 `Open()`,把 `SQL_IDENTIFIER_QUOTE_CHAR` 取回来。 三,`SQLGetInfo` 说「不支持」的方式是返回一个空格。代码里写 `if quote = '' then quote = '"'`,空格不等于空串,默认值永远轮不上。三个 `SQLGetInfo` 的结果都得先 trim。(吐槽:一个空格骗过一行判断,这种事在 C 接口里天天有。) 四,零行的 UPDATE、DELETE。ODBC 3.x 把它报成 `SQL_NO_DATA`(100),不是错误,是一条成功但影响零行的语句。原来的代码只认 `SQL_SUCCESS`,于是 ORM 删一条不存在的记录就抛异常。现在 `SQL_NO_DATA` 和成功同等看待,行数记 0。 五,连接串前缀。ADO 风格的 `ODBC;DSN=...` 得先剥掉前缀再交给驱动,不剥的话驱动会把 `ODBC;` 当成键名。这个错法很隐蔽,报错信息里只看得到找不到数据源。 ## 六、验证到了哪一步 新增的 `run-ado-odbc.sh` 在没装 unixODBC 或没有驱动时自动 SKIP,CI 的 Linux job 里则装上 `unixodbc` 和 `libsqliteodbc`,把这条路径真跑一遍。Linux 回归从 126/126 变成 127/127;loadcheck 还是 73/79,没被带坏。 测试覆盖的东西比提交信息里写的多:多语句加自增、参数绑定、零行增删改、事务提交与回滚、跨 4096 的大文本和 BLOB、空串与 NULL 的区别、2^53 以上的 Int64、布尔、NUMERIC 的小数位、复合主键与外键、ORM 的一整套增删改查、失败后的整批回滚、懒打开的 `Begin()`、`Init()` 装引号、`ODBC;` 前缀。 不过得说清楚:跑的是 SQLite 的 ODBC 驱动,省了一台服务器,测试里也留着 `last_insert_rowid()` 和 `sqlite_master` 这种 SQLite 味的东西。MySQL、PostgreSQL、SQL Server 都还没进 CI,方言、类型、隔离级别的差异,得拿真库一处一处撞。 ## 结语 一条连接串,Windows 走 COM 的 ADO,Linux 走 unixODBC,上层的 ORM 不用知道自己在哪。这只是「能连上」,离「都测过」还有距离。但门开在了该开的地方,后面的数据库驱动可以自己走进来。 这八百多行里,最贵的不是那二十个函数指针,是那五个跟头。