简介:这份 SQLite3 VC6.0 资源面向仍在 Visual C++ 6.0 环境下开发或维护旧项目的开发者,提供从官方源码编译得到的 Windows 平台 SQLite3 库及相关工程材料,可用于数据库引擎学习、源码查阅、自定义编译与现有应用调试。压缩包为 7z 格式,体积约 5.4MB,文件总数与类型明细上游暂未提供;从描述可知其中包含 VC6.0 工作空间、工程配置、SQLite3 文档、静态库/动态库测试示例以及编译产物,便于直接链接或二次构建。已有 228 人浏览学习。对于需要理解 SQLite3 内部实现、验证 API 用法或在老环境中排查链接问题的团队,这份材料能提供可运行的工程入口、测试参考和文档线索,并保留源码级研究空间,适合中级开发者按需取用。
1. 老工程要接 sqlite3,VC6.0 这道坎得先迈过去
去年接手一个维护了十几年的 MFC 老程序,需求很朴素:把本地的配置、流水日志和几张业务小表落盘,装个数据库服务不现实,评估下来 sqlite3 是唯一合理的选择——单文件、无服务端、静态链接进去就能跑。麻烦在于开发环境还是 VC6.0,编译器停在 C89 那一代,而手上的 sqlite3 amalgamation 是拿新编译器喂出来的。把 sqlite3.c 拖进工程点一下编译,从第一个头文件开始一路飘红,这是必然的,不是运气不好。这篇笔记就按实际动手的顺序拆:文件怎么摆、编译报错怎么改、接口怎么封、中文怎么塞进去、哪些地方最容易加班,适合手里有老工程又不想换工具链的人。
2. 拆包:amalgamation 的三个文件,哪个能进 VC6 工程
先别急着建工程。从 sqlite3 官方渠道拿到的 amalgamation 版本解压出来,通常就那么几个文件,但每个的用途天差地别,塞错一个后面全是无用功。
2.1 sqlite3.c、sqlite3.h、sqlite3ext.h 各管什么
| 文件 | 作用 | 要不要进工程 |
|---|---|---|
| sqlite3.c | 内核全部实现压成的一个编译单元 | 要,且只要这一个 .c |
| sqlite3.h | 公共 API 声明,自带extern "C"守卫 | 要,业务工程只引这个 |
| sqlite3ext.h | 给可加载扩展模块用的虚表头 | 不要,普通增删改查用不上 |
| shell.c | 命令行工具,内含main | 不要,会和你的main打架 |
sqlite3.c 是把几百个源文件合并出来的单编译单元,好处是没有头文件依赖地狱,坏处是它一个文件就顶别人一个模块的量。VC6 的编译器在处理这种体量的单一编译单元时,本身就容易出状况,这一点第 3 章会细说。sqlite3ext.h 是扩展机制的头,只有在写虚表扩展、做自定义函数模块时才需要,普通业务代码引了反而多一层编译负担。shell.c 是那个能敲.tables的命令行程序,它有自己的main,一旦混进静态库里链接阶段就会报重复定义,这是新手最常见的一个开局失误。
我的习惯是建一个独立的静态库工程专门编 sqlite3.c,业务工程只引头文件和 .lib。这么做有两个好处:一是编译 sqlite3.c 那几分钟只在库工程里发生,业务代码改动不用重编它;二是宏定义(线程模型、裁剪开关)集中在一个工程的设置里,不会出现「主工程改了宏但库没重编」这种查半天的问题。
2.2 在 VC6 里建静态库工程:目录和设置
目录建议这样摆,业务工程和第三方库彻底分开:
project_root/ ├── app/ 业务工程(MFC 或 Console) │ └── app.dsp ├── third_party/ │ └── sqlite3/ │ ├── sqlite3.c │ ├── sqlite3.h │ ├── sqlite3lib.dsp │ └── lib/ <- 库工程输出目录 └── data/ 运行期生成的 .db 文件建工程的步骤按这个顺序走,中间别跳:
- File → New → Projects → Win32 Static Library,工程名 sqlite3lib,路径指到
third_party/sqlite3/。 - 把 sqlite3.c 加入工程,sqlite3.h 不用加(头文件不需要编译)。
- Project → Settings → C/C++ → Precompiled Headers,选Not using precompiled headers。这一步必须做,否则 VC6 会去找 stdafx.h,直接 C1083。
- 同一个设置页的 Code Generation,Runtime Library 选和业务工程完全一致的那一项。业务工程是 Debug 就用 Debug Multithreaded DLL,是 Release 就用 Multithreaded DLL。两边不一致,链接期必炸,这是第 5 章要展开的坑。
- Project → Settings → Link → Output file name,改成
lib/sqlite3_d.lib之类,把输出统一收进 lib 目录。 - 在业务工程里,Project → Settings → C/C++ → Preprocessor → Additional include directories 加上
..\third_party\sqlite3,Link 页的 Object/library modules 里加上..\third_party\sqlite3\lib\sqlite3_d.lib。
第 4 步那个运行库一致的要求,很多人觉得是老生常谈,但在 VC6 上它真的会以LNK2005: _malloc already defined这种面目出现,而且报错指向的是一堆 CRT 内部符号,完全看不出是自己库的设置问题。
2.3 编译开关:哪些宏现在就该定下来
在库工程的 Preprocessor definitions 里加下面这几个,一次定好,后面少折腾:
| 宏 | 取值 | 影响 | 什么时候别这么设 |
|---|---|---|---|
SQLITE_THREADSAFE | 0 | 去掉互斥锁相关代码,体积和调用开销都降 | 有多个线程访问同一个sqlite3*连接时必须设 1 |
SQLITE_OMIT_LOAD_EXTENSION | 定义即可 | 去掉动态加载扩展的能力 | 需要.load扩展时保留 |
SQLITE_OMIT_DEPRECATED | 定义即可 | 去掉一批老接口 | 老代码里还在用sqlite3_exec之外的废弃 API 就别加 |
SQLITE_ENABLE_COLUMN_METADATA | 定义即可 | 能用sqlite3_column_table_name那组函数 | 不需要从结果集反查表名就别开,会占体积 |
SQLITE_TEMP_STORE | 2 | 临时表优先放内存 | 临时数据量大到吃内存的场景要往下调 |
SQLITE_DEFAULT_MEMSTATUS | 0 | 关掉内存统计,省一点全局开销 | 需要调sqlite3_memory_used做诊断时保留 |
SQLITE_THREADSAFE=0是最值得先想清楚的一个。VC6 时代的老程序很多是单线程界面线程 + 一个工作线程的结构,如果只有工作线程碰数据库,设 0 完全没问题;但只要有任何一个界面回调里也去查了一下表,就必须设 1,否则出问题是概率事件,而且崩的位置和原因对不上,排查成本极高。
改完宏一定要Build → Rebuild All。VC6 的依赖跟踪非常弱,头文件和宏的改动经常不触发重编,你以为改了其实没改,这种「黑匣子」式的困惑我踩过不止一次。
3. 编译适配:VC6 编译器啃 sqlite3.c 的六处改动
工程建好之后点编译,才是真正开始干活的地方。这一章的报错都是我在 VC6 上按这个顺序遭遇的,按顺序改能省时间。
3.1 先认清 VC6 的 C 语言边界
VC6 自带编译器大致是 C89 加上一部分微软扩展,它不具备 C99 的这些东西:
- 没有
long long,64 位整型只能写__int64; - 没有
stdint.h、inttypes.h、stdbool.h这三个标准头; - 没有
inline关键字(C 模式下),要用__inline; - 不支持可变参数宏
#define F(...) __VA_ARGS__; - 变量声明仍然必须在语句块开头,C99 那种「用到再声明」不行。
而 amalgamation 版本是跟着主流编译器走的,近些年的版本里已经默认假定stdint.h存在、long long可用。所以第一次编译的报错几乎必然集中在头文件和整型上。
一个提醒:VC6 其实是支持//行注释的,这一点和很多人的印象相反,所以不要看到报错就先去批量替换注释风格,那是白费功夫。
3.2 类型与头文件的替换清单
先在 sqlite3.c 文件最开头(第一个#include之前)插一段兼容定义:
/* sqlite3_vc6_compat.h 的内容,直接贴进 sqlite3.c 顶部 */ #if defined(_MSC_VER) && (_MSC_VER < 1300) /* VC6 的 _MSC_VER 是 1200 */ typedef signed char int8_t; typedef unsigned char uint8_t; typedef short int16_t; typedef unsigned short uint16_t; typedef int int32_t; typedef unsigned int uint32_t; typedef __int64 int64_t; typedef unsigned __int64 uint64_t; #define SQLITE_VC6_BUILD 1 #endif这段的作用就是把 C99 那套定长整型名字补上,让后面的代码能用int64_t这种写法。__int64是微软自己的 64 位整型写法,VC6 认它不认long long,两者在内存布局上是一回事。
然后是全文件的批量替换,一共三类:
long long→__int64;unsigned long long→unsigned __int64;#include <stdint.h>、#include <inttypes.h>这两行注释掉(兼容定义已经放在前面了)。
替换用编辑器的全局替换功能,注意勾选「区分大小写」和「全字匹配」,否则会把long long_var这种变量名误伤。
还有一种情况是inline报错。如果看到C2054: expected '(' to follow 'inline',说明这个函数在 C 模式下被声明成了inline。改法不是一个个替换,而是在文件顶部加一行:
#if defined(_MSC_VER) && (_MSC_VER < 1300) #define inline __inline #endif放在所有代码之前,一行就覆盖全文件。
3.3 编译错误逐条对照
改完上面这些再编,剩下的报错会少很多,剩下的一般是这几种:
| 报错原文 | 真实原因 | 处理方式 |
|---|---|---|
fatal error C1083: Cannot open include file: 'stdint.h' | 该版本 C 标准头不存在 | 注释掉该#include,用 3.2 的兼容块 |
error C2632: 'long' followed by 'long' is illegal | 用了long long | 换成__int64 |
error C2065: 'int64_t' : undeclared identifier | 兼容块没放对位置 | 确认它在第一个#include之前 |
error C2054: expected '(' to follow 'inline' | C 模式下的inline | 顶部加#define inline __inline |
fatal error C1002: compiler is out of heap space in pass 2 | 单编译单元太大,VC6 编译器自己先撑不住 | 见下面的处理顺序 |
fatal error C1060: compiler is out of heap space | 同上,另一种表现形式 | 同上 |
C1002 / C1060 这两个不是代码写错了,是 32 位编译器处理超大翻译单元时的资源问题。我一般按这个顺序试:
- Project → Settings → C/C++ → Generate browse info 取消勾选。这个选项会生成 .bsc 浏览数据库,非常吃内存,老工程里通常默认开着。
- Link 页里的 Generate debug info 也收一收,Release 下本来就该关。
- 优化级别从 /O2 降到 /O1,或者干脆 Debug 配置下关优化先编通。
- 确保工程不在网络盘或很慢的磁盘上,VC6 编译中间文件时对 IO 很敏感。
这四条走完,绝大多数机器都能编过。真正的血泪经验是第 1 条——当年为了一个 C1002 折腾了半天,最后发现只是浏览器信息生成开着。
编过之后先别急着写业务代码,写个最小的main验证一下链接:
#include "sqlite3.h" #include <stdio.h> int main() { /* 只取版本号,不建库不建表,验证符号能链上 */ printf("sqlite version: %s\n", sqlite3_libversion()); return 0; }能打印出版本字符串,说明头文件路径、静态库路径、运行库设置三件事都是对的,可以往下走了。如果这一步就报LNK2001: unresolved external symbol _sqlite3_libversion,问题一定在链接配置上,不要怀疑代码。
4. 接口封装:打开、查询、事务的最小可用层
sqlite3 的 C 接口本身不难,难的是在老工程里用得稳。这一层我一般只封四个函数:打开、执行语句、参数化写入、参数化查询,够覆盖九成场景,再往上封 ORM 在老项目里是给自己找麻烦。
4.1 打开数据库与 PRAGMA 基线
#include "sqlite3.h" #include <stdio.h> /* 执行一条不需要结果的语句,顺带把错误吃掉并打印 */ static void ExecSilent(sqlite3* db, const char* sql) { char* err = NULL; if (sqlite3_exec(db, sql, NULL, NULL, &err) != SQLITE_OK && err != NULL) { printf("[pragma] %s failed: %s\n", sql, err); sqlite3_free(err); /* 必须用 sqlite3_free,不能用 free */ } } sqlite3* OpenDb(const char* file) { sqlite3* db = NULL; /* 第三个参数不需要,sqlite3_open 只认两个参数 */ if (sqlite3_open(file, &db) != SQLITE_OK) { if (db != NULL) { sqlite3_close(db); /* open 失败也可能分配了句柄,必须关 */ } return NULL; } /* 拿不到锁时最多重试 3000 毫秒,而不是立刻返回 SQLITE_BUSY */ sqlite3_busy_timeout(db, 3000); ExecSilent(db, "PRAGMA synchronous = NORMAL;"); ExecSilent(db, "PRAGMA cache_size = 2000;"); ExecSilent(db, "PRAGMA temp_store = MEMORY;"); return db; }逻辑上只有三件事:打开、设超时、设参数。sqlite3_open失败时句柄不一定是空的,很多人只写return NULL不关句柄,跑久了句柄泄漏,最后表现成「第一次能开,第二次打不开数据库」,查起来很绕。
参数说明:
busy_timeout的单位是毫秒。设成 0 意味着只要锁被占用就立刻返回SQLITE_BUSY,业务代码必须自己处理重试;设成 3000 相当于把重试逻辑交给 sqlite3 内部。单进程单连接场景基本不会撞锁,但只要有第二个进程(比如用命令行工具开着看数据)就会有。synchronous = NORMAL是持久性和速度的折中。默认 FULL 每次提交都强制落盘,小数据量业务感觉不明显,批量写的时候差距就出来了。如果这批数据丢了不可接受(比如财务流水),老老实实留 FULL。cache_size的单位是页,2000 页按默认页大小算大概是几 MB 量级,老机器上不要设太大,MFC 程序本身内存占用就不低。temp_store = MEMORY让临时表和排序中间结果放内存。数据量小的时候能明显减少磁盘临时文件,数据量大的时候反而可能吃爆内存,按实际数据规模取舍。
4.2 prepare / bind / step 的四步封装
带参数写入是最常用的,也最容易写错。标准写法是 prepare 一次、循环 bind/step/reset:
/* 批量插入:SQL 只 prepare 一次,循环复用 */ int InsertUsers(sqlite3* db, const char* const* names, const int* ages, int n) { sqlite3_stmt* stmt = NULL; const char* sql = "INSERT INTO t_user(name, age) VALUES(?, ?)"; int i, rc; /* 参数 -1 表示让 sqlite3 自己数 SQL 字符串长度 */ rc = sqlite3_prepare(db, sql, -1, &stmt, NULL); if (rc != SQLITE_OK) { return rc; } for (i = 0; i < n; i++) { /* 第 5 个参数 SQLITE_TRANSIENT 表示让 sqlite 复制一份字符串 */ sqlite3_bind_text(stmt, 1, names[i], -1, SQLITE_TRANSIENT); sqlite3_bind_int (stmt, 2, ages[i]); rc = sqlite3_step(stmt); if (rc != SQLITE_DONE) { sqlite3_finalize(stmt); return rc; } sqlite3_reset(stmt); /* 复位到可再次绑定的状态 */ sqlite3_clear_bindings(stmt); /* 清掉上一轮的值,避免残留 */ } sqlite3_finalize(stmt); /* 一定要 finalize,否则语句对象泄漏 */ return SQLITE_OK; }几个关键点必须说清楚:
sqlite3_bind_text的第 5 个参数是最容易翻车的地方。传SQLITE_TRANSIENT,sqlite 会把字符串复制一份存到自己管理的内存里,你后面怎么改原缓冲区都不影响;传SQLITE_STATIC,它只记个指针,要求这块内存在step完成之前一直有效。如果你的names[i]来自一个循环里的局部char buf[64],用SQLITE_STATIC就会在下一轮循环被覆盖,写进库的数据看起来「随机错乱」,而且因为地址没变,编译器也不会报任何东西。这种问题我见过不止一次,统一用SQLITE_TRANSIENT是最省心的选择,多一次拷贝的代价在这种老业务里根本不值一提。
sqlite3_reset和sqlite3_clear_bindings的作用不一样。reset把语句恢复到刚 prepare 完、可以重新 bind 的状态,但已经绑定的值还在;clear_bindings才是清空。只 reset 不清 bind,如果某一轮某个参数忘了绑,它会沿用上一轮的值,表现成「数据串行」——这属于典型的玄学 bug,因为代码看起来一点问题都没有。
sqlite3_finalize必须调用,即使是出错提前返回的分支。stmt 是有状态的资源,不释放的话语句对象会一直挂着,最终耗尽内存。
查询那边同理,sqlite3_step返回SQLITE_ROW表示有一行数据,SQLITE_DONE表示结束了。取列的时候要注意一件事:sqlite3_column_text返回的指针在reset或finalize之后就会失效,如果业务层要长期保存这个值,必须自己strcpy到自己的缓冲区里,不能只存指针。
4.3 事务与批量写入的吞吐差
批量写不包事务,性能会难看到让人怀疑 sqlite3 是不是有问题。原因是每一条INSERT都独立提交一次,每次提交都要走一次同步落盘动作,行数越多落盘次数越多。
int ImportBatch(sqlite3* db, const char* const* names, const int* ages, int n) { int rc; /* BEGIN IMMEDIATE 立刻拿写锁,避免中途升级锁失败 */ rc = sqlite3_exec(db, "BEGIN IMMEDIATE;", NULL, NULL, NULL); if (rc != SQLITE_OK) { return rc; } rc = InsertUsers(db, names, ages, n); if (rc == SQLITE_OK) { rc = sqlite3_exec(db, "COMMIT;", NULL, NULL, NULL); } else { /* 任何一步失败都回滚,别留半截数据 */ sqlite3_exec(db, "ROLLBACK;", NULL, NULL, NULL); } return rc; }事务包好之后,几万行数据的感觉完全是另一个量级,因为整批数据只有一次提交、一次落盘。
BEGIN有三种形式,BEGIN、BEGIN IMMEDIATE、BEGIN EXCLUSIVE,区别在于拿锁的时机。默认的BEGIN是延迟拿锁,直到第一条写语句才升级成写锁,多进程场景下可能在升级时失败;IMMEDIATE一开始就拿写锁,失败得早、也更好处理。批量导入这种一上来就要写全部数据的场景,用IMMEDIATE更合适。
还有一个细节:事务没提交就关闭连接,数据会丢。这不是 sqlite3 的问题,是事务语义本身如此。老代码里如果在异常分支里直接sqlite3_close而不 ROLLBACK,看起来是回滚了(未提交的东西本来就不生效),但如果逻辑写在循环里、中途 return 了,前面的数据也没提交成功,表现成「明明跑了但数据没进去」。
4.4 中文与编码的处理
sqlite3 内部一律 UTF-8。VC6 的工程默认是 GBK(Windows 代码页 936),直接把 GBK 字符串bind_text进去,读出来还能对得上,因为存进去和取出来用的是同一套错误编码,看起来没问题。真正的麻烦出现在三种情况:用别的工具打开这个库、库里混了别的来源的数据、或者要做全文比对。这时候就会出现典型的乱码。
稳妥的做法是在写入前把 GBK 转成 UTF-8,读取后再转回来。VC6 上有MultiByteToWideChar/WideCharToMultiByte,走 UTF-16 中转:
/* 代码页:936 = GBK,65001 = UTF-8 */ static void Convert(const char* from, char* to, int toSize, UINT fromCp, UINT toCp) { /* 先转宽字符,再转目标编码,VC6 没有直接的 GBK<->UTF8 函数 */ WCHAR wbuf[1024]; int wlen = MultiByteToWideChar(fromCp, 0, from, -1, wbuf, 1024); if (wlen > 0) { WideCharToMultiByte(toCp, 0, wbuf, -1, to, toSize, NULL, NULL); } }调用时机很明确:所有sqlite3_bind_text之前调一次 GBK→UTF-8,所有sqlite3_column_text之后调一次 UTF-8→GBK。库里的数据始终是 UTF-8,对外接口始终是 GBK,中间这层转换是固定的,不要在不同地方混着写,否则过几个月自己都分不清哪个字段是什么编码。
注意MultiByteToWideChar的缓冲区长度要按最坏情况预估。GBK 中文是两个字节,UTF-8 中文通常是三个,转换后长度会变长,缓冲区不够会截断,截断点还可能落在多字节字符中间,产生半个字符。
5. 避坑排查:VC6 接 sqlite3 最常翻车的四个现场
这一章按我实际遇到的顺序排,每个都写清楚现象、原因和解决,遇到对得上的直接照做。
5.1 编译期:改完了还是报旧错误
现象:按第 3 章把long long全替换了,重新编译还是报C2632: 'long' followed by 'long' is illegal,而且报错行号指向的代码看着已经是__int64了。
原因:VC6 的增量编译依赖跟踪非常不可靠,尤其是对宏定义和头文件的改动。它经常拿旧的 .obj 直接链接,你在编辑器里改的内容根本没进编译流程。另一个可能是工程里存在多份 sqlite3.c 的副本,你改的是 A,实际编译的是 B。
解决:Build → Rebuild All,无条件全量重编一次。如果还报,用 Windows 搜索确认工程目录下有几个 sqlite3.c,把多余的删掉或从工程里移除。这件事之后我养成了一个习惯:任何涉及宏和头文件的改动,一律 Rebuild All,不再相信增量编译。
5.2 链接期:LNK2005 与 unresolved external symbol
现象:库编出来了,业务工程链的时候报一堆LNK2005: _malloc already defined in LIBCMTD.lib,或者LNK2001: unresolved external symbol _sqlite3_open。
原因:两种错误根因不同。LNK2005是运行库版本不一致,库工程用了静态 CRT(/MT 系列),业务工程用了动态 CRT(/MD 系列),两边都把 CRT 的符号定义了一遍,链接器不知道该用哪个。LNK2001通常是 .lib 路径写错、或者压根没加进 Link 页,符号根本没找到。
解决:先看两边工程 Project → Settings → C/C++ → Code Generation 里的 Runtime Library 是不是同一项,不一致就改成一致。然后 Clean 掉两边的中间文件重新全编。LNK2001的话,在 Link 页确认 Object/library modules 里有那个 .lib 的完整相对路径,路径里不要有空格和中文。还有一种隐蔽情况:业务工程是 Unicode 配置、库工程是 ANSI,这种不会直接报链接错,但字符串传进去会是怪值,属于后面第 5.4 条的范围。
5.3 运行期:database is locked 反复出现
现象:程序跑一段时间后,写操作开始返回SQLITE_BUSY,错误信息是database is locked,重启程序又好了。
原因:常见有三种。一是没设busy_timeout,撞上锁直接失败;二是有一段代码开事务后没提交也没回滚,直接把连接留着,写锁一直不释放;三是有另一个进程(比如你开着命令行工具在看数据)持有锁。
解决:首先在OpenDb里加sqlite3_busy_timeout,给一个合理的重试窗口。然后全局搜一遍BEGIN,确认每个BEGIN都有配对的COMMIT或ROLLBACK,重点是异常分支——很多老代码在 catch 块里只打了日志就返回了,事务挂在那里。最后确认有没有第二个进程在访问同一个 .db 文件,开发期这种情况特别常见。
一个排查技巧:把sqlite3_errcode和sqlite3_errmsg的返回值在出错时打出来,SQLITE_BUSY和SQLITE_LOCKED是两回事,前者是文件锁冲突,后者是同一连接内部的语句冲突,处理方式不一样。只打一句「数据库错误」等于没打。
5.4 数据层:中文乱码与 64 位整型的格式化
现象:中文写进去,用别的工具打开是乱码;或者 64 位 ID 打出来是负数、是 0、或者是一串完全不对的数字。
原因:中文是编码问题,见 4.4。整型的问题出在格式化字符串上——VC6 的 CRT 不认%lld,而sqlite3_column_int64返回的是__int64,用%lld去格式化,结果是未定义的,常见表现就是打出 0 或者负数。
解决:中文统一走 4.4 那层转换,不要图省事。整型格式化用微软自己的写法%I64d:
sqlite3_int64 id = sqlite3_column_int64(stmt, 0); char buf[32]; _stprintf(buf, _T("%I64d"), id); /* VC6 上用 %I64d,不是 %lld */如果是写日志,printf系列同理,%I64d也能用在sprintf上。这一条看起来很小,但 64 位 ID 在业务表里到处都是,用错了会让整个排查方向跑偏——你会以为是数据写错了,其实是打印错了。
6. 交付前的自检:一个三十行的冒烟程序
库编通了、接口封好了、数据也能读了,最后一步是给自己留一个能在任何机器上快速判断「这个 .db 文件是不是好的」的小工具。老项目交付现场经常没法装调试器,能跑一个控制台程序把问题定位到「文件坏了」还是「代码坏了」,能省掉大量来回沟通。
6.1 冒烟自检程序
#include "sqlite3.h" #include <stdio.h> /* exec 的回调:每行结果调一次,这里只打印第一列 */ static int RowCb(void* unused, int argc, char** argv, char** colName) { if (argc > 0 && argv[0] != NULL) { printf(" %s\n", argv[0]); } return 0; /* 返回非 0 会中断查询 */ } int SmokeTest(const char* file) { sqlite3* db = NULL; char* err = NULL; int rc; if (sqlite3_open(file, &db) != SQLITE_OK) { printf("open failed: %s\n", sqlite3_errmsg(db)); if (db) sqlite3_close(db); return -1; } /* 第一关:文件结构完整性检查,正常只会输出一行 ok */ printf("[1] integrity_check:\n"); rc = sqlite3_exec(db, "PRAGMA integrity_check;", RowCb, NULL, &err); if (rc != SQLITE_OK) { printf(" failed: %s\n", err ? err : "unknown"); sqlite3_free(err); sqlite3_close(db); return -2; } /* 第二关:列出所有表,确认 schema 符合预期 */ printf("[2] tables:\n"); sqlite3_exec(db, "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name;", RowCb, NULL, &err); /* 第三关:统计一张关键业务表的行数,确认数据真的在 */ printf("[3] row count:\n"); sqlite3_exec(db, "SELECT COUNT(*) FROM t_user;", RowCb, NULL, &err); sqlite3_close(db); return 0; }三关的定位能力是递进的。第一关PRAGMA integrity_check检查的是文件本身,如果它返回的不是ok,说明这个 .db 文件至少在页结构层面有问题,后面的代码怎么改都没用,得先想办法恢复数据。第二关查sqlite_master,能看到所有表和索引,如果客户现场说「表不存在」,跑一下就知道是库连错了路径还是真没建表。第三关查行数,确认数据在不在,避免把「查不到」和「没写进去」混为一谈。
回调函数返回非 0 会中断当前查询,这一点在只想看前几行的时候有用,但
本文还有配套的精品资源,点击获取