fix(driver-memory): persistence opt-in again + move the remaining suites to in-memory SQLite (#4065) - #4100
Merged
Merged
Conversation
…yDriver is pure in-memory (#4065) `InMemoryDriverConfig.persistence` defaulted to `'auto'`, and on Node.js `'auto'` resolves to FILE persistence. So `new InMemoryDriver()` — the shape every caller in this repo uses — silently wrote `.objectstack/data/memory-driver.json` into the process CWD and reloaded it on the next boot. The default is now `false`. This restores the design #815 accepted rather than replacing it: its requirement #1 reads "默认情况下不启用持久化(纯内存,行为不变)", and its own config examples list `new InMemoryDriver()` under "纯内存". `'auto'` was drift. Symptom: `datasource-autoconnect.test.ts` seeds two rows with fixed ids and asserts the exact set. Run 1 passed and wrote them to disk; run 2 loaded them back, appended two more and failed with four; run N had 2N. CI never saw it — every job is a fresh clone, so every CI run is run 1 — but `pnpm test` twice in one working tree could only ever go green once. What let the drift survive is not "there was no test". `MemoryConfigSchema` pinned the default and asserted `'auto'`; the driver honoured `'auto'`; the pair agreed and looked verified. Nothing checked that the agreed value was #815's. The driver's own persistence tests could not have caught it either — every case passed `persistence` explicitly, leaving the omitted-value path untested. Both sides are now covered: three behavioural tests (no CWD write, no cross-instance row carry-over, opt-in still persists) plus the flipped schema assertion. Verified they fail against the old default before landing the fix. Hosts that genuinely want durability now say so instead of inheriting it: - `resolveSqliteDriver`'s last-resort rung mirrors the sqlite target it stands in for — `persistence: 'file'` for a file database, `false` for `:memory:` — so a dev whose native and wasm SQLite both failed keeps the durability they would have had. - `createDefaultDatasourceDriverFactory`'s `memory` branch is ephemeral unless the datasource DEFINITION declares `config.persistence` (Prime Directive #12: a contract-level knob, not a silent host default). - `DevPlugin`'s driver is explicitly `persistence: false`, matching the cache, queue, job, i18n, storage and search stubs it ships beside — it was the one piece of that stack that quietly outlived the process. Also stops this suite writing into its own package dir: the shorthand `'file'`/`'auto'` cases take the adapter's default RELATIVE path, so they now run inside a scratch CWD. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L5q3BZMpdiAueHf2emhDY9
…ry SQLite (#4065) Ten test files used `InMemoryDriver` as a convenience backing store — somewhere for rows to go while the suite proved something else (REST routing, datasource auto-connect, the batch `$ref` contract, metadata history). They now run on `SqliteWasmDriver` at `:memory:`, the engine `@objectstack/verify`'s `bootStack` already gives the dogfood gate: pure JS (no native build, CI-safe) and real SQL. The point is fidelity, not tidiness. Production runs SQL, and mingo differs from it in ways that let a suite pass while the behaviour it stands for is broken. Every failure this migration produced was a fixture defect the memory driver had been absorbing: - Tables were never created. `create()` on the memory driver is a bare `table.push()` onto an auto-vivified array, so an object registered AFTER `kernel.bootstrap()` — which misses the boot-time schema sync — looked fine. On SQL the first write fails with `no such table`, which the REST error mapper turns into a 404 OBJECT_NOT_FOUND: a routing-shaped symptom for a DDL-shaped cause. Four suites needed an explicit `syncObjectSchema`; the `schemaMode: 'external'` datasource in datasource-autoconnect needed an `initObjects` standing in for the DDL that already ran on the remote side — the step a real external datasource genuinely requires. - A missing object declaration read as working. notifications.hono.integration writes `sys_notification`, which MessagingServicePlugin does not declare (it is a platform object, and that lean kernel never boots platform-objects). Auto-vivification hid it. The suite now registers the REAL SysNotification rather than a hand-copied stand-in, keeping one schema (Prime Directive #12). - `connect()` was optional. The memory driver needs none; a SQL driver does. Deliberately NOT moved: read-coercion-conformance keeps its two-driver matrix (proving a stored value reads back as its declared type on BOTH engines is the point of that gate), and the suites whose subject IS the memory driver or its wiring — standalone-stack (`memory://`), sqlite-driver-fallback (the dev step-down), the CLI driver-label tests, and driver-memory's own suite. No new coverage is claimed: each suite asserts exactly what it did before, against a more faithful store. 2,375 tests green across the eight affected packages. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L5q3BZMpdiAueHf2emhDY9
…#4065) `os init` and the `create-objectstack` blank template both listed `@objectstack/driver-memory` in the generated `dependencies`. It was the only driver named, which read as an endorsement — "this is the driver your app runs on" — when it is the LAST-RESORT rung of the dev step-down (native better-sqlite3 → WASM SQLite → mingo). A new project's first impression of the data layer should not be the engine that enforces no primary keys, no uniqueness, no NOT NULL and no column types. It was also redundant: `@objectstack/runtime` already depends on driver-sql, driver-sqlite-wasm and driver-memory, and every script in both scaffolds runs through the CLI, which carries all four. Removing the line changes nothing a generated project can do — `objectstack dev` still resolves SQLite by default and OS_DATABASE_URL still selects Postgres / MySQL / MongoDB. Docs updated to match: - *Your first project*'s package table dropped its driver row and now says where drivers actually come from. - *Database Drivers* § Memory Driver claimed "Data is lost when the process exits" — false while `'auto'` was the default, which wrote a file into the working directory. It now documents the opt-in persistence default, carries a migration callout for callers who relied on the old behaviour, and points test authors at in-memory SQLite with the reason (mingo enforces no constraints, so a green run against it is weaker evidence than it looks). The Vercel deployment snippet is left as-is: it builds an InMemoryDriver deliberately to stay self-contained and already warns that serverless loses all data on cold start — still accurate, and now true by construction rather than by the serverless detection in `'auto'`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L5q3BZMpdiAueHf2emhDY9
…does not enforce (#4065) The class docstring opened with "A production-ready implementation of the ObjectStack Driver Protocol". The driver stores no constraints of any kind: `create()` is a `table.push()` and `syncSchema()` only allocates an array and indexes temporal fields, so there is no primary key, no uniqueness, no NOT NULL, no foreign key and no column typing. `bulkCreate` lands two rows with the same id where a SQL driver raises a constraint violation, and a read returns both — the second finding in #4065, and the mechanism behind the 2N row growth there. Per Prime Directive #10 the options for `declared ≠ enforced` are implement it, trim the claim, or file it. With this driver moving to maintenance-only — kept for the last-resort rung of the dev step-down, browser/edge runtimes with no SQLite build, and the read-coercion parity gate — the claim is what goes. The docstring now states the missing constraints plainly, names the driver as a WEAK oracle for tests, and points at in-memory SQLite instead. No behaviour change; 197 driver-memory tests unchanged and green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L5q3BZMpdiAueHf2emhDY9
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 10 package(s): 125 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
…ling tests into dist (#4065) Two defects behind the last stray `.objectstack/` directory — the one under `packages/cli/`. Neither turned out to be cosmetic. 1. `projectRoot` only got half the stack. `createStandaloneStack`'s `projectRoot` is documented as scoping a boot's on-disk state to the project folder, and it did redirect the default sqlite database — but it was never passed to MetadataPlugin, whose FileSystemRepository kept rooting at `process.cwd()`. One "project root" therefore meant two directories: a boot pointed at project A wrote `A/.objectstack/data/` and `<cwd>/.objectstack/metadata/`. It now forwards `rootDir`, and `bootSchemaStack` takes a `projectRoot` to pass down (default `process.cwd()` — right for every real `os migrate`, which runs from the project dir). The two migrate integration suites now scope their boots to the tempdir fixture they already build. 2. The CLI compiled its own tests into dist — and vitest ran them. `tsconfig.build.json` included all of `src` with no exclude, so every `src/**/*.test.ts` was emitted as `dist/**/*.test.js`. `files: ["dist"]` published them, and — this package has no vitest config — `vitest run` collected the compiled copies alongside the sources: 81 test files / 849 tests where the sources hold 58 / 581. That silently defeats edits. A fix to a source test appeared not to work because the run was still executing the pre-fix compiled duplicate; that is how this residue survived a correct fix long enough to look like a different bug. It also lets a source test be edited to pass while its stale twin keeps asserting the old behaviour, with neither obviously wrong. Tests are now excluded from the build. No other package is affected — the rest build with tsup, which emits only declared entry points. Verified by scanning every packages/*/dist for `*.test.js`; the CLI was the only hit. Verified: full forced suite INCLUDING dogfood green (132/132 tasks), CLI suite run twice back-to-back leaves no `.objectstack` behind, lint clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L5q3BZMpdiAueHf2emhDY9
main landed #4083 (#4111 + #4108) on the same hazard while this branch was open. That work is kept; conflicts resolved in its favour wherever it overlapped: - `default-datasource-driver-factory.ts` — took main's wholesale. Its `buildMemoryConfig`/`scopeMemoryPersistence` expand a REQUESTED persistence mode into a per-datasource destination, which mine did not do; mine is dropped. - `sqlite-driver-fallback.ts` — took main's unconditional `persistence: false`. Mine mirrored the sqlite target it stands in for (file → `'file'`), which would have reintroduced exactly the process-global shared-file aliasing #4083 fixed. - `datasource-autoconnect.test.ts` — took main's wholesale, reverting this branch's migration of that file to wasm SQLite. Main added a regression test there for the memory pool leaving nothing behind; on a SQLite pool it would pass vacuously, since such a pool never writes `.objectstack/` at all. The file stays on the memory driver and keeps guarding what it was written to guard. What survives from this branch is the half #4083 deliberately left open: the DRIVER's own default. #4111 fixed the factory path and #4108 documented that a directly-constructed `new InMemoryDriver()` keeps `'auto'` and still writes `.objectstack/data/memory-driver.json` relative to the CWD. #815's requirement #1 ("默认情况下不启用持久化(纯内存,行为不变)") says that default was never the accepted design, so it goes to `false` here. The two compose: #4083's scoping remains the only thing that stops two OPTED-IN pools aliasing one file, and its explicit `false` becomes belt-and-braces. Comments asserting `'auto'` is the current default are corrected to past tense in the three places main wrote them, so the tree does not contradict itself, and the `drivers.mdx` build-path table and both changesets are reconciled the same way. Verified after the merge: full forced suite INCLUDING dogfood green (132/132), lint clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L5q3BZMpdiAueHf2emhDY9
os-zhuang
marked this pull request as ready for review
July 30, 2026 12:53
os-zhuang
added a commit
that referenced
this pull request
Jul 30, 2026
…-dist guard (#4156) Three CI changes, all lessons #4065 taught the hard way. CI configuration only. 1. Nightly rerun-safety gate. Every job in this repo runs on a fresh clone, which makes CI structurally incapable of seeing a suite that pollutes its own working tree and therefore passes exactly once — CI always runs pass #1, so it is always green. #4065 sat in the repo through every CI run it ever had and surfaced only because somebody ran the full suite twice in one checkout while doing unrelated work, where it looked like THEIR change had broken something. The new job runs the full suite twice in one tree with `--force` (turbo would otherwise replay the cache and report green without executing anything) and fails if pass 2 disagrees with pass 1. 2. `timeout-minutes` on all eight ci.yml jobs. There were none, so every job inherited GitHub's 6-hour default — and a job stuck that way reads as "still running" rather than broken, the worst failure mode a gate can have. 3. A build-output guard against compiled test files. A package built with plain `tsc` that does not exclude tests emits `dist/**/*.test.js`: `files: ["dist"]` publishes them, and a package with no vitest config COLLECTS them alongside its sources, so every `src/**/*.test.ts` also runs as a stale duplicate frozen at the last build. That silently defeats edits. @objectstack/cli shipped exactly that (81 test files / 849 tests where its sources hold 58 / 581) until #4065. Everything else builds with tsup, so this gate exists to stop the NEXT tsc-built package repeating it. Follow-up to #4100. Related: #4154.
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.
Closes #4065.
保留
@objectstack/driver-memory(不删包),但把它退回文档里早已写明的角色,并停止在测试与脚手架里依赖它。关键发现:
'auto'默认值是实现偏离,不是产品决策#4065 判断「改默认是破坏性的(#815 的 dev/UED 诉求依赖它)」。核对 #815 的验收需求后,这句不成立:
#815 明确要求持久化 opt-in,并把
new InMemoryDriver()标注为纯内存。今天的'auto'(Node.js 下 = 落盘)是从这份已验收设计上漂移出来的。漂移能存活的原因不是「没有测试」:
MemoryConfigSchema确实钉住了默认值,断言的是'auto';driver 也实现'auto';两边一致,看起来已验证。没人核对的是「一致的那个值是不是 #815 定的那个」。driver 自己的persistence.test.ts也抓不到——每个用例都显式传persistence,省略路径从未被覆盖。改了什么
P0 — 默认值(
82b161f,经合并后收敛)InMemoryDriverConfig.persistence与MemoryConfigSchema默认值'auto'→falseDevPlugin显式persistence: false,与它并排的 cache / queue / job / i18n / storage / search stub 一致P1 — 测试迁移(
a7e932f)9 个测试文件从 mingo 换到
SqliteWasmDriver({ filename: ':memory:' }),与bootStack一致(纯 JS 无原生编译,CI 安全;真 SQL 语义)。迁移暴露的每一处失败都是 memory driver 一直在吸收的 fixture 缺陷:OBJECT_NOT_FOUNDno such table,被 REST 错误映射成 404 —— 路由形状的症状,DDL 形状的病因sys_notification写入失败MessagingServicePlugin写它但不声明它(那是 platform object),精简 kernel 没加载 platform-objects。mingo 首次触碰自动建表,把这个遗漏完全盖住connect(),SQL 需要刻意不动的:
read-coercion-conformance保留双 driver 矩阵;以及主体就是 memory driver 或其接线的套件(standalone-stack的memory://、sqlite-driver-fallback、CLI driver 标签测试、driver-memory 自身套件),外加合并后新增的datasource-autoconnect。P2 — 脚手架与文档(
988910a)os init与create-objectstackblank 模板不再列@objectstack/driver-memory(冗余:@objectstack/runtime已依赖三个 driver,CLI 四个全带;且唯一点名 memory 读起来像背书,而它是 dev 步降的末位)drivers.mdx的「两条构建路径」表格保留 docs(drivers,spec): say which memory-driver path persists, now that only one of them does (#4083) #4108 的结构,事实按新默认值更正(
e6c2adb) 类 docstring 的 "production-ready" 去掉。它不存储任何约束(create()就是table.push(),无主键/唯一/NOT NULL/外键/列类型),bulkCreate会落两行同id—— #4065 的第二个发现。按 Prime Directive #10,driver 转维护态就裁掉声明。(
e7d31c6)收尾 cwd 写副作用 —— 追这个残留挖出两个真问题:projectRoot只管了一半的栈。createStandaloneStack的projectRoot文档说是把 boot 的落盘状态限定在项目目录,它也确实重定向了默认 sqlite 库——但从未传给MetadataPlugin,其FileSystemRepository仍以process.cwd()为根。于是同一个「项目根」指向两个目录。现已转发rootDir,bootSchemaStack也接受projectRoot。CLI 把自己的测试编译进了
dist/,而 vitest 会去跑它们。tsconfig.build.json收录整个src且无 exclude,于是每个src/**/*.test.ts都产出dist/**/*.test.js:files: ["dist"]把它们发布了出去;本包没有 vitest 配置,vitest run会把编译副本和源码一起收集——81 个测试文件 / 849 个用例,而源码只有 58 / 581。这会静默地让改动失效:对源码测试的修复看起来「没生效」,因为跑的仍是修复前的编译副本。它同样允许源码测试被改成通过、而陈旧副本继续断言旧行为。现已在构建中排除测试文件。其他包不受影响(用 tsup,只产出声明的入口;已扫描确认只有 CLI 命中)。
验证
--force强制真实重跑):129/129 两次全绿。此前第二遍必红 —— 本地pnpm test只能跑绿一次:InMemoryDriver 默认把数据落盘,datasource-autoconnect 测试在第二次运行必然失败 #4065 的核心诉求已达成.objectstackpnpm run lint干净一处保留(by design,非残留)
showcase_external.db。这是 showcase 外部 datasource 声明按设计写的相对路径,fixture 播种有count === 0守卫、幂等、已 gitignore —— 与memory-driver.json每跑一次累加 2N 行不是同一类。写在examples/app-showcase/自己项目目录里是正确的;dogfood 目录下那份是因为 dogfood 从自己的包目录引导 showcase app。要消除后者只能改 example app 的声明契约、加测试专用 env 开关、或重构 dogfood 的 cwd —— 都比一个幂等的 gitignored 小文件代价大,故未动。