JSON、SQLite3 与笔记服务¶
这一章讲什么: 进程死了数据怎么办。三段台阶: 第一段把 JSON 当数据质量练习(逐行校验、好账坏账分计); 第二段直插 SQLite3 的 C API,学会数据库连接的生命周期; 第三段是全书 comptime 主题的第二次高潮——让编译器从结构体定义生成 SQL, 最后包一个零框架 HTTP 笔记服务。
主线例子:popSQL 的编译期合同。
Event{ id: i64, event: []const u8, timestamp: []const u8 }是唯一手写的事实;建表语句、(?, ?, ?) 占位符、三个绑定调用,全部在编译期从它推导。
1. 顶层全景:一条数据的三种形态¶
磁盘上的 NDJSON/JSON →(std.json 解析)→ 内存里的 Event 结构体
│
comptime 从结构体生成:
CREATE TABLE … (i64→INTEGER, []const u8→TEXT NOT NULL)
(?, ?, ?) 占位符
bind_int64 / bind_text 序列
│
SQLite3 (单文件、嵌入式、无服务器进程)
│
HTTP 笔记服务(五个端点,单线程,参数化 SQL)
图说:结构体是唯一事实来源;JSON 解析器、表定义、绑定序列三者「可证明地一致」,
因为它们都派生自同一份声明。[^1]
2. JSON 数据质量:先分账,再转换¶
第一个程序读 NDJSON(每行一个独立 JSON 对象的流式格式),把每条记录从 ISO 8601 时间戳(2024-03-12T21:06:11Z,恰好 20 字符)重排成人可读的 日-月-年 时:分:秒,同时统计有效与无效记录数——数据导入前的质量快检2。
校验函数 isIso8601() 查两层:结构(分隔符 -、T、:、Z 在固定位置)与内容(其余位置全是数字,且月 ≤ 12、时 ≤ 23 等范围合法)3。两层都查不是洁癖:reformat() 按固定下标切片,缺了内容层,XXXX-XX-XXTXX:XX:XXZ 这种「形状对、全是字母」的串会静默切出垃圾4。reformat() 返回前把 event 字段也 dupe 一份——新 Record 两个字段都自有堆内存,调用方清楚该 free 哪几部分;只拷指针会造成所有权骨折5。
管道三段:整文件一次读进内存(线性可预期的 I/O,假设文件装得下堆),标量分隔符逐行切(切片直指原缓冲,零分配),每行 parseFromSlice 成动态 std.json.Value、再尝试映射到 Record——映射失败、校验失败、重排失败,都记进 invalid 计数,fail-fast 保证后续管道只碰符合内部 schema 的数据6。每行解析完 defer val.deinit(),内存占用与行数无关7。实测 25 条样本:Valid 16、Invalid 98。
3. SQLite3 五步生命周期与错误翻译¶
SQLite3 是嵌入式数据库:作为库链接进应用,没有独立的服务器进程,所有数据在一个文件里;简单、可移植、轻量9。Zig 通过 @cImport(@cInclude("sqlite3.h")) 直接用它——macOS 上 Homebrew 与 Apple 各装了一份,编译时要显式指 -I/opt/homebrew/include -L/opt/homebrew/lib -lsqlite3 告诉编译器头文件与库的地图10。
连接、读写的统一骨架五步:
open sqlite3_open(path, &db) 打开或新建;句柄是可空指针
prepare sqlite3_prepare_v2(db, sql, …) 把 SQL 编译成内部字节码
bind sqlite3_bind_text/int(…, 1, …) 值填进 ? 占位符(从 1 数)
step sqlite3_step(stmt) 执行;SELECT 则每次推一行,返回 SQLITE_ROW
finalize sqlite3_finalize(stmt) 语句资源释放
图说:全程 defer 配对——db 关闭前判空(if (db) |db_ptr|),finalize 同样;C 库的错误消息是堆上分配的字符串,用完必须 sqlite3_free11。
checkError() 是两种语言之间的翻译层:SQLite 的 C API 里「成功」不是一个值——SQLITE_OK 是圆满,SQLITE_ROW / SQLITE_DONE 同样是成功的特定状态;除此之外一律视为失败,翻成 Zig 错误联合12。
防注入的分界线:值可以参数化(? 占位符 + bind),表名和列名不能——占位符只对值生效;接受标识符当参数的函数必须在应用层校验或转义,否则就算所有值都安全参数化了,往格式串里直接插标识符照样是 SQL 注入洞13。删除用 sqlite3_changes() 拿到实际删掉的行数——实测连删两次同名记录,第一次 Deleted 4 record(s)、第二次 Deleted 014。
查询的一处桥接细节:sqlite3_column_text() 返回 C 的零结尾指针,std.mem.span() 把它算长变回 Zig 切片——但 span 要求指针非空,这是未定义行为;可空列要先查 sqlite3_column_type() == SQLITE_NULL。示例里安全,只因为两表的 TEXT 列都声明了 NOT NULL15。
4. 主线例子:编译期合同如何让 schema 漂移不可能¶
popSQL 把 JSON 数组灌进 SQLite。朴素做法(解析成动态联合、手工逐字段提取)的坏处在哪?每个列名至少出现在三个地方:结构体定义、JSON 提取、SQL 绑定——一次 schema 改动,三处独立地坏16。comptime 管道的答案是:字段映射的活从执行期搬到编译期,产出的机器代码和为每张表手写专用例程一样直接,源码却只有一份事实17。
三个 comptime 件,全部从 Event 推导18:
| 件 | 机制 | 产物 |
|---|---|---|
sqlType(T) |
comptime switch 类型→字符串 | i64→INTEGER、[]const u8→TEXT NOT NULL;未覆盖类型 @compileError |
ensureTableExists(T) |
@typeInfo(T).@"struct" 取字段元数据,inline for + ++ 拼串 |
完整 CREATE TABLE IF NOT EXISTS events(…),编译期钉死在二进制只读数据段,运行时零分配零格式化;字段名叫 id 的追加 PRIMARY KEY |
insertRecord(T) |
同上,占位符 (?, ?, ?) 与绑定循环 |
@field(record, field.name) 按名取值,编出与手写 record.id 完全相同的指令;bindValue 对 @TypeOf(val) 的 switch 在编译期消支,Event 的发射代码是无分支的三连 bind |
图说:@typeInfo 返回描述任意类型内部结构的联合;结构体的 .@"struct" 变体携带字段名(comptime 字符串)、类型、默认值19。anytype 参数是编译期多态:编译器为每个实参类型生成一份全特化的函数体——「编译期强制的鸭子类型,传进来的类型伺候不了就构建失败」20。连 pk 这种小变量也要显式标注 comptime——inline for 里循环变量虽是 comptime 已知,编译器不会自动提升条件表达式的结果,comptime if 强制编译期求值,++ 才能拼接21。
代价也摆明了:结构体必须编译期全知(schema 不能来自配置文件或启动时的数据库内省);每种字段类型都要显式伺候,缺口是构建错误而不是运行期优雅降级;生成的 SQL 只管主键,不管外键、索引、约束22。买到的是一份保证:JSON 解析器、表定义、绑定序列彼此可证一致,因为都派生自同一声明——schema 漂移是生产环境最常见的 bug 源之一,而这里编译器成了合同的执法者,成本由加字段的开发者一次性付在构建期,而不是每个用户在运行期反复付23。
事务的账:SQLite 给不在显式事务里的每条语句套隐式事务——批量导入时,每条 INSERT 都把预写日志刷盘一次,1000 条就是 1000 次顺序 fsync,典型 SSD 每条 0.1–1 ms;BEGIN/COMMIT 一对把 1000 次刷盘压成 1 次,一到两个数量级的加速;中途失败还能 ROLLBACK,数据库回到导入前——经验法则:碰多于一行的事,一律包显式事务24。
5. 笔记服务:零框架 HTTP + SQLite¶
设计宣言:单文件、无框架、无中间件,HTTP 连接和数据库之间没有抽象层;每个请求过同一个函数,每条查询是 prepared statement,每个响应是纯文本25。五个端点:/status 存活探针、/list 全部笔记、/search?q=(title/content 的 LIKE '%q%')、/insert?title=&content=、/delete?id=;其余 40426。
路由是路径字符串的 if 链,lookupParam() 手写的查询串解析(找 = 与 &,按名取值);两个刻意取舍:/insert 用 GET 是为了让浏览器和裸 curl 直接可测(生产应 POST + 读 body);/delete 用 POST 是为了避免最严重后果——浏览器预取或爬虫静默删库27。
单线程是刻意的:一个客户端被服务时,其余全在 OS 的 accept 队列里排队;没有共享可变状态要护,没有分配器要线程安全,也绕开了 SQLite 连接不可跨线程共享的约束(除非用 serialized 模式)28。要并发,书的路线是每线程一条自己的连接,或互斥保护的连接池——accept 循环、连接处理、SQLite 调用全部不变,只有循环体和连接管理要演进29。
SIGINT 收尾(第 7 章原子标志模式的复现):handler 里只有一句 g_running.store(false, .release)——POSIX 的 async-signal-safe 白名单几乎什么都不许,printf/malloc/exit 全禁,无锁无 I/O 的原子写是安全清单上的一员;accept 循环每轮 load(.acquire) 查旗,失败路径也查30。三个细节:handler 要 export(C 链接可见,否则 signal() 存不下这个函数指针)、callconv(.c)(操作系统按 C ABI 调它,调用约定不配是运行时栈损坏)、生产应换 sigaction()(signal() 的后续行为在部分平台是实现定义的)31。
6. 验证:curl 字节级 + Zig 客户端¶
curl 会话走通全部路由,顺带暴露四个真问题32:
- AUTOINCREMENT 的 ID 不回退——删光所有行,下一个 ID 照旧接着涨;写测试断言具体 ID 值时要记住「不保证从 1 开始」。
- URL 编码不对称:
curl --data-urlencode把空格编成+,Zig 客户端按 RFC 3986 编成%20,服务器没有解码层、原样入库原样回显——同一条「Learn Zig」,curl 存进去是Learn+Zig,Zig 客户端存进去是Learn%20Zig。技术上都合法,可见地不同。 - 空
content的 insert 被优雅拒绝:Error: title and content must not be empty。 /list对空表回「No notes found.」而非空响应——刻意的可用性选择。
Zig 客户端是 std.http.Client 的薄壳:子命令与服务端路由一一对应;urlEncode() 按 RFC 3986 非保留字符集放行、其余 %XX;std.http.Client.fetch() 一条调用完成建连、发请求、收响应;joinArgs() 让 insert Shopping milk eggs bread 免引号——title 取第一个词,content 拼剩余的33。
7. 作者的判断与证据¶
- 判断: 编译期合同比运行期动态更值——schema 漂移是生产最常见的 bug 源,执法者是编译器,成本付在构建期23。证据:机制完整可验(生成串与手写无二致);局限诚实:不支持外键/索引、schema 必须编译期全知。
- 判断: 显式事务一到两个数量级加速,且给批量操作原子性24。证据:机制(fsnc 每次)可核,但没给实测数字——「immediately measurable even on a loopback benchmark」是承诺不是数据。
- 判断: 单线程服务器是地基不是死路;并发化只需换循环体和连接管理29。证据:SQLite 三种线程模式的完整交代(serialized 是多数发行版默认,技术可共享连接)。
- 诚实的坑:
/insert用 GET 是教学简化,书自己点破生产该 POST27。
8. 边界与局限¶
- comptime 管道不支持外键、索引、约束;schema 无法来自配置或运行期内省22。
- 笔记服务单线程,生产不可用;没有连接池、没有优雅停机(只有 SIGINT 的雏形)、没有结构化错误体。
- 服务器无 URL 解码层,编码差异原样入库——curl 与自有客户端的编码不一致会被用户看见32。
- popSQL 的去重是逐条
SELECT 1预检 + 插入,非事务内批量,竞态下两个进程同插一个 id 仍可能撞主键(教学够用,生产要INSERT OR IGNORE或唯一约束兜底)。 - 事务加速一段只给了机制推演,没有前后对照的实测表。
9. 可带走的¶
- 校验分两层:结构(分隔符位置)与内容(数字+范围),只查一层就会静默产垃圾。
std.json两段式:parseFromSlice(Value)先成动态树,再parseFromValue(Record)映射成静态类型;映射失败进坏账,不炸管道。- SQLite 五步:open → prepare → bind → step → finalize;句柄判空、错误串
sqlite3_free、defer 全程配对。 - checkError 一家族:OK/DONE/ROW 都是成功,其余皆错。
- 占位符只对值,不对标识符;标识符必须应用层白名单。
std.mem.span吃非空指针;可空列先查column_type == SQLITE_NULL。- 结构体当唯一事实来源:
@typeInfo取字段、++编译期拼 SQL、@field按名取值、anytype编译期特化。 - 未覆盖的类型
@compileError——schema 改动在构建期炸,不在生产期炸。 - 多行写入包
BEGIN/COMMIT:1000 次 fsync 变 1 次,顺带原子性。 - 信号 handler 三件套:export + callconv(.c) + 只做原子写;生产用 sigaction。
- curl 先验字节级行为,再写客户端;AUTOINCREMENT 不回退、URL 编码不对称这两类坑,只有端到端才看得见。
10. 原文地图¶
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| NDJSON 质量分账 | Converting date and time formats in JSON records | text/124-fm-converting-date-and-time-formats-in-json-records.txt:15(搜「validates」) |
| 两层校验 | Converting date and time formats in JSON records | text/124-fm-converting-date-and-time-formats-in-json-records.txt:174(搜「structure and content」) |
| fail-fast 管道 | Converting date and time formats in JSON records | text/124-fm-converting-date-and-time-formats-in-json-records.txt:264(搜「fail-fast」) |
| Valid 16 / Invalid 9 | Converting date and time formats in JSON records | text/124-fm-converting-date-and-time-formats-in-json-records.txt:490(搜「Valid: 16」) |
| SQLite 嵌入式定位 | Converting date and time formats in JSON records | text/124-fm-converting-date-and-time-formats-in-json-records.txt:522(搜「embedded」) |
| Homebrew 双版本坑 | Connecting to SQLite3 | text/125-fm-connecting-to-sqlite3.txt:137(搜「HomeBrew」) |
| checkError 三码 | Connecting to SQLite3 | text/125-fm-connecting-to-sqlite3.txt:80(搜「SQLITE_OK」) |
| 占位符防注入 | Inserting data into SQLite3 | text/126-fm-inserting-data-into-sqlite3.txt:164(搜「SQL injection」) |
| execSql 与错误串释放 | Inserting data into SQLite3 | text/126-fm-inserting-data-into-sqlite3.txt:244(搜「pointer-to-a-pointer」) |
| span 的 null 陷阱 | Querying data | text/127-fm-querying-data.txt:159(搜「undefined behavior」) |
| 标识符不可参数化 | Deleting data | text/128-fm-deleting-data.txt:58(搜「cannot be bound」) |
| changes() 实测 | Deleting data | text/128-fm-deleting-data.txt:153(搜「Deleted 4」) |
| 三处独立的坏 | Deleting data | text/128-fm-deleting-data.txt:180(搜「three places」) |
| comptime 即 Zig | Advanced comptime | text/129-fm-advanced-comptime.txt:17(搜「just Zig」) |
| @typeInfo 反射 | Reflection: using @typeInfo to inspect struct fields | text/130-fm-reflection-using-typeinfo-to-inspect-struct-fiel.txt:5(搜「@typeInfo」) |
| anytype 编译期多态 | Using anytype for generic database mapping | text/131-fm-using-anytype-for-generic-database-mapping.txt:13(搜「distinct」) |
| @field 编出同名指令 | Using comptime to map SQL rows to Zig structs | text/132-fm-using-comptime-to-map-sql-rows-to-zig-structs.txt:51(搜「same instruction」) |
| sqlType 的 compileError | Implementing the utility | text/133-fm-implementing-the-utility.txt:251(搜「unsupported field type」) |
| 生成的 SQL 在只读段 | Implementing the utility | text/133-fm-implementing-the-utility.txt:111(搜「read-only data segment」) |
| 1000 次 fsync 的账 | Using the utility | text/134-fm-using-the-utility.txt:84(搜「1,000 sequential fsyncs」) |
| 编译器当执法者 | Using the utility | text/134-fm-using-the-utility.txt:180(搜「enforcer」) |
| 零框架宣言 | Using the utility | text/134-fm-using-the-utility.txt:196(搜「no framework」) |
| 五端点 | Specifying the functionality of the web service | text/135-fm-specifying-the-functionality-of-the-web-service.txt:39(搜「/status」) |
| 端口 0 拒绝 | Implementing the web service | text/136-fm-implementing-the-web-service.txt:112(搜「ephemeral」) |
| 单线程的取舍 | Implementing the web service | text/136-fm-implementing-the-web-service.txt:172(搜「strictly single-threaded」) |
| SQLite 三线程模式 | Implementing the web service | text/136-fm-implementing-the-web-service.txt:222(搜「single-thread mode」) |
| /insert 用 GET 的坦白 | Implementing the web service | text/136-fm-implementing-the-web-service.txt:406(搜「instructional simplification」) |
| signal handler 白名单 | Implementing the web service | text/136-fm-implementing-the-web-service.txt:767(搜「async-signal-safe」) |
| AUTOINCREMENT 不回退 | Testing the web service with curl(1) | text/137-fm-testing-the-web-service-with-curl-1.txt:105(搜「AUTOINCREMENT」) |
| + 与 %20 的不对称 | Testing the web service with curl(1) | text/137-fm-testing-the-web-service-with-curl-1.txt:131(搜「rather than」) |
| urlEncode 的 RFC 3986 | Testing the web service with curl(1) | text/137-fm-testing-the-web-service-with-curl-1.txt:329(搜「unreserved」) |
本章拆解依据原书第 11 章(含各小节)。每条出处可用 node scripts/book-verify.mjs systems-programming-with-zig 回核。
-
出处:「Using the utility」第 176 段(
text/134-fm-using-the-utility.txt:176,搜「provably consistent」)。 ↩ -
出处:「Converting date and time formats in JSON records」第 15 段(
text/124-fm-converting-date-and-time-formats-in-json-records.txt:15,搜「validates」)。 ↩ -
出处:「Converting date and time formats in JSON records」第 174 段(
text/124-fm-converting-date-and-time-formats-in-json-records.txt:174,搜「structure and content」)。 ↩ -
出处:「Converting date and time formats in JSON records」第 200 段(
text/124-fm-converting-date-and-time-formats-in-json-records.txt:200,搜「garbage output」)。 ↩ -
出处:「Converting date and time formats in JSON records」第 132 段(
text/124-fm-converting-date-and-time-formats-in-json-records.txt:132,搜「ownership」)。 ↩ -
出处:「Converting date and time formats in JSON records」第 264 段(
text/124-fm-converting-date-and-time-formats-in-json-records.txt:264,搜「fail-fast」)。 ↩ -
出处:「Converting date and time formats in JSON records」第 394 段(
text/124-fm-converting-date-and-time-formats-in-json-records.txt:394,搜「regardless of the number of lines」)。 ↩ -
出处:「Converting date and time formats in JSON records」第 490 段(
text/124-fm-converting-date-and-time-formats-in-json-records.txt:490,搜「Valid: 16」)。 ↩ -
出处:「Converting date and time formats in JSON records」第 522 段(
text/124-fm-converting-date-and-time-formats-in-json-records.txt:522,搜「embedded」)。 ↩ -
出处:「Connecting to SQLite3」第 137 段(
text/125-fm-connecting-to-sqlite3.txt:137,搜「HomeBrew」)。 ↩ -
出处:「Inserting data into SQLite3」第 244 段(
text/126-fm-inserting-data-into-sqlite3.txt:244,搜「pointer-to-a-pointer」)。 ↩ -
出处:「Connecting to SQLite3」第 80 段(
text/125-fm-connecting-to-sqlite3.txt:80,搜「SQLITE_OK」)。 ↩ -
出处:「Deleting data」第 58 段(
text/128-fm-deleting-data.txt:58,搜「cannot be bound」)。 ↩ -
出处:「Deleting data」第 153 段(
text/128-fm-deleting-data.txt:153,搜「Deleted 4」)。 ↩ -
出处:「Querying data」第 159 段(
text/127-fm-querying-data.txt:159,搜「undefined behavior」)。 ↩ -
出处:「Deleting data」第 180 段(
text/128-fm-deleting-data.txt:180,搜「three places」)。 ↩ -
出处:「Deleting data」第 184 段(
text/128-fm-deleting-data.txt:184,搜「single point of truth」)。 ↩ -
出处:「Implementing the utility」第 39 段(
text/133-fm-implementing-the-utility.txt:39,搜「derived automatically」)。 ↩ -
出处:「Reflection: using @typeInfo to inspect struct fields」第 5 段(
text/130-fm-reflection-using-typeinfo-to-inspect-struct-fiel.txt:5,搜「@typeInfo」)。 ↩ -
出处:「Using anytype for generic database mapping」第 13 段(
text/131-fm-using-anytype-for-generic-database-mapping.txt:13,搜「distinct」)。 ↩ -
出处:「Reflection: using @typeInfo to inspect struct fields」第 49 段(
text/130-fm-reflection-using-typeinfo-to-inspect-struct-fiel.txt:49,搜「annotated」)。 ↩ -
出处:「Using the utility」第 154 段(
text/134-fm-using-the-utility.txt:154,搜「fully known」)。 ↩↩ -
出处:「Using the utility」第 180 段(
text/134-fm-using-the-utility.txt:180,搜「enforcer」)。 ↩↩ -
出处:「Using the utility」第 84 段(
text/134-fm-using-the-utility.txt:84,搜「1,000 sequential fsyncs」)与第 114 段(text/134-fm-using-the-utility.txt:114,搜「more than one row」)。 ↩↩ -
出处:「Using the utility」第 196 段(
text/134-fm-using-the-utility.txt:196,搜「no framework」)。 ↩ -
出处:「Specifying the functionality of the web service」第 39 段(
text/135-fm-specifying-the-functionality-of-the-web-service.txt:39,搜「/status」)。 ↩ -
出处:「Implementing the web service」第 406 段(
text/136-fm-implementing-the-web-service.txt:406,搜「instructional simplification」)。 ↩↩ -
出处:「Implementing the web service」第 172 段(
text/136-fm-implementing-the-web-service.txt:172,搜「strictly single-threaded」)与第 180 段(text/136-fm-implementing-the-web-service.txt:180,搜「sidesteps」)。 ↩ -
出处:「Implementing the web service」第 214 段(
text/136-fm-implementing-the-web-service.txt:214,搜「foundation」)。 ↩↩ -
出处:「Implementing the web service」第 767 段(
text/136-fm-implementing-the-web-service.txt:767,搜「async-signal-safe」)与第 797 段(text/136-fm-implementing-the-web-service.txt:797,搜「acquire」)。 ↩ -
出处:「Implementing the web service」第 727 段(
text/136-fm-implementing-the-web-service.txt:727,搜「export」)、第 741 段(text/136-fm-implementing-the-web-service.txt:741,搜「stack corruption」)与第 855 段(text/136-fm-implementing-the-web-service.txt:855,搜「SIG_DFL」)。 ↩ -
出处:「Testing the web service with curl(1)」第 105 段(
text/137-fm-testing-the-web-service-with-curl-1.txt:105,搜「AUTOINCREMENT」)与第 131 段(text/137-fm-testing-the-web-service-with-curl-1.txt:131,搜「rather than」)。 ↩↩ -
出处:「Testing the web service with curl(1)」第 333 段(
text/137-fm-testing-the-web-service-with-curl-1.txt:333,搜「fetch」)与第 367 段(text/137-fm-testing-the-web-service-with-curl-1.txt:367,搜「without quoting」)。 ↩