Repository navigation
cli/driver-sql: os migrate plan 自称 dry-run,却仍会在全新项目上创建空数据库文件(#6469 的残余写副作用) #6743
Description
Activity
os-project-manager commented
on Aug 9, 2026 CollaboratorAuthorMore actionsSeat grade (
domain:cli):finding→pm:queue. Queued — but not on route A, and I do not think this needs a maintainer ruling.Why the "维护者裁" framing dissolves
The filer defers to the maintainer because A "牵涉
@objectstack/driver-sql的公开 open 行为". Adding afileMustExist/ read-only option to an open path is additive — no existing caller changes behaviour, so there is no acceptance-surface decision to rule on. What actually deserved a second look is something the three routes all quietly assume, and I think it is wrong:Route A as written trades a true report for a refusal. On a fresh project, today's output — "every table needs creating" — is not a false report. It is an accurate statement of what a migration would do against an empty database. A's replacement ("这个项目还没有数据库,
plan无从 diff") removes information at precisely the moment a user most wants it: the firstplanof a new project. That is a UX regression paying for a hygiene fix.The route to take
Open read-only, and when the file does not exist, diff against an empty schema in memory. Same report as today, byte for byte, minus the file. The defect is the write side effect alone; the output was never the problem, and no route should pay for the fix with it.
That dissolves the fork: it is A's open-mode change (one semantic, on the driver's capability boundary, where the filer correctly wanted it) without A's output change. B's two-places-drift objection does not apply — the
existsSyncjudgement stays inside the driver. C stays rejected for the reason the filer gives: build-then-delete leaves debris on interrupt and hides the root cause.⚠️ Feasibility is unverified and the dev owes a check before writing code: this only works if the schema-diff path can run against an absent/empty target without an open write handle. If it cannot — ifbootSchemaStackneeds a real handle to produce a plan at all — then fall back to A and say so explicitly in the PR body, including what you measured that ruled the better route out. Do not silently land A and let this comment stand as if it had been achieved.Anchors for whoever takes it
- Reproduction is in the issue body and is cheap (fresh project,
os build,ls,os migrate plan,ls). Run it before the fix and quote bothlsoutputs; that is the red. - The pin worth leaving behind is "a fresh project after
os migrate planhas no.objectstack/data/" — including-wal/-shm, which the issue correctly flags and which a naive assertion on the.dbpath alone will miss. - Related: [17.0.0-rc.0] os migrate apply: no occupancy/lock detection for SQLite, and boot-time DDL runs before the confirmation prompt #3917 (defer DDL) established the defer path this extends; fix(cli,runtime): os dev / os start / os migrate 解析出三个不同的默认数据库 —— migrate plan 因此在自己刚建的空库上报告全量 drift #6469 removed the amplifier that made this dangerous rather than merely untidy.
Priority stays unlabelled: a stray empty file in a fresh project is real but nobody's outage.
Generated by Claude Code
- Reproduction is in the issue body and is cheap (fresh project,
os-project-manager commented
on Aug 9, 2026 CollaboratorAuthorMore actionsClaim: PM loop round 16 (
domain:cliseat)
Session:session_01USNUyHEr7uaU6MoEWXitei
Branch:claude/issue-6743-migrate-plan-readonly-open
Worktree:objectstack-issue-6743
Domain:domain:cli
File surface:packages/drivers/driver-sql/**(open mode),packages/cli/src/commands/migrate*, tests in both
Serial constraints cleared: sibling dispatch #6728 this round ispackages/cli/src/commands/login.ts+utils/format.ts— file-disjoint insidepackages/cli, no shared barrel. No in-flight claim onpackages/drivers/driver-sql.
Container verdict: M —mode:subagent(shared container); verification radius is driver-sql + cli.Dispatching on the seat grade above: read-only open, diff against an empty schema in memory — same report as today, minus the file. The feasibility check is owed before code, and falling back to route A requires saying so in the PR body with the measurement that ruled the better route out.
Generated by Claude Code
os-project-manager commented
on Aug 9, 2026 CollaboratorAuthorMore actionsDev delivered → PR #6997. Handing review and landing to the
domain:cliseat (#6024) — this seat does NOT accept, flip to ready, or queue it.Context: dispatched out of lane by the
domain:servicesseat (#6021). Maintainer's call, 2026-08-09: runs finish, cli seat owns landing. Disclosure on #6024.The stop condition was honoured in the direction that matters
The ruling required the feasibility check before code, and route A only as a documented fallback. The check ran first and came back positive: the schema-diff path runs against an in-memory target and produced a plan body byte-for-byte identical to the one against a real empty file. So route A was not needed and was not used — the "silently land A and let the ruling read as achieved" failure mode this card was written to prevent did not occur.
Ruled end state met: same plan, no file. Reproduced first on
origin/main(ls→No such file or directory, then a 4096-byteobjectstack.dbafteros migrate plan), and after the fix bothlsoutputs are absent-directory.Implementation shape
Additive
SqlDriverConfig.sqliteAbsentFile('create' | 'empty-in-memory'), withresolveSqliteAbsentFileTarget()in driver-sql as the oneexistsSyncjudgement, inside the driver — which is where your grade put it, and it is what kills route B's two-places-drift objection. Threaded as a host-composition option, set only byos migrate plan.this.configkeeps the declared path while only the Knex instance holds:memory:, so theDatabase:line still names the real path;sqliteFilename()returning null suppresses both the mkdir and the WAL PRAGMA.⚠️ One measured deviation, and it is the interesting partThe dev did not flip the existing-file open to
readonly: true, and measured why on better-sqlite3: opening an existing WAL database read-only creates-shm/-waland cannot remove them on close (dir after a read-only close:['t.db','t.db-shm','t.db-wal']), whereas today's read-write open removes both. A blanket read-only open would therefore add two stray files on the existing-file path — the very defect class this card removes — while fixing nothing there.That is a falsification of the obvious reading of "open read-only", backed by measurement. Worth your eye at review, since it narrows the change from what the phrase suggests.
Pins and evidence
The pin asserts no
.objectstack/data/at all — including-wal/-shmplus a sweep for anything else indata/(the trap the card flagged, which an assertion on the.dbpath alone would miss) — and that the pending-work set anddbLabelmatch a control boot that does create the file. That second half is the output-unchanged guarantee, which is the half of the ruling easiest to lose while fixing the file half.Reverse verification, direction predicted first: neutering the resolver turned red exactly the new-behaviour pins (
expected [ Array(1) ] to deeply equal []— the strayobjectstack.dbreappearing) while every unchanged-behaviour test stayed green, including "apply keeps its file". Suites: driver-sql 1140 passed, service-datasource 267, runtime 1744, cli 1047; 46 gates enumerated one-by-one fromlint.yml, all pass. CI on #6997: 25/25 complete, ESLint + TypeScript Type Checksuccess, zero failures.Out-of-scope finding filed
#7000 (
finding, nopm:queue, unassigned) —os migrate planstill creates.objectstack/metadata/on a fresh project: the residual, different-subsystem half of the same dry-run write-side-effect class. Correctly left out, since this card's ruling and pin are scoped to.objectstack/data/.
Generated by Claude Code
- added a commit that references this issue
on Aug 17, 2026 - added a commit that references this issue
on Sep 28, 2026
发现于 #6469 的实施(PR 见下方链接)。#6469 的裁定面是「三条 fallback 收敛到一个共享解析函数」,本条不在其中 —— 解析修好后 #6469 报告的危害已经消失,剩下的是一个独立的 driver open-mode 问题,故另记、未认领。
查重:按
migrate plan/ dry-run / 空库 /fileMustExist/ sqlite open mode 在本仓 open issues 与 open PR 中检索,0 命中(#6469 自身除外)。现象
os migrate plan自称 dry-run,#3917 之后连 boot 时的建表 DDL 与 artifact seed 都改成了「报告而不执行」。但它仍然会在磁盘上创建数据库文件本身 —— sqlite driver 以默认的「不存在就建」模式打开目标路径。在 #6469 修复之前,这条是那个 bug 的放大器:
plan解析到一个谁都没在用的standalone.db,把它建出来,然后在这个空库上报告全量 drift;第二次跑,磁盘上已经真有这么个文件,假报告看起来更像既定事实。#6469 落地后,
plan指向的是服务真正在用的库,所以原报告的那条危害路径已经死了。剩下的残余是:在一个全新的、还没跑过任何东西的项目里跑os migrate plan,会留下一个 0 表的.objectstack/data/objectstack.db(以及可能的-wal/-shm)。复现(全新项目,从未 start/dev 过):
为什么仍值得记一笔
危害等级比 #6469 低得多(不再有假报告,只是一个空文件),但性质没变:一个声明为 dry-run 的命令留下了写副作用。它同时让「这个项目还没有数据库」这个状态变得不可区分 —— 下一个命令看到文件存在,就不会再走首次初始化的判断。
修复方向(未裁)
核心问题是 sqlite driver 的 open 模式在「只读探测」与「正常 boot」之间没有区分:
bootSchemaStack({ deferSchemaDdl: true })这条路径一个 read-only /fileMustExist语义,目标文件不存在时不建,而是明确报告「这个项目还没有数据库,plan无从 diff」([17.0.0-rc.0] os migrate apply: no occupancy/lock detection for SQLite, and boot-time DDL runs before the confirmation prompt #3917 已经确立了 defer 这条路径,这里是把 defer 从 DDL 延伸到 open 本身);existsSync探一次,不存在就短路成一条说明性输出,不 boot stack。代价是这个判断会与 driver 真正的 open 语义分成两处,容易漂移;倾向 A(修在 driver 的能力边界上,一处语义),但这牵涉
@objectstack/driver-sql的公开 open 行为,应由维护者裁。相关:#3917(defer DDL / 占用守卫)、#6469(默认库解析统一)。