fix(driver-sqlite-wasm): persist RETURNING writes — stop() 返回即落盘,冷启动 e2e 解封 (#4518) - #4564
Merged
Merged
Conversation
… e2e (#4518) A file-backed sqlite-wasm database flushed its schema at boot and then recorded nothing else: every table on disk, every subsequent row only in the WASM heap. `bootStack({ databaseFile })` therefore could not cold-boot, which blocked #4470's third minimal form. Root cause is in the Knex dialect, not the harness. `_query` picked its execution branch from "does this statement return rows" and then set the dirty flag only on the other, row-less branch. `INSERT ... RETURNING *` returns rows — and that is the shape ObjectQL writes with — so it executed on the row-returning branch and never marked the database dirty. The `on-disconnect` flush is gated on the same flag, so both persist strategies dropped the write; `knex.raw('INSERT ...')` (no Knex `method`) was lost the same way. "Does this statement change the database?" is now one exported predicate, `statementMutatesDatabase(sql, method)`, classifying by method AND SQL text and applied at a single funnel after execution — independent of which branch ran it. Transaction control still routes to `noteTransactionControl` so flushes stay deferred until a transaction closes (#1494); mutating PRAGMA assignments now count as writes. `WasmSqliteConnection.markDirty()` loses its method argument: re-filtering there made the same decision in two places that could disagree, which is precisely how the branches diverged. Tests: a new driver-level suite pins every execution branch (all six fail when the fix is reverted), and `flow-durable-suspend.dogfood.test.ts` loses its KNOWN GAP — it now suspends, shuts the kernel down, cold-boots a second kernel over the same file, resumes there, and proves the result survives a third boot, plus the plain-record assertion that identified this as a driver defect rather than a suspended-run one. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012C2cd7tL8QDoZ2QKN3djJ5
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 2 package(s): 7 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
os-zhuang
marked this pull request as ready for review
August 2, 2026 03:24
os-zhuang
enabled auto-merge
August 2, 2026 03:24
This was referenced Aug 2, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #4518
结论先说:不在 harness,在驱动
issue 给了三个候选方向(查
stop()链路 / harness 里显式flush()/ 换persist策略),实测下来一个都不是——三者都建立在"flush 该发生却没发生"这个前提上,而真实情况是驱动压根不认为自己脏了,所以再怎么 flush 都是空转。用探针把链路拆开看:
两条线索合在一起就指向答案了:
persist其实是on-write(不是 issue 猜的on-disconnect,default-datasource-driver-factory.ts对文件型 sqlite-wasm 就是这么配的),也就是说每次写都该 flush;但盘上只留下了 boot 早期的一次快照(所以文件大小两次完全一致 1073152)。既然on-write都不生效,stop()那条链路是否走全就无关紧要了——它最后调用的close()→flush()第一行就是if (!this.dirty) return。根因
packages/plugins/driver-sqlite-wasm/src/knex-wasm-dialect.ts的_query用**「这条语句返不返回行」挑执行分支,然后只在无行返回的那条分支上**调markDirty():INSERT … RETURNING *是返回行的,于是走上面那条分支。而 ObjectQL 的写路径就是用RETURNING *(它要把落库后的整行还给调用方),所以受影响的是几乎全部业务数据——这也是为什么它表现成"harness 冷启动不了"而不是"某个驱动 bug"。knex.raw('INSERT …')这类不带 Knexmethod的语句同样掉进这条分支,同样丢。on-disconnect也救不了它:最后那次 flush 用的是同一个dirty标志位。两种持久化策略一起漏。sys_user之所以还在,是因为 better-auth 的插入没走RETURNING;这也是当初这个 bug 能长期潜伏的原因之一——objectstack dev那种"跑到被杀"的用法里,身份/配置类数据看起来是持久的。修法:让"是否变更了数据库"只有一个答案
不在 harness 里补
flush()(那是把驱动的错误结论当成既定事实去绕开),而是把分类和执行彻底拆开:method和 SQL 文本,所以既不会因为"走了返回行的分支"漏掉,也不会因为"没带 method"漏掉;_query重构成两段:① EXECUTE(三种执行形态,都不做持久化决策)→ ② PERSIST(唯一一处决策)。这样"某条分支忘了说自己写过"在结构上不再可能;noteTransactionControl,继续把 flush 推迟到事务真正关闭(sql.js 的export()会 close+reopen,事务中导出会把事务冲掉 —— sqlite-wasm: authenticated record insert fails with "COMMIT; - cannot commit - no transaction is active" #1494);PRAGMA赋值(auto_vacuum、user_version)也算成写。同时
WasmSqliteConnection.markDirty(method?)改成markDirty()。它原本会拿调用方给的 method 再过一遍自己的白名单——"这条语句写了吗"于是有两个判断点,而这两个点恰好就是分歧发生的地方。现在方言分类、连接执行,一个决策一个归属。结果:
stop()返回 ⇒ 数据已落盘这条保证现在是结构性的,不需要调用方配合。
packages/verify里databaseFile的 TSDoc 补了这句话,顺带写明"冷启动看到表没有行 = 驱动 bug,不是 fixture 忘了等"。测试
flow-durable-suspend.dogfood.test.ts的 KNOWN GAP 注释删掉了,换成它当初就是为之而写的那个用例:第一条是刻意排在最前的:当初把这个问题识别成驱动缺陷、而不是挂起存储缺陷的关键事实,就是普通业务记录也活不过重启。留着它,以后回归时能直接告诉你坏在哪一层。第四条则堵住"resume 只在 WASM 堆里成功"这种假绿。
驱动层新增
sqlite-wasm-driver-returning-persist.test.ts,逐条钉住每种执行形态(INSERT … RETURNING、UPDATE … RETURNING、knex.raw、事务内提交、on-disconnect),外加对statementMutatesDatabase本身的分类断言。回滚验证(把两个源文件 stash 掉重新 build):
注:
on-disconnect那条用例一开始是"假绿"的——建表 DDL 本来就会标脏,而on-disconnect直到 close 才 flush,于是那次导出顺手把没标脏的行也带上了。加了一次显式flush()把标志位清掉,才复现出on-write在生产里的真实处境(每次 flush 都清标志,之后再没人置位)。这一条现在也随回滚失败。全量:
已 merge 最新
origin/main(a7163ea)并按 AGENTS.md §9 重装 + 全量 build 后复跑。影响面提醒
文件型 wasm SQLite 现在真的会按
on-write落盘,写入次数会明显多于从前——从前几乎为零。:memory:不受影响。🤖 Generated with Claude Code
Generated by Claude Code